Skip to content

Commit 292cca5

Browse files
Syncs UCVNIs, IPAM namespaces, and prefixes
Adds ability to run Neutron resync before Ironic resync to ensure IPAM data exists in Nautobot.
1 parent 812b870 commit 292cca5

11 files changed

Lines changed: 327 additions & 214 deletions

File tree

python/understack-workflows/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ netapp-create-svm = "understack_workflows.main.netapp_create_svm:main"
4040
openstack-oslo-event = "understack_workflows.main.openstack_oslo_event:main"
4141
resync-ironic-nautobot = "understack_workflows.main.resync_ironic_to_nautobot:main"
4242
resync-keystone-nautobot = "understack_workflows.main.resync_keystone_to_nautobot:main"
43+
resync-neutron-nautobot = "understack_workflows.main.resync_neutron_to_nautobot:main"
4344
sync-keystone = "understack_workflows.main.sync_keystone:main"
4445
sync-network-segment-range = "understack_workflows.main.sync_ucvni_group_range:main"
4546
undersync-switch = "understack_workflows.main.undersync_switch:main"

python/understack-workflows/tests/test_oslo_event_neutron_network.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ def test_handle_network_create(network_create_event_data):
3333
mock_nautobot = Mock()
3434
mock_nautobot.ipam.namespaces.create.return_value = "123"
3535

36+
# Mock update to return 404 so create path is taken
37+
mock_pynautobot_req = Mock()
38+
mock_pynautobot_req.status_code = 404
39+
mock_nautobot.plugins.undercloud_vni.ucvnis.update.side_effect = (
40+
pynautobot.RequestError(mock_pynautobot_req)
41+
)
42+
3643
result = neutron_network.handle_network_create_or_update(
3744
None, mock_nautobot, network_create_event_data, ucvni_group_name="FOO"
3845
)
@@ -59,7 +66,8 @@ def test_handle_network_create(network_create_event_data):
5966

6067
def test_handle_network_update(network_update_event_data):
6168
mock_nautobot = Mock()
62-
mock_nautobot.ipam.namespaces.update.return_value = "123"
69+
mock_nautobot.ipam.namespaces.create.return_value = "123"
70+
mock_nautobot.plugins.undercloud_vni.ucvnis.update.return_value = "updated"
6371

