diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml new file mode 100644 index 0000000..e699c37 --- /dev/null +++ b/.github/workflows/flake8.yml @@ -0,0 +1,24 @@ +name: flake8 + +on: + pull_request: + types: + - 'synchronize' + - 'opened' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + - name: Run flake8 + run: flake8 . diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..f6e80e4 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,26 @@ +name: unit_tests + +on: + pull_request: + types: + - 'synchronize' + - 'opened' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v1 + with: + python-version: 3.9 + - name: Install pytest + run: | + python -m pip install --upgrade pip + pip install pytest + - name: Install package + run: python setup.py install + - name: Run tests + run: pytest -v tests diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d102db9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea/* +build/* +dist/* +ixhardware.egg* +__pycache__ +*.pyc diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..c4c0846 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +ixhardware (1.0-0~truenas+1) electriceel-truenas-unstable; urgency=medium + + * Initial release + + -- Vladimir Vinogradenko Tue, 20 Feb 2024 12:33:00 +0200 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..1fb3912 --- /dev/null +++ b/debian/control @@ -0,0 +1,19 @@ +Source: ixhardware +Section: contrib/python +Priority: optional +Maintainer: Vladimir Vinogradenko +Build-Depends: debhelper-compat (= 12), + dh-python, + python3-dev, + python3-setuptools +Standards-Version: 4.4.1 +Homepage: https://github.com/truenas/ixhardware +Testsuite: autopkgtest-pkg-python + +Package: python3-ixhardware +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + ${python3:Depends} +Description: Detect iXsystems hardware. + This package detects iXsystems hardware. diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..7185a39 --- /dev/null +++ b/debian/rules @@ -0,0 +1,9 @@ +#!/usr/bin/make -f +export DH_VERBOSE = 1 + +export PYBUILD_NAME=ixhardware + +%: + dh $@ --with python3 --buildsystem=pybuild + +override_dh_auto_test: diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/debian/source/options b/debian/source/options new file mode 100644 index 0000000..cb61fa5 --- /dev/null +++ b/debian/source/options @@ -0,0 +1 @@ +extend-diff-ignore = "^[^/]*[.]egg-info/" diff --git a/ixhardware/__init__.py b/ixhardware/__init__.py new file mode 100644 index 0000000..211c7ef --- /dev/null +++ b/ixhardware/__init__.py @@ -0,0 +1,4 @@ +from .chassis import PLATFORM_PREFIXES, TRUENAS_UNKNOWN, get_chassis_hardware +from .dmi import DMIInfo, DMIParser, parse_dmi + +__all__ = ["PLATFORM_PREFIXES", "TRUENAS_UNKNOWN", "get_chassis_hardware", "DMIInfo", "DMIParser", "parse_dmi"] diff --git a/ixhardware/chassis.py b/ixhardware/chassis.py new file mode 100644 index 0000000..81ff4df --- /dev/null +++ b/ixhardware/chassis.py @@ -0,0 +1,32 @@ +from .dmi import DMIInfo + +__all__ = ["PLATFORM_PREFIXES", "TRUENAS_UNKNOWN", "get_chassis_hardware"] + + +# We tag SMBIOS with relevant strings for each platform +# before we ship to customer. These are the various prefixes +# that represent each hardware platform. +# ("TRUENAS-X10", "TRUENAS-M50", "TRUENAS-MINI-X+", "FREENAS-MINI-X", etc) +PLATFORM_PREFIXES = ( + "TRUENAS-Z", # z-series + "TRUENAS-X", # x-series + "TRUENAS-M", # m-series AND current mini platforms + "TRUENAS-F", # f-series (F60, F100, F130) + "TRUENAS-H", # h-series (H10, H20) + "TRUENAS-R", # freenas certified replacement + "FREENAS-MINI", # minis tagged with legacy information +) +TRUENAS_UNKNOWN = "TRUENAS-UNKNOWN" + + +def get_chassis_hardware(dmi: DMIInfo): + if dmi.system_product_name.startswith(PLATFORM_PREFIXES): + return dmi.system_product_name + + if dmi.baseboard_product_name == "iXsystems TrueNAS X10": + # could be that production didn"t burn in the correct x-series + # model information so let"s check the motherboard model as a + # last resort + return "TRUENAS-X" + + return TRUENAS_UNKNOWN diff --git a/ixhardware/dmi.py b/ixhardware/dmi.py new file mode 100644 index 0000000..7b66025 --- /dev/null +++ b/ixhardware/dmi.py @@ -0,0 +1,99 @@ +from dataclasses import dataclass +from datetime import date, datetime +import logging +import subprocess +from typing import Optional + +logger = logging.getLogger(__name__) + +__all__ = ["DMIInfo", "DMIParser", "parse_dmi"] + + +@dataclass +class DMIInfo: + bios_release_date: Optional[date] = None + ecc_memory: bool = False + baseboard_manufacturer: str = "" + baseboard_product_name: str = "" + system_manufacturer: str = "" + system_product_name: str = "" + system_serial_number: str = "" + system_version: str = "" + has_ipmi: bool = False + + +class DMIParser: + command = ["dmidecode", "-t", "0,1,2,16,38"] + + def parse(self, output: str) -> DMIInfo: + return self._parse_dmi(output.splitlines()) + + def _parse_dmi(self, lines: [str]) -> DMIInfo: + info = DMIInfo() + + _type = None + for line in lines: + if "DMI type 0," in line: + _type = "RELEASE_DATE" + if "DMI type 1," in line: + _type = "SYSINFO" + if "DMI type 2," in line: + _type = "BBINFO" + if "DMI type 38," in line: + _type = "IPMI" + + if not line or ":" not in line: + # "sections" are separated by the category name and then + # a newline so ignore those lines + continue + + sect, val = [i.strip() for i in line.split(":", 1)] + if sect == "Release Date": + info.bios_release_date = self._parse_bios_release_date(val) + elif sect == "Manufacturer": + if _type == "SYSINFO": + info.system_manufacturer = val + else: + info.baseboard_manufacturer = val + elif sect == "Product Name": + if _type == "SYSINFO": + info.system_product_name = val + else: + info.baseboard_product_name = val + elif sect == "Serial Number" and _type == "SYSINFO": + info.system_serial_number = val + elif sect == "Version" and _type == "SYSINFO": + info.system_version = val + elif sect == "I2C Slave Address": + info.has_ipmi = True + elif sect == "Error Correction Type": + info.ecc_memory = "ECC" in val + # we break the for loop here since "16" is the last section + # that gets processed + break + + return info + + def _parse_bios_release_date(self, string): + parts = string.strip().split("/") + if len(parts) < 3: + # Don"t know what the BIOS is reporting so assume it"s invalid + return + + # Give the best effort to convert to a date object. + # Searched hundreds of debugs that have been provided + # via end-users and 99% all reported the same date + # format, however, there are a couple that had a + # 2 digit year instead of a 4 digit year...gross + formatter = "%m/%d/%Y" if len(parts[-1]) == 4 else "%m/%d/%y" + try: + return datetime.strptime(string, formatter).date() + except Exception as e: + logger.warning(f"Failed to format BIOS release date to datetime object: {e!r}") + + +def parse_dmi() -> DMIInfo: + return DMIParser().parse( + subprocess.run(DMIParser.command, check=False, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + encoding="utf-8", errors="ignore").stdout + ) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..aa079ec --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=120 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..baeddcb --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from distutils.core import setup +from setuptools import find_packages + + +setup( + name="ixhardware", + description="Detect iXsystems hardware", + version="1.0", + include_package_data=True, + packages=find_packages(), + license="GNU3", + platforms="any", +) diff --git a/tests/test_dmi.py b/tests/test_dmi.py new file mode 100644 index 0000000..74fcfb2 --- /dev/null +++ b/tests/test_dmi.py @@ -0,0 +1,409 @@ +from datetime import date + +from ixhardware import DMIInfo, DMIParser + + +def test__missing(): + assert DMIParser().parse("") == DMIInfo( + bios_release_date=None, + ecc_memory=False, + baseboard_manufacturer="", + baseboard_product_name="", + system_manufacturer="", + system_product_name="", + system_serial_number="", + system_version="", + has_ipmi=False, + ) + + +full_dmi = """ +# dmidecode 3.3 +Getting SMBIOS data from sysfs. +SMBIOS 3.2.1 present. + +Handle 0x0000, DMI type 0, 26 bytes +BIOS Information + Vendor: American Megatrends Inc. + Version: 3.3aV3 + Release Date: 12/03/2020 + Address: 0xF0000 + Runtime Size: 64 kB + ROM Size: 32 MB + Characteristics: + PCI is supported + BIOS is upgradeable + BIOS shadowing is allowed + Boot from CD is supported + Selectable boot is supported + BIOS ROM is socketed + EDD is supported + 5.25"/1.2 MB floppy services are supported (int 13h) + 3.5"/720 kB floppy services are supported (int 13h) + 3.5"/2.88 MB floppy services are supported (int 13h) + Print screen service is supported (int 5h) + Serial services are supported (int 14h) + Printer services are supported (int 17h) + ACPI is supported + USB legacy is supported + BIOS boot specification is supported + Targeted content distribution is supported + UEFI is supported + BIOS Revision: 5.14 + +Handle 0x0001, DMI type 1, 27 bytes +System Information + Manufacturer: iXsystems + Product Name: TRUENAS-M60 + Version: 0123456789 + Serial Number: A1-111111 + UUID: 00000000-0000-0000-0000-3cecef5ee7d6 + Wake-up Type: Power Switch + SKU Number: To be filled by O.E.M. + Family: To be filled by O.E.M. + +Handle 0x0002, DMI type 2, 15 bytes +Base Board Information + Manufacturer: Supermicro + Product Name: X11SPH-nCTPF + Version: 1.01 + Serial Number: 0000000000000 + Asset Tag: To be filled by O.E.M. + Features: + Board is a hosting board + Board is replaceable + Location In Chassis: To be filled by O.E.M. + Chassis Handle: 0x0003 + Type: Motherboard + Contained Object Handles: 0 + +Handle 0x0014, DMI type 38, 18 bytes +IPMI Device Information + Interface Type: KCS (Keyboard Control Style) + Specification Version: 2.0 + I2C Slave Address: 0x10 + NV Storage Device: Not Present + Base Address: 0x0000000000000CA2 (I/O) + Register Spacing: Successive Byte Boundaries + +Handle 0x0024, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Single-bit ECC + Maximum Capacity: 2304 GB + Error Information Handle: Not Provided + Number Of Devices: 4 + +Handle 0x002C, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Single-bit ECC + Maximum Capacity: 2304 GB + Error Information Handle: Not Provided + Number Of Devices: 4 + +""" + + +def test__full(): + assert DMIParser().parse(full_dmi) == DMIInfo( + bios_release_date=date(2020, 12, 3), + ecc_memory=True, + baseboard_manufacturer="Supermicro", + baseboard_product_name="X11SPH-nCTPF", + system_manufacturer="iXsystems", + system_product_name="TRUENAS-M60", + system_serial_number="A1-111111", + system_version="0123456789", + has_ipmi=True, + ) + + +double_colon_dmi = """ +# dmidecode 3.3 +Getting SMBIOS data from sysfs. +SMBIOS 2.7 present.Handle 0x0001, DMI type 1, 27 bytes +System Information + Manufacturer: Supermicro + Product Name: X9DRi-LN4+/X9DR3-LN4+ + Version: 0123456789 + Serial Number: 0123456789 + UUID: 00000000-0000-0000-0000-002590f3967a + Wake-up Type: Power Switch + SKU Number: To be filled by O.E.M. + Family: To be filled by O.E.M.Handle 0x0002, DMI type 2, 15 bytes +Base Board Information + Manufacturer: Supermicro + Product Name: X9DRi-LN4+/X9DR3-LN4+ + Version: REV:1.20A + Serial Number: FAKE + Asset Tag: To be filled by O.E.M. + Features: + Board is a hosting board + Board is replaceable + Location In Chassis: To be filled by O.E.M. + Chassis Handle: 0x0003 + Type: Motherboard + Contained Object Handles: 0Handle 0x002F, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Multi-bit ECC + Maximum Capacity: 768 GB + Error Information Handle: Not Provided + Number Of Devices: 12Handle 0x0049, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Multi-bit ECC + Maximum Capacity: 768 GB + Error Information Handle: Not Provided + Number Of Devices: 12 +""" + + +def test__double_colon(): + assert DMIParser().parse(double_colon_dmi) == DMIInfo( + bios_release_date=None, + ecc_memory=True, + baseboard_manufacturer="Supermicro", + baseboard_product_name="X9DRi-LN4+/X9DR3-LN4+", + system_manufacturer="Supermicro", + system_product_name="X9DRi-LN4+/X9DR3-LN4+", + system_serial_number="0123456789", + system_version="0123456789", + has_ipmi=False, + ) + + +missing_dmi_type1 = """ +# dmidecode 3.3 +Getting SMBIOS data from sysfs. +SMBIOS 2.8 present. + +Handle 0x0002, DMI type 2, 15 bytes +Base Board Information + Manufacturer: Supermicro + Product Name: X11SPH-nCTPF + Version: 1.01 + Serial Number: 0000000000000 + Asset Tag: To be filled by O.E.M. + Features: + Board is a hosting board + Board is replaceable + Location In Chassis: To be filled by O.E.M. + Chassis Handle: 0x0003 + Type: Motherboard + Contained Object Handles: 0 + +Handle 0x1000, DMI type 16, 23 bytes +Physical Memory Array + Location: Other + Use: System Memory + Error Correction Type: Multi-bit ECC + Maximum Capacity: 8 GB + Error Information Handle: Not Provided + Number Of Devices: 1 + +""" + + +def test__missing_dmi_type1(): + assert DMIParser().parse(missing_dmi_type1) == DMIInfo( + bios_release_date=None, + ecc_memory=True, + baseboard_manufacturer="Supermicro", + baseboard_product_name="X11SPH-nCTPF", + system_manufacturer="", + system_product_name="", + system_serial_number="", + system_version="", + has_ipmi=False, + ) + + +missing_dmi_type2 = """ +# dmidecode 3.3 +Getting SMBIOS data from sysfs. +SMBIOS 2.8 present. + +Handle 0x0100, DMI type 1, 27 bytes +System Information + Manufacturer: QEMU + Product Name: Standard PC (Q35 + ICH9, 2009) + Version: pc-q35-5.2 + Serial Number: Not Specified + UUID: 236ce080-e87b-4d21-b9dd-3e43b8fb58dd + Wake-up Type: Power Switch + SKU Number: Not Specified + Family: Not Specified + +Handle 0x1000, DMI type 16, 23 bytes +Physical Memory Array + Location: Other + Use: System Memory + Error Correction Type: Multi-bit ECC + Maximum Capacity: 8 GB + Error Information Handle: Not Provided + Number Of Devices: 1 + +""" + + +def test__missing_dmi_type2(): + assert DMIParser().parse(missing_dmi_type2) == DMIInfo( + bios_release_date=None, + ecc_memory=True, + baseboard_manufacturer="", + baseboard_product_name="", + system_manufacturer="QEMU", + system_product_name="Standard PC (Q35 + ICH9, 2009)", + system_serial_number="Not Specified", + system_version="pc-q35-5.2", + has_ipmi=False, + ) + + +missing_dmi_type16 = """ +# dmidecode 3.3 +Getting SMBIOS data from sysfs. +SMBIOS 2.8 present. + +Handle 0x0100, DMI type 1, 27 bytes +System Information + Manufacturer: QEMU + Product Name: Standard PC (Q35 + ICH9, 2009) + Version: pc-q35-5.2 + Serial Number: Not Specified + UUID: 236ce080-e87b-4d21-b9dd-3e43b8fb58dd + Wake-up Type: Power Switch + SKU Number: Not Specified + Family: Not Specified + +Handle 0x0002, DMI type 2, 15 bytes +Base Board Information + Manufacturer: Supermicro + Product Name: X11SPH-nCTPF + Version: 1.01 + Serial Number: 0000000000000 + Asset Tag: To be filled by O.E.M. + Features: + Board is a hosting board + Board is replaceable + Location In Chassis: To be filled by O.E.M. + Chassis Handle: 0x0003 + Type: Motherboard + Contained Object Handles: 0 + +""" + + +def test__missing_dmi_type16(): + assert DMIParser().parse(missing_dmi_type16) == DMIInfo( + bios_release_date=None, + ecc_memory=False, + baseboard_manufacturer="Supermicro", + baseboard_product_name="X11SPH-nCTPF", + system_manufacturer="QEMU", + system_product_name="Standard PC (Q35 + ICH9, 2009)", + system_serial_number="Not Specified", + system_version="pc-q35-5.2", + has_ipmi=False, + ) + + +missing_dmi_type38 = """ +# dmidecode 3.3 +Getting SMBIOS data from sysfs. +SMBIOS 3.2.1 present. + +Handle 0x0000, DMI type 0, 26 bytes +BIOS Information + Vendor: American Megatrends Inc. + Version: 3.3aV3 + Release Date: 12/03/2020 + Address: 0xF0000 + Runtime Size: 64 kB + ROM Size: 32 MB + Characteristics: + PCI is supported + BIOS is upgradeable + BIOS shadowing is allowed + Boot from CD is supported + Selectable boot is supported + BIOS ROM is socketed + EDD is supported + 5.25"/1.2 MB floppy services are supported (int 13h) + 3.5"/720 kB floppy services are supported (int 13h) + 3.5"/2.88 MB floppy services are supported (int 13h) + Print screen service is supported (int 5h) + Serial services are supported (int 14h) + Printer services are supported (int 17h) + ACPI is supported + USB legacy is supported + BIOS boot specification is supported + Targeted content distribution is supported + UEFI is supported + BIOS Revision: 5.14 + +Handle 0x0001, DMI type 1, 27 bytes +System Information + Manufacturer: iXsystems + Product Name: TRUENAS-M60 + Version: 0123456789 + Serial Number: A1-111111 + UUID: 00000000-0000-0000-0000-3cecef5ee7d6 + Wake-up Type: Power Switch + SKU Number: To be filled by O.E.M. + Family: To be filled by O.E.M. + +Handle 0x0002, DMI type 2, 15 bytes +Base Board Information + Manufacturer: Supermicro + Product Name: X11SPH-nCTPF + Version: 1.01 + Serial Number: 0000000000000 + Asset Tag: To be filled by O.E.M. + Features: + Board is a hosting board + Board is replaceable + Location In Chassis: To be filled by O.E.M. + Chassis Handle: 0x0003 + Type: Motherboard + Contained Object Handles: 0 + +Handle 0x0024, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Single-bit ECC + Maximum Capacity: 2304 GB + Error Information Handle: Not Provided + Number Of Devices: 4 + +Handle 0x002C, DMI type 16, 23 bytes +Physical Memory Array + Location: System Board Or Motherboard + Use: System Memory + Error Correction Type: Single-bit ECC + Maximum Capacity: 2304 GB + Error Information Handle: Not Provided + Number Of Devices: 4 + +""" + + +def test__missing_dmi_type38(): + assert DMIParser().parse(missing_dmi_type38) == DMIInfo( + bios_release_date=date(2020, 12, 3), + ecc_memory=True, + baseboard_manufacturer="Supermicro", + baseboard_product_name="X11SPH-nCTPF", + system_manufacturer="iXsystems", + system_product_name="TRUENAS-M60", + system_serial_number="A1-111111", + system_version="0123456789", + has_ipmi=False, + )