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
nautobot/extras/api/views.py
CHANGED
|
@@ -3,7 +3,7 @@ from django.conf import settings
|
|
|
3
3
|
from django.contrib.contenttypes.models import ContentType
|
|
4
4
|
from django.forms import ValidationError as FormsValidationError
|
|
5
5
|
from django.http import Http404
|
|
6
|
-
from django.shortcuts import get_object_or_404
|
|
6
|
+
from django.shortcuts import get_object_or_404, render
|
|
7
7
|
from django.utils import timezone
|
|
8
8
|
from drf_spectacular.types import OpenApiTypes
|
|
9
9
|
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
|
@@ -12,6 +12,7 @@ from graphql import GraphQLError
|
|
|
12
12
|
from rest_framework import status
|
|
13
13
|
from rest_framework.decorators import action
|
|
14
14
|
from rest_framework.exceptions import MethodNotAllowed, PermissionDenied, ValidationError
|
|
15
|
+
from rest_framework.filters import OrderingFilter
|
|
15
16
|
from rest_framework.parsers import JSONParser, MultiPartParser
|
|
16
17
|
from rest_framework.permissions import IsAuthenticated
|
|
17
18
|
from rest_framework.response import Response
|
|
@@ -20,7 +21,7 @@ from rest_framework import mixins, viewsets
|
|
|
20
21
|
|
|
21
22
|
from nautobot.core.api.authentication import TokenPermissions
|
|
22
23
|
from nautobot.core.api.filter_backends import NautobotFilterBackend
|
|
23
|
-
from nautobot.core.api.utils import get_serializer_for_model
|
|
24
|
+
from nautobot.core.api.utils import get_data_for_serializer_parameter, get_serializer_for_model
|
|
24
25
|
from nautobot.core.api.views import (
|
|
25
26
|
BulkDestroyModelMixin,
|
|
26
27
|
BulkUpdateModelMixin,
|
|
@@ -30,10 +31,9 @@ from nautobot.core.api.views import (
|
|
|
30
31
|
from nautobot.core.exceptions import CeleryWorkerNotRunningException
|
|
31
32
|
from nautobot.core.graphql import execute_saved_query
|
|
32
33
|
from nautobot.core.models.querysets import count_related
|
|
33
|
-
from nautobot.core.utils.
|
|
34
|
+
from nautobot.core.utils.lookup import get_table_for_model
|
|
34
35
|
from nautobot.extras import filters
|
|
35
|
-
from nautobot.extras.choices import JobExecutionType
|
|
36
|
-
from nautobot.extras.datasources import enqueue_pull_git_repository_and_refresh_data
|
|
36
|
+
from nautobot.extras.choices import JobExecutionType
|
|
37
37
|
from nautobot.extras.filters import RoleFilterSet
|
|
38
38
|
from nautobot.extras.models import (
|
|
39
39
|
ComputedField,
|
|
@@ -66,9 +66,9 @@ from nautobot.extras.models import (
|
|
|
66
66
|
Webhook,
|
|
67
67
|
)
|
|
68
68
|
from nautobot.extras.models import CustomField, CustomFieldChoice
|
|
69
|
-
from nautobot.extras.
|
|
70
|
-
from nautobot.extras.utils import
|
|
71
|
-
from . import
|
|
69
|
+
from nautobot.extras.secrets.exceptions import SecretError
|
|
70
|
+
from nautobot.extras.utils import get_worker_count
|
|
71
|
+
from . import serializers
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
class ExtrasRootView(APIRootView):
|
|
@@ -112,6 +112,84 @@ class NotesViewSetMixin:
|
|
|
112
112
|
return self.get_paginated_response(serializer.data)
|
|
113
113
|
|
|
114
114
|
|
|
115
|
+
# TODO: This is part of the drf-react-template work towards auto-generating create/edit form UI from the REST API.
|
|
116
|
+
# TODO: Why is this in extras instead of core?
|
|
117
|
+
class FormFieldsViewSetMixin:
|
|
118
|
+
"""TODO: docstring needed."""
|
|
119
|
+
|
|
120
|
+
# TODO: shouldn't this function generally be named "get_field_groups" not "get_field_group"?
|
|
121
|
+
def get_field_group(self):
|
|
122
|
+
return []
|
|
123
|
+
|
|
124
|
+
# TODO: schema doesn't *look* correct to me based on a reading of the below code. Should this even be in the schema?
|
|
125
|
+
@extend_schema(
|
|
126
|
+
responses={
|
|
127
|
+
200: {
|
|
128
|
+
"type": "array",
|
|
129
|
+
"items": {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"properties": {
|
|
132
|
+
"type": {"type": "string"},
|
|
133
|
+
"label": {"type": "string"},
|
|
134
|
+
"required": {"type": "string"},
|
|
135
|
+
"help_text": {"type": "string"},
|
|
136
|
+
"choices": {"type": "array", "items": {"type": "array"}},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
@action(detail=False, url_path="form-fields", methods=["get"])
|
|
143
|
+
def form_fields(self, request):
|
|
144
|
+
groups = self.get_field_group()
|
|
145
|
+
model = self.queryset.model
|
|
146
|
+
fields = get_data_for_serializer_parameter(model)
|
|
147
|
+
if groups:
|
|
148
|
+
data = {
|
|
149
|
+
group_name: [fields.get(field) for field in group_fields] for group_name, group_fields in groups.items()
|
|
150
|
+
}
|
|
151
|
+
else:
|
|
152
|
+
model_name = model._meta.model_name
|
|
153
|
+
data = {model_name.capitalize(): fields.values()}
|
|
154
|
+
|
|
155
|
+
return Response(data)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# TODO: This is part of the drf-react-template work towards auto-generating create/edit form UI from the REST API.
|
|
159
|
+
# TODO: Why is this in extras instead of core?
|
|
160
|
+
class TableFieldsViewSetMixin:
|
|
161
|
+
"""TODO: docstring needed."""
|
|
162
|
+
|
|
163
|
+
# TODO: this schema is definitely incorrect. Should this view even be in the schema?
|
|
164
|
+
@extend_schema(
|
|
165
|
+
responses={
|
|
166
|
+
200: {
|
|
167
|
+
"type": "array",
|
|
168
|
+
"items": {
|
|
169
|
+
"type": "object",
|
|
170
|
+
"properties": {
|
|
171
|
+
"type": {"type": "string"},
|
|
172
|
+
"label": {"type": "string"},
|
|
173
|
+
"required": {"type": "string"},
|
|
174
|
+
"help_text": {"type": "string"},
|
|
175
|
+
"choices": {"type": "array", "items": {"type": "array"}},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
@action(detail=False, url_path="table-fields", methods=["get"])
|
|
182
|
+
def table_fields(self, request):
|
|
183
|
+
table = get_table_for_model(self.queryset.model)
|
|
184
|
+
table_instance = table(user=request.user, data=[])
|
|
185
|
+
data = [
|
|
186
|
+
{"name": item[0], "label": item[1]}
|
|
187
|
+
for item in table_instance.configurable_columns
|
|
188
|
+
if item[0] in table_instance.visible_columns
|
|
189
|
+
]
|
|
190
|
+
return Response({"data": data})
|
|
191
|
+
|
|
192
|
+
|
|
115
193
|
#
|
|
116
194
|
# Computed Fields
|
|
117
195
|
#
|
|
@@ -155,20 +233,20 @@ class ConfigContextQuerySetMixin:
|
|
|
155
233
|
data annotation or not.
|
|
156
234
|
"""
|
|
157
235
|
|
|
158
|
-
filter_backends = [ConfigContextFilterBackend]
|
|
236
|
+
filter_backends = [ConfigContextFilterBackend, OrderingFilter]
|
|
159
237
|
|
|
160
238
|
def get_queryset(self):
|
|
161
239
|
"""
|
|
162
240
|
Build the proper queryset based on the request context
|
|
163
241
|
|
|
164
|
-
If the `
|
|
242
|
+
If the `exclude` query param
|
|
165
243
|
includes `config_context` as a value, return the base queryset.
|
|
166
244
|
|
|
167
245
|
Else, return the queryset annotated with config context data
|
|
168
246
|
"""
|
|
169
247
|
queryset = super().get_queryset()
|
|
170
248
|
request = self.get_serializer_context()["request"]
|
|
171
|
-
if
|
|
249
|
+
if request is not None and "config_context" in request.query_params.get("exclude", []):
|
|
172
250
|
return queryset
|
|
173
251
|
return queryset.annotate_config_context_data()
|
|
174
252
|
|
|
@@ -249,12 +327,23 @@ class CustomFieldModelViewSet(ModelViewSet):
|
|
|
249
327
|
return context
|
|
250
328
|
|
|
251
329
|
|
|
252
|
-
class NautobotModelViewSet(CustomFieldModelViewSet, NotesViewSetMixin):
|
|
330
|
+
class NautobotModelViewSet(CustomFieldModelViewSet, NotesViewSetMixin, FormFieldsViewSetMixin, TableFieldsViewSetMixin):
|
|
253
331
|
"""Base class to use for API ViewSets based on OrganizationalModel or PrimaryModel.
|
|
254
332
|
|
|
255
333
|
Can also be used for models derived from BaseModel, so long as they support Notes.
|
|
256
334
|
"""
|
|
257
335
|
|
|
336
|
+
# TODO: this currently throws a 500 error in drf_react_template because it's returning an HttpResponse but
|
|
337
|
+
# drf_react_template thinks it's a REST endpoint that should be returning a JsonResponse
|
|
338
|
+
@action(detail=True, url_path="app_full_width_fragment")
|
|
339
|
+
def app_full_width_fragment(self, request, pk):
|
|
340
|
+
"""
|
|
341
|
+
Return html fragment from a plugin.
|
|
342
|
+
"""
|
|
343
|
+
obj = get_object_or_404(self.queryset, pk=pk)
|
|
344
|
+
|
|
345
|
+
return render(request, "generic/object_retrieve_plugin_full_width.html", {"object": obj})
|
|
346
|
+
|
|
258
347
|
|
|
259
348
|
#
|
|
260
349
|
# Custom Links
|
|
@@ -349,7 +438,7 @@ class GitRepositoryViewSet(NautobotModelViewSet):
|
|
|
349
438
|
raise CeleryWorkerNotRunningException()
|
|
350
439
|
|
|
351
440
|
repository = get_object_or_404(GitRepository, id=pk)
|
|
352
|
-
|
|
441
|
+
repository.sync(user=request.user)
|
|
353
442
|
return Response({"message": f"Repository {repository} sync job added to queue."})
|
|
354
443
|
|
|
355
444
|
|
|
@@ -397,25 +486,16 @@ class ImageAttachmentViewSet(ModelViewSet):
|
|
|
397
486
|
#
|
|
398
487
|
|
|
399
488
|
|
|
400
|
-
def _create_schedule(serializer, data,
|
|
489
|
+
def _create_schedule(serializer, data, job_model, user, approval_required, task_queue=None):
|
|
401
490
|
"""
|
|
402
491
|
This is an internal function to create a scheduled job from API data.
|
|
403
492
|
It has to handle both once-offs (i.e. of type TYPE_FUTURE) and interval
|
|
404
493
|
jobs.
|
|
405
494
|
"""
|
|
406
|
-
task_kwargs = {
|
|
407
|
-
"data": data,
|
|
408
|
-
"request": copy_safe_request(request),
|
|
409
|
-
"user": request.user.pk,
|
|
410
|
-
"commit": commit,
|
|
411
|
-
"name": job.class_path,
|
|
412
|
-
"celery_kwargs": celery_kwargs,
|
|
413
|
-
"task_queue": task_queue,
|
|
414
|
-
}
|
|
415
495
|
type_ = serializer["interval"]
|
|
416
496
|
if type_ == JobExecutionType.TYPE_IMMEDIATELY:
|
|
417
497
|
time = timezone.now()
|
|
418
|
-
name = serializer.get("name") or f"{
|
|
498
|
+
name = serializer.get("name") or f"{job_model.name} - {time}"
|
|
419
499
|
elif type_ == JobExecutionType.TYPE_CUSTOM:
|
|
420
500
|
time = serializer.get("start_time") # doing .get("key", "default") returns None instead of "default"
|
|
421
501
|
if time is None:
|
|
@@ -428,6 +508,11 @@ def _create_schedule(serializer, data, commit, job, job_model, request, celery_k
|
|
|
428
508
|
name = serializer["name"]
|
|
429
509
|
crontab = serializer.get("crontab", "")
|
|
430
510
|
|
|
511
|
+
celery_kwargs = {
|
|
512
|
+
"nautobot_job_profile": False,
|
|
513
|
+
"queue": task_queue,
|
|
514
|
+
}
|
|
515
|
+
|
|
431
516
|
# 2.0 TODO: To revisit this as part of a larger Jobs cleanup in 2.0.
|
|
432
517
|
#
|
|
433
518
|
# We pass in job_class and job_model here partly for forward/backward compatibility logic, and
|
|
@@ -437,16 +522,17 @@ def _create_schedule(serializer, data, commit, job, job_model, request, celery_k
|
|
|
437
522
|
# scheduled for.
|
|
438
523
|
scheduled_job = ScheduledJob(
|
|
439
524
|
name=name,
|
|
440
|
-
task=
|
|
441
|
-
job_class=
|
|
525
|
+
task=job_model.job_class.registered_name,
|
|
526
|
+
job_class=job_model.class_path,
|
|
442
527
|
job_model=job_model,
|
|
443
528
|
start_time=time,
|
|
444
|
-
description=f"Nautobot job {name} scheduled by {
|
|
445
|
-
kwargs=
|
|
529
|
+
description=f"Nautobot job {name} scheduled by {user} for {time}",
|
|
530
|
+
kwargs=data,
|
|
531
|
+
celery_kwargs=celery_kwargs,
|
|
446
532
|
interval=type_,
|
|
447
533
|
one_off=(type_ == JobExecutionType.TYPE_FUTURE),
|
|
448
|
-
user=
|
|
449
|
-
approval_required=
|
|
534
|
+
user=user,
|
|
535
|
+
approval_required=approval_required,
|
|
450
536
|
crontab=crontab,
|
|
451
537
|
queue=task_queue,
|
|
452
538
|
)
|
|
@@ -454,178 +540,6 @@ def _create_schedule(serializer, data, commit, job, job_model, request, celery_k
|
|
|
454
540
|
return scheduled_job
|
|
455
541
|
|
|
456
542
|
|
|
457
|
-
def _run_job(request, job_model, legacy_response=False):
|
|
458
|
-
"""An internal function providing logic shared between JobModelViewSet.run() and JobViewSet.run()."""
|
|
459
|
-
if not request.user.has_perm("extras.run_job"):
|
|
460
|
-
raise PermissionDenied("This user does not have permission to run jobs.")
|
|
461
|
-
if not job_model.enabled:
|
|
462
|
-
raise PermissionDenied("This job is not enabled to be run.")
|
|
463
|
-
if not job_model.installed:
|
|
464
|
-
raise MethodNotAllowed(request.method, detail="This job is not presently installed and cannot be run")
|
|
465
|
-
if job_model.has_sensitive_variables:
|
|
466
|
-
if (
|
|
467
|
-
"schedule" in request.data
|
|
468
|
-
and "interval" in request.data["schedule"]
|
|
469
|
-
and request.data["schedule"]["interval"] != JobExecutionType.TYPE_IMMEDIATELY
|
|
470
|
-
):
|
|
471
|
-
raise ValidationError(
|
|
472
|
-
{"schedule": {"interval": ["Unable to schedule job: Job may have sensitive input variables"]}}
|
|
473
|
-
)
|
|
474
|
-
if job_model.approval_required:
|
|
475
|
-
raise ValidationError(
|
|
476
|
-
"Unable to run or schedule job: "
|
|
477
|
-
"This job is flagged as possibly having sensitive variables but is also flagged as requiring approval."
|
|
478
|
-
"One of these two flags must be removed before this job can be scheduled or run."
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
job_class = job_model.job_class
|
|
482
|
-
if job_class is None:
|
|
483
|
-
raise MethodNotAllowed(request.method, detail="This job's source code could not be located and cannot be run")
|
|
484
|
-
job = job_class()
|
|
485
|
-
|
|
486
|
-
valid_queues = job_model.task_queues if job_model.task_queues else [settings.CELERY_TASK_DEFAULT_QUEUE]
|
|
487
|
-
# Get a default queue from either the job model's specified task queue or system default to fall back on if request doesn't provide one
|
|
488
|
-
default_valid_queue = valid_queues[0]
|
|
489
|
-
|
|
490
|
-
# We need to call request.data for both cases as this is what pulls and caches the request data
|
|
491
|
-
data = request.data
|
|
492
|
-
files = None
|
|
493
|
-
schedule_data = None
|
|
494
|
-
|
|
495
|
-
# We must extract from the request:
|
|
496
|
-
# - Job Form data (for submission to the job itself)
|
|
497
|
-
# - Schedule data
|
|
498
|
-
# - Commit flag state
|
|
499
|
-
# - Desired task queue
|
|
500
|
-
# Depending on request content type (largely for backwards compatibility) the keys at which these are found are different
|
|
501
|
-
if "multipart/form-data" in request.content_type:
|
|
502
|
-
data = request._data.dict() # .data will return data and files, we just want the data
|
|
503
|
-
files = request.FILES
|
|
504
|
-
|
|
505
|
-
# JobMultiPartInputSerializer is a "flattened" version of JobInputSerializer
|
|
506
|
-
input_serializer = serializers.JobMultiPartInputSerializer(data=data, context={"request": request})
|
|
507
|
-
input_serializer.is_valid(raise_exception=True)
|
|
508
|
-
|
|
509
|
-
commit = input_serializer.validated_data.get("_commit", None)
|
|
510
|
-
task_queue = input_serializer.validated_data.get("_task_queue", default_valid_queue)
|
|
511
|
-
|
|
512
|
-
# JobMultiPartInputSerializer only has keys for executing job (commit, task_queue, etc),
|
|
513
|
-
# everything else is a candidate for the job form's data.
|
|
514
|
-
# job_class.validate_data will throw an error for any unexpected key/value pairs.
|
|
515
|
-
non_job_keys = input_serializer.validated_data.keys()
|
|
516
|
-
for non_job_key in non_job_keys:
|
|
517
|
-
data.pop(non_job_key, None)
|
|
518
|
-
|
|
519
|
-
# List of keys in serializer that are effectively exploded versions of the schedule dictionary from JobInputSerializer
|
|
520
|
-
schedule_keys = ("_schedule_name", "_schedule_start_time", "_schedule_interval", "_schedule_crontab")
|
|
521
|
-
|
|
522
|
-
# Assign the key from the validated_data output to dictionary without prefixed "_schedule_"
|
|
523
|
-
# For all the keys that are schedule keys
|
|
524
|
-
# Assign only if the key is in the output since we don't want None's if not provided
|
|
525
|
-
if any(schedule_key in non_job_keys for schedule_key in schedule_keys):
|
|
526
|
-
schedule_data = {
|
|
527
|
-
k.replace("_schedule_", ""): input_serializer.validated_data[k]
|
|
528
|
-
for k in schedule_keys
|
|
529
|
-
if k in input_serializer.validated_data
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
else:
|
|
533
|
-
input_serializer = serializers.JobInputSerializer(data=data, context={"request": request})
|
|
534
|
-
input_serializer.is_valid(raise_exception=True)
|
|
535
|
-
|
|
536
|
-
data = input_serializer.validated_data.get("data", {})
|
|
537
|
-
commit = input_serializer.validated_data.get("commit", None)
|
|
538
|
-
task_queue = input_serializer.validated_data.get("task_queue", default_valid_queue)
|
|
539
|
-
schedule_data = input_serializer.validated_data.get("schedule", None)
|
|
540
|
-
|
|
541
|
-
if commit is None:
|
|
542
|
-
commit = job_model.commit_default
|
|
543
|
-
|
|
544
|
-
if task_queue not in valid_queues:
|
|
545
|
-
raise ValidationError({"task_queue": [f'"{task_queue}" is not a valid choice.']})
|
|
546
|
-
|
|
547
|
-
cleaned_data = None
|
|
548
|
-
try:
|
|
549
|
-
cleaned_data = job.validate_data(data, files=files)
|
|
550
|
-
cleaned_data.pop(
|
|
551
|
-
"_commit", None
|
|
552
|
-
) # We don't get commit from the form, instead it's part of the serializer's validated data
|
|
553
|
-
|
|
554
|
-
except FormsValidationError as e:
|
|
555
|
-
# message_dict can only be accessed if ValidationError got a dict
|
|
556
|
-
# in the constructor (saved as error_dict). Otherwise we get a list
|
|
557
|
-
# of errors under messages
|
|
558
|
-
return Response({"errors": e.message_dict if hasattr(e, "error_dict") else e.messages}, status=400)
|
|
559
|
-
|
|
560
|
-
if not get_worker_count(queue=task_queue):
|
|
561
|
-
raise CeleryWorkerNotRunningException(queue=task_queue)
|
|
562
|
-
|
|
563
|
-
job_content_type = get_job_content_type()
|
|
564
|
-
|
|
565
|
-
# Default to a null JobResult.
|
|
566
|
-
job_result = None
|
|
567
|
-
|
|
568
|
-
# Assert that a job with `approval_required=True` has a schedule that enforces approval and
|
|
569
|
-
# executes immediately.
|
|
570
|
-
if schedule_data is None and job_model.approval_required:
|
|
571
|
-
schedule_data = {"interval": JobExecutionType.TYPE_IMMEDIATELY}
|
|
572
|
-
|
|
573
|
-
# Skip creating a ScheduledJob when job can be executed immediately
|
|
574
|
-
elif (
|
|
575
|
-
schedule_data
|
|
576
|
-
and schedule_data["interval"] == JobExecutionType.TYPE_IMMEDIATELY
|
|
577
|
-
and not job_model.approval_required
|
|
578
|
-
):
|
|
579
|
-
schedule_data = None
|
|
580
|
-
|
|
581
|
-
# Try to create a ScheduledJob, or...
|
|
582
|
-
if schedule_data:
|
|
583
|
-
schedule = _create_schedule(
|
|
584
|
-
schedule_data,
|
|
585
|
-
job_class.serialize_data(cleaned_data),
|
|
586
|
-
commit,
|
|
587
|
-
job,
|
|
588
|
-
job_model,
|
|
589
|
-
request,
|
|
590
|
-
celery_kwargs={"queue": task_queue},
|
|
591
|
-
task_queue=input_serializer.validated_data.get("task_queue", None),
|
|
592
|
-
)
|
|
593
|
-
else:
|
|
594
|
-
schedule = None
|
|
595
|
-
|
|
596
|
-
# ... If we can't create one, create a JobResult instead.
|
|
597
|
-
if schedule is None:
|
|
598
|
-
job_result = JobResult.enqueue_job(
|
|
599
|
-
run_job,
|
|
600
|
-
job.class_path,
|
|
601
|
-
job_content_type,
|
|
602
|
-
request.user,
|
|
603
|
-
celery_kwargs={"queue": task_queue},
|
|
604
|
-
data=job_class.serialize_data(cleaned_data),
|
|
605
|
-
request=copy_safe_request(request),
|
|
606
|
-
commit=commit,
|
|
607
|
-
task_queue=input_serializer.validated_data.get("task_queue", None),
|
|
608
|
-
)
|
|
609
|
-
job.result = job_result
|
|
610
|
-
|
|
611
|
-
if legacy_response:
|
|
612
|
-
# Old-style JobViewSet response - serialize the Job class in the response for some reason?
|
|
613
|
-
serializer = serializers.JobClassDetailSerializer(job, context={"request": request})
|
|
614
|
-
return Response(serializer.data)
|
|
615
|
-
else:
|
|
616
|
-
# New-style JobModelViewSet response - serialize the schedule or job_result as appropriate
|
|
617
|
-
data = {"scheduled_job": None, "job_result": None}
|
|
618
|
-
if schedule:
|
|
619
|
-
data["scheduled_job"] = nested_serializers.NestedScheduledJobSerializer(
|
|
620
|
-
schedule, context={"request": request}
|
|
621
|
-
).data
|
|
622
|
-
if job_result:
|
|
623
|
-
data["job_result"] = nested_serializers.NestedJobResultSerializer(
|
|
624
|
-
job_result, context={"request": request}
|
|
625
|
-
).data
|
|
626
|
-
return Response(data, status=status.HTTP_201_CREATED)
|
|
627
|
-
|
|
628
|
-
|
|
629
543
|
class JobViewSet(
|
|
630
544
|
# DRF mixins:
|
|
631
545
|
# note no CreateModelMixin
|
|
@@ -642,43 +556,6 @@ class JobViewSet(
|
|
|
642
556
|
serializer_class = serializers.JobSerializer
|
|
643
557
|
filterset_class = filters.JobFilterSet
|
|
644
558
|
|
|
645
|
-
@extend_schema(
|
|
646
|
-
deprecated=True,
|
|
647
|
-
operation_id="extras_jobs_read_deprecated",
|
|
648
|
-
responses={"200": serializers.JobClassDetailSerializer()},
|
|
649
|
-
)
|
|
650
|
-
@action(
|
|
651
|
-
detail=False, # a /jobs/... URL, not a /jobs/<pk>/... URL
|
|
652
|
-
methods=["get"],
|
|
653
|
-
url_path="(?P<class_path>[^/]+/[^/]+/[^/]+)", # /api/extras/jobs/<class_path>/
|
|
654
|
-
url_name="detail",
|
|
655
|
-
)
|
|
656
|
-
def retrieve_deprecated(self, request, class_path):
|
|
657
|
-
"""
|
|
658
|
-
Get details of a Job as identified by its class-path.
|
|
659
|
-
|
|
660
|
-
This API endpoint is deprecated; it is recommended to use the extras_jobs_read endpoint instead.
|
|
661
|
-
"""
|
|
662
|
-
if not request.user.has_perm("extras.view_job"):
|
|
663
|
-
raise PermissionDenied("This user does not have permission to view jobs.")
|
|
664
|
-
try:
|
|
665
|
-
job_model = Job.objects.restrict(request.user, "view").get_for_class_path(class_path)
|
|
666
|
-
except Job.DoesNotExist:
|
|
667
|
-
raise Http404
|
|
668
|
-
if not job_model.installed or job_model.job_class is None:
|
|
669
|
-
raise Http404
|
|
670
|
-
job_content_type = get_job_content_type()
|
|
671
|
-
job = job_model.job_class() # TODO: why do we need to instantiate the job_class?
|
|
672
|
-
job.result = JobResult.objects.filter(
|
|
673
|
-
obj_type=job_content_type,
|
|
674
|
-
name=job.class_path,
|
|
675
|
-
status__in=JobResultStatusChoices.READY_STATES,
|
|
676
|
-
).first()
|
|
677
|
-
|
|
678
|
-
serializer = serializers.JobClassDetailSerializer(job, context={"request": request})
|
|
679
|
-
|
|
680
|
-
return Response(serializer.data)
|
|
681
|
-
|
|
682
559
|
@extend_schema(responses={"200": serializers.JobVariableSerializer(many=True)})
|
|
683
560
|
@action(detail=True, filterset_class=None)
|
|
684
561
|
def variables(self, request, pk):
|
|
@@ -744,36 +621,150 @@ class JobViewSet(
|
|
|
744
621
|
def run(self, request, *args, pk, **kwargs):
|
|
745
622
|
"""Run the specified Job."""
|
|
746
623
|
job_model = self.get_object()
|
|
747
|
-
return _run_job(request, job_model)
|
|
748
|
-
|
|
749
|
-
@extend_schema(
|
|
750
|
-
deprecated=True,
|
|
751
|
-
methods=["post"],
|
|
752
|
-
request=serializers.JobInputSerializer,
|
|
753
|
-
responses={"200": serializers.JobClassDetailSerializer()},
|
|
754
|
-
operation_id="extras_jobs_run_deprecated",
|
|
755
|
-
)
|
|
756
|
-
@action(
|
|
757
|
-
detail=False, # a /jobs/... URL, not a /jobs/<pk>/... URL
|
|
758
|
-
methods=["post"],
|
|
759
|
-
permission_classes=[JobRunTokenPermissions],
|
|
760
|
-
url_path="(?P<class_path>[^/]+/[^/]+/[^/]+)/run", # /api/extras/jobs/<class_path>/run/
|
|
761
|
-
url_name="run",
|
|
762
|
-
parser_classes=[JSONParser, MultiPartParser],
|
|
763
|
-
)
|
|
764
|
-
def run_deprecated(self, request, class_path):
|
|
765
|
-
"""
|
|
766
|
-
Run a Job as identified by its class-path.
|
|
767
|
-
|
|
768
|
-
This API endpoint is deprecated; it is recommended to use the extras_jobs_run endpoint instead.
|
|
769
|
-
"""
|
|
770
624
|
if not request.user.has_perm("extras.run_job"):
|
|
771
625
|
raise PermissionDenied("This user does not have permission to run jobs.")
|
|
626
|
+
if not job_model.enabled:
|
|
627
|
+
raise PermissionDenied("This job is not enabled to be run.")
|
|
628
|
+
if not job_model.installed:
|
|
629
|
+
raise MethodNotAllowed(request.method, detail="This job is not presently installed and cannot be run")
|
|
630
|
+
if job_model.has_sensitive_variables:
|
|
631
|
+
if (
|
|
632
|
+
"schedule" in request.data
|
|
633
|
+
and "interval" in request.data["schedule"]
|
|
634
|
+
and request.data["schedule"]["interval"] != JobExecutionType.TYPE_IMMEDIATELY
|
|
635
|
+
):
|
|
636
|
+
raise ValidationError(
|
|
637
|
+
{"schedule": {"interval": ["Unable to schedule job: Job may have sensitive input variables"]}}
|
|
638
|
+
)
|
|
639
|
+
if job_model.approval_required:
|
|
640
|
+
raise ValidationError(
|
|
641
|
+
"Unable to run or schedule job: "
|
|
642
|
+
"This job is flagged as possibly having sensitive variables but is also flagged as requiring approval."
|
|
643
|
+
"One of these two flags must be removed before this job can be scheduled or run."
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
job_class = job_model.job_class
|
|
647
|
+
if job_class is None:
|
|
648
|
+
raise MethodNotAllowed(
|
|
649
|
+
request.method, detail="This job's source code could not be located and cannot be run"
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
valid_queues = job_model.task_queues if job_model.task_queues else [settings.CELERY_TASK_DEFAULT_QUEUE]
|
|
653
|
+
# Get a default queue from either the job model's specified task queue or system default to fall back on if request doesn't provide one
|
|
654
|
+
default_valid_queue = valid_queues[0]
|
|
655
|
+
|
|
656
|
+
# We need to call request.data for both cases as this is what pulls and caches the request data
|
|
657
|
+
data = request.data
|
|
658
|
+
files = None
|
|
659
|
+
schedule_data = None
|
|
660
|
+
|
|
661
|
+
# We must extract from the request:
|
|
662
|
+
# - Job Form data (for submission to the job itself)
|
|
663
|
+
# - Schedule data
|
|
664
|
+
# - Desired task queue
|
|
665
|
+
# Depending on request content type (largely for backwards compatibility) the keys at which these are found are different
|
|
666
|
+
if "multipart/form-data" in request.content_type:
|
|
667
|
+
data = request._data.dict() # .data will return data and files, we just want the data
|
|
668
|
+
files = request.FILES
|
|
669
|
+
|
|
670
|
+
# JobMultiPartInputSerializer is a "flattened" version of JobInputSerializer
|
|
671
|
+
input_serializer = serializers.JobMultiPartInputSerializer(data=data, context={"request": request})
|
|
672
|
+
input_serializer.is_valid(raise_exception=True)
|
|
673
|
+
|
|
674
|
+
task_queue = input_serializer.validated_data.get("_task_queue", default_valid_queue)
|
|
675
|
+
|
|
676
|
+
# JobMultiPartInputSerializer only has keys for executing job (task_queue, etc),
|
|
677
|
+
# everything else is a candidate for the job form's data.
|
|
678
|
+
# job_class.validate_data will throw an error for any unexpected key/value pairs.
|
|
679
|
+
non_job_keys = input_serializer.validated_data.keys()
|
|
680
|
+
for non_job_key in non_job_keys:
|
|
681
|
+
data.pop(non_job_key, None)
|
|
682
|
+
|
|
683
|
+
# List of keys in serializer that are effectively exploded versions of the schedule dictionary from JobInputSerializer
|
|
684
|
+
schedule_keys = ("_schedule_name", "_schedule_start_time", "_schedule_interval", "_schedule_crontab")
|
|
685
|
+
|
|
686
|
+
# Assign the key from the validated_data output to dictionary without prefixed "_schedule_"
|
|
687
|
+
# For all the keys that are schedule keys
|
|
688
|
+
# Assign only if the key is in the output since we don't want None's if not provided
|
|
689
|
+
if any(schedule_key in non_job_keys for schedule_key in schedule_keys):
|
|
690
|
+
schedule_data = {
|
|
691
|
+
k.replace("_schedule_", ""): input_serializer.validated_data[k]
|
|
692
|
+
for k in schedule_keys
|
|
693
|
+
if k in input_serializer.validated_data
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
else:
|
|
697
|
+
input_serializer = serializers.JobInputSerializer(data=data, context={"request": request})
|
|
698
|
+
input_serializer.is_valid(raise_exception=True)
|
|
699
|
+
|
|
700
|
+
data = input_serializer.validated_data.get("data", {})
|
|
701
|
+
task_queue = input_serializer.validated_data.get("task_queue", default_valid_queue)
|
|
702
|
+
schedule_data = input_serializer.validated_data.get("schedule", None)
|
|
703
|
+
|
|
704
|
+
if task_queue not in valid_queues:
|
|
705
|
+
raise ValidationError({"task_queue": [f'"{task_queue}" is not a valid choice.']})
|
|
706
|
+
|
|
707
|
+
cleaned_data = None
|
|
772
708
|
try:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
709
|
+
cleaned_data = job_class().validate_data(data, files=files)
|
|
710
|
+
cleaned_data = job_model.job_class.prepare_job_kwargs(cleaned_data)
|
|
711
|
+
|
|
712
|
+
except FormsValidationError as e:
|
|
713
|
+
# message_dict can only be accessed if ValidationError got a dict
|
|
714
|
+
# in the constructor (saved as error_dict). Otherwise we get a list
|
|
715
|
+
# of errors under messages
|
|
716
|
+
return Response({"errors": e.message_dict if hasattr(e, "error_dict") else e.messages}, status=400)
|
|
717
|
+
|
|
718
|
+
if not get_worker_count(queue=task_queue):
|
|
719
|
+
raise CeleryWorkerNotRunningException(queue=task_queue)
|
|
720
|
+
|
|
721
|
+
# Default to a null JobResult.
|
|
722
|
+
job_result = None
|
|
723
|
+
|
|
724
|
+
# Approval is not required for dryrun
|
|
725
|
+
if job_class.supports_dryrun:
|
|
726
|
+
dryrun = data.get("dryrun", False)
|
|
727
|
+
approval_required = not dryrun and job_model.approval_required
|
|
728
|
+
else:
|
|
729
|
+
approval_required = job_model.approval_required
|
|
730
|
+
|
|
731
|
+
# Set schedule for jobs that require approval but request did not supply schedule data
|
|
732
|
+
if schedule_data is None and approval_required:
|
|
733
|
+
schedule_data = {"interval": JobExecutionType.TYPE_IMMEDIATELY}
|
|
734
|
+
|
|
735
|
+
# Skip creating a ScheduledJob when job can be executed immediately
|
|
736
|
+
elif schedule_data and schedule_data["interval"] == JobExecutionType.TYPE_IMMEDIATELY and not approval_required:
|
|
737
|
+
schedule_data = None
|
|
738
|
+
|
|
739
|
+
# Try to create a ScheduledJob, or...
|
|
740
|
+
if schedule_data:
|
|
741
|
+
schedule = _create_schedule(
|
|
742
|
+
schedule_data,
|
|
743
|
+
job_class.serialize_data(cleaned_data),
|
|
744
|
+
job_model,
|
|
745
|
+
request.user,
|
|
746
|
+
approval_required,
|
|
747
|
+
task_queue=input_serializer.validated_data.get("task_queue", None),
|
|
748
|
+
)
|
|
749
|
+
else:
|
|
750
|
+
schedule = None
|
|
751
|
+
|
|
752
|
+
# ... If we can't create one, create a JobResult instead.
|
|
753
|
+
if schedule is None:
|
|
754
|
+
job_result = JobResult.enqueue_job(
|
|
755
|
+
job_model,
|
|
756
|
+
request.user,
|
|
757
|
+
task_queue=task_queue,
|
|
758
|
+
**job_class.serialize_data(cleaned_data),
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
# New-style JobModelViewSet response - serialize the schedule or job_result as appropriate
|
|
762
|
+
data = {"scheduled_job": None, "job_result": None}
|
|
763
|
+
if schedule:
|
|
764
|
+
data["scheduled_job"] = serializers.ScheduledJobSerializer(schedule, context={"request": request}).data
|
|
765
|
+
if job_result:
|
|
766
|
+
data["job_result"] = serializers.JobResultSerializer(job_result, context={"request": request}).data
|
|
767
|
+
return Response(data, status=status.HTTP_201_CREATED)
|
|
777
768
|
|
|
778
769
|
|
|
779
770
|
#
|
|
@@ -819,7 +810,7 @@ class JobResultViewSet(
|
|
|
819
810
|
Retrieve a list of job results
|
|
820
811
|
"""
|
|
821
812
|
|
|
822
|
-
queryset = JobResult.objects.select_related("job_model", "
|
|
813
|
+
queryset = JobResult.objects.select_related("job_model", "user")
|
|
823
814
|
serializer_class = serializers.JobResultSerializer
|
|
824
815
|
filterset_class = filters.JobResultFilterSet
|
|
825
816
|
|
|
@@ -827,7 +818,7 @@ class JobResultViewSet(
|
|
|
827
818
|
def logs(self, request, pk=None):
|
|
828
819
|
job_result = self.get_object()
|
|
829
820
|
logs = job_result.job_log_entries.all()
|
|
830
|
-
serializer =
|
|
821
|
+
serializer = serializers.JobLogEntrySerializer(logs, context={"request": request}, many=True)
|
|
831
822
|
return Response(serializer.data)
|
|
832
823
|
|
|
833
824
|
|
|
@@ -967,21 +958,19 @@ class ScheduledJobViewSet(ReadOnlyModelViewSet):
|
|
|
967
958
|
job_model = scheduled_job.job_model
|
|
968
959
|
if job_model is None or not job_model.runnable:
|
|
969
960
|
raise MethodNotAllowed("This job cannot be dry-run at this time.")
|
|
961
|
+
if not job_model.supports_dryrun:
|
|
962
|
+
raise MethodNotAllowed("This job does not support dry-run.")
|
|
970
963
|
if not Job.objects.check_perms(request.user, instance=job_model, action="run"):
|
|
971
964
|
raise PermissionDenied("You do not have permission to run this job.")
|
|
972
965
|
|
|
973
|
-
# Immediately enqueue the job
|
|
974
|
-
|
|
966
|
+
# Immediately enqueue the job
|
|
967
|
+
job_kwargs = job_model.job_class.prepare_job_kwargs(scheduled_job.kwargs.get("data", {}))
|
|
968
|
+
job_kwargs["dryrun"] = True
|
|
975
969
|
job_result = JobResult.enqueue_job(
|
|
976
|
-
|
|
977
|
-
job_model.class_path,
|
|
978
|
-
job_content_type,
|
|
970
|
+
job_model,
|
|
979
971
|
request.user,
|
|
980
|
-
celery_kwargs=scheduled_job.
|
|
981
|
-
|
|
982
|
-
request=copy_safe_request(request),
|
|
983
|
-
commit=False, # force a dry-run
|
|
984
|
-
task_queue=scheduled_job.kwargs.get("task_queue", None),
|
|
972
|
+
celery_kwargs=scheduled_job.celery_kwargs or {},
|
|
973
|
+
**job_model.job_class.serialize_data(job_kwargs),
|
|
985
974
|
)
|
|
986
975
|
serializer = serializers.JobResultSerializer(job_result, context={"request": request})
|
|
987
976
|
|
|
@@ -993,7 +982,7 @@ class ScheduledJobViewSet(ReadOnlyModelViewSet):
|
|
|
993
982
|
#
|
|
994
983
|
|
|
995
984
|
|
|
996
|
-
class NoteViewSet(ModelViewSet):
|
|
985
|
+
class NoteViewSet(ModelViewSet, TableFieldsViewSetMixin):
|
|
997
986
|
queryset = Note.objects.select_related("user")
|
|
998
987
|
serializer_class = serializers.NoteSerializer
|
|
999
988
|
filterset_class = filters.NoteFilterSet
|
|
@@ -1008,7 +997,7 @@ class NoteViewSet(ModelViewSet):
|
|
|
1008
997
|
#
|
|
1009
998
|
|
|
1010
999
|
|
|
1011
|
-
class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
|
1000
|
+
class ObjectChangeViewSet(ReadOnlyModelViewSet, TableFieldsViewSetMixin):
|
|
1012
1001
|
"""
|
|
1013
1002
|
Retrieve a list of recent changes.
|
|
1014
1003
|
"""
|
|
@@ -1060,6 +1049,31 @@ class SecretsViewSet(NautobotModelViewSet):
|
|
|
1060
1049
|
serializer_class = serializers.SecretSerializer
|
|
1061
1050
|
filterset_class = filters.SecretFilterSet
|
|
1062
1051
|
|
|
1052
|
+
@extend_schema(
|
|
1053
|
+
responses={
|
|
1054
|
+
200: {
|
|
1055
|
+
"type": "object",
|
|
1056
|
+
"properties": {
|
|
1057
|
+
"result": {"type": "boolean"},
|
|
1058
|
+
"message": {"type": "string"},
|
|
1059
|
+
},
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
)
|
|
1063
|
+
@action(methods=["GET"], detail=True)
|
|
1064
|
+
def check(self, request, pk):
|
|
1065
|
+
"""Check that a secret's value is accessible."""
|
|
1066
|
+
result = False
|
|
1067
|
+
message = "Unknown error"
|
|
1068
|
+
try:
|
|
1069
|
+
self.get_object().get_value()
|
|
1070
|
+
result = True
|
|
1071
|
+
message = "Passed"
|
|
1072
|
+
except SecretError as e:
|
|
1073
|
+
message = str(e)
|
|
1074
|
+
response = {"result": result, "message": message}
|
|
1075
|
+
return Response(response)
|
|
1076
|
+
|
|
1063
1077
|
|
|
1064
1078
|
class SecretsGroupViewSet(NautobotModelViewSet):
|
|
1065
1079
|
"""
|