From 17d515d035ee5efe8f447cfa5e0fe60db522d810 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Mon, 12 Oct 2026 23:42:14 +0000 Subject: [PATCH 01/10] Add package skeleton pyproject.toml (Apache-2.0, Python >= 3.10, depends on the DMTF redfish library) and an empty napalm_redfish package. No napalm.drivers entry point yet: the driver class waits for the ManagementControllerDriver role base. --- napalm_redfish/__init__.py | 3 +++ pyproject.toml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 napalm_redfish/__init__.py create mode 100644 pyproject.toml diff --git a/napalm_redfish/__init__.py b/napalm_redfish/__init__.py new file mode 100644 index 0000000..265035b --- /dev/null +++ b/napalm_redfish/__init__.py @@ -0,0 +1,3 @@ +"""NAPALM driver for server BMCs over DMTF Redfish.""" + +__version__ = "0.1.0.dev0" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2c82a1e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[project] +name = "napalm-redfish" +version = "0.1.0.dev0" +description = "NAPALM driver for server baseboard management controllers over DMTF Redfish" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.10" +dependencies = [ + "redfish>=3.2", +] +classifiers = [ + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Topic :: System :: Hardware", + "Topic :: System :: Networking", +] + +[project.optional-dependencies] +test = [ + "pytest>=7", + "responses>=0.23", +] + +[tool.setuptools.packages.find] +include = ["napalm_redfish*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] From c8bcd5ace129bbd0e98011fc3fa85c4871d8c1b2 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Mon, 12 Oct 2026 23:42:14 +0000 Subject: [PATCH 02/10] Add DMTF mockup fixtures and test helpers Unmodified files from DMTF Redfish-Publications (public-rackmount1, public-localstorage, public-telemetry), only those the tests use, each set with DMTF's BSD-3-Clause license and a COMMANDS.txt naming the source commit and request paths. conftest.py loads them by request path and serves a mockup over mocked HTTP, with a SessionService that hands out a token, for the transport tests. --- tests/conftest.py | 112 +++++++++ .../dmtf-public-localstorage/COMMANDS.txt | 17 ++ .../1U/Drives/32ADF365C6C1B7BD/index.json | 40 +++ .../1U/Drives/35D38F11ACEF7BD3/index.json | 46 ++++ .../1U/Drives/3D58ECBC375FD9F2/index.json | 49 ++++ .../1U/Drives/3F5A8C54207B7233/index.json | 46 ++++ .../Chassis/1U/Power/index.json | 139 +++++++++++ .../Chassis/1U/Thermal/index.json | 159 ++++++++++++ .../Chassis/1U/index.json | 46 ++++ .../dmtf-public-localstorage/LICENSE.md | 29 +++ .../Systems/437XR1138R2/Storage/1/index.json | 73 ++++++ .../Systems/437XR1138R2/Storage/index.json | 12 + .../AccountService/Accounts/1/index.json | 33 +++ .../AccountService/Accounts/2/index.json | 38 +++ .../AccountService/Accounts/index.json | 12 + .../AccountService/index.json | 160 ++++++++++++ .../dmtf-public-rackmount1/COMMANDS.txt | 66 +++++ .../Chassis/1U/Power/index.json | 139 +++++++++++ .../PowerSupplies/Bay1/Metrics/index.json | 85 +++++++ .../PowerSupplies/Bay1/index.json | 99 ++++++++ .../PowerSupplies/Bay2/index.json | 19 ++ .../PowerSubsystem/PowerSupplies/index.json | 15 ++ .../Chassis/1U/PowerSubsystem/index.json | 47 ++++ .../Chassis/1U/Sensors/CPU1Temp/index.json | 83 +++++++ .../Chassis/1U/Sensors/CPUFan1/index.json | 36 +++ .../Chassis/1U/Sensors/IntakeTemp/index.json | 16 ++ .../1U/Sensors/PS1InputVoltage/index.json | 41 +++ .../Chassis/1U/Sensors/TotalPower/index.json | 53 ++++ .../Chassis/1U/Sensors/index.json | 132 ++++++++++ .../Chassis/1U/Thermal/index.json | 159 ++++++++++++ .../1U/ThermalSubsystem/Fans/Bay1/index.json | 31 +++ .../1U/ThermalSubsystem/Fans/Bay2/index.json | 31 +++ .../1U/ThermalSubsystem/Fans/CPU1/index.json | 31 +++ .../1U/ThermalSubsystem/Fans/CPU2/index.json | 31 +++ .../1U/ThermalSubsystem/Fans/index.json | 21 ++ .../ThermalMetrics/index.json | 37 +++ .../Chassis/1U/ThermalSubsystem/index.json | 57 +++++ .../Chassis/1U/index.json | 104 ++++++++ .../dmtf-public-rackmount1/Chassis/index.json | 12 + .../dmtf-public-rackmount1/LICENSE.md | 29 +++ .../BMC/EthernetInterfaces/ToHost/index.json | 57 +++++ .../BMC/EthernetInterfaces/eth0/index.json | 83 +++++++ .../BMC/EthernetInterfaces/index.json | 16 ++ .../BMC/LogServices/Log/Entries/index.json | 30 +++ .../Managers/BMC/LogServices/Log/index.json | 24 ++ .../Managers/BMC/LogServices/index.json | 13 + .../Managers/BMC/NetworkProtocol/index.json | 87 +++++++ .../Managers/BMC/index.json | 100 ++++++++ .../Managers/index.json | 12 + .../SessionService/Sessions/index.json | 15 ++ .../SessionService/index.json | 19 ++ .../LogServices/Log1/Entries/index.json | 49 ++++ .../437XR1138R2/LogServices/Log1/index.json | 24 ++ .../437XR1138R2/LogServices/index.json | 13 + .../437XR1138R2/Memory/DIMM1/index.json | 41 +++ .../437XR1138R2/Memory/DIMM2/index.json | 41 +++ .../437XR1138R2/Memory/DIMM3/index.json | 41 +++ .../437XR1138R2/Memory/DIMM4/index.json | 23 ++ .../Systems/437XR1138R2/Memory/index.json | 21 ++ .../437XR1138R2/Processors/CPU1/index.json | 68 +++++ .../437XR1138R2/Processors/CPU2/index.json | 19 ++ .../437XR1138R2/Processors/FPGA1/index.json | 88 +++++++ .../Systems/437XR1138R2/Processors/index.json | 18 ++ .../437XR1138R2/SimpleStorage/1/index.json | 48 ++++ .../437XR1138R2/SimpleStorage/index.json | 12 + .../Systems/437XR1138R2/index.json | 233 ++++++++++++++++++ .../dmtf-public-rackmount1/Systems/index.json | 12 + .../FirmwareInventory/BIOS/index.json | 35 +++ .../FirmwareInventory/BMC/index.json | 48 ++++ .../FirmwareInventory/SS/index.json | 34 +++ .../FirmwareInventory/index.json | 18 ++ .../dmtf-public-rackmount1/index.json | 66 +++++ .../dmtf-public-telemetry/COMMANDS.txt | 11 + .../fixtures/dmtf-public-telemetry/LICENSE.md | 29 +++ .../Managers/1/NetworkProtocol/index.json | 61 +++++ .../Systems/1/ResetActionInfo/index.json | 24 ++ .../Systems/1/index.json | 133 ++++++++++ 77 files changed, 4051 insertions(+) create mode 100644 tests/conftest.py create mode 100644 tests/fixtures/dmtf-public-localstorage/COMMANDS.txt create mode 100644 tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/32ADF365C6C1B7BD/index.json create mode 100644 tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/35D38F11ACEF7BD3/index.json create mode 100644 tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/3D58ECBC375FD9F2/index.json create mode 100644 tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/3F5A8C54207B7233/index.json create mode 100644 tests/fixtures/dmtf-public-localstorage/Chassis/1U/Power/index.json create mode 100644 tests/fixtures/dmtf-public-localstorage/Chassis/1U/Thermal/index.json create mode 100644 tests/fixtures/dmtf-public-localstorage/Chassis/1U/index.json create mode 100644 tests/fixtures/dmtf-public-localstorage/LICENSE.md create mode 100644 tests/fixtures/dmtf-public-localstorage/Systems/437XR1138R2/Storage/1/index.json create mode 100644 tests/fixtures/dmtf-public-localstorage/Systems/437XR1138R2/Storage/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/AccountService/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/COMMANDS.txt create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Power/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPU1Temp/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPUFan1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/IntakeTemp/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputVoltage/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/TotalPower/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Thermal/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/Bay1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/Bay2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/CPU1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/CPU2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/ThermalMetrics/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/LICENSE.md create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/ToHost/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/eth0/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/Log/Entries/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/Log/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/BMC/NetworkProtocol/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/BMC/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Managers/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/SessionService/Sessions/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/SessionService/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/Log1/Entries/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/Log1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM3/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM4/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/CPU1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/CPU2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/FPGA1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/SimpleStorage/1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/SimpleStorage/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/BIOS/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/BMC/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/SS/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/index.json create mode 100644 tests/fixtures/dmtf-public-telemetry/COMMANDS.txt create mode 100644 tests/fixtures/dmtf-public-telemetry/LICENSE.md create mode 100644 tests/fixtures/dmtf-public-telemetry/Managers/1/NetworkProtocol/index.json create mode 100644 tests/fixtures/dmtf-public-telemetry/Systems/1/ResetActionInfo/index.json create mode 100644 tests/fixtures/dmtf-public-telemetry/Systems/1/index.json diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1ebd55c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,112 @@ +"""Shared helpers for loading the recorded Redfish responses under tests/fixtures/.""" + +import json +import re +from pathlib import Path +from urllib.parse import urlsplit + +import pytest +import responses + +FIXTURES = Path(__file__).parent / "fixtures" +PREFIX = "/redfish/v1" + + +def fixture_file(mockup: str, path: str) -> Path: + """Map a request path to its file: /redfish/v1/X/Y -> /X/Y/index.json.""" + path = path.split("#", 1)[0].split("?", 1)[0].rstrip("/") + if not path.startswith(PREFIX): + raise ValueError(f"not a Redfish path: {path}") + relative = path[len(PREFIX):].lstrip("/") + return FIXTURES / mockup / relative / "index.json" + + +def load(mockup: str, path: str) -> dict: + return json.loads(fixture_file(mockup, path).read_text()) + + +@pytest.fixture +def rackmount(): + """Loader for the DMTF public-rackmount1 mockup.""" + return lambda path: load("dmtf-public-rackmount1", path) + + +@pytest.fixture +def localstorage(): + """Loader for the DMTF public-localstorage mockup.""" + return lambda path: load("dmtf-public-localstorage", path) + + +@pytest.fixture +def telemetry(): + """Loader for the DMTF public-telemetry mockup.""" + return lambda path: load("dmtf-public-telemetry", path) + + +BASE_URL = "https://bmc.example.net" +SESSIONS = "/redfish/v1/SessionService/Sessions" +SESSION_PATH = SESSIONS + "/0123456789abcdef" +TOKEN = "secret-session-token" + + +class MockupServer: + """Serve a fixture mockup over mocked HTTP, plus a SessionService that hands out one token. + + Unknown paths return a Redfish error body with 404, like a real service. ``override`` + replaces the answer for one method and path (the path may include a query string). + """ + + def __init__(self, rsps, mockup: str): + self.rsps = rsps + self.mockup = mockup + self.overrides: dict[tuple[str, str], tuple[int, dict, dict]] = {} + anything = re.compile(re.escape(BASE_URL) + r"/.*") + for method in (responses.GET, responses.POST, responses.DELETE): + rsps.add_callback(method, anything, callback=self._handle) + + def override(self, method: str, path: str, status: int, body: dict | str | None = None, + headers: dict | None = None): + self.overrides[(method, path)] = (status, body if body is not None else {}, headers or {}) + + @staticmethod + def _reply(status, body, headers=None): + if isinstance(body, str): + text = body + else: + text = json.dumps(body) if body != {} or status != 204 else "" + return status, {"Content-Type": "application/json", **(headers or {})}, text + + def _handle(self, request): + parts = urlsplit(request.url) + for key in ((request.method, f"{parts.path}?{parts.query}"), (request.method, parts.path)): + if key in self.overrides: + return self._reply(*self.overrides[key]) + if request.method == "POST" and parts.path == SESSIONS: + return self._reply(201, {"@odata.id": SESSION_PATH, "Id": "0123456789abcdef"}, + {"X-Auth-Token": TOKEN, "Location": BASE_URL + SESSION_PATH}) + if request.method == "DELETE" and parts.path == SESSION_PATH: + return self._reply(204, {}) + if request.method == "GET": + try: + return self._reply(200, load(self.mockup, parts.path)) + except (FileNotFoundError, ValueError): + pass + return self._reply(404, {"error": {"code": "Base.1.0.ResourceMissingAtURI", + "message": f"{parts.path} not found"}}) + + def calls(self, method: str | None = None, path: str | None = None): + return [c for c in self.rsps.calls + if (method is None or c.request.method == method) + and (path is None or urlsplit(c.request.url).path == path)] + + +@pytest.fixture +def http(): + with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: + yield rsps + + +@pytest.fixture +def server(http): + """The public-rackmount1 mockup served on BASE_URL.""" + return MockupServer(http, "dmtf-public-rackmount1") diff --git a/tests/fixtures/dmtf-public-localstorage/COMMANDS.txt b/tests/fixtures/dmtf-public-localstorage/COMMANDS.txt new file mode 100644 index 0000000..2786cdf --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/COMMANDS.txt @@ -0,0 +1,17 @@ +Source: https://github.com/DMTF/Redfish-Publications +Commit: 4f81814e399213c9055863ddaff42791c6b3706a +Mockup: mockups/public-localstorage + +Files are unmodified copies. Each request path maps to /index.json +(the service root is ./index.json), as in the mockup and as served by Redfish-Mockup-Server. + +Request paths: +GET /redfish/v1/Chassis/1U +GET /redfish/v1/Chassis/1U/Thermal +GET /redfish/v1/Chassis/1U/Power +GET /redfish/v1/Systems/437XR1138R2/Storage +GET /redfish/v1/Systems/437XR1138R2/Storage/1 +GET /redfish/v1/Chassis/1U/Drives/35D38F11ACEF7BD3 +GET /redfish/v1/Chassis/1U/Drives/3F5A8C54207B7233 +GET /redfish/v1/Chassis/1U/Drives/32ADF365C6C1B7BD +GET /redfish/v1/Chassis/1U/Drives/3D58ECBC375FD9F2 diff --git a/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/32ADF365C6C1B7BD/index.json b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/32ADF365C6C1B7BD/index.json new file mode 100644 index 0000000..25c6944 --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/32ADF365C6C1B7BD/index.json @@ -0,0 +1,40 @@ +{ + "@odata.type": "#Drive.v1_22_0.Drive", + "Id": "32ADF365C6C1B7BD", + "Name": "Drive Sample", + "IndicatorLED": "Lit", + "Model": "C123", + "Revision": "100A", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "CapacityBytes": 899527000064, + "FailurePredicted": false, + "Protocol": "SAS", + "MediaType": "HDD", + "Manufacturer": "Contoso", + "SerialNumber": "1234570", + "PartNumber": "C123-1111", + "Identifiers": [ + { + "DurableNameFormat": "NAA", + "DurableName": "32ADF365C6C1B7BD" + } + ], + "HotspareType": "Global", + "HotspareReplacementMode": "Revertible", + "EncryptionAbility": "SelfEncryptingDrive", + "EncryptionStatus": "Unlocked", + "RotationSpeedRPM": 15000, + "BlockSizeBytes": 512, + "CapableSpeedGbs": 12, + "NegotiatedSpeedGbs": 12, + "Actions": { + "#Drive.SecureErase": { + "target": "/redfish/v1/Chassis/1U/Drives/32ADF365C6C1B7BD/Actions/Drive.SecureErase" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Drives/32ADF365C6C1B7BD", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/35D38F11ACEF7BD3/index.json b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/35D38F11ACEF7BD3/index.json new file mode 100644 index 0000000..aef2536 --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/35D38F11ACEF7BD3/index.json @@ -0,0 +1,46 @@ +{ + "@odata.type": "#Drive.v1_22_0.Drive", + "Id": "35D38F11ACEF7BD3", + "Name": "Drive Sample", + "IndicatorLED": "Lit", + "Model": "C123", + "Revision": "100A", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "CapacityBytes": 899527000064, + "FailurePredicted": false, + "Protocol": "SAS", + "MediaType": "HDD", + "Manufacturer": "Contoso", + "SerialNumber": "1234567", + "PartNumber": "C123-1111", + "Identifiers": [ + { + "DurableNameFormat": "NAA", + "DurableName": "35D38F11ACEF7BD3" + } + ], + "HotspareType": "None", + "EncryptionAbility": "SelfEncryptingDrive", + "EncryptionStatus": "Unlocked", + "RotationSpeedRPM": 15000, + "BlockSizeBytes": 512, + "CapableSpeedGbs": 12, + "NegotiatedSpeedGbs": 12, + "Links": { + "Volumes": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1" + } + ] + }, + "Actions": { + "#Drive.SecureErase": { + "target": "/redfish/v1/Chassis/1U/Drives/35D38F11ACEF7BD3/Actions/Drive.SecureErase" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Drives/35D38F11ACEF7BD3", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/3D58ECBC375FD9F2/index.json b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/3D58ECBC375FD9F2/index.json new file mode 100644 index 0000000..02a626b --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/3D58ECBC375FD9F2/index.json @@ -0,0 +1,49 @@ +{ + "@odata.type": "#Drive.v1_22_0.Drive", + "Id": "3D58ECBC375FD9F2", + "Name": "Drive Sample", + "IndicatorLED": "Lit", + "Model": "C123", + "Revision": "100A", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "CapacityBytes": 899527000064, + "FailurePredicted": false, + "Protocol": "SAS", + "MediaType": "HDD", + "Manufacturer": "Contoso", + "SerialNumber": "1234568", + "PartNumber": "C123-1111", + "Identifiers": [ + { + "DurableNameFormat": "NAA", + "DurableName": "32ADF365C6C1B7BD" + } + ], + "HotspareType": "None", + "EncryptionAbility": "SelfEncryptingDrive", + "EncryptionStatus": "Unlocked", + "RotationSpeedRPM": 15000, + "BlockSizeBytes": 512, + "CapableSpeedGbs": 12, + "NegotiatedSpeedGbs": 12, + "Links": { + "Volumes": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/2" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/3" + } + ] + }, + "Actions": { + "#Drive.SecureErase": { + "target": "/redfish/v1/Chassis/1U/Drives/3D58ECBC375FD9F2/Actions/Drive.SecureErase" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Drives/3D58ECBC375FD9F2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/3F5A8C54207B7233/index.json b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/3F5A8C54207B7233/index.json new file mode 100644 index 0000000..571eae4 --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Drives/3F5A8C54207B7233/index.json @@ -0,0 +1,46 @@ +{ + "@odata.type": "#Drive.v1_22_0.Drive", + "Id": "3F5A8C54207B7233", + "Name": "Drive Sample", + "IndicatorLED": "Lit", + "Model": "C123", + "Revision": "100A", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "CapacityBytes": 899527000064, + "FailurePredicted": false, + "Protocol": "SAS", + "MediaType": "HDD", + "Manufacturer": "Contoso", + "SerialNumber": "1234569", + "PartNumber": "C123-1111", + "Identifiers": [ + { + "DurableNameFormat": "NAA", + "DurableName": "3F5A8C54207B7233" + } + ], + "HotspareType": "None", + "EncryptionAbility": "SelfEncryptingDrive", + "EncryptionStatus": "Unlocked", + "RotationSpeedRPM": 15000, + "BlockSizeBytes": 512, + "CapableSpeedGbs": 12, + "NegotiatedSpeedGbs": 12, + "Links": { + "Volumes": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/1" + } + ] + }, + "Actions": { + "#Drive.SecureErase": { + "target": "/redfish/v1/Chassis/1U/Drives/3F5A8C54207B7233/Actions/Drive.SecureErase" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Drives/3F5A8C54207B7233", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Power/index.json b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Power/index.json new file mode 100644 index 0000000..c0862b3 --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Power/index.json @@ -0,0 +1,139 @@ +{ + "@odata.type": "#Power.v1_7_3.Power", + "Id": "Power", + "Name": "Power", + "PowerControl": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Power#/PowerControl/0", + "MemberId": "0", + "Name": "Server Power Control", + "PowerConsumedWatts": 344, + "PowerRequestedWatts": 800, + "PowerAvailableWatts": 0, + "PowerCapacityWatts": 800, + "PowerAllocatedWatts": 800, + "PowerMetrics": { + "IntervalInMin": 30, + "MinConsumedWatts": 271, + "MaxConsumedWatts": 489, + "AverageConsumedWatts": 319 + }, + "PowerLimit": { + "LimitInWatts": 500, + "LimitException": "LogEventOnly", + "CorrectionInMs": 50 + }, + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ], + "Status": { + "State": "Enabled", + "Health": "OK" + } + } + ], + "Voltages": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Power#/Voltages/0", + "MemberId": "0", + "Name": "VRM1 Voltage", + "SensorNumber": 11, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingVolts": 12, + "UpperThresholdNonCritical": 12.5, + "UpperThresholdCritical": 13, + "UpperThresholdFatal": 15, + "LowerThresholdNonCritical": 11.5, + "LowerThresholdCritical": 11, + "LowerThresholdFatal": 10, + "MinReadingRange": 0, + "MaxReadingRange": 20, + "PhysicalContext": "VoltageRegulator", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Power#/Voltages/1", + "MemberId": "1", + "Name": "VRM2 Voltage", + "SensorNumber": 12, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingVolts": 5, + "UpperThresholdNonCritical": 5.5, + "UpperThresholdCritical": 7, + "LowerThresholdNonCritical": 4.75, + "LowerThresholdCritical": 4.5, + "MinReadingRange": 0, + "MaxReadingRange": 20, + "PhysicalContext": "VoltageRegulator", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + } + ], + "PowerSupplies": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Power#/PowerSupplies/0", + "MemberId": "0", + "Name": "Power Supply Bay", + "Status": { + "State": "Enabled", + "Health": "Warning" + }, + "PowerSupplyType": "AC", + "LineInputVoltageType": "ACWideRange", + "LineInputVoltage": 120, + "PowerCapacityWatts": 800, + "LastPowerOutputWatts": 325, + "Model": "499253-B21", + "Manufacturer": "ManufacturerName", + "FirmwareVersion": "1.00", + "SerialNumber": "1Z0000001", + "PartNumber": "0000001A3A", + "SparePartNumber": "0000001A3A", + "InputRanges": [ + { + "InputType": "AC", + "MinimumVoltage": 100, + "MaximumVoltage": 120, + "OutputWattage": 800 + }, + { + "InputType": "AC", + "MinimumVoltage": 200, + "MaximumVoltage": 240, + "OutputWattage": 1300 + } + ], + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Power", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Thermal/index.json b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Thermal/index.json new file mode 100644 index 0000000..db748c5 --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/Thermal/index.json @@ -0,0 +1,159 @@ +{ + "@odata.type": "#Thermal.v1_7_3.Thermal", + "Id": "Thermal", + "Name": "Thermal", + "Temperatures": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Temperatures/0", + "MemberId": "0", + "Name": "CPU1 Temp", + "SensorNumber": 5, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingCelsius": 41, + "UpperThresholdNonCritical": 42, + "UpperThresholdCritical": 45, + "UpperThresholdFatal": 48, + "MinReadingRangeTemp": 0, + "MaxReadingRangeTemp": 60, + "PhysicalContext": "CPU", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1" + } + ] + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Temperatures/1", + "MemberId": "1", + "Name": "CPU2 Temp", + "SensorNumber": 6, + "Status": { + "State": "Disabled" + }, + "UpperThresholdNonCritical": 42, + "UpperThresholdCritical": 45, + "UpperThresholdFatal": 48, + "MinReadingRangeTemp": 0, + "MaxReadingRangeTemp": 60, + "PhysicalContext": "CPU", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU2" + } + ] + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Temperatures/2", + "MemberId": "2", + "Name": "Chassis Intake Temp", + "SensorNumber": 9, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingCelsius": 25, + "UpperThresholdNonCritical": 30, + "UpperThresholdCritical": 40, + "UpperThresholdFatal": 50, + "LowerThresholdNonCritical": 10, + "LowerThresholdCritical": 5, + "LowerThresholdFatal": 0, + "MinReadingRangeTemp": 0, + "MaxReadingRangeTemp": 60, + "PhysicalContext": "Intake", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + } + ] + } + ], + "Fans": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Fans/0", + "MemberId": "0", + "Name": "BaseBoard System Fan", + "PhysicalContext": "Backplane", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 2100, + "ReadingUnits": "RPM", + "LowerThresholdFatal": 0, + "MinReadingRange": 0, + "MaxReadingRange": 5000, + "Redundancy": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Redundancy/0" + } + ], + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Fans/1", + "MemberId": "1", + "Name": "BaseBoard System Fan Backup", + "PhysicalContext": "Backplane", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 2050, + "ReadingUnits": "RPM", + "LowerThresholdFatal": 0, + "MinReadingRange": 0, + "MaxReadingRange": 5000, + "Redundancy": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Redundancy/0" + } + ], + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + } + ], + "Redundancy": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Redundancy/0", + "MemberId": "0", + "Name": "BaseBoard System Fans", + "RedundancySet": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Fans/0" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Fans/1" + } + ], + "Mode": "N+m", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "MinNumNeeded": 1, + "MaxNumSupported": 2 + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Thermal", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-localstorage/Chassis/1U/index.json b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/index.json new file mode 100644 index 0000000..c621258 --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Chassis/1U/index.json @@ -0,0 +1,46 @@ +{ + "@odata.type": "#Chassis.v1_29_0.Chassis", + "Id": "1U", + "Name": "Computer System Chassis", + "ChassisType": "RackMount", + "AssetTag": "Chicago-45Z-2381", + "Manufacturer": "Contoso", + "Model": "3500RX", + "SKU": "8675309", + "SerialNumber": "437XR1138R2", + "PartNumber": "224071-J23", + "PowerState": "On", + "IndicatorLED": "Lit", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Thermal": { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal" + }, + "Power": { + "@odata.id": "/redfish/v1/Chassis/1U/Power" + }, + "Drives": { + "@odata.id": "/redfish/v1/Chassis/1U/Drives" + }, + "Links": { + "ComputerSystems": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ], + "ManagersInChassis": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ] + }, + "@odata.id": "/redfish/v1/Chassis/1U", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-localstorage/LICENSE.md b/tests/fixtures/dmtf-public-localstorage/LICENSE.md new file mode 100644 index 0000000..231cf4a --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2014-2025, Contributing Member(s) of Distributed Management Task +Force, Inc.. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/fixtures/dmtf-public-localstorage/Systems/437XR1138R2/Storage/1/index.json b/tests/fixtures/dmtf-public-localstorage/Systems/437XR1138R2/Storage/1/index.json new file mode 100644 index 0000000..695a1e6 --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Systems/437XR1138R2/Storage/1/index.json @@ -0,0 +1,73 @@ +{ + "@odata.type": "#Storage.v1_23_0.Storage", + "Id": "1", + "Name": "Local Storage Controller", + "Description": "Integrated RAID Controller", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + }, + "StorageControllers": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1#/StorageControllers/0", + "MemberId": "0", + "Name": "Contoso Integrated RAID", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Identifiers": [ + { + "DurableNameFormat": "NAA", + "DurableName": "345C59DBD970859C" + } + ], + "Manufacturer": "Contoso", + "Model": "12Gbs Integrated RAID", + "SerialNumber": "2M220100SL", + "PartNumber": "CT18754", + "SpeedGbps": 12, + "FirmwareVersion": "1.0.0.7", + "SupportedControllerProtocols": [ + "PCIe" + ], + "SupportedDeviceProtocols": [ + "SAS", + "SATA" + ], + "CacheSummary": { + "TotalCacheSizeMiB": 1024, + "PersistentCacheSizeMiB": 1024, + "Status": { + "State": "Enabled", + "Health": "OK" + } + } + } + ], + "Drives": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Drives/35D38F11ACEF7BD3" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Drives/3F5A8C54207B7233" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Drives/32ADF365C6C1B7BD" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Drives/3D58ECBC375FD9F2" + } + ], + "Volumes": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes" + }, + "Actions": { + "#Storage.SetEncryptionKey": { + "target": "/redfish/v1/Systems/437XR1138R2/Storage/1/Actions/Storage.SetEncryptionKey" + } + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-localstorage/Systems/437XR1138R2/Storage/index.json b/tests/fixtures/dmtf-public-localstorage/Systems/437XR1138R2/Storage/index.json new file mode 100644 index 0000000..096299f --- /dev/null +++ b/tests/fixtures/dmtf-public-localstorage/Systems/437XR1138R2/Storage/index.json @@ -0,0 +1,12 @@ +{ + "@odata.type": "#StorageCollection.StorageCollection", + "Name": "Storage Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage/1" + } + ], + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Storage", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/1/index.json b/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/1/index.json new file mode 100644 index 0000000..95f3b12 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/1/index.json @@ -0,0 +1,33 @@ +{ + "@odata.type": "#ManagerAccount.v1_14_2.ManagerAccount", + "Id": "1", + "Name": "User Account", + "Description": "User Account", + "Enabled": true, + "Password": null, + "PasswordChangeRequired": false, + "AccountTypes": [ + "Redfish" + ], + "UserName": "Administrator", + "EmailAddress": "Tiffany@contoso.com", + "PhoneNumber": "312-555-1212", + "OneTimePasscodeDeliveryAddress": "3125551212@smsrelay.contoso.com", + "RoleId": "Administrator", + "Locked": false, + "Certificates": { + "@odata.id": "/redfish/v1/AccountService/Accounts/1/Certificates" + }, + "Links": { + "Role": { + "@odata.id": "/redfish/v1/AccountService/Roles/Administrator" + } + }, + "Actions": { + "#ManagerAccount.ChangePassword": { + "target": "/redfish/v1/AccountService/Accounts/1/Actions/ManagerAccount.ChangePassword" + } + }, + "@odata.id": "/redfish/v1/AccountService/Accounts/1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/2/index.json b/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/2/index.json new file mode 100644 index 0000000..24252ff --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/2/index.json @@ -0,0 +1,38 @@ +{ + "@odata.type": "#ManagerAccount.v1_14_2.ManagerAccount", + "Id": "2", + "Name": "Employee #457", + "Description": "This user is both a Redfish administrator and a SNMPv3 user.", + "Enabled": true, + "Password": null, + "UserName": "contoso_employee457", + "MFABypass": { + "BypassTypes": [ + "All" + ] + }, + "RoleId": "Administrator", + "Locked": false, + "AccountTypes": [ + "Redfish", + "SNMP" + ], + "SNMP": { + "AuthenticationKey": null, + "AuthenticationProtocol": "None", + "EncryptionKey": null, + "EncryptionProtocol": "CFB128_AES128" + }, + "Links": { + "Role": { + "@odata.id": "/redfish/v1/AccountService/Roles/Administrator" + } + }, + "Actions": { + "#ManagerAccount.ChangePassword": { + "target": "/redfish/v1/AccountService/Accounts/2/Actions/ManagerAccount.ChangePassword" + } + }, + "@odata.id": "/redfish/v1/AccountService/Accounts/2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/index.json b/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/index.json new file mode 100644 index 0000000..f509d47 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/AccountService/Accounts/index.json @@ -0,0 +1,12 @@ +{ + "@odata.type": "#ManagerAccountCollection.ManagerAccountCollection", + "Name": "Accounts Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/AccountService/Accounts/1" + } + ], + "@odata.id": "/redfish/v1/AccountService/Accounts", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/AccountService/index.json b/tests/fixtures/dmtf-public-rackmount1/AccountService/index.json new file mode 100644 index 0000000..e21b775 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/AccountService/index.json @@ -0,0 +1,160 @@ +{ + "@odata.type": "#AccountService.v1_19_0.AccountService", + "Id": "AccountService", + "Name": "Account Service", + "Description": "Account Service", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ServiceEnabled": true, + "AuthFailureLoggingThreshold": 3, + "MinPasswordLength": 8, + "AccountLockoutThreshold": 5, + "AccountLockoutDuration": 30, + "AccountLockoutCounterResetAfter": 30, + "AccountLockoutCounterResetEnabled": true, + "EnforcePasswordHistoryCount": 10, + "Accounts": { + "@odata.id": "/redfish/v1/AccountService/Accounts" + }, + "Roles": { + "@odata.id": "/redfish/v1/AccountService/Roles" + }, + "LocalAccountAuth": "Enabled", + "MultiFactorAuth": { + "OneTimePasscode": { + "Enabled": true + } + }, + "LDAP": { + "AccountProviderType": "LDAPService", + "ServiceEnabled": false, + "ServiceAddresses": [ + "ldaps://ldap.example.org:636" + ], + "Authentication": { + "AuthenticationType": "UsernameAndPassword", + "Username": "cn=Manager,dc=example,dc=org", + "Password": null + }, + "LDAPService": { + "SearchSettings": { + "BaseDistinguishedNames": [ + "dc=example,dc=org" + ], + "UsernameAttribute": "uid", + "GroupsAttribute": "memberof", + "EmailAttribute": "mail" + } + }, + "RemoteRoleMapping": [ + { + "RemoteUser": "cn=Manager,dc=example,dc=org", + "LocalRole": "Administrator", + "LocalAccountTypes": [ + "Redfish", + "HostConsole", + "ManagerConsole" + ] + }, + { + "RemoteGroup": "cn=Admins,ou=Groups,dc=example,dc=org", + "LocalRole": "Administrator", + "LocalAccountTypes": [ + "Redfish", + "HostConsole", + "ManagerConsole" + ] + }, + { + "RemoteGroup": "cn=PowerUsers,ou=Groups,dc=example,dc=org", + "LocalRole": "Operator", + "LocalAccountTypes": [ + "Redfish", + "HostConsole" + ] + }, + { + "RemoteGroup": "(cn=*)", + "LocalRole": "ReadOnly", + "LocalAccountTypes": [ + "Redfish" + ] + } + ] + }, + "ActiveDirectory": { + "AccountProviderType": "ActiveDirectoryService", + "ServiceEnabled": true, + "ServiceAddresses": [ + "ad1.example.org", + "ad2.example.org", + null, + null + ], + "Authentication": { + "AuthenticationType": "KerberosKeytab", + "KerberosKeytab": null + }, + "RemoteRoleMapping": [ + { + "RemoteGroup": "Administrators", + "LocalRole": "Administrator", + "LocalAccountTypes": [ + "Redfish", + "HostConsole", + "ManagerConsole" + ] + }, + { + "RemoteUser": "DOMAIN\\Bob", + "LocalRole": "Operator", + "LocalAccountTypes": [ + "Redfish", + "HostConsole" + ] + }, + { + "RemoteGroup": "PowerUsers", + "LocalRole": "Operator", + "LocalAccountTypes": [ + "Redfish", + "HostConsole" + ] + }, + { + "RemoteGroup": "Everybody", + "LocalRole": "ReadOnly", + "LocalAccountTypes": [ + "Redfish" + ] + } + ] + }, + "OAuth2": { + "AccountProviderType": "OAuth2", + "ServiceEnabled": true, + "ServiceAddresses": [ + "https://contoso.org/.well-known/oauth-authorization-server" + ], + "OAuth2Service": { + "Mode": "Discovery", + "Issuer": "https://contoso.org/services/oauth2", + "Audience": [ + "23:11:8A:33:CF:EA", + "92384634-2938-2342-8820-489239905423" + ], + "OAuthServiceSigningKeys": "ewogICAgImtleXMiOiBbCiAgICAgICAgewogICAgICAgICAgICAiZSI6ICJBUUFCIiwKICAgICAgICAgICAgImtpZCI6ICJvV1F2Tl8tRUxnd25HMzFNTEl4Rm5hTVlfNTd0Wk9FSUVQYnpaYlBqS2FBIiwKICAgICAgICAgICAgImt0eSI6ICJSU0EiLAogICAgICAgICAgICAibiI6ICJvYS1Kb2pkSGw2VHQ3N2Vma2h6aW5MMU9XNmZjMGhjQjloOTN2WUZyRVlPakozRjhfTFh1c1ItMW95MUFKdExZcF9KeVI4Q1ZET28yVGNCaTBuSkhKczF1dkN4WWljX202WE5vMHptVm1VQ2hVMnBYSlBGelZKRGV5VVhIWWVONG5yQ1k4XzlTLXlzWnlKa2RRaWJmeHlBRndUMHdpNlZldF9MYTE4N3JNZUFCU0tXdEdIVmpkOWk1UmhELWNGU1JaNk05M0s2bXFPUUpaX0pMXzJKUU93aGk5dFl4Q2drVVpqc1gyWXowMEZpdUVTWDhHcXRGQlI1UzhIT2p0b2FhZUJrTk5hYWZFOFdSUVl6QlVmOHpiNkVobUFMemc0Z0YxTVhRb3dZV25GcGpzYXcwNVRONHk3SWZJUVhLODlqbGxnbWFkWVVjbndaUmRMc1o2VTdBS1EiCiAgICAgICAgfQogICAgXQp9" + } + }, + "AdditionalExternalAccountProviders": { + "@odata.id": "/redfish/v1/AccountService/ExternalAccountProviders" + }, + "OutboundConnections": { + "@odata.id": "/redfish/v1/AccountService/OutboundConnections" + }, + "RequireChangePasswordAction": false, + "@odata.id": "/redfish/v1/AccountService", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/COMMANDS.txt b/tests/fixtures/dmtf-public-rackmount1/COMMANDS.txt new file mode 100644 index 0000000..15a7aa7 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/COMMANDS.txt @@ -0,0 +1,66 @@ +Source: https://github.com/DMTF/Redfish-Publications +Commit: 4f81814e399213c9055863ddaff42791c6b3706a +Mockup: mockups/public-rackmount1 + +Files are unmodified copies. Each request path maps to /index.json +(the service root is ./index.json), as in the mockup and as served by Redfish-Mockup-Server. + +Request paths: +GET /redfish/v1/ +GET /redfish/v1/Systems +GET /redfish/v1/Systems/437XR1138R2 +GET /redfish/v1/Systems/437XR1138R2/Processors +GET /redfish/v1/Systems/437XR1138R2/Processors/CPU1 +GET /redfish/v1/Systems/437XR1138R2/Processors/CPU2 +GET /redfish/v1/Systems/437XR1138R2/Processors/FPGA1 +GET /redfish/v1/Systems/437XR1138R2/Memory +GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM1 +GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM2 +GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM3 +GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM4 +GET /redfish/v1/Systems/437XR1138R2/SimpleStorage +GET /redfish/v1/Systems/437XR1138R2/SimpleStorage/1 +GET /redfish/v1/Systems/437XR1138R2/LogServices +GET /redfish/v1/Systems/437XR1138R2/LogServices/Log1 +GET /redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries +GET /redfish/v1/Chassis +GET /redfish/v1/Chassis/1U +GET /redfish/v1/Chassis/1U/Thermal +GET /redfish/v1/Chassis/1U/Power +GET /redfish/v1/Chassis/1U/ThermalSubsystem +GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans +GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay1 +GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay2 +GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU1 +GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU2 +GET /redfish/v1/Chassis/1U/ThermalSubsystem/ThermalMetrics +GET /redfish/v1/Chassis/1U/PowerSubsystem +GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies +GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1 +GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics +GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2 +GET /redfish/v1/Chassis/1U/Sensors +GET /redfish/v1/Chassis/1U/Sensors/CPU1Temp +GET /redfish/v1/Chassis/1U/Sensors/IntakeTemp +GET /redfish/v1/Chassis/1U/Sensors/CPUFan1 +GET /redfish/v1/Chassis/1U/Sensors/TotalPower +GET /redfish/v1/Chassis/1U/Sensors/PS1InputVoltage +GET /redfish/v1/Managers +GET /redfish/v1/Managers/BMC +GET /redfish/v1/Managers/BMC/EthernetInterfaces +GET /redfish/v1/Managers/BMC/EthernetInterfaces/eth0 +GET /redfish/v1/Managers/BMC/EthernetInterfaces/ToHost +GET /redfish/v1/Managers/BMC/NetworkProtocol +GET /redfish/v1/Managers/BMC/LogServices +GET /redfish/v1/Managers/BMC/LogServices/Log +GET /redfish/v1/Managers/BMC/LogServices/Log/Entries +GET /redfish/v1/AccountService +GET /redfish/v1/AccountService/Accounts +GET /redfish/v1/AccountService/Accounts/1 +GET /redfish/v1/AccountService/Accounts/2 +GET /redfish/v1/UpdateService/FirmwareInventory +GET /redfish/v1/UpdateService/FirmwareInventory/BMC +GET /redfish/v1/UpdateService/FirmwareInventory/SS +GET /redfish/v1/UpdateService/FirmwareInventory/BIOS +GET /redfish/v1/SessionService +GET /redfish/v1/SessionService/Sessions diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Power/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Power/index.json new file mode 100644 index 0000000..a9af33c --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Power/index.json @@ -0,0 +1,139 @@ +{ + "@odata.type": "#Power.v1_7_3.Power", + "Id": "Power", + "Name": "Power", + "PowerControl": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Power#/PowerControl/0", + "MemberId": "0", + "Name": "System Input Power", + "PowerConsumedWatts": 344, + "PowerRequestedWatts": 800, + "PowerAvailableWatts": 0, + "PowerCapacityWatts": 800, + "PowerAllocatedWatts": 800, + "PowerMetrics": { + "IntervalInMin": 30, + "MinConsumedWatts": 271, + "MaxConsumedWatts": 489, + "AverageConsumedWatts": 319 + }, + "PowerLimit": { + "LimitInWatts": 500, + "LimitException": "LogEventOnly", + "CorrectionInMs": 50 + }, + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ], + "Status": { + "State": "Enabled", + "Health": "OK" + } + } + ], + "Voltages": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Power#/Voltages/0", + "MemberId": "0", + "Name": "VRM1 Voltage", + "SensorNumber": 11, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingVolts": 12, + "UpperThresholdNonCritical": 12.5, + "UpperThresholdCritical": 13, + "UpperThresholdFatal": 15, + "LowerThresholdNonCritical": 11.5, + "LowerThresholdCritical": 11, + "LowerThresholdFatal": 10, + "MinReadingRange": 0, + "MaxReadingRange": 20, + "PhysicalContext": "VoltageRegulator", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Power#/Voltages/1", + "MemberId": "1", + "Name": "VRM2 Voltage", + "SensorNumber": 12, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingVolts": 5, + "UpperThresholdNonCritical": 5.5, + "UpperThresholdCritical": 7, + "LowerThresholdNonCritical": 4.75, + "LowerThresholdCritical": 4.5, + "MinReadingRange": 0, + "MaxReadingRange": 20, + "PhysicalContext": "VoltageRegulator", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + } + ], + "PowerSupplies": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Power#/PowerSupplies/0", + "MemberId": "0", + "Name": "Power Supply Bay", + "Status": { + "State": "Enabled", + "Health": "Warning" + }, + "PowerSupplyType": "AC", + "LineInputVoltageType": "ACWideRange", + "LineInputVoltage": 120, + "PowerCapacityWatts": 800, + "LastPowerOutputWatts": 325, + "Model": "499253-B21", + "Manufacturer": "ManufacturerName", + "FirmwareVersion": "1.00", + "SerialNumber": "1Z0000001", + "PartNumber": "0000001A3A", + "SparePartNumber": "0000001A3A", + "InputRanges": [ + { + "InputType": "AC", + "MinimumVoltage": 100, + "MaximumVoltage": 120, + "OutputWattage": 800 + }, + { + "InputType": "AC", + "MinimumVoltage": 200, + "MaximumVoltage": 240, + "OutputWattage": 1300 + } + ], + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Power", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics/index.json new file mode 100644 index 0000000..9b70c74 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics/index.json @@ -0,0 +1,85 @@ +{ + "@odata.type": "#PowerSupplyMetrics.v1_3_0.PowerSupplyMetrics", + "Id": "Metrics", + "Name": "Metrics for Power Supply 1", + "Status": { + "State": "Enabled", + "Health": "Warning" + }, + "InputVoltage": { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1InputVoltage", + "Reading": 230.2 + }, + "InputCurrentAmps": { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1InputCurrent", + "Reading": 5.19 + }, + "InputPowerWatts": { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1InputPower", + "Reading": 937.4 + }, + "RailVoltage": [ + { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1Out3V", + "Reading": 3.31 + }, + { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1Out5V", + "Reading": 5.03 + }, + { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1Out12V", + "Reading": 12.06 + } + ], + "RailCurrentAmps": [ + { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1Out3VCurrent", + "Reading": 9.84 + }, + { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1Out5VCurrent", + "Reading": 1.25 + }, + { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1Out12VCurrent", + "Reading": 2.58 + } + ], + "OutputPowerWatts": { + "Reading": 937.4 + }, + "RailPowerWatts": [ + { + "Reading": 79.84 + }, + { + "Reading": 26.25 + }, + { + "Reading": 91.58 + } + ], + "EnergykWh": { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1Energy", + "Reading": 325675 + }, + "FrequencyHz": { + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/PS1Frequency", + "Reading": 60 + }, + "TemperatureCelsius": { + "Reading": 43.9 + }, + "FanSpeedPercent": { + "Reading": 68, + "SpeedRPM": 3290 + }, + "Actions": { + "#PowerSupplyMetrics.ResetMetrics": { + "target": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics/Actions/PowerSupplyMetrics.ResetMetrics" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/index.json new file mode 100644 index 0000000..e63a0b4 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/index.json @@ -0,0 +1,99 @@ +{ + "@odata.type": "#PowerSupply.v1_8_0.PowerSupply", + "Id": "Bay1", + "Name": "Power Supply Bay 1", + "Status": { + "State": "Enabled", + "Health": "Warning", + "Conditions": [ + { + "MessageId": "Power.1.0.PowerSupplyPredictiveFailure", + "Timestamp": "2023-06-08T12:25:00-05:00", + "Message": "Power supply 'PSU 1' has a predicted failure condition.", + "Severity": "Warning", + "MessageArgs": [ + "PSU 1" + ] + } + ] + }, + "LineInputStatus": "Normal", + "Model": "RKS-440DC", + "Manufacturer": "Contoso Power", + "FirmwareVersion": "1.00", + "SerialNumber": "3488247", + "PartNumber": "23456-133", + "SparePartNumber": "93284-133", + "LocationIndicatorActive": false, + "HotPluggable": false, + "PowerCapacityWatts": 400, + "PhaseWiringType": "OnePhase3Wire", + "PlugType": "IEC_60320_C14", + "InputRanges": [ + { + "NominalVoltageType": "AC200To240V", + "CapacityWatts": 400 + }, + { + "NominalVoltageType": "AC120V", + "CapacityWatts": 350 + }, + { + "NominalVoltageType": "DC380V", + "CapacityWatts": 400 + } + ], + "EfficiencyRatings": [ + { + "LoadPercent": 25, + "EfficiencyPercent": 75 + }, + { + "LoadPercent": 50, + "EfficiencyPercent": 85 + }, + { + "LoadPercent": 90, + "EfficiencyPercent": 80 + } + ], + "OutputRails": [ + { + "NominalVoltage": 3.3, + "PhysicalContext": "SystemBoard" + }, + { + "NominalVoltage": 5, + "PhysicalContext": "SystemBoard" + }, + { + "NominalVoltage": 12, + "PhysicalContext": "StorageDevice" + } + ], + "Location": { + "PartLocation": { + "ServiceLabel": "PSU 1", + "LocationType": "Bay", + "LocationOrdinalValue": 0 + } + }, + "Links": { + "Outlet": { + "@odata.id": "https://redfishpdu.contoso.com/redfish/v1/PowerEquipment/RackPDUs/1/Outlets/A4" + } + }, + "Assembly": { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Assembly" + }, + "Metrics": { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics" + }, + "Actions": { + "#PowerSupply.Reset": { + "target": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Actions/PowerSupply.Reset" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2/index.json new file mode 100644 index 0000000..b8b4f07 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2/index.json @@ -0,0 +1,19 @@ +{ + "@odata.type": "#PowerSupply.v1_8_0.PowerSupply", + "Id": "Bay2", + "Name": "Power Supply Bay 2", + "Status": { + "State": "Absent" + }, + "PowerSupplyType": "PSU", + "HotPluggable": false, + "Location": { + "PartLocation": { + "ServiceLabel": "PSU 2", + "LocationType": "Bay", + "LocationOrdinalValue": 1 + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/index.json new file mode 100644 index 0000000..9c87502 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/PowerSupplies/index.json @@ -0,0 +1,15 @@ +{ + "@odata.type": "#PowerSupplyCollection.PowerSupplyCollection", + "Name": "Power Supply Collection", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2" + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/index.json new file mode 100644 index 0000000..e971aa0 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/PowerSubsystem/index.json @@ -0,0 +1,47 @@ +{ + "@odata.type": "#PowerSubsystem.v1_2_0.PowerSubsystem", + "Id": "PowerSubsystem", + "Name": "Power Subsystem for Chassis", + "CapacityWatts": 2000, + "Allocation": { + "RequestedWatts": 1500, + "AllocatedWatts": 1200 + }, + "PowerSupplyRedundancy": [ + { + "RedundancyType": "Failover", + "MaxSupportedInGroup": 2, + "MinNeededInGroup": 1, + "MinNeededForFaultTolerance": 2, + "RedundancyGroup": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2" + } + ], + "ActiveRedundancyGroup": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1" + } + ], + "Status": { + "State": "UnavailableOffline", + "Health": "OK" + } + } + ], + "PowerSupplies": { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies" + }, + "Batteries": { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem/Batteries" + }, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPU1Temp/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPU1Temp/index.json new file mode 100644 index 0000000..e77355e --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPU1Temp/index.json @@ -0,0 +1,83 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "CPU1Temp", + "Name": "CPU #1 Temperature", + "PhysicalContext": "CPU", + "Status": { + "State": "Enabled", + "Health": "Warning", + "Conditions": [ + { + "MessageId": "Sensor.1.0.ReadingAboveUpperCautionThreshold", + "Timestamp": "2024-08-12T12:25:00-05:00", + "Message": "Sensor 'CPU1 Temp' reading of 44 (Cel) is above the 42 upper caution threshold.", + "Severity": "Warning", + "MessageArgs": [ + "CPU1 Temp", + "44", + "Cel", + "42" + ] + } + ] + }, + "Reading": 44, + "ReadingUnits": "Cel", + "Thresholds": { + "UpperCaution": { + "Reading": 42, + "DwellTime": "PT5M", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "UpperCautionUser": { + "Reading": 40, + "DwellTime": "PT5M", + "HysteresisReading": 0, + "HysteresisDuration": "PT10M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperCritical": { + "Reading": 45, + "DwellTime": "PT1M", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "UpperCriticalUser": { + "Reading": 45, + "DwellTime": "PT10S", + "HysteresisReading": -1, + "HysteresisDuration": "PT5M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperFatal": { + "Reading": 50, + "DwellTime": "PT30S", + "Activation": "Increasing", + "@Redfish.WriteableProperties": [] + } + }, + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1" + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPU1Temp", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPUFan1/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPUFan1/index.json new file mode 100644 index 0000000..d49350f --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPUFan1/index.json @@ -0,0 +1,36 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "CPUFan1", + "Name": "CPU #1 Fan Speed", + "PhysicalContext": "CPU", + "Reading": 45, + "SpeedRPM": 1490, + "ReadingRangeMax": 100, + "ReadingRangeMin": 0, + "ReadingType": "Percent", + "ReadingUnits": "%", + "Status": { + "Health": "OK", + "State": "Enabled" + }, + "Thresholds": { + "LowerCaution": { + "Reading": 5, + "DwellTime": "PT30S", + "Activation": "Decreasing", + "HysteresisReading": 2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "LowerCritical": { + "Reading": 0, + "DwellTime": "PT10S", + "Activation": "Decreasing", + "HysteresisReading": 2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPUFan1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/IntakeTemp/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/IntakeTemp/index.json new file mode 100644 index 0000000..123cb98 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/IntakeTemp/index.json @@ -0,0 +1,16 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "IntakeTemp", + "Name": "Front Panel Intake Temperature", + "PhysicalContext": "Intake", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 24.8, + "ReadingUnits": "Cel", + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/IntakeTemp", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputVoltage/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputVoltage/index.json new file mode 100644 index 0000000..49358c1 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputVoltage/index.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1InputVoltage", + "Name": "Power Supply #1 Input Voltage", + "ReadingType": "Voltage", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ElectricalContext": "Total", + "Reading": 119.27, + "ReadingUnits": "V", + "ReadingRangeMin": 0, + "ReadingRangeMax": 260, + "Accuracy": 0.02, + "Precision": 2, + "SensingInterval": "PT0.125S", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Input", + "Thresholds": { + "UpperCritical": { + "Reading": 125, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 122, + "DwellTime": "PT10M" + }, + "LowerCaution": { + "Reading": 118, + "DwellTime": "PT5M" + }, + "LowerCritical": { + "Reading": 115, + "DwellTime": "PT1M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1InputVoltage", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/TotalPower/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/TotalPower/index.json new file mode 100644 index 0000000..010d1a2 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/TotalPower/index.json @@ -0,0 +1,53 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "TotalPower", + "Name": "Power reading for the Chassis", + "ReadingType": "Power", + "ElectricalContext": "Total", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 374, + "ReadingUnits": "W", + "Implementation": "Synthesized", + "ReadingTime": "2019-08-13T04:14:33+06:00", + "ReadingRangeMin": 0, + "ReadingRangeMax": 600, + "ApparentVA": 2749.2, + "ReactiveVAR": 281.4, + "PowerFactor": 0.99, + "Accuracy": 0.2, + "Precision": 1, + "SensingInterval": "PT0.25S", + "PhysicalContext": "Chassis", + "Thresholds": { + "UpperCritical": { + "Reading": 600, + "Activation": "Increasing", + "DwellTime": "PT1M", + "HysteresisReading": -10, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperCaution": { + "Reading": 580, + "DwellTime": "PT10M", + "HysteresisReading": -10, + "HysteresisDuration": "PT10M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/TotalPower", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/index.json new file mode 100644 index 0000000..df437b8 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/index.json @@ -0,0 +1,132 @@ +{ + "@odata.type": "#SensorCollection.SensorCollection", + "Name": "Chassis sensors", + "Members@odata.count": 41, + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/AmbientTemp" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPUFan1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPUFan2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPU1Temp" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/DIMM1Temp" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/DIMM2Temp" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/DIMM3Temp" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/ExhaustTemp" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/FanBay1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/FanBay2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/IntakeTemp" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Energy" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Frequency" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1InputCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1InputPower" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1InputVoltage" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out12V" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out12VCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out3V" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out3VCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out5V" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out5VCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Energy" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Frequency" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2InputCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2InputPower" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2InputVoltage" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out12V" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out12VCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out3V" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out3VCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out5V" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out5VCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/TotalEnergy" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/TotalPower" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1Temp" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1InputVoltage" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1InputCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1OutputVoltage" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1OutputCurrent" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1StateOfHealth" + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Sensors", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Thermal/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Thermal/index.json new file mode 100644 index 0000000..db748c5 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Thermal/index.json @@ -0,0 +1,159 @@ +{ + "@odata.type": "#Thermal.v1_7_3.Thermal", + "Id": "Thermal", + "Name": "Thermal", + "Temperatures": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Temperatures/0", + "MemberId": "0", + "Name": "CPU1 Temp", + "SensorNumber": 5, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingCelsius": 41, + "UpperThresholdNonCritical": 42, + "UpperThresholdCritical": 45, + "UpperThresholdFatal": 48, + "MinReadingRangeTemp": 0, + "MaxReadingRangeTemp": 60, + "PhysicalContext": "CPU", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1" + } + ] + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Temperatures/1", + "MemberId": "1", + "Name": "CPU2 Temp", + "SensorNumber": 6, + "Status": { + "State": "Disabled" + }, + "UpperThresholdNonCritical": 42, + "UpperThresholdCritical": 45, + "UpperThresholdFatal": 48, + "MinReadingRangeTemp": 0, + "MaxReadingRangeTemp": 60, + "PhysicalContext": "CPU", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU2" + } + ] + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Temperatures/2", + "MemberId": "2", + "Name": "Chassis Intake Temp", + "SensorNumber": 9, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingCelsius": 25, + "UpperThresholdNonCritical": 30, + "UpperThresholdCritical": 40, + "UpperThresholdFatal": 50, + "LowerThresholdNonCritical": 10, + "LowerThresholdCritical": 5, + "LowerThresholdFatal": 0, + "MinReadingRangeTemp": 0, + "MaxReadingRangeTemp": 60, + "PhysicalContext": "Intake", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + } + ] + } + ], + "Fans": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Fans/0", + "MemberId": "0", + "Name": "BaseBoard System Fan", + "PhysicalContext": "Backplane", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 2100, + "ReadingUnits": "RPM", + "LowerThresholdFatal": 0, + "MinReadingRange": 0, + "MaxReadingRange": 5000, + "Redundancy": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Redundancy/0" + } + ], + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Fans/1", + "MemberId": "1", + "Name": "BaseBoard System Fan Backup", + "PhysicalContext": "Backplane", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 2050, + "ReadingUnits": "RPM", + "LowerThresholdFatal": 0, + "MinReadingRange": 0, + "MaxReadingRange": 5000, + "Redundancy": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Redundancy/0" + } + ], + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ] + } + ], + "Redundancy": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Redundancy/0", + "MemberId": "0", + "Name": "BaseBoard System Fans", + "RedundancySet": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Fans/0" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal#/Fans/1" + } + ], + "Mode": "N+m", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "MinNumNeeded": 1, + "MaxNumSupported": 2 + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Thermal", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/Bay1/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/Bay1/index.json new file mode 100644 index 0000000..330c1db --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/Bay1/index.json @@ -0,0 +1,31 @@ +{ + "@odata.type": "#Fan.v1_6_0.Fan", + "Id": "Bay1", + "Name": "Fan Bay 1", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "PhysicalContext": "Chassis", + "Model": "RKS-440DC", + "Manufacturer": "Contoso Fans", + "PartNumber": "23456-133", + "SparePartNumber": "93284-133", + "LocationIndicatorActive": true, + "HotPluggable": true, + "SpeedPercent": { + "Reading": 45, + "SpeedRPM": 2200, + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/FanBay1" + }, + "RatedSpeedRPM": 4900, + "Location": { + "PartLocation": { + "ServiceLabel": "Chassis Fan Bay 1", + "LocationType": "Bay", + "LocationOrdinalValue": 0 + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/Bay2/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/Bay2/index.json new file mode 100644 index 0000000..3860deb --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/Bay2/index.json @@ -0,0 +1,31 @@ +{ + "@odata.type": "#Fan.v1_6_0.Fan", + "Id": "Bay2", + "Name": "Fan Bay 2", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "PhysicalContext": "Chassis", + "Model": "RKS-440DC", + "Manufacturer": "Contoso Fans", + "PartNumber": "23456-133", + "SparePartNumber": "93284-133", + "LocationIndicatorActive": false, + "HotPluggable": true, + "SpeedPercent": { + "Reading": 45, + "SpeedRPM": 2400, + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/FanBay2" + }, + "RatedSpeedRPM": 4900, + "Location": { + "PartLocation": { + "ServiceLabel": "Chassis Fan Bay 2", + "LocationType": "Bay", + "LocationOrdinalValue": 1 + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/CPU1/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/CPU1/index.json new file mode 100644 index 0000000..9c31445 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/CPU1/index.json @@ -0,0 +1,31 @@ +{ + "@odata.type": "#Fan.v1_6_0.Fan", + "Id": "CPU1", + "Name": "Fan for CPU 1", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "PhysicalContext": "CPU", + "Model": "RKS-440DC", + "Manufacturer": "Contoso Fans", + "PartNumber": "23456-133", + "SparePartNumber": "93284-133", + "LocationIndicatorActive": false, + "HotPluggable": false, + "SpeedPercent": { + "Reading": 45, + "SpeedRPM": 1490, + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/CPUFan1" + }, + "RatedSpeedRPM": 3300, + "Location": { + "PartLocation": { + "ServiceLabel": "CPU #1 Fan", + "LocationType": "Bay", + "LocationOrdinalValue": 0 + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/CPU2/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/CPU2/index.json new file mode 100644 index 0000000..06bdb40 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/CPU2/index.json @@ -0,0 +1,31 @@ +{ + "@odata.type": "#Fan.v1_6_0.Fan", + "Id": "CPU2", + "Name": "Fan for CPU 2", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "PhysicalContext": "CPU", + "Model": "RKS-440DC", + "Manufacturer": "Contoso Fans", + "PartNumber": "23456-133", + "SparePartNumber": "93284-133", + "LocationIndicatorActive": false, + "HotPluggable": false, + "SpeedPercent": { + "Reading": 45, + "SpeedRPM": 1490, + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/CPUFan2" + }, + "RatedSpeedRPM": 3300, + "Location": { + "PartLocation": { + "ServiceLabel": "CPU #2 Fan", + "LocationType": "Bay", + "LocationOrdinalValue": 1 + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/index.json new file mode 100644 index 0000000..a8de534 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/Fans/index.json @@ -0,0 +1,21 @@ +{ + "@odata.type": "#FanCollection.FanCollection", + "Name": "Fan Club", + "Members@odata.count": 4, + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay2" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU2" + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/ThermalMetrics/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/ThermalMetrics/index.json new file mode 100644 index 0000000..abac31b --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/ThermalMetrics/index.json @@ -0,0 +1,37 @@ +{ + "@odata.type": "#ThermalMetrics.v1_3_3.ThermalMetrics", + "Id": "ThermalMetrics", + "Name": "Chassis Thermal Metrics", + "TemperatureSummaryCelsius": { + "Internal": { + "Reading": 39, + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/CPU1Temp" + }, + "Intake": { + "Reading": 24.8, + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/IntakeTemp" + }, + "Ambient": { + "Reading": 22.5, + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/AmbientTemp" + }, + "Exhaust": { + "Reading": 40.5, + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/ExhaustTemp" + } + }, + "TemperatureReadingsCelsius": [ + { + "Reading": 24.8, + "DeviceName": "Intake", + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/IntakeTemp" + }, + { + "Reading": 40.5, + "DeviceName": "Exhaust", + "DataSourceUri": "/redfish/v1/Chassis/1U/Sensors/ExhaustTemp" + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/ThermalMetrics", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/index.json new file mode 100644 index 0000000..53bcf67 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/ThermalSubsystem/index.json @@ -0,0 +1,57 @@ +{ + "@odata.type": "#ThermalSubsystem.v1_5_0.ThermalSubsystem", + "Id": "ThermalSubsystem", + "Name": "Thermal Subsystem for Chassis", + "FanRedundancy": [ + { + "RedundancyType": "NPlusM", + "MaxSupportedInGroup": 2, + "MinNeededInGroup": 1, + "MinNeededForFaultTolerance": 2, + "RedundancyGroup": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay2" + } + ], + "Status": { + "State": "Enabled", + "Health": "OK" + } + }, + { + "RedundancyType": "NPlusM", + "MaxSupportedInGroup": 2, + "MinNeededInGroup": 1, + "MinNeededForFaultTolerance": 2, + "RedundancyGroup": [ + { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU1" + }, + { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU2" + } + ], + "Status": { + "State": "Disabled" + } + } + ], + "Fans": { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Fans" + }, + "ThermalMetrics": { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/ThermalMetrics" + }, + "Heaters": { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem/Heaters" + }, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/index.json new file mode 100644 index 0000000..35dae85 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/index.json @@ -0,0 +1,104 @@ +{ + "@odata.type": "#Chassis.v1_29_0.Chassis", + "Id": "1U", + "Name": "Computer System Chassis", + "ChassisType": "RackMount", + "AssetTag": "Portland-45Z-2381", + "Manufacturer": "Contoso", + "Model": "3500RX", + "SKU": "8675309", + "SerialNumber": "437XR1138R2", + "PartNumber": "224071-J23", + "PowerState": "On", + "LocationIndicatorActive": true, + "RackUnits": "EIA_310", + "HeightRackUnits": 1, + "HeightMm": 44.45, + "WidthMm": 431.8, + "DepthMm": 711, + "WeightKg": 15.31, + "Location": { + "PhysicalAddress": { + "Country": "US", + "StateOrProvince": "Oregon", + "City": "Portland", + "StreetAddress": "1001 SW 5th Avenue", + "PostalCode": "97204", + "ISOCountryCode": "USA", + "ISOSubdivisionCode": "OR" + }, + "Placement": { + "Row": "North", + "Rack": "WEB43", + "RackOffsetUnits": "EIA_310", + "RackOffset": 12, + "Room": "35", + "FacilityName": "Onsite-Main" + } + }, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ThermalSubsystem": { + "@odata.id": "/redfish/v1/Chassis/1U/ThermalSubsystem" + }, + "PowerSubsystem": { + "@odata.id": "/redfish/v1/Chassis/1U/PowerSubsystem" + }, + "EnvironmentMetrics": { + "@odata.id": "/redfish/v1/Chassis/1U/EnvironmentMetrics" + }, + "Sensors": { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors" + }, + "Controls": { + "@odata.id": "/redfish/v1/Chassis/1U/Controls" + }, + "TrustedComponents": { + "@odata.id": "/redfish/v1/Chassis/1U/TrustedComponents" + }, + "Thermal@Redfish.Deprecated": "Please migrate to use /redfish/v1/Chassis/1U/ThermalSubsystem", + "Thermal": { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal" + }, + "Power@Redfish.Deprecated": "Please migrate to use /redfish/v1/Chassis/1U/PowerSubsystem", + "Power": { + "@odata.id": "/redfish/v1/Chassis/1U/Power" + }, + "Links": { + "ComputerSystems": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ], + "ManagersInChassis": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ] + }, + "Actions": { + "#Chassis.Reset": { + "target": "/redfish/v1/Chassis/1U/Actions/Chassis.Reset", + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "GracefulShutdown", + "GracefulRestart", + "ForceRestart", + "Nmi", + "ForceOn", + "PushPowerButton", + "FullPowerCycle" + ] + } + }, + "@odata.id": "/redfish/v1/Chassis/1U", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/index.json new file mode 100644 index 0000000..9066a5e --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/index.json @@ -0,0 +1,12 @@ +{ + "@odata.type": "#ChassisCollection.ChassisCollection", + "Name": "Chassis Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ], + "@odata.id": "/redfish/v1/Chassis", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/LICENSE.md b/tests/fixtures/dmtf-public-rackmount1/LICENSE.md new file mode 100644 index 0000000..231cf4a --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2014-2025, Contributing Member(s) of Distributed Management Task +Force, Inc.. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/ToHost/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/ToHost/index.json new file mode 100644 index 0000000..9b3cffd --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/ToHost/index.json @@ -0,0 +1,57 @@ +{ + "@odata.type": "#EthernetInterface.v1_12_6.EthernetInterface", + "Id": "ToHost", + "Name": "Manager Ethernet Interface", + "Description": "Management Network Interface", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "InterfaceEnabled": true, + "PermanentMACAddress": "AA:BB:CC:DD:EE:FF", + "MACAddress": "AA:BB:CC:DD:EE:FF", + "SpeedMbps": 100, + "AutoNeg": true, + "FullDuplex": true, + "MTUSize": 1500, + "HostName": "bmc-host-interface", + "FQDN": "bmc-host-interface.dmtf.org", + "MaxIPv6StaticAddresses": 1, + "IPv4Addresses": [ + { + "Address": "192.168.20.55", + "SubnetMask": "255.255.255.0", + "AddressOrigin": "Static", + "Gateway": "192.168.20.1" + } + ], + "IPv6AddressPolicyTable": [ + { + "Prefix": "::1/128", + "Precedence": 50, + "Label": 0 + } + ], + "IPv6StaticAddresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e24", + "PrefixLength": 64 + } + ], + "IPv6DefaultGateway": "fe80::1ec1:deff:fe6f:1e24", + "IPv6Addresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e24", + "PrefixLength": 64, + "AddressOrigin": "Static", + "AddressState": "Preferred" + } + ], + "Links": { + "HostInterface": { + "@odata.id": "/redfish/v1/Managers/BMC/HostInterfaces/1" + } + }, + "@odata.id": "/redfish/v1/Managers/BMC/EthernetInterfaces/ToHost", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/eth0/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/eth0/index.json new file mode 100644 index 0000000..e74eba6 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/eth0/index.json @@ -0,0 +1,83 @@ +{ + "@odata.type": "#EthernetInterface.v1_12_6.EthernetInterface", + "Id": "eth0", + "Name": "Manager Ethernet Interface", + "Description": "Management network interface on a dedicated port", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "LinkStatus": "LinkUp", + "InterfaceEnabled": true, + "PermanentMACAddress": "23:11:8A:33:CF:EA", + "MACAddress": "23:11:8A:33:CF:EA", + "SpeedMbps": 100, + "AutoNeg": true, + "FullDuplex": true, + "MTUSize": 1500, + "HostName": "web483-bmc", + "FQDN": "web483-bmc.dmtf.org", + "MaxIPv6StaticAddresses": 1, + "VLAN": { + "VLANEnable": true, + "VLANId": 101 + }, + "IPv4Addresses": [ + { + "Address": "192.168.0.10", + "SubnetMask": "255.255.252.0", + "AddressOrigin": "DHCP", + "Gateway": "192.168.0.1" + } + ], + "IPv6AddressPolicyTable": [ + { + "Prefix": "::1/128", + "Precedence": 50, + "Label": 0 + } + ], + "IPv6StaticAddresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e24", + "PrefixLength": 16 + } + ], + "IPv6DefaultGateway": "fe80::1ec1:deff:fe6f:1e24", + "IPv6Addresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e24", + "PrefixLength": 64, + "AddressOrigin": "SLAAC", + "AddressState": "Preferred" + } + ], + "NameServers": [ + "names.dmtf.org" + ], + "Links": { + "Ports": [ + { + "@odata.id": "/redfish/v1/Managers/BMC/DedicatedNetworkPorts/1" + } + ] + }, + "@Redfish.Settings": { + "@odata.type": "#Settings.v1_5_0.Settings", + "SettingsObject": { + "@odata.id": "/redfish/v1/Managers/BMC/EthernetInterfaces/eth0/SD" + }, + "Time": "2012-03-07T14:44.30-05:00", + "ETag": "84ffcbb050ddc7fa9cddb59014546e59", + "Messages": [ + { + "MessageId": "Base.1.0.SettingsFailed", + "RelatedProperties": [ + "/IPv6Addresses/PrefixLength" + ] + } + ] + }, + "@odata.id": "/redfish/v1/Managers/BMC/EthernetInterfaces/eth0", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/index.json new file mode 100644 index 0000000..bfd84db --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/EthernetInterfaces/index.json @@ -0,0 +1,16 @@ +{ + "@odata.type": "#EthernetInterfaceCollection.EthernetInterfaceCollection", + "Name": "Ethernet Network Interface Collection", + "Description": "Collection of EthernetInterfaces for this Manager", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/BMC/EthernetInterfaces/eth0" + }, + { + "@odata.id": "/redfish/v1/Managers/BMC/EthernetInterfaces/ToHost" + } + ], + "@odata.id": "/redfish/v1/Managers/BMC/EthernetInterfaces", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/Log/Entries/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/Log/Entries/index.json new file mode 100644 index 0000000..83b2e53 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/Log/Entries/index.json @@ -0,0 +1,30 @@ +{ + "@odata.type": "#LogEntryCollection.LogEntryCollection", + "Name": "Log Service Collection", + "Description": "Collection of Logs for this System", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/BMC/LogServices/Log/Entries/1", + "@odata.type": "#LogEntry.v1_22_0.LogEntry", + "Id": "1", + "Name": "Log Entry 1", + "EntryType": "Event", + "Severity": "Critical", + "Created": "2012-03-07T14:44:00Z", + "SensorNumber": 1, + "Message": "System May be Melting", + "MessageId": "Event.1.0.TempWayTooHot", + "MessageArgs": [ + "88" + ], + "Links": { + "OriginOfCondition": { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal" + } + } + } + ], + "@odata.id": "/redfish/v1/Managers/BMC/LogServices/Log/Entries", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/Log/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/Log/index.json new file mode 100644 index 0000000..6d2dbdf --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/Log/index.json @@ -0,0 +1,24 @@ +{ + "@odata.type": "#LogService.v1_9_0.LogService", + "Id": "Log", + "Name": "System Log Service", + "MaxNumberOfRecords": 1000, + "OverWritePolicy": "WrapsWhenFull", + "DateTime": "2015-03-13T04:14:33+06:00", + "DateTimeLocalOffset": "+06:00", + "ServiceEnabled": true, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Actions": { + "#LogService.ClearLog": { + "target": "/redfish/v1/Managers/BMC/LogServices/Log/Actions/LogService.ClearLog" + } + }, + "Entries": { + "@odata.id": "/redfish/v1/Managers/BMC/LogServices/Log/Entries" + }, + "@odata.id": "/redfish/v1/Managers/BMC/LogServices/Log", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/index.json new file mode 100644 index 0000000..0de6f42 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/LogServices/index.json @@ -0,0 +1,13 @@ +{ + "@odata.type": "#LogServiceCollection.LogServiceCollection", + "Name": "Log Service Collection", + "Description": "Collection of Log Services for this Manager", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/BMC/LogServices/Log" + } + ], + "@odata.id": "/redfish/v1/Managers/BMC/LogServices", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/NetworkProtocol/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/NetworkProtocol/index.json new file mode 100644 index 0000000..0444ca2 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/NetworkProtocol/index.json @@ -0,0 +1,87 @@ +{ + "@odata.type": "#ManagerNetworkProtocol.v1_14_0.ManagerNetworkProtocol", + "Id": "NetworkProtocol", + "Name": "Manager Network Protocol", + "Description": "Manager Network Service", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "HostName": "web483-bmc", + "FQDN": "web483-bmc.dmtf.org", + "HTTP": { + "ProtocolEnabled": true, + "Port": 80 + }, + "HTTPS": { + "ProtocolEnabled": true, + "Port": 443, + "Certificates": { + "@odata.id": "/redfish/v1/Managers/BMC/NetworkProtocol/HTTPS/Certificates" + } + }, + "Proxy": { + "Enabled": true, + "ProxyServerURI": "https://proxy.intranet.contoso.com:9900", + "Username": "proxy", + "Password": null, + "PasswordSet": true, + "ExcludeAddresses": [], + "ProxyAutoConfigURI": "proxy.intranet.contoso.com/proxy.pac" + }, + "IPMI": { + "ProtocolEnabled": true, + "Port": 623 + }, + "SSH": { + "ProtocolEnabled": true, + "Port": 22 + }, + "SNMP": { + "ProtocolEnabled": true, + "EnableSNMPv1": true, + "EnableSNMPv2c": false, + "EnableSNMPv3": true, + "Port": 161, + "CommunityStrings": [ + { + "Name": "Read-only community", + "CommunityString": null, + "AccessMode": "Limited" + }, + { + "Name": "Full access community", + "CommunityString": null, + "AccessMode": "Full" + } + ], + "EngineId": { + "PrivateEnterpriseId": "00 00 01 9C", + "EnterpriseSpecificMethod": "00 11 22 33 44 55 66 77" + }, + "HideCommunityStrings": true, + "AuthenticationProtocol": "HMAC_SHA96", + "EncryptionProtocol": "CFB128_AES128" + }, + "VirtualMedia": { + "ProtocolEnabled": true, + "Port": 17988 + }, + "SSDP": { + "ProtocolEnabled": true, + "Port": 1900, + "NotifyMulticastIntervalSeconds": 600, + "NotifyTTL": 5, + "NotifyIPv6Scope": "Site" + }, + "Telnet": { + "ProtocolEnabled": true, + "Port": 23 + }, + "KVMIP": { + "ProtocolEnabled": true, + "Port": 5288 + }, + "@odata.id": "/redfish/v1/Managers/BMC/NetworkProtocol", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/index.json new file mode 100644 index 0000000..52003b4 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/BMC/index.json @@ -0,0 +1,100 @@ +{ + "@odata.type": "#Manager.v1_25_0.Manager", + "Id": "BMC", + "Name": "Manager", + "ManagerType": "BMC", + "Description": "Contoso BMC", + "ServiceEntryPointUUID": "92384634-2938-2342-8820-489239905423", + "UUID": "58893887-8974-2487-2389-841168418919", + "Model": "Joo Janta 200", + "DateTime": "2015-03-13T04:14:33+06:00", + "DateTimeLocalOffset": "+06:00", + "DateTimeSource": "NTP", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "PowerState": "On", + "GraphicalConsole": { + "ServiceEnabled": true, + "MaxConcurrentSessions": 2, + "ConnectTypesSupported": [ + "KVMIP" + ] + }, + "SerialConsole": { + "ServiceEnabled": true, + "MaxConcurrentSessions": 1, + "ConnectTypesSupported": [ + "Telnet", + "SSH", + "IPMI" + ] + }, + "CommandShell": { + "ServiceEnabled": true, + "MaxConcurrentSessions": 4, + "ConnectTypesSupported": [ + "Telnet", + "SSH" + ] + }, + "FirmwareVersion": "1.45.455b66-rev4", + "AdditionalFirmwareVersions": { + "Bootloader": "v2022.01", + "Kernel": "Linux 5.13.0-30-generic arm71", + "Oem": { + "Contoso": { + "@odata.type": "#ContosoSoftwareInventory.v1_0_0.AdditionalVersions", + "ManagementApp": "1.30" + } + } + }, + "NetworkProtocol": { + "@odata.id": "/redfish/v1/Managers/BMC/NetworkProtocol" + }, + "EthernetInterfaces": { + "@odata.id": "/redfish/v1/Managers/BMC/EthernetInterfaces" + }, + "HostInterfaces": { + "@odata.id": "/redfish/v1/Managers/BMC/HostInterfaces" + }, + "SerialInterfaces": { + "@odata.id": "/redfish/v1/Managers/BMC/SerialInterfaces" + }, + "LogServices": { + "@odata.id": "/redfish/v1/Managers/BMC/LogServices" + }, + "DedicatedNetworkPorts": { + "@odata.id": "/redfish/v1/Managers/BMC/DedicatedNetworkPorts" + }, + "SecurityPolicy": { + "@odata.id": "/redfish/v1/Managers/BMC/SecurityPolicy" + }, + "Links": { + "ManagerForServers": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + } + ], + "ManagerForChassis": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ], + "ManagerInChassis": { + "@odata.id": "/redfish/v1/Chassis/1U" + } + }, + "Actions": { + "#Manager.Reset": { + "target": "/redfish/v1/Managers/BMC/Actions/Manager.Reset", + "ResetType@Redfish.AllowableValues": [ + "ForceRestart", + "GracefulRestart" + ] + } + }, + "@odata.id": "/redfish/v1/Managers/BMC", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Managers/index.json b/tests/fixtures/dmtf-public-rackmount1/Managers/index.json new file mode 100644 index 0000000..2de8374 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Managers/index.json @@ -0,0 +1,12 @@ +{ + "@odata.type": "#ManagerCollection.ManagerCollection", + "Name": "Manager Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ], + "@odata.id": "/redfish/v1/Managers", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/SessionService/Sessions/index.json b/tests/fixtures/dmtf-public-rackmount1/SessionService/Sessions/index.json new file mode 100644 index 0000000..c89fdfa --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/SessionService/Sessions/index.json @@ -0,0 +1,15 @@ +{ + "@odata.type": "#SessionCollection.SessionCollection", + "Name": "Session Collection", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/SessionService/Sessions/1234567890ABCDEF" + }, + { + "@odata.id": "/redfish/v1/SessionService/Sessions/1234567890ABCDEG" + } + ], + "@odata.id": "/redfish/v1/SessionService/Sessions", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/SessionService/index.json b/tests/fixtures/dmtf-public-rackmount1/SessionService/index.json new file mode 100644 index 0000000..27d658e --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/SessionService/index.json @@ -0,0 +1,19 @@ +{ + "@odata.type": "#SessionService.v1_2_0.SessionService", + "Id": "SessionService", + "Name": "Session Service", + "Description": "Session Service", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ServiceEnabled": true, + "SessionTimeout": 30, + "AbsoluteSessionTimeout": 3600, + "AbsoluteSessionTimeoutEnabled": true, + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + }, + "@odata.id": "/redfish/v1/SessionService", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/Log1/Entries/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/Log1/Entries/index.json new file mode 100644 index 0000000..50c62a5 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/Log1/Entries/index.json @@ -0,0 +1,49 @@ +{ + "@odata.type": "#LogEntryCollection.LogEntryCollection", + "Name": "Log Service Collection", + "Description": "Collection of Logs for this System", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries/1", + "@odata.type": "#LogEntry.v1_22_0.LogEntry", + "Id": "1", + "Name": "Log Entry 1", + "EntryType": "SEL", + "Severity": "Critical", + "Created": "2012-03-07T14:44:00Z", + "EntryCode": "Upper Critical - going high", + "SensorType": "Temperature", + "SensorNumber": 1, + "Message": "Temperature threshold exceeded", + "MessageId": "0x592A28", + "Links": { + "OriginOfCondition": { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal" + } + } + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries/2", + "@odata.type": "#LogEntry.v1_22_0.LogEntry", + "Id": "2", + "Name": "Log Entry 2", + "EntryType": "SEL", + "Severity": "Critical", + "Created": "2012-03-07T14:45:00Z", + "EntryCode": "Upper Critical - going high", + "SensorType": "Temperature", + "SensorNumber": 2, + "Message": "Temperature threshold exceeded", + "MessageId": "0x592E28", + "Links": { + "OriginOfCondition": { + "@odata.id": "/redfish/v1/Chassis/1U/Thermal" + } + } + } + ], + "@odata.nextLink": "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries?$skiptoken=2", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/Log1/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/Log1/index.json new file mode 100644 index 0000000..4a80e14 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/Log1/index.json @@ -0,0 +1,24 @@ +{ + "@odata.type": "#LogService.v1_9_0.LogService", + "Id": "Log1", + "Name": "System Log Service", + "MaxNumberOfRecords": 1000, + "OverWritePolicy": "WrapsWhenFull", + "DateTime": "2015-03-13T04:14:33+06:00", + "DateTimeLocalOffset": "+06:00", + "ServiceEnabled": true, + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Actions": { + "#LogService.ClearLog": { + "target": "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Actions/LogService.ClearLog" + } + }, + "Entries": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries" + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices/Log1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/index.json new file mode 100644 index 0000000..fb9f651 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/LogServices/index.json @@ -0,0 +1,13 @@ +{ + "@odata.type": "#LogServiceCollection.LogServiceCollection", + "Name": "Log Service Collection", + "Description": "Collection of Logs for this System", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices/Log1" + } + ], + "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM1/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM1/index.json new file mode 100644 index 0000000..051c553 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM1/index.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Memory.v1_24_0.Memory", + "Id": "DIMM1", + "Name": "DIMM Slot 1", + "RankCount": 2, + "MaxTDPMilliWatts": [ + 12000 + ], + "CapacityMiB": 32768, + "DataWidthBits": 64, + "BusWidthBits": 72, + "ErrorCorrection": "MultiBitECC", + "MemoryLocation": { + "Socket": 1, + "MemoryController": 1, + "Channel": 1, + "Slot": 1 + }, + "Location": { + "PartLocation": { + "ServiceLabel": "DIMM 1", + "LocationType": "Slot", + "LocationOrdinalValue": 0 + } + }, + "MemoryType": "DRAM", + "MemoryDeviceType": "DDR4", + "BaseModuleType": "RDIMM", + "MemoryMedia": [ + "DRAM" + ], + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "EnvironmentMetrics": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM1/EnvironmentMetrics" + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM2/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM2/index.json new file mode 100644 index 0000000..1a6a577 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM2/index.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Memory.v1_24_0.Memory", + "Id": "DIMM2", + "Name": "DIMM Slot 2", + "RankCount": 2, + "MaxTDPMilliWatts": [ + 12000 + ], + "CapacityMiB": 32768, + "DataWidthBits": 64, + "BusWidthBits": 72, + "ErrorCorrection": "MultiBitECC", + "MemoryLocation": { + "Socket": 1, + "MemoryController": 1, + "Channel": 1, + "Slot": 2 + }, + "Location": { + "PartLocation": { + "ServiceLabel": "DIMM 2", + "LocationType": "Slot", + "LocationOrdinalValue": 1 + } + }, + "MemoryType": "DRAM", + "MemoryDeviceType": "DDR4", + "BaseModuleType": "RDIMM", + "MemoryMedia": [ + "DRAM" + ], + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "EnvironmentMetrics": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM2/EnvironmentMetrics" + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM3/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM3/index.json new file mode 100644 index 0000000..be59d2a --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM3/index.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Memory.v1_24_0.Memory", + "Id": "DIMM3", + "Name": "DIMM Slot 3", + "RankCount": 2, + "MaxTDPMilliWatts": [ + 12000 + ], + "CapacityMiB": 32768, + "DataWidthBits": 64, + "BusWidthBits": 72, + "ErrorCorrection": "MultiBitECC", + "MemoryLocation": { + "Socket": 2, + "MemoryController": 2, + "Channel": 1, + "Slot": 3 + }, + "Location": { + "PartLocation": { + "ServiceLabel": "DIMM 3", + "LocationType": "Slot", + "LocationOrdinalValue": 2 + } + }, + "MemoryType": "DRAM", + "MemoryDeviceType": "DDR4", + "BaseModuleType": "RDIMM", + "MemoryMedia": [ + "DRAM" + ], + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "EnvironmentMetrics": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM3/EnvironmentMetrics" + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM3", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM4/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM4/index.json new file mode 100644 index 0000000..f9b71b1 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/DIMM4/index.json @@ -0,0 +1,23 @@ +{ + "@odata.type": "#Memory.v1_24_0.Memory", + "Id": "DIMM4", + "Name": "DIMM Slot 4", + "MemoryLocation": { + "Socket": 2, + "MemoryController": 2, + "Channel": 1, + "Slot": 4 + }, + "Location": { + "PartLocation": { + "ServiceLabel": "DIMM 4", + "LocationType": "Slot", + "LocationOrdinalValue": 3 + } + }, + "Status": { + "State": "Absent" + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM4", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/index.json new file mode 100644 index 0000000..0abde07 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Memory/index.json @@ -0,0 +1,21 @@ +{ + "@odata.type": "#MemoryCollection.MemoryCollection", + "Name": "Memory Module Collection", + "Members@odata.count": 4, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM1" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM2" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM3" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM4" + } + ], + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/CPU1/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/CPU1/index.json new file mode 100644 index 0000000..9e701e8 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/CPU1/index.json @@ -0,0 +1,68 @@ +{ + "@odata.type": "#Processor.v1_24_0.Processor", + "Id": "CPU1", + "Name": "Processor", + "Socket": "CPU 1", + "ProcessorType": "CPU", + "ProcessorArchitecture": "x86", + "InstructionSet": "x86-64", + "Manufacturer": "Intel(R) Corporation", + "Model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series", + "ProcessorId": { + "VendorId": "GenuineIntel", + "IdentificationRegisters": "0x34AC34DC8901274A", + "EffectiveFamily": "0x42", + "EffectiveModel": "0x61", + "Step": "0x1", + "MicrocodeInfo": "0x429943" + }, + "Location": { + "PartLocation": { + "ServiceLabel": "CPU 1", + "LocationType": "Socket", + "LocationOrdinalValue": 0 + } + }, + "AdditionalFirmwareVersions": { + "Microcode": "0x46" + }, + "MaxSpeedMHz": 3700, + "OperatingSpeedMHz": 2333, + "OperatingSpeedRangeMHz": { + "DataSourceUri": "/redfish/v1/Chassis/1U/Controls/CPU1Freq", + "ControlMode": "Automatic", + "AllowableMin": 1200, + "SettingMin": 2000, + "SettingMax": 2400, + "AllowableMax": 3700 + }, + "TotalCores": 8, + "TotalThreads": 16, + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "Warning", + "Conditions": [ + { + "MessageId": "Sensor.1.0.ReadingAboveUpperCautionThreshold", + "Timestamp": "2024-08-12T12:25:00-05:00", + "Message": "Sensor 'CPU1 Temp' reading of 44 (Cel) is above the 42 upper caution threshold.", + "Severity": "Warning", + "MessageArgs": [ + "CPU1 Temp", + "44", + "Cel", + "42" + ], + "OriginOfCondition": { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPU1Temp" + } + } + ] + }, + "EnvironmentMetrics": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1/EnvironmentMetrics" + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/CPU2/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/CPU2/index.json new file mode 100644 index 0000000..903a8d1 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/CPU2/index.json @@ -0,0 +1,19 @@ +{ + "@odata.type": "#Processor.v1_24_0.Processor", + "Id": "CPU2", + "Name": "Processor", + "Socket": "CPU 2", + "ProcessorType": "CPU", + "Status": { + "State": "Absent" + }, + "Location": { + "PartLocation": { + "ServiceLabel": "CPU 2", + "LocationType": "Socket", + "LocationOrdinalValue": 1 + } + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/FPGA1/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/FPGA1/index.json new file mode 100644 index 0000000..737da22 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/FPGA1/index.json @@ -0,0 +1,88 @@ +{ + "@odata.type": "#Processor.v1_24_0.Processor", + "Id": "FPGA1", + "Name": "FPGA", + "ProcessorType": "FPGA", + "ProcessorArchitecture": "OEM", + "InstructionSet": "OEM", + "Manufacturer": "Intel(R) Corporation", + "Model": "Stratix 10", + "UUID": "00000000-0000-0000-0000-000000000000", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Metrics": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/FPGA1/ProcessorMetrics" + }, + "Location": { + "PartLocation": { + "ServiceLabel": "U168", + "LocationType": "Embedded", + "LocationOrdinalValue": 0 + } + }, + "TDPWatts": 120, + "MaxTDPWatts": 150, + "ProcessorMemory": [ + { + "IntegratedMemory": true, + "MemoryType": "HBM2", + "CapacityMiB": 512, + "SpeedMHz": 1066 + } + ], + "FPGA": { + "FpgaType": "Discrete", + "Model": "Stratix10", + "FirmwareId": "0x6400002fc614bb9", + "FirmwareManufacturer": "Intel(R) Corporation", + "FirmwareVersion": "Blue v.1.00.86", + "HostInterface": { + "InterfaceType": "PCIe", + "PCIe": { + "MaxPCIeType": "Gen4", + "MaxLanes": 8 + } + }, + "ExternalInterfaces": [ + { + "InterfaceType": "Ethernet", + "Ethernet": { + "MaxSpeedMbps": 10240, + "MaxLanes": 4 + } + } + ], + "PCIeVirtualFunctions": 1, + "ProgrammableFromHost": true, + "ReconfigurationSlots": [ + { + "SlotId": "AFU0", + "UUID": "00000000-0000-0000-0000-000000000000", + "ProgrammableFromHost": true, + "AccelerationFunction": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/FPGA1/AccelerationFunctions/Compression" + } + } + ] + }, + "AccelerationFunctions": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/FPGA1/AccelerationFunctions" + }, + "Links": { + "Endpoints": [], + "ConnectedProcessors": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1" + } + ] + }, + "Actions": { + "#Processor.Reset": { + "target": "/redfish/v1/Systems/437XR1138R2/Processors/FPGA1/Actions/Processor.Reset" + } + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/FPGA1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/index.json new file mode 100644 index 0000000..56997dd --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/Processors/index.json @@ -0,0 +1,18 @@ +{ + "@odata.type": "#ProcessorCollection.ProcessorCollection", + "Name": "Processors Collection", + "Members@odata.count": 3, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU1" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/CPU2" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors/FPGA1" + } + ], + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/SimpleStorage/1/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/SimpleStorage/1/index.json new file mode 100644 index 0000000..b126769 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/SimpleStorage/1/index.json @@ -0,0 +1,48 @@ +{ + "@odata.type": "#SimpleStorage.v1_3_2.SimpleStorage", + "Id": "1", + "Name": "Simple Storage Controller", + "Description": "System SATA", + "UefiDevicePath": "Acpi(PNP0A03,0)/Pci(1F|1)/Ata(Primary,Master)/HD(Part3, Sig00110011)", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "Warning" + }, + "Devices": [ + { + "Name": "SATA Bay 1", + "Manufacturer": "Contoso", + "Model": "3000GT8", + "CapacityBytes": 8000000000000, + "Status": { + "State": "Enabled", + "Health": "OK" + } + }, + { + "Name": "SATA Bay 2", + "Manufacturer": "Contoso", + "Model": "3000GT7", + "CapacityBytes": 4000000000000, + "Status": { + "State": "Enabled", + "Health": "Warning" + } + }, + { + "Name": "SATA Bay 3", + "Status": { + "State": "Absent" + } + }, + { + "Name": "SATA Bay 4", + "Status": { + "State": "Absent" + } + } + ], + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage/1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/SimpleStorage/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/SimpleStorage/index.json new file mode 100644 index 0000000..c876f42 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/SimpleStorage/index.json @@ -0,0 +1,12 @@ +{ + "@odata.type": "#SimpleStorageCollection.SimpleStorageCollection", + "Name": "Simple Storage Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage/1" + } + ], + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/index.json new file mode 100644 index 0000000..274e77d --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/index.json @@ -0,0 +1,233 @@ +{ + "@odata.type": "#ComputerSystem.v1_29_0.ComputerSystem", + "Id": "437XR1138R2", + "Name": "WebFrontEnd483", + "SystemType": "Physical", + "AssetTag": "Chicago-45Z-2381", + "Manufacturer": "Contoso", + "Model": "3500", + "SubModel": "RX", + "SKU": "8675309", + "SerialNumber": "437XR1138R2", + "PartNumber": "224071-J23", + "Description": "Web Front End node", + "UUID": "38947555-7742-3448-3784-823347823834", + "HostName": "web483", + "BootImagePushURI": "/redfish/v1/Systems/437XR1138R2/BootImagePushURI", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "Warning", + "Conditions": [ + { + "MessageId": "Sensor.1.0.ReadingAboveUpperCautionThreshold", + "Timestamp": "2024-08-12T12:25:00-05:00", + "Message": "Sensor 'CPU1 Temp' reading of 44 (Cel) is above the 42 upper caution threshold.", + "Severity": "Warning", + "MessageArgs": [ + "CPU1 Temp", + "44", + "Cel", + "42" + ], + "OriginOfCondition": { + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPU1Temp" + } + } + ] + }, + "HostingRoles": [ + "ApplicationServer" + ], + "IndicatorLED": "Off", + "PowerState": "On", + "Boot": { + "BootSourceOverrideEnabled": "Once", + "BootSourceOverrideTarget": "Pxe", + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Pxe", + "Cd", + "Usb", + "Hdd", + "BiosSetup", + "Utilities", + "Diags", + "SDCard", + "UefiTarget" + ], + "BootSourceOverrideMode": "UEFI", + "UefiTargetBootSourceOverride": "/0x31/0x33/0x01/0x01" + }, + "TrustedModules": [ + { + "FirmwareVersion": "1.13b", + "InterfaceType": "TPM1_2", + "Status": { + "State": "Enabled", + "Health": "OK" + } + } + ], + "Oem": { + "Contoso": { + "@odata.type": "#Contoso.ComputerSystem", + "ProductionLocation": { + "FacilityName": "PacWest Production Facility", + "Country": "USA" + } + }, + "Chipwise": { + "@odata.type": "#Chipwise.ComputerSystem", + "Style": "Executive" + } + }, + "BootProgress": { + "LastState": "OSRunning", + "LastStateTime": "2021-03-13T04:14:13+06:00", + "LastBootTimeSeconds": 676 + }, + "LastResetTime": "2021-03-13T04:02:57+06:00", + "BiosVersion": "P79 v1.45 (12/06/2017)", + "ProcessorSummary": { + "Count": 2, + "Model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series", + "LogicalProcessorCount": 16, + "CoreCount": 8, + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + } + }, + "MemorySummary": { + "TotalSystemMemoryGiB": 96, + "TotalSystemPersistentMemoryGiB": 0, + "MemoryMirroring": "None", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + } + }, + "GraphicalConsole": { + "MaxConcurrentSessions": 4, + "VNC": { + "ServiceEnabled": true, + "Port": 5900, + "HostName": "10.1.2.3", + "AuthenticationMethod": "ExplicitLogin", + "SupportedAccessModes": [ + "ReadWrite", + "ReadOnly" + ], + "AllowsSharedAccess": true, + "SupportedEncodings": [ + "Raw", + "Tight", + "ZRLE" + ], + "SupportsOneTimePassword": true, + "SupportsPersistentPassword": true + }, + "HTML5": { + "ServiceEnabled": true, + "Port": 443, + "HostName": "bmc.example.com", + "ConsoleURI": "https://bmc.example.com/console", + "AuthenticationMethod": "ExplicitLogin", + "SupportedAccessModes": [ + "ReadWrite" + ], + "AllowsSharedAccess": false, + "SupportsOneTimeURL": true + }, + "KVMIP": { + "ServiceEnabled": false, + "Port": 7000, + "HostName": "10.1.2.3", + "AuthenticationMethod": "ExplicitLogin", + "SupportedAccessModes": [ + "ReadWrite" + ], + "AllowsSharedAccess": false + } + }, + "Bios": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Bios" + }, + "SecureBoot": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SecureBoot" + }, + "Processors": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Processors" + }, + "Memory": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory" + }, + "EthernetInterfaces": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces" + }, + "SimpleStorage": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage" + }, + "LogServices": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/LogServices" + }, + "GraphicsControllers": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/GraphicsControllers" + }, + "USBControllers": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/USBControllers" + }, + "Certificates": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Certificates" + }, + "VirtualMedia": { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/VirtualMedia" + }, + "Links": { + "Chassis": [ + { + "@odata.id": "/redfish/v1/Chassis/1U" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ] + }, + "Actions": { + "#ComputerSystem.Reset": { + "target": "/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.Reset", + "ResetType@Redfish.AllowableValues": [ + "On", + "ForceOff", + "GracefulShutdown", + "GracefulRestart", + "ForceRestart", + "Nmi", + "ForceOn", + "PushPowerButton", + "FullPowerCycle" + ] + }, + "#ComputerSystem.GraphicalConsoleGetOneTimeURL": { + "target": "/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.GraphicalConsoleGetOneTimeURL" + }, + "#ComputerSystem.GraphicalConsoleSetOneTimePassword": { + "target": "/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.GraphicalConsoleSetOneTimePassword" + }, + "#ComputerSystem.GraphicalConsoleSetPassword": { + "target": "/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.GraphicalConsoleSetPassword" + }, + "Oem": { + "#Contoso.Reset": { + "target": "/redfish/v1/Systems/437XR1138R2/Oem/Contoso/Actions/Contoso.Reset" + } + } + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/index.json new file mode 100644 index 0000000..af7ef3f --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/index.json @@ -0,0 +1,12 @@ +{ + "@odata.type": "#ComputerSystemCollection.ComputerSystemCollection", + "Name": "Computer System Collection", + "Members@odata.count": 1, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2" + } + ], + "@odata.id": "/redfish/v1/Systems", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/BIOS/index.json b/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/BIOS/index.json new file mode 100644 index 0000000..17b5c03 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/BIOS/index.json @@ -0,0 +1,35 @@ +{ + "@odata.type": "#SoftwareInventory.v1_15_0.SoftwareInventory", + "Id": "BIOS", + "Name": "Contoso BIOS Firmware", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Updateable": true, + "Manufacturer": "Contoso", + "ReleaseDate": "2017-12-06T12:00:00Z", + "Version": "P79 v1.45", + "SoftwareId": "FEE82A67-6CE2-4625-9F44-237AD2402C28", + "LowestSupportedVersion": "P79 v1.10", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Bios" + } + ], + "ImageState": "Active", + "Active": true, + "Staged": false, + "Armed": false, + "Links": { + "ActiveTargets": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Bios" + } + ], + "StagedTargets": [], + "ArmedTargets": [] + }, + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BIOS", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/BMC/index.json b/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/BMC/index.json new file mode 100644 index 0000000..defb6d0 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/BMC/index.json @@ -0,0 +1,48 @@ +{ + "@odata.type": "#SoftwareInventory.v1_15_0.SoftwareInventory", + "Id": "BMC", + "Name": "Contoso BMC Firmware", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Updateable": true, + "Manufacturer": "Contoso", + "ReleaseDate": "2017-08-22T12:00:00Z", + "Version": "1.45.455b66-rev4", + "SoftwareId": "1624A9DF-5E13-47FC-874A-DF3AFF143089", + "LowestSupportedVersion": "1.30.367a12-rev1", + "UefiDevicePaths": [ + "BMC(0x1,0x0ABCDEF)" + ], + "AdditionalVersions": { + "Bootloader": "v2022.01", + "Kernel": "Linux 5.13.0-30-generic arm71", + "Oem": { + "Contoso": { + "@odata.type": "#ContosoSoftwareInventory.v1_0_0.AdditionalVersions", + "ManagementApp": "1.30" + } + } + }, + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ], + "ImageState": "Active", + "Active": true, + "Staged": false, + "Armed": false, + "Links": { + "ActiveTargets": [ + { + "@odata.id": "/redfish/v1/Managers/BMC" + } + ], + "StagedTargets": [], + "ArmedTargets": [] + }, + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BMC", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/SS/index.json b/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/SS/index.json new file mode 100644 index 0000000..b91165a --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/SS/index.json @@ -0,0 +1,34 @@ +{ + "@odata.type": "#SoftwareInventory.v1_15_0.SoftwareInventory", + "Id": "SS", + "Name": "Contoso Simple Storage Firmware", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Updateable": true, + "Manufacturer": "Contoso", + "ReleaseDate": "2021-10-18T12:00:00Z", + "Version": "2.50", + "LowestSupportedVersion": "2.30.rev1", + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage/1" + } + ], + "ImageState": "Active", + "Active": true, + "Staged": false, + "Armed": false, + "Links": { + "ActiveTargets": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/SimpleStorage/1" + } + ], + "StagedTargets": [], + "ArmedTargets": [] + }, + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/SS", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/index.json b/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/index.json new file mode 100644 index 0000000..c30be38 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/UpdateService/FirmwareInventory/index.json @@ -0,0 +1,18 @@ +{ + "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection", + "Name": "Firmware Collection", + "Members@odata.count": 2, + "Members": [ + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BMC" + }, + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/SS" + }, + { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/BIOS" + } + ], + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/index.json b/tests/fixtures/dmtf-public-rackmount1/index.json new file mode 100644 index 0000000..6177142 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/index.json @@ -0,0 +1,66 @@ +{ + "@odata.type": "#ServiceRoot.v1_22_0.ServiceRoot", + "Id": "RootService", + "Name": "Root Service", + "RedfishVersion": "1.15.0", + "UUID": "92384634-2938-2342-8820-489239905423", + "ProtocolFeaturesSupported": { + "ExpandQuery": { + "ExpandAll": true, + "Levels": true, + "MaxLevels": 6, + "Links": true, + "NoLinks": true + }, + "SelectQuery": false, + "FilterQuery": false, + "OnlyMemberQuery": true, + "ExcerptQuery": true + }, + "Systems": { + "@odata.id": "/redfish/v1/Systems" + }, + "Chassis": { + "@odata.id": "/redfish/v1/Chassis" + }, + "Managers": { + "@odata.id": "/redfish/v1/Managers" + }, + "Tasks": { + "@odata.id": "/redfish/v1/TaskService" + }, + "SessionService": { + "@odata.id": "/redfish/v1/SessionService" + }, + "AccountService": { + "@odata.id": "/redfish/v1/AccountService" + }, + "EventService": { + "@odata.id": "/redfish/v1/EventService" + }, + "Registries": { + "@odata.id": "/redfish/v1/Registries" + }, + "UpdateService": { + "@odata.id": "/redfish/v1/UpdateService" + }, + "CertificateService": { + "@odata.id": "/redfish/v1/CertificateService" + }, + "KeyService": { + "@odata.id": "/redfish/v1/KeyService" + }, + "ServiceConditions": { + "@odata.id": "/redfish/v1/ServiceConditions" + }, + "Links": { + "Sessions": { + "@odata.id": "/redfish/v1/SessionService/Sessions" + } + }, + "ComponentIntegrity": { + "@odata.id": "/redfish/v1/ComponentIntegrity" + }, + "@odata.id": "/redfish/v1/", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-telemetry/COMMANDS.txt b/tests/fixtures/dmtf-public-telemetry/COMMANDS.txt new file mode 100644 index 0000000..1904ecb --- /dev/null +++ b/tests/fixtures/dmtf-public-telemetry/COMMANDS.txt @@ -0,0 +1,11 @@ +Source: https://github.com/DMTF/Redfish-Publications +Commit: 4f81814e399213c9055863ddaff42791c6b3706a +Mockup: mockups/public-telemetry + +Files are unmodified copies. Each request path maps to /index.json +(the service root is ./index.json), as in the mockup and as served by Redfish-Mockup-Server. + +Request paths: +GET /redfish/v1/Systems/1 +GET /redfish/v1/Systems/1/ResetActionInfo +GET /redfish/v1/Managers/1/NetworkProtocol diff --git a/tests/fixtures/dmtf-public-telemetry/LICENSE.md b/tests/fixtures/dmtf-public-telemetry/LICENSE.md new file mode 100644 index 0000000..231cf4a --- /dev/null +++ b/tests/fixtures/dmtf-public-telemetry/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2014-2025, Contributing Member(s) of Distributed Management Task +Force, Inc.. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/fixtures/dmtf-public-telemetry/Managers/1/NetworkProtocol/index.json b/tests/fixtures/dmtf-public-telemetry/Managers/1/NetworkProtocol/index.json new file mode 100644 index 0000000..fc38581 --- /dev/null +++ b/tests/fixtures/dmtf-public-telemetry/Managers/1/NetworkProtocol/index.json @@ -0,0 +1,61 @@ +{ + "@odata.type": "#ManagerNetworkProtocol.v1_14_0.ManagerNetworkProtocol", + "Id": "NetworkProtocol", + "Name": "Manager Network Protocol", + "Description": "Manager Network Service Status", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "HostName": "mymanager", + "FQDN": "mymanager.mydomain.com", + "HTTP": { + "ProtocolEnabled": true, + "Port": 80 + }, + "HTTPS": { + "ProtocolEnabled": true, + "Port": 443 + }, + "IPMI": { + "ProtocolEnabled": true, + "Port": 623 + }, + "SSH": { + "ProtocolEnabled": true, + "Port": 22 + }, + "SNMP": { + "ProtocolEnabled": true, + "Port": 161 + }, + "VirtualMedia": { + "ProtocolEnabled": true, + "Port": 17988 + }, + "SSDP": { + "ProtocolEnabled": true, + "Port": 1900, + "NotifyMulticastIntervalSeconds": 600, + "NotifyTTL": 5, + "NotifyIPv6Scope": "Site" + }, + "Telnet": { + "ProtocolEnabled": true, + "Port": 23 + }, + "KVMIP": { + "ProtocolEnabled": true, + "Port": 5288 + }, + "NTP": { + "ProtocolEnabled": true, + "NTPServers": [ + "0.pool.ntp.org", + "1.pool.ntp.org", + "time-a.nist.gov" + ] + }, + "@odata.id": "/redfish/v1/Managers/1/NetworkProtocol", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-telemetry/Systems/1/ResetActionInfo/index.json b/tests/fixtures/dmtf-public-telemetry/Systems/1/ResetActionInfo/index.json new file mode 100644 index 0000000..722b374 --- /dev/null +++ b/tests/fixtures/dmtf-public-telemetry/Systems/1/ResetActionInfo/index.json @@ -0,0 +1,24 @@ +{ + "@odata.type": "#ActionInfo.v1_6_0.ActionInfo", + "Id": "ResetActionInfo", + "Name": "Reset Action Info", + "Parameters": [ + { + "Name": "ResetType", + "Required": true, + "DataType": "String", + "AllowableValues": [ + "On", + "ForceOff", + "GracefulShutdown", + "GracefulRestart", + "ForceRestart", + "Nmi", + "ForceOn", + "PushPowerButton" + ] + } + ], + "@odata.id": "/redfish/v1/Systems/1/ResetActionInfo", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-telemetry/Systems/1/index.json b/tests/fixtures/dmtf-public-telemetry/Systems/1/index.json new file mode 100644 index 0000000..ed62c16 --- /dev/null +++ b/tests/fixtures/dmtf-public-telemetry/Systems/1/index.json @@ -0,0 +1,133 @@ +{ + "@odata.type": "#ComputerSystem.v1_29_0.ComputerSystem", + "Id": "1", + "Name": "My Computer System", + "SystemType": "Physical", + "AssetTag": "free form asset tag", + "Manufacturer": "Manufacturer Name", + "Model": "Model Name", + "SKU": "", + "SerialNumber": "2M220100SL", + "PartNumber": "", + "Description": "Description of server", + "UUID": "00000000-0000-0000-0000-000000000000", + "HostName": "web-srv344", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + }, + "IndicatorLED": "Off", + "PowerState": "On", + "Boot": { + "BootSourceOverrideEnabled": "Once", + "BootSourceOverrideMode": "UEFI", + "BootSourceOverrideTarget": "Pxe", + "BootSourceOverrideTarget@Redfish.AllowableValues": [ + "None", + "Pxe", + "Floppy", + "Cd", + "Usb", + "Hdd", + "BiosSetup", + "Utilities", + "Diags", + "UefiTarget", + "SDCard", + "UefiHttp", + "UefiBootNext" + ], + "UefiTargetBootSourceOverride": "" + }, + "BiosVersion": "P79 v1.00 (09/20/2013)", + "ProcessorSummary": { + "Count": 8, + "LogicalProcessorCount": 256, + "Model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + } + }, + "MemorySummary": { + "TotalSystemMemoryGiB": 16, + "MemoryMirroring": "System", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + } + }, + "TrustedModules": [ + { + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "InterfaceType": "TPM2_0", + "FirmwareVersion": "3.1", + "FirmwareVersion2": "1", + "InterfaceTypeSelection": "None" + } + ], + "HostWatchdogTimer": { + "FunctionEnabled": true, + "WarningAction": "DiagnosticInterrupt", + "WarningAction@Redfish.AllowableValues": [ + "DiagnosticInterrupt", + "SMI", + "None" + ], + "TimeoutAction": "ResetSystem", + "TimeoutAction@Redfish.AllowableValues": [ + "ResetSystem", + "PowerCycle", + "PowerDown", + "None" + ], + "Status": { + "State": "StandbyOffline" + } + }, + "Processors": { + "@odata.id": "/redfish/v1/Systems/1/Processors" + }, + "Memory": { + "@odata.id": "/redfish/v1/Systems/1/Memory" + }, + "Links": { + "Chassis": [ + { + "@odata.id": "/redfish/v1/Chassis/1" + } + ], + "ManagedBy": [ + { + "@odata.id": "/redfish/v1/Managers/1" + } + ] + }, + "Actions": { + "#ComputerSystem.Reset": { + "target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", + "@Redfish.ActionInfo": "/redfish/v1/Systems/1/ResetActionInfo" + } + }, + "Oem": { + "Contoso": { + "@odata.type": "#Contoso.ComputerSystem", + "ProductionLocation": { + "FacilityName": "PacWest Production Facility", + "Country": "USA" + } + }, + "Chipwise": { + "@odata.type": "#Chipwise.ComputerSystem", + "Style": "Executive" + } + }, + "@odata.id": "/redfish/v1/Systems/1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file From 8998e1342e4fb0a6aa447594d505715b1a285d05 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Mon, 12 Oct 2026 23:42:14 +0000 Subject: [PATCH 03/10] Add Redfish transport Session login through the SessionService with X-Auth-Token, the session deleted on close (also on errors and when the DELETE fails), basic auth as fallback only when the service cannot create sessions, never after a rejected password. Certificate verification is on by default and configurable per device; True passes the requests CA bundle explicitly because the redfish library disables verification without a CA file. Collections are followed by @odata.id, including expanded members and nextLink pages. Links to other hosts are refused so the session token stays on the BMC. --- napalm_redfish/transport.py | 327 +++++++++++++++++++++++++++ tests/test_transport.py | 428 ++++++++++++++++++++++++++++++++++++ 2 files changed, 755 insertions(+) create mode 100644 napalm_redfish/transport.py create mode 100644 tests/test_transport.py diff --git a/napalm_redfish/transport.py b/napalm_redfish/transport.py new file mode 100644 index 0000000..b6f45db --- /dev/null +++ b/napalm_redfish/transport.py @@ -0,0 +1,327 @@ +"""HTTPS transport to a Redfish service, on top of the DMTF ``redfish`` library. + +The library does the HTTP work; this module adds what a BMC driver needs on top: + +- Session login through the SessionService (``X-Auth-Token``), with the session deleted on + close, because BMCs allow only a few concurrent sessions. Basic auth is the fallback for + services that cannot create sessions, never a second try after a rejected password. +- Certificate verification that is on unless turned off per device. The library disables + verification whenever it is not given a CA file, so ``verify=True`` passes the CA bundle + of ``requests`` explicitly. +- Collections followed by ``@odata.id`` (including ``nextLink`` pages and expanded members) + instead of assumed member IDs. +- Links followed only on the BMC itself: an ``@odata.id`` pointing at another host (a PDU + outlet, for example) is refused, so the session token never leaves the BMC. +""" + +from __future__ import annotations + +import ipaddress +import logging +import os +from typing import Any, Mapping +from urllib.parse import urlsplit + +import requests +from redfish.rest.v1 import ( + AuthMethod, + InvalidCredentialsError, + RedfishPasswordChangeRequiredError, + RetriesExhaustedError, + ServerDownOrUnreachableError, + SessionCreationError, + redfish_client, +) + +LOG = logging.getLogger(__name__) + +SERVICE_ROOT = "/redfish/v1/" +DEFAULT_SESSIONS = "/redfish/v1/SessionService/Sessions" +AUTH_METHODS = ("auto", "session", "basic") +MAX_PAGES = 1000 + +Reference = str | Mapping[str, Any] + + +class RedfishError(Exception): + """A Redfish request failed.""" + + def __init__(self, message: str, *, status: int | None = None, path: str | None = None): + super().__init__(message) + self.status = status + self.path = path + + +class RedfishAuthError(RedfishError): + """Login failed, or the account may not read the resource (HTTP 401/403).""" + + +class RedfishHTTPError(RedfishError): + """The service answered with an HTTP error status.""" + + +class RedfishNotFound(RedfishHTTPError): + """The resource does not exist (HTTP 404).""" + + +def _error_message(body: Any) -> str: + """The human-readable part of a Redfish error body, if any.""" + if not isinstance(body, Mapping): + return "" + error = body.get("error") + if not isinstance(error, Mapping): + return "" + messages = [ + info.get("Message") + for info in error.get("@Message.ExtendedInfo") or [] + if isinstance(info, Mapping) and info.get("Message") + ] + if not messages and error.get("message"): + messages = [error["message"]] + return "; ".join(str(m) for m in messages) + + +class RedfishTransport: + """One authenticated connection to a Redfish service. + + :param hostname: BMC host name or IP address (IPv6 without brackets is fine). + :param port: HTTPS port, default 443. + :param verify: ``True`` verifies the certificate against the CA bundle of ``requests``, + a path verifies against that CA file, ``False`` turns verification off. + :param auth: ``"auto"`` (session, basic auth if the service cannot create sessions), + ``"session"`` or ``"basic"``. + :param timeout: seconds per request. + :param retries: extra attempts after a connection error (the library waits 1 s between). + """ + + def __init__( + self, + hostname: str, + username: str, + password: str, + *, + port: int | None = None, + verify: bool | str = True, + auth: str = "auto", + timeout: float = 30, + retries: int = 1, + ): + if auth not in AUTH_METHODS: + raise ValueError(f"auth must be one of {', '.join(AUTH_METHODS)}, not {auth!r}") + if isinstance(verify, str) and not os.path.isfile(verify): + raise ValueError(f"CA file not found: {verify}") + self.hostname = hostname + self.username = username + self.password = password + self.port = port + self.verify = verify + self.auth = auth + self.timeout = timeout + self.retries = retries + self.root: dict[str, Any] = {} + self.auth_method: str | None = None + self._client = None + + @property + def base_url(self) -> str: + host = self.hostname + try: + if ipaddress.ip_address(host).version == 6: + host = f"[{host}]" + except ValueError: + pass + return f"https://{host}" + (f":{self.port}" if self.port else "") + + @property + def _cafile(self) -> str | None: + if self.verify is True: + return requests.certs.where() + if self.verify is False: + return None + return self.verify + + # --- connection -------------------------------------------------------------------- + + def open(self) -> None: + """Read the service root (no credentials needed) and log in. No-op when open.""" + if self._client is not None: + return + try: + client = redfish_client( + base_url=self.base_url, + username=self.username, + password=self.password, + default_prefix=SERVICE_ROOT, + cafile=self._cafile, + timeout=self.timeout, + max_retry=self.retries, + ) + except (RetriesExhaustedError, ServerDownOrUnreachableError) as exc: + raise RedfishError(f"{self.base_url}: service root not reachable: {exc}") from exc + except Exception as exc: + raise RedfishError(f"{self.base_url}: cannot read the service root: {exc}") from exc + self._client = client + self.root = dict(getattr(client, "root", None) or {}) + try: + self.auth_method = self._login(client) + except BaseException: + self._client = None + client._session.close() + raise + + def _login(self, client) -> str: + if self.auth == "basic": + self._call(client.login, auth=AuthMethod.BASIC) + return "basic" + try: + self._call(client.login, auth=AuthMethod.SESSION) + except _SessionUnsupported as exc: + if self.auth == "session": + raise RedfishAuthError(f"{self.base_url}: session login failed: {exc}") from exc + LOG.info("%s: no Redfish session (%s), falling back to basic auth", self.base_url, exc) + else: + if client.get_session_key(): + return "session" + if self.auth == "session": + raise RedfishAuthError(f"{self.base_url}: session login returned no X-Auth-Token") + LOG.info("%s: session login returned no token, falling back to basic auth", + self.base_url) + self._call(client.login, auth=AuthMethod.BASIC) + return "basic" + + def _call(self, login, **kwargs) -> None: + try: + login(**kwargs) + except InvalidCredentialsError as exc: + raise RedfishAuthError(f"{self.base_url}: invalid credentials", status=401) from exc + except RedfishPasswordChangeRequiredError as exc: + raise RedfishAuthError(f"{self.base_url}: the account must change its password " + "before it can be used") from exc + except SessionCreationError as exc: + raise _SessionUnsupported(str(exc).strip()) from exc + except (RetriesExhaustedError, ServerDownOrUnreachableError) as exc: + raise RedfishError(f"{self.base_url}: login failed: {exc}") from exc + + def close(self) -> None: + """Delete the session (if any) and close the HTTP connection. Never raises.""" + client, self._client = self._client, None + self.auth_method = None + if client is None: + return + try: + client.logout() + except Exception as exc: # a leaked session times out; a failing close must not mask + LOG.warning("%s: could not delete the Redfish session: %s", self.base_url, exc) + client._session.close() + + def __enter__(self) -> "RedfishTransport": + self.open() + return self + + def __exit__(self, *exc_info) -> None: + self.close() + + # --- requests ---------------------------------------------------------------------- + + def path(self, ref: Reference) -> str: + """Request path for a path, an absolute URL on this BMC or an ``{"@odata.id": ...}``.""" + if isinstance(ref, Mapping): + ref = ref.get("@odata.id") + if not isinstance(ref, str) or not ref: + raise RedfishError(f"not a Redfish reference: {ref!r}") + parts = urlsplit(ref) + if parts.scheme or parts.netloc: + ours = urlsplit(self.base_url) + if parts.scheme != "https" or parts.netloc.lower() != ours.netloc.lower(): + raise RedfishError(f"refusing to follow a link to another host: {ref}") + if not parts.path.startswith("/"): + raise RedfishError(f"not an absolute Redfish path: {ref}") + return parts.path + (f"?{parts.query}" if parts.query else "") + + def _request(self, method: str, ref: Reference, body: Any = None) -> dict[str, Any]: + if self._client is None: + raise RedfishError("not connected: call open() first") + path = self.path(ref) + try: + if method == "GET": + resp = self._client.get(path) + else: + resp = self._client.post(path, body=body) + except RetriesExhaustedError as exc: + raise RedfishError(f"{method} {path}: no response", path=path) from exc + status = resp.status + try: + data = resp.dict + except Exception as exc: + if status < 400: + raise RedfishError(f"{method} {path}: response is not JSON", + status=status, path=path) from exc + data = {} + if status < 400: + if not isinstance(data, dict): + raise RedfishError(f"{method} {path}: response is not a JSON object", + status=status, path=path) + return data + detail = _error_message(data) + message = f"{method} {path}: HTTP {status}" + (f": {detail}" if detail else "") + if status in (401, 403): + raise RedfishAuthError(message, status=status, path=path) + if status == 404: + raise RedfishNotFound(message, status=status, path=path) + raise RedfishHTTPError(message, status=status, path=path) + + def get(self, ref: Reference) -> dict[str, Any]: + return self._request("GET", ref) + + def get_optional(self, ref: Reference | None) -> dict[str, Any] | None: + """GET, but ``None`` for a missing reference or a 404.""" + if ref is None: + return None + try: + return self.get(ref) + except RedfishNotFound: + return None + + def post(self, ref: Reference, body: Mapping[str, Any]) -> dict[str, Any]: + return self._request("POST", ref, dict(body)) + + def follow(self, resource: Mapping[str, Any], key: str) -> dict[str, Any] | None: + """The resource linked as ``resource[key]``; ``None`` if not linked or not found.""" + link = resource.get(key) + if not isinstance(link, Mapping) or "@odata.id" not in link: + return None + return self.get_optional(link) + + def members(self, collection: Reference, *, limit: int | None = None) -> list[dict[str, Any]]: + """All members of a collection, each fetched by its ``@odata.id``. + + Members the service already expanded inline are used as they are. ``nextLink`` pages + are followed. ``limit`` stops after that many members. + """ + result: list[dict[str, Any]] = [] + page_ref: Reference | None = collection + seen: set[str] = set() + while page_ref is not None and len(seen) < MAX_PAGES: + page_path = self.path(page_ref) + if page_path in seen: + LOG.warning("%s: nextLink loop at %s", self.base_url, page_path) + break + seen.add(page_path) + page = self.get(page_path) + for member in page.get("Members") or []: + if limit is not None and len(result) >= limit: + return result + if not isinstance(member, Mapping): + continue + if "Id" in member or "@odata.type" in member: + result.append(dict(member)) + elif "@odata.id" in member: + result.append(self.get(member)) + if limit is not None and len(result) >= limit: + break + page_ref = page.get("Members@odata.nextLink") or page.get("@odata.nextLink") + return result + + +class _SessionUnsupported(Exception): + """Session creation failed for a reason other than bad credentials.""" diff --git a/tests/test_transport.py b/tests/test_transport.py new file mode 100644 index 0000000..ed8732e --- /dev/null +++ b/tests/test_transport.py @@ -0,0 +1,428 @@ +"""Transport tests against mocked HTTP serving the DMTF public-rackmount1 mockup.""" + +import base64 +import json +import logging +import os + +import pytest +import requests +from conftest import BASE_URL, SESSION_PATH, SESSIONS, TOKEN + +from napalm_redfish.transport import ( + RedfishAuthError, + RedfishError, + RedfishHTTPError, + RedfishNotFound, + RedfishTransport, +) + +BASIC = "Basic " + base64.b64encode(b"netork:s3cret").decode() + + +def make(**kwargs) -> RedfishTransport: + kwargs.setdefault("verify", False) + kwargs.setdefault("retries", 0) + return RedfishTransport("bmc.example.net", "netork", "s3cret", **kwargs) + + +# --- base URL ------------------------------------------------------------------------------ + + +@pytest.mark.parametrize( + "hostname, port, expected", + [ + ("bmc.example.net", None, "https://bmc.example.net"), + ("bmc.example.net", 8443, "https://bmc.example.net:8443"), + ("192.0.2.10", None, "https://192.0.2.10"), + ("2001:db8::10", None, "https://[2001:db8::10]"), + ("2001:db8::10", 8443, "https://[2001:db8::10]:8443"), + ], +) +def test_base_url(hostname, port, expected): + assert RedfishTransport(hostname, "u", "p", port=port).base_url == expected + + +def test_rejects_unknown_auth_method(): + with pytest.raises(ValueError): + RedfishTransport("bmc.example.net", "u", "p", auth="digest") + + +# --- session login ------------------------------------------------------------------------- + + +def test_open_reads_service_root_before_login(server): + t = make() + t.open() + first = server.rsps.calls[0].request + assert first.method == "GET" and first.url.rstrip("/") == BASE_URL + "/redfish/v1" + assert "X-Auth-Token" not in first.headers and "Authorization" not in first.headers + assert t.root["RedfishVersion"] == "1.15.0" + t.close() + + +def test_session_login_posts_credentials_and_uses_token(server): + t = make() + t.open() + assert t.auth_method == "session" + (login,) = server.calls("POST", SESSIONS) + assert json.loads(login.request.body) == {"UserName": "netork", "Password": "s3cret"} + + t.get("/redfish/v1/Systems") + get = server.calls("GET", "/redfish/v1/Systems")[0].request + assert get.headers["X-Auth-Token"] == TOKEN + assert "Authorization" not in get.headers + t.close() + + +def test_close_deletes_the_session(server): + t = make() + t.open() + t.close() + (delete,) = server.calls("DELETE", SESSION_PATH) + assert delete.request.headers["X-Auth-Token"] == TOKEN + assert t.auth_method is None + + +def test_open_twice_logs_in_once(server): + t = make() + t.open() + t.open() + t.close() + assert len(server.calls("POST", SESSIONS)) == 1 + assert len(server.calls("DELETE", SESSION_PATH)) == 1 + + +def test_close_twice_deletes_once(server): + t = make() + t.open() + t.close() + t.close() + assert len(server.calls("DELETE")) == 1 + + +def test_context_manager_deletes_session_on_error(server): + with pytest.raises(RuntimeError): + with make() as t: + t.get("/redfish/v1/Systems") + raise RuntimeError("boom") + assert len(server.calls("DELETE", SESSION_PATH)) == 1 + + +def test_close_survives_failed_logout(server, caplog): + server.override("DELETE", SESSION_PATH, 500, {"error": {"message": "nope"}}) + t = make() + t.open() + with caplog.at_level(logging.WARNING): + t.close() + assert t.auth_method is None + assert "session" in caplog.text.lower() + + +@pytest.mark.filterwarnings("ignore:Service incorrectly did not provide the 'Location'") +def test_session_uri_from_body_when_location_missing(server): + server.override("POST", SESSIONS, 201, {"@odata.id": SESSION_PATH}, {"X-Auth-Token": TOKEN}) + t = make() + t.open() + t.close() + assert len(server.calls("DELETE", SESSION_PATH)) == 1 + + +def test_wrong_password_raises_without_basic_retry(server): + server.override("POST", SESSIONS, 401, {"error": {"message": "bad credentials"}}) + t = make() + with pytest.raises(RedfishAuthError): + t.open() + # One failed login only: a second attempt with basic auth would count twice against + # the BMC's lockout threshold. + assert len(server.calls("POST")) == 1 + assert not [c for c in server.rsps.calls if "Authorization" in c.request.headers] + + +@pytest.mark.parametrize("status", [404, 405, 501]) +def test_falls_back_to_basic_when_sessions_unsupported(server, status): + server.override("POST", SESSIONS, status, {"error": {"message": "not supported"}}) + t = make() + t.open() + assert t.auth_method == "basic" + t.get("/redfish/v1/Systems") + get = server.calls("GET", "/redfish/v1/Systems")[0].request + assert get.headers["Authorization"] == BASIC + assert "X-Auth-Token" not in get.headers + t.close() + assert not server.calls("DELETE") + + +@pytest.mark.filterwarnings("ignore:Service incorrectly did not provide the 'Location'") +def test_falls_back_to_basic_when_no_token_returned(server): + server.override("POST", SESSIONS, 201, {"@odata.id": SESSION_PATH}) + t = make() + t.open() + assert t.auth_method == "basic" + t.close() + + +def test_no_fallback_when_session_auth_is_forced(server): + server.override("POST", SESSIONS, 405, {}) + t = make(auth="session") + with pytest.raises(RedfishAuthError): + t.open() + + +def test_basic_auth_never_creates_a_session(server): + t = make(auth="basic") + t.open() + assert t.auth_method == "basic" + t.get("/redfish/v1/Systems") + assert not server.calls("POST") + assert server.calls("GET", "/redfish/v1/Systems")[0].request.headers["Authorization"] == BASIC + t.close() + + +def test_basic_auth_wrong_password(server): + server.override("GET", SESSIONS, 401, {}) + with pytest.raises(RedfishAuthError): + make(auth="basic").open() + + +def test_login_url_from_service_root(server): + custom = dict(_root(), Links={"Sessions": {"@odata.id": "/redfish/v1/Custom/Sessions"}}) + server.override("GET", "/redfish/v1/", 200, custom) + server.override("GET", "/redfish/v1", 200, custom) + server.override("POST", "/redfish/v1/Custom/Sessions", 201, {}, + {"X-Auth-Token": TOKEN, "Location": SESSION_PATH}) + t = make() + t.open() + assert server.calls("POST", "/redfish/v1/Custom/Sessions") + t.close() + + +def _root(): + from conftest import load + + return load("dmtf-public-rackmount1", "/redfish/v1") + + +def test_unreachable_service_raises_redfish_error(http): + t = make() + with pytest.raises(RedfishError): + t.open() # nothing registered on the mock: connection refused + + +def test_requests_before_open_fail(server): + with pytest.raises(RedfishError): + make().get("/redfish/v1/Systems") + + +# --- certificate verification -------------------------------------------------------------- + + +def _verify_of(server): + return server.rsps.calls[0].request.req_kwargs["verify"] + + +def test_verify_true_uses_ca_bundle(server): + t = make(verify=True) + t.open() + verify = _verify_of(server) + # The redfish library turns verification off unless it gets a CA file, so True must + # become a real bundle, not None/False. + assert isinstance(verify, str) and os.path.isfile(verify) + assert verify == requests.certs.where() + t.close() + + +def test_verify_false_disables_verification(server): + t = make(verify=False) + t.open() + assert _verify_of(server) is False + t.close() + + +def test_verify_path_is_passed_through(server, tmp_path): + ca = tmp_path / "bmc-ca.pem" + ca.write_text("dummy") + t = make(verify=str(ca)) + t.open() + assert _verify_of(server) == str(ca) + t.close() + + +def test_verify_path_must_exist(): + with pytest.raises(ValueError): + RedfishTransport("bmc.example.net", "u", "p", verify="/nonexistent/ca.pem") + + +def test_verify_is_the_default(): + assert RedfishTransport("bmc.example.net", "u", "p").verify is True + + +# --- GET and errors ------------------------------------------------------------------------ + + +@pytest.fixture +def transport(server): + t = make() + t.open() + yield t + t.close() + + +def test_get_returns_json(transport): + assert transport.get("/redfish/v1/Systems/437XR1138R2")["Model"] == "3500" + + +def test_get_accepts_odata_reference(transport): + ref = {"@odata.id": "/redfish/v1/Systems/437XR1138R2"} + assert transport.get(ref)["Id"] == "437XR1138R2" + + +def test_get_strips_fragment(transport, server): + data = transport.get("/redfish/v1/Chassis/1U/Thermal#/Fans/0") + assert data["Id"] == "Thermal" + assert server.calls("GET", "/redfish/v1/Chassis/1U/Thermal")[0].request.url.endswith("/Thermal") + + +def test_get_accepts_same_host_absolute_url(transport): + assert transport.get(BASE_URL + "/redfish/v1/Systems")["Members@odata.count"] == 1 + + +def test_get_refuses_links_to_other_hosts(transport, server): + before = len(server.rsps.calls) + # PowerSupply.Links.Outlet in the mockup points at a PDU on another host. Following it + # would send this BMC's session token there. + with pytest.raises(RedfishError): + transport.get("https://redfishpdu.contoso.com/redfish/v1/PowerEquipment/RackPDUs/1") + assert len(server.rsps.calls) == before + + +def test_get_refuses_relative_paths(transport): + with pytest.raises(RedfishError): + transport.get("Systems/1") + + +def test_get_404_raises_not_found(transport): + with pytest.raises(RedfishNotFound) as exc: + transport.get("/redfish/v1/Systems/1") + assert exc.value.status == 404 + assert exc.value.path == "/redfish/v1/Systems/1" + + +def test_get_optional_returns_none_on_404(transport): + assert transport.get_optional("/redfish/v1/Chassis/1U/Missing") is None + assert transport.get_optional(None) is None + + +def test_get_error_carries_redfish_message(transport, server): + server.override("GET", "/redfish/v1/Managers", 500, { + "error": {"code": "Base.1.0.InternalError", "message": "General error", + "@Message.ExtendedInfo": [{"Message": "The BMC is busy."}]}}) + with pytest.raises(RedfishHTTPError) as exc: + transport.get("/redfish/v1/Managers") + assert exc.value.status == 500 + assert "The BMC is busy." in str(exc.value) + + +@pytest.mark.parametrize("status", [401, 403]) +def test_get_auth_errors(transport, server, status): + server.override("GET", "/redfish/v1/AccountService", status, {}) + with pytest.raises(RedfishAuthError): + transport.get("/redfish/v1/AccountService") + + +def test_get_invalid_json(transport, server): + server.override("GET", "/redfish/v1/Managers", 200, "login") + with pytest.raises(RedfishError): + transport.get("/redfish/v1/Managers") + + +# --- collections --------------------------------------------------------------------------- + + +def test_members_follows_odata_ids(transport, server): + systems = transport.members("/redfish/v1/Systems") + # The mockup's system ID is its serial number, not "1". + assert [s["Id"] for s in systems] == ["437XR1138R2"] + assert server.calls("GET", "/redfish/v1/Systems/437XR1138R2") + + +def test_members_ignores_member_count(transport): + # Members@odata.count says 2, but three members are listed: trust the list. + fw = transport.members("/redfish/v1/UpdateService/FirmwareInventory") + assert [f["Id"] for f in fw] == ["BMC", "SS", "BIOS"] + + +def test_members_from_link_on_resource(transport): + system = transport.get("/redfish/v1/Systems/437XR1138R2") + cpus = transport.members(system["Processors"]) + assert [c["Id"] for c in cpus] == ["CPU1", "CPU2", "FPGA1"] + + +def test_members_uses_expanded_members_without_extra_requests(transport, server): + entries = transport.members("/redfish/v1/Managers/BMC/LogServices/Log/Entries") + assert entries[0]["Message"] == "System May be Melting" + assert not server.calls("GET", "/redfish/v1/Managers/BMC/LogServices/Log/Entries/1") + + +def test_members_follows_next_link(transport, server): + path = "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries" + server.override("GET", path + "?$skiptoken=2", 200, { + "Members": [{"@odata.id": path + "/3", "Id": "3", "Message": "third"}], + "@odata.id": path, + }) + entries = transport.members(path) + assert [e["Id"] for e in entries] == ["1", "2", "3"] + + +def test_members_next_link_loop_terminates(transport, server): + path = "/redfish/v1/Managers" + server.override("GET", path, 200, { + "Members": [{"@odata.id": "/redfish/v1/Managers/BMC"}], + "Members@odata.nextLink": path, + }) + assert [m["Id"] for m in transport.members(path)] == ["BMC"] + + +def test_members_limit(transport, server): + path = "/redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries" + entries = transport.members(path, limit=1) + assert [e["Id"] for e in entries] == ["1"] + assert len(server.calls("GET", path)) == 1 + + +def test_members_of_missing_collection(transport): + with pytest.raises(RedfishNotFound): + transport.members("/redfish/v1/Systems/437XR1138R2/Storage") + + +def test_follow(transport): + manager = transport.get("/redfish/v1/Managers/BMC") + assert transport.follow(manager, "NetworkProtocol")["Id"] == "NetworkProtocol" + assert transport.follow(manager, "NoSuchLink") is None + + +def test_follow_missing_target_is_none(transport, server): + system = transport.get("/redfish/v1/Systems/437XR1138R2") + # Storage is not linked from this system; Bios is linked but not in the fixtures. + assert transport.follow(system, "Storage") is None + assert transport.follow(system, "Bios") is None + + +# --- POST ---------------------------------------------------------------------------------- + + +def test_post_sends_json_and_returns_body(transport, server): + target = "/redfish/v1/Systems/437XR1138R2/Actions/ComputerSystem.Reset" + server.override("POST", target, 204, {}) + assert transport.post(target, {"ResetType": "GracefulRestart"}) == {} + (call,) = server.calls("POST", target) + assert json.loads(call.request.body) == {"ResetType": "GracefulRestart"} + assert call.request.headers["X-Auth-Token"] == TOKEN + + +def test_post_error(transport, server): + target = "/redfish/v1/Managers/BMC/Actions/Manager.Reset" + server.override("POST", target, 400, {"error": {"message": "ActionParameterValueNotInList"}}) + with pytest.raises(RedfishHTTPError) as exc: + transport.post(target, {"ResetType": "PowerCycle"}) + assert exc.value.status == 400 From cab1c84cbab65e80526d1e42b9c9e4baf215ab02 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Mon, 12 Oct 2026 23:42:14 +0000 Subject: [PATCH 04/10] Add parsers for the Redfish resources in redfish-map.md Pure functions for ServiceRoot, ComputerSystem (facts, power state, reset action and ActionInfo), Processors, Memory, Storage/Drives/SimpleStorage, Chassis Thermal and Power as well as ThermalSubsystem/PowerSubsystem/Sensors (with environment_sources to pick whichever exists), Manager, EthernetInterfaces, ManagerNetworkProtocol, accounts, firmware inventory and log services/entries. --- napalm_redfish/parsers/__init__.py | 62 +++++ napalm_redfish/parsers/accounts.py | 40 ++++ napalm_redfish/parsers/common.py | 134 +++++++++++ napalm_redfish/parsers/environment.py | 307 +++++++++++++++++++++++++ napalm_redfish/parsers/firmware.py | 23 ++ napalm_redfish/parsers/inventory.py | 128 +++++++++++ napalm_redfish/parsers/logs.py | 41 ++++ napalm_redfish/parsers/manager.py | 150 ++++++++++++ napalm_redfish/parsers/service_root.py | 29 +++ napalm_redfish/parsers/system.py | 73 ++++++ tests/parsers/test_accounts.py | 52 +++++ tests/parsers/test_environment.py | 283 +++++++++++++++++++++++ tests/parsers/test_firmware.py | 31 +++ tests/parsers/test_inventory.py | 175 ++++++++++++++ tests/parsers/test_logs.py | 64 ++++++ tests/parsers/test_manager.py | 159 +++++++++++++ tests/parsers/test_service_root.py | 46 ++++ tests/parsers/test_system.py | 129 +++++++++++ 18 files changed, 1926 insertions(+) create mode 100644 napalm_redfish/parsers/__init__.py create mode 100644 napalm_redfish/parsers/accounts.py create mode 100644 napalm_redfish/parsers/common.py create mode 100644 napalm_redfish/parsers/environment.py create mode 100644 napalm_redfish/parsers/firmware.py create mode 100644 napalm_redfish/parsers/inventory.py create mode 100644 napalm_redfish/parsers/logs.py create mode 100644 napalm_redfish/parsers/manager.py create mode 100644 napalm_redfish/parsers/service_root.py create mode 100644 napalm_redfish/parsers/system.py create mode 100644 tests/parsers/test_accounts.py create mode 100644 tests/parsers/test_environment.py create mode 100644 tests/parsers/test_firmware.py create mode 100644 tests/parsers/test_inventory.py create mode 100644 tests/parsers/test_logs.py create mode 100644 tests/parsers/test_manager.py create mode 100644 tests/parsers/test_service_root.py create mode 100644 tests/parsers/test_system.py diff --git a/napalm_redfish/parsers/__init__.py b/napalm_redfish/parsers/__init__.py new file mode 100644 index 0000000..1399060 --- /dev/null +++ b/napalm_redfish/parsers/__init__.py @@ -0,0 +1,62 @@ +"""Pure parsers: one Redfish resource (a dict) in, plain Python values out. + +No I/O here. The transport fetches, these functions read; tests run them on recorded +responses under ``tests/fixtures/``. +""" + +from .accounts import parse_account, to_napalm_users +from .environment import ( + build_environment, + environment_sources, + parse_fan, + parse_power, + parse_power_subsystem, + parse_power_supply, + parse_sensor, + parse_thermal, + parse_thermal_metrics, + parse_thermal_subsystem, +) +from .firmware import parse_firmware +from .inventory import ( + parse_drive, + parse_memory, + parse_processor, + parse_simple_storage, + parse_storage, +) +from .logs import parse_log_entry, parse_log_service +from .manager import parse_ethernet_interface, parse_manager, parse_network_protocol +from .service_root import parse_service_root +from .system import parse_action_info, parse_power_state, parse_reset_action, parse_system + +__all__ = [ + "build_environment", + "environment_sources", + "parse_account", + "parse_action_info", + "parse_drive", + "parse_ethernet_interface", + "parse_fan", + "parse_firmware", + "parse_log_entry", + "parse_log_service", + "parse_manager", + "parse_memory", + "parse_network_protocol", + "parse_power", + "parse_power_state", + "parse_power_subsystem", + "parse_power_supply", + "parse_processor", + "parse_reset_action", + "parse_sensor", + "parse_service_root", + "parse_simple_storage", + "parse_storage", + "parse_system", + "parse_thermal", + "parse_thermal_metrics", + "parse_thermal_subsystem", + "to_napalm_users", +] diff --git a/napalm_redfish/parsers/accounts.py b/napalm_redfish/parsers/accounts.py new file mode 100644 index 0000000..14508d5 --- /dev/null +++ b/napalm_redfish/parsers/accounts.py @@ -0,0 +1,40 @@ +"""AccountService accounts (``AccountService/Accounts/*``).""" + +from __future__ import annotations + +from typing import Any, Iterable, Mapping + +from .common import boolean, text + +# Privilege level for NAPALM's get_users, from the Redfish standard roles. +ROLE_LEVELS = {"Administrator": 15, "Operator": 5, "ReadOnly": 1} + + +def parse_account(account: Mapping[str, Any]) -> dict[str, Any] | None: + """One account; ``None`` for an unused slot (no UserName), as iDRAC and Supermicro list.""" + username = text(account.get("UserName")) + if username is None: + return None + types = account.get("AccountTypes") + return { + "id": text(account.get("Id")), + "username": username, + "role_id": text(account.get("RoleId")), + "enabled": boolean(account.get("Enabled")), + "locked": boolean(account.get("Locked")), + "account_types": [str(t) for t in types] if isinstance(types, list) else [], + } + + +def to_napalm_users(accounts: Iterable[Mapping[str, Any] | None]) -> dict[str, dict[str, Any]]: + """NAPALM ``get_users`` from parsed accounts. Unknown (OEM) roles get level 0.""" + users = {} + for acc in accounts: + if not acc: + continue + users[acc["username"]] = { + "level": ROLE_LEVELS.get(acc.get("role_id") or "", 0), + "password": "", + "sshkeys": [], + } + return users diff --git a/napalm_redfish/parsers/common.py b/napalm_redfish/parsers/common.py new file mode 100644 index 0000000..7e6a567 --- /dev/null +++ b/napalm_redfish/parsers/common.py @@ -0,0 +1,134 @@ +"""Helpers shared by the parsers. All pure: dict in, plain values out.""" + +from __future__ import annotations + +from typing import Any, Mapping + +# DMI defaults that board vendors leave in place (Supermicro boards ship most of them). +PLACEHOLDERS = frozenset({ + "to be filled by o.e.m.", + "default string", + "not specified", + "not applicable", + "n/a", + "none", + "0123456789", + "system serial number", + "system product name", + "system manufacturer", +}) + + +def text(value: Any) -> str | None: + """A stripped string, ``None`` for missing or blank values.""" + if value is None: + return None + value = str(value).strip() + return value or None + + +def ident(value: Any) -> str | None: + """Like ``text``, but DMI placeholder strings count as missing too.""" + value = text(value) + if value is None or value.lower() in PLACEHOLDERS: + return None + return value + + +def number(value: Any) -> float | None: + """A float, ``None`` if the value is missing or not a number.""" + if isinstance(value, bool) or value is None: + return None + try: + return float(value) + except (TypeError, ValueError): + return None + + +def integer(value: Any) -> int | None: + if isinstance(value, bool) or value is None: + return None + try: + return int(value) + except (TypeError, ValueError): + return None + + +def boolean(value: Any) -> bool | None: + return value if isinstance(value, bool) else None + + +def odata_id(ref: Any) -> str | None: + """The ``@odata.id`` of a reference object, or ``None``.""" + if isinstance(ref, Mapping): + return text(ref.get("@odata.id")) + return None + + +def link(resource: Mapping[str, Any], *keys: str) -> str | None: + """Follow ``keys`` into nested objects and return the ``@odata.id`` found there.""" + value: Any = resource + for key in keys: + if not isinstance(value, Mapping): + return None + value = value.get(key) + return odata_id(value) + + +def links(resource: Mapping[str, Any]) -> dict[str, str]: + """All top-level properties of ``resource`` that are single references.""" + found = {} + for key, value in resource.items(): + if key.startswith("@") or key == "Links": + continue + path = odata_id(value) + if path is not None: + found[key] = path + return found + + +def link_list(resource: Mapping[str, Any], *keys: str) -> list[str]: + """The ``@odata.id`` of each reference in the array at ``keys``.""" + value: Any = resource + for key in keys: + if not isinstance(value, Mapping): + return [] + value = value.get(key) + if not isinstance(value, list): + return [] + return [path for path in (odata_id(ref) for ref in value) if path is not None] + + +def status(resource: Mapping[str, Any]) -> tuple[str | None, str | None]: + """``(Status.State, Status.Health)``.""" + st = resource.get("Status") + if not isinstance(st, Mapping): + return None, None + return text(st.get("State")), text(st.get("Health")) + + +def present(resource: Mapping[str, Any]) -> bool: + return status(resource)[0] != "Absent" + + +def location_label(resource: Mapping[str, Any]) -> str | None: + """``Location.PartLocation.ServiceLabel``, the printed label of a slot or bay.""" + loc = resource.get("Location") + if not isinstance(loc, Mapping): + return None + part = loc.get("PartLocation") + if not isinstance(part, Mapping): + return None + return text(part.get("ServiceLabel")) + + +def unique_name(names: Mapping[str, Any], name: str, suffix: Any) -> str: + """``name``, or ``name (suffix)`` if it is already taken.""" + if name not in names: + return name + candidate = f"{name} ({suffix})" + n = 2 + while candidate in names: + candidate = f"{name} ({suffix}-{n})" + n += 1 + return candidate diff --git a/napalm_redfish/parsers/environment.py b/napalm_redfish/parsers/environment.py new file mode 100644 index 0000000..e2f2f95 --- /dev/null +++ b/napalm_redfish/parsers/environment.py @@ -0,0 +1,307 @@ +"""Chassis environment: legacy Thermal/Power, or ThermalSubsystem/PowerSubsystem/Sensors. + +Services from Redfish 2020.4 on link ``ThermalSubsystem``, ``PowerSubsystem`` and +``Sensors`` from the Chassis and deprecate ``Thermal`` and ``Power``; older ones (such as +Supermicro X10) have only the latter. ``environment_sources`` picks per part, and both +paths end in the same shape, the parts of NAPALM's ``get_environment``: + +- ``fans``: ``{name: {"status": bool}}`` +- ``temperature``: ``{name: {"temperature": float, "is_alert": bool, "is_critical": bool}}`` +- ``power``: ``{name: {"status": bool, "capacity": float, "output": float}}`` (-1.0 unknown) + +plus ``voltages`` (``{name: {"voltage", "is_alert", "is_critical"}}``) and +``consumed_watts`` for the whole chassis. Absent parts and sensors without a reading are +left out. +""" + +from __future__ import annotations + +from typing import Any, Iterable, Mapping + +from .common import link, number, present, status, text, unique_name + +_SENSOR_TYPES_BY_UNITS = {"Cel": "Temperature", "V": "Voltage", "W": "Power", "A": "Current", + "RPM": "Rotational", "%": "Percent", "Hz": "Frequency"} + + +def environment_sources(chassis: Mapping[str, Any]) -> dict[str, Any]: + """Which resources to read for a Chassis, preferring the newer ones per part. + + Returns ``{"thermal": {"schema", "path"} | None, "power": ... | None, + "sensors": path | None}``. + """ + + def pick(new: str, old: str) -> dict[str, str] | None: + for schema in (new, old): + path = link(chassis, schema) + if path is not None: + return {"schema": schema, "path": path} + return None + + return { + "thermal": pick("ThermalSubsystem", "Thermal"), + "power": pick("PowerSubsystem", "Power"), + "sensors": link(chassis, "Sensors"), + } + + +def _ok(resource: Mapping[str, Any]) -> bool: + """Working: enabled (or no state given) and healthy (or no health given).""" + state, health = status(resource) + return state in (None, "Enabled") and health in (None, "OK") + + +def _health_flags(resource: Mapping[str, Any]) -> tuple[bool, bool]: + _, health = status(resource) + return health in ("Warning", "Critical"), health == "Critical" + + +def _legacy_flags(item: Mapping[str, Any], reading: float) -> tuple[bool, bool]: + """Alert/critical from Thermal/Power v1 threshold properties and health.""" + alert, critical = _health_flags(item) + + def beyond(suffix: str) -> bool: + upper = number(item.get(f"UpperThreshold{suffix}")) + lower = number(item.get(f"LowerThreshold{suffix}")) + return (upper is not None and reading >= upper) or (lower is not None and reading <= lower) + + if beyond("Critical") or beyond("Fatal"): + return True, True + if beyond("NonCritical"): + alert = True + return alert, critical + + +def _sensor_flags(sensor: Mapping[str, Any], reading: float) -> tuple[bool, bool]: + """Alert/critical from Sensor ``Thresholds`` and health.""" + alert, critical = _health_flags(sensor) + thresholds = sensor.get("Thresholds") if isinstance(sensor.get("Thresholds"), Mapping) else {} + + def beyond(level: str) -> bool: + def limit(name: str) -> float | None: + t = thresholds.get(name) + return number(t.get("Reading")) if isinstance(t, Mapping) else None + + upper, lower = limit(f"Upper{level}"), limit(f"Lower{level}") + return (upper is not None and reading >= upper) or (lower is not None and reading <= lower) + + if beyond("Critical") or beyond("Fatal"): + return True, True + if beyond("Caution"): + alert = True + return alert, critical + + +def _name(item: Mapping[str, Any], *keys: str) -> str: + for key in keys: + name = text(item.get(key)) + if name: + return name + return text(item.get("MemberId")) or text(item.get("Id")) or "unknown" + + +def _members(resource: Mapping[str, Any], key: str) -> Iterable[tuple[int, Mapping[str, Any]]]: + items = resource.get(key) + for index, item in enumerate(items if isinstance(items, list) else []): + if isinstance(item, Mapping): + yield index, item + + +# --- legacy ------------------------------------------------------------------------------- + + +def parse_thermal(thermal: Mapping[str, Any]) -> dict[str, Any]: + """``Chassis/{id}/Thermal``: ``fans`` and ``temperature``.""" + temperature: dict[str, Any] = {} + for index, item in _members(thermal, "Temperatures"): + reading = number(item.get("ReadingCelsius")) + if reading is None or not present(item): + continue + alert, critical = _legacy_flags(item, reading) + name = unique_name(temperature, _name(item, "Name"), item.get("MemberId", index)) + temperature[name] = {"temperature": reading, "is_alert": alert, "is_critical": critical} + + fans: dict[str, Any] = {} + for index, item in _members(thermal, "Fans"): + if not present(item): + continue + name = unique_name(fans, _name(item, "Name", "FanName"), item.get("MemberId", index)) + fans[name] = {"status": _ok(item)} + return {"fans": fans, "temperature": temperature} + + +def parse_power(power: Mapping[str, Any]) -> dict[str, Any]: + """``Chassis/{id}/Power``: ``power`` (supplies), ``voltages``, ``consumed_watts``.""" + supplies: dict[str, Any] = {} + for index, item in _members(power, "PowerSupplies"): + if not present(item): + continue + name = unique_name(supplies, _name(item, "Name"), item.get("MemberId", index)) + capacity = number(item.get("PowerCapacityWatts")) + output = number(item.get("LastPowerOutputWatts")) + supplies[name] = { + "status": _ok(item), + "capacity": capacity if capacity is not None else -1.0, + "output": output if output is not None else -1.0, + } + + voltages: dict[str, Any] = {} + for index, item in _members(power, "Voltages"): + reading = number(item.get("ReadingVolts")) + if reading is None or not present(item): + continue + alert, critical = _legacy_flags(item, reading) + name = unique_name(voltages, _name(item, "Name"), item.get("MemberId", index)) + voltages[name] = {"voltage": reading, "is_alert": alert, "is_critical": critical} + + consumed = None + for _, item in _members(power, "PowerControl"): + watts = number(item.get("PowerConsumedWatts")) + if watts is not None: + consumed = (consumed or 0.0) + watts + return {"power": supplies, "voltages": voltages, "consumed_watts": consumed} + + +# --- ThermalSubsystem, PowerSubsystem, Sensors -------------------------------------------- + + +def parse_thermal_subsystem(subsystem: Mapping[str, Any]) -> dict[str, Any]: + """``Chassis/{id}/ThermalSubsystem``: where the fans and metrics are.""" + state, health = status(subsystem) + return { + "fans_path": link(subsystem, "Fans"), + "thermal_metrics_path": link(subsystem, "ThermalMetrics"), + "state": state, + "health": health, + } + + +def parse_power_subsystem(subsystem: Mapping[str, Any]) -> dict[str, Any]: + """``Chassis/{id}/PowerSubsystem``: where the supplies are, total capacity.""" + state, health = status(subsystem) + return { + "power_supplies_path": link(subsystem, "PowerSupplies"), + "capacity_watts": number(subsystem.get("CapacityWatts")), + "state": state, + "health": health, + } + + +def _excerpt(resource: Mapping[str, Any], key: str, field: str = "Reading") -> float | None: + value = resource.get(key) + return number(value.get(field)) if isinstance(value, Mapping) else None + + +def parse_fan(fan: Mapping[str, Any]) -> dict[str, Any]: + """One member of ``ThermalSubsystem/Fans``.""" + state, health = status(fan) + rpm = _excerpt(fan, "SpeedPercent", "SpeedRPM") + return { + "id": text(fan.get("Id")), + "name": _name(fan, "Name"), + "present": present(fan), + "status": _ok(fan), + "speed_rpm": int(rpm) if rpm is not None else None, + "speed_percent": _excerpt(fan, "SpeedPercent"), + "state": state, + "health": health, + } + + +def parse_power_supply(supply: Mapping[str, Any], + metrics: Mapping[str, Any] | None = None) -> dict[str, Any]: + """One member of ``PowerSubsystem/PowerSupplies``, with its ``Metrics`` if read.""" + state, health = status(supply) + capacity = number(supply.get("PowerCapacityWatts")) + output = _excerpt(metrics, "OutputPowerWatts") if metrics else None + return { + "id": text(supply.get("Id")), + "name": _name(supply, "Name"), + "present": present(supply), + "status": _ok(supply), + "capacity": capacity if capacity is not None else -1.0, + "output": output if output is not None else -1.0, + "manufacturer": text(supply.get("Manufacturer")), + "model": text(supply.get("Model")), + "serial_number": text(supply.get("SerialNumber")), + "part_number": text(supply.get("PartNumber")), + "firmware_version": text(supply.get("FirmwareVersion")), + "state": state, + "health": health, + } + + +def parse_sensor(sensor: Mapping[str, Any]) -> dict[str, Any]: + """One member of ``Chassis/{id}/Sensors``. + + ``reading_type`` falls back to one derived from ``ReadingUnits`` (some sensors, such as + temperatures in the DMTF mockups, carry only units). + """ + state, health = status(sensor) + units = text(sensor.get("ReadingUnits")) + reading = number(sensor.get("Reading")) + alert, critical = _sensor_flags(sensor, reading) if reading is not None else (False, False) + return { + "id": text(sensor.get("Id")), + "name": _name(sensor, "Name"), + "reading_type": text(sensor.get("ReadingType")) or _SENSOR_TYPES_BY_UNITS.get(units or ""), + "reading": reading, + "units": units, + "physical_context": text(sensor.get("PhysicalContext")), + "electrical_context": text(sensor.get("ElectricalContext")), + "state": state, + "health": health, + "is_alert": alert, + "is_critical": critical, + } + + +def parse_thermal_metrics(metrics: Mapping[str, Any]) -> dict[str, Any]: + """``ThermalSubsystem/ThermalMetrics``: temperatures without thresholds. + + A fallback for services with a ThermalSubsystem but no Sensors collection. + """ + temperature: dict[str, Any] = {} + for index, item in _members(metrics, "TemperatureReadingsCelsius"): + reading = number(item.get("Reading")) + if reading is None: + continue + name = unique_name(temperature, _name(item, "DeviceName"), index) + temperature[name] = {"temperature": reading, "is_alert": False, "is_critical": False} + return {"temperature": temperature} + + +def build_environment(*, fans: Iterable[Mapping[str, Any]] = (), + power_supplies: Iterable[Mapping[str, Any]] = (), + sensors: Iterable[Mapping[str, Any]] = ()) -> dict[str, Any]: + """Combine parsed fans, power supplies and sensors into the shape of the legacy parsers. + + ``consumed_watts`` comes from the chassis total power sensor + (``ReadingType`` Power, ``ElectricalContext`` Total, ``PhysicalContext`` Chassis). + """ + env: dict[str, Any] = {"fans": {}, "temperature": {}, "power": {}, "voltages": {}, + "consumed_watts": None} + for fan in fans: + if fan["present"]: + name = unique_name(env["fans"], fan["name"], fan["id"]) + env["fans"][name] = {"status": fan["status"]} + for psu in power_supplies: + if psu["present"]: + name = unique_name(env["power"], psu["name"], psu["id"]) + env["power"][name] = {"status": psu["status"], "capacity": psu["capacity"], + "output": psu["output"]} + for sensor in sensors: + reading = sensor["reading"] + if reading is None: + continue + flags = {"is_alert": sensor["is_alert"], "is_critical": sensor["is_critical"]} + if sensor["reading_type"] == "Temperature": + name = unique_name(env["temperature"], sensor["name"], sensor["id"]) + env["temperature"][name] = {"temperature": reading, **flags} + elif sensor["reading_type"] == "Voltage": + name = unique_name(env["voltages"], sensor["name"], sensor["id"]) + env["voltages"][name] = {"voltage": reading, **flags} + elif (sensor["reading_type"] == "Power" and sensor["electrical_context"] == "Total" + and sensor["physical_context"] == "Chassis" and env["consumed_watts"] is None): + env["consumed_watts"] = reading + return env diff --git a/napalm_redfish/parsers/firmware.py b/napalm_redfish/parsers/firmware.py new file mode 100644 index 0000000..1c008e1 --- /dev/null +++ b/napalm_redfish/parsers/firmware.py @@ -0,0 +1,23 @@ +"""UpdateService firmware inventory (``UpdateService/FirmwareInventory/*``).""" + +from __future__ import annotations + +from typing import Any, Mapping + +from .common import boolean, link_list, status, text + + +def parse_firmware(item: Mapping[str, Any]) -> dict[str, Any]: + """One SoftwareInventory member: what it is, its version, whether it can be updated.""" + state, health = status(item) + return { + "id": text(item.get("Id")), + "name": text(item.get("Name")), + "version": text(item.get("Version")), + "updateable": boolean(item.get("Updateable")), + "manufacturer": text(item.get("Manufacturer")), + "release_date": text(item.get("ReleaseDate")), + "state": state, + "health": health, + "related": link_list(item, "RelatedItem"), + } diff --git a/napalm_redfish/parsers/inventory.py b/napalm_redfish/parsers/inventory.py new file mode 100644 index 0000000..6b7f4bb --- /dev/null +++ b/napalm_redfish/parsers/inventory.py @@ -0,0 +1,128 @@ +"""Hardware of the managed server: Processors, Memory, Storage, Drives, SimpleStorage.""" + +from __future__ import annotations + +from typing import Any, Mapping + +from .common import boolean, ident, integer, link, link_list, location_label, present, status, text + + +def parse_processor(cpu: Mapping[str, Any]) -> dict[str, Any]: + """One member of ``Systems/{id}/Processors``.""" + state, health = status(cpu) + return { + "id": text(cpu.get("Id")), + "name": text(cpu.get("Name")), + "socket": text(cpu.get("Socket")) or location_label(cpu), + "processor_type": text(cpu.get("ProcessorType")), + "manufacturer": ident(cpu.get("Manufacturer")), + "model": ident(cpu.get("Model")), + "architecture": text(cpu.get("ProcessorArchitecture")), + "instruction_set": text(cpu.get("InstructionSet")), + "max_speed_mhz": integer(cpu.get("MaxSpeedMHz")), + "cores": integer(cpu.get("TotalCores")), + "threads": integer(cpu.get("TotalThreads")), + "serial_number": ident(cpu.get("SerialNumber")), + "present": present(cpu), + "state": state, + "health": health, + } + + +def parse_memory(dimm: Mapping[str, Any]) -> dict[str, Any]: + """One member of ``Systems/{id}/Memory``. + + An enabled slot with a capacity of 0 counts as empty: some BMCs list empty slots so. + """ + state, health = status(dimm) + capacity = integer(dimm.get("CapacityMiB")) + return { + "id": text(dimm.get("Id")), + "name": text(dimm.get("Name")), + "location": (location_label(dimm) or text(dimm.get("DeviceLocator")) + or text(dimm.get("Name"))), + "capacity_mib": capacity, + "memory_device_type": text(dimm.get("MemoryDeviceType")), + "base_module_type": text(dimm.get("BaseModuleType")), + "speed_mhz": integer(dimm.get("OperatingSpeedMhz")), + "manufacturer": ident(dimm.get("Manufacturer")), + "part_number": ident(dimm.get("PartNumber")), + "serial_number": ident(dimm.get("SerialNumber")), + "present": present(dimm) and capacity != 0, + "state": state, + "health": health, + } + + +def _controller(ctrl: Mapping[str, Any]) -> dict[str, Any]: + state, health = status(ctrl) + return { + "name": text(ctrl.get("Name")), + "manufacturer": ident(ctrl.get("Manufacturer")), + "model": ident(ctrl.get("Model")), + "serial_number": ident(ctrl.get("SerialNumber")), + "firmware_version": text(ctrl.get("FirmwareVersion")), + "speed_gbps": ctrl.get("SpeedGbps") if isinstance(ctrl.get("SpeedGbps"), (int, float)) + else None, + "state": state, + "health": health, + } + + +def parse_storage(storage: Mapping[str, Any]) -> dict[str, Any]: + """One member of ``Systems/{id}/Storage``. + + Drives are separate resources: ``drive_paths`` lists them for ``parse_drive``. Storage + 1.15 and later move controllers into a collection at ``controllers_path``. + """ + state, health = status(storage) + controllers = storage.get("StorageControllers") + return { + "id": text(storage.get("Id")), + "name": text(storage.get("Name")), + "state": state, + "health": health, + "controllers": [_controller(c) for c in controllers if isinstance(c, Mapping)] + if isinstance(controllers, list) else [], + "controllers_path": link(storage, "Controllers"), + "drive_paths": link_list(storage, "Drives"), + } + + +def parse_drive(drive: Mapping[str, Any]) -> dict[str, Any]: + """A Drive resource, as linked from Storage.""" + state, health = status(drive) + return { + "id": text(drive.get("Id")), + "name": text(drive.get("Name")), + "manufacturer": ident(drive.get("Manufacturer")), + "model": ident(drive.get("Model")), + "serial_number": ident(drive.get("SerialNumber")), + "part_number": ident(drive.get("PartNumber")), + "revision": text(drive.get("Revision")), + "capacity_bytes": integer(drive.get("CapacityBytes")), + "media_type": text(drive.get("MediaType")), + "protocol": text(drive.get("Protocol")), + "failure_predicted": boolean(drive.get("FailurePredicted")), + "present": present(drive), + "state": state, + "health": health, + } + + +def parse_simple_storage(storage: Mapping[str, Any]) -> dict[str, Any]: + """One member of ``Systems/{id}/SimpleStorage`` (older services, no Storage). + + Its devices come out with the same keys as ``parse_drive``. + """ + state, health = status(storage) + devices = storage.get("Devices") + drives = [parse_drive(dev) for dev in devices if isinstance(dev, Mapping)] \ + if isinstance(devices, list) else [] + return { + "id": text(storage.get("Id")), + "name": text(storage.get("Name")), + "state": state, + "health": health, + "drives": drives, + } diff --git a/napalm_redfish/parsers/logs.py b/napalm_redfish/parsers/logs.py new file mode 100644 index 0000000..dfe3ff9 --- /dev/null +++ b/napalm_redfish/parsers/logs.py @@ -0,0 +1,41 @@ +"""LogServices and their entries (the SEL lives under Systems or Managers, by vendor).""" + +from __future__ import annotations + +from typing import Any, Mapping + +from .common import boolean, integer, link, status, text + + +def parse_log_service(service: Mapping[str, Any]) -> dict[str, Any]: + """One member of ``.../LogServices``.""" + state, health = status(service) + return { + "id": text(service.get("Id")), + "name": text(service.get("Name")), + "enabled": boolean(service.get("ServiceEnabled")), + "entries_path": link(service, "Entries"), + "max_records": integer(service.get("MaxNumberOfRecords")), + "overwrite_policy": text(service.get("OverWritePolicy")), + "entry_type": text(service.get("LogEntryType")), + "state": state, + "health": health, + } + + +def parse_log_entry(entry: Mapping[str, Any]) -> dict[str, Any]: + """One LogEntry (SEL record or Redfish event).""" + args = entry.get("MessageArgs") + return { + "id": text(entry.get("Id")), + "created": text(entry.get("Created")), + "severity": text(entry.get("Severity")), + "message": text(entry.get("Message")), + "message_id": text(entry.get("MessageId")), + "message_args": [str(a) for a in args] if isinstance(args, list) else [], + "entry_type": text(entry.get("EntryType")), + "entry_code": text(entry.get("EntryCode")), + "sensor_type": text(entry.get("SensorType")), + "sensor_number": integer(entry.get("SensorNumber")), + "origin": link(entry, "Links", "OriginOfCondition"), + } diff --git a/napalm_redfish/parsers/manager.py b/napalm_redfish/parsers/manager.py new file mode 100644 index 0000000..a9ac778 --- /dev/null +++ b/napalm_redfish/parsers/manager.py @@ -0,0 +1,150 @@ +"""Manager (the BMC itself), its EthernetInterfaces and its NetworkProtocol.""" + +from __future__ import annotations + +import ipaddress +from typing import Any, Mapping + +from .common import boolean, integer, link_list, links, number, status, text + + +def parse_manager(manager: Mapping[str, Any]) -> dict[str, Any]: + """``Managers/{id}``: model, firmware and links of the BMC.""" + state, health = status(manager) + return { + "id": text(manager.get("Id")), + "name": text(manager.get("Name")), + "manager_type": text(manager.get("ManagerType")), + "model": text(manager.get("Model")), + "firmware_version": text(manager.get("FirmwareVersion")), + "uuid": text(manager.get("UUID")), + "power_state": text(manager.get("PowerState")), + "date_time": text(manager.get("DateTime")), + "state": state, + "health": health, + "links": links(manager), + "manager_for_servers": link_list(manager, "Links", "ManagerForServers"), + "manager_for_chassis": link_list(manager, "Links", "ManagerForChassis"), + } + + +def _mac(value: Any) -> str: + mac = text(value) + return mac.upper().replace("-", ":") if mac else "" + + +def _ipv4(entries: Any) -> dict[str, dict[str, int]]: + found = {} + for entry in entries if isinstance(entries, list) else []: + if not isinstance(entry, Mapping): + continue + try: + addr = ipaddress.IPv4Address(text(entry.get("Address")) or "") + mask = text(entry.get("SubnetMask")) + prefix = ipaddress.IPv4Network(f"0.0.0.0/{mask}").prefixlen if mask else 32 + except ValueError: + continue + if addr.is_unspecified: + continue + found[str(addr)] = {"prefix_length": prefix} + return found + + +def _ipv6(entries: Any) -> dict[str, dict[str, int]]: + found = {} + for entry in entries if isinstance(entries, list) else []: + if not isinstance(entry, Mapping): + continue + try: + addr = ipaddress.IPv6Address(text(entry.get("Address")) or "") + except ValueError: + continue + if addr.is_unspecified: + continue + prefix = integer(entry.get("PrefixLength")) + found[str(addr)] = {"prefix_length": prefix if prefix is not None else 128} + return found + + +def parse_ethernet_interface(iface: Mapping[str, Any]) -> dict[str, Any]: + """One EthernetInterface, as NAPALM ``get_interfaces``/``get_interfaces_ip`` parts. + + ``interface`` is the ``get_interfaces`` value, ``ipv4``/``ipv6`` the ``get_interfaces_ip`` + one. Without ``LinkStatus`` (older schemas) an enabled interface counts as up. Unset + addresses (``0.0.0.0``, ``::``) are left out. + """ + state, _ = status(iface) + enabled = boolean(iface.get("InterfaceEnabled")) + if enabled is None: + enabled = state == "Enabled" + link_status = text(iface.get("LinkStatus")) + is_up = enabled and (link_status == "LinkUp" if link_status else state == "Enabled") + speed = number(iface.get("SpeedMbps")) + vlan = iface.get("VLAN") + vlan_id = None + if isinstance(vlan, Mapping) and vlan.get("VLANEnable"): + vlan_id = integer(vlan.get("VLANId")) + return { + "name": text(iface.get("Id")), + "interface": { + "is_up": bool(is_up), + "is_enabled": bool(enabled), + "description": text(iface.get("Description")) or "", + "last_flapped": -1.0, + "speed": speed if speed is not None else -1.0, + "mtu": integer(iface.get("MTUSize")) or 0, + "mac_address": _mac(iface.get("MACAddress")), + }, + "ipv4": _ipv4(iface.get("IPv4Addresses")), + "ipv6": _ipv6(iface.get("IPv6Addresses")), + "hostname": text(iface.get("HostName")), + "fqdn": text(iface.get("FQDN")), + "vlan": vlan_id, + "permanent_mac_address": _mac(iface.get("PermanentMACAddress")) or None, + } + + +def parse_network_protocol(proto: Mapping[str, Any]) -> dict[str, Any]: + """``Managers/{id}/NetworkProtocol``: which services the BMC runs, NTP and SNMP. + + ``protocols`` holds every property with a ``ProtocolEnabled`` flag, so OEM-free services + the schema adds later show up without code changes. NTP servers and the SNMP details + exist only in newer schema versions; missing values are ``None``. + """ + protocols = {} + for key, value in proto.items(): + if isinstance(value, Mapping) and "ProtocolEnabled" in value: + protocols[key] = { + "enabled": boolean(value.get("ProtocolEnabled")), + "port": integer(value.get("Port")), + } + + ntp = proto.get("NTP") if isinstance(proto.get("NTP"), Mapping) else {} + servers: list[str] = [] + for server in ntp.get("NTPServers") or []: + server = text(server) + if server and server not in servers: + servers.append(server) + + snmp = proto.get("SNMP") if isinstance(proto.get("SNMP"), Mapping) else {} + communities = [] + for community in snmp.get("CommunityStrings") or []: + if isinstance(community, Mapping): + # The community string itself is never kept, even if a BMC returns it. + communities.append({"name": text(community.get("Name")), + "access": text(community.get("AccessMode"))}) + + return { + "hostname": text(proto.get("HostName")), + "fqdn": text(proto.get("FQDN")), + "protocols": protocols, + "ntp": {"enabled": boolean(ntp.get("ProtocolEnabled")), "servers": servers}, + "snmp": { + "enabled": boolean(snmp.get("ProtocolEnabled")), + "port": integer(snmp.get("Port")), + "v1": boolean(snmp.get("EnableSNMPv1")), + "v2c": boolean(snmp.get("EnableSNMPv2c")), + "v3": boolean(snmp.get("EnableSNMPv3")), + "communities": communities, + }, + } diff --git a/napalm_redfish/parsers/service_root.py b/napalm_redfish/parsers/service_root.py new file mode 100644 index 0000000..2bf8f66 --- /dev/null +++ b/napalm_redfish/parsers/service_root.py @@ -0,0 +1,29 @@ +"""ServiceRoot: ``GET /redfish/v1/`` (readable without credentials).""" + +from __future__ import annotations + +from typing import Any, Mapping + +from .common import link, links, text + + +def parse_service_root(root: Mapping[str, Any]) -> dict[str, Any]: + """Version, identity and top-level links of a Redfish service. + + ``vendor`` and ``product`` exist from ServiceRoot 1.5 on; ``oem_vendors`` (the keys under + ``Oem``) is what older services offer for picking a quirk module. + """ + found = links(root) + sessions = link(root, "Links", "Sessions") + if sessions is not None: + found["Sessions"] = sessions + oem = root.get("Oem") + return { + "name": text(root.get("Name")), + "redfish_version": text(root.get("RedfishVersion")), + "uuid": text(root.get("UUID")), + "vendor": text(root.get("Vendor")), + "product": text(root.get("Product")), + "oem_vendors": sorted(oem) if isinstance(oem, Mapping) else [], + "links": found, + } diff --git a/napalm_redfish/parsers/system.py b/napalm_redfish/parsers/system.py new file mode 100644 index 0000000..753dbee --- /dev/null +++ b/napalm_redfish/parsers/system.py @@ -0,0 +1,73 @@ +"""ComputerSystem (``Systems/{id}``): the managed server, its power state and reset action.""" + +from __future__ import annotations + +from typing import Any, Mapping + +from .common import ident, integer, link_list, links, number, status, text + + +def parse_system(system: Mapping[str, Any]) -> dict[str, Any]: + """Identity, summaries and links of the managed server.""" + state, health = status(system) + cpus = system.get("ProcessorSummary") if isinstance(system.get("ProcessorSummary"), + Mapping) else {} + memory = system.get("MemorySummary") if isinstance(system.get("MemorySummary"), + Mapping) else {} + return { + "id": text(system.get("Id")), + "name": text(system.get("Name")), + "manufacturer": ident(system.get("Manufacturer")), + "model": ident(system.get("Model")), + "serial_number": ident(system.get("SerialNumber")), + "uuid": ident(system.get("UUID")), + "hostname": text(system.get("HostName")), + "sku": ident(system.get("SKU")), + "part_number": ident(system.get("PartNumber")), + "asset_tag": ident(system.get("AssetTag")), + "bios_version": text(system.get("BiosVersion")), + "system_type": text(system.get("SystemType")), + "power_state": parse_power_state(system), + "state": state, + "health": health, + "processor_count": integer(cpus.get("Count")), + "processor_model": text(cpus.get("Model")), + "memory_gib": number(memory.get("TotalSystemMemoryGiB")), + "links": links(system), + "chassis": link_list(system, "Links", "Chassis"), + "managed_by": link_list(system, "Links", "ManagedBy"), + } + + +def parse_power_state(system: Mapping[str, Any]) -> str | None: + """``PowerState``: On, Off, PoweringOn, PoweringOff or Paused; ``None`` if not reported.""" + return text(system.get("PowerState")) + + +def parse_reset_action(resource: Mapping[str, Any], + action: str = "#ComputerSystem.Reset") -> dict[str, Any]: + """Target and allowed ``ResetType`` values of a reset action. + + ``allowed`` is ``None`` when the service does not list the values inline. Then + ``action_info`` may name the ActionInfo resource that does (see ``parse_action_info``). + Works for ``#Manager.Reset`` too. + """ + actions = resource.get("Actions") + entry = actions.get(action) if isinstance(actions, Mapping) else None + if not isinstance(entry, Mapping): + return {"target": None, "allowed": None, "action_info": None} + allowed = entry.get("ResetType@Redfish.AllowableValues") + return { + "target": text(entry.get("target")), + "allowed": [str(v) for v in allowed] if isinstance(allowed, list) else None, + "action_info": text(entry.get("@Redfish.ActionInfo")), + } + + +def parse_action_info(info: Mapping[str, Any], parameter: str = "ResetType") -> list[str] | None: + """``AllowableValues`` of one parameter in an ActionInfo resource.""" + for param in info.get("Parameters") or []: + if isinstance(param, Mapping) and param.get("Name") == parameter: + values = param.get("AllowableValues") + return [str(v) for v in values] if isinstance(values, list) else None + return None diff --git a/tests/parsers/test_accounts.py b/tests/parsers/test_accounts.py new file mode 100644 index 0000000..8b9349c --- /dev/null +++ b/tests/parsers/test_accounts.py @@ -0,0 +1,52 @@ +from napalm_redfish.parsers import parse_account, to_napalm_users + + +def test_account(rackmount): + acc = parse_account(rackmount("/redfish/v1/AccountService/Accounts/1")) + assert acc == { + "id": "1", + "username": "Administrator", + "role_id": "Administrator", + "enabled": True, + "locked": False, + "account_types": ["Redfish"], + } + + +def test_account_snmp_user(rackmount): + acc = parse_account(rackmount("/redfish/v1/AccountService/Accounts/2")) + assert acc["username"] == "contoso_employee457" + assert acc["account_types"] == ["Redfish", "SNMP"] + + +def test_empty_account_slot_is_none(): + # iDRAC and Supermicro list every slot, unused ones with an empty UserName. + assert parse_account({"Id": "3", "UserName": "", "Enabled": False, "RoleId": "None"}) is None + assert parse_account({"Id": "4"}) is None + + +def test_account_defaults(): + acc = parse_account({"Id": "2", "UserName": "ADMIN"}) + assert acc["enabled"] is None + assert acc["locked"] is None + assert acc["role_id"] is None + assert acc["account_types"] == [] + + +def test_to_napalm_users(rackmount): + accounts = [parse_account(rackmount(f"/redfish/v1/AccountService/Accounts/{i}")) + for i in (1, 2)] + accounts.append({"id": "3", "username": "netork", "role_id": "ReadOnly", "enabled": True, + "locked": False, "account_types": []}) + accounts.append({"id": "4", "username": "ops", "role_id": "Operator", "enabled": False, + "locked": False, "account_types": []}) + accounts.append({"id": "5", "username": "custom", "role_id": "OemSpecial", "enabled": True, + "locked": False, "account_types": []}) + accounts.append(None) + assert to_napalm_users(accounts) == { + "Administrator": {"level": 15, "password": "", "sshkeys": []}, + "contoso_employee457": {"level": 15, "password": "", "sshkeys": []}, + "netork": {"level": 1, "password": "", "sshkeys": []}, + "ops": {"level": 5, "password": "", "sshkeys": []}, + "custom": {"level": 0, "password": "", "sshkeys": []}, + } diff --git a/tests/parsers/test_environment.py b/tests/parsers/test_environment.py new file mode 100644 index 0000000..86f3b53 --- /dev/null +++ b/tests/parsers/test_environment.py @@ -0,0 +1,283 @@ +import pytest + +from napalm_redfish.parsers import ( + build_environment, + environment_sources, + parse_fan, + parse_power, + parse_power_subsystem, + parse_power_supply, + parse_sensor, + parse_thermal, + parse_thermal_metrics, + parse_thermal_subsystem, +) + +CHASSIS = "/redfish/v1/Chassis/1U" + + +# --- choosing legacy or new resources ------------------------------------------------------ + + +def test_sources_prefer_new_resources(rackmount): + # This chassis links both; the new ones are current, Thermal/Power are deprecated. + src = environment_sources(rackmount(CHASSIS)) + assert src == { + "thermal": {"schema": "ThermalSubsystem", "path": CHASSIS + "/ThermalSubsystem"}, + "power": {"schema": "PowerSubsystem", "path": CHASSIS + "/PowerSubsystem"}, + "sensors": CHASSIS + "/Sensors", + } + + +def test_sources_legacy_only(localstorage): + src = environment_sources(localstorage(CHASSIS)) + assert src == { + "thermal": {"schema": "Thermal", "path": CHASSIS + "/Thermal"}, + "power": {"schema": "Power", "path": CHASSIS + "/Power"}, + "sensors": None, + } + + +def test_sources_mixed_and_missing(): + src = environment_sources({ + "ThermalSubsystem": {"@odata.id": "/redfish/v1/Chassis/X/ThermalSubsystem"}, + "Power": {"@odata.id": "/redfish/v1/Chassis/X/Power"}, + }) + assert src["thermal"]["schema"] == "ThermalSubsystem" + assert src["power"]["schema"] == "Power" + assert environment_sources({}) == {"thermal": None, "power": None, "sensors": None} + + +# --- legacy Thermal and Power -------------------------------------------------------------- + + +def test_thermal(rackmount): + env = parse_thermal(rackmount(CHASSIS + "/Thermal")) + assert env["temperature"] == { + "CPU1 Temp": {"temperature": 41.0, "is_alert": False, "is_critical": False}, + # "CPU2 Temp" is Disabled and has no reading: left out. + "Chassis Intake Temp": {"temperature": 25.0, "is_alert": False, "is_critical": False}, + } + assert env["fans"] == { + "BaseBoard System Fan": {"status": True}, + "BaseBoard System Fan Backup": {"status": True}, + } + + +@pytest.mark.parametrize( + "reading, health, alert, critical", + [ + (41, "OK", False, False), + (42, "OK", True, False), # at UpperThresholdNonCritical + (45, "OK", True, True), # at UpperThresholdCritical + (49, "OK", True, True), # above UpperThresholdFatal + (4, "OK", True, True), # below LowerThresholdCritical + (9, "OK", True, False), # below LowerThresholdNonCritical + (30, "Warning", True, False), + (30, "Critical", True, True), + ], +) +def test_thermal_thresholds(reading, health, alert, critical): + env = parse_thermal({"Temperatures": [{ + "Name": "T", "ReadingCelsius": reading, "Status": {"State": "Enabled", "Health": health}, + "UpperThresholdNonCritical": 42, "UpperThresholdCritical": 45, "UpperThresholdFatal": 48, + "LowerThresholdNonCritical": 10, "LowerThresholdCritical": 5, + }]}) + assert env["temperature"]["T"] == { + "temperature": float(reading), "is_alert": alert, "is_critical": critical} + + +def test_thermal_old_schema_names_and_duplicates(): + # Thermal 1.0 uses FanName; Supermicro repeats names across members. + env = parse_thermal({ + "Fans": [ + {"MemberId": "0", "FanName": "FAN1", "Reading": 3000, "Status": {"State": "Enabled", + "Health": "OK"}}, + {"MemberId": "1", "FanName": "FAN2", "Status": {"State": "Absent"}}, + {"MemberId": "2", "FanName": "FAN3", "Reading": 0, + "Status": {"State": "Enabled", "Health": "Critical"}}, + {"MemberId": "3", "FanName": "FAN1", "Reading": 2900, + "Status": {"State": "Enabled", "Health": "OK"}}, + ], + "Temperatures": [ + {"MemberId": "0", "Name": "System Temp", "ReadingCelsius": 30}, + {"MemberId": "1", "Name": "System Temp", "ReadingCelsius": 31}, + ], + }) + assert env["fans"] == { + "FAN1": {"status": True}, + "FAN3": {"status": False}, + "FAN1 (3)": {"status": True}, + } + assert list(env["temperature"]) == ["System Temp", "System Temp (1)"] + + +def test_power(rackmount): + env = parse_power(rackmount(CHASSIS + "/Power")) + assert env["power"] == { + # Health Warning: the supply works but reports a problem. + "Power Supply Bay": {"status": False, "capacity": 800.0, "output": 325.0}, + } + assert env["consumed_watts"] == 344.0 + assert env["voltages"] == { + "VRM1 Voltage": {"voltage": 12.0, "is_alert": False, "is_critical": False}, + "VRM2 Voltage": {"voltage": 5.0, "is_alert": False, "is_critical": False}, + } + + +def test_power_unknown_values_are_minus_one(): + env = parse_power({"PowerSupplies": [ + {"MemberId": "0", "Name": "PS1", "Status": {"State": "Enabled", "Health": "OK"}}, + {"MemberId": "1", "Name": "PS2", "Status": {"State": "Absent"}}, + ]}) + assert env["power"] == {"PS1": {"status": True, "capacity": -1.0, "output": -1.0}} + assert env["consumed_watts"] is None + assert env["voltages"] == {} + + +# --- ThermalSubsystem, PowerSubsystem, Sensors --------------------------------------------- + + +def test_thermal_subsystem(rackmount): + ts = parse_thermal_subsystem(rackmount(CHASSIS + "/ThermalSubsystem")) + assert ts == { + "fans_path": CHASSIS + "/ThermalSubsystem/Fans", + "thermal_metrics_path": CHASSIS + "/ThermalSubsystem/ThermalMetrics", + "state": "Enabled", + "health": "OK", + } + + +def test_power_subsystem(rackmount): + ps = parse_power_subsystem(rackmount(CHASSIS + "/PowerSubsystem")) + assert ps == { + "power_supplies_path": CHASSIS + "/PowerSubsystem/PowerSupplies", + "capacity_watts": 2000.0, + "state": "Enabled", + "health": "OK", + } + + +def test_fan(rackmount): + fan = parse_fan(rackmount(CHASSIS + "/ThermalSubsystem/Fans/CPU1")) + assert fan == { + "id": "CPU1", + "name": "Fan for CPU 1", + "present": True, + "status": True, + "speed_rpm": 1490, + "speed_percent": 45.0, + "state": "Enabled", + "health": "OK", + } + + +def test_power_supply_with_metrics(rackmount): + base = CHASSIS + "/PowerSubsystem/PowerSupplies/Bay1" + psu = parse_power_supply(rackmount(base), rackmount(base + "/Metrics")) + assert psu == { + "id": "Bay1", + "name": "Power Supply Bay 1", + "present": True, + "status": False, # predicted failure, Health Warning + "capacity": 400.0, + "output": 937.4, + "manufacturer": "Contoso Power", + "model": "RKS-440DC", + "serial_number": "3488247", + "part_number": "23456-133", + "firmware_version": "1.00", + "state": "Enabled", + "health": "Warning", + } + assert psu["output"] == 937.4 + + +def test_power_supply_without_metrics(rackmount): + base = CHASSIS + "/PowerSubsystem/PowerSupplies/Bay1" + assert parse_power_supply(rackmount(base))["output"] == -1.0 + absent = parse_power_supply(rackmount(CHASSIS + "/PowerSubsystem/PowerSupplies/Bay2")) + assert absent["present"] is False + assert absent["capacity"] == -1.0 + + +def test_sensor_temperature_by_units(rackmount): + # No ReadingType on this sensor: "Cel" makes it a temperature. + s = parse_sensor(rackmount(CHASSIS + "/Sensors/CPU1Temp")) + assert s["id"] == "CPU1Temp" + assert s["name"] == "CPU #1 Temperature" + assert s["reading_type"] == "Temperature" + assert s["reading"] == 44.0 + assert s["units"] == "Cel" + assert s["physical_context"] == "CPU" + assert s["health"] == "Warning" + assert s["is_alert"] is True # 44 >= UpperCaution 42 + assert s["is_critical"] is False # 44 < UpperCritical 45 + + +def test_sensor_power(rackmount): + s = parse_sensor(rackmount(CHASSIS + "/Sensors/TotalPower")) + assert s["reading_type"] == "Power" + assert s["reading"] == 374.0 + assert s["electrical_context"] == "Total" + assert s["is_alert"] is False + + +def test_sensor_voltage_and_fan(rackmount): + v = parse_sensor(rackmount(CHASSIS + "/Sensors/PS1InputVoltage")) + assert v["reading_type"] == "Voltage" + assert v["reading"] == 119.27 + assert v["is_alert"] is False + fan = parse_sensor(rackmount(CHASSIS + "/Sensors/CPUFan1")) + assert fan["reading_type"] == "Percent" + assert fan["reading"] == 45.0 + + +def test_sensor_without_reading(): + s = parse_sensor({"Id": "X", "Name": "X", "ReadingUnits": "Cel", + "Status": {"State": "Disabled"}}) + assert s["reading"] is None + assert s["is_alert"] is False + + +def test_thermal_metrics(rackmount): + env = parse_thermal_metrics(rackmount(CHASSIS + "/ThermalSubsystem/ThermalMetrics")) + assert env == {"temperature": { + "Intake": {"temperature": 24.8, "is_alert": False, "is_critical": False}, + "Exhaust": {"temperature": 40.5, "is_alert": False, "is_critical": False}, + }} + + +def test_build_environment_from_new_resources(rackmount): + fans = [parse_fan(rackmount(f"{CHASSIS}/ThermalSubsystem/Fans/{f}")) + for f in ("Bay1", "Bay2", "CPU1", "CPU2")] + bay1 = f"{CHASSIS}/PowerSubsystem/PowerSupplies/Bay1" + psus = [parse_power_supply(rackmount(bay1), rackmount(bay1 + "/Metrics")), + parse_power_supply(rackmount(f"{CHASSIS}/PowerSubsystem/PowerSupplies/Bay2"))] + sensors = [parse_sensor(rackmount(f"{CHASSIS}/Sensors/{s}")) + for s in ("CPU1Temp", "IntakeTemp", "CPUFan1", "TotalPower", "PS1InputVoltage")] + env = build_environment(fans=fans, power_supplies=psus, sensors=sensors) + assert env["fans"] == { + "Fan Bay 1": {"status": True}, + "Fan Bay 2": {"status": True}, + "Fan for CPU 1": {"status": True}, + "Fan for CPU 2": {"status": True}, + } + assert env["power"] == { + "Power Supply Bay 1": {"status": False, "capacity": 400.0, "output": 937.4}, + } + assert env["temperature"] == { + "CPU #1 Temperature": {"temperature": 44.0, "is_alert": True, "is_critical": False}, + "Front Panel Intake Temperature": { + "temperature": 24.8, "is_alert": False, "is_critical": False}, + } + assert env["consumed_watts"] == 374.0 + assert env["voltages"] == { + "Power Supply #1 Input Voltage": { + "voltage": 119.27, "is_alert": False, "is_critical": False}, + } + + +def test_build_environment_empty(): + assert build_environment() == { + "fans": {}, "temperature": {}, "power": {}, "voltages": {}, "consumed_watts": None} diff --git a/tests/parsers/test_firmware.py b/tests/parsers/test_firmware.py new file mode 100644 index 0000000..ff08211 --- /dev/null +++ b/tests/parsers/test_firmware.py @@ -0,0 +1,31 @@ +from napalm_redfish.parsers import parse_firmware + +INVENTORY = "/redfish/v1/UpdateService/FirmwareInventory" + + +def test_firmware_bmc(rackmount): + fw = parse_firmware(rackmount(INVENTORY + "/BMC")) + assert fw == { + "id": "BMC", + "name": "Contoso BMC Firmware", + "version": "1.45.455b66-rev4", + "updateable": True, + "manufacturer": "Contoso", + "release_date": "2017-08-22T12:00:00Z", + "state": "Enabled", + "health": "OK", + "related": ["/redfish/v1/Managers/BMC"], + } + + +def test_firmware_bios(rackmount): + fw = parse_firmware(rackmount(INVENTORY + "/BIOS")) + assert fw["version"] == "P79 v1.45" + assert fw["related"] == ["/redfish/v1/Systems/437XR1138R2/Bios"] + + +def test_firmware_minimal(): + fw = parse_firmware({"Id": "BMC", "Name": "BMC", "Version": "03.91"}) + assert fw["version"] == "03.91" + assert fw["updateable"] is None + assert fw["related"] == [] diff --git a/tests/parsers/test_inventory.py b/tests/parsers/test_inventory.py new file mode 100644 index 0000000..5ed9e72 --- /dev/null +++ b/tests/parsers/test_inventory.py @@ -0,0 +1,175 @@ +from napalm_redfish.parsers import ( + parse_drive, + parse_memory, + parse_processor, + parse_simple_storage, + parse_storage, +) + +SYSTEM = "/redfish/v1/Systems/437XR1138R2" + + +# --- Processors ---------------------------------------------------------------------------- + + +def test_processor(rackmount): + cpu = parse_processor(rackmount(SYSTEM + "/Processors/CPU1")) + assert cpu == { + "id": "CPU1", + "name": "Processor", + "socket": "CPU 1", + "processor_type": "CPU", + "manufacturer": "Intel(R) Corporation", + "model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series", + "architecture": "x86", + "instruction_set": "x86-64", + "max_speed_mhz": 3700, + "cores": 8, + "threads": 16, + "serial_number": None, + "present": True, + "state": "Enabled", + # The CPU itself is OK; its HealthRollup (temperature warning) is not its health. + "health": "OK", + } + + +def test_processor_absent(rackmount): + cpu = parse_processor(rackmount(SYSTEM + "/Processors/CPU2")) + assert cpu["present"] is False + assert cpu["state"] == "Absent" + assert cpu["health"] is None + assert cpu["socket"] == "CPU 2" + assert cpu["model"] is None + assert cpu["cores"] is None + + +def test_processor_fpga(rackmount): + fpga = parse_processor(rackmount(SYSTEM + "/Processors/FPGA1")) + assert fpga["processor_type"] == "FPGA" + assert fpga["model"] == "Stratix 10" + # No Socket property: the location label stands in. + assert fpga["socket"] == "U168" + + +# --- Memory -------------------------------------------------------------------------------- + + +def test_memory(rackmount): + dimm = parse_memory(rackmount(SYSTEM + "/Memory/DIMM1")) + assert dimm == { + "id": "DIMM1", + "name": "DIMM Slot 1", + "location": "DIMM 1", + "capacity_mib": 32768, + "memory_device_type": "DDR4", + "base_module_type": "RDIMM", + "speed_mhz": None, + "manufacturer": None, + "part_number": None, + "serial_number": None, + "present": True, + "state": "Enabled", + "health": "OK", + } + + +def test_memory_absent(rackmount): + dimm = parse_memory(rackmount(SYSTEM + "/Memory/DIMM4")) + assert dimm["present"] is False + assert dimm["capacity_mib"] is None + assert dimm["location"] == "DIMM 4" + + +def test_memory_older_schema(): + # Memory 1.0-1.x: DeviceLocator instead of Location, speed in OperatingSpeedMhz. + dimm = parse_memory({ + "Id": "1", "Name": "Memory 1", "DeviceLocator": "P1-DIMMA1", "CapacityMiB": 16384, + "OperatingSpeedMhz": 2133, "Manufacturer": "Samsung ", "PartNumber": "M393A2G40DB0-CPB", + "SerialNumber": "12345678", "MemoryDeviceType": "DDR4", + "Status": {"State": "Enabled", "Health": "OK"}, + }) + assert dimm["location"] == "P1-DIMMA1" + assert dimm["speed_mhz"] == 2133 + assert dimm["manufacturer"] == "Samsung" + assert dimm["serial_number"] == "12345678" + + +def test_memory_zero_capacity_is_empty_slot(): + # Some BMCs list empty slots as enabled with a capacity of 0. + dimm = parse_memory({"Id": "DIMM9", "CapacityMiB": 0, "Status": {"State": "Enabled"}}) + assert dimm["present"] is False + + +# --- Storage ------------------------------------------------------------------------------- + + +def test_storage(localstorage): + st = parse_storage(localstorage(SYSTEM + "/Storage/1")) + assert st["id"] == "1" + assert st["name"] == "Local Storage Controller" + assert st["state"] == "Enabled" + assert st["health"] == "OK" + assert st["controllers"] == [{ + "name": "Contoso Integrated RAID", + "manufacturer": "Contoso", + "model": "12Gbs Integrated RAID", + "serial_number": "2M220100SL", + "firmware_version": "1.0.0.7", + "speed_gbps": 12, + "state": "Enabled", + "health": "OK", + }] + assert st["drive_paths"] == [ + "/redfish/v1/Chassis/1U/Drives/35D38F11ACEF7BD3", + "/redfish/v1/Chassis/1U/Drives/3F5A8C54207B7233", + "/redfish/v1/Chassis/1U/Drives/32ADF365C6C1B7BD", + "/redfish/v1/Chassis/1U/Drives/3D58ECBC375FD9F2", + ] + assert st["controllers_path"] is None + + +def test_storage_with_controller_collection(): + # Storage 1.15 and later link a StorageController collection instead of an array. + st = parse_storage({"Id": "RAID", "Controllers": {"@odata.id": "/redfish/v1/x/Controllers"}}) + assert st["controllers"] == [] + assert st["controllers_path"] == "/redfish/v1/x/Controllers" + assert st["drive_paths"] == [] + + +def test_drive(localstorage): + drive = parse_drive(localstorage("/redfish/v1/Chassis/1U/Drives/35D38F11ACEF7BD3")) + assert drive == { + "id": "35D38F11ACEF7BD3", + "name": "Drive Sample", + "manufacturer": "Contoso", + "model": "C123", + "serial_number": "1234567", + "part_number": "C123-1111", + "revision": "100A", + "capacity_bytes": 899527000064, + "media_type": "HDD", + "protocol": "SAS", + "failure_predicted": False, + "present": True, + "state": "Enabled", + "health": "OK", + } + + +def test_simple_storage(rackmount): + st = parse_simple_storage(rackmount(SYSTEM + "/SimpleStorage/1")) + assert st["id"] == "1" + assert st["name"] == "Simple Storage Controller" + assert st["health"] == "OK" + assert [d["name"] for d in st["drives"]] == [ + "SATA Bay 1", "SATA Bay 2", "SATA Bay 3", "SATA Bay 4"] + bay1, bay2, bay3, _ = st["drives"] + assert bay1["model"] == "3000GT8" + assert bay1["capacity_bytes"] == 8000000000000 + assert bay1["present"] is True + assert bay2["health"] == "Warning" + assert bay3["present"] is False + assert bay3["model"] is None + # Same keys as parse_drive, so callers can treat both alike. + assert set(bay1) == set(parse_drive({})) diff --git a/tests/parsers/test_logs.py b/tests/parsers/test_logs.py new file mode 100644 index 0000000..50296ea --- /dev/null +++ b/tests/parsers/test_logs.py @@ -0,0 +1,64 @@ +from napalm_redfish.parsers import parse_log_entry, parse_log_service + +SYSTEM_LOG = "/redfish/v1/Systems/437XR1138R2/LogServices/Log1" +MANAGER_LOG = "/redfish/v1/Managers/BMC/LogServices/Log" + + +def test_log_service(rackmount): + ls = parse_log_service(rackmount(MANAGER_LOG)) + assert ls == { + "id": "Log", + "name": "System Log Service", + "enabled": True, + "entries_path": MANAGER_LOG + "/Entries", + "max_records": 1000, + "overwrite_policy": "WrapsWhenFull", + "entry_type": None, + "state": "Enabled", + "health": "OK", + } + + +def test_log_service_sel(): + entries = {"@odata.id": "/redfish/v1/Systems/1/LogServices/SEL/Entries"} + ls = parse_log_service({"Id": "SEL", "LogEntryType": "SEL", "Entries": entries}) + assert ls["entry_type"] == "SEL" + assert ls["entries_path"] == "/redfish/v1/Systems/1/LogServices/SEL/Entries" + assert ls["enabled"] is None + + +def test_sel_entry(rackmount): + (first, second) = rackmount(SYSTEM_LOG + "/Entries")["Members"] + entry = parse_log_entry(first) + assert entry == { + "id": "1", + "created": "2012-03-07T14:44:00Z", + "severity": "Critical", + "message": "Temperature threshold exceeded", + "message_id": "0x592A28", + "message_args": [], + "entry_type": "SEL", + "entry_code": "Upper Critical - going high", + "sensor_type": "Temperature", + "sensor_number": 1, + "origin": "/redfish/v1/Chassis/1U/Thermal", + } + assert parse_log_entry(second)["sensor_number"] == 2 + + +def test_event_entry(rackmount): + (entry,) = rackmount(MANAGER_LOG + "/Entries")["Members"] + parsed = parse_log_entry(entry) + assert parsed["entry_type"] == "Event" + assert parsed["message"] == "System May be Melting" + assert parsed["message_id"] == "Event.1.0.TempWayTooHot" + assert parsed["message_args"] == ["88"] + assert parsed["entry_code"] is None + + +def test_entry_minimal(): + entry = parse_log_entry({"Id": "7", "Message": " Power supply AC lost \n"}) + assert entry["message"] == "Power supply AC lost" + assert entry["created"] is None + assert entry["severity"] is None + assert entry["origin"] is None diff --git a/tests/parsers/test_manager.py b/tests/parsers/test_manager.py new file mode 100644 index 0000000..7052c26 --- /dev/null +++ b/tests/parsers/test_manager.py @@ -0,0 +1,159 @@ +import pytest + +from napalm_redfish.parsers import ( + parse_ethernet_interface, + parse_manager, + parse_network_protocol, +) + +MANAGER = "/redfish/v1/Managers/BMC" + + +def test_manager(rackmount): + m = parse_manager(rackmount(MANAGER)) + assert m["id"] == "BMC" + assert m["name"] == "Manager" + assert m["manager_type"] == "BMC" + assert m["model"] == "Joo Janta 200" + assert m["firmware_version"] == "1.45.455b66-rev4" + assert m["uuid"] == "58893887-8974-2487-2389-841168418919" + assert m["power_state"] == "On" + assert m["date_time"] == "2015-03-13T04:14:33+06:00" + assert m["state"] == "Enabled" + assert m["health"] == "OK" + assert m["links"]["EthernetInterfaces"] == MANAGER + "/EthernetInterfaces" + assert m["links"]["NetworkProtocol"] == MANAGER + "/NetworkProtocol" + assert m["links"]["LogServices"] == MANAGER + "/LogServices" + assert m["manager_for_servers"] == ["/redfish/v1/Systems/437XR1138R2"] + assert m["manager_for_chassis"] == ["/redfish/v1/Chassis/1U"] + + +def test_manager_minimal(): + m = parse_manager({"Id": "1", "FirmwareVersion": " 3.91 "}) + assert m["firmware_version"] == "3.91" + assert m["model"] is None + assert m["links"] == {} + assert m["manager_for_servers"] == [] + + +# --- EthernetInterfaces -------------------------------------------------------------------- + + +def test_ethernet_interface(rackmount): + eth = parse_ethernet_interface(rackmount(MANAGER + "/EthernetInterfaces/eth0")) + assert eth["name"] == "eth0" + assert eth["interface"] == { + "is_up": True, + "is_enabled": True, + "description": "Management network interface on a dedicated port", + "last_flapped": -1.0, + "speed": 100.0, + "mtu": 1500, + "mac_address": "23:11:8A:33:CF:EA", + } + assert eth["ipv4"] == {"192.168.0.10": {"prefix_length": 22}} + assert eth["ipv6"] == {"fe80::1ec1:deff:fe6f:1e24": {"prefix_length": 64}} + assert eth["hostname"] == "web483-bmc" + assert eth["fqdn"] == "web483-bmc.dmtf.org" + assert eth["vlan"] == 101 + assert eth["permanent_mac_address"] == "23:11:8A:33:CF:EA" + + +def test_ethernet_interface_without_link_status(rackmount): + # No LinkStatus (as on older schemas): enabled and Status Enabled counts as up. + eth = parse_ethernet_interface(rackmount(MANAGER + "/EthernetInterfaces/ToHost")) + assert eth["interface"]["is_up"] is True + assert eth["ipv4"] == {"192.168.20.55": {"prefix_length": 24}} + assert eth["vlan"] is None + + +@pytest.mark.parametrize( + "props, is_up, is_enabled", + [ + ({"LinkStatus": "LinkDown", "InterfaceEnabled": True}, False, True), + ({"LinkStatus": "NoLink", "InterfaceEnabled": True}, False, True), + ({"LinkStatus": "LinkUp", "InterfaceEnabled": False}, False, False), + ({"Status": {"State": "Disabled"}}, False, False), + ({"Status": {"State": "Enabled"}}, True, True), + ({}, False, False), + ], +) +def test_ethernet_link_state(props, is_up, is_enabled): + eth = parse_ethernet_interface({"Id": "1", **props}) + assert eth["interface"]["is_up"] is is_up + assert eth["interface"]["is_enabled"] is is_enabled + + +def test_ethernet_unset_addresses_and_formats(): + # Supermicro and others report unset addresses as 0.0.0.0 or "::" and MACs in lower case. + eth = parse_ethernet_interface({ + "Id": "1", + "MACAddress": "0c:c4:7a:aa:bb:cc", + "SpeedMbps": None, + "IPv4Addresses": [ + {"Address": "0.0.0.0", "SubnetMask": "0.0.0.0"}, + {"Address": "10.1.2.3", "SubnetMask": "255.255.255.0"}, + {"Address": "10.1.2.4", "SubnetMask": "garbage"}, + {"Address": None}, + ], + "IPv6Addresses": [ + {"Address": "::", "PrefixLength": 0}, + {"Address": "2001:DB8::5", "PrefixLength": 64}, + {"Address": "2001:db8::6"}, + ], + }) + assert eth["interface"]["mac_address"] == "0C:C4:7A:AA:BB:CC" + assert eth["interface"]["speed"] == -1.0 + assert eth["interface"]["mtu"] == 0 + assert eth["interface"]["description"] == "" + assert eth["ipv4"] == {"10.1.2.3": {"prefix_length": 24}} + assert eth["ipv6"] == {"2001:db8::5": {"prefix_length": 64}, + "2001:db8::6": {"prefix_length": 128}} + + +# --- NetworkProtocol ----------------------------------------------------------------------- + + +def test_network_protocol(rackmount): + np = parse_network_protocol(rackmount(MANAGER + "/NetworkProtocol")) + assert np["hostname"] == "web483-bmc" + assert np["fqdn"] == "web483-bmc.dmtf.org" + assert np["protocols"] == { + "HTTP": {"enabled": True, "port": 80}, + "HTTPS": {"enabled": True, "port": 443}, + "IPMI": {"enabled": True, "port": 623}, + "SSH": {"enabled": True, "port": 22}, + "SNMP": {"enabled": True, "port": 161}, + "VirtualMedia": {"enabled": True, "port": 17988}, + "SSDP": {"enabled": True, "port": 1900}, + "Telnet": {"enabled": True, "port": 23}, + "KVMIP": {"enabled": True, "port": 5288}, + } + assert np["ntp"] == {"enabled": None, "servers": []} + assert np["snmp"] == { + "enabled": True, "port": 161, "v1": True, "v2c": False, "v3": True, + "communities": [ + {"name": "Read-only community", "access": "Limited"}, + {"name": "Full access community", "access": "Full"}, + ], + } + + +def test_network_protocol_ntp(telemetry): + np = parse_network_protocol(telemetry("/redfish/v1/Managers/1/NetworkProtocol")) + assert np["ntp"] == { + "enabled": True, + "servers": ["0.pool.ntp.org", "1.pool.ntp.org", "time-a.nist.gov"], + } + assert np["protocols"]["NTP"] == {"enabled": True, "port": None} + # Older SNMP schema: only ProtocolEnabled and Port. + assert np["snmp"] == {"enabled": True, "port": 161, "v1": None, "v2c": None, "v3": None, + "communities": []} + + +def test_network_protocol_ntp_blank_entries(): + servers = ["", None, " pool.ntp.org ", "pool.ntp.org"] + np = parse_network_protocol({"NTP": {"ProtocolEnabled": True, "NTPServers": servers}}) + assert np["ntp"]["servers"] == ["pool.ntp.org"] + assert np["snmp"] == {"enabled": None, "port": None, "v1": None, "v2c": None, "v3": None, + "communities": []} diff --git a/tests/parsers/test_service_root.py b/tests/parsers/test_service_root.py new file mode 100644 index 0000000..2be1313 --- /dev/null +++ b/tests/parsers/test_service_root.py @@ -0,0 +1,46 @@ +from napalm_redfish.parsers import parse_service_root + + +def test_service_root(rackmount): + root = parse_service_root(rackmount("/redfish/v1/")) + assert root["redfish_version"] == "1.15.0" + assert root["uuid"] == "92384634-2938-2342-8820-489239905423" + assert root["name"] == "Root Service" + # Vendor and Product only exist from ServiceRoot 1.5 on, and this mockup has neither. + assert root["vendor"] is None + assert root["product"] is None + assert root["oem_vendors"] == [] + + +def test_service_root_links(rackmount): + links = parse_service_root(rackmount("/redfish/v1/"))["links"] + assert links["Systems"] == "/redfish/v1/Systems" + assert links["Chassis"] == "/redfish/v1/Chassis" + assert links["Managers"] == "/redfish/v1/Managers" + assert links["AccountService"] == "/redfish/v1/AccountService" + assert links["UpdateService"] == "/redfish/v1/UpdateService" + assert links["SessionService"] == "/redfish/v1/SessionService" + # Links.Sessions is where the session login goes. + assert links["Sessions"] == "/redfish/v1/SessionService/Sessions" + assert "@odata.id" not in links + + +def test_service_root_vendor_and_oem(): + root = parse_service_root({ + "RedfishVersion": "1.0.1", + "Vendor": "Supermicro", + "Product": "X10DRL-i", + "Oem": {"Supermicro": {"@odata.type": "#SmcServiceRoot"}}, + "Systems": {"@odata.id": "/redfish/v1/Systems"}, + }) + assert root["vendor"] == "Supermicro" + assert root["product"] == "X10DRL-i" + assert root["oem_vendors"] == ["Supermicro"] + assert root["uuid"] is None + assert root["links"] == {"Systems": "/redfish/v1/Systems"} + + +def test_service_root_empty(): + root = parse_service_root({}) + assert root["redfish_version"] is None + assert root["links"] == {} diff --git a/tests/parsers/test_system.py b/tests/parsers/test_system.py new file mode 100644 index 0000000..4080f2c --- /dev/null +++ b/tests/parsers/test_system.py @@ -0,0 +1,129 @@ +import pytest + +from napalm_redfish.parsers import ( + parse_action_info, + parse_power_state, + parse_reset_action, + parse_system, +) + +SYSTEM = "/redfish/v1/Systems/437XR1138R2" + + +def test_system_facts(rackmount): + s = parse_system(rackmount(SYSTEM)) + assert s["id"] == "437XR1138R2" + assert s["name"] == "WebFrontEnd483" + assert s["manufacturer"] == "Contoso" + assert s["model"] == "3500" + assert s["serial_number"] == "437XR1138R2" + assert s["uuid"] == "38947555-7742-3448-3784-823347823834" + assert s["hostname"] == "web483" + assert s["sku"] == "8675309" + assert s["part_number"] == "224071-J23" + assert s["asset_tag"] == "Chicago-45Z-2381" + assert s["bios_version"] == "P79 v1.45 (12/06/2017)" + assert s["system_type"] == "Physical" + assert s["power_state"] == "On" + assert s["state"] == "Enabled" + assert s["health"] == "OK" + assert s["processor_count"] == 2 + assert s["processor_model"] == "Multi-Core Intel(R) Xeon(R) processor 7xxx Series" + assert s["memory_gib"] == 96 + + +def test_system_links(rackmount): + links = parse_system(rackmount(SYSTEM))["links"] + assert links["Processors"] == SYSTEM + "/Processors" + assert links["Memory"] == SYSTEM + "/Memory" + assert links["SimpleStorage"] == SYSTEM + "/SimpleStorage" + assert links["EthernetInterfaces"] == SYSTEM + "/EthernetInterfaces" + assert links["LogServices"] == SYSTEM + "/LogServices" + assert "Storage" not in links + s = parse_system(rackmount(SYSTEM)) + assert s["chassis"] == ["/redfish/v1/Chassis/1U"] + assert s["managed_by"] == ["/redfish/v1/Managers/BMC"] + + +def test_system_placeholders_and_blanks_are_none(): + # Supermicro boards report the DMI defaults the board vendor never filled in. + s = parse_system({ + "Id": "1", + "Manufacturer": "Supermicro", + "Model": "X10DRL-i", + "SerialNumber": "0123456789", + "SKU": "To Be Filled By O.E.M.", + "HostName": "", + "UUID": " ", + "PartNumber": "Default string", + }) + assert s["manufacturer"] == "Supermicro" + assert s["model"] == "X10DRL-i" + assert s["serial_number"] is None + assert s["sku"] is None + assert s["hostname"] is None + assert s["uuid"] is None + assert s["part_number"] is None + assert s["processor_count"] is None + assert s["memory_gib"] is None + assert s["links"] == {} + assert s["chassis"] == [] + + +def test_power_state(rackmount): + assert parse_power_state(rackmount(SYSTEM)) == "On" + + +@pytest.mark.parametrize("value", ["On", "Off", "PoweringOn", "PoweringOff", "Paused"]) +def test_power_state_values(value): + assert parse_power_state({"PowerState": value}) == value + + +def test_power_state_missing(): + assert parse_power_state({}) is None + assert parse_power_state({"PowerState": None}) is None + + +def test_reset_action_inline_allowable_values(rackmount): + reset = parse_reset_action(rackmount(SYSTEM)) + assert reset["target"] == SYSTEM + "/Actions/ComputerSystem.Reset" + assert reset["allowed"] == [ + "On", "ForceOff", "GracefulShutdown", "GracefulRestart", "ForceRestart", + "Nmi", "ForceOn", "PushPowerButton", "FullPowerCycle", + ] + assert reset["action_info"] is None + + +def test_reset_action_with_action_info(telemetry): + reset = parse_reset_action(telemetry("/redfish/v1/Systems/1")) + assert reset["target"] == "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset" + # Values live in the ActionInfo resource, not inline: unknown until that is read. + assert reset["allowed"] is None + assert reset["action_info"] == "/redfish/v1/Systems/1/ResetActionInfo" + + +def test_action_info(telemetry): + allowed = parse_action_info(telemetry("/redfish/v1/Systems/1/ResetActionInfo")) + assert allowed == ["On", "ForceOff", "GracefulShutdown", "GracefulRestart", + "ForceRestart", "Nmi", "ForceOn", "PushPowerButton"] + + +def test_action_info_other_parameter(): + info = {"Parameters": [{"Name": "ResetType", "AllowableValues": ["On"]}, + {"Name": "Delay", "AllowableValues": ["0", "5"]}]} + assert parse_action_info(info, "Delay") == ["0", "5"] + assert parse_action_info(info, "Missing") is None + assert parse_action_info({}) is None + + +def test_reset_action_manager(rackmount): + reset = parse_reset_action(rackmount("/redfish/v1/Managers/BMC"), "#Manager.Reset") + assert reset["target"] == "/redfish/v1/Managers/BMC/Actions/Manager.Reset" + assert reset["allowed"] == ["ForceRestart", "GracefulRestart"] + + +def test_reset_action_missing(): + assert parse_reset_action({}) == {"target": None, "allowed": None, "action_info": None} + # Reset advertised without allowable values: allowed stays unknown, not empty. + reset = parse_reset_action({"Actions": {"#ComputerSystem.Reset": {"target": "/x"}}}) + assert reset == {"target": "/x", "allowed": None, "action_info": None} From 8b512b6e0d8dca0737c1a74b789a27e6dda4a472 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Mon, 12 Oct 2026 23:42:14 +0000 Subject: [PATCH 05/10] Update README and references for the transport and parsers --- README.md | 19 +++++++++++++++++-- docs/references.md | 7 ++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2474955..6100000 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,12 @@ NAPALM driver for server **baseboard management controllers (BMCs)** over boards that predate Redfish. Built on [napalm-device-types](https://git.netork.io/NAPALM/napalm-device-types). Written for netOrk. -**Status: planning.** There is no code yet. This repository holds the design so far. The -first test device is a Supermicro X10DRL-i (BMC firmware 3.91, Redfish). +**Status: early development.** The HTTPS transport (`napalm_redfish/transport.py`) and pure +parsers for the Redfish resources in [docs/redfish-map.md](docs/redfish-map.md) +(`napalm_redfish/parsers/`) exist and are tested against DMTF mockups. The driver class and +its `napalm.drivers` entry point wait for the `ManagementControllerDriver` role base in +napalm-device-types. The first test device is a Supermicro X10DRL-i (BMC firmware 3.91, +Redfish). ## Why Redfish, and why not one driver per vendor @@ -60,6 +64,17 @@ These follow the other drivers in the `NAPALM` organisation: - `napalm-device-types` is not on PyPI: install it from `git+https://git.netork.io/NAPALM/napalm-device-types.git` before this package. +## Development + +```bash +python -m venv .venv && . .venv/bin/activate +pip install -e '.[test]' +pytest +``` + +Fixtures from the DMTF mockups live under `tests/fixtures/dmtf-/`, unmodified, with +the DMTF license and a `COMMANDS.txt` naming the source commit and request paths. + ## License Apache License 2.0, see [LICENSE](LICENSE). diff --git a/docs/references.md b/docs/references.md index e6d3a72..4b672d2 100644 --- a/docs/references.md +++ b/docs/references.md @@ -22,9 +22,10 @@ Collected 2026-10-13. | `sushy-tools` (OpenStack) | Redfish emulator for tests | Apache-2.0 | active | | `pyghmi` | IPMI over LAN, pure Python | Apache-2.0 | release 1.6.19, 2026-07-13 on PyPI (the GitHub mirror is archived) | -Choosing between the thin `redfish` client and `sushy` is open. The thin client keeps -quirk handling in this driver; `sushy` brings an object model but also its own assumptions -about Ironic's needs. +The transport uses the thin `redfish` client, which keeps quirk handling in this driver; +`sushy` brings an object model but also its own assumptions about Ironic's needs. Note that +the `redfish` library turns certificate verification off whenever it is not given a CA file, +so the transport always passes one when verification is on. ## Supermicro From 209537a7930d0a065809488498c47b930031bfaf Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Tue, 13 Oct 2026 05:02:58 +0000 Subject: [PATCH 06/10] Extend parsers for the driver parse_power_supply also reads items of the legacy Power.PowerSupplies array, parse_ethernet_interface exposes the raw LinkStatus, and parse_network_protocol reports protocols that carry a Port without a ProtocolEnabled flag. --- napalm_redfish/parsers/environment.py | 24 +++++++++++++----------- napalm_redfish/parsers/manager.py | 7 ++++--- tests/parsers/test_environment.py | 14 ++++++++++++++ tests/parsers/test_manager.py | 7 +++++++ 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/napalm_redfish/parsers/environment.py b/napalm_redfish/parsers/environment.py index e2f2f95..508c299 100644 --- a/napalm_redfish/parsers/environment.py +++ b/napalm_redfish/parsers/environment.py @@ -134,16 +134,12 @@ def parse_power(power: Mapping[str, Any]) -> dict[str, Any]: """``Chassis/{id}/Power``: ``power`` (supplies), ``voltages``, ``consumed_watts``.""" supplies: dict[str, Any] = {} for index, item in _members(power, "PowerSupplies"): - if not present(item): + psu = parse_power_supply(item) + if not psu["present"]: continue - name = unique_name(supplies, _name(item, "Name"), item.get("MemberId", index)) - capacity = number(item.get("PowerCapacityWatts")) - output = number(item.get("LastPowerOutputWatts")) - supplies[name] = { - "status": _ok(item), - "capacity": capacity if capacity is not None else -1.0, - "output": output if output is not None else -1.0, - } + name = unique_name(supplies, psu["name"], item.get("MemberId", index)) + supplies[name] = {"status": psu["status"], "capacity": psu["capacity"], + "output": psu["output"]} voltages: dict[str, Any] = {} for index, item in _members(power, "Voltages"): @@ -210,12 +206,18 @@ def parse_fan(fan: Mapping[str, Any]) -> dict[str, Any]: def parse_power_supply(supply: Mapping[str, Any], metrics: Mapping[str, Any] | None = None) -> dict[str, Any]: - """One member of ``PowerSubsystem/PowerSupplies``, with its ``Metrics`` if read.""" + """One member of ``PowerSubsystem/PowerSupplies``, with its ``Metrics`` if read. + + An item of the legacy ``Power.PowerSupplies`` array parses too (``MemberId`` as id, + ``LastPowerOutputWatts`` as output). + """ state, health = status(supply) capacity = number(supply.get("PowerCapacityWatts")) output = _excerpt(metrics, "OutputPowerWatts") if metrics else None + if output is None: + output = number(supply.get("LastPowerOutputWatts")) return { - "id": text(supply.get("Id")), + "id": text(supply.get("Id")) or text(supply.get("MemberId")), "name": _name(supply, "Name"), "present": present(supply), "status": _ok(supply), diff --git a/napalm_redfish/parsers/manager.py b/napalm_redfish/parsers/manager.py index a9ac778..9cdbca6 100644 --- a/napalm_redfish/parsers/manager.py +++ b/napalm_redfish/parsers/manager.py @@ -99,6 +99,7 @@ def parse_ethernet_interface(iface: Mapping[str, Any]) -> dict[str, Any]: "ipv6": _ipv6(iface.get("IPv6Addresses")), "hostname": text(iface.get("HostName")), "fqdn": text(iface.get("FQDN")), + "link_status": link_status, "vlan": vlan_id, "permanent_mac_address": _mac(iface.get("PermanentMACAddress")) or None, } @@ -107,13 +108,13 @@ def parse_ethernet_interface(iface: Mapping[str, Any]) -> dict[str, Any]: def parse_network_protocol(proto: Mapping[str, Any]) -> dict[str, Any]: """``Managers/{id}/NetworkProtocol``: which services the BMC runs, NTP and SNMP. - ``protocols`` holds every property with a ``ProtocolEnabled`` flag, so OEM-free services - the schema adds later show up without code changes. NTP servers and the SNMP details + ``protocols`` holds every property with a ``ProtocolEnabled`` flag or a ``Port``, so + services the schema adds later show up without code changes. NTP servers and the SNMP details exist only in newer schema versions; missing values are ``None``. """ protocols = {} for key, value in proto.items(): - if isinstance(value, Mapping) and "ProtocolEnabled" in value: + if isinstance(value, Mapping) and ("ProtocolEnabled" in value or "Port" in value): protocols[key] = { "enabled": boolean(value.get("ProtocolEnabled")), "port": integer(value.get("Port")), diff --git a/tests/parsers/test_environment.py b/tests/parsers/test_environment.py index 86f3b53..7aecaa5 100644 --- a/tests/parsers/test_environment.py +++ b/tests/parsers/test_environment.py @@ -281,3 +281,17 @@ def test_build_environment_from_new_resources(rackmount): def test_build_environment_empty(): assert build_environment() == { "fans": {}, "temperature": {}, "power": {}, "voltages": {}, "consumed_watts": None} + + +def test_power_supply_legacy_item(rackmount): + # Items of the legacy Power.PowerSupplies array parse the same way: MemberId as id, + # LastPowerOutputWatts as output. + (item,) = rackmount(CHASSIS + "/Power")["PowerSupplies"] + psu = parse_power_supply(item) + assert psu["id"] == "0" + assert psu["name"] == "Power Supply Bay" + assert psu["capacity"] == 800.0 + assert psu["output"] == 325.0 + assert psu["model"] == "499253-B21" + assert psu["serial_number"] == "1Z0000001" + assert psu["status"] is False diff --git a/tests/parsers/test_manager.py b/tests/parsers/test_manager.py index 7052c26..d5e2e31 100644 --- a/tests/parsers/test_manager.py +++ b/tests/parsers/test_manager.py @@ -57,6 +57,7 @@ def test_ethernet_interface(rackmount): assert eth["fqdn"] == "web483-bmc.dmtf.org" assert eth["vlan"] == 101 assert eth["permanent_mac_address"] == "23:11:8A:33:CF:EA" + assert eth["link_status"] == "LinkUp" def test_ethernet_interface_without_link_status(rackmount): @@ -64,6 +65,7 @@ def test_ethernet_interface_without_link_status(rackmount): eth = parse_ethernet_interface(rackmount(MANAGER + "/EthernetInterfaces/ToHost")) assert eth["interface"]["is_up"] is True assert eth["ipv4"] == {"192.168.20.55": {"prefix_length": 24}} + assert eth["link_status"] is None assert eth["vlan"] is None @@ -151,6 +153,11 @@ def test_network_protocol_ntp(telemetry): "communities": []} +def test_network_protocol_port_without_flag(): + np = parse_network_protocol({"IPMI": {"Port": 623}, "Proxy": {"Enabled": True}}) + assert np["protocols"] == {"IPMI": {"enabled": None, "port": 623}} + + def test_network_protocol_ntp_blank_entries(): servers = ["", None, " pool.ntp.org ", "pool.ntp.org"] np = parse_network_protocol({"NTP": {"ProtocolEnabled": True, "NTPServers": servers}}) From 7c366955bb146726b5d18bce83b2a5616510644f Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Tue, 13 Oct 2026 05:02:58 +0000 Subject: [PATCH 07/10] Add mockup files for the driver tests UpdateService, the system's EthernetInterfaces and all chassis Sensors from public-rackmount1, unmodified. --- .../dmtf-public-rackmount1/COMMANDS.txt | 119 ++++++++++++------ .../Chassis/1U/Sensors/AmbientTemp/index.json | 16 +++ .../Sensors/Battery1InputCurrent/index.json | 30 +++++ .../Sensors/Battery1InputVoltage/index.json | 30 +++++ .../Sensors/Battery1OutputCurrent/index.json | 30 +++++ .../Sensors/Battery1OutputVoltage/index.json | 30 +++++ .../Sensors/Battery1StateOfHealth/index.json | 24 ++++ .../1U/Sensors/Battery1Temp/index.json | 29 +++++ .../Chassis/1U/Sensors/CPU1Power/index.json | 33 +++++ .../Chassis/1U/Sensors/CPUFan2/index.json | 36 ++++++ .../Chassis/1U/Sensors/DIMM1Temp/index.json | 71 +++++++++++ .../Chassis/1U/Sensors/DIMM2Temp/index.json | 71 +++++++++++ .../Chassis/1U/Sensors/DIMM3Temp/index.json | 71 +++++++++++ .../Chassis/1U/Sensors/ExhaustTemp/index.json | 16 +++ .../Chassis/1U/Sensors/FanBay1/index.json | 26 ++++ .../Chassis/1U/Sensors/FanBay2/index.json | 26 ++++ .../Chassis/1U/Sensors/PS1Energy/index.json | 21 ++++ .../1U/Sensors/PS1Frequency/index.json | 40 ++++++ .../1U/Sensors/PS1InputCurrent/index.json | 34 +++++ .../1U/Sensors/PS1InputPower/index.json | 36 ++++++ .../Chassis/1U/Sensors/PS1Out12V/index.json | 41 ++++++ .../1U/Sensors/PS1Out12VCurrent/index.json | 33 +++++ .../Chassis/1U/Sensors/PS1Out3V/index.json | 41 ++++++ .../1U/Sensors/PS1Out3VCurrent/index.json | 34 +++++ .../Chassis/1U/Sensors/PS1Out5V/index.json | 41 ++++++ .../1U/Sensors/PS1Out5VCurrent/index.json | 33 +++++ .../Chassis/1U/Sensors/PS2Energy/index.json | 18 +++ .../1U/Sensors/PS2Frequency/index.json | 13 ++ .../1U/Sensors/PS2InputCurrent/index.json | 14 +++ .../1U/Sensors/PS2InputPower/index.json | 14 +++ .../1U/Sensors/PS2InputVoltage/index.json | 14 +++ .../Chassis/1U/Sensors/PS2Out12V/index.json | 14 +++ .../1U/Sensors/PS2Out12VCurrent/index.json | 13 ++ .../Chassis/1U/Sensors/PS2Out3V/index.json | 14 +++ .../1U/Sensors/PS2Out3VCurrent/index.json | 13 ++ .../Chassis/1U/Sensors/PS2Out5V/index.json | 14 +++ .../1U/Sensors/PS2Out5VCurrent/index.json | 13 ++ .../Chassis/1U/Sensors/TotalEnergy/index.json | 20 +++ .../12446A3B0411/index.json | 48 +++++++ .../12446A3B8890/index.json | 45 +++++++ .../EthernetInterfaces/ToManager/index.json | 55 ++++++++ .../EthernetInterfaces/VLAN1/index.json | 52 ++++++++ .../437XR1138R2/EthernetInterfaces/index.json | 22 ++++ .../UpdateService/index.json | 45 +++++++ 44 files changed, 1415 insertions(+), 38 deletions(-) create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/AmbientTemp/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1InputCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1InputVoltage/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1OutputCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1OutputVoltage/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1StateOfHealth/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1Temp/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPU1Power/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPUFan2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM1Temp/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM2Temp/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM3Temp/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/ExhaustTemp/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/FanBay1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/FanBay2/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Energy/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Frequency/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputPower/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out12V/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out12VCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out3V/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out3VCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out5V/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out5VCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Energy/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Frequency/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputPower/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputVoltage/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out12V/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out12VCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out3V/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out3VCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out5V/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out5VCurrent/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/TotalEnergy/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/12446A3B0411/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/12446A3B8890/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/ToManager/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/VLAN1/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/index.json create mode 100644 tests/fixtures/dmtf-public-rackmount1/UpdateService/index.json diff --git a/tests/fixtures/dmtf-public-rackmount1/COMMANDS.txt b/tests/fixtures/dmtf-public-rackmount1/COMMANDS.txt index 15a7aa7..4bc4356 100644 --- a/tests/fixtures/dmtf-public-rackmount1/COMMANDS.txt +++ b/tests/fixtures/dmtf-public-rackmount1/COMMANDS.txt @@ -7,26 +7,62 @@ Files are unmodified copies. Each request path maps to / Request paths: GET /redfish/v1/ -GET /redfish/v1/Systems -GET /redfish/v1/Systems/437XR1138R2 -GET /redfish/v1/Systems/437XR1138R2/Processors -GET /redfish/v1/Systems/437XR1138R2/Processors/CPU1 -GET /redfish/v1/Systems/437XR1138R2/Processors/CPU2 -GET /redfish/v1/Systems/437XR1138R2/Processors/FPGA1 -GET /redfish/v1/Systems/437XR1138R2/Memory -GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM1 -GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM2 -GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM3 -GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM4 -GET /redfish/v1/Systems/437XR1138R2/SimpleStorage -GET /redfish/v1/Systems/437XR1138R2/SimpleStorage/1 -GET /redfish/v1/Systems/437XR1138R2/LogServices -GET /redfish/v1/Systems/437XR1138R2/LogServices/Log1 -GET /redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries +GET /redfish/v1/AccountService +GET /redfish/v1/AccountService/Accounts +GET /redfish/v1/AccountService/Accounts/1 +GET /redfish/v1/AccountService/Accounts/2 GET /redfish/v1/Chassis GET /redfish/v1/Chassis/1U -GET /redfish/v1/Chassis/1U/Thermal GET /redfish/v1/Chassis/1U/Power +GET /redfish/v1/Chassis/1U/PowerSubsystem +GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies +GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1 +GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics +GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2 +GET /redfish/v1/Chassis/1U/Sensors +GET /redfish/v1/Chassis/1U/Sensors/AmbientTemp +GET /redfish/v1/Chassis/1U/Sensors/Battery1InputCurrent +GET /redfish/v1/Chassis/1U/Sensors/Battery1InputVoltage +GET /redfish/v1/Chassis/1U/Sensors/Battery1OutputCurrent +GET /redfish/v1/Chassis/1U/Sensors/Battery1OutputVoltage +GET /redfish/v1/Chassis/1U/Sensors/Battery1StateOfHealth +GET /redfish/v1/Chassis/1U/Sensors/Battery1Temp +GET /redfish/v1/Chassis/1U/Sensors/CPU1Power +GET /redfish/v1/Chassis/1U/Sensors/CPU1Temp +GET /redfish/v1/Chassis/1U/Sensors/CPUFan1 +GET /redfish/v1/Chassis/1U/Sensors/CPUFan2 +GET /redfish/v1/Chassis/1U/Sensors/DIMM1Temp +GET /redfish/v1/Chassis/1U/Sensors/DIMM2Temp +GET /redfish/v1/Chassis/1U/Sensors/DIMM3Temp +GET /redfish/v1/Chassis/1U/Sensors/ExhaustTemp +GET /redfish/v1/Chassis/1U/Sensors/FanBay1 +GET /redfish/v1/Chassis/1U/Sensors/FanBay2 +GET /redfish/v1/Chassis/1U/Sensors/IntakeTemp +GET /redfish/v1/Chassis/1U/Sensors/PS1Energy +GET /redfish/v1/Chassis/1U/Sensors/PS1Frequency +GET /redfish/v1/Chassis/1U/Sensors/PS1InputCurrent +GET /redfish/v1/Chassis/1U/Sensors/PS1InputPower +GET /redfish/v1/Chassis/1U/Sensors/PS1InputVoltage +GET /redfish/v1/Chassis/1U/Sensors/PS1Out12V +GET /redfish/v1/Chassis/1U/Sensors/PS1Out12VCurrent +GET /redfish/v1/Chassis/1U/Sensors/PS1Out3V +GET /redfish/v1/Chassis/1U/Sensors/PS1Out3VCurrent +GET /redfish/v1/Chassis/1U/Sensors/PS1Out5V +GET /redfish/v1/Chassis/1U/Sensors/PS1Out5VCurrent +GET /redfish/v1/Chassis/1U/Sensors/PS2Energy +GET /redfish/v1/Chassis/1U/Sensors/PS2Frequency +GET /redfish/v1/Chassis/1U/Sensors/PS2InputCurrent +GET /redfish/v1/Chassis/1U/Sensors/PS2InputPower +GET /redfish/v1/Chassis/1U/Sensors/PS2InputVoltage +GET /redfish/v1/Chassis/1U/Sensors/PS2Out12V +GET /redfish/v1/Chassis/1U/Sensors/PS2Out12VCurrent +GET /redfish/v1/Chassis/1U/Sensors/PS2Out3V +GET /redfish/v1/Chassis/1U/Sensors/PS2Out3VCurrent +GET /redfish/v1/Chassis/1U/Sensors/PS2Out5V +GET /redfish/v1/Chassis/1U/Sensors/PS2Out5VCurrent +GET /redfish/v1/Chassis/1U/Sensors/TotalEnergy +GET /redfish/v1/Chassis/1U/Sensors/TotalPower +GET /redfish/v1/Chassis/1U/Thermal GET /redfish/v1/Chassis/1U/ThermalSubsystem GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay1 @@ -34,33 +70,40 @@ GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans/Bay2 GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU1 GET /redfish/v1/Chassis/1U/ThermalSubsystem/Fans/CPU2 GET /redfish/v1/Chassis/1U/ThermalSubsystem/ThermalMetrics -GET /redfish/v1/Chassis/1U/PowerSubsystem -GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies -GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1 -GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay1/Metrics -GET /redfish/v1/Chassis/1U/PowerSubsystem/PowerSupplies/Bay2 -GET /redfish/v1/Chassis/1U/Sensors -GET /redfish/v1/Chassis/1U/Sensors/CPU1Temp -GET /redfish/v1/Chassis/1U/Sensors/IntakeTemp -GET /redfish/v1/Chassis/1U/Sensors/CPUFan1 -GET /redfish/v1/Chassis/1U/Sensors/TotalPower -GET /redfish/v1/Chassis/1U/Sensors/PS1InputVoltage GET /redfish/v1/Managers GET /redfish/v1/Managers/BMC GET /redfish/v1/Managers/BMC/EthernetInterfaces -GET /redfish/v1/Managers/BMC/EthernetInterfaces/eth0 GET /redfish/v1/Managers/BMC/EthernetInterfaces/ToHost -GET /redfish/v1/Managers/BMC/NetworkProtocol +GET /redfish/v1/Managers/BMC/EthernetInterfaces/eth0 GET /redfish/v1/Managers/BMC/LogServices GET /redfish/v1/Managers/BMC/LogServices/Log GET /redfish/v1/Managers/BMC/LogServices/Log/Entries -GET /redfish/v1/AccountService -GET /redfish/v1/AccountService/Accounts -GET /redfish/v1/AccountService/Accounts/1 -GET /redfish/v1/AccountService/Accounts/2 -GET /redfish/v1/UpdateService/FirmwareInventory -GET /redfish/v1/UpdateService/FirmwareInventory/BMC -GET /redfish/v1/UpdateService/FirmwareInventory/SS -GET /redfish/v1/UpdateService/FirmwareInventory/BIOS +GET /redfish/v1/Managers/BMC/NetworkProtocol GET /redfish/v1/SessionService GET /redfish/v1/SessionService/Sessions +GET /redfish/v1/Systems +GET /redfish/v1/Systems/437XR1138R2 +GET /redfish/v1/Systems/437XR1138R2/EthernetInterfaces +GET /redfish/v1/Systems/437XR1138R2/EthernetInterfaces/12446A3B0411 +GET /redfish/v1/Systems/437XR1138R2/EthernetInterfaces/12446A3B8890 +GET /redfish/v1/Systems/437XR1138R2/EthernetInterfaces/ToManager +GET /redfish/v1/Systems/437XR1138R2/EthernetInterfaces/VLAN1 +GET /redfish/v1/Systems/437XR1138R2/LogServices +GET /redfish/v1/Systems/437XR1138R2/LogServices/Log1 +GET /redfish/v1/Systems/437XR1138R2/LogServices/Log1/Entries +GET /redfish/v1/Systems/437XR1138R2/Memory +GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM1 +GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM2 +GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM3 +GET /redfish/v1/Systems/437XR1138R2/Memory/DIMM4 +GET /redfish/v1/Systems/437XR1138R2/Processors +GET /redfish/v1/Systems/437XR1138R2/Processors/CPU1 +GET /redfish/v1/Systems/437XR1138R2/Processors/CPU2 +GET /redfish/v1/Systems/437XR1138R2/Processors/FPGA1 +GET /redfish/v1/Systems/437XR1138R2/SimpleStorage +GET /redfish/v1/Systems/437XR1138R2/SimpleStorage/1 +GET /redfish/v1/UpdateService +GET /redfish/v1/UpdateService/FirmwareInventory +GET /redfish/v1/UpdateService/FirmwareInventory/BIOS +GET /redfish/v1/UpdateService/FirmwareInventory/BMC +GET /redfish/v1/UpdateService/FirmwareInventory/SS diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/AmbientTemp/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/AmbientTemp/index.json new file mode 100644 index 0000000..473a4d3 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/AmbientTemp/index.json @@ -0,0 +1,16 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "AmbientTemp", + "Name": "Ambient Temperature", + "PhysicalContext": "Room", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 22.5, + "ReadingUnits": "Cel", + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/AmbientTemp", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1InputCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1InputCurrent/index.json new file mode 100644 index 0000000..5242fe4 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1InputCurrent/index.json @@ -0,0 +1,30 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "Battery1InputCurrent", + "Name": "Battery #1 Input Current", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingType": "Current", + "Reading": 0, + "ReadingUnits": "A", + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "PhysicalContext": "Battery", + "PhysicalSubContext": "Input", + "Thresholds": { + "UpperCritical": { + "Reading": 60, + "Activation": "Increasing", + "DwellTime": "PT1S" + }, + "UpperCaution": { + "Reading": 55, + "Activation": "Increasing", + "DwellTime": "PT1S" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1InputCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1InputVoltage/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1InputVoltage/index.json new file mode 100644 index 0000000..8f2441e --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1InputVoltage/index.json @@ -0,0 +1,30 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "Battery1InputVoltage", + "Name": "Battery #1 Input Voltage", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingType": "Voltage", + "Reading": 12.22, + "ReadingUnits": "V", + "ReadingRangeMin": 0, + "ReadingRangeMax": 15, + "PhysicalContext": "Battery", + "PhysicalSubContext": "Input", + "Thresholds": { + "UpperCritical": { + "Reading": 14, + "Activation": "Increasing", + "DwellTime": "PT1S" + }, + "UpperCaution": { + "Reading": 13, + "Activation": "Increasing", + "DwellTime": "PT1S" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1InputVoltage", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1OutputCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1OutputCurrent/index.json new file mode 100644 index 0000000..d4cde44 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1OutputCurrent/index.json @@ -0,0 +1,30 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "Battery1OutputCurrent", + "Name": "Battery #1 Output Current", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingType": "Current", + "Reading": 0, + "ReadingUnits": "A", + "ReadingRangeMin": 0, + "ReadingRangeMax": 15, + "PhysicalContext": "Battery", + "PhysicalSubContext": "Output", + "Thresholds": { + "UpperCritical": { + "Reading": 10, + "Activation": "Increasing", + "DwellTime": "PT1S" + }, + "UpperCaution": { + "Reading": 8, + "Activation": "Increasing", + "DwellTime": "PT1S" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1OutputCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1OutputVoltage/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1OutputVoltage/index.json new file mode 100644 index 0000000..02929b7 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1OutputVoltage/index.json @@ -0,0 +1,30 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "Battery1OutputVoltage", + "Name": "Battery #1 Output Voltage", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingType": "Voltage", + "Reading": 12.22, + "ReadingUnits": "V", + "ReadingRangeMin": 0, + "ReadingRangeMax": 15, + "PhysicalContext": "Battery", + "PhysicalSubContext": "Output", + "Thresholds": { + "UpperCritical": { + "Reading": 14, + "Activation": "Increasing", + "DwellTime": "PT1S" + }, + "UpperCaution": { + "Reading": 13, + "Activation": "Increasing", + "DwellTime": "PT1S" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1OutputVoltage", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1StateOfHealth/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1StateOfHealth/index.json new file mode 100644 index 0000000..8a21e87 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1StateOfHealth/index.json @@ -0,0 +1,24 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "Battery1StateOfHealth", + "Name": "Battery #1 State of Health", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingType": "Percent", + "Reading": 91, + "ReadingUnits": "%", + "ReadingRangeMin": 0, + "ReadingRangeMax": 100, + "PhysicalContext": "Battery", + "Thresholds": { + "LowerCritical": { + "Reading": 30, + "Activation": "Decreasing", + "DwellTime": "PT1S" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1StateOfHealth", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1Temp/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1Temp/index.json new file mode 100644 index 0000000..308c21b --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/Battery1Temp/index.json @@ -0,0 +1,29 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "Battery1Temp", + "Name": "Battery #1 Temperature", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ReadingType": "Temperature", + "Reading": 33, + "ReadingUnits": "Cel", + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "PhysicalContext": "Battery", + "Thresholds": { + "UpperCritical": { + "Reading": 60, + "DwellTime": "PT15S", + "Activation": "Increasing" + }, + "UpperCaution": { + "Reading": 50, + "Activation": "Increasing", + "DwellTime": "PT15S" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/Battery1Temp", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPU1Power/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPU1Power/index.json new file mode 100644 index 0000000..66aac52 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPU1Power/index.json @@ -0,0 +1,33 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "CPU1Power", + "Name": "Power reading for CPU #1", + "ReadingType": "Power", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 90, + "ReadingUnits": "W", + "Implementation": "Reported", + "ReadingTime": "2019-08-13T04:14:33+06:00", + "ReadingRangeMin": 0, + "ReadingRangeMax": 150, + "Accuracy": 0.2, + "Precision": 1, + "SensingInterval": "PT0.01S", + "PhysicalContext": "CPU", + "Thresholds": { + "UpperCriticalUser": { + "Reading": 115, + "Activation": "Increasing", + "DwellTime": "PT0.03S" + }, + "UpperCautionUser": { + "Reading": 82, + "DwellTime": "PT1S" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPU1Power", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPUFan2/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPUFan2/index.json new file mode 100644 index 0000000..3e47cf5 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/CPUFan2/index.json @@ -0,0 +1,36 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "CPUFan2", + "Name": "CPU #2 Fan Speed", + "PhysicalContext": "CPU", + "Reading": 45, + "SpeedRPM": 1490, + "ReadingRangeMax": 100, + "ReadingRangeMin": 0, + "ReadingType": "Percent", + "ReadingUnits": "%", + "Status": { + "Health": "OK", + "State": "Enabled" + }, + "Thresholds": { + "LowerCaution": { + "Reading": 5, + "DwellTime": "PT30S", + "Activation": "Decreasing", + "HysteresisReading": 2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "LowerCritical": { + "Reading": 0, + "DwellTime": "PT10S", + "Activation": "Decreasing", + "HysteresisReading": 2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/CPUFan2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM1Temp/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM1Temp/index.json new file mode 100644 index 0000000..2f3c204 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM1Temp/index.json @@ -0,0 +1,71 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "DIMM1Temp", + "Name": "DIMM #1 Temperature", + "PhysicalContext": "Memory", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 44, + "ReadingUnits": "Cel", + "Thresholds": { + "UpperCaution": { + "Reading": 55, + "DwellTime": "PT5M", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "UpperCautionUser": { + "Reading": 50, + "DwellTime": "PT5M", + "HysteresisReading": 0, + "HysteresisDuration": "PT10M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperCritical": { + "Reading": 65, + "DwellTime": "PT5M", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "UpperCriticalUser": { + "Reading": 60, + "DwellTime": "PT5M", + "HysteresisReading": -1, + "HysteresisDuration": "PT5M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperFatal": { + "Reading": 75, + "DwellTime": "PT30S", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT30S", + "@Redfish.WriteableProperties": [] + } + }, + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM1" + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/DIMM1Temp", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM2Temp/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM2Temp/index.json new file mode 100644 index 0000000..77e8028 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM2Temp/index.json @@ -0,0 +1,71 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "DIMM2Temp", + "Name": "DIMM #2 Temperature", + "PhysicalContext": "Memory", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 43, + "ReadingUnits": "Cel", + "Thresholds": { + "UpperCaution": { + "Reading": 55, + "DwellTime": "PT5M", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "UpperCautionUser": { + "Reading": 50, + "DwellTime": "PT5M", + "HysteresisReading": 0, + "HysteresisDuration": "PT10M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperCritical": { + "Reading": 65, + "DwellTime": "PT5M", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "UpperCriticalUser": { + "Reading": 60, + "DwellTime": "PT5M", + "HysteresisReading": -1, + "HysteresisDuration": "PT5M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperFatal": { + "Reading": 75, + "DwellTime": "PT30S", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT30S", + "@Redfish.WriteableProperties": [] + } + }, + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM2" + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/DIMM2Temp", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM3Temp/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM3Temp/index.json new file mode 100644 index 0000000..dc21944 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/DIMM3Temp/index.json @@ -0,0 +1,71 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "DIMM3Temp", + "Name": "DIMM #3 Temperature", + "PhysicalContext": "Memory", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 45, + "ReadingUnits": "Cel", + "Thresholds": { + "UpperCaution": { + "Reading": 55, + "DwellTime": "PT5M", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "UpperCautionUser": { + "Reading": 50, + "DwellTime": "PT5M", + "HysteresisReading": 0, + "HysteresisDuration": "PT10M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperCritical": { + "Reading": 65, + "DwellTime": "PT5M", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT1M", + "@Redfish.WriteableProperties": [] + }, + "UpperCriticalUser": { + "Reading": 60, + "DwellTime": "PT5M", + "HysteresisReading": -1, + "HysteresisDuration": "PT5M", + "@Redfish.WriteableProperties": [ + "Reading", + "DwellTime", + "HysteresisReading", + "HysteresisDuration" + ] + }, + "UpperFatal": { + "Reading": 75, + "DwellTime": "PT30S", + "Activation": "Increasing", + "HysteresisReading": -2, + "HysteresisDuration": "PT30S", + "@Redfish.WriteableProperties": [] + } + }, + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "RelatedItem": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/Memory/DIMM3" + } + ], + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/DIMM3Temp", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/ExhaustTemp/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/ExhaustTemp/index.json new file mode 100644 index 0000000..48af526 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/ExhaustTemp/index.json @@ -0,0 +1,16 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "ExhaustTemp", + "Name": "Fan Bay #1 Exhaust Temperature", + "PhysicalContext": "Exhaust", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 40.5, + "ReadingUnits": "Cel", + "ReadingRangeMin": 0, + "ReadingRangeMax": 75, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/ExhaustTemp", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/FanBay1/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/FanBay1/index.json new file mode 100644 index 0000000..2badaa5 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/FanBay1/index.json @@ -0,0 +1,26 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "FanBay1", + "Name": "Chassis Fan #1", + "PhysicalContext": "Chassis", + "Reading": 45, + "SpeedRPM": 1900, + "ReadingRangeMax": 100, + "ReadingRangeMin": 0, + "ReadingType": "Percent", + "ReadingUnits": "%", + "Status": { + "Health": "OK", + "State": "Enabled" + }, + "Thresholds": { + "LowerCaution": { + "Reading": 5 + }, + "LowerCritical": { + "Reading": 0 + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/FanBay1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/FanBay2/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/FanBay2/index.json new file mode 100644 index 0000000..268ae84 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/FanBay2/index.json @@ -0,0 +1,26 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "FanBay2", + "Name": "Chassis Fan #2", + "PhysicalContext": "Chassis", + "Reading": 45, + "SpeedRPM": 2100, + "ReadingRangeMax": 100, + "ReadingRangeMin": 0, + "ReadingType": "Percent", + "ReadingUnits": "%", + "Status": { + "Health": "OK", + "State": "Enabled" + }, + "Thresholds": { + "LowerCaution": { + "Reading": 5 + }, + "LowerCritical": { + "Reading": 0 + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/FanBay2", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Energy/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Energy/index.json new file mode 100644 index 0000000..1079697 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Energy/index.json @@ -0,0 +1,21 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1Energy", + "Name": "Power Supply #1 Energy", + "ReadingType": "EnergykWh", + "ElectricalContext": "Total", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 7855, + "ReadingUnits": "kW.h", + "PhysicalContext": "PowerSupply", + "Actions": { + "#Sensor.ResetMetrics": { + "target": "/redfish/v1/Chassis/1U/Sensors/PS1Energy/Actions/Sensor.ResetMetrics" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Energy", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Frequency/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Frequency/index.json new file mode 100644 index 0000000..773b3dd --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Frequency/index.json @@ -0,0 +1,40 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1Frequency", + "Name": "Power Supply #1 Frequency", + "ReadingType": "Frequency", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 60.1, + "ReadingUnits": "Hz", + "ReadingRangeMin": 0, + "ReadingRangeMax": 160, + "Accuracy": 0.02, + "Precision": 2, + "SensingInterval": "PT0.125S", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Input", + "Thresholds": { + "UpperCritical": { + "Reading": 60.37, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 60.2, + "DwellTime": "PT10M" + }, + "LowerCaution": { + "Reading": 59.5, + "DwellTime": "PT5M" + }, + "LowerCritical": { + "Reading": 58.75, + "DwellTime": "PT1M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Frequency", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputCurrent/index.json new file mode 100644 index 0000000..ab8a388 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputCurrent/index.json @@ -0,0 +1,34 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1InputCurrent", + "Name": "Power Supply #1 Input Current", + "ReadingType": "Current", + "ElectricalContext": "Total", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 8.92, + "ReadingUnits": "A", + "ReadingRangeMin": 0, + "ReadingRangeMax": 40, + "Accuracy": 0.05, + "Precision": 2, + "SensingInterval": "PT0.1S", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Input", + "Thresholds": { + "UpperCritical": { + "Reading": 10, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 9, + "Activation": "Increasing", + "DwellTime": "PT10M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1InputCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputPower/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputPower/index.json new file mode 100644 index 0000000..aa6a006 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1InputPower/index.json @@ -0,0 +1,36 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1InputPower", + "Name": "Power Supply #1 Input Power", + "ReadingType": "Power", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ElectricalContext": "Total", + "Reading": 374, + "ReadingUnits": "W", + "ReadingRangeMin": 0, + "ReadingRangeMax": 500, + "ApparentVA": 350, + "ReactiveVAR": 29, + "PowerFactor": 0.83, + "Accuracy": 1, + "Precision": 1, + "SensingInterval": "PT0.1S", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Input", + "Thresholds": { + "UpperCritical": { + "Reading": 525, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 510, + "DwellTime": "PT10M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1InputPower", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out12V/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out12V/index.json new file mode 100644 index 0000000..4b6f68a --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out12V/index.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1Out12V", + "Name": "Power Supply #1 12V Output Voltage", + "ReadingType": "Voltage", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ElectricalContext": "Total", + "Reading": 12.08, + "ReadingUnits": "V", + "ReadingRangeMin": 0, + "ReadingRangeMax": 15, + "Accuracy": 0.02, + "Precision": 2, + "SensingInterval": "PT0.125S", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Output", + "Thresholds": { + "UpperCritical": { + "Reading": 12.5, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 12.35, + "DwellTime": "PT10M" + }, + "LowerCaution": { + "Reading": 11.85, + "DwellTime": "PT5M" + }, + "LowerCritical": { + "Reading": 11.5, + "DwellTime": "PT1M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out12V", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out12VCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out12VCurrent/index.json new file mode 100644 index 0000000..8bd2ff7 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out12VCurrent/index.json @@ -0,0 +1,33 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1Out12VCurrent", + "Name": "Power Supply #1 12V Output Current", + "ReadingType": "Current", + "ElectricalContext": "Total", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 2.79, + "ReadingUnits": "A", + "ReadingRangeMin": 0, + "ReadingRangeMax": 40, + "Accuracy": 0.05, + "Precision": 2, + "SensingInterval": "PT0.1S", + "PhysicalContext": "Chassis", + "Thresholds": { + "UpperCritical": { + "Reading": 7, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 6, + "Activation": "Increasing", + "DwellTime": "PT10M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out12VCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out3V/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out3V/index.json new file mode 100644 index 0000000..f99f4a5 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out3V/index.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1Out3V", + "Name": "Power Supply #1 3V Output Voltage", + "ReadingType": "Voltage", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ElectricalContext": "Total", + "Reading": 3.32, + "ReadingUnits": "V", + "ReadingRangeMin": 0, + "ReadingRangeMax": 5, + "Accuracy": 0.02, + "Precision": 2, + "SensingInterval": "PT0.125S", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Output", + "Thresholds": { + "UpperCritical": { + "Reading": 3.5, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 3.35, + "DwellTime": "PT10M" + }, + "LowerCaution": { + "Reading": 3.25, + "DwellTime": "PT5M" + }, + "LowerCritical": { + "Reading": 3.1, + "DwellTime": "PT1M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out3V", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out3VCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out3VCurrent/index.json new file mode 100644 index 0000000..499c19e --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out3VCurrent/index.json @@ -0,0 +1,34 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1Out3VCurrent", + "Name": "Power Supply #1 3V Output Current", + "ReadingType": "Current", + "ElectricalContext": "Total", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 8.92, + "ReadingUnits": "A", + "ReadingRangeMin": 0, + "ReadingRangeMax": 40, + "Accuracy": 0.05, + "Precision": 2, + "SensingInterval": "PT0.1S", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Output", + "Thresholds": { + "UpperCritical": { + "Reading": 25, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 20, + "Activation": "Increasing", + "DwellTime": "PT10M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out3VCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out5V/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out5V/index.json new file mode 100644 index 0000000..3f70c05 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out5V/index.json @@ -0,0 +1,41 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1Out5V", + "Name": "Power Supply #1 5V Output Voltage", + "ReadingType": "Voltage", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "ElectricalContext": "Total", + "Reading": 5.04, + "ReadingUnits": "V", + "ReadingRangeMin": 0, + "ReadingRangeMax": 10, + "Accuracy": 0.02, + "Precision": 2, + "SensingInterval": "PT0.125S", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Output", + "Thresholds": { + "UpperCritical": { + "Reading": 5.5, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 5.35, + "DwellTime": "PT10M" + }, + "LowerCaution": { + "Reading": 4.9, + "DwellTime": "PT5M" + }, + "LowerCritical": { + "Reading": 4.5, + "DwellTime": "PT1M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out5V", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out5VCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out5VCurrent/index.json new file mode 100644 index 0000000..d2c5ccd --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS1Out5VCurrent/index.json @@ -0,0 +1,33 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS1Out5VCurrent", + "Name": "Power Supply #1 5V Output Current", + "ReadingType": "Current", + "ElectricalContext": "Total", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 3.41, + "ReadingUnits": "A", + "ReadingRangeMin": 0, + "ReadingRangeMax": 10, + "Accuracy": 0.05, + "Precision": 2, + "SensingInterval": "PT0.1S", + "PhysicalContext": "PowerSupply", + "Thresholds": { + "UpperCritical": { + "Reading": 8, + "Activation": "Increasing", + "DwellTime": "PT1M" + }, + "UpperCaution": { + "Reading": 5, + "Activation": "Increasing", + "DwellTime": "PT10M" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS1Out5VCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Energy/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Energy/index.json new file mode 100644 index 0000000..f6cea81 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Energy/index.json @@ -0,0 +1,18 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2Energy", + "Name": "Power Supply #2 Energy", + "ReadingType": "EnergykWh", + "ElectricalContext": "Total", + "Status": { + "State": "Absent" + }, + "PhysicalContext": "PowerSupply", + "Actions": { + "#Sensor.ResetMetrics": { + "target": "/redfish/v1/Chassis/1U/Sensors/PS2Energy/Actions/Sensor.ResetMetrics" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Energy", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Frequency/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Frequency/index.json new file mode 100644 index 0000000..b098664 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Frequency/index.json @@ -0,0 +1,13 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2Frequency", + "Name": "Power Supply #2 Frequency", + "ReadingType": "Frequency", + "Status": { + "State": "Absent" + }, + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Input", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Frequency", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputCurrent/index.json new file mode 100644 index 0000000..ce670bf --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputCurrent/index.json @@ -0,0 +1,14 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2InputCurrent", + "Name": "Power Supply #2 Input Current", + "ReadingType": "Current", + "ElectricalContext": "Total", + "Status": { + "State": "Absent" + }, + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Input", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2InputCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputPower/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputPower/index.json new file mode 100644 index 0000000..150218b --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputPower/index.json @@ -0,0 +1,14 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2InputPower", + "Name": "Power Supply #2 Input Power", + "ReadingType": "Power", + "Status": { + "State": "Absent" + }, + "ElectricalContext": "Total", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Input", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2InputPower", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputVoltage/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputVoltage/index.json new file mode 100644 index 0000000..cf7b742 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2InputVoltage/index.json @@ -0,0 +1,14 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2InputVoltage", + "Name": "Power Supply #2 Input Voltage", + "ReadingType": "Voltage", + "Status": { + "State": "Absent" + }, + "ElectricalContext": "Total", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Input", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2InputVoltage", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out12V/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out12V/index.json new file mode 100644 index 0000000..25345ab --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out12V/index.json @@ -0,0 +1,14 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2Out12V", + "Name": "Power Supply #2 12V Output Voltage", + "ReadingType": "Voltage", + "Status": { + "State": "Absent" + }, + "ElectricalContext": "Total", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Output", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out12V", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out12VCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out12VCurrent/index.json new file mode 100644 index 0000000..3a5397a --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out12VCurrent/index.json @@ -0,0 +1,13 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2Out12VCurrent", + "Name": "Power Supply #2 12V Output Current", + "ReadingType": "Current", + "ElectricalContext": "Total", + "Status": { + "State": "Absent" + }, + "PhysicalContext": "PowerSupply", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out12VCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out3V/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out3V/index.json new file mode 100644 index 0000000..ec1763c --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out3V/index.json @@ -0,0 +1,14 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2Out3V", + "Name": "Power Supply #2 3V Output Voltage", + "ReadingType": "Voltage", + "Status": { + "State": "Absent" + }, + "ElectricalContext": "Total", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Output", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out3V", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out3VCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out3VCurrent/index.json new file mode 100644 index 0000000..9c1e555 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out3VCurrent/index.json @@ -0,0 +1,13 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2Out3VCurrent", + "Name": "Power Supply #2 3V Output Current", + "ReadingType": "Current", + "ElectricalContext": "Total", + "Status": { + "State": "Absent" + }, + "PhysicalContext": "PowerSupply", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out3VCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out5V/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out5V/index.json new file mode 100644 index 0000000..39ebca0 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out5V/index.json @@ -0,0 +1,14 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2Out5V", + "Name": "Power Supply #2 5V Output Voltage", + "ReadingType": "Voltage", + "Status": { + "State": "Absent" + }, + "ElectricalContext": "Total", + "PhysicalContext": "PowerSupply", + "PhysicalSubContext": "Output", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out5V", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out5VCurrent/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out5VCurrent/index.json new file mode 100644 index 0000000..16b75e5 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/PS2Out5VCurrent/index.json @@ -0,0 +1,13 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "PS2Out5VCurrent", + "Name": "Power Supply #2 5V Output Current", + "ReadingType": "Current", + "ElectricalContext": "Total", + "Status": { + "State": "Absent" + }, + "PhysicalContext": "PowerSupply", + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/PS2Out5VCurrent", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/TotalEnergy/index.json b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/TotalEnergy/index.json new file mode 100644 index 0000000..49ebac6 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Chassis/1U/Sensors/TotalEnergy/index.json @@ -0,0 +1,20 @@ +{ + "@odata.type": "#Sensor.v1_14_0.Sensor", + "Id": "TotalEnergy", + "Name": "Total Energy", + "ReadingType": "EnergykWh", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "Reading": 325675, + "ReadingUnits": "kW.h", + "PhysicalContext": "Chassis", + "Actions": { + "#Sensor.ResetMetrics": { + "target": "/redfish/v1/Chassis/1U/Sensors/TotalEnergy/Actions/Sensor.ResetMetrics" + } + }, + "@odata.id": "/redfish/v1/Chassis/1U/Sensors/TotalEnergy", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/12446A3B0411/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/12446A3B0411/index.json new file mode 100644 index 0000000..1fa2e05 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/12446A3B0411/index.json @@ -0,0 +1,48 @@ +{ + "@odata.type": "#EthernetInterface.v1_12_6.EthernetInterface", + "Id": "12446A3B0411", + "Name": "Ethernet Interface", + "Description": "System NIC 1", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "EthernetInterfaceType": "Physical", + "LinkStatus": "LinkUp", + "PermanentMACAddress": "12:44:6A:3B:04:11", + "MACAddress": "12:44:6A:3B:04:11", + "SpeedMbps": 1000, + "FullDuplex": true, + "HostName": "web483", + "FQDN": "web483.contoso.com", + "IPv6DefaultGateway": "fe80::3ed9:2bff:fe34:600", + "NameServers": [ + "names.contoso.com" + ], + "IPv4Addresses": [ + { + "Address": "192.168.0.10", + "SubnetMask": "255.255.252.0", + "AddressOrigin": "Static", + "Gateway": "192.168.0.1" + } + ], + "IPv6Addresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e24", + "PrefixLength": 64, + "AddressOrigin": "Static", + "AddressState": "Preferred" + } + ], + "TeamMode": "None", + "Links": { + "AffiliatedInterfaces": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/VLAN1" + } + ] + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/12446A3B0411", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/12446A3B8890/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/12446A3B8890/index.json new file mode 100644 index 0000000..89fff15 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/12446A3B8890/index.json @@ -0,0 +1,45 @@ +{ + "@odata.type": "#EthernetInterface.v1_12_6.EthernetInterface", + "Id": "12446A3B8890", + "Name": "Ethernet Interface", + "Description": "System NIC 2", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "EthernetInterfaceType": "Physical", + "LinkStatus": "LinkUp", + "PermanentMACAddress": "12:44:6A:3B:88:90", + "MACAddress": "AA:BB:CC:DD:EE:00", + "SpeedMbps": 1000, + "FullDuplex": true, + "HostName": "backup-web483", + "FQDN": "backup-web483.contoso.com", + "IPv6DefaultGateway": "fe80::3ed9:2bff:fe34:600", + "NameServers": [ + "names.contoso.com" + ], + "IPv4Addresses": [ + { + "Address": "192.168.0.11", + "SubnetMask": "255.255.255.0", + "AddressOrigin": "Static", + "Gateway": "192.168.0.1" + } + ], + "IPv6Addresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e33", + "PrefixLength": 64, + "AddressOrigin": "Static", + "AddressState": "Preferred" + } + ], + "VLAN": { + "VLANEnable": true, + "VLANId": 101 + }, + "TeamMode": "None", + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/12446A3B8890", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/ToManager/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/ToManager/index.json new file mode 100644 index 0000000..7638f53 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/ToManager/index.json @@ -0,0 +1,55 @@ +{ + "@odata.type": "#EthernetInterface.v1_12_6.EthernetInterface", + "Id": "ToManager", + "Name": "Host Ethernet Interface", + "Description": "Host Network Interface", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "InterfaceEnabled": true, + "PermanentMACAddress": "AA:BB:CC:DD:EE:FE", + "MACAddress": "AA:BB:CC:DD:EE:FE", + "SpeedMbps": 100, + "AutoNeg": true, + "FullDuplex": true, + "MTUSize": 1500, + "MaxIPv6StaticAddresses": 1, + "IPv4Addresses": [ + { + "Address": "192.168.20.56", + "SubnetMask": "255.255.255.0", + "AddressOrigin": "Static", + "Gateway": "192.168.20.1" + } + ], + "IPv6AddressPolicyTable": [ + { + "Prefix": "::1/128", + "Precedence": 50, + "Label": 0 + } + ], + "IPv6StaticAddresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e25", + "PrefixLength": 64 + } + ], + "IPv6DefaultGateway": "fe80::1ec1:deff:fe6f:1e24", + "IPv6Addresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e25", + "PrefixLength": 64, + "AddressOrigin": "Static", + "AddressState": "Preferred" + } + ], + "Links": { + "HostInterface": { + "@odata.id": "/redfish/v1/Managers/BMC/HostInterfaces/1" + } + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/ToManager", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/VLAN1/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/VLAN1/index.json new file mode 100644 index 0000000..473450c --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/VLAN1/index.json @@ -0,0 +1,52 @@ +{ + "@odata.type": "#EthernetInterface.v1_12_6.EthernetInterface", + "Id": "VLAN1", + "Name": "Ethernet Interface", + "Description": "VLAN from System NIC 1", + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "EthernetInterfaceType": "Virtual", + "LinkStatus": "LinkUp", + "PermanentMACAddress": "12:44:6A:3B:04:11", + "MACAddress": "12:44:6A:3B:04:11", + "SpeedMbps": 1000, + "FullDuplex": true, + "HostName": "web483", + "FQDN": "web483.contoso.com", + "IPv6DefaultGateway": "fe80::3ed9:2bff:fe34:600", + "NameServers": [ + "names.contoso.com" + ], + "IPv4Addresses": [ + { + "Address": "192.168.150.236", + "SubnetMask": "255.255.252.0", + "AddressOrigin": "Static", + "Gateway": "192.168.150.1" + } + ], + "IPv6Addresses": [ + { + "Address": "fe80::1ec1:deff:fe6f:1e24", + "PrefixLength": 64, + "AddressOrigin": "Static", + "AddressState": "Preferred" + } + ], + "VLAN": { + "VLANEnable": true, + "VLANId": 1000 + }, + "TeamMode": "None", + "Links": { + "RelatedInterfaces": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/12446A3B0411" + } + ] + }, + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/VLAN1", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/index.json b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/index.json new file mode 100644 index 0000000..ccf3551 --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/Systems/437XR1138R2/EthernetInterfaces/index.json @@ -0,0 +1,22 @@ +{ + "@odata.type": "#EthernetInterfaceCollection.EthernetInterfaceCollection", + "Name": "Ethernet Interface Collection", + "Description": "System NICs on Contoso Servers", + "Members@odata.count": 4, + "Members": [ + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/12446A3B0411" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/12446A3B8890" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/VLAN1" + }, + { + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces/ToManager" + } + ], + "@odata.id": "/redfish/v1/Systems/437XR1138R2/EthernetInterfaces", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file diff --git a/tests/fixtures/dmtf-public-rackmount1/UpdateService/index.json b/tests/fixtures/dmtf-public-rackmount1/UpdateService/index.json new file mode 100644 index 0000000..bce6f0c --- /dev/null +++ b/tests/fixtures/dmtf-public-rackmount1/UpdateService/index.json @@ -0,0 +1,45 @@ +{ + "@odata.type": "#UpdateService.v1_17_1.UpdateService", + "Id": "UpdateService", + "Name": "Update service", + "Status": { + "State": "Enabled", + "Health": "OK", + "HealthRollup": "OK" + }, + "ServiceEnabled": true, + "HttpPushUri": "/FWUpdate", + "HttpPushUriOptions": { + "HttpPushUriApplyTime": { + "ApplyTime": "Immediate", + "ApplyTime@Redfish.AllowableValues": [ + "Immediate", + "OnReset", + "AtMaintenanceWindowStart", + "InMaintenanceWindowOnReset" + ], + "MaintenanceWindowStartTime": "2018-12-01T03:00:00+06:00", + "MaintenanceWindowDurationInSeconds": 600 + } + }, + "HttpPushUriOptionsBusy": false, + "FirmwareInventory": { + "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory" + }, + "LocalImageStoreAvailableCapacityBytes": 16106127360, + "LocalImageStoreTotalCapacityBytes": 21474836480, + "LocalImageStore": { + "@odata.id": "/redfish/v1/UpdateService/LocalImageStore" + }, + "ClientCertificates": { + "@odata.id": "/redfish/v1/UpdateService/ClientCertificates" + }, + "Actions": { + "#UpdateService.SimpleUpdate": { + "target": "/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate", + "@Redfish.ActionInfo": "/redfish/v1/UpdateService/SimpleUpdateActionInfo" + } + }, + "@odata.id": "/redfish/v1/UpdateService", + "@Redfish.Copyright": "Copyright 2014-2026 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright." +} \ No newline at end of file From 42940d7e9009dd352e05aa4602a6d20028cfe031 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Tue, 13 Oct 2026 05:02:58 +0000 Subject: [PATCH 08/10] Add RedfishDriver on ManagementControllerDriver The redfish driver implements get_facts, get_interfaces, get_interfaces_ip, get_environment, get_users, get_ntp_servers and get_snmp_information for the BMC, and the role methods get_power_state, set_power_state, get_hardware_inventory, get_event_log, get_firmware_inventory and get_management_protocols for the managed server. reboot_host resets the BMC through Manager.Reset, never the server. Registered as the napalm.drivers entry point redfish; depends on napalm and napalm-device-types >= 4.6. --- README.md | 32 ++- napalm_redfish/__init__.py | 3 + napalm_redfish/driver.py | 542 +++++++++++++++++++++++++++++++++++++ pyproject.toml | 6 + tests/test_driver.py | 489 +++++++++++++++++++++++++++++++++ 5 files changed, 1066 insertions(+), 6 deletions(-) create mode 100644 napalm_redfish/driver.py create mode 100644 tests/test_driver.py diff --git a/README.md b/README.md index 6100000..87e9205 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,11 @@ NAPALM driver for server **baseboard management controllers (BMCs)** over boards that predate Redfish. Built on [napalm-device-types](https://git.netork.io/NAPALM/napalm-device-types). Written for netOrk. -**Status: early development.** The HTTPS transport (`napalm_redfish/transport.py`) and pure -parsers for the Redfish resources in [docs/redfish-map.md](docs/redfish-map.md) -(`napalm_redfish/parsers/`) exist and are tested against DMTF mockups. The driver class and -its `napalm.drivers` entry point wait for the `ManagementControllerDriver` role base in -napalm-device-types. The first test device is a Supermicro X10DRL-i (BMC firmware 3.91, -Redfish). +**Status: early development.** The `redfish` driver (`napalm_redfish.RedfishDriver`, a +`ManagementControllerDriver` from napalm-device-types) implements the NAPALM getters and +the role methods in [docs/redfish-map.md](docs/redfish-map.md), tested end to end against +DMTF mockups. It has not yet run against real hardware; the first test device is a +Supermicro X10DRL-i (BMC firmware 3.91, Redfish). The `ipmi` fallback does not exist yet. ## Why Redfish, and why not one driver per vendor @@ -64,10 +63,31 @@ These follow the other drivers in the `NAPALM` organisation: - `napalm-device-types` is not on PyPI: install it from `git+https://git.netork.io/NAPALM/napalm-device-types.git` before this package. +## Usage + +```python +import napalm + +driver = napalm.get_network_driver("redfish") +with driver("bmc01.example.net", "netork", "secret", + optional_args={"verify_ssl": "/etc/netork/bmc-ca.pem"}) as bmc: + bmc.get_facts() # the BMC: firmware as os_version, its ports + bmc.get_power_state() # the server: {"state": "On", "actions": [...]} + bmc.get_hardware_inventory() +``` + +Optional args: `port` (443), `verify_ssl` (`True`, `False` or a CA file path; default +`True`), `auth` (`auto`, `session` or `basic`; `auto` uses a Redfish session and falls back +to basic auth only if the service cannot create sessions), `retries` (1). + +`reboot_host` restarts the BMC, never the server; the server is switched through +`set_power_state`. `get_config` is not implemented: a BMC has no configuration file. + ## Development ```bash python -m venv .venv && . .venv/bin/activate +pip install git+https://git.netork.io/NAPALM/napalm-device-types.git pip install -e '.[test]' pytest ``` diff --git a/napalm_redfish/__init__.py b/napalm_redfish/__init__.py index 265035b..7ff3894 100644 --- a/napalm_redfish/__init__.py +++ b/napalm_redfish/__init__.py @@ -1,3 +1,6 @@ """NAPALM driver for server BMCs over DMTF Redfish.""" +from napalm_redfish.driver import RedfishDriver + __version__ = "0.1.0.dev0" +__all__ = ["RedfishDriver"] diff --git a/napalm_redfish/driver.py b/napalm_redfish/driver.py new file mode 100644 index 0000000..d1a4ece --- /dev/null +++ b/napalm_redfish/driver.py @@ -0,0 +1,542 @@ +"""NAPALM driver for server BMCs over DMTF Redfish. + +The device is the BMC. NAPALM's getters describe the BMC (its ports, users, services); +the ``ManagementControllerDriver`` methods describe and control the server it manages. +``reboot_host`` restarts the BMC (``Manager.Reset``), never the server. + +Optional args +------------- +port : int + HTTPS port (default 443). +verify_ssl : bool or str + ``True`` (default) verifies the certificate against the CA bundle of ``requests``, + a path verifies against that CA file, ``False`` turns verification off (self-signed + BMC certificates are common). +auth : str + ``auto`` (default: session, basic auth if the service cannot create sessions), + ``session`` or ``basic``. +retries : int + Extra attempts after a connection error (default 1). + +The managed server is the first member of ``Systems``; its BMC is the first manager in +its ``Links.ManagedBy`` and its chassis the first in its ``Links.Chassis`` (the first +member of ``Managers``/``Chassis`` when the links are missing). +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import Any + +from napalm.base.exceptions import ConnectionException +from napalm_device_types import ManagementControllerDriver +from napalm_device_types.base import FingerprintRule, PortSpec +from napalm_device_types.models import POWER_ACTIONS + +from napalm_redfish.parsers import ( + build_environment, + environment_sources, + parse_account, + parse_action_info, + parse_drive, + parse_ethernet_interface, + parse_fan, + parse_firmware, + parse_log_entry, + parse_log_service, + parse_manager, + parse_memory, + parse_network_protocol, + parse_power, + parse_power_state, + parse_power_subsystem, + parse_power_supply, + parse_processor, + parse_reset_action, + parse_sensor, + parse_service_root, + parse_simple_storage, + parse_storage, + parse_system, + parse_thermal, + parse_thermal_metrics, + parse_thermal_subsystem, + to_napalm_users, +) +from napalm_redfish.parsers.common import link, link_list, odata_id +from napalm_redfish.transport import RedfishError, RedfishNotFound, RedfishTransport + +LOG = logging.getLogger(__name__) + +_JsonDict = dict[str, Any] + +#: ``get_management_protocols`` key -> ManagerNetworkProtocol property. +_PROTOCOLS = { + "ipmi": "IPMI", + "ssh": "SSH", + "http": "HTTP", + "https": "HTTPS", + "snmp": "SNMP", + "kvm": "KVMIP", + "virtual_media": "VirtualMedia", +} +_SEVERITIES = ("OK", "Warning", "Critical") + + +def _timestamp(created: str | None) -> float | None: + """Unix time of a Redfish ``Created`` value; a time without offset counts as UTC.""" + if not created: + return None + try: + when = datetime.fromisoformat(created.replace("Z", "+00:00")) + except ValueError: + return None + if when.tzinfo is None: + when = when.replace(tzinfo=timezone.utc) + return when.timestamp() + + +def _status(item: _JsonDict) -> str: + """Role ``status``: ``Absent`` for an empty slot, else the Redfish health.""" + if not item.get("present", True): + return "Absent" + return item.get("health") or "" + + +class RedfishDriver(ManagementControllerDriver): + """NAPALM driver for Redfish BMCs (Supermicro, iDRAC, iLO, XCC and others).""" + + platform = "redfish" + DRIVER_NAME = "redfish" + #: Every Redfish service answers ``GET /redfish/v1/`` without credentials. + PORT_SPECS = [PortSpec("https", 443, ("/redfish/v1/",))] + HTTP_FINGERPRINT = [FingerprintRule("redfishversion", 10.0), + FingerprintRule("#serviceroot.", 5.0)] + #: A BMC restart takes a few minutes before Redfish answers again. + REBOOT_SETTLE_SECONDS = 180 + + def __init__( + self, + hostname: str, + username: str, + password: str, + timeout: int = 60, + optional_args: _JsonDict | None = None, + ) -> None: + self.hostname = hostname + self.username = username + self.password = password + self.timeout = timeout + self.optional_args: _JsonDict = optional_args or {} + self.transport = RedfishTransport( + hostname, + username, + password, + port=self.optional_args.get("port"), + verify=self.optional_args.get("verify_ssl", True), + auth=self.optional_args.get("auth", "auto"), + timeout=timeout, + retries=self.optional_args.get("retries", 1), + ) + self._cache: _JsonDict = {} + + # --- connection ---------------------------------------------------------------------- + + def open(self) -> None: + try: + self.transport.open() + except RedfishError as exc: + raise ConnectionException(f"Cannot connect to Redfish at {self.hostname}: {exc}") \ + from exc + + def close(self) -> None: + self.transport.close() + self._cache = {} + + def is_alive(self) -> _JsonDict: + try: + self.transport.get("/redfish/v1/") + except Exception: + return {"is_alive": False} + return {"is_alive": True} + + # --- discovery (cached per connection) ------------------------------------------------- + + def _cached(self, key: str, load): + if key not in self._cache: + self._cache[key] = load() + return self._cache[key] + + def _root(self) -> _JsonDict: + return self._cached("root", lambda: parse_service_root(self.transport.root)) + + def _first(self, collection: str) -> _JsonDict: + path = self._root()["links"].get(collection) + if path is None: + raise RedfishError(f"the service root links no {collection}") + members = self.transport.members(path, limit=1) + if not members: + raise RedfishError(f"{path} has no members") + return members[0] + + def _system_raw(self) -> _JsonDict: + return self._cached("system", lambda: self._first("Systems")) + + def _system_path(self) -> str: + return odata_id(self._system_raw()) or "" + + def _linked_or_first(self, key: str, links_key: str, collection: str) -> _JsonDict: + def load() -> _JsonDict: + linked = link_list(self._system_raw(), "Links", links_key) + if linked: + return self.transport.get(linked[0]) + return self._first(collection) + + return self._cached(key, load) + + def _manager_raw(self) -> _JsonDict: + return self._linked_or_first("manager", "ManagedBy", "Managers") + + def _chassis_raw(self) -> _JsonDict: + return self._linked_or_first("chassis", "Chassis", "Chassis") + + def _members_of(self, resource: _JsonDict, key: str) -> list[_JsonDict]: + """Members of the collection ``resource[key]`` links; [] if not linked.""" + path = link(resource, key) + return self.transport.members(path) if path else [] + + def _service(self, name: str) -> _JsonDict | None: + path = self._root()["links"].get(name) + return self.transport.get_optional(path) if path else None + + def _network_protocol(self) -> _JsonDict: + raw = self.transport.follow(self._manager_raw(), "NetworkProtocol") + return parse_network_protocol(raw or {}) + + def _manager_interfaces(self) -> list[_JsonDict]: + return [parse_ethernet_interface(raw) + for raw in self._members_of(self._manager_raw(), "EthernetInterfaces")] + + # --- NAPALM getters ---------------------------------------------------------------- + + def get_facts(self) -> _JsonDict: + system = parse_system(self._system_raw()) + manager = parse_manager(self._manager_raw()) + interfaces = self._manager_interfaces() + protocol = self._network_protocol() + hostname = (protocol["hostname"] + or next((i["hostname"] for i in interfaces if i["hostname"]), None) + or self.hostname) + fqdn = (protocol["fqdn"] + or next((i["fqdn"] for i in interfaces if i["fqdn"]), None) + or hostname) + return { + "uptime": -1.0, # Redfish has no uptime + "vendor": system["manufacturer"] or "", + "model": system["model"] or "", + "hostname": hostname, + "fqdn": fqdn, + "os_version": manager["firmware_version"] or "", + "serial_number": system["serial_number"] or "", + "interface_list": sorted(i["name"] for i in interfaces if i["name"]), + } + + def get_interfaces(self) -> _JsonDict: + return {i["name"]: i["interface"] for i in self._manager_interfaces() if i["name"]} + + def get_interfaces_ip(self) -> _JsonDict: + result = {} + for iface in self._manager_interfaces(): + families = {f: iface[f] for f in ("ipv4", "ipv6") if iface[f]} + if iface["name"] and families: + result[iface["name"]] = families + return result + + def get_environment(self) -> _JsonDict: + """Fans, temperatures and power supplies of the chassis; ``cpu`` and ``memory`` are + empty, since a BMC does not know the host OS's load.""" + sources = environment_sources(self._chassis_raw()) + fans: _JsonDict = {} + temperature: _JsonDict = {} + power: _JsonDict = {} + + thermal = sources["thermal"] + if thermal and thermal["schema"] == "Thermal": + legacy = parse_thermal(self.transport.get(thermal["path"])) + fans, temperature = legacy["fans"], legacy["temperature"] + elif thermal: + subsystem = parse_thermal_subsystem(self.transport.get(thermal["path"])) + parsed_fans = [parse_fan(raw) for raw in self.transport.members( + subsystem["fans_path"])] if subsystem["fans_path"] else [] + if sources["sensors"]: + sensors = [parse_sensor(raw) + for raw in self.transport.members(sources["sensors"])] + temperature = build_environment(sensors=sensors)["temperature"] + elif subsystem["thermal_metrics_path"]: + metrics = self.transport.get_optional(subsystem["thermal_metrics_path"]) + temperature = parse_thermal_metrics(metrics or {})["temperature"] + fans = build_environment(fans=parsed_fans)["fans"] + + supply = sources["power"] + if supply and supply["schema"] == "Power": + power = parse_power(self.transport.get(supply["path"]))["power"] + elif supply: + psus = [parse_power_supply(raw, self.transport.follow(raw, "Metrics")) + for raw in self._power_supplies_raw(supply["path"])] + power = build_environment(power_supplies=psus)["power"] + + return {"fans": fans, "temperature": temperature, "power": power, + "cpu": {}, "memory": {}} + + def _power_supplies_raw(self, subsystem_path: str) -> list[_JsonDict]: + subsystem = parse_power_subsystem(self.transport.get(subsystem_path)) + path = subsystem["power_supplies_path"] + return self.transport.members(path) if path else [] + + def get_users(self) -> _JsonDict: + service = self._service("AccountService") + if service is None: + return {} + return to_napalm_users(parse_account(raw) for raw in self._members_of(service, + "Accounts")) + + def get_ntp_servers(self) -> _JsonDict: + return {server: {} for server in self._network_protocol()["ntp"]["servers"]} + + def get_snmp_information(self) -> _JsonDict: + """SNMP state from ManagerNetworkProtocol. Redfish hides community strings, so the + ``community`` keys are the communities' names; contact and location are not in + Redfish.""" + snmp = self._network_protocol()["snmp"] + communities = {} + if snmp["enabled"]: + for community in snmp["communities"]: + if community["name"]: + mode = "rw" if community["access"] == "Full" else "ro" + communities[community["name"]] = {"acl": "", "mode": mode} + return {"chassis_id": "", "community": communities, "contact": "", "location": ""} + + # --- role: power ------------------------------------------------------------------- + + def _accepted_actions(self, system: _JsonDict) -> list[str]: + """The PowerAction values the BMC accepts for ``system`` in its current state.""" + reset = parse_reset_action(system) + if reset["target"] is None: + return [] + allowed = reset["allowed"] + if allowed is None and reset["action_info"]: + info = self.transport.get_optional(reset["action_info"]) + allowed = parse_action_info(info) if info else None + if allowed is None: + # Not advertised: assume the standard values; the BMC rejects what it lacks. + allowed = list(POWER_ACTIONS) + actions = [a for a in POWER_ACTIONS if a in allowed] + state = parse_power_state(system) + if state == "Off": + return [a for a in actions if a == "On"] + if state == "On": + return [a for a in actions if a != "On"] + return actions + + def get_power_state(self) -> _JsonDict: + system = self.transport.get(self._system_path()) + return {"state": parse_power_state(system) or "", + "actions": self._accepted_actions(system)} + + def set_power_state(self, action: str) -> None: + if action not in POWER_ACTIONS: + raise ValueError(f"{action!r} is not one of {', '.join(POWER_ACTIONS)}") + system = self.transport.get(self._system_path()) + target = parse_reset_action(system)["target"] + if target is None: + raise RuntimeError(f"{self.hostname}: the system offers no reset action") + accepted = self._accepted_actions(system) + if action not in accepted: + raise RuntimeError( + f"{self.hostname}: {action} not accepted while the server is " + f"{parse_power_state(system) or 'in an unknown state'} " + f"(accepted: {', '.join(accepted) or 'none'})") + try: + self.transport.post(target, {"ResetType": action}) + except RedfishError as exc: + raise RuntimeError(f"{self.hostname}: {action} refused: {exc}") from exc + + def reboot_host(self) -> None: + """Restart the BMC (``Manager.Reset``, graceful when offered). The server keeps + running; the Redfish session ends with the restart.""" + manager = self.transport.get(odata_id(self._manager_raw()) or "") + reset = parse_reset_action(manager, "#Manager.Reset") + if reset["target"] is None: + raise RuntimeError(f"{self.hostname}: the BMC offers no reset action") + body: _JsonDict = {} + if reset["allowed"] is not None: + reset_type = next((t for t in ("GracefulRestart", "ForceRestart") + if t in reset["allowed"]), None) + if reset_type is None: + raise RuntimeError(f"{self.hostname}: BMC reset types " + f"{reset['allowed']} include no restart") + body["ResetType"] = reset_type + try: + self.transport.post(reset["target"], body) + except RedfishError as exc: + raise RuntimeError(f"{self.hostname}: BMC reset refused: {exc}") from exc + + # --- role: inventory ----------------------------------------------------------------- + + def get_hardware_inventory(self) -> _JsonDict: + raw = self._system_raw() + system = parse_system(raw) + return { + "manufacturer": system["manufacturer"] or "", + "model": system["model"] or "", + "serial_number": system["serial_number"] or "", + "system_uuid": (system["uuid"] or "").lower(), + "cpus": self._cpus(raw), + "memory": self._dimms(raw), + "drives": self._drives(raw), + "power_supplies": self._psus(), + "nics": self._nics(raw), + } + + def _cpus(self, system: _JsonDict) -> list[_JsonDict]: + cpus = [] + for raw in self._members_of(system, "Processors"): + cpu = parse_processor(raw) + if cpu["processor_type"] not in (None, "CPU"): + continue # GPUs, FPGAs, accelerators + cpus.append({"socket": cpu["socket"] or cpu["id"] or "", "model": cpu["model"] or "", + "cores": cpu["cores"], "threads": cpu["threads"], + "status": _status(cpu)}) + return cpus + + def _dimms(self, system: _JsonDict) -> list[_JsonDict]: + dimms = [] + for raw in self._members_of(system, "Memory"): + dimm = parse_memory(raw) + size = dimm["capacity_mib"] + dimms.append({ + "slot": dimm["location"] or dimm["id"] or "", + "size_bytes": size * 1024 * 1024 if size else None, + "type": dimm["memory_device_type"] or "", + "speed_mts": dimm["speed_mhz"], # Redfish: MHz or MT/s "as appropriate" + "part_number": dimm["part_number"] or "", + "serial_number": dimm["serial_number"] or "", + "status": _status(dimm), + }) + return dimms + + def _drives(self, system: _JsonDict) -> list[_JsonDict]: + parsed: list[_JsonDict] = [] + if link(system, "Storage"): + seen: set[str] = set() + for storage in self._members_of(system, "Storage"): + for path in parse_storage(storage)["drive_paths"]: + if path not in seen: + seen.add(path) + drive = self.transport.get_optional(path) + if drive is not None: + parsed.append(parse_drive(drive)) + else: + for storage in self._members_of(system, "SimpleStorage"): + parsed.extend(parse_simple_storage(storage)["drives"]) + return [{ + "slot": d["name"] or d["id"] or "", + "model": d["model"] or "", + "serial_number": d["serial_number"] or "", + "size_bytes": d["capacity_bytes"], + "media": d["media_type"] or "", + "protocol": d["protocol"] or "", + "status": _status(d), + } for d in parsed] + + def _psus(self) -> list[_JsonDict]: + source = environment_sources(self._chassis_raw())["power"] + if source is None: + return [] + if source["schema"] == "Power": + items = self.transport.get(source["path"]).get("PowerSupplies") or [] + else: + items = self._power_supplies_raw(source["path"]) + psus = [] + for raw in items: + if not isinstance(raw, dict): + continue + psu = parse_power_supply(raw) + psus.append({ + "slot": psu["name"], + "model": psu["model"] or "", + "serial_number": psu["serial_number"] or "", + "capacity_watts": int(psu["capacity"]) if psu["capacity"] >= 0 else None, + "status": _status(psu), + }) + return psus + + def _nics(self, system: _JsonDict) -> list[_JsonDict]: + nics = [] + for raw in self._members_of(system, "EthernetInterfaces"): + nic = parse_ethernet_interface(raw) + speed = nic["interface"]["speed"] + link_up = {"LinkUp": True, "LinkDown": False, "NoLink": False}.get( + nic["link_status"] or "") + nics.append({ + "name": nic["name"] or "", + "mac_address": nic["interface"]["mac_address"].lower(), + "speed_mbps": int(speed) if speed >= 0 else None, + "link_up": link_up, + }) + return nics + + # --- role: event log, firmware, protocols -------------------------------------------- + + def get_event_log(self, limit: int = 500) -> list[_JsonDict]: + """Entries of every log service of the system and of the BMC, newest first. + + Where the SEL lives varies by vendor, so all of them are read; entries without a + time sort last. + """ + entries: list[_JsonDict] = [] + for owner in (self._system_raw(), self._manager_raw()): + try: + services = self._members_of(owner, "LogServices") + except RedfishNotFound: + continue + for raw_service in services: + service = parse_log_service(raw_service) + if not service["entries_path"]: + continue + try: + raw_entries = self.transport.members(service["entries_path"]) + except RedfishNotFound: + LOG.warning("%s: log %s has no entries collection", self.hostname, + service["id"]) + continue + for raw in raw_entries: + entry = parse_log_entry(raw) + severity = entry["severity"] + entries.append({ + "id": odata_id(raw) or f"{service['id']}/{entry['id']}", + "timestamp": _timestamp(entry["created"]), + "severity": severity if severity in _SEVERITIES else "OK", + "message": entry["message"] or "", + }) + entries.sort(key=lambda e: (e["timestamp"] is not None, e["timestamp"] or 0.0), + reverse=True) + return entries[:limit] + + def get_firmware_inventory(self) -> list[_JsonDict]: + service = self._service("UpdateService") + if service is None: + return [] + items = [parse_firmware(raw) for raw in self._members_of(service, "FirmwareInventory")] + return [{"name": fw["name"] or fw["id"] or "", "version": fw["version"] or "", + "updateable": fw["updateable"]} for fw in items] + + def get_management_protocols(self) -> _JsonDict: + """What the BMC listens on. A protocol the BMC does not report counts as off.""" + protocols = self._network_protocol()["protocols"] + result = {} + for key, name in _PROTOCOLS.items(): + entry = protocols.get(name) or {} + result[key] = {"enabled": bool(entry.get("enabled")), "port": entry.get("port")} + return result diff --git a/pyproject.toml b/pyproject.toml index 2c82a1e..8e4a94b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,9 @@ readme = "README.md" license = { file = "LICENSE" } requires-python = ">=3.10" dependencies = [ + "napalm>=5.0", + # Not on PyPI: install from git before this package (see README). + "napalm-device-types>=4.6", "redfish>=3.2", ] classifiers = [ @@ -25,6 +28,9 @@ test = [ "responses>=0.23", ] +[project.entry-points."napalm.drivers"] +redfish = "napalm_redfish:RedfishDriver" + [tool.setuptools.packages.find] include = ["napalm_redfish*"] diff --git a/tests/test_driver.py b/tests/test_driver.py new file mode 100644 index 0000000..05a068c --- /dev/null +++ b/tests/test_driver.py @@ -0,0 +1,489 @@ +"""RedfishDriver end to end: the DMTF public-rackmount1 mockup over mocked HTTP.""" + +import json +from datetime import datetime +from importlib.metadata import entry_points + +import napalm +import pytest +from conftest import SESSION_PATH, SESSIONS, load +from napalm.base import models as napalm_models +from napalm.base.exceptions import ConnectionException +from napalm_device_types import ManagementControllerDriver, models +from napalm_device_types.roles import primary_role_of + +from napalm_redfish import RedfishDriver + +SYSTEM = "/redfish/v1/Systems/437XR1138R2" +CHASSIS = "/redfish/v1/Chassis/1U" +MANAGER = "/redfish/v1/Managers/BMC" +SYSTEM_LOG = SYSTEM + "/LogServices/Log1/Entries" + + +def assert_shape(value, typed_dict): + """``value`` has exactly the keys of ``typed_dict`` (all of them required).""" + assert isinstance(value, dict) + assert set(value) == set(typed_dict.__annotations__), typed_dict.__name__ + + +def ts(iso: str) -> float: + return datetime.fromisoformat(iso.replace("Z", "+00:00")).timestamp() + + +@pytest.fixture +def driver(server): + # The mockup's system log advertises a nextLink page it does not contain. + server.override("GET", SYSTEM_LOG + "?$skiptoken=2", 200, { + "Members": [{"@odata.id": SYSTEM_LOG + "/3", "Id": "3", "Severity": "Warning", + "Created": "2012-03-07T14:46:00Z", "Message": "Fan redundancy lost"}], + }) + d = RedfishDriver("bmc.example.net", "netork", "s3cret", optional_args={"verify_ssl": False}) + d.open() + yield d + d.close() + + +def without(path: str, *keys: str) -> dict: + data = load("dmtf-public-rackmount1", path) + for key in keys: + data.pop(key) + return data + + +# --- class, registration, connection ------------------------------------------------------- + + +def test_role(): + assert issubclass(RedfishDriver, ManagementControllerDriver) + assert primary_role_of(RedfishDriver) == "management_controller" + assert RedfishDriver.USES_SSH is False + assert RedfishDriver.DRIVER_NAME == "redfish" + for method in ("get_power_state", "set_power_state", "get_hardware_inventory", + "get_event_log", "get_firmware_inventory", "get_management_protocols", + "reboot_host"): + assert hasattr(RedfishDriver, method), method + + +def test_entry_point(): + (ep,) = [e for e in entry_points(group="napalm.drivers") if e.name == "redfish"] + assert ep.load() is RedfishDriver + + +def test_napalm_finds_the_driver(): + assert napalm.get_network_driver("redfish") is RedfishDriver + + +def test_open_and_close(server): + d = RedfishDriver("bmc.example.net", "netork", "s3cret", optional_args={"verify_ssl": False}) + d.open() + assert d.is_alive() == {"is_alive": True} + d.close() + assert d.is_alive() == {"is_alive": False} + assert len(server.calls("POST", SESSIONS)) == 1 + assert len(server.calls("DELETE", SESSION_PATH)) == 1 + + +def test_open_failure_is_connection_exception(server): + server.override("POST", SESSIONS, 401, {}) + d = RedfishDriver("bmc.example.net", "netork", "wrong", optional_args={"verify_ssl": False}) + with pytest.raises(ConnectionException): + d.open() + + +def test_optional_args(server): + d = RedfishDriver("bmc.example.net", "u", "p", timeout=12, + optional_args={"port": 8443, "verify_ssl": False, "auth": "basic"}) + assert d.transport.base_url == "https://bmc.example.net:8443" + assert d.transport.verify is False + assert d.transport.auth == "basic" + assert d.transport.timeout == 12 + assert RedfishDriver("h", "u", "p").transport.verify is True + + +def test_discovery_is_cached(driver, server): + driver.get_facts() + driver.get_hardware_inventory() + assert len(server.calls("GET", SYSTEM)) == 1 + # The power state is read fresh each time; the collection is still not re-read. + driver.get_power_state() + driver.get_power_state() + assert len(server.calls("GET", SYSTEM)) == 3 + assert len(server.calls("GET", "/redfish/v1/Systems")) == 1 + + +# --- NAPALM getters ------------------------------------------------------------------------ + + +def test_get_facts(driver): + facts = driver.get_facts() + assert_shape(facts, napalm_models.FactsDict) + assert facts == { + "uptime": -1.0, + "vendor": "Contoso", + "model": "3500", + "hostname": "web483-bmc", + "fqdn": "web483-bmc.dmtf.org", + "os_version": "1.45.455b66-rev4", + "serial_number": "437XR1138R2", + "interface_list": ["ToHost", "eth0"], + } + + +def test_get_interfaces(driver): + ifaces = driver.get_interfaces() + assert set(ifaces) == {"eth0", "ToHost"} + assert_shape(ifaces["eth0"], napalm_models.InterfaceDict) + assert ifaces["eth0"]["mac_address"] == "23:11:8A:33:CF:EA" + assert ifaces["eth0"]["is_up"] is True + assert ifaces["eth0"]["speed"] == 100.0 + + +def test_get_interfaces_ip(driver): + assert driver.get_interfaces_ip() == { + "eth0": {"ipv4": {"192.168.0.10": {"prefix_length": 22}}, + "ipv6": {"fe80::1ec1:deff:fe6f:1e24": {"prefix_length": 64}}}, + "ToHost": {"ipv4": {"192.168.20.55": {"prefix_length": 24}}, + "ipv6": {"fe80::1ec1:deff:fe6f:1e24": {"prefix_length": 64}}}, + } + + +def test_get_environment_new_resources(driver, server): + env = driver.get_environment() + assert_shape(env, napalm_models.EnvironmentDict) + assert env["cpu"] == {} + assert env["memory"] == {} + assert env["fans"] == { + "Fan Bay 1": {"status": True}, + "Fan Bay 2": {"status": True}, + "Fan for CPU 1": {"status": True}, + "Fan for CPU 2": {"status": True}, + } + assert env["power"] == { + "Power Supply Bay 1": {"status": False, "capacity": 400.0, "output": 937.4}, + } + assert len(env["temperature"]) == 8 + assert env["temperature"]["CPU #1 Temperature"] == { + "temperature": 44.0, "is_alert": True, "is_critical": False} + # The legacy resources were not read. + assert not server.calls("GET", CHASSIS + "/Thermal") + assert not server.calls("GET", CHASSIS + "/Power") + + +def test_get_environment_legacy(driver, server): + server.override("GET", CHASSIS, 200, + without(CHASSIS, "ThermalSubsystem", "PowerSubsystem", "Sensors")) + env = driver.get_environment() + assert_shape(env, napalm_models.EnvironmentDict) + assert env["fans"] == {"BaseBoard System Fan": {"status": True}, + "BaseBoard System Fan Backup": {"status": True}} + assert env["temperature"]["CPU1 Temp"] == { + "temperature": 41.0, "is_alert": False, "is_critical": False} + assert env["power"] == {"Power Supply Bay": {"status": False, "capacity": 800.0, + "output": 325.0}} + assert not server.calls("GET", CHASSIS + "/Sensors") + + +def test_get_environment_thermal_metrics_without_sensors(driver, server): + server.override("GET", CHASSIS, 200, without(CHASSIS, "Sensors")) + env = driver.get_environment() + assert env["temperature"] == { + "Intake": {"temperature": 24.8, "is_alert": False, "is_critical": False}, + "Exhaust": {"temperature": 40.5, "is_alert": False, "is_critical": False}, + } + assert len(env["fans"]) == 4 + + +def test_get_users(driver): + users = driver.get_users() + # The collection lists account 1 only; account 2 exists but is not a member. + assert users == {"Administrator": {"level": 15, "password": "", "sshkeys": []}} + + +def test_get_ntp_servers(driver, server): + assert driver.get_ntp_servers() == {} + server.override("GET", MANAGER + "/NetworkProtocol", 200, + load("dmtf-public-telemetry", "/redfish/v1/Managers/1/NetworkProtocol")) + assert driver.get_ntp_servers() == { + "0.pool.ntp.org": {}, "1.pool.ntp.org": {}, "time-a.nist.gov": {}} + + +def test_get_snmp_information(driver): + snmp = driver.get_snmp_information() + assert_shape(snmp, napalm_models.SNMPDict) + assert snmp == { + "chassis_id": "", + "contact": "", + "location": "", + "community": { + "Read-only community": {"acl": "", "mode": "ro"}, + "Full access community": {"acl": "", "mode": "rw"}, + }, + } + + +def test_get_config_not_implemented(driver): + with pytest.raises(NotImplementedError): + driver.get_config() + + +# --- role: power --------------------------------------------------------------------------- + + +def test_get_power_state(driver): + state = driver.get_power_state() + assert_shape(state, models.PowerStateDict) + # On: "On" is not offered; Redfish-only values (Nmi, ForceOn, FullPowerCycle) are dropped. + assert state == {"state": "On", + "actions": ["GracefulShutdown", "ForceOff", "GracefulRestart", + "ForceRestart"]} + + +def test_get_power_state_off(driver, server): + system = load("dmtf-public-rackmount1", SYSTEM) + system["PowerState"] = "Off" + server.override("GET", SYSTEM, 200, system) + assert driver.get_power_state() == {"state": "Off", "actions": ["On"]} + + +def test_get_power_state_from_action_info(driver, server): + system = load("dmtf-public-rackmount1", SYSTEM) + reset = system["Actions"]["#ComputerSystem.Reset"] + del reset["ResetType@Redfish.AllowableValues"] + reset["@Redfish.ActionInfo"] = SYSTEM + "/ResetActionInfo" + server.override("GET", SYSTEM, 200, system) + server.override("GET", SYSTEM + "/ResetActionInfo", 200, {"Parameters": [ + {"Name": "ResetType", "AllowableValues": ["On", "ForceOff", "PowerCycle"]}]}) + assert driver.get_power_state()["actions"] == ["ForceOff", "PowerCycle"] + + +def test_get_power_state_unlisted_values(driver, server): + # A reset action without allowable values: the standard actions are assumed. + system = load("dmtf-public-rackmount1", SYSTEM) + del system["Actions"]["#ComputerSystem.Reset"]["ResetType@Redfish.AllowableValues"] + server.override("GET", SYSTEM, 200, system) + assert driver.get_power_state()["actions"] == [ + "GracefulShutdown", "ForceOff", "GracefulRestart", "ForceRestart", "PowerCycle"] + + +def test_get_power_state_without_reset_action(driver, server): + server.override("GET", SYSTEM, 200, without(SYSTEM, "Actions")) + assert driver.get_power_state() == {"state": "On", "actions": []} + + +def test_set_power_state(driver, server): + target = SYSTEM + "/Actions/ComputerSystem.Reset" + server.override("POST", target, 204) + driver.set_power_state("GracefulRestart") + (call,) = server.calls("POST", target) + assert json.loads(call.request.body) == {"ResetType": "GracefulRestart"} + + +def test_set_power_state_invalid_action(driver, server): + with pytest.raises(ValueError): + driver.set_power_state("Nmi") + assert not server.calls("POST", SYSTEM + "/Actions/ComputerSystem.Reset") + + +@pytest.mark.parametrize("action", ["On", "PowerCycle"]) +def test_set_power_state_not_accepted_now(driver, server, action): + # "On" for a server that is on; "PowerCycle" which this BMC does not list. + with pytest.raises(RuntimeError): + driver.set_power_state(action) + assert not server.calls("POST", SYSTEM + "/Actions/ComputerSystem.Reset") + + +def test_set_power_state_refused_by_bmc(driver, server): + target = SYSTEM + "/Actions/ComputerSystem.Reset" + server.override("POST", target, 409, {"error": {"message": "Operation in progress"}}) + with pytest.raises(RuntimeError, match="Operation in progress"): + driver.set_power_state("ForceOff") + + +def test_set_power_state_rereads_state(driver, server): + # The state that gates the action is read fresh, not from the discovery cache. + driver.get_power_state() + system = load("dmtf-public-rackmount1", SYSTEM) + system["PowerState"] = "Off" + server.override("GET", SYSTEM, 200, system) + server.override("POST", SYSTEM + "/Actions/ComputerSystem.Reset", 204) + driver.set_power_state("On") + + +def test_reboot_host_resets_the_bmc(driver, server): + target = MANAGER + "/Actions/Manager.Reset" + server.override("POST", target, 204) + driver.reboot_host() + (call,) = server.calls("POST", target) + assert json.loads(call.request.body) == {"ResetType": "GracefulRestart"} + # Never the managed server. + assert not server.calls("POST", SYSTEM + "/Actions/ComputerSystem.Reset") + + +def test_reboot_host_force_only(driver, server): + manager = load("dmtf-public-rackmount1", MANAGER) + manager["Actions"]["#Manager.Reset"]["ResetType@Redfish.AllowableValues"] = ["ForceRestart"] + server.override("GET", MANAGER, 200, manager) + server.override("POST", MANAGER + "/Actions/Manager.Reset", 204) + driver.reboot_host() + (call,) = server.calls("POST", MANAGER + "/Actions/Manager.Reset") + assert json.loads(call.request.body) == {"ResetType": "ForceRestart"} + + +def test_reboot_host_without_action(driver, server): + server.override("GET", MANAGER, 200, without(MANAGER, "Actions")) + with pytest.raises(RuntimeError): + driver.reboot_host() + + +# --- role: inventory, logs, firmware, protocols -------------------------------------------- + + +def test_get_hardware_inventory(driver): + inv = driver.get_hardware_inventory() + assert_shape(inv, models.HardwareInventoryDict) + assert inv["manufacturer"] == "Contoso" + assert inv["model"] == "3500" + assert inv["serial_number"] == "437XR1138R2" + assert inv["system_uuid"] == "38947555-7742-3448-3784-823347823834" + + for cpu in inv["cpus"]: + assert_shape(cpu, models.BmcCpuDict) + # The FPGA is a processor but not a CPU. + assert inv["cpus"] == [ + {"socket": "CPU 1", "model": "Multi-Core Intel(R) Xeon(R) processor 7xxx Series", + "cores": 8, "threads": 16, "status": "OK"}, + {"socket": "CPU 2", "model": "", "cores": None, "threads": None, "status": "Absent"}, + ] + + for dimm in inv["memory"]: + assert_shape(dimm, models.BmcDimmDict) + assert [d["slot"] for d in inv["memory"]] == ["DIMM 1", "DIMM 2", "DIMM 3", "DIMM 4"] + assert inv["memory"][0] == {"slot": "DIMM 1", "size_bytes": 32768 * 1024 * 1024, + "type": "DDR4", "speed_mts": None, "part_number": "", + "serial_number": "", "status": "OK"} + assert inv["memory"][3]["status"] == "Absent" + + for drive in inv["drives"]: + assert_shape(drive, models.BmcDriveDict) + # This system has SimpleStorage only. + assert inv["drives"][0] == {"slot": "SATA Bay 1", "model": "3000GT8", "serial_number": "", + "size_bytes": 8000000000000, "media": "", "protocol": "", + "status": "OK"} + assert [d["status"] for d in inv["drives"]] == ["OK", "Warning", "Absent", "Absent"] + + for psu in inv["power_supplies"]: + assert_shape(psu, models.BmcPsuDict) + assert inv["power_supplies"] == [ + {"slot": "Power Supply Bay 1", "model": "RKS-440DC", "serial_number": "3488247", + "capacity_watts": 400, "status": "Warning"}, + {"slot": "Power Supply Bay 2", "model": "", "serial_number": "", + "capacity_watts": None, "status": "Absent"}, + ] + + for nic in inv["nics"]: + assert_shape(nic, models.BmcNicDict) + nics = {n["name"]: n for n in inv["nics"]} + assert set(nics) == {"12446A3B0411", "12446A3B8890", "ToManager", "VLAN1"} + assert nics["12446A3B0411"] == {"name": "12446A3B0411", "mac_address": "12:44:6a:3b:04:11", + "speed_mbps": 1000, "link_up": True} + assert nics["ToManager"]["link_up"] is None + + +def test_get_hardware_inventory_storage_drives(driver, server): + # A system with Storage reads the drives each storage subsystem links. + system = load("dmtf-public-rackmount1", SYSTEM) + del system["SimpleStorage"] + system["Storage"] = {"@odata.id": SYSTEM + "/Storage"} + server.override("GET", SYSTEM, 200, system) + server.override("GET", SYSTEM + "/Storage", 200, + load("dmtf-public-localstorage", SYSTEM + "/Storage")) + server.override("GET", SYSTEM + "/Storage/1", 200, + load("dmtf-public-localstorage", SYSTEM + "/Storage/1")) + for drive in ("35D38F11ACEF7BD3", "3F5A8C54207B7233", "32ADF365C6C1B7BD", "3D58ECBC375FD9F2"): + path = f"{CHASSIS}/Drives/{drive}" + server.override("GET", path, 200, load("dmtf-public-localstorage", path)) + drives = driver.get_hardware_inventory()["drives"] + assert len(drives) == 4 + assert drives[0] == {"slot": "Drive Sample", "model": "C123", "serial_number": "1234567", + "size_bytes": 899527000064, "media": "HDD", "protocol": "SAS", + "status": "OK"} + + +def test_get_hardware_inventory_legacy_power(driver, server): + server.override("GET", CHASSIS, 200, without(CHASSIS, "PowerSubsystem")) + assert driver.get_hardware_inventory()["power_supplies"] == [ + {"slot": "Power Supply Bay", "model": "499253-B21", "serial_number": "1Z0000001", + "capacity_watts": 800, "status": "Warning"}, + ] + + +def test_get_event_log(driver): + log = driver.get_event_log() + for entry in log: + assert_shape(entry, models.EventLogEntryDict) + assert log == [ + {"id": SYSTEM_LOG + "/3", "timestamp": ts("2012-03-07T14:46:00Z"), + "severity": "Warning", "message": "Fan redundancy lost"}, + {"id": SYSTEM_LOG + "/2", "timestamp": ts("2012-03-07T14:45:00Z"), + "severity": "Critical", "message": "Temperature threshold exceeded"}, + {"id": SYSTEM_LOG + "/1", "timestamp": ts("2012-03-07T14:44:00Z"), + "severity": "Critical", "message": "Temperature threshold exceeded"}, + {"id": MANAGER + "/LogServices/Log/Entries/1", "timestamp": ts("2012-03-07T14:44:00Z"), + "severity": "Critical", "message": "System May be Melting"}, + ] + + +def test_get_event_log_limit(driver): + log = driver.get_event_log(limit=2) + assert [e["id"] for e in log] == [SYSTEM_LOG + "/3", SYSTEM_LOG + "/2"] + + +def test_get_event_log_timestamps(driver, server): + entries = SYSTEM_LOG + "?$skiptoken=2" + server.override("GET", entries, 200, {"Members": [ + {"Id": "3", "Created": "2026-10-13T08:00:00+02:00", "Message": "offset"}, + {"Id": "4", "Message": "no time", "Severity": "Informational"}, + {"Id": "5", "Created": "garbage", "Message": "bad time"}, + ]}) + log = {e["message"]: e for e in driver.get_event_log()} + assert log["offset"]["timestamp"] == ts("2026-10-13T06:00:00Z") + assert log["no time"]["timestamp"] is None + assert log["no time"]["severity"] == "OK" + assert log["bad time"]["timestamp"] is None + # Entries without a time sort last. + assert list(log)[0] == "offset" + assert set(list(log)[-2:]) == {"no time", "bad time"} + + +def test_get_firmware_inventory(driver): + fw = driver.get_firmware_inventory() + for item in fw: + assert_shape(item, models.FirmwareDict) + assert fw == [ + {"name": "Contoso BMC Firmware", "version": "1.45.455b66-rev4", "updateable": True}, + {"name": "Contoso Simple Storage Firmware", "version": "2.50", "updateable": True}, + {"name": "Contoso BIOS Firmware", "version": "P79 v1.45", "updateable": True}, + ] + + +def test_get_management_protocols(driver): + protocols = driver.get_management_protocols() + assert_shape(protocols, models.ManagementProtocolsDict) + assert protocols == { + "ipmi": {"enabled": True, "port": 623}, + "ssh": {"enabled": True, "port": 22}, + "http": {"enabled": True, "port": 80}, + "https": {"enabled": True, "port": 443}, + "snmp": {"enabled": True, "port": 161}, + "kvm": {"enabled": True, "port": 5288}, + "virtual_media": {"enabled": True, "port": 17988}, + } + + +def test_get_management_protocols_missing(driver, server): + server.override("GET", MANAGER + "/NetworkProtocol", 200, + {"HTTPS": {"ProtocolEnabled": True, "Port": 443}, "IPMI": {"Port": 623}}) + protocols = driver.get_management_protocols() + assert protocols["https"] == {"enabled": True, "port": 443} + assert protocols["ipmi"] == {"enabled": False, "port": 623} + assert protocols["ssh"] == {"enabled": False, "port": None} From 05731d4b0fbbf1d914e01a48a41f456c23ba1905 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Tue, 13 Oct 2026 05:10:02 +0000 Subject: [PATCH 09/10] Require napalm-device-types 4.7 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8e4a94b..1566dbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ requires-python = ">=3.10" dependencies = [ "napalm>=5.0", # Not on PyPI: install from git before this package (see README). - "napalm-device-types>=4.6", + "napalm-device-types>=4.7", "redfish>=3.2", ] classifiers = [ From 2090b1cf8ca467d9cd4b0d7ad86289389e7641de Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Tue, 13 Oct 2026 05:13:45 +0000 Subject: [PATCH 10/10] Release 0.1.0 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ napalm_redfish/__init__.py | 2 +- pyproject.toml | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..988f03f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.1.0] - 2026-10-13 + +### Added +- `redfish` NAPALM driver (`RedfishDriver`) on `ManagementControllerDriver` from + napalm-device-types 4.7, registered as the `napalm.drivers` entry point `redfish` +- NAPALM getters for the BMC: `get_facts`, `get_interfaces`, `get_interfaces_ip`, + `get_environment` (fans, temperatures, power supplies; `cpu` and `memory` empty), + `get_users`, `get_ntp_servers`, `get_snmp_information` +- Role methods for the managed server: `get_power_state`, `set_power_state`, + `get_hardware_inventory`, `get_event_log`, `get_firmware_inventory`, + `get_management_protocols`; `reboot_host` resets the BMC (`Manager.Reset`) +- HTTPS transport on the DMTF `redfish` library: Redfish session login with the session + deleted on close, basic auth fallback only when sessions are unsupported, certificate + verification on by default (`verify_ssl` accepts a CA file), collections followed by + `@odata.id` including `nextLink` pages, links to other hosts refused +- Environment from `ThermalSubsystem`/`PowerSubsystem`/`Sensors` where present, legacy + `Thermal`/`Power` otherwise +- Pure parsers for every resource in `docs/redfish-map.md` +- Tests against DMTF Redfish mockups (public-rackmount1, public-localstorage, + public-telemetry) over mocked HTTP + +### Known limitations +- Not yet run against real hardware; Supermicro X10 behaviour is from the design notes +- No `ipmi` fallback driver yet +- `get_config` is not implemented (a BMC has no configuration file) diff --git a/napalm_redfish/__init__.py b/napalm_redfish/__init__.py index 7ff3894..fb14610 100644 --- a/napalm_redfish/__init__.py +++ b/napalm_redfish/__init__.py @@ -2,5 +2,5 @@ from napalm_redfish.driver import RedfishDriver -__version__ = "0.1.0.dev0" +__version__ = "0.1.0" __all__ = ["RedfishDriver"] diff --git a/pyproject.toml b/pyproject.toml index 1566dbc..11845f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "napalm-redfish" -version = "0.1.0.dev0" +version = "0.1.0" description = "NAPALM driver for server baseboard management controllers over DMTF Redfish" readme = "README.md" license = { file = "LICENSE" }