nautobot 2.0.0a3__py3-none-any.whl → 2.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nautobot/apps/api.py +6 -8
- nautobot/apps/forms.py +0 -2
- nautobot/apps/ui.py +0 -8
- nautobot/circuits/api/serializers.py +9 -117
- nautobot/circuits/api/urls.py +1 -1
- nautobot/circuits/api/views.py +0 -1
- nautobot/circuits/forms.py +0 -65
- nautobot/circuits/migrations/0014_related_name_changes.py +1 -1
- nautobot/circuits/migrations/0016_tagsfield.py +34 -0
- nautobot/circuits/migrations/0017_fixup_null_statuses.py +22 -0
- nautobot/circuits/migrations/0018_status_nonnullable.py +22 -0
- nautobot/circuits/models.py +3 -87
- nautobot/circuits/navigation.py +14 -69
- nautobot/circuits/signals.py +0 -2
- nautobot/circuits/tables.py +39 -1
- nautobot/circuits/tests/integration/test_relationships.py +9 -9
- nautobot/circuits/tests/test_api.py +4 -8
- nautobot/circuits/tests/test_filters.py +10 -4
- nautobot/circuits/tests/test_models.py +5 -1
- nautobot/circuits/tests/test_views.py +27 -5
- nautobot/circuits/views.py +18 -10
- nautobot/core/api/__init__.py +8 -2
- nautobot/core/api/fields.py +15 -6
- nautobot/core/api/filter_backends.py +3 -2
- nautobot/core/api/metadata.py +237 -30
- nautobot/core/api/mixins.py +94 -0
- nautobot/core/api/pagination.py +4 -0
- nautobot/core/api/parsers.py +154 -0
- nautobot/core/api/renderers.py +153 -2
- nautobot/core/api/schema.py +46 -2
- nautobot/core/api/serializers.py +377 -35
- nautobot/core/api/urls.py +11 -3
- nautobot/core/api/utils.py +174 -2
- nautobot/core/api/versioning.py +32 -10
- nautobot/core/api/views.py +266 -72
- nautobot/core/apps/__init__.py +138 -220
- nautobot/core/celery/__init__.py +112 -41
- nautobot/core/celery/backends.py +19 -12
- nautobot/core/celery/control.py +46 -0
- nautobot/core/celery/encoders.py +53 -0
- nautobot/core/celery/log.py +38 -0
- nautobot/core/celery/schedulers.py +23 -4
- nautobot/core/celery/task.py +1 -16
- nautobot/core/checks.py +0 -27
- nautobot/core/choices.py +0 -113
- nautobot/core/{cli.py → cli/__init__.py} +1 -1
- nautobot/core/cli/__main__.py +3 -0
- nautobot/core/constants.py +0 -24
- nautobot/core/context_processors.py +12 -0
- nautobot/core/filters.py +2 -2
- nautobot/core/forms/__init__.py +0 -4
- nautobot/core/forms/fields.py +38 -65
- nautobot/core/forms/forms.py +4 -1
- nautobot/core/forms/utils.py +0 -52
- nautobot/core/graphql/schema.py +4 -27
- nautobot/core/jobs/__init__.py +75 -0
- nautobot/core/management/commands/build_ui.py +255 -0
- nautobot/core/management/commands/generate_test_data.py +3 -2
- nautobot/core/management/commands/post_upgrade.py +24 -24
- nautobot/core/models/__init__.py +26 -1
- nautobot/core/models/fields.py +24 -5
- nautobot/core/models/generics.py +2 -42
- nautobot/core/models/managers.py +5 -0
- nautobot/core/models/name_color_content_types.py +0 -14
- nautobot/core/models/tree_queries.py +14 -4
- nautobot/core/models/utils.py +5 -6
- nautobot/core/models/validators.py +17 -8
- nautobot/core/releases.py +8 -10
- nautobot/core/settings.py +80 -42
- nautobot/core/tables.py +5 -5
- nautobot/core/tasks.py +4 -7
- nautobot/core/templates/base.html +1 -49
- nautobot/core/templates/base_django.html +49 -0
- nautobot/core/templates/base_react.html +55 -0
- nautobot/core/templates/buttons/export.html +6 -4
- nautobot/core/templates/generic/object_bulk_create.html +10 -21
- nautobot/core/templates/generic/object_list.html +3 -1
- nautobot/core/templates/generic/object_retrieve_plugin_full_width.html +3 -0
- nautobot/core/templates/inc/footer.html +1 -0
- nautobot/core/templates/inc/javascript.html +0 -14
- nautobot/core/templates/inc/nav_menu.html +28 -33
- nautobot/core/templates/inc/object_details_advanced_panel.html +13 -0
- nautobot/core/templates/inc/relationships_table_rows.html +2 -2
- nautobot/core/templates/nautobot_config.py.j2 +8 -20
- nautobot/core/templates/plugin_template/__init__.py-tpl +1 -2
- nautobot/core/templates/rest_framework/api.html +8 -0
- nautobot/core/templatetags/buttons.py +32 -28
- nautobot/core/testing/__init__.py +47 -44
- nautobot/core/testing/api.py +362 -47
- nautobot/core/testing/filters.py +1 -1
- nautobot/core/testing/migrations.py +2 -0
- nautobot/core/testing/mixins.py +22 -9
- nautobot/core/testing/schema.py +2 -1
- nautobot/core/testing/views.py +21 -46
- nautobot/core/tests/integration/test_filters.py +17 -8
- nautobot/core/tests/integration/test_navbar.py +11 -34
- nautobot/core/tests/integration/test_plugin_navbar.py +9 -103
- nautobot/core/tests/nautobot_config.py +2 -3
- nautobot/core/tests/test_api.py +290 -21
- nautobot/core/tests/test_checks.py +0 -7
- nautobot/core/tests/test_filters.py +107 -59
- nautobot/core/tests/test_forms.py +26 -92
- nautobot/core/tests/test_graphql.py +110 -77
- nautobot/core/tests/test_logging.py +4 -0
- nautobot/core/tests/test_managers.py +3 -1
- nautobot/core/tests/test_models.py +2 -0
- nautobot/core/tests/test_paginator.py +3 -1
- nautobot/core/tests/test_releases.py +12 -12
- nautobot/core/tests/test_templatetags_helpers.py +4 -4
- nautobot/core/tests/test_utils.py +32 -68
- nautobot/core/tests/test_views.py +12 -15
- nautobot/core/utils/data.py +17 -0
- nautobot/core/utils/deprecation.py +9 -6
- nautobot/core/utils/filtering.py +8 -3
- nautobot/core/utils/git.py +12 -4
- nautobot/core/utils/lookup.py +3 -1
- nautobot/core/utils/requests.py +1 -104
- nautobot/core/views/__init__.py +1 -0
- nautobot/core/views/generic.py +75 -110
- nautobot/core/views/mixins.py +52 -61
- nautobot/core/views/renderers.py +6 -7
- nautobot/core/views/utils.py +80 -0
- nautobot/dcim/api/serializers.py +160 -667
- nautobot/dcim/api/urls.py +1 -1
- nautobot/dcim/api/views.py +7 -44
- nautobot/dcim/choices.py +2 -0
- nautobot/dcim/filters/__init__.py +21 -0
- nautobot/dcim/form_mixins.py +1 -27
- nautobot/dcim/forms.py +19 -765
- nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -1
- nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -13
- nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -1
- nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -1
- nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
- nautobot/dcim/migrations/0035_related_name_changes.py +1 -1
- nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -1
- nautobot/dcim/migrations/0040_tagsfield.py +109 -0
- nautobot/dcim/migrations/{0040_ipam__namespaces.py → 0041_ipam__namespaces.py} +1 -1
- nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
- nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
- nautobot/dcim/models/cables.py +3 -33
- nautobot/dcim/models/device_component_templates.py +6 -0
- nautobot/dcim/models/device_components.py +12 -198
- nautobot/dcim/models/devices.py +30 -143
- nautobot/dcim/models/locations.py +3 -64
- nautobot/dcim/models/power.py +3 -50
- nautobot/dcim/models/racks.py +7 -84
- nautobot/dcim/navigation.py +141 -467
- nautobot/dcim/signals.py +0 -2
- nautobot/dcim/tables/locations.py +2 -2
- nautobot/dcim/tables/power.py +1 -2
- nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/devicetype.html +2 -2
- nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/location.html +16 -1
- nautobot/dcim/templates/dcim/locationtype.html +15 -0
- nautobot/dcim/templates/dcim/power_port_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/rackgroup.html +0 -12
- nautobot/dcim/tests/test_api.py +166 -81
- nautobot/dcim/tests/test_cablepaths.py +41 -35
- nautobot/dcim/tests/test_filters.py +67 -23
- nautobot/dcim/tests/test_forms.py +5 -205
- nautobot/dcim/tests/test_graphql.py +7 -2
- nautobot/dcim/tests/test_migrations.py +6 -11
- nautobot/dcim/tests/test_models.py +182 -110
- nautobot/dcim/tests/test_natural_ordering.py +11 -8
- nautobot/dcim/tests/test_signals.py +6 -3
- nautobot/dcim/tests/test_views.py +197 -175
- nautobot/dcim/urls.py +11 -16
- nautobot/dcim/views.py +7 -134
- nautobot/docs/additional-features/caching.md +6 -87
- nautobot/docs/additional-features/job-scheduling-and-approvals.md +3 -0
- nautobot/docs/additional-features/jobs.md +177 -195
- nautobot/docs/administration/nautobot-server.md +6 -21
- nautobot/docs/administration/replicating-nautobot.md +0 -10
- nautobot/docs/configuration/optional-settings.md +32 -41
- nautobot/docs/configuration/required-settings.md +11 -52
- nautobot/docs/development/application-registry.md +2 -13
- nautobot/docs/development/extending-models.md +15 -17
- nautobot/docs/development/generic-views.md +0 -2
- nautobot/docs/development/getting-started.md +55 -5
- nautobot/docs/development/navigation-menu.md +22 -93
- nautobot/docs/development/react-ui.md +105 -0
- nautobot/docs/development/role-internals.md +1 -3
- nautobot/docs/development/style-guide.md +6 -4
- nautobot/docs/index.md +3 -2
- nautobot/docs/installation/migrating-from-netbox.md +11 -42
- nautobot/docs/installation/nautobot.md +1 -1
- nautobot/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
- nautobot/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
- nautobot/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
- nautobot/docs/installation/tables/v2-code-location-changes.yaml +241 -0
- nautobot/docs/installation/tables/v2-code-removals.yaml +67 -0
- nautobot/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
- nautobot/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
- nautobot/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
- nautobot/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
- nautobot/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
- nautobot/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
- nautobot/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
- nautobot/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
- nautobot/docs/installation/upgrading-from-nautobot-v1.md +170 -747
- nautobot/docs/models/dcim/device.md +3 -0
- nautobot/docs/models/dcim/deviceredundancygroup.md +3 -3
- nautobot/docs/models/extras/computedfield.md +4 -4
- nautobot/docs/models/extras/gitrepository.md +3 -0
- nautobot/docs/models/extras/job.md +1 -0
- nautobot/docs/models/extras/jobbutton.md +18 -13
- nautobot/docs/models/extras/jobhook.md +7 -4
- nautobot/docs/models/extras/jobresult.md +6 -2
- nautobot/docs/models/extras/relationship.md +2 -2
- nautobot/docs/models/extras/status.md +6 -19
- nautobot/docs/models/ipam/ipaddress.md +3 -0
- nautobot/docs/models/virtualization/virtualmachine.md +3 -0
- nautobot/docs/plugins/development.md +83 -21
- nautobot/docs/release-notes/version-1.5.md +53 -0
- nautobot/docs/release-notes/version-2.0.md +180 -0
- nautobot/docs/requirements.txt +1 -0
- nautobot/docs/rest-api/overview.md +384 -215
- nautobot/docs/rest-api/ui-related-endpoints.md +9 -0
- nautobot/extras/admin.py +3 -5
- nautobot/extras/api/customfields.py +15 -39
- nautobot/extras/api/fields.py +0 -11
- nautobot/extras/api/mixins.py +45 -0
- nautobot/extras/api/relationships.py +63 -158
- nautobot/extras/api/serializers.py +165 -700
- nautobot/extras/api/urls.py +1 -1
- nautobot/extras/api/views.py +294 -280
- nautobot/extras/apps.py +4 -7
- nautobot/extras/choices.py +11 -9
- nautobot/extras/constants.py +9 -3
- nautobot/extras/datasources/__init__.py +2 -0
- nautobot/extras/datasources/git.py +135 -186
- nautobot/extras/datasources/registry.py +25 -35
- nautobot/extras/filters/__init__.py +20 -19
- nautobot/extras/filters/mixins.py +4 -4
- nautobot/extras/forms/forms.py +63 -127
- nautobot/extras/forms/mixins.py +23 -51
- nautobot/extras/health_checks.py +0 -33
- nautobot/extras/jobs.py +387 -565
- nautobot/extras/management/commands/runjob.py +24 -62
- nautobot/extras/managers.py +30 -7
- nautobot/extras/migrations/0058_jobresult_add_time_status_idxs.py +38 -0
- nautobot/extras/migrations/{0058_joblogentry_scheduledjob_webhook_data_migration.py → 0059_joblogentry_scheduledjob_webhook_data_migration.py} +1 -1
- nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -1
- nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -7
- nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -32
- nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -1
- nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -1
- nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
- nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -1
- nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -2
- nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -1
- nautobot/extras/migrations/{0068_remove_site_and_region_attributes_from_config_context.py → 0069_remove_site_and_region_attributes_from_config_context.py} +1 -1
- nautobot/extras/migrations/{0069_replace_related_names.py → 0070_replace_related_names.py} +1 -1
- nautobot/extras/migrations/{0070_rename_model_fields.py → 0071_rename_model_fields.py} +1 -1
- nautobot/extras/migrations/0072_job__unique_name_data_migration.py +86 -0
- nautobot/extras/migrations/{0072_job__unique_name.py → 0073_job__unique_name.py} +13 -9
- nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -1
- nautobot/extras/migrations/{0074_rename_slug_to_key_for_custom_field.py → 0075_rename_slug_to_key_for_custom_field.py} +1 -1
- nautobot/extras/migrations/{0075_migrate_custom_field_data.py → 0076_migrate_custom_field_data.py} +1 -1
- nautobot/extras/migrations/{0076_remove_name_field_and_make_label_field_non_nullable.py → 0077_remove_name_field_and_make_label_field_non_nullable.py} +1 -1
- nautobot/extras/migrations/{0077_remove_slug.py → 0078_remove_slug.py} +1 -5
- nautobot/extras/migrations/0079_tagsfield.py +28 -0
- nautobot/extras/migrations/0080_rename_relationship_slug_to_key.py +17 -0
- nautobot/extras/migrations/0081_rename_relationship_name_to_label.py +29 -0
- nautobot/extras/migrations/0082_ensure_relationship_keys_are_unique.py +43 -0
- nautobot/extras/migrations/0083_rename_computed_field_slug_to_key.py +21 -0
- nautobot/extras/migrations/0084_taggeditem_cleanup.py +43 -0
- nautobot/extras/migrations/0085_taggeditem_uniqueness.py +22 -0
- nautobot/extras/migrations/0086_job__celery_task_fields__dryrun_support.py +81 -0
- nautobot/extras/migrations/0087_job__commit_default_data_migration.py +26 -0
- nautobot/extras/migrations/0088_joblogentry__log_level_default.py +17 -0
- nautobot/extras/migrations/0089_joblogentry__log_level_data_migration.py +34 -0
- nautobot/extras/migrations/0090_scheduledjob__data_migration.py +57 -0
- nautobot/extras/models/__init__.py +2 -3
- nautobot/extras/models/change_logging.py +0 -36
- nautobot/extras/models/customfields.py +39 -33
- nautobot/extras/models/datasources.py +48 -50
- nautobot/extras/models/groups.py +5 -6
- nautobot/extras/models/jobs.py +189 -321
- nautobot/extras/models/mixins.py +0 -71
- nautobot/extras/models/models.py +0 -19
- nautobot/extras/models/relationships.py +19 -13
- nautobot/extras/models/roles.py +0 -34
- nautobot/extras/models/secrets.py +2 -26
- nautobot/extras/models/statuses.py +6 -5
- nautobot/extras/models/tags.py +2 -17
- nautobot/extras/navigation.py +89 -307
- nautobot/extras/plugins/__init__.py +3 -120
- nautobot/extras/plugins/utils.py +0 -3
- nautobot/extras/plugins/validators.py +5 -4
- nautobot/extras/plugins/views.py +16 -3
- nautobot/extras/querysets.py +1 -7
- nautobot/extras/registry.py +3 -0
- nautobot/extras/signals.py +26 -60
- nautobot/extras/tables.py +34 -40
- nautobot/extras/tasks.py +0 -12
- nautobot/extras/templates/extras/configcontext.html +1 -1
- nautobot/extras/templates/extras/configcontextschema.html +16 -1
- nautobot/extras/templates/extras/customfield.html +0 -13
- nautobot/extras/templates/extras/gitrepository.html +3 -3
- nautobot/extras/templates/extras/inc/jobresult.html +10 -0
- nautobot/extras/templates/extras/inc/panel_jobhistory.html +1 -1
- nautobot/extras/templates/extras/job.html +35 -25
- nautobot/extras/templates/extras/job_approval_request.html +15 -30
- nautobot/extras/templates/extras/job_detail.html +13 -31
- nautobot/extras/templates/extras/job_edit.html +15 -17
- nautobot/extras/templates/extras/jobresult.html +24 -6
- nautobot/extras/templates/extras/scheduledjob.html +2 -2
- nautobot/extras/templates/extras/secret.html +28 -0
- nautobot/extras/templatetags/job_buttons.py +1 -0
- nautobot/extras/{tests/example_jobs → test_jobs}/api_test_job.py +13 -6
- nautobot/extras/test_jobs/atomic_transaction.py +53 -0
- nautobot/extras/test_jobs/dry_run.py +29 -0
- nautobot/extras/{tests/example_jobs/test_duplicate_name.py → test_jobs/duplicate_name.py} +4 -0
- nautobot/extras/test_jobs/duplicate_name2.py +9 -0
- nautobot/extras/test_jobs/fail.py +23 -0
- nautobot/extras/{tests/example_jobs/test_field_default.py → test_jobs/field_default.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_field_order.py → test_jobs/field_order.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_file_upload_fail.py → test_jobs/file_upload_fail.py} +11 -6
- nautobot/extras/test_jobs/file_upload_pass.py +25 -0
- nautobot/extras/test_jobs/has_sensitive_variables.py +25 -0
- nautobot/extras/test_jobs/ipaddress_vars.py +66 -0
- nautobot/extras/test_jobs/job_button_receiver.py +28 -0
- nautobot/extras/test_jobs/job_hook_receiver.py +29 -0
- nautobot/extras/test_jobs/job_variables.py +88 -0
- nautobot/extras/test_jobs/location_with_custom_field.py +45 -0
- nautobot/extras/test_jobs/log_redaction.py +20 -0
- nautobot/extras/test_jobs/log_skip_db_logging.py +17 -0
- nautobot/extras/test_jobs/modify_db.py +25 -0
- nautobot/extras/{tests/example_jobs/test_no_field_order.py → test_jobs/no_field_order.py} +4 -0
- nautobot/extras/test_jobs/object_var_optional.py +21 -0
- nautobot/extras/test_jobs/object_var_required.py +21 -0
- nautobot/extras/test_jobs/object_vars.py +26 -0
- nautobot/extras/test_jobs/pass.py +25 -0
- nautobot/extras/test_jobs/profiling.py +32 -0
- nautobot/extras/test_jobs/read_only_job.py +15 -0
- nautobot/extras/{tests/example_jobs/test_required_args.py → test_jobs/required_args.py} +4 -0
- nautobot/extras/{tests/example_jobs/test_soft_time_limit_greater_than_time_limit.py → test_jobs/soft_time_limit_greater_than_time_limit.py} +5 -1
- nautobot/extras/{tests/example_jobs/test_task_queues.py → test_jobs/task_queues.py} +5 -1
- nautobot/extras/tests/integration/test_computedfields.py +1 -1
- nautobot/extras/tests/integration/test_configcontextschema.py +5 -3
- nautobot/extras/tests/integration/test_customfields.py +4 -2
- nautobot/extras/tests/integration/test_dynamicgroups.py +1 -1
- nautobot/extras/tests/integration/test_jobs.py +25 -27
- nautobot/extras/tests/integration/test_notes.py +8 -4
- nautobot/extras/tests/integration/test_relationships.py +2 -2
- nautobot/extras/tests/test_api.py +649 -642
- nautobot/extras/tests/test_changelog.py +3 -3
- nautobot/extras/tests/test_context_managers.py +5 -3
- nautobot/extras/tests/test_customfields.py +92 -50
- nautobot/extras/tests/test_datasources.py +189 -112
- nautobot/extras/tests/test_dynamicgroups.py +7 -8
- nautobot/extras/tests/test_filters.py +137 -89
- nautobot/extras/tests/test_forms.py +73 -75
- nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
- nautobot/extras/tests/test_jobs.py +262 -263
- nautobot/extras/tests/test_migrations.py +4 -3
- nautobot/extras/tests/test_models.py +116 -161
- nautobot/extras/tests/test_plugins.py +38 -60
- nautobot/extras/tests/test_relationships.py +167 -120
- nautobot/extras/tests/test_tags.py +6 -11
- nautobot/extras/tests/test_utils.py +31 -1
- nautobot/extras/tests/test_views.py +201 -145
- nautobot/extras/tests/test_webhooks.py +6 -2
- nautobot/extras/urls.py +42 -42
- nautobot/extras/utils.py +137 -163
- nautobot/extras/views.py +78 -152
- nautobot/ipam/api/fields.py +17 -0
- nautobot/ipam/api/serializers.py +58 -164
- nautobot/ipam/api/urls.py +1 -1
- nautobot/ipam/api/views.py +3 -2
- nautobot/ipam/apps.py +1 -2
- nautobot/ipam/filters.py +1 -10
- nautobot/ipam/forms.py +4 -177
- nautobot/ipam/lookups.py +1 -0
- nautobot/ipam/management/commands/__init__.py +0 -0
- nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
- nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -1
- nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -38
- nautobot/ipam/migrations/0020_related_name_changes.py +1 -1
- nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +2 -2
- nautobot/ipam/migrations/0028_tagsfield.py +44 -0
- nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
- nautobot/ipam/migrations/{0028_ipam__namespaces.py → 0030_ipam__namespaces.py} +77 -28
- nautobot/ipam/migrations/0031_ipam__prefix__add_parent.py +58 -0
- nautobot/ipam/migrations/0032_ipam__namespaces_finish.py +63 -0
- nautobot/ipam/migrations/0033_fixup_null_statuses.py +26 -0
- nautobot/ipam/migrations/0034_status_nonnullable.py +36 -0
- nautobot/ipam/models.py +100 -236
- nautobot/ipam/navigation.py +36 -181
- nautobot/ipam/querysets.py +20 -25
- nautobot/ipam/signals.py +49 -6
- nautobot/ipam/tables.py +10 -3
- nautobot/ipam/templates/ipam/namespace_ipaddresses.html +11 -0
- nautobot/ipam/templates/ipam/namespace_prefixes.html +11 -0
- nautobot/ipam/templates/ipam/namespace_retrieve.html +17 -4
- nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
- nautobot/ipam/templates/ipam/prefix.html +1 -1
- nautobot/ipam/templates/ipam/vlangroup.html +0 -13
- nautobot/ipam/templates/ipam/vrf_edit.html +6 -0
- nautobot/ipam/tests/integration/test_prefixes.py +3 -26
- nautobot/ipam/tests/test_api.py +22 -19
- nautobot/ipam/tests/test_filters.py +59 -23
- nautobot/ipam/tests/test_migrations.py +6 -10
- nautobot/ipam/tests/test_models.py +323 -198
- nautobot/ipam/tests/test_ordering.py +2 -2
- nautobot/ipam/tests/test_querysets.py +44 -24
- nautobot/ipam/tests/test_views.py +73 -26
- nautobot/ipam/urls.py +16 -0
- nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
- nautobot/ipam/utils/migrations.py +713 -0
- nautobot/ipam/views.py +137 -20
- nautobot/project-static/docs/404.html +1178 -10
- nautobot/project-static/docs/additional-features/caching.html +1224 -159
- nautobot/project-static/docs/additional-features/change-logging.html +1180 -12
- nautobot/project-static/docs/additional-features/config-contexts.html +1180 -12
- nautobot/project-static/docs/additional-features/graphql.html +1179 -11
- nautobot/project-static/docs/additional-features/healthcheck.html +1180 -12
- nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1184 -12
- nautobot/project-static/docs/additional-features/jobs.html +1514 -328
- nautobot/project-static/docs/additional-features/napalm.html +1180 -12
- nautobot/project-static/docs/additional-features/prometheus-metrics.html +1180 -12
- nautobot/project-static/docs/additional-features/template-filters.html +1180 -12
- nautobot/project-static/docs/administration/celery-queues.html +1178 -10
- nautobot/project-static/docs/administration/nautobot-server.html +1451 -304
- nautobot/project-static/docs/administration/nautobot-shell.html +1178 -10
- nautobot/project-static/docs/administration/permissions.html +1178 -10
- nautobot/project-static/docs/administration/replicating-nautobot.html +1262 -113
- nautobot/project-static/docs/apps/index.html +1178 -10
- nautobot/project-static/docs/apps/nautobot-apps.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1580 -426
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3481 -1838
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1185 -11
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +1719 -551
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2062 -930
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +1946 -659
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1180 -12
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1189 -21
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +9283 -6218
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2734 -2122
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1178 -10
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2337 -1300
- nautobot/project-static/docs/configuration/authentication/ldap.html +1178 -10
- nautobot/project-static/docs/configuration/authentication/remote.html +1178 -10
- nautobot/project-static/docs/configuration/authentication/sso.html +1178 -10
- nautobot/project-static/docs/configuration/index.html +1178 -10
- nautobot/project-static/docs/configuration/optional-settings.html +1311 -160
- nautobot/project-static/docs/configuration/required-settings.html +1312 -211
- nautobot/project-static/docs/core-functionality/circuits.html +1178 -10
- nautobot/project-static/docs/core-functionality/device-types.html +1178 -10
- nautobot/project-static/docs/core-functionality/devices.html +1182 -10
- nautobot/project-static/docs/core-functionality/ipam.html +1182 -10
- nautobot/project-static/docs/core-functionality/power.html +1178 -10
- nautobot/project-static/docs/core-functionality/secrets.html +1178 -10
- nautobot/project-static/docs/core-functionality/services.html +1178 -10
- nautobot/project-static/docs/core-functionality/sites-and-racks.html +1178 -10
- nautobot/project-static/docs/core-functionality/tenancy.html +1178 -10
- nautobot/project-static/docs/core-functionality/virtualization.html +1182 -10
- nautobot/project-static/docs/core-functionality/vlans.html +1179 -11
- nautobot/project-static/docs/development/application-registry.html +1190 -42
- nautobot/project-static/docs/development/best-practices.html +1178 -10
- nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1178 -10
- nautobot/project-static/docs/development/extending-models.html +1238 -83
- nautobot/project-static/docs/development/generic-views.html +1180 -14
- nautobot/project-static/docs/development/getting-started.html +1365 -90
- nautobot/project-static/docs/development/homepage.html +1178 -10
- nautobot/project-static/docs/development/index.html +1178 -10
- nautobot/project-static/docs/development/model-features.html +1178 -10
- nautobot/project-static/docs/development/natural-keys.html +1178 -10
- nautobot/project-static/docs/development/navigation-menu.html +1215 -125
- nautobot/project-static/docs/development/react-ui.html +4199 -0
- nautobot/project-static/docs/development/release-checklist.html +1178 -10
- nautobot/project-static/docs/development/role-internals.html +1179 -12
- nautobot/project-static/docs/development/style-guide.html +1188 -19
- nautobot/project-static/docs/development/templates.html +1178 -10
- nautobot/project-static/docs/development/testing.html +1178 -10
- nautobot/project-static/docs/development/user-preferences.html +1178 -10
- nautobot/project-static/docs/docker/index.html +1178 -10
- nautobot/project-static/docs/index.html +1183 -12
- nautobot/project-static/docs/installation/centos.html +1178 -10
- nautobot/project-static/docs/installation/external-authentication.html +1178 -10
- nautobot/project-static/docs/installation/http-server.html +1178 -10
- nautobot/project-static/docs/installation/index.html +1178 -10
- nautobot/project-static/docs/installation/migrating-from-netbox.html +1305 -189
- nautobot/project-static/docs/installation/migrating-from-postgresql.html +1178 -10
- nautobot/project-static/docs/installation/nautobot.html +1179 -11
- nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1178 -10
- nautobot/project-static/docs/installation/selinux-troubleshooting.html +1178 -10
- nautobot/project-static/docs/installation/services.html +1178 -10
- nautobot/project-static/docs/installation/tables/v2-api-behavior-changes.yaml +70 -0
- nautobot/project-static/docs/installation/tables/v2-api-removed-fields.yaml +142 -0
- nautobot/project-static/docs/installation/tables/v2-api-renamed-fields.yaml +124 -0
- nautobot/project-static/docs/installation/tables/v2-code-location-changes.yaml +241 -0
- nautobot/project-static/docs/installation/tables/v2-code-removals.yaml +67 -0
- nautobot/project-static/docs/installation/tables/v2-database-behavior-changes.yaml +37 -0
- nautobot/project-static/docs/installation/tables/v2-database-removed-fields.yaml +166 -0
- nautobot/project-static/docs/installation/tables/v2-database-renamed-fields.yaml +340 -0
- nautobot/project-static/docs/installation/tables/v2-filters-corrected-fields.yaml +28 -0
- nautobot/project-static/docs/installation/tables/v2-filters-enhanced-fields.yaml +241 -0
- nautobot/project-static/docs/installation/tables/v2-filters-removed-fields.yaml +553 -0
- nautobot/project-static/docs/installation/tables/v2-filters-renamed-fields.yaml +223 -0
- nautobot/project-static/docs/installation/tables/v2-logging-renamed-loggers.yaml +23 -0
- nautobot/project-static/docs/installation/ubuntu.html +1178 -10
- nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +3823 -2152
- nautobot/project-static/docs/installation/upgrading.html +1178 -10
- nautobot/project-static/docs/models/circuits/circuit.html +1293 -103
- nautobot/project-static/docs/models/circuits/circuittermination.html +1293 -103
- nautobot/project-static/docs/models/circuits/circuittype.html +1293 -103
- nautobot/project-static/docs/models/circuits/provider.html +1293 -103
- nautobot/project-static/docs/models/circuits/providernetwork.html +1293 -103
- nautobot/project-static/docs/models/dcim/cable.html +1324 -103
- nautobot/project-static/docs/models/dcim/consoleport.html +1293 -103
- nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/consoleserverport.html +1293 -103
- nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/device.html +1326 -132
- nautobot/project-static/docs/models/dcim/devicebay.html +1293 -103
- nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1379 -97
- nautobot/project-static/docs/models/dcim/devicetype.html +1293 -103
- nautobot/project-static/docs/models/dcim/frontport.html +1293 -103
- nautobot/project-static/docs/models/dcim/frontporttemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/interface.html +1293 -103
- nautobot/project-static/docs/models/dcim/interfacetemplate.html +1293 -103
- nautobot/project-static/docs/models/dcim/inventoryitem.html +1293 -103
- nautobot/project-static/docs/models/dcim/location.html +1293 -103
- nautobot/project-static/docs/models/dcim/locationtype.html +1293 -103
- nautobot/project-static/docs/models/dcim/manufacturer.html +1292 -102
- nautobot/project-static/docs/models/dcim/platform.html +1272 -82
- nautobot/project-static/docs/models/dcim/powerfeed.html +1270 -80
- nautobot/project-static/docs/models/dcim/poweroutlet.html +1272 -82
- nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1272 -82
- nautobot/project-static/docs/models/dcim/powerpanel.html +1270 -80
- nautobot/project-static/docs/models/dcim/powerport.html +1272 -82
- nautobot/project-static/docs/models/dcim/powerporttemplate.html +1272 -82
- nautobot/project-static/docs/models/dcim/rack.html +1272 -82
- nautobot/project-static/docs/models/dcim/rackgroup.html +1272 -82
- nautobot/project-static/docs/models/dcim/rackreservation.html +1272 -82
- nautobot/project-static/docs/models/dcim/rearport.html +1286 -96
- nautobot/project-static/docs/models/dcim/rearporttemplate.html +1286 -96
- nautobot/project-static/docs/models/dcim/region.html +1178 -10
- nautobot/project-static/docs/models/dcim/site.html +1178 -10
- nautobot/project-static/docs/models/dcim/virtualchassis.html +1284 -94
- nautobot/project-static/docs/models/extras/computedfield.html +1184 -16
- nautobot/project-static/docs/models/extras/configcontext.html +1314 -86
- nautobot/project-static/docs/models/extras/configcontextschema.html +1276 -86
- nautobot/project-static/docs/models/extras/customfield.html +1180 -12
- nautobot/project-static/docs/models/extras/customlink.html +1180 -12
- nautobot/project-static/docs/models/extras/dynamicgroup.html +1180 -12
- nautobot/project-static/docs/models/extras/exporttemplate.html +1180 -12
- nautobot/project-static/docs/models/extras/gitrepository.html +1184 -12
- nautobot/project-static/docs/models/extras/graphqlquery.html +1321 -86
- nautobot/project-static/docs/models/extras/imageattachment.html +1276 -86
- nautobot/project-static/docs/models/extras/job.html +1277 -86
- nautobot/project-static/docs/models/extras/jobbutton.html +1201 -29
- nautobot/project-static/docs/models/extras/jobhook.html +1188 -16
- nautobot/project-static/docs/models/extras/joblogentry.html +1274 -84
- nautobot/project-static/docs/models/extras/jobresult.html +1364 -169
- nautobot/project-static/docs/models/extras/note.html +1180 -12
- nautobot/project-static/docs/models/extras/relationship.html +1182 -14
- nautobot/project-static/docs/models/extras/role.html +1320 -86
- nautobot/project-static/docs/models/extras/secret.html +1314 -86
- nautobot/project-static/docs/models/extras/secretsgroup.html +1276 -86
- nautobot/project-static/docs/models/extras/status.html +1188 -59
- nautobot/project-static/docs/models/extras/tag.html +1180 -12
- nautobot/project-static/docs/models/extras/webhook.html +1180 -12
- nautobot/project-static/docs/models/ipam/ipaddress.html +1327 -102
- nautobot/project-static/docs/models/ipam/prefix.html +1276 -86
- nautobot/project-static/docs/models/ipam/rir.html +1276 -86
- nautobot/project-static/docs/models/ipam/routetarget.html +1276 -86
- nautobot/project-static/docs/models/ipam/service.html +1276 -86
- nautobot/project-static/docs/models/ipam/vlan.html +1276 -86
- nautobot/project-static/docs/models/ipam/vlangroup.html +1276 -86
- nautobot/project-static/docs/models/ipam/vrf.html +1276 -86
- nautobot/project-static/docs/models/tenancy/tenant.html +1276 -86
- nautobot/project-static/docs/models/tenancy/tenantgroup.html +1276 -86
- nautobot/project-static/docs/models/users/objectpermission.html +1314 -86
- nautobot/project-static/docs/models/users/token.html +1276 -86
- nautobot/project-static/docs/models/virtualization/cluster.html +1276 -86
- nautobot/project-static/docs/models/virtualization/clustergroup.html +1276 -86
- nautobot/project-static/docs/models/virtualization/clustertype.html +1276 -86
- nautobot/project-static/docs/models/virtualization/virtualmachine.html +1321 -127
- nautobot/project-static/docs/models/virtualization/vminterface.html +1276 -86
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/plugins/development.html +1726 -495
- nautobot/project-static/docs/plugins/index.html +1178 -10
- nautobot/project-static/docs/plugins/porting-from-netbox.html +1178 -10
- nautobot/project-static/docs/release-notes/index.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.0.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.1.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.2.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.3.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.4.html +1178 -10
- nautobot/project-static/docs/release-notes/version-1.5.html +1608 -225
- nautobot/project-static/docs/release-notes/version-2.0.html +1547 -47
- nautobot/project-static/docs/requirements.txt +1 -0
- nautobot/project-static/docs/rest-api/authentication.html +1179 -11
- nautobot/project-static/docs/rest-api/filtering.html +1178 -10
- nautobot/project-static/docs/rest-api/overview.html +1841 -446
- nautobot/project-static/docs/rest-api/ui-related-endpoints.html +4057 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +197 -187
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guides/custom-fields.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/index.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/ipam.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/platforms.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/regions.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/tenants.html +1178 -10
- nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1178 -10
- nautobot/project-static/docs/user-guides/git-data-source.html +1178 -10
- nautobot/project-static/docs/user-guides/graphql.html +1178 -10
- nautobot/project-static/docs/user-guides/relationships.html +1178 -10
- nautobot/project-static/docs/user-guides/s3-django-storage.html +1178 -10
- nautobot/project-static/js/forms.js +16 -9
- nautobot/project-static/js/theme.js +5 -0
- nautobot/tenancy/api/serializers.py +4 -32
- nautobot/tenancy/api/urls.py +1 -1
- nautobot/tenancy/forms.py +0 -28
- nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
- nautobot/tenancy/models.py +0 -25
- nautobot/tenancy/navigation.py +6 -39
- nautobot/tenancy/templates/tenancy/tenant.html +12 -12
- nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
- nautobot/tenancy/tests/test_api.py +1 -3
- nautobot/tenancy/tests/test_filters.py +10 -5
- nautobot/tenancy/views.py +0 -2
- nautobot/ui/.eslintignore +6 -0
- nautobot/ui/.gitignore +10 -0
- nautobot/ui/.prettierignore +9 -0
- nautobot/ui/.prettierrc +4 -0
- nautobot/ui/README.md +33 -0
- nautobot/ui/app_imports.js.j2 +7 -0
- nautobot/ui/craco.config.js +46 -0
- nautobot/ui/jsconfig-base.json +11 -0
- nautobot/ui/jsconfig.json +5 -0
- nautobot/ui/lib/nautobot-craco-alias-plugin.js +40 -0
- nautobot/ui/package-lock.json +21451 -0
- nautobot/ui/package.json +70 -0
- nautobot/ui/public/index.html +47 -0
- nautobot/ui/public/logo192.png +0 -0
- nautobot/ui/public/logo512.png +0 -0
- nautobot/ui/public/manifest.json +25 -0
- nautobot/ui/public/nautobot_logo.svg +131 -0
- nautobot/ui/public/robots.txt +3 -0
- nautobot/ui/src/App.js +71 -0
- nautobot/ui/src/components/AppFullWidthComponents.js +8 -0
- nautobot/ui/src/components/AppTab.js +40 -0
- nautobot/ui/src/components/Apps.js +60 -0
- nautobot/ui/src/components/HomeChangelogPanel.js +98 -0
- nautobot/ui/src/components/HomePanel.js +58 -0
- nautobot/ui/src/components/JobHistoryTable.js +78 -0
- nautobot/ui/src/components/Layout.js +53 -0
- nautobot/ui/src/components/LoadingWidget.js +25 -0
- nautobot/ui/src/components/Navbar.js +116 -0
- nautobot/ui/src/components/NotificationPopover.js +27 -0
- nautobot/ui/src/components/ObjectListTable.js +209 -0
- nautobot/ui/src/components/ReferenceDataTag.js +35 -0
- nautobot/ui/src/components/RouterButton.js +10 -0
- nautobot/ui/src/components/RouterLink.js +10 -0
- nautobot/ui/src/components/SidebarNav.js +147 -0
- nautobot/ui/src/components/Table.js +48 -0
- nautobot/ui/src/components/TableItem.js +71 -0
- nautobot/ui/src/components/__tests__/AppFullWidthComponents.test.js +16 -0
- nautobot/ui/src/components/__tests__/AppTab.test.js +21 -0
- nautobot/ui/src/components/__tests__/Apps.test.js +14 -0
- nautobot/ui/src/components/__tests__/Layout.test.js +33 -0
- nautobot/ui/src/components/__tests__/Table.test.js +36 -0
- nautobot/ui/src/components/__tests__/TableItem.test.js +37 -0
- nautobot/ui/src/components/__tests__/paginator.test.js +43 -0
- nautobot/ui/src/components/__tests__/paginator_form.test.js +13 -0
- nautobot/ui/src/components/pagination.js +93 -0
- nautobot/ui/src/components/paginator.js +79 -0
- nautobot/ui/src/components/paginator_form.js +43 -0
- nautobot/ui/src/components/usePagination.js +57 -0
- nautobot/ui/src/constants/apiPath.js +10 -0
- nautobot/ui/src/constants/icons.js +15 -0
- nautobot/ui/src/constants/size.js +15 -0
- nautobot/ui/src/index.js +65 -0
- nautobot/ui/src/reportWebVitals.js +15 -0
- nautobot/ui/src/router.js +77 -0
- nautobot/ui/src/utils/api.js +131 -0
- nautobot/ui/src/utils/app-import.js +15 -0
- nautobot/ui/src/utils/color.js +15 -0
- nautobot/ui/src/utils/date.js +14 -0
- nautobot/ui/src/utils/index.js +15 -0
- nautobot/ui/src/utils/navigation.js +32 -0
- nautobot/ui/src/utils/session.js +64 -0
- nautobot/ui/src/utils/store.js +242 -0
- nautobot/ui/src/utils/string.js +6 -0
- nautobot/ui/src/utils/url.js +4 -0
- nautobot/ui/src/views/Home.js +138 -0
- nautobot/ui/src/views/InstalledApps.js +80 -0
- nautobot/ui/src/views/Login.js +48 -0
- nautobot/ui/src/views/Logout.js +20 -0
- nautobot/ui/src/views/__tests__/BSCreateViewTemplate.test.js +11 -0
- nautobot/ui/src/views/__tests__/BSListViewTemplate.test.js +107 -0
- nautobot/ui/src/views/__tests__/Login.test.js +15 -0
- nautobot/ui/src/views/generic/GenericView.js +142 -0
- nautobot/ui/src/views/generic/ObjectCreate.js +96 -0
- nautobot/ui/src/views/generic/ObjectList.js +127 -0
- nautobot/ui/src/views/generic/ObjectRetrieve.js +551 -0
- nautobot/users/admin.py +1 -1
- nautobot/users/api/serializers.py +51 -61
- nautobot/users/api/urls.py +1 -1
- nautobot/users/api/views.py +53 -2
- nautobot/users/tests/test_api.py +110 -25
- nautobot/virtualization/api/serializers.py +18 -130
- nautobot/virtualization/api/urls.py +1 -1
- nautobot/virtualization/api/views.py +1 -22
- nautobot/virtualization/forms.py +13 -99
- nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -1
- nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -11
- nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -1
- nautobot/virtualization/migrations/0018_related_name_changes.py +1 -1
- nautobot/virtualization/migrations/0021_tagsfield_and_vminterface_to_primarymodel.py +39 -0
- nautobot/virtualization/migrations/0022_vminterface_timestamps_data_migration.py +17 -0
- nautobot/virtualization/migrations/{0021_ipam__namespaces.py → 0023_ipam__namespaces.py} +2 -2
- nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
- nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
- nautobot/virtualization/models.py +31 -123
- nautobot/virtualization/navigation.py +18 -99
- nautobot/virtualization/templates/virtualization/virtualmachine.html +2 -1
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
- nautobot/virtualization/tests/test_api.py +25 -26
- nautobot/virtualization/tests/test_filters.py +41 -15
- nautobot/virtualization/tests/test_models.py +31 -7
- nautobot/virtualization/tests/test_views.py +42 -25
- nautobot/virtualization/views.py +7 -6
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +3 -7
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +744 -602
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/WHEEL +1 -1
- nautobot/circuits/api/nested_serializers.py +0 -69
- nautobot/core/templates/plugin_template/navigation.py-tpl +0 -22
- nautobot/dcim/api/nested_serializers.py +0 -356
- nautobot/dcim/templates/dcim/device_import.html +0 -5
- nautobot/dcim/templates/dcim/device_import_child.html +0 -5
- nautobot/dcim/templates/dcim/inc/device_import_header.html +0 -4
- nautobot/extras/api/nested_serializers.py +0 -353
- nautobot/extras/migrations/0064_configcontext_data_migrations.py +0 -41
- nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -46
- nautobot/extras/reports.py +0 -60
- nautobot/extras/scripts.py +0 -72
- nautobot/extras/tests/example_jobs/script_variables.py +0 -67
- nautobot/extras/tests/example_jobs/test_duplicate_name2.py +0 -5
- nautobot/extras/tests/example_jobs/test_fail.py +0 -16
- nautobot/extras/tests/example_jobs/test_file_upload_pass.py +0 -20
- nautobot/extras/tests/example_jobs/test_ipaddress_vars.py +0 -52
- nautobot/extras/tests/example_jobs/test_job_button_receiver.py +0 -21
- nautobot/extras/tests/example_jobs/test_job_hook_receiver.py +0 -20
- nautobot/extras/tests/example_jobs/test_location_with_custom_field.py +0 -35
- nautobot/extras/tests/example_jobs/test_log_redaction.py +0 -14
- nautobot/extras/tests/example_jobs/test_modify_db.py +0 -18
- nautobot/extras/tests/example_jobs/test_object_var_optional.py +0 -14
- nautobot/extras/tests/example_jobs/test_object_var_required.py +0 -14
- nautobot/extras/tests/example_jobs/test_object_vars.py +0 -29
- nautobot/extras/tests/example_jobs/test_pass.py +0 -19
- nautobot/extras/tests/example_jobs/test_read_only_fail.py +0 -24
- nautobot/extras/tests/example_jobs/test_read_only_no_commit_field.py +0 -10
- nautobot/extras/tests/example_jobs/test_read_only_pass.py +0 -22
- nautobot/ipam/api/nested_serializers.py +0 -159
- nautobot/ipam/migrations/0029_ipam__prefix__add_parent.py +0 -31
- nautobot/ipam/migrations/0030_ipam__prefix__data_migration.py +0 -13
- nautobot/ipam/migrations/0031_ipam__ipaddress__add_parent.py +0 -41
- nautobot/ipam/migrations/0032_ipam__ipaddress__data_migration.py +0 -11
- nautobot/tenancy/api/nested_serializers.py +0 -31
- nautobot/users/api/nested_serializers.py +0 -67
- nautobot/virtualization/api/nested_serializers.py +0 -65
- /nautobot/extras/{tests/example_jobs → test_jobs}/__init__.py +0 -0
- /nautobot/{dcim/models/sites.py → ipam/management/__init__.py} +0 -0
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.0.0a3.dist-info → nautobot-2.0.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import collections
|
|
2
|
+
|
|
3
|
+
from django.core.exceptions import ValidationError
|
|
4
|
+
from django.db import models
|
|
5
|
+
import netaddr
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
BASE_NAME = "Cleanup Namespace"
|
|
9
|
+
DESCRIPTION = "Created by Nautobot 2.0 IPAM data migrations."
|
|
10
|
+
GLOBAL_NS = "Global"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def process_namespaces(apps, schema_editor):
|
|
14
|
+
print("\n", end="")
|
|
15
|
+
|
|
16
|
+
# Fail if any interface or vm interface has IPs with different VRFs
|
|
17
|
+
check_interface_vrfs(apps)
|
|
18
|
+
|
|
19
|
+
# Prefix Broadcast is a derived field, so we should update it before we start
|
|
20
|
+
ensure_correct_prefix_broadcast(apps)
|
|
21
|
+
|
|
22
|
+
# Cleanup Prefixes and IPAddresses version fields
|
|
23
|
+
add_prefix_and_ip_address_version(apps)
|
|
24
|
+
|
|
25
|
+
# VRFs
|
|
26
|
+
process_vrfs(apps)
|
|
27
|
+
|
|
28
|
+
# IPAddresses
|
|
29
|
+
process_ip_addresses(apps)
|
|
30
|
+
|
|
31
|
+
# Prefixes
|
|
32
|
+
process_prefix_duplicates(apps)
|
|
33
|
+
reparent_prefixes(apps)
|
|
34
|
+
|
|
35
|
+
# Make another pass across all VRFs to duplicate it if it has prefixes
|
|
36
|
+
# in another namespace (non-unique VRFs with duplicate Prefixes)
|
|
37
|
+
copy_vrfs_to_cleanup_namespaces(apps)
|
|
38
|
+
|
|
39
|
+
# [VM]Interfaces
|
|
40
|
+
process_interfaces(apps)
|
|
41
|
+
|
|
42
|
+
# VRF-Prefix M2M
|
|
43
|
+
process_vrfs_prefixes_m2m(apps)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_interface_vrfs(apps):
|
|
47
|
+
"""
|
|
48
|
+
Enumerate all Interface and VMInterface objects and raise an exception if any interface is found that is associated
|
|
49
|
+
to more than one distinct VRF through the ip_address many-to-many relationship.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
apps: Django apps module
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
None
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
Interface = apps.get_model("dcim", "Interface")
|
|
59
|
+
VMInterface = apps.get_model("virtualization", "VMInterface")
|
|
60
|
+
|
|
61
|
+
interfaces_with_multiple_vrfs = Interface.objects.annotate(vrf_count=models.Count("ip_addresses__vrf")).filter(
|
|
62
|
+
vrf_count__gt=1
|
|
63
|
+
)
|
|
64
|
+
vm_interfaces_with_multiple_vrfs = VMInterface.objects.annotate(vrf_count=models.Count("ip_addresses__vrf")).filter(
|
|
65
|
+
vrf_count__gt=1
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if interfaces_with_multiple_vrfs.exists() or vm_interfaces_with_multiple_vrfs.exists():
|
|
69
|
+
raise ValidationError(
|
|
70
|
+
"You cannot migrate Interfaces or VMInterfaces that have IPs with differing VRFs.",
|
|
71
|
+
list(interfaces_with_multiple_vrfs),
|
|
72
|
+
list(vm_interfaces_with_multiple_vrfs),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def process_vrfs(apps):
|
|
77
|
+
"""
|
|
78
|
+
Enumerate all VRF objects in the database and attempt to find suitable Namespace with no duplicate VRF names and
|
|
79
|
+
no duplicate Prefixes associated to the VRF. Any VRF with `enforce_unique` set and has related prefixes will be
|
|
80
|
+
moved to its own Namespace. All other VRFs will be checked for duplicate names and Prefixes and moved to a cleanup
|
|
81
|
+
Namespace if any duplicates are found.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
apps: Django apps module
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
None
|
|
88
|
+
"""
|
|
89
|
+
VRF = apps.get_model("ipam", "VRF")
|
|
90
|
+
vrfs = VRF.objects.all().order_by("name", "rd")
|
|
91
|
+
unique_non_empty_vrfs = vrfs.filter(enforce_unique=True).exclude(ip_addresses__isnull=True, prefixes__isnull=True)
|
|
92
|
+
# At the point in the migration where we iterate through vrfs in global_ns_vrfs, every vrf that
|
|
93
|
+
# has already been processed has been moved to a new namespace. Anything left in the global
|
|
94
|
+
# namespace has yet to be processed which is why we're iterating through this on the second
|
|
95
|
+
# loop.
|
|
96
|
+
global_ns_vrfs = vrfs.filter(namespace__name=GLOBAL_NS)
|
|
97
|
+
|
|
98
|
+
# Case 0: VRFs with enforce_unique move to their own Namespace.
|
|
99
|
+
for vrf in unique_non_empty_vrfs:
|
|
100
|
+
print(f">>> Processing migration for VRF {vrf.name!r}, Namespace {vrf.namespace.name!r}")
|
|
101
|
+
vrf.namespace = create_vrf_namespace(apps, vrf)
|
|
102
|
+
vrf.save()
|
|
103
|
+
vrf.prefixes.update(namespace=vrf.namespace)
|
|
104
|
+
print(f" VRF {vrf.name!r} migrated to Namespace {vrf.namespace.name!r}")
|
|
105
|
+
|
|
106
|
+
# Case 00: VRFs with duplicate names or prefixes move to a Cleanup Namespace.
|
|
107
|
+
# Case 1 is not included here because it is a no-op.
|
|
108
|
+
for vrf in global_ns_vrfs.annotate(prefix_count=models.Count("prefixes")).order_by("-prefix_count"):
|
|
109
|
+
print(f">>> Processing migration for VRF {vrf.name!r}, Namespace {vrf.namespace.name!r}")
|
|
110
|
+
original_namespace = vrf.namespace
|
|
111
|
+
vrf.namespace = get_next_vrf_cleanup_namespace(apps, vrf)
|
|
112
|
+
vrf.save()
|
|
113
|
+
if vrf.namespace != original_namespace:
|
|
114
|
+
vrf.prefixes.update(namespace=vrf.namespace)
|
|
115
|
+
print(f" VRF {vrf.name!r} migrated from Namespace {original_namespace.name!r} to {vrf.namespace.name!r}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def add_prefix_and_ip_address_version(apps):
|
|
119
|
+
"""
|
|
120
|
+
Enumerate all Prefix and IPAddress objects in the database and populate the ip_version field.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
apps: Django apps module
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
None
|
|
127
|
+
"""
|
|
128
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
129
|
+
IPAddress = apps.get_model("ipam", "IPAddress")
|
|
130
|
+
|
|
131
|
+
print(">>> Populating Prefix.ip_version field")
|
|
132
|
+
for pfx in Prefix.objects.all():
|
|
133
|
+
cidr = validate_cidr(apps, pfx)
|
|
134
|
+
pfx.ip_version = cidr.version
|
|
135
|
+
pfx.save()
|
|
136
|
+
|
|
137
|
+
print(">>> Populating IPAddress.ip_version field")
|
|
138
|
+
for ip in IPAddress.objects.all():
|
|
139
|
+
cidr = validate_cidr(apps, ip)
|
|
140
|
+
ip.ip_version = cidr.version
|
|
141
|
+
ip.save()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def process_ip_addresses(apps):
|
|
145
|
+
"""
|
|
146
|
+
Enumerate collected IPs and parent them.
|
|
147
|
+
|
|
148
|
+
- For IPs with found parents: Set that parent and save the `IPAddress`.
|
|
149
|
+
- For orphaned IPs (missing parents):
|
|
150
|
+
- Generate a network from the `IPAddress`
|
|
151
|
+
- Get or create the parent `Prefix`
|
|
152
|
+
- Set that as the parent and save the `IPAddress`
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
apps: Django apps module
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
None
|
|
159
|
+
"""
|
|
160
|
+
# Find the correct namespace for each IPAddress and move it if necessary.
|
|
161
|
+
IPAddress = apps.get_model("ipam", "IPAddress")
|
|
162
|
+
Namespace = apps.get_model("ipam", "Namespace")
|
|
163
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
164
|
+
|
|
165
|
+
# Explicitly set the parent for those that were found and save them.
|
|
166
|
+
for ip in IPAddress.objects.filter(parent__isnull=True).order_by("-vrf", "-tenant"):
|
|
167
|
+
potential_parents = get_closest_parent(apps, ip, Prefix.objects.all())
|
|
168
|
+
for prefix in potential_parents:
|
|
169
|
+
if not prefix.ip_addresses.filter(host=ip.host).exists():
|
|
170
|
+
ip.parent = prefix
|
|
171
|
+
ip.save()
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
# For IPs with no discovered parent, create one and assign it to the IP.
|
|
175
|
+
global_ns = Namespace.objects.get(name=GLOBAL_NS)
|
|
176
|
+
for orphaned_ip in IPAddress.objects.filter(parent__isnull=True):
|
|
177
|
+
ip_repr = str(validate_cidr(apps, orphaned_ip))
|
|
178
|
+
print(f">>> Processing Parent migration for orphaned IPAddress {ip_repr!r}")
|
|
179
|
+
|
|
180
|
+
new_parent_cidr = generate_parent_prefix(apps, orphaned_ip)
|
|
181
|
+
network = new_parent_cidr.network
|
|
182
|
+
prefix_length = new_parent_cidr.prefixlen
|
|
183
|
+
potential_parents = Prefix.objects.filter(network=network, prefix_length=prefix_length).exclude(
|
|
184
|
+
ip_addresses__host=orphaned_ip.host
|
|
185
|
+
)
|
|
186
|
+
if potential_parents.exists():
|
|
187
|
+
new_parent = potential_parents.first()
|
|
188
|
+
|
|
189
|
+
else:
|
|
190
|
+
broadcast = new_parent_cidr[-1]
|
|
191
|
+
# This can result in duplicate Prefixes being created in the global_ns but that will be
|
|
192
|
+
# cleaned up subsequently in `process_prefix_duplicates`.
|
|
193
|
+
new_parent = Prefix.objects.create(
|
|
194
|
+
ip_version=orphaned_ip.ip_version,
|
|
195
|
+
network=network,
|
|
196
|
+
broadcast=broadcast,
|
|
197
|
+
tenant=orphaned_ip.tenant,
|
|
198
|
+
vrf=orphaned_ip.vrf,
|
|
199
|
+
prefix_length=prefix_length,
|
|
200
|
+
namespace=orphaned_ip.vrf.namespace if orphaned_ip.vrf else global_ns,
|
|
201
|
+
description=DESCRIPTION,
|
|
202
|
+
)
|
|
203
|
+
orphaned_ip.parent = new_parent
|
|
204
|
+
orphaned_ip.save()
|
|
205
|
+
|
|
206
|
+
parent_repr = str(validate_cidr(apps, new_parent))
|
|
207
|
+
print(
|
|
208
|
+
f" IPAddress {ip_repr!r} migrated to Parent Prefix {parent_repr!r} in Namespace {new_parent.namespace.name!r}"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# By this point we should arrive at NO orphaned IPAddress objects.
|
|
212
|
+
if IPAddress.objects.filter(parent__isnull=True).exists():
|
|
213
|
+
raise SystemExit("Unexpected orphaned IPAddress objects found.")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def process_prefix_duplicates(apps):
|
|
217
|
+
"""
|
|
218
|
+
Enumerate all Prefix objects in the database and attempt to find suitable Namespace with no Prefixes with duplicate
|
|
219
|
+
network and prefix length. Duplicate prefixes will be moved to a cleanup Namespace if any duplicates are found.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
apps: Django apps module
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
None
|
|
226
|
+
"""
|
|
227
|
+
Namespace = apps.get_model("ipam", "Namespace")
|
|
228
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
229
|
+
global_namespace = Namespace.objects.get(name=GLOBAL_NS)
|
|
230
|
+
|
|
231
|
+
namespaces = list(Namespace.objects.all())
|
|
232
|
+
# Always start with the Global Namespace.
|
|
233
|
+
namespaces.remove(global_namespace)
|
|
234
|
+
namespaces.insert(0, global_namespace)
|
|
235
|
+
|
|
236
|
+
for ns in namespaces:
|
|
237
|
+
dupe_prefixes = find_duplicate_prefixes(apps, ns)
|
|
238
|
+
|
|
239
|
+
# process tenants in order of number of related prefixes (fewest first)
|
|
240
|
+
tenant_ids_sorted = (
|
|
241
|
+
Prefix.objects.filter(namespace=ns)
|
|
242
|
+
.values("tenant")
|
|
243
|
+
.annotate(tenant_count=models.Count("tenant"))
|
|
244
|
+
.order_by("tenant_count")
|
|
245
|
+
.values_list("tenant", flat=True)
|
|
246
|
+
)
|
|
247
|
+
for dupe in dupe_prefixes:
|
|
248
|
+
print(f">>> Processing Namespace migration for duplicate Prefix {dupe!r}")
|
|
249
|
+
network, prefix_length = dupe.split("/")
|
|
250
|
+
objects = Prefix.objects.filter(network=network, prefix_length=prefix_length, namespace=ns)
|
|
251
|
+
# Leave the last instance of the Prefix in the original Namespace
|
|
252
|
+
last_prefix = objects.filter(tenant_id=tenant_ids_sorted.last()).last()
|
|
253
|
+
|
|
254
|
+
for tenant_id in tenant_ids_sorted:
|
|
255
|
+
for _, prefix in enumerate(objects):
|
|
256
|
+
if prefix == last_prefix or prefix.tenant_id != tenant_id:
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
namespace_base_name = BASE_NAME
|
|
260
|
+
if prefix.tenant is not None:
|
|
261
|
+
namespace_base_name += f" {prefix.tenant.name}"
|
|
262
|
+
prefix.namespace = get_next_prefix_cleanup_namespace(apps, prefix, base_name=namespace_base_name)
|
|
263
|
+
prefix.save()
|
|
264
|
+
print(
|
|
265
|
+
f" Prefix {dupe!r} migrated from Namespace {ns.name} to Namespace {prefix.namespace.name!r}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def reparent_prefixes(apps):
|
|
270
|
+
"""
|
|
271
|
+
Enumerate all Prefix objects in the database and attempt to find parent Prefix objects in the same Namespace.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
apps: Django apps module
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
None
|
|
278
|
+
"""
|
|
279
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
280
|
+
|
|
281
|
+
print("\n>>> Processing Prefix parents, please standby...")
|
|
282
|
+
for pfx in Prefix.objects.all().order_by("-prefix_length", "tenant"):
|
|
283
|
+
try:
|
|
284
|
+
parent = get_closest_parent(apps, pfx, pfx.namespace.prefixes.all())
|
|
285
|
+
if pfx.namespace != parent.namespace:
|
|
286
|
+
raise ValidationError("Prefix and parent are in different Namespaces")
|
|
287
|
+
print(f">>> {pfx.network}/{pfx.prefix_length} parent: {parent.network}/{parent.prefix_length}")
|
|
288
|
+
pfx.parent = parent
|
|
289
|
+
pfx.save()
|
|
290
|
+
except Prefix.DoesNotExist:
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def copy_vrfs_to_cleanup_namespaces(apps):
|
|
295
|
+
"""
|
|
296
|
+
Enumerate every Prefix with a non-null vrf and if the vrf namespace doesn't match the prefix namespace, make
|
|
297
|
+
a copy of the vrf in the cleanup namespace.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
apps: Django apps module
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
None
|
|
304
|
+
"""
|
|
305
|
+
|
|
306
|
+
IPAddress = apps.get_model("ipam", "IPAddress")
|
|
307
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
308
|
+
VRF = apps.get_model("ipam", "VRF")
|
|
309
|
+
Namespace = apps.get_model("ipam", "Namespace")
|
|
310
|
+
|
|
311
|
+
for vrf in VRF.objects.all():
|
|
312
|
+
if not vrf.prefixes.exclude(namespace=vrf.namespace).exists():
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
namespaces = (
|
|
316
|
+
vrf.prefixes.exclude(namespace=vrf.namespace).order_by().values_list("namespace", flat=True).distinct()
|
|
317
|
+
)
|
|
318
|
+
for namespace_pk in namespaces:
|
|
319
|
+
namespace = Namespace.objects.get(pk=namespace_pk)
|
|
320
|
+
print(f">>> Copying VRF {vrf.name!r} to namespace {namespace.name!r}")
|
|
321
|
+
vrf_copy = VRF.objects.create(
|
|
322
|
+
namespace=namespace,
|
|
323
|
+
name=vrf.name,
|
|
324
|
+
rd=vrf.rd,
|
|
325
|
+
tenant=vrf.tenant,
|
|
326
|
+
enforce_unique=vrf.enforce_unique,
|
|
327
|
+
_custom_field_data=vrf._custom_field_data,
|
|
328
|
+
description=DESCRIPTION,
|
|
329
|
+
)
|
|
330
|
+
vrf_copy.import_targets.set(vrf.import_targets.all())
|
|
331
|
+
vrf_copy.export_targets.set(vrf.export_targets.all())
|
|
332
|
+
Prefix.objects.filter(vrf=vrf, namespace_id=namespace_pk).update(vrf=vrf_copy)
|
|
333
|
+
IPAddress.objects.filter(vrf=vrf, parent__namespace_id=namespace_pk).update(vrf=vrf_copy)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def process_interfaces(apps):
|
|
337
|
+
"""
|
|
338
|
+
Process [VM]Interface objects.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
apps: Django apps module
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
None
|
|
345
|
+
"""
|
|
346
|
+
Interface = apps.get_model("dcim", "Interface")
|
|
347
|
+
VMInterface = apps.get_model("virtualization", "VMInterface")
|
|
348
|
+
VRFDeviceAssignment = apps.get_model("ipam", "VRFDeviceAssignment")
|
|
349
|
+
|
|
350
|
+
# Interfaces with vrfs
|
|
351
|
+
ip_interfaces = Interface.objects.filter(ip_addresses__vrf__isnull=False)
|
|
352
|
+
ip_vminterfaces = VMInterface.objects.filter(ip_addresses__vrf__isnull=False)
|
|
353
|
+
|
|
354
|
+
# Case 2: Interface has one or more IP address assigned to it with no more than 1 distinct associated VRF (none is excluded)
|
|
355
|
+
# The interface's VRF foreign key should be set to the VRF of any related IP Address with a non-null VRF.
|
|
356
|
+
# The interface's parent device or virtual machine should adopt an assocation to the VRF (VRFDeviceAssignment) as well.
|
|
357
|
+
for ifc in ip_interfaces:
|
|
358
|
+
print(f">>> Processing VRF migration for numbered Interface {ifc.name!r}")
|
|
359
|
+
# Set the Interface VRF to that of the first assigned IPAddress.
|
|
360
|
+
first_ip = ifc.ip_addresses.filter(vrf__isnull=False).first()
|
|
361
|
+
|
|
362
|
+
ifc_vrf = first_ip.vrf
|
|
363
|
+
ifc.vrf = ifc_vrf
|
|
364
|
+
ifc.save()
|
|
365
|
+
|
|
366
|
+
# Create the VRF-to-device assignment.
|
|
367
|
+
VRFDeviceAssignment.objects.get_or_create(vrf=ifc_vrf, device=ifc.device, rd=ifc_vrf.rd, name=ifc_vrf.name)
|
|
368
|
+
|
|
369
|
+
print(f" VRF {ifc_vrf.name!r} migrated from IPAddress {first_ip.host!r} to Interface {ifc.name!r}")
|
|
370
|
+
|
|
371
|
+
# VirtualMachine should adopt an association to the VRF (VRFDeviceAssignment) as well.
|
|
372
|
+
for ifc in ip_vminterfaces:
|
|
373
|
+
print(f">>> Processing VRF migration for numbered VMInterface {ifc.name!r}")
|
|
374
|
+
# Set the VMInterface VRF to that of the first assigned IPAddress.
|
|
375
|
+
first_ip = ifc.ip_addresses.filter(vrf__isnull=False).first()
|
|
376
|
+
|
|
377
|
+
ifc_vrf = first_ip.vrf
|
|
378
|
+
ifc.vrf = ifc_vrf
|
|
379
|
+
ifc.save()
|
|
380
|
+
|
|
381
|
+
# Create the VRF-to-device assignment.
|
|
382
|
+
VRFDeviceAssignment.objects.get_or_create(
|
|
383
|
+
vrf=ifc_vrf, virtual_machine=ifc.virtual_machine, rd=ifc_vrf.rd, name=ifc_vrf.name
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
print(f" VRF {ifc_vrf.name!r} migrated from IPAddress {first_ip.host!r} to VMInterface {ifc.name!r}")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def process_vrfs_prefixes_m2m(apps):
|
|
390
|
+
"""
|
|
391
|
+
Convert the Prefix -> VRF FK relationship to a M2M relationship.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
apps: Django apps module
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
None
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
VRF = apps.get_model("ipam", "VRF")
|
|
401
|
+
|
|
402
|
+
vrfs_with_prefixes = VRF.objects.filter(prefixes__isnull=False).order_by().distinct()
|
|
403
|
+
|
|
404
|
+
for vrf in vrfs_with_prefixes:
|
|
405
|
+
print(f" Converting Prefix relationships to VRF {vrf.name} to M2M.")
|
|
406
|
+
vrf.prefixes_m2m.set(vrf.prefixes.all())
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def get_prefixes(qs):
|
|
410
|
+
"""
|
|
411
|
+
Given a queryset, return the prefixes as 2-tuples of (network, prefix_length).
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
qs (QuerySet): QuerySet of Prefix objects.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
list
|
|
418
|
+
"""
|
|
419
|
+
return sorted(qs.values_list("network", "prefix_length"))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def compare_prefix_querysets(a, b):
|
|
423
|
+
"""
|
|
424
|
+
Compare two QuerySets of Prefix objects and return the set intersection of both.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
a (QuerySet): Left-side QuerySet
|
|
428
|
+
b (QuerySet): Right-side QuerySet
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
set(tuple)
|
|
432
|
+
"""
|
|
433
|
+
set_a = set(get_prefixes(a))
|
|
434
|
+
set_b = set(get_prefixes(b))
|
|
435
|
+
return set_a.intersection(set_b)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def create_vrf_namespace(apps, vrf):
|
|
439
|
+
"""
|
|
440
|
+
Given a VRF, get or create a unique "VRF Namespace" for it.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
apps: Django apps module
|
|
444
|
+
vrf (VRF): VRF instance
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Namespace
|
|
448
|
+
"""
|
|
449
|
+
Namespace = apps.get_model("ipam", "Namespace")
|
|
450
|
+
base_name = f"VRF Namespace {vrf.name}"
|
|
451
|
+
counter = 1
|
|
452
|
+
created = False
|
|
453
|
+
name = base_name
|
|
454
|
+
while not created:
|
|
455
|
+
ns, created = Namespace.objects.get_or_create(
|
|
456
|
+
name=name,
|
|
457
|
+
defaults={"description": DESCRIPTION},
|
|
458
|
+
)
|
|
459
|
+
counter += 1
|
|
460
|
+
name = f"{base_name} ({counter})"
|
|
461
|
+
|
|
462
|
+
return ns
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def find_duplicate_prefixes(apps, namespace):
|
|
466
|
+
"""
|
|
467
|
+
Return a list of duplicate prefixes for a given Namespace.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
apps: Django apps module
|
|
471
|
+
namespace (Namespace): Namespace instance
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
list(str)
|
|
475
|
+
"""
|
|
476
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
477
|
+
prefixes = Prefix.objects.filter(namespace=namespace).values_list("network", "prefix_length")
|
|
478
|
+
counter = collections.Counter(prefixes)
|
|
479
|
+
dupes = [p for p, cnt in counter.most_common() if cnt > 1]
|
|
480
|
+
return [f"{network}/{prefix_length}" for network, prefix_length in dupes]
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def generate_parent_prefix(apps, address):
|
|
484
|
+
"""
|
|
485
|
+
For a given `address`, generate a containing parent network address.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
apps: Django apps module
|
|
489
|
+
address: Prefix/IPAddress instance or string
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
netaddr.IPNetwork
|
|
493
|
+
"""
|
|
494
|
+
cidr = validate_cidr(apps, address)
|
|
495
|
+
return cidr.cidr
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def get_closest_parent(apps, obj, qs):
|
|
499
|
+
"""
|
|
500
|
+
This is forklifted from `Prefix.objects.get_closest_parent()` so that it can safely be used in
|
|
501
|
+
migrations.
|
|
502
|
+
|
|
503
|
+
Return the closest matching parent Prefix for a `cidr` even if it doesn't exist in the database.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
obj: Prefix/IPAddress instance
|
|
507
|
+
qs (QuerySet): QuerySet of Prefix objects
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Prefix or a filtered queryset
|
|
511
|
+
"""
|
|
512
|
+
# Validate that it's a real CIDR
|
|
513
|
+
cidr = validate_cidr(apps, obj)
|
|
514
|
+
broadcast = str(cidr.broadcast or cidr.ip)
|
|
515
|
+
|
|
516
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
517
|
+
IPAddress = apps.get_model("ipam", "IPAddress")
|
|
518
|
+
|
|
519
|
+
# Prepare the queryset filter
|
|
520
|
+
lookup_kwargs = {
|
|
521
|
+
"ip_version": cidr.version,
|
|
522
|
+
"network__lte": cidr.network,
|
|
523
|
+
"broadcast__gte": broadcast,
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if isinstance(obj, Prefix):
|
|
527
|
+
lookup_kwargs["prefix_length__lt"] = cidr.prefixlen
|
|
528
|
+
qs = qs.exclude(id=obj.id)
|
|
529
|
+
else:
|
|
530
|
+
lookup_kwargs["prefix_length__lte"] = cidr.prefixlen
|
|
531
|
+
|
|
532
|
+
# Search for possible ancestors by network/prefix, returning them in
|
|
533
|
+
# reverse prefix length order, so that we can choose the first one.
|
|
534
|
+
possible_ancestors = (
|
|
535
|
+
qs.filter(**lookup_kwargs)
|
|
536
|
+
.annotate(
|
|
537
|
+
custom_sort_order=models.Case(
|
|
538
|
+
models.When(tenant=obj.tenant, vrf=obj.vrf, then=models.Value(1)),
|
|
539
|
+
models.When(tenant__isnull=True, vrf=obj.vrf, then=models.Value(2)),
|
|
540
|
+
models.When(tenant=obj.tenant, vrf__isnull=True, then=models.Value(3)),
|
|
541
|
+
models.When(vrf=obj.vrf, then=models.Value(4)),
|
|
542
|
+
models.When(tenant__isnull=True, vrf__isnull=True, then=models.Value(5)),
|
|
543
|
+
models.When(vrf__isnull=True, then=models.Value(6)),
|
|
544
|
+
default=models.Value(7),
|
|
545
|
+
)
|
|
546
|
+
)
|
|
547
|
+
.order_by("-prefix_length", "custom_sort_order")
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if isinstance(obj, IPAddress):
|
|
551
|
+
# IP should not fall back to less specific prefixes
|
|
552
|
+
if not possible_ancestors.exists():
|
|
553
|
+
return qs.none()
|
|
554
|
+
prefix_length = possible_ancestors.first().prefix_length
|
|
555
|
+
return possible_ancestors.filter(prefix_length=prefix_length)
|
|
556
|
+
|
|
557
|
+
# If we've got any matches, the first one is our closest parent.
|
|
558
|
+
try:
|
|
559
|
+
return possible_ancestors[0]
|
|
560
|
+
except IndexError:
|
|
561
|
+
raise Prefix.DoesNotExist(f"Could not determine parent Prefix for {cidr}")
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def get_next_vrf_cleanup_namespace(apps, vrf):
|
|
565
|
+
"""
|
|
566
|
+
Try to get the next available Cleanup Namespace based on `vrf` found in the "Global" Namespace.
|
|
567
|
+
|
|
568
|
+
The Global Namespace is always scanned first to check for duplicates. If none are found then the
|
|
569
|
+
Global Namespace will be returned, otherwise Cleanup Namespaces will be iterated until one
|
|
570
|
+
without a duplicate is found. If a Namespace without duplicates cannot be found, a new one will
|
|
571
|
+
be created.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
apps: Django apps module
|
|
575
|
+
vrf (VRF): VRF instance
|
|
576
|
+
|
|
577
|
+
Returns:
|
|
578
|
+
Namespace
|
|
579
|
+
"""
|
|
580
|
+
Namespace = apps.get_model("ipam", "Namespace")
|
|
581
|
+
VRF = apps.get_model("ipam", "VRF")
|
|
582
|
+
|
|
583
|
+
counter = 1
|
|
584
|
+
vrf_prefixes = vrf.prefixes.all()
|
|
585
|
+
|
|
586
|
+
global_ns = Namespace.objects.get(name=GLOBAL_NS)
|
|
587
|
+
global_ns_prefixes = global_ns.prefixes.exclude(vrf=vrf)
|
|
588
|
+
global_dupe_prefixes = compare_prefix_querysets(vrf_prefixes, global_ns_prefixes)
|
|
589
|
+
global_dupe_vrfs = VRF.objects.filter(namespace=global_ns, name=vrf.name).exclude(pk=vrf.pk).exists()
|
|
590
|
+
|
|
591
|
+
if global_dupe_prefixes:
|
|
592
|
+
print(f" VRF {vrf.name} has duplicate prefixes with NS {global_ns.name}")
|
|
593
|
+
|
|
594
|
+
if global_dupe_vrfs:
|
|
595
|
+
print(f" VRF {vrf.name} has duplicate VRF name with NS {global_ns.name}")
|
|
596
|
+
|
|
597
|
+
if not any([global_dupe_prefixes, global_dupe_vrfs]):
|
|
598
|
+
return global_ns
|
|
599
|
+
|
|
600
|
+
# Iterate non-enforce_unique VRFS
|
|
601
|
+
# - Compare duplicate prefixes for each VRF
|
|
602
|
+
# - If a VRF has duplicates, it moves to a new namespace
|
|
603
|
+
while True:
|
|
604
|
+
base_name = f"{BASE_NAME} ({counter})"
|
|
605
|
+
namespace, created = Namespace.objects.get_or_create(
|
|
606
|
+
name=base_name,
|
|
607
|
+
defaults={"description": DESCRIPTION},
|
|
608
|
+
)
|
|
609
|
+
if created:
|
|
610
|
+
return namespace
|
|
611
|
+
|
|
612
|
+
ns_prefixes = namespace.prefixes.exclude(vrf=vrf)
|
|
613
|
+
dupe_prefixes = compare_prefix_querysets(vrf_prefixes, ns_prefixes)
|
|
614
|
+
dupe_vrfs = VRF.objects.filter(namespace=namespace, name=vrf.name).exclude(pk=vrf.pk).exists()
|
|
615
|
+
|
|
616
|
+
if dupe_prefixes:
|
|
617
|
+
print(f" VRF {vrf.name} has duplicate prefixes with NS {namespace.name}")
|
|
618
|
+
|
|
619
|
+
if dupe_vrfs:
|
|
620
|
+
print(f" VRF {vrf.name} has duplicate VRF name with NS {namespace.name}")
|
|
621
|
+
|
|
622
|
+
if any([dupe_prefixes, dupe_vrfs]):
|
|
623
|
+
counter += 1
|
|
624
|
+
continue
|
|
625
|
+
|
|
626
|
+
return namespace
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def get_next_prefix_cleanup_namespace(apps, prefix, base_name=BASE_NAME):
|
|
630
|
+
"""
|
|
631
|
+
Try to ge the next avialable Cleanup Namespace based on `prefix` found in the "Global" Namespace.
|
|
632
|
+
|
|
633
|
+
It is implied that the Prefix will be in the Global Namespace, so Cleanup Namespaces are
|
|
634
|
+
automatically iterated to find a suitable match that has no duplicates. If a Namespace without
|
|
635
|
+
duplicates cannot be found, a new one will be created.
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
apps: Django apps module
|
|
639
|
+
prefix (Prefix): Prefix instance
|
|
640
|
+
base_name (str): Base name to use for the Namespace
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
Namespace
|
|
644
|
+
"""
|
|
645
|
+
Namespace = apps.get_model("ipam", "Namespace")
|
|
646
|
+
|
|
647
|
+
counter = 1
|
|
648
|
+
while True:
|
|
649
|
+
name = f"{base_name} ({counter})"
|
|
650
|
+
namespace, created = Namespace.objects.get_or_create(
|
|
651
|
+
name=name,
|
|
652
|
+
defaults={"description": DESCRIPTION},
|
|
653
|
+
)
|
|
654
|
+
if created:
|
|
655
|
+
return namespace
|
|
656
|
+
|
|
657
|
+
cidr = f"{prefix.network}/{prefix.prefix_length}"
|
|
658
|
+
has_dupe = namespace.prefixes.filter(network=prefix.network, prefix_length=prefix.prefix_length).exists()
|
|
659
|
+
|
|
660
|
+
if has_dupe:
|
|
661
|
+
print(f" Prefix {cidr} is duplicated in NS {namespace.name}")
|
|
662
|
+
counter += 1
|
|
663
|
+
continue
|
|
664
|
+
|
|
665
|
+
return namespace
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def validate_cidr(apps, value):
|
|
669
|
+
"""
|
|
670
|
+
Validate whether `value` is a valid IPv4/IPv6 CIDR.
|
|
671
|
+
|
|
672
|
+
Args:
|
|
673
|
+
value (str): IP address
|
|
674
|
+
|
|
675
|
+
Returns:
|
|
676
|
+
netaddr.IPNetwork
|
|
677
|
+
"""
|
|
678
|
+
IPAddress = apps.get_model("ipam", "IPAddress")
|
|
679
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
680
|
+
|
|
681
|
+
if isinstance(value, IPAddress):
|
|
682
|
+
value = f"{value.host}/{value.prefix_length}"
|
|
683
|
+
elif isinstance(value, Prefix):
|
|
684
|
+
value = f"{value.network}/{value.prefix_length}"
|
|
685
|
+
else:
|
|
686
|
+
value = str(value)
|
|
687
|
+
|
|
688
|
+
try:
|
|
689
|
+
return netaddr.IPNetwork(value)
|
|
690
|
+
except netaddr.AddrFormatError as err:
|
|
691
|
+
raise ValidationError({"cidr": f"{value} does not appear to be an IPv4 or IPv6 network."}) from err
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def ensure_correct_prefix_broadcast(apps):
|
|
695
|
+
"""
|
|
696
|
+
Ensure that the prefix broadcast address is correct.
|
|
697
|
+
|
|
698
|
+
Args:
|
|
699
|
+
apps: Django apps module
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
None
|
|
703
|
+
"""
|
|
704
|
+
Prefix = apps.get_model("ipam", "Prefix")
|
|
705
|
+
|
|
706
|
+
for prefix in Prefix.objects.all():
|
|
707
|
+
true_broadcast = str(netaddr.IPNetwork(f"{prefix.network}/{prefix.prefix_length}")[-1])
|
|
708
|
+
if prefix.broadcast != true_broadcast:
|
|
709
|
+
print(
|
|
710
|
+
f"Updating {prefix.network}/{prefix.prefix_length} broadcast from {prefix.broadcast} to {true_broadcast}"
|
|
711
|
+
)
|
|
712
|
+
prefix.broadcast = true_broadcast
|
|
713
|
+
prefix.save()
|