Skip to content

Commit 9044aa2

Browse files
authored
Merge pull request #1710 from rackerlabs/resync-keystone-to-nautobot
feat: unified nautobot resync workflow
2 parents eeba6d6 + 6c1d581 commit 9044aa2

16 files changed

Lines changed: 757 additions & 239 deletions

python/understack-workflows/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ netapp-configure-interfaces = "understack_workflows.main.netapp_configure_net:ma
3939
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"
42+
resync-keystone-nautobot = "understack_workflows.main.resync_keystone_to_nautobot:main"
43+
resync-neutron-nautobot = "understack_workflows.main.resync_neutron_to_nautobot:main"
4244
sync-keystone = "understack_workflows.main.sync_keystone:main"
4345
sync-network-segment-range = "understack_workflows.main.sync_ucvni_group_range:main"
4446
undersync-switch = "understack_workflows.main.undersync_switch:main"

python/understack-workflows/tests/test_oslo_event_neutron_network.py

Lines changed: 40 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",
@@ -119,6 +127,34 @@ def test_handle_network_create_idempotent_ucvni(network_create_event_data):
119127
assert result == 0
120128

121129

130+
def test_handle_network_create_duplicate_ucvni_group_and_id(network_create_event_data):
131+
"""Test that duplicate ucvni_group + ucvni_id combination fails the sync."""
132+
mock_nautobot = Mock()
133+
mock_nautobot.ipam.namespaces.create.return_value = "123"
134+
135+
# Mock update to return 404 so create path is taken
136+
mock_update_req = Mock()
137+
mock_update_req.status_code = 404
138+
mock_nautobot.plugins.undercloud_vni.ucvnis.update.side_effect = (
139+
pynautobot.RequestError(mock_update_req)
140+
)
141+
142+
# Mock create to fail with duplicate ucvni_group + ucvni_id error
143+
mock_create_req = Mock()
144+
mock_create_req.status_code = 400
145+
mock_create_req.text = "UCVNI with this Ucvni group and UCVNI_ID already exists."
146+
147+
mock_nautobot.plugins.undercloud_vni.ucvnis.create.side_effect = (
148+
pynautobot.RequestError(mock_create_req)
149+
)
150+
151+
result = neutron_network.handle_network_create_or_update(
152+
None, mock_nautobot, network_create_event_data, ucvni_group_name="FOO"
153+
)
154+
# Should return 1 (failure) due to segmentation ID conflict
155+
assert result == 1
156+
157+
122158
def test_handle_network_delete(network_delete_event_data):
123159
mock_nautobot_namespace = Mock()
124160
mock_nautobot_namespace.delete.return_value = "123"

python/understack-workflows/tests/test_resync_ironic_to_nautobot.py

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,10 @@
33
from unittest.mock import MagicMock
44
from unittest.mock import patch
55

6-
from understack_workflows.main.resync_ironic_to_nautobot import SyncResult
76
from understack_workflows.main.resync_ironic_to_nautobot import argument_parser
87
from understack_workflows.main.resync_ironic_to_nautobot import main
98
from understack_workflows.main.resync_ironic_to_nautobot import sync_nodes
10-
11-
12-
class TestSyncResult:
13-
"""Test cases for SyncResult dataclass."""
14-
15-
def test_defaults(self):
16-
result = SyncResult()
17-
assert result.total == 0
18-
assert result.failed == 0
19-
assert result.succeeded == 0
20-
21-
def test_succeeded_calculation(self):
22-
result = SyncResult(total=10, failed=3)
23-
assert result.succeeded == 7
24-
25-
def test_all_failed(self):
26-
result = SyncResult(total=5, failed=5)
27-
assert result.succeeded == 0
28-
29-
def test_none_failed(self):
30-
result = SyncResult(total=5, failed=0)
31-
assert result.succeeded == 5
9+
from understack_workflows.resync import SyncResult
3210

3311

3412
class TestArgumentParser:
@@ -37,18 +15,7 @@ class TestArgumentParser:
3715
def test_default_args(self):
3816
parser = argument_parser()
3917
args = parser.parse_args([])
40-
assert args.node is None
41-
assert args.dry_run is False
42-
43-
def test_node_arg(self):
44-
parser = argument_parser()
45-
args = parser.parse_args(["--node", "test-uuid"])
46-
assert args.node == "test-uuid"
47-
48-
def test_dry_run_arg(self):
49-
parser = argument_parser()
50-
args = parser.parse_args(["--dry-run"])
51-
assert args.dry_run is True
18+
assert args.nautobot_url is not None or args.nautobot_token is None
5219