6472
result = neutron_network.handle_network_create_or_update(
6573
None, mock_nautobot, network_update_event_data, ucvni_group_name="FOO"
@@ -68,9 +76,9 @@ def test_handle_network_update(network_update_event_data):
6876
mock_nautobot.ipam.namespaces.create.assert_called_once_with(
6977
name="f4aa4b99-2c2f-4698-a2f0-ac9920b1ee81"
7078
)
71-
mock_nautobot.plugins.undercloud_vni.ucvnis.create.assert_called_once_with(
72-
{
73-
"id": "f4aa4b99-2c2f-4698-a2f0-ac9920b1ee81",
79+
mock_nautobot.plugins.undercloud_vni.ucvnis.update.assert_called_once_with(
80+
id="f4aa4b99-2c2f-4698-a2f0-ac9920b1ee81",
81+
data={
7482
"name": "service-net",
7583
"status": {
7684
"name": "Active",

python/understack-workflows/tests/test_resync_ironic_to_nautobot.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,7 @@ class TestArgumentParser:
1515
def test_default_args(self):
1616
parser = argument_parser()
1717
args = parser.parse_args([])
18-
assert args.node is None
19-
assert args.dry_run is False
20-
21-
def test_node_arg(self):
22-
parser = argument_parser()
23-
args = parser.parse_args(["--node", "test-uuid"])
24-
assert args.node == "test-uuid"
25-
26-
def test_dry_run_arg(self):
27-
parser = argument_parser()
28-
args = parser.parse_args(["--dry-run"])
29-
assert args.dry_run is True
18+
assert args.nautobot_url is not None or args.nautobot_token is None
3019

3120

3221
class TestSyncNodes:
@@ -59,15 +48,15 @@ def test_sync_single_node(self, mock_sync, mock_ironic_class):
5948
mock_ironic = MagicMock()
6049
mock_ironic_class.return_value = mock_ironic
6150
mock_node = MagicMock(uuid="uuid-1", name="node-1")
62-
mock_ironic.get_node.return_value = mock_node
51+
mock_ironic.list_nodes.return_value = [mock_node]
6352
mock_sync.return_value = 0
6453

6554
nautobot = MagicMock()
66-
result = sync_nodes(nautobot, node_uuid="uuid-1")
55+
result = sync_nodes(nautobot)
6756

6857
assert result.total == 1
6958
assert result.failed == 0
70-
mock_ironic.get_node.assert_called_once_with("uuid-1")
59+
mock_ironic.list_nodes.assert_called_once()
7160

7261
@patch("understack_workflows.main.resync_ironic_to_nautobot.IronicClient")
7362
@patch(
@@ -89,16 +78,18 @@ def test_sync_with_failures(self, mock_sync, mock_ironic_class):
8978
assert result.succeeded == 1
9079

9180
@patch("understack_workflows.main.resync_ironic_to_nautobot.IronicClient")
92-
def test_dry_run_skips_sync(self, mock_ironic_class):
81+
@patch(
82+
"understack_workflows.main.resync_ironic_to_nautobot.sync_device_to_nautobot"
83+
)
84+
def test_sync_no_nodes(self, mock_sync, mock_ironic_class):
9385
mock_ironic = MagicMock()
9486
mock_ironic_class.return_value = mock_ironic
95-
mock_node = MagicMock(uuid="uuid-1", name="node-1")
96-
mock_ironic.list_nodes.return_value = [mock_node]
87+
mock_ironic.list_nodes.return_value = []
9788

9889
nautobot = MagicMock()
99-
result = sync_nodes(nautobot, dry_run=True)
90+
result = sync_nodes(nautobot)
10091

101-
assert result.total == 1
92+
assert result.total == 0
10293
assert result.failed == 0
10394

10495

python/understack-workflows/tests/test_resync_keystone_to_nautobot.py

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,7 @@ class TestArgumentParser:
3434
def test_default_args(self):
3535
parser = argument_parser()
3636
args = parser.parse_args([])
37-
assert args.project is None
38-
assert args.dry_run is False
39-
40-
def test_project_arg(self):
41-
parser = argument_parser()
42-
args = parser.parse_args(["--project", "test-uuid"])
43-
assert args.project == "test-uuid"
44-
45-
def test_dry_run_arg(self):
46-
parser = argument_parser()
47-
args = parser.parse_args(["--dry-run"])
48-
assert args.dry_run is True
37+
assert args.nautobot_url is not None or args.nautobot_token is None
4938

5039

5140
class TestSyncProjects:
@@ -85,23 +74,19 @@ def test_sync_single_project(self):
8574
name="project-1",
8675
is_domain=False,
8776
)
88-
conn.identity.get_project.return_value = project
77+
conn.identity.projects.return_value = [project]
8978

9079
nautobot = MagicMock()
9180

9281
with patch(
9382
"understack_workflows.main.resync_keystone_to_nautobot.handle_project_update"
9483
) as mock_update:
9584
mock_update.return_value = 0
96-
result = sync_projects(
97-
conn, nautobot, project_uuid="12345678-1234-5678-1234-567812345678"
98-
)
85+
result = sync_projects(conn, nautobot)
9986

10087
assert result.total == 1
10188
assert result.failed == 0
102-
conn.identity.get_project.assert_called_once_with(
103-
"12345678-1234-5678-1234-567812345678"
104-
)
89+
conn.identity.projects.assert_called_once()
10590

10691
def test_sync_with_failures(self):
10792
conn = MagicMock()
@@ -156,21 +141,16 @@ def test_sync_skips_domains(self):
156141
assert result.succeeded == 1
157142
mock_update.assert_called_once()
158143

159-
def test_dry_run_skips_sync(self):
144+
def test_sync_no_projects(self):
160145
conn = MagicMock()
161-
project = MagicMock(id="uuid-1", name="project-1", is_domain=False)
162-
conn.identity.projects.return_value = [project]
146+
conn.identity.projects.return_value = []
163147

164148
nautobot = MagicMock()
165149

166-
with patch(
167-
"understack_workflows.main.resync_keystone_to_nautobot.handle_project_update"
168-
) as mock_update:
169-
result = sync_projects(conn, nautobot, dry_run=True)
150+
result = sync_projects(conn, nautobot)
170151

171-
assert result.total == 1
152+
assert result.total == 0
172153
assert result.failed == 0
173-
mock_update.assert_not_called()
174154

175155

176156
class TestMain:

python/understack-workflows/understack_workflows/main/resync_ironic_to_nautobot.py

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,48 +11,30 @@
1111

1212
import pynautobot
1313

14+
from understack_workflows.helpers import parser_nautobot_args
1415
from understack_workflows.helpers import setup_logger
1516
from understack_workflows.ironic.client import IronicClient
1617
from understack_workflows.oslo_event.nautobot_device_sync import sync_device_to_nautobot
1718
from understack_workflows.resync import SyncResult
1819
from understack_workflows.resync import get_nautobot_client
1920
from understack_workflows.resync import log_sync_result
20-
from understack_workflows.resync import parser_resync_args
2121

2222
logger = logging.getLogger(__name__)
2323

2424

2525
def argument_parser() -> argparse.ArgumentParser:
2626
parser = argparse.ArgumentParser(description="Resync Ironic nodes to Nautobot")
27-
return parser_resync_args(parser, item_name="node", item_flag="--node")
27+
return parser_nautobot_args(parser)
2828

2929

30-
def sync_nodes(
31-
nautobot: pynautobot.api,
32-
node_uuid: str | None = None,
33-
dry_run: bool = False,
34-
) -> SyncResult:
35-
"""Sync Ironic nodes to Nautobot.
36-
37-
Args:
38-
nautobot: Nautobot API instance
39-
node_uuid: Optional specific node UUID to sync (syncs all if None)
40-
dry_run: If True, only log what would be synced
41-
42-
Returns:
43-
SyncResult with total and failed counts
44-
"""
30+
def sync_nodes(nautobot: pynautobot.api) -> SyncResult:
31+
"""Sync Ironic nodes to Nautobot."""
4532
ironic = IronicClient()
46-
nodes = [ironic.get_node(node_uuid)] if node_uuid else ironic.list_nodes()
33+
nodes = ironic.list_nodes()
4734
result = SyncResult()
4835

4936
for node in nodes:
5037
result.total += 1
51-
52-
if dry_run:
53-
logger.info("Would sync node: %s (%s)", node.uuid, node.name)
54-
continue
55-
5638
logger.info("Syncing node: %s (%s)", node.uuid, node.name)
5739
if sync_device_to_nautobot(node.uuid, nautobot) != 0:
5840
result.failed += 1
@@ -66,6 +48,6 @@ def main() -> int:
6648
args = argument_parser().parse_args()
6749

6850
nautobot = get_nautobot_client(args)
69-
result = sync_nodes(nautobot, args.node or None, args.dry_run)
51+
result = sync_nodes(nautobot)
7052

71-
return log_sync_result(result, "node", args.dry_run)
53+
return log_sync_result(result, "node")

python/understack-workflows/understack_workflows/main/resync_keystone_to_nautobot.py

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import pynautobot
1414

15+
from understack_workflows.helpers import parser_nautobot_args
1516
from understack_workflows.helpers import setup_logger
1617
from understack_workflows.main.sync_keystone import handle_project_update
1718
from understack_workflows.main.sync_keystone import is_domain
@@ -20,7 +21,6 @@
2021
from understack_workflows.resync import SyncResult
2122
from understack_workflows.resync import get_nautobot_client
2223
from understack_workflows.resync import log_sync_result
23-
from understack_workflows.resync import parser_resync_args
2424

2525
logger = logging.getLogger(__name__)
2626

@@ -29,32 +29,13 @@ def argument_parser() -> argparse.ArgumentParser:
2929
parser = argparse.ArgumentParser(
3030
description="Resync Keystone projects to Nautobot tenants"
3131
)
32-
return parser_resync_args(parser, item_name="project", item_flag="--project")
33-
34-
35-
def sync_projects(
36-
conn: Connection,
37-
nautobot: pynautobot.api,
38-
project_uuid: str | None = None,
39-
dry_run: bool = False,
40-
) -> SyncResult:
41-
"""Sync Keystone projects to Nautobot tenants.
42-
43-
Args:
44-
conn: OpenStack connection
45-
nautobot: Nautobot API instance
46-
project_uuid: Optional specific project UUID to sync (syncs all if None)
47-
dry_run: If True, only log what would be synced
48-
49-
Returns:
50-
SyncResult with total, failed, and skipped counts
51-
"""
52-
result = SyncResult()
32+
return parser_nautobot_args(parser)
33+
5334

54-
if project_uuid:
55-
projects = [conn.identity.get_project(project_uuid)] # pyright: ignore[reportAttributeAccessIssue]
56-
else:
57-
projects = list(conn.identity.projects()) # pyright: ignore[reportAttributeAccessIssue]
35+
def sync_projects(conn: Connection, nautobot: pynautobot.api) -> SyncResult:
36+
"""Sync Keystone projects to Nautobot tenants."""
37+
result = SyncResult()
38+
projects = list(conn.identity.projects()) # pyright: ignore[reportAttributeAccessIssue]
5839

5940
for project in projects:
6041
result.total += 1
@@ -64,10 +45,6 @@ def sync_projects(
6445
result.skipped += 1
6546
continue
6647

67-
if dry_run:
68-
logger.info("Would sync project: %s (%s)", project.id, project.name)
69-
continue
70-
7148
logger.info("Syncing project: %s (%s)", project.id, project.name)
7249
if handle_project_update(conn, nautobot, uuid.UUID(project.id)) != 0:
7350
result.failed += 1
@@ -82,6 +59,6 @@ def main() -> int:
8259

8360
conn = get_openstack_client()
8461
nautobot = get_nautobot_client(args)
85-
result = sync_projects(conn, nautobot, args.project or None, args.dry_run)
62+
result = sync_projects(conn, nautobot)
8663

87-
return log_sync_result(result, "project", args.dry_run)
64+
return log_sync_result(result, "project")

0 commit comments

Comments
 (0)