nautobot 2.3.7__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/inc/paginator.html +3 -0
- nautobot/core/templates/nautobot_config.py.j2 +4 -0
- nautobot/core/templates/utilities/obj_table.html +1 -1
- nautobot/dcim/api/serializers.py +10 -5
- nautobot/dcim/forms.py +41 -34
- nautobot/dcim/models/device_components.py +12 -4
- nautobot/dcim/tables/devices.py +4 -2
- nautobot/dcim/tests/test_api.py +28 -0
- nautobot/dcim/tests/test_forms.py +17 -1
- nautobot/dcim/tests/test_models.py +58 -4
- nautobot/dcim/utils.py +9 -6
- 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/plugins/views.py +18 -3
- 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/ipam/filters.py +2 -2
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/templates/ipam/ipaddress.html +2 -2
- nautobot/ipam/templates/ipam/ipaddress_interfaces.html +3 -0
- nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +3 -0
- nautobot/ipam/templates/ipam/prefix.html +3 -3
- nautobot/ipam/templates/ipam/routetarget.html +2 -2
- nautobot/ipam/templates/ipam/vlan.html +3 -0
- nautobot/ipam/templates/ipam/vrf.html +7 -4
- nautobot/ipam/tests/test_models.py +68 -12
- nautobot/ipam/views.py +43 -0
- 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 +337 -109
- 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.7.dist-info → nautobot-2.3.9.dist-info}/METADATA +2 -2
- {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/RECORD +334 -333
- 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.7.dist-info → nautobot-2.3.9.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/NOTICE +0 -0
- {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/WHEEL +0 -0
- {nautobot-2.3.7.dist-info → nautobot-2.3.9.dist-info}/entry_points.txt +0 -0
nautobot/dcim/utils.py
CHANGED
|
@@ -139,12 +139,14 @@ def validate_interface_tagged_vlans(instance, model, pk_set):
|
|
|
139
139
|
)
|
|
140
140
|
|
|
141
141
|
# Filter the model objects based on the primary keys passed in kwargs and exclude the ones that have
|
|
142
|
-
# a location that is not the parent's location or None
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
# a location that is not the parent's location, or parent's location's ancestors, or None
|
|
143
|
+
location = getattr(instance.parent, "location", None)
|
|
144
|
+
if location:
|
|
145
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
146
|
+
else:
|
|
147
|
+
location_ids = []
|
|
146
148
|
tagged_vlans = (
|
|
147
|
-
model.objects.filter(pk__in=pk_set).exclude(locations__isnull=True).exclude(locations__in=
|
|
149
|
+
model.objects.filter(pk__in=pk_set).exclude(locations__isnull=True).exclude(locations__in=location_ids)
|
|
148
150
|
)
|
|
149
151
|
|
|
150
152
|
if tagged_vlans.count():
|
|
@@ -152,7 +154,8 @@ def validate_interface_tagged_vlans(instance, model, pk_set):
|
|
|
152
154
|
{
|
|
153
155
|
"tagged_vlans": (
|
|
154
156
|
f"Tagged VLAN with names {list(tagged_vlans.values_list('name', flat=True))} must all belong to the "
|
|
155
|
-
|
|
157
|
+
"same location as the interface's parent device, "
|
|
158
|
+
"one of the parent locations of the interface's parent device's location, or it must be global."
|
|
156
159
|
)
|
|
157
160
|
}
|
|
158
161
|
)
|
|
@@ -180,7 +180,7 @@ def web_request_context(
|
|
|
180
180
|
Valid choices are in `nautobot.extras.choices.ObjectChangeEventContextChoices`.
|
|
181
181
|
:param request: Optional web request instance, one will be generated if not supplied
|
|
182
182
|
"""
|
|
183
|
-
from nautobot.extras.jobs import enqueue_job_hooks
|
|
183
|
+
from nautobot.extras.jobs import enqueue_job_hooks # prevent circular import
|
|
184
184
|
|
|
185
185
|
valid_contexts = {
|
|
186
186
|
ObjectChangeEventContextChoices.CONTEXT_JOB: JobChangeContext,
|
|
@@ -203,15 +203,14 @@ def web_request_context(
|
|
|
203
203
|
with change_logging(change_context):
|
|
204
204
|
yield request
|
|
205
205
|
finally:
|
|
206
|
-
|
|
206
|
+
jobs_reloaded = False
|
|
207
207
|
# enqueue jobhooks and webhooks, use change_context.change_id in case change_id was not supplied
|
|
208
|
-
for object_change in
|
|
208
|
+
for object_change in (
|
|
209
|
+
ObjectChange.objects.filter(request_id=change_context.change_id).order_by("time").iterator()
|
|
210
|
+
):
|
|
209
211
|
if context != ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
|
|
210
|
-
# Make sure JobHooks are up to date (once) before calling them
|
|
211
|
-
|
|
212
|
-
get_jobs(reload=True)
|
|
213
|
-
jobs_refreshed = True
|
|
214
|
-
enqueue_job_hooks(object_change)
|
|
212
|
+
# Make sure JobHooks are up to date (only once) before calling them
|
|
213
|
+
jobs_reloaded |= enqueue_job_hooks(object_change, may_reload_jobs=(not jobs_reloaded))
|
|
215
214
|
enqueue_webhooks(object_change)
|
|
216
215
|
|
|
217
216
|
|
|
@@ -4,6 +4,7 @@ from .git import (
|
|
|
4
4
|
ensure_git_repository,
|
|
5
5
|
get_repo_access_url,
|
|
6
6
|
git_repository_dry_run,
|
|
7
|
+
refresh_job_code_from_repository,
|
|
7
8
|
)
|
|
8
9
|
from .registry import (
|
|
9
10
|
get_datasource_content_choices,
|
|
@@ -20,4 +21,5 @@ __all__ = (
|
|
|
20
21
|
"get_repo_access_url",
|
|
21
22
|
"git_repository_dry_run",
|
|
22
23
|
"refresh_datasource_content",
|
|
24
|
+
"refresh_job_code_from_repository",
|
|
23
25
|
)
|
|
@@ -96,8 +96,11 @@ def get_repo_access_url(repository_record):
|
|
|
96
96
|
obj=repository_record,
|
|
97
97
|
)
|
|
98
98
|
except ObjectDoesNotExist:
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
logger.warning(
|
|
100
|
+
"HTTP Token not found for secrets group %s associated with repository %s",
|
|
101
|
+
repository_record.secrets_group,
|
|
102
|
+
repository_record,
|
|
103
|
+
)
|
|
101
104
|
try:
|
|
102
105
|
user = repository_record.secrets_group.get_secret_value(
|
|
103
106
|
SecretsGroupAccessTypeChoices.TYPE_HTTP,
|
|
@@ -105,8 +108,12 @@ def get_repo_access_url(repository_record):
|
|
|
105
108
|
obj=repository_record,
|
|
106
109
|
)
|
|
107
110
|
except ObjectDoesNotExist:
|
|
108
|
-
#
|
|
109
|
-
|
|
111
|
+
# May not be needed for this repository, so just log as debug rather than warning
|
|
112
|
+
logger.debug(
|
|
113
|
+
"HTTP Username not found for secrets group %s associated with repository %s",
|
|
114
|
+
repository_record.secrets_group,
|
|
115
|
+
repository_record,
|
|
116
|
+
)
|
|
110
117
|
|
|
111
118
|
if token and token not in from_url:
|
|
112
119
|
# Some git repositories require a user as well as a token.
|
|
@@ -147,7 +154,7 @@ def ensure_git_repository(repository_record, logger=None, head=None): # pylint:
|
|
|
147
154
|
(bool): Whether any change to the local repo actually occurred.
|
|
148
155
|
"""
|
|
149
156
|
# We want to check if the repo is already checked out at head. We also want to avoid calling
|
|
150
|
-
#
|
|
157
|
+
# get_repo_from_url_to_path_and_from_branch, because it will cause the URL to be rebuilt causing calls to a secrets
|
|
151
158
|
# backend. As such, if head is None, we can't perform these checks.
|
|
152
159
|
if head is not None:
|
|
153
160
|
# If the repo exists and has HEAD already checked out, the repo is present and has the correct branch selected.
|
|
@@ -205,7 +212,6 @@ def git_repository_dry_run(repository_record, logger): # pylint: disable=redefi
|
|
|
205
212
|
logger.info("%s - `%s`", item.status, item.text)
|
|
206
213
|
else:
|
|
207
214
|
logger.info("Repository has no changes")
|
|
208
|
-
|
|
209
215
|
except Exception as exc:
|
|
210
216
|
logger.error(str(exc))
|
|
211
217
|
raise
|
|
@@ -435,19 +441,12 @@ def import_config_context(context_data, repository_record, job_result):
|
|
|
435
441
|
created = False
|
|
436
442
|
modified = False
|
|
437
443
|
save_needed = False
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
except ConfigContext.DoesNotExist:
|
|
445
|
-
context_record = ConfigContext(
|
|
446
|
-
name=context_metadata.get("name"),
|
|
447
|
-
owner_content_type=git_repository_content_type,
|
|
448
|
-
owner_object_id=repository_record.pk,
|
|
449
|
-
)
|
|
450
|
-
created = True
|
|
444
|
+
context_record, created = ConfigContext.objects.get_or_create(
|
|
445
|
+
name=context_metadata.get("name"),
|
|
446
|
+
owner_content_type=git_repository_content_type,
|
|
447
|
+
owner_object_id=repository_record.pk,
|
|
448
|
+
defaults={"data": {}},
|
|
449
|
+
)
|
|
451
450
|
|
|
452
451
|
for field in ("weight", "description", "is_active"):
|
|
453
452
|
new_value = context_metadata[field]
|
|
@@ -662,20 +661,12 @@ def import_config_context_schema(context_schema_data, repository_record, job_res
|
|
|
662
661
|
|
|
663
662
|
schema_metadata = context_schema_data["_metadata"]
|
|
664
663
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
except ConfigContextSchema.DoesNotExist:
|
|
672
|
-
schema_record = ConfigContextSchema(
|
|
673
|
-
name=schema_metadata["name"],
|
|
674
|
-
owner_content_type=git_repository_content_type,
|
|
675
|
-
owner_object_id=repository_record.pk,
|
|
676
|
-
data_schema=context_schema_data["data_schema"],
|
|
677
|
-
)
|
|
678
|
-
created = True
|
|
664
|
+
schema_record, created = ConfigContextSchema.objects.get_or_create(
|
|
665
|
+
name=schema_metadata["name"],
|
|
666
|
+
owner_content_type=git_repository_content_type,
|
|
667
|
+
owner_object_id=repository_record.pk,
|
|
668
|
+
defaults={"data_schema": context_schema_data["data_schema"]},
|
|
669
|
+
)
|
|
679
670
|
|
|
680
671
|
if schema_record.description != schema_metadata.get("description", ""):
|
|
681
672
|
schema_record.description = schema_metadata.get("description", "")
|
|
@@ -868,22 +859,12 @@ def update_git_export_templates(repository_record, job_result):
|
|
|
868
859
|
# To reduce noise until the base issue is fixed, we need to explicitly detect object changes:
|
|
869
860
|
created = False
|
|
870
861
|
modified = False
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
)
|
|
878
|
-
except ExportTemplate.DoesNotExist:
|
|
879
|
-
template_record = ExportTemplate(
|
|
880
|
-
content_type=model_content_type,
|
|
881
|
-
name=file_name,
|
|
882
|
-
owner_content_type=git_repository_content_type,
|
|
883
|
-
owner_object_id=repository_record.pk,
|
|
884
|
-
)
|
|
885
|
-
created = True
|
|
886
|
-
modified = True
|
|
862
|
+
template_record, created = ExportTemplate.objects.get_or_create(
|
|
863
|
+
content_type=model_content_type,
|
|
864
|
+
name=file_name,
|
|
865
|
+
owner_content_type=git_repository_content_type,
|
|
866
|
+
owner_object_id=repository_record.pk,
|
|
867
|
+
)
|
|
887
868
|
|
|
888
869
|
if template_record.template_code != template_content:
|
|
889
870
|
template_record.template_code = template_content
|
|
@@ -32,7 +32,7 @@ def refresh_datasource_content(model_name, record, user, job_result, delete=Fals
|
|
|
32
32
|
job_result (JobResult): Passed through to the callback functions to use with logging their actions.
|
|
33
33
|
delete (bool): True if the record is being deleted; False if it is being created/updated.
|
|
34
34
|
"""
|
|
35
|
-
job_result.log(f"Refreshing data provided by {record}...", level_choice=LogLevelChoices.LOG_INFO)
|
|
35
|
+
job_result.log(f"Refreshing data provided by {record}...", level_choice=LogLevelChoices.LOG_INFO, obj=record)
|
|
36
36
|
|
|
37
37
|
for entry in get_datasource_contents(model_name):
|
|
38
38
|
job_result.log(f"Refreshing {entry.name}...", level_choice=LogLevelChoices.LOG_INFO)
|
|
@@ -54,4 +54,4 @@ def refresh_datasource_content(model_name, record, user, job_result, delete=Fals
|
|
|
54
54
|
raise RuntimeError(msg)
|
|
55
55
|
|
|
56
56
|
# Otherwise, log a friendly info message.
|
|
57
|
-
job_result.log(f"Data refresh from {record} complete!", level_choice=LogLevelChoices.LOG_INFO)
|
|
57
|
+
job_result.log(f"Data refresh from {record} complete!", level_choice=LogLevelChoices.LOG_INFO, obj=record)
|
nautobot/extras/jobs.py
CHANGED
|
@@ -1144,20 +1144,23 @@ def run_job(self, job_class_path, *args, **kwargs):
|
|
|
1144
1144
|
raise
|
|
1145
1145
|
|
|
1146
1146
|
|
|
1147
|
-
def enqueue_job_hooks(object_change):
|
|
1147
|
+
def enqueue_job_hooks(object_change, may_reload_jobs=True):
|
|
1148
1148
|
"""
|
|
1149
|
-
Find job hook(s) assigned to this changed object type + action and enqueue them
|
|
1150
|
-
|
|
1149
|
+
Find job hook(s) assigned to this changed object type + action and enqueue them to be processed.
|
|
1150
|
+
|
|
1151
|
+
Returns:
|
|
1152
|
+
jobs_reloaded (bool): whether Jobs were reloaded to make this happen
|
|
1151
1153
|
"""
|
|
1154
|
+
jobs_reloaded = False
|
|
1152
1155
|
|
|
1153
1156
|
# Job hooks cannot trigger other job hooks
|
|
1154
1157
|
if object_change.change_context == ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
|
|
1155
|
-
return
|
|
1158
|
+
return jobs_reloaded
|
|
1156
1159
|
|
|
1157
1160
|
# Determine whether this type of object supports job hooks
|
|
1158
1161
|
content_type = object_change.changed_object_type
|
|
1159
1162
|
if content_type not in change_logged_models_queryset():
|
|
1160
|
-
return
|
|
1163
|
+
return jobs_reloaded
|
|
1161
1164
|
|
|
1162
1165
|
# Retrieve any applicable job hooks
|
|
1163
1166
|
action_flag = {
|
|
@@ -1167,7 +1170,14 @@ def enqueue_job_hooks(object_change):
|
|
|
1167
1170
|
}[object_change.action]
|
|
1168
1171
|
job_hooks = JobHook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
|
|
1169
1172
|
|
|
1173
|
+
if not job_hooks.exists():
|
|
1174
|
+
return jobs_reloaded
|
|
1175
|
+
|
|
1170
1176
|
# Enqueue the jobs related to the job_hooks
|
|
1177
|
+
if may_reload_jobs:
|
|
1178
|
+
get_jobs(reload=True)
|
|
1179
|
+
jobs_reloaded = True
|
|
1180
|
+
|
|
1171
1181
|
for job_hook in job_hooks:
|
|
1172
1182
|
job_model = job_hook.job
|
|
1173
1183
|
if not job_model.installed or not job_model.enabled:
|
|
@@ -1178,3 +1188,5 @@ def enqueue_job_hooks(object_change):
|
|
|
1178
1188
|
logger.error("JobHook %s is enabled, but the underlying Job implementation is missing", job_hook)
|
|
1179
1189
|
else:
|
|
1180
1190
|
JobResult.enqueue_job(job_model, object_change.user, object_change=object_change.pk)
|
|
1191
|
+
|
|
1192
|
+
return jobs_reloaded
|
|
@@ -115,6 +115,12 @@ class GitRepository(PrimaryModel):
|
|
|
115
115
|
"provides contents overlapping with this repository."
|
|
116
116
|
)
|
|
117
117
|
|
|
118
|
+
# Changing branch or remote_url invalidates current_head
|
|
119
|
+
if self.present_in_database:
|
|
120
|
+
past = GitRepository.objects.get(id=self.id)
|
|
121
|
+
if self.remote_url != past.remote_url or self.branch != past.branch:
|
|
122
|
+
self.current_head = ""
|
|
123
|
+
|
|
118
124
|
def get_latest_sync(self):
|
|
119
125
|
"""
|
|
120
126
|
Return a `JobResult` for the latest sync operation.
|
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
|
#
|