Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/site-workflows/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ resources:
- sensors/sensor-neutron-olso-event.yaml
- sensors/sensor-ironic-reclean.yaml
- sensors/sensor-ironic-node-port.yaml
- sensors/sensor-ironic-node-portgroup.yaml
- sensors/sensor-ironic-oslo-event.yaml

helmCharts:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
apiVersion: argoproj.io/v1alpha1
kind: Sensor
metadata:
name: ironic-node-portgroup
annotations:
workflows.argoproj.io/title: Sync Portgroups to Nautobot LAGs
workflows.argoproj.io/description: |+
Triggers on the following Ironic Events:

- baremetal.portgroup.create.end which happens when a baremetal portgroup is created
- baremetal.portgroup.update.end which happens when a portgroup is updated
- baremetal.portgroup.delete.end which happens when a portgroup is deleted

This sensor:
1. Validates that portgroup names are prefixed with the node name
2. Creates/updates LAGs (Link Aggregation Groups) in Nautobot
3. Strips the node name prefix when creating LAG names in Nautobot

Resulting code should be very similar to:

```
argo -n argo-events submit --from workflowtemplate/openstack-oslo-event \
-p event-json "JSON-payload"
```

Defined in `workflows/argo-events/sensors/ironic-node-portgroup.yaml`
spec:
dependencies:
- eventName: openstack
eventSourceName: openstack-ironic
name: ironic-dep
transform:
# the event is a string-ified JSON so we need to decode it
# replace the whole event body
jq: |
.body = (.body["oslo.message"] | fromjson)
filters:
# applies each of the items in data with 'and' but there's only one
dataLogicalOperator: "and"
data:
- path: "body.event_type"
type: "string"
value:
- "baremetal.portgroup.create.end"
- "baremetal.portgroup.update.end"
- "baremetal.portgroup.delete.end"
template:
serviceAccountName: sensor-submit-workflow
triggers:
- template:
name: ironic-node-portgroup
# creates workflow object directly via k8s API
k8s:
operation: create
parameters:
# first parameter is the parsed oslo.message
- dest: spec.arguments.parameters.0.value
src:
dataKey: body
dependencyName: ironic-dep
source:
# create a workflow in argo-events prefixed with ironic-node-portgroup-
resource:
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: ironic-node-portgroup-
namespace: argo-events
spec:
# defines the parameters being replaced above
arguments:
parameters:
- name: event-json
# references the workflow
workflowTemplateRef:
name: openstack-oslo-event
Original file line number Diff line number Diff line change
@@ -0,0 +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\"}"}
Original file line number Diff line number Diff line change
@@ -0,0 +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\"}"}
Original file line number Diff line number Diff line change
@@ -0,0 +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\"}"}
32 changes: 19 additions & 13 deletions python/understack-workflows/tests/test_oslo_event_ironic_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def test_from_event_dict_create(self, port_create_event_data):

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

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

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

def test_interface_name_parsing(self):
"""Test interface name parsing from event name."""
def test_interface_name_with_mac_address(self):
"""Test interface name returns MAC address when set."""
event = IronicPortEvent(
uuid="test-uuid",
name="1327172-hp1:NIC2-1",
address="00:11:22:33:44:55",
name="00110a69a999", # MAC address (normalized)
address="00:11:0a:69:a9:99",
node_uuid="node-uuid",
physical_network="test-network",
pxe_enabled=True,
remote_port_id="Ethernet1/1",
remote_switch_info="switch1.example.com",
remote_switch_id="aa:bb:cc:dd:ee:ff",
)
assert event.interface_name == "NIC2-1"
assert event.interface_name == "00110a69a999"

def test_interface_name_fallback(self):
"""Test interface name fallback to UUID when parsing fails."""
def test_interface_name_fallback_to_uuid(self):
"""Test interface name falls back to UUID when name is empty."""
event = IronicPortEvent(
uuid="test-uuid",
name="no-colon-name",
uuid="test-uuid-123",
name="", # Empty name
address="00:11:22:33:44:55",
node_uuid="node-uuid",
physical_network="test-network",
Expand All @@ -113,7 +119,7 @@ def test_interface_name_fallback(self):
remote_switch_info="switch1.example.com",
remote_switch_id="aa:bb:cc:dd:ee:ff",
)
assert event.interface_name == "test-uuid"
assert event.interface_name == "test-uuid-123"


class TestCableManagement:
Expand Down
Loading
Loading