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
|
@@ -47,6 +47,7 @@ from nautobot.core.utils.lookup import get_filterset_for_model, get_route_for_mo
|
|
|
47
47
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
48
48
|
from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
|
|
49
49
|
from nautobot.core.views.utils import get_obj_from_context
|
|
50
|
+
from nautobot.data_validation.tables import DataComplianceTable
|
|
50
51
|
from nautobot.dcim.models import Rack
|
|
51
52
|
from nautobot.extras.choices import CustomFieldTypeChoices
|
|
52
53
|
from nautobot.extras.tables import AssociatedContactsTable, DynamicGroupTable, ObjectMetadataTable
|
|
@@ -101,6 +102,7 @@ class ObjectDetailContent:
|
|
|
101
102
|
_ObjectDetailContactsTab(),
|
|
102
103
|
_ObjectDetailGroupsTab(),
|
|
103
104
|
_ObjectDetailMetadataTab(),
|
|
105
|
+
_ObjectDetailDataComplianceTab(),
|
|
104
106
|
]
|
|
105
107
|
if extra_tabs is not None:
|
|
106
108
|
tabs.extend(extra_tabs)
|
|
@@ -262,8 +264,11 @@ class Button(Component):
|
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
def should_render(self, context: Context):
|
|
267
|
+
# Only show if the user has the permission, which is enforce in super.
|
|
265
268
|
if not super().should_render(context):
|
|
266
269
|
return False
|
|
270
|
+
if self.render_on_tab_id == "__all__":
|
|
271
|
+
return True
|
|
267
272
|
return context.get("active_tab", "main") == self.render_on_tab_id
|
|
268
273
|
|
|
269
274
|
def render(self, context: Context):
|
|
@@ -377,8 +382,9 @@ class Tab(Component):
|
|
|
377
382
|
WEIGHT_CONTACTS_TAB = 300
|
|
378
383
|
WEIGHT_GROUPS_TAB = 400
|
|
379
384
|
WEIGHT_METADATA_TAB = 500
|
|
380
|
-
|
|
381
|
-
|
|
385
|
+
WEIGHT_DATACOMPLIANCE_TAB = 600
|
|
386
|
+
WEIGHT_NOTES_TAB = 700 # reserved, not yet using this framework
|
|
387
|
+
WEIGHT_CHANGELOG_TAB = 800 # reserved, not yet using this framework
|
|
382
388
|
|
|
383
389
|
def panels_for_section(self, section):
|
|
384
390
|
"""
|
|
@@ -540,6 +546,7 @@ class Panel(Component):
|
|
|
540
546
|
self,
|
|
541
547
|
*,
|
|
542
548
|
label="",
|
|
549
|
+
css_class="default",
|
|
543
550
|
section=SectionChoices.FULL_WIDTH,
|
|
544
551
|
body_id=None,
|
|
545
552
|
body_content_template_path=None,
|
|
@@ -554,6 +561,7 @@ class Panel(Component):
|
|
|
554
561
|
|
|
555
562
|
Args:
|
|
556
563
|
label (str): Label to display for this panel. Optional; if an empty string, the panel will have no label.
|
|
564
|
+
css_class (str): Panel variant to render as, e.g. "default", "warning", "info".
|
|
557
565
|
section (str): One of the [`SectionChoices`](./ui.md#nautobot.apps.ui.SectionChoices) values, indicating the layout section this Panel belongs to.
|
|
558
566
|
body_id (str): HTML element `id` to attach to the rendered body wrapper of the panel.
|
|
559
567
|
body_content_template_path (str): Template path to render the content contained *within* the panel body.
|
|
@@ -565,6 +573,7 @@ class Panel(Component):
|
|
|
565
573
|
(a `div` or `table`) as well as its contents. Generally you won't override this as a user.
|
|
566
574
|
"""
|
|
567
575
|
self.label = label
|
|
576
|
+
self.css_class = css_class
|
|
568
577
|
self.section = section
|
|
569
578
|
self.body_id = body_id
|
|
570
579
|
self.body_content_template_path = body_content_template_path
|
|
@@ -593,6 +602,7 @@ class Panel(Component):
|
|
|
593
602
|
self.template_path,
|
|
594
603
|
context,
|
|
595
604
|
label=self.render_label(context),
|
|
605
|
+
css_class=self.css_class,
|
|
596
606
|
header_extra_content=self.render_header_extra_content(context),
|
|
597
607
|
body=self.render_body(context),
|
|
598
608
|
footer_content=self.render_footer_content(context),
|
|
@@ -1299,8 +1309,11 @@ class EChartsPanel(Panel, EChartsBase):
|
|
|
1299
1309
|
self.width = width
|
|
1300
1310
|
self.height = height
|
|
1301
1311
|
self.chart_container_id = chart_container_id
|
|
1312
|
+
self.body_id = (
|
|
1313
|
+
self.chart_container_id or f"{slugify('echart-' + chart_kwargs.get('header', ''))}-{uuid.uuid4().hex[:8]}"
|
|
1314
|
+
)
|
|
1302
1315
|
|
|
1303
|
-
super().__init__(body_wrapper_template_path=body_wrapper_template_path, **kwargs)
|
|
1316
|
+
super().__init__(body_wrapper_template_path=body_wrapper_template_path, body_id=self.body_id, **kwargs)
|
|
1304
1317
|
EChartsBase.__init__(self, **chart_kwargs)
|
|
1305
1318
|
|
|
1306
1319
|
def get_data(self, context: Context) -> dict[str, Any] | None:
|
|
@@ -1344,8 +1357,7 @@ class EChartsPanel(Panel, EChartsBase):
|
|
|
1344
1357
|
"chart_config": chart_config,
|
|
1345
1358
|
"chart_width": self.width,
|
|
1346
1359
|
"chart_height": self.height,
|
|
1347
|
-
"chart_container_id": self.
|
|
1348
|
-
or f"{slugify(f'echart-{self.header}')}-{uuid.uuid4().hex[:8]}",
|
|
1360
|
+
"chart_container_id": self.body_id,
|
|
1349
1361
|
}
|
|
1350
1362
|
|
|
1351
1363
|
|
|
@@ -1542,7 +1554,7 @@ class GroupedKeyValueTablePanel(KeyValueTablePanel):
|
|
|
1542
1554
|
super().__init__(body_id=body_id, **kwargs)
|
|
1543
1555
|
|
|
1544
1556
|
def render_header_extra_content(self, context: Context):
|
|
1545
|
-
"""Add a "Collapse All" button to the header."""
|
|
1557
|
+
"""Add a "Collapse All Groups" button to the header."""
|
|
1546
1558
|
return format_html(
|
|
1547
1559
|
"""
|
|
1548
1560
|
<button
|
|
@@ -1552,7 +1564,7 @@ class GroupedKeyValueTablePanel(KeyValueTablePanel):
|
|
|
1552
1564
|
data-nb-toggle="collapse-all"
|
|
1553
1565
|
type="button"
|
|
1554
1566
|
>
|
|
1555
|
-
Collapse All
|
|
1567
|
+
Collapse All Groups
|
|
1556
1568
|
</button>
|
|
1557
1569
|
""",
|
|
1558
1570
|
body_id=self.body_id,
|
|
@@ -1727,7 +1739,7 @@ class StatsPanel(Panel):
|
|
|
1727
1739
|
instance = get_obj_from_context(context)
|
|
1728
1740
|
request = context["request"]
|
|
1729
1741
|
if isinstance(instance, TreeModel):
|
|
1730
|
-
self.filter_pks = (
|
|
1742
|
+
self.filter_pks = list(
|
|
1731
1743
|
instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
|
|
1732
1744
|
)
|
|
1733
1745
|
else:
|
|
@@ -1743,9 +1755,10 @@ class StatsPanel(Panel):
|
|
|
1743
1755
|
else:
|
|
1744
1756
|
related_object_model_class, query = related_field, f"{self.filter_name}__in"
|
|
1745
1757
|
filter_dict = {query: self.filter_pks}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1758
|
+
qs = related_object_model_class.objects.restrict(request.user, "view").filter(**filter_dict)
|
|
1759
|
+
if len(self.filter_pks) > 1:
|
|
1760
|
+
qs = qs.distinct()
|
|
1761
|
+
related_object_count = qs.count()
|
|
1749
1762
|
related_object_model_class_meta = related_object_model_class._meta
|
|
1750
1763
|
related_object_list_url = validated_viewname(related_object_model_class, "list")
|
|
1751
1764
|
related_object_title = bettertitle(related_object_model_class_meta.verbose_name_plural)
|
|
@@ -2153,6 +2166,72 @@ class _ObjectDetailContactsTab(Tab):
|
|
|
2153
2166
|
)
|
|
2154
2167
|
|
|
2155
2168
|
|
|
2169
|
+
class _ObjectDetailDataComplianceTab(DistinctViewTab):
|
|
2170
|
+
"""Built-in class for a Tab displaying information about data compliance."""
|
|
2171
|
+
|
|
2172
|
+
def __init__(
|
|
2173
|
+
self,
|
|
2174
|
+
*,
|
|
2175
|
+
tab_id="data_compliance",
|
|
2176
|
+
label="Data Compliance",
|
|
2177
|
+
weight=Tab.WEIGHT_DATACOMPLIANCE_TAB,
|
|
2178
|
+
panels=None,
|
|
2179
|
+
**kwargs,
|
|
2180
|
+
):
|
|
2181
|
+
if panels is None:
|
|
2182
|
+
panels = (
|
|
2183
|
+
ObjectsTablePanel(
|
|
2184
|
+
weight=100,
|
|
2185
|
+
table_class=DataComplianceTable,
|
|
2186
|
+
table_attribute="associated_data_compliance",
|
|
2187
|
+
related_field_name="object_id",
|
|
2188
|
+
label="Data Compliance",
|
|
2189
|
+
add_button_route=None,
|
|
2190
|
+
header_extra_content_template_path=None,
|
|
2191
|
+
include_paginator=True,
|
|
2192
|
+
),
|
|
2193
|
+
)
|
|
2194
|
+
super().__init__(url_name="", tab_id=tab_id, label=label, weight=weight, panels=panels, **kwargs)
|
|
2195
|
+
|
|
2196
|
+
def get_extra_context(self, context: Context):
|
|
2197
|
+
return {"url": get_obj_from_context(context).get_data_compliance_url()}
|
|
2198
|
+
|
|
2199
|
+
def should_render(self, context: Context):
|
|
2200
|
+
if not super().should_render(context):
|
|
2201
|
+
return False
|
|
2202
|
+
return getattr(get_obj_from_context(context), "is_data_compliance_model", False)
|
|
2203
|
+
|
|
2204
|
+
|
|
2205
|
+
class DynamicGroupsTextPanel(BaseTextPanel):
|
|
2206
|
+
"""Panel displaying a note about caching of dynamic groups."""
|
|
2207
|
+
|
|
2208
|
+
def __init__(
|
|
2209
|
+
self,
|
|
2210
|
+
*,
|
|
2211
|
+
weight,
|
|
2212
|
+
render_as=BaseTextPanel.RenderOptions.MARKDOWN,
|
|
2213
|
+
label="Dynamic Group caching",
|
|
2214
|
+
css_class="warning",
|
|
2215
|
+
**kwargs,
|
|
2216
|
+
):
|
|
2217
|
+
super().__init__(weight=weight, render_as=render_as, label=label, css_class=css_class, **kwargs)
|
|
2218
|
+
|
|
2219
|
+
def get_value(self, context):
|
|
2220
|
+
dg_list_url = reverse("extras:dynamicgroup_list")
|
|
2221
|
+
job_run_url = reverse(
|
|
2222
|
+
"extras:job_run_by_class_path",
|
|
2223
|
+
kwargs={"class_path": "nautobot.core.jobs.groups.RefreshDynamicGroupCaches"},
|
|
2224
|
+
)
|
|
2225
|
+
return (
|
|
2226
|
+
"Dynamic group membership is cached for performance reasons, "
|
|
2227
|
+
"therefore this page may not always be up-to-date.\n\n"
|
|
2228
|
+
"You can refresh the membership of any specific group by accessing it from the list below or from the "
|
|
2229
|
+
f'[Dynamic Groups list view]({dg_list_url}) and clicking the "Refresh Members" button.\n\n'
|
|
2230
|
+
"You can also refresh the membership of **all** groups by running the "
|
|
2231
|
+
f"[Refresh Dynamic Group Caches job]({job_run_url})."
|
|
2232
|
+
)
|
|
2233
|
+
|
|
2234
|
+
|
|
2156
2235
|
@dataclass
|
|
2157
2236
|
class _ObjectDetailGroupsTab(Tab):
|
|
2158
2237
|
"""Built-in class for a Tab displaying information about associated dynamic groups."""
|
|
@@ -2169,8 +2248,9 @@ class _ObjectDetailGroupsTab(Tab):
|
|
|
2169
2248
|
):
|
|
2170
2249
|
if panels is None:
|
|
2171
2250
|
panels = (
|
|
2251
|
+
DynamicGroupsTextPanel(weight=100),
|
|
2172
2252
|
ObjectsTablePanel(
|
|
2173
|
-
weight=
|
|
2253
|
+
weight=200,
|
|
2174
2254
|
table_class=DynamicGroupTable,
|
|
2175
2255
|
table_attribute="dynamic_groups",
|
|
2176
2256
|
exclude_columns=["content_type"],
|
nautobot/core/urls.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from django.conf import settings
|
|
2
2
|
from django.http import HttpResponse, HttpResponseNotFound
|
|
3
3
|
from django.urls import include, path
|
|
4
|
-
from django.views.generic import TemplateView
|
|
4
|
+
from django.views.generic import RedirectView, TemplateView
|
|
5
5
|
|
|
6
6
|
from nautobot.core.views import (
|
|
7
7
|
AboutView,
|
|
8
|
+
AppDocsView,
|
|
8
9
|
CustomGraphQLView,
|
|
9
10
|
get_file_with_authorization,
|
|
10
11
|
HomeView,
|
|
@@ -46,6 +47,7 @@ urlpatterns = [
|
|
|
46
47
|
path("user/", include("nautobot.users.urls")),
|
|
47
48
|
path("users/", include("nautobot.users.urls", "users")),
|
|
48
49
|
path("virtualization/", include("nautobot.virtualization.urls")),
|
|
50
|
+
path("vpn/", include("nautobot.vpn.urls")),
|
|
49
51
|
path("wireless/", include("nautobot.wireless.urls")),
|
|
50
52
|
# API
|
|
51
53
|
path("api/", include("nautobot.core.api.urls")),
|
|
@@ -59,6 +61,15 @@ urlpatterns = [
|
|
|
59
61
|
path("media-failure/", StaticMediaFailureView.as_view(), name="media_failure"),
|
|
60
62
|
# Apps
|
|
61
63
|
path("apps/", include((apps_patterns, "apps"))),
|
|
64
|
+
# Redirect /docs/<app_base_url>/ -> /docs/<app_base_url>/index.html
|
|
65
|
+
path(
|
|
66
|
+
"docs/<str:app_base_url>/",
|
|
67
|
+
RedirectView.as_view(pattern_name="docs_file", permanent=False),
|
|
68
|
+
kwargs={"path": "index.html"},
|
|
69
|
+
name="docs_index_redirect",
|
|
70
|
+
),
|
|
71
|
+
# Apps docs - Serve docs file
|
|
72
|
+
path("docs/<str:app_base_url>/<path:path>", AppDocsView.as_view(), name="docs_file"),
|
|
62
73
|
path("plugins/", include((plugin_patterns, "plugins"))),
|
|
63
74
|
path("admin/plugins/", include(plugin_admin_patterns)),
|
|
64
75
|
# Social auth/SSO
|
nautobot/core/utils/cache.py
CHANGED
|
@@ -66,5 +66,6 @@ def construct_cache_key(obj, *, method_name=None, branch_aware=True, **params):
|
|
|
66
66
|
if params_tokens:
|
|
67
67
|
cache_key += f"({','.join(params_tokens)})"
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
# Disabled as it's very noisy in some cases
|
|
70
|
+
# logger.debug("Constructed cache key is %s", cache_key)
|
|
70
71
|
return cache_key
|
nautobot/core/utils/filtering.py
CHANGED
|
@@ -17,6 +17,21 @@ from nautobot.core.utils.lookup import get_filterset_for_model
|
|
|
17
17
|
# e.g `name__ic` has lookup expr `ic (icontains)` while `name` has no lookup expr
|
|
18
18
|
CONTAINS_LOOKUP_EXPR_RE = re.compile(r"(?<=__)\w+")
|
|
19
19
|
|
|
20
|
+
MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING = {
|
|
21
|
+
"approval_workflow_definitions": "approval_workflows",
|
|
22
|
+
"cables": "cable_terminations",
|
|
23
|
+
"data_compliance": "custom_validators",
|
|
24
|
+
"location_types": "locations",
|
|
25
|
+
"metadata_types": "metadata",
|
|
26
|
+
"min_max_validation_rules": "custom_validators",
|
|
27
|
+
"object_metadata": "metadata",
|
|
28
|
+
"regular_expression_validation_rules": "custom_validators",
|
|
29
|
+
"relationship_associations": "relationships",
|
|
30
|
+
"required_validation_rules": "custom_validators",
|
|
31
|
+
"static_group_associations": "dynamic_groups",
|
|
32
|
+
"unique_validation_rules": "custom_validators",
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
|
|
21
36
|
def build_lookup_label(field_name, _verbose_name):
|
|
22
37
|
"""
|
|
@@ -131,26 +146,11 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
|
|
|
131
146
|
elif isinstance(
|
|
132
147
|
field, ContentTypeMultipleChoiceFilter
|
|
133
148
|
): # While there are other objects using `ContentTypeMultipleChoiceFilter`, the case where
|
|
134
|
-
# models that have such a filter and the `verbose_name_plural`
|
|
149
|
+
# models that have such a filter and the `verbose_name_plural` does not match, we can lookup the feature name.
|
|
135
150
|
from nautobot.core.models.fields import slugify_dashes_to_underscores # Avoid circular import
|
|
136
151
|
|
|
137
152
|
plural_name = slugify_dashes_to_underscores(model._meta.verbose_name_plural)
|
|
138
|
-
|
|
139
|
-
# Cable-connectable models use "cable_terminations", not "cables", as the feature name
|
|
140
|
-
if plural_name == "cables":
|
|
141
|
-
plural_name = "cable_terminations"
|
|
142
|
-
elif plural_name == "metadata_types":
|
|
143
|
-
plural_name = "metadata"
|
|
144
|
-
elif plural_name == "object_metadata":
|
|
145
|
-
plural_name = "metadata"
|
|
146
|
-
elif plural_name in [
|
|
147
|
-
"data_compliance",
|
|
148
|
-
"min_max_validation_rules",
|
|
149
|
-
"regular_expression_validation_rules",
|
|
150
|
-
"required_validation_rules",
|
|
151
|
-
"unique_validation_rules",
|
|
152
|
-
]:
|
|
153
|
-
plural_name = "custom_validators"
|
|
153
|
+
plural_name = MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING.get(plural_name, plural_name)
|
|
154
154
|
try:
|
|
155
155
|
form_field = MultipleContentTypeField(choices_as_strings=True, feature=plural_name)
|
|
156
156
|
except KeyError:
|
nautobot/core/utils/lookup.py
CHANGED
|
@@ -11,9 +11,10 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
11
11
|
from django.core.exceptions import ObjectDoesNotExist
|
|
12
12
|
from django.db.models import ForeignKey, Model
|
|
13
13
|
from django.urls import get_resolver, resolve, reverse, URLPattern, URLResolver
|
|
14
|
-
from django.utils.module_loading import import_string
|
|
15
14
|
from django.views.generic.base import RedirectView
|
|
16
15
|
|
|
16
|
+
from nautobot.core.utils.module_loading import import_string_optional
|
|
17
|
+
|
|
17
18
|
|
|
18
19
|
def resolve_attr(obj, dotted_field):
|
|
19
20
|
"""
|
|
@@ -181,13 +182,7 @@ def get_related_class_for_model(model, module_name, object_suffix):
|
|
|
181
182
|
object_name = f"{model.__name__}{object_suffix}"
|
|
182
183
|
object_path = f"{app_config.name}.{module_name}.{object_name}"
|
|
183
184
|
|
|
184
|
-
|
|
185
|
-
return import_string(object_path)
|
|
186
|
-
# The name of the module is not correct or unable to find the desired object for this model
|
|
187
|
-
except (AttributeError, ImportError, ModuleNotFoundError):
|
|
188
|
-
pass
|
|
189
|
-
|
|
190
|
-
return None
|
|
185
|
+
return import_string_optional(object_path)
|
|
191
186
|
|
|
192
187
|
|
|
193
188
|
def get_filterset_for_model(model):
|
|
@@ -7,9 +7,30 @@ import os
|
|
|
7
7
|
import pkgutil
|
|
8
8
|
import sys
|
|
9
9
|
|
|
10
|
+
from django.utils.module_loading import import_string
|
|
11
|
+
|
|
10
12
|
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
12
14
|
|
|
15
|
+
def import_string_optional(dotted_path):
|
|
16
|
+
"""An extension/wrapper of Django's `import_string()` that returns `None` if no such dotted path exists."""
|
|
17
|
+
try:
|
|
18
|
+
return import_string(dotted_path)
|
|
19
|
+
except ModuleNotFoundError as err:
|
|
20
|
+
# No such module
|
|
21
|
+
module_name, _ = dotted_path.rsplit(".", 1)
|
|
22
|
+
if module_name.startswith(err.name): # tried to import foo.bar.baz but couldn't find foo.bar, etc.
|
|
23
|
+
return None
|
|
24
|
+
# Some import *from within* the given module couldn't find what it was looking for?
|
|
25
|
+
raise
|
|
26
|
+
except ImportError as err:
|
|
27
|
+
if "does not define" in str(err):
|
|
28
|
+
# Exception raised by Django if the module exists but has no such attribute
|
|
29
|
+
return None
|
|
30
|
+
# Maybe a legitimate problem with the import?
|
|
31
|
+
raise
|
|
32
|
+
|
|
33
|
+
|
|
13
34
|
@contextmanager
|
|
14
35
|
def _temporarily_add_to_sys_path(path):
|
|
15
36
|
"""
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
2
|
+
from django.db import router, transaction
|
|
3
|
+
from django.db.utils import IntegrityError
|
|
4
|
+
from social_core.exceptions import AuthAlreadyAssociated
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Social Auth Account Takeover Vulnerability Patch
|
|
8
|
+
=================================================
|
|
9
|
+
|
|
10
|
+
This module patches CVE-2025-61783, a medium security vulnerability in social_django that allows
|
|
11
|
+
account takeover when using OAuth providers that don't verify email addresses.
|
|
12
|
+
|
|
13
|
+
VULNERABILITY OVERVIEW
|
|
14
|
+
----------------------
|
|
15
|
+
The vulnerability exists in social_django.storage.DjangoUserMixin.create_user(),
|
|
16
|
+
specifically in how it handles IntegrityError exceptions. When user creation fails due to
|
|
17
|
+
a duplicate email or username, the original code catches the IntegrityError and blindly
|
|
18
|
+
retrieves an existing user via manager.get(), returning that user without verifying that
|
|
19
|
+
a social auth association exists for the provider/UID combination.
|
|
20
|
+
|
|
21
|
+
PATCHING STRATEGY
|
|
22
|
+
-----------------
|
|
23
|
+
This implementation patches the `create_user` method on the `user` class property of
|
|
24
|
+
DjangoStorage, which is where the vulnerability manifests. The patch changes the behavior
|
|
25
|
+
to raise AuthAlreadyAssociated when an IntegrityError occurs, preventing the silent
|
|
26
|
+
return of an existing user.
|
|
27
|
+
|
|
28
|
+
By patching at this level, we:
|
|
29
|
+
- Maintain compatibility with custom pipelines
|
|
30
|
+
- Don't require changes to user's social auth configuration
|
|
31
|
+
- Apply the fix exactly where the vulnerability occurs
|
|
32
|
+
- Preserve all other social auth functionality
|
|
33
|
+
|
|
34
|
+
REMOVAL
|
|
35
|
+
-------
|
|
36
|
+
Remove this patch when upgrading to social-auth-app-django >= 5.6.0
|
|
37
|
+
(version that includes PR #803 merged into the main branch).
|
|
38
|
+
|
|
39
|
+
To verify if you still need the patch:
|
|
40
|
+
pip show social-auth-app-django
|
|
41
|
+
# Check version against PR #803 merge status
|
|
42
|
+
|
|
43
|
+
REFERENCES
|
|
44
|
+
----------
|
|
45
|
+
- Vulnerability Report: https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg
|
|
46
|
+
- Original Issue: https://github.com/python-social-auth/social-app-django/issues/220
|
|
47
|
+
- Official Fix PR: https://github.com/python-social-auth/social-app-django/pull/803
|
|
48
|
+
|
|
49
|
+
SECURITY NOTICE
|
|
50
|
+
---------------
|
|
51
|
+
This patch addresses a MEDIUM security vulnerability
|
|
52
|
+
|
|
53
|
+
Disabling this patch without mitigation will expose your application to account
|
|
54
|
+
takeover attacks.
|
|
55
|
+
|
|
56
|
+
AUTHOR & MAINTENANCE
|
|
57
|
+
--------------------
|
|
58
|
+
Patch implemented as temporary security measure for Nautobot deployment.
|
|
59
|
+
As picking up the latest social_django would require a major version upgrade of Django,
|
|
60
|
+
which itself would require a breaking change to the Nautobot configuration, this patch
|
|
61
|
+
is intended to be a stopgap until such time as Nautobot can upgrade to a version of
|
|
62
|
+
social_django that includes the fix.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def patch_django_storage(original_django_storage):
|
|
67
|
+
"""
|
|
68
|
+
Apply security patch to DjangoStorage.user.create_user method.
|
|
69
|
+
|
|
70
|
+
This patches the vulnerability in python-social-auth where create_user
|
|
71
|
+
catches IntegrityError and blindly returns an existing user, enabling
|
|
72
|
+
account takeover via unverified OAuth providers.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
storage_class (DjangoStorage): The original DjangoStorage class to patch.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
None
|
|
79
|
+
|
|
80
|
+
Note:
|
|
81
|
+
The patch is a nearly verbatim copy of the original create_user method
|
|
82
|
+
from social_django.storage.DjangoUserMixin from 5.4.3, except that it
|
|
83
|
+
adopts the fail-closed change described in
|
|
84
|
+
https://github.com/python-social-auth/social-app-django/pull/803
|
|
85
|
+
|
|
86
|
+
The modified lines are called out with "Patched logic" comments below.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def patched_create_user(cls, *args, **kwargs):
|
|
90
|
+
username_field = cls.username_field()
|
|
91
|
+
if "username" in kwargs:
|
|
92
|
+
if username_field not in kwargs:
|
|
93
|
+
kwargs[username_field] = kwargs.pop("username")
|
|
94
|
+
else:
|
|
95
|
+
# If username_field is 'email' and there is no field named "username"
|
|
96
|
+
# then latest should be removed from kwargs.
|
|
97
|
+
try:
|
|
98
|
+
cls.user_model()._meta.get_field("username")
|
|
99
|
+
except FieldDoesNotExist:
|
|
100
|
+
kwargs.pop("username")
|
|
101
|
+
try:
|
|
102
|
+
if hasattr(transaction, "atomic"):
|
|
103
|
+
# In Django versions that have an "atomic" transaction decorator / context
|
|
104
|
+
# manager, there's a transaction wrapped around this call.
|
|
105
|
+
# If the create fails below due to an IntegrityError, ensure that the transaction
|
|
106
|
+
# stays undamaged by wrapping the create in an atomic.
|
|
107
|
+
using = router.db_for_write(cls.user_model())
|
|
108
|
+
with transaction.atomic(using=using):
|
|
109
|
+
user = cls.user_model()._default_manager.create_user(*args, **kwargs)
|
|
110
|
+
else:
|
|
111
|
+
user = cls.user_model()._default_manager.create_user(*args, **kwargs)
|
|
112
|
+
except IntegrityError as exc:
|
|
113
|
+
# ORIGINAL CODE BELOW:
|
|
114
|
+
# # If email comes in as None it won't get found in the get
|
|
115
|
+
# if kwargs.get("email", True) is None:
|
|
116
|
+
# kwargs["email"] = ""
|
|
117
|
+
# try:
|
|
118
|
+
# user = cls.user_model()._default_manager.get(*args, **kwargs)
|
|
119
|
+
# except cls.user_model().DoesNotExist:
|
|
120
|
+
# raise exc
|
|
121
|
+
|
|
122
|
+
# BEGIN Patched logic
|
|
123
|
+
raise AuthAlreadyAssociated(None) from exc
|
|
124
|
+
# END Patched logic
|
|
125
|
+
return user
|
|
126
|
+
|
|
127
|
+
# Apply the patch to the original DjangoStorage.user.create_user method
|
|
128
|
+
original_django_storage.user.create_user = classmethod(patched_create_user)
|
nautobot/core/views/__init__.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import datetime
|
|
3
|
+
from importlib import resources
|
|
3
4
|
import logging
|
|
5
|
+
import mimetypes
|
|
4
6
|
import os
|
|
5
7
|
import platform
|
|
6
8
|
import posixpath
|
|
@@ -16,7 +18,7 @@ from django.contrib.auth.decorators import permission_required
|
|
|
16
18
|
from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin, UserPassesTestMixin
|
|
17
19
|
from django.contrib.contenttypes.models import ContentType
|
|
18
20
|
from django.core.cache import cache
|
|
19
|
-
from django.http import HttpResponseForbidden, HttpResponseServerError, JsonResponse
|
|
21
|
+
from django.http import FileResponse, HttpResponseForbidden, HttpResponseServerError, JsonResponse
|
|
20
22
|
from django.shortcuts import get_object_or_404, render
|
|
21
23
|
from django.template import loader, RequestContext, Template
|
|
22
24
|
from django.template.exceptions import TemplateDoesNotExist
|
|
@@ -61,6 +63,7 @@ from nautobot.core.views.utils import (
|
|
|
61
63
|
)
|
|
62
64
|
from nautobot.extras.forms import GraphQLQueryForm
|
|
63
65
|
from nautobot.extras.models import FileProxy, GraphQLQuery, Status
|
|
66
|
+
from nautobot.extras.plugins.urls import BASE_URL_TO_APP_LABEL
|
|
64
67
|
from nautobot.extras.registry import registry
|
|
65
68
|
from nautobot.extras.tables import StatusTable
|
|
66
69
|
|
|
@@ -144,6 +147,40 @@ class HomeView(AccessMixin, TemplateView):
|
|
|
144
147
|
return self.render_to_response(context)
|
|
145
148
|
|
|
146
149
|
|
|
150
|
+
class AppDocsView(LoginRequiredMixin, View):
|
|
151
|
+
"""
|
|
152
|
+
Serve documentation files for any pip-installed app from inside the package,
|
|
153
|
+
only for authenticated users.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def get(self, request, app_base_url, path="index.html"):
|
|
157
|
+
app_label = BASE_URL_TO_APP_LABEL.get(app_base_url)
|
|
158
|
+
if not app_label:
|
|
159
|
+
return JsonResponse({"detail": f"Unknown base_url '{app_base_url}'."}, status=404)
|
|
160
|
+
try:
|
|
161
|
+
base_dir = resources.files(app_label)
|
|
162
|
+
except ModuleNotFoundError:
|
|
163
|
+
return JsonResponse({"detail": f"App {app_label} not found."}, status=404)
|
|
164
|
+
|
|
165
|
+
# Dir to documentation inside the package
|
|
166
|
+
docs_dir = base_dir / "docs"
|
|
167
|
+
# Normalize path to avoid (../) etc.
|
|
168
|
+
normalized_path = posixpath.normpath(path).lstrip("/")
|
|
169
|
+
file_path = docs_dir / normalized_path
|
|
170
|
+
|
|
171
|
+
# Additional check to ensure the resolved path is still within docs_dir
|
|
172
|
+
if not file_path.resolve().is_relative_to(docs_dir.resolve()):
|
|
173
|
+
return JsonResponse({"detail": "Access denied."}, status=403)
|
|
174
|
+
|
|
175
|
+
if not file_path.is_file():
|
|
176
|
+
return JsonResponse({"detail": f"File {file_path} not found."}, status=404)
|
|
177
|
+
|
|
178
|
+
# Determine the MIME type based on the file extension and return the file as an HTTP response.
|
|
179
|
+
# This ensures that browsers interpret the file correctly (e.g., HTML, CSS, JS, images).
|
|
180
|
+
content_type, _ = mimetypes.guess_type(str(file_path))
|
|
181
|
+
return FileResponse(open(file_path, "rb"), content_type=content_type)
|
|
182
|
+
|
|
183
|
+
|
|
147
184
|
class MediaView(AccessMixin, View):
|
|
148
185
|
"""
|
|
149
186
|
Serves media files while enforcing login restrictions.
|
nautobot/core/views/generic.py
CHANGED
|
@@ -587,7 +587,7 @@ class ObjectDeleteView(UIComponentsMixin, GetReturnURLMixin, ObjectPermissionReq
|
|
|
587
587
|
"""
|
|
588
588
|
|
|
589
589
|
queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
590
|
-
template_name = "generic/
|
|
590
|
+
template_name = "generic/object_destroy.html"
|
|
591
591
|
|
|
592
592
|
def get_required_permission(self):
|
|
593
593
|
return get_permission_for_model(self.queryset.model, "delete")
|
|
@@ -1044,7 +1044,7 @@ class BulkEditView(
|
|
|
1044
1044
|
filterset: Optional[type[FilterSet]] = None
|
|
1045
1045
|
table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1046
1046
|
form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1047
|
-
template_name = "generic/
|
|
1047
|
+
template_name = "generic/object_bulk_update.html"
|
|
1048
1048
|
|
|
1049
1049
|
def get_required_permission(self):
|
|
1050
1050
|
return get_permission_for_model(self.queryset.model, "change")
|
|
@@ -1246,7 +1246,7 @@ class BulkDeleteView(
|
|
|
1246
1246
|
filterset: Optional[type[FilterSet]] = None
|
|
1247
1247
|
table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
|
|
1248
1248
|
form: Optional[type[Form]] = None
|
|
1249
|
-
template_name = "generic/
|
|
1249
|
+
template_name = "generic/object_bulk_destroy.html"
|
|
1250
1250
|
|
|
1251
1251
|
def get_required_permission(self):
|
|
1252
1252
|
return get_permission_for_model(self.queryset.model, "delete")
|
nautobot/core/views/mixins.py
CHANGED
|
@@ -73,6 +73,7 @@ PERMISSIONS_ACTION_MAP = {
|
|
|
73
73
|
"bulk_update": "change",
|
|
74
74
|
"changelog": "view",
|
|
75
75
|
"notes": "view",
|
|
76
|
+
"data_compliance": "view",
|
|
76
77
|
"approve": "change",
|
|
77
78
|
"deny": "change",
|
|
78
79
|
}
|
|
@@ -532,7 +533,8 @@ class NautobotViewSetMixin(GenericViewSet, UIComponentsMixin, AccessMixin, GetRe
|
|
|
532
533
|
form.add_error(None, msg)
|
|
533
534
|
return form
|
|
534
535
|
|
|
535
|
-
def _handle_not_implemented_error(self):
|
|
536
|
+
def _handle_not_implemented_error(self, error):
|
|
537
|
+
self.logger.debug(f"NotImplementedError raised on action {self.action} resulting in error: {error}")
|
|
536
538
|
# Blanket handler for NotImplementedError raised by form helper functions
|
|
537
539
|
msg = "Please provide the appropriate mixin before using this helper function"
|
|
538
540
|
messages.error(self.request, msg)
|
|
@@ -567,8 +569,8 @@ class NautobotViewSetMixin(GenericViewSet, UIComponentsMixin, AccessMixin, GetRe
|
|
|
567
569
|
self._handle_validation_error(e)
|
|
568
570
|
except ObjectDoesNotExist:
|
|
569
571
|
form = self._handle_object_does_not_exist(form)
|
|
570
|
-
except NotImplementedError:
|
|
571
|
-
self._handle_not_implemented_error()
|
|
572
|
+
except NotImplementedError as error:
|
|
573
|
+
self._handle_not_implemented_error(error)
|
|
572
574
|
|
|
573
575
|
if not self.has_error:
|
|
574
576
|
self.logger.debug("Form validation was successful")
|
|
@@ -1527,3 +1529,13 @@ class ObjectNotesViewMixin(NautobotViewSetMixin):
|
|
|
1527
1529
|
"active_tab": "notes",
|
|
1528
1530
|
}
|
|
1529
1531
|
return Response(data)
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
class ObjectDataComplianceViewMixin(NautobotViewSetMixin):
|
|
1535
|
+
"""
|
|
1536
|
+
UI Mixin for a DataCompliance to show up for a given object.
|
|
1537
|
+
"""
|
|
1538
|
+
|
|
1539
|
+
@drf_action(detail=True)
|
|
1540
|
+
def data_compliance(self, request, *args, **kwargs):
|
|
1541
|
+
return Response({})
|
nautobot/core/views/renderers.py
CHANGED
|
@@ -365,6 +365,8 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
|
|
|
365
365
|
# See form_valid() for self.action == "bulk_create".
|
|
366
366
|
self.template = data.get("template", view.get_template_name())
|
|
367
367
|
|
|
368
|
+
data["request"] = request
|
|
369
|
+
|
|
368
370
|
return super().render(data, accepted_media_type=accepted_media_type, renderer_context=renderer_context)
|
|
369
371
|
|
|
370
372
|
@staticmethod
|
nautobot/core/views/viewsets.py
CHANGED
|
@@ -11,9 +11,10 @@ class NautobotUIViewSet(
|
|
|
11
11
|
mixins.ObjectBulkUpdateViewMixin,
|
|
12
12
|
mixins.ObjectChangeLogViewMixin,
|
|
13
13
|
mixins.ObjectNotesViewMixin,
|
|
14
|
+
mixins.ObjectDataComplianceViewMixin,
|
|
14
15
|
):
|
|
15
16
|
"""
|
|
16
17
|
Nautobot BaseViewSet that is intended for UI use only. It provides default Nautobot functionalities such as
|
|
17
18
|
`create()`, `update()`, `partial_update()`, `bulk_update()`, `destroy()`, `bulk_destroy()`, `retrieve()`
|
|
18
|
-
`notes()`, `changelog()` and `
|
|
19
|
+
`notes()`, `changelog()`, `list()`, and `data_compliance()` actions.
|
|
19
20
|
"""
|