Skip to content

Commit 247e160

Browse files
authored
Merge pull request #277 from cdtomkins/feature_add_custom_ordering_diff
feat: ✨ Add a new CustomOrderingDiff to support custom ordering
2 parents 8b81a40 + a0f5f9d commit 247e160

3 files changed

Lines changed: 146 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Custom Diff object for {{ cookiecutter.system_of_record }} SSoT integration."""
2+
3+
from diffsync.diff import Diff
4+
from diffsync.enum import DiffSyncActions
5+
6+
7+
class CustomOrderingDiff(Diff):
8+
"""Customised Diff Object, extend it to support custom ordering."""
9+
10+
def get_children(self):
11+
"""Iterate over all child elements in all groups in self.children.
12+
13+
Check if a custom order method is defined, and otherwise use the default
14+
method.
15+
"""
16+
overall_deferred_children = []
17+
18+
for group in self.groups():
19+
group_deferred_chdrn = []
20+
21+
for child in self.children[group].values():
22+
# Custom handling to defer deletions until last
23+
if child.action == DiffSyncActions.DELETE:
24+
group_deferred_chdrn.append(child)
25+
else:
26+
yield child
27+
28+
# Custom handling logic for each group
29+
if group_deferred_chdrn:
30+
# Custom handling to order location deletions correctly
31+
if group == "location":
32+
33+
def location_depth(loc):
34+
keys = loc.keys
35+
depth = 0
36+
if keys.get("parent__name"):
37+
depth += 1
38+
if keys.get("parent__parent__name"):
39+
depth += 1
40+
# TODO: If locations are nested deeper, extend this functionality
41+
return depth
42+
43+
# Sort the locations by depth
44+
group_deferred_chdrn.sort(key=location_depth)
45+
46+
overall_deferred_children.extend(group_deferred_chdrn)
47+
48+
# Reverse ALL deferred deletions from their natural (creation) order
49+
yield from reversed(overall_deferred_children)

nautobot-app-ssot/{{ cookiecutter.project_slug }}/{{ cookiecutter.app_name }}/jobs.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from nautobot.apps.jobs import BooleanVar, register_jobs
44
from nautobot_ssot.jobs.base import DataSource, DataTarget
55

6+
from {{ cookiecutter.app_name }}.diff import CustomOrderingDiff
67
from {{ cookiecutter.app_name }}.diffsync.adapters import {{ cookiecutter.system_of_record_camel }}RemoteAdapter, {{ cookiecutter.system_of_record_camel }}NautobotAdapter
78

89
name = "{{ cookiecutter.system_of_record }} SSoT" # pylint: disable=invalid-name
@@ -89,6 +90,16 @@ def run(self, dryrun, memory_profiling, debug, *args, **kwargs): # pylint: disa
8990
self.memory_profiling = memory_profiling
9091
super().run(dryrun=self.dryrun, memory_profiling=self.memory_profiling, *args, **kwargs)
9192

93+
def execute_sync(self):
94+
"""Method to synchronize the difference from `self.diff`, from SOURCE to TARGET adapter.
95+
96+
Overridden to use a CustomOrderingDiff diff_class.
97+
"""
98+
if self.source_adapter is not None and self.target_adapter is not None:
99+
self.source_adapter.sync_to(self.target_adapter, flags=self.diffsync_flags, diff_class=CustomOrderingDiff)
100+
else:
101+
self.logger.warning("One of the adapters was not properly initialized prior to synchronization.")
102+
92103

93104
jobs = [{{ cookiecutter.system_of_record_camel }}DataSource, {{ cookiecutter.system_of_record_camel }}DataTarget]
94105
register_jobs(*jobs)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Test {{ cookiecutter.system_of_record }} SSoT integration customised Diff Object."""
2+
3+
from unittest.mock import MagicMock
4+
5+
from diffsync.enum import DiffSyncActions
6+
from nautobot.core.testing import TestCase
7+
8+
from {{ cookiecutter.app_name }}.jobs import CustomOrderingDiff
9+
10+
11+
class TestCustomOrderingDiff(TestCase):
12+
"""Test the CustomOrderingDiff."""
13+
14+
def setUp(self):
15+
"""Setup CustomOrderingDiff instance."""
16+
self.diff = CustomOrderingDiff()
17+
self.diff.children = {}
18+
self.diff.groups = MagicMock()
19+
20+
def test_yields_non_delete_first_then_deletes(self):
21+
"""Verify deletes are yielded last and reversed."""
22+
self.diff.groups.return_value = ["group1"]
23+
child1 = MagicMock()
24+
child1.action = DiffSyncActions.DELETE
25+
child2 = MagicMock()
26+
child2.action = DiffSyncActions.CREATE
27+
child3 = MagicMock()
28+
child3.action = DiffSyncActions.DELETE
29+
child4 = MagicMock()
30+
child4.action = DiffSyncActions.UPDATE
31+
32+
self.diff.children = {"group1": {"a": child1, "b": child2, "c": child3, "d": child4}}
33+
34+
results = list(self.diff.get_children())
35+
# Non-deletes first
36+
self.assertIn(child2, results[:2])
37+
self.assertIn(child4, results[:2])
38+
# Deletes at the end (in reverse order)
39+
self.assertEqual(results[-1], child1)
40+
self.assertEqual(results[-2], child3)
41+
42+
def test_location_deletes_sorted_by_depth(self):
43+
"""Verify Location deletes are correctly sorted by depth."""
44+
self.diff.groups.return_value = ["location"]
45+
46+
def make_loc(keys):
47+
loc = MagicMock()
48+
loc.action = DiffSyncActions.DELETE
49+
loc.keys = keys
50+
return loc
51+
52+
loc1 = make_loc({"parent__name": "parentA"})
53+
loc2 = make_loc({"parent__name": "parentA", "parent__parent__name": "grandparent"})
54+
loc3 = make_loc({})
55+
56+
self.diff.children = {"location": {"loc1": loc1, "loc2": loc2, "loc3": loc3}}
57+
58+
results = list(self.diff.get_children())
59+
60+
self.assertEqual(results, [loc2, loc1, loc3])
61+
62+
def test_mixed_groups(self):
63+
"""Verify mixed groups with mixed delete and non-delete children are returned in the correct order"""
64+
self.diff.groups.return_value = ["group1", "location"]
65+
66+
# Group1 children
67+
child1 = MagicMock()
68+
child1.action = DiffSyncActions.UPDATE
69+
child2 = MagicMock()
70+
child2.action = DiffSyncActions.DELETE
71+
72+
def make_loc(keys):
73+
loc = MagicMock()
74+
loc.action = DiffSyncActions.DELETE
75+
loc.keys = keys
76+
return loc
77+
78+
loc1 = make_loc({"parent__name": "p1"})
79+
loc2 = make_loc({})
80+
81+
self.diff.children = {"group1": {"c1": child1, "c2": child2}, "location": {"l1": loc1, "l2": loc2}}
82+
83+
results = list(self.diff.get_children())
84+
85+
self.assertEqual(results[0], child1)
86+
self.assertEqual(results[-3:], [loc1, loc2, child2])

0 commit comments

Comments
 (0)