nautobot 2.2.2__py3-none-any.whl → 2.2.4__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/jobs.py +2 -0
- nautobot/core/api/utils.py +12 -9
- nautobot/core/apps/__init__.py +2 -2
- nautobot/core/celery/__init__.py +79 -68
- nautobot/core/celery/backends.py +9 -1
- nautobot/core/celery/control.py +4 -7
- nautobot/core/celery/schedulers.py +4 -2
- nautobot/core/celery/task.py +78 -5
- nautobot/core/graphql/schema.py +2 -1
- nautobot/core/jobs/__init__.py +2 -1
- nautobot/core/settings.py +6 -4
- nautobot/core/settings.yaml +51 -16
- nautobot/core/templates/admin/base.html +2 -2
- nautobot/core/templates/base_django.html +2 -2
- nautobot/core/templates/buttons/export.html +47 -47
- nautobot/core/templates/generic/object_list.html +3 -3
- nautobot/core/templates/inc/javascript.html +3 -0
- nautobot/core/templates/inc/media.html +3 -0
- nautobot/core/templates/login.html +2 -2
- nautobot/core/templates/nautobot_config.py.j2 +2 -0
- nautobot/core/templatetags/helpers.py +66 -9
- nautobot/core/testing/__init__.py +6 -1
- nautobot/core/testing/api.py +12 -13
- nautobot/core/testing/mixins.py +2 -2
- nautobot/core/testing/views.py +50 -51
- nautobot/core/tests/test_api.py +23 -2
- nautobot/core/tests/test_jobs.py +79 -2
- nautobot/core/tests/test_templatetags_helpers.py +32 -0
- nautobot/core/tests/test_views.py +52 -0
- nautobot/core/tests/test_views_utils.py +22 -1
- nautobot/core/utils/module_loading.py +89 -0
- nautobot/core/views/mixins.py +4 -0
- nautobot/core/views/utils.py +3 -2
- nautobot/dcim/choices.py +14 -0
- nautobot/dcim/forms.py +51 -1
- nautobot/dcim/models/device_components.py +9 -5
- nautobot/dcim/templates/dcim/location.html +32 -13
- nautobot/dcim/templates/dcim/location_migrate_data_to_contact.html +102 -0
- nautobot/dcim/tests/test_views.py +376 -55
- nautobot/dcim/urls.py +5 -0
- nautobot/dcim/views.py +172 -21
- nautobot/extras/api/serializers.py +17 -6
- nautobot/extras/api/views.py +21 -10
- nautobot/extras/constants.py +3 -3
- nautobot/extras/datasources/git.py +47 -58
- nautobot/extras/forms/forms.py +3 -1
- nautobot/extras/jobs.py +79 -146
- nautobot/extras/models/datasources.py +0 -2
- nautobot/extras/models/jobs.py +36 -18
- nautobot/extras/plugins/__init__.py +1 -20
- nautobot/extras/signals.py +6 -9
- nautobot/extras/test_jobs/__init__.py +8 -0
- nautobot/extras/test_jobs/dry_run.py +3 -2
- nautobot/extras/test_jobs/fail.py +43 -0
- nautobot/extras/test_jobs/ipaddress_vars.py +40 -1
- nautobot/extras/test_jobs/jobs_module/__init__.py +5 -0
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/__init__.py +1 -0
- nautobot/extras/test_jobs/jobs_module/jobs_submodule/jobs.py +6 -0
- nautobot/extras/test_jobs/pass.py +40 -0
- nautobot/extras/test_jobs/relative_import.py +11 -0
- nautobot/extras/tests/test_api.py +3 -0
- nautobot/extras/tests/test_context_managers.py +18 -0
- nautobot/extras/tests/test_datasources.py +125 -118
- nautobot/extras/tests/test_job_variables.py +57 -15
- nautobot/extras/tests/test_jobs.py +135 -1
- nautobot/extras/tests/test_models.py +26 -19
- nautobot/extras/tests/test_plugins.py +1 -3
- nautobot/extras/tests/test_views.py +2 -4
- nautobot/extras/utils.py +2 -1
- nautobot/extras/views.py +82 -116
- nautobot/ipam/api/views.py +8 -1
- nautobot/ipam/graphql/types.py +11 -0
- nautobot/ipam/mixins.py +32 -0
- nautobot/ipam/models.py +2 -1
- nautobot/ipam/querysets.py +6 -1
- nautobot/ipam/tests/test_models.py +82 -0
- nautobot/ipam/views.py +6 -6
- nautobot/project-static/docs/404.html +107 -51
- nautobot/project-static/docs/apps/index.html +107 -51
- nautobot/project-static/docs/apps/nautobot-apps.html +107 -51
- nautobot/project-static/docs/assets/_mkdocstrings.css +6 -1
- nautobot/project-static/docs/assets/extra.css +11 -0
- nautobot/project-static/docs/assets/javascripts/bundle.3220b9d7.min.js +29 -0
- nautobot/project-static/docs/assets/javascripts/bundle.3220b9d7.min.js.map +7 -0
- nautobot/project-static/docs/assets/stylesheets/main.66ac8b77.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.66ac8b77.min.css.map +1 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +108 -52
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +287 -262
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +107 -51
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +107 -51
- nautobot/project-static/docs/development/apps/api/configuration-view.html +110 -54
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +110 -54
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +107 -51
- nautobot/project-static/docs/development/apps/api/models/global-search.html +110 -54
- nautobot/project-static/docs/development/apps/api/models/graphql.html +113 -57
- nautobot/project-static/docs/development/apps/api/models/index.html +107 -51
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +113 -57
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +107 -51
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +110 -54
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +107 -51
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +107 -51
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +110 -54
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +111 -55
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +110 -54
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +110 -54
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +107 -51
- nautobot/project-static/docs/development/apps/api/prometheus.html +110 -54
- nautobot/project-static/docs/development/apps/api/setup.html +107 -51
- nautobot/project-static/docs/development/apps/api/testing.html +113 -57
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +110 -54
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +110 -54
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +107 -51
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +107 -51
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +113 -57
- nautobot/project-static/docs/development/apps/api/views/base-template.html +107 -51
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +110 -54
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +107 -51
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +110 -54
- nautobot/project-static/docs/development/apps/api/views/index.html +107 -51
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +113 -57
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +122 -66
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +110 -54
- nautobot/project-static/docs/development/apps/api/views/notes.html +110 -54
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +107 -51
- nautobot/project-static/docs/development/apps/api/views/urls.html +107 -51
- nautobot/project-static/docs/development/apps/index.html +128 -72
- nautobot/project-static/docs/development/apps/migration/code-updates.html +107 -51
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +107 -51
- nautobot/project-static/docs/development/apps/migration/from-v1.html +107 -51
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +107 -51
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +107 -51
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +107 -51
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +107 -51
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +110 -54
- nautobot/project-static/docs/development/core/application-registry.html +242 -144
- nautobot/project-static/docs/development/core/best-practices.html +122 -66
- nautobot/project-static/docs/development/core/bootstrap-ui.html +107 -51
- nautobot/project-static/docs/development/core/caching.html +107 -51
- nautobot/project-static/docs/development/core/controllers.html +107 -51
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +113 -57
- nautobot/project-static/docs/development/core/generic-views.html +110 -54
- nautobot/project-static/docs/development/core/getting-started.html +135 -79
- nautobot/project-static/docs/development/core/homepage.html +110 -54
- nautobot/project-static/docs/development/core/index.html +107 -51
- nautobot/project-static/docs/development/core/model-checklist.html +156 -52
- nautobot/project-static/docs/development/core/model-features.html +108 -52
- nautobot/project-static/docs/development/core/natural-keys.html +110 -54
- nautobot/project-static/docs/development/core/navigation-menu.html +107 -51
- nautobot/project-static/docs/development/core/release-checklist.html +107 -51
- nautobot/project-static/docs/development/core/role-internals.html +107 -51
- nautobot/project-static/docs/development/core/settings.html +107 -51
- nautobot/project-static/docs/development/core/style-guide.html +110 -54
- nautobot/project-static/docs/development/core/templates.html +113 -57
- nautobot/project-static/docs/development/core/testing.html +125 -69
- nautobot/project-static/docs/development/core/user-preferences.html +107 -51
- nautobot/project-static/docs/development/index.html +107 -51
- nautobot/project-static/docs/development/jobs/index.html +504 -172
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +111 -55
- nautobot/project-static/docs/docker/index.html +3 -3
- nautobot/project-static/docs/index.html +125 -69
- nautobot/project-static/docs/installation/selinux-troubleshooting.html +3 -3
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/release-notes/index.html +107 -51
- nautobot/project-static/docs/release-notes/version-1.0.html +107 -51
- nautobot/project-static/docs/release-notes/version-1.1.html +107 -51
- nautobot/project-static/docs/release-notes/version-1.2.html +107 -51
- nautobot/project-static/docs/release-notes/version-1.3.html +107 -51
- nautobot/project-static/docs/release-notes/version-1.4.html +107 -51
- nautobot/project-static/docs/release-notes/version-1.5.html +116 -60
- nautobot/project-static/docs/release-notes/version-1.6.html +107 -51
- nautobot/project-static/docs/release-notes/version-2.0.html +110 -54
- nautobot/project-static/docs/release-notes/version-2.1.html +107 -51
- nautobot/project-static/docs/release-notes/version-2.2.html +500 -114
- nautobot/project-static/docs/requirements.txt +2 -2
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +262 -262
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +107 -51
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +107 -51
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +107 -51
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +107 -51
- nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +251 -164
- nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +113 -57
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +107 -51
- nautobot/project-static/docs/user-guide/administration/guides/caching.html +113 -57
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +107 -51
- nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +107 -51
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +107 -51
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +113 -57
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +107 -51
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +107 -51
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +107 -51
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +171 -112
- nautobot/project-static/docs/user-guide/administration/installation/docker.html +13 -8626
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +117 -61
- nautobot/project-static/docs/user-guide/administration/installation/health-checks.html +13 -8614
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +252 -165
- nautobot/project-static/docs/user-guide/administration/installation/index.html +165 -192
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +411 -691
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +248 -229
- nautobot/project-static/docs/user-guide/administration/installation/selinux-troubleshooting.html +13 -8118
- nautobot/project-static/docs/user-guide/administration/installation/services.html +350 -240
- nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +8684 -0
- nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +8672 -0
- nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +8176 -0
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +110 -54
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +110 -54
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +155 -99
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +107 -51
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +109 -53
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +107 -51
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +107 -51
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +107 -51
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +107 -51
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +107 -51
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +107 -51
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +117 -58
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +116 -60
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +119 -63
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +125 -69
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +128 -72
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +227 -60
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +110 -54
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +107 -51
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +113 -57
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +113 -57
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +113 -57
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +110 -54
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +113 -57
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +107 -51
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +110 -54
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +107 -51
- nautobot/project-static/docs/user-guide/index.html +109 -53
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +107 -51
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +113 -57
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +128 -72
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +107 -51
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +125 -69
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +107 -51
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +110 -54
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +125 -69
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +110 -54
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +107 -51
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +107 -51
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +143 -100
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +113 -57
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +113 -57
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +110 -54
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +123 -67
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +107 -51
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +110 -54
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +116 -60
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +110 -54
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +131 -75
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +149 -93
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +110 -54
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +107 -51
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +116 -60
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +119 -63
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +110 -54
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +137 -81
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +110 -54
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +107 -51
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +110 -54
- nautobot/project-static/js/forms.js +18 -11
- nautobot/tenancy/views.py +2 -6
- nautobot/virtualization/views.py +5 -9
- {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/METADATA +4 -4
- {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/RECORD +357 -348
- nautobot/extras/test_jobs/job_variables.py +0 -93
- nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js +0 -29
- nautobot/project-static/docs/assets/javascripts/bundle.bd41221c.min.js.map +0 -7
- nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.bcfcd587.min.css.map +0 -1
- {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/NOTICE +0 -0
- {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/WHEEL +0 -0
- {nautobot-2.2.2.dist-info → nautobot-2.2.4.dist-info}/entry_points.txt +0 -0
nautobot/extras/views.py
CHANGED
|
@@ -152,19 +152,18 @@ class ConfigContextView(generic.ObjectView):
|
|
|
152
152
|
queryset = ConfigContext.objects.all()
|
|
153
153
|
|
|
154
154
|
def get_extra_context(self, request, instance):
|
|
155
|
+
context = super().get_extra_context(request, instance)
|
|
155
156
|
# Determine user's preferred output format
|
|
156
157
|
if request.GET.get("format") in ["json", "yaml"]:
|
|
157
|
-
|
|
158
|
+
context["format"] = request.GET.get("format")
|
|
158
159
|
if request.user.is_authenticated:
|
|
159
|
-
request.user.set_config("extras.configcontext.format",
|
|
160
|
+
request.user.set_config("extras.configcontext.format", context["format"], commit=True)
|
|
160
161
|
elif request.user.is_authenticated:
|
|
161
|
-
|
|
162
|
+
context["format"] = request.user.get_config("extras.configcontext.format", "json")
|
|
162
163
|
else:
|
|
163
|
-
|
|
164
|
+
context["format"] = "json"
|
|
164
165
|
|
|
165
|
-
return
|
|
166
|
-
"format": format_,
|
|
167
|
-
}
|
|
166
|
+
return context
|
|
168
167
|
|
|
169
168
|
|
|
170
169
|
class ConfigContextEditView(generic.ObjectEditView):
|
|
@@ -236,19 +235,18 @@ class ConfigContextSchemaView(generic.ObjectView):
|
|
|
236
235
|
queryset = ConfigContextSchema.objects.all()
|
|
237
236
|
|
|
238
237
|
def get_extra_context(self, request, instance):
|
|
238
|
+
context = super().get_extra_context(request, instance)
|
|
239
239
|
# Determine user's preferred output format
|
|
240
240
|
if request.GET.get("format") in ["json", "yaml"]:
|
|
241
|
-
|
|
241
|
+
context["format"] = request.GET.get("format")
|
|
242
242
|
if request.user.is_authenticated:
|
|
243
|
-
request.user.set_config("extras.configcontextschema.format",
|
|
243
|
+
request.user.set_config("extras.configcontextschema.format", context["format"], commit=True)
|
|
244
244
|
elif request.user.is_authenticated:
|
|
245
|
-
|
|
245
|
+
context["format"] = request.user.get_config("extras.configcontextschema.format", "json")
|
|
246
246
|
else:
|
|
247
|
-
|
|
247
|
+
context["format"] = "json"
|
|
248
248
|
|
|
249
|
-
return
|
|
250
|
-
"format": format_,
|
|
251
|
-
}
|
|
249
|
+
return context
|
|
252
250
|
|
|
253
251
|
|
|
254
252
|
class ConfigContextSchemaObjectValidationView(generic.ObjectView):
|
|
@@ -404,10 +402,8 @@ class ContactAssociationUIViewSet(
|
|
|
404
402
|
non_filter_params = ("export", "page", "per_page", "sort")
|
|
405
403
|
|
|
406
404
|
|
|
407
|
-
class
|
|
408
|
-
|
|
409
|
-
model_form = forms.ObjectNewContactForm
|
|
410
|
-
template_name = "extras/object_new_contact.html"
|
|
405
|
+
class ObjectContactTeamMixin:
|
|
406
|
+
"""Mixin that contains a custom post() method to create a new contact/team and assign it to an existing object"""
|
|
411
407
|
|
|
412
408
|
def post(self, request, *args, **kwargs):
|
|
413
409
|
obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
|
@@ -428,13 +424,22 @@ class ObjectNewContactView(generic.ObjectEditView):
|
|
|
428
424
|
if hasattr(form, "save_note") and callable(form.save_note):
|
|
429
425
|
form.save_note(instance=obj, user=request.user)
|
|
430
426
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
427
|
+
if isinstance(obj, Contact):
|
|
428
|
+
association = ContactAssociation(
|
|
429
|
+
contact=obj,
|
|
430
|
+
associated_object_type=ContentType.objects.get(id=request.POST.get("associated_object_type")),
|
|
431
|
+
associated_object_id=request.POST.get("associated_object_id"),
|
|
432
|
+
status=Status.objects.get(id=request.POST.get("status")),
|
|
433
|
+
role=Role.objects.get(id=request.POST.get("role")) if request.POST.get("role") else None,
|
|
434
|
+
)
|
|
435
|
+
else:
|
|
436
|
+
association = ContactAssociation(
|
|
437
|
+
team=obj,
|
|
438
|
+
associated_object_type=ContentType.objects.get(id=request.POST.get("associated_object_type")),
|
|
439
|
+
associated_object_id=request.POST.get("associated_object_id"),
|
|
440
|
+
status=Status.objects.get(id=request.POST.get("status")),
|
|
441
|
+
role=Role.objects.get(id=request.POST.get("role")) if request.POST.get("role") else None,
|
|
442
|
+
)
|
|
438
443
|
association.validated_save()
|
|
439
444
|
self.successful_post(request, obj, object_created, logger)
|
|
440
445
|
|
|
@@ -474,75 +479,17 @@ class ObjectNewContactView(generic.ObjectEditView):
|
|
|
474
479
|
)
|
|
475
480
|
|
|
476
481
|
|
|
477
|
-
class
|
|
482
|
+
class ObjectNewContactView(ObjectContactTeamMixin, generic.ObjectEditView):
|
|
483
|
+
queryset = Contact.objects.all()
|
|
484
|
+
model_form = forms.ObjectNewContactForm
|
|
485
|
+
template_name = "extras/object_new_contact.html"
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class ObjectNewTeamView(ObjectContactTeamMixin, generic.ObjectEditView):
|
|
478
489
|
queryset = Team.objects.all()
|
|
479
490
|
model_form = forms.ObjectNewTeamForm
|
|
480
491
|
template_name = "extras/object_new_team.html"
|
|
481
492
|
|
|
482
|
-
def post(self, request, *args, **kwargs):
|
|
483
|
-
obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
|
|
484
|
-
form = self.model_form(data=request.POST, files=request.FILES, instance=obj)
|
|
485
|
-
restrict_form_fields(form, request.user)
|
|
486
|
-
|
|
487
|
-
if form.is_valid():
|
|
488
|
-
logger.debug("Form validation was successful")
|
|
489
|
-
|
|
490
|
-
try:
|
|
491
|
-
with transaction.atomic():
|
|
492
|
-
object_created = not form.instance.present_in_database
|
|
493
|
-
obj = form.save()
|
|
494
|
-
|
|
495
|
-
# Check that the new object conforms with any assigned object-level permissions
|
|
496
|
-
self.queryset.get(pk=obj.pk)
|
|
497
|
-
|
|
498
|
-
if hasattr(form, "save_note") and callable(form.save_note):
|
|
499
|
-
form.save_note(instance=obj, user=request.user)
|
|
500
|
-
|
|
501
|
-
association = ContactAssociation(
|
|
502
|
-
team=obj,
|
|
503
|
-
associated_object_type=ContentType.objects.get(id=request.POST.get("associated_object_type")),
|
|
504
|
-
associated_object_id=request.POST.get("associated_object_id"),
|
|
505
|
-
status=Status.objects.get(id=request.POST.get("status")),
|
|
506
|
-
role=Role.objects.get(id=request.POST.get("role")) if request.POST.get("role") else None,
|
|
507
|
-
)
|
|
508
|
-
association.validated_save()
|
|
509
|
-
self.successful_post(request, obj, object_created, logger)
|
|
510
|
-
|
|
511
|
-
if "_addanother" in request.POST:
|
|
512
|
-
# If the object has clone_fields, pre-populate a new instance of the form
|
|
513
|
-
if hasattr(obj, "clone_fields"):
|
|
514
|
-
url = f"{request.path}?{prepare_cloned_fields(obj)}"
|
|
515
|
-
return redirect(url)
|
|
516
|
-
|
|
517
|
-
return redirect(request.get_full_path())
|
|
518
|
-
|
|
519
|
-
return_url = form.cleaned_data.get("return_url")
|
|
520
|
-
if url_has_allowed_host_and_scheme(url=return_url, allowed_hosts=request.get_host()):
|
|
521
|
-
return redirect(iri_to_uri(return_url))
|
|
522
|
-
else:
|
|
523
|
-
return redirect(self.get_return_url(request, obj))
|
|
524
|
-
|
|
525
|
-
except ObjectDoesNotExist:
|
|
526
|
-
msg = "Object save failed due to object-level permissions violation"
|
|
527
|
-
logger.debug(msg)
|
|
528
|
-
form.add_error(None, msg)
|
|
529
|
-
|
|
530
|
-
else:
|
|
531
|
-
logger.debug("Form validation failed")
|
|
532
|
-
|
|
533
|
-
return render(
|
|
534
|
-
request,
|
|
535
|
-
self.template_name,
|
|
536
|
-
{
|
|
537
|
-
"obj": obj,
|
|
538
|
-
"obj_type": self.queryset.model._meta.verbose_name,
|
|
539
|
-
"form": form,
|
|
540
|
-
"return_url": self.get_return_url(request, obj),
|
|
541
|
-
"editing": obj.present_in_database,
|
|
542
|
-
**self.get_extra_context(request, obj),
|
|
543
|
-
},
|
|
544
|
-
)
|
|
545
|
-
|
|
546
493
|
|
|
547
494
|
class ObjectAssignContactOrTeamView(generic.ObjectEditView):
|
|
548
495
|
queryset = ContactAssociation.objects.all()
|
|
@@ -1045,6 +992,7 @@ class GitRepositoryView(generic.ObjectView):
|
|
|
1045
992
|
def get_extra_context(self, request, instance):
|
|
1046
993
|
return {
|
|
1047
994
|
"datasource_contents": get_datasource_contents("extras.gitrepository"),
|
|
995
|
+
**super().get_extra_context(request, instance),
|
|
1048
996
|
}
|
|
1049
997
|
|
|
1050
998
|
|
|
@@ -1250,7 +1198,10 @@ class JobListView(generic.ObjectListView):
|
|
|
1250
1198
|
filterset = filters.JobFilterSet
|
|
1251
1199
|
filterset_form = forms.JobFilterForm
|
|
1252
1200
|
action_buttons = ()
|
|
1253
|
-
non_filter_params = (
|
|
1201
|
+
non_filter_params = (
|
|
1202
|
+
*generic.ObjectListView.non_filter_params,
|
|
1203
|
+
"display",
|
|
1204
|
+
)
|
|
1254
1205
|
template_name = "extras/job_list.html"
|
|
1255
1206
|
|
|
1256
1207
|
def alter_queryset(self, request):
|
|
@@ -1305,11 +1256,9 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1305
1256
|
job_model = self._get_job_model_or_404(class_path, pk)
|
|
1306
1257
|
|
|
1307
1258
|
try:
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
# job_class may be None
|
|
1312
|
-
raise RuntimeError("Job code for this job is not currently installed or loadable") from exc
|
|
1259
|
+
job_class = get_job(job_model.class_path, reload=True)
|
|
1260
|
+
if job_class is None:
|
|
1261
|
+
raise RuntimeError("Job code for this job is not currently installed or loadable")
|
|
1313
1262
|
initial = normalize_querydict(request.GET, form_class=job_class.as_form_class())
|
|
1314
1263
|
if "kwargs_from_job_result" in initial:
|
|
1315
1264
|
job_result_pk = initial.pop("kwargs_from_job_result")
|
|
@@ -1357,7 +1306,8 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1357
1306
|
def post(self, request, class_path=None, pk=None):
|
|
1358
1307
|
job_model = self._get_job_model_or_404(class_path, pk)
|
|
1359
1308
|
|
|
1360
|
-
|
|
1309
|
+
job_class = get_job(job_model.class_path, reload=True)
|
|
1310
|
+
job_form = job_class.as_form(request.POST, request.FILES) if job_class is not None else None
|
|
1361
1311
|
schedule_form = forms.JobScheduleForm(request.POST)
|
|
1362
1312
|
task_queue = request.POST.get("_task_queue")
|
|
1363
1313
|
|
|
@@ -1370,7 +1320,7 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1370
1320
|
# Allow execution only if a worker process is running and the job is runnable.
|
|
1371
1321
|
if not get_worker_count(queue=task_queue):
|
|
1372
1322
|
messages.error(request, "Unable to run or schedule job: Celery worker process not running.")
|
|
1373
|
-
elif not job_model.installed or
|
|
1323
|
+
elif not job_model.installed or job_class is None:
|
|
1374
1324
|
messages.error(request, "Unable to run or schedule job: Job is not presently installed.")
|
|
1375
1325
|
elif not job_model.enabled:
|
|
1376
1326
|
messages.error(request, "Unable to run or schedule job: Job is not enabled to be run.")
|
|
@@ -1422,11 +1372,11 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1422
1372
|
celery_kwargs = {"nautobot_job_profile": profile, "queue": task_queue}
|
|
1423
1373
|
scheduled_job = ScheduledJob(
|
|
1424
1374
|
name=schedule_name,
|
|
1425
|
-
task=job_model.
|
|
1375
|
+
task=job_model.class_path,
|
|
1426
1376
|
job_model=job_model,
|
|
1427
1377
|
start_time=schedule_datetime,
|
|
1428
1378
|
description=f"Nautobot job {schedule_name} scheduled by {request.user} for {schedule_datetime}",
|
|
1429
|
-
kwargs=
|
|
1379
|
+
kwargs=job_class.serialize_data(job_form.cleaned_data),
|
|
1430
1380
|
celery_kwargs=celery_kwargs,
|
|
1431
1381
|
interval=schedule_type,
|
|
1432
1382
|
one_off=schedule_type == JobExecutionType.TYPE_FUTURE,
|
|
@@ -1446,13 +1396,13 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1446
1396
|
|
|
1447
1397
|
else:
|
|
1448
1398
|
# Enqueue job for immediate execution
|
|
1449
|
-
job_kwargs =
|
|
1399
|
+
job_kwargs = job_class.prepare_job_kwargs(job_form.cleaned_data)
|
|
1450
1400
|
job_result = JobResult.enqueue_job(
|
|
1451
1401
|
job_model,
|
|
1452
1402
|
request.user,
|
|
1453
1403
|
profile=profile,
|
|
1454
1404
|
task_queue=task_queue,
|
|
1455
|
-
**
|
|
1405
|
+
**job_class.serialize_data(job_kwargs),
|
|
1456
1406
|
)
|
|
1457
1407
|
|
|
1458
1408
|
if return_url:
|
|
@@ -1471,10 +1421,10 @@ class JobRunView(ObjectPermissionRequiredMixin, View):
|
|
|
1471
1421
|
return redirect(return_url)
|
|
1472
1422
|
|
|
1473
1423
|
template_name = "extras/job.html"
|
|
1474
|
-
if
|
|
1424
|
+
if job_class is not None and hasattr(job_class, "template_name"):
|
|
1475
1425
|
try:
|
|
1476
|
-
get_template(
|
|
1477
|
-
template_name =
|
|
1426
|
+
get_template(job_class.template_name)
|
|
1427
|
+
template_name = job_class.template_name
|
|
1478
1428
|
except TemplateDoesNotExist as err:
|
|
1479
1429
|
messages.error(request, f'Unable to render requested custom job template "{template_name}": {err}')
|
|
1480
1430
|
|
|
@@ -1555,7 +1505,7 @@ class JobApprovalRequestView(generic.ObjectView):
|
|
|
1555
1505
|
"""
|
|
1556
1506
|
job_model = instance.job_model
|
|
1557
1507
|
if job_model is not None:
|
|
1558
|
-
job_class = job_model.
|
|
1508
|
+
job_class = get_job(job_model.class_path, reload=True)
|
|
1559
1509
|
else:
|
|
1560
1510
|
# 2.0 TODO: remove this fallback?
|
|
1561
1511
|
job_class = get_job(instance.job_class)
|
|
@@ -1569,9 +1519,7 @@ class JobApprovalRequestView(generic.ObjectView):
|
|
|
1569
1519
|
else:
|
|
1570
1520
|
job_form = None
|
|
1571
1521
|
|
|
1572
|
-
return {
|
|
1573
|
-
"job_form": job_form,
|
|
1574
|
-
}
|
|
1522
|
+
return {"job_form": job_form, **super().get_extra_context(request, instance)}
|
|
1575
1523
|
|
|
1576
1524
|
def post(self, request, pk):
|
|
1577
1525
|
"""
|
|
@@ -1591,6 +1539,7 @@ class JobApprovalRequestView(generic.ObjectView):
|
|
|
1591
1539
|
dry_run = "_dry_run" in post_data
|
|
1592
1540
|
|
|
1593
1541
|
job_model = scheduled_job.job_model
|
|
1542
|
+
job_class = get_job(job_model.class_path, reload=True)
|
|
1594
1543
|
|
|
1595
1544
|
if dry_run:
|
|
1596
1545
|
# To dry-run a job, a user needs the same permissions that would be needed to run the job directly
|
|
@@ -1604,13 +1553,13 @@ class JobApprovalRequestView(generic.ObjectView):
|
|
|
1604
1553
|
messages.error(request, "This job does not support dryrun")
|
|
1605
1554
|
else:
|
|
1606
1555
|
# Immediately enqueue the job and send the user to the normal JobResult view
|
|
1607
|
-
job_kwargs =
|
|
1556
|
+
job_kwargs = job_class.prepare_job_kwargs(scheduled_job.kwargs or {})
|
|
1608
1557
|
job_kwargs["dryrun"] = True
|
|
1609
1558
|
job_result = JobResult.enqueue_job(
|
|
1610
1559
|
job_model,
|
|
1611
1560
|
request.user,
|
|
1612
1561
|
celery_kwargs=scheduled_job.celery_kwargs,
|
|
1613
|
-
**
|
|
1562
|
+
**job_class.serialize_data(job_kwargs),
|
|
1614
1563
|
)
|
|
1615
1564
|
|
|
1616
1565
|
return redirect("extras:jobresult", pk=job_result.pk)
|
|
@@ -1697,10 +1646,14 @@ class ScheduledJobView(generic.ObjectView):
|
|
|
1697
1646
|
for name, var in job_class._get_vars().items():
|
|
1698
1647
|
field = var.as_field()
|
|
1699
1648
|
if field.label:
|
|
1700
|
-
labels[name] =
|
|
1649
|
+
labels[name] = field.label
|
|
1701
1650
|
else:
|
|
1702
1651
|
labels[name] = pretty_name(name)
|
|
1703
|
-
return {
|
|
1652
|
+
return {
|
|
1653
|
+
"labels": labels,
|
|
1654
|
+
"job_class_found": (job_class is not None),
|
|
1655
|
+
**super().get_extra_context(request, instance),
|
|
1656
|
+
}
|
|
1704
1657
|
|
|
1705
1658
|
|
|
1706
1659
|
class ScheduledJobDeleteView(generic.ObjectDeleteView):
|
|
@@ -1724,7 +1677,10 @@ class JobHookView(generic.ObjectView):
|
|
|
1724
1677
|
queryset = JobHook.objects.all()
|
|
1725
1678
|
|
|
1726
1679
|
def get_extra_context(self, request, instance):
|
|
1727
|
-
return {
|
|
1680
|
+
return {
|
|
1681
|
+
"content_types": instance.content_types.order_by("app_label", "model"),
|
|
1682
|
+
**super().get_extra_context(request, instance),
|
|
1683
|
+
}
|
|
1728
1684
|
|
|
1729
1685
|
|
|
1730
1686
|
class JobHookEditView(generic.ObjectEditView):
|
|
@@ -1809,6 +1765,7 @@ class JobResultView(generic.ObjectView):
|
|
|
1809
1765
|
"job": job_class,
|
|
1810
1766
|
"associated_record": associated_record,
|
|
1811
1767
|
"result": instance,
|
|
1768
|
+
**super().get_extra_context(request, instance),
|
|
1812
1769
|
}
|
|
1813
1770
|
|
|
1814
1771
|
|
|
@@ -1896,6 +1853,7 @@ class ObjectChangeView(generic.ObjectView):
|
|
|
1896
1853
|
"prev_change": instance.get_prev_change(request.user),
|
|
1897
1854
|
"related_changes_table": related_changes_table,
|
|
1898
1855
|
"related_changes_count": related_changes.count(),
|
|
1856
|
+
**super().get_extra_context(request, instance),
|
|
1899
1857
|
}
|
|
1900
1858
|
|
|
1901
1859
|
|
|
@@ -2240,6 +2198,7 @@ class SecretView(generic.ObjectView):
|
|
|
2240
2198
|
"format": format_,
|
|
2241
2199
|
"provider_name": provider.name if provider else instance.provider,
|
|
2242
2200
|
"groups_table": groups_table,
|
|
2201
|
+
**super().get_extra_context(request, instance),
|
|
2243
2202
|
}
|
|
2244
2203
|
|
|
2245
2204
|
|
|
@@ -2455,7 +2414,10 @@ class StatusView(generic.ObjectView):
|
|
|
2455
2414
|
|
|
2456
2415
|
def get_extra_context(self, request, instance):
|
|
2457
2416
|
"""Return ordered content types."""
|
|
2458
|
-
return {
|
|
2417
|
+
return {
|
|
2418
|
+
"content_types": instance.content_types.order_by("app_label", "model"),
|
|
2419
|
+
**super().get_extra_context(request, instance),
|
|
2420
|
+
}
|
|
2459
2421
|
|
|
2460
2422
|
|
|
2461
2423
|
#
|
|
@@ -2490,6 +2452,7 @@ class TagView(generic.ObjectView):
|
|
|
2490
2452
|
"items_count": tagged_items.count(),
|
|
2491
2453
|
"items_table": items_table,
|
|
2492
2454
|
"content_types": instance.content_types.order_by("app_label", "model"),
|
|
2455
|
+
**super().get_extra_context(request, instance),
|
|
2493
2456
|
}
|
|
2494
2457
|
|
|
2495
2458
|
|
|
@@ -2570,7 +2533,10 @@ class WebhookView(generic.ObjectView):
|
|
|
2570
2533
|
queryset = Webhook.objects.all()
|
|
2571
2534
|
|
|
2572
2535
|
def get_extra_context(self, request, instance):
|
|
2573
|
-
return {
|
|
2536
|
+
return {
|
|
2537
|
+
"content_types": instance.content_types.order_by("app_label", "model"),
|
|
2538
|
+
**super().get_extra_context(request, instance),
|
|
2539
|
+
}
|
|
2574
2540
|
|
|
2575
2541
|
|
|
2576
2542
|
class WebhookEditView(generic.ObjectEditView):
|
nautobot/ipam/api/views.py
CHANGED
|
@@ -160,7 +160,13 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
160
160
|
|
|
161
161
|
@extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
|
162
162
|
@extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=False)})
|
|
163
|
-
@action(
|
|
163
|
+
@action(
|
|
164
|
+
detail=True,
|
|
165
|
+
name="Available Prefixes",
|
|
166
|
+
url_path="available-prefixes",
|
|
167
|
+
methods=["get", "post"],
|
|
168
|
+
filterset_class=None,
|
|
169
|
+
)
|
|
164
170
|
def available_prefixes(self, request, pk=None):
|
|
165
171
|
"""
|
|
166
172
|
A convenience method for returning available child prefixes within a parent.
|
|
@@ -237,6 +243,7 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
237
243
|
)
|
|
238
244
|
@action(
|
|
239
245
|
detail=True,
|
|
246
|
+
name="Available IPs",
|
|
240
247
|
url_path="available-ips",
|
|
241
248
|
methods=["get", "post"],
|
|
242
249
|
queryset=IPAddress.objects.all(),
|
nautobot/ipam/graphql/types.py
CHANGED
|
@@ -26,6 +26,7 @@ class PrefixType(OptimizedNautobotObjectType):
|
|
|
26
26
|
prefix = graphene.String()
|
|
27
27
|
ip_version = graphene.Int()
|
|
28
28
|
dynamic_groups = graphene.List("nautobot.extras.graphql.types.DynamicGroupType")
|
|
29
|
+
location = graphene.Field("nautobot.dcim.graphql.types.LocationType")
|
|
29
30
|
|
|
30
31
|
class Meta:
|
|
31
32
|
model = models.Prefix
|
|
@@ -33,3 +34,13 @@ class PrefixType(OptimizedNautobotObjectType):
|
|
|
33
34
|
|
|
34
35
|
def resolve_dynamic_groups(self, args):
|
|
35
36
|
return DynamicGroup.objects.get_for_object(self, use_cache=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class VLANType(OptimizedNautobotObjectType):
|
|
40
|
+
"""Graphql Type Object for VLAN model."""
|
|
41
|
+
|
|
42
|
+
location = graphene.Field("nautobot.dcim.graphql.types.LocationType")
|
|
43
|
+
|
|
44
|
+
class Meta:
|
|
45
|
+
model = models.VLAN
|
|
46
|
+
filterset_class = filters.VLANFilterSet
|
nautobot/ipam/mixins.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class LocationToLocationsQuerySetMixin:
|
|
2
|
+
"""
|
|
3
|
+
A mixin for Django QuerySets to support backward compatibility by converting
|
|
4
|
+
queries from a previously used 'location' field to the new
|
|
5
|
+
'locations'. This mixin intercepts `filter` and `exclude` calls
|
|
6
|
+
to transform references from 'location' to 'locations'.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def _convert_location_to_locations(self, kwargs):
|
|
10
|
+
"""Transforms query parameters that reference 'location' field into the corresponding 'locations' field."""
|
|
11
|
+
updated_kwargs = {}
|
|
12
|
+
for field, value in kwargs.items():
|
|
13
|
+
if field == "location":
|
|
14
|
+
# If there is no lookup expression, it means 'location' is queried directly,
|
|
15
|
+
# thus use 'locations__in' to accommodate the ManyToMany relationship
|
|
16
|
+
updated_kwargs["locations__in"] = [value]
|
|
17
|
+
elif field.startswith("location__"):
|
|
18
|
+
# If there is a lookup expression following 'location', prepend it with 'locations'
|
|
19
|
+
_, lookup_expr = field.split("location", maxsplit=1)
|
|
20
|
+
locations_field = f"locations{lookup_expr}".strip()
|
|
21
|
+
updated_kwargs[locations_field] = value
|
|
22
|
+
else:
|
|
23
|
+
updated_kwargs[field] = value
|
|
24
|
+
return updated_kwargs
|
|
25
|
+
|
|
26
|
+
def filter(self, *args, **kwargs):
|
|
27
|
+
kwargs = self._convert_location_to_locations(kwargs)
|
|
28
|
+
return super().filter(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
def exclude(self, *args, **kwargs):
|
|
31
|
+
kwargs = self._convert_location_to_locations(kwargs)
|
|
32
|
+
return super().exclude(*args, **kwargs)
|
nautobot/ipam/models.py
CHANGED
|
@@ -22,7 +22,7 @@ from nautobot.ipam import choices, constants
|
|
|
22
22
|
from nautobot.virtualization.models import VMInterface
|
|
23
23
|
|
|
24
24
|
from .fields import VarbinaryIPField
|
|
25
|
-
from .querysets import IPAddressQuerySet, PrefixQuerySet, RIRQuerySet
|
|
25
|
+
from .querysets import IPAddressQuerySet, PrefixQuerySet, RIRQuerySet, VLANQuerySet
|
|
26
26
|
from .validators import DNSValidator
|
|
27
27
|
|
|
28
28
|
__all__ = (
|
|
@@ -1380,6 +1380,7 @@ class VLAN(PrimaryModel):
|
|
|
1380
1380
|
]
|
|
1381
1381
|
|
|
1382
1382
|
natural_key_field_names = ["pk"]
|
|
1383
|
+
objects = BaseManager.from_queryset(VLANQuerySet)()
|
|
1383
1384
|
|
|
1384
1385
|
class Meta:
|
|
1385
1386
|
ordering = (
|
nautobot/ipam/querysets.py
CHANGED
|
@@ -6,6 +6,7 @@ import netaddr
|
|
|
6
6
|
|
|
7
7
|
from nautobot.core.models.querysets import RestrictedQuerySet
|
|
8
8
|
from nautobot.core.utils.data import merge_dicts_without_collision
|
|
9
|
+
from nautobot.ipam.mixins import LocationToLocationsQuerySetMixin
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class RIRQuerySet(RestrictedQuerySet):
|
|
@@ -194,7 +195,7 @@ class BaseNetworkQuerySet(RestrictedQuerySet):
|
|
|
194
195
|
return ip, last_ip
|
|
195
196
|
|
|
196
197
|
|
|
197
|
-
class PrefixQuerySet(BaseNetworkQuerySet):
|
|
198
|
+
class PrefixQuerySet(LocationToLocationsQuerySetMixin, BaseNetworkQuerySet):
|
|
198
199
|
"""Queryset for `Prefix` objects."""
|
|
199
200
|
|
|
200
201
|
def net_equals(self, *prefixes):
|
|
@@ -474,3 +475,7 @@ class IPAddressQuerySet(BaseNetworkQuerySet):
|
|
|
474
475
|
q |= Q(pk__in=pk_values)
|
|
475
476
|
|
|
476
477
|
return super().filter(q)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class VLANQuerySet(LocationToLocationsQuerySetMixin, RestrictedQuerySet):
|
|
481
|
+
"""Queryset for `VLAN` objects."""
|
|
@@ -295,6 +295,50 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
295
295
|
self.child1 = Prefix.objects.create(prefix="101.102.0.0/26", status=self.status, namespace=self.namespace)
|
|
296
296
|
self.child2 = Prefix.objects.create(prefix="101.102.0.64/26", status=self.status, namespace=self.namespace)
|
|
297
297
|
|
|
298
|
+
def test_location_queries(self):
|
|
299
|
+
locations = Location.objects.all()[:4]
|
|
300
|
+
for location in locations:
|
|
301
|
+
location.location_type.content_types.add(ContentType.objects.get_for_model(Prefix))
|
|
302
|
+
for i in range(10):
|
|
303
|
+
pfx = Prefix.objects.create(prefix=f"1.1.1.{4*i}/30", status=self.status, namespace=self.namespace)
|
|
304
|
+
if i > 4:
|
|
305
|
+
pfx.locations.set(locations)
|
|
306
|
+
|
|
307
|
+
with self.subTest("Assert filtering and excluding `location`"):
|
|
308
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
309
|
+
Prefix.objects.filter(location=locations[0]),
|
|
310
|
+
Prefix.objects.filter(locations__in=[locations[0]]),
|
|
311
|
+
)
|
|
312
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
313
|
+
Prefix.objects.exclude(location=locations[0]),
|
|
314
|
+
Prefix.objects.exclude(locations__in=[locations[0]]),
|
|
315
|
+
)
|
|
316
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
317
|
+
Prefix.objects.filter(location__in=[locations[0]]),
|
|
318
|
+
Prefix.objects.filter(locations__in=[locations[0]]),
|
|
319
|
+
)
|
|
320
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
321
|
+
Prefix.objects.exclude(location__in=[locations[0]]),
|
|
322
|
+
Prefix.objects.exclude(locations__in=[locations[0]]),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# We use `assertQuerysetEqualAndNotEmpty` for test validation. Including a nullable field could lead
|
|
326
|
+
# to flaky tests where querysets might return None, causing tests to fail. Therefore, we select
|
|
327
|
+
# fields that consistently contain values to ensure reliable filtering.
|
|
328
|
+
query_params = ["name", "location_type", "status"]
|
|
329
|
+
|
|
330
|
+
for field_name in query_params:
|
|
331
|
+
with self.subTest(f"Assert location__{field_name} query."):
|
|
332
|
+
value = getattr(locations[0], field_name)
|
|
333
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
334
|
+
Prefix.objects.filter(**{f"location__{field_name}": value}),
|
|
335
|
+
Prefix.objects.filter(**{f"locations__{field_name}": value}),
|
|
336
|
+
)
|
|
337
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
338
|
+
Prefix.objects.exclude(**{f"location__{field_name}": value}),
|
|
339
|
+
Prefix.objects.exclude(**{f"locations__{field_name}": value}),
|
|
340
|
+
)
|
|
341
|
+
|
|
298
342
|
def test_prefix_validation(self):
|
|
299
343
|
location_type = LocationType.objects.get(name="Room")
|
|
300
344
|
location = Location.objects.filter(location_type=location_type).first()
|
|
@@ -1201,6 +1245,44 @@ class TestVLAN(ModelTestCases.BaseModelTestCase):
|
|
|
1201
1245
|
location.vlans.add(vlan)
|
|
1202
1246
|
self.assertIn(f"{location} is a Floor and may not have VLANs associated to it.", str(cm.exception))
|
|
1203
1247
|
|
|
1248
|
+
def test_location_queries(self):
|
|
1249
|
+
location = VLAN.objects.filter(locations__isnull=False).first().locations.first()
|
|
1250
|
+
|
|
1251
|
+
with self.subTest("Assert filtering and excluding `location`"):
|
|
1252
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1253
|
+
VLAN.objects.filter(location=location),
|
|
1254
|
+
VLAN.objects.filter(locations__in=[location]),
|
|
1255
|
+
)
|
|
1256
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1257
|
+
VLAN.objects.exclude(location=location),
|
|
1258
|
+
VLAN.objects.exclude(locations__in=[location]),
|
|
1259
|
+
)
|
|
1260
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1261
|
+
VLAN.objects.filter(location__in=[location]),
|
|
1262
|
+
VLAN.objects.filter(locations__in=[location]),
|
|
1263
|
+
)
|
|
1264
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1265
|
+
VLAN.objects.exclude(location__in=[location]),
|
|
1266
|
+
VLAN.objects.exclude(locations__in=[location]),
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
# We use `assertQuerysetEqualAndNotEmpty` for test validation. Including a nullable field could lead
|
|
1270
|
+
# to flaky tests where querysets might return None, causing tests to fail. Therefore, we select
|
|
1271
|
+
# fields that consistently contain values to ensure reliable filtering.
|
|
1272
|
+
query_params = ["name", "location_type", "status"]
|
|
1273
|
+
|
|
1274
|
+
for field_name in query_params:
|
|
1275
|
+
with self.subTest(f"Assert location__{field_name} query."):
|
|
1276
|
+
value = getattr(location, field_name)
|
|
1277
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1278
|
+
VLAN.objects.filter(**{f"location__{field_name}": value}),
|
|
1279
|
+
VLAN.objects.filter(**{f"locations__{field_name}": value}),
|
|
1280
|
+
)
|
|
1281
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1282
|
+
VLAN.objects.exclude(**{f"location__{field_name}": value}),
|
|
1283
|
+
VLAN.objects.exclude(**{f"locations__{field_name}": value}),
|
|
1284
|
+
)
|
|
1285
|
+
|
|
1204
1286
|
|
|
1205
1287
|
class TestVRF(ModelTestCases.BaseModelTestCase):
|
|
1206
1288
|
model = VRF
|
nautobot/ipam/views.py
CHANGED
|
@@ -332,6 +332,7 @@ class RouteTargetView(generic.ObjectView):
|
|
|
332
332
|
return {
|
|
333
333
|
"importing_vrfs_table": importing_vrfs_table,
|
|
334
334
|
"exporting_vrfs_table": exporting_vrfs_table,
|
|
335
|
+
**super().get_extra_context(request, instance),
|
|
335
336
|
}
|
|
336
337
|
|
|
337
338
|
|
|
@@ -389,9 +390,7 @@ class RIRView(generic.ObjectView):
|
|
|
389
390
|
}
|
|
390
391
|
RequestConfig(request, paginate).configure(assigned_prefix_table)
|
|
391
392
|
|
|
392
|
-
return {
|
|
393
|
-
"assigned_prefix_table": assigned_prefix_table,
|
|
394
|
-
}
|
|
393
|
+
return {"assigned_prefix_table": assigned_prefix_table, **super().get_extra_context(request, instance)}
|
|
395
394
|
|
|
396
395
|
|
|
397
396
|
class RIREditView(generic.ObjectEditView):
|
|
@@ -457,6 +456,7 @@ class PrefixView(generic.ObjectView):
|
|
|
457
456
|
return {
|
|
458
457
|
"vrf_table": vrf_table,
|
|
459
458
|
"parent_prefix_table": parent_prefix_table,
|
|
459
|
+
**super().get_extra_context(request, instance),
|
|
460
460
|
}
|
|
461
461
|
|
|
462
462
|
|
|
@@ -780,6 +780,7 @@ class IPAddressView(generic.ObjectView):
|
|
|
780
780
|
return {
|
|
781
781
|
"parent_prefixes_table": parent_prefixes_table,
|
|
782
782
|
"related_ips_table": related_ips_table,
|
|
783
|
+
**super().get_extra_context(request, instance),
|
|
783
784
|
}
|
|
784
785
|
|
|
785
786
|
|
|
@@ -1261,6 +1262,7 @@ class VLANGroupView(generic.ObjectView):
|
|
|
1261
1262
|
"vlan_table": vlan_table,
|
|
1262
1263
|
"permissions": permissions,
|
|
1263
1264
|
"vlans_count": vlans_count,
|
|
1265
|
+
**super().get_extra_context(request, instance),
|
|
1264
1266
|
}
|
|
1265
1267
|
|
|
1266
1268
|
|
|
@@ -1317,9 +1319,7 @@ class VLANView(generic.ObjectView):
|
|
|
1317
1319
|
prefix_table = tables.PrefixTable(list(prefixes))
|
|
1318
1320
|
prefix_table.exclude = ("vlan",)
|
|
1319
1321
|
|
|
1320
|
-
return {
|
|
1321
|
-
"prefix_table": prefix_table,
|
|
1322
|
-
}
|
|
1322
|
+
return {"prefix_table": prefix_table, **super().get_extra_context(request, instance)}
|
|
1323
1323
|
|
|
1324
1324
|
|
|
1325
1325
|
class VLANInterfacesView(generic.ObjectView):
|