nautobot 3.0.0a2__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 +0 -2
- nautobot/apps/filters.py +7 -9
- nautobot/apps/models.py +2 -2
- nautobot/apps/ui.py +9 -1
- nautobot/circuits/filters.py +3 -2
- nautobot/circuits/navigation.py +3 -2
- nautobot/circuits/templates/circuits/circuit.html +1 -1
- nautobot/circuits/templates/circuits/circuit_create.html +3 -3
- nautobot/circuits/templates/circuits/circuittermination.html +1 -1
- nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
- nautobot/circuits/templates/circuits/circuittype.html +1 -1
- nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
- nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
- nautobot/circuits/templates/circuits/providernetwork.html +1 -1
- nautobot/circuits/tests/integration/test_circuit.py +10 -13
- 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 +1 -0
- nautobot/core/api/views.py +8 -0
- nautobot/core/apps/__init__.py +11 -10
- nautobot/core/celery/__init__.py +3 -5
- nautobot/core/checks.py +46 -0
- nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
- nautobot/core/cli/migrate_deprecated_templates.py +200 -0
- nautobot/core/constants.py +3 -0
- nautobot/core/context_processors.py +9 -1
- nautobot/core/forms/forms.py +1 -1
- nautobot/core/jobs/__init__.py +6 -3
- nautobot/core/jobs/groups.py +31 -1
- nautobot/core/management/commands/generate_test_data.py +28 -9
- nautobot/core/models/generics.py +9 -1
- nautobot/core/models/tree_queries.py +10 -5
- nautobot/core/settings.py +18 -12
- nautobot/core/settings.yaml +13 -7
- 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/config/config.html +12 -12
- nautobot/core/templates/admin/index.html +3 -3
- 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/panel.html +3 -3
- nautobot/core/templates/components/tab/content_wrapper.html +2 -3
- nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
- nautobot/core/templates/echarts/echarts.html +1 -1
- nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
- nautobot/core/templates/generic/object_bulk_create.html +4 -3
- nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
- 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_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 +2 -3
- nautobot/core/templates/graphene/graphiql.html +7 -7
- 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/form_static_field.html +6 -0
- nautobot/core/templates/inc/header.html +1 -1
- nautobot/core/templates/inc/image_attachments.html +2 -1
- nautobot/core/templates/inc/nav_menu.html +2 -1
- nautobot/core/templates/inc/search_panel.html +4 -4
- nautobot/core/templates/login.html +4 -2
- nautobot/core/templates/nautobot_config.py.j2 +6 -5
- nautobot/core/templates/redoc_ui.html +7 -0
- 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 +4 -4
- nautobot/core/templates/utilities/theme_preview.html +16 -3
- nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
- nautobot/core/templatetags/helpers.py +52 -6
- nautobot/core/testing/api.py +68 -9
- nautobot/core/testing/filters.py +0 -23
- nautobot/core/testing/integration.py +23 -10
- nautobot/core/testing/mixins.py +2 -0
- nautobot/core/testing/views.py +4 -0
- nautobot/core/tests/integration/test_app_home.py +34 -30
- nautobot/core/tests/integration/test_app_navbar.py +3 -0
- nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
- nautobot/core/tests/runner.py +9 -1
- nautobot/core/tests/test_api.py +5 -3
- nautobot/core/tests/test_breadcrumbs.py +6 -7
- 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 +75 -1
- 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_tables.py +3 -1
- nautobot/core/tests/test_templatetags_helpers.py +53 -13
- nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
- nautobot/core/tests/test_tree_queries.py +14 -1
- nautobot/core/tests/test_ui.py +1 -1
- nautobot/core/tests/test_utils.py +31 -4
- nautobot/core/tests/test_views.py +159 -31
- nautobot/core/ui/breadcrumbs.py +2 -12
- nautobot/core/ui/choices.py +142 -10
- nautobot/core/ui/constants.py +76 -12
- nautobot/core/ui/object_detail.py +92 -12
- nautobot/core/urls.py +12 -1
- nautobot/core/utils/cache.py +2 -1
- nautobot/core/utils/filtering.py +17 -17
- nautobot/core/utils/lookup.py +3 -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 +15 -3
- nautobot/core/views/renderers.py +2 -0
- 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 +0 -12
- 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 +0 -13
- 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} +2 -35
- nautobot/dcim/forms.py +1 -1
- 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/device_components.py +81 -68
- nautobot/dcim/models/devices.py +13 -16
- nautobot/dcim/navigation.py +7 -6
- nautobot/dcim/tables/devices.py +3 -0
- nautobot/dcim/tables/template_code.py +14 -14
- nautobot/dcim/templates/dcim/cable.html +2 -61
- nautobot/dcim/templates/dcim/cable_connect.html +28 -112
- nautobot/dcim/templates/dcim/cable_edit.html +2 -5
- nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
- nautobot/dcim/templates/dcim/cable_trace.html +1 -3
- nautobot/dcim/templates/dcim/cable_update.html +5 -0
- nautobot/dcim/templates/dcim/consoleport.html +6 -5
- nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
- nautobot/dcim/templates/dcim/device/config.html +2 -2
- nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
- nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
- nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
- nautobot/dcim/templates/dcim/device/frontports.html +1 -1
- nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
- nautobot/dcim/templates/dcim/device/inventory.html +1 -1
- nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
- nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
- nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
- nautobot/dcim/templates/dcim/device/powerports.html +1 -1
- nautobot/dcim/templates/dcim/device/rearports.html +1 -1
- nautobot/dcim/templates/dcim/device/status.html +8 -8
- nautobot/dcim/templates/dcim/device/wireless.html +1 -1
- 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_populate.html +2 -2
- nautobot/dcim/templates/dcim/devicetype.html +1 -1
- 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 +9 -8
- nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +26 -6
- nautobot/dcim/templates/dcim/interface_bulk_delete.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/location_retrieve.html +1 -242
- nautobot/dcim/templates/dcim/module/base.html +49 -9
- nautobot/dcim/templates/dcim/module_list.html +57 -8
- nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
- nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
- nautobot/dcim/templates/dcim/platform_create.html +1 -1
- nautobot/dcim/templates/dcim/powerfeed.html +1 -1
- nautobot/dcim/templates/dcim/powerpanel.html +1 -1
- nautobot/dcim/templates/dcim/powerport.html +5 -4
- nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
- nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
- nautobot/dcim/templates/dcim/rearport.html +7 -6
- nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
- nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
- nautobot/dcim/tests/integration/test_controller.py +1 -0
- nautobot/dcim/tests/test_api.py +8 -0
- nautobot/dcim/tests/test_custom_validators.py +229 -0
- nautobot/dcim/tests/test_filters.py +12 -6
- nautobot/dcim/tests/test_models.py +63 -4
- nautobot/dcim/tests/test_views.py +63 -22
- nautobot/dcim/urls.py +64 -21
- nautobot/dcim/utils.py +3 -3
- nautobot/dcim/views.py +547 -273
- nautobot/extras/api/views.py +9 -1
- nautobot/extras/choices.py +2 -13
- 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} +14 -46
- nautobot/extras/forms/forms.py +5 -13
- nautobot/extras/forms/mixins.py +0 -41
- nautobot/extras/management/__init__.py +9 -0
- 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/models/__init__.py +1 -2
- nautobot/extras/models/approvals.py +22 -13
- nautobot/extras/models/contacts.py +2 -0
- nautobot/extras/models/groups.py +44 -5
- nautobot/extras/models/jobs.py +59 -1
- nautobot/extras/models/mixins.py +28 -0
- nautobot/extras/models/models.py +13 -0
- 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/tables.py +3 -3
- nautobot/extras/plugins/urls.py +2 -21
- nautobot/extras/plugins/utils.py +1 -33
- nautobot/extras/plugins/views.py +0 -4
- nautobot/extras/signals.py +20 -19
- nautobot/extras/tables.py +52 -68
- 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/computedfield.html +1 -1
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
- nautobot/extras/templates/extras/customfield.html +1 -1
- 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/graphqlquery_retrieve.html +1 -96
- 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 -37
- nautobot/extras/templates/extras/plugin_detail.html +26 -21
- 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/secretsgroup.html +1 -1
- nautobot/extras/templates/extras/tag.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_configcontextschema.py +27 -26
- nautobot/extras/tests/integration/test_customfields.py +8 -7
- nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
- nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
- nautobot/extras/tests/integration/test_plugins.py +18 -6
- nautobot/extras/tests/test_api.py +27 -18
- nautobot/extras/tests/test_approvals.py +38 -38
- nautobot/extras/tests/test_changelog.py +35 -3
- 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 +21 -19
- 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 +13 -31
- nautobot/extras/tests/test_plugins.py +36 -10
- nautobot/extras/tests/test_views.py +31 -30
- nautobot/extras/views.py +81 -19
- nautobot/ipam/factory.py +7 -0
- nautobot/ipam/filter_mixins.py +38 -0
- nautobot/ipam/filters.py +27 -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 +13 -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_update.html +15 -0
- nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
- nautobot/ipam/templates/ipam/prefix_list.html +14 -13
- nautobot/ipam/templates/ipam/service.html +1 -1
- nautobot/ipam/templates/ipam/vlan.html +1 -1
- 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 +10 -0
- nautobot/ipam/tests/test_forms.py +1 -1
- nautobot/ipam/tests/test_models.py +43 -1
- 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 +46 -6
- nautobot/project-static/dist/css/nautobot.css +1 -1
- nautobot/project-static/dist/css/nautobot.css.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/interface_filtering.js +20 -16
- nautobot/project-static/nautobot-icons/battery-3.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/src/js/collapse.js +3 -3
- nautobot/ui/src/js/nautobot.js +16 -0
- nautobot/ui/src/scss/colors.scss +1 -1
- nautobot/ui/src/scss/nautobot.scss +61 -28
- 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/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.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_models.py +44 -4
- 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/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.0a3.dist-info}/METADATA +14 -14
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
- nautobot/data_validation/template_content.py +0 -42
- nautobot/dcim/filters/mixins.py +0 -354
- nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
- /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
- {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.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(
|
|
@@ -415,7 +416,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
415
416
|
"actions",
|
|
416
417
|
)
|
|
417
418
|
row_attrs = {
|
|
418
|
-
"class": lambda record: "success" if not record.present_in_database else "",
|
|
419
|
+
"class": lambda record: "table-success" if not record.present_in_database else "",
|
|
419
420
|
}
|
|
420
421
|
|
|
421
422
|
|
|
@@ -449,6 +450,7 @@ class PrefixDetailTable(PrefixTable):
|
|
|
449
450
|
"role",
|
|
450
451
|
"description",
|
|
451
452
|
"tags",
|
|
453
|
+
"actions",
|
|
452
454
|
)
|
|
453
455
|
default_columns = (
|
|
454
456
|
"pk",
|
|
@@ -463,6 +465,7 @@ class PrefixDetailTable(PrefixTable):
|
|
|
463
465
|
"vlan",
|
|
464
466
|
"role",
|
|
465
467
|
"description",
|
|
468
|
+
"actions",
|
|
466
469
|
)
|
|
467
470
|
|
|
468
471
|
|
|
@@ -499,6 +502,7 @@ class IPAddressTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
499
502
|
distinct=True,
|
|
500
503
|
verbose_name="Virtual Machines",
|
|
501
504
|
)
|
|
505
|
+
actions = ButtonsColumn(Prefix)
|
|
502
506
|
|
|
503
507
|
class Meta(BaseTable.Meta):
|
|
504
508
|
model = IPAddress
|
|
@@ -516,9 +520,10 @@ class IPAddressTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
516
520
|
"interface_parent_count",
|
|
517
521
|
"vm_interface_count",
|
|
518
522
|
"vm_interface_parent_count",
|
|
523
|
+
"actions",
|
|
519
524
|
)
|
|
520
525
|
row_attrs = {
|
|
521
|
-
"class": lambda record: "success" if not isinstance(record, IPAddress) else "",
|
|
526
|
+
"class": lambda record: "table-success" if not isinstance(record, IPAddress) else "",
|
|
522
527
|
}
|
|
523
528
|
|
|
524
529
|
|
|
@@ -545,6 +550,7 @@ class IPAddressDetailTable(IPAddressTable):
|
|
|
545
550
|
"dns_name",
|
|
546
551
|
"description",
|
|
547
552
|
"tags",
|
|
553
|
+
"actions",
|
|
548
554
|
)
|
|
549
555
|
default_columns = (
|
|
550
556
|
"pk",
|
|
@@ -557,6 +563,7 @@ class IPAddressDetailTable(IPAddressTable):
|
|
|
557
563
|
"assigned",
|
|
558
564
|
"dns_name",
|
|
559
565
|
"description",
|
|
566
|
+
"actions",
|
|
560
567
|
)
|
|
561
568
|
|
|
562
569
|
|
|
@@ -651,7 +658,7 @@ class IPAddressInterfaceTable(InterfaceTable):
|
|
|
651
658
|
"connection",
|
|
652
659
|
]
|
|
653
660
|
row_attrs = {
|
|
654
|
-
"
|
|
661
|
+
"class": cable_status_color_css,
|
|
655
662
|
"data-name": lambda record: record.name,
|
|
656
663
|
}
|
|
657
664
|
|
|
@@ -744,7 +751,7 @@ class VLANTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
744
751
|
"description",
|
|
745
752
|
)
|
|
746
753
|
row_attrs = {
|
|
747
|
-
"class": lambda record: "success" if not isinstance(record, VLAN) else "",
|
|
754
|
+
"class": lambda record: "table-success" if not isinstance(record, VLAN) else "",
|
|
748
755
|
}
|
|
749
756
|
|
|
750
757
|
|
|
@@ -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 %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
{% extends '
|
|
1
|
+
{% extends 'generic/object_retrieve.html' %}
|
|
2
2
|
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
{% extends '
|
|
1
|
+
{% extends 'generic/object_retrieve.html' %}
|
|
2
2
|
{% comment %}3.0 TODO: remove this template, which only exists for backward compatibility with 2.4 and earlier{% endcomment %}
|
|
@@ -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"))
|