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
@@ -13,7 +13,15 @@ from rest_framework import status
13
13
  from nautobot.core.testing import APITestCase, APIViewTestCases, disable_warnings
14
14
  from nautobot.core.testing.api import APITransactionTestCase
15
15
  from nautobot.dcim.choices import InterfaceTypeChoices
16
- from nautobot.dcim.models import Device, DeviceType, Interface, Location, LocationType, Manufacturer
16
+ from nautobot.dcim.models import (
17
+ Device,
18
+ DeviceType,
19
+ Interface,
20
+ Location,
21
+ LocationType,
22
+ Manufacturer,
23
+ VirtualDeviceContext,
24
+ )
17
25
  from nautobot.extras.models import CustomField, Role, Status
18
26
  from nautobot.ipam import choices
19
27
  from nautobot.ipam.models import (
@@ -118,6 +126,7 @@ class VRFDeviceAssignmentTest(APIViewTestCases.APIViewTestCase):
118
126
  def setUpTestData(cls):
119
127
  cls.vrfs = VRF.objects.all()
120
128
  cls.devices = Device.objects.all()
129
+ cls.vdcs = VirtualDeviceContext.objects.all()
121
130
  locations = Location.objects.filter(location_type__name="Campus")
122
131
  cluster_type = ClusterType.objects.create(name="Test Cluster Type")
123
132
  clusters = (
@@ -154,25 +163,42 @@ class VRFDeviceAssignmentTest(APIViewTestCases.APIViewTestCase):
154
163
  virtual_machine=cls.test_vm,
155
164
  rd="65000:4",
156
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
+
177
+ cls.update_data = {
178
+ "name": "VRFDeviceAssignment 2",
179
+ "rd": "65000:7",
180
+ }
157
181
 
158
182
  cls.create_data = [
159
183
  {
160
184
  "vrf": cls.vrfs[2].pk,
161
185
  "device": cls.devices[4].pk,
162
- "virtual_machine": None,
163
- "rd": "65000:4",
186
+ "rd": "65000:7",
164
187
  },
165
188
  {
166
189
  "vrf": cls.vrfs[3].pk,
167
- "device": None,
168
190
  "virtual_machine": cls.test_vm.pk,
169
- "rd": "65000:5",
191
+ "rd": "65000:8",
170
192
  },
171
193
  {
172
194
  "vrf": cls.vrfs[4].pk,
173
195
  "device": cls.devices[6].pk,
174
- "virtual_machine": None,
175
- "rd": "65000:6",
196
+ "name": "VRFDeviceAssignment 3",
197
+ "rd": "65000:9",
198
+ },
199
+ {
200
+ "vrf": cls.vrfs[4].pk,
201
+ "virtual_device_context": cls.vdcs[0].pk,
176
202
  },
177
203
  ]
178
204
  cls.bulk_update_data = {
@@ -181,39 +207,66 @@ class VRFDeviceAssignmentTest(APIViewTestCases.APIViewTestCase):
181
207
 
182
208
  def test_creating_invalid_vrf_device_assignments(self):
183
209
  # Add object-level permission
184
- duplicate_device_create_data = {
185
- "vrf": self.vrfs[0].pk,
186
- "device": self.devices[1].pk,
187
- "virtual_machine": None,
188
- "rd": "65000:6",
189
- }
190
- duplicate_vm_create_data = {
191
- "vrf": self.vrfs[1].pk,
192
- "device": None,
193
- "virtual_machine": self.test_vm.pk,
194
- "rd": "65000:6",
195
- }
196
- invalid_create_data = {
197
- "vrf": self.vrfs[2].pk,
198
- "device": self.devices[6].pk,
199
- "virtual_machine": self.test_vm.pk,
200
- "rd": "65000:6",
201
- }
202
210
  self.add_permissions("ipam.add_vrfdeviceassignment")
203
- response = self.client.post(self._get_list_url(), duplicate_device_create_data, format="json", **self.header)
204
- self.assertContains(
205
- response, "The fields device, vrf must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
206
- )
207
- response = self.client.post(self._get_list_url(), duplicate_vm_create_data, format="json", **self.header)
208
- self.assertContains(
209
- response, "The fields virtual_machine, vrf must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
210
- )
211
- response = self.client.post(self._get_list_url(), invalid_create_data, format="json", **self.header)
212
- self.assertContains(
213
- response,
214
- "A VRF cannot be associated with both a device and a virtual machine.",
215
- status_code=status.HTTP_400_BAD_REQUEST,
216
- )
211
+ duplicate_create_data = [
212
+ {
213
+ "vrf": self.vrfs[0].pk,
214
+ "device": self.devices[1].pk,
215
+ "rd": "65000:6",
216
+ },
217
+ {
218
+ "vrf": self.vrfs[1].pk,
219
+ "virtual_machine": self.test_vm.pk,
220
+ "rd": "65000:6",
221
+ },
222
+ {
223
+ "vrf": self.vrfs[0].pk,
224
+ "virtual_device_context": self.vdcs[1].pk,
225
+ "rd": "65000:6",
226
+ },
227
+ ]
228
+ expected_responses = [
229
+ "The fields device, vrf must make a unique set.",
230
+ "The fields virtual_machine, vrf must make a unique set.",
231
+ "The fields virtual_device_context, vrf must make a unique set.",
232
+ ]
233
+ for i, data in enumerate(duplicate_create_data):
234
+ response = self.client.post(self._get_list_url(), data, format="json", **self.header)
235
+ self.assertContains(response, expected_responses[i], status_code=status.HTTP_400_BAD_REQUEST)
236
+
237
+ # Test VRFDeviceAssignment model clean() code paths
238
+ vrf = VRF.objects.create(name="New VRF ", namespace=Namespace.objects.first())
239
+ invalid_create_data = [
240
+ {
241
+ "vrf": vrf.pk,
242
+ "device": self.devices[6].pk,
243
+ "virtual_machine": self.test_vm.pk,
244
+ },
245
+ {
246
+ "vrf": vrf.pk,
247
+ "device": self.devices[7].pk,
248
+ "virtual_device_context": self.vdcs[2].pk,
249
+ },
250
+ {
251
+ "vrf": vrf.pk,
252
+ "virtual_machine": self.test_vm.pk,
253
+ "virtual_device_context": self.vdcs[3].pk,
254
+ },
255
+ {
256
+ "vrf": vrf.pk,
257
+ "name": "VRFDeviceAssignment 5",
258
+ "rd": "65000:6",
259
+ },
260
+ ]
261
+ expected_responses = [
262
+ "A VRFDeviceAssignment entry cannot be associated with both a device and a virtual machine.",
263
+ "A VRFDeviceAssignment entry cannot be associated with both a device and a virtual device context.",
264
+ "A VRFDeviceAssignment entry cannot be associated with both a virtual machine and a virtual device context.",
265
+ "A VRFDeviceAssignment entry must be associated with a device, a virtual machine, or a virtual device context.",
266
+ ]
267
+ for i, data in enumerate(invalid_create_data):
268
+ response = self.client.post(self._get_list_url(), data, format="json", **self.header)
269
+ self.assertContains(response, expected_responses[i], status_code=status.HTTP_400_BAD_REQUEST)
217
270
 
218
271
 
219
272
  class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
@@ -221,50 +274,38 @@ class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
221
274
 
222
275
  @classmethod
223
276
  def setUpTestData(cls):
277
+ cls.namespace = (
278
+ Namespace.objects.annotate(prefixes_count=Count("prefixes")).filter(prefixes_count__gte=3).first()
279
+ )
224
280
  cls.vrfs = (
225
- VRF.objects.annotate(prefixes_count=Count("namespace__prefixes")).filter(prefixes_count__gte=2).distinct()
281
+ VRF.objects.create(name="TEST VRF 1", namespace=cls.namespace),
282
+ VRF.objects.create(name="TEST VRF 2", namespace=cls.namespace),
226
283
  )
227
- cls.prefixes = Prefix.objects.all()
284
+ cls.prefixes = Prefix.objects.filter(namespace=cls.namespace)
228
285
 
229
- VRFPrefixAssignment.objects.create(
230
- vrf=cls.vrfs[0],
231
- prefix=cls.prefixes.filter(namespace=cls.vrfs[0].namespace)[0],
232
- )
233
- VRFPrefixAssignment.objects.create(
234
- vrf=cls.vrfs[0],
235
- prefix=cls.prefixes.filter(namespace=cls.vrfs[0].namespace)[1],
236
- )
237
- VRFPrefixAssignment.objects.create(
238
- vrf=cls.vrfs[1],
239
- prefix=cls.prefixes.filter(namespace=cls.vrfs[1].namespace)[0],
240
- )
241
- VRFPrefixAssignment.objects.create(
242
- vrf=cls.vrfs[1],
243
- prefix=cls.prefixes.filter(namespace=cls.vrfs[1].namespace)[1],
244
- )
245
286
  cls.create_data = [
246
287
  {
247
- "vrf": cls.vrfs[2].pk,
248
- "prefix": cls.prefixes.filter(namespace=cls.vrfs[2].namespace).exclude(vrfs=cls.vrfs[2])[0].pk,
288
+ "vrf": cls.vrfs[0].pk,
289
+ "prefix": cls.prefixes.first().pk,
249
290
  },
250
291
  {
251
- "vrf": cls.vrfs[3].pk,
252
- "prefix": cls.prefixes.filter(namespace=cls.vrfs[3].namespace).exclude(vrfs=cls.vrfs[3])[0].pk,
292
+ "vrf": cls.vrfs[0].pk,
293
+ "prefix": cls.prefixes.last().pk,
253
294
  },
254
295
  {
255
- "vrf": cls.vrfs[4].pk,
256
- "prefix": cls.prefixes.filter(namespace=cls.vrfs[4].namespace).exclude(vrfs=cls.vrfs[4])[0].pk,
296
+ "vrf": cls.vrfs[1].pk,
297
+ "prefix": cls.prefixes.first().pk,
257
298
  },
258
299
  ]
259
300
 
260
301
  def test_creating_invalid_vrf_prefix_assignments(self):
261
302
  duplicate_create_data = {
262
- "vrf": self.vrfs[0].pk,
263
- "prefix": self.prefixes.filter(namespace=self.vrfs[0].namespace)[0].pk,
303
+ "vrf": VRFPrefixAssignment.objects.first().vrf.pk,
304
+ "prefix": VRFPrefixAssignment.objects.first().prefix.pk,
264
305
  }
265
306
  wrong_namespace_create_data = {
266
307
  "vrf": self.vrfs[0].pk,
267
- "prefix": self.prefixes.exclude(namespace=self.vrfs[0].namespace)[0].pk,
308
+ "prefix": Prefix.objects.exclude(namespace=self.namespace)[0].pk,
268
309
  }
269
310
  missing_field_create_data = {
270
311
  "vrf": self.vrfs[0].pk,
@@ -10,6 +10,7 @@ from nautobot.dcim.models import (
10
10
  Location,
11
11
  LocationType,
12
12
  Manufacturer,
13
+ VirtualDeviceContext,
13
14
  )
14
15
  from nautobot.extras.models import Role, Status, Tag
15
16
  from nautobot.ipam.choices import PrefixTypeChoices, ServiceProtocolChoices
@@ -24,7 +25,9 @@ from nautobot.ipam.filters import (
24
25
  VLANFilterSet,
25
26
  VLANGroupFilterSet,
26
27
  VLANLocationAssignmentFilterSet,
28
+ VRFDeviceAssignmentFilterSet,
27
29
  VRFFilterSet,
30
+ VRFPrefixAssignmentFilterSet,
28
31
  )
29
32
  from nautobot.ipam.models import (
30
33
  IPAddress,
@@ -39,6 +42,8 @@ from nautobot.ipam.models import (
39
42
  VLANGroup,
40
43
  VLANLocationAssignment,
41
44
  VRF,
45
+ VRFDeviceAssignment,
46
+ VRFPrefixAssignment,
42
47
  )
43
48
  from nautobot.tenancy.models import Tenant
44
49
  from nautobot.virtualization.models import (
@@ -66,12 +71,19 @@ class VRFTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterT
66
71
  # skip testing "rd" attribute for generic q filter test as it's not trivially modifiable
67
72
  exclude_q_filter_predicates = ["rd"]
68
73
  generic_filter_tests = (
74
+ # ("device", "devices__id"),
75
+ # ("device", "devices__name"),
69
76
  ("export_targets", "export_targets__id"),
70
77
  ("export_targets", "export_targets__name"),
71
78
  ("import_targets", "import_targets__id"),
72
79
  ("import_targets", "import_targets__name"),
80
+ ("prefix", "prefixes__id"),
73
81
  ("name",),
82
+ ("namespace", "namespace__id"),
83
+ ("namespace", "namespace__name"),
74
84
  ("rd",),
85
+ # ("virtual_machines", "virtual_machines__id"),
86
+ # ("virtual_machines", "virtual_machines__name"),
75
87
  )
76
88
 
77
89
  @classmethod
@@ -79,6 +91,36 @@ class VRFTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterT
79
91
  instance = cls.queryset.first()
80
92
  instance.tags.set(Tag.objects.all()[:2])
81
93
 
94
+ def test_prefix_filter_by_string(self):
95
+ """Test filtering by prefix strings as an alternative to pk."""
96
+ prefix = self.queryset.filter(prefixes__isnull=False).first().prefixes.first()
97
+ params = {"prefix": [prefix.prefix]}
98
+ self.assertQuerysetEqualAndNotEmpty(
99
+ self.filterset(params, self.queryset).qs,
100
+ self.queryset.filter(prefixes__network=prefix.network, prefixes__prefix_length=prefix.prefix_length),
101
+ ordered=False,
102
+ )
103
+
104
+
105
+ class VRFPrefixAssignmentTestCase(FilterTestCases.FilterTestCase):
106
+ queryset = VRFPrefixAssignment.objects.all()
107
+ filterset = VRFPrefixAssignmentFilterSet
108
+ generic_filter_tests = (
109
+ ("prefix", "prefix__id"),
110
+ ("vrf", "vrf__id"),
111
+ ("vrf", "vrf__name"),
112
+ )
113
+
114
+ def test_prefix_filter_by_string(self):
115
+ """Test filtering by prefix strings as an alternative to pk."""
116
+ prefix = self.queryset.first().prefix
117
+ params = {"prefix": [prefix.prefix]}
118
+ self.assertQuerysetEqualAndNotEmpty(
119
+ self.filterset(params, self.queryset).qs,
120
+ self.queryset.filter(prefix__network=prefix.network, prefix__prefix_length=prefix.prefix_length),
121
+ ordered=False,
122
+ )
123
+
82
124
 
83
125
  class RouteTargetTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
84
126
  queryset = RouteTarget.objects.all()
@@ -190,15 +232,32 @@ class PrefixLocationAssignmentTestCase(FilterTestCases.FilterTestCase):
190
232
  # )
191
233
 
192
234
  def test_prefix(self):
193
- ipv4_prefix = str(self.queryset.filter(prefix__ip_version=4).first().prefix)
194
- ipv6_prefix = str(self.queryset.filter(prefix__ip_version=6).first().prefix)
235
+ ipv4_prefix = self.queryset.filter(prefix__ip_version=4).first().prefix
236
+ ipv6_prefix = self.queryset.filter(prefix__ip_version=6).first().prefix
237
+
238
+ params = {"prefix": [ipv4_prefix.prefix, ipv6_prefix.pk]}
239
+
240
+ self.assertQuerysetEqualAndNotEmpty(
241
+ self.filterset(params, self.queryset).qs,
242
+ self.queryset.filter(prefix=ipv6_prefix)
243
+ | self.queryset.filter(
244
+ prefix__network=ipv4_prefix.network,
245
+ prefix__prefix_length=ipv4_prefix.prefix_length,
246
+ prefix__broadcast=ipv4_prefix.broadcast,
247
+ ),
248
+ ordered=False,
249
+ )
195
250
 
196
- params = {"prefix": [ipv4_prefix, ipv6_prefix]}
197
- prefix_queryset = Prefix.objects.net_equals(ipv4_prefix, ipv6_prefix)
251
+ params = {"prefix": [ipv4_prefix.pk, ipv6_prefix.prefix]}
198
252
 
199
253
  self.assertQuerysetEqualAndNotEmpty(
200
254
  self.filterset(params, self.queryset).qs,
201
- self.queryset.filter(prefix__in=prefix_queryset),
255
+ self.queryset.filter(prefix=ipv4_prefix)
256
+ | self.queryset.filter(
257
+ prefix__network=ipv6_prefix.network,
258
+ prefix__prefix_length=ipv6_prefix.prefix_length,
259
+ prefix__broadcast=ipv6_prefix.broadcast,
260
+ ),
202
261
  ordered=False,
203
262
  )
204
263
 
@@ -289,11 +348,21 @@ class PrefixFilterCustomDataTestCase(TestCase):
289
348
  self.assertQuerysetEqualAndNotEmpty(
290
349
  self.filterset(params, self.queryset).qs, self.queryset.filter(parent=parent4)
291
350
  )
351
+ params = {"parent": ["10.0.0.0/16"]}
352
+ self.assertQuerysetEqualAndNotEmpty(
353
+ self.filterset(params, self.queryset).qs,
354
+ self.queryset.filter(parent__network="10.0.0.0", parent__prefix_length=16),
355
+ )
292
356
  parent6 = Prefix.objects.get(prefix="2001:db8::/32", namespace=self.namespace)
293
357
  params = {"parent": [str(parent6.pk)]}
294
358
  self.assertQuerysetEqualAndNotEmpty(
295
359
  self.filterset(params, self.queryset).qs, self.queryset.filter(parent=parent6)
296
360
  )
361
+ params = {"parent": ["2001:db8::/32"]}
362
+ self.assertQuerysetEqualAndNotEmpty(
363
+ self.filterset(params, self.queryset).qs,
364
+ self.queryset.filter(parent__network="2001:db8::", parent__prefix_length=32),
365
+ )
297
366
 
298
367
  def test_ip_version(self):
299
368
  params = {"ip_version": "6"}
@@ -980,6 +1049,77 @@ class IPAddressToInterfaceTestCase(FilterTestCases.FilterTestCase):
980
1049
  )
981
1050
 
982
1051
 
1052
+ class VRFDeviceAssignmentTestCase(FilterTestCases.FilterTestCase):
1053
+ queryset = VRFDeviceAssignment.objects.all()
1054
+ filterset = VRFDeviceAssignmentFilterSet
1055
+ generic_filter_tests = (
1056
+ ["vrf", "vrf__id"],
1057
+ ["vrf", "vrf__name"],
1058
+ ["device", "device__id"],
1059
+ ["device", "device__name"],
1060
+ ["virtual_machine", "virtual_machine__id"],
1061
+ ["virtual_machine", "virtual_machine__name"],
1062
+ ["virtual_device_context", "virtual_device_context__id"],
1063
+ ["virtual_device_context", "virtual_device_context__name"],
1064
+ ["name"],
1065
+ ["rd"],
1066
+ )
1067
+
1068
+ @classmethod
1069
+ def setUpTestData(cls):
1070
+ # Creating VRFDeviceAssignment instances manually until VRFFactory is enhanced to generate VRFDeviceAssignments
1071
+ cls.vrfs = VRF.objects.all()
1072
+ cls.devices = Device.objects.all()
1073
+ cls.vdcs = VirtualDeviceContext.objects.all()
1074
+ locations = Location.objects.filter(location_type__name="Campus")
1075
+ cluster_type = ClusterType.objects.create(name="Test Cluster Type")
1076
+ cluster = Cluster.objects.create(name="Cluster 1", cluster_type=cluster_type, location=locations[0])
1077
+ vm_status = Status.objects.get_for_model(VirtualMachine).first()
1078
+ vm_role = Role.objects.get_for_model(VirtualMachine).first()
1079
+ cls.test_vm_1 = VirtualMachine.objects.create(
1080
+ cluster=cluster,
1081
+ name="VM 1",
1082
+ role=vm_role,
1083
+ status=vm_status,
1084
+ )
1085
+ cls.test_vm_2 = VirtualMachine.objects.create(
1086
+ cluster=cluster,
1087
+ name="VM 2",
1088
+ role=vm_role,
1089
+ status=vm_status,
1090
+ )
1091
+ VRFDeviceAssignment.objects.create(
1092
+ vrf=cls.vrfs[0],
1093
+ device=cls.devices[0],
1094
+ rd="65000:1",
1095
+ )
1096
+ VRFDeviceAssignment.objects.create(
1097
+ vrf=cls.vrfs[0],
1098
+ device=cls.devices[1],
1099
+ rd="65000:2",
1100
+ )
1101
+ VRFDeviceAssignment.objects.create(
1102
+ vrf=cls.vrfs[0],
1103
+ virtual_machine=cls.test_vm_1,
1104
+ rd="65000:3",
1105
+ )
1106
+ VRFDeviceAssignment.objects.create(
1107
+ vrf=cls.vrfs[1],
1108
+ virtual_machine=cls.test_vm_2,
1109
+ rd="65000:4",
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
+
1122
+
983
1123
  class VLANGroupTestCase(FilterTestCases.FilterTestCase):
984
1124
  queryset = VLANGroup.objects.all()
985
1125
  filterset = VLANGroupFilterSet
@@ -1301,6 +1301,22 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
1301
1301
  str(err.exception),
1302
1302
  )
1303
1303
 
1304
+ def test_get_or_create_address_kwarg(self):
1305
+ status = Status.objects.get(name="Active")
1306
+ namespace = Namespace.objects.create(name="Test IPAddress get_or_create with address kwarg")
1307
+ Prefix.objects.create(prefix="10.0.0.0/24", namespace=namespace, status=status)
1308
+ ip_address, created = IPAddress.objects.get_or_create(
1309
+ address="10.0.0.40/32", namespace=namespace, defaults={"status": status}
1310
+ )
1311
+ self.assertEqual(ip_address.host, "10.0.0.40")
1312
+ self.assertEqual(ip_address.mask_length, 32)
1313
+ self.assertTrue(created)
1314
+ _, created = IPAddress.objects.get_or_create(
1315
+ address="10.0.0.40/32", namespace=namespace, defaults={"status": status}
1316
+ )
1317
+ self.assertFalse(created)
1318
+ self.assertTrue(IPAddress.objects.filter(address="10.0.0.40/32", parent__namespace=namespace).exists())
1319
+
1304
1320
  def test_create_field_population(self):
1305
1321
  """Test that the various ways of creating an IPAddress result in correctly populated fields."""
1306
1322
  if self.namespace != get_default_namespace():
@@ -2,6 +2,7 @@ import datetime
2
2
  import random
3
3
 
4
4
  from django.contrib.contenttypes.models import ContentType
5
+ from django.db.models import Count
5
6
  from django.test import override_settings
6
7
  from django.urls import reverse
7
8
  from django.utils.html import strip_tags
@@ -13,7 +14,15 @@ from nautobot.core.templatetags.helpers import hyperlinked_object, queryset_to_p
13
14
  from nautobot.core.testing import ModelViewTestCase, post_data, ViewTestCases
14
15
  from nautobot.core.testing.utils import extract_page_body
15
16
  from nautobot.core.utils.lookup import get_route_for_model
16
- from nautobot.dcim.models import Device, DeviceType, Interface, Location, LocationType, Manufacturer
17
+ from nautobot.dcim.models import (
18
+ Device,
19
+ DeviceType,
20
+ Interface,
21
+ Location,
22
+ LocationType,
23
+ Manufacturer,
24
+ VirtualDeviceContext,
25
+ )
17
26
  from nautobot.extras.choices import CustomFieldTypeChoices, RelationshipTypeChoices
18
27
  from nautobot.extras.models import (
19
28
  CustomField,
@@ -72,8 +81,9 @@ class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
72
81
  @classmethod
73
82
  def setUpTestData(cls):
74
83
  tenants = Tenant.objects.all()[:2]
75
- namespace = Prefix.objects.first().namespace
84
+ namespace = Namespace.objects.annotate(prefix_count=Count("prefixes")).filter(prefix_count__gt=2).first()
76
85
  prefixes = Prefix.objects.filter(namespace=namespace)
86
+ vdcs = VirtualDeviceContext.objects.all()
77
87
  vrf_statuses = Status.objects.get_for_model(VRF)
78
88
 
79
89
  cls.form_data = {
@@ -85,6 +95,7 @@ class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
85
95
  "prefixes": [prefixes[1].id],
86
96
  "tags": [t.pk for t in Tag.objects.get_for_model(VRF)],
87
97
  "status": vrf_statuses.first().pk,
98
+ "virtual_device_contexts": [vdcs[0].id, vdcs[1].id],
88
99
  }
89
100
 
90
101
  cls.bulk_edit_data = {
@@ -94,6 +105,8 @@ class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
94
105
  "namespace": prefixes[0].namespace.id,
95
106
  "add_prefixes": [prefixes[0].id],
96
107
  "remove_prefixes": [prefixes[1].id],
108
+ "add_virtual_device_contexts": [vdcs[2].id, vdcs[3].id],
109
+ "remove_virtual_device_contexts": [vdcs[0].id],
97
110
  }
98
111
 
99
112
 
nautobot/ipam/urls.py CHANGED
@@ -7,7 +7,6 @@ from . import views
7
7
  from .models import (
8
8
  IPAddress,
9
9
  Prefix,
10
- RIR,
11
10
  Service,
12
11
  VLAN,
13
12
  VLANGroup,
@@ -20,28 +19,9 @@ router.register("ip-address-to-interface", views.IPAddressToInterfaceUIViewSet)
20
19
  router.register("namespaces", views.NamespaceUIViewSet)
21
20
  router.register("route-targets", views.RouteTargetUIViewSet)
22
21
  router.register("vrfs", views.VRFUIViewSet)
22
+ router.register("rirs", views.RIRUIViewSet)
23
23
 
24
24
  urlpatterns = [
25
- # RIRs
26
- path("rirs/", views.RIRListView.as_view(), name="rir_list"),
27
- path("rirs/add/", views.RIREditView.as_view(), name="rir_add"),
28
- path("rirs/import/", views.RIRBulkImportView.as_view(), name="rir_import"), # 3.0 TODO: remove, unused
29
- path("rirs/delete/", views.RIRBulkDeleteView.as_view(), name="rir_bulk_delete"),
30
- path("rirs/<uuid:pk>/", views.RIRView.as_view(), name="rir"),
31
- path("rirs/<uuid:pk>/edit/", views.RIREditView.as_view(), name="rir_edit"),
32
- path("rirs/<uuid:pk>/delete/", views.RIRDeleteView.as_view(), name="rir_delete"),
33
- path(
34
- "rirs/<uuid:pk>/changelog/",
35
- ObjectChangeLogView.as_view(),
36
- name="rir_changelog",
37
- kwargs={"model": RIR},
38
- ),
39
- path(
40
- "rirs/<uuid:pk>/notes/",
41
- ObjectNotesView.as_view(),
42
- name="rir_notes",
43
- kwargs={"model": RIR},
44
- ),
45
25
  # Namespaces
46
26
  path(
47
27
  "namespaces/<uuid:pk>/ip-addresses/",
nautobot/ipam/views.py CHANGED
@@ -325,49 +325,32 @@ class RouteTargetUIViewSet(NautobotUIViewSet):
325
325
  #
326
326
 
327
327
 
328
- class RIRListView(generic.ObjectListView):
328
+ class RIRUIViewSet(NautobotUIViewSet):
329
+ bulk_update_form_class = forms.RIRBulkEditForm
330
+ filterset_class = filters.RIRFilterSet
331
+ filterset_form_class = forms.RIRFilterForm
332
+ form_class = forms.RIRForm
329
333
  queryset = RIR.objects.all()
330
- filterset = filters.RIRFilterSet
331
- filterset_form = forms.RIRFilterForm
332
- table = tables.RIRTable
334
+ serializer_class = serializers.RIRSerializer
335
+ table_class = tables.RIRTable
333
336
 
334
-
335
- class RIRView(generic.ObjectView):
336
- queryset = RIR.objects.all()
337
-
338
- def get_extra_context(self, request, instance):
339
- # Prefixes
340
- assigned_prefixes = Prefix.objects.restrict(request.user, "view").filter(rir=instance).select_related("tenant")
341
-
342
- assigned_prefix_table = tables.PrefixTable(assigned_prefixes, hide_hierarchy_ui=True)
343
-
344
- paginate = {
345
- "paginator_class": EnhancedPaginator,
346
- "per_page": get_paginate_count(request),
347
- }
348
- RequestConfig(request, paginate).configure(assigned_prefix_table)
349
-
350
- return {"assigned_prefix_table": assigned_prefix_table, **super().get_extra_context(request, instance)}
351
-
352
-
353
- class RIREditView(generic.ObjectEditView):
354
- queryset = RIR.objects.all()
355
- model_form = forms.RIRForm
356
-
357
-
358
- class RIRDeleteView(generic.ObjectDeleteView):
359
- queryset = RIR.objects.all()
360
-
361
-
362
- class RIRBulkImportView(generic.BulkImportView): # 3.0 TODO: remove, unused
363
- queryset = RIR.objects.all()
364
- table = tables.RIRTable
365
-
366
-
367
- class RIRBulkDeleteView(generic.BulkDeleteView):
368
- queryset = RIR.objects.all()
369
- filterset = filters.RIRFilterSet
370
- table = tables.RIRTable
337
+ object_detail_content = object_detail.ObjectDetailContent(
338
+ panels=(
339
+ object_detail.ObjectFieldsPanel(
340
+ section=SectionChoices.LEFT_HALF,
341
+ weight=100,
342
+ fields="__all__",
343
+ ),
344
+ object_detail.ObjectsTablePanel(
345
+ section=SectionChoices.FULL_WIDTH,
346
+ weight=100,
347
+ table_title="Assigned Prefixes",
348
+ table_class=tables.PrefixTable,
349
+ table_filter="rir",
350
+ hide_hierarchy_ui=True,
351
+ ),
352
+ ),
353
+ )
371
354
 
372
355
 
373
356
  #
@@ -1022,3 +1022,14 @@ a.tile.clickable:visited {
1022
1022
  text-decoration: none;
1023
1023
  background-color: #646464;
1024
1024
  }
1025
+
1026
+ /* Monaco Editor container */
1027
+ .editor-container {
1028
+ display: flex; /* Use flexbox for layout */
1029
+ flex-direction: column;
1030
+ flex: 1; /* Allow container to grow */
1031
+ min-height: 100px;
1032
+ position: relative; /* Required for Monaco's absolute positioning */
1033
+ overflow: hidden; /* Let Monaco handle scrolling */
1034
+ }
1035
+
@@ -21,7 +21,8 @@ html[data-theme="dark"] .color-block, /* Colored choices, li
21
21
  html[data-theme="dark"] .select2-selection__choice, /* Colored choices, like statuses */
22
22
  html[data-theme="dark"] #select2-id_color-results, /* Colored choices, like statuses */
23
23
  html[data-theme="dark"] #select2-id_color-container, /* Colored choices, like statuses */
24
- html[data-theme="dark"] .hljs { /* highlight.js maintains its own dark theme */
24
+ html[data-theme="dark"] .hljs, /* highlight.js maintains its own dark theme */
25
+ html[data-theme="dark"] .editor-container { /* Monaco editor maintains its own dark theme */
25
26
  filter: invert(1) hue-rotate(180deg);
26
27
  }
27
28