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
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import re

from ironic import objects
from ironic.common import exception
from ironic.drivers.modules.inspector.hooks import base
from oslo_log import log as logging

LOG = logging.getLogger(__name__)


class InspectHookChassisModel(base.InspectionHook):
"""Update baremetal node properties with chassis model number from inventory.

We set both a baremetal node property and a trait.
"""

def __call__(self, task, inventory, _plugin_data):
node = task.node
chassis_model = _extract_chassis_model(node, inventory)
manufacturer = _extract_manufacturer(node, inventory)
trait_name = _trait_name(manufacturer, chassis_model)
_set_node_traits(task, "CUSTOM_CHASSIS_MODEL_", trait_name)


def _set_node_traits(task, prefix: str, required_trait: str):
"""Manage the subset of node traits whose names begin with `prefix`."""
node = task.node
existing_traits = node.traits.get_trait_names()

required_traits = {x for x in existing_traits if not x.startswith(prefix)}
required_traits.add(required_trait)

LOG.debug(
"Checking traits for node %s: existing=%s required=%s",
node.uuid,
existing_traits,
required_trait,
)
if existing_traits != required_traits:
objects.TraitList.create(task.context, task.node.id, required_traits)
node.save()


def _extract_chassis_model(node, inventory: dict) -> str:
"""Extract up the system_vendor product name.

Return a cleaned-up string like "POWEREDGE_R7615".
"""
chassis_model = inventory.get("system_vendor", {}).get("product_name")
if chassis_model is None:
raise exception.InvalidNodeInventory(
node=node.uuid, reason="Missing product_name in inventory data."
)
return re.sub(r" \(.*\)", "", str(chassis_model))


def _extract_manufacturer(node, inventory: dict) -> str:
"""Extract up the system ventor manufacturer name.

Return a cleaned-up string like "Dell" or "HP".
"""
name = inventory.get("system_vendor", {}).get("manufacturer")
if name is None:
raise exception.InvalidNodeInventory(
node=node.uuid, reason="No manufacturer found in inventory data."
)

if "DELL" in name.upper():
return "Dell"
elif "HP" in name.upper():
return "HP"
else:
return name.replace(" ", "_")


def _trait_name(manufacturer: str, chassis_model: str) -> str:
"""The node trait that should be present on this node."""
return f"{manufacturer}_#{chassis_model}".upper().replace(" ", "_")
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from ironic.common import exception
from ironic.drivers.modules.inspector.hooks import base
from oslo_log import log as logging

LOG = logging.getLogger(__name__)


class InspectHookNodeNameCheck(base.InspectionHook):
"""Check baremetal node name against system identity from inventory data.

Expect the node name to be a string like "Dell_AN3Z23A" consistent with the
manufacturer and serial number in the inventory data.

If the node name does not match, abort the inspection process to force
operator intervention.
"""

def __call__(self, task, inventory, _plugin_data):
node = task.node
sys_data = inventory.get("system_vendor", {})

serial_number = sys_data.get("sku", sys_data.get("serial_number"))
if serial_number is None:
raise exception.InvalidNodeInventory(
node=node.uuid, reason="No serial number found in inventory data."
)

manufacturer = sys_data.get("manufacturer")
if manufacturer is None:
raise exception.InvalidNodeInventory(
node=node.uuid, reason="No manufacturer found in inventory data."
)

manufacturer_slug = _manufacturer_slug(manufacturer)

if node.name == f"{manufacturer}_{serial_number}":
LOG.debug("Node Name Check passed for node %s", node.uuid)
else:
raise RuntimeError(
"Hardware Identity Crisis with baremetal node %s! The current "
"node name %s is inconsistent with its hardware manufacturer "
"%s and serial number/service tag %s. If this is a "
"replacement hardware, the baremetal node should be deleted "
"and re-enrolled.",
node.uuid,
node.name,
manufacturer_slug,
serial_number,
)


def _manufacturer_slug(manufacturer_name: str) -> str:
name = str(manufacturer_name).upper()
if "DELL" in name:
return "Dell"
elif "HP" in name:
return "HP"
else:
return manufacturer_name.replace(" ", "_")
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from typing import Any

from ironic import objects
Expand All @@ -13,7 +14,7 @@
LOG = logging.getLogger(__name__)


class UpdateBaremetalPortsHook(base.InspectionHook):
class InspectHookUpdateBaremetalPorts(base.InspectionHook):
"""Hook to update ports according to LLDP data."""

