nautobot 2.0.5__py3-none-any.whl → 2.1.0b1__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/circuits/navigation.py +0 -25
- nautobot/circuits/templates/circuits/circuit_retrieve.html +0 -9
- nautobot/circuits/templates/circuits/providernetwork_retrieve.html +0 -2
- nautobot/circuits/tests/test_filters.py +1 -0
- nautobot/core/api/serializers.py +15 -5
- nautobot/core/api/views.py +18 -19
- nautobot/core/choices.py +1 -1
- nautobot/core/filters.py +12 -4
- nautobot/core/jobs/__init__.py +125 -3
- nautobot/core/management/commands/generate_test_data.py +4 -1
- nautobot/core/models/fields.py +12 -2
- nautobot/core/settings.py +8 -7
- nautobot/core/templates/base_django.html +2 -2
- nautobot/core/templates/buttons/export.html +57 -30
- nautobot/core/templates/generic/object_list.html +2 -2
- nautobot/core/templates/generic/object_retrieve.html +8 -1
- nautobot/core/templates/home.html +5 -5
- nautobot/core/templates/inc/created_updated.html +2 -2
- nautobot/core/templates/inc/footer.html +2 -2
- nautobot/core/templates/inc/javascript.html +0 -10
- nautobot/core/templates/inc/media.html +2 -0
- nautobot/core/templates/inc/nav_menu.html +66 -68
- nautobot/core/templates/inc/object_details_advanced_panel.html +19 -0
- nautobot/core/templates/nautobot_config.py.j2 +10 -4
- nautobot/core/templates/panel_table.html +1 -1
- nautobot/core/templates/template.css +89 -0
- nautobot/core/templates/utilities/templatetags/table_config_form.html +1 -0
- nautobot/core/templatetags/buttons.py +7 -2
- nautobot/core/testing/views.py +34 -4
- nautobot/core/tests/integration/test_home.py +1 -43
- nautobot/core/tests/integration/test_navbar.py +10 -64
- nautobot/core/tests/integration/test_plugin_home.py +4 -5
- nautobot/core/tests/integration/test_plugin_navbar.py +20 -16
- nautobot/core/tests/integration/test_theme.py +4 -0
- nautobot/core/tests/test_api.py +14 -66
- nautobot/core/tests/test_filters.py +127 -0
- nautobot/core/tests/test_forms.py +1 -1
- nautobot/core/tests/test_graphql.py +165 -2
- nautobot/core/tests/test_jobs.py +112 -0
- nautobot/core/tests/test_openapi.py +6 -0
- nautobot/core/tests/test_views.py +11 -85
- nautobot/core/urls.py +6 -1
- nautobot/core/utils/lookup.py +28 -0
- nautobot/core/utils/requests.py +2 -3
- nautobot/core/views/__init__.py +3 -4
- nautobot/core/views/generic.py +9 -4
- nautobot/core/views/mixins.py +4 -2
- nautobot/core/views/renderers.py +5 -0
- nautobot/dcim/models/device_components.py +1 -0
- nautobot/dcim/navigation.py +10 -165
- nautobot/dcim/templates/dcim/location.html +1 -1
- nautobot/dcim/tests/features/locations.feature +143 -0
- nautobot/dcim/tests/test_api.py +1 -1
- nautobot/dcim/tests/test_filters.py +11 -3
- nautobot/extras/admin.py +1 -1
- nautobot/extras/api/serializers.py +33 -0
- nautobot/extras/api/urls.py +6 -0
- nautobot/extras/api/views.py +45 -6
- nautobot/extras/factory.py +28 -2
- nautobot/extras/filters/__init__.py +52 -0
- nautobot/extras/filters/mixins.py +4 -29
- nautobot/extras/forms/forms.py +43 -0
- nautobot/extras/jobs.py +31 -9
- nautobot/extras/migrations/0100_fileproxy_job_result.py +32 -0
- nautobot/extras/migrations/0101_externalintegration.py +61 -0
- nautobot/extras/migrations/0102_set_null_objectchange_contenttype.py +32 -0
- nautobot/extras/models/__init__.py +2 -0
- nautobot/extras/models/change_logging.py +2 -2
- nautobot/extras/models/models.py +96 -16
- nautobot/extras/navigation.py +17 -29
- nautobot/extras/signals.py +15 -0
- nautobot/extras/tables.py +27 -0
- nautobot/extras/templates/extras/externalintegration_retrieve.html +37 -0
- nautobot/extras/templates/extras/inc/jobresult.html +24 -0
- nautobot/extras/templates/extras/jobresult.html +24 -0
- nautobot/extras/test_jobs/file_output.py +16 -0
- nautobot/extras/tests/test_api.py +92 -0
- nautobot/extras/tests/test_filters.py +64 -2
- nautobot/extras/tests/test_jobs.py +39 -0
- nautobot/extras/tests/test_models.py +34 -0
- nautobot/extras/tests/test_views.py +22 -2
- nautobot/extras/urls.py +1 -0
- nautobot/extras/views.py +15 -0
- nautobot/ipam/forms.py +16 -0
- nautobot/ipam/models.py +3 -0
- nautobot/ipam/navigation.py +2 -59
- nautobot/ipam/templates/ipam/ipaddress.html +0 -9
- nautobot/ipam/templates/ipam/prefix.html +0 -9
- nautobot/ipam/tests/features/prefixes.feature +134 -0
- nautobot/ipam/tests/test_filters.py +5 -10
- nautobot/ipam/tests/test_views.py +8 -1
- nautobot/ipam/views.py +3 -0
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css +191 -191
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.css.map +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap-theme.min.css.map +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css +874 -881
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.css.map +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css +1 -1
- nautobot/project-static/bootstrap-3.4.1-dist/css/bootstrap.min.css.map +1 -1
- nautobot/project-static/css/base.css +135 -99
- nautobot/project-static/css/dark.css +65 -6
- nautobot/project-static/docs/404.html +44 -16
- nautobot/project-static/docs/apps/index.html +44 -16
- nautobot/project-static/docs/apps/nautobot-apps.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1597 -1457
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +45 -17
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +353 -432
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +66 -38
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2154 -2307
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +807 -691
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +58 -30
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +3600 -3456
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +44 -16
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +101 -75
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +3083 -3019
- nautobot/project-static/docs/development/apps/api/configuration-view.html +44 -16
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +44 -16
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +44 -16
- nautobot/project-static/docs/development/apps/api/models/global-search.html +44 -16
- nautobot/project-static/docs/development/apps/api/models/graphql.html +44 -16
- nautobot/project-static/docs/development/apps/api/models/index.html +44 -16
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +44 -16
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +44 -16
- nautobot/project-static/docs/development/apps/api/prometheus.html +44 -16
- nautobot/project-static/docs/development/apps/api/setup.html +44 -16
- nautobot/project-static/docs/development/apps/api/testing.html +44 -16
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +44 -16
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +44 -16
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +44 -16
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +44 -16
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-detail-views.html +44 -16
- nautobot/project-static/docs/development/apps/api/ui-extensions/tabs.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/base-template.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/index.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/notes.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/urls.html +44 -16
- nautobot/project-static/docs/development/apps/api/views/view-overrides.html +44 -16
- nautobot/project-static/docs/development/apps/index.html +44 -16
- nautobot/project-static/docs/development/apps/migration/code-updates.html +44 -16
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +44 -16
- nautobot/project-static/docs/development/apps/migration/from-v1.html +44 -16
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +44 -16
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +44 -16
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +44 -16
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +44 -16
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +44 -16
- nautobot/project-static/docs/development/core/application-registry.html +44 -16
- nautobot/project-static/docs/development/core/best-practices.html +44 -16
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +44 -16
- nautobot/project-static/docs/development/core/extending-models.html +44 -16
- nautobot/project-static/docs/development/core/generic-views.html +44 -16
- nautobot/project-static/docs/development/core/getting-started.html +44 -16
- nautobot/project-static/docs/development/core/homepage.html +44 -16
- nautobot/project-static/docs/development/core/index.html +44 -16
- nautobot/project-static/docs/development/core/model-features.html +44 -16
- nautobot/project-static/docs/development/core/natural-keys.html +44 -16
- nautobot/project-static/docs/development/core/navigation-menu.html +44 -21
- nautobot/project-static/docs/development/core/react-ui.html +44 -16
- nautobot/project-static/docs/development/core/release-checklist.html +44 -16
- nautobot/project-static/docs/development/core/role-internals.html +44 -16
- nautobot/project-static/docs/development/core/style-guide.html +44 -16
- nautobot/project-static/docs/development/core/templates.html +44 -16
- nautobot/project-static/docs/development/core/testing.html +44 -16
- nautobot/project-static/docs/development/core/user-preferences.html +44 -16
- nautobot/project-static/docs/development/index.html +44 -16
- nautobot/project-static/docs/development/jobs/index.html +280 -234
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +44 -16
- nautobot/project-static/docs/index.html +44 -16
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/index.html +47 -19
- nautobot/project-static/docs/release-notes/version-1.0.html +44 -16
- nautobot/project-static/docs/release-notes/version-1.1.html +44 -16
- nautobot/project-static/docs/release-notes/version-1.2.html +44 -16
- nautobot/project-static/docs/release-notes/version-1.3.html +44 -16
- nautobot/project-static/docs/release-notes/version-1.4.html +44 -16
- nautobot/project-static/docs/release-notes/version-1.5.html +44 -16
- nautobot/project-static/docs/release-notes/version-1.6.html +44 -16
- nautobot/project-static/docs/release-notes/version-2.0.html +47 -19
- nautobot/project-static/docs/release-notes/version-2.1.html +5724 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +247 -237
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +44 -16
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +44 -16
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +44 -16
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +44 -16
- nautobot/project-static/docs/user-guide/administration/configuration/node-configuration.html +44 -16
- nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +109 -43
- nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +44 -16
- nautobot/project-static/docs/user-guide/administration/guides/caching.html +44 -16
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +44 -16
- nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +44 -16
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +44 -16
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +44 -16
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +44 -16
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +48 -19
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +44 -16
- nautobot/project-static/docs/user-guide/administration/installation/docker.html +44 -16
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +44 -16
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +44 -16
- nautobot/project-static/docs/user-guide/administration/installation/index.html +44 -16
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +44 -16
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +44 -16
- nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +44 -16
- nautobot/project-static/docs/user-guide/administration/installation/services.html +44 -16
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +44 -16
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +44 -16
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +44 -16
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +44 -16
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +44 -16
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +44 -16
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +44 -16
- nautobot/project-static/docs/user-guide/index.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +110 -16
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +47 -19
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +5359 -0
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +47 -19
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +113 -44
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +44 -16
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +44 -16
- nautobot/project-static/fonts/UFL.txt +96 -0
- nautobot/project-static/fonts/Ubuntu-Bold.woff2 +0 -0
- nautobot/project-static/fonts/Ubuntu-BoldItalic.woff2 +0 -0
- nautobot/project-static/fonts/Ubuntu-Italic.woff2 +0 -0
- nautobot/project-static/fonts/Ubuntu-Medium.woff2 +0 -0
- nautobot/project-static/fonts/Ubuntu-MediumItalic.woff2 +0 -0
- nautobot/project-static/fonts/Ubuntu-Regular.woff2 +0 -0
- nautobot/project-static/fonts/UbuntuMono-Bold.woff2 +0 -0
- nautobot/project-static/fonts/UbuntuMono-BoldItalic.woff2 +0 -0
- nautobot/project-static/fonts/UbuntuMono-Italic.woff2 +0 -0
- nautobot/project-static/fonts/UbuntuMono-Regular.woff2 +0 -0
- nautobot/project-static/img/dark-theme.png +0 -0
- nautobot/project-static/img/light-theme.png +0 -0
- nautobot/project-static/img/nautobot_chevron.svg +5 -0
- nautobot/project-static/img/nautobot_chevron_header.svg +5 -0
- nautobot/project-static/img/system-theme.png +0 -0
- nautobot/tenancy/navigation.py +0 -13
- nautobot/ui/package-lock.json +2 -2
- nautobot/ui/package.json +1 -1
- nautobot/users/admin.py +44 -0
- nautobot/users/migrations/0007_alter_objectpermission_object_types.py +33 -0
- nautobot/users/models.py +3 -2
- nautobot/virtualization/navigation.py +1 -33
- nautobot/virtualization/tests/test_api.py +1 -1
- nautobot/virtualization/tests/test_filters.py +1 -1
- {nautobot-2.0.5.dist-info → nautobot-2.1.0b1.dist-info}/METADATA +1 -1
- {nautobot-2.0.5.dist-info → nautobot-2.1.0b1.dist-info}/RECORD +376 -351
- {nautobot-2.0.5.dist-info → nautobot-2.1.0b1.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.0.5.dist-info → nautobot-2.1.0b1.dist-info}/WHEEL +0 -0
- {nautobot-2.0.5.dist-info → nautobot-2.1.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -47,6 +47,8 @@ from nautobot.dcim.models import (
|
|
|
47
47
|
DeviceType,
|
|
48
48
|
FrontPort,
|
|
49
49
|
Interface,
|
|
50
|
+
InterfaceRedundancyGroup,
|
|
51
|
+
InterfaceRedundancyGroupAssociation,
|
|
50
52
|
Location,
|
|
51
53
|
LocationType,
|
|
52
54
|
PowerFeed,
|
|
@@ -70,7 +72,16 @@ from nautobot.extras.models import (
|
|
|
70
72
|
)
|
|
71
73
|
from nautobot.extras.registry import registry
|
|
72
74
|
from nautobot.ipam.factory import VLANGroupFactory
|
|
73
|
-
from nautobot.ipam.models import
|
|
75
|
+
from nautobot.ipam.models import (
|
|
76
|
+
IPAddress,
|
|
77
|
+
IPAddressToInterface,
|
|
78
|
+
Namespace,
|
|
79
|
+
Prefix,
|
|
80
|
+
VLAN,
|
|
81
|
+
VRF,
|
|
82
|
+
VRFDeviceAssignment,
|
|
83
|
+
VRFPrefixAssignment,
|
|
84
|
+
)
|
|
74
85
|
from nautobot.users.models import ObjectPermission, Token
|
|
75
86
|
from nautobot.tenancy.models import Tenant
|
|
76
87
|
from nautobot.virtualization.factory import ClusterTypeFactory
|
|
@@ -834,9 +845,55 @@ class GraphQLQueryTest(TestCase):
|
|
|
834
845
|
device=cls.device1,
|
|
835
846
|
status=interface_status,
|
|
836
847
|
)
|
|
848
|
+
cls.namespace = Namespace.objects.first()
|
|
849
|
+
cls.intr_group_status = Status.objects.get_for_model(InterfaceRedundancyGroup).first()
|
|
850
|
+
cls.interface_redundancy_group_1 = InterfaceRedundancyGroup.objects.create(
|
|
851
|
+
name="IRGroup 1",
|
|
852
|
+
status=cls.intr_group_status,
|
|
853
|
+
)
|
|
854
|
+
cls.interface_redundancy_group_2 = InterfaceRedundancyGroup.objects.create(
|
|
855
|
+
name="IRGroup 2",
|
|
856
|
+
status=cls.intr_group_status,
|
|
857
|
+
)
|
|
858
|
+
cls.interface_redundancy_group_3 = InterfaceRedundancyGroup.objects.create(
|
|
859
|
+
name="IRGroup 3",
|
|
860
|
+
status=cls.intr_group_status,
|
|
861
|
+
)
|
|
862
|
+
cls.irg_associations = (
|
|
863
|
+
InterfaceRedundancyGroupAssociation.objects.create(
|
|
864
|
+
interface_redundancy_group=cls.interface_redundancy_group_1,
|
|
865
|
+
interface=cls.interface11,
|
|
866
|
+
priority=123,
|
|
867
|
+
),
|
|
868
|
+
InterfaceRedundancyGroupAssociation.objects.create(
|
|
869
|
+
interface_redundancy_group=cls.interface_redundancy_group_2,
|
|
870
|
+
interface=cls.interface12,
|
|
871
|
+
priority=456,
|
|
872
|
+
),
|
|
873
|
+
InterfaceRedundancyGroupAssociation.objects.create(
|
|
874
|
+
interface_redundancy_group=cls.interface_redundancy_group_3,
|
|
875
|
+
interface=cls.interface11,
|
|
876
|
+
priority=789,
|
|
877
|
+
),
|
|
878
|
+
InterfaceRedundancyGroupAssociation.objects.create(
|
|
879
|
+
interface_redundancy_group=cls.interface_redundancy_group_3,
|
|
880
|
+
interface=cls.interface12,
|
|
881
|
+
priority=789,
|
|
882
|
+
),
|
|
883
|
+
)
|
|
884
|
+
prefixes = Prefix.objects.all()[:2]
|
|
885
|
+
cls.namespace = prefixes[0].namespace
|
|
886
|
+
vrfs = (
|
|
887
|
+
VRF.objects.create(name="VRF 1", rd="65000:100", namespace=cls.namespace),
|
|
888
|
+
VRF.objects.create(name="VRF 2", rd="65000:200", namespace=cls.namespace),
|
|
889
|
+
)
|
|
890
|
+
prefixes[0].vrfs.add(vrfs[0])
|
|
891
|
+
prefixes[0].vrfs.add(vrfs[1])
|
|
892
|
+
prefixes[1].vrfs.add(vrfs[0])
|
|
893
|
+
prefixes[1].vrfs.add(vrfs[1])
|
|
894
|
+
|
|
837
895
|
cls.ip_statuses = list(Status.objects.get_for_model(IPAddress))[:2]
|
|
838
896
|
cls.prefix_statuses = list(Status.objects.get_for_model(Prefix))[:2]
|
|
839
|
-
cls.namespace = Namespace.objects.first()
|
|
840
897
|
cls.prefix1 = Prefix.objects.create(
|
|
841
898
|
prefix="10.0.1.0/24", namespace=cls.namespace, status=cls.prefix_statuses[0]
|
|
842
899
|
)
|
|
@@ -855,6 +912,10 @@ class GraphQLQueryTest(TestCase):
|
|
|
855
912
|
tenant=cls.tenant2,
|
|
856
913
|
face="rear",
|
|
857
914
|
)
|
|
915
|
+
cls.device2.vrfs.add(vrfs[0])
|
|
916
|
+
cls.device2.vrfs.add(vrfs[1])
|
|
917
|
+
cls.device2.vrfs.add(vrfs[0])
|
|
918
|
+
cls.device2.vrfs.add(vrfs[1])
|
|
858
919
|
|
|
859
920
|
cls.interface21 = Interface.objects.create(
|
|
860
921
|
name="Int1",
|
|
@@ -1178,6 +1239,108 @@ query {
|
|
|
1178
1239
|
# Assert GraphQL returned properties match those expected
|
|
1179
1240
|
self.assertEqual(console_port_entry["connected_console_server_port"], connected_console_server_port)
|
|
1180
1241
|
|
|
1242
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1243
|
+
def test_interface_redundancy_group_associations(self):
|
|
1244
|
+
"""Test graphql functionality for InterfaceRedundancyGroupAssociation"""
|
|
1245
|
+
|
|
1246
|
+
query = """\
|
|
1247
|
+
query {
|
|
1248
|
+
interface_redundancy_group_associations {
|
|
1249
|
+
id
|
|
1250
|
+
interface { id }
|
|
1251
|
+
interface_redundancy_group { id }
|
|
1252
|
+
priority
|
|
1253
|
+
}
|
|
1254
|
+
}"""
|
|
1255
|
+
|
|
1256
|
+
result = self.execute_query(query)
|
|
1257
|
+
self.assertIsNone(result.errors)
|
|
1258
|
+
self.assertEqual(
|
|
1259
|
+
len(InterfaceRedundancyGroupAssociation.objects.all()),
|
|
1260
|
+
len(result.data["interface_redundancy_group_associations"]),
|
|
1261
|
+
)
|
|
1262
|
+
for association in result.data["interface_redundancy_group_associations"]:
|
|
1263
|
+
association_obj = InterfaceRedundancyGroupAssociation.objects.get(id=association["id"])
|
|
1264
|
+
# Assert GraphQL returned properties match those expected
|
|
1265
|
+
self.assertEqual(association["interface"]["id"], str(association_obj.interface.pk))
|
|
1266
|
+
self.assertEqual(
|
|
1267
|
+
association["interface_redundancy_group"]["id"], str(association_obj.interface_redundancy_group.pk)
|
|
1268
|
+
)
|
|
1269
|
+
self.assertEqual(association["priority"], association_obj.priority)
|
|
1270
|
+
|
|
1271
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1272
|
+
def test_ip_address_to_interface(self):
|
|
1273
|
+
"""Test graphql functionality for IPAddressToInterface"""
|
|
1274
|
+
|
|
1275
|
+
query = """\
|
|
1276
|
+
query {
|
|
1277
|
+
ip_address_assignments {
|
|
1278
|
+
id
|
|
1279
|
+
interface { id }
|
|
1280
|
+
vm_interface { id }
|
|
1281
|
+
ip_address { id }
|
|
1282
|
+
}
|
|
1283
|
+
}"""
|
|
1284
|
+
|
|
1285
|
+
result = self.execute_query(query)
|
|
1286
|
+
self.assertIsNone(result.errors)
|
|
1287
|
+
self.assertEqual(
|
|
1288
|
+
len(IPAddressToInterface.objects.all()),
|
|
1289
|
+
len(result.data["ip_address_assignments"]),
|
|
1290
|
+
)
|
|
1291
|
+
for association in result.data["ip_address_assignments"]:
|
|
1292
|
+
association_obj = IPAddressToInterface.objects.get(id=association["id"])
|
|
1293
|
+
# Assert GraphQL returned properties match those expected
|
|
1294
|
+
if association_obj.interface:
|
|
1295
|
+
self.assertEqual(association["interface"]["id"], str(association_obj.interface.pk))
|
|
1296
|
+
else:
|
|
1297
|
+
self.assertEqual(association["interface"], None)
|
|
1298
|
+
if association_obj.vm_interface:
|
|
1299
|
+
self.assertEqual(association["vm_interface"]["id"], str(association_obj.vm_interface.pk))
|
|
1300
|
+
else:
|
|
1301
|
+
self.assertEqual(association["vm_interface"], None)
|
|
1302
|
+
self.assertEqual(association["ip_address"]["id"], str(association_obj.ip_address.pk))
|
|
1303
|
+
|
|
1304
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1305
|
+
def test_vrf_assignments(self):
|
|
1306
|
+
"""Test graphql functionality for VRFDeviceAssignment and VRFPrefixAssignment"""
|
|
1307
|
+
|
|
1308
|
+
query = """\
|
|
1309
|
+
query {
|
|
1310
|
+
vrf_device_assignments {
|
|
1311
|
+
id
|
|
1312
|
+
vrf { id }
|
|
1313
|
+
device { id }
|
|
1314
|
+
}
|
|
1315
|
+
vrf_prefix_assignments {
|
|
1316
|
+
id
|
|
1317
|
+
vrf { id }
|
|
1318
|
+
prefix { id }
|
|
1319
|
+
}
|
|
1320
|
+
}"""
|
|
1321
|
+
|
|
1322
|
+
result = self.execute_query(query)
|
|
1323
|
+
self.assertIsNone(result.errors)
|
|
1324
|
+
self.assertEqual(
|
|
1325
|
+
len(VRFDeviceAssignment.objects.all()),
|
|
1326
|
+
len(result.data["vrf_device_assignments"]),
|
|
1327
|
+
)
|
|
1328
|
+
self.assertEqual(
|
|
1329
|
+
len(VRFPrefixAssignment.objects.all()),
|
|
1330
|
+
len(result.data["vrf_prefix_assignments"]),
|
|
1331
|
+
)
|
|
1332
|
+
for assignment in result.data["vrf_device_assignments"]:
|
|
1333
|
+
assignment_obj = VRFDeviceAssignment.objects.get(id=assignment["id"])
|
|
1334
|
+
# Assert GraphQL returned properties match those expected
|
|
1335
|
+
self.assertEqual(assignment["vrf"]["id"], str(assignment_obj.vrf.pk))
|
|
1336
|
+
self.assertEqual(assignment["device"]["id"], str(assignment_obj.device.pk))
|
|
1337
|
+
|
|
1338
|
+
for assignment in result.data["vrf_prefix_assignments"]:
|
|
1339
|
+
assignment_obj = VRFPrefixAssignment.objects.get(id=assignment["id"])
|
|
1340
|
+
# Assert GraphQL returned properties match those expected
|
|
1341
|
+
self.assertEqual(assignment["vrf"]["id"], str(assignment_obj.vrf.pk))
|
|
1342
|
+
self.assertEqual(assignment["prefix"]["id"], str(assignment_obj.prefix.pk))
|
|
1343
|
+
|
|
1181
1344
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1182
1345
|
def test_query_console_server_ports_cable_peer(self):
|
|
1183
1346
|
"""Test querying console server port terminations for their cable peers"""
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from django.contrib.contenttypes.models import ContentType
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from nautobot.core.testing import TransactionTestCase, create_job_result_and_run_job
|
|
7
|
+
from nautobot.dcim.models import DeviceType, Manufacturer
|
|
8
|
+
from nautobot.extras.choices import JobResultStatusChoices, LogLevelChoices
|
|
9
|
+
from nautobot.extras.models import ExportTemplate, JobLogEntry, Status
|
|
10
|
+
from nautobot.users.models import ObjectPermission
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExportObjectListTest(TransactionTestCase):
|
|
14
|
+
"""
|
|
15
|
+
Test the ExportObjectList system job.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
databases = ("default", "job_logs")
|
|
19
|
+
|
|
20
|
+
def test_export_without_permission(self):
|
|
21
|
+
"""Job should enforce user permissions on the content-type being asked for export."""
|
|
22
|
+
job_result = create_job_result_and_run_job(
|
|
23
|
+
"nautobot.core.jobs",
|
|
24
|
+
"ExportObjectList",
|
|
25
|
+
username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
|
|
26
|
+
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
27
|
+
)
|
|
28
|
+
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_FAILURE)
|
|
29
|
+
log_error = JobLogEntry.objects.get(job_result=job_result, log_level=LogLevelChoices.LOG_ERROR)
|
|
30
|
+
self.assertEqual(log_error.message, f'User "{self.user}" does not have permission to view status objects')
|
|
31
|
+
self.assertFalse(job_result.files.exists())
|
|
32
|
+
|
|
33
|
+
def test_export_with_constrained_permission(self):
|
|
34
|
+
"""Job should only allow the user to export objects they have permission to view."""
|
|
35
|
+
instance1, instance2 = Status.objects.all()[:2]
|
|
36
|
+
obj_perm = ObjectPermission(
|
|
37
|
+
name="Test permission",
|
|
38
|
+
constraints={"pk": instance1.pk},
|
|
39
|
+
actions=["view"],
|
|
40
|
+
)
|
|
41
|
+
obj_perm.save()
|
|
42
|
+
obj_perm.users.add(self.user)
|
|
43
|
+
obj_perm.object_types.add(ContentType.objects.get_for_model(Status))
|
|
44
|
+
job_result = create_job_result_and_run_job(
|
|
45
|
+
"nautobot.core.jobs",
|
|
46
|
+
"ExportObjectList",
|
|
47
|
+
username=self.user.username, # otherwise run_job_for_testing defaults to a superuser account
|
|
48
|
+
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
49
|
+
)
|
|
50
|
+
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
|
|
51
|
+
self.assertTrue(job_result.files.exists())
|
|
52
|
+
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.csv")
|
|
53
|
+
csv_data = job_result.files.first().file.read().decode("utf-8")
|
|
54
|
+
self.assertIn(str(instance1.pk), csv_data)
|
|
55
|
+
self.assertNotIn(str(instance2.pk), csv_data)
|
|
56
|
+
|
|
57
|
+
def test_export_all_to_csv(self):
|
|
58
|
+
"""By default, job should export all instances to CSV."""
|
|
59
|
+
job_result = create_job_result_and_run_job(
|
|
60
|
+
"nautobot.core.jobs",
|
|
61
|
+
"ExportObjectList",
|
|
62
|
+
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
63
|
+
)
|
|
64
|
+
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
|
|
65
|
+
self.assertTrue(job_result.files.exists())
|
|
66
|
+
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.csv")
|
|
67
|
+
csv_data = job_result.files.first().file.read().decode("utf-8")
|
|
68
|
+
# May be more than one line per Status if they have newlines in their description strings
|
|
69
|
+
self.assertGreaterEqual(len(csv_data.split("\n")), Status.objects.count() + 1, csv_data) # +1 for CSV header
|
|
70
|
+
|
|
71
|
+
def test_export_all_via_export_template(self):
|
|
72
|
+
"""When an export-template is specified, it should be used."""
|
|
73
|
+
et = ExportTemplate.objects.create(
|
|
74
|
+
content_type=ContentType.objects.get_for_model(Status),
|
|
75
|
+
name="Simple Export Template",
|
|
76
|
+
template_code="{% for obj in queryset %}{{ obj.name }}\n{% endfor %}",
|
|
77
|
+
file_extension="txt",
|
|
78
|
+
)
|
|
79
|
+
job_result = create_job_result_and_run_job(
|
|
80
|
+
"nautobot.core.jobs",
|
|
81
|
+
"ExportObjectList",
|
|
82
|
+
content_type=ContentType.objects.get_for_model(Status).pk,
|
|
83
|
+
export_template=et.pk,
|
|
84
|
+
)
|
|
85
|
+
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
|
|
86
|
+
self.assertTrue(job_result.files.exists())
|
|
87
|
+
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_statuses.txt")
|
|
88
|
+
text_data = job_result.files.first().file.read().decode("utf-8")
|
|
89
|
+
self.assertEqual(len(text_data.split("\n")), Status.objects.count() + 1)
|
|
90
|
+
for status in Status.objects.iterator():
|
|
91
|
+
self.assertIn(status.name, text_data)
|
|
92
|
+
|
|
93
|
+
def test_export_devicetype_to_yaml(self):
|
|
94
|
+
"""Export device-type to YAML."""
|
|
95
|
+
mfr = Manufacturer.objects.create(name="Cisco")
|
|
96
|
+
DeviceType.objects.create(
|
|
97
|
+
manufacturer=mfr,
|
|
98
|
+
model="Cisco CSR1000v",
|
|
99
|
+
u_height=0,
|
|
100
|
+
)
|
|
101
|
+
job_result = create_job_result_and_run_job(
|
|
102
|
+
"nautobot.core.jobs",
|
|
103
|
+
"ExportObjectList",
|
|
104
|
+
content_type=ContentType.objects.get_for_model(DeviceType).pk,
|
|
105
|
+
export_format="yaml",
|
|
106
|
+
)
|
|
107
|
+
self.assertEqual(job_result.status, JobResultStatusChoices.STATUS_SUCCESS)
|
|
108
|
+
self.assertTrue(job_result.files.exists())
|
|
109
|
+
self.assertEqual(Path(job_result.files.first().file.name).name, "nautobot_device_types.yaml")
|
|
110
|
+
yaml_data = job_result.files.first().file.read().decode("utf-8")
|
|
111
|
+
data = yaml.safe_load(yaml_data)
|
|
112
|
+
self.assertEqual(data["manufacturer"], "Cisco")
|
|
@@ -41,6 +41,9 @@ class OpenAPITest(TestCase):
|
|
|
41
41
|
query_params = self.schema["paths"]["/dcim/devices/"]["get"]["parameters"]
|
|
42
42
|
at_least_one_test = False
|
|
43
43
|
for query_param_info in query_params:
|
|
44
|
+
if query_param_info["name"].endswith("_isnull"):
|
|
45
|
+
# The broad catch below does not apply to isnull, which will return a boolean.
|
|
46
|
+
continue
|
|
44
47
|
if query_param_info["name"].startswith("created") or query_param_info["name"].startswith("last_updated"):
|
|
45
48
|
self.assertEqual("array", query_param_info["schema"]["type"])
|
|
46
49
|
self.assertEqual("string", query_param_info["schema"]["items"]["type"])
|
|
@@ -57,6 +60,9 @@ class OpenAPITest(TestCase):
|
|
|
57
60
|
query_params = self.schema["paths"]["/dcim/devices/"]["get"]["parameters"]
|
|
58
61
|
at_least_one_test = False
|
|
59
62
|
for query_param_info in query_params:
|
|
63
|
+
if query_param_info["name"].endswith("_isnull"):
|
|
64
|
+
# The broad catch below does not apply to isnull, which will return a boolean.
|
|
65
|
+
continue
|
|
60
66
|
if query_param_info["name"].startswith("device_redundancy_group_priority"):
|
|
61
67
|
self.assertEqual("array", query_param_info["schema"]["type"])
|
|
62
68
|
self.assertEqual("integer", query_param_info["schema"]["items"]["type"])
|
|
@@ -37,7 +37,7 @@ class HomeViewTestCase(TestCase):
|
|
|
37
37
|
|
|
38
38
|
# Search bar in nav
|
|
39
39
|
nav_search_bar_pattern = re.compile(
|
|
40
|
-
'<nav.*<form action="/search/" method="get" class="navbar-form
|
|
40
|
+
'<nav.*<form action="/search/" method="get" class="navbar-form" id="navbar_search" role="search">.*</form>.*</nav>'
|
|
41
41
|
)
|
|
42
42
|
nav_search_bar_result = nav_search_bar_pattern.search(
|
|
43
43
|
response.content.decode(response.charset).replace("\n", "")
|
|
@@ -53,8 +53,7 @@ class HomeViewTestCase(TestCase):
|
|
|
53
53
|
|
|
54
54
|
return nav_search_bar_result, body_search_bar_result
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
def test_search_bar_not_visible_if_user_not_authenticated_and_hide_restricted_ui_True(self):
|
|
56
|
+
def test_search_bar_not_visible_if_user_not_authenticated(self):
|
|
58
57
|
self.client.logout()
|
|
59
58
|
|
|
60
59
|
nav_search_bar_result, body_search_bar_result = self.make_request()
|
|
@@ -62,23 +61,7 @@ class HomeViewTestCase(TestCase):
|
|
|
62
61
|
self.assertIsNone(nav_search_bar_result)
|
|
63
62
|
self.assertIsNone(body_search_bar_result)
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
def test_search_bar_visible_if_user_authenticated_and_hide_restricted_ui_True(self):
|
|
67
|
-
nav_search_bar_result, body_search_bar_result = self.make_request()
|
|
68
|
-
|
|
69
|
-
self.assertIsNotNone(nav_search_bar_result)
|
|
70
|
-
self.assertIsNotNone(body_search_bar_result)
|
|
71
|
-
|
|
72
|
-
@override_settings(HIDE_RESTRICTED_UI=False)
|
|
73
|
-
def test_search_bar_visible_if_hide_restricted_ui_False(self):
|
|
74
|
-
# Assert if user is authenticated
|
|
75
|
-
nav_search_bar_result, body_search_bar_result = self.make_request()
|
|
76
|
-
|
|
77
|
-
self.assertIsNotNone(nav_search_bar_result)
|
|
78
|
-
self.assertIsNotNone(body_search_bar_result)
|
|
79
|
-
|
|
80
|
-
# Assert if user is logout
|
|
81
|
-
self.client.logout()
|
|
64
|
+
def test_search_bar_visible_if_user_authenticated(self):
|
|
82
65
|
nav_search_bar_result, body_search_bar_result = self.make_request()
|
|
83
66
|
|
|
84
67
|
self.assertIsNotNone(nav_search_bar_result)
|
|
@@ -229,27 +212,8 @@ class NavRestrictedUI(TestCase):
|
|
|
229
212
|
response = self.client.get(reverse("home"))
|
|
230
213
|
return response.content.decode(response.charset)
|
|
231
214
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"""The "Installed Apps" menu item should be available to is_staff user regardless of HIDE_RESTRICTED_UI."""
|
|
235
|
-
# Make user admin
|
|
236
|
-
self.user.is_staff = True
|
|
237
|
-
self.user.save()
|
|
238
|
-
|
|
239
|
-
response_content = self.make_request()
|
|
240
|
-
self.assertInHTML(
|
|
241
|
-
f"""
|
|
242
|
-
<a href="{self.url}"
|
|
243
|
-
data-item-weight="{self.item_weight}">
|
|
244
|
-
Installed Plugins
|
|
245
|
-
</a>
|
|
246
|
-
""",
|
|
247
|
-
response_content,
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
@override_settings(HIDE_RESTRICTED_UI=False)
|
|
251
|
-
def test_installed_apps_visible_to_staff_with_hide_restricted_ui_false(self):
|
|
252
|
-
"""The "Installed Apps" menu item should be available to is_staff user regardless of HIDE_RESTRICTED_UI."""
|
|
215
|
+
def test_installed_apps_visible_to_staff(self):
|
|
216
|
+
"""The "Installed Apps" menu item should be available to is_staff user."""
|
|
253
217
|
# Make user admin
|
|
254
218
|
self.user.is_staff = True
|
|
255
219
|
self.user.save()
|
|
@@ -265,29 +229,11 @@ class NavRestrictedUI(TestCase):
|
|
|
265
229
|
response_content,
|
|
266
230
|
)
|
|
267
231
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
"""The "Installed Apps" menu item should be hidden from a non-staff user when HIDE_RESTRICTED_UI=True."""
|
|
271
|
-
response_content = self.make_request()
|
|
272
|
-
|
|
273
|
-
self.assertNotRegex(response_content, r"Installed\s+Apps")
|
|
274
|
-
|
|
275
|
-
@override_settings(HIDE_RESTRICTED_UI=False)
|
|
276
|
-
def test_installed_apps_disabled_to_non_staff_user_with_hide_restricted_ui_false(self):
|
|
277
|
-
"""The "Installed Apps" menu item should be disabled for a non-staff user when HIDE_RESTRICTED_UI=False."""
|
|
232
|
+
def test_installed_apps_not_visible_to_non_staff_user_without_permission(self):
|
|
233
|
+
"""The "Installed Apps" menu item should be hidden from a non-staff user without permission."""
|
|
278
234
|
response_content = self.make_request()
|
|
279
235
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
self.assertInHTML(
|
|
283
|
-
f"""
|
|
284
|
-
<a href="{self.url}"
|
|
285
|
-
data-item-weight="{self.item_weight}">
|
|
286
|
-
Installed Plugins
|
|
287
|
-
</a>
|
|
288
|
-
""",
|
|
289
|
-
response_content,
|
|
290
|
-
)
|
|
236
|
+
self.assertNotRegex(response_content, r"Installed\s+Plugins")
|
|
291
237
|
|
|
292
238
|
|
|
293
239
|
class LoginUI(TestCase):
|
|
@@ -326,9 +272,9 @@ class LoginUI(TestCase):
|
|
|
326
272
|
sso_login_search_result = self.make_request()
|
|
327
273
|
self.assertIsNotNone(sso_login_search_result)
|
|
328
274
|
|
|
329
|
-
@override_settings(
|
|
330
|
-
def
|
|
331
|
-
"""Assert that api docs and graphql redirects to login page if user is unauthenticated
|
|
275
|
+
@override_settings(BANNER_TOP="Hello, Banner Top", BANNER_BOTTOM="Hello, Banner Bottom")
|
|
276
|
+
def test_routes_redirect_back_to_login_unauthenticated(self):
|
|
277
|
+
"""Assert that api docs and graphql redirects to login page if user is unauthenticated."""
|
|
332
278
|
self.client.logout()
|
|
333
279
|
headers = {"HTTP_ACCEPT": "text/html"}
|
|
334
280
|
urls = [reverse("api_docs"), reverse("graphql")]
|
|
@@ -346,26 +292,6 @@ class LoginUI(TestCase):
|
|
|
346
292
|
self.assertNotIn("Hello, Banner Top", response_content)
|
|
347
293
|
self.assertNotIn("Hello, Banner Bottom", response_content)
|
|
348
294
|
|
|
349
|
-
@override_settings(HIDE_RESTRICTED_UI=False, BANNER_TOP="Hello, Banner Top", BANNER_BOTTOM="Hello, Banner Bottom")
|
|
350
|
-
def test_routes_no_redirect_back_to_login_if_hide_restricted_ui_false(self):
|
|
351
|
-
"""Assert that api docs and graphql do not redirects to login page if user is unauthenticated and HIDE_RESTRICTED_UI=False."""
|
|
352
|
-
self.client.logout()
|
|
353
|
-
headers = {"HTTP_ACCEPT": "text/html"}
|
|
354
|
-
urls = [reverse("api_docs"), reverse("graphql")]
|
|
355
|
-
for url in urls:
|
|
356
|
-
response = self.client.get(url, **headers)
|
|
357
|
-
self.assertHttpStatus(response, 200)
|
|
358
|
-
self.assertEqual(response.request["PATH_INFO"], url)
|
|
359
|
-
response_content = response.content.decode(response.charset).replace("\n", "")
|
|
360
|
-
# Assert Footer items(`self.footer_elements`), Banner and Banner Top is not hidden
|
|
361
|
-
for footer_text in self.footer_elements:
|
|
362
|
-
self.assertInHTML(footer_text, response_content)
|
|
363
|
-
|
|
364
|
-
# Only API Docs implements BANNERS
|
|
365
|
-
if url == urls[0]:
|
|
366
|
-
self.assertInHTML("Hello, Banner Top", response_content)
|
|
367
|
-
self.assertInHTML("Hello, Banner Bottom", response_content)
|
|
368
|
-
|
|
369
295
|
|
|
370
296
|
class MetricsViewTestCase(TestCase):
|
|
371
297
|
def query_and_parse_metrics(self):
|
nautobot/core/urls.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from django.conf import settings
|
|
2
2
|
from django.conf.urls import include
|
|
3
3
|
from django.urls import path
|
|
4
|
+
from django.views.generic import TemplateView
|
|
4
5
|
from django.views.static import serve
|
|
5
6
|
|
|
6
7
|
from nautobot.core.views import CustomGraphQLView, HomeView, StaticMediaFailureView, SearchView, nautobot_metrics_view
|
|
@@ -31,7 +32,7 @@ urlpatterns = [
|
|
|
31
32
|
path("api/", include("nautobot.core.api.urls")),
|
|
32
33
|
# GraphQL
|
|
33
34
|
path("graphql/", CustomGraphQLView.as_view(graphiql=True), name="graphql"),
|
|
34
|
-
# Serving static media in Django
|
|
35
|
+
# Serving static media in Django (TODO: should be DEBUG mode only - "This view is NOT hardened for production use")
|
|
35
36
|
path("media/<path:path>", serve, {"document_root": settings.MEDIA_ROOT}),
|
|
36
37
|
# Admin
|
|
37
38
|
path("admin/", admin_site.urls),
|
|
@@ -46,6 +47,10 @@ urlpatterns = [
|
|
|
46
47
|
path(r"health/", include("health_check.urls")),
|
|
47
48
|
# FileProxy attachments download/get URLs used in admin views only
|
|
48
49
|
path("files/", include("db_file_storage.urls")),
|
|
50
|
+
# Templated css file
|
|
51
|
+
path(
|
|
52
|
+
"template.css", TemplateView.as_view(template_name="template.css", content_type="text/css"), name="template_css"
|
|
53
|
+
),
|
|
49
54
|
]
|
|
50
55
|
|
|
51
56
|
|
nautobot/core/utils/lookup.py
CHANGED
|
@@ -192,3 +192,31 @@ def get_table_for_model(model):
|
|
|
192
192
|
(Union[Table, None]): Either the `Table` class or `None`
|
|
193
193
|
"""
|
|
194
194
|
return get_related_class_for_model(model, module_name="tables", object_suffix="Table")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_created_and_last_updated_usernames_for_model(instance):
|
|
198
|
+
"""
|
|
199
|
+
Args:
|
|
200
|
+
instance: A model class instance
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
created_by: Username of the user that created the instance
|
|
204
|
+
last_updated_by: Username of the user that last modified the instance
|
|
205
|
+
"""
|
|
206
|
+
from nautobot.extras.choices import ObjectChangeActionChoices
|
|
207
|
+
from nautobot.extras.models import ObjectChange
|
|
208
|
+
|
|
209
|
+
object_change_records = get_changes_for_model(instance)
|
|
210
|
+
created_by = None
|
|
211
|
+
last_updated_by = None
|
|
212
|
+
try:
|
|
213
|
+
created_by_record = object_change_records.get(action=ObjectChangeActionChoices.ACTION_CREATE)
|
|
214
|
+
created_by = created_by_record.user_name
|
|
215
|
+
except ObjectChange.DoesNotExist:
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
last_updated_by_record = object_change_records.first()
|
|
219
|
+
if last_updated_by_record:
|
|
220
|
+
last_updated_by = last_updated_by_record.user_name
|
|
221
|
+
|
|
222
|
+
return created_by, last_updated_by
|
nautobot/core/utils/requests.py
CHANGED
|
@@ -114,13 +114,12 @@ def get_filterable_params_from_filter_params(filter_params, non_filter_params, f
|
|
|
114
114
|
Returns:
|
|
115
115
|
(QueryDict): Filter param querydict with only queryset filterable params
|
|
116
116
|
"""
|
|
117
|
-
for non_filter_param in non_filter_params:
|
|
118
|
-
filter_params.pop(non_filter_param, None)
|
|
119
|
-
|
|
120
117
|
# Some FilterSet field only accept single choice not multiple choices
|
|
121
118
|
# e.g datetime field, bool fields etc.
|
|
122
119
|
final_filter_params = {}
|
|
123
120
|
for field in filter_params.keys():
|
|
121
|
+
if field in non_filter_params:
|
|
122
|
+
continue
|
|
124
123
|
if filter_params.get(field):
|
|
125
124
|
# `is_single_choice_field` implements `get_filterset_field`, which throws an exception if a field is not found.
|
|
126
125
|
# If an exception is thrown, instead of throwing an exception, set `_is_single_choice_field` to 'False'
|
nautobot/core/views/__init__.py
CHANGED
|
@@ -25,7 +25,6 @@ from prometheus_client.registry import Collector
|
|
|
25
25
|
from nautobot.core.constants import SEARCH_MAX_RESULTS
|
|
26
26
|
from nautobot.core.forms import SearchForm
|
|
27
27
|
from nautobot.core.releases import get_latest_release
|
|
28
|
-
from nautobot.core.utils.config import get_settings_or_config
|
|
29
28
|
from nautobot.core.utils.lookup import get_route_for_model
|
|
30
29
|
from nautobot.extras.models import GraphQLQuery
|
|
31
30
|
from nautobot.extras.registry import registry
|
|
@@ -58,8 +57,8 @@ class HomeView(AccessMixin, TemplateView):
|
|
|
58
57
|
return template.render(additional_context)
|
|
59
58
|
|
|
60
59
|
def get(self, request, *args, **kwargs):
|
|
61
|
-
# Redirect user to login page if not authenticated
|
|
62
|
-
if not request.user.is_authenticated
|
|
60
|
+
# Redirect user to login page if not authenticated
|
|
61
|
+
if not request.user.is_authenticated:
|
|
63
62
|
return self.handle_no_permission()
|
|
64
63
|
# Check whether a new release is available. (Only for staff/superusers.)
|
|
65
64
|
new_release = None
|
|
@@ -242,7 +241,7 @@ def csrf_failure(request, reason="", template_name="403_csrf_failure.html"):
|
|
|
242
241
|
|
|
243
242
|
class CustomGraphQLView(GraphQLView):
|
|
244
243
|
def render_graphiql(self, request, **data):
|
|
245
|
-
if not request.user.is_authenticated
|
|
244
|
+
if not request.user.is_authenticated:
|
|
246
245
|
graphql_url = reverse("graphql")
|
|
247
246
|
login_url = reverse(settings.LOGIN_URL)
|
|
248
247
|
return redirect(f"{login_url}?next={graphql_url}")
|
nautobot/core/views/generic.py
CHANGED
|
@@ -39,6 +39,7 @@ from nautobot.core.forms import (
|
|
|
39
39
|
from nautobot.core.forms.forms import DynamicFilterFormSet
|
|
40
40
|
from nautobot.core.templatetags.helpers import bettertitle, validated_viewname
|
|
41
41
|
from nautobot.core.utils.config import get_settings_or_config
|
|
42
|
+
from nautobot.core.utils.lookup import get_created_and_last_updated_usernames_for_model
|
|
42
43
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
43
44
|
from nautobot.core.utils.requests import (
|
|
44
45
|
convert_querydict_to_factory_formset_acceptable_querydict,
|
|
@@ -100,6 +101,8 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
|
|
|
100
101
|
Generic GET handler for accessing an object.
|
|
101
102
|
"""
|
|
102
103
|
instance = get_object_or_404(self.queryset, **kwargs)
|
|
104
|
+
# Get the ObjectChange records to populate the advanced tab information
|
|
105
|
+
created_by, last_updated_by = get_created_and_last_updated_usernames_for_model(instance)
|
|
103
106
|
|
|
104
107
|
# TODO: this feels inelegant - should the tabs lookup be a dedicated endpoint rather than piggybacking
|
|
105
108
|
# on the object-retrieve endpoint?
|
|
@@ -127,6 +130,8 @@ class ObjectView(ObjectPermissionRequiredMixin, View):
|
|
|
127
130
|
"object": instance,
|
|
128
131
|
"verbose_name": self.queryset.model._meta.verbose_name,
|
|
129
132
|
"verbose_name_plural": self.queryset.model._meta.verbose_name_plural,
|
|
133
|
+
"created_by": created_by,
|
|
134
|
+
"last_updated_by": last_updated_by,
|
|
130
135
|
**self.get_extra_context(request, instance),
|
|
131
136
|
},
|
|
132
137
|
)
|
|
@@ -152,7 +157,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
152
157
|
template_name = "generic/object_list.html"
|
|
153
158
|
action_buttons = ("add", "import", "export")
|
|
154
159
|
non_filter_params = (
|
|
155
|
-
"export", # trigger for CSV/export-template/YAML export
|
|
160
|
+
"export", # trigger for CSV/export-template/YAML export # 3.0 TODO: remove, irrelevant after #4746
|
|
156
161
|
"page", # used by django-tables2.RequestConfig
|
|
157
162
|
"per_page", # used by get_paginate_count
|
|
158
163
|
"sort", # table sorting
|
|
@@ -166,7 +171,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
166
171
|
def get_required_permission(self):
|
|
167
172
|
return get_permission_for_model(self.queryset.model, "view")
|
|
168
173
|
|
|
169
|
-
# TODO: remove
|
|
174
|
+
# 3.0 TODO: remove, irrelevant after #4746
|
|
170
175
|
def queryset_to_yaml(self):
|
|
171
176
|
"""
|
|
172
177
|
Export the queryset of objects as concatenated YAML documents.
|
|
@@ -229,7 +234,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
229
234
|
filter_form = self.filterset_form(filter_params, label_suffix="")
|
|
230
235
|
|
|
231
236
|
# Check for export template rendering
|
|
232
|
-
if request.GET.get("export"):
|
|
237
|
+
if request.GET.get("export"): # 3.0 TODO: remove, irrelevant after #4746
|
|
233
238
|
et = get_object_or_404(
|
|
234
239
|
ExportTemplate,
|
|
235
240
|
content_type=content_type,
|
|
@@ -244,7 +249,7 @@ class ObjectListView(ObjectPermissionRequiredMixin, View):
|
|
|
244
249
|
)
|
|
245
250
|
|
|
246
251
|
# Check for YAML export support
|
|
247
|
-
elif "export" in request.GET and hasattr(model, "to_yaml"):
|
|
252
|
+
elif "export" in request.GET and hasattr(model, "to_yaml"): # 3.0 TODO: remove, irrelevant after #4746
|
|
248
253
|
response = HttpResponse(self.queryset_to_yaml(), content_type="text/yaml")
|
|
249
254
|
filename = f"{settings.BRANDING_PREPENDED_FILENAME}{self.queryset.model._meta.verbose_name_plural}.yaml"
|
|
250
255
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|