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/jobs.py
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
"""Jobs functionality - consolidates and replaces legacy "custom scripts" and "reports" features."""
|
|
2
2
|
from collections import OrderedDict
|
|
3
|
-
import
|
|
3
|
+
import functools
|
|
4
4
|
import inspect
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
7
|
import os
|
|
8
|
-
import
|
|
8
|
+
import tempfile
|
|
9
9
|
from textwrap import dedent
|
|
10
|
-
import traceback
|
|
11
10
|
import warnings
|
|
12
11
|
|
|
12
|
+
from billiard.einfo import ExceptionInfo
|
|
13
|
+
from celery import states
|
|
14
|
+
from celery.exceptions import NotRegistered, Retry
|
|
15
|
+
from celery.result import EagerResult
|
|
16
|
+
from celery.utils.functional import maybe_list
|
|
17
|
+
from celery.utils.log import get_task_logger
|
|
18
|
+
from celery.utils.nodenames import gethostname
|
|
13
19
|
from db_file_storage.form_widgets import DBClearableFileInput
|
|
14
20
|
from django import forms
|
|
15
21
|
from django.conf import settings
|
|
@@ -17,24 +23,33 @@ from django.contrib.auth import get_user_model
|
|
|
17
23
|
from django.contrib.contenttypes.models import ContentType
|
|
18
24
|
from django.core.files.uploadedfile import InMemoryUploadedFile
|
|
19
25
|
from django.core.validators import RegexValidator
|
|
20
|
-
from django.db import transaction, IntegrityError
|
|
21
26
|
from django.db.models import Model
|
|
22
27
|
from django.db.models.query import QuerySet
|
|
28
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
23
29
|
from django.forms import ValidationError
|
|
24
|
-
from django.test.client import RequestFactory
|
|
25
|
-
from django.utils import timezone
|
|
26
30
|
from django.utils.functional import classproperty
|
|
31
|
+
from kombu.utils.uuid import uuid
|
|
27
32
|
import netaddr
|
|
28
33
|
import yaml
|
|
29
34
|
|
|
30
|
-
from nautobot.core.celery import
|
|
31
|
-
from nautobot.core.
|
|
35
|
+
from nautobot.core.celery import app as celery_app
|
|
36
|
+
from nautobot.core.celery.task import Task
|
|
32
37
|
from nautobot.core.forms import (
|
|
33
38
|
DynamicModelChoiceField,
|
|
34
39
|
DynamicModelMultipleChoiceField,
|
|
35
40
|
)
|
|
36
41
|
from nautobot.core.utils.lookup import get_model_from_name
|
|
37
|
-
from nautobot.
|
|
42
|
+
from nautobot.extras.choices import ObjectChangeActionChoices, ObjectChangeEventContextChoices
|
|
43
|
+
from nautobot.extras.context_managers import change_logging, JobChangeContext, JobHookChangeContext
|
|
44
|
+
from nautobot.extras.forms import JobForm
|
|
45
|
+
from nautobot.extras.models import (
|
|
46
|
+
FileProxy,
|
|
47
|
+
Job as JobModel,
|
|
48
|
+
JobHook,
|
|
49
|
+
JobResult,
|
|
50
|
+
ObjectChange,
|
|
51
|
+
)
|
|
52
|
+
from nautobot.extras.utils import ChangeLoggedModelsQuery, task_queues_as_choices
|
|
38
53
|
from nautobot.ipam.formfields import IPAddressFormField, IPNetworkFormField
|
|
39
54
|
from nautobot.ipam.validators import (
|
|
40
55
|
MaxPrefixLengthValidator,
|
|
@@ -42,14 +57,6 @@ from nautobot.ipam.validators import (
|
|
|
42
57
|
prefix_validator,
|
|
43
58
|
)
|
|
44
59
|
|
|
45
|
-
from .choices import LogLevelChoices, ObjectChangeActionChoices, ObjectChangeEventContextChoices
|
|
46
|
-
from .context_managers import change_logging, JobChangeContext, JobHookChangeContext
|
|
47
|
-
from .datasources.git import ensure_git_repository
|
|
48
|
-
from .forms import JobForm
|
|
49
|
-
from .models import FileProxy, GitRepository, Job as JobModel, JobHook, ObjectChange, ScheduledJob
|
|
50
|
-
from .registry import registry
|
|
51
|
-
from .utils import ChangeLoggedModelsQuery, get_job_content_type, jobs_in_directory, task_queues_as_choices
|
|
52
|
-
|
|
53
60
|
|
|
54
61
|
User = get_user_model()
|
|
55
62
|
|
|
@@ -73,17 +80,17 @@ __all__ = [
|
|
|
73
80
|
logger = logging.getLogger(__name__)
|
|
74
81
|
|
|
75
82
|
|
|
76
|
-
class
|
|
77
|
-
"""
|
|
83
|
+
class RunJobTaskFailed(Exception):
|
|
84
|
+
"""Celery task failed for some reason."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class BaseJob(Task):
|
|
88
|
+
"""Base model for jobs.
|
|
78
89
|
|
|
79
90
|
Users can subclass this directly if they want to provide their own base class for implementing multiple jobs
|
|
80
91
|
with shared functionality; if no such sharing is required, use Job class instead.
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
1. run(self, data, commit) - First method called when invoking a Job, can handle setup and parameter storage.
|
|
85
|
-
2. test_*(self) - Any method matching this pattern will be called next
|
|
86
|
-
3. post_run(self) - Last method called, will be called even in case of an exception during the above methods
|
|
93
|
+
Jobs must define at minimum a run method.
|
|
87
94
|
"""
|
|
88
95
|
|
|
89
96
|
class Meta:
|
|
@@ -92,10 +99,8 @@ class BaseJob:
|
|
|
92
99
|
|
|
93
100
|
- name (str)
|
|
94
101
|
- description (str)
|
|
95
|
-
- commit_default (bool)
|
|
96
102
|
- hidden (bool)
|
|
97
103
|
- field_order (list)
|
|
98
|
-
- read_only (bool)
|
|
99
104
|
- approval_required (bool)
|
|
100
105
|
- soft_time_limit (int)
|
|
101
106
|
- time_limit (int)
|
|
@@ -104,25 +109,274 @@ class BaseJob:
|
|
|
104
109
|
"""
|
|
105
110
|
|
|
106
111
|
def __init__(self):
|
|
107
|
-
self.logger =
|
|
112
|
+
self.logger = get_task_logger(self.__module__)
|
|
108
113
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
def __call__(self, *args, **kwargs):
|
|
115
|
+
# Attempt to resolve serialized data back into original form by creating querysets or model instances
|
|
116
|
+
# If we fail to find any objects, we consider this a job execution error, and fail.
|
|
117
|
+
# This might happen when a job sits on the queue for a while (i.e. scheduled) and data has changed
|
|
118
|
+
# or it might be bad input from an API request, or manual execution.
|
|
119
|
+
try:
|
|
120
|
+
deserialized_kwargs = self.deserialize_data(kwargs)
|
|
121
|
+
except Exception as err:
|
|
122
|
+
raise RunJobTaskFailed("Error initializing job") from err
|
|
123
|
+
context_class = JobHookChangeContext if isinstance(self, JobHookReceiver) else JobChangeContext
|
|
124
|
+
change_context = context_class(user=self.user, context_detail=self.class_path)
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
with change_logging(change_context):
|
|
127
|
+
if self.celery_kwargs.get("nautobot_job_profile", False) is True:
|
|
128
|
+
import cProfile
|
|
129
|
+
|
|
130
|
+
# TODO: This should probably be available as a file download rather than dumped to the hard drive.
|
|
131
|
+
# Pending this: https://github.com/nautobot/nautobot/issues/3352
|
|
132
|
+
profiling_path = f"{tempfile.gettempdir()}/nautobot-jobresult-{self.job_result.id}.pstats"
|
|
133
|
+
self.logger.info(
|
|
134
|
+
"Writing profiling information to %s.", profiling_path, extra={"grouping": "initialization"}
|
|
135
|
+
)
|
|
116
136
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
with cProfile.Profile() as pr:
|
|
138
|
+
try:
|
|
139
|
+
output = self.run(*args, **deserialized_kwargs)
|
|
140
|
+
except Exception as err:
|
|
141
|
+
pr.dump_stats(profiling_path)
|
|
142
|
+
raise err
|
|
143
|
+
else:
|
|
144
|
+
pr.dump_stats(profiling_path)
|
|
145
|
+
return output
|
|
146
|
+
else:
|
|
147
|
+
return self.run(*args, **deserialized_kwargs)
|
|
120
148
|
|
|
121
149
|
def __str__(self):
|
|
122
150
|
return str(self.name)
|
|
123
151
|
|
|
124
152
|
# See https://github.com/PyCQA/pylint-django/issues/240 for why we have a pylint disable on each classproperty below
|
|
125
153
|
|
|
154
|
+
# TODO(jathan): Could be interesting for custom stuff when the Job is
|
|
155
|
+
# enabled in the database and then therefore registered in Celery
|
|
156
|
+
@classmethod
|
|
157
|
+
def on_bound(cls, app):
|
|
158
|
+
"""Called when the task is bound to an app.
|
|
159
|
+
|
|
160
|
+
Note:
|
|
161
|
+
This class method can be defined to do additional actions when
|
|
162
|
+
the task class is bound to an app.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# TODO(jathan): Could be interesting for showing the Job's class path as the
|
|
166
|
+
# shadow name vs. the Celery task_name?
|
|
167
|
+
def shadow_name(self, args, kwargs, options):
|
|
168
|
+
"""Override for custom task name in worker logs/monitoring.
|
|
169
|
+
|
|
170
|
+
Example:
|
|
171
|
+
from celery.utils.imports import qualname
|
|
172
|
+
|
|
173
|
+
def shadow_name(task, args, kwargs, options):
|
|
174
|
+
return qualname(args[0])
|
|
175
|
+
|
|
176
|
+
@app.task(shadow_name=shadow_name, serializer='pickle')
|
|
177
|
+
def apply_function_async(fun, *args, **kwargs):
|
|
178
|
+
return fun(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
Arguments:
|
|
181
|
+
args (Tuple): Task positional arguments.
|
|
182
|
+
kwargs (Dict): Task keyword arguments.
|
|
183
|
+
options (Dict): Task execution options.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def before_start(self, task_id, args, kwargs):
|
|
187
|
+
"""Handler called before the task starts.
|
|
188
|
+
|
|
189
|
+
Arguments:
|
|
190
|
+
task_id (str): Unique id of the task to execute.
|
|
191
|
+
args (Tuple): Original arguments for the task to execute.
|
|
192
|
+
kwargs (Dict): Original keyword arguments for the task to execute.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
None: The return value of this handler is ignored.
|
|
196
|
+
"""
|
|
197
|
+
self.clear_cache()
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
self.job_result
|
|
201
|
+
except ObjectDoesNotExist as err:
|
|
202
|
+
raise RunJobTaskFailed(f"Unable to find associated job result for job {task_id}") from err
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
self.job_model
|
|
206
|
+
except ObjectDoesNotExist as err:
|
|
207
|
+
raise RunJobTaskFailed(f"Unable to find associated job model for job {task_id}") from err
|
|
208
|
+
|
|
209
|
+
if not self.job_model.enabled:
|
|
210
|
+
self.logger.error(
|
|
211
|
+
"Job %s is not enabled to be run!",
|
|
212
|
+
self.job_model,
|
|
213
|
+
extra={"object": self.job_model, "grouping": "initialization"},
|
|
214
|
+
)
|
|
215
|
+
raise RunJobTaskFailed(f"Job {self.job_model} is not enabled to be run!")
|
|
216
|
+
|
|
217
|
+
soft_time_limit = self.job_model.soft_time_limit or settings.CELERY_TASK_SOFT_TIME_LIMIT
|
|
218
|
+
time_limit = self.job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT
|
|
219
|
+
if time_limit <= soft_time_limit:
|
|
220
|
+
self.logger.warning(
|
|
221
|
+
"The hard time limit of %s seconds is less than "
|
|
222
|
+
"or equal to the soft time limit of %s seconds. "
|
|
223
|
+
"This job will fail silently after %s seconds.",
|
|
224
|
+
time_limit,
|
|
225
|
+
soft_time_limit,
|
|
226
|
+
time_limit,
|
|
227
|
+
extra={"grouping": "initialization"},
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
self.logger.info("Running job", extra={"grouping": "initialization"})
|
|
231
|
+
|
|
232
|
+
def run(self, *args, **kwargs):
|
|
233
|
+
"""
|
|
234
|
+
Method invoked when this Job is run.
|
|
235
|
+
"""
|
|
236
|
+
raise NotImplementedError("Jobs must define the run method.")
|
|
237
|
+
|
|
238
|
+
def on_success(self, retval, task_id, args, kwargs):
|
|
239
|
+
"""Success handler.
|
|
240
|
+
|
|
241
|
+
Run by the worker if the task executes successfully.
|
|
242
|
+
|
|
243
|
+
Arguments:
|
|
244
|
+
retval (Any): The return value of the task.
|
|
245
|
+
task_id (str): Unique id of the executed task.
|
|
246
|
+
args (Tuple): Original arguments for the executed task.
|
|
247
|
+
kwargs (Dict): Original keyword arguments for the executed task.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
None: The return value of this handler is ignored.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def on_retry(self, exc, task_id, args, kwargs, einfo):
|
|
254
|
+
"""Retry handler.
|
|
255
|
+
|
|
256
|
+
This is run by the worker when the task is to be retried.
|
|
257
|
+
|
|
258
|
+
Arguments:
|
|
259
|
+
exc (Exception): The exception sent to :meth:`retry`.
|
|
260
|
+
task_id (str): Unique id of the retried task.
|
|
261
|
+
args (Tuple): Original arguments for the retried task.
|
|
262
|
+
kwargs (Dict): Original keyword arguments for the retried task.
|
|
263
|
+
einfo (~billiard.einfo.ExceptionInfo): Exception information.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
None: The return value of this handler is ignored.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
|
270
|
+
"""Error handler.
|
|
271
|
+
|
|
272
|
+
This is run by the worker when the task fails.
|
|
273
|
+
|
|
274
|
+
Arguments:
|
|
275
|
+
exc (Exception): The exception raised by the task.
|
|
276
|
+
task_id (str): Unique id of the failed task.
|
|
277
|
+
args (Tuple): Original arguments for the task that failed.
|
|
278
|
+
kwargs (Dict): Original keyword arguments for the task that failed.
|
|
279
|
+
einfo (~billiard.einfo.ExceptionInfo): Exception information.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
None: The return value of this handler is ignored.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def after_return(self, status, retval, task_id, args, kwargs, einfo):
|
|
286
|
+
"""
|
|
287
|
+
Handler called after the task returns.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
status - Current task state.
|
|
291
|
+
retval - Task return value/exception.
|
|
292
|
+
task_id - Unique id of the task.
|
|
293
|
+
args - Original arguments for the task that returned.
|
|
294
|
+
kwargs - Original keyword arguments for the task that returned.
|
|
295
|
+
|
|
296
|
+
Keyword Arguments
|
|
297
|
+
einfo - ExceptionInfo instance, containing the traceback (if any).
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
None: The return value of this handler is ignored.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
# Cleanup FileProxy objects
|
|
304
|
+
file_fields = list(self._get_file_vars())
|
|
305
|
+
file_ids = [kwargs[f] for f in file_fields]
|
|
306
|
+
if file_ids:
|
|
307
|
+
self.delete_files(*file_ids)
|
|
308
|
+
|
|
309
|
+
self.logger.info("Job completed", extra={"grouping": "post_run"})
|
|
310
|
+
|
|
311
|
+
# TODO(gary): document this in job author docs
|
|
312
|
+
# Super.after_return must be called for chords to function properly
|
|
313
|
+
super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
|
|
314
|
+
|
|
315
|
+
def apply(
|
|
316
|
+
self,
|
|
317
|
+
args=None,
|
|
318
|
+
kwargs=None,
|
|
319
|
+
link=None,
|
|
320
|
+
link_error=None,
|
|
321
|
+
task_id=None,
|
|
322
|
+
retries=None,
|
|
323
|
+
throw=None,
|
|
324
|
+
logfile=None,
|
|
325
|
+
loglevel=None,
|
|
326
|
+
headers=None,
|
|
327
|
+
**options,
|
|
328
|
+
):
|
|
329
|
+
"""Fix celery's apply method to propagate options to the task result"""
|
|
330
|
+
# trace imports Task, so need to import inline.
|
|
331
|
+
from celery.app.trace import build_tracer
|
|
332
|
+
|
|
333
|
+
app = self._get_app()
|
|
334
|
+
args = args or ()
|
|
335
|
+
kwargs = kwargs or {}
|
|
336
|
+
task_id = task_id or uuid()
|
|
337
|
+
retries = retries or 0
|
|
338
|
+
if throw is None:
|
|
339
|
+
throw = app.conf.task_eager_propagates
|
|
340
|
+
|
|
341
|
+
# Make sure we get the task instance, not class.
|
|
342
|
+
task = app._tasks[self.name]
|
|
343
|
+
|
|
344
|
+
request = {
|
|
345
|
+
"id": task_id,
|
|
346
|
+
"retries": retries,
|
|
347
|
+
"is_eager": True,
|
|
348
|
+
"logfile": logfile,
|
|
349
|
+
"loglevel": loglevel or 0,
|
|
350
|
+
"hostname": gethostname(),
|
|
351
|
+
"callbacks": maybe_list(link),
|
|
352
|
+
"errbacks": maybe_list(link_error),
|
|
353
|
+
"headers": headers,
|
|
354
|
+
"ignore_result": options.get("ignore_result", False),
|
|
355
|
+
"delivery_info": {
|
|
356
|
+
"is_eager": True,
|
|
357
|
+
"exchange": options.get("exchange"),
|
|
358
|
+
"routing_key": options.get("routing_key"),
|
|
359
|
+
"priority": options.get("priority"),
|
|
360
|
+
},
|
|
361
|
+
"properties": options, # one line fix to overloaded method
|
|
362
|
+
}
|
|
363
|
+
tb = None
|
|
364
|
+
tracer = build_tracer(
|
|
365
|
+
task.name,
|
|
366
|
+
task,
|
|
367
|
+
eager=True,
|
|
368
|
+
propagate=throw,
|
|
369
|
+
app=self._get_app(),
|
|
370
|
+
)
|
|
371
|
+
ret = tracer(task_id, args, kwargs, request)
|
|
372
|
+
retval = ret.retval
|
|
373
|
+
if isinstance(retval, ExceptionInfo):
|
|
374
|
+
retval, tb = retval.exception, retval.traceback
|
|
375
|
+
if isinstance(retval, Retry) and retval.sig is not None:
|
|
376
|
+
return retval.sig.apply(retries=retries + 1)
|
|
377
|
+
state = states.SUCCESS if ret.info is None else ret.info.state
|
|
378
|
+
return EagerResult(task_id, retval, state, traceback=tb)
|
|
379
|
+
|
|
126
380
|
@classproperty
|
|
127
381
|
def file_path(cls): # pylint: disable=no-self-argument
|
|
128
382
|
return inspect.getfile(cls)
|
|
@@ -130,47 +384,31 @@ class BaseJob:
|
|
|
130
384
|
@classproperty
|
|
131
385
|
def class_path(cls): # pylint: disable=no-self-argument
|
|
132
386
|
"""
|
|
133
|
-
Unique identifier of a specific Job class, in the form <
|
|
387
|
+
Unique identifier of a specific Job class, in the form <module_name>.<ClassName>.
|
|
134
388
|
|
|
135
389
|
Examples:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
390
|
+
my_script.MyScript - Local Job
|
|
391
|
+
nautobot.core.jobs.MySystemJob - System Job
|
|
392
|
+
my_plugin.jobs.MyPluginJob - App-provided Job
|
|
393
|
+
git_repository.jobs.myjob.MyJob - GitRepository Job
|
|
139
394
|
"""
|
|
140
|
-
|
|
141
|
-
if cls in registry["plugin_jobs"]:
|
|
142
|
-
source_grouping = "plugins"
|
|
143
|
-
elif cls.file_path.startswith(settings.JOBS_ROOT):
|
|
144
|
-
source_grouping = "local"
|
|
145
|
-
elif cls.file_path.startswith(settings.GIT_ROOT):
|
|
146
|
-
# $GIT_ROOT/<repo_slug>/jobs/job.py -> <repo_slug>
|
|
147
|
-
source_grouping = ".".join(
|
|
148
|
-
[
|
|
149
|
-
"git",
|
|
150
|
-
os.path.basename(os.path.dirname(os.path.dirname(cls.file_path))),
|
|
151
|
-
]
|
|
152
|
-
)
|
|
153
|
-
else:
|
|
154
|
-
raise RuntimeError(
|
|
155
|
-
f"Unknown/unexpected job file_path {cls.file_path}, should be one of "
|
|
156
|
-
+ ", ".join([settings.JOBS_ROOT, settings.GIT_ROOT])
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
return "/".join([source_grouping, cls.__module__, cls.__name__])
|
|
395
|
+
return f"{cls.__module__}.{cls.__name__}"
|
|
160
396
|
|
|
161
397
|
@classproperty
|
|
162
398
|
def class_path_dotted(cls): # pylint: disable=no-self-argument
|
|
163
399
|
"""
|
|
164
400
|
Dotted class_path, suitable for use in things like Python logger names.
|
|
401
|
+
|
|
402
|
+
Deprecated as of Nautobot 2.0: just use .class_path instead.
|
|
165
403
|
"""
|
|
166
|
-
return cls.class_path
|
|
404
|
+
return cls.class_path
|
|
167
405
|
|
|
168
406
|
@classproperty
|
|
169
407
|
def class_path_js_escaped(cls): # pylint: disable=no-self-argument
|
|
170
408
|
"""
|
|
171
409
|
Escape various characters so that the class_path can be used as a jQuery selector.
|
|
172
410
|
"""
|
|
173
|
-
return cls.class_path.replace("
|
|
411
|
+
return cls.class_path.replace(".", r"\.")
|
|
174
412
|
|
|
175
413
|
@classproperty
|
|
176
414
|
def grouping(cls): # pylint: disable=no-self-argument
|
|
@@ -192,8 +430,8 @@ class BaseJob:
|
|
|
192
430
|
return ""
|
|
193
431
|
|
|
194
432
|
@classproperty
|
|
195
|
-
def
|
|
196
|
-
return getattr(cls.Meta, "
|
|
433
|
+
def dryrun_default(cls): # pylint: disable=no-self-argument
|
|
434
|
+
return getattr(cls.Meta, "dryrun_default", False)
|
|
197
435
|
|
|
198
436
|
@classproperty
|
|
199
437
|
def hidden(cls): # pylint: disable=no-self-argument
|
|
@@ -223,6 +461,10 @@ class BaseJob:
|
|
|
223
461
|
def has_sensitive_variables(cls): # pylint: disable=no-self-argument
|
|
224
462
|
return getattr(cls.Meta, "has_sensitive_variables", True)
|
|
225
463
|
|
|
464
|
+
@classproperty
|
|
465
|
+
def supports_dryrun(cls): # pylint: disable=no-self-argument
|
|
466
|
+
return isinstance(getattr(cls, "dryrun", None), DryRunVar)
|
|
467
|
+
|
|
226
468
|
@classproperty
|
|
227
469
|
def task_queues(cls): # pylint: disable=no-self-argument
|
|
228
470
|
return getattr(cls.Meta, "task_queues", [])
|
|
@@ -239,15 +481,17 @@ class BaseJob:
|
|
|
239
481
|
"grouping": cls.grouping,
|
|
240
482
|
"description": cls.description,
|
|
241
483
|
"approval_required": cls.approval_required,
|
|
242
|
-
"commit_default": cls.commit_default,
|
|
243
484
|
"hidden": cls.hidden,
|
|
244
|
-
"read_only": cls.read_only,
|
|
245
485
|
"soft_time_limit": cls.soft_time_limit,
|
|
246
486
|
"time_limit": cls.time_limit,
|
|
247
487
|
"has_sensitive_variables": cls.has_sensitive_variables,
|
|
248
488
|
"task_queues": cls.task_queues,
|
|
249
489
|
}
|
|
250
490
|
|
|
491
|
+
@classproperty
|
|
492
|
+
def registered_name(cls): # pylint: disable=no-self-argument
|
|
493
|
+
return f"{cls.__module__}.{cls.__name__}"
|
|
494
|
+
|
|
251
495
|
@classmethod
|
|
252
496
|
def _get_vars(cls):
|
|
253
497
|
"""
|
|
@@ -276,33 +520,6 @@ class BaseJob:
|
|
|
276
520
|
|
|
277
521
|
return file_vars
|
|
278
522
|
|
|
279
|
-
@property
|
|
280
|
-
def job_result(self):
|
|
281
|
-
return self._job_result
|
|
282
|
-
|
|
283
|
-
@job_result.setter
|
|
284
|
-
def job_result(self, value):
|
|
285
|
-
# Initialize job_result data format for our usage
|
|
286
|
-
value.data = OrderedDict()
|
|
287
|
-
|
|
288
|
-
self._job_result = value
|
|
289
|
-
|
|
290
|
-
@property
|
|
291
|
-
def results(self):
|
|
292
|
-
"""
|
|
293
|
-
The results generated by this job.
|
|
294
|
-
** If you need the logs, you will need to filter on JobLogEntry **
|
|
295
|
-
Ex.
|
|
296
|
-
from nautobot.extras.models import JogLogEntry
|
|
297
|
-
|
|
298
|
-
JobLogEntry.objects.filter(job_result=self.job_result, <other criteria>)
|
|
299
|
-
|
|
300
|
-
{
|
|
301
|
-
"output": "...",
|
|
302
|
-
}
|
|
303
|
-
"""
|
|
304
|
-
return self.job_result.data if self.job_result else None
|
|
305
|
-
|
|
306
523
|
def as_form_class(self):
|
|
307
524
|
"""
|
|
308
525
|
Dynamically generate a Django form class corresponding to the variables in this Job.
|
|
@@ -324,27 +541,22 @@ class BaseJob:
|
|
|
324
541
|
|
|
325
542
|
try:
|
|
326
543
|
job_model = JobModel.objects.get_for_class_path(self.class_path)
|
|
327
|
-
|
|
328
|
-
commit_default = job_model.commit_default if job_model.commit_default_override else self.commit_default
|
|
544
|
+
dryrun_default = job_model.dryrun_default if job_model.dryrun_default_override else self.dryrun_default
|
|
329
545
|
task_queues = job_model.task_queues if job_model.task_queues_override else self.task_queues
|
|
330
546
|
except JobModel.DoesNotExist:
|
|
331
|
-
# 2.0 TODO: remove this fallback, Job records should always exist.
|
|
332
547
|
logger.error("No Job instance found in the database corresponding to %s", self.class_path)
|
|
333
|
-
|
|
334
|
-
commit_default = self.commit_default
|
|
548
|
+
dryrun_default = self.dryrun_default
|
|
335
549
|
task_queues = self.task_queues
|
|
336
550
|
|
|
337
|
-
if read_only:
|
|
338
|
-
# Hide the commit field for read only jobs
|
|
339
|
-
form.fields["_commit"].widget = forms.HiddenInput()
|
|
340
|
-
form.fields["_commit"].initial = False
|
|
341
|
-
elif not initial or "_commit" not in initial:
|
|
342
|
-
# Set initial "commit" checkbox state based on the Meta parameter
|
|
343
|
-
form.fields["_commit"].initial = commit_default
|
|
344
|
-
|
|
345
551
|
# Update task queue choices
|
|
346
552
|
form.fields["_task_queue"].choices = task_queues_as_choices(task_queues)
|
|
347
553
|
|
|
554
|
+
if self.supports_dryrun and (not initial or "dryrun" not in initial):
|
|
555
|
+
# Set initial "dryrun" checkbox state based on the Meta parameter
|
|
556
|
+
form.fields["dryrun"].initial = dryrun_default
|
|
557
|
+
if not settings.DEBUG:
|
|
558
|
+
form.fields["_profile"].widget = forms.HiddenInput()
|
|
559
|
+
|
|
348
560
|
# https://github.com/PyCQA/pylint/issues/3484
|
|
349
561
|
if self.field_order: # pylint: disable=using-constant-test
|
|
350
562
|
form.order_fields(self.field_order)
|
|
@@ -354,11 +566,42 @@ class BaseJob:
|
|
|
354
566
|
for _, field in form.fields.items():
|
|
355
567
|
field.disabled = True
|
|
356
568
|
|
|
357
|
-
# Alter the commit help text to avoid confusion concerning approval dry-runs
|
|
358
|
-
form.fields["_commit"].help_text = "Commit changes to the database"
|
|
359
|
-
|
|
360
569
|
return form
|
|
361
570
|
|
|
571
|
+
def clear_cache(self):
|
|
572
|
+
"""
|
|
573
|
+
Clear all cached properties on this instance without accessing them. This is required because
|
|
574
|
+
celery reuses task instances for multiple runs.
|
|
575
|
+
"""
|
|
576
|
+
try:
|
|
577
|
+
del self.celery_kwargs
|
|
578
|
+
except AttributeError:
|
|
579
|
+
pass
|
|
580
|
+
try:
|
|
581
|
+
del self.job_result
|
|
582
|
+
except AttributeError:
|
|
583
|
+
pass
|
|
584
|
+
try:
|
|
585
|
+
del self.job_model
|
|
586
|
+
except AttributeError:
|
|
587
|
+
pass
|
|
588
|
+
|
|
589
|
+
@functools.cached_property
|
|
590
|
+
def job_model(self):
|
|
591
|
+
return JobModel.objects.get(module_name=self.__module__, job_class_name=self.__name__)
|
|
592
|
+
|
|
593
|
+
@functools.cached_property
|
|
594
|
+
def job_result(self):
|
|
595
|
+
return JobResult.objects.get(id=self.request.id)
|
|
596
|
+
|
|
597
|
+
@functools.cached_property
|
|
598
|
+
def celery_kwargs(self):
|
|
599
|
+
return self.job_result.celery_kwargs or {}
|
|
600
|
+
|
|
601
|
+
@property
|
|
602
|
+
def user(self):
|
|
603
|
+
return getattr(self.job_result, "user", None)
|
|
604
|
+
|
|
362
605
|
@staticmethod
|
|
363
606
|
def serialize_data(data):
|
|
364
607
|
"""
|
|
@@ -390,6 +633,7 @@ class BaseJob:
|
|
|
390
633
|
|
|
391
634
|
return return_data
|
|
392
635
|
|
|
636
|
+
# TODO: can the deserialize_data logic be moved to NautobotKombuJSONEncoder?
|
|
393
637
|
@classmethod
|
|
394
638
|
def deserialize_data(cls, data):
|
|
395
639
|
"""
|
|
@@ -409,7 +653,7 @@ class BaseJob:
|
|
|
409
653
|
raise TypeError("Data should be a dictionary.")
|
|
410
654
|
|
|
411
655
|
for field_name, value in data.items():
|
|
412
|
-
# If a field isn't a var, skip it (e.g. `
|
|
656
|
+
# If a field isn't a var, skip it (e.g. `_task_queue`).
|
|
413
657
|
try:
|
|
414
658
|
var = cls_vars[field_name]
|
|
415
659
|
except KeyError:
|
|
@@ -467,6 +711,12 @@ class BaseJob:
|
|
|
467
711
|
|
|
468
712
|
return f.cleaned_data
|
|
469
713
|
|
|
714
|
+
@classmethod
|
|
715
|
+
def prepare_job_kwargs(cls, job_kwargs):
|
|
716
|
+
"""Process dict and return kwargs that exist as ScriptVariables on this job."""
|
|
717
|
+
job_vars = cls._get_vars()
|
|
718
|
+
return {k: v for k, v in job_kwargs.items() if k in job_vars}
|
|
719
|
+
|
|
470
720
|
@staticmethod
|
|
471
721
|
def load_file(pk):
|
|
472
722
|
"""Load a file proxy stored in the database by primary key.
|
|
@@ -495,8 +745,7 @@ class BaseJob:
|
|
|
495
745
|
fp = FileProxy.objects.create(name=uploaded_file.name, file=uploaded_file)
|
|
496
746
|
return fp.pk
|
|
497
747
|
|
|
498
|
-
|
|
499
|
-
def delete_files(*files_to_delete):
|
|
748
|
+
def delete_files(self, *files_to_delete):
|
|
500
749
|
"""Given an unpacked list of primary keys for `FileProxy` objects, delete them.
|
|
501
750
|
|
|
502
751
|
Args:
|
|
@@ -510,86 +759,9 @@ class BaseJob:
|
|
|
510
759
|
for fp in files:
|
|
511
760
|
fp.delete() # Call delete() on each, so `FileAttachment` is reaped
|
|
512
761
|
num += 1
|
|
513
|
-
logger.debug(
|
|
762
|
+
self.logger.debug("Deleted %d file proxies", num, extra={"grouping": "post_run"})
|
|
514
763
|
return num
|
|
515
764
|
|
|
516
|
-
def run(self, data, commit):
|
|
517
|
-
"""
|
|
518
|
-
Method invoked when this Job is run, before any "test_*" methods.
|
|
519
|
-
"""
|
|
520
|
-
|
|
521
|
-
def post_run(self):
|
|
522
|
-
"""
|
|
523
|
-
Method invoked after "run()" and all "test_*" methods.
|
|
524
|
-
"""
|
|
525
|
-
|
|
526
|
-
# Logging
|
|
527
|
-
|
|
528
|
-
def _log(self, obj, message, level_choice=LogLevelChoices.LOG_DEFAULT):
|
|
529
|
-
"""
|
|
530
|
-
Log a message. Do not call this method directly; use one of the log_* wrappers below.
|
|
531
|
-
"""
|
|
532
|
-
self.job_result.log(
|
|
533
|
-
message,
|
|
534
|
-
obj=obj,
|
|
535
|
-
level_choice=level_choice,
|
|
536
|
-
grouping=self.active_test,
|
|
537
|
-
logger=self.logger,
|
|
538
|
-
)
|
|
539
|
-
|
|
540
|
-
def log(self, message):
|
|
541
|
-
"""
|
|
542
|
-
Log a generic message which is not associated with a particular object.
|
|
543
|
-
"""
|
|
544
|
-
self._log(None, message, level_choice=LogLevelChoices.LOG_DEFAULT)
|
|
545
|
-
|
|
546
|
-
def log_debug(self, message):
|
|
547
|
-
"""
|
|
548
|
-
Log a debug message which is not associated with a particular object.
|
|
549
|
-
"""
|
|
550
|
-
self._log(None, message, level_choice=LogLevelChoices.LOG_DEFAULT)
|
|
551
|
-
|
|
552
|
-
def log_success(self, obj=None, message=None):
|
|
553
|
-
"""
|
|
554
|
-
Record a successful test against an object. Logging a message is optional.
|
|
555
|
-
If the object provided is a string, treat it as a message. This is a carryover of Netbox Report API
|
|
556
|
-
"""
|
|
557
|
-
if isinstance(obj, str) and message is None:
|
|
558
|
-
self._log(obj=None, message=obj, level_choice=LogLevelChoices.LOG_SUCCESS)
|
|
559
|
-
else:
|
|
560
|
-
self._log(obj, message, level_choice=LogLevelChoices.LOG_SUCCESS)
|
|
561
|
-
|
|
562
|
-
def log_info(self, obj=None, message=None):
|
|
563
|
-
"""
|
|
564
|
-
Log an informational message.
|
|
565
|
-
If the object provided is a string, treat it as a message. This is a carryover of Netbox Report API
|
|
566
|
-
"""
|
|
567
|
-
if isinstance(obj, str) and message is None:
|
|
568
|
-
self._log(obj=None, message=obj, level_choice=LogLevelChoices.LOG_INFO)
|
|
569
|
-
else:
|
|
570
|
-
self._log(obj, message, level_choice=LogLevelChoices.LOG_INFO)
|
|
571
|
-
|
|
572
|
-
def log_warning(self, obj=None, message=None):
|
|
573
|
-
"""
|
|
574
|
-
Log a warning.
|
|
575
|
-
If the object provided is a string, treat it as a message. This is a carryover of Netbox Report API
|
|
576
|
-
"""
|
|
577
|
-
if isinstance(obj, str) and message is None:
|
|
578
|
-
self._log(obj=None, message=obj, level_choice=LogLevelChoices.LOG_WARNING)
|
|
579
|
-
else:
|
|
580
|
-
self._log(obj, message, level_choice=LogLevelChoices.LOG_WARNING)
|
|
581
|
-
|
|
582
|
-
def log_failure(self, obj=None, message=None):
|
|
583
|
-
"""
|
|
584
|
-
Log a failure. Calling this method will automatically mark the overall job as failed.
|
|
585
|
-
If the object provided is a string, treat it as a message. This is a carryover of Netbox Report API
|
|
586
|
-
"""
|
|
587
|
-
if isinstance(obj, str) and message is None:
|
|
588
|
-
self._log(obj=None, message=obj, level_choice=LogLevelChoices.LOG_FAILURE)
|
|
589
|
-
else:
|
|
590
|
-
self._log(obj, message, level_choice=LogLevelChoices.LOG_FAILURE)
|
|
591
|
-
raise RunJobTaskFailed(message)
|
|
592
|
-
|
|
593
765
|
# Convenience functions
|
|
594
766
|
|
|
595
767
|
def load_yaml(self, filename):
|
|
@@ -728,6 +900,23 @@ class BooleanVar(ScriptVariable):
|
|
|
728
900
|
self.field_attrs["required"] = False
|
|
729
901
|
|
|
730
902
|
|
|
903
|
+
class DryRunVar(BooleanVar):
|
|
904
|
+
"""
|
|
905
|
+
Special boolean variable that bypasses approval requirements if this is set to True on job execution.
|
|
906
|
+
"""
|
|
907
|
+
|
|
908
|
+
description = "Check to run job in dryrun mode."
|
|
909
|
+
|
|
910
|
+
def __init__(self, *args, **kwargs):
|
|
911
|
+
# Default must be false unless overridden through `dryrun_default` meta attribute
|
|
912
|
+
kwargs["default"] = False
|
|
913
|
+
|
|
914
|
+
# Default description if one was not provided
|
|
915
|
+
kwargs.setdefault("description", self.description)
|
|
916
|
+
|
|
917
|
+
super().__init__(*args, **kwargs)
|
|
918
|
+
|
|
919
|
+
|
|
731
920
|
class ChoiceVar(ScriptVariable):
|
|
732
921
|
"""
|
|
733
922
|
Select one of several predefined static choices, passed as a list of two-tuples. Example:
|
|
@@ -866,9 +1055,8 @@ class JobHookReceiver(Job):
|
|
|
866
1055
|
|
|
867
1056
|
object_change = ObjectVar(model=ObjectChange)
|
|
868
1057
|
|
|
869
|
-
def run(self,
|
|
1058
|
+
def run(self, object_change):
|
|
870
1059
|
"""JobHookReceiver subclasses generally shouldn't need to override this method."""
|
|
871
|
-
object_change = data["object_change"]
|
|
872
1060
|
self.receive_job_hook(
|
|
873
1061
|
change=object_change,
|
|
874
1062
|
action=object_change.action,
|
|
@@ -895,11 +1083,8 @@ class JobButtonReceiver(Job):
|
|
|
895
1083
|
object_pk = StringVar()
|
|
896
1084
|
object_model_name = StringVar()
|
|
897
1085
|
|
|
898
|
-
def run(self,
|
|
1086
|
+
def run(self, object_pk, object_model_name):
|
|
899
1087
|
"""JobButtonReceiver subclasses generally shouldn't need to override this method."""
|
|
900
|
-
object_pk = data["object_pk"]
|
|
901
|
-
object_model_name = data["object_model_name"]
|
|
902
|
-
|
|
903
1088
|
model = get_model_from_name(object_model_name)
|
|
904
1089
|
obj = model.objects.get(pk=object_pk)
|
|
905
1090
|
|
|
@@ -918,11 +1103,8 @@ def is_job(obj):
|
|
|
918
1103
|
"""
|
|
919
1104
|
Returns True if the given object is a Job subclass.
|
|
920
1105
|
"""
|
|
921
|
-
from .scripts import Script, BaseScript
|
|
922
|
-
from .reports import Report
|
|
923
|
-
|
|
924
1106
|
try:
|
|
925
|
-
return issubclass(obj, Job) and obj not in [Job,
|
|
1107
|
+
return issubclass(obj, Job) and obj not in [Job, JobHookReceiver, JobButtonReceiver]
|
|
926
1108
|
except TypeError:
|
|
927
1109
|
return False
|
|
928
1110
|
|
|
@@ -934,372 +1116,23 @@ def is_variable(obj):
|
|
|
934
1116
|
return isinstance(obj, ScriptVariable)
|
|
935
1117
|
|
|
936
1118
|
|
|
937
|
-
def get_jobs():
|
|
938
|
-
"""
|
|
939
|
-
Compile a dictionary of all jobs available across all modules in the jobs path(s).
|
|
940
|
-
|
|
941
|
-
Returns an OrderedDict:
|
|
942
|
-
|
|
943
|
-
{
|
|
944
|
-
"local": {
|
|
945
|
-
<module_name>: {
|
|
946
|
-
"name": <human-readable module name>,
|
|
947
|
-
"jobs": {
|
|
948
|
-
<class_name>: <job_class>,
|
|
949
|
-
<class_name>: <job_class>,
|
|
950
|
-
...
|
|
951
|
-
},
|
|
952
|
-
},
|
|
953
|
-
<module_name>: { ... },
|
|
954
|
-
...
|
|
955
|
-
},
|
|
956
|
-
"git.<repository-slug>": {
|
|
957
|
-
<module_name>: { ... },
|
|
958
|
-
},
|
|
959
|
-
...
|
|
960
|
-
"plugins": {
|
|
961
|
-
<module_name>: { ... },
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
"""
|
|
965
|
-
jobs = OrderedDict()
|
|
966
|
-
|
|
967
|
-
paths = _get_job_source_paths()
|
|
968
|
-
|
|
969
|
-
# Iterate over all filesystem sources (local, git.<slug1>, git.<slug2>, etc.)
|
|
970
|
-
for source, path in paths.items():
|
|
971
|
-
for job_info in jobs_in_directory(path):
|
|
972
|
-
jobs.setdefault(source, {})
|
|
973
|
-
if job_info.module_name not in jobs[source]:
|
|
974
|
-
jobs[source][job_info.module_name] = {"name": job_info.job_class.grouping, "jobs": OrderedDict()}
|
|
975
|
-
jobs[source][job_info.module_name]["jobs"][job_info.job_class_name] = job_info.job_class
|
|
976
|
-
|
|
977
|
-
# Add jobs from plugins (which were already imported at startup)
|
|
978
|
-
for cls in registry["plugin_jobs"]:
|
|
979
|
-
module = inspect.getmodule(cls)
|
|
980
|
-
jobs.setdefault("plugins", {}).setdefault(module.__name__, {"name": cls.grouping, "jobs": OrderedDict()})
|
|
981
|
-
jobs["plugins"][module.__name__]["jobs"][cls.__name__] = cls
|
|
982
|
-
|
|
983
|
-
return jobs
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
def _get_job_source_paths():
|
|
987
|
-
"""
|
|
988
|
-
Helper function to get_jobs().
|
|
989
|
-
|
|
990
|
-
Constructs a dict of {"grouping": filesystem_path, ...}.
|
|
991
|
-
Current groupings are "local", "git.<repository_slug>".
|
|
992
|
-
Plugin jobs aren't loaded dynamically from a source_path and so are not included in this function
|
|
993
|
-
"""
|
|
994
|
-
paths = {}
|
|
995
|
-
# Locally installed jobs
|
|
996
|
-
if settings.JOBS_ROOT and os.path.exists(settings.JOBS_ROOT):
|
|
997
|
-
paths["local"] = settings.JOBS_ROOT
|
|
998
|
-
|
|
999
|
-
# Jobs derived from Git repositories
|
|
1000
|
-
if settings.GIT_ROOT and os.path.isdir(settings.GIT_ROOT):
|
|
1001
|
-
for repository_record in GitRepository.objects.all():
|
|
1002
|
-
if "extras.job" not in repository_record.provided_contents:
|
|
1003
|
-
# This repository isn't marked as containing jobs that we should use.
|
|
1004
|
-
continue
|
|
1005
|
-
|
|
1006
|
-
try:
|
|
1007
|
-
# In the case where we have multiple Nautobot instances, or multiple worker instances,
|
|
1008
|
-
# they are not required to share a common filesystem; therefore, we may need to refresh our local clone
|
|
1009
|
-
# of the Git repository to ensure that it is in sync with the latest repository clone from any instance.
|
|
1010
|
-
ensure_git_repository(
|
|
1011
|
-
repository_record,
|
|
1012
|
-
head=repository_record.current_head,
|
|
1013
|
-
logger=logger,
|
|
1014
|
-
)
|
|
1015
|
-
except Exception as exc:
|
|
1016
|
-
logger.error(f"Error during local clone of Git repository {repository_record}: {exc}")
|
|
1017
|
-
continue
|
|
1018
|
-
|
|
1019
|
-
jobs_path = os.path.join(repository_record.filesystem_path, "jobs")
|
|
1020
|
-
if os.path.isdir(jobs_path):
|
|
1021
|
-
paths[f"git.{repository_record.slug}"] = jobs_path
|
|
1022
|
-
else:
|
|
1023
|
-
logger.warning(f"Git repository {repository_record} is configured to provide jobs, but none are found!")
|
|
1024
|
-
|
|
1025
|
-
# TODO(Glenn): when a Git repo is deleted or its slug is changed, we update the local filesystem
|
|
1026
|
-
# (see extras/signals.py, extras/models/datasources.py), but as noted above, there may be multiple filesystems
|
|
1027
|
-
# involved, so not all local clones of deleted Git repositories may have been deleted yet.
|
|
1028
|
-
# For now, if we encounter a "leftover" Git repo here, we delete it now.
|
|
1029
|
-
for git_slug in os.listdir(settings.GIT_ROOT):
|
|
1030
|
-
git_path = os.path.join(settings.GIT_ROOT, git_slug)
|
|
1031
|
-
if not os.path.isdir(git_path):
|
|
1032
|
-
logger.warning(
|
|
1033
|
-
f"Found non-directory {git_slug} in {settings.GIT_ROOT}. Only Git repositories should exist here."
|
|
1034
|
-
)
|
|
1035
|
-
elif not os.path.isdir(os.path.join(git_path, ".git")):
|
|
1036
|
-
logger.warning(f"Directory {git_slug} in {settings.GIT_ROOT} does not appear to be a Git repository.")
|
|
1037
|
-
elif not GitRepository.objects.filter(slug=git_slug):
|
|
1038
|
-
logger.warning(f"Deleting unmanaged (leftover?) repository at {git_path}")
|
|
1039
|
-
shutil.rmtree(git_path)
|
|
1040
|
-
|
|
1041
|
-
return paths
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
def get_job_classpaths():
|
|
1045
|
-
"""
|
|
1046
|
-
Get a list of all known Job class_path strings.
|
|
1047
|
-
|
|
1048
|
-
This is used as a cacheable, light-weight alternative to calling get_jobs() or get_job()
|
|
1049
|
-
when all that's needed is to verify whether a given job exists.
|
|
1050
|
-
"""
|
|
1051
|
-
jobs_dict = get_jobs()
|
|
1052
|
-
result = set()
|
|
1053
|
-
for grouping_name, modules_dict in jobs_dict.items():
|
|
1054
|
-
for module_name in modules_dict:
|
|
1055
|
-
for class_name in modules_dict[module_name]["jobs"]:
|
|
1056
|
-
result.add(f"{grouping_name}/{module_name}/{class_name}")
|
|
1057
|
-
return result
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
1119
|
def get_job(class_path):
|
|
1061
1120
|
"""
|
|
1062
|
-
Retrieve a specific job class by its class_path.
|
|
1121
|
+
Retrieve a specific job class by its class_path (<module_name>.<JobClassName>).
|
|
1063
1122
|
|
|
1064
|
-
|
|
1065
|
-
if all you need to do is to verify whether a given class_path exists, use get_job_classpaths() instead.
|
|
1066
|
-
|
|
1067
|
-
Returns None if not found.
|
|
1123
|
+
May return None if the job isn't properly registered with Celery at this time.
|
|
1068
1124
|
"""
|
|
1069
1125
|
try:
|
|
1070
|
-
|
|
1071
|
-
except
|
|
1072
|
-
logger.error(f'Invalid class_path value "{class_path}"')
|
|
1126
|
+
return celery_app.tasks[class_path].__class__
|
|
1127
|
+
except NotRegistered:
|
|
1073
1128
|
return None
|
|
1074
1129
|
|
|
1075
|
-
jobs = get_jobs()
|
|
1076
|
-
return jobs.get(grouping_name, {}).get(module_name, {}).get("jobs", {}).get(class_name, None)
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
class RunJobTaskFailed(Exception):
|
|
1080
|
-
"""Celery task failed for some reason."""
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
@nautobot_task
|
|
1084
|
-
def run_job(data, request, job_result_pk, commit=True, *args, **kwargs):
|
|
1085
|
-
"""
|
|
1086
|
-
Helper function to call the "run()", "test_*()", and "post_run" methods on a Job.
|
|
1087
|
-
|
|
1088
|
-
This function is responsible for setting up the job execution, handing the DB tranaction
|
|
1089
|
-
and rollback conditions, plus post execution cleanup and saving the JobResult record.
|
|
1090
|
-
"""
|
|
1091
|
-
from nautobot.extras.models import JobResult # avoid circular import
|
|
1092
|
-
|
|
1093
|
-
# Getting the correct job result can fail if the stored data cannot be serialized.
|
|
1094
|
-
# Catching `TypeError: the JSON object must be str, bytes or bytearray, not int`
|
|
1095
|
-
job_result = JobResult.objects.get(pk=job_result_pk)
|
|
1096
|
-
|
|
1097
|
-
job_model = job_result.job_model
|
|
1098
|
-
initialization_failure = None
|
|
1099
|
-
job_model = JobModel.objects.get_for_class_path(job_result.name)
|
|
1100
|
-
|
|
1101
|
-
if not job_model.enabled:
|
|
1102
|
-
initialization_failure = f"Job {job_model} is not enabled to be run!"
|
|
1103
|
-
else:
|
|
1104
|
-
job_class = job_model.job_class
|
|
1105
|
-
|
|
1106
|
-
if not job_model.installed or not job_class:
|
|
1107
|
-
initialization_failure = f'Unable to locate job "{job_result.name}" to run it!'
|
|
1108
|
-
|
|
1109
|
-
if initialization_failure:
|
|
1110
|
-
job_result.log(
|
|
1111
|
-
message=initialization_failure,
|
|
1112
|
-
obj=job_model,
|
|
1113
|
-
level_choice=LogLevelChoices.LOG_FAILURE,
|
|
1114
|
-
grouping="initialization",
|
|
1115
|
-
logger=logger,
|
|
1116
|
-
)
|
|
1117
|
-
raise RunJobTaskFailed(initialization_failure)
|
|
1118
|
-
|
|
1119
|
-
job = job_class()
|
|
1120
|
-
job.active_test = "initialization"
|
|
1121
|
-
job.job_result = job_result
|
|
1122
|
-
|
|
1123
|
-
soft_time_limit = job_model.soft_time_limit or settings.CELERY_TASK_SOFT_TIME_LIMIT
|
|
1124
|
-
time_limit = job_model.time_limit or settings.CELERY_TASK_TIME_LIMIT
|
|
1125
|
-
if time_limit <= soft_time_limit:
|
|
1126
|
-
job_result.log(
|
|
1127
|
-
f"The hard time limit of {time_limit} seconds is less than "
|
|
1128
|
-
f"or equal to the soft time limit of {soft_time_limit} seconds. "
|
|
1129
|
-
f"This job will fail silently after {time_limit} seconds.",
|
|
1130
|
-
level_choice=LogLevelChoices.LOG_WARNING,
|
|
1131
|
-
grouping="initialization",
|
|
1132
|
-
logger=logger,
|
|
1133
|
-
)
|
|
1134
|
-
|
|
1135
|
-
file_ids = None
|
|
1136
|
-
try:
|
|
1137
|
-
# Capture the file IDs for any FileProxy objects created so we can cleanup later.
|
|
1138
|
-
file_fields = list(job._get_file_vars())
|
|
1139
|
-
file_ids = [data[f] for f in file_fields]
|
|
1140
|
-
|
|
1141
|
-
# Attempt to resolve serialized data back into original form by creating querysets or model instances
|
|
1142
|
-
# If we fail to find any objects, we consider this a job execution error, and fail.
|
|
1143
|
-
# This might happen when a job sits on the queue for a while (i.e. scheduled) and data has changed
|
|
1144
|
-
# or it might be bad input from an API request, or manual execution.
|
|
1145
|
-
|
|
1146
|
-
data = job_class.deserialize_data(data)
|
|
1147
|
-
# TODO(jathan): Another place where because `log()` is called which mutates `.data`, we must
|
|
1148
|
-
# explicitly call `save()` again. We need to see if we can move more of this to `NauotbotTask`
|
|
1149
|
-
# and/or the DB backend as well.
|
|
1150
|
-
except Exception:
|
|
1151
|
-
stacktrace = traceback.format_exc()
|
|
1152
|
-
job_result.log(
|
|
1153
|
-
f"Error initializing job:\n```\n{stacktrace}\n```",
|
|
1154
|
-
level_choice=LogLevelChoices.LOG_FAILURE,
|
|
1155
|
-
grouping="initialization",
|
|
1156
|
-
logger=logger,
|
|
1157
|
-
)
|
|
1158
|
-
job_result.save()
|
|
1159
|
-
if file_ids:
|
|
1160
|
-
# Cleanup FileProxy objects
|
|
1161
|
-
job.delete_files(*file_ids) # pylint: disable=not-an-iterable
|
|
1162
|
-
raise
|
|
1163
|
-
|
|
1164
|
-
if job_model.read_only:
|
|
1165
|
-
# Force commit to false for read only jobs.
|
|
1166
|
-
commit = False
|
|
1167
|
-
|
|
1168
|
-
# TODO(Glenn): validate that all args required by this job are set in the data or else log helpful errors?
|
|
1169
|
-
|
|
1170
|
-
job.logger.info(f"Running job (commit={commit})")
|
|
1171
|
-
|
|
1172
|
-
# Add the current request as a property of the job
|
|
1173
|
-
job.request = request
|
|
1174
|
-
|
|
1175
|
-
def _run_job():
|
|
1176
|
-
"""
|
|
1177
|
-
Core job execution task.
|
|
1178
|
-
|
|
1179
|
-
We capture this within a subfunction to allow for conditionally wrapping it with the change_logging
|
|
1180
|
-
context manager (which is only relevant if commit == True).
|
|
1181
|
-
|
|
1182
|
-
If the job is marked as read_only == True, then commit is forced to False and no log messages will be
|
|
1183
|
-
emitted related to reverting database changes.
|
|
1184
|
-
"""
|
|
1185
|
-
started = timezone.now()
|
|
1186
|
-
job.results["output"] = ""
|
|
1187
|
-
try:
|
|
1188
|
-
with transaction.atomic():
|
|
1189
|
-
# Script-like behavior
|
|
1190
|
-
job.active_test = "run"
|
|
1191
|
-
output = job.run(data=data, commit=commit)
|
|
1192
|
-
if output:
|
|
1193
|
-
job.results["output"] += "\n" + str(output)
|
|
1194
|
-
|
|
1195
|
-
# Report-like behavior
|
|
1196
|
-
for method_name in job.test_methods:
|
|
1197
|
-
job.active_test = method_name
|
|
1198
|
-
output = getattr(job, method_name)()
|
|
1199
|
-
if output:
|
|
1200
|
-
job.results["output"] += "\n" + str(output)
|
|
1201
|
-
|
|
1202
|
-
job.logger.info("job completed successfully")
|
|
1203
|
-
|
|
1204
|
-
if not commit:
|
|
1205
|
-
raise AbortTransaction("Database changes have been reverted automatically.")
|
|
1206
|
-
|
|
1207
|
-
except AbortTransaction:
|
|
1208
|
-
if not job_model.read_only:
|
|
1209
|
-
job.log_info(message="Database changes have been reverted automatically.")
|
|
1210
|
-
|
|
1211
|
-
except Exception:
|
|
1212
|
-
if not job_model.read_only:
|
|
1213
|
-
job.log_info(message="Database changes have been reverted due to error.")
|
|
1214
|
-
raise
|
|
1215
|
-
|
|
1216
|
-
# TODO(jathan): For now we still need to call `save()` so that any output data from the job
|
|
1217
|
-
# that was stored gets saved to the `JobResult`. We need to consider where this should be
|
|
1218
|
-
# moved as we get closer to eliminating `run_job()` entirely. Hint: Probably inside of
|
|
1219
|
-
# `NautobotTask` class.
|
|
1220
|
-
finally:
|
|
1221
|
-
_data = copy.deepcopy(job_result.data)
|
|
1222
|
-
job_result.refresh_from_db()
|
|
1223
|
-
job_result.data = _data
|
|
1224
|
-
try:
|
|
1225
|
-
job_result.save()
|
|
1226
|
-
except IntegrityError:
|
|
1227
|
-
# handle job_model deleted while job was running
|
|
1228
|
-
job_result.job_model = None
|
|
1229
|
-
job_result.save()
|
|
1230
|
-
if file_ids:
|
|
1231
|
-
job.delete_files(*file_ids) # Cleanup FileProxy objects
|
|
1232
|
-
|
|
1233
|
-
# TODO(jathan): Pretty sure this can also be handled by the backend, but
|
|
1234
|
-
# leaving it for now.
|
|
1235
|
-
# record data about this jobrun in the scheduled_job
|
|
1236
|
-
if job_result.scheduled_job:
|
|
1237
|
-
job_result.scheduled_job.total_run_count += 1
|
|
1238
|
-
job_result.scheduled_job.last_run_at = started
|
|
1239
|
-
job_result.scheduled_job.save()
|
|
1240
|
-
|
|
1241
|
-
# Perform any post-run tasks
|
|
1242
|
-
# 2.0 TODO Remove post_run() method entirely
|
|
1243
|
-
job.active_test = "post_run"
|
|
1244
|
-
output = job.post_run()
|
|
1245
|
-
# TODO(jathan): We need to call `save()` here too so that any appended output from
|
|
1246
|
-
# `post_run` gets stored on the `JobResult`. We need to move this out of here as well.
|
|
1247
|
-
if output:
|
|
1248
|
-
job.results["output"] += "\n" + str(output)
|
|
1249
|
-
job_result.save()
|
|
1250
|
-
|
|
1251
|
-
job_result.refresh_from_db()
|
|
1252
|
-
job.logger.info(f"Job completed in {job_result.duration}")
|
|
1253
|
-
|
|
1254
|
-
# TODO(jathan): For now this is only output from `post_run()` which is not straightforward.
|
|
1255
|
-
# We need to think about what we want to be returned from job runs and stored as
|
|
1256
|
-
# `JobResult.result`, otherwise it will always be `None`.
|
|
1257
|
-
return output
|
|
1258
|
-
|
|
1259
|
-
# Execute the job. If commit == True, wrap it with the change_logging context manager to ensure we
|
|
1260
|
-
# process change logs, webhooks, etc.
|
|
1261
|
-
if commit:
|
|
1262
|
-
context_class = JobHookChangeContext if job_model.is_job_hook_receiver else JobChangeContext
|
|
1263
|
-
change_context = context_class(user=request.user, context_detail=job_model.slug)
|
|
1264
|
-
with change_logging(change_context):
|
|
1265
|
-
output = _run_job()
|
|
1266
|
-
else:
|
|
1267
|
-
output = _run_job()
|
|
1268
|
-
|
|
1269
|
-
# This is just passing through the return value from `post_run()` which for now will always be
|
|
1270
|
-
# `None` (see above).
|
|
1271
|
-
return output
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
@nautobot_task
|
|
1275
|
-
def scheduled_job_handler(*args, **kwargs):
|
|
1276
|
-
"""
|
|
1277
|
-
A thin wrapper around JobResult.enqueue_job() that allows for it to be called as an async task
|
|
1278
|
-
for the purposes of enqueuing scheduled jobs at their recurring intervals. Thus, JobResult.enqueue_job()
|
|
1279
|
-
is responsible for enqueuing the actual job for execution and this method is the task executed
|
|
1280
|
-
by the scheduler to kick off the job execution on a recurring interval.
|
|
1281
|
-
"""
|
|
1282
|
-
from nautobot.extras.models import JobResult # avoid circular import
|
|
1283
|
-
|
|
1284
|
-
user_pk = kwargs.pop("user")
|
|
1285
|
-
user = User.objects.get(pk=user_pk)
|
|
1286
|
-
name = kwargs.pop("name")
|
|
1287
|
-
scheduled_job_pk = kwargs.pop("scheduled_job_pk")
|
|
1288
|
-
celery_kwargs = kwargs.pop("celery_kwargs", {})
|
|
1289
|
-
schedule = ScheduledJob.objects.get(pk=scheduled_job_pk)
|
|
1290
|
-
|
|
1291
|
-
job_content_type = get_job_content_type()
|
|
1292
|
-
JobResult.enqueue_job(
|
|
1293
|
-
run_job, name, job_content_type, user, celery_kwargs=celery_kwargs, schedule=schedule, **kwargs
|
|
1294
|
-
)
|
|
1295
|
-
|
|
1296
1130
|
|
|
1297
1131
|
def enqueue_job_hooks(object_change):
|
|
1298
1132
|
"""
|
|
1299
1133
|
Find job hook(s) assigned to this changed object type + action and enqueue them
|
|
1300
1134
|
to be processed
|
|
1301
1135
|
"""
|
|
1302
|
-
from nautobot.extras.models import JobResult # avoid circular import
|
|
1303
1136
|
|
|
1304
1137
|
# Job hooks cannot trigger other job hooks
|
|
1305
1138
|
if object_change.change_context == ObjectChangeEventContextChoices.CONTEXT_JOB_HOOK:
|
|
@@ -1321,16 +1154,5 @@ def enqueue_job_hooks(object_change):
|
|
|
1321
1154
|
|
|
1322
1155
|
# Enqueue the jobs related to the job_hooks
|
|
1323
1156
|
for job_hook in job_hooks:
|
|
1324
|
-
job_content_type = get_job_content_type()
|
|
1325
1157
|
job_model = job_hook.job
|
|
1326
|
-
|
|
1327
|
-
request.user = object_change.user
|
|
1328
|
-
JobResult.enqueue_job(
|
|
1329
|
-
run_job,
|
|
1330
|
-
job_model.class_path,
|
|
1331
|
-
job_content_type,
|
|
1332
|
-
object_change.user,
|
|
1333
|
-
data=job_model.job_class.serialize_data({"object_change": object_change}),
|
|
1334
|
-
request=copy_safe_request(request),
|
|
1335
|
-
commit=True,
|
|
1336
|
-
)
|
|
1158
|
+
JobResult.enqueue_job(job_model, object_change.user, object_change=object_change.pk)
|