Skip to content

Commit 85e1fef

Browse files
sync Ironic portgroups as LAG interfaces to Nautobot via Oslo event handlers
1 parent 21756a6 commit 85e1fef

File tree

10 files changed

+695
-43
lines changed

10 files changed

+695
-43
lines changed

components/site-workflows/kustomization.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ resources:
2222
- sensors/sensor-neutron-olso-event.yaml
2323
- sensors/sensor-ironic-reclean.yaml
2424
- sensors/sensor-ironic-node-port.yaml
25+
- sensors/sensor-ironic-node-portgroup.yaml
2526
- sensors/sensor-ironic-oslo-event.yaml
2627

2728
helmCharts:
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
apiVersion: argoproj.io/v1alpha1
3+
kind: Sensor
4+
metadata:
5+
name: ironic-node-portgroup
6+
annotations:
7+
workflows.argoproj.io/title: Sync Portgroups to Nautobot LAGs
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+
- baremetal.portgroup.update.end which happens when a portgroup is updated
13+
- baremetal.portgroup.delete.end which happens when a portgroup is deleted
14+
15+
This sensor:
16+
1. Validates that portgroup names are prefixed with the node name
17+
2. Creates/updates LAGs (Link Aggregation Groups) in Nautobot
18+
3. Strips the node name prefix when creating LAG names in Nautobot
19+
20+
Resulting code should be very similar to:
21+
22+
```
23+
argo -n argo-events submit --from workflowtemplate/openstack-oslo-event \
24+
-p event-json "JSON-payload"
25+
```
26+
27+
Defined in `workflows/argo-events/sensors/ironic-node-portgroup.yaml`
28+
spec:
29+
dependencies:
30+
- eventName: openstack
31+
eventSourceName: openstack-ironic
32+
name: ironic-dep
33+
transform:
34+
# the event is a string-ified JSON so we need to decode it
35+
# replace the whole event body
36+
jq: |
37+
.body = (.body["oslo.message"] | fromjson)
38+
filters:
39+
# applies each of the items in data with 'and' but there's only one
40+
dataLogicalOperator: "and"
41+
data:
42+
- path: "body.event_type"
43+
type: "string"
44+
value:
45+
- "baremetal.portgroup.create.end"
46+
- "baremetal.portgroup.update.end"
47+
- "baremetal.portgroup.delete.end"
48+
template:
49+
serviceAccountName: sensor-submit-workflow
50+
triggers:
51+
- template:
52+
name: ironic-node-portgroup
53+
# creates workflow object directly via k8s API
54+
k8s:
55+
operation: create
56+
parameters:
57+
# first parameter is the parsed oslo.message
58+
- dest: spec.arguments.parameters.0.value
59+
src:
60+
dataKey: body
61+
dependencyName: ironic-dep
62+
source:
63+
# create a workflow in argo-events prefixed with ironic-node-portgroup-
64+
resource:
65+
apiVersion: argoproj.io/v1alpha1
66+
kind: Workflow
67+
metadata:
68+
generateName: ironic-node-portgroup-
69+
namespace: argo-events
70+
spec:
71+
# defines the parameters being replaced above
72+
arguments:
73+
parameters:
74+
- name: event-json
75+
# references the workflow
76+
workflowTemplateRef:
77+
name: openstack-oslo-event
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"oslo.version": "2.0", "oslo.message": "{\"message_id\": \"e1d67320-b0ee-4931-8898-c0d50b30da5d\", \"publisher_id\": \"ironic-api.ironic-api-df96c5d6f-c5qc9\", \"event_type\": \"baremetal.portgroup.create.end\", \"priority\": \"INFO\", \"payload\": {\"ironic_object.name\": \"PortgroupCRUDPayload\", \"ironic_object.namespace\": \"ironic\", \"ironic_object.version\": \"1.0\", \"ironic_object.data\": {\"address\": \"52:54:00:aa:bb:cc\", \"extra\": {}, \"mode\": \"active-backup\", \"name\": \"bond0\", \"node_uuid\": \"7ca98881-bca5-4c82-9369-66eb36292a95\", \"properties\": {}, \"standalone_ports_supported\": true, \"created_at\": \"2025-05-06T15:24:51Z\", \"updated_at\": null, \"uuid\": \"629b8821-6c0a-4a6f-9312-109fe8a0931f\"}}, \"timestamp\": \"2025-05-06 15:24:51.499233\", \"_unique_id\": \"8b1280e345594bbb9dc4b57b85276431\"}"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"oslo.version": "2.0", "oslo.message": "{\"message_id\": \"a3b89541-d2aa-6b53-0b00-e2f72d52fc7f\", \"publisher_id\": \"ironic-api.ironic-api-df96c5d6f-c5qc9\", \"event_type\": \"baremetal.portgroup.delete.end\", \"priority\": \"INFO\", \"payload\": {\"ironic_object.name\": \"PortgroupCRUDPayload\", \"ironic_object.namespace\": \"ironic\", \"ironic_object.version\": \"1.0\", \"ironic_object.data\": {\"address\": \"52:54:00:aa:bb:cc\", \"extra\": {}, \"mode\": \"802.3ad\", \"name\": \"server-123_bond0\", \"node_uuid\": \"7ca98881-bca5-4c82-9369-66eb36292a95\", \"properties\": {}, \"standalone_ports_supported\": true, \"created_at\": \"2025-05-06T15:24:51Z\", \"updated_at\": \"2025-05-06T16:30:00Z\", \"uuid\": \"629b8821-6c0a-4a6f-9312-109fe8a0931f\"}}, \"timestamp\": \"2025-05-06 17:00:00.789012\", \"_unique_id\": \"0d3402g567716ddd1fe6d79d07498653\"}"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"oslo.version": "2.0", "oslo.message": "{\"message_id\": \"f2e78430-c1ff-5a42-9a99-d1e61c41eb6e\", \"publisher_id\": \"ironic-api.ironic-api-df96c5d6f-c5qc9\", \"event_type\": \"baremetal.portgroup.update.end\", \"priority\": \"INFO\", \"payload\": {\"ironic_object.name\": \"PortgroupCRUDPayload\", \"ironic_object.namespace\": \"ironic\", \"ironic_object.version\": \"1.0\", \"ironic_object.data\": {\"address\": \"52:54:00:aa:bb:cc\", \"extra\": {}, \"mode\": \"802.3ad\", \"name\": \"server-123_bond0\", \"node_uuid\": \"7ca98881-bca5-4c82-9369-66eb36292a95\", \"properties\": {}, \"standalone_ports_supported\": true, \"created_at\": \"2025-05-06T15:24:51Z\", \"updated_at\": \"2025-05-06T16:30:00Z\", \"uuid\": \"629b8821-6c0a-4a6f-9312-109fe8a0931f\"}}, \"timestamp\": \"2025-05-06 16:30:00.123456\", \"_unique_id\": \"9c2391f456605ccc0ed5c68c96387542\"}"}

