nautobot 2.3.12__py3-none-any.whl → 2.3.13__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/core/api/serializers.py +1 -0
- nautobot/core/celery/log.py +4 -4
- nautobot/core/models/tree_queries.py +5 -2
- nautobot/core/settings.py +1 -1
- nautobot/core/tables.py +60 -10
- nautobot/core/templatetags/helpers.py +7 -1
- nautobot/core/testing/api.py +5 -1
- nautobot/core/tests/test_api.py +20 -0
- nautobot/core/tests/test_csv.py +25 -3
- nautobot/core/tests/test_utils.py +8 -0
- nautobot/core/utils/lookup.py +11 -8
- nautobot/dcim/api/views.py +3 -0
- nautobot/dcim/filters/__init__.py +26 -1
- nautobot/dcim/forms.py +4 -0
- nautobot/dcim/tests/test_filters.py +33 -0
- nautobot/dcim/tests/test_views.py +6 -0
- nautobot/extras/api/serializers.py +1 -0
- nautobot/extras/api/views.py +2 -0
- nautobot/extras/forms/forms.py +2 -0
- nautobot/extras/group_sync.py +3 -3
- nautobot/extras/plugins/__init__.py +13 -2
- nautobot/extras/tests/test_views.py +2 -0
- nautobot/ipam/lookups.py +101 -62
- nautobot/ipam/tables.py +18 -4
- nautobot/ipam/tests/test_querysets.py +49 -1
- nautobot/ipam/utils/__init__.py +24 -0
- nautobot/ipam/views.py +61 -68
- 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 +1 -1
- 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 +197 -5
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +1 -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 +16 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +1 -1
- 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 +23 -4
- 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 +614 -179
- 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 +401 -199
- 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 +3 -3
- 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 +1 -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/users/api/serializers.py +1 -0
- nautobot/virtualization/filters.py +19 -2
- nautobot/virtualization/tests/test_filters.py +9 -0
- {nautobot-2.3.12.dist-info → nautobot-2.3.13.dist-info}/METADATA +3 -3
- {nautobot-2.3.12.dist-info → nautobot-2.3.13.dist-info}/RECORD +312 -312
- {nautobot-2.3.12.dist-info → nautobot-2.3.13.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.12.dist-info → nautobot-2.3.13.dist-info}/NOTICE +0 -0
- {nautobot-2.3.12.dist-info → nautobot-2.3.13.dist-info}/WHEEL +0 -0
- {nautobot-2.3.12.dist-info → nautobot-2.3.13.dist-info}/entry_points.txt +0 -0
nautobot/core/api/serializers.py
CHANGED
|
@@ -134,6 +134,7 @@ class BaseModelSerializer(OptInFieldsMixin, serializers.HyperlinkedModelSerializ
|
|
|
134
134
|
|
|
135
135
|
serializer_related_field = NautobotHyperlinkedRelatedField
|
|
136
136
|
|
|
137
|
+
id = serializers.UUIDField(read_only=False, default=serializers.CreateOnlyDefault(uuid.uuid4))
|
|
137
138
|
display = serializers.SerializerMethodField(read_only=True, help_text="Human friendly display value")
|
|
138
139
|
object_type = ObjectTypeField()
|
|
139
140
|
# composite_key = serializers.SerializerMethodField() # TODO: Revisit if we reintroduce composite keys
|
nautobot/core/celery/log.py
CHANGED
|
@@ -11,6 +11,10 @@ class NautobotDatabaseHandler(logging.Handler):
|
|
|
11
11
|
if current_task is None:
|
|
12
12
|
return
|
|
13
13
|
|
|
14
|
+
# Skip recording the log entry if it has been marked as such
|
|
15
|
+
if getattr(record, "skip_db_logging", False):
|
|
16
|
+
return
|
|
17
|
+
|
|
14
18
|
from nautobot.extras.models.jobs import JobResult
|
|
15
19
|
|
|
16
20
|
try:
|
|
@@ -24,10 +28,6 @@ class NautobotDatabaseHandler(logging.Handler):
|
|
|
24
28
|
# JobResult.DoesNotExist - because we might not have a JobResult with that ID
|
|
25
29
|
return
|
|
26
30
|
|
|
27
|
-
# Skip recording the log entry if it has been marked as such
|
|
28
|
-
if getattr(record, "skip_db_logging", False):
|
|
29
|
-
return
|
|
30
|
-
|
|
31
31
|
job_result.log(
|
|
32
32
|
message=record.message,
|
|
33
33
|
level_choice=record.levelname.lower(),
|
|
@@ -111,8 +111,11 @@ class TreeModel(TreeNode):
|
|
|
111
111
|
if display_str:
|
|
112
112
|
return display_str
|
|
113
113
|
try:
|
|
114
|
-
if self.
|
|
115
|
-
|
|
114
|
+
if self.parent_id is not None:
|
|
115
|
+
parent_display_str = cache.get(cache_key.replace(str(self.id), str(self.parent_id)), "")
|
|
116
|
+
if not parent_display_str:
|
|
117
|
+
parent_display_str = self.parent.display
|
|
118
|
+
display_str = parent_display_str + " → "
|
|
116
119
|
except self.DoesNotExist:
|
|
117
120
|
# Expected to occur at times during bulk-delete operations
|
|
118
121
|
pass
|
nautobot/core/settings.py
CHANGED
|
@@ -225,7 +225,7 @@ if SSO_ENABLE_GROUP_SYNC:
|
|
|
225
225
|
"social_core.pipeline.social_auth.associate_user",
|
|
226
226
|
"social_core.pipeline.social_auth.load_extra_data",
|
|
227
227
|
"social_core.pipeline.user.user_details",
|
|
228
|
-
"nautobot.extras.group_sync",
|
|
228
|
+
"nautobot.extras.group_sync.group_sync",
|
|
229
229
|
)
|
|
230
230
|
# OAuth2/OIDC claim where the list of groups the authenticating user is a part of
|
|
231
231
|
SSO_CLAIMS_GROUP = os.getenv("NAUTOBOT_SSO_CLAIMS_GROUP", "groups")
|
nautobot/core/tables.py
CHANGED
|
@@ -14,7 +14,8 @@ from django.utils.http import urlencode
|
|
|
14
14
|
from django.utils.safestring import mark_safe
|
|
15
15
|
from django.utils.text import Truncator
|
|
16
16
|
import django_tables2
|
|
17
|
-
from django_tables2.data import TableQuerysetData
|
|
17
|
+
from django_tables2.data import TableData, TableQuerysetData
|
|
18
|
+
from django_tables2.rows import BoundRows
|
|
18
19
|
from django_tables2.utils import Accessor, OrderBy, OrderByTuple
|
|
19
20
|
from tree_queries.models import TreeNode
|
|
20
21
|
|
|
@@ -46,8 +47,24 @@ class BaseTable(django_tables2.Table):
|
|
|
46
47
|
user=None,
|
|
47
48
|
hide_hierarchy_ui=False,
|
|
48
49
|
order_by=None,
|
|
50
|
+
data_transform_callback=None,
|
|
49
51
|
**kwargs,
|
|
50
52
|
):
|
|
53
|
+
"""
|
|
54
|
+
Instantiate a BaseTable.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
*args (list, optional): Passed through to django_tables2.Table
|
|
58
|
+
table_changes_pending (bool): TODO
|
|
59
|
+
saved_view (SavedView, optional): TODO
|
|
60
|
+
user (User, optional): TODO
|
|
61
|
+
hide_hierarchy_ui (bool): Whether to display or hide hierarchy indentation of nested objects.
|
|
62
|
+
order_by (list, optional): Field(s) to sort by
|
|
63
|
+
data_transform_callback (function, optional): A function that takes the given `data` as an input and
|
|
64
|
+
returns new data. Runs after all of the queryset auto-optimization performed by this class.
|
|
65
|
+
Used for example in IPAM views to inject "fake" records for "available" Prefixes, IPAddresses, or VLANs.
|
|
66
|
+
**kwargs (dict, optional): Passed through to django_tables2.Table
|
|
67
|
+
"""
|
|
51
68
|
# Add custom field columns
|
|
52
69
|
model = self._meta.model
|
|
53
70
|
|
|
@@ -89,6 +106,14 @@ class BaseTable(django_tables2.Table):
|
|
|
89
106
|
# Init table
|
|
90
107
|
super().__init__(*args, order_by=order_by, **kwargs)
|
|
91
108
|
|
|
109
|
+
if not isinstance(self.data, TableQuerysetData):
|
|
110
|
+
# LinkedCountColumns don't work properly if the data is a list of dicts instead of a queryset,
|
|
111
|
+
# as they rely on a `queryset.annotate()` call to gather the data.
|
|
112
|
+
self.exclude = [
|
|
113
|
+
*self.exclude,
|
|
114
|
+
*[column.name for column in self.columns if isinstance(column.column, LinkedCountColumn)],
|
|
115
|
+
]
|
|
116
|
+
|
|
92
117
|
# Don't show hierarchy if we're sorted
|
|
93
118
|
if order_by is not None and hide_hierarchy_ui is None:
|
|
94
119
|
hide_hierarchy_ui = True
|
|
@@ -122,7 +147,7 @@ class BaseTable(django_tables2.Table):
|
|
|
122
147
|
columns = user.get_config(f"tables.{self.__class__.__name__}.columns")
|
|
123
148
|
if columns:
|
|
124
149
|
for name, column in self.base_columns.items():
|
|
125
|
-
if name in columns:
|
|
150
|
+
if name in columns and name not in self.exclude:
|
|
126
151
|
self.columns.show(name)
|
|
127
152
|
else:
|
|
128
153
|
self.columns.hide(name)
|
|
@@ -161,7 +186,8 @@ class BaseTable(django_tables2.Table):
|
|
|
161
186
|
logger.error("Couldn't find model for %s", column.column.viewname)
|
|
162
187
|
continue
|
|
163
188
|
reverse_lookup = column.column.reverse_lookup or next(iter(column.column.url_params.keys()))
|
|
164
|
-
|
|
189
|
+
distinct = column.column.distinct
|
|
190
|
+
count_fields.append((column.name, column_model, reverse_lookup, distinct))
|
|
165
191
|
try:
|
|
166
192
|
lookup = column.column.lookup or get_related_field_for_models(model, column_model).name
|
|
167
193
|
# For some reason get_related_field_for_models(Tag, DynamicGroup) gives a M2M with the name
|
|
@@ -173,7 +199,8 @@ class BaseTable(django_tables2.Table):
|
|
|
173
199
|
if lookup is not None:
|
|
174
200
|
# Also attempt to prefetch the first matching record for display - see LinkedCountColumn
|
|
175
201
|
prefetch_fields.append(
|
|
176
|
-
|
|
202
|
+
# Use order_by() because we don't care about ordering here and it's potentially expensive
|
|
203
|
+
Prefetch(lookup, column_model.objects.order_by()[:1], to_attr=f"{lookup}_list")
|
|
177
204
|
)
|
|
178
205
|
continue
|
|
179
206
|
|
|
@@ -256,24 +283,34 @@ class BaseTable(django_tables2.Table):
|
|
|
256
283
|
)
|
|
257
284
|
|
|
258
285
|
if count_fields:
|
|
259
|
-
for column_name, column_model, lookup_name in count_fields:
|
|
286
|
+
for column_name, column_model, lookup_name, distinct in count_fields:
|
|
260
287
|
if hasattr(queryset.first(), column_name):
|
|
261
288
|
continue
|
|
262
289
|
try:
|
|
263
290
|
logger.debug(
|
|
264
|
-
"Applying .annotate(%s=count_related(%s, %r) to %s QuerySet",
|
|
291
|
+
"Applying .annotate(%s=count_related(%s, %r, distinct=%s) to %s QuerySet",
|
|
265
292
|
column_name,
|
|
266
293
|
column_model.__name__,
|
|
267
294
|
lookup_name,
|
|
295
|
+
distinct,
|
|
268
296
|
model.__name__,
|
|
269
297
|
)
|
|
270
|
-
queryset = queryset.annotate(
|
|
298
|
+
queryset = queryset.annotate(
|
|
299
|
+
**{column_name: count_related(column_model, lookup_name, distinct=distinct)}
|
|
300
|
+
)
|
|
271
301
|
except FieldError:
|
|
272
302
|
# No error message logged here as the above is *very much* best-effort
|
|
273
303
|
pass
|
|
274
304
|
|
|
275
305
|
self.data.data = queryset
|
|
276
306
|
|
|
307
|
+
# TODO: it would be better if we could apply this transformation and the above queryset optimizations
|
|
308
|
+
# **before** calling super().__init__(), but the current implementation works for now, though inelegant.
|
|
309
|
+
if data_transform_callback is not None:
|
|
310
|
+
self.data = TableData.from_data(data_transform_callback(self.data.data))
|
|
311
|
+
self.data.set_table(self)
|
|
312
|
+
self.rows = BoundRows(data=self.data, table=self, pinned_data=self.pinned_data)
|
|
313
|
+
|
|
277
314
|
@property
|
|
278
315
|
def configurable_columns(self):
|
|
279
316
|
selected_columns = [
|
|
@@ -288,7 +325,7 @@ class BaseTable(django_tables2.Table):
|
|
|
288
325
|
|
|
289
326
|
@property
|
|
290
327
|
def visible_columns(self):
|
|
291
|
-
return [name for name in self.sequence if self.columns[name].visible]
|
|
328
|
+
return [name for name in self.sequence if self.columns[name].visible and name not in self.exclude]
|
|
292
329
|
|
|
293
330
|
@property
|
|
294
331
|
def order_by(self):
|
|
@@ -490,6 +527,7 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
490
527
|
TODO: this currently does *not* support nested lookups via `__`. That may be solvable in the future.
|
|
491
528
|
reverse_lookup (str, optional): The reverse lookup parameter to use to derive the count.
|
|
492
529
|
If not specified, the first key in `url_params` will be implicitly used as the `reverse_lookup` value.
|
|
530
|
+
distinct (bool, optional): Parameter passed through to `count_related()`.
|
|
493
531
|
**kwargs (dict, optional): As the parent Column class.
|
|
494
532
|
|
|
495
533
|
Examples:
|
|
@@ -514,21 +552,33 @@ class LinkedCountColumn(django_tables2.Column):
|
|
|
514
552
|
# We'd like to do the below but this module isn't currently smart enough to build the right Prefetch()
|
|
515
553
|
# for a nested lookup:
|
|
516
554
|
# lookup="circuit_terminations__circuit",
|
|
517
|
-
# For the count,
|
|
555
|
+
# For the count,
|
|
556
|
+
# .annotate(circuit_count=count_related(Circuit, "circuit_terminations__cloud_network", distinct=True))
|
|
518
557
|
reverse_lookup="circuit_terminations__cloud_network",
|
|
558
|
+
distinct=True,
|
|
519
559
|
verbose_name="Circuits",
|
|
520
560
|
)
|
|
521
561
|
```
|
|
522
562
|
"""
|
|
523
563
|
|
|
524
564
|
def __init__(
|
|
525
|
-
self,
|
|
565
|
+
self,
|
|
566
|
+
viewname,
|
|
567
|
+
*args,
|
|
568
|
+
view_kwargs=None,
|
|
569
|
+
url_params=None,
|
|
570
|
+
lookup=None,
|
|
571
|
+
reverse_lookup=None,
|
|
572
|
+
distinct=False,
|
|
573
|
+
default=None,
|
|
574
|
+
**kwargs,
|
|
526
575
|
):
|
|
527
576
|
self.viewname = viewname
|
|
528
577
|
self.lookup = lookup
|
|
529
578
|
self.view_kwargs = view_kwargs or {}
|
|
530
579
|
self.url_params = url_params
|
|
531
580
|
self.reverse_lookup = reverse_lookup or next(iter(url_params.keys()))
|
|
581
|
+
self.distinct = distinct
|
|
532
582
|
self.model = get_model_for_view_name(self.viewname)
|
|
533
583
|
super().__init__(*args, default=default, **kwargs)
|
|
534
584
|
|
|
@@ -795,7 +795,13 @@ def saved_view_modal(
|
|
|
795
795
|
"clear_view",
|
|
796
796
|
]
|
|
797
797
|
|
|
798
|
-
|
|
798
|
+
view_class = lookup.get_view_for_model(model, "List")
|
|
799
|
+
table_name = None
|
|
800
|
+
if hasattr(view_class, "table"):
|
|
801
|
+
table_name = view_class.table.__name__
|
|
802
|
+
if hasattr(view_class, "table_class"):
|
|
803
|
+
table_name = view_class.table_class.__name__
|
|
804
|
+
|
|
799
805
|
for param in non_filter_params:
|
|
800
806
|
if param == "saved_view":
|
|
801
807
|
current_saved_view_pk = filters_applied.pop(param, None)
|
nautobot/core/testing/api.py
CHANGED
|
@@ -707,7 +707,11 @@ class APIViewTestCases:
|
|
|
707
707
|
self.assertHttpStatus(response, status.HTTP_201_CREATED, csv_data)
|
|
708
708
|
# Note that create via CSV is always treated as a bulk-create, and so the response is always a list of dicts
|
|
709
709
|
new_instance = self._get_queryset().get(pk=response.data[0]["id"])
|
|
710
|
-
|
|
710
|
+
if isinstance(orig_pk, int):
|
|
711
|
+
self.assertNotEqual(new_instance.pk, orig_pk)
|
|
712
|
+
else:
|
|
713
|
+
# for our non-integer PKs, we're expecting the creation to respect the requested PK
|
|
714
|
+
self.assertEqual(new_instance.pk, orig_pk)
|
|
711
715
|
|
|
712
716
|
new_serializer = serializer_class(new_instance, context={"request": None})
|
|
713
717
|
new_data = new_serializer.data
|
nautobot/core/tests/test_api.py
CHANGED
|
@@ -3,6 +3,7 @@ from io import BytesIO, StringIO
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
from unittest import skip
|
|
6
|
+
import uuid
|
|
6
7
|
|
|
7
8
|
from constance import config
|
|
8
9
|
from constance.test import override_config
|
|
@@ -754,6 +755,25 @@ class WritableNestedSerializerTest(testing.APITestCase):
|
|
|
754
755
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
755
756
|
self.assertEqual(ipam_models.VLAN.objects.filter(name="Test VLAN 100").count(), 0)
|
|
756
757
|
|
|
758
|
+
def test_create_with_specified_id(self):
|
|
759
|
+
data = {
|
|
760
|
+
"id": str(uuid.uuid4()),
|
|
761
|
+
"vid": 400,
|
|
762
|
+
"name": "Test VLAN 400",
|
|
763
|
+
"status": self.statuses.first().pk,
|
|
764
|
+
"vlan_group": self.vlan_group1.pk,
|
|
765
|
+
}
|
|
766
|
+
url = reverse("ipam-api:vlan-list")
|
|
767
|
+
self.add_permissions("ipam.add_vlan")
|
|
768
|
+
|
|
769
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
770
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
771
|
+
self.assertEqual(str(response.data["vlan_group"]["url"]), self.absolute_api_url(self.vlan_group1))
|
|
772
|
+
self.assertEqual(str(response.data["id"]), data["id"])
|
|
773
|
+
vlan = ipam_models.VLAN.objects.get(pk=response.data["id"])
|
|
774
|
+
self.assertEqual(vlan.status, self.statuses.first())
|
|
775
|
+
self.assertEqual(vlan.vlan_group, self.vlan_group1)
|
|
776
|
+
|
|
757
777
|
|
|
758
778
|
class APIOrderingTestCase(testing.APITestCase):
|
|
759
779
|
"""
|
nautobot/core/tests/test_csv.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import io
|
|
3
|
+
|
|
1
4
|
from django.contrib.contenttypes.models import ContentType
|
|
2
5
|
from django.test import override_settings, RequestFactory, TestCase
|
|
3
6
|
from django.urls import reverse
|
|
@@ -261,9 +264,28 @@ class CSVParsingRelatedTestCase(TestCase):
|
|
|
261
264
|
self.assertEqual(response.status_code, 200)
|
|
262
265
|
response_data = response.content.decode(response.charset)
|
|
263
266
|
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
|
|
267
|
+
# parse the csv data
|
|
268
|
+
csv_reader = csv.DictReader(response_data.splitlines())
|
|
269
|
+
# remove the 'id' column so that all the items are imported new
|
|
270
|
+
fieldnames = [field for field in csv_reader.fieldnames if field != "id"]
|
|
271
|
+
# read all entries into a list
|
|
272
|
+
response_csv = list(csv_reader)
|
|
273
|
+
|
|
274
|
+
# mutate our data for testing purposes
|
|
275
|
+
for row in response_csv:
|
|
276
|
+
if row["name"] == "TestDevice1":
|
|
277
|
+
row["name"] = "TestDevice3"
|
|
278
|
+
elif row["name"] == "TestDevice2":
|
|
279
|
+
row["name"] = ""
|
|
280
|
+
|
|
281
|
+
# prep our data to write out
|
|
282
|
+
with io.StringIO() as import_csv:
|
|
283
|
+
writer = csv.DictWriter(import_csv, fieldnames=fieldnames)
|
|
284
|
+
writer.writeheader()
|
|
285
|
+
for row in response_csv:
|
|
286
|
+
filtered_row = {key: row[key] for key in fieldnames}
|
|
287
|
+
writer.writerow(filtered_row)
|
|
288
|
+
data = {"csv_data": import_csv.getvalue()}
|
|
267
289
|
url = reverse("dcim:device_import")
|
|
268
290
|
response = self.client.post(url, data)
|
|
269
291
|
self.assertEqual(response.status_code, 200)
|
|
@@ -268,6 +268,14 @@ class GetFooForModelTest(TestCase):
|
|
|
268
268
|
lookup.get_model_for_view_name("unknown:plugins:example_app:examplemodel_list")
|
|
269
269
|
self.assertEqual(str(err.exception), "Unexpected View Name: unknown:plugins:example_app:examplemodel_list")
|
|
270
270
|
|
|
271
|
+
def test_get_table_class_string_from_view_name(self):
|
|
272
|
+
# Testing UIViewSet
|
|
273
|
+
self.assertEqual(lookup.get_table_class_string_from_view_name("circuits:circuit_list"), "CircuitTable")
|
|
274
|
+
# Testing Legacy View
|
|
275
|
+
self.assertEqual(lookup.get_table_class_string_from_view_name("dcim:location_list"), "LocationTable")
|
|
276
|
+
# Testing unconventional table name
|
|
277
|
+
self.assertEqual(lookup.get_table_class_string_from_view_name("ipam:prefix_list"), "PrefixDetailTable")
|
|
278
|
+
|
|
271
279
|
|
|
272
280
|
class IsTaggableTest(TestCase):
|
|
273
281
|
def test_is_taggable_true(self):
|
nautobot/core/utils/lookup.py
CHANGED
|
@@ -8,7 +8,7 @@ from django.conf import settings
|
|
|
8
8
|
from django.contrib.auth.models import Group
|
|
9
9
|
from django.contrib.contenttypes.models import ContentType
|
|
10
10
|
from django.db.models import Model
|
|
11
|
-
from django.urls import get_resolver, URLPattern, URLResolver
|
|
11
|
+
from django.urls import get_resolver, resolve, reverse, URLPattern, URLResolver
|
|
12
12
|
from django.utils.module_loading import import_string
|
|
13
13
|
|
|
14
14
|
|
|
@@ -212,7 +212,7 @@ def get_related_field_for_models(from_model, to_model):
|
|
|
212
212
|
return matching_field
|
|
213
213
|
|
|
214
214
|
|
|
215
|
-
def get_table_for_model(model):
|
|
215
|
+
def get_table_for_model(model, suffix=None):
|
|
216
216
|
"""Return the `Table` class associated with a given `model`.
|
|
217
217
|
|
|
218
218
|
The `Table` class is expected to be in the `tables` module within the application
|
|
@@ -222,11 +222,12 @@ def get_table_for_model(model):
|
|
|
222
222
|
|
|
223
223
|
Args:
|
|
224
224
|
model (BaseModel): A model class
|
|
225
|
+
suffix (str): A replacement suffix for the table name (e.g. `DetailTable`, such as to retrieve `FooDetailTable`)
|
|
225
226
|
|
|
226
227
|
Returns:
|
|
227
228
|
(Union[Table, None]): Either the `Table` class or `None`
|
|
228
229
|
"""
|
|
229
|
-
return get_related_class_for_model(model, module_name="tables", object_suffix="Table")
|
|
230
|
+
return get_related_class_for_model(model, module_name="tables", object_suffix=suffix or "Table")
|
|
230
231
|
|
|
231
232
|
|
|
232
233
|
def get_view_for_model(model, view_type=""):
|
|
@@ -275,11 +276,13 @@ def get_table_class_string_from_view_name(view_name):
|
|
|
275
276
|
Returns:
|
|
276
277
|
table_class_name (String): The name of the model table class or None e.g. LocationTable, CircuitTable
|
|
277
278
|
"""
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
279
|
+
|
|
280
|
+
view_func = resolve(reverse(view_name)).func
|
|
281
|
+
view_class = getattr(view_func, "cls", getattr(view_func, "view_class", None))
|
|
282
|
+
if hasattr(view_class, "table_class"):
|
|
283
|
+
return view_class.table_class.__name__
|
|
284
|
+
if hasattr(view_class, "table"):
|
|
285
|
+
return view_class.table.__name__
|
|
283
286
|
return None
|
|
284
287
|
|
|
285
288
|
|
nautobot/dcim/api/views.py
CHANGED
|
@@ -11,6 +11,7 @@ from drf_spectacular.types import OpenApiTypes
|
|
|
11
11
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
|
12
12
|
from rest_framework.decorators import action
|
|
13
13
|
from rest_framework.mixins import ListModelMixin
|
|
14
|
+
from rest_framework.parsers import JSONParser, MultiPartParser
|
|
14
15
|
from rest_framework.permissions import IsAuthenticated
|
|
15
16
|
from rest_framework.response import Response
|
|
16
17
|
from rest_framework.viewsets import GenericViewSet, ViewSet
|
|
@@ -18,6 +19,7 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
|
|
|
18
19
|
from nautobot.circuits.models import Circuit
|
|
19
20
|
from nautobot.cloud.models import CloudAccount
|
|
20
21
|
from nautobot.core.api.exceptions import ServiceUnavailable
|
|
22
|
+
from nautobot.core.api.parsers import NautobotCSVParser
|
|
21
23
|
from nautobot.core.api.utils import get_serializer_for_model
|
|
22
24
|
from nautobot.core.api.views import ModelViewSet
|
|
23
25
|
from nautobot.core.models.querysets import count_related
|
|
@@ -300,6 +302,7 @@ class DeviceTypeViewSet(NautobotModelViewSet):
|
|
|
300
302
|
)
|
|
301
303
|
serializer_class = serializers.DeviceTypeSerializer
|
|
302
304
|
filterset_class = filters.DeviceTypeFilterSet
|
|
305
|
+
parser_classes = [JSONParser, NautobotCSVParser, MultiPartParser]
|
|
303
306
|
|
|
304
307
|
|
|
305
308
|
#
|
|
@@ -903,6 +903,19 @@ class DeviceFilterSet(
|
|
|
903
903
|
to_field_name="version",
|
|
904
904
|
label="Software version (version or ID)",
|
|
905
905
|
)
|
|
906
|
+
ip_addresses = MultiValueCharFilter(
|
|
907
|
+
method="filter_ip_addresses",
|
|
908
|
+
label="IP addresses (address or ID)",
|
|
909
|
+
distinct=True,
|
|
910
|
+
)
|
|
911
|
+
has_ip_addresses = RelatedMembershipBooleanFilter(field_name="interfaces__ip_addresses", label="Has IP addresses")
|
|
912
|
+
|
|
913
|
+
def filter_ip_addresses(self, queryset, name, value):
|
|
914
|
+
pk_values = set(item for item in value if is_uuid(item))
|
|
915
|
+
addresses = set(item for item in value if item not in pk_values)
|
|
916
|
+
|
|
917
|
+
ip_queryset = IPAddress.objects.filter_address_or_pk_in(addresses, pk_values)
|
|
918
|
+
return queryset.filter(interfaces__ip_addresses__in=ip_queryset).distinct()
|
|
906
919
|
|
|
907
920
|
class Meta:
|
|
908
921
|
model = Device
|
|
@@ -1130,6 +1143,19 @@ class InterfaceFilterSet(
|
|
|
1130
1143
|
queryset=InterfaceRedundancyGroup.objects.all(),
|
|
1131
1144
|
to_field_name="name",
|
|
1132
1145
|
)
|
|
1146
|
+
ip_addresses = MultiValueCharFilter(
|
|
1147
|
+
method="filter_ip_addresses",
|
|
1148
|
+
label="IP addresses (address or ID)",
|
|
1149
|
+
distinct=True,
|
|
1150
|
+
)
|
|
1151
|
+
has_ip_addresses = RelatedMembershipBooleanFilter(field_name="ip_addresses", label="Has IP addresses")
|
|
1152
|
+
|
|
1153
|
+
def filter_ip_addresses(self, queryset, name, value):
|
|
1154
|
+
pk_values = set(item for item in value if is_uuid(item))
|
|
1155
|
+
addresses = set(item for item in value if item not in pk_values)
|
|
1156
|
+
|
|
1157
|
+
ip_queryset = IPAddress.objects.filter_address_or_pk_in(addresses, pk_values)
|
|
1158
|
+
return queryset.filter(ip_addresses__in=ip_queryset).distinct()
|
|
1133
1159
|
|
|
1134
1160
|
class Meta:
|
|
1135
1161
|
model = Interface
|
|
@@ -1144,7 +1170,6 @@ class InterfaceFilterSet(
|
|
|
1144
1170
|
"description",
|
|
1145
1171
|
"label",
|
|
1146
1172
|
"tags",
|
|
1147
|
-
"interface_redundancy_groups",
|
|
1148
1173
|
]
|
|
1149
1174
|
|
|
1150
1175
|
def generate_query_filter_device(self, value):
|
nautobot/dcim/forms.py
CHANGED
|
@@ -2123,6 +2123,8 @@ class DeviceBulkEditForm(
|
|
|
2123
2123
|
rack_group = DynamicModelChoiceField(
|
|
2124
2124
|
queryset=RackGroup.objects.all(), required=False, query_params={"location": "$location"}
|
|
2125
2125
|
)
|
|
2126
|
+
cluster = DynamicModelChoiceField(queryset=Cluster.objects.all(), required=False)
|
|
2127
|
+
comments = CommentField(widget=SmallTextarea, label="Comments")
|
|
2126
2128
|
tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False)
|
|
2127
2129
|
platform = DynamicModelChoiceField(queryset=Platform.objects.all(), required=False)
|
|
2128
2130
|
serial = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False, label="Serial Number")
|
|
@@ -2146,6 +2148,8 @@ class DeviceBulkEditForm(
|
|
|
2146
2148
|
"position",
|
|
2147
2149
|
"face",
|
|
2148
2150
|
"rack_group",
|
|
2151
|
+
"cluster",
|
|
2152
|
+
"comments",
|
|
2149
2153
|
"secrets_group",
|
|
2150
2154
|
"device_redundancy_group",
|
|
2151
2155
|
"device_redundancy_group_priority",
|
|
@@ -1701,6 +1701,7 @@ class DeviceTestCase(
|
|
|
1701
1701
|
("front_ports", "front_ports__id"),
|
|
1702
1702
|
("interfaces", "interfaces__id"),
|
|
1703
1703
|
("interfaces", "interfaces__name"),
|
|
1704
|
+
("ip_addresses", "interfaces__ip_addresses__id"),
|
|
1704
1705
|
("mac_address", "interfaces__mac_address"),
|
|
1705
1706
|
("manufacturer", "device_type__manufacturer__id"),
|
|
1706
1707
|
("manufacturer", "device_type__manufacturer__name"),
|
|
@@ -1874,6 +1875,14 @@ class DeviceTestCase(
|
|
|
1874
1875
|
Device.objects.filter(primary_ip4__isnull=True, primary_ip6__isnull=True),
|
|
1875
1876
|
)
|
|
1876
1877
|
|
|
1878
|
+
def test_ip_addresses(self):
|
|
1879
|
+
addresses = list(IPAddress.objects.filter(interfaces__isnull=False)[:2])
|
|
1880
|
+
params = {"ip_addresses": [addresses[0].address, addresses[1].id]}
|
|
1881
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
1882
|
+
self.filterset(params, self.queryset).qs,
|
|
1883
|
+
self.queryset.filter(interfaces__ip_addresses__in=addresses).distinct(),
|
|
1884
|
+
)
|
|
1885
|
+
|
|
1877
1886
|
def test_virtual_chassis_member(self):
|
|
1878
1887
|
# TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
|
|
1879
1888
|
with self.subTest():
|
|
@@ -2168,6 +2177,7 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2168
2177
|
("child_interfaces", "child_interfaces__name"),
|
|
2169
2178
|
("description",),
|
|
2170
2179
|
# ("device", "device__id"), # TODO - InterfaceFilterSet overrides device as a MultiValueCharFilter on name only
|
|
2180
|
+
("ip_addresses", "ip_addresses__id"),
|
|
2171
2181
|
("label",),
|
|
2172
2182
|
("lag", "lag__id"),
|
|
2173
2183
|
("lag", "lag__name"),
|
|
@@ -2413,6 +2423,21 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2413
2423
|
status=interface_statuses[3],
|
|
2414
2424
|
)
|
|
2415
2425
|
|
|
2426
|
+
ipaddr_status = Status.objects.get_for_model(IPAddress).first()
|
|
2427
|
+
prefix_status = Status.objects.get_for_model(Prefix).first()
|
|
2428
|
+
namespace = Namespace.objects.first()
|
|
2429
|
+
Prefix.objects.create(prefix="192.0.2.0/24", namespace=namespace, status=prefix_status)
|
|
2430
|
+
Prefix.objects.create(prefix="2600::/64", namespace=namespace, status=prefix_status)
|
|
2431
|
+
ipaddresses = (
|
|
2432
|
+
IPAddress.objects.create(address="192.0.2.1/24", namespace=namespace, status=ipaddr_status),
|
|
2433
|
+
IPAddress.objects.create(address="192.0.2.2/24", namespace=namespace, status=ipaddr_status),
|
|
2434
|
+
IPAddress.objects.create(address="2600::1/120", namespace=namespace, status=ipaddr_status),
|
|
2435
|
+
IPAddress.objects.create(address="2600::0100/120", namespace=namespace, status=ipaddr_status),
|
|
2436
|
+
)
|
|
2437
|
+
|
|
2438
|
+
cabled_interfaces[0].add_ip_addresses([ipaddresses[0], ipaddresses[2]])
|
|
2439
|
+
cabled_interfaces[1].add_ip_addresses([ipaddresses[1], ipaddresses[3]])
|
|
2440
|
+
|
|
2416
2441
|
def test_enabled(self):
|
|
2417
2442
|
# TODO: Not a generic_filter_test because this is a boolean filter but not a RelatedMembershipBooleanFilter
|
|
2418
2443
|
with self.subTest():
|
|
@@ -2646,6 +2671,14 @@ class InterfaceTestCase(PathEndpointModelTestMixin, ModularDeviceComponentTestMi
|
|
|
2646
2671
|
with self.subTest("device (pk) filter with an invalid uuid"):
|
|
2647
2672
|
self.assertFalse(self.filterset({"device": [uuid.uuid4()]}, self.queryset).is_valid())
|
|
2648
2673
|
|
|
2674
|
+
def test_ip_addresses(self):
|
|
2675
|
+
addresses = list(IPAddress.objects.filter(interfaces__isnull=False)[:2])
|
|
2676
|
+
params = {"ip_addresses": [addresses[0].address, addresses[1].id]}
|
|
2677
|
+
self.assertQuerysetEqualAndNotEmpty(
|
|
2678
|
+
self.filterset(params, self.queryset).qs,
|
|
2679
|
+
self.queryset.filter(ip_addresses__in=addresses).distinct(),
|
|
2680
|
+
)
|
|
2681
|
+
|
|
2649
2682
|
def test_kind(self):
|
|
2650
2683
|
# TODO: Not a generic_filter_test because this is a single-value filter
|
|
2651
2684
|
# 2.0 TODO: Support filtering for multiple values
|
|
@@ -130,6 +130,7 @@ from nautobot.ipam.choices import IPAddressTypeChoices
|
|
|
130
130
|
from nautobot.ipam.models import IPAddress, Namespace, Prefix, VLAN, VLANGroup, VRF
|
|
131
131
|
from nautobot.tenancy.models import Tenant
|
|
132
132
|
from nautobot.users.models import ObjectPermission
|
|
133
|
+
from nautobot.virtualization.models import Cluster, ClusterType
|
|
133
134
|
|
|
134
135
|
# Use the proper swappable User model
|
|
135
136
|
User = get_user_model()
|
|
@@ -2047,6 +2048,9 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2047
2048
|
|
|
2048
2049
|
rack_group = RackGroup.objects.create(location=locations[0], name="Rack Group 1")
|
|
2049
2050
|
|
|
2051
|
+
cluster_type = ClusterType.objects.create(name="Cluster Type 1")
|
|
2052
|
+
cluster = Cluster.objects.create(name="Cluster 1", cluster_type=cluster_type)
|
|
2053
|
+
|
|
2050
2054
|
rack_status = Status.objects.get_for_model(Rack).first()
|
|
2051
2055
|
racks = (
|
|
2052
2056
|
Rack.objects.create(
|
|
@@ -2218,6 +2222,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2218
2222
|
"status": statuses[2].pk,
|
|
2219
2223
|
"location": locations[1].pk,
|
|
2220
2224
|
"rack": racks[1].pk,
|
|
2225
|
+
"cluster": cluster.pk,
|
|
2226
|
+
"comments": "An older device",
|
|
2221
2227
|
"position": None,
|
|
2222
2228
|
"face": DeviceFaceChoices.FACE_FRONT,
|
|
2223
2229
|
"secrets_group": secrets_groups[1].pk,
|
|
@@ -270,6 +270,7 @@ class ContactAssociationSerializer(NautobotModelSerializer):
|
|
|
270
270
|
|
|
271
271
|
|
|
272
272
|
class ContentTypeSerializer(BaseModelSerializer):
|
|
273
|
+
id = serializers.IntegerField(read_only=True)
|
|
273
274
|
url = serializers.HyperlinkedIdentityField(view_name="extras-api:contenttype-detail")
|
|
274
275
|
display = serializers.SerializerMethodField()
|
|
275
276
|
|
nautobot/extras/api/views.py
CHANGED
|
@@ -17,6 +17,7 @@ from rest_framework.permissions import IsAuthenticated
|
|
|
17
17
|
from rest_framework.response import Response
|
|
18
18
|
|
|
19
19
|
from nautobot.core.api.authentication import TokenPermissions
|
|
20
|
+
from nautobot.core.api.parsers import NautobotCSVParser
|
|
20
21
|
from nautobot.core.api.utils import get_serializer_for_model
|
|
21
22
|
from nautobot.core.api.views import (
|
|
22
23
|
BulkDestroyModelMixin,
|
|
@@ -484,6 +485,7 @@ class ImageAttachmentViewSet(ModelViewSet):
|
|
|
484
485
|
queryset = ImageAttachment.objects.all()
|
|
485
486
|
serializer_class = serializers.ImageAttachmentSerializer
|
|
486
487
|
filterset_class = filters.ImageAttachmentFilterSet
|
|
488
|
+
parser_classes = [JSONParser, NautobotCSVParser, MultiPartParser]
|
|
487
489
|
|
|
488
490
|
|
|
489
491
|
#
|
nautobot/extras/forms/forms.py
CHANGED
|
@@ -1682,6 +1682,8 @@ class RoleBulkEditForm(NautobotBulkEditForm):
|
|
|
1682
1682
|
|
|
1683
1683
|
pk = forms.ModelMultipleChoiceField(queryset=Role.objects.all(), widget=forms.MultipleHiddenInput)
|
|
1684
1684
|
color = forms.CharField(max_length=6, required=False, widget=ColorSelect())
|
|
1685
|
+
description = forms.CharField(max_length=CHARFIELD_MAX_LENGTH, required=False)
|
|
1686
|
+
weight = forms.IntegerField(required=False)
|
|
1685
1687
|
content_types = MultipleContentTypeField(
|
|
1686
1688
|
queryset=RoleModelsQuery().as_queryset(), required=False, label="Content Type(s)"
|
|
1687
1689
|
)
|
nautobot/extras/group_sync.py
CHANGED
|
@@ -8,15 +8,15 @@ from django.contrib.auth.models import Group
|
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
CLAIMS_GROUP_NAME = getattr(settings, "
|
|
11
|
+
CLAIMS_GROUP_NAME = getattr(settings, "SSO_CLAIMS_GROUP", "groups")
|
|
12
12
|
""" Which claim to look at in the OAuth2/OIDC response
|
|
13
13
|
|
|
14
14
|
For Okta you can look at `Okta -> Authorization Servers -> Claims`. And a reasonable
|
|
15
15
|
default is "groups". For Azure a reasonable default is "roles".
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
SUPERUSER_GROUPS = getattr(settings, "
|
|
19
|
-
STAFF_GROUPS = getattr(settings, "
|
|
18
|
+
SUPERUSER_GROUPS = getattr(settings, "SSO_SUPERUSER_GROUPS", [])
|
|
19
|
+
STAFF_GROUPS = getattr(settings, "SSO_STAFF_GROUPS", [])
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def group_sync(uid, user=None, response=None, *args, **kwargs):
|