nautobot 2.2.6__py3-none-any.whl → 2.2.8__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/mixins.py +6 -1
- nautobot/core/api/schema.py +3 -1
- nautobot/core/celery/__init__.py +1 -1
- nautobot/core/graphql/generators.py +2 -2
- nautobot/core/graphql/schema.py +7 -4
- nautobot/core/tests/integration/test_general_functionality.py +36 -0
- nautobot/core/tests/runner.py +10 -0
- nautobot/core/tests/test_graphql.py +51 -5
- nautobot/dcim/models/devices.py +10 -3
- nautobot/dcim/tests/test_models.py +75 -0
- nautobot/extras/models/groups.py +9 -2
- nautobot/extras/models/relationships.py +12 -0
- nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/test_dynamicgroups.py +8 -1
- nautobot/extras/tests/test_relationships.py +221 -1
- nautobot/extras/views.py +1 -1
- nautobot/ipam/api/serializers.py +46 -16
- nautobot/ipam/api/views.py +29 -16
- nautobot/ipam/tests/test_api.py +15 -20
- nautobot/project-static/docs/404.html +3 -3
- nautobot/project-static/docs/apps/index.html +3 -3
- nautobot/project-static/docs/apps/nautobot-apps.html +3 -3
- nautobot/project-static/docs/assets/javascripts/{bundle.ad660dcc.min.js → bundle.fe8b6f2b.min.js} +4 -4
- nautobot/project-static/docs/assets/javascripts/{bundle.ad660dcc.min.js.map → bundle.fe8b6f2b.min.js.map} +3 -3
- nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css → main.76a95c52.min.css} +1 -1
- nautobot/project-static/docs/assets/stylesheets/{main.6543a935.min.css.map → main.76a95c52.min.css.map} +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +3 -3
- nautobot/project-static/docs/development/apps/api/configuration-view.html +3 -3
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +3 -3
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +3 -3
- nautobot/project-static/docs/development/apps/api/models/global-search.html +3 -3
- nautobot/project-static/docs/development/apps/api/models/graphql.html +3 -3
- nautobot/project-static/docs/development/apps/api/models/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +3 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +3 -3
- nautobot/project-static/docs/development/apps/api/setup.html +3 -3
- nautobot/project-static/docs/development/apps/api/testing.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/base-template.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/notes.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/urls.html +3 -3
- nautobot/project-static/docs/development/apps/index.html +3 -3
- nautobot/project-static/docs/development/apps/migration/code-updates.html +3 -3
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +3 -3
- nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +3 -3
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +3 -3
- nautobot/project-static/docs/development/core/application-registry.html +3 -3
- nautobot/project-static/docs/development/core/best-practices.html +3 -3
- nautobot/project-static/docs/development/core/bootstrap-ui.html +3 -3
- nautobot/project-static/docs/development/core/caching.html +3 -3
- nautobot/project-static/docs/development/core/controllers.html +3 -3
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +3 -3
- nautobot/project-static/docs/development/core/generic-views.html +3 -3
- nautobot/project-static/docs/development/core/getting-started.html +5 -6
- nautobot/project-static/docs/development/core/homepage.html +3 -3
- nautobot/project-static/docs/development/core/index.html +3 -3
- nautobot/project-static/docs/development/core/model-checklist.html +3 -3
- nautobot/project-static/docs/development/core/model-features.html +3 -3
- nautobot/project-static/docs/development/core/natural-keys.html +3 -3
- nautobot/project-static/docs/development/core/navigation-menu.html +3 -3
- nautobot/project-static/docs/development/core/release-checklist.html +3 -3
- nautobot/project-static/docs/development/core/role-internals.html +3 -3
- nautobot/project-static/docs/development/core/settings.html +3 -3
- nautobot/project-static/docs/development/core/style-guide.html +3 -3
- nautobot/project-static/docs/development/core/templates.html +3 -3
- nautobot/project-static/docs/development/core/testing.html +16 -4
- nautobot/project-static/docs/development/core/user-preferences.html +3 -3
- nautobot/project-static/docs/development/index.html +3 -3
- nautobot/project-static/docs/development/jobs/index.html +3 -3
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +3 -3
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +3 -3
- nautobot/project-static/docs/overview/design_philosophy.html +3 -3
- nautobot/project-static/docs/overview/index.html +3 -3
- nautobot/project-static/docs/release-notes/index.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.0.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.1.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.2.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.3.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.4.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.5.html +3 -3
- nautobot/project-static/docs/release-notes/version-1.6.html +3 -3
- nautobot/project-static/docs/release-notes/version-2.0.html +3 -3
- nautobot/project-static/docs/release-notes/version-2.1.html +3 -3
- nautobot/project-static/docs/release-notes/version-2.2.html +370 -99
- nautobot/project-static/docs/requirements.txt +2 -1
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +256 -256
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/optional-settings.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/required-settings.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/caching.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/healthcheck.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +7 -7
- nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +8 -4
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/services.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation-extras/docker.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation-extras/health-checks.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation-extras/selinux-troubleshooting.html +3 -3
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +3 -3
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +3 -3
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +3 -3
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +3 -3
- nautobot/project-static/docs/user-guide/index.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +3 -3
- nautobot/project-static/js/connection_toggles.js +7 -6
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/METADATA +4 -4
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/RECORD +292 -291
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/NOTICE +0 -0
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/WHEEL +0 -0
- {nautobot-2.2.6.dist-info → nautobot-2.2.8.dist-info}/entry_points.txt +0 -0
|
@@ -15,9 +15,11 @@ from nautobot.core.tables import RelationshipColumn
|
|
|
15
15
|
from nautobot.core.testing import TestCase
|
|
16
16
|
from nautobot.core.testing.models import ModelTestCases
|
|
17
17
|
from nautobot.core.utils.lookup import get_route_for_model
|
|
18
|
+
from nautobot.dcim.forms import DeviceForm
|
|
18
19
|
from nautobot.dcim.models import (
|
|
19
20
|
Controller,
|
|
20
21
|
Device,
|
|
22
|
+
DeviceType,
|
|
21
23
|
DeviceTypeToSoftwareImageFile,
|
|
22
24
|
Location,
|
|
23
25
|
LocationType,
|
|
@@ -27,7 +29,7 @@ from nautobot.dcim.models import (
|
|
|
27
29
|
from nautobot.dcim.tables import LocationTable
|
|
28
30
|
from nautobot.dcim.tests.test_views import create_test_device
|
|
29
31
|
from nautobot.extras.choices import RelationshipRequiredSideChoices, RelationshipSideChoices, RelationshipTypeChoices
|
|
30
|
-
from nautobot.extras.models import Relationship, RelationshipAssociation, Status
|
|
32
|
+
from nautobot.extras.models import Relationship, RelationshipAssociation, Role, Status
|
|
31
33
|
from nautobot.ipam.models import VLAN, VLANGroup
|
|
32
34
|
|
|
33
35
|
|
|
@@ -486,6 +488,224 @@ class RelationshipTest(RelationshipBaseTest, ModelTestCases.BaseModelTestCase):
|
|
|
486
488
|
with self.assertNumQueries(0):
|
|
487
489
|
manager_method(Location)
|
|
488
490
|
|
|
491
|
+
def test_required_related_object_errors(self):
|
|
492
|
+
"""
|
|
493
|
+
Confirm that the fix in https://github.com/nautobot/nautobot/pull/5570 is working as expected
|
|
494
|
+
"""
|
|
495
|
+
device_ct = ContentType.objects.get_for_model(Device)
|
|
496
|
+
status = Status.objects.get_for_model(Device).first()
|
|
497
|
+
device_type = DeviceType.objects.exclude(manufacturer__isnull=True).first()
|
|
498
|
+
# Create a Device with role Role 1
|
|
499
|
+
role_1 = Role.objects.create(name="Role 1")
|
|
500
|
+
role_1.content_types.add(ContentType.objects.get_for_model(Device))
|
|
501
|
+
device_1 = Device.objects.create(
|
|
502
|
+
device_type=device_type, role=role_1, name="Device 1", location=self.locations[0], status=status
|
|
503
|
+
)
|
|
504
|
+
# Create a Device with role Role 2
|
|
505
|
+
role_2 = Role.objects.create(name="Role 2")
|
|
506
|
+
role_2.content_types.add(ContentType.objects.get_for_model(Device))
|
|
507
|
+
device_2 = Device.objects.create(
|
|
508
|
+
device_type=device_type, role=role_2, name="Device 2", location=self.locations[0], status=status
|
|
509
|
+
)
|
|
510
|
+
# Create a Device with role Role 3
|
|
511
|
+
role_3 = Role.objects.create(name="Role 3")
|
|
512
|
+
role_3.content_types.add(ContentType.objects.get_for_model(Device))
|
|
513
|
+
device_3 = Device.objects.create(
|
|
514
|
+
device_type=device_type, role=role_3, name="Device 3", location=self.locations[0], status=status
|
|
515
|
+
)
|
|
516
|
+
# Create a one-to-many relationship with destination required, source filter: {"role": ["Role 1"]}
|
|
517
|
+
# and destination filter {"role": ["Role 2"]}
|
|
518
|
+
relationship = Relationship.objects.create(
|
|
519
|
+
label="Device to Devices",
|
|
520
|
+
key="device_to_devices",
|
|
521
|
+
source_type=device_ct,
|
|
522
|
+
source_filter={"role": ["Role 1"]},
|
|
523
|
+
destination_type=device_ct,
|
|
524
|
+
destination_filter={"role": ["Role 2"]},
|
|
525
|
+
type=RelationshipTypeChoices.TYPE_ONE_TO_MANY,
|
|
526
|
+
required_on="destination",
|
|
527
|
+
)
|
|
528
|
+
# Attempt to update device_3 which will not be in the queryset filtered by the destination filter
|
|
529
|
+
# Assert that the form is valid and no ValueError is raised.
|
|
530
|
+
update_status = Status.objects.get_for_model(Device).last()
|
|
531
|
+
update_data_for_device_3 = {
|
|
532
|
+
"location": device_3.location.pk,
|
|
533
|
+
"device_type": device_3.device_type.pk,
|
|
534
|
+
"role": device_3.role.pk,
|
|
535
|
+
"name": device_3.name,
|
|
536
|
+
"status": update_status.pk,
|
|
537
|
+
}
|
|
538
|
+
form = DeviceForm(instance=device_3, data=update_data_for_device_3)
|
|
539
|
+
self.assertTrue(form.is_valid())
|
|
540
|
+
# Attempt to update device_1 which will not be in the destination filter,
|
|
541
|
+
# but is in the source filter.
|
|
542
|
+
update_data_for_device_1 = {
|
|
543
|
+
"location": device_1.location.pk,
|
|
544
|
+
"device_type": device_1.device_type.pk,
|
|
545
|
+
"role": device_1.role.pk,
|
|
546
|
+
"name": device_1.name,
|
|
547
|
+
"status": update_status.pk,
|
|
548
|
+
}
|
|
549
|
+
form2 = DeviceForm(instance=device_1, data=update_data_for_device_1)
|
|
550
|
+
self.assertTrue(form2.is_valid())
|
|
551
|
+
# Attempt to update device_2 which will be in the destination filter, so it should
|
|
552
|
+
# require the relationship.
|
|
553
|
+
update_data_for_device_2 = {
|
|
554
|
+
"location": device_2.location.pk,
|
|
555
|
+
"device_type": device_2.device_type.pk,
|
|
556
|
+
"role": device_2.role.pk,
|
|
557
|
+
"name": "Device 2",
|
|
558
|
+
"status": update_status.pk,
|
|
559
|
+
}
|
|
560
|
+
form3 = DeviceForm(instance=device_2, data=update_data_for_device_2)
|
|
561
|
+
self.assertFalse(form3.is_valid())
|
|
562
|
+
# Device 1 has a relationship to Device 2
|
|
563
|
+
update_data_for_device_1 = {
|
|
564
|
+
"location": device_1.location.pk,
|
|
565
|
+
"device_type": device_1.device_type.pk,
|
|
566
|
+
"role": device_1.role.pk,
|
|
567
|
+
"name": device_1.name,
|
|
568
|
+
"status": update_status.pk,
|
|
569
|
+
"cr_device_to_devices__destination": [device_2.pk],
|
|
570
|
+
}
|
|
571
|
+
form4 = DeviceForm(instance=device_1, data=update_data_for_device_1)
|
|
572
|
+
self.assertTrue(form4.is_valid())
|
|
573
|
+
form4.save()
|
|
574
|
+
# Device 2 has a relationship to Device 1, form should validate and save.
|
|
575
|
+
update_data_for_device_2 = {
|
|
576
|
+
"location": device_2.location.pk,
|
|
577
|
+
"device_type": device_2.device_type.pk,
|
|
578
|
+
"role": device_2.role.pk,
|
|
579
|
+
"name": "Device 2",
|
|
580
|
+
"status": update_status.pk,
|
|
581
|
+
"cr_device_to_devices__source": device_1.pk,
|
|
582
|
+
}
|
|
583
|
+
form5 = DeviceForm(instance=device_2, data=update_data_for_device_2)
|
|
584
|
+
self.assertTrue(form5.is_valid())
|
|
585
|
+
form5.save()
|
|
586
|
+
# Device 2 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
|
|
587
|
+
update_data_for_device_2 = {
|
|
588
|
+
"location": device_2.location.pk,
|
|
589
|
+
"device_type": device_2.device_type.pk,
|
|
590
|
+
"role": device_2.role.pk,
|
|
591
|
+
"name": "Device 2",
|
|
592
|
+
"status": update_status.pk,
|
|
593
|
+
"cr_device_to_devices__source": device_3.pk,
|
|
594
|
+
}
|
|
595
|
+
form6 = DeviceForm(instance=device_2, data=update_data_for_device_2)
|
|
596
|
+
with self.assertRaises(ValidationError):
|
|
597
|
+
form6.save()
|
|
598
|
+
# Device 1 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
|
|
599
|
+
update_data_for_device_1 = {
|
|
600
|
+
"location": device_1.location.pk,
|
|
601
|
+
"device_type": device_1.device_type.pk,
|
|
602
|
+
"role": device_1.role.pk,
|
|
603
|
+
"name": "Device 1",
|
|
604
|
+
"status": update_status.pk,
|
|
605
|
+
"cr_device_to_devices__destination": [
|
|
606
|
+
device_3.pk,
|
|
607
|
+
],
|
|
608
|
+
}
|
|
609
|
+
form6 = DeviceForm(instance=device_1, data=update_data_for_device_1)
|
|
610
|
+
with self.assertRaises(ValidationError):
|
|
611
|
+
form6.save()
|
|
612
|
+
|
|
613
|
+
relationship.required_on = "source"
|
|
614
|
+
relationship.save()
|
|
615
|
+
# Attempt to update device_3 which will not be in the queryset filtered by the destination filter
|
|
616
|
+
# Assert that the form is valid and no ValueError is raised. This ensures that an object that
|
|
617
|
+
# does not take part in any relationships can still be updated, addressing issue #5569.
|
|
618
|
+
update_status = Status.objects.get_for_model(Device).last()
|
|
619
|
+
update_data_for_device_3 = {
|
|
620
|
+
"location": device_3.location.pk,
|
|
621
|
+
"device_type": device_3.device_type.pk,
|
|
622
|
+
"role": device_3.role.pk,
|
|
623
|
+
"name": device_3.name,
|
|
624
|
+
"status": update_status.pk,
|
|
625
|
+
}
|
|
626
|
+
form = DeviceForm(instance=device_3, data=update_data_for_device_3)
|
|
627
|
+
self.assertTrue(form.is_valid())
|
|
628
|
+
# Attempt to update device_1 which will not be in the destination filter,
|
|
629
|
+
# but is in the source filter. Should fail as device_to_devices is required.
|
|
630
|
+
device_1.delete()
|
|
631
|
+
device_1 = Device.objects.create(
|
|
632
|
+
device_type=device_type, role=role_1, name="Device 1", location=self.locations[0], status=status
|
|
633
|
+
)
|
|
634
|
+
update_data_for_device_1 = {
|
|
635
|
+
"location": device_1.location.pk,
|
|
636
|
+
"device_type": device_1.device_type.pk,
|
|
637
|
+
"role": device_1.role.pk,
|
|
638
|
+
"name": device_1.name,
|
|
639
|
+
"status": update_status.pk,
|
|
640
|
+
}
|
|
641
|
+
form2 = DeviceForm(instance=device_1, data=update_data_for_device_1)
|
|
642
|
+
self.assertFalse(form2.is_valid())
|
|
643
|
+
# Attempt to update device_2 which will be in the destination filter, which should not require the
|
|
644
|
+
# relationship anymore.
|
|
645
|
+
device_2.delete()
|
|
646
|
+
device_2 = Device.objects.create(
|
|
647
|
+
device_type=device_type, role=role_2, name="Device 2", location=self.locations[0], status=status
|
|
648
|
+
)
|
|
649
|
+
update_data_for_device_2 = {
|
|
650
|
+
"location": device_2.location.pk,
|
|
651
|
+
"device_type": device_2.device_type.pk,
|
|
652
|
+
"role": device_2.role.pk,
|
|
653
|
+
"name": "Device 2",
|
|
654
|
+
"status": update_status.pk,
|
|
655
|
+
}
|
|
656
|
+
form3 = DeviceForm(instance=device_2, data=update_data_for_device_2)
|
|
657
|
+
self.assertTrue(form3.is_valid())
|
|
658
|
+
# Device 1 has a relationship to Device 2
|
|
659
|
+
update_data_for_device_1 = {
|
|
660
|
+
"location": device_1.location.pk,
|
|
661
|
+
"device_type": device_1.device_type.pk,
|
|
662
|
+
"role": device_1.role.pk,
|
|
663
|
+
"name": device_1.name,
|
|
664
|
+
"status": update_status.pk,
|
|
665
|
+
"cr_device_to_devices__destination": [device_2.pk],
|
|
666
|
+
}
|
|
667
|
+
form4 = DeviceForm(instance=device_1, data=update_data_for_device_1)
|
|
668
|
+
self.assertTrue(form4.is_valid())
|
|
669
|
+
form4.save()
|
|
670
|
+
# Device 2 has a relationship to Device 1, form should validate and save.
|
|
671
|
+
update_data_for_device_2 = {
|
|
672
|
+
"location": device_2.location.pk,
|
|
673
|
+
"device_type": device_2.device_type.pk,
|
|
674
|
+
"role": device_2.role.pk,
|
|
675
|
+
"name": "Device 2",
|
|
676
|
+
"status": update_status.pk,
|
|
677
|
+
"cr_device_to_devices__source": device_1.pk,
|
|
678
|
+
}
|
|
679
|
+
form5 = DeviceForm(instance=device_2, data=update_data_for_device_2)
|
|
680
|
+
self.assertTrue(form5.is_valid())
|
|
681
|
+
form5.save()
|
|
682
|
+
# Device 2 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
|
|
683
|
+
update_data_for_device_2 = {
|
|
684
|
+
"location": device_2.location.pk,
|
|
685
|
+
"device_type": device_2.device_type.pk,
|
|
686
|
+
"role": device_2.role.pk,
|
|
687
|
+
"name": "Device 2",
|
|
688
|
+
"status": update_status.pk,
|
|
689
|
+
"cr_device_to_devices__source": device_3.pk,
|
|
690
|
+
}
|
|
691
|
+
form6 = DeviceForm(instance=device_2, data=update_data_for_device_2)
|
|
692
|
+
with self.assertRaises(ValidationError):
|
|
693
|
+
form6.save()
|
|
694
|
+
# Device 1 has a relationship to Device 3, save should fail as Device 3 doesn't match filter.
|
|
695
|
+
update_data_for_device_1 = {
|
|
696
|
+
"location": device_1.location.pk,
|
|
697
|
+
"device_type": device_1.device_type.pk,
|
|
698
|
+
"role": device_1.role.pk,
|
|
699
|
+
"name": "Device 1",
|
|
700
|
+
"status": update_status.pk,
|
|
701
|
+
"cr_device_to_devices__destination": [
|
|
702
|
+
device_3.pk,
|
|
703
|
+
],
|
|
704
|
+
}
|
|
705
|
+
form6 = DeviceForm(instance=device_1, data=update_data_for_device_1)
|
|
706
|
+
with self.assertRaises(ValidationError):
|
|
707
|
+
form6.save()
|
|
708
|
+
|
|
489
709
|
|
|
490
710
|
class RelationshipAssociationTest(RelationshipBaseTest, ModelTestCases.BaseModelTestCase):
|
|
491
711
|
model = RelationshipAssociation
|
nautobot/extras/views.py
CHANGED
|
@@ -1069,7 +1069,7 @@ def check_and_call_git_repository_function(request, pk, func):
|
|
|
1069
1069
|
# Allow execution only if a worker process is running.
|
|
1070
1070
|
if not get_worker_count():
|
|
1071
1071
|
messages.error(request, "Unable to run job: Celery worker process not running.")
|
|
1072
|
-
return redirect(
|
|
1072
|
+
return redirect(reverse("extras:gitrepository", args=(pk,)), permanent=False)
|
|
1073
1073
|
else:
|
|
1074
1074
|
repository = get_object_or_404(GitRepository.objects.restrict(request.user, "change"), pk=pk)
|
|
1075
1075
|
job_result = func(repository, request.user)
|
nautobot/ipam/api/serializers.py
CHANGED
|
@@ -321,27 +321,30 @@ class PrefixLocationAssignmentSerializer(ValidatedModelSerializer):
|
|
|
321
321
|
fields = "__all__"
|
|
322
322
|
|
|
323
323
|
|
|
324
|
-
class PrefixLengthSerializer(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
324
|
+
class PrefixLengthSerializer(PrefixLegacySerializer):
|
|
325
|
+
"""
|
|
326
|
+
Input serializer for POST to /api/ipam/prefixes/<id>/available-prefixes/, i.e. allocating one or more sub-prefixes.
|
|
327
|
+
|
|
328
|
+
Since setting of multiple locations on create is not supported, this uses the legacy single-location option.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
prefix_length = serializers.IntegerField(required=True)
|
|
332
|
+
|
|
333
|
+
class Meta(PrefixLegacySerializer.Meta):
|
|
334
|
+
fields = PrefixLegacySerializer.Meta.fields.copy()
|
|
335
|
+
fields.remove("prefix")
|
|
336
|
+
fields.remove("network")
|
|
337
|
+
fields.remove("broadcast")
|
|
338
|
+
fields.remove("parent")
|
|
339
|
+
fields.remove("ip_version")
|
|
340
|
+
fields.remove("namespace")
|
|
340
341
|
|
|
341
342
|
|
|
342
343
|
class AvailablePrefixSerializer(serializers.Serializer):
|
|
343
344
|
"""
|
|
344
345
|
Representation of a prefix which does not exist in the database.
|
|
346
|
+
|
|
347
|
+
Response serializer for a GET to /api/ipam/prefixes/<id>/available-prefixes/.
|
|
345
348
|
"""
|
|
346
349
|
|
|
347
350
|
ip_version = serializers.IntegerField(read_only=True)
|
|
@@ -437,6 +440,8 @@ class IPAddressSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
|
|
|
437
440
|
class AvailableIPSerializer(serializers.Serializer):
|
|
438
441
|
"""
|
|
439
442
|
Representation of an IP address which does not exist in the database.
|
|
443
|
+
|
|
444
|
+
Response serializer for a GET to /api/ipam/prefixes/<id>/available-ips/.
|
|
440
445
|
"""
|
|
441
446
|
|
|
442
447
|
ip_version = serializers.IntegerField(read_only=True)
|
|
@@ -451,6 +456,31 @@ class AvailableIPSerializer(serializers.Serializer):
|
|
|
451
456
|
)
|
|
452
457
|
|
|
453
458
|
|
|
459
|
+
class IPAllocationSerializer(NautobotModelSerializer, TaggedModelSerializerMixin):
|
|
460
|
+
"""
|
|
461
|
+
Input serializer for POST to /api/ipam/prefixes/<id>/available-ips/, i.e. allocating addresses from a prefix.
|
|
462
|
+
"""
|
|
463
|
+
|
|
464
|
+
class Meta:
|
|
465
|
+
model = IPAddress
|
|
466
|
+
fields = (
|
|
467
|
+
# not address/namespace/parent as those are implied by the selected prefix
|
|
468
|
+
"status",
|
|
469
|
+
"type",
|
|
470
|
+
"dns_name",
|
|
471
|
+
"description",
|
|
472
|
+
"role",
|
|
473
|
+
"tenant",
|
|
474
|
+
"nat_inside",
|
|
475
|
+
"tags",
|
|
476
|
+
"custom_fields",
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
def validate(self, data):
|
|
480
|
+
data["mask_length"] = self.context["prefix"].prefix_length
|
|
481
|
+
return super().validate(data)
|
|
482
|
+
|
|
483
|
+
|
|
454
484
|
#
|
|
455
485
|
# IP address to interface
|
|
456
486
|
#
|
nautobot/ipam/api/views.py
CHANGED
|
@@ -121,8 +121,6 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
121
121
|
filterset_class = filters.PrefixFilterSet
|
|
122
122
|
|
|
123
123
|
def get_serializer_class(self):
|
|
124
|
-
if self.action == "available_prefixes" and self.request.method == "POST":
|
|
125
|
-
return serializers.PrefixLengthSerializer
|
|
126
124
|
if (
|
|
127
125
|
not getattr(self, "swagger_fake_view", False)
|
|
128
126
|
and self.request.major_version == 2
|
|
@@ -159,7 +157,11 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
159
157
|
raise self.LocationIncompatibleLegacyBehavior from e
|
|
160
158
|
|
|
161
159
|
@extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
|
162
|
-
@extend_schema(
|
|
160
|
+
@extend_schema(
|
|
161
|
+
methods=["post"],
|
|
162
|
+
request=serializers.PrefixLengthSerializer,
|
|
163
|
+
responses={201: serializers.PrefixSerializer(many=True)},
|
|
164
|
+
)
|
|
163
165
|
@action(
|
|
164
166
|
detail=True,
|
|
165
167
|
name="Available Prefixes",
|
|
@@ -169,10 +171,10 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
169
171
|
)
|
|
170
172
|
def available_prefixes(self, request, pk=None):
|
|
171
173
|
"""
|
|
172
|
-
A convenience method for
|
|
174
|
+
A convenience method for listing and/or allocating available child prefixes within a parent.
|
|
173
175
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
This uses a Redis lock to prevent this API from being invoked in parallel, in order to avoid a race condition
|
|
177
|
+
if multiple clients tried to simultaneously request allocation from the same parent prefix.
|
|
176
178
|
"""
|
|
177
179
|
prefix = get_object_or_404(self.queryset, pk=pk)
|
|
178
180
|
if request.method == "POST":
|
|
@@ -214,9 +216,9 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
214
216
|
# Initialize the serializer with a list or a single object depending on what was requested
|
|
215
217
|
context = {"request": request, "depth": 0}
|
|
216
218
|
if isinstance(request.data, list):
|
|
217
|
-
serializer =
|
|
219
|
+
serializer = self.get_serializer_class()(data=requested_prefixes, many=True, context=context)
|
|
218
220
|
else:
|
|
219
|
-
serializer =
|
|
221
|
+
serializer = self.get_serializer_class()(data=requested_prefixes[0], context=context)
|
|
220
222
|
|
|
221
223
|
# Create the new Prefix(es)
|
|
222
224
|
serializer.is_valid(raise_exception=True)
|
|
@@ -238,8 +240,8 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
238
240
|
@extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)})
|
|
239
241
|
@extend_schema(
|
|
240
242
|
methods=["post"],
|
|
241
|
-
responses={201: serializers.
|
|
242
|
-
request=serializers.
|
|
243
|
+
responses={201: serializers.IPAddressSerializer(many=True)},
|
|
244
|
+
request=serializers.IPAllocationSerializer(many=True),
|
|
243
245
|
)
|
|
244
246
|
@action(
|
|
245
247
|
detail=True,
|
|
@@ -251,12 +253,13 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
251
253
|
)
|
|
252
254
|
def available_ips(self, request, pk=None):
|
|
253
255
|
"""
|
|
254
|
-
A convenience method for
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
A convenience method for listing and/or allocating available IP addresses within a prefix.
|
|
257
|
+
|
|
258
|
+
By default, the number of IPs returned will be equivalent to PAGINATE_COUNT.
|
|
259
|
+
An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed, however results will not be paginated.
|
|
257
260
|
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
This uses a Redis lock to prevent this API from being invoked in parallel, in order to avoid a race condition
|
|
262
|
+
if multiple clients tried to simultaneously request allocation from the same parent prefix.
|
|
260
263
|
"""
|
|
261
264
|
prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
|
|
262
265
|
|
|
@@ -266,7 +269,17 @@ class PrefixViewSet(NautobotModelViewSet):
|
|
|
266
269
|
"nautobot.ipam.api.views.available_ips", blocking_timeout=5, timeout=settings.REDIS_LOCK_TIMEOUT
|
|
267
270
|
):
|
|
268
271
|
# Normalize to a list of objects
|
|
269
|
-
|
|
272
|
+
serializer = serializers.IPAllocationSerializer(
|
|
273
|
+
data=request.data if isinstance(request.data, list) else [request.data],
|
|
274
|
+
many=True,
|
|
275
|
+
context={
|
|
276
|
+
"request": request,
|
|
277
|
+
"prefix": prefix,
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
serializer.is_valid(raise_exception=True)
|
|
281
|
+
|
|
282
|
+
requested_ips = serializer.validated_data
|
|
270
283
|
|
|
271
284
|
# Determine if the requested number of IPs is available
|
|
272
285
|
available_ips = prefix.get_available_ips()
|
nautobot/ipam/tests/test_api.py
CHANGED
|
@@ -454,7 +454,6 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
454
454
|
for i in range(4):
|
|
455
455
|
data = {
|
|
456
456
|
"prefix_length": child_prefix_length,
|
|
457
|
-
"namespace": self.namespace.pk,
|
|
458
457
|
"status": self.status.pk,
|
|
459
458
|
"description": f"Test Prefix {i + 1}",
|
|
460
459
|
}
|
|
@@ -464,13 +463,18 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
464
463
|
self.assertEqual(str(response.data["namespace"]["url"]), self.absolute_api_url(prefix.namespace))
|
|
465
464
|
self.assertEqual(response.data["description"], data["description"])
|
|
466
465
|
|
|
467
|
-
# Try to create one more prefix
|
|
468
|
-
|
|
466
|
+
# Try to create one more prefix, and expect a HTTP 204 response.
|
|
467
|
+
# This feels wrong to me (shouldn't it be a 4xx or 5xx?) but it's how the API has historically behaved.
|
|
468
|
+
response = self.client.post(
|
|
469
|
+
url, {"prefix_length": child_prefix_length, "status": self.status.pk}, format="json", **self.header
|
|
470
|
+
)
|
|
469
471
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
470
472
|
self.assertIn("detail", response.data)
|
|
471
473
|
|
|
472
|
-
#
|
|
473
|
-
response = self.client.post(
|
|
474
|
+
# Invalid data does trigger a HTTP 400 response.
|
|
475
|
+
response = self.client.post(
|
|
476
|
+
url, {"prefix_length": "hello", "status": self.status.pk}, format="json", **self.header
|
|
477
|
+
)
|
|
474
478
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
475
479
|
self.assertIn("prefix_length", response.data[0])
|
|
476
480
|
|
|
@@ -495,35 +499,31 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
495
499
|
{
|
|
496
500
|
"prefix_length": child_prefix_length,
|
|
497
501
|
"description": "Test Prefix 1",
|
|
498
|
-
"namespace": self.namespace.pk,
|
|
499
502
|
"status": self.status.pk,
|
|
500
503
|
},
|
|
501
504
|
{
|
|
502
505
|
"prefix_length": child_prefix_length,
|
|
503
506
|
"description": "Test Prefix 2",
|
|
504
|
-
"namespace": self.namespace.pk,
|
|
505
507
|
"status": self.status.pk,
|
|
506
508
|
},
|
|
507
509
|
{
|
|
508
510
|
"prefix_length": child_prefix_length,
|
|
509
511
|
"description": "Test Prefix 3",
|
|
510
|
-
"namespace": self.namespace.pk,
|
|
511
512
|
"status": self.status.pk,
|
|
512
513
|
},
|
|
513
514
|
{
|
|
514
515
|
"prefix_length": child_prefix_length,
|
|
515
516
|
"description": "Test Prefix 4",
|
|
516
|
-
"namespace": self.namespace.pk,
|
|
517
517
|
"status": self.status.pk,
|
|
518
518
|
},
|
|
519
519
|
{
|
|
520
520
|
"prefix_length": child_prefix_length,
|
|
521
521
|
"description": "Test Prefix 5",
|
|
522
|
-
"namespace": self.namespace.pk,
|
|
523
522
|
"status": self.status.pk,
|
|
524
523
|
},
|
|
525
524
|
]
|
|
526
525
|
response = self.client.post(url, data, format="json", **self.header)
|
|
526
|
+
# This feels wrong to me (shouldn't it be a 4xx or 5xx?) but it's how the API has historically behaved.
|
|
527
527
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
528
528
|
self.assertIn("detail", response.data)
|
|
529
529
|
|
|
@@ -577,7 +577,6 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
577
577
|
for i in range(1, 7):
|
|
578
578
|
data = {
|
|
579
579
|
"description": f"Test IP {i}",
|
|
580
|
-
"namespace": self.namespace.pk,
|
|
581
580
|
"status": self.status.pk,
|
|
582
581
|
}
|
|
583
582
|
response = self.client.post(url, data, format="json", **self.header)
|
|
@@ -586,7 +585,8 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
586
585
|
self.assertEqual(response.data["description"], data["description"])
|
|
587
586
|
|
|
588
587
|
# Try to create one more IP
|
|
589
|
-
response = self.client.post(url, {}, format="json", **self.header)
|
|
588
|
+
response = self.client.post(url, {"status": self.status.pk}, format="json", **self.header)
|
|
589
|
+
# This feels wrong to me (shouldn't it be a 4xx or 5xx?) but it's how the API has historically behaved.
|
|
590
590
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
591
591
|
self.assertIn("detail", response.data)
|
|
592
592
|
|
|
@@ -604,19 +604,14 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
|
|
604
604
|
self.add_permissions("ipam.view_prefix", "ipam.add_ipaddress", "extras.view_status")
|
|
605
605
|
|
|
606
606
|
# Try to create seven IPs (only six are available)
|
|
607
|
-
data = [
|
|
608
|
-
{"description": f"Test IP {i}", "namespace": self.namespace.pk, "status": self.status.pk}
|
|
609
|
-
for i in range(1, 8)
|
|
610
|
-
] # 7 IPs
|
|
607
|
+
data = [{"description": f"Test IP {i}", "status": self.status.pk} for i in range(1, 8)] # 7 IPs
|
|
611
608
|
response = self.client.post(url, data, format="json", **self.header)
|
|
609
|
+
# This feels wrong to me (shouldn't it be a 4xx or 5xx?) but it's how the API has historically behaved.
|
|
612
610
|
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
613
611
|
self.assertIn("detail", response.data)
|
|
614
612
|
|
|
615
613
|
# Create all six available IPs in a single request
|
|
616
|
-
data = [
|
|
617
|
-
{"description": f"Test IP {i}", "namespace": self.namespace.pk, "status": self.status.pk}
|
|
618
|
-
for i in range(1, 7)
|
|
619
|
-
] # 6 IPs
|
|
614
|
+
data = [{"description": f"Test IP {i}", "status": self.status.pk} for i in range(1, 7)] # 6 IPs
|
|
620
615
|
response = self.client.post(url, data, format="json", **self.header)
|
|
621
616
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
622
617
|
self.assertEqual(len(response.data), 6)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
<link rel="icon" href="/projects/core/en/stable/assets/favicon.ico">
|
|
15
|
-
<meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.
|
|
15
|
+
<meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
<link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/main.
|
|
23
|
+
<link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/main.76a95c52.min.css">
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
<link rel="stylesheet" href="/projects/core/en/stable/assets/stylesheets/palette.06af60db.min.css">
|
|
@@ -7913,7 +7913,7 @@
|
|
|
7913
7913
|
<script id="__config" type="application/json">{"base": "/projects/core/en/stable/", "features": ["content.code.copy", "content.tabs.link", "navigation.footer", "navigation.tabs", "navigation.tabs.sticky", "navigation.tracking", "search.highlight", "search.share", "search.suggest"], "search": "/projects/core/en/stable/assets/javascripts/workers/search.b8dbb3d2.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}}</script>
|
|
7914
7914
|
|
|
7915
7915
|
|
|
7916
|
-
<script src="/projects/core/en/stable/assets/javascripts/bundle.
|
|
7916
|
+
<script src="/projects/core/en/stable/assets/javascripts/bundle.fe8b6f2b.min.js"></script>
|
|
7917
7917
|
|
|
7918
7918
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
7919
7919
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
<link rel="icon" href="../assets/favicon.ico">
|
|
21
|
-
<meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.
|
|
21
|
+
<meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
<link rel="stylesheet" href="../assets/stylesheets/main.
|
|
29
|
+
<link rel="stylesheet" href="../assets/stylesheets/main.76a95c52.min.css">
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
<link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">
|
|
@@ -8080,7 +8080,7 @@
|
|
|
8080
8080
|
<script id="__config" type="application/json">{"base": "..", "features": ["content.code.copy", "content.tabs.link", "navigation.footer", "navigation.tabs", "navigation.tabs.sticky", "navigation.tracking", "search.highlight", "search.share", "search.suggest"], "search": "../assets/javascripts/workers/search.b8dbb3d2.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}}</script>
|
|
8081
8081
|
|
|
8082
8082
|
|
|
8083
|
-
<script src="../assets/javascripts/bundle.
|
|
8083
|
+
<script src="../assets/javascripts/bundle.fe8b6f2b.min.js"></script>
|
|
8084
8084
|
|
|
8085
8085
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
8086
8086
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
<link rel="icon" href="../assets/favicon.ico">
|
|
19
|
-
<meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.
|
|
19
|
+
<meta name="generator" content="mkdocs-1.6.0, mkdocs-material-9.5.29">
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
<link rel="stylesheet" href="../assets/stylesheets/main.
|
|
27
|
+
<link rel="stylesheet" href="../assets/stylesheets/main.76a95c52.min.css">
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
<link rel="stylesheet" href="../assets/stylesheets/palette.06af60db.min.css">
|
|
@@ -8014,7 +8014,7 @@
|
|
|
8014
8014
|
<script id="__config" type="application/json">{"base": "..", "features": ["content.code.copy", "content.tabs.link", "navigation.footer", "navigation.tabs", "navigation.tabs.sticky", "navigation.tracking", "search.highlight", "search.share", "search.suggest"], "search": "../assets/javascripts/workers/search.b8dbb3d2.min.js", "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}}</script>
|
|
8015
8015
|
|
|
8016
8016
|
|
|
8017
|
-
<script src="../assets/javascripts/bundle.
|
|
8017
|
+
<script src="../assets/javascripts/bundle.fe8b6f2b.min.js"></script>
|
|
8018
8018
|
|
|
8019
8019
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
8020
8020
|
|