From 99b71d89b3b658eeceac0db004d1ad4f50c8c439 Mon Sep 17 00:00:00 2001 From: Amir Chatur Date: Fri, 21 Nov 2025 14:03:27 -0600 Subject: [PATCH 1/2] feat: add storage deletion workflow with conditional execution Add storage cleanup steps to keystone oslo event sensor with proper conditional execution based on event type. Centralize deletion logic in openstack-oslo-event workflow template output parameters. - Add ansible-delete-server step triggered by server_storage_delete - Pass node_uuid as device_id to server deletion playbook - Added oslo sensor for nova for delete - Added handler and registered it --- .../sensors/sensor-nova-oslo-event.yaml | 110 ++++++++++++++++++ .../main/openstack_oslo_event.py | 1 + .../oslo_event/ironic_node.py | 38 ++++++ .../workflowtemplates/ansible-run.yaml | 3 + .../openstack-oslo-event.yaml | 4 + 5 files changed, 156 insertions(+) create mode 100644 components/site-workflows/sensors/sensor-nova-oslo-event.yaml diff --git a/components/site-workflows/sensors/sensor-nova-oslo-event.yaml b/components/site-workflows/sensors/sensor-nova-oslo-event.yaml new file mode 100644 index 000000000..30b455dd1 --- /dev/null +++ b/components/site-workflows/sensors/sensor-nova-oslo-event.yaml @@ -0,0 +1,110 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: Sensor +metadata: + name: nova-oslo-event + annotations: + workflows.argoproj.io/title: Process oslo_events for nova compute + workflows.argoproj.io/description: |+ + Triggers on the following Nova Events: + + - compute.instance.delete.end which happens when a server is deleted + + Resulting code should be very similar to: + + ``` + argo -n argo-events submit --from workflowtemplate/openstack-oslo-event \ + -p event-json "JSON-payload" + ``` + + Defined in `components/site-workflows/sensors/sensor-nova-oslo-event.yaml` +spec: + dependencies: + - eventName: notifications + eventSourceName: openstack-nova + name: nova-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: + - "compute.instance.delete.end" + template: + serviceAccountName: sensor-submit-workflow + triggers: + - template: + name: nova-instance-delete + k8s: + operation: create + parameters: + # first parameter is the parsed oslo.message + - dest: spec.arguments.parameters.0.value + src: + dataKey: body + dependencyName: nova-dep + - dest: spec.arguments.parameters.1.value + src: + dataKey: body.payload.instance_id + dependencyName: nova-dep + - dest: spec.arguments.parameters.2.value + src: + dataKey: body.payload.tenant_id + dependencyName: nova-dep + source: + # create a workflow in argo-events prefixed with nova-delete- + resource: + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: nova-delete- + namespace: argo-events + spec: + serviceAccountName: workflow + entrypoint: main + # defines the parameters being replaced above + arguments: + parameters: + - name: event-json + - name: instance_id + - name: project_id + templates: + - name: main + steps: + - - name: oslo-events + templateRef: + name: openstack-oslo-event + template: main + arguments: + parameters: + - name: event-json + value: "{{workflow.parameters.event-json}}" + - name: convert-project-id + inline: + script: + image: python:alpine + command: [python] + source: | + import uuid + project_id_without_dashes = "{{workflow.parameters.project_id}}" + print(str(uuid.UUID(project_id_without_dashes))) + + - - name: ansible-delete-server-storage + when: "{{steps.oslo-events.outputs.parameters.server_storage_deleted}} == True" + templateRef: + name: ansible-workflow-template + template: ansible-run + arguments: + parameters: + - name: playbook + value: storage_on_server_delete.yml + - name: extra_vars + value: device_id={{steps.oslo-events.outputs.parameters.node_uuid}} instance_id={{workflow.parameters.instance_id}} project_id={{steps.convert-project-id.outputs.result}} + - name: check_mode + value: "true" diff --git a/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py b/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py index ef7dd90e6..d8e5791b0 100644 --- a/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py +++ b/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py @@ -79,6 +79,7 @@ class NoEventHandlerError(Exception): ironic_node.handle_provision_end, nautobot_device_sync.handle_node_event, ], + "compute.instance.delete.end": ironic_node.handle_instance_delete, "identity.project.created": keystone_project.handle_project_created, "identity.project.updated": keystone_project.handle_project_updated, "identity.project.deleted": keystone_project.handle_project_deleted, diff --git a/python/understack-workflows/understack_workflows/oslo_event/ironic_node.py b/python/understack-workflows/understack_workflows/oslo_event/ironic_node.py index b436134f3..588dc18ce 100644 --- a/python/understack-workflows/understack_workflows/oslo_event/ironic_node.py +++ b/python/understack-workflows/understack_workflows/oslo_event/ironic_node.py @@ -119,3 +119,41 @@ def create_volume_connector(conn: Connection, event: IronicProvisionSetEvent): def instance_nqn(instance_id: str | None) -> str: return f"nqn.2014-08.org.nvmexpress:uuid:{instance_id}" + + +def handle_instance_delete(_conn: Connection, _: Nautobot, event_data: dict) -> int: + """Operates on a Nova instance delete event to clean up storage networking.""" + payload = event_data.get("payload", {}) + instance_uuid = payload.get("instance_id") + project_id = payload.get("tenant_id") + + if not instance_uuid or not project_id: + logger.error("No instance_id found in delete event payload") + return 1 + + logger.info("Processing instance delete for {}, Tenant {}".format(instance_uuid, project_id)) + + # Get the server to find the node_uuid + try: + + # Check if this server had storage enabled + if payload.metadata.get("storage") != "wanted": + logger.info("Server %s did not have storage enabled, skipping cleanup", instance_uuid) + save_output("server_storage_deleted", "False") + return 0 + + # Get node_uuid from the server's hypervisor_hostname or other field + # The node_uuid might be in server properties + node_uuid = payload.get("node") + + logger.info("Marking server storage for deletion: instance=%s, node=%s", instance_uuid, node_uuid) + save_output("server_storage_deleted", "True") + save_output("node_uuid", str(node_uuid) if node_uuid else "unknown") + save_output("instance_uuid", str(instance_uuid)) + save_output("project_id", project_id) + + return 0 + + except Exception as e: + logger.exception("Error processing instance delete: %s", e) + return 1 diff --git a/workflows/argo-events/workflowtemplates/ansible-run.yaml b/workflows/argo-events/workflowtemplates/ansible-run.yaml index 53ff28f08..5746078c8 100644 --- a/workflows/argo-events/workflowtemplates/ansible-run.yaml +++ b/workflows/argo-events/workflowtemplates/ansible-run.yaml @@ -19,6 +19,8 @@ spec: default: "var=default" - name: inventory_file default: inventory/in-cluster/01-nautobot.yaml + - name: check_mode + default: "false" container: image: ghcr.io/rss-engineering/undercloud-nautobot/ansible:latest command: [ansible-playbook] @@ -29,6 +31,7 @@ spec: - "-i" - "{{ inputs.parameters.inventory_file }}" - "-vvv" + - "{{- if eq inputs.parameters.check_mode \"true\" }}--check{{- end }}" env: - name: NAUTOBOT_TOKEN valueFrom: diff --git a/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml b/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml index 7b9094b18..34b9ba236 100644 --- a/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml +++ b/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml @@ -80,6 +80,10 @@ spec: valueFrom: path: /var/run/argo/output.svm_created default: "False" + - name: server_storage_deleted + valueFrom: + path: /var/run/argo/output.server_storage_deleted + default: "False" - name: project_tags valueFrom: path: /var/run/argo/output.project_tags From feff08b299eb7f9907997085997b82d1fb96ca41 Mon Sep 17 00:00:00 2001 From: Amir Chatur Date: Fri, 16 Jan 2026 10:30:40 -0600 Subject: [PATCH 2/2] refactor: use Argo Events filters for instance delete storage cleanup Replace Python handler with direct sensor filtering for Nova instance delete events. This simplifies the workflow by using Argo Events data filters to check if storage cleanup is needed. Changes: - Add data filter to check metadata.storage == 'wanted' in sensor - Extract device_id and project_id directly from event payload - Remove Python handler step and UUID conversion - Trigger storage_on_server_delete.yml ansible playbook directly - Deprecate handle_instance_delete() Python function - Set check_mode to false for actual execution Benefits: - Simpler workflow with fewer steps - Faster execution without Python handler overhead - Easier to understand and maintain - Follows event-driven architecture patterns --- .../sensors/sensor-nova-oslo-event.yaml | 55 +++++++------------ .../main/openstack_oslo_event.py | 3 +- .../oslo_event/ironic_node.py | 14 ++++- .../openstack-oslo-event.yaml | 10 ++-- 4 files changed, 40 insertions(+), 42 deletions(-) diff --git a/components/site-workflows/sensors/sensor-nova-oslo-event.yaml b/components/site-workflows/sensors/sensor-nova-oslo-event.yaml index 30b455dd1..6cbefa700 100644 --- a/components/site-workflows/sensors/sensor-nova-oslo-event.yaml +++ b/components/site-workflows/sensors/sensor-nova-oslo-event.yaml @@ -9,13 +9,16 @@ metadata: Triggers on the following Nova Events: - compute.instance.delete.end which happens when a server is deleted + AND the server has metadata.storage == "wanted" - Resulting code should be very similar to: + This sensor uses data filters to check the storage metadata and directly + triggers the ansible playbook without requiring a Python handler. - ``` - argo -n argo-events submit --from workflowtemplate/openstack-oslo-event \ - -p event-json "JSON-payload" - ``` + The sensor extracts the following parameters from the event: + - device_id: from payload.node (the Ironic node UUID) + - project_id: from payload.tenant_id (UUID without dashes) + + These are passed directly to the storage_on_server_delete.yml ansible playbook. Defined in `components/site-workflows/sensors/sensor-nova-oslo-event.yaml` spec: @@ -29,13 +32,18 @@ spec: jq: | .body = (.body["oslo.message"] | fromjson) filters: - # applies each of the items in data with 'and' but there's only one + # applies each of the items in data with 'and' dataLogicalOperator: "and" data: - path: "body.event_type" type: "string" value: - "compute.instance.delete.end" + # Only process if storage was wanted + - path: "body.payload.metadata.storage" + type: "string" + value: + - "wanted" template: serviceAccountName: sensor-submit-workflow triggers: @@ -44,16 +52,11 @@ spec: k8s: operation: create parameters: - # first parameter is the parsed oslo.message - dest: spec.arguments.parameters.0.value src: - dataKey: body + dataKey: body.payload.node dependencyName: nova-dep - dest: spec.arguments.parameters.1.value - src: - dataKey: body.payload.instance_id - dependencyName: nova-dep - - dest: spec.arguments.parameters.2.value src: dataKey: body.payload.tenant_id dependencyName: nova-dep @@ -68,35 +71,15 @@ spec: spec: serviceAccountName: workflow entrypoint: main - # defines the parameters being replaced above + # defines the parameters extracted from the event arguments: parameters: - - name: event-json - - name: instance_id + - name: device_id - name: project_id templates: - name: main steps: - - - name: oslo-events - templateRef: - name: openstack-oslo-event - template: main - arguments: - parameters: - - name: event-json - value: "{{workflow.parameters.event-json}}" - - name: convert-project-id - inline: - script: - image: python:alpine - command: [python] - source: | - import uuid - project_id_without_dashes = "{{workflow.parameters.project_id}}" - print(str(uuid.UUID(project_id_without_dashes))) - - - name: ansible-delete-server-storage - when: "{{steps.oslo-events.outputs.parameters.server_storage_deleted}} == True" templateRef: name: ansible-workflow-template template: ansible-run @@ -105,6 +88,6 @@ spec: - name: playbook value: storage_on_server_delete.yml - name: extra_vars - value: device_id={{steps.oslo-events.outputs.parameters.node_uuid}} instance_id={{workflow.parameters.instance_id}} project_id={{steps.convert-project-id.outputs.result}} + value: device_id={{workflow.parameters.device_id}} project_id={{workflow.parameters.project_id}} - name: check_mode - value: "true" + value: "false" diff --git a/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py b/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py index d8e5791b0..5adbae579 100644 --- a/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py +++ b/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py @@ -79,7 +79,8 @@ class NoEventHandlerError(Exception): ironic_node.handle_provision_end, nautobot_device_sync.handle_node_event, ], - "compute.instance.delete.end": ironic_node.handle_instance_delete, + # "compute.instance.delete.end" is now handled directly by the sensor with filters + # See: components/site-workflows/sensors/sensor-nova-oslo-event.yaml "identity.project.created": keystone_project.handle_project_created, "identity.project.updated": keystone_project.handle_project_updated, "identity.project.deleted": keystone_project.handle_project_deleted, diff --git a/python/understack-workflows/understack_workflows/oslo_event/ironic_node.py b/python/understack-workflows/understack_workflows/oslo_event/ironic_node.py index 588dc18ce..9f9923b2e 100644 --- a/python/understack-workflows/understack_workflows/oslo_event/ironic_node.py +++ b/python/understack-workflows/understack_workflows/oslo_event/ironic_node.py @@ -122,7 +122,19 @@ def instance_nqn(instance_id: str | None) -> str: def handle_instance_delete(_conn: Connection, _: Nautobot, event_data: dict) -> int: - """Operates on a Nova instance delete event to clean up storage networking.""" + """DEPRECATED: This handler is no longer used. + + Instance delete events are now handled directly by the sensor using data filters. + See: components/site-workflows/sensors/sensor-nova-oslo-event.yaml + + This function is kept for reference but should not be called. + + Original purpose: Operated on a Nova instance delete event to clean up storage networking. + """ + logger.warning( + "handle_instance_delete called but is deprecated. " + "This event should be handled by the sensor directly." + ) payload = event_data.get("payload", {}) instance_uuid = payload.get("instance_id") project_id = payload.get("tenant_id") diff --git a/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml b/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml index 34b9ba236..3dd5c4a65 100644 --- a/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml +++ b/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml @@ -80,10 +80,6 @@ spec: valueFrom: path: /var/run/argo/output.svm_created default: "False" - - name: server_storage_deleted - valueFrom: - path: /var/run/argo/output.server_storage_deleted - default: "False" - name: project_tags valueFrom: path: /var/run/argo/output.project_tags @@ -100,3 +96,9 @@ spec: valueFrom: path: /var/run/argo/output.instance_uuid default: "undefined" + # server_storage_deleted is deprecated - now handled by sensor filters + # Kept for backward compatibility but should not be used + - name: server_storage_deleted + valueFrom: + path: /var/run/argo/output.server_storage_deleted + default: "False"