nautobot 2.4.5__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/core/api/mixins.py +10 -0
- nautobot/core/celery/encoders.py +2 -2
- nautobot/core/forms/fields.py +21 -5
- nautobot/core/forms/utils.py +1 -0
- 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/api.py +18 -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 +7 -0
- 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_models.py +65 -0
- nautobot/dcim/tests/test_views.py +3 -0
- nautobot/extras/forms/forms.py +7 -3
- nautobot/extras/plugins/marketplace_manifest.yml +18 -0
- nautobot/extras/tables.py +4 -5
- 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/tests/integration/test_notes.py +1 -1
- nautobot/extras/tests/test_api.py +22 -7
- nautobot/extras/tests/test_changelog.py +4 -4
- nautobot/extras/tests/test_customfields.py +3 -0
- nautobot/extras/tests/test_plugins.py +19 -13
- nautobot/extras/tests/test_relationships.py +9 -0
- 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 +10 -54
- nautobot/ipam/tables.py +6 -2
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -41
- 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_views.py +44 -2
- nautobot/ipam/urls.py +2 -47
- nautobot/ipam/utils/migrations.py +185 -152
- nautobot/ipam/utils/testing.py +177 -0
- nautobot/ipam/views.py +95 -157
- 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/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/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 +226 -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.5.dist-info → nautobot-2.4.6.dist-info}/METADATA +6 -5
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/RECORD +113 -108
- {nautobot-2.4.5.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.5.dist-info → nautobot-2.4.6.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/NOTICE +0 -0
- {nautobot-2.4.5.dist-info → nautobot-2.4.6.dist-info}/entry_points.txt +0 -0
nautobot/ipam/tests/test_api.py
CHANGED
|
@@ -163,16 +163,6 @@ class VRFDeviceAssignmentTest(APIViewTestCases.APIViewTestCase):
|
|
|
163
163
|
virtual_machine=cls.test_vm,
|
|
164
164
|
rd="65000:4",
|
|
165
165
|
)
|
|
166
|
-
VRFDeviceAssignment.objects.create(
|
|
167
|
-
vrf=cls.vrfs[0],
|
|
168
|
-
virtual_device_context=cls.vdcs[0],
|
|
169
|
-
name="VRFDeviceAssignment 1",
|
|
170
|
-
rd="65000:5",
|
|
171
|
-
)
|
|
172
|
-
VRFDeviceAssignment.objects.create(
|
|
173
|
-
vrf=cls.vrfs[0],
|
|
174
|
-
virtual_device_context=cls.vdcs[1],
|
|
175
|
-
)
|
|
176
166
|
|
|
177
167
|
cls.update_data = {
|
|
178
168
|
"name": "VRFDeviceAssignment 2",
|
|
@@ -207,21 +197,30 @@ class VRFDeviceAssignmentTest(APIViewTestCases.APIViewTestCase):
|
|
|
207
197
|
|
|
208
198
|
def test_creating_invalid_vrf_device_assignments(self):
|
|
209
199
|
# Add object-level permission
|
|
210
|
-
self.add_permissions(
|
|
200
|
+
self.add_permissions(
|
|
201
|
+
"ipam.add_vrfdeviceassignment",
|
|
202
|
+
"dcim.view_device",
|
|
203
|
+
"dcim.view_virtualdevicecontext",
|
|
204
|
+
"ipam.view_vrf",
|
|
205
|
+
"virtualization.view_virtualmachine",
|
|
206
|
+
)
|
|
207
|
+
existing_vrf_device = VRFDeviceAssignment.objects.filter(device__isnull=False).first()
|
|
208
|
+
existing_vrf_vm = VRFDeviceAssignment.objects.filter(virtual_machine__isnull=False).first()
|
|
209
|
+
existing_vrf_vdc = VRFDeviceAssignment.objects.filter(virtual_device_context__isnull=False).first()
|
|
211
210
|
duplicate_create_data = [
|
|
212
211
|
{
|
|
213
|
-
"vrf":
|
|
214
|
-
"device":
|
|
212
|
+
"vrf": existing_vrf_device.vrf.pk,
|
|
213
|
+
"device": existing_vrf_device.device.pk,
|
|
215
214
|
"rd": "65000:6",
|
|
216
215
|
},
|
|
217
216
|
{
|
|
218
|
-
"vrf":
|
|
219
|
-
"virtual_machine":
|
|
217
|
+
"vrf": existing_vrf_vm.vrf.pk,
|
|
218
|
+
"virtual_machine": existing_vrf_vm.virtual_machine.pk,
|
|
220
219
|
"rd": "65000:6",
|
|
221
220
|
},
|
|
222
221
|
{
|
|
223
|
-
"vrf":
|
|
224
|
-
"virtual_device_context":
|
|
222
|
+
"vrf": existing_vrf_vdc.vrf.pk,
|
|
223
|
+
"virtual_device_context": existing_vrf_vdc.virtual_device_context.pk,
|
|
225
224
|
"rd": "65000:6",
|
|
226
225
|
},
|
|
227
226
|
]
|
|
@@ -311,7 +310,7 @@ class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
|
|
|
311
310
|
"vrf": self.vrfs[0].pk,
|
|
312
311
|
"prefix": None,
|
|
313
312
|
}
|
|
314
|
-
self.add_permissions("ipam.add_vrfprefixassignment")
|
|
313
|
+
self.add_permissions("ipam.add_vrfprefixassignment", "ipam.view_prefix", "ipam.view_vrf")
|
|
315
314
|
response = self.client.post(self._get_list_url(), duplicate_create_data, format="json", **self.header)
|
|
316
315
|
self.assertContains(
|
|
317
316
|
response, "The fields vrf, prefix must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
|
|
@@ -420,7 +419,16 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
420
419
|
"""
|
|
421
420
|
Tests for the 2.0/2.1 REST API of Prefixes.
|
|
422
421
|
"""
|
|
423
|
-
self.add_permissions(
|
|
422
|
+
self.add_permissions(
|
|
423
|
+
"dcim.view_location",
|
|
424
|
+
"ipam.view_prefix",
|
|
425
|
+
"ipam.add_prefix",
|
|
426
|
+
"ipam.change_prefix",
|
|
427
|
+
"ipam.view_ipaddress",
|
|
428
|
+
"ipam.view_namespace",
|
|
429
|
+
"ipam.view_rir",
|
|
430
|
+
"extras.view_status",
|
|
431
|
+
)
|
|
424
432
|
|
|
425
433
|
with self.subTest("valid GET"):
|
|
426
434
|
prefix = Prefix.objects.annotate(location_count=Count("locations")).filter(location_count=1).first()
|
|
@@ -516,7 +524,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
516
524
|
else:
|
|
517
525
|
self.fail("Suitable prefix fixture not found")
|
|
518
526
|
url = reverse("ipam-api:prefix-available-prefixes", kwargs={"pk": prefix.pk})
|
|
519
|
-
self.add_permissions("ipam.add_prefix")
|
|
527
|
+
self.add_permissions("ipam.add_prefix", "ipam.view_namespace", "extras.view_status", "extras.add_customfield")
|
|
520
528
|
|
|
521
529
|
# Create four available prefixes with individual requests
|
|
522
530
|
child_prefix_length = prefix.prefix_length + 2
|
|
@@ -566,7 +574,9 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
566
574
|
self.fail("Suitable prefix fixture not found")
|
|
567
575
|
|
|
568
576
|
url = reverse("ipam-api:prefix-available-prefixes", kwargs={"pk": prefix.pk})
|
|
569
|
-
self.add_permissions(
|
|
577
|
+
self.add_permissions(
|
|
578
|
+
"ipam.view_prefix", "ipam.add_prefix", "extras.view_status", "extras.add_customfield", "ipam.view_namespace"
|
|
579
|
+
)
|
|
570
580
|
|
|
571
581
|
# Try to create five prefixes (only four are available)
|
|
572
582
|
child_prefix_length = prefix.prefix_length + 2
|
|
@@ -626,7 +636,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
626
636
|
description="This is the Prefix created for whole network.",
|
|
627
637
|
)
|
|
628
638
|
url = reverse("ipam-api:prefix-available-prefixes", kwargs={"pk": prefix.pk})
|
|
629
|
-
self.add_permissions("ipam.view_prefix")
|
|
639
|
+
self.add_permissions("ipam.view_prefix", "ipam.view_namespace", "extras.view_status")
|
|
630
640
|
self.add_permissions(
|
|
631
641
|
"ipam.add_prefix", constraints={"description__startswith": "This is the Prefix created for"}
|
|
632
642
|
)
|
|
@@ -683,9 +693,10 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
683
693
|
description="This is the Prefix created for whole network.",
|
|
684
694
|
)
|
|
685
695
|
url = reverse("ipam-api:prefix-available-prefixes", kwargs={"pk": prefix.pk})
|
|
686
|
-
self.add_permissions("ipam.view_prefix")
|
|
696
|
+
self.add_permissions("ipam.view_prefix", "ipam.view_namespace", "extras.view_status")
|
|
687
697
|
self.add_permissions(
|
|
688
|
-
"ipam.add_prefix",
|
|
698
|
+
"ipam.add_prefix",
|
|
699
|
+
constraints={"description__startswith": "This is the Prefix created for"},
|
|
689
700
|
)
|
|
690
701
|
|
|
691
702
|
# Test invalid request
|
|
@@ -812,7 +823,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
812
823
|
namespace=self.namespace,
|
|
813
824
|
)
|
|
814
825
|
url = reverse("ipam-api:prefix-available-ips", kwargs={"pk": prefix.pk})
|
|
815
|
-
self.add_permissions("ipam.view_prefix", "ipam.add_ipaddress", "extras.view_status")
|
|
826
|
+
self.add_permissions("ipam.view_prefix", "ipam.add_ipaddress", "ipam.view_namespace", "extras.view_status")
|
|
816
827
|
|
|
817
828
|
data = {
|
|
818
829
|
"status": self.status.pk,
|
|
@@ -839,7 +850,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
839
850
|
cf = CustomField.objects.create(key="ipcf", label="IP Custom Field", type="text")
|
|
840
851
|
cf.content_types.add(ContentType.objects.get_for_model(IPAddress))
|
|
841
852
|
url = reverse("ipam-api:prefix-available-ips", kwargs={"pk": prefix.pk})
|
|
842
|
-
self.add_permissions("ipam.view_prefix", "ipam.add_ipaddress", "extras.view_status")
|
|
853
|
+
self.add_permissions("ipam.view_prefix", "ipam.view_namespace", "ipam.add_ipaddress", "extras.view_status")
|
|
843
854
|
|
|
844
855
|
# Create all six available IPs with individual requests
|
|
845
856
|
for i in range(1, 7):
|
|
@@ -875,7 +886,13 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
875
886
|
cf = CustomField.objects.create(key="ipcf", label="IP Custom Field", type="text")
|
|
876
887
|
cf.content_types.add(ContentType.objects.get_for_model(IPAddress))
|
|
877
888
|
url = reverse("ipam-api:prefix-available-ips", kwargs={"pk": prefix.pk})
|
|
878
|
-
self.add_permissions(
|
|
889
|
+
self.add_permissions(
|
|
890
|
+
"ipam.view_prefix",
|
|
891
|
+
"ipam.add_ipaddress",
|
|
892
|
+
"ipam.view_namespace",
|
|
893
|
+
"extras.view_customfield",
|
|
894
|
+
"extras.view_status",
|
|
895
|
+
)
|
|
879
896
|
|
|
880
897
|
# Try to create seven IPs (only six are available)
|
|
881
898
|
data = [
|
|
@@ -906,7 +923,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
906
923
|
description="This is the Prefix created for whole network.",
|
|
907
924
|
)
|
|
908
925
|
url = reverse("ipam-api:prefix-available-ips", kwargs={"pk": prefix.pk})
|
|
909
|
-
self.add_permissions("ipam.view_prefix", "ipam.view_ipaddress")
|
|
926
|
+
self.add_permissions("ipam.view_prefix", "ipam.view_ipaddress", "ipam.view_namespace", "extras.view_status")
|
|
910
927
|
self.add_permissions(
|
|
911
928
|
"ipam.add_ipaddress", constraints={"description__startswith": "This is the IP created for"}
|
|
912
929
|
)
|
|
@@ -959,7 +976,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
959
976
|
description="This is a Prefix created for whole network.",
|
|
960
977
|
)
|
|
961
978
|
url = reverse("ipam-api:prefix-available-ips", kwargs={"pk": prefix.pk})
|
|
962
|
-
self.add_permissions("ipam.view_prefix", "ipam.view_ipaddress")
|
|
979
|
+
self.add_permissions("ipam.view_prefix", "ipam.view_ipaddress", "ipam.view_namespace", "extras.view_status")
|
|
963
980
|
self.add_permissions(
|
|
964
981
|
"ipam.add_ipaddress", constraints={"description__startswith": "This is the IP created for"}
|
|
965
982
|
)
|
|
@@ -1183,7 +1200,7 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
|
|
1183
1200
|
|
|
1184
1201
|
def test_create_requires_parent_or_namespace(self):
|
|
1185
1202
|
"""Test that missing parent/namespace fields result in an error."""
|
|
1186
|
-
self.add_permissions("ipam.add_ipaddress")
|
|
1203
|
+
self.add_permissions("ipam.add_ipaddress", "extras.view_status")
|
|
1187
1204
|
data = {
|
|
1188
1205
|
"address": "192.168.0.10/32",
|
|
1189
1206
|
"status": self.statuses[0].pk,
|
|
@@ -1214,7 +1231,7 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
|
|
1214
1231
|
def test_create_multiple_outside_nat_success(self):
|
|
1215
1232
|
"""Validate NAT inside address can tie to multiple NAT outside addresses."""
|
|
1216
1233
|
# Create the two outside NAT IP Addresses tied back to the single inside NAT address
|
|
1217
|
-
self.add_permissions("ipam.add_ipaddress", "ipam.view_ipaddress")
|
|
1234
|
+
self.add_permissions("ipam.add_ipaddress", "ipam.view_ipaddress", "ipam.view_namespace", "extras.view_status")
|
|
1218
1235
|
nat_inside = IPAddress.objects.filter(nat_outside_list__isnull=True).first()
|
|
1219
1236
|
# Create NAT outside with above address IP as inside NAT
|
|
1220
1237
|
ip1 = self.client.post(
|
|
@@ -1251,7 +1268,9 @@ class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
|
|
1251
1268
|
self.assertEqual(response.data["nat_outside_list"][1]["address"], "192.168.0.20/24")
|
|
1252
1269
|
|
|
1253
1270
|
def test_creating_ipaddress_with_an_invalid_parent(self):
|
|
1254
|
-
self.add_permissions(
|
|
1271
|
+
self.add_permissions(
|
|
1272
|
+
"ipam.add_ipaddress", "extras.view_status", "ipam.view_prefix", "ipam.view_ipaddress", "ipam.view_namespace"
|
|
1273
|
+
)
|
|
1255
1274
|
prefixes = (
|
|
1256
1275
|
Prefix.objects.create(prefix="10.0.0.0/8", status=self.statuses[0], namespace=self.namespace),
|
|
1257
1276
|
Prefix.objects.create(prefix="192.168.0.0/25", status=self.statuses[0], namespace=self.namespace),
|
|
@@ -1403,6 +1422,8 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
1403
1422
|
"ipam.view_vlangroup",
|
|
1404
1423
|
"ipam.view_vlan",
|
|
1405
1424
|
"ipam.add_vlan",
|
|
1425
|
+
"extras.view_status",
|
|
1426
|
+
"extras.view_customfield",
|
|
1406
1427
|
)
|
|
1407
1428
|
|
|
1408
1429
|
# Create all nine available VLANs with individual requests
|
|
@@ -1445,6 +1466,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
1445
1466
|
"ipam.view_vlangroup",
|
|
1446
1467
|
"ipam.view_vlan",
|
|
1447
1468
|
"ipam.add_vlan",
|
|
1469
|
+
"extras.view_status",
|
|
1448
1470
|
)
|
|
1449
1471
|
|
|
1450
1472
|
# Try to create ten VLANs (only nine are available)
|
|
@@ -1492,6 +1514,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
1492
1514
|
"ipam.view_vlangroup",
|
|
1493
1515
|
"ipam.view_vlan",
|
|
1494
1516
|
"ipam.add_vlan",
|
|
1517
|
+
"extras.view_status",
|
|
1495
1518
|
)
|
|
1496
1519
|
|
|
1497
1520
|
# Try to create VLANs with specified VLAN IDs. Also, explicitly (and redundantly) specify a VLAN Group.
|
|
@@ -1562,6 +1585,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
1562
1585
|
self.add_permissions(
|
|
1563
1586
|
"ipam.view_vlangroup",
|
|
1564
1587
|
"ipam.view_vlan",
|
|
1588
|
+
"extras.view_status",
|
|
1565
1589
|
)
|
|
1566
1590
|
self.add_permissions("ipam.add_vlan", constraints={"description__startswith": "This is the VLAN created for"})
|
|
1567
1591
|
|
|
@@ -1609,6 +1633,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
1609
1633
|
self.add_permissions(
|
|
1610
1634
|
"ipam.view_vlangroup",
|
|
1611
1635
|
"ipam.view_vlan",
|
|
1636
|
+
"extras.view_status",
|
|
1612
1637
|
)
|
|
1613
1638
|
self.add_permissions("ipam.add_vlan", constraints={"description__startswith": "This is the VLAN created for"})
|
|
1614
1639
|
|
|
@@ -1714,9 +1739,14 @@ class VLANTest(APIViewTestCases.APIViewTestCase):
|
|
|
1714
1739
|
def test_vlan_2_1_api_version_response(self):
|
|
1715
1740
|
"""Assert location can be used in VLAN API create/retrieve."""
|
|
1716
1741
|
|
|
1717
|
-
self.add_permissions(
|
|
1718
|
-
|
|
1719
|
-
|
|
1742
|
+
self.add_permissions(
|
|
1743
|
+
"dcim.view_location",
|
|
1744
|
+
"ipam.view_vlan",
|
|
1745
|
+
"ipam.add_vlan",
|
|
1746
|
+
"ipam.change_vlan",
|
|
1747
|
+
"ipam.view_vlangroup",
|
|
1748
|
+
"extras.view_status",
|
|
1749
|
+
)
|
|
1720
1750
|
with self.subTest("Assert GET"):
|
|
1721
1751
|
vlan = VLAN.objects.annotate(locations_count=Count("locations")).filter(locations_count=1).first()
|
|
1722
1752
|
url = reverse("ipam-api:vlan-detail", kwargs={"pk": vlan.pk})
|
|
@@ -1878,7 +1908,7 @@ class ServiceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1878
1908
|
|
|
1879
1909
|
Ref: https://github.com/nautobot/nautobot/issues/265
|
|
1880
1910
|
"""
|
|
1881
|
-
self.add_permissions("ipam.add_service")
|
|
1911
|
+
self.add_permissions("ipam.add_service", "dcim.view_device")
|
|
1882
1912
|
url = reverse("ipam-api:service-list")
|
|
1883
1913
|
device = self.devices[0]
|
|
1884
1914
|
|
|
@@ -1108,16 +1108,6 @@ class VRFDeviceAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
1108
1108
|
virtual_machine=cls.test_vm_2,
|
|
1109
1109
|
rd="65000:4",
|
|
1110
1110
|
)
|
|
1111
|
-
VRFDeviceAssignment.objects.create(
|
|
1112
|
-
vrf=cls.vrfs[0],
|
|
1113
|
-
virtual_device_context=cls.vdcs[0],
|
|
1114
|
-
name="VRFDeviceAssignment 1",
|
|
1115
|
-
rd="65000:5",
|
|
1116
|
-
)
|
|
1117
|
-
VRFDeviceAssignment.objects.create(
|
|
1118
|
-
vrf=cls.vrfs[0],
|
|
1119
|
-
virtual_device_context=cls.vdcs[1],
|
|
1120
|
-
)
|
|
1121
1111
|
|
|
1122
1112
|
|
|
1123
1113
|
class VLANGroupTestCase(FilterTestCases.FilterTestCase):
|
|
@@ -6,6 +6,7 @@ from django.db.models import Count
|
|
|
6
6
|
from django.test import override_settings
|
|
7
7
|
from django.urls import reverse
|
|
8
8
|
from django.utils.html import strip_tags
|
|
9
|
+
from django.utils.http import urlencode
|
|
9
10
|
from django.utils.timezone import make_aware
|
|
10
11
|
from netaddr import IPNetwork
|
|
11
12
|
|
|
@@ -145,6 +146,25 @@ class RIRTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|
|
145
146
|
# Ensure that we have at least one RIR with no prefixes that can be used for the "delete_object" tests.
|
|
146
147
|
RIR.objects.create(name="RIR XYZ")
|
|
147
148
|
|
|
149
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
150
|
+
def test_list_objects_with_permission(self):
|
|
151
|
+
"""Test rendering of LinkedCountColumn for related fields without display_field override."""
|
|
152
|
+
response = super().test_list_objects_with_permission()
|
|
153
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
154
|
+
|
|
155
|
+
prefix_list_url = reverse(get_route_for_model(Prefix, "list"))
|
|
156
|
+
|
|
157
|
+
for rir in self._get_queryset().all():
|
|
158
|
+
if str(rir.pk) in response_body:
|
|
159
|
+
count = rir.prefixes.count()
|
|
160
|
+
if count > 1:
|
|
161
|
+
self.assertBodyContains(
|
|
162
|
+
response,
|
|
163
|
+
f'<a href="{prefix_list_url}?{urlencode({"rir": rir.name})}" class="badge">{count}</a>',
|
|
164
|
+
)
|
|
165
|
+
elif count == 1:
|
|
166
|
+
self.assertBodyContains(response, hyperlinked_object(rir.prefixes.first()))
|
|
167
|
+
|
|
148
168
|
|
|
149
169
|
class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase, ViewTestCases.ListObjectsViewTestCase):
|
|
150
170
|
model = Prefix
|
|
@@ -194,7 +214,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase, ViewTestCases.List
|
|
|
194
214
|
|
|
195
215
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
196
216
|
def test_list_objects_with_permission(self):
|
|
197
|
-
"""Test rendering of LinkedCountColumn for related fields."""
|
|
217
|
+
"""Test rendering of LinkedCountColumn for related fields with display_field override."""
|
|
198
218
|
response = super().test_list_objects_with_permission()
|
|
199
219
|
response_body = extract_page_body(response.content.decode(response.charset))
|
|
200
220
|
|
|
@@ -208,7 +228,7 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase, ViewTestCases.List
|
|
|
208
228
|
response, f'<a href="{locations_list_url}?prefixes={prefix.pk}" class="badge">{count}</a>'
|
|
209
229
|
)
|
|
210
230
|
elif count == 1:
|
|
211
|
-
self.assertBodyContains(response, hyperlinked_object(prefix.locations.first()))
|
|
231
|
+
self.assertBodyContains(response, hyperlinked_object(prefix.locations.first(), "name"))
|
|
212
232
|
|
|
213
233
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
214
234
|
def test_empty_queryset(self):
|
|
@@ -1239,3 +1259,25 @@ class ServiceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
1239
1259
|
}
|
|
1240
1260
|
response = self.client.post(**request)
|
|
1241
1261
|
self.assertBodyContains(response, "A service must be associated with either a device or a virtual machine.")
|
|
1262
|
+
|
|
1263
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1264
|
+
def test_port_bulk_edit_invalid(self):
|
|
1265
|
+
self.add_permissions("ipam.change_service")
|
|
1266
|
+
url = self._get_url("bulk_edit")
|
|
1267
|
+
pk_list = list(self._get_queryset().values_list("pk", flat=True)[:3])
|
|
1268
|
+
|
|
1269
|
+
data = {
|
|
1270
|
+
"pk": pk_list,
|
|
1271
|
+
"protocol": ServiceProtocolChoices.PROTOCOL_UDP,
|
|
1272
|
+
"ports": "[106,107]", # String representation of the list
|
|
1273
|
+
"description": "New description",
|
|
1274
|
+
"_apply": True,
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
response = self.client.post(url, data)
|
|
1278
|
+
response_content = response.content.decode(response.charset)
|
|
1279
|
+
self.assertHttpStatus(response, 200)
|
|
1280
|
+
self.assertInHTML(
|
|
1281
|
+
' <strong class="panel-title">Ports</strong>: <ul class="errorlist"><li>invalid literal for int() with base 10: '[106'</li></ul>',
|
|
1282
|
+
response_content,
|
|
1283
|
+
)
|
nautobot/ipam/urls.py
CHANGED
|
@@ -7,7 +7,6 @@ from . import views
|
|
|
7
7
|
from .models import (
|
|
8
8
|
IPAddress,
|
|
9
9
|
Prefix,
|
|
10
|
-
Service,
|
|
11
10
|
VLAN,
|
|
12
11
|
VLANGroup,
|
|
13
12
|
)
|
|
@@ -17,27 +16,12 @@ app_name = "ipam"
|
|
|
17
16
|
router = NautobotUIViewSetRouter()
|
|
18
17
|
router.register("ip-address-to-interface", views.IPAddressToInterfaceUIViewSet)
|
|
19
18
|
router.register("namespaces", views.NamespaceUIViewSet)
|
|
19
|
+
router.register("rirs", views.RIRUIViewSet)
|
|
20
20
|
router.register("route-targets", views.RouteTargetUIViewSet)
|
|
21
|
+
router.register("services", views.ServiceUIViewSet)
|
|
21
22
|
router.register("vrfs", views.VRFUIViewSet)
|
|
22
|
-
router.register("rirs", views.RIRUIViewSet)
|
|
23
23
|
|
|
24
24
|
urlpatterns = [
|
|
25
|
-
# Namespaces
|
|
26
|
-
path(
|
|
27
|
-
"namespaces/<uuid:pk>/ip-addresses/",
|
|
28
|
-
views.NamespaceIPAddressesView.as_view(),
|
|
29
|
-
name="namespace_ipaddresses",
|
|
30
|
-
),
|
|
31
|
-
path(
|
|
32
|
-
"namespaces/<uuid:pk>/prefixes/",
|
|
33
|
-
views.NamespacePrefixesView.as_view(),
|
|
34
|
-
name="namespace_prefixes",
|
|
35
|
-
),
|
|
36
|
-
path(
|
|
37
|
-
"namespaces/<uuid:pk>/vrfs/",
|
|
38
|
-
views.NamespaceVRFsView.as_view(),
|
|
39
|
-
name="namespace_vrfs",
|
|
40
|
-
),
|
|
41
25
|
# Prefixes
|
|
42
26
|
path("prefixes/", views.PrefixListView.as_view(), name="prefix_list"),
|
|
43
27
|
path("prefixes/add/", views.PrefixEditView.as_view(), name="prefix_add"),
|
|
@@ -222,35 +206,6 @@ urlpatterns = [
|
|
|
222
206
|
name="vlan_notes",
|
|
223
207
|
kwargs={"model": VLAN},
|
|
224
208
|
),
|
|
225
|
-
# Services
|
|
226
|
-
path("services/", views.ServiceListView.as_view(), name="service_list"),
|
|
227
|
-
path("services/add/", views.ServiceEditView.as_view(), name="service_add"),
|
|
228
|
-
path("services/import/", views.ServiceBulkImportView.as_view(), name="service_import"), # 3.0 TODO: remove, unused
|
|
229
|
-
path("services/edit/", views.ServiceBulkEditView.as_view(), name="service_bulk_edit"),
|
|
230
|
-
path(
|
|
231
|
-
"services/delete/",
|
|
232
|
-
views.ServiceBulkDeleteView.as_view(),
|
|
233
|
-
name="service_bulk_delete",
|
|
234
|
-
),
|
|
235
|
-
path("services/<uuid:pk>/", views.ServiceView.as_view(), name="service"),
|
|
236
|
-
path("services/<uuid:pk>/edit/", views.ServiceEditView.as_view(), name="service_edit"),
|
|
237
|
-
path(
|
|
238
|
-
"services/<uuid:pk>/delete/",
|
|
239
|
-
views.ServiceDeleteView.as_view(),
|
|
240
|
-
name="service_delete",
|
|
241
|
-
),
|
|
242
|
-
path(
|
|
243
|
-
"services/<uuid:pk>/changelog/",
|
|
244
|
-
ObjectChangeLogView.as_view(),
|
|
245
|
-
name="service_changelog",
|
|
246
|
-
kwargs={"model": Service},
|
|
247
|
-
),
|
|
248
|
-
path(
|
|
249
|
-
"services/<uuid:pk>/notes/",
|
|
250
|
-
ObjectNotesView.as_view(),
|
|
251
|
-
name="service_notes",
|
|
252
|
-
kwargs={"model": Service},
|
|
253
|
-
),
|
|
254
209
|
]
|
|
255
210
|
|
|
256
211
|
urlpatterns += router.urls
|