5320

5421
class TestSyncNodes:
@@ -81,15 +48,15 @@ def test_sync_single_node(self, mock_sync, mock_ironic_class):
8148
mock_ironic = MagicMock()
8249
mock_ironic_class.return_value = mock_ironic
8350
mock_node = MagicMock(uuid="uuid-1", name="node-1")
84-
mock_ironic.get_node.return_value = mock_node
51+
mock_ironic.list_nodes.return_value = [mock_node]
8552
mock_sync.return_value = 0
8653

8754
nautobot = MagicMock()
88-
result = sync_nodes(nautobot, node_uuid="uuid-1")
55+
result = sync_nodes(nautobot)
8956

9057
assert result.total == 1
9158
assert result.failed == 0
92-
mock_ironic.get_node.assert_called_once_with("uuid-1")
59+
mock_ironic.list_nodes.assert_called_once()
9360

9461
@patch("understack_workflows.main.resync_ironic_to_nautobot.IronicClient")
9562
@patch(
@@ -111,33 +78,30 @@ def test_sync_with_failures(self, mock_sync, mock_ironic_class):
11178
assert result.succeeded == 1
11279

11380
@patch("understack_workflows.main.resync_ironic_to_nautobot.IronicClient")
114-
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):
11585
mock_ironic = MagicMock()
11686
mock_ironic_class.return_value = mock_ironic
117-
mock_node = MagicMock(uuid="uuid-1", name="node-1")
118-
mock_ironic.list_nodes.return_value = [mock_node]
87+
mock_ironic.list_nodes.return_value = []
11988

12089
nautobot = MagicMock()
121-
result = sync_nodes(nautobot, dry_run=True)
90+
result = sync_nodes(nautobot)
12291

123-
assert result.total == 1
92+
assert result.total == 0
12493
assert result.failed == 0
12594

12695

12796
class TestMain:
12897
"""Test cases for main function."""
12998

13099
@patch("understack_workflows.main.resync_ironic_to_nautobot.sync_nodes")
131-
@patch("understack_workflows.main.resync_ironic_to_nautobot.pynautobot")
132-
@patch("understack_workflows.main.resync_ironic_to_nautobot.credential")
100+
@patch("understack_workflows.main.resync_ironic_to_nautobot.get_nautobot_client")
133101
@patch("understack_workflows.main.resync_ironic_to_nautobot.setup_logger")
134102
@patch("understack_workflows.main.resync_ironic_to_nautobot.argument_parser")
135-
def test_main_success(
136-
self, mock_parser, mock_logger, mock_cred, mock_pynb, mock_sync
137-
):
103+
def test_main_success(self, mock_parser, mock_logger, mock_get_nb, mock_sync):
138104
mock_args = MagicMock()
139-
mock_args.nautobot_token = "token"
140-
mock_args.nautobot_url = "http://nautobot"
141105
mock_args.node = None
142106
mock_args.dry_run = False
143107
mock_parser.return_value.parse_args.return_value = mock_args
@@ -148,16 +112,11 @@ def test_main_success(
148112
assert result == 0
149113

150114
@patch("understack_workflows.main.resync_ironic_to_nautobot.sync_nodes")
151-
@patch("understack_workflows.main.resync_ironic_to_nautobot.pynautobot")
152-
@patch("understack_workflows.main.resync_ironic_to_nautobot.credential")
115+
@patch("understack_workflows.main.resync_ironic_to_nautobot.get_nautobot_client")
153116
@patch("understack_workflows.main.resync_ironic_to_nautobot.setup_logger")
154117
@patch("understack_workflows.main.resync_ironic_to_nautobot.argument_parser")
155-
def test_main_with_failures(
156-
self, mock_parser, mock_logger, mock_cred, mock_pynb, mock_sync
157-
):
118+
def test_main_with_failures(self, mock_parser, mock_logger, mock_get_nb, mock_sync):
158119
mock_args = MagicMock()
159-
mock_args.nautobot_token = "token"
160-
mock_args.nautobot_url = "http://nautobot"
161120
mock_args.node = None
162121
mock_args.dry_run = False
163122
mock_parser.return_value.parse_args.return_value = mock_args

0 commit comments

Comments
 (0)