Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions python/understack-workflows/tests/test_bmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import pytest

from understack_workflows.bmc import Bmc
from understack_workflows.bmc import bmc_for_ip_address


Expand All @@ -25,3 +26,48 @@ def test_bmc_for_ip_address(mock_creds, monkeypatch):
assert bmc.url() == "https://1.2.3.4"
assert bmc.username == "root"
assert bmc.password == "1MlzcjJ7bnICKp98wrdx"


def test_manager_attributes_path_uses_advertised_idrac10_resource(mocker):
bmc = Bmc("10.46.104.6")
bmc._manager_path = "/redfish/v1/Managers/iDRAC.Embedded.1"
mocker.patch.object(
bmc,
"redfish_request",
return_value={
"Links": {
"Oem": {
"Dell": {
"DellAttributes": [
{
"@odata.id": (
"/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/"
"DellAttributes/System.Embedded.1"
)
},
{
"@odata.id": (
"/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/"
"DellAttributes/iDRAC.Embedded.1"
)
},
]
}
}
}
},
)

assert bmc.manager_attributes_path == (
"/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellAttributes/iDRAC.Embedded.1"
)


def test_manager_attributes_path_falls_back_to_idrac9_resource(mocker):
bmc = Bmc("10.46.104.6")
bmc._manager_path = "/redfish/v1/Managers/iDRAC.Embedded.1"
mocker.patch.object(bmc, "redfish_request", return_value={})

assert bmc.manager_attributes_path == (
"/redfish/v1/Managers/iDRAC.Embedded.1/Attributes"
)
61 changes: 61 additions & 0 deletions python/understack-workflows/tests/test_bmc_network_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from ipaddress import IPv4Address
from ipaddress import IPv4Interface

from understack_workflows.bmc import Bmc
from understack_workflows.bmc_chassis_info import InterfaceInfo
from understack_workflows.bmc_network_config import bmc_set_permanent_ip_addr


def _dhcp_interface():
return InterfaceInfo(
name="iDRAC",
description="Dedicated iDRAC interface",
mac_address="6C:3C:8C:A9:6C:6C",
ipv4_address=IPv4Interface("10.46.104.7/23"),
ipv4_gateway=IPv4Address("10.46.104.1"),
dhcp=True,
)


def test_set_permanent_ip_uses_static_attributes_for_idrac10(mocker):
attribute_path = (
"/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellAttributes/iDRAC.Embedded.1"
)
bmc = mocker.Mock(spec=Bmc)
bmc.manager_attributes_path = attribute_path

bmc_set_permanent_ip_addr(bmc, _dhcp_interface())

bmc.redfish_request.assert_called_once_with(
attribute_path,
method="PATCH",
payload={
"Attributes": {
"IPv4.1.DHCPEnable": "Disabled",
"IPv4.1.StaticAddress": "10.46.104.7",
"IPv4.1.StaticGateway": "10.46.104.1",
"IPv4.1.StaticNetmask": "255.255.254.0",
}
},
)


def test_set_permanent_ip_keeps_legacy_idrac9_patch(mocker):
attribute_path = "/redfish/v1/Managers/iDRAC.Embedded.1/Attributes"
bmc = mocker.Mock(spec=Bmc)
bmc.manager_attributes_path = attribute_path

bmc_set_permanent_ip_addr(bmc, _dhcp_interface())

bmc.redfish_request.assert_called_once_with(
attribute_path,
method="PATCH",
payload={
"Attributes": {
"IPv4.1.DHCPEnable": "Disabled",
"IPv4.1.Address": "10.46.104.7",
"IPv4.1.Gateway": "10.46.104.1",
"IPv4.1.Netmask": "255.255.254.0",
}
},
)
109 changes: 109 additions & 0 deletions python/understack-workflows/tests/test_bmc_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from understack_workflows import bmc_settings
from understack_workflows.bmc import Bmc

MANAGER_ATTRIBUTES_PATH = (
"/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellAttributes/iDRAC.Embedded.1"
)
UNCONFIGURED_IPV4 = "0.0.0.0" # noqa: S104 -- value returned by iDRAC


def _current_attribute_values():
return {
key: value
for key, value in bmc_settings.REQUIRED_VALUES.items()
if value is not None
}


