diff --git a/components/images-openstack.yaml b/components/images-openstack.yaml index 441414df5..8f76ee86c 100644 --- a/components/images-openstack.yaml +++ b/components/images-openstack.yaml @@ -23,7 +23,7 @@ images: # ironic ironic_api: "ghcr.io/rackerlabs/understack/ironic:2025.2-ubuntu_jammy" - ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:2025.2-ubuntu_jammy" + ironic_conductor: "ghcr.io/rackerlabs/understack/ironic:pr-1347" ironic_pxe: "ghcr.io/rackerlabs/understack/ironic:2025.2-ubuntu_jammy" ironic_pxe_init: "ghcr.io/rackerlabs/understack/ironic:2025.2-ubuntu_jammy" ironic_pxe_http: "docker.io/nginx:1.13.3" diff --git a/components/ironic/values.yaml b/components/ironic/values.yaml index a0935fc8b..4fc822877 100644 --- a/components/ironic/values.yaml +++ b/components/ironic/values.yaml @@ -92,7 +92,10 @@ conf: # Default hooks include: ramdisk-error,validate-interfaces,ports,architecture # We override to exclude 'ports' from the default hooks default_hooks: "ramdisk-error,validate-interfaces,architecture" - hooks: "$default_hooks,pci-devices,parse-lldp,local-link-connection,resource-class" + hooks: "$default_hooks,pci-devices,validate-interfaces,parse-lldp,resource-class,update-baremetal-port" + redfish: + inspection_hooks: "ramdisk-error,validate-interfaces,ports,port-bios-name,architecture,pci-devices" + add_ports: "all" # enable sensors and metrics for redfish metrics - https://docs.openstack.org/ironic/latest/admin/drivers/redfish/metrics.html sensor_data: send_sensor_data: true diff --git a/python/ironic-understack/ironic_understack/conf.py b/python/ironic-understack/ironic_understack/conf.py index d41bbb234..459d4ff30 100644 --- a/python/ironic-understack/ironic_understack/conf.py +++ b/python/ironic-understack/ironic_understack/conf.py @@ -10,7 +10,22 @@ def setup_conf(): "device_types_dir", help="directory storing Device Type description YAML files", default="/var/lib/understack/device-types", - ) + ), + cfg.DictOpt( + "switch_name_vlan_group_mapping", + help="Dictionary of switch hostname suffix to vlan group name", + default={ + "1": "network", + "2": "network", + "3": "network", + "4": "network", + "1f": "storage", + "2f": "storage", + "3f": "storage-appliance", + "4f": "storage-appliance", + "1d": "bmc", + }, + ), ] cfg.CONF.register_group(grp) cfg.CONF.register_opts(opts, group=grp) diff --git a/python/ironic-understack/ironic_understack/example-inventory.json b/python/ironic-understack/ironic_understack/example-inventory.json new file mode 100644 index 000000000..27ae4b184 --- /dev/null +++ b/python/ironic-understack/ironic_understack/example-inventory.json @@ -0,0 +1,897 @@ +{ + "inventory": { + "interfaces": [ + { + "name": "ens2f1np1", + "mac_address": "14:23:f3:f5:3c:a1", + "ipv4_address": null, + "ipv6_address": "fe80::1623:f3ff:fef5:3ca1%ens2f1np1", + "has_carrier": true, + "lldp": [ + [ + 1, + "04c47ee0e7a037" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "45746865726e6574312f31" + ], + [ + 5, + "6632302d332d32662e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f0405dc" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d3237323430384e39" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c4:00.1", + "driver": "bnxt_en" + }, + { + "name": "eno8403", + "mac_address": "c4:cb:e1:bf:90:8f", + "ipv4_address": null, + "ipv6_address": null, + "has_carrier": false, + "lldp": [], + "vendor": "0x14e4", + "product": "0x165f", + "client_id": null, + "biosdevname": null, + "speed_mbps": 1000, + "pci_address": "0000:c3:00.1", + "driver": "tg3" + }, + { + "name": "eno3np0", + "mac_address": "d4:04:e6:4f:7a:dc", + "ipv4_address": "10.4.50.78", + "ipv6_address": "fe80::d604:e6ff:fe4f:7adc%eno3np0", + "has_carrier": true, + "lldp": [ + [ + 1, + "04c47ee0e4553f" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "44656c6c2d393347535730345f4e49432e496e74656772617465642e312d31" + ], + [ + 5, + "6632302d332d312e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f042328" + ], + [ + 127, + "0080c2030faa11343031305f70726f766973696f6e696e67" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d323732343042384d" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.0", + "driver": "bnxt_en" + }, + { + "name": "eno4np1", + "mac_address": "d4:04:e6:4f:7a:dd", + "ipv4_address": null, + "ipv6_address": "fe80::d604:e6ff:fe4f:7add%eno4np1", + "has_carrier": true, + "lldp": [ + [ + 1, + "04401482813ee3" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "45746865726e6574312f31" + ], + [ + 5, + "6632302d332d31662e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f0405dc" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d3237323430384d4c" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.1", + "driver": "bnxt_en" + }, + { + "name": "ens2f0np0", + "mac_address": "14:23:f3:f5:3c:a0", + "ipv4_address": "10.4.50.88", + "ipv6_address": "fe80::1623:f3ff:fef5:3ca0%ens2f0np0", + "has_carrier": true, + "lldp": [ + [ + 1, + "04c47ee0e40337" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "44656c6c2d393347535730345f4e49432e536c6f742e312d31" + ], + [ + 5, + "6632302d332d322e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f042328" + ], + [ + 127, + "0080c2030faa11343031305f70726f766973696f6e696e67" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d3237323430424432" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c4:00.0", + "driver": "bnxt_en" + }, + { + "name": "eno8303", + "mac_address": "c4:cb:e1:bf:90:8e", + "ipv4_address": null, + "ipv6_address": null, + "has_carrier": false, + "lldp": [], + "vendor": "0x14e4", + "product": "0x165f", + "client_id": null, + "biosdevname": null, + "speed_mbps": 1000, + "pci_address": "0000:c3:00.0", + "driver": "tg3" + } + ], + "cpu": { + "model_name": "AMD EPYC 9124 16-Core Processor", + "frequency": "", + "count": 32, + "architecture": "x86_64", + "flags": [ + "fpu", + "vme", + "de", + "pse", + "tsc", + "msr", + "pae", + "mce", + "cx8", + "apic", + "sep", + "mtrr", + "pge", + "mca", + "cmov", + "pat", + "pse36", + "clflush", + "mmx", + "fxsr", + "sse", + "sse2", + "ht", + "syscall", + "nx", + "mmxext", + "fxsr_opt", + "pdpe1gb", + "rdtscp", + "lm", + "constant_tsc", + "rep_good", + "amd_lbr_v2", + "nopl", + "nonstop_tsc", + "cpuid", + "extd_apicid", + "aperfmperf", + "rapl", + "pni", + "pclmulqdq", + "monitor", + "ssse3", + "fma", + "cx16", + "pcid", + "sse4_1", + "sse4_2", + "x2apic", + "movbe", + "popcnt", + "aes", + "xsave", + "avx", + "f16c", + "rdrand", + "lahf_lm", + "cmp_legacy", + "svm", + "extapic", + "cr8_legacy", + "abm", + "sse4a", + "misalignsse", + "3dnowprefetch", + "osvw", + "ibs", + "skinit", + "wdt", + "tce", + "topoext", + "perfctr_core", + "perfctr_nb", + "bpext", + "perfctr_llc", + "mwaitx", + "cpb", + "cat_l3", + "cdp_l3", + "invpcid_single", + "hw_pstate", + "ssbd", + "mba", + "perfmon_v2", + "ibrs", + "ibpb", + "stibp", + "ibrs_enhanced", + "vmmcall", + "fsgsbase", + "bmi1", + "avx2", + "smep", + "bmi2", + "erms", + "invpcid", + "cqm", + "rdt_a", + "avx512f", + "avx512dq", + "rdseed", + "adx", + "smap", + "avx512ifma", + "clflushopt", + "clwb", + "avx512cd", + "sha_ni", + "avx512bw", + "avx512vl", + "xsaveopt", + "xsavec", + "xgetbv1", + "xsaves", + "cqm_llc", + "cqm_occup_llc", + "cqm_mbm_total", + "cqm_mbm_local", + "avx512_bf16", + "clzero", + "irperf", + "xsaveerptr", + "rdpru", + "wbnoinvd", + "amd_ppin", + "cppc", + "arat", + "npt", + "lbrv", + "svm_lock", + "nrip_save", + "tsc_scale", + "vmcb_clean", + "flushbyasid", + "decodeassists", + "pausefilter", + "pfthreshold", + "avic", + "v_vmsave_vmload", + "vgif", + "x2avic", + "v_spec_ctrl", + "avx512vbmi", + "umip", + "pku", + "ospke", + "avx512_vbmi2", + "gfni", + "vaes", + "vpclmulqdq", + "avx512_vnni", + "avx512_bitalg", + "avx512_vpopcntdq", + "la57", + "rdpid", + "overflow_recov", + "succor", + "smca", + "fsrm", + "flush_l1d" + ], + "socket_count": 1 + }, + "disks": [ + { + "name": "/dev/sda", + "model": "PERC H755 Front", + "size": 479559942144, + "rotational": false, + "wwn": "0x6f4ee0806aa75c0030a09e5ae911ff27", + "serial": "0027ff11e95a9ea030005ca76a80e04e", + "vendor": "DELL", + "wwn_with_extension": "0x6f4ee0806aa75c0030a09e5ae911ff27", + "wwn_vendor_extension": "0x30a09e5ae911ff27", + "hctl": "0:3:111:0", + "by_path": "/dev/disk/by-path/pci-0000:41:00.0-scsi-0:3:111:0", + "logical_sectors": 512, + "physical_sectors": 512 + } + ], + "memory": { + "total": 100793757696, + "physical_mb": 98304 + }, + "bmc_address": "10.46.96.164", + "bmc_v6address": "::/0", + "system_vendor": { + "product_name": "PowerEdge R7615 (SKU=0AF7;ModelName=PowerEdge R7615)", + "serial_number": "93GSW04", + "manufacturer": "Dell Inc.", + "firmware": { + "vendor": "Dell Inc.", + "version": "1.6.10", + "build_date": "12/08/2023" + } + }, + "boot": { + "current_boot_mode": "uefi", + "pxe_interface": "d4:04:e6:4f:7a:dc" + }, + "hostname": "debian", + "bmc_mac": "a8:3c:a5:35:4a:b2" + }, + "plugin_data": { + "root_disk": { + "name": "/dev/sda", + "model": "PERC H755 Front", + "size": 479559942144, + "rotational": false, + "wwn": "0x6f4ee0806aa75c0030a09e5ae911ff27", + "serial": "0027ff11e95a9ea030005ca76a80e04e", + "vendor": "DELL", + "wwn_with_extension": "0x6f4ee0806aa75c0030a09e5ae911ff27", + "wwn_vendor_extension": "0x30a09e5ae911ff27", + "hctl": "0:3:111:0", + "by_path": "/dev/disk/by-path/pci-0000:41:00.0-scsi-0:3:111:0", + "logical_sectors": 512, + "physical_sectors": 512 + }, + "boot_interface": "d4:04:e6:4f:7a:dc", + "configuration": { + "collectors": [ + "default", + "logs" + ], + "managers": [ + { + "name": "generic_hardware_manager", + "version": "1.2" + } + ] + }, + "error": null, + "all_interfaces": { + "ens2f1np1": { + "name": "ens2f1np1", + "mac_address": "14:23:f3:f5:3c:a1", + "ipv4_address": null, + "ipv6_address": "fe80::1623:f3ff:fef5:3ca1", + "has_carrier": true, + "lldp": [ + [ + 1, + "04c47ee0e7a037" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "45746865726e6574312f31" + ], + [ + 5, + "6632302d332d32662e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f0405dc" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d3237323430384e39" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c4:00.1", + "driver": "bnxt_en", + "pxe_enabled": false + }, + "eno8403": { + "name": "eno8403", + "mac_address": "c4:cb:e1:bf:90:8f", + "ipv4_address": null, + "ipv6_address": null, + "has_carrier": false, + "lldp": [], + "vendor": "0x14e4", + "product": "0x165f", + "client_id": null, + "biosdevname": null, + "speed_mbps": 1000, + "pci_address": "0000:c3:00.1", + "driver": "tg3", + "pxe_enabled": false + }, + "eno3np0": { + "name": "eno3np0", + "mac_address": "d4:04:e6:4f:7a:dc", + "ipv4_address": "10.4.50.78", + "ipv6_address": "fe80::d604:e6ff:fe4f:7adc", + "has_carrier": true, + "lldp": [ + [ + 1, + "04c47ee0e4553f" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "44656c6c2d393347535730345f4e49432e496e74656772617465642e312d31" + ], + [ + 5, + "6632302d332d312e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f042328" + ], + [ + 127, + "0080c2030faa11343031305f70726f766973696f6e696e67" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d323732343042384d" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.0", + "driver": "bnxt_en", + "pxe_enabled": true + }, + "eno4np1": { + "name": "eno4np1", + "mac_address": "d4:04:e6:4f:7a:dd", + "ipv4_address": null, + "ipv6_address": "fe80::d604:e6ff:fe4f:7add", + "has_carrier": true, + "lldp": [ + [ + 1, + "04401482813ee3" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "45746865726e6574312f31" + ], + [ + 5, + "6632302d332d31662e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f0405dc" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d3237323430384d4c" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.1", + "driver": "bnxt_en", + "pxe_enabled": false + }, + "ens2f0np0": { + "name": "ens2f0np0", + "mac_address": "14:23:f3:f5:3c:a0", + "ipv4_address": "10.4.50.88", + "ipv6_address": "fe80::1623:f3ff:fef5:3ca0", + "has_carrier": true, + "lldp": [ + [ + 1, + "04c47ee0e40337" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "44656c6c2d393347535730345f4e49432e536c6f742e312d31" + ], + [ + 5, + "6632302d332d322e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f042328" + ], + [ + 127, + "0080c2030faa11343031305f70726f766973696f6e696e67" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d3237323430424432" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c4:00.0", + "driver": "bnxt_en", + "pxe_enabled": false + }, + "eno8303": { + "name": "eno8303", + "mac_address": "c4:cb:e1:bf:90:8e", + "ipv4_address": null, + "ipv6_address": null, + "has_carrier": false, + "lldp": [], + "vendor": "0x14e4", + "product": "0x165f", + "client_id": null, + "biosdevname": null, + "speed_mbps": 1000, + "pci_address": "0000:c3:00.0", + "driver": "tg3", + "pxe_enabled": false + } + }, + "valid_interfaces": { + "eno3np0": { + "name": "eno3np0", + "mac_address": "d4:04:e6:4f:7a:dc", + "ipv4_address": "10.4.50.78", + "ipv6_address": "fe80::d604:e6ff:fe4f:7adc", + "has_carrier": true, + "lldp": [ + [ + 1, + "04c47ee0e4553f" + ], + [ + 2, + "0545746865726e6574312f31" + ], + [ + 3, + "0078" + ], + [ + 4, + "44656c6c2d393347535730345f4e49432e496e74656772617465642e312d31" + ], + [ + 5, + "6632302d332d312e69616433" + ], + [ + 127, + "0001420101" + ], + [ + 127, + "00120f042328" + ], + [ + 127, + "0080c2030faa11343031305f70726f766973696f6e696e67" + ], + [ + 127, + "0080c2070100000000" + ], + [ + 127, + "00014208464c4d323732343042384d" + ], + [ + 0, + "" + ] + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": null, + "biosdevname": null, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.0", + "driver": "bnxt_en", + "pxe_enabled": true + } + }, + "macs": [ + "d4:04:e6:4f:7a:dc" + ], + "parsed_lldp": { + "ens2f1np1": { + "switch_chassis_id": "c4:7e:e0:e7:a0:37", + "switch_port_id": "Ethernet1/1", + "switch_port_description": "Ethernet1/1", + "switch_system_name": "f20-3-2f.iad3", + "switch_port_mtu": 1500, + "switch_port_link_aggregation_enabled": false, + "switch_port_link_aggregation_support": true, + "switch_port_link_aggregation_id": 0 + }, + "eno3np0": { + "switch_chassis_id": "c4:7e:e0:e4:55:3f", + "switch_port_id": "Ethernet1/1", + "switch_port_description": "Dell-93GSW04_NIC.Integrated.1-1", + "switch_system_name": "f20-3-1.iad3", + "switch_port_mtu": 9000, + "switch_port_vlans": [ + { + "name": "4010_provisioning", + "id": 4010 + } + ], + "switch_port_link_aggregation_enabled": false, + "switch_port_link_aggregation_support": true, + "switch_port_link_aggregation_id": 0 + }, + "eno4np1": { + "switch_chassis_id": "40:14:82:81:3e:e3", + "switch_port_id": "Ethernet1/1", + "switch_port_description": "Ethernet1/1", + "switch_system_name": "f20-3-1f.iad3", + "switch_port_mtu": 1500, + "switch_port_link_aggregation_enabled": false, + "switch_port_link_aggregation_support": true, + "switch_port_link_aggregation_id": 0 + }, + "ens2f0np0": { + "switch_chassis_id": "c4:7e:e0:e4:03:37", + "switch_port_id": "Ethernet1/1", + "switch_port_description": "Dell-93GSW04_NIC.Slot.1-1", + "switch_system_name": "f20-3-2.iad3", + "switch_port_mtu": 9000, + "switch_port_vlans": [ + { + "name": "4010_provisioning", + "id": 4010 + } + ], + "switch_port_link_aggregation_enabled": false, + "switch_port_link_aggregation_support": true, + "switch_port_link_aggregation_id": 0 + } + } + } +} diff --git a/python/ironic-understack/ironic_understack/inspected_port.py b/python/ironic-understack/ironic_understack/inspected_port.py new file mode 100644 index 000000000..420afb594 --- /dev/null +++ b/python/ironic-understack/ironic_understack/inspected_port.py @@ -0,0 +1,53 @@ +from argparse import ArgumentError +from dataclasses import dataclass + + +@dataclass +class InspectedPort: + """Represents the 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) -> tuple[str, 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 -" + ) + + rack_name, switch_suffix = parts + + return rack_name, switch_suffix, data_center_name + + @property + def rack_name(self) -> str: + return self.parsed_name[0] + + @property + def switch_suffix(self) -> str: + return self.parsed_name[1] + + @property + def data_center_name(self) -> str: + return self.parsed_name[2] diff --git a/python/ironic-understack/ironic_understack/ironic_wrapper.py b/python/ironic-understack/ironic_understack/ironic_wrapper.py new file mode 100644 index 000000000..adbbe8002 --- /dev/null +++ b/python/ironic-understack/ironic_understack/ironic_wrapper.py @@ -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) diff --git a/python/ironic-understack/ironic_understack/output-inspection b/python/ironic-understack/ironic_understack/output-inspection new file mode 100644 index 000000000..3e504d9cf --- /dev/null +++ b/python/ironic-understack/ironic_understack/output-inspection @@ -0,0 +1,611 @@ +2025-10-21 10:06:12.416 1 DEBUG ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] called with task= + + +inventory = { + "interfaces": [ + { + "name": "eno3np0", + "mac_address": "d4:04:e6:4f:7a:dc", + "ipv4_address": "10.4.50.78", + "ipv6_address": "fe80::d604:e6ff:fe4f:7adc%eno3np0", + "has_carrier": True, + "lldp": [ + [1, "04c47ee0e4553f"], + [2, "0545746865726e6574312f31"], + [3, "0078"], + [4, "44656c6c2d393347535730345f4e49432e496e74656772617465642e312d31"], + [5, "6632302d332d312e69616433"], + [127, "0001420101"], + [127, "00120f042328"], + [127, "0080c2030faa11343031305f70726f766973696f6e696e67"], + [127, "0080c2070100000000"], + [127, "00014208464c4d323732343042384d"], + [0, ""], + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.0", + "driver": "bnxt_en", + }, + { + "name": "ens2f0np0", + "mac_address": "14:23:f3:f5:3c:a0", + "ipv4_address": None, + "ipv6_address": None, + "has_carrier": False, + "lldp": [], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c4:00.0", + "driver": "bnxt_en", + }, + { + "name": "eno4np1", + "mac_address": "d4:04:e6:4f:7a:dd", + "ipv4_address": None, + "ipv6_address": "fe80::d604:e6ff:fe4f:7add%eno4np1", + "has_carrier": True, + "lldp": [ + [1, "04401482813ee3"], + [2, "0545746865726e6574312f31"], + [3, "0078"], + [4, "45746865726e6574312f31"], + [5, "6632302d332d31662e69616433"], + [127, "0001420101"], + [127, "00120f0405dc"], + [127, "0080c2070100000000"], + [127, "00014208464c4d3237323430384d4c"], + [0, ""], + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.1", + "driver": "bnxt_en", + }, + { + "name": "ens2f1np1", + "mac_address": "14:23:f3:f5:3c:a1", + "ipv4_address": None, + "ipv6_address": "fe80::1623:f3ff:fef5:3ca1%ens2f1np1", + "has_carrier": True, + "lldp": [ + [1, "04c47ee0e7a037"], + [2, "0545746865726e6574312f31"], + [3, "0078"], + [4, "45746865726e6574312f31"], + [5, "6632302d332d32662e69616433"], + [127, "0001420101"], + [127, "00120f0405dc"], + [127, "0080c2070100000000"], + [127, "00014208464c4d3237323430384e39"], + [0, ""], + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c4:00.1", + "driver": "bnxt_en", + }, + { + "name": "eno8303", + "mac_address": "c4:cb:e1:bf:90:8e", + "ipv4_address": None, + "ipv6_address": None, + "has_carrier": False, + "lldp": [], + "vendor": "0x14e4", + "product": "0x165f", + "client_id": None, + "biosdevname": None, + "speed_mbps": 1000, + "pci_address": "0000:c3:00.0", + "driver": "tg3", + }, + { + "name": "eno8403", + "mac_address": "c4:cb:e1:bf:90:8f", + "ipv4_address": None, + "ipv6_address": None, + "has_carrier": False, + "lldp": [], + "vendor": "0x14e4", + "product": "0x165f", + "client_id": None, + "biosdevname": None, + "speed_mbps": 1000, + "pci_address": "0000:c3:00.1", + "driver": "tg3", + }, + ], + "cpu": { + "model_name": "AMD EPYC 9124 16-Core Processor", + "frequency": "", + "count": 32, + "architecture": "x86_64", + "flags": [ + "fpu", + "vme", + "de", + "pse", + "tsc", + "msr", + "pae", + "mce", + "cx8", + "apic", + "sep", + "mtrr", + "pge", + "mca", + "cmov", + "pat", + "pse36", + "clflush", + "mmx", + "fxsr", + "sse", + "sse2", + "ht", + "syscall", + "nx", + "mmxext", + "fxsr_opt", + "pdpe1gb", + "rdtscp", + "lm", + "constant_tsc", + "rep_good", + "amd_lbr_v2", + "nopl", + "nonstop_tsc", + "cpuid", + "extd_apicid", + "aperfmperf", + "rapl", + "pni", + "pclmulqdq", + "monitor", + "ssse3", + "fma", + "cx16", + "pcid", + "sse4_1", + "sse4_2", + "x2apic", + "movbe", + "popcnt", + "aes", + "xsave", + "avx", + "f16c", + "rdrand", + "lahf_lm", + "cmp_legacy", + "svm", + "extapic", + "cr8_legacy", + "abm", + "sse4a", + "misalignsse", + "3dnowprefetch", + "osvw", + "ibs", + "skinit", + "wdt", + "tce", + "topoext", + "perfctr_core", + "perfctr_nb", + "bpext", + "perfctr_llc", + "mwaitx", + "cpb", + "cat_l3", + "cdp_l3", + "invpcid_single", + "hw_pstate", + "ssbd", + "mba", + "perfmon_v2", + "ibrs", + "ibpb", + "stibp", + "ibrs_enhanced", + "vmmcall", + "fsgsbase", + "bmi1", + "avx2", + "smep", + "bmi2", + "erms", + "invpcid", + "cqm", + "rdt_a", + "avx512f", + "avx512dq", + "rdseed", + "adx", + "smap", + "avx512ifma", + "clflushopt", + "clwb", + "avx512cd", + "sha_ni", + "avx512bw", + "avx512vl", + "xsaveopt", + "xsavec", + "xgetbv1", + "xsaves", + "cqm_llc", + "cqm_occup_llc", + "cqm_mbm_total", + "cqm_mbm_local", + "avx512_bf16", + "clzero", + "irperf", + "xsaveerptr", + "rdpru", + "wbnoinvd", + "amd_ppin", + "cppc", + "arat", + "npt", + "lbrv", + "svm_lock", + "nrip_save", + "tsc_scale", + "vmcb_clean", + "flushbyasid", + "decodeassists", + "pausefilter", + "pfthreshold", + "avic", + "v_vmsave_vmload", + "vgif", + "x2avic", + "v_spec_ctrl", + "avx512vbmi", + "umip", + "pku", + "ospke", + "avx512_vbmi2", + "gfni", + "vaes", + "vpclmulqdq", + "avx512_vnni", + "avx512_bitalg", + "avx512_vpopcntdq", + "la57", + "rdpid", + "overflow_recov", + "succor", + "smca", + "fsrm", + "flush_l1d", + ], + "socket_count": 1, + }, + "disks": [ + { + "name": "/dev/sda", + "model": "PERC H755 Front", + "size": 479559942144, + "rotational": False, + "wwn": "0x6f4ee0806aa75c00300aa5c17e4ad41a", + "serial": "001ad44a7ec1a50a30005ca76a80e04e", + "vendor": "DELL", + "wwn_with_extension": "0x6f4ee0806aa75c00300aa5c17e4ad41a", + "wwn_vendor_extension": "0x300aa5c17e4ad41a", + "hctl": "0:3:110:0", + "by_path": "/dev/disk/by-path/pci-0000:41:00.0-scsi-0:3:110:0", + "logical_sectors": 512, + "physical_sectors": 512, + } + ], + "memory": {"total": 100793757696, "physical_mb": 98304}, + "bmc_address": "10.46.96.164", + "bmc_v6address": "::/0", + "system_vendor": { + "product_name": "PowerEdge R7615 (SKU=0AF7;ModelName=PowerEdge R7615)", + "serial_number": "93GSW04", + "manufacturer": "Dell Inc.", + "firmware": { + "vendor": "Dell Inc.", + "version": "1.6.10", + "build_date": "12/08/2023", + }, + }, + "boot": {"current_boot_mode": "uefi", "pxe_interface": "d4:04:e6:4f:7a:dc"}, + "hostname": "debian", + "bmc_mac": "a8:3c:a5:35:4a:b2", +} + + +plugin_data = { + "root_disk": { + "name": "/dev/sda", + "model": "PERC H755 Front", + "size": 479559942144, + "rotational": False, + "wwn": "0x6f4ee0806aa75c00300aa5c17e4ad41a", + "serial": "001ad44a7ec1a50a30005ca76a80e04e", + "vendor": "DELL", + "wwn_with_extension": "0x6f4ee0806aa75c00300aa5c17e4ad41a", + "wwn_vendor_extension": "0x300aa5c17e4ad41a", + "hctl": "0:3:110:0", + "by_path": "/dev/disk/by-path/pci-0000:41:00.0-scsi-0:3:110:0", + "logical_sectors": 512, + "physical_sectors": 512, + }, + "boot_interface": "d4:04:e6:4f:7a:dc", + "configuration": { + "collectors": ["default", "logs"], + "managers": [{"name": "generic_hardware_manager", "version": "1.2"}], + }, + "all_interfaces": { + "eno3np0": { + "name": "eno3np0", + "mac_address": "d4:04:e6:4f:7a:dc", + "ipv4_address": "10.4.50.78", + "ipv6_address": "fe80::d604:e6ff:fe4f:7adc", + "has_carrier": True, + "lldp": [ + [1, "04c47ee0e4553f"], + [2, "0545746865726e6574312f31"], + [3, "0078"], + [4, "44656c6c2d393347535730345f4e49432e496e74656772617465642e312d31"], + [5, "6632302d332d312e69616433"], + [127, "0001420101"], + [127, "00120f042328"], + [127, "0080c2030faa11343031305f70726f766973696f6e696e67"], + [127, "0080c2070100000000"], + [127, "00014208464c4d323732343042384d"], + [0, ""], + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.0", + "driver": "bnxt_en", + "pxe_enabled": True, + }, + "ens2f0np0": { + "name": "ens2f0np0", + "mac_address": "14:23:f3:f5:3c:a0", + "ipv4_address": None, + "ipv6_address": None, + "has_carrier": False, + "lldp": [], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c4:00.0", + "driver": "bnxt_en", + "pxe_enabled": False, + }, + "eno4np1": { + "name": "eno4np1", + "mac_address": "d4:04:e6:4f:7a:dd", + "ipv4_address": None, + "ipv6_address": "fe80::d604:e6ff:fe4f:7add", + "has_carrier": True, + "lldp": [ + [1, "04401482813ee3"], + [2, "0545746865726e6574312f31"], + [3, "0078"], + [4, "45746865726e6574312f31"], + [5, "6632302d332d31662e69616433"], + [127, "0001420101"], + [127, "00120f0405dc"], + [127, "0080c2070100000000"], + [127, "00014208464c4d3237323430384d4c"], + [0, ""], + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.1", + "driver": "bnxt_en", + "pxe_enabled": False, + }, + "ens2f1np1": { + "name": "ens2f1np1", + "mac_address": "14:23:f3:f5:3c:a1", + "ipv4_address": None, + "ipv6_address": "fe80::1623:f3ff:fef5:3ca1", + "has_carrier": True, + "lldp": [ + [1, "04c47ee0e7a037"], + [2, "0545746865726e6574312f31"], + [3, "0078"], + [4, "45746865726e6574312f31"], + [5, "6632302d332d32662e69616433"], + [127, "0001420101"], + [127, "00120f0405dc"], + [127, "0080c2070100000000"], + [127, "00014208464c4d3237323430384e39"], + [0, ""], + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c4:00.1", + "driver": "bnxt_en", + "pxe_enabled": False, + }, + "eno8303": { + "name": "eno8303", + "mac_address": "c4:cb:e1:bf:90:8e", + "ipv4_address": None, + "ipv6_address": None, + "has_carrier": False, + "lldp": [], + "vendor": "0x14e4", + "product": "0x165f", + "client_id": None, + "biosdevname": None, + "speed_mbps": 1000, + "pci_address": "0000:c3:00.0", + "driver": "tg3", + "pxe_enabled": False, + }, + "eno8403": { + "name": "eno8403", + "mac_address": "c4:cb:e1:bf:90:8f", + "ipv4_address": None, + "ipv6_address": None, + "has_carrier": False, + "lldp": [], + "vendor": "0x14e4", + "product": "0x165f", + "client_id": None, + "biosdevname": None, + "speed_mbps": 1000, + "pci_address": "0000:c3:00.1", + "driver": "tg3", + "pxe_enabled": False, + }, + }, + "valid_interfaces": { + "eno3np0": { + "name": "eno3np0", + "mac_address": "d4:04:e6:4f:7a:dc", + "ipv4_address": "10.4.50.78", + "ipv6_address": "fe80::d604:e6ff:fe4f:7adc", + "has_carrier": True, + "lldp": [ + [1, "04c47ee0e4553f"], + [2, "0545746865726e6574312f31"], + [3, "0078"], + [4, "44656c6c2d393347535730345f4e49432e496e74656772617465642e312d31"], + [5, "6632302d332d312e69616433"], + [127, "0001420101"], + [127, "00120f042328"], + [127, "0080c2030faa11343031305f70726f766973696f6e696e67"], + [127, "0080c2070100000000"], + [127, "00014208464c4d323732343042384d"], + [0, ""], + ], + "vendor": "0x14e4", + "product": "0x16d7", + "client_id": None, + "biosdevname": None, + "speed_mbps": 25000, + "pci_address": "0000:c5:00.0", + "driver": "bnxt_en", + "pxe_enabled": True, + } + }, + "macs": ["d4:04:e6:4f:7a:dc"], +} + + + +__call__ /var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py:39 + +2025-10-21 10:06:12.429 1 DEBUG ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Updating port 1459 for node 160 local_link_connection {'port_id': 'Ethernet1/1', 'switch_id': 'c4:7e:e0:e4:55:3f', 'switch_info': 'f20-3-1.iad3'} _set_local_link_connection /var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py:87 +2025-10-21 10:06:12.452 1 WARNING ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Failed to extract local_link_info from LLDP data for 160 +2025-10-21 10:06:12.453 1 DEBUG ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Updating port 1463 for node 160 local_link_connection {} _set_local_link_connection /var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py:87 +2025-10-21 10:06:12.475 1 DEBUG ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Updating port 1461 for node 160 local_link_connection {'port_id': 'Ethernet1/1', 'switch_id': '40:14:82:81:3e:e3', 'switch_info': 'f20-3-1f.iad3'} _set_local_link_connection /var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py:87 +2025-10-21 10:06:12.493 1 DEBUG ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Updating port 1465 for node 160 local_link_connection {'port_id': 'Ethernet1/1', 'switch_id': 'c4:7e:e0:e7:a0:37', 'switch_info': 'f20-3-2f.iad3'} _set_local_link_connection /var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py:87 +2025-10-21 10:06:12.513 1 WARNING ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Failed to extract local_link_info from LLDP data for 160 +2025-10-21 10:06:12.513 1 DEBUG ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Updating port 1454 for node 160 local_link_connection {} _set_local_link_connection /var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py:87 +2025-10-21 10:06:12.529 1 WARNING ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Failed to extract local_link_info from LLDP data for 160 +2025-10-21 10:06:12.530 1 DEBUG ironic_understack.update_baremetal_port [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Updating port 1457 for node 160 local_link_connection {} _set_local_link_connection /var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py:87 +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Unexpected exception while running inspection hooks for node b6b6dcec-7d48-48c4-89ff-da04b8af40b7: AttributeError: 'Node' object has no attribute 'add_trait' +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils Traceback (most recent call last): +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils File "/var/lib/openstack/lib/python3.10/site-packages/ironic/drivers/modules/inspect_utils.py", line 545, in run_inspection_hooks +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils _run_post_hooks(task, inventory, plugin_data, hooks) +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils File "/var/lib/openstack/lib/python3.10/site-packages/ironic/drivers/modules/inspect_utils.py", line 621, in _run_post_hooks +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils hook.obj.__call__(task, inventory, plugin_data) +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils File "/var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py", line 82, in __call__ +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils _update_node_traits(task, vlan_groups) +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils File "/var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py", line 177, in _update_node_traits +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils task.node.add_trait(TRAIT_STORAGE_SWITCH) +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils AttributeError: 'Node' object has no attribute 'add_trait' +2025-10-21 10:06:12.540 1 ERROR ironic.drivers.modules.inspect_utils +2025-10-21 10:06:12.542 1 INFO ironic.drivers.utils [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Ramdisk logs were stored in local storage for node b6b6dcec-7d48-48c4-89ff-da04b8af40b7 +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Error when processing inspection data for node b6b6dcec-7d48-48c4-89ff-da04b8af40b7: ironic.common.exception.HardwareInspectionFailure: Failed to inspect hardware. Reason: Unexpected exception AttributeError during processing for node: b6b6dcec-7d48-48c4-89ff-da04b8af40b7. Error: 'Node' object has no attribute 'add_trait' +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection Traceback (most recent call last): +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection File "/var/lib/openstack/lib/python3.10/site-packages/ironic/drivers/modules/inspect_utils.py", line 545, in run_inspection_hooks +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection _run_post_hooks(task, inventory, plugin_data, hooks) +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection File "/var/lib/openstack/lib/python3.10/site-packages/ironic/drivers/modules/inspect_utils.py", line 621, in _run_post_hooks +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection hook.obj.__call__(task, inventory, plugin_data) +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection File "/var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py", line 82, in __call__ +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection _update_node_traits(task, vlan_groups) +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection File "/var/lib/openstack/lib/python3.10/site-packages/ironic_understack/update_baremetal_port.py", line 177, in _update_node_traits +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection task.node.add_trait(TRAIT_STORAGE_SWITCH) +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection AttributeError: 'Node' object has no attribute 'add_trait' +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection During handling of the above exception, another exception occurred: +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection Traceback (most recent call last): +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection File "/var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/inspection.py", line 125, in continue_inspection +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection result = task.driver.inspect.continue_inspection( +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection File "/var/lib/openstack/lib/python3.10/site-packages/ironic/drivers/modules/inspector/agent.py", line 94, in continue_inspection +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection inspect_utils.run_inspection_hooks(task, inventory, plugin_data, +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection File "/var/lib/openstack/lib/python3.10/site-packages/ironic/drivers/modules/inspect_utils.py", line 560, in run_inspection_hooks +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection raise exception.HardwareInspectionFailure(error=msg) +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection ironic.common.exception.HardwareInspectionFailure: Failed to inspect hardware. Reason: Unexpected exception AttributeError during processing for node: b6b6dcec-7d48-48c4-89ff-da04b8af40b7. Error: 'Node' object has no attribute 'add_trait' +2025-10-21 10:06:12.543 1 ERROR ironic.conductor.inspection +2025-10-21 10:06:12.550 1 DEBUG ironic.common.states [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Exiting old state 'inspecting' in response to event 'fail' on_exit /var/lib/openstack/lib/python3.10/site-packages/ironic/common/states.py:361 +2025-10-21 10:06:12.550 1 DEBUG ironic.common.states [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Entering new state 'inspect failed' in response to event 'fail' on_enter /var/lib/openstack/lib/python3.10/site-packages/ironic/common/states.py:367 +2025-10-21 10:06:12.571 1 ERROR ironic.conductor.task_manager [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Node b6b6dcec-7d48-48c4-89ff-da04b8af40b7 moved to provision state "inspect failed" from state "inspecting"; target provision state is "manageable": ironic.common.exception.HardwareInspectionFailure: Failed to inspect hardware. Reason: Unexpected exception AttributeError during processing for node: b6b6dcec-7d48-48c4-89ff-da04b8af40b7. Error: 'Node' object has no attribute 'add_trait' +2025-10-21 10:06:12.585 1 DEBUG ironic.conductor.task_manager [req-f2511219-7d71-4852-9b0e-cb5793c9d297 req-284c47c2-5626-42b0-81a9-1a4e880a1afd - - - - - -] Successfully released exclusive lock for continue inspection on node b6b6dcec-7d48-48c4-89ff-da04b8af40b7 (lock was held 0.26 sec) release_resources /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:459 +2025-10-21 10:07:01.159 1 DEBUG ironic.common.hash_ring [-] Rebuilding cached hash rings ring /var/lib/openstack/lib/python3.10/site-packages/ironic/common/hash_ring.py:62 +2025-10-21 10:07:01.187 1 DEBUG ironic.common.hash_ring [-] Finished rebuilding hash rings, available drivers are :fake-hardware, :idrac, :ilo, :ilo5, :redfish ring /var/lib/openstack/lib/python3.10/site-packages/ironic/common/hash_ring.py:65 +2025-10-21 10:07:01.313 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 2fb79bdb-c925-4701-b304-b3768deeb85e (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:01.316 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 86eb7354-cc10-4173-8ff2-d1ac2ea6befd (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:01.321 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking async import configuration task. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.322 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking async firmware update tasks. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.325 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking async RAID config failed. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.370 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking if async firmware update failed. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.371 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking if async update of firmware component failed. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.371 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking async RAID config tasks. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.372 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking async RAID config failed. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.373 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking async firmware update tasks. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.378 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking if async firmware update failed. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.378 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking async RAID config tasks. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.383 1 DEBUG ironic.conductor.periodics [-] Completed periodic task for purpose checking async update of firmware component. wrapper /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/periodics.py:174 +2025-10-21 10:07:01.384 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node a8a8548c-fc07-4d9c-a5f2-5f2c6fe7992c (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:01.387 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node b6b6dcec-7d48-48c4-89ff-da04b8af40b7 (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:01.390 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 3271d507-9c1b-4440-bd39-0b1a9e779c5b (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:01.393 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 2f75cab3-63d7-45ad-9045-b80f44e86132 (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:01.396 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 49f69ba1-bcce-4b43-aab5-a610a49f29bf (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:01.400 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 3df4d4ef-a65e-4f41-abe8-66169ea51a21 (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:02.852 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Successfully released shared lock for power state sync on node a8a8548c-fc07-4d9c-a5f2-5f2c6fe7992c (lock was held 1.47 sec) release_resources /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:459 +2025-10-21 10:07:02.857 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Successfully released shared lock for power state sync on node b6b6dcec-7d48-48c4-89ff-da04b8af40b7 (lock was held 1.47 sec) release_resources /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:459 +2025-10-21 10:07:02.873 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Successfully released shared lock for power state sync on node 2fb79bdb-c925-4701-b304-b3768deeb85e (lock was held 1.56 sec) release_resources /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:459 +2025-10-21 10:07:02.905 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node f86c82b1-dd24-41de-b32a-aeb3eb0ff020 (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:02.908 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 5180e19d-c3c6-4afb-b626-08d70ec1f456 (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:02.912 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node c5b249c8-e707-4acf-9c36-ad9ce574282f (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:02.939 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Successfully released shared lock for power state sync on node 86eb7354-cc10-4173-8ff2-d1ac2ea6befd (lock was held 1.62 sec) release_resources /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:459 +2025-10-21 10:07:02.940 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Successfully released shared lock for power state sync on node 3271d507-9c1b-4440-bd39-0b1a9e779c5b (lock was held 1.55 sec) release_resources /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:459 +2025-10-21 10:07:02.954 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 055818eb-7de7-43f5-b747-e8704ad7db45 (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:02.961 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 609a8c97-32a2-4308-95ff-e4256706d28f (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:03.021 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Successfully released shared lock for power state sync on node 2f75cab3-63d7-45ad-9045-b80f44e86132 (lock was held 1.63 sec) release_resources /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:459 +2025-10-21 10:07:03.038 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 5c1bfa75-d081-4fbe-9448-417eb54552b7 (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +2025-10-21 10:07:03.218 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Successfully released shared lock for power state sync on node 49f69ba1-bcce-4b43-aab5-a610a49f29bf (lock was held 1.82 sec) release_resources /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:459 +2025-10-21 10:07:03.233 1 DEBUG ironic.conductor.task_manager [None req-f1715a00-bc9f-4443-95d8-e529c376bfad - - - - - -] Attempting to get shared lock on node 461737c4-037c-41bf-9c17-f4f33ff20dd7 (for power state sync) __init__ /var/lib/openstack/lib/python3.10/site-packages/ironic/conductor/task_manager.py:235 +-- +2 diff --git a/python/ironic-understack/ironic_understack/port_bios_name_hook.py b/python/ironic-understack/ironic_understack/port_bios_name_hook.py new file mode 100644 index 000000000..bb0a09525 --- /dev/null +++ b/python/ironic-understack/ironic_understack/port_bios_name_hook.py @@ -0,0 +1,47 @@ +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): + """Hook to set port.extra.bios_name field 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.""" + LOG.debug(f"{__class__} called with {task=!r} {inventory=!r} {plugin_data=!r}") + + 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 + } + + 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.debug( + "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() diff --git a/python/ironic-understack/ironic_understack/tests/test_port_bios_name_hook.py b/python/ironic-understack/ironic_understack/tests/test_port_bios_name_hook.py new file mode 100644 index 000000000..596b6bc3e --- /dev/null +++ b/python/ironic-understack/ironic_understack/tests/test_port_bios_name_hook.py @@ -0,0 +1,62 @@ +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_once() + + +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_once() diff --git a/python/ironic-understack/ironic_understack/tests/test_update_baremetal_port.py b/python/ironic-understack/ironic_understack/tests/test_update_baremetal_port.py new file mode 100644 index 000000000..640dadb09 --- /dev/null +++ b/python/ironic-understack/ironic_understack/tests/test_update_baremetal_port.py @@ -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() diff --git a/python/ironic-understack/ironic_understack/tests/test_vlan_group_name_convention.py b/python/ironic-understack/ironic_understack/tests/test_vlan_group_name_convention.py new file mode 100644 index 000000000..cfd952120 --- /dev/null +++ b/python/ironic-understack/ironic_understack/tests/test_vlan_group_name_convention.py @@ -0,0 +1,143 @@ +import pytest + +from ironic_understack.inspected_port import InspectedPort +from ironic_understack.vlan_group_name_convention import TopologyError +from ironic_understack.vlan_group_name_convention import vlan_group_names + +mapping = { + "1": "network", + "2": "network", + "3": "network", + "4": "network", + "1f": "storage", + "2f": "storage", + "3f": "storage-appliance", + "4f": "storage-appliance", + "1d": "bmc", +} + + +def port(switch: str): + return InspectedPort( + mac_address="", + name="", + switch_system_name=switch, + switch_chassis_id="", + switch_port_id="", + ) + + +def test_vlan_group_name_single_cab(): + assert vlan_group_names( + [ + port("a1-1-1.abc1"), + port("a1-1-2.abc1"), + port("a1-1-1f.abc1"), + port("a1-1-2f.abc1"), + ], + mapping, + ) == { + "a1-1-1.abc1": "a1-1-network", + "a1-1-2.abc1": "a1-1-network", + "a1-1-1f.abc1": "a1-1-storage", + "a1-1-2f.abc1": "a1-1-storage", + } + + +def test_vlan_group_name_pair_cab(): + assert vlan_group_names( + [ + port("a1-1-1.abc1"), + port("a1-2-1.abc1"), + port("a1-1-1f.abc1"), + port("a1-2-1f.abc1"), + ], + mapping, + ) == { + "a1-1-1.abc1": "a1-1/a1-2-network", + "a1-2-1.abc1": "a1-1/a1-2-network", + "a1-1-1f.abc1": "a1-1/a1-2-storage", + "a1-2-1f.abc1": "a1-1/a1-2-storage", + } + + +def test_vlan_group_name_with_domain(): + assert vlan_group_names( + [ + port("a1-1-1.abc1.domain"), + port("a1-1-2.abc1.domain"), + port("a1-1-1f.abc1.domain"), + port("a1-1-2f.abc1.domain"), + ], + mapping, + ) == { + "a1-1-1.abc1.domain": "a1-1-network", + "a1-1-2.abc1.domain": "a1-1-network", + "a1-1-1f.abc1.domain": "a1-1-storage", + "a1-1-2f.abc1.domain": "a1-1-storage", + } + + +def test_vlan_group_name_invalid_format(): + with pytest.raises(ValueError, match="Unknown switch name format"): + vlan_group_names([port("invalid.abc1")], mapping) + + with pytest.raises(ValueError, match="Unknown switch name format"): + vlan_group_names([port(".abc1")], mapping) + + +def test_vlan_group_name_unknown_suffix(): + with pytest.raises(TopologyError, match="suffix a1-1-99.abc1 is not present"): + vlan_group_names([port("a1-1-99.abc1")], mapping) + + with pytest.raises(TopologyError, match="suffix a1-1-5f.abc1 is not present"): + vlan_group_names([port("a1-1-5f.abc1")], mapping) + + with pytest.raises(TopologyError, match="suffix a1-1-xyz.abc1 is not present"): + vlan_group_names([port("a1-1-xyz.abc1")], mapping) + + +def test_vlan_group_name_many_dc(): + with pytest.raises(TopologyError, match="multiple"): + vlan_group_names( + [ + port("a1-1-1.abc1.domain"), + port("a1-1-1.xyz2.domain"), + ], + mapping, + ) + + +def test_vlan_group_name_too_many_racks(): + with pytest.raises(TopologyError, match="more than two racks"): + vlan_group_names( + [ + port("a1-1-1.abc1.domain"), + port("a1-2-1.abc1.domain"), + port("a1-3-1.abc1.domain"), + ], + mapping, + ) + + +def test_vlan_group_name_too_many_switches(): + with pytest.raises(TopologyError, match="exactly two network switches"): + vlan_group_names( + [ + port("a1-1-1.abc1.domain"), + port("a1-1-2.abc1.domain"), + port("a1-1-3.abc1.domain"), + ], + mapping, + ) + + +def test_vlan_group_name_not_enough_switches(): + with pytest.raises(TopologyError, match="exactly two network switches"): + vlan_group_names( + [ + port("a1-1-1.abc1.domain"), + port("a1-1-1.abc1.domain"), + ], + mapping, + ) diff --git a/python/ironic-understack/ironic_understack/update_baremetal_port.py b/python/ironic-understack/ironic_understack/update_baremetal_port.py new file mode 100644 index 000000000..8850c3c25 --- /dev/null +++ b/python/ironic-understack/ironic_understack/update_baremetal_port.py @@ -0,0 +1,206 @@ +from typing import Any + +from ironic import objects +import openstack +from ironic.common import exception +from ironic.drivers.modules.inspector.hooks import base +from oslo_log import log as logging + +import ironic_understack.vlan_group_name_convention +from ironic_understack.conf import CONF +from ironic_understack.inspected_port import InspectedPort +from ironic_understack.ironic_wrapper import ironic_ports_for_node + +LOG = logging.getLogger(__name__) + + +class UpdateBaremetalPortsHook(base.InspectionHook): + """Hook to update ports according to LLDP data.""" + + # "validate-interfaces" provides the all_interfaces field in plugin_data. + # "parse-lldp" provides the parsed_lldp field in plugin_data. + dependencies = ["validate-interfaces", "parse-lldp"] + + def __call__(self, task, inventory, plugin_data): + """Update Ports' local_link_info and physnet based on LLDP data. + + Using the parsed_lldp data as discovered by inspection, validate the + topology and determine the local_link_connection and vlan group names + for each of our connections. + + The ports plugin has already created/deleted the ports as appropriate + and set their "pxe" flag. + + We update attributes of all baremetal ports for this node: + + - local_link_info.port_id (e.g. "Ethernet1/1") + - local_link_info.switch_id (e.g. "aa:bb:cc:dd:ee:ff") + - local_link_info.switch_info (e.g. "a1-1-1.ord1") + - physical_network (e.g. "a1-1-network") + + We also add or remove node "traits" based on the inventory data. We + control the trait "CUSTOM_STORAGE_SWITCH". + """ + LOG.debug(f"{__class__} called with {task=!r} {inventory=!r} {plugin_data=!r}") + + node_uuid: str = task.node.uuid + + inspected_ports = _parse_plugin_data(plugin_data) + if not inspected_ports: + LOG.error("No LLDP data for node %s", node_uuid) + return + + ports_by_mac = {p.mac_address: p for p in inspected_ports} + + vlan_groups = ironic_understack.vlan_group_name_convention.vlan_group_names( + inspected_ports, + CONF.ironic_understack.switch_name_vlan_group_mapping, + ) + LOG.debug( + "Node=%(node)s vlan_groups=%(groups)s", + {"node": node_uuid, "groups": vlan_groups}, + ) + + for baremetal_port in ironic_ports_for_node(task.context, task.node.id): + inspected_port = ports_by_mac.get(baremetal_port.address) + if inspected_port: + LOG.info( + "Port=%(uuid)s Node=%(node)s is connected %(lldp)s", + { + "uuid": baremetal_port.uuid, + "node": node_uuid, + "lldp": inspected_port, + }, + ) + vlan_group = vlan_groups.get(inspected_port.switch_system_name) + if not vlan_group: + LOG.error("Missing VLAN group for %s", inspected_port) + elif vlan_group.endswith("-network"): + _set_port_attributes( + baremetal_port, node_uuid, inspected_port, vlan_group + ) + else: + _clear_port_attributes(baremetal_port, node_uuid) + else: + LOG.info( + "Port=%(uuid)s Node=%(node)s has no LLDP connection", + {"uuid": baremetal_port.uuid, "node": node_uuid}, + ) + _clear_port_attributes(baremetal_port, node_uuid) + + _set_node_traits(task, {x for x in vlan_groups.values() if x}) + + +def _parse_plugin_data(plugin_data: dict) -> list[InspectedPort]: + mac = { + interface["name"]: interface["mac_address"] + for interface in plugin_data["all_interfaces"].values() + } + + return [ + InspectedPort( + mac_address=mac[name], + name=name, + switch_system_name=str(lldp["switch_system_name"]).lower(), + switch_chassis_id=str(lldp["switch_chassis_id"]).lower(), + switch_port_id=str(lldp["switch_port_id"]), + ) + for name, lldp in plugin_data["parsed_lldp"].items() + ] + + +def _set_port_attributes( + port: Any, node_uuid: str, inspected_port: InspectedPort, physical_network: str +): + try: + if port.local_link_connection != inspected_port.local_link_connection: + LOG.debug( + "Updating node %s port %s local_link_connection %s => %s", + node_uuid, + port.uuid, + port.local_link_connection, + inspected_port.local_link_connection, + ) + port.local_link_connection = inspected_port.local_link_connection + + if port.physical_network != physical_network: + LOG.debug( + "Updating node %s port %s physical_network from %s to %s", + node_uuid, + port.id, + port.physical_network, + physical_network, + ) + port.physical_network = physical_network + + port.save() + except exception.IronicException as e: + LOG.warning( + "Failed to update port %(uuid)s for node %(node)s. Error: %(error)s", + {"uuid": port.id, "node": node_uuid, "error": e}, + ) + + +def _clear_port_attributes(port: Any, node_uuid: str): + try: + port.local_link_connection = {} + port.physical_network = None + port.save() + except exception.IronicException as e: + LOG.warning( + "Failed to clear port %(uuid)s for node %(node)s. Error: %(error)s", + {"uuid": port.id, "node": node_uuid, "error": e}, + ) + + +def _set_node_traits(task, vlan_groups: set[str]): + """Add or remove traits to the node. + + We manage a traits for each type of VLAN Group that can be connected to a + node. + + For example, a connection to VLAN Group whose name ends in "-storage" will + result in a trait being added to the node called "CUSTOM_STORAGE_SWITCH". + + We remove pre-existing traits if the node does not have the required + 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) + + traits_to_remove = sorted(existing_traits.difference(required_traits)) + traits_to_add = sorted(required_traits.difference(existing_traits)) + + LOG.debug( + "Checking traits for node %s: existing=%s required=%s", + node.uuid, + existing_traits, + required_traits, + ) + + for trait in traits_to_remove: + LOG.debug("Removing trait %s from node %s", trait, node.uuid) + try: + node.traits.destroy(trait) + except openstack.exceptions.NotFoundException: + pass + + if traits_to_add: + LOG.debug("Adding traits %s to node %s", traits_to_add, node.uuid) + + node.traits = objects.TraitList.create( + task.context, node.id, list(traits_to_add) + ) + + if traits_to_add or traits_to_remove: + node.save() + + +def _trait_name(vlan_group_name: str) -> str: + suffix = vlan_group_name.upper().split("-")[-1] + return f"CUSTOM_{suffix}_SWITCH" diff --git a/python/ironic-understack/ironic_understack/vlan_group_name_convention.py b/python/ironic-understack/ironic_understack/vlan_group_name_convention.py new file mode 100644 index 000000000..cf5802fac --- /dev/null +++ b/python/ironic-understack/ironic_understack/vlan_group_name_convention.py @@ -0,0 +1,99 @@ +from collections.abc import Iterable + +from ironic_understack.inspected_port import InspectedPort + + +class TopologyError(Exception): + pass + + +def vlan_group_names( + ports: list[InspectedPort], mapping: dict[str, str] +) -> dict[str, str | None]: + """The VLAN GROUP name is a function of the switch names. + + Given the set of all connections to a single baremetal node, + + Assert that data_center is the same for all switches. + + Assert that the switches are spread across no more than two racks. + + Assert that there are exactly two connections to each "network" switch. + + Use the supplied mapping to categorise the connected switches. + + If both switches are in the same rack, the vlan_group name looks like this: + + ["a11-12-1", "a11-12-2"] => "a11-12-network" + + If those switches are spread across a pair of racks, the VLAN name has both + racks separated by a slash: + + ["a11-12-1", "a11-13-1"] => "a11-12/a11-13-network" + """ + assert_consistent_data_center(ports) + assert_single_or_paired_racks(ports) + assert_switch_names_have_known_suffixes(ports, mapping) + + vlan_groups = group_by_switch_category(ports, mapping) + + assert_redundant_network_connections(vlan_groups) + + vlan_group_names = {} + for switch_category, ports_in_group in vlan_groups.items(): + rack_names = {p.rack_name for p in ports_in_group} + vlan_group_name = "/".join(sorted(rack_names)) + "-" + switch_category + for p in ports_in_group: + vlan_group_names[p.switch_system_name] = vlan_group_name + return vlan_group_names + + +def assert_consistent_data_center(ports: Iterable[InspectedPort]): + data_centers = {p.data_center_name for p in ports} + if len(data_centers) > 1: + raise TopologyError("Connected to switches in multiple data centers: %s", ports) + + +def assert_single_or_paired_racks(ports: Iterable[InspectedPort]): + network_rack_names = {p.rack_name for p in ports} + if len(network_rack_names) > 2: + raise TopologyError("Connected to switches in more than two racks: %s", ports) + + +def assert_switch_names_have_known_suffixes( + ports: Iterable[InspectedPort], mapping: dict +): + for port in ports: + if port.switch_suffix not in mapping: + raise TopologyError( + f"Switch suffix {port.switch_system_name} is not present " + f"in the mapping configured in " + f"ironic_understack.switch_name_vlan_group_mapping. " + f"Recognised suffixes are: {mapping.keys()}" + ) + + +def assert_redundant_network_connections(vlan_groups: dict[str, list[InspectedPort]]): + network_ports = vlan_groups.get("network", []) + switch_names = {p.switch_system_name for p in network_ports} + if len(switch_names) != 2: + raise TopologyError( + "Expected connections to exactly two network switches, got %s", + network_ports, + ) + + +def group_by_switch_category( + ports: list[InspectedPort], mapping: dict[str, str] +) -> dict[str, list[InspectedPort]]: + groups = {} + + for port in ports: + switch_category = mapping[port.switch_suffix] + + if switch_category not in groups: + groups[switch_category] = [] + + groups[switch_category].append(port) + + return groups diff --git a/python/ironic-understack/pyproject.toml b/python/ironic-understack/pyproject.toml index 9e01289e4..622584bd8 100644 --- a/python/ironic-understack/pyproject.toml +++ b/python/ironic-understack/pyproject.toml @@ -12,12 +12,15 @@ readme = "README.md" license = "MIT" dependencies = [ "ironic>=32.0,<33", + "pytest-mock>=3.15.1", "pyyaml~=6.0", "understack-flavor-matcher", ] [project.entry-points."ironic.inspection.hooks"] resource-class = "ironic_understack.resource_class:ResourceClassHook" +update-baremetal-port = "ironic_understack.update_baremetal_port:UpdateBaremetalPortsHook" +port-bios-name = "ironic_understack.port_bios_name_hook:PortBiosNameHook" [project.entry-points."ironic.hardware.interfaces.inspect"] redfish-understack = "ironic_understack.redfish_inspect_understack:UnderstackRedfishInspect" diff --git a/python/ironic-understack/uv.lock b/python/ironic-understack/uv.lock index 4a2df78ff..b65339fe5 100644 --- a/python/ironic-understack/uv.lock +++ b/python/ironic-understack/uv.lock @@ -454,6 +454,7 @@ version = "0.0.0" source = { editable = "." } dependencies = [ { name = "ironic" }, + { name = "pytest-mock" }, { name = "pyyaml" }, { name = "understack-flavor-matcher" }, ] @@ -468,6 +469,7 @@ test = [ [package.metadata] requires-dist = [ { name = "ironic", specifier = ">=32.0,<33" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "pyyaml", specifier = "~=6.0" }, { name = "understack-flavor-matcher", directory = "../understack-flavor-matcher" }, ] @@ -1308,6 +1310,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/73/7b0b15cb8605ee967b34aa1d949737ab664f94e6b0f1534e8339d9e64ab2/pytest_github_actions_annotate_failures-0.3.0-py3-none-any.whl", hash = "sha256:41ea558ba10c332c0bfc053daeee0c85187507b2034e990f21e4f7e5fef044cf", size = 6030, upload-time = "2025-01-17T22:39:31.701Z" }, ] +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"