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
113 changes: 84 additions & 29 deletions python/ironic-understack/ironic_understack/port_bios_name_hook.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
from typing import ClassVar

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

from ironic_understack.ironic_wrapper import ironic_ports_for_node

LOG = logging.getLogger(__name__)

PXE_BIOS_NAME_PREFIXES = ["NIC.Integrated", "NIC.Slot"]
BIOS_SETTING_NAME = "HttpDev1Interface"


class PortBiosNameHook(base.InspectionHook):
"""Set name, extra.bios_name and pxe_enabled fields from redfish data.
"""Set bios_name, pxe_enabled, local_link_connection and physical_network.

In addition, this hook ensures that the PXE port has sufficient data to
allow neutron to boot a new node for inspection. If the physical_network
and local_link_connections are not populated, we fill them with placeholder
data.
Populates extra.bios_name and port name from inspection inventory, then
determines the PXE port from the BIOS HttpDev1Interface setting (populated
during enrolment). Falls back to a naming-convention heuristic if the
BIOS setting is unavailable.

This is necessary because neutron throws errors if the port doesn't have
those fields filled in.
The PXE port gets pxe_enabled=True plus placeholder physical_network and
local_link_connection values that neutron requires.
"""

# "ports" hook creates baremetal ports for each physical NIC, be sure to run
# this first because we will only be updating ports that already exist:
dependencies: ClassVar[list[str]] = ["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)
Expand All @@ -37,25 +37,80 @@ def __call__(self, task, inventory, plugin_data):
i["mac_address"].upper(): i["name"] for i in inspected_interfaces
}

pxe_interface = _pxe_interface_name(inspected_interfaces, task.node.uuid)
pxe_nic = _bios_pxe_nic(task)

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)
required_pxe = required_bios_name == pxe_interface

_set_port_extra(baremetal_port, mac, required_bios_name)
_set_port_pxe_enabled(baremetal_port, mac, required_pxe)
_set_port_name(baremetal_port, mac, required_bios_name, task.node.name)
_set_port_physical_network(baremetal_port, mac, required_pxe)
_set_port_local_link_connection(baremetal_port, mac, required_pxe)

bios_name = interface_names.get(mac)

_set_port_extra(baremetal_port, mac, bios_name)
_set_port_name(baremetal_port, mac, bios_name, task.node.name)

if pxe_nic:
is_pxe = bios_name is not None and (
pxe_nic.startswith(bios_name) or bios_name.startswith(pxe_nic)
)
else:
# Fallback: heuristic based on naming convention
is_pxe = bios_name == _pxe_interface_name(
inspected_interfaces, task.node.uuid
)

if baremetal_port.pxe_enabled != is_pxe:
LOG.info(
"Port %s (%s) pxe_enabled %s -> %s",
mac,
bios_name,
baremetal_port.pxe_enabled,
is_pxe,
)
baremetal_port.pxe_enabled = is_pxe
baremetal_port.save()

if is_pxe:
_set_port_physical_network(baremetal_port, mac)
_set_port_local_link_connection(baremetal_port, mac)


def _bios_pxe_nic(task):
"""Read the BIOS PXE NIC FQDD, or return None if unavailable."""
try:
task.driver.bios.cache_bios_settings(task)
except Exception:
LOG.warning(
"Cannot cache BIOS settings for node %s, "
"falling back to naming heuristic for PXE port.",
task.node.uuid,
)
return None

try:
setting = BIOSSetting.get(task.context, task.node.id, BIOS_SETTING_NAME)
except exception.BIOSSettingNotFound:
LOG.warning(
"BIOS setting %s not found for node %s, "
"falling back to naming heuristic for PXE port.",
BIOS_SETTING_NAME,
task.node.uuid,
)
return None

if not setting.value:
LOG.warning(
"BIOS setting %s is empty for node %s, "
"falling back to naming heuristic for PXE port.",
BIOS_SETTING_NAME,
task.node.uuid,
)
return None

def _set_port_pxe_enabled(baremetal_port, mac, required_pxe):
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()
LOG.info(
"Node %s BIOS %s = %s",
task.node.uuid,
BIOS_SETTING_NAME,
setting.value,
)
return setting.value


def _set_port_extra(baremetal_port, mac, required_bios_name):
Expand Down Expand Up @@ -90,15 +145,15 @@ def _set_port_name(baremetal_port, mac, required_bios_name, node_name):
baremetal_port.save()


def _set_port_physical_network(baremetal_port, mac, required_pxe):
if required_pxe and not baremetal_port.physical_network:
def _set_port_physical_network(baremetal_port, mac):
if not baremetal_port.physical_network:
LOG.info("Port %s changing physical_network from None to 'enrol'", mac)
baremetal_port.physical_network = "enrol"
baremetal_port.save()


def _set_port_local_link_connection(baremetal_port, mac, required_pxe):
if required_pxe and not baremetal_port.local_link_connection:
def _set_port_local_link_connection(baremetal_port, mac):
if not baremetal_port.local_link_connection:
baremetal_port.local_link_connection = {
"port_id": "None",
"switch_id": "00:00:00:00:00:00",
Expand Down
Loading
Loading