nautobot 2.3.8__py3-none-any.whl → 2.3.9__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/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/dcim/forms.py +30 -27
- nautobot/dcim/models/device_components.py +5 -0
- nautobot/dcim/tables/devices.py +4 -2
- nautobot/dcim/tests/test_models.py +16 -0
- nautobot/extras/context_managers.py +7 -8
- 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 +17 -5
- 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/templates/extras/plugin_detail.html +33 -0
- nautobot/extras/tests/test_context_managers.py +16 -7
- 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/views.py +3 -1
- 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 +49 -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 +285 -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 +41 -5
- nautobot/virtualization/tables.py +1 -1
- {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/METADATA +2 -2
- {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/RECORD +316 -315
- 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.9.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/NOTICE +0 -0
- {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/WHEEL +0 -0
- {nautobot-2.3.8.dist-info → nautobot-2.3.9.dist-info}/entry_points.txt +0 -0
nautobot/extras/models/groups.py
CHANGED
|
@@ -8,6 +8,7 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
8
8
|
from django.core.exceptions import ValidationError
|
|
9
9
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
10
10
|
from django.db import models
|
|
11
|
+
from django.db.models.signals import pre_delete
|
|
11
12
|
from django.utils.functional import cached_property
|
|
12
13
|
import django_filters
|
|
13
14
|
|
|
@@ -319,24 +320,18 @@ class DynamicGroup(PrimaryModel):
|
|
|
319
320
|
if isinstance(value, models.QuerySet):
|
|
320
321
|
if value.model != self.model:
|
|
321
322
|
raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
|
|
322
|
-
to_remove = self.members.
|
|
323
|
+
to_remove = self.members.only("id").difference(value.only("id"))
|
|
323
324
|
self._remove_members(to_remove)
|
|
324
|
-
to_add = value.
|
|
325
|
+
to_add = value.only("id").difference(self.members.only("id"))
|
|
325
326
|
self._add_members(to_add)
|
|
326
327
|
else:
|
|
327
328
|
for obj in value:
|
|
328
329
|
if not isinstance(obj, self.model):
|
|
329
330
|
raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
|
|
330
|
-
|
|
331
|
-
for
|
|
332
|
-
if member not in value:
|
|
333
|
-
to_remove.append(member)
|
|
331
|
+
existing_members = self.members
|
|
332
|
+
to_remove = [obj for obj in existing_members if obj not in value]
|
|
334
333
|
self._remove_members(to_remove)
|
|
335
|
-
to_add = []
|
|
336
|
-
members = self.members
|
|
337
|
-
for candidate in value:
|
|
338
|
-
if candidate not in members:
|
|
339
|
-
to_add.append(candidate)
|
|
334
|
+
to_add = [obj for obj in value if obj not in existing_members]
|
|
340
335
|
self._add_members(to_add)
|
|
341
336
|
|
|
342
337
|
return self.members
|
|
@@ -345,63 +340,81 @@ class DynamicGroup(PrimaryModel):
|
|
|
345
340
|
"""Add the given list or QuerySet of objects to this staticly defined group."""
|
|
346
341
|
if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
347
342
|
raise ValidationError(f"Group {self} is not staticly defined, adding members directly is not permitted.")
|
|
348
|
-
return self._add_members(objects_to_add)
|
|
349
|
-
|
|
350
|
-
def _add_members(self, objects_to_add):
|
|
351
|
-
"""Internal API for adding the given list or QuerySet of objects to the cached/static members of this group."""
|
|
352
343
|
if isinstance(objects_to_add, models.QuerySet):
|
|
353
344
|
if objects_to_add.model != self.model:
|
|
354
345
|
raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
|
|
346
|
+
objects_to_add = objects_to_add.only("id").difference(self.members.only("id"))
|
|
355
347
|
else:
|
|
356
348
|
for obj in objects_to_add:
|
|
357
349
|
if not isinstance(obj, self.model):
|
|
358
350
|
raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
|
|
351
|
+
existing_members = self.members
|
|
352
|
+
objects_to_add = [obj for obj in objects_to_add if obj not in existing_members]
|
|
353
|
+
return self._add_members(objects_to_add)
|
|
359
354
|
|
|
355
|
+
def _add_members(self, objects_to_add):
|
|
356
|
+
"""
|
|
357
|
+
Internal API for adding the given list or QuerySet of objects to the cached/static members of this group.
|
|
358
|
+
|
|
359
|
+
Assumes that objects_to_add has already been filtered to exclude any existing member objects.
|
|
360
|
+
"""
|
|
360
361
|
if self.group_type == DynamicGroupTypeChoices.TYPE_STATIC:
|
|
361
362
|
for obj in objects_to_add:
|
|
362
363
|
# We don't use `.bulk_create()` currently because we want change logging for these creates.
|
|
363
364
|
# Might be a good future performance improvement though.
|
|
364
|
-
StaticGroupAssociation.all_objects.
|
|
365
|
+
StaticGroupAssociation.all_objects.create(
|
|
365
366
|
dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
|
|
366
367
|
)
|
|
367
368
|
else:
|
|
368
369
|
# Cached/hidden static group associations, so we can use bulk-create to bypass change logging.
|
|
369
|
-
existing_members = self.members
|
|
370
370
|
sgas = [
|
|
371
371
|
StaticGroupAssociation(
|
|
372
372
|
dynamic_group=self, associated_object_type=self.content_type, associated_object_id=obj.pk
|
|
373
373
|
)
|
|
374
374
|
for obj in objects_to_add
|
|
375
|
-
if obj not in existing_members
|
|
376
375
|
]
|
|
377
|
-
StaticGroupAssociation.all_objects.bulk_create(sgas)
|
|
376
|
+
StaticGroupAssociation.all_objects.bulk_create(sgas, batch_size=1000)
|
|
378
377
|
|
|
379
378
|
def remove_members(self, objects_to_remove):
|
|
380
379
|
"""Remove the given list or QuerySet of objects from this staticly defined group."""
|
|
381
380
|
if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
382
381
|
raise ValidationError(f"Group {self} is not staticly defined, removing members directly is not permitted.")
|
|
383
|
-
return self._remove_members(objects_to_remove)
|
|
384
|
-
|
|
385
|
-
def _remove_members(self, objects_to_remove):
|
|
386
|
-
"""Internal API for removing the given list or QuerySet from the cached/static members of this Group."""
|
|
387
382
|
if isinstance(objects_to_remove, models.QuerySet):
|
|
388
383
|
if objects_to_remove.model != self.model:
|
|
389
384
|
raise TypeError(f"QuerySet does not contain {self.model._meta.label_lower} objects")
|
|
390
|
-
StaticGroupAssociation.all_objects.filter(
|
|
391
|
-
dynamic_group=self,
|
|
392
|
-
associated_object_type=self.content_type,
|
|
393
|
-
associated_object_id__in=objects_to_remove.values_list("pk", flat=True),
|
|
394
|
-
).delete()
|
|
395
385
|
else:
|
|
396
|
-
pks_to_remove = set()
|
|
397
386
|
for obj in objects_to_remove:
|
|
398
387
|
if not isinstance(obj, self.model):
|
|
399
388
|
raise TypeError(f"{obj} is not a {self.model._meta.label_lower}")
|
|
400
|
-
|
|
389
|
+
return self._remove_members(objects_to_remove)
|
|
401
390
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
391
|
+
def _remove_members(self, objects_to_remove):
|
|
392
|
+
"""Internal API for removing the given list or QuerySet from the cached/static members of this Group."""
|
|
393
|
+
from nautobot.extras.signals import _handle_deleted_object # avoid circular import
|
|
394
|
+
|
|
395
|
+
# For non-static groups, we aren't going to change log the StaticGroupAssociation deletes anyway,
|
|
396
|
+
# so save some performance on signals -- important especially when we're dealing with thousands of records
|
|
397
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
398
|
+
logger.debug("Temporarily disconnecting the _handle_deleted_object signal for performance")
|
|
399
|
+
pre_delete.disconnect(_handle_deleted_object)
|
|
400
|
+
try:
|
|
401
|
+
if isinstance(objects_to_remove, models.QuerySet):
|
|
402
|
+
StaticGroupAssociation.all_objects.filter(
|
|
403
|
+
dynamic_group=self,
|
|
404
|
+
associated_object_type=self.content_type,
|
|
405
|
+
associated_object_id__in=objects_to_remove.values_list("id", flat=True),
|
|
406
|
+
).delete()
|
|
407
|
+
else:
|
|
408
|
+
pks_to_remove = [obj.id for obj in objects_to_remove]
|
|
409
|
+
StaticGroupAssociation.all_objects.filter(
|
|
410
|
+
dynamic_group=self,
|
|
411
|
+
associated_object_type=self.content_type,
|
|
412
|
+
associated_object_id__in=pks_to_remove,
|
|
413
|
+
).delete()
|
|
414
|
+
finally:
|
|
415
|
+
if self.group_type != DynamicGroupTypeChoices.TYPE_STATIC:
|
|
416
|
+
logger.debug("Re-connecting the _handle_deleted_object signal")
|
|
417
|
+
pre_delete.connect(_handle_deleted_object)
|
|
405
418
|
|
|
406
419
|
@property
|
|
407
420
|
@method_deprecated("Members are now cached in the database via StaticGroupAssociations rather than in Redis.")
|
|
@@ -430,6 +443,7 @@ class DynamicGroup(PrimaryModel):
|
|
|
430
443
|
else:
|
|
431
444
|
raise RuntimeError(f"Unknown/invalid group_type {self.group_type}")
|
|
432
445
|
|
|
446
|
+
logger.debug("Refreshing members cache for %s", self)
|
|
433
447
|
self._set_members(members)
|
|
434
448
|
logger.debug("Refreshed cache for %s, now with %d members", self, self.count)
|
|
435
449
|
|
nautobot/extras/models/jobs.py
CHANGED
|
@@ -754,7 +754,7 @@ class JobResult(BaseModel, CustomFieldModel):
|
|
|
754
754
|
grouping="main",
|
|
755
755
|
):
|
|
756
756
|
"""
|
|
757
|
-
General-purpose API for
|
|
757
|
+
General-purpose API for creating JobLogEntry records associated with a JobResult.
|
|
758
758
|
|
|
759
759
|
message (str): Message to log (an attempt will be made to sanitize sensitive information from this message)
|
|
760
760
|
obj (object): Object associated with this message, if any
|
|
@@ -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
|
#
|
|
@@ -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,7 +74,7 @@ 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")
|
|
77
|
+
@mock.patch("nautobot.extras.jobs.enqueue_job_hooks", return_value=True)
|
|
78
78
|
@mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
|
|
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"""
|
|
@@ -88,11 +88,13 @@ 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(
|
|
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
|
+
[mock.call(oc_list[0], may_reload_jobs=True), mock.call(oc_list[1], may_reload_jobs=False)],
|
|
97
|
+
)
|
|
96
98
|
mock_enqueue_webhooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
|
|
97
99
|
|
|
98
100
|
def test_update_then_delete(self):
|
|
@@ -307,6 +309,13 @@ class BulkEditDeleteChangeLogging(TestCase):
|
|
|
307
309
|
for i in range(1, 4)
|
|
308
310
|
]
|
|
309
311
|
Location.objects.bulk_create(locations)
|
|
312
|
+
# Create a JobHook that applies to Locations
|
|
313
|
+
_, job_model = get_job_class_and_model("job_hook_receiver", "TestJobHookReceiverLog")
|
|
314
|
+
mock_import_jobs.assert_called_once()
|
|
315
|
+
mock_import_jobs.reset_mock()
|
|
316
|
+
job_hook = JobHook.objects.create(name="JobHookTest", type_update=True, job=job_model)
|
|
317
|
+
job_hook.content_types.set([ContentType.objects.get_for_model(Location)])
|
|
318
|
+
|
|
310
319
|
pk_list = []
|
|
311
320
|
with web_request_context(self.user):
|
|
312
321
|
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):
|
|
@@ -304,6 +304,10 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
|
|
|
304
304
|
sg.add_members(Prefix.objects.filter(ip_version=4))
|
|
305
305
|
self.assertIsInstance(sg.members, PrefixQuerySet)
|
|
306
306
|
self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=4))
|
|
307
|
+
# test cumulative construction and alternate code path
|
|
308
|
+
sg.add_members(list(Prefix.objects.filter(ip_version=6)))
|
|
309
|
+
self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.all())
|
|
310
|
+
self.assertEqual(sg.static_group_associations.count(), Prefix.objects.all().count())
|
|
307
311
|
# test duplicate objects aren't re-added
|
|
308
312
|
sg.add_members(Prefix.objects.all())
|
|
309
313
|
self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.all())
|
|
@@ -321,10 +325,18 @@ class DynamicGroupModelTest(DynamicGroupTestBase): # TODO: BaseModelTestCase mi
|
|
|
321
325
|
sg.remove_members(list(Prefix.objects.filter(ip_version=4)))
|
|
322
326
|
self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=6))
|
|
323
327
|
self.assertEqual(sg.static_group_associations.count(), Prefix.objects.filter(ip_version=6).count())
|
|
328
|
+
# test cumulative removal and alternate code path
|
|
329
|
+
sg.remove_members(list(Prefix.objects.filter(ip_version=6)))
|
|
330
|
+
self.assertQuerysetEqual(sg.members, Prefix.objects.none())
|
|
331
|
+
self.assertEqual(sg.static_group_associations.count(), 0)
|
|
324
332
|
|
|
325
333
|
# test property setter
|
|
326
334
|
sg.members = Prefix.objects.filter(ip_version=4)
|
|
327
335
|
self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=4))
|
|
336
|
+
sg.members = Prefix.objects.filter(ip_version=6)
|
|
337
|
+
self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=6))
|
|
338
|
+
sg.members = list(Prefix.objects.filter(ip_version=4))
|
|
339
|
+
self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=4))
|
|
328
340
|
sg.members = list(Prefix.objects.filter(ip_version=6))
|
|
329
341
|
self.assertQuerysetEqualAndNotEmpty(sg.members, Prefix.objects.filter(ip_version=6))
|
|
330
342
|
|