Skip to content

Commit 2ec6720

Browse files
adding nautobot-sync post-inspection hook
1 parent 154def1 commit 2ec6720

File tree

5 files changed

+590
-2
lines changed

5 files changed

+590
-2
lines changed

components/ironic/values.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,16 @@ conf:
8686
rabbit_ha_queues: true
8787
pxe:
8888
loader_file_paths: "snponly.efi:/usr/lib/ipxe/snponly.efi"
89+
redfish:
90+
# Redfish inspection hooks - run hooks for redfish-based inspection
91+
inspection_hooks: "$default_inspection_hooks,nautobot-sync"
8992
inspector:
9093
extra_kernel_params: ipa-collect-lldp=1
9194
# Agent inspection hooks - ports hook removed to prevent port manipulation during agent inspection
9295
# Default hooks include: ramdisk-error,validate-interfaces,ports,architecture
9396
# We override to exclude 'ports' from the default hooks
9497
default_hooks: "ramdisk-error,validate-interfaces,architecture"
95-
hooks: "$default_hooks,pci-devices,parse-lldp,local-link-connection,resource-class"
98+
hooks: "$default_hooks,pci-devices,parse-lldp,local-link-connection,resource-class,nautobot-sync"
9699
# enable sensors and metrics for redfish metrics - https://docs.openstack.org/ironic/latest/admin/drivers/redfish/metrics.html
97100
sensor_data:
98101
send_sensor_data: true
@@ -239,6 +242,9 @@ pod:
239242
sources:
240243
- secret:
241244
name: ironic-ks-etc
245+
- secret:
246+
name: ironic-nautobot-token
247+
optional: true
242248
ironic_api:
243249
ironic_api:
244250
volumeMounts:

python/ironic-understack/ironic_understack/conf.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,18 @@ def setup_conf():
1010
"device_types_dir",
1111
help="directory storing Device Type description YAML files",
1212
default="/var/lib/understack/device-types",
13-
)
13+
),
14+
cfg.StrOpt(
15+
"nautobot_url",
16+
help="Nautobot API URL",
17+
default=None,
18+
),
19+
cfg.StrOpt(
20+
"nautobot_token",
21+
help="Nautobot API token",
22+
secret=True,
23+
default=None,
24+
),
1425
]
1526
cfg.CONF.register_group(grp)
1627
cfg.CONF.register_opts(opts, group=grp)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Ironic inspection hook to sync device information to Nautobot."""
2+
3+
import pynautobot
4+
from ironic import objects
5+
from ironic.drivers.modules.inspector.hooks import base
6+
from oslo_log import log as logging
7+
8+
from ironic_understack.conf import CONF
9+
10+
LOG = logging.getLogger(__name__)
11+
12+
13+
class NautobotSyncHook(base.InspectionHook):
14+
"""Hook to sync discovered device information to Nautobot."""
15+
16+
# Run after port information has been enriched with BIOS names and LLDP data
17+
dependencies = ["update-baremetal-port", "port-bios-name"]
18+
19+
def __call__(self, task, inventory, plugin_data):
20+
"""Sync device inventory to Nautobot.
21+
22+
:param task: Ironic task context containing node and driver info
23+
:param inventory: Hardware inventory dict from inspection
24+
:param plugin_data: Shared data dict between hooks
25+
"""
26+
try:
27+
nautobot_url = CONF.ironic_understack.nautobot_url
28+
nautobot_token = CONF.ironic_understack.nautobot_token
29+
30+
if not nautobot_url or not nautobot_token:
31+
LOG.warning(
32+
"Nautobot URL or token not configured, skipping sync for node %s",
33+
task.node.uuid,
34+
)
35+
return
36+
37+
# Initialize Nautobot client
38+
nautobot = pynautobot.api(url=nautobot_url, token=nautobot_token)
39+
40+
# Extract device information from inventory
41+
device_data = self._extract_device_data(task, inventory)
42+
43+
# Sync to Nautobot
44+
self._sync_to_nautobot(nautobot, device_data, task.node)
45+
46+
LOG.info(
47+
"Successfully synced device information to Nautobot for node %s",
48+
task.node.uuid,
49+
)
50+
51+
except (KeyError, ValueError, TypeError) as e:
52+
msg = (
53+
f"Failed to extract device information from inventory for node "
54+
f"{task.node.uuid}: {e}"
55+
)
56+
LOG.error(msg)
57+
# Don't fail inspection, just log the error
58+
except Exception as e:
59+
msg = f"Failed to sync device to Nautobot for node {task.node.uuid}: {e}"
60+
LOG.error(msg)
61+
# Don't fail inspection, just log the error
62+
63+
def _extract_device_data(self, task, inventory):
64+
"""Extract relevant device data from inventory and baremetal ports."""
65+
data = {
66+
"serial": inventory.get("system_vendor", {}).get("serial_number"),
67+
"manufacturer": inventory.get("system_vendor", {}).get("manufacturer"),
68+
"model": inventory.get("system_vendor", {}).get("product_name"),
69+
"uuid": task.node.uuid,
70+
"name": task.node.name or task.node.uuid,
71+
}
72+
73+
# Extract interface information from baremetal ports
74+
# These ports have been enriched by
75+
# update-baremetal-port and port-bios-name hooks
76+
interfaces = []
77+
try:
78+
ports = objects.Port.list_by_node_id(task.context, task.node.id)
79+
for port in ports:
80+
interface_data = {
81+
"mac_address": port.address,
82+
"name": port.name,
83+
"bios_name": port.extra.get("bios_name"),
84+
"pxe_enabled": port.pxe_enabled,
85+
}
86+
87+
# local_link_connection info from update-baremetal-port hook
88+
if port.local_link_connection:
89+
interface_data["switch_id"] = port.local_link_connection.get(
90+
"switch_id"
91+
)
92+
interface_data["switch_info"] = port.local_link_connection.get(
93+
"switch_info"
94+
)
95+
interface_data["port_id"] = port.local_link_connection.get(
96+
"port_id"
97+
)
98+
99+
# Add physical_network (VLAN group) if available
100+
if port.physical_network:
101+
interface_data["physical_network"] = port.physical_network
102+
103+
interfaces.append(interface_data)
104+
105+
LOG.debug(
106+
"Extracted %d interfaces for node %s", len(interfaces), task.node.uuid
107+
)
108+
except Exception as e:
109+
LOG.warning(
110+
"Failed to extract interface data from ports for node %s: %s",
111+
task.node.uuid,
112+
e,
113+
)
114+
115+
data["interfaces"] = interfaces
116+
117+
return data
118+
119+
def _sync_to_nautobot(self, nautobot, device_data, node):
120+
"""Sync device data to Nautobot."""
121+
serial = device_data.get("serial")
122+
if not serial:
123+
LOG.warning("Node %s, cannot sync to Nautobot", node.uuid)
124+
return
125+
126+
# Check if device exists in Nautobot
127+
device = self._find_device(nautobot, serial)
128+
129+
if device:
130+
LOG.info("Device %s already exists in Nautobot", serial)
131+
# Update device if needed
132+
self._update_device(nautobot, device, device_data)
133+
else:
134+
LOG.info("Device %s not found in Nautobot, would create", serial)
135+
# Note: Creation requires location/rack info
136+
# which we don't have from inspection
137+
# This would need to be configured or derived from other sources
138+
139+
def _find_device(self, nautobot, serial):
140+
"""Find device in Nautobot by serial number."""
141+
try:
142+
devices = nautobot.dcim.devices.filter(serial=serial)
143+
if devices:
144+
return devices[0]
145+
except Exception:
146+
LOG.exception("Error querying Nautobot for device with serial %s", serial)
147+
return None
148+
149+
def _update_device(self, nautobot, device, device_data):
150+
"""Update device information in Nautobot."""
151+
# Update basic device info if needed
152+
# This is a placeholder - actual update logic would depend on requirements
153+
LOG.debug("Would update device %s with data: %s", device.id, device_data)

0 commit comments

Comments
 (0)