nautobot 2.3.9__py3-none-any.whl → 2.3.11__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/utils.py +2 -0
- nautobot/cloud/tables.py +1 -0
- nautobot/core/forms/forms.py +5 -1
- nautobot/core/models/query_functions.py +147 -1
- nautobot/core/tables.py +88 -22
- nautobot/core/templates/generic/object_bulk_destroy.html +12 -3
- nautobot/core/templates/generic/object_bulk_update.html +4 -2
- nautobot/core/templates/generic/object_create.html +1 -1
- nautobot/core/templates/rest_framework/api.html +3 -0
- nautobot/core/testing/api.py +3 -1
- nautobot/core/testing/integration.py +64 -0
- nautobot/core/testing/views.py +33 -27
- nautobot/core/tests/integration/test_app_navbar.py +3 -3
- nautobot/core/tests/integration/test_navbar.py +1 -1
- nautobot/core/tests/test_csv.py +3 -0
- nautobot/core/tests/test_models_query_functions.py +108 -0
- nautobot/core/tests/test_utils.py +25 -5
- nautobot/core/utils/lookup.py +35 -0
- nautobot/core/views/generic.py +50 -39
- nautobot/core/views/mixins.py +97 -43
- nautobot/core/views/renderers.py +8 -5
- nautobot/dcim/tables/devices.py +3 -0
- nautobot/dcim/templates/dcim/device_component_add.html +8 -8
- nautobot/dcim/templates/dcim/modulebay_create.html +39 -0
- nautobot/dcim/templates/dcim/modulebay_update.html +39 -0
- nautobot/dcim/templates/dcim/virtualchassis_add_member.html +2 -2
- nautobot/dcim/templates/dcim/virtualchassis_edit.html +2 -2
- nautobot/dcim/tests/integration/test_create_device.py +86 -0
- nautobot/dcim/views.py +1 -1
- nautobot/extras/api/customfields.py +3 -10
- nautobot/extras/context_managers.py +23 -3
- nautobot/extras/jobs.py +20 -14
- nautobot/extras/models/customfields.py +12 -0
- nautobot/extras/signals.py +2 -0
- nautobot/extras/tasks.py +88 -69
- nautobot/extras/tests/test_context_managers.py +9 -4
- nautobot/extras/tests/test_relationships.py +1 -0
- nautobot/extras/tests/test_webhooks.py +1 -1
- nautobot/extras/views.py +1 -0
- nautobot/extras/webhooks.py +16 -7
- nautobot/ipam/factory.py +3 -0
- nautobot/ipam/filters.py +5 -0
- nautobot/ipam/forms.py +17 -0
- nautobot/ipam/models.py +2 -1
- nautobot/ipam/signals.py +2 -2
- nautobot/ipam/tables.py +3 -3
- nautobot/ipam/templates/ipam/ipaddress_assign.html +2 -2
- nautobot/ipam/tests/test_models.py +113 -1
- nautobot/ipam/tests/test_views.py +39 -5
- nautobot/project-static/docs/404.html +1 -1
- nautobot/project-static/docs/apps/index.html +1 -1
- nautobot/project-static/docs/apps/nautobot-apps.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +62 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +132 -7
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +176 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +95 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +5 -5
- nautobot/project-static/docs/development/apps/api/configuration-view.html +1 -1
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/global-search.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/graphql.html +1 -1
- nautobot/project-static/docs/development/apps/api/models/index.html +1 -1
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +1 -1
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +1 -1
- nautobot/project-static/docs/development/apps/api/prometheus.html +1 -1
- nautobot/project-static/docs/development/apps/api/setup.html +1 -1
- nautobot/project-static/docs/development/apps/api/testing.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +1 -1
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/base-template.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/index.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/notes.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +1 -1
- nautobot/project-static/docs/development/apps/api/views/urls.html +1 -1
- nautobot/project-static/docs/development/apps/index.html +1 -1
- nautobot/project-static/docs/development/apps/migration/code-updates.html +1 -1
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +1 -1
- nautobot/project-static/docs/development/apps/migration/from-v1.html +1 -1
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +1 -1
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +1 -1
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +1 -1
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +1 -1
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +1 -1
- nautobot/project-static/docs/development/core/application-registry.html +1 -1
- nautobot/project-static/docs/development/core/best-practices.html +1 -1
- nautobot/project-static/docs/development/core/bootstrap-ui.html +1 -1
- nautobot/project-static/docs/development/core/caching.html +1 -1
- nautobot/project-static/docs/development/core/controllers.html +1 -1
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +1 -1
- nautobot/project-static/docs/development/core/generic-views.html +1 -1
- nautobot/project-static/docs/development/core/getting-started.html +1 -1
- nautobot/project-static/docs/development/core/homepage.html +1 -1
- nautobot/project-static/docs/development/core/index.html +1 -1
- nautobot/project-static/docs/development/core/model-checklist.html +1 -1
- nautobot/project-static/docs/development/core/model-features.html +1 -1
- nautobot/project-static/docs/development/core/natural-keys.html +1 -1
- nautobot/project-static/docs/development/core/navigation-menu.html +1 -1
- nautobot/project-static/docs/development/core/release-checklist.html +1 -1
- nautobot/project-static/docs/development/core/role-internals.html +1 -1
- nautobot/project-static/docs/development/core/settings.html +1 -1
- nautobot/project-static/docs/development/core/style-guide.html +1 -1
- nautobot/project-static/docs/development/core/templates.html +1 -1
- nautobot/project-static/docs/development/core/testing.html +1 -1
- nautobot/project-static/docs/development/core/user-preferences.html +1 -1
- nautobot/project-static/docs/development/index.html +1 -1
- nautobot/project-static/docs/development/jobs/index.html +1 -1
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +1 -1
- nautobot/project-static/docs/index.html +1 -1
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +1 -1
- nautobot/project-static/docs/overview/design_philosophy.html +1 -1
- nautobot/project-static/docs/release-notes/index.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.0.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.1.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.2.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.3.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.4.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.5.html +1 -1
- nautobot/project-static/docs/release-notes/version-1.6.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.0.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.1.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.2.html +1 -1
- nautobot/project-static/docs/release-notes/version-2.3.html +440 -140
- nautobot/project-static/docs/requirements.txt +1 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +270 -270
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +1 -1
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +40 -1
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +1 -1
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +1 -1
- nautobot/project-static/docs/user-guide/administration/installation/services.html +1 -1
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +1 -1
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +1 -1
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +1 -1
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +1 -1
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +1 -1
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +1 -1
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +1 -1
- nautobot/project-static/docs/user-guide/index.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +1 -1
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +1 -1
- nautobot/project-static/js/forms.js +0 -38
- nautobot/virtualization/forms.py +24 -0
- nautobot/virtualization/templates/virtualization/vminterface_edit.html +1 -0
- nautobot/virtualization/tests/test_views.py +7 -2
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/METADATA +2 -2
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/RECORD +335 -331
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/NOTICE +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/WHEEL +0 -0
- {nautobot-2.3.9.dist-info → nautobot-2.3.11.dist-info}/entry_points.txt +0 -0
|
@@ -391,6 +391,18 @@ class CustomFieldManager(BaseManager.from_queryset(RestrictedQuerySet)):
|
|
|
391
391
|
|
|
392
392
|
get_for_model.cache_key_prefix = "nautobot.extras.customfield.get_for_model"
|
|
393
393
|
|
|
394
|
+
def keys_for_model(self, model):
|
|
395
|
+
"""Return list of all keys for CustomFields assigned to the given model."""
|
|
396
|
+
concrete_model = model._meta.concrete_model
|
|
397
|
+
cache_key = f"{self.keys_for_model.cache_key_prefix}.{concrete_model._meta.label_lower}"
|
|
398
|
+
keys = cache.get(cache_key)
|
|
399
|
+
if keys is None:
|
|
400
|
+
keys = list(self.get_for_model(model).values_list("key", flat=True))
|
|
401
|
+
cache.set(cache_key, keys)
|
|
402
|
+
return keys
|
|
403
|
+
|
|
404
|
+
keys_for_model.cache_key_prefix = "nautobot.extras.customfield.keys_for_model"
|
|
405
|
+
|
|
394
406
|
|
|
395
407
|
@extras_features("webhooks")
|
|
396
408
|
class CustomField(
|
nautobot/extras/signals.py
CHANGED
|
@@ -91,6 +91,8 @@ def invalidate_models_cache(sender, **kwargs):
|
|
|
91
91
|
with contextlib.suppress(redis.exceptions.ConnectionError):
|
|
92
92
|
# TODO: *maybe* target more narrowly, e.g. only clear the cache for specific related content-types?
|
|
93
93
|
cache.delete_pattern(f"{manager.get_for_model.cache_key_prefix}.*")
|
|
94
|
+
if hasattr(manager, "keys_for_model"):
|
|
95
|
+
cache.delete_pattern(f"{manager.keys_for_model.cache_key_prefix}.*")
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
@receiver(post_save, sender=Relationship)
|
nautobot/extras/tasks.py
CHANGED
|
@@ -2,18 +2,42 @@ from logging import getLogger
|
|
|
2
2
|
|
|
3
3
|
from django.conf import settings
|
|
4
4
|
from django.contrib.contenttypes.models import ContentType
|
|
5
|
-
from django.db import transaction
|
|
6
5
|
from jinja2.exceptions import TemplateError
|
|
7
6
|
import requests
|
|
8
7
|
|
|
9
8
|
from nautobot.core.celery import nautobot_task
|
|
9
|
+
from nautobot.core.models.query_functions import JSONRemove, JSONSet
|
|
10
10
|
from nautobot.extras.choices import CustomFieldTypeChoices, ObjectChangeActionChoices
|
|
11
11
|
from nautobot.extras.utils import generate_signature
|
|
12
12
|
|
|
13
13
|
logger = getLogger("nautobot.extras.tasks")
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
def _generate_bulk_object_changes(context, queryset, task_logger):
|
|
17
|
+
# Circular import
|
|
18
|
+
from nautobot.extras.context_managers import (
|
|
19
|
+
change_logging,
|
|
20
|
+
ChangeContext,
|
|
21
|
+
deferred_change_logging_for_bulk_operation,
|
|
22
|
+
)
|
|
23
|
+
from nautobot.extras.signals import _handle_changed_object
|
|
24
|
+
|
|
25
|
+
task_logger.info("Creating deferred ObjectChange records for bulk operation...")
|
|
26
|
+
|
|
27
|
+
# Note: we use change_logging() here instead of web_request_context() because we don't want these change records to
|
|
28
|
+
# trigger jobhooks and webhooks.
|
|
29
|
+
# TODO: this could be made much faster if we ensure the queryset has appropriate select_related/prefetch_related?
|
|
30
|
+
change_context = ChangeContext(**context)
|
|
31
|
+
i = 0
|
|
32
|
+
with change_logging(change_context):
|
|
33
|
+
with deferred_change_logging_for_bulk_operation():
|
|
34
|
+
for i, instance in enumerate(queryset.iterator(), start=1):
|
|
35
|
+
_handle_changed_object(queryset.model, instance, created=False)
|
|
36
|
+
|
|
37
|
+
task_logger.info("Created %d ObjectChange records", i)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@nautobot_task(soft_time_limit=1800, time_limit=2000)
|
|
17
41
|
def update_custom_field_choice_data(field_id, old_value, new_value, change_context=None):
|
|
18
42
|
"""
|
|
19
43
|
Update the values for a custom field choice used in objects' _custom_field_data for the given field.
|
|
@@ -22,47 +46,48 @@ def update_custom_field_choice_data(field_id, old_value, new_value, change_conte
|
|
|
22
46
|
field_id (uuid4): The PK of the custom field to which this choice value relates
|
|
23
47
|
old_value (str): The existing value of the choice
|
|
24
48
|
new_value (str): The value which will be used as replacement
|
|
49
|
+
change_context (dict): Optional dict representation of change context for ObjectChange creation
|
|
25
50
|
"""
|
|
26
51
|
# Circular Import
|
|
27
52
|
from nautobot.extras.context_managers import web_request_context
|
|
28
53
|
from nautobot.extras.models import CustomField
|
|
29
54
|
|
|
55
|
+
task_logger = getLogger("celery.task.update_custom_field_choice_data")
|
|
56
|
+
|
|
30
57
|
try:
|
|
31
58
|
field = CustomField.objects.get(pk=field_id)
|
|
32
59
|
except CustomField.DoesNotExist:
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
task_logger.error("Custom field with ID %s not found, failing to act on choice data.", field_id)
|
|
61
|
+
raise
|
|
35
62
|
|
|
36
63
|
if field.type == CustomFieldTypeChoices.TYPE_SELECT:
|
|
37
64
|
# Loop through all field content types and search for values to update
|
|
38
65
|
for ct in field.content_types.all():
|
|
39
66
|
model = ct.model_class()
|
|
67
|
+
queryset = model.objects.filter(**{f"_custom_field_data__{field.key}": old_value})
|
|
40
68
|
if change_context is not None:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
pk_list = list(queryset.values_list("pk", flat=True))
|
|
70
|
+
task_logger.info(
|
|
71
|
+
"Updating selection for custom field `%s` from `%s` to `%s` on %s records...",
|
|
72
|
+
field.key,
|
|
73
|
+
old_value,
|
|
74
|
+
new_value,
|
|
75
|
+
ct.model,
|
|
76
|
+
extra={"object": field},
|
|
77
|
+
)
|
|
78
|
+
count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, new_value))
|
|
79
|
+
task_logger.info("Updated %d records", count)
|
|
80
|
+
if change_context is not None:
|
|
81
|
+
# Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
|
|
82
|
+
_generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
|
|
54
83
|
|
|
55
84
|
elif field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
|
|
56
85
|
# Loop through all field content types and search for values to update
|
|
86
|
+
# TODO: can we implement a bulk operator for this?
|
|
57
87
|
for ct in field.content_types.all():
|
|
58
88
|
model = ct.model_class()
|
|
59
89
|
if change_context is not None:
|
|
60
|
-
with web_request_context(
|
|
61
|
-
user=change_context.get("user"),
|
|
62
|
-
change_id=change_context.get("change_id"),
|
|
63
|
-
context_detail=change_context.get("context_detail"),
|
|
64
|
-
context=change_context.get("context"),
|
|
65
|
-
):
|
|
90
|
+
with web_request_context(**change_context):
|
|
66
91
|
for obj in model.objects.filter(**{f"_custom_field_data__{field.key}__contains": old_value}):
|
|
67
92
|
old_list = obj._custom_field_data[field.key]
|
|
68
93
|
new_list = [new_value if e == old_value else e for e in old_list]
|
|
@@ -76,13 +101,13 @@ def update_custom_field_choice_data(field_id, old_value, new_value, change_conte
|
|
|
76
101
|
obj.save()
|
|
77
102
|
|
|
78
103
|
else:
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
task_logger.error(f"Unknown field type, failing to act on choice data for this field {field.key}.")
|
|
105
|
+
raise ValueError
|
|
81
106
|
|
|
82
107
|
return True
|
|
83
108
|
|
|
84
109
|
|
|
85
|
-
@nautobot_task
|
|
110
|
+
@nautobot_task(soft_time_limit=1800, time_limit=2000)
|
|
86
111
|
def delete_custom_field_data(field_key, content_type_pk_set, change_context=None):
|
|
87
112
|
"""
|
|
88
113
|
Delete the values for a custom field
|
|
@@ -90,30 +115,23 @@ def delete_custom_field_data(field_key, content_type_pk_set, change_context=None
|
|
|
90
115
|
Args:
|
|
91
116
|
field_key (str): The key of the custom field which is being deleted
|
|
92
117
|
content_type_pk_set (list): List of PKs for content types to act upon
|
|
118
|
+
change_context (dict): Optional change context for ObjectChange creation
|
|
93
119
|
"""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
obj.save()
|
|
110
|
-
else:
|
|
111
|
-
for obj in model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False}):
|
|
112
|
-
del obj._custom_field_data[field_key]
|
|
113
|
-
obj.save()
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@nautobot_task
|
|
120
|
+
task_logger = getLogger("celery.task.delete_custom_field_data")
|
|
121
|
+
for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
|
|
122
|
+
model = ct.model_class()
|
|
123
|
+
queryset = model.objects.filter(**{f"_custom_field_data__{field_key}__isnull": False})
|
|
124
|
+
if change_context is not None:
|
|
125
|
+
pk_list = list(queryset.values_list("pk", flat=True))
|
|
126
|
+
task_logger.info("Deleting existing values for custom field `%s` from %s records...", field_key, ct.model)
|
|
127
|
+
count = queryset.update(_custom_field_data=JSONRemove("_custom_field_data", field_key))
|
|
128
|
+
task_logger.info("Updated %d records", count)
|
|
129
|
+
if change_context is not None:
|
|
130
|
+
# Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
|
|
131
|
+
_generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@nautobot_task(soft_time_limit=1800, time_limit=2000)
|
|
117
135
|
def provision_field(field_id, content_type_pk_set, change_context=None):
|
|
118
136
|
"""
|
|
119
137
|
Provision a new custom field on all relevant content type object instances.
|
|
@@ -121,34 +139,35 @@ def provision_field(field_id, content_type_pk_set, change_context=None):
|
|
|
121
139
|
Args:
|
|
122
140
|
field_id (uuid4): The PK of the custom field being provisioned
|
|
123
141
|
content_type_pk_set (list): List of PKs for content types to act upon
|
|
142
|
+
change_context (dict): Optional change context for ObjectChange creation.
|
|
124
143
|
"""
|
|
125
144
|
# Circular Import
|
|
126
|
-
from nautobot.extras.context_managers import web_request_context
|
|
127
145
|
from nautobot.extras.models import CustomField
|
|
128
146
|
|
|
147
|
+
task_logger = getLogger("celery.task.provision_field")
|
|
148
|
+
|
|
129
149
|
try:
|
|
130
150
|
field = CustomField.objects.get(pk=field_id)
|
|
131
151
|
except CustomField.DoesNotExist:
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
task_logger.error(f"Custom field with ID {field_id} not found, failing to provision.")
|
|
153
|
+
raise
|
|
134
154
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
obj.save()
|
|
155
|
+
for ct in ContentType.objects.filter(pk__in=content_type_pk_set):
|
|
156
|
+
model = ct.model_class()
|
|
157
|
+
queryset = model.objects.filter(**{f"_custom_field_data__{field.key}__isnull": True})
|
|
158
|
+
if change_context is not None:
|
|
159
|
+
pk_list = list(queryset.values_list("pk", flat=True))
|
|
160
|
+
task_logger.info(
|
|
161
|
+
"Adding data for custom field `%s` to %s records...",
|
|
162
|
+
field.key,
|
|
163
|
+
ct.model,
|
|
164
|
+
extra={"object": field},
|
|
165
|
+
)
|
|
166
|
+
count = queryset.update(_custom_field_data=JSONSet("_custom_field_data", field.key, field.default))
|
|
167
|
+
task_logger.info("Updated %d records.", count)
|
|
168
|
+
if change_context is not None:
|
|
169
|
+
# Since we used update() above, we bypassed ObjectChange automatic creation via signals. Create them now
|
|
170
|
+
_generate_bulk_object_changes(change_context, model.objects.filter(pk__in=pk_list), task_logger)
|
|
152
171
|
|
|
153
172
|
return True
|
|
154
173
|
|
|
@@ -74,8 +74,8 @@ class WebRequestContextTestCase(TestCase):
|
|
|
74
74
|
self.assertEqual(oc_list[0].changed_object, location)
|
|
75
75
|
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
|
76
76
|
|
|
77
|
-
@mock.patch("nautobot.extras.jobs.enqueue_job_hooks", return_value=True)
|
|
78
|
-
@mock.patch("nautobot.extras.context_managers.enqueue_webhooks")
|
|
77
|
+
@mock.patch("nautobot.extras.jobs.enqueue_job_hooks", return_value=(True, None))
|
|
78
|
+
@mock.patch("nautobot.extras.context_managers.enqueue_webhooks", return_value=None)
|
|
79
79
|
def test_create_then_delete(self, mock_enqueue_webhooks, mock_enqueue_job_hooks):
|
|
80
80
|
"""Test that a create followed by a delete is logged as two changes"""
|
|
81
81
|
location_type = LocationType.objects.get(name="Campus")
|
|
@@ -93,9 +93,14 @@ class WebRequestContextTestCase(TestCase):
|
|
|
93
93
|
self.assertEqual(oc_list[0].action, ObjectChangeActionChoices.ACTION_CREATE)
|
|
94
94
|
self.assertEqual(oc_list[1].action, ObjectChangeActionChoices.ACTION_DELETE)
|
|
95
95
|
mock_enqueue_job_hooks.assert_has_calls(
|
|
96
|
-
[
|
|
96
|
+
[
|
|
97
|
+
mock.call(oc_list[0], may_reload_jobs=True, jobhook_queryset=None),
|
|
98
|
+
mock.call(oc_list[1], may_reload_jobs=False, jobhook_queryset=None),
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
mock_enqueue_webhooks.assert_has_calls(
|
|
102
|
+
[mock.call(oc_list[0], webhook_queryset=None), mock.call(oc_list[1], webhook_queryset=None)]
|
|
97
103
|
)
|
|
98
|
-
mock_enqueue_webhooks.assert_has_calls([mock.call(oc_list[0]), mock.call(oc_list[1])])
|
|
99
104
|
|
|
100
105
|
def test_update_then_delete(self):
|
|
101
106
|
"""Test that an update followed by a delete is logged as a single delete"""
|
|
@@ -1395,6 +1395,7 @@ class RequiredRelationshipTestMixin:
|
|
|
1395
1395
|
# Protected FK to SoftwareVersion prevents deletion
|
|
1396
1396
|
Controller.objects.all().delete()
|
|
1397
1397
|
Device.objects.all().update(software_version=None)
|
|
1398
|
+
Device.objects.all().delete()
|
|
1398
1399
|
|
|
1399
1400
|
# Create required relationships:
|
|
1400
1401
|
device_ct = ContentType.objects.get_for_model(Device)
|
|
@@ -345,7 +345,7 @@ class WebhookTest(APITestCase):
|
|
|
345
345
|
|
|
346
346
|
all_changes = get_changes_for_model(location)
|
|
347
347
|
self.assertEqual(all_changes.count(), 1)
|
|
348
|
-
mock_enqueue_webhooks.assert_called_once_with(all_changes.first())
|
|
348
|
+
mock_enqueue_webhooks.assert_called_once_with(all_changes.first(), webhook_queryset=None)
|
|
349
349
|
|
|
350
350
|
def test_all_webhook_supported_models(self):
|
|
351
351
|
"""
|
nautobot/extras/views.py
CHANGED
nautobot/extras/webhooks.py
CHANGED
|
@@ -6,16 +6,22 @@ from nautobot.extras.registry import registry
|
|
|
6
6
|
from nautobot.extras.tasks import process_webhook
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def enqueue_webhooks(object_change):
|
|
9
|
+
def enqueue_webhooks(object_change, webhook_queryset=None):
|
|
10
10
|
"""
|
|
11
|
-
Find Webhook(s) assigned to this instance + action and enqueue them
|
|
12
|
-
|
|
11
|
+
Find Webhook(s) assigned to this instance + action and enqueue them to be processed.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
object_change (ObjectChange): The change that may trigger Webhooks to be sent.
|
|
15
|
+
webhook_queryset (QuerySet): Previously retrieved set of Webhooks to potentially send.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
webhook_queryset (QuerySet): for reuse when processing multiple ObjectChange with the same content-type+action.
|
|
13
19
|
"""
|
|
14
20
|
# Determine whether this type of object supports webhooks
|
|
15
21
|
app_label = object_change.changed_object_type.app_label
|
|
16
22
|
model_name = object_change.changed_object_type.model
|
|
17
23
|
if model_name not in registry["model_features"]["webhooks"].get(app_label, []):
|
|
18
|
-
return
|
|
24
|
+
return webhook_queryset
|
|
19
25
|
|
|
20
26
|
# Retrieve any applicable Webhooks
|
|
21
27
|
content_type = object_change.changed_object_type
|
|
@@ -24,16 +30,17 @@ def enqueue_webhooks(object_change):
|
|
|
24
30
|
ObjectChangeActionChoices.ACTION_UPDATE: "type_update",
|
|
25
31
|
ObjectChangeActionChoices.ACTION_DELETE: "type_delete",
|
|
26
32
|
}[object_change.action]
|
|
27
|
-
|
|
33
|
+
if webhook_queryset is None:
|
|
34
|
+
webhook_queryset = Webhook.objects.filter(content_types=content_type, enabled=True, **{action_flag: True})
|
|
28
35
|
|
|
29
|
-
if
|
|
36
|
+
if webhook_queryset: # not .exists() as we *want* to populate the queryset cache
|
|
30
37
|
# fall back to object_data if object_data_v2 is not available
|
|
31
38
|
serialized_data = object_change.object_data_v2
|
|
32
39
|
if serialized_data is None:
|
|
33
40
|
serialized_data = object_change.object_data
|
|
34
41
|
|
|
35
42
|
# Enqueue the webhooks
|
|
36
|
-
for webhook in
|
|
43
|
+
for webhook in webhook_queryset:
|
|
37
44
|
args = [
|
|
38
45
|
webhook.pk,
|
|
39
46
|
serialized_data,
|
|
@@ -45,3 +52,5 @@ def enqueue_webhooks(object_change):
|
|
|
45
52
|
object_change.get_snapshots(),
|
|
46
53
|
]
|
|
47
54
|
process_webhook.apply_async(args=args)
|
|
55
|
+
|
|
56
|
+
return webhook_queryset
|
nautobot/ipam/factory.py
CHANGED
|
@@ -238,6 +238,9 @@ class VLANFactory(PrimaryModelFactory):
|
|
|
238
238
|
lambda: Location.objects.filter(location_type__content_types__in=[vlan_ct]), minimum=0
|
|
239
239
|
)
|
|
240
240
|
)
|
|
241
|
+
if self.vlan_group and self.vlan_group.location:
|
|
242
|
+
# add the parent of the vlan group location to the vlan locations
|
|
243
|
+
self.locations.add(self.vlan_group.location.ancestors(include_self=True)[0])
|
|
241
244
|
|
|
242
245
|
|
|
243
246
|
class VLANGetOrCreateFactory(VLANFactory):
|
nautobot/ipam/filters.py
CHANGED
|
@@ -90,6 +90,11 @@ class VRFFilterSet(NautobotFilterSet, StatusModelFilterSetMixin, TenancyModelFil
|
|
|
90
90
|
to_field_name="name",
|
|
91
91
|
label="Device (ID or name)",
|
|
92
92
|
)
|
|
93
|
+
virtual_machines = NaturalKeyOrPKMultipleChoiceFilter(
|
|
94
|
+
queryset=VirtualMachine.objects.all(),
|
|
95
|
+
to_field_name="name",
|
|
96
|
+
label="Virtual Machine (ID or name)",
|
|
97
|
+
)
|
|
93
98
|
prefix = NaturalKeyOrPKMultipleChoiceFilter(
|
|
94
99
|
field_name="prefixes",
|
|
95
100
|
queryset=Prefix.objects.all(),
|
nautobot/ipam/forms.py
CHANGED
|
@@ -752,6 +752,7 @@ class VLANForm(NautobotModelForm, TenancyForm):
|
|
|
752
752
|
)
|
|
753
753
|
vlan_group = DynamicModelChoiceField(
|
|
754
754
|
queryset=VLANGroup.objects.all(),
|
|
755
|
+
query_params={"location": "$locations"},
|
|
755
756
|
required=False,
|
|
756
757
|
)
|
|
757
758
|
|
|
@@ -779,6 +780,22 @@ class VLANForm(NautobotModelForm, TenancyForm):
|
|
|
779
780
|
}
|
|
780
781
|
|
|
781
782
|
def clean(self):
|
|
783
|
+
vlan_group = self.cleaned_data["vlan_group"]
|
|
784
|
+
locations = self.cleaned_data["locations"]
|
|
785
|
+
# Validate Vlan Group Location is one of the ancestors of the VLAN locations specified.
|
|
786
|
+
if vlan_group and vlan_group.location and locations:
|
|
787
|
+
vlan_group_location = vlan_group.location
|
|
788
|
+
is_vlan_group_valid = False
|
|
789
|
+
for location in locations:
|
|
790
|
+
if vlan_group_location in location.ancestors(include_self=True):
|
|
791
|
+
is_vlan_group_valid = True
|
|
792
|
+
break
|
|
793
|
+
|
|
794
|
+
if not is_vlan_group_valid:
|
|
795
|
+
locations = list(locations.values_list("name", flat=True))
|
|
796
|
+
raise ValidationError(
|
|
797
|
+
{"vlan_group": [f"VLAN Group {vlan_group} is not in locations {locations} or their ancestors."]}
|
|
798
|
+
)
|
|
782
799
|
# Validation error raised in signal is not properly handled in form clean
|
|
783
800
|
# Hence handling any validationError that might occur.
|
|
784
801
|
try:
|
nautobot/ipam/models.py
CHANGED
|
@@ -1296,7 +1296,8 @@ class IPAddressToInterface(BaseModel):
|
|
|
1296
1296
|
|
|
1297
1297
|
def __str__(self):
|
|
1298
1298
|
if self.interface:
|
|
1299
|
-
|
|
1299
|
+
parent_name = self.interface.parent.name if self.interface.parent else "No Parent"
|
|
1300
|
+
return f"{self.ip_address!s} {parent_name} {self.interface.name}"
|
|
1300
1301
|
else:
|
|
1301
1302
|
return f"{self.ip_address!s} {self.vm_interface.virtual_machine.name} {self.vm_interface.name}"
|
|
1302
1303
|
|
nautobot/ipam/signals.py
CHANGED
|
@@ -65,9 +65,9 @@ def ip_address_to_interface_pre_delete(instance, raw=False, **kwargs):
|
|
|
65
65
|
# that is the primary_v{version} of the host machine.
|
|
66
66
|
|
|
67
67
|
if getattr(instance, "interface"):
|
|
68
|
-
host = instance.interface.
|
|
68
|
+
host = instance.interface.parent
|
|
69
69
|
other_assignments_exist = (
|
|
70
|
-
IPAddressToInterface.objects.filter(
|
|
70
|
+
IPAddressToInterface.objects.filter(interface__in=host.all_interfaces, ip_address=instance.ip_address)
|
|
71
71
|
.exclude(id=instance.id)
|
|
72
72
|
.exists()
|
|
73
73
|
)
|
nautobot/ipam/tables.py
CHANGED
|
@@ -346,7 +346,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
346
346
|
prefix = tables.TemplateColumn(
|
|
347
347
|
template_code=PREFIX_COPY_LINK, attrs={"td": {"class": "text-nowrap"}}, order_by=("network", "prefix_length")
|
|
348
348
|
)
|
|
349
|
-
|
|
349
|
+
vrf_count = LinkedCountColumn(viewname="ipam:vrf_list", url_params={"prefixes": "pk"}, verbose_name="VRFs")
|
|
350
350
|
tenant = TenantColumn()
|
|
351
351
|
namespace = tables.Column(linkify=True)
|
|
352
352
|
vlan = tables.Column(linkify=True, verbose_name="VLAN")
|
|
@@ -368,7 +368,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
368
368
|
"type",
|
|
369
369
|
"status",
|
|
370
370
|
"children",
|
|
371
|
-
|
|
371
|
+
"vrf_count",
|
|
372
372
|
"namespace",
|
|
373
373
|
"tenant",
|
|
374
374
|
"location_count",
|
|
@@ -384,7 +384,7 @@ class PrefixTable(StatusTableMixin, RoleTableMixin, BaseTable):
|
|
|
384
384
|
"prefix",
|
|
385
385
|
"type",
|
|
386
386
|
"status",
|
|
387
|
-
|
|
387
|
+
"vrf_count",
|
|
388
388
|
"namespace",
|
|
389
389
|
"tenant",
|
|
390
390
|
"location_count",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
{% endif %}
|
|
19
19
|
{% endfor %}
|
|
20
20
|
<div class="row">
|
|
21
|
-
<div class="col-md-
|
|
21
|
+
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
|
|
22
22
|
<h3>Assign an IP Address</h3>
|
|
23
23
|
{% include 'ipam/inc/ipadress_edit_header.html' with active_tab='assign' %}
|
|
24
24
|
{% if form.non_field_errors %}
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
</div>
|
|
39
39
|
</div>
|
|
40
40
|
<div class="row">
|
|
41
|
-
<div class="col-md-
|
|
41
|
+
<div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1 text-right">
|
|
42
42
|
<button type="submit" class="btn btn-primary">Search</button>
|
|
43
43
|
<a href="{{ return_url }}" class="btn btn-default">Cancel</a>
|
|
44
44
|
</div>
|
|
@@ -9,7 +9,7 @@ import netaddr
|
|
|
9
9
|
|
|
10
10
|
from nautobot.core.testing.models import ModelTestCases
|
|
11
11
|
from nautobot.dcim import choices as dcim_choices
|
|
12
|
-
from nautobot.dcim.models import Device, DeviceType, Interface, Location, LocationType
|
|
12
|
+
from nautobot.dcim.models import Device, DeviceType, Interface, Location, LocationType, Module, ModuleBay, ModuleType
|
|
13
13
|
from nautobot.extras.models import Role, Status
|
|
14
14
|
from nautobot.ipam.choices import IPAddressTypeChoices, PrefixTypeChoices, ServiceProtocolChoices
|
|
15
15
|
from nautobot.ipam.models import (
|
|
@@ -171,6 +171,118 @@ class IPAddressToInterfaceTest(TestCase):
|
|
|
171
171
|
IPAddressToInterface.objects.create(vm_interface=None, interface=None, ip_address=ip_addr)
|
|
172
172
|
self.assertIn("Must associate to either an Interface or a VMInterface.", str(cm.exception))
|
|
173
173
|
|
|
174
|
+
def test_primary_ip_retained_when_deleted_from_device_or_module_interface(self):
|
|
175
|
+
"""Test primary_ip4 remains set when the same IP is assigned to multiple interfaces and deleted from one."""
|
|
176
|
+
|
|
177
|
+
# Create a module bay on the existing device
|
|
178
|
+
device_module_bay = ModuleBay.objects.create(parent_device=self.test_device, name="Test Bay")
|
|
179
|
+
|
|
180
|
+
# Create a module with an interface and add it to the module bay on the device
|
|
181
|
+
module = Module.objects.create(
|
|
182
|
+
module_type=ModuleType.objects.first(),
|
|
183
|
+
status=Status.objects.get_for_model(Module).first(),
|
|
184
|
+
parent_module_bay=device_module_bay,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Set status for the module interface
|
|
188
|
+
int_status = Status.objects.get_for_model(Interface).first()
|
|
189
|
+
|
|
190
|
+
# Create an interface on the module
|
|
191
|
+
interface_module = Interface.objects.create(
|
|
192
|
+
name="eth0_module",
|
|
193
|
+
module=module,
|
|
194
|
+
type=dcim_choices.InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
195
|
+
status=int_status,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Link the module to the device
|
|
199
|
+
self.test_device.installed_device = interface_module
|
|
200
|
+
self.test_device.save()
|
|
201
|
+
|
|
202
|
+
# Create IP and assign it to multiple interfaces
|
|
203
|
+
ip_address = IPAddress.objects.create(address="192.0.2.1/24", namespace=self.namespace, status=self.status)
|
|
204
|
+
assignment_device_int1 = IPAddressToInterface.objects.create(interface=self.test_int1, ip_address=ip_address)
|
|
205
|
+
assignment_module_int1 = IPAddressToInterface.objects.create(interface=interface_module, ip_address=ip_address)
|
|
206
|
+
|
|
207
|
+
# Set the primary IP on the device
|
|
208
|
+
self.test_device.primary_ip4 = assignment_device_int1.ip_address
|
|
209
|
+
self.test_device.save()
|
|
210
|
+
|
|
211
|
+
# Verify that the primary IP is set
|
|
212
|
+
self.assertEqual(self.test_device.primary_ip4, ip_address)
|
|
213
|
+
|
|
214
|
+
# Delete the IP assignment from one interface
|
|
215
|
+
assignment_device_int1.delete()
|
|
216
|
+
|
|
217
|
+
# Refresh and check that the primary IP is still assigned
|
|
218
|
+
self.test_device.refresh_from_db()
|
|
219
|
+
self.assertEqual(self.test_device.primary_ip4, ip_address)
|
|
220
|
+
|
|
221
|
+
# Verify remaining IP assignments on the IP object
|
|
222
|
+
remaining_assignments = ip_address.interface_assignments.all()
|
|
223
|
+
self.assertEqual(remaining_assignments.count(), 1)
|
|
224
|
+
self.assertIn(assignment_module_int1, remaining_assignments)
|
|
225
|
+
|
|
226
|
+
def test_primary_ip_retained_when_deleted_from_device_interface_with_nested_module(self):
|
|
227
|
+
"""Test primary_ip4 remains set when the same IP is assigned to a device and nested module interfaces, and deleted from the device interface."""
|
|
228
|
+
|
|
229
|
+
# Create a module bay on the existing device
|
|
230
|
+
device_module_bay = ModuleBay.objects.create(parent_device=self.test_device, name="Primary Module Bay")
|
|
231
|
+
|
|
232
|
+
# Create a primary module with an interface and add it to the module bay on the device
|
|
233
|
+
primary_module = Module.objects.create(
|
|
234
|
+
module_type=ModuleType.objects.first(),
|
|
235
|
+
status=Status.objects.get_for_model(Module).first(),
|
|
236
|
+
parent_module_bay=device_module_bay,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Create a secondary module bay within the primary module for nested module creation
|
|
240
|
+
nested_module_bay = ModuleBay.objects.create(parent_module=primary_module, name="Nested Module Bay")
|
|
241
|
+
|
|
242
|
+
# Create a nested module within the primary module's module bay
|
|
243
|
+
nested_module = Module.objects.create(
|
|
244
|
+
module_type=ModuleType.objects.first(),
|
|
245
|
+
status=Status.objects.get_for_model(Module).first(),
|
|
246
|
+
parent_module_bay=nested_module_bay,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Set status for the nested module interface
|
|
250
|
+
int_status = Status.objects.get_for_model(Interface).first()
|
|
251
|
+
|
|
252
|
+
# Create an interface on the nested module and assign an IP
|
|
253
|
+
nested_interface = Interface.objects.create(
|
|
254
|
+
name="eth0_nested",
|
|
255
|
+
module=nested_module,
|
|
256
|
+
type=dcim_choices.InterfaceTypeChoices.TYPE_1GE_FIXED,
|
|
257
|
+
status=int_status,
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# Create IP and assign it to both the device and the nested module interface
|
|
261
|
+
ip_address = IPAddress.objects.create(address="192.0.2.1/24", namespace=self.namespace, status=self.status)
|
|
262
|
+
assignment_device_int1 = IPAddressToInterface.objects.create(interface=self.test_int1, ip_address=ip_address)
|
|
263
|
+
assignment_nested_module = IPAddressToInterface.objects.create(
|
|
264
|
+
interface=nested_interface, ip_address=ip_address
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# Set the primary IP on the device to the IP on the device interface
|
|
268
|
+
self.test_device.primary_ip4 = assignment_nested_module.ip_address
|
|
269
|
+
self.test_device.save()
|
|
270
|
+
|
|
271
|
+
# Verify that the primary IP is correctly set
|
|
272
|
+
self.assertEqual(self.test_device.primary_ip4, ip_address)
|
|
273
|
+
|
|
274
|
+
# Delete the IP assignment from the device interface
|
|
275
|
+
assignment_device_int1.delete()
|
|
276
|
+
|
|
277
|
+
# Refresh and check that the primary IP is still assigned to the device
|
|
278
|
+
self.test_device.refresh_from_db()
|
|
279
|
+
self.assertEqual(self.test_device.primary_ip4, ip_address)
|
|
280
|
+
|
|
281
|
+
# Confirm that the IP is still associated with the nested module interface
|
|
282
|
+
remaining_assignments = ip_address.interface_assignments.all()
|
|
283
|
+
self.assertEqual(remaining_assignments.count(), 1)
|
|
284
|
+
self.assertIn(assignment_nested_module, remaining_assignments)
|
|
285
|
+
|
|
174
286
|
|
|
175
287
|
class TestVarbinaryIPField(TestCase):
|
|
176
288
|
"""Tests for `nautobot.ipam.fields.VarbinaryIPField`."""
|