Skip to content
Open
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\"}"}
83 changes: 82 additions & 1 deletion python/understack-workflows/tests/test_oslo_event_ironic_port.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,82 @@ def test_cable_management_switch_not_found(
assert result == 1


class TestPortNameValidation:
"""Test port name validation functionality."""

def test_should_fix_port_name_missing_prefix(self):
"""Test name validation when prefix is missing."""
from understack_workflows.oslo_event.ironic_port import _should_fix_port_name

assert _should_fix_port_name("eth0", "server-123") is True

def test_should_fix_port_name_correct_prefix(self):
"""Test name validation when prefix is correct."""
from understack_workflows.oslo_event.ironic_port import _should_fix_port_name

assert _should_fix_port_name("server-123:eth0", "server-123") is False

def test_should_fix_port_name_empty(self):
"""Test name validation when name is empty."""
from understack_workflows.oslo_event.ironic_port import _should_fix_port_name

assert _should_fix_port_name("", "server-123") is False

def test_get_node_name_success(self):
"""Test successful node name retrieval."""
from understack_workflows.oslo_event.ironic_port import _get_node_name

mock_conn = Mock()
mock_node = Mock()
mock_node.name = "server-123"
mock_conn.baremetal.get_node.return_value = mock_node

result = _get_node_name(mock_conn, "node-uuid")

assert result == "server-123"
mock_conn.baremetal.get_node.assert_called_once_with("node-uuid")

def test_get_node_name_not_found(self):
"""Test node name retrieval when node not found."""
from understack_workflows.oslo_event.ironic_port import _get_node_name

mock_conn = Mock()
mock_conn.baremetal.get_node.return_value = None

result = _get_node_name(mock_conn, "node-uuid")

assert result is None

def test_fix_port_name_if_needed(self):
"""Test port name fixing."""
from understack_workflows.oslo_event.ironic_port import IronicPortEvent
from understack_workflows.oslo_event.ironic_port import _fix_port_name_if_needed

mock_conn = Mock()
mock_node = Mock()
mock_node.name = "server-123"
mock_conn.baremetal.get_node.return_value = mock_node

event = IronicPortEvent(
uuid="test-uuid",
name="eth0", # Missing prefix
address="aa:bb:cc:dd:ee:ff",
node_uuid="node-uuid",
physical_network="test-network",
pxe_enabled=True,
remote_port_id=None,
remote_switch_info=None,
remote_switch_id=None,
)

_fix_port_name_if_needed(mock_conn, event)

# Verify name was fixed
mock_conn.baremetal.update_port.assert_called_once_with(
"test-uuid", name="server-123:eth0"
)


class TestHandlePortCreateUpdate:
"""Test handle_port_create_update function."""

Expand All @@ -266,7 +342,12 @@ def mock_nautobot(self):
@pytest.fixture
def mock_conn(self):
"""Create mock connection."""
return Mock()
conn = Mock()
# Mock node lookup for name validation
mock_node = Mock()
mock_node.name = "1327172-hp1"
conn.baremetal.get_node.return_value = mock_node
return conn

def test_handle_port_create_update_with_remote_info(
self, mock_conn, mock_nautobot, port_create_event_data
Expand Down
Loading
Loading