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.

Files changed (198) hide show
  1. nautobot/__init__.py +19 -3
  2. nautobot/apps/filters.py +2 -0
  3. nautobot/circuits/filters.py +1 -1
  4. nautobot/circuits/tests/test_models.py +5 -3
  5. nautobot/cloud/filters.py +3 -6
  6. nautobot/cloud/tests/test_filters.py +21 -0
  7. nautobot/core/admin.py +2 -0
  8. nautobot/core/celery/__init__.py +5 -3
  9. nautobot/core/jobs/__init__.py +5 -3
  10. nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
  11. nautobot/core/models/utils.py +6 -1
  12. nautobot/core/templates/inc/javascript.html +1 -0
  13. nautobot/core/templatetags/ui_framework.py +20 -4
  14. nautobot/core/testing/__init__.py +2 -0
  15. nautobot/core/testing/forms.py +1 -1
  16. nautobot/core/testing/mixins.py +9 -0
  17. nautobot/core/tests/test_api.py +1 -1
  18. nautobot/core/tests/test_graphql.py +3 -3
  19. nautobot/core/tests/test_jobs.py +30 -28
  20. nautobot/core/ui/object_detail.py +1 -1
  21. nautobot/dcim/api/serializers.py +36 -0
  22. nautobot/dcim/api/views.py +1 -1
  23. nautobot/dcim/elevations.py +17 -4
  24. nautobot/dcim/factory.py +9 -1
  25. nautobot/dcim/filters/__init__.py +27 -1
  26. nautobot/dcim/forms.py +13 -1
  27. nautobot/dcim/models/devices.py +11 -5
  28. nautobot/dcim/signals.py +26 -0
  29. nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
  30. nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
  31. nautobot/dcim/tests/test_api.py +176 -0
  32. nautobot/dcim/tests/test_filters.py +56 -3
  33. nautobot/dcim/tests/test_jobs.py +4 -6
  34. nautobot/dcim/tests/test_models.py +40 -0
  35. nautobot/dcim/views.py +24 -14
  36. nautobot/extras/api/mixins.py +1 -1
  37. nautobot/extras/api/views.py +2 -2
  38. nautobot/extras/choices.py +8 -3
  39. nautobot/extras/filters/__init__.py +4 -0
  40. nautobot/extras/jobs.py +181 -103
  41. nautobot/extras/management/utils.py +13 -2
  42. nautobot/extras/models/datasources.py +11 -4
  43. nautobot/extras/models/jobs.py +20 -17
  44. nautobot/extras/plugins/__init__.py +26 -1
  45. nautobot/extras/tables.py +25 -29
  46. nautobot/extras/templates/extras/inc/jobresult.html +12 -13
  47. nautobot/extras/templates/extras/objectchange.html +28 -12
  48. nautobot/extras/test_jobs/atomic_transaction.py +6 -6
  49. nautobot/extras/test_jobs/fail.py +75 -1
  50. nautobot/extras/tests/test_api.py +17 -16
  51. nautobot/extras/tests/test_datasources.py +64 -54
  52. nautobot/extras/tests/test_filters.py +2 -0
  53. nautobot/extras/tests/test_jobs.py +69 -62
  54. nautobot/extras/tests/test_models.py +1 -1
  55. nautobot/extras/tests/test_plugins.py +32 -1
  56. nautobot/extras/tests/test_relationships.py +5 -5
  57. nautobot/extras/tests/test_views.py +12 -2
  58. nautobot/extras/views.py +10 -1
  59. nautobot/ipam/api/serializers.py +7 -8
  60. nautobot/ipam/api/views.py +2 -2
  61. nautobot/ipam/factory.py +27 -8
  62. nautobot/ipam/filters.py +67 -29
  63. nautobot/ipam/formfields.py +51 -0
  64. nautobot/ipam/forms.py +28 -1
  65. nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
  66. nautobot/ipam/models.py +63 -5
  67. nautobot/ipam/querysets.py +6 -0
  68. nautobot/ipam/tables.py +21 -7
  69. nautobot/ipam/templates/ipam/rir.html +1 -43
  70. nautobot/ipam/tests/test_api.py +107 -66
  71. nautobot/ipam/tests/test_filters.py +145 -5
  72. nautobot/ipam/tests/test_models.py +16 -0
  73. nautobot/ipam/tests/test_views.py +15 -2
  74. nautobot/ipam/urls.py +1 -21
  75. nautobot/ipam/views.py +24 -41
  76. nautobot/project-static/css/base.css +11 -0
  77. nautobot/project-static/css/dark.css +2 -1
  78. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
  79. nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
  80. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
  81. nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
  82. nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
  83. nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
  84. nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
  85. nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
  86. nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
  87. nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
  88. nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
  89. nautobot/project-static/docs/development/apps/api/testing.html +0 -6
  90. nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
  91. nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
  92. nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
  93. nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
  94. nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
  95. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
  96. nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
  97. nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
  98. nautobot/project-static/docs/development/apps/index.html +2 -35
  99. nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
  100. nautobot/project-static/docs/development/core/application-registry.html +0 -6
  101. nautobot/project-static/docs/development/core/best-practices.html +0 -27
  102. nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
  103. nautobot/project-static/docs/development/core/getting-started.html +12 -16
  104. nautobot/project-static/docs/development/core/homepage.html +0 -3
  105. nautobot/project-static/docs/development/core/style-guide.html +0 -5
  106. nautobot/project-static/docs/development/core/templates.html +0 -3
  107. nautobot/project-static/docs/development/core/testing.html +0 -9
  108. nautobot/project-static/docs/development/jobs/index.html +30 -43
  109. nautobot/project-static/docs/objects.inv +0 -0
  110. nautobot/project-static/docs/overview/application_stack.html +0 -18
  111. nautobot/project-static/docs/release-notes/version-2.4.html +374 -0
  112. nautobot/project-static/docs/requirements.txt +2 -2
  113. nautobot/project-static/docs/search/search_index.json +1 -1
  114. nautobot/project-static/docs/sitemap.xml +290 -290
  115. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  116. nautobot/project-static/docs/user-guide/administration/configuration/settings.html +0 -10
  117. nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
  118. nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
  119. nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
  120. nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
  121. nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
  122. nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
  123. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
  124. nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
  125. nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
  126. nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
  127. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
  128. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
  129. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
  130. nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
  131. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
  132. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
  133. nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
  134. nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
  135. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
  136. nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
  137. nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
  138. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
  139. nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
  140. nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
  141. nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
  142. nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
  143. nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
  144. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
  145. nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
  146. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
  147. nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
  148. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
  149. nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
  150. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
  151. nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
  152. nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
  153. nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
  154. nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
  155. nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
  156. nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
  157. nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
  158. nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
  159. nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
  160. nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
  161. nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
  162. nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
  163. nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
  164. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
  165. nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
  166. nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
  167. nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
  168. nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
  169. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
  170. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
  171. nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
  172. nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
  173. nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
  174. nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
  175. nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
  176. nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
  177. nautobot/project-static/js/editor.js +292 -0
  178. nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
  179. nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  180. nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
  181. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
  182. nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
  183. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
  184. nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
  185. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
  186. nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
  187. nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
  188. nautobot/tenancy/filters/__init__.py +3 -5
  189. nautobot/tenancy/tests/test_filters.py +10 -0
  190. nautobot/virtualization/views.py +0 -1
  191. nautobot/wireless/tables.py +9 -4
  192. nautobot/wireless/tests/test_api.py +0 -9
  193. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/METADATA +4 -4
  194. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/RECORD +198 -186
  195. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
  196. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
  197. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
  198. {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
@@ -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
- device = Device.objects.first()
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=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=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
+ )
@@ -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.first()
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
- logs = JobLogEntry.objects.filter(job_result=job_result)
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.first()
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.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
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, exclude=("virtual_machine", "device"))
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
- def get_extra_context(self, request, instance):
4518
- if self.action == "retrieve":
4519
- interfaces_table = tables.InterfaceTable(
4520
- instance.interfaces.restrict(request.user, "view"), orderable=False, exclude=("device",)
4521
- )
4522
-
4523
- return {
4524
- "interfaces_table": interfaces_table,
4525
- **super().get_extra_context(request, instance),
4526
- }
4527
- return super().get_extra_context(request, instance)
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
+ )
@@ -37,7 +37,7 @@ class TaggedModelSerializerMixin(BaseModelSerializer):
37
37
 
38
38
  def _save_tags(self, instance, tags):
39
39
  if tags:
40
- instance.tags.set([t.name for t in tags])
40
+ instance.tags.set(tags)
41
41
  else:
42
42
  instance.tags.clear()
43
43
 
@@ -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 specifiy only one or another."
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 specifiy only one or another."}
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
 
@@ -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