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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from datetime import datetime, timedelta
|
|
2
2
|
import uuid
|
|
3
|
+
import tempfile
|
|
3
4
|
from unittest import mock, skip
|
|
4
5
|
|
|
5
6
|
from django.conf import settings
|
|
@@ -25,10 +26,8 @@ from nautobot.dcim.models import (
|
|
|
25
26
|
Rack,
|
|
26
27
|
RackGroup,
|
|
27
28
|
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
from nautobot.extras.api.nested_serializers import NestedJobResultSerializer
|
|
31
|
-
from nautobot.extras.api.serializers import ConfigContextSerializer
|
|
29
|
+
from nautobot.dcim.tests import test_views
|
|
30
|
+
from nautobot.extras.api.serializers import ConfigContextSerializer, JobResultSerializer
|
|
32
31
|
from nautobot.extras.choices import (
|
|
33
32
|
DynamicGroupOperatorChoices,
|
|
34
33
|
JobExecutionType,
|
|
@@ -67,11 +66,11 @@ from nautobot.extras.models import (
|
|
|
67
66
|
)
|
|
68
67
|
from nautobot.extras.models.jobs import JobHook, JobButton
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
from nautobot.extras.tests.test_relationships import RequiredRelationshipTestMixin
|
|
71
70
|
from nautobot.extras.utils import TaggableClassesQuery
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
from nautobot.ipam.models import VLANGroup
|
|
72
|
+
from nautobot.ipam.factory import VLANFactory
|
|
73
|
+
from nautobot.ipam.models import VLANGroup, VLAN
|
|
75
74
|
from nautobot.users.models import ObjectPermission
|
|
76
75
|
|
|
77
76
|
|
|
@@ -91,35 +90,24 @@ class AppTest(APITestCase):
|
|
|
91
90
|
#
|
|
92
91
|
|
|
93
92
|
|
|
94
|
-
@skip(reason="Content Types are BROKEN")
|
|
95
93
|
class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
96
94
|
model = ComputedField
|
|
97
|
-
brief_fields = [
|
|
98
|
-
"content_type",
|
|
99
|
-
"display",
|
|
100
|
-
"id",
|
|
101
|
-
"label",
|
|
102
|
-
"url",
|
|
103
|
-
]
|
|
104
95
|
choices_fields = ["content_type"]
|
|
105
96
|
create_data = [
|
|
106
97
|
{
|
|
107
98
|
"content_type": "dcim.location",
|
|
108
|
-
"slug": "cf4",
|
|
109
99
|
"label": "Computed Field 4",
|
|
110
100
|
"template": "{{ obj.name }}",
|
|
111
101
|
"fallback_value": "error",
|
|
112
102
|
},
|
|
113
103
|
{
|
|
114
104
|
"content_type": "dcim.location",
|
|
115
|
-
"slug": "cf5",
|
|
116
105
|
"label": "Computed Field 5",
|
|
117
106
|
"template": "{{ obj.name }}",
|
|
118
107
|
"fallback_value": "error",
|
|
119
108
|
},
|
|
120
109
|
{
|
|
121
110
|
"content_type": "dcim.location",
|
|
122
|
-
"slug": "cf6",
|
|
123
111
|
"label": "Computed Field 6",
|
|
124
112
|
"template": "{{ obj.name }}",
|
|
125
113
|
},
|
|
@@ -132,7 +120,7 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
132
120
|
]
|
|
133
121
|
update_data = {
|
|
134
122
|
"content_type": "dcim.location",
|
|
135
|
-
"
|
|
123
|
+
"key": "cf1",
|
|
136
124
|
"label": "My Computed Field",
|
|
137
125
|
}
|
|
138
126
|
bulk_update_data = {
|
|
@@ -146,21 +134,21 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
146
134
|
location_ct = ContentType.objects.get_for_model(Location)
|
|
147
135
|
|
|
148
136
|
ComputedField.objects.create(
|
|
149
|
-
|
|
137
|
+
key="cf1",
|
|
150
138
|
label="Computed Field One",
|
|
151
139
|
template="{{ obj.name }}",
|
|
152
140
|
fallback_value="error",
|
|
153
141
|
content_type=location_ct,
|
|
154
142
|
)
|
|
155
143
|
ComputedField.objects.create(
|
|
156
|
-
|
|
144
|
+
key="cf2",
|
|
157
145
|
label="Computed Field Two",
|
|
158
146
|
template="{{ obj.name }}",
|
|
159
147
|
fallback_value="error",
|
|
160
148
|
content_type=location_ct,
|
|
161
149
|
)
|
|
162
150
|
ComputedField.objects.create(
|
|
163
|
-
|
|
151
|
+
key="cf3",
|
|
164
152
|
label="Computed Field Three",
|
|
165
153
|
template="{{ obj.name }}",
|
|
166
154
|
fallback_value="error",
|
|
@@ -186,7 +174,6 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
186
174
|
|
|
187
175
|
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
188
176
|
model = ConfigContext
|
|
189
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
190
177
|
create_data = [
|
|
191
178
|
{
|
|
192
179
|
"name": "Config Context 4",
|
|
@@ -219,8 +206,11 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
219
206
|
manufacturer = Manufacturer.objects.first()
|
|
220
207
|
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1", slug="device-type-1")
|
|
221
208
|
devicerole = Role.objects.get_for_model(Device).first()
|
|
209
|
+
devicestatus = Status.objects.get_for_model(Device).first()
|
|
222
210
|
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
223
|
-
device = Device.objects.create(
|
|
211
|
+
device = Device.objects.create(
|
|
212
|
+
name="Device 1", device_type=devicetype, role=devicerole, status=devicestatus, location=location
|
|
213
|
+
)
|
|
224
214
|
|
|
225
215
|
# Test default config contexts (created at test setup)
|
|
226
216
|
rendered_context = device.get_config_context()
|
|
@@ -231,7 +221,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
231
221
|
# Test API response as well
|
|
232
222
|
self.add_permissions("dcim.view_device")
|
|
233
223
|
device_url = reverse("dcim-api:device-detail", kwargs={"pk": device.pk})
|
|
234
|
-
response = self.client.get(device_url, **self.header)
|
|
224
|
+
response = self.client.get(device_url + "?include=config_context", **self.header)
|
|
235
225
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
236
226
|
self.assertIn("config_context", response.data)
|
|
237
227
|
self.assertEqual(response.data["config_context"], {"foo": 123, "bar": 456, "baz": 789}, response.data)
|
|
@@ -242,7 +232,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
242
232
|
configcontext4.locations.add(location)
|
|
243
233
|
rendered_context = device.get_config_context()
|
|
244
234
|
self.assertEqual(rendered_context["location_data"], "ABC")
|
|
245
|
-
response = self.client.get(device_url, **self.header)
|
|
235
|
+
response = self.client.get(device_url + "?include=config_context", **self.header)
|
|
246
236
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
247
237
|
self.assertIn("config_context", response.data)
|
|
248
238
|
self.assertEqual(response.data["config_context"]["location_data"], "ABC", response.data["config_context"])
|
|
@@ -253,7 +243,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
253
243
|
configcontext5.locations.add(location)
|
|
254
244
|
rendered_context = device.get_config_context()
|
|
255
245
|
self.assertEqual(rendered_context["foo"], 999)
|
|
256
|
-
response = self.client.get(device_url, **self.header)
|
|
246
|
+
response = self.client.get(device_url + "?include=config_context", **self.header)
|
|
257
247
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
258
248
|
self.assertIn("config_context", response.data)
|
|
259
249
|
self.assertEqual(response.data["config_context"]["foo"], 999, response.data["config_context"])
|
|
@@ -265,7 +255,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
265
255
|
configcontext6.locations.add(location2)
|
|
266
256
|
rendered_context = device.get_config_context()
|
|
267
257
|
self.assertEqual(rendered_context["bar"], 456)
|
|
268
|
-
response = self.client.get(device_url, **self.header)
|
|
258
|
+
response = self.client.get(device_url + "?include=config_context", **self.header)
|
|
269
259
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
270
260
|
self.assertIn("config_context", response.data)
|
|
271
261
|
self.assertEqual(response.data["config_context"]["bar"], 456, response.data["config_context"])
|
|
@@ -289,7 +279,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
289
279
|
}
|
|
290
280
|
response = self.client.post(self._get_list_url(), data, format="json", **self.header)
|
|
291
281
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
292
|
-
self.assertEqual(response.data["config_context_schema"]
|
|
282
|
+
self.assertEqual(response.data["config_context_schema"], self.absolute_api_url(schema))
|
|
293
283
|
|
|
294
284
|
def test_schema_validation_fails(self):
|
|
295
285
|
"""
|
|
@@ -326,7 +316,6 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
326
316
|
|
|
327
317
|
class ConfigContextSchemaTest(APIViewTestCases.APIViewTestCase):
|
|
328
318
|
model = ConfigContextSchema
|
|
329
|
-
brief_fields = ["display", "id", "name", "slug", "url"]
|
|
330
319
|
create_data = [
|
|
331
320
|
{
|
|
332
321
|
"name": "Schema 4",
|
|
@@ -389,31 +378,33 @@ class ContentTypeTest(APITestCase):
|
|
|
389
378
|
|
|
390
379
|
|
|
391
380
|
class CreatedUpdatedFilterTest(APITestCase):
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
location=
|
|
402
|
-
rack_group=
|
|
403
|
-
role=
|
|
381
|
+
@classmethod
|
|
382
|
+
def setUpTestData(cls):
|
|
383
|
+
cls.location1 = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
384
|
+
cls.rackgroup1 = RackGroup.objects.create(
|
|
385
|
+
location=cls.location1, name="Test Rack Group 1", slug="test-rack-group-1"
|
|
386
|
+
)
|
|
387
|
+
cls.rackrole1 = Role.objects.get_for_model(Rack).first()
|
|
388
|
+
cls.rackstatus1 = Status.objects.get_for_model(Rack).first()
|
|
389
|
+
cls.rack1 = Rack.objects.create(
|
|
390
|
+
location=cls.location1,
|
|
391
|
+
rack_group=cls.rackgroup1,
|
|
392
|
+
role=cls.rackrole1,
|
|
393
|
+
status=cls.rackstatus1,
|
|
404
394
|
name="Test Rack 1",
|
|
405
395
|
u_height=42,
|
|
406
396
|
)
|
|
407
|
-
|
|
408
|
-
location=
|
|
409
|
-
rack_group=
|
|
410
|
-
role=
|
|
397
|
+
cls.rack2 = Rack.objects.create(
|
|
398
|
+
location=cls.location1,
|
|
399
|
+
rack_group=cls.rackgroup1,
|
|
400
|
+
role=cls.rackrole1,
|
|
401
|
+
status=cls.rackstatus1,
|
|
411
402
|
name="Test Rack 2",
|
|
412
403
|
u_height=42,
|
|
413
404
|
)
|
|
414
405
|
|
|
415
406
|
# change the created and last_updated of one
|
|
416
|
-
Rack.objects.filter(pk=
|
|
407
|
+
Rack.objects.filter(pk=cls.rack2.pk).update(
|
|
417
408
|
created=make_aware(datetime(2001, 2, 3, 0, 1, 2, 3)),
|
|
418
409
|
last_updated=make_aware(datetime(2001, 2, 3, 1, 2, 3, 4)),
|
|
419
410
|
)
|
|
@@ -483,12 +474,10 @@ class CreatedUpdatedFilterTest(APITestCase):
|
|
|
483
474
|
self.assertEqual(response.data["results"][0]["id"], str(self.rack2.pk))
|
|
484
475
|
|
|
485
476
|
|
|
486
|
-
@skip(reason="Content Types are BROKEN")
|
|
487
477
|
class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
|
488
478
|
"""Tests for the CustomField REST API."""
|
|
489
479
|
|
|
490
480
|
model = CustomField
|
|
491
|
-
brief_fields = ["display", "id", "key", "url"]
|
|
492
481
|
create_data = [
|
|
493
482
|
{
|
|
494
483
|
"content_types": ["dcim.location"],
|
|
@@ -561,7 +550,6 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
561
550
|
|
|
562
551
|
class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|
563
552
|
model = CustomLink
|
|
564
|
-
brief_fields = ["content_type", "display", "id", "name", "url"]
|
|
565
553
|
create_data = [
|
|
566
554
|
{
|
|
567
555
|
"content_type": "dcim.location",
|
|
@@ -627,10 +615,17 @@ class DynamicGroupTestMixin:
|
|
|
627
615
|
def setUpTestData(cls):
|
|
628
616
|
# Create the objects required for devices.
|
|
629
617
|
location_type = LocationType.objects.get(name="Campus")
|
|
618
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
630
619
|
locations = (
|
|
631
|
-
Location.objects.create(
|
|
632
|
-
|
|
633
|
-
|
|
620
|
+
Location.objects.create(
|
|
621
|
+
name="Location 1", slug="location-1", location_type=location_type, status=location_status
|
|
622
|
+
),
|
|
623
|
+
Location.objects.create(
|
|
624
|
+
name="Location 2", slug="location-2", location_type=location_type, status=location_status
|
|
625
|
+
),
|
|
626
|
+
Location.objects.create(
|
|
627
|
+
name="Location 3", slug="location-3", location_type=location_type, status=location_status
|
|
628
|
+
),
|
|
634
629
|
)
|
|
635
630
|
|
|
636
631
|
manufacturer = Manufacturer.objects.first()
|
|
@@ -686,7 +681,6 @@ class DynamicGroupTestMixin:
|
|
|
686
681
|
|
|
687
682
|
class DynamicGroupTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
|
|
688
683
|
model = DynamicGroup
|
|
689
|
-
brief_fields = ["content_type", "display", "id", "name", "url"]
|
|
690
684
|
choices_fields = ["content_type"]
|
|
691
685
|
create_data = [
|
|
692
686
|
{
|
|
@@ -719,7 +713,6 @@ class DynamicGroupTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
|
|
|
719
713
|
|
|
720
714
|
class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
|
|
721
715
|
model = DynamicGroupMembership
|
|
722
|
-
brief_fields = ["display", "group", "id", "operator", "parent_group", "url", "weight"]
|
|
723
716
|
choices_fields = ["operator"]
|
|
724
717
|
|
|
725
718
|
@classmethod
|
|
@@ -778,10 +771,18 @@ class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIView
|
|
|
778
771
|
},
|
|
779
772
|
]
|
|
780
773
|
|
|
774
|
+
# TODO: Either improve test base or or write a more specific test for this model.
|
|
775
|
+
@skip("DynamicGroupMembership has a `name` property but it's the Group name and not exposed on the API")
|
|
776
|
+
def test_list_objects_ascending_ordered(self):
|
|
777
|
+
pass
|
|
778
|
+
|
|
779
|
+
@skip("DynamicGroupMembership has a `name` property but it's the Group name and not exposed on the API")
|
|
780
|
+
def test_list_objects_descending_ordered(self):
|
|
781
|
+
pass
|
|
782
|
+
|
|
781
783
|
|
|
782
784
|
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
|
783
785
|
model = ExportTemplate
|
|
784
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
785
786
|
create_data = [
|
|
786
787
|
{
|
|
787
788
|
"content_type": "dcim.device",
|
|
@@ -827,12 +828,12 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
|
|
827
828
|
|
|
828
829
|
class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
829
830
|
model = GitRepository
|
|
830
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
831
831
|
bulk_update_data = {
|
|
832
832
|
"branch": "develop",
|
|
833
833
|
}
|
|
834
834
|
choices_fields = ["provided_contents"]
|
|
835
835
|
slug_source = "name"
|
|
836
|
+
slugify_function = staticmethod(slugify_dashes_to_underscores)
|
|
836
837
|
|
|
837
838
|
@classmethod
|
|
838
839
|
def setUpTestData(cls):
|
|
@@ -844,37 +845,38 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
844
845
|
cls.repos = (
|
|
845
846
|
GitRepository(
|
|
846
847
|
name="Repo 1",
|
|
847
|
-
slug="
|
|
848
|
+
slug="repo_1",
|
|
848
849
|
remote_url="https://example.com/repo1.git",
|
|
849
850
|
secrets_group=secrets_groups[0],
|
|
850
851
|
),
|
|
851
852
|
GitRepository(
|
|
852
853
|
name="Repo 2",
|
|
853
|
-
slug="
|
|
854
|
+
slug="repo_2",
|
|
854
855
|
remote_url="https://example.com/repo2.git",
|
|
855
856
|
secrets_group=secrets_groups[0],
|
|
856
857
|
),
|
|
857
|
-
GitRepository(name="Repo 3", slug="
|
|
858
|
+
GitRepository(name="Repo 3", slug="repo_3", remote_url="https://example.com/repo3.git"),
|
|
858
859
|
)
|
|
859
860
|
for repo in cls.repos:
|
|
860
|
-
repo.save(
|
|
861
|
+
repo.save()
|
|
861
862
|
|
|
862
863
|
cls.create_data = [
|
|
863
864
|
{
|
|
864
865
|
"name": "New Git Repository 1",
|
|
865
|
-
"slug": "
|
|
866
|
+
"slug": "new_git_repository_1",
|
|
866
867
|
"remote_url": "https://example.com/newrepo1.git",
|
|
867
868
|
"secrets_group": secrets_groups[1].pk,
|
|
869
|
+
"provided_contents": ["extras.configcontext", "extras.exporttemplate"],
|
|
868
870
|
},
|
|
869
871
|
{
|
|
870
872
|
"name": "New Git Repository 2",
|
|
871
|
-
"slug": "
|
|
873
|
+
"slug": "new_git_repository_2",
|
|
872
874
|
"remote_url": "https://example.com/newrepo2.git",
|
|
873
875
|
"secrets_group": secrets_groups[1].pk,
|
|
874
876
|
},
|
|
875
877
|
{
|
|
876
878
|
"name": "New Git Repository 3",
|
|
877
|
-
"slug": "
|
|
879
|
+
"slug": "new_git_repository_3",
|
|
878
880
|
"remote_url": "https://example.com/newrepo3.git",
|
|
879
881
|
"secrets_group": secrets_groups[1].pk,
|
|
880
882
|
},
|
|
@@ -885,6 +887,13 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
885
887
|
},
|
|
886
888
|
]
|
|
887
889
|
|
|
890
|
+
# slug is enforced non-editable in clean because we want it to be providable by the user on creation
|
|
891
|
+
# but not modified afterward
|
|
892
|
+
cls.update_data = {
|
|
893
|
+
"name": "A Different Repo Name",
|
|
894
|
+
"remote_url": "https://example.com/fake.git",
|
|
895
|
+
}
|
|
896
|
+
|
|
888
897
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
889
898
|
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
890
899
|
def test_run_git_sync_no_celery_worker(self, mock_get_worker_count):
|
|
@@ -920,10 +929,9 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
920
929
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
921
930
|
|
|
922
931
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
923
|
-
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
924
|
-
def test_run_git_sync_with_permissions(self,
|
|
932
|
+
@mock.patch("nautobot.extras.api.views.get_worker_count", return_value=1)
|
|
933
|
+
def test_run_git_sync_with_permissions(self, _):
|
|
925
934
|
"""Git sync request can be submitted successfully."""
|
|
926
|
-
mock_get_worker_count.return_value = 1
|
|
927
935
|
self.add_permissions("extras.add_gitrepository")
|
|
928
936
|
self.add_permissions("extras.change_gitrepository")
|
|
929
937
|
url = reverse("extras-api:gitrepository-sync", kwargs={"pk": self.repos[0].id})
|
|
@@ -937,7 +945,7 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
937
945
|
url = self._get_list_url()
|
|
938
946
|
data = {
|
|
939
947
|
"name": "plugin_test",
|
|
940
|
-
"slug": "
|
|
948
|
+
"slug": "plugin_test",
|
|
941
949
|
"remote_url": "https://localhost/plugin-test",
|
|
942
950
|
"provided_contents": ["example_plugin.textfile"],
|
|
943
951
|
}
|
|
@@ -948,8 +956,6 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
948
956
|
|
|
949
957
|
class GraphQLQueryTest(APIViewTestCases.APIViewTestCase):
|
|
950
958
|
model = GraphQLQuery
|
|
951
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
952
|
-
|
|
953
959
|
create_data = [
|
|
954
960
|
{
|
|
955
961
|
"name": "graphql-query-4",
|
|
@@ -1097,7 +1103,6 @@ class ImageAttachmentTest(
|
|
|
1097
1103
|
APIViewTestCases.DeleteObjectViewTestCase,
|
|
1098
1104
|
):
|
|
1099
1105
|
model = ImageAttachment
|
|
1100
|
-
brief_fields = ["display", "id", "image", "name", "url"]
|
|
1101
1106
|
choices_fields = ["content_type"]
|
|
1102
1107
|
|
|
1103
1108
|
@classmethod
|
|
@@ -1131,6 +1136,15 @@ class ImageAttachmentTest(
|
|
|
1131
1136
|
image_width=100,
|
|
1132
1137
|
)
|
|
1133
1138
|
|
|
1139
|
+
# TODO: Unskip after resolving #2908, #2909
|
|
1140
|
+
@skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
|
|
1141
|
+
def test_list_objects_ascending_ordered(self):
|
|
1142
|
+
pass
|
|
1143
|
+
|
|
1144
|
+
@skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
|
|
1145
|
+
def test_list_objects_descending_ordered(self):
|
|
1146
|
+
pass
|
|
1147
|
+
|
|
1134
1148
|
|
|
1135
1149
|
class JobTest(
|
|
1136
1150
|
# note no CreateObjectViewTestCase - we do not support user creation of Job records
|
|
@@ -1142,7 +1156,6 @@ class JobTest(
|
|
|
1142
1156
|
"""Test cases for the Jobs REST API."""
|
|
1143
1157
|
|
|
1144
1158
|
model = Job
|
|
1145
|
-
brief_fields = ["display", "grouping", "id", "job_class_name", "module_name", "name", "slug", "source", "url"]
|
|
1146
1159
|
choices_fields = None
|
|
1147
1160
|
update_data = {
|
|
1148
1161
|
# source, module_name, job_class_name, installed are NOT editable
|
|
@@ -1150,18 +1163,15 @@ class JobTest(
|
|
|
1150
1163
|
"grouping": "Overridden grouping",
|
|
1151
1164
|
"name_override": True,
|
|
1152
1165
|
"name": "Overridden name",
|
|
1153
|
-
"slug": "overridden-slug",
|
|
1154
1166
|
"description_override": True,
|
|
1155
1167
|
"description": "This is an overridden description.",
|
|
1156
1168
|
"enabled": True,
|
|
1157
1169
|
"approval_required_override": True,
|
|
1158
1170
|
"approval_required": True,
|
|
1159
|
-
"
|
|
1160
|
-
"
|
|
1171
|
+
"dryrun_default_override": True,
|
|
1172
|
+
"dryrun_default": True,
|
|
1161
1173
|
"hidden_override": True,
|
|
1162
1174
|
"hidden": True,
|
|
1163
|
-
"read_only_override": True,
|
|
1164
|
-
"read_only": True,
|
|
1165
1175
|
"soft_time_limit_override": True,
|
|
1166
1176
|
"soft_time_limit": 350.1,
|
|
1167
1177
|
"time_limit_override": True,
|
|
@@ -1181,14 +1191,16 @@ class JobTest(
|
|
|
1181
1191
|
|
|
1182
1192
|
def setUp(self):
|
|
1183
1193
|
super().setUp()
|
|
1184
|
-
self.default_job_name = "
|
|
1194
|
+
self.default_job_name = "api_test_job.APITestJob"
|
|
1195
|
+
self.job_class = get_job(self.default_job_name)
|
|
1196
|
+
self.assertIsNotNone(self.job_class)
|
|
1185
1197
|
self.job_model = Job.objects.get_for_class_path(self.default_job_name)
|
|
1186
1198
|
self.job_model.enabled = True
|
|
1187
1199
|
self.job_model.validated_save()
|
|
1188
1200
|
|
|
1189
1201
|
run_success_response_status = status.HTTP_201_CREATED
|
|
1190
1202
|
|
|
1191
|
-
def get_run_url(self, class_path="
|
|
1203
|
+
def get_run_url(self, class_path="api_test_job.APITestJob"):
|
|
1192
1204
|
job_model = Job.objects.get_for_class_path(class_path)
|
|
1193
1205
|
return reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
|
|
1194
1206
|
|
|
@@ -1208,7 +1220,7 @@ class JobTest(
|
|
|
1208
1220
|
|
|
1209
1221
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1210
1222
|
def test_update_job_with_sensitive_variables_set_approval_required_to_true(self):
|
|
1211
|
-
job_model = Job.objects.get_for_class_path("
|
|
1223
|
+
job_model = Job.objects.get_for_class_path("api_test_job.APITestJob")
|
|
1212
1224
|
job_model.has_sensitive_variables = True
|
|
1213
1225
|
job_model.has_sensitive_variables_override = True
|
|
1214
1226
|
job_model.validated_save()
|
|
@@ -1230,7 +1242,7 @@ class JobTest(
|
|
|
1230
1242
|
|
|
1231
1243
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1232
1244
|
def test_update_approval_required_job_set_has_sensitive_variables_to_true(self):
|
|
1233
|
-
job_model = Job.objects.get_for_class_path("
|
|
1245
|
+
job_model = Job.objects.get_for_class_path("api_test_job.APITestJob")
|
|
1234
1246
|
job_model.approval_required = True
|
|
1235
1247
|
job_model.approval_required_override = True
|
|
1236
1248
|
job_model.validated_save()
|
|
@@ -1275,7 +1287,7 @@ class JobTest(
|
|
|
1275
1287
|
mock_get_worker_count.return_value = 1
|
|
1276
1288
|
obj_perm = ObjectPermission(
|
|
1277
1289
|
name="Test permission",
|
|
1278
|
-
constraints={"module_name__in": ["
|
|
1290
|
+
constraints={"module_name__in": ["pass", "fail"]},
|
|
1279
1291
|
actions=["run"],
|
|
1280
1292
|
)
|
|
1281
1293
|
obj_perm.save()
|
|
@@ -1289,10 +1301,10 @@ class JobTest(
|
|
|
1289
1301
|
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
|
|
1290
1302
|
|
|
1291
1303
|
# Try post to permitted job
|
|
1292
|
-
job_model = Job.objects.get_for_class_path("
|
|
1304
|
+
job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
1293
1305
|
job_model.enabled = True
|
|
1294
1306
|
job_model.validated_save()
|
|
1295
|
-
url = self.get_run_url("
|
|
1307
|
+
url = self.get_run_url("pass.TestPass")
|
|
1296
1308
|
response = self.client.post(url, **self.header)
|
|
1297
1309
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1298
1310
|
|
|
@@ -1320,7 +1332,6 @@ class JobTest(
|
|
|
1320
1332
|
self.add_permissions("extras.run_job")
|
|
1321
1333
|
|
|
1322
1334
|
job_model = Job(
|
|
1323
|
-
source="local",
|
|
1324
1335
|
module_name="uninstalled_module",
|
|
1325
1336
|
job_class_name="NoSuchJob",
|
|
1326
1337
|
grouping="Uninstalled Module",
|
|
@@ -1330,7 +1341,7 @@ class JobTest(
|
|
|
1330
1341
|
)
|
|
1331
1342
|
job_model.validated_save()
|
|
1332
1343
|
|
|
1333
|
-
url = self.get_run_url("
|
|
1344
|
+
url = self.get_run_url("uninstalled_module.NoSuchJob")
|
|
1334
1345
|
with disable_warnings("django.request"):
|
|
1335
1346
|
response = self.client.post(url, {}, format="json", **self.header)
|
|
1336
1347
|
self.assertHttpStatus(response, status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
@@ -1351,7 +1362,6 @@ class JobTest(
|
|
|
1351
1362
|
|
|
1352
1363
|
data = {
|
|
1353
1364
|
"data": job_data,
|
|
1354
|
-
"commit": True,
|
|
1355
1365
|
}
|
|
1356
1366
|
|
|
1357
1367
|
url = self.get_run_url()
|
|
@@ -1377,7 +1387,6 @@ class JobTest(
|
|
|
1377
1387
|
|
|
1378
1388
|
data = {
|
|
1379
1389
|
"data": job_data,
|
|
1380
|
-
"commit": True,
|
|
1381
1390
|
"schedule": {
|
|
1382
1391
|
"name": "test",
|
|
1383
1392
|
"interval": "future",
|
|
@@ -1390,17 +1399,19 @@ class JobTest(
|
|
|
1390
1399
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1391
1400
|
|
|
1392
1401
|
schedule = ScheduledJob.objects.last()
|
|
1393
|
-
self.assertEqual(schedule.kwargs["
|
|
1402
|
+
self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
|
|
1394
1403
|
|
|
1395
1404
|
self.assertIn("scheduled_job", response.data)
|
|
1396
1405
|
self.assertIn("job_result", response.data)
|
|
1397
1406
|
self.assertEqual(response.data["scheduled_job"]["id"], str(schedule.pk))
|
|
1407
|
+
self.assertEqual(response.data["scheduled_job"]["url"], self.absolute_api_url(schedule))
|
|
1408
|
+
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1409
|
+
# Python < 3.11 doesn't understand the datetime string "2023-04-27T18:33:16.017865Z",
|
|
1410
|
+
# but it *does* understand the string "2023-04-27T18:33:17.330836+00:00"
|
|
1398
1411
|
self.assertEqual(
|
|
1399
|
-
response.data["scheduled_job"]["
|
|
1400
|
-
|
|
1412
|
+
datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
|
|
1413
|
+
schedule.start_time,
|
|
1401
1414
|
)
|
|
1402
|
-
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1403
|
-
self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
|
|
1404
1415
|
self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
|
|
1405
1416
|
self.assertIsNone(response.data["job_result"])
|
|
1406
1417
|
|
|
@@ -1429,7 +1440,6 @@ class JobTest(
|
|
|
1429
1440
|
|
|
1430
1441
|
data = {
|
|
1431
1442
|
"data": job_data,
|
|
1432
|
-
"commit": True,
|
|
1433
1443
|
# schedule is omitted
|
|
1434
1444
|
}
|
|
1435
1445
|
|
|
@@ -1445,13 +1455,15 @@ class JobTest(
|
|
|
1445
1455
|
self.assertIsNotNone(schedule)
|
|
1446
1456
|
self.assertEqual(schedule.interval, JobExecutionType.TYPE_IMMEDIATELY)
|
|
1447
1457
|
self.assertEqual(schedule.approval_required, self.job_model.approval_required)
|
|
1448
|
-
self.assertEqual(schedule.kwargs["
|
|
1458
|
+
self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
|
|
1449
1459
|
|
|
1450
1460
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1451
1461
|
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
1452
|
-
|
|
1462
|
+
@mock.patch("nautobot.extras.models.jobs.JobResult.enqueue_job")
|
|
1463
|
+
def test_run_job_object_var_lookup(self, mock_enqueue_job, mock_get_worker_count):
|
|
1453
1464
|
"""Job run requests can reference objects by their attributes."""
|
|
1454
1465
|
mock_get_worker_count.return_value = 1
|
|
1466
|
+
mock_enqueue_job.return_value = None
|
|
1455
1467
|
self.add_permissions("extras.run_job")
|
|
1456
1468
|
device_role = Role.objects.get_for_model(Device).first()
|
|
1457
1469
|
job_data = {
|
|
@@ -1463,7 +1475,7 @@ class JobTest(
|
|
|
1463
1475
|
|
|
1464
1476
|
# This handles things like ObjectVar fields looked up by non-UUID
|
|
1465
1477
|
# Jobs are executed with deserialized data
|
|
1466
|
-
deserialized_data =
|
|
1478
|
+
deserialized_data = self.job_class.deserialize_data(job_data)
|
|
1467
1479
|
|
|
1468
1480
|
self.assertEqual(
|
|
1469
1481
|
deserialized_data,
|
|
@@ -1474,24 +1486,39 @@ class JobTest(
|
|
|
1474
1486
|
response = self.client.post(url, {"data": job_data}, format="json", **self.header)
|
|
1475
1487
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1476
1488
|
|
|
1477
|
-
|
|
1478
|
-
self.
|
|
1489
|
+
# Ensure the enqueue_job args deserialize to the same as originally inputted
|
|
1490
|
+
expected_enqueue_job_args = (self.job_model, self.user)
|
|
1491
|
+
expected_enqueue_job_kwargs = {
|
|
1492
|
+
"task_queue": settings.CELERY_TASK_DEFAULT_QUEUE,
|
|
1493
|
+
**self.job_class.serialize_data(deserialized_data),
|
|
1494
|
+
}
|
|
1495
|
+
mock_enqueue_job.assert_called_with(*expected_enqueue_job_args, **expected_enqueue_job_kwargs)
|
|
1479
1496
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1497
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1498
|
+
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
1499
|
+
def test_run_job_response_job_result(self, mock_get_worker_count):
|
|
1500
|
+
"""Test job run response contains nested job result."""
|
|
1501
|
+
mock_get_worker_count.return_value = 1
|
|
1502
|
+
self.add_permissions("extras.run_job")
|
|
1503
|
+
device_role = Role.objects.get_for_model(Device).first()
|
|
1504
|
+
job_data = {
|
|
1505
|
+
"var1": "FooBar",
|
|
1506
|
+
"var2": 123,
|
|
1507
|
+
"var3": False,
|
|
1508
|
+
"var4": {"name": device_role.name},
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
url = self.get_run_url()
|
|
1512
|
+
response = self.client.post(url, {"data": job_data}, format="json", **self.header)
|
|
1513
|
+
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1514
|
+
|
|
1515
|
+
job_result = JobResult.objects.get(name=self.job_model.name)
|
|
1484
1516
|
|
|
1485
1517
|
self.assertIn("scheduled_job", response.data)
|
|
1486
1518
|
self.assertIn("job_result", response.data)
|
|
1487
1519
|
self.assertIsNone(response.data["scheduled_job"])
|
|
1488
|
-
# The urls in a NestedJobResultSerializer depends on the request context, which we don't have
|
|
1489
1520
|
data_job_result = response.data["job_result"]
|
|
1490
|
-
|
|
1491
|
-
del data_job_result["user"]["url"]
|
|
1492
|
-
expected_data_job_result = NestedJobResultSerializer(job_result, context={"request": None}).data
|
|
1493
|
-
del expected_data_job_result["url"]
|
|
1494
|
-
del expected_data_job_result["user"]["url"]
|
|
1521
|
+
expected_data_job_result = JobResultSerializer(job_result, context={"request": response.wsgi_request}).data
|
|
1495
1522
|
self.assertEqual(data_job_result, expected_data_job_result)
|
|
1496
1523
|
|
|
1497
1524
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -1501,7 +1528,7 @@ class JobTest(
|
|
|
1501
1528
|
|
|
1502
1529
|
test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
|
|
1503
1530
|
|
|
1504
|
-
job_model = Job.objects.get_for_class_path("
|
|
1531
|
+
job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
|
|
1505
1532
|
job_model.enabled = True
|
|
1506
1533
|
job_model.validated_save()
|
|
1507
1534
|
|
|
@@ -1512,10 +1539,9 @@ class JobTest(
|
|
|
1512
1539
|
"var2": "Ground control to Major Tom",
|
|
1513
1540
|
"var23": "Commencing countdown, engines on",
|
|
1514
1541
|
"var1": test_file,
|
|
1515
|
-
"_commit": True,
|
|
1516
1542
|
}
|
|
1517
1543
|
|
|
1518
|
-
url = self.get_run_url(class_path="
|
|
1544
|
+
url = self.get_run_url(class_path="field_order.TestFieldOrder")
|
|
1519
1545
|
response = self.client.post(url, data=job_data, **self.header)
|
|
1520
1546
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1521
1547
|
|
|
@@ -1526,7 +1552,7 @@ class JobTest(
|
|
|
1526
1552
|
|
|
1527
1553
|
test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
|
|
1528
1554
|
|
|
1529
|
-
job_model = Job.objects.get_for_class_path("
|
|
1555
|
+
job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
|
|
1530
1556
|
job_model.enabled = True
|
|
1531
1557
|
job_model.validated_save()
|
|
1532
1558
|
|
|
@@ -1539,7 +1565,7 @@ class JobTest(
|
|
|
1539
1565
|
"var1": test_file,
|
|
1540
1566
|
}
|
|
1541
1567
|
|
|
1542
|
-
url = self.get_run_url(class_path="
|
|
1568
|
+
url = self.get_run_url(class_path="field_order.TestFieldOrder")
|
|
1543
1569
|
response = self.client.post(url, data=job_data, **self.header)
|
|
1544
1570
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1545
1571
|
|
|
@@ -1550,7 +1576,7 @@ class JobTest(
|
|
|
1550
1576
|
|
|
1551
1577
|
test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
|
|
1552
1578
|
|
|
1553
|
-
job_model = Job.objects.get_for_class_path("
|
|
1579
|
+
job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
|
|
1554
1580
|
job_model.enabled = True
|
|
1555
1581
|
job_model.validated_save()
|
|
1556
1582
|
|
|
@@ -1561,13 +1587,12 @@ class JobTest(
|
|
|
1561
1587
|
"var2": "Ground control to Major Tom",
|
|
1562
1588
|
"var23": "Commencing countdown, engines on",
|
|
1563
1589
|
"var1": test_file,
|
|
1564
|
-
"_commit": True,
|
|
1565
1590
|
"_schedule_start_time": str(datetime.now() + timedelta(minutes=1)),
|
|
1566
1591
|
"_schedule_interval": "future",
|
|
1567
1592
|
"_schedule_name": "test",
|
|
1568
1593
|
}
|
|
1569
1594
|
|
|
1570
|
-
url = self.get_run_url(class_path="
|
|
1595
|
+
url = self.get_run_url(class_path="field_order.TestFieldOrder")
|
|
1571
1596
|
response = self.client.post(url, data=job_data, **self.header)
|
|
1572
1597
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1573
1598
|
|
|
@@ -1580,7 +1605,6 @@ class JobTest(
|
|
|
1580
1605
|
d = Role.objects.get_for_model(Device).first()
|
|
1581
1606
|
data = {
|
|
1582
1607
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1583
|
-
"commit": True,
|
|
1584
1608
|
"schedule": {
|
|
1585
1609
|
"start_time": str(datetime.now() + timedelta(minutes=1)),
|
|
1586
1610
|
"interval": "future",
|
|
@@ -1593,17 +1617,17 @@ class JobTest(
|
|
|
1593
1617
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1594
1618
|
|
|
1595
1619
|
schedule = ScheduledJob.objects.last()
|
|
1596
|
-
self.assertEqual(schedule.kwargs["scheduled_job_pk"], str(schedule.pk))
|
|
1597
|
-
|
|
1598
1620
|
self.assertIn("scheduled_job", response.data)
|
|
1599
1621
|
self.assertIn("job_result", response.data)
|
|
1600
1622
|
self.assertEqual(response.data["scheduled_job"]["id"], str(schedule.pk))
|
|
1623
|
+
self.assertEqual(response.data["scheduled_job"]["url"], self.absolute_api_url(schedule))
|
|
1624
|
+
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1625
|
+
# Python < 3.11 doesn't understand the datetime string "2023-04-27T18:33:16.017865Z",
|
|
1626
|
+
# but it *does* understand the string "2023-04-27T18:33:17.330836+00:00"
|
|
1601
1627
|
self.assertEqual(
|
|
1602
|
-
response.data["scheduled_job"]["
|
|
1603
|
-
|
|
1628
|
+
datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
|
|
1629
|
+
schedule.start_time,
|
|
1604
1630
|
)
|
|
1605
|
-
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1606
|
-
self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
|
|
1607
1631
|
self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
|
|
1608
1632
|
self.assertIsNone(response.data["job_result"])
|
|
1609
1633
|
|
|
@@ -1620,7 +1644,6 @@ class JobTest(
|
|
|
1620
1644
|
url = reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
|
|
1621
1645
|
data = {
|
|
1622
1646
|
"data": {},
|
|
1623
|
-
"commit": True,
|
|
1624
1647
|
"schedule": {
|
|
1625
1648
|
"start_time": str(datetime.now() + timedelta(minutes=1)),
|
|
1626
1649
|
"interval": "future",
|
|
@@ -1651,7 +1674,6 @@ class JobTest(
|
|
|
1651
1674
|
url = reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
|
|
1652
1675
|
data = {
|
|
1653
1676
|
"data": {},
|
|
1654
|
-
"commit": True,
|
|
1655
1677
|
"schedule": {
|
|
1656
1678
|
"interval": "immediately",
|
|
1657
1679
|
"name": "test",
|
|
@@ -1668,30 +1690,27 @@ class JobTest(
|
|
|
1668
1690
|
)
|
|
1669
1691
|
|
|
1670
1692
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1671
|
-
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
1672
|
-
def test_run_a_job_with_sensitive_variables_immediately(self,
|
|
1673
|
-
mock_get_worker_count.return_value = 1
|
|
1693
|
+
@mock.patch("nautobot.extras.api.views.get_worker_count", return_value=1)
|
|
1694
|
+
def test_run_a_job_with_sensitive_variables_immediately(self, _):
|
|
1674
1695
|
self.add_permissions("extras.run_job")
|
|
1675
1696
|
d = Role.objects.get_for_model(Device).first()
|
|
1676
1697
|
data = {
|
|
1677
1698
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1678
|
-
"commit": True,
|
|
1679
1699
|
"schedule": {
|
|
1680
1700
|
"interval": "immediately",
|
|
1681
1701
|
"name": "test",
|
|
1682
1702
|
},
|
|
1683
1703
|
}
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
job.validated_save()
|
|
1704
|
+
self.job_model.has_sensitive_variables = True
|
|
1705
|
+
self.job_model.has_sensitive_variables_override = True
|
|
1706
|
+
self.job_model.validated_save()
|
|
1688
1707
|
|
|
1689
1708
|
url = self.get_run_url()
|
|
1690
1709
|
response = self.client.post(url, data, format="json", **self.header)
|
|
1691
1710
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1692
1711
|
|
|
1693
|
-
job_result = JobResult.objects.get(name=self.
|
|
1694
|
-
self.assertEqual(job_result.task_kwargs,
|
|
1712
|
+
job_result = JobResult.objects.get(name=self.job_model.name)
|
|
1713
|
+
self.assertEqual(job_result.task_kwargs, {})
|
|
1695
1714
|
|
|
1696
1715
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1697
1716
|
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
@@ -1701,7 +1720,6 @@ class JobTest(
|
|
|
1701
1720
|
d = Role.objects.get_for_model(Device).first()
|
|
1702
1721
|
data = {
|
|
1703
1722
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1704
|
-
"commit": True,
|
|
1705
1723
|
"schedule": {
|
|
1706
1724
|
"start_time": str(datetime.now() - timedelta(minutes=1)),
|
|
1707
1725
|
"interval": "future",
|
|
@@ -1721,7 +1739,6 @@ class JobTest(
|
|
|
1721
1739
|
d = Role.objects.get_for_model(Device).first()
|
|
1722
1740
|
data = {
|
|
1723
1741
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1724
|
-
"commit": True,
|
|
1725
1742
|
"schedule": {
|
|
1726
1743
|
"start_time": str(datetime.now() + timedelta(minutes=1)),
|
|
1727
1744
|
"interval": "hourly",
|
|
@@ -1738,12 +1755,14 @@ class JobTest(
|
|
|
1738
1755
|
self.assertIn("scheduled_job", response.data)
|
|
1739
1756
|
self.assertIn("job_result", response.data)
|
|
1740
1757
|
self.assertEqual(response.data["scheduled_job"]["id"], str(schedule.pk))
|
|
1758
|
+
self.assertEqual(response.data["scheduled_job"]["url"], self.absolute_api_url(schedule))
|
|
1759
|
+
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1760
|
+
# Python < 3.11 doesn't understand the datetime string "2023-04-27T18:33:16.017865Z",
|
|
1761
|
+
# but it *does* understand the string "2023-04-27T18:33:17.330836+00:00"
|
|
1741
1762
|
self.assertEqual(
|
|
1742
|
-
response.data["scheduled_job"]["
|
|
1743
|
-
|
|
1763
|
+
datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
|
|
1764
|
+
schedule.start_time,
|
|
1744
1765
|
)
|
|
1745
|
-
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1746
|
-
self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
|
|
1747
1766
|
self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
|
|
1748
1767
|
self.assertIsNone(response.data["job_result"])
|
|
1749
1768
|
|
|
@@ -1753,7 +1772,6 @@ class JobTest(
|
|
|
1753
1772
|
|
|
1754
1773
|
data = {
|
|
1755
1774
|
"data": "invalid",
|
|
1756
|
-
"commit": True,
|
|
1757
1775
|
}
|
|
1758
1776
|
|
|
1759
1777
|
url = self.get_run_url()
|
|
@@ -1773,7 +1791,6 @@ class JobTest(
|
|
|
1773
1791
|
|
|
1774
1792
|
data = {
|
|
1775
1793
|
"data": job_data,
|
|
1776
|
-
"commit": True,
|
|
1777
1794
|
}
|
|
1778
1795
|
|
|
1779
1796
|
url = self.get_run_url()
|
|
@@ -1792,7 +1809,6 @@ class JobTest(
|
|
|
1792
1809
|
|
|
1793
1810
|
data = {
|
|
1794
1811
|
"data": job_data,
|
|
1795
|
-
"commit": True,
|
|
1796
1812
|
}
|
|
1797
1813
|
|
|
1798
1814
|
url = self.get_run_url()
|
|
@@ -1808,7 +1824,6 @@ class JobTest(
|
|
|
1808
1824
|
d = Role.objects.get_for_model(Device).first()
|
|
1809
1825
|
data = {
|
|
1810
1826
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1811
|
-
"commit": True,
|
|
1812
1827
|
"task_queue": "invalid",
|
|
1813
1828
|
}
|
|
1814
1829
|
|
|
@@ -1827,7 +1842,6 @@ class JobTest(
|
|
|
1827
1842
|
d = Role.objects.get_for_model(Device).first()
|
|
1828
1843
|
data = {
|
|
1829
1844
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1830
|
-
"commit": True,
|
|
1831
1845
|
"task_queue": settings.CELERY_TASK_DEFAULT_QUEUE,
|
|
1832
1846
|
}
|
|
1833
1847
|
|
|
@@ -1840,21 +1854,28 @@ class JobTest(
|
|
|
1840
1854
|
def test_run_job_with_default_queue_with_empty_job_model_task_queues(self, _):
|
|
1841
1855
|
self.add_permissions("extras.run_job")
|
|
1842
1856
|
data = {
|
|
1843
|
-
"commit": True,
|
|
1844
1857
|
"task_queue": settings.CELERY_TASK_DEFAULT_QUEUE,
|
|
1845
1858
|
}
|
|
1846
1859
|
|
|
1847
|
-
job_model = Job.objects.get_for_class_path("
|
|
1860
|
+
job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
1848
1861
|
job_model.enabled = True
|
|
1849
1862
|
job_model.validated_save()
|
|
1850
|
-
url = self.get_run_url("
|
|
1863
|
+
url = self.get_run_url("pass.TestPass")
|
|
1851
1864
|
response = self.client.post(url, data, format="json", **self.header)
|
|
1852
1865
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1853
1866
|
|
|
1867
|
+
# TODO: Either improve test base or or write a more specific test for this model.
|
|
1868
|
+
@skip("Job has a `name` property but grouping is also used to sort Jobs")
|
|
1869
|
+
def test_list_objects_ascending_ordered(self):
|
|
1870
|
+
pass
|
|
1871
|
+
|
|
1872
|
+
@skip("Job has a `name` property but grouping is also used to sort Jobs")
|
|
1873
|
+
def test_list_objects_descending_ordered(self):
|
|
1874
|
+
pass
|
|
1875
|
+
|
|
1854
1876
|
|
|
1855
1877
|
class JobHookTest(APIViewTestCases.APIViewTestCase):
|
|
1856
1878
|
model = JobHook
|
|
1857
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
1858
1879
|
choices_fields = []
|
|
1859
1880
|
update_data = {
|
|
1860
1881
|
"name": "Overridden name",
|
|
@@ -1961,7 +1982,6 @@ class JobHookTest(APIViewTestCases.APIViewTestCase):
|
|
|
1961
1982
|
|
|
1962
1983
|
class JobButtonTest(APIViewTestCases.APIViewTestCase):
|
|
1963
1984
|
model = JobButton
|
|
1964
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
1965
1985
|
choices_fields = ["button_class"]
|
|
1966
1986
|
|
|
1967
1987
|
@classmethod
|
|
@@ -2020,49 +2040,37 @@ class JobResultTest(
|
|
|
2020
2040
|
APIViewTestCases.DeleteObjectViewTestCase,
|
|
2021
2041
|
):
|
|
2022
2042
|
model = JobResult
|
|
2023
|
-
brief_fields = ["date_created", "date_done", "display", "id", "name", "status", "url", "user"]
|
|
2024
2043
|
|
|
2025
2044
|
@classmethod
|
|
2026
2045
|
def setUpTestData(cls):
|
|
2027
2046
|
jobs = Job.objects.all()[:2]
|
|
2028
|
-
job_ct = ContentType.objects.get_for_model(Job)
|
|
2029
|
-
git_ct = ContentType.objects.get_for_model(GitRepository)
|
|
2030
2047
|
|
|
2031
2048
|
JobResult.objects.create(
|
|
2032
2049
|
job_model=jobs[0],
|
|
2033
2050
|
name=jobs[0].class_path,
|
|
2034
|
-
|
|
2035
|
-
date_done=datetime.now(),
|
|
2051
|
+
date_done=now(),
|
|
2036
2052
|
user=None,
|
|
2037
2053
|
status=JobResultStatusChoices.STATUS_SUCCESS,
|
|
2038
|
-
|
|
2039
|
-
task_kwargs=None,
|
|
2054
|
+
task_kwargs={},
|
|
2040
2055
|
scheduled_job=None,
|
|
2041
|
-
task_id=uuid.uuid4(),
|
|
2042
2056
|
)
|
|
2043
2057
|
JobResult.objects.create(
|
|
2044
2058
|
job_model=None,
|
|
2045
|
-
name="
|
|
2046
|
-
|
|
2047
|
-
date_done=datetime.now(),
|
|
2059
|
+
name="deleted_module.deleted_job",
|
|
2060
|
+
date_done=now(),
|
|
2048
2061
|
user=None,
|
|
2049
2062
|
status=JobResultStatusChoices.STATUS_SUCCESS,
|
|
2050
|
-
data=None,
|
|
2051
2063
|
task_kwargs={"repository_pk": uuid.uuid4()},
|
|
2052
2064
|
scheduled_job=None,
|
|
2053
|
-
task_id=uuid.uuid4(),
|
|
2054
2065
|
)
|
|
2055
2066
|
JobResult.objects.create(
|
|
2056
2067
|
job_model=jobs[1],
|
|
2057
2068
|
name=jobs[1].class_path,
|
|
2058
|
-
obj_type=job_ct,
|
|
2059
2069
|
date_done=None,
|
|
2060
2070
|
user=None,
|
|
2061
2071
|
status=JobResultStatusChoices.STATUS_PENDING,
|
|
2062
|
-
data=None,
|
|
2063
2072
|
task_kwargs={"data": {"device": uuid.uuid4(), "multichoices": ["red", "green"], "checkbox": False}},
|
|
2064
2073
|
scheduled_job=None,
|
|
2065
|
-
task_id=uuid.uuid4(),
|
|
2066
2074
|
)
|
|
2067
2075
|
|
|
2068
2076
|
|
|
@@ -2071,27 +2079,11 @@ class JobLogEntryTest(
|
|
|
2071
2079
|
APIViewTestCases.ListObjectsViewTestCase,
|
|
2072
2080
|
):
|
|
2073
2081
|
model = JobLogEntry
|
|
2074
|
-
brief_fields = [
|
|
2075
|
-
"absolute_url",
|
|
2076
|
-
"created",
|
|
2077
|
-
"display",
|
|
2078
|
-
"grouping",
|
|
2079
|
-
"id",
|
|
2080
|
-
"job_result",
|
|
2081
|
-
"log_level",
|
|
2082
|
-
"log_object",
|
|
2083
|
-
"message",
|
|
2084
|
-
"url",
|
|
2085
|
-
]
|
|
2086
2082
|
choices_fields = []
|
|
2087
2083
|
|
|
2088
2084
|
@classmethod
|
|
2089
2085
|
def setUpTestData(cls):
|
|
2090
|
-
cls.job_result = JobResult.objects.create(
|
|
2091
|
-
name="test",
|
|
2092
|
-
task_id=uuid.uuid4(),
|
|
2093
|
-
obj_type=ContentType.objects.get_for_model(GitRepository),
|
|
2094
|
-
)
|
|
2086
|
+
cls.job_result = JobResult.objects.create(name="test")
|
|
2095
2087
|
|
|
2096
2088
|
for log_level in ("debug", "info", "success", "warning"):
|
|
2097
2089
|
JobLogEntry.objects.create(
|
|
@@ -2114,16 +2106,15 @@ class ScheduledJobTest(
|
|
|
2114
2106
|
APIViewTestCases.ListObjectsViewTestCase,
|
|
2115
2107
|
):
|
|
2116
2108
|
model = ScheduledJob
|
|
2117
|
-
brief_fields = ["crontab", "display", "id", "interval", "name", "start_time", "url"]
|
|
2118
2109
|
choices_fields = []
|
|
2119
2110
|
|
|
2120
2111
|
@classmethod
|
|
2121
2112
|
def setUpTestData(cls):
|
|
2122
2113
|
user = User.objects.create(username="user1", is_active=True)
|
|
2123
|
-
job_model = Job.objects.get_for_class_path("
|
|
2114
|
+
job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
2124
2115
|
ScheduledJob.objects.create(
|
|
2125
2116
|
name="test1",
|
|
2126
|
-
task="
|
|
2117
|
+
task="pass.TestPass",
|
|
2127
2118
|
job_class=job_model.class_path,
|
|
2128
2119
|
job_model=job_model,
|
|
2129
2120
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2133,7 +2124,7 @@ class ScheduledJobTest(
|
|
|
2133
2124
|
)
|
|
2134
2125
|
ScheduledJob.objects.create(
|
|
2135
2126
|
name="test2",
|
|
2136
|
-
task="
|
|
2127
|
+
task="pass.TestPass",
|
|
2137
2128
|
job_class=job_model.class_path,
|
|
2138
2129
|
job_model=job_model,
|
|
2139
2130
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2143,7 +2134,7 @@ class ScheduledJobTest(
|
|
|
2143
2134
|
)
|
|
2144
2135
|
ScheduledJob.objects.create(
|
|
2145
2136
|
name="test3",
|
|
2146
|
-
task="
|
|
2137
|
+
task="pass.TestPass",
|
|
2147
2138
|
job_class=job_model.class_path,
|
|
2148
2139
|
job_model=job_model,
|
|
2149
2140
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2152,17 +2143,26 @@ class ScheduledJobTest(
|
|
|
2152
2143
|
start_time=now(),
|
|
2153
2144
|
)
|
|
2154
2145
|
|
|
2146
|
+
# TODO: Unskip after resolving #2908, #2909
|
|
2147
|
+
@skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
|
|
2148
|
+
def test_list_objects_ascending_ordered(self):
|
|
2149
|
+
pass
|
|
2150
|
+
|
|
2151
|
+
@skip("DRF's built-in OrderingFilter triggering natural key attribute error in our base")
|
|
2152
|
+
def test_list_objects_descending_ordered(self):
|
|
2153
|
+
pass
|
|
2154
|
+
|
|
2155
2155
|
|
|
2156
2156
|
class JobApprovalTest(APITestCase):
|
|
2157
2157
|
@classmethod
|
|
2158
2158
|
def setUpTestData(cls):
|
|
2159
2159
|
cls.additional_user = User.objects.create(username="user1", is_active=True)
|
|
2160
|
-
cls.job_model = Job.objects.get_for_class_path("
|
|
2160
|
+
cls.job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
2161
2161
|
cls.job_model.enabled = True
|
|
2162
2162
|
cls.job_model.save()
|
|
2163
2163
|
cls.scheduled_job = ScheduledJob.objects.create(
|
|
2164
2164
|
name="test",
|
|
2165
|
-
task="
|
|
2165
|
+
task="pass.TestPass",
|
|
2166
2166
|
job_class=cls.job_model.class_path,
|
|
2167
2167
|
job_model=cls.job_model,
|
|
2168
2168
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2170,6 +2170,19 @@ class JobApprovalTest(APITestCase):
|
|
|
2170
2170
|
approval_required=True,
|
|
2171
2171
|
start_time=now(),
|
|
2172
2172
|
)
|
|
2173
|
+
cls.dryrun_job_model = Job.objects.get_for_class_path("dry_run.TestDryRun")
|
|
2174
|
+
cls.dryrun_job_model.enabled = True
|
|
2175
|
+
cls.dryrun_job_model.save()
|
|
2176
|
+
cls.dryrun_scheduled_job = ScheduledJob.objects.create(
|
|
2177
|
+
name="test",
|
|
2178
|
+
task="dry_run.TestDryRun",
|
|
2179
|
+
job_class=cls.dryrun_job_model.class_path,
|
|
2180
|
+
job_model=cls.dryrun_job_model,
|
|
2181
|
+
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
2182
|
+
user=cls.additional_user,
|
|
2183
|
+
approval_required=True,
|
|
2184
|
+
start_time=now(),
|
|
2185
|
+
)
|
|
2173
2186
|
|
|
2174
2187
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2175
2188
|
def test_approve_job_anonymous(self):
|
|
@@ -2203,7 +2216,7 @@ class JobApprovalTest(APITestCase):
|
|
|
2203
2216
|
self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
|
|
2204
2217
|
scheduled_job = ScheduledJob.objects.create(
|
|
2205
2218
|
name="test",
|
|
2206
|
-
task="
|
|
2219
|
+
task="pass.TestPass",
|
|
2207
2220
|
job_class=self.job_model.class_path,
|
|
2208
2221
|
job_model=self.job_model,
|
|
2209
2222
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2227,7 +2240,7 @@ class JobApprovalTest(APITestCase):
|
|
|
2227
2240
|
self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
|
|
2228
2241
|
scheduled_job = ScheduledJob.objects.create(
|
|
2229
2242
|
name="test",
|
|
2230
|
-
task="
|
|
2243
|
+
task="pass.TestPass",
|
|
2231
2244
|
job_class=self.job_model.class_path,
|
|
2232
2245
|
job_model=self.job_model,
|
|
2233
2246
|
interval=JobExecutionType.TYPE_FUTURE,
|
|
@@ -2245,7 +2258,7 @@ class JobApprovalTest(APITestCase):
|
|
|
2245
2258
|
self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
|
|
2246
2259
|
scheduled_job = ScheduledJob.objects.create(
|
|
2247
2260
|
name="test",
|
|
2248
|
-
task="
|
|
2261
|
+
task="pass.TestPass",
|
|
2249
2262
|
job_class=self.job_model.class_path,
|
|
2250
2263
|
job_model=self.job_model,
|
|
2251
2264
|
interval=JobExecutionType.TYPE_FUTURE,
|
|
@@ -2289,7 +2302,7 @@ class JobApprovalTest(APITestCase):
|
|
|
2289
2302
|
|
|
2290
2303
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
2291
2304
|
def test_dry_run_job_without_permission(self):
|
|
2292
|
-
url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.
|
|
2305
|
+
url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.dryrun_scheduled_job.pk})
|
|
2293
2306
|
with disable_warnings("django.request"):
|
|
2294
2307
|
response = self.client.post(url, **self.header)
|
|
2295
2308
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
@@ -2297,29 +2310,27 @@ class JobApprovalTest(APITestCase):
|
|
|
2297
2310
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2298
2311
|
def test_dry_run_job_without_run_job_permission(self):
|
|
2299
2312
|
self.add_permissions("extras.view_scheduledjob")
|
|
2300
|
-
url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.
|
|
2313
|
+
url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.dryrun_scheduled_job.pk})
|
|
2301
2314
|
response = self.client.post(url, **self.header)
|
|
2302
2315
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
2303
2316
|
|
|
2304
2317
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2305
2318
|
def test_dry_run_job(self):
|
|
2306
2319
|
self.add_permissions("extras.run_job", "extras.view_scheduledjob")
|
|
2307
|
-
url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.
|
|
2320
|
+
url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.dryrun_scheduled_job.pk})
|
|
2308
2321
|
response = self.client.post(url, **self.header)
|
|
2309
2322
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2310
2323
|
|
|
2324
|
+
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2325
|
+
def test_dry_run_not_supported(self):
|
|
2326
|
+
self.add_permissions("extras.run_job", "extras.view_scheduledjob")
|
|
2327
|
+
url = reverse("extras-api:scheduledjob-dry-run", kwargs={"pk": self.scheduled_job.pk})
|
|
2328
|
+
response = self.client.post(url, **self.header)
|
|
2329
|
+
self.assertHttpStatus(response, status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
2330
|
+
|
|
2311
2331
|
|
|
2312
2332
|
class NoteTest(APIViewTestCases.APIViewTestCase):
|
|
2313
2333
|
model = Note
|
|
2314
|
-
brief_fields = [
|
|
2315
|
-
"assigned_object",
|
|
2316
|
-
"display",
|
|
2317
|
-
"id",
|
|
2318
|
-
"note",
|
|
2319
|
-
"slug",
|
|
2320
|
-
"url",
|
|
2321
|
-
"user",
|
|
2322
|
-
]
|
|
2323
2334
|
choices_fields = ["assigned_object_type"]
|
|
2324
2335
|
|
|
2325
2336
|
@classmethod
|
|
@@ -2370,376 +2381,363 @@ class NoteTest(APIViewTestCases.APIViewTestCase):
|
|
|
2370
2381
|
)
|
|
2371
2382
|
|
|
2372
2383
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
#
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
#
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
#
|
|
2627
|
-
|
|
2628
|
-
#
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
#
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
#
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
#
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
#
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
#
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
#
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
#
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
#
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
# if method == "post":
|
|
2729
|
-
# self.assertHttpStatus(response, 201)
|
|
2730
|
-
# else:
|
|
2731
|
-
# self.assertHttpStatus(response, 200)
|
|
2732
|
-
|
|
2733
|
-
# # Check the relationship associations were actually created
|
|
2734
|
-
# for vlan in response.json():
|
|
2735
|
-
# associated_device = vlan["relationships"]["vlans-devices-m2m"]["source"]["objects"][0]
|
|
2736
|
-
# self.assertEqual(str(device_for_association.id), associated_device["id"])
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
@skip(reason="Content Types are BROKEN")
|
|
2384
|
+
class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTestMixin):
|
|
2385
|
+
model = Relationship
|
|
2386
|
+
|
|
2387
|
+
create_data = [
|
|
2388
|
+
{
|
|
2389
|
+
"label": "Device VLANs",
|
|
2390
|
+
"key": "device_vlans",
|
|
2391
|
+
"type": "many-to-many",
|
|
2392
|
+
"source_type": "ipam.vlan",
|
|
2393
|
+
"destination_type": "dcim.device",
|
|
2394
|
+
},
|
|
2395
|
+
{
|
|
2396
|
+
"label": "Primary VLAN",
|
|
2397
|
+
"key": "primary_vlan",
|
|
2398
|
+
"type": "one-to-many",
|
|
2399
|
+
"source_type": "ipam.vlan",
|
|
2400
|
+
"destination_type": "dcim.device",
|
|
2401
|
+
},
|
|
2402
|
+
{
|
|
2403
|
+
"label": "Primary Interface",
|
|
2404
|
+
"key": "primary_interface",
|
|
2405
|
+
"type": "one-to-one",
|
|
2406
|
+
"source_type": "dcim.device",
|
|
2407
|
+
"source_label": "primary interface",
|
|
2408
|
+
"destination_type": "dcim.interface",
|
|
2409
|
+
"destination_hidden": True,
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
"label": "Relationship 1",
|
|
2413
|
+
"type": "one-to-one",
|
|
2414
|
+
"source_type": "dcim.device",
|
|
2415
|
+
"source_label": "primary interface",
|
|
2416
|
+
"destination_type": "dcim.interface",
|
|
2417
|
+
"destination_hidden": True,
|
|
2418
|
+
},
|
|
2419
|
+
]
|
|
2420
|
+
|
|
2421
|
+
bulk_update_data = {
|
|
2422
|
+
"source_filter": {"slug": ["some-slug"]},
|
|
2423
|
+
}
|
|
2424
|
+
choices_fields = ["destination_type", "source_type", "type", "required_on"]
|
|
2425
|
+
slug_source = "label"
|
|
2426
|
+
slugify_function = staticmethod(slugify_dashes_to_underscores)
|
|
2427
|
+
|
|
2428
|
+
@classmethod
|
|
2429
|
+
def setUpTestData(cls):
|
|
2430
|
+
location_type = ContentType.objects.get_for_model(Location)
|
|
2431
|
+
device_type = ContentType.objects.get_for_model(Device)
|
|
2432
|
+
|
|
2433
|
+
cls.relationships = (
|
|
2434
|
+
Relationship(
|
|
2435
|
+
label="Related locations",
|
|
2436
|
+
key="related_locations",
|
|
2437
|
+
type="symmetric-many-to-many",
|
|
2438
|
+
source_type=location_type,
|
|
2439
|
+
destination_type=location_type,
|
|
2440
|
+
),
|
|
2441
|
+
Relationship(
|
|
2442
|
+
label="Unrelated locations",
|
|
2443
|
+
key="unrelated_locations",
|
|
2444
|
+
type="many-to-many",
|
|
2445
|
+
source_type=location_type,
|
|
2446
|
+
source_label="Other locations (from source side)",
|
|
2447
|
+
destination_type=location_type,
|
|
2448
|
+
destination_label="Other locations (from destination side)",
|
|
2449
|
+
),
|
|
2450
|
+
Relationship(
|
|
2451
|
+
label="Devices found elsewhere",
|
|
2452
|
+
key="devices_elsewhere",
|
|
2453
|
+
type="many-to-many",
|
|
2454
|
+
source_type=location_type,
|
|
2455
|
+
destination_type=device_type,
|
|
2456
|
+
),
|
|
2457
|
+
)
|
|
2458
|
+
for relationship in cls.relationships:
|
|
2459
|
+
relationship.validated_save()
|
|
2460
|
+
cls.lt = LocationType.objects.get(name="Campus")
|
|
2461
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
2462
|
+
cls.location = Location.objects.create(name="Location 1", status=location_status, location_type=cls.lt)
|
|
2463
|
+
|
|
2464
|
+
def test_get_all_relationships_on_location(self):
|
|
2465
|
+
"""Verify that all relationships are accurately represented when requested."""
|
|
2466
|
+
self.add_permissions("dcim.view_location")
|
|
2467
|
+
response = self.client.get(
|
|
2468
|
+
reverse("dcim-api:location-detail", kwargs={"pk": self.location.pk}) + "?include=relationships",
|
|
2469
|
+
**self.header,
|
|
2470
|
+
)
|
|
2471
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2472
|
+
self.assertIn("relationships", response.data)
|
|
2473
|
+
self.assertIsInstance(response.data["relationships"], dict)
|
|
2474
|
+
self.maxDiff = None
|
|
2475
|
+
self.assertEqual(
|
|
2476
|
+
{
|
|
2477
|
+
self.relationships[0].key: {
|
|
2478
|
+
"id": str(self.relationships[0].pk),
|
|
2479
|
+
"url": self.absolute_api_url(self.relationships[0]),
|
|
2480
|
+
"label": self.relationships[0].label,
|
|
2481
|
+
"type": self.relationships[0].type,
|
|
2482
|
+
"peer": {
|
|
2483
|
+
"label": "locations",
|
|
2484
|
+
"object_type": "dcim.location",
|
|
2485
|
+
"objects": [],
|
|
2486
|
+
},
|
|
2487
|
+
},
|
|
2488
|
+
self.relationships[1].key: {
|
|
2489
|
+
"id": str(self.relationships[1].pk),
|
|
2490
|
+
"url": self.absolute_api_url(self.relationships[1]),
|
|
2491
|
+
"label": self.relationships[1].label,
|
|
2492
|
+
"type": self.relationships[1].type,
|
|
2493
|
+
"destination": {
|
|
2494
|
+
"label": self.relationships[1].source_label, # yes -- it's a bit confusing
|
|
2495
|
+
"object_type": "dcim.location",
|
|
2496
|
+
"objects": [],
|
|
2497
|
+
},
|
|
2498
|
+
"source": {
|
|
2499
|
+
"label": self.relationships[1].destination_label, # yes -- it's a bit confusing
|
|
2500
|
+
"object_type": "dcim.location",
|
|
2501
|
+
"objects": [],
|
|
2502
|
+
},
|
|
2503
|
+
},
|
|
2504
|
+
self.relationships[2].key: {
|
|
2505
|
+
"id": str(self.relationships[2].pk),
|
|
2506
|
+
"url": self.absolute_api_url(self.relationships[2]),
|
|
2507
|
+
"label": self.relationships[2].label,
|
|
2508
|
+
"type": self.relationships[2].type,
|
|
2509
|
+
"destination": {
|
|
2510
|
+
"label": "devices",
|
|
2511
|
+
"object_type": "dcim.device",
|
|
2512
|
+
"objects": [],
|
|
2513
|
+
},
|
|
2514
|
+
},
|
|
2515
|
+
},
|
|
2516
|
+
response.data["relationships"],
|
|
2517
|
+
)
|
|
2518
|
+
|
|
2519
|
+
def test_populate_relationship_associations_on_location_create(self):
|
|
2520
|
+
"""Verify that relationship associations can be populated at instance creation time."""
|
|
2521
|
+
location_type = LocationType.objects.get(name="Campus")
|
|
2522
|
+
existing_location_1 = Location.objects.create(
|
|
2523
|
+
name="Existing Location 1",
|
|
2524
|
+
status=Status.objects.get_for_model(Location).first(),
|
|
2525
|
+
location_type=location_type,
|
|
2526
|
+
)
|
|
2527
|
+
existing_location_2 = Location.objects.create(
|
|
2528
|
+
name="Existing Location 2",
|
|
2529
|
+
status=Status.objects.get_for_model(Location).first(),
|
|
2530
|
+
location_type=location_type,
|
|
2531
|
+
)
|
|
2532
|
+
manufacturer = Manufacturer.objects.first()
|
|
2533
|
+
device_type = DeviceType.objects.create(
|
|
2534
|
+
manufacturer=manufacturer,
|
|
2535
|
+
model="device Type 1",
|
|
2536
|
+
slug="device-type-1",
|
|
2537
|
+
)
|
|
2538
|
+
device_role = Role.objects.get_for_model(Device).first()
|
|
2539
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
2540
|
+
existing_device_1 = Device.objects.create(
|
|
2541
|
+
name="existing-device-location-1",
|
|
2542
|
+
status=device_status,
|
|
2543
|
+
role=device_role,
|
|
2544
|
+
device_type=device_type,
|
|
2545
|
+
location=existing_location_1,
|
|
2546
|
+
)
|
|
2547
|
+
existing_device_2 = Device.objects.create(
|
|
2548
|
+
name="existing-device-location-2",
|
|
2549
|
+
status=device_status,
|
|
2550
|
+
role=device_role,
|
|
2551
|
+
device_type=device_type,
|
|
2552
|
+
location=existing_location_2,
|
|
2553
|
+
)
|
|
2554
|
+
|
|
2555
|
+
self.add_permissions("dcim.view_location", "dcim.add_location", "extras.add_relationshipassociation")
|
|
2556
|
+
response = self.client.post(
|
|
2557
|
+
reverse("dcim-api:location-list"),
|
|
2558
|
+
data={
|
|
2559
|
+
"name": "New location",
|
|
2560
|
+
"status": Status.objects.get_for_model(Location).first().pk,
|
|
2561
|
+
"location_type": location_type.pk,
|
|
2562
|
+
"relationships": {
|
|
2563
|
+
self.relationships[0].key: {
|
|
2564
|
+
"peer": {
|
|
2565
|
+
"objects": [str(existing_location_1.pk)],
|
|
2566
|
+
},
|
|
2567
|
+
},
|
|
2568
|
+
self.relationships[1].key: {
|
|
2569
|
+
"source": {
|
|
2570
|
+
"objects": [str(existing_location_2.pk)],
|
|
2571
|
+
},
|
|
2572
|
+
},
|
|
2573
|
+
self.relationships[2].key: {
|
|
2574
|
+
"destination": {
|
|
2575
|
+
"objects": [
|
|
2576
|
+
{"name": "existing-device-location-1"},
|
|
2577
|
+
{"name": "existing-device-location-2"},
|
|
2578
|
+
],
|
|
2579
|
+
},
|
|
2580
|
+
},
|
|
2581
|
+
},
|
|
2582
|
+
},
|
|
2583
|
+
format="json",
|
|
2584
|
+
**self.header,
|
|
2585
|
+
)
|
|
2586
|
+
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
2587
|
+
new_location_id = response.data["id"]
|
|
2588
|
+
# Peer case - don't distinguish source/destination
|
|
2589
|
+
self.assertTrue(
|
|
2590
|
+
RelationshipAssociation.objects.filter(
|
|
2591
|
+
relationship=self.relationships[0],
|
|
2592
|
+
source_type=self.relationships[0].source_type,
|
|
2593
|
+
source_id__in=[existing_location_1.pk, new_location_id],
|
|
2594
|
+
destination_type=self.relationships[0].destination_type,
|
|
2595
|
+
destination_id__in=[existing_location_1.pk, new_location_id],
|
|
2596
|
+
).exists()
|
|
2597
|
+
)
|
|
2598
|
+
self.assertTrue(
|
|
2599
|
+
RelationshipAssociation.objects.filter(
|
|
2600
|
+
relationship=self.relationships[1],
|
|
2601
|
+
source_type=self.relationships[1].source_type,
|
|
2602
|
+
source_id=existing_location_2.pk,
|
|
2603
|
+
destination_type=self.relationships[1].destination_type,
|
|
2604
|
+
destination_id=new_location_id,
|
|
2605
|
+
).exists()
|
|
2606
|
+
)
|
|
2607
|
+
self.assertTrue(
|
|
2608
|
+
RelationshipAssociation.objects.filter(
|
|
2609
|
+
relationship=self.relationships[2],
|
|
2610
|
+
source_type=self.relationships[2].source_type,
|
|
2611
|
+
source_id=new_location_id,
|
|
2612
|
+
destination_type=self.relationships[2].destination_type,
|
|
2613
|
+
destination_id=existing_device_1.pk,
|
|
2614
|
+
).exists()
|
|
2615
|
+
)
|
|
2616
|
+
self.assertTrue(
|
|
2617
|
+
RelationshipAssociation.objects.filter(
|
|
2618
|
+
relationship=self.relationships[2],
|
|
2619
|
+
source_type=self.relationships[2].source_type,
|
|
2620
|
+
source_id=new_location_id,
|
|
2621
|
+
destination_type=self.relationships[2].destination_type,
|
|
2622
|
+
destination_id=existing_device_2.pk,
|
|
2623
|
+
).exists()
|
|
2624
|
+
)
|
|
2625
|
+
|
|
2626
|
+
def test_required_relationships(self):
|
|
2627
|
+
"""
|
|
2628
|
+
1. Try creating an object when no required target object exists
|
|
2629
|
+
2. Try creating an object without specifying required target object(s)
|
|
2630
|
+
3. Try creating an object when all required data is present
|
|
2631
|
+
4. Test various bulk create/edit scenarios
|
|
2632
|
+
"""
|
|
2633
|
+
|
|
2634
|
+
# Parameterized tests (for creating and updating single objects):
|
|
2635
|
+
self.required_relationships_test(interact_with="api")
|
|
2636
|
+
|
|
2637
|
+
# 4. Bulk create/edit tests:
|
|
2638
|
+
|
|
2639
|
+
# VLAN endpoint to POST, PATCH and PUT multiple objects to:
|
|
2640
|
+
vlan_list_endpoint = reverse(get_route_for_model(VLAN, "list", api=True))
|
|
2641
|
+
|
|
2642
|
+
def send_bulk_data(http_method, data):
|
|
2643
|
+
return getattr(self.client, http_method)(
|
|
2644
|
+
vlan_list_endpoint,
|
|
2645
|
+
data=data,
|
|
2646
|
+
format="json",
|
|
2647
|
+
**self.header,
|
|
2648
|
+
)
|
|
2649
|
+
|
|
2650
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
2651
|
+
|
|
2652
|
+
# Try deleting all devices and then creating 2 VLANs (fails):
|
|
2653
|
+
Device.objects.all().delete()
|
|
2654
|
+
response = send_bulk_data(
|
|
2655
|
+
"post",
|
|
2656
|
+
data=[
|
|
2657
|
+
{"vid": "1", "name": "1", "status": device_status.pk},
|
|
2658
|
+
{"vid": "2", "name": "2", "status": device_status.pk},
|
|
2659
|
+
],
|
|
2660
|
+
)
|
|
2661
|
+
self.assertHttpStatus(response, 400)
|
|
2662
|
+
self.assertEqual(
|
|
2663
|
+
{
|
|
2664
|
+
"relationships": {
|
|
2665
|
+
"vlans_devices_m2m": [
|
|
2666
|
+
"VLANs require at least one device, but no devices exist yet. "
|
|
2667
|
+
"Create a device by posting to /api/dcim/devices/",
|
|
2668
|
+
'You need to specify ["relationships"]["vlans_devices_m2m"]["source"]["objects"].',
|
|
2669
|
+
]
|
|
2670
|
+
}
|
|
2671
|
+
},
|
|
2672
|
+
response.json(),
|
|
2673
|
+
)
|
|
2674
|
+
|
|
2675
|
+
# Create test device for association
|
|
2676
|
+
device_for_association = test_views.create_test_device("VLAN Required Device")
|
|
2677
|
+
required_relationship_json = {"vlans_devices_m2m": {"source": {"objects": [str(device_for_association.id)]}}}
|
|
2678
|
+
expected_error_json = {
|
|
2679
|
+
"relationships": {
|
|
2680
|
+
"vlans_devices_m2m": [
|
|
2681
|
+
'You need to specify ["relationships"]["vlans_devices_m2m"]["source"]["objects"].'
|
|
2682
|
+
]
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
# Test POST, PATCH and PUT
|
|
2687
|
+
for method in ["post", "patch", "put"]:
|
|
2688
|
+
if method == "post":
|
|
2689
|
+
vlan1_json_data = {
|
|
2690
|
+
"vid": "1",
|
|
2691
|
+
"name": "1",
|
|
2692
|
+
"status": device_status.pk,
|
|
2693
|
+
}
|
|
2694
|
+
vlan2_json_data = {
|
|
2695
|
+
"vid": "2",
|
|
2696
|
+
"name": "2",
|
|
2697
|
+
"status": device_status.pk,
|
|
2698
|
+
}
|
|
2699
|
+
else:
|
|
2700
|
+
vlan1, vlan2 = VLANFactory.create_batch(2)
|
|
2701
|
+
vlan1_json_data = {"status": device_status.pk, "id": str(vlan1.id)}
|
|
2702
|
+
# Add required fields for PUT method:
|
|
2703
|
+
if method == "put":
|
|
2704
|
+
vlan1_json_data.update({"vid": vlan1.vid, "name": vlan1.name})
|
|
2705
|
+
|
|
2706
|
+
vlan2_json_data = {"status": device_status.pk, "id": str(vlan2.id)}
|
|
2707
|
+
# Add required fields for PUT method:
|
|
2708
|
+
if method == "put":
|
|
2709
|
+
vlan2_json_data.update({"vid": vlan2.vid, "name": vlan2.name})
|
|
2710
|
+
|
|
2711
|
+
# Try method without specifying required relationships for either vlan1 or vlan2 (fails)
|
|
2712
|
+
json_data = [vlan1_json_data, vlan2_json_data]
|
|
2713
|
+
response = send_bulk_data(method, json_data)
|
|
2714
|
+
self.assertHttpStatus(response, 400)
|
|
2715
|
+
self.assertEqual(response.json(), expected_error_json)
|
|
2716
|
+
|
|
2717
|
+
# Try method specifying required relationships for just vlan1 (fails)
|
|
2718
|
+
vlan1_json_data["relationships"] = required_relationship_json
|
|
2719
|
+
json_data = [vlan1_json_data, vlan2_json_data]
|
|
2720
|
+
response = send_bulk_data(method, json_data)
|
|
2721
|
+
self.assertHttpStatus(response, 400)
|
|
2722
|
+
self.assertEqual(response.json(), expected_error_json)
|
|
2723
|
+
|
|
2724
|
+
# Try method specifying required relationships for both vlan1 and vlan2 (succeeds)
|
|
2725
|
+
vlan2_json_data["relationships"] = required_relationship_json
|
|
2726
|
+
json_data = [vlan1_json_data, vlan2_json_data]
|
|
2727
|
+
response = send_bulk_data(method, json_data)
|
|
2728
|
+
if method == "post":
|
|
2729
|
+
self.assertHttpStatus(response, 201)
|
|
2730
|
+
else:
|
|
2731
|
+
self.assertHttpStatus(response, 200)
|
|
2732
|
+
|
|
2733
|
+
# Check the relationship associations were actually created
|
|
2734
|
+
for vlan in response.json():
|
|
2735
|
+
associated_device = vlan["relationships"]["vlans_devices_m2m"]["source"]["objects"][0]
|
|
2736
|
+
self.assertEqual(str(device_for_association.id), associated_device["id"])
|
|
2737
|
+
|
|
2738
|
+
|
|
2740
2739
|
class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
2741
2740
|
model = RelationshipAssociation
|
|
2742
|
-
brief_fields = ["destination_id", "display", "id", "relationship", "source_id", "url"]
|
|
2743
2741
|
choices_fields = ["destination_type", "source_type"]
|
|
2744
2742
|
|
|
2745
2743
|
@classmethod
|
|
@@ -2749,8 +2747,8 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
2749
2747
|
cls.location_status = Status.objects.get_for_model(Location).first()
|
|
2750
2748
|
|
|
2751
2749
|
cls.relationship = Relationship(
|
|
2752
|
-
|
|
2753
|
-
|
|
2750
|
+
label="Devices found elsewhere",
|
|
2751
|
+
key="elsewhere_devices",
|
|
2754
2752
|
type="many-to-many",
|
|
2755
2753
|
source_type=cls.location_type,
|
|
2756
2754
|
destination_type=cls.device_type,
|
|
@@ -2840,8 +2838,8 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
2840
2838
|
"""Test creation of invalid relationship association restricted by destination/source filter."""
|
|
2841
2839
|
|
|
2842
2840
|
relationship = Relationship.objects.create(
|
|
2843
|
-
|
|
2844
|
-
|
|
2841
|
+
label="Device to location Rel 1",
|
|
2842
|
+
key="device_to_location_rel_1",
|
|
2845
2843
|
source_type=self.device_type,
|
|
2846
2844
|
source_filter={"name": [self.devices[0].name]},
|
|
2847
2845
|
destination_type=self.location_type,
|
|
@@ -2882,7 +2880,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
2882
2880
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
2883
2881
|
self.assertEqual(
|
|
2884
2882
|
response.data[side],
|
|
2885
|
-
[f"{field_error_name} violates {relationship.
|
|
2883
|
+
[f"{field_error_name} violates {relationship.label} {side}_filter restriction"],
|
|
2886
2884
|
)
|
|
2887
2885
|
|
|
2888
2886
|
def test_model_clean_method_is_called(self):
|
|
@@ -2910,62 +2908,37 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
2910
2908
|
"""
|
|
2911
2909
|
self.add_permissions("dcim.view_location")
|
|
2912
2910
|
response = self.client.get(
|
|
2913
|
-
reverse("dcim-api:location-detail", kwargs={"pk": self.locations[0].pk})
|
|
2911
|
+
reverse("dcim-api:location-detail", kwargs={"pk": self.locations[0].pk})
|
|
2912
|
+
+ "?include=relationships"
|
|
2913
|
+
+ "&depth=1",
|
|
2914
2914
|
**self.header,
|
|
2915
2915
|
)
|
|
2916
2916
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2917
2917
|
self.assertIn("relationships", response.data)
|
|
2918
2918
|
self.assertIsInstance(response.data["relationships"], dict)
|
|
2919
2919
|
# Ensure consistent ordering
|
|
2920
|
-
response.data["relationships"][self.relationship.
|
|
2920
|
+
response.data["relationships"][self.relationship.key]["destination"]["objects"].sort(key=lambda v: v["name"])
|
|
2921
2921
|
self.maxDiff = None
|
|
2922
|
-
self.
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
"display": self.devices[0].display,
|
|
2943
|
-
"name": self.devices[0].name,
|
|
2944
|
-
},
|
|
2945
|
-
{
|
|
2946
|
-
"id": str(self.devices[1].pk),
|
|
2947
|
-
"url": (
|
|
2948
|
-
"http://nautobot.example.com"
|
|
2949
|
-
+ reverse("dcim-api:device-detail", kwargs={"pk": self.devices[1].pk})
|
|
2950
|
-
),
|
|
2951
|
-
"display": self.devices[1].display,
|
|
2952
|
-
"name": self.devices[1].name,
|
|
2953
|
-
},
|
|
2954
|
-
{
|
|
2955
|
-
"id": str(self.devices[2].pk),
|
|
2956
|
-
"url": (
|
|
2957
|
-
"http://nautobot.example.com"
|
|
2958
|
-
+ reverse("dcim-api:device-detail", kwargs={"pk": self.devices[2].pk})
|
|
2959
|
-
),
|
|
2960
|
-
"display": self.devices[2].display,
|
|
2961
|
-
"name": self.devices[2].name,
|
|
2962
|
-
},
|
|
2963
|
-
],
|
|
2964
|
-
},
|
|
2965
|
-
},
|
|
2966
|
-
},
|
|
2967
|
-
response.data["relationships"],
|
|
2968
|
-
)
|
|
2922
|
+
relationship_data = response.data["relationships"][self.relationship.key]
|
|
2923
|
+
self.assertEqual(relationship_data["id"], str(self.relationship.pk))
|
|
2924
|
+
self.assertEqual(relationship_data["url"], self.absolute_api_url(self.relationship))
|
|
2925
|
+
self.assertEqual(relationship_data["label"], self.relationship.label)
|
|
2926
|
+
self.assertEqual(relationship_data["type"], "many-to-many")
|
|
2927
|
+
self.assertEqual(relationship_data["destination"]["label"], "devices")
|
|
2928
|
+
self.assertEqual(relationship_data["destination"]["object_type"], "dcim.device")
|
|
2929
|
+
|
|
2930
|
+
objects = response.data["relationships"][self.relationship.key]["destination"]["objects"]
|
|
2931
|
+
for i, obj in enumerate(objects):
|
|
2932
|
+
self.assertEqual(obj["id"], str(self.devices[i].pk))
|
|
2933
|
+
self.assertEqual(obj["url"], self.absolute_api_url(self.devices[i]))
|
|
2934
|
+
self.assertEqual(
|
|
2935
|
+
obj["display"],
|
|
2936
|
+
self.devices[i].display,
|
|
2937
|
+
)
|
|
2938
|
+
self.assertEqual(
|
|
2939
|
+
obj["name"],
|
|
2940
|
+
self.devices[i].name,
|
|
2941
|
+
)
|
|
2969
2942
|
|
|
2970
2943
|
def test_update_association_data_on_location(self):
|
|
2971
2944
|
"""
|
|
@@ -3027,21 +3000,21 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3027
3000
|
|
|
3028
3001
|
with self.subTest("Error handling: wrong relationship"):
|
|
3029
3002
|
Relationship.objects.create(
|
|
3030
|
-
|
|
3031
|
-
|
|
3003
|
+
label="Device-to-Device",
|
|
3004
|
+
key="device_to_device",
|
|
3032
3005
|
source_type=self.device_type,
|
|
3033
3006
|
destination_type=self.device_type,
|
|
3034
3007
|
type=RelationshipTypeChoices.TYPE_ONE_TO_ONE,
|
|
3035
3008
|
)
|
|
3036
3009
|
response = self.client.patch(
|
|
3037
3010
|
url,
|
|
3038
|
-
{"relationships": {"
|
|
3011
|
+
{"relationships": {"device_to_device": {"peer": {"objects": []}}}},
|
|
3039
3012
|
format="json",
|
|
3040
3013
|
**self.header,
|
|
3041
3014
|
)
|
|
3042
3015
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
3043
3016
|
self.assertEqual(
|
|
3044
|
-
str(response.data["relationships"][0]), '"
|
|
3017
|
+
str(response.data["relationships"][0]), '"device_to_device" is not a relationship on dcim.Location'
|
|
3045
3018
|
)
|
|
3046
3019
|
self.assertEqual(3, RelationshipAssociation.objects.filter(relationship=self.relationship).count())
|
|
3047
3020
|
for association in self.associations:
|
|
@@ -3050,7 +3023,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3050
3023
|
with self.subTest("Error handling: wrong relationship side"):
|
|
3051
3024
|
response = self.client.patch(
|
|
3052
3025
|
url,
|
|
3053
|
-
{"relationships": {self.relationship.
|
|
3026
|
+
{"relationships": {self.relationship.key: {"source": {"objects": []}}}},
|
|
3054
3027
|
format="json",
|
|
3055
3028
|
**self.header,
|
|
3056
3029
|
)
|
|
@@ -3068,7 +3041,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3068
3041
|
url,
|
|
3069
3042
|
{
|
|
3070
3043
|
"relationships": {
|
|
3071
|
-
self.relationship.
|
|
3044
|
+
self.relationship.key: {
|
|
3072
3045
|
"destination": {
|
|
3073
3046
|
"objects": [
|
|
3074
3047
|
# remove devices[0] by omission
|
|
@@ -3095,7 +3068,6 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3095
3068
|
|
|
3096
3069
|
class SecretTest(APIViewTestCases.APIViewTestCase):
|
|
3097
3070
|
model = Secret
|
|
3098
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
3099
3071
|
bulk_update_data = {}
|
|
3100
3072
|
|
|
3101
3073
|
create_data = [
|
|
@@ -3146,10 +3118,50 @@ class SecretTest(APIViewTestCases.APIViewTestCase):
|
|
|
3146
3118
|
for secret in secrets:
|
|
3147
3119
|
secret.validated_save()
|
|
3148
3120
|
|
|
3121
|
+
def test_secret_check(self):
|
|
3122
|
+
"""
|
|
3123
|
+
Ensure that we can check the validity of a secret.
|
|
3124
|
+
"""
|
|
3125
|
+
|
|
3126
|
+
with self.subTest("Secret is not accessible"):
|
|
3127
|
+
test_secret = Secret.objects.create(
|
|
3128
|
+
name="secret-check-test-not-accessible",
|
|
3129
|
+
provider="text-file",
|
|
3130
|
+
parameters={"path": "/tmp/does-not-matter"},
|
|
3131
|
+
)
|
|
3132
|
+
response = self.client.get(reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header)
|
|
3133
|
+
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
3134
|
+
|
|
3135
|
+
self.add_permissions("extras.view_secret")
|
|
3136
|
+
|
|
3137
|
+
with self.subTest("Secret check successful"):
|
|
3138
|
+
with tempfile.NamedTemporaryFile() as secret_file:
|
|
3139
|
+
secret_file.write(b"HELLO WORLD")
|
|
3140
|
+
test_secret = Secret.objects.create(
|
|
3141
|
+
name="secret-check-test-accessible",
|
|
3142
|
+
provider="text-file",
|
|
3143
|
+
parameters={"path": secret_file.name},
|
|
3144
|
+
)
|
|
3145
|
+
response = self.client.get(
|
|
3146
|
+
reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header
|
|
3147
|
+
)
|
|
3148
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
3149
|
+
self.assertEqual(response.data["result"], True)
|
|
3150
|
+
|
|
3151
|
+
with self.subTest("Secret check failed"):
|
|
3152
|
+
test_secret = Secret.objects.create(
|
|
3153
|
+
name="secret-check-test-failed",
|
|
3154
|
+
provider="text-file",
|
|
3155
|
+
parameters={"path": "/tmp/does-not-exist"},
|
|
3156
|
+
)
|
|
3157
|
+
response = self.client.get(reverse("extras-api:secret-check", kwargs={"pk": test_secret.pk}), **self.header)
|
|
3158
|
+
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
3159
|
+
self.assertEqual(response.data["result"], False)
|
|
3160
|
+
self.assertIn("SecretValueNotFoundError", response.data["message"])
|
|
3161
|
+
|
|
3149
3162
|
|
|
3150
3163
|
class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
|
|
3151
3164
|
model = SecretsGroup
|
|
3152
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
3153
3165
|
bulk_update_data = {}
|
|
3154
3166
|
|
|
3155
3167
|
@classmethod
|
|
@@ -3200,7 +3212,6 @@ class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
3200
3212
|
|
|
3201
3213
|
class SecretsGroupAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
3202
3214
|
model = SecretsGroupAssociation
|
|
3203
|
-
brief_fields = ["access_type", "display", "id", "secret", "secret_type", "url"]
|
|
3204
3215
|
bulk_update_data = {}
|
|
3205
3216
|
choices_fields = ["access_type", "secret_type"]
|
|
3206
3217
|
|
|
@@ -3267,7 +3278,6 @@ class SecretsGroupAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3267
3278
|
|
|
3268
3279
|
class StatusTest(APIViewTestCases.APIViewTestCase):
|
|
3269
3280
|
model = Status
|
|
3270
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
3271
3281
|
bulk_update_data = {
|
|
3272
3282
|
"color": "000000",
|
|
3273
3283
|
}
|
|
@@ -3298,7 +3308,6 @@ class StatusTest(APIViewTestCases.APIViewTestCase):
|
|
|
3298
3308
|
|
|
3299
3309
|
class TagTest(APIViewTestCases.APIViewTestCase):
|
|
3300
3310
|
model = Tag
|
|
3301
|
-
brief_fields = ["color", "display", "id", "name", "slug", "url"]
|
|
3302
3311
|
create_data = [
|
|
3303
3312
|
{"name": "Tag 4", "slug": "tag-4", "content_types": [Location._meta.label_lower]},
|
|
3304
3313
|
{"name": "Tag 5", "slug": "tag-5", "content_types": [Location._meta.label_lower]},
|
|
@@ -3382,7 +3391,6 @@ class TagTest(APIViewTestCases.APIViewTestCase):
|
|
|
3382
3391
|
|
|
3383
3392
|
class WebhookTest(APIViewTestCases.APIViewTestCase):
|
|
3384
3393
|
model = Webhook
|
|
3385
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
3386
3394
|
create_data = [
|
|
3387
3395
|
{
|
|
3388
3396
|
"content_types": ["dcim.consoleport"],
|
|
@@ -3611,7 +3619,6 @@ class WebhookTest(APIViewTestCases.APIViewTestCase):
|
|
|
3611
3619
|
|
|
3612
3620
|
class RoleTest(APIViewTestCases.APIViewTestCase):
|
|
3613
3621
|
model = Role
|
|
3614
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
3615
3622
|
bulk_update_data = {
|
|
3616
3623
|
"color": "000000",
|
|
3617
3624
|
}
|