nautobot 2.3.6__py3-none-any.whl → 2.3.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/__init__.py +4 -2
- nautobot/circuits/tests/test_views.py +4 -5
- nautobot/core/api/views.py +15 -3
- nautobot/core/templates/inc/javascript.html +2 -0
- nautobot/core/templates/inc/nav_menu.html +0 -251
- nautobot/core/templates/inc/paginator.html +3 -0
- nautobot/core/templates/utilities/obj_table.html +1 -1
- 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_views.py +39 -56
- nautobot/core/views/__init__.py +27 -11
- nautobot/dcim/api/serializers.py +10 -5
- nautobot/dcim/forms.py +11 -7
- nautobot/dcim/models/device_components.py +7 -4
- nautobot/dcim/tests/test_api.py +28 -0
- nautobot/dcim/tests/test_forms.py +17 -1
- nautobot/dcim/tests/test_models.py +42 -4
- nautobot/dcim/tests/test_views.py +26 -67
- nautobot/dcim/utils.py +9 -6
- nautobot/extras/datasources/git.py +6 -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/plugins/views.py +18 -3
- nautobot/extras/tests/test_customfields.py +9 -16
- nautobot/extras/tests/test_dynamicgroups.py +116 -0
- nautobot/extras/tests/test_plugins.py +4 -6
- nautobot/extras/tests/test_utils.py +5 -0
- nautobot/extras/tests/test_views.py +61 -159
- nautobot/extras/utils.py +50 -11
- nautobot/ipam/filters.py +2 -2
- nautobot/ipam/models.py +29 -2
- nautobot/ipam/templates/ipam/ipaddress.html +2 -2
- nautobot/ipam/templates/ipam/ipaddress_interfaces.html +3 -0
- nautobot/ipam/templates/ipam/ipaddress_vm_interfaces.html +3 -0
- nautobot/ipam/templates/ipam/prefix.html +3 -3
- nautobot/ipam/templates/ipam/routetarget.html +2 -2
- nautobot/ipam/templates/ipam/vlan.html +3 -0
- nautobot/ipam/templates/ipam/vrf.html +7 -4
- nautobot/ipam/tests/test_api.py +18 -12
- nautobot/ipam/tests/test_models.py +68 -12
- nautobot/ipam/tests/test_views.py +6 -15
- nautobot/ipam/views.py +43 -0
- 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 +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +3 -3
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +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 +3 -3
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +3 -3
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +3 -3
- nautobot/project-static/docs/development/apps/api/prometheus.html +3 -3
- nautobot/project-static/docs/development/apps/api/setup.html +3 -3
- nautobot/project-static/docs/development/apps/api/testing.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +3 -3
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/base-template.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/index.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/notes.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +3 -3
- nautobot/project-static/docs/development/apps/api/views/urls.html +3 -3
- nautobot/project-static/docs/development/apps/index.html +3 -3
- nautobot/project-static/docs/development/apps/migration/code-updates.html +3 -3
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +3 -3
- nautobot/project-static/docs/development/apps/migration/from-v1.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +3 -3
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +3 -3
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +3 -3
- nautobot/project-static/docs/development/core/application-registry.html +3 -3
- nautobot/project-static/docs/development/core/best-practices.html +3 -3
- nautobot/project-static/docs/development/core/bootstrap-ui.html +3 -3
- nautobot/project-static/docs/development/core/caching.html +3 -3
- nautobot/project-static/docs/development/core/controllers.html +3 -3
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +3 -3
- nautobot/project-static/docs/development/core/generic-views.html +3 -3
- nautobot/project-static/docs/development/core/getting-started.html +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 +304 -96
- 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 +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +3 -3
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +3 -3
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +3 -3
- nautobot/project-static/docs/user-guide/index.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +3 -3
- nautobot/project-static/docs/user-guide/platform-functionality/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.6.dist-info → nautobot-2.3.8.dist-info}/METADATA +2 -1
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/RECORD +333 -331
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.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.6.dist-info → nautobot-2.3.8.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/NOTICE +0 -0
- {nautobot-2.3.6.dist-info → nautobot-2.3.8.dist-info}/entry_points.txt +0 -0
nautobot/dcim/forms.py
CHANGED
|
@@ -213,23 +213,27 @@ class InterfaceCommonForm(forms.Form):
|
|
|
213
213
|
elif mode == InterfaceModeChoices.MODE_TAGGED_ALL:
|
|
214
214
|
self.cleaned_data["tagged_vlans"] = []
|
|
215
215
|
|
|
216
|
-
# Validate tagged VLANs; must be a global VLAN or in the same location
|
|
217
|
-
#
|
|
218
|
-
# belongs to the parent Location or the child location of the parent device to the `tagged_vlan` field of the interface?
|
|
216
|
+
# Validate tagged VLANs; must be a global VLAN or in the same location as the
|
|
217
|
+
# parent device/VM or any of that location's parent locations
|
|
219
218
|
elif mode == InterfaceModeChoices.MODE_TAGGED:
|
|
220
|
-
|
|
219
|
+
location = self.cleaned_data[parent_field].location
|
|
220
|
+
if location:
|
|
221
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
222
|
+
else:
|
|
223
|
+
location_ids = []
|
|
221
224
|
invalid_vlans = [
|
|
222
225
|
str(v)
|
|
223
226
|
for v in tagged_vlans
|
|
224
227
|
if v.locations.without_tree_fields().exists()
|
|
225
|
-
and not VLANLocationAssignment.objects.filter(
|
|
228
|
+
and not VLANLocationAssignment.objects.filter(location__in=location_ids, vlan=v).exists()
|
|
226
229
|
]
|
|
227
230
|
|
|
228
231
|
if invalid_vlans:
|
|
229
232
|
raise forms.ValidationError(
|
|
230
233
|
{
|
|
231
|
-
"tagged_vlans": f"The tagged VLANs ({', '.join(invalid_vlans)}) must
|
|
232
|
-
|
|
234
|
+
"tagged_vlans": f"The tagged VLANs ({', '.join(invalid_vlans)}) must have the same location as the "
|
|
235
|
+
"interface's parent device, or is in one of the parents of the interface's parent device's location, "
|
|
236
|
+
"or it must be global."
|
|
233
237
|
}
|
|
234
238
|
)
|
|
235
239
|
|
|
@@ -727,19 +727,22 @@ class Interface(ModularComponentModel, CableTermination, PathEndpoint, BaseInter
|
|
|
727
727
|
)
|
|
728
728
|
|
|
729
729
|
# Validate untagged VLAN
|
|
730
|
-
|
|
731
|
-
|
|
730
|
+
location = self.parent.location if self.parent is not None else None
|
|
731
|
+
if location:
|
|
732
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
733
|
+
else:
|
|
734
|
+
location_ids = []
|
|
732
735
|
if (
|
|
733
736
|
self.untagged_vlan
|
|
734
737
|
and self.untagged_vlan.locations.exists()
|
|
735
738
|
and self.parent
|
|
736
|
-
and not self.untagged_vlan.locations.filter(
|
|
739
|
+
and not self.untagged_vlan.locations.filter(pk__in=location_ids).exists()
|
|
737
740
|
):
|
|
738
741
|
raise ValidationError(
|
|
739
742
|
{
|
|
740
743
|
"untagged_vlan": (
|
|
741
744
|
f"The untagged VLAN ({self.untagged_vlan}) must have a common location as the interface's parent "
|
|
742
|
-
f"device, or it must be global."
|
|
745
|
+
f"device, or is in one of the parents of the interface's parent device's location, or it must be global."
|
|
743
746
|
)
|
|
744
747
|
}
|
|
745
748
|
)
|
nautobot/dcim/tests/test_api.py
CHANGED
|
@@ -2198,6 +2198,34 @@ class InterfaceTest(Mixins.ModularDeviceComponentMixin, Mixins.BasePortTestMixin
|
|
|
2198
2198
|
self.client.post(url, self.untagged_vlan_data, format="json", **self.header), status.HTTP_201_CREATED
|
|
2199
2199
|
)
|
|
2200
2200
|
|
|
2201
|
+
def test_tagged_vlan_must_be_in_the_location_or_parent_locations_of_the_parent_device(self):
|
|
2202
|
+
self.add_permissions("dcim.add_interface")
|
|
2203
|
+
|
|
2204
|
+
interface_status = Status.objects.get_for_model(Interface).first()
|
|
2205
|
+
location = self.devices[0].location
|
|
2206
|
+
location_ids = [ancestor.id for ancestor in location.ancestors()]
|
|
2207
|
+
non_valid_locations = Location.objects.exclude(pk__in=location_ids)
|
|
2208
|
+
faulty_vlan = self.vlans[0]
|
|
2209
|
+
faulty_vlan.locations.set([non_valid_locations.first().pk])
|
|
2210
|
+
faulty_vlan.validated_save()
|
|
2211
|
+
faulty_data = {
|
|
2212
|
+
"device": self.devices[0].pk,
|
|
2213
|
+
"name": "Test Vlans Interface",
|
|
2214
|
+
"type": "virtual",
|
|
2215
|
+
"status": interface_status.pk,
|
|
2216
|
+
"mode": InterfaceModeChoices.MODE_TAGGED,
|
|
2217
|
+
"parent_interface": self.interfaces[1].pk,
|
|
2218
|
+
"tagged_vlans": [faulty_vlan.pk, self.vlans[1].pk],
|
|
2219
|
+
"untagged_vlan": self.vlans[2].pk,
|
|
2220
|
+
}
|
|
2221
|
+
response = self.client.post(self._get_list_url(), data=faulty_data, format="json", **self.header)
|
|
2222
|
+
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
2223
|
+
self.assertIn(
|
|
2224
|
+
b"must have the same location as the interface's parent device, or is in one of the parents of the interface's parent device's location, or "
|
|
2225
|
+
b"it must be global.",
|
|
2226
|
+
response.content,
|
|
2227
|
+
)
|
|
2228
|
+
|
|
2201
2229
|
def test_interface_belonging_to_common_device_or_vc_allowed(self):
|
|
2202
2230
|
"""Test parent, bridge, and LAG interfaces belonging to common device or VC is valid"""
|
|
2203
2231
|
self.add_permissions("dcim.add_interface")
|
|
@@ -341,9 +341,25 @@ class InterfaceTestCase(TestCase):
|
|
|
341
341
|
"tagged_vlans": [cls.vlan.pk],
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
+
def test_interface_form_clean_vlan_location_success(self):
|
|
345
|
+
"""Assert that form validation succeeds when matching locations/parent locations are associated to tagged VLAN"""
|
|
346
|
+
location = self.device.location
|
|
347
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
348
|
+
self.vlan.locations.set([location.id])
|
|
349
|
+
self.data["tagged_vlans"] = [self.vlan]
|
|
350
|
+
form = InterfaceForm(data=self.data, instance=self.interface)
|
|
351
|
+
self.assertTrue(form.is_valid())
|
|
352
|
+
self.vlan.locations.set(location_ids[:2])
|
|
353
|
+
self.data["tagged_vlans"] = [self.vlan]
|
|
354
|
+
form = InterfaceForm(data=self.data, instance=self.interface)
|
|
355
|
+
self.assertTrue(form.is_valid())
|
|
356
|
+
|
|
344
357
|
def test_interface_form_clean_vlan_location_fail(self):
|
|
345
358
|
"""Assert that form validation fails when no matching locations are associated to tagged VLAN"""
|
|
346
|
-
|
|
359
|
+
location = self.device.location
|
|
360
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
361
|
+
self.vlan.locations.set(list(Location.objects.exclude(pk__in=location_ids))[:2])
|
|
362
|
+
self.data["tagged_vlans"] = [self.vlan]
|
|
347
363
|
form = InterfaceForm(data=self.data, instance=self.interface)
|
|
348
364
|
self.assertFalse(form.is_valid())
|
|
349
365
|
|
|
@@ -2376,19 +2376,57 @@ class InterfaceTestCase(ModularDeviceComponentTestCaseMixin, ModelTestCases.Base
|
|
|
2376
2376
|
)
|
|
2377
2377
|
|
|
2378
2378
|
def test_error_raised_when_adding_tagged_vlan_with_different_location_from_interface_parent_location(self):
|
|
2379
|
+
intf_status = Status.objects.get_for_model(Interface).first()
|
|
2380
|
+
intf_role = Role.objects.get_for_model(Interface).first()
|
|
2381
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
2382
|
+
child_location = Location.objects.filter(parent__isnull=False, location_type=location_type).first()
|
|
2383
|
+
self.device.location = child_location
|
|
2384
|
+
self.device.validated_save()
|
|
2385
|
+
# Same location as the device
|
|
2386
|
+
interface = Interface.objects.create(
|
|
2387
|
+
name="Test Interface 2",
|
|
2388
|
+
mode=InterfaceModeChoices.MODE_TAGGED,
|
|
2389
|
+
device=self.device,
|
|
2390
|
+
status=intf_status,
|
|
2391
|
+
role=intf_role,
|
|
2392
|
+
)
|
|
2393
|
+
self.other_location_vlan.locations.set([self.device.location.pk])
|
|
2394
|
+
interface.tagged_vlans.set([self.other_location_vlan.pk])
|
|
2395
|
+
|
|
2396
|
+
# One of the parent locations of the device's location
|
|
2397
|
+
interface = Interface.objects.create(
|
|
2398
|
+
name="Test Interface 3",
|
|
2399
|
+
mode=InterfaceModeChoices.MODE_TAGGED,
|
|
2400
|
+
device=self.device,
|
|
2401
|
+
status=intf_status,
|
|
2402
|
+
role=intf_role,
|
|
2403
|
+
)
|
|
2404
|
+
self.other_location_vlan.locations.set([self.device.location.ancestors().first().pk])
|
|
2405
|
+
interface.tagged_vlans.set([self.other_location_vlan.pk])
|
|
2406
|
+
|
|
2379
2407
|
with self.assertRaises(ValidationError) as err:
|
|
2380
2408
|
interface = Interface.objects.create(
|
|
2381
|
-
name="Test Interface",
|
|
2409
|
+
name="Test Interface 1",
|
|
2382
2410
|
mode=InterfaceModeChoices.MODE_TAGGED,
|
|
2383
2411
|
device=self.device,
|
|
2384
|
-
status=
|
|
2385
|
-
role=
|
|
2412
|
+
status=intf_status,
|
|
2413
|
+
role=intf_role,
|
|
2414
|
+
)
|
|
2415
|
+
location_3 = Location.objects.create(
|
|
2416
|
+
name="Invalid VLAN Location",
|
|
2417
|
+
location_type=LocationType.objects.get(name="Campus"),
|
|
2418
|
+
status=Status.objects.get_for_model(Location).first(),
|
|
2386
2419
|
)
|
|
2420
|
+
# clear the valid locations
|
|
2421
|
+
self.other_location_vlan.locations.set([])
|
|
2422
|
+
# assign the invalid location
|
|
2423
|
+
self.other_location_vlan.location = location_3
|
|
2424
|
+
self.other_location_vlan.validated_save()
|
|
2387
2425
|
interface.tagged_vlans.add(self.other_location_vlan)
|
|
2388
2426
|
self.assertEqual(
|
|
2389
2427
|
err.exception.message_dict["tagged_vlans"][0],
|
|
2390
2428
|
f"Tagged VLAN with names {[self.other_location_vlan.name]} must all belong to the "
|
|
2391
|
-
f"same location as the interface's parent device, or it must be global.",
|
|
2429
|
+
f"same location as the interface's parent device, one of the parent locations of the interface's parent device's location, or it must be global.",
|
|
2392
2430
|
)
|
|
2393
2431
|
|
|
2394
2432
|
def test_add_ip_addresses(self):
|
|
@@ -284,9 +284,7 @@ class LocationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
284
284
|
"data": post_data(test_form_data),
|
|
285
285
|
}
|
|
286
286
|
response = self.client.post(**request)
|
|
287
|
-
self.
|
|
288
|
-
response_body = response.content.decode(response.charset)
|
|
289
|
-
self.assertIn("“Generic Site” is not a valid UUID.", response_body)
|
|
287
|
+
self.assertBodyContains(response, "“Generic Site” is not a valid UUID.")
|
|
290
288
|
test_form_data["parent"] = site_1.pk
|
|
291
289
|
request["data"] = post_data(test_form_data)
|
|
292
290
|
self.assertHttpStatus(self.client.post(**request), 302)
|
|
@@ -1172,8 +1170,7 @@ module-bays:
|
|
|
1172
1170
|
}
|
|
1173
1171
|
|
|
1174
1172
|
response = self.client.post(url, data)
|
|
1175
|
-
self.
|
|
1176
|
-
self.assertIn("failed validation", response.content.decode(response.charset))
|
|
1173
|
+
self.assertBodyContains(response, "failed validation")
|
|
1177
1174
|
|
|
1178
1175
|
|
|
1179
1176
|
class ModuleTypeTestCase(
|
|
@@ -2279,12 +2276,8 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2279
2276
|
|
|
2280
2277
|
url = reverse("dcim:device_interfaces", kwargs={"pk": device.pk})
|
|
2281
2278
|
response = self.client.get(url)
|
|
2282
|
-
self.assertHttpStatus(response, 200)
|
|
2283
|
-
response_body = response.content.decode(response.charset)
|
|
2284
|
-
# Count the number of occurrences of "Add IP address" in the response_body
|
|
2285
|
-
count = response_body.count("Add IP address")
|
|
2286
2279
|
# Assert that "Add IP address" appears for each of the three interfaces
|
|
2287
|
-
self.
|
|
2280
|
+
self.assertBodyContains(response, "Add IP address", count=3)
|
|
2288
2281
|
|
|
2289
2282
|
def test_device_interface_assign_ipaddress(self):
|
|
2290
2283
|
device = Device.objects.first()
|
|
@@ -2322,29 +2315,19 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2322
2315
|
"data": post_data(assign_ip_form_data),
|
|
2323
2316
|
}
|
|
2324
2317
|
|
|
2325
|
-
with self.subTest("Assert
|
|
2318
|
+
with self.subTest("Assert Cannot assign IPAddress('Add New') without permission"):
|
|
2326
2319
|
# Assert Add new IPAddress
|
|
2327
2320
|
response = self.client.post(**add_new_ip_request, follow=True)
|
|
2328
|
-
|
|
2329
|
-
self.assertHttpStatus(response, 200)
|
|
2321
|
+
self.assertBodyContains(response, f"Interface with id "{self.interfaces[0].pk}" not found")
|
|
2330
2322
|
self.interfaces[0].refresh_from_db()
|
|
2331
2323
|
self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
|
|
2332
|
-
self.assertIn(
|
|
2333
|
-
f"Interface with id "{self.interfaces[0].pk}" not found",
|
|
2334
|
-
response_body,
|
|
2335
|
-
)
|
|
2336
2324
|
|
|
2337
|
-
with self.subTest("Assert
|
|
2325
|
+
with self.subTest("Assert Cannot assign IPAddress(Existing IP) without permission"):
|
|
2338
2326
|
# Assert Assign Exsisting IPAddress
|
|
2339
2327
|
response = self.client.post(**assign_ip_request, follow=True)
|
|
2340
|
-
|
|
2341
|
-
self.assertHttpStatus(response, 200)
|
|
2328
|
+
self.assertBodyContains(response, f"Interface with id "{self.interfaces[1].pk}" not found")
|
|
2342
2329
|
self.interfaces[1].refresh_from_db()
|
|
2343
2330
|
self.assertEqual(self.interfaces[1].ip_addresses.all().count(), 0)
|
|
2344
|
-
self.assertIn(
|
|
2345
|
-
f"Interface with id "{self.interfaces[1].pk}" not found",
|
|
2346
|
-
response_body,
|
|
2347
|
-
)
|
|
2348
2331
|
|
|
2349
2332
|
self.add_permissions("dcim.change_interface", "ipam.view_ipaddress")
|
|
2350
2333
|
|
|
@@ -2375,10 +2358,7 @@ class DeviceTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2375
2358
|
"data": post_data(assign_ip_form_data),
|
|
2376
2359
|
}
|
|
2377
2360
|
response = self.client.post(**assign_ip_request, follow=True)
|
|
2378
|
-
self.
|
|
2379
|
-
self.assertIn(
|
|
2380
|
-
"Please select at least one IP Address from the table.", response.content.decode(response.charset)
|
|
2381
|
-
)
|
|
2361
|
+
self.assertBodyContains(response, "Please select at least one IP Address from the table.")
|
|
2382
2362
|
|
|
2383
2363
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2384
2364
|
def test_device_rearports(self):
|
|
@@ -2683,12 +2663,8 @@ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2683
2663
|
|
|
2684
2664
|
url = reverse("dcim:module_interfaces", kwargs={"pk": module.pk})
|
|
2685
2665
|
response = self.client.get(url)
|
|
2686
|
-
self.assertHttpStatus(response, 200)
|
|
2687
|
-
response_body = response.content.decode(response.charset)
|
|
2688
|
-
# Count the number of occurrences of "Add IP address" in the response_body
|
|
2689
|
-
count = response_body.count("Add IP address")
|
|
2690
2666
|
# Assert that "Add IP address" appears for each of the three interfaces
|
|
2691
|
-
self.
|
|
2667
|
+
self.assertBodyContains(response, "Add IP address", count=3)
|
|
2692
2668
|
|
|
2693
2669
|
def test_module_interface_assign_ipaddress(self):
|
|
2694
2670
|
module = Module.objects.first()
|
|
@@ -2726,29 +2702,19 @@ class ModuleTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
2726
2702
|
"data": post_data(assign_ip_form_data),
|
|
2727
2703
|
}
|
|
2728
2704
|
|
|
2729
|
-
with self.subTest("Assert
|
|
2705
|
+
with self.subTest("Assert Cannot assign IPAddress('Add New') without permission"):
|
|
2730
2706
|
# Assert Add new IPAddress
|
|
2731
2707
|
response = self.client.post(**add_new_ip_request, follow=True)
|
|
2732
|
-
|
|
2733
|
-
self.assertHttpStatus(response, 200)
|
|
2708
|
+
self.assertBodyContains(response, f"Interface with id "{self.interfaces[0].pk}" not found")
|
|
2734
2709
|
self.interfaces[0].refresh_from_db()
|
|
2735
2710
|
self.assertEqual(self.interfaces[0].ip_addresses.all().count(), 0)
|
|
2736
|
-
self.assertIn(
|
|
2737
|
-
f"Interface with id "{self.interfaces[0].pk}" not found",
|
|
2738
|
-
response_body,
|
|
2739
|
-
)
|
|
2740
2711
|
|
|
2741
|
-
with self.subTest("Assert
|
|
2712
|
+
with self.subTest("Assert Cannot assign IPAddress(Existing IP) without permission"):
|
|
2742
2713
|
# Assert Assign Exsisting IPAddress
|
|
2743
2714
|
response = self.client.post(**assign_ip_request, follow=True)
|
|
2744
|
-
|
|
2745
|
-
self.assertHttpStatus(response, 200)
|
|
2715
|
+
self.assertBodyContains(response, f"Interface with id "{self.interfaces[1].pk}" not found")
|
|
2746
2716
|
self.interfaces[1].refresh_from_db()
|
|
2747
2717
|
self.assertEqual(self.interfaces[1].ip_addresses.all().count(), 0)
|
|
2748
|
-
self.assertIn(
|
|
2749
|
-
f"Interface with id "{self.interfaces[1].pk}" not found",
|
|
2750
|
-
response_body,
|
|
2751
|
-
)
|
|
2752
2718
|
|
|
2753
2719
|
self.add_permissions("dcim.change_interface", "ipam.view_ipaddress")
|
|
2754
2720
|
|
|
@@ -3228,8 +3194,8 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|
|
3228
3194
|
invalid_ipaddress_link = reverse("ipam:ipaddress_edit", args=(ipaddress.pk,))
|
|
3229
3195
|
valid_ipaddress_link = ipaddress.get_absolute_url()
|
|
3230
3196
|
response = self.client.get(interface.get_absolute_url() + "?tab=main")
|
|
3231
|
-
|
|
3232
|
-
|
|
3197
|
+
self.assertBodyContains(response, valid_ipaddress_link)
|
|
3198
|
+
response_content = extract_page_body(response.content.decode(response.charset))
|
|
3233
3199
|
self.assertNotIn(invalid_ipaddress_link, response_content)
|
|
3234
3200
|
|
|
3235
3201
|
|
|
@@ -4113,7 +4079,6 @@ class InterfaceConnectionsTestCase(ViewTestCases.ListObjectsViewTestCase):
|
|
|
4113
4079
|
response = self.client.get(f"{self._get_url('list')}?id={instance1.pk}")
|
|
4114
4080
|
self.assertHttpStatus(response, 200)
|
|
4115
4081
|
content = extract_page_body(response.content.decode(response.charset))
|
|
4116
|
-
# TODO: it'd make test failures more readable if we strip the page headers/footers from the content
|
|
4117
4082
|
if hasattr(self.model, "name"):
|
|
4118
4083
|
self.assertIn(instance1.name, content, msg=content)
|
|
4119
4084
|
self.assertNotIn(instance2.name, content, msg=content)
|
|
@@ -4209,11 +4174,11 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4209
4174
|
Interface.objects.create(device=self.devices[2], name="device 2 interface 1", status=interface_status)
|
|
4210
4175
|
Interface.objects.create(device=self.devices[2], name="device 2 interface 2", status=interface_status)
|
|
4211
4176
|
response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
|
|
4212
|
-
self.
|
|
4213
|
-
self.
|
|
4214
|
-
self.
|
|
4215
|
-
self.
|
|
4216
|
-
self.
|
|
4177
|
+
self.assertBodyContains(response, 'Interfaces <span class="badge">6</span>')
|
|
4178
|
+
self.assertBodyContains(response, "device 1 interface 1")
|
|
4179
|
+
self.assertBodyContains(response, "device 1 interface 2")
|
|
4180
|
+
self.assertBodyContains(response, "device 2 interface 1")
|
|
4181
|
+
self.assertBodyContains(response, "device 2 interface 2")
|
|
4217
4182
|
|
|
4218
4183
|
def test_device_column_visible(self):
|
|
4219
4184
|
"""
|
|
@@ -4226,7 +4191,7 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4226
4191
|
Interface.objects.create(device=self.devices[0], name="eth0", status=interface_status)
|
|
4227
4192
|
Interface.objects.create(device=self.devices[0], name="eth1", status=interface_status)
|
|
4228
4193
|
response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[0].pk}))
|
|
4229
|
-
self.
|
|
4194
|
+
self.assertBodyContains(response, "<th>Device</th>", html=True)
|
|
4230
4195
|
|
|
4231
4196
|
def test_device_column_not_visible(self):
|
|
4232
4197
|
"""
|
|
@@ -4239,9 +4204,9 @@ class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4239
4204
|
Interface.objects.create(device=self.devices[1], name="eth2", status=interface_status)
|
|
4240
4205
|
Interface.objects.create(device=self.devices[1], name="eth3", status=interface_status)
|
|
4241
4206
|
response = self.client.get(reverse("dcim:device_interfaces", kwargs={"pk": self.devices[1].pk}))
|
|
4242
|
-
self.assertNotIn("<th >Device</th>",
|
|
4207
|
+
self.assertNotIn("<th >Device</th>", extract_page_body(response.content.decode(response.charset)))
|
|
4243
4208
|
# Sanity check:
|
|
4244
|
-
self.
|
|
4209
|
+
self.assertBodyContains(response, "<th>Name</th>", html=True)
|
|
4245
4210
|
|
|
4246
4211
|
|
|
4247
4212
|
class PowerPanelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
@@ -4408,9 +4373,7 @@ class PathTraceViewTestCase(ModelViewTestCase):
|
|
|
4408
4373
|
url = reverse("dcim:rearport_trace", args=[obj.pk])
|
|
4409
4374
|
cablepath_id = CablePath.objects.first().id
|
|
4410
4375
|
response = self.client.get(url + f"?cablepath_id={cablepath_id}")
|
|
4411
|
-
self.
|
|
4412
|
-
content = extract_page_body(response.content.decode(response.charset))
|
|
4413
|
-
self.assertInHTML("<h1>Cable Trace for Rear Port Rear Port 1</h1>", content)
|
|
4376
|
+
self.assertBodyContains(response, "<h1>Cable Trace for Rear Port Rear Port 1</h1>", html=True)
|
|
4414
4377
|
|
|
4415
4378
|
|
|
4416
4379
|
class DeviceRedundancyGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
@@ -4621,10 +4584,8 @@ class SoftwareImageFileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4621
4584
|
"_confirm": True, # Form button
|
|
4622
4585
|
}
|
|
4623
4586
|
response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
|
|
4624
|
-
self.assertHttpStatus(response, 200)
|
|
4625
|
-
response_body = response.content.decode(response.charset)
|
|
4626
4587
|
# Assert protected error message included in the response body
|
|
4627
|
-
self.
|
|
4588
|
+
self.assertBodyContains(response, f"<span>{device_type_to_software_image_file}</span>", html=True)
|
|
4628
4589
|
|
|
4629
4590
|
|
|
4630
4591
|
class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
@@ -4701,10 +4662,8 @@ class SoftwareVersionTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|
|
4701
4662
|
"_confirm": True, # Form button
|
|
4702
4663
|
}
|
|
4703
4664
|
response = self.client.post(self._get_url("bulk_delete"), data, follow=True)
|
|
4704
|
-
self.assertHttpStatus(response, 200)
|
|
4705
|
-
response_body = response.content.decode(response.charset)
|
|
4706
4665
|
# Assert protected error message included in the response body
|
|
4707
|
-
self.
|
|
4666
|
+
self.assertBodyContains(response, f"<span>{device_type_to_software_image_file}</span>", html=True)
|
|
4708
4667
|
|
|
4709
4668
|
|
|
4710
4669
|
class ControllerTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
nautobot/dcim/utils.py
CHANGED
|
@@ -139,12 +139,14 @@ def validate_interface_tagged_vlans(instance, model, pk_set):
|
|
|
139
139
|
)
|
|
140
140
|
|
|
141
141
|
# Filter the model objects based on the primary keys passed in kwargs and exclude the ones that have
|
|
142
|
-
# a location that is not the parent's location or None
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
# a location that is not the parent's location, or parent's location's ancestors, or None
|
|
143
|
+
location = getattr(instance.parent, "location", None)
|
|
144
|
+
if location:
|
|
145
|
+
location_ids = location.ancestors(include_self=True).values_list("id", flat=True)
|
|
146
|
+
else:
|
|
147
|
+
location_ids = []
|
|
146
148
|
tagged_vlans = (
|
|
147
|
-
model.objects.filter(pk__in=pk_set).exclude(locations__isnull=True).exclude(locations__in=
|
|
149
|
+
model.objects.filter(pk__in=pk_set).exclude(locations__isnull=True).exclude(locations__in=location_ids)
|
|
148
150
|
)
|
|
149
151
|
|
|
150
152
|
if tagged_vlans.count():
|
|
@@ -152,7 +154,8 @@ def validate_interface_tagged_vlans(instance, model, pk_set):
|
|
|
152
154
|
{
|
|
153
155
|
"tagged_vlans": (
|
|
154
156
|
f"Tagged VLAN with names {list(tagged_vlans.values_list('name', flat=True))} must all belong to the "
|
|
155
|
-
|
|
157
|
+
"same location as the interface's parent device, "
|
|
158
|
+
"one of the parent locations of the interface's parent device's location, or it must be global."
|
|
156
159
|
)
|
|
157
160
|
}
|
|
158
161
|
)
|
|
@@ -177,7 +177,12 @@ def ensure_git_repository(repository_record, logger=None, head=None): # pylint:
|
|
|
177
177
|
if logger:
|
|
178
178
|
if changed:
|
|
179
179
|
logger.info("Repository successfully refreshed")
|
|
180
|
-
logger.info(
|
|
180
|
+
logger.info(
|
|
181
|
+
'%s: the current Git repository hash is "%s"',
|
|
182
|
+
repository_record.name,
|
|
183
|
+
repository_record.current_head,
|
|
184
|
+
extra={"object": repository_record},
|
|
185
|
+
)
|
|
181
186
|
|
|
182
187
|
return changed
|
|
183
188
|
|
|
@@ -10,6 +10,9 @@ def set_dynamic_group_group_types(apps, schema_editor):
|
|
|
10
10
|
# The group_type field defaults to TYPE_DYNAMIC_FILTER
|
|
11
11
|
# There are no preexisting TYPE_STATIC groups as that's a new feature
|
|
12
12
|
# Any group that has children should be converted to TYPE_DYNAMIC_SET
|
|
13
|
+
# NOTE: The below is actually incorrect (see migration 0116) as for some reason, during migrations ONLY,
|
|
14
|
+
# Django somehow swaps the `parent` and `children` relations on DynamicGroup such that the below actually detects
|
|
15
|
+
# the opposite set of groups from what would be expected.
|
|
13
16
|
DynamicGroup.objects.filter(children__isnull=False).distinct().update(
|
|
14
17
|
group_type=DynamicGroupTypeChoices.TYPE_DYNAMIC_SET
|
|
15
18
|
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.db import migrations
|
|
2
|
+
|
|
3
|
+
from nautobot.extras.utils import fixup_dynamic_group_group_types
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
dependencies = [
|
|
8
|
+
("extras", "0115_scheduledjob_time_zone"),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
operations = [
|
|
12
|
+
migrations.RunPython(
|
|
13
|
+
code=fixup_dynamic_group_group_types,
|
|
14
|
+
reverse_code=migrations.operations.special.RunPython.noop,
|
|
15
|
+
),
|
|
16
|
+
]
|
nautobot/extras/plugins/views.py
CHANGED
|
@@ -30,6 +30,21 @@ class InstalledAppsView(GenericView):
|
|
|
30
30
|
data = []
|
|
31
31
|
for app in apps.get_app_configs():
|
|
32
32
|
if app.name in settings.PLUGINS:
|
|
33
|
+
try:
|
|
34
|
+
reverse(app.home_view_name)
|
|
35
|
+
home_url = app.home_view_name
|
|
36
|
+
except NoReverseMatch:
|
|
37
|
+
home_url = None
|
|
38
|
+
try:
|
|
39
|
+
reverse(app.config_view_name)
|
|
40
|
+
config_url = app.config_view_name
|
|
41
|
+
except NoReverseMatch:
|
|
42
|
+
config_url = None
|
|
43
|
+
try:
|
|
44
|
+
reverse(app.docs_view_name)
|
|
45
|
+
docs_url = app.docs_view_name
|
|
46
|
+
except NoReverseMatch:
|
|
47
|
+
docs_url = None
|
|
33
48
|
data.append(
|
|
34
49
|
{
|
|
35
50
|
"name": app.verbose_name,
|
|
@@ -40,9 +55,9 @@ class InstalledAppsView(GenericView):
|
|
|
40
55
|
"description": app.description,
|
|
41
56
|
"version": app.version,
|
|
42
57
|
"actions": {
|
|
43
|
-
"home":
|
|
44
|
-
"configure":
|
|
45
|
-
"docs":
|
|
58
|
+
"home": home_url,
|
|
59
|
+
"configure": config_url,
|
|
60
|
+
"docs": docs_url,
|
|
46
61
|
},
|
|
47
62
|
}
|
|
48
63
|
)
|
|
@@ -15,7 +15,7 @@ from nautobot.core.models.fields import slugify_dashes_to_underscores
|
|
|
15
15
|
from nautobot.core.tables import CustomFieldColumn
|
|
16
16
|
from nautobot.core.testing import APITestCase, TestCase, TransactionTestCase
|
|
17
17
|
from nautobot.core.testing.models import ModelTestCases
|
|
18
|
-
from nautobot.core.testing.utils import post_data
|
|
18
|
+
from nautobot.core.testing.utils import extract_page_body, post_data
|
|
19
19
|
from nautobot.core.utils.lookup import get_changes_for_model
|
|
20
20
|
from nautobot.dcim.filters import LocationFilterSet
|
|
21
21
|
from nautobot.dcim.forms import RackFilterForm
|
|
@@ -995,18 +995,15 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
995
995
|
},
|
|
996
996
|
}
|
|
997
997
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
998
|
-
self.
|
|
999
|
-
self.assertIn("Value must be a string", str(response.content))
|
|
998
|
+
self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1000
999
|
|
|
1001
1000
|
data["custom_fields"].update({self.cf_text.key: 2})
|
|
1002
1001
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1003
|
-
self.
|
|
1004
|
-
self.assertIn("Value must be a string", str(response.content))
|
|
1002
|
+
self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1005
1003
|
|
|
1006
1004
|
data["custom_fields"].update({self.cf_text.key: True})
|
|
1007
1005
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1008
|
-
self.
|
|
1009
|
-
self.assertIn("Value must be a string", str(response.content))
|
|
1006
|
+
self.assertContains(response, "Value must be a string", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1010
1007
|
|
|
1011
1008
|
def test_create_without_required_field(self):
|
|
1012
1009
|
self.cf_text.default = None
|
|
@@ -1019,8 +1016,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1019
1016
|
"status": self.statuses[0].pk,
|
|
1020
1017
|
}
|
|
1021
1018
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1022
|
-
self.
|
|
1023
|
-
self.assertIn("Required field cannot be empty", str(response.content))
|
|
1019
|
+
self.assertContains(response, "Required field cannot be empty", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1024
1020
|
|
|
1025
1021
|
# Try in CSV format too
|
|
1026
1022
|
csvdata = "\n".join(
|
|
@@ -1030,8 +1026,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1030
1026
|
]
|
|
1031
1027
|
)
|
|
1032
1028
|
response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
|
|
1033
|
-
self.
|
|
1034
|
-
self.assertIn("Required field cannot be empty", str(response.content))
|
|
1029
|
+
self.assertContains(response, "Required field cannot be empty", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1035
1030
|
|
|
1036
1031
|
def test_create_invalid_select_choice(self):
|
|
1037
1032
|
data = {
|
|
@@ -1043,8 +1038,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1043
1038
|
},
|
|
1044
1039
|
}
|
|
1045
1040
|
response = self.client.post(self.list_url, data, format="json", **self.header)
|
|
1046
|
-
self.
|
|
1047
|
-
self.assertIn("Invalid choice", str(response.content))
|
|
1041
|
+
self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1048
1042
|
|
|
1049
1043
|
# Try in CSV format too
|
|
1050
1044
|
csvdata = "\n".join(
|
|
@@ -1054,8 +1048,7 @@ class CustomFieldDataAPITest(APITestCase):
|
|
|
1054
1048
|
]
|
|
1055
1049
|
)
|
|
1056
1050
|
response = self.client.post(self.list_url, csvdata, content_type="text/csv", **self.header)
|
|
1057
|
-
self.
|
|
1058
|
-
self.assertIn("Invalid choice", str(response.content))
|
|
1051
|
+
self.assertContains(response, "Invalid choice", status_code=status.HTTP_400_BAD_REQUEST)
|
|
1059
1052
|
|
|
1060
1053
|
|
|
1061
1054
|
class CustomFieldImportTest(TestCase):
|
|
@@ -1157,7 +1150,7 @@ class CustomFieldImportTest(TestCase):
|
|
|
1157
1150
|
try:
|
|
1158
1151
|
location1 = Location.objects.get(name="Location 1")
|
|
1159
1152
|
except Location.DoesNotExist:
|
|
1160
|
-
self.fail(
|
|
1153
|
+
self.fail(extract_page_body(response.content.decode(response.charset)))
|
|
1161
1154
|
self.assertEqual(len(location1.cf), 8)
|
|
1162
1155
|
self.assertEqual(location1.cf["text"], "ABC")
|
|
1163
1156
|
self.assertEqual(location1.cf["integer"], 123)
|