From 6da445f7c06e1b7babdc20928492f1b0f7c59618 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Tue, 12 Mar 2019 17:12:16 -0400 Subject: [PATCH] remove /api/v1 and deprecated credential fields --- awx/api/filters.py | 47 -- awx/api/generics.py | 30 +- awx/api/metadata.py | 13 - awx/api/serializers.py | 521 ++---------------- .../templates/api/job_template_callback.md | 8 +- awx/api/templates/api/user_me_list.md | 2 +- awx/api/urls/job.py | 2 - awx/api/urls/urls.py | 48 +- awx/api/versioning.py | 13 - awx/api/views/__init__.py | 228 ++------ awx/api/views/inventory.py | 3 +- awx/api/views/root.py | 30 +- awx/conf/serializers.py | 2 +- awx/conf/tests/functional/test_api.py | 35 -- awx/conf/views.py | 15 +- awx/main/access.py | 18 - awx/main/conf.py | 2 +- awx/main/fields.py | 10 +- .../commands/create_preload_data.py | 2 +- awx/main/models/__init__.py | 8 +- awx/main/models/credential/__init__.py | 250 +-------- awx/main/models/jobs.py | 57 +- awx/main/tasks.py | 15 +- awx/main/tests/factories/fixtures.py | 2 +- .../tests/functional/api/test_credential.py | 450 +-------------- .../test_deprecated_credential_assignment.py | 170 ------ .../functional/api/test_job_runtime_params.py | 18 +- .../tests/functional/api/test_job_template.py | 227 +------- .../functional/api/test_rbac_displays.py | 4 +- .../functional/api/test_workflow_node.py | 4 +- .../functional/models/test_unified_job.py | 5 +- awx/main/tests/functional/test_credential.py | 21 +- .../functional/test_rbac_job_templates.py | 80 +-- .../test_job_template_serializers.py | 3 +- awx/main/tests/unit/test_tasks.py | 14 +- awx/main/tests/unit/test_views.py | 16 +- awx/ui/context_processors.py | 2 +- docs/custom_credential_types.md | 46 -- docs/notification_system.md | 1 - .../rbac_dummy_data_generator.py | 4 +- tools/docker-compose/README | 2 +- tools/elastic/README.md | 2 +- tools/scripts/launch_job.py | 54 -- 43 files changed, 272 insertions(+), 2212 deletions(-) delete mode 100755 tools/scripts/launch_job.py diff --git a/awx/api/filters.py b/awx/api/filters.py index b41a19162720..547d1b3a1afd 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -24,20 +24,6 @@ # AWX from awx.main.utils import get_type_for_model, to_python_boolean from awx.main.utils.db import get_all_field_names -from awx.main.models.credential import CredentialType - - -class V1CredentialFilterBackend(BaseFilterBackend): - ''' - For /api/v1/ requests, filter out v2 (custom) credentials - ''' - - def filter_queryset(self, request, queryset, view): - # TODO: remove in 3.3 - from awx.api.versioning import get_request_version - if get_request_version(request) == 1: - queryset = queryset.filter(credential_type__managed_by_tower=True) - return queryset class TypeFilterBackend(BaseFilterBackend): @@ -292,39 +278,6 @@ def filter_queryset(self, request, queryset, view): key = key[5:] q_not = True - # Make legacy v1 Job/Template fields work for backwards compatability - # TODO: remove after API v1 deprecation period - if queryset.model._meta.object_name in ('JobTemplate', 'Job') and key in ( - 'credential', 'vault_credential', 'cloud_credential', 'network_credential' - ) or queryset.model._meta.object_name in ('InventorySource', 'InventoryUpdate') and key == 'credential': - key = 'credentials' - - # Make legacy v1 Credential fields work for backwards compatability - # TODO: remove after API v1 deprecation period - # - # convert v1 `Credential.kind` queries to `Credential.credential_type__pk` - if queryset.model._meta.object_name == 'Credential' and key == 'kind': - key = key.replace('kind', 'credential_type') - - if 'ssh' in values: - # In 3.2, SSH and Vault became separate credential types, but in the v1 API, - # they're both still "kind=ssh" - # under the hood, convert `/api/v1/credentials/?kind=ssh` to - # `/api/v1/credentials/?or__credential_type=&or__credential_type=` - values = set(values) - values.add('vault') - values = list(values) - q_or = True - - for i, kind in enumerate(values): - if kind == 'vault': - type_ = CredentialType.objects.get(kind=kind) - else: - type_ = CredentialType.from_v1_kind(kind) - if type_ is None: - raise ParseError(_('cannot filter on kind %s') % kind) - values[i] = type_.pk - # Convert value(s) to python and add to the appropriate list. for value in values: if q_int: diff --git a/awx/api/generics.py b/awx/api/generics.py index 6417e59871a9..0a33b889cd55 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -34,7 +34,7 @@ # AWX from awx.api.filters import FieldLookupBackend from awx.main.models import ( - UnifiedJob, UnifiedJobTemplate, User, Role + UnifiedJob, UnifiedJobTemplate, User, Role, Credential ) from awx.main.access import access_registry from awx.main.utils import ( @@ -46,7 +46,7 @@ ) from awx.main.utils.db import get_all_field_names from awx.api.serializers import ResourceAccessListElementSerializer, CopySerializer, UserSerializer -from awx.api.versioning import URLPathVersioning, get_request_version +from awx.api.versioning import URLPathVersioning from awx.api.metadata import SublistAttachDetatchMetadata, Metadata __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView', @@ -288,12 +288,6 @@ def get_description(self, request, html=False): template_list.append('api/%s.md' % template_basename) context = self.get_description_context() - # "v2" -> 2 - default_version = int(settings.REST_FRAMEWORK['DEFAULT_VERSION'].lstrip('v')) - request_version = get_request_version(self.request) - if request_version is not None and request_version < default_version: - context['deprecated'] = True - description = render_to_string(template_list, context) if context.get('deprecated') and context.get('swagger_method') is None: # render deprecation messages at the very top @@ -842,10 +836,6 @@ class CopyAPIView(GenericAPIView): new_in_330 = True new_in_api_v2 = True - def v1_not_allowed(self): - return Response({'detail': 'Action only possible starting with v2 API.'}, - status=status.HTTP_404_NOT_FOUND) - def _get_copy_return_serializer(self, *args, **kwargs): if not self.copy_return_serializer_class: return self.get_serializer(*args, **kwargs) @@ -859,15 +849,15 @@ def _get_copy_return_serializer(self, *args, **kwargs): def _decrypt_model_field_if_needed(obj, field_name, field_val): if field_name in getattr(type(obj), 'REENCRYPTION_BLACKLIST_AT_COPY', []): return field_val - if isinstance(field_val, dict): + if isinstance(obj, Credential) and field_name == 'inputs': + for secret in obj.credential_type.secret_fields: + if secret in field_val: + field_val[secret] = decrypt_field(obj, secret) + elif isinstance(field_val, dict): for sub_field in field_val: if isinstance(sub_field, str) \ and isinstance(field_val[sub_field], str): - try: - field_val[sub_field] = decrypt_field(obj, field_name, sub_field) - except AttributeError: - # Catching the corner case with v1 credential fields - field_val[sub_field] = decrypt_field(obj, sub_field) + field_val[sub_field] = decrypt_field(obj, field_name, sub_field) elif isinstance(field_val, str): try: field_val = decrypt_field(obj, field_name) @@ -952,8 +942,6 @@ def copy_model_obj(old_parent, new_parent, model, obj, creater, copy_name='', cr return ret def get(self, request, *args, **kwargs): - if get_request_version(request) < 2: - return self.v1_not_allowed() obj = self.get_object() if not request.user.can_access(obj.__class__, 'read', obj): raise PermissionDenied() @@ -968,8 +956,6 @@ def get(self, request, *args, **kwargs): return Response({'can_copy': can_copy}) def post(self, request, *args, **kwargs): - if get_request_version(request) < 2: - return self.v1_not_allowed() obj = self.get_object() create_kwargs = self._build_create_dict(obj) create_kwargs_check = {} diff --git a/awx/api/metadata.py b/awx/api/metadata.py index 3f7ff7ea0b1b..cc44e6d0e968 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -232,19 +232,6 @@ def determine_metadata(self, request, view): return metadata -# TODO: Tower 3.3 remove class and all uses in views.py when API v1 is removed -class JobTypeMetadata(Metadata): - def get_field_info(self, field): - res = super(JobTypeMetadata, self).get_field_info(field) - - if field.field_name == 'job_type': - res['choices'] = [ - choice for choice in res['choices'] - if choice[0] != 'scan' - ] - return res - - class SublistAttachDetatchMetadata(Metadata): def determine_actions(self, request, view): diff --git a/awx/api/serializers.py b/awx/api/serializers.py index ad001e44ccbe..0614a52e5a98 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -54,7 +54,7 @@ OAuth2AccessToken, OAuth2Application, Organization, Project, ProjectUpdate, ProjectUpdateEvent, RefreshToken, Role, Schedule, SystemJob, SystemJobEvent, SystemJobTemplate, Team, UnifiedJob, - UnifiedJobTemplate, V1Credential, WorkflowJob, WorkflowJobNode, + UnifiedJobTemplate, WorkflowJob, WorkflowJobNode, WorkflowJobTemplate, WorkflowJobTemplateNode, StdoutMaxBytesExceeded ) from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES @@ -72,7 +72,7 @@ from awx.main.validators import vars_validate_or_raise -from awx.api.versioning import reverse, get_request_version +from awx.api.versioning import reverse from awx.api.fields import (BooleanNullField, CharNullField, ChoiceNullField, VerbatimField, DeprecatedCredentialField) @@ -113,7 +113,6 @@ 'source_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), 'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'), - 'vault_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud', 'credential_type_id'), 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed', 'type'), 'job_template': DEFAULT_SUMMARY_FIELDS, 'workflow_job_template': DEFAULT_SUMMARY_FIELDS, @@ -144,7 +143,7 @@ def reverse_gfk(content_object, request): Returns a dictionary of the form { '': reverse() } for example - { 'organization': '/api/v1/organizations/1/' } + { 'organization': '/api/v2/organizations/1/' } ''' if content_object is None or not hasattr(content_object, 'get_absolute_url'): return {} @@ -301,10 +300,7 @@ def __init__(self, *args, **kwargs): @property def version(self): - """ - The request version component of the URL as an integer i.e., 1 or 2 - """ - return get_request_version(self.context.get('request')) or 1 + return 2 def get_type(self, obj): return get_type_for_model(self.Meta.model) @@ -359,10 +355,9 @@ def get_related(self, obj): if view and (hasattr(view, 'retrieve') or view.request.method == 'POST') and \ type(obj) in settings.NAMED_URL_GRAPH: original_url = self.get_url(obj) - if not original_url.startswith('/api/v1'): - res['named_url'] = self._generate_named_url( - original_url, obj, settings.NAMED_URL_GRAPH[type(obj)] - ) + res['named_url'] = self._generate_named_url( + original_url, obj, settings.NAMED_URL_GRAPH[type(obj)] + ) if getattr(obj, 'created_by', None): res['created_by'] = self.reverse('api:user_detail', kwargs={'pk': obj.created_by.pk}) if getattr(obj, 'modified_by', None): @@ -396,8 +391,6 @@ def get_summary_fields(self, obj): continue summary_fields[fk] = OrderedDict() for field in related_fields: - if self.version < 2 and field == 'credential_type_id': # TODO: remove version check in 3.3 - continue fval = getattr(fkval, field, None) @@ -884,10 +877,10 @@ class Meta: 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn', 'last_login', 'external_account') - def to_representation(self, obj): # TODO: Remove in 3.3 + def to_representation(self, obj): ret = super(UserSerializer, self).to_representation(obj) ret.pop('password', None) - if obj and type(self) is UserSerializer or self.version == 1: + if obj and type(self) is UserSerializer: ret['auth'] = obj.social_auth.values('provider', 'uid') return ret @@ -1364,9 +1357,9 @@ def get_related(self, obj): notification_templates_error = self.reverse('api:project_notification_templates_error_list', kwargs={'pk': obj.pk}), access_list = self.reverse('api:project_access_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:project_object_roles_list', kwargs={'pk': obj.pk}), + copy = self.reverse('api:project_copy', kwargs={'pk': obj.pk}) + )) - if self.version > 1: - res['copy'] = self.reverse('api:project_copy', kwargs={'pk': obj.pk}) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) @@ -1561,9 +1554,8 @@ def get_related(self, obj): access_list = self.reverse('api:inventory_access_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:inventory_object_roles_list', kwargs={'pk': obj.pk}), instance_groups = self.reverse('api:inventory_instance_groups_list', kwargs={'pk': obj.pk}), + copy = self.reverse('api:inventory_copy', kwargs={'pk': obj.pk}) )) - if self.version > 1: - res['copy'] = self.reverse('api:inventory_copy', kwargs={'pk': obj.pk}) if obj.insights_credential: res['insights_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.insights_credential.pk}) if obj.organization: @@ -1615,20 +1607,6 @@ def validate(self, attrs): return super(InventorySerializer, self).validate(attrs) -# TODO: Remove entire serializer in 3.3, replace with normal serializer -class InventoryDetailSerializer(InventorySerializer): - - def get_fields(self): - fields = super(InventoryDetailSerializer, self).get_fields() - if self.version == 1: - fields['can_run_ad_hoc_commands'] = serializers.SerializerMethodField() - return fields - - def get_can_run_ad_hoc_commands(self, obj): - view = self.context.get('view', None) - return bool(obj and view and view.request and view.request.user and view.request.user.can_access(Inventory, 'run_ad_hoc_commands', obj)) - - class InventoryScriptSerializer(InventorySerializer): class Meta: @@ -1668,19 +1646,15 @@ def get_related(self, obj): smart_inventories = self.reverse('api:host_smart_inventories_list', kwargs={'pk': obj.pk}), ad_hoc_commands = self.reverse('api:host_ad_hoc_commands_list', kwargs={'pk': obj.pk}), ad_hoc_command_events = self.reverse('api:host_ad_hoc_command_events_list', kwargs={'pk': obj.pk}), + insights = self.reverse('api:host_insights', kwargs={'pk': obj.pk}), + ansible_facts = self.reverse('api:host_ansible_facts_detail', kwargs={'pk': obj.pk}), )) - if self.version > 1: - res['insights'] = self.reverse('api:host_insights', kwargs={'pk': obj.pk}) if obj.inventory: res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) if obj.last_job: res['last_job'] = self.reverse('api:job_detail', kwargs={'pk': obj.last_job.pk}) if obj.last_job_host_summary: res['last_job_host_summary'] = self.reverse('api:job_host_summary_detail', kwargs={'pk': obj.last_job_host_summary.pk}) - if self.version > 1: - res.update(dict( - ansible_facts = self.reverse('api:host_ansible_facts_detail', kwargs={'pk': obj.pk}), - )) return res def get_summary_fields(self, obj): @@ -1766,6 +1740,7 @@ def to_representation(self, obj): class GroupSerializer(BaseSerializerWithVariables): + show_capabilities = ['copy', 'edit', 'delete'] capabilities_prefetch = ['inventory.admin', 'inventory.adhoc'] groups_with_active_failures = serializers.IntegerField( read_only=True, @@ -1779,13 +1754,6 @@ class Meta: 'total_hosts', 'hosts_with_active_failures', 'total_groups', 'groups_with_active_failures', 'has_inventory_sources') - @property - def show_capabilities(self): # TODO: consolidate in 3.3 - if self.version == 1: - return ['copy', 'edit', 'start', 'schedule', 'delete'] - else: - return ['copy', 'edit', 'delete'] - def build_relational_field(self, field_name, relation_info): field_class, field_kwargs = super(GroupSerializer, self).build_relational_field(field_name, relation_info) # Inventory is read-only unless creating a new group. @@ -1794,20 +1762,6 @@ def build_relational_field(self, field_name, relation_info): field_kwargs.pop('queryset', None) return field_class, field_kwargs - def get_summary_fields(self, obj): # TODO: remove in 3.3 - summary_fields = super(GroupSerializer, self).get_summary_fields(obj) - if self.version == 1: - try: - inv_src = obj.deprecated_inventory_source - summary_fields['inventory_source'] = {} - for field in SUMMARIZABLE_FK_FIELDS['inventory_source']: - fval = getattr(inv_src, field, None) - if fval is not None: - summary_fields['inventory_source'][field] = fval - except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: - pass - return summary_fields - def get_related(self, obj): res = super(GroupSerializer, self).get_related(obj) res.update(dict( @@ -1822,24 +1776,10 @@ def get_related(self, obj): inventory_sources = self.reverse('api:group_inventory_sources_list', kwargs={'pk': obj.pk}), ad_hoc_commands = self.reverse('api:group_ad_hoc_commands_list', kwargs={'pk': obj.pk}), )) - if self.version == 1: # TODO: remove in 3.3 - try: - res['inventory_source'] = self.reverse('api:inventory_source_detail', - kwargs={'pk': obj.deprecated_inventory_source.pk}) - except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: - pass if obj.inventory: res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) return res - def create(self, validated_data): # TODO: remove in 3.3 - instance = super(GroupSerializer, self).create(validated_data) - if self.version == 1: # TODO: remove in 3.3 - manual_src = InventorySource(deprecated_group=instance, inventory=instance.inventory) - manual_src.v1_group_name = instance.name - manual_src.save() - return instance - def validate_name(self, value): if value in ('all', '_meta'): raise serializers.ValidationError(_('Invalid group name.')) @@ -1941,9 +1881,8 @@ def get_related(self, obj): res = super(CustomInventoryScriptSerializer, self).get_related(obj) res.update(dict( object_roles = self.reverse('api:inventory_script_object_roles_list', kwargs={'pk': obj.pk}), + copy = self.reverse('api:inventory_script_copy', kwargs={'pk': obj.pk}), )) - if self.version > 1: - res['copy'] = self.reverse('api:inventory_script_copy', kwargs={'pk': obj.pk}) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) @@ -2004,27 +1943,6 @@ def validate(self, attrs): return super(InventorySourceOptionsSerializer, self).validate(attrs) - # TODO: remove when old 'credential' fields are removed - def get_summary_fields(self, obj): - summary_fields = super(InventorySourceOptionsSerializer, self).get_summary_fields(obj) - all_creds = [] - if 'credential' in summary_fields: - cred = obj.get_cloud_credential() - if cred: - summarized_cred = { - 'id': cred.id, 'name': cred.name, 'description': cred.description, - 'kind': cred.kind, 'cloud': True - } - summary_fields['credential'] = summarized_cred - all_creds.append(summarized_cred) - if self.version > 1: - summary_fields['credential']['credential_type_id'] = cred.credential_type_id - else: - summary_fields.pop('credential') - if self.version > 1: - summary_fields['credentials'] = all_creds - return summary_fields - class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOptionsSerializer): @@ -2036,14 +1954,12 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt {'admin': 'inventory.admin'}, {'start': 'inventory.update'} ] - group = serializers.SerializerMethodField( - help_text=_('Automatic group relationship, will be removed in 3.3')) class Meta: model = InventorySource fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', 'source_project', 'update_on_project_update') + \ - ('last_update_failed', 'last_updated', 'group') # Backwards compatibility. + ('last_update_failed', 'last_updated') # Backwards compatibility. def get_related(self, obj): res = super(InventorySourceSerializer, self).get_related(obj) @@ -2069,30 +1985,10 @@ def get_related(self, obj): if obj.last_update: res['last_update'] = self.reverse('api:inventory_update_detail', kwargs={'pk': obj.last_update.pk}) - if self.version == 1: # TODO: remove in 3.3 - if obj.deprecated_group: - res['group'] = self.reverse('api:group_detail', kwargs={'pk': obj.deprecated_group.pk}) else: res['credentials'] = self.reverse('api:inventory_source_credentials_list', kwargs={'pk': obj.pk}) return res - def get_fields(self): # TODO: remove in 3.3 - fields = super(InventorySourceSerializer, self).get_fields() - if self.version > 1: - fields.pop('group', None) - return fields - - def get_summary_fields(self, obj): # TODO: remove in 3.3 - summary_fields = super(InventorySourceSerializer, self).get_summary_fields(obj) - if self.version == 1 and obj.deprecated_group_id: - g = obj.deprecated_group - summary_fields['group'] = {} - for field in SUMMARIZABLE_FK_FIELDS['group']: - fval = getattr(g, field, None) - if fval is not None: - summary_fields['group'][field] = fval - return summary_fields - def get_group(self, obj): # TODO: remove in 3.3 if obj.deprecated_group: return obj.deprecated_group.id @@ -2127,12 +2023,6 @@ def validate_source_project(self, value): raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory.")) return value - def validate_source(self, value): - if value == '': - raise serializers.ValidationError(_( - "Manual inventory sources are created automatically when a group is created in the v1 API.")) - return value - def validate_update_on_project_update(self, value): if value and self.instance and self.instance.schedules.exists(): raise serializers.ValidationError(_("Setting not compatible with existing schedules.")) @@ -2253,8 +2143,7 @@ def get_related(self, obj): if obj.inventory: res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) - if self.version > 1: - res['credentials'] = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk}) + res['credentials'] = self.reverse('api:inventory_update_credentials_list', kwargs={'pk': obj.pk}) return res @@ -2562,67 +2451,22 @@ def filter_field_metadata(self, fields, method): return fields -# TODO: remove when API v1 is removed -class V1CredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass): - - class Meta: - model = Credential - fields = ('*', 'kind', 'cloud', 'host', 'username', - 'password', 'security_token', 'project', 'domain', - 'ssh_key_data', 'ssh_key_unlock', 'become_method', - 'become_username', 'become_password', 'vault_password', - 'subscription', 'tenant', 'secret', 'client', 'authorize', - 'authorize_password') - - def build_field(self, field_name, info, model_class, nested_depth): - if field_name in V1Credential.FIELDS: - return self.build_standard_field(field_name, - V1Credential.FIELDS[field_name]) - return super(V1CredentialFields, self).build_field(field_name, info, model_class, nested_depth) - - -class V2CredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass): +class CredentialSerializer(BaseSerializer): + show_capabilities = ['edit', 'delete', 'copy', 'use'] + capabilities_prefetch = ['admin', 'use'] class Meta: model = Credential - fields = ('*', 'credential_type', 'inputs') - + fields = ('*', 'organization', 'credential_type', 'inputs', 'kind', 'cloud') extra_kwargs = { 'credential_type': { 'label': _('Credential Type'), }, } - -class CredentialSerializer(BaseSerializer): - show_capabilities = ['edit', 'delete', 'copy', 'use'] - capabilities_prefetch = ['admin', 'use'] - - class Meta: - model = Credential - fields = ('*', 'organization') - - def get_fields(self): - fields = super(CredentialSerializer, self).get_fields() - - # TODO: remove when API v1 is removed - if self.version == 1: - fields.update(V1CredentialFields().get_fields()) - else: - fields.update(V2CredentialFields().get_fields()) - return fields - def to_representation(self, data): value = super(CredentialSerializer, self).to_representation(data) - # TODO: remove when API v1 is removed - if self.version == 1: - if value.get('kind') == 'vault': - value['kind'] = 'ssh' - for field in V1Credential.PASSWORD_FIELDS: - if field in value and force_text(value[field]).startswith('$encrypted$'): - value[field] = '$encrypted$' - if 'inputs' in value: value['inputs'] = data.display_inputs() return value @@ -2639,16 +2483,10 @@ def get_related(self, obj): object_roles = self.reverse('api:credential_object_roles_list', kwargs={'pk': obj.pk}), owner_users = self.reverse('api:credential_owner_users_list', kwargs={'pk': obj.pk}), owner_teams = self.reverse('api:credential_owner_teams_list', kwargs={'pk': obj.pk}), + copy = self.reverse('api:credential_copy', kwargs={'pk': obj.pk}), + input_sources = self.reverse('api:credential_input_source_sublist', kwargs={'pk': obj.pk}), + credential_type = self.reverse('api:credential_type_detail', kwargs={'pk': obj.credential_type.pk}), )) - if self.version > 1: - res['copy'] = self.reverse('api:credential_copy', kwargs={'pk': obj.pk}) - res['input_sources'] = self.reverse('api:credential_input_source_sublist', kwargs={'pk': obj.pk}) - - # TODO: remove when API v1 is removed - if self.version > 1: - res.update(dict( - credential_type = self.reverse('api:credential_type_detail', kwargs={'pk': obj.credential_type.pk}), - )) parents = [role for role in obj.admin_role.parents.all() if role.object_id is not None] if parents: @@ -2684,54 +2522,12 @@ def get_summary_fields(self, obj): return summary_dict def get_validation_exclusions(self, obj=None): - # CredentialType is now part of validation; legacy v1 fields (e.g., - # 'username', 'password') in JSON POST payloads use the - # CredentialType's inputs definition to determine their validity ret = super(CredentialSerializer, self).get_validation_exclusions(obj) for field in ('credential_type', 'inputs'): if field in ret: ret.remove(field) return ret - def to_internal_value(self, data): - # TODO: remove when API v1 is removed - if 'credential_type' not in data and self.version == 1: - # If `credential_type` is not provided, assume the payload is a - # v1 credential payload that specifies a `kind` and a flat list - # of field values - # - # In this scenario, we should automatically detect the proper - # CredentialType based on the provided values - kind = data.get('kind', 'ssh') - credential_type = CredentialType.from_v1_kind(kind, data) - if credential_type is None: - raise serializers.ValidationError({"kind": _('"%s" is not a valid choice' % kind)}) - data['credential_type'] = credential_type.pk - value = OrderedDict( - list({'credential_type': credential_type}.items()) + - list(super(CredentialSerializer, self).to_internal_value(data).items()) - ) - - # Make a set of the keys in the POST/PUT payload - # - Subtract real fields (name, organization, inputs) - # - Subtract virtual v1 fields defined on the determined credential - # type (username, password, etc...) - # - Any leftovers are invalid for the determined credential type - valid_fields = set(super(CredentialSerializer, self).get_fields().keys()) - valid_fields.update(V2CredentialFields().get_fields().keys()) - valid_fields.update(['kind', 'cloud']) - - for field in set(data.keys()) - valid_fields - set(credential_type.defined_fields): - if data.get(field): - raise serializers.ValidationError( - {"detail": _("'{field_name}' is not a valid field for {credential_type_name}").format( - field_name=field, credential_type_name=credential_type.name - )} - ) - value.pop('kind', None) - return value - return super(CredentialSerializer, self).to_internal_value(data) - def validate_credential_type(self, credential_type): if self.instance and credential_type.pk != self.instance.credential_type.pk: for rel in ( @@ -2788,35 +2584,12 @@ def validate(self, attrs): if attrs.get('team'): attrs['organization'] = attrs['team'].organization - try: - return super(CredentialSerializerCreate, self).validate(attrs) - except ValidationError as e: - # TODO: remove when API v1 is removed - # If we have an `inputs` error on `/api/v1/`: - # {'inputs': {'username': [...]}} - # ...instead, send back: - # {'username': [...]} - if self.version == 1 and isinstance(e.detail.get('inputs'), dict): - e.detail = e.detail['inputs'] - raise e - else: - raise + return super(CredentialSerializerCreate, self).validate(attrs) def create(self, validated_data): user = validated_data.pop('user', None) team = validated_data.pop('team', None) - # If our payload contains v1 credential fields, translate to the new - # model - # TODO: remove when API v1 is removed - if self.version == 1: - for attr in ( - set(V1Credential.FIELDS) & set(validated_data.keys()) # set intersection - ): - validated_data.setdefault('inputs', {}) - value = validated_data.pop(attr) - if value: - validated_data['inputs'][attr] = value credential = super(CredentialSerializerCreate, self).create(validated_data) if user: @@ -2895,35 +2668,6 @@ def get_summary_fields(self, obj): return res -# TODO: remove when API v1 is removed -class V1JobOptionsSerializer(BaseSerializer, metaclass=BaseSerializerMetaclass): - - class Meta: - model = Credential - fields = ('*', 'cloud_credential', 'network_credential') - - V1_FIELDS = ('cloud_credential', 'network_credential',) - - def build_field(self, field_name, info, model_class, nested_depth): - if field_name in self.V1_FIELDS: - return (DeprecatedCredentialField, {}) - return super(V1JobOptionsSerializer, self).build_field(field_name, info, model_class, nested_depth) - - -class LegacyCredentialFields(BaseSerializer, metaclass=BaseSerializerMetaclass): - - class Meta: - model = Credential - fields = ('*', 'credential', 'vault_credential') - - LEGACY_FIELDS = ('credential', 'vault_credential',) - - def build_field(self, field_name, info, model_class, nested_depth): - if field_name in self.LEGACY_FIELDS: - return (DeprecatedCredentialField, {}) - return super(LegacyCredentialFields, self).build_field(field_name, info, model_class, nested_depth) - - class JobOptionsSerializer(LabelsListMixin, BaseSerializer): class Meta: @@ -2932,16 +2676,6 @@ class Meta: 'force_handlers', 'skip_tags', 'start_at_task', 'timeout', 'use_fact_cache',) - def get_fields(self): - fields = super(JobOptionsSerializer, self).get_fields() - - # TODO: remove when API v1 is removed - if self.version == 1: - fields.update(V1JobOptionsSerializer().get_fields()) - - fields.update(LegacyCredentialFields().get_fields()) - return fields - def get_related(self, obj): res = super(JobOptionsSerializer, self).get_related(obj) res['labels'] = self.reverse('api:job_template_label_list', kwargs={'pk': obj.pk}) @@ -2955,40 +2689,18 @@ def get_related(self, obj): res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk}) except ObjectDoesNotExist: setattr(obj, 'project', None) - try: - if obj.credential: - res['credential'] = self.reverse( - 'api:credential_detail', kwargs={'pk': obj.credential} - ) - except ObjectDoesNotExist: - setattr(obj, 'credential', None) - try: - if obj.vault_credential: - res['vault_credential'] = self.reverse( - 'api:credential_detail', kwargs={'pk': obj.vault_credential} - ) - except ObjectDoesNotExist: - setattr(obj, 'vault_credential', None) - if self.version > 1: - if isinstance(obj, UnifiedJobTemplate): - res['extra_credentials'] = self.reverse( - 'api:job_template_extra_credentials_list', - kwargs={'pk': obj.pk} - ) - res['credentials'] = self.reverse( - 'api:job_template_credentials_list', - kwargs={'pk': obj.pk} - ) - elif isinstance(obj, UnifiedJob): - res['extra_credentials'] = self.reverse('api:job_extra_credentials_list', kwargs={'pk': obj.pk}) - res['credentials'] = self.reverse('api:job_credentials_list', kwargs={'pk': obj.pk}) - else: - cloud_cred = obj.cloud_credential - if cloud_cred: - res['cloud_credential'] = self.reverse('api:credential_detail', kwargs={'pk': cloud_cred}) - net_cred = obj.network_credential - if net_cred: - res['network_credential'] = self.reverse('api:credential_detail', kwargs={'pk': net_cred}) + if isinstance(obj, UnifiedJobTemplate): + res['extra_credentials'] = self.reverse( + 'api:job_template_extra_credentials_list', + kwargs={'pk': obj.pk} + ) + res['credentials'] = self.reverse( + 'api:job_template_credentials_list', + kwargs={'pk': obj.pk} + ) + elif isinstance(obj, UnifiedJob): + res['extra_credentials'] = self.reverse('api:job_extra_credentials_list', kwargs={'pk': obj.pk}) + res['credentials'] = self.reverse('api:job_credentials_list', kwargs={'pk': obj.pk}) return res @@ -3002,70 +2714,9 @@ def to_representation(self, obj): ret['project'] = None if 'playbook' in ret: ret['playbook'] = '' - ret['credential'] = obj.credential - ret['vault_credential'] = obj.vault_credential - if self.version == 1: - ret['cloud_credential'] = obj.cloud_credential - ret['network_credential'] = obj.network_credential return ret - def create(self, validated_data): - deprecated_fields = {} - for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'): - if key in validated_data: - deprecated_fields[key] = validated_data.pop(key) - obj = super(JobOptionsSerializer, self).create(validated_data) - if deprecated_fields: # TODO: remove in 3.3 - self._update_deprecated_fields(deprecated_fields, obj) - return obj - - def update(self, obj, validated_data): - deprecated_fields = {} - for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'): - if key in validated_data: - deprecated_fields[key] = validated_data.pop(key) - obj = super(JobOptionsSerializer, self).update(obj, validated_data) - if deprecated_fields: # TODO: remove in 3.3 - self._update_deprecated_fields(deprecated_fields, obj) - return obj - - def _update_deprecated_fields(self, fields, obj): - for key, existing in ( - ('credential', obj.credentials.filter(credential_type__kind='ssh')), - ('vault_credential', obj.credentials.filter(credential_type__kind='vault')), - ('cloud_credential', obj.cloud_credentials), - ('network_credential', obj.network_credentials), - ): - if key in fields: - new_cred = fields[key] - if new_cred not in existing: - for cred in existing: - obj.credentials.remove(cred) - if new_cred: - obj.credentials.add(new_cred) - def validate(self, attrs): - v1_credentials = {} - view = self.context.get('view', None) - for attr, kind, error in ( - ('cloud_credential', 'cloud', _('You must provide a cloud credential.')), - ('network_credential', 'net', _('You must provide a network credential.')), - ('credential', 'ssh', _('You must provide an SSH credential.')), - ('vault_credential', 'vault', _('You must provide a vault credential.')), - ): - if kind in ('cloud', 'net') and self.version > 1: - continue # cloud and net deprecated creds are v1 only - if attr in attrs: - v1_credentials[attr] = None - pk = attrs.pop(attr) - if pk: - cred = v1_credentials[attr] = Credential.objects.get(pk=pk) - if cred.credential_type.kind != kind: - raise serializers.ValidationError({attr: error}) - if ((not self.instance or cred.pk != getattr(self.instance, attr)) and - view and view.request and view.request.user not in cred.use_role): - raise PermissionDenied() - if 'project' in self.fields and 'playbook' in self.fields: project = attrs.get('project', self.instance and self.instance.project or None) playbook = attrs.get('playbook', self.instance and self.instance.playbook or '') @@ -3079,7 +2730,6 @@ def validate(self, attrs): raise serializers.ValidationError({'playbook': _('Must select playbook for project.')}) ret = super(JobOptionsSerializer, self).validate(attrs) - ret.update(v1_credentials) return ret @@ -3105,12 +2755,6 @@ def get_summary_fields(self, obj): if obj.survey_spec is not None and ('name' in obj.survey_spec and 'description' in obj.survey_spec): d['survey'] = dict(title=obj.survey_spec['name'], description=obj.survey_spec['description']) d['recent_jobs'] = self._recent_jobs(obj) - - # TODO: remove in 3.3 - if self.version == 1 and 'vault_credential' in d: - if d['vault_credential'].get('kind','') == 'vault': - d['vault_credential']['kind'] = 'ssh' - return d @@ -3146,9 +2790,8 @@ def get_related(self, obj): object_roles = self.reverse('api:job_template_object_roles_list', kwargs={'pk': obj.pk}), instance_groups = self.reverse('api:job_template_instance_groups_list', kwargs={'pk': obj.pk}), slice_workflow_jobs = self.reverse('api:job_template_slice_workflow_jobs_list', kwargs={'pk': obj.pk}), + copy = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk}), )) - if self.version > 1: - res['copy'] = self.reverse('api:job_template_copy', kwargs={'pk': obj.pk}) if obj.host_config_key: res['callback'] = self.reverse('api:job_template_callback', kwargs={'pk': obj.pk}) return res @@ -3181,9 +2824,6 @@ def get_summary_fields(self, obj): summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj) all_creds = [] # Organize credential data into multitude of deprecated fields - # TODO: remove most of this as v1 is removed - vault_credential = None - credential = None extra_creds = [] if obj.pk: for cred in obj.credentials.all(): @@ -3194,30 +2834,12 @@ def get_summary_fields(self, obj): 'kind': cred.kind, 'cloud': cred.credential_type.kind == 'cloud' } - if self.version > 1: - summarized_cred['credential_type_id'] = cred.credential_type_id all_creds.append(summarized_cred) if cred.credential_type.kind in ('cloud', 'net'): extra_creds.append(summarized_cred) - elif summarized_cred['kind'] == 'ssh': - credential = summarized_cred - elif summarized_cred['kind'] == 'vault': - vault_credential = summarized_cred - # Selectively apply those fields, depending on view deetails - if (self.is_detail_view or self.version == 1) and credential: - summary_fields['credential'] = credential - else: - # Credential could be an empty dictionary in this case - summary_fields.pop('credential', None) - if (self.is_detail_view or self.version == 1) and vault_credential: - summary_fields['vault_credential'] = vault_credential - else: - # vault credential could be empty dictionary - summary_fields.pop('vault_credential', None) - if self.version > 1: - if self.is_detail_view: - summary_fields['extra_credentials'] = extra_creds - summary_fields['credentials'] = all_creds + if self.is_detail_view: + summary_fields['extra_credentials'] = extra_creds + summary_fields['credentials'] = all_creds return summary_fields @@ -3250,6 +2872,7 @@ def get_related(self, obj): activity_stream = self.reverse('api:job_activity_stream_list', kwargs={'pk': obj.pk}), notifications = self.reverse('api:job_notifications_list', kwargs={'pk': obj.pk}), labels = self.reverse('api:job_label_list', kwargs={'pk': obj.pk}), + create_schedule = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk}), )) try: if obj.job_template: @@ -3257,8 +2880,6 @@ def get_related(self, obj): kwargs={'pk': obj.job_template.pk}) except ObjectDoesNotExist: setattr(obj, 'job_template', None) - if (obj.can_start or True) and self.version == 1: # TODO: remove in 3.3 - res['start'] = self.reverse('api:job_start', kwargs={'pk': obj.pk}) if obj.can_cancel or True: res['cancel'] = self.reverse('api:job_cancel', kwargs={'pk': obj.pk}) try: @@ -3268,8 +2889,6 @@ def get_related(self, obj): ) except ObjectDoesNotExist: pass - if self.version > 1: - res['create_schedule'] = self.reverse('api:job_create_schedule', kwargs={'pk': obj.pk}) res['relaunch'] = self.reverse('api:job_relaunch', kwargs={'pk': obj.pk}) return res @@ -3320,9 +2939,6 @@ def get_summary_fields(self, obj): summary_fields = super(JobSerializer, self).get_summary_fields(obj) all_creds = [] # Organize credential data into multitude of deprecated fields - # TODO: remove most of this as v1 is removed - vault_credential = None - credential = None extra_creds = [] if obj.pk: for cred in obj.credentials.all(): @@ -3333,30 +2949,12 @@ def get_summary_fields(self, obj): 'kind': cred.kind, 'cloud': cred.credential_type.kind == 'cloud' } - if self.version > 1: - summarized_cred['credential_type_id'] = cred.credential_type_id all_creds.append(summarized_cred) if cred.credential_type.kind in ('cloud', 'net'): extra_creds.append(summarized_cred) - elif summarized_cred['kind'] == 'ssh': - credential = summarized_cred - elif summarized_cred['kind'] == 'vault': - vault_credential = summarized_cred - # Selectively apply those fields, depending on view deetails - if (self.is_detail_view or self.version == 1) and credential: - summary_fields['credential'] = credential - else: - # Credential could be an empty dictionary in this case - summary_fields.pop('credential', None) - if (self.is_detail_view or self.version == 1) and vault_credential: - summary_fields['vault_credential'] = vault_credential - else: - # vault credential could be empty dictionary - summary_fields.pop('vault_credential', None) - if self.version > 1: - if self.is_detail_view: - summary_fields['extra_credentials'] = extra_creds - summary_fields['credentials'] = all_creds + if self.is_detail_view: + summary_fields['extra_credentials'] = extra_creds + summary_fields['credentials'] = all_creds return summary_fields @@ -3696,9 +3294,8 @@ def get_related(self, obj): access_list = self.reverse('api:workflow_job_template_access_list', kwargs={'pk': obj.pk}), object_roles = self.reverse('api:workflow_job_template_object_roles_list', kwargs={'pk': obj.pk}), survey_spec = self.reverse('api:workflow_job_template_survey_spec', kwargs={'pk': obj.pk}), + copy = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}), )) - if self.version > 1: - res['copy'] = self.reverse('api:workflow_job_template_copy', kwargs={'pk': obj.pk}) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) return res @@ -4401,17 +3998,16 @@ def get_defaults(self, obj): name=getattrd(obj, '%s.name' % field_name, None), id=getattrd(obj, '%s.pk' % field_name, None)) elif field_name == 'credentials': - if self.version > 1: - for cred in obj.credentials.all(): - cred_dict = dict( - id=cred.id, - name=cred.name, - credential_type=cred.credential_type.pk, - passwords_needed=cred.passwords_needed - ) - if cred.credential_type.managed_by_tower and 'vault_id' in cred.credential_type.defined_fields: - cred_dict['vault_id'] = cred.get_input('vault_id', default=None) - defaults_dict.setdefault(field_name, []).append(cred_dict) + for cred in obj.credentials.all(): + cred_dict = dict( + id=cred.id, + name=cred.name, + credential_type=cred.credential_type.pk, + passwords_needed=cred.passwords_needed + ) + if cred.credential_type.managed_by_tower and 'vault_id' in cred.credential_type.defined_fields: + cred_dict['vault_id'] = cred.get_input('vault_id', default=None) + defaults_dict.setdefault(field_name, []).append(cred_dict) else: defaults_dict[field_name] = getattr(obj, field_name) return defaults_dict @@ -4584,9 +4180,8 @@ def get_related(self, obj): res.update(dict( test = self.reverse('api:notification_template_test', kwargs={'pk': obj.pk}), notifications = self.reverse('api:notification_template_notification_list', kwargs={'pk': obj.pk}), + copy = self.reverse('api:notification_template_copy', kwargs={'pk': obj.pk}), )) - if self.version > 1: - res['copy'] = self.reverse('api:notification_template_copy', kwargs={'pk': obj.pk}) if obj.organization: res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk}) return res diff --git a/awx/api/templates/api/job_template_callback.md b/awx/api/templates/api/job_template_callback.md index 99ae79b42a37..ef3348b829f6 100644 --- a/awx/api/templates/api/job_template_callback.md +++ b/awx/api/templates/api/job_template_callback.md @@ -8,15 +8,15 @@ job template. For example, using curl: - curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY"}' http://server/api/v1/job_templates/N/callback/ + curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY"}' http://server/api/v2/job_templates/N/callback/ Or using wget: - wget -O /dev/null --post-data='{"host_config_key": "HOST_CONFIG_KEY"}' --header=Content-Type:application/json http://server/api/v1/job_templates/N/callback/ + wget -O /dev/null --post-data='{"host_config_key": "HOST_CONFIG_KEY"}' --header=Content-Type:application/json http://server/api/v2/job_templates/N/callback/ You may also pass `extra_vars` to the callback: - curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY", "extra_vars": {"key": "value"}}' http://server/api/v1/job_templates/N/callback/ + curl -H "Content-Type: application/json" -d '{"host_config_key": "HOST_CONFIG_KEY", "extra_vars": {"key": "value"}}' http://server/api/v2/job_templates/N/callback/ The response will return status 202 if the request is valid, 403 for an invalid host config key, or 400 if the host cannot be determined from the @@ -30,7 +30,7 @@ A GET request may be used to verify that the correct host will be selected. This request must authenticate as a valid user with permission to edit the job template. For example: - curl http://user:password@server/api/v1/job_templates/N/callback/ + curl http://user:password@server/api/v2/job_templates/N/callback/ The response will include the host config key as well as the host name(s) that would match the request: diff --git a/awx/api/templates/api/user_me_list.md b/awx/api/templates/api/user_me_list.md index 3935d23e097e..ebed9cac2c7d 100644 --- a/awx/api/templates/api/user_me_list.md +++ b/awx/api/templates/api/user_me_list.md @@ -6,4 +6,4 @@ One result should be returned containing the following fields: {% include "api/_result_fields_common.md" %} -Use the primary URL for the user (/api/v1/users/N/) to modify the user. +Use the primary URL for the user (/api/v2/users/N/) to modify the user. diff --git a/awx/api/urls/job.py b/awx/api/urls/job.py index ca7d1b2f14e9..de45cba9aa22 100644 --- a/awx/api/urls/job.py +++ b/awx/api/urls/job.py @@ -6,7 +6,6 @@ from awx.api.views import ( JobList, JobDetail, - JobStart, JobCancel, JobRelaunch, JobCreateSchedule, @@ -23,7 +22,6 @@ urls = [ url(r'^$', JobList.as_view(), name='job_list'), url(r'^(?P[0-9]+)/$', JobDetail.as_view(), name='job_detail'), - url(r'^(?P[0-9]+)/start/$', JobStart.as_view(), name='job_start'), # Todo: Remove In 3.3 url(r'^(?P[0-9]+)/cancel/$', JobCancel.as_view(), name='job_cancel'), url(r'^(?P[0-9]+)/relaunch/$', JobRelaunch.as_view(), name='job_relaunch'), url(r'^(?P[0-9]+)/create_schedule/$', JobCreateSchedule.as_view(), name='job_create_schedule'), diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 4a8fb61b1ff6..31eb6b78d0d6 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -11,10 +11,9 @@ ) from awx.api.views import ( ApiRootView, - ApiV1RootView, ApiV2RootView, - ApiV1PingView, - ApiV1ConfigView, + ApiV2PingView, + ApiV2ConfigView, AuthView, UserMeList, DashboardView, @@ -74,10 +73,25 @@ from .oauth2_root import urls as oauth2_root_urls -v1_urls = [ - url(r'^$', ApiV1RootView.as_view(), name='api_v1_root_view'), - url(r'^ping/$', ApiV1PingView.as_view(), name='api_v1_ping_view'), - url(r'^config/$', ApiV1ConfigView.as_view(), name='api_v1_config_view'), +v2_urls = [ + url(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'), + url(r'^credential_types/', include(credential_type_urls)), + url(r'^credential_input_sources/', include(credential_input_source_urls)), + url(r'^hosts/(?P[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'), + url(r'^jobs/(?P[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'), + url(r'^jobs/(?P[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'), + url(r'^job_templates/(?P[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'), + url(r'^job_templates/(?P[0-9]+)/credentials/$', JobTemplateCredentialsList.as_view(), name='job_template_credentials_list'), + url(r'^schedules/preview/$', SchedulePreview.as_view(), name='schedule_rrule'), + url(r'^schedules/zoneinfo/$', ScheduleZoneInfo.as_view(), name='schedule_zoneinfo'), + url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'), + url(r'^applications/(?P[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'), + url(r'^applications/(?P[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'), + url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'), + url(r'^', include(oauth2_urls)), + url(r'^metrics/$', MetricsView.as_view(), name='metrics_view'), + url(r'^ping/$', ApiV2PingView.as_view(), name='api_v2_ping_view'), + url(r'^config/$', ApiV2ConfigView.as_view(), name='api_v2_config_view'), url(r'^auth/$', AuthView.as_view()), url(r'^me/$', UserMeList.as_view(), name='user_me_list'), url(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'), @@ -119,30 +133,10 @@ url(r'^activity_stream/', include(activity_stream_urls)), ] -v2_urls = [ - url(r'^$', ApiV2RootView.as_view(), name='api_v2_root_view'), - url(r'^credential_types/', include(credential_type_urls)), - url(r'^credential_input_sources/', include(credential_input_source_urls)), - url(r'^hosts/(?P[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'), - url(r'^jobs/(?P[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'), - url(r'^jobs/(?P[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'), - url(r'^job_templates/(?P[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'), - url(r'^job_templates/(?P[0-9]+)/credentials/$', JobTemplateCredentialsList.as_view(), name='job_template_credentials_list'), - url(r'^schedules/preview/$', SchedulePreview.as_view(), name='schedule_rrule'), - url(r'^schedules/zoneinfo/$', ScheduleZoneInfo.as_view(), name='schedule_zoneinfo'), - url(r'^applications/$', OAuth2ApplicationList.as_view(), name='o_auth2_application_list'), - url(r'^applications/(?P[0-9]+)/$', OAuth2ApplicationDetail.as_view(), name='o_auth2_application_detail'), - url(r'^applications/(?P[0-9]+)/tokens/$', ApplicationOAuth2TokenList.as_view(), name='application_o_auth2_token_list'), - url(r'^tokens/$', OAuth2TokenList.as_view(), name='o_auth2_token_list'), - url(r'^', include(oauth2_urls)), - url(r'^metrics/$', MetricsView.as_view(), name='metrics_view'), -] - app_name = 'api' urlpatterns = [ url(r'^$', ApiRootView.as_view(), name='api_root_view'), url(r'^(?P(v2))/', include(v2_urls)), - url(r'^(?P(v1|v2))/', include(v1_urls)), url(r'^login/$', LoggedLoginView.as_view( template_name='rest_framework/login.html', extra_context={'inside_login_context': True} diff --git a/awx/api/versioning.py b/awx/api/versioning.py index 4e5d5a9288b8..3ad96388327a 100644 --- a/awx/api/versioning.py +++ b/awx/api/versioning.py @@ -27,19 +27,6 @@ def drf_reverse(viewname, args=None, kwargs=None, request=None, format=None, **e return url -def get_request_version(request): - """ - The API version of a request as an integer i.e., 1 or 2 - """ - version = settings.REST_FRAMEWORK['DEFAULT_VERSION'] - if request and hasattr(request, 'version'): - version = request.version - if version is None: - # For requests to /api/ - return None - return int(version.lstrip('v')) - - def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): if request is None or getattr(request, 'version', None) is None: # We need the "current request" to determine the correct version to diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index e8754af8bf82..c6f51ac4932c 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -62,7 +62,6 @@ # AWX from awx.main.tasks import send_notifications, update_inventory_computed_fields from awx.main.access import get_user_queryset, HostAccess -from awx.api.filters import V1CredentialFilterBackend from awx.api.generics import ( APIView, BaseUsersList, CopyAPIView, DeleteLastUnattachLabelMixin, GenericAPIView, ListAPIView, ListCreateAPIView, @@ -72,7 +71,7 @@ SubListCreateAPIView, SubListCreateAttachDetachAPIView, SubListDestroyAPIView, get_view_name ) -from awx.api.versioning import reverse, get_request_version +from awx.api.versioning import reverse from awx.conf.license import get_license from awx.main import models from awx.main.utils import ( @@ -96,7 +95,7 @@ ) from awx.api import renderers from awx.api import serializers -from awx.api.metadata import RoleMetadata, JobTypeMetadata +from awx.api.metadata import RoleMetadata from awx.main.constants import ACTIVE_STATES from awx.main.scheduler.dag_workflow import WorkflowDAG from awx.api.views.mixin import ( @@ -143,10 +142,9 @@ ApiRootView, ApiOAuthAuthorizationRootView, ApiVersionRootView, - ApiV1RootView, ApiV2RootView, - ApiV1PingView, - ApiV1ConfigView, + ApiV2PingView, + ApiV2ConfigView, ) @@ -1246,22 +1244,10 @@ class CredentialTypeActivityStreamList(SubListAPIView): search_fields = ('changes',) -# remove in 3.3 -class CredentialViewMixin(object): - - @property - def related_search_fields(self): - ret = super(CredentialViewMixin, self).related_search_fields - if get_request_version(self.request) == 1 and 'credential_type__search' in ret: - ret.remove('credential_type__search') - return ret - - -class CredentialList(CredentialViewMixin, ListCreateAPIView): +class CredentialList(ListCreateAPIView): model = models.Credential serializer_class = serializers.CredentialSerializerCreate - filter_backends = ListCreateAPIView.filter_backends + [V1CredentialFilterBackend] class CredentialOwnerUsersList(SubListAPIView): @@ -1289,13 +1275,12 @@ def get_queryset(self): return self.model.objects.filter(pk__in=teams) -class UserCredentialsList(CredentialViewMixin, SubListCreateAPIView): +class UserCredentialsList(SubListCreateAPIView): model = models.Credential serializer_class = serializers.UserCredentialSerializerCreate parent_model = models.User parent_key = 'user' - filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend] def get_queryset(self): user = self.get_parent_object() @@ -1306,13 +1291,12 @@ def get_queryset(self): return user_creds & visible_creds -class TeamCredentialsList(CredentialViewMixin, SubListCreateAPIView): +class TeamCredentialsList(SubListCreateAPIView): model = models.Credential serializer_class = serializers.TeamCredentialSerializerCreate parent_model = models.Team parent_key = 'team' - filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend] def get_queryset(self): team = self.get_parent_object() @@ -1323,13 +1307,12 @@ def get_queryset(self): return (team_creds & visible_creds).distinct() -class OrganizationCredentialList(CredentialViewMixin, SubListCreateAPIView): +class OrganizationCredentialList(SubListCreateAPIView): model = models.Credential serializer_class = serializers.OrganizationCredentialSerializerCreate parent_model = models.Organization parent_key = 'organization' - filter_backends = SubListCreateAPIView.filter_backends + [V1CredentialFilterBackend] def get_queryset(self): organization = self.get_parent_object() @@ -1348,7 +1331,6 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView): model = models.Credential serializer_class = serializers.CredentialSerializer - filter_backends = RetrieveUpdateDestroyAPIView.filter_backends + [V1CredentialFilterBackend] class CredentialActivityStreamList(SubListAPIView): @@ -1754,10 +1736,10 @@ class EnforceParentRelationshipMixin(object): * Tower uses a shallow (2-deep only) url pattern. For example: When an object hangs off of a parent object you would have the url of the - form /api/v1/parent_model/34/child_model. If you then wanted a child of the - child model you would NOT do /api/v1/parent_model/34/child_model/87/child_child_model - Instead, you would access the child_child_model via /api/v1/child_child_model/87/ - and you would create child_child_model's off of /api/v1/child_model/87/child_child_model_set + form /api/v2/parent_model/34/child_model. If you then wanted a child of the + child model you would NOT do /api/v2/parent_model/34/child_model/87/child_child_model + Instead, you would access the child_child_model via /api/v2/child_child_model/87/ + and you would create child_child_model's off of /api/v2/child_model/87/child_child_model_set Now, when creating child_child_model related to child_model you still want to link child_child_model to parent_model. That's what this class is for ''' @@ -1899,11 +1881,6 @@ def destroy(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'delete', obj): raise PermissionDenied() - if get_request_version(request) == 1: # TODO: deletion of automatic inventory_source, remove in 3.3 - try: - obj.deprecated_inventory_source.delete() - except models.Group.deprecated_inventory_source.RelatedObjectDoesNotExist: - pass obj.delete_recursive() return Response(status=status.HTTP_204_NO_CONTENT) @@ -2093,13 +2070,6 @@ class InventorySourceList(ListCreateAPIView): serializer_class = serializers.InventorySourceSerializer always_allow_superuser = False - @property - def allowed_methods(self): - methods = super(InventorySourceList, self).allowed_methods - if get_request_version(getattr(self, 'request', None)) == 1: - methods.remove('POST') - return methods - class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): @@ -2290,7 +2260,6 @@ class InventoryUpdateNotificationsList(SubListAPIView): class JobTemplateList(ListCreateAPIView): model = models.JobTemplate - metadata_class = JobTypeMetadata serializer_class = serializers.JobTemplateSerializer always_allow_superuser = False @@ -2305,7 +2274,6 @@ def post(self, request, *args, **kwargs): class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = models.JobTemplate - metadata_class = JobTypeMetadata serializer_class = serializers.JobTemplateSerializer always_allow_superuser = False @@ -2314,7 +2282,6 @@ class JobTemplateLaunch(RetrieveAPIView): model = models.JobTemplate obj_permission_type = 'start' - metadata_class = JobTypeMetadata serializer_class = serializers.JobLaunchSerializer always_allow_superuser = False @@ -2358,65 +2325,44 @@ def modernize_launch_payload(self, data, obj): ignored_fields = {} modern_data = data.copy() - for fd in ('credential', 'vault_credential', 'inventory'): - id_fd = '{}_id'.format(fd) - if fd not in modern_data and id_fd in modern_data: - modern_data[fd] = modern_data[id_fd] - - # This block causes `extra_credentials` to _always_ raise error if - # the launch endpoint if we're accessing `/api/v1/` - if get_request_version(self.request) == 1 and 'extra_credentials' in modern_data: - raise ParseError({"extra_credentials": _( - "Field is not allowed for use with v1 API." - )}) + id_fd = '{}_id'.format('inventory') + if 'inventory' not in modern_data and id_fd in modern_data: + modern_data['inventory'] = modern_data[id_fd] # Automatically convert legacy launch credential arguments into a list of `.credentials` - if 'credentials' in modern_data and ( - 'credential' in modern_data or - 'vault_credential' in modern_data or - 'extra_credentials' in modern_data - ): + if 'credentials' in modern_data and 'extra_credentials' in modern_data: raise ParseError({"error": _( - "'credentials' cannot be used in combination with 'credential', 'vault_credential', or 'extra_credentials'." + "'credentials' cannot be used in combination with 'extra_credentials'." )}) - if ( - 'credential' in modern_data or - 'vault_credential' in modern_data or - 'extra_credentials' in modern_data - ): + if 'extra_credentials' in modern_data: # make a list of the current credentials existing_credentials = obj.credentials.all() template_credentials = list(existing_credentials) # save copy of existing new_credentials = [] - for key, conditional, _type, type_repr in ( - ('credential', lambda cred: cred.credential_type.kind != 'ssh', int, 'pk value'), - ('vault_credential', lambda cred: cred.credential_type.kind != 'vault', int, 'pk value'), - ('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net'), Iterable, 'a list') - ): - if key in modern_data: - # if a specific deprecated key is specified, remove all - # credentials of _that_ type from the list of current - # credentials - existing_credentials = filter(conditional, existing_credentials) - prompted_value = modern_data.pop(key) - - # validate type, since these are not covered by a serializer - if not isinstance(prompted_value, _type): - msg = _( - "Incorrect type. Expected {}, received {}." - ).format(type_repr, prompted_value.__class__.__name__) - raise ParseError({key: [msg], 'credentials': [msg]}) - - # add the deprecated credential specified in the request - if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str): - prompted_value = [prompted_value] - - # If user gave extra_credentials, special case to use exactly - # the given list without merging with JT credentials - if key == 'extra_credentials' and prompted_value: - obj._deprecated_credential_launch = True # signal to not merge credentials - new_credentials.extend(prompted_value) + if 'extra_credentials' in modern_data: + existing_credentials = [ + cred for cred in existing_credentials + if cred.credential_type.kind not in ('cloud', 'net') + ] + prompted_value = modern_data.pop('extra_credentials') + + # validate type, since these are not covered by a serializer + if not isinstance(prompted_value, Iterable): + msg = _( + "Incorrect type. Expected a list received {}." + ).format(prompted_value.__class__.__name__) + raise ParseError({'extra_credentials': [msg], 'credentials': [msg]}) + + # add the deprecated credential specified in the request + if not isinstance(prompted_value, Iterable) or isinstance(prompted_value, str): + prompted_value = [prompted_value] + + # If user gave extra_credentials, special case to use exactly + # the given list without merging with JT credentials + if prompted_value: + obj._deprecated_credential_launch = True # signal to not merge credentials + new_credentials.extend(prompted_value) # combine the list of "new" and the filtered list of "old" new_credentials.extend([cred.pk for cred in existing_credentials]) @@ -2926,7 +2872,7 @@ def post(self, request, *args, **kwargs): return Response(status=status.HTTP_201_CREATED, headers=headers) -class JobTemplateJobsList(SubListCreateAPIView): +class JobTemplateJobsList(SubListAPIView): model = models.Job serializer_class = serializers.JobListSerializer @@ -2934,13 +2880,6 @@ class JobTemplateJobsList(SubListCreateAPIView): relationship = 'jobs' parent_key = 'job_template' - @property - def allowed_methods(self): - methods = super(JobTemplateJobsList, self).allowed_methods - if get_request_version(getattr(self, 'request', None)) > 1: - methods.remove('POST') - return methods - class JobTemplateSliceWorkflowJobsList(SubListCreateAPIView): @@ -3135,8 +3074,6 @@ class WorkflowJobTemplateCopy(CopyAPIView): copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer def get(self, request, *args, **kwargs): - if get_request_version(request) < 2: - return self.v1_not_allowed() obj = self.get_object() if not request.user.can_access(obj.__class__, 'read', obj): raise PermissionDenied() @@ -3493,56 +3430,17 @@ class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetac relationship = 'notification_templates_success' -class JobList(ListCreateAPIView): +class JobList(ListAPIView): model = models.Job - metadata_class = JobTypeMetadata serializer_class = serializers.JobListSerializer - @property - def allowed_methods(self): - methods = super(JobList, self).allowed_methods - if get_request_version(getattr(self, 'request', None)) > 1: - methods.remove('POST') - return methods - - # NOTE: Remove in 3.3, switch ListCreateAPIView to ListAPIView - def post(self, request, *args, **kwargs): - if get_request_version(self.request) > 1: - return Response({"error": _("POST not allowed for Job launching in version 2 of the api")}, - status=status.HTTP_405_METHOD_NOT_ALLOWED) - return super(JobList, self).post(request, *args, **kwargs) - -class JobDetail(UnifiedJobDeletionMixin, RetrieveUpdateDestroyAPIView): +class JobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.Job - metadata_class = JobTypeMetadata serializer_class = serializers.JobDetailSerializer - # NOTE: When removing the V1 API in 3.4, delete the following four methods, - # and let this class inherit from RetrieveDestroyAPIView instead of - # RetrieveUpdateDestroyAPIView. - @property - def allowed_methods(self): - methods = super(JobDetail, self).allowed_methods - if get_request_version(getattr(self, 'request', None)) > 1: - methods.remove('PUT') - methods.remove('PATCH') - return methods - - def put(self, request, *args, **kwargs): - if get_request_version(self.request) > 1: - return Response({"error": _("PUT not allowed for Job Details in version 2 of the API")}, - status=status.HTTP_405_METHOD_NOT_ALLOWED) - return super(JobDetail, self).put(request, *args, **kwargs) - - def patch(self, request, *args, **kwargs): - if get_request_version(self.request) > 1: - return Response({"error": _("PUT not allowed for Job Details in version 2 of the API")}, - status=status.HTTP_405_METHOD_NOT_ALLOWED) - return super(JobDetail, self).patch(request, *args, **kwargs) - def update(self, request, *args, **kwargs): obj = self.get_object() # Only allow changes (PUT/PATCH) when job status is "new". @@ -3591,44 +3489,6 @@ class JobActivityStreamList(SubListAPIView): search_fields = ('changes',) -# TODO: remove endpoint in 3.3 -class JobStart(GenericAPIView): - - model = models.Job - obj_permission_type = 'start' - serializer_class = serializers.EmptySerializer - deprecated = True - - def v2_not_allowed(self): - return Response({'detail': 'Action only possible through v1 API.'}, - status=status.HTTP_404_NOT_FOUND) - - def get(self, request, *args, **kwargs): - if get_request_version(request) > 1: - return self.v2_not_allowed() - obj = self.get_object() - data = dict( - can_start=obj.can_start, - ) - if obj.can_start: - data['passwords_needed_to_start'] = obj.passwords_needed_to_start - return Response(data) - - def post(self, request, *args, **kwargs): - if get_request_version(request) > 1: - return self.v2_not_allowed() - obj = self.get_object() - if obj.can_start: - result = obj.signal_start(**request.data) - if not result: - data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) - return Response(data, status=status.HTTP_400_BAD_REQUEST) - else: - return Response(status=status.HTTP_202_ACCEPTED) - else: - return self.http_method_not_allowed(request, *args, **kwargs) - - class JobCancel(RetrieveAPIView): model = models.Job diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py index a057c7cdf2c6..15daa552325b 100644 --- a/awx/api/views/inventory.py +++ b/awx/api/views/inventory.py @@ -44,7 +44,6 @@ InstanceGroupSerializer, InventoryUpdateEventSerializer, CustomInventoryScriptSerializer, - InventoryDetailSerializer, JobTemplateSerializer, ) from awx.api.views.mixin import ( @@ -119,7 +118,7 @@ class InventoryList(ListCreateAPIView): class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): model = Inventory - serializer_class = InventoryDetailSerializer + serializer_class = InventorySerializer def update(self, request, *args, **kwargs): obj = self.get_object() diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 0456631ce8c5..2e4312d61381 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -25,7 +25,7 @@ get_custom_venv_choices, to_python_boolean, ) -from awx.api.versioning import reverse, get_request_version, drf_reverse +from awx.api.versioning import reverse, drf_reverse from awx.conf.license import get_license from awx.main.constants import PRIVILEGE_ESCALATION_METHODS from awx.main.models import ( @@ -50,12 +50,11 @@ class ApiRootView(APIView): def get(self, request, format=None): ''' List supported API versions ''' - v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'}) v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'}) data = OrderedDict() data['description'] = _('AWX REST API') data['current_version'] = v2 - data['available_versions'] = dict(v1 = v1, v2 = v2) + data['available_versions'] = dict(v2 = v2) data['oauth2'] = drf_reverse('api:oauth_authorization_root_view') data['custom_logo'] = settings.CUSTOM_LOGO data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO @@ -85,10 +84,10 @@ class ApiVersionRootView(APIView): def get(self, request, format=None): ''' List top level resources ''' data = OrderedDict() - data['ping'] = reverse('api:api_v1_ping_view', request=request) + data['ping'] = reverse('api:api_v2_ping_view', request=request) data['instances'] = reverse('api:instance_list', request=request) data['instance_groups'] = reverse('api:instance_group_list', request=request) - data['config'] = reverse('api:api_v1_config_view', request=request) + data['config'] = reverse('api:api_v2_config_view', request=request) data['settings'] = reverse('api:setting_category_list', request=request) data['me'] = reverse('api:user_me_list', request=request) data['dashboard'] = reverse('api:dashboard_view', request=request) @@ -98,12 +97,11 @@ def get(self, request, format=None): data['project_updates'] = reverse('api:project_update_list', request=request) data['teams'] = reverse('api:team_list', request=request) data['credentials'] = reverse('api:credential_list', request=request) - if get_request_version(request) > 1: - data['credential_types'] = reverse('api:credential_type_list', request=request) - data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request) - data['applications'] = reverse('api:o_auth2_application_list', request=request) - data['tokens'] = reverse('api:o_auth2_token_list', request=request) - data['metrics'] = reverse('api:metrics_view', request=request) + data['credential_types'] = reverse('api:credential_type_list', request=request) + data['credential_input_sources'] = reverse('api:credential_input_source_list', request=request) + data['applications'] = reverse('api:o_auth2_application_list', request=request) + data['tokens'] = reverse('api:o_auth2_token_list', request=request) + data['metrics'] = reverse('api:metrics_view', request=request) data['inventory'] = reverse('api:inventory_list', request=request) data['inventory_scripts'] = reverse('api:inventory_script_list', request=request) data['inventory_sources'] = reverse('api:inventory_source_list', request=request) @@ -131,15 +129,11 @@ def get(self, request, format=None): return Response(data) -class ApiV1RootView(ApiVersionRootView): - view_name = _('Version 1') - - class ApiV2RootView(ApiVersionRootView): view_name = _('Version 2') -class ApiV1PingView(APIView): +class ApiV2PingView(APIView): """A simple view that reports very basic information about this instance, which is acceptable to be public information. """ @@ -174,14 +168,14 @@ def get(self, request, format=None): return Response(response) -class ApiV1ConfigView(APIView): +class ApiV2ConfigView(APIView): permission_classes = (IsAuthenticated,) view_name = _('Configuration') swagger_topic = 'System Configuration' def check_permissions(self, request): - super(ApiV1ConfigView, self).check_permissions(request) + super(ApiV2ConfigView, self).check_permissions(request) if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}: self.permission_denied(request) # Raises PermissionDenied exception. diff --git a/awx/conf/serializers.py b/awx/conf/serializers.py index 3c643d841708..e297fe1e6968 100644 --- a/awx/conf/serializers.py +++ b/awx/conf/serializers.py @@ -88,7 +88,7 @@ def get_fields(self): continue extra_kwargs = {} # Make LICENSE and AWX_ISOLATED_KEY_GENERATION read-only here; - # LICENSE is only updated via /api/v1/config/ + # LICENSE is only updated via /api/v2/config/ # AWX_ISOLATED_KEY_GENERATION is only set/unset via the setup playbook if key in ('LICENSE', 'AWX_ISOLATED_KEY_GENERATION'): extra_kwargs['read_only'] = True diff --git a/awx/conf/tests/functional/test_api.py b/awx/conf/tests/functional/test_api.py index 15789c501b6c..aac1a561273b 100644 --- a/awx/conf/tests/functional/test_api.py +++ b/awx/conf/tests/functional/test_api.py @@ -65,41 +65,6 @@ def test_non_admin_user_does_not_see_categories(api_request, dummy_setting, norm assert not response.data['results'] -@pytest.mark.django_db -@mock.patch( - 'awx.conf.views.VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE', - { - 1: set([]), - 2: set(['foobar']), - } -) -def test_version_specific_category_slug_to_exclude_does_not_show_up(api_request, dummy_setting): - with dummy_setting( - 'FOO_BAR', - field_class=fields.IntegerField, - category='FooBar', - category_slug='foobar' - ): - response = api_request( - 'get', - reverse('api:setting_category_list', - kwargs={'version': 'v2'}) - ) - for item in response.data['results']: - assert item['slug'] != 'foobar' - response = api_request( - 'get', - reverse('api:setting_category_list', - kwargs={'version': 'v1'}) - ) - contains = False - for item in response.data['results']: - if item['slug'] != 'foobar': - contains = True - break - assert contains - - @pytest.mark.django_db def test_setting_singleton_detail_retrieve(api_request, dummy_setting): with dummy_setting( diff --git a/awx/conf/views.py b/awx/conf/views.py index ac704e9f3718..bab468e0ebaa 100644 --- a/awx/conf/views.py +++ b/awx/conf/views.py @@ -24,7 +24,7 @@ RetrieveUpdateDestroyAPIView, ) from awx.api.permissions import IsSuperUser -from awx.api.versioning import reverse, get_request_version +from awx.api.versioning import reverse from awx.main.utils import camelcase_to_underscore from awx.main.utils.handlers import AWXProxyHandler, LoggingConnectivityException from awx.main.tasks import handle_setting_changes @@ -35,13 +35,6 @@ SettingCategory = collections.namedtuple('SettingCategory', ('url', 'slug', 'name')) -VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE = { - 1: set([ - 'named-url', - ]), - 2: set([]), -} - class SettingCategoryList(ListAPIView): @@ -60,8 +53,6 @@ def get_queryset(self): else: categories = {} for category_slug in sorted(categories.keys()): - if category_slug in VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]: - continue url = reverse('api:setting_singleton_detail', kwargs={'category_slug': category_slug}, request=self.request) setting_categories.append(SettingCategory(url, category_slug, categories[category_slug])) return setting_categories @@ -77,8 +68,6 @@ class SettingSingletonDetail(RetrieveUpdateDestroyAPIView): def get_queryset(self): self.category_slug = self.kwargs.get('category_slug', 'all') all_category_slugs = list(settings_registry.get_registered_categories().keys()) - for slug_to_delete in VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)]: - all_category_slugs.remove(slug_to_delete) if self.request.user.is_superuser or getattr(self.request.user, 'is_system_auditor', False): category_slugs = all_category_slugs else: @@ -90,7 +79,6 @@ def get_queryset(self): registered_settings = settings_registry.get_registered_settings( category_slug=self.category_slug, read_only=False, - slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)] ) if self.category_slug == 'user': return Setting.objects.filter(key__in=registered_settings, user=self.request.user) @@ -101,7 +89,6 @@ def get_object(self): settings_qs = self.get_queryset() registered_settings = settings_registry.get_registered_settings( category_slug=self.category_slug, - slugs_to_ignore=VERSION_SPECIFIC_CATEGORIES_TO_EXCLUDE[get_request_version(self.request)] ) all_settings = {} for setting in settings_qs: diff --git a/awx/main/access.py b/awx/main/access.py index 1b3753c412c1..09b16585e5de 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -1001,19 +1001,6 @@ def can_attach(self, obj, sub_obj, relationship, data, def can_delete(self, obj): return bool(obj and self.user in obj.inventory.admin_role) - def can_start(self, obj, validate_license=True): - # TODO: Delete for 3.3, only used by v1 serializer - # Used as another alias to inventory_source start access for user_capabilities - if obj: - try: - return self.user.can_access( - InventorySource, 'start', obj.deprecated_inventory_source, - validate_license=validate_license) - obj.deprecated_inventory_source - except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: - return False - return False - class InventorySourceAccess(NotificationAttachMixin, BaseAccess): ''' @@ -2387,11 +2374,6 @@ def filtered_queryset(self): Q(inventorysource__inventory__id__in=Inventory._accessible_pk_qs( Inventory, self.user, 'read_role'))) - def get_queryset(self): - # TODO: remove after the depreciation of v1 API - qs = super(UnifiedJobTemplateAccess, self).get_queryset() - return qs.exclude(inventorysource__source="") - def can_start(self, obj, validate_license=True): access_class = access_registry[obj.__class__] access_instance = access_class(self.user) diff --git a/awx/main/conf.py b/awx/main/conf.py index 7f7ace83f083..7db0737acfc5 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -118,7 +118,7 @@ def _load_default_license_from_file(): default=_load_default_license_from_file, label=_('License'), help_text=_('The license controls which features and functionality are ' - 'enabled. Use /api/v1/config/ to update or change ' + 'enabled. Use /api/v2/config/ to update or change ' 'the license.'), category=_('System'), category_slug='system', diff --git a/awx/main/fields.py b/awx/main/fields.py index ecd2211711a6..70ee365086ba 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -638,7 +638,7 @@ def validate(self, value, model_instance): v != '$encrypted$', model_instance.pk ]): - if not isinstance(getattr(model_instance, k), str): + if not isinstance(model_instance.inputs.get(k), str): raise django_exceptions.ValidationError( _('secret values must be of type string, not {}').format(type(v).__name__), code='invalid', @@ -704,15 +704,15 @@ def validate(self, value, model_instance): # 'ssh_key_unlock': 'do-you-need-me?', # } # ...we have to fetch the actual key value from the database - if model_instance.pk and model_instance.ssh_key_data == '$encrypted$': - model_instance.ssh_key_data = model_instance.__class__.objects.get( + if model_instance.pk and model_instance.inputs.get('ssh_key_data') == '$encrypted$': + model_instance.inputs['ssh_key_data'] = model_instance.__class__.objects.get( pk=model_instance.pk - ).ssh_key_data + ).inputs.get('ssh_key_data') if model_instance.has_encrypted_ssh_key_data and not value.get('ssh_key_unlock'): errors['ssh_key_unlock'] = [_('must be set when SSH key is encrypted.')] if all([ - model_instance.ssh_key_data, + model_instance.inputs.get('ssh_key_data'), value.get('ssh_key_unlock'), not model_instance.has_encrypted_ssh_key_data ]): diff --git a/awx/main/management/commands/create_preload_data.py b/awx/main/management/commands/create_preload_data.py index 21a07827e569..297622af4677 100644 --- a/awx/main/management/commands/create_preload_data.py +++ b/awx/main/management/commands/create_preload_data.py @@ -34,7 +34,7 @@ def handle(self, *args, **kwargs): scm_update_cache_timeout=0, organization=o) p.save(skip_update=True) - ssh_type = CredentialType.from_v1_kind('ssh') + ssh_type = CredentialType.objects.filter(namespace='ssh').first() c = Credential.objects.create(credential_type=ssh_type, name='Demo Credential', inputs={ diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index c105dd4efcac..974aca40c833 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -16,7 +16,7 @@ Organization, Profile, Team, UserSessionMembership ) from awx.main.models.credential import ( # noqa - Credential, CredentialType, CredentialInputSource, ManagedCredentialType, V1Credential, build_safe_env + Credential, CredentialType, CredentialInputSource, ManagedCredentialType, build_safe_env ) from awx.main.models.projects import Project, ProjectUpdate # noqa from awx.main.models.inventory import ( # noqa @@ -174,9 +174,6 @@ def user_is_in_enterprise_category(user, category): def o_auth2_application_get_absolute_url(self, request=None): - # this page does not exist in v1 - if request.version == 'v1': - return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}) # use default version return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}, request=request) @@ -184,9 +181,6 @@ def o_auth2_application_get_absolute_url(self, request=None): def o_auth2_token_get_absolute_url(self, request=None): - # this page does not exist in v1 - if request.version == 'v1': - return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}) # use default version return reverse('api:o_auth2_token_detail', kwargs={'pk': self.pk}, request=request) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 227766a855a5..e99401ad7c45 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -42,7 +42,7 @@ from awx.main.utils import encrypt_field from . import injectors as builtin_injectors -__all__ = ['Credential', 'CredentialType', 'CredentialInputSource', 'V1Credential', 'build_safe_env'] +__all__ = ['Credential', 'CredentialType', 'CredentialInputSource', 'build_safe_env'] logger = logging.getLogger('awx.main.models.credential') credential_plugins = dict( @@ -73,164 +73,6 @@ def build_safe_env(env): return safe_env -class V1Credential(object): - - # - # API v1 backwards compat; as long as we continue to support the - # /api/v1/credentials/ endpoint, we'll keep these definitions around. - # The credential serializers are smart enough to detect the request - # version and use *these* fields for constructing the serializer if the URL - # starts with /api/v1/ - # - PASSWORD_FIELDS = ('password', 'security_token', 'ssh_key_data', - 'ssh_key_unlock', 'become_password', - 'vault_password', 'secret', 'authorize_password') - KIND_CHOICES = [ - ('ssh', 'Machine'), - ('net', 'Network'), - ('scm', 'Source Control'), - ('aws', 'Amazon Web Services'), - ('vmware', 'VMware vCenter'), - ('satellite6', 'Red Hat Satellite 6'), - ('cloudforms', 'Red Hat CloudForms'), - ('gce', 'Google Compute Engine'), - ('azure_rm', 'Microsoft Azure Resource Manager'), - ('openstack', 'OpenStack'), - ('rhv', 'Red Hat Virtualization'), - ('insights', 'Insights'), - ('tower', 'Ansible Tower'), - ] - FIELDS = { - 'kind': models.CharField( - max_length=32, - choices=[ - (kind[0], _(kind[1])) - for kind in KIND_CHOICES - ], - default='ssh', - ), - 'cloud': models.BooleanField( - default=False, - editable=False, - ), - 'host': models.CharField( - blank=True, - default='', - max_length=1024, - verbose_name=_('Host'), - help_text=_('The hostname or IP address to use.'), - ), - 'username': models.CharField( - blank=True, - default='', - max_length=1024, - verbose_name=_('Username'), - help_text=_('Username for this credential.'), - ), - 'password': models.CharField( - blank=True, - default='', - max_length=1024, - verbose_name=_('Password'), - help_text=_('Password for this credential (or "ASK" to prompt the ' - 'user for machine credentials).'), - ), - 'security_token': models.CharField( - blank=True, - default='', - max_length=1024, - verbose_name=_('Security Token'), - help_text=_('Security Token for this credential'), - ), - 'project': models.CharField( - blank=True, - default='', - max_length=100, - verbose_name=_('Project'), - help_text=_('The identifier for the project.'), - ), - 'domain': models.CharField( - blank=True, - default='', - max_length=100, - verbose_name=_('Domain'), - help_text=_('The identifier for the domain.'), - ), - 'ssh_key_data': models.TextField( - blank=True, - default='', - verbose_name=_('SSH private key'), - help_text=_('RSA or DSA private key to be used instead of password.'), - ), - 'ssh_key_unlock': models.CharField( - max_length=1024, - blank=True, - default='', - verbose_name=_('SSH key unlock'), - help_text=_('Passphrase to unlock SSH private key if encrypted (or ' - '"ASK" to prompt the user for machine credentials).'), - ), - 'become_method': models.CharField( - max_length=32, - blank=True, - default='', - help_text=_('Privilege escalation method.') - ), - 'become_username': models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Privilege escalation username.'), - ), - 'become_password': models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Password for privilege escalation method.') - ), - 'vault_password': models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Vault password (or "ASK" to prompt the user).'), - ), - 'authorize': models.BooleanField( - default=False, - help_text=_('Whether to use the authorize mechanism.'), - ), - 'authorize_password': models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Password used by the authorize mechanism.'), - ), - 'client': models.CharField( - max_length=128, - blank=True, - default='', - help_text=_('Client Id or Application Id for the credential'), - ), - 'secret': models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Secret Token for this credential'), - ), - 'subscription': models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Subscription identifier for this credential'), - ), - 'tenant': models.CharField( - max_length=1024, - blank=True, - default='', - help_text=_('Tenant identifier for this credential'), - ) - } - - class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin): ''' A credential contains information about how to talk to a remote resource @@ -286,34 +128,9 @@ class Meta: 'admin_role', ]) - def __getattr__(self, item): - if item != 'inputs': - if item in V1Credential.FIELDS: - return self.inputs.get(item, V1Credential.FIELDS[item].default) - elif item in self.inputs: - return self.inputs[item] - raise AttributeError(item) - - def __setattr__(self, item, value): - if item in V1Credential.FIELDS and item in self.credential_type.defined_fields: - if value: - self.inputs[item] = value - elif item in self.inputs: - del self.inputs[item] - return - super(Credential, self).__setattr__(item, value) - @property def kind(self): - # TODO 3.3: remove the need for this helper property by removing its - # usage throughout the codebase - type_ = self.credential_type - if type_.kind != 'cloud': - return type_.kind - for field in V1Credential.KIND_CHOICES: - kind, name = field - if name == type_.name: - return kind + return self.credential_type.namespace @property def cloud(self): @@ -330,7 +147,7 @@ def get_absolute_url(self, request=None): # @property def needs_ssh_password(self): - return self.credential_type.kind == 'ssh' and self.password == 'ASK' + return self.credential_type.kind == 'ssh' and self.inputs.get('password') == 'ASK' @property def has_encrypted_ssh_key_data(self): @@ -350,17 +167,17 @@ def has_encrypted_ssh_key_data(self): @property def needs_ssh_key_unlock(self): - if self.credential_type.kind == 'ssh' and self.ssh_key_unlock in ('ASK', ''): + if self.credential_type.kind == 'ssh' and self.inputs.get('ssh_key_unlock') in ('ASK', ''): return self.has_encrypted_ssh_key_data return False @property def needs_become_password(self): - return self.credential_type.kind == 'ssh' and self.become_password == 'ASK' + return self.credential_type.kind == 'ssh' and self.inputs.get('become_password') == 'ASK' @property def needs_vault_password(self): - return self.credential_type.kind == 'vault' and self.vault_password == 'ASK' + return self.credential_type.kind == 'vault' and self.inputs.get('vault_password') == 'ASK' @property def passwords_needed(self): @@ -396,6 +213,10 @@ def save(self, *args, **kwargs): super(Credential, self).save(*args, **kwargs) + def mark_field_for_save(self, update_fields, field): + if 'inputs' not in update_fields: + update_fields.append('inputs') + def encrypt_field(self, field, ask): if field not in self.inputs: return None @@ -405,13 +226,6 @@ def encrypt_field(self, field, ask): elif field in self.inputs: del self.inputs[field] - def mark_field_for_save(self, update_fields, field): - if field in self.credential_type.secret_fields: - # If we've encrypted a v1 field, we actually want to persist - # self.inputs - field = 'inputs' - super(Credential, self).mark_field_for_save(update_fields, field) - def display_inputs(self): field_val = self.inputs.copy() for k, v in field_val.items(): @@ -429,7 +243,7 @@ def unique_hash(self, display=False): type_alias = self.credential_type.name else: type_alias = self.credential_type_id - if self.kind == 'vault' and self.has_input('vault_id'): + if self.credential_type.kind == 'vault' and self.has_input('vault_id'): if display: fmt_str = '{} (id={})' else: @@ -456,7 +270,7 @@ def get_input(self, field_name, **kwargs): :param field_name(str): The name of the input field. :param default(optional[str]): A default return value to use. """ - if self.kind != 'external' and field_name in self.dynamic_input_fields: + if self.credential_type.kind != 'external' and field_name in self.dynamic_input_fields: return self._get_dynamic_input(field_name) if field_name in self.credential_type.secret_fields: try: @@ -552,15 +366,8 @@ def from_db(cls, db, field_names, values): return instance def get_absolute_url(self, request=None): - # Page does not exist in API v1 - if request.version == 'v1': - return reverse('api:credential_type_detail', kwargs={'pk': self.pk}) return reverse('api:credential_type_detail', kwargs={'pk': self.pk}, request=request) - @property - def unique_by_kind(self): - return self.kind != 'cloud' - @property def defined_fields(self): return [field.get('id') for field in self.inputs.get('fields', [])] @@ -629,29 +436,6 @@ def load_plugin(cls, ns, plugin): inputs=plugin.inputs ) - @classmethod - def from_v1_kind(cls, kind, data={}): - match = None - kind = kind or 'ssh' - kind_choices = dict(V1Credential.KIND_CHOICES) - requirements = {} - if kind == 'ssh': - if data.get('vault_password'): - requirements['kind'] = 'vault' - else: - requirements['kind'] = 'ssh' - elif kind in ('net', 'scm', 'insights'): - requirements['kind'] = kind - elif kind in kind_choices: - requirements.update(dict( - kind='cloud', - name=kind_choices[kind] - )) - if requirements: - requirements['managed_by_tower'] = True - match = cls.objects.filter(**requirements)[:1].get() - return match - def inject_credential(self, credential, env, safe_env, args, private_data_dir): """ Inject credential data into the environment variables and arguments @@ -678,9 +462,11 @@ def inject_credential(self, credential, env, safe_env, args, private_data_dir): files) """ if not self.injectors: - if self.managed_by_tower and credential.kind in dir(builtin_injectors): + if self.managed_by_tower and credential.credential_type.namespace in dir(builtin_injectors): injected_env = {} - getattr(builtin_injectors, credential.kind)(credential, injected_env, private_data_dir) + getattr(builtin_injectors, credential.credential_type.namespace)( + credential, injected_env, private_data_dir + ) env.update(injected_env) safe_env.update(build_safe_env(injected_env)) return @@ -1335,12 +1121,12 @@ class Meta: ) def clean_target_credential(self): - if self.target_credential.kind == 'external': + if self.target_credential.credential_type.kind == 'external': raise ValidationError(_('Target must be a non-external credential')) return self.target_credential def clean_source_credential(self): - if self.source_credential.kind != 'external': + if self.source_credential.credential_type.kind != 'external': raise ValidationError(_('Source must be an external credential')) return self.source_credential diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 27b4b28394c5..b8f2661c97f5 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -18,7 +18,7 @@ from django.utils.encoding import smart_str from django.utils.timezone import now from django.utils.translation import ugettext_lazy as _ -from django.core.exceptions import ValidationError, FieldDoesNotExist +from django.core.exceptions import FieldDoesNotExist # REST Framework from rest_framework.exceptions import ParseError @@ -152,21 +152,9 @@ class Meta: extra_vars_dict = VarsDictProperty('extra_vars', True) - def clean_credential(self): - cred = self.credential - if cred and cred.kind != 'ssh': - raise ValidationError( - _('You must provide an SSH credential.'), - ) - return cred - - def clean_vault_credential(self): - cred = self.vault_credential - if cred and cred.kind != 'vault': - raise ValidationError( - _('You must provide a Vault credential.'), - ) - return cred + @property + def machine_credential(self): + return self.credentials.filter(credential_type__kind='ssh').first() @property def network_credentials(self): @@ -180,41 +168,6 @@ def cloud_credentials(self): def vault_credentials(self): return list(self.credentials.filter(credential_type__kind='vault')) - @property - def credential(self): - cred = self.get_deprecated_credential('ssh') - if cred is not None: - return cred.pk - - @property - def vault_credential(self): - cred = self.get_deprecated_credential('vault') - if cred is not None: - return cred.pk - - def get_deprecated_credential(self, kind): - for cred in self.credentials.all(): - if cred.credential_type.kind == kind: - return cred - else: - return None - - # TODO: remove when API v1 is removed - @property - def cloud_credential(self): - try: - return self.cloud_credentials[-1].pk - except IndexError: - return None - - # TODO: remove when API v1 is removed - @property - def network_credential(self): - try: - return self.network_credentials[-1].pk - except IndexError: - return None - @property def passwords_needed_to_start(self): '''Return list of password field names needed to start the job.''' @@ -707,7 +660,7 @@ def notification_data(self, block=5): data.update(dict(inventory=self.inventory.name if self.inventory else None, project=self.project.name if self.project else None, playbook=self.playbook, - credential=getattr(self.get_deprecated_credential('ssh'), 'name', None), + credential=getattr(self.machine_credential, 'name', None), limit=self.limit, extra_vars=self.display_extra_vars(), hosts=all_hosts)) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 0321cf910fba..662a93cd7921 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -794,7 +794,7 @@ def build_private_data_files(self, instance, private_data_dir): data += '\n' # For credentials used with ssh-add, write to a named pipe which # will be read then closed, instead of leaving the SSH key on disk. - if credential and credential.kind in ('ssh', 'scm') and not ssh_too_old: + if credential and credential.credential_type.namespace in ('ssh', 'scm') and not ssh_too_old: try: os.mkdir(os.path.join(private_data_dir, 'env')) except OSError as e: @@ -1324,7 +1324,7 @@ def build_passwords(self, job, runtime_passwords): and ansible-vault. ''' passwords = super(RunJob, self).build_passwords(job, runtime_passwords) - cred = job.get_deprecated_credential('ssh') + cred = job.machine_credential if cred: for field in ('ssh_key_unlock', 'ssh_password', 'become_password', 'vault_password'): value = runtime_passwords.get(field, cred.get_input('password' if field == 'ssh_password' else field, default='')) @@ -1408,6 +1408,9 @@ def build_env(self, job, private_data_dir, isolated=False, private_data_files=No # Set environment variables for cloud credentials. cred_files = private_data_files.get('credentials', {}) + for cloud_cred in job.cloud_credentials: + if cloud_cred and cloud_cred.credential_type.namespace == 'openstack': + env['OS_CLIENT_CONFIG_FILE'] = cred_files.get(cloud_cred, '') for network_cred in job.network_credentials: env['ANSIBLE_NET_USERNAME'] = network_cred.get_input('username', default='') @@ -1429,7 +1432,7 @@ def build_args(self, job, private_data_dir, passwords): Build command line argument list for running ansible-playbook, optionally using ssh-agent for public/private key authentication. ''' - creds = job.get_deprecated_credential('ssh') + creds = job.machine_credential ssh_username, become_username, become_method = '', '', '' if creds: @@ -2226,9 +2229,9 @@ def build_args(self, ad_hoc_command, private_data_dir, passwords): creds = ad_hoc_command.credential ssh_username, become_username, become_method = '', '', '' if creds: - ssh_username = creds.username - become_method = creds.become_method - become_username = creds.become_username + ssh_username = creds.get_input('username', default='') + become_method = creds.get_input('become_method', default='') + become_username = creds.get_input('become_username', default='') else: become_method = None become_username = "" diff --git a/awx/main/tests/factories/fixtures.py b/awx/main/tests/factories/fixtures.py index 2f3ec0656f3b..d4ad255e4a47 100644 --- a/awx/main/tests/factories/fixtures.py +++ b/awx/main/tests/factories/fixtures.py @@ -168,7 +168,7 @@ def mk_job_template(name, job_type='run', if persisted and credential: jt.save() jt.credentials.add(credential) - if jt.credential is None: + if jt.machine_credential is None: jt.ask_credential_on_launch = True jt.project = project diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index 879a3e4de6b1..31d2c444f0f4 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -1,4 +1,3 @@ -import itertools import re from unittest import mock # noqa @@ -27,197 +26,6 @@ def test_idempotent_credential_type_setup(): assert CredentialType.objects.count() == total -@pytest.mark.django_db -@pytest.mark.parametrize('kind, total', [ - ('ssh', 1), ('net', 0) -]) -def test_filter_by_v1_kind(get, admin, organization, kind, total): - CredentialType.setup_tower_managed_defaults() - cred = Credential( - credential_type=CredentialType.from_v1_kind('ssh'), - name='Best credential ever', - organization=organization, - inputs={ - 'username': u'jim', - 'password': u'secret' - } - ) - cred.save() - - response = get( - reverse('api:credential_list', kwargs={'version': 'v1'}), - admin, - QUERY_STRING='kind=%s' % kind - ) - assert response.status_code == 200 - assert response.data['count'] == total - - -@pytest.mark.django_db -def test_filter_by_v1_kind_with_vault(get, admin, organization): - CredentialType.setup_tower_managed_defaults() - cred = Credential( - credential_type=CredentialType.objects.get(kind='ssh'), - name='Best credential ever', - organization=organization, - inputs={ - 'username': u'jim', - 'password': u'secret' - } - ) - cred.save() - cred = Credential( - credential_type=CredentialType.objects.get(kind='vault'), - name='Best credential ever', - organization=organization, - inputs={ - 'vault_password': u'vault!' - } - ) - cred.save() - - response = get( - reverse('api:credential_list', kwargs={'version': 'v1'}), - admin, - QUERY_STRING='kind=ssh' - ) - assert response.status_code == 200 - assert response.data['count'] == 2 - - -@pytest.mark.django_db -def test_insights_credentials_in_v1_api_list(get, admin, organization): - credential_type = CredentialType.defaults['insights']() - credential_type.save() - cred = Credential( - credential_type=credential_type, - name='Best credential ever', - organization=organization, - inputs={ - 'username': u'joe', - 'password': u'secret' - } - ) - cred.save() - - response = get( - reverse('api:credential_list', kwargs={'version': 'v1'}), - admin - ) - assert response.status_code == 200 - assert response.data['count'] == 1 - cred = response.data['results'][0] - assert cred['kind'] == 'insights' - assert cred['username'] == 'joe' - assert cred['password'] == '$encrypted$' - - -@pytest.mark.django_db -def test_create_insights_credentials_in_v1(get, post, admin, organization): - credential_type = CredentialType.defaults['insights']() - credential_type.save() - - response = post( - reverse('api:credential_list', kwargs={'version': 'v1'}), - { - 'name': 'Best Credential Ever', - 'organization': organization.id, - 'kind': 'insights', - 'username': 'joe', - 'password': 'secret' - }, - admin - ) - assert response.status_code == 201 - cred = Credential.objects.get(pk=response.data['id']) - assert cred.username == 'joe' - assert decrypt_field(cred, 'password') == 'secret' - assert cred.credential_type == credential_type - - -@pytest.mark.django_db -def test_custom_credentials_not_in_v1_api_list(get, admin, organization): - """ - 'Custom' credentials (those not managed by Tower) shouldn't be visible from - the V1 credentials API list - """ - credential_type = CredentialType( - kind='cloud', - name='MyCloud', - inputs = { - 'fields': [{ - 'id': 'password', - 'label': 'Password', - 'type': 'string', - 'secret': True - }] - } - ) - credential_type.save() - cred = Credential( - credential_type=credential_type, - name='Best credential ever', - organization=organization, - inputs={ - 'password': u'secret' - } - ) - cred.save() - - response = get( - reverse('api:credential_list', kwargs={'version': 'v1'}), - admin - ) - assert response.status_code == 200 - assert response.data['count'] == 0 - - -@pytest.mark.django_db -def test_custom_credentials_not_in_v1_api_detail(get, admin, organization): - """ - 'Custom' credentials (those not managed by Tower) shouldn't be visible from - the V1 credentials API detail - """ - credential_type = CredentialType( - kind='cloud', - name='MyCloud', - inputs = { - 'fields': [{ - 'id': 'password', - 'label': 'Password', - 'type': 'string', - 'secret': True - }] - } - ) - credential_type.save() - cred = Credential( - credential_type=credential_type, - name='Best credential ever', - organization=organization, - inputs={ - 'password': u'secret' - } - ) - cred.save() - - response = get( - reverse('api:credential_detail', kwargs={'version': 'v1', 'pk': cred.pk}), - admin - ) - assert response.status_code == 404 - - -@pytest.mark.django_db -def test_filter_by_v1_invalid_kind(get, admin, organization): - response = get( - reverse('api:credential_list', kwargs={'version': 'v1'}), - admin, - QUERY_STRING='kind=bad_kind' - ) - assert response.status_code == 400 - - # # user credential creation # @@ -225,7 +33,6 @@ def test_filter_by_v1_invalid_kind(get, admin, organization): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_user_credential_via_credentials_list(post, get, alice, credentialtype_ssh, version, params): @@ -245,7 +52,6 @@ def test_create_user_credential_via_credentials_list(post, get, alice, credentia @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_credential_validation_error_with_bad_user(post, admin, version, credentialtype_ssh, params): @@ -262,7 +68,6 @@ def test_credential_validation_error_with_bad_user(post, admin, version, credent @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_user_credential_via_user_credentials_list(post, get, alice, credentialtype_ssh, version, params): @@ -282,7 +87,6 @@ def test_create_user_credential_via_user_credentials_list(post, get, alice, cred @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_user_credential_via_credentials_list_xfail(post, alice, bob, version, params): @@ -298,7 +102,6 @@ def test_create_user_credential_via_credentials_list_xfail(post, alice, bob, ver @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob, version, params): @@ -319,7 +122,6 @@ def test_create_user_credential_via_user_credentials_list_xfail(post, alice, bob @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_team_credential(post, get, team, organization, org_admin, team_member, credentialtype_ssh, version, params): @@ -345,7 +147,6 @@ def test_create_team_credential(post, get, team, organization, org_admin, team_m @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_team_credential_via_team_credentials_list(post, get, team, org_admin, team_member, credentialtype_ssh, version, params): @@ -368,7 +169,6 @@ def test_create_team_credential_via_team_credentials_list(post, get, team, org_a @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_team_credential_by_urelated_user_xfail(post, team, organization, alice, team_member, version, params): @@ -385,7 +185,6 @@ def test_create_team_credential_by_urelated_user_xfail(post, team, organization, @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_team_credential_by_team_member_xfail(post, team, organization, alice, team_member, version, params): @@ -407,7 +206,7 @@ def test_create_team_credential_by_team_member_xfail(post, team, organization, a @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_org_credential_to_org_user_through_role_users(post, credential, organization, org_admin, org_member, version): credential.organization = organization credential.save() @@ -418,7 +217,7 @@ def test_grant_org_credential_to_org_user_through_role_users(post, credential, o @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_org_credential_to_org_user_through_user_roles(post, credential, organization, org_admin, org_member, version): credential.organization = organization credential.save() @@ -429,7 +228,7 @@ def test_grant_org_credential_to_org_user_through_user_roles(post, credential, o @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_org_credential_to_non_org_user_through_role_users(post, credential, organization, org_admin, alice, version): credential.organization = organization credential.save() @@ -440,7 +239,7 @@ def test_grant_org_credential_to_non_org_user_through_role_users(post, credentia @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_org_credential_to_non_org_user_through_user_roles(post, credential, organization, org_admin, alice, version): credential.organization = organization credential.save() @@ -451,7 +250,7 @@ def test_grant_org_credential_to_non_org_user_through_user_roles(post, credentia @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_private_credential_to_user_through_role_users(post, credential, alice, bob, version): # normal users can't do this credential.admin_role.members.add(alice) @@ -462,7 +261,7 @@ def test_grant_private_credential_to_user_through_role_users(post, credential, a @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_private_credential_to_org_user_through_role_users(post, credential, org_admin, org_member, version): # org admins can't either credential.admin_role.members.add(org_admin) @@ -473,7 +272,7 @@ def test_grant_private_credential_to_org_user_through_role_users(post, credentia @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_sa_grant_private_credential_to_user_through_role_users(post, credential, admin, bob, version): # but system admins can response = post(reverse('api:role_users_list', kwargs={'version': version, 'pk': credential.use_role.id}), { @@ -483,7 +282,7 @@ def test_sa_grant_private_credential_to_user_through_role_users(post, credential @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_private_credential_to_user_through_user_roles(post, credential, alice, bob, version): # normal users can't do this credential.admin_role.members.add(alice) @@ -494,7 +293,7 @@ def test_grant_private_credential_to_user_through_user_roles(post, credential, a @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_private_credential_to_org_user_through_user_roles(post, credential, org_admin, org_member, version): # org admins can't either credential.admin_role.members.add(org_admin) @@ -505,7 +304,7 @@ def test_grant_private_credential_to_org_user_through_user_roles(post, credentia @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_sa_grant_private_credential_to_user_through_user_roles(post, credential, admin, bob, version): # but system admins can response = post(reverse('api:user_roles_list', kwargs={'version': version, 'pk': bob.id}), { @@ -515,7 +314,7 @@ def test_sa_grant_private_credential_to_user_through_user_roles(post, credential @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_org_credential_to_team_through_role_teams(post, credential, organization, org_admin, org_auditor, team, version): assert org_auditor not in credential.read_role credential.organization = organization @@ -528,7 +327,7 @@ def test_grant_org_credential_to_team_through_role_teams(post, credential, organ @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_grant_org_credential_to_team_through_team_roles(post, credential, organization, org_admin, org_auditor, team, version): assert org_auditor not in credential.read_role credential.organization = organization @@ -541,7 +340,7 @@ def test_grant_org_credential_to_team_through_team_roles(post, credential, organ @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_sa_grant_private_credential_to_team_through_role_teams(post, credential, admin, team, version): # not even a system admin can grant a private cred to a team though response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': credential.use_role.id}), { @@ -551,7 +350,7 @@ def test_sa_grant_private_credential_to_team_through_role_teams(post, credential @pytest.mark.django_db -@pytest.mark.parametrize('version', ['v1', 'v2']) +@pytest.mark.parametrize('version', ['v2']) def test_sa_grant_private_credential_to_team_through_team_roles(post, credential, admin, team, version): # not even a system admin can grant a private cred to a team though response = post(reverse('api:role_teams_list', kwargs={'version': version, 'pk': team.id}), { @@ -567,7 +366,6 @@ def test_sa_grant_private_credential_to_team_through_team_roles(post, credential @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_org_credential_as_not_admin(post, organization, org_member, credentialtype_ssh, version, params): @@ -583,7 +381,6 @@ def test_create_org_credential_as_not_admin(post, organization, org_member, cred @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_create_org_credential_as_admin(post, organization, org_admin, credentialtype_ssh, version, params): @@ -599,7 +396,6 @@ def test_create_org_credential_as_admin(post, organization, org_admin, credentia @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_credential_detail(post, get, organization, org_admin, credentialtype_ssh, version, params): @@ -624,7 +420,6 @@ def test_credential_detail(post, get, organization, org_admin, credentialtype_ss @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'username': 'someusername'}], ['v2', {'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) def test_list_created_org_credentials(post, get, organization, org_admin, org_member, credentialtype_ssh, version, params): @@ -667,12 +462,11 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me @pytest.mark.parametrize('order_by', ('password', '-password', 'password,pk', '-password,pk')) -@pytest.mark.parametrize('version', ('v1', 'v2')) @pytest.mark.django_db -def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by, version): +def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by): for i, password in enumerate(('abc', 'def', 'xyz')): response = post( - reverse('api:credential_list', kwargs={'version': version}), + reverse('api:credential_list', kwargs={'version': 'v2'}), { 'organization': organization.id, 'name': 'C%d' % i, @@ -682,7 +476,7 @@ def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin ) response = get( - reverse('api:credential_list', kwargs={'version': version}), + reverse('api:credential_list', kwargs={'version': 'v2'}), org_admin, QUERY_STRING='order_by=%s' % order_by, status=400 @@ -690,22 +484,6 @@ def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin assert response.status_code == 400 -@pytest.mark.django_db -def test_v1_credential_kind_validity(get, post, organization, admin, credentialtype_ssh): - params = { - 'name': 'Best credential ever', - 'organization': organization.id, - 'kind': 'nonsense' - } - response = post( - reverse('api:credential_list', kwargs={'version': 'v1'}), - params, - admin - ) - assert response.status_code == 400 - assert response.data['kind'] == ['"nonsense" is not a valid choice'] - - @pytest.mark.django_db def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, credentialtype_ssh): params = { @@ -725,34 +503,6 @@ def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, cred assert "'invalid_field' was unexpected" in response.data['inputs'][0] -@pytest.mark.django_db -@pytest.mark.parametrize('field_name, field_value', itertools.product( - ['username', 'password', 'ssh_key_data', 'become_method', 'become_username', 'become_password'], # noqa - ['', None] -)) -def test_nullish_field_data(get, post, organization, admin, field_name, field_value): - ssh = CredentialType.defaults['ssh']() - ssh.save() - params = { - 'name': 'Best credential ever', - 'credential_type': ssh.pk, - 'organization': organization.id, - 'inputs': { - field_name: field_value - } - } - response = post( - reverse('api:credential_list', kwargs={'version': 'v2'}), - params, - admin - ) - assert response.status_code == 201 - - assert Credential.objects.count() == 1 - cred = Credential.objects.all()[:1].get() - assert getattr(cred, field_name) == '' - - @pytest.mark.django_db @pytest.mark.parametrize('field_value', ['', None, False]) def test_falsey_field_data(get, post, organization, admin, field_value): @@ -776,7 +526,7 @@ def test_falsey_field_data(get, post, organization, admin, field_value): assert Credential.objects.count() == 1 cred = Credential.objects.all()[:1].get() - assert cred.authorize is False + assert cred.inputs['authorize'] is False @pytest.mark.django_db @@ -811,14 +561,6 @@ def test_field_dependencies(get, post, organization, admin, kind, extraneous): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'scm', - 'name': 'Best credential ever', - 'username': 'some_username', - 'password': 'some_password', - 'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY, - 'ssh_key_unlock': 'some_key_unlock', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -851,12 +593,6 @@ def test_scm_create_ok(post, organization, admin, version, params): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'ssh', - 'name': 'Best credential ever', - 'password': 'secret', - 'vault_password': '', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -882,38 +618,11 @@ def test_ssh_create_ok(post, organization, admin, version, params): assert decrypt_field(cred, 'password') == 'secret' -@pytest.mark.django_db -def test_v1_ssh_vault_ambiguity(post, organization, admin): - vault = CredentialType.defaults['vault']() - vault.save() - params = { - 'organization': organization.id, - 'kind': 'ssh', - 'name': 'Best credential ever', - 'username': 'joe', - 'password': 'secret', - 'ssh_key_data': 'some_key_data', - 'ssh_key_unlock': 'some_key_unlock', - 'vault_password': 'vault_password', - } - response = post( - reverse('api:credential_list', kwargs={'version': 'v1'}), - params, - admin - ) - assert response.status_code == 400 - - # # Vault Credentials # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'ssh', - 'name': 'Best credential ever', - 'vault_password': 'some_password', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -968,16 +677,6 @@ def test_vault_password_required(post, organization, admin): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'net', - 'name': 'Best credential ever', - 'username': 'some_username', - 'password': 'some_password', - 'ssh_key_data': EXAMPLE_ENCRYPTED_PRIVATE_KEY, - 'ssh_key_unlock': 'some_key_unlock', - 'authorize': True, - 'authorize_password': 'some_authorize_password', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1017,13 +716,6 @@ def test_net_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'cloudforms', - 'name': 'Best credential ever', - 'host': 'some_host', - 'username': 'some_username', - 'password': 'some_password', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1057,13 +749,6 @@ def test_cloudforms_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'gce', - 'name': 'Best credential ever', - 'username': 'some_username', - 'project': 'some_project', - 'ssh_key_data': EXAMPLE_PRIVATE_KEY, - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1097,16 +782,6 @@ def test_gce_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'azure_rm', - 'name': 'Best credential ever', - 'subscription': 'some_subscription', - 'username': 'some_username', - 'password': 'some_password', - 'client': 'some_client', - 'secret': 'some_secret', - 'tenant': 'some_tenant' - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1146,13 +821,6 @@ def test_azure_rm_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'satellite6', - 'name': 'Best credential ever', - 'host': 'some_host', - 'username': 'some_username', - 'password': 'some_password', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1186,13 +854,6 @@ def test_satellite6_create_ok(post, organization, admin, version, params): # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'aws', - 'name': 'Best credential ever', - 'username': 'some_username', - 'password': 'some_password', - 'security_token': 'abc123' - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1223,10 +884,6 @@ def test_aws_create_ok(post, organization, admin, version, params): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'aws', - 'name': 'Best credential ever', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1258,13 +915,6 @@ def test_aws_create_fail_required_fields(post, organization, admin, version, par # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'vmware', - 'host': 'some_host', - 'name': 'Best credential ever', - 'username': 'some_username', - 'password': 'some_password' - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1295,10 +945,6 @@ def test_vmware_create_ok(post, organization, admin, version, params): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'vmware', - 'name': 'Best credential ever', - }], ['v2', { 'credential_type': 1, 'name': 'Best credential ever', @@ -1330,12 +976,6 @@ def test_vmware_create_fail_required_fields(post, organization, admin, version, # @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'username': 'some_user', - 'password': 'some_password', - 'project': 'some_project', - 'host': 'some_host', - }], ['v2', { 'credential_type': 1, 'inputs': { @@ -1396,7 +1036,6 @@ def test_openstack_verify_ssl(get, post, organization, admin, verify_ssl, expect @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {}], ['v2', { 'credential_type': 1, 'inputs': {} @@ -1425,12 +1064,6 @@ def test_openstack_create_fail_required_fields(post, organization, admin, versio @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'name': 'Best credential ever', - 'kind': 'ssh', - 'username': 'joe', - 'password': '', - }], ['v2', { 'name': 'Best credential ever', 'credential_type': 1, @@ -1624,12 +1257,6 @@ def _change_credential_type(): @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'name': 'Best credential ever', - 'kind': 'ssh', - 'username': 'joe', - 'ssh_key_data': '$encrypted$', - }], ['v2', { 'name': 'Best credential ever', 'credential_type': 1, @@ -1664,13 +1291,6 @@ def test_ssh_unlock_needed(put, organization, admin, credentialtype_ssh, version @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'name': 'Best credential ever', - 'kind': 'ssh', - 'username': 'joe', - 'ssh_key_data': '$encrypted$', - 'ssh_key_unlock': 'superfluous-key-unlock', - }], ['v2', { 'name': 'Best credential ever', 'credential_type': 1, @@ -1705,13 +1325,6 @@ def test_ssh_unlock_not_needed(put, organization, admin, credentialtype_ssh, ver @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'name': 'Best credential ever', - 'kind': 'ssh', - 'username': 'joe', - 'ssh_key_data': '$encrypted$', - 'ssh_key_unlock': 'new-unlock', - }], ['v2', { 'name': 'Best credential ever', 'credential_type': 1, @@ -1753,11 +1366,6 @@ def test_ssh_unlock_with_prior_value(put, organization, admin, credentialtype_ss @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'kind': 'ssh', - 'username': 'joe', - 'password': 'secret', - }], ['v2', { 'credential_type': 1, 'inputs': { @@ -1783,12 +1391,8 @@ def test_secret_encryption_on_create(get, post, organization, admin, credentialt assert response.status_code == 200 assert response.data['count'] == 1 cred = response.data['results'][0] - if version == 'v1': - assert cred['username'] == 'joe' - assert cred['password'] == '$encrypted$' - elif version == 'v2': - assert cred['inputs']['username'] == 'joe' - assert cred['inputs']['password'] == '$encrypted$' + assert cred['inputs']['username'] == 'joe' + assert cred['inputs']['password'] == '$encrypted$' cred = Credential.objects.all()[:1].get() assert cred.inputs['password'].startswith('$encrypted$UTF8$AES') @@ -1797,7 +1401,6 @@ def test_secret_encryption_on_create(get, post, organization, admin, credentialt @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', {'password': 'secret'}], ['v2', {'inputs': {'username': 'joe', 'password': 'secret'}}] ]) def test_secret_encryption_on_update(get, post, patch, organization, admin, credentialtype_ssh, version, params): @@ -1829,12 +1432,8 @@ def test_secret_encryption_on_update(get, post, patch, organization, admin, cred assert response.status_code == 200 assert response.data['count'] == 1 cred = response.data['results'][0] - if version == 'v1': - assert cred['username'] == 'joe' - assert cred['password'] == '$encrypted$' - elif version == 'v2': - assert cred['inputs']['username'] == 'joe' - assert cred['inputs']['password'] == '$encrypted$' + assert cred['inputs']['username'] == 'joe' + assert cred['inputs']['password'] == '$encrypted$' cred = Credential.objects.all()[:1].get() assert cred.inputs['password'].startswith('$encrypted$UTF8$AES') @@ -1843,10 +1442,6 @@ def test_secret_encryption_on_update(get, post, patch, organization, admin, cred @pytest.mark.django_db @pytest.mark.parametrize('version, params', [ - ['v1', { - 'username': 'joe', - 'password': '$encrypted$', - }], ['v2', { 'inputs': { 'username': 'joe', @@ -1930,7 +1525,6 @@ def test_custom_credential_type_create(get, post, organization, admin): @pytest.mark.parametrize('version, params', [ - ['v1', {'name': 'Some name', 'username': 'someusername'}], ['v2', {'name': 'Some name', 'credential_type': 1, 'inputs': {'username': 'someusername'}}] ]) @pytest.mark.django_db diff --git a/awx/main/tests/functional/api/test_deprecated_credential_assignment.py b/awx/main/tests/functional/api/test_deprecated_credential_assignment.py index 37ab829e9ede..0c35bfce4156 100644 --- a/awx/main/tests/functional/api/test_deprecated_credential_assignment.py +++ b/awx/main/tests/functional/api/test_deprecated_credential_assignment.py @@ -1,4 +1,3 @@ -import json from unittest import mock import pytest @@ -25,74 +24,6 @@ def job_template(job_template, project, inventory): return job_template -@pytest.mark.django_db -@pytest.mark.parametrize('key', ('credential', 'vault_credential')) -def test_credential_access_empty(get, job_template, admin, key): - url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk}) - resp = get(url, admin) - assert resp.data[key] is None - assert key not in resp.data['summary_fields'] - - -@pytest.mark.django_db -def test_ssh_credential_access(get, job_template, admin, machine_credential): - job_template.credentials.add(machine_credential) - url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk}) - resp = get(url, admin) - assert resp.data['credential'] == machine_credential.pk - assert resp.data['summary_fields']['credential']['credential_type_id'] == machine_credential.pk - assert resp.data['summary_fields']['credential']['kind'] == 'ssh' - - -@pytest.mark.django_db -@pytest.mark.parametrize('key', ('credential', 'vault_credential', 'cloud_credential', 'network_credential')) -def test_invalid_credential_update(get, patch, job_template, admin, key): - url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk, 'version': 'v1'}) - resp = patch(url, {key: 999999}, admin, expect=400) - assert 'Credential 999999 does not exist' in json.loads(smart_str(smart_str(resp.content)))[key] - - -@pytest.mark.django_db -def test_ssh_credential_update(get, patch, job_template, admin, machine_credential): - url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk}) - patch(url, {'credential': machine_credential.pk}, admin, expect=200) - resp = get(url, admin) - assert resp.data['credential'] == machine_credential.pk - - -@pytest.mark.django_db -def test_ssh_credential_update_invalid_kind(get, patch, job_template, admin, vault_credential): - url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk}) - resp = patch(url, {'credential': vault_credential.pk}, admin, expect=400) - assert 'You must provide an SSH credential.' in smart_str(resp.content) - - -@pytest.mark.django_db -def test_vault_credential_access(get, job_template, admin, vault_credential): - job_template.credentials.add(vault_credential) - url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk}) - resp = get(url, admin) - assert resp.data['vault_credential'] == vault_credential.pk - assert resp.data['summary_fields']['vault_credential']['credential_type_id'] == vault_credential.pk # noqa - assert resp.data['summary_fields']['vault_credential']['kind'] == 'vault' - - -@pytest.mark.django_db -def test_vault_credential_update(get, patch, job_template, admin, vault_credential): - url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk}) - patch(url, {'vault_credential': vault_credential.pk}, admin, expect=200) - resp = get(url, admin) - assert resp.data['vault_credential'] == vault_credential.pk - - -@pytest.mark.django_db -def test_vault_credential_update_invalid_kind(get, patch, job_template, admin, - machine_credential): - url = reverse('api:job_template_detail', kwargs={'pk': job_template.pk}) - resp = patch(url, {'vault_credential': machine_credential.pk}, admin, expect=400) - assert 'You must provide a vault credential.' in smart_str(resp.content) - - @pytest.mark.django_db def test_extra_credentials_filtering(get, job_template, admin, machine_credential, vault_credential, credential): @@ -209,24 +140,6 @@ def _new_cred(name): assert 'Cannot assign multiple Amazon Web Services credentials.' in smart_str(resp.content) -@pytest.mark.django_db -def test_ssh_credential_at_launch(get, post, job_template, admin, machine_credential): - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - pk = post(url, {'credential': machine_credential.pk}, admin, expect=201).data['job'] - summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields'] - - assert len(summary_fields['credentials']) == 1 - - -@pytest.mark.django_db -def test_vault_credential_at_launch(get, post, job_template, admin, vault_credential): - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - pk = post(url, {'vault_credential': vault_credential.pk}, admin, expect=201).data['job'] - summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields'] - - assert len(summary_fields['credentials']) == 1 - - @pytest.mark.django_db def test_extra_credentials_at_launch(get, post, job_template, admin, credential): url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) @@ -236,30 +149,6 @@ def test_extra_credentials_at_launch(get, post, job_template, admin, credential) assert len(summary_fields['credentials']) == 1 -@pytest.mark.django_db -def test_modify_ssh_credential_at_launch(get, post, job_template, admin, - machine_credential, vault_credential, credential): - job_template.credentials.add(vault_credential) - job_template.credentials.add(credential) - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - pk = post(url, {'credential': machine_credential.pk}, admin, expect=201).data['job'] - - summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields'] - assert len(summary_fields['credentials']) == 3 - - -@pytest.mark.django_db -def test_modify_vault_credential_at_launch(get, post, job_template, admin, - machine_credential, vault_credential, credential): - job_template.credentials.add(machine_credential) - job_template.credentials.add(credential) - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - pk = post(url, {'vault_credential': vault_credential.pk}, admin, expect=201).data['job'] - - summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields'] - assert len(summary_fields['credentials']) == 3 - - @pytest.mark.django_db def test_modify_extra_credentials_at_launch(get, post, job_template, admin, machine_credential, vault_credential, credential): @@ -272,22 +161,6 @@ def test_modify_extra_credentials_at_launch(get, post, job_template, admin, assert len(summary_fields['credentials']) == 3 -@pytest.mark.django_db -def test_overwrite_ssh_credential_at_launch(get, post, job_template, admin, machine_credential): - job_template.credentials.add(machine_credential) - - new_cred = machine_credential - new_cred.pk = None - new_cred.save() - - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - pk = post(url, {'credential': new_cred.pk}, admin, expect=201).data['job'] - - summary_fields = get(reverse('api:job_detail', kwargs={'pk': pk}), admin).data['summary_fields'] - assert len(summary_fields['credentials']) == 1 - assert summary_fields['credentials'][0]['id'] == new_cred.pk - - @pytest.mark.django_db def test_ssh_password_prompted_at_launch(get, post, job_template, admin, machine_credential): job_template.credentials.add(machine_credential) @@ -375,49 +248,6 @@ def test_invalid_mixed_credentials_specification(get, post, job_template, admin, user=admin, expect=400) -@pytest.mark.django_db -def test_rbac_default_credential_usage(get, post, job_template, alice, machine_credential): - job_template.credentials.add(machine_credential) - job_template.execute_role.members.add(alice) - - # alice can launch; she's not adding any _new_ credentials, and she has - # execute access to the JT - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - post(url, {'credential': machine_credential.pk}, alice, expect=201) - - # make (copy) a _new_ SSH cred - new_cred = Credential.objects.create( - name=machine_credential.name, - credential_type=machine_credential.credential_type, - inputs=machine_credential.inputs - ) - - # alice is attempting to launch with a *different* SSH cred, but - # she does not have access to it, so she cannot launch - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - post(url, {'credential': new_cred.pk}, alice, expect=403) - - # if alice has gains access to the credential, she *can* launch - new_cred.use_role.members.add(alice) - url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk}) - post(url, {'credential': new_cred.pk}, alice, expect=201) - - -@pytest.mark.django_db -def test_inventory_source_deprecated_credential(get, patch, admin, ec2_source, credential): - url = reverse('api:inventory_source_detail', kwargs={'pk': ec2_source.pk}) - patch(url, {'credential': credential.pk}, admin, expect=200) - resp = get(url, admin, expect=200) - assert json.loads(smart_str(resp.content))['credential'] == credential.pk - - -@pytest.mark.django_db -def test_inventory_source_invalid_deprecated_credential(patch, admin, ec2_source, credential): - url = reverse('api:inventory_source_detail', kwargs={'pk': ec2_source.pk}) - resp = patch(url, {'credential': 999999}, admin, expect=400) - assert 'Credential 999999 does not exist' in smart_str(resp.content) - - @pytest.mark.django_db def test_deprecated_credential_activity_stream(patch, admin_user, machine_credential, job_template): job_template.credentials.add(machine_credential) diff --git a/awx/main/tests/functional/api/test_job_runtime_params.py b/awx/main/tests/functional/api/test_job_runtime_params.py index 7a8f1844761a..d90f63e18026 100644 --- a/awx/main/tests/functional/api/test_job_runtime_params.py +++ b/awx/main/tests/functional/api/test_job_runtime_params.py @@ -309,8 +309,8 @@ def test_job_launch_with_default_creds(machine_credential, vault_credential, dep prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv) job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields) - assert job_obj.credential == machine_credential.pk - assert job_obj.vault_credential == vault_credential.pk + assert job_obj.machine_credential.pk == machine_credential.pk + assert job_obj.vault_credentials[0].pk == vault_credential.pk @pytest.mark.django_db @@ -350,14 +350,14 @@ def test_job_launch_with_empty_creds(machine_credential, vault_credential, deplo prompted_fields, ignored_fields, errors = deploy_jobtemplate._accept_or_ignore_job_kwargs(**serializer.validated_data) job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields) - assert job_obj.credential is deploy_jobtemplate.credential - assert job_obj.vault_credential is deploy_jobtemplate.vault_credential + assert job_obj.machine_credential.pk == deploy_jobtemplate.machine_credential.pk + assert job_obj.vault_credentials[0].pk == deploy_jobtemplate.vault_credentials[0].pk @pytest.mark.django_db def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_credential, deploy_jobtemplate, post, rando): - vault_credential.vault_password = 'ASK' + vault_credential.inputs['vault_password'] = 'ASK' vault_credential.save() deploy_jobtemplate.credentials.add(vault_credential) deploy_jobtemplate.execute_role.members.add(rando) @@ -440,7 +440,7 @@ def test_job_launch_fails_with_missing_multivault_password(machine_credential, v @pytest.mark.django_db def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_jobtemplate, post, rando): - machine_credential.password = 'ASK' + machine_credential.inputs['password'] = 'ASK' machine_credential.save() deploy_jobtemplate.credentials.add(machine_credential) deploy_jobtemplate.execute_role.members.add(rando) @@ -457,9 +457,9 @@ def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_j @pytest.mark.django_db def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential, vault_credential, deploy_jobtemplate, post, rando): - vault_credential.vault_password = 'ASK' + vault_credential.inputs['vault_password'] = 'ASK' vault_credential.save() - machine_credential.password = 'ASK' + machine_credential.inputs['password'] = 'ASK' machine_credential.save() deploy_jobtemplate.credentials.add(machine_credential) deploy_jobtemplate.credentials.add(vault_credential) @@ -477,7 +477,7 @@ def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential @pytest.mark.django_db def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_credential, deploy_jobtemplate, post, rando): - vault_credential.vault_password = 'ASK' + vault_credential.inputs['vault_password'] = 'ASK' vault_credential.save() deploy_jobtemplate.credentials.add(machine_credential) deploy_jobtemplate.credentials.add(vault_credential) diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index 9d62c70a6944..10c978eb06a7 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -19,148 +19,27 @@ @pytest.mark.django_db @pytest.mark.parametrize( - "grant_project, grant_credential, grant_inventory, expect", [ - (True, True, True, 201), - (True, True, False, 403), - (True, False, True, 403), - (False, True, True, 403), + "grant_project, grant_inventory, expect", [ + (True, True, 201), + (True, False, 403), + (False, True, 403), ] ) -def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_credential, grant_inventory, expect): +def test_create(post, project, machine_credential, inventory, alice, grant_project, grant_inventory, expect): if grant_project: project.use_role.members.add(alice) - if grant_credential: - machine_credential.use_role.members.add(alice) if grant_inventory: inventory.use_role.members.add(alice) r = post(reverse('api:job_template_list'), { 'name': 'Some name', 'project': project.id, - 'credential': machine_credential.id, # TODO: remove in 3.3 'inventory': inventory.id, 'playbook': 'helloworld.yml', }, alice) - if expect == 201: - jt = JobTemplate.objects.get(id=r.data['id']) - assert set(jt.credentials.values_list('id', flat=True)) == set([machine_credential.id]) assert r.status_code == expect -# TODO: remove in 3.3 -@pytest.mark.django_db -def test_create_with_v1_deprecated_credentials(get, post, project, machine_credential, credential, net_credential, inventory, alice): - project.use_role.members.add(alice) - machine_credential.use_role.members.add(alice) - credential.use_role.members.add(alice) - net_credential.use_role.members.add(alice) - inventory.use_role.members.add(alice) - - pk = post(reverse('api:job_template_list', kwargs={'version': 'v1'}), { - 'name': 'Some name', - 'project': project.id, - 'credential': machine_credential.id, - 'cloud_credential': credential.id, - 'network_credential': net_credential.id, - 'inventory': inventory.id, - 'playbook': 'helloworld.yml', - }, alice, expect=201).data['id'] - - url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': pk}) - response = get(url, alice) - assert response.data.get('cloud_credential') == credential.pk - assert response.data.get('network_credential') == net_credential.pk - - -# TODO: remove in 3.3 -@pytest.mark.django_db -def test_create_with_empty_v1_deprecated_credentials(get, post, project, machine_credential, inventory, alice): - project.use_role.members.add(alice) - machine_credential.use_role.members.add(alice) - inventory.use_role.members.add(alice) - - pk = post(reverse('api:job_template_list', kwargs={'version': 'v1'}), { - 'name': 'Some name', - 'project': project.id, - 'credential': machine_credential.id, - 'cloud_credential': None, - 'network_credential': None, - 'inventory': inventory.id, - 'playbook': 'helloworld.yml', - }, alice, expect=201).data['id'] - - url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': pk}) - response = get(url, alice) - assert response.data.get('cloud_credential') is None - assert response.data.get('network_credential') is None - - -# TODO: remove in 3.3 -@pytest.mark.django_db -def test_create_v1_rbac_check(get, post, project, credential, net_credential, rando): - project.use_role.members.add(rando) - - base_kwargs = dict( - name = 'Made with cloud/net creds I have no access to', - project = project.id, - ask_inventory_on_launch = True, - ask_credential_on_launch = True, - playbook = 'helloworld.yml', - ) - - base_kwargs['cloud_credential'] = credential.pk - post(reverse('api:job_template_list', kwargs={'version': 'v1'}), base_kwargs, rando, expect=403) - - base_kwargs.pop('cloud_credential') - base_kwargs['network_credential'] = net_credential.pk - post(reverse('api:job_template_list', kwargs={'version': 'v1'}), base_kwargs, rando, expect=403) - - -# TODO: remove as each field tested has support removed -@pytest.mark.django_db -def test_jt_deprecated_summary_fields( - project, inventory, - machine_credential, net_credential, vault_credential, - mocker): - jt = JobTemplate.objects.create( - project=project, - inventory=inventory, - playbook='helloworld.yml' - ) - - class MockView: - kwargs = {} - request = None - - class MockRequest: - version = 'v1' - user = None - - view = MockView() - request = MockRequest() - view.request = request - serializer = JobTemplateSerializer(instance=jt, context={'view': view, 'request': request}) - - for kwargs in [{}, {'pk': 1}]: # detail vs. list view - for version in ['v1', 'v2']: - view.kwargs = kwargs - request.version = version - sf = serializer.get_summary_fields(jt) - assert 'credential' not in sf - assert 'vault_credential' not in sf - - jt.credentials.add(machine_credential, net_credential, vault_credential) - - view.kwargs = {'pk': 1} - for version in ['v1', 'v2']: - request.version = version - sf = serializer.get_summary_fields(jt) - assert 'credential' in sf - assert sf['credential'] # not empty dict - assert 'vault_credential' in sf - assert sf['vault_credential'] - - @pytest.mark.django_db def test_extra_credential_creation(get, post, organization_factory, job_template_factory, credentialtype_aws): objs = organization_factory("org", superusers=['admin']) @@ -293,79 +172,6 @@ def test_attach_extra_credential_wrong_kind_xfail(get, post, organization_factor assert response.data.get('count') == 0 -# TODO: remove in 3.3 -@pytest.mark.django_db -def test_v1_extra_credentials_detail(get, organization_factory, job_template_factory, credential, net_credential): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - jt.credentials.add(credential) - jt.credentials.add(net_credential) - jt.save() - - url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk}) - response = get(url, user=objs.superusers.admin) - assert response.data.get('cloud_credential') == credential.pk - assert response.data.get('network_credential') == net_credential.pk - - -# TODO: remove in 3.3 -@pytest.mark.django_db -def test_v1_set_extra_credentials_assignment(get, patch, organization_factory, job_template_factory, credential, net_credential): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - jt.save() - - url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk}) - response = patch(url, { - 'cloud_credential': credential.pk, - 'network_credential': net_credential.pk - }, objs.superusers.admin) - assert response.status_code == 200 - - url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk}) - response = get(url, user=objs.superusers.admin) - assert response.status_code == 200 - assert response.data.get('cloud_credential') == credential.pk - assert response.data.get('network_credential') == net_credential.pk - - url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk}) - response = patch(url, { - 'cloud_credential': None, - 'network_credential': None, - }, objs.superusers.admin) - assert response.status_code == 200 - - url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk}) - response = get(url, user=objs.superusers.admin) - assert response.status_code == 200 - assert response.data.get('cloud_credential') is None - assert response.data.get('network_credential') is None - - -@pytest.mark.django_db -def test_filter_by_v1(get, organization_factory, job_template_factory, credential, net_credential): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - jt.credentials.add(credential) - jt.credentials.add(net_credential) - jt.save() - - for query in ( - ('cloud_credential', str(credential.pk)), - ('network_credential', str(net_credential.pk)) - ): - url = reverse('api:job_template_list', kwargs={'version': 'v1'}) - response = get( - url, - user=objs.superusers.admin, - QUERY_STRING='='.join(query) - ) - assert response.data.get('count') == 1 - - @pytest.mark.django_db @pytest.mark.parametrize( "grant_project, grant_inventory, expect", [ @@ -588,29 +394,6 @@ def test_launch_with_extra_credentials_not_allowed(get, post, organization_facto assert resp.data.get('count') == 0 -@pytest.mark.django_db -def test_v1_launch_with_extra_credentials(get, post, organization_factory, - job_template_factory, machine_credential, - credential, net_credential): - # launch requests to `/api/v1/job_templates/N/launch/` should ignore - # `extra_credentials`, as they're only supported in v2 of the API. - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - jt.ask_credential_on_launch = True - jt.save() - - resp = post( - reverse('api:job_template_launch', kwargs={'pk': jt.pk, 'version': 'v1'}), - dict( - credential=machine_credential.pk, - extra_credentials=[credential.pk, net_credential.pk] - ), - objs.superusers.admin, expect=400 - ) - assert 'Field is not allowed for use with v1 API' in resp.data.get('extra_credentials') - - @pytest.mark.django_db def test_jt_without_project(inventory): data = dict(name="Test", job_type="run", diff --git a/awx/main/tests/functional/api/test_rbac_displays.py b/awx/main/tests/functional/api/test_rbac_displays.py index 362fcb924a18..4180647d4472 100644 --- a/awx/main/tests/functional/api/test_rbac_displays.py +++ b/awx/main/tests/functional/api/test_rbac_displays.py @@ -65,7 +65,7 @@ def jt_copy_edit(self, job_template_factory, project): return objects.job_template def fake_context(self, user): - request = RequestFactory().get('/api/v1/resource/42/') + request = RequestFactory().get('/api/v2/resource/42/') request.user = user class FakeView(object): @@ -151,7 +151,7 @@ def mock_access_method(mocker): class TestAccessListCapabilities: """ Test that the access_list serializer shows the exact output of the RoleAccess.can_attach - - looks at /api/v1/inventories/N/access_list/ + - looks at /api/v2/inventories/N/access_list/ - test for types: direct, indirect, and team access """ diff --git a/awx/main/tests/functional/api/test_workflow_node.py b/awx/main/tests/functional/api/test_workflow_node.py index 3e8a756e48fa..6de3e5b53383 100644 --- a/awx/main/tests/functional/api/test_workflow_node.py +++ b/awx/main/tests/functional/api/test_workflow_node.py @@ -55,7 +55,7 @@ def test_node_rejects_unprompted_fields(inventory, project, workflow_job_templat ask_limit_on_launch = False ) url = reverse('api:workflow_job_template_workflow_nodes_list', - kwargs={'pk': workflow_job_template.pk, 'version': 'v1'}) + kwargs={'pk': workflow_job_template.pk, 'version': 'v2'}) r = post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'}, user=admin_user, expect=400) assert 'limit' in r.data @@ -71,7 +71,7 @@ def test_node_accepts_prompted_fields(inventory, project, workflow_job_template, ask_limit_on_launch = True ) url = reverse('api:workflow_job_template_workflow_nodes_list', - kwargs={'pk': workflow_job_template.pk, 'version': 'v1'}) + kwargs={'pk': workflow_job_template.pk, 'version': 'v2'}) post(url, {'unified_job_template': job_template.pk, 'limit': 'webservers'}, user=admin_user, expect=201) diff --git a/awx/main/tests/functional/models/test_unified_job.py b/awx/main/tests/functional/models/test_unified_job.py index b9bb3df52312..402e2ac1f6d1 100644 --- a/awx/main/tests/functional/models/test_unified_job.py +++ b/awx/main/tests/functional/models/test_unified_job.py @@ -63,7 +63,10 @@ def test_job_relaunch_copy_vars(self, machine_credential, inventory, second_job = job_with_links.copy_unified_job() # Check that job data matches the original variables - assert second_job.credential == job_with_links.credential + assert [c.pk for c in second_job.credentials.all()] == [ + machine_credential.pk, + net_credential.pk + ] assert second_job.inventory == job_with_links.inventory assert second_job.limit == 'my_server' assert net_credential in second_job.credentials.all() diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index 4683dcbfde2e..a131fc1a4821 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -99,25 +99,6 @@ def test_default_cred_types(): assert type_().managed_by_tower is True -@pytest.mark.django_db -@pytest.mark.parametrize('kind', ['net', 'scm', 'ssh', 'vault']) -def test_cred_type_kind_uniqueness(kind): - """ - non-cloud credential types are exclusive_on_kind (you can only use *one* of - them at a time) - """ - assert CredentialType.defaults[kind]().unique_by_kind is True - - -@pytest.mark.django_db -def test_cloud_kind_uniqueness(): - """ - you can specify more than one cloud credential type (as long as they have - different names so you don't e.g., use ec2 twice") - """ - assert CredentialType.defaults['aws']().unique_by_kind is False - - @pytest.mark.django_db def test_credential_creation(organization_factory): org = organization_factory('test').organization @@ -141,7 +122,7 @@ def test_credential_creation(organization_factory): cred.full_clean() assert isinstance(cred, Credential) assert cred.name == "Bob's Credential" - assert cred.inputs['username'] == cred.username == 'bob' + assert cred.inputs['username'] == 'bob' @pytest.mark.django_db diff --git a/awx/main/tests/functional/test_rbac_job_templates.py b/awx/main/tests/functional/test_rbac_job_templates.py index 558e1f41f6ca..39058d9d8ad3 100644 --- a/awx/main/tests/functional/test_rbac_job_templates.py +++ b/awx/main/tests/functional/test_rbac_job_templates.py @@ -1,10 +1,7 @@ from unittest import mock import pytest -from rest_framework.exceptions import PermissionDenied - from awx.api.versioning import reverse -from awx.api.serializers import JobTemplateSerializer from awx.main.access import ( BaseAccess, JobTemplateAccess, @@ -29,16 +26,18 @@ def test_job_template_access_superuser(check_license, user, deploy_jobtemplate): @pytest.mark.django_db def test_job_template_access_read_level(jt_linked, rando): + ssh_cred = jt_linked.machine_credential + vault_cred = jt_linked.vault_credentials[0] access = JobTemplateAccess(rando) jt_linked.project.read_role.members.add(rando) jt_linked.inventory.read_role.members.add(rando) - jt_linked.get_deprecated_credential('ssh').read_role.members.add(rando) + ssh_cred.read_role.members.add(rando) proj_pk = jt_linked.project.pk assert not access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk)) - assert not access.can_add(dict(credential=jt_linked.credential, project=proj_pk)) - assert not access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk)) + assert not access.can_add(dict(credential=ssh_cred.pk, project=proj_pk)) + assert not access.can_add(dict(vault_credential=vault_cred.pk, project=proj_pk)) for cred in jt_linked.credentials.all(): assert not access.can_unattach(jt_linked, cred, 'credentials', {}) @@ -46,17 +45,19 @@ def test_job_template_access_read_level(jt_linked, rando): @pytest.mark.django_db def test_job_template_access_use_level(jt_linked, rando): + ssh_cred = jt_linked.machine_credential + vault_cred = jt_linked.vault_credentials[0] access = JobTemplateAccess(rando) jt_linked.project.use_role.members.add(rando) jt_linked.inventory.use_role.members.add(rando) - jt_linked.get_deprecated_credential('ssh').use_role.members.add(rando) - jt_linked.get_deprecated_credential('vault').use_role.members.add(rando) + ssh_cred.use_role.members.add(rando) + vault_cred.use_role.members.add(rando) proj_pk = jt_linked.project.pk assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk)) - assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk)) - assert access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk)) + assert access.can_add(dict(credential=ssh_cred.pk, project=proj_pk)) + assert access.can_add(dict(vault_credential=vault_cred.pk, project=proj_pk)) for cred in jt_linked.credentials.all(): assert not access.can_unattach(jt_linked, cred, 'credentials', {}) @@ -65,6 +66,8 @@ def test_job_template_access_use_level(jt_linked, rando): @pytest.mark.django_db @pytest.mark.parametrize("role_names", [("admin_role",), ("job_template_admin_role", "inventory_admin_role", "project_admin_role")]) def test_job_template_access_admin(role_names, jt_linked, rando): + ssh_cred = jt_linked.machine_credential + access = JobTemplateAccess(rando) # Appoint this user as admin of the organization #jt_linked.inventory.organization.admin_role.members.add(rando) @@ -77,11 +80,11 @@ def test_job_template_access_admin(role_names, jt_linked, rando): # Assign organization permission in the same way the create view does organization = jt_linked.inventory.organization - jt_linked.get_deprecated_credential('ssh').admin_role.parents.add(organization.admin_role) + ssh_cred.admin_role.parents.add(organization.admin_role) proj_pk = jt_linked.project.pk assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk)) - assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk)) + assert access.can_add(dict(credential=ssh_cred.pk, project=proj_pk)) for cred in jt_linked.credentials.all(): assert access.can_unattach(jt_linked, cred, 'credentials', {}) @@ -104,7 +107,7 @@ def test_job_template_extra_credentials_prompts_access( jt.execute_role.members.add(rando) r = post( reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}), - {'vault_credential': vault_credential.pk}, rando + {'credentials': [machine_credential.pk, vault_credential.pk]}, rando ) assert r.status_code == 403 @@ -126,57 +129,6 @@ def test_job_template_can_add_extra_credentials(self, job_template, credential, assert JobTemplateAccess(rando).can_attach( job_template, credential, 'credentials', {}) - def test_job_template_vault_cred_check(self, mocker, job_template, vault_credential, rando, project): - # TODO: remove in 3.4 - job_template.admin_role.members.add(rando) - # not allowed to use the vault cred - # this is checked in the serializer validate method, not access.py - view = mocker.MagicMock() - view.request = mocker.MagicMock() - view.request.user = rando - serializer = JobTemplateSerializer(job_template, context={'view': view}) - with pytest.raises(PermissionDenied): - serializer.validate({ - 'vault_credential': vault_credential.pk, - 'project': project, # necessary because job_template fixture fails validation - 'ask_inventory_on_launch': True, - }) - - def test_job_template_vault_cred_check_noop(self, mocker, job_template, vault_credential, rando, project): - # TODO: remove in 3.4 - job_template.credentials.add(vault_credential) - job_template.admin_role.members.add(rando) - # not allowed to use the vault cred - # this is checked in the serializer validate method, not access.py - view = mocker.MagicMock() - view.request = mocker.MagicMock() - view.request.user = rando - serializer = JobTemplateSerializer(job_template, context={'view': view}) - # should not raise error: - serializer.validate({ - 'vault_credential': vault_credential.pk, - 'project': project, # necessary because job_template fixture fails validation - 'playbook': 'helloworld.yml', - 'ask_inventory_on_launch': True, - }) - - def test_new_jt_with_vault(self, mocker, vault_credential, project, rando): - project.admin_role.members.add(rando) - # TODO: remove in 3.4 - # this is checked in the serializer validate method, not access.py - view = mocker.MagicMock() - view.request = mocker.MagicMock() - view.request.user = rando - serializer = JobTemplateSerializer(context={'view': view}) - with pytest.raises(PermissionDenied): - serializer.validate({ - 'vault_credential': vault_credential.pk, - 'project': project, - 'playbook': 'helloworld.yml', - 'ask_inventory_on_launch': True, - 'name': 'asdf' - }) - @pytest.mark.django_db class TestOrphanJobTemplate: diff --git a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py index 437f6d9404e0..3e842a059829 100644 --- a/awx/main/tests/unit/api/serializers/test_job_template_serializers.py +++ b/awx/main/tests/unit/api/serializers/test_job_template_serializers.py @@ -103,8 +103,7 @@ def test_copy_edit_standard(self, mocker, job_template_factory): with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'): with mocker.patch("awx.main.access.JobTemplateAccess.can_change", return_value='foobar'): with mocker.patch("awx.main.access.JobTemplateAccess.can_copy", return_value='foo'): - with mock.patch.object(jt_obj.__class__, 'get_deprecated_credential', return_value=None): - response = serializer.get_summary_fields(jt_obj) + response = serializer.get_summary_fields(jt_obj) assert response['user_capabilities']['copy'] == 'foo' assert response['user_capabilities']['edit'] == 'foobar' diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 143e461daf3e..99136dcc63d7 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -688,13 +688,19 @@ def job(self): job.websocket_emit_status = mock.Mock() job._credentials = [] + def _credentials_filter(credential_type__kind=None): + creds = job._credentials + if credential_type__kind: + creds = [c for c in creds if c.credential_type.kind == credential_type__kind] + return mock.Mock( + __iter__ = lambda *args: iter(creds), + first = lambda: creds[0] if len(creds) else None + ) + credentials_mock = mock.Mock(**{ 'all': lambda: job._credentials, 'add': job._credentials.append, - 'filter.return_value': mock.Mock( - __iter__ = lambda *args: iter(job._credentials), - first = lambda: job._credentials[0] - ), + 'filter.side_effect': _credentials_filter, 'prefetch_related': lambda _: credentials_mock, 'spec_set': ['all', 'add', 'filter', 'prefetch_related'], }) diff --git a/awx/main/tests/unit/test_views.py b/awx/main/tests/unit/test_views.py index 0454b23d7d5e..b9a96d434494 100644 --- a/awx/main/tests/unit/test_views.py +++ b/awx/main/tests/unit/test_views.py @@ -7,7 +7,7 @@ # AWX from awx.main.views import ApiErrorView -from awx.api.views import JobList, InventorySourceList +from awx.api.views import JobList from awx.api.generics import ListCreateAPIView, SubListAttachDetachAPIView @@ -40,20 +40,10 @@ def test_exception_view_raises_exception(api_view_obj_fixture, method_name): getattr(api_view_obj_fixture, method_name)(request_mock) -@pytest.mark.parametrize('version, supports_post', [(1, True), (2, False)]) -def test_disable_post_on_v2_jobs_list(version, supports_post): +def test_disable_post_on_v2_jobs_list(): job_list = JobList() job_list.request = mock.MagicMock() - with mock.patch('awx.api.views.get_request_version', return_value=version): - assert ('POST' in job_list.allowed_methods) == supports_post - - -@pytest.mark.parametrize('version, supports_post', [(1, False), (2, True)]) -def test_disable_post_on_v1_inventory_source_list(version, supports_post): - inv_source_list = InventorySourceList() - inv_source_list.request = mock.MagicMock() - with mock.patch('awx.api.views.get_request_version', return_value=version): - assert ('POST' in inv_source_list.allowed_methods) == supports_post + assert ('POST' in job_list.allowed_methods) is False def test_views_have_search_fields(all_views): diff --git a/awx/ui/context_processors.py b/awx/ui/context_processors.py index fb231f8c9b45..38976a6eaac1 100644 --- a/awx/ui/context_processors.py +++ b/awx/ui/context_processors.py @@ -22,5 +22,5 @@ def version(request): context.get('view'), 'deprecated', False - ) or request.path.startswith('/api/v1/') + ) } diff --git a/docs/custom_credential_types.md b/docs/custom_credential_types.md index 64c1143aa22a..33cb903b7876 100644 --- a/docs/custom_credential_types.md +++ b/docs/custom_credential_types.md @@ -262,52 +262,6 @@ endpoint: } -API Backwards Compatability ---------------------------- - -`/api/v1/credentials/` still exists in Tower 3.2, and it transparently works as -before with minimal surprises by attempting to translate `/api/v1/` requests to -the new ``Credential`` and ``Credential Type`` models. - -* When creating or modifying a ``Job Template`` through `v1` of the API, - old-style credential assignment will transparently map to the new model. For - example, the following `POST`'ed payload: - - { - credential: , - vault_credential: , - cloud_credential: , - network_credential: , - } - - ...would transparently update ``JobTemplate.extra_credentials`` to a list - containing both the cloud and network ``Credentials``. - - Similarly, an `HTTP GET /api/v1/job_credentials/N/` will populate - `cloud_credential`, and `network_credential` with the *most recently applied* - matching credential in the list. - -* Custom ``Credentials`` will not be returned in the ``v1`` API; if a user - defines their own ``Credential Type``, its credentials won't show up in the - ``v1`` API. - -* ``HTTP POST`` requests to ``/api/v1/credentials/`` will transparently map - old-style attributes (i.e., ``username``, ``password``, ``ssh_key_data``) to - the appropriate new-style model. Similarly, ``HTTP GET - /api/v1/credentials/N/`` requests will continue to contain old-style - key-value mappings in their payloads. - -* Vault credentials are a new first-level type of credential in Tower 3.2. - As such, any ``Credentials`` pre-Tower 3.2 that contain *both* SSH and Vault - parameters will be migrated to separate distinct ``Credentials`` - post-migration. - - For example, if your Tower 3.1 installation has one ``Credential`` with - a defined ``username``, ``password``, and ``vault_password``, after migration - *two* ``Credentials`` will exist (one which contains the ``username`` and - ``password``, and another which contains only the ``vault_password``). - - Additional Criteria ------------------- * Rackspace is being removed from official support in Tower 3.2. Pre-existing diff --git a/docs/notification_system.md b/docs/notification_system.md index e3819f44ef32..1415f30c96ae 100644 --- a/docs/notification_system.md +++ b/docs/notification_system.md @@ -30,7 +30,6 @@ Notifications can succeed or fail but that will not cause its associated job to Once a Notification Template is created, its configuration can be tested by utilizing the endpoint at `/api/v2/notification_templates//test` This will emit a test notification given the configuration defined by the notification. These test notifications will also appear in the notifications list at `/api/v2/notifications` - # Notification Types The currently defined Notification Types are: diff --git a/tools/data_generators/rbac_dummy_data_generator.py b/tools/data_generators/rbac_dummy_data_generator.py index f6bf8124df9f..d88320904f44 100755 --- a/tools/data_generators/rbac_dummy_data_generator.py +++ b/tools/data_generators/rbac_dummy_data_generator.py @@ -332,7 +332,7 @@ def make_the_data(): name='%s Credential %d User %d' % (prefix, credential_id, user_idx), defaults=dict(created_by=next(creator_gen), modified_by=next(modifier_gen)), - credential_type=CredentialType.from_v1_kind('ssh') + credential_type=CredentialType.objects.filter(namespace='ssh').first() ) credential.admin_role.members.add(user) credentials.append(credential) @@ -355,7 +355,7 @@ def make_the_data(): name='%s Credential %d team %d' % (prefix, credential_id, team_idx), defaults=dict(created_by=next(creator_gen), modified_by=next(modifier_gen)), - credential_type=CredentialType.from_v1_kind('ssh') + credential_type=CredentialType.objects.filter(namespace='ssh').first() ) credential.admin_role.parents.add(team.member_role) credentials.append(credential) diff --git a/tools/docker-compose/README b/tools/docker-compose/README index 118fe9dad06e..96510937ce37 100644 --- a/tools/docker-compose/README +++ b/tools/docker-compose/README @@ -3,7 +3,7 @@ docker run --name awx_test -it --memory="4g" --cpuset="0,1" -v /Users/meyers/ans ## How to use the logstash container -POST the following content to `/api/v1/settings/logging/` (this uses +POST the following content to `/api/v2/settings/logging/` (this uses authentication set up inside of the logstash configuration file). ``` diff --git a/tools/elastic/README.md b/tools/elastic/README.md index 7681acae694c..eb9d4f653157 100644 --- a/tools/elastic/README.md +++ b/tools/elastic/README.md @@ -95,7 +95,7 @@ and } ``` These can be entered via Configure-Tower-in-Tower by making a POST to -`/api/v1/settings/logging/`. +`/api/v2/settings/logging/`. ### Connecting Logstash to 3rd Party Receivers diff --git a/tools/scripts/launch_job.py b/tools/scripts/launch_job.py deleted file mode 100755 index aa9136c639fb..000000000000 --- a/tools/scripts/launch_job.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python - -import datetime -import getpass -import json -import urllib2 - -REST_API_URL = "http://awx.example.com/api/v1/" -REST_API_USER = "admin" -REST_API_PASS = "password" -JOB_TEMPLATE_ID = 1 - -# Setup urllib2 for basic password authentication. -password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm() -password_mgr.add_password(None, REST_API_URL, REST_API_USER, REST_API_PASS) -handler = urllib2.HTTPBasicAuthHandler(password_mgr) -opener = urllib2.build_opener(handler) -urllib2.install_opener(opener) - -# Read the job template. -JOB_TEMPLATE_URL="%sjob_templates/%d/" % (REST_API_URL, JOB_TEMPLATE_ID) -response = urllib2.urlopen(JOB_TEMPLATE_URL) -data = json.loads(response.read()) - -# Update data if needed for the new job. -data.pop('id') -data.update({ - 'name': 'my new job started at %s' % str(datetime.datetime.now()), - 'verbosity': 3, -}) - -# Create a new job based on the template and data. -JOB_TEMPLATE_JOBS_URL="%sjobs/" % JOB_TEMPLATE_URL -request = urllib2.Request(JOB_TEMPLATE_JOBS_URL, json.dumps(data), - {'Content-type': 'application/json'}) -response = urllib2.urlopen(request) -data = json.loads(response.read()) - -# Get the job ID and check for passwords needed to start the job. -JOB_ID = data['id'] -JOB_START_URL = '%sjobs/%d/start/' % (REST_API_URL, JOB_ID) -response = urllib2.urlopen(JOB_START_URL) -data = json.loads(response.read()) - -# Prompt for any passwords needed. -start_data = {} -for password in data.get('passwords_needed_to_start', []): - value = getpass.getpass('%s: ' % password) - start_data[password] = value - -# Make POST request to start the job. -request = urllib2.Request(JOB_START_URL, json.dumps(start_data), - {'Content-type': 'application/json'}) -response = urllib2.urlopen(request)