nautobot 2.3.8__py3-none-any.whl → 2.3.10__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/tables.py +2 -0
- nautobot/core/forms/__init__.py +4 -0
- nautobot/core/forms/fields.py +32 -0
- nautobot/core/jobs/__init__.py +24 -8
- nautobot/core/models/query_functions.py +147 -1
- nautobot/core/models/tree_queries.py +8 -0
- nautobot/core/settings.py +7 -0
- nautobot/core/settings.yaml +10 -0
- nautobot/core/signals.py +5 -4
- nautobot/core/templates/nautobot_config.py.j2 +4 -0
- nautobot/core/tests/test_models_query_functions.py +108 -0
- nautobot/dcim/forms.py +30 -27
- nautobot/dcim/models/device_components.py +5 -0
- nautobot/dcim/tables/devices.py +4 -2
- nautobot/dcim/templates/dcim/modulebay_create.html +39 -0
- nautobot/dcim/templates/dcim/modulebay_update.html +39 -0
- nautobot/dcim/tests/test_models.py +16 -0
- nautobot/dcim/views.py +1 -1
- nautobot/extras/api/customfields.py +3 -10
- nautobot/extras/context_managers.py +28 -9
- nautobot/extras/datasources/__init__.py +2 -0
- nautobot/extras/datasources/git.py +30 -49
- nautobot/extras/datasources/registry.py +2 -2
- nautobot/extras/jobs.py +30 -12
- nautobot/extras/models/customfields.py +12 -0
- nautobot/extras/models/datasources.py +6 -0
- nautobot/extras/models/groups.py +47 -33
- nautobot/extras/models/jobs.py +1 -1
- nautobot/extras/plugins/__init__.py +165 -0
- nautobot/extras/signals.py +2 -0
- nautobot/extras/tasks.py +88 -69
- nautobot/extras/templates/extras/plugin_detail.html +33 -0
- nautobot/extras/tests/test_context_managers.py +23 -9
- nautobot/extras/tests/test_datasources.py +88 -1
- nautobot/extras/tests/test_dynamicgroups.py +12 -0
- nautobot/extras/tests/test_plugins.py +94 -0
- nautobot/extras/tests/test_webhooks.py +1 -1
- nautobot/extras/views.py +3 -1
- nautobot/extras/webhooks.py +16 -7
- nautobot/project-static/docs/404.html +24 -3
- nautobot/project-static/docs/apps/index.html +24 -3
- nautobot/project-static/docs/apps/nautobot-apps.html +24 -3
- nautobot/project-static/docs/assets/javascripts/{bundle.525ec568.min.js → bundle.83f73b43.min.js} +2 -2
- nautobot/project-static/docs/assets/javascripts/{bundle.525ec568.min.js.map → bundle.83f73b43.min.js.map} +3 -3
- nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.0253249f.min.css.map +1 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +106 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +138 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +24 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +24 -3
- nautobot/project-static/docs/development/apps/api/configuration-view.html +24 -3
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +24 -3
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +24 -3
- nautobot/project-static/docs/development/apps/api/models/global-search.html +24 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +24 -3
- nautobot/project-static/docs/development/apps/api/models/index.html +24 -3
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +24 -3
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +24 -3
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +24 -3
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +24 -3
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +24 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +24 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +24 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +24 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +27 -6
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +8823 -0
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +27 -6
- nautobot/project-static/docs/development/apps/api/prometheus.html +24 -3
- nautobot/project-static/docs/development/apps/api/setup.html +33 -11
- nautobot/project-static/docs/development/apps/api/testing.html +24 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +24 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +24 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +24 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +24 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/base-template.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/index.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/notes.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +24 -3
- nautobot/project-static/docs/development/apps/api/views/urls.html +24 -3
- nautobot/project-static/docs/development/apps/index.html +24 -3
- nautobot/project-static/docs/development/apps/migration/code-updates.html +24 -3
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +24 -3
- nautobot/project-static/docs/development/apps/migration/from-v1.html +24 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +24 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +24 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +24 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +24 -3
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +24 -3
- nautobot/project-static/docs/development/core/application-registry.html +24 -3
- nautobot/project-static/docs/development/core/best-practices.html +24 -3
- nautobot/project-static/docs/development/core/bootstrap-ui.html +24 -3
- nautobot/project-static/docs/development/core/caching.html +24 -3
- nautobot/project-static/docs/development/core/controllers.html +24 -3
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +24 -3
- nautobot/project-static/docs/development/core/generic-views.html +24 -3
- nautobot/project-static/docs/development/core/getting-started.html +24 -3
- nautobot/project-static/docs/development/core/homepage.html +24 -3
- nautobot/project-static/docs/development/core/index.html +24 -3
- nautobot/project-static/docs/development/core/model-checklist.html +24 -3
- nautobot/project-static/docs/development/core/model-features.html +24 -3
- nautobot/project-static/docs/development/core/natural-keys.html +24 -3
- nautobot/project-static/docs/development/core/navigation-menu.html +24 -3
- nautobot/project-static/docs/development/core/release-checklist.html +24 -3
- nautobot/project-static/docs/development/core/role-internals.html +24 -3
- nautobot/project-static/docs/development/core/settings.html +24 -3
- nautobot/project-static/docs/development/core/style-guide.html +24 -3
- nautobot/project-static/docs/development/core/templates.html +24 -3
- nautobot/project-static/docs/development/core/testing.html +24 -3
- nautobot/project-static/docs/development/core/user-preferences.html +24 -3
- nautobot/project-static/docs/development/index.html +24 -3
- nautobot/project-static/docs/development/jobs/index.html +24 -3
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +24 -3
- nautobot/project-static/docs/index.html +24 -3
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +24 -3
- nautobot/project-static/docs/overview/design_philosophy.html +24 -3
- nautobot/project-static/docs/release-notes/index.html +24 -3
- nautobot/project-static/docs/release-notes/version-1.0.html +24 -3
- nautobot/project-static/docs/release-notes/version-1.1.html +24 -3
- nautobot/project-static/docs/release-notes/version-1.2.html +24 -3
- nautobot/project-static/docs/release-notes/version-1.3.html +24 -3
- nautobot/project-static/docs/release-notes/version-1.4.html +24 -3
- nautobot/project-static/docs/release-notes/version-1.5.html +24 -3
- nautobot/project-static/docs/release-notes/version-1.6.html +24 -3
- nautobot/project-static/docs/release-notes/version-2.0.html +24 -3
- nautobot/project-static/docs/release-notes/version-2.1.html +24 -3
- nautobot/project-static/docs/release-notes/version-2.2.html +24 -3
- nautobot/project-static/docs/release-notes/version-2.3.html +430 -114
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +273 -269
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +24 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +24 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +24 -3
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +24 -3
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +24 -3
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +29 -4
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +24 -3
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +24 -3
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +24 -3
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +24 -3
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +24 -3
- nautobot/project-static/docs/user-guide/administration/installation/index.html +24 -3
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +24 -3
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +24 -3
- nautobot/project-static/docs/user-guide/administration/installation/services.html +24 -3
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +24 -3
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +24 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +24 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +24 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +24 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +24 -3
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +24 -3
- nautobot/project-static/docs/user-guide/index.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +24 -3
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +24 -3
- nautobot/project-static/js/forms.js +3 -5
- nautobot/virtualization/tables.py +1 -1
- {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/METADATA +3 -3
- {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/RECORD +327 -323
- nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +0 -1
- {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/NOTICE +0 -0
- {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/WHEEL +0 -0
- {nautobot-2.3.8.dist-info → nautobot-2.3.10.dist-info}/entry_points.txt +0 -0
|
@@ -94,6 +94,7 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
94
94
|
metrics = "metrics.metrics"
|
|
95
95
|
menu_items = "navigation.menu_items"
|
|
96
96
|
secrets_providers = "secrets.secrets_providers"
|
|
97
|
+
table_extensions = "table_extensions.table_extensions"
|
|
97
98
|
template_extensions = "template_content.template_extensions"
|
|
98
99
|
override_views = "views.override_views"
|
|
99
100
|
|
|
@@ -211,6 +212,9 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
211
212
|
)
|
|
212
213
|
register_override_views(override_views, self.name)
|
|
213
214
|
|
|
215
|
+
# Register tables extensions (if any).
|
|
216
|
+
self._register_table_extensions()
|
|
217
|
+
|
|
214
218
|
@classmethod
|
|
215
219
|
def validate(cls, user_config, nautobot_version):
|
|
216
220
|
"""Validate the user_config for baseline correctness."""
|
|
@@ -262,6 +266,13 @@ class NautobotAppConfig(NautobotConfig):
|
|
|
262
266
|
if setting not in user_config and setting not in cls.constance_config:
|
|
263
267
|
user_config[setting] = value
|
|
264
268
|
|
|
269
|
+
def _register_table_extensions(self):
|
|
270
|
+
"""Register tables extensions (if any)."""
|
|
271
|
+
table_extensions = import_object(f"{self.__module__}.{self.table_extensions}")
|
|
272
|
+
if table_extensions is not None:
|
|
273
|
+
register_table_extensions(table_extensions, self.name)
|
|
274
|
+
self.features["table_extensions"] = get_table_extension_features(table_extensions)
|
|
275
|
+
|
|
265
276
|
|
|
266
277
|
@class_deprecated_in_favor_of(NautobotAppConfig)
|
|
267
278
|
class PluginConfig(NautobotAppConfig):
|
|
@@ -491,6 +502,160 @@ def register_filter_extensions(filter_extensions, plugin_name):
|
|
|
491
502
|
)
|
|
492
503
|
|
|
493
504
|
|
|
505
|
+
#
|
|
506
|
+
# Table Extensions
|
|
507
|
+
#
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class TableExtension:
|
|
511
|
+
"""Template class for extending Tables.
|
|
512
|
+
|
|
513
|
+
An app can override the default columns for a table by either:
|
|
514
|
+
- Extending the original default columns to include custom columns.
|
|
515
|
+
- add_to_default_columns = ("my_app_name_new_column",)
|
|
516
|
+
- Removing native columns from the default columns.
|
|
517
|
+
- remove_from_default_columns = ("tenant",)
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
model = None
|
|
521
|
+
table_columns = {}
|
|
522
|
+
add_to_default_columns = ()
|
|
523
|
+
remove_from_default_columns = ()
|
|
524
|
+
|
|
525
|
+
@classmethod
|
|
526
|
+
def alter_queryset(cls, queryset):
|
|
527
|
+
"""Alter the View class QuerySet.
|
|
528
|
+
|
|
529
|
+
This is a good place to add `prefetch_related` to the view queryset.
|
|
530
|
+
example:
|
|
531
|
+
return queryset.prefetch_related("my_model_set")
|
|
532
|
+
"""
|
|
533
|
+
return queryset
|
|
534
|
+
|
|
535
|
+
@classmethod
|
|
536
|
+
def _get_table_columns_registrations(cls):
|
|
537
|
+
"""Return a list of register labels fro each column."""
|
|
538
|
+
if not cls.table_columns:
|
|
539
|
+
return []
|
|
540
|
+
return [f"{cls.model} -> {column_name}" for column_name in cls.table_columns]
|
|
541
|
+
|
|
542
|
+
@classmethod
|
|
543
|
+
def _get_add_to_default_columns_registrations(cls):
|
|
544
|
+
"""Return a list of register labels for each column added to defaults."""
|
|
545
|
+
if not cls.add_to_default_columns:
|
|
546
|
+
return []
|
|
547
|
+
return [f"{cls.model} -> {cls.add_to_default_columns}"]
|
|
548
|
+
|
|
549
|
+
@classmethod
|
|
550
|
+
def _get_remove_from_default_columns_registrations(cls):
|
|
551
|
+
"""Return a list of register labels for each column removed from defaults."""
|
|
552
|
+
if not cls.remove_from_default_columns:
|
|
553
|
+
return []
|
|
554
|
+
return [f"{cls.model} -> {cls.remove_from_default_columns}"]
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def get_table_extension_features(table_extensions):
|
|
558
|
+
"""Return a dictionary of TableExtension features for the App detail view."""
|
|
559
|
+
return {
|
|
560
|
+
"columns": [
|
|
561
|
+
label
|
|
562
|
+
for table_extension in table_extensions
|
|
563
|
+
for label in table_extension._get_table_columns_registrations()
|
|
564
|
+
],
|
|
565
|
+
"add_to_default_columns": [
|
|
566
|
+
label
|
|
567
|
+
for table_extension in table_extensions
|
|
568
|
+
for label in table_extension._get_add_to_default_columns_registrations()
|
|
569
|
+
],
|
|
570
|
+
"remove_from_default_columns": [
|
|
571
|
+
label
|
|
572
|
+
for table_extension in table_extensions
|
|
573
|
+
for label in table_extension._get_remove_from_default_columns_registrations()
|
|
574
|
+
],
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def register_table_extensions(table_extensions, app_name):
|
|
579
|
+
"""Register a list of TableExtension classes."""
|
|
580
|
+
for table_extension in table_extensions:
|
|
581
|
+
_validate_is_subclass_of_table_extension(table_extension)
|
|
582
|
+
_add_columns_into_model_table(table_extension, app_name)
|
|
583
|
+
_modify_default_table_columns(table_extension, app_name)
|
|
584
|
+
_alter_table_view_queryset(table_extension, app_name)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _add_columns_into_model_table(table_extension, app_name):
|
|
588
|
+
"""Inject each new column into the Model Table."""
|
|
589
|
+
from nautobot.core.utils.lookup import get_table_for_model
|
|
590
|
+
|
|
591
|
+
if not isinstance(table_extension.table_columns, dict):
|
|
592
|
+
error = f"{app_name} TableExtension: 'table_columns' attribute must be of type 'dict'."
|
|
593
|
+
logger.error(error)
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
table = get_table_for_model(table_extension.model)
|
|
597
|
+
for name, column in table_extension.table_columns.items():
|
|
598
|
+
_validate_table_column_name_is_prefixed_with_app_name(name, app_name)
|
|
599
|
+
_add_column_to_table_base_columns(table, name, column, app_name)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _add_column_to_table_base_columns(table, column_name, column, app_name):
|
|
603
|
+
"""Attach a column to an existing table."""
|
|
604
|
+
import django_tables2
|
|
605
|
+
|
|
606
|
+
if not isinstance(column, django_tables2.Column):
|
|
607
|
+
raise TypeError(f"Custom column `{column_name}` is not an instance of django_tables2.Column.")
|
|
608
|
+
|
|
609
|
+
if column_name in table.base_columns:
|
|
610
|
+
logger.error(
|
|
611
|
+
f"{app_name}: There was a name conflict with existing table column `{column_name}`, the custom column was ignored."
|
|
612
|
+
)
|
|
613
|
+
else:
|
|
614
|
+
table.base_columns[column_name] = column
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _alter_table_view_queryset(table_extension, app_name):
|
|
618
|
+
"""Replace the model view queryset with an optimized queryset from the app."""
|
|
619
|
+
from nautobot.core.utils.lookup import get_view_for_model
|
|
620
|
+
|
|
621
|
+
# TODO: Investigate if there is a more targeted way to patch only the list view queryset
|
|
622
|
+
# when targeting a subclass of `NautobotUIViewSet`.
|
|
623
|
+
view = get_view_for_model(table_extension.model, view_type="List")
|
|
624
|
+
view.queryset = table_extension.alter_queryset(view.queryset)
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _modify_default_table_columns(table_extension, app_name):
|
|
628
|
+
"""Add or remove columns from the table default columns."""
|
|
629
|
+
from nautobot.core.utils.lookup import get_table_for_model
|
|
630
|
+
|
|
631
|
+
table = get_table_for_model(table_extension.model)
|
|
632
|
+
message = (
|
|
633
|
+
f"{app_name}: Cannot {{action}} column `{{column_name}}` {{preposition}} the default columns for `{table}`."
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
for column_name in table_extension.add_to_default_columns:
|
|
637
|
+
if column_name in table.base_columns:
|
|
638
|
+
table.Meta.default_columns = (*table.Meta.default_columns, column_name)
|
|
639
|
+
else:
|
|
640
|
+
logger.debug(message.format(action="add", column_name=column_name, preposition="to"))
|
|
641
|
+
|
|
642
|
+
for column_name in table_extension.remove_from_default_columns:
|
|
643
|
+
if column_name in table.Meta.default_columns:
|
|
644
|
+
table.Meta.default_columns = tuple(name for name in table.Meta.default_columns if name != column_name)
|
|
645
|
+
else:
|
|
646
|
+
logger.debug(message.format(action="remove", column_name=column_name, preposition="from"))
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _validate_is_subclass_of_table_extension(table_extension):
|
|
650
|
+
if not issubclass(table_extension, TableExtension):
|
|
651
|
+
raise TypeError(f"{table_extension} is not a subclass of nautobot.apps.filters.TableExtension!")
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _validate_table_column_name_is_prefixed_with_app_name(name, app_name):
|
|
655
|
+
if not name.startswith(f"{app_name}_"):
|
|
656
|
+
raise ValueError(f"Attempted to create a custom table column `{name}` that did not start with `{app_name}`")
|
|
657
|
+
|
|
658
|
+
|
|
494
659
|
#
|
|
495
660
|
# Navigation menu links
|
|
496
661
|
#
|
nautobot/extras/signals.py
CHANGED
|
@@ -91,6 +91,8 @@ def invalidate_models_cache(sender, **kwargs):
|
|
|
91
91
|
with contextlib.suppress(redis.exceptions.ConnectionError):
|
|
92
92
|
# TODO: *maybe* target more narrowly, e.g. only clear the cache for specific related content-types?
|
|
93
93
|
cache.delete_pattern(f"{manager.get_for_model.cache_key_prefix}.*")
|
|
94
|
+
if hasattr(manager, "keys_for_model"):
|
|
95
|
+
cache.delete_pattern(f"{manager.keys_for_model.cache_key_prefix}.*")
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
@receiver(post_save, sender=Relationship)
|
nautobot/extras/tasks.py
CHANGED
|
@@ -2,18 +2,42 @@ from logging import getLogger
|
|
|
2
2
|
|
|
3
3
|
from django.conf import settings
|
|
4
4
|
from django.contrib.contenttypes.models import ContentType
|
|
5
|
-
from django.db import transaction
|
|
6
5
|
from jinja2.exceptions import TemplateError
|
|
7
6
|
import requests
|
|
8
7
|
|
|
9
8
|
from nautobot.core.celery import nautobot_task
|
|
9
|
+
from nautobot.core.models.query_functions import JSONRemove, JSONSet
|
|
10
10
|
from nautobot.extras.choices import CustomFieldTypeChoices, ObjectChangeActionChoices
|
|
11
11
|
from nautobot.extras.utils import generate_signature
|
|
12
12
|
|
|
13
13
|
logger = getLogger("nautobot.extras.tasks")
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
def _generate_bulk_object_changes(context, queryset, task_logger):
|
|
17
|
+
# Circular import
|
|
18
|
+
from nautobot.extras.context_managers import (
|
|
19
|
+
change_logging,
|
|
20
|
+
ChangeContext,
|
|
21
|
+
deferred_change_logging_for_bulk_operation,
|
|
22
|
+
)
|
|
23
|
+
from nautobot.extras.signals import _handle_changed_object
|
|
24
|
+
|
|
25
|
+
task_logger.info("Creating deferred ObjectChange records for bulk operation...")
|
|
26
|
+
|
|
27
|
+
# Note: we use change_logging() here instead of web_request_context() because we don't want these change records to
|
|
28
|
+
# trigger jobhooks and webhooks.
|
|
29
|
+
# TODO: this could be made much faster if we ensure the queryset has appropriate select_related/prefetch_related?
|
|
30
|
+
change_context = ChangeContext(**context)
|
|
31
|
+
i = 0
|
|
32
|
+
with change_logging(change_context):
|
|
33
|
+
with deferred_change_logging_for_bulk_operation():
|
|
34
|
+
for i, instance in enumerate(queryset.iterator(), start=1):
|
|
35
|
+
_handle_changed_object(queryset.model, instance, created=False)
|
|
36
|
+
|
|
37
|
+
task_logger.info("Created %d ObjectChange records", i)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@nautobot_task(soft_time_limit=1800, time_limit=2000)
|
|
17
41
|
def update_custom_field_choice_data(field_id, old_value, new_value, change_context=None):
|
|
18
42
|
"""
|
|
19
43
|
Update the values for a custom field choice used in objects' _custom_field_data for the given field.
|
|
@@ -22,47 +46,48 @@ def update_custom_field_choice_data(field_id, old_value, new_value, change_conte
|
|
|
22
46
|
field_id (uuid4): The PK of the custom field to which this choice value relates
|
|
23
47
|
old_value (str): The existing value of the choice
|
|
24
48
|
new_value (str): The value which will be used as replacement
|
|
49
|
+
change_context (dict): Optional dict representation of change context for ObjectChange creation
|
|
25
50
|
"""
|
|
26
51
|
# Circular Import
|
|
27
52
|
from nautobot.extras.context_managers import web_request_context
|
|
28
53
|
from nautobot.extras.models import CustomField
|
|
29
54
|
|
|
55
|
+
task_logger = getLogger("celery.task.update_custom_field_choice_data")
|
|
56
|
+
|
|
30
57
|
try:
|
|
31
58
|
field = CustomField.objects.get(pk=field_id)
|
|
32
59
|
except CustomField.DoesNotExist:
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
task_logger.error("Custom field with ID %s not found, failing to act on choice data.", field_id)
|
|
61
|
+
raise
|
|
35
62
|
|
|
36
63
|
if field.type == CustomFieldTypeChoices.TYPE_SELECT:
|
|
37
64
|
# Loop through all field content types and search for values to update
|
|
38
65
|
for ct in field.content_types.all():
|
|
39
66
|
model = ct.model_class()
|
|
67
|
+
queryset = model.objects.filter(**{f"_custom_field_data__{field.key}": old_value})
|
|
40
68
|
if change_context is not None:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
pk_list = list(queryset.values_list("pk", flat=True))
|
|
70
|
+
task_logger.info(
|
|
71
|
+
"Updating selection for custom field `%s` from `%s` to `%s` on %s records...",
|
|
72
|
+
field.key,
|
|
73
|
+
old_value,
|
|
74
|
+
new_value,
|
|
75
|
+
ct.model,
|
|
76
|
+
extra={"object": field},
|
|
77
|
+
)
|
|
78
|
+
count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, new_value))
|
|
79
|
+
task_logger.info("Updated %d records", count)
|
|
80
|
+
if change_context is not None:
|
|
81
|
+
# Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
|
|
82
|
+
_generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
|
|
54
83
|
|
|
55
84
|
elif field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
|
56
85
|
# Loop through all field content types and search for values to update
|
|
86
|
+
# TODO: can we implement a bulk operator for this?
|
|
57
87
|
for ct in field.content_types.all():
|
|
58
88
|
model = ct.model_class()
|
|
59
89
|
if change_context is not None:
|
|
60
|
-
with web_request_context(
|
|
61
|
-
user=change_context.get("user"),
|
|
62
|
-
change_id=change_context.get("change_id"),
|
|
63
|
-
context_detail=change_context.get("context_detail"),
|
|
64
|
-
context=change_context.get("context"),
|
|
65
|
-
):
|
|
90
|
+
with web_request_context(**change_context):
|
|
66
91
|
for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
|
|
67
92
|
old_list = obj._custom_field_data[field.key]
|
|
68
93
|
new_list = [new_value if e == old_value else e for e in old_list]
|
|
@@ -76,13 +101,13 @@ def update_custom_field_choice_data(field_id, old_value, new_value, change_conte
|
|
|
76
101
|
obj.save()
|
|
77
102
|
|
|
78
103
|
else:
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
task_logger.error(f"Unknown field type, failing to act on choice data for this field {field.key}.")
|
|
105
|
+
raise ValueError
|
|
81
106
|
|
|
82
107
|
return True
|
|
83
108
|
|
|
84
109
|
|
|
85
|
-
@nautobot_task
|
|
110
|
+
@nautobot_task(soft_time_limit=1800, time_limit=2000)
|
|
86
111
|
def delete_custom_field_data(field_key, content_type_pk_set, change_context=None):
|
|
87
112
|
"""
|
|
88
113
|
Delete the values for a custom field
|
|
@@ -90,30 +115,23 @@ def delete_custom_field_data(field_key, content_type_pk_set, change_context=None
|
|
|
90
115
|
Args:
|
|
91
116
|
field_key (str): The key of the custom field which is being deleted
|
|
92
117
|
content_type_pk_set (list): List of PKs for content types to act upon
|
|
118
|
+
change_context (dict): Optional change context for ObjectChange creation
|
|
93
119
|
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
obj.save()
|
|
110
|
-
else:
|
|
111
|
-
for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
|
|
112
|
-
del obj._custom_field_data[field_key]
|
|
113
|
-
obj.save()
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@nautobot_task
|
|
120
|
+
task_logger = getLogger("celery.task.delete_custom_field_data")
|
|
121
|
+
for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
|
|
122
|
+
model = ct.model_class()
|
|
123
|
+
queryset = model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False})
|
|
124
|
+
if change_context is not None:
|
|
125
|
+
pk_list = list(queryset.values_list("pk", flat=True))
|
|
126
|
+
task_logger.info("Deleting existing values for custom field `%s` from %s records...", field_key, ct.model)
|
|
127
|
+
count = queryset.update(_custom_field_data=JSONRemove("_custom_field_data", field_key))
|
|
128
|
+
task_logger.info("Updated %d records", count)
|
|
129
|
+
if change_context is not None:
|
|
130
|
+
# Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
|
|
131
|
+
_generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@nautobot_task(soft_time_limit=1800, time_limit=2000)
|
|
117
135
|
def provision_field(field_id, content_type_pk_set, change_context=None):
|
|
118
136
|
"""
|
|
119
137
|
Provision a new custom field on all relevant content type object instances.
|
|
@@ -121,34 +139,35 @@ def provision_field(field_id, content_type_pk_set, change_context=None):
|
|
|
121
139
|
Args:
|
|
122
140
|
field_id (uuid4): The PK of the custom field being provisioned
|
|
123
141
|
content_type_pk_set (list): List of PKs for content types to act upon
|
|
142
|
+
change_context (dict): Optional change context for ObjectChange creation.
|
|
124
143
|
"""
|
|
125
144
|
# Circular Import
|
|
126
|
-
from nautobot.extras.context_managers import web_request_context
|
|
127
145
|
from nautobot.extras.models import CustomField
|
|
128
146
|
|
|
147
|
+
task_logger = getLogger("celery.task.provision_field")
|
|
148
|
+
|
|
129
149
|
try:
|
|
130
150
|
field = CustomField.objects.get(pk=field_id)
|
|
131
151
|
except CustomField.DoesNotExist:
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
task_logger.error(f"Custom field with ID {field_id} not found, failing to provision.")
|
|
153
|
+
raise
|
|
134
154
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
obj.save()
|
|
155
|
+
for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
|
|
156
|
+
model = ct.model_class()
|
|
157
|
+
queryset = model.objects.filter(**{f"_custom_field_data__{field.key}__isnull": True})
|
|
158
|
+
if change_context is not None:
|
|
159
|
+
pk_list = list(queryset.values_list("pk", flat=True))
|
|
160
|
+
task_logger.info(
|
|
161
|
+
"Adding data for custom field `%s` to %s records...",
|
|
162
|
+
field.key,
|
|
163
|
+
ct.model,
|
|
164
|
+
extra={"object": field},
|
|
165
|
+
)
|
|
166
|
+
count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, field.default))
|
|
167
|
+
task_logger.info("Updated %d records.", count)
|
|
168
|
+
if change_context is not None:
|
|
169
|
+
# Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
|
|
170
|
+
_generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
|
|
152
171
|
|
|
153
172
|
return True
|
|
154
173
|
|
|
@@ -277,6 +277,39 @@
|
|
|
277
277
|
{% endif %}
|
|
278
278
|
</td>
|
|
279
279
|
</tr>
|
|
280
|
+
<tr>
|
|
281
|
+
<td>Table Extensions</td>
|
|
282
|
+
<td>
|
|
283
|
+
{% if features.table_extensions %}
|
|
284
|
+
{% if features.table_extensions.columns %}
|
|
285
|
+
<b>Custom Columns</b>
|
|
286
|
+
<ul class="list-unstyled">
|
|
287
|
+
{% for column in features.table_extensions.columns %}
|
|
288
|
+
<li><code>{{ column }}</code></li>
|
|
289
|
+
{% endfor %}
|
|
290
|
+
</ul>
|
|
291
|
+
{% endif %}
|
|
292
|
+
{% if features.table_extensions.add_to_default_columns %}
|
|
293
|
+
<b>Additional Default Columns</b>
|
|
294
|
+
<ul class="list-unstyled">
|
|
295
|
+
{% for column in features.table_extensions.add_to_default_columns %}
|
|
296
|
+
<li><code>{{ column }}</code></li>
|
|
297
|
+
{% endfor %}
|
|
298
|
+
</ul>
|
|
299
|
+
{% endif %}
|
|
300
|
+
{% if features.table_extensions.remove_from_default_columns %}
|
|
301
|
+
<b>Remove from Default Columns</b>
|
|
302
|
+
<ul class="list-unstyled">
|
|
303
|
+
{% for column in features.table_extensions.remove_from_default_columns %}
|
|
304
|
+
<li><code>{{ column }}</code></li>
|
|
305
|
+
{% endfor %}
|
|
306
|
+
</ul>
|
|
307
|
+
{% endif %}
|
|
308
|
+
{% else %}
|
|
309
|
+
{% include 'utilities/render_boolean.html' with value=features.table_extensions %}
|
|
310
|
+
{% endif %}
|
|
311
|
+
</td>
|
|
312
|
+
</tr>
|
|
280
313
|
<tr>
|
|
281
314
|
<td>Views/URLs</td>
|
|
282
315
|
<td>
|
|
@@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
5
5
|
from django.test import TestCase
|
|
6
6
|
|
|
7
7
|
from nautobot.core.celery import app
|
|
8
|
-
from nautobot.core.testing import TransactionTestCase
|
|
8
|
+
from nautobot.core.testing import get_job_class_and_model, TransactionTestCase
|
|
9
9
|
from nautobot.core.utils.lookup import get_changes_for_model
|
|
10
10
|
from nautobot.dcim.models import (
|
|
11
11
|
DeviceType,
|
|
@@ -22,7 +22,7 @@ from nautobot.extras.context_managers import (
|
|
|
22
22
|
deferred_change_logging_for_bulk_operation,
|
|
23
23
|
web_request_context,
|
|
24
24
|
)
|
|
25
|
-
from nautobot.extras.models import Status, Webhook
|
|
25
|
+
from nautobot.extras.models import JobHook, Status, Webhook
|
|
26
26
|
from nautobot.extras.utils import bulk_delete_with_bulk_change_logging
|
|
27
27
|
|
|
28
28
|
# Use the proper swappable User model
|
|
@@ -74,8 +74,8 @@ class WebRequestContextTestCase(TestCase):
|
|
|
74
74
|
self.assertEqual(oc_list[0].changed_object, location)
|
|
75
75
|
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
|
76
76
|
|
|
77
|
-
@mock.patch("nautobot.extras.jobs.enqueue_job_hooks")
|
|
78
|
-
@mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
|
|
77
|
+
@mock.patch("nautobot.extras.jobs.enqueue_job_hooks", return_value=(True, None))
|
|
78
|
+
@mock.patch("nautobot.extras.context_managers.enqueue_webhooks", return_value=None)
|
|
79
79
|
def test_create_then_delete(self, mock_enqueue_webhooks, mock_enqueue_job_hooks):
|
|
80
80
|
"""Test that a create followed by a delete is logged as two changes"""
|
|
81
81
|
location_type = LocationType.objects.get(name="Campus")
|
|
@@ -88,12 +88,19 @@ class WebRequestContextTestCase(TestCase):
|
|
|
88
88
|
|
|
89
89
|
location = Location.objects.filter(pk=location_pk)
|
|
90
90
|
self.assertFalse(location.exists())
|
|
91
|
-
oc_list = get_changes_for_model(Location).filter(changed_object_id=location_pk)
|
|
91
|
+
oc_list = get_changes_for_model(Location).filter(changed_object_id=location_pk).order_by("time")
|
|
92
92
|
self.assertEqual(len(oc_list), 2)
|
|
93
|
-
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.
|
|
94
|
-
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.
|
|
95
|
-
mock_enqueue_job_hooks.assert_has_calls(
|
|
96
|
-
|
|
93
|
+
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
|
94
|
+
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_DELETE)
|
|
95
|
+
mock_enqueue_job_hooks.assert_has_calls(
|
|
96
|
+
[
|
|
97
|
+
mock.call(oc_list[0], may_reload_jobs=True, jobhook_queryset=None),
|
|
98
|
+
mock.call(oc_list[1], may_reload_jobs=False, jobhook_queryset=None),
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
mock_enqueue_webhooks.assert_has_calls(
|
|
102
|
+
[mock.call(oc_list[0], webhook_queryset=None), mock.call(oc_list[1], webhook_queryset=None)]
|
|
103
|
+
)
|
|
97
104
|
|
|
98
105
|
def test_update_then_delete(self):
|
|
99
106
|
"""Test that an update followed by a delete is logged as a single delete"""
|
|
@@ -307,6 +314,13 @@ class BulkEditDeleteChangeLogging(TestCase):
|
|
|
307
314
|
for i in range(1, 4)
|
|
308
315
|
]
|
|
309
316
|
Location.objects.bulk_create(locations)
|
|
317
|
+
# Create a JobHook that applies to Locations
|
|
318
|
+
_, job_model = get_job_class_and_model("job_hook_receiver", "TestJobHookReceiverLog")
|
|
319
|
+
mock_import_jobs.assert_called_once()
|
|
320
|
+
mock_import_jobs.reset_mock()
|
|
321
|
+
job_hook = JobHook.objects.create(name="JobHookTest", type_update=True, job=job_model)
|
|
322
|
+
job_hook.content_types.set([ContentType.objects.get_for_model(Location)])
|
|
323
|
+
|
|
310
324
|
pk_list = []
|
|
311
325
|
with web_request_context(self.user):
|
|
312
326
|
with deferred_change_logging_for_bulk_operation():
|
|
@@ -412,7 +412,7 @@ class GitTest(TransactionTestCase):
|
|
|
412
412
|
|
|
413
413
|
def test_pull_git_repository_and_refresh_data_with_bad_data(self):
|
|
414
414
|
"""
|
|
415
|
-
The test_pull_git_repository_and_refresh_data job should gracefully handle bad data in the Git repository
|
|
415
|
+
The test_pull_git_repository_and_refresh_data job should gracefully handle bad data in the Git repository.
|
|
416
416
|
"""
|
|
417
417
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
418
418
|
with self.settings(GIT_ROOT=tempdir):
|
|
@@ -433,6 +433,18 @@ class GitTest(TransactionTestCase):
|
|
|
433
433
|
job_result.result,
|
|
434
434
|
)
|
|
435
435
|
|
|
436
|
+
# Due to transaction rollback on failure, the database should still/again match the pre-sync state, of
|
|
437
|
+
# no records owned by the repository.
|
|
438
|
+
self.assertFalse(ConfigContextSchema.objects.filter(owner_object_id=self.repo.id).exists())
|
|
439
|
+
self.assertFalse(ConfigContext.objects.filter(owner_object_id=self.repo.id).exists())
|
|
440
|
+
self.assertFalse(ExportTemplate.objects.filter(owner_object_id=self.repo.id).exists())
|
|
441
|
+
self.assertFalse(Job.objects.filter(module_name__startswith=f"{self.repo.slug}.").exists())
|
|
442
|
+
device = Device.objects.get(name=self.device.name)
|
|
443
|
+
self.assertIsNone(device.local_config_context_data)
|
|
444
|
+
self.assertIsNone(device.local_config_context_data_owner)
|
|
445
|
+
self.repo.refresh_from_db()
|
|
446
|
+
self.assertEqual(self.repo.current_head, "")
|
|
447
|
+
|
|
436
448
|
# Check for specific log messages
|
|
437
449
|
log_entries = JobLogEntry.objects.filter(job_result=job_result)
|
|
438
450
|
warning_logs = log_entries.filter(log_level=LogLevelChoices.LOG_WARNING)
|
|
@@ -592,6 +604,81 @@ class GitTest(TransactionTestCase):
|
|
|
592
604
|
|
|
593
605
|
self.assert_job_exists(installed=False)
|
|
594
606
|
|
|
607
|
+
def test_git_repository_sync_rollback(self):
|
|
608
|
+
"""
|
|
609
|
+
Once a "known-good" sync state is achieved, resync to a new "bad" head commit should fail and be rolled back.
|
|
610
|
+
"""
|
|
611
|
+
with tempfile.TemporaryDirectory() as tempdir:
|
|
612
|
+
with self.settings(GIT_ROOT=tempdir):
|
|
613
|
+
# Initially have a successful sync to a good commit that provides data
|
|
614
|
+
self.repo.branch = "valid-files" # actually a tag
|
|
615
|
+
self.repo.save()
|
|
616
|
+
job_model = GitRepositorySync().job_model
|
|
617
|
+
job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
|
|
618
|
+
job_result.refresh_from_db()
|
|
619
|
+
self.assertEqual(
|
|
620
|
+
job_result.status,
|
|
621
|
+
JobResultStatusChoices.STATUS_SUCCESS,
|
|
622
|
+
(job_result.traceback, list(job_result.job_log_entries.values_list("message", flat=True))),
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
|
|
626
|
+
self.assert_implicit_config_context_exists("Location context")
|
|
627
|
+
self.assert_config_context_schema_record_exists("Config Context Schema 1")
|
|
628
|
+
self.assert_device_exists(self.device.name)
|
|
629
|
+
self.assert_export_template_device("template.j2")
|
|
630
|
+
self.assert_export_template_html_exist("template2.html")
|
|
631
|
+
self.assert_export_template_vlan_exists("template.j2")
|
|
632
|
+
self.assert_job_exists(name="MyJob")
|
|
633
|
+
self.assert_job_exists(name="MyJobButtonReceiver")
|
|
634
|
+
self.assert_job_exists(name="MyJobHookReceiver")
|
|
635
|
+
|
|
636
|
+
# Create JobButton and JobHook
|
|
637
|
+
JobButton.objects.create(
|
|
638
|
+
name="MyJobButton", enabled=True, text="Click me", job=Job.objects.get(name="MyJobButtonReceiver")
|
|
639
|
+
)
|
|
640
|
+
JobHook.objects.create(name="MyJobHook", enabled=True, job=Job.objects.get(name="MyJobHookReceiver"))
|
|
641
|
+
|
|
642
|
+
self.repo.refresh_from_db()
|
|
643
|
+
self.assertNotEqual(self.repo.current_head, "")
|
|
644
|
+
good_current_head = self.repo.current_head
|
|
645
|
+
|
|
646
|
+
# Now change to the `main` branch (which includes the current commit, followed by a "bad" commit)
|
|
647
|
+
self.repo.branch = "main"
|
|
648
|
+
self.repo.save()
|
|
649
|
+
|
|
650
|
+
# Resync, attempting and failing to update to the new commit
|
|
651
|
+
job_result = run_job_for_testing(job=job_model, repository=self.repo.pk)
|
|
652
|
+
job_result.refresh_from_db()
|
|
653
|
+
self.assertEqual(
|
|
654
|
+
job_result.status,
|
|
655
|
+
JobResultStatusChoices.STATUS_FAILURE,
|
|
656
|
+
job_result.result,
|
|
657
|
+
)
|
|
658
|
+
log_entries = JobLogEntry.objects.filter(job_result=job_result)
|
|
659
|
+
|
|
660
|
+
# Assert database changes were rolled back
|
|
661
|
+
self.repo.refresh_from_db()
|
|
662
|
+
try:
|
|
663
|
+
self.assertEqual(self.repo.current_head, good_current_head)
|
|
664
|
+
self.assert_explicit_config_context_exists("Frobozz 1000 NTP servers")
|
|
665
|
+
self.assert_implicit_config_context_exists("Location context")
|
|
666
|
+
self.assert_config_context_schema_record_exists("Config Context Schema 1")
|
|
667
|
+
self.assert_device_exists(self.device.name)
|
|
668
|
+
self.assert_export_template_device("template.j2")
|
|
669
|
+
self.assert_export_template_html_exist("template2.html")
|
|
670
|
+
self.assert_export_template_vlan_exists("template.j2")
|
|
671
|
+
self.assert_job_exists(name="MyJob")
|
|
672
|
+
self.assert_job_exists(name="MyJobButtonReceiver")
|
|
673
|
+
self.assert_job_exists(name="MyJobHookReceiver")
|
|
674
|
+
self.assertTrue(JobButton.objects.get(name="MyJobButton").enabled)
|
|
675
|
+
self.assertTrue(JobHook.objects.get(name="MyJobHook").enabled)
|
|
676
|
+
except Exception:
|
|
677
|
+
for log in log_entries:
|
|
678
|
+
print(log.message)
|
|
679
|
+
print(job_result.traceback)
|
|
680
|
+
raise
|
|
681
|
+
|
|
595
682
|
def test_git_dry_run(self):
|
|
596
683
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
597
684
|
with self.settings(GIT_ROOT=tempdir):
|