nautobot 2.4.3__py3-none-any.whl → 2.4.4__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.
- 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/jobs/__init__.py +2 -1
- 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/forms.py +1 -1
- nautobot/core/tests/test_api.py +1 -1
- nautobot/core/tests/test_graphql.py +3 -3
- nautobot/core/tests/test_jobs.py +4 -1
- 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_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/filters/__init__.py +4 -0
- nautobot/extras/models/datasources.py +7 -3
- nautobot/extras/plugins/__init__.py +26 -1
- nautobot/extras/templates/extras/inc/jobresult.html +12 -13
- nautobot/extras/templates/extras/objectchange.html +28 -12
- nautobot/extras/tests/test_api.py +16 -15
- nautobot/extras/tests/test_filters.py +2 -0
- nautobot/extras/tests/test_plugins.py +32 -1
- nautobot/extras/tests/test_views.py +12 -2
- nautobot/extras/views.py +3 -0
- 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 +13 -1
- nautobot/ipam/migrations/0051_added_optional_vrf_relationship_to_vdc.py +41 -0
- nautobot/ipam/models.py +63 -5
- nautobot/ipam/tables.py +21 -7
- nautobot/ipam/tests/test_api.py +107 -66
- nautobot/ipam/tests/test_filters.py +145 -5
- nautobot/ipam/tests/test_views.py +15 -2
- 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/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 +3 -29
- 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 +191 -0
- nautobot/project-static/docs/requirements.txt +1 -1
- 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.4.dist-info}/METADATA +2 -2
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/RECORD +175 -163
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/NOTICE +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/WHEEL +0 -0
- {nautobot-2.4.3.dist-info → nautobot-2.4.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{% extends 'generic/object_retrieve.html' %}
|
|
2
2
|
{% load helpers %}
|
|
3
|
+
{% load static %}
|
|
3
4
|
|
|
4
5
|
{% block title %}{{ object }}{% endblock %}
|
|
5
6
|
|
|
@@ -95,6 +96,20 @@
|
|
|
95
96
|
</tr>
|
|
96
97
|
</table>
|
|
97
98
|
</div>
|
|
99
|
+
<div class="panel panel-default">
|
|
100
|
+
<div class="panel-heading">
|
|
101
|
+
<strong>Object Data</strong>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="panel-body">
|
|
104
|
+
<div class="editor-container"
|
|
105
|
+
data-lang="json"
|
|
106
|
+
data-value="{{ object.object_data|render_json:False }}"
|
|
107
|
+
style="max-height: 300px">
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="col-md-7">
|
|
98
113
|
<div class="panel panel-default">
|
|
99
114
|
<div class="panel-heading">
|
|
100
115
|
<strong>Difference</strong>
|
|
@@ -119,22 +134,17 @@
|
|
|
119
134
|
{% endif %}
|
|
120
135
|
</span>
|
|
121
136
|
{% else %}
|
|
122
|
-
<
|
|
123
|
-
|
|
137
|
+
<div class="editor-container"
|
|
138
|
+
data-mode="diff"
|
|
139
|
+
data-original="{{ diff_removed | render_json:False }}"
|
|
140
|
+
data-modified="{{ diff_added | render_json:False }}"
|
|
141
|
+
data-lang="json"
|
|
142
|
+
style="max-height: 730px">
|
|
143
|
+
</div>
|
|
124
144
|
{% endif %}
|
|
125
145
|
</div>
|
|
126
146
|
</div>
|
|
127
147
|
</div>
|
|
128
|
-
<div class="col-md-7">
|
|
129
|
-
<div class="panel panel-default">
|
|
130
|
-
<div class="panel-heading">
|
|
131
|
-
<strong>Object Data</strong>
|
|
132
|
-
</div>
|
|
133
|
-
<div class="panel-body">
|
|
134
|
-
<pre>{{ object.object_data|render_json }}</pre>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
148
|
</div>
|
|
139
149
|
<div class="row">
|
|
140
150
|
<div class="col-md-12">
|
|
@@ -147,3 +157,9 @@
|
|
|
147
157
|
</div>
|
|
148
158
|
</div>
|
|
149
159
|
{% endblock %}
|
|
160
|
+
|
|
161
|
+
{% block javascript %}
|
|
162
|
+
{{ block.super }}
|
|
163
|
+
<script src="{% static 'js/editor.js' %}"></script>
|
|
164
|
+
{% endblock %}
|
|
165
|
+
|
|
@@ -196,20 +196,6 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
196
196
|
|
|
197
197
|
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
198
198
|
model = ConfigContext
|
|
199
|
-
create_data = [
|
|
200
|
-
{
|
|
201
|
-
"name": "Config Context 4",
|
|
202
|
-
"data": {"more_foo": True},
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
"name": "Config Context 5",
|
|
206
|
-
"data": {"more_bar": False},
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
"name": "Config Context 6",
|
|
210
|
-
"data": {"more_baz": None},
|
|
211
|
-
},
|
|
212
|
-
]
|
|
213
199
|
bulk_update_data = {
|
|
214
200
|
"description": "New description",
|
|
215
201
|
}
|
|
@@ -220,6 +206,21 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
220
206
|
ConfigContext.objects.create(name="Config Context 1", weight=100, data={"foo": 123})
|
|
221
207
|
ConfigContext.objects.create(name="Config Context 2", weight=200, data={"bar": 456})
|
|
222
208
|
ConfigContext.objects.create(name="Config Context 3", weight=300, data={"baz": 789})
|
|
209
|
+
cls.create_data = [
|
|
210
|
+
{
|
|
211
|
+
"name": "Config Context 4",
|
|
212
|
+
"data": {"more_foo": True},
|
|
213
|
+
"tags": [tag.pk for tag in Tag.objects.get_for_model(Device)],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "Config Context 5",
|
|
217
|
+
"data": {"more_bar": False},
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "Config Context 6",
|
|
221
|
+
"data": {"more_baz": None},
|
|
222
|
+
},
|
|
223
|
+
]
|
|
223
224
|
|
|
224
225
|
def test_render_configcontext_for_object(self):
|
|
225
226
|
"""
|
|
@@ -1797,7 +1798,7 @@ class JobTest(
|
|
|
1797
1798
|
)
|
|
1798
1799
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1799
1800
|
self.assertIn(
|
|
1800
|
-
"task_queue and job_queue are both specified. Please
|
|
1801
|
+
"task_queue and job_queue are both specified. Please specify only one or another.", str(response.content)
|
|
1801
1802
|
)
|
|
1802
1803
|
|
|
1803
1804
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -880,6 +880,8 @@ class JobFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
880
880
|
generic_filter_tests = (
|
|
881
881
|
("grouping",),
|
|
882
882
|
("job_class_name",),
|
|
883
|
+
("job_queues", "job_queues__id"),
|
|
884
|
+
("job_queues", "job_queues__name"),
|
|
883
885
|
("module_name",),
|
|
884
886
|
("name",),
|
|
885
887
|
)
|
|
@@ -17,11 +17,12 @@ from nautobot.dcim.models import Device, DeviceType, Location, LocationType, Man
|
|
|
17
17
|
from nautobot.dcim.tests.test_views import create_test_device
|
|
18
18
|
from nautobot.extras import plugins
|
|
19
19
|
from nautobot.extras.choices import CustomFieldTypeChoices, RelationshipTypeChoices
|
|
20
|
+
from nautobot.extras.context_managers import web_request_context
|
|
20
21
|
from nautobot.extras.jobs import get_job
|
|
21
22
|
from nautobot.extras.models import CustomField, Relationship, RelationshipAssociation, Role, Secret, Status
|
|
22
23
|
from nautobot.extras.plugins.exceptions import PluginImproperlyConfigured
|
|
23
24
|
from nautobot.extras.plugins.utils import load_plugin
|
|
24
|
-
from nautobot.extras.plugins.validators import wrap_model_clean_methods
|
|
25
|
+
from nautobot.extras.plugins.validators import CustomValidator, wrap_model_clean_methods
|
|
25
26
|
from nautobot.extras.plugins.views import extract_app_data
|
|
26
27
|
from nautobot.extras.registry import DatasourceContent, registry
|
|
27
28
|
from nautobot.ipam.models import IPAddress, Namespace, Prefix
|
|
@@ -478,6 +479,16 @@ class AppAPITest(APIViewTestCases.APIViewTestCase):
|
|
|
478
479
|
pass
|
|
479
480
|
|
|
480
481
|
|
|
482
|
+
class TestUserContextCustomValidator(CustomValidator):
|
|
483
|
+
model = "dcim.locationtype"
|
|
484
|
+
|
|
485
|
+
def clean(self):
|
|
486
|
+
"""
|
|
487
|
+
Used to validate that the correct user context is available in the custom validator.
|
|
488
|
+
"""
|
|
489
|
+
self.validation_error(self.context["user"])
|
|
490
|
+
|
|
491
|
+
|
|
481
492
|
class AppCustomValidationTest(TestCase):
|
|
482
493
|
def setUp(self):
|
|
483
494
|
# When creating a fresh test DB, wrapping model clean methods fails, which is normal.
|
|
@@ -485,6 +496,7 @@ class AppCustomValidationTest(TestCase):
|
|
|
485
496
|
# must manually call the method again to actually perform the action, now that the
|
|
486
497
|
# ContentType table has been created.
|
|
487
498
|
wrap_model_clean_methods()
|
|
499
|
+
super().setUp()
|
|
488
500
|
|
|
489
501
|
def test_custom_validator_raises_exception(self):
|
|
490
502
|
location_type = LocationType.objects.get(name="Campus")
|
|
@@ -513,6 +525,25 @@ class AppCustomValidationTest(TestCase):
|
|
|
513
525
|
with self.assertRaises(ValidationError):
|
|
514
526
|
relationship_assoc.clean()
|
|
515
527
|
|
|
528
|
+
def test_custom_validator_non_web_request_uses_anonymous_user(self):
|
|
529
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
530
|
+
registry["plugin_custom_validators"]["dcim.locationtype"] = [TestUserContextCustomValidator]
|
|
531
|
+
|
|
532
|
+
from django.contrib.auth.models import AnonymousUser
|
|
533
|
+
|
|
534
|
+
with self.assertRaises(ValidationError) as context:
|
|
535
|
+
location_type.clean()
|
|
536
|
+
self.assertEqual(context.exception.message, AnonymousUser())
|
|
537
|
+
|
|
538
|
+
def test_custom_validator_web_request_uses_real_user(self):
|
|
539
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
540
|
+
registry["plugin_custom_validators"]["dcim.locationtype"] = [TestUserContextCustomValidator]
|
|
541
|
+
|
|
542
|
+
with self.assertRaises(ValidationError) as context:
|
|
543
|
+
with web_request_context(user=self.user):
|
|
544
|
+
location_type.clean()
|
|
545
|
+
self.assertEqual(context.exception.message, self.user)
|
|
546
|
+
|
|
516
547
|
|
|
517
548
|
class ExampleModelCustomActionViewTest(TestCase):
|
|
518
549
|
"""Test for custom action view `all_names` added to Example App"""
|
|
@@ -823,9 +823,12 @@ class DynamicGroupTestCase(
|
|
|
823
823
|
location_ct = ContentType.objects.get_for_model(Location)
|
|
824
824
|
instance = self._get_queryset().exclude(content_type=location_ct).first()
|
|
825
825
|
# Add view permissions for the group's members:
|
|
826
|
-
self.add_permissions(
|
|
826
|
+
self.add_permissions(
|
|
827
|
+
get_permission_for_model(instance.content_type.model_class(), "view"), "extras.view_dynamicgroup"
|
|
828
|
+
)
|
|
827
829
|
|
|
828
|
-
response =
|
|
830
|
+
response = self.client.get(instance.get_absolute_url())
|
|
831
|
+
self.assertHttpStatus(response, 200)
|
|
829
832
|
|
|
830
833
|
response_body = extract_page_body(response.content.decode(response.charset))
|
|
831
834
|
# Check that the "members" table in the detail view includes all appropriate member objects
|
|
@@ -1177,6 +1180,13 @@ class GitRepositoryTestCase(
|
|
|
1177
1180
|
self.form_data = form_data
|
|
1178
1181
|
super().test_edit_object_with_constrained_permission()
|
|
1179
1182
|
|
|
1183
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1184
|
+
def test_view_when_no_sync_job_result_exists(self):
|
|
1185
|
+
instance = self._get_queryset().first()
|
|
1186
|
+
response = self.client.get(reverse("extras:gitrepository_result", kwargs={"pk": instance.pk}))
|
|
1187
|
+
self.assertEqual(response.status_code, 200)
|
|
1188
|
+
self.assertEqual(response.context["result"], {})
|
|
1189
|
+
|
|
1180
1190
|
def test_post_sync_repo_anonymous(self):
|
|
1181
1191
|
self.client.logout()
|
|
1182
1192
|
url = reverse("extras:gitrepository_sync", kwargs={"pk": self._get_queryset().first().pk})
|
nautobot/extras/views.py
CHANGED
|
@@ -1165,6 +1165,9 @@ class GitRepositoryResultView(generic.ObjectView):
|
|
|
1165
1165
|
def get_extra_context(self, request, instance):
|
|
1166
1166
|
job_result = instance.get_latest_sync()
|
|
1167
1167
|
|
|
1168
|
+
if job_result is None:
|
|
1169
|
+
job_result = {}
|
|
1170
|
+
|
|
1168
1171
|
return {
|
|
1169
1172
|
"result": job_result,
|
|
1170
1173
|
"base_template": "extras/gitrepository.html",
|
nautobot/ipam/api/serializers.py
CHANGED
|
@@ -63,14 +63,13 @@ class VRFDeviceAssignmentSerializer(ValidatedModelSerializer):
|
|
|
63
63
|
validators = []
|
|
64
64
|
|
|
65
65
|
def validate(self, attrs):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
validator(attrs, self)
|
|
66
|
+
foreign_key_fields = ["device", "virtual_machine", "virtual_device_context"]
|
|
67
|
+
for foreign_key in foreign_key_fields:
|
|
68
|
+
if attrs.get(foreign_key):
|
|
69
|
+
validator = UniqueTogetherValidator(
|
|
70
|
+
queryset=VRFDeviceAssignment.objects.all(), fields=(foreign_key, "vrf")
|
|
71
|
+
)
|
|
72
|
+
validator(attrs, self)
|
|
74
73
|
return super().validate(attrs)
|
|
75
74
|
|
|
76
75
|
|
nautobot/ipam/api/views.py
CHANGED
|
@@ -55,13 +55,13 @@ class VRFViewSet(NautobotModelViewSet):
|
|
|
55
55
|
filterset_class = filters.VRFFilterSet
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
class VRFDeviceAssignmentViewSet(
|
|
58
|
+
class VRFDeviceAssignmentViewSet(ModelViewSet):
|
|
59
59
|
queryset = VRFDeviceAssignment.objects.all()
|
|
60
60
|
serializer_class = serializers.VRFDeviceAssignmentSerializer
|
|
61
61
|
filterset_class = filters.VRFDeviceAssignmentFilterSet
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
class VRFPrefixAssignmentViewSet(
|
|
64
|
+
class VRFPrefixAssignmentViewSet(ModelViewSet):
|
|
65
65
|
queryset = VRFPrefixAssignment.objects.all()
|
|
66
66
|
serializer_class = serializers.VRFPrefixAssignmentSerializer
|
|
67
67
|
filterset_class = filters.VRFPrefixAssignmentFilterSet
|
nautobot/ipam/factory.py
CHANGED
|
@@ -15,7 +15,7 @@ from nautobot.core.factory import (
|
|
|
15
15
|
random_instance,
|
|
16
16
|
UniqueFaker,
|
|
17
17
|
)
|
|
18
|
-
from nautobot.dcim.models import Location
|
|
18
|
+
from nautobot.dcim.models import Location, VirtualDeviceContext
|
|
19
19
|
from nautobot.extras.models import Role, Status
|
|
20
20
|
from nautobot.ipam.choices import PrefixTypeChoices
|
|
21
21
|
from nautobot.ipam.models import IPAddress, Namespace, Prefix, RIR, RouteTarget, VLAN, VLANGroup, VRF
|
|
@@ -127,6 +127,24 @@ class VRFFactory(PrimaryModelFactory):
|
|
|
127
127
|
else:
|
|
128
128
|
self.export_targets.set(get_random_instances(RouteTarget))
|
|
129
129
|
|
|
130
|
+
@factory.post_generation
|
|
131
|
+
def prefixes(self, create, extracted, **kwargs):
|
|
132
|
+
if create:
|
|
133
|
+
if extracted:
|
|
134
|
+
self.prefixes.set(extracted)
|
|
135
|
+
else:
|
|
136
|
+
self.prefixes.set(
|
|
137
|
+
get_random_instances(lambda: Prefix.objects.filter(namespace=self.namespace), minimum=0)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
@factory.post_generation
|
|
141
|
+
def virtual_device_contexts(self, create, extracted, **kwargs):
|
|
142
|
+
if create:
|
|
143
|
+
if extracted:
|
|
144
|
+
self.virtual_device_contexts.set(extracted)
|
|
145
|
+
else:
|
|
146
|
+
self.virtual_device_contexts.set(get_random_instances(VirtualDeviceContext))
|
|
147
|
+
|
|
130
148
|
|
|
131
149
|
class VLANGroupFactory(OrganizationalModelFactory):
|
|
132
150
|
class Meta:
|
|
@@ -295,7 +313,6 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
295
313
|
has_role = NautobotBoolIterator()
|
|
296
314
|
has_tenant = NautobotBoolIterator()
|
|
297
315
|
has_vlan = NautobotBoolIterator()
|
|
298
|
-
# has_vrf = NautobotBoolIterator()
|
|
299
316
|
is_ipv6 = NautobotBoolIterator()
|
|
300
317
|
|
|
301
318
|
prefix = factory.Maybe(
|
|
@@ -321,12 +338,6 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
321
338
|
None,
|
|
322
339
|
)
|
|
323
340
|
namespace = random_instance(Namespace, allow_null=False)
|
|
324
|
-
# TODO: Update for M2M tests
|
|
325
|
-
# vrf = factory.Maybe(
|
|
326
|
-
# "has_vrf",
|
|
327
|
-
# factory.SubFactory(VRFGetOrCreateFactory, tenant=factory.SelfAttribute("..tenant")),
|
|
328
|
-
# None,
|
|
329
|
-
# )
|
|
330
341
|
rir = factory.Maybe("has_rir", random_instance(RIR, allow_null=False), None)
|
|
331
342
|
date_allocated = factory.Maybe("has_date_allocated", factory.Faker("date_time", tzinfo=datetime.timezone.utc), None)
|
|
332
343
|
|
|
@@ -343,6 +354,14 @@ class PrefixFactory(PrimaryModelFactory):
|
|
|
343
354
|
)
|
|
344
355
|
)
|
|
345
356
|
|
|
357
|
+
@factory.post_generation
|
|
358
|
+
def vrfs(self, create, extracted, **kwargs):
|
|
359
|
+
if create:
|
|
360
|
+
if extracted:
|
|
361
|
+
self.vrfs.set(extracted)
|
|
362
|
+
else:
|
|
363
|
+
self.vrfs.set(get_random_instances(lambda: VRF.objects.filter(namespace=self.namespace), minimum=0))
|
|
364
|
+
|
|
346
365
|
@factory.post_generation
|
|
347
366
|
def children(self, create, extracted, **kwargs):
|
|
348
367
|
"""Creates child prefixes and ip addresses within the prefix IP space.
|
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 = [
|
|
@@ -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
|
+
]
|