Skip to content

Commit 751dd59

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

File tree

10 files changed

+849
-2
lines changed

10 files changed

+849
-2
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: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,82 @@ def test_cable_management_switch_not_found(
248248
assert result == 1
249249

250250

251+
class TestPortNameValidation:
252+
"""Test port name validation functionality."""
253+
254+
def test_should_fix_port_name_missing_prefix(self):
255+
"""Test name validation when prefix is missing."""
256+
from understack_workflows.oslo_event.ironic_port import _should_fix_port_name
257+
258+
assert _should_fix_port_name("eth0", "server-123") is True
259+
260+
def test_should_fix_port_name_correct_prefix(self):
261+
"""Test name validation when prefix is correct."""
262+
from understack_workflows.oslo_event.ironic_port import _should_fix_port_name
263+
264+
assert _should_fix_port_name("server-123:eth0", "server-123") is False
265+
266+
def test_should_fix_port_name_empty(self):
267+
"""Test name validation when name is empty."""
268+
from understack_workflows.oslo_event.ironic_port import _should_fix_port_name
269+
270+
assert _should_fix_port_name("", "server-123") is False
271+
272+
def test_get_node_name_success(self):
273+
"""Test successful node name retrieval."""
274+
from understack_workflows.oslo_event.ironic_port import _get_node_name
275+
276+
mock_conn = Mock()
277+
mock_node = Mock()
278+
mock_node.name = "server-123"
279+
mock_conn.baremetal.get_node.return_value = mock_node
280+
281+
result = _get_node_name(mock_conn, "node-uuid")
282+
283+
assert result == "server-123"
284+
mock_conn.baremetal.get_node.assert_called_once_with("node-uuid")
285+
286+
def test_get_node_name_not_found(self):
287+
"""Test node name retrieval when node not found."""
288+
from understack_workflows.oslo_event.ironic_port import _get_node_name
289+
290+
mock_conn = Mock()
291+
mock_conn.baremetal.get_node.return_value = None
292+
293+
result = _get_node_name(mock_conn, "node-uuid")
294+
295+
assert result is None
296+
297+
def test_fix_port_name_if_needed(self):
298+
"""Test port name fixing."""
299+
from understack_workflows.oslo_event.ironic_port import IronicPortEvent
300+
from understack_workflows.oslo_event.ironic_port import _fix_port_name_if_needed
301+
302+
mock_conn = Mock()
303+
mock_node = Mock()
304+
mock_node.name = "server-123"
305+
mock_conn.baremetal.get_node.return_value = mock_node
306+
307+
event = IronicPortEvent(
308+
uuid="test-uuid",
309+
name="eth0", # Missing prefix
310+
address="aa:bb:cc:dd:ee:ff",
311+
node_uuid="node-uuid",
312+
physical_network="test-network",
313+
pxe_enabled=True,
314+
remote_port_id=None,
315+
remote_switch_info=None,
316+
remote_switch_id=None,
317+
)
318+
319+
_fix_port_name_if_needed(mock_conn, event)
320+
321+
# Verify name was fixed
322+
mock_conn.baremetal.update_port.assert_called_once_with(
323+
"test-uuid", name="server-123:eth0"
324+
)
325+
326+
251327
class TestHandlePortCreateUpdate:
252328
"""Test handle_port_create_update function."""
253329

@@ -266,7 +342,12 @@ def mock_nautobot(self):
266342
@pytest.fixture
267343
def mock_conn(self):
268344
"""Create mock connection."""
269-
return Mock()
345+
conn = Mock()
346+
# Mock node lookup for name validation
347+
mock_node = Mock()
348+
mock_node.name = "1327172-hp1"
349+
conn.baremetal.get_node.return_value = mock_node
350+
return conn
270351

271352
def test_handle_port_create_update_with_remote_info(
272353
self, mock_conn, mock_nautobot, port_create_event_data

0 commit comments

Comments
 (0)