diff --git a/nodescraper/plugins/inband/dimm/dimm_collector.py b/nodescraper/plugins/inband/dimm/dimm_collector.py index b6b91987..96b9db6f 100644 --- a/nodescraper/plugins/inband/dimm/dimm_collector.py +++ b/nodescraper/plugins/inband/dimm/dimm_collector.py @@ -23,27 +23,37 @@ # SOFTWARE. # ############################################################################### -import re -from typing import Optional +import csv +from typing import ClassVar, Optional + +from typing_extensions import override from nodescraper.base import InBandDataCollector -from nodescraper.connection.inband import TextFileArtifact +from nodescraper.connection.inband import CommandArtifact, TextFileArtifact from nodescraper.enums import EventCategory, EventPriority, ExecutionStatus, OSFamily from nodescraper.models import TaskResult from .collector_args import DimmCollectorArgs -from .dimmdata import DimmDataModel +from .dimmdata import DimmDataModel, DimmInfo class DimmCollector(InBandDataCollector[DimmDataModel, DimmCollectorArgs]): """Collect data on installed DIMMs""" - DATA_MODEL = DimmDataModel + DATA_MODEL: type[DimmDataModel] = DimmDataModel + + # Both platforms dump every field the firmware exposes and decode it here, + # rather than filtering on the SUT, so that a shell quirk on any given host + # cannot silently drop modules from the inventory. + CMD_WINDOWS: ClassVar[str] = "wmic memorychip get /format:csv" + CMD_DMIDECODE: ClassVar[str] = "dmidecode -q --type 17" + CMD_DMIDECODE_FULL: ClassVar[str] = "dmidecode" - CMD_WINDOWS = "wmic memorychip get Capacity" - CMD = """sh -c 'dmidecode -t 17 | tr -s " " | grep -v "Volatile\\|None\\|Module" | grep Size' 2>/dev/null""" - CMD_DMIDECODE_FULL = "dmidecode" + # Section title of a DMI type 17 record, which is all that identifies one + # under -q since that hides the handle lines. + MEMORY_DEVICE: ClassVar[str] = "Memory Device" + @override def collect_data( self, args: Optional[DimmCollectorArgs] = None, @@ -52,97 +62,21 @@ def collect_data( if args is None: args = DimmCollectorArgs() - dimm_str = None if self.system_info.os_family == OSFamily.WINDOWS: - res = self._run_sut_cmd(self.CMD_WINDOWS) - if res.exit_code == 0: - capacities = {} - total = 0 - for line in res.stdout.splitlines(): - value = line.strip() - if value.isdigit(): - value = int(value) - total += value - if value not in capacities: - capacities[value] = 1 - else: - capacities[value] += 1 - dimm_str = f"{total / 1024 / 1024:.2f}GB @ " - for capacity, count in capacities.items(): - dimm_str += f"{count} x {capacity / 1024 / 1024:.2f}GB " + records = self._collect_windows_records() else: if args.skip_sudo: self.result.message = "Skipping sudo plugin" self.result.status = ExecutionStatus.NOT_RAN return self.result, None - # Collect full dmidecode output as artifact - dmidecode_full_res = self._run_sut_cmd(self.CMD_DMIDECODE_FULL, sudo=True) - if dmidecode_full_res.exit_code == 0 and dmidecode_full_res.stdout: - self.result.artifacts.append( - TextFileArtifact(filename="dmidecode.txt", contents=dmidecode_full_res.stdout) - ) - else: - self._log_event( - category=EventCategory.OS, - description="Could not collect full dmidecode output", - data={ - "command": dmidecode_full_res.command, - "exit_code": dmidecode_full_res.exit_code, - "stderr": dmidecode_full_res.stderr, - }, - priority=EventPriority.WARNING, - ) - - res = self._run_sut_cmd(self.CMD, sudo=True) - if res.exit_code == 0: - total = 0 - topology = {} - size = "" - dimm_size_pattern = re.compile(r"Size:\s+(\d+)\s+([A-Za-z]+)") - matches = dimm_size_pattern.findall(res.stdout) - if matches: - for match in matches: - size = match[1] - total += int(match[0]) - key = match[0] + match[1] - if not topology.get(key, None): - topology[key] = 1 - else: - topology[key] += 1 - topology["total"] = total - topology["size"] = size - total_gb = topology.pop("total") - size = topology.pop("size") - if total_gb == 0: - dimm_str = "0 GB" - else: - dimm_entries = [f"{v} x {k}" for k, v in topology.items()] - dimm_str = f"{total_gb}{size} @ {' '.join(dimm_entries)}" - if res.exit_code != 0: - self._log_event( - category=EventCategory.OS, - description="Error checking dimms", - data={ - "command": res.command, - "exit_code": res.exit_code, - "stderr": res.stderr, - }, - priority=EventPriority.ERROR, - console_log=True, - ) + records = self._collect_dmidecode_records() - if dimm_str: - dimm_data = DimmDataModel(dimms=dimm_str) - self._log_event( - category=EventCategory.IO, - description="Installed DIMM check", - data=dimm_data.model_dump(), - priority=EventPriority.INFO, - ) - self.result.message = f"DIMM: {dimm_str}" - else: - dimm_data = None + # Records for empty slots, and any the firmware reports too incompletely + # to decode, come back as None and are dropped from the inventory. + dimms = [dimm for dimm in map(DimmInfo.from_record, records) if dimm] + + if not dimms: self._log_event( category=EventCategory.IO, description="DIMM info not found", @@ -150,5 +84,153 @@ def collect_data( ) self.result.message = "DIMM info not found" self.result.status = ExecutionStatus.ERROR + return self.result, None + + dimm_data = DimmDataModel(dimms=dimms) + self.result.message = f"DIMM: {dimm_data}" return self.result, dimm_data + + def _collect_dmidecode_records(self) -> list[dict[str, str]]: + """Dump the DMI tables and pull the memory device records out of them. + + A full dump is collected first and kept as an artifact, then the type 17 + records are read from a quiet, targeted dump. The full dump is used as a + fallback when the targeted one fails, since it carries the same records. + + Returns: + list[dict[str, str]]: one raw record per memory device slot. + """ + full_dump = self._run_dmidecode(self.CMD_DMIDECODE_FULL) + if full_dump: + self.result.artifacts.append( + TextFileArtifact(filename="dmidecode.txt", contents=full_dump) + ) + else: + self._log_event( + category=EventCategory.OS, + description="Could not collect full dmidecode output", + priority=EventPriority.WARNING, + ) + + dump = self._run_dmidecode(self.CMD_DMIDECODE) or full_dump + if not dump: + return [] + + return self._parse_dmidecode(dump) + + def _run_dmidecode(self, command: str) -> Optional[str]: + """Run a dmidecode command and return its output. + + Args: + command (str): dmidecode command to run. + + Returns: + Optional[str]: raw stdout, or None if the command produced none. + """ + res = self._run_sut_cmd(command, sudo=True) + + # Hosts that already run as root often ship no sudo binary at all, so a + # sudo specific failure is worth one retry without it. + if res.exit_code != 0 and "sudo" in str(res.stderr).lower(): + res = self._run_sut_cmd(command, sudo=False) + + if res.exit_code != 0 or not res.stdout: + self._log_cmd_error(res) + return None + + return res.stdout + + def _collect_windows_records(self) -> list[dict[str, str]]: + """Dump every Win32_PhysicalMemory property and split it into records. + + Returns: + list[dict[str, str]]: one raw record per memory device slot. + """ + res = self._run_sut_cmd(self.CMD_WINDOWS) + if res.exit_code != 0 or not res.stdout: + self._log_cmd_error(res) + return [] + + self.result.artifacts.append( + TextFileArtifact(filename="memorychip.csv", contents=res.stdout) + ) + + return self._parse_wmic_csv(res.stdout) + + def _log_cmd_error(self, res: CommandArtifact) -> None: + """Log a failed memory query without aborting the rest of the collection. + + Args: + res (CommandArtifact): result of the failed command. + """ + self._log_event( + category=EventCategory.OS, + description="Error checking dimms", + priority=EventPriority.ERROR, + console_log=True, + ) + + @classmethod + def _parse_dmidecode(cls, dump: str) -> list[dict[str, str]]: + """Split a raw dmidecode dump into its memory device records. + + Only DMI type 17 records are kept, and only their top level fields, so + that the "Volatile Size", "Cache Size" and "Logical Size" fields nested + under NVDIMM records are never mistaken for a module capacity. + + Args: + dump (str): raw dmidecode output. + + Returns: + list[dict[str, str]]: one raw record per memory device slot. + """ + records: list[dict[str, str]] = [] + fields: Optional[dict[str, str]] = None + + for line in dump.splitlines(): + field = line.strip() + if not field: + # A blank line closes the current record. + fields = None + elif field.startswith("Handle "): + # Without -q every record opens with a handle line naming its + # DMI type, which is the most precise way to spot type 17. + fields = None + if "DMI type 17," in field: + fields = {} + records.append(fields) + elif field == cls.MEMORY_DEVICE: + # Under -q the handle lines are hidden, so the section title is + # what opens the record. + if fields is None: + fields = {} + records.append(fields) + elif fields is not None: + key, separator, value = field.partition(":") + key = key.strip() + # Nested fields repeat names such as "Size", so the first value + # seen for a key wins. + if separator and key not in fields: + fields[key] = value.strip() + + return records + + @staticmethod + def _parse_wmic_csv(stdout: str) -> list[dict[str, str]]: + """Split `wmic memorychip get /format:csv` output into records. + + wmic emits a header row naming each property followed by one row per + module, so the header is used to key the fields and any property that a + given Windows build does not report is simply absent from the record. + + Args: + stdout (str): raw wmic output. + + Returns: + list[dict[str, str]]: one raw record per memory device slot. + """ + # wmic pads its output with blank lines that confuse the csv reader. + rows = [line for line in stdout.splitlines() if line.strip()] + + return list(csv.DictReader(rows)) diff --git a/nodescraper/plugins/inband/dimm/dimm_plugin.py b/nodescraper/plugins/inband/dimm/dimm_plugin.py index 30dcc86d..5a0f5672 100644 --- a/nodescraper/plugins/inband/dimm/dimm_plugin.py +++ b/nodescraper/plugins/inband/dimm/dimm_plugin.py @@ -23,7 +23,10 @@ # SOFTWARE. # ############################################################################### +from typing import Optional + from nodescraper.base import InBandDataPlugin +from nodescraper.interfaces.dataplugin import CollectorArgsClasses, CollectorClasses from .collector_args import DimmCollectorArgs from .dimm_collector import DimmCollector @@ -33,8 +36,8 @@ class DimmPlugin(InBandDataPlugin[DimmDataModel, DimmCollectorArgs, None]): """Plugin for collection and analysis of DIMM data""" - DATA_MODEL = DimmDataModel + DATA_MODEL: type[DimmDataModel] = DimmDataModel - COLLECTOR = DimmCollector + COLLECTOR: Optional[CollectorClasses] = DimmCollector - COLLECTOR_ARGS = DimmCollectorArgs + COLLECTOR_ARGS: Optional[CollectorArgsClasses] = DimmCollectorArgs diff --git a/nodescraper/plugins/inband/dimm/dimmdata.py b/nodescraper/plugins/inband/dimm/dimmdata.py index 3bc80a8c..c710b7d9 100644 --- a/nodescraper/plugins/inband/dimm/dimmdata.py +++ b/nodescraper/plugins/inband/dimm/dimmdata.py @@ -23,8 +23,392 @@ # SOFTWARE. # ############################################################################### +from typing import ClassVar, Optional + +from pydantic import ( + AliasChoices, + BaseModel, + ConfigDict, + Field, + ValidationError, + computed_field, + field_validator, +) +from typing_extensions import override + from nodescraper.models import DataModel +# Byte multiplier for every size unit that dmidecode may report for a module. +SIZE_UNITS = { + "B": 1, + "KB": 1024, + "MB": 1024**2, + "GB": 1024**3, + "TB": 1024**4, + "PB": 1024**5, +} + +# Units considered when rendering a byte count, largest first so that the +# shortest exact representation wins. +DISPLAY_UNITS = ("PB", "TB", "GB", "MB", "KB") + +# Field values that mean "nothing here" rather than a real value. +PLACEHOLDERS = frozenset( + { + "", + "unknown", + "not specified", + "not provided", + "none", + "no module installed", + "[empty]", + "to be filled by o.e.m.", + } +) + +# SMBIOS memory type codes (SMBIOS spec 7.18.2). dmidecode resolves these +# itself; on Windows they arrive as raw codes in SMBIOSMemoryType. +SMBIOS_MEMORY_TYPES = { + 1: "Other", + 2: "Unknown", + 3: "DRAM", + 15: "SDRAM", + 16: "SGRAM", + 17: "RDRAM", + 18: "DDR", + 19: "DDR2", + 20: "DDR2 FB-DIMM", + 24: "DDR3", + 25: "FBD2", + 26: "DDR4", + 27: "LPDDR", + 28: "LPDDR2", + 29: "LPDDR3", + 30: "LPDDR4", + 31: "Logical non-volatile device", + 32: "HBM", + 33: "HBM2", + 34: "DDR5", + 35: "LPDDR5", + 36: "HBM3", +} + +# Win32_PhysicalMemory FormFactor codes. WMI uses its own enumeration here, +# which does not match the SMBIOS form factor codes. +WMI_FORM_FACTORS = { + 0: "Unknown", + 1: "Other", + 2: "SIP", + 3: "DIP", + 4: "ZIP", + 5: "SOJ", + 6: "Proprietary", + 7: "SIMM", + 8: "DIMM", + 9: "TSOP", + 10: "PGA", + 11: "RIMM", + 12: "SODIMM", + 13: "SRIMM", + 14: "SMD", + 15: "SSMP", + 16: "QFP", + 17: "TQFP", + 18: "SOIC", + 19: "LCC", + 20: "PLCC", + 21: "BGA", + 22: "FPBGA", + 23: "LGA", +} + + +def format_size(size_bytes: int) -> str: + """Render a byte count using the largest unit that divides it evenly. + + Args: + size_bytes (int): size in bytes. + + Returns: + str: human readable size, e.g. "64GB". + """ + for unit in DISPLAY_UNITS: + factor = SIZE_UNITS[unit] + if size_bytes >= factor and not size_bytes % factor: + return f"{size_bytes // factor}{unit}" + return f"{size_bytes}B" + + +def clean(value: str) -> Optional[str]: + """Normalise a text field, mapping placeholder values to None.""" + value = value.strip() + return value if value.lower() not in PLACEHOLDERS else None + + +def parse_value(value: str) -> Optional[tuple[int, str]]: + """Split a numeric field into its number and its unit. + + Every numeric field either source reports is a count followed by an + optional unit, e.g. "64 GB", "4800 MT/s", "80 bits" or a bare "2". + + Args: + value (str): raw field. + + Returns: + Optional[tuple[int, str]]: the number and its unit, the unit being + empty when the field carries none. None when the field does not start + with a number at all, e.g. "No Module Installed" or "DDR5". + """ + number, _, unit = value.strip().partition(" ") + if not number.isdigit(): + return None + + return int(number), unit.strip() + + +def parse_size(value: str) -> Optional[int]: + """Convert a size field such as "64 GB" into bytes. + + Args: + value (str): raw size field. + + Returns: + Optional[int]: size in bytes, or None when the slot holds no usable + module, e.g. "No Module Installed", "Unknown" or a zero size. + """ + parsed = parse_value(value) + if not parsed: + return None + + size, unit = parsed + # A size with no unit is already a byte count, which is how + # Win32_PhysicalMemory reports capacity. + factor = SIZE_UNITS.get(unit.upper() or "B") + if not factor: + return None + + return size * factor or None + + +def decode(codes: dict[int, str], value: str) -> Optional[str]: + """Resolve an SMBIOS or WMI enum code to its name. + + Values that are already named, as dmidecode reports them, pass through + untouched. + + Args: + codes (dict[int, str]): enum table to resolve against. + value (str): raw code or name. + + Returns: + Optional[str]: the decoded name, the raw code if the table does not + cover it, or None if the value carries no meaning. + """ + parsed = parse_value(value) + # dmidecode names the value outright, Windows reports a bare enum code. + if not parsed or parsed[1]: + return clean(value) + + code = parsed[0] + return clean(codes.get(code, f"Type {code}")) + + +class DimmInfo(BaseModel): + """Details of a single populated memory slot. + + Every field is aliased to the names dmidecode and Win32_PhysicalMemory use + for it, so a raw record from either source validates straight into the + model and the units, enum codes and placeholder values are decoded here + rather than by the caller. + """ + + model_config: ClassVar[ConfigDict] = ConfigDict(populate_by_name=True) + + size_bytes: int = Field( + validation_alias=AliasChoices("size_bytes", "Size", "Capacity"), + description="Module capacity in bytes", + ) + locator: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("locator", "Locator", "DeviceLocator"), + description="Slot the module is installed in", + ) + bank_locator: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("bank_locator", "Bank Locator", "BankLabel"), + description="Memory bank the slot belongs to", + ) + manufacturer: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("manufacturer", "Manufacturer"), + ) + part_number: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("part_number", "Part Number", "PartNumber"), + ) + serial_number: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("serial_number", "Serial Number", "SerialNumber"), + ) + memory_type: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("memory_type", "Type", "SMBIOSMemoryType"), + description="Memory technology, e.g. DDR5", + ) + form_factor: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("form_factor", "Form Factor", "FormFactor"), + description="Physical form factor, e.g. DIMM", + ) + speed_mts: Optional[int] = Field( + default=None, + validation_alias=AliasChoices("speed_mts", "Speed"), + description="Rated speed in MT/s", + ) + configured_speed_mts: Optional[int] = Field( + default=None, + validation_alias=AliasChoices( + "configured_speed_mts", "Configured Memory Speed", "ConfiguredClockSpeed" + ), + description="Speed the module is running at in MT/s", + ) + rank: Optional[int] = Field( + default=None, + validation_alias=AliasChoices("rank", "Rank"), + ) + data_width_bits: Optional[int] = Field( + default=None, + validation_alias=AliasChoices("data_width_bits", "Data Width", "DataWidth"), + ) + total_width_bits: Optional[int] = Field( + default=None, + validation_alias=AliasChoices("total_width_bits", "Total Width", "TotalWidth"), + description="Data width plus any error correction width", + ) + + @field_validator("size_bytes", mode="before") + @classmethod + def size_conformer(cls, value: object) -> object: + """Convert a "64 GB" style size, or a raw byte count, into bytes.""" + return parse_size(value) if isinstance(value, str) else value + + @field_validator( + "locator", + "bank_locator", + "manufacturer", + "part_number", + "serial_number", + mode="before", + ) + @classmethod + def text_conformer(cls, value: object) -> object: + """Drop placeholder text such as "Unknown" or "Not Specified".""" + return clean(value) if isinstance(value, str) else value + + @field_validator( + "speed_mts", + "configured_speed_mts", + "rank", + "data_width_bits", + "total_width_bits", + mode="before", + ) + @classmethod + def int_conformer(cls, value: object) -> object: + """Take the number, dropping units such as the "MT/s" of a speed.""" + if not isinstance(value, str): + return value + + parsed = parse_value(value) + return parsed[0] if parsed else None + + @field_validator("memory_type", mode="before") + @classmethod + def memory_type_conformer(cls, value: object) -> object: + """Resolve the SMBIOSMemoryType codes that Windows reports.""" + return decode(SMBIOS_MEMORY_TYPES, value) if isinstance(value, str) else value + + @field_validator("form_factor", mode="before") + @classmethod + def form_factor_conformer(cls, value: object) -> object: + """Resolve the FormFactor codes that Windows reports.""" + return decode(WMI_FORM_FACTORS, value) if isinstance(value, str) else value + + @classmethod + def from_record(cls, record: dict[str, str]) -> Optional["DimmInfo"]: + """Build a module from a raw dmidecode or wmic record. + + Args: + record (dict[str, str]): raw field names mapped to their raw values. + + Returns: + Optional[DimmInfo]: the module, or None if the record describes an + empty slot or is too malformed to decode. + """ + try: + return cls.model_validate(record) + except ValidationError: + return None + + @computed_field # type: ignore[misc] + @property + def size(self) -> str: + """Module capacity as a human readable string, e.g. "64GB".""" + return format_size(self.size_bytes) + + @override + def __str__(self) -> str: + """Describe the module, e.g. "DIMM 0: 64GB DDR5 4800MT/s Micron".""" + details = [ + detail + for detail in ( + self.size, + self.memory_type, + f"{self.speed_mts}MT/s" if self.speed_mts else None, + self.manufacturer, + ) + if detail + ] + summary = " ".join(details) + return f"{self.locator}: {summary}" if self.locator else summary + class DimmDataModel(DataModel): - dimms: str + """Inventory of the memory modules installed in the system""" + + dimms: list[DimmInfo] = Field(default_factory=list) + + @computed_field # type: ignore[misc] + @property + def dimm_count(self) -> int: + """Number of populated memory slots.""" + return len(self.dimms) + + @computed_field # type: ignore[misc] + @property + def total_size_bytes(self) -> int: + """Combined capacity of every populated module, in bytes.""" + return sum(dimm.size_bytes for dimm in self.dimms) + + @computed_field # type: ignore[misc] + @property + def total_size(self) -> str: + """Combined capacity as a human readable string, e.g. "256GB".""" + return format_size(self.total_size_bytes) + + @computed_field # type: ignore[misc] + @property + def population(self) -> dict[str, int]: + """Module count keyed by capacity, smallest capacity first.""" + counts: dict[int, int] = {} + for dimm in self.dimms: + counts[dimm.size_bytes] = counts.get(dimm.size_bytes, 0) + 1 + return {format_size(size): count for size, count in sorted(counts.items())} + + @override + def __str__(self) -> str: + """Summarise the inventory, e.g. "256GB @ 2 x 64GB 1 x 128GB".""" + if not self.dimms: + return "0GB" + breakdown = " ".join(f"{count} x {size}" for size, count in self.population.items()) + return f"{self.total_size} @ {breakdown}" diff --git a/test/unit/plugin/fixtures/dmidecode_full.txt b/test/unit/plugin/fixtures/dmidecode_full.txt new file mode 100644 index 00000000..0fc8d89a --- /dev/null +++ b/test/unit/plugin/fixtures/dmidecode_full.txt @@ -0,0 +1,63 @@ +# dmidecode 3.3 +Getting SMBIOS data from sysfs. +SMBIOS 3.5.0 present. + +Handle 0x0036, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Size: 6 TB + Number Of Devices: 3 + +Handle 0x0041, DMI type 17, 92 bytes +Memory Device + Array Handle: 0x0036 + Total Width: 72 bits + Data Width: 64 bits + Size: 16384 MB + Form Factor: DIMM + Locator: DIMM_A1 + Bank Locator: NODE 0 + Type: DDR4 + Speed: 3200 MT/s + Manufacturer: Micron Technology + Serial Number: DEADBEEF + Part Number: 36ASF2G72PZ-3G2E1 + Rank: 2 + Configured Memory Speed: 2933 MT/s + Memory Operating Mode Capability: Volatile memory + Volatile Size: 8 GB + Cache Size: None + Logical Size: None + +Handle 0x0042, DMI type 17, 92 bytes +Memory Device + Array Handle: 0x0036 + Total Width: Unknown + Data Width: Unknown + Size: No Module Installed + Form Factor: Unknown + Locator: DIMM_A2 + Bank Locator: NODE 0 + Type: Unknown + Speed: Unknown + Manufacturer: Not Specified + Serial Number: Not Specified + Part Number: Not Specified + +Handle 0x0043, DMI type 17, 92 bytes +Memory Device + Array Handle: 0x0036 + Total Width: 80 bits + Data Width: 64 bits + Size: 64 GB + Form Factor: DIMM + Locator: DIMM_B1 + Bank Locator: NODE 1 + Type: DDR5 + Speed: 4800 MT/s + Manufacturer: To Be Filled By O.E.M. + Serial Number: 00000000 + Part Number: M393A8G40AB2-CWE + Rank: 2 + Configured Memory Speed: 4800 MT/s diff --git a/test/unit/plugin/fixtures/dmidecode_quiet.txt b/test/unit/plugin/fixtures/dmidecode_quiet.txt new file mode 100644 index 00000000..ea5ef41d --- /dev/null +++ b/test/unit/plugin/fixtures/dmidecode_quiet.txt @@ -0,0 +1,767 @@ +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_A + Bank Locator: _Node0_Channel0_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2432-50F82DCB + Asset Tag: CPU0_A_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_B + Bank Locator: _Node0_Channel1_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2432-50F83B85 + Asset Tag: CPU0_B_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_C + Bank Locator: _Node0_Channel2_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2432-50F83B92 + Asset Tag: CPU0_C_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_D + Bank Locator: _Node0_Channel3_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2432-50F83B8C + Asset Tag: CPU0_D_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_E + Bank Locator: _Node0_Channel4_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2432-50F833EE + Asset Tag: CPU0_E_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_F + Bank Locator: _Node0_Channel5_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5A2B + Asset Tag: CPU0_F_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_G + Bank Locator: _Node0_Channel6_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2432-50F833D3 + Asset Tag: CPU0_G_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_H + Bank Locator: _Node0_Channel7_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E59BD + Asset Tag: CPU0_H_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_I + Bank Locator: _Node0_Channel8_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5880 + Asset Tag: CPU0_I_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_J + Bank Locator: _Node0_Channel9_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5998 + Asset Tag: CPU0_J_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_K + Bank Locator: _Node0_Channel10_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5207 + Asset Tag: CPU0_K_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU0_L + Bank Locator: _Node0_Channel11_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5169 + Asset Tag: CPU0_L_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_A + Bank Locator: _Node1_Channel0_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E517A + Asset Tag: CPU1_A_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_B + Bank Locator: _Node1_Channel1_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5311 + Asset Tag: CPU1_B_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_C + Bank Locator: _Node1_Channel2_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5778 + Asset Tag: CPU1_C_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_D + Bank Locator: _Node1_Channel3_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5884 + Asset Tag: CPU1_D_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_E + Bank Locator: _Node1_Channel4_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5755 + Asset Tag: CPU1_E_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_F + Bank Locator: _Node1_Channel5_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E520A + Asset Tag: CPU1_F_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_G + Bank Locator: _Node1_Channel6_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5774 + Asset Tag: CPU1_G_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_H + Bank Locator: _Node1_Channel7_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E56E5 + Asset Tag: CPU1_H_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_I + Bank Locator: _Node1_Channel8_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5883 + Asset Tag: CPU1_I_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_J + Bank Locator: _Node1_Channel9_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E56E6 + Asset Tag: CPU1_J_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_K + Bank Locator: _Node1_Channel10_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E599C + Asset Tag: CPU1_K_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None + +Memory Device + Total Width: 80 bits + Data Width: 64 bits + Size: 96 GB + Form Factor: DIMM + Set: None + Locator: CPU1_L + Bank Locator: _Node1_Channel11_Dimm0 + Type: DDR5 + Type Detail: Synchronous Registered (Buffered) + Speed: 5600 MT/s + Manufacturer: Samsung + Serial Number: 2435-512E5D04 + Asset Tag: CPU1_L_AssetTag + Part Number: M321RYGA0PB0-CWMXJ + Rank: 2 + Configured Memory Speed: 5600 MT/s + Minimum Voltage: 1.1 V + Maximum Voltage: 1.1 V + Configured Voltage: 1.1 V + Memory Technology: DRAM + Memory Operating Mode Capability: Volatile memory + Firmware Version: 0000 + Module Manufacturer ID: Bank 1, Hex 0xCE + Module Product ID: Unknown + Memory Subsystem Controller Manufacturer ID: Unknown + Memory Subsystem Controller Product ID: Unknown + Non-Volatile Size: None + Volatile Size: 96 GB + Cache Size: None + Logical Size: None diff --git a/test/unit/plugin/fixtures/dmideode_wmic.txt b/test/unit/plugin/fixtures/dmideode_wmic.txt new file mode 100644 index 00000000..5cbfa899 --- /dev/null +++ b/test/unit/plugin/fixtures/dmideode_wmic.txt @@ -0,0 +1,6 @@ + +Node,Attributes,BankLabel,Capacity,Caption,ConfiguredClockSpeed,ConfiguredVoltage,CreationClassName,DataWidth,Description,DeviceLocator,FormFactor,HotSwappable,InstallDate,InterleaveDataDepth,InterleavePosition,Manufacturer,MaxVoltage,MemoryType,MinVoltage,Model,Name,OtherIdentifyingInfo,PartNumber,PositionInRow,PoweredOn,Removable,Replaceable,SerialNumber,SKU,SMBIOSMemoryType,Speed,Status,Tag,TotalWidth,TypeDetail,Version +MKMGRAEPAUL01,2,P0 CHANNEL A,8589934592,Physical Memory,6400,500,Win32_PhysicalMemory,32,Physical Memory,DIMM 0,1,,,,,Micron Technology,500,0,500,,Physical Memory,,MT62F2G32D8DR-031 WT,,,,,00000000,,35,6400,,Physical Memory 0,32,16512, +MKMGRAEPAUL01,2,P0 CHANNEL B,8589934592,Physical Memory,6400,500,Win32_PhysicalMemory,32,Physical Memory,DIMM 0,1,,,,,Micron Technology,500,0,500,,Physical Memory,,MT62F2G32D8DR-031 WT,,,,,00000000,,35,6400,,Physical Memory 1,32,16512, +MKMGRAEPAUL01,2,P0 CHANNEL C,8589934592,Physical Memory,6400,500,Win32_PhysicalMemory,32,Physical Memory,DIMM 0,1,,,,,Micron Technology,500,0,500,,Physical Memory,,MT62F2G32D8DR-031 WT,,,,,00000000,,35,6400,,Physical Memory 2,32,16512, +MKMGRAEPAUL01,2,P0 CHANNEL D,8589934592,Physical Memory,6400,500,Win32_PhysicalMemory,32,Physical Memory,DIMM 0,1,,,,,Micron Technology,500,0,500,,Physical Memory,,MT62F2G32D8DR-031 WT,,,,,00000000,,35,6400,,Physical Memory 3,32,16512, diff --git a/test/unit/plugin/test_dimms_collector.py b/test/unit/plugin/test_dimms_collector.py index eeaa15ff..564c3418 100644 --- a/test/unit/plugin/test_dimms_collector.py +++ b/test/unit/plugin/test_dimms_collector.py @@ -23,6 +23,7 @@ # SOFTWARE. # ############################################################################### +from pathlib import Path from unittest.mock import MagicMock import pytest @@ -31,12 +32,30 @@ from nodescraper.enums.executionstatus import ExecutionStatus from nodescraper.enums.systeminteraction import SystemInteractionLevel from nodescraper.models.systeminfo import OSFamily +from nodescraper.plugins.inband.dimm.collector_args import DimmCollectorArgs from nodescraper.plugins.inband.dimm.dimm_collector import DimmCollector from nodescraper.plugins.inband.dimm.dimmdata import DimmDataModel +FIXTURES = Path(__file__).parent / "fixtures" + +# `dmidecode -q --type 17` from a dual socket host, 24 slots of Samsung DDR5. +# Being quiet, it carries no handle lines naming the DMI type, so a record is +# only identifiable by its section title and closed by a blank line. +QUIET_DUMP = (FIXTURES / "dmidecode_quiet.txt").read_text() + +# An unfiltered dump, where every record opens with a handle line naming its +# type. It covers the awkward records a real fleet throws up: a type 16 array +# whose size is not a module at all, a module sized in megabytes that nests a +# further volatile size, an empty slot, and placeholder manufacturer text. +FULL_DUMP = (FIXTURES / "dmidecode_full.txt").read_text() + +# `wmic memorychip get /format:csv` output from a host with soldered LPDDR5. +WMIC_DUMP = (FIXTURES / "dmideode_wmic.txt").read_text() + @pytest.fixture def collector(system_info, conn_mock): + system_info.os_family = OSFamily.LINUX return DimmCollector( system_info=system_info, system_interaction_level=SystemInteractionLevel.PASSIVE, @@ -44,69 +63,267 @@ def collector(system_info, conn_mock): ) -def test_run_windows(system_info, conn_mock): +@pytest.fixture +def windows_collector(system_info, conn_mock): system_info.os_family = OSFamily.WINDOWS - collector = DimmCollector( + return DimmCollector( system_info=system_info, system_interaction_level=SystemInteractionLevel.PASSIVE, connection=conn_mock, ) + +def cmd_result(exit_code=0, stdout="", stderr="", command="dmidecode"): + """Build a stand in for the CommandArtifact that _run_sut_cmd returns.""" + return MagicMock(exit_code=exit_code, stdout=stdout, stderr=stderr, command=command) + + +def descriptions(result): + """List the description of every event logged against a result.""" + return [event.description for event in result.events] + + +def linux_run(collector, targeted=QUIET_DUMP, full=FULL_DUMP): + """Collect with the full dump and the targeted dump each stubbed out.""" collector._run_sut_cmd = MagicMock( - return_value=MagicMock( - exit_code=0, - stdout="8589934592\n8589934592\n17179869184\n", - ) + side_effect=[cmd_result(stdout=full), cmd_result(stdout=targeted)] ) + return collector.collect_data() + + +def test_run_linux(collector): + result, data = linux_run(collector) - result, data = collector.collect_data() - assert data == DimmDataModel(dimms="32768.00GB @ 2 x 8192.00GB 1 x 16384.00GB ") assert result.status == ExecutionStatus.OK + assert data.dimm_count == 24 + assert data.total_size_bytes == 24 * 96 * 1024**3 + assert data.total_size == "2304GB" + assert data.population == {"96GB": 24} + assert str(data) == "2304GB @ 24 x 96GB" + assert result.message == "DIMM: 2304GB @ 24 x 96GB" -def test_run_linux(collector, system_info): - system_info.os_family = OSFamily.LINUX +def test_run_linux_decodes_every_field(collector): + _, data = linux_run(collector) + dimm = data.dimms[0] + + assert dimm.size_bytes == 96 * 1024**3 + assert dimm.size == "96GB" + assert dimm.locator == "CPU0_A" + assert dimm.bank_locator == "_Node0_Channel0_Dimm0" + assert dimm.manufacturer == "Samsung" + # dmidecode pads the part number out to its full field width. + assert dimm.part_number == "M321RYGA0PB0-CWMXJ" + assert dimm.serial_number == "2432-50F82DCB" + assert dimm.memory_type == "DDR5" + assert dimm.form_factor == "DIMM" + assert dimm.speed_mts == 5600 + assert dimm.configured_speed_mts == 5600 + assert dimm.rank == 2 + assert dimm.data_width_bits == 64 + assert dimm.total_width_bits == 80 + assert str(dimm) == "CPU0_A: 96GB DDR5 5600MT/s Samsung" + + +def test_run_linux_covers_every_slot(collector): + _, data = linux_run(collector) + + assert [dimm.locator for dimm in data.dimms] == [ + f"CPU{cpu}_{channel}" for cpu in (0, 1) for channel in "ABCDEFGHIJKL" + ] + + +def test_run_linux_keeps_full_dump_as_artifact(collector): + result, _ = linux_run(collector) + artifacts = [a for a in result.artifacts if a.filename == "dmidecode.txt"] + assert len(artifacts) == 1 + assert artifacts[0].contents == FULL_DUMP + + +def test_run_linux_parses_unfiltered_dump(collector): + # The targeted dump fails, so the records come from the full dump instead. collector._run_sut_cmd = MagicMock( side_effect=[ - MagicMock( - exit_code=0, - stdout="Full dmidecode output...", - ), - MagicMock( - exit_code=0, - stdout="Size: 64 GB\nSize: 64 GB\nSize: 128 GB\n", - ), + cmd_result(stdout=FULL_DUMP), + cmd_result(exit_code=1, stderr="invalid option -- 'q'"), ] ) - result, data = collector.collect_data() + _, data = collector.collect_data() - assert result.status == ExecutionStatus.OK - assert data == DimmDataModel(dimms="256GB @ 2 x 64GB 1 x 128GB") + # The type 16 array size, the nested volatile size and the empty slot are + # all left out, and the megabyte size normalises against the gigabyte one. + assert data.dimm_count == 2 + assert [dimm.locator for dimm in data.dimms] == ["DIMM_A1", "DIMM_B1"] + assert str(data) == "80GB @ 1 x 16GB 1 x 64GB" -def test_run_linux_error(collector, system_info): - system_info.os_family = OSFamily.LINUX +def test_run_linux_drops_placeholder_text(collector): + collector._run_sut_cmd = MagicMock( + side_effect=[ + cmd_result(stdout=FULL_DUMP), + cmd_result(exit_code=1, stderr="invalid option -- 'q'"), + ] + ) + + _, data = collector.collect_data() + dimm = data.dimms[1] + + assert dimm.manufacturer is None + # A serial of all zeroes is a real value rather than a placeholder. + assert dimm.serial_number == "00000000" + assert str(dimm) == "DIMM_B1: 64GB DDR5 4800MT/s" + + +def test_run_linux_warns_when_full_dump_fails(collector): + collector._run_sut_cmd = MagicMock( + side_effect=[ + cmd_result(exit_code=1, stderr="command not found"), + cmd_result(stdout=QUIET_DUMP), + ] + ) + + result, data = collector.collect_data() + + # The inventory still comes back, the missing artifact is only a warning. + assert data.dimm_count == 24 + assert "Could not collect full dmidecode output" in descriptions(result) + assert not [a for a in result.artifacts if a.filename == "dmidecode.txt"] + +def test_run_linux_retries_without_sudo(collector): collector._run_sut_cmd = MagicMock( side_effect=[ - MagicMock( - exit_code=1, - stderr="Error occurred", - command="dmidecode", - ), - MagicMock( - exit_code=1, - stderr="Error occurred", - command="sh -c 'dmidecode -t 17 | ...'", - ), + cmd_result(exit_code=127, stderr="sudo: command not found"), + cmd_result(stdout=FULL_DUMP), + cmd_result(exit_code=127, stderr="sudo: command not found"), + cmd_result(stdout=QUIET_DUMP), ] ) result, data = collector.collect_data() + assert result.status == ExecutionStatus.OK + assert data.dimm_count == 24 + assert [call.kwargs["sudo"] for call in collector._run_sut_cmd.call_args_list] == [ + True, + False, + True, + False, + ] + + +def test_run_linux_error(collector): + collector._run_sut_cmd = MagicMock( + return_value=cmd_result(exit_code=1, stderr="Error occurred") + ) + + result, data = collector.collect_data() + + assert result.status == ExecutionStatus.ERROR + assert data is None + assert result.message.startswith("DIMM info not found") + assert descriptions(result) == [ + "Error checking dimms", + "Could not collect full dmidecode output", + "Error checking dimms", + "DIMM info not found", + ] + assert result.events[0].category == EventCategory.OS.value + + +def test_run_linux_no_modules_installed(collector): + empty = "Memory Device\n\tSize: No Module Installed\n\tLocator: DIMM_A1\n" + + result, data = linux_run(collector, targeted=empty) + + assert result.status == ExecutionStatus.ERROR + assert data is None + assert "DIMM info not found" in descriptions(result) + + +def test_skip_sudo(collector): + collector._run_sut_cmd = MagicMock() + + result, data = collector.collect_data(DimmCollectorArgs(skip_sudo=True)) + + assert result.status == ExecutionStatus.NOT_RAN + assert data is None + collector._run_sut_cmd.assert_not_called() + + +def test_run_windows(windows_collector): + windows_collector._run_sut_cmd = MagicMock(return_value=cmd_result(stdout=WMIC_DUMP)) + + result, data = windows_collector.collect_data() + + assert result.status == ExecutionStatus.OK + assert data.dimm_count == 4 + assert data.total_size_bytes == 4 * 8 * 1024**3 + assert data.population == {"8GB": 4} + assert str(data) == "32GB @ 4 x 8GB" + windows_collector._run_sut_cmd.assert_called_once_with(DimmCollector.CMD_WINDOWS) + + +def test_run_windows_decodes_every_field(windows_collector): + windows_collector._run_sut_cmd = MagicMock(return_value=cmd_result(stdout=WMIC_DUMP)) + + _, data = windows_collector.collect_data() + dimm = data.dimms[0] + + # Capacity arrives as a bare byte count rather than a "8 GB" style size. + assert dimm.size_bytes == 8 * 1024**3 + assert dimm.size == "8GB" + assert dimm.locator == "DIMM 0" + assert dimm.bank_locator == "P0 CHANNEL A" + assert dimm.manufacturer == "Micron Technology" + assert dimm.part_number == "MT62F2G32D8DR-031 WT" + assert dimm.serial_number == "00000000" + # SMBIOSMemoryType 35 and FormFactor 1 are raw enum codes on Windows, and + # the two enumerations do not share a numbering. + assert dimm.memory_type == "LPDDR5" + assert dimm.form_factor == "Other" + assert dimm.speed_mts == 6400 + assert dimm.configured_speed_mts == 6400 + assert dimm.rank is None + assert dimm.data_width_bits == 32 + assert dimm.total_width_bits == 32 + assert str(dimm) == "DIMM 0: 8GB LPDDR5 6400MT/s Micron Technology" + + +def test_run_windows_covers_every_slot(windows_collector): + windows_collector._run_sut_cmd = MagicMock(return_value=cmd_result(stdout=WMIC_DUMP)) + + result, data = windows_collector.collect_data() + + # Every module reports the same device locator, so only the bank tells the + # four soldered channels apart. + assert [dimm.bank_locator for dimm in data.dimms] == [ + "P0 CHANNEL A", + "P0 CHANNEL B", + "P0 CHANNEL C", + "P0 CHANNEL D", + ] + assert len([a for a in result.artifacts if a.filename == "memorychip.csv"]) == 1 + + +def test_run_windows_error(windows_collector): + windows_collector._run_sut_cmd = MagicMock( + return_value=cmd_result(exit_code=1, stderr="Invalid query", command="wmic") + ) + + result, data = windows_collector.collect_data() + assert result.status == ExecutionStatus.ERROR assert data is None - assert result.events[1].category == EventCategory.OS.value - assert result.events[1].description == "Error checking dimms" + assert descriptions(result) == ["Error checking dimms", "DIMM info not found"] + + +def test_empty_model(): + data = DimmDataModel() + + assert data.dimm_count == 0 + assert data.total_size_bytes == 0 + assert data.population == {} + assert str(data) == "0GB"