nautobot 2.4.16__py3-none-any.whl → 2.4.17__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/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/templates/components/panel/panel.html +1 -1
- nautobot/core/templates/inc/paginator.html +3 -3
- nautobot/core/templates/inc/table.html +2 -2
- nautobot/core/templatetags/helpers.py +80 -6
- nautobot/core/testing/mixins.py +1 -1
- nautobot/core/testing/views.py +2 -4
- nautobot/core/ui/bulk_buttons.py +53 -53
- nautobot/core/ui/object_detail.py +9 -4
- nautobot/core/utils/data.py +13 -0
- nautobot/core/utils/deprecation.py +2 -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/templates/dcim/rack_elevation_list.html +4 -4
- nautobot/dcim/views.py +9 -0
- nautobot/extras/models/customfields.py +45 -9
- 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/inc/job_table.html +1 -1
- nautobot/extras/templates/extras/inc/object_contact_header.html +2 -2
- nautobot/extras/templates/extras/note_retrieve.html +53 -53
- 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_models.py +216 -0
- nautobot/extras/tests/test_views.py +2 -2
- nautobot/extras/views.py +1 -0
- 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/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_list.html +1 -1
- nautobot/ipam/templates/ipam/vlan_retrieve.html +1 -77
- nautobot/ipam/tests/test_jobs.py +454 -0
- nautobot/ipam/tests/test_models.py +290 -122
- nautobot/ipam/tests/test_views.py +40 -164
- nautobot/ipam/urls.py +0 -11
- nautobot/ipam/utils/testing.py +9 -4
- nautobot/ipam/views.py +166 -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 +9 -6
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +28 -9
- 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 +9 -6
- 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 +11 -8
- 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 -6
- 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 +36 -33
- 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 +267 -6
- nautobot/project-static/docs/requirements.txt +2 -2
- 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 +11 -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/js/forms.js +35 -2
- nautobot/virtualization/filters.py +7 -0
- {nautobot-2.4.16.dist-info → nautobot-2.4.17.dist-info}/METADATA +6 -6
- {nautobot-2.4.16.dist-info → nautobot-2.4.17.dist-info}/RECORD +369 -364
- nautobot/project-static/docs/assets/javascripts/bundle.50899def.min.js +0 -16
- {nautobot-2.4.16.dist-info → nautobot-2.4.17.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.4.16.dist-info → nautobot-2.4.17.dist-info}/NOTICE +0 -0
- {nautobot-2.4.16.dist-info → nautobot-2.4.17.dist-info}/WHEEL +0 -0
- {nautobot-2.4.16.dist-info → nautobot-2.4.17.dist-info}/entry_points.txt +0 -0
|
@@ -153,6 +153,10 @@ class ComputedFieldTest(ModelTestCases.BaseModelTestCase):
|
|
|
153
153
|
secret_type=SecretsGroupSecretTypeChoices.TYPE_SECRET,
|
|
154
154
|
)
|
|
155
155
|
|
|
156
|
+
# Template strings for validation testing (cannot be saved due to syntax errors)
|
|
157
|
+
self.invalid_template_unclosed_bracket = "{{ obj.name }"
|
|
158
|
+
self.invalid_template_unknown_tag = "{% unknowntag %}{{ obj.name }}{% endunknowntag %}"
|
|
159
|
+
|
|
156
160
|
def test_render_method(self):
|
|
157
161
|
rendered_value = self.good_computed_field.render(context={"obj": self.location1})
|
|
158
162
|
self.assertEqual(rendered_value, f"{self.location1.name} is awesome!")
|
|
@@ -214,6 +218,218 @@ class ComputedFieldTest(ModelTestCases.BaseModelTestCase):
|
|
|
214
218
|
str(error.exception),
|
|
215
219
|
)
|
|
216
220
|
|
|
221
|
+
def test_template_validation_invalid_syntax(self):
|
|
222
|
+
"""
|
|
223
|
+
Test that ComputedField with invalid Jinja2 template syntax raises ValidationError.
|
|
224
|
+
"""
|
|
225
|
+
# Invalid template with syntax error - unclosed bracket
|
|
226
|
+
invalid_computed_field = ComputedField(
|
|
227
|
+
label="Invalid Template Test",
|
|
228
|
+
key="invalid_template_test",
|
|
229
|
+
template=self.invalid_template_unclosed_bracket,
|
|
230
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
with self.assertRaises(ValidationError) as context:
|
|
234
|
+
invalid_computed_field.full_clean()
|
|
235
|
+
|
|
236
|
+
# Check that the error message contains template-specific information
|
|
237
|
+
error_dict = context.exception.error_dict
|
|
238
|
+
self.assertIn("template", error_dict)
|
|
239
|
+
self.assertIn("Template syntax error", str(error_dict["template"][0]))
|
|
240
|
+
self.assertIn("line", str(error_dict["template"][0]))
|
|
241
|
+
|
|
242
|
+
def test_template_validation_invalid_tag(self):
|
|
243
|
+
"""
|
|
244
|
+
Test that ComputedField with invalid Jinja2 tag raises ValidationError.
|
|
245
|
+
"""
|
|
246
|
+
# Invalid template with unknown tag
|
|
247
|
+
invalid_computed_field = ComputedField(
|
|
248
|
+
label="Invalid Tag Test",
|
|
249
|
+
key="invalid_tag_test",
|
|
250
|
+
template=self.invalid_template_unknown_tag,
|
|
251
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
with self.assertRaises(ValidationError) as context:
|
|
255
|
+
invalid_computed_field.full_clean()
|
|
256
|
+
|
|
257
|
+
# Check that the error message contains template-specific information
|
|
258
|
+
error_dict = context.exception.error_dict
|
|
259
|
+
self.assertIn("template", error_dict)
|
|
260
|
+
self.assertIn("Template syntax error", str(error_dict["template"][0]))
|
|
261
|
+
|
|
262
|
+
def test_bulk_create_valid_templates(self):
|
|
263
|
+
"""Test that bulk_create works with valid templates."""
|
|
264
|
+
valid_fields = [
|
|
265
|
+
ComputedField(
|
|
266
|
+
label="Bulk Test 1",
|
|
267
|
+
key="bulk_test_1",
|
|
268
|
+
template="{{ obj.name }} - Test 1",
|
|
269
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
270
|
+
),
|
|
271
|
+
ComputedField(
|
|
272
|
+
label="Bulk Test 2",
|
|
273
|
+
key="bulk_test_2",
|
|
274
|
+
template="{{ obj.id }} - Test 2",
|
|
275
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
276
|
+
),
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
# Should not raise ValidationError
|
|
280
|
+
created_fields = ComputedField.objects.bulk_create(valid_fields)
|
|
281
|
+
self.assertEqual(len(created_fields), 2)
|
|
282
|
+
|
|
283
|
+
def test_bulk_create_invalid_templates(self):
|
|
284
|
+
"""Test that bulk_create fails with invalid templates and reports all errors."""
|
|
285
|
+
invalid_fields = [
|
|
286
|
+
ComputedField(
|
|
287
|
+
label="Invalid Test 1",
|
|
288
|
+
key="invalid_test_1",
|
|
289
|
+
template=self.invalid_template_unclosed_bracket,
|
|
290
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
291
|
+
),
|
|
292
|
+
ComputedField(
|
|
293
|
+
label="Invalid Test 2",
|
|
294
|
+
key="invalid_test_2",
|
|
295
|
+
template=self.invalid_template_unknown_tag,
|
|
296
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
297
|
+
),
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
with self.assertRaises(ValidationError) as context:
|
|
301
|
+
ComputedField.objects.bulk_create(invalid_fields)
|
|
302
|
+
|
|
303
|
+
# Check that both errors are reported
|
|
304
|
+
error_message = str(context.exception)
|
|
305
|
+
self.assertIn("Template validation failed", error_message)
|
|
306
|
+
self.assertIn("Invalid Test 1", error_message)
|
|
307
|
+
self.assertIn("Invalid Test 2", error_message)
|
|
308
|
+
|
|
309
|
+
def test_bulk_update_template_field(self):
|
|
310
|
+
"""Test that bulk_update validates templates when template field is updated."""
|
|
311
|
+
# Create valid objects first
|
|
312
|
+
valid_fields = [
|
|
313
|
+
ComputedField(
|
|
314
|
+
label="Update Test 1",
|
|
315
|
+
key="update_test_1",
|
|
316
|
+
template="{{ obj.name }}",
|
|
317
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
318
|
+
),
|
|
319
|
+
ComputedField(
|
|
320
|
+
label="Update Test 2",
|
|
321
|
+
key="update_test_2",
|
|
322
|
+
template="{{ obj.id }}",
|
|
323
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
324
|
+
),
|
|
325
|
+
]
|
|
326
|
+
created_fields = ComputedField.objects.bulk_create(valid_fields)
|
|
327
|
+
|
|
328
|
+
# Update with invalid templates
|
|
329
|
+
for field in created_fields:
|
|
330
|
+
field.template = self.invalid_template_unclosed_bracket
|
|
331
|
+
|
|
332
|
+
with self.assertRaises(ValidationError) as context:
|
|
333
|
+
ComputedField.objects.bulk_update(created_fields, ["template"])
|
|
334
|
+
|
|
335
|
+
# Check that validation error occurred
|
|
336
|
+
error_message = str(context.exception)
|
|
337
|
+
self.assertIn("Template validation failed", error_message)
|
|
338
|
+
self.assertIn("Update Test 1", error_message)
|
|
339
|
+
self.assertIn("Update Test 2", error_message)
|
|
340
|
+
|
|
341
|
+
def test_bulk_update_non_template_field(self):
|
|
342
|
+
"""Test that bulk_update skips template validation when template field is not updated."""
|
|
343
|
+
# Create a field with invalid template (bypassing validation for this test)
|
|
344
|
+
field = ComputedField(
|
|
345
|
+
label="Non-template Update Test",
|
|
346
|
+
key="non_template_update_test",
|
|
347
|
+
template="{{ obj.name }}", # Start with valid template
|
|
348
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
349
|
+
)
|
|
350
|
+
field.save()
|
|
351
|
+
|
|
352
|
+
# Manually set invalid template (simulating existing invalid data)
|
|
353
|
+
ComputedField.objects.filter(pk=field.pk).update(template=self.invalid_template_unclosed_bracket)
|
|
354
|
+
field.refresh_from_db()
|
|
355
|
+
|
|
356
|
+
# Update only the label field - should not trigger template validation
|
|
357
|
+
field.label = "Updated Label"
|
|
358
|
+
try:
|
|
359
|
+
ComputedField.objects.bulk_update([field], ["label"])
|
|
360
|
+
except ValidationError:
|
|
361
|
+
self.fail("bulk_update should not validate templates when template field is not being updated")
|
|
362
|
+
|
|
363
|
+
def test_bulk_create_mixed_valid_invalid(self):
|
|
364
|
+
"""Test that bulk_create fails when mixing valid and invalid templates."""
|
|
365
|
+
mixed_fields = [
|
|
366
|
+
ComputedField(
|
|
367
|
+
label="Valid Mixed Test",
|
|
368
|
+
key="valid_mixed_test",
|
|
369
|
+
template="{{ obj.name }} - Valid",
|
|
370
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
371
|
+
),
|
|
372
|
+
ComputedField(
|
|
373
|
+
label="Invalid Mixed Test",
|
|
374
|
+
key="invalid_mixed_test",
|
|
375
|
+
template=self.invalid_template_unclosed_bracket,
|
|
376
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
377
|
+
),
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
with self.assertRaises(ValidationError) as context:
|
|
381
|
+
ComputedField.objects.bulk_create(mixed_fields)
|
|
382
|
+
|
|
383
|
+
# Check that the invalid object is reported but valid one is not
|
|
384
|
+
error_message = str(context.exception)
|
|
385
|
+
self.assertIn("Template validation failed", error_message)
|
|
386
|
+
self.assertIn("Invalid Mixed Test", error_message)
|
|
387
|
+
# Valid object should not appear in error message
|
|
388
|
+
self.assertNotIn("Valid Mixed Test", error_message)
|
|
389
|
+
|
|
390
|
+
# Verify no objects were created (all-or-nothing behavior)
|
|
391
|
+
self.assertFalse(ComputedField.objects.filter(key="valid_mixed_test").exists())
|
|
392
|
+
self.assertFalse(ComputedField.objects.filter(key="invalid_mixed_test").exists())
|
|
393
|
+
|
|
394
|
+
def test_bulk_update_mixed_valid_invalid(self):
|
|
395
|
+
"""Test that bulk_update fails when mixing valid and invalid template updates."""
|
|
396
|
+
# Create valid objects first
|
|
397
|
+
valid_fields = [
|
|
398
|
+
ComputedField(
|
|
399
|
+
label="Valid Update Mixed",
|
|
400
|
+
key="valid_update_mixed",
|
|
401
|
+
template="{{ obj.name }}",
|
|
402
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
403
|
+
),
|
|
404
|
+
ComputedField(
|
|
405
|
+
label="Invalid Update Mixed",
|
|
406
|
+
key="invalid_update_mixed",
|
|
407
|
+
template="{{ obj.id }}",
|
|
408
|
+
content_type=ContentType.objects.get_for_model(Device),
|
|
409
|
+
),
|
|
410
|
+
]
|
|
411
|
+
created_fields = ComputedField.objects.bulk_create(valid_fields)
|
|
412
|
+
|
|
413
|
+
# Update: one with valid template, one with invalid
|
|
414
|
+
created_fields[0].template = "{{ obj.name }} - Updated Valid" # Valid
|
|
415
|
+
created_fields[1].template = self.invalid_template_unclosed_bracket # Invalid
|
|
416
|
+
|
|
417
|
+
with self.assertRaises(ValidationError) as context:
|
|
418
|
+
ComputedField.objects.bulk_update(created_fields, ["template"])
|
|
419
|
+
|
|
420
|
+
# Check that only the invalid object is reported
|
|
421
|
+
error_message = str(context.exception)
|
|
422
|
+
self.assertIn("Template validation failed", error_message)
|
|
423
|
+
self.assertIn("Invalid Update Mixed", error_message)
|
|
424
|
+
# Valid object should not appear in error message
|
|
425
|
+
self.assertNotIn("Valid Update Mixed", error_message)
|
|
426
|
+
|
|
427
|
+
# Verify templates were not updated (all-or-nothing behavior)
|
|
428
|
+
created_fields[0].refresh_from_db()
|
|
429
|
+
created_fields[1].refresh_from_db()
|
|
430
|
+
self.assertEqual(created_fields[0].template, "{{ obj.name }}") # Original template
|
|
431
|
+
self.assertEqual(created_fields[1].template, "{{ obj.id }}") # Original template
|
|
432
|
+
|
|
217
433
|
|
|
218
434
|
class ConfigContextTest(ModelTestCases.BaseModelTestCase):
|
|
219
435
|
"""
|
|
@@ -205,7 +205,7 @@ class ComputedFieldRenderingTestCase(TestCase):
|
|
|
205
205
|
def test_view_object_with_computed_field_fallback_value(self):
|
|
206
206
|
"""Ensure that the fallback_value is rendered if the template fails to render."""
|
|
207
207
|
# Make the template invalid to demonstrate the fallback value
|
|
208
|
-
self.computedfield.template = "FOO {{ obj
|
|
208
|
+
self.computedfield.template = "FOO {{ obj | invalid_filter }}"
|
|
209
209
|
self.computedfield.validated_save()
|
|
210
210
|
response = self.client.get(self.location_type.get_absolute_url(), follow=True)
|
|
211
211
|
self.assertEqual(response.status_code, 200)
|
|
@@ -224,7 +224,7 @@ class ComputedFieldRenderingTestCase(TestCase):
|
|
|
224
224
|
|
|
225
225
|
def test_view_object_with_computed_field_unsafe_fallback_value(self):
|
|
226
226
|
"""Ensure that computed field fallback values can't be used as an XSS vector."""
|
|
227
|
-
self.computedfield.template = "FOO {{ obj
|
|
227
|
+
self.computedfield.template = "FOO {{ obj | invalid_filter }}"
|
|
228
228
|
self.computedfield.fallback_value = '<script>alert("Hello world!"</script>'
|
|
229
229
|
self.computedfield.validated_save()
|
|
230
230
|
response = self.client.get(self.location_type.get_absolute_url(), follow=True)
|
nautobot/extras/views.py
CHANGED
nautobot/ipam/apps.py
CHANGED
|
@@ -19,6 +19,7 @@ class IPAMConfig(NautobotConfig):
|
|
|
19
19
|
from graphene_django.converter import convert_django_field, convert_field_to_string
|
|
20
20
|
|
|
21
21
|
from nautobot.ipam.fields import VarbinaryIPField
|
|
22
|
+
import nautobot.ipam.jobs
|
|
22
23
|
import nautobot.ipam.signals # noqa: F401 # unused-import -- but this import installs the signals
|
|
23
24
|
|
|
24
25
|
# Register VarbinaryIPField to be converted to a string type
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
from django.core.exceptions import PermissionDenied
|
|
2
|
+
from django.db import models
|
|
3
|
+
|
|
4
|
+
from nautobot.core.choices import ChoiceSet
|
|
5
|
+
from nautobot.extras.jobs import DryRunVar, IPNetworkVar, Job, MultiChoiceVar, ObjectVar
|
|
6
|
+
from nautobot.ipam.models import get_default_namespace_pk, IPAddress, Namespace, Prefix
|
|
7
|
+
|
|
8
|
+
name = "System Jobs"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CleanupTypes(ChoiceSet):
|
|
12
|
+
IPADDRESS = "ipam.IPAddress"
|
|
13
|
+
PREFIX = "ipam.Prefix"
|
|
14
|
+
|
|
15
|
+
CHOICES = (
|
|
16
|
+
(IPADDRESS, "IP addresses"),
|
|
17
|
+
(PREFIX, "Prefixes"),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FixIPAMParents(Job):
|
|
22
|
+
cleanup_types = MultiChoiceVar(
|
|
23
|
+
choices=CleanupTypes.CHOICES, required=True, default=[CleanupTypes.IPADDRESS, CleanupTypes.PREFIX]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
restrict_to_namespace = ObjectVar(
|
|
27
|
+
model=Namespace, required=False, description="Check only records within this namespace"
|
|
28
|
+
)
|
|
29
|
+
restrict_to_network = IPNetworkVar(required=False, description="Check only records within this network")
|
|
30
|
+
|
|
31
|
+
dryrun = DryRunVar()
|
|
32
|
+
|
|
33
|
+
class Meta:
|
|
34
|
+
name = "Check/Fix IPAM Parents"
|
|
35
|
+
description = "Check for and/or fix incorrect 'parent' values on IP Address and/or Prefix records."
|
|
36
|
+
has_sensitive_variables = False
|
|
37
|
+
|
|
38
|
+
def run( # pylint: disable=arguments-differ
|
|
39
|
+
self, *, cleanup_types, restrict_to_namespace=None, restrict_to_network=None, dryrun=False
|
|
40
|
+
):
|
|
41
|
+
all_relevant_prefixes = Prefix.objects.restrict(self.user, "change")
|
|
42
|
+
|
|
43
|
+
if restrict_to_namespace is not None:
|
|
44
|
+
self.logger.info(
|
|
45
|
+
"Inspecting only records in namespace %s",
|
|
46
|
+
restrict_to_namespace.name,
|
|
47
|
+
extra={"object": restrict_to_namespace},
|
|
48
|
+
)
|
|
49
|
+
all_relevant_prefixes = all_relevant_prefixes.filter(namespace=restrict_to_namespace)
|
|
50
|
+
if restrict_to_network is not None:
|
|
51
|
+
self.logger.info("Inspecting only records that fall within %s", restrict_to_network)
|
|
52
|
+
all_relevant_prefixes = all_relevant_prefixes.net_contained_or_equal(restrict_to_network)
|
|
53
|
+
|
|
54
|
+
if CleanupTypes.PREFIX in cleanup_types:
|
|
55
|
+
if not self.user.has_perm("ipam.change_prefix"):
|
|
56
|
+
self.fail('User "%s" does not have permission to update Prefix records', self.user.username)
|
|
57
|
+
raise PermissionDenied("User does not have update permission for Prefix records")
|
|
58
|
+
|
|
59
|
+
self.logger.info("Inspecting Prefix records...")
|
|
60
|
+
|
|
61
|
+
self.logger.debug("Beginning with a quick check for obviously wrong `parent` values...")
|
|
62
|
+
# 1. Obviously wrong Prefix parents
|
|
63
|
+
# - parent is set but has wrong IP version
|
|
64
|
+
# - parent is set but has wrong namespace
|
|
65
|
+
# - parent is set but its network/broadcast range doesn't contain the given subnet
|
|
66
|
+
prefixes_with_invalid_parents = (
|
|
67
|
+
all_relevant_prefixes.exclude(parent__ip_version=models.F("ip_version"))
|
|
68
|
+
| all_relevant_prefixes.exclude(parent__namespace_id=models.F("namespace_id"))
|
|
69
|
+
| all_relevant_prefixes.filter(parent__network__gt=models.F("network"))
|
|
70
|
+
| all_relevant_prefixes.filter(parent__broadcast__lt=models.F("broadcast"))
|
|
71
|
+
| all_relevant_prefixes.filter(parent__prefix_length__gte=models.F("prefix_length"))
|
|
72
|
+
).exclude(parent__isnull=True)
|
|
73
|
+
|
|
74
|
+
prefixes_with_invalid_parents = prefixes_with_invalid_parents.select_related("parent")
|
|
75
|
+
|
|
76
|
+
if prefixes_with_invalid_parents.exists():
|
|
77
|
+
fixed_prefixes = []
|
|
78
|
+
for pfx in prefixes_with_invalid_parents:
|
|
79
|
+
candidate_parents = Prefix.objects.all()
|
|
80
|
+
# Preserve namespace
|
|
81
|
+
candidate_parents = candidate_parents.filter(namespace_id=pfx.namespace_id)
|
|
82
|
+
try:
|
|
83
|
+
parent = candidate_parents.get_closest_parent(pfx.prefix, include_self=False)
|
|
84
|
+
except Prefix.DoesNotExist:
|
|
85
|
+
parent = None
|
|
86
|
+
self.logger.warning(
|
|
87
|
+
"Parent for %s should be corrected from %s to %s",
|
|
88
|
+
pfx.prefix,
|
|
89
|
+
pfx.parent.display if pfx.parent is not None else None,
|
|
90
|
+
parent.display if parent is not None else None,
|
|
91
|
+
extra={"object": pfx},
|
|
92
|
+
)
|
|
93
|
+
pfx.parent = parent
|
|
94
|
+
fixed_prefixes.append(pfx)
|
|
95
|
+
|
|
96
|
+
if dryrun:
|
|
97
|
+
self.logger.warning(
|
|
98
|
+
"Would correct invalid `parent` for %d Prefixes if this were not a dry-run", len(fixed_prefixes)
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
update_count = Prefix.objects.bulk_update(fixed_prefixes, ["parent"], batch_size=1000)
|
|
102
|
+
self.logger.success("Corrected invalid `parent` for %d Prefixes", update_count)
|
|
103
|
+
else:
|
|
104
|
+
self.logger.success("No Prefix records had clearly invalid `parent` values")
|
|
105
|
+
|
|
106
|
+
self.logger.debug("Continuing by checking Prefixes with null `parent` to make sure that's correct...")
|
|
107
|
+
# 2. parent is null but should not be
|
|
108
|
+
fixed_prefixes = []
|
|
109
|
+
processed_pfx_count = 0
|
|
110
|
+
for pfx in all_relevant_prefixes.filter(parent__isnull=True):
|
|
111
|
+
candidate_parents = Prefix.objects.all()
|
|
112
|
+
# Preserve namespace
|
|
113
|
+
candidate_parents = candidate_parents.filter(namespace_id=pfx.namespace_id)
|
|
114
|
+
try:
|
|
115
|
+
parent = candidate_parents.get_closest_parent(pfx.prefix, include_self=False)
|
|
116
|
+
except Prefix.DoesNotExist:
|
|
117
|
+
parent = None
|
|
118
|
+
|
|
119
|
+
if parent is not None:
|
|
120
|
+
self.logger.warning(
|
|
121
|
+
"Parent for %s should be set to %s instead of None",
|
|
122
|
+
pfx.display,
|
|
123
|
+
parent.prefix,
|
|
124
|
+
extra={"object": pfx},
|
|
125
|
+
)
|
|
126
|
+
pfx.parent = parent
|
|
127
|
+
fixed_prefixes.append(pfx)
|
|
128
|
+
processed_pfx_count += 1
|
|
129
|
+
|
|
130
|
+
self.logger.debug(
|
|
131
|
+
"Inspected %d Prefixes with null `parent` to see if an appropriate parent exists",
|
|
132
|
+
processed_pfx_count,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if fixed_prefixes:
|
|
136
|
+
if dryrun:
|
|
137
|
+
self.logger.warning(
|
|
138
|
+
"Would set a more precise `parent` for %d Prefixes if this were not a dry-run",
|
|
139
|
+
len(fixed_prefixes),
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
update_count = Prefix.objects.bulk_update(fixed_prefixes, ["parent"], batch_size=1000)
|
|
143
|
+
self.logger.success("Corrected imprecise `parent` for %d Prefixes", update_count)
|
|
144
|
+
|
|
145
|
+
self.logger.debug("Continuing with a more involved check for more subtly incorrect `parent` values...")
|
|
146
|
+
# 3. More subtly wrong Prefix parents
|
|
147
|
+
# - parent is set but a more specific parent Prefix also exists
|
|
148
|
+
fixed_prefixes = []
|
|
149
|
+
processed_pfx_count = 0
|
|
150
|
+
for pfx in all_relevant_prefixes.filter(
|
|
151
|
+
parent__prefix_length__lt=models.F("prefix_length") - 1
|
|
152
|
+
).select_related("parent"):
|
|
153
|
+
try:
|
|
154
|
+
parent = pfx.parent.subnets(include_self=True).get_closest_parent(pfx.prefix, include_self=False)
|
|
155
|
+
except Prefix.DoesNotExist:
|
|
156
|
+
parent = None
|
|
157
|
+
if parent != pfx.parent:
|
|
158
|
+
self.logger.warning(
|
|
159
|
+
"Parent for %s should be corrected from %s to %s",
|
|
160
|
+
pfx.display,
|
|
161
|
+
pfx.parent.prefix if pfx.parent else None,
|
|
162
|
+
parent.prefix if parent else None,
|
|
163
|
+
extra={"object": pfx},
|
|
164
|
+
)
|
|
165
|
+
pfx.parent = parent
|
|
166
|
+
fixed_prefixes.append(pfx)
|
|
167
|
+
processed_pfx_count += 1
|
|
168
|
+
|
|
169
|
+
self.logger.debug(
|
|
170
|
+
"Inspected %d Prefixes for more subtly incorrect `parent` values",
|
|
171
|
+
processed_pfx_count,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if fixed_prefixes:
|
|
175
|
+
if dryrun:
|
|
176
|
+
self.logger.warning(
|
|
177
|
+
"Would set a more precise `parent` for %d Prefixes if this were not a dry-run",
|
|
178
|
+
len(fixed_prefixes),
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
update_count = Prefix.objects.bulk_update(fixed_prefixes, ["parent"], batch_size=1000)
|
|
182
|
+
self.logger.success("Corrected imprecise `parent` for %d Prefixes", update_count)
|
|
183
|
+
|
|
184
|
+
if CleanupTypes.IPADDRESS in cleanup_types:
|
|
185
|
+
if not self.user.has_perm("ipam.change_ipaddress"):
|
|
186
|
+
self.fail('User "%s" does not have permission to update IP Address records', self.user.username)
|
|
187
|
+
raise PermissionDenied("User does not have update permission for IP Address records")
|
|
188
|
+
|
|
189
|
+
self.logger.info("Inspecting IP Address records...")
|
|
190
|
+
|
|
191
|
+
all_relevant_ips = IPAddress.objects.restrict(self.user, "change")
|
|
192
|
+
if restrict_to_namespace is not None:
|
|
193
|
+
self.logger.info("Inspecting only records in namespace %s", restrict_to_namespace.name)
|
|
194
|
+
all_relevant_ips = all_relevant_ips.filter(parent__namespace=restrict_to_namespace)
|
|
195
|
+
if restrict_to_network is not None:
|
|
196
|
+
self.logger.info("Inspecting only records that fall within %s", restrict_to_network)
|
|
197
|
+
all_relevant_ips = all_relevant_ips.net_host_contained(restrict_to_network)
|
|
198
|
+
|
|
199
|
+
self.logger.debug("Beginning with a quick check for obviously wrong `parent` values...")
|
|
200
|
+
# 4. Obviously wrong IPAddress parents
|
|
201
|
+
# - parent is unset entirely
|
|
202
|
+
# - parent is set but has wrong IP version
|
|
203
|
+
# - parent is set but its network/broadcast range doesn't contain the given host IP
|
|
204
|
+
ips_with_invalid_parents = (
|
|
205
|
+
all_relevant_ips.filter(parent__isnull=True)
|
|
206
|
+
| all_relevant_ips.exclude(parent__ip_version=models.F("ip_version"))
|
|
207
|
+
| all_relevant_ips.filter(parent__network__gt=models.F("host"))
|
|
208
|
+
| all_relevant_ips.filter(parent__broadcast__lt=models.F("host"))
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
ips_with_invalid_parents = ips_with_invalid_parents.select_related("parent")
|
|
212
|
+
|
|
213
|
+
if ips_with_invalid_parents.exists():
|
|
214
|
+
fixed_ips = []
|
|
215
|
+
for ip in ips_with_invalid_parents:
|
|
216
|
+
candidate_parents = Prefix.objects.all()
|
|
217
|
+
# Preserve namespace
|
|
218
|
+
if ip.parent is not None:
|
|
219
|
+
candidate_parents = candidate_parents.filter(namespace_id=ip.parent.namespace_id)
|
|
220
|
+
else:
|
|
221
|
+
candidate_parents = candidate_parents.filter(namespace_id=get_default_namespace_pk())
|
|
222
|
+
try:
|
|
223
|
+
parent = candidate_parents.get_closest_parent(ip.host, include_self=True)
|
|
224
|
+
self.logger.warning(
|
|
225
|
+
"Parent for %s should be corrected from %s to %s",
|
|
226
|
+
ip.host,
|
|
227
|
+
ip.parent.prefix if ip.parent is not None else None,
|
|
228
|
+
parent.prefix,
|
|
229
|
+
extra={"object": ip},
|
|
230
|
+
)
|
|
231
|
+
ip.parent = parent
|
|
232
|
+
fixed_ips.append(ip)
|
|
233
|
+
except Prefix.DoesNotExist:
|
|
234
|
+
self.logger.warning(
|
|
235
|
+
"No valid parent Prefix could be identified for %s. "
|
|
236
|
+
"You should create a %s/%d Prefix or similar to contain this IP Address.",
|
|
237
|
+
ip.host,
|
|
238
|
+
ip.address.network,
|
|
239
|
+
ip.mask_length,
|
|
240
|
+
extra={"object": ip},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if dryrun:
|
|
244
|
+
self.logger.warning(
|
|
245
|
+
"Would correct invalid `parent` for %d IP Addresses if this were not a dry-run", len(fixed_ips)
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
update_count = IPAddress.objects.bulk_update(fixed_ips, ["parent"], batch_size=1000)
|
|
249
|
+
self.logger.success("Corrected invalid `parent` for %d IP Addresses", update_count)
|
|
250
|
+
else:
|
|
251
|
+
self.logger.success("No IP Address records had null or clearly invalid `parent` values")
|
|
252
|
+
|
|
253
|
+
self.logger.debug("Continuing with a more involved check for more subtly incorrect `parent` values...")
|
|
254
|
+
# 5. More subtly wrong IPAddress parents
|
|
255
|
+
# - parent is set and contains the IP, but is not the most specific such Prefix
|
|
256
|
+
fixed_ips = []
|
|
257
|
+
processed_ip_count = 0
|
|
258
|
+
for ip in all_relevant_ips.exclude(parent__children__isnull=True).select_related("parent"):
|
|
259
|
+
candidate_parents = ip.parent.subnets(include_self=True)
|
|
260
|
+
try:
|
|
261
|
+
parent = candidate_parents.get_closest_parent(ip.host, include_self=True)
|
|
262
|
+
if parent.id != ip.parent_id:
|
|
263
|
+
self.logger.warning(
|
|
264
|
+
"Parent for %s should be corrected from %s to %s",
|
|
265
|
+
ip.host,
|
|
266
|
+
ip.parent.prefix,
|
|
267
|
+
parent.prefix,
|
|
268
|
+
extra={"object": ip},
|
|
269
|
+
)
|
|
270
|
+
ip.parent = parent
|
|
271
|
+
fixed_ips.append(ip)
|
|
272
|
+
except Prefix.DoesNotExist:
|
|
273
|
+
self.logger.warning(
|
|
274
|
+
"No valid parent Prefix could be identified for %s. "
|
|
275
|
+
"You should create a %s/%d Prefix or similar to contain this IP Address.",
|
|
276
|
+
ip.host,
|
|
277
|
+
ip.address.network,
|
|
278
|
+
ip.mask_length,
|
|
279
|
+
extra={"object": ip},
|
|
280
|
+
)
|
|
281
|
+
processed_ip_count += 1
|
|
282
|
+
|
|
283
|
+
self.logger.debug(
|
|
284
|
+
"Inspected %d IP Addresses for more subtly incorrect `parent` values",
|
|
285
|
+
processed_ip_count,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if fixed_ips:
|
|
289
|
+
if dryrun:
|
|
290
|
+
self.logger.warning(
|
|
291
|
+
"Would set a more precise `parent` for %d IP Addresses if this were not a dry-run",
|
|
292
|
+
len(fixed_ips),
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
update_count = IPAddress.objects.bulk_update(fixed_ips, ["parent"], batch_size=1000)
|
|
296
|
+
self.logger.success("Corrected imprecise `parent` for %d IP Addresses", update_count)
|