nautobot 2.4.2__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/templates/circuits/inc/circuit_termination.html +1 -1
- nautobot/circuits/tests/integration/test_circuit.py +135 -0
- nautobot/circuits/tests/test_models.py +5 -3
- nautobot/circuits/views.py +4 -1
- nautobot/cloud/api/views.py +3 -3
- nautobot/cloud/filters.py +3 -6
- nautobot/cloud/tests/test_filters.py +21 -0
- nautobot/core/admin.py +2 -0
- nautobot/core/constants.py +0 -1
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/forms.py +2 -1
- nautobot/core/forms/widgets.py +8 -0
- nautobot/core/jobs/__init__.py +2 -1
- nautobot/core/management/commands/generate_performance_test_endpoints.py +271 -0
- nautobot/core/models/utils.py +6 -1
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_edit.html +1 -1
- nautobot/core/templates/generic/object_bulk_import.html +1 -1
- nautobot/core/templates/generic/object_create.html +5 -0
- nautobot/core/templates/generic/object_delete.html +1 -1
- nautobot/core/templates/generic/object_detail.html +1 -1
- nautobot/core/templates/generic/object_edit.html +1 -1
- nautobot/core/templates/inc/javascript.html +3 -0
- nautobot/core/templates/widgets/clearable_file.html +5 -0
- nautobot/core/templatetags/helpers.py +3 -3
- nautobot/core/templatetags/ui_framework.py +20 -4
- nautobot/core/testing/forms.py +1 -1
- nautobot/core/testing/integration.py +37 -7
- nautobot/core/tests/test_api.py +1 -1
- nautobot/core/tests/test_commands.py +31 -0
- nautobot/core/tests/test_graphql.py +3 -3
- nautobot/core/tests/test_jobs.py +4 -1
- nautobot/core/tests/test_utils.py +17 -2
- nautobot/core/ui/object_detail.py +1 -1
- nautobot/core/utils/lookup.py +12 -1
- nautobot/core/views/generic.py +9 -1
- nautobot/core/views/mixins.py +9 -1
- nautobot/dcim/api/serializers.py +36 -0
- nautobot/dcim/api/views.py +12 -11
- nautobot/dcim/elevations.py +17 -4
- nautobot/dcim/factory.py +9 -1
- nautobot/dcim/filters/__init__.py +27 -1
- nautobot/dcim/forms.py +16 -7
- nautobot/dcim/models/devices.py +12 -7
- nautobot/dcim/signals.py +26 -0
- nautobot/dcim/templates/dcim/cable_trace.html +4 -4
- nautobot/dcim/templates/dcim/consoleport.html +14 -4
- nautobot/dcim/templates/dcim/consoleserverport.html +14 -4
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +3 -3
- nautobot/dcim/templates/dcim/frontport.html +7 -2
- nautobot/dcim/templates/dcim/interface.html +9 -4
- nautobot/dcim/templates/dcim/powerfeed.html +8 -3
- nautobot/dcim/templates/dcim/poweroutlet.html +14 -4
- nautobot/dcim/templates/dcim/powerport.html +14 -4
- nautobot/dcim/templates/dcim/rearport.html +7 -2
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -62
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +6 -0
- nautobot/dcim/tests/integration/test_fileinputpicker.py +87 -0
- nautobot/dcim/tests/test_api.py +176 -0
- nautobot/dcim/tests/test_filters.py +56 -3
- nautobot/dcim/tests/test_models.py +41 -1
- nautobot/dcim/views.py +24 -14
- nautobot/extras/api/mixins.py +1 -1
- nautobot/extras/api/views.py +4 -4
- nautobot/extras/filters/__init__.py +4 -0
- nautobot/extras/forms/forms.py +4 -0
- nautobot/extras/jobs.py +8 -1
- 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/job.html +1 -0
- nautobot/extras/templates/extras/objectchange.html +28 -12
- nautobot/extras/tests/test_api.py +16 -15
- nautobot/extras/tests/test_dynamicgroups.py +14 -0
- nautobot/extras/tests/test_filters.py +2 -0
- nautobot/extras/tests/test_plugins.py +32 -1
- nautobot/extras/tests/test_views.py +209 -11
- nautobot/extras/utils.py +30 -0
- nautobot/extras/views.py +32 -14
- nautobot/ipam/api/serializers.py +7 -8
- nautobot/ipam/api/views.py +5 -5
- nautobot/ipam/factory.py +27 -8
- nautobot/ipam/filters.py +67 -29
- nautobot/ipam/formfields.py +51 -0
- nautobot/ipam/forms.py +15 -7
- 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/bootstrap-filestyle-1.2.3/bootstrap-filestyle.min.js +11 -0
- nautobot/project-static/css/base.css +11 -0
- nautobot/project-static/css/dark.css +2 -1
- nautobot/project-static/docs/apps/index.html +1 -1
- nautobot/project-static/docs/apps/nautobot-apps.html +1 -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 +9 -13
- 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 +2 -5
- 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/setup.html +1 -1
- 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 +7 -6
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
- nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
- nautobot/project-static/docs/development/core/application-registry.html +0 -6
- nautobot/project-static/docs/development/core/best-practices.html +1 -28
- nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +65 -11
- nautobot/project-static/docs/development/core/getting-started.html +14 -18
- nautobot/project-static/docs/development/core/homepage.html +0 -3
- nautobot/project-static/docs/development/core/index.html +1 -1
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +3 -3
- nautobot/project-static/docs/development/core/model-checklist.html +1 -1
- nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/settings.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +4 -9
- 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 +11 -30
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -2
- nautobot/project-static/docs/index.html +3 -2
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +2 -20
- nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.4.html +17 -17
- nautobot/project-static/docs/release-notes/version-1.5.html +8 -8
- nautobot/project-static/docs/release-notes/version-1.6.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.0.html +10 -10
- nautobot/project-static/docs/release-notes/version-2.1.html +7 -7
- nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.3.html +4 -4
- nautobot/project-static/docs/release-notes/version-2.4.html +379 -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/authentication/ldap.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +4 -4
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +3 -13
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +5 -5
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -18
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +4 -4
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +15 -15
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +0 -16
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +7 -10
- nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -12
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/security/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/security/notices.html +1 -0
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +5 -35
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
- 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 +12 -9
- 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 +15 -15
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- 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 +1 -27
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +0 -8
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -6
- 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/jobqueue.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +6 -6
- 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/dropdown.js +28 -0
- 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/forms.py +9 -0
- nautobot/tenancy/templates/tenancy/tenant_create.html +21 -0
- nautobot/tenancy/templates/tenancy/tenant_edit.html +2 -21
- nautobot/tenancy/templates/tenancy/tenantgroup.html +2 -44
- nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +1 -0
- nautobot/tenancy/tests/test_filters.py +10 -0
- nautobot/tenancy/tests/test_views.py +5 -1
- nautobot/tenancy/urls.py +7 -79
- nautobot/tenancy/views.py +51 -80
- nautobot/virtualization/views.py +0 -1
- nautobot/wireless/api/serializers.py +6 -1
- nautobot/wireless/api/views.py +3 -3
- nautobot/wireless/tables.py +9 -4
- nautobot/wireless/tests/test_api.py +5 -9
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/METADATA +9 -9
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/RECORD +267 -246
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/NOTICE +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/WHEEL +0 -0
- {nautobot-2.4.2.dist-info → nautobot-2.4.4.dist-info}/entry_points.txt +0 -0
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|
|
@@ -57,8 +62,13 @@
|
|
|
57
62
|
</tr>
|
|
58
63
|
{% if object.connected_endpoint %}
|
|
59
64
|
<tr>
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
{% if object.connected_endpoint.device %}
|
|
66
|
+
<td>Device</td>
|
|
67
|
+
<td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
|
|
68
|
+
{% else %}
|
|
69
|
+
<td>Module</td>
|
|
70
|
+
<td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
|
|
71
|
+
{% endif %}
|
|
62
72
|
</tr>
|
|
63
73
|
<tr>
|
|
64
74
|
<td>Power Port</td>
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|
|
@@ -57,8 +62,13 @@
|
|
|
57
62
|
</tr>
|
|
58
63
|
{% if object.connected_endpoint %}
|
|
59
64
|
<tr>
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
{% if object.connected_endpoint.device %}
|
|
66
|
+
<td>Device</td>
|
|
67
|
+
<td>{{ object.connected_endpoint.device|hyperlinked_object }}</td>
|
|
68
|
+
{% else %}
|
|
69
|
+
<td>Module</td>
|
|
70
|
+
<td>{{ object.connected_endpoint.module|hyperlinked_object }}</td>
|
|
71
|
+
{% endif %}
|
|
62
72
|
</tr>
|
|
63
73
|
<tr>
|
|
64
74
|
<td>Power Outlet / Feed</td>
|
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
</div>
|
|
9
9
|
<table class="table table-hover panel-body attr-table">
|
|
10
10
|
<tr>
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
{% if object.device %}
|
|
12
|
+
<td>Device</td>
|
|
13
|
+
<td>{{ object.device|hyperlinked_object }}</td>
|
|
14
|
+
{% else %}
|
|
15
|
+
<td>Module</td>
|
|
16
|
+
<td>{{ object.module|hyperlinked_object }}</td>
|
|
17
|
+
{% endif %}
|
|
13
18
|
</tr>
|
|
14
19
|
<tr>
|
|
15
20
|
<td>Name</td>
|
|
@@ -4,65 +4,3 @@
|
|
|
4
4
|
{% block extra_breadcrumbs %}
|
|
5
5
|
<li><a href="{% url 'dcim:device' pk=object.device.pk %}">{{ object.device }}</a></li>
|
|
6
6
|
{% endblock extra_breadcrumbs %}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
{% block content_left_page %}
|
|
10
|
-
<div class="panel panel-default">
|
|
11
|
-
<div class="panel-heading">
|
|
12
|
-
<strong>Virtual Device Context</strong>
|
|
13
|
-
</div>
|
|
14
|
-
<table class="table table-hover panel-body attr-table">
|
|
15
|
-
<tr>
|
|
16
|
-
<td>Name</td>
|
|
17
|
-
<td>
|
|
18
|
-
{{ object.name }}
|
|
19
|
-
</td>
|
|
20
|
-
</tr>
|
|
21
|
-
<tr>
|
|
22
|
-
<td>Identifier</td>
|
|
23
|
-
<td>
|
|
24
|
-
{{ object.identifier }}
|
|
25
|
-
</td>
|
|
26
|
-
</tr>
|
|
27
|
-
<tr>
|
|
28
|
-
<td>Role</td>
|
|
29
|
-
<td>
|
|
30
|
-
{{ object.role| hyperlinked_object_with_color }}
|
|
31
|
-
</td>
|
|
32
|
-
</tr>
|
|
33
|
-
<tr>
|
|
34
|
-
<td>Status</td>
|
|
35
|
-
<td>
|
|
36
|
-
{{ object.status| hyperlinked_object_with_color }}
|
|
37
|
-
</td>
|
|
38
|
-
</tr>
|
|
39
|
-
<tr>
|
|
40
|
-
<td>Device</td>
|
|
41
|
-
<td>
|
|
42
|
-
{{ object.device|hyperlinked_object }}
|
|
43
|
-
</td>
|
|
44
|
-
</tr>
|
|
45
|
-
<tr>
|
|
46
|
-
<td>Primary IPv4</td>
|
|
47
|
-
<td>
|
|
48
|
-
{{ object.primary_ip4|hyperlinked_object }}
|
|
49
|
-
</td>
|
|
50
|
-
</tr>
|
|
51
|
-
<tr>
|
|
52
|
-
<td>Primary IPv6</td>
|
|
53
|
-
<td>
|
|
54
|
-
{{ object.primary_ip6|hyperlinked_object }}
|
|
55
|
-
</td>
|
|
56
|
-
</tr>
|
|
57
|
-
{% include 'inc/tenant_table_row.html' %}
|
|
58
|
-
<tr>
|
|
59
|
-
<td>Description</td>
|
|
60
|
-
<td>{{ object.description|placeholder }}</td>
|
|
61
|
-
</tr>
|
|
62
|
-
</table>
|
|
63
|
-
</div>
|
|
64
|
-
{% endblock content_left_page %}
|
|
65
|
-
|
|
66
|
-
{% block content_full_width_page %}
|
|
67
|
-
{% include 'panel_table.html' with table=interfaces_table heading="Interfaces" %}
|
|
68
|
-
{% endblock content_full_width_page %}
|
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
{% endif %}
|
|
24
24
|
</div>
|
|
25
25
|
</div>
|
|
26
|
+
<div class="panel panel-default">
|
|
27
|
+
<div class="panel-heading"><strong>VRF Assignments</strong></div>
|
|
28
|
+
<div class="panel-body">
|
|
29
|
+
{% render_field form.vrfs %}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
26
32
|
{% include 'inc/tenancy_form_panel.html' %}
|
|
27
33
|
{% include 'inc/extras_features_edit_form_fields.html' %}
|
|
28
34
|
{% endblock %}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from django.urls import reverse
|
|
2
|
+
|
|
3
|
+
from nautobot.core.testing.integration import SeleniumTestCase, WebDriverWait
|
|
4
|
+
from nautobot.dcim.models import Location, LocationType
|
|
5
|
+
from nautobot.extras.models import Job, Status
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ClearableFileInputTestCase(SeleniumTestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
super().setUp()
|
|
11
|
+
self.user.is_superuser = True
|
|
12
|
+
self.user.save()
|
|
13
|
+
self.login(self.user.username, self.password)
|
|
14
|
+
|
|
15
|
+
def tearDown(self):
|
|
16
|
+
self.logout()
|
|
17
|
+
super().tearDown()
|
|
18
|
+
|
|
19
|
+
def _assert_file_picker(self, uri_to_visit: str, page_loaded_confirmation: str, file_input_selector_id: str):
|
|
20
|
+
"""
|
|
21
|
+
Ensure clearable input file type has working clear and info display.
|
|
22
|
+
"""
|
|
23
|
+
self.browser.visit(f"{self.live_server_url}{uri_to_visit}")
|
|
24
|
+
WebDriverWait(self.browser, 10).until(lambda driver: driver.is_text_present(page_loaded_confirmation))
|
|
25
|
+
|
|
26
|
+
# Find the first file input button and scroll to it
|
|
27
|
+
front_image_button = self.browser.find_by_css("span.group-span-filestyle.input-group-btn").first
|
|
28
|
+
front_image_button.scroll_to()
|
|
29
|
+
|
|
30
|
+
# cancel button is NOT visible initially
|
|
31
|
+
self.assertFalse(self.browser.find_by_css("button.clear-button").first.visible)
|
|
32
|
+
|
|
33
|
+
# Test file text changes after selecting a file
|
|
34
|
+
file_selection_indicator_css = "div.bootstrap-filestyle input[type='text'].form-control"
|
|
35
|
+
self.assertEqual(self.browser.find_by_css(file_selection_indicator_css).first.value, "")
|
|
36
|
+
front_image_file_input = self.browser.find_by_id(file_input_selector_id).first
|
|
37
|
+
front_image_file_input.value = "/dev/null"
|
|
38
|
+
self.assertEqual(self.browser.find_by_css(file_selection_indicator_css).first.value, "null")
|
|
39
|
+
|
|
40
|
+
# clear button is now visible
|
|
41
|
+
clear_button = self.browser.find_by_css("button.clear-button").first
|
|
42
|
+
self.assertTrue(clear_button.visible)
|
|
43
|
+
|
|
44
|
+
# clicking clearbutton should hide the button, and wipe the file input value
|
|
45
|
+
clear_button.click()
|
|
46
|
+
self.assertFalse(clear_button.visible)
|
|
47
|
+
self.assertEqual(front_image_file_input.value, "")
|
|
48
|
+
|
|
49
|
+
def test_add_device_page(self):
|
|
50
|
+
"""
|
|
51
|
+
Confirm device type add page input is working correctly.
|
|
52
|
+
"""
|
|
53
|
+
self._assert_file_picker(
|
|
54
|
+
uri_to_visit=reverse("dcim:devicetype_add"),
|
|
55
|
+
page_loaded_confirmation="Device Type",
|
|
56
|
+
file_input_selector_id="id_front_image",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def test_job_runner_page(self):
|
|
60
|
+
"""
|
|
61
|
+
Confirm job run page file input is working correctly.
|
|
62
|
+
"""
|
|
63
|
+
example_job = Job.objects.get(name="Example File Input/Output job").pk
|
|
64
|
+
job_example_file_uri = reverse("extras:job_run", kwargs={"pk": example_job})
|
|
65
|
+
self._assert_file_picker(
|
|
66
|
+
uri_to_visit=job_example_file_uri,
|
|
67
|
+
page_loaded_confirmation="Example File",
|
|
68
|
+
file_input_selector_id="id_input_file",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def test_location_image_attachment_view(self):
|
|
72
|
+
"""
|
|
73
|
+
Confirm location image attachment page is working correctly.
|
|
74
|
+
"""
|
|
75
|
+
location_type, _ = LocationType.objects.get_or_create(name="Campus")
|
|
76
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
77
|
+
location, _ = Location.objects.get_or_create(
|
|
78
|
+
name="Test Location 1", location_type=location_type, status=location_status
|
|
79
|
+
)
|
|
80
|
+
location_image_attach_uri = reverse(
|
|
81
|
+
"dcim:location_add_image", kwargs={"object_id": location.id, "model": Location}
|
|
82
|
+
)
|
|
83
|
+
self._assert_file_picker(
|
|
84
|
+
uri_to_visit=location_image_attach_uri,
|
|
85
|
+
page_loaded_confirmation="Image attachment",
|
|
86
|
+
file_input_selector_id="id_image",
|
|
87
|
+
)
|
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -1716,6 +1716,182 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
|
1716
1716
|
)
|
|
1717
1717
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1718
1718
|
|
|
1719
|
+
def _parent_device_test_data(self):
|
|
1720
|
+
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
1721
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
1722
|
+
device_role = Role.objects.get_for_model(Device).first()
|
|
1723
|
+
device_type = DeviceType.objects.first()
|
|
1724
|
+
|
|
1725
|
+
device_type_parent = DeviceType.objects.create(
|
|
1726
|
+
manufacturer=device_type.manufacturer,
|
|
1727
|
+
model=f"{device_type.model} Parent",
|
|
1728
|
+
u_height=device_type.u_height,
|
|
1729
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_PARENT,
|
|
1730
|
+
)
|
|
1731
|
+
device_type_child = DeviceType.objects.create(
|
|
1732
|
+
manufacturer=device_type.manufacturer,
|
|
1733
|
+
model=f"{device_type.model} Child",
|
|
1734
|
+
u_height=device_type.u_height,
|
|
1735
|
+
subdevice_role=SubdeviceRoleChoices.ROLE_CHILD,
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
parent_device = Device.objects.create(
|
|
1739
|
+
device_type=device_type_parent,
|
|
1740
|
+
role=device_role,
|
|
1741
|
+
status=device_status,
|
|
1742
|
+
name="Device Parent",
|
|
1743
|
+
location=location,
|
|
1744
|
+
)
|
|
1745
|
+
device_bay_1 = DeviceBay.objects.create(name="db1", device_id=parent_device.pk)
|
|
1746
|
+
device_bay_2 = DeviceBay.objects.create(name="db2", device_id=parent_device.pk)
|
|
1747
|
+
|
|
1748
|
+
return parent_device, device_bay_1, device_bay_2, device_type_child
|
|
1749
|
+
|
|
1750
|
+
def test_creating_device_with_parent_bay(self):
|
|
1751
|
+
# Create test data
|
|
1752
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1753
|
+
|
|
1754
|
+
self.add_permissions("dcim.add_device")
|
|
1755
|
+
url = reverse("dcim-api:device-list")
|
|
1756
|
+
|
|
1757
|
+
# Test creating device with parent bay by device bay data
|
|
1758
|
+
data = {
|
|
1759
|
+
"device_type": device_type_child.pk,
|
|
1760
|
+
"role": parent_device.role.pk,
|
|
1761
|
+
"location": parent_device.location.pk,
|
|
1762
|
+
"name": "Device parent bay test #1",
|
|
1763
|
+
"status": parent_device.status.pk,
|
|
1764
|
+
"parent_bay": {"device": {"name": parent_device.name}, "name": device_bay_1.name},
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1768
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1769
|
+
|
|
1770
|
+
created_device = Device.objects.get(name="Device parent bay test #1")
|
|
1771
|
+
self.assertEqual(created_device.parent_bay.pk, device_bay_1.pk)
|
|
1772
|
+
|
|
1773
|
+
# Test creating device with parent bay by device_bay.pk
|
|
1774
|
+
data = {
|
|
1775
|
+
"device_type": device_type_child.pk,
|
|
1776
|
+
"role": parent_device.role.pk,
|
|
1777
|
+
"location": parent_device.location.pk,
|
|
1778
|
+
"name": "Device parent bay test #2",
|
|
1779
|
+
"status": parent_device.status.pk,
|
|
1780
|
+
"parent_bay": device_bay_2.pk,
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1784
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1785
|
+
|
|
1786
|
+
created_device = Device.objects.get(name="Device parent bay test #2")
|
|
1787
|
+
self.assertEqual(created_device.parent_bay.pk, device_bay_2.pk)
|
|
1788
|
+
|
|
1789
|
+
# Test creating device with parent bay already taken
|
|
1790
|
+
data = {
|
|
1791
|
+
"device_type": device_type_child.pk,
|
|
1792
|
+
"role": parent_device.role.pk,
|
|
1793
|
+
"location": parent_device.location.pk,
|
|
1794
|
+
"name": "Device parent bay test #3",
|
|
1795
|
+
"status": parent_device.status.pk,
|
|
1796
|
+
"parent_bay": device_bay_1.pk,
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1800
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1801
|
+
self.assertIn("Cannot install device; parent bay is already taken", response.content.decode(response.charset))
|
|
1802
|
+
|
|
1803
|
+
# Assert that on the #1 device, parent bay stayed the same
|
|
1804
|
+
old_device = Device.objects.get(name="Device parent bay test #1")
|
|
1805
|
+
self.assertEqual(old_device.parent_bay.pk, device_bay_1.pk)
|
|
1806
|
+
|
|
1807
|
+
def test_update_device_with_parent_bay(self):
|
|
1808
|
+
# Create test data
|
|
1809
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1810
|
+
|
|
1811
|
+
self.add_permissions("dcim.change_device")
|
|
1812
|
+
|
|
1813
|
+
child_device = Device.objects.create(
|
|
1814
|
+
device_type=device_type_child,
|
|
1815
|
+
role=parent_device.role,
|
|
1816
|
+
location=parent_device.location,
|
|
1817
|
+
name="Device parent bay test #4",
|
|
1818
|
+
status=parent_device.status,
|
|
1819
|
+
)
|
|
1820
|
+
# Test setting parent bay during the update
|
|
1821
|
+
patch_data = {"parent_bay": device_bay_1.pk}
|
|
1822
|
+
response = self.client.patch(self._get_detail_url(child_device), patch_data, format="json", **self.header)
|
|
1823
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1824
|
+
|
|
1825
|
+
updated_device = Device.objects.get(name="Device parent bay test #4")
|
|
1826
|
+
self.assertEqual(updated_device.parent_bay.pk, device_bay_1.pk)
|
|
1827
|
+
|
|
1828
|
+
# Changing the parent bay is not allowed without removing it first
|
|
1829
|
+
patch_data = {"parent_bay": device_bay_2.pk}
|
|
1830
|
+
response = self.client.patch(self._get_detail_url(child_device), patch_data, format="json", **self.header)
|
|
1831
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1832
|
+
self.assertIn(
|
|
1833
|
+
f"Cannot install the specified device; device is already installed in {device_bay_1.name}",
|
|
1834
|
+
response.content.decode(response.charset),
|
|
1835
|
+
)
|
|
1836
|
+
|
|
1837
|
+
# Assert that parent bay stayed the same
|
|
1838
|
+
updated_device = Device.objects.get(name="Device parent bay test #4")
|
|
1839
|
+
self.assertEqual(updated_device.parent_bay.pk, device_bay_1.pk)
|
|
1840
|
+
|
|
1841
|
+
def test_reassign_device_to_parent_bay(self):
|
|
1842
|
+
# Create test data
|
|
1843
|
+
parent_device, device_bay_1, device_bay_2, device_type_child = self._parent_device_test_data()
|
|
1844
|
+
device_name = "Device parent bay test #5"
|
|
1845
|
+
child_device = Device.objects.create(
|
|
1846
|
+
device_type=device_type_child,
|
|
1847
|
+
role=parent_device.role,
|
|
1848
|
+
location=parent_device.location,
|
|
1849
|
+
name=device_name,
|
|
1850
|
+
status=parent_device.status,
|
|
1851
|
+
)
|
|
1852
|
+
device_bay_1.installed_device = child_device
|
|
1853
|
+
device_bay_1.save()
|
|
1854
|
+
|
|
1855
|
+
self.add_permissions("dcim.change_device", "dcim.view_device", "dcim.change_devicebay")
|
|
1856
|
+
child_device_detail_url = self._get_detail_url(child_device)
|
|
1857
|
+
|
|
1858
|
+
response = self.client.get(child_device_detail_url, **self.header)
|
|
1859
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1860
|
+
self.assertEqual(response.json()["parent_bay"]["id"], str(device_bay_1.pk))
|
|
1861
|
+
|
|
1862
|
+
# Test unassigning parent bay
|
|
1863
|
+
patch_data = {"parent_bay": None}
|
|
1864
|
+
response = self.client.patch(child_device_detail_url, patch_data, format="json", **self.header)
|
|
1865
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1866
|
+
|
|
1867
|
+
child_device.refresh_from_db()
|
|
1868
|
+
with self.assertRaises(DeviceBay.DoesNotExist):
|
|
1869
|
+
child_device.parent_bay
|
|
1870
|
+
|
|
1871
|
+
# And assign it again
|
|
1872
|
+
patch_data = {"parent_bay": device_bay_2.pk}
|
|
1873
|
+
response = self.client.patch(child_device_detail_url, patch_data, format="json", **self.header)
|
|
1874
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1875
|
+
|
|
1876
|
+
child_device.refresh_from_db()
|
|
1877
|
+
self.assertEqual(child_device.parent_bay.pk, device_bay_2.pk)
|
|
1878
|
+
|
|
1879
|
+
# Unassign it through device bay
|
|
1880
|
+
patch_data = {"installed_device": None}
|
|
1881
|
+
response = self.client.patch(self._get_detail_url(device_bay_2), patch_data, format="json", **self.header)
|
|
1882
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1883
|
+
|
|
1884
|
+
child_device.refresh_from_db()
|
|
1885
|
+
self.assertFalse(hasattr(child_device, "parent_bay"))
|
|
1886
|
+
|
|
1887
|
+
# And assign through device bay
|
|
1888
|
+
patch_data = {"installed_device": child_device.pk}
|
|
1889
|
+
response = self.client.patch(self._get_detail_url(device_bay_1), patch_data, format="json", **self.header)
|
|
1890
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
1891
|
+
|
|
1892
|
+
child_device.refresh_from_db()
|
|
1893
|
+
self.assertEqual(child_device.parent_bay.pk, device_bay_1.pk)
|
|
1894
|
+
|
|
1719
1895
|
|
|
1720
1896
|
class ModuleTestCase(APIViewTestCases.APIViewTestCase):
|
|
1721
1897
|
model = Module
|
|
@@ -157,6 +157,8 @@ def common_test_data(cls):
|
|
|
157
157
|
cls.loc0 = loc0
|
|
158
158
|
cls.loc1 = loc1
|
|
159
159
|
cls.nested_loc = nested_loc
|
|
160
|
+
cls.loc2 = loc2
|
|
161
|
+
cls.loc3 = loc3
|
|
160
162
|
|
|
161
163
|
provider = Provider.objects.first()
|
|
162
164
|
circuit_type = CircuitType.objects.first()
|
|
@@ -1150,6 +1152,24 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase):
|
|
|
1150
1152
|
name="Rack Group 4",
|
|
1151
1153
|
location=cls.loc1,
|
|
1152
1154
|
)
|
|
1155
|
+
RackGroup.objects.create(
|
|
1156
|
+
name="Rack Group 5",
|
|
1157
|
+
location=cls.loc2,
|
|
1158
|
+
description="C",
|
|
1159
|
+
)
|
|
1160
|
+
RackGroup.objects.create(
|
|
1161
|
+
name="Rack Group 6",
|
|
1162
|
+
location=cls.loc2,
|
|
1163
|
+
)
|
|
1164
|
+
RackGroup.objects.create(
|
|
1165
|
+
name="Rack Group 7",
|
|
1166
|
+
location=cls.loc3,
|
|
1167
|
+
description="C",
|
|
1168
|
+
)
|
|
1169
|
+
RackGroup.objects.create(
|
|
1170
|
+
name="Rack Group 8",
|
|
1171
|
+
location=cls.loc3,
|
|
1172
|
+
)
|
|
1153
1173
|
|
|
1154
1174
|
def test_children(self):
|
|
1155
1175
|
child_groups = RackGroup.objects.filter(name__startswith="Child").filter(parent__isnull=False)[:2]
|
|
@@ -1161,6 +1181,24 @@ class RackGroupTestCase(FilterTestCases.FilterTestCase):
|
|
|
1161
1181
|
params = {"children": [rack_group_4.pk, rack_group_4.pk]}
|
|
1162
1182
|
self.assertFalse(self.filterset(params, self.queryset).qs.exists())
|
|
1163
1183
|
|
|
1184
|
+
def test_ancestors(self):
|
|
1185
|
+
with self.subTest():
|
|
1186
|
+
pk_list = []
|
|
1187
|
+
parent_locations = self.loc3.ancestors(include_self=True)
|
|
1188
|
+
pk_list.extend([v.pk for v in parent_locations])
|
|
1189
|
+
params = Q(location__pk__in=pk_list)
|
|
1190
|
+
expected_queryset = RackGroup.objects.filter(params)
|
|
1191
|
+
params = {"ancestors": [self.loc3.pk]}
|
|
1192
|
+
self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
|
|
1193
|
+
with self.subTest():
|
|
1194
|
+
pk_list = []
|
|
1195
|
+
parent_locations = self.loc2.ancestors(include_self=True)
|
|
1196
|
+
pk_list.extend([v.pk for v in parent_locations])
|
|
1197
|
+
params = Q(location__pk__in=pk_list)
|
|
1198
|
+
expected_queryset = RackGroup.objects.filter(params)
|
|
1199
|
+
params = {"ancestors": [self.loc2.pk]}
|
|
1200
|
+
self.assertQuerysetEqualAndNotEmpty(self.filterset(params, self.queryset).qs, expected_queryset)
|
|
1201
|
+
|
|
1164
1202
|
|
|
1165
1203
|
class RackTestCase(FilterTestCases.FilterTestCase, FilterTestCases.TenancyFilterTestCaseMixin):
|
|
1166
1204
|
queryset = Rack.objects.all()
|
|
@@ -4188,12 +4226,13 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4188
4226
|
|
|
4189
4227
|
@classmethod
|
|
4190
4228
|
def setUpTestData(cls):
|
|
4191
|
-
|
|
4229
|
+
device_1 = Device.objects.first()
|
|
4230
|
+
device_2 = Device.objects.last()
|
|
4192
4231
|
vdc_status = Status.objects.get_for_model(VirtualDeviceContext)[0]
|
|
4193
4232
|
interface_status = Status.objects.get_for_model(Interface)[0]
|
|
4194
4233
|
interfaces = [
|
|
4195
4234
|
Interface.objects.create(
|
|
4196
|
-
device=
|
|
4235
|
+
device=device_1,
|
|
4197
4236
|
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
4198
4237
|
name=f"Interface 00{idx}",
|
|
4199
4238
|
status=interface_status,
|
|
@@ -4202,7 +4241,7 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4202
4241
|
]
|
|
4203
4242
|
vdcs = [
|
|
4204
4243
|
VirtualDeviceContext.objects.create(
|
|
4205
|
-
device=
|
|
4244
|
+
device=device_1,
|
|
4206
4245
|
status=vdc_status,
|
|
4207
4246
|
identifier=200 + idx,
|
|
4208
4247
|
name=f"Test VDC {idx}",
|
|
@@ -4213,3 +4252,17 @@ class InterfaceVDCAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
4213
4252
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[1], interface=interfaces[0])
|
|
4214
4253
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[1], interface=interfaces[1])
|
|
4215
4254
|
InterfaceVDCAssignment.objects.create(virtual_device_context=vdcs[2], interface=interfaces[2])
|
|
4255
|
+
InterfaceVDCAssignment.objects.create(
|
|
4256
|
+
virtual_device_context=VirtualDeviceContext.objects.create(
|
|
4257
|
+
device=device_2,
|
|
4258
|
+
status=vdc_status,
|
|
4259
|
+
identifier=200,
|
|
4260
|
+
name="Test VDC 0",
|
|
4261
|
+
),
|
|
4262
|
+
interface=Interface.objects.create(
|
|
4263
|
+
device=device_2,
|
|
4264
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
4265
|
+
name="Interface 000",
|
|
4266
|
+
status=interface_status,
|
|
4267
|
+
),
|
|
4268
|
+
)
|
|
@@ -1166,6 +1166,19 @@ class LocationTypeTestCase(TestCase):
|
|
|
1166
1166
|
child_loc.delete()
|
|
1167
1167
|
child.validated_save()
|
|
1168
1168
|
|
|
1169
|
+
def test_removing_content_type(self):
|
|
1170
|
+
"""Validation check to prevent removing an in-use content type from a LocationType."""
|
|
1171
|
+
|
|
1172
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
1173
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
1174
|
+
|
|
1175
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1176
|
+
location_type.content_types.remove(device_ct)
|
|
1177
|
+
self.assertIn(
|
|
1178
|
+
f"Cannot remove the content type {device_ct} as currently at least one device is associated to a location",
|
|
1179
|
+
str(cm.exception),
|
|
1180
|
+
)
|
|
1181
|
+
|
|
1169
1182
|
|
|
1170
1183
|
class LocationTestCase(ModelTestCases.BaseModelTestCase):
|
|
1171
1184
|
model = Location
|
|
@@ -1895,6 +1908,33 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
1895
1908
|
parent_device.rack = rack
|
|
1896
1909
|
parent_device.save()
|
|
1897
1910
|
|
|
1911
|
+
# Test assigning a rack in the child location of the parent device location
|
|
1912
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
1913
|
+
child_location = Location.objects.create(
|
|
1914
|
+
name="Child Location 1",
|
|
1915
|
+
location_type=self.location_type_3,
|
|
1916
|
+
status=location_status,
|
|
1917
|
+
parent=parent_device.location,
|
|
1918
|
+
)
|
|
1919
|
+
child_rack = Rack.objects.create(name="Rack 2", location=child_location, status=self.device_status)
|
|
1920
|
+
parent_device.rack = child_rack
|
|
1921
|
+
parent_device.validated_save()
|
|
1922
|
+
|
|
1923
|
+
# Test assigning a rack outside the child locations of the parent device location
|
|
1924
|
+
new_location = Location.objects.create(
|
|
1925
|
+
name="New Location 1",
|
|
1926
|
+
status=location_status,
|
|
1927
|
+
location_type=self.location_type_3,
|
|
1928
|
+
)
|
|
1929
|
+
invalid_rack = Rack.objects.create(name="Rack 3", location=new_location, status=self.device_status)
|
|
1930
|
+
parent_device.rack = invalid_rack
|
|
1931
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1932
|
+
parent_device.validated_save()
|
|
1933
|
+
self.assertIn(
|
|
1934
|
+
f'Rack "{invalid_rack}" does not belong to location "{parent_device.location}" and its descendants.',
|
|
1935
|
+
str(cm.exception),
|
|
1936
|
+
)
|
|
1937
|
+
|
|
1898
1938
|
child_mtime_after_parent_rack_update_save = str(Device.objects.get(name="Child Device 1").last_updated)
|
|
1899
1939
|
|
|
1900
1940
|
self.assertNotEqual(child_mtime_after_parent_noop_save, child_mtime_after_parent_rack_update_save)
|
|
@@ -2790,7 +2830,7 @@ class ControllerTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
2790
2830
|
controller.validated_save()
|
|
2791
2831
|
self.assertEqual(
|
|
2792
2832
|
error.exception.message_dict["location"][0],
|
|
2793
|
-
f'
|
|
2833
|
+
f'Controllers may not associate to locations of type "{location_type}".',
|
|
2794
2834
|
)
|
|
2795
2835
|
|
|
2796
2836
|
|
nautobot/dcim/views.py
CHANGED
|
@@ -61,7 +61,7 @@ from nautobot.dcim.utils import get_all_network_driver_mappings, get_network_dri
|
|
|
61
61
|
from nautobot.extras.models import Contact, ContactAssociation, Role, Status, Team
|
|
62
62
|
from nautobot.extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectDynamicGroupsView
|
|
63
63
|
from nautobot.ipam.models import IPAddress, Prefix, Service, VLAN
|
|
64
|
-
from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable
|
|
64
|
+
from nautobot.ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable, VRFDeviceAssignmentTable, VRFTable
|
|
65
65
|
from nautobot.virtualization.models import VirtualMachine
|
|
66
66
|
from nautobot.wireless.forms import ControllerManagedDeviceGroupWirelessNetworkFormSet
|
|
67
67
|
from nautobot.wireless.models import (
|
|
@@ -1922,7 +1922,7 @@ class DeviceView(generic.ObjectView):
|
|
|
1922
1922
|
|
|
1923
1923
|
# VRF assignments
|
|
1924
1924
|
vrf_assignments = instance.vrf_assignments.restrict(request.user, "view")
|
|
1925
|
-
vrf_table = VRFDeviceAssignmentTable(vrf_assignments
|
|
1925
|
+
vrf_table = VRFDeviceAssignmentTable(vrf_assignments)
|
|
1926
1926
|
|
|
1927
1927
|
# Software images
|
|
1928
1928
|
if instance.software_version is not None:
|
|
@@ -4513,15 +4513,25 @@ class VirtualDeviceContextUIViewSet(NautobotUIViewSet):
|
|
|
4513
4513
|
queryset = VirtualDeviceContext.objects.all()
|
|
4514
4514
|
serializer_class = serializers.VirtualDeviceContextSerializer
|
|
4515
4515
|
table_class = tables.VirtualDeviceContextTable
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4516
|
+
object_detail_content = object_detail.ObjectDetailContent(
|
|
4517
|
+
panels=(
|
|
4518
|
+
object_detail.ObjectFieldsPanel(
|
|
4519
|
+
section=SectionChoices.LEFT_HALF,
|
|
4520
|
+
weight=100,
|
|
4521
|
+
fields="__all__",
|
|
4522
|
+
),
|
|
4523
|
+
object_detail.ObjectsTablePanel(
|
|
4524
|
+
weight=200,
|
|
4525
|
+
table_class=tables.InterfaceTable,
|
|
4526
|
+
table_attribute="interfaces",
|
|
4527
|
+
section=SectionChoices.FULL_WIDTH,
|
|
4528
|
+
exclude_columns=["device"],
|
|
4529
|
+
),
|
|
4530
|
+
object_detail.ObjectsTablePanel(
|
|
4531
|
+
weight=300,
|
|
4532
|
+
table_class=VRFTable,
|
|
4533
|
+
table_attribute="vrfs",
|
|
4534
|
+
section=SectionChoices.FULL_WIDTH,
|
|
4535
|
+
),
|
|
4536
|
+
),
|
|
4537
|
+
)
|