Skip to content
Draft
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
48 changes: 15 additions & 33 deletions components/site-workflows/sensors/sensor-ironic-node-update.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,15 @@ metadata:
workflows.argoproj.io/description: |+
Triggers on the following Ironic Events:

- baremetal.node.provision_set.end which happens after a state change on the node
- baremetal.node.update.end which happens when node fields are updated.

Currently parses out the following fields:

- provision_state
- baremetal.node.create.end which happens when a baremetal node is created
- baremetal.node.update.end which happens when node fields are updated
- baremetal.node.delete.end which happens when a node is deleted

Resulting code should be very similar to:

```
argo -n argo-events submit --from workflowtemplate/sync-provision-state-to-nautobot \
-p device_uuid=00000000-0000-0000-0000-000000000000 -p provision_state=available
argo -n argo-events submit --from workflowtemplate/openstack-oslo-event \
-p event-json "JSON-payload"
```

Defined in `workflows/argo-events/sensors/ironic-node-update.yaml`
Expand All @@ -30,16 +27,19 @@ spec:
name: ironic-dep
transform:
# the event is a string-ified JSON so we need to decode it
jq: ".body[\"oslo.message\"] | fromjson"
# 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: "event_type"
- path: "body.event_type"
type: "string"
value:
- "baremetal.node.create.end"
- "baremetal.node.update.end"
- "baremetal.node.provision_set.end"
- "baremetal.node.delete.end"
template:
serviceAccountName: sensor-submit-workflow
triggers:
Expand All @@ -49,25 +49,10 @@ spec:
k8s:
operation: create
parameters:
# first parameter's value is replaced with the uuid
# first parameter is the parsed oslo.message
- dest: spec.arguments.parameters.0.value
src:
dataKey: payload.ironic_object\.data.uuid
dependencyName: ironic-dep
# second parameter's value is replaced with the provision_state
- dest: spec.arguments.parameters.1.value
src:
dataKey: payload.ironic_object\.data.provision_state
dependencyName: ironic-dep
# third parameter's value is replaced with the lessee
- dest: spec.arguments.parameters.2.value
src:
dataKey: payload.ironic_object\.data.lessee
dependencyName: ironic-dep
# fourth parameter's value is replaced with the resource_class
- dest: spec.arguments.parameters.3.value
src:
dataKey: payload.ironic_object\.data.resource_class
dataKey: body
dependencyName: ironic-dep
source:
# create a workflow in argo-events prefixed with ironic-node-update-
Expand All @@ -81,10 +66,7 @@ spec:
# defines the parameters being replaced above
arguments:
parameters:
- name: device_uuid
- name: provision_state
- name: lessee
- name: resource_class
- name: event-json
# references the workflow
workflowTemplateRef:
name: sync-provision-state-to-nautobot
name: openstack-oslo-event
24 changes: 13 additions & 11 deletions components/site-workflows/sensors/sensor-ironic-oslo-event.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,19 @@ metadata:

- baremetal.node.provision_set.end which happens after a state change on the node

This sensor handles ALL provision state changes. The event handlers internally
filter for specific states:
- deploying: Sets up storage (volume connectors) for instances with storage enabled
- inspecting: Updates Nautobot device with inspection data (inventory, ports)

Resulting code should be very similar to:

```
argo -n argo-events submit --from workflowtemplate/ironic-oslo-event \
-p event-json "JSON-payload" -p device_id=<UUID> -p project_id=<UUID>
argo -n argo-events submit --from workflowtemplate/openstack-oslo-event \
-p event-json "JSON-payload"
```

Defined in `workflows/argo-events/sensors/sensor-ironic-oslo-event.yaml`
Defined in `components/site-workflows/sensors/sensor-ironic-oslo-event.yaml`
spec:
dependencies:
- eventName: openstack
Expand All @@ -39,10 +44,6 @@ spec:
type: "string"
value:
- "baremetal.node.provision_set.end"
- path: "body.ironic_object.previous_provision_state"
type: "string"
value:
- "deploying"
template:
serviceAccountName: sensor-submit-workflow
triggers:
Expand All @@ -56,9 +57,9 @@ spec:
src:
dataKey: body
dependencyName: ironic-dep
- dest: spec.arguments.parameters.1.value # device_id
- dest: spec.arguments.parameters.1.value # previous_provision_state
src:
dataKey: body.ironic_object.uuid
dataKey: body.ironic_object.previous_provision_state
dependencyName: ironic-dep
- dest: spec.arguments.parameters.2.value # project_id
src:
Expand All @@ -79,7 +80,7 @@ spec:
arguments:
parameters:
- name: event-json
- name: device_id
- name: previous_provision_state
- name: project_id
templates:
- name: main
Expand All @@ -93,6 +94,7 @@ spec:
- name: event-json
value: "{{workflow.parameters.event-json}}"
- name: convert-project-id
when: "\"{{workflow.parameters.previous_provision_state}}\" == deploying"
inline:
script:
image: python:alpine
Expand All @@ -102,7 +104,7 @@ spec:
project_id_without_dashes = "{{workflow.parameters.project_id}}"
print(str(uuid.UUID(project_id_without_dashes)))
- - name: ansible-storage-update
when: "\"{{steps.oslo-events.outputs.parameters.storage}}\" == wanted"
when: "\"{{steps.oslo-events.outputs.parameters.storage}}\" == wanted && \"{{workflow.parameters.previous_provision_state}}\" == deploying"
templateRef:
name: ansible-workflow-template
template: ansible-run
Expand Down
82 changes: 79 additions & 3 deletions python/understack-workflows/tests/test_oslo_event_ironic_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,70 @@

