nautobot 3.0.0a2__py3-none-any.whl → 3.0.0a3__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/apps/choices.py +0 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +9 -1
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit.html +1 -1
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination.html +1 -1
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/circuittype.html +1 -1
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/templates/circuits/providernetwork.html +1 -1
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- nautobot/cloud/filters.py +1 -1
- nautobot/cloud/navigation.py +3 -2
- nautobot/core/api/schema.py +1 -1
- nautobot/core/api/serializers.py +6 -1
- nautobot/core/api/urls.py +1 -0
- nautobot/core/api/views.py +8 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
- nautobot/core/cli/migrate_deprecated_templates.py +200 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/jobs/__init__.py +6 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/settings.py +18 -12
- nautobot/core/settings.yaml +13 -7
- nautobot/core/signals.py +12 -1
- nautobot/core/tables.py +13 -6
- nautobot/core/templates/40x.html +1 -1
- nautobot/core/templates/500.html +2 -2
- nautobot/core/templates/admin/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- nautobot/core/templates/buttons/export.html +1 -1
- nautobot/core/templates/components/button/dropdown.html +5 -3
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +2 -3
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +1 -1
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +4 -3
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- nautobot/core/templates/generic/object_bulk_remove.html +2 -2
- nautobot/core/templates/generic/object_bulk_update.html +5 -4
- nautobot/core/templates/generic/object_create.html +5 -4
- nautobot/core/templates/generic/object_import.html +2 -1
- nautobot/core/templates/generic/object_list.html +12 -4
- nautobot/core/templates/generic/object_notes.html +5 -3
- nautobot/core/templates/generic/object_retrieve.html +2 -3
- nautobot/core/templates/graphene/graphiql.html +7 -7
- nautobot/core/templates/home.html +1 -1
- nautobot/core/templates/import_success.html +2 -1
- nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/created_updated.html +7 -3
- nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +1 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/nav_menu.html +2 -1
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -5
- nautobot/core/templates/redoc_ui.html +7 -0
- nautobot/core/templates/search.html +1 -1
- nautobot/core/templates/swagger_ui.html +17 -3
- nautobot/core/templates/system_jobs/import_objects.html +1 -2
- nautobot/core/templates/utilities/confirmation_form.html +2 -2
- nautobot/core/templates/utilities/obj_table.html +10 -2
- nautobot/core/templates/utilities/render_field.html +7 -7
- nautobot/core/templates/utilities/render_jinja2.html +2 -2
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +4 -4
- nautobot/core/templates/utilities/theme_preview.html +16 -3
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +52 -6
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +23 -10
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/views.py +4 -0
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +9 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +6 -7
- nautobot/core/tests/test_checks.py +28 -0
- nautobot/core/tests/test_cli.py +40 -0
- nautobot/core/tests/test_config.py +2 -1
- nautobot/core/tests/test_forms.py +55 -13
- nautobot/core/tests/test_jobs.py +75 -1
- nautobot/core/tests/test_nautobot_server.py +2 -0
- nautobot/core/tests/test_navigations.py +76 -1
- nautobot/core/tests/test_patch_social_django.py +42 -0
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +53 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +1 -1
- nautobot/core/tests/test_utils.py +31 -4
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +2 -12
- nautobot/core/ui/choices.py +142 -10
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/object_detail.py +92 -12
- nautobot/core/urls.py +12 -1
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +17 -17
- nautobot/core/utils/lookup.py +3 -8
- nautobot/core/utils/module_loading.py +21 -0
- nautobot/core/utils/patch_social_django.py +128 -0
- nautobot/core/views/__init__.py +38 -1
- nautobot/core/views/generic.py +3 -3
- nautobot/core/views/mixins.py +15 -3
- nautobot/core/views/renderers.py +2 -0
- nautobot/core/views/viewsets.py +2 -1
- nautobot/data_validation/apps.py +1 -5
- nautobot/data_validation/custom_validators.py +4 -4
- nautobot/data_validation/filters.py +1 -1
- nautobot/data_validation/forms.py +40 -0
- nautobot/data_validation/migrations/0001_initial.py +0 -7
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +0 -12
- nautobot/data_validation/models.py +16 -7
- nautobot/data_validation/navigation.py +8 -1
- nautobot/data_validation/tables.py +12 -5
- nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
- nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
- nautobot/data_validation/tests/__init__.py +2 -2
- nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
- nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
- nautobot/data_validation/tests/test_filters.py +8 -6
- nautobot/data_validation/tests/test_models.py +15 -0
- nautobot/data_validation/tests/test_views.py +190 -32
- nautobot/data_validation/urls.py +2 -5
- nautobot/data_validation/views.py +73 -40
- nautobot/dcim/api/serializers.py +0 -13
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +16 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +2 -35
- nautobot/dcim/forms.py +1 -1
- nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
- nautobot/dcim/models/device_components.py +81 -68
- nautobot/dcim/models/devices.py +13 -16
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +3 -0
- nautobot/dcim/tables/template_code.py +14 -14
- nautobot/dcim/templates/dcim/cable.html +2 -61
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_edit.html +2 -5
- nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
- nautobot/dcim/templates/dcim/cable_trace.html +1 -3
- nautobot/dcim/templates/dcim/cable_update.html +5 -0
- nautobot/dcim/templates/dcim/consoleport.html +6 -5
- nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
- nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
- nautobot/dcim/templates/dcim/device/frontports.html +1 -1
- nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
- nautobot/dcim/templates/dcim/device/inventory.html +1 -1
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
- nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/device/powerports.html +1 -1
- nautobot/dcim/templates/dcim/device/rearports.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- nautobot/dcim/templates/dcim/device/wireless.html +1 -1
- nautobot/dcim/templates/dcim/device.html +1 -1
- nautobot/dcim/templates/dcim/device_component_add.html +2 -2
- nautobot/dcim/templates/dcim/device_create.html +5 -3
- nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
- nautobot/dcim/templates/dcim/device_list.html +73 -10
- nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
- nautobot/dcim/templates/dcim/frontport.html +9 -8
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +26 -6
- nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
- nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
- nautobot/dcim/templates/dcim/module/base.html +49 -9
- nautobot/dcim/templates/dcim/module_list.html +57 -8
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +5 -4
- nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
- nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
- nautobot/dcim/templates/dcim/rearport.html +7 -6
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
- nautobot/dcim/tests/integration/test_controller.py +1 -0
- nautobot/dcim/tests/test_api.py +8 -0
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +12 -6
- nautobot/dcim/tests/test_models.py +63 -4
- nautobot/dcim/tests/test_views.py +63 -22
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +547 -273
- nautobot/extras/api/views.py +9 -1
- nautobot/extras/choices.py +2 -13
- nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
- nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
- nautobot/extras/{filters/__init__.py → filters.py} +14 -46
- nautobot/extras/forms/forms.py +5 -13
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/management/__init__.py +9 -0
- nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
- nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
- nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
- nautobot/extras/models/__init__.py +1 -2
- nautobot/extras/models/approvals.py +22 -13
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +59 -1
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +13 -0
- nautobot/extras/models/secrets.py +1 -0
- nautobot/extras/models/statuses.py +0 -15
- nautobot/extras/navigation.py +13 -9
- nautobot/extras/plugins/__init__.py +33 -55
- nautobot/extras/plugins/tables.py +3 -3
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/views.py +0 -4
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +52 -68
- nautobot/extras/templates/extras/approval_dashboard.html +7 -5
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
- nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
- nautobot/extras/templates/extras/computedfield.html +1 -1
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/customfield.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
- nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
- nautobot/extras/templates/extras/gitrepository_result.html +0 -2
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
- nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
- nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
- nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
- nautobot/extras/templates/extras/inc/json_format.html +10 -3
- nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
- nautobot/extras/templates/extras/job.html +29 -12
- nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
- nautobot/extras/templates/extras/job_edit.html +52 -46
- nautobot/extras/templates/extras/job_list.html +29 -25
- nautobot/extras/templates/extras/marketplace.html +5 -9
- nautobot/extras/templates/extras/object_configcontext.html +1 -1
- nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
- nautobot/extras/templates/extras/objectchange_retrieve.html +19 -37
- nautobot/extras/templates/extras/plugin_detail.html +26 -21
- nautobot/extras/templates/extras/plugins_list.html +16 -26
- nautobot/extras/templates/extras/role_retrieve.html +64 -0
- nautobot/extras/templates/extras/scheduledjob.html +4 -2
- nautobot/extras/templates/extras/secretsgroup.html +1 -1
- nautobot/extras/templates/extras/tag.html +1 -1
- nautobot/extras/templatetags/custom_links.py +12 -12
- nautobot/extras/templatetags/job_buttons.py +14 -12
- nautobot/extras/test_jobs/invalid_import.py +9 -0
- nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
- nautobot/extras/test_jobs/missing_import.py +11 -0
- nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +8 -7
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/test_api.py +27 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +35 -3
- nautobot/extras/tests/test_customfields.py +22 -13
- nautobot/extras/tests/test_customfields_filters.py +479 -0
- nautobot/extras/tests/test_dynamicgroups.py +39 -1
- nautobot/extras/tests/test_filters.py +21 -19
- nautobot/extras/tests/test_forms.py +18 -21
- nautobot/extras/tests/test_jobs.py +25 -4
- nautobot/extras/tests/test_migrations.py +1 -0
- nautobot/extras/tests/test_models.py +13 -31
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_views.py +31 -30
- nautobot/extras/views.py +81 -19
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +27 -38
- nautobot/ipam/formfields.py +1 -1
- nautobot/ipam/forms.py +6 -3
- nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
- nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
- nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/navigation.py +3 -2
- nautobot/ipam/signals.py +71 -0
- nautobot/ipam/tables.py +13 -6
- nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
- nautobot/ipam/templates/ipam/ipaddress.html +14 -0
- nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
- nautobot/ipam/templates/ipam/namespace_update.html +15 -0
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- nautobot/ipam/templates/ipam/service.html +1 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/tests/migration/test_migrations.py +89 -0
- nautobot/ipam/tests/test_api.py +13 -6
- nautobot/ipam/tests/test_filters.py +10 -0
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +43 -1
- nautobot/ipam/tests/test_tables.py +1 -2
- nautobot/ipam/tests/test_utils.py +1 -1
- nautobot/ipam/tests/test_views.py +13 -14
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/utils/migrations.py +16 -2
- nautobot/ipam/utils/testing.py +9 -3
- nautobot/ipam/views.py +46 -6
- nautobot/project-static/dist/css/nautobot.css +1 -1
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/nautobot.js +1 -1
- nautobot/project-static/dist/js/nautobot.js.map +1 -1
- nautobot/project-static/js/cabletrace.js +1 -1
- nautobot/project-static/js/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud.svg +1 -1
- nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
- nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
- nautobot/project-static/nautobot-icons/elements.svg +1 -1
- nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
- nautobot/project-static/nautobot-icons/hammer.svg +1 -1
- nautobot/project-static/nautobot-icons/organization.svg +3 -0
- nautobot/project-static/nautobot-icons/secrets.svg +1 -1
- nautobot/project-static/nautobot-icons/security.svg +3 -0
- nautobot/project-static/nautobot-icons/server.svg +1 -1
- nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
- nautobot/project-static/nautobot-icons/star.svg +1 -1
- nautobot/tenancy/api/serializers.py +1 -0
- nautobot/tenancy/api/views.py +2 -1
- nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
- nautobot/tenancy/navigation.py +3 -1
- nautobot/tenancy/tests/test_filters.py +0 -2
- nautobot/tenancy/views.py +2 -1
- nautobot/ui/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -0
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +61 -28
- nautobot/users/templates/users/profile.html +45 -12
- nautobot/users/templates/users/sessionkey_delete.html +1 -1
- nautobot/users/tests/test_api.py +4 -0
- nautobot/users/views.py +4 -2
- nautobot/virtualization/models.py +1 -68
- nautobot/virtualization/navigation.py +3 -2
- nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
- nautobot/virtualization/tests/test_api.py +3 -0
- nautobot/virtualization/tests/test_models.py +44 -4
- nautobot/vpn/__init__.py +0 -0
- nautobot/vpn/api/serializers.py +113 -0
- nautobot/vpn/api/urls.py +19 -0
- nautobot/vpn/api/views.py +70 -0
- nautobot/vpn/apps.py +8 -0
- nautobot/vpn/choices.py +171 -0
- nautobot/vpn/factory.py +209 -0
- nautobot/vpn/filters.py +233 -0
- nautobot/vpn/forms.py +486 -0
- nautobot/vpn/homepage.py +19 -0
- nautobot/vpn/migrations/0001_initial.py +541 -0
- nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
- nautobot/vpn/migrations/__init__.py +0 -0
- nautobot/vpn/models.py +527 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +380 -0
- nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +341 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +294 -0
- nautobot/vpn/tests/test_models.py +97 -0
- nautobot/vpn/tests/test_views.py +281 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +437 -0
- nautobot/wireless/navigation.py +3 -2
- nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
- nautobot/wireless/tests/test_api.py +1 -1
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +14 -14
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
- nautobot/data_validation/template_content.py +0 -42
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
|
@@ -2,25 +2,43 @@
|
|
|
2
2
|
{% load helpers %}
|
|
3
3
|
{% load static %}
|
|
4
4
|
|
|
5
|
-
{% block
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
</
|
|
12
|
-
{% endblock
|
|
5
|
+
{% block extra_styles %}
|
|
6
|
+
<!-- Invert the rack elevation in dark mode as a quick fix until we have a proper dark mode svg generation -->
|
|
7
|
+
<style>
|
|
8
|
+
[data-theme="dark"] .rack_elevation {
|
|
9
|
+
filter: invert(1) hue-rotate(180deg);
|
|
10
|
+
}
|
|
11
|
+
</style>
|
|
12
|
+
{% endblock %}
|
|
13
13
|
|
|
14
|
-
{% block
|
|
15
|
-
|
|
16
|
-
{%
|
|
17
|
-
|
|
14
|
+
{% block extra_buttons %}
|
|
15
|
+
{% if prev_rack %}
|
|
16
|
+
<a href="{% url 'dcim:rack' pk=prev_rack.pk %}" class="btn btn-primary">
|
|
17
|
+
<span class="mdi mdi-chevron-left" aria-hidden="true"></span> Previous Rack
|
|
18
|
+
</a>
|
|
19
|
+
{% else %}
|
|
20
|
+
<a aria-disabled="true" class="btn btn-primary disabled">
|
|
21
|
+
<span class="mdi mdi-chevron-left" aria-hidden="true"></span> Previous Rack
|
|
22
|
+
</a>
|
|
23
|
+
{% endif %}
|
|
24
|
+
{% if next_rack %}
|
|
25
|
+
<a href="{% url 'dcim:rack' pk=next_rack.pk %}" class="btn btn-primary">
|
|
26
|
+
<span class="mdi mdi-chevron-right" aria-hidden="true"></span> Next Rack
|
|
27
|
+
</a>
|
|
28
|
+
{% else %}
|
|
29
|
+
<a aria-disabled="true" class="btn btn-primary disabled">
|
|
30
|
+
<span class="mdi mdi-chevron-right" aria-hidden="true"></span> Next Rack
|
|
31
|
+
</a>
|
|
32
|
+
{% endif %}
|
|
33
|
+
<button class="btn btn-secondary toggle-fullname" selected="selected">
|
|
18
34
|
<span class="mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Device Full Name
|
|
19
35
|
</button>
|
|
20
|
-
<button class="btn btn-
|
|
36
|
+
<button class="btn btn-secondary toggle-images" selected="selected">
|
|
21
37
|
<span class="mdi mdi-checkbox-marked-circle-outline" aria-hidden="true"></span> Show Images
|
|
22
38
|
</button>
|
|
23
|
-
{% endblock
|
|
39
|
+
{% endblock extra_buttons %}
|
|
40
|
+
|
|
41
|
+
{% block title %}Rack {{ object }}{% endblock title %}
|
|
24
42
|
|
|
25
43
|
{% block content_left_page %}
|
|
26
44
|
<div class="card">
|
|
@@ -160,7 +178,7 @@
|
|
|
160
178
|
<span class="badge" style="color: {{ powerfeed.status.color|fgcolor }}; background-color: #{{powerfeed.status.color}}">{{ powerfeed.get_status_display }}</span>
|
|
161
179
|
</td>
|
|
162
180
|
<td>
|
|
163
|
-
<span class="badge
|
|
181
|
+
<span class="badge bg-{{ powerfeed.get_type_class }}">{{ powerfeed.get_type_display }}</span>
|
|
164
182
|
</td>
|
|
165
183
|
{% with power_port=powerfeed.connected_endpoint %}
|
|
166
184
|
{% if power_port %}
|
|
@@ -72,14 +72,15 @@
|
|
|
72
72
|
Not connected
|
|
73
73
|
{% if perms.dcim.add_cable %}
|
|
74
74
|
<span class="dropdown float-end">
|
|
75
|
-
|
|
75
|
+
{# comment The "fixed" strategy allows the dropdown to break out of the containing card #}
|
|
76
|
+
<button type="button" class="btn btn-primary btn-sm dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-bs-popper-config='{"strategy": "fixed"}'>
|
|
76
77
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
|
77
78
|
</button>
|
|
78
|
-
<ul class="dropdown-menu dropdown-menu-
|
|
79
|
-
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='interface' %}?return_url={{ object.get_absolute_url }}">Interface</a></li>
|
|
80
|
-
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}">Front Port</a></li>
|
|
81
|
-
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}">Rear Port</a></li>
|
|
82
|
-
<li><a href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='circuit-termination' %}?return_url={{ object.get_absolute_url }}">Circuit Termination</a></li>
|
|
79
|
+
<ul class="dropdown-menu dropdown-menu-end">
|
|
80
|
+
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='interface' %}?return_url={{ object.get_absolute_url }}">Interface</a></li>
|
|
81
|
+
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}">Front Port</a></li>
|
|
82
|
+
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}">Rear Port</a></li>
|
|
83
|
+
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='circuit-termination' %}?return_url={{ object.get_absolute_url }}">Circuit Termination</a></li>
|
|
83
84
|
</ul>
|
|
84
85
|
</span>
|
|
85
86
|
{% endif %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
{% extends '
|
|
1
|
+
{% extends 'generic/object_retrieve.html' %}
|
|
2
2
|
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -3,25 +3,27 @@
|
|
|
3
3
|
|
|
4
4
|
{% block content %}
|
|
5
5
|
<form action="" method="post" enctype="multipart/form-data" class="h-100 vstack">
|
|
6
|
-
{% csrf_token %}
|
|
7
|
-
<div class="
|
|
6
|
+
{% csrf_token %}
|
|
7
|
+
<div class="row justify-content-center">
|
|
8
8
|
<h3 class="mb-16">{% block title %}Add New Member to Virtual Chassis {{ virtual_chassis }}{% endblock %}</h3>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<div class="card
|
|
12
|
-
<
|
|
9
|
+
<div class="col-xl-8 col-lg-10">
|
|
10
|
+
{% if membership_form.non_field_errors %}
|
|
11
|
+
<div class="card border-danger">
|
|
12
|
+
<div class="card-header bg-danger-subtle border-danger text-body">
|
|
13
|
+
<strong>Errors</strong>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card-body">
|
|
16
|
+
{{ membership_form.non_field_errors }}
|
|
17
|
+
</div>
|
|
13
18
|
</div>
|
|
19
|
+
{% endif %}
|
|
20
|
+
<div class="card">
|
|
21
|
+
<div class="card-header"><strong>Add New Member</strong></div>
|
|
14
22
|
<div class="card-body">
|
|
15
|
-
{
|
|
23
|
+
{% render_form member_select_form %}
|
|
24
|
+
{% render_form membership_form %}
|
|
16
25
|
</div>
|
|
17
26
|
</div>
|
|
18
|
-
{% endif %}
|
|
19
|
-
<div class="card">
|
|
20
|
-
<div class="card-header"><strong>Add New Member</strong></div>
|
|
21
|
-
<div class="card-body">
|
|
22
|
-
{% render_form member_select_form %}
|
|
23
|
-
{% render_form membership_form %}
|
|
24
|
-
</div>
|
|
25
27
|
</div>
|
|
26
28
|
</div>
|
|
27
29
|
<div class="nb-form-sticky-footer">
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
{% load helpers %}
|
|
3
3
|
{% load form_helpers %}
|
|
4
4
|
|
|
5
|
+
{% block title %}{% if vc_form.instance %}Editing {{ vc_form.instance }}{% else %}New Virtual Chassis{% endif %}{% endblock %}
|
|
6
|
+
|
|
5
7
|
{% block content %}
|
|
6
8
|
<form action="" method="post" enctype="multipart/form-data" class="h-100 vstack">
|
|
7
9
|
{% csrf_token %}
|
|
8
10
|
{{ pk_form.pk }}
|
|
9
11
|
{{ formset.management_form }}
|
|
10
|
-
<div class="row
|
|
11
|
-
<div class="col-xl-8
|
|
12
|
-
<h3 class="mb-16">{% block title %}{% if vc_form.instance %}Editing {{ vc_form.instance }}{% else %}New Virtual Chassis{% endif %}{% endblock %}</h3>
|
|
12
|
+
<div class="row justify-content-center flex-fill">
|
|
13
|
+
<div class="col-xl-8 col-lg-10">
|
|
13
14
|
{% if vc_form.non_field_errors %}
|
|
14
15
|
<div class="card border-danger">
|
|
15
16
|
<div class="card-header bg-danger-subtle border-danger text-body">
|
|
@@ -72,9 +73,16 @@
|
|
|
72
73
|
</td>
|
|
73
74
|
<td>
|
|
74
75
|
{% if virtual_chassis.present_in_database %}
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
76
|
+
{% if virtual_chassis.master != device %}
|
|
77
|
+
<a href="{% url 'dcim:virtualchassis_remove_member' pk=device.pk %}?return_url={% url 'dcim:virtualchassis_edit' pk=virtual_chassis.pk %}"
|
|
78
|
+
class="btn btn-danger btn-sm">
|
|
79
|
+
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
|
|
80
|
+
</a>
|
|
81
|
+
{% else %}
|
|
82
|
+
<a aria-disabled="true" class="btn btn-danger btn-sm disabled">
|
|
83
|
+
<span class="mdi mdi-trash-can-outline" aria-hidden="true"></span>
|
|
84
|
+
</a>
|
|
85
|
+
{% endif %}
|
|
78
86
|
{% endif %}
|
|
79
87
|
</td>
|
|
80
88
|
</tr>
|
|
@@ -38,6 +38,7 @@ class ControllerTestCase(SeleniumTestCase):
|
|
|
38
38
|
|
|
39
39
|
# Create Controller
|
|
40
40
|
self.click_navbar_entry("Devices", "Controllers")
|
|
41
|
+
|
|
41
42
|
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:controller_list"))
|
|
42
43
|
self.click_list_view_add_button()
|
|
43
44
|
self.assertEqual(self.browser.url, self.live_server_url + reverse("dcim:controller_add"))
|
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -1471,6 +1471,9 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
|
|
|
1471
1471
|
class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
1472
1472
|
model = Device
|
|
1473
1473
|
choices_fields = ["face"]
|
|
1474
|
+
validation_excluded_fields = [
|
|
1475
|
+
"software_image_files", # M2M field, excluded by default
|
|
1476
|
+
]
|
|
1474
1477
|
|
|
1475
1478
|
@classmethod
|
|
1476
1479
|
def setUpTestData(cls):
|
|
@@ -2168,6 +2171,9 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2168
2171
|
model = Interface
|
|
2169
2172
|
peer_termination_type = Interface
|
|
2170
2173
|
choices_fields = ["mode", "type"]
|
|
2174
|
+
validation_excluded_fields = [
|
|
2175
|
+
"tagged_vlans", # M2M field, excluded by default
|
|
2176
|
+
]
|
|
2171
2177
|
|
|
2172
2178
|
@classmethod
|
|
2173
2179
|
def setUpTestData(cls):
|
|
@@ -3588,6 +3594,7 @@ class DeviceTypeToSoftwareImageFileTestCase(
|
|
|
3588
3594
|
|
|
3589
3595
|
class ControllerTestCase(APIViewTestCases.APIViewTestCase):
|
|
3590
3596
|
model = Controller
|
|
3597
|
+
choices_fields = ("capabilities",)
|
|
3591
3598
|
|
|
3592
3599
|
def get_deletable_object(self):
|
|
3593
3600
|
# This method is used in `test_recreate_object_csv`,
|
|
@@ -3647,6 +3654,7 @@ class ControllerTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
3647
3654
|
|
|
3648
3655
|
class ControllerManagedDeviceGroupTestCase(APIViewTestCases.APIViewTestCase):
|
|
3649
3656
|
model = ControllerManagedDeviceGroup
|
|
3657
|
+
choices_fields = ("capabilities",)
|
|
3650
3658
|
|
|
3651
3659
|
def get_deletable_object(self):
|
|
3652
3660
|
# This method is used in `test_recreate_object_csv`,
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from django.contrib.contenttypes.models import ContentType
|
|
2
|
+
from django.core.exceptions import ValidationError
|
|
3
|
+
from django.test import override_settings, TestCase
|
|
4
|
+
|
|
5
|
+
from nautobot.core.testing.mixins import NautobotTestCaseMixin
|
|
6
|
+
from nautobot.data_validation.models import RequiredValidationRule
|
|
7
|
+
from nautobot.dcim.choices import DeviceUniquenessChoices
|
|
8
|
+
from nautobot.dcim.models import Device, DeviceType, Location
|
|
9
|
+
from nautobot.extras.models import Role, Status
|
|
10
|
+
from nautobot.tenancy.models import Tenant
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DeviceUniquenessValidatorTest(NautobotTestCaseMixin, TestCase):
|
|
14
|
+
"""Tests for the DeviceUniquenessValidator custom validator."""
|
|
15
|
+
|
|
16
|
+
def setUp(self):
|
|
17
|
+
super().setUp()
|
|
18
|
+
self.device_status = Status.objects.get_for_model(Device).first()
|
|
19
|
+
self.device_type = DeviceType.objects.first()
|
|
20
|
+
self.device_role = Role.objects.get_for_model(Device).first()
|
|
21
|
+
self.location = Location.objects.first()
|
|
22
|
+
self.tenant = Tenant.objects.create(name="Tenant")
|
|
23
|
+
self.device_name = "Device"
|
|
24
|
+
self.device = Device.objects.create(
|
|
25
|
+
name=self.device_name,
|
|
26
|
+
device_type=self.device_type,
|
|
27
|
+
role=self.device_role,
|
|
28
|
+
location=self.location,
|
|
29
|
+
status=self.device_status,
|
|
30
|
+
tenant=self.tenant,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME)
|
|
34
|
+
def test_duplicate_same_location_tenant_name_fails(self):
|
|
35
|
+
"""Same name, tenant, and location should raise ValidationError."""
|
|
36
|
+
dup_device = Device(
|
|
37
|
+
name=self.device_name,
|
|
38
|
+
device_type=self.device_type,
|
|
39
|
+
role=self.device_role,
|
|
40
|
+
location=self.location,
|
|
41
|
+
status=self.device_status,
|
|
42
|
+
tenant=self.tenant,
|
|
43
|
+
)
|
|
44
|
+
with self.assertRaises(ValidationError) as contextmanager:
|
|
45
|
+
dup_device.full_clean()
|
|
46
|
+
self.assertIn(
|
|
47
|
+
f"A device named '{self.device_name}' already exists in this location: {self.location} and tenant: {self.tenant}. ",
|
|
48
|
+
str(contextmanager.exception),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME)
|
|
52
|
+
def test_different_tenant_allows_duplicate_name(self):
|
|
53
|
+
"""Same name and location, different tenant should be allowed."""
|
|
54
|
+
tenant = Tenant.objects.create(name="Tenant2")
|
|
55
|
+
non_dup_device = Device(
|
|
56
|
+
name=self.device_name,
|
|
57
|
+
device_type=self.device_type,
|
|
58
|
+
role=self.device_role,
|
|
59
|
+
location=self.location,
|
|
60
|
+
status=self.device_status,
|
|
61
|
+
tenant=tenant,
|
|
62
|
+
)
|
|
63
|
+
non_dup_device.full_clean() # should not raise
|
|
64
|
+
|
|
65
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME)
|
|
66
|
+
def test_different_location_allows_duplicate_name(self):
|
|
67
|
+
"""Same name and tenant, different location should be allowed."""
|
|
68
|
+
location = Location.objects.last()
|
|
69
|
+
non_dup_device = Device(
|
|
70
|
+
name=self.device_name,
|
|
71
|
+
device_type=self.device_type,
|
|
72
|
+
role=self.device_role,
|
|
73
|
+
location=location,
|
|
74
|
+
status=self.device_status,
|
|
75
|
+
tenant=self.tenant,
|
|
76
|
+
)
|
|
77
|
+
non_dup_device.full_clean() # should not raise
|
|
78
|
+
|
|
79
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME)
|
|
80
|
+
def test_duplicate_name_with_null_tenant_fails(self):
|
|
81
|
+
"""Duplicate name with tenant=None should raise ValidationError."""
|
|
82
|
+
Device.objects.create(
|
|
83
|
+
name="Device-2",
|
|
84
|
+
location=self.location,
|
|
85
|
+
tenant=None,
|
|
86
|
+
device_type=self.device_type,
|
|
87
|
+
role=self.device_role,
|
|
88
|
+
status=self.device_status,
|
|
89
|
+
)
|
|
90
|
+
dup = Device(
|
|
91
|
+
name="Device-2",
|
|
92
|
+
location=self.location,
|
|
93
|
+
tenant=None,
|
|
94
|
+
device_type=self.device_type,
|
|
95
|
+
role=self.device_role,
|
|
96
|
+
status=self.device_status,
|
|
97
|
+
)
|
|
98
|
+
with self.assertRaises(ValidationError) as contextmanager:
|
|
99
|
+
dup.full_clean()
|
|
100
|
+
self.assertIn(
|
|
101
|
+
f"A device named '{dup.name}' with no tenant already exists in this location: {self.location}. ",
|
|
102
|
+
str(contextmanager.exception),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME)
|
|
106
|
+
def test_duplicate_name_globally_fails(self):
|
|
107
|
+
"""Duplicate name should raise ValidationError."""
|
|
108
|
+
tenant = Tenant.objects.create(name="Tenant2")
|
|
109
|
+
location = Location.objects.last()
|
|
110
|
+
dup_device = Device(
|
|
111
|
+
name=self.device_name,
|
|
112
|
+
device_type=self.device_type,
|
|
113
|
+
role=self.device_role,
|
|
114
|
+
location=location,
|
|
115
|
+
status=self.device_status,
|
|
116
|
+
tenant=tenant,
|
|
117
|
+
)
|
|
118
|
+
with self.assertRaises(ValidationError) as contextmanager:
|
|
119
|
+
dup_device.full_clean()
|
|
120
|
+
self.assertIn(
|
|
121
|
+
f"At least one other device named '{dup_device.name}' already exists. ", str(contextmanager.exception)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME)
|
|
125
|
+
def test_different_name_succeeds(self):
|
|
126
|
+
"""Different name should be allowed globally."""
|
|
127
|
+
non_dup_device = Device(
|
|
128
|
+
name="Device-2",
|
|
129
|
+
device_type=self.device_type,
|
|
130
|
+
role=self.device_role,
|
|
131
|
+
location=self.location,
|
|
132
|
+
status=self.device_status,
|
|
133
|
+
tenant=self.tenant,
|
|
134
|
+
)
|
|
135
|
+
non_dup_device.full_clean() # should not raise
|
|
136
|
+
|
|
137
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME)
|
|
138
|
+
def test_unnamed_device_allowed_if_name_not_required(self):
|
|
139
|
+
"""Unnamed device allowed if DEVICE_NAME_REQUIRED is False."""
|
|
140
|
+
Device.objects.create(
|
|
141
|
+
name=None,
|
|
142
|
+
location=self.location,
|
|
143
|
+
tenant=self.tenant,
|
|
144
|
+
device_type=self.device_type,
|
|
145
|
+
role=self.device_role,
|
|
146
|
+
status=self.device_status,
|
|
147
|
+
)
|
|
148
|
+
unnamed2 = Device(
|
|
149
|
+
name=None,
|
|
150
|
+
location=self.location,
|
|
151
|
+
tenant=self.tenant,
|
|
152
|
+
device_type=self.device_type,
|
|
153
|
+
role=self.device_role,
|
|
154
|
+
status=self.device_status,
|
|
155
|
+
)
|
|
156
|
+
self.assertFalse(
|
|
157
|
+
RequiredValidationRule.objects.filter(
|
|
158
|
+
content_type=ContentType.objects.get_for_model(Device), field="name"
|
|
159
|
+
).exists()
|
|
160
|
+
)
|
|
161
|
+
unnamed2.full_clean() # should not raise
|
|
162
|
+
|
|
163
|
+
def test_unnamed_device_fails_if_name_is_required(self):
|
|
164
|
+
"""Unnamed device should raise a ValidationError if DEVICE_NAME_REQUIRED is True."""
|
|
165
|
+
unnamed = Device(
|
|
166
|
+
name=None,
|
|
167
|
+
location=self.location,
|
|
168
|
+
tenant=self.tenant,
|
|
169
|
+
device_type=self.device_type,
|
|
170
|
+
role=self.device_role,
|
|
171
|
+
status=self.device_status,
|
|
172
|
+
)
|
|
173
|
+
RequiredValidationRule.objects.create(content_type=ContentType.objects.get_for_model(Device), field="name")
|
|
174
|
+
with self.assertRaises(ValidationError) as contextmanager:
|
|
175
|
+
unnamed.full_clean()
|
|
176
|
+
# This error is from RequiredValidationRule
|
|
177
|
+
self.assertIn("{'name': ['This field cannot be blank.']}", str(contextmanager.exception))
|
|
178
|
+
|
|
179
|
+
def test_empty_device_fails_if_name_is_required(self):
|
|
180
|
+
"""Empty name device should raise a ValidationError if DEVICE_NAME_REQUIRED is True."""
|
|
181
|
+
unnamed = Device(
|
|
182
|
+
name="",
|
|
183
|
+
location=self.location,
|
|
184
|
+
tenant=self.tenant,
|
|
185
|
+
device_type=self.device_type,
|
|
186
|
+
role=self.device_role,
|
|
187
|
+
status=self.device_status,
|
|
188
|
+
)
|
|
189
|
+
RequiredValidationRule.objects.create(content_type=ContentType.objects.get_for_model(Device), field="name")
|
|
190
|
+
with self.assertRaises(ValidationError) as contextmanager:
|
|
191
|
+
unnamed.full_clean()
|
|
192
|
+
# This error is from RequiredValidationRule
|
|
193
|
+
self.assertIn("{'name': ['This field cannot be blank.']}", str(contextmanager.exception))
|
|
194
|
+
|
|
195
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE)
|
|
196
|
+
def test_no_uniqueness_enforced(self):
|
|
197
|
+
"""Devices should not trigger validation errors when uniqueness is disabled."""
|
|
198
|
+
dup_device = Device(
|
|
199
|
+
name=self.device.name,
|
|
200
|
+
location=self.location,
|
|
201
|
+
tenant=self.tenant,
|
|
202
|
+
role=self.device_role,
|
|
203
|
+
device_type=self.device_type,
|
|
204
|
+
status=self.device_status,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Should NOT raise any error since uniqueness enforcement is off
|
|
208
|
+
dup_device.full_clean()
|
|
209
|
+
|
|
210
|
+
@override_settings(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE)
|
|
211
|
+
def test_allow_duplicate_devices_with_empty_name_when_uniqueness_is_none(self):
|
|
212
|
+
"""Allow duplicate devices with empty name when DEVICE_UNIQUENESS="none"."""
|
|
213
|
+
Device.objects.create(
|
|
214
|
+
name="",
|
|
215
|
+
location=self.location,
|
|
216
|
+
tenant=self.tenant,
|
|
217
|
+
device_type=self.device_type,
|
|
218
|
+
role=self.device_role,
|
|
219
|
+
status=self.device_status,
|
|
220
|
+
)
|
|
221
|
+
empty_name = Device(
|
|
222
|
+
name="",
|
|
223
|
+
location=self.location,
|
|
224
|
+
tenant=self.tenant,
|
|
225
|
+
device_type=self.device_type,
|
|
226
|
+
role=self.device_role,
|
|
227
|
+
status=self.device_status,
|
|
228
|
+
)
|
|
229
|
+
empty_name.full_clean()
|
|
@@ -124,8 +124,9 @@ from nautobot.dcim.models import (
|
|
|
124
124
|
VirtualChassis,
|
|
125
125
|
VirtualDeviceContext,
|
|
126
126
|
)
|
|
127
|
-
from nautobot.extras.
|
|
127
|
+
from nautobot.extras.filter_mixins import RoleFilter, StatusFilter
|
|
128
128
|
from nautobot.extras.models import ExternalIntegration, Role, SecretsGroup, Status, Tag
|
|
129
|
+
from nautobot.extras.tests.test_customfields_filters import CustomFieldsFilters
|
|
129
130
|
from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup
|
|
130
131
|
from nautobot.tenancy.models import Tenant
|
|
131
132
|
from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine
|
|
@@ -1039,7 +1040,9 @@ class PathEndpointModelTestMixin:
|
|
|
1039
1040
|
)
|
|
1040
1041
|
|
|
1041
1042
|
|
|
1042
|
-
class LocationTypeFilterSetTestCase(
|
|
1043
|
+
class LocationTypeFilterSetTestCase(
|
|
1044
|
+
FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin
|
|
1045
|
+
):
|
|
1043
1046
|
queryset = LocationType.objects.all()
|
|
1044
1047
|
filterset = LocationTypeFilterSet
|
|
1045
1048
|
generic_filter_tests = [
|
|
@@ -1076,7 +1079,10 @@ class LocationTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
1076
1079
|
)
|
|
1077
1080
|
|
|
1078
1081
|
|
|
1079
|
-
class LocationFilterSetTestCase(
|
|
1082
|
+
class LocationFilterSetTestCase(
|
|
1083
|
+
FilterTestCases.FilterTestCase,
|
|
1084
|
+
FilterTestCases.TenancyFilterTestCaseMixin,
|
|
1085
|
+
):
|
|
1080
1086
|
queryset = Location.objects.all()
|
|
1081
1087
|
filterset = LocationFilterSet
|
|
1082
1088
|
tenancy_related_name = "locations"
|
|
@@ -1149,7 +1155,7 @@ class LocationFilterSetTestCase(FilterTestCases.FilterTestCase, FilterTestCases.
|
|
|
1149
1155
|
)
|
|
1150
1156
|
|
|
1151
1157
|
|
|
1152
|
-
class RackGroupTestCase(FilterTestCases.FilterTestCase):
|
|
1158
|
+
class RackGroupTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
|
|
1153
1159
|
queryset = RackGroup.objects.all()
|
|
1154
1160
|
filterset = RackGroupFilterSet
|
|
1155
1161
|
generic_filter_tests = [
|
|
@@ -1357,7 +1363,7 @@ class RackReservationTestCase(FilterTestCases.FilterTestCase, FilterTestCases.Te
|
|
|
1357
1363
|
common_test_data(cls)
|
|
1358
1364
|
|
|
1359
1365
|
|
|
1360
|
-
class ManufacturerTestCase(FilterTestCases.FilterTestCase):
|
|
1366
|
+
class ManufacturerTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
|
|
1361
1367
|
queryset = Manufacturer.objects.all()
|
|
1362
1368
|
filterset = ManufacturerFilterSet
|
|
1363
1369
|
generic_filter_tests = [
|
|
@@ -1393,7 +1399,7 @@ class DeviceFamilyTestCase(FilterTestCases.FilterTestCase):
|
|
|
1393
1399
|
]
|
|
1394
1400
|
|
|
1395
1401
|
|
|
1396
|
-
class DeviceTypeTestCase(FilterTestCases.FilterTestCase):
|
|
1402
|
+
class DeviceTypeTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
|
|
1397
1403
|
queryset = DeviceType.objects.all()
|
|
1398
1404
|
filterset = DeviceTypeFilterSet
|
|
1399
1405
|
generic_filter_tests = [
|
|
@@ -2,6 +2,7 @@ from decimal import Decimal
|
|
|
2
2
|
|
|
3
3
|
from constance.test import override_config
|
|
4
4
|
from django.contrib.contenttypes.models import ContentType
|
|
5
|
+
from django.core.cache import caches
|
|
5
6
|
from django.core.exceptions import ValidationError
|
|
6
7
|
from django.db import IntegrityError
|
|
7
8
|
from django.db.models import Model
|
|
@@ -16,6 +17,7 @@ from nautobot.dcim.choices import (
|
|
|
16
17
|
CableTypeChoices,
|
|
17
18
|
ConsolePortTypeChoices,
|
|
18
19
|
DeviceFaceChoices,
|
|
20
|
+
DeviceUniquenessChoices,
|
|
19
21
|
InterfaceModeChoices,
|
|
20
22
|
InterfaceTypeChoices,
|
|
21
23
|
PortTypeChoices,
|
|
@@ -1423,6 +1425,10 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
1423
1425
|
model = Device
|
|
1424
1426
|
|
|
1425
1427
|
def setUp(self):
|
|
1428
|
+
# clear Constance cache
|
|
1429
|
+
cache = caches[settings.CONSTANCE_DATABASE_CACHE_BACKEND]
|
|
1430
|
+
cache.clear()
|
|
1431
|
+
|
|
1426
1432
|
manufacturer = Manufacturer.objects.first()
|
|
1427
1433
|
self.device_type = DeviceType.objects.create(
|
|
1428
1434
|
manufacturer=manufacturer,
|
|
@@ -1532,12 +1538,25 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
1532
1538
|
|
|
1533
1539
|
def test_natural_key_overrides(self):
|
|
1534
1540
|
"""Ensure that the natural-key for Device is affected by settings/Constance."""
|
|
1535
|
-
with override_config(
|
|
1541
|
+
with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME):
|
|
1536
1542
|
self.assertEqual([self.device.name], self.device.natural_key())
|
|
1537
1543
|
# self.assertEqual(construct_composite_key([self.device.name]), self.device.composite_key) # TODO: Revist this if we reintroduce composite keys
|
|
1538
1544
|
self.assertEqual(self.device, Device.objects.get_by_natural_key([self.device.name]))
|
|
1539
1545
|
# self.assertEqual(self.device, Device.objects.get(composite_key=self.device.composite_key)) # TODO: Revist this if we reintroduce composite keys
|
|
1540
1546
|
|
|
1547
|
+
with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME):
|
|
1548
|
+
self.assertEqual(
|
|
1549
|
+
[self.device.name, self.device.tenant, self.device.location.name], self.device.natural_key()
|
|
1550
|
+
)
|
|
1551
|
+
self.assertEqual(
|
|
1552
|
+
self.device,
|
|
1553
|
+
Device.objects.get_by_natural_key([self.device.name, self.device.tenant, self.device.location]),
|
|
1554
|
+
)
|
|
1555
|
+
|
|
1556
|
+
with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE):
|
|
1557
|
+
self.assertEqual([str(self.device.pk)], self.device.natural_key())
|
|
1558
|
+
self.assertEqual(self.device, Device.objects.get_by_natural_key([self.device.pk]))
|
|
1559
|
+
|
|
1541
1560
|
with override_config(LOCATION_NAME_AS_NATURAL_KEY=True):
|
|
1542
1561
|
self.assertEqual([self.device.name, None, self.device.location.name], self.device.natural_key())
|
|
1543
1562
|
# self.assertEqual(
|
|
@@ -2899,6 +2918,11 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2899
2918
|
self.assertEqual(count, 1)
|
|
2900
2919
|
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
|
|
2901
2920
|
|
|
2921
|
+
# add a single instance which is already there
|
|
2922
|
+
count = interface.add_ip_addresses(ips[-1])
|
|
2923
|
+
self.assertEqual(count, 0)
|
|
2924
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
|
|
2925
|
+
|
|
2902
2926
|
# add multiple instances
|
|
2903
2927
|
count = interface.add_ip_addresses(ips[:5])
|
|
2904
2928
|
self.assertEqual(count, 5)
|
|
@@ -2906,6 +2930,20 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2906
2930
|
for ip in ips[:5]:
|
|
2907
2931
|
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
2908
2932
|
|
|
2933
|
+
# add multiple instances all of which are already there
|
|
2934
|
+
count = interface.add_ip_addresses(ips[:5])
|
|
2935
|
+
self.assertEqual(count, 0)
|
|
2936
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 6)
|
|
2937
|
+
for ip in ips[:5]:
|
|
2938
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
2939
|
+
|
|
2940
|
+
# add multiple IPs some of which are there
|
|
2941
|
+
count = interface.add_ip_addresses(ips[3:7])
|
|
2942
|
+
self.assertEqual(count, 2)
|
|
2943
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 8)
|
|
2944
|
+
for ip in ips[3:7]:
|
|
2945
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
2946
|
+
|
|
2909
2947
|
def test_remove_ip_addresses(self):
|
|
2910
2948
|
"""Test the `remove_ip_addresses` helper method on `Interface`"""
|
|
2911
2949
|
interface = Interface.objects.create(
|
|
@@ -2928,13 +2966,28 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2928
2966
|
self.assertEqual(count, 1)
|
|
2929
2967
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
|
|
2930
2968
|
|
|
2969
|
+
# remove a single instance which has already been removed
|
|
2970
|
+
count = interface.remove_ip_addresses(ips[-1])
|
|
2971
|
+
self.assertEqual(count, 0)
|
|
2972
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
|
|
2973
|
+
|
|
2931
2974
|
# remove multiple instances
|
|
2932
2975
|
count = interface.remove_ip_addresses(ips[:5])
|
|
2933
2976
|
self.assertEqual(count, 5)
|
|
2934
2977
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
|
|
2935
2978
|
|
|
2979
|
+
# remove multiple instances all which have already been removed
|
|
2980
|
+
count = interface.remove_ip_addresses(ips[:5])
|
|
2981
|
+
self.assertEqual(count, 0)
|
|
2982
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
|
|
2983
|
+
|
|
2984
|
+
# remove multiple instances some of which have already been removed
|
|
2985
|
+
count = interface.remove_ip_addresses(ips[3:7])
|
|
2986
|
+
self.assertEqual(count, 2)
|
|
2987
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 2)
|
|
2988
|
+
|
|
2936
2989
|
count = interface.remove_ip_addresses(ips)
|
|
2937
|
-
self.assertEqual(count,
|
|
2990
|
+
self.assertEqual(count, 2)
|
|
2938
2991
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 0)
|
|
2939
2992
|
|
|
2940
2993
|
# Test the pre_delete signal for IPAddressToInterface instances
|
|
@@ -2942,10 +2995,16 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2942
2995
|
self.device.primary_ip4 = interface.ip_addresses.all().filter(ip_version=4).first()
|
|
2943
2996
|
self.device.primary_ip6 = interface.ip_addresses.all().filter(ip_version=6).first()
|
|
2944
2997
|
self.device.save()
|
|
2945
|
-
|
|
2998
|
+
|
|
2999
|
+
count = interface.remove_ip_addresses(self.device.primary_ip4)
|
|
3000
|
+
self.assertEqual(count, 1)
|
|
2946
3001
|
self.device.refresh_from_db()
|
|
2947
3002
|
self.assertEqual(self.device.primary_ip4, None)
|
|
2948
|
-
|
|
3003
|
+
# NOTE: This effectively tests what happens when you pass remove_ip_addresses None; it
|
|
3004
|
+
# NOTE: does not remove a v6 address, because there are no v6 IPs created in this test
|
|
3005
|
+
# NOTE: class.
|
|
3006
|
+
count = interface.remove_ip_addresses(self.device.primary_ip6)
|
|
3007
|
+
self.assertEqual(count, 0)
|
|
2949
3008
|
self.device.refresh_from_db()
|
|
2950
3009
|
self.assertEqual(self.device.primary_ip6, None)
|
|
2951
3010
|
|