def test_update_dell_drac_settings_uses_static_attributes_for_idrac10_dns(mocker):
mocker.patch.dict(
bmc_settings.REQUIRED_VALUES,
{
"IPv4.1.DNS1": "10.0.0.53",
"IPv4.1.DNS2": "10.0.0.54",
},
)
mocker.patch.dict(
bmc_settings.VALUES_TO_POST,
{
"IPv4.1.DNS1": "10.0.0.53",
"IPv4.1.DNS2": "10.0.0.54",
},
)
bmc = mocker.Mock(spec=Bmc)
bmc.manager_attributes_path = MANAGER_ATTRIBUTES_PATH
bmc.redfish_request.side_effect = [
{
"Attributes": _current_attribute_values()
| {
"IPv4.1.DNS1": UNCONFIGURED_IPV4,
"IPv4.1.DNS2": UNCONFIGURED_IPV4,
"IPv4.1.StaticDNS1": UNCONFIGURED_IPV4,
"IPv4.1.StaticDNS2": UNCONFIGURED_IPV4,
}
},
{},
]

changes = bmc_settings.update_dell_drac_settings(bmc)

assert changes == {
"IPv4.1.StaticDNS1": "10.0.0.53",
"IPv4.1.StaticDNS2": "10.0.0.54",
}
assert bmc.redfish_request.call_args_list == [
mocker.call(MANAGER_ATTRIBUTES_PATH),
mocker.call(
MANAGER_ATTRIBUTES_PATH,
payload={
"Attributes": {
"IPv4.1.StaticDNS1": "10.0.0.53",
"IPv4.1.StaticDNS2": "10.0.0.54",
}
},
method="PATCH",
),
]


def test_update_dell_drac_settings_keeps_legacy_idrac9_dns_patch(mocker):
attribute_path = "/redfish/v1/Managers/iDRAC.Embedded.1/Attributes"
mocker.patch.dict(
bmc_settings.REQUIRED_VALUES,
{
"IPv4.1.DNS1": "10.0.0.53",
"IPv4.1.DNS2": "10.0.0.54",
},
)
mocker.patch.dict(
bmc_settings.VALUES_TO_POST,
{
"IPv4.1.DNS1": "10.0.0.53",
"IPv4.1.DNS2": "10.0.0.54",
},
)
current_values = _current_attribute_values() | {
"IPv4.1.DNS1": UNCONFIGURED_IPV4,
"IPv4.1.DNS2": UNCONFIGURED_IPV4,
}
bmc = mocker.Mock(spec=Bmc)
bmc.manager_attributes_path = attribute_path
bmc.redfish_request.side_effect = [{"Attributes": current_values}, {}]

changes = bmc_settings.update_dell_drac_settings(bmc)

