nautobot 2.4.4__py3-none-any.whl → 2.4.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. nautobot/__init__.py +19 -3
  2. nautobot/core/api/mixins.py +10 -0
  3. nautobot/core/celery/__init__.py +5 -3
  4. nautobot/core/celery/encoders.py +2 -2
  5. nautobot/core/forms/fields.py +21 -5
  6. nautobot/core/forms/utils.py +1 -0
  7. nautobot/core/jobs/__init__.py +3 -2
  8. nautobot/core/jobs/bulk_actions.py +1 -1
  9. nautobot/core/management/commands/generate_test_data.py +1 -1
  10. nautobot/core/models/name_color_content_types.py +9 -0
  11. nautobot/core/models/validators.py +7 -0
  12. nautobot/core/settings.py +0 -14
  13. nautobot/core/settings.yaml +0 -28
  14. nautobot/core/tables.py +6 -1
  15. nautobot/core/templates/generic/object_retrieve.html +1 -1
  16. nautobot/core/testing/__init__.py +2 -0
  17. nautobot/core/testing/api.py +18 -0
  18. nautobot/core/testing/mixins.py +9 -0
  19. nautobot/core/tests/nautobot_config.py +0 -2
  20. nautobot/core/tests/runner.py +17 -140
  21. nautobot/core/tests/test_api.py +4 -4
  22. nautobot/core/tests/test_authentication.py +83 -4
  23. nautobot/core/tests/test_forms.py +11 -8
  24. nautobot/core/tests/test_graphql.py +9 -0
  25. nautobot/core/tests/test_jobs.py +33 -27
  26. nautobot/core/ui/object_detail.py +31 -0
  27. nautobot/dcim/factory.py +2 -0
  28. nautobot/dcim/filters/__init__.py +5 -0
  29. nautobot/dcim/forms.py +17 -1
  30. nautobot/dcim/migrations/0068_alter_softwareimagefile_download_url.py +19 -0
  31. nautobot/dcim/migrations/0069_softwareimagefile_external_integration.py +25 -0
  32. nautobot/dcim/models/devices.py +9 -2
  33. nautobot/dcim/tables/devices.py +1 -0
  34. nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +4 -0
  35. nautobot/dcim/tests/test_api.py +74 -31
  36. nautobot/dcim/tests/test_filters.py +2 -0
  37. nautobot/dcim/tests/test_jobs.py +4 -6
  38. nautobot/dcim/tests/test_models.py +65 -0
  39. nautobot/dcim/tests/test_views.py +3 -0
  40. nautobot/extras/choices.py +8 -3
  41. nautobot/extras/forms/forms.py +7 -3
  42. nautobot/extras/jobs.py +181 -103
  43. nautobot/extras/management/utils.py +13 -2
  44. nautobot/extras/models/datasources.py +4 -1
  45. nautobot/extras/models/jobs.py +20 -17
  46. nautobot/extras/plugins/marketplace_manifest.yml +18 -0
  47. nautobot/extras/tables.py +29 -34
  48. nautobot/extras/templates/extras/inc/panel_changelog.html +1 -1
  49. nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
  50. nautobot/extras/templates/extras/status.html +1 -37
  51. nautobot/extras/test_jobs/atomic_transaction.py +6 -6
  52. nautobot/extras/test_jobs/fail.py +75 -1
  53. nautobot/extras/tests/integration/test_notes.py +1 -1
  54. nautobot/extras/tests/test_api.py +23 -8
  55. nautobot/extras/tests/test_changelog.py +4 -4
  56. nautobot/extras/tests/test_customfields.py +3 -0
  57. nautobot/extras/tests/test_datasources.py +64 -54
  58. nautobot/extras/tests/test_jobs.py +69 -62
  59. nautobot/extras/tests/test_models.py +1 -1
  60. nautobot/extras/tests/test_plugins.py +19 -13
  61. nautobot/extras/tests/test_relationships.py +14 -5
  62. nautobot/extras/tests/test_tags.py +2 -2
  63. nautobot/extras/tests/test_views.py +15 -6
  64. nautobot/extras/urls.py +1 -30
  65. nautobot/extras/views.py +17 -55
  66. nautobot/ipam/forms.py +15 -0
  67. nautobot/ipam/querysets.py +6 -0
  68. nautobot/ipam/tables.py +6 -2
  69. nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
  70. nautobot/ipam/templates/ipam/rir.html +1 -43
  71. nautobot/ipam/templates/ipam/service.html +2 -46
  72. nautobot/ipam/templates/ipam/service_edit.html +1 -17
  73. nautobot/ipam/templates/ipam/service_retrieve.html +7 -0
  74. nautobot/ipam/tests/migration/__init__.py +0 -0
  75. nautobot/ipam/tests/migration/test_migrations.py +510 -0
  76. nautobot/ipam/tests/test_api.py +66 -36
  77. nautobot/ipam/tests/test_filters.py +0 -10
  78. nautobot/ipam/tests/test_models.py +16 -0
  79. nautobot/ipam/tests/test_views.py +44 -2
  80. nautobot/ipam/urls.py +2 -67
  81. nautobot/ipam/utils/migrations.py +185 -152
  82. nautobot/ipam/utils/testing.py +177 -0
  83. nautobot/ipam/views.py +119 -198
  84. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
  85. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +47 -0
  86. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -0
  87. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
  88. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +63 -0
  89. nautobot/project-static/docs/development/apps/api/testing.html +0 -87
  90. nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
  91. nautobot/project-static/docs/development/core/best-practices.html +3 -3
  92. nautobot/project-static/docs/development/core/getting-started.html +78 -107
  93. nautobot/project-static/docs/development/core/release-checklist.html +1 -1
  94. nautobot/project-static/docs/development/core/style-guide.html +1 -1
  95. nautobot/project-static/docs/development/core/testing.html +24 -198
  96. nautobot/project-static/docs/development/jobs/index.html +27 -14
  97. nautobot/project-static/docs/media/user-guide/administration/getting-started/nautobot-cloud.png +0 -0
  98. nautobot/project-static/docs/objects.inv +0 -0
  99. nautobot/project-static/docs/overview/application_stack.html +1 -1
  100. nautobot/project-static/docs/release-notes/version-2.4.html +409 -1
  101. nautobot/project-static/docs/requirements.txt +1 -1
  102. nautobot/project-static/docs/search/search_index.json +1 -1
  103. nautobot/project-static/docs/sitemap.xml +290 -290
  104. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  105. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -48
  106. nautobot/project-static/docs/user-guide/administration/guides/permissions.html +71 -0
  107. nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -1
  108. nautobot/project-static/docs/user-guide/administration/installation/index.html +257 -16
  109. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
  110. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
  111. nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -0
  112. nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
  113. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +8 -8
  114. nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -0
  115. nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +40 -25
  116. nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
  117. nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
  118. nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +77 -5
  119. nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
  120. nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -1
  121. nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
  122. nautobot/project-static/docs/user-guide/index.html +89 -2
  123. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +207 -122
  124. nautobot/virtualization/forms.py +20 -0
  125. nautobot/virtualization/templates/virtualization/clustergroup.html +1 -39
  126. nautobot/virtualization/templates/virtualization/clustertype.html +1 -0
  127. nautobot/virtualization/tests/test_api.py +14 -3
  128. nautobot/virtualization/tests/test_views.py +10 -2
  129. nautobot/virtualization/urls.py +10 -93
  130. nautobot/virtualization/views.py +33 -72
  131. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/METADATA +8 -7
  132. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/RECORD +137 -132
  133. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/WHEEL +1 -1
  134. nautobot/core/tests/performance_baselines.yml +0 -8900
  135. nautobot/ipam/tests/test_migrations.py +0 -462
  136. /nautobot/ipam/templates/ipam/{namespace_ipaddresses.html → namespace_ip_addresses.html} +0 -0
  137. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
  138. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
  139. {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/entry_points.txt +0 -0
@@ -1,38 +1,2 @@
1
1
  {% extends 'generic/object_retrieve.html' %}
2
- {% load helpers %}
3
-
4
- {% block content_left_page %}
5
- <div class="panel panel-default">
6
- <div class="panel-heading">
7
- <strong>Status</strong>
8
- </div>
9
- <table class="table table-hover panel-body attr-table">
10
- <tr>
11
- <td>Name</td>
12
- <td>
13
- {{ object.name }}
14
- </td>
15
- </tr>
16
- <tr>
17
- <td>Content Type(s)</td>
18
- <td>
19
- {% for ct in content_types %}
20
- {{ ct }}<br>
21
- {% endfor %}
22
- </td>
23
- </tr>
24
- <tr>
25
- <td>Color</td>
26
- <td>
27
- <span class="label color-block" style="background-color: #{{ object.color }}">&nbsp;</span>
28
- </td>
29
- </tr>
30
- <tr>
31
- <td>Description</td>
32
- <td>
33
- {{ object.description|placeholder }}
34
- </td>
35
- </tr>
36
- </table>
37
- </div>
38
- {% endblock content_left_page %}
2
+ {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
@@ -16,13 +16,13 @@ class TestAtomicDecorator(Job):
16
16
  Job that uses @transaction.atomic decorator to roll back changes.
17
17
  """
18
18
 
19
- fail = BooleanVar()
19
+ should_fail = BooleanVar()
20
20
 
21
21
  @transaction.atomic
22
- def run(self, fail=False): # pylint:disable=arguments-differ
22
+ def run(self, should_fail=False): # pylint:disable=arguments-differ
23
23
  try:
24
24
  Status.objects.create(name="Test database atomic rollback 1")
25
- if fail:
25
+ if should_fail:
26
26
  raise SimulatedError("simulated failure")
27
27
  except Exception:
28
28
  logger.error("Job failed, all database changes have been rolled back.")
@@ -35,13 +35,13 @@ class TestAtomicContextManager(Job):
35
35
  Job that uses `with transaction.atomic()` context manager to roll back changes.
36
36
  """
37
37
 
38
- fail = BooleanVar()
38
+ should_fail = BooleanVar()
39
39
 
40
- def run(self, fail=False): # pylint:disable=arguments-differ
40
+ def run(self, should_fail=False): # pylint:disable=arguments-differ
41
41
  try:
42
42
  with transaction.atomic():
43
43
  Status.objects.create(name="Test database atomic rollback 2")
44
- if fail:
44
+ if should_fail:
45
45
  raise SimulatedError("simulated failure")
46
46
  except Exception as err:
47
47
  logger.error("Job failed, all database changes have been rolled back.")
@@ -62,6 +62,20 @@ class TestFailJob(Job):
62
62
  logger.info("after_return() was called as expected")
63
63
 
64
64
 
65
+ class TestFailInBeforeStart(TestFailJob):
66
+ """
67
+ Job that raises an exception in before_start().
68
+ """
69
+
70
+ def before_start(self, task_id, args, kwargs):
71
+ super().before_start(task_id, args, kwargs)
72
+ logger.info("I'm a test job that fails!")
73
+ raise RunJobTaskFailed("Setup failure")
74
+
75
+ def run(self):
76
+ raise RuntimeError("run() was unexpectedly called after a failure in before_start()")
77
+
78
+
65
79
  class TestFailWithSanitization(Job):
66
80
  """
67
81
  Job with fail result that should be sanitized.
@@ -91,4 +105,64 @@ class TestFailWithSanitization(Job):
91
105
  raise exc
92
106
 
93
107
 
94
- register_jobs(TestFailJob, TestFailWithSanitization)
108
+ class TestFailCleanly(TestFailJob):
109
+ """
110
+ Job that fails "cleanly" through self.fail() instead of raising an exception.
111
+ """
112
+
113
+ def run(self): # pylint: disable=arguments-differ
114
+ logger.info("I'm a test job that fails!")
115
+ self.fail("Failure")
116
+ return "We failed"
117
+
118
+ def on_failure(self, exc, task_id, args, kwargs, einfo):
119
+ if exc != "We failed":
120
+ raise RuntimeError(f"Expected exc to be the message returned from run(), but it was {exc!r}")
121
+ if task_id != self.request.id: # pylint: disable=no-member
122
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}") # pylint: disable=no-member
123
+ if args:
124
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
125
+ if kwargs:
126
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
127
+ if einfo is not None:
128
+ raise RuntimeError(f"Expected einfo to be None, but it was {einfo!r}")
129
+ logger.info("on_failure() was called as expected")
130
+
131
+ def after_return(self, status, retval, task_id, args, kwargs, einfo):
132
+ if status is not JobResultStatusChoices.STATUS_FAILURE:
133
+ raise RuntimeError(f"Expected status to be {JobResultStatusChoices.STATUS_FAILURE}, but it was {status!r}")
134
+ if retval != "We failed":
135
+ raise RuntimeError(f"Expected retval to be the message returned from run(), but it was {retval!r}")
136
+ if task_id != self.request.id: # pylint: disable=no-member
137
+ raise RuntimeError(f"Expected task_id {task_id} to equal self.request.id {self.request.id}") # pylint: disable=no-member
138
+ if args:
139
+ raise RuntimeError(f"Expected args to be empty, but it was {args!r}")
140
+ if kwargs:
141
+ raise RuntimeError(f"Expected kwargs to be empty, but it was {kwargs!r}")
142
+ if einfo is not None:
143
+ raise RuntimeError(f"Expected einfo to be None, but it was {einfo!r}")
144
+ logger.info("after_return() was called as expected")
145
+
146
+
147
+ class TestFailCleanlyInBeforeStart(TestFailCleanly):
148
+ """
149
+ Job that fails "cleanly" during before_start() through self.fail() instead of raising an exception.
150
+ """
151
+
152
+ def before_start(self, task_id, args, kwargs):
153
+ super().before_start(task_id, args, kwargs)
154
+ logger.info("I'm a test job that fails!")
155
+ self.fail("We failed")
156
+ return "We failed"
157
+
158
+ def run(self):
159
+ raise RuntimeError("run() was unexpectedly called after a failure in before_start()")
160
+
161
+
162
+ register_jobs(
163
+ TestFailJob,
164
+ TestFailInBeforeStart,
165
+ TestFailWithSanitization,
166
+ TestFailCleanly,
167
+ TestFailCleanlyInBeforeStart,
168
+ )
@@ -42,4 +42,4 @@ class NoteTestCase(SeleniumTestCase):
42
42
  self.browser.find_by_text("Create").click()
43
43
 
44
44
  # Verify form redirect and presence of content.
45
- self.assertTrue(self.browser.is_text_present("Created Note"))
45
+ self.assertTrue(self.browser.is_text_present("Created note"))
@@ -292,7 +292,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
292
292
  schema = ConfigContextSchema.objects.create(
293
293
  name="Schema 1", data_schema={"type": "object", "properties": {"foo": {"type": "string"}}}
294
294
  )
295
- self.add_permissions("extras.add_configcontext")
295
+ self.add_permissions("extras.add_configcontext", "extras.view_configcontextschema")
296
296
 
297
297
  data = {
298
298
  "name": "Config Context with schema",
@@ -1917,7 +1917,7 @@ class JobTest(
1917
1917
  mock_get_worker_count.return_value = 1
1918
1918
  self.add_permissions("extras.run_job")
1919
1919
 
1920
- job_model = Job.objects.get(job_class_name="ExampleJob")
1920
+ job_model = Job.objects.get(job_class_name="TestHasSensitiveVariables")
1921
1921
  job_model.enabled = True
1922
1922
  job_model.validated_save()
1923
1923
 
@@ -2248,7 +2248,7 @@ class JobHookTest(APIViewTestCases.APIViewTestCase):
2248
2248
  "type_delete": True,
2249
2249
  }
2250
2250
 
2251
- self.add_permissions("extras.add_jobhook")
2251
+ self.add_permissions("extras.add_jobhook", "extras.view_job")
2252
2252
  response = self.client.post(self._get_list_url(), data, format="json", **self.header)
2253
2253
  self.assertContains(
2254
2254
  response,
@@ -2264,7 +2264,7 @@ class JobHookTest(APIViewTestCases.APIViewTestCase):
2264
2264
  "type_delete": True,
2265
2265
  }
2266
2266
 
2267
- self.add_permissions("extras.change_jobhook")
2267
+ self.add_permissions("extras.change_jobhook", "extras.view_job")
2268
2268
  job_hook2 = JobHook.objects.get(name="JobHook2")
2269
2269
  response = self.client.patch(self._get_detail_url(job_hook2), data, format="json", **self.header)
2270
2270
  self.assertContains(
@@ -2553,7 +2553,7 @@ class UserSavedViewAssociationTest(APIViewTestCases.APIViewTestCase):
2553
2553
  "saved_view": saved_view.pk,
2554
2554
  "view_name": duplicate_view_name,
2555
2555
  }
2556
- self.add_permissions("extras.add_usersavedviewassociation")
2556
+ self.add_permissions("extras.add_usersavedviewassociation", "users.view_user", "extras.view_savedview")
2557
2557
  response = self.client.post(
2558
2558
  self._get_list_url(), duplicate_user_to_savedview_create_data, format="json", **self.header
2559
2559
  )
@@ -3240,7 +3240,15 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
3240
3240
  location=existing_location_2,
3241
3241
  )
3242
3242
 
3243
- self.add_permissions("dcim.view_location", "dcim.add_location", "extras.add_relationshipassociation")
3243
+ self.add_permissions(
3244
+ "dcim.view_location",
3245
+ "dcim.view_locationtype",
3246
+ "dcim.view_device",
3247
+ "dcim.add_location",
3248
+ "extras.view_relationship",
3249
+ "extras.add_relationshipassociation",
3250
+ "extras.view_status",
3251
+ )
3244
3252
  response = self.client.post(
3245
3253
  reverse("dcim-api:location-list"),
3246
3254
  data={
@@ -3564,7 +3572,9 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
3564
3572
  ),
3565
3573
  ]
3566
3574
 
3567
- self.add_permissions("extras.add_relationshipassociation")
3575
+ self.add_permissions(
3576
+ "extras.add_relationshipassociation", "dcim.view_device", "dcim.view_location", "extras.view_relationship"
3577
+ )
3568
3578
 
3569
3579
  for side, field_error_name, data in associations:
3570
3580
  response = self.client.post(self._get_list_url(), data, format="json", **self.header)
@@ -3585,7 +3595,9 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
3585
3595
  "destination_id": self.devices[2].pk,
3586
3596
  }
3587
3597
 
3588
- self.add_permissions("extras.add_relationshipassociation")
3598
+ self.add_permissions(
3599
+ "extras.add_relationshipassociation", "extras.view_relationship", "dcim.view_device", "dcim.view_location"
3600
+ )
3589
3601
 
3590
3602
  response = self.client.post(self._get_list_url(), data, format="json", **self.header)
3591
3603
  self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
@@ -3636,8 +3648,11 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
3636
3648
  Check that relationship-associations can be updated via the 'relationships' field.
3637
3649
  """
3638
3650
  self.add_permissions(
3651
+ "dcim.view_device",
3639
3652
  "dcim.view_location",
3640
3653
  "dcim.change_location",
3654
+ "extras.view_relationship",
3655
+ "extras.view_relationshipassociation",
3641
3656
  "extras.add_relationshipassociation",
3642
3657
  "extras.delete_relationshipassociation",
3643
3658
  )
@@ -277,7 +277,7 @@ class ChangeLogAPITest(APITestCase):
277
277
  ],
278
278
  }
