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
nautobot/ipam/filters.py CHANGED
@@ -19,10 +19,10 @@ from nautobot.core.filters import (
19
19
  TreeNodeMultipleChoiceFilter,
20
20
  )
21
21
  from nautobot.dcim.filters import LocatableModelFilterSetMixin
22
- from nautobot.dcim.models import Device, Interface, Location
22
+ from nautobot.dcim.models import Device, Interface, Location, VirtualDeviceContext
23
23
  from nautobot.extras.filters import NautobotFilterSet, RoleModelFilterSetMixin, StatusModelFilterSetMixin
24
- from nautobot.ipam import choices
25
- from nautobot.tenancy.filters import TenancyModelFilterSetMixin
24
+ from nautobot.ipam import choices, formfields
25
+ from nautobot.tenancy.filters.mixins import TenancyModelFilterSetMixin
26
26
  from nautobot.virtualization.models import VirtualMachine, VMInterface
27
27
 
28
28
  from .models import (
@@ -45,6 +45,7 @@ from .models import (
45
45
  __all__ = (
46
46
  "IPAddressFilterSet",
47
47
  "NamespaceFilterSet",
48
+ "PrefixFilter",
48
49
  "PrefixFilterSet",
49
50
  "RIRFilterSet",
50
51
  "RouteTargetFilterSet",
@@ -55,6 +56,39 @@ __all__ = (
55
56
  )
56
57
 
57
58
 
59
+ class PrefixFilter(NaturalKeyOrPKMultipleChoiceFilter):
60
+ """
61
+ Filter that supports filtering a foreign key to Prefix by either its PK or by a literal `prefix` string.
62
+ """
63
+
64
+ field_class = formfields.PrefixFilterFormField
65
+
66
+ def __init__(self, *args, **kwargs):
67
+ kwargs.setdefault("to_field_name", "pk")
68
+ kwargs.setdefault("label", "Prefix (ID or prefix string)")
69
+ kwargs.setdefault("queryset", Prefix.objects.all())
70
+ super().__init__(*args, **kwargs)
71
+
72
+ def get_filter_predicate(self, v):
73
+ # Null value filtering
74
+ if v is None:
75
+ return {f"{self.field_name}__isnull": True}
76
+
77
+ # If value is a model instance, stringify it to a pk.
78
+ if isinstance(v, Prefix):
79
+ v = v.pk
80
+
81
+ # Try to cast the value to a UUID to distinguish between PKs and prefix strings
82
+ v = str(v)
83
+ try:
84
+ uuid.UUID(v)
85
+ return {self.field_name: v}
86
+ except (AttributeError, TypeError, ValueError):
87
+ # It's a prefix string
88
+ prefixes_queryset = Prefix.objects.net_equals(v)
89
+ return {f"{self.field_name}__in": prefixes_queryset.values_list("pk", flat=True)}
90
+
91
+
58
92
  class NamespaceFilterSet(NautobotFilterSet):
59
93
  q = SearchFilter(
60
94
  filter_predicates={
@@ -96,12 +130,7 @@ class VRFFilterSet(NautobotFilterSet, StatusModelFilterSetMixin, TenancyModelFil
96
130
  to_field_name="name",
97
131
  label="Virtual Machine (ID or name)",
98
132
  )
99
- prefix = NaturalKeyOrPKMultipleChoiceFilter(
100
- field_name="prefixes",
101
- queryset=Prefix.objects.all(),
102
- to_field_name="pk", # TODO: Make this work with `prefix` "somehow"
103
- label="Prefix (ID or name)",
104
- )
133
+ prefix = PrefixFilter(field_name="prefixes")
105
134
  namespace = NaturalKeyOrPKMultipleChoiceFilter(
106
135
  queryset=Namespace.objects.all(),
107
136
  to_field_name="name",
@@ -114,6 +143,21 @@ class VRFFilterSet(NautobotFilterSet, StatusModelFilterSetMixin, TenancyModelFil
114
143
 
115
144
 
116
145
  class VRFDeviceAssignmentFilterSet(NautobotFilterSet):
146
+ q = SearchFilter(
147
+ filter_predicates={
148
+ "name": "icontains",
149
+ "vrf__name": "icontains",
150
+ "device__name": "icontains",
151
+ "virtual_machine__name": "icontains",
152
+ "virtual_device_context__name": "icontains",
153
+ "rd": "icontains",
154
+ },
155
+ )
156
+ vrf = NaturalKeyOrPKMultipleChoiceFilter(
157
+ queryset=VRF.objects.all(),
158
+ to_field_name="name",
159
+ label="VRF (ID or name)",
160
+ )
117
161
  device = NaturalKeyOrPKMultipleChoiceFilter(
118
162
  queryset=Device.objects.all(),
119
163
  to_field_name="name",
@@ -124,18 +168,25 @@ class VRFDeviceAssignmentFilterSet(NautobotFilterSet):
124
168
  to_field_name="name",
125
169
  label="Virtual Machine (ID or name)",
126
170
  )
171
+ virtual_device_context = NaturalKeyOrPKMultipleChoiceFilter(
172
+ queryset=VirtualDeviceContext.objects.all(),
173
+ to_field_name="name",
174
+ label="Virtual Device Context (ID or name)",
175
+ )
127
176
 
128
177
  class Meta:
129
178
  model = VRFDeviceAssignment
130
- fields = ["id", "vrf", "device", "virtual_machine"]
179
+ fields = ["id", "name", "rd"]
131
180
 
132
181
 
133
182
  class VRFPrefixAssignmentFilterSet(NautobotFilterSet):
134
- prefix = NaturalKeyOrPKMultipleChoiceFilter(
135
- queryset=Prefix.objects.all(),
136
- to_field_name="pk", # TODO: Make this work with `prefix` "somehow"
137
- label="Prefix (ID or name)",
183
+ q = SearchFilter(
184
+ filter_predicates={
185
+ # "prefix__prefix": "iexact", # TODO?
186
+ "vrf__name": "icontains",
187
+ },
138
188
  )
189
+ prefix = PrefixFilter()
139
190
  vrf = NaturalKeyOrPKMultipleChoiceFilter(
140
191
  queryset=VRF.objects.all(),
141
192
  to_field_name="name",
@@ -200,10 +251,7 @@ class PrefixFilterSet(
200
251
  StatusModelFilterSetMixin,
201
252
  RoleModelFilterSetMixin,
202
253
  ):
203
- # Prefix doesn't have an appropriate single natural-key field for a NaturalKeyOrPKMultipleChoiceFilter
204
- parent = django_filters.ModelMultipleChoiceFilter(
205
- queryset=Prefix.objects.all(),
206
- )
254
+ parent = PrefixFilter()
207
255
  prefix = MultiValueCharFilter(
208
256
  method="filter_prefix",
209
257
  label="Prefix",
@@ -343,10 +391,7 @@ class PrefixLocationAssignmentFilterSet(NautobotFilterSet):
343
391
  "location__name": "icontains",
344
392
  },
345
393
  )
346
- prefix = MultiValueCharFilter(
347
- method="filter_prefix",
348
- label="Prefix",
349
- )
394
+ prefix = PrefixFilter()
350
395
  location = TreeNodeMultipleChoiceFilter(
351
396
  prefers_id=True,
352
397
  queryset=Location.objects.all(),
@@ -357,13 +402,6 @@ class PrefixLocationAssignmentFilterSet(NautobotFilterSet):
357
402
  def _strip_values(self, values):
358
403
  return [value.strip() for value in values if value.strip()]
359
404
 
360
- def filter_prefix(self, queryset, name, value):
361
- prefixes = self._strip_values(value)
362
- with contextlib.suppress(netaddr.AddrFormatError, ValueError):
363
- prefixes_queryset = Prefix.objects.net_equals(*prefixes)
364
- return queryset.filter(prefix__in=prefixes_queryset)
365
- return queryset.none()
366
-
367
405
  class Meta:
368
406
  model = PrefixLocationAssignment
369
407
  fields = ["id", "prefix", "location"]
@@ -1,8 +1,12 @@
1
1
  from django import forms
2
2
  from django.core.exceptions import ValidationError
3
3
  from django.core.validators import validate_ipv4_address, validate_ipv6_address
4
+ from django.db.models import Q
4
5
  from netaddr import AddrFormatError, IPAddress, IPNetwork
5
6
 
7
+ from nautobot.core.forms.fields import MultiMatchModelMultipleChoiceField
8
+ from nautobot.core.utils.data import is_uuid
9
+
6
10
  #
7
11
  # Form fields
8
12
  #
@@ -58,3 +62,50 @@ class IPNetworkFormField(forms.Field):
58
62
  return IPNetwork(value)
59
63
  except AddrFormatError:
60
64
  raise ValidationError("Please specify a valid IPv4 or IPv6 address.")
65
+
66
+
67
+ class PrefixFilterFormField(MultiMatchModelMultipleChoiceField):
68
+ @property
69
+ def filter(self):
70
+ from nautobot.ipam.filters import PrefixFilter # avoid circular definition
71
+
72
+ return PrefixFilter
73
+
74
+ def _check_values(self, values): # pylint: disable=arguments-renamed
75
+ null_value_present = self.null_label is not None and values and self.null_value in values
76
+ if null_value_present:
77
+ values = [v for v in values if v != self.null_value]
78
+ # deduplicate given values to avoid creating many querysets or
79
+ # requiring the database backend deduplicate efficiently.
80
+ try:
81
+ values = frozenset(values)
82
+ except TypeError:
83
+ raise ValidationError(self.error_messages["invalid_list"], code="invalid_list")
84
+ pk_values = set()
85
+ prefix_queries = []
86
+ for value in values:
87
+ if is_uuid(value):
88
+ pk_values.add(value)
89
+ query = Q(pk=value)
90
+ else:
91
+ ipnetwork = IPNetwork(value)
92
+ query = Q(
93
+ network=ipnetwork.network,
94
+ prefix_length=ipnetwork.prefixlen,
95
+ broadcast=ipnetwork.broadcast or ipnetwork[-1],
96
+ )
97
+ prefix_queries.append(query)
98
+ if not self.queryset.filter(query).exists():
99
+ raise ValidationError(
100
+ self.error_messages["invalid_choice"],
101
+ code="invalid_choice",
102
+ params={"value": value},
103
+ )
104
+ aggregate_query = Q(pk__in=pk_values)
105
+ for prefix_query in prefix_queries:
106
+ aggregate_query |= prefix_query
107
+ qs = self.queryset.filter(aggregate_query)
108
+ result = list(qs)
109
+ if null_value_present:
110
+ result.append(self.null_value)
111
+ return result
nautobot/ipam/forms.py CHANGED
@@ -23,7 +23,7 @@ from nautobot.dcim.form_mixins import (
23
23
  LocatableModelFilterFormMixin,
24
24
  LocatableModelFormMixin,
25
25
  )
26
- from nautobot.dcim.models import Device, Location, Rack
26
+ from nautobot.dcim.models import Device, Location, Rack, VirtualDeviceContext
27
27
  from nautobot.extras.forms import (
28
28
  NautobotBulkEditForm,
29
29
  NautobotFilterForm,
@@ -111,6 +111,9 @@ class VRFForm(NautobotModelForm, TenancyForm):
111
111
  namespace = DynamicModelChoiceField(queryset=Namespace.objects.all())
112
112
  devices = DynamicModelMultipleChoiceField(queryset=Device.objects.all(), required=False)
113
113
  virtual_machines = DynamicModelMultipleChoiceField(queryset=VirtualMachine.objects.all(), required=False)
114
+ virtual_device_contexts = DynamicModelMultipleChoiceField(
115
+ queryset=VirtualDeviceContext.objects.all(), required=False
116
+ )
114
117
  prefixes = DynamicModelMultipleChoiceField(
115
118
  queryset=Prefix.objects.all(),
116
119
  required=False,
@@ -134,6 +137,7 @@ class VRFForm(NautobotModelForm, TenancyForm):
134
137
  "tags",
135
138
  "devices",
136
139
  "virtual_machines",
140
+ "virtual_device_contexts",
137
141
  "prefixes",
138
142
  ]
139
143
  labels = {
@@ -156,6 +160,14 @@ class VRFBulkEditForm(TagsBulkEditFormMixin, StatusModelBulkEditFormMixin, Nauto
156
160
  remove_prefixes = DynamicModelMultipleChoiceField(
157
161
  queryset=Prefix.objects.all(), required=False, query_params={"namespace": "$namespace"}
158
162
  )
163
+ add_virtual_device_contexts = DynamicModelMultipleChoiceField(
164
+ queryset=VirtualDeviceContext.objects.all(),
165
+ required=False,
166
+ )
167
+ remove_virtual_device_contexts = DynamicModelMultipleChoiceField(
168
+ queryset=VirtualDeviceContext.objects.all(),
169
+ required=False,
170
+ )
159
171
 
160
172
  class Meta:
161
173
  nullable_fields = [
@@ -250,6 +262,21 @@ class RIRFilterForm(NautobotFilterForm):
250
262
  )
251
263
 
252
264
 
265
+ class RIRBulkEditForm(NautobotBulkEditForm):
266
+ pk = forms.ModelMultipleChoiceField(queryset=RIR.objects.all(), widget=forms.MultipleHiddenInput())
267
+ is_private = forms.NullBooleanField(
268
+ required=False,
269
+ label="Private",
270
+ widget=StaticSelect2(choices=BOOLEAN_WITH_BLANK_CHOICES),
271
+ )
272
+ description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
273
+
274
+ class Meta:
275
+ nullable_fields = [
276
+ "description",
277
+ ]
278
+
279
+
253
280
  #
254
281
  # Prefixes
255
282
  #
@@ -0,0 +1,41 @@
1
+ # Generated by Django 4.2.19 on 2025-02-24 21:20
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ dependencies = [
9
+ ("virtualization", "0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more"),
10
+ ("dcim", "0067_controllermanageddevicegroup_tenant"),
11
+ ("ipam", "0050_vlangroup_range"),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.AddField(
16
+ model_name="vrf",
17
+ name="virtual_device_contexts",
18
+ field=models.ManyToManyField(
19
+ related_name="vrfs", through="ipam.VRFDeviceAssignment", to="dcim.virtualdevicecontext"
20
+ ),
21
+ ),
22
+ migrations.AddField(
23
+ model_name="vrfdeviceassignment",
24
+ name="virtual_device_context",
25
+ field=models.ForeignKey(
26
+ blank=True,
27
+ null=True,
28
+ on_delete=django.db.models.deletion.CASCADE,
29
+ related_name="vrf_assignments",
30
+ to="dcim.virtualdevicecontext",
31
+ ),
32
+ ),
33
+ migrations.AlterUniqueTogether(
34
+ name="vrfdeviceassignment",
35
+ unique_together={
36
+ ("vrf", "virtual_device_context"),
37
+ ("vrf", "virtual_machine"),
38
+ ("vrf", "device"),
39
+ },
40
+ ),
41
+ ]
nautobot/ipam/models.py CHANGED
@@ -135,6 +135,12 @@ class VRF(PrimaryModel):
135
135
  through="ipam.VRFDeviceAssignment",
136
136
  through_fields=("vrf", "virtual_machine"),
137
137
  )
138
+ virtual_device_contexts = models.ManyToManyField(
139
+ to="dcim.VirtualDeviceContext",
140
+ related_name="vrfs",
141
+ through="ipam.VRFDeviceAssignment",
142
+ through_fields=("vrf", "virtual_device_context"),
143
+ )
138
144
  prefixes = models.ManyToManyField(
139
145
  to="ipam.Prefix",
140
146
  related_name="vrfs",
@@ -238,6 +244,41 @@ class VRF(PrimaryModel):
238
244
  instance = self.virtual_machines.through.objects.get(vrf=self, virtual_machine=virtual_machine)
239
245
  return instance.delete()
240
246
 
247
+ def add_virtual_device_context(self, virtual_device_context, rd="", name=""):
248
+ """
249
+ Add a `virtual_device_context` to this VRF, optionally overloading `rd` and `name`.
250
+
251
+ If `rd` or `name` are not provided, the values from this VRF will be inherited.
252
+
253
+ Args:
254
+ virtual_device_context (VirtualDeviceContext): VirtualDeviceContext instance
255
+ rd (str): (Optional) RD of the VRF when associated with this VirtualDeviceContext
256
+ name (str): (Optional) Name of the VRF when associated with this VirtualDeviceContext
257
+
258
+ Returns:
259
+ VRFDeviceAssignment instance
260
+ """
261
+ instance = self.virtual_device_contexts.through(
262
+ vrf=self, virtual_device_context=virtual_device_context, rd=rd, name=name
263
+ )
264
+ instance.validated_save()
265
+ return instance
266
+
267
+ def remove_virtual_device_context(self, virtual_device_context):
268
+ """
269
+ Remove a `virtual_device_context` from this VRF.
270
+
271
+ Args:
272
+ virtual_device_context (VirtualDeviceContext): VirtualDeviceContext instance
273
+
274
+ Returns:
275
+ tuple (int, dict): Number of objects deleted and a dict with number of deletions.
276
+ """
277
+ instance = self.virtual_device_contexts.through.objects.get(
278
+ vrf=self, virtual_device_context=virtual_device_context
279
+ )
280
+ return instance.delete()
281
+
241
282
  def add_prefix(self, prefix):
242
283
  """
243
284
  Add a `prefix` to this VRF. Each object must be in the same Namespace.
@@ -275,6 +316,9 @@ class VRFDeviceAssignment(BaseModel):
275
316
  virtual_machine = models.ForeignKey(
276
317
  "virtualization.VirtualMachine", null=True, blank=True, on_delete=models.CASCADE, related_name="vrf_assignments"
277
318
  )
319
+ virtual_device_context = models.ForeignKey(
320
+ "dcim.VirtualDeviceContext", null=True, blank=True, on_delete=models.CASCADE, related_name="vrf_assignments"
321
+ )
278
322
  rd = models.CharField( # noqa: DJ001 # django-nullable-model-string-field -- see below
279
323
  max_length=constants.VRF_RD_MAX_LENGTH,
280
324
  blank=True,
@@ -289,14 +333,16 @@ class VRFDeviceAssignment(BaseModel):
289
333
  unique_together = [
290
334
  ["vrf", "device"],
291
335
  ["vrf", "virtual_machine"],
336
+ ["vrf", "virtual_device_context"],
292
337
  # TODO: desirable in the future, but too strict for 1.x-to-2.0 data migrations,
293
338
  # as multiple "cleanup" VRFs in different cleanup namespaces might be assigned to a single device/VM.
294
339
  # ["device", "rd", "name"],
295
340
  # ["virtual_machine", "rd", "name"],
341
+ # ["virtual_device_context", "rd", "name"],
296
342
  ]
297
343
 
298
344
  def __str__(self):
299
- obj = self.device or self.virtual_machine
345
+ obj = self.device or self.virtual_machine or self.virtual_device_context
300
346
  return f"{self.vrf} [{obj}] (rd: {self.rd}, name: {self.name})"
301
347
 
302
348
  def clean(self):
@@ -310,11 +356,23 @@ class VRFDeviceAssignment(BaseModel):
310
356
  if not self.name:
311
357
  self.name = self.vrf.name
312
358
 
313
- # A VRF must belong to a Device *or* to a VirtualMachine.
359
+ # A VRF must belong to a Device *or* to a VirtualMachine *or* to a Virtual Device Context.
314
360
  if all([self.device, self.virtual_machine]):
315
- raise ValidationError("A VRF cannot be associated with both a device and a virtual machine.")
316
- if not any([self.device, self.virtual_machine]):
317
- raise ValidationError("A VRF must be associated with either a device or a virtual machine.")
361
+ raise ValidationError(
362
+ "A VRFDeviceAssignment entry cannot be associated with both a device and a virtual machine."
363
+ )
364
+ if all([self.device, self.virtual_device_context]):
365
+ raise ValidationError(
366
+ "A VRFDeviceAssignment entry cannot be associated with both a device and a virtual device context."
367
+ )
368
+ if all([self.virtual_machine, self.virtual_device_context]):
369
+ raise ValidationError(
370
+ "A VRFDeviceAssignment entry cannot be associated with both a virtual machine and a virtual device context."
371
+ )
372
+ if not any([self.device, self.virtual_machine, self.virtual_device_context]):
373
+ raise ValidationError(
374
+ "A VRFDeviceAssignment entry must be associated with a device, a virtual machine, or a virtual device context."
375
+ )
318
376
 
319
377
 
320
378
  @extras_features("graphql")
@@ -405,6 +405,12 @@ class IPAddressQuerySet(BaseNetworkQuerySet):
405
405
  namespace = kwargs.pop("namespace", None)
406
406
  host = kwargs.get("host")
407
407
  mask_length = kwargs.get("mask_length")
408
+ address = kwargs.get("address")
409
+ if host is None and address is not None:
410
+ address = netaddr.IPNetwork(address)
411
+ host = str(address.ip)
412
+ mask_length = address.prefixlen
413
+
408
414
  # If `host` or `mask_length` is None skip; then there is no way of getting the closest parent;
409
415
  if parent is None and host is not None and mask_length is not None:
410
416
  if namespace is None:
nautobot/ipam/tables.py CHANGED
@@ -247,13 +247,27 @@ class VRFDeviceAssignmentTable(BaseTable):
247
247
  linkify=lambda record: record.vrf.namespace.get_absolute_url(),
248
248
  accessor="vrf.namespace.name",
249
249
  )
250
- device = tables.Column(
251
- linkify=lambda record: record.device.get_absolute_url(), accessor="device.name", verbose_name="Device"
250
+ related_object_type = tables.TemplateColumn(
251
+ template_code="""
252
+ {% if record.device %}
253
+ Device
254
+ {% elif record.virtual_machine %}
255
+ Virtual Machine
256
+ {% else %}
257
+ Virtual Device Context
258
+ {% endif %}
259
+ """
252
260
  )
253
- virtual_machine = tables.Column(
254
- linkify=lambda record: record.virtual_machine.get_absolute_url(),
255
- accessor="virtual_machine.name",
256
- verbose_name="Virtual Machine",
261
+ related_object_name = tables.TemplateColumn(
262
+ template_code="""
263
+ {% if record.device %}
264
+ <a href="{{ record.device.get_absolute_url }}">{{ record.device.name }}</a>
265
+ {% elif record.virtual_machine %}
266
+ <a href="{{ record.virtual_machine.get_absolute_url }}">{{ record.virtual_machine.name }}</a>
267
+ {% else %}
268
+ <a href="{{ record.virtual_device_context.get_absolute_url }}">{{ record.virtual_device_context.name }}</a>
269
+ {% endif %}
270
+ """
257
271
  )
258
272
  rd = tables.Column(verbose_name="VRF RD")
259
273
  tenant = TenantColumn(accessor="vrf.tenant")
@@ -261,7 +275,7 @@ class VRFDeviceAssignmentTable(BaseTable):
261
275
  class Meta(BaseTable.Meta):
262
276
  model = VRFDeviceAssignment
263
277
  orderable = False
264
- fields = ("vrf", "namespace", "device", "virtual_machine", "rd", "tenant")
278
+ fields = ("vrf", "related_object_type", "related_object_name", "namespace", "rd", "tenant")
265
279
 
266
280
 
267
281
  class VRFPrefixAssignmentTable(BaseTable):
@@ -1,44 +1,2 @@
1
1
  {% extends 'generic/object_retrieve.html' %}
2
- {% load helpers %}
3
-
4
- {% block content_left_page %}
5
- <div class="panel panel-default">
6
- <div class="panel-heading">
7
- <strong>RIR</strong>
8
- </div>
9
- <table class="table table-hover panel-body attr-table">
10
- <tr>
11
- <td>Description</td>
12
- <td>{{ object.description|placeholder }}</td>
13
- </tr>
14
- <tr>
15
- <td>Private</td>
16
- <td>{{ object.is_private | render_boolean }}</td>
17
- </tr>
18
- <tr>
19
- <td>Assigned Prefixes</td>
20
- <td>
21
- <a href="{% url 'ipam:prefix_list' %}?rir={{ object.name }}">{{ assigned_prefix_table.rows|length }}</a>
22
- </td>
23
- </tr>
24
- </table>
25
- </div>
26
- {% endblock content_left_page %}
27
-
28
- {% block content_full_width_page %}
29
- <div class="panel panel-default">
30
- <div class="panel-heading">
31
- <strong>Assigned Prefixes</strong>
32
- </div>
33
- {% include 'inc/table.html' with table=assigned_prefix_table %}
34
- {% if perms.ipam.add_prefix %}
35
- <div class="panel-footer text-right noprint">
36
- <a href="{% url 'ipam:prefix_add' %}?rir={{ object.pk }}" class="btn btn-xs btn-primary">
37
- <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add prefix
38
- </a>
39
- </div>
40
- {% endif %}
41
- </div>
42
- {% include 'inc/paginator.html' with paginator=assigned_prefix_table.paginator page=assigned_prefix_table.page %}
43
- <div class="row"></div>
44
- {% endblock content_full_width_page %}
2
+ {% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}