Skip to content

Commit a2ecff9

Browse files
validate and fix port and portgroup names in Ironic to ensure they are prefixed with the node name.
1 parent 016f524 commit a2ecff9

File tree

4 files changed

+219
-1
lines changed

4 files changed

+219
-1
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
apiVersion: argoproj.io/v1alpha1
3+
kind: Sensor
4+
metadata:
5+
name: ironic-node-portgroup
6+
annotations:
7+
workflows.argoproj.io/title: Validate Portgroup Names from Ironic Events
8+
workflows.argoproj.io/description: |+
9+
Triggers on the following Ironic Events:
10+
11+
- baremetal.portgroup.create.end which happens when a baremetal portgroup is created
12+
13+
This sensor validates that portgroup names are prefixed with the node name.
14+
If not, it automatically updates the portgroup name to include the prefix.
15+
16+
Resulting code should be very similar to:
17+
18+
```
19+
argo -n argo-events submit --from workflowtemplate/openstack-oslo-event \
20+
-p event-json "JSON-payload"
21+
```
22+
23+
Defined in `workflows/argo-events/sensors/ironic-node-portgroup.yaml`
24+
spec:
25+
dependencies:
26+
- eventName: openstack
27+
eventSourceName: openstack-ironic
28+
name: ironic-dep
29+
transform:
30+
# the event is a string-ified JSON so we need to decode it
31+
# replace the whole event body
32+
jq: |
33+
.body = (.body["oslo.message"] | fromjson)
34+
filters:
35+
# applies each of the items in data with 'and' but there's only one
36+
dataLogicalOperator: "and"
37+
data:
38+
- path: "body.event_type"
39+
type: "string"
40+
value:
41+
- "baremetal.portgroup.create.end"
42+
template:
43+
serviceAccountName: sensor-submit-workflow
44+
triggers:
45+
- template:
46+
name: ironic-node-portgroup
47+
# creates workflow object directly via k8s API
48+
k8s:
49+
operation: create
50+
parameters:
51+
# first parameter is the parsed oslo.message
52+
- dest: spec.arguments.parameters.0.value
53+
src:
54+
dataKey: body
55+
dependencyName: ironic-dep
56+
source:
57+
# create a workflow in argo-events prefixed with ironic-node-portgroup-
58+
resource:
59+
apiVersion: argoproj.io/v1alpha1
60+
kind: Workflow
61+
metadata:
62+
generateName: ironic-node-portgroup-
63+
namespace: argo-events
64+
spec:
65+
# defines the parameters being replaced above
66+
arguments:
67+
parameters:
68+
- name: event-json
69+
# references the workflow
70+
workflowTemplateRef:
71+
name: openstack-oslo-event

python/understack-workflows/understack_workflows/main/openstack_oslo_event.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from understack_workflows.openstack.client import get_openstack_client
1616
from understack_workflows.oslo_event import ironic_node
1717
from understack_workflows.oslo_event import ironic_port
18+
from understack_workflows.oslo_event import ironic_portgroup
1819
from understack_workflows.oslo_event import keystone_project
1920
from understack_workflows.oslo_event import neutron_network
2021
from understack_workflows.oslo_event import neutron_subnet
@@ -67,6 +68,7 @@ class NoEventHandlerError(Exception):
6768
"baremetal.port.create.end": ironic_port.handle_port_create_update,
6869
"baremetal.port.update.end": ironic_port.handle_port_create_update,
6970
"baremetal.port.delete.end": ironic_port.handle_port_delete,
71+
"baremetal.portgroup.create.end": ironic_portgroup.handle_portgroup_create,
7072
"baremetal.node.provision_set.end": ironic_node.handle_provision_end,
7173
"identity.project.created": keystone_project.handle_project_created,
7274
"identity.project.updated": keystone_project.handle_project_updated,

python/understack-workflows/understack_workflows/oslo_event/ironic_port.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,64 @@ def from_event_dict(cls, data: dict) -> IronicPortEvent:
5959
)
6060

6161