expected_changes = {
"IPv4.1.DNS1": "10.0.0.53",
"IPv4.1.DNS2": "10.0.0.54",
}
assert changes == expected_changes
bmc.redfish_request.assert_has_calls(
[
mocker.call(attribute_path),
mocker.call(
attribute_path,
payload={"Attributes": expected_changes},
method="PATCH",
),
]
)
32 changes: 32 additions & 0 deletions python/understack-workflows/understack_workflows/bmc.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__(
self._base_path: str | None = None
self._system_path: str | None = None
self._manager_path: str | None = None
self._manager_attributes_path: str | None = None

@property
def base_path(self) -> str:
Expand All @@ -65,6 +66,14 @@ def manager_path(self) -> str:
self._manager_path = self._manager_path or self.get_manager_path()
return self._manager_path

@property
def manager_attributes_path(self) -> str:
"""Read the vendor-supported Manager attributes path from the BMC."""
self._manager_attributes_path = (
self._manager_attributes_path or self.get_manager_attributes_path()
)
return self._manager_attributes_path

def __str__(self):
"""Stringify without password being printed."""
return f"BMC {self.url()}"
Expand All @@ -88,6 +97,29 @@ def get_manager_path(self):
_result = self.redfish_request("/redfish/v1/Managers/")
return _result["Members"][0]["@odata.id"].rstrip("/")

def get_manager_attributes_path(self) -> str:
"""Get the Manager attributes path advertised by the BMC.

iDRAC10 exposes Manager attributes through the DellAttributes OEM
resource. Older iDRAC9 versions use the Manager's Attributes child.
"""
manager_path = self.manager_path
manager = self.redfish_request(manager_path)
dell_attributes = (
manager.get("Links", {})
.get("Oem", {})
.get("Dell", {})
.get("DellAttributes", [])
)
manager_id = manager_path.rsplit("/", maxsplit=1)[-1]

for resource in dell_attributes:
path = resource.get("@odata.id")
if path and path.rstrip("/").rsplit("/", maxsplit=1)[-1] == manager_id:
return path.rstrip("/")

return f"{manager_path}/Attributes"

def get_manufacturer(self) -> str:
"""Read and return Manufacturer."""
return self.redfish_request(self.system_path)["Manufacturer"].lower()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,30 @@ def bmc_set_permanent_ip_addr(bmc: Bmc, interface_info: InterfaceInfo):
if not (interface_info.ipv4_address and interface_info.ipv4_gateway):
raise ValueError("BMC InterfaceInfo has missing IP information")

payload = {
"Attributes": {
"IPv4.1.DHCPEnable": "Disabled",
"IPv4.1.Address": str(interface_info.ipv4_address.ip),
"IPv4.1.Gateway": str(interface_info.ipv4_gateway),
"IPv4.1.Netmask": str(interface_info.ipv4_address.netmask),
}
}
logger.info(
"BMC was DHCP IP %s, making this permanent", interface_info.ipv4_address
)
attribute_path = bmc.manager_path + "/Attributes"
bmc.redfish_request(attribute_path, method="PATCH", payload=payload)
attribute_path = bmc.manager_attributes_path
uses_idrac10_attributes = not attribute_path.rstrip("/").endswith("/Attributes")
address_key = (
"IPv4.1.StaticAddress" if uses_idrac10_attributes else "IPv4.1.Address"
)
gateway_key = (
"IPv4.1.StaticGateway" if uses_idrac10_attributes else "IPv4.1.Gateway"
)
netmask_key = (
"IPv4.1.StaticNetmask" if uses_idrac10_attributes else "IPv4.1.Netmask"
)

bmc.redfish_request(
attribute_path,
method="PATCH",
payload={
"Attributes": {
"IPv4.1.DHCPEnable": "Disabled",
address_key: str(interface_info.ipv4_address.ip),
gateway_key: str(interface_info.ipv4_gateway),
netmask_key: str(interface_info.ipv4_address.netmask),
}
},
)
26 changes: 21 additions & 5 deletions python/understack-workflows/understack_workflows/bmc_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@
"SNMP.1.SNMPProtocol": "0",
}

IDRAC10_ATTRIBUTE_ALIASES = {
"IPv4.1.DNS1": "IPv4.1.StaticDNS1",
"IPv4.1.DNS2": "IPv4.1.StaticDNS2",
}


class BiosSettingException(Exception):
"""Exception when a required key are not present."""
Expand All @@ -36,11 +41,18 @@ def update_dell_drac_settings(bmc: Bmc) -> dict:

Returns the changes that were made
"""
attribute_path = bmc.manager_path + "/Attributes"
attribute_path = bmc.manager_attributes_path
current_values = bmc.redfish_request(attribute_path)["Attributes"]

uses_idrac10_attributes = not attribute_path.rstrip("/").endswith("/Attributes")
required_changes = {}
for key, required_value in REQUIRED_VALUES.items():
required_display_values = {}
for configured_key, required_value in REQUIRED_VALUES.items():
key = (
IDRAC10_ATTRIBUTE_ALIASES.get(configured_key, configured_key)
if uses_idrac10_attributes
else configured_key
)
if not required_value:
logger.warning(
"We have no required value configured for BMC attribute '%s'", key
Expand All @@ -50,18 +62,22 @@ def update_dell_drac_settings(bmc: Bmc) -> dict:
elif current_values[key] == required_value:
logger.info("%s: '%s' already set to '%s'", bmc, key, required_value)
else:
required_changes[key] = VALUES_TO_POST[key]
required_changes[key] = VALUES_TO_POST[configured_key]
required_display_values[key] = required_value

if required_changes:
logger.info("%s Updating DRAC settings:", bmc)
for k in required_changes:
logger.info(" %s: %s->%s", k, current_values[k], REQUIRED_VALUES[k])
logger.info(
" %s: %s->%s", k, current_values[k], required_display_values[k]
)

payload = {"Attributes": required_changes}
bmc.redfish_request(attribute_path, payload=payload, method="PATCH")

logger.info("%s DRAC settings have been updated", bmc)
else:

if not required_changes:
logger.info("%s all required DRAC settings present and correct", bmc)

return required_changes
Loading