nautobot 2.4.3__py3-none-any.whl → 2.4.5__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.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/__init__.py +19 -3
- nautobot/apps/filters.py +2 -0
- nautobot/circuits/filters.py +1 -1
- nautobot/circuits/tests/test_models.py +5 -3
- nautobot/cloud/filters.py +3 -6
- nautobot/cloud/tests/test_filters.py +21 -0
- nautobot/core/admin.py +2 -0
- nautobot/core/celery/__init__.py +5 -3
- nautobot/core/jobs/__init__.py +5 -3
- nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
- nautobot/core/models/utils.py +6 -1
- nautobot/core/templates/inc/javascript.html +1 -0
- nautobot/core/templatetags/ui_framework.py +20 -4
- nautobot/core/testing/__init__.py +2 -0
- nautobot/core/testing/forms.py +1 -1
- nautobot/core/testing/mixins.py +9 -0
- nautobot/core/tests/test_api.py +1 -1
- nautobot/core/tests/test_graphql.py +3 -3
- nautobot/core/tests/test_jobs.py +30 -28
- nautobot/core/ui/object_detail.py +1 -1
- nautobot/dcim/api/serializers.py +36 -0
- nautobot/dcim/api/views.py +1 -1
- nautobot/dcim/elevations.py +17 -4
- nautobot/dcim/factory.py +9 -1
- nautobot/dcim/filters/__init__.py +27 -1
- nautobot/dcim/forms.py +13 -1
- nautobot/dcim/models/devices.py +11 -5
- nautobot/dcim/signals.py +26 -0
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
- nautobot/dcim/tests/test_api.py +176 -0
- nautobot/dcim/tests/test_filters.py +56 -3
- nautobot/dcim/tests/test_jobs.py +4 -6
- nautobot/dcim/tests/test_models.py +40 -0
- nautobot/dcim/views.py +24 -14
- nautobot/extras/api/mixins.py +1 -1
- nautobot/extras/api/views.py +2 -2
- nautobot/extras/choices.py +8 -3
- nautobot/extras/filters/__init__.py +4 -0
- nautobot/extras/jobs.py +181 -103
- nautobot/extras/management/utils.py +13 -2
- nautobot/extras/models/datasources.py +11 -4
- nautobot/extras/models/jobs.py +20 -17
- nautobot/extras/plugins/__init__.py +26 -1
- nautobot/extras/tables.py +25 -29
- nautobot/extras/templates/extras/inc/jobresult.html +12 -13
- nautobot/extras/templates/extras/objectchange.html +28 -12
- nautobot/extras/test_jobs/atomic_transaction.py +6 -6
- nautobot/extras/test_jobs/fail.py +75 -1
- nautobot/extras/tests/test_api.py +17 -16
- nautobot/extras/tests/test_datasources.py +64 -54
- nautobot/extras/tests/test_filters.py +2 -0
- nautobot/extras/tests/test_jobs.py +69 -62
- nautobot/extras/tests/test_models.py +1 -1
- nautobot/extras/tests/test_plugins.py +32 -1
- nautobot/extras/tests/test_relationships.py +5 -5
- nautobot/extras/tests/test_views.py +12 -2
- nautobot/extras/views.py +10 -1
- nautobot/ipam/api/serializers.py +7 -8
- nautobot/ipam/api/views.py +2 -2
- nautobot/ipam/factory.py +27 -8
- nautobot/ipam/filters.py +67 -29
- nautobot/ipam/formfields.py +51 -0
- nautobot/ipam/forms.py +28 -1
- nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
- nautobot/ipam/models.py +63 -5
- nautobot/ipam/querysets.py +6 -0
- nautobot/ipam/tables.py +21 -7
- nautobot/ipam/templates/ipam/rir.html +1 -43
- nautobot/ipam/tests/test_api.py +107 -66
- nautobot/ipam/tests/test_filters.py +145 -5
- nautobot/ipam/tests/test_models.py +16 -0
- nautobot/ipam/tests/test_views.py +15 -2
- nautobot/ipam/urls.py +1 -21
- nautobot/ipam/views.py +24 -41
- nautobot/project-static/css/base.css +11 -0
- nautobot/project-static/css/dark.css +2 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
- nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
- nautobot/project-static/docs/development/apps/api/testing.html +0 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
- nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
- nautobot/project-static/docs/development/apps/index.html +2 -35
- nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +0 -6
- nautobot/project-static/docs/development/core/best-practices.html +0 -27
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
- nautobot/project-static/docs/development/core/getting-started.html +12 -16
- nautobot/project-static/docs/development/core/homepage.html +0 -3
- nautobot/project-static/docs/development/core/style-guide.html +0 -5
- nautobot/project-static/docs/development/core/templates.html +0 -3
- nautobot/project-static/docs/development/core/testing.html +0 -9
- nautobot/project-static/docs/development/jobs/index.html +30 -43
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +0 -18
- nautobot/project-static/docs/release-notes/version-2.4.html +374 -0
- nautobot/project-static/docs/requirements.txt +2 -2
- 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 +0 -10
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
- nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
- nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
- nautobot/project-static/js/editor.js +292 -0
- nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
- nautobot/tenancy/filters/__init__.py +3 -5
- nautobot/tenancy/tests/test_filters.py +10 -0
- nautobot/virtualization/views.py +0 -1
- nautobot/wireless/tables.py +9 -4
- nautobot/wireless/tests/test_api.py +0 -9
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/METADATA +4 -4
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/RECORD +198 -186
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -1716,6 +1716,182 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1716
1716
|
)
|
|
1717
1717
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1718
1718
|
|
|
1719
|
+
def _parent_device_test_data(self):
|
|
1720
|
+
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
1721
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
1722
|
+
device_role = Role.objects.get_for_model(Device).first()
|
|
1723
|
+
device_type = DeviceType.objects.first()
|
|
1724
|
+
|
|
1725
|
+
device_type_parent = DeviceType.objects.create(
|
|
1726
|
+
manufacturer=device_type.manufacturer,
|
|
1727
|
+
model=f"{device_type.model} Parent",
|
|
1728
|
+
u_height=device_type.u_height,
|
|
1729
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
|
|
1730
|
+
)
|
|
1731
|
+
device_type_child = DeviceType.objects.create(
|
|
1732
|
+
manufacturer=device_type.manufacturer,
|
|
1733
|
+
model=f"{device_type.model} Child",
|
|
1734
|
+
u_height=device_type.u_height,
|
|
1735
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
parent_device = Device.objects.create(
|
|
1739
|
+
device_type=device_type_parent,
|
|
1740
|
+
role=device_role,
|
|
1741
|
+
status=device_status,
|
|
1742
|
+
name="Device Parent",
|
|
1743
|
+
location=location,
|
|
1744
|
+
)
|
|
1745
|
+
device_bay_1 = DeviceBay.objects.create(name="db1", device_id=parent_device.pk)
|
|
1746
|
+
device_bay_2 = DeviceBay.objects.create(name="db2", device_id=parent_device.pk)
|
|
1747
|
+
|
|
1748
|
+
return parent_device, device_bay_1, device_bay_2, device_type_child
|
|
1749
|
+
|
|
1750
|
+
def test_creating_device_with_parent_bay(self):
|
|
1751
|
+
# Create test data
|
|
1752
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1753
|
+
|
|
1754
|
+
self.add_permissions("dcim.add_device")
|
|
1755
|
+
url = reverse("dcim-api:device-list")
|
|
1756
|
+
|
|
1757
|
+
# Test creating device with parent bay by device bay data
|
|
1758
|
+
data = {
|
|
1759
|
+
"device_type": device_type_child.pk,
|
|
1760
|
+
"role": parent_device.role.pk,
|
|
1761
|
+
"location": parent_device.location.pk,
|
|
1762
|
+
"name": "Device parent bay test #1",
|
|
1763
|
+
"status": parent_device.status.pk,
|
|
1764
|
+
"parent_bay": {"device": {"name": parent_device.name}, "name": device_bay_1.name},
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1768
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1769
|
+
|
|
1770
|
+
created_device = Device.objects.get(name="Device parent bay test #1")
|
|
1771
|
+
self.assertEqual(created_device.parent_bay.pk, device_bay_1.pk)
|
|
1772
|
+
|
|
1773
|
+
# Test creating device with parent bay by device_bay.pk
|
|
1774
|
+
data = {
|
|
1775
|
+
"device_type": device_type_child.pk,
|
|
1776
|
+
"role": parent_device.role.pk,
|
|
1777
|
+
"location": parent_device.location.pk,
|
|
1778
|
+
"name": "Device parent bay test #2",
|
|
1779
|
+
"status": parent_device.status.pk,
|
|
1780
|
+
"parent_bay": device_bay_2.pk,
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1784
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1785
|
+
|
|
1786
|
+
created_device = Device.objects.get(name="Device parent bay test #2")
|
|
1787
|
+
self.assertEqual(created_device.parent_bay.pk, device_bay_2.pk)
|
|
1788
|
+
|
|
1789
|
+
# Test creating device with parent bay already taken
|
|
1790
|
+
data = {
|
|
1791
|
+
"device_type": device_type_child.pk,
|
|
1792
|
+
"role": parent_device.role.pk,
|
|
1793
|
+
"location": parent_device.location.pk,
|
|
1794
|
+
"name": "Device parent bay test #3",
|
|
1795
|
+
"status": parent_device.status.pk,
|
|
1796
|
+
"parent_bay": device_bay_1.pk,
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1800
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1801
|
+
self.assertIn("Cannot install device; parent bay is already taken", response.content.decode(response.charset))
|
|
1802
|
+
|
|
1803
|
+
# Assert that on the #1 device, parent bay stayed the same
|
|
1804
|
+
old_device = Device.objects.get(name="Device parent bay test #1")
|
|
1805
|
+
self.assertEqual(old_device.parent_bay.pk, device_bay_1.pk)
|
|
1806
|
+
|
|
1807
|
+
def test_update_device_with_parent_bay(self):
|
|
1808
|
+
# Create test data
|
|
1809
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1810
|
+
|
|
1811
|
+
self.add_permissions("dcim.change_device")
|
|
1812
|
+
|
|
1813
|
+
child_device = Device.objects.create(
|
|
1814
|
+
device_type=device_type_child,
|
|
1815
|
+
role=parent_device.role,
|
|
1816
|
+
location=parent_device.location,
|
|
1817
|
+
name="Device parent bay test #4",
|
|
1818
|
+
status=parent_device.status,
|
|
1819
|
+
)
|
|
1820
|
+
# Test setting parent bay during the update
|
|
1821
|
+
patch_data = {"parent_bay": device_bay_1.pk}
|
|
1822
|
+
response = self.client.patch(self._get_detail_url(child_device), patch_data, format="json", **self.header)
|
|
1823
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1824
|
+
|
|
1825
|
+
updated_device = Device.objects.get(name="Device parent bay test #4")
|
|
1826
|
+
self.assertEqual(updated_device.parent_bay.pk, device_bay_1.pk)
|
|
1827
|
+
|
|
1828
|
+
# Changing the parent bay is not allowed without removing it first
|
|
1829
|
+
patch_data = {"parent_bay": device_bay_2.pk}
|
|
1830
|
+
response = self.client.patch(self._get_detail_url(child_device), patch_data, format="json", **self.header)
|
|
1831
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1832
|
+
self.assertIn(
|
|
1833
|
+
f"Cannot install the specified device; device is already installed in {device_bay_1.name}",
|
|
1834
|
+
response.content.decode(response.charset),
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
# Assert that parent bay stayed the same
|
|
1838
|
+
updated_device = Device.objects.get(name="Device parent bay test #4")
|
|
1839
|
+
self.assertEqual(updated_device.parent_bay.pk, device_bay_1.pk)
|
|
1840
|
+
|
|
1841
|
+
def test_reassign_device_to_parent_bay(self):
|
|
1842
|
+
# Create test data
|
|
1843
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1844
|
+
device_name = "Device parent bay test #5"
|
|
1845
|
+
child_device = Device.objects.create(
|
|
1846
|
+
device_type=device_type_child,
|
|
1847
|
+
role=parent_device.role,
|
|
1848
|
+
location=parent_device.location,
|
|
1849
|
+
name=device_name,
|
|
1850
|
+
status=parent_device.status,
|
|
1851
|
+
)
|
|
1852
|
+
device_bay_1.installed_device = child_device
|
|
1853
|
+
device_bay_1.save()
|
|
1854
|
+
|
|
1855
|
+
self.add_permissions("dcim.change_device", "dcim.view_device", "dcim.change_devicebay")
|
|
1856
|
+
child_device_detail_url = self._get_detail_url(child_device)
|
|
1857
|
+
|
|
1858
|
+
response = self.client.get(child_device_detail_url, **self.header)
|
|
1859
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1860
|
+
self.assertEqual(response.json()["parent_bay"]["id"], str(device_bay_1.pk))
|
|
1861
|
+
|
|
1862
|
+
# Test unassigning parent bay
|
|
1863
|
+
patch_data = {"parent_bay": None}
|
|
1864
|
+
response = self.client.patch(child_device_detail_url, patch_data, format="json", **self.header)
|
|
1865
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1866
|
+
|
|
1867
|
+
child_device.refresh_from_db()
|
|
1868
|
+
with self.assertRaises(DeviceBay.DoesNotExist):
|
|
1869
|
+
child_device.parent_bay
|
|
1870
|
+
|
|
1871
|
+
# And assign it again
|
|
1872
|
+
patch_data = {"parent_bay": device_bay_2.pk}
|
|
1873
|
+
response = self.client.patch(child_device_detail_url, patch_data, format="json", **self.header)
|
|
1874
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1875
|
+
|
|
1876
|
+
child_device.refresh_from_db()
|
|
1877
|
+
self.assertEqual(child_device.parent_bay.pk, device_bay_2.pk)
|
|
1878
|
+
|
|
1879
|
+
# Unassign it through device bay
|
|
1880
|
+
patch_data = {"installed_device": None}
|
|
1881
|
+
response = self.client.patch(self._get_detail_url(device_bay_2), patch_data, format="json", **self.header)
|
|
1882
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1883
|
+
|
|
1884
|
+
child_device.refresh_from_db()
|
|
1885
|
+
self.assertFalse(hasattr(child_device, "parent_bay"))
|
|
1886
|
+
|
|
1887
|
+
# And assign through device bay
|
|
1888
|
+
patch_data = {"installed_device": child_device.pk}
|
|
1889
|
+
response = self.client.patch(self._get_detail_url(device_bay_1), patch_data, format="json", **self.header)
|
|
1890
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1891
|
+
|
|
1892
|
+
child_device.refresh_from_db()
|
|
1893
|
+
self.assertEqual(child_device.parent_bay.pk, device_bay_1.pk)
|
|
1894
|
+
|
|
1719
1895
|
|
|
1720
1896
|
class ModuleTestCase(APIViewTestCases.APIViewTestCase):
|
|
1721
1897
|
model = Module
|
|
@@ -157,6 +157,8 @@ def common_test_data(cls):
|
|
|
157
157
|
cls.loc0 = loc0
|
|
158
158
|
cls.loc1 = loc1
|
|
159
159
|
cls.nested_loc = nested_loc
|
|
160
|
+
cls.loc2 = loc2
|
|
161
|
+
cls.loc3 = loc3
|
|
160
162
|
|
|
161
163
|
provider = Provider.objects.first()
|
|
162
164
|
circuit_type = CircuitType.objects.first()
|
|
@@ -1150,6 +1152,24 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase):
|
|
|
1150
1152
|
name="Rack Group 4",
|
|
1151
1153
|
location=cls.loc1,
|
|
1152
1154
|
)
|
|
1155
|
+
RackGroup.objects.create(
|
|
1156
|
+
name="Rack Group 5",
|
|
1157
|
+
location=cls.loc2,
|
|
1158
|
+
description="C",
|
|
1159
|
+
)
|
|
1160
|
+
RackGroup.objects.create(
|
|
1161
|
+
name="Rack Group 6",
|
|
1162
|
+
location=cls.loc2,
|
|
1163
|
+
)
|
|
1164
|
+
RackGroup.objects.create(
|
|
1165
|
+
name="Rack Group 7",
|
|
1166
|
+
location=cls.loc3,
|
|
1167
|
+
description="C",
|
|
1168
|
+
)
|
|
1169
|
+
RackGroup.objects.create(
|
|
1170
|
+
name="Rack Group 8",
|
|
1171
|
+
location=cls.loc3,
|
|
1172
|
+
)
|
|
1153
1173
|
|
|
1154
1174
|
def test_children(self):
|
|
1155
1175
|
child_groups = RackGroup.objects.filter(name__startswith="Child").filter(parent__isnull=False)[:2]
|
|
@@ -1161,6 +1181,24 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase):
|
|
|
1161
1181
|
params = {"children": [rack_group_4.pk, rack_group_4.pk]}
|
|
1162
1182
|
self.assertFalse(self.filterset(params, self.queryset).qs.exists())
|
|
1163
1183
|
|
|
1184
|
+
def test_ancestors(self):
|
|
1185
|
+
with self.subTest():
|
|
1186
|
+
pk_list = []
|
|
1187
|
+
parent_locations = self.loc3.ancestors(include_self=True)
|
|
1188
|
+
pk_list.extend([v.pk for v in parent_locations])
|
|
1189
|
+
params = Q(location__pk__in=pk_list)
|
|
1190
|
+
expected_queryset = RackGroup.objects.filter(params)
|
|
1191
|
+
params = {"ancestors": [self.loc3.pk]}
|
|
1192
|
+
self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
|
|
1193
|
+
with self.subTest():
|
|
1194
|
+
pk_list = []
|
|
1195
|
+
parent_locations = self.loc2.ancestors(include_self=True)
|
|
1196
|
+
pk_list.extend([v.pk for v in parent_locations])
|
|
1197
|
+
params = Q(location__pk__in=pk_list)
|
|
1198
|
+
expected_queryset = RackGroup.objects.filter(params)
|
|
1199
|
+
params = {"ancestors": [self.loc2.pk]}
|
|
1200
|
+
self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
|
|
1201
|
+
|
|
1164
1202
|
|
|
1165
1203
|
class RackTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
|
|
1166
1204
|
queryset = Rack.objects.all()
|
|
@@ -4188,12 +4226,13 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4188
4226
|
|
|
4189
4227
|
@classmethod
|
|
4190
4228
|
def setUpTestData(cls):
|
|
4191
|
-
|
|
4229
|
+
device_1 = Device.objects.first()
|
|
4230
|
+
device_2 = Device.objects.last()
|
|
4192
4231
|
vdc_status = Status.objects.get_for_model(VirtualDeviceContext)[0]
|
|
4193
4232
|
interface_status = Status.objects.get_for_model(Interface)[0]
|
|
4194
4233
|
interfaces = [
|
|
4195
4234
|
Interface.objects.create(
|
|
4196
|
-
device=
|
|
4235
|
+
device=device_1,
|
|
4197
4236
|
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
4198
4237
|
name=f"Interface 00{idx}",
|
|
4199
4238
|
status=interface_status,
|
|
@@ -4202,7 +4241,7 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4202
4241
|
]
|
|
4203
4242
|
vdcs = [
|
|
4204
4243
|
VirtualDeviceContext.objects.create(
|
|
4205
|
-
device=
|
|
4244
|
+
device=device_1,
|
|
4206
4245
|
status=vdc_status,
|
|
4207
4246
|
identifier=200 + idx,
|
|
4208
4247
|
name=f"Test VDC {idx}",
|
|
@@ -4213,3 +4252,17 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4213
4252
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[1], interface=interfaces[0])
|
|
4214
4253
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[1], interface=interfaces[1])
|
|
4215
4254
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[2], interface=interfaces[2])
|
|
4255
|
+
InterfaceVDCAssignment.objects.create(
|
|
4256
|
+
virtual_device_context=VirtualDeviceContext.objects.create(
|
|
4257
|
+
device=device_2,
|
|
4258
|
+
status=vdc_status,
|
|
4259
|
+
identifier=200,
|
|
4260
|
+
name="Test VDC 0",
|
|
4261
|
+
),
|
|
4262
|
+
interface=Interface.objects.create(
|
|
4263
|
+
device=device_2,
|
|
4264
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
4265
|
+
name="Interface 000",
|
|
4266
|
+
status=interface_status,
|
|
4267
|
+
),
|
|
4268
|
+
)
|
nautobot/dcim/tests/test_jobs.py
CHANGED
|
@@ -71,7 +71,7 @@ def create_common_data_for_software_related_test_cases():
|
|
|
71
71
|
class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
72
72
|
def test_correct_handling_for_model_protected_error(self):
|
|
73
73
|
create_common_data_for_software_related_test_cases()
|
|
74
|
-
software_image_file = SoftwareImageFile.objects.
|
|
74
|
+
software_image_file = SoftwareImageFile.objects.get(image_file_name="software_image_file_qs_test_1.bin")
|
|
75
75
|
|
|
76
76
|
self.add_permissions("dcim.delete_softwareimagefile")
|
|
77
77
|
pk_list = [str(software_image_file.pk)]
|
|
@@ -86,9 +86,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
|
86
86
|
pk_list=pk_list,
|
|
87
87
|
username=self.user.username,
|
|
88
88
|
)
|
|
89
|
-
|
|
90
|
-
print([(log.message, log.log_level) for log in logs])
|
|
91
|
-
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
|
|
89
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
92
90
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
93
91
|
self.assertIn("Caught ProtectedError while attempting to delete objects", error_log.message)
|
|
94
92
|
self.assertEqual(initial_count, SoftwareImageFile.objects.all().count())
|
|
@@ -97,7 +95,7 @@ class TestSoftwareImageFileTestCase(TransactionTestCase):
|
|
|
97
95
|
class TestSoftwareVersionTestCase(TransactionTestCase):
|
|
98
96
|
def test_correct_handling_for_model_protected_error(self):
|
|
99
97
|
create_common_data_for_software_related_test_cases()
|
|
100
|
-
software_version = SoftwareVersion.objects.
|
|
98
|
+
software_version = SoftwareVersion.objects.get(version="Test version 1.0.0")
|
|
101
99
|
|
|
102
100
|
initial_count = SoftwareVersion.objects.all().count()
|
|
103
101
|
self.add_permissions("dcim.delete_softwareversion")
|
|
@@ -112,7 +110,7 @@ class TestSoftwareVersionTestCase(TransactionTestCase):
|
|
|
112
110
|
pk_list=pk_list,
|
|
113
111
|
username=self.user.username,
|
|
114
112
|
)
|
|
115
|
-
self.
|
|
113
|
+
self.assertJobResultStatus(job_result, JobResultStatusChoices.STATUS_FAILURE)
|
|
116
114
|
error_log = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
117
115
|
self.assertIn("Caught ProtectedError while attempting to delete objects", error_log.message)
|
|
118
116
|
self.assertEqual(initial_count, SoftwareVersion.objects.all().count())
|
|
@@ -1166,6 +1166,19 @@ class LocationTypeTestCase(TestCase):
|
|
|
1166
1166
|
child_loc.delete()
|
|
1167
1167
|
child.validated_save()
|
|
1168
1168
|
|
|
1169
|
+
def test_removing_content_type(self):
|
|
1170
|
+
"""Validation check to prevent removing an in-use content type from a LocationType."""
|
|
1171
|
+
|
|
1172
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
1173
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
1174
|
+
|
|
1175
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1176
|
+
location_type.content_types.remove(device_ct)
|
|
1177
|
+
self.assertIn(
|
|
1178
|
+
f"Cannot remove the content type {device_ct} as currently at least one device is associated to a location",
|
|
1179
|
+
str(cm.exception),
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1169
1182
|
|
|
1170
1183
|
class LocationTestCase(ModelTestCases.BaseModelTestCase):
|
|
1171
1184
|
model = Location
|
|
@@ -1895,6 +1908,33 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
1895
1908
|
parent_device.rack = rack
|
|
1896
1909
|
parent_device.save()
|
|
1897
1910
|
|
|
1911
|
+
# Test assigning a rack in the child location of the parent device location
|
|
1912
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
1913
|
+
child_location = Location.objects.create(
|
|
1914
|
+
name="Child Location 1",
|
|
1915
|
+
location_type=self.location_type_3,
|
|
1916
|
+
status=location_status,
|
|
1917
|
+
parent=parent_device.location,
|
|
1918
|
+
)
|
|
1919
|
+
child_rack = Rack.objects.create(name="Rack 2", location=child_location, status=self.device_status)
|
|
1920
|
+
parent_device.rack = child_rack
|
|
1921
|
+
parent_device.validated_save()
|
|
1922
|
+
|
|
1923
|
+
# Test assigning a rack outside the child locations of the parent device location
|
|
1924
|
+
new_location = Location.objects.create(
|
|
1925
|
+
name="New Location 1",
|
|
1926
|
+
status=location_status,
|
|
1927
|
+
location_type=self.location_type_3,
|
|
1928
|
+
)
|
|
1929
|
+
invalid_rack = Rack.objects.create(name="Rack 3", location=new_location, status=self.device_status)
|
|
1930
|
+
parent_device.rack = invalid_rack
|
|
1931
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1932
|
+
parent_device.validated_save()
|
|
1933
|
+
self.assertIn(
|
|
1934
|
+
f'Rack "{invalid_rack}" does not belong to location "{parent_device.location}" and its descendants.',
|
|
1935
|
+
str(cm.exception),
|
|
1936
|
+
)
|
|
1937
|
+
|
|
1898
1938
|
child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1899
1939
|
|
|
1900
1940
|
self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
|
nautobot/dcim/views.py
CHANGED
|
@@ -61,7 +61,7 @@ from nautobot.dcim.utils import get_all_network_driver_mappings, get_network_dri
|
|
|
61
61
|
from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Team
|
|
62
62
|
from nautobot.extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectDynamicGroupsView
|
|
63
63
|
from nautobot.ipam.models import IPAddress, Prefix, Service, VLAN
|
|
64
|
-
from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable
|
|
64
|
+
from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable, VRFTable
|
|
65
65
|
from nautobot.virtualization.models import VirtualMachine
|
|
66
66
|
from nautobot.wireless.forms import ControllerManagedDeviceGroupWirelessNetworkFormSet
|
|
67
67
|
from nautobot.wireless.models import (
|
|
@@ -1922,7 +1922,7 @@ class DeviceView(generic.ObjectView):
|
|
|
1922
1922
|
|
|
1923
1923
|
# VRF assignments
|
|
1924
1924
|
vrf_assignments = instance.vrf_assignments.restrict(request.user, "view")
|
|
1925
|
-
vrf_table = VRFDeviceAssignmentTable(vrf_assignments
|
|
1925
|
+
vrf_table = VRFDeviceAssignmentTable(vrf_assignments)
|
|
1926
1926
|
|
|
1927
1927
|
# Software images
|
|
1928
1928
|
if instance.software_version is not None:
|
|
@@ -4513,15 +4513,25 @@ class VirtualDeviceContextUIViewSet(NautobotUIViewSet):
|
|
|
4513
4513
|
queryset = VirtualDeviceContext.objects.all()
|
|
4514
4514
|
serializer_class = serializers.VirtualDeviceContextSerializer
|
|
4515
4515
|
table_class = tables.VirtualDeviceContextTable
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4516
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
4517
|
+
panels=(
|
|
4518
|
+
object_detail.ObjectFieldsPanel(
|
|
4519
|
+
section=SectionChoices.LEFT_HALF,
|
|
4520
|
+
weight=100,
|
|
4521
|
+
fields="__all__",
|
|
4522
|
+
),
|
|
4523
|
+
object_detail.ObjectsTablePanel(
|
|
4524
|
+
weight=200,
|
|
4525
|
+
table_class=tables.InterfaceTable,
|
|
4526
|
+
table_attribute="interfaces",
|
|
4527
|
+
section=SectionChoices.FULL_WIDTH,
|
|
4528
|
+
exclude_columns=["device"],
|
|
4529
|
+
),
|
|
4530
|
+
object_detail.ObjectsTablePanel(
|
|
4531
|
+
weight=300,
|
|
4532
|
+
table_class=VRFTable,
|
|
4533
|
+
table_attribute="vrfs",
|
|
4534
|
+
section=SectionChoices.FULL_WIDTH,
|
|
4535
|
+
),
|
|
4536
|
+
),
|
|
4537
|
+
)
|
nautobot/extras/api/mixins.py
CHANGED
nautobot/extras/api/views.py
CHANGED
|
@@ -642,7 +642,7 @@ class JobViewSetBase(
|
|
|
642
642
|
):
|
|
643
643
|
raise ValidationError(
|
|
644
644
|
{
|
|
645
|
-
"_task_queue": "_task_queue and _job_queue are both specified. Please
|
|
645
|
+
"_task_queue": "_task_queue and _job_queue are both specified. Please specify only one or another."
|
|
646
646
|
}
|
|
647
647
|
)
|
|
648
648
|
|
|
@@ -685,7 +685,7 @@ class JobViewSetBase(
|
|
|
685
685
|
"job_queue", None
|
|
686
686
|
):
|
|
687
687
|
raise ValidationError(
|
|
688
|
-
{"task_queue": "task_queue and job_queue are both specified. Please
|
|
688
|
+
{"task_queue": "task_queue and job_queue are both specified. Please specify only one or another."}
|
|
689
689
|
)
|
|
690
690
|
schedule_data = input_serializer.validated_data.get("schedule", None)
|
|
691
691
|
|
nautobot/extras/choices.py
CHANGED
|
@@ -251,8 +251,10 @@ class JobResultStatusChoices(ChoiceSet):
|
|
|
251
251
|
"""
|
|
252
252
|
|
|
253
253
|
STATUS_FAILURE = states.FAILURE
|
|
254
|
+
STATUS_IGNORED = states.IGNORED
|
|
254
255
|
STATUS_PENDING = states.PENDING
|
|
255
256
|
STATUS_RECEIVED = states.RECEIVED
|
|
257
|
+
STATUS_REJECTED = states.REJECTED
|
|
256
258
|
STATUS_RETRY = states.RETRY
|
|
257
259
|
STATUS_REVOKED = states.REVOKED
|
|
258
260
|
STATUS_STARTED = states.STARTED
|
|
@@ -302,27 +304,30 @@ class JobResultStatusChoices(ChoiceSet):
|
|
|
302
304
|
class LogLevelChoices(ChoiceSet):
|
|
303
305
|
LOG_DEBUG = "debug"
|
|
304
306
|
LOG_INFO = "info"
|
|
307
|
+
LOG_SUCCESS = "success"
|
|
305
308
|
LOG_WARNING = "warning"
|
|
309
|
+
LOG_FAILURE = "failure"
|
|
306
310
|
LOG_ERROR = "error"
|
|
307
311
|
LOG_CRITICAL = "critical"
|
|
308
|
-
LOG_SUCCESS = "success"
|
|
309
312
|
|
|
310
313
|
CHOICES = (
|
|
311
314
|
(LOG_DEBUG, "Debug"),
|
|
312
315
|
(LOG_INFO, "Info"),
|
|
316
|
+
(LOG_SUCCESS, "Success"),
|
|
313
317
|
(LOG_WARNING, "Warning"),
|
|
318
|
+
(LOG_FAILURE, "Failure"),
|
|
314
319
|
(LOG_ERROR, "Error"),
|
|
315
320
|
(LOG_CRITICAL, "Critical"),
|
|
316
|
-
(LOG_SUCCESS, "Success"),
|
|
317
321
|
)
|
|
318
322
|
|
|
319
323
|
CSS_CLASSES = {
|
|
320
324
|
LOG_DEBUG: "debug",
|
|
321
325
|
LOG_INFO: "info",
|
|
326
|
+
LOG_SUCCESS: "success",
|
|
322
327
|
LOG_WARNING: "warning",
|
|
328
|
+
LOG_FAILURE: "failure",
|
|
323
329
|
LOG_ERROR: "error",
|
|
324
330
|
LOG_CRITICAL: "critical",
|
|
325
|
-
LOG_SUCCESS: "success",
|
|
326
331
|
}
|
|
327
332
|
|
|
328
333
|
|
|
@@ -854,6 +854,10 @@ class JobFilterSet(BaseFilterSet, CustomFieldModelFilterSetMixin):
|
|
|
854
854
|
"description": "icontains",
|
|
855
855
|
},
|
|
856
856
|
)
|
|
857
|
+
job_queues = NaturalKeyOrPKMultipleChoiceFilter(
|
|
858
|
+
queryset=JobQueue.objects.all(),
|
|
859
|
+
label="Job Queue (name or ID)",
|
|
860
|
+
)
|
|
857
861
|
|
|
858
862
|
class Meta:
|
|
859
863
|
model = Job
|