From 89edb59c0d1fd56d3df5e13d3a11e6e5bcc61d01 Mon Sep 17 00:00:00 2001 From: haseeb Date: Fri, 27 Feb 2026 23:27:42 +0530 Subject: [PATCH 1/2] Add resync-keystone-nautobot to sync tenants before devices Added resync-keystone-nautobot workflow to sync Keystone projects as Nautobot tenants, ensuring tenants exist before devices reference them. --- python/understack-workflows/pyproject.toml | 1 + .../tests/test_resync_ironic_to_nautobot.py | 42 +--- .../tests/test_resync_keystone_to_nautobot.py | 214 ++++++++++++++++++ .../main/resync_ironic_to_nautobot.py | 50 +--- .../main/resync_keystone_to_nautobot.py | 87 +++++++ .../main/sync_keystone.py | 4 +- .../understack_workflows/resync.py | 102 +++++++++ .../cronworkflows/resync-ironic-nautobot.yaml | 26 --- .../cronworkflows/resync-nautobot.yaml | 29 +++ workflows/argo-events/kustomization.yaml | 4 +- .../resync-ironic-nautobot.yaml | 60 ----- .../workflowtemplates/resync-nautobot.yaml | 94 ++++++++ 12 files changed, 543 insertions(+), 170 deletions(-) create mode 100644 python/understack-workflows/tests/test_resync_keystone_to_nautobot.py create mode 100644 python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py create mode 100644 python/understack-workflows/understack_workflows/resync.py delete mode 100644 workflows/argo-events/cronworkflows/resync-ironic-nautobot.yaml create mode 100644 workflows/argo-events/cronworkflows/resync-nautobot.yaml delete mode 100644 workflows/argo-events/workflowtemplates/resync-ironic-nautobot.yaml create mode 100644 workflows/argo-events/workflowtemplates/resync-nautobot.yaml diff --git a/python/understack-workflows/pyproject.toml b/python/understack-workflows/pyproject.toml index c7c9d9f3c..73e337ef8 100644 --- a/python/understack-workflows/pyproject.toml +++ b/python/understack-workflows/pyproject.toml @@ -39,6 +39,7 @@ netapp-configure-interfaces = "understack_workflows.main.netapp_configure_net:ma netapp-create-svm = "understack_workflows.main.netapp_create_svm:main" openstack-oslo-event = "understack_workflows.main.openstack_oslo_event:main" resync-ironic-nautobot = "understack_workflows.main.resync_ironic_to_nautobot:main" +resync-keystone-nautobot = "understack_workflows.main.resync_keystone_to_nautobot:main" sync-keystone = "understack_workflows.main.sync_keystone:main" sync-network-segment-range = "understack_workflows.main.sync_ucvni_group_range:main" undersync-switch = "understack_workflows.main.undersync_switch:main" diff --git a/python/understack-workflows/tests/test_resync_ironic_to_nautobot.py b/python/understack-workflows/tests/test_resync_ironic_to_nautobot.py index 22bd1e9ad..bbec13590 100644 --- a/python/understack-workflows/tests/test_resync_ironic_to_nautobot.py +++ b/python/understack-workflows/tests/test_resync_ironic_to_nautobot.py @@ -3,32 +3,10 @@ from unittest.mock import MagicMock from unittest.mock import patch -from understack_workflows.main.resync_ironic_to_nautobot import SyncResult from understack_workflows.main.resync_ironic_to_nautobot import argument_parser from understack_workflows.main.resync_ironic_to_nautobot import main from understack_workflows.main.resync_ironic_to_nautobot import sync_nodes - - -class TestSyncResult: - """Test cases for SyncResult dataclass.""" - - def test_defaults(self): - result = SyncResult() - assert result.total == 0 - assert result.failed == 0 - assert result.succeeded == 0 - - def test_succeeded_calculation(self): - result = SyncResult(total=10, failed=3) - assert result.succeeded == 7 - - def test_all_failed(self): - result = SyncResult(total=5, failed=5) - assert result.succeeded == 0 - - def test_none_failed(self): - result = SyncResult(total=5, failed=0) - assert result.succeeded == 5 +from understack_workflows.resync import SyncResult class TestArgumentParser: @@ -128,16 +106,11 @@ class TestMain: """Test cases for main function.""" @patch("understack_workflows.main.resync_ironic_to_nautobot.sync_nodes") - @patch("understack_workflows.main.resync_ironic_to_nautobot.pynautobot") - @patch("understack_workflows.main.resync_ironic_to_nautobot.credential") + @patch("understack_workflows.main.resync_ironic_to_nautobot.get_nautobot_client") @patch("understack_workflows.main.resync_ironic_to_nautobot.setup_logger") @patch("understack_workflows.main.resync_ironic_to_nautobot.argument_parser") - def test_main_success( - self, mock_parser, mock_logger, mock_cred, mock_pynb, mock_sync - ): + def test_main_success(self, mock_parser, mock_logger, mock_get_nb, mock_sync): mock_args = MagicMock() - mock_args.nautobot_token = "token" - mock_args.nautobot_url = "http://nautobot" mock_args.node = None mock_args.dry_run = False mock_parser.return_value.parse_args.return_value = mock_args @@ -148,16 +121,11 @@ def test_main_success( assert result == 0 @patch("understack_workflows.main.resync_ironic_to_nautobot.sync_nodes") - @patch("understack_workflows.main.resync_ironic_to_nautobot.pynautobot") - @patch("understack_workflows.main.resync_ironic_to_nautobot.credential") + @patch("understack_workflows.main.resync_ironic_to_nautobot.get_nautobot_client") @patch("understack_workflows.main.resync_ironic_to_nautobot.setup_logger") @patch("understack_workflows.main.resync_ironic_to_nautobot.argument_parser") - def test_main_with_failures( - self, mock_parser, mock_logger, mock_cred, mock_pynb, mock_sync - ): + def test_main_with_failures(self, mock_parser, mock_logger, mock_get_nb, mock_sync): mock_args = MagicMock() - mock_args.nautobot_token = "token" - mock_args.nautobot_url = "http://nautobot" mock_args.node = None mock_args.dry_run = False mock_parser.return_value.parse_args.return_value = mock_args diff --git a/python/understack-workflows/tests/test_resync_keystone_to_nautobot.py b/python/understack-workflows/tests/test_resync_keystone_to_nautobot.py new file mode 100644 index 000000000..721180b40 --- /dev/null +++ b/python/understack-workflows/tests/test_resync_keystone_to_nautobot.py @@ -0,0 +1,214 @@ +"""Tests for resync_keystone_to_nautobot module.""" + +from unittest.mock import MagicMock +from unittest.mock import patch + +from understack_workflows.main.resync_keystone_to_nautobot import argument_parser +from understack_workflows.main.resync_keystone_to_nautobot import main +from understack_workflows.main.resync_keystone_to_nautobot import sync_projects +from understack_workflows.main.sync_keystone import is_domain +from understack_workflows.resync import SyncResult + + +class TestIsDomain: + """Test cases for is_domain helper function.""" + + def test_is_domain_true(self): + project = MagicMock() + project.is_domain = True + assert is_domain(project) is True + + def test_is_domain_false(self): + project = MagicMock() + project.is_domain = False + assert is_domain(project) is False + + def test_is_domain_missing_attr(self): + project = MagicMock(spec=[]) + assert is_domain(project) is False + + +class TestArgumentParser: + """Test cases for argument_parser function.""" + + def test_default_args(self): + parser = argument_parser() + args = parser.parse_args([]) + assert args.project is None + assert args.dry_run is False + + def test_project_arg(self): + parser = argument_parser() + args = parser.parse_args(["--project", "test-uuid"]) + assert args.project == "test-uuid" + + def test_dry_run_arg(self): + parser = argument_parser() + args = parser.parse_args(["--dry-run"]) + assert args.dry_run is True + + +class TestSyncProjects: + """Test cases for sync_projects function.""" + + def test_sync_all_projects_success(self): + conn = MagicMock() + project1 = MagicMock( + id="12345678-1234-5678-1234-567812345678", + name="project-1", + is_domain=False, + ) + project2 = MagicMock( + id="87654321-4321-8765-4321-876543218765", + name="project-2", + is_domain=False, + ) + conn.identity.projects.return_value = [project1, project2] + + nautobot = MagicMock() + + with patch( + "understack_workflows.main.resync_keystone_to_nautobot.handle_project_update" + ) as mock_update: + mock_update.return_value = 0 + result = sync_projects(conn, nautobot) + + assert result.total == 2 + assert result.failed == 0 + assert result.skipped == 0 + assert mock_update.call_count == 2 + + def test_sync_single_project(self): + conn = MagicMock() + project = MagicMock( + id="12345678-1234-5678-1234-567812345678", + name="project-1", + is_domain=False, + ) + conn.identity.get_project.return_value = project + + nautobot = MagicMock() + + with patch( + "understack_workflows.main.resync_keystone_to_nautobot.handle_project_update" + ) as mock_update: + mock_update.return_value = 0 + result = sync_projects( + conn, nautobot, project_uuid="12345678-1234-5678-1234-567812345678" + ) + + assert result.total == 1 + assert result.failed == 0 + conn.identity.get_project.assert_called_once_with( + "12345678-1234-5678-1234-567812345678" + ) + + def test_sync_with_failures(self): + conn = MagicMock() + project1 = MagicMock( + id="12345678-1234-5678-1234-567812345678", + name="project-1", + is_domain=False, + ) + project2 = MagicMock( + id="87654321-4321-8765-4321-876543218765", + name="project-2", + is_domain=False, + ) + conn.identity.projects.return_value = [project1, project2] + + nautobot = MagicMock() + + with patch( + "understack_workflows.main.resync_keystone_to_nautobot.handle_project_update" + ) as mock_update: + mock_update.side_effect = [0, 1] # First succeeds, second fails + result = sync_projects(conn, nautobot) + + assert result.total == 2 + assert result.failed == 1 + assert result.succeeded == 1 + + def test_sync_skips_domains(self): + conn = MagicMock() + project = MagicMock( + id="12345678-1234-5678-1234-567812345678", + name="project-1", + is_domain=False, + ) + domain = MagicMock( + id="87654321-4321-8765-4321-876543218765", + name="domain-1", + is_domain=True, + ) + conn.identity.projects.return_value = [project, domain] + + nautobot = MagicMock() + + with patch( + "understack_workflows.main.resync_keystone_to_nautobot.handle_project_update" + ) as mock_update: + mock_update.return_value = 0 + result = sync_projects(conn, nautobot) + + assert result.total == 2 + assert result.skipped == 1 + assert result.succeeded == 1 + mock_update.assert_called_once() + + def test_dry_run_skips_sync(self): + conn = MagicMock() + project = MagicMock(id="uuid-1", name="project-1", is_domain=False) + conn.identity.projects.return_value = [project] + + nautobot = MagicMock() + + with patch( + "understack_workflows.main.resync_keystone_to_nautobot.handle_project_update" + ) as mock_update: + result = sync_projects(conn, nautobot, dry_run=True) + + assert result.total == 1 + assert result.failed == 0 + mock_update.assert_not_called() + + +class TestMain: + """Test cases for main function.""" + + @patch("understack_workflows.main.resync_keystone_to_nautobot.sync_projects") + @patch("understack_workflows.main.resync_keystone_to_nautobot.get_nautobot_client") + @patch("understack_workflows.main.resync_keystone_to_nautobot.get_openstack_client") + @patch("understack_workflows.main.resync_keystone_to_nautobot.setup_logger") + @patch("understack_workflows.main.resync_keystone_to_nautobot.argument_parser") + def test_main_success( + self, mock_parser, mock_logger, mock_get_os, mock_get_nb, mock_sync + ): + mock_args = MagicMock() + mock_args.project = None + mock_args.dry_run = False + mock_parser.return_value.parse_args.return_value = mock_args + mock_sync.return_value = SyncResult(total=5, failed=0) + + result = main() + + assert result == 0 + mock_get_os.assert_called_once_with() + + @patch("understack_workflows.main.resync_keystone_to_nautobot.sync_projects") + @patch("understack_workflows.main.resync_keystone_to_nautobot.get_nautobot_client") + @patch("understack_workflows.main.resync_keystone_to_nautobot.get_openstack_client") + @patch("understack_workflows.main.resync_keystone_to_nautobot.setup_logger") + @patch("understack_workflows.main.resync_keystone_to_nautobot.argument_parser") + def test_main_with_failures( + self, mock_parser, mock_logger, mock_get_os, mock_get_nb, mock_sync + ): + mock_args = MagicMock() + mock_args.project = None + mock_args.dry_run = False + mock_parser.return_value.parse_args.return_value = mock_args + mock_sync.return_value = SyncResult(total=5, failed=2) + + result = main() + + assert result == 1 diff --git a/python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py b/python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py index bfa45e494..7cd4f3393 100644 --- a/python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py +++ b/python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py @@ -8,44 +8,23 @@ import argparse import logging -from dataclasses import dataclass import pynautobot -from understack_workflows.helpers import credential -from understack_workflows.helpers import parser_nautobot_args from understack_workflows.helpers import setup_logger from understack_workflows.ironic.client import IronicClient from understack_workflows.oslo_event.nautobot_device_sync import sync_device_to_nautobot +from understack_workflows.resync import SyncResult +from understack_workflows.resync import get_nautobot_client +from understack_workflows.resync import log_sync_result +from understack_workflows.resync import parser_resync_args logger = logging.getLogger(__name__) -_EXIT_SUCCESS = 0 -_EXIT_SYNC_FAILURES = 1 - - -@dataclass -class SyncResult: - """Result of a sync operation.""" - - total: int = 0 - failed: int = 0 - - @property - def succeeded(self) -> int: - return self.total - self.failed - def argument_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Resync Ironic nodes to Nautobot") - parser.add_argument( - "--node", type=str, help="Sync specific node UUID (default: all nodes)" - ) - parser.add_argument( - "--dry-run", action="store_true", help="List nodes without syncing" - ) - parser = parser_nautobot_args(parser) - return parser + return parser_resync_args(parser, item_name="node", item_flag="--node") def sync_nodes( @@ -86,22 +65,7 @@ def main() -> int: setup_logger(level=logging.INFO) args = argument_parser().parse_args() - nb_token = args.nautobot_token or credential("nb-token", "token") - nautobot = pynautobot.api(args.nautobot_url, token=nb_token) - + nautobot = get_nautobot_client(args) result = sync_nodes(nautobot, args.node or None, args.dry_run) - if args.dry_run: - logger.info("Dry run complete. %d nodes would be synced.", result.total) - else: - logger.info( - "Sync complete. %d/%d nodes synced successfully.", - result.succeeded, - result.total, - ) - - if result.failed: - logger.error("Failed to sync %d nodes", result.failed) - return _EXIT_SYNC_FAILURES - - return _EXIT_SUCCESS + return log_sync_result(result, "node", args.dry_run) diff --git a/python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py b/python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py new file mode 100644 index 000000000..ccba5e9e8 --- /dev/null +++ b/python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py @@ -0,0 +1,87 @@ +"""Resync Keystone projects to Nautobot tenants. + +Use when Nautobot gets out of sync with Keystone, e.g., after: +- Nautobot database restore +- Missed events +- Manual Nautobot changes +""" + +import argparse +import logging +import uuid + +import pynautobot + +from understack_workflows.helpers import setup_logger +from understack_workflows.main.sync_keystone import handle_project_update +from understack_workflows.main.sync_keystone import is_domain +from understack_workflows.openstack.client import Connection +from understack_workflows.openstack.client import get_openstack_client +from understack_workflows.resync import SyncResult +from understack_workflows.resync import get_nautobot_client +from understack_workflows.resync import log_sync_result +from understack_workflows.resync import parser_resync_args + +logger = logging.getLogger(__name__) + + +def argument_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Resync Keystone projects to Nautobot tenants" + ) + return parser_resync_args(parser, item_name="project", item_flag="--project") + + +def sync_projects( + conn: Connection, + nautobot: pynautobot.api, + project_uuid: str | None = None, + dry_run: bool = False, +) -> SyncResult: + """Sync Keystone projects to Nautobot tenants. + + Args: + conn: OpenStack connection + nautobot: Nautobot API instance + project_uuid: Optional specific project UUID to sync (syncs all if None) + dry_run: If True, only log what would be synced + + Returns: + SyncResult with total, failed, and skipped counts + """ + result = SyncResult() + + if project_uuid: + projects = [conn.identity.get_project(project_uuid)] # pyright: ignore[reportAttributeAccessIssue] + else: + projects = list(conn.identity.projects()) # pyright: ignore[reportAttributeAccessIssue] + + for project in projects: + result.total += 1 + + if is_domain(project): + logger.debug("Skipping domain: %s (%s)", project.id, project.name) + result.skipped += 1 + continue + + if dry_run: + logger.info("Would sync project: %s (%s)", project.id, project.name) + continue + + logger.info("Syncing project: %s (%s)", project.id, project.name) + if handle_project_update(conn, nautobot, uuid.UUID(project.id)) != 0: + result.failed += 1 + logger.error("Failed to sync project %s", project.id) + + return result + + +def main() -> int: + setup_logger(level=logging.INFO) + args = argument_parser().parse_args() + + conn = get_openstack_client() + nautobot = get_nautobot_client(args) + result = sync_projects(conn, nautobot, args.project or None, args.dry_run) + + return log_sync_result(result, "project", args.dry_run) diff --git a/python/understack-workflows/understack_workflows/main/sync_keystone.py b/python/understack-workflows/understack_workflows/main/sync_keystone.py index 6ab7bfcc4..76e8dfa36 100644 --- a/python/understack-workflows/understack_workflows/main/sync_keystone.py +++ b/python/understack-workflows/understack_workflows/main/sync_keystone.py @@ -54,7 +54,7 @@ def _get_project(conn: Connection, project_id: uuid.UUID) -> Project: return conn.identity.get_project(project_id.hex) # type: ignore -def _is_domain(project: Project) -> bool: +def is_domain(project: Project) -> bool: """Check if a project is actually a domain. Returns True if the project is a domain, False otherwise. @@ -127,7 +127,7 @@ def handle_project_update( logger.info("got request to update tenant %s", project_id.hex) project = _get_project(conn, project_id) - if _is_domain(project): + if is_domain(project): logger.info( "Skipping domain %s - domains are not synced to Nautobot", project_id.hex ) diff --git a/python/understack-workflows/understack_workflows/resync.py b/python/understack-workflows/understack_workflows/resync.py new file mode 100644 index 000000000..5c4d20ef5 --- /dev/null +++ b/python/understack-workflows/understack_workflows/resync.py @@ -0,0 +1,102 @@ +"""Shared utilities for resync operations. + +Common patterns for resyncing data from various sources to Nautobot. +""" + +import argparse +import logging +from dataclasses import dataclass + +import pynautobot + +from understack_workflows.helpers import credential +from understack_workflows.helpers import parser_nautobot_args + +logger = logging.getLogger(__name__) + +EXIT_SUCCESS = 0 +EXIT_SYNC_FAILURES = 1 + + +@dataclass +class SyncResult: + """Result of a sync operation.""" + + total: int = 0 + failed: int = 0 + skipped: int = 0 + + @property + def succeeded(self) -> int: + return self.total - self.failed - self.skipped + + +def parser_resync_args( + parser: argparse.ArgumentParser, + item_name: str = "item", + item_flag: str = "--item", +) -> argparse.ArgumentParser: + """Add common resync arguments to a parser. + + Args: + parser: ArgumentParser to add arguments to + item_name: Name of the item being synced (for help text) + item_flag: Flag name for specifying a single item + + Returns: + The parser with added arguments + """ + parser.add_argument( + item_flag, + type=str, + help=f"Sync specific {item_name} UUID (default: all {item_name}s)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help=f"List {item_name}s without syncing", + ) + return parser_nautobot_args(parser) + + +def get_nautobot_client(args: argparse.Namespace) -> pynautobot.api: + """Create a Nautobot API client from parsed arguments.""" + nb_token = args.nautobot_token or credential("nb-token", "token") + return pynautobot.api(args.nautobot_url, token=nb_token) + + +def log_sync_result( + result: SyncResult, + item_name: str, + dry_run: bool = False, +) -> int: + """Log sync results and return appropriate exit code. + + Args: + result: SyncResult from the sync operation + item_name: Name of the item type (e.g., "node", "project") + dry_run: Whether this was a dry run + + Returns: + Exit code (EXIT_SUCCESS or EXIT_SYNC_FAILURES) + """ + if dry_run: + count = result.total - result.skipped + msg = f"Dry run complete. {count} {item_name}s would be synced" + if result.skipped: + msg += f" ({result.skipped} skipped)" + logger.info("%s.", msg) + else: + msg = ( + f"Sync complete. {result.succeeded}/{result.total} " + f"{item_name}s synced successfully" + ) + if result.skipped: + msg += f" ({result.skipped} skipped)" + logger.info("%s.", msg) + + if result.failed: + logger.error("Failed to sync %d %ss", result.failed, item_name) + return EXIT_SYNC_FAILURES + + return EXIT_SUCCESS diff --git a/workflows/argo-events/cronworkflows/resync-ironic-nautobot.yaml b/workflows/argo-events/cronworkflows/resync-ironic-nautobot.yaml deleted file mode 100644 index baa03d9a3..000000000 --- a/workflows/argo-events/cronworkflows/resync-ironic-nautobot.yaml +++ /dev/null @@ -1,26 +0,0 @@ ---- -apiVersion: argoproj.io/v1alpha1 -kind: CronWorkflow -metadata: - name: resync-ironic-nautobot - annotations: - workflows.argoproj.io/title: Scheduled resync of Ironic nodes to Nautobot - workflows.argoproj.io/description: | - Periodically resyncs all Ironic nodes to Nautobot to catch any drift. - Runs daily at 2:00 AM UTC by default. - - To manually trigger: - ``` - argo -n argo-events submit --from cronworkflow/resync-ironic-nautobot - ``` - - Defined in `workflows/argo-events/cronworkflows/resync-ironic-nautobot.yaml` -spec: - schedule: "0 2 * * *" - timezone: "UTC" - concurrencyPolicy: "Forbid" - successfulJobsHistoryLimit: 3 - failedJobsHistoryLimit: 3 - workflowSpec: - workflowTemplateRef: - name: resync-ironic-nautobot diff --git a/workflows/argo-events/cronworkflows/resync-nautobot.yaml b/workflows/argo-events/cronworkflows/resync-nautobot.yaml new file mode 100644 index 000000000..01d570f02 --- /dev/null +++ b/workflows/argo-events/cronworkflows/resync-nautobot.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: CronWorkflow +metadata: + name: resync-nautobot + annotations: + workflows.argoproj.io/title: Scheduled resync of Keystone and Ironic to Nautobot + workflows.argoproj.io/description: | + Periodically resyncs Keystone projects and Ironic nodes to Nautobot to + catch any drift. Runs daily at 2:00 AM UTC by default. + + Keystone projects are synced first (tenants must exist before devices can + reference them), then Ironic nodes. + + To manually trigger: + ``` + argo -n argo-events submit --from cronworkflow/resync-nautobot + ``` + + Defined in `workflows/argo-events/cronworkflows/resync-nautobot.yaml` +spec: + schedule: "0 2 * * *" + timezone: "UTC" + concurrencyPolicy: "Forbid" + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 3 + workflowSpec: + workflowTemplateRef: + name: resync-nautobot diff --git a/workflows/argo-events/kustomization.yaml b/workflows/argo-events/kustomization.yaml index a64e56edf..463da110e 100644 --- a/workflows/argo-events/kustomization.yaml +++ b/workflows/argo-events/kustomization.yaml @@ -25,9 +25,9 @@ resources: - sensors/alertmanager-webhook-sensor.yaml - eventsources/alertmanager-webhook-eventsource.yaml - workflowtemplates/alert-automation-neutron-agent-down.yaml - - workflowtemplates/resync-ironic-nautobot.yaml + - workflowtemplates/resync-nautobot.yaml # CronWorkflows - - cronworkflows/resync-ironic-nautobot.yaml + - cronworkflows/resync-nautobot.yaml helmCharts: - name: nautobot-token diff --git a/workflows/argo-events/workflowtemplates/resync-ironic-nautobot.yaml b/workflows/argo-events/workflowtemplates/resync-ironic-nautobot.yaml deleted file mode 100644 index 9708cd04b..000000000 --- a/workflows/argo-events/workflowtemplates/resync-ironic-nautobot.yaml +++ /dev/null @@ -1,60 +0,0 @@ ---- -apiVersion: argoproj.io/v1alpha1 -metadata: - name: resync-ironic-nautobot - annotations: - workflows.argoproj.io/title: Resync Ironic nodes to Nautobot - workflows.argoproj.io/description: | - Resyncs Ironic node data to Nautobot. Use when Nautobot gets out of sync - with Ironic, e.g., after database restore, missed events, or manual changes. - - To resync all nodes: - ``` - argo -n argo-events submit --from workflowtemplate/resync-ironic-nautobot - ``` - - To resync a specific node: - ``` - argo -n argo-events submit --from workflowtemplate/resync-ironic-nautobot \ - -p node="" - ``` - - Defined in `workflows/argo-events/workflowtemplates/resync-ironic-nautobot.yaml` -kind: WorkflowTemplate -spec: - entrypoint: main - serviceAccountName: workflow - arguments: - parameters: - - name: node - value: "" # empty = all nodes - templates: - - name: main - container: - image: ghcr.io/rackerlabs/understack/ironic-nautobot-client:latest - command: - - resync-ironic-nautobot - args: - - "--node" - - "{{workflow.parameters.node}}" - volumeMounts: - - mountPath: /etc/nb-token/ - name: nb-token - readOnly: true - - mountPath: /etc/openstack - name: baremetal-manage - readOnly: true - envFrom: - - configMapRef: - name: cluster-metadata - optional: false - volumes: - - name: nb-token - secret: - secretName: nautobot-token - - name: baremetal-manage - secret: - secretName: baremetal-manage - items: - - key: clouds.yaml - path: clouds.yaml diff --git a/workflows/argo-events/workflowtemplates/resync-nautobot.yaml b/workflows/argo-events/workflowtemplates/resync-nautobot.yaml new file mode 100644 index 000000000..554520696 --- /dev/null +++ b/workflows/argo-events/workflowtemplates/resync-nautobot.yaml @@ -0,0 +1,94 @@ +--- +apiVersion: argoproj.io/v1alpha1 +kind: WorkflowTemplate +metadata: + name: resync-nautobot + annotations: + workflows.argoproj.io/title: Resync Keystone and Ironic to Nautobot + workflows.argoproj.io/description: | + Resyncs Keystone projects and Ironic nodes to Nautobot. Use when Nautobot + gets out of sync, e.g., after database restore, missed events, or manual changes. + + Keystone projects are synced first (tenants must exist before devices can + reference them), then Ironic nodes. + + To resync all: + ``` + argo -n argo-events submit --from workflowtemplate/resync-nautobot + ``` + + To resync specific items: + ``` + argo -n argo-events submit --from workflowtemplate/resync-nautobot \ + -p project="" -p node="" + ``` + + Defined in `workflows/argo-events/workflowtemplates/resync-nautobot.yaml` +spec: + entrypoint: main + serviceAccountName: workflow + arguments: + parameters: + - name: project + value: "" # empty = all projects + - name: node + value: "" # empty = all nodes + templates: + - name: main + steps: + - - name: resync-keystone + template: resync-keystone + - - name: resync-ironic + template: resync-ironic + + - name: resync-keystone + container: + image: ghcr.io/rackerlabs/understack/ironic-nautobot-client:pr-1710 + command: + - resync-keystone-nautobot + args: + - "--project" + - "{{workflow.parameters.project}}" + volumeMounts: + - mountPath: /etc/nb-token/ + name: nb-token + readOnly: true + - mountPath: /etc/openstack + name: baremetal-manage + readOnly: true + envFrom: + - configMapRef: + name: cluster-metadata + optional: false + + - name: resync-ironic + container: + image: ghcr.io/rackerlabs/understack/ironic-nautobot-client:pr-1710 + command: + - resync-ironic-nautobot + args: + - "--node" + - "{{workflow.parameters.node}}" + volumeMounts: + - mountPath: /etc/nb-token/ + name: nb-token + readOnly: true + - mountPath: /etc/openstack + name: baremetal-manage + readOnly: true + envFrom: + - configMapRef: + name: cluster-metadata + optional: false + + volumeClaimTemplates: [] + volumes: + - name: nb-token + secret: + secretName: nautobot-token + - name: baremetal-manage + secret: + secretName: baremetal-manage + items: + - key: clouds.yaml + path: clouds.yaml From 259b1e999522a819dac8dbb3c40063a2a160b961 Mon Sep 17 00:00:00 2001 From: haseeb Date: Sat, 28 Feb 2026 21:09:40 +0530 Subject: [PATCH 2/2] Syncs UCVNIs, IPAM namespaces, and prefixes Adds ability to run Neutron resync before Ironic resync to ensure IPAM data exists in Nautobot. --- python/understack-workflows/pyproject.toml | 1 + .../main/resync_ironic_to_nautobot.py | 32 ++---- .../main/resync_keystone_to_nautobot.py | 41 ++------ .../main/resync_neutron_to_nautobot.py | 97 +++++++++++++++++++ .../oslo_event/neutron_network.py | 92 +++++++++++++++++- .../oslo_event/neutron_subnet.py | 72 ++++++++++---- .../understack_workflows/resync.py | 73 ++------------ .../workflowtemplates/resync-nautobot.yaml | 31 +++--- 8 files changed, 283 insertions(+), 156 deletions(-) create mode 100644 python/understack-workflows/understack_workflows/main/resync_neutron_to_nautobot.py diff --git a/python/understack-workflows/pyproject.toml b/python/understack-workflows/pyproject.toml index 73e337ef8..654128643 100644 --- a/python/understack-workflows/pyproject.toml +++ b/python/understack-workflows/pyproject.toml @@ -40,6 +40,7 @@ netapp-create-svm = "understack_workflows.main.netapp_create_svm:main" openstack-oslo-event = "understack_workflows.main.openstack_oslo_event:main" resync-ironic-nautobot = "understack_workflows.main.resync_ironic_to_nautobot:main" resync-keystone-nautobot = "understack_workflows.main.resync_keystone_to_nautobot:main" +resync-neutron-nautobot = "understack_workflows.main.resync_neutron_to_nautobot:main" sync-keystone = "understack_workflows.main.sync_keystone:main" sync-network-segment-range = "understack_workflows.main.sync_ucvni_group_range:main" undersync-switch = "understack_workflows.main.undersync_switch:main" diff --git a/python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py b/python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py index 7cd4f3393..af3f1e116 100644 --- a/python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py +++ b/python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py @@ -14,45 +14,27 @@ from understack_workflows.helpers import setup_logger from understack_workflows.ironic.client import IronicClient from understack_workflows.oslo_event.nautobot_device_sync import sync_device_to_nautobot +from understack_workflows.helpers import parser_nautobot_args from understack_workflows.resync import SyncResult from understack_workflows.resync import get_nautobot_client from understack_workflows.resync import log_sync_result -from understack_workflows.resync import parser_resync_args logger = logging.getLogger(__name__) def argument_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Resync Ironic nodes to Nautobot") - return parser_resync_args(parser, item_name="node", item_flag="--node") + return parser_nautobot_args(parser) -def sync_nodes( - nautobot: pynautobot.api, - node_uuid: str | None = None, - dry_run: bool = False, -) -> SyncResult: - """Sync Ironic nodes to Nautobot. - - Args: - nautobot: Nautobot API instance - node_uuid: Optional specific node UUID to sync (syncs all if None) - dry_run: If True, only log what would be synced - - Returns: - SyncResult with total and failed counts - """ +def sync_nodes(nautobot: pynautobot.api) -> SyncResult: + """Sync Ironic nodes to Nautobot.""" ironic = IronicClient() - nodes = [ironic.get_node(node_uuid)] if node_uuid else ironic.list_nodes() + nodes = ironic.list_nodes() result = SyncResult() for node in nodes: result.total += 1 - - if dry_run: - logger.info("Would sync node: %s (%s)", node.uuid, node.name) - continue - logger.info("Syncing node: %s (%s)", node.uuid, node.name) if sync_device_to_nautobot(node.uuid, nautobot) != 0: result.failed += 1 @@ -66,6 +48,6 @@ def main() -> int: args = argument_parser().parse_args() nautobot = get_nautobot_client(args) - result = sync_nodes(nautobot, args.node or None, args.dry_run) + result = sync_nodes(nautobot) - return log_sync_result(result, "node", args.dry_run) + return log_sync_result(result, "node") diff --git a/python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py b/python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py index ccba5e9e8..01389ec45 100644 --- a/python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py +++ b/python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py @@ -17,10 +17,10 @@ from understack_workflows.main.sync_keystone import is_domain from understack_workflows.openstack.client import Connection from understack_workflows.openstack.client import get_openstack_client +from understack_workflows.helpers import parser_nautobot_args from understack_workflows.resync import SyncResult from understack_workflows.resync import get_nautobot_client from understack_workflows.resync import log_sync_result -from understack_workflows.resync import parser_resync_args logger = logging.getLogger(__name__) @@ -29,32 +29,13 @@ def argument_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Resync Keystone projects to Nautobot tenants" ) - return parser_resync_args(parser, item_name="project", item_flag="--project") - - -def sync_projects( - conn: Connection, - nautobot: pynautobot.api, - project_uuid: str | None = None, - dry_run: bool = False, -) -> SyncResult: - """Sync Keystone projects to Nautobot tenants. - - Args: - conn: OpenStack connection - nautobot: Nautobot API instance - project_uuid: Optional specific project UUID to sync (syncs all if None) - dry_run: If True, only log what would be synced - - Returns: - SyncResult with total, failed, and skipped counts - """ - result = SyncResult() + return parser_nautobot_args(parser) + - if project_uuid: - projects = [conn.identity.get_project(project_uuid)] # pyright: ignore[reportAttributeAccessIssue] - else: - projects = list(conn.identity.projects()) # pyright: ignore[reportAttributeAccessIssue] +def sync_projects(conn: Connection, nautobot: pynautobot.api) -> SyncResult: + """Sync Keystone projects to Nautobot tenants.""" + result = SyncResult() + projects = list(conn.identity.projects()) # pyright: ignore[reportAttributeAccessIssue] for project in projects: result.total += 1 @@ -64,10 +45,6 @@ def sync_projects( result.skipped += 1 continue - if dry_run: - logger.info("Would sync project: %s (%s)", project.id, project.name) - continue - logger.info("Syncing project: %s (%s)", project.id, project.name) if handle_project_update(conn, nautobot, uuid.UUID(project.id)) != 0: result.failed += 1 @@ -82,6 +59,6 @@ def main() -> int: conn = get_openstack_client() nautobot = get_nautobot_client(args) - result = sync_projects(conn, nautobot, args.project or None, args.dry_run) + result = sync_projects(conn, nautobot) - return log_sync_result(result, "project", args.dry_run) + return log_sync_result(result, "project") diff --git a/python/understack-workflows/understack_workflows/main/resync_neutron_to_nautobot.py b/python/understack-workflows/understack_workflows/main/resync_neutron_to_nautobot.py new file mode 100644 index 000000000..cec2c7224 --- /dev/null +++ b/python/understack-workflows/understack_workflows/main/resync_neutron_to_nautobot.py @@ -0,0 +1,97 @@ +"""Resync Neutron networks and subnets to Nautobot. + +Use when Nautobot gets out of sync with Neutron, e.g., after: +- Nautobot database restore +- Missed events +- Manual Nautobot changes + +Should be run before resync-ironic-nautobot to ensure IPAM namespaces +and prefixes exist before device/interface sync. +""" + +import argparse +import logging + +import pynautobot + +from understack_workflows.helpers import setup_logger +from understack_workflows.openstack.client import get_openstack_client +from understack_workflows.oslo_event.neutron_network import sync_network_to_nautobot +from understack_workflows.oslo_event.neutron_subnet import sync_subnet_to_nautobot +from understack_workflows.helpers import parser_nautobot_args +from understack_workflows.resync import SyncResult +from understack_workflows.resync import get_nautobot_client +from understack_workflows.resync import log_sync_result + +logger = logging.getLogger(__name__) + + +def argument_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description="Resync Neutron to Nautobot") + return parser_nautobot_args(parser) + + +def sync_neutron_to_nautobot( + nautobot: pynautobot.api, +) -> tuple[SyncResult, SyncResult]: + """Sync Neutron networks and subnets to Nautobot.""" + conn = get_openstack_client() + + network_result = SyncResult() + subnet_result = SyncResult() + networks = list(conn.network.networks()) # type: ignore[attr-defined] + + network_external_map: dict[str, bool] = {} + + for network in networks: + network_result.total += 1 + network_external_map[network.id] = network.is_router_external or False + + logger.info("Syncing network: %s (%s)", network.id, network.name) + if ( + sync_network_to_nautobot( + nautobot, + str(network.id), + network.name, + str(network.project_id), + network.provider_segmentation_id, + ) + != 0 + ): + network_result.failed += 1 + logger.error("Failed to sync network %s", network.id) + + subnets = list(conn.network.subnets()) # type: ignore[attr-defined] + + for subnet in subnets: + subnet_result.total += 1 + + logger.info("Syncing subnet: %s (%s)", subnet.id, subnet.name) + is_external = network_external_map.get(subnet.network_id, False) + if ( + sync_subnet_to_nautobot( + nautobot, + str(subnet.id), + str(subnet.network_id), + str(subnet.project_id), + subnet.cidr, + is_external, + ) + != 0 + ): + subnet_result.failed += 1 + logger.error("Failed to sync subnet %s", subnet.id) + + return network_result, subnet_result + + +def main() -> int: + setup_logger(level=logging.INFO) + args = argument_parser().parse_args() + + nautobot = get_nautobot_client(args) + network_result, subnet_result = sync_neutron_to_nautobot(nautobot) + + exit_code = log_sync_result(network_result, "network") + exit_code |= log_sync_result(subnet_result, "subnet") + return exit_code diff --git a/python/understack-workflows/understack_workflows/oslo_event/neutron_network.py b/python/understack-workflows/understack_workflows/oslo_event/neutron_network.py index 8c0f72935..e33881133 100644 --- a/python/understack-workflows/understack_workflows/oslo_event/neutron_network.py +++ b/python/understack-workflows/understack_workflows/oslo_event/neutron_network.py @@ -43,10 +43,14 @@ def handle_network_create_or_update( event = NetworkEvent.from_event_dict(event_data) logger.info("Handling Network create/update for %s", event.network_name) - _ensure_nautobot_ipam_namespace_exists(nautobot, str(event.network_uuid)) - _create_nautobot_ucvni(nautobot, event, ucvni_group_name) - - return 0 + return sync_network_to_nautobot( + nautobot, + str(event.network_uuid), + event.network_name, + str(event.tenant_id), + event.provider_segmentation_id, + ucvni_group_name, + ) def handle_network_delete(_conn, nautobot: Nautobot, event_data: dict) -> int: @@ -134,3 +138,83 @@ def _delete_nautobot_prefixes_in_namespace(nautobot: Nautobot, namespace_id: str prefix = cast(Record, prefix) prefix.delete() logger.info("Deleted dependent prefix %s from Nautobot", prefix.prefix) + + +def sync_network_to_nautobot( + nautobot: Nautobot, + network_id: str, + network_name: str, + tenant_id: str, + segmentation_id: int | None = None, + ucvni_group_name: str | None = None, +) -> int: + """Sync a single network to Nautobot. + + Creates IPAM namespace and UCVNI for the network. + + Args: + nautobot: Nautobot API client + network_id: Network UUID + network_name: Network name + tenant_id: Tenant/project UUID + segmentation_id: Provider segmentation ID (optional) + ucvni_group_name: UCVNI group name (defaults to UCVNI_GROUP_NAME env var) + + Returns: + 0 on success, 1 on failure + """ + try: + # Create IPAM namespace + _ensure_nautobot_ipam_namespace_exists(nautobot, network_id) + + # Create or update UCVNI if segmentation ID exists + if segmentation_id: + event = NetworkEvent( + event_type="network.sync", + network_uuid=UUID(network_id), + network_name=network_name, + tenant_id=UUID(tenant_id), + external=False, + provider_segmentation_id=segmentation_id, + ) + if not _update_nautobot_ucvni(nautobot, event, ucvni_group_name): + _create_nautobot_ucvni(nautobot, event, ucvni_group_name) + + return 0 + except Exception: + logger.exception("Failed to sync network %s", network_id) + return 1 + + +def _update_nautobot_ucvni( + nautobot: Nautobot, + event: NetworkEvent, + ucvni_group_name: str | None = None, +) -> bool: + """Update existing UCVNI. Returns True if updated, False if not found.""" + ucvni_id = str(event.network_uuid) + + if ucvni_group_name is None: + ucvni_group_name = os.getenv("UCVNI_GROUP_NAME") + if ucvni_group_name is None: + raise RuntimeError("Please set environment variable UCVNI_GROUP_NAME") + + payload = { + "name": event.network_name, + "status": {"name": "Active"}, + "tenant": str(event.tenant_id), + "ucvni_group": {"name": ucvni_group_name}, + "ucvni_id": event.provider_segmentation_id, + } + + try: + response = nautobot.plugins.undercloud_vni.ucvnis.update( + id=ucvni_id, data=payload + ) + logger.info("Updated Nautobot UCVNI: %s", response) + return True + except pynautobot.RequestError as e: + if e.req.status_code == 404: + logger.debug("No pre-existing Nautobot UCVNI with id=%s", ucvni_id) + return False + raise NautobotRequestError(e) from e diff --git a/python/understack-workflows/understack_workflows/oslo_event/neutron_subnet.py b/python/understack-workflows/understack_workflows/oslo_event/neutron_subnet.py index e0f4610ff..17b2a0515 100644 --- a/python/understack-workflows/understack_workflows/oslo_event/neutron_subnet.py +++ b/python/understack-workflows/understack_workflows/oslo_event/neutron_subnet.py @@ -48,26 +48,15 @@ def handle_subnet_create_or_update( """Handle Openstack Neutron Subnet create/update Event.""" subnet = SubnetEvent.from_event_dict(event_data) - id = str(subnet.subnet_uuid) - - if subnet.external: - namespace = "Global" - else: - namespace = str(subnet.network_uuid) - - nautobot_prefix_payload = { - "id": id, - "prefix": subnet.cidr, - "status": "Active", - "namespace": {"name": namespace}, - "tenant": {"id": str(subnet.tenant_uuid)}, - } - - existing = _update_nautobot_prefix(nautobot, id, nautobot_prefix_payload) - if not existing: - _create_nautobot_prefix(nautobot, nautobot_prefix_payload) - - return 0 + logger.info("Handling Subnet create/update for %s", subnet.subnet_name) + return sync_subnet_to_nautobot( + nautobot, + str(subnet.subnet_uuid), + str(subnet.network_uuid), + str(subnet.tenant_uuid), + subnet.cidr, + subnet.external, + ) def handle_subnet_delete( @@ -104,3 +93,46 @@ def _create_nautobot_prefix(nautobot, payload: dict): logger.info("Created Nautobot prefix: %s", response) except pynautobot.RequestError as e: raise NautobotRequestError(e) from e + + +def sync_subnet_to_nautobot( + nautobot: Nautobot, + subnet_id: str, + network_id: str, + tenant_id: str, + cidr: str, + is_external: bool = False, +) -> int: + """Sync a single subnet to Nautobot. + + Args: + nautobot: Nautobot API client + subnet_id: Subnet UUID + network_id: Parent network UUID + tenant_id: Tenant/project UUID + cidr: Subnet CIDR + is_external: Whether the parent network is external (determines namespace) + + Returns: + 0 on success, 1 on failure + """ + try: + # External network subnets go to Global namespace + namespace = "Global" if is_external else network_id + + payload = { + "id": subnet_id, + "prefix": cidr, + "status": "Active", + "namespace": {"name": namespace}, + "tenant": {"id": tenant_id}, + } + + # Try update first, then create + if not _update_nautobot_prefix(nautobot, subnet_id, payload): + _create_nautobot_prefix(nautobot, payload) + + return 0 + except Exception: + logger.exception("Failed to sync subnet %s", subnet_id) + return 1 diff --git a/python/understack-workflows/understack_workflows/resync.py b/python/understack-workflows/understack_workflows/resync.py index 5c4d20ef5..1281e14de 100644 --- a/python/understack-workflows/understack_workflows/resync.py +++ b/python/understack-workflows/understack_workflows/resync.py @@ -1,16 +1,11 @@ -"""Shared utilities for resync operations. +"""Shared utilities for resync operations.""" -Common patterns for resyncing data from various sources to Nautobot. -""" - -import argparse import logging from dataclasses import dataclass import pynautobot from understack_workflows.helpers import credential -from understack_workflows.helpers import parser_nautobot_args logger = logging.getLogger(__name__) @@ -31,69 +26,21 @@ def succeeded(self) -> int: return self.total - self.failed - self.skipped -def parser_resync_args( - parser: argparse.ArgumentParser, - item_name: str = "item", - item_flag: str = "--item", -) -> argparse.ArgumentParser: - """Add common resync arguments to a parser. - - Args: - parser: ArgumentParser to add arguments to - item_name: Name of the item being synced (for help text) - item_flag: Flag name for specifying a single item - - Returns: - The parser with added arguments - """ - parser.add_argument( - item_flag, - type=str, - help=f"Sync specific {item_name} UUID (default: all {item_name}s)", - ) - parser.add_argument( - "--dry-run", - action="store_true", - help=f"List {item_name}s without syncing", - ) - return parser_nautobot_args(parser) - - def get_nautobot_client(args: argparse.Namespace) -> pynautobot.api: """Create a Nautobot API client from parsed arguments.""" nb_token = args.nautobot_token or credential("nb-token", "token") return pynautobot.api(args.nautobot_url, token=nb_token) -def log_sync_result( - result: SyncResult, - item_name: str, - dry_run: bool = False, -) -> int: - """Log sync results and return appropriate exit code. - - Args: - result: SyncResult from the sync operation - item_name: Name of the item type (e.g., "node", "project") - dry_run: Whether this was a dry run - - Returns: - Exit code (EXIT_SUCCESS or EXIT_SYNC_FAILURES) - """ - if dry_run: - count = result.total - result.skipped - msg = f"Dry run complete. {count} {item_name}s would be synced" - if result.skipped: - msg += f" ({result.skipped} skipped)" - logger.info("%s.", msg) - else: - msg = ( - f"Sync complete. {result.succeeded}/{result.total} " - f"{item_name}s synced successfully" - ) - if result.skipped: - msg += f" ({result.skipped} skipped)" - logger.info("%s.", msg) +def log_sync_result(result: SyncResult, item_name: str) -> int: + """Log sync results and return appropriate exit code.""" + msg = ( + f"Sync complete. {result.succeeded}/{result.total} " + f"{item_name}s synced successfully" + ) + if result.skipped: + msg += f" ({result.skipped} skipped)" + logger.info("%s.", msg) if result.failed: logger.error("Failed to sync %d %ss", result.failed, item_name) diff --git a/workflows/argo-events/workflowtemplates/resync-nautobot.yaml b/workflows/argo-events/workflowtemplates/resync-nautobot.yaml index 554520696..e5043f06b 100644 --- a/workflows/argo-events/workflowtemplates/resync-nautobot.yaml +++ b/workflows/argo-events/workflowtemplates/resync-nautobot.yaml @@ -27,17 +27,13 @@ metadata: spec: entrypoint: main serviceAccountName: workflow - arguments: - parameters: - - name: project - value: "" # empty = all projects - - name: node - value: "" # empty = all nodes templates: - name: main steps: - - name: resync-keystone template: resync-keystone + - - name: resync-neutron + template: resync-neutron - - name: resync-ironic template: resync-ironic @@ -46,9 +42,23 @@ spec: image: ghcr.io/rackerlabs/understack/ironic-nautobot-client:pr-1710 command: - resync-keystone-nautobot - args: - - "--project" - - "{{workflow.parameters.project}}" + volumeMounts: + - mountPath: /etc/nb-token/ + name: nb-token + readOnly: true + - mountPath: /etc/openstack + name: baremetal-manage + readOnly: true + envFrom: + - configMapRef: + name: cluster-metadata + optional: false + + - name: resync-neutron + container: + image: ghcr.io/rackerlabs/understack/ironic-nautobot-client:pr-1710 + command: + - resync-neutron-nautobot volumeMounts: - mountPath: /etc/nb-token/ name: nb-token @@ -66,9 +76,6 @@ spec: image: ghcr.io/rackerlabs/understack/ironic-nautobot-client:pr-1710 command: - resync-ironic-nautobot - args: - - "--node" - - "{{workflow.parameters.node}}" volumeMounts: - mountPath: /etc/nb-token/ name: nb-token