# "validate-interfaces" provides the all_interfaces field in plugin_data.
Expand Down Expand Up @@ -93,7 +94,7 @@ def _update_port_attrs(task, ports_by_mac, vlan_groups, node_uuid):
if inspected_port:
vlan_group = vlan_groups.get(inspected_port.switch_system_name)
LOG.info(
"Port=%(uuid)s Node=%(node)s is connected " "%(lldp)s, %(vlan_group)s",
"Port=%(uuid)s Node=%(node)s is connected %(lldp)s, %(vlan_group)s",
{
"uuid": baremetal_port.uuid,
"node": node_uuid,
Expand Down Expand Up @@ -173,24 +174,32 @@ def _set_node_traits(task, vlan_groups: set[str]):
connections.
"""
node = task.node
all_possible_suffixes = set(
CONF.ironic_understack.switch_name_vlan_group_mapping.values()
)
our_traits = {_trait_name(x) for x in all_possible_suffixes if x}
required_traits = {_trait_name(x) for x in vlan_groups if x}
existing_traits = set(node.traits.get_trait_names()).intersection(our_traits)

LOG.debug(
"Checking traits for node %s: existing=%s required=%s",
node.uuid,
existing_traits,
required_traits,
)
if existing_traits != required_traits:
existing_traits = set(node.traits.get_trait_names())
vlan_group_traits = {_trait_name(x) for x in vlan_groups if x}
irrelevant_existing_traits = {x for x in existing_traits if not _is_our_trait(x)}
required_traits = irrelevant_existing_traits.intersection(vlan_group_traits)

if existing_traits == required_traits:
LOG.debug(
"Node %s traits %s are all present and correct",
node.uuid,
vlan_group_traits,
)
else:
LOG.info(
"Updating traits for node %s from %s to %s",
node.uuid,
existing_traits,
required_traits,
)
objects.TraitList.create(task.context, task.node.id, required_traits)
node.save()


def _trait_name(vlan_group_name: str) -> str:
suffix = vlan_group_name.upper().split("-")[-1]
return f"CUSTOM_{suffix}_SWITCH"


def _is_our_trait(name: str) -> bool:
return bool(re.match(r"^CUSTOM_[A-Z0-9]+_SWITCH$", name))
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import ironic.objects
from oslo_utils import uuidutils

from ironic_understack.update_baremetal_port import UpdateBaremetalPortsHook
from ironic_understack.inspect_hook_update_baremetal_ports import (
InspectHookUpdateBaremetalPorts,
)

# load some metaprgramming normally taken care of during Ironic initialization:
ironic.objects.register_all()
Expand Down Expand Up @@ -78,18 +80,20 @@ def test_with_valid_data(mocker, caplog):
)

mocker.patch(
"ironic_understack.update_baremetal_port.ironic_ports_for_node",
"ironic_understack.inspect_hook_update_baremetal_ports.ironic_ports_for_node",
return_value=[mock_port],
)
mocker.patch(
"ironic_understack.update_baremetal_port.CONF.ironic_understack.switch_name_vlan_group_mapping",
"ironic_understack.inspect_hook_update_baremetal_ports.CONF.ironic_understack.switch_name_vlan_group_mapping",
MAPPING,
)
mocker.patch("ironic_understack.update_baremetal_port.objects.TraitList.create")
mocker.patch(
"ironic_understack.inspect_hook_update_baremetal_ports.objects.TraitList.create"
)

mock_traits.get_trait_names.return_value = ["CUSTOM_BMC_SWITCH", "bar"]

UpdateBaremetalPortsHook().__call__(mock_task, _INVENTORY, _PLUGIN_DATA)
InspectHookUpdateBaremetalPorts().__call__(mock_task, _INVENTORY, _PLUGIN_DATA)

assert mock_port.local_link_connection == {
"port_id": "Ethernet1/18",
Expand Down
4 changes: 3 additions & 1 deletion python/ironic-understack/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ dependencies = [

[project.entry-points."ironic.inspection.hooks"]
resource-class = "ironic_understack.resource_class:ResourceClassHook"
update-baremetal-port = "ironic_understack.update_baremetal_port:UpdateBaremetalPortsHook"
update-baremetal-port = "ironic_understack.inspect_hook_update_baremetal_ports:InspectHookUpdateBaremetalPorts"
port-bios-name = "ironic_understack.port_bios_name_hook:PortBiosNameHook"
node-name-check = "ironic_understack.inspect_hook_node_name_check:InspectHookNodeNameCheck"
chassis_model = "ironic_understack.inspect_hook_chassis_model:InspectHookChassisModel"

[project.entry-points."ironic.hardware.interfaces.inspect"]
redfish-understack = "ironic_understack.redfish_inspect_understack:UnderstackRedfishInspect"
Expand Down
Loading