From 80edc948d8a95430051dd016b27c821930eac21f Mon Sep 17 00:00:00 2001 From: haseeb Date: Wed, 17 Jun 2026 22:23:15 +0530 Subject: [PATCH] feat: PUC-1686: ranges of IPs that can be used by OpenStack and then handed out by OpenStack 1. Custom fields used by prefixes which will be synced by Nautobot operator 2- Ansible role reads from Nautobot and creates corresponding OpenStack subnet pools 3- Remove event handler which syncs Openstack network to Nautobot Prefixes --- ansible/neutron-post-deploy.yaml | 1 + ansible/roles/custom_fields/tasks/main.yml | 66 +++++++++++++++++ .../openstack_subnet_pools/defaults/main.yml | 14 ++++ .../openstack_subnet_pools/tasks/main.yml | 71 +++++++++++++++++++ .../templates/sensor-neutron-oslo-event.yaml | 67 ----------------- .../nautobot/job-nautobot-post-deploy.yaml | 2 +- .../neutron/job-neutron-post-deploy.yaml | 2 +- go/nautobotop/go.mod | 2 +- .../internal/nautobot/models/prefix.go | 33 ++++----- .../internal/nautobot/sync/prefix.go | 5 ++ .../main/openstack_oslo_event.py | 8 --- 11 files changed, 177 insertions(+), 94 deletions(-) create mode 100644 ansible/roles/openstack_subnet_pools/defaults/main.yml create mode 100644 ansible/roles/openstack_subnet_pools/tasks/main.yml delete mode 100644 charts/site-workflows/templates/sensor-neutron-oslo-event.yaml diff --git a/ansible/neutron-post-deploy.yaml b/ansible/neutron-post-deploy.yaml index 19afbac43..d6ebdeac0 100644 --- a/ansible/neutron-post-deploy.yaml +++ b/ansible/neutron-post-deploy.yaml @@ -25,4 +25,5 @@ roles: - role: neutron_segment_range + - role: openstack_subnet_pools - role: openstack_network diff --git a/ansible/roles/custom_fields/tasks/main.yml b/ansible/roles/custom_fields/tasks/main.yml index 3e73ca5e1..05e60fe10 100644 --- a/ansible/roles/custom_fields/tasks/main.yml +++ b/ansible/roles/custom_fields/tasks/main.yml @@ -84,3 +84,69 @@ content_types: - dcim.interface filter_logic: exact + +- name: Create Custom Field Subnet Pool Name + networktocode.nautobot.custom_field: + state: present + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + description: Name of the OpenStack subnet pool to create from this prefix + label: Subnet Pool Name + key: subnet_pool_name + type: text + required: false + weight: 100 + content_types: + - ipam.prefix + filter_logic: exact + +- name: Create Custom Field Subnet Pool Default Prefixlen + networktocode.nautobot.custom_field: + state: present + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + description: Default prefix length for subnets created from this pool + label: Default Prefixlen + key: subnet_pool_default_prefixlen + type: integer + required: false + weight: 100 + content_types: + - ipam.prefix + validation_minimum: 1 + validation_maximum: 32 + filter_logic: exact + +- name: Create Custom Field Subnet Pool Min Prefixlen + networktocode.nautobot.custom_field: + state: present + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + description: Minimum prefix length for subnets created from this pool + label: Min Prefixlen + key: subnet_pool_min_prefixlen + type: integer + required: false + weight: 100 + content_types: + - ipam.prefix + validation_minimum: 1 + validation_maximum: 32 + filter_logic: exact + +- name: Create Custom Field Subnet Pool Max Prefixlen + networktocode.nautobot.custom_field: + state: present + url: "{{ nautobot_url }}" + token: "{{ nautobot_token }}" + description: Maximum prefix length for subnets created from this pool + label: Max Prefixlen + key: subnet_pool_max_prefixlen + type: integer + required: false + weight: 100 + content_types: + - ipam.prefix + validation_minimum: 1 + validation_maximum: 32 + filter_logic: exact diff --git a/ansible/roles/openstack_subnet_pools/defaults/main.yml b/ansible/roles/openstack_subnet_pools/defaults/main.yml new file mode 100644 index 000000000..bd5cdfdba --- /dev/null +++ b/ansible/roles/openstack_subnet_pools/defaults/main.yml @@ -0,0 +1,14 @@ +--- +# Default variables for openstack_subnet_pools role + +# OpenStack cloud configuration name from clouds.yaml +openstack_cloud: understack + +# Tag to filter prefixes for subnet pool creation +subnet_pool_tag: openstack-subnet-pool + +# Whether to create address scopes (optional) +create_address_scopes: false + +# Address scope name mapping (if create_address_scopes is true) +address_scope_name: "{{ openstack_cloud }}-address-scope" diff --git a/ansible/roles/openstack_subnet_pools/tasks/main.yml b/ansible/roles/openstack_subnet_pools/tasks/main.yml new file mode 100644 index 000000000..dfa66289a --- /dev/null +++ b/ansible/roles/openstack_subnet_pools/tasks/main.yml @@ -0,0 +1,71 @@ +--- +# Create OpenStack subnet pools from Nautobot prefixes +# This role reads prefixes tagged with 'openstack-subnet-pool' and creates +# corresponding OpenStack subnet pools with the configured allocation settings + +- name: Query Nautobot for prefixes tagged with openstack-subnet-pool + ansible.builtin.uri: + url: "{{ nautobot_url }}/api/ipam/prefixes/?tags={{ subnet_pool_tag }}" + method: GET + headers: + Authorization: "Token {{ nautobot_token }}" + Accept: "application/json; version={{ nautobot_api_version }}" + status_code: 200 + register: prefixes_query + check_mode: false + +- name: Log queried prefixes + ansible.builtin.debug: + msg: "Found {{ prefixes_query.json.count }} prefixes tagged with '{{ subnet_pool_tag }}'" + +- name: Assert prefixes were found + ansible.builtin.assert: + that: + - prefixes_query.json.count > 0 + fail_msg: "No prefixes found with tag '{{ subnet_pool_tag }}' in Nautobot" + +- name: Group prefixes by subnet pool name + ansible.builtin.set_fact: + subnet_pools: >- + {{ + subnet_pools | default({}) | combine({ + item.custom_fields.subnet_pool_name: (subnet_pools.get(item.custom_fields.subnet_pool_name, []) + [item]) + }) + }} + loop: "{{ prefixes_query.json.results }}" + when: + - item.custom_fields.subnet_pool_name is defined + - item.custom_fields.subnet_pool_name | length > 0 + +- name: Log discovered subnet pools + ansible.builtin.debug: + msg: "Discovered subnet pools: {{ subnet_pools.keys() | list }}" + +- name: Create or update OpenStack subnet pools + openstack.cloud.subnet_pool: + cloud: "{{ openstack_cloud }}" + state: present + name: "{{ pool_name }}" + prefixes: "{{ pool_prefixes | map(attribute='prefix') | list }}" + default_prefixlen: "{{ (pool_prefixes | first).custom_fields.subnet_pool_default_prefixlen }}" + min_prefixlen: "{{ (pool_prefixes | first).custom_fields.subnet_pool_min_prefixlen }}" + max_prefixlen: "{{ (pool_prefixes | first).custom_fields.subnet_pool_max_prefixlen }}" + shared: true + loop: "{{ subnet_pools | dict2items }}" + loop_control: + loop_var: subnet_pool_item + label: "{{ pool_name }}" + vars: + pool_name: "{{ subnet_pool_item.key }}" + pool_prefixes: "{{ subnet_pool_item.value }}" + register: subnet_pool_results + +- name: Log subnet pool creation results + ansible.builtin.debug: + msg: "Subnet pool '{{ item.subnet_pool_item.key }}': {{ 'created' if item is changed else 'unchanged' }}" + loop: "{{ subnet_pool_results.results }}" + when: item is not skipped + +- name: Verify subnet pools were created + ansible.builtin.debug: + msg: "Successfully created/updated {{ subnet_pool_results.results | selectattr('subnet_pool_item.key', 'defined') | list | length }} subnet pools" diff --git a/charts/site-workflows/templates/sensor-neutron-oslo-event.yaml b/charts/site-workflows/templates/sensor-neutron-oslo-event.yaml deleted file mode 100644 index 2b0de2773..000000000 --- a/charts/site-workflows/templates/sensor-neutron-oslo-event.yaml +++ /dev/null @@ -1,67 +0,0 @@ -{{- if .Values.enabled.neutron }} -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: neutron-oslo-event - annotations: - workflows.argoproj.io/title: Add/edit Nautobot objects according to Neutron events - workflows.argoproj.io/description: |+ - Triggers on Events from Neutron: - - - network create/update/delete - - Submits workflow using workflowtemplate/openstack-oslo-event - - Defined in `components/site-workflows/sensors/sensor-neutron-oslo-event.yaml` -spec: - dependencies: - - eventName: notifications - eventSourceName: openstack-neutron - name: oslo-event-type - transform: - # the event is a string-ified JSON so we need to decode it - jq: '{body: (.body["oslo.message"] | fromjson)}' - filters: - dataLogicalOperator: "and" - data: - - path: body.event_type - type: "string" - # any of the values will trigger - value: - - "network.create.end" - - "network.update.end" - - "network.delete.end" - - "subnet.create.end" - - "subnet.update.end" - - "subnet.delete.end" - template: - serviceAccountName: sensor-submit-workflow - triggers: - - template: - name: neutron-oslo-event - # 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: oslo-event-type - source: - # create a workflow in argo-events prefixed with ironic-node-update- - resource: - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: neutron-oslo-event- - namespace: argo-events - spec: - # defines the parameters being replaced above - arguments: - parameters: - - name: event-json - # references the workflow - workflowTemplateRef: - name: openstack-oslo-event -{{- end }} diff --git a/components/nautobot/job-nautobot-post-deploy.yaml b/components/nautobot/job-nautobot-post-deploy.yaml index 9132efaeb..13332afeb 100644 --- a/components/nautobot/job-nautobot-post-deploy.yaml +++ b/components/nautobot/job-nautobot-post-deploy.yaml @@ -23,7 +23,7 @@ spec: type: RuntimeDefault containers: - name: ansible - image: ghcr.io/rackerlabs/understack/ansible:latest + image: ghcr.io/rackerlabs/understack/ansible:pr-2081 imagePullPolicy: Always command: ["ansible-runner", "run", "/runner", "--playbook", "nautobot-post-deploy.yaml"] resources: diff --git a/components/neutron/job-neutron-post-deploy.yaml b/components/neutron/job-neutron-post-deploy.yaml index 24374e56e..7550d6872 100644 --- a/components/neutron/job-neutron-post-deploy.yaml +++ b/components/neutron/job-neutron-post-deploy.yaml @@ -23,7 +23,7 @@ spec: type: RuntimeDefault containers: - name: ansible - image: ghcr.io/rackerlabs/understack/ansible:latest + image: ghcr.io/rackerlabs/understack/ansible:pr-2081 imagePullPolicy: Always command: ["ansible-runner", "run", "/runner", "--playbook", "neutron-post-deploy.yaml"] resources: diff --git a/go/nautobotop/go.mod b/go/nautobotop/go.mod index 21632d42b..7401e2bec 100644 --- a/go/nautobotop/go.mod +++ b/go/nautobotop/go.mod @@ -14,6 +14,7 @@ require ( k8s.io/api v0.33.0 k8s.io/apimachinery v0.33.0 k8s.io/client-go v0.33.0 + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 sigs.k8s.io/controller-runtime v0.21.0 ) @@ -118,7 +119,6 @@ require ( k8s.io/component-base v0.33.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/randfill v1.0.0 // indirect diff --git a/go/nautobotop/internal/nautobot/models/prefix.go b/go/nautobotop/internal/nautobot/models/prefix.go index f0791bec0..7ba3e0adf 100644 --- a/go/nautobotop/internal/nautobot/models/prefix.go +++ b/go/nautobotop/internal/nautobot/models/prefix.go @@ -5,20 +5,21 @@ type Prefixes struct { } type Prefix struct { - ID string `json:"id" yaml:"id"` - Prefix string `json:"prefix" yaml:"prefix"` - Namespace string `json:"namespace" yaml:"namespace"` - Type string `json:"type" yaml:"type"` - Status string `json:"status" yaml:"status"` - Role string `json:"role" yaml:"role"` - Rir string `json:"rir" yaml:"rir"` - DateAllocated string `json:"date_allocated" yaml:"date_allocated"` - Description string `json:"description" yaml:"description"` - Vrfs []string `json:"vrfs" yaml:"vrfs"` - Locations []string `json:"locations" yaml:"locations"` - VlanGroup string `json:"vlan_group" yaml:"vlan_group"` - Vlan string `json:"vlan" yaml:"vlan"` - TenantGroup string `json:"tenant_group" yaml:"tenant_group"` - Tenant string `json:"tenant" yaml:"tenant"` - Tags []string `json:"tags" yaml:"tags"` + ID string `json:"id" yaml:"id"` + Prefix string `json:"prefix" yaml:"prefix"` + Namespace string `json:"namespace" yaml:"namespace"` + Type string `json:"type" yaml:"type"` + Status string `json:"status" yaml:"status"` + Role string `json:"role" yaml:"role"` + Rir string `json:"rir" yaml:"rir"` + DateAllocated string `json:"date_allocated" yaml:"date_allocated"` + Description string `json:"description" yaml:"description"` + Vrfs []string `json:"vrfs" yaml:"vrfs"` + Locations []string `json:"locations" yaml:"locations"` + VlanGroup string `json:"vlan_group" yaml:"vlan_group"` + Vlan string `json:"vlan" yaml:"vlan"` + TenantGroup string `json:"tenant_group" yaml:"tenant_group"` + Tenant string `json:"tenant" yaml:"tenant"` + Tags []string `json:"tags" yaml:"tags"` + CustomFields map[string]interface{} `json:"custom_fields" yaml:"custom_fields"` } diff --git a/go/nautobotop/internal/nautobot/sync/prefix.go b/go/nautobotop/internal/nautobot/sync/prefix.go index 0222edb19..7a74240d7 100644 --- a/go/nautobotop/internal/nautobot/sync/prefix.go +++ b/go/nautobotop/internal/nautobot/sync/prefix.go @@ -170,6 +170,11 @@ func (s *PrefixSync) syncSinglePrefix(ctx context.Context, prefix models.Prefix) if len(prefix.Locations) > 1 { customFields["locations"] = s.buildLocationIDs(ctx, prefix.Locations[1:]) } + if prefix.CustomFields != nil { + for k, v := range prefix.CustomFields { + customFields[k] = v + } + } if len(customFields) > 0 { prefixRequest.CustomFields = customFields } 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 29533d95f..eef6c8d75 100644 --- a/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py +++ b/python/understack-workflows/understack_workflows/main/openstack_oslo_event.py @@ -20,8 +20,6 @@ from understack_workflows.oslo_event import ironic_portgroup from understack_workflows.oslo_event import keystone_project from understack_workflows.oslo_event import nautobot_device_sync -from understack_workflows.oslo_event import neutron_network -from understack_workflows.oslo_event import neutron_subnet logger = logging.getLogger(__name__) @@ -79,12 +77,6 @@ class NoEventHandlerError(Exception): "volume_type_project.access.remove": ( cinder_volume_type.handle_volume_type_access_removed ), - "network.create.end": neutron_network.handle_network_create_or_update, - "network.update.end": neutron_network.handle_network_create_or_update, - "network.delete.end": neutron_network.handle_network_delete, - "subnet.create.end": neutron_subnet.handle_subnet_create_or_update, - "subnet.update.end": neutron_subnet.handle_subnet_create_or_update, - "subnet.delete.end": neutron_subnet.handle_subnet_delete, }