nautobot 2.4.3__py3-none-any.whl → 2.4.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/__init__.py +19 -3
- nautobot/apps/filters.py +2 -0
- nautobot/circuits/filters.py +1 -1
- nautobot/circuits/tests/test_models.py +5 -3
- nautobot/cloud/filters.py +3 -6
- nautobot/cloud/tests/test_filters.py +21 -0
- nautobot/core/admin.py +2 -0
- nautobot/core/celery/__init__.py +5 -3
- nautobot/core/jobs/__init__.py +5 -3
- nautobot/core/management/commands/generate_performance_test_endpoints.py +9 -6
- nautobot/core/models/utils.py +6 -1
- nautobot/core/templates/inc/javascript.html +1 -0
- nautobot/core/templatetags/ui_framework.py +20 -4
- nautobot/core/testing/__init__.py +2 -0
- nautobot/core/testing/forms.py +1 -1
- nautobot/core/testing/mixins.py +9 -0
- nautobot/core/tests/test_api.py +1 -1
- nautobot/core/tests/test_graphql.py +3 -3
- nautobot/core/tests/test_jobs.py +30 -28
- nautobot/core/ui/object_detail.py +1 -1
- nautobot/dcim/api/serializers.py +36 -0
- nautobot/dcim/api/views.py +1 -1
- nautobot/dcim/elevations.py +17 -4
- nautobot/dcim/factory.py +9 -1
- nautobot/dcim/filters/__init__.py +27 -1
- nautobot/dcim/forms.py +13 -1
- nautobot/dcim/models/devices.py +11 -5
- nautobot/dcim/signals.py +26 -0
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
- nautobot/dcim/tests/test_api.py +176 -0
- nautobot/dcim/tests/test_filters.py +56 -3
- nautobot/dcim/tests/test_jobs.py +4 -6
- nautobot/dcim/tests/test_models.py +40 -0
- nautobot/dcim/views.py +24 -14
- nautobot/extras/api/mixins.py +1 -1
- nautobot/extras/api/views.py +2 -2
- nautobot/extras/choices.py +8 -3
- nautobot/extras/filters/__init__.py +4 -0
- nautobot/extras/jobs.py +181 -103
- nautobot/extras/management/utils.py +13 -2
- nautobot/extras/models/datasources.py +11 -4
- nautobot/extras/models/jobs.py +20 -17
- nautobot/extras/plugins/__init__.py +26 -1
- nautobot/extras/tables.py +25 -29
- nautobot/extras/templates/extras/inc/jobresult.html +12 -13
- nautobot/extras/templates/extras/objectchange.html +28 -12
- nautobot/extras/test_jobs/atomic_transaction.py +6 -6
- nautobot/extras/test_jobs/fail.py +75 -1
- nautobot/extras/tests/test_api.py +17 -16
- nautobot/extras/tests/test_datasources.py +64 -54
- nautobot/extras/tests/test_filters.py +2 -0
- nautobot/extras/tests/test_jobs.py +69 -62
- nautobot/extras/tests/test_models.py +1 -1
- nautobot/extras/tests/test_plugins.py +32 -1
- nautobot/extras/tests/test_relationships.py +5 -5
- nautobot/extras/tests/test_views.py +12 -2
- nautobot/extras/views.py +10 -1
- nautobot/ipam/api/serializers.py +7 -8
- nautobot/ipam/api/views.py +2 -2
- nautobot/ipam/factory.py +27 -8
- nautobot/ipam/filters.py +67 -29
- nautobot/ipam/formfields.py +51 -0
- nautobot/ipam/forms.py +28 -1
- nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
- nautobot/ipam/models.py +63 -5
- nautobot/ipam/querysets.py +6 -0
- nautobot/ipam/tables.py +21 -7
- nautobot/ipam/templates/ipam/rir.html +1 -43
- nautobot/ipam/tests/test_api.py +107 -66
- nautobot/ipam/tests/test_filters.py +145 -5
- nautobot/ipam/tests/test_models.py +16 -0
- nautobot/ipam/tests/test_views.py +15 -2
- nautobot/ipam/urls.py +1 -21
- nautobot/ipam/views.py +24 -41
- nautobot/project-static/css/base.css +11 -0
- nautobot/project-static/css/dark.css +2 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +43 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +35 -0
- nautobot/project-static/docs/development/apps/api/configuration-view.html +0 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +0 -4
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +94 -1
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +0 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +0 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +0 -3
- nautobot/project-static/docs/development/apps/api/testing.html +0 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +0 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +0 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +0 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +0 -4
- nautobot/project-static/docs/development/apps/api/views/notes.html +0 -3
- nautobot/project-static/docs/development/apps/index.html +2 -35
- nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +0 -6
- nautobot/project-static/docs/development/core/best-practices.html +0 -27
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +58 -4
- nautobot/project-static/docs/development/core/getting-started.html +12 -16
- nautobot/project-static/docs/development/core/homepage.html +0 -3
- nautobot/project-static/docs/development/core/style-guide.html +0 -5
- nautobot/project-static/docs/development/core/templates.html +0 -3
- nautobot/project-static/docs/development/core/testing.html +0 -9
- nautobot/project-static/docs/development/jobs/index.html +30 -43
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +0 -18
- nautobot/project-static/docs/release-notes/version-2.4.html +374 -0
- nautobot/project-static/docs/requirements.txt +2 -2
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +290 -290
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +0 -10
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +0 -15
- nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -4
- nautobot/project-static/docs/user-guide/administration/installation/services.html +0 -11
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/tables/v2-code-location-changes.yaml +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -17
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -7
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +0 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +0 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +0 -4
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +0 -8
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +0 -6
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -15
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +0 -26
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +0 -7
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -10
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +0 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +0 -14
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +0 -19
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -9
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +0 -4
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -13
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +0 -5
- nautobot/project-static/js/editor.js +292 -0
- nautobot/project-static/monaco-editor-0.52.2/README.md +81 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/base/worker/workerMain.js +31 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/xml/xml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/basic-languages/yaml/yaml.js +10 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.css +8 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/editor/editor.main.js +798 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonMode.js +19 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/language/json/jsonWorker.js +42 -0
- nautobot/project-static/monaco-editor-0.52.2/vs/loader.js +11 -0
- nautobot/tenancy/filters/__init__.py +3 -5
- nautobot/tenancy/tests/test_filters.py +10 -0
- nautobot/virtualization/views.py +0 -1
- nautobot/wireless/tables.py +9 -4
- nautobot/wireless/tests/test_api.py +0 -9
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/METADATA +4 -4
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/RECORD +198 -186
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/NOTICE +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/WHEEL +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.5.dist-info}/entry_points.txt +0 -0
nautobot/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 =
|
|
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", "
|
|
179
|
+
fields = ["id", "name", "rd"]
|
|
131
180
|
|
|
132
181
|
|
|
133
182
|
class VRFPrefixAssignmentFilterSet(NautobotFilterSet):
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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 =
|
|
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"]
|
nautobot/ipam/formfields.py
CHANGED
|
@@ -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(
|
|
316
|
-
|
|
317
|
-
|
|
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")
|
nautobot/ipam/querysets.py
CHANGED
|
@@ -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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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", "
|
|
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
|
-
{%
|
|
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 %}
|