nautobot 2.4.16__py3-none-any.whl → 2.4.18__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/apps/utils.py +2 -0
- nautobot/apps/views.py +2 -0
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -8
- nautobot/circuits/templates/circuits/inc/circuit_termination_speed_fragment.html +9 -0
- nautobot/circuits/tests/integration/test_circuit.py +2 -2
- nautobot/circuits/views.py +32 -15
- nautobot/cloud/templates/cloud/cloudresourcetype_retrieve.html +3 -3
- nautobot/cloud/views.py +7 -0
- nautobot/core/apps/__init__.py +1 -0
- nautobot/core/celery/__init__.py +2 -1
- nautobot/core/filters.py +2 -2
- nautobot/core/settings.py +1 -0
- nautobot/core/settings.yaml +9 -0
- nautobot/core/tables.py +21 -23
- nautobot/core/templates/components/breadcrumbs.html +19 -0
- nautobot/core/templates/components/panel/panel.html +1 -1
- nautobot/core/templates/generic/object_changelog.html +0 -2
- nautobot/core/templates/generic/object_list.html +15 -12
- nautobot/core/templates/generic/object_notes.html +0 -2
- nautobot/core/templates/generic/object_retrieve.html +16 -9
- nautobot/core/templates/inc/paginator.html +3 -3
- nautobot/core/templates/inc/table.html +2 -2
- nautobot/core/templatetags/helpers.py +104 -6
- nautobot/core/templatetags/ui_framework.py +40 -5
- nautobot/core/testing/filters.py +37 -21
- nautobot/core/testing/mixins.py +1 -1
- nautobot/core/testing/views.py +27 -4
- nautobot/core/tests/test_tables.py +43 -6
- nautobot/core/tests/test_templatetags_ui_framework.py +146 -0
- nautobot/core/tests/test_titles.py +2 -2
- nautobot/core/tests/test_ui.py +14 -1
- nautobot/core/tests/test_views.py +45 -0
- nautobot/core/ui/breadcrumbs.py +13 -8
- nautobot/core/ui/bulk_buttons.py +53 -53
- nautobot/core/ui/object_detail.py +52 -9
- nautobot/core/ui/titles.py +9 -5
- nautobot/core/utils/data.py +13 -0
- nautobot/core/utils/deprecation.py +2 -0
- nautobot/core/views/__init__.py +24 -3
- nautobot/core/views/generic.py +42 -17
- nautobot/core/views/mixins.py +146 -12
- nautobot/core/views/utils.py +117 -0
- nautobot/dcim/migrations/0073_alter_powerport_power_factor_and_more.py +41 -0
- nautobot/dcim/models/device_component_templates.py +4 -2
- nautobot/dcim/models/device_components.py +3 -2
- nautobot/dcim/models/devices.py +4 -0
- nautobot/dcim/tables/__init__.py +2 -0
- nautobot/dcim/tables/devices.py +24 -0
- nautobot/dcim/tables/power.py +2 -2
- nautobot/dcim/templates/dcim/device/base.html +1 -11
- nautobot/dcim/templates/dcim/device_component.html +0 -19
- nautobot/dcim/templates/dcim/modulebay_retrieve.html +0 -16
- nautobot/dcim/templates/dcim/rack_elevation_list.html +4 -4
- nautobot/dcim/templates/dcim/virtualchassis_retrieve.html +1 -50
- nautobot/dcim/tests/test_views.py +41 -0
- nautobot/dcim/views.py +169 -39
- nautobot/extras/filters/mixins.py +1 -1
- nautobot/extras/forms/forms.py +15 -0
- nautobot/extras/models/customfields.py +45 -9
- nautobot/extras/models/groups.py +10 -1
- nautobot/extras/models/jobs.py +2 -2
- nautobot/extras/plugins/views.py +18 -5
- nautobot/extras/tables.py +4 -2
- nautobot/extras/templates/extras/configcontext_retrieve.html +1 -1
- nautobot/extras/templates/extras/configcontext_update.html +49 -49
- nautobot/extras/templates/extras/configcontextschema_retrieve.html +47 -47
- nautobot/extras/templates/extras/configcontextschema_update.html +18 -18
- nautobot/extras/templates/extras/customfield_retrieve.html +1 -128
- nautobot/extras/templates/extras/dynamicgroup.html +2 -99
- nautobot/extras/templates/extras/dynamicgroup_edit.html +2 -199
- nautobot/extras/templates/extras/dynamicgroup_retrieve.html +99 -0
- nautobot/extras/templates/extras/dynamicgroup_update.html +199 -0
- nautobot/extras/templates/extras/gitrepository.html +2 -82
- nautobot/extras/templates/extras/gitrepository_object_edit.html +2 -13
- nautobot/extras/templates/extras/gitrepository_retrieve.html +82 -0
- nautobot/extras/templates/extras/gitrepository_update.html +13 -0
- nautobot/extras/templates/extras/inc/job_table.html +1 -1
- nautobot/extras/templates/extras/inc/object_contact_header.html +2 -2
- nautobot/extras/templates/extras/note_retrieve.html +1 -53
- nautobot/extras/templates/extras/plugin_detail.html +3 -7
- nautobot/extras/templates/extras/plugins_list.html +0 -2
- nautobot/extras/templates/extras/tag_retrieve.html +1 -1
- nautobot/extras/templates/extras/tag_update.html +14 -14
- nautobot/extras/templates/extras/team_retrieve.html +1 -1
- nautobot/extras/tests/test_dynamicgroups.py +73 -18
- nautobot/extras/tests/test_models.py +216 -0
- nautobot/extras/tests/test_views.py +7 -2
- nautobot/extras/urls.py +2 -94
- nautobot/extras/views.py +425 -430
- nautobot/ipam/apps.py +1 -0
- nautobot/ipam/jobs/__init__.py +10 -0
- nautobot/ipam/jobs/cleanup.py +296 -0
- nautobot/ipam/models.py +301 -178
- nautobot/ipam/querysets.py +3 -3
- nautobot/ipam/signals.py +6 -1
- nautobot/ipam/templates/ipam/inc/ipadress_edit_header.html +3 -3
- nautobot/ipam/templates/ipam/inc/toggle_available.html +2 -2
- nautobot/ipam/templates/ipam/ipaddress_assign.html +1 -1
- nautobot/ipam/templates/ipam/prefix.html +0 -8
- nautobot/ipam/templates/ipam/prefix_list.html +1 -1
- nautobot/ipam/templates/ipam/vlan_retrieve.html +1 -77
- nautobot/ipam/tests/test_api.py +5 -0
- nautobot/ipam/tests/test_jobs.py +454 -0
- nautobot/ipam/tests/test_models.py +677 -122
- nautobot/ipam/tests/test_querysets.py +46 -0
- nautobot/ipam/tests/test_views.py +40 -164
- nautobot/ipam/urls.py +0 -11
- nautobot/ipam/utils/migrations.py +1 -1
- nautobot/ipam/utils/testing.py +9 -4
- nautobot/ipam/views.py +175 -235
- nautobot/project-static/docs/404.html +9 -6
- nautobot/project-static/docs/apps/index.html +9 -6
- nautobot/project-static/docs/apps/nautobot-apps.html +9 -6
- nautobot/project-static/docs/assets/javascripts/bundle.92b07e13.min.js +16 -0
- nautobot/project-static/docs/assets/javascripts/{bundle.50899def.min.js.map → bundle.92b07e13.min.js.map} +2 -2
- nautobot/project-static/docs/assets/javascripts/workers/{search.d50fe291.min.js → search.973d3a69.min.js} +4 -4
- nautobot/project-static/docs/assets/javascripts/workers/{search.d50fe291.min.js.map → search.973d3a69.min.js.map} +1 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/change_logging.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +10 -7
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/constants.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/events.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/exceptions.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/factory.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/graphql.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/jobs.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +11 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/querysets.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +11 -8
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +81 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +73 -18
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +69 -7
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +402 -21
- nautobot/project-static/docs/development/apps/api/configuration-view.html +13 -10
- nautobot/project-static/docs/development/apps/api/database-backend-config.html +11 -8
- nautobot/project-static/docs/development/apps/api/models/django-admin.html +13 -10
- nautobot/project-static/docs/development/apps/api/models/global-search.html +10 -7
- nautobot/project-static/docs/development/apps/api/models/graphql.html +18 -15
- nautobot/project-static/docs/development/apps/api/models/index.html +14 -11
- nautobot/project-static/docs/development/apps/api/nautobot-app-config.html +12 -9
- nautobot/project-static/docs/development/apps/api/platform-features/custom-validators.html +15 -12
- nautobot/project-static/docs/development/apps/api/platform-features/filter-extensions.html +9 -6
- nautobot/project-static/docs/development/apps/api/platform-features/git-repository-content.html +15 -12
- nautobot/project-static/docs/development/apps/api/platform-features/index.html +9 -6
- nautobot/project-static/docs/development/apps/api/platform-features/jinja2-filters.html +11 -8
- nautobot/project-static/docs/development/apps/api/platform-features/jobs.html +16 -13
- nautobot/project-static/docs/development/apps/api/platform-features/populating-extensibility-features.html +12 -10305
- nautobot/project-static/docs/development/apps/api/platform-features/prepopulating-data.html +10722 -0
- nautobot/project-static/docs/development/apps/api/platform-features/secrets-providers.html +15 -12
- nautobot/project-static/docs/development/apps/api/platform-features/table-extensions.html +14 -11
- nautobot/project-static/docs/development/apps/api/platform-features/uniquely-identify-objects.html +9 -6
- nautobot/project-static/docs/development/apps/api/prometheus.html +15 -12
- nautobot/project-static/docs/development/apps/api/setup.html +9 -6
- nautobot/project-static/docs/development/apps/api/testing.html +9 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/banners.html +12 -9
- nautobot/project-static/docs/development/apps/api/ui-extensions/home-page.html +9 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/index.html +9 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/navigation.html +9 -6
- nautobot/project-static/docs/development/apps/api/ui-extensions/object-views.html +20 -17
- nautobot/project-static/docs/development/apps/api/views/base-template.html +9 -6
- nautobot/project-static/docs/development/apps/api/views/core-view-overrides.html +15 -12
- nautobot/project-static/docs/development/apps/api/views/django-generic-views.html +14 -11
- nautobot/project-static/docs/development/apps/api/views/help-documentation.html +9 -6
- nautobot/project-static/docs/development/apps/api/views/index.html +9 -6
- nautobot/project-static/docs/development/apps/api/views/nautobot-generic-views.html +10 -7
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewset.html +24 -21
- nautobot/project-static/docs/development/apps/api/views/nautobotuiviewsetrouter.html +12 -9
- nautobot/project-static/docs/development/apps/api/views/notes.html +10 -7
- nautobot/project-static/docs/development/apps/api/views/rest-api.html +19 -16
- nautobot/project-static/docs/development/apps/api/views/urls.html +11 -8
- nautobot/project-static/docs/development/apps/index.html +9 -6
- nautobot/project-static/docs/development/apps/migration/code-updates.html +19 -16
- nautobot/project-static/docs/development/apps/migration/dependency-updates.html +9 -6
- nautobot/project-static/docs/development/apps/migration/from-v1.html +9 -6
- nautobot/project-static/docs/development/apps/migration/model-updates/dcim.html +22 -19
- nautobot/project-static/docs/development/apps/migration/model-updates/extras.html +9 -6
- nautobot/project-static/docs/development/apps/migration/model-updates/global.html +9 -6
- nautobot/project-static/docs/development/apps/migration/model-updates/ipam.html +9 -6
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/best-practices.html +9 -6
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/breadcrumbs-titles.html +14 -11
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/custom-content.html +27 -24
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/index.html +20 -17
- nautobot/project-static/docs/development/apps/migration/ui-component-framework/migration-steps.html +20 -17
- nautobot/project-static/docs/development/apps/porting-from-netbox.html +9 -6
- nautobot/project-static/docs/development/core/application-registry.html +23 -20
- nautobot/project-static/docs/development/core/best-practices.html +23 -20
- nautobot/project-static/docs/development/core/bootstrap-ui.html +9 -6
- nautobot/project-static/docs/development/core/caching.html +9 -6
- nautobot/project-static/docs/development/core/controllers.html +9 -6
- nautobot/project-static/docs/development/core/docker-compose-advanced-use-cases.html +10 -7
- nautobot/project-static/docs/development/core/generic-views.html +9 -6
- nautobot/project-static/docs/development/core/getting-started.html +9 -21
- nautobot/project-static/docs/development/core/homepage.html +12 -9
- nautobot/project-static/docs/development/core/index.html +9 -6
- nautobot/project-static/docs/development/core/minikube-dev-environment-for-k8s-jobs.html +9 -6
- nautobot/project-static/docs/development/core/model-checklist.html +9 -6
- nautobot/project-static/docs/development/core/model-features.html +11 -8
- nautobot/project-static/docs/development/core/natural-keys.html +21 -18
- nautobot/project-static/docs/development/core/navigation-menu.html +10 -7
- nautobot/project-static/docs/development/core/release-checklist.html +9 -6
- nautobot/project-static/docs/development/core/role-internals.html +9 -6
- nautobot/project-static/docs/development/core/settings.html +9 -6
- nautobot/project-static/docs/development/core/style-guide.html +32 -29
- nautobot/project-static/docs/development/core/templates.html +9 -6
- nautobot/project-static/docs/development/core/testing.html +10 -7
- nautobot/project-static/docs/development/core/ui-component-framework.html +42 -44
- nautobot/project-static/docs/development/core/user-preferences.html +9 -6
- nautobot/project-static/docs/development/index.html +9 -6
- nautobot/project-static/docs/development/jobs/getting-started.html +13 -10
- nautobot/project-static/docs/development/jobs/index.html +9 -6
- nautobot/project-static/docs/development/jobs/installation.html +23 -20
- nautobot/project-static/docs/development/jobs/job-extensions.html +25 -22
- nautobot/project-static/docs/development/jobs/job-logging.html +12 -9
- nautobot/project-static/docs/development/jobs/job-patterns.html +45 -42
- nautobot/project-static/docs/development/jobs/job-structure.html +53 -50
- nautobot/project-static/docs/development/jobs/migration/from-v1.html +23 -20
- nautobot/project-static/docs/development/jobs/testing.html +14 -11
- nautobot/project-static/docs/index.html +9 -6
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/overview/application_stack.html +9 -6
- nautobot/project-static/docs/overview/design_philosophy.html +9 -6
- nautobot/project-static/docs/release-notes/index.html +9 -6
- nautobot/project-static/docs/release-notes/version-1.0.html +9 -6
- nautobot/project-static/docs/release-notes/version-1.1.html +9 -6
- nautobot/project-static/docs/release-notes/version-1.2.html +10 -7
- nautobot/project-static/docs/release-notes/version-1.3.html +9 -6
- nautobot/project-static/docs/release-notes/version-1.4.html +9 -6
- nautobot/project-static/docs/release-notes/version-1.5.html +13 -10
- nautobot/project-static/docs/release-notes/version-1.6.html +9 -6
- nautobot/project-static/docs/release-notes/version-2.0.html +9 -6
- nautobot/project-static/docs/release-notes/version-2.1.html +9 -6
- nautobot/project-static/docs/release-notes/version-2.2.html +9 -6
- nautobot/project-static/docs/release-notes/version-2.3.html +9 -6
- nautobot/project-static/docs/release-notes/version-2.4.html +489 -6
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +301 -301
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/ldap.html +15 -12
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/remote.html +9 -6
- nautobot/project-static/docs/user-guide/administration/configuration/authentication/sso.html +16 -13
- nautobot/project-static/docs/user-guide/administration/configuration/index.html +9 -6
- nautobot/project-static/docs/user-guide/administration/configuration/redis.html +9 -6
- nautobot/project-static/docs/user-guide/administration/configuration/settings.html +38 -8
- nautobot/project-static/docs/user-guide/administration/configuration/time-zones.html +9 -6
- nautobot/project-static/docs/user-guide/administration/guides/celery-queues.html +9 -6
- nautobot/project-static/docs/user-guide/administration/guides/docker.html +9 -6
- nautobot/project-static/docs/user-guide/administration/guides/health-checks.html +9 -6
- nautobot/project-static/docs/user-guide/administration/guides/permissions.html +9 -6
- nautobot/project-static/docs/user-guide/administration/guides/prometheus-metrics.html +9 -6
- nautobot/project-static/docs/user-guide/administration/guides/replicating-nautobot.html +9 -6
- nautobot/project-static/docs/user-guide/administration/guides/request-profiling.html +9 -6
- nautobot/project-static/docs/user-guide/administration/guides/s3-django-storage.html +16 -13
- nautobot/project-static/docs/user-guide/administration/guides/selinux-troubleshooting.html +9 -6
- nautobot/project-static/docs/user-guide/administration/installation/app-install.html +9 -6
- nautobot/project-static/docs/user-guide/administration/installation/external-authentication.html +9 -6
- nautobot/project-static/docs/user-guide/administration/installation/http-server.html +9 -6
- nautobot/project-static/docs/user-guide/administration/installation/index.html +9 -6
- nautobot/project-static/docs/user-guide/administration/installation/install_system.html +9 -6
- nautobot/project-static/docs/user-guide/administration/installation/nautobot.html +9 -6
- nautobot/project-static/docs/user-guide/administration/installation/services.html +12 -9
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-netbox.html +13 -10
- nautobot/project-static/docs/user-guide/administration/migration/migrating-from-postgresql.html +10 -7
- nautobot/project-static/docs/user-guide/administration/security/index.html +9 -6
- nautobot/project-static/docs/user-guide/administration/security/notices.html +9 -6
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-server.html +9 -6
- nautobot/project-static/docs/user-guide/administration/tools/nautobot-shell.html +10 -7
- nautobot/project-static/docs/user-guide/administration/upgrading/database-backup.html +9 -6
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/after-you-upgrade.html +9 -6
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/before-you-upgrade.html +9 -6
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/for-developers.html +9 -6
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/index.html +9 -6
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/ipam/whats-changed.html +9 -6
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/region-and-site-data-migration-guide.html +9 -6
- nautobot/project-static/docs/user-guide/administration/upgrading/from-v1/upgrading-from-nautobot-v1.html +15 -12
- nautobot/project-static/docs/user-guide/administration/upgrading/upgrading.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuit.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittermination.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/circuits/circuittype.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/circuits/provider.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/circuits/providernetwork.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloud.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudaccount.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetwork.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudnetworkprefixassignment.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudresourcetype.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservice.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/cloud/cloudservicenetworkassignment.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/cable.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleport.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleporttemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverport.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/consoleserverporttemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controller.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/controllermanageddevicegroup.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/device.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebay.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicebaytemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicefamily.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/deviceredundancygroup.html +13 -10
- nautobot/project-static/docs/user-guide/core-data-model/dcim/devicetype.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontport.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/frontporttemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interface.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfaceredundancygroup.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/interfacetemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/inventoryitem.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/location.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/locationtype.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/manufacturer.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/module.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebay.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulebaytemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/modulefamily.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/moduletype.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/platform.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerfeed.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlet.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/poweroutlettemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerpanel.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerport.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/powerporttemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rack.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackgroup.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rackreservation.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearport.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/rearporttemplate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareimagefile.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/softwareversion.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualchassis.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/dcim/virtualdevicecontext.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontext.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/configcontextschema.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/extras/contact.html +11 -8
- nautobot/project-static/docs/user-guide/core-data-model/extras/team.html +11 -8
- nautobot/project-static/docs/user-guide/core-data-model/ipam/ipaddress.html +41 -41
- nautobot/project-static/docs/user-guide/core-data-model/ipam/namespace.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/ipam/prefix.html +197 -54
- nautobot/project-static/docs/user-guide/core-data-model/ipam/rir.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/ipam/routetarget.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/ipam/service.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlan.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vlangroup.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/ipam/vrf.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/overview/introduction.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenant.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/tenancy/tenantgroup.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/cluster.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustergroup.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/clustertype.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/virtualmachine.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/virtualization/vminterface.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/wireless/index.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/wireless/radioprofile.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/wireless/supporteddatarate.html +9 -6
- nautobot/project-static/docs/user-guide/core-data-model/wireless/wirelessnetwork.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/contacts-and-teams.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/custom-fields.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-devices.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/creating-location-types-and-locations.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/index.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/interfaces.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/ipam.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/platforms.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/search-bar.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/tenants.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/getting-started/vlans-and-vlan-groups.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/git-data-source.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/graphql.html +13 -10
- nautobot/project-static/docs/user-guide/feature-guides/ip-address-merge-tool.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/relationships.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/software-image-files-and-versions.html +9 -6
- nautobot/project-static/docs/user-guide/feature-guides/wireless-networks-and-controllers.html +9 -6
- nautobot/project-static/docs/user-guide/index.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/change-logging.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/computedfield.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/customfield.html +10 -7
- nautobot/project-static/docs/user-guide/platform-functionality/customlink.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/dynamicgroup.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/events.html +11 -8
- nautobot/project-static/docs/user-guide/platform-functionality/exporttemplate.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/externalintegration.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/gitrepository.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/graphql.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/graphqlquery.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/imageattachment.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/index.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/job-scheduling-and-approvals.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobbutton.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobhook.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/jobqueue.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/kubernetes-job-support.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/managing-jobs.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/jobs/models.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/napalm.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/note.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/objectmetadata.html +12 -9
- nautobot/project-static/docs/user-guide/platform-functionality/relationship.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/rendering-jinja-templates.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/authentication.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/filtering.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/overview.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/rest-api/ui-related-endpoints.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/role.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/savedview.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/secret.html +11 -8
- nautobot/project-static/docs/user-guide/platform-functionality/staticgroupassociation.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/status.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/tag.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/template-filters.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/users/objectpermission.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/users/token.html +9 -6
- nautobot/project-static/docs/user-guide/platform-functionality/webhook.html +9 -6
- nautobot/project-static/fonts/UFL.txt +96 -96
- nautobot/project-static/img/nautobot_icon.svg +32 -34
- nautobot/project-static/js/forms.js +35 -2
- nautobot/project-static/js/table_sorting_indicator.js +0 -2
- nautobot/virtualization/filters.py +7 -0
- {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/METADATA +8 -8
- {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/RECORD +431 -421
- nautobot/core/templates/inc/breadcrumbs.html +0 -14
- nautobot/project-static/docs/assets/javascripts/bundle.50899def.min.js +0 -16
- nautobot/project-static/docs/requirements.txt +0 -14
- {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/NOTICE +0 -0
- {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/WHEEL +0 -0
- {nautobot-2.4.16.dist-info → nautobot-2.4.18.dist-info}/entry_points.txt +0 -0
|
@@ -284,6 +284,393 @@ class IPAddressToInterfaceTest(TestCase):
|
|
|
284
284
|
self.assertEqual(remaining_assignments.count(), 1)
|
|
285
285
|
self.assertIn(assignment_nested_module, remaining_assignments)
|
|
286
286
|
|
|
287
|
+
def test_ip_address_to_interface_delete_signal_no_save_when_device_has_no_primary_ips(self):
|
|
288
|
+
"""
|
|
289
|
+
Test that Device is not saved when removing an IP from an interface and the device
|
|
290
|
+
has no primary IPs assigned.
|
|
291
|
+
"""
|
|
292
|
+
# Create an IP address
|
|
293
|
+
ip_addr = IPAddress.objects.create(address="192.0.2.1/24", status=self.status, namespace=self.namespace)
|
|
294
|
+
|
|
295
|
+
# Assign IP to interface
|
|
296
|
+
assignment_device_int1 = IPAddressToInterface.objects.create(interface=self.test_int1, ip_address=ip_addr)
|
|
297
|
+
|
|
298
|
+
# Mock the device save method to verify it's not called
|
|
299
|
+
with patch.object(self.test_device, "save") as mock_save:
|
|
300
|
+
# Remove the IP assignment from interface - this should NOT trigger a device save
|
|
301
|
+
assignment_device_int1.delete()
|
|
302
|
+
|
|
303
|
+
# Assert save was not called since no primary IPs were affected
|
|
304
|
+
mock_save.assert_not_called()
|
|
305
|
+
|
|
306
|
+
def test_ip_address_to_interface_delete_signal_no_save_when_removing_non_primary_ip_from_device(self):
|
|
307
|
+
"""
|
|
308
|
+
Test that Device is not saved when removing an IP from an interface and the IP
|
|
309
|
+
is not the device's primary IP.
|
|
310
|
+
"""
|
|
311
|
+
# Test removing non-primary IPv4 assignment
|
|
312
|
+
with self.subTest("IPv4 non-primary IP removal"):
|
|
313
|
+
# Create IPv4 addresses
|
|
314
|
+
primary_ipv4_addr = IPAddress.objects.create(
|
|
315
|
+
address="192.0.2.1/24", status=self.status, namespace=self.namespace
|
|
316
|
+
)
|
|
317
|
+
other_ipv4_addr = IPAddress.objects.create(
|
|
318
|
+
address="192.0.2.2/24", status=self.status, namespace=self.namespace
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Assign primary IP to interface first (required before setting as primary)
|
|
322
|
+
IPAddressToInterface.objects.create(interface=self.test_int2, ip_address=primary_ipv4_addr)
|
|
323
|
+
|
|
324
|
+
# Set it as the device's primary IP
|
|
325
|
+
self.test_device.primary_ip4 = primary_ipv4_addr
|
|
326
|
+
self.test_device.save()
|
|
327
|
+
|
|
328
|
+
# Assign the other IP to a different interface
|
|
329
|
+
assignment_other_ipv4 = IPAddressToInterface.objects.create(
|
|
330
|
+
interface=self.test_int1, ip_address=other_ipv4_addr
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
with patch.object(self.test_device, "save") as mock_save:
|
|
334
|
+
assignment_other_ipv4.delete()
|
|
335
|
+
mock_save.assert_not_called()
|
|
336
|
+
|
|
337
|
+
# Test removing non-primary IPv6 assignment
|
|
338
|
+
with self.subTest("IPv6 non-primary IP removal"):
|
|
339
|
+
# Create IPv6 prefix first
|
|
340
|
+
Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
|
|
341
|
+
|
|
342
|
+
# Create IPv6 addresses
|
|
343
|
+
primary_ipv6_addr = IPAddress.objects.create(
|
|
344
|
+
address="2001:db8::1/64", status=self.status, namespace=self.namespace
|
|
345
|
+
)
|
|
346
|
+
other_ipv6_addr = IPAddress.objects.create(
|
|
347
|
+
address="2001:db8::2/64", status=self.status, namespace=self.namespace
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Assign primary IP to interface first (required before setting as primary)
|
|
351
|
+
IPAddressToInterface.objects.create(interface=self.test_int2, ip_address=primary_ipv6_addr)
|
|
352
|
+
|
|
353
|
+
# Set it as the device's primary IP
|
|
354
|
+
self.test_device.primary_ip6 = primary_ipv6_addr
|
|
355
|
+
self.test_device.save()
|
|
356
|
+
|
|
357
|
+
# Assign the other IP to a different interface
|
|
358
|
+
assignment_other_ipv6 = IPAddressToInterface.objects.create(
|
|
359
|
+
interface=self.test_int1, ip_address=other_ipv6_addr
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
with patch.object(self.test_device, "save") as mock_save:
|
|
363
|
+
assignment_other_ipv6.delete()
|
|
364
|
+
mock_save.assert_not_called()
|
|
365
|
+
|
|
366
|
+
def test_ip_address_to_interface_delete_signal_save_when_removing_primary_ip_from_device(self):
|
|
367
|
+
"""
|
|
368
|
+
Test that Device is saved when removing an IP from an interface and the IP is the device's
|
|
369
|
+
primary IP and no other assignments exist.
|
|
370
|
+
"""
|
|
371
|
+
# Test removing primary IPv4 assignment
|
|
372
|
+
with self.subTest("IPv4 primary IP removal"):
|
|
373
|
+
# Create primary IPv4 address
|
|
374
|
+
primary_ipv4_addr = IPAddress.objects.create(
|
|
375
|
+
address="192.0.2.1/24", status=self.status, namespace=self.namespace
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Assign primary IPv4 to an interface first
|
|
379
|
+
assignment_primary_ipv4 = IPAddressToInterface.objects.create(
|
|
380
|
+
interface=self.test_int1, ip_address=primary_ipv4_addr
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Set it as the device's primary IP
|
|
384
|
+
self.test_device.primary_ip4 = primary_ipv4_addr
|
|
385
|
+
self.test_device.save()
|
|
386
|
+
|
|
387
|
+
# Mock the device save method to verify it's called
|
|
388
|
+
with patch.object(self.test_device, "save") as mock_save:
|
|
389
|
+
# Remove the primary IP assignment - this SHOULD trigger a device save
|
|
390
|
+
assignment_primary_ipv4.delete()
|
|
391
|
+
|
|
392
|
+
# Assert save was called to nullify primary_ip4
|
|
393
|
+
mock_save.assert_called_once()
|
|
394
|
+
|
|
395
|
+
# Test removing primary IPv6 assignment
|
|
396
|
+
with self.subTest("IPv6 primary IP removal"):
|
|
397
|
+
# Create IPv6 prefix first
|
|
398
|
+
Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
|
|
399
|
+
|
|
400
|
+
# Create primary IPv6 address
|
|
401
|
+
primary_ipv6_addr = IPAddress.objects.create(
|
|
402
|
+
address="2001:db8::1/64", status=self.status, namespace=self.namespace
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
# Assign primary IPv6 to an interface first
|
|
406
|
+
assignment_primary_ipv6 = IPAddressToInterface.objects.create(
|
|
407
|
+
interface=self.test_int1, ip_address=primary_ipv6_addr
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Set it as the device's primary IP
|
|
411
|
+
self.test_device.primary_ip6 = primary_ipv6_addr
|
|
412
|
+
self.test_device.save()
|
|
413
|
+
|
|
414
|
+
# Mock the device save method to verify it's called
|
|
415
|
+
with patch.object(self.test_device, "save") as mock_save:
|
|
416
|
+
# Remove the primary IP assignment - this SHOULD trigger a device save
|
|
417
|
+
assignment_primary_ipv6.delete()
|
|
418
|
+
|
|
419
|
+
# Assert save was called to nullify primary_ip6
|
|
420
|
+
mock_save.assert_called_once()
|
|
421
|
+
|
|
422
|
+
def test_ip_address_to_interface_delete_signal_no_save_when_device_primary_ip_has_multiple_assignments(self):
|
|
423
|
+
"""
|
|
424
|
+
Test that Device is not saved when removing an IP from an interface and the IP is the device's
|
|
425
|
+
primary IP but is assigned to other interfaces on that device.
|
|
426
|
+
"""
|
|
427
|
+
# Test IPv4 primary IP with multiple assignments
|
|
428
|
+
with self.subTest("IPv4 primary IP with multiple assignments"):
|
|
429
|
+
# Create primary IPv4 address
|
|
430
|
+
primary_ipv4_addr = IPAddress.objects.create(
|
|
431
|
+
address="192.0.2.1/24", status=self.status, namespace=self.namespace
|
|
432
|
+
)
|
|
433
|
+
# Assign primary IPv4 to multiple interfaces
|
|
434
|
+
assignment_ipv4_int1 = IPAddressToInterface.objects.create(
|
|
435
|
+
interface=self.test_int1, ip_address=primary_ipv4_addr
|
|
436
|
+
)
|
|
437
|
+
IPAddressToInterface.objects.create(interface=self.test_int2, ip_address=primary_ipv4_addr)
|
|
438
|
+
|
|
439
|
+
# Set it as the device's primary IP
|
|
440
|
+
self.test_device.primary_ip4 = primary_ipv4_addr
|
|
441
|
+
self.test_device.save()
|
|
442
|
+
|
|
443
|
+
# Mock the device save method to verify it's not called
|
|
444
|
+
with patch.object(self.test_device, "save") as mock_save:
|
|
445
|
+
# Remove primary IP from one interface - should NOT trigger save since other assignment exists
|
|
446
|
+
assignment_ipv4_int1.delete()
|
|
447
|
+
|
|
448
|
+
# Assert save was not called since IP is still assigned to test_int2
|
|
449
|
+
mock_save.assert_not_called()
|
|
450
|
+
|
|
451
|
+
# Test IPv6 primary IP with multiple assignments
|
|
452
|
+
with self.subTest("IPv6 primary IP with multiple assignments"):
|
|
453
|
+
# Create IPv6 prefix first
|
|
454
|
+
Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
|
|
455
|
+
|
|
456
|
+
# Create primary IPv6 address
|
|
457
|
+
primary_ipv6_addr = IPAddress.objects.create(
|
|
458
|
+
address="2001:db8::1/64", status=self.status, namespace=self.namespace
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
# Assign primary IPv6 to multiple interfaces
|
|
462
|
+
assignment_ipv6_int1 = IPAddressToInterface.objects.create(
|
|
463
|
+
interface=self.test_int1, ip_address=primary_ipv6_addr
|
|
464
|
+
)
|
|
465
|
+
IPAddressToInterface.objects.create(interface=self.test_int2, ip_address=primary_ipv6_addr)
|
|
466
|
+
|
|
467
|
+
# Set it as the device's primary IP
|
|
468
|
+
self.test_device.primary_ip6 = primary_ipv6_addr
|
|
469
|
+
self.test_device.save()
|
|
470
|
+
|
|
471
|
+
# Mock the device save method to verify it's not called
|
|
472
|
+
with patch.object(self.test_device, "save") as mock_save:
|
|
473
|
+
# Remove primary IP from one interface - should NOT trigger save since other assignment exists
|
|
474
|
+
assignment_ipv6_int1.delete()
|
|
475
|
+
|
|
476
|
+
# Assert save was not called since IP is still assigned to test_int2
|
|
477
|
+
mock_save.assert_not_called()
|
|
478
|
+
|
|
479
|
+
def test_ip_address_to_interface_delete_signal_no_save_when_vm_has_no_primary_ips(self):
|
|
480
|
+
"""
|
|
481
|
+
Test that VM is not saved when removing an IP from a VM interface and the VM
|
|
482
|
+
has no primary IPs assigned.
|
|
483
|
+
"""
|
|
484
|
+
# Create an IP address
|
|
485
|
+
ip_addr = IPAddress.objects.create(address="192.0.2.1/24", status=self.status, namespace=self.namespace)
|
|
486
|
+
|
|
487
|
+
# VM has no primary IPs by default (both are None)
|
|
488
|
+
|
|
489
|
+
# Assign IP to VM interface
|
|
490
|
+
assignment_vm_int1 = IPAddressToInterface.objects.create(vm_interface=self.test_vmint1, ip_address=ip_addr)
|
|
491
|
+
|
|
492
|
+
# Mock the VM save method to verify it's not called
|
|
493
|
+
with patch.object(self.test_vm, "save") as mock_save:
|
|
494
|
+
# Remove the IP assignment from VM interface - this should NOT trigger a VM save
|
|
495
|
+
assignment_vm_int1.delete()
|
|
496
|
+
|
|
497
|
+
# Assert save was not called since no primary IPs were affected
|
|
498
|
+
mock_save.assert_not_called()
|
|
499
|
+
|
|
500
|
+
def test_ip_address_to_interface_delete_signal_no_save_when_removing_non_primary_ip_from_vm(self):
|
|
501
|
+
"""
|
|
502
|
+
Test that VM is not saved when removing an IP from a VM interface and the IP
|
|
503
|
+
is not the VM's primary IP.
|
|
504
|
+
"""
|
|
505
|
+
# Test removing non-primary IPv4 assignment
|
|
506
|
+
with self.subTest("IPv4 non-primary IP removal"):
|
|
507
|
+
# Create IPv4 addresses
|
|
508
|
+
primary_ipv4_addr = IPAddress.objects.create(
|
|
509
|
+
address="192.0.2.1/24", status=self.status, namespace=self.namespace
|
|
510
|
+
)
|
|
511
|
+
other_ipv4_addr = IPAddress.objects.create(
|
|
512
|
+
address="192.0.2.2/24", status=self.status, namespace=self.namespace
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Assign primary IP to VM interface first (required before setting as primary)
|
|
516
|
+
IPAddressToInterface.objects.create(vm_interface=self.test_vmint2, ip_address=primary_ipv4_addr)
|
|
517
|
+
|
|
518
|
+
# Set it as the VM's primary IP
|
|
519
|
+
self.test_vm.primary_ip4 = primary_ipv4_addr
|
|
520
|
+
self.test_vm.save()
|
|
521
|
+
|
|
522
|
+
# Assign the other IP to a different VM interface
|
|
523
|
+
assignment_vm_int1 = IPAddressToInterface.objects.create(
|
|
524
|
+
vm_interface=self.test_vmint1, ip_address=other_ipv4_addr
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
with patch.object(self.test_vm, "save") as mock_save:
|
|
528
|
+
assignment_vm_int1.delete()
|
|
529
|
+
mock_save.assert_not_called()
|
|
530
|
+
|
|
531
|
+
# Test removing non-primary IPv6 assignment
|
|
532
|
+
with self.subTest("IPv6 non-primary IP removal"):
|
|
533
|
+
# Create IPv6 prefix first
|
|
534
|
+
Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
|
|
535
|
+
|
|
536
|
+
# Create IPv6 addresses
|
|
537
|
+
primary_ipv6_addr = IPAddress.objects.create(
|
|
538
|
+
address="2001:db8::1/64", status=self.status, namespace=self.namespace
|
|
539
|
+
)
|
|
540
|
+
other_ipv6_addr = IPAddress.objects.create(
|
|
541
|
+
address="2001:db8::2/64", status=self.status, namespace=self.namespace
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# Assign primary IP to VM interface first (required before setting as primary)
|
|
545
|
+
IPAddressToInterface.objects.create(vm_interface=self.test_vmint2, ip_address=primary_ipv6_addr)
|
|
546
|
+
|
|
547
|
+
# Set it as the VM's primary IP
|
|
548
|
+
self.test_vm.primary_ip6 = primary_ipv6_addr
|
|
549
|
+
self.test_vm.save()
|
|
550
|
+
|
|
551
|
+
# Assign the other IP to a different VM interface
|
|
552
|
+
assignment_vm_int1 = IPAddressToInterface.objects.create(
|
|
553
|
+
vm_interface=self.test_vmint1, ip_address=other_ipv6_addr
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
with patch.object(self.test_vm, "save") as mock_save:
|
|
557
|
+
assignment_vm_int1.delete()
|
|
558
|
+
mock_save.assert_not_called()
|
|
559
|
+
|
|
560
|
+
def test_ip_address_to_interface_delete_signal_save_when_removing_primary_ip_from_vm(self):
|
|
561
|
+
"""
|
|
562
|
+
Test that VM is saved when removing an IP from a VM interface and the IP is the VM's
|
|
563
|
+
primary IP and no other assignments exist.
|
|
564
|
+
"""
|
|
565
|
+
# Test removing primary IPv4 assignment
|
|
566
|
+
with self.subTest("IPv4 primary IP removal"):
|
|
567
|
+
# Create primary IPv4 address
|
|
568
|
+
primary_ipv4_addr = IPAddress.objects.create(
|
|
569
|
+
address="192.0.2.1/24", status=self.status, namespace=self.namespace
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
# Assign primary IPv4 to VM interface first
|
|
573
|
+
assignment_primary_ipv4 = IPAddressToInterface.objects.create(
|
|
574
|
+
vm_interface=self.test_vmint1, ip_address=primary_ipv4_addr
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# Set it as the VM's primary IP
|
|
578
|
+
self.test_vm.primary_ip4 = primary_ipv4_addr
|
|
579
|
+
self.test_vm.save()
|
|
580
|
+
|
|
581
|
+
# Mock the VM save method to verify it's called
|
|
582
|
+
with patch.object(self.test_vm, "save") as mock_save:
|
|
583
|
+
# Remove the primary IP assignment - this SHOULD trigger a VM save
|
|
584
|
+
assignment_primary_ipv4.delete()
|
|
585
|
+
|
|
586
|
+
# Assert save was called to nullify primary_ip4
|
|
587
|
+
mock_save.assert_called_once()
|
|
588
|
+
|
|
589
|
+
# Test removing primary IPv6 assignment
|
|
590
|
+
with self.subTest("IPv6 primary IP removal"):
|
|
591
|
+
# Create IPv6 prefix first
|
|
592
|
+
Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
|
|
593
|
+
|
|
594
|
+
# Create primary IPv6 address
|
|
595
|
+
primary_ipv6_addr = IPAddress.objects.create(
|
|
596
|
+
address="2001:db8::1/64", status=self.status, namespace=self.namespace
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Assign primary IPv6 to VM interface first
|
|
600
|
+
assignment_primary_ipv6 = IPAddressToInterface.objects.create(
|
|
601
|
+
vm_interface=self.test_vmint1, ip_address=primary_ipv6_addr
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
# Set it as the VM's primary IP
|
|
605
|
+
self.test_vm.primary_ip6 = primary_ipv6_addr
|
|
606
|
+
self.test_vm.save()
|
|
607
|
+
|
|
608
|
+
# Mock the VM save method to verify it's called
|
|
609
|
+
with patch.object(self.test_vm, "save") as mock_save:
|
|
610
|
+
# Remove the primary IP assignment - this SHOULD trigger a VM save
|
|
611
|
+
assignment_primary_ipv6.delete()
|
|
612
|
+
|
|
613
|
+
# Assert save was called to nullify primary_ip6
|
|
614
|
+
mock_save.assert_called_once()
|
|
615
|
+
|
|
616
|
+
def test_ip_address_to_interface_delete_signal_no_save_when_vm_primary_ip_has_multiple_assignments(self):
|
|
617
|
+
"""
|
|
618
|
+
Test that VM is not saved when removing an IP from a VM interface and the IP is the VM's
|
|
619
|
+
primary IP but is assigned to other VM interfaces on that VM.
|
|
620
|
+
"""
|
|
621
|
+
# Test IPv4 primary IP with multiple assignments
|
|
622
|
+
with self.subTest("IPv4 primary IP with multiple assignments"):
|
|
623
|
+
# Create primary IPv4 address
|
|
624
|
+
primary_ipv4_addr = IPAddress.objects.create(
|
|
625
|
+
address="192.0.2.1/24", status=self.status, namespace=self.namespace
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
# Assign primary IPv4 to multiple VM interfaces
|
|
629
|
+
assignment_ipv4_vmint1 = IPAddressToInterface.objects.create(
|
|
630
|
+
vm_interface=self.test_vmint1, ip_address=primary_ipv4_addr
|
|
631
|
+
)
|
|
632
|
+
IPAddressToInterface.objects.create(vm_interface=self.test_vmint2, ip_address=primary_ipv4_addr)
|
|
633
|
+
|
|
634
|
+
# Set it as the VM's primary IP
|
|
635
|
+
self.test_vm.primary_ip4 = primary_ipv4_addr
|
|
636
|
+
self.test_vm.save()
|
|
637
|
+
|
|
638
|
+
# Mock the VM save method to verify it's not called
|
|
639
|
+
with patch.object(self.test_vm, "save") as mock_save:
|
|
640
|
+
# Remove primary IP from one VM interface - should NOT trigger save since other assignment exists
|
|
641
|
+
assignment_ipv4_vmint1.delete()
|
|
642
|
+
|
|
643
|
+
# Assert save was not called since IP is still assigned to test_vmint2
|
|
644
|
+
mock_save.assert_not_called()
|
|
645
|
+
|
|
646
|
+
# Test IPv6 primary IP with multiple assignments
|
|
647
|
+
with self.subTest("IPv6 primary IP with multiple assignments"):
|
|
648
|
+
# Create IPv6 prefix first
|
|
649
|
+
Prefix.objects.create(prefix="2001:db8::/64", status=self.status, namespace=self.namespace)
|
|
650
|
+
|
|
651
|
+
# Create primary IPv6 address
|
|
652
|
+
primary_ipv6_addr = IPAddress.objects.create(
|
|
653
|
+
address="2001:db8::1/64", status=self.status, namespace=self.namespace
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# Assign primary IPv6 to multiple VM interfaces
|
|
657
|
+
assignment_ipv6_vmint1 = IPAddressToInterface.objects.create(
|
|
658
|
+
vm_interface=self.test_vmint1, ip_address=primary_ipv6_addr
|
|
659
|
+
)
|
|
660
|
+
IPAddressToInterface.objects.create(vm_interface=self.test_vmint2, ip_address=primary_ipv6_addr)
|
|
661
|
+
|
|
662
|
+
# Set it as the VM's primary IP
|
|
663
|
+
self.test_vm.primary_ip6 = primary_ipv6_addr
|
|
664
|
+
self.test_vm.save()
|
|
665
|
+
|
|
666
|
+
# Mock the VM save method to verify it's not called
|
|
667
|
+
with patch.object(self.test_vm, "save") as mock_save:
|
|
668
|
+
# Remove primary IP from one VM interface - should NOT trigger save since other assignment exists
|
|
669
|
+
assignment_ipv6_vmint1.delete()
|
|
670
|
+
|
|
671
|
+
# Assert save was not called since IP is still assigned to test_vmint2
|
|
672
|
+
mock_save.assert_not_called()
|
|
673
|
+
|
|
287
674
|
|
|
288
675
|
class TestVarbinaryIPField(TestCase):
|
|
289
676
|
"""Tests for `nautobot.ipam.fields.VarbinaryIPField`."""
|
|
@@ -406,17 +793,20 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
406
793
|
self.status = self.statuses.first()
|
|
407
794
|
self.status.content_types.add(ContentType.objects.get_for_model(IPAddress))
|
|
408
795
|
self.root = Prefix.objects.create(
|
|
409
|
-
prefix="101.102.0.0/
|
|
796
|
+
prefix="101.102.0.0/16", status=self.status, namespace=self.namespace, type=PrefixTypeChoices.TYPE_CONTAINER
|
|
410
797
|
)
|
|
411
798
|
self.parent = Prefix.objects.create(
|
|
412
|
-
prefix="101.102.
|
|
799
|
+
prefix="101.102.103.0/24",
|
|
800
|
+
status=self.status,
|
|
801
|
+
namespace=self.namespace,
|
|
802
|
+
type=PrefixTypeChoices.TYPE_CONTAINER,
|
|
413
803
|
)
|
|
414
|
-
self.child1 = Prefix.objects.create(prefix="101.102.
|
|
415
|
-
self.child2 = Prefix.objects.create(prefix="101.102.
|
|
804
|
+
self.child1 = Prefix.objects.create(prefix="101.102.103.0/26", status=self.status, namespace=self.namespace)
|
|
805
|
+
self.child2 = Prefix.objects.create(prefix="101.102.103.104/32", status=self.status, namespace=self.namespace)
|
|
416
806
|
|
|
417
807
|
def test_parent_exists_after_model_clean(self):
|
|
418
808
|
prefix = Prefix(
|
|
419
|
-
prefix="101.102.0
|
|
809
|
+
prefix="101.102.1.0/24",
|
|
420
810
|
status=self.status,
|
|
421
811
|
namespace=self.namespace,
|
|
422
812
|
type=PrefixTypeChoices.TYPE_CONTAINER,
|
|
@@ -626,18 +1016,18 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
626
1016
|
# siblings()
|
|
627
1017
|
self.assertEqual(list(self.child1.siblings()), [self.child2])
|
|
628
1018
|
self.assertEqual(list(self.child1.siblings(include_self=True)), [self.child1, self.child2])
|
|
629
|
-
parent2 = Prefix.objects.create(prefix="101.102.0
|
|
1019
|
+
parent2 = Prefix.objects.create(prefix="101.102.128.0/24", status=self.status, namespace=self.namespace)
|
|
630
1020
|
self.assertEqual(list(self.parent.siblings()), [parent2])
|
|
631
1021
|
self.assertEqual(list(self.parent.siblings(include_self=True)), [self.parent, parent2])
|
|
632
1022
|
|
|
633
|
-
def
|
|
1023
|
+
def test_reparenting_on_create_and_delete(self):
|
|
634
1024
|
"""Test that reparenting algorithm works in its most basic form."""
|
|
635
1025
|
# tree hierarchy
|
|
636
1026
|
self.assertIsNone(self.root.parent)
|
|
637
1027
|
self.assertEqual(self.parent.parent, self.root)
|
|
638
1028
|
self.assertEqual(self.child1.parent, self.parent)
|
|
639
1029
|
|
|
640
|
-
# Delete the parent (/
|
|
1030
|
+
# Delete the parent (/24); child1/child2 now have root (/16) as their parent.
|
|
641
1031
|
num_deleted, _ = self.parent.delete()
|
|
642
1032
|
self.assertEqual(num_deleted, 1)
|
|
643
1033
|
|
|
@@ -648,9 +1038,14 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
648
1038
|
self.assertEqual(self.child2.parent, self.root)
|
|
649
1039
|
self.assertEqual(list(self.child1.ancestors()), [self.root])
|
|
650
1040
|
|
|
651
|
-
# Add /
|
|
652
|
-
# /
|
|
653
|
-
self.parent
|
|
1041
|
+
# Add /24 back in as a parent and assert that child1/child2 now have it as their parent, and
|
|
1042
|
+
# /16 is its parent.
|
|
1043
|
+
self.parent = Prefix.objects.create(
|
|
1044
|
+
prefix="101.102.103.0/24",
|
|
1045
|
+
status=self.status,
|
|
1046
|
+
namespace=self.namespace,
|
|
1047
|
+
type=PrefixTypeChoices.TYPE_CONTAINER,
|
|
1048
|
+
)
|
|
654
1049
|
self.child1.refresh_from_db()
|
|
655
1050
|
self.child2.refresh_from_db()
|
|
656
1051
|
self.assertEqual(self.child1.parent, self.parent)
|
|
@@ -687,13 +1082,266 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
687
1082
|
|
|
688
1083
|
# Add /25 back in as a parent and assert that child1/child2 now have it as their parent, and
|
|
689
1084
|
# /24 is its parent.
|
|
690
|
-
parent
|
|
1085
|
+
parent = Prefix.objects.create(
|
|
1086
|
+
prefix="101.102.0.0/25", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
|
|
1087
|
+
)
|
|
691
1088
|
child1.refresh_from_db()
|
|
692
1089
|
child2.refresh_from_db()
|
|
693
1090
|
self.assertEqual(child1.parent, parent)
|
|
694
1091
|
self.assertEqual(child2.parent, parent)
|
|
695
1092
|
self.assertEqual(list(child1.ancestors()), [root, parent])
|
|
696
1093
|
|
|
1094
|
+
def test_reparenting_on_field_updates(self):
|
|
1095
|
+
"""Test that reparenting occurs when network, prefix_length, etc. are updated."""
|
|
1096
|
+
self.assertIsNone(self.root.parent)
|
|
1097
|
+
self.assertEqual(self.parent.parent, self.root)
|
|
1098
|
+
self.assertEqual(self.child1.parent, self.parent)
|
|
1099
|
+
self.assertEqual(self.child2.parent, self.parent)
|
|
1100
|
+
|
|
1101
|
+
ip1 = IPAddress.objects.create(address="101.102.103.127/32", status=self.status, namespace=self.namespace)
|
|
1102
|
+
ip2 = IPAddress.objects.create(address="101.102.103.128/32", status=self.status, namespace=self.namespace)
|
|
1103
|
+
self.assertEqual(ip1.parent, self.parent)
|
|
1104
|
+
self.assertEqual(ip2.parent, self.parent)
|
|
1105
|
+
|
|
1106
|
+
with self.subTest("Decrease prefix_length, gaining children"):
|
|
1107
|
+
self.child1.prefix_length = 25
|
|
1108
|
+
self.child1.save()
|
|
1109
|
+
self.child1.refresh_from_db()
|
|
1110
|
+
self.child2.refresh_from_db()
|
|
1111
|
+
self.assertEqual(self.child2.parent, self.child1)
|
|
1112
|
+
ip1.refresh_from_db()
|
|
1113
|
+
self.assertEqual(ip1.parent, self.child1)
|
|
1114
|
+
|
|
1115
|
+
with self.subTest("Increase prefix_length, losing children"):
|
|
1116
|
+
self.child1.prefix_length = 26
|
|
1117
|
+
self.child1.save()
|
|
1118
|
+
self.child1.refresh_from_db()
|
|
1119
|
+
self.child2.refresh_from_db()
|
|
1120
|
+
self.assertEqual(self.child2.parent, self.parent)
|
|
1121
|
+
ip1.refresh_from_db()
|
|
1122
|
+
self.assertEqual(ip1.parent, self.parent)
|
|
1123
|
+
|
|
1124
|
+
with self.subTest("Broaden prefix, becoming parent of former parent"):
|
|
1125
|
+
self.parent.prefix = "101.0.0.0/8"
|
|
1126
|
+
self.parent.save()
|
|
1127
|
+
self.assertIsNone(self.parent.parent)
|
|
1128
|
+
# Former root is now a child of parent
|
|
1129
|
+
self.root.refresh_from_db()
|
|
1130
|
+
self.assertEqual(self.root.parent, self.parent)
|
|
1131
|
+
# Former children are now children of former root
|
|
1132
|
+
self.child1.refresh_from_db()
|
|
1133
|
+
self.assertEqual(self.child1.parent, self.root)
|
|
1134
|
+
self.child2.refresh_from_db()
|
|
1135
|
+
self.assertEqual(self.child2.parent, self.root)
|
|
1136
|
+
ip1.refresh_from_db()
|
|
1137
|
+
self.assertEqual(ip1.parent, self.root)
|
|
1138
|
+
ip2.refresh_from_db()
|
|
1139
|
+
self.assertEqual(ip2.parent, self.root)
|
|
1140
|
+
|
|
1141
|
+
with self.subTest("Narrow prefix, becoming child of former child"):
|
|
1142
|
+
self.parent.prefix = "101.102.103.0/24"
|
|
1143
|
+
self.parent.save()
|
|
1144
|
+
self.assertEqual(self.parent.parent, self.root)
|
|
1145
|
+
# Former root is now again root
|
|
1146
|
+
self.root.refresh_from_db()
|
|
1147
|
+
self.assertIsNone(self.root.parent)
|
|
1148
|
+
# Former children are again children of parent
|
|
1149
|
+
self.child1.refresh_from_db()
|
|
1150
|
+
self.assertEqual(self.child1.parent, self.parent)
|
|
1151
|
+
self.child2.refresh_from_db()
|
|
1152
|
+
self.assertEqual(self.child2.parent, self.parent)
|
|
1153
|
+
ip1.refresh_from_db()
|
|
1154
|
+
self.assertEqual(ip1.parent, self.parent)
|
|
1155
|
+
ip2.refresh_from_db()
|
|
1156
|
+
self.assertEqual(ip2.parent, self.parent)
|
|
1157
|
+
|
|
1158
|
+
with self.subTest("Change former root on multiple dimensions"):
|
|
1159
|
+
self.root.network = "101.102.103.0"
|
|
1160
|
+
self.root.prefix_length = 25
|
|
1161
|
+
self.root.save()
|
|
1162
|
+
self.assertEqual(self.root.parent, self.parent)
|
|
1163
|
+
self.parent.refresh_from_db()
|
|
1164
|
+
self.assertEqual(self.parent.parent, None)
|
|
1165
|
+
self.child1.refresh_from_db()
|
|
1166
|
+
self.assertEqual(self.child1.parent, self.root)
|
|
1167
|
+
self.child2.refresh_from_db()
|
|
1168
|
+
self.assertEqual(self.child2.parent, self.root)
|
|
1169
|
+
ip1.refresh_from_db()
|
|
1170
|
+
self.assertEqual(ip1.parent, self.root)
|
|
1171
|
+
ip2.refresh_from_db()
|
|
1172
|
+
self.assertEqual(ip2.parent, self.parent)
|
|
1173
|
+
|
|
1174
|
+
with self.subTest("Reclaim root position"):
|
|
1175
|
+
self.root.network = "101.0.0.0"
|
|
1176
|
+
self.root.prefix_length = 8
|
|
1177
|
+
self.root.save()
|
|
1178
|
+
self.assertIsNone(self.root.parent)
|
|
1179
|
+
self.parent.refresh_from_db()
|
|
1180
|
+
self.assertEqual(self.parent.parent, self.root)
|
|
1181
|
+
self.child1.refresh_from_db()
|
|
1182
|
+
self.assertEqual(self.child1.parent, self.parent)
|
|
1183
|
+
self.child2.refresh_from_db()
|
|
1184
|
+
self.assertEqual(self.child2.parent, self.parent)
|
|
1185
|
+
ip1.refresh_from_db()
|
|
1186
|
+
self.assertEqual(ip1.parent, self.parent)
|
|
1187
|
+
ip2.refresh_from_db()
|
|
1188
|
+
self.assertEqual(ip2.parent, self.parent)
|
|
1189
|
+
|
|
1190
|
+
def test_clean_fails_if_would_orphan_ips(self):
|
|
1191
|
+
"""Test that clean() fails if reparenting would orphan IPs."""
|
|
1192
|
+
self.ip = IPAddress.objects.create(address="101.102.1.1/32", status=self.status, namespace=self.namespace)
|
|
1193
|
+
self.assertEqual(self.ip.parent, self.root)
|
|
1194
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1195
|
+
self.root.prefix = "102.103.0.0/16"
|
|
1196
|
+
self.root.clean()
|
|
1197
|
+
self.assertIn(
|
|
1198
|
+
f"1 existing IP addresses (including {self.ip.host}) would no longer have a valid parent", str(cm.exception)
|
|
1199
|
+
)
|
|
1200
|
+
self.root.refresh_from_db()
|
|
1201
|
+
self.ip2 = IPAddress.objects.create(address="101.102.1.2/32", status=self.status, namespace=self.namespace)
|
|
1202
|
+
self.assertEqual(self.ip2.parent, self.root)
|
|
1203
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1204
|
+
self.root.prefix = "102.103.0.0/16"
|
|
1205
|
+
self.root.clean()
|
|
1206
|
+
self.assertIn(
|
|
1207
|
+
f"2 existing IP addresses (including {self.ip.host}) would no longer have a valid parent",
|
|
1208
|
+
str(cm.exception),
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
def test_clean_fails_if_namespace_changed_and_vrfs_involved(self):
|
|
1212
|
+
vrf = VRF.objects.create(name="VRF Red", namespace=self.namespace)
|
|
1213
|
+
vrf.add_prefix(self.root)
|
|
1214
|
+
|
|
1215
|
+
new_namespace = Namespace.objects.exclude(id=self.namespace.id).first()
|
|
1216
|
+
|
|
1217
|
+
self.root.namespace = new_namespace
|
|
1218
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1219
|
+
self.root.clean()
|
|
1220
|
+
self.assertIn("Cannot move to a different Namespace while associated to VRFs", str(cm.exception))
|
|
1221
|
+
|
|
1222
|
+
vrf.remove_prefix(self.root)
|
|
1223
|
+
self.root.clean()
|
|
1224
|
+
|
|
1225
|
+
vrf.add_prefix(self.parent)
|
|
1226
|
+
with self.assertRaises(ValidationError) as cm:
|
|
1227
|
+
self.root.clean()
|
|
1228
|
+
self.assertIn(
|
|
1229
|
+
"Cannot move to a different Namespace with descendant Prefixes associated to VRFs", str(cm.exception)
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
def test_namespace_change_success_updates_descendants_and_claims_new_children(self):
|
|
1233
|
+
new_namespace = Namespace.objects.exclude(id=self.namespace.id).first()
|
|
1234
|
+
new_catchall = Prefix.objects.create(prefix="0.0.0.0/0", status=self.status, namespace=new_namespace)
|
|
1235
|
+
new_parent = Prefix.objects.create(prefix="101.102.200.0/24", status=self.status, namespace=new_namespace)
|
|
1236
|
+
new_child = Prefix.objects.create(prefix="101.102.103.64/26", status=self.status, namespace=new_namespace)
|
|
1237
|
+
new_grandchild = Prefix.objects.create(prefix="101.102.103.0/27", status=self.status, namespace=new_namespace)
|
|
1238
|
+
new_ip = IPAddress.objects.create(address="101.102.150.200/32", status=self.status, namespace=new_namespace)
|
|
1239
|
+
|
|
1240
|
+
# Before:
|
|
1241
|
+
# self.namespace
|
|
1242
|
+
# self.root 101.102.0.0/16
|
|
1243
|
+
# self.parent 101.102.103.0/24
|
|
1244
|
+
# self.child1 101.102.103.0/26
|
|
1245
|
+
# self.child2 101.102.103.104/32
|
|
1246
|
+
# new_namespace
|
|
1247
|
+
# new_catchall 0.0.0.0/0
|
|
1248
|
+
# new_grandchild 101.102.103.0/27
|
|
1249
|
+
# new_child 101.102.103.64/26
|
|
1250
|
+
# new_ip 101.102.150.200/32
|
|
1251
|
+
# new_parent 101.102.200.0/24
|
|
1252
|
+
#
|
|
1253
|
+
# After:
|
|
1254
|
+
# new_namespace
|
|
1255
|
+
# new_catchall 0.0.0.0/0
|
|
1256
|
+
# self.root 101.102.0.0/16
|
|
1257
|
+
# self.parent 101.102.103.0/24
|
|
1258
|
+
# self.child1 101.102.103.0/26
|
|
1259
|
+
# new_grandchild 101.102.103.0/27
|
|
1260
|
+
# new_child 101.102.103.64/26
|
|
1261
|
+
# self.child2 101.102.103.104/32
|
|
1262
|
+
# new_ip 101.102.150.200/32
|
|
1263
|
+
# new_parent 101.102.200.0/24
|
|
1264
|
+
|
|
1265
|
+
self.root.namespace = new_namespace
|
|
1266
|
+
self.root.save()
|
|
1267
|
+
self.assertEqual(self.root.namespace, new_namespace)
|
|
1268
|
+
self.assertEqual(self.root.parent, new_catchall) # automatically updated
|
|
1269
|
+
self.parent.refresh_from_db()
|
|
1270
|
+
self.assertEqual(self.parent.namespace, new_namespace) # automatically updated
|
|
1271
|
+
self.assertEqual(self.parent.parent, self.root) # unchanged
|
|
1272
|
+
self.child1.refresh_from_db()
|
|
1273
|
+
self.assertEqual(self.child1.namespace, new_namespace) # automatically updated
|
|
1274
|
+
self.assertEqual(self.child1.parent, self.parent) # unchanged
|
|
1275
|
+
self.child2.refresh_from_db()
|
|
1276
|
+
self.assertEqual(self.child2.namespace, new_namespace) # automatically updated
|
|
1277
|
+
self.assertEqual(self.child2.parent, new_child) # automatically updated
|
|
1278
|
+
new_parent.refresh_from_db()
|
|
1279
|
+
self.assertEqual(new_parent.namespace, new_namespace) # unchanged
|
|
1280
|
+
self.assertEqual(new_parent.parent, self.root) # automatically updated
|
|
1281
|
+
new_child.refresh_from_db()
|
|
1282
|
+
self.assertEqual(new_child.namespace, new_namespace) # unchanged
|
|
1283
|
+
self.assertEqual(new_child.parent, self.parent) # automatically updated
|
|
1284
|
+
new_grandchild.refresh_from_db()
|
|
1285
|
+
self.assertEqual(new_grandchild.namespace, new_namespace) # unchanged
|
|
1286
|
+
self.assertEqual(new_grandchild.parent, self.child1) # automatically updated
|
|
1287
|
+
new_ip.refresh_from_db()
|
|
1288
|
+
self.assertEqual(new_ip.parent, self.root)
|
|
1289
|
+
|
|
1290
|
+
def test_namespace_change_results_in_merge_collisions(self):
|
|
1291
|
+
new_namespace = Namespace.objects.exclude(id=self.namespace.id).first()
|
|
1292
|
+
new_root = Prefix.objects.create(prefix="101.102.0.0/16", status=self.status, namespace=new_namespace)
|
|
1293
|
+
|
|
1294
|
+
self.root.namespace = new_namespace
|
|
1295
|
+
with self.assertRaises(IntegrityError):
|
|
1296
|
+
self.root.save()
|
|
1297
|
+
self.root.refresh_from_db()
|
|
1298
|
+
self.assertEqual(self.root.namespace, self.namespace)
|
|
1299
|
+
self.parent.refresh_from_db()
|
|
1300
|
+
self.assertEqual(self.parent.namespace, self.namespace)
|
|
1301
|
+
self.child1.refresh_from_db()
|
|
1302
|
+
self.assertEqual(self.child1.namespace, self.namespace)
|
|
1303
|
+
self.child2.refresh_from_db()
|
|
1304
|
+
self.assertEqual(self.child2.namespace, self.namespace)
|
|
1305
|
+
|
|
1306
|
+
new_root.delete()
|
|
1307
|
+
new_parent = Prefix.objects.create(prefix="101.102.103.0/24", status=self.status, namespace=new_namespace)
|
|
1308
|
+
|
|
1309
|
+
self.root.namespace = new_namespace
|
|
1310
|
+
with self.assertRaises(IntegrityError):
|
|
1311
|
+
self.root.save()
|
|
1312
|
+
self.root.refresh_from_db()
|
|
1313
|
+
self.assertEqual(self.root.namespace, self.namespace)
|
|
1314
|
+
self.parent.refresh_from_db()
|
|
1315
|
+
self.assertEqual(self.parent.namespace, self.namespace)
|
|
1316
|
+
self.child1.refresh_from_db()
|
|
1317
|
+
self.assertEqual(self.child1.namespace, self.namespace)
|
|
1318
|
+
self.child2.refresh_from_db()
|
|
1319
|
+
self.assertEqual(self.child2.namespace, self.namespace)
|
|
1320
|
+
|
|
1321
|
+
new_parent.delete()
|
|
1322
|
+
|
|
1323
|
+
existing_ip = IPAddress.objects.create(address="101.102.103.1/32", status=self.status, namespace=self.namespace)
|
|
1324
|
+
new_prefix = Prefix.objects.create(prefix="0.0.0.0/0", status=self.status, namespace=new_namespace)
|
|
1325
|
+
new_ip = IPAddress.objects.create(address="101.102.103.1/32", status=self.status, namespace=new_namespace)
|
|
1326
|
+
self.assertEqual(new_ip.parent, new_prefix)
|
|
1327
|
+
|
|
1328
|
+
self.root.namespace = new_namespace
|
|
1329
|
+
with self.assertRaises(IntegrityError):
|
|
1330
|
+
self.root.save()
|
|
1331
|
+
self.root.refresh_from_db()
|
|
1332
|
+
self.assertIsNone(self.root.parent)
|
|
1333
|
+
self.assertEqual(self.root.namespace, self.namespace)
|
|
1334
|
+
self.parent.refresh_from_db()
|
|
1335
|
+
self.assertEqual(self.parent.namespace, self.namespace)
|
|
1336
|
+
self.child1.refresh_from_db()
|
|
1337
|
+
self.assertEqual(self.child1.namespace, self.namespace)
|
|
1338
|
+
self.child2.refresh_from_db()
|
|
1339
|
+
self.assertEqual(self.child2.namespace, self.namespace)
|
|
1340
|
+
existing_ip.refresh_from_db()
|
|
1341
|
+
self.assertEqual(existing_ip.parent, self.child1)
|
|
1342
|
+
new_ip.refresh_from_db()
|
|
1343
|
+
self.assertEqual(new_ip.parent, new_prefix)
|
|
1344
|
+
|
|
697
1345
|
def test_descendants(self):
|
|
698
1346
|
prefixes = (
|
|
699
1347
|
Prefix.objects.create(
|
|
@@ -901,8 +1549,10 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
901
1549
|
self.assertEqual(slash25.get_utilization(), (4, 128))
|
|
902
1550
|
|
|
903
1551
|
# When the pool does not overlap with broadcast or network address, the denominator decrements by 2
|
|
904
|
-
pool.
|
|
905
|
-
pool.
|
|
1552
|
+
pool.delete()
|
|
1553
|
+
pool = Prefix.objects.create(
|
|
1554
|
+
prefix="10.0.0.132/30", type=PrefixTypeChoices.TYPE_POOL, status=self.status, namespace=self.namespace
|
|
1555
|
+
)
|
|
906
1556
|
self.assertEqual(slash25.get_utilization(), (4, 126))
|
|
907
1557
|
|
|
908
1558
|
# Further distinguishing between get_child_ips() and get_all_ips():
|
|
@@ -1026,86 +1676,10 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
1026
1676
|
Prefix.objects.create(
|
|
1027
1677
|
prefix="11.0.0.0/24", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
|
|
1028
1678
|
)
|
|
1029
|
-
# 3.0 TODO: replace with the commented below once type enforcement is enabled
|
|
1030
|
-
# pool_prefix = Prefix.objects.create(
|
|
1031
1679
|
Prefix.objects.create(
|
|
1032
1680
|
prefix="12.0.0.0/24", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
|
|
1033
1681
|
)
|
|
1034
1682
|
|
|
1035
|
-
# 3.0 TODO: uncomment the below tests once type enforcement is enabled
|
|
1036
|
-
|
|
1037
|
-
# with self.assertRaises(ValidationError, msg="Network prefix parent cannot be a network"):
|
|
1038
|
-
# Prefix.objects.create(
|
|
1039
|
-
# prefix="11.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
|
|
1040
|
-
# )
|
|
1041
|
-
|
|
1042
|
-
# with self.assertRaises(ValidationError, msg="Network prefix parent cannot be a pool"):
|
|
1043
|
-
# Prefix.objects.create(
|
|
1044
|
-
# prefix="12.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
|
|
1045
|
-
# )
|
|
1046
|
-
|
|
1047
|
-
# with self.assertRaises(ValidationError, msg="Container prefix parent cannot be a network"):
|
|
1048
|
-
# Prefix.objects.create(
|
|
1049
|
-
# prefix="11.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
|
|
1050
|
-
# )
|
|
1051
|
-
|
|
1052
|
-
# with self.assertRaises(ValidationError, msg="Container prefix parent cannot be a pool"):
|
|
1053
|
-
# Prefix.objects.create(
|
|
1054
|
-
# prefix="12.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
|
|
1055
|
-
# )
|
|
1056
|
-
|
|
1057
|
-
# with self.assertRaises(ValidationError, msg="Pool prefix parent cannot be a container"):
|
|
1058
|
-
# Prefix.objects.create(
|
|
1059
|
-
# prefix="10.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
|
|
1060
|
-
# )
|
|
1061
|
-
|
|
1062
|
-
# with self.assertRaises(ValidationError, msg="Pool prefix parent cannot be a pool"):
|
|
1063
|
-
# Prefix.objects.create(
|
|
1064
|
-
# prefix="12.0.0.0/30", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
|
|
1065
|
-
# )
|
|
1066
|
-
|
|
1067
|
-
# with self.assertRaises(
|
|
1068
|
-
# ValidationError, msg="Test that an invalid parent cannot be created (network parenting container)"
|
|
1069
|
-
# ):
|
|
1070
|
-
# Prefix.objects.create(
|
|
1071
|
-
# prefix="10.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
|
|
1072
|
-
# )
|
|
1073
|
-
|
|
1074
|
-
# with self.assertRaises(
|
|
1075
|
-
# ValidationError, msg="Test that an invalid parent cannot be created (pool parenting container)"
|
|
1076
|
-
# ):
|
|
1077
|
-
# Prefix.objects.create(
|
|
1078
|
-
# prefix="10.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
|
|
1079
|
-
# )
|
|
1080
|
-
|
|
1081
|
-
# with self.assertRaises(
|
|
1082
|
-
# ValidationError, msg="Test that an invalid parent cannot be created (network parenting network)"
|
|
1083
|
-
# ):
|
|
1084
|
-
# Prefix.objects.create(
|
|
1085
|
-
# prefix="11.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
|
|
1086
|
-
# )
|
|
1087
|
-
|
|
1088
|
-
# with self.assertRaises(
|
|
1089
|
-
# ValidationError, msg="Test that an invalid parent cannot be created (pool parenting network)"
|
|
1090
|
-
# ):
|
|
1091
|
-
# Prefix.objects.create(
|
|
1092
|
-
# prefix="11.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
|
|
1093
|
-
# )
|
|
1094
|
-
|
|
1095
|
-
# with self.assertRaises(
|
|
1096
|
-
# ValidationError, msg="Test that an invalid parent cannot be created (container parenting pool)"
|
|
1097
|
-
# ):
|
|
1098
|
-
# Prefix.objects.create(
|
|
1099
|
-
# prefix="12.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
|
|
1100
|
-
# )
|
|
1101
|
-
|
|
1102
|
-
# with self.assertRaises(
|
|
1103
|
-
# ValidationError, msg="Test that an invalid parent cannot be created (pool parenting pool)"
|
|
1104
|
-
# ):
|
|
1105
|
-
# Prefix.objects.create(
|
|
1106
|
-
# prefix="12.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
|
|
1107
|
-
# )
|
|
1108
|
-
|
|
1109
1683
|
with self.subTest("Test that valid parents can be created"):
|
|
1110
1684
|
Prefix.objects.create(
|
|
1111
1685
|
prefix="12.0.0.0/16", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_NETWORK
|
|
@@ -1125,14 +1699,6 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
1125
1699
|
prefix="10.0.0.0/26", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_CONTAINER
|
|
1126
1700
|
)
|
|
1127
1701
|
|
|
1128
|
-
# 3.0 TODO: uncomment once type enforcement is enabled
|
|
1129
|
-
# with self.assertRaises(
|
|
1130
|
-
# ValidationError,
|
|
1131
|
-
# msg="Test that modifying a prefix's type fails if it would result in an invalid parent/child relationship",
|
|
1132
|
-
# ):
|
|
1133
|
-
# pool_prefix.type = PrefixTypeChoices.TYPE_NETWORK
|
|
1134
|
-
# pool_prefix.validated_save()
|
|
1135
|
-
|
|
1136
1702
|
with self.subTest(
|
|
1137
1703
|
"Test that modifying a prefix's type is allowed if it does not create an invalid relationship"
|
|
1138
1704
|
):
|
|
@@ -1159,13 +1725,6 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
1159
1725
|
prefix="10.0.0.0/26", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
|
|
1160
1726
|
)
|
|
1161
1727
|
|
|
1162
|
-
# 3.0 TODO: uncomment once type enforcement is enabled
|
|
1163
|
-
# with self.assertRaises(
|
|
1164
|
-
# ProtectedError,
|
|
1165
|
-
# msg="Test that deleting a network prefix that would make a pool prefix's parent a container raises a ProtectedError",
|
|
1166
|
-
# ):
|
|
1167
|
-
# network.delete()
|
|
1168
|
-
|
|
1169
1728
|
with self.subTest("Test that deleting a parent prefix properly reparents the child prefixes"):
|
|
1170
1729
|
container.delete()
|
|
1171
1730
|
root.refresh_from_db()
|
|
@@ -1178,18 +1737,11 @@ class TestPrefix(ModelTestCases.BaseModelTestCase):
|
|
|
1178
1737
|
ip = IPAddress.objects.create(address="10.0.0.1/32", status=self.status, namespace=namespace)
|
|
1179
1738
|
|
|
1180
1739
|
with self.subTest("Test that deleting a pool prefix containing IPs succeeds"):
|
|
1181
|
-
self.assertEqual(ip.parent, pool)
|
|
1740
|
+
self.assertEqual(ip.parent, pool)
|
|
1182
1741
|
pool.delete()
|
|
1183
1742
|
ip.refresh_from_db()
|
|
1184
1743
|
self.assertEqual(ip.parent, network)
|
|
1185
1744
|
|
|
1186
|
-
# 3.0 TODO: uncomment once type enforcement is enabled
|
|
1187
|
-
# with self.assertRaises(
|
|
1188
|
-
# ProtectedError,
|
|
1189
|
-
# msg="Test that deleting a network prefix that would make an IP's parent a container raises a ProtectedError",
|
|
1190
|
-
# ):
|
|
1191
|
-
# network.delete()
|
|
1192
|
-
|
|
1193
1745
|
with self.subTest("Test that deleting the root prefix succeeds"):
|
|
1194
1746
|
root.delete()
|
|
1195
1747
|
network.refresh_from_db()
|
|
@@ -1433,17 +1985,11 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
|
|
|
1433
1985
|
prefix="12.0.0.0/24", status=self.status, namespace=namespace, type=PrefixTypeChoices.TYPE_POOL
|
|
1434
1986
|
)
|
|
1435
1987
|
|
|
1436
|
-
# 3.0 TODO: uncomment once type enforcement is enabled
|
|
1437
|
-
# with self.assertRaises(ValidationError, msg="IP Address parent cannot be a container"):
|
|
1438
|
-
# IPAddress.objects.create(address="10.0.0.1/32", status=self.status, namespace=namespace)
|
|
1439
|
-
|
|
1440
|
-
# with self.assertRaises(Prefix.DoesNotExist, msg="IP Address parent cannot be a pool"):
|
|
1441
|
-
# IPAddress.objects.create(address="12.0.0.1/32", status=self.status, namespace=namespace)
|
|
1442
|
-
|
|
1443
1988
|
with self.assertRaises(ValidationError) as err:
|
|
1444
1989
|
IPAddress.objects.create(address="13.0.0.1/32", status=self.status, namespace=namespace)
|
|
1445
1990
|
self.assertEqual(
|
|
1446
|
-
err.exception.message_dict["namespace"][0],
|
|
1991
|
+
err.exception.message_dict["namespace"][0],
|
|
1992
|
+
"No suitable parent Prefix for 13.0.0.1 exists in Namespace test_parenting_constraints",
|
|
1447
1993
|
)
|
|
1448
1994
|
|
|
1449
1995
|
with self.subTest("Test that IP address can be assigned to a valid parent"):
|
|
@@ -1483,7 +2029,7 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
|
|
|
1483
2029
|
self.assertIn("namespace", err.exception.message_dict)
|
|
1484
2030
|
self.assertEqual(
|
|
1485
2031
|
err.exception.message_dict["namespace"][0],
|
|
1486
|
-
"No suitable parent Prefix exists in
|
|
2032
|
+
"No suitable parent Prefix for 1976:2023::1 exists in Namespace Global",
|
|
1487
2033
|
)
|
|
1488
2034
|
|
|
1489
2035
|
# Appropriate parent exists in the default namespace --> no error
|
|
@@ -1523,6 +2069,15 @@ class TestIPAddress(ModelTestCases.BaseModelTestCase):
|
|
|
1523
2069
|
ip.validated_save()
|
|
1524
2070
|
self.assertEqual(ip.parent, prefixes[0])
|
|
1525
2071
|
|
|
2072
|
+
def test_change_host(self):
|
|
2073
|
+
ip = IPAddress.objects.create(address="192.0.2.1/32", status=self.status, namespace=self.namespace)
|
|
2074
|
+
self.assertEqual(ip.parent, self.prefix)
|
|
2075
|
+
|
|
2076
|
+
ip.host = "192.168.1.1"
|
|
2077
|
+
with self.assertRaises(ValidationError) as cm:
|
|
2078
|
+
ip.validated_save()
|
|
2079
|
+
self.assertIn("Host address cannot be changed once created", str(cm.exception))
|
|
2080
|
+
|
|
1526
2081
|
def test_varbinary_ip_fields_with_empty_values_do_not_violate_not_null_constrains(self):
|
|
1527
2082
|
# Assert that an error is triggered when the host is not provided.
|
|
1528
2083
|
# Initially, VarbinaryIPField fields with None values are stored as the binary representation of b'',
|