nautobot 3.0.0a2__py3-none-any.whl → 3.0.0rc1__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/choices.py +4 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +13 -1
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
- nautobot/circuits/views.py +6 -2
- 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 +2 -0
- nautobot/core/api/views.py +12 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/choices.py +1 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
- nautobot/core/cli/migrate_deprecated_templates.py +227 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/jobs/__init__.py +62 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/__init__.py +11 -0
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/models/utils.py +1 -1
- nautobot/core/settings.py +35 -19
- nautobot/core/settings.yaml +17 -33
- 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/base.html +1 -2
- nautobot/core/templates/admin/change_list.html +9 -12
- nautobot/core/templates/admin/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- nautobot/core/templates/base_django.html +1 -2
- 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/header_extra_content_table.html +1 -1
- nautobot/core/templates/components/panel/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +6 -7
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +22 -9
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +6 -5
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- 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_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_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/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 +4 -5
- nautobot/core/templates/graphene/graphiql.html +7 -8
- 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/footer.html +3 -1
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +11 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/media.html +14 -0
- nautobot/core/templates/inc/nav_menu.html +3 -9
- nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -11
- nautobot/core/templates/redoc_ui.html +7 -0
- nautobot/core/templates/rest_framework/api.html +103 -2
- 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 +37 -4
- nautobot/core/templates/utilities/theme_preview.html +19 -3
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +76 -18
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +41 -17
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/utils.py +18 -4
- nautobot/core/testing/views.py +104 -13
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- nautobot/core/tests/integration/test_filters.py +48 -11
- nautobot/core/tests/integration/test_theme.py +22 -21
- nautobot/core/tests/nautobot_config.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +8 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +27 -28
- 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 +144 -3
- 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_renderers.py +59 -0
- nautobot/core/tests/test_settings_schema.py +1 -0
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +62 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_titles.py +0 -16
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +123 -4
- nautobot/core/tests/test_utils.py +72 -5
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +70 -29
- nautobot/core/ui/bulk_buttons.py +1 -1
- nautobot/core/ui/choices.py +143 -27
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/echarts.py +15 -20
- nautobot/core/ui/object_detail.py +143 -55
- nautobot/core/ui/titles.py +3 -6
- nautobot/core/urls.py +20 -9
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +28 -18
- nautobot/core/utils/lookup.py +49 -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 +45 -22
- nautobot/core/views/renderers.py +4 -3
- 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 +3 -14
- 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 +3 -13
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +65 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/factory.py +1 -1
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
- nautobot/dcim/forms.py +90 -4
- nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
- nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
- nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
- nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
- nautobot/dcim/migrations/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
- nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
- nautobot/dcim/models/device_component_templates.py +33 -1
- nautobot/dcim/models/device_components.py +98 -64
- nautobot/dcim/models/devices.py +30 -20
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +18 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/tables/racks.py +0 -2
- nautobot/dcim/tables/template_code.py +15 -15
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_trace.html +0 -4
- nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
- nautobot/dcim/templates/dcim/consoleport.html +7 -6
- nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- 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.html +1 -1
- nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
- 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 +10 -9
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
- nautobot/dcim/templates/dcim/interface.html +35 -7
- nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/templates/dcim/inventoryitem.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/module/base.html +49 -9
- nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
- nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/module_frontports.html +1 -1
- nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
- nautobot/dcim/templates/dcim/module_list.html +57 -8
- nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
- nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/module_powerports.html +1 -1
- nautobot/dcim/templates/dcim/module_rearports.html +1 -1
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +6 -5
- nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
- nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
- nautobot/dcim/templates/dcim/rearport.html +8 -7
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
- nautobot/dcim/tests/integration/test_controller.py +4 -6
- nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
- nautobot/dcim/tests/integration/test_create_device.py +0 -2
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
- nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
- nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
- nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
- nautobot/dcim/tests/test_api.py +194 -6
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +55 -7
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +328 -4
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +132 -29
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +777 -397
- nautobot/extras/api/views.py +60 -45
- nautobot/extras/choices.py +2 -13
- nautobot/extras/datasources/git.py +3 -1
- 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} +33 -48
- nautobot/extras/forms/forms.py +14 -15
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/jobs.py +2 -0
- nautobot/extras/jobs_ui.py +4 -3
- nautobot/extras/management/__init__.py +11 -0
- nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
- 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/migrations/0131_configcontext_device_families.py +18 -0
- nautobot/extras/models/__init__.py +1 -2
- nautobot/extras/models/approvals.py +33 -14
- nautobot/extras/models/change_logging.py +4 -0
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +60 -4
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +23 -2
- 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/marketplace_manifest.yml +49 -1
- 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 -9
- nautobot/extras/querysets.py +8 -0
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +64 -68
- nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
- 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/configcontext_update.html +1 -0
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- 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/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 -39
- nautobot/extras/templates/extras/plugin_detail.html +29 -24
- 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/secret_create.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_computedfields.py +8 -9
- nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +9 -10
- nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/integration/test_relationships.py +0 -2
- nautobot/extras/tests/test_api.py +90 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +59 -5
- 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 +57 -22
- 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 +51 -33
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_utils.py +3 -4
- nautobot/extras/tests/test_views.py +52 -112
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +164 -71
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +53 -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 +19 -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_ip_addresses.html +1 -1
- nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
- nautobot/ipam/templates/ipam/namespace_update.html +15 -0
- nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- 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 +36 -1
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +44 -2
- 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 +53 -11
- nautobot/load_balancers/__init__.py +0 -0
- nautobot/load_balancers/api/__init__.py +1 -0
- nautobot/load_balancers/api/serializers.py +75 -0
- nautobot/load_balancers/api/urls.py +23 -0
- nautobot/load_balancers/api/views.py +61 -0
- nautobot/load_balancers/apps.py +17 -0
- nautobot/load_balancers/choices.py +167 -0
- nautobot/load_balancers/filters.py +225 -0
- nautobot/load_balancers/forms.py +532 -0
- nautobot/load_balancers/management/commands/__init__.py +0 -0
- nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
- nautobot/load_balancers/migrations/0001_initial.py +465 -0
- nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
- nautobot/load_balancers/migrations/__init__.py +0 -0
- nautobot/load_balancers/models.py +423 -0
- nautobot/load_balancers/navigation.py +80 -0
- nautobot/load_balancers/tables.py +255 -0
- nautobot/load_balancers/tests/__init__.py +474 -0
- nautobot/load_balancers/tests/test_api.py +353 -0
- nautobot/load_balancers/tests/test_filters.py +134 -0
- nautobot/load_balancers/tests/test_forms.py +266 -0
- nautobot/load_balancers/tests/test_models.py +195 -0
- nautobot/load_balancers/tests/test_views.py +229 -0
- nautobot/load_balancers/urls.py +17 -0
- nautobot/load_balancers/views.py +248 -0
- nautobot/project-static/dist/css/github-dark.min.css +10 -0
- nautobot/project-static/dist/css/github.min.css +10 -0
- nautobot/project-static/dist/css/nautobot.css +1 -11
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/libraries.js +1 -1
- nautobot/project-static/dist/js/libraries.js.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/forms.js +13 -0
- nautobot/project-static/js/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-shield.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/package-lock.json +87 -4
- nautobot/ui/package.json +2 -1
- nautobot/ui/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -1
- nautobot/ui/src/js/select2.js +53 -2
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +112 -30
- nautobot/ui/webpack.config.js +13 -0
- nautobot/users/templates/users/preferences.html +11 -2
- 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/filters.py +6 -1
- 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_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_filters.py +10 -1
- nautobot/virtualization/tests/test_models.py +45 -4
- nautobot/virtualization/views.py +4 -1
- 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 +219 -0
- nautobot/vpn/filters.py +234 -0
- nautobot/vpn/forms.py +487 -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 +535 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +383 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +336 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +293 -0
- nautobot/vpn/tests/test_models.py +147 -0
- nautobot/vpn/tests/test_views.py +300 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +495 -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.0rc1.dist-info}/METADATA +15 -15
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
- nautobot/circuits/templates/circuits/circuit.html +0 -2
- nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
- nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/circuit_update.html +0 -1
- nautobot/circuits/templates/circuits/circuittermination.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
- nautobot/circuits/templates/circuits/circuittype.html +0 -2
- nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
- nautobot/circuits/templates/circuits/provider.html +0 -2
- nautobot/circuits/templates/circuits/provider_edit.html +0 -2
- nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
- nautobot/circuits/templates/circuits/provider_update.html +0 -1
- nautobot/circuits/templates/circuits/providernetwork.html +0 -2
- nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
- nautobot/core/templates/buttons/import.html +0 -9
- nautobot/data_validation/template_content.py +0 -42
- nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/dcim/templates/dcim/controller/base.html +0 -2
- nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/device/base.html +0 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
- nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
- nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
- nautobot/dcim/templates/dcim/device/frontports.html +0 -2
- nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
- nautobot/dcim/templates/dcim/device/inventory.html +0 -2
- nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
- nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
- nautobot/dcim/templates/dcim/device/powerports.html +0 -2
- nautobot/dcim/templates/dcim/device/rearports.html +0 -2
- nautobot/dcim/templates/dcim/device/wireless.html +0 -2
- nautobot/dcim/templates/dcim/device_component.html +0 -2
- nautobot/dcim/templates/dcim/device_edit.html +0 -2
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/devicetype.html +0 -2
- nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
- nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
- nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/location.html +0 -2
- nautobot/dcim/templates/dcim/location_edit.html +0 -2
- nautobot/dcim/templates/dcim/location_retrieve.html +0 -243
- nautobot/dcim/templates/dcim/locationtype.html +0 -2
- nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/manufacturer.html +0 -2
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
- nautobot/dcim/templates/dcim/platform.html +0 -2
- nautobot/dcim/templates/dcim/powerfeed.html +0 -2
- nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/rack.html +0 -2
- nautobot/dcim/templates/dcim/rack_edit.html +0 -2
- nautobot/dcim/templates/dcim/rackgroup.html +0 -2
- nautobot/dcim/templates/dcim/rackreservation.html +0 -2
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
- nautobot/dcim/ui.py +0 -29
- nautobot/extras/templates/extras/computedfield.html +0 -2
- nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
- nautobot/extras/templates/extras/configcontext.html +0 -2
- nautobot/extras/templates/extras/configcontext_edit.html +0 -2
- nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
- nautobot/extras/templates/extras/configcontextschema.html +0 -2
- nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
- nautobot/extras/templates/extras/contact_retrieve.html +0 -2
- nautobot/extras/templates/extras/customfield.html +0 -2
- nautobot/extras/templates/extras/customfield_edit.html +0 -2
- nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
- nautobot/extras/templates/extras/customlink.html +0 -2
- nautobot/extras/templates/extras/dynamicgroup.html +0 -2
- nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
- nautobot/extras/templates/extras/exporttemplate.html +0 -2
- nautobot/extras/templates/extras/gitrepository.html +0 -2
- nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
- nautobot/extras/templates/extras/graphqlquery.html +0 -2
- nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -97
- nautobot/extras/templates/extras/job_detail.html +0 -2
- nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
- nautobot/extras/templates/extras/jobhook.html +0 -2
- nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
- nautobot/extras/templates/extras/jobresult.html +0 -2
- nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
- nautobot/extras/templates/extras/note.html +0 -2
- nautobot/extras/templates/extras/note_retrieve.html +0 -1
- nautobot/extras/templates/extras/object_changelog.html +0 -2
- nautobot/extras/templates/extras/object_notes.html +0 -2
- nautobot/extras/templates/extras/objectchange.html +0 -2
- nautobot/extras/templates/extras/objectchange_list.html +0 -3
- nautobot/extras/templates/extras/relationship.html +0 -1
- nautobot/extras/templates/extras/secret.html +0 -1
- nautobot/extras/templates/extras/secret_edit.html +0 -1
- nautobot/extras/templates/extras/secretsgroup.html +0 -2
- nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
- nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
- nautobot/extras/templates/extras/status.html +0 -2
- nautobot/extras/templates/extras/tag.html +0 -2
- nautobot/extras/templates/extras/tag_edit.html +0 -2
- nautobot/extras/templates/extras/tag_retrieve.html +0 -2
- nautobot/extras/templates/extras/team_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
- nautobot/ipam/templates/ipam/prefix.html +0 -2
- nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
- nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/rir.html +0 -2
- nautobot/ipam/templates/ipam/routetarget.html +0 -1
- nautobot/ipam/templates/ipam/service.html +0 -2
- nautobot/ipam/templates/ipam/service_edit.html +0 -2
- nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/vlan.html +0 -2
- nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
- nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/vlangroup.html +0 -2
- nautobot/ipam/templates/ipam/vrf.html +0 -1
- nautobot/tenancy/templates/tenancy/tenant.html +0 -2
- nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
- nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
- nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
- nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
- nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
- /nautobot/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -13,7 +13,9 @@ from nautobot.core.testing import APITestCase, APIViewTestCases
|
|
|
13
13
|
from nautobot.core.testing.utils import generate_random_device_asset_tag_of_specified_size, get_deletable_objects
|
|
14
14
|
from nautobot.dcim.choices import (
|
|
15
15
|
ConsolePortTypeChoices,
|
|
16
|
+
InterfaceDuplexChoices,
|
|
16
17
|
InterfaceModeChoices,
|
|
18
|
+
InterfaceSpeedChoices,
|
|
17
19
|
InterfaceTypeChoices,
|
|
18
20
|
PortTypeChoices,
|
|
19
21
|
PowerFeedBreakerPoleChoices,
|
|
@@ -1177,6 +1179,7 @@ class PowerOutletTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins
|
|
|
1177
1179
|
|
|
1178
1180
|
class InterfaceTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.BasePortTemplateTestMixin):
|
|
1179
1181
|
model = InterfaceTemplate
|
|
1182
|
+
choices_fields = ["duplex", "type"]
|
|
1180
1183
|
modular_component_create_data = {"type": InterfaceTypeChoices.TYPE_1GE_FIXED}
|
|
1181
1184
|
|
|
1182
1185
|
@classmethod
|
|
@@ -1200,6 +1203,62 @@ class InterfaceTemplateTest(Mixins.ModularDeviceComponentTemplateMixin, Mixins.B
|
|
|
1200
1203
|
},
|
|
1201
1204
|
]
|
|
1202
1205
|
|
|
1206
|
+
def test_create_base_t_with_speed_and_duplex(self):
|
|
1207
|
+
self.add_permissions("dcim.add_interfacetemplate", "dcim.view_interfacetemplate", "dcim.view_devicetype")
|
|
1208
|
+
url = self._get_list_url()
|
|
1209
|
+
payload = {
|
|
1210
|
+
"device_type": self.device_type.pk,
|
|
1211
|
+
"name": "Eth1",
|
|
1212
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
1213
|
+
"mgmt_only": False,
|
|
1214
|
+
"speed": InterfaceSpeedChoices.SPEED_1G,
|
|
1215
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
1216
|
+
}
|
|
1217
|
+
response = self.client.post(url, data=payload, format="json", **self.header)
|
|
1218
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1219
|
+
obj = InterfaceTemplate.objects.get(pk=response.data["id"]) # type: ignore[index]
|
|
1220
|
+
self.assertEqual(obj.speed, InterfaceSpeedChoices.SPEED_1G)
|
|
1221
|
+
self.assertEqual(obj.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
|
|
1222
|
+
|
|
1223
|
+
def test_create_sfp_with_duplex_rejected(self):
|
|
1224
|
+
self.add_permissions("dcim.add_interfacetemplate", "dcim.view_interfacetemplate", "dcim.view_devicetype")
|
|
1225
|
+
url = self._get_list_url()
|
|
1226
|
+
payload = {
|
|
1227
|
+
"device_type": self.device_type.pk,
|
|
1228
|
+
"name": "SFP1",
|
|
1229
|
+
"type": InterfaceTypeChoices.TYPE_1GE_SFP,
|
|
1230
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
1231
|
+
}
|
|
1232
|
+
response = self.client.post(url, data=payload, format="json", **self.header)
|
|
1233
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1234
|
+
self.assertIn("duplex", response.data)
|
|
1235
|
+
|
|
1236
|
+
def test_create_lag_with_speed_rejected(self):
|
|
1237
|
+
self.add_permissions("dcim.add_interfacetemplate", "dcim.view_interfacetemplate", "dcim.view_devicetype")
|
|
1238
|
+
url = self._get_list_url()
|
|
1239
|
+
payload = {
|
|
1240
|
+
"device_type": self.device_type.pk,
|
|
1241
|
+
"name": "Port-Channel1",
|
|
1242
|
+
"type": InterfaceTypeChoices.TYPE_LAG,
|
|
1243
|
+
"speed": InterfaceSpeedChoices.SPEED_1G,
|
|
1244
|
+
}
|
|
1245
|
+
response = self.client.post(url, data=payload, format="json", **self.header)
|
|
1246
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1247
|
+
self.assertIn("speed", response.data)
|
|
1248
|
+
|
|
1249
|
+
def test_create_virtual_with_speed_rejected(self):
|
|
1250
|
+
self.add_permissions("dcim.add_interfacetemplate", "dcim.view_interfacetemplate", "dcim.view_devicetype")
|
|
1251
|
+
url = self._get_list_url()
|
|
1252
|
+
payload = {
|
|
1253
|
+
"device_type": self.device_type.pk,
|
|
1254
|
+
"name": "V0",
|
|
1255
|
+
"type": InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
1256
|
+
"speed": InterfaceSpeedChoices.SPEED_1G,
|
|
1257
|
+
}
|
|
1258
|
+
response = self.client.post(url, data=payload, format="json", **self.header)
|
|
1259
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
1260
|
+
self.assertIn("speed", response.data)
|
|
1261
|
+
|
|
1203
1262
|
|
|
1204
1263
|
class FrontPortTemplateTest(Mixins.BasePortTemplateTestMixin):
|
|
1205
1264
|
model = FrontPortTemplate
|
|
@@ -1471,6 +1530,9 @@ class PlatformTest(APIViewTestCases.APIViewTestCase):
|
|
|
1471
1530
|
class DeviceTest(APIViewTestCases.APIViewTestCase):
|
|
1472
1531
|
model = Device
|
|
1473
1532
|
choices_fields = ["face"]
|
|
1533
|
+
validation_excluded_fields = [
|
|
1534
|
+
"software_image_files", # M2M field, excluded by default
|
|
1535
|
+
]
|
|
1474
1536
|
|
|
1475
1537
|
@classmethod
|
|
1476
1538
|
def setUpTestData(cls):
|
|
@@ -2167,7 +2229,10 @@ class PowerOutletTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMix
|
|
|
2167
2229
|
class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
|
|
2168
2230
|
model = Interface
|
|
2169
2231
|
peer_termination_type = Interface
|
|
2170
|
-
choices_fields = ["mode", "type"]
|
|
2232
|
+
choices_fields = ["duplex", "mode", "type"]
|
|
2233
|
+
validation_excluded_fields = [
|
|
2234
|
+
"tagged_vlans", # M2M field, excluded by default
|
|
2235
|
+
]
|
|
2171
2236
|
|
|
2172
2237
|
@classmethod
|
|
2173
2238
|
def setUpTestData(cls):
|
|
@@ -2208,14 +2273,14 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2208
2273
|
Interface.objects.create(
|
|
2209
2274
|
device=cls.devices[0],
|
|
2210
2275
|
name="Test Interface 1",
|
|
2211
|
-
type=
|
|
2276
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2212
2277
|
status=non_default_status,
|
|
2213
2278
|
role=intf_role,
|
|
2214
2279
|
),
|
|
2215
2280
|
Interface.objects.create(
|
|
2216
2281
|
device=cls.devices[0],
|
|
2217
2282
|
name="Test Interface 2",
|
|
2218
|
-
type=
|
|
2283
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2219
2284
|
status=non_default_status,
|
|
2220
2285
|
),
|
|
2221
2286
|
Interface.objects.create(
|
|
@@ -2266,7 +2331,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2266
2331
|
{
|
|
2267
2332
|
"device": cls.devices[0].pk,
|
|
2268
2333
|
"name": "Test Interface 8",
|
|
2269
|
-
"type":
|
|
2334
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2270
2335
|
"status": interface_status.pk,
|
|
2271
2336
|
"role": intf_role.pk,
|
|
2272
2337
|
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
@@ -2277,7 +2342,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2277
2342
|
{
|
|
2278
2343
|
"device": cls.devices[0].pk,
|
|
2279
2344
|
"name": "Test Interface 9",
|
|
2280
|
-
"type":
|
|
2345
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2281
2346
|
"status": interface_status.pk,
|
|
2282
2347
|
"role": intf_role.pk,
|
|
2283
2348
|
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
@@ -2289,13 +2354,35 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2289
2354
|
{
|
|
2290
2355
|
"device": cls.devices[0].pk,
|
|
2291
2356
|
"name": "Test Interface 10",
|
|
2292
|
-
"type":
|
|
2357
|
+
"type": InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
2293
2358
|
"status": interface_status.pk,
|
|
2294
2359
|
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
2295
2360
|
"parent_interface": cls.interfaces[1].pk,
|
|
2296
2361
|
"tagged_vlans": [cls.vlans[0].pk, cls.vlans[1].pk],
|
|
2297
2362
|
"untagged_vlan": cls.vlans[2].pk,
|
|
2298
2363
|
},
|
|
2364
|
+
{
|
|
2365
|
+
"device": cls.devices[0].pk,
|
|
2366
|
+
"name": "Test Interface 11",
|
|
2367
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2368
|
+
"status": interface_status.pk,
|
|
2369
|
+
"speed": InterfaceSpeedChoices.SPEED_1G,
|
|
2370
|
+
},
|
|
2371
|
+
{
|
|
2372
|
+
"device": cls.devices[0].pk,
|
|
2373
|
+
"name": "Test Interface 12",
|
|
2374
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2375
|
+
"status": interface_status.pk,
|
|
2376
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
2377
|
+
},
|
|
2378
|
+
{
|
|
2379
|
+
"device": cls.devices[0].pk,
|
|
2380
|
+
"name": "Test Interface 13",
|
|
2381
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2382
|
+
"status": interface_status.pk,
|
|
2383
|
+
"speed": InterfaceSpeedChoices.SPEED_1G,
|
|
2384
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
2385
|
+
},
|
|
2299
2386
|
]
|
|
2300
2387
|
|
|
2301
2388
|
cls.untagged_vlan_data = {
|
|
@@ -2504,6 +2591,105 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2504
2591
|
response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
|
|
2505
2592
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2506
2593
|
|
|
2594
|
+
def test_speed_duplex_invalid_by_type(self):
|
|
2595
|
+
"""Test that API rejects speed/duplex for disallowed interface types."""
|
|
2596
|
+
self.add_permissions("dcim.add_interface", "dcim.view_interface", "dcim.view_device", "extras.view_status")
|
|
2597
|
+
|
|
2598
|
+
# LAG disallows speed/duplex
|
|
2599
|
+
for field, value in (("speed", InterfaceSpeedChoices.SPEED_1G), ("duplex", InterfaceDuplexChoices.DUPLEX_FULL)):
|
|
2600
|
+
with self.subTest(if_type=InterfaceTypeChoices.TYPE_LAG, field=field):
|
|
2601
|
+
payload = {
|
|
2602
|
+
"device": self.devices[0].pk,
|
|
2603
|
+
"name": f"if-lag-{field}",
|
|
2604
|
+
"type": InterfaceTypeChoices.TYPE_LAG,
|
|
2605
|
+
"status": Status.objects.get_for_model(Interface).first().pk,
|
|
2606
|
+
field: value,
|
|
2607
|
+
}
|
|
2608
|
+
response = self.client.post(self._get_list_url(), data=payload, format="json", **self.header)
|
|
2609
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
2610
|
+
self.assertIn(field, response.data)
|
|
2611
|
+
|
|
2612
|
+
# Virtual/wireless disallow speed/duplex
|
|
2613
|
+
for if_type in (InterfaceTypeChoices.TYPE_VIRTUAL, InterfaceTypeChoices.TYPE_80211AC):
|
|
2614
|
+
for field, value in (
|
|
2615
|
+
("speed", InterfaceSpeedChoices.SPEED_1G),
|
|
2616
|
+
("duplex", InterfaceDuplexChoices.DUPLEX_FULL),
|
|
2617
|
+
):
|
|
2618
|
+
with self.subTest(if_type=if_type, field=field):
|
|
2619
|
+
payload = {
|
|
2620
|
+
"device": self.devices[0].pk,
|
|
2621
|
+
"name": f"if-{if_type}-{field}",
|
|
2622
|
+
"type": if_type,
|
|
2623
|
+
"status": Status.objects.get_for_model(Interface).first().pk,
|
|
2624
|
+
field: value,
|
|
2625
|
+
}
|
|
2626
|
+
response = self.client.post(self._get_list_url(), data=payload, format="json", **self.header)
|
|
2627
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
2628
|
+
self.assertIn(field, response.data)
|
|
2629
|
+
|
|
2630
|
+
# Optical disallows duplex
|
|
2631
|
+
with self.subTest(if_type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS, field="duplex"):
|
|
2632
|
+
payload = {
|
|
2633
|
+
"device": self.devices[0].pk,
|
|
2634
|
+
"name": "if-opt-duplex",
|
|
2635
|
+
"type": InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
2636
|
+
"status": Status.objects.get_for_model(Interface).first().pk,
|
|
2637
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
2638
|
+
}
|
|
2639
|
+
response = self.client.post(self._get_list_url(), data=payload, format="json", **self.header)
|
|
2640
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
2641
|
+
self.assertIn("duplex", response.data)
|
|
2642
|
+
|
|
2643
|
+
def test_update_type_to_optical_fails_when_duplex_set(self):
|
|
2644
|
+
"""Test that changing a copper interface with duplex set to an optical type fails."""
|
|
2645
|
+
self.add_permissions("dcim.change_interface")
|
|
2646
|
+
interface = self.interfaces[0] # 1000base-t
|
|
2647
|
+
|
|
2648
|
+
# Ensure duplex is set on copper via API
|
|
2649
|
+
response = self.client.patch(
|
|
2650
|
+
self._get_detail_url(interface),
|
|
2651
|
+
data={"duplex": InterfaceDuplexChoices.DUPLEX_FULL},
|
|
2652
|
+
format="json",
|
|
2653
|
+
**self.header,
|
|
2654
|
+
)
|
|
2655
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2656
|
+
self.assertEqual(response.data["duplex"]["value"], InterfaceDuplexChoices.DUPLEX_FULL)
|
|
2657
|
+
|
|
2658
|
+
# Attempt to change type to optical while duplex remains set
|
|
2659
|
+
response = self.client.patch(
|
|
2660
|
+
self._get_detail_url(interface),
|
|
2661
|
+
data={"type": InterfaceTypeChoices.TYPE_10GE_SFP_PLUS},
|
|
2662
|
+
format="json",
|
|
2663
|
+
**self.header,
|
|
2664
|
+
)
|
|
2665
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
2666
|
+
self.assertIn("duplex", response.data)
|
|
2667
|
+
|
|
2668
|
+
def test_update_type_to_optical_succeeds_when_unsetting_duplex(self):
|
|
2669
|
+
"""Test that changing type with duplex set to optical while unsetting duplex in the same request succeeds."""
|
|
2670
|
+
self.add_permissions("dcim.change_interface")
|
|
2671
|
+
interface = self.interfaces[1] # 1000base-t
|
|
2672
|
+
|
|
2673
|
+
# Ensure duplex is set on copper first
|
|
2674
|
+
response = self.client.patch(
|
|
2675
|
+
self._get_detail_url(interface),
|
|
2676
|
+
data={"duplex": InterfaceDuplexChoices.DUPLEX_FULL},
|
|
2677
|
+
format="json",
|
|
2678
|
+
**self.header,
|
|
2679
|
+
)
|
|
2680
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2681
|
+
self.assertEqual(response.data["duplex"]["value"], InterfaceDuplexChoices.DUPLEX_FULL)
|
|
2682
|
+
|
|
2683
|
+
# Change to optical and unset duplex in same call
|
|
2684
|
+
response = self.client.patch(
|
|
2685
|
+
self._get_detail_url(interface),
|
|
2686
|
+
data={"type": InterfaceTypeChoices.TYPE_10GE_SFP_PLUS, "duplex": ""},
|
|
2687
|
+
format="json",
|
|
2688
|
+
**self.header,
|
|
2689
|
+
)
|
|
2690
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2691
|
+
self.assertIsNone(response.data["duplex"])
|
|
2692
|
+
|
|
2507
2693
|
|
|
2508
2694
|
class FrontPortTest(Mixins.BasePortTestMixin):
|
|
2509
2695
|
model = FrontPort
|
|
@@ -3588,6 +3774,7 @@ class DeviceTypeToSoftwareImageFileTestCase(
|
|
|
3588
3774
|
|
|
3589
3775
|
class ControllerTestCase(APIViewTestCases.APIViewTestCase):
|
|
3590
3776
|
model = Controller
|
|
3777
|
+
choices_fields = ("capabilities",)
|
|
3591
3778
|
|
|
3592
3779
|
def get_deletable_object(self):
|
|
3593
3780
|
# This method is used in `test_recreate_object_csv`,
|
|
@@ -3647,6 +3834,7 @@ class ControllerTestCase(APIViewTestCases.APIViewTestCase):
|
|
|
3647
3834
|
|
|
3648
3835
|
class ControllerManagedDeviceGroupTestCase(APIViewTestCases.APIViewTestCase):
|
|
3649
3836
|
model = ControllerManagedDeviceGroup
|
|
3837
|
+
choices_fields = ("capabilities",)
|
|
3650
3838
|
|
|
3651
3839
|
def get_deletable_object(self):
|
|
3652
3840
|
# 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()
|
|
@@ -11,7 +11,9 @@ from nautobot.dcim.choices import (
|
|
|
11
11
|
CableLengthUnitChoices,
|
|
12
12
|
CableTypeChoices,
|
|
13
13
|
DeviceFaceChoices,
|
|
14
|
+
InterfaceDuplexChoices,
|
|
14
15
|
InterfaceModeChoices,
|
|
16
|
+
InterfaceSpeedChoices,
|
|
15
17
|
InterfaceTypeChoices,
|
|
16
18
|
PortTypeChoices,
|
|
17
19
|
PowerFeedBreakerPoleChoices,
|
|
@@ -124,9 +126,10 @@ from nautobot.dcim.models import (
|
|
|
124
126
|
VirtualChassis,
|
|
125
127
|
VirtualDeviceContext,
|
|
126
128
|
)
|
|
127
|
-
from nautobot.extras.
|
|
129
|
+
from nautobot.extras.filter_mixins import RoleFilter, StatusFilter
|
|
128
130
|
from nautobot.extras.models import ExternalIntegration, Role, SecretsGroup, Status, Tag
|
|
129
|
-
from nautobot.
|
|
131
|
+
from nautobot.extras.tests.test_customfields_filters import CustomFieldsFilters
|
|
132
|
+
from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup, VRF, VRFDeviceAssignment
|
|
130
133
|
from nautobot.tenancy.models import Tenant
|
|
131
134
|
from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine
|
|
132
135
|
from nautobot.wireless.models import RadioProfile, WirelessNetwork
|
|
@@ -1039,7 +1042,9 @@ class PathEndpointModelTestMixin:
|
|
|
1039
1042
|
)
|
|
1040
1043
|
|
|
1041
1044
|
|
|
1042
|
-
class LocationTypeFilterSetTestCase(
|
|
1045
|
+
class LocationTypeFilterSetTestCase(
|
|
1046
|
+
FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin
|
|
1047
|
+
):
|
|
1043
1048
|
queryset = LocationType.objects.all()
|
|
1044
1049
|
filterset = LocationTypeFilterSet
|
|
1045
1050
|
generic_filter_tests = [
|
|
@@ -1076,7 +1081,10 @@ class LocationTypeFilterSetTestCase(FilterTestCases.FilterTestCase):
|
|
|
1076
1081
|
)
|
|
1077
1082
|
|
|
1078
1083
|
|
|
1079
|
-
class LocationFilterSetTestCase(
|
|
1084
|
+
class LocationFilterSetTestCase(
|
|
1085
|
+
FilterTestCases.FilterTestCase,
|
|
1086
|
+
FilterTestCases.TenancyFilterTestCaseMixin,
|
|
1087
|
+
):
|
|
1080
1088
|
queryset = Location.objects.all()
|
|
1081
1089
|
filterset = LocationFilterSet
|
|
1082
1090
|
tenancy_related_name = "locations"
|
|
@@ -1149,7 +1157,7 @@ class LocationFilterSetTestCase(FilterTestCases.FilterTestCase, FilterTestCases.
|
|
|
1149
1157
|
)
|
|
1150
1158
|
|
|
1151
1159
|
|
|
1152
|
-
class RackGroupTestCase(FilterTestCases.FilterTestCase):
|
|
1160
|
+
class RackGroupTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
|
|
1153
1161
|
queryset = RackGroup.objects.all()
|
|
1154
1162
|
filterset = RackGroupFilterSet
|
|
1155
1163
|
generic_filter_tests = [
|
|
@@ -1357,7 +1365,7 @@ class RackReservationTestCase(FilterTestCases.FilterTestCase, FilterTestCases.Te
|
|
|
1357
1365
|
common_test_data(cls)
|
|
1358
1366
|
|
|
1359
1367
|
|
|
1360
|
-
class ManufacturerTestCase(FilterTestCases.FilterTestCase):
|
|
1368
|
+
class ManufacturerTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
|
|
1361
1369
|
queryset = Manufacturer.objects.all()
|
|
1362
1370
|
filterset = ManufacturerFilterSet
|
|
1363
1371
|
generic_filter_tests = [
|
|
@@ -1393,7 +1401,7 @@ class DeviceFamilyTestCase(FilterTestCases.FilterTestCase):
|
|
|
1393
1401
|
]
|
|
1394
1402
|
|
|
1395
1403
|
|
|
1396
|
-
class DeviceTypeTestCase(FilterTestCases.FilterTestCase):
|
|
1404
|
+
class DeviceTypeTestCase(FilterTestCases.FilterTestCase, CustomFieldsFilters.CustomFieldsFilterSetTestCaseMixin):
|
|
1397
1405
|
queryset = DeviceType.objects.all()
|
|
1398
1406
|
filterset = DeviceTypeFilterSet
|
|
1399
1407
|
generic_filter_tests = [
|
|
@@ -1822,6 +1830,8 @@ class DeviceTestCase(
|
|
|
1822
1830
|
("vc_priority",),
|
|
1823
1831
|
("virtual_chassis", "virtual_chassis__id"),
|
|
1824
1832
|
("virtual_chassis", "virtual_chassis__name"),
|
|
1833
|
+
("vrfs", "vrfs__id"),
|
|
1834
|
+
("vrfs", "vrfs__rd"),
|
|
1825
1835
|
("wireless_networks", "controller_managed_device_group__wireless_networks__id"),
|
|
1826
1836
|
("wireless_networks", "controller_managed_device_group__wireless_networks__name"),
|
|
1827
1837
|
]
|
|
@@ -1928,6 +1938,14 @@ class DeviceTestCase(
|
|
|
1928
1938
|
virtual_chassis_2 = VirtualChassis.objects.create(name="vc2", master=devices[2])
|
|
1929
1939
|
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis_2, vc_position=1, vc_priority=1)
|
|
1930
1940
|
|
|
1941
|
+
# VRF assignment for filtering
|
|
1942
|
+
vrfs = (
|
|
1943
|
+
VRF.objects.create(name="VRF 1", rd="1:1"),
|
|
1944
|
+
VRF.objects.create(name="VRF 2", rd="1:2"),
|
|
1945
|
+
)
|
|
1946
|
+
VRFDeviceAssignment.objects.create(device=devices[0], vrf=vrfs[0])
|
|
1947
|
+
VRFDeviceAssignment.objects.create(device=devices[1], vrf=vrfs[1])
|
|
1948
|
+
|
|
1931
1949
|
def test_special_filters(self):
|
|
1932
1950
|
# TODO: Not a generic_filter_test because this is a single-value filter
|
|
1933
1951
|
with self.subTest("face"):
|
|
@@ -2247,6 +2265,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2247
2265
|
("name",),
|
|
2248
2266
|
("parent_interface", "parent_interface__id"),
|
|
2249
2267
|
("parent_interface", "parent_interface__name"),
|
|
2268
|
+
("speed",),
|
|
2269
|
+
("duplex",),
|
|
2250
2270
|
("role", "role__id"),
|
|
2251
2271
|
("role", "role__name"),
|
|
2252
2272
|
("status", "status__id"),
|
|
@@ -2326,6 +2346,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2326
2346
|
mtu=100,
|
|
2327
2347
|
status=interface_statuses[0],
|
|
2328
2348
|
untagged_vlan=vlans[0],
|
|
2349
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
2350
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
2329
2351
|
)
|
|
2330
2352
|
|
|
2331
2353
|
Interface.objects.filter(pk=cabled_interfaces[1].pk).update(
|
|
@@ -2335,6 +2357,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2335
2357
|
mtu=200,
|
|
2336
2358
|
status=interface_statuses[3],
|
|
2337
2359
|
untagged_vlan=vlans[1],
|
|
2360
|
+
speed=InterfaceSpeedChoices.SPEED_10G,
|
|
2361
|
+
duplex=InterfaceDuplexChoices.DUPLEX_HALF,
|
|
2338
2362
|
)
|
|
2339
2363
|
|
|
2340
2364
|
Interface.objects.filter(pk=cabled_interfaces[2].pk).update(
|
|
@@ -2348,6 +2372,16 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2348
2372
|
for interface in cabled_interfaces:
|
|
2349
2373
|
interface.refresh_from_db()
|
|
2350
2374
|
|
|
2375
|
+
# Additional optical interface for speed filtering (no duplex)
|
|
2376
|
+
Interface.objects.create(
|
|
2377
|
+
device=devices[2],
|
|
2378
|
+
name="Filter Optical IF",
|
|
2379
|
+
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
2380
|
+
status=interface_statuses[0],
|
|
2381
|
+
speed=InterfaceSpeedChoices.SPEED_10G,
|
|
2382
|
+
duplex="",
|
|
2383
|
+
)
|
|
2384
|
+
|
|
2351
2385
|
cable_statuses = Status.objects.get_for_model(Cable)
|
|
2352
2386
|
connected_status = cable_statuses.get(name="Connected")
|
|
2353
2387
|
|
|
@@ -2543,6 +2577,20 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2543
2577
|
params = {"mode": [InterfaceModeChoices.MODE_ACCESS]}
|
|
2544
2578
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
|
2545
2579
|
|
|
2580
|
+
def test_speed_multi(self):
|
|
2581
|
+
params = {"speed": [InterfaceSpeedChoices.SPEED_1G, InterfaceSpeedChoices.SPEED_10G]}
|
|
2582
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
2583
|
+
self.filterset(params, self.queryset).qs,
|
|
2584
|
+
self.queryset.filter(speed__in=params["speed"]),
|
|
2585
|
+
)
|
|
2586
|
+
|
|
2587
|
+
def test_speed_and_duplex(self):
|
|
2588
|
+
params = {"speed": [InterfaceSpeedChoices.SPEED_10G], "duplex": [InterfaceDuplexChoices.DUPLEX_HALF]}
|
|
2589
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
2590
|
+
self.filterset(params, self.queryset).qs,
|
|
2591
|
+
self.queryset.filter(speed__in=params["speed"], duplex__in=params["duplex"]),
|
|
2592
|
+
)
|
|
2593
|
+
|
|
2546
2594
|
def test_device_with_common_vc(self):
|
|
2547
2595
|
"""Assert only interfaces belonging to devices with common VC are returned"""
|
|
2548
2596
|
device_type = DeviceType.objects.first()
|