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..6cbefa700 --- /dev/null +++ b/components/site-workflows/sensors/sensor-nova-oslo-event.yaml @@ -0,0 +1,93 @@ +--- +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 + AND the server has metadata.storage == "wanted" + + This sensor uses data filters to check the storage metadata and directly + triggers the ansible playbook without requiring a Python handler. + + 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: + 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' + 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: + - template: + name: nova-instance-delete + k8s: + operation: create + parameters: + - dest: spec.arguments.parameters.0.value + src: + dataKey: body.payload.node + dependencyName: nova-dep + - dest: spec.arguments.parameters.1.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 extracted from the event + arguments: + parameters: + - name: device_id + - name: project_id + templates: + - name: main + steps: + - - name: ansible-delete-server-storage + templateRef: + name: ansible-workflow-template + template: ansible-run + arguments: + parameters: + - name: playbook + value: storage_on_server_delete.yml + - name: extra_vars + value: device_id={{workflow.parameters.device_id}} project_id={{workflow.parameters.project_id}} + - name: check_mode + 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 ef7dd90e6..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,6 +79,8 @@ class NoEventHandlerError(Exception): ironic_node.handle_provision_end, nautobot_device_sync.handle_node_event, ], + # "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 b436134f3..9f9923b2e 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,53 @@ 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: + """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") + + 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..3dd5c4a65 100644 --- a/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml +++ b/workflows/argo-events/workflowtemplates/openstack-oslo-event.yaml @@ -96,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"