Skip to content
Open
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
7 changes: 5 additions & 2 deletions docs/operator-guide/openstack-ironic-inspection-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,12 @@ The `chassis_model` hook adds a custom trait identifying the specific hardware c

1. Extracts chassis model from `system_vendor.product_name`
2. Normalizes manufacturer name (handles "DELL", "HP" variants)
3. Creates a trait in the format: `CUSTOM_{MANUFACTURER}_{CHASSIS_MODEL}`
3. Creates a trait in the format: `CUSTOM_CHASSIS_{MANUFACTURER}_{CHASSIS_MODEL}`

**Example**: A Dell PowerEdge R7615 receives the trait `CUSTOM_DELL_POWEREDGE_R7615`
**Example**: A Dell PowerEdge R7615 receives the trait `CUSTOM_CHASSIS_DELL_POWEREDGE_R7615`

The hook owns the `CUSTOM_CHASSIS_` namespace: on re-inspection it removes any
other `CUSTOM_CHASSIS_` trait and leaves all other traits untouched.

This trait enables [flavor definitions](../design-guide/flavors.md) to target specific hardware models. See [hardware traits](../design-guide/hardware-traits.md) for more on how traits work.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

LOG = logging.getLogger(__name__)

# Reserved namespace for the chassis-model trait. Owning a dedicated prefix
# lets this hook prune stale chassis traits without touching traits managed
# elsewhere (e.g. CUSTOM_*_SWITCH, CUSTOM_FIRMWARE_UPDATE_*).
CHASSIS_TRAIT_PREFIX = "CUSTOM_CHASSIS_"


class InspectHookChassisModel(base.InspectionHook):
"""Update baremetal node properties with chassis model number from inventory.
Expand All @@ -19,16 +24,23 @@ def __call__(self, task, inventory, _plugin_data):
chassis_model = _extract_chassis_model(node, inventory)
manufacturer = _extract_manufacturer(node, inventory)
trait_name = _trait_name(manufacturer, chassis_model)
_set_node_traits(task, "CUSTOM_", trait_name)
_set_node_traits(task, trait_name)


def _set_node_traits(task, required_trait: str):
"""Set the ``CUSTOM_CHASSIS_{MANUFACTURER}_{MODEL}`` trait on the node.

def _set_node_traits(task, prefix: str, required_trait: str):
"""Manage the subset of node traits whose names begin with `prefix`."""
Manages only the ``CUSTOM_CHASSIS_`` namespace: the required trait is
added and any other ``CUSTOM_CHASSIS_`` trait is removed. All other traits
(e.g. ``CUSTOM_*_SWITCH``, ``CUSTOM_FIRMWARE_UPDATE_*``) are left untouched.
"""
node = task.node
existing_traits = node.traits.get_trait_names()
required_trait = prefix + required_trait
required_trait = CHASSIS_TRAIT_PREFIX + required_trait

required_traits = {x for x in existing_traits if not x.startswith(prefix)}
existing_traits = set(node.traits.get_trait_names())
required_traits = {
t for t in existing_traits if not t.startswith(CHASSIS_TRAIT_PREFIX)
}
required_traits.add(required_trait)

LOG.debug(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import logging

import ironic.objects

from ironic_understack.inspect_hook_chassis_model import InspectHookChassisModel

# Populate ironic.objects.TraitList so it can be patched below.
ironic.objects.register_all()

_INVENTORY = {
"system_vendor": {
"manufacturer": "Dell Inc.",
"product_name": "PowerEdge R7615",
}
}
_PLUGIN_DATA = {}


def _mock_task(mocker, existing_traits):
mock_traits = mocker.Mock()
mock_traits.get_trait_names.return_value = list(existing_traits)
mock_context = mocker.Mock()
mock_node = mocker.Mock(id=1234, uuid="node-uuid", traits=mock_traits)
return mocker.Mock(node=mock_node, context=mock_context), mock_node, mock_context


def test_preserves_other_custom_traits(mocker, caplog):
"""Re-inspection must not wipe traits set by other hooks/rules."""
caplog.set_level(logging.DEBUG)

existing = [
"CUSTOM_FIRMWARE_UPDATE_R7615",
"CUSTOM_NETGROUP_F20_1_NETWORK",
"CUSTOM_NETWORK_SWITCH",
"CUSTOM_STORAGE_SWITCH",
]
mock_task, mock_node, mock_context = _mock_task(mocker, existing)
trait_create = mocker.patch(
"ironic_understack.inspect_hook_chassis_model.objects.TraitList.create"
)

InspectHookChassisModel().__call__(mock_task, _INVENTORY, _PLUGIN_DATA)

mock_node.save.assert_called_once()
trait_create.assert_called_once_with(
mock_context,
1234,
set(existing) | {"CUSTOM_CHASSIS_DELL_POWEREDGE_R7615"},
)


def test_replaces_stale_chassis_trait(mocker):
"""A stale CUSTOM_CHASSIS_ trait is removed; other traits are kept."""
existing = ["CUSTOM_NETWORK_SWITCH", "CUSTOM_CHASSIS_DELL_POWEREDGE_R6615"]
mock_task, mock_node, mock_context = _mock_task(mocker, existing)
trait_create = mocker.patch(
"ironic_understack.inspect_hook_chassis_model.objects.TraitList.create"
)

InspectHookChassisModel().__call__(mock_task, _INVENTORY, _PLUGIN_DATA)

mock_node.save.assert_called_once()
trait_create.assert_called_once_with(
mock_context,
1234,
{"CUSTOM_NETWORK_SWITCH", "CUSTOM_CHASSIS_DELL_POWEREDGE_R7615"},
)
Loading