62+
def _get_node_name(conn: Connection, node_uuid: str) -> str | None:
63+
"""Get the node name from Ironic by UUID."""
64+
try:
65+
node = conn.baremetal.get_node(node_uuid) # pyright: ignore
66+
return node.name if node else None
67+
except Exception:
68+
logger.exception("Failed to get node %s from Ironic", node_uuid)
69+
return None
70+
71+
72+
def _should_fix_port_name(name: str, node_name: str) -> bool:
73+
"""Check if port name needs to be prefixed with node name."""
74+
if not name:
75+
return False
76+
return not name.startswith(f"{node_name}-")
77+
78+
79+
def _fix_port_name_if_needed(conn: Connection, event: IronicPortEvent) -> None:
80+
"""Fix port name by prefixing with node name if needed."""
81+
if not event.name:
82+
logger.debug("Port %s has no name, skipping name validation", event.uuid)
83+
return
84+
85+
# Get the node name
86+
node_name = _get_node_name(conn, event.node_uuid)
87+
if not node_name:
88+
logger.error("Could not get node name for node %s", event.node_uuid)
89+
return
90+
91+
# Check if name needs fixing
92+
if not _should_fix_port_name(event.name, node_name):
93+
logger.debug(
94+
"Port %s name '%s' already has correct prefix", event.uuid, event.name
95+
)
96+
return
97+
98+
# Fix the name by prefixing with node name
99+
new_name = f"{node_name}-{event.name}"
100+
logger.info(
101+
"Updating port %s name from '%s' to '%s'", event.uuid, event.name, new_name
102+
)
103+
104+
try:
105+
conn.baremetal.update_port(event.uuid, name=new_name) # pyright: ignore
106+
logger.info("Successfully updated port %s name", event.uuid)
107+
except Exception:
108+
logger.exception("Failed to update port %s name", event.uuid)
109+
110+
62111
def handle_port_create_update(
63-
_conn: Connection, nautobot: Nautobot, event_data: dict
112+
conn: Connection, nautobot: Nautobot, event_data: dict
64113
) -> int:
65114
"""Operates on an Ironic Port create and update event."""
66115
event = IronicPortEvent.from_event_dict(event_data)
67116

117+
# Check and fix port name if needed
118+
_fix_port_name_if_needed(conn, event)
119+
68120
logger.debug("looking up interface in nautobot by UUID: %s", event.uuid)
69121
intf = nautobot.dcim.interfaces.get(id=event.uuid)
70122
if not intf:
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from dataclasses import dataclass
5+
6+
from openstack.connection import Connection
7+
from pynautobot.core.api import Api as Nautobot
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
@dataclass
13+
class IronicPortgroupEvent:
14+
uuid: str
15+
name: str | None
16+
node_uuid: str
17+
address: str | None
18+
mode: str | None
19+
20+
@classmethod
21+
def from_event_dict(cls, data: dict) -> IronicPortgroupEvent:
22+
payload = data.get("payload")
23+
if payload is None:
24+
raise Exception("Invalid event. No 'payload'")
25+
26+
# Extract the actual data from the nested ironic object structure
27+
payload_data = payload.get("ironic_object.data")
28+
if payload_data is None:
29+
raise Exception("Invalid event. No 'ironic_object.data' in payload")
30+
31+
return IronicPortgroupEvent(
32+
payload_data["uuid"],
33+
payload_data.get("name"),
34+
payload_data["node_uuid"],
35+
payload_data.get("address"),
36+
payload_data.get("mode"),
37+
)
38+
39+
40+
def _get_node_name(conn: Connection, node_uuid: str) -> str | None:
41+
"""Get the node name from Ironic by UUID."""
42+
try:
43+
node = conn.baremetal.get_node(node_uuid) # pyright: ignore
44+
return node.name if node else None
45+
except Exception:
46+
logger.exception("Failed to get node %s from Ironic", node_uuid)
47+
return None
48+
49+
50+
def _should_fix_portgroup_name(name: str | None, node_name: str) -> bool:
51+
"""Check if portgroup name needs to be prefixed with node name."""
52+
if not name:
53+
return False
54+
return not name.startswith(f"{node_name}-")
55+
56+
57+
def handle_portgroup_create(
58+
conn: Connection, _nautobot: Nautobot, event_data: dict
59+
) -> int:
60+
"""Handle Ironic Portgroup create event and fix name if needed."""
61+
event = IronicPortgroupEvent.from_event_dict(event_data)
62+
63+
logger.info("Processing portgroup create event for %s", event.uuid)
64+
65+
# Get the node name
66+
node_name = _get_node_name(conn, event.node_uuid)
67+
if not node_name:
68+
logger.error("Could not get node name for node %s", event.node_uuid)
69+
return 1
70+
71+
# Check if name needs fixing
72+
if not _should_fix_portgroup_name(event.name, node_name):
73+
logger.info(
74+
"Portgroup %s name '%s' already has correct prefix", event.uuid, event.name
75+
)
76+
return 0
77+
78+
# Fix the name by prefixing with node name
79+
new_name = f"{node_name}-{event.name}"
80+
logger.info(
81+
"Updating portgroup %s name from '%s' to '%s'",
82+
event.uuid,
83+
event.name,
84+
new_name,
85+
)
86+
87+
try:
88+
conn.baremetal.update_port_group(event.uuid, name=new_name) # pyright: ignore
89+
logger.info("Successfully updated portgroup %s name", event.uuid)
90+
return 0
91+
except Exception:
92+
logger.exception("Failed to update portgroup %s name", event.uuid)
93+
return 1

0 commit comments

Comments
 (0)