python/understack-workflows/tests/test_oslo_event_ironic_port.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ def test_from_event_dict_create(self, port_create_event_data):
5050

5151
assert event.uuid == "63a3c79c-dd84-4569-a398-cc795287300f"
5252
assert event.name == "1327172-hp1:NIC2-1"
53-
assert event.interface_name == "NIC2-1"
53+
# interface_name now returns the port name directly (MAC address)
54+
assert event.interface_name == "1327172-hp1:NIC2-1"
5455
assert event.address == "00:11:0a:69:a9:99"
5556
assert event.node_uuid == "7ca98881-bca5-4c82-9369-66eb36292a95"
5657
assert event.physical_network == "f20-1-network"
@@ -65,7 +66,8 @@ def test_from_event_dict_update(self, port_update_event_data):
6566

6667
assert event.uuid == "438711ba-1bcd-4f19-8b34-53cdc6d61bc4"
6768
assert event.name == "1327172-hp1:NIC1-1"
68-
assert event.interface_name == "NIC1-1"
69+
# interface_name now returns the port name directly (MAC address)
70+
assert event.interface_name == "1327172-hp1:NIC1-1"
6971
assert event.address == "00:11:0a:6a:c7:05"
7072
assert event.node_uuid == "7ca98881-bca5-4c82-9369-66eb36292a95"
7173
assert event.remote_port_id == "Ethernet1/1"
@@ -78,33 +80,37 @@ def test_from_event_dict_delete(self, port_delete_event_data):
7880

7981
assert event.uuid == "f8888f0b-1451-432e-9ae7-4b77303dd9ef"
8082
assert event.name == "f8888f0b-1451-432e-9ae7-4b77303dd9ef:NIC.Integrated.1-2"
81-
assert event.interface_name == "NIC.Integrated.1-2"
83+
# interface_name now returns the port name directly (MAC address)
84+
assert (
85+
event.interface_name
86+
== "f8888f0b-1451-432e-9ae7-4b77303dd9ef:NIC.Integrated.1-2"
87+
)
8288
assert event.address == "d4:04:e6:4f:64:5d"
8389
assert event.node_uuid == "74feccaf-3aae-401c-bc1f-eeeb26b9f542"
8490
assert event.remote_port_id == "Ethernet1/14"
8591
assert event.remote_switch_info == "f20-5-1f.iad3.rackspace.net"
8692
assert event.remote_switch_id == "f4:ee:31:c0:8c:b3"
8793

88-
def test_interface_name_parsing(self):
89-
"""Test interface name parsing from event name."""
94+
def test_interface_name_with_mac_address(self):
95+
"""Test interface name returns MAC address when set."""
9096
event = IronicPortEvent(
9197
uuid="test-uuid",
92-
name="1327172-hp1:NIC2-1",
93-
address="00:11:22:33:44:55",
98+
name="00110a69a999", # MAC address (normalized)
99+
address="00:11:0a:69:a9:99",
94100
node_uuid="node-uuid",
95101
physical_network="test-network",
96102
pxe_enabled=True,
97103
remote_port_id="Ethernet1/1",
98104
remote_switch_info="switch1.example.com",
99105
remote_switch_id="aa:bb:cc:dd:ee:ff",
100106
)
101-
assert event.interface_name == "NIC2-1"
107+
assert event.interface_name == "00110a69a999"
102108

103-
def test_interface_name_fallback(self):
104-
"""Test interface name fallback to UUID when parsing fails."""
109+
def test_interface_name_fallback_to_uuid(self):
110+
"""Test interface name falls back to UUID when name is empty."""
105111
event = IronicPortEvent(
106-
uuid="test-uuid",
107-
name="no-colon-name",
112+
uuid="test-uuid-123",
113+
name="", # Empty name
108114
address="00:11:22:33:44:55",
109115
node_uuid="node-uuid",
110116
physical_network="test-network",
@@ -113,7 +119,7 @@ def test_interface_name_fallback(self):
113119
remote_switch_info="switch1.example.com",
114120
remote_switch_id="aa:bb:cc:dd:ee:ff",
115121
)
116-
assert event.interface_name == "test-uuid"
122+
assert event.interface_name == "test-uuid-123"
117123

118124

119125
class TestCableManagement:

0 commit comments

Comments
 (0)