nautobot 2.3.5__py3-none-any.whl → 2.3.7__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/__init__.py +4 -2
- nautobot/circuits/tests/test_views.py +4 -5
- nautobot/core/api/utils.py +12 -2
- nautobot/core/api/views.py +15 -3
- nautobot/core/forms/fields.py +5 -2
- nautobot/core/forms/utils.py +31 -6
- nautobot/core/models/fields.py +56 -0
- nautobot/core/templates/inc/javascript.html +2 -0
- nautobot/core/templates/inc/nav_menu.html +0 -251
- nautobot/core/testing/mixins.py +59 -2
- nautobot/core/testing/views.py +45 -61
- nautobot/core/tests/runner.py +6 -3
- nautobot/core/tests/test_paginator.py +4 -3
- nautobot/core/tests/test_utils.py +83 -0
- nautobot/core/tests/test_views.py +39 -56
- nautobot/core/views/__init__.py +27 -11
- nautobot/dcim/tests/test_api.py +4 -1
- nautobot/dcim/tests/test_views.py +26 -67
- nautobot/extras/datasources/git.py +6 -1
- nautobot/extras/factory.py +2 -1
- nautobot/extras/migrations/0112_dynamic_group_group_type_data_migration.py +3 -0
- nautobot/extras/migrations/0116_fix_dynamic_group_group_type_data_migration.py +16 -0
- nautobot/extras/models/models.py +2 -0
- nautobot/extras/tests/test_api.py +3 -3
- nautobot/extras/tests/test_customfields.py +9 -16
- nautobot/extras/tests/test_dynamicgroups.py +116 -0
- nautobot/extras/tests/test_forms.py +2 -0
- nautobot/extras/tests/test_plugins.py +4 -6
- nautobot/extras/tests/test_utils.py +5 -0
- nautobot/extras/tests/test_views.py +63 -161
- nautobot/extras/utils.py +50 -11
- nautobot/ipam/api/serializers.py +30 -1
- nautobot/ipam/api/views.py +165 -3
- nautobot/ipam/filters.py +1 -1
- nautobot/ipam/forms.py +2 -0
- nautobot/ipam/migrations/0050_vlangroup_range.py +24 -0
- nautobot/ipam/models.py +51 -8
- nautobot/ipam/tables.py +4 -4
- nautobot/ipam/templates/ipam/vlangroup.html +4 -0
- nautobot/ipam/tests/test_api.py +192 -12
- nautobot/ipam/tests/test_models.py +35 -1
- nautobot/ipam/tests/test_utils.py +61 -0
- nautobot/ipam/tests/test_views.py +8 -15
- nautobot/ipam/utils/__init__.py +10 -17
- nautobot/ipam/views.py +1 -1
- 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.525ec568.min.js +16 -0
- nautobot/project-static/docs/assets/javascripts/{bundle.56dfad97.min.js.map → bundle.525ec568.min.js.map} +4 -4
- nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.8c3ca2c6.min.css.map +1 -0
- 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 +5 -5
- 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 +124 -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 +5 -5
- 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 +3 -3
- 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 +3 -3
- 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/index.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/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 +3 -3
- nautobot/project-static/docs/release-notes/version-2.3.html +392 -95
- nautobot/project-static/docs/requirements.txt +1 -1
- 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 +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/redis.html +3 -3
- nautobot/project-static/docs/user-guide/administration/configuration/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/celery-queues.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +3 -3
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.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/guides/selinux-troubleshooting.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 +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/index.html +3 -3
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +3 -3
- 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/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/cloud/cloud.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.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/module.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.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 +308 -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/objectmetadata.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/savedview.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.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/nav_menu.js +249 -0
- nautobot/tenancy/templates/tenancy/tenant.html +1 -1
- nautobot/users/tests/test_views.py +9 -11
- nautobot/virtualization/tests/test_views.py +3 -5
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/METADATA +2 -1
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/RECORD +334 -330
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/WHEEL +1 -1
- nautobot/project-static/docs/assets/javascripts/bundle.56dfad97.min.js +0 -16
- nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.35f28582.min.css.map +0 -1
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/NOTICE +0 -0
- {nautobot-2.3.5.dist-info → nautobot-2.3.7.dist-info}/entry_points.txt +0 -0
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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by Django 4.2.16 on 2024-10-02 17:14
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
import nautobot.core.models.fields
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
dependencies = [
|
|
10
|
+
("ipam", "0049_vrf_data_migration"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name="vlangroup",
|
|
16
|
+
name="range",
|
|
17
|
+
field=nautobot.core.models.fields.PositiveRangeNumberTextField(default="1-4094"),
|
|
18
|
+
),
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name="vlangroup",
|
|
21
|
+
name="tags",
|
|
22
|
+
field=nautobot.core.models.fields.TagsField(through="extras.TaggedItem", to="extras.Tag"),
|
|
23
|
+
),
|
|
24
|
+
]
|
nautobot/ipam/models.py
CHANGED
|
@@ -10,8 +10,9 @@ from django.utils.functional import cached_property
|
|
|
10
10
|
import netaddr
|
|
11
11
|
|
|
12
12
|
from nautobot.core.constants import CHARFIELD_MAX_LENGTH
|
|
13
|
+
from nautobot.core.forms.utils import parse_numeric_range
|
|
13
14
|
from nautobot.core.models import BaseManager, BaseModel
|
|
14
|
-
from nautobot.core.models.fields import JSONArrayField
|
|
15
|
+
from nautobot.core.models.fields import JSONArrayField, PositiveRangeNumberTextField
|
|
15
16
|
from nautobot.core.models.generics import OrganizationalModel, PrimaryModel
|
|
16
17
|
from nautobot.core.models.utils import array_to_string
|
|
17
18
|
from nautobot.core.utils.data import UtilizationData
|
|
@@ -1274,11 +1275,14 @@ class IPAddressToInterface(BaseModel):
|
|
|
1274
1275
|
|
|
1275
1276
|
|
|
1276
1277
|
@extras_features(
|
|
1278
|
+
"custom_links",
|
|
1277
1279
|
"custom_validators",
|
|
1280
|
+
"export_templates",
|
|
1278
1281
|
"graphql",
|
|
1279
1282
|
"locations",
|
|
1283
|
+
"webhooks",
|
|
1280
1284
|
)
|
|
1281
|
-
class VLANGroup(
|
|
1285
|
+
class VLANGroup(PrimaryModel):
|
|
1282
1286
|
"""
|
|
1283
1287
|
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
|
1284
1288
|
"""
|
|
@@ -1293,11 +1297,36 @@ class VLANGroup(OrganizationalModel):
|
|
|
1293
1297
|
)
|
|
1294
1298
|
description = models.CharField(max_length=CHARFIELD_MAX_LENGTH, blank=True)
|
|
1295
1299
|
|
|
1300
|
+
range = PositiveRangeNumberTextField(
|
|
1301
|
+
blank=False,
|
|
1302
|
+
default="1-4094",
|
|
1303
|
+
help_text="Permitted VID range(s) as comma-separated list, default '1-4094' if left blank.",
|
|
1304
|
+
min_boundary=constants.VLAN_VID_MIN,
|
|
1305
|
+
max_boundary=constants.VLAN_VID_MAX,
|
|
1306
|
+
)
|
|
1307
|
+
|
|
1296
1308
|
class Meta:
|
|
1297
1309
|
ordering = ("name",)
|
|
1298
1310
|
verbose_name = "VLAN group"
|
|
1299
1311
|
verbose_name_plural = "VLAN groups"
|
|
1300
1312
|
|
|
1313
|
+
@property
|
|
1314
|
+
def expanded_range(self):
|
|
1315
|
+
"""
|
|
1316
|
+
Expand VLAN's range into a list of integers (VLAN IDs).
|
|
1317
|
+
"""
|
|
1318
|
+
return parse_numeric_range(self.range)
|
|
1319
|
+
|
|
1320
|
+
@property
|
|
1321
|
+
def available_vids(self):
|
|
1322
|
+
"""
|
|
1323
|
+
Return all available VLAN IDs within this VLANGroup as a list.
|
|
1324
|
+
"""
|
|
1325
|
+
used_ids = self.vlans.all().values_list("vid", flat=True)
|
|
1326
|
+
available = sorted([vid for vid in self.expanded_range if vid not in used_ids])
|
|
1327
|
+
|
|
1328
|
+
return available
|
|
1329
|
+
|
|
1301
1330
|
def clean(self):
|
|
1302
1331
|
super().clean()
|
|
1303
1332
|
|
|
@@ -1308,18 +1337,25 @@ class VLANGroup(OrganizationalModel):
|
|
|
1308
1337
|
{"location": f'VLAN groups may not associate to locations of type "{self.location.location_type}".'}
|
|
1309
1338
|
)
|
|
1310
1339
|
|
|
1340
|
+
# Validate ranges for related VLANs.
|
|
1341
|
+
_expanded_range = self.expanded_range
|
|
1342
|
+
out_of_range_vids = [_vlan.vid for _vlan in self.vlans.all() if _vlan.vid not in _expanded_range]
|
|
1343
|
+
if out_of_range_vids:
|
|
1344
|
+
raise ValidationError(
|
|
1345
|
+
{
|
|
1346
|
+
"range": f"VLAN group range may not be re-sized due to existing VLANs (IDs: {','.join(map(str, out_of_range_vids))})."
|
|
1347
|
+
}
|
|
1348
|
+
)
|
|
1349
|
+
|
|
1311
1350
|
def __str__(self):
|
|
1312
1351
|
return self.name
|
|
1313
1352
|
|
|
1314
1353
|
def get_next_available_vid(self):
|
|
1315
1354
|
"""
|
|
1316
|
-
Return the first available VLAN ID
|
|
1355
|
+
Return the first available VLAN ID in the group's range.
|
|
1317
1356
|
"""
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
if i not in vlan_ids:
|
|
1321
|
-
return i
|
|
1322
|
-
return None
|
|
1357
|
+
_available_vids = self.available_vids
|
|
1358
|
+
return _available_vids[0] if _available_vids else None
|
|
1323
1359
|
|
|
1324
1360
|
|
|
1325
1361
|
@extras_features(
|
|
@@ -1438,6 +1474,13 @@ class VLAN(PrimaryModel):
|
|
|
1438
1474
|
# Return all VM interfaces assigned to this VLAN
|
|
1439
1475
|
return VMInterface.objects.filter(Q(untagged_vlan_id=self.pk) | Q(tagged_vlans=self.pk)).distinct()
|
|
1440
1476
|
|
|
1477
|
+
def clean(self):
|
|
1478
|
+
super().clean()
|
|
1479
|
+
|
|
1480
|
+
# Validate Vlan Group Range
|
|
1481
|
+
if self.vlan_group and self.vid not in self.vlan_group.expanded_range:
|
|
1482
|
+
raise ValidationError({"vid": f"VLAN ID is not contained in VLAN Group range ({self.vlan_group.range})"})
|
|
1483
|
+
|
|
1441
1484
|
|
|
1442
1485
|
@extras_features("graphql")
|
|
1443
1486
|
class VLANLocationAssignment(BaseModel):
|
nautobot/ipam/tables.py
CHANGED
|
@@ -161,9 +161,9 @@ VLAN_LINK = """
|
|
|
161
161
|
{% url 'ipam:vlan_add' %}\
|
|
162
162
|
?vid={{ record.vid }}&vlan_group={{ vlan_group.pk }}\
|
|
163
163
|
{% if vlan_group.location %}&location={{ vlan_group.location.pk }}{% endif %}\
|
|
164
|
-
" class="btn btn-xs btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available</a>\
|
|
164
|
+
" class="btn btn-xs btn-success">{{ record.available }} VLAN{{ record.available|pluralize }} available ({{ record.range }})</a>\
|
|
165
165
|
{% else %}
|
|
166
|
-
{{ record.available }} VLAN{{ record.available|pluralize }} available
|
|
166
|
+
{{ record.available }} VLAN{{ record.available|pluralize }} available ({{ record.range }})
|
|
167
167
|
{% endif %}
|
|
168
168
|
"""
|
|
169
169
|
|
|
@@ -665,8 +665,8 @@ class VLANGroupTable(BaseTable):
|
|
|
665
665
|
|
|
666
666
|
class Meta(BaseTable.Meta):
|
|
667
667
|
model = VLANGroup
|
|
668
|
-
fields = ("pk", "name", "location", "vlan_count", "description", "actions")
|
|
669
|
-
default_columns = ("pk", "name", "location", "vlan_count", "description", "actions")
|
|
668
|
+
fields = ("pk", "name", "location", "range", "vlan_count", "description", "actions")
|
|
669
|
+
default_columns = ("pk", "name", "range", "location", "vlan_count", "description", "actions")
|
|
670
670
|
|
|
671
671
|
|
|
672
672
|
#
|
nautobot/ipam/tests/test_api.py
CHANGED
|
@@ -200,14 +200,19 @@ class VRFDeviceAssignmentTest(APIViewTestCases.APIViewTestCase):
|
|
|
200
200
|
}
|
|
201
201
|
self.add_permissions("ipam.add_vrfdeviceassignment")
|
|
202
202
|
response = self.client.post(self._get_list_url(), duplicate_device_create_data, format="json", **self.header)
|
|
203
|
-
self.
|
|
204
|
-
|
|
203
|
+
self.assertContains(
|
|
204
|
+
response, "The fields device, vrf must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
|
|
205
|
+
)
|
|
205
206
|
response = self.client.post(self._get_list_url(), duplicate_vm_create_data, format="json", **self.header)
|
|
206
|
-
self.
|
|
207
|
-
|
|
207
|
+
self.assertContains(
|
|
208
|
+
response, "The fields virtual_machine, vrf must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
|
|
209
|
+
)
|
|
208
210
|
response = self.client.post(self._get_list_url(), invalid_create_data, format="json", **self.header)
|
|
209
|
-
self.
|
|
210
|
-
|
|
211
|
+
self.assertContains(
|
|
212
|
+
response,
|
|
213
|
+
"A VRF cannot be associated with both a device and a virtual machine.",
|
|
214
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
215
|
+
)
|
|
211
216
|
|
|
212
217
|
|
|
213
218
|
class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
|
|
@@ -266,14 +271,15 @@ class VRFPrefixAssignmentTest(APIViewTestCases.APIViewTestCase):
|
|
|
266
271
|
}
|
|
267
272
|
self.add_permissions("ipam.add_vrfprefixassignment")
|
|
268
273
|
response = self.client.post(self._get_list_url(), duplicate_create_data, format="json", **self.header)
|
|
269
|
-
self.
|
|
270
|
-
|
|
274
|
+
self.assertContains(
|
|
275
|
+
response, "The fields vrf, prefix must make a unique set.", status_code=status.HTTP_400_BAD_REQUEST
|
|
276
|
+
)
|
|
271
277
|
response = self.client.post(self._get_list_url(), wrong_namespace_create_data, format="json", **self.header)
|
|
272
|
-
self.
|
|
273
|
-
|
|
278
|
+
self.assertContains(
|
|
279
|
+
response, "Prefix must be in same namespace as VRF", status_code=status.HTTP_400_BAD_REQUEST
|
|
280
|
+
)
|
|
274
281
|
response = self.client.post(self._get_list_url(), missing_field_create_data, format="json", **self.header)
|
|
275
|
-
self.
|
|
276
|
-
self.assertIn("This field may not be null.", str(response.content))
|
|
282
|
+
self.assertContains(response, "This field may not be null.", status_code=status.HTTP_400_BAD_REQUEST)
|
|
277
283
|
|
|
278
284
|
|
|
279
285
|
class RouteTargetTest(APIViewTestCases.APIViewTestCase):
|
|
@@ -991,6 +997,15 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
991
997
|
"description": "New description",
|
|
992
998
|
}
|
|
993
999
|
|
|
1000
|
+
@classmethod
|
|
1001
|
+
def setUpTestData(cls):
|
|
1002
|
+
cls.vlan_group = VLANGroup.objects.create(name="Test", range="5-10,15-20")
|
|
1003
|
+
cls.default_status = Status.objects.first()
|
|
1004
|
+
VLAN.objects.create(name="vlan_5", vid=5, status=cls.default_status, vlan_group=cls.vlan_group)
|
|
1005
|
+
VLAN.objects.create(name="vlan_10", vid=10, status=cls.default_status, vlan_group=cls.vlan_group)
|
|
1006
|
+
VLAN.objects.create(name="vlan_17", vid=17, status=cls.default_status, vlan_group=cls.vlan_group)
|
|
1007
|
+
cls.unused_vids = [6, 7, 8, 9, 15, 16, 18, 19, 20]
|
|
1008
|
+
|
|
994
1009
|
def get_deletable_object(self):
|
|
995
1010
|
return VLANGroup.objects.create(name="DELETE ME")
|
|
996
1011
|
|
|
@@ -1002,6 +1017,171 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
1002
1017
|
]
|
|
1003
1018
|
return [vg.pk for vg in vlangroups]
|
|
1004
1019
|
|
|
1020
|
+
def test_list_available_vlans(self):
|
|
1021
|
+
"""
|
|
1022
|
+
Test retrieval of all available VLAN IDs within a VLANGroup.
|
|
1023
|
+
"""
|
|
1024
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1025
|
+
self.add_permissions("ipam.view_vlangroup")
|
|
1026
|
+
|
|
1027
|
+
# Retrieve all available VLAN IDs
|
|
1028
|
+
response = self.client.get(url, **self.header)
|
|
1029
|
+
|
|
1030
|
+
self.assertEqual(response.data["results"], self.unused_vids)
|
|
1031
|
+
self.assertEqual(response.data["count"], len(self.unused_vids))
|
|
1032
|
+
|
|
1033
|
+
def test_create_single_available_vlan(self):
|
|
1034
|
+
"""
|
|
1035
|
+
Test creation of the first available VLAN within a VLANGroup.
|
|
1036
|
+
"""
|
|
1037
|
+
cf = CustomField.objects.create(key="sor", label="Source of Record Field", type="text")
|
|
1038
|
+
cf.content_types.add(ContentType.objects.get_for_model(VLAN))
|
|
1039
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1040
|
+
self.add_permissions(
|
|
1041
|
+
"ipam.view_vlangroup",
|
|
1042
|
+
"ipam.add_vlan",
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
# Create all nine available VLANs with individual requests
|
|
1046
|
+
for unused_vid in self.unused_vids:
|
|
1047
|
+
data = {
|
|
1048
|
+
"name": f"VLAN_{unused_vid}",
|
|
1049
|
+
"description": f"Test VLAN {unused_vid}",
|
|
1050
|
+
"status": self.default_status.pk,
|
|
1051
|
+
"custom_fields": {"sor": "Nautobot"},
|
|
1052
|
+
}
|
|
1053
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1054
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1055
|
+
self.assertEqual(response.data["results"]["name"], data["name"])
|
|
1056
|
+
self.assertEqual(response.data["results"]["vid"], unused_vid)
|
|
1057
|
+
self.assertEqual(response.data["results"]["description"], data["description"])
|
|
1058
|
+
self.assertEqual(response.data["results"]["vlan_group"]["id"], self.vlan_group.pk)
|
|
1059
|
+
self.assertIn("custom_fields", response.data["results"])
|
|
1060
|
+
self.assertIn("sor", response.data["results"]["custom_fields"])
|
|
1061
|
+
self.assertEqual("Nautobot", response.data["results"]["custom_fields"]["sor"])
|
|
1062
|
+
|
|
1063
|
+
# Try to create one more VLAN
|
|
1064
|
+
response = self.client.post(
|
|
1065
|
+
url, {"name": "UTILIZED_VLAN_GROUP", "status": self.default_status.pk}, format="json", **self.header
|
|
1066
|
+
)
|
|
1067
|
+
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
1068
|
+
self.assertIn("detail", response.data)
|
|
1069
|
+
self.assertIn(
|
|
1070
|
+
f"An insufficient number of VLANs are available within the VLANGroup {self.vlan_group}",
|
|
1071
|
+
response.data["detail"],
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
def test_create_multiple_available_vlans(self):
|
|
1075
|
+
"""
|
|
1076
|
+
Test the creation of available VLANS within a VLANGroup.
|
|
1077
|
+
"""
|
|
1078
|
+
cf = CustomField.objects.create(key="sor", label="Source of Record Field", type="text")
|
|
1079
|
+
cf.content_types.add(ContentType.objects.get_for_model(VLAN))
|
|
1080
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1081
|
+
self.add_permissions(
|
|
1082
|
+
"ipam.view_vlangroup",
|
|
1083
|
+
"ipam.add_vlan",
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
# Try to create ten VLANs (only nine are available)
|
|
1087
|
+
data = [ # First nine VLANs
|
|
1088
|
+
{
|
|
1089
|
+
"name": f"VLAN_{unused_vid}",
|
|
1090
|
+
"description": f"Test VLAN {unused_vid}",
|
|
1091
|
+
"status": self.default_status.pk,
|
|
1092
|
+
"custom_fields": {"sor": "Nautobot"},
|
|
1093
|
+
}
|
|
1094
|
+
for unused_vid in self.unused_vids
|
|
1095
|
+
]
|
|
1096
|
+
additional_vlan = [
|
|
1097
|
+
{
|
|
1098
|
+
"name": "VLAN_10", # Out of range VLAN
|
|
1099
|
+
"description": "Test VLAN 10",
|
|
1100
|
+
"status": self.default_status.pk,
|
|
1101
|
+
"custom_fields": {"sor": "Nautobot"},
|
|
1102
|
+
}
|
|
1103
|
+
]
|
|
1104
|
+
response = self.client.post(url, data + additional_vlan, format="json", **self.header)
|
|
1105
|
+
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
1106
|
+
self.assertIn("detail", response.data)
|
|
1107
|
+
|
|
1108
|
+
# Create all nine available VLANs in a single request
|
|
1109
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1110
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1111
|
+
self.assertEqual(len(response.data["results"]), 9)
|
|
1112
|
+
|
|
1113
|
+
for i, vlan_data in enumerate(data):
|
|
1114
|
+
self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
|
|
1115
|
+
self.assertEqual(response.data["results"][i]["vid"], int(vlan_data["name"].replace("VLAN_", "")))
|
|
1116
|
+
self.assertEqual(response.data["results"][i]["description"], vlan_data["description"])
|
|
1117
|
+
self.assertEqual(response.data["results"][i]["vlan_group"]["id"], self.vlan_group.pk)
|
|
1118
|
+
self.assertIn("custom_fields", response.data["results"][i])
|
|
1119
|
+
self.assertIn("sor", response.data["results"][i]["custom_fields"])
|
|
1120
|
+
self.assertEqual("Nautobot", response.data["results"][i]["custom_fields"]["sor"])
|
|
1121
|
+
|
|
1122
|
+
def test_create_multiple_explicit_vlans(self):
|
|
1123
|
+
"""
|
|
1124
|
+
Test the creation of available VLANS within a VLANGroup requesting explicit VLAN IDs.
|
|
1125
|
+
"""
|
|
1126
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1127
|
+
self.add_permissions(
|
|
1128
|
+
"ipam.view_vlangroup",
|
|
1129
|
+
"ipam.add_vlan",
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
# Try to create VLANs with specified VLAN IDs. Also, explicitly (and redundantly) specify a VLAN Group.
|
|
1133
|
+
data = [
|
|
1134
|
+
{"name": "VLAN_6", "status": self.default_status.pk, "vid": 6},
|
|
1135
|
+
{"name": "VLAN_7", "status": self.default_status.pk, "vid": 7},
|
|
1136
|
+
{"name": "VLAN_8", "status": self.default_status.pk},
|
|
1137
|
+
{"name": "VLAN_9", "status": self.default_status.pk, "vid": 9, "vlan_group": self.vlan_group.pk},
|
|
1138
|
+
{"name": "VLAN_15", "status": self.default_status.pk},
|
|
1139
|
+
{"name": "VLAN_16", "status": self.default_status.pk, "vid": 16, "vlan_group": self.vlan_group.pk},
|
|
1140
|
+
]
|
|
1141
|
+
|
|
1142
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1143
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1144
|
+
self.assertEqual(len(response.data["results"]), 6)
|
|
1145
|
+
|
|
1146
|
+
for i, vlan_data in enumerate(data):
|
|
1147
|
+
self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
|
|
1148
|
+
self.assertEqual(response.data["results"][i]["vid"], int(vlan_data["name"].replace("VLAN_", "")))
|
|
1149
|
+
self.assertEqual(response.data["results"][i]["vlan_group"]["id"], self.vlan_group.pk)
|
|
1150
|
+
|
|
1151
|
+
def test_create_invalid_vlans(self):
|
|
1152
|
+
"""
|
|
1153
|
+
Test the creation of VLANs using invalid requests.
|
|
1154
|
+
"""
|
|
1155
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1156
|
+
self.add_permissions(
|
|
1157
|
+
"ipam.view_vlangroup",
|
|
1158
|
+
"ipam.add_vlan",
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
# Try to create VLANs using same vid
|
|
1162
|
+
data = [
|
|
1163
|
+
{"name": "VLAN_6", "status": self.default_status.pk, "vid": 6},
|
|
1164
|
+
{"name": "VLAN_7", "status": self.default_status.pk, "vid": 6},
|
|
1165
|
+
{"name": "VLAN_8", "status": self.default_status.pk},
|
|
1166
|
+
]
|
|
1167
|
+
|
|
1168
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1169
|
+
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
1170
|
+
self.assertIn("detail", response.data)
|
|
1171
|
+
self.assertEqual("VLAN 6 is not available within the VLANGroup.", response.data["detail"])
|
|
1172
|
+
|
|
1173
|
+
# Try to create VLANs specifying other VLAN Group
|
|
1174
|
+
some_other_vlan_group = VLANGroup.objects.create(name="VLAN Group 100-200", range="100-200")
|
|
1175
|
+
data = [{"name": "VLAN_7", "status": self.default_status.pk, "vlan_group": some_other_vlan_group.pk}]
|
|
1176
|
+
|
|
1177
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1178
|
+
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
1179
|
+
self.assertIn("detail", response.data)
|
|
1180
|
+
self.assertEqual(
|
|
1181
|
+
f"Invalid VLAN Group requested: {some_other_vlan_group}. Only VLAN Group {self.vlan_group} is permitted.",
|
|
1182
|
+
response.data["detail"],
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1005
1185
|
|
|
1006
1186
|
class VLANTest(APIViewTestCases.APIViewTestCase):
|
|
1007
1187
|
model = VLAN
|