nautobot 2.4.21__py3-none-any.whl → 3.0.0a3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/apps/choices.py +2 -2
- nautobot/apps/filters.py +9 -9
- nautobot/apps/forms.py +2 -0
- nautobot/apps/models.py +7 -2
- nautobot/apps/ui.py +20 -1
- nautobot/apps/utils.py +2 -3
- nautobot/apps/views.py +7 -1
- nautobot/circuits/filters.py +8 -23
- nautobot/circuits/navigation.py +3 -1
- nautobot/circuits/templates/circuits/circuit_create.html +9 -9
- nautobot/circuits/templates/circuits/circuit_terminations_swap.html +2 -2
- nautobot/circuits/templates/circuits/circuittermination_create.html +24 -33
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +10 -10
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +13 -13
- nautobot/circuits/templates/circuits/inc/circuit_termination_header_extra_content.html +6 -6
- nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +3 -3
- nautobot/circuits/templates/circuits/inc/speed_widget.html +13 -13
- nautobot/circuits/templates/circuits/provider_create.html +9 -9
- nautobot/circuits/tests/integration/test_circuit.py +19 -19
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +3 -0
- nautobot/circuits/tests/integration/test_relationships.py +4 -12
- nautobot/circuits/views.py +0 -2
- nautobot/cloud/filters.py +1 -13
- nautobot/cloud/navigation.py +3 -1
- nautobot/cloud/templates/cloud/cloudnetwork_update.html +9 -9
- nautobot/cloud/templates/cloud/cloudservice_update.html +6 -6
- nautobot/core/api/fields.py +30 -2
- nautobot/core/api/schema.py +1 -1
- nautobot/core/api/serializers.py +9 -2
- nautobot/core/api/urls.py +2 -0
- nautobot/core/api/views.py +58 -37
- nautobot/core/apps/__init__.py +6 -12
- nautobot/core/branching.py +83 -0
- nautobot/core/celery/__init__.py +11 -6
- nautobot/core/celery/backends.py +2 -0
- nautobot/core/celery/encoders.py +7 -0
- nautobot/core/celery/task.py +44 -0
- nautobot/core/checks.py +60 -0
- nautobot/core/cli/bootstrap_v3_to_v5.py +776 -0
- nautobot/core/constants.py +9 -0
- nautobot/core/context_processors.py +84 -0
- nautobot/core/filters.py +131 -2
- nautobot/core/forms/__init__.py +4 -2
- nautobot/core/forms/fields.py +10 -8
- nautobot/core/forms/forms.py +21 -9
- nautobot/core/forms/search.py +0 -15
- nautobot/core/forms/widgets.py +3 -2
- nautobot/core/graphql/__init__.py +8 -26
- nautobot/core/graphql/generators.py +16 -6
- nautobot/core/graphql/schema.py +1 -1
- nautobot/core/graphql/schema_init.py +1 -2
- nautobot/core/graphql/utils.py +7 -9
- nautobot/core/jobs/__init__.py +158 -0
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/__init__.py +17 -2
- nautobot/core/models/fields.py +3 -2
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/name_color_content_types.py +1 -1
- nautobot/core/models/ordering.py +7 -5
- nautobot/core/models/querysets.py +77 -2
- nautobot/core/models/tree_queries.py +6 -4
- nautobot/core/settings.py +30 -16
- nautobot/core/settings.yaml +13 -7
- nautobot/core/tables.py +114 -44
- nautobot/core/templates/403.html +1 -1
- nautobot/core/templates/403_csrf_failure.html +1 -1
- nautobot/core/templates/404.html +1 -1
- nautobot/core/templates/40x.html +8 -8
- nautobot/core/templates/500.html +10 -10
- nautobot/core/templates/about.html +13 -12
- nautobot/core/templates/admin/actions.html +1 -1
- nautobot/core/templates/admin/app_index.html +3 -3
- nautobot/core/templates/admin/base.html +45 -52
- nautobot/core/templates/admin/base_site.html +0 -9
- nautobot/core/templates/admin/change_form.html +5 -5
- nautobot/core/templates/admin/change_list.html +8 -12
- nautobot/core/templates/admin/change_list_results.html +3 -3
- nautobot/core/templates/admin/config/config.html +24 -24
- nautobot/core/templates/admin/delete_confirmation.html +5 -5
- nautobot/core/templates/admin/edit_inline/stacked.html +5 -5
- nautobot/core/templates/admin/edit_inline/tabular.html +3 -3
- nautobot/core/templates/admin/includes/fieldset.html +15 -15
- nautobot/core/templates/admin/index.html +8 -8
- nautobot/core/templates/admin/submit_line.html +5 -5
- nautobot/core/templates/base_django.html +36 -32
- nautobot/core/templates/buttons/add.html +1 -1
- nautobot/core/templates/buttons/consolidated_detail_view_action_buttons.html +2 -2
- nautobot/core/templates/buttons/export.html +17 -18
- nautobot/core/templates/buttons/job_import.html +2 -2
- nautobot/core/templates/components/breadcrumbs.html +19 -17
- nautobot/core/templates/components/button/dropdown.html +7 -5
- nautobot/core/templates/components/echarts.html +2 -0
- nautobot/core/templates/components/layout/one_over_two.html +3 -3
- nautobot/core/templates/components/layout/two_over_one.html +3 -3
- nautobot/core/templates/components/panel/body_content_data_table.html +2 -2
- nautobot/core/templates/components/panel/body_content_tags.html +1 -1
- nautobot/core/templates/components/panel/body_wrapper_generic.html +4 -2
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/body_wrapper_key_value_table.html +5 -3
- nautobot/core/templates/components/panel/body_wrapper_table.html +4 -2
- nautobot/core/templates/components/panel/footer_contacts_table.html +4 -4
- nautobot/core/templates/components/panel/footer_content_table.html +3 -3
- nautobot/core/templates/components/panel/grouping_toggle.html +12 -11
- nautobot/core/templates/components/panel/header_extra_content_table.html +2 -11
- nautobot/core/templates/components/panel/panel.html +6 -3
- nautobot/core/templates/components/panel/stats_panel_body.html +9 -7
- nautobot/core/templates/components/tab/content_wrapper.html +29 -1
- nautobot/core/templates/components/tab/label_wrapper.html +10 -2
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +11 -4
- nautobot/core/templates/echarts/echarts.html +20 -0
- nautobot/core/templates/exceptions/import_error.html +2 -2
- nautobot/core/templates/exceptions/permission_error.html +1 -1
- nautobot/core/templates/exceptions/programming_error.html +2 -2
- nautobot/core/templates/generic/object_bulk_add_component.html +29 -20
- nautobot/core/templates/generic/object_bulk_create.html +87 -75
- nautobot/core/templates/generic/object_bulk_destroy.html +35 -37
- nautobot/core/templates/generic/object_bulk_remove.html +30 -26
- nautobot/core/templates/generic/object_bulk_rename.html +53 -40
- nautobot/core/templates/generic/object_bulk_update.html +36 -29
- nautobot/core/templates/generic/object_create.html +40 -27
- nautobot/core/templates/generic/object_import.html +36 -24
- nautobot/core/templates/generic/object_list.html +279 -215
- nautobot/core/templates/generic/object_notes.html +21 -11
- nautobot/core/templates/generic/object_retrieve.html +161 -213
- nautobot/core/templates/graphene/graphiql.html +113 -60
- nautobot/core/templates/home.html +164 -87
- nautobot/core/templates/import_success.html +3 -2
- nautobot/core/templates/inc/ajax_loader.html +1 -1
- nautobot/core/templates/inc/computed_fields/panel_data.html +25 -13
- nautobot/core/templates/inc/created_updated.html +12 -7
- nautobot/core/templates/inc/custom_fields/panel_data.html +28 -16
- nautobot/core/templates/inc/custom_fields_panel.html +3 -3
- nautobot/core/templates/inc/dynamic_groups_panel.html +3 -3
- nautobot/core/templates/inc/extras_features_edit_form_fields.html +15 -15
- nautobot/core/templates/inc/footer.html +90 -40
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +75 -0
- nautobot/core/templates/inc/header_banners.html +17 -0
- nautobot/core/templates/inc/header_messages.html +6 -0
- nautobot/core/templates/inc/image_attachments.html +9 -9
- nautobot/core/templates/inc/javascript.html +7 -24
- nautobot/core/templates/inc/media.html +4 -29
- nautobot/core/templates/inc/modal.html +2 -2
- nautobot/core/templates/inc/nav_favorites.html +27 -0
- nautobot/core/templates/inc/nav_menu.html +150 -108
- nautobot/core/templates/inc/object_details_advanced_panel.html +84 -71
- nautobot/core/templates/inc/page_title.html +23 -0
- nautobot/core/templates/inc/paginator.html +39 -28
- nautobot/core/templates/inc/relationships/panel_override.html +3 -3
- nautobot/core/templates/inc/relationships_panel.html +3 -3
- nautobot/core/templates/inc/relationships_table_rows.html +1 -1
- nautobot/core/templates/inc/search_panel.html +22 -16
- nautobot/core/templates/inc/table.html +61 -36
- nautobot/core/templates/inc/tenancy_form_panel.html +3 -3
- nautobot/core/templates/login.html +17 -59
- nautobot/core/templates/modals/modal_theme.html +12 -23
- nautobot/core/templates/nautobot_config.py.j2 +6 -5
- nautobot/core/templates/panel_table.html +8 -12
- nautobot/core/templates/redoc_ui.html +80 -0
- nautobot/core/templates/rest_framework/api.html +43 -21
- nautobot/core/templates/search.html +12 -13
- nautobot/core/templates/swagger_ui.html +19 -4
- nautobot/core/templates/system_jobs/import_objects.html +70 -58
- nautobot/core/templates/template.css +0 -6
- nautobot/core/templates/utilities/comment_form.html +34 -0
- nautobot/core/templates/utilities/confirmation_form.html +17 -9
- nautobot/core/templates/utilities/obj_table.html +19 -11
- nautobot/core/templates/utilities/render_field.html +27 -21
- nautobot/core/templates/utilities/render_jinja2.html +22 -25
- nautobot/core/templates/utilities/templatetags/advanced_filter_indicator.html +8 -0
- nautobot/core/templates/utilities/templatetags/badge.html +1 -1
- nautobot/core/templates/utilities/templatetags/dynamic_group_assignment_modal.html +2 -3
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +482 -0
- nautobot/core/templates/utilities/templatetags/modal_form_as_dialog.html +14 -18
- nautobot/core/templates/utilities/templatetags/saved_view_modal.html +11 -11
- nautobot/core/templates/utilities/templatetags/table_config_form.html +51 -24
- nautobot/core/templates/utilities/templatetags/tag.html +1 -1
- nautobot/core/templates/utilities/templatetags/utilization_graph.html +3 -3
- nautobot/core/templates/utilities/theme_preview.html +829 -566
- nautobot/core/templates/utilities/worker_status.html +42 -41
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templates/widgets/sluginput.html +2 -2
- nautobot/core/templatetags/buttons.py +38 -40
- nautobot/core/templatetags/helpers.py +105 -28
- nautobot/core/templatetags/ui_framework.py +17 -0
- nautobot/core/testing/api.py +76 -12
- nautobot/core/testing/filters.py +11 -27
- nautobot/core/testing/integration.py +128 -10
- nautobot/core/testing/mixins.py +7 -4
- nautobot/core/testing/utils.py +28 -5
- nautobot/core/testing/views.py +125 -27
- nautobot/core/tests/integration/test_app_home.py +39 -35
- nautobot/core/tests/integration/test_app_navbar.py +60 -67
- nautobot/core/tests/integration/test_filters.py +123 -55
- nautobot/core/tests/integration/test_general_functionality.py +1 -1
- nautobot/core/tests/integration/test_home.py +10 -18
- nautobot/core/tests/integration/test_import_objects_ui.py +2 -9
- nautobot/core/tests/integration/test_navbar.py +41 -16
- nautobot/core/tests/integration/test_swagger.py +1 -7
- nautobot/core/tests/integration/test_theme.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +6 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_branching.py +154 -0
- nautobot/core/tests/test_breadcrumbs.py +7 -8
- nautobot/core/tests/test_checks.py +28 -0
- nautobot/core/tests/test_commands.py +0 -41
- nautobot/core/tests/test_config.py +2 -1
- nautobot/core/tests/test_csv.py +4 -7
- nautobot/core/tests/test_filters.py +326 -318
- nautobot/core/tests/test_forms.py +19 -30
- nautobot/core/tests/test_graphql.py +67 -57
- nautobot/core/tests/test_models.py +1 -1
- nautobot/core/tests/test_nautobot_server.py +2 -0
- nautobot/core/tests/test_navigations.py +78 -10
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +61 -21
- nautobot/core/tests/test_templatetags_ui_framework.py +36 -18
- nautobot/core/tests/test_ui.py +207 -2
- nautobot/core/tests/test_utils.py +147 -2
- nautobot/core/tests/test_views.py +201 -64
- nautobot/core/tests/test_views_utils.py +1 -1
- nautobot/core/ui/breadcrumbs.py +2 -12
- nautobot/core/ui/choices.py +190 -0
- nautobot/core/ui/constants.py +86 -0
- nautobot/core/ui/echarts.py +474 -0
- nautobot/core/ui/nav.py +5 -1
- nautobot/core/ui/object_detail.py +180 -16
- nautobot/core/urls.py +13 -1
- nautobot/core/utils/cache.py +71 -0
- nautobot/core/utils/data.py +8 -5
- nautobot/core/utils/filtering.py +8 -2
- nautobot/core/utils/git.py +3 -3
- nautobot/core/utils/lookup.py +87 -13
- nautobot/core/utils/migrations.py +22 -0
- nautobot/core/utils/module_loading.py +26 -0
- nautobot/core/utils/permissions.py +9 -5
- nautobot/core/views/__init__.py +114 -63
- nautobot/core/views/generic.py +34 -27
- nautobot/core/views/mixins.py +49 -27
- nautobot/core/views/renderers.py +3 -5
- nautobot/core/views/utils.py +10 -5
- nautobot/core/views/viewsets.py +2 -1
- nautobot/data_validation/__init__.py +0 -0
- nautobot/data_validation/api/__init__.py +1 -0
- nautobot/data_validation/api/serializers.py +80 -0
- nautobot/data_validation/api/urls.py +20 -0
- nautobot/data_validation/api/views.py +44 -0
- nautobot/data_validation/apps.py +18 -0
- nautobot/data_validation/custom_validators.py +330 -0
- nautobot/data_validation/filters.py +133 -0
- nautobot/data_validation/form_mixin.py +25 -0
- nautobot/data_validation/forms.py +342 -0
- nautobot/data_validation/migrations/0001_initial.py +224 -0
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +324 -0
- nautobot/data_validation/migrations/__init__.py +0 -0
- nautobot/data_validation/models.py +361 -0
- nautobot/data_validation/navigation.py +74 -0
- nautobot/data_validation/signals.py +30 -0
- nautobot/data_validation/tables.py +259 -0
- nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +1 -0
- nautobot/data_validation/templates/data_validation/datacompliance_tab.html +11 -0
- nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
- nautobot/data_validation/tests/__init__.py +20 -0
- nautobot/data_validation/tests/migrations/__init__.py +0 -0
- nautobot/data_validation/tests/migrations/test_migrations.py +489 -0
- nautobot/data_validation/tests/test_api.py +238 -0
- nautobot/data_validation/tests/test_custom_validators.py +423 -0
- nautobot/data_validation/tests/test_data_compliance_rules.py +85 -0
- nautobot/data_validation/tests/test_filters.py +240 -0
- nautobot/data_validation/tests/test_form_mixin.py +115 -0
- nautobot/data_validation/tests/test_models.py +393 -0
- nautobot/data_validation/tests/test_views.py +435 -0
- nautobot/data_validation/urls.py +21 -0
- nautobot/data_validation/views.py +227 -0
- nautobot/dcim/api/serializers.py +10 -13
- nautobot/dcim/api/urls.py +2 -0
- nautobot/dcim/api/views.py +7 -0
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +16 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +70 -157
- nautobot/dcim/forms.py +12 -6
- nautobot/dcim/graphql/types.py +1 -0
- nautobot/dcim/migrations/0075_add_deviceclusterassignment.py +52 -0
- nautobot/dcim/migrations/0076_device_cluster_to_clusters_data_migration.py +40 -0
- nautobot/dcim/migrations/0077_remove_device_cluster.py +14 -0
- nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
- nautobot/dcim/models/__init__.py +2 -0
- nautobot/dcim/models/device_components.py +3 -1
- nautobot/dcim/models/devices.py +115 -51
- nautobot/dcim/navigation.py +7 -3
- nautobot/dcim/querysets.py +6 -0
- nautobot/dcim/signals.py +19 -0
- nautobot/dcim/tables/devices.py +9 -5
- nautobot/dcim/tables/template_code.py +191 -102
- nautobot/dcim/templates/dcim/cable.html +1 -1
- nautobot/dcim/templates/dcim/cable_connect.html +62 -146
- nautobot/dcim/templates/dcim/cable_retrieve.html +10 -10
- nautobot/dcim/templates/dcim/cable_trace.html +15 -17
- nautobot/dcim/templates/dcim/console_port_connection_list.html +2 -2
- nautobot/dcim/templates/dcim/consoleport.html +18 -17
- nautobot/dcim/templates/dcim/consoleserverport.html +18 -17
- nautobot/dcim/templates/dcim/controller_create.html +12 -8
- nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +1 -1
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_create.html +6 -6
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/device/config.html +17 -19
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +4 -4
- nautobot/dcim/templates/dcim/device/status.html +20 -20
- nautobot/dcim/templates/dcim/device_component_add.html +24 -15
- nautobot/dcim/templates/dcim/device_create.html +120 -120
- nautobot/dcim/templates/dcim/device_list.html +75 -12
- nautobot/dcim/templates/dcim/devicebay.html +7 -7
- nautobot/dcim/templates/dcim/devicebay_populate.html +29 -23
- nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +6 -6
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- nautobot/dcim/templates/dcim/devicetype_component_add.html +25 -19
- nautobot/dcim/templates/dcim/devicetype_list.html +4 -4
- nautobot/dcim/templates/dcim/devicetype_update.html +9 -9
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +3 -3
- nautobot/dcim/templates/dcim/frontport.html +21 -20
- nautobot/dcim/templates/dcim/inc/cable_form.html +7 -7
- nautobot/dcim/templates/dcim/inc/cable_termination.html +1 -1
- nautobot/dcim/templates/dcim/inc/cable_toggle_buttons.html +18 -9
- nautobot/dcim/templates/dcim/inc/detail_softwareversion_softwareimagefile_rows.html +1 -1
- nautobot/dcim/templates/dcim/inc/device_interface_filter.html +1 -1
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +10 -10
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/inc/homepage_connections.html +2 -2
- nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +10 -10
- nautobot/dcim/templates/dcim/inc/rack_elevation.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +42 -22
- nautobot/dcim/templates/dcim/interface_connection_list.html +2 -2
- nautobot/dcim/templates/dcim/interface_edit.html +26 -11
- nautobot/dcim/templates/dcim/interfaceredundancygroupassociation_create.html +3 -3
- nautobot/dcim/templates/dcim/inventoryitem.html +3 -3
- nautobot/dcim/templates/dcim/inventoryitem_add.html +21 -10
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +6 -4
- nautobot/dcim/templates/dcim/location.html +1 -1
- nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +24 -18
- nautobot/dcim/templates/dcim/location_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/location_update.html +9 -9
- nautobot/dcim/templates/dcim/locationtype.html +0 -1
- nautobot/dcim/templates/dcim/module/base.html +67 -27
- nautobot/dcim/templates/dcim/module_consoleports.html +13 -15
- nautobot/dcim/templates/dcim/module_consoleserverports.html +13 -15
- nautobot/dcim/templates/dcim/module_frontports.html +13 -15
- nautobot/dcim/templates/dcim/module_interfaces.html +14 -16
- nautobot/dcim/templates/dcim/module_list.html +59 -10
- nautobot/dcim/templates/dcim/module_modulebays.html +12 -14
- nautobot/dcim/templates/dcim/module_poweroutlets.html +13 -15
- nautobot/dcim/templates/dcim/module_powerports.html +13 -15
- nautobot/dcim/templates/dcim/module_rearports.html +13 -15
- nautobot/dcim/templates/dcim/module_retrieve.html +3 -3
- nautobot/dcim/templates/dcim/module_update.html +15 -9
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -93
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +7 -7
- nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +74 -35
- nautobot/dcim/templates/dcim/platform_create.html +9 -9
- nautobot/dcim/templates/dcim/power_port_connection_list.html +3 -3
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed_edit.html +15 -15
- nautobot/dcim/templates/dcim/poweroutlet.html +13 -13
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +17 -16
- nautobot/dcim/templates/dcim/rack.html +1 -1
- nautobot/dcim/templates/dcim/rack_elevation.html +2 -2
- nautobot/dcim/templates/dcim/rack_elevation_list.html +21 -9
- nautobot/dcim/templates/dcim/rack_retrieve.html +75 -57
- nautobot/dcim/templates/dcim/rack_update.html +14 -14
- nautobot/dcim/templates/dcim/rackreservation.html +1 -1
- nautobot/dcim/templates/dcim/rackreservation_edit.html +6 -6
- nautobot/dcim/templates/dcim/rearport.html +19 -18
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/trace/circuit.html +1 -1
- nautobot/dcim/templates/dcim/trace/device.html +1 -1
- nautobot/dcim/templates/dcim/trace/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/trace/termination.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +25 -16
- nautobot/dcim/templates/dcim/virtualchassis_create.html +6 -6
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_update.html +36 -22
- nautobot/dcim/templates/dcim/virtualdevicecontext_update.html +9 -9
- nautobot/dcim/tests/integration/test_controller.py +6 -6
- nautobot/dcim/tests/integration/test_controller_managed_device_group.py +7 -7
- nautobot/dcim/tests/integration/test_create_device.py +9 -9
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +7 -2
- nautobot/dcim/tests/integration/test_fileinputpicker.py +5 -7
- nautobot/dcim/tests/integration/test_location_bulk_operations.py +2 -0
- nautobot/dcim/tests/integration/test_module_bay_position.py +4 -1
- nautobot/dcim/tests/test_api.py +86 -6
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +159 -110
- nautobot/dcim/tests/test_graphql.py +32 -36
- nautobot/dcim/tests/test_jobs.py +1 -1
- nautobot/dcim/tests/test_models.py +229 -1
- nautobot/dcim/tests/test_views.py +31 -20
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +77 -41
- nautobot/extras/api/serializers.py +83 -19
- nautobot/extras/api/urls.py +7 -0
- nautobot/extras/api/views.py +243 -140
- nautobot/extras/choices.py +34 -13
- nautobot/extras/constants.py +1 -1
- nautobot/extras/context_managers.py +26 -26
- nautobot/extras/datasources/git.py +22 -0
- nautobot/extras/datasources/registry.py +3 -0
- nautobot/extras/exceptions.py +5 -0
- nautobot/extras/factory.py +11 -1
- nautobot/extras/{filters/mixins.py → filter_mixins.py} +4 -3
- nautobot/extras/{filters/__init__.py → filters.py} +203 -58
- nautobot/extras/forms/base.py +2 -1
- nautobot/extras/forms/forms.py +225 -20
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/homepage.py +21 -2
- nautobot/extras/jobs.py +2 -8
- nautobot/extras/jobs_ui.py +2 -2
- nautobot/extras/management/__init__.py +9 -0
- nautobot/extras/managers.py +31 -22
- nautobot/extras/migrations/0126_approval_workflow_pre_check.py +58 -0
- nautobot/extras/migrations/0127_approval_workflow_models.py +266 -0
- nautobot/extras/migrations/0128_remove_job_approval_required_and_more.py +29 -0
- nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
- nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
- nautobot/extras/models/__init__.py +14 -3
- nautobot/extras/models/approvals.py +556 -0
- nautobot/extras/models/change_logging.py +1 -0
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/customfields.py +57 -22
- nautobot/extras/models/datasources.py +21 -0
- nautobot/extras/models/groups.py +2 -0
- nautobot/extras/models/jobs.py +122 -39
- nautobot/extras/models/metadata.py +2 -3
- nautobot/extras/models/mixins.py +129 -1
- nautobot/extras/models/models.py +22 -14
- nautobot/extras/models/relationships.py +47 -10
- nautobot/extras/models/secrets.py +1 -0
- nautobot/extras/models/statuses.py +0 -15
- nautobot/extras/models/tags.py +1 -1
- nautobot/extras/navigation.py +42 -15
- nautobot/extras/plugins/__init__.py +33 -55
- nautobot/extras/plugins/marketplace_manifest.yml +1 -23
- nautobot/extras/plugins/tables.py +8 -6
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/validators.py +10 -10
- nautobot/extras/plugins/views.py +1 -5
- nautobot/extras/querysets.py +17 -21
- nautobot/extras/signals.py +23 -8
- nautobot/extras/tables.py +420 -99
- nautobot/extras/templates/extras/approval_dashboard.html +15 -0
- nautobot/extras/templates/extras/approval_workflow/approve.html +11 -0
- nautobot/extras/templates/extras/approval_workflow/comment.html +9 -0
- nautobot/extras/templates/extras/approval_workflow/deny.html +10 -0
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +77 -0
- nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +29 -0
- nautobot/extras/templates/extras/configcontext_update.html +12 -12
- nautobot/extras/templates/extras/configcontextschema.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_retrieve.html +9 -9
- nautobot/extras/templates/extras/configcontextschema_update.html +6 -6
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/customfield_update.html +12 -12
- nautobot/extras/templates/extras/dynamicgroup.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_edit.html +1 -1
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +17 -17
- nautobot/extras/templates/extras/dynamicgroup_update.html +24 -24
- nautobot/extras/templates/extras/externalintegration_update.html +6 -6
- nautobot/extras/templates/extras/gitrepository.html +1 -1
- nautobot/extras/templates/extras/gitrepository_object_edit.html +1 -1
- nautobot/extras/templates/extras/gitrepository_result.html +1 -1
- nautobot/extras/templates/extras/gitrepository_retrieve.html +12 -12
- nautobot/extras/templates/extras/gitrepository_update.html +25 -7
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -1
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +38 -0
- nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +14 -13
- nautobot/extras/templates/extras/inc/configcontext_format.html +11 -4
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +7 -7
- nautobot/extras/templates/extras/inc/job_label.html +5 -5
- nautobot/extras/templates/extras/inc/job_table.html +23 -10
- nautobot/extras/templates/extras/inc/job_tiles.html +33 -21
- nautobot/extras/templates/extras/inc/jobresult.html +6 -6
- nautobot/extras/templates/extras/inc/json_format.html +11 -4
- nautobot/extras/templates/extras/inc/object_contact_header.html +6 -6
- nautobot/extras/templates/extras/inc/overridable_field.html +16 -15
- nautobot/extras/templates/extras/inc/panel_approvalworkflowstage.html +34 -0
- nautobot/extras/templates/extras/inc/panel_changelog.html +9 -9
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +8 -6
- nautobot/extras/templates/extras/inc/tags_panel.html +3 -3
- nautobot/extras/templates/extras/job.html +154 -155
- nautobot/extras/templates/extras/job_approval_confirmation.html +4 -27
- nautobot/extras/templates/extras/job_bulk_edit.html +18 -1
- nautobot/extras/templates/extras/job_detail.html +1 -1
- nautobot/extras/templates/extras/job_edit.html +69 -64
- nautobot/extras/templates/extras/job_list.html +37 -60
- nautobot/extras/templates/extras/jobresult.html +1 -1
- nautobot/extras/templates/extras/jobresult_retrieve.html +17 -17
- nautobot/extras/templates/extras/marketplace.html +62 -71
- nautobot/extras/templates/extras/metadatatype_create.html +9 -9
- nautobot/extras/templates/extras/note.html +1 -1
- nautobot/extras/templates/extras/object_approvalworkflow.html +36 -0
- nautobot/extras/templates/extras/object_assign_contact_or_team.html +16 -7
- nautobot/extras/templates/extras/object_configcontext.html +20 -20
- nautobot/extras/templates/extras/object_new_contact.html +6 -6
- nautobot/extras/templates/extras/object_new_team.html +6 -6
- nautobot/extras/templates/extras/objectchange.html +1 -1
- nautobot/extras/templates/extras/objectchange_retrieve.html +37 -56
- nautobot/extras/templates/extras/plugin_detail.html +40 -41
- nautobot/extras/templates/extras/plugins_list.html +23 -38
- nautobot/extras/templates/extras/plugins_tiles.html +28 -28
- nautobot/extras/templates/extras/role_retrieve.html +112 -48
- nautobot/extras/templates/extras/scheduledjob.html +25 -28
- nautobot/extras/templates/extras/secret_create.html +11 -11
- nautobot/extras/templates/extras/secretsgroup_update.html +6 -6
- nautobot/extras/templates/extras/staticgroupassociation_retrieve.html +3 -3
- nautobot/extras/templates/extras/status.html +1 -1
- nautobot/extras/templates/extras/tag.html +1 -1
- nautobot/extras/templates/extras/tag_update.html +3 -3
- nautobot/extras/templates/extras/templatetags/log_level.html +1 -1
- nautobot/extras/templates/extras/templatetags/plugin_object_detail_tabs.html +2 -2
- nautobot/extras/templates/extras/webhook.html +12 -12
- nautobot/extras/templatetags/approvals.py +19 -0
- 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 +5 -8
- nautobot/extras/tests/integration/test_configcontextschema.py +43 -48
- nautobot/extras/tests/integration/test_customfields.py +33 -33
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -10
- nautobot/extras/tests/integration/test_jobs.py +2 -4
- nautobot/extras/tests/integration/test_notes.py +3 -9
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +35 -27
- nautobot/extras/tests/integration/test_relationships.py +7 -11
- nautobot/extras/tests/integration/test_tagfilter.py +3 -11
- nautobot/extras/tests/test_api.py +786 -242
- nautobot/extras/tests/test_approvals.py +715 -0
- nautobot/extras/tests/test_changelog.py +18 -14
- nautobot/extras/tests/test_customfields.py +14 -13
- nautobot/extras/tests/test_datasources.py +1 -1
- nautobot/extras/tests/test_dynamicgroups.py +9 -4
- nautobot/extras/tests/test_filters.py +443 -13
- nautobot/extras/tests/test_forms.py +18 -57
- nautobot/extras/tests/test_jobs.py +25 -4
- nautobot/extras/tests/test_migrations.py +81 -1
- nautobot/extras/tests/test_models.py +378 -47
- nautobot/extras/tests/test_plugins.py +47 -13
- nautobot/extras/tests/test_relationships.py +7 -2
- nautobot/extras/tests/test_utils.py +2 -0
- nautobot/extras/tests/test_views.py +780 -493
- nautobot/extras/urls.py +36 -12
- nautobot/extras/utils.py +58 -12
- nautobot/extras/views.py +668 -209
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +35 -71
- 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 -1
- nautobot/ipam/querysets.py +1 -2
- nautobot/ipam/tables.py +26 -17
- nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +6 -6
- nautobot/ipam/templates/ipam/inc/service.html +8 -8
- nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +3 -2
- nautobot/ipam/templates/ipam/ipaddress.html +27 -13
- nautobot/ipam/templates/ipam/ipaddress_assign.html +31 -24
- nautobot/ipam/templates/ipam/ipaddress_bulk_add.html +3 -3
- nautobot/ipam/templates/ipam/ipaddress_edit.html +9 -9
- nautobot/ipam/templates/ipam/ipaddress_interfaces.html +7 -9
- nautobot/ipam/templates/ipam/ipaddress_merge.html +195 -186
- nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +7 -9
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +7 -5
- 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_create.html +9 -9
- nautobot/ipam/templates/ipam/prefix_list.html +15 -14
- nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_update.html +6 -6
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/templates/ipam/vrf_edit.html +15 -15
- nautobot/ipam/tests/integration/test_prefixes.py +5 -13
- nautobot/ipam/tests/migration/test_migrations.py +89 -0
- nautobot/ipam/tests/test_api.py +20 -7
- nautobot/ipam/tests/test_filters.py +10 -0
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +1 -1
- nautobot/ipam/tests/test_tables.py +1 -2
- nautobot/ipam/tests/test_utils.py +1 -1
- nautobot/ipam/tests/test_views.py +24 -21
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/utils/migrations.py +16 -2
- nautobot/ipam/utils/testing.py +9 -3
- nautobot/ipam/views.py +49 -7
- nautobot/project-static/dist/css/graphql-libraries.css +655 -0
- nautobot/project-static/dist/css/graphql-libraries.css.map +1 -0
- nautobot/project-static/dist/css/materialdesignicons.css +3 -0
- nautobot/project-static/dist/css/materialdesignicons.css.map +1 -0
- nautobot/project-static/dist/css/nautobot.css +13 -0
- nautobot/project-static/dist/css/nautobot.css.map +1 -0
- nautobot/project-static/dist/js/graphql-libraries.js +3 -0
- nautobot/project-static/dist/js/graphql-libraries.js.LICENSE.txt +62 -0
- nautobot/project-static/dist/js/graphql-libraries.js.map +1 -0
- nautobot/project-static/dist/js/libraries.js +3 -0
- nautobot/project-static/dist/js/libraries.js.LICENSE.txt +65 -0
- nautobot/project-static/dist/js/libraries.js.map +1 -0
- nautobot/project-static/dist/js/materialdesignicons.js +0 -0
- nautobot/project-static/dist/js/nautobot-graphiql.js +2 -0
- nautobot/project-static/dist/js/nautobot-graphiql.js.map +1 -0
- nautobot/project-static/dist/js/nautobot.js +2 -0
- nautobot/project-static/dist/js/nautobot.js.map +1 -0
- nautobot/project-static/fonts/Montserrat-v30-Bold.woff2 +0 -0
- nautobot/project-static/fonts/Montserrat-v30-Light.woff2 +0 -0
- nautobot/project-static/fonts/Montserrat-v30-Regular.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Bold.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Light.woff2 +0 -0
- nautobot/project-static/fonts/Roboto-v48-Regular.woff2 +0 -0
- nautobot/project-static/img/jinja_logo.svg +21 -92
- nautobot/project-static/js/cabletrace.js +1 -1
- nautobot/project-static/js/editor.js +4 -4
- nautobot/project-static/js/forms.js +67 -717
- nautobot/project-static/js/job_result.js +2 -2
- nautobot/project-static/nautobot-icons/360-degrees.svg +3 -0
- nautobot/project-static/nautobot-icons/arrow-decision.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-expand-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-move-2-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/arrows-move-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/atom.svg +3 -0
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/branch.svg +3 -0
- nautobot/project-static/nautobot-icons/briefcase-2.svg +3 -0
- nautobot/project-static/nautobot-icons/cable-data-2.svg +3 -0
- nautobot/project-static/nautobot-icons/cable-data.svg +3 -0
- nautobot/project-static/nautobot-icons/cast.svg +3 -0
- nautobot/project-static/nautobot-icons/check-circle.svg +3 -0
- nautobot/project-static/nautobot-icons/checkbox-circle.svg +3 -0
- nautobot/project-static/nautobot-icons/checkbox-rec.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-check.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-lightning.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud-upload.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud.svg +3 -0
- nautobot/project-static/nautobot-icons/compass.svg +3 -0
- nautobot/project-static/nautobot-icons/control-panel.svg +3 -0
- nautobot/project-static/nautobot-icons/credit-card.svg +3 -0
- nautobot/project-static/nautobot-icons/device-lifecycle.svg +3 -0
- nautobot/project-static/nautobot-icons/direction.svg +3 -0
- nautobot/project-static/nautobot-icons/elements.svg +3 -0
- nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
- nautobot/project-static/nautobot-icons/globe-2.svg +3 -0
- nautobot/project-static/nautobot-icons/globe.svg +3 -0
- nautobot/project-static/nautobot-icons/hammer.svg +3 -0
- nautobot/project-static/nautobot-icons/history.svg +3 -0
- nautobot/project-static/nautobot-icons/ip.svg +3 -0
- nautobot/project-static/nautobot-icons/laptop.svg +3 -0
- nautobot/project-static/nautobot-icons/lightning.svg +3 -0
- nautobot/project-static/nautobot-icons/list-unordered.svg +3 -0
- nautobot/project-static/nautobot-icons/map-view.svg +3 -0
- nautobot/project-static/nautobot-icons/organization.svg +3 -0
- nautobot/project-static/nautobot-icons/pin-2.svg +3 -0
- nautobot/project-static/nautobot-icons/pin-3.svg +3 -0
- nautobot/project-static/nautobot-icons/plug.svg +3 -0
- nautobot/project-static/nautobot-icons/refresh-cw.svg +3 -0
- nautobot/project-static/nautobot-icons/rocket-2.svg +3 -0
- nautobot/project-static/nautobot-icons/rotate-cw.svg +3 -0
- nautobot/project-static/nautobot-icons/route.svg +3 -0
- nautobot/project-static/nautobot-icons/secrets.svg +3 -0
- nautobot/project-static/nautobot-icons/security.svg +3 -0
- nautobot/project-static/nautobot-icons/server-2.svg +3 -0
- nautobot/project-static/nautobot-icons/server.svg +3 -0
- nautobot/project-static/nautobot-icons/share.svg +3 -0
- nautobot/project-static/nautobot-icons/shield-check.svg +3 -0
- nautobot/project-static/nautobot-icons/sitemap-outline.svg +3 -0
- nautobot/project-static/nautobot-icons/sliders-vert-2.svg +3 -0
- nautobot/project-static/nautobot-icons/sliders-vert.svg +3 -0
- nautobot/project-static/nautobot-icons/star-filled.svg +3 -0
- nautobot/project-static/nautobot-icons/star.svg +3 -0
- nautobot/project-static/nautobot-icons/transform.svg +3 -0
- nautobot/project-static/nautobot-icons/wifi.svg +3 -0
- 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/templates/tenancy/tenant_create.html +6 -6
- nautobot/tenancy/tests/test_filters.py +0 -2
- nautobot/tenancy/views.py +2 -1
- nautobot/ui/.gitignore +137 -0
- nautobot/ui/.node-version +1 -0
- nautobot/ui/.prettierignore +3 -0
- nautobot/ui/eslint.config.js +33 -0
- nautobot/ui/package-lock.json +6594 -0
- nautobot/ui/package.json +67 -0
- nautobot/ui/prettier.config.js +9 -0
- nautobot/ui/src/js/collapse.js +69 -0
- nautobot/ui/src/js/cookie.js +31 -0
- nautobot/ui/src/js/draggable.js +101 -0
- nautobot/ui/src/js/drawer.js +106 -0
- nautobot/ui/src/js/form.js +23 -0
- nautobot/ui/src/js/history.js +51 -0
- nautobot/ui/src/js/nautobot-graphiql.js +19 -0
- nautobot/ui/src/js/nautobot.js +128 -0
- nautobot/ui/src/js/search.js +274 -0
- nautobot/ui/src/js/select2.js +318 -0
- nautobot/ui/src/js/sidenav.js +87 -0
- nautobot/ui/src/js/tabs.js +139 -0
- nautobot/ui/src/js/theme.js +104 -0
- nautobot/ui/src/js/utils.js +54 -0
- nautobot/ui/src/scss/colors.scss +58 -0
- nautobot/ui/src/scss/nautobot.scss +2471 -0
- nautobot/ui/webpack.config.js +148 -0
- nautobot/users/apps.py +3 -0
- nautobot/users/filters.py +7 -11
- nautobot/users/forms.py +10 -0
- nautobot/users/models.py +8 -0
- nautobot/users/templates/users/advanced_settings_edit.html +31 -21
- nautobot/users/templates/users/api_tokens.html +61 -51
- nautobot/users/templates/users/base.html +23 -31
- nautobot/users/templates/users/change_password.html +29 -19
- nautobot/users/templates/users/preferences.html +55 -45
- nautobot/users/templates/users/profile.html +45 -14
- nautobot/users/tests/test_api.py +4 -0
- nautobot/users/urls.py +2 -0
- nautobot/users/views.py +70 -2
- nautobot/virtualization/api/views.py +1 -1
- nautobot/virtualization/filters.py +18 -32
- nautobot/virtualization/forms.py +22 -59
- nautobot/virtualization/models.py +1 -19
- nautobot/virtualization/navigation.py +3 -1
- nautobot/virtualization/tables.py +10 -6
- nautobot/virtualization/templates/virtualization/cluster.html +13 -13
- nautobot/virtualization/templates/virtualization/cluster_edit.html +6 -6
- nautobot/virtualization/templates/virtualization/inc/virtualmachine_vminterface_filter.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_component_add.html +24 -16
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_list.html +4 -4
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +27 -25
- nautobot/virtualization/templates/virtualization/vminterface.html +5 -5
- nautobot/virtualization/templates/virtualization/vminterface_edit.html +27 -11
- nautobot/virtualization/tests/test_api.py +3 -0
- nautobot/virtualization/tests/test_models.py +20 -5
- nautobot/virtualization/tests/test_views.py +3 -5
- nautobot/virtualization/urls.py +0 -11
- nautobot/virtualization/views.py +5 -122
- nautobot/vpn/__init__.py +0 -0
- nautobot/vpn/api/serializers.py +113 -0
- nautobot/vpn/api/urls.py +19 -0
- nautobot/vpn/api/views.py +70 -0
- nautobot/vpn/apps.py +8 -0
- nautobot/vpn/choices.py +171 -0
- nautobot/vpn/factory.py +209 -0
- nautobot/vpn/filters.py +233 -0
- nautobot/vpn/forms.py +486 -0
- nautobot/vpn/homepage.py +19 -0
- nautobot/vpn/migrations/0001_initial.py +541 -0
- nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
- nautobot/vpn/migrations/__init__.py +0 -0
- nautobot/vpn/models.py +527 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +380 -0
- nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +341 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +294 -0
- nautobot/vpn/tests/test_models.py +97 -0
- nautobot/vpn/tests/test_views.py +281 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +437 -0
- nautobot/wireless/filters.py +0 -8
- nautobot/wireless/navigation.py +3 -1
- nautobot/wireless/templates/wireless/wirelessnetwork_create.html +6 -6
- nautobot/wireless/tests/integration/test_radio_profile.py +3 -7
- nautobot/wireless/tests/test_api.py +1 -1
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +5 -4
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +802 -707
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
- nautobot/core/management/commands/check_job_approval_status.py +0 -47
- nautobot/core/templates/search_form.html +0 -9
- nautobot/core/templates/utilities/templatetags/filter_form_modal.html +0 -87
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/extras/templates/extras/job_approval_request.html +0 -134
- nautobot/extras/templates/extras/scheduled_jobs_approval_queue_list.html +0 -28
- nautobot/ipam/mixins.py +0 -32
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css +0 -587
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +0 -6865
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +0 -1
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.eot +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.svg +0 -288
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.ttf +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/fonts/glyphicons-halflings-regular.woff2 +0 -0
- nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.js +0 -2580
- nautobot/project-static/bootstrap-3.4.1-dist/js/bootstrap.min.js +0 -6
- nautobot/project-static/bootstrap-3.4.1-dist/js/npm.js +0 -13
- nautobot/project-static/clipboard.js-2.0.9/clipboard.min.js +0 -7
- nautobot/project-static/css/base.css +0 -1040
- nautobot/project-static/css/dark.css +0 -282
- nautobot/project-static/flatpickr-4.6.9/flatpickr.min.js +0 -2
- nautobot/project-static/flatpickr-4.6.9/themes/light.min.css +0 -1
- nautobot/project-static/graphiql-1.5.16/graphiql.min.css +0 -12
- nautobot/project-static/graphiql-1.5.16/graphiql.min.js +0 -11
- nautobot/project-static/highlight.js-11.9.0/github-dark.min.css +0 -10
- nautobot/project-static/highlight.js-11.9.0/github.min.css +0 -10
- nautobot/project-static/highlight.js-11.9.0/highlight.min.js +0 -378
- nautobot/project-static/jquery/jquery-3.7.1.min.js +0 -2
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_444444_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_555555_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777620_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_777777_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_cc0000_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/images/ui-icons_ffffff_256x240.png +0 -0
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.css +0 -7
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.min.js +0 -6
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.structure.min.css +0 -5
- nautobot/project-static/jquery-ui-1.13.2/jquery-ui.theme.min.css +0 -5
- nautobot/project-static/js/homepage_layout.js +0 -182
- nautobot/project-static/js/nav_menu.js +0 -250
- nautobot/project-static/js/theme.js +0 -133
- nautobot/project-static/materialdesignicons-7.4.47/LICENSE +0 -20
- nautobot/project-static/materialdesignicons-7.4.47/css/materialdesignicons.min.css +0 -3
- nautobot/project-static/react-16.14.0/react.production.min.js +0 -32
- nautobot/project-static/react-dom-16.14.0/react-dom.production.min.js +0 -239
- nautobot/project-static/select2-4.0.13/i18n/af.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ar.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/az.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bg.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bn.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/bs.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ca.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/cs.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/da.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/de.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/dsb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/el.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/en.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/es.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/et.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/eu.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fa.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/fr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/gl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/he.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hsb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hu.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/hy.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/id.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/is.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/it.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ja.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ka.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/km.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ko.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/lt.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/lv.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/mk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ms.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/nb.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ne.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/nl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ps.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pt-BR.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/pt.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ro.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/ru.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sq.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sr-Cyrl.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/sv.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/th.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/tk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/tr.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/uk.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/vi.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/zh-CN.js +0 -3
- nautobot/project-static/select2-4.0.13/i18n/zh-TW.js +0 -3
- nautobot/project-static/select2-4.0.13/select2.min.css +0 -1
- nautobot/project-static/select2-4.0.13/select2.min.js +0 -2
- nautobot/project-static/select2-bootstrap-0.1.0-beta.10/select2-bootstrap.min.css +0 -7
- nautobot/project-static/subscriptions-transport-ws-0.9.18/client.min.js +0 -8
- nautobot/project-static/whatwg-fetch-3.6.2/fetch.umd.min.js +0 -8
- nautobot/virtualization/templates/virtualization/cluster_add_devices.html +0 -37
- /nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.ttf → dist/1fcc36272ea3e53d0031.ttf} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.eot → dist/2146c3c82b553977abc7.eot} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff → dist/e55a20c80650829ec5fd.woff} +0 -0
- /nautobot/project-static/{materialdesignicons-7.4.47/fonts/materialdesignicons-webfont.woff2 → dist/ec024da790d2972da002.woff2} +0 -0
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
- {nautobot-2.4.21.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
nautobot/dcim/forms.py
CHANGED
|
@@ -2112,9 +2112,9 @@ class DeviceForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm, LocalC
|
|
|
2112
2112
|
queryset=ClusterGroup.objects.all(),
|
|
2113
2113
|
required=False,
|
|
2114
2114
|
null_option="None",
|
|
2115
|
-
initial_params={"
|
|
2115
|
+
initial_params={"clusters__in": "$clusters"},
|
|
2116
2116
|
)
|
|
2117
|
-
|
|
2117
|
+
clusters = DynamicModelMultipleChoiceField(
|
|
2118
2118
|
queryset=Cluster.objects.all(),
|
|
2119
2119
|
required=False,
|
|
2120
2120
|
query_params={"cluster_group": "$cluster_group"},
|
|
@@ -2160,7 +2160,7 @@ class DeviceForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm, LocalC
|
|
|
2160
2160
|
"primary_ip6",
|
|
2161
2161
|
"secrets_group",
|
|
2162
2162
|
"cluster_group",
|
|
2163
|
-
"
|
|
2163
|
+
"clusters",
|
|
2164
2164
|
"tenant_group",
|
|
2165
2165
|
"tenant",
|
|
2166
2166
|
"vrfs",
|
|
@@ -2238,6 +2238,7 @@ class DeviceForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm, LocalC
|
|
|
2238
2238
|
self.initial["location"] = self.instance.parent_bay.device.location_id
|
|
2239
2239
|
self.initial["rack"] = self.instance.parent_bay.device.rack_id
|
|
2240
2240
|
|
|
2241
|
+
self.initial["clusters"] = self.instance.clusters.values_list("id", flat=True)
|
|
2241
2242
|
self.initial["vrfs"] = self.instance.vrfs.values_list("id", flat=True)
|
|
2242
2243
|
|
|
2243
2244
|
else:
|
|
@@ -2274,6 +2275,7 @@ class DeviceForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm, LocalC
|
|
|
2274
2275
|
def save(self, *args, **kwargs):
|
|
2275
2276
|
instance = super().save(*args, **kwargs)
|
|
2276
2277
|
instance.vrfs.set(self.cleaned_data["vrfs"])
|
|
2278
|
+
instance.clusters.set(self.cleaned_data["clusters"])
|
|
2277
2279
|
return instance
|
|
2278
2280
|
|
|
2279
2281
|
|
|
@@ -2306,7 +2308,12 @@ class DeviceBulkEditForm(
|
|
|
2306
2308
|
rack_group = DynamicModelChoiceField(
|
|
2307
2309
|
queryset=RackGroup.objects.all(), required=False, query_params={"location": "$location"}
|
|
2308
2310
|
)
|
|
2309
|
-
|
|
2311
|
+
add_clusters = DynamicModelMultipleChoiceField(
|
|
2312
|
+
queryset=Cluster.objects.all(), required=False, label="Add to clusters"
|
|
2313
|
+
)
|
|
2314
|
+
remove_clusters = DynamicModelMultipleChoiceField(
|
|
2315
|
+
queryset=Cluster.objects.all(), required=False, label="Remove from clusters"
|
|
2316
|
+
)
|
|
2310
2317
|
comments = CommentField(widget=SmallTextarea, label="Comments")
|
|
2311
2318
|
tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
|
2312
2319
|
platform = DynamicModelChoiceField(queryset=Platform.objects.all(), required=False)
|
|
@@ -2329,7 +2336,6 @@ class DeviceBulkEditForm(
|
|
|
2329
2336
|
"rack",
|
|
2330
2337
|
"position",
|
|
2331
2338
|
"face",
|
|
2332
|
-
"cluster",
|
|
2333
2339
|
"comments",
|
|
2334
2340
|
"secrets_group",
|
|
2335
2341
|
"device_redundancy_group",
|
|
@@ -4571,7 +4577,7 @@ class BaseVCMemberFormSet(forms.BaseModelFormSet):
|
|
|
4571
4577
|
vc_position_list.append(vc_position)
|
|
4572
4578
|
|
|
4573
4579
|
|
|
4574
|
-
class DeviceVCMembershipForm(forms.ModelForm):
|
|
4580
|
+
class DeviceVCMembershipForm(BootstrapMixin, forms.ModelForm):
|
|
4575
4581
|
class Meta:
|
|
4576
4582
|
model = Device
|
|
4577
4583
|
fields = [
|
nautobot/dcim/graphql/types.py
CHANGED
|
@@ -68,6 +68,7 @@ class DeviceType(OptimizedNautobotObjectType):
|
|
|
68
68
|
all_power_ports = graphene.List("nautobot.dcim.graphql.types.PowerPortType")
|
|
69
69
|
all_power_outlets = graphene.List("nautobot.dcim.graphql.types.PowerOutletType")
|
|
70
70
|
all_rear_ports = graphene.List("nautobot.dcim.graphql.types.RearPortType")
|
|
71
|
+
cluster = graphene.Field("nautobot.virtualization.graphql.types.ClusterType")
|
|
71
72
|
common_vc_interfaces = graphene.List("nautobot.dcim.graphql.types.InterfaceType")
|
|
72
73
|
dynamic_groups = graphene.List("nautobot.extras.graphql.types.DynamicGroupType")
|
|
73
74
|
primary_ip = graphene.Field("nautobot.ipam.graphql.types.IPAddressType")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("dcim", "0074_alter_rack_u_height"),
|
|
10
|
+
("virtualization", "0030_alter_virtualmachine_local_config_context_data_owner_content_type_and_more"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.CreateModel(
|
|
15
|
+
name="DeviceClusterAssignment",
|
|
16
|
+
fields=[
|
|
17
|
+
(
|
|
18
|
+
"id",
|
|
19
|
+
models.UUIDField(
|
|
20
|
+
default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True
|
|
21
|
+
),
|
|
22
|
+
),
|
|
23
|
+
(
|
|
24
|
+
"cluster",
|
|
25
|
+
models.ForeignKey(
|
|
26
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
27
|
+
related_name="device_assignments",
|
|
28
|
+
to="virtualization.cluster",
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
"device",
|
|
33
|
+
models.ForeignKey(
|
|
34
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
35
|
+
related_name="cluster_assignments",
|
|
36
|
+
to="dcim.device",
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
options={
|
|
41
|
+
"ordering": ["device", "cluster"],
|
|
42
|
+
"unique_together": {("device", "cluster")},
|
|
43
|
+
},
|
|
44
|
+
),
|
|
45
|
+
migrations.AddField(
|
|
46
|
+
model_name="device",
|
|
47
|
+
name="clusters",
|
|
48
|
+
field=models.ManyToManyField(
|
|
49
|
+
blank=True, related_name="devices", through="dcim.DeviceClusterAssignment", to="virtualization.cluster"
|
|
50
|
+
),
|
|
51
|
+
),
|
|
52
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from django.db import migrations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def migrate_device_cluster_assignments(apps, schema_editor):
|
|
5
|
+
"""
|
|
6
|
+
Migrate existing device cluster assignments from the old ForeignKey field
|
|
7
|
+
to the new many-to-many relationship through DeviceClusterAssignment.
|
|
8
|
+
"""
|
|
9
|
+
Device = apps.get_model("dcim", "Device")
|
|
10
|
+
DeviceClusterAssignment = apps.get_model("dcim", "DeviceClusterAssignment")
|
|
11
|
+
devices_with_clusters = Device.objects.filter(cluster__isnull=False).select_related("cluster")
|
|
12
|
+
assignments_to_create = []
|
|
13
|
+
for device in devices_with_clusters:
|
|
14
|
+
assignments_to_create.append(DeviceClusterAssignment(device=device, cluster=device.cluster))
|
|
15
|
+
if assignments_to_create:
|
|
16
|
+
DeviceClusterAssignment.objects.bulk_create(assignments_to_create, batch_size=1000)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def reverse_migrate_device_cluster_assignments(apps, schema_editor):
|
|
20
|
+
"""
|
|
21
|
+
Reverse migration - restore the cluster field from DeviceClusterAssignment records.
|
|
22
|
+
Note: This can only restore one cluster per device if multiple exist.
|
|
23
|
+
"""
|
|
24
|
+
DeviceClusterAssignment = apps.get_model("dcim", "DeviceClusterAssignment")
|
|
25
|
+
|
|
26
|
+
# For each device, get the first cluster assignment and set it as the device's cluster
|
|
27
|
+
for assignment in DeviceClusterAssignment.objects.select_related("device", "cluster"):
|
|
28
|
+
if not assignment.device.cluster:
|
|
29
|
+
assignment.device.cluster = assignment.cluster
|
|
30
|
+
assignment.device.save(update_fields=["cluster"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Migration(migrations.Migration):
|
|
34
|
+
dependencies = [
|
|
35
|
+
("dcim", "0075_add_deviceclusterassignment"),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
operations = [
|
|
39
|
+
migrations.RunPython(migrate_device_cluster_assignments, reverse_migrate_device_cluster_assignments),
|
|
40
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from django.db import migrations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration(migrations.Migration):
|
|
5
|
+
dependencies = [
|
|
6
|
+
("dcim", "0076_device_cluster_to_clusters_data_migration"),
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
operations = [
|
|
10
|
+
migrations.RemoveField(
|
|
11
|
+
model_name="device",
|
|
12
|
+
name="cluster",
|
|
13
|
+
),
|
|
14
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Generated by Django 4.2.24 on 2025-10-07 07:18
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("dcim", "0077_remove_device_cluster"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.AlterUniqueTogether(
|
|
13
|
+
name="device",
|
|
14
|
+
unique_together={("rack", "position", "face"), ("virtual_chassis", "vc_position")},
|
|
15
|
+
),
|
|
16
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Generated by Django 4.2.24 on 2025-10-07 07:18
|
|
2
|
+
|
|
3
|
+
from constance.models import Constance
|
|
4
|
+
from django.db import migrations
|
|
5
|
+
|
|
6
|
+
from nautobot.core.utils.config import get_settings_or_config
|
|
7
|
+
from nautobot.dcim.choices import DeviceUniquenessChoices
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def migrate_device_uniqueness_setting(apps, schema_editor):
|
|
11
|
+
"""
|
|
12
|
+
Migrate deprecated DEVICE_NAME_AS_NATURAL_KEY setting to DEVICE_UNIQUENESS and DEVICE_NAME_REQUIRED.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
old_value = get_settings_or_config("DEVICE_NAME_AS_NATURAL_KEY")
|
|
16
|
+
except AttributeError:
|
|
17
|
+
old_value = False
|
|
18
|
+
new_value = DeviceUniquenessChoices.NAME if old_value else DeviceUniquenessChoices.LOCATION_TENANT_NAME
|
|
19
|
+
|
|
20
|
+
# Update or create DEVICE_UNIQUENESS in Constance
|
|
21
|
+
Constance.objects.update_or_create(
|
|
22
|
+
key="DEVICE_UNIQUENESS",
|
|
23
|
+
defaults={"value": new_value},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# cleanup: remove deprecated setting entry if it exists
|
|
27
|
+
Constance.objects.filter(key="DEVICE_NAME_AS_NATURAL_KEY").delete()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def reverse_migrate_device_uniqueness_setting(apps, schema_editor):
|
|
31
|
+
"""
|
|
32
|
+
Reverse migration set DEVICE_NAME_AS_NATURAL_KEY based on DEVICE_UNIQUENESS.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
device_uniqueness = Constance.objects.get(key="DEVICE_UNIQUENESS").value
|
|
36
|
+
except Constance.DoesNotExist:
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
if device_uniqueness == DeviceUniquenessChoices.NAME:
|
|
40
|
+
Constance.objects.update_or_create(key="DEVICE_NAME_AS_NATURAL_KEY", defaults={"value": True})
|
|
41
|
+
else:
|
|
42
|
+
Constance.objects.update_or_create(key="DEVICE_NAME_AS_NATURAL_KEY", defaults={"value": False})
|
|
43
|
+
|
|
44
|
+
# cleanup: remove DEVICE_UNIQUENESS setting entry if it exists
|
|
45
|
+
Constance.objects.filter(key="DEVICE_UNIQUENESS").delete()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Migration(migrations.Migration):
|
|
49
|
+
dependencies = [
|
|
50
|
+
("dcim", "0078_remove_device_location_tenant_name_uniqueness"),
|
|
51
|
+
("constance", "0003_drop_pickle"),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
operations = [
|
|
55
|
+
migrations.RunPython(
|
|
56
|
+
migrate_device_uniqueness_setting,
|
|
57
|
+
reverse_migrate_device_uniqueness_setting,
|
|
58
|
+
),
|
|
59
|
+
]
|
nautobot/dcim/models/__init__.py
CHANGED
|
@@ -31,6 +31,7 @@ from .devices import (
|
|
|
31
31
|
Controller,
|
|
32
32
|
ControllerManagedDeviceGroup,
|
|
33
33
|
Device,
|
|
34
|
+
DeviceClusterAssignment,
|
|
34
35
|
DeviceFamily,
|
|
35
36
|
DeviceRedundancyGroup,
|
|
36
37
|
DeviceType,
|
|
@@ -64,6 +65,7 @@ __all__ = (
|
|
|
64
65
|
"Device",
|
|
65
66
|
"DeviceBay",
|
|
66
67
|
"DeviceBayTemplate",
|
|
68
|
+
"DeviceClusterAssignment",
|
|
67
69
|
"DeviceFamily",
|
|
68
70
|
"DeviceRedundancyGroup",
|
|
69
71
|
"DeviceType",
|
|
@@ -16,6 +16,7 @@ from nautobot.core.models.generics import BaseModel, PrimaryModel
|
|
|
16
16
|
from nautobot.core.models.ordering import naturalize_interface
|
|
17
17
|
from nautobot.core.models.query_functions import CollateAsChar
|
|
18
18
|
from nautobot.core.models.tree_queries import TreeModel
|
|
19
|
+
from nautobot.core.utils.cache import construct_cache_key
|
|
19
20
|
from nautobot.core.utils.data import UtilizationData
|
|
20
21
|
from nautobot.dcim.choices import (
|
|
21
22
|
ConsolePortTypeChoices,
|
|
@@ -1349,4 +1350,5 @@ class ModuleBay(PrimaryModel):
|
|
|
1349
1350
|
|
|
1350
1351
|
if self.parent_device is not None:
|
|
1351
1352
|
# Set the has_module_bays cache key on the parent device - see Device.has_module_bays()
|
|
1352
|
-
|
|
1353
|
+
cache_key = construct_cache_key(self.parent_device, method_name="has_module_bays", branch_aware=True)
|
|
1354
|
+
cache.set(cache_key, True, timeout=5)
|
nautobot/dcim/models/devices.py
CHANGED
|
@@ -18,18 +18,21 @@ from nautobot.core.models import BaseManager, RestrictedQuerySet
|
|
|
18
18
|
from nautobot.core.models.fields import JSONArrayField, LaxURLField, NaturalOrderingField
|
|
19
19
|
from nautobot.core.models.generics import BaseModel, OrganizationalModel, PrimaryModel
|
|
20
20
|
from nautobot.core.models.tree_queries import TreeModel
|
|
21
|
+
from nautobot.core.templatetags.helpers import HTML_NONE
|
|
22
|
+
from nautobot.core.utils.cache import construct_cache_key
|
|
21
23
|
from nautobot.core.utils.config import get_settings_or_config
|
|
22
24
|
from nautobot.dcim.choices import (
|
|
23
25
|
ControllerCapabilitiesChoices,
|
|
24
26
|
DeviceFaceChoices,
|
|
25
27
|
DeviceRedundancyGroupFailoverStrategyChoices,
|
|
28
|
+
DeviceUniquenessChoices,
|
|
26
29
|
SoftwareImageFileHashingAlgorithmChoices,
|
|
27
30
|
SubdeviceRoleChoices,
|
|
28
31
|
)
|
|
29
32
|
from nautobot.dcim.constants import MODULE_RECURSION_DEPTH_LIMIT
|
|
33
|
+
from nautobot.dcim.querysets import DeviceQuerySet
|
|
30
34
|
from nautobot.dcim.utils import get_all_network_driver_mappings, get_network_driver_mapping_tool_names
|
|
31
35
|
from nautobot.extras.models import ChangeLoggedModel, ConfigContextModel, RoleField, StatusField
|
|
32
|
-
from nautobot.extras.querysets import ConfigContextModelQuerySet
|
|
33
36
|
from nautobot.extras.utils import extras_features
|
|
34
37
|
from nautobot.wireless.models import (
|
|
35
38
|
ControllerManagedDeviceGroupRadioProfileAssignment,
|
|
@@ -565,13 +568,44 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
565
568
|
null=True,
|
|
566
569
|
verbose_name="Primary IPv6",
|
|
567
570
|
)
|
|
568
|
-
|
|
571
|
+
clusters = models.ManyToManyField(
|
|
569
572
|
to="virtualization.Cluster",
|
|
570
|
-
on_delete=models.SET_NULL,
|
|
571
573
|
related_name="devices",
|
|
574
|
+
through="dcim.DeviceClusterAssignment",
|
|
572
575
|
blank=True,
|
|
573
|
-
null=True,
|
|
574
576
|
)
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def cluster(self):
|
|
580
|
+
"""
|
|
581
|
+
Returns the only cluster assigned to this device.
|
|
582
|
+
|
|
583
|
+
Deprecated. Use `clusters` instead.
|
|
584
|
+
|
|
585
|
+
TODO: Remove this property in v4.0.0
|
|
586
|
+
"""
|
|
587
|
+
if self.clusters.count() > 1:
|
|
588
|
+
raise self.clusters.model.MultipleObjectsReturned(
|
|
589
|
+
"Multiple Cluster objects returned. Please refer to clusters."
|
|
590
|
+
)
|
|
591
|
+
return self.clusters.first()
|
|
592
|
+
|
|
593
|
+
@cluster.setter
|
|
594
|
+
def cluster(self, value):
|
|
595
|
+
"""
|
|
596
|
+
Sets the clusters field to a single value.
|
|
597
|
+
|
|
598
|
+
Deprecated. Use `clusters` instead.
|
|
599
|
+
|
|
600
|
+
TODO: Remove this property in v4.0.0
|
|
601
|
+
"""
|
|
602
|
+
# If the device hasn't been saved yet, defer the cluster assignment
|
|
603
|
+
if not self.present_in_database:
|
|
604
|
+
self._deferred_cluster = value
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
self.assign_cluster(value)
|
|
608
|
+
|
|
575
609
|
virtual_chassis = models.ForeignKey(
|
|
576
610
|
to="VirtualChassis",
|
|
577
611
|
on_delete=models.SET_NULL,
|
|
@@ -632,7 +666,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
632
666
|
null=True,
|
|
633
667
|
)
|
|
634
668
|
|
|
635
|
-
objects = BaseManager.from_queryset(
|
|
669
|
+
objects = BaseManager.from_queryset(DeviceQuerySet)()
|
|
636
670
|
|
|
637
671
|
clone_fields = [
|
|
638
672
|
"device_type",
|
|
@@ -642,26 +676,27 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
642
676
|
"location",
|
|
643
677
|
"rack",
|
|
644
678
|
"status",
|
|
645
|
-
"
|
|
679
|
+
"clusters",
|
|
646
680
|
"secrets_group",
|
|
647
681
|
]
|
|
648
682
|
|
|
649
683
|
@classproperty # https://github.com/PyCQA/pylint-django/issues/240
|
|
650
684
|
def natural_key_field_names(cls): # pylint: disable=no-self-argument
|
|
651
685
|
"""
|
|
652
|
-
|
|
686
|
+
Check DEVICE_UNIQUENESS from settings or Constance and return proper field.
|
|
653
687
|
"""
|
|
654
|
-
if get_settings_or_config("
|
|
655
|
-
# opt-in
|
|
688
|
+
if get_settings_or_config("DEVICE_UNIQUENESS") == DeviceUniquenessChoices.NAME:
|
|
689
|
+
# Simplified pseudo-natural key (opt-in for name-only uniqueness)
|
|
656
690
|
return ["name"]
|
|
691
|
+
elif get_settings_or_config("DEVICE_UNIQUENESS") == DeviceUniquenessChoices.LOCATION_TENANT_NAME:
|
|
692
|
+
# Full natural key based on tenant, location, and name
|
|
693
|
+
return ["name", "tenant", "location"]
|
|
657
694
|
else:
|
|
658
|
-
|
|
659
|
-
return ["name", "tenant", "location"] # location should be last since it's potentially variadic
|
|
695
|
+
return ["pk"]
|
|
660
696
|
|
|
661
697
|
class Meta:
|
|
662
698
|
ordering = ("_name",) # Name may be null
|
|
663
699
|
unique_together = (
|
|
664
|
-
("location", "tenant", "name"), # See validate_unique below
|
|
665
700
|
("rack", "position", "face"),
|
|
666
701
|
("virtual_chassis", "vc_position"),
|
|
667
702
|
)
|
|
@@ -669,15 +704,19 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
669
704
|
def __str__(self):
|
|
670
705
|
return self.display or super().__str__()
|
|
671
706
|
|
|
672
|
-
def
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if self.
|
|
677
|
-
|
|
678
|
-
|
|
707
|
+
def assign_cluster(self, cluster):
|
|
708
|
+
"""
|
|
709
|
+
Assign a single cluster to the device.
|
|
710
|
+
"""
|
|
711
|
+
if self.clusters.count() > 1:
|
|
712
|
+
raise self.clusters.model.MultipleObjectsReturned(
|
|
713
|
+
"Multiple Cluster objects returned. Please refer to clusters."
|
|
714
|
+
)
|
|
679
715
|
|
|
680
|
-
|
|
716
|
+
if cluster is None:
|
|
717
|
+
self.clusters.clear()
|
|
718
|
+
else:
|
|
719
|
+
self.clusters.set([cluster])
|
|
681
720
|
|
|
682
721
|
def clean(self):
|
|
683
722
|
from nautobot.ipam import models as ipam_models # circular import workaround
|
|
@@ -810,15 +849,16 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
810
849
|
)
|
|
811
850
|
|
|
812
851
|
# A Device can only be assigned to a Cluster in the same location or parent location, if any
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
and
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
852
|
+
# Validate any pending cluster assignment that was deferred during creation
|
|
853
|
+
# Don't validate `self.clusters` here as that's a M2M and is updated out-of-step with `self.save()`
|
|
854
|
+
if self.location is not None and hasattr(self, "_deferred_cluster"):
|
|
855
|
+
cluster = self._deferred_cluster
|
|
856
|
+
if cluster.location is not None and cluster.location not in self.location.ancestors(include_self=True):
|
|
857
|
+
raise ValidationError(
|
|
858
|
+
{
|
|
859
|
+
"clusters": f"Cluster {cluster} belongs to a location, {cluster.location}, that does not include {self.location}."
|
|
860
|
+
}
|
|
861
|
+
)
|
|
822
862
|
|
|
823
863
|
# Validate virtual chassis assignment
|
|
824
864
|
if self.virtual_chassis and self.vc_position is None:
|
|
@@ -861,8 +901,19 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
861
901
|
def save(self, *args, **kwargs):
|
|
862
902
|
is_new = not self.present_in_database
|
|
863
903
|
|
|
904
|
+
# to avoid circular import
|
|
905
|
+
from nautobot.dcim.custom_validators import DeviceUniquenessValidator
|
|
906
|
+
|
|
907
|
+
DeviceUniquenessValidator(self).clean()
|
|
908
|
+
|
|
864
909
|
super().save(*args, **kwargs)
|
|
865
910
|
|
|
911
|
+
# Apply any pending cluster assignment that was deferred during creation
|
|
912
|
+
if hasattr(self, "_deferred_cluster"):
|
|
913
|
+
cluster = self._deferred_cluster
|
|
914
|
+
delattr(self, "_deferred_cluster")
|
|
915
|
+
self.assign_cluster(cluster)
|
|
916
|
+
|
|
866
917
|
# If this is a new Device, instantiate all related components per the DeviceType definition
|
|
867
918
|
if is_new:
|
|
868
919
|
self.create_components()
|
|
@@ -900,7 +951,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
900
951
|
instantiated_components = []
|
|
901
952
|
for model, templates in component_models:
|
|
902
953
|
model.objects.bulk_create([x.instantiate(device=self) for x in templates])
|
|
903
|
-
cache_key =
|
|
954
|
+
cache_key = construct_cache_key(self, method_name="has_module_bays", branch_aware=True)
|
|
904
955
|
cache.delete(cache_key)
|
|
905
956
|
return instantiated_components
|
|
906
957
|
|
|
@@ -1003,7 +1054,7 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
1003
1054
|
"""
|
|
1004
1055
|
Cacheable property for determining whether this Device has any ModuleBays, and therefore may contain Modules.
|
|
1005
1056
|
"""
|
|
1006
|
-
cache_key =
|
|
1057
|
+
cache_key = construct_cache_key(self, method_name="has_module_bays", branch_aware=True)
|
|
1007
1058
|
module_bays_exists = cache.get(cache_key)
|
|
1008
1059
|
if module_bays_exists is None:
|
|
1009
1060
|
module_bays_exists = self.module_bays.exists()
|
|
@@ -1109,6 +1160,35 @@ class Device(PrimaryModel, ConfigContextModel):
|
|
|
1109
1160
|
)
|
|
1110
1161
|
|
|
1111
1162
|
|
|
1163
|
+
@extras_features("graphql")
|
|
1164
|
+
class DeviceClusterAssignment(BaseModel):
|
|
1165
|
+
device = models.ForeignKey("dcim.Device", on_delete=models.CASCADE, related_name="cluster_assignments")
|
|
1166
|
+
cluster = models.ForeignKey("virtualization.Cluster", on_delete=models.CASCADE, related_name="device_assignments")
|
|
1167
|
+
is_metadata_associable_model = False
|
|
1168
|
+
documentation_static_path = "docs/user-guide/core-data-model/dcim/device.html"
|
|
1169
|
+
|
|
1170
|
+
class Meta:
|
|
1171
|
+
unique_together = ["device", "cluster"]
|
|
1172
|
+
ordering = ["device", "cluster"]
|
|
1173
|
+
|
|
1174
|
+
def __str__(self):
|
|
1175
|
+
return f"{self.device}: {self.cluster}"
|
|
1176
|
+
|
|
1177
|
+
def clean(self):
|
|
1178
|
+
super().clean()
|
|
1179
|
+
if self.device.location is not None and self.cluster.location is not None:
|
|
1180
|
+
if self.cluster.location not in self.device.location.ancestors(include_self=True):
|
|
1181
|
+
raise ValidationError(
|
|
1182
|
+
{
|
|
1183
|
+
"__all__": f"Cluster {self.cluster} belongs to a location, {self.cluster.location}, that does not include the location of device {self.device}, {self.device.location}"
|
|
1184
|
+
}
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
def save(self, *args, **kwargs):
|
|
1188
|
+
self.clean()
|
|
1189
|
+
super().save(*args, **kwargs)
|
|
1190
|
+
|
|
1191
|
+
|
|
1112
1192
|
#
|
|
1113
1193
|
# Virtual chassis
|
|
1114
1194
|
#
|
|
@@ -1501,8 +1581,8 @@ class Controller(PrimaryModel):
|
|
|
1501
1581
|
|
|
1502
1582
|
def get_capabilities_display(self):
|
|
1503
1583
|
if not self.capabilities:
|
|
1504
|
-
return
|
|
1505
|
-
return format_html_join(" ", '<span class="
|
|
1584
|
+
return HTML_NONE
|
|
1585
|
+
return format_html_join(" ", '<span class="badge bg-secondary">{}</span>', ((v,) for v in self.capabilities))
|
|
1506
1586
|
|
|
1507
1587
|
@property
|
|
1508
1588
|
def wireless_network_assignments(self):
|
|
@@ -1598,8 +1678,8 @@ class ControllerManagedDeviceGroup(TreeModel, PrimaryModel):
|
|
|
1598
1678
|
|
|
1599
1679
|
def get_capabilities_display(self):
|
|
1600
1680
|
if not self.capabilities:
|
|
1601
|
-
return
|
|
1602
|
-
return format_html_join(" ", '<span class="
|
|
1681
|
+
return HTML_NONE
|
|
1682
|
+
return format_html_join(" ", '<span class="badge bg-secondary">{}</span>', ((v,) for v in self.capabilities))
|
|
1603
1683
|
|
|
1604
1684
|
|
|
1605
1685
|
#
|
|
@@ -2117,22 +2197,6 @@ class VirtualDeviceContext(PrimaryModel):
|
|
|
2117
2197
|
raise ValidationError(
|
|
2118
2198
|
{f"{field}": f"{ip} is not part of an interface that belongs to this VDC's device."}
|
|
2119
2199
|
)
|
|
2120
|
-
# Note: The validation for primary IPs `validate_primary_ips` is commented out due to the order in which Django processes form validation with
|
|
2121
|
-
# Many-to-Many (M2M) fields. During form saving, Django creates the instance first before assigning the M2M fields (in this case, interfaces).
|
|
2122
|
-
# As a result, the primary_ips fields could fail validation at this point because the interfaces are not yet linked to the instance,
|
|
2123
|
-
# leading to validation errors.
|
|
2124
|
-
# interfaces = self.interfaces.all()
|
|
2125
|
-
# if IPAddressToInterface.objects.filter(ip_address=ip, interface__in=interfaces).exists():
|
|
2126
|
-
# pass
|
|
2127
|
-
# elif (
|
|
2128
|
-
# ip.nat_inside is None
|
|
2129
|
-
# or not IPAddressToInterface.objects.filter(
|
|
2130
|
-
# ip_address=ip.nat_inside, interface__in=interfaces
|
|
2131
|
-
# ).exists()
|
|
2132
|
-
# ):
|
|
2133
|
-
# raise ValidationError(
|
|
2134
|
-
# {f"{field}": f"The specified IP address ({ip}) is not assigned to this Virtual Device Context."}
|
|
2135
|
-
# )
|
|
2136
2200
|
|
|
2137
2201
|
def clean(self):
|
|
2138
2202
|
super().clean()
|
nautobot/dcim/navigation.py
CHANGED
|
@@ -4,11 +4,13 @@ from nautobot.core.apps import (
|
|
|
4
4
|
NavMenuItem,
|
|
5
5
|
NavMenuTab,
|
|
6
6
|
)
|
|
7
|
+
from nautobot.core.ui.choices import NavigationIconChoices, NavigationWeightChoices
|
|
7
8
|
|
|
8
9
|
menu_items = (
|
|
9
10
|
NavMenuTab(
|
|
10
11
|
name="Organization",
|
|
11
|
-
|
|
12
|
+
icon=NavigationIconChoices.ORGANIZATION,
|
|
13
|
+
weight=NavigationWeightChoices.ORGANIZATION,
|
|
12
14
|
groups=(
|
|
13
15
|
NavMenuGroup(
|
|
14
16
|
name="Locations",
|
|
@@ -115,7 +117,8 @@ menu_items = (
|
|
|
115
117
|
),
|
|
116
118
|
NavMenuTab(
|
|
117
119
|
name="Devices",
|
|
118
|
-
|
|
120
|
+
icon=NavigationIconChoices.DEVICES,
|
|
121
|
+
weight=NavigationWeightChoices.DEVICES,
|
|
119
122
|
groups=(
|
|
120
123
|
NavMenuGroup(
|
|
121
124
|
name="Devices",
|
|
@@ -547,7 +550,8 @@ menu_items = (
|
|
|
547
550
|
),
|
|
548
551
|
NavMenuTab(
|
|
549
552
|
name="Power",
|
|
550
|
-
|
|
553
|
+
icon=NavigationIconChoices.POWER,
|
|
554
|
+
weight=NavigationWeightChoices.POWER,
|
|
551
555
|
groups=(
|
|
552
556
|
NavMenuGroup(
|
|
553
557
|
name="Power",
|
nautobot/dcim/signals.py
CHANGED
|
@@ -381,3 +381,22 @@ def content_type_changed(instance, action, **kwargs):
|
|
|
381
381
|
)
|
|
382
382
|
}
|
|
383
383
|
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
#
|
|
387
|
+
# Device/Cluster assignments
|
|
388
|
+
#
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@receiver(m2m_changed, sender=Device.clusters.through)
|
|
392
|
+
def ensure_device_and_cluster_locations_are_compatible(sender, instance, action, pk_set, **kwargs):
|
|
393
|
+
"""
|
|
394
|
+
When adding clusters to a device, clean the added DeviceClusterAssignment records to enforce location compatibility.
|
|
395
|
+
"""
|
|
396
|
+
if action == "post_add" and pk_set:
|
|
397
|
+
if isinstance(instance, Device):
|
|
398
|
+
for assignment in instance.cluster_assignments.filter(cluster_id__in=pk_set):
|
|
399
|
+
assignment.clean()
|
|
400
|
+
else: # instance is a Cluster
|
|
401
|
+
for assignment in instance.device_assignments.filter(device_id__in=pk_set):
|
|
402
|
+
assignment.clean()
|