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.
- nautobot/__init__.py +19 -3
- nautobot/core/api/mixins.py +10 -0
- nautobot/core/celery/__init__.py +5 -3
- nautobot/core/celery/encoders.py +2 -2
- nautobot/core/forms/fields.py +21 -5
- nautobot/core/forms/utils.py +1 -0
- nautobot/core/jobs/__init__.py +3 -2
- nautobot/core/jobs/bulk_actions.py +1 -1
- nautobot/core/management/commands/generate_test_data.py +1 -1
- nautobot/core/models/name_color_content_types.py +9 -0
- nautobot/core/models/validators.py +7 -0
- nautobot/core/settings.py +0 -14
- nautobot/core/settings.yaml +0 -28
- nautobot/core/tables.py +6 -1
- nautobot/core/templates/generic/object_retrieve.html +1 -1
- nautobot/core/testing/__init__.py +2 -0
- nautobot/core/testing/api.py +18 -0
- nautobot/core/testing/mixins.py +9 -0
- nautobot/core/tests/nautobot_config.py +0 -2
- nautobot/core/tests/runner.py +17 -140
- nautobot/core/tests/test_api.py +4 -4
- nautobot/core/tests/test_authentication.py +83 -4
- nautobot/core/tests/test_forms.py +11 -8
- nautobot/core/tests/test_graphql.py +9 -0
- nautobot/core/tests/test_jobs.py +33 -27
- nautobot/core/ui/object_detail.py +31 -0
- nautobot/dcim/factory.py +2 -0
- nautobot/dcim/filters/__init__.py +5 -0
- nautobot/dcim/forms.py +17 -1
- nautobot/dcim/migrations/0068_alter_softwareimagefile_download_url.py +19 -0
- nautobot/dcim/migrations/0069_softwareimagefile_external_integration.py +25 -0
- nautobot/dcim/models/devices.py +9 -2
- nautobot/dcim/tables/devices.py +1 -0
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +4 -0
- nautobot/dcim/tests/test_api.py +74 -31
- nautobot/dcim/tests/test_filters.py +2 -0
- nautobot/dcim/tests/test_jobs.py +4 -6
- nautobot/dcim/tests/test_models.py +65 -0
- nautobot/dcim/tests/test_views.py +3 -0
- nautobot/extras/choices.py +8 -3
- nautobot/extras/forms/forms.py +7 -3
- nautobot/extras/jobs.py +181 -103
- nautobot/extras/management/utils.py +13 -2
- nautobot/extras/models/datasources.py +4 -1
- nautobot/extras/models/jobs.py +20 -17
- nautobot/extras/plugins/marketplace_manifest.yml +18 -0
- nautobot/extras/tables.py +29 -34
- nautobot/extras/templates/extras/inc/panel_changelog.html +1 -1
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
- nautobot/extras/templates/extras/status.html +1 -37
- nautobot/extras/test_jobs/atomic_transaction.py +6 -6
- nautobot/extras/test_jobs/fail.py +75 -1
- nautobot/extras/tests/integration/test_notes.py +1 -1
- nautobot/extras/tests/test_api.py +23 -8
- nautobot/extras/tests/test_changelog.py +4 -4
- nautobot/extras/tests/test_customfields.py +3 -0
- nautobot/extras/tests/test_datasources.py +64 -54
- nautobot/extras/tests/test_jobs.py +69 -62
- nautobot/extras/tests/test_models.py +1 -1
- nautobot/extras/tests/test_plugins.py +19 -13
- nautobot/extras/tests/test_relationships.py +14 -5
- nautobot/extras/tests/test_tags.py +2 -2
- nautobot/extras/tests/test_views.py +15 -6
- nautobot/extras/urls.py +1 -30
- nautobot/extras/views.py +17 -55
- nautobot/ipam/forms.py +15 -0
- nautobot/ipam/querysets.py +6 -0
- nautobot/ipam/tables.py +6 -2
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
- nautobot/ipam/templates/ipam/rir.html +1 -43
- nautobot/ipam/templates/ipam/service.html +2 -46
- nautobot/ipam/templates/ipam/service_edit.html +1 -17
- nautobot/ipam/templates/ipam/service_retrieve.html +7 -0
- nautobot/ipam/tests/migration/__init__.py +0 -0
- nautobot/ipam/tests/migration/test_migrations.py +510 -0
- nautobot/ipam/tests/test_api.py +66 -36
- nautobot/ipam/tests/test_filters.py +0 -10
- nautobot/ipam/tests/test_models.py +16 -0
- nautobot/ipam/tests/test_views.py +44 -2
- nautobot/ipam/urls.py +2 -67
- nautobot/ipam/utils/migrations.py +185 -152
- nautobot/ipam/utils/testing.py +177 -0
- nautobot/ipam/views.py +119 -198
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +47 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +18 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +63 -0
- nautobot/project-static/docs/development/apps/api/testing.html +0 -87
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
- nautobot/project-static/docs/development/core/best-practices.html +3 -3
- nautobot/project-static/docs/development/core/getting-started.html +78 -107
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +1 -1
- nautobot/project-static/docs/development/core/testing.html +24 -198
- nautobot/project-static/docs/development/jobs/index.html +27 -14
- nautobot/project-static/docs/media/user-guide/administration/getting-started/nautobot-cloud.png +0 -0
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.4.html +409 -1
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -48
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +71 -0
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +3 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +257 -16
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +4 -0
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +11 -11
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +8 -8
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -0
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +40 -25
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +4 -4
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +77 -5
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +0 -1
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/index.html +89 -2
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +207 -122
- nautobot/virtualization/forms.py +20 -0
- nautobot/virtualization/templates/virtualization/clustergroup.html +1 -39
- nautobot/virtualization/templates/virtualization/clustertype.html +1 -0
- nautobot/virtualization/tests/test_api.py +14 -3
- nautobot/virtualization/tests/test_views.py +10 -2
- nautobot/virtualization/urls.py +10 -93
- nautobot/virtualization/views.py +33 -72
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/METADATA +8 -7
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/RECORD +137 -132
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/WHEEL +1 -1
- nautobot/core/tests/performance_baselines.yml +0 -8900
- nautobot/ipam/tests/test_migrations.py +0 -462
- /nautobot/ipam/templates/ipam/{namespace_ipaddresses.html → namespace_ip_addresses.html} +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.4.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
- {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
|
-
{%
|
|
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 }}"> </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
|
-
|
|
19
|
+
should_fail = BooleanVar()
|
|
20
20
|
|
|
21
21
|
@transaction.atomic
|
|
22
|
-
def run(self,
|
|
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
|
|
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
|
-
|
|
38
|
+
should_fail = BooleanVar()
|
|
39
39
|
|
|
40
|
-
def run(self,
|
|
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
|
|
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
|
-
|
|
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
|
+
)
|
|
@@ -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="
|
|
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(
|
|
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(
|
|
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(
|
|
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()
|
|
@@ -238,11 +238,7 @@ class GitTest(TransactionTestCase):
|
|
|
238
238
|
repository=self.repo.pk,
|
|
239
239
|
)
|
|
240
240
|
|
|
241
|
-
self.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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")
|