import pytest

from understack_workflows.oslo_event.ironic_node import IronicNodeEvent
from understack_workflows.oslo_event.ironic_node import IronicProvisionSetEvent
from understack_workflows.oslo_event.ironic_node import create_volume_connector
from understack_workflows.oslo_event.ironic_node import handle_provision_end
from understack_workflows.oslo_event.ironic_node import instance_nqn


class TestIronicNodeEvent:
"""Test cases for IronicNodeEvent class."""

def test_from_event_dict_success(self):
"""Test successful node event parsing."""
event_data = {
"payload": {
"ironic_object.data": {
"uuid": "test-uuid-123",
"name": "test-node",
"provision_state": "available",
}
}
}

event = IronicNodeEvent.from_event_dict(event_data)

assert event.uuid == "test-uuid-123"

def test_from_event_dict_minimal_data(self):
"""Test parsing with only required UUID field."""
event_data = {"payload": {"ironic_object.data": {"uuid": "minimal-uuid"}}}

event = IronicNodeEvent.from_event_dict(event_data)

assert event.uuid == "minimal-uuid"

def test_from_event_dict_no_payload(self):
"""Test event parsing with missing payload."""
event_data = {}

with pytest.raises(ValueError, match="Invalid event. No 'payload'"):
IronicNodeEvent.from_event_dict(event_data)

def test_from_event_dict_no_ironic_object_data(self):
"""Test event parsing with missing ironic_object.data."""
event_data = {"payload": {"other_field": "value"}}

with pytest.raises(
ValueError, match="Invalid event. No 'ironic_object.data' in payload"
):
IronicNodeEvent.from_event_dict(event_data)

def test_from_event_dict_missing_uuid(self):
"""Test event parsing with missing UUID field."""
event_data = {"payload": {"ironic_object.data": {"name": "test-node"}}}

with pytest.raises(KeyError): # KeyError when uuid is missing
IronicNodeEvent.from_event_dict(event_data)

def test_direct_initialization(self):
"""Test direct initialization of IronicNodeEvent."""
event = IronicNodeEvent(uuid="direct-uuid")

assert event.uuid == "direct-uuid"


class TestIronicProvisionSetEvent:
"""Test cases for IronicProvisionSetEvent class."""

Expand Down Expand Up @@ -124,6 +182,7 @@ def valid_event_data(self):
"lessee": uuid.uuid4(),
"event": "provision_end",
"uuid": uuid.uuid4(),
"previous_provision_state": "deploying",
}
},
}
Expand Down Expand Up @@ -264,9 +323,26 @@ def test_handle_provision_end_storage_metadata_missing(
mock_server.metadata = {"other_key": "value"}
mock_conn.get_server_by_id.return_value = mock_server

# This should raise a KeyError when accessing metadata["storage"]
with pytest.raises(KeyError):
handle_provision_end(mock_conn, mock_nautobot, valid_event_data)
# When storage key is missing, it should treat it as "not-set"
result = handle_provision_end(mock_conn, mock_nautobot, valid_event_data)

assert result == 0
ironic_data = valid_event_data["payload"]["ironic_object.data"]
instance_uuid = ironic_data["instance_uuid"]
node_uuid = ironic_data["uuid"]

mock_conn.get_server_by_id.assert_called_once_with(instance_uuid)

# Check save_output calls - storage should be "not-set" when key is missing
expected_calls = [
("storage", "not-set"),
("node_uuid", str(node_uuid)),
("instance_uuid", str(instance_uuid)),
]
actual_calls = [call.args for call in mock_save_output.call_args_list]
assert actual_calls == expected_calls

mock_create_connector.assert_called_once()

@patch("understack_workflows.oslo_event.ironic_node.is_project_svm_enabled")
def test_handle_provision_end_invalid_event_data(
Expand Down
Loading
Loading