-
Notifications
You must be signed in to change notification settings - Fork 8
feat(ironic): Add update_baremetal_port ironic inspection hook #1347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
bb807b7
Add pytest-mock dep
stevekeay 4579a65
Add a configuration option specifying the vlan group switch name mapping
stevekeay 2050345
Implement ironic post-inspection hook to set port bios_name, pxe_enabled
stevekeay 78210f4
Implement ironic inspect hook setting port local_link, physical_network
stevekeay bb4bd7a
Configure ironic Inspection hooks to manage port and node attributes
stevekeay File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
python/ironic-understack/ironic_understack/inspected_port.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| from dataclasses import dataclass | ||
|
|
||
|
|
||
| @dataclass | ||
| class InspectedPort: | ||
| """Represents a parsed entry from Ironic inspection (inventory) data.""" | ||
|
|
||
| mac_address: str | ||
| name: str | ||
| switch_system_name: str | ||
| switch_port_id: str | ||
| switch_chassis_id: str | ||
|
|
||
| @property | ||
| def local_link_connection(self) -> dict: | ||
| return { | ||
| "port_id": self.switch_port_id, | ||
| "switch_id": self.switch_chassis_id, | ||
| "switch_info": self.switch_system_name, | ||
| } | ||
|
|
||
| @property | ||
| def parsed_name(self) -> dict[str, str]: | ||
| parts = self.switch_system_name.split(".", maxsplit=1) | ||
| if len(parts) != 2: | ||
| raise ValueError( | ||
| "Failed to parse switch hostname - expecting name.dc in %s", self | ||
| ) | ||
| switch_name, data_center_name = parts | ||
|
|
||
| parts = switch_name.rsplit("-", maxsplit=1) | ||
| if len(parts) != 2: | ||
| raise ValueError( | ||
| f"Unknown switch name format: {switch_name} - this hook requires " | ||
| f"that switch names follow the convention <cabinet-name>-<suffix>" | ||
| ) | ||
|
|
||
| rack_name, switch_suffix = parts | ||
|
|
||
| return { | ||
| "rack_name": rack_name, | ||
| "switch_suffix": switch_suffix, | ||
| "data_center_name": data_center_name, | ||
| } | ||
|
|
||
| @property | ||
| def rack_name(self) -> str: | ||
| return self.parsed_name["rack_name"] | ||
|
|
||
| @property | ||
| def switch_suffix(self) -> str: | ||
| return self.parsed_name["switch_suffix"] | ||
|
|
||
| @property | ||
| def data_center_name(self) -> str: | ||
| return self.parsed_name["data_center_name"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import ironic.objects | ||
|
|
||
|
|
||
| def ironic_ports_for_node(context, node_id: str) -> list: | ||
| return ironic.objects.Port.list_by_node_id(context, node_id) |
62 changes: 62 additions & 0 deletions
62
python/ironic-understack/ironic_understack/port_bios_name_hook.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| from ironic.drivers.modules.inspector.hooks import base | ||
| from oslo_log import log as logging | ||
|
|
||
| from ironic_understack.ironic_wrapper import ironic_ports_for_node | ||
|
|
||
| LOG = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class PortBiosNameHook(base.InspectionHook): | ||
| """Set port.extra.bios_name and pxe_enabled fields from redfish data.""" | ||
|
|
||
| # "ports" creates baremetal ports for each physical NIC, be sure to run this | ||
| # first because we will only be updating ports that already exist: | ||
| dependencies = ["ports"] | ||
|
|
||
| def __call__(self, task, inventory, plugin_data): | ||
| """Populate the baremetal_port.extra.bios_name attribute.""" | ||
| inspected_interfaces = inventory.get("interfaces") | ||
| if not inspected_interfaces: | ||
| LOG.error("No interfaces in inventory for node %s", task.node.uuid) | ||
| return | ||
|
|
||
| interface_names = { | ||
| i["mac_address"].upper(): i["name"] for i in inspected_interfaces | ||
| } | ||
|
|
||
| pxe_interface = _pxe_interface_name(inspected_interfaces) | ||
|
|
||
| for baremetal_port in ironic_ports_for_node(task.context, task.node.id): | ||
| mac = baremetal_port.address.upper() | ||
| required_bios_name = interface_names.get(mac) | ||
| extra = baremetal_port.extra | ||
| current_bios_name = extra.get("bios_name") | ||
|
|
||
| if current_bios_name != required_bios_name: | ||
| LOG.info( | ||
| "Port %(mac)s updating bios_name from %(old)s to %(new)s", | ||
| {"mac": mac, "old": current_bios_name, "new": required_bios_name}, | ||
| ) | ||
|
|
||
| if required_bios_name: | ||
| extra["bios_name"] = required_bios_name | ||
| else: | ||
| extra.pop("bios_name", None) | ||
|
|
||
| baremetal_port.extra = extra | ||
| baremetal_port.save() | ||
|
|
||
| required_pxe = required_bios_name == pxe_interface | ||
| if baremetal_port.pxe_enabled != required_pxe: | ||
| LOG.info("Port %s changed pxe_enabled to %s", mac, required_pxe) | ||
| baremetal_port.pxe_enabled = required_pxe | ||
| baremetal_port.save() | ||
|
|
||
|
|
||
| def _pxe_interface_name(inspected_interfaces: list[dict]) -> str: | ||
| """Use a heuristic to determine our default interface for PXE.""" | ||
| names = sorted(i["name"] for i in inspected_interfaces) | ||
| for prefix in ["NIC.Integrated", "NIC.Slot"]: | ||
| for name in names: | ||
| if name.startswith(prefix): | ||
| return name | ||
63 changes: 63 additions & 0 deletions
63
python/ironic-understack/ironic_understack/tests/test_port_bios_name_hook.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import logging | ||
|
|
||
| from oslo_utils import uuidutils | ||
|
|
||
| from ironic_understack.port_bios_name_hook import PortBiosNameHook | ||
|
|
||
| _INVENTORY = { | ||
| "memory": {"physical_mb": 98304}, | ||
| "interfaces": [ | ||
| {"mac_address": "11:11:11:11:11:11", "name": "NIC.Integrated.1-1"}, | ||
| {"mac_address": "22:22:22:22:22:22", "name": "NIC.Integrated.1-2"}, | ||
| ], | ||
| } | ||
|
|
||
|
|
||
| def test_adding_bios_name(mocker, caplog): | ||
| caplog.set_level(logging.DEBUG) | ||
|
|
||
| node_uuid = uuidutils.generate_uuid() | ||
| mock_context = mocker.Mock() | ||
| mock_node = mocker.Mock(id=1234) | ||
| mock_task = mocker.Mock(node=mock_node, context=mock_context) | ||
| mock_port = mocker.Mock( | ||
| uuid=uuidutils.generate_uuid(), | ||
| node_id=node_uuid, | ||
| address="11:11:11:11:11:11", | ||
| extra={}, | ||
| ) | ||
|
|
||
| mocker.patch( | ||
| "ironic_understack.port_bios_name_hook.ironic_ports_for_node", | ||
| return_value=[mock_port], | ||
| ) | ||
|
|
||
| PortBiosNameHook().__call__(mock_task, _INVENTORY, {}) | ||
|
|
||
| assert mock_port.extra == {"bios_name": "NIC.Integrated.1-1"} | ||
| mock_port.save.assert_called() | ||
|
|
||
|
|
||
| def test_removing_bios_name(mocker, caplog): | ||
| caplog.set_level(logging.DEBUG) | ||
|
|
||
| node_uuid = uuidutils.generate_uuid() | ||
| mock_context = mocker.Mock() | ||
| mock_node = mocker.Mock(id=1234) | ||
| mock_task = mocker.Mock(node=mock_node, context=mock_context) | ||
| mock_port = mocker.Mock( | ||
| uuid=uuidutils.generate_uuid(), | ||
| node_id=node_uuid, | ||
| address="33:33:33:33:33:33", | ||
| extra={"bios_name": "old_name_no_longer_valid"}, | ||
| ) | ||
|
|
||
| mocker.patch( | ||
| "ironic_understack.port_bios_name_hook.ironic_ports_for_node", | ||
| return_value=[mock_port], | ||
| ) | ||
|
|
||
| PortBiosNameHook().__call__(mock_task, _INVENTORY, {}) | ||
|
|
||
| assert "bios_name" not in mock_port.extra | ||
| mock_port.save.assert_called() |
108 changes: 108 additions & 0 deletions
108
python/ironic-understack/ironic_understack/tests/test_update_baremetal_port.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import logging | ||
|
|
||
| import ironic.objects | ||
| from oslo_utils import uuidutils | ||
|
|
||
| import ironic_understack | ||
| from ironic_understack.update_baremetal_port import UpdateBaremetalPortsHook | ||
|
|
||
| # load some metaprgramming normally taken care of during Ironic initialization: | ||
| ironic.objects.register_all() | ||
|
|
||
| _INVENTORY = {} | ||
| _PLUGIN_DATA = { | ||
| "all_interfaces": { | ||
| "ex1": { | ||
| "name": "ex1", | ||
| "mac_address": "11:11:11:11:11:11", | ||
| }, | ||
| "ex2": { | ||
| "name": "ex2", | ||
| "mac_address": "22:22:22:22:22:22", | ||
| }, | ||
| "ex3": { | ||
| "name": "ex3", | ||
| "mac_address": "33:33:33:33:33:33", | ||
| }, | ||
| "ex4": { | ||
| "name": "ex4", | ||
| "mac_address": "44:44:44:44:44:44", | ||
| }, | ||
| }, | ||
| "parsed_lldp": { | ||
| "ex1": { | ||
| "switch_chassis_id": "88:5a:92:ec:54:59", | ||
| "switch_port_id": "Ethernet1/18", | ||
| "switch_system_name": "f20-3-1.iad3", | ||
| }, | ||
| "ex2": { | ||
| "switch_chassis_id": "88:5a:92:ec:54:59", | ||
| "switch_port_id": "Ethernet1/18", | ||
| "switch_system_name": "f20-3-2.iad3", | ||
| }, | ||
| "ex3": { | ||
| "switch_chassis_id": "88:5a:92:ec:54:59", | ||
| "switch_port_id": "Ethernet1/18", | ||
| "switch_system_name": "f20-3-1f.iad3", | ||
| }, | ||
| "ex4": { | ||
| "switch_chassis_id": "88:5a:92:ec:54:59", | ||
| "switch_port_id": "Ethernet1/18", | ||
| "switch_system_name": "f20-3-2f.iad3", | ||
| }, | ||
| }, | ||
| } | ||
|
|
||
| MAPPING = { | ||
| "1": "network", | ||
| "2": "network", | ||
| "1f": "storage", | ||
| "2f": "storage", | ||
| "-1d": "bmc", | ||
| } | ||
|
|
||
|
|
||
| def test_with_valid_data(mocker, caplog): | ||
| caplog.set_level(logging.DEBUG) | ||
|
|
||
| node_uuid = uuidutils.generate_uuid() | ||
| mock_traits = mocker.Mock() | ||
| mock_context = mocker.Mock() | ||
| mock_node = mocker.Mock(id=1234, traits=mock_traits) | ||
| mock_task = mocker.Mock(node=mock_node, context=mock_context) | ||
| mock_port = mocker.Mock( | ||
| uuid=uuidutils.generate_uuid(), | ||
| node_id=node_uuid, | ||
| address="11:11:11:11:11:11", | ||
| local_link_connection={}, | ||
| physical_network="original_value", | ||
| ) | ||
|
|
||
| mocker.patch( | ||
| "ironic_understack.update_baremetal_port.ironic_ports_for_node", | ||
| return_value=[mock_port], | ||
| ) | ||
| mocker.patch( | ||
| "ironic_understack.update_baremetal_port.CONF.ironic_understack.switch_name_vlan_group_mapping", | ||
| MAPPING, | ||
| ) | ||
| mocker.patch("ironic_understack.update_baremetal_port.objects.TraitList.create") | ||
|
|
||
| mock_traits.get_trait_names.return_value = ["CUSTOM_BMC_SWITCH", "bar"] | ||
|
|
||
| UpdateBaremetalPortsHook().__call__(mock_task, _INVENTORY, _PLUGIN_DATA) | ||
|
|
||
| assert mock_port.local_link_connection == { | ||
| "port_id": "Ethernet1/18", | ||
| "switch_id": "88:5a:92:ec:54:59", | ||
| "switch_info": "f20-3-1.iad3", | ||
| } | ||
| assert mock_port.physical_network == "f20-3-network" | ||
| mock_port.save.assert_called() | ||
|
|
||
| mock_traits.get_trait_names.assert_called_once() | ||
| mock_traits.destroy.assert_called_once_with("CUSTOM_BMC_SWITCH") | ||
| ironic_understack.update_baremetal_port.objects.TraitList.create.assert_called_once_with( | ||
| mock_context, 1234, ["CUSTOM_NETWORK_SWITCH", "CUSTOM_STORAGE_SWITCH"] | ||
| ) | ||
| mock_node.save.assert_called_once() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also unconditionally set
baremetal_port.descriptionThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Description is cool, however the "description" suggests to me "human readable" whereas this field is intended for a machine to consume. Parsing a description seems fragile because people might be tempted to add other "helpful" information in there.
Also, our openstack today doesn't seem to have a description field:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's intended for machines then I agree with the field in extra. As far as the field not showing up, upgrade your client.