nautobot 3.0.0a2__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/apps/choices.py +4 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +13 -1
- nautobot/apps/utils.py +8 -0
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- nautobot/circuits/tests/integration/test_circuits_bulk_operations.py +0 -3
- nautobot/circuits/views.py +6 -2
- nautobot/cloud/filters.py +1 -1
- nautobot/cloud/navigation.py +3 -2
- nautobot/core/api/schema.py +1 -1
- nautobot/core/api/serializers.py +6 -1
- nautobot/core/api/urls.py +2 -0
- nautobot/core/api/views.py +12 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/choices.py +1 -1
- nautobot/core/cli/bootstrap_v3_to_v5.py +105 -13
- nautobot/core/cli/migrate_deprecated_templates.py +227 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/filters.py +4 -0
- nautobot/core/forms/__init__.py +2 -0
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/forms/widgets.py +21 -2
- nautobot/core/jobs/__init__.py +62 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/__init__.py +11 -0
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/models/utils.py +1 -1
- nautobot/core/settings.py +35 -19
- nautobot/core/settings.yaml +17 -33
- nautobot/core/signals.py +12 -1
- nautobot/core/tables.py +13 -6
- nautobot/core/templates/40x.html +1 -1
- nautobot/core/templates/500.html +2 -2
- nautobot/core/templates/admin/base.html +1 -2
- nautobot/core/templates/admin/change_list.html +9 -12
- nautobot/core/templates/admin/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- nautobot/core/templates/base_django.html +1 -2
- nautobot/core/templates/buttons/export.html +1 -1
- nautobot/core/templates/components/button/dropdown.html +5 -3
- nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
- nautobot/core/templates/components/panel/header_extra_content_table.html +1 -1
- nautobot/core/templates/components/panel/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +6 -7
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +22 -9
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +6 -5
- nautobot/core/templates/generic/object_bulk_delete.html +1 -1
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- nautobot/core/templates/generic/object_bulk_edit.html +1 -1
- nautobot/core/templates/generic/object_bulk_import.html +1 -1
- nautobot/core/templates/generic/object_bulk_remove.html +2 -2
- nautobot/core/templates/generic/object_bulk_update.html +5 -4
- nautobot/core/templates/generic/object_create.html +5 -4
- nautobot/core/templates/generic/object_delete.html +1 -1
- nautobot/core/templates/generic/object_detail.html +1 -1
- nautobot/core/templates/generic/object_edit.html +1 -1
- nautobot/core/templates/generic/object_import.html +2 -1
- nautobot/core/templates/generic/object_list.html +12 -4
- nautobot/core/templates/generic/object_notes.html +5 -3
- nautobot/core/templates/generic/object_retrieve.html +4 -5
- nautobot/core/templates/graphene/graphiql.html +7 -8
- nautobot/core/templates/home.html +1 -1
- nautobot/core/templates/import_success.html +2 -1
- nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/created_updated.html +7 -3
- nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
- nautobot/core/templates/inc/footer.html +3 -1
- nautobot/core/templates/inc/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +11 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/media.html +14 -0
- nautobot/core/templates/inc/nav_menu.html +3 -9
- nautobot/core/templates/inc/object_details_advanced_panel.html +2 -2
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -11
- nautobot/core/templates/redoc_ui.html +7 -0
- nautobot/core/templates/rest_framework/api.html +103 -2
- nautobot/core/templates/search.html +1 -1
- nautobot/core/templates/swagger_ui.html +17 -3
- nautobot/core/templates/system_jobs/import_objects.html +1 -2
- nautobot/core/templates/utilities/confirmation_form.html +2 -2
- nautobot/core/templates/utilities/obj_table.html +10 -2
- nautobot/core/templates/utilities/render_field.html +7 -7
- nautobot/core/templates/utilities/render_jinja2.html +2 -2
- nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +37 -4
- nautobot/core/templates/utilities/theme_preview.html +19 -3
- nautobot/core/templates/widgets/number_input_with_choices.html +44 -0
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +76 -18
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +41 -17
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/utils.py +18 -4
- nautobot/core/testing/views.py +104 -13
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- nautobot/core/tests/integration/test_filters.py +48 -11
- nautobot/core/tests/integration/test_theme.py +22 -21
- nautobot/core/tests/nautobot_config.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +8 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +27 -28
- nautobot/core/tests/test_checks.py +28 -0
- nautobot/core/tests/test_cli.py +40 -0
- nautobot/core/tests/test_config.py +2 -1
- nautobot/core/tests/test_forms.py +55 -13
- nautobot/core/tests/test_jobs.py +144 -3
- nautobot/core/tests/test_nautobot_server.py +2 -0
- nautobot/core/tests/test_navigations.py +76 -1
- nautobot/core/tests/test_patch_social_django.py +42 -0
- nautobot/core/tests/test_renderers.py +59 -0
- nautobot/core/tests/test_settings_schema.py +1 -0
- nautobot/core/tests/test_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +62 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_titles.py +0 -16
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +123 -4
- nautobot/core/tests/test_utils.py +72 -5
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +70 -29
- nautobot/core/ui/bulk_buttons.py +1 -1
- nautobot/core/ui/choices.py +143 -27
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/echarts.py +15 -20
- nautobot/core/ui/object_detail.py +143 -55
- nautobot/core/ui/titles.py +3 -6
- nautobot/core/urls.py +20 -9
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +28 -18
- nautobot/core/utils/lookup.py +49 -8
- nautobot/core/utils/module_loading.py +21 -0
- nautobot/core/utils/patch_social_django.py +128 -0
- nautobot/core/views/__init__.py +38 -1
- nautobot/core/views/generic.py +3 -3
- nautobot/core/views/mixins.py +45 -22
- nautobot/core/views/renderers.py +4 -3
- nautobot/core/views/viewsets.py +2 -1
- nautobot/data_validation/apps.py +1 -5
- nautobot/data_validation/custom_validators.py +4 -4
- nautobot/data_validation/filters.py +1 -1
- nautobot/data_validation/forms.py +40 -0
- nautobot/data_validation/migrations/0001_initial.py +0 -7
- nautobot/data_validation/migrations/0002_data_migration_from_app.py +3 -14
- nautobot/data_validation/models.py +16 -7
- nautobot/data_validation/navigation.py +8 -1
- nautobot/data_validation/tables.py +12 -5
- nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
- nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
- nautobot/data_validation/tests/__init__.py +2 -2
- nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
- nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
- nautobot/data_validation/tests/test_filters.py +8 -6
- nautobot/data_validation/tests/test_models.py +15 -0
- nautobot/data_validation/tests/test_views.py +190 -32
- nautobot/data_validation/urls.py +2 -5
- nautobot/data_validation/views.py +73 -40
- nautobot/dcim/api/serializers.py +3 -13
- nautobot/dcim/apps.py +4 -0
- nautobot/dcim/choices.py +65 -0
- nautobot/dcim/constants.py +7 -0
- nautobot/dcim/custom_validators.py +84 -0
- nautobot/dcim/factory.py +1 -1
- nautobot/dcim/filter_mixins.py +353 -4
- nautobot/dcim/{filters/__init__.py → filters.py} +15 -36
- nautobot/dcim/forms.py +90 -4
- nautobot/dcim/migrations/0075_interface_duplex_interface_speed_and_more.py +32 -0
- nautobot/dcim/migrations/{0075_add_deviceclusterassignment.py → 0076_add_deviceclusterassignment.py} +1 -1
- nautobot/dcim/migrations/{0076_device_cluster_to_clusters_data_migration.py → 0077_device_cluster_to_clusters_data_migration.py} +1 -1
- nautobot/dcim/migrations/{0077_remove_device_cluster.py → 0078_remove_device_cluster.py} +1 -1
- nautobot/dcim/migrations/0079_remove_device_location_tenant_name_uniqueness.py +16 -0
- nautobot/dcim/migrations/0080_device_name_data_migration.py +59 -0
- nautobot/dcim/migrations/0081_alter_device_device_redundancy_group_priority_and_more.py +25 -0
- nautobot/dcim/models/device_component_templates.py +33 -1
- nautobot/dcim/models/device_components.py +98 -64
- nautobot/dcim/models/devices.py +30 -20
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +18 -0
- nautobot/dcim/tables/devicetypes.py +8 -1
- nautobot/dcim/tables/racks.py +0 -2
- nautobot/dcim/tables/template_code.py +15 -15
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_trace.html +0 -4
- nautobot/dcim/templates/dcim/{cable_edit.html → cable_update.html} +1 -1
- nautobot/dcim/templates/dcim/consoleport.html +7 -6
- nautobot/dcim/templates/dcim/consoleserverport.html +7 -6
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- nautobot/dcim/templates/dcim/device.html +1 -1
- nautobot/dcim/templates/dcim/device_component_add.html +2 -2
- nautobot/dcim/templates/dcim/device_create.html +5 -3
- nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
- nautobot/dcim/templates/dcim/device_list.html +73 -10
- nautobot/dcim/templates/dcim/devicebay.html +1 -1
- nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
- nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
- nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
- nautobot/dcim/templates/dcim/frontport.html +10 -9
- nautobot/dcim/templates/dcim/inc/devicetype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/inc/moduletype_component_table.html +1 -1
- nautobot/dcim/templates/dcim/inc/rack_elevation.html +1 -1
- nautobot/dcim/templates/dcim/interface.html +35 -7
- nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/interface_edit.html +2 -0
- nautobot/dcim/templates/dcim/inventoryitem.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
- nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
- nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
- nautobot/dcim/templates/dcim/module/base.html +49 -9
- nautobot/dcim/templates/dcim/module_consoleports.html +1 -1
- nautobot/dcim/templates/dcim/module_consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/module_frontports.html +1 -1
- nautobot/dcim/templates/dcim/module_interfaces.html +1 -1
- nautobot/dcim/templates/dcim/module_list.html +57 -8
- nautobot/dcim/templates/dcim/module_modulebays.html +1 -1
- nautobot/dcim/templates/dcim/module_poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/module_powerports.html +1 -1
- nautobot/dcim/templates/dcim/module_rearports.html +1 -1
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_list.html +2 -2
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/poweroutlet.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +6 -5
- nautobot/dcim/templates/dcim/rack_elevation_list.html +17 -5
- nautobot/dcim/templates/dcim/rack_retrieve.html +22 -15
- nautobot/dcim/templates/dcim/rearport.html +8 -7
- nautobot/dcim/templates/dcim/trace/cable.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +15 -7
- nautobot/dcim/tests/integration/test_controller.py +4 -6
- nautobot/dcim/tests/integration/test_controller_managed_device_group.py +1 -5
- nautobot/dcim/tests/integration/test_create_device.py +0 -2
- nautobot/dcim/tests/integration/test_device_bulk_operations.py +1 -3
- nautobot/dcim/tests/integration/test_fileinputpicker.py +6 -10
- nautobot/dcim/tests/integration/test_location_bulk_operations.py +0 -2
- nautobot/dcim/tests/integration/test_module_bay_position.py +3 -4
- nautobot/dcim/tests/test_api.py +194 -6
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +55 -7
- nautobot/dcim/tests/test_forms.py +110 -8
- nautobot/dcim/tests/test_graphql.py +44 -1
- nautobot/dcim/tests/test_models.py +328 -4
- nautobot/dcim/tests/test_tables.py +160 -0
- nautobot/dcim/tests/test_views.py +132 -29
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +777 -397
- nautobot/extras/api/views.py +60 -45
- nautobot/extras/choices.py +2 -13
- nautobot/extras/datasources/git.py +3 -1
- nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
- nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
- nautobot/extras/{filters/__init__.py → filters.py} +33 -48
- nautobot/extras/forms/forms.py +14 -15
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/jobs.py +2 -0
- nautobot/extras/jobs_ui.py +4 -3
- nautobot/extras/management/__init__.py +11 -0
- nautobot/extras/management/commands/refresh_dynamic_group_member_caches.py +4 -1
- nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
- nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
- nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
- nautobot/extras/migrations/0131_configcontext_device_families.py +18 -0
- nautobot/extras/models/__init__.py +1 -2
- nautobot/extras/models/approvals.py +33 -14
- nautobot/extras/models/change_logging.py +4 -0
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +60 -4
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +23 -2
- nautobot/extras/models/secrets.py +1 -0
- nautobot/extras/models/statuses.py +0 -15
- nautobot/extras/navigation.py +13 -9
- nautobot/extras/plugins/__init__.py +33 -55
- nautobot/extras/plugins/marketplace_manifest.yml +49 -1
- nautobot/extras/plugins/tables.py +3 -3
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/views.py +0 -9
- nautobot/extras/querysets.py +8 -0
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +64 -68
- nautobot/extras/templates/django_ajax_tables/ajax_wrapper.html +2 -0
- nautobot/extras/templates/extras/approval_dashboard.html +7 -5
- nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
- nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
- nautobot/extras/templates/extras/configcontext_update.html +1 -0
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
- nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
- nautobot/extras/templates/extras/gitrepository_result.html +0 -2
- nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
- nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
- nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
- nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
- nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
- nautobot/extras/templates/extras/inc/json_format.html +10 -3
- nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
- nautobot/extras/templates/extras/job.html +29 -12
- nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
- nautobot/extras/templates/extras/job_edit.html +52 -46
- nautobot/extras/templates/extras/job_list.html +29 -25
- nautobot/extras/templates/extras/marketplace.html +5 -9
- nautobot/extras/templates/extras/object_configcontext.html +1 -1
- nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
- nautobot/extras/templates/extras/objectchange_retrieve.html +19 -39
- nautobot/extras/templates/extras/plugin_detail.html +29 -24
- nautobot/extras/templates/extras/plugins_list.html +16 -26
- nautobot/extras/templates/extras/role_retrieve.html +64 -0
- nautobot/extras/templates/extras/scheduledjob.html +4 -2
- nautobot/extras/templates/extras/secret_create.html +1 -1
- nautobot/extras/templatetags/custom_links.py +12 -12
- nautobot/extras/templatetags/job_buttons.py +14 -12
- nautobot/extras/test_jobs/invalid_import.py +9 -0
- nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
- nautobot/extras/test_jobs/missing_import.py +11 -0
- nautobot/extras/tests/integration/test_computedfields.py +8 -9
- nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +9 -10
- nautobot/extras/tests/integration/test_dynamicgroups.py +12 -9
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/integration/test_relationships.py +0 -2
- nautobot/extras/tests/test_api.py +90 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +59 -5
- nautobot/extras/tests/test_customfields.py +22 -13
- nautobot/extras/tests/test_customfields_filters.py +479 -0
- nautobot/extras/tests/test_dynamicgroups.py +39 -1
- nautobot/extras/tests/test_filters.py +57 -22
- nautobot/extras/tests/test_forms.py +18 -21
- nautobot/extras/tests/test_jobs.py +25 -4
- nautobot/extras/tests/test_migrations.py +1 -0
- nautobot/extras/tests/test_models.py +51 -33
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_utils.py +3 -4
- nautobot/extras/tests/test_views.py +52 -112
- nautobot/extras/urls.py +0 -14
- nautobot/extras/views.py +164 -71
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +53 -38
- nautobot/ipam/formfields.py +1 -1
- nautobot/ipam/forms.py +6 -3
- nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
- nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
- nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/navigation.py +3 -2
- nautobot/ipam/signals.py +71 -0
- nautobot/ipam/tables.py +19 -6
- nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
- nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
- nautobot/ipam/templates/ipam/ipaddress.html +14 -0
- nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
- nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
- nautobot/ipam/templates/ipam/namespace_ip_addresses.html +1 -1
- nautobot/ipam/templates/ipam/namespace_prefixes.html +1 -1
- nautobot/ipam/templates/ipam/namespace_update.html +15 -0
- nautobot/ipam/templates/ipam/namespace_vrfs.html +1 -1
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
- nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
- nautobot/ipam/tests/migration/test_migrations.py +89 -0
- nautobot/ipam/tests/test_api.py +13 -6
- nautobot/ipam/tests/test_filters.py +36 -1
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +44 -2
- nautobot/ipam/tests/test_tables.py +1 -2
- nautobot/ipam/tests/test_utils.py +1 -1
- nautobot/ipam/tests/test_views.py +13 -14
- nautobot/ipam/ui.py +0 -17
- nautobot/ipam/utils/migrations.py +16 -2
- nautobot/ipam/utils/testing.py +9 -3
- nautobot/ipam/views.py +53 -11
- nautobot/load_balancers/__init__.py +0 -0
- nautobot/load_balancers/api/__init__.py +1 -0
- nautobot/load_balancers/api/serializers.py +75 -0
- nautobot/load_balancers/api/urls.py +23 -0
- nautobot/load_balancers/api/views.py +61 -0
- nautobot/load_balancers/apps.py +17 -0
- nautobot/load_balancers/choices.py +167 -0
- nautobot/load_balancers/filters.py +225 -0
- nautobot/load_balancers/forms.py +532 -0
- nautobot/load_balancers/management/commands/__init__.py +0 -0
- nautobot/load_balancers/management/commands/generate_load_balancer_models_test_data.py +38 -0
- nautobot/load_balancers/migrations/0001_initial.py +465 -0
- nautobot/load_balancers/migrations/0002_create_default_statuses_pool_members.py +31 -0
- nautobot/load_balancers/migrations/__init__.py +0 -0
- nautobot/load_balancers/models.py +423 -0
- nautobot/load_balancers/navigation.py +80 -0
- nautobot/load_balancers/tables.py +255 -0
- nautobot/load_balancers/tests/__init__.py +474 -0
- nautobot/load_balancers/tests/test_api.py +353 -0
- nautobot/load_balancers/tests/test_filters.py +134 -0
- nautobot/load_balancers/tests/test_forms.py +266 -0
- nautobot/load_balancers/tests/test_models.py +195 -0
- nautobot/load_balancers/tests/test_views.py +229 -0
- nautobot/load_balancers/urls.py +17 -0
- nautobot/load_balancers/views.py +248 -0
- nautobot/project-static/dist/css/github-dark.min.css +10 -0
- nautobot/project-static/dist/css/github.min.css +10 -0
- nautobot/project-static/dist/css/nautobot.css +1 -11
- nautobot/project-static/dist/css/nautobot.css.map +1 -1
- nautobot/project-static/dist/js/libraries.js +1 -1
- nautobot/project-static/dist/js/libraries.js.map +1 -1
- nautobot/project-static/dist/js/nautobot.js +1 -1
- nautobot/project-static/dist/js/nautobot.js.map +1 -1
- nautobot/project-static/js/cabletrace.js +1 -1
- nautobot/project-static/js/forms.js +13 -0
- nautobot/project-static/js/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-globe.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-shield-check.svg +3 -0
- nautobot/project-static/nautobot-icons/bus-shield.svg +3 -0
- nautobot/project-static/nautobot-icons/cloud.svg +1 -1
- nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
- nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
- nautobot/project-static/nautobot-icons/elements.svg +1 -1
- nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
- nautobot/project-static/nautobot-icons/hammer.svg +1 -1
- nautobot/project-static/nautobot-icons/organization.svg +3 -0
- nautobot/project-static/nautobot-icons/secrets.svg +1 -1
- nautobot/project-static/nautobot-icons/security.svg +3 -0
- nautobot/project-static/nautobot-icons/server.svg +1 -1
- nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
- nautobot/project-static/nautobot-icons/star.svg +1 -1
- nautobot/tenancy/api/serializers.py +1 -0
- nautobot/tenancy/api/views.py +2 -1
- nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
- nautobot/tenancy/navigation.py +3 -1
- nautobot/tenancy/tests/test_filters.py +0 -2
- nautobot/tenancy/views.py +2 -1
- nautobot/ui/package-lock.json +87 -4
- nautobot/ui/package.json +2 -1
- nautobot/ui/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -1
- nautobot/ui/src/js/select2.js +53 -2
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +112 -30
- nautobot/ui/webpack.config.js +13 -0
- nautobot/users/templates/users/preferences.html +11 -2
- nautobot/users/templates/users/profile.html +45 -12
- nautobot/users/templates/users/sessionkey_delete.html +1 -1
- nautobot/users/tests/test_api.py +4 -0
- nautobot/users/views.py +4 -2
- nautobot/virtualization/filters.py +6 -1
- nautobot/virtualization/models.py +1 -68
- nautobot/virtualization/navigation.py +3 -2
- nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
- nautobot/virtualization/tests/test_api.py +3 -0
- nautobot/virtualization/tests/test_filters.py +10 -1
- nautobot/virtualization/tests/test_models.py +45 -4
- nautobot/virtualization/views.py +4 -1
- nautobot/vpn/__init__.py +0 -0
- nautobot/vpn/api/serializers.py +113 -0
- nautobot/vpn/api/urls.py +19 -0
- nautobot/vpn/api/views.py +70 -0
- nautobot/vpn/apps.py +8 -0
- nautobot/vpn/choices.py +171 -0
- nautobot/vpn/factory.py +219 -0
- nautobot/vpn/filters.py +234 -0
- nautobot/vpn/forms.py +487 -0
- nautobot/vpn/homepage.py +19 -0
- nautobot/vpn/migrations/0001_initial.py +541 -0
- nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
- nautobot/vpn/migrations/__init__.py +0 -0
- nautobot/vpn/models.py +535 -0
- nautobot/vpn/navigation.py +98 -0
- nautobot/vpn/tables.py +383 -0
- nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
- nautobot/vpn/tests/__init__.py +0 -0
- nautobot/vpn/tests/test_api.py +336 -0
- nautobot/vpn/tests/test_filters.py +139 -0
- nautobot/vpn/tests/test_forms.py +293 -0
- nautobot/vpn/tests/test_models.py +147 -0
- nautobot/vpn/tests/test_views.py +300 -0
- nautobot/vpn/urls.py +16 -0
- nautobot/vpn/views.py +495 -0
- nautobot/wireless/navigation.py +3 -2
- nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
- nautobot/wireless/tests/test_api.py +1 -1
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/METADATA +15 -15
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/RECORD +514 -572
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/entry_points.txt +1 -0
- nautobot/circuits/templates/circuits/circuit.html +0 -2
- nautobot/circuits/templates/circuits/circuit_edit.html +0 -2
- nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/circuit_update.html +0 -1
- nautobot/circuits/templates/circuits/circuittermination.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_edit.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/circuittermination_update.html +0 -1
- nautobot/circuits/templates/circuits/circuittype.html +0 -2
- nautobot/circuits/templates/circuits/circuittype_retrieve.html +0 -2
- nautobot/circuits/templates/circuits/inc/circuit_termination.html +0 -85
- nautobot/circuits/templates/circuits/provider.html +0 -2
- nautobot/circuits/templates/circuits/provider_edit.html +0 -2
- nautobot/circuits/templates/circuits/provider_retrieve.html +0 -1
- nautobot/circuits/templates/circuits/provider_update.html +0 -1
- nautobot/circuits/templates/circuits/providernetwork.html +0 -2
- nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudaccount_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudnetwork_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +0 -2
- nautobot/cloud/templates/cloud/cloudservice_retrieve.html +0 -2
- nautobot/core/templates/buttons/import.html +0 -9
- nautobot/data_validation/template_content.py +0 -42
- nautobot/data_validation/templates/data_validation/datacompliance_retrieve.html +0 -1
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/dcim/templates/dcim/controller/base.html +0 -2
- nautobot/dcim/templates/dcim/controller_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/controller_wirelessnetworks.html +0 -2
- nautobot/dcim/templates/dcim/controllermanageddevicegroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/device/base.html +0 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +0 -2
- nautobot/dcim/templates/dcim/device/consoleserverports.html +0 -2
- nautobot/dcim/templates/dcim/device/devicebays.html +0 -2
- nautobot/dcim/templates/dcim/device/frontports.html +0 -2
- nautobot/dcim/templates/dcim/device/interfaces.html +0 -2
- nautobot/dcim/templates/dcim/device/inventory.html +0 -2
- nautobot/dcim/templates/dcim/device/modulebays.html +0 -2
- nautobot/dcim/templates/dcim/device/poweroutlets.html +0 -2
- nautobot/dcim/templates/dcim/device/powerports.html +0 -2
- nautobot/dcim/templates/dcim/device/rearports.html +0 -2
- nautobot/dcim/templates/dcim/device/wireless.html +0 -2
- nautobot/dcim/templates/dcim/device_component.html +0 -2
- nautobot/dcim/templates/dcim/device_edit.html +0 -2
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/deviceredundancygroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/devicetype.html +0 -2
- nautobot/dcim/templates/dcim/devicetype_edit.html +0 -2
- nautobot/dcim/templates/dcim/devicetype_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/inc/device_napalm_tabs.html +0 -1
- nautobot/dcim/templates/dcim/interfaceredundancygroup_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/location.html +0 -2
- nautobot/dcim/templates/dcim/location_edit.html +0 -2
- nautobot/dcim/templates/dcim/location_retrieve.html +0 -243
- nautobot/dcim/templates/dcim/locationtype.html +0 -2
- nautobot/dcim/templates/dcim/locationtype_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/manufacturer.html +0 -2
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -1
- nautobot/dcim/templates/dcim/platform.html +0 -2
- nautobot/dcim/templates/dcim/powerfeed.html +0 -2
- nautobot/dcim/templates/dcim/powerfeed_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel_edit.html +0 -2
- nautobot/dcim/templates/dcim/powerpanel_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/rack.html +0 -2
- nautobot/dcim/templates/dcim/rack_edit.html +0 -2
- nautobot/dcim/templates/dcim/rackgroup.html +0 -2
- nautobot/dcim/templates/dcim/rackreservation.html +0 -2
- nautobot/dcim/templates/dcim/softwareimagefile_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/softwareversion_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_add.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +0 -2
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +0 -2
- nautobot/dcim/templates/dcim/virtualdevicecontext_retrieve.html +0 -2
- nautobot/dcim/ui.py +0 -29
- nautobot/extras/templates/extras/computedfield.html +0 -2
- nautobot/extras/templates/extras/computedfield_retrieve.html +0 -2
- nautobot/extras/templates/extras/configcontext.html +0 -2
- nautobot/extras/templates/extras/configcontext_edit.html +0 -2
- nautobot/extras/templates/extras/configcontext_retrieve.html +0 -2
- nautobot/extras/templates/extras/configcontextschema.html +0 -2
- nautobot/extras/templates/extras/configcontextschema_edit.html +0 -2
- nautobot/extras/templates/extras/contact_retrieve.html +0 -2
- nautobot/extras/templates/extras/customfield.html +0 -2
- nautobot/extras/templates/extras/customfield_edit.html +0 -2
- nautobot/extras/templates/extras/customfield_retrieve.html +0 -2
- nautobot/extras/templates/extras/customlink.html +0 -2
- nautobot/extras/templates/extras/dynamicgroup.html +0 -2
- nautobot/extras/templates/extras/dynamicgroup_edit.html +0 -2
- nautobot/extras/templates/extras/exporttemplate.html +0 -2
- nautobot/extras/templates/extras/gitrepository.html +0 -2
- nautobot/extras/templates/extras/gitrepository_object_edit.html +0 -2
- nautobot/extras/templates/extras/graphqlquery.html +0 -2
- nautobot/extras/templates/extras/graphqlquery_list.html +0 -1
- nautobot/extras/templates/extras/graphqlquery_retrieve.html +0 -97
- nautobot/extras/templates/extras/job_detail.html +0 -2
- nautobot/extras/templates/extras/jobbutton_retrieve.html +0 -2
- nautobot/extras/templates/extras/jobhook.html +0 -2
- nautobot/extras/templates/extras/jobqueue_retrieve.html +0 -2
- nautobot/extras/templates/extras/jobresult.html +0 -2
- nautobot/extras/templates/extras/metadatatype_retrieve.html +0 -2
- nautobot/extras/templates/extras/note.html +0 -2
- nautobot/extras/templates/extras/note_retrieve.html +0 -1
- nautobot/extras/templates/extras/object_changelog.html +0 -2
- nautobot/extras/templates/extras/object_notes.html +0 -2
- nautobot/extras/templates/extras/objectchange.html +0 -2
- nautobot/extras/templates/extras/objectchange_list.html +0 -3
- nautobot/extras/templates/extras/relationship.html +0 -1
- nautobot/extras/templates/extras/secret.html +0 -1
- nautobot/extras/templates/extras/secret_edit.html +0 -1
- nautobot/extras/templates/extras/secretsgroup.html +0 -2
- nautobot/extras/templates/extras/secretsgroup_edit.html +0 -2
- nautobot/extras/templates/extras/secretsgroup_retrieve.html +0 -2
- nautobot/extras/templates/extras/status.html +0 -2
- nautobot/extras/templates/extras/tag.html +0 -2
- nautobot/extras/templates/extras/tag_edit.html +0 -2
- nautobot/extras/templates/extras/tag_retrieve.html +0 -2
- nautobot/extras/templates/extras/team_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- nautobot/ipam/templates/ipam/namespace_retrieve.html +0 -1
- nautobot/ipam/templates/ipam/prefix.html +0 -2
- nautobot/ipam/templates/ipam/prefix_edit.html +0 -1
- nautobot/ipam/templates/ipam/prefix_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/rir.html +0 -2
- nautobot/ipam/templates/ipam/routetarget.html +0 -1
- nautobot/ipam/templates/ipam/service.html +0 -2
- nautobot/ipam/templates/ipam/service_edit.html +0 -2
- nautobot/ipam/templates/ipam/service_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/vlan.html +0 -2
- nautobot/ipam/templates/ipam/vlan_edit.html +0 -2
- nautobot/ipam/templates/ipam/vlan_retrieve.html +0 -2
- nautobot/ipam/templates/ipam/vlangroup.html +0 -2
- nautobot/ipam/templates/ipam/vrf.html +0 -1
- nautobot/tenancy/templates/tenancy/tenant.html +0 -2
- nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -2
- nautobot/tenancy/templates/tenancy/tenantgroup.html +0 -2
- nautobot/tenancy/templates/tenancy/tenantgroup_retrieve.html +0 -1
- nautobot/virtualization/templates/virtualization/clustergroup.html +0 -2
- nautobot/virtualization/templates/virtualization/clustertype.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +0 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/radioprofile_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/supporteddatarate_retrieve.html +0 -2
- nautobot/wireless/templates/wireless/wirelessnetwork_retrieve.html +0 -2
- /nautobot/dcim/templates/dcim/{cable.html → cable_retrieve.html} +0 -0
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0rc1.dist-info}/WHEEL +0 -0
nautobot/ipam/forms.py
CHANGED
|
@@ -72,10 +72,10 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice(
|
|
|
72
72
|
#
|
|
73
73
|
|
|
74
74
|
|
|
75
|
-
class NamespaceForm(LocatableModelFormMixin, NautobotModelForm):
|
|
75
|
+
class NamespaceForm(LocatableModelFormMixin, NautobotModelForm, TenancyForm):
|
|
76
76
|
class Meta:
|
|
77
77
|
model = Namespace
|
|
78
|
-
fields = ["name", "description", "location", "tags"]
|
|
78
|
+
fields = ["name", "description", "tenant", "location", "tags"]
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
class NamespaceBulkEditForm(
|
|
@@ -84,18 +84,21 @@ class NamespaceBulkEditForm(
|
|
|
84
84
|
NautobotBulkEditForm,
|
|
85
85
|
):
|
|
86
86
|
pk = forms.ModelMultipleChoiceField(queryset=Namespace.objects.all(), widget=forms.MultipleHiddenInput())
|
|
87
|
+
tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
|
87
88
|
description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
|
|
88
89
|
|
|
89
90
|
class Meta:
|
|
90
91
|
model = Namespace
|
|
91
92
|
nullable_fields = [
|
|
92
93
|
"description",
|
|
94
|
+
"tenant",
|
|
93
95
|
"location",
|
|
94
96
|
]
|
|
95
97
|
|
|
96
98
|
|
|
97
|
-
class NamespaceFilterForm(LocatableModelFilterFormMixin, NautobotFilterForm):
|
|
99
|
+
class NamespaceFilterForm(LocatableModelFilterFormMixin, NautobotFilterForm, TenancyFilterForm):
|
|
98
100
|
model = Namespace
|
|
101
|
+
field_order = ["q", "name", "tenant_group", "tenant"]
|
|
99
102
|
q = forms.CharField(required=False, label="Search")
|
|
100
103
|
name = forms.CharField(required=False)
|
|
101
104
|
|
|
@@ -10,6 +10,17 @@ import nautobot.extras.models.mixins
|
|
|
10
10
|
import nautobot.ipam.models
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def create_default_namespace(apps, schema):
|
|
14
|
+
Namespace = apps.get_model("ipam", "Namespace")
|
|
15
|
+
|
|
16
|
+
namespace, _ = Namespace.objects.get_or_create(
|
|
17
|
+
name="Global", defaults={"description": "Default Global namespace. Created by Nautobot."}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Populate the contextvars cache so that subsequent calls to get_default_namespace_pk() do not error out
|
|
21
|
+
nautobot.ipam.models.default_namespace_pk.set(namespace.pk)
|
|
22
|
+
|
|
23
|
+
|
|
13
24
|
class Migration(migrations.Migration):
|
|
14
25
|
dependencies = [
|
|
15
26
|
("extras", "0072_rename_model_fields"),
|
|
@@ -130,6 +141,8 @@ class Migration(migrations.Migration):
|
|
|
130
141
|
name="ip_version",
|
|
131
142
|
field=models.IntegerField(db_index=True, editable=False, null=True),
|
|
132
143
|
),
|
|
144
|
+
# We shouldn't mix data migrations with schema migrations, but we didn't catch this data dependency until much later
|
|
145
|
+
migrations.RunPython(create_default_namespace, migrations.RunPython.noop),
|
|
133
146
|
migrations.AddField(
|
|
134
147
|
model_name="prefix",
|
|
135
148
|
name="namespace",
|
|
@@ -17,7 +17,10 @@ def reverse_it(apps, schema_editor):
|
|
|
17
17
|
Interface = apps.get_model("dcim", "Interface")
|
|
18
18
|
VMInterface = apps.get_model("virtualization", "VMInterface")
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# This may be overly defensive, but it doesn't hurt to be safe.
|
|
21
|
+
ns_global, _ = Namespace.objects.get_or_create(
|
|
22
|
+
name="Global", defaults={"description": "Default Global namespace. Created by Nautobot."}
|
|
23
|
+
)
|
|
21
24
|
Prefix.objects.update(namespace=ns_global)
|
|
22
25
|
VRF.objects.update(namespace=ns_global)
|
|
23
26
|
Namespace.objects.exclude(name=ns_global.name).delete()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated by Django 4.2.23 on 2025-08-30 13:10
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
dependencies = [
|
|
9
|
+
("tenancy", "0009_update_all_charfields_max_length_to_255"),
|
|
10
|
+
("ipam", "0053_alter_vrfdeviceassignment_options_and_more"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name="namespace",
|
|
16
|
+
name="tenant",
|
|
17
|
+
field=models.ForeignKey(
|
|
18
|
+
blank=True,
|
|
19
|
+
null=True,
|
|
20
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
21
|
+
related_name="namespaces",
|
|
22
|
+
to="tenancy.tenant",
|
|
23
|
+
),
|
|
24
|
+
),
|
|
25
|
+
]
|
nautobot/ipam/models.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import contextvars
|
|
1
2
|
import logging
|
|
2
3
|
import operator
|
|
3
4
|
from typing import Optional
|
|
@@ -48,6 +49,9 @@ __all__ = (
|
|
|
48
49
|
logger = logging.getLogger(__name__)
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
default_namespace_pk = contextvars.ContextVar("default_namespace_pk", default=None)
|
|
53
|
+
|
|
54
|
+
|
|
51
55
|
@extras_features(
|
|
52
56
|
"custom_links",
|
|
53
57
|
"custom_validators",
|
|
@@ -68,6 +72,13 @@ class Namespace(PrimaryModel):
|
|
|
68
72
|
blank=True,
|
|
69
73
|
null=True,
|
|
70
74
|
)
|
|
75
|
+
tenant = models.ForeignKey(
|
|
76
|
+
to="tenancy.Tenant",
|
|
77
|
+
on_delete=models.PROTECT,
|
|
78
|
+
related_name="namespaces",
|
|
79
|
+
blank=True,
|
|
80
|
+
null=True,
|
|
81
|
+
)
|
|
71
82
|
|
|
72
83
|
@property
|
|
73
84
|
def ip_addresses(self):
|
|
@@ -80,9 +91,17 @@ class Namespace(PrimaryModel):
|
|
|
80
91
|
def __str__(self):
|
|
81
92
|
return self.name
|
|
82
93
|
|
|
94
|
+
def delete(self, *args, **kwargs):
|
|
95
|
+
if self.name == "Global":
|
|
96
|
+
default_namespace_pk.set(None)
|
|
97
|
+
super().delete(*args, **kwargs)
|
|
98
|
+
|
|
83
99
|
|
|
84
100
|
def get_default_namespace():
|
|
85
|
-
"""Return the Global namespace.
|
|
101
|
+
"""Return the Global namespace.
|
|
102
|
+
|
|
103
|
+
Because this has no access to historical models, this MUST NOT be called during migrations.
|
|
104
|
+
"""
|
|
86
105
|
obj, _ = Namespace.objects.get_or_create(
|
|
87
106
|
name="Global", defaults={"description": "Default Global namespace. Created by Nautobot."}
|
|
88
107
|
)
|
|
@@ -91,7 +110,15 @@ def get_default_namespace():
|
|
|
91
110
|
|
|
92
111
|
def get_default_namespace_pk():
|
|
93
112
|
"""Return the PK of the Global namespace for use in default value for foreign keys."""
|
|
94
|
-
|
|
113
|
+
pk = default_namespace_pk.get()
|
|
114
|
+
if pk is None:
|
|
115
|
+
# MUST NEVER HAPPEN DURING MIGRATIONS, because get_default_namespace() doesn't use historical models.
|
|
116
|
+
# This is accommodated in migration ipam__0030 to directly set default_namespace_pk *from* the historical model
|
|
117
|
+
# but any other migrations using this function may need to implement similar workarounds.
|
|
118
|
+
pk = get_default_namespace().pk
|
|
119
|
+
default_namespace_pk.set(pk)
|
|
120
|
+
|
|
121
|
+
return pk
|
|
95
122
|
|
|
96
123
|
|
|
97
124
|
@extras_features(
|
nautobot/ipam/navigation.py
CHANGED
|
@@ -4,12 +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="IPAM",
|
|
11
|
-
icon=
|
|
12
|
-
weight=
|
|
12
|
+
icon=NavigationIconChoices.IPAM,
|
|
13
|
+
weight=NavigationWeightChoices.IPAM,
|
|
13
14
|
groups=(
|
|
14
15
|
NavMenuGroup(
|
|
15
16
|
name="IP Addresses",
|
nautobot/ipam/signals.py
CHANGED
|
@@ -5,6 +5,7 @@ from django.db.models.signals import m2m_changed, pre_delete, pre_save
|
|
|
5
5
|
from django.dispatch import receiver
|
|
6
6
|
|
|
7
7
|
from nautobot.ipam.models import (
|
|
8
|
+
IPAddress,
|
|
8
9
|
IPAddressToInterface,
|
|
9
10
|
Prefix,
|
|
10
11
|
PrefixLocationAssignment,
|
|
@@ -136,3 +137,73 @@ def assert_locations_content_types(sender, instance, action, reverse, model, pk_
|
|
|
136
137
|
raise ValidationError(
|
|
137
138
|
{key: f"{instance} is a {instance.location_type} and may not have {label} associated to it."}
|
|
138
139
|
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _validate_interface_ipaddress_assignments(sender, instance, pk_set, interface_field):
|
|
143
|
+
"""
|
|
144
|
+
Helper function to validate IPAddressToInterface instances on Interface and VMInterface objects after M2M operations.
|
|
145
|
+
|
|
146
|
+
For example:
|
|
147
|
+
* interface.ip_addresses.add(ip_address)
|
|
148
|
+
* vm_interface.ip_addresses.add(ip_address)
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
sender: The through model class (IPAddressToInterface)
|
|
152
|
+
instance: The interface instance (Interface or VMInterface)
|
|
153
|
+
pk_set: Set of IP address PKs being modified
|
|
154
|
+
interface_field: Field name to filter on ('interface' or 'vm_interface')
|
|
155
|
+
"""
|
|
156
|
+
# Get the through model instances that were just created
|
|
157
|
+
filter_kwargs = {interface_field: instance, "ip_address_id__in": pk_set}
|
|
158
|
+
through_instances = sender.objects.filter(**filter_kwargs)
|
|
159
|
+
|
|
160
|
+
# Validate each through model instance
|
|
161
|
+
for through_instance in through_instances:
|
|
162
|
+
through_instance.full_clean()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _validate_ipaddress_interface_assignments(sender, instance, pk_set, interface_model):
|
|
166
|
+
"""
|
|
167
|
+
Helper function to validate IPAddressToInterface instances on IPAddress objects after M2M operations.
|
|
168
|
+
|
|
169
|
+
For example:
|
|
170
|
+
* ip_address.interfaces.add(interface)
|
|
171
|
+
* ip_address.vm_interfaces.add(vm_interface)
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
sender: The through model class (IPAddressToInterface)
|
|
175
|
+
instance: The interface instance (Interface or VMInterface)
|
|
176
|
+
pk_set: Set of IP address PKs being modified
|
|
177
|
+
interface_model: Field name to filter on ('interface' or 'vm_interface')
|
|
178
|
+
"""
|
|
179
|
+
filter_kwargs = {"ip_address": instance, interface_model + "_id__in": pk_set}
|
|
180
|
+
through_instances = sender.objects.filter(**filter_kwargs)
|
|
181
|
+
for through_instance in through_instances:
|
|
182
|
+
through_instance.full_clean()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@receiver(m2m_changed, sender=IPAddressToInterface)
|
|
186
|
+
def validate_interface_ip_assignments(sender, instance, action, pk_set, **kwargs):
|
|
187
|
+
"""
|
|
188
|
+
Validate IPAddressToInterface instances after M2M operations.
|
|
189
|
+
|
|
190
|
+
Handles both physical Interface and VMInterface IP assignments.
|
|
191
|
+
Since Django's M2M add() with through_defaults bypasses save() methods,
|
|
192
|
+
we validate the through model instances via signal handler.
|
|
193
|
+
"""
|
|
194
|
+
from nautobot.dcim.models import Interface
|
|
195
|
+
from nautobot.virtualization.models import VMInterface
|
|
196
|
+
|
|
197
|
+
if action == "post_add" and pk_set:
|
|
198
|
+
# Route to appropriate validation based on instance type
|
|
199
|
+
if isinstance(instance, Interface):
|
|
200
|
+
_validate_interface_ipaddress_assignments(sender, instance, pk_set, "interface")
|
|
201
|
+
elif isinstance(instance, VMInterface):
|
|
202
|
+
_validate_interface_ipaddress_assignments(sender, instance, pk_set, "vm_interface")
|
|
203
|
+
elif isinstance(instance, IPAddress):
|
|
204
|
+
interface_model = kwargs["model"]
|
|
205
|
+
|
|
206
|
+
if interface_model == Interface:
|
|
207
|
+
_validate_ipaddress_interface_assignments(sender, instance, pk_set, "interface")
|
|
208
|
+
elif interface_model == VMInterface:
|
|
209
|
+
_validate_ipaddress_interface_assignments(sender, instance, pk_set, "vm_interface")
|
nautobot/ipam/tables.py
CHANGED
|
@@ -205,11 +205,12 @@ VLANGROUP_ADD_VLAN = """
|
|
|
205
205
|
class NamespaceTable(BaseTable):
|
|
206
206
|
pk = ToggleColumn()
|
|
207
207
|
name = tables.LinkColumn()
|
|
208
|
+
tenant = TenantColumn()
|
|
208
209
|
tags = TagColumn(url_name="ipam:namespace_list")
|
|
209
210
|
|
|
210
211
|
class Meta(BaseTable.Meta):
|
|
211
212
|
model = Namespace
|
|
212
|
-
fields = ("pk", "name", "description", "location")
|
|
213
|
+
fields = ("pk", "name", "description", "tenant", "location")
|
|
213
214
|
|
|
214
215
|
|
|
215
216
|
#
|
|
@@ -369,7 +370,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
369
370
|
tenant = TenantColumn()
|
|
370
371
|
namespace = tables.Column(linkify=True)
|
|
371
372
|
vlan = tables.Column(linkify=True, verbose_name="VLAN")
|
|
372
|
-
rir = tables.Column(linkify=True)
|
|
373
|
+
rir = tables.Column(linkify=True, verbose_name="RIR")
|
|
373
374
|
children = tables.Column(accessor="descendants_count", orderable=False)
|
|
374
375
|
date_allocated = tables.DateTimeColumn()
|
|
375
376
|
location_count = LinkedCountColumn(
|
|
@@ -378,6 +379,11 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
378
379
|
cloud_networks_count = LinkedCountColumn(
|
|
379
380
|
viewname="cloud:cloudnetwork_list", url_params={"prefixes": "pk"}, verbose_name="Cloud Networks"
|
|
380
381
|
)
|
|
382
|
+
tunnel_endpoints_count = LinkedCountColumn(
|
|
383
|
+
viewname="vpn:vpntunnelendpoint_list",
|
|
384
|
+
url_params={"protected_prefixes": "pk"},
|
|
385
|
+
verbose_name="VPN Tunnel Endpoints",
|
|
386
|
+
)
|
|
381
387
|
actions = ButtonsColumn(Prefix)
|
|
382
388
|
|
|
383
389
|
class Meta(BaseTable.Meta):
|
|
@@ -393,6 +399,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
393
399
|
"tenant",
|
|
394
400
|
"location_count",
|
|
395
401
|
"cloud_networks_count",
|
|
402
|
+
"tunnel_endpoints_count",
|
|
396
403
|
"vlan",
|
|
397
404
|
"role",
|
|
398
405
|
"rir",
|
|
@@ -415,7 +422,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
415
422
|
"actions",
|
|
416
423
|
)
|
|
417
424
|
row_attrs = {
|
|
418
|
-
"class": lambda record: "success" if not record.present_in_database else "",
|
|
425
|
+
"class": lambda record: "table-success" if not record.present_in_database else "",
|
|
419
426
|
}
|
|
420
427
|
|
|
421
428
|
|
|
@@ -449,6 +456,7 @@ class PrefixDetailTable(PrefixTable):
|
|
|
449
456
|
"role",
|
|
450
457
|
"description",
|
|
451
458
|
"tags",
|
|
459
|
+
"actions",
|
|
452
460
|
)
|
|
453
461
|
default_columns = (
|
|
454
462
|
"pk",
|
|
@@ -463,6 +471,7 @@ class PrefixDetailTable(PrefixTable):
|
|
|
463
471
|
"vlan",
|
|
464
472
|
"role",
|
|
465
473
|
"description",
|
|
474
|
+
"actions",
|
|
466
475
|
)
|
|
467
476
|
|
|
468
477
|
|
|
@@ -499,6 +508,7 @@ class IPAddressTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
499
508
|
distinct=True,
|
|
500
509
|
verbose_name="Virtual Machines",
|
|
501
510
|
)
|
|
511
|
+
actions = ButtonsColumn(Prefix)
|
|
502
512
|
|
|
503
513
|
class Meta(BaseTable.Meta):
|
|
504
514
|
model = IPAddress
|
|
@@ -516,9 +526,10 @@ class IPAddressTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
516
526
|
"interface_parent_count",
|
|
517
527
|
"vm_interface_count",
|
|
518
528
|
"vm_interface_parent_count",
|
|
529
|
+
"actions",
|
|
519
530
|
)
|
|
520
531
|
row_attrs = {
|
|
521
|
-
"class": lambda record: "success" if not isinstance(record, IPAddress) else "",
|
|
532
|
+
"class": lambda record: "table-success" if not isinstance(record, IPAddress) else "",
|
|
522
533
|
}
|
|
523
534
|
|
|
524
535
|
|
|
@@ -545,6 +556,7 @@ class IPAddressDetailTable(IPAddressTable):
|
|
|
545
556
|
"dns_name",
|
|
546
557
|
"description",
|
|
547
558
|
"tags",
|
|
559
|
+
"actions",
|
|
548
560
|
)
|
|
549
561
|
default_columns = (
|
|
550
562
|
"pk",
|
|
@@ -557,6 +569,7 @@ class IPAddressDetailTable(IPAddressTable):
|
|
|
557
569
|
"assigned",
|
|
558
570
|
"dns_name",
|
|
559
571
|
"description",
|
|
572
|
+
"actions",
|
|
560
573
|
)
|
|
561
574
|
|
|
562
575
|
|
|
@@ -651,7 +664,7 @@ class IPAddressInterfaceTable(InterfaceTable):
|
|
|
651
664
|
"connection",
|
|
652
665
|
]
|
|
653
666
|
row_attrs = {
|
|
654
|
-
"
|
|
667
|
+
"class": cable_status_color_css,
|
|
655
668
|
"data-name": lambda record: record.name,
|
|
656
669
|
}
|
|
657
670
|
|
|
@@ -744,7 +757,7 @@ class VLANTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
744
757
|
"description",
|
|
745
758
|
)
|
|
746
759
|
row_attrs = {
|
|
747
|
-
"class": lambda record: "success" if not isinstance(record, VLAN) else "",
|
|
760
|
+
"class": lambda record: "table-success" if not isinstance(record, VLAN) else "",
|
|
748
761
|
}
|
|
749
762
|
|
|
750
763
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{% load helpers %}
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
</
|
|
11
|
-
|
|
2
|
+
<div class="btn-group" role="group">
|
|
3
|
+
<a href="{% django_querystring show_available='true' %}"
|
|
4
|
+
class="btn btn-primary{% if show_available %} bg-primary nb-text-body-bg{% endif %}">
|
|
5
|
+
<span class="mdi mdi-eye-outline"></span> Show {{ label }}
|
|
6
|
+
</a>
|
|
7
|
+
<a href="{% django_querystring show_available='false' %}"
|
|
8
|
+
class="btn btn-primary{% if not show_available %} bg-primary nb-text-body-bg{% endif %}">
|
|
9
|
+
<span class="mdi mdi-eye-off-outline"></span> Hide {{ label }}
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
{# TODO: this file is unused as far as I can tell - remove it? #}
|
|
1
2
|
<div class="float-end">
|
|
2
3
|
{% if perms.ipam.add_vlan and first_available_vlan %}
|
|
3
4
|
<a href="{% url 'ipam:vlan_add' %}?vid={{ first_available_vlan }}&group={{ object.pk }}{% if object.location %}&location={{ object.location.pk }}{% endif %}" class="btn btn-success">
|
|
@@ -136,6 +136,20 @@
|
|
|
136
136
|
{% endif %}
|
|
137
137
|
</td>
|
|
138
138
|
</tr>
|
|
139
|
+
<tr>
|
|
140
|
+
<td>VPN Endpoints</td>
|
|
141
|
+
<td>
|
|
142
|
+
{% if object.vpn_tunnel_endpoints_src_ip.exists %}
|
|
143
|
+
<ul class="list-unstyled">
|
|
144
|
+
{% for endpoint in object.vpn_tunnel_endpoints_src_ip.all %}
|
|
145
|
+
<li>{{ endpoint|hyperlinked_object }}</li>
|
|
146
|
+
{% endfor %}
|
|
147
|
+
</ul>
|
|
148
|
+
{% else %}
|
|
149
|
+
<span class="text-secondary">None</span>
|
|
150
|
+
{% endif %}
|
|
151
|
+
</td>
|
|
152
|
+
</tr>
|
|
139
153
|
</table>
|
|
140
154
|
</div>
|
|
141
155
|
{% endblock content_right_page %}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
{% block content %}
|
|
17
17
|
<form action="" method="post" enctype="multipart/form-data" class="h-100 vstack">
|
|
18
18
|
{% csrf_token %}
|
|
19
|
-
<div class="row align-content-start flex-fill">
|
|
20
|
-
<div class="col-lg-10
|
|
19
|
+
<div class="row justify-content-center align-content-start flex-fill">
|
|
20
|
+
<div class="col-lg-10">
|
|
21
21
|
<div class="card border-info">
|
|
22
22
|
<div class="card-header bg-info-subtle border-info fw-medium text-body"><strong>Confirm Merging IP Addresses</strong></div>
|
|
23
23
|
<div class="card-body">
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
</div>
|
|
27
27
|
</div>
|
|
28
28
|
</div>
|
|
29
|
-
<div class="col-lg-10
|
|
29
|
+
<div class="col-lg-10">
|
|
30
30
|
<div class="card">
|
|
31
31
|
<div class="table-responsive">
|
|
32
32
|
<table class="table table-hover nb-table-headings">
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{% extends 'generic/object_create.html' %}
|
|
2
|
+
{% load form_helpers %}
|
|
3
|
+
|
|
4
|
+
{% block form %}
|
|
5
|
+
<div class="card">
|
|
6
|
+
<div class="card-header"><strong>Namespace</strong></div>
|
|
7
|
+
<div class="card-body">
|
|
8
|
+
{% render_field form.name %}
|
|
9
|
+
{% render_field form.description %}
|
|
10
|
+
{% render_field form.location %}
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
{% include 'inc/tenancy_form_panel.html' %}
|
|
14
|
+
{% include 'inc/extras_features_edit_form_fields.html' %}
|
|
15
|
+
{% endblock %}
|
|
@@ -2,19 +2,20 @@
|
|
|
2
2
|
{% load helpers %}
|
|
3
3
|
|
|
4
4
|
{% block buttons %}
|
|
5
|
-
<div class="
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<
|
|
5
|
+
<div class="dropdown">
|
|
6
|
+
<button class="btn btn-secondary dropdown-toggle" type="button" id="max_length" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
|
7
|
+
Max Length{% if "prefix_length__lte" in request.GET %}: {{ request.GET.prefix_length__lte }}{% endif %}
|
|
8
|
+
<span class="mdi mdi-chevron-down"></span>
|
|
9
|
+
</button>
|
|
10
|
+
<ul class="dropdown-menu" aria-labelledby="max_length">
|
|
11
|
+
{% for i in "4,8,12,16,20,24,28,32,40,48,56,64"|split %}
|
|
12
|
+
<li>
|
|
13
|
+
<a class="dropdown-item"
|
|
14
|
+
href="{% url 'ipam:prefix_list' %}{% legacy_querystring request prefix_length__lte=i page=1 %}">
|
|
14
15
|
{{ i }} {% if request.GET.prefix_length__lte == i %}<span class="mdi mdi-check-bold"></span>{% endif %}
|
|
15
|
-
</a
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</
|
|
16
|
+
</a>
|
|
17
|
+
</li>
|
|
18
|
+
{% endfor %}
|
|
19
|
+
</ul>
|
|
19
20
|
</div>
|
|
20
21
|
{% endblock %}
|
|
@@ -508,3 +508,92 @@ class IPAMDataMigration0031TestCase(MigratorTestCase):
|
|
|
508
508
|
for ip in IPAddress.objects.iterator():
|
|
509
509
|
self.assertLessEqual(netaddr.IPAddress(ip.parent.network), netaddr.IPAddress(ip.host))
|
|
510
510
|
self.assertGreaterEqual(netaddr.IPAddress(ip.parent.broadcast), netaddr.IPAddress(ip.host))
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
class NamespaceTenantCircularDependencyTestCase(MigratorTestCase):
|
|
514
|
+
"""Test that our 3-phase migration approach resolves circular dependencies and supports rollback."""
|
|
515
|
+
|
|
516
|
+
migrate_from = ("ipam", "0029_ip_address_to_interface_uniqueness_constraints") # Before namespace creation
|
|
517
|
+
migrate_to = ("ipam", "0054_namespace_tenant") # After our complete sequence
|
|
518
|
+
|
|
519
|
+
def prepare(self):
|
|
520
|
+
"""Create minimal data to test namespace-tenant migration sequence."""
|
|
521
|
+
# Create basic tenancy data in old state
|
|
522
|
+
TenantGroup = self.old_state.apps.get_model("tenancy", "TenantGroup")
|
|
523
|
+
Tenant = self.old_state.apps.get_model("tenancy", "Tenant")
|
|
524
|
+
|
|
525
|
+
self.tenant_group = TenantGroup.objects.create(name="Test Group")
|
|
526
|
+
self.tenant1 = Tenant.objects.create(name="Test Tenant 1", tenant_group=self.tenant_group)
|
|
527
|
+
self.tenant2 = Tenant.objects.create(name="Test Tenant 2", tenant_group=self.tenant_group)
|
|
528
|
+
|
|
529
|
+
def test_forward_migration_resolves_circular_dependency(self):
|
|
530
|
+
"""Test that fresh database creation works with tenant on Namespace."""
|
|
531
|
+
Namespace = self.new_state.apps.get_model("ipam", "Namespace")
|
|
532
|
+
VRF = self.new_state.apps.get_model("ipam", "VRF")
|
|
533
|
+
Prefix = self.new_state.apps.get_model("ipam", "Prefix")
|
|
534
|
+
|
|
535
|
+
# Verify Global namespace exists
|
|
536
|
+
global_namespace = Namespace.objects.get(name="Global")
|
|
537
|
+
self.assertIsNotNone(global_namespace)
|
|
538
|
+
|
|
539
|
+
# Verify tenant field exists on Namespace
|
|
540
|
+
self.assertTrue(hasattr(global_namespace, "tenant"))
|
|
541
|
+
|
|
542
|
+
# Verify VRF and Prefix have namespace defaults working
|
|
543
|
+
vrf_field = VRF._meta.get_field("namespace")
|
|
544
|
+
prefix_field = Prefix._meta.get_field("namespace")
|
|
545
|
+
self.assertIsNotNone(vrf_field.default)
|
|
546
|
+
self.assertIsNotNone(prefix_field.default)
|
|
547
|
+
|
|
548
|
+
def test_namespace_tenant_field_added(self):
|
|
549
|
+
"""Test that tenant field was properly added to Namespace model."""
|
|
550
|
+
Namespace = self.new_state.apps.get_model("ipam", "Namespace")
|
|
551
|
+
|
|
552
|
+
# Create a namespace with tenant assignment
|
|
553
|
+
test_namespace = Namespace.objects.create(
|
|
554
|
+
name="Test Namespace", tenant_id=self.tenant1.pk, description="Test namespace with tenant"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Verify tenant field exists and works
|
|
558
|
+
self.assertEqual(test_namespace.tenant_id, self.tenant1.pk)
|
|
559
|
+
self.assertEqual(test_namespace.name, "Test Namespace")
|
|
560
|
+
|
|
561
|
+
# Verify tenant relationship works
|
|
562
|
+
retrieved_namespace = Namespace.objects.get(name="Test Namespace")
|
|
563
|
+
self.assertEqual(retrieved_namespace.tenant_id, self.tenant1.pk)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
class NamespaceTenantRollbackTestCase(MigratorTestCase):
|
|
567
|
+
"""Test rolling back the namespace-tenant feature works safely."""
|
|
568
|
+
|
|
569
|
+
# For rollback testing: migrate_from is AFTER our changes, migrate_to is BEFORE
|
|
570
|
+
migrate_from = ("ipam", "0054_namespace_tenant") # After our complete sequence
|
|
571
|
+
migrate_to = ("ipam", "0052_alter_ipaddress_index_together_and_more") # Before our changes
|
|
572
|
+
|
|
573
|
+
def prepare(self):
|
|
574
|
+
"""Create namespace data in the 'after' state to test rollback."""
|
|
575
|
+
# Create tenancy data first
|
|
576
|
+
TenantGroup = self.old_state.apps.get_model("tenancy", "TenantGroup")
|
|
577
|
+
Tenant = self.old_state.apps.get_model("tenancy", "Tenant")
|
|
578
|
+
|
|
579
|
+
self.tenant_group = TenantGroup.objects.create(name="Test Tenant Group")
|
|
580
|
+
self.tenant1 = Tenant.objects.create(name="Test Tenant 1", tenant_group=self.tenant_group)
|
|
581
|
+
|
|
582
|
+
# Create namespace with tenant assignment in the 'after' state
|
|
583
|
+
Namespace = self.old_state.apps.get_model("ipam", "Namespace")
|
|
584
|
+
self.test_namespace = Namespace.objects.create(
|
|
585
|
+
name="Test Namespace", tenant_id=self.tenant1.pk, description="Test namespace for rollback"
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
def test_rollback_migration_preserves_data_integrity(self):
|
|
589
|
+
"""Test that rolling back preserves namespace data but removes tenant field."""
|
|
590
|
+
# In the rolled-back state, tenant field should be gone
|
|
591
|
+
Namespace = self.new_state.apps.get_model("ipam", "Namespace")
|
|
592
|
+
rolled_back_namespace = Namespace.objects.get(pk=self.test_namespace.pk)
|
|
593
|
+
|
|
594
|
+
# Verify core fields preserved
|
|
595
|
+
self.assertEqual(rolled_back_namespace.name, "Test Namespace")
|
|
596
|
+
self.assertEqual(rolled_back_namespace.description, "Test namespace for rollback")
|
|
597
|
+
|
|
598
|
+
# Verify tenant field no longer exists (rolled back)
|
|
599
|
+
self.assertFalse(hasattr(rolled_back_namespace, "tenant"))
|