nautobot 2.3.4__py3-none-any.whl → 2.3.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/core/api/utils.py +12 -2
- nautobot/core/forms/fields.py +5 -2
- nautobot/core/forms/utils.py +31 -6
- nautobot/core/models/fields.py +56 -0
- nautobot/core/tests/runner.py +13 -6
- nautobot/core/tests/test_utils.py +83 -0
- nautobot/core/tests/test_views.py +40 -1
- nautobot/core/views/generic.py +15 -15
- nautobot/core/views/mixins.py +12 -1
- nautobot/core/views/renderers.py +3 -1
- nautobot/core/views/utils.py +1 -1
- nautobot/dcim/api/serializers.py +1 -0
- nautobot/dcim/api/views.py +2 -0
- nautobot/dcim/forms.py +1 -1
- nautobot/dcim/templates/dcim/devicefamily_retrieve.html +1 -1
- nautobot/dcim/tests/test_api.py +61 -4
- nautobot/dcim/tests/test_models.py +0 -2
- nautobot/dcim/views.py +5 -2
- nautobot/extras/api/views.py +9 -0
- nautobot/extras/factory.py +2 -1
- nautobot/extras/models/jobs.py +9 -1
- nautobot/extras/models/models.py +2 -0
- nautobot/extras/querysets.py +10 -1
- nautobot/extras/tables.py +3 -0
- nautobot/extras/tests/test_api.py +39 -3
- nautobot/extras/tests/test_forms.py +2 -0
- nautobot/extras/tests/test_views.py +78 -3
- nautobot/extras/views.py +10 -7
- nautobot/ipam/api/serializers.py +30 -1
- nautobot/ipam/api/views.py +165 -3
- nautobot/ipam/filters.py +1 -1
- nautobot/ipam/forms.py +8 -1
- nautobot/ipam/migrations/0050_vlangroup_range.py +24 -0
- nautobot/ipam/models.py +56 -19
- nautobot/ipam/navigation.py +8 -1
- nautobot/ipam/tables.py +4 -4
- nautobot/ipam/templates/ipam/prefix.html +1 -1
- nautobot/ipam/templates/ipam/vlangroup.html +4 -0
- nautobot/ipam/tests/test_api.py +174 -0
- nautobot/ipam/tests/test_filters.py +1 -1
- nautobot/ipam/tests/test_models.py +35 -1
- nautobot/ipam/tests/test_utils.py +61 -0
- nautobot/ipam/tests/test_views.py +43 -41
- nautobot/ipam/utils/__init__.py +10 -17
- nautobot/ipam/views.py +2 -2
- nautobot/project-static/docs/404.html +2 -2
- nautobot/project-static/docs/apps/index.html +2 -2
- nautobot/project-static/docs/apps/nautobot-apps.html +2 -2
- nautobot/project-static/docs/assets/javascripts/workers/{search.07f07601.min.js → search.6ce7567c.min.js} +3 -3
- nautobot/project-static/docs/assets/javascripts/workers/{search.07f07601.min.js.map → search.6ce7567c.min.js.map} +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +4 -4
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +2 -2
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2 -2
- nautobot/project-static/docs/development/apps/api/configuration-view.html +2 -2
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +2 -2
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +2 -2
- nautobot/project-static/docs/development/apps/api/models/global-search.html +2 -2
- nautobot/project-static/docs/development/apps/api/models/graphql.html +2 -2
- nautobot/project-static/docs/development/apps/api/models/index.html +4 -4
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +2 -2
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +2 -2
- nautobot/project-static/docs/development/apps/api/prometheus.html +2 -2
- nautobot/project-static/docs/development/apps/api/setup.html +2 -2
- nautobot/project-static/docs/development/apps/api/testing.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +2 -2
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/base-template.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/index.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/notes.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +2 -2
- nautobot/project-static/docs/development/apps/api/views/urls.html +2 -2
- nautobot/project-static/docs/development/apps/index.html +2 -2
- nautobot/project-static/docs/development/apps/migration/code-updates.html +2 -2
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +2 -2
- nautobot/project-static/docs/development/apps/migration/from-v1.html +2 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +2 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +2 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +2 -2
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +2 -2
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +2 -2
- nautobot/project-static/docs/development/core/application-registry.html +2 -2
- nautobot/project-static/docs/development/core/best-practices.html +2 -2
- nautobot/project-static/docs/development/core/bootstrap-ui.html +2 -2
- nautobot/project-static/docs/development/core/caching.html +2 -2
- nautobot/project-static/docs/development/core/controllers.html +2 -2
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +2 -2
- nautobot/project-static/docs/development/core/generic-views.html +2 -2
- nautobot/project-static/docs/development/core/getting-started.html +7 -6
- nautobot/project-static/docs/development/core/homepage.html +2 -2
- nautobot/project-static/docs/development/core/index.html +2 -2
- nautobot/project-static/docs/development/core/model-checklist.html +2 -2
- nautobot/project-static/docs/development/core/model-features.html +2 -2
- nautobot/project-static/docs/development/core/natural-keys.html +2 -2
- nautobot/project-static/docs/development/core/navigation-menu.html +2 -2
- nautobot/project-static/docs/development/core/release-checklist.html +2 -2
- nautobot/project-static/docs/development/core/role-internals.html +2 -2
- nautobot/project-static/docs/development/core/settings.html +2 -2
- nautobot/project-static/docs/development/core/style-guide.html +2 -2
- nautobot/project-static/docs/development/core/templates.html +2 -2
- nautobot/project-static/docs/development/core/testing.html +12 -4
- nautobot/project-static/docs/development/core/user-preferences.html +2 -2
- nautobot/project-static/docs/development/index.html +2 -2
- nautobot/project-static/docs/development/jobs/index.html +2 -2
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +2 -2
- nautobot/project-static/docs/index.html +2 -2
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +2 -2
- nautobot/project-static/docs/overview/design_philosophy.html +2 -2
- nautobot/project-static/docs/release-notes/index.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.0.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.1.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.2.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.3.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.4.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.5.html +2 -2
- nautobot/project-static/docs/release-notes/version-1.6.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.0.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.1.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.2.html +2 -2
- nautobot/project-static/docs/release-notes/version-2.3.html +402 -80
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +269 -269
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +2 -2
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +2 -2
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/index.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +2 -2
- nautobot/project-static/docs/user-guide/administration/installation/services.html +2 -2
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +2 -2
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +2 -2
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +2 -2
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +2 -2
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +307 -2
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +2 -2
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +2 -2
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +2 -2
- nautobot/project-static/docs/user-guide/index.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +2 -2
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +2 -2
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/METADATA +2 -2
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/RECORD +327 -325
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/NOTICE +0 -0
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/WHEEL +0 -0
- {nautobot-2.3.4.dist-info → nautobot-2.3.6.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
@@ -889,22 +890,16 @@ class Prefix(PrimaryModel):
|
|
|
889
890
|
)
|
|
890
891
|
return available_ips
|
|
891
892
|
|
|
892
|
-
def
|
|
893
|
+
def get_all_ips(self):
|
|
893
894
|
"""
|
|
894
|
-
Return IP addresses
|
|
895
|
-
|
|
896
|
-
In a future release, if this prefix is a pool, it will return IP addresses within the pool's address space.
|
|
895
|
+
Return all IP addresses contained within this prefix, including child prefixes' IP addresses.
|
|
897
896
|
|
|
898
897
|
Returns:
|
|
899
898
|
IPAddress QuerySet
|
|
900
899
|
"""
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
# parent__namespace=self.namespace, host__gte=self.network, host__lte=self.broadcast
|
|
905
|
-
# )
|
|
906
|
-
# else:
|
|
907
|
-
return self.ip_addresses.all()
|
|
900
|
+
return IPAddress.objects.filter(
|
|
901
|
+
parent__namespace=self.namespace, host__gte=self.network, host__lte=self.broadcast
|
|
902
|
+
)
|
|
908
903
|
|
|
909
904
|
def get_first_available_prefix(self):
|
|
910
905
|
"""
|
|
@@ -1280,11 +1275,14 @@ class IPAddressToInterface(BaseModel):
|
|
|
1280
1275
|
|
|
1281
1276
|
|
|
1282
1277
|
@extras_features(
|
|
1278
|
+
"custom_links",
|
|
1283
1279
|
"custom_validators",
|
|
1280
|
+
"export_templates",
|
|
1284
1281
|
"graphql",
|
|
1285
1282
|
"locations",
|
|
1283
|
+
"webhooks",
|
|
1286
1284
|
)
|
|
1287
|
-
class VLANGroup(
|
|
1285
|
+
class VLANGroup(PrimaryModel):
|
|
1288
1286
|
"""
|
|
1289
1287
|
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
|
|
1290
1288
|
"""
|
|
@@ -1299,11 +1297,36 @@ class VLANGroup(OrganizationalModel):
|
|
|
1299
1297
|
)
|
|
1300
1298
|
description = models.CharField(max_length=CHARFIELD_MAX_LENGTH, blank=True)
|
|
1301
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
|
+
|
|
1302
1308
|
class Meta:
|
|
1303
1309
|
ordering = ("name",)
|
|
1304
1310
|
verbose_name = "VLAN group"
|
|
1305
1311
|
verbose_name_plural = "VLAN groups"
|
|
1306
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
|
+
|
|
1307
1330
|
def clean(self):
|
|
1308
1331
|
super().clean()
|
|
1309
1332
|
|
|
@@ -1314,18 +1337,25 @@ class VLANGroup(OrganizationalModel):
|
|
|
1314
1337
|
{"location": f'VLAN groups may not associate to locations of type "{self.location.location_type}".'}
|
|
1315
1338
|
)
|
|
1316
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
|
+
|
|
1317
1350
|
def __str__(self):
|
|
1318
1351
|
return self.name
|
|
1319
1352
|
|
|
1320
1353
|
def get_next_available_vid(self):
|
|
1321
1354
|
"""
|
|
1322
|
-
Return the first available VLAN ID
|
|
1355
|
+
Return the first available VLAN ID in the group's range.
|
|
1323
1356
|
"""
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
if i not in vlan_ids:
|
|
1327
|
-
return i
|
|
1328
|
-
return None
|
|
1357
|
+
_available_vids = self.available_vids
|
|
1358
|
+
return _available_vids[0] if _available_vids else None
|
|
1329
1359
|
|
|
1330
1360
|
|
|
1331
1361
|
@extras_features(
|
|
@@ -1444,6 +1474,13 @@ class VLAN(PrimaryModel):
|
|
|
1444
1474
|
# Return all VM interfaces assigned to this VLAN
|
|
1445
1475
|
return VMInterface.objects.filter(Q(untagged_vlan_id=self.pk) | Q(tagged_vlans=self.pk)).distinct()
|
|
1446
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
|
+
|
|
1447
1484
|
|
|
1448
1485
|
@extras_features("graphql")
|
|
1449
1486
|
class VLANLocationAssignment(BaseModel):
|
nautobot/ipam/navigation.py
CHANGED
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
|
#
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</li>
|
|
34
34
|
{% if perms.ipam.view_ipaddress %}
|
|
35
35
|
<li role="presentation"{% if active_tab == 'ip-addresses' %} class="active"{% endif %}>
|
|
36
|
-
<a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">IP Addresses <span class="badge">{{ object.
|
|
36
|
+
<a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">IP Addresses <span class="badge">{{ object.get_all_ips.count }}</span></a>
|
|
37
37
|
</li>
|
|
38
38
|
{% endif %}
|
|
39
39
|
{% endblock extra_nav_tabs %}
|
nautobot/ipam/tests/test_api.py
CHANGED
|
@@ -991,6 +991,15 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
991
991
|
"description": "New description",
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
@classmethod
|
|
995
|
+
def setUpTestData(cls):
|
|
996
|
+
cls.vlan_group = VLANGroup.objects.create(name="Test", range="5-10,15-20")
|
|
997
|
+
cls.default_status = Status.objects.first()
|
|
998
|
+
VLAN.objects.create(name="vlan_5", vid=5, status=cls.default_status, vlan_group=cls.vlan_group)
|
|
999
|
+
VLAN.objects.create(name="vlan_10", vid=10, status=cls.default_status, vlan_group=cls.vlan_group)
|
|
1000
|
+
VLAN.objects.create(name="vlan_17", vid=17, status=cls.default_status, vlan_group=cls.vlan_group)
|
|
1001
|
+
cls.unused_vids = [6, 7, 8, 9, 15, 16, 18, 19, 20]
|
|
1002
|
+
|
|
994
1003
|
def get_deletable_object(self):
|
|
995
1004
|
return VLANGroup.objects.create(name="DELETE ME")
|
|
996
1005
|
|
|
@@ -1002,6 +1011,171 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
1002
1011
|
]
|
|
1003
1012
|
return [vg.pk for vg in vlangroups]
|
|
1004
1013
|
|
|
1014
|
+
def test_list_available_vlans(self):
|
|
1015
|
+
"""
|
|
1016
|
+
Test retrieval of all available VLAN IDs within a VLANGroup.
|
|
1017
|
+
"""
|
|
1018
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1019
|
+
self.add_permissions("ipam.view_vlangroup")
|
|
1020
|
+
|
|
1021
|
+
# Retrieve all available VLAN IDs
|
|
1022
|
+
response = self.client.get(url, **self.header)
|
|
1023
|
+
|
|
1024
|
+
self.assertEqual(response.data["results"], self.unused_vids)
|
|
1025
|
+
self.assertEqual(response.data["count"], len(self.unused_vids))
|
|
1026
|
+
|
|
1027
|
+
def test_create_single_available_vlan(self):
|
|
1028
|
+
"""
|
|
1029
|
+
Test creation of the first available VLAN within a VLANGroup.
|
|
1030
|
+
"""
|
|
1031
|
+
cf = CustomField.objects.create(key="sor", label="Source of Record Field", type="text")
|
|
1032
|
+
cf.content_types.add(ContentType.objects.get_for_model(VLAN))
|
|
1033
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1034
|
+
self.add_permissions(
|
|
1035
|
+
"ipam.view_vlangroup",
|
|
1036
|
+
"ipam.add_vlan",
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
# Create all nine available VLANs with individual requests
|
|
1040
|
+
for unused_vid in self.unused_vids:
|
|
1041
|
+
data = {
|
|
1042
|
+
"name": f"VLAN_{unused_vid}",
|
|
1043
|
+
"description": f"Test VLAN {unused_vid}",
|
|
1044
|
+
"status": self.default_status.pk,
|
|
1045
|
+
"custom_fields": {"sor": "Nautobot"},
|
|
1046
|
+
}
|
|
1047
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1048
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1049
|
+
self.assertEqual(response.data["results"]["name"], data["name"])
|
|
1050
|
+
self.assertEqual(response.data["results"]["vid"], unused_vid)
|
|
1051
|
+
self.assertEqual(response.data["results"]["description"], data["description"])
|
|
1052
|
+
self.assertEqual(response.data["results"]["vlan_group"]["id"], self.vlan_group.pk)
|
|
1053
|
+
self.assertIn("custom_fields", response.data["results"])
|
|
1054
|
+
self.assertIn("sor", response.data["results"]["custom_fields"])
|
|
1055
|
+
self.assertEqual("Nautobot", response.data["results"]["custom_fields"]["sor"])
|
|
1056
|
+
|
|
1057
|
+
# Try to create one more VLAN
|
|
1058
|
+
response = self.client.post(
|
|
1059
|
+
url, {"name": "UTILIZED_VLAN_GROUP", "status": self.default_status.pk}, format="json", **self.header
|
|
1060
|
+
)
|
|
1061
|
+
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
1062
|
+
self.assertIn("detail", response.data)
|
|
1063
|
+
self.assertIn(
|
|
1064
|
+
f"An insufficient number of VLANs are available within the VLANGroup {self.vlan_group}",
|
|
1065
|
+
response.data["detail"],
|
|
1066
|
+
)
|
|
1067
|
+
|
|
1068
|
+
def test_create_multiple_available_vlans(self):
|
|
1069
|
+
"""
|
|
1070
|
+
Test the creation of available VLANS within a VLANGroup.
|
|
1071
|
+
"""
|
|
1072
|
+
cf = CustomField.objects.create(key="sor", label="Source of Record Field", type="text")
|
|
1073
|
+
cf.content_types.add(ContentType.objects.get_for_model(VLAN))
|
|
1074
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1075
|
+
self.add_permissions(
|
|
1076
|
+
"ipam.view_vlangroup",
|
|
1077
|
+
"ipam.add_vlan",
|
|
1078
|
+
)
|
|
1079
|
+
|
|
1080
|
+
# Try to create ten VLANs (only nine are available)
|
|
1081
|
+
data = [ # First nine VLANs
|
|
1082
|
+
{
|
|
1083
|
+
"name": f"VLAN_{unused_vid}",
|
|
1084
|
+
"description": f"Test VLAN {unused_vid}",
|
|
1085
|
+
"status": self.default_status.pk,
|
|
1086
|
+
"custom_fields": {"sor": "Nautobot"},
|
|
1087
|
+
}
|
|
1088
|
+
for unused_vid in self.unused_vids
|
|
1089
|
+
]
|
|
1090
|
+
additional_vlan = [
|
|
1091
|
+
{
|
|
1092
|
+
"name": "VLAN_10", # Out of range VLAN
|
|
1093
|
+
"description": "Test VLAN 10",
|
|
1094
|
+
"status": self.default_status.pk,
|
|
1095
|
+
"custom_fields": {"sor": "Nautobot"},
|
|
1096
|
+
}
|
|
1097
|
+
]
|
|
1098
|
+
response = self.client.post(url, data + additional_vlan, format="json", **self.header)
|
|
1099
|
+
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
1100
|
+
self.assertIn("detail", response.data)
|
|
1101
|
+
|
|
1102
|
+
# Create all nine available VLANs in a single request
|
|
1103
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1104
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1105
|
+
self.assertEqual(len(response.data["results"]), 9)
|
|
1106
|
+
|
|
1107
|
+
for i, vlan_data in enumerate(data):
|
|
1108
|
+
self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
|
|
1109
|
+
self.assertEqual(response.data["results"][i]["vid"], int(vlan_data["name"].replace("VLAN_", "")))
|
|
1110
|
+
self.assertEqual(response.data["results"][i]["description"], vlan_data["description"])
|
|
1111
|
+
self.assertEqual(response.data["results"][i]["vlan_group"]["id"], self.vlan_group.pk)
|
|
1112
|
+
self.assertIn("custom_fields", response.data["results"][i])
|
|
1113
|
+
self.assertIn("sor", response.data["results"][i]["custom_fields"])
|
|
1114
|
+
self.assertEqual("Nautobot", response.data["results"][i]["custom_fields"]["sor"])
|
|
1115
|
+
|
|
1116
|
+
def test_create_multiple_explicit_vlans(self):
|
|
1117
|
+
"""
|
|
1118
|
+
Test the creation of available VLANS within a VLANGroup requesting explicit VLAN IDs.
|
|
1119
|
+
"""
|
|
1120
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1121
|
+
self.add_permissions(
|
|
1122
|
+
"ipam.view_vlangroup",
|
|
1123
|
+
"ipam.add_vlan",
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
# Try to create VLANs with specified VLAN IDs. Also, explicitly (and redundantly) specify a VLAN Group.
|
|
1127
|
+
data = [
|
|
1128
|
+
{"name": "VLAN_6", "status": self.default_status.pk, "vid": 6},
|
|
1129
|
+
{"name": "VLAN_7", "status": self.default_status.pk, "vid": 7},
|
|
1130
|
+
{"name": "VLAN_8", "status": self.default_status.pk},
|
|
1131
|
+
{"name": "VLAN_9", "status": self.default_status.pk, "vid": 9, "vlan_group": self.vlan_group.pk},
|
|
1132
|
+
{"name": "VLAN_15", "status": self.default_status.pk},
|
|
1133
|
+
{"name": "VLAN_16", "status": self.default_status.pk, "vid": 16, "vlan_group": self.vlan_group.pk},
|
|
1134
|
+
]
|
|
1135
|
+
|
|
1136
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1137
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
1138
|
+
self.assertEqual(len(response.data["results"]), 6)
|
|
1139
|
+
|
|
1140
|
+
for i, vlan_data in enumerate(data):
|
|
1141
|
+
self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
|
|
1142
|
+
self.assertEqual(response.data["results"][i]["vid"], int(vlan_data["name"].replace("VLAN_", "")))
|
|
1143
|
+
self.assertEqual(response.data["results"][i]["vlan_group"]["id"], self.vlan_group.pk)
|
|
1144
|
+
|
|
1145
|
+
def test_create_invalid_vlans(self):
|
|
1146
|
+
"""
|
|
1147
|
+
Test the creation of VLANs using invalid requests.
|
|
1148
|
+
"""
|
|
1149
|
+
url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
|
|
1150
|
+
self.add_permissions(
|
|
1151
|
+
"ipam.view_vlangroup",
|
|
1152
|
+
"ipam.add_vlan",
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
# Try to create VLANs using same vid
|
|
1156
|
+
data = [
|
|
1157
|
+
{"name": "VLAN_6", "status": self.default_status.pk, "vid": 6},
|
|
1158
|
+
{"name": "VLAN_7", "status": self.default_status.pk, "vid": 6},
|
|
1159
|
+
{"name": "VLAN_8", "status": self.default_status.pk},
|
|
1160
|
+
]
|
|
1161
|
+
|
|
1162
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1163
|
+
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
1164
|
+
self.assertIn("detail", response.data)
|
|
1165
|
+
self.assertEqual("VLAN 6 is not available within the VLANGroup.", response.data["detail"])
|
|
1166
|
+
|
|
1167
|
+
# Try to create VLANs specifying other VLAN Group
|
|
1168
|
+
some_other_vlan_group = VLANGroup.objects.create(name="VLAN Group 100-200", range="100-200")
|
|
1169
|
+
data = [{"name": "VLAN_7", "status": self.default_status.pk, "vlan_group": some_other_vlan_group.pk}]
|
|
1170
|
+
|
|
1171
|
+
response = self.client.post(url, data, format="json", **self.header)
|
|
1172
|
+
self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT)
|
|
1173
|
+
self.assertIn("detail", response.data)
|
|
1174
|
+
self.assertEqual(
|
|
1175
|
+
f"Invalid VLAN Group requested: {some_other_vlan_group}. Only VLAN Group {self.vlan_group} is permitted.",
|
|
1176
|
+
response.data["detail"],
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1005
1179
|
|
|
1006
1180
|
class VLANTest(APIViewTestCases.APIViewTestCase):
|
|
1007
1181
|
model = VLAN
|
|
@@ -1256,7 +1256,7 @@ class VLANLocationAssignmentTestCase(FilterTestCases.FilterTestCase):
|
|
|
1256
1256
|
params = {"q": vlan_vid}
|
|
1257
1257
|
queryset = VLANLocationAssignment.objects.exclude(location__name__icontains=vlan_vid)
|
|
1258
1258
|
filterset = VLANLocationAssignmentFilterSet(params, queryset).qs
|
|
1259
|
-
expected_queryset =
|
|
1259
|
+
expected_queryset = queryset.filter(vlan__vid__exact=vlan_vid)
|
|
1260
1260
|
self.assertQuerysetEqualAndNotEmpty(filterset, expected_queryset)
|
|
1261
1261
|
|
|
1262
1262
|
|
|
@@ -1302,7 +1302,7 @@ class TestVLANGroup(ModelTestCases.BaseModelTestCase):
|
|
|
1302
1302
|
self.assertIn(f'VLAN groups may not associate to locations of type "{location_type.name}"', str(cm.exception))
|
|
1303
1303
|
|
|
1304
1304
|
def test_get_next_available_vid(self):
|
|
1305
|
-
vlangroup = VLANGroup.objects.create(name="VLAN Group 1")
|
|
1305
|
+
vlangroup = VLANGroup.objects.create(name="VLAN Group 1", range="1-6")
|
|
1306
1306
|
status = Status.objects.get_for_model(VLAN).first()
|
|
1307
1307
|
VLAN.objects.bulk_create(
|
|
1308
1308
|
(
|
|
@@ -1316,6 +1316,40 @@ class TestVLANGroup(ModelTestCases.BaseModelTestCase):
|
|
|
1316
1316
|
|
|
1317
1317
|
VLAN.objects.bulk_create((VLAN(name="VLAN 4", vid=4, vlan_group=vlangroup, status=status),))
|
|
1318
1318
|
self.assertEqual(vlangroup.get_next_available_vid(), 6)
|
|
1319
|
+
# Next out of range.
|
|
1320
|
+
VLAN.objects.bulk_create((VLAN(name="VLAN 6", vid=6, vlan_group=vlangroup, status=status),))
|
|
1321
|
+
self.assertEqual(vlangroup.get_next_available_vid(), None)
|
|
1322
|
+
|
|
1323
|
+
def test_range_resize(self):
|
|
1324
|
+
vlangroup = VLANGroup.objects.create(name="VLAN Group 1", range="1-3")
|
|
1325
|
+
status = Status.objects.get_for_model(VLAN).first()
|
|
1326
|
+
VLAN.objects.bulk_create(
|
|
1327
|
+
(
|
|
1328
|
+
VLAN(name="VLAN 1", vid=1, vlan_group=vlangroup, status=status),
|
|
1329
|
+
VLAN(name="VLAN 2", vid=2, vlan_group=vlangroup, status=status),
|
|
1330
|
+
VLAN(name="VLAN 3", vid=3, vlan_group=vlangroup, status=status),
|
|
1331
|
+
)
|
|
1332
|
+
)
|
|
1333
|
+
with self.assertRaises(ValidationError) as exc:
|
|
1334
|
+
vlangroup.range = "1-2"
|
|
1335
|
+
vlangroup.validated_save()
|
|
1336
|
+
self.assertEqual(
|
|
1337
|
+
str(exc.exception), "{'range': ['VLAN group range may not be re-sized due to existing VLANs (IDs: 3).']}"
|
|
1338
|
+
)
|
|
1339
|
+
|
|
1340
|
+
def test_assign_vlan_out_of_range(self):
|
|
1341
|
+
vlangroup = VLANGroup.objects.create(name="VLAN Group 1", range="1-2")
|
|
1342
|
+
status = Status.objects.get_for_model(VLAN).first()
|
|
1343
|
+
VLAN.objects.bulk_create(
|
|
1344
|
+
(
|
|
1345
|
+
VLAN(name="VLAN 1", vid=1, vlan_group=vlangroup, status=status),
|
|
1346
|
+
VLAN(name="VLAN 2", vid=2, vlan_group=vlangroup, status=status),
|
|
1347
|
+
)
|
|
1348
|
+
)
|
|
1349
|
+
with self.assertRaises(ValidationError) as exc:
|
|
1350
|
+
vlan = VLAN(name="VLAN 3", vid=3, vlan_group=vlangroup, status=status)
|
|
1351
|
+
vlan.validated_save()
|
|
1352
|
+
self.assertEqual(str(exc.exception), "{'vid': ['VLAN ID is not contained in VLAN Group range (1-2)']}")
|
|
1319
1353
|
|
|
1320
1354
|
|
|
1321
1355
|
class TestVLAN(ModelTestCases.BaseModelTestCase):
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from django.test import TestCase
|
|
2
|
+
|
|
3
|
+
from nautobot.core.forms.utils import parse_numeric_range
|
|
4
|
+
from nautobot.extras.models import Status
|
|
5
|
+
from nautobot.ipam.models import VLAN, VLANGroup
|
|
6
|
+
from nautobot.ipam.utils import add_available_vlans
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AddAvailableVlansTest(TestCase):
|
|
10
|
+
"""Tests for add_available_vlans()."""
|
|
11
|
+
|
|
12
|
+
def test_add_available_vlans(self):
|
|
13
|
+
vlan_group = VLANGroup.objects.create(name="VLAN Group 1", range="100-105,110-112,115")
|
|
14
|
+
status = Status.objects.get_for_model(VLAN).first()
|
|
15
|
+
vlan_100 = {"vid": 100, "available": 2, "range": "100-101"}
|
|
16
|
+
vlan_102 = VLAN.objects.create(name="VLAN 102", vid=102, vlan_group=vlan_group, status=status)
|
|
17
|
+
vlan_103 = VLAN.objects.create(name="VLAN 103", vid=103, vlan_group=vlan_group, status=status)
|
|
18
|
+
vlan_104 = {"vid": 104, "available": 2, "range": "104-105"}
|
|
19
|
+
vlan_110 = VLAN.objects.create(name="VLAN 110", vid=110, vlan_group=vlan_group, status=status)
|
|
20
|
+
vlan_111 = VLAN.objects.create(name="VLAN 111", vid=111, vlan_group=vlan_group, status=status)
|
|
21
|
+
vlan_112 = {"vid": 112, "available": 1, "range": "112"}
|
|
22
|
+
vlan_115 = VLAN.objects.create(name="VLAN 115", vid=115, vlan_group=vlan_group, status=status)
|
|
23
|
+
|
|
24
|
+
self.assertEqual(
|
|
25
|
+
list(add_available_vlans(vlan_group=vlan_group, vlans=vlan_group.vlans.all())),
|
|
26
|
+
[vlan_100, vlan_102, vlan_103, vlan_104, vlan_110, vlan_111, vlan_112, vlan_115],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ParseNumericRangeTest(TestCase):
|
|
31
|
+
"""Tests for add_available_vlans()."""
|
|
32
|
+
|
|
33
|
+
def test_parse(self):
|
|
34
|
+
self.assertEqual(parse_numeric_range(input_string="5"), [5])
|
|
35
|
+
self.assertEqual(parse_numeric_range(input_string="5-5"), [5])
|
|
36
|
+
self.assertEqual(parse_numeric_range(input_string="5-5,5,5"), [5])
|
|
37
|
+
self.assertEqual(parse_numeric_range(input_string="1-5"), [1, 2, 3, 4, 5])
|
|
38
|
+
self.assertEqual(parse_numeric_range(input_string="1,2,3,4,5"), [1, 2, 3, 4, 5])
|
|
39
|
+
self.assertEqual(parse_numeric_range(input_string="5,4,3,1,2"), [1, 2, 3, 4, 5])
|
|
40
|
+
self.assertEqual(parse_numeric_range(input_string="1-5,10"), [1, 2, 3, 4, 5, 10])
|
|
41
|
+
self.assertEqual(parse_numeric_range(input_string="1,5,10-11"), [1, 5, 10, 11])
|
|
42
|
+
self.assertEqual(parse_numeric_range(input_string="10-11,1,5"), [1, 5, 10, 11])
|
|
43
|
+
self.assertEqual(parse_numeric_range(input_string="a", base=16), [10])
|
|
44
|
+
self.assertEqual(parse_numeric_range(input_string="a,b", base=16), [10, 11])
|
|
45
|
+
self.assertEqual(parse_numeric_range(input_string="9-c,f", base=16), [9, 10, 11, 12, 15])
|
|
46
|
+
self.assertEqual(parse_numeric_range(input_string="15-19", base=16), [21, 22, 23, 24, 25])
|
|
47
|
+
self.assertEqual(parse_numeric_range(input_string="fa-ff", base=16), [250, 251, 252, 253, 254, 255])
|
|
48
|
+
|
|
49
|
+
def test_invalid_input(self):
|
|
50
|
+
invalid_inputs = [
|
|
51
|
+
[1, 2, 3],
|
|
52
|
+
None,
|
|
53
|
+
1,
|
|
54
|
+
"",
|
|
55
|
+
"3-",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
for x in invalid_inputs:
|
|
59
|
+
with self.assertRaises(TypeError) as exc:
|
|
60
|
+
parse_numeric_range(input_string=x)
|
|
61
|
+
self.assertEqual(str(exc.exception), "Input value must be a string using a range format.")
|
|
@@ -328,6 +328,44 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase, ViewTestCases.List
|
|
|
328
328
|
strip_tags(content),
|
|
329
329
|
)
|
|
330
330
|
|
|
331
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
332
|
+
def test_prefix_ipaddresses_table_list_includes_child_ips(self):
|
|
333
|
+
ip_status = Status.objects.get_for_model(IPAddress).first()
|
|
334
|
+
instance = Prefix.objects.create(
|
|
335
|
+
prefix="5.5.10.0/23",
|
|
336
|
+
namespace=self.namespace,
|
|
337
|
+
type=PrefixTypeChoices.TYPE_NETWORK,
|
|
338
|
+
status=self.statuses[1],
|
|
339
|
+
)
|
|
340
|
+
Prefix.objects.create(
|
|
341
|
+
prefix="5.5.10.0/30",
|
|
342
|
+
namespace=self.namespace,
|
|
343
|
+
type=PrefixTypeChoices.TYPE_POOL,
|
|
344
|
+
status=self.statuses[1],
|
|
345
|
+
)
|
|
346
|
+
IPAddress.objects.create(
|
|
347
|
+
address="5.5.10.1/23",
|
|
348
|
+
status=ip_status,
|
|
349
|
+
namespace=self.namespace,
|
|
350
|
+
)
|
|
351
|
+
IPAddress.objects.create(
|
|
352
|
+
address="5.5.10.4/23",
|
|
353
|
+
status=ip_status,
|
|
354
|
+
namespace=self.namespace,
|
|
355
|
+
)
|
|
356
|
+
url = reverse("ipam:prefix_ipaddresses", args=(instance.pk,))
|
|
357
|
+
response = self.client.get(url)
|
|
358
|
+
self.assertHttpStatus(response, 200)
|
|
359
|
+
content = response.content.decode(response.charset)
|
|
360
|
+
# This validates that both parent prefix and child prefix IPAddresses are present in parent prefix IPAddresses list
|
|
361
|
+
self.assertIn("5.5.10.1/23", strip_tags(content))
|
|
362
|
+
self.assertIn("5.5.10.4/23", strip_tags(content))
|
|
363
|
+
print(response.content.decode(response.charset))
|
|
364
|
+
ip_address_tab = (
|
|
365
|
+
f'<li role="presentation" class="active"><a href="{url}">IP Addresses <span class="badge">2</span></a></li>'
|
|
366
|
+
)
|
|
367
|
+
self.assertInHTML(ip_address_tab, content)
|
|
368
|
+
|
|
331
369
|
|
|
332
370
|
class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
333
371
|
model = IPAddress
|
|
@@ -993,6 +1031,8 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|
|
993
1031
|
"name": "VLAN Group X",
|
|
994
1032
|
"location": location.pk,
|
|
995
1033
|
"description": "A new VLAN group",
|
|
1034
|
+
"range": "1-4094",
|
|
1035
|
+
"tags": [t.pk for t in Tag.objects.get_for_model(VLANGroup)],
|
|
996
1036
|
}
|
|
997
1037
|
|
|
998
1038
|
def get_deletable_object(self):
|
|
@@ -1005,7 +1045,6 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
1005
1045
|
@classmethod
|
|
1006
1046
|
def setUpTestData(cls):
|
|
1007
1047
|
cls.locations = Location.objects.filter(location_type=LocationType.objects.get(name="Campus"))
|
|
1008
|
-
location_1 = cls.locations.first()
|
|
1009
1048
|
|
|
1010
1049
|
vlangroups = (
|
|
1011
1050
|
VLANGroup.objects.create(name="VLAN Group 1", location=cls.locations.first()),
|
|
@@ -1014,51 +1053,14 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
1014
1053
|
|
|
1015
1054
|
roles = Role.objects.get_for_model(VLAN)[:2]
|
|
1016
1055
|
|
|
1017
|
-
|
|
1018
|
-
status_1 = statuses[0]
|
|
1019
|
-
status_2 = statuses[1]
|
|
1020
|
-
|
|
1021
|
-
vlans = (
|
|
1022
|
-
VLAN.objects.create(
|
|
1023
|
-
vlan_group=vlangroups[0],
|
|
1024
|
-
vid=101,
|
|
1025
|
-
name="VLAN101",
|
|
1026
|
-
role=roles[0],
|
|
1027
|
-
status=status_1,
|
|
1028
|
-
_custom_field_data={"custom_field": "Value"},
|
|
1029
|
-
),
|
|
1030
|
-
VLAN.objects.create(
|
|
1031
|
-
vlan_group=vlangroups[0],
|
|
1032
|
-
vid=102,
|
|
1033
|
-
name="VLAN102",
|
|
1034
|
-
role=roles[0],
|
|
1035
|
-
status=status_1,
|
|
1036
|
-
_custom_field_data={"custom_field": "Value"},
|
|
1037
|
-
),
|
|
1038
|
-
VLAN.objects.create(
|
|
1039
|
-
vlan_group=vlangroups[0],
|
|
1040
|
-
vid=103,
|
|
1041
|
-
name="VLAN103",
|
|
1042
|
-
role=roles[0],
|
|
1043
|
-
status=status_1,
|
|
1044
|
-
_custom_field_data={"custom_field": "Value"},
|
|
1045
|
-
),
|
|
1046
|
-
)
|
|
1047
|
-
vlans[0].locations.add(location_1)
|
|
1048
|
-
vlans[1].locations.add(location_1)
|
|
1049
|
-
vlans[2].locations.add(location_1)
|
|
1050
|
-
|
|
1051
|
-
custom_field = CustomField.objects.create(
|
|
1052
|
-
type=CustomFieldTypeChoices.TYPE_TEXT, label="Custom Field", default=""
|
|
1053
|
-
)
|
|
1054
|
-
custom_field.content_types.set([ContentType.objects.get_for_model(VLAN)])
|
|
1056
|
+
status = Status.objects.get_for_model(VLAN).first()
|
|
1055
1057
|
|
|
1056
1058
|
cls.form_data = {
|
|
1057
1059
|
"vlan_group": vlangroups[1].pk,
|
|
1058
1060
|
"vid": 999,
|
|
1059
1061
|
"name": "VLAN999 with an unwieldy long name since we increased the limit to more than 64 characters",
|
|
1060
1062
|
"tenant": None,
|
|
1061
|
-
"status":
|
|
1063
|
+
"status": status.pk,
|
|
1062
1064
|
"role": roles[1].pk,
|
|
1063
1065
|
"locations": list(cls.locations.values_list("pk", flat=True)[:2]),
|
|
1064
1066
|
"description": "A new VLAN",
|
|
@@ -1068,7 +1070,7 @@ class VLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
1068
1070
|
cls.bulk_edit_data = {
|
|
1069
1071
|
"vlan_group": vlangroups[0].pk,
|
|
1070
1072
|
"tenant": Tenant.objects.first().pk,
|
|
1071
|
-
"status":
|
|
1073
|
+
"status": status.pk,
|
|
1072
1074
|
"role": roles[0].pk,
|
|
1073
1075
|
"description": "New description",
|
|
1074
1076
|
}
|