diff --git a/.github/workflows/containers.yaml b/.github/workflows/containers.yaml index 76eb282cc..2332768e7 100644 --- a/.github/workflows/containers.yaml +++ b/.github/workflows/containers.yaml @@ -9,6 +9,7 @@ on: paths: &triggerpaths - "ansible/**" - "containers/ansible/**" + - "containers/diff-nautobot-understack/**" - "containers/dnsmasq/**" - "containers/ironic-nautobot-client/**" - "containers/ironic-vnc-client/**" @@ -30,6 +31,8 @@ jobs: container: - name: ansible target: prod + - name: diff-nautobot-understack + target: prod - name: dnsmasq target: prod - name: ironic-nautobot-client diff --git a/containers/diff-nautobot-understack/Dockerfile b/containers/diff-nautobot-understack/Dockerfile new file mode 100644 index 000000000..1d86cc4bd --- /dev/null +++ b/containers/diff-nautobot-understack/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.12-slim AS builder + +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && \ + apt-get install -y --no-install-recommends build-essential && \ + rm -rf /var/lib/apt/lists/* +RUN --mount=type=cache,target=/root/.cache/pip pip install 'wheel==0.43.0' +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +# copy in the code +COPY python/diff-nautobot-understack /tmp/understack/python/diff-nautobot-understack + +# Set version for setuptools-scm since .git is not available in build context +ENV SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 + +# install our requirements and our packages +RUN --mount=type=cache,target=/root/.cache/uv \ + uv venv /opt/venv --clear && \ + uv pip install \ + --python /opt/venv/bin/python \ + /tmp/understack/python/diff-nautobot-understack + +FROM python:3.12-slim AS prod +LABEL org.opencontainers.image.description="Diff Nautobot Understack - Compare OpenStack and Nautobot data" +ENV PATH="/opt/venv/bin:${PATH}" + +RUN groupadd --system --gid 10001 appgroup && \ + useradd --system --create-home --uid 10001 --gid appgroup appuser + +COPY --from=builder --link /opt/venv /opt/venv + +USER appuser diff --git a/docs/operator-guide/openstack-nautobot-sync.md b/docs/operator-guide/openstack-nautobot-sync.md index ed7922ac5..4089ccad7 100644 --- a/docs/operator-guide/openstack-nautobot-sync.md +++ b/docs/operator-guide/openstack-nautobot-sync.md @@ -73,6 +73,38 @@ The following events trigger Nautobot updates: When Nautobot gets out of sync with OpenStack (e.g., after database restore, missed events, or manual changes), you can perform a bulk resync. +### Dry-Run (Diff Preview) + +Before running a resync, you can preview what changes would be made using the +diff workflow. This compares OpenStack and Nautobot data without making changes: + +```bash +argo -n argo-events submit --from workflowtemplate/diff-nautobot +``` + +The diff workflow compares: + +- Keystone projects ↔ Nautobot tenants +- Neutron networks ↔ Nautobot UCVNIs +- Neutron subnets ↔ Nautobot prefixes +- Ironic nodes ↔ Nautobot devices + +You can also run the diff CLI directly: + +```bash +# Compare all projects +uc-diff projects + +# Compare all networks +uc-diff network + +# Compare all subnets +uc-diff subnets + +# Compare all devices +uc-diff devices +``` + ### Resync Order The resync workflow runs three steps sequentially in dependency order: diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/cli.py b/python/diff-nautobot-understack/diff_nautobot_understack/cli.py index 11bb857fe..2981eeeb7 100644 --- a/python/diff-nautobot-understack/diff_nautobot_understack/cli.py +++ b/python/diff-nautobot-understack/diff_nautobot_understack/cli.py @@ -7,6 +7,7 @@ from rich.console import Console from rich.table import Table +from diff_nautobot_understack.device.main import ironic_nodes_diff_from_nautobot_devices from diff_nautobot_understack.network.main import ( openstack_network_diff_from_ucvni_network, ) @@ -17,6 +18,9 @@ openstack_project_diff_from_nautobot_tenant, ) from diff_nautobot_understack.settings import app_settings as settings +from diff_nautobot_understack.subnet.main import ( + openstack_subnets_diff_from_nautobot_prefixes, +) required_env_vars = ["NAUTOBOT_TOKEN", "NAUTOBOT_URL", "OS_CLOUD"] @@ -29,6 +33,8 @@ diff_outputs = { "project": {"title": "Project Diff", "id_column_name": "Project ID"}, "network": {"title": "Network Diff", "id_column_name": "Network ID"}, + "subnet": {"title": "Subnet Diff", "id_column_name": "Subnet ID"}, + "device": {"title": "Device Diff", "id_column_name": "Device ID"}, } @@ -108,6 +114,32 @@ def network( display_output(diff_result, "network", output_format) +@app.command() +def subnets( + debug: bool = typer.Option(False, "--debug", "-v", help="Enable debug mode"), + output_format: str = typer.Option( + "json", "--format", help="Available formats: json, table, human" + ), +): + """OpenStack subnets ⟹ Nautobot prefixes""" + settings.debug = debug + diff_result = openstack_subnets_diff_from_nautobot_prefixes() + display_output(diff_result, "subnet", output_format) + + +@app.command() +def devices( + debug: bool = typer.Option(False, "--debug", "-v", help="Enable debug mode"), + output_format: str = typer.Option( + "json", "--format", help="Available formats: json, table, human" + ), +): + """Ironic nodes ⟹ Nautobot devices""" + settings.debug = debug + diff_result = ironic_nodes_diff_from_nautobot_devices() + display_output(diff_result, "device", output_format) + + def check_env_vars(required_vars): missing_vars = [var for var in required_vars if var not in os.environ] diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/device/__init__.py b/python/diff-nautobot-understack/diff_nautobot_understack/device/__init__.py new file mode 100644 index 000000000..30d6f9c4a --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/device/__init__.py @@ -0,0 +1 @@ +# Device diff adapters (Ironic nodes <-> Nautobot devices) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/__init__.py b/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/__init__.py new file mode 100644 index 000000000..e5d89d4d4 --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/__init__.py @@ -0,0 +1 @@ +# Device adapters diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/ironic_node.py b/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/ironic_node.py new file mode 100644 index 000000000..6f5d4d15c --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/ironic_node.py @@ -0,0 +1,50 @@ +from diffsync import Adapter + +from diff_nautobot_understack.clients.openstack import API +from diff_nautobot_understack.device import models + +# Map Ironic provision states to Nautobot statuses +PROVISION_STATE_MAP = { + "active": "Active", + "enroll": "Planned", + "available": "Available", + "deploy failed": "Quarantine", + "error": "Quarantine", + "rescue": "Quarantine", + "rescue failed": "Quarantine", + "unrescueing": "Quarantine", + "manageable": "Staged", + "inspecting": "Provisioning", + "deploying": "Provisioning", + "cleaning": "Quarantine", + "clean failed": "Quarantine", + "deleting": "Decommissioning", +} + + +class Nodes(Adapter): + """Adapter for Ironic baremetal nodes.""" + + device = models.DeviceModel + + top_level = ["device"] + type = "IronicNode" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + openstack_api = API() + self.cloud = openstack_api.cloud_connection + + def load(self): + for node in self.cloud.baremetal.nodes(): + # Map provision state to Nautobot status + status = PROVISION_STATE_MAP.get(node.provision_state) + + self.add( + self.device( + id=node.id, + name=node.name, + status=status, + tenant_id=node.lessee, + ) + ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/nautobot_device.py b/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/nautobot_device.py new file mode 100644 index 000000000..dc25af65d --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/device/adapters/nautobot_device.py @@ -0,0 +1,44 @@ +from diffsync import Adapter + +from diff_nautobot_understack.clients.nautobot import API +from diff_nautobot_understack.device import models + + +class Devices(Adapter): + """Adapter for Nautobot devices.""" + + device = models.DeviceModel + + top_level = ["device"] + type = "NautobotDevice" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_client = API() + + def load(self): + # Filter by role=server to only get baremetal devices + url = "/api/dcim/devices/?role=server" + devices_response = self.api_client.make_api_request(url, paginated=True) + + for device in devices_response: + device_id = device.get("id") + if not device_id: + continue + + # Get status name + status = device.get("status", {}) + status_name = status.get("name") if status else None + + # Get tenant ID + tenant = device.get("tenant", {}) + tenant_id = tenant.get("id") if tenant else None + + self.add( + self.device( + id=device_id, + name=device.get("name"), + status=status_name, + tenant_id=tenant_id, + ) + ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/device/main.py b/python/diff-nautobot-understack/diff_nautobot_understack/device/main.py new file mode 100644 index 000000000..b87a78a4b --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/device/main.py @@ -0,0 +1,18 @@ +from diffsync.diff import Diff +from diffsync.enum import DiffSyncFlags + +from diff_nautobot_understack.device.adapters.ironic_node import Nodes +from diff_nautobot_understack.device.adapters.nautobot_device import Devices + + +def ironic_nodes_diff_from_nautobot_devices() -> Diff: + """Compare all Ironic nodes with Nautobot devices.""" + ironic_nodes = Nodes() + ironic_nodes.load() + + nautobot_devices = Devices() + nautobot_devices.load() + + return nautobot_devices.diff_from( + ironic_nodes, flags=DiffSyncFlags.CONTINUE_ON_FAILURE + ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/device/models.py b/python/diff-nautobot-understack/diff_nautobot_understack/device/models.py new file mode 100644 index 000000000..29c0f445d --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/device/models.py @@ -0,0 +1,18 @@ +from diffsync import DiffSyncModel + + +class DeviceModel(DiffSyncModel): + """Model for comparing Ironic nodes with Nautobot devices.""" + + _modelname = "device" + _identifiers = ("id",) + _attributes = ( + "name", + "status", + "tenant_id", + ) + + id: str + name: str | None = None + status: str | None = None + tenant_id: str | None = None diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/openstack_network.py b/python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/openstack_network.py index 63c5d88d9..a5f9e9e50 100644 --- a/python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/openstack_network.py +++ b/python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/openstack_network.py @@ -21,8 +21,7 @@ def load(self): self.network( id=network.id, name=network.name, - status=network.status.lower(), - provider_physical_network=network.provider_physical_network, - vni_id=network.provider_segmentation_id, + tenant_id=network.project_id, + ucvni_id=network.provider_segmentation_id, ) ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/ucvni.py b/python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/ucvni.py index b08710477..327907a90 100644 --- a/python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/ucvni.py +++ b/python/diff-nautobot-understack/diff_nautobot_understack/network/adapters/ucvni.py @@ -1,26 +1,12 @@ from diffsync import Adapter -from pydantic import BaseModel from diff_nautobot_understack.clients.nautobot import API from diff_nautobot_understack.network import models -class UcvniDetails(BaseModel): - id: str - name: str - status: str - ucvni_id: int - ucvni_group: str - vlan_group: str - vlan_id: int - - -class NautobotError(Exception): - message = "Nautobot error" - - class Network(Adapter): - CALLER_FRAME = 1 + """Adapter for Nautobot UCVNIs.""" + network = models.NetworkModel top_level = ["network"] @@ -31,75 +17,23 @@ def __init__(self, **kwargs): self.api_client = API() def load(self): - ucvni_data: list[UcvniDetails] = self.ucvni_get() - for ucvni_item in ucvni_data: - network = self.network( - id=ucvni_item.id, - name=ucvni_item.name, - vni_id=ucvni_item.vlan_id, - provider_physical_network=ucvni_item.vlan_group, - status=ucvni_item.status, - ) - self.add(network) - - def ucvni_get( - self, - ) -> list[UcvniDetails]: - ucvni_detail_list: list[UcvniDetails] = [] - - url = "/api/plugins/undercloud-vni/ucvnis/?include=relationship" - - ucvnis_response = self.api_client.make_api_request( - f"{url}/?include=relationships", paginated=True - ) - - for ucvni_item in ucvnis_response: - ucvni_group = self.api_client.make_api_request( - url=ucvni_item.get("ucvni_group", {}).get("url") - ) - status = self.api_client.make_api_request( - url=ucvni_item.get("status", {}).get("url") - ) - vlan_uuid_objects = ( - ucvni_item.get("relationships", {}) - .get("ucvni_vlans", {}) - .get("destination") - .get("objects") - ) - vlan_details = self.get_vlan_details(vlan_uuid_objects) - vlan_group, vlan_ids = next(iter(vlan_details.items())) - ucvni_details = UcvniDetails( - id=ucvni_item.get("id"), - name=ucvni_item.get("name"), - ucvni_id=ucvni_item.get("ucvni_id"), - ucvni_group=ucvni_group.get("name"), - status=status.get("name").lower(), - vlan_group=vlan_group, - vlan_id=int(vlan_ids[0]), - ) - ucvni_detail_list.append(ucvni_details) - return ucvni_detail_list - - def get_vlan_details(self, vlan_uuid_objects): - vlan_uuids = [vlan_uuid_object["id"] for vlan_uuid_object in vlan_uuid_objects] - vlan_details = {} - - vlan_uuids_query_params = "&".join(f"id={value}" for value in vlan_uuids) - vlan_url = f"/api/ipam/vlans/?{vlan_uuids_query_params}" - - vlans_response = self.api_client.make_api_request(url=vlan_url, paginated=True) - - for vlan_response in vlans_response: - vlan_group_url = vlan_response.get("vlan_group", {}).get("url") - - if vlan_group_url: - vlan_group_response = self.api_client.make_api_request( - url=vlan_group_url + url = "/api/plugins/undercloud-vni/ucvnis/" + ucvnis_response = self.api_client.make_api_request(url, paginated=True) + + for ucvni in ucvnis_response: + ucvni_id = ucvni.get("id") + if not ucvni_id: + continue + + # Get tenant ID + tenant = ucvni.get("tenant", {}) + tenant_id = tenant.get("id") if tenant else None + + self.add( + self.network( + id=ucvni_id, + name=ucvni.get("name", ""), + tenant_id=tenant_id, + ucvni_id=ucvni.get("ucvni_id"), ) - vlan_group_name = vlan_group_response.get("name") - vlan_id = vlan_response.get("vid") - - if vlan_group_name: - vlan_details.setdefault(vlan_group_name, []).append(vlan_id) - - return vlan_details + ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/network/models.py b/python/diff-nautobot-understack/diff_nautobot_understack/network/models.py index c80753e13..b9efefccb 100644 --- a/python/diff-nautobot-understack/diff_nautobot_understack/network/models.py +++ b/python/diff-nautobot-understack/diff_nautobot_understack/network/models.py @@ -2,17 +2,23 @@ class NetworkModel(DiffSyncModel): + """Model for comparing OpenStack networks with Nautobot UCVNIs. + + Fields match what's actually synced by neutron_network.sync_network_to_nautobot: + - name: Network name + - tenant_id: Project/tenant UUID + - ucvni_id: Provider segmentation ID (VXLAN VNI) + """ + _modelname = "network" _identifiers = ("id",) _attributes = ( "name", - "status", - "provider_physical_network", - "vni_id", + "tenant_id", + "ucvni_id", ) id: str name: str - status: str - provider_physical_network: str | None = None - vni_id: int | None = None + tenant_id: str | None = None + ucvni_id: int | None = None diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/nautobot_tenant.py b/python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/nautobot_tenant.py index 850becdb4..11272f50e 100644 --- a/python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/nautobot_tenant.py +++ b/python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/nautobot_tenant.py @@ -31,8 +31,8 @@ def load(self): self.add( self.project( id=_remove_hyphens(tenant.get("id")), - name=tenant.get("name"), - description=tenant.get("description"), + name=tenant.get("name", ""), + description=tenant.get("description") or "", ) ) @@ -57,7 +57,7 @@ def load(self): self.add( self.project( id=_remove_hyphens(tenant.get("id")), - name=tenant.get("name"), - description=tenant.get("description"), + name=tenant.get("name", ""), + description=tenant.get("description") or "", ) ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/openstack_project.py b/python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/openstack_project.py index 1cf5f0590..9acbad122 100644 --- a/python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/openstack_project.py +++ b/python/diff-nautobot-understack/diff_nautobot_understack/project/adapters/openstack_project.py @@ -8,6 +8,21 @@ logger = logging.getLogger(__name__) +def _get_tenant_name(cloud, project) -> str: + """Generate tenant name as domain:project_name to match sync logic.""" + domain_id = project.domain_id + + if domain_id == "default": + domain_name = "default" + elif domain_id: + domain = cloud.identity.get_domain(domain_id) + domain_name = domain.name if domain else "unknown" + else: + domain_name = "unknown" + + return f"{domain_name}:{project.name}" + + class Project(Adapter): project = models.ProjectModel @@ -25,11 +40,18 @@ def load(self): if not os_project: logger.error(f"Project '{self.project_name}' not found.") return + + # Skip domains - they are not synced to Nautobot + if getattr(os_project, "is_domain", False): + logger.debug(f"Skipping domain '{self.project_name}'") + return + + tenant_name = _get_tenant_name(self.cloud, os_project) self.add( self.project( id=os_project.id, - name=os_project.name, - description=os_project.description, + name=tenant_name, + description=os_project.description or "", ) ) @@ -47,10 +69,16 @@ def __init__(self, **kwargs): def load(self): for os_project in self.cloud.identity.projects(): + # Skip domains - they are not synced to Nautobot + if getattr(os_project, "is_domain", False): + logger.debug(f"Skipping domain '{os_project.name}'") + continue + + tenant_name = _get_tenant_name(self.cloud, os_project) self.add( self.project( id=os_project.id, - name=os_project.name, - description=os_project.description, + name=tenant_name, + description=os_project.description or "", ) ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/subnet/__init__.py b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/__init__.py new file mode 100644 index 000000000..692cd5d7e --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/__init__.py @@ -0,0 +1 @@ +# Subnet diff adapters diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/__init__.py b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/__init__.py new file mode 100644 index 000000000..ea76ac86e --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/__init__.py @@ -0,0 +1 @@ +# Subnet adapters diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/nautobot_prefix.py b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/nautobot_prefix.py new file mode 100644 index 000000000..b1bae15ac --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/nautobot_prefix.py @@ -0,0 +1,44 @@ +from diffsync import Adapter + +from diff_nautobot_understack.clients.nautobot import API +from diff_nautobot_understack.subnet import models + + +class Prefixes(Adapter): + """Adapter for Nautobot IPAM prefixes.""" + + subnet = models.SubnetModel + + top_level = ["subnet"] + type = "NautobotPrefix" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.api_client = API() + + def load(self): + url = "/api/ipam/prefixes/" + prefixes_response = self.api_client.make_api_request(url, paginated=True) + + for prefix in prefixes_response: + # Skip prefixes without a proper UUID (system-generated ones) + prefix_id = prefix.get("id") + if not prefix_id: + continue + + # Get namespace name - this maps to network_id for non-Global + namespace = prefix.get("namespace", {}) + namespace_name = namespace.get("name") if namespace else None + + # Get tenant ID + tenant = prefix.get("tenant", {}) + tenant_id = tenant.get("id") if tenant else None + + self.add( + self.subnet( + id=prefix_id, + cidr=prefix.get("prefix", ""), + network_id=namespace_name or "", + tenant_id=tenant_id, + ) + ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/openstack_subnet.py b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/openstack_subnet.py new file mode 100644 index 000000000..535573744 --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/adapters/openstack_subnet.py @@ -0,0 +1,29 @@ +from diffsync import Adapter + +from diff_nautobot_understack.clients.openstack import API +from diff_nautobot_understack.subnet import models + + +class Subnets(Adapter): + """Adapter for OpenStack Neutron subnets.""" + + subnet = models.SubnetModel + + top_level = ["subnet"] + type = "OpenstackSubnet" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + openstack_api = API() + self.cloud = openstack_api.cloud_connection + + def load(self): + for subnet in self.cloud.network.subnets(): + self.add( + self.subnet( + id=subnet.id, + cidr=subnet.cidr, + network_id=subnet.network_id, + tenant_id=subnet.project_id, + ) + ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/subnet/main.py b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/main.py new file mode 100644 index 000000000..6f846cd95 --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/main.py @@ -0,0 +1,18 @@ +from diffsync.diff import Diff +from diffsync.enum import DiffSyncFlags + +from diff_nautobot_understack.subnet.adapters.nautobot_prefix import Prefixes +from diff_nautobot_understack.subnet.adapters.openstack_subnet import Subnets + + +def openstack_subnets_diff_from_nautobot_prefixes() -> Diff: + """Compare all OpenStack subnets with Nautobot prefixes.""" + openstack_subnets = Subnets() + openstack_subnets.load() + + nautobot_prefixes = Prefixes() + nautobot_prefixes.load() + + return nautobot_prefixes.diff_from( + openstack_subnets, flags=DiffSyncFlags.CONTINUE_ON_FAILURE + ) diff --git a/python/diff-nautobot-understack/diff_nautobot_understack/subnet/models.py b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/models.py new file mode 100644 index 000000000..be132a797 --- /dev/null +++ b/python/diff-nautobot-understack/diff_nautobot_understack/subnet/models.py @@ -0,0 +1,18 @@ +from diffsync import DiffSyncModel + + +class SubnetModel(DiffSyncModel): + """Model for comparing OpenStack subnets with Nautobot prefixes.""" + + _modelname = "subnet" + _identifiers = ("id",) + _attributes = ( + "cidr", + "network_id", + "tenant_id", + ) + + id: str + cidr: str + network_id: str + tenant_id: str | None = None diff --git a/workflows/argo-events/workflowtemplates/diff-nautobot.yaml b/workflows/argo-events/workflowtemplates/diff-nautobot.yaml new file mode 100644 index 000000000..9a5ca3175 --- /dev/null +++ b/workflows/argo-events/workflowtemplates/diff-nautobot.yaml @@ -0,0 +1,173 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: diff-nautobot + annotations: + workflows.argoproj.io/title: Diff OpenStack and Nautobot (Dry-Run) + workflows.argoproj.io/description: | + Shows differences between OpenStack (Keystone, Neutron, Ironic) and Nautobot + without making any changes. Use this as a dry-run before running + resync-nautobot. + + Usage: + ``` + argo -n argo-events submit --from workflowtemplate/diff-nautobot + ``` + + To actually apply changes, use: + ``` + argo -n argo-events submit --from workflowtemplate/resync-nautobot + ``` + + Defined in `workflows/argo-events/workflowtemplates/diff-nautobot.yaml` +spec: + entrypoint: main + serviceAccountName: workflow + templates: + - name: main + steps: + - - name: diff-projects + template: diff-projects + continueOn: + failed: true + - - name: diff-networks + template: diff-networks + continueOn: + failed: true + - - name: diff-subnets + template: diff-subnets + continueOn: + failed: true + - - name: diff-devices + template: diff-devices + continueOn: + failed: true + + - name: diff-projects + container: + image: ghcr.io/rackerlabs/understack/diff-nautobot-understack:latest + command: + - uc-diff + - projects + volumeMounts: + - mountPath: /etc/nb-token/ + name: nb-token + readOnly: true + - mountPath: /etc/openstack + name: baremetal-manage + readOnly: true + env: + - name: NAUTOBOT_TOKEN + valueFrom: + secretKeyRef: + name: nautobot-token + key: token + - name: NAUTOBOT_URL + valueFrom: + configMapKeyRef: + name: cluster-metadata + key: nautobot_url + - name: OS_CLIENT_CONFIG_FILE + value: /etc/openstack/clouds.yaml + - name: OS_CLOUD + value: understack + + - name: diff-networks + container: + image: ghcr.io/rackerlabs/understack/diff-nautobot-understack:latest + command: + - uc-diff + - network + volumeMounts: + - mountPath: /etc/nb-token/ + name: nb-token + readOnly: true + - mountPath: /etc/openstack + name: baremetal-manage + readOnly: true + env: + - name: NAUTOBOT_TOKEN + valueFrom: + secretKeyRef: + name: nautobot-token + key: token + - name: NAUTOBOT_URL + valueFrom: + configMapKeyRef: + name: cluster-metadata + key: nautobot_url + - name: OS_CLIENT_CONFIG_FILE + value: /etc/openstack/clouds.yaml + - name: OS_CLOUD + value: understack + + - name: diff-subnets + container: + image: ghcr.io/rackerlabs/understack/diff-nautobot-understack:latest + command: + - uc-diff + - subnets + volumeMounts: + - mountPath: /etc/nb-token/ + name: nb-token + readOnly: true + - mountPath: /etc/openstack + name: baremetal-manage + readOnly: true + env: + - name: NAUTOBOT_TOKEN + valueFrom: + secretKeyRef: + name: nautobot-token + key: token + - name: NAUTOBOT_URL + valueFrom: + configMapKeyRef: + name: cluster-metadata + key: nautobot_url + - name: OS_CLIENT_CONFIG_FILE + value: /etc/openstack/clouds.yaml + - name: OS_CLOUD + value: understack + + - name: diff-devices + container: + image: ghcr.io/rackerlabs/understack/diff-nautobot-understack:latest + command: + - uc-diff + - devices + volumeMounts: + - mountPath: /etc/nb-token/ + name: nb-token + readOnly: true + - mountPath: /etc/openstack + name: baremetal-manage + readOnly: true + env: + - name: NAUTOBOT_TOKEN + valueFrom: + secretKeyRef: + name: nautobot-token + key: token + - name: NAUTOBOT_URL + valueFrom: + configMapKeyRef: + name: cluster-metadata + key: nautobot_url + - name: OS_CLIENT_CONFIG_FILE + value: /etc/openstack/clouds.yaml + - name: OS_CLOUD + value: understack + + volumeClaimTemplates: [] + volumes: + - name: nb-token + secret: + secretName: nautobot-token + - name: baremetal-manage + secret: + secretName: baremetal-manage + items: + - key: clouds.yaml + path: clouds.yaml