Skip to content

Commit a5b980e

Browse files
authored
Merge pull request #1167 from nautobot/release/4.2.1
Release v4.2.1
2 parents 25dba34 + 1bee96d commit a5b980e

File tree

14 files changed

+968
-750
lines changed

14 files changed

+968
-750
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
/nautobot_ssot/integrations/itential/ @jtdub @nautobot/plugin-ssot
1212
/nautobot_ssot/integrations/librenms/ @bile0026 @nautobot/plugin-ssot
1313
/nautobot_ssot/integrations/meraki/ @jdrew82 @nautobot/plugin-ssot
14-
/nautobot_ssot/integrations/servicenow/ @glennmatthews @qduk @nautobot/plugin-ssot
14+
/nautobot_ssot/integrations/servicenow/ @glennmatthews @qduk @garymccann @nautobot/plugin-ssot
1515
/nautobot_ssot/integrations/slurpit/ @lpconsulting321 @pietos @nautobot/plugin-ssot
16-
/nautobot_ssot/integrations/solarwinds/ @jdrew82 @nopg @nautobot/plugin-ssot
16+
/nautobot_ssot/integrations/solarwinds/ @jdrew82 @nopg @garymccann @nautobot/plugin-ssot
1717

development/development.env

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ NAUTOBOT_SSOT_DEVICE42_HOST=""
8181
NAUTOBOT_SSOT_DEVICE42_USERNAME=""
8282
NAUTOBOT_SSOT_DEVICE42_PASSWORD=""
8383

84-
NAUTOBOT_SSOT_ENABLE_DNA_CENTER="True"
84+
NAUTOBOT_SSOT_ENABLE_DNA_CENTER="False"
8585
NAUTOBOT_DNAC_SSOT_DNA_CENTER_IMPORT_GLOBAL="True"
8686
NAUTOBOT_DNAC_SSOT_DNA_CENTER_IMPORT_MERAKIS="False"
8787
NAUTOBOT_DNAC_SSOT_DNA_CENTER_UPDATE_LOCATIONS="True"
@@ -116,7 +116,7 @@ NAUTOBOT_SSOT_IPFABRIC_HOST="https://ipfabric.example.com"
116116
NAUTOBOT_SSOT_IPFABRIC_SSL_VERIFY="True"
117117
NAUTOBOT_SSOT_IPFABRIC_TIMEOUT=15
118118

119-
NAUTOBOT_SSOT_ENABLE_ITENTIAL="True"
119+
NAUTOBOT_SSOT_ENABLE_ITENTIAL="False"
120120

121121
NAUTOBOT_SSOT_ENABLE_SLURPIT="False"
122122
SLURPIT_HOST="https://sandbox.slurpit.io"

docs/admin/release_notes/version_4.2.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ This document describes all new features and changes in the release. The format
99

1010
<!-- towncrier release notes start -->
1111

12+
## [v4.2.1 (2026-04-01)](https://github.com/nautobot/nautobot-app-ssot/releases/tag/v4.2.1)
13+
14+
### Fixed
15+
16+
- [#1155](https://github.com/nautobot/nautobot-app-ssot/issues/1155) - Support uv controlled development environment to run unittest.
17+
- [#1160](https://github.com/nautobot/nautobot-app-ssot/issues/1160) - Fixed a bug in the DNA Center integration where the get_locations method was not correctly paginating the results.
18+
- [#1162](https://github.com/nautobot/nautobot-app-ssot/issues/1162) - Fixed SSoT tests that assumed integration/example Job records were preloaded, so they now pass regardless of local integration enablement settings.
19+
- [#1165](https://github.com/nautobot/nautobot-app-ssot/issues/1165) - Fixed maintainer
20+
1221
## [v4.2.0 (2025-03-23)](https://github.com/nautobot/nautobot-app-ssot/releases/tag/v4.2.0)
1322

1423
### Added

nautobot_ssot/integrations/dna_center/utils/dna_center.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,11 @@ def get_locations(self):
4545
try:
4646
total_num_sites = self.conn.sites.get_site_count()["response"]
4747
offset = 1
48+
# Default page size is 50, max is 500, so we'll use a comfortable middle ground.
49+
limit = 200
4850
while len(loc_data) < total_num_sites:
49-
loc_data.extend(self.conn.sites.get_site(offset=offset)["response"])
50-
offset = len(loc_data)
51+
loc_data.extend(self.conn.sites.get_site(offset=offset, limit=limit)["response"])
52+
offset += limit
5153
for _, item in enumerate(loc_data):
5254
if item["siteNameHierarchy"] not in loc_names:
5355
loc_names.append(item["siteNameHierarchy"])

nautobot_ssot/integrations/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
def each_enabled_integration() -> Generator[str, None, None]:
1515
"""Return all enabled integrations."""
16-
config = settings.PLUGINS_CONFIG["nautobot_ssot"]
16+
config = settings.PLUGINS_CONFIG.get("nautobot_ssot", {})
1717

1818
for path in Path(__file__).parent.iterdir():
1919
if config.get(f"enable_{path.name}", False):

nautobot_ssot/jobs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
logger = logging.getLogger("nautobot.ssot")
1616

17-
hide_jobs_setting = settings.PLUGINS_CONFIG["nautobot_ssot"].get("hide_example_jobs", False)
17+
hide_jobs_setting = settings.PLUGINS_CONFIG.get("nautobot_ssot", {}).get("hide_example_jobs", False)
1818
if is_truthy(hide_jobs_setting):
1919
jobs = []
2020
else:

nautobot_ssot/tests/dna_center/test_jobs.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from nautobot.extras.models import (
1515
CustomField,
1616
ExternalIntegration,
17-
Job,
1817
JobLogEntry,
1918
JobResult,
2019
Secret,
@@ -31,6 +30,7 @@
3130
MULTI_LEVEL_LOCATION_FIXTURE,
3231
PORT_FIXTURE,
3332
)
33+
from nautobot_ssot.tests.utils.job_helpers import get_test_job_model
3434

3535

3636
class DnaCenterDataSourceJobTest(TestCase):
@@ -123,10 +123,7 @@ def setUp(self):
123123
self.test_loc = Location.objects.create(
124124
name="HQ", location_type=self.building_loctype, parent=us_region, status=self.status_active
125125
)
126-
self.job = Job.objects.get(
127-
job_class_name="DnaCenterDataSource",
128-
module_name="nautobot_ssot.integrations.dna_center.jobs",
129-
)
126+
self.job = get_test_job_model(jobs.DnaCenterDataSource)
130127
self.job_result = JobResult.objects.create(
131128
name=self.job.class_path, task_name="Fake task", user=None, id=uuid.uuid4()
132129
)

nautobot_ssot/tests/dna_center/test_utils_dna_center.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def setUp(self):
3333
self.verify = False
3434
self.dnac = DnaCenterClient(self.url, self.username, self.password, verify=self.verify)
3535
self.dnac.conn = MagicMock()
36-
self.dnac.conn.sites.get_site_count.return_value = {"response": 4}
36+
self.dnac.conn.sites.get_site_count.return_value = {"response": 31}
3737
self.dnac.conn.devices.get_device_count.return_value = {"response": 3}
3838

3939
self.mock_response = create_autospec(Response)
@@ -70,6 +70,26 @@ def test_get_locations(self):
7070
actual = self.dnac.get_locations()
7171
self.assertEqual(actual, LOCATION_FIXTURE)
7272

73+
def test_get_locations_pagination(self):
74+
"""Test the get_locations method correctly paginates without skipping items.
75+
76+
Simulates a 1-based offset API returning a fixed page size. With N total items
77+
and page_size P, an off-by-one in offset causes one duplicate per page. When the
78+
accumulated duplicates inflate loc_data to >= total before all unique items are
79+
fetched, the loop exits early and items are silently skipped.
80+
"""
81+
all_locations = RECV_LOCATION_FIXTURE["response"][:30]
82+
83+
def fake_get_site(offset=1, limit=10, **kwargs):
84+
"""Simulate 1-based offset API returning up to limit items."""
85+
start = offset - 1
86+
return {"response": all_locations[start : start + limit]}
87+
88+
self.dnac.conn.sites.get_site.side_effect = fake_get_site
89+
self.dnac.conn.sites.get_site_count.return_value = {"response": len(all_locations)}
90+
actual = self.dnac.get_locations()
91+
self.assertEqual(len(actual), len(all_locations))
92+
7393
def test_get_locations_catches_api_error(self):
7494
"""Test the get_locations method in DnaCenterClient catches dnacentersdkException."""
7595
self.dnac.conn.sites.get_site.side_effect = dnacentersdkException(self.mock_response)

nautobot_ssot/tests/itential/test_jobs.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
from django.test import override_settings
44
from nautobot.apps.testing import run_job_for_testing
5-
from nautobot.extras.models import Job, JobLogEntry
5+
from nautobot.extras.models import JobLogEntry
66

7+
from nautobot_ssot.integrations.itential.jobs import ItentialAutomationGatewayDataTarget
78
from nautobot_ssot.integrations.itential.models import AutomationGatewayModel
89
from nautobot_ssot.tests.itential.fixtures import base
10+
from nautobot_ssot.tests.utils.job_helpers import get_test_job_model
911

1012

1113
@override_settings(
@@ -22,10 +24,7 @@ class ItentialSSoTJobsTestCase(base.ItentialSSoTBaseTransactionTestCase):
2224

2325
def test_job_success(self):
2426
"""Test successful job."""
25-
self.job = Job.objects.get(
26-
job_class_name="ItentialAutomationGatewayDataTarget",
27-
module_name="nautobot_ssot.integrations.itential.jobs",
28-
)
27+
self.job = get_test_job_model(ItentialAutomationGatewayDataTarget)
2928
job_result = run_job_for_testing(
3029
self.job, dryrun=False, memory_profiling=False, gateway=self.gateway.pk, status=self.status.pk
3130
)
@@ -39,10 +38,7 @@ def test_job_success(self):
3938
def test_job_disabled_gateway(self):
4039
"""Test job with disabled automation gateway."""
4140
gateway = AutomationGatewayModel.objects.get(name="IAG10")
42-
self.job = Job.objects.get(
43-
job_class_name="ItentialAutomationGatewayDataTarget",
44-
module_name="nautobot_ssot.integrations.itential.jobs",
45-
)
41+
self.job = get_test_job_model(ItentialAutomationGatewayDataTarget)
4642
job_result = run_job_for_testing(
4743
self.job, dryrun=False, memory_profiling=False, gateway=gateway.pk, status=self.status.pk
4844
)

nautobot_ssot/tests/test_models.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
from django.test import TestCase
88
from django.utils.timezone import now
99
from nautobot.extras.choices import JobResultStatusChoices
10-
from nautobot.extras.models import Job, JobResult
10+
from nautobot.extras.models import JobResult
1111

12+
from nautobot_ssot.jobs.examples import ExampleDataSource, ExampleDataTarget
1213
from nautobot_ssot.models import Sync
14+
from nautobot_ssot.tests.utils.job_helpers import get_test_job_model
1315

1416

1517
class SyncTestCase(TestCase):
@@ -64,15 +66,17 @@ def test_get_source_target_url(self):
6466
self.assertIsNone(self.target_sync.get_source_url())
6567
self.assertIsNone(self.source_sync.get_target_url())
6668

69+
source_job_model = get_test_job_model(ExampleDataSource)
70+
target_job_model = get_test_job_model(ExampleDataTarget)
6771
self.source_sync.job_result = JobResult(
6872
name="ExampleDataSource",
69-
job_model=Job.objects.get(module_name="nautobot_ssot.jobs.examples", job_class_name="ExampleDataSource"),
73+
job_model=source_job_model,
7074
task_name="nautobot_ssot.jobs.examples.ExampleDataSource",
7175
worker="default",
7276
)
7377
self.target_sync.job_result = JobResult(
7478
name="ExampleDataTarget",
75-
job_model=Job.objects.get(module_name="nautobot_ssot.jobs.examples", job_class_name="ExampleDataTarget"),
79+
job_model=target_job_model,
7680
task_name="nautobot_ssot.jobs.examples.ExampleDataTarget",
7781
worker="default",
7882
)

0 commit comments

Comments
 (0)