nautobot 3.0.0a3__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 -0
- nautobot/apps/ui.py +4 -0
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
- nautobot/circuits/views.py +6 -2
- nautobot/core/api/serializers.py +1 -1
- nautobot/core/api/urls.py +1 -0
- nautobot/core/api/views.py +4 -0
- nautobot/core/choices.py +1 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +36 -13
- nautobot/core/cli/migrate_deprecated_templates.py +36 -9
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/jobs/__init__.py +56 -0
- nautobot/core/management/commands/generate_test_data.py +3 -3
- nautobot/core/models/__init__.py +11 -0
- nautobot/core/models/utils.py +1 -1
- nautobot/core/settings.py +17 -7
- nautobot/core/settings.yaml +4 -26
- nautobot/core/templates/admin/base.html +1 -2
- nautobot/core/templates/admin/change_list.html +9 -12
- nautobot/core/templates/base_django.html +1 -2
- nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
- nautobot/core/templates/components/tab/content_wrapper.html +4 -4
- nautobot/core/templates/echarts/echarts.html +21 -8
- nautobot/core/templates/generic/object_bulk_create.html +2 -2
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_edit.html +1 -1
- nautobot/core/templates/generic/object_bulk_import.html +1 -1
- nautobot/core/templates/generic/object_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_retrieve.html +2 -2
- nautobot/core/templates/graphene/graphiql.html +0 -1
- nautobot/core/templates/inc/footer.html +3 -1
- nautobot/core/templates/inc/header.html +10 -0
- nautobot/core/templates/inc/media.html +14 -0
- nautobot/core/templates/inc/nav_menu.html +1 -8
- nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
- nautobot/core/templates/nautobot_config.py.j2 +0 -6
- nautobot/core/templates/rest_framework/api.html +103 -2
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +33 -0
- nautobot/core/templates/utilities/theme_preview.html +3 -0
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templatetags/helpers.py +24 -12
- nautobot/core/testing/integration.py +24 -13
- nautobot/core/testing/utils.py +18 -4
- nautobot/core/testing/views.py +104 -17
- 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/runner.py +1 -2
- nautobot/core/tests/test_breadcrumbs.py +21 -21
- nautobot/core/tests/test_jobs.py +73 -6
- nautobot/core/tests/test_renderers.py +59 -0
- nautobot/core/tests/test_settings_schema.py +1 -0
- nautobot/core/tests/test_templatetags_helpers.py +9 -0
- nautobot/core/tests/test_titles.py +0 -16
- nautobot/core/tests/test_ui.py +122 -3
- nautobot/core/tests/test_utils.py +41 -1
- nautobot/core/ui/breadcrumbs.py +68 -17
- nautobot/core/ui/bulk_buttons.py +1 -1
- nautobot/core/ui/choices.py +49 -65
- nautobot/core/ui/echarts.py +15 -20
- nautobot/core/ui/object_detail.py +54 -46
- nautobot/core/ui/titles.py +3 -6
- nautobot/core/urls.py +8 -8
- nautobot/core/utils/filtering.py +11 -1
- nautobot/core/utils/lookup.py +46 -0
- nautobot/core/views/mixins.py +31 -20
- nautobot/core/views/renderers.py +2 -3
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -2
- nautobot/dcim/api/serializers.py +3 -0
- nautobot/dcim/choices.py +49 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/factory.py +1 -1
- nautobot/dcim/filters.py +13 -1
- nautobot/dcim/forms.py +89 -3
- 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/{0078_remove_device_location_tenant_name_uniqueness.py → 0079_remove_device_location_tenant_name_uniqueness.py} +1 -1
- nautobot/dcim/migrations/{0079_device_name_data_migration.py → 0080_device_name_data_migration.py} +1 -1
- 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 +22 -1
- nautobot/dcim/models/devices.py +17 -4
- nautobot/dcim/tables/devices.py +15 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/tables/racks.py +0 -2
- nautobot/dcim/tables/template_code.py +1 -1
- nautobot/dcim/templates/dcim/cable_trace.html +0 -2
- nautobot/dcim/templates/dcim/consoleport.html +1 -1
- nautobot/dcim/templates/dcim/consoleserverport.html +1 -1
- nautobot/dcim/templates/dcim/devicebay.html +1 -1
- nautobot/dcim/templates/dcim/frontport.html +1 -1
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
- 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 +9 -1
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
- 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_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/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +1 -1
- nautobot/dcim/templates/dcim/rack_elevation_list.html +1 -1
- nautobot/dcim/templates/dcim/rack_retrieve.html +0 -11
- nautobot/dcim/templates/dcim/rearport.html +1 -1
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_update.html +1 -1
- nautobot/dcim/tests/integration/test_controller.py +3 -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 +186 -6
- nautobot/dcim/tests/test_filters.py +43 -1
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +265 -0
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +69 -7
- nautobot/dcim/views.py +232 -126
- nautobot/extras/api/views.py +51 -44
- nautobot/extras/datasources/git.py +3 -1
- nautobot/extras/filters.py +19 -2
- nautobot/extras/forms/forms.py +9 -2
- nautobot/extras/jobs.py +2 -0
- nautobot/extras/jobs_ui.py +4 -3
- nautobot/extras/management/__init__.py +2 -0
- nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
- nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
- nautobot/extras/models/approvals.py +11 -1
- nautobot/extras/models/change_logging.py +4 -0
- nautobot/extras/models/jobs.py +1 -3
- nautobot/extras/models/models.py +10 -2
- nautobot/extras/plugins/marketplace_manifest.yml +49 -1
- nautobot/extras/plugins/views.py +0 -5
- nautobot/extras/querysets.py +8 -0
- nautobot/extras/tables.py +12 -0
- nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
- nautobot/extras/templates/extras/configcontext_update.html +1 -0
- nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
- nautobot/extras/templates/extras/objectchange_retrieve.html +0 -2
- nautobot/extras/templates/extras/plugin_detail.html +3 -3
- nautobot/extras/templates/extras/secret_create.html +1 -1
- nautobot/extras/tests/integration/test_computedfields.py +8 -9
- nautobot/extras/tests/integration/test_customfields.py +1 -3
- nautobot/extras/tests/integration/test_dynamicgroups.py +7 -8
- nautobot/extras/tests/integration/test_relationships.py +0 -2
- nautobot/extras/tests/test_api.py +63 -0
- nautobot/extras/tests/test_changelog.py +24 -2
- nautobot/extras/tests/test_filters.py +36 -3
- nautobot/extras/tests/test_models.py +38 -2
- nautobot/extras/tests/test_utils.py +3 -4
- nautobot/extras/tests/test_views.py +22 -83
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +83 -52
- nautobot/ipam/filters.py +26 -0
- nautobot/ipam/tables.py +6 -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_vrfs.html +1 -1
- nautobot/ipam/tests/test_filters.py +26 -1
- nautobot/ipam/tests/test_models.py +1 -1
- nautobot/ipam/views.py +9 -7
- 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/forms.js +13 -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/ui/package-lock.json +87 -4
- nautobot/ui/package.json +2 -1
- nautobot/ui/src/js/nautobot.js +0 -1
- nautobot/ui/src/js/select2.js +53 -2
- nautobot/ui/src/scss/nautobot.scss +51 -2
- nautobot/ui/webpack.config.js +13 -0
- nautobot/users/templates/users/preferences.html +11 -2
- nautobot/virtualization/filters.py +6 -1
- nautobot/virtualization/tests/test_filters.py +10 -1
- nautobot/virtualization/tests/test_models.py +1 -0
- nautobot/virtualization/views.py +4 -1
- nautobot/vpn/factory.py +25 -15
- nautobot/vpn/filters.py +1 -0
- nautobot/vpn/forms.py +1 -0
- nautobot/vpn/migrations/0001_initial.py +1 -1
- nautobot/vpn/models.py +16 -8
- nautobot/vpn/tables.py +5 -2
- nautobot/vpn/tests/test_api.py +0 -5
- nautobot/vpn/tests/test_forms.py +1 -2
- nautobot/vpn/tests/test_models.py +57 -7
- nautobot/vpn/tests/test_views.py +22 -3
- nautobot/vpn/views.py +78 -20
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +4 -4
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +243 -352
- 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/templates/data_validation/datacompliance_retrieve.html +0 -1
- nautobot/dcim/templates/dcim/cable.html +0 -2
- nautobot/dcim/templates/dcim/cable_edit.html +0 -2
- 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 -2
- 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 -2
- 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/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/vpn/templates/vpn/vpnprofile.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-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
- {nautobot-3.0.0a3.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +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
|
|
@@ -2170,7 +2229,7 @@ class PowerOutletTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMix
|
|
|
2170
2229
|
class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin):
|
|
2171
2230
|
model = Interface
|
|
2172
2231
|
peer_termination_type = Interface
|
|
2173
|
-
choices_fields = ["mode", "type"]
|
|
2232
|
+
choices_fields = ["duplex", "mode", "type"]
|
|
2174
2233
|
validation_excluded_fields = [
|
|
2175
2234
|
"tagged_vlans", # M2M field, excluded by default
|
|
2176
2235
|
]
|
|
@@ -2214,14 +2273,14 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2214
2273
|
Interface.objects.create(
|
|
2215
2274
|
device=cls.devices[0],
|
|
2216
2275
|
name="Test Interface 1",
|
|
2217
|
-
type=
|
|
2276
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2218
2277
|
status=non_default_status,
|
|
2219
2278
|
role=intf_role,
|
|
2220
2279
|
),
|
|
2221
2280
|
Interface.objects.create(
|
|
2222
2281
|
device=cls.devices[0],
|
|
2223
2282
|
name="Test Interface 2",
|
|
2224
|
-
type=
|
|
2283
|
+
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2225
2284
|
status=non_default_status,
|
|
2226
2285
|
),
|
|
2227
2286
|
Interface.objects.create(
|
|
@@ -2272,7 +2331,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2272
2331
|
{
|
|
2273
2332
|
"device": cls.devices[0].pk,
|
|
2274
2333
|
"name": "Test Interface 8",
|
|
2275
|
-
"type":
|
|
2334
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2276
2335
|
"status": interface_status.pk,
|
|
2277
2336
|
"role": intf_role.pk,
|
|
2278
2337
|
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
@@ -2283,7 +2342,7 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2283
2342
|
{
|
|
2284
2343
|
"device": cls.devices[0].pk,
|
|
2285
2344
|
"name": "Test Interface 9",
|
|
2286
|
-
"type":
|
|
2345
|
+
"type": InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
2287
2346
|
"status": interface_status.pk,
|
|
2288
2347
|
"role": intf_role.pk,
|
|
2289
2348
|
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
@@ -2295,13 +2354,35 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2295
2354
|
{
|
|
2296
2355
|
"device": cls.devices[0].pk,
|
|
2297
2356
|
"name": "Test Interface 10",
|
|
2298
|
-
"type":
|
|
2357
|
+
"type": InterfaceTypeChoices.TYPE_VIRTUAL,
|
|
2299
2358
|
"status": interface_status.pk,
|
|
2300
2359
|
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
2301
2360
|
"parent_interface": cls.interfaces[1].pk,
|
|
2302
2361
|
"tagged_vlans": [cls.vlans[0].pk, cls.vlans[1].pk],
|
|
2303
2362
|
"untagged_vlan": cls.vlans[2].pk,
|
|
2304
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
|
+
},
|
|
2305
2386
|
]
|
|
2306
2387
|
|
|
2307
2388
|
cls.untagged_vlan_data = {
|
|
@@ -2510,6 +2591,105 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2510
2591
|
response = self.client.patch(self._get_detail_url(interface), data=payload, format="json", **self.header)
|
|
2511
2592
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2512
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
|
+
|
|
2513
2693
|
|
|
2514
2694
|
class FrontPortTest(Mixins.BasePortTestMixin):
|
|
2515
2695
|
model = FrontPort
|
|
@@ -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,
|
|
@@ -127,7 +129,7 @@ from nautobot.dcim.models import (
|
|
|
127
129
|
from nautobot.extras.filter_mixins import RoleFilter, StatusFilter
|
|
128
130
|
from nautobot.extras.models import ExternalIntegration, Role, SecretsGroup, Status, Tag
|
|
129
131
|
from nautobot.extras.tests.test_customfields_filters import CustomFieldsFilters
|
|
130
|
-
from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup
|
|
132
|
+
from nautobot.ipam.models import IPAddress, Namespace, Prefix, Service, VLAN, VLANGroup, VRF, VRFDeviceAssignment
|
|
131
133
|
from nautobot.tenancy.models import Tenant
|
|
132
134
|
from nautobot.virtualization.models import Cluster, ClusterType, VirtualMachine
|
|
133
135
|
from nautobot.wireless.models import RadioProfile, WirelessNetwork
|
|
@@ -1828,6 +1830,8 @@ class DeviceTestCase(
|
|
|
1828
1830
|
("vc_priority",),
|
|
1829
1831
|
("virtual_chassis", "virtual_chassis__id"),
|
|
1830
1832
|
("virtual_chassis", "virtual_chassis__name"),
|
|
1833
|
+
("vrfs", "vrfs__id"),
|
|
1834
|
+
("vrfs", "vrfs__rd"),
|
|
1831
1835
|
("wireless_networks", "controller_managed_device_group__wireless_networks__id"),
|
|
1832
1836
|
("wireless_networks", "controller_managed_device_group__wireless_networks__name"),
|
|
1833
1837
|
]
|
|
@@ -1934,6 +1938,14 @@ class DeviceTestCase(
|
|
|
1934
1938
|
virtual_chassis_2 = VirtualChassis.objects.create(name="vc2", master=devices[2])
|
|
1935
1939
|
Device.objects.filter(pk=devices[2].pk).update(virtual_chassis=virtual_chassis_2, vc_position=1, vc_priority=1)
|
|
1936
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
|
+
|
|
1937
1949
|
def test_special_filters(self):
|
|
1938
1950
|
# TODO: Not a generic_filter_test because this is a single-value filter
|
|
1939
1951
|
with self.subTest("face"):
|
|
@@ -2253,6 +2265,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2253
2265
|
("name",),
|
|
2254
2266
|
("parent_interface", "parent_interface__id"),
|
|
2255
2267
|
("parent_interface", "parent_interface__name"),
|
|
2268
|
+
("speed",),
|
|
2269
|
+
("duplex",),
|
|
2256
2270
|
("role", "role__id"),
|
|
2257
2271
|
("role", "role__name"),
|
|
2258
2272
|
("status", "status__id"),
|
|
@@ -2332,6 +2346,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2332
2346
|
mtu=100,
|
|
2333
2347
|
status=interface_statuses[0],
|
|
2334
2348
|
untagged_vlan=vlans[0],
|
|
2349
|
+
speed=InterfaceSpeedChoices.SPEED_1G,
|
|
2350
|
+
duplex=InterfaceDuplexChoices.DUPLEX_FULL,
|
|
2335
2351
|
)
|
|
2336
2352
|
|
|
2337
2353
|
Interface.objects.filter(pk=cabled_interfaces[1].pk).update(
|
|
@@ -2341,6 +2357,8 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2341
2357
|
mtu=200,
|
|
2342
2358
|
status=interface_statuses[3],
|
|
2343
2359
|
untagged_vlan=vlans[1],
|
|
2360
|
+
speed=InterfaceSpeedChoices.SPEED_10G,
|
|
2361
|
+
duplex=InterfaceDuplexChoices.DUPLEX_HALF,
|
|
2344
2362
|
)
|
|
2345
2363
|
|
|
2346
2364
|
Interface.objects.filter(pk=cabled_interfaces[2].pk).update(
|
|
@@ -2354,6 +2372,16 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2354
2372
|
for interface in cabled_interfaces:
|
|
2355
2373
|
interface.refresh_from_db()
|
|
2356
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
|
+
|
|
2357
2385
|
cable_statuses = Status.objects.get_for_model(Cable)
|
|
2358
2386
|
connected_status = cable_statuses.get(name="Connected")
|
|
2359
2387
|
|
|
@@ -2549,6 +2577,20 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2549
2577
|
params = {"mode": [InterfaceModeChoices.MODE_ACCESS]}
|
|
2550
2578
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
|
2551
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
|
+
|
|
2552
2594
|
def test_device_with_common_vc(self):
|
|
2553
2595
|
"""Assert only interfaces belonging to devices with common VC are returned"""
|
|
2554
2596
|
device_type = DeviceType.objects.first()
|
|
@@ -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)
|