279
279
  url = reverse("dcim-api:location-list")
280
- self.add_permissions("dcim.add_location", "extras.view_status")
280
+ self.add_permissions("dcim.add_location", "dcim.view_locationtype", "extras.view_tag", "extras.view_status")
281
281
 
282
282
  response = self.client.post(url, data, format="json", **self.header)
283
283
  self.assertHttpStatus(response, status.HTTP_201_CREATED)
@@ -310,7 +310,7 @@ class ChangeLogAPITest(APITestCase):
310
310
  },
311
311
  "tags": [{"name": self.tags[2].name}],
312
312
  }
313
- self.add_permissions("dcim.change_location", "extras.view_status")
313
+ self.add_permissions("dcim.change_location", "extras.view_status", "dcim.view_locationtype", "extras.view_tag")
314
314
  url = reverse("dcim-api:location-detail", kwargs={"pk": location.pk})
315
315
 
316
316
  response = self.client.put(url, data, format="json", **self.header)
@@ -457,7 +457,7 @@ class ChangeLogAPITest(APITestCase):
457
457
  "status": self.statuses[0].pk,
458
458
  "location_type": location_type.pk,
459
459
  }
460
- self.add_permissions("dcim.add_location")
460
+ self.add_permissions("dcim.add_location", "dcim.view_locationtype", "extras.view_status")
461
461
  url = reverse("dcim-api:location-list")
462
462
 
463
463
  response = self.client.post(url, location_payload, format="json", **self.header)
@@ -492,7 +492,7 @@ class ChangeLogAPITest(APITestCase):
492
492
  )
493
493
 
494
494
  payload = {"tagged_vlans": [str(tagged_vlan.pk)], "description": "test vm interface m2m change"}
495
- self.add_permissions("virtualization.change_vminterface", "ipam.change_vlan")
495
+ self.add_permissions("virtualization.change_vminterface", "ipam.change_vlan", "ipam.view_vlan")
496
496
  url = reverse("virtualization-api:vminterface-detail", kwargs={"pk": vm_interface.pk})
497
497
  response = self.client.patch(url, payload, format="json", **self.header)
498
498
  vm_interface.refresh_from_db()
@@ -472,6 +472,9 @@ class CustomFieldDataAPITest(APITestCase):
472
472
  "dcim.add_location",
473
473
  "dcim.change_location",
474
474
  "dcim.view_location",
475
+ "dcim.view_locationtype",
476
+ "extras.view_status",
477
+ "extras.view_customfield",
475
478
  )
476
479
 
477
480
  def setUp(self):
@@ -238,11 +238,7 @@ class GitTest(TransactionTestCase):
238
238
  repository=self.repo.pk,
239
239
  )
240
240
 
241
- self.assertEqual(
242
- job_result.status,
243
- JobResultStatusChoices.STATUS_FAILURE,
244
- (job_result.result, list(job_result.job_log_entries.values_list("message", "log_object"))),
245
- )
241
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
246
242
  self.repo.refresh_from_db()
247
243
 
248
244
  log_entries = JobLogEntry.objects.filter(job_result=job_result)
@@ -308,11 +304,7 @@ class GitTest(TransactionTestCase):
308
304
  repository=self.repo.pk,
309
305
  )
310
306
 
311
- self.assertEqual(
312
- job_result.status,
313
- JobResultStatusChoices.STATUS_SUCCESS,
314
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
315
- )
307
+ self.assertJobResultStatus(job_result)
316
308
  self.repo.refresh_from_db()
317
309
  MockGitRepo.assert_called_with(
318
310
  os.path.join(tempdir, self.repo.slug),
@@ -331,11 +323,7 @@ class GitTest(TransactionTestCase):
331
323
  job_model = GitRepositorySync().job_model
332
324
  job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
333
325
  job_result.refresh_from_db()
334
- self.assertEqual(
335
- job_result.status,
336
- JobResultStatusChoices.STATUS_SUCCESS,
337
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
338
- )
326
+ self.assertJobResultStatus(job_result)
339
327
 
340
328
  # Make sure explicit ConfigContext was successfully loaded from file
341
329
  self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
@@ -383,11 +371,7 @@ class GitTest(TransactionTestCase):
383
371
  # Run the Git operation and refresh the object from the DB
384
372
  job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
385
373
  job_result.refresh_from_db()
386
- self.assertEqual(
387
- job_result.status,
388
- JobResultStatusChoices.STATUS_SUCCESS,
389
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
390
- )
374
+ self.assertJobResultStatus(job_result)
391
375
 
392
376
  # Verify that objects have been removed from the database
393
377
  self.assertEqual(
@@ -442,11 +426,7 @@ class GitTest(TransactionTestCase):
442
426
  )
443
427
  job_result.refresh_from_db()
444
428
 
445
- self.assertEqual(
446
- job_result.status,
447
- JobResultStatusChoices.STATUS_FAILURE,
448
- job_result.result,
449
- )
429
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
450
430
 
451
431
  # Due to transaction rollback on failure, the database should still/again match the pre-sync state, of
452
432
  # no records owned by the repository.
@@ -547,11 +527,7 @@ class GitTest(TransactionTestCase):
547
527
  )
548
528
  job_result.refresh_from_db()
549
529
 
550
- self.assertEqual(
551
- job_result.status,
552
- JobResultStatusChoices.STATUS_SUCCESS,
553
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
554
- )
530
+ self.assertJobResultStatus(job_result)
555
531
 
556
532
  # Make sure ConfigContext was successfully loaded from file
557
533
  config_context = ConfigContext.objects.get(
@@ -591,15 +567,7 @@ class GitTest(TransactionTestCase):
591
567
  delete_job_result = JobResult.objects.filter(name=repo_name).first()
592
568
  # Make sure we didn't get the wrong JobResult
593
569
  self.assertNotEqual(job_result, delete_job_result)
594
- self.assertEqual(
595
- delete_job_result.status,
596
- JobResultStatusChoices.STATUS_SUCCESS,
597
- (
598
- delete_job_result,
599
- delete_job_result.traceback,
600
- list(delete_job_result.job_log_entries.values_list("message", flat=True)),
601
- ),
602
- )
570
+ self.assertJobResultStatus(delete_job_result)
603
571
 
604
572
  with self.assertRaises(ConfigContext.DoesNotExist):
605
573
  ConfigContext.objects.get(
@@ -637,11 +605,7 @@ class GitTest(TransactionTestCase):
637
605
  job_model = GitRepositorySync().job_model
638
606
  job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
639
607
  job_result.refresh_from_db()
640
- self.assertEqual(
641
- job_result.status,
642
- JobResultStatusChoices.STATUS_SUCCESS,
643
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
644
- )
608
+ self.assertJobResultStatus(job_result)
645
609
 
646
610
  self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
647
611
  self.assert_implicit_config_context_exists("Location context")
@@ -673,11 +637,7 @@ class GitTest(TransactionTestCase):
673
637
  # Resync, attempting and failing to update to the new commit
674
638
  job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
675
639
  job_result.refresh_from_db()
676
- self.assertEqual(
677
- job_result.status,
678
- JobResultStatusChoices.STATUS_FAILURE,
679
- job_result.result,
680
- )
640
+ self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
681
641
  log_entries = JobLogEntry.objects.filter(job_result=job_result)
682
642
 
683
643
  # Assert database changes were rolled back
@@ -718,11 +678,7 @@ class GitTest(TransactionTestCase):
718
678
  )
719
679
  job_result.refresh_from_db()
720
680
 
721
- self.assertEqual(
722
- job_result.status,
723
- JobResultStatusChoices.STATUS_SUCCESS,
724
- (job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
725
- )
681
+ self.assertJobResultStatus(job_result)
726
682
 
727
683
  log_entries = JobLogEntry.objects.filter(job_result=job_result)
728
684
 
@@ -798,3 +754,57 @@ class GitTest(TransactionTestCase):
798
754
  "provides contents overlapping with this repository.",
799
755
  str(cm.exception),
800
756
  )
757
+
758
+ @mock.patch("nautobot.extras.models.datasources.GitRepo")
759
+ def test_clone_to_directory_with_secrets(self, MockGitRepo):
760
+ """
761
+ The clone_to_directory method should correctly make use of secrets.
762
+ """
763
+ with tempfile.TemporaryDirectory() as tempdir:
764
+ # Prepare secrets values
765
+ with open(os.path.join(tempdir, "username.txt"), "wt") as handle:
766
+ handle.write("núñez")
767
+
768
+ with open(os.path.join(tempdir, "token.txt"), "wt") as handle:
769
+ handle.write("1:3@/?=ab@")
770
+
771
+ # Create secrets and assign
772
+ username_secret = Secret.objects.create(
773
+ name="Git Username",
774
+ provider="text-file",
775
+ parameters={"path": os.path.join(tempdir, "username.txt")},
776
+ )
777
+ token_secret = Secret.objects.create(
778
+ name="Git Token",
779
+ provider="text-file",
780
+ parameters={"path": os.path.join(tempdir, "token.txt")},
781
+ )
782
+ secrets_group = SecretsGroup.objects.create(name="Git Credentials")
783
+ SecretsGroupAssociation.objects.create(
784
+ secret=username_secret,
785
+ secrets_group=secrets_group,
786
+ access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP,
787
+ secret_type=SecretsGroupSecretTypeChoices.TYPE_USERNAME,
788
+ )
789
+ SecretsGroupAssociation.objects.create(
790
+ secret=token_secret,
791
+ secrets_group=secrets_group,
792
+ access_type=SecretsGroupAccessTypeChoices.TYPE_HTTP,
793
+ secret_type=SecretsGroupSecretTypeChoices.TYPE_TOKEN,
794
+ )
795
+
796
+ # Configure GitRepository model
797
+ self.repo.secrets_group = secrets_group
798
+ self.repo.remote_url = "http://localhost/git.git"
799
+ self.repo.save()
800
+
801
+ # Try to clone it
802
+ self.repo.clone_to_directory(tempdir, "main")
803
+
804
+ # Assert that GitRepo was called with proper args
805
+ args, kwargs = MockGitRepo.call_args
806
+ path, from_url = args
807
+ self.assertTrue(path.startswith(os.path.join(tempdir, self.repo.slug)))
808
+ self.assertEqual(from_url, "http://n%C3%BA%C3%B1ez:1%3A3%40%2F%3F%3Dab%40@localhost/git.git")
809
+ self.assertEqual(kwargs["depth"], 0)
810
+ self.assertEqual(kwargs["branch"], "main")