nautobot 2.3.4__py3-none-any.whl → 2.3.6__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.
- nautobot/core/api/utils.py +12 -2
- nautobot/core/forms/fields.py +5 -2
- nautobot/core/forms/utils.py +31 -6
- nautobot/core/models/fields.py +56 -0
- nautobot/core/tests/runner.py +13 -6
- nautobot/core/tests/test_utils.py +83 -0
- nautobot/core/tests/test_views.py +40 -1
- nautobot/core/views/generic.py +15 -15
- nautobot/core/views/mixins.py +12 -1
- nautobot/core/views/renderers.py +3 -1
- nautobot/core/views/utils.py +1 -1
- nautobot/dcim/api/serializers.py +1 -0
- nautobot/dcim/api/views.py +2 -0
- nautobot/dcim/forms.py +1 -1
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +1 -1
- nautobot/dcim/tests/test_api.py +61 -4
- nautobot/dcim/tests/test_models.py +0 -2
- nautobot/dcim/views.py +5 -2
- nautobot/extras/api/views.py +9 -0
- nautobot/extras/factory.py +2 -1
- nautobot/extras/models/jobs.py +9 -1
- nautobot/extras/models/models.py +2 -0
- nautobot/extras/querysets.py +10 -1
- nautobot/extras/tables.py +3 -0
- nautobot/extras/tests/test_api.py +39 -3
- nautobot/extras/tests/test_forms.py +2 -0
- nautobot/extras/tests/test_views.py +78 -3
- nautobot/extras/views.py +10 -7
- nautobot/ipam/api/serializers.py +30 -1
- nautobot/ipam/api/views.py +165 -3
- nautobot/ipam/filters.py +1 -1
- nautobot/ipam/forms.py +8 -1
- nautobot/ipam/migrations/0050_vlangroup_range.py +24 -0
- nautobot/ipam/models.py +56 -19
- nautobot/ipam/navigation.py +8 -1
- nautobot/ipam/tables.py +4 -4
- nautobot/ipam/templates/ipam/prefix.html +1 -1
- nautobot/ipam/templates/ipam/vlangroup.html +4 -0
- nautobot/ipam/tests/test_api.py +174 -0
- nautobot/ipam/tests/test_filters.py +1 -1
- nautobot/ipam/tests/test_models.py +35 -1
- nautobot/ipam/tests/test_utils.py +61 -0
- nautobot/ipam/tests/test_views.py +43 -41
- nautobot/ipam/utils/__init__.py +10 -17
- nautobot/ipam/views.py +2 -2
- nautobot/project-static/docs/404.html +2 -2
- nautobot/project-static/docs/apps/index.html +2 -2
- nautobot/project-static/docs/apps/nautobot-apps.html +2 -2
- nautobot/project-static/docs/assets/javascripts/workers/{search.07f07601.min.js → search.6ce7567c.min.js} +3 -3
- nautobot/project-static/docs/assets/javascripts/workers/{search.07f07601.min.js.map → search.6ce7567c.min.js.map} +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
- nautobot/project-static/docs/development/apps/api/configuration-view.html +2 -2
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +2 -2
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +2 -2
- nautobot/project-static/docs/development/apps/api/models/global-search.html +2 -2
- nautobot/project-static/docs/development/apps/api/models/graphql.html +2 -2
- nautobot/project-static/docs/development/apps/api/models/index.html +4 -4
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +2 -2
- nautobot/project-static/docs/development/apps/api/prometheus.html +2 -2
- nautobot/project-static/docs/development/apps/api/setup.html +2 -2
- nautobot/project-static/docs/development/apps/api/testing.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/base-template.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/index.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/notes.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/urls.html +2 -2
- nautobot/project-static/docs/development/apps/index.html +2 -2
- nautobot/project-static/docs/development/apps/migration/code-updates.html +2 -2
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
- nautobot/project-static/docs/development/apps/migration/from-v1.html +2 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +2 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +2 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +2 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +2 -2
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +2 -2
- nautobot/project-static/docs/development/core/application-registry.html +2 -2
- nautobot/project-static/docs/development/core/best-practices.html +2 -2
- nautobot/project-static/docs/development/core/bootstrap-ui.html +2 -2
- nautobot/project-static/docs/development/core/caching.html +2 -2
- nautobot/project-static/docs/development/core/controllers.html +2 -2
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +2 -2
- nautobot/project-static/docs/development/core/generic-views.html +2 -2
- nautobot/project-static/docs/development/core/getting-started.html +7 -6
- nautobot/project-static/docs/development/core/homepage.html +2 -2
- nautobot/project-static/docs/development/core/index.html +2 -2
- nautobot/project-static/docs/development/core/model-checklist.html +2 -2
- nautobot/project-static/docs/development/core/model-features.html +2 -2
- nautobot/project-static/docs/development/core/natural-keys.html +2 -2
- nautobot/project-static/docs/development/core/navigation-menu.html +2 -2
- nautobot/project-static/docs/development/core/release-checklist.html +2 -2
- nautobot/project-static/docs/development/core/role-internals.html +2 -2
- nautobot/project-static/docs/development/core/settings.html +2 -2
- nautobot/project-static/docs/development/core/style-guide.html +2 -2
- nautobot/project-static/docs/development/core/templates.html +2 -2
- nautobot/project-static/docs/development/core/testing.html +12 -4
- nautobot/project-static/docs/development/core/user-preferences.html +2 -2
- nautobot/project-static/docs/development/index.html +2 -2
- nautobot/project-static/docs/development/jobs/index.html +2 -2
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +2 -2
- nautobot/project-static/docs/index.html +2 -2
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +2 -2
- nautobot/project-static/docs/overview/design_philosophy.html +2 -2
- nautobot/project-static/docs/release-notes/index.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.2.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.3.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.4.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.5.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.6.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.0.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.1.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.2.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.3.html +402 -80
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +269 -269
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/index.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/services.html +2 -2
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +2 -2
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +2 -2
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +2 -2
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +307 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +2 -2
- nautobot/project-static/docs/user-guide/index.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +2 -2
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/METADATA +2 -2
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/RECORD +327 -325
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/NOTICE +0 -0
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/WHEEL +0 -0
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/entry_points.txt +0 -0
|
@@ -65,7 +65,6 @@ from nautobot.dcim.models import (
|
|
|
65
65
|
from nautobot.extras import context_managers
|
|
66
66
|
from nautobot.extras.choices import CustomFieldTypeChoices
|
|
67
67
|
from nautobot.extras.models import CustomField, Role, SecretsGroup, Status
|
|
68
|
-
from nautobot.ipam.factory import VLANGroupFactory
|
|
69
68
|
from nautobot.ipam.models import IPAddress, IPAddressToInterface, Namespace, Prefix, VLAN, VLANGroup
|
|
70
69
|
from nautobot.tenancy.models import Tenant
|
|
71
70
|
from nautobot.users.models import User
|
|
@@ -2351,7 +2350,6 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2351
2350
|
vid=100,
|
|
2352
2351
|
location=location_2,
|
|
2353
2352
|
status=vlan_status,
|
|
2354
|
-
vlan_group=VLANGroupFactory.create(location=location_2),
|
|
2355
2353
|
)
|
|
2356
2354
|
|
|
2357
2355
|
cls.namespace = Namespace.objects.create(name="dcim_test_interface_ip_addresses")
|
nautobot/dcim/views.py
CHANGED
|
@@ -3148,7 +3148,7 @@ class DeviceBayPopulateView(generic.ObjectEditView):
|
|
|
3148
3148
|
f"Added {device_bay.installed_device} to {device_bay}.",
|
|
3149
3149
|
)
|
|
3150
3150
|
|
|
3151
|
-
return redirect("dcim:
|
|
3151
|
+
return redirect("dcim:device_devicebays", pk=device_bay.device.pk)
|
|
3152
3152
|
|
|
3153
3153
|
return render(
|
|
3154
3154
|
request,
|
|
@@ -3191,7 +3191,7 @@ class DeviceBayDepopulateView(generic.ObjectEditView):
|
|
|
3191
3191
|
f"Removed {removed_device} from {device_bay}.",
|
|
3192
3192
|
)
|
|
3193
3193
|
|
|
3194
|
-
return redirect("dcim:
|
|
3194
|
+
return redirect("dcim:device_devicebays", pk=device_bay.device.pk)
|
|
3195
3195
|
|
|
3196
3196
|
return render(
|
|
3197
3197
|
request,
|
|
@@ -4139,9 +4139,12 @@ class DeviceFamilyUIViewSet(NautobotUIViewSet):
|
|
|
4139
4139
|
context["device_type_table"] = device_type_table
|
|
4140
4140
|
|
|
4141
4141
|
total_devices = 0
|
|
4142
|
+
device_type_count = 0
|
|
4142
4143
|
for device_type in device_types:
|
|
4143
4144
|
total_devices += device_type.device_count
|
|
4145
|
+
device_type_count += 1
|
|
4144
4146
|
context["total_devices"] = total_devices
|
|
4147
|
+
context["device_type_count"] = device_type_count
|
|
4145
4148
|
|
|
4146
4149
|
return context
|
|
4147
4150
|
|
nautobot/extras/api/views.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from django.conf import settings
|
|
2
2
|
from django.contrib.contenttypes.models import ContentType
|
|
3
|
+
from django.db.models import ProtectedError
|
|
3
4
|
from django.forms import ValidationError as FormsValidationError
|
|
4
5
|
from django.http import FileResponse, Http404
|
|
5
6
|
from django.shortcuts import get_object_or_404
|
|
@@ -729,6 +730,14 @@ class JobViewSet(
|
|
|
729
730
|
):
|
|
730
731
|
lookup_value_regex = r"[-0-9a-fA-F]+"
|
|
731
732
|
|
|
733
|
+
def perform_destroy(self, obj):
|
|
734
|
+
if obj.module_name.startswith("nautobot."):
|
|
735
|
+
raise ProtectedError(
|
|
736
|
+
f"Unable to delete Job {obj}. System Job cannot be deleted",
|
|
737
|
+
[],
|
|
738
|
+
)
|
|
739
|
+
super().perform_destroy(obj)
|
|
740
|
+
|
|
732
741
|
|
|
733
742
|
@extend_schema_view(
|
|
734
743
|
destroy=extend_schema(operation_id="extras_jobs_destroy_by_name"),
|
nautobot/extras/factory.py
CHANGED
nautobot/extras/models/jobs.py
CHANGED
|
@@ -11,7 +11,7 @@ from django.contrib.contenttypes.models import ContentType
|
|
|
11
11
|
from django.core.exceptions import ValidationError
|
|
12
12
|
from django.core.validators import MinValueValidator
|
|
13
13
|
from django.db import models, transaction
|
|
14
|
-
from django.db.models import signals
|
|
14
|
+
from django.db.models import ProtectedError, signals
|
|
15
15
|
from django.utils import timezone
|
|
16
16
|
from django.utils.functional import cached_property
|
|
17
17
|
from django_celery_beat.clockedschedule import clocked
|
|
@@ -234,6 +234,14 @@ class Job(PrimaryModel):
|
|
|
234
234
|
def __str__(self):
|
|
235
235
|
return self.name
|
|
236
236
|
|
|
237
|
+
def delete(self):
|
|
238
|
+
if self.module_name.startswith("nautobot."):
|
|
239
|
+
raise ProtectedError(
|
|
240
|
+
f"Unable to delete Job {self}. System Job cannot be deleted",
|
|
241
|
+
[],
|
|
242
|
+
)
|
|
243
|
+
super().delete()
|
|
244
|
+
|
|
237
245
|
@property
|
|
238
246
|
def job_class(self):
|
|
239
247
|
"""
|
nautobot/extras/models/models.py
CHANGED
|
@@ -575,6 +575,8 @@ class FileAttachment(BaseModel):
|
|
|
575
575
|
filename = models.CharField(max_length=CHARFIELD_MAX_LENGTH)
|
|
576
576
|
mimetype = models.CharField(max_length=CHARFIELD_MAX_LENGTH)
|
|
577
577
|
|
|
578
|
+
is_metadata_associable_model = False
|
|
579
|
+
|
|
578
580
|
natural_key_field_names = ["pk"]
|
|
579
581
|
|
|
580
582
|
def __str__(self):
|
nautobot/extras/querysets.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from django.conf import settings
|
|
2
2
|
from django.contrib.contenttypes.models import ContentType
|
|
3
|
-
from django.db.models import F, Model, OuterRef, Q, Subquery
|
|
3
|
+
from django.db.models import F, Model, OuterRef, ProtectedError, Q, Subquery
|
|
4
4
|
from django.db.models.functions import JSONObject
|
|
5
5
|
|
|
6
6
|
from nautobot.core.models.query_functions import EmptyGroupByJSONBAgg
|
|
@@ -224,6 +224,15 @@ class JobQuerySet(RestrictedQuerySet):
|
|
|
224
224
|
Extend the standard queryset with a get_for_class_path method.
|
|
225
225
|
"""
|
|
226
226
|
|
|
227
|
+
def delete(self):
|
|
228
|
+
for job in self:
|
|
229
|
+
if job.module_name.startswith("nautobot."):
|
|
230
|
+
raise ProtectedError(
|
|
231
|
+
f"Unable to delete Job {job}. System Job cannot be deleted",
|
|
232
|
+
[],
|
|
233
|
+
)
|
|
234
|
+
return super().delete()
|
|
235
|
+
|
|
227
236
|
def get_for_class_path(self, class_path):
|
|
228
237
|
try:
|
|
229
238
|
module_name, job_class_name = class_path.rsplit(".", 1)
|
nautobot/extras/tables.py
CHANGED
|
@@ -1031,6 +1031,9 @@ class ObjectMetadataTable(BaseTable):
|
|
|
1031
1031
|
class NoteTable(BaseTable):
|
|
1032
1032
|
actions = ButtonsColumn(Note)
|
|
1033
1033
|
created = tables.LinkColumn()
|
|
1034
|
+
note = tables.Column(
|
|
1035
|
+
attrs={"td": {"class": "rendered-markdown"}},
|
|
1036
|
+
)
|
|
1034
1037
|
|
|
1035
1038
|
class Meta(BaseTable.Meta):
|
|
1036
1039
|
model = Note
|
|
@@ -1388,6 +1388,42 @@ class JobTest(
|
|
|
1388
1388
|
job_model = Job.objects.get_for_class_path(class_path)
|
|
1389
1389
|
return reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
|
|
1390
1390
|
|
|
1391
|
+
def get_deletable_object(self):
|
|
1392
|
+
"""
|
|
1393
|
+
Get an instance that can be deleted.
|
|
1394
|
+
Exclude system jobs
|
|
1395
|
+
"""
|
|
1396
|
+
# filter out the system jobs:
|
|
1397
|
+
queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
|
|
1398
|
+
instance = get_deletable_objects(self.model, queryset).first()
|
|
1399
|
+
if instance is None:
|
|
1400
|
+
self.fail("Couldn't find a single deletable object!")
|
|
1401
|
+
return instance
|
|
1402
|
+
|
|
1403
|
+
def get_deletable_object_pks(self):
|
|
1404
|
+
"""
|
|
1405
|
+
Get a list of PKs corresponding to jobs that can be safely bulk-deleted.
|
|
1406
|
+
Exclude system jobs
|
|
1407
|
+
"""
|
|
1408
|
+
queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
|
|
1409
|
+
instances = get_deletable_objects(self.model, queryset).values_list("pk", flat=True)[:3]
|
|
1410
|
+
if len(instances) < 3:
|
|
1411
|
+
self.fail(f"Couldn't find 3 deletable objects, only found {len(instances)}!")
|
|
1412
|
+
return instances
|
|
1413
|
+
|
|
1414
|
+
def test_delete_system_jobs_fail(self):
|
|
1415
|
+
self.add_permissions("extras.delete_job")
|
|
1416
|
+
instance = self._get_queryset().filter(module_name__startswith="nautobot.").first()
|
|
1417
|
+
job_name = instance.name
|
|
1418
|
+
url = self._get_detail_url(instance)
|
|
1419
|
+
self.client.delete(url, **self.header)
|
|
1420
|
+
# assert Job still exists
|
|
1421
|
+
self.assertTrue(self._get_queryset().filter(name=job_name).exists())
|
|
1422
|
+
self.user.is_superuser = True
|
|
1423
|
+
self.client.delete(url, **self.header)
|
|
1424
|
+
# assert Job still exists
|
|
1425
|
+
self.assertTrue(self._get_queryset().filter(name=job_name).exists())
|
|
1426
|
+
|
|
1391
1427
|
def test_get_job_variables(self):
|
|
1392
1428
|
"""Test the job/<pk>/variables API endpoint."""
|
|
1393
1429
|
self.add_permissions("extras.view_job")
|
|
@@ -4044,14 +4080,14 @@ class TagTest(APIViewTestCases.APIViewTestCase):
|
|
|
4044
4080
|
def test_create_tags_with_invalid_content_types(self):
|
|
4045
4081
|
self.add_permissions("extras.add_tag")
|
|
4046
4082
|
|
|
4047
|
-
#
|
|
4048
|
-
data = {**self.create_data[0], "content_types": [
|
|
4083
|
+
# Manufacturer is an OrganizationalModel, not a PrimaryModel, and therefore does not support tags
|
|
4084
|
+
data = {**self.create_data[0], "content_types": [Manufacturer._meta.label_lower]}
|
|
4049
4085
|
response = self.client.post(self._get_list_url(), data, format="json", **self.header)
|
|
4050
4086
|
|
|
4051
4087
|
tag = Tag.objects.filter(name=data["name"])
|
|
4052
4088
|
self.assertHttpStatus(response, 400)
|
|
4053
4089
|
self.assertFalse(tag.exists())
|
|
4054
|
-
self.assertIn(f"Invalid content type: {
|
|
4090
|
+
self.assertIn(f"Invalid content type: {Manufacturer._meta.label_lower}", response.data["content_types"])
|
|
4055
4091
|
|
|
4056
4092
|
def test_create_tags_without_content_types(self):
|
|
4057
4093
|
self.add_permissions("extras.add_tag")
|
|
@@ -388,6 +388,7 @@ class RelationshipModelFormTestCase(TestCase):
|
|
|
388
388
|
cls.vlangroup_form_base_data = {
|
|
389
389
|
"location": cls.location.pk,
|
|
390
390
|
"name": "New VLAN Group",
|
|
391
|
+
"range": "1-4094",
|
|
391
392
|
}
|
|
392
393
|
|
|
393
394
|
def test_create_relationship_associations_valid_1(self):
|
|
@@ -665,6 +666,7 @@ class RelationshipModelFormTestCase(TestCase):
|
|
|
665
666
|
data={
|
|
666
667
|
"name": self.vlangroup_1.name,
|
|
667
668
|
"location": self.location,
|
|
669
|
+
"range": "1-4094",
|
|
668
670
|
f"cr_{self.relationship_2.key}__source": self.device_2.pk,
|
|
669
671
|
},
|
|
670
672
|
)
|
|
@@ -18,7 +18,7 @@ from nautobot.core.choices import ColorChoices
|
|
|
18
18
|
from nautobot.core.models.fields import slugify_dashes_to_underscores
|
|
19
19
|
from nautobot.core.templatetags.helpers import bettertitle
|
|
20
20
|
from nautobot.core.testing import extract_form_failures, extract_page_body, ModelViewTestCase, TestCase, ViewTestCases
|
|
21
|
-
from nautobot.core.testing.utils import disable_warnings, post_data
|
|
21
|
+
from nautobot.core.testing.utils import disable_warnings, get_deletable_objects, post_data
|
|
22
22
|
from nautobot.core.utils.permissions import get_permission_for_model
|
|
23
23
|
from nautobot.dcim.models import (
|
|
24
24
|
ConsolePort,
|
|
@@ -1840,6 +1840,9 @@ class ApprovalQueueTestCase(
|
|
|
1840
1840
|
return reverse("extras:scheduledjob_approval_request_view", kwargs={"pk": instance.pk})
|
|
1841
1841
|
raise ValueError("This override is only valid for list and view test cases")
|
|
1842
1842
|
|
|
1843
|
+
def get_list_url(self):
|
|
1844
|
+
return reverse("extras:scheduledjob_approval_queue_list")
|
|
1845
|
+
|
|
1843
1846
|
def setUp(self):
|
|
1844
1847
|
super().setUp()
|
|
1845
1848
|
self.job_model = Job.objects.get_for_class_path("dry_run.TestDryRun")
|
|
@@ -2363,6 +2366,78 @@ class JobTestCase(
|
|
|
2363
2366
|
"clear_task_queues_override": False,
|
|
2364
2367
|
}
|
|
2365
2368
|
|
|
2369
|
+
def get_deletable_object(self):
|
|
2370
|
+
"""
|
|
2371
|
+
Get an instance that can be deleted.
|
|
2372
|
+
Exclude system jobs
|
|
2373
|
+
"""
|
|
2374
|
+
# filter out the system jobs:
|
|
2375
|
+
queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
|
|
2376
|
+
return get_deletable_objects(self.model, queryset).first()
|
|
2377
|
+
|
|
2378
|
+
def get_deletable_object_pks(self):
|
|
2379
|
+
"""
|
|
2380
|
+
Get a list of PKs corresponding to jobs that can be safely bulk-deleted.
|
|
2381
|
+
Excluding system jobs
|
|
2382
|
+
"""
|
|
2383
|
+
queryset = self._get_queryset().exclude(module_name__startswith="nautobot.")
|
|
2384
|
+
return get_deletable_objects(self.model, queryset).values_list("pk", flat=True)[:3]
|
|
2385
|
+
|
|
2386
|
+
def test_delete_system_jobs_fail(self):
|
|
2387
|
+
instance = self._get_queryset().filter(module_name__startswith="nautobot.").first()
|
|
2388
|
+
job_name = instance.name
|
|
2389
|
+
request = {
|
|
2390
|
+
"path": self._get_url("delete", instance),
|
|
2391
|
+
"data": post_data({"confirm": True}),
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
# Try delete with delete job permission
|
|
2395
|
+
self.add_permissions("extras.delete_job")
|
|
2396
|
+
response = self.client.post(**request, follow=True)
|
|
2397
|
+
self.assertHttpStatus(response, 403)
|
|
2398
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2399
|
+
self.assertIn(f"Unable to delete Job {instance}. System Job cannot be deleted", response_body)
|
|
2400
|
+
# assert Job still exists
|
|
2401
|
+
self.assertTrue(self._get_queryset().filter(name=job_name).exists())
|
|
2402
|
+
|
|
2403
|
+
# Try delete as a superuser
|
|
2404
|
+
self.user.is_superuser = True
|
|
2405
|
+
response = self.client.post(**request, follow=True)
|
|
2406
|
+
self.assertHttpStatus(response, 403)
|
|
2407
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2408
|
+
self.assertIn(f"Unable to delete Job {instance}. System Job cannot be deleted", response_body)
|
|
2409
|
+
# assert Job still exists
|
|
2410
|
+
self.assertTrue(self._get_queryset().filter(name=job_name).exists())
|
|
2411
|
+
|
|
2412
|
+
def test_bulk_delete_system_jobs_fail(self):
|
|
2413
|
+
system_job_queryset = self.model.objects.filter(module_name__startswith="nautobot.")
|
|
2414
|
+
pk_list = system_job_queryset.values_list("pk", flat=True)[:3]
|
|
2415
|
+
initial_count = self._get_queryset().count()
|
|
2416
|
+
data = {
|
|
2417
|
+
"pk": pk_list,
|
|
2418
|
+
"confirm": True,
|
|
2419
|
+
"_confirm": True, # Form button
|
|
2420
|
+
}
|
|
2421
|
+
# Try bulk delete with delete job permission
|
|
2422
|
+
self.add_permissions("extras.delete_job")
|
|
2423
|
+
response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
|
|
2424
|
+
self.assertHttpStatus(response, 403)
|
|
2425
|
+
self.assertEqual(self._get_queryset().count(), initial_count)
|
|
2426
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2427
|
+
self.assertIn(
|
|
2428
|
+
f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
|
|
2429
|
+
)
|
|
2430
|
+
|
|
2431
|
+
# Try bulk delete as a superuser
|
|
2432
|
+
self.user.is_superuser = True
|
|
2433
|
+
response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
|
|
2434
|
+
self.assertHttpStatus(response, 403)
|
|
2435
|
+
self.assertEqual(self._get_queryset().count(), initial_count)
|
|
2436
|
+
response_body = extract_page_body(response.content.decode(response.charset))
|
|
2437
|
+
self.assertIn(
|
|
2438
|
+
f"Unable to delete Job {system_job_queryset.first()}. System Job cannot be deleted", response_body
|
|
2439
|
+
)
|
|
2440
|
+
|
|
2366
2441
|
def validate_job_data_after_bulk_edit(self, pk_list, old_data):
|
|
2367
2442
|
# Name is bulk-editable
|
|
2368
2443
|
overridable_fields = [field for field in JOB_OVERRIDABLE_FIELDS if field != "name"]
|
|
@@ -3520,11 +3595,11 @@ class TagTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|
|
3520
3595
|
|
|
3521
3596
|
def test_create_tags_with_invalid_content_types(self):
|
|
3522
3597
|
self.add_permissions("extras.add_tag")
|
|
3523
|
-
|
|
3598
|
+
manufacturer_content_type = ContentType.objects.get_for_model(Manufacturer)
|
|
3524
3599
|
|
|
3525
3600
|
form_data = {
|
|
3526
3601
|
**self.form_data,
|
|
3527
|
-
"content_types": [
|
|
3602
|
+
"content_types": [manufacturer_content_type.id],
|
|
3528
3603
|
}
|
|
3529
3604
|
|
|
3530
3605
|
request = {
|
nautobot/extras/views.py
CHANGED
|
@@ -4,7 +4,6 @@ from urllib.parse import parse_qs
|
|
|
4
4
|
from celery import chain
|
|
5
5
|
from django.conf import settings
|
|
6
6
|
from django.contrib import messages
|
|
7
|
-
from django.contrib.auth.models import AnonymousUser
|
|
8
7
|
from django.contrib.contenttypes.models import ContentType
|
|
9
8
|
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
10
9
|
from django.db import IntegrityError, transaction
|
|
@@ -23,6 +22,7 @@ from django.views.generic import View
|
|
|
23
22
|
from django_tables2 import RequestConfig
|
|
24
23
|
from jsonschema.validators import Draft7Validator
|
|
25
24
|
from rest_framework.decorators import action
|
|
25
|
+
from rest_framework.permissions import IsAuthenticated
|
|
26
26
|
|
|
27
27
|
try:
|
|
28
28
|
from zoneinfo import ZoneInfo
|
|
@@ -1650,6 +1650,9 @@ class SavedViewUIViewSet(
|
|
|
1650
1650
|
serializer_class = serializers.SavedViewSerializer
|
|
1651
1651
|
table_class = tables.SavedViewTable
|
|
1652
1652
|
action_buttons = ("export",)
|
|
1653
|
+
permission_classes = [
|
|
1654
|
+
IsAuthenticated,
|
|
1655
|
+
]
|
|
1653
1656
|
|
|
1654
1657
|
def alter_queryset(self, request):
|
|
1655
1658
|
"""
|
|
@@ -1676,15 +1679,15 @@ class SavedViewUIViewSet(
|
|
|
1676
1679
|
|
|
1677
1680
|
def check_permissions(self, request):
|
|
1678
1681
|
"""
|
|
1679
|
-
Override this method to not check any permissions.
|
|
1682
|
+
Override this method to not check any nautobot-specific object permissions and to only check if the user is authenticated.
|
|
1680
1683
|
Since users with <app_label>.view_<model_name> permissions should be able to view saved views related to this model.
|
|
1681
1684
|
And those permissions will be enforced in the related view.
|
|
1682
1685
|
"""
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1686
|
+
for permission in self.get_permissions():
|
|
1687
|
+
if not permission.has_permission(request, self):
|
|
1688
|
+
self.permission_denied(
|
|
1689
|
+
request, message=getattr(permission, "message", None), code=getattr(permission, "code", None)
|
|
1690
|
+
)
|
|
1688
1691
|
|
|
1689
1692
|
def extra_message_context(self, obj):
|
|
1690
1693
|
"""
|
nautobot/ipam/api/serializers.py
CHANGED
|
@@ -118,7 +118,7 @@ class RIRSerializer(NautobotModelSerializer):
|
|
|
118
118
|
#
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
class VLANGroupSerializer(NautobotModelSerializer):
|
|
121
|
+
class VLANGroupSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
|
|
122
122
|
vlan_count = serializers.IntegerField(read_only=True)
|
|
123
123
|
|
|
124
124
|
class Meta:
|
|
@@ -481,6 +481,35 @@ class IPAllocationSerializer(NautobotModelSerializer, TaggedModelSerializerMixin
|
|
|
481
481
|
return super().validate(data)
|
|
482
482
|
|
|
483
483
|
|
|
484
|
+
class VLANAllocationSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
|
|
485
|
+
"""
|
|
486
|
+
Input serializer for POST to /api/ipam/vlan-groups/<id>/available-vlans/, i.e. allocating VLAN from VLANGroup.
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
vid = serializers.IntegerField(required=False, min_value=constants.VLAN_VID_MIN, max_value=constants.VLAN_VID_MAX)
|
|
490
|
+
|
|
491
|
+
def validate(self, data):
|
|
492
|
+
"""Skip `ValidatedModel` validation.
|
|
493
|
+
This allows to skip `vid` attribute of `VLAN` model, while validate name and status.
|
|
494
|
+
"""
|
|
495
|
+
return data
|
|
496
|
+
|
|
497
|
+
class Meta(VLANSerializer.Meta):
|
|
498
|
+
model = VLAN
|
|
499
|
+
fields = (
|
|
500
|
+
# permit "vid" and "vlan_group" for `VLAN` consistency.
|
|
501
|
+
# validate them under `VLANGroupViewSet`
|
|
502
|
+
"vid",
|
|
503
|
+
"vlan_group",
|
|
504
|
+
"name",
|
|
505
|
+
"status",
|
|
506
|
+
"role",
|
|
507
|
+
"tenant",
|
|
508
|
+
"description",
|
|
509
|
+
"custom_fields",
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
484
513
|
#
|
|
485
514
|
# IP address to interface
|
|
486
515
|
#
|
nautobot/ipam/api/views.py
CHANGED
|
@@ -6,12 +6,15 @@ from rest_framework import status
|
|
|
6
6
|
from rest_framework.decorators import action
|
|
7
7
|
from rest_framework.exceptions import APIException
|
|
8
8
|
from rest_framework.response import Response
|
|
9
|
+
from rest_framework.serializers import IntegerField, ListSerializer
|
|
9
10
|
|
|
11
|
+
from nautobot.core.api.authentication import TokenPermissions
|
|
10
12
|
from nautobot.core.models.querysets import count_related
|
|
11
13
|
from nautobot.core.utils.config import get_settings_or_config
|
|
12
14
|
from nautobot.dcim.models import Location
|
|
13
15
|
from nautobot.extras.api.views import NautobotModelViewSet
|
|
14
16
|
from nautobot.ipam import filters
|
|
17
|
+
from nautobot.ipam.api import serializers
|
|
15
18
|
from nautobot.ipam.models import (
|
|
16
19
|
IPAddress,
|
|
17
20
|
IPAddressToInterface,
|
|
@@ -29,8 +32,6 @@ from nautobot.ipam.models import (
|
|
|
29
32
|
VRFPrefixAssignment,
|
|
30
33
|
)
|
|
31
34
|
|
|
32
|
-
from . import serializers
|
|
33
|
-
|
|
34
35
|
#
|
|
35
36
|
# Namespace
|
|
36
37
|
#
|
|
@@ -387,10 +388,171 @@ class IPAddressToInterfaceViewSet(NautobotModelViewSet):
|
|
|
387
388
|
|
|
388
389
|
|
|
389
390
|
class VLANGroupViewSet(NautobotModelViewSet):
|
|
390
|
-
queryset =
|
|
391
|
+
queryset = (
|
|
392
|
+
VLANGroup.objects.select_related("location")
|
|
393
|
+
.prefetch_related("tags")
|
|
394
|
+
.annotate(vlan_count=count_related(VLAN, "vlan_group"))
|
|
395
|
+
)
|
|
391
396
|
serializer_class = serializers.VLANGroupSerializer
|
|
392
397
|
filterset_class = filters.VLANGroupFilterSet
|
|
393
398
|
|
|
399
|
+
def restrict_queryset(self, request, *args, **kwargs):
|
|
400
|
+
"""
|
|
401
|
+
Apply "view" permissions on the POST /available-vlans/ endpoint, otherwise as ModelViewSetMixin.
|
|
402
|
+
"""
|
|
403
|
+
if request.user.is_authenticated and self.action == "available_vlans":
|
|
404
|
+
self.queryset = self.queryset.restrict(request.user, "view")
|
|
405
|
+
else:
|
|
406
|
+
super().restrict_queryset(request, *args, **kwargs)
|
|
407
|
+
|
|
408
|
+
class AvailableVLANPermissions(TokenPermissions):
|
|
409
|
+
"""As nautobot.core.api.authentication.TokenPermissions, but enforcing add_vlan permission."""
|
|
410
|
+
|
|
411
|
+
perms_map = {
|
|
412
|
+
"GET": ["ipam.view_vlangroup"],
|
|
413
|
+
"POST": ["ipam.view_vlangroup", "ipam.add_vlan"],
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
@extend_schema(methods=["get"], responses={200: ListSerializer(child=IntegerField())})
|
|
417
|
+
@extend_schema(
|
|
418
|
+
methods=["post"],
|
|
419
|
+
responses={201: serializers.VLANSerializer(many=True)},
|
|
420
|
+
request=serializers.VLANAllocationSerializer(many=True),
|
|
421
|
+
)
|
|
422
|
+
@action(
|
|
423
|
+
detail=True,
|
|
424
|
+
name="Available VLAN IDs",
|
|
425
|
+
url_path="available-vlans",
|
|
426
|
+
methods=["get", "post"],
|
|
427
|
+
permission_classes=[AvailableVLANPermissions],
|
|
428
|
+
filterset_class=None,
|
|
429
|
+
)
|
|
430
|
+
def available_vlans(self, request, pk=None):
|
|
431
|
+
"""
|
|
432
|
+
A convenience method for listing available VLAN IDs within a VLANGroup.
|
|
433
|
+
By default, the number of VIDs returned will be equivalent to PAGINATE_COUNT.
|
|
434
|
+
An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed, however results will not be paginated.
|
|
435
|
+
"""
|
|
436
|
+
vlan_group = get_object_or_404(self.queryset, pk=pk)
|
|
437
|
+
|
|
438
|
+
if request.method == "POST":
|
|
439
|
+
with cache.lock(
|
|
440
|
+
"nautobot.ipam.api.views.available_vlans", blocking_timeout=5, timeout=settings.REDIS_LOCK_TIMEOUT
|
|
441
|
+
):
|
|
442
|
+
# Normalize to a list of objects
|
|
443
|
+
serializer = serializers.VLANAllocationSerializer(
|
|
444
|
+
data=request.data if isinstance(request.data, list) else [request.data],
|
|
445
|
+
many=True,
|
|
446
|
+
context={
|
|
447
|
+
"request": request,
|
|
448
|
+
"vlan_group": vlan_group,
|
|
449
|
+
},
|
|
450
|
+
)
|
|
451
|
+
serializer.is_valid(raise_exception=True)
|
|
452
|
+
requested_vlans = serializer.validated_data
|
|
453
|
+
|
|
454
|
+
# Determine if the requested number of VLANs is available
|
|
455
|
+
available_vids = vlan_group.available_vids
|
|
456
|
+
if len(available_vids) < len(requested_vlans):
|
|
457
|
+
return Response(
|
|
458
|
+
{
|
|
459
|
+
"detail": (
|
|
460
|
+
f"An insufficient number of VLANs are available within the VLANGroup {vlan_group} "
|
|
461
|
+
f"({len(requested_vlans)} requested, {len(available_vids)} available)"
|
|
462
|
+
)
|
|
463
|
+
},
|
|
464
|
+
status=status.HTTP_204_NO_CONTENT,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Prioritise and check for explicitly requested VIDs. Remove them from available_vids
|
|
468
|
+
for requested_vlan in requested_vlans:
|
|
469
|
+
# Check requested `vid` for availability.
|
|
470
|
+
# This will also catch if same `vid` was requested multiple times in a request.
|
|
471
|
+
if "vid" in requested_vlan and requested_vlan["vid"] not in available_vids:
|
|
472
|
+
return Response(
|
|
473
|
+
{"detail": f"VLAN {requested_vlan['vid']} is not available within the VLANGroup."},
|
|
474
|
+
status=status.HTTP_204_NO_CONTENT,
|
|
475
|
+
)
|
|
476
|
+
elif "vid" in requested_vlan and requested_vlan["vid"] in available_vids:
|
|
477
|
+
available_vids.remove(requested_vlan["vid"])
|
|
478
|
+
|
|
479
|
+
# Assign VLAN IDs from the list of VLANGroup's available VLAN IDs.
|
|
480
|
+
# Available_vids now does not contain explicitly requested vids.
|
|
481
|
+
_available_vids = iter(available_vids)
|
|
482
|
+
|
|
483
|
+
for requested_vlan in requested_vlans:
|
|
484
|
+
if "vid" not in requested_vlan:
|
|
485
|
+
requested_vlan["vid"] = next(_available_vids)
|
|
486
|
+
|
|
487
|
+
# Check requested `vlan_group`
|
|
488
|
+
if "vlan_group" in requested_vlan and requested_vlan["vlan_group"] != vlan_group:
|
|
489
|
+
return Response(
|
|
490
|
+
{
|
|
491
|
+
"detail": f"Invalid VLAN Group requested: {requested_vlan['vlan_group']}. "
|
|
492
|
+
f"Only VLAN Group {vlan_group} is permitted."
|
|
493
|
+
},
|
|
494
|
+
status=status.HTTP_204_NO_CONTENT,
|
|
495
|
+
)
|
|
496
|
+
else:
|
|
497
|
+
requested_vlan["vlan_group"] = vlan_group.pk
|
|
498
|
+
|
|
499
|
+
# Rewrite custom field data
|
|
500
|
+
requested_vlan["custom_fields"] = requested_vlan.pop("_custom_field_data", {})
|
|
501
|
+
|
|
502
|
+
# Initialize the serializer with a list or a single object depending on what was requested
|
|
503
|
+
context = {"request": request, "depth": 0}
|
|
504
|
+
|
|
505
|
+
if isinstance(request.data, list):
|
|
506
|
+
serializer = serializers.VLANSerializer(data=requested_vlans, many=True, context=context)
|
|
507
|
+
else:
|
|
508
|
+
serializer = serializers.VLANSerializer(data=requested_vlans[0], context=context)
|
|
509
|
+
|
|
510
|
+
# Create the new VLANs
|
|
511
|
+
serializer.is_valid(raise_exception=True)
|
|
512
|
+
serializer.save()
|
|
513
|
+
|
|
514
|
+
data = serializer.data
|
|
515
|
+
|
|
516
|
+
return Response(
|
|
517
|
+
data={
|
|
518
|
+
"count": len(data),
|
|
519
|
+
"next": None,
|
|
520
|
+
"previous": None,
|
|
521
|
+
"results": data,
|
|
522
|
+
},
|
|
523
|
+
status=status.HTTP_201_CREATED,
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
else:
|
|
527
|
+
try:
|
|
528
|
+
limit = int(request.query_params.get("limit", get_settings_or_config("PAGINATE_COUNT")))
|
|
529
|
+
except ValueError:
|
|
530
|
+
limit = get_settings_or_config("PAGINATE_COUNT")
|
|
531
|
+
|
|
532
|
+
if get_settings_or_config("MAX_PAGE_SIZE"):
|
|
533
|
+
limit = min(limit, get_settings_or_config("MAX_PAGE_SIZE"))
|
|
534
|
+
|
|
535
|
+
if isinstance(limit, int) and limit >= 0:
|
|
536
|
+
vids = vlan_group.available_vids[0:limit]
|
|
537
|
+
else:
|
|
538
|
+
vids = vlan_group.available_vids
|
|
539
|
+
|
|
540
|
+
serializer = ListSerializer(
|
|
541
|
+
child=IntegerField(),
|
|
542
|
+
data=vids,
|
|
543
|
+
)
|
|
544
|
+
serializer.is_valid(raise_exception=True)
|
|
545
|
+
data = serializer.validated_data
|
|
546
|
+
|
|
547
|
+
return Response(
|
|
548
|
+
{
|
|
549
|
+
"count": len(data),
|
|
550
|
+
"next": None,
|
|
551
|
+
"previous": None,
|
|
552
|
+
"results": data,
|
|
553
|
+
}
|
|
554
|
+
)
|
|
555
|
+
|
|
394
556
|
|
|
395
557
|
#
|
|
396
558
|
# VLANs
|
nautobot/ipam/filters.py
CHANGED
|
@@ -539,7 +539,7 @@ class IPAddressToInterfaceFilterSet(NautobotFilterSet):
|
|
|
539
539
|
class VLANGroupFilterSet(NautobotFilterSet, LocatableModelFilterSetMixin, NameSearchFilterSet):
|
|
540
540
|
class Meta:
|
|
541
541
|
model = VLANGroup
|
|
542
|
-
fields = ["id", "name", "description"]
|
|
542
|
+
fields = ["id", "name", "description", "tags"]
|
|
543
543
|
|
|
544
544
|
|
|
545
545
|
class VLANFilterSet(
|
nautobot/ipam/forms.py
CHANGED
|
@@ -727,7 +727,9 @@ class VLANGroupForm(LocatableModelFormMixin, NautobotModelForm):
|
|
|
727
727
|
fields = [
|
|
728
728
|
"location",
|
|
729
729
|
"name",
|
|
730
|
+
"range",
|
|
730
731
|
"description",
|
|
732
|
+
"tags",
|
|
731
733
|
]
|
|
732
734
|
|
|
733
735
|
|
|
@@ -873,6 +875,12 @@ class ServiceForm(NautobotModelForm):
|
|
|
873
875
|
base_field=forms.IntegerField(min_value=SERVICE_PORT_MIN, max_value=SERVICE_PORT_MAX),
|
|
874
876
|
help_text="Comma-separated list of one or more port numbers. A range may be specified using a hyphen.",
|
|
875
877
|
)
|
|
878
|
+
ip_addresses = DynamicModelMultipleChoiceField(
|
|
879
|
+
queryset=IPAddress.objects.all(),
|
|
880
|
+
required=False,
|
|
881
|
+
label="IP addresses",
|
|
882
|
+
query_params={"device_id": "$device", "virtual_machine_id": "$virtual_machine"},
|
|
883
|
+
)
|
|
876
884
|
|
|
877
885
|
class Meta:
|
|
878
886
|
model = Service
|
|
@@ -892,7 +900,6 @@ class ServiceForm(NautobotModelForm):
|
|
|
892
900
|
}
|
|
893
901
|
widgets = {
|
|
894
902
|
"protocol": StaticSelect2(),
|
|
895
|
-
"ip_addresses": StaticSelect2Multiple(),
|
|
896
903
|
}
|
|
897
904
|
|
|
898
905
|
def __init__(self, *args, **kwargs):
|