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
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
from constance.test import override_config
|
|
1
2
|
from django.test import TestCase
|
|
2
3
|
|
|
3
4
|
from nautobot.core.testing.forms import FormTestCases
|
|
4
5
|
from nautobot.core.testing.mixins import NautobotTestCaseMixin
|
|
5
|
-
from nautobot.dcim.choices import
|
|
6
|
+
from nautobot.dcim.choices import (
|
|
7
|
+
DeviceFaceChoices,
|
|
8
|
+
InterfaceDuplexChoices,
|
|
9
|
+
InterfaceModeChoices,
|
|
10
|
+
InterfaceSpeedChoices,
|
|
11
|
+
InterfaceTypeChoices,
|
|
12
|
+
RackWidthChoices,
|
|
13
|
+
)
|
|
14
|
+
from nautobot.dcim.constants import RACK_U_HEIGHT_DEFAULT
|
|
6
15
|
from nautobot.dcim.forms import (
|
|
7
16
|
DeviceFilterForm,
|
|
8
17
|
DeviceForm,
|
|
@@ -327,24 +336,56 @@ class RackTestCase(TestCase):
|
|
|
327
336
|
form = RackForm(data=data, instance=racks[0])
|
|
328
337
|
self.assertTrue(form.is_valid())
|
|
329
338
|
|
|
339
|
+
def test_rack_form_initial_u_height_default(self):
|
|
340
|
+
"""Test that RackForm sets initial u_height from default Constance config (42)."""
|
|
341
|
+
# Create a new form (not bound to an instance)
|
|
342
|
+
form = RackForm()
|
|
343
|
+
|
|
344
|
+
# The initial value should be 42 (default Constance config)
|
|
345
|
+
self.assertEqual(form.fields["u_height"].initial, RACK_U_HEIGHT_DEFAULT)
|
|
346
|
+
|
|
347
|
+
@override_config(RACK_DEFAULT_U_HEIGHT=48)
|
|
348
|
+
def test_rack_form_initial_u_height_custom(self):
|
|
349
|
+
"""Test that RackForm sets initial u_height from custom Constance config."""
|
|
350
|
+
# Create a new form (not bound to an instance)
|
|
351
|
+
form = RackForm()
|
|
352
|
+
|
|
353
|
+
# The initial value should be 48 (from Constance config)
|
|
354
|
+
self.assertEqual(form.fields["u_height"].initial, 48)
|
|
355
|
+
|
|
356
|
+
def test_rack_form_initial_u_height_not_set_on_edit(self):
|
|
357
|
+
"""Test that RackForm does NOT override u_height when editing an existing rack."""
|
|
358
|
+
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
359
|
+
status = Status.objects.get(name="Active")
|
|
360
|
+
|
|
361
|
+
# Create a rack with u_height of 24
|
|
362
|
+
rack = Rack.objects.create(name="Test Rack", location=location, status=status, u_height=24)
|
|
363
|
+
|
|
364
|
+
# Create a form bound to the existing rack
|
|
365
|
+
form = RackForm(instance=rack)
|
|
366
|
+
|
|
367
|
+
# The initial value should NOT be overridden - it should use the rack's actual value
|
|
368
|
+
# (The form will show the model instance's value, not the Constance config)
|
|
369
|
+
self.assertEqual(form.initial["u_height"], 24)
|
|
370
|
+
|
|
330
371
|
|
|
331
372
|
class InterfaceTestCase(NautobotTestCaseMixin, TestCase):
|
|
332
373
|
@classmethod
|
|
333
374
|
def setUpTestData(cls):
|
|
334
375
|
cls.device = Device.objects.first()
|
|
335
|
-
status = Status.objects.get_for_model(Interface).first()
|
|
376
|
+
cls.status = Status.objects.get_for_model(Interface).first()
|
|
336
377
|
cls.interface = Interface.objects.create(
|
|
337
378
|
device=cls.device,
|
|
338
379
|
name="test interface form 0.0",
|
|
339
380
|
type=InterfaceTypeChoices.TYPE_2GFC_SFP,
|
|
340
|
-
status=status,
|
|
381
|
+
status=cls.status,
|
|
341
382
|
)
|
|
342
383
|
cls.vlan = VLAN.objects.first()
|
|
343
384
|
cls.data = {
|
|
344
385
|
"device": cls.device.pk,
|
|
345
386
|
"name": "test interface form 0.0",
|
|
346
387
|
"type": InterfaceTypeChoices.TYPE_2GFC_SFP,
|
|
347
|
-
"status": status.pk,
|
|
388
|
+
"status": cls.status.pk,
|
|
348
389
|
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
349
390
|
"tagged_vlans": [cls.vlan.pk],
|
|
350
391
|
}
|
|
@@ -394,7 +435,6 @@ class InterfaceTestCase(NautobotTestCaseMixin, TestCase):
|
|
|
394
435
|
Assert that untagged_vlans field dropdown are populated correctly in InterfaceForm and InterfaceBulkEditForm,
|
|
395
436
|
and that the queryset is the same for both forms.
|
|
396
437
|
"""
|
|
397
|
-
status = Status.objects.get_for_model(Interface).first()
|
|
398
438
|
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
399
439
|
devices = Device.objects.all()[:3]
|
|
400
440
|
for device in devices:
|
|
@@ -405,19 +445,19 @@ class InterfaceTestCase(NautobotTestCaseMixin, TestCase):
|
|
|
405
445
|
device=devices[0],
|
|
406
446
|
name="Test Interface 1",
|
|
407
447
|
type=InterfaceTypeChoices.TYPE_2GFC_SFP,
|
|
408
|
-
status=status,
|
|
448
|
+
status=self.status,
|
|
409
449
|
),
|
|
410
450
|
Interface.objects.create(
|
|
411
451
|
device=devices[1],
|
|
412
452
|
name="Test Interface 2",
|
|
413
453
|
type=InterfaceTypeChoices.TYPE_LAG,
|
|
414
|
-
status=status,
|
|
454
|
+
status=self.status,
|
|
415
455
|
),
|
|
416
456
|
Interface.objects.create(
|
|
417
457
|
device=devices[2],
|
|
418
458
|
name="Test Interface 3",
|
|
419
459
|
type=InterfaceTypeChoices.TYPE_100ME_FIXED,
|
|
420
|
-
status=status,
|
|
460
|
+
status=self.status,
|
|
421
461
|
),
|
|
422
462
|
)
|
|
423
463
|
edit_form = InterfaceForm(data=self.data, instance=interfaces[0])
|
|
@@ -429,3 +469,65 @@ class InterfaceTestCase(NautobotTestCaseMixin, TestCase):
|
|
|
429
469
|
edit_form.fields["untagged_vlan"].queryset,
|
|
430
470
|
bulk_edit_form.fields["untagged_vlan"].queryset,
|
|
431
471
|
)
|
|
472
|
+
|
|
473
|
+
def test_interface_form_fields_and_blank(self):
|
|
474
|
+
data = {
|
|
475
|
+
"device": self.device.pk,
|
|
476
|
+
"name": self.interface.name,
|
|
477
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
478
|
+
"status": self.status.pk,
|
|
479
|
+
"speed": "", # blank should coerce to None
|
|
480
|
+
"duplex": "", # blank allowed
|
|
481
|
+
}
|
|
482
|
+
form = InterfaceForm(data=data, instance=self.interface)
|
|
483
|
+
self.assertIn("speed", form.fields)
|
|
484
|
+
self.assertIn("duplex", form.fields)
|
|
485
|
+
self.assertTrue(form.is_valid())
|
|
486
|
+
self.assertIsNone(form.cleaned_data["speed"]) # TypedChoiceField(empty->None)
|
|
487
|
+
self.assertEqual(form.cleaned_data["duplex"], "")
|
|
488
|
+
|
|
489
|
+
def test_interface_form_speed_choice_coerces_int(self):
|
|
490
|
+
speed_choice = InterfaceSpeedChoices.SPEED_10G
|
|
491
|
+
data = {
|
|
492
|
+
"device": self.device.pk,
|
|
493
|
+
"name": self.interface.name,
|
|
494
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
495
|
+
"status": self.status.pk,
|
|
496
|
+
# Posted value is a string; TypedChoiceField should coerce to int
|
|
497
|
+
"speed": str(speed_choice),
|
|
498
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_FULL,
|
|
499
|
+
}
|
|
500
|
+
form = InterfaceForm(data=data, instance=self.interface)
|
|
501
|
+
self.assertTrue(form.is_valid())
|
|
502
|
+
self.assertIsInstance(form.cleaned_data["speed"], int)
|
|
503
|
+
self.assertEqual(form.cleaned_data["speed"], speed_choice)
|
|
504
|
+
self.assertEqual(form.cleaned_data["duplex"], InterfaceDuplexChoices.DUPLEX_FULL)
|
|
505
|
+
|
|
506
|
+
def test_interface_create_form_blank_and_choice(self):
|
|
507
|
+
# Blank speed
|
|
508
|
+
data_blank = {
|
|
509
|
+
"device": self.device.pk,
|
|
510
|
+
"name_pattern": "eth1",
|
|
511
|
+
"status": self.status.pk,
|
|
512
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
513
|
+
"speed": "",
|
|
514
|
+
"duplex": "",
|
|
515
|
+
}
|
|
516
|
+
form_blank = InterfaceCreateForm(data_blank)
|
|
517
|
+
self.assertTrue(form_blank.is_valid())
|
|
518
|
+
self.assertIsNone(form_blank.cleaned_data["speed"]) # TypedChoiceField(empty->None)
|
|
519
|
+
|
|
520
|
+
# With a specific choice
|
|
521
|
+
speed_choice = InterfaceSpeedChoices.SPEED_1G
|
|
522
|
+
data_choice = {
|
|
523
|
+
"device": self.device.pk,
|
|
524
|
+
"name_pattern": "eth2",
|
|
525
|
+
"status": self.status.pk,
|
|
526
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
527
|
+
"speed": str(speed_choice),
|
|
528
|
+
"duplex": InterfaceDuplexChoices.DUPLEX_AUTO,
|
|
529
|
+
}
|
|
530
|
+
form_choice = InterfaceCreateForm(data_choice)
|
|
531
|
+
self.assertTrue(form_choice.is_valid())
|
|
532
|
+
self.assertEqual(form_choice.cleaned_data["speed"], speed_choice)
|
|
533
|
+
self.assertEqual(form_choice.cleaned_data["duplex"], InterfaceDuplexChoices.DUPLEX_AUTO)
|
|
@@ -3,7 +3,7 @@ from django.test import override_settings
|
|
|
3
3
|
|
|
4
4
|
from nautobot.core.graphql import execute_query
|
|
5
5
|
from nautobot.core.testing import create_test_user, TestCase
|
|
6
|
-
from nautobot.dcim.choices import InterfaceTypeChoices
|
|
6
|
+
from nautobot.dcim.choices import InterfaceDuplexChoices, InterfaceSpeedChoices, InterfaceTypeChoices
|
|
7
7
|
from nautobot.dcim.models import (
|
|
8
8
|
Controller,
|
|
9
9
|
Device,
|
|
@@ -52,6 +52,22 @@ class GraphQLTestCase(TestCase):
|
|
|
52
52
|
type=InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
53
53
|
mac_address=None,
|
|
54
54
|
),
|
|
55
|
+
Interface.objects.create(
|
|
56
|
+
device=self.device,
|
|
57
|
+
name="eth2",
|
|
58
|
+
status=interface_status,
|
|
59
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
60
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
61
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
62
|
+
),
|
|
63
|
+
Interface.objects.create(
|
|
64
|
+
device=self.device,
|
|
65
|
+
name="eth3",
|
|
66
|
+
status=interface_status,
|
|
67
|
+
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
68
|
+
speed=InterfaceSpeedChoices.SPEED_10G,
|
|
69
|
+
duplex="",
|
|
70
|
+
),
|
|
55
71
|
)
|
|
56
72
|
for interface in self.interfaces:
|
|
57
73
|
interface.validated_save()
|
|
@@ -131,3 +147,30 @@ class GraphQLTestCase(TestCase):
|
|
|
131
147
|
self.assertIsNone(resp.errors)
|
|
132
148
|
for device in resp.data["devices"]:
|
|
133
149
|
self.assertNotEqual(device["serial"], "")
|
|
150
|
+
|
|
151
|
+
with self.subTest("interface speed/duplex fields on device query"):
|
|
152
|
+
query = "query { devices { name interfaces { name speed duplex } } }"
|
|
153
|
+
resp = execute_query(query, user=self.user)
|
|
154
|
+
self.assertFalse(resp.errors)
|
|
155
|
+
interfaces = [i for d in resp.data["devices"] if d["name"] == self.device.name for i in d["interfaces"]]
|
|
156
|
+
eth2 = next(i for i in interfaces if i["name"] == "eth2")
|
|
157
|
+
eth3 = next(i for i in interfaces if i["name"] == "eth3")
|
|
158
|
+
self.assertEqual(eth2["speed"], InterfaceSpeedChoices.SPEED_1G)
|
|
159
|
+
self.assertEqual(eth2["duplex"].lower(), InterfaceDuplexChoices.DUPLEX_FULL)
|
|
160
|
+
self.assertEqual(eth3["speed"], InterfaceSpeedChoices.SPEED_10G)
|
|
161
|
+
self.assertEqual(eth3["duplex"], None)
|
|
162
|
+
|
|
163
|
+
with self.subTest("interfaces root filter by speed and duplex"):
|
|
164
|
+
query = f"query {{ interfaces(speed: {InterfaceSpeedChoices.SPEED_1G}) {{ name }} }}"
|
|
165
|
+
resp = execute_query(query, user=self.user)
|
|
166
|
+
self.assertFalse(resp.errors)
|
|
167
|
+
names = {i["name"] for i in resp.data["interfaces"]}
|
|
168
|
+
self.assertIn("eth2", names)
|
|
169
|
+
self.assertNotIn("eth3", names)
|
|
170
|
+
|
|
171
|
+
query = 'query { interfaces(duplex: ["full"]) { name } }'
|
|
172
|
+
resp = execute_query(query, user=self.user)
|
|
173
|
+
self.assertFalse(resp.errors)
|
|
174
|
+
names = {i["name"] for i in resp.data["interfaces"]}
|
|
175
|
+
self.assertIn("eth2", names)
|
|
176
|
+
self.assertNotIn("eth3", names)
|
|
@@ -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,7 +17,10 @@ from nautobot.dcim.choices import (
|
|
|
16
17
|
CableTypeChoices,
|
|
17
18
|
ConsolePortTypeChoices,
|
|
18
19
|
DeviceFaceChoices,
|
|
20
|
+
DeviceUniquenessChoices,
|
|
21
|
+
InterfaceDuplexChoices,
|
|
19
22
|
InterfaceModeChoices,
|
|
23
|
+
InterfaceSpeedChoices,
|
|
20
24
|
InterfaceTypeChoices,
|
|
21
25
|
PortTypeChoices,
|
|
22
26
|
PowerFeedBreakerPoleChoices,
|
|
@@ -725,6 +729,101 @@ class InterfaceTemplateTestCase(ModularDeviceComponentTemplateTestCaseMixin, Tes
|
|
|
725
729
|
first_status = Status.objects.get_for_model(Interface).first()
|
|
726
730
|
self.assertIsNotNone(device_2.interfaces.get(name="Test_Template_1").status, first_status)
|
|
727
731
|
|
|
732
|
+
def test_speed_disallowed_for_lag_virtual_wireless(self):
|
|
733
|
+
"""speed must be None for LAG, virtual, and wireless templates."""
|
|
734
|
+
manufacturer = Manufacturer.objects.first()
|
|
735
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="SpeedGuard 1000")
|
|
736
|
+
|
|
737
|
+
for if_type in (
|
|
738
|
+
InterfaceTypeChoices.TYPE_LAG,
|
|
739
|
+
InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
740
|
+
InterfaceTypeChoices.TYPE_80211N,
|
|
741
|
+
):
|
|
742
|
+
with self.subTest(if_type=if_type):
|
|
743
|
+
with self.assertRaises(ValidationError) as cm:
|
|
744
|
+
InterfaceTemplate(
|
|
745
|
+
device_type=device_type,
|
|
746
|
+
name=f"bad-{if_type}",
|
|
747
|
+
type=if_type,
|
|
748
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
749
|
+
).full_clean()
|
|
750
|
+
self.assertIn("Speed is not applicable to this interface type.", str(cm.exception))
|
|
751
|
+
|
|
752
|
+
def test_duplex_disallowed_for_lag_virtual_wireless(self):
|
|
753
|
+
"""duplex must be blank for LAG, virtual, and wireless templates."""
|
|
754
|
+
manufacturer = Manufacturer.objects.first()
|
|
755
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="DuplexGuard 1000")
|
|
756
|
+
|
|
757
|
+
for itype in (
|
|
758
|
+
InterfaceTypeChoices.TYPE_LAG,
|
|
759
|
+
InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
760
|
+
InterfaceTypeChoices.TYPE_80211N,
|
|
761
|
+
):
|
|
762
|
+
with self.assertRaises(ValidationError):
|
|
763
|
+
InterfaceTemplate(
|
|
764
|
+
device_type=device_type,
|
|
765
|
+
name=f"bad-{itype}",
|
|
766
|
+
type=itype,
|
|
767
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
768
|
+
).full_clean()
|
|
769
|
+
|
|
770
|
+
def test_duplex_disallowed_for_non_base_t(self):
|
|
771
|
+
"""duplex must be blank for non-BASE-T physical types (e.g., SFP)."""
|
|
772
|
+
manufacturer = Manufacturer.objects.first()
|
|
773
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="SfpGuard 1000")
|
|
774
|
+
|
|
775
|
+
with self.assertRaises(ValidationError) as cm:
|
|
776
|
+
InterfaceTemplate(
|
|
777
|
+
device_type=device_type,
|
|
778
|
+
name="sfp0",
|
|
779
|
+
type=InterfaceTypeChoices.TYPE_1GE_SFP,
|
|
780
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
781
|
+
).full_clean()
|
|
782
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
|
|
783
|
+
|
|
784
|
+
def test_duplex_and_speed_allowed_for_base_t(self):
|
|
785
|
+
"""BASE-T physical types accept duplex and speed values."""
|
|
786
|
+
manufacturer = Manufacturer.objects.first()
|
|
787
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="CopperOK 1000")
|
|
788
|
+
|
|
789
|
+
tmpl = InterfaceTemplate(
|
|
790
|
+
device_type=device_type,
|
|
791
|
+
name="eth0",
|
|
792
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
793
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
794
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
795
|
+
)
|
|
796
|
+
tmpl.full_clean() # should not raise
|
|
797
|
+
|
|
798
|
+
def test_instantiation_propagates_speed_and_duplex(self):
|
|
799
|
+
"""Interface created from template inherits speed and duplex."""
|
|
800
|
+
statuses = Status.objects.get_for_model(Device)
|
|
801
|
+
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
802
|
+
manufacturer = Manufacturer.objects.first()
|
|
803
|
+
device_role = Role.objects.get_for_model(Device).first()
|
|
804
|
+
device_type = DeviceType.objects.create(manufacturer=manufacturer, model="Propagate 2000")
|
|
805
|
+
|
|
806
|
+
InterfaceTemplate.objects.create(
|
|
807
|
+
device_type=device_type,
|
|
808
|
+
name="EthX",
|
|
809
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
810
|
+
mgmt_only=False,
|
|
811
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
812
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
device = Device.objects.create(
|
|
816
|
+
device_type=device_type,
|
|
817
|
+
role=device_role,
|
|
818
|
+
status=statuses[0],
|
|
819
|
+
name="Device-Prop",
|
|
820
|
+
location=location,
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
iface = device.interfaces.get(name="EthX")
|
|
824
|
+
self.assertEqual(iface.speed, InterfaceSpeedChoices.SPEED_1G)
|
|
825
|
+
self.assertEqual(iface.duplex, InterfaceDuplexChoices.DUPLEX_FULL)
|
|
826
|
+
|
|
728
827
|
|
|
729
828
|
class InterfaceRedundancyGroupTestCase(ModelTestCases.BaseModelTestCase):
|
|
730
829
|
model = InterfaceRedundancyGroup
|
|
@@ -1423,6 +1522,10 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
1423
1522
|
model = Device
|
|
1424
1523
|
|
|
1425
1524
|
def setUp(self):
|
|
1525
|
+
# clear Constance cache
|
|
1526
|
+
cache = caches[settings.CONSTANCE_DATABASE_CACHE_BACKEND]
|
|
1527
|
+
cache.clear()
|
|
1528
|
+
|
|
1426
1529
|
manufacturer = Manufacturer.objects.first()
|
|
1427
1530
|
self.device_type = DeviceType.objects.create(
|
|
1428
1531
|
manufacturer=manufacturer,
|
|
@@ -1532,12 +1635,25 @@ class DeviceTestCase(ModelTestCases.BaseModelTestCase):
|
|
|
1532
1635
|
|
|
1533
1636
|
def test_natural_key_overrides(self):
|
|
1534
1637
|
"""Ensure that the natural-key for Device is affected by settings/Constance."""
|
|
1535
|
-
with override_config(
|
|
1638
|
+
with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NAME):
|
|
1536
1639
|
self.assertEqual([self.device.name], self.device.natural_key())
|
|
1537
1640
|
# self.assertEqual(construct_composite_key([self.device.name]), self.device.composite_key) # TODO: Revist this if we reintroduce composite keys
|
|
1538
1641
|
self.assertEqual(self.device, Device.objects.get_by_natural_key([self.device.name]))
|
|
1539
1642
|
# self.assertEqual(self.device, Device.objects.get(composite_key=self.device.composite_key)) # TODO: Revist this if we reintroduce composite keys
|
|
1540
1643
|
|
|
1644
|
+
with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.LOCATION_TENANT_NAME):
|
|
1645
|
+
self.assertEqual(
|
|
1646
|
+
[self.device.name, self.device.tenant, self.device.location.name], self.device.natural_key()
|
|
1647
|
+
)
|
|
1648
|
+
self.assertEqual(
|
|
1649
|
+
self.device,
|
|
1650
|
+
Device.objects.get_by_natural_key([self.device.name, self.device.tenant, self.device.location]),
|
|
1651
|
+
)
|
|
1652
|
+
|
|
1653
|
+
with override_config(DEVICE_UNIQUENESS=DeviceUniquenessChoices.NONE):
|
|
1654
|
+
self.assertEqual([str(self.device.pk)], self.device.natural_key())
|
|
1655
|
+
self.assertEqual(self.device, Device.objects.get_by_natural_key([self.device.pk]))
|
|
1656
|
+
|
|
1541
1657
|
with override_config(LOCATION_NAME_AS_NATURAL_KEY=True):
|
|
1542
1658
|
self.assertEqual([self.device.name, None, self.device.location.name], self.device.natural_key())
|
|
1543
1659
|
# self.assertEqual(
|
|
@@ -2760,6 +2876,7 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2760
2876
|
name="VLAN 1", vid=100, location=location, status=vlan_status, vlan_group=vlan_group
|
|
2761
2877
|
)
|
|
2762
2878
|
status = Status.objects.get_for_model(Device).first()
|
|
2879
|
+
cls.intf_status = Status.objects.get_for_model(Interface).first()
|
|
2763
2880
|
cls.device = Device.objects.create(
|
|
2764
2881
|
name="Device 1",
|
|
2765
2882
|
device_type=devicetype,
|
|
@@ -2899,6 +3016,11 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2899
3016
|
self.assertEqual(count, 1)
|
|
2900
3017
|
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
|
|
2901
3018
|
|
|
3019
|
+
# add a single instance which is already there
|
|
3020
|
+
count = interface.add_ip_addresses(ips[-1])
|
|
3021
|
+
self.assertEqual(count, 0)
|
|
3022
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ips[-1], interface=interface).count(), 1)
|
|
3023
|
+
|
|
2902
3024
|
# add multiple instances
|
|
2903
3025
|
count = interface.add_ip_addresses(ips[:5])
|
|
2904
3026
|
self.assertEqual(count, 5)
|
|
@@ -2906,6 +3028,20 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2906
3028
|
for ip in ips[:5]:
|
|
2907
3029
|
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
2908
3030
|
|
|
3031
|
+
# add multiple instances all of which are already there
|
|
3032
|
+
count = interface.add_ip_addresses(ips[:5])
|
|
3033
|
+
self.assertEqual(count, 0)
|
|
3034
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 6)
|
|
3035
|
+
for ip in ips[:5]:
|
|
3036
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
3037
|
+
|
|
3038
|
+
# add multiple IPs some of which are there
|
|
3039
|
+
count = interface.add_ip_addresses(ips[3:7])
|
|
3040
|
+
self.assertEqual(count, 2)
|
|
3041
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 8)
|
|
3042
|
+
for ip in ips[3:7]:
|
|
3043
|
+
self.assertEqual(IPAddressToInterface.objects.filter(ip_address=ip, interface=interface).count(), 1)
|
|
3044
|
+
|
|
2909
3045
|
def test_remove_ip_addresses(self):
|
|
2910
3046
|
"""Test the `remove_ip_addresses` helper method on `Interface`"""
|
|
2911
3047
|
interface = Interface.objects.create(
|
|
@@ -2928,13 +3064,28 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2928
3064
|
self.assertEqual(count, 1)
|
|
2929
3065
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
|
|
2930
3066
|
|
|
3067
|
+
# remove a single instance which has already been removed
|
|
3068
|
+
count = interface.remove_ip_addresses(ips[-1])
|
|
3069
|
+
self.assertEqual(count, 0)
|
|
3070
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 9)
|
|
3071
|
+
|
|
2931
3072
|
# remove multiple instances
|
|
2932
3073
|
count = interface.remove_ip_addresses(ips[:5])
|
|
2933
3074
|
self.assertEqual(count, 5)
|
|
2934
3075
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
|
|
2935
3076
|
|
|
3077
|
+
# remove multiple instances all which have already been removed
|
|
3078
|
+
count = interface.remove_ip_addresses(ips[:5])
|
|
3079
|
+
self.assertEqual(count, 0)
|
|
3080
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 4)
|
|
3081
|
+
|
|
3082
|
+
# remove multiple instances some of which have already been removed
|
|
3083
|
+
count = interface.remove_ip_addresses(ips[3:7])
|
|
3084
|
+
self.assertEqual(count, 2)
|
|
3085
|
+
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 2)
|
|
3086
|
+
|
|
2936
3087
|
count = interface.remove_ip_addresses(ips)
|
|
2937
|
-
self.assertEqual(count,
|
|
3088
|
+
self.assertEqual(count, 2)
|
|
2938
3089
|
self.assertEqual(IPAddressToInterface.objects.filter(interface=interface).count(), 0)
|
|
2939
3090
|
|
|
2940
3091
|
# Test the pre_delete signal for IPAddressToInterface instances
|
|
@@ -2942,13 +3093,186 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2942
3093
|
self.device.primary_ip4 = interface.ip_addresses.all().filter(ip_version=4).first()
|
|
2943
3094
|
self.device.primary_ip6 = interface.ip_addresses.all().filter(ip_version=6).first()
|
|
2944
3095
|
self.device.save()
|
|
2945
|
-
|
|
3096
|
+
|
|
3097
|
+
count = interface.remove_ip_addresses(self.device.primary_ip4)
|
|
3098
|
+
self.assertEqual(count, 1)
|
|
2946
3099
|
self.device.refresh_from_db()
|
|
2947
3100
|
self.assertEqual(self.device.primary_ip4, None)
|
|
2948
|
-
|
|
3101
|
+
# NOTE: This effectively tests what happens when you pass remove_ip_addresses None; it
|
|
3102
|
+
# NOTE: does not remove a v6 address, because there are no v6 IPs created in this test
|
|
3103
|
+
# NOTE: class.
|
|
3104
|
+
count = interface.remove_ip_addresses(self.device.primary_ip6)
|
|
3105
|
+
self.assertEqual(count, 0)
|
|
2949
3106
|
self.device.refresh_from_db()
|
|
2950
3107
|
self.assertEqual(self.device.primary_ip6, None)
|
|
2951
3108
|
|
|
3109
|
+
def _assert_invalid_speed_duplex(self, if_type, speed=None, duplex="", expected_error=""):
|
|
3110
|
+
iface = Interface(
|
|
3111
|
+
device=self.device,
|
|
3112
|
+
name=f"test-{if_type}",
|
|
3113
|
+
type=if_type,
|
|
3114
|
+
status=self.intf_status,
|
|
3115
|
+
speed=speed,
|
|
3116
|
+
duplex=duplex,
|
|
3117
|
+
)
|
|
3118
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3119
|
+
iface.full_clean()
|
|
3120
|
+
self.assertIn(expected_error, str(cm.exception))
|
|
3121
|
+
|
|
3122
|
+
def test_disallowed_speed_and_duplex_matrix(self):
|
|
3123
|
+
"""Test that interface types with no speed or duplex disallow those settings."""
|
|
3124
|
+
test_cases = [
|
|
3125
|
+
# LAG
|
|
3126
|
+
(
|
|
3127
|
+
InterfaceTypeChoices.TYPE_LAG,
|
|
3128
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3129
|
+
None,
|
|
3130
|
+
"Speed is not applicable to this interface type.",
|
|
3131
|
+
),
|
|
3132
|
+
(
|
|
3133
|
+
InterfaceTypeChoices.TYPE_LAG,
|
|
3134
|
+
None,
|
|
3135
|
+
InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3136
|
+
"Duplex is not applicable to this interface type.",
|
|
3137
|
+
),
|
|
3138
|
+
# Virtual
|
|
3139
|
+
(
|
|
3140
|
+
InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
3141
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3142
|
+
None,
|
|
3143
|
+
"Speed is not applicable to this interface type.",
|
|
3144
|
+
),
|
|
3145
|
+
(
|
|
3146
|
+
InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
3147
|
+
None,
|
|
3148
|
+
InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3149
|
+
"Duplex is not applicable to this interface type.",
|
|
3150
|
+
),
|
|
3151
|
+
# Wireless
|
|
3152
|
+
(
|
|
3153
|
+
InterfaceTypeChoices.TYPE_80211AC,
|
|
3154
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3155
|
+
None,
|
|
3156
|
+
"Speed is not applicable to this interface type.",
|
|
3157
|
+
),
|
|
3158
|
+
(
|
|
3159
|
+
InterfaceTypeChoices.TYPE_80211AC,
|
|
3160
|
+
None,
|
|
3161
|
+
InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3162
|
+
"Duplex is not applicable to this interface type.",
|
|
3163
|
+
),
|
|
3164
|
+
# Copper (negative speed is invalid)
|
|
3165
|
+
(InterfaceTypeChoices.TYPE_1GE_FIXED, -100, None, "Ensure this value is greater than or equal to 0."),
|
|
3166
|
+
# Copper (speed as a string is invalid)
|
|
3167
|
+
(InterfaceTypeChoices.TYPE_1GE_FIXED, "100 Mbps", None, "value must be an integer."),
|
|
3168
|
+
# Copper (invalid duplex is invalid)
|
|
3169
|
+
(
|
|
3170
|
+
InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
3171
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3172
|
+
"invalid",
|
|
3173
|
+
"Value 'invalid' is not a valid choice.",
|
|
3174
|
+
),
|
|
3175
|
+
# Optical (no duplex allowed)
|
|
3176
|
+
(
|
|
3177
|
+
InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
3178
|
+
InterfaceSpeedChoices.SPEED_1M,
|
|
3179
|
+
InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3180
|
+
"Duplex is only applicable to copper twisted-pair interfaces.",
|
|
3181
|
+
),
|
|
3182
|
+
]
|
|
3183
|
+
for if_type, speed, duplex, expected_error in test_cases:
|
|
3184
|
+
with self.subTest(f"{if_type} with speed={speed} and duplex={duplex}"):
|
|
3185
|
+
self._assert_invalid_speed_duplex(if_type, speed, duplex, expected_error)
|
|
3186
|
+
|
|
3187
|
+
def test_copper_allows_duplex_and_non_negative_speed(self):
|
|
3188
|
+
"""Test that copper interfaces allow duplex and non-negative speed."""
|
|
3189
|
+
iface = Interface(
|
|
3190
|
+
device=self.device,
|
|
3191
|
+
name="eth1",
|
|
3192
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED, # 1000BASE-T
|
|
3193
|
+
status=self.intf_status,
|
|
3194
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
3195
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3196
|
+
)
|
|
3197
|
+
# Should not raise
|
|
3198
|
+
iface.full_clean()
|
|
3199
|
+
|
|
3200
|
+
iface.speed = 0
|
|
3201
|
+
iface.full_clean()
|
|
3202
|
+
|
|
3203
|
+
def test_lag_allows_no_speed_or_duplex(self):
|
|
3204
|
+
"""Test that LAG interfaces pass validation when speed and duplex are not set."""
|
|
3205
|
+
iface = Interface(
|
|
3206
|
+
device=self.device,
|
|
3207
|
+
name="Port-Channel1",
|
|
3208
|
+
type=InterfaceTypeChoices.TYPE_LAG,
|
|
3209
|
+
status=self.intf_status,
|
|
3210
|
+
)
|
|
3211
|
+
# Should not raise when speed and duplex are not set
|
|
3212
|
+
iface.full_clean()
|
|
3213
|
+
|
|
3214
|
+
def test_optical_disallows_duplex_allows_speed(self):
|
|
3215
|
+
"""Test that optical interfaces do not allow duplex and allow positive speed."""
|
|
3216
|
+
# Duplex set should error
|
|
3217
|
+
iface_bad = Interface(
|
|
3218
|
+
device=self.device,
|
|
3219
|
+
name="xe0",
|
|
3220
|
+
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
3221
|
+
status=self.intf_status,
|
|
3222
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3223
|
+
)
|
|
3224
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3225
|
+
iface_bad.full_clean()
|
|
3226
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
|
|
3227
|
+
|
|
3228
|
+
# Speed positive should pass
|
|
3229
|
+
iface_ok = Interface(
|
|
3230
|
+
device=self.device,
|
|
3231
|
+
name="xe1",
|
|
3232
|
+
type=InterfaceTypeChoices.TYPE_10GE_SFP_PLUS,
|
|
3233
|
+
status=self.intf_status,
|
|
3234
|
+
speed=InterfaceSpeedChoices.SPEED_10G,
|
|
3235
|
+
)
|
|
3236
|
+
iface_ok.full_clean()
|
|
3237
|
+
|
|
3238
|
+
def test_changing_copper_interface_with_speed_and_duplex_to_optical_fails(self):
|
|
3239
|
+
"""Test that changing a copper interface with speed and duplex to an optical interface fails."""
|
|
3240
|
+
|
|
3241
|
+
with self.subTest("speed"):
|
|
3242
|
+
iface = Interface(
|
|
3243
|
+
device=self.device,
|
|
3244
|
+
name="eth3",
|
|
3245
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
3246
|
+
status=self.intf_status,
|
|
3247
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
3248
|
+
)
|
|
3249
|
+
iface.full_clean()
|
|
3250
|
+
|
|
3251
|
+
iface.type = InterfaceTypeChoices.TYPE_LAG
|
|
3252
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3253
|
+
iface.full_clean()
|
|
3254
|
+
self.assertIn("Speed is not applicable to this interface type.", str(cm.exception))
|
|
3255
|
+
|
|
3256
|
+
with self.subTest("duplex"):
|
|
3257
|
+
iface = Interface(
|
|
3258
|
+
device=self.device,
|
|
3259
|
+
name="eth3",
|
|
3260
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
3261
|
+
status=self.intf_status,
|
|
3262
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
3263
|
+
)
|
|
3264
|
+
iface.full_clean()
|
|
3265
|
+
|
|
3266
|
+
iface.type = InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
|
3267
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3268
|
+
iface.full_clean()
|
|
3269
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
|
|
3270
|
+
|
|
3271
|
+
iface.type = InterfaceTypeChoices.TYPE_10GE_SFP_PLUS
|
|
3272
|
+
with self.assertRaises(ValidationError) as cm:
|
|
3273
|
+
iface.full_clean()
|
|
3274
|
+
self.assertIn("Duplex is only applicable to copper twisted-pair interfaces.", str(cm.exception))
|
|
3275
|
+
|
|
2952
3276
|
|
|
2953
3277
|
class SoftwareImageFileTestCase(ModelTestCases.BaseModelTestCase):
|
|
2954
3278
|
model = SoftwareImageFile
|