nautobot 2.0.0a2__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/__init__.py +1 -5
- nautobot/apps/api.py +6 -8
- nautobot/apps/forms.py +0 -2
- nautobot/apps/ui.py +0 -8
- nautobot/circuits/api/serializers.py +9 -119
- nautobot/circuits/api/urls.py +1 -1
- nautobot/circuits/api/views.py +0 -1
- nautobot/circuits/choices.py +0 -2
- nautobot/circuits/filters.py +7 -6
- nautobot/circuits/forms.py +3 -73
- nautobot/circuits/migrations/0001_initial_part_1.py +0 -1
- nautobot/circuits/migrations/0002_initial_part_2.py +0 -1
- nautobot/circuits/migrations/0003_auto_slug.py +0 -1
- nautobot/circuits/migrations/0004_increase_provider_account_length.py +0 -1
- nautobot/circuits/migrations/0005_providernetwork.py +0 -1
- nautobot/circuits/migrations/0006_cache_circuit_terminations.py +0 -1
- nautobot/circuits/migrations/0007_circuitterminations_primary_model.py +0 -1
- nautobot/circuits/migrations/0008_add_natural_indexing.py +0 -1
- nautobot/circuits/migrations/0009_circuittermination_location.py +0 -1
- nautobot/circuits/migrations/0010_rename_foreign_keys_and_related_names.py +0 -1
- nautobot/circuits/migrations/0011_remove_site_foreign_key_from_circuit_termination_class.py +0 -1
- nautobot/circuits/migrations/0012_created_datetime.py +0 -1
- nautobot/circuits/migrations/0013_alter_circuittermination__path.py +0 -1
- nautobot/circuits/migrations/0014_related_name_changes.py +1 -2
- nautobot/circuits/migrations/0015_remove_circuittype_provider_slug.py +20 -0
- 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 -93
- nautobot/circuits/navigation.py +14 -69
- nautobot/circuits/signals.py +0 -2
- nautobot/circuits/tables.py +42 -5
- nautobot/circuits/templates/circuits/circuit_retrieve.html +1 -1
- nautobot/circuits/templates/circuits/circuittermination_retrieve.html +1 -1
- nautobot/circuits/templates/circuits/circuittype_retrieve.html +1 -1
- nautobot/circuits/templates/circuits/provider_create.html +0 -1
- nautobot/circuits/templates/circuits/provider_retrieve.html +1 -1
- nautobot/circuits/tests/integration/test_relationships.py +13 -16
- nautobot/circuits/tests/test_api.py +13 -43
- nautobot/circuits/tests/test_filters.py +20 -15
- nautobot/circuits/tests/test_models.py +7 -3
- nautobot/circuits/tests/test_views.py +57 -67
- nautobot/circuits/views.py +18 -9
- nautobot/core/api/__init__.py +8 -2
- nautobot/core/api/authentication.py +0 -3
- 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 +3 -3
- nautobot/core/api/parsers.py +154 -0
- nautobot/core/api/renderers.py +153 -2
- nautobot/core/api/schema.py +47 -3
- nautobot/core/api/serializers.py +377 -37
- 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 -75
- nautobot/core/apps/__init__.py +138 -221
- nautobot/core/celery/__init__.py +112 -41
- nautobot/core/celery/backends.py +19 -13
- 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 +21 -113
- nautobot/core/{cli.py → cli/__init__.py} +1 -2
- nautobot/core/cli/__main__.py +3 -0
- nautobot/core/constants.py +25 -43
- 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 +39 -68
- nautobot/core/forms/forms.py +27 -27
- nautobot/core/forms/utils.py +7 -59
- nautobot/core/forms/widgets.py +0 -1
- nautobot/core/graphql/__init__.py +2 -2
- 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/celery.py +0 -1
- nautobot/core/management/commands/generate_test_data.py +18 -13
- nautobot/core/management/commands/post_upgrade.py +24 -24
- nautobot/core/management/commands/validate_models.py +0 -1
- nautobot/core/middleware.py +0 -1
- nautobot/core/models/__init__.py +26 -1
- nautobot/core/models/fields.py +24 -5
- nautobot/core/models/generics.py +2 -46
- nautobot/core/models/managers.py +5 -0
- nautobot/core/models/name_color_content_types.py +1 -19
- nautobot/core/models/tree_queries.py +14 -4
- nautobot/core/models/utils.py +9 -10
- nautobot/core/models/validators.py +17 -8
- nautobot/core/releases.py +8 -10
- nautobot/core/settings.py +81 -53
- 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 +4 -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 -25
- 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 -29
- nautobot/core/templatetags/helpers.py +1 -1
- nautobot/core/testing/__init__.py +47 -44
- nautobot/core/testing/api.py +365 -47
- nautobot/core/testing/filters.py +12 -7
- nautobot/core/testing/integration.py +1 -1
- nautobot/core/testing/migrations.py +2 -0
- nautobot/core/testing/mixins.py +22 -12
- nautobot/core/testing/schema.py +2 -1
- nautobot/core/testing/views.py +28 -51
- 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/runner.py +0 -1
- nautobot/core/tests/test_api.py +290 -24
- nautobot/core/tests/test_authentication.py +57 -14
- nautobot/core/tests/test_checks.py +0 -7
- nautobot/core/tests/test_choices.py +0 -1
- nautobot/core/tests/test_filters.py +117 -110
- nautobot/core/tests/test_forms.py +47 -110
- nautobot/core/tests/test_graphql.py +158 -135
- nautobot/core/tests/test_logging.py +4 -1
- nautobot/core/tests/test_managers.py +3 -5
- nautobot/core/tests/test_models.py +2 -0
- nautobot/core/tests/test_ordering.py +0 -2
- nautobot/core/tests/test_paginator.py +3 -1
- nautobot/core/tests/test_releases.py +12 -12
- nautobot/core/tests/test_templatetags_helpers.py +7 -4
- nautobot/core/tests/test_utils.py +112 -78
- nautobot/core/tests/test_views.py +12 -17
- nautobot/core/tests/test_views_utils.py +6 -9
- nautobot/core/utils/data.py +17 -0
- nautobot/core/utils/deprecation.py +13 -20
- nautobot/core/utils/filtering.py +53 -9
- nautobot/core/utils/git.py +12 -4
- nautobot/core/utils/lookup.py +3 -1
- nautobot/core/utils/requests.py +23 -116
- nautobot/core/views/__init__.py +1 -2
- nautobot/core/views/generic.py +131 -119
- nautobot/core/views/mixins.py +53 -62
- nautobot/core/views/paginator.py +0 -1
- nautobot/core/views/renderers.py +14 -12
- nautobot/core/views/utils.py +87 -4
- nautobot/dcim/api/serializers.py +160 -672
- nautobot/dcim/api/urls.py +1 -1
- nautobot/dcim/api/views.py +7 -46
- nautobot/dcim/choices.py +2 -25
- nautobot/dcim/elevations.py +0 -1
- nautobot/dcim/factory.py +15 -4
- nautobot/dcim/filters/__init__.py +42 -13
- nautobot/dcim/form_mixins.py +1 -27
- nautobot/dcim/forms.py +58 -797
- nautobot/dcim/management/commands/trace_paths.py +0 -1
- nautobot/dcim/migrations/0001_initial_part_1.py +0 -1
- nautobot/dcim/migrations/0002_initial_part_2.py +0 -1
- nautobot/dcim/migrations/0003_initial_part_3.py +0 -1
- nautobot/dcim/migrations/0004_initial_part_4.py +0 -1
- nautobot/dcim/migrations/0005_device_local_context_schema.py +0 -1
- nautobot/dcim/migrations/0006_auto_slug.py +0 -1
- nautobot/dcim/migrations/0007_device_secrets_group.py +0 -1
- nautobot/dcim/migrations/0008_increase_all_serial_lengths.py +0 -1
- nautobot/dcim/migrations/0009_add_natural_indexing.py +0 -1
- nautobot/dcim/migrations/0010_interface_status.py +0 -1
- nautobot/dcim/migrations/0011_interface_status_data_migration.py +0 -1
- nautobot/dcim/migrations/0012_interface_parent_bridge.py +0 -1
- nautobot/dcim/migrations/0013_location_location_type.py +0 -1
- nautobot/dcim/migrations/0014_location_status_data_migration.py +0 -1
- nautobot/dcim/migrations/0015_device_components__changeloggedmodel.py +0 -1
- nautobot/dcim/migrations/0016_device_components__timestamp_data_migration.py +0 -1
- nautobot/dcim/migrations/0017_locationtype_nestable.py +0 -1
- nautobot/dcim/migrations/0018_device_redundancy_group.py +0 -1
- nautobot/dcim/migrations/0019_device_redundancy_group_data_migration.py +0 -1
- nautobot/dcim/migrations/0020_move_site_fields_to_location_model.py +0 -1
- nautobot/dcim/migrations/0021_mptt_to_tree_queries.py +0 -1
- nautobot/dcim/migrations/0022_interface_mac_address_data_migration.py +0 -1
- nautobot/dcim/migrations/0023_alter_interface_mac_address.py +0 -1
- nautobot/dcim/migrations/0024_alter_device_and_rack_role_add_new_role.py +2 -2
- nautobot/dcim/migrations/0025_device_and_rack_roles_data_migrations.py +19 -14
- nautobot/dcim/migrations/0026_rename_device_and_rack_role.py +0 -1
- nautobot/dcim/migrations/0027_remove_device_role_and_rack_role.py +1 -2
- nautobot/dcim/migrations/0028_rename_foreignkey_fields.py +1 -2
- nautobot/dcim/migrations/0029_add_tree_managers_and_foreign_keys_pre_data_migration.py +0 -1
- nautobot/dcim/migrations/0030_migrate_region_and_site_data_to_locations.py +2 -2
- nautobot/dcim/migrations/0031_rename_path_end_point_related_name.py +0 -1
- nautobot/dcim/migrations/0032_remove_site_foreign_key_from_dcim_models.py +0 -1
- nautobot/dcim/migrations/0033_created_datetime.py +0 -1
- nautobot/dcim/migrations/0034_fixup_fks_and_related_names.py +0 -1
- nautobot/dcim/migrations/0035_related_name_changes.py +1 -2
- nautobot/dcim/migrations/0036_remove_region_and_site.py +1 -2
- nautobot/dcim/migrations/0037_interface_ip_addresses_m2m.py +0 -1
- nautobot/dcim/migrations/0038_alter_location_managers.py +0 -1
- nautobot/dcim/migrations/0039_remove_slug.py +24 -0
- nautobot/dcim/migrations/0040_tagsfield.py +109 -0
- nautobot/dcim/migrations/0041_ipam__namespaces.py +25 -0
- nautobot/dcim/migrations/0042_fixup_null_statuses.py +51 -0
- nautobot/dcim/migrations/0043_status_nonnullable.py +72 -0
- nautobot/dcim/models/cables.py +4 -35
- nautobot/dcim/models/device_component_templates.py +7 -2
- nautobot/dcim/models/device_components.py +26 -203
- nautobot/dcim/models/devices.py +30 -152
- nautobot/dcim/models/locations.py +3 -64
- nautobot/dcim/models/power.py +3 -51
- nautobot/dcim/models/racks.py +7 -86
- nautobot/dcim/navigation.py +141 -467
- nautobot/dcim/signals.py +0 -2
- nautobot/dcim/tables/devices.py +8 -5
- nautobot/dcim/tables/devicetypes.py +1 -1
- nautobot/dcim/tables/locations.py +2 -2
- nautobot/dcim/tables/power.py +2 -2
- nautobot/dcim/templates/dcim/console_port_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/device.html +15 -4
- nautobot/dcim/templates/dcim/device_edit.html +6 -0
- nautobot/dcim/templates/dcim/deviceredundancygroup_create.html +0 -1
- nautobot/dcim/templates/dcim/devicetype.html +2 -2
- nautobot/dcim/templates/dcim/interface.html +4 -0
- nautobot/dcim/templates/dcim/interface_connection_list.html +7 -0
- nautobot/dcim/templates/dcim/interface_edit.html +1 -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/integration/test_cable_connect_form.py +4 -4
- nautobot/dcim/tests/test_api.py +202 -130
- nautobot/dcim/tests/test_cablepaths.py +47 -42
- nautobot/dcim/tests/test_filters.py +156 -134
- nautobot/dcim/tests/test_forms.py +12 -213
- nautobot/dcim/tests/test_graphql.py +8 -3
- nautobot/dcim/tests/test_migrations.py +6 -11
- nautobot/dcim/tests/test_models.py +208 -158
- nautobot/dcim/tests/test_natural_ordering.py +12 -14
- nautobot/dcim/tests/test_signals.py +7 -4
- nautobot/dcim/tests/test_views.py +270 -264
- nautobot/dcim/urls.py +21 -26
- nautobot/dcim/views.py +14 -156
- 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 +179 -197
- nautobot/docs/administration/nautobot-server.md +9 -24
- nautobot/docs/administration/nautobot-shell.md +6 -6
- nautobot/docs/administration/replicating-nautobot.md +0 -10
- nautobot/docs/configuration/index.md +9 -9
- nautobot/docs/configuration/optional-settings.md +32 -61
- nautobot/docs/configuration/required-settings.md +11 -52
- nautobot/docs/development/application-registry.md +2 -13
- nautobot/docs/development/best-practices.md +2 -1
- nautobot/docs/development/docker-compose-advanced-use-cases.md +1 -1
- nautobot/docs/development/extending-models.md +15 -17
- nautobot/docs/development/generic-views.md +0 -2
- nautobot/docs/development/getting-started.md +56 -6
- nautobot/docs/development/navigation-menu.md +22 -93
- nautobot/docs/development/react-ui.md +105 -0
- nautobot/docs/development/release-checklist.md +3 -3
- nautobot/docs/development/role-internals.md +1 -3
- nautobot/docs/development/style-guide.md +6 -4
- nautobot/docs/development/templates.md +2 -1
- nautobot/docs/docker/index.md +16 -14
- nautobot/docs/index.md +7 -3
- nautobot/docs/installation/index.md +4 -1
- nautobot/docs/installation/migrating-from-netbox.md +12 -43
- nautobot/docs/installation/migrating-from-postgresql.md +1 -1
- 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 +190 -636
- nautobot/docs/installation/upgrading.md +5 -2
- 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/dynamicgroup.md +9 -9
- 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/ipam/vrf.md +0 -3
- nautobot/docs/models/virtualization/virtualmachine.md +3 -0
- nautobot/docs/plugins/development.md +92 -24
- nautobot/docs/release-notes/version-1.5.md +96 -0
- nautobot/docs/release-notes/version-2.0.md +216 -0
- nautobot/docs/requirements.txt +5 -4
- 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 -159
- nautobot/extras/api/serializers.py +165 -706
- nautobot/extras/api/urls.py +1 -1
- nautobot/extras/api/views.py +295 -282
- nautobot/extras/apps.py +4 -7
- nautobot/extras/choices.py +11 -22
- 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/factory.py +1 -3
- nautobot/extras/filters/__init__.py +49 -47
- nautobot/extras/filters/mixins.py +10 -8
- nautobot/extras/forms/forms.py +72 -148
- nautobot/extras/forms/mixins.py +34 -57
- nautobot/extras/health_checks.py +0 -33
- nautobot/extras/jobs.py +387 -566
- nautobot/extras/management/__init__.py +55 -48
- nautobot/extras/management/commands/renaturalize.py +0 -1
- nautobot/extras/management/commands/runjob.py +24 -62
- nautobot/extras/management/commands/webhook_receiver.py +0 -1
- nautobot/extras/managers.py +30 -7
- nautobot/extras/migrations/0001_initial_part_1.py +0 -1
- nautobot/extras/migrations/0002_initial_part_2.py +0 -1
- nautobot/extras/migrations/0003_initial_part_3.py +0 -1
- nautobot/extras/migrations/0004_populate_default_status_records.py +0 -1
- nautobot/extras/migrations/0005_configcontext_device_types.py +0 -1
- nautobot/extras/migrations/0006_graphqlquery.py +0 -1
- nautobot/extras/migrations/0007_configcontextschema.py +0 -1
- nautobot/extras/migrations/0008_jobresult__custom_field_data.py +0 -1
- nautobot/extras/migrations/0009_computedfield.py +0 -1
- nautobot/extras/migrations/0010_change_cf_validation_max_min_field_to_bigint.py +0 -1
- nautobot/extras/migrations/0011_fileattachment_fileproxy.py +0 -1
- nautobot/extras/migrations/0012_healthchecktestmodel.py +0 -1
- nautobot/extras/migrations/0013_default_fallback_value_computedfield.py +0 -1
- nautobot/extras/migrations/0014_auto_slug.py +0 -1
- nautobot/extras/migrations/0015_scheduled_job.py +0 -1
- nautobot/extras/migrations/0016_secret.py +0 -1
- nautobot/extras/migrations/0017_joblogentry.py +0 -1
- nautobot/extras/migrations/0018_joblog_data_migration.py +0 -2
- nautobot/extras/migrations/0019_joblogentry__meta_options__related_name.py +0 -1
- nautobot/extras/migrations/0020_customfield_changelog.py +0 -1
- nautobot/extras/migrations/0021_customfield_changelog_data.py +0 -1
- nautobot/extras/migrations/0022_objectchange_object_datav2.py +0 -1
- nautobot/extras/migrations/0023_job_model.py +0 -1
- nautobot/extras/migrations/0024_job_data_migration.py +0 -1
- nautobot/extras/migrations/0025_add_advanced_ui_boolean_to_customfield_conputedfield_and_relationship.py +0 -1
- nautobot/extras/migrations/0026_job_add_gitrepository_fk.py +0 -1
- nautobot/extras/migrations/0027_job_gitrepository_data_migration.py +0 -1
- nautobot/extras/migrations/0028_job_reduce_source.py +0 -1
- nautobot/extras/migrations/0029_dynamicgroup.py +0 -1
- nautobot/extras/migrations/0030_webhook_alter_unique_together.py +0 -1
- nautobot/extras/migrations/0031_tag_content_types.py +0 -1
- nautobot/extras/migrations/0032_tag_content_types_data_migration.py +0 -1
- nautobot/extras/migrations/0033_add__optimized_indexing.py +0 -1
- nautobot/extras/migrations/0034_alter_fileattachment_mimetype.py +0 -1
- nautobot/extras/migrations/0035_scheduledjob_crontab.py +0 -1
- nautobot/extras/migrations/0036_job_add_has_sensitive_variables.py +0 -1
- nautobot/extras/migrations/0037_configcontextschema__remove_name_unique__create_constraint_unique_name_owner.py +0 -1
- nautobot/extras/migrations/0038_configcontext_locations.py +0 -1
- nautobot/extras/migrations/0039_objectchange__add_change_context.py +0 -1
- nautobot/extras/migrations/0040_dynamicgroup__dynamicgroupmembership.py +0 -1
- nautobot/extras/migrations/0041_jobresult_job_kwargs.py +0 -1
- nautobot/extras/migrations/0042_job__add_is_job_hook_receiver.py +0 -1
- nautobot/extras/migrations/0043_note.py +0 -1
- nautobot/extras/migrations/0044_add_job_hook.py +0 -1
- nautobot/extras/migrations/0045_add_custom_field_slug.py +0 -1
- nautobot/extras/migrations/0046_populate_custom_field_slug_label.py +0 -1
- nautobot/extras/migrations/0047_enforce_custom_field_slug.py +0 -1
- nautobot/extras/migrations/0048_alter_objectchange_change_context_detail.py +0 -1
- nautobot/extras/migrations/0049_alter_tag_slug.py +0 -1
- nautobot/extras/migrations/0050_customfield_grouping.py +0 -1
- nautobot/extras/migrations/0051_add_job_task_queues.py +0 -1
- nautobot/extras/migrations/0052_configcontext_device_redundancy_groups.py +0 -1
- nautobot/extras/migrations/0053_relationship_required_on.py +0 -1
- nautobot/extras/migrations/0054_scheduledjob_kwargs_request_user_change.py +0 -1
- nautobot/extras/migrations/0055_configcontext_dynamic_groups.py +0 -1
- nautobot/extras/migrations/0056_objectchange_add_reverse_time_idx.py +0 -1
- nautobot/extras/migrations/0057_jobbutton.py +0 -1
- 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 -2
- nautobot/extras/migrations/{0059_alter_joblogentry_scheduledjob_webhook_fields.py → 0060_alter_joblogentry_scheduledjob_webhook_fields.py} +1 -2
- nautobot/extras/migrations/{0060_role_and_alter_status.py → 0061_role_and_alter_status.py} +1 -8
- nautobot/extras/migrations/{0061_collect_roles_from_related_apps_roles.py → 0062_collect_roles_from_related_apps_roles.py} +33 -33
- nautobot/extras/migrations/{0062_alter_role_options.py → 0063_alter_role_options.py} +1 -2
- nautobot/extras/migrations/{0063_alter_configcontext_and_add_new_role.py → 0064_alter_configcontext_and_add_new_role.py} +1 -2
- nautobot/extras/migrations/0065_configcontext_data_migrations.py +44 -0
- nautobot/extras/migrations/{0065_rename_configcontext_role.py → 0066_rename_configcontext_role.py} +1 -2
- nautobot/extras/migrations/{0066_jobresult__add_celery_fields.py → 0067_jobresult__add_celery_fields.py} +36 -3
- nautobot/extras/migrations/{0067_created_datetime.py → 0068_created_datetime.py} +1 -2
- nautobot/extras/migrations/{0068_remove_site_and_region_attributes_from_config_context.py → 0069_remove_site_and_region_attributes_from_config_context.py} +1 -2
- 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 -2
- 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 -10
- nautobot/extras/migrations/{0073_remove_gitrepository_fields.py → 0074_remove_gitrepository_fields.py} +1 -2
- 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/0078_remove_slug.py +45 -0
- 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 -12
- nautobot/extras/models/jobs.py +190 -323
- nautobot/extras/models/mixins.py +0 -71
- nautobot/extras/models/models.py +1 -22
- nautobot/extras/models/relationships.py +20 -21
- nautobot/extras/models/roles.py +0 -23
- nautobot/extras/models/secrets.py +2 -31
- 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 -121
- nautobot/extras/plugins/utils.py +0 -3
- nautobot/extras/plugins/validators.py +5 -4
- nautobot/extras/plugins/views.py +16 -4
- nautobot/extras/querysets.py +1 -7
- nautobot/extras/registry.py +3 -0
- nautobot/extras/signals.py +26 -60
- nautobot/extras/tables.py +42 -49
- 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/dynamicgroup_edit.html +0 -1
- 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 +14 -17
- nautobot/extras/templates/extras/jobresult.html +24 -6
- nautobot/extras/templates/extras/objectchange_list.html +1 -1
- nautobot/extras/templates/extras/scheduledjob.html +2 -2
- nautobot/extras/templates/extras/secret.html +28 -0
- nautobot/extras/templates/extras/secret_edit.html +0 -1
- nautobot/extras/templates/extras/secretsgroup_edit.html +0 -1
- nautobot/extras/templatetags/custom_links.py +0 -2
- nautobot/extras/templatetags/job_buttons.py +1 -0
- nautobot/extras/templatetags/plugins.py +0 -1
- 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/__init__.py +3 -3
- nautobot/extras/tests/integration/test_computedfields.py +1 -1
- nautobot/extras/tests/integration/test_configcontextschema.py +7 -5
- nautobot/extras/tests/integration/test_customfields.py +4 -2
- nautobot/extras/tests/integration/test_dynamicgroups.py +2 -2
- nautobot/extras/tests/integration/test_jobs.py +25 -27
- nautobot/extras/tests/integration/test_notes.py +8 -4
- nautobot/extras/tests/integration/test_plugins.py +4 -4
- nautobot/extras/tests/integration/test_relationships.py +2 -2
- nautobot/extras/tests/test_api.py +371 -381
- nautobot/extras/tests/test_changelog.py +17 -16
- nautobot/extras/tests/test_context_managers.py +5 -6
- nautobot/extras/tests/test_customfields.py +112 -73
- nautobot/extras/tests/test_datasources.py +191 -117
- nautobot/extras/tests/test_dynamicgroups.py +45 -68
- nautobot/extras/tests/test_filters.py +170 -130
- nautobot/extras/tests/test_forms.py +107 -109
- nautobot/extras/tests/{test_scripts.py → test_job_variables.py} +43 -49
- nautobot/extras/tests/test_jobs.py +271 -273
- nautobot/extras/tests/test_management.py +3 -6
- nautobot/extras/tests/test_migrations.py +5 -3
- nautobot/extras/tests/test_models.py +121 -173
- nautobot/extras/tests/test_notes.py +0 -1
- nautobot/extras/tests/test_plugins.py +55 -89
- nautobot/extras/tests/test_relationships.py +174 -130
- nautobot/extras/tests/test_tags.py +6 -12
- nautobot/extras/tests/test_utils.py +31 -1
- nautobot/extras/tests/test_views.py +223 -184
- nautobot/extras/tests/test_webhooks.py +16 -15
- nautobot/extras/urls.py +69 -69
- nautobot/extras/utils.py +137 -163
- nautobot/extras/views.py +81 -153
- nautobot/ipam/api/fields.py +17 -0
- nautobot/ipam/api/serializers.py +77 -164
- nautobot/ipam/api/urls.py +4 -1
- nautobot/ipam/api/views.py +28 -19
- nautobot/ipam/apps.py +1 -0
- nautobot/ipam/choices.py +5 -12
- nautobot/ipam/constants.py +1 -0
- nautobot/ipam/factory.py +41 -30
- nautobot/ipam/filters.py +58 -25
- nautobot/ipam/forms.py +82 -211
- nautobot/ipam/graphql/types.py +0 -9
- nautobot/ipam/lookups.py +13 -8
- nautobot/ipam/management/commands/__init__.py +0 -0
- nautobot/ipam/management/commands/fix_prefix_broadcast.py +17 -0
- nautobot/ipam/migrations/0001_initial_part_1.py +0 -1
- nautobot/ipam/migrations/0002_initial_part_2.py +0 -1
- nautobot/ipam/migrations/0003_remove_max_length.py +0 -1
- nautobot/ipam/migrations/0004_fixup_p2p_broadcast.py +0 -1
- nautobot/ipam/migrations/0005_auto_slug.py +0 -1
- nautobot/ipam/migrations/0006_ipaddress_nat_outside_list.py +0 -1
- nautobot/ipam/migrations/0007_add_natural_indexing.py +0 -1
- nautobot/ipam/migrations/0008_prefix_vlan_vlangroup_location.py +0 -1
- nautobot/ipam/migrations/0009_alter_vlan_name.py +0 -1
- nautobot/ipam/migrations/0010_alter_ipam_role_add_new_role.py +1 -2
- nautobot/ipam/migrations/0011_migrate_ipam_role_data.py +32 -39
- nautobot/ipam/migrations/0012_rename_ipam_roles.py +0 -1
- nautobot/ipam/migrations/0013_delete_role.py +0 -1
- nautobot/ipam/migrations/0014_rename_foreign_keys_and_related_names.py +0 -1
- nautobot/ipam/migrations/0015_prefix_add_type.py +0 -1
- nautobot/ipam/migrations/0016_prefix_type_data_migration.py +0 -3
- nautobot/ipam/migrations/0017_prefix_remove_is_pool.py +0 -1
- nautobot/ipam/migrations/0018_remove_site_foreign_key_from_ipam_models.py +0 -1
- nautobot/ipam/migrations/0019_created_datetime.py +0 -1
- nautobot/ipam/migrations/0020_related_name_changes.py +1 -2
- nautobot/ipam/migrations/0021_prefix_add_rir_and_date_allocated.py +0 -1
- nautobot/ipam/migrations/0022_aggregate_to_prefix_data_migration.py +3 -5
- nautobot/ipam/migrations/0023_delete_aggregate.py +0 -1
- nautobot/ipam/migrations/0024_interface_to_ipaddress_m2m.py +0 -1
- nautobot/ipam/migrations/0025_interface_ipaddress_m2m_data_migration.py +0 -1
- nautobot/ipam/migrations/0026_ipaddress_remove_assigned_object.py +0 -1
- nautobot/ipam/migrations/0027_remove_rir_slug.py +16 -0
- nautobot/ipam/migrations/0028_tagsfield.py +44 -0
- nautobot/ipam/migrations/0029_ip_address_to_interface_uniqueness_constraints.py +18 -0
- nautobot/ipam/migrations/0030_ipam__namespaces.py +231 -0
- 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 +579 -368
- nautobot/ipam/navigation.py +36 -159
- nautobot/ipam/querysets.py +117 -90
- nautobot/ipam/signals.py +89 -0
- nautobot/ipam/tables.py +86 -28
- nautobot/ipam/templates/ipam/ipaddress.html +14 -30
- nautobot/ipam/templates/ipam/ipaddress_edit.html +1 -0
- 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 +42 -0
- nautobot/ipam/templates/ipam/namespace_vrfs.html +11 -0
- nautobot/ipam/templates/ipam/prefix.html +27 -33
- nautobot/ipam/templates/ipam/prefix_edit.html +7 -1
- nautobot/ipam/templates/ipam/vlangroup.html +0 -13
- nautobot/ipam/templates/ipam/vrf.html +6 -4
- nautobot/ipam/templates/ipam/vrf_edit.html +20 -2
- nautobot/ipam/tests/integration/test_prefixes.py +4 -27
- nautobot/ipam/tests/test_api.py +60 -61
- nautobot/ipam/tests/test_filters.py +187 -126
- nautobot/ipam/tests/test_forms.py +12 -6
- nautobot/ipam/tests/test_graphql.py +8 -6
- nautobot/ipam/tests/test_migrations.py +8 -13
- nautobot/ipam/tests/test_models.py +426 -274
- nautobot/ipam/tests/test_ordering.py +6 -3
- nautobot/ipam/tests/test_querysets.py +340 -96
- nautobot/ipam/tests/test_views.py +100 -55
- nautobot/ipam/urls.py +28 -5
- nautobot/ipam/{utils.py → utils/__init__.py} +2 -2
- nautobot/ipam/utils/migrations.py +713 -0
- nautobot/ipam/views.py +237 -122
- nautobot/project-static/docs/404.html +1399 -166
- nautobot/project-static/docs/additional-features/caching.html +1416 -320
- nautobot/project-static/docs/additional-features/change-logging.html +1389 -190
- nautobot/project-static/docs/additional-features/config-contexts.html +1389 -190
- nautobot/project-static/docs/additional-features/graphql.html +1389 -190
- nautobot/project-static/docs/additional-features/healthcheck.html +1389 -190
- nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +1393 -190
- nautobot/project-static/docs/additional-features/jobs.html +1677 -460
- nautobot/project-static/docs/additional-features/napalm.html +1389 -190
- nautobot/project-static/docs/additional-features/prometheus-metrics.html +1389 -190
- nautobot/project-static/docs/additional-features/template-filters.html +1389 -190
- nautobot/project-static/docs/administration/celery-queues.html +1389 -190
- nautobot/project-static/docs/administration/nautobot-server.html +1553 -375
- nautobot/project-static/docs/administration/nautobot-shell.html +1395 -196
- nautobot/project-static/docs/administration/permissions.html +1389 -190
- nautobot/project-static/docs/administration/replicating-nautobot.html +1387 -207
- nautobot/project-static/docs/apps/index.html +1389 -190
- nautobot/project-static/docs/apps/nautobot-apps.html +1387 -175
- nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js +29 -0
- nautobot/project-static/docs/assets/javascripts/bundle.51198bba.min.js.map +8 -0
- nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js → search.208ed371.min.js} +9 -15
- nautobot/project-static/docs/assets/javascripts/workers/{search.16e2a7d4.min.js.map → search.208ed371.min.js.map} +4 -4
- nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/main.ded33207.min.css.map +1 -0
- nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css +1 -0
- nautobot/project-static/docs/assets/stylesheets/palette.a0c5b2b5.min.css.map +1 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +1775 -590
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +1389 -190
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +3588 -1922
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +1461 -262
- nautobot/project-static/docs/code-reference/nautobot/apps/config.html +1401 -170
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +1396 -191
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +2095 -894
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +2357 -1194
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +2258 -940
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +1389 -190
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +1400 -201
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +11068 -7861
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +2867 -2224
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +1389 -190
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +2641 -1573
- nautobot/project-static/docs/configuration/authentication/ldap.html +1389 -190
- nautobot/project-static/docs/configuration/authentication/remote.html +1389 -190
- nautobot/project-static/docs/configuration/authentication/sso.html +1389 -190
- nautobot/project-static/docs/configuration/index.html +1398 -199
- nautobot/project-static/docs/configuration/optional-settings.html +1418 -274
- nautobot/project-static/docs/configuration/required-settings.html +1419 -287
- nautobot/project-static/docs/core-functionality/circuits.html +1446 -247
- nautobot/project-static/docs/core-functionality/device-types.html +1448 -249
- nautobot/project-static/docs/core-functionality/devices.html +1452 -249
- nautobot/project-static/docs/core-functionality/ipam.html +1452 -253
- nautobot/project-static/docs/core-functionality/power.html +1448 -249
- nautobot/project-static/docs/core-functionality/secrets.html +1448 -249
- nautobot/project-static/docs/core-functionality/services.html +1448 -249
- nautobot/project-static/docs/core-functionality/sites-and-racks.html +1448 -249
- nautobot/project-static/docs/core-functionality/tenancy.html +1448 -249
- nautobot/project-static/docs/core-functionality/virtualization.html +1452 -249
- nautobot/project-static/docs/core-functionality/vlans.html +1448 -249
- nautobot/project-static/docs/development/application-registry.html +1393 -214
- nautobot/project-static/docs/development/best-practices.html +1392 -192
- nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +1390 -191
- nautobot/project-static/docs/development/extending-models.html +1443 -257
- nautobot/project-static/docs/development/generic-views.html +1403 -174
- nautobot/project-static/docs/development/getting-started.html +1568 -262
- nautobot/project-static/docs/development/homepage.html +1389 -190
- nautobot/project-static/docs/development/index.html +1389 -190
- nautobot/project-static/docs/development/model-features.html +1389 -190
- nautobot/project-static/docs/development/natural-keys.html +1389 -190
- nautobot/project-static/docs/development/navigation-menu.html +1451 -330
- nautobot/project-static/docs/development/react-ui.html +4199 -0
- nautobot/project-static/docs/development/release-checklist.html +1392 -193
- nautobot/project-static/docs/development/role-internals.html +1402 -172
- nautobot/project-static/docs/development/style-guide.html +1399 -199
- nautobot/project-static/docs/development/templates.html +1391 -191
- nautobot/project-static/docs/development/testing.html +1389 -190
- nautobot/project-static/docs/development/user-preferences.html +1389 -190
- nautobot/project-static/docs/docker/index.html +1408 -206
- nautobot/project-static/docs/index.html +1397 -180
- nautobot/project-static/docs/installation/centos.html +1401 -170
- nautobot/project-static/docs/installation/external-authentication.html +1389 -190
- nautobot/project-static/docs/installation/http-server.html +1389 -190
- nautobot/project-static/docs/installation/index.html +1394 -191
- nautobot/project-static/docs/installation/migrating-from-netbox.html +1452 -305
- nautobot/project-static/docs/installation/migrating-from-postgresql.html +1390 -191
- nautobot/project-static/docs/installation/nautobot.html +1390 -191
- nautobot/project-static/docs/installation/region-and-site-data-migration-guide.html +1389 -190
- nautobot/project-static/docs/installation/selinux-troubleshooting.html +1401 -170
- nautobot/project-static/docs/installation/services.html +1389 -190
- 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 +1401 -170
- nautobot/project-static/docs/installation/upgrading-from-nautobot-v1.html +4254 -1923
- nautobot/project-static/docs/installation/upgrading.html +1395 -192
- nautobot/project-static/docs/models/circuits/circuit.html +1427 -174
- nautobot/project-static/docs/models/circuits/circuittermination.html +1427 -174
- nautobot/project-static/docs/models/circuits/circuittype.html +1427 -174
- nautobot/project-static/docs/models/circuits/provider.html +1427 -174
- nautobot/project-static/docs/models/circuits/providernetwork.html +1427 -174
- nautobot/project-static/docs/models/dcim/cable.html +1458 -174
- nautobot/project-static/docs/models/dcim/consoleport.html +1427 -174
- nautobot/project-static/docs/models/dcim/consoleporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/consoleserverport.html +1427 -174
- nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/device.html +1431 -174
- nautobot/project-static/docs/models/dcim/devicebay.html +1427 -174
- nautobot/project-static/docs/models/dcim/devicebaytemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +1522 -177
- nautobot/project-static/docs/models/dcim/devicetype.html +1427 -174
- nautobot/project-static/docs/models/dcim/frontport.html +1427 -174
- nautobot/project-static/docs/models/dcim/frontporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/interface.html +1427 -174
- nautobot/project-static/docs/models/dcim/interfacetemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/inventoryitem.html +1427 -174
- nautobot/project-static/docs/models/dcim/location.html +1427 -174
- nautobot/project-static/docs/models/dcim/locationtype.html +1427 -174
- nautobot/project-static/docs/models/dcim/manufacturer.html +1427 -174
- nautobot/project-static/docs/models/dcim/platform.html +1427 -174
- nautobot/project-static/docs/models/dcim/powerfeed.html +1425 -172
- nautobot/project-static/docs/models/dcim/poweroutlet.html +1427 -174
- nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/powerpanel.html +1425 -172
- nautobot/project-static/docs/models/dcim/powerport.html +1427 -174
- nautobot/project-static/docs/models/dcim/powerporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/rack.html +1427 -174
- nautobot/project-static/docs/models/dcim/rackgroup.html +1427 -174
- nautobot/project-static/docs/models/dcim/rackreservation.html +1427 -174
- nautobot/project-static/docs/models/dcim/rearport.html +1427 -174
- nautobot/project-static/docs/models/dcim/rearporttemplate.html +1427 -174
- nautobot/project-static/docs/models/dcim/region.html +1401 -170
- nautobot/project-static/docs/models/dcim/site.html +1401 -170
- nautobot/project-static/docs/models/dcim/virtualchassis.html +1425 -172
- nautobot/project-static/docs/models/extras/computedfield.html +1393 -194
- nautobot/project-static/docs/models/extras/configcontext.html +1465 -174
- nautobot/project-static/docs/models/extras/configcontextschema.html +1421 -168
- nautobot/project-static/docs/models/extras/customfield.html +1389 -190
- nautobot/project-static/docs/models/extras/customlink.html +1389 -190
- nautobot/project-static/docs/models/extras/dynamicgroup.html +1398 -199
- nautobot/project-static/docs/models/extras/exporttemplate.html +1389 -190
- nautobot/project-static/docs/models/extras/gitrepository.html +1393 -190
- nautobot/project-static/docs/models/extras/graphqlquery.html +1469 -171
- nautobot/project-static/docs/models/extras/imageattachment.html +1434 -181
- nautobot/project-static/docs/models/extras/job.html +1411 -157
- nautobot/project-static/docs/models/extras/jobbutton.html +1410 -207
- nautobot/project-static/docs/models/extras/jobhook.html +1397 -194
- nautobot/project-static/docs/models/extras/joblogentry.html +1408 -155
- nautobot/project-static/docs/models/extras/jobresult.html +1417 -159
- nautobot/project-static/docs/models/extras/note.html +1389 -190
- nautobot/project-static/docs/models/extras/relationship.html +1391 -192
- nautobot/project-static/docs/models/extras/role.html +1495 -198
- nautobot/project-static/docs/models/extras/secret.html +1492 -201
- nautobot/project-static/docs/models/extras/secretsgroup.html +1410 -157
- nautobot/project-static/docs/models/extras/status.html +1381 -221
- nautobot/project-static/docs/models/extras/tag.html +1389 -190
- nautobot/project-static/docs/models/extras/webhook.html +1389 -190
- nautobot/project-static/docs/models/ipam/ipaddress.html +1488 -200
- nautobot/project-static/docs/models/ipam/prefix.html +1410 -157
- nautobot/project-static/docs/models/ipam/rir.html +1410 -157
- nautobot/project-static/docs/models/ipam/routetarget.html +1410 -157
- nautobot/project-static/docs/models/ipam/service.html +1410 -157
- nautobot/project-static/docs/models/ipam/vlan.html +1410 -157
- nautobot/project-static/docs/models/ipam/vlangroup.html +1410 -157
- nautobot/project-static/docs/models/ipam/vrf.html +1410 -161
- nautobot/project-static/docs/models/tenancy/tenant.html +1412 -159
- nautobot/project-static/docs/models/tenancy/tenantgroup.html +1412 -159
- nautobot/project-static/docs/models/users/objectpermission.html +1462 -171
- nautobot/project-static/docs/models/users/token.html +1410 -157
- nautobot/project-static/docs/models/virtualization/cluster.html +1410 -157
- nautobot/project-static/docs/models/virtualization/clustergroup.html +1410 -157
- nautobot/project-static/docs/models/virtualization/clustertype.html +1410 -157
- nautobot/project-static/docs/models/virtualization/virtualmachine.html +1414 -157
- nautobot/project-static/docs/models/virtualization/vminterface.html +1410 -157
- nautobot/project-static/docs/objects.inv +0 -0
- nautobot/project-static/docs/plugins/development.html +1916 -646
- nautobot/project-static/docs/plugins/index.html +1389 -190
- nautobot/project-static/docs/plugins/porting-from-netbox.html +1389 -190
- nautobot/project-static/docs/release-notes/index.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.0.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.1.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.2.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.3.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.4.html +1389 -190
- nautobot/project-static/docs/release-notes/version-1.5.html +2016 -397
- nautobot/project-static/docs/release-notes/version-2.0.html +1935 -287
- nautobot/project-static/docs/requirements.txt +5 -4
- nautobot/project-static/docs/rest-api/authentication.html +1389 -190
- nautobot/project-static/docs/rest-api/filtering.html +1389 -190
- nautobot/project-static/docs/rest-api/overview.html +2002 -576
- 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 +1390 -191
- nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +1392 -193
- nautobot/project-static/docs/user-guides/getting-started/index.html +1388 -189
- nautobot/project-static/docs/user-guides/getting-started/interfaces.html +1388 -189
- nautobot/project-static/docs/user-guides/getting-started/ipam.html +1386 -187
- nautobot/project-static/docs/user-guides/getting-started/platforms.html +1448 -249
- nautobot/project-static/docs/user-guides/getting-started/regions.html +1411 -212
- nautobot/project-static/docs/user-guides/getting-started/search-bar.html +1395 -196
- nautobot/project-static/docs/user-guides/getting-started/tenants.html +1448 -249
- nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +1448 -249
- nautobot/project-static/docs/user-guides/git-data-source.html +1405 -206
- nautobot/project-static/docs/user-guides/graphql.html +1402 -203
- nautobot/project-static/docs/user-guides/relationships.html +1448 -249
- nautobot/project-static/docs/user-guides/s3-django-storage.html +1448 -249
- nautobot/project-static/js/forms.js +16 -9
- nautobot/project-static/js/theme.js +5 -0
- nautobot/tenancy/api/serializers.py +4 -34
- nautobot/tenancy/api/urls.py +1 -1
- nautobot/tenancy/filters/__init__.py +9 -7
- nautobot/tenancy/filters/mixins.py +3 -2
- nautobot/tenancy/forms.py +3 -36
- nautobot/tenancy/migrations/0001_initial.py +0 -1
- nautobot/tenancy/migrations/0002_auto_slug.py +0 -1
- nautobot/tenancy/migrations/0003_mptt_to_tree_queries.py +0 -1
- nautobot/tenancy/migrations/0004_change_tree_manager_on_tree_models.py +0 -1
- nautobot/tenancy/migrations/0005_rename_foreign_keys_and_related_names.py +0 -1
- nautobot/tenancy/migrations/0006_created_datetime.py +0 -1
- nautobot/tenancy/migrations/0007_remove_tenant_tenantgroup_slug.py +20 -0
- nautobot/tenancy/migrations/0008_tagsfield.py +19 -0
- nautobot/tenancy/models.py +0 -30
- nautobot/tenancy/navigation.py +6 -39
- nautobot/tenancy/tables.py +4 -4
- nautobot/tenancy/templates/tenancy/tenant.html +12 -12
- nautobot/tenancy/templates/tenancy/tenant_edit.html +0 -1
- nautobot/tenancy/templates/tenancy/tenantgroup.html +1 -1
- nautobot/tenancy/tests/test_api.py +1 -12
- nautobot/tenancy/tests/test_filters.py +20 -12
- nautobot/tenancy/tests/test_views.py +11 -29
- nautobot/tenancy/urls.py +10 -10
- nautobot/tenancy/views.py +0 -3
- 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/migrations/0001_initial.py +0 -1
- nautobot/users/migrations/0002_token_ordering_by_created.py +0 -1
- nautobot/users/migrations/0003_alter_user_options.py +0 -1
- nautobot/users/migrations/0004_alter_user_managers.py +0 -1
- nautobot/users/tests/test_api.py +109 -28
- nautobot/users/tests/test_filters.py +0 -4
- nautobot/users/tests/test_models.py +0 -1
- nautobot/users/views.py +0 -7
- nautobot/virtualization/api/serializers.py +18 -132
- nautobot/virtualization/api/urls.py +1 -1
- nautobot/virtualization/api/views.py +1 -22
- nautobot/virtualization/choices.py +0 -2
- nautobot/virtualization/filters.py +12 -7
- nautobot/virtualization/forms.py +21 -117
- nautobot/virtualization/migrations/0001_initial.py +0 -1
- nautobot/virtualization/migrations/0002_virtualmachine_local_context_schema.py +0 -1
- nautobot/virtualization/migrations/0003_vminterface_verbose_name.py +0 -1
- nautobot/virtualization/migrations/0004_auto_slug.py +0 -1
- nautobot/virtualization/migrations/0005_add_natural_indexing.py +0 -1
- nautobot/virtualization/migrations/0006_vminterface_status.py +0 -1
- nautobot/virtualization/migrations/0007_vminterface_status_data_migration.py +0 -1
- nautobot/virtualization/migrations/0008_vminterface_parent.py +0 -1
- nautobot/virtualization/migrations/0009_cluster_location.py +0 -1
- nautobot/virtualization/migrations/0010_vminterface_mac_address_data_migration.py +0 -1
- nautobot/virtualization/migrations/0011_alter_vminterface_mac_address.py +0 -1
- nautobot/virtualization/migrations/0012_alter_virtualmachine_role_add_new_role.py +1 -2
- nautobot/virtualization/migrations/0013_migrate_virtualmachine_role_data.py +18 -12
- nautobot/virtualization/migrations/0014_rename_virtualmachine_roles.py +0 -1
- nautobot/virtualization/migrations/0015_rename_foreignkey_fields.py +1 -2
- nautobot/virtualization/migrations/0016_remove_site_foreign_key_from_cluster_class.py +0 -1
- nautobot/virtualization/migrations/0017_created_datetime.py +0 -1
- nautobot/virtualization/migrations/0018_related_name_changes.py +1 -2
- nautobot/virtualization/migrations/0019_vminterface_ip_addresses_m2m.py +0 -1
- nautobot/virtualization/migrations/0020_remove_clustergroup_clustertype_slug.py +20 -0
- 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/0023_ipam__namespaces.py +25 -0
- nautobot/virtualization/migrations/0024_fixup_null_statuses.py +25 -0
- nautobot/virtualization/migrations/0025_status_nonnullable.py +29 -0
- nautobot/virtualization/models.py +39 -131
- nautobot/virtualization/navigation.py +18 -99
- nautobot/virtualization/tables.py +4 -4
- nautobot/virtualization/templates/virtualization/virtualmachine.html +13 -2
- nautobot/virtualization/templates/virtualization/virtualmachine_edit.html +6 -0
- nautobot/virtualization/tests/test_api.py +42 -52
- nautobot/virtualization/tests/test_filters.py +98 -75
- nautobot/virtualization/tests/test_models.py +36 -13
- nautobot/virtualization/tests/test_views.py +68 -73
- nautobot/virtualization/urls.py +10 -10
- nautobot/virtualization/views.py +8 -14
- {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/METADATA +15 -22
- {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/RECORD +987 -834
- {nautobot-2.0.0a2.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 -42
- nautobot/extras/migrations/0071_job__unique_name_data_migration.py +0 -47
- 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 -19
- 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 -143
- nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js +0 -29
- nautobot/project-static/docs/assets/javascripts/bundle.5a2dcb6a.min.js.map +0 -8
- nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js +0 -18
- nautobot/project-static/docs/assets/javascripts/extra/bundle.5f09fbc3.min.js.map +0 -8
- nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/extra.0d2c79a8.min.css.map +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/main.975780f9.min.css.map +0 -1
- nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css +0 -1
- nautobot/project-static/docs/assets/stylesheets/palette.2505c338.min.css.map +0 -1
- 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.0a2.dist-info → nautobot-2.0.0b1.dist-info}/LICENSE.txt +0 -0
- {nautobot-2.0.0a2.dist-info → nautobot-2.0.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from datetime import datetime, timedelta
|
|
2
2
|
import uuid
|
|
3
|
-
|
|
3
|
+
import tempfile
|
|
4
|
+
from unittest import mock, skip
|
|
4
5
|
|
|
5
6
|
from django.conf import settings
|
|
6
7
|
from django.contrib.auth import get_user_model
|
|
@@ -26,8 +27,7 @@ from nautobot.dcim.models import (
|
|
|
26
27
|
RackGroup,
|
|
27
28
|
)
|
|
28
29
|
from nautobot.dcim.tests import test_views
|
|
29
|
-
from nautobot.extras.api.
|
|
30
|
-
from nautobot.extras.api.serializers import ConfigContextSerializer
|
|
30
|
+
from nautobot.extras.api.serializers import ConfigContextSerializer, JobResultSerializer
|
|
31
31
|
from nautobot.extras.choices import (
|
|
32
32
|
DynamicGroupOperatorChoices,
|
|
33
33
|
JobExecutionType,
|
|
@@ -65,10 +65,12 @@ from nautobot.extras.models import (
|
|
|
65
65
|
Webhook,
|
|
66
66
|
)
|
|
67
67
|
from nautobot.extras.models.jobs import JobHook, JobButton
|
|
68
|
+
|
|
68
69
|
from nautobot.extras.tests.test_relationships import RequiredRelationshipTestMixin
|
|
69
70
|
from nautobot.extras.utils import TaggableClassesQuery
|
|
71
|
+
|
|
70
72
|
from nautobot.ipam.factory import VLANFactory
|
|
71
|
-
from nautobot.ipam.models import
|
|
73
|
+
from nautobot.ipam.models import VLANGroup, VLAN
|
|
72
74
|
from nautobot.users.models import ObjectPermission
|
|
73
75
|
|
|
74
76
|
|
|
@@ -90,32 +92,22 @@ class AppTest(APITestCase):
|
|
|
90
92
|
|
|
91
93
|
class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
92
94
|
model = ComputedField
|
|
93
|
-
brief_fields = [
|
|
94
|
-
"content_type",
|
|
95
|
-
"display",
|
|
96
|
-
"id",
|
|
97
|
-
"label",
|
|
98
|
-
"url",
|
|
99
|
-
]
|
|
100
95
|
choices_fields = ["content_type"]
|
|
101
96
|
create_data = [
|
|
102
97
|
{
|
|
103
98
|
"content_type": "dcim.location",
|
|
104
|
-
"slug": "cf4",
|
|
105
99
|
"label": "Computed Field 4",
|
|
106
100
|
"template": "{{ obj.name }}",
|
|
107
101
|
"fallback_value": "error",
|
|
108
102
|
},
|
|
109
103
|
{
|
|
110
104
|
"content_type": "dcim.location",
|
|
111
|
-
"slug": "cf5",
|
|
112
105
|
"label": "Computed Field 5",
|
|
113
106
|
"template": "{{ obj.name }}",
|
|
114
107
|
"fallback_value": "error",
|
|
115
108
|
},
|
|
116
109
|
{
|
|
117
110
|
"content_type": "dcim.location",
|
|
118
|
-
"slug": "cf6",
|
|
119
111
|
"label": "Computed Field 6",
|
|
120
112
|
"template": "{{ obj.name }}",
|
|
121
113
|
},
|
|
@@ -128,7 +120,7 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
128
120
|
]
|
|
129
121
|
update_data = {
|
|
130
122
|
"content_type": "dcim.location",
|
|
131
|
-
"
|
|
123
|
+
"key": "cf1",
|
|
132
124
|
"label": "My Computed Field",
|
|
133
125
|
}
|
|
134
126
|
bulk_update_data = {
|
|
@@ -142,21 +134,21 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
142
134
|
location_ct = ContentType.objects.get_for_model(Location)
|
|
143
135
|
|
|
144
136
|
ComputedField.objects.create(
|
|
145
|
-
|
|
137
|
+
key="cf1",
|
|
146
138
|
label="Computed Field One",
|
|
147
139
|
template="{{ obj.name }}",
|
|
148
140
|
fallback_value="error",
|
|
149
141
|
content_type=location_ct,
|
|
150
142
|
)
|
|
151
143
|
ComputedField.objects.create(
|
|
152
|
-
|
|
144
|
+
key="cf2",
|
|
153
145
|
label="Computed Field Two",
|
|
154
146
|
template="{{ obj.name }}",
|
|
155
147
|
fallback_value="error",
|
|
156
148
|
content_type=location_ct,
|
|
157
149
|
)
|
|
158
150
|
ComputedField.objects.create(
|
|
159
|
-
|
|
151
|
+
key="cf3",
|
|
160
152
|
label="Computed Field Three",
|
|
161
153
|
template="{{ obj.name }}",
|
|
162
154
|
fallback_value="error",
|
|
@@ -182,7 +174,6 @@ class ComputedFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
182
174
|
|
|
183
175
|
class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
184
176
|
model = ConfigContext
|
|
185
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
186
177
|
create_data = [
|
|
187
178
|
{
|
|
188
179
|
"name": "Config Context 4",
|
|
@@ -212,11 +203,14 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
212
203
|
"""
|
|
213
204
|
Test rendering config context data for a device.
|
|
214
205
|
"""
|
|
215
|
-
manufacturer = Manufacturer.objects.
|
|
206
|
+
manufacturer = Manufacturer.objects.first()
|
|
216
207
|
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1", slug="device-type-1")
|
|
217
208
|
devicerole = Role.objects.get_for_model(Device).first()
|
|
209
|
+
devicestatus = Status.objects.get_for_model(Device).first()
|
|
218
210
|
location = Location.objects.filter(location_type=LocationType.objects.get(name="Campus")).first()
|
|
219
|
-
device = Device.objects.create(
|
|
211
|
+
device = Device.objects.create(
|
|
212
|
+
name="Device 1", device_type=devicetype, role=devicerole, status=devicestatus, location=location
|
|
213
|
+
)
|
|
220
214
|
|
|
221
215
|
# Test default config contexts (created at test setup)
|
|
222
216
|
rendered_context = device.get_config_context()
|
|
@@ -227,7 +221,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
227
221
|
# Test API response as well
|
|
228
222
|
self.add_permissions("dcim.view_device")
|
|
229
223
|
device_url = reverse("dcim-api:device-detail", kwargs={"pk": device.pk})
|
|
230
|
-
response = self.client.get(device_url, **self.header)
|
|
224
|
+
response = self.client.get(device_url + "?include=config_context", **self.header)
|
|
231
225
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
232
226
|
self.assertIn("config_context", response.data)
|
|
233
227
|
self.assertEqual(response.data["config_context"], {"foo": 123, "bar": 456, "baz": 789}, response.data)
|
|
@@ -238,7 +232,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
238
232
|
configcontext4.locations.add(location)
|
|
239
233
|
rendered_context = device.get_config_context()
|
|
240
234
|
self.assertEqual(rendered_context["location_data"], "ABC")
|
|
241
|
-
response = self.client.get(device_url, **self.header)
|
|
235
|
+
response = self.client.get(device_url + "?include=config_context", **self.header)
|
|
242
236
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
243
237
|
self.assertIn("config_context", response.data)
|
|
244
238
|
self.assertEqual(response.data["config_context"]["location_data"], "ABC", response.data["config_context"])
|
|
@@ -249,7 +243,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
249
243
|
configcontext5.locations.add(location)
|
|
250
244
|
rendered_context = device.get_config_context()
|
|
251
245
|
self.assertEqual(rendered_context["foo"], 999)
|
|
252
|
-
response = self.client.get(device_url, **self.header)
|
|
246
|
+
response = self.client.get(device_url + "?include=config_context", **self.header)
|
|
253
247
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
254
248
|
self.assertIn("config_context", response.data)
|
|
255
249
|
self.assertEqual(response.data["config_context"]["foo"], 999, response.data["config_context"])
|
|
@@ -261,7 +255,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
261
255
|
configcontext6.locations.add(location2)
|
|
262
256
|
rendered_context = device.get_config_context()
|
|
263
257
|
self.assertEqual(rendered_context["bar"], 456)
|
|
264
|
-
response = self.client.get(device_url, **self.header)
|
|
258
|
+
response = self.client.get(device_url + "?include=config_context", **self.header)
|
|
265
259
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
266
260
|
self.assertIn("config_context", response.data)
|
|
267
261
|
self.assertEqual(response.data["config_context"]["bar"], 456, response.data["config_context"])
|
|
@@ -285,7 +279,7 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
285
279
|
}
|
|
286
280
|
response = self.client.post(self._get_list_url(), data, format="json", **self.header)
|
|
287
281
|
self.assertHttpStatus(response, status.HTTP_201_CREATED)
|
|
288
|
-
self.assertEqual(response.data["config_context_schema"]
|
|
282
|
+
self.assertEqual(response.data["config_context_schema"], self.absolute_api_url(schema))
|
|
289
283
|
|
|
290
284
|
def test_schema_validation_fails(self):
|
|
291
285
|
"""
|
|
@@ -322,7 +316,6 @@ class ConfigContextTest(APIViewTestCases.APIViewTestCase):
|
|
|
322
316
|
|
|
323
317
|
class ConfigContextSchemaTest(APIViewTestCases.APIViewTestCase):
|
|
324
318
|
model = ConfigContextSchema
|
|
325
|
-
brief_fields = ["display", "id", "name", "slug", "url"]
|
|
326
319
|
create_data = [
|
|
327
320
|
{
|
|
328
321
|
"name": "Schema 4",
|
|
@@ -385,31 +378,33 @@ class ContentTypeTest(APITestCase):
|
|
|
385
378
|
|
|
386
379
|
|
|
387
380
|
class CreatedUpdatedFilterTest(APITestCase):
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
location=
|
|
398
|
-
rack_group=
|
|
399
|
-
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,
|
|
400
394
|
name="Test Rack 1",
|
|
401
395
|
u_height=42,
|
|
402
396
|
)
|
|
403
|
-
|
|
404
|
-
location=
|
|
405
|
-
rack_group=
|
|
406
|
-
role=
|
|
397
|
+
cls.rack2 = Rack.objects.create(
|
|
398
|
+
location=cls.location1,
|
|
399
|
+
rack_group=cls.rackgroup1,
|
|
400
|
+
role=cls.rackrole1,
|
|
401
|
+
status=cls.rackstatus1,
|
|
407
402
|
name="Test Rack 2",
|
|
408
403
|
u_height=42,
|
|
409
404
|
)
|
|
410
405
|
|
|
411
406
|
# change the created and last_updated of one
|
|
412
|
-
Rack.objects.filter(pk=
|
|
407
|
+
Rack.objects.filter(pk=cls.rack2.pk).update(
|
|
413
408
|
created=make_aware(datetime(2001, 2, 3, 0, 1, 2, 3)),
|
|
414
409
|
last_updated=make_aware(datetime(2001, 2, 3, 1, 2, 3, 4)),
|
|
415
410
|
)
|
|
@@ -483,7 +478,6 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
483
478
|
"""Tests for the CustomField REST API."""
|
|
484
479
|
|
|
485
480
|
model = CustomField
|
|
486
|
-
brief_fields = ["display", "id", "key", "url"]
|
|
487
481
|
create_data = [
|
|
488
482
|
{
|
|
489
483
|
"content_types": ["dcim.location"],
|
|
@@ -556,7 +550,6 @@ class CustomFieldTest(APIViewTestCases.APIViewTestCase):
|
|
|
556
550
|
|
|
557
551
|
class CustomLinkTest(APIViewTestCases.APIViewTestCase):
|
|
558
552
|
model = CustomLink
|
|
559
|
-
brief_fields = ["content_type", "display", "id", "name", "url"]
|
|
560
553
|
create_data = [
|
|
561
554
|
{
|
|
562
555
|
"content_type": "dcim.location",
|
|
@@ -622,38 +615,44 @@ class DynamicGroupTestMixin:
|
|
|
622
615
|
def setUpTestData(cls):
|
|
623
616
|
# Create the objects required for devices.
|
|
624
617
|
location_type = LocationType.objects.get(name="Campus")
|
|
618
|
+
location_status = Status.objects.get_for_model(Location).first()
|
|
625
619
|
locations = (
|
|
626
|
-
Location.objects.create(
|
|
627
|
-
|
|
628
|
-
|
|
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
|
+
),
|
|
629
629
|
)
|
|
630
630
|
|
|
631
|
-
manufacturer = Manufacturer.objects.
|
|
631
|
+
manufacturer = Manufacturer.objects.first()
|
|
632
632
|
device_type = DeviceType.objects.create(
|
|
633
633
|
manufacturer=manufacturer,
|
|
634
634
|
model="device Type 1",
|
|
635
635
|
slug="device-type-1",
|
|
636
636
|
)
|
|
637
637
|
device_role = Role.objects.get_for_model(Device).first()
|
|
638
|
-
|
|
639
|
-
status_planned = Status.objects.get(slug="planned")
|
|
638
|
+
statuses = Status.objects.get_for_model(Device)
|
|
640
639
|
Device.objects.create(
|
|
641
640
|
name="device-location-1",
|
|
642
|
-
status=
|
|
641
|
+
status=statuses[0],
|
|
643
642
|
role=device_role,
|
|
644
643
|
device_type=device_type,
|
|
645
644
|
location=locations[0],
|
|
646
645
|
)
|
|
647
646
|
Device.objects.create(
|
|
648
647
|
name="device-location-2",
|
|
649
|
-
status=
|
|
648
|
+
status=statuses[0],
|
|
650
649
|
role=device_role,
|
|
651
650
|
device_type=device_type,
|
|
652
651
|
location=locations[1],
|
|
653
652
|
)
|
|
654
653
|
Device.objects.create(
|
|
655
654
|
name="device-location-3",
|
|
656
|
-
status=
|
|
655
|
+
status=statuses[1],
|
|
657
656
|
role=device_role,
|
|
658
657
|
device_type=device_type,
|
|
659
658
|
location=locations[2],
|
|
@@ -661,22 +660,19 @@ class DynamicGroupTestMixin:
|
|
|
661
660
|
|
|
662
661
|
# Then the DynamicGroups.
|
|
663
662
|
cls.content_type = ContentType.objects.get_for_model(Device)
|
|
664
|
-
cls.groups = [
|
|
663
|
+
cls.groups = cls.groups = [
|
|
665
664
|
DynamicGroup.objects.create(
|
|
666
665
|
name="API DynamicGroup 1",
|
|
667
|
-
slug="api-dynamicgroup-1",
|
|
668
666
|
content_type=cls.content_type,
|
|
669
|
-
filter={"status": [
|
|
667
|
+
filter={"status": [statuses[0].name]},
|
|
670
668
|
),
|
|
671
669
|
DynamicGroup.objects.create(
|
|
672
670
|
name="API DynamicGroup 2",
|
|
673
|
-
slug="api-dynamicgroup-2",
|
|
674
671
|
content_type=cls.content_type,
|
|
675
|
-
filter={"status": [
|
|
672
|
+
filter={"status": [statuses[0].name]},
|
|
676
673
|
),
|
|
677
674
|
DynamicGroup.objects.create(
|
|
678
675
|
name="API DynamicGroup 3",
|
|
679
|
-
slug="api-dynamicgroup-3",
|
|
680
676
|
content_type=cls.content_type,
|
|
681
677
|
filter={"location": [f"{locations[2].slug}"]},
|
|
682
678
|
),
|
|
@@ -685,24 +681,20 @@ class DynamicGroupTestMixin:
|
|
|
685
681
|
|
|
686
682
|
class DynamicGroupTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
|
|
687
683
|
model = DynamicGroup
|
|
688
|
-
brief_fields = ["content_type", "display", "id", "name", "slug", "url"]
|
|
689
684
|
choices_fields = ["content_type"]
|
|
690
685
|
create_data = [
|
|
691
686
|
{
|
|
692
687
|
"name": "API DynamicGroup 4",
|
|
693
|
-
"slug": "api-dynamicgroup-4",
|
|
694
688
|
"content_type": "dcim.device",
|
|
695
689
|
"filter": {"location": ["location-1"]},
|
|
696
690
|
},
|
|
697
691
|
{
|
|
698
692
|
"name": "API DynamicGroup 5",
|
|
699
|
-
"slug": "api-dynamicgroup-5",
|
|
700
693
|
"content_type": "dcim.device",
|
|
701
694
|
"filter": {"has_interfaces": False},
|
|
702
695
|
},
|
|
703
696
|
{
|
|
704
697
|
"name": "API DynamicGroup 6",
|
|
705
|
-
"slug": "api-dynamicgroup-6",
|
|
706
698
|
"content_type": "dcim.device",
|
|
707
699
|
"filter": {"location": ["location-2"]},
|
|
708
700
|
},
|
|
@@ -721,7 +713,6 @@ class DynamicGroupTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
|
|
|
721
713
|
|
|
722
714
|
class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIViewTestCase):
|
|
723
715
|
model = DynamicGroupMembership
|
|
724
|
-
brief_fields = ["display", "group", "id", "operator", "parent_group", "url", "weight"]
|
|
725
716
|
choices_fields = ["operator"]
|
|
726
717
|
|
|
727
718
|
@classmethod
|
|
@@ -730,13 +721,11 @@ class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIView
|
|
|
730
721
|
|
|
731
722
|
parent = DynamicGroup.objects.create(
|
|
732
723
|
name="parent",
|
|
733
|
-
slug="parent",
|
|
734
724
|
content_type=cls.content_type,
|
|
735
725
|
filter={},
|
|
736
726
|
)
|
|
737
727
|
parent2 = DynamicGroup.objects.create(
|
|
738
728
|
name="parent2",
|
|
739
|
-
slug="parent2",
|
|
740
729
|
content_type=cls.content_type,
|
|
741
730
|
filter={},
|
|
742
731
|
)
|
|
@@ -782,10 +771,18 @@ class DynamicGroupMembershipTest(DynamicGroupTestMixin, APIViewTestCases.APIView
|
|
|
782
771
|
},
|
|
783
772
|
]
|
|
784
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
|
+
|
|
785
783
|
|
|
786
784
|
class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
|
787
785
|
model = ExportTemplate
|
|
788
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
789
786
|
create_data = [
|
|
790
787
|
{
|
|
791
788
|
"content_type": "dcim.device",
|
|
@@ -831,54 +828,55 @@ class ExportTemplateTest(APIViewTestCases.APIViewTestCase):
|
|
|
831
828
|
|
|
832
829
|
class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
833
830
|
model = GitRepository
|
|
834
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
835
831
|
bulk_update_data = {
|
|
836
832
|
"branch": "develop",
|
|
837
833
|
}
|
|
838
834
|
choices_fields = ["provided_contents"]
|
|
839
835
|
slug_source = "name"
|
|
836
|
+
slugify_function = staticmethod(slugify_dashes_to_underscores)
|
|
840
837
|
|
|
841
838
|
@classmethod
|
|
842
839
|
def setUpTestData(cls):
|
|
843
840
|
secrets_groups = (
|
|
844
|
-
SecretsGroup.objects.create(name="Secrets Group 1"
|
|
845
|
-
SecretsGroup.objects.create(name="Secrets Group 2"
|
|
841
|
+
SecretsGroup.objects.create(name="Secrets Group 1"),
|
|
842
|
+
SecretsGroup.objects.create(name="Secrets Group 2"),
|
|
846
843
|
)
|
|
847
844
|
|
|
848
845
|
cls.repos = (
|
|
849
846
|
GitRepository(
|
|
850
847
|
name="Repo 1",
|
|
851
|
-
slug="
|
|
848
|
+
slug="repo_1",
|
|
852
849
|
remote_url="https://example.com/repo1.git",
|
|
853
850
|
secrets_group=secrets_groups[0],
|
|
854
851
|
),
|
|
855
852
|
GitRepository(
|
|
856
853
|
name="Repo 2",
|
|
857
|
-
slug="
|
|
854
|
+
slug="repo_2",
|
|
858
855
|
remote_url="https://example.com/repo2.git",
|
|
859
856
|
secrets_group=secrets_groups[0],
|
|
860
857
|
),
|
|
861
|
-
GitRepository(name="Repo 3", slug="
|
|
858
|
+
GitRepository(name="Repo 3", slug="repo_3", remote_url="https://example.com/repo3.git"),
|
|
862
859
|
)
|
|
863
860
|
for repo in cls.repos:
|
|
864
|
-
repo.save(
|
|
861
|
+
repo.save()
|
|
865
862
|
|
|
866
863
|
cls.create_data = [
|
|
867
864
|
{
|
|
868
865
|
"name": "New Git Repository 1",
|
|
869
|
-
"slug": "
|
|
866
|
+
"slug": "new_git_repository_1",
|
|
870
867
|
"remote_url": "https://example.com/newrepo1.git",
|
|
871
868
|
"secrets_group": secrets_groups[1].pk,
|
|
869
|
+
"provided_contents": ["extras.configcontext", "extras.exporttemplate"],
|
|
872
870
|
},
|
|
873
871
|
{
|
|
874
872
|
"name": "New Git Repository 2",
|
|
875
|
-
"slug": "
|
|
873
|
+
"slug": "new_git_repository_2",
|
|
876
874
|
"remote_url": "https://example.com/newrepo2.git",
|
|
877
875
|
"secrets_group": secrets_groups[1].pk,
|
|
878
876
|
},
|
|
879
877
|
{
|
|
880
878
|
"name": "New Git Repository 3",
|
|
881
|
-
"slug": "
|
|
879
|
+
"slug": "new_git_repository_3",
|
|
882
880
|
"remote_url": "https://example.com/newrepo3.git",
|
|
883
881
|
"secrets_group": secrets_groups[1].pk,
|
|
884
882
|
},
|
|
@@ -889,6 +887,13 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
889
887
|
},
|
|
890
888
|
]
|
|
891
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
|
+
|
|
892
897
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
893
898
|
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
894
899
|
def test_run_git_sync_no_celery_worker(self, mock_get_worker_count):
|
|
@@ -924,10 +929,9 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
924
929
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
925
930
|
|
|
926
931
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
927
|
-
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
928
|
-
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, _):
|
|
929
934
|
"""Git sync request can be submitted successfully."""
|
|
930
|
-
mock_get_worker_count.return_value = 1
|
|
931
935
|
self.add_permissions("extras.add_gitrepository")
|
|
932
936
|
self.add_permissions("extras.change_gitrepository")
|
|
933
937
|
url = reverse("extras-api:gitrepository-sync", kwargs={"pk": self.repos[0].id})
|
|
@@ -941,7 +945,7 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
941
945
|
url = self._get_list_url()
|
|
942
946
|
data = {
|
|
943
947
|
"name": "plugin_test",
|
|
944
|
-
"slug": "
|
|
948
|
+
"slug": "plugin_test",
|
|
945
949
|
"remote_url": "https://localhost/plugin-test",
|
|
946
950
|
"provided_contents": ["example_plugin.textfile"],
|
|
947
951
|
}
|
|
@@ -952,17 +956,13 @@ class GitRepositoryTest(APIViewTestCases.APIViewTestCase):
|
|
|
952
956
|
|
|
953
957
|
class GraphQLQueryTest(APIViewTestCases.APIViewTestCase):
|
|
954
958
|
model = GraphQLQuery
|
|
955
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
956
|
-
|
|
957
959
|
create_data = [
|
|
958
960
|
{
|
|
959
961
|
"name": "graphql-query-4",
|
|
960
|
-
"slug": "graphql-query-4",
|
|
961
962
|
"query": "{ query: locations {name} }",
|
|
962
963
|
},
|
|
963
964
|
{
|
|
964
965
|
"name": "graphql-query-5",
|
|
965
|
-
"slug": "graphql-query-5",
|
|
966
966
|
"query": '{ devices(role: "edge") { id, name, role { name slug } } }',
|
|
967
967
|
},
|
|
968
968
|
{
|
|
@@ -970,24 +970,20 @@ class GraphQLQueryTest(APIViewTestCases.APIViewTestCase):
|
|
|
970
970
|
"query": '{ devices(role: "edge") { id, name, role { name slug } } }',
|
|
971
971
|
},
|
|
972
972
|
]
|
|
973
|
-
slug_source = "name"
|
|
974
973
|
|
|
975
974
|
@classmethod
|
|
976
975
|
def setUpTestData(cls):
|
|
977
976
|
cls.graphqlqueries = (
|
|
978
977
|
GraphQLQuery(
|
|
979
978
|
name="graphql-query-1",
|
|
980
|
-
slug="graphql-query-1",
|
|
981
979
|
query="{ locations {name} }",
|
|
982
980
|
),
|
|
983
981
|
GraphQLQuery(
|
|
984
982
|
name="graphql-query-2",
|
|
985
|
-
slug="graphql-query-2",
|
|
986
983
|
query='{ devices(role: "edge") { id, name, role { name slug } } }',
|
|
987
984
|
),
|
|
988
985
|
GraphQLQuery(
|
|
989
986
|
name="graphql-query-3",
|
|
990
|
-
slug="graphql-query-3",
|
|
991
987
|
query="""
|
|
992
988
|
query ($device: [String!]) {
|
|
993
989
|
devices(name: $device) {
|
|
@@ -1014,7 +1010,6 @@ query ($device: [String!]) {
|
|
|
1014
1010
|
}
|
|
1015
1011
|
platform {
|
|
1016
1012
|
name
|
|
1017
|
-
slug
|
|
1018
1013
|
manufacturer {
|
|
1019
1014
|
name
|
|
1020
1015
|
}
|
|
@@ -1108,7 +1103,6 @@ class ImageAttachmentTest(
|
|
|
1108
1103
|
APIViewTestCases.DeleteObjectViewTestCase,
|
|
1109
1104
|
):
|
|
1110
1105
|
model = ImageAttachment
|
|
1111
|
-
brief_fields = ["display", "id", "image", "name", "url"]
|
|
1112
1106
|
choices_fields = ["content_type"]
|
|
1113
1107
|
|
|
1114
1108
|
@classmethod
|
|
@@ -1142,6 +1136,15 @@ class ImageAttachmentTest(
|
|
|
1142
1136
|
image_width=100,
|
|
1143
1137
|
)
|
|
1144
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
|
+
|
|
1145
1148
|
|
|
1146
1149
|
class JobTest(
|
|
1147
1150
|
# note no CreateObjectViewTestCase - we do not support user creation of Job records
|
|
@@ -1153,7 +1156,6 @@ class JobTest(
|
|
|
1153
1156
|
"""Test cases for the Jobs REST API."""
|
|
1154
1157
|
|
|
1155
1158
|
model = Job
|
|
1156
|
-
brief_fields = ["display", "grouping", "id", "job_class_name", "module_name", "name", "slug", "source", "url"]
|
|
1157
1159
|
choices_fields = None
|
|
1158
1160
|
update_data = {
|
|
1159
1161
|
# source, module_name, job_class_name, installed are NOT editable
|
|
@@ -1161,18 +1163,15 @@ class JobTest(
|
|
|
1161
1163
|
"grouping": "Overridden grouping",
|
|
1162
1164
|
"name_override": True,
|
|
1163
1165
|
"name": "Overridden name",
|
|
1164
|
-
"slug": "overridden-slug",
|
|
1165
1166
|
"description_override": True,
|
|
1166
1167
|
"description": "This is an overridden description.",
|
|
1167
1168
|
"enabled": True,
|
|
1168
1169
|
"approval_required_override": True,
|
|
1169
1170
|
"approval_required": True,
|
|
1170
|
-
"
|
|
1171
|
-
"
|
|
1171
|
+
"dryrun_default_override": True,
|
|
1172
|
+
"dryrun_default": True,
|
|
1172
1173
|
"hidden_override": True,
|
|
1173
1174
|
"hidden": True,
|
|
1174
|
-
"read_only_override": True,
|
|
1175
|
-
"read_only": True,
|
|
1176
1175
|
"soft_time_limit_override": True,
|
|
1177
1176
|
"soft_time_limit": 350.1,
|
|
1178
1177
|
"time_limit_override": True,
|
|
@@ -1192,14 +1191,16 @@ class JobTest(
|
|
|
1192
1191
|
|
|
1193
1192
|
def setUp(self):
|
|
1194
1193
|
super().setUp()
|
|
1195
|
-
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)
|
|
1196
1197
|
self.job_model = Job.objects.get_for_class_path(self.default_job_name)
|
|
1197
1198
|
self.job_model.enabled = True
|
|
1198
1199
|
self.job_model.validated_save()
|
|
1199
1200
|
|
|
1200
1201
|
run_success_response_status = status.HTTP_201_CREATED
|
|
1201
1202
|
|
|
1202
|
-
def get_run_url(self, class_path="
|
|
1203
|
+
def get_run_url(self, class_path="api_test_job.APITestJob"):
|
|
1203
1204
|
job_model = Job.objects.get_for_class_path(class_path)
|
|
1204
1205
|
return reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
|
|
1205
1206
|
|
|
@@ -1219,7 +1220,7 @@ class JobTest(
|
|
|
1219
1220
|
|
|
1220
1221
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1221
1222
|
def test_update_job_with_sensitive_variables_set_approval_required_to_true(self):
|
|
1222
|
-
job_model = Job.objects.get_for_class_path("
|
|
1223
|
+
job_model = Job.objects.get_for_class_path("api_test_job.APITestJob")
|
|
1223
1224
|
job_model.has_sensitive_variables = True
|
|
1224
1225
|
job_model.has_sensitive_variables_override = True
|
|
1225
1226
|
job_model.validated_save()
|
|
@@ -1241,7 +1242,7 @@ class JobTest(
|
|
|
1241
1242
|
|
|
1242
1243
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1243
1244
|
def test_update_approval_required_job_set_has_sensitive_variables_to_true(self):
|
|
1244
|
-
job_model = Job.objects.get_for_class_path("
|
|
1245
|
+
job_model = Job.objects.get_for_class_path("api_test_job.APITestJob")
|
|
1245
1246
|
job_model.approval_required = True
|
|
1246
1247
|
job_model.approval_required_override = True
|
|
1247
1248
|
job_model.validated_save()
|
|
@@ -1286,7 +1287,7 @@ class JobTest(
|
|
|
1286
1287
|
mock_get_worker_count.return_value = 1
|
|
1287
1288
|
obj_perm = ObjectPermission(
|
|
1288
1289
|
name="Test permission",
|
|
1289
|
-
constraints={"module_name__in": ["
|
|
1290
|
+
constraints={"module_name__in": ["pass", "fail"]},
|
|
1290
1291
|
actions=["run"],
|
|
1291
1292
|
)
|
|
1292
1293
|
obj_perm.save()
|
|
@@ -1300,10 +1301,10 @@ class JobTest(
|
|
|
1300
1301
|
self.assertHttpStatus(response, status.HTTP_404_NOT_FOUND)
|
|
1301
1302
|
|
|
1302
1303
|
# Try post to permitted job
|
|
1303
|
-
job_model = Job.objects.get_for_class_path("
|
|
1304
|
+
job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
1304
1305
|
job_model.enabled = True
|
|
1305
1306
|
job_model.validated_save()
|
|
1306
|
-
url = self.get_run_url("
|
|
1307
|
+
url = self.get_run_url("pass.TestPass")
|
|
1307
1308
|
response = self.client.post(url, **self.header)
|
|
1308
1309
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1309
1310
|
|
|
@@ -1331,7 +1332,6 @@ class JobTest(
|
|
|
1331
1332
|
self.add_permissions("extras.run_job")
|
|
1332
1333
|
|
|
1333
1334
|
job_model = Job(
|
|
1334
|
-
source="local",
|
|
1335
1335
|
module_name="uninstalled_module",
|
|
1336
1336
|
job_class_name="NoSuchJob",
|
|
1337
1337
|
grouping="Uninstalled Module",
|
|
@@ -1341,7 +1341,7 @@ class JobTest(
|
|
|
1341
1341
|
)
|
|
1342
1342
|
job_model.validated_save()
|
|
1343
1343
|
|
|
1344
|
-
url = self.get_run_url("
|
|
1344
|
+
url = self.get_run_url("uninstalled_module.NoSuchJob")
|
|
1345
1345
|
with disable_warnings("django.request"):
|
|
1346
1346
|
response = self.client.post(url, {}, format="json", **self.header)
|
|
1347
1347
|
self.assertHttpStatus(response, status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
@@ -1362,7 +1362,6 @@ class JobTest(
|
|
|
1362
1362
|
|
|
1363
1363
|
data = {
|
|
1364
1364
|
"data": job_data,
|
|
1365
|
-
"commit": True,
|
|
1366
1365
|
}
|
|
1367
1366
|
|
|
1368
1367
|
url = self.get_run_url()
|
|
@@ -1388,7 +1387,6 @@ class JobTest(
|
|
|
1388
1387
|
|
|
1389
1388
|
data = {
|
|
1390
1389
|
"data": job_data,
|
|
1391
|
-
"commit": True,
|
|
1392
1390
|
"schedule": {
|
|
1393
1391
|
"name": "test",
|
|
1394
1392
|
"interval": "future",
|
|
@@ -1401,17 +1399,19 @@ class JobTest(
|
|
|
1401
1399
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1402
1400
|
|
|
1403
1401
|
schedule = ScheduledJob.objects.last()
|
|
1404
|
-
self.assertEqual(schedule.kwargs["
|
|
1402
|
+
self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
|
|
1405
1403
|
|
|
1406
1404
|
self.assertIn("scheduled_job", response.data)
|
|
1407
1405
|
self.assertIn("job_result", response.data)
|
|
1408
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"
|
|
1409
1411
|
self.assertEqual(
|
|
1410
|
-
response.data["scheduled_job"]["
|
|
1411
|
-
|
|
1412
|
+
datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
|
|
1413
|
+
schedule.start_time,
|
|
1412
1414
|
)
|
|
1413
|
-
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1414
|
-
self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
|
|
1415
1415
|
self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
|
|
1416
1416
|
self.assertIsNone(response.data["job_result"])
|
|
1417
1417
|
|
|
@@ -1440,7 +1440,6 @@ class JobTest(
|
|
|
1440
1440
|
|
|
1441
1441
|
data = {
|
|
1442
1442
|
"data": job_data,
|
|
1443
|
-
"commit": True,
|
|
1444
1443
|
# schedule is omitted
|
|
1445
1444
|
}
|
|
1446
1445
|
|
|
@@ -1456,13 +1455,15 @@ class JobTest(
|
|
|
1456
1455
|
self.assertIsNotNone(schedule)
|
|
1457
1456
|
self.assertEqual(schedule.interval, JobExecutionType.TYPE_IMMEDIATELY)
|
|
1458
1457
|
self.assertEqual(schedule.approval_required, self.job_model.approval_required)
|
|
1459
|
-
self.assertEqual(schedule.kwargs["
|
|
1458
|
+
self.assertEqual(schedule.kwargs["var4"], str(device_role.pk))
|
|
1460
1459
|
|
|
1461
1460
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1462
1461
|
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
1463
|
-
|
|
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):
|
|
1464
1464
|
"""Job run requests can reference objects by their attributes."""
|
|
1465
1465
|
mock_get_worker_count.return_value = 1
|
|
1466
|
+
mock_enqueue_job.return_value = None
|
|
1466
1467
|
self.add_permissions("extras.run_job")
|
|
1467
1468
|
device_role = Role.objects.get_for_model(Device).first()
|
|
1468
1469
|
job_data = {
|
|
@@ -1474,7 +1475,7 @@ class JobTest(
|
|
|
1474
1475
|
|
|
1475
1476
|
# This handles things like ObjectVar fields looked up by non-UUID
|
|
1476
1477
|
# Jobs are executed with deserialized data
|
|
1477
|
-
deserialized_data =
|
|
1478
|
+
deserialized_data = self.job_class.deserialize_data(job_data)
|
|
1478
1479
|
|
|
1479
1480
|
self.assertEqual(
|
|
1480
1481
|
deserialized_data,
|
|
@@ -1485,24 +1486,39 @@ class JobTest(
|
|
|
1485
1486
|
response = self.client.post(url, {"data": job_data}, format="json", **self.header)
|
|
1486
1487
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1487
1488
|
|
|
1488
|
-
|
|
1489
|
-
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)
|
|
1490
1496
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
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)
|
|
1495
1516
|
|
|
1496
1517
|
self.assertIn("scheduled_job", response.data)
|
|
1497
1518
|
self.assertIn("job_result", response.data)
|
|
1498
1519
|
self.assertIsNone(response.data["scheduled_job"])
|
|
1499
|
-
# The urls in a NestedJobResultSerializer depends on the request context, which we don't have
|
|
1500
1520
|
data_job_result = response.data["job_result"]
|
|
1501
|
-
|
|
1502
|
-
del data_job_result["user"]["url"]
|
|
1503
|
-
expected_data_job_result = NestedJobResultSerializer(job_result, context={"request": None}).data
|
|
1504
|
-
del expected_data_job_result["url"]
|
|
1505
|
-
del expected_data_job_result["user"]["url"]
|
|
1521
|
+
expected_data_job_result = JobResultSerializer(job_result, context={"request": response.wsgi_request}).data
|
|
1506
1522
|
self.assertEqual(data_job_result, expected_data_job_result)
|
|
1507
1523
|
|
|
1508
1524
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
@@ -1512,7 +1528,7 @@ class JobTest(
|
|
|
1512
1528
|
|
|
1513
1529
|
test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
|
|
1514
1530
|
|
|
1515
|
-
job_model = Job.objects.get_for_class_path("
|
|
1531
|
+
job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
|
|
1516
1532
|
job_model.enabled = True
|
|
1517
1533
|
job_model.validated_save()
|
|
1518
1534
|
|
|
@@ -1523,10 +1539,9 @@ class JobTest(
|
|
|
1523
1539
|
"var2": "Ground control to Major Tom",
|
|
1524
1540
|
"var23": "Commencing countdown, engines on",
|
|
1525
1541
|
"var1": test_file,
|
|
1526
|
-
"_commit": True,
|
|
1527
1542
|
}
|
|
1528
1543
|
|
|
1529
|
-
url = self.get_run_url(class_path="
|
|
1544
|
+
url = self.get_run_url(class_path="field_order.TestFieldOrder")
|
|
1530
1545
|
response = self.client.post(url, data=job_data, **self.header)
|
|
1531
1546
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1532
1547
|
|
|
@@ -1537,7 +1552,7 @@ class JobTest(
|
|
|
1537
1552
|
|
|
1538
1553
|
test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
|
|
1539
1554
|
|
|
1540
|
-
job_model = Job.objects.get_for_class_path("
|
|
1555
|
+
job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
|
|
1541
1556
|
job_model.enabled = True
|
|
1542
1557
|
job_model.validated_save()
|
|
1543
1558
|
|
|
@@ -1550,7 +1565,7 @@ class JobTest(
|
|
|
1550
1565
|
"var1": test_file,
|
|
1551
1566
|
}
|
|
1552
1567
|
|
|
1553
|
-
url = self.get_run_url(class_path="
|
|
1568
|
+
url = self.get_run_url(class_path="field_order.TestFieldOrder")
|
|
1554
1569
|
response = self.client.post(url, data=job_data, **self.header)
|
|
1555
1570
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1556
1571
|
|
|
@@ -1561,7 +1576,7 @@ class JobTest(
|
|
|
1561
1576
|
|
|
1562
1577
|
test_file = SimpleUploadedFile(name="test_file.txt", content=b"I am content.\n")
|
|
1563
1578
|
|
|
1564
|
-
job_model = Job.objects.get_for_class_path("
|
|
1579
|
+
job_model = Job.objects.get_for_class_path("field_order.TestFieldOrder")
|
|
1565
1580
|
job_model.enabled = True
|
|
1566
1581
|
job_model.validated_save()
|
|
1567
1582
|
|
|
@@ -1572,13 +1587,12 @@ class JobTest(
|
|
|
1572
1587
|
"var2": "Ground control to Major Tom",
|
|
1573
1588
|
"var23": "Commencing countdown, engines on",
|
|
1574
1589
|
"var1": test_file,
|
|
1575
|
-
"_commit": True,
|
|
1576
1590
|
"_schedule_start_time": str(datetime.now() + timedelta(minutes=1)),
|
|
1577
1591
|
"_schedule_interval": "future",
|
|
1578
1592
|
"_schedule_name": "test",
|
|
1579
1593
|
}
|
|
1580
1594
|
|
|
1581
|
-
url = self.get_run_url(class_path="
|
|
1595
|
+
url = self.get_run_url(class_path="field_order.TestFieldOrder")
|
|
1582
1596
|
response = self.client.post(url, data=job_data, **self.header)
|
|
1583
1597
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1584
1598
|
|
|
@@ -1591,7 +1605,6 @@ class JobTest(
|
|
|
1591
1605
|
d = Role.objects.get_for_model(Device).first()
|
|
1592
1606
|
data = {
|
|
1593
1607
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1594
|
-
"commit": True,
|
|
1595
1608
|
"schedule": {
|
|
1596
1609
|
"start_time": str(datetime.now() + timedelta(minutes=1)),
|
|
1597
1610
|
"interval": "future",
|
|
@@ -1604,17 +1617,17 @@ class JobTest(
|
|
|
1604
1617
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1605
1618
|
|
|
1606
1619
|
schedule = ScheduledJob.objects.last()
|
|
1607
|
-
self.assertEqual(schedule.kwargs["scheduled_job_pk"], str(schedule.pk))
|
|
1608
|
-
|
|
1609
1620
|
self.assertIn("scheduled_job", response.data)
|
|
1610
1621
|
self.assertIn("job_result", response.data)
|
|
1611
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"
|
|
1612
1627
|
self.assertEqual(
|
|
1613
|
-
response.data["scheduled_job"]["
|
|
1614
|
-
|
|
1628
|
+
datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
|
|
1629
|
+
schedule.start_time,
|
|
1615
1630
|
)
|
|
1616
|
-
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1617
|
-
self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
|
|
1618
1631
|
self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
|
|
1619
1632
|
self.assertIsNone(response.data["job_result"])
|
|
1620
1633
|
|
|
@@ -1631,7 +1644,6 @@ class JobTest(
|
|
|
1631
1644
|
url = reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
|
|
1632
1645
|
data = {
|
|
1633
1646
|
"data": {},
|
|
1634
|
-
"commit": True,
|
|
1635
1647
|
"schedule": {
|
|
1636
1648
|
"start_time": str(datetime.now() + timedelta(minutes=1)),
|
|
1637
1649
|
"interval": "future",
|
|
@@ -1662,7 +1674,6 @@ class JobTest(
|
|
|
1662
1674
|
url = reverse("extras-api:job-run", kwargs={"pk": job_model.pk})
|
|
1663
1675
|
data = {
|
|
1664
1676
|
"data": {},
|
|
1665
|
-
"commit": True,
|
|
1666
1677
|
"schedule": {
|
|
1667
1678
|
"interval": "immediately",
|
|
1668
1679
|
"name": "test",
|
|
@@ -1679,30 +1690,27 @@ class JobTest(
|
|
|
1679
1690
|
)
|
|
1680
1691
|
|
|
1681
1692
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1682
|
-
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
1683
|
-
def test_run_a_job_with_sensitive_variables_immediately(self,
|
|
1684
|
-
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, _):
|
|
1685
1695
|
self.add_permissions("extras.run_job")
|
|
1686
1696
|
d = Role.objects.get_for_model(Device).first()
|
|
1687
1697
|
data = {
|
|
1688
1698
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1689
|
-
"commit": True,
|
|
1690
1699
|
"schedule": {
|
|
1691
1700
|
"interval": "immediately",
|
|
1692
1701
|
"name": "test",
|
|
1693
1702
|
},
|
|
1694
1703
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
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()
|
|
1699
1707
|
|
|
1700
1708
|
url = self.get_run_url()
|
|
1701
1709
|
response = self.client.post(url, data, format="json", **self.header)
|
|
1702
1710
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1703
1711
|
|
|
1704
|
-
job_result = JobResult.objects.get(name=self.
|
|
1705
|
-
self.assertEqual(job_result.task_kwargs,
|
|
1712
|
+
job_result = JobResult.objects.get(name=self.job_model.name)
|
|
1713
|
+
self.assertEqual(job_result.task_kwargs, {})
|
|
1706
1714
|
|
|
1707
1715
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
1708
1716
|
@mock.patch("nautobot.extras.api.views.get_worker_count")
|
|
@@ -1712,7 +1720,6 @@ class JobTest(
|
|
|
1712
1720
|
d = Role.objects.get_for_model(Device).first()
|
|
1713
1721
|
data = {
|
|
1714
1722
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1715
|
-
"commit": True,
|
|
1716
1723
|
"schedule": {
|
|
1717
1724
|
"start_time": str(datetime.now() - timedelta(minutes=1)),
|
|
1718
1725
|
"interval": "future",
|
|
@@ -1732,7 +1739,6 @@ class JobTest(
|
|
|
1732
1739
|
d = Role.objects.get_for_model(Device).first()
|
|
1733
1740
|
data = {
|
|
1734
1741
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1735
|
-
"commit": True,
|
|
1736
1742
|
"schedule": {
|
|
1737
1743
|
"start_time": str(datetime.now() + timedelta(minutes=1)),
|
|
1738
1744
|
"interval": "hourly",
|
|
@@ -1749,12 +1755,14 @@ class JobTest(
|
|
|
1749
1755
|
self.assertIn("scheduled_job", response.data)
|
|
1750
1756
|
self.assertIn("job_result", response.data)
|
|
1751
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"
|
|
1752
1762
|
self.assertEqual(
|
|
1753
|
-
response.data["scheduled_job"]["
|
|
1754
|
-
|
|
1763
|
+
datetime.fromisoformat(response.data["scheduled_job"]["start_time"].replace("Z", "+00:00")),
|
|
1764
|
+
schedule.start_time,
|
|
1755
1765
|
)
|
|
1756
|
-
self.assertEqual(response.data["scheduled_job"]["name"], schedule.name)
|
|
1757
|
-
self.assertEqual(response.data["scheduled_job"]["start_time"], schedule.start_time)
|
|
1758
1766
|
self.assertEqual(response.data["scheduled_job"]["interval"], schedule.interval)
|
|
1759
1767
|
self.assertIsNone(response.data["job_result"])
|
|
1760
1768
|
|
|
@@ -1764,7 +1772,6 @@ class JobTest(
|
|
|
1764
1772
|
|
|
1765
1773
|
data = {
|
|
1766
1774
|
"data": "invalid",
|
|
1767
|
-
"commit": True,
|
|
1768
1775
|
}
|
|
1769
1776
|
|
|
1770
1777
|
url = self.get_run_url()
|
|
@@ -1784,7 +1791,6 @@ class JobTest(
|
|
|
1784
1791
|
|
|
1785
1792
|
data = {
|
|
1786
1793
|
"data": job_data,
|
|
1787
|
-
"commit": True,
|
|
1788
1794
|
}
|
|
1789
1795
|
|
|
1790
1796
|
url = self.get_run_url()
|
|
@@ -1803,7 +1809,6 @@ class JobTest(
|
|
|
1803
1809
|
|
|
1804
1810
|
data = {
|
|
1805
1811
|
"data": job_data,
|
|
1806
|
-
"commit": True,
|
|
1807
1812
|
}
|
|
1808
1813
|
|
|
1809
1814
|
url = self.get_run_url()
|
|
@@ -1819,7 +1824,6 @@ class JobTest(
|
|
|
1819
1824
|
d = Role.objects.get_for_model(Device).first()
|
|
1820
1825
|
data = {
|
|
1821
1826
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1822
|
-
"commit": True,
|
|
1823
1827
|
"task_queue": "invalid",
|
|
1824
1828
|
}
|
|
1825
1829
|
|
|
@@ -1838,7 +1842,6 @@ class JobTest(
|
|
|
1838
1842
|
d = Role.objects.get_for_model(Device).first()
|
|
1839
1843
|
data = {
|
|
1840
1844
|
"data": {"var1": "x", "var2": 1, "var3": False, "var4": d.pk},
|
|
1841
|
-
"commit": True,
|
|
1842
1845
|
"task_queue": settings.CELERY_TASK_DEFAULT_QUEUE,
|
|
1843
1846
|
}
|
|
1844
1847
|
|
|
@@ -1851,21 +1854,28 @@ class JobTest(
|
|
|
1851
1854
|
def test_run_job_with_default_queue_with_empty_job_model_task_queues(self, _):
|
|
1852
1855
|
self.add_permissions("extras.run_job")
|
|
1853
1856
|
data = {
|
|
1854
|
-
"commit": True,
|
|
1855
1857
|
"task_queue": settings.CELERY_TASK_DEFAULT_QUEUE,
|
|
1856
1858
|
}
|
|
1857
1859
|
|
|
1858
|
-
job_model = Job.objects.get_for_class_path("
|
|
1860
|
+
job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
1859
1861
|
job_model.enabled = True
|
|
1860
1862
|
job_model.validated_save()
|
|
1861
|
-
url = self.get_run_url("
|
|
1863
|
+
url = self.get_run_url("pass.TestPass")
|
|
1862
1864
|
response = self.client.post(url, data, format="json", **self.header)
|
|
1863
1865
|
self.assertHttpStatus(response, self.run_success_response_status)
|
|
1864
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
|
+
|
|
1865
1876
|
|
|
1866
1877
|
class JobHookTest(APIViewTestCases.APIViewTestCase):
|
|
1867
1878
|
model = JobHook
|
|
1868
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
1869
1879
|
choices_fields = []
|
|
1870
1880
|
update_data = {
|
|
1871
1881
|
"name": "Overridden name",
|
|
@@ -1972,7 +1982,6 @@ class JobHookTest(APIViewTestCases.APIViewTestCase):
|
|
|
1972
1982
|
|
|
1973
1983
|
class JobButtonTest(APIViewTestCases.APIViewTestCase):
|
|
1974
1984
|
model = JobButton
|
|
1975
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
1976
1985
|
choices_fields = ["button_class"]
|
|
1977
1986
|
|
|
1978
1987
|
@classmethod
|
|
@@ -2031,49 +2040,37 @@ class JobResultTest(
|
|
|
2031
2040
|
APIViewTestCases.DeleteObjectViewTestCase,
|
|
2032
2041
|
):
|
|
2033
2042
|
model = JobResult
|
|
2034
|
-
brief_fields = ["date_created", "date_done", "display", "id", "name", "status", "url", "user"]
|
|
2035
2043
|
|
|
2036
2044
|
@classmethod
|
|
2037
2045
|
def setUpTestData(cls):
|
|
2038
2046
|
jobs = Job.objects.all()[:2]
|
|
2039
|
-
job_ct = ContentType.objects.get_for_model(Job)
|
|
2040
|
-
git_ct = ContentType.objects.get_for_model(GitRepository)
|
|
2041
2047
|
|
|
2042
2048
|
JobResult.objects.create(
|
|
2043
2049
|
job_model=jobs[0],
|
|
2044
2050
|
name=jobs[0].class_path,
|
|
2045
|
-
|
|
2046
|
-
date_done=datetime.now(),
|
|
2051
|
+
date_done=now(),
|
|
2047
2052
|
user=None,
|
|
2048
2053
|
status=JobResultStatusChoices.STATUS_SUCCESS,
|
|
2049
|
-
|
|
2050
|
-
task_kwargs=None,
|
|
2054
|
+
task_kwargs={},
|
|
2051
2055
|
scheduled_job=None,
|
|
2052
|
-
task_id=uuid.uuid4(),
|
|
2053
2056
|
)
|
|
2054
2057
|
JobResult.objects.create(
|
|
2055
2058
|
job_model=None,
|
|
2056
|
-
name="
|
|
2057
|
-
|
|
2058
|
-
date_done=datetime.now(),
|
|
2059
|
+
name="deleted_module.deleted_job",
|
|
2060
|
+
date_done=now(),
|
|
2059
2061
|
user=None,
|
|
2060
2062
|
status=JobResultStatusChoices.STATUS_SUCCESS,
|
|
2061
|
-
data=None,
|
|
2062
2063
|
task_kwargs={"repository_pk": uuid.uuid4()},
|
|
2063
2064
|
scheduled_job=None,
|
|
2064
|
-
task_id=uuid.uuid4(),
|
|
2065
2065
|
)
|
|
2066
2066
|
JobResult.objects.create(
|
|
2067
2067
|
job_model=jobs[1],
|
|
2068
2068
|
name=jobs[1].class_path,
|
|
2069
|
-
obj_type=job_ct,
|
|
2070
2069
|
date_done=None,
|
|
2071
2070
|
user=None,
|
|
2072
2071
|
status=JobResultStatusChoices.STATUS_PENDING,
|
|
2073
|
-
data=None,
|
|
2074
2072
|
task_kwargs={"data": {"device": uuid.uuid4(), "multichoices": ["red", "green"], "checkbox": False}},
|
|
2075
2073
|
scheduled_job=None,
|
|
2076
|
-
task_id=uuid.uuid4(),
|
|
2077
2074
|
)
|
|
2078
2075
|
|
|
2079
2076
|
|
|
@@ -2082,27 +2079,11 @@ class JobLogEntryTest(
|
|
|
2082
2079
|
APIViewTestCases.ListObjectsViewTestCase,
|
|
2083
2080
|
):
|
|
2084
2081
|
model = JobLogEntry
|
|
2085
|
-
brief_fields = [
|
|
2086
|
-
"absolute_url",
|
|
2087
|
-
"created",
|
|
2088
|
-
"display",
|
|
2089
|
-
"grouping",
|
|
2090
|
-
"id",
|
|
2091
|
-
"job_result",
|
|
2092
|
-
"log_level",
|
|
2093
|
-
"log_object",
|
|
2094
|
-
"message",
|
|
2095
|
-
"url",
|
|
2096
|
-
]
|
|
2097
2082
|
choices_fields = []
|
|
2098
2083
|
|
|
2099
2084
|
@classmethod
|
|
2100
2085
|
def setUpTestData(cls):
|
|
2101
|
-
cls.job_result = JobResult.objects.create(
|
|
2102
|
-
name="test",
|
|
2103
|
-
task_id=uuid.uuid4(),
|
|
2104
|
-
obj_type=ContentType.objects.get_for_model(GitRepository),
|
|
2105
|
-
)
|
|
2086
|
+
cls.job_result = JobResult.objects.create(name="test")
|
|
2106
2087
|
|
|
2107
2088
|
for log_level in ("debug", "info", "success", "warning"):
|
|
2108
2089
|
JobLogEntry.objects.create(
|
|
@@ -2125,16 +2106,15 @@ class ScheduledJobTest(
|
|
|
2125
2106
|
APIViewTestCases.ListObjectsViewTestCase,
|
|
2126
2107
|
):
|
|
2127
2108
|
model = ScheduledJob
|
|
2128
|
-
brief_fields = ["crontab", "display", "id", "interval", "name", "start_time", "url"]
|
|
2129
2109
|
choices_fields = []
|
|
2130
2110
|
|
|
2131
2111
|
@classmethod
|
|
2132
2112
|
def setUpTestData(cls):
|
|
2133
2113
|
user = User.objects.create(username="user1", is_active=True)
|
|
2134
|
-
job_model = Job.objects.get_for_class_path("
|
|
2114
|
+
job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
2135
2115
|
ScheduledJob.objects.create(
|
|
2136
2116
|
name="test1",
|
|
2137
|
-
task="
|
|
2117
|
+
task="pass.TestPass",
|
|
2138
2118
|
job_class=job_model.class_path,
|
|
2139
2119
|
job_model=job_model,
|
|
2140
2120
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2144,7 +2124,7 @@ class ScheduledJobTest(
|
|
|
2144
2124
|
)
|
|
2145
2125
|
ScheduledJob.objects.create(
|
|
2146
2126
|
name="test2",
|
|
2147
|
-
task="
|
|
2127
|
+
task="pass.TestPass",
|
|
2148
2128
|
job_class=job_model.class_path,
|
|
2149
2129
|
job_model=job_model,
|
|
2150
2130
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2154,7 +2134,7 @@ class ScheduledJobTest(
|
|
|
2154
2134
|
)
|
|
2155
2135
|
ScheduledJob.objects.create(
|
|
2156
2136
|
name="test3",
|
|
2157
|
-
task="
|
|
2137
|
+
task="pass.TestPass",
|
|
2158
2138
|
job_class=job_model.class_path,
|
|
2159
2139
|
job_model=job_model,
|
|
2160
2140
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2163,17 +2143,26 @@ class ScheduledJobTest(
|
|
|
2163
2143
|
start_time=now(),
|
|
2164
2144
|
)
|
|
2165
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
|
+
|
|
2166
2155
|
|
|
2167
2156
|
class JobApprovalTest(APITestCase):
|
|
2168
2157
|
@classmethod
|
|
2169
2158
|
def setUpTestData(cls):
|
|
2170
2159
|
cls.additional_user = User.objects.create(username="user1", is_active=True)
|
|
2171
|
-
cls.job_model = Job.objects.get_for_class_path("
|
|
2160
|
+
cls.job_model = Job.objects.get_for_class_path("pass.TestPass")
|
|
2172
2161
|
cls.job_model.enabled = True
|
|
2173
2162
|
cls.job_model.save()
|
|
2174
2163
|
cls.scheduled_job = ScheduledJob.objects.create(
|
|
2175
2164
|
name="test",
|
|
2176
|
-
task="
|
|
2165
|
+
task="pass.TestPass",
|
|
2177
2166
|
job_class=cls.job_model.class_path,
|
|
2178
2167
|
job_model=cls.job_model,
|
|
2179
2168
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2181,6 +2170,19 @@ class JobApprovalTest(APITestCase):
|
|
|
2181
2170
|
approval_required=True,
|
|
2182
2171
|
start_time=now(),
|
|
2183
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
|
+
)
|
|
2184
2186
|
|
|
2185
2187
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2186
2188
|
def test_approve_job_anonymous(self):
|
|
@@ -2214,7 +2216,7 @@ class JobApprovalTest(APITestCase):
|
|
|
2214
2216
|
self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
|
|
2215
2217
|
scheduled_job = ScheduledJob.objects.create(
|
|
2216
2218
|
name="test",
|
|
2217
|
-
task="
|
|
2219
|
+
task="pass.TestPass",
|
|
2218
2220
|
job_class=self.job_model.class_path,
|
|
2219
2221
|
job_model=self.job_model,
|
|
2220
2222
|
interval=JobExecutionType.TYPE_IMMEDIATELY,
|
|
@@ -2238,7 +2240,7 @@ class JobApprovalTest(APITestCase):
|
|
|
2238
2240
|
self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
|
|
2239
2241
|
scheduled_job = ScheduledJob.objects.create(
|
|
2240
2242
|
name="test",
|
|
2241
|
-
task="
|
|
2243
|
+
task="pass.TestPass",
|
|
2242
2244
|
job_class=self.job_model.class_path,
|
|
2243
2245
|
job_model=self.job_model,
|
|
2244
2246
|
interval=JobExecutionType.TYPE_FUTURE,
|
|
@@ -2256,7 +2258,7 @@ class JobApprovalTest(APITestCase):
|
|
|
2256
2258
|
self.add_permissions("extras.approve_job", "extras.view_scheduledjob", "extras.change_scheduledjob")
|
|
2257
2259
|
scheduled_job = ScheduledJob.objects.create(
|
|
2258
2260
|
name="test",
|
|
2259
|
-
task="
|
|
2261
|
+
task="pass.TestPass",
|
|
2260
2262
|
job_class=self.job_model.class_path,
|
|
2261
2263
|
job_model=self.job_model,
|
|
2262
2264
|
interval=JobExecutionType.TYPE_FUTURE,
|
|
@@ -2300,7 +2302,7 @@ class JobApprovalTest(APITestCase):
|
|
|
2300
2302
|
|
|
2301
2303
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=[])
|
|
2302
2304
|
def test_dry_run_job_without_permission(self):
|
|
2303
|
-
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})
|
|
2304
2306
|
with disable_warnings("django.request"):
|
|
2305
2307
|
response = self.client.post(url, **self.header)
|
|
2306
2308
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
@@ -2308,29 +2310,27 @@ class JobApprovalTest(APITestCase):
|
|
|
2308
2310
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2309
2311
|
def test_dry_run_job_without_run_job_permission(self):
|
|
2310
2312
|
self.add_permissions("extras.view_scheduledjob")
|
|
2311
|
-
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})
|
|
2312
2314
|
response = self.client.post(url, **self.header)
|
|
2313
2315
|
self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
|
|
2314
2316
|
|
|
2315
2317
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=["*"])
|
|
2316
2318
|
def test_dry_run_job(self):
|
|
2317
2319
|
self.add_permissions("extras.run_job", "extras.view_scheduledjob")
|
|
2318
|
-
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})
|
|
2319
2321
|
response = self.client.post(url, **self.header)
|
|
2320
2322
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2321
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
|
+
|
|
2322
2331
|
|
|
2323
2332
|
class NoteTest(APIViewTestCases.APIViewTestCase):
|
|
2324
2333
|
model = Note
|
|
2325
|
-
brief_fields = [
|
|
2326
|
-
"assigned_object",
|
|
2327
|
-
"display",
|
|
2328
|
-
"id",
|
|
2329
|
-
"note",
|
|
2330
|
-
"slug",
|
|
2331
|
-
"url",
|
|
2332
|
-
"user",
|
|
2333
|
-
]
|
|
2334
2334
|
choices_fields = ["assigned_object_type"]
|
|
2335
2335
|
|
|
2336
2336
|
@classmethod
|
|
@@ -2383,26 +2383,25 @@ class NoteTest(APIViewTestCases.APIViewTestCase):
|
|
|
2383
2383
|
|
|
2384
2384
|
class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTestMixin):
|
|
2385
2385
|
model = Relationship
|
|
2386
|
-
brief_fields = ["display", "id", "name", "slug", "url"]
|
|
2387
2386
|
|
|
2388
2387
|
create_data = [
|
|
2389
2388
|
{
|
|
2390
|
-
"
|
|
2391
|
-
"
|
|
2389
|
+
"label": "Device VLANs",
|
|
2390
|
+
"key": "device_vlans",
|
|
2392
2391
|
"type": "many-to-many",
|
|
2393
2392
|
"source_type": "ipam.vlan",
|
|
2394
2393
|
"destination_type": "dcim.device",
|
|
2395
2394
|
},
|
|
2396
2395
|
{
|
|
2397
|
-
"
|
|
2398
|
-
"
|
|
2396
|
+
"label": "Primary VLAN",
|
|
2397
|
+
"key": "primary_vlan",
|
|
2399
2398
|
"type": "one-to-many",
|
|
2400
2399
|
"source_type": "ipam.vlan",
|
|
2401
2400
|
"destination_type": "dcim.device",
|
|
2402
2401
|
},
|
|
2403
2402
|
{
|
|
2404
|
-
"
|
|
2405
|
-
"
|
|
2403
|
+
"label": "Primary Interface",
|
|
2404
|
+
"key": "primary_interface",
|
|
2406
2405
|
"type": "one-to-one",
|
|
2407
2406
|
"source_type": "dcim.device",
|
|
2408
2407
|
"source_label": "primary interface",
|
|
@@ -2410,7 +2409,7 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2410
2409
|
"destination_hidden": True,
|
|
2411
2410
|
},
|
|
2412
2411
|
{
|
|
2413
|
-
"
|
|
2412
|
+
"label": "Relationship 1",
|
|
2414
2413
|
"type": "one-to-one",
|
|
2415
2414
|
"source_type": "dcim.device",
|
|
2416
2415
|
"source_label": "primary interface",
|
|
@@ -2423,7 +2422,7 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2423
2422
|
"source_filter": {"slug": ["some-slug"]},
|
|
2424
2423
|
}
|
|
2425
2424
|
choices_fields = ["destination_type", "source_type", "type", "required_on"]
|
|
2426
|
-
slug_source = "
|
|
2425
|
+
slug_source = "label"
|
|
2427
2426
|
slugify_function = staticmethod(slugify_dashes_to_underscores)
|
|
2428
2427
|
|
|
2429
2428
|
@classmethod
|
|
@@ -2433,15 +2432,15 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2433
2432
|
|
|
2434
2433
|
cls.relationships = (
|
|
2435
2434
|
Relationship(
|
|
2436
|
-
|
|
2437
|
-
|
|
2435
|
+
label="Related locations",
|
|
2436
|
+
key="related_locations",
|
|
2438
2437
|
type="symmetric-many-to-many",
|
|
2439
2438
|
source_type=location_type,
|
|
2440
2439
|
destination_type=location_type,
|
|
2441
2440
|
),
|
|
2442
2441
|
Relationship(
|
|
2443
|
-
|
|
2444
|
-
|
|
2442
|
+
label="Unrelated locations",
|
|
2443
|
+
key="unrelated_locations",
|
|
2445
2444
|
type="many-to-many",
|
|
2446
2445
|
source_type=location_type,
|
|
2447
2446
|
source_label="Other locations (from source side)",
|
|
@@ -2449,8 +2448,8 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2449
2448
|
destination_label="Other locations (from destination side)",
|
|
2450
2449
|
),
|
|
2451
2450
|
Relationship(
|
|
2452
|
-
|
|
2453
|
-
|
|
2451
|
+
label="Devices found elsewhere",
|
|
2452
|
+
key="devices_elsewhere",
|
|
2454
2453
|
type="many-to-many",
|
|
2455
2454
|
source_type=location_type,
|
|
2456
2455
|
destination_type=device_type,
|
|
@@ -2459,9 +2458,8 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2459
2458
|
for relationship in cls.relationships:
|
|
2460
2459
|
relationship.validated_save()
|
|
2461
2460
|
cls.lt = LocationType.objects.get(name="Campus")
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
)
|
|
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)
|
|
2465
2463
|
|
|
2466
2464
|
def test_get_all_relationships_on_location(self):
|
|
2467
2465
|
"""Verify that all relationships are accurately represented when requested."""
|
|
@@ -2476,13 +2474,10 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2476
2474
|
self.maxDiff = None
|
|
2477
2475
|
self.assertEqual(
|
|
2478
2476
|
{
|
|
2479
|
-
self.relationships[0].
|
|
2477
|
+
self.relationships[0].key: {
|
|
2480
2478
|
"id": str(self.relationships[0].pk),
|
|
2481
|
-
"url": (
|
|
2482
|
-
|
|
2483
|
-
+ reverse("extras-api:relationship-detail", kwargs={"pk": self.relationships[0].pk})
|
|
2484
|
-
),
|
|
2485
|
-
"name": self.relationships[0].name,
|
|
2479
|
+
"url": self.absolute_api_url(self.relationships[0]),
|
|
2480
|
+
"label": self.relationships[0].label,
|
|
2486
2481
|
"type": self.relationships[0].type,
|
|
2487
2482
|
"peer": {
|
|
2488
2483
|
"label": "locations",
|
|
@@ -2490,13 +2485,10 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2490
2485
|
"objects": [],
|
|
2491
2486
|
},
|
|
2492
2487
|
},
|
|
2493
|
-
self.relationships[1].
|
|
2488
|
+
self.relationships[1].key: {
|
|
2494
2489
|
"id": str(self.relationships[1].pk),
|
|
2495
|
-
"url": (
|
|
2496
|
-
|
|
2497
|
-
+ reverse("extras-api:relationship-detail", kwargs={"pk": self.relationships[1].pk})
|
|
2498
|
-
),
|
|
2499
|
-
"name": self.relationships[1].name,
|
|
2490
|
+
"url": self.absolute_api_url(self.relationships[1]),
|
|
2491
|
+
"label": self.relationships[1].label,
|
|
2500
2492
|
"type": self.relationships[1].type,
|
|
2501
2493
|
"destination": {
|
|
2502
2494
|
"label": self.relationships[1].source_label, # yes -- it's a bit confusing
|
|
@@ -2509,13 +2501,10 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2509
2501
|
"objects": [],
|
|
2510
2502
|
},
|
|
2511
2503
|
},
|
|
2512
|
-
self.relationships[2].
|
|
2504
|
+
self.relationships[2].key: {
|
|
2513
2505
|
"id": str(self.relationships[2].pk),
|
|
2514
|
-
"url": (
|
|
2515
|
-
|
|
2516
|
-
+ reverse("extras-api:relationship-detail", kwargs={"pk": self.relationships[2].pk})
|
|
2517
|
-
),
|
|
2518
|
-
"name": self.relationships[2].name,
|
|
2506
|
+
"url": self.absolute_api_url(self.relationships[2]),
|
|
2507
|
+
"label": self.relationships[2].label,
|
|
2519
2508
|
"type": self.relationships[2].type,
|
|
2520
2509
|
"destination": {
|
|
2521
2510
|
"label": "devices",
|
|
@@ -2531,28 +2520,33 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2531
2520
|
"""Verify that relationship associations can be populated at instance creation time."""
|
|
2532
2521
|
location_type = LocationType.objects.get(name="Campus")
|
|
2533
2522
|
existing_location_1 = Location.objects.create(
|
|
2534
|
-
name="Existing Location 1",
|
|
2523
|
+
name="Existing Location 1",
|
|
2524
|
+
status=Status.objects.get_for_model(Location).first(),
|
|
2525
|
+
location_type=location_type,
|
|
2535
2526
|
)
|
|
2536
2527
|
existing_location_2 = Location.objects.create(
|
|
2537
|
-
name="Existing Location 2",
|
|
2528
|
+
name="Existing Location 2",
|
|
2529
|
+
status=Status.objects.get_for_model(Location).first(),
|
|
2530
|
+
location_type=location_type,
|
|
2538
2531
|
)
|
|
2539
|
-
manufacturer = Manufacturer.objects.
|
|
2532
|
+
manufacturer = Manufacturer.objects.first()
|
|
2540
2533
|
device_type = DeviceType.objects.create(
|
|
2541
2534
|
manufacturer=manufacturer,
|
|
2542
2535
|
model="device Type 1",
|
|
2543
2536
|
slug="device-type-1",
|
|
2544
2537
|
)
|
|
2545
2538
|
device_role = Role.objects.get_for_model(Device).first()
|
|
2539
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
2546
2540
|
existing_device_1 = Device.objects.create(
|
|
2547
2541
|
name="existing-device-location-1",
|
|
2548
|
-
status=
|
|
2542
|
+
status=device_status,
|
|
2549
2543
|
role=device_role,
|
|
2550
2544
|
device_type=device_type,
|
|
2551
2545
|
location=existing_location_1,
|
|
2552
2546
|
)
|
|
2553
2547
|
existing_device_2 = Device.objects.create(
|
|
2554
2548
|
name="existing-device-location-2",
|
|
2555
|
-
status=
|
|
2549
|
+
status=device_status,
|
|
2556
2550
|
role=device_role,
|
|
2557
2551
|
device_type=device_type,
|
|
2558
2552
|
location=existing_location_2,
|
|
@@ -2563,20 +2557,20 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2563
2557
|
reverse("dcim-api:location-list"),
|
|
2564
2558
|
data={
|
|
2565
2559
|
"name": "New location",
|
|
2566
|
-
"status": Status.objects.
|
|
2560
|
+
"status": Status.objects.get_for_model(Location).first().pk,
|
|
2567
2561
|
"location_type": location_type.pk,
|
|
2568
2562
|
"relationships": {
|
|
2569
|
-
self.relationships[0].
|
|
2563
|
+
self.relationships[0].key: {
|
|
2570
2564
|
"peer": {
|
|
2571
2565
|
"objects": [str(existing_location_1.pk)],
|
|
2572
2566
|
},
|
|
2573
2567
|
},
|
|
2574
|
-
self.relationships[1].
|
|
2568
|
+
self.relationships[1].key: {
|
|
2575
2569
|
"source": {
|
|
2576
2570
|
"objects": [str(existing_location_2.pk)],
|
|
2577
2571
|
},
|
|
2578
2572
|
},
|
|
2579
|
-
self.relationships[2].
|
|
2573
|
+
self.relationships[2].key: {
|
|
2580
2574
|
"destination": {
|
|
2581
2575
|
"objects": [
|
|
2582
2576
|
{"name": "existing-device-location-1"},
|
|
@@ -2653,25 +2647,25 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2653
2647
|
**self.header,
|
|
2654
2648
|
)
|
|
2655
2649
|
|
|
2656
|
-
|
|
2650
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
2657
2651
|
|
|
2658
2652
|
# Try deleting all devices and then creating 2 VLANs (fails):
|
|
2659
2653
|
Device.objects.all().delete()
|
|
2660
2654
|
response = send_bulk_data(
|
|
2661
2655
|
"post",
|
|
2662
2656
|
data=[
|
|
2663
|
-
{"vid": "1", "name": "1", "status":
|
|
2664
|
-
{"vid": "2", "name": "2", "status":
|
|
2657
|
+
{"vid": "1", "name": "1", "status": device_status.pk},
|
|
2658
|
+
{"vid": "2", "name": "2", "status": device_status.pk},
|
|
2665
2659
|
],
|
|
2666
2660
|
)
|
|
2667
2661
|
self.assertHttpStatus(response, 400)
|
|
2668
2662
|
self.assertEqual(
|
|
2669
2663
|
{
|
|
2670
2664
|
"relationships": {
|
|
2671
|
-
"
|
|
2665
|
+
"vlans_devices_m2m": [
|
|
2672
2666
|
"VLANs require at least one device, but no devices exist yet. "
|
|
2673
2667
|
"Create a device by posting to /api/dcim/devices/",
|
|
2674
|
-
'You need to specify ["relationships"]["
|
|
2668
|
+
'You need to specify ["relationships"]["vlans_devices_m2m"]["source"]["objects"].',
|
|
2675
2669
|
]
|
|
2676
2670
|
}
|
|
2677
2671
|
},
|
|
@@ -2680,11 +2674,11 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2680
2674
|
|
|
2681
2675
|
# Create test device for association
|
|
2682
2676
|
device_for_association = test_views.create_test_device("VLAN Required Device")
|
|
2683
|
-
required_relationship_json = {"
|
|
2677
|
+
required_relationship_json = {"vlans_devices_m2m": {"source": {"objects": [str(device_for_association.id)]}}}
|
|
2684
2678
|
expected_error_json = {
|
|
2685
2679
|
"relationships": {
|
|
2686
|
-
"
|
|
2687
|
-
'You need to specify ["relationships"]["
|
|
2680
|
+
"vlans_devices_m2m": [
|
|
2681
|
+
'You need to specify ["relationships"]["vlans_devices_m2m"]["source"]["objects"].'
|
|
2688
2682
|
]
|
|
2689
2683
|
}
|
|
2690
2684
|
}
|
|
@@ -2695,21 +2689,21 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2695
2689
|
vlan1_json_data = {
|
|
2696
2690
|
"vid": "1",
|
|
2697
2691
|
"name": "1",
|
|
2698
|
-
"status":
|
|
2692
|
+
"status": device_status.pk,
|
|
2699
2693
|
}
|
|
2700
2694
|
vlan2_json_data = {
|
|
2701
2695
|
"vid": "2",
|
|
2702
2696
|
"name": "2",
|
|
2703
|
-
"status":
|
|
2697
|
+
"status": device_status.pk,
|
|
2704
2698
|
}
|
|
2705
2699
|
else:
|
|
2706
2700
|
vlan1, vlan2 = VLANFactory.create_batch(2)
|
|
2707
|
-
vlan1_json_data = {"status":
|
|
2701
|
+
vlan1_json_data = {"status": device_status.pk, "id": str(vlan1.id)}
|
|
2708
2702
|
# Add required fields for PUT method:
|
|
2709
2703
|
if method == "put":
|
|
2710
2704
|
vlan1_json_data.update({"vid": vlan1.vid, "name": vlan1.name})
|
|
2711
2705
|
|
|
2712
|
-
vlan2_json_data = {"status":
|
|
2706
|
+
vlan2_json_data = {"status": device_status.pk, "id": str(vlan2.id)}
|
|
2713
2707
|
# Add required fields for PUT method:
|
|
2714
2708
|
if method == "put":
|
|
2715
2709
|
vlan2_json_data.update({"vid": vlan2.vid, "name": vlan2.name})
|
|
@@ -2738,24 +2732,23 @@ class RelationshipTest(APIViewTestCases.APIViewTestCase, RequiredRelationshipTes
|
|
|
2738
2732
|
|
|
2739
2733
|
# Check the relationship associations were actually created
|
|
2740
2734
|
for vlan in response.json():
|
|
2741
|
-
associated_device = vlan["relationships"]["
|
|
2735
|
+
associated_device = vlan["relationships"]["vlans_devices_m2m"]["source"]["objects"][0]
|
|
2742
2736
|
self.assertEqual(str(device_for_association.id), associated_device["id"])
|
|
2743
2737
|
|
|
2744
2738
|
|
|
2745
2739
|
class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
2746
2740
|
model = RelationshipAssociation
|
|
2747
|
-
brief_fields = ["destination_id", "display", "id", "relationship", "source_id", "url"]
|
|
2748
2741
|
choices_fields = ["destination_type", "source_type"]
|
|
2749
2742
|
|
|
2750
2743
|
@classmethod
|
|
2751
2744
|
def setUpTestData(cls):
|
|
2752
2745
|
cls.location_type = ContentType.objects.get_for_model(Location)
|
|
2753
2746
|
cls.device_type = ContentType.objects.get_for_model(Device)
|
|
2754
|
-
cls.
|
|
2747
|
+
cls.location_status = Status.objects.get_for_model(Location).first()
|
|
2755
2748
|
|
|
2756
2749
|
cls.relationship = Relationship(
|
|
2757
|
-
|
|
2758
|
-
|
|
2750
|
+
label="Devices found elsewhere",
|
|
2751
|
+
key="elsewhere_devices",
|
|
2759
2752
|
type="many-to-many",
|
|
2760
2753
|
source_type=cls.location_type,
|
|
2761
2754
|
destination_type=cls.device_type,
|
|
@@ -2764,28 +2757,29 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
2764
2757
|
cls.lt = LocationType.objects.get(name="Campus")
|
|
2765
2758
|
cls.locations = (
|
|
2766
2759
|
Location.objects.create(
|
|
2767
|
-
name="Empty Location", slug="empty", status=cls.
|
|
2760
|
+
name="Empty Location", slug="empty", status=cls.location_status, location_type=cls.lt
|
|
2768
2761
|
),
|
|
2769
2762
|
Location.objects.create(
|
|
2770
|
-
name="Occupied Location", slug="occupied", status=cls.
|
|
2763
|
+
name="Occupied Location", slug="occupied", status=cls.location_status, location_type=cls.lt
|
|
2771
2764
|
),
|
|
2772
2765
|
Location.objects.create(
|
|
2773
2766
|
name="Another Empty Location",
|
|
2774
2767
|
slug="another-empty",
|
|
2775
|
-
status=cls.
|
|
2768
|
+
status=cls.location_status,
|
|
2776
2769
|
location_type=cls.lt,
|
|
2777
2770
|
),
|
|
2778
2771
|
)
|
|
2779
|
-
manufacturer = Manufacturer.objects.
|
|
2772
|
+
manufacturer = Manufacturer.objects.first()
|
|
2780
2773
|
devicetype = DeviceType.objects.create(manufacturer=manufacturer, model="Device Type 1", slug="device-type-1")
|
|
2781
2774
|
devicerole = Role.objects.get_for_model(Device).first()
|
|
2775
|
+
device_status = Status.objects.get_for_model(Device).first()
|
|
2782
2776
|
cls.devices = [
|
|
2783
2777
|
Device.objects.create(
|
|
2784
2778
|
name=f"Device {num}",
|
|
2785
2779
|
device_type=devicetype,
|
|
2786
2780
|
role=devicerole,
|
|
2787
2781
|
location=cls.locations[1],
|
|
2788
|
-
status=
|
|
2782
|
+
status=device_status,
|
|
2789
2783
|
)
|
|
2790
2784
|
for num in range(1, 5)
|
|
2791
2785
|
]
|
|
@@ -2844,8 +2838,8 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
2844
2838
|
"""Test creation of invalid relationship association restricted by destination/source filter."""
|
|
2845
2839
|
|
|
2846
2840
|
relationship = Relationship.objects.create(
|
|
2847
|
-
|
|
2848
|
-
|
|
2841
|
+
label="Device to location Rel 1",
|
|
2842
|
+
key="device_to_location_rel_1",
|
|
2849
2843
|
source_type=self.device_type,
|
|
2850
2844
|
source_filter={"name": [self.devices[0].name]},
|
|
2851
2845
|
destination_type=self.location_type,
|
|
@@ -2886,7 +2880,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
2886
2880
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
2887
2881
|
self.assertEqual(
|
|
2888
2882
|
response.data[side],
|
|
2889
|
-
[f"{field_error_name} violates {relationship.
|
|
2883
|
+
[f"{field_error_name} violates {relationship.label} {side}_filter restriction"],
|
|
2890
2884
|
)
|
|
2891
2885
|
|
|
2892
2886
|
def test_model_clean_method_is_called(self):
|
|
@@ -2914,62 +2908,37 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
2914
2908
|
"""
|
|
2915
2909
|
self.add_permissions("dcim.view_location")
|
|
2916
2910
|
response = self.client.get(
|
|
2917
|
-
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",
|
|
2918
2914
|
**self.header,
|
|
2919
2915
|
)
|
|
2920
2916
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
|
2921
2917
|
self.assertIn("relationships", response.data)
|
|
2922
2918
|
self.assertIsInstance(response.data["relationships"], dict)
|
|
2923
2919
|
# Ensure consistent ordering
|
|
2924
|
-
response.data["relationships"][self.relationship.
|
|
2920
|
+
response.data["relationships"][self.relationship.key]["destination"]["objects"].sort(key=lambda v: v["name"])
|
|
2925
2921
|
self.maxDiff = None
|
|
2926
|
-
self.
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
"display": self.devices[0].display,
|
|
2947
|
-
"name": self.devices[0].name,
|
|
2948
|
-
},
|
|
2949
|
-
{
|
|
2950
|
-
"id": str(self.devices[1].pk),
|
|
2951
|
-
"url": (
|
|
2952
|
-
"http://nautobot.example.com"
|
|
2953
|
-
+ reverse("dcim-api:device-detail", kwargs={"pk": self.devices[1].pk})
|
|
2954
|
-
),
|
|
2955
|
-
"display": self.devices[1].display,
|
|
2956
|
-
"name": self.devices[1].name,
|
|
2957
|
-
},
|
|
2958
|
-
{
|
|
2959
|
-
"id": str(self.devices[2].pk),
|
|
2960
|
-
"url": (
|
|
2961
|
-
"http://nautobot.example.com"
|
|
2962
|
-
+ reverse("dcim-api:device-detail", kwargs={"pk": self.devices[2].pk})
|
|
2963
|
-
),
|
|
2964
|
-
"display": self.devices[2].display,
|
|
2965
|
-
"name": self.devices[2].name,
|
|
2966
|
-
},
|
|
2967
|
-
],
|
|
2968
|
-
},
|
|
2969
|
-
},
|
|
2970
|
-
},
|
|
2971
|
-
response.data["relationships"],
|
|
2972
|
-
)
|
|
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
|
+
)
|
|
2973
2942
|
|
|
2974
2943
|
def test_update_association_data_on_location(self):
|
|
2975
2944
|
"""
|
|
@@ -3031,21 +3000,21 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3031
3000
|
|
|
3032
3001
|
with self.subTest("Error handling: wrong relationship"):
|
|
3033
3002
|
Relationship.objects.create(
|
|
3034
|
-
|
|
3035
|
-
|
|
3003
|
+
label="Device-to-Device",
|
|
3004
|
+
key="device_to_device",
|
|
3036
3005
|
source_type=self.device_type,
|
|
3037
3006
|
destination_type=self.device_type,
|
|
3038
3007
|
type=RelationshipTypeChoices.TYPE_ONE_TO_ONE,
|
|
3039
3008
|
)
|
|
3040
3009
|
response = self.client.patch(
|
|
3041
3010
|
url,
|
|
3042
|
-
{"relationships": {"
|
|
3011
|
+
{"relationships": {"device_to_device": {"peer": {"objects": []}}}},
|
|
3043
3012
|
format="json",
|
|
3044
3013
|
**self.header,
|
|
3045
3014
|
)
|
|
3046
3015
|
self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
|
|
3047
3016
|
self.assertEqual(
|
|
3048
|
-
str(response.data["relationships"][0]), '"
|
|
3017
|
+
str(response.data["relationships"][0]), '"device_to_device" is not a relationship on dcim.Location'
|
|
3049
3018
|
)
|
|
3050
3019
|
self.assertEqual(3, RelationshipAssociation.objects.filter(relationship=self.relationship).count())
|
|
3051
3020
|
for association in self.associations:
|
|
@@ -3054,7 +3023,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3054
3023
|
with self.subTest("Error handling: wrong relationship side"):
|
|
3055
3024
|
response = self.client.patch(
|
|
3056
3025
|
url,
|
|
3057
|
-
{"relationships": {self.relationship.
|
|
3026
|
+
{"relationships": {self.relationship.key: {"source": {"objects": []}}}},
|
|
3058
3027
|
format="json",
|
|
3059
3028
|
**self.header,
|
|
3060
3029
|
)
|
|
@@ -3072,7 +3041,7 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3072
3041
|
url,
|
|
3073
3042
|
{
|
|
3074
3043
|
"relationships": {
|
|
3075
|
-
self.relationship.
|
|
3044
|
+
self.relationship.key: {
|
|
3076
3045
|
"destination": {
|
|
3077
3046
|
"objects": [
|
|
3078
3047
|
# remove devices[0] by omission
|
|
@@ -3099,7 +3068,6 @@ class RelationshipAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3099
3068
|
|
|
3100
3069
|
class SecretTest(APIViewTestCases.APIViewTestCase):
|
|
3101
3070
|
model = Secret
|
|
3102
|
-
brief_fields = ["display", "id", "name", "slug", "url"]
|
|
3103
3071
|
bulk_update_data = {}
|
|
3104
3072
|
|
|
3105
3073
|
create_data = [
|
|
@@ -3120,14 +3088,12 @@ class SecretTest(APIViewTestCases.APIViewTestCase):
|
|
|
3120
3088
|
},
|
|
3121
3089
|
{
|
|
3122
3090
|
"name": "GitHub Token for My Repository",
|
|
3123
|
-
"slug": "github-token-my-repository",
|
|
3124
3091
|
"provider": "text-file",
|
|
3125
3092
|
"parameters": {
|
|
3126
3093
|
"path": "/github-tokens/user/myusername.txt",
|
|
3127
3094
|
},
|
|
3128
3095
|
},
|
|
3129
3096
|
]
|
|
3130
|
-
slug_source = "name"
|
|
3131
3097
|
|
|
3132
3098
|
@classmethod
|
|
3133
3099
|
def setUpTestData(cls):
|
|
@@ -3152,17 +3118,55 @@ class SecretTest(APIViewTestCases.APIViewTestCase):
|
|
|
3152
3118
|
for secret in secrets:
|
|
3153
3119
|
secret.validated_save()
|
|
3154
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
|
+
|
|
3155
3162
|
|
|
3156
3163
|
class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
|
|
3157
3164
|
model = SecretsGroup
|
|
3158
|
-
brief_fields = ["display", "id", "name", "slug", "url"]
|
|
3159
3165
|
bulk_update_data = {}
|
|
3160
3166
|
|
|
3161
|
-
slug_source = "name"
|
|
3162
|
-
|
|
3163
3167
|
@classmethod
|
|
3164
3168
|
def setUpTestData(cls):
|
|
3165
|
-
secrets = (
|
|
3169
|
+
secrets = secrets = (
|
|
3166
3170
|
Secret.objects.create(
|
|
3167
3171
|
name="secret-1", provider="environment-variable", parameters={"variable": "SOME_VAR"}
|
|
3168
3172
|
),
|
|
@@ -3172,9 +3176,9 @@ class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
3172
3176
|
)
|
|
3173
3177
|
|
|
3174
3178
|
secrets_groups = (
|
|
3175
|
-
SecretsGroup.objects.create(name="Group A"
|
|
3176
|
-
SecretsGroup.objects.create(name="Group B"
|
|
3177
|
-
SecretsGroup.objects.create(name="Group C",
|
|
3179
|
+
SecretsGroup.objects.create(name="Group A"),
|
|
3180
|
+
SecretsGroup.objects.create(name="Group B"),
|
|
3181
|
+
SecretsGroup.objects.create(name="Group C", description="Some group"),
|
|
3178
3182
|
)
|
|
3179
3183
|
|
|
3180
3184
|
SecretsGroupAssociation.objects.create(
|
|
@@ -3193,7 +3197,6 @@ class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
3193
3197
|
cls.create_data = [
|
|
3194
3198
|
{
|
|
3195
3199
|
"name": "Secrets Group 1",
|
|
3196
|
-
"slug": "secrets-group-1",
|
|
3197
3200
|
"description": "First Secrets Group",
|
|
3198
3201
|
},
|
|
3199
3202
|
{
|
|
@@ -3209,7 +3212,6 @@ class SecretsGroupTest(APIViewTestCases.APIViewTestCase):
|
|
|
3209
3212
|
|
|
3210
3213
|
class SecretsGroupAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
3211
3214
|
model = SecretsGroupAssociation
|
|
3212
|
-
brief_fields = ["access_type", "display", "id", "secret", "secret_type", "url"]
|
|
3213
3215
|
bulk_update_data = {}
|
|
3214
3216
|
choices_fields = ["access_type", "secret_type"]
|
|
3215
3217
|
|
|
@@ -3228,9 +3230,9 @@ class SecretsGroupAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3228
3230
|
)
|
|
3229
3231
|
|
|
3230
3232
|
secrets_groups = (
|
|
3231
|
-
SecretsGroup.objects.create(name="Group A"
|
|
3232
|
-
SecretsGroup.objects.create(name="Group B"
|
|
3233
|
-
SecretsGroup.objects.create(name="Group C",
|
|
3233
|
+
SecretsGroup.objects.create(name="Group A"),
|
|
3234
|
+
SecretsGroup.objects.create(name="Group B"),
|
|
3235
|
+
SecretsGroup.objects.create(name="Group C", description="Some group"),
|
|
3234
3236
|
)
|
|
3235
3237
|
|
|
3236
3238
|
SecretsGroupAssociation.objects.create(
|
|
@@ -3276,7 +3278,6 @@ class SecretsGroupAssociationTest(APIViewTestCases.APIViewTestCase):
|
|
|
3276
3278
|
|
|
3277
3279
|
class StatusTest(APIViewTestCases.APIViewTestCase):
|
|
3278
3280
|
model = Status
|
|
3279
|
-
brief_fields = ["display", "id", "name", "slug", "url"]
|
|
3280
3281
|
bulk_update_data = {
|
|
3281
3282
|
"color": "000000",
|
|
3282
3283
|
}
|
|
@@ -3284,19 +3285,16 @@ class StatusTest(APIViewTestCases.APIViewTestCase):
|
|
|
3284
3285
|
create_data = [
|
|
3285
3286
|
{
|
|
3286
3287
|
"name": "Pizza",
|
|
3287
|
-
"slug": "pizza",
|
|
3288
3288
|
"color": "0000ff",
|
|
3289
3289
|
"content_types": ["dcim.device", "dcim.rack"],
|
|
3290
3290
|
},
|
|
3291
3291
|
{
|
|
3292
3292
|
"name": "Oysters",
|
|
3293
|
-
"slug": "oysters",
|
|
3294
3293
|
"color": "00ff00",
|
|
3295
3294
|
"content_types": ["ipam.ipaddress", "ipam.prefix"],
|
|
3296
3295
|
},
|
|
3297
3296
|
{
|
|
3298
3297
|
"name": "Bad combinations",
|
|
3299
|
-
"slug": "bad-combinations",
|
|
3300
3298
|
"color": "ff0000",
|
|
3301
3299
|
"content_types": ["dcim.device"],
|
|
3302
3300
|
},
|
|
@@ -3306,12 +3304,10 @@ class StatusTest(APIViewTestCases.APIViewTestCase):
|
|
|
3306
3304
|
"content_types": ["dcim.device"],
|
|
3307
3305
|
},
|
|
3308
3306
|
]
|
|
3309
|
-
slug_source = "name"
|
|
3310
3307
|
|
|
3311
3308
|
|
|
3312
3309
|
class TagTest(APIViewTestCases.APIViewTestCase):
|
|
3313
3310
|
model = Tag
|
|
3314
|
-
brief_fields = ["color", "display", "id", "name", "slug", "url"]
|
|
3315
3311
|
create_data = [
|
|
3316
3312
|
{"name": "Tag 4", "slug": "tag-4", "content_types": [Location._meta.label_lower]},
|
|
3317
3313
|
{"name": "Tag 5", "slug": "tag-5", "content_types": [Location._meta.label_lower]},
|
|
@@ -3395,7 +3391,6 @@ class TagTest(APIViewTestCases.APIViewTestCase):
|
|
|
3395
3391
|
|
|
3396
3392
|
class WebhookTest(APIViewTestCases.APIViewTestCase):
|
|
3397
3393
|
model = Webhook
|
|
3398
|
-
brief_fields = ["display", "id", "name", "url"]
|
|
3399
3394
|
create_data = [
|
|
3400
3395
|
{
|
|
3401
3396
|
"content_types": ["dcim.consoleport"],
|
|
@@ -3624,7 +3619,6 @@ class WebhookTest(APIViewTestCases.APIViewTestCase):
|
|
|
3624
3619
|
|
|
3625
3620
|
class RoleTest(APIViewTestCases.APIViewTestCase):
|
|
3626
3621
|
model = Role
|
|
3627
|
-
brief_fields = ["display", "id", "name", "slug", "url"]
|
|
3628
3622
|
bulk_update_data = {
|
|
3629
3623
|
"color": "000000",
|
|
3630
3624
|
}
|
|
@@ -3632,21 +3626,17 @@ class RoleTest(APIViewTestCases.APIViewTestCase):
|
|
|
3632
3626
|
create_data = [
|
|
3633
3627
|
{
|
|
3634
3628
|
"name": "Role 1",
|
|
3635
|
-
"slug": "role-1",
|
|
3636
3629
|
"color": "0000ff",
|
|
3637
3630
|
"content_types": ["dcim.device", "dcim.rack"],
|
|
3638
3631
|
},
|
|
3639
3632
|
{
|
|
3640
3633
|
"name": "Role 2",
|
|
3641
|
-
"slug": "role-2",
|
|
3642
3634
|
"color": "0000ff",
|
|
3643
3635
|
"content_types": ["dcim.rack"],
|
|
3644
3636
|
},
|
|
3645
3637
|
{
|
|
3646
3638
|
"name": "Role 3",
|
|
3647
|
-
"slug": "role-3",
|
|
3648
3639
|
"color": "0000ff",
|
|
3649
3640
|
"content_types": ["ipam.ipaddress", "ipam.vlan"],
|
|
3650
3641
|
},
|
|
3651
3642
|
]
|
|
3652
|
-
slug_source = "name"
|