mirror of
https://github.com/ansible/awx.git
synced 2026-03-28 14:25:05 -02:30
Merge pull request #595 from ryanpetrello/multicred
replace all Job/JT relations with a single M2M credentials relation
This commit is contained in:
@@ -269,8 +269,10 @@ class FieldLookupBackend(BaseFilterBackend):
|
|||||||
|
|
||||||
# Make legacy v1 Job/Template fields work for backwards compatability
|
# Make legacy v1 Job/Template fields work for backwards compatability
|
||||||
# TODO: remove after API v1 deprecation period
|
# TODO: remove after API v1 deprecation period
|
||||||
if queryset.model._meta.object_name in ('JobTemplate', 'Job') and key in ('cloud_credential', 'network_credential'):
|
if queryset.model._meta.object_name in ('JobTemplate', 'Job') and key in (
|
||||||
key = 'extra_credentials'
|
'credential', 'vault_credential', 'cloud_credential', 'network_credential'
|
||||||
|
):
|
||||||
|
key = 'credentials'
|
||||||
|
|
||||||
# Make legacy v1 Credential fields work for backwards compatability
|
# Make legacy v1 Credential fields work for backwards compatability
|
||||||
# TODO: remove after API v1 deprecation period
|
# TODO: remove after API v1 deprecation period
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ class APIView(views.APIView):
|
|||||||
'new_in_300': getattr(self, 'new_in_300', False),
|
'new_in_300': getattr(self, 'new_in_300', False),
|
||||||
'new_in_310': getattr(self, 'new_in_310', False),
|
'new_in_310': getattr(self, 'new_in_310', False),
|
||||||
'new_in_320': getattr(self, 'new_in_320', False),
|
'new_in_320': getattr(self, 'new_in_320', False),
|
||||||
|
'new_in_330': getattr(self, 'new_in_330', False),
|
||||||
'new_in_api_v2': getattr(self, 'new_in_api_v2', False),
|
'new_in_api_v2': getattr(self, 'new_in_api_v2', False),
|
||||||
'deprecated': getattr(self, 'deprecated', False),
|
'deprecated': getattr(self, 'deprecated', False),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, Ver
|
|||||||
|
|
||||||
logger = logging.getLogger('awx.api.serializers')
|
logger = logging.getLogger('awx.api.serializers')
|
||||||
|
|
||||||
|
DEPRECATED = 'This resource has been deprecated and will be removed in a future release'
|
||||||
|
|
||||||
# Fields that should be summarized regardless of object type.
|
# Fields that should be summarized regardless of object type.
|
||||||
DEFAULT_SUMMARY_FIELDS = ('id', 'name', 'description')# , 'created_by', 'modified_by')#, 'type')
|
DEFAULT_SUMMARY_FIELDS = ('id', 'name', 'description')# , 'created_by', 'modified_by')#, 'type')
|
||||||
|
|
||||||
@@ -538,6 +540,13 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
kwargs['request'] = self.context.get('request')
|
kwargs['request'] = self.context.get('request')
|
||||||
return reverse(*args, **kwargs)
|
return reverse(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_detail_view(self):
|
||||||
|
if 'view' in self.context:
|
||||||
|
if 'pk' in self.context['view'].kwargs:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class EmptySerializer(serializers.Serializer):
|
class EmptySerializer(serializers.Serializer):
|
||||||
pass
|
pass
|
||||||
@@ -2286,8 +2295,8 @@ class V1JobOptionsSerializer(BaseSerializer):
|
|||||||
fields = ('*', 'cloud_credential', 'network_credential')
|
fields = ('*', 'cloud_credential', 'network_credential')
|
||||||
|
|
||||||
V1_FIELDS = {
|
V1_FIELDS = {
|
||||||
'cloud_credential': models.PositiveIntegerField(blank=True, null=True, default=None),
|
'cloud_credential': models.PositiveIntegerField(blank=True, null=True, default=None, help_text=DEPRECATED),
|
||||||
'network_credential': models.PositiveIntegerField(blank=True, null=True, default=None)
|
'network_credential': models.PositiveIntegerField(blank=True, null=True, default=None, help_text=DEPRECATED),
|
||||||
}
|
}
|
||||||
|
|
||||||
def build_field(self, field_name, info, model_class, nested_depth):
|
def build_field(self, field_name, info, model_class, nested_depth):
|
||||||
@@ -2297,20 +2306,41 @@ class V1JobOptionsSerializer(BaseSerializer):
|
|||||||
return super(V1JobOptionsSerializer, self).build_field(field_name, info, model_class, nested_depth)
|
return super(V1JobOptionsSerializer, self).build_field(field_name, info, model_class, nested_depth)
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(BaseSerializerMetaclass)
|
||||||
|
class LegacyCredentialFields(BaseSerializer):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Credential
|
||||||
|
fields = ('*', 'credential', 'vault_credential')
|
||||||
|
|
||||||
|
LEGACY_FIELDS = {
|
||||||
|
'credential': models.PositiveIntegerField(blank=True, null=True, default=None, help_text=DEPRECATED),
|
||||||
|
'vault_credential': models.PositiveIntegerField(blank=True, null=True, default=None, help_text=DEPRECATED),
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_field(self, field_name, info, model_class, nested_depth):
|
||||||
|
if field_name in self.LEGACY_FIELDS:
|
||||||
|
return self.build_standard_field(field_name,
|
||||||
|
self.LEGACY_FIELDS[field_name])
|
||||||
|
return super(LegacyCredentialFields, self).build_field(field_name, info, model_class, nested_depth)
|
||||||
|
|
||||||
|
|
||||||
class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('*', 'job_type', 'inventory', 'project', 'playbook',
|
fields = ('*', 'job_type', 'inventory', 'project', 'playbook',
|
||||||
'credential', 'vault_credential', 'forks', 'limit',
|
'forks', 'limit', 'verbosity', 'extra_vars', 'job_tags',
|
||||||
'verbosity', 'extra_vars', 'job_tags', 'force_handlers',
|
'force_handlers', 'skip_tags', 'start_at_task', 'timeout',
|
||||||
'skip_tags', 'start_at_task', 'timeout', 'use_fact_cache',)
|
'use_fact_cache',)
|
||||||
|
|
||||||
def get_fields(self):
|
def get_fields(self):
|
||||||
fields = super(JobOptionsSerializer, self).get_fields()
|
fields = super(JobOptionsSerializer, self).get_fields()
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
# TODO: remove when API v1 is removed
|
||||||
if self.version == 1 and 'credential' in self.Meta.fields:
|
if self.version == 1:
|
||||||
fields.update(V1JobOptionsSerializer().get_fields())
|
fields.update(V1JobOptionsSerializer().get_fields())
|
||||||
|
|
||||||
|
fields.update(LegacyCredentialFields().get_fields())
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
@@ -2321,17 +2351,22 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
|||||||
if obj.project:
|
if obj.project:
|
||||||
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
|
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
|
||||||
if obj.credential:
|
if obj.credential:
|
||||||
res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential.pk})
|
res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential})
|
||||||
if obj.vault_credential:
|
if obj.vault_credential:
|
||||||
res['vault_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.vault_credential.pk})
|
res['vault_credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.vault_credential})
|
||||||
if self.version > 1:
|
if self.version > 1:
|
||||||
if isinstance(obj, UnifiedJobTemplate):
|
if isinstance(obj, UnifiedJobTemplate):
|
||||||
res['extra_credentials'] = self.reverse(
|
res['extra_credentials'] = self.reverse(
|
||||||
'api:job_template_extra_credentials_list',
|
'api:job_template_extra_credentials_list',
|
||||||
kwargs={'pk': obj.pk}
|
kwargs={'pk': obj.pk}
|
||||||
)
|
)
|
||||||
|
res['credentials'] = self.reverse(
|
||||||
|
'api:job_template_credentials_list',
|
||||||
|
kwargs={'pk': obj.pk}
|
||||||
|
)
|
||||||
elif isinstance(obj, UnifiedJob):
|
elif isinstance(obj, UnifiedJob):
|
||||||
res['extra_credentials'] = self.reverse('api:job_extra_credentials_list', kwargs={'pk': obj.pk})
|
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:
|
else:
|
||||||
cloud_cred = obj.cloud_credential
|
cloud_cred = obj.cloud_credential
|
||||||
if cloud_cred:
|
if cloud_cred:
|
||||||
@@ -2352,64 +2387,67 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
|||||||
ret['project'] = None
|
ret['project'] = None
|
||||||
if 'playbook' in ret:
|
if 'playbook' in ret:
|
||||||
ret['playbook'] = ''
|
ret['playbook'] = ''
|
||||||
if 'credential' in ret and not obj.credential:
|
ret['credential'] = obj.credential
|
||||||
ret['credential'] = None
|
ret['vault_credential'] = obj.vault_credential
|
||||||
if 'vault_credential' in ret and not obj.vault_credential:
|
if self.version == 1:
|
||||||
ret['vault_credential'] = None
|
|
||||||
if self.version == 1 and 'credential' in self.Meta.fields:
|
|
||||||
ret['cloud_credential'] = obj.cloud_credential
|
ret['cloud_credential'] = obj.cloud_credential
|
||||||
ret['network_credential'] = obj.network_credential
|
ret['network_credential'] = obj.network_credential
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
deprecated_fields = {}
|
deprecated_fields = {}
|
||||||
for key in ('cloud_credential', 'network_credential'):
|
for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'):
|
||||||
if key in validated_data:
|
if key in validated_data:
|
||||||
deprecated_fields[key] = validated_data.pop(key)
|
deprecated_fields[key] = validated_data.pop(key)
|
||||||
obj = super(JobOptionsSerializer, self).create(validated_data)
|
obj = super(JobOptionsSerializer, self).create(validated_data)
|
||||||
if self.version == 1 and deprecated_fields: # TODO: remove in 3.3
|
if deprecated_fields: # TODO: remove in 3.3
|
||||||
self._update_deprecated_fields(deprecated_fields, obj)
|
self._update_deprecated_fields(deprecated_fields, obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def update(self, obj, validated_data):
|
def update(self, obj, validated_data):
|
||||||
deprecated_fields = {}
|
deprecated_fields = {}
|
||||||
for key in ('cloud_credential', 'network_credential'):
|
for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'):
|
||||||
if key in validated_data:
|
if key in validated_data:
|
||||||
deprecated_fields[key] = validated_data.pop(key)
|
deprecated_fields[key] = validated_data.pop(key)
|
||||||
obj = super(JobOptionsSerializer, self).update(obj, validated_data)
|
obj = super(JobOptionsSerializer, self).update(obj, validated_data)
|
||||||
if self.version == 1 and deprecated_fields: # TODO: remove in 3.3
|
if deprecated_fields: # TODO: remove in 3.3
|
||||||
self._update_deprecated_fields(deprecated_fields, obj)
|
self._update_deprecated_fields(deprecated_fields, obj)
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def _update_deprecated_fields(self, fields, obj):
|
def _update_deprecated_fields(self, fields, obj):
|
||||||
for key, existing in (
|
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),
|
('cloud_credential', obj.cloud_credentials),
|
||||||
('network_credential', obj.network_credentials),
|
('network_credential', obj.network_credentials),
|
||||||
):
|
):
|
||||||
if key in fields:
|
if key in fields:
|
||||||
for cred in existing:
|
for cred in existing:
|
||||||
obj.extra_credentials.remove(cred)
|
obj.credentials.remove(cred)
|
||||||
if fields[key]:
|
if fields[key]:
|
||||||
obj.extra_credentials.add(fields[key])
|
obj.credentials.add(fields[key])
|
||||||
obj.save()
|
obj.save()
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
v1_credentials = {}
|
v1_credentials = {}
|
||||||
view = self.context.get('view', None)
|
view = self.context.get('view', None)
|
||||||
if self.version == 1: # TODO: remove in 3.3
|
for attr, kind, error in (
|
||||||
for attr, kind, error in (
|
('cloud_credential', 'cloud', _('You must provide a cloud credential.')),
|
||||||
('cloud_credential', 'cloud', _('You must provide a cloud credential.')),
|
('network_credential', 'net', _('You must provide a network 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 attr in attrs:
|
):
|
||||||
v1_credentials[attr] = None
|
if kind in ('cloud', 'net') and self.version > 1:
|
||||||
pk = attrs.pop(attr)
|
continue # cloud and net deprecated creds are v1 only
|
||||||
if pk:
|
if attr in attrs:
|
||||||
cred = v1_credentials[attr] = Credential.objects.get(pk=pk)
|
v1_credentials[attr] = None
|
||||||
if cred.credential_type.kind != kind:
|
pk = attrs.pop(attr)
|
||||||
raise serializers.ValidationError({attr: error})
|
if pk:
|
||||||
if (not view) or (not view.request) or (view.request.user not in cred.use_role):
|
cred = v1_credentials[attr] = Credential.objects.get(pk=pk)
|
||||||
raise PermissionDenied()
|
if cred.credential_type.kind != kind:
|
||||||
|
raise serializers.ValidationError({attr: error})
|
||||||
|
if 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:
|
if 'project' in self.fields and 'playbook' in self.fields:
|
||||||
project = attrs.get('project', self.instance and self.instance.project or None)
|
project = attrs.get('project', self.instance and self.instance.project or None)
|
||||||
@@ -2492,19 +2530,11 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
|||||||
return attrs.get(fd, self.instance and getattr(self.instance, fd) or None)
|
return attrs.get(fd, self.instance and getattr(self.instance, fd) or None)
|
||||||
|
|
||||||
inventory = get_field_from_model_or_attrs('inventory')
|
inventory = get_field_from_model_or_attrs('inventory')
|
||||||
credential = get_field_from_model_or_attrs('credential')
|
|
||||||
vault_credential = get_field_from_model_or_attrs('vault_credential')
|
|
||||||
project = get_field_from_model_or_attrs('project')
|
project = get_field_from_model_or_attrs('project')
|
||||||
|
|
||||||
prompting_error_message = _("Must either set a default value or ask to prompt on launch.")
|
prompting_error_message = _("Must either set a default value or ask to prompt on launch.")
|
||||||
if project is None:
|
if project is None:
|
||||||
raise serializers.ValidationError({'project': _("Job types 'run' and 'check' must have assigned a project.")})
|
raise serializers.ValidationError({'project': _("Job types 'run' and 'check' must have assigned a project.")})
|
||||||
elif all([
|
|
||||||
credential is None,
|
|
||||||
vault_credential is None,
|
|
||||||
not get_field_from_model_or_attrs('ask_credential_on_launch'),
|
|
||||||
]):
|
|
||||||
raise serializers.ValidationError({'credential': prompting_error_message})
|
|
||||||
elif inventory is None and not get_field_from_model_or_attrs('ask_inventory_on_launch'):
|
elif inventory is None and not get_field_from_model_or_attrs('ask_inventory_on_launch'):
|
||||||
raise serializers.ValidationError({'inventory': prompting_error_message})
|
raise serializers.ValidationError({'inventory': prompting_error_message})
|
||||||
|
|
||||||
@@ -2515,17 +2545,27 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
|||||||
|
|
||||||
def get_summary_fields(self, obj):
|
def get_summary_fields(self, obj):
|
||||||
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
|
summary_fields = super(JobTemplateSerializer, self).get_summary_fields(obj)
|
||||||
if 'pk' in self.context['view'].kwargs and self.version > 1: # TODO: remove version check in 3.3
|
if self.is_detail_view:
|
||||||
|
all_creds = []
|
||||||
extra_creds = []
|
extra_creds = []
|
||||||
for cred in obj.extra_credentials.all():
|
for cred in obj.credentials.all():
|
||||||
extra_creds.append({
|
summarized_cred = {
|
||||||
'id': cred.pk,
|
'id': cred.pk,
|
||||||
'name': cred.name,
|
'name': cred.name,
|
||||||
'description': cred.description,
|
'description': cred.description,
|
||||||
'kind': cred.kind,
|
'kind': cred.kind,
|
||||||
'credential_type_id': cred.credential_type_id
|
'credential_type_id': cred.credential_type_id
|
||||||
})
|
}
|
||||||
summary_fields['extra_credentials'] = extra_creds
|
all_creds.append(summarized_cred)
|
||||||
|
if cred.credential_type.kind in ('cloud', 'net'):
|
||||||
|
extra_creds.append(summarized_cred)
|
||||||
|
elif cred.credential_type.kind == 'ssh':
|
||||||
|
summary_fields['credential'] = summarized_cred
|
||||||
|
elif cred.credential_type.kind == 'vault':
|
||||||
|
summary_fields['vault_credential'] = summarized_cred
|
||||||
|
if self.version > 1:
|
||||||
|
summary_fields['extra_credentials'] = extra_creds
|
||||||
|
summary_fields['credentials'] = all_creds
|
||||||
return summary_fields
|
return summary_fields
|
||||||
|
|
||||||
|
|
||||||
@@ -2618,17 +2658,27 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
|||||||
|
|
||||||
def get_summary_fields(self, obj):
|
def get_summary_fields(self, obj):
|
||||||
summary_fields = super(JobSerializer, self).get_summary_fields(obj)
|
summary_fields = super(JobSerializer, self).get_summary_fields(obj)
|
||||||
if 'pk' in self.context['view'].kwargs and self.version > 1: # TODO: remove version check in 3.3
|
if self.is_detail_view: # TODO: remove version check in 3.3
|
||||||
|
all_creds = []
|
||||||
extra_creds = []
|
extra_creds = []
|
||||||
for cred in obj.extra_credentials.all():
|
for cred in obj.credentials.all():
|
||||||
extra_creds.append({
|
summarized_cred = {
|
||||||
'id': cred.pk,
|
'id': cred.pk,
|
||||||
'name': cred.name,
|
'name': cred.name,
|
||||||
'description': cred.description,
|
'description': cred.description,
|
||||||
'kind': cred.kind,
|
'kind': cred.kind,
|
||||||
'credential_type_id': cred.credential_type_id
|
'credential_type_id': cred.credential_type_id
|
||||||
})
|
}
|
||||||
summary_fields['extra_credentials'] = extra_creds
|
all_creds.append(summarized_cred)
|
||||||
|
if cred.credential_type.kind in ('cloud', 'net'):
|
||||||
|
extra_creds.append(summarized_cred)
|
||||||
|
elif cred.credential_type.kind == 'ssh':
|
||||||
|
summary_fields['credential'] = summarized_cred
|
||||||
|
elif cred.credential_type.kind == 'vault':
|
||||||
|
summary_fields['vault_credential'] = summarized_cred
|
||||||
|
if self.version > 1:
|
||||||
|
summary_fields['extra_credentials'] = extra_creds
|
||||||
|
summary_fields['credentials'] = all_creds
|
||||||
return summary_fields
|
return summary_fields
|
||||||
|
|
||||||
|
|
||||||
@@ -3250,7 +3300,7 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
fields = ('can_start_without_user_input', 'passwords_needed_to_start',
|
fields = ('can_start_without_user_input', 'passwords_needed_to_start',
|
||||||
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
|
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
|
||||||
'credential', 'extra_credentials', 'ask_variables_on_launch', 'ask_tags_on_launch',
|
'credentials', 'ask_variables_on_launch', 'ask_tags_on_launch',
|
||||||
'ask_diff_mode_on_launch', 'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_limit_on_launch',
|
'ask_diff_mode_on_launch', 'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_limit_on_launch',
|
||||||
'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch',
|
'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch',
|
||||||
'survey_enabled', 'variables_needed_to_start', 'credential_needed_to_start',
|
'survey_enabled', 'variables_needed_to_start', 'credential_needed_to_start',
|
||||||
@@ -3260,8 +3310,7 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch',
|
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch',
|
||||||
'ask_inventory_on_launch', 'ask_credential_on_launch',)
|
'ask_inventory_on_launch', 'ask_credential_on_launch',)
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'credential': {'write_only': True,},
|
'credentials': {'write_only': True, 'default': [], 'allow_empty': True},
|
||||||
'extra_credentials': {'write_only': True, 'default': [], 'allow_empty': True},
|
|
||||||
'limit': {'write_only': True,},
|
'limit': {'write_only': True,},
|
||||||
'job_tags': {'write_only': True,},
|
'job_tags': {'write_only': True,},
|
||||||
'skip_tags': {'write_only': True,},
|
'skip_tags': {'write_only': True,},
|
||||||
@@ -3270,15 +3319,8 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
'verbosity': {'write_only': True,}
|
'verbosity': {'write_only': True,}
|
||||||
}
|
}
|
||||||
|
|
||||||
# TODO: remove in 3.3
|
|
||||||
def get_fields(self):
|
|
||||||
ret = super(JobLaunchSerializer, self).get_fields()
|
|
||||||
if self.version == 1:
|
|
||||||
ret.pop('extra_credentials')
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def get_credential_needed_to_start(self, obj):
|
def get_credential_needed_to_start(self, obj):
|
||||||
return not (obj and obj.credential)
|
return False
|
||||||
|
|
||||||
def get_inventory_needed_to_start(self, obj):
|
def get_inventory_needed_to_start(self, obj):
|
||||||
return not (obj and obj.inventory)
|
return not (obj and obj.inventory)
|
||||||
@@ -3293,11 +3335,15 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
ask_for_vars_dict['vault_credential'] = False
|
ask_for_vars_dict['vault_credential'] = False
|
||||||
defaults_dict = {}
|
defaults_dict = {}
|
||||||
for field in ask_for_vars_dict:
|
for field in ask_for_vars_dict:
|
||||||
if field in ('inventory', 'credential', 'vault_credential'):
|
if field == 'inventory':
|
||||||
defaults_dict[field] = dict(
|
defaults_dict[field] = dict(
|
||||||
name=getattrd(obj, '%s.name' % field, None),
|
name=getattrd(obj, '%s.name' % field, None),
|
||||||
id=getattrd(obj, '%s.pk' % field, None))
|
id=getattrd(obj, '%s.pk' % field, None))
|
||||||
elif field == 'extra_credentials':
|
elif field in ('credential', 'vault_credential', 'extra_credentials'):
|
||||||
|
# don't prefill legacy defaults; encourage API users to specify
|
||||||
|
# credentials at launch time using the new `credentials` key
|
||||||
|
pass
|
||||||
|
elif field == 'credentials':
|
||||||
if self.version > 1:
|
if self.version > 1:
|
||||||
defaults_dict[field] = [
|
defaults_dict[field] = [
|
||||||
dict(
|
dict(
|
||||||
@@ -3305,7 +3351,7 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
name=cred.name,
|
name=cred.name,
|
||||||
credential_type=cred.credential_type.pk
|
credential_type=cred.credential_type.pk
|
||||||
)
|
)
|
||||||
for cred in obj.extra_credentials.all()
|
for cred in obj.credentials.all()
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
defaults_dict[field] = getattr(obj, field)
|
defaults_dict[field] = getattr(obj, field)
|
||||||
@@ -3326,23 +3372,7 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
if obj.inventory and obj.inventory.pending_deletion is True:
|
if obj.inventory and obj.inventory.pending_deletion is True:
|
||||||
errors['inventory'] = _("The inventory associated with this Job Template is being deleted.")
|
errors['inventory'] = _("The inventory associated with this Job Template is being deleted.")
|
||||||
|
|
||||||
if (not obj.ask_credential_on_launch) or (not attrs.get('credential', None)):
|
|
||||||
credential = obj.credential
|
|
||||||
else:
|
|
||||||
credential = attrs.get('credential', None)
|
|
||||||
|
|
||||||
# fill passwords dict with request data passwords
|
|
||||||
for cred in (credential, obj.vault_credential):
|
|
||||||
if cred and cred.passwords_needed:
|
|
||||||
passwords = self.context.get('passwords')
|
|
||||||
try:
|
|
||||||
for p in cred.passwords_needed:
|
|
||||||
passwords[p] = data[p]
|
|
||||||
except KeyError:
|
|
||||||
errors.setdefault('passwords_needed_to_start', []).extend(cred.passwords_needed)
|
|
||||||
|
|
||||||
extra_vars = attrs.get('extra_vars', {})
|
extra_vars = attrs.get('extra_vars', {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
extra_vars = parse_yaml_or_json(extra_vars, silent_failure=False)
|
extra_vars = parse_yaml_or_json(extra_vars, silent_failure=False)
|
||||||
except ParseError as e:
|
except ParseError as e:
|
||||||
@@ -3354,14 +3384,15 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
if validation_errors:
|
if validation_errors:
|
||||||
errors['variables_needed_to_start'] = validation_errors
|
errors['variables_needed_to_start'] = validation_errors
|
||||||
|
|
||||||
extra_cred_kinds = []
|
# Prohibit credential assign of the same CredentialType.kind
|
||||||
for cred in data.get('extra_credentials', []):
|
# Note: when multi-vault is supported, we'll have to carve out an
|
||||||
|
# exception to this logic
|
||||||
|
distinct_cred_kinds = []
|
||||||
|
for cred in data.get('credentials', []):
|
||||||
cred = Credential.objects.get(id=cred)
|
cred = Credential.objects.get(id=cred)
|
||||||
if cred.credential_type.pk in extra_cred_kinds:
|
if cred.credential_type.pk in distinct_cred_kinds:
|
||||||
errors['extra_credentials'] = _('Cannot assign multiple %s credentials.' % cred.credential_type.name)
|
errors['credentials'] = _('Cannot assign multiple %s credentials.' % cred.credential_type.name)
|
||||||
if cred.credential_type.kind not in ('net', 'cloud'):
|
distinct_cred_kinds.append(cred.credential_type.pk)
|
||||||
errors['extra_credentials'] = _('Extra credentials must be network or cloud.')
|
|
||||||
extra_cred_kinds.append(cred.credential_type.pk)
|
|
||||||
|
|
||||||
# Special prohibited cases for scan jobs
|
# Special prohibited cases for scan jobs
|
||||||
errors.update(obj._extra_job_type_errors(data))
|
errors.update(obj._extra_job_type_errors(data))
|
||||||
@@ -3375,9 +3406,8 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
JT_job_tags = obj.job_tags
|
JT_job_tags = obj.job_tags
|
||||||
JT_skip_tags = obj.skip_tags
|
JT_skip_tags = obj.skip_tags
|
||||||
JT_inventory = obj.inventory
|
JT_inventory = obj.inventory
|
||||||
JT_credential = obj.credential
|
|
||||||
JT_verbosity = obj.verbosity
|
JT_verbosity = obj.verbosity
|
||||||
extra_credentials = attrs.pop('extra_credentials', None)
|
credentials = attrs.pop('credentials', None)
|
||||||
attrs = super(JobLaunchSerializer, self).validate(attrs)
|
attrs = super(JobLaunchSerializer, self).validate(attrs)
|
||||||
obj.extra_vars = JT_extra_vars
|
obj.extra_vars = JT_extra_vars
|
||||||
obj.limit = JT_limit
|
obj.limit = JT_limit
|
||||||
@@ -3385,10 +3415,29 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
obj.skip_tags = JT_skip_tags
|
obj.skip_tags = JT_skip_tags
|
||||||
obj.job_tags = JT_job_tags
|
obj.job_tags = JT_job_tags
|
||||||
obj.inventory = JT_inventory
|
obj.inventory = JT_inventory
|
||||||
obj.credential = JT_credential
|
|
||||||
obj.verbosity = JT_verbosity
|
obj.verbosity = JT_verbosity
|
||||||
if extra_credentials is not None:
|
if credentials is not None:
|
||||||
attrs['extra_credentials'] = extra_credentials
|
attrs['credentials'] = credentials
|
||||||
|
|
||||||
|
# if the POST includes a list of credentials, verify that they don't
|
||||||
|
# require launch-time passwords
|
||||||
|
# if the POST *does not* include a list of credentials, fall back to
|
||||||
|
# checking the credentials on the JobTemplate
|
||||||
|
credentials = attrs['credentials'] if 'credentials' in data else obj.credentials.all()
|
||||||
|
passwords_needed = []
|
||||||
|
for cred in credentials:
|
||||||
|
if cred.passwords_needed:
|
||||||
|
passwords = self.context.get('passwords')
|
||||||
|
try:
|
||||||
|
for p in cred.passwords_needed:
|
||||||
|
passwords[p] = data[p]
|
||||||
|
except KeyError:
|
||||||
|
passwords_needed.extend(cred.passwords_needed)
|
||||||
|
if len(passwords_needed):
|
||||||
|
raise serializers.ValidationError({
|
||||||
|
'passwords_needed_to_start': passwords_needed
|
||||||
|
})
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,5 @@
|
|||||||
{% if new_in_300 %}> _Added in Ansible Tower 3.0.0_{% endif %}
|
{% if new_in_300 %}> _Added in Ansible Tower 3.0.0_{% endif %}
|
||||||
{% if new_in_310 %}> _New in Ansible Tower 3.1.0_{% endif %}
|
{% if new_in_310 %}> _New in Ansible Tower 3.1.0_{% endif %}
|
||||||
{% if new_in_320 %}> _New in Ansible Tower 3.2.0_{% endif %}
|
{% if new_in_320 %}> _New in Ansible Tower 3.2.0_{% endif %}
|
||||||
|
{% if new_in_330 %}> _New in Ansible Tower 3.3.0_{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -26,9 +26,6 @@ The response will include the following fields:
|
|||||||
job_template (array, read-only)
|
job_template (array, read-only)
|
||||||
* `survey_enabled`: Flag indicating whether the job_template has an enabled
|
* `survey_enabled`: Flag indicating whether the job_template has an enabled
|
||||||
survey (boolean, read-only)
|
survey (boolean, read-only)
|
||||||
* `credential_needed_to_start`: Flag indicating the presence of a credential
|
|
||||||
associated with the job template. If not then one should be supplied when
|
|
||||||
launching the job (boolean, read-only)
|
|
||||||
* `inventory_needed_to_start`: Flag indicating the presence of an inventory
|
* `inventory_needed_to_start`: Flag indicating the presence of an inventory
|
||||||
associated with the job template. If not then one should be supplied when
|
associated with the job template. If not then one should be supplied when
|
||||||
launching the job (boolean, read-only)
|
launching the job (boolean, read-only)
|
||||||
@@ -36,9 +33,8 @@ The response will include the following fields:
|
|||||||
Make a POST request to this resource to launch the job_template. If any
|
Make a POST request to this resource to launch the job_template. If any
|
||||||
passwords, inventory, or extra variables (extra_vars) are required, they must
|
passwords, inventory, or extra variables (extra_vars) are required, they must
|
||||||
be passed via POST data, with extra_vars given as a YAML or JSON string and
|
be passed via POST data, with extra_vars given as a YAML or JSON string and
|
||||||
escaped parentheses. If `credential_needed_to_start` is `True` then the
|
escaped parentheses. If the `inventory_needed_to_start` is `True` then the
|
||||||
`credential` field is required and if the `inventory_needed_to_start` is
|
`inventory` is required.
|
||||||
`True` then the `inventory` is required as well.
|
|
||||||
|
|
||||||
If successful, the response status code will be 201. If any required passwords
|
If successful, the response status code will be 201. If any required passwords
|
||||||
are not provided, a 400 status code will be returned. If the job cannot be
|
are not provided, a 400 status code will be returned. If the job cannot be
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ from awx.api.views import (
|
|||||||
UnifiedJobTemplateList,
|
UnifiedJobTemplateList,
|
||||||
UnifiedJobList,
|
UnifiedJobList,
|
||||||
HostAnsibleFactsDetail,
|
HostAnsibleFactsDetail,
|
||||||
|
JobCredentialsList,
|
||||||
JobExtraCredentialsList,
|
JobExtraCredentialsList,
|
||||||
|
JobTemplateCredentialsList,
|
||||||
JobTemplateExtraCredentialsList,
|
JobTemplateExtraCredentialsList,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,7 +110,9 @@ v2_urls = [
|
|||||||
url(r'^credential_types/', include(credential_type_urls)),
|
url(r'^credential_types/', include(credential_type_urls)),
|
||||||
url(r'^hosts/(?P<pk>[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'),
|
url(r'^hosts/(?P<pk>[0-9]+)/ansible_facts/$', HostAnsibleFactsDetail.as_view(), name='host_ansible_facts_detail'),
|
||||||
url(r'^jobs/(?P<pk>[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'),
|
url(r'^jobs/(?P<pk>[0-9]+)/extra_credentials/$', JobExtraCredentialsList.as_view(), name='job_extra_credentials_list'),
|
||||||
|
url(r'^jobs/(?P<pk>[0-9]+)/credentials/$', JobCredentialsList.as_view(), name='job_credentials_list'),
|
||||||
url(r'^job_templates/(?P<pk>[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'),
|
url(r'^job_templates/(?P<pk>[0-9]+)/extra_credentials/$', JobTemplateExtraCredentialsList.as_view(), name='job_template_extra_credentials_list'),
|
||||||
|
url(r'^job_templates/(?P<pk>[0-9]+)/credentials/$', JobTemplateCredentialsList.as_view(), name='job_template_credentials_list'),
|
||||||
]
|
]
|
||||||
|
|
||||||
app_name = 'api'
|
app_name = 'api'
|
||||||
|
|||||||
125
awx/api/views.py
125
awx/api/views.py
@@ -13,7 +13,7 @@ import sys
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict, Iterable
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -2669,7 +2669,7 @@ class JobTemplateList(ListCreateAPIView):
|
|||||||
always_allow_superuser = False
|
always_allow_superuser = False
|
||||||
capabilities_prefetch = [
|
capabilities_prefetch = [
|
||||||
'admin', 'execute',
|
'admin', 'execute',
|
||||||
{'copy': ['project.use', 'inventory.use', 'credential.use', 'vault_credential.use']}
|
{'copy': ['project.use', 'inventory.use']}
|
||||||
]
|
]
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
@@ -2711,15 +2711,13 @@ class JobTemplateLaunch(RetrieveAPIView):
|
|||||||
data['extra_vars'] = extra_vars
|
data['extra_vars'] = extra_vars
|
||||||
ask_for_vars_dict = obj._ask_for_vars_dict()
|
ask_for_vars_dict = obj._ask_for_vars_dict()
|
||||||
ask_for_vars_dict.pop('extra_vars')
|
ask_for_vars_dict.pop('extra_vars')
|
||||||
if get_request_version(self.request) == 1: # TODO: remove in 3.3
|
|
||||||
ask_for_vars_dict.pop('extra_credentials')
|
|
||||||
for field in ask_for_vars_dict:
|
for field in ask_for_vars_dict:
|
||||||
if not ask_for_vars_dict[field]:
|
if not ask_for_vars_dict[field]:
|
||||||
data.pop(field, None)
|
data.pop(field, None)
|
||||||
elif field == 'inventory' or field == 'credential':
|
elif field == 'inventory':
|
||||||
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
|
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
|
||||||
elif field == 'extra_credentials':
|
elif field == 'credentials':
|
||||||
data[field] = [cred.id for cred in obj.extra_credentials.all()]
|
data[field] = [cred.id for cred in obj.credentials.all()]
|
||||||
else:
|
else:
|
||||||
data[field] = getattr(obj, field)
|
data[field] = getattr(obj, field)
|
||||||
return data
|
return data
|
||||||
@@ -2733,13 +2731,56 @@ class JobTemplateLaunch(RetrieveAPIView):
|
|||||||
if fd not in request.data and id_fd in request.data:
|
if fd not in request.data and id_fd in request.data:
|
||||||
request.data[fd] = request.data[id_fd]
|
request.data[fd] = request.data[id_fd]
|
||||||
|
|
||||||
if get_request_version(self.request) == 1 and 'extra_credentials' in request.data: # TODO: remove in 3.3
|
# This block causes `extra_credentials` to _always_ be ignored for
|
||||||
|
# the launch endpoint if we're accessing `/api/v1/`
|
||||||
|
if get_request_version(self.request) == 1 and 'extra_credentials' in request.data:
|
||||||
if hasattr(request.data, '_mutable') and not request.data._mutable:
|
if hasattr(request.data, '_mutable') and not request.data._mutable:
|
||||||
request.data._mutable = True
|
request.data._mutable = True
|
||||||
extra_creds = request.data.pop('extra_credentials', None)
|
extra_creds = request.data.pop('extra_credentials', None)
|
||||||
if extra_creds is not None:
|
if extra_creds is not None:
|
||||||
ignored_fields['extra_credentials'] = extra_creds
|
ignored_fields['extra_credentials'] = extra_creds
|
||||||
|
|
||||||
|
# Automatically convert legacy launch credential arguments into a list of `.credentials`
|
||||||
|
if 'credentials' in request.data and (
|
||||||
|
'credential' in request.data or
|
||||||
|
'vault_credential' in request.data or
|
||||||
|
'extra_credentials' in request.data
|
||||||
|
):
|
||||||
|
return Response(dict(
|
||||||
|
error=_("'credentials' cannot be used in combination with 'credential', 'vault_credential', or 'extra_credentials'.")), # noqa
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
'credential' in request.data or
|
||||||
|
'vault_credential' in request.data or
|
||||||
|
'extra_credentials' in request.data
|
||||||
|
):
|
||||||
|
# make a list of the current credentials
|
||||||
|
existing_credentials = obj.credentials.all()
|
||||||
|
new_credentials = []
|
||||||
|
for key, conditional in (
|
||||||
|
('credential', lambda cred: cred.credential_type.kind != 'ssh'),
|
||||||
|
('vault_credential', lambda cred: cred.credential_type.kind != 'vault'),
|
||||||
|
('extra_credentials', lambda cred: cred.credential_type.kind not in ('cloud', 'net'))
|
||||||
|
):
|
||||||
|
if key in request.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 = request.data.pop(key)
|
||||||
|
|
||||||
|
# add the deprecated credential specified in the request
|
||||||
|
if not isinstance(prompted_value, Iterable):
|
||||||
|
prompted_value = [prompted_value]
|
||||||
|
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])
|
||||||
|
if new_credentials:
|
||||||
|
request.data['credentials'] = new_credentials
|
||||||
|
|
||||||
passwords = {}
|
passwords = {}
|
||||||
serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
|
serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
@@ -2749,17 +2790,14 @@ class JobTemplateLaunch(RetrieveAPIView):
|
|||||||
prompted_fields = _accepted_or_ignored[0]
|
prompted_fields = _accepted_or_ignored[0]
|
||||||
ignored_fields.update(_accepted_or_ignored[1])
|
ignored_fields.update(_accepted_or_ignored[1])
|
||||||
|
|
||||||
for fd, model in (
|
fd = 'inventory'
|
||||||
('credential', Credential),
|
if fd in prompted_fields and prompted_fields[fd] != getattrd(obj, '{}.pk'.format(fd), None):
|
||||||
('vault_credential', Credential),
|
new_res = get_object_or_400(Inventory, pk=get_pk_from_dict(prompted_fields, fd))
|
||||||
('inventory', Inventory)):
|
use_role = getattr(new_res, 'use_role')
|
||||||
if fd in prompted_fields and prompted_fields[fd] != getattrd(obj, '{}.pk'.format(fd), None):
|
if request.user not in use_role:
|
||||||
new_res = get_object_or_400(model, pk=get_pk_from_dict(prompted_fields, fd))
|
raise PermissionDenied()
|
||||||
use_role = getattr(new_res, 'use_role')
|
|
||||||
if request.user not in use_role:
|
|
||||||
raise PermissionDenied()
|
|
||||||
|
|
||||||
for cred in prompted_fields.get('extra_credentials', []):
|
for cred in prompted_fields.get('credentials', []):
|
||||||
new_credential = get_object_or_400(Credential, pk=cred)
|
new_credential = get_object_or_400(Credential, pk=cred)
|
||||||
if request.user not in new_credential.use_role:
|
if request.user not in new_credential.use_role:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
@@ -2920,17 +2958,17 @@ class JobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIVi
|
|||||||
new_in_300 = True
|
new_in_300 = True
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateExtraCredentialsList(SubListCreateAttachDetachAPIView):
|
class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView):
|
||||||
|
|
||||||
model = Credential
|
model = Credential
|
||||||
serializer_class = CredentialSerializer
|
serializer_class = CredentialSerializer
|
||||||
parent_model = JobTemplate
|
parent_model = JobTemplate
|
||||||
relationship = 'extra_credentials'
|
relationship = 'credentials'
|
||||||
new_in_320 = True
|
new_in_330 = True
|
||||||
new_in_api_v2 = True
|
new_in_api_v2 = True
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Return the full list of extra_credentials
|
# Return the full list of credentials
|
||||||
parent = self.get_parent_object()
|
parent = self.get_parent_object()
|
||||||
self.check_parent_access(parent)
|
self.check_parent_access(parent)
|
||||||
sublist_qs = getattrd(parent, self.relationship)
|
sublist_qs = getattrd(parent, self.relationship)
|
||||||
@@ -2941,15 +2979,29 @@ class JobTemplateExtraCredentialsList(SubListCreateAttachDetachAPIView):
|
|||||||
return sublist_qs
|
return sublist_qs
|
||||||
|
|
||||||
def is_valid_relation(self, parent, sub, created=False):
|
def is_valid_relation(self, parent, sub, created=False):
|
||||||
current_extra_types = [
|
current_extra_types = [cred.credential_type.pk for cred in parent.credentials.all()]
|
||||||
cred.credential_type.pk for cred in parent.extra_credentials.all()
|
|
||||||
]
|
|
||||||
if sub.credential_type.pk in current_extra_types:
|
if sub.credential_type.pk in current_extra_types:
|
||||||
return {'error': _('Cannot assign multiple %s credentials.' % sub.credential_type.name)}
|
return {'error': _('Cannot assign multiple %s credentials.' % sub.credential_type.name)}
|
||||||
|
|
||||||
if sub.credential_type.kind not in ('net', 'cloud'):
|
return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created)
|
||||||
|
|
||||||
|
|
||||||
|
class JobTemplateExtraCredentialsList(JobTemplateCredentialsList):
|
||||||
|
|
||||||
|
deprecated = True
|
||||||
|
new_in_320 = True
|
||||||
|
new_in_330 = False
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
sublist_qs = super(JobTemplateExtraCredentialsList, self).get_queryset()
|
||||||
|
sublist_qs = sublist_qs.filter(**{'credential_type__kind__in': ['cloud', 'net']})
|
||||||
|
return sublist_qs
|
||||||
|
|
||||||
|
def is_valid_relation(self, parent, sub, created=False):
|
||||||
|
valid = super(JobTemplateExtraCredentialsList, self).is_valid_relation(parent, sub, created)
|
||||||
|
if sub.credential_type.kind not in ('cloud', 'net'):
|
||||||
return {'error': _('Extra credentials must be network or cloud.')}
|
return {'error': _('Extra credentials must be network or cloud.')}
|
||||||
return super(JobTemplateExtraCredentialsList, self).is_valid_relation(parent, sub, created)
|
return valid
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
||||||
@@ -3720,14 +3772,26 @@ class JobDetail(UnifiedJobDeletionMixin, RetrieveUpdateDestroyAPIView):
|
|||||||
return super(JobDetail, self).update(request, *args, **kwargs)
|
return super(JobDetail, self).update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class JobExtraCredentialsList(SubListAPIView):
|
class JobCredentialsList(SubListAPIView):
|
||||||
|
|
||||||
model = Credential
|
model = Credential
|
||||||
serializer_class = CredentialSerializer
|
serializer_class = CredentialSerializer
|
||||||
parent_model = Job
|
parent_model = Job
|
||||||
relationship = 'extra_credentials'
|
relationship = 'credentials'
|
||||||
new_in_320 = True
|
|
||||||
new_in_api_v2 = True
|
new_in_api_v2 = True
|
||||||
|
new_in_330 = True
|
||||||
|
|
||||||
|
|
||||||
|
class JobExtraCredentialsList(JobCredentialsList):
|
||||||
|
|
||||||
|
deprecated = True
|
||||||
|
new_in_320 = True
|
||||||
|
new_in_330 = False
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
sublist_qs = super(JobExtraCredentialsList, self).get_queryset()
|
||||||
|
sublist_qs = sublist_qs.filter(**{'credential_type__kind__in': ['cloud', 'net']})
|
||||||
|
return sublist_qs
|
||||||
|
|
||||||
|
|
||||||
class JobLabelList(SubListAPIView):
|
class JobLabelList(SubListAPIView):
|
||||||
@@ -4252,7 +4316,6 @@ class UnifiedJobTemplateList(ListAPIView):
|
|||||||
capabilities_prefetch = [
|
capabilities_prefetch = [
|
||||||
'admin', 'execute',
|
'admin', 'execute',
|
||||||
{'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use',
|
{'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use',
|
||||||
'jobtemplate.credential.use', 'jobtemplate.vault_credential.use',
|
|
||||||
'workflowjobtemplate.organization.admin']}
|
'workflowjobtemplate.organization.admin']}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1149,7 +1149,7 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
|
|
||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
select_related = ('created_by', 'modified_by', 'inventory', 'project',
|
select_related = ('created_by', 'modified_by', 'inventory', 'project',
|
||||||
'credential', 'next_schedule',)
|
'next_schedule',)
|
||||||
|
|
||||||
def filtered_queryset(self):
|
def filtered_queryset(self):
|
||||||
return self.model.accessible_objects(self.user, 'read_role')
|
return self.model.accessible_objects(self.user, 'read_role')
|
||||||
@@ -1187,13 +1187,10 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# If a credential is provided, the user should have use access to it.
|
# If credentials is provided, the user should have use access to them.
|
||||||
if not self.check_related('credential', Credential, data, role_field='use_role'):
|
for pk in data.get('credentials', []):
|
||||||
return False
|
if self.user not in get_object_or_400(Credential, pk=pk).use_role:
|
||||||
|
return False
|
||||||
# If a vault credential is provided, the user should have use access to it.
|
|
||||||
if not self.check_related('vault_credential', Credential, data, role_field='use_role'):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# If an inventory is provided, the user should have use access.
|
# If an inventory is provided, the user should have use access.
|
||||||
inventory = get_value(Inventory, 'inventory')
|
inventory = get_value(Inventory, 'inventory')
|
||||||
@@ -1239,7 +1236,7 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
self.check_license(feature='surveys')
|
self.check_license(feature='surveys')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for required_field in ('credential', 'inventory', 'project', 'vault_credential'):
|
for required_field in ('inventory', 'project'):
|
||||||
required_obj = getattr(obj, required_field, None)
|
required_obj = getattr(obj, required_field, None)
|
||||||
if required_field not in data_for_change and required_obj is not None:
|
if required_field not in data_for_change and required_obj is not None:
|
||||||
data_for_change[required_field] = required_obj.pk
|
data_for_change[required_field] = required_obj.pk
|
||||||
@@ -1288,7 +1285,7 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
if not obj.project.organization:
|
if not obj.project.organization:
|
||||||
return False
|
return False
|
||||||
return self.user.can_access(type(sub_obj), "read", sub_obj) and self.user in obj.project.organization.admin_role
|
return self.user.can_access(type(sub_obj), "read", sub_obj) and self.user in obj.project.organization.admin_role
|
||||||
if relationship == 'extra_credentials' and isinstance(sub_obj, Credential):
|
if relationship == 'credentials' and isinstance(sub_obj, Credential):
|
||||||
return self.user in obj.admin_role and self.user in sub_obj.use_role
|
return self.user in obj.admin_role and self.user in sub_obj.use_role
|
||||||
return super(JobTemplateAccess, self).can_attach(
|
return super(JobTemplateAccess, self).can_attach(
|
||||||
obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
|
obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
|
||||||
@@ -1297,7 +1294,7 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
|
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
|
||||||
if relationship == "instance_groups":
|
if relationship == "instance_groups":
|
||||||
return self.can_attach(obj, sub_obj, relationship, *args, **kwargs)
|
return self.can_attach(obj, sub_obj, relationship, *args, **kwargs)
|
||||||
if relationship == 'extra_credentials' and isinstance(sub_obj, Credential):
|
if relationship == 'credentials' and isinstance(sub_obj, Credential):
|
||||||
return self.user in obj.admin_role
|
return self.user in obj.admin_role
|
||||||
return super(JobTemplateAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs)
|
return super(JobTemplateAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs)
|
||||||
|
|
||||||
@@ -1316,7 +1313,7 @@ class JobAccess(BaseAccess):
|
|||||||
|
|
||||||
model = Job
|
model = Job
|
||||||
select_related = ('created_by', 'modified_by', 'job_template', 'inventory',
|
select_related = ('created_by', 'modified_by', 'job_template', 'inventory',
|
||||||
'project', 'credential', 'job_template',)
|
'project', 'job_template',)
|
||||||
prefetch_related = (
|
prefetch_related = (
|
||||||
'unified_job_template',
|
'unified_job_template',
|
||||||
'instance_group',
|
'instance_group',
|
||||||
@@ -1399,29 +1396,27 @@ class JobAccess(BaseAccess):
|
|||||||
if self.user.is_superuser:
|
if self.user.is_superuser:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
credential_access = all([self.user in cred.use_role for cred in obj.credentials.all()])
|
||||||
inventory_access = obj.inventory and self.user in obj.inventory.use_role
|
inventory_access = obj.inventory and self.user in obj.inventory.use_role
|
||||||
credential_access = obj.credential and self.user in obj.credential.use_role
|
job_credentials = set(obj.credentials.all())
|
||||||
job_extra_credentials = set(obj.extra_credentials.all())
|
|
||||||
if job_extra_credentials:
|
|
||||||
credential_access = False
|
|
||||||
|
|
||||||
# Check if JT execute access (and related prompts) is sufficient
|
# Check if JT execute access (and related prompts) is sufficient
|
||||||
if obj.job_template is not None:
|
if obj.job_template is not None:
|
||||||
prompts_access = True
|
prompts_access = True
|
||||||
job_fields = {}
|
job_fields = {}
|
||||||
jt_extra_credentials = set(obj.job_template.extra_credentials.all())
|
jt_credentials = set(obj.job_template.credentials.all())
|
||||||
for fd in obj.job_template._ask_for_vars_dict():
|
for fd in obj.job_template._ask_for_vars_dict():
|
||||||
if fd == 'extra_credentials':
|
if fd == 'credentials':
|
||||||
job_fields[fd] = job_extra_credentials
|
job_fields[fd] = job_credentials
|
||||||
job_fields[fd] = getattr(obj, fd)
|
job_fields[fd] = getattr(obj, fd)
|
||||||
accepted_fields, ignored_fields = obj.job_template._accept_or_ignore_job_kwargs(**job_fields)
|
accepted_fields, ignored_fields = obj.job_template._accept_or_ignore_job_kwargs(**job_fields)
|
||||||
# Check if job fields are not allowed by current _on_launch settings
|
# Check if job fields are not allowed by current _on_launch settings
|
||||||
for fd in ignored_fields:
|
for fd in ignored_fields:
|
||||||
if fd == 'extra_vars':
|
if fd == 'extra_vars':
|
||||||
continue # we cannot yet validate validity of prompted extra_vars
|
continue # we cannot yet validate validity of prompted extra_vars
|
||||||
elif fd == 'extra_credentials':
|
elif fd == 'credentials':
|
||||||
if job_extra_credentials != jt_extra_credentials:
|
if job_credentials != jt_credentials:
|
||||||
# Job has extra_credentials that are not promptable
|
# Job has credentials that are not promptable
|
||||||
prompts_access = False
|
prompts_access = False
|
||||||
break
|
break
|
||||||
elif job_fields[fd] != getattr(obj.job_template, fd):
|
elif job_fields[fd] != getattr(obj.job_template, fd):
|
||||||
@@ -1431,19 +1426,16 @@ class JobAccess(BaseAccess):
|
|||||||
# For those fields that are allowed by prompting, but differ
|
# For those fields that are allowed by prompting, but differ
|
||||||
# from JT, assure that user has explicit access to them
|
# from JT, assure that user has explicit access to them
|
||||||
if prompts_access:
|
if prompts_access:
|
||||||
if obj.credential != obj.job_template.credential and not credential_access:
|
|
||||||
prompts_access = False
|
|
||||||
if obj.inventory != obj.job_template.inventory and not inventory_access:
|
if obj.inventory != obj.job_template.inventory and not inventory_access:
|
||||||
prompts_access = False
|
prompts_access = False
|
||||||
if prompts_access and job_extra_credentials != jt_extra_credentials:
|
if prompts_access and job_credentials != jt_credentials:
|
||||||
for cred in job_extra_credentials:
|
for cred in job_credentials:
|
||||||
if self.user not in cred.use_role:
|
if self.user not in cred.use_role:
|
||||||
prompts_access = False
|
prompts_access = False
|
||||||
break
|
break
|
||||||
if prompts_access and self.user in obj.job_template.execute_role:
|
if prompts_access and self.user in obj.job_template.execute_role:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
org_access = obj.inventory and self.user in obj.inventory.organization.admin_role
|
org_access = obj.inventory and self.user in obj.inventory.organization.admin_role
|
||||||
project_access = obj.project is None or self.user in obj.project.admin_role
|
project_access = obj.project is None or self.user in obj.project.admin_role
|
||||||
|
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ class Command(BaseCommand):
|
|||||||
inventory=i,
|
inventory=i,
|
||||||
variables="ansible_connection: local",
|
variables="ansible_connection: local",
|
||||||
created_by=superuser)
|
created_by=superuser)
|
||||||
JobTemplate.objects.create(name='Demo Job Template',
|
jt = JobTemplate.objects.create(name='Demo Job Template',
|
||||||
playbook='hello_world.yml',
|
playbook='hello_world.yml',
|
||||||
project=p,
|
project=p,
|
||||||
inventory=i,
|
inventory=i)
|
||||||
credential=c)
|
jt.credentials.add(c)
|
||||||
print('Default organization added.')
|
print('Default organization added.')
|
||||||
print('Demo Credential, Inventory, and Job Template added.')
|
print('Demo Credential, Inventory, and Job Template added.')
|
||||||
|
|||||||
53
awx/main/migrations/0009_v330_multi_credential.py
Normal file
53
awx/main/migrations/0009_v330_multi_credential.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from awx.main.migrations import _migration_utils as migration_utils
|
||||||
|
from awx.main.migrations._multi_cred import migrate_to_multi_cred
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0008_v320_drop_v1_credential_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='unifiedjob',
|
||||||
|
name='credentials',
|
||||||
|
field=models.ManyToManyField(related_name='unifiedjobs', to='main.Credential'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='unifiedjobtemplate',
|
||||||
|
name='credentials',
|
||||||
|
field=models.ManyToManyField(related_name='unifiedjobtemplates', to='main.Credential'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migration_utils.set_current_apps_for_migrations),
|
||||||
|
migrations.RunPython(migrate_to_multi_cred),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='job',
|
||||||
|
name='credential',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='job',
|
||||||
|
name='extra_credentials',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='job',
|
||||||
|
name='vault_credential',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='jobtemplate',
|
||||||
|
name='credential',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='jobtemplate',
|
||||||
|
name='extra_credentials',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='jobtemplate',
|
||||||
|
name='vault_credential',
|
||||||
|
),
|
||||||
|
]
|
||||||
12
awx/main/migrations/_multi_cred.py
Normal file
12
awx/main/migrations/_multi_cred.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
def migrate_to_multi_cred(app, schema_editor):
|
||||||
|
Job = app.get_model('main', 'Job')
|
||||||
|
JobTemplate = app.get_model('main', 'JobTemplate')
|
||||||
|
|
||||||
|
for cls in (Job, JobTemplate):
|
||||||
|
for j in cls.objects.iterator():
|
||||||
|
if j.credential:
|
||||||
|
j.credentials.add(j.credential)
|
||||||
|
if j.vault_credential:
|
||||||
|
j.credentials.add(j.vault_credential)
|
||||||
|
for cred in j.extra_credentials.all():
|
||||||
|
j.credentials.add(cred)
|
||||||
@@ -91,26 +91,6 @@ class JobOptions(BaseModel):
|
|||||||
default='',
|
default='',
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
credential = models.ForeignKey(
|
|
||||||
'Credential',
|
|
||||||
related_name='%(class)ss',
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
default=None,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
)
|
|
||||||
vault_credential = models.ForeignKey(
|
|
||||||
'Credential',
|
|
||||||
related_name='%(class)ss_as_vault_credential+',
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
default=None,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
)
|
|
||||||
extra_credentials = models.ManyToManyField(
|
|
||||||
'Credential',
|
|
||||||
related_name='%(class)ss_as_extra_credential+',
|
|
||||||
)
|
|
||||||
forks = models.PositiveIntegerField(
|
forks = models.PositiveIntegerField(
|
||||||
blank=True,
|
blank=True,
|
||||||
default=0,
|
default=0,
|
||||||
@@ -184,22 +164,31 @@ class JobOptions(BaseModel):
|
|||||||
)
|
)
|
||||||
return cred
|
return cred
|
||||||
|
|
||||||
@property
|
|
||||||
def all_credentials(self):
|
|
||||||
credentials = list(self.extra_credentials.all())
|
|
||||||
if self.vault_credential:
|
|
||||||
credentials.insert(0, self.vault_credential)
|
|
||||||
if self.credential:
|
|
||||||
credentials.insert(0, self.credential)
|
|
||||||
return credentials
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def network_credentials(self):
|
def network_credentials(self):
|
||||||
return [cred for cred in self.extra_credentials.all() if cred.credential_type.kind == 'net']
|
return [cred for cred in self.credentials.all() if cred.credential_type.kind == 'net']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cloud_credentials(self):
|
def cloud_credentials(self):
|
||||||
return [cred for cred in self.extra_credentials.all() if cred.credential_type.kind == 'cloud']
|
return [cred for cred in self.credentials.all() if cred.credential_type.kind == 'cloud']
|
||||||
|
|
||||||
|
@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):
|
||||||
|
try:
|
||||||
|
return [cred for cred in self.credentials.all() if cred.credential_type.kind == kind][0]
|
||||||
|
except IndexError:
|
||||||
|
return None
|
||||||
|
|
||||||
# TODO: remove when API v1 is removed
|
# TODO: remove when API v1 is removed
|
||||||
@property
|
@property
|
||||||
@@ -221,10 +210,8 @@ class JobOptions(BaseModel):
|
|||||||
def passwords_needed_to_start(self):
|
def passwords_needed_to_start(self):
|
||||||
'''Return list of password field names needed to start the job.'''
|
'''Return list of password field names needed to start the job.'''
|
||||||
needed = []
|
needed = []
|
||||||
if self.credential:
|
for cred in self.credentials.all():
|
||||||
needed.extend(self.credential.passwords_needed)
|
needed.extend(cred.passwords_needed)
|
||||||
if self.vault_credential:
|
|
||||||
needed.extend(self.vault_credential.passwords_needed)
|
|
||||||
return needed
|
return needed
|
||||||
|
|
||||||
|
|
||||||
@@ -234,6 +221,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
playbook) to an inventory source with a given credential.
|
playbook) to an inventory source with a given credential.
|
||||||
'''
|
'''
|
||||||
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name')]
|
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name')]
|
||||||
|
PASSWORD_FIELDS = ('credential', 'vault_credential')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
@@ -298,12 +286,12 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
@classmethod
|
@classmethod
|
||||||
def _get_unified_job_field_names(cls):
|
def _get_unified_job_field_names(cls):
|
||||||
return ['name', 'description', 'job_type', 'inventory', 'project',
|
return ['name', 'description', 'job_type', 'inventory', 'project',
|
||||||
'playbook', 'credential', 'vault_credential',
|
'playbook', 'credentials', 'forks', 'schedule', 'limit',
|
||||||
'extra_credentials', 'forks', 'schedule', 'limit', 'verbosity',
|
'verbosity', 'job_tags', 'extra_vars', 'launch_type',
|
||||||
'job_tags', 'extra_vars', 'launch_type', 'force_handlers',
|
'force_handlers', 'skip_tags', 'start_at_task',
|
||||||
'skip_tags', 'start_at_task', 'become_enabled', 'labels',
|
'become_enabled', 'labels', 'survey_passwords',
|
||||||
'survey_passwords', 'allow_simultaneous', 'timeout',
|
'allow_simultaneous', 'timeout', 'use_fact_cache',
|
||||||
'use_fact_cache', 'diff_mode',]
|
'diff_mode',]
|
||||||
|
|
||||||
def resource_validation_data(self):
|
def resource_validation_data(self):
|
||||||
'''
|
'''
|
||||||
@@ -317,10 +305,6 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
resources_needed_to_start.append('inventory')
|
resources_needed_to_start.append('inventory')
|
||||||
if not self.ask_inventory_on_launch:
|
if not self.ask_inventory_on_launch:
|
||||||
validation_errors['inventory'] = [_("Job Template must provide 'inventory' or allow prompting for it."),]
|
validation_errors['inventory'] = [_("Job Template must provide 'inventory' or allow prompting for it."),]
|
||||||
if self.credential is None and self.vault_credential is None:
|
|
||||||
resources_needed_to_start.append('credential')
|
|
||||||
if not self.ask_credential_on_launch:
|
|
||||||
validation_errors['credential'] = [_("Job Template must provide 'credential' or allow prompting for it."),]
|
|
||||||
|
|
||||||
# Job type dependent checks
|
# Job type dependent checks
|
||||||
if self.project is None:
|
if self.project is None:
|
||||||
@@ -379,9 +363,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
job_type=self.ask_job_type_on_launch,
|
job_type=self.ask_job_type_on_launch,
|
||||||
verbosity=self.ask_verbosity_on_launch,
|
verbosity=self.ask_verbosity_on_launch,
|
||||||
inventory=self.ask_inventory_on_launch,
|
inventory=self.ask_inventory_on_launch,
|
||||||
credential=self.ask_credential_on_launch,
|
credentials=self.ask_credential_on_launch,
|
||||||
vault_credential=self.ask_credential_on_launch,
|
|
||||||
extra_credentials=self.ask_credential_on_launch,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _accept_or_ignore_job_kwargs(self, **kwargs):
|
def _accept_or_ignore_job_kwargs(self, **kwargs):
|
||||||
@@ -715,17 +697,6 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana
|
|||||||
return self.global_instance_groups
|
return self.global_instance_groups
|
||||||
return selected_groups
|
return selected_groups
|
||||||
|
|
||||||
# Job Credential required
|
|
||||||
@property
|
|
||||||
def can_start(self):
|
|
||||||
if not super(Job, self).can_start:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not (self.credential) and not (self.vault_credential):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
JobNotificationMixin
|
JobNotificationMixin
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
|||||||
default='ok',
|
default='ok',
|
||||||
editable=False,
|
editable=False,
|
||||||
)
|
)
|
||||||
|
credentials = models.ManyToManyField(
|
||||||
|
'Credential',
|
||||||
|
related_name='%(class)ss',
|
||||||
|
)
|
||||||
labels = models.ManyToManyField(
|
labels = models.ManyToManyField(
|
||||||
"Label",
|
"Label",
|
||||||
blank=True,
|
blank=True,
|
||||||
@@ -579,6 +583,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
|||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
help_text=_('The Rampart/Instance group the job was run under'),
|
help_text=_('The Rampart/Instance group the job was run under'),
|
||||||
)
|
)
|
||||||
|
credentials = models.ManyToManyField(
|
||||||
|
'Credential',
|
||||||
|
related_name='%(class)ss',
|
||||||
|
)
|
||||||
|
|
||||||
def get_absolute_url(self, request=None):
|
def get_absolute_url(self, request=None):
|
||||||
real_instance = self.get_real_instance()
|
real_instance = self.get_real_instance()
|
||||||
|
|||||||
@@ -791,12 +791,15 @@ class BaseTask(LogErrorsTask):
|
|||||||
safe_env = self.build_safe_env(env, **kwargs)
|
safe_env = self.build_safe_env(env, **kwargs)
|
||||||
|
|
||||||
# handle custom injectors specified on the CredentialType
|
# handle custom injectors specified on the CredentialType
|
||||||
if hasattr(instance, 'all_credentials'):
|
credentials = []
|
||||||
credentials = instance.all_credentials
|
if isinstance(instance, Job):
|
||||||
|
credentials = instance.credentials.all()
|
||||||
elif hasattr(instance, 'credential'):
|
elif hasattr(instance, 'credential'):
|
||||||
|
# once other UnifiedJobs (project updates, inventory updates)
|
||||||
|
# move from a .credential -> .credentials model, we can
|
||||||
|
# lose this block
|
||||||
credentials = [instance.credential]
|
credentials = [instance.credential]
|
||||||
else:
|
|
||||||
credentials = []
|
|
||||||
for credential in credentials:
|
for credential in credentials:
|
||||||
if credential:
|
if credential:
|
||||||
credential.credential_type.inject_credential(
|
credential.credential_type.inject_credential(
|
||||||
@@ -927,7 +930,7 @@ class RunJob(BaseTask):
|
|||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
private_data = {'credentials': {}}
|
private_data = {'credentials': {}}
|
||||||
for credential in job.all_credentials:
|
for credential in job.credentials.all():
|
||||||
# If we were sent SSH credentials, decrypt them and send them
|
# If we were sent SSH credentials, decrypt them and send them
|
||||||
# back (they will be written to a temporary file).
|
# back (they will be written to a temporary file).
|
||||||
if credential.ssh_key_data not in (None, ''):
|
if credential.ssh_key_data not in (None, ''):
|
||||||
@@ -957,11 +960,11 @@ class RunJob(BaseTask):
|
|||||||
and ansible-vault.
|
and ansible-vault.
|
||||||
'''
|
'''
|
||||||
passwords = super(RunJob, self).build_passwords(job, **kwargs)
|
passwords = super(RunJob, self).build_passwords(job, **kwargs)
|
||||||
for cred, fields in {
|
for kind, fields in {
|
||||||
'credential': ('ssh_key_unlock', 'ssh_password', 'become_password'),
|
'ssh': ('ssh_key_unlock', 'ssh_password', 'become_password'),
|
||||||
'vault_credential': ('vault_password',)
|
'vault': ('vault_password',)
|
||||||
}.items():
|
}.items():
|
||||||
cred = getattr(job, cred, None)
|
cred = job.get_deprecated_credential(kind)
|
||||||
if cred:
|
if cred:
|
||||||
for field in fields:
|
for field in fields:
|
||||||
if field == 'ssh_password':
|
if field == 'ssh_password':
|
||||||
@@ -1072,7 +1075,8 @@ class RunJob(BaseTask):
|
|||||||
Build command line argument list for running ansible-playbook,
|
Build command line argument list for running ansible-playbook,
|
||||||
optionally using ssh-agent for public/private key authentication.
|
optionally using ssh-agent for public/private key authentication.
|
||||||
'''
|
'''
|
||||||
creds = job.credential
|
creds = job.get_deprecated_credential('ssh')
|
||||||
|
|
||||||
ssh_username, become_username, become_method = '', '', ''
|
ssh_username, become_username, become_method = '', '', ''
|
||||||
if creds:
|
if creds:
|
||||||
ssh_username = kwargs.get('username', creds.username)
|
ssh_username = kwargs.get('username', creds.username)
|
||||||
|
|||||||
@@ -141,11 +141,11 @@ def mk_job(job_type='run', status='new', job_template=None, inventory=None,
|
|||||||
|
|
||||||
job.job_template = job_template
|
job.job_template = job_template
|
||||||
job.inventory = inventory
|
job.inventory = inventory
|
||||||
job.credential = credential
|
|
||||||
job.project = project
|
|
||||||
|
|
||||||
if persisted:
|
if persisted:
|
||||||
job.save()
|
job.save()
|
||||||
|
job.credentials.add(credential)
|
||||||
|
job.project = project
|
||||||
|
|
||||||
return job
|
return job
|
||||||
|
|
||||||
|
|
||||||
@@ -164,9 +164,11 @@ def mk_job_template(name, job_type='run',
|
|||||||
if jt.inventory is None:
|
if jt.inventory is None:
|
||||||
jt.ask_inventory_on_launch = True
|
jt.ask_inventory_on_launch = True
|
||||||
|
|
||||||
jt.credential = credential
|
if persisted and credential:
|
||||||
if jt.credential is None:
|
jt.save()
|
||||||
jt.ask_credential_on_launch = True
|
jt.credentials.add(credential)
|
||||||
|
if jt.credential is None:
|
||||||
|
jt.ask_credential_on_launch = True
|
||||||
|
|
||||||
jt.project = project
|
jt.project = project
|
||||||
|
|
||||||
@@ -178,10 +180,10 @@ def mk_job_template(name, job_type='run',
|
|||||||
jt.save()
|
jt.save()
|
||||||
if cloud_credential:
|
if cloud_credential:
|
||||||
cloud_credential.save()
|
cloud_credential.save()
|
||||||
jt.extra_credentials.add(cloud_credential)
|
jt.credentials.add(cloud_credential)
|
||||||
if network_credential:
|
if network_credential:
|
||||||
network_credential.save()
|
network_credential.save()
|
||||||
jt.extra_credentials.add(network_credential)
|
jt.credentials.add(network_credential)
|
||||||
jt.save()
|
jt.save()
|
||||||
return jt
|
return jt
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import mock
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.main.models import Credential, Job
|
||||||
|
from awx.api.versioning import reverse
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def job_template(job_template, project, inventory):
|
||||||
|
job_template.playbook = 'helloworld.yml'
|
||||||
|
job_template.project = project
|
||||||
|
job_template.inventory = inventory
|
||||||
|
job_template.ask_credential_on_launch = True
|
||||||
|
job_template.save()
|
||||||
|
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
|
||||||
|
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 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 resp.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_extra_credentials_filtering(get, job_template, admin,
|
||||||
|
machine_credential, vault_credential, credential):
|
||||||
|
job_template.credentials.add(machine_credential)
|
||||||
|
job_template.credentials.add(vault_credential)
|
||||||
|
job_template.credentials.add(credential)
|
||||||
|
url = reverse(
|
||||||
|
'api:job_template_extra_credentials_list',
|
||||||
|
kwargs={'version': 'v2', 'pk': job_template.pk}
|
||||||
|
)
|
||||||
|
resp = get(url, admin, expect=200)
|
||||||
|
assert resp.data['count'] == 1
|
||||||
|
assert resp.data['results'][0]['id'] == credential.pk
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_extra_credentials_requires_cloud_or_net(get, post, job_template, admin,
|
||||||
|
machine_credential, vault_credential, credential,
|
||||||
|
net_credential):
|
||||||
|
url = reverse(
|
||||||
|
'api:job_template_extra_credentials_list',
|
||||||
|
kwargs={'version': 'v2', 'pk': job_template.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
for cred in (machine_credential, vault_credential):
|
||||||
|
resp = post(url, {'associate': True, 'id': cred.pk}, admin, expect=400)
|
||||||
|
assert 'Extra credentials must be network or cloud.' in resp.content
|
||||||
|
|
||||||
|
post(url, {'associate': True, 'id': credential.pk}, admin, expect=204)
|
||||||
|
assert get(url, admin).data['count'] == 1
|
||||||
|
|
||||||
|
post(url, {'associate': True, 'id': net_credential.pk}, admin, expect=204)
|
||||||
|
assert get(url, admin).data['count'] == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_prevent_multiple_machine_creds(get, post, job_template, admin, machine_credential):
|
||||||
|
url = reverse(
|
||||||
|
'api:job_template_credentials_list',
|
||||||
|
kwargs={'version': 'v2', 'pk': job_template.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _new_cred(name):
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'credential_type': machine_credential.credential_type.pk,
|
||||||
|
'inputs': {
|
||||||
|
'username': 'bob',
|
||||||
|
'password': 'secret',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post(url, _new_cred('First Cred'), admin, expect=201)
|
||||||
|
assert get(url, admin).data['count'] == 1
|
||||||
|
|
||||||
|
resp = post(url, _new_cred('Second Cred'), admin, expect=400)
|
||||||
|
assert 'Cannot assign multiple Machine credentials.' in resp.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_prevent_multiple_machine_creds_at_launch(get, post, job_template, admin, machine_credential):
|
||||||
|
other_cred = Credential(credential_type=machine_credential.credential_type, name="Second",
|
||||||
|
inputs={'username': 'bob'})
|
||||||
|
other_cred.save()
|
||||||
|
creds = [machine_credential.pk, other_cred.pk]
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
resp = post(url, {'credentials': creds}, admin)
|
||||||
|
assert 'Cannot assign multiple Machine credentials.' in resp.content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_extra_credentials_unique_by_kind(get, post, job_template, admin,
|
||||||
|
credentialtype_aws):
|
||||||
|
url = reverse(
|
||||||
|
'api:job_template_extra_credentials_list',
|
||||||
|
kwargs={'version': 'v2', 'pk': job_template.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _new_cred(name):
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'credential_type': credentialtype_aws.pk,
|
||||||
|
'inputs': {
|
||||||
|
'username': 'bob',
|
||||||
|
'password': 'secret',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post(url, _new_cred('First Cred'), admin, expect=201)
|
||||||
|
assert get(url, admin).data['count'] == 1
|
||||||
|
|
||||||
|
resp = post(url, _new_cred('Second Cred'), admin, expect=400)
|
||||||
|
assert 'Cannot assign multiple Amazon Web Services credentials.' in 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})
|
||||||
|
pk = post(url, {'extra_credentials': [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_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)
|
||||||
|
job_template.save()
|
||||||
|
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)
|
||||||
|
job_template.save()
|
||||||
|
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):
|
||||||
|
job_template.credentials.add(machine_credential)
|
||||||
|
job_template.credentials.add(vault_credential)
|
||||||
|
job_template.save()
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
pk = post(url, {'extra_credentials': [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_overwrite_ssh_credential_at_launch(get, post, job_template, admin, machine_credential):
|
||||||
|
job_template.credentials.add(machine_credential)
|
||||||
|
job_template.save()
|
||||||
|
|
||||||
|
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)
|
||||||
|
job_template.save()
|
||||||
|
machine_credential.inputs['password'] = 'ASK'
|
||||||
|
machine_credential.save()
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
resp = get(url, admin)
|
||||||
|
assert 'ssh_password' in resp.data['passwords_needed_to_start']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_prompted_credential_removed_on_launch(get, post, job_template, admin, machine_credential):
|
||||||
|
# If a JT has a credential that needs a password, but the launch POST
|
||||||
|
# specifies {"credentials": []}, don't require any passwords
|
||||||
|
job_template.credentials.add(machine_credential)
|
||||||
|
job_template.save()
|
||||||
|
machine_credential.inputs['password'] = 'ASK'
|
||||||
|
machine_credential.save()
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
resp = post(url, {}, admin, expect=400)
|
||||||
|
resp = post(url, {'credentials': []}, admin, expect=201)
|
||||||
|
assert 'job' in resp.data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_ssh_credential_with_password_at_launch(get, post, job_template, admin, machine_credential):
|
||||||
|
machine_credential.inputs['password'] = 'ASK'
|
||||||
|
machine_credential.save()
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
resp = post(url, {'credentials': [machine_credential.pk]}, admin, expect=400)
|
||||||
|
assert resp.data['passwords_needed_to_start'] == ['ssh_password']
|
||||||
|
|
||||||
|
with mock.patch.object(Job, 'signal_start') as signal_start:
|
||||||
|
resp = post(url, {
|
||||||
|
'credentials': [machine_credential.pk],
|
||||||
|
'ssh_password': 'testing123'
|
||||||
|
}, admin, expect=201)
|
||||||
|
signal_start.assert_called_with(ssh_password='testing123')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_vault_password_prompted_at_launch(get, post, job_template, admin, vault_credential):
|
||||||
|
job_template.credentials.add(vault_credential)
|
||||||
|
job_template.save()
|
||||||
|
vault_credential.inputs['vault_password'] = 'ASK'
|
||||||
|
vault_credential.save()
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
resp = get(url, admin)
|
||||||
|
assert 'vault_password' in resp.data['passwords_needed_to_start']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_vault_credential_with_password_at_launch(get, post, job_template, admin, vault_credential):
|
||||||
|
vault_credential.inputs['vault_password'] = 'ASK'
|
||||||
|
vault_credential.save()
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
|
||||||
|
resp = post(url, {'credentials': [vault_credential.pk]}, admin, expect=400)
|
||||||
|
assert resp.data['passwords_needed_to_start'] == ['vault_password']
|
||||||
|
|
||||||
|
with mock.patch.object(Job, 'signal_start') as signal_start:
|
||||||
|
resp = post(url, {
|
||||||
|
'credentials': [vault_credential.pk],
|
||||||
|
'vault_password': 'testing123'
|
||||||
|
}, admin, expect=201)
|
||||||
|
signal_start.assert_called_with(vault_password='testing123')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_extra_creds_prompted_at_launch(get, post, job_template, admin, net_credential):
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
resp = post(url, {'extra_credentials': [net_credential.pk]}, admin, expect=201)
|
||||||
|
|
||||||
|
summary_fields = get(
|
||||||
|
reverse('api:job_detail', kwargs={'pk': resp.data['job']}),
|
||||||
|
admin
|
||||||
|
).data['summary_fields']
|
||||||
|
assert len(summary_fields['credentials']) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_invalid_mixed_credentials_specification(get, post, job_template, admin, net_credential):
|
||||||
|
url = reverse('api:job_template_launch', kwargs={'pk': job_template.pk})
|
||||||
|
post(url, {'credentials': [net_credential.pk], 'extra_credentials': [net_credential.pk]}, admin, expect=400)
|
||||||
@@ -10,7 +10,7 @@ def test_extra_credentials(get, organization_factory, job_template_factory, cred
|
|||||||
objs = organization_factory("org", superusers=['admin'])
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
jt = job_template_factory("jt", organization=objs.organization,
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
inventory='test_inv', project='test_proj').job_template
|
inventory='test_inv', project='test_proj').job_template
|
||||||
jt.extra_credentials.add(credential)
|
jt.credentials.add(credential)
|
||||||
jt.save()
|
jt.save()
|
||||||
job = jt.create_unified_job()
|
job = jt.create_unified_job()
|
||||||
|
|
||||||
@@ -22,8 +22,8 @@ def test_extra_credentials(get, organization_factory, job_template_factory, cred
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_relaunch_permission_denied_response(
|
def test_job_relaunch_permission_denied_response(
|
||||||
post, get, inventory, project, credential, net_credential, machine_credential):
|
post, get, inventory, project, credential, net_credential, machine_credential):
|
||||||
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project,
|
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project)
|
||||||
credential=machine_credential)
|
jt.credentials.add(machine_credential)
|
||||||
jt_user = User.objects.create(username='jobtemplateuser')
|
jt_user = User.objects.create(username='jobtemplateuser')
|
||||||
jt.execute_role.members.add(jt_user)
|
jt.execute_role.members.add(jt_user)
|
||||||
job = jt.create_unified_job()
|
job = jt.create_unified_job()
|
||||||
@@ -33,7 +33,7 @@ def test_job_relaunch_permission_denied_response(
|
|||||||
assert r.data['summary_fields']['user_capabilities']['start']
|
assert r.data['summary_fields']['user_capabilities']['start']
|
||||||
|
|
||||||
# Job has prompted extra_credential, launch denied w/ message
|
# Job has prompted extra_credential, launch denied w/ message
|
||||||
job.extra_credentials.add(net_credential)
|
job.credentials.add(net_credential)
|
||||||
r = post(reverse('api:job_relaunch', kwargs={'pk':job.pk}), {}, jt_user, expect=403)
|
r = post(reverse('api:job_relaunch', kwargs={'pk':job.pk}), {}, jt_user, expect=403)
|
||||||
assert 'launched with prompted fields' in r.data['detail']
|
assert 'launched with prompted fields' in r.data['detail']
|
||||||
assert 'do not have permission' in r.data['detail']
|
assert 'do not have permission' in r.data['detail']
|
||||||
@@ -50,8 +50,9 @@ def test_job_relaunch_on_failed_hosts(post, inventory, project, machine_credenti
|
|||||||
h3 = inventory.hosts.create(name='host3') # failed host
|
h3 = inventory.hosts.create(name='host3') # failed host
|
||||||
jt = JobTemplate.objects.create(
|
jt = JobTemplate.objects.create(
|
||||||
name='testjt', inventory=inventory,
|
name='testjt', inventory=inventory,
|
||||||
project=project, credential=machine_credential
|
project=project
|
||||||
)
|
)
|
||||||
|
jt.credentials.add(machine_credential)
|
||||||
job = jt.create_unified_job(_eager_fields={'status': 'failed', 'limit': 'host1,host2,host3'})
|
job = jt.create_unified_job(_eager_fields={'status': 'failed', 'limit': 'host1,host2,host3'})
|
||||||
job.job_events.create(event='playbook_on_stats')
|
job.job_events.create(event='playbook_on_stats')
|
||||||
job.job_host_summaries.create(host=h1, failed=False, ok=1, changed=0, failures=0, host_name=h1.name)
|
job.job_host_summaries.create(host=h1, failed=False, ok=1, changed=0, failures=0, host_name=h1.name)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def runtime_data(organization, credentialtype_ssh):
|
|||||||
job_tags='provision',
|
job_tags='provision',
|
||||||
skip_tags='restart',
|
skip_tags='restart',
|
||||||
inventory=inv_obj.pk,
|
inventory=inv_obj.pk,
|
||||||
credential=cred_obj.pk,
|
credentials=[cred_obj.pk],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,11 +40,10 @@ def job_with_links(machine_credential, inventory):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def job_template_prompts(project, inventory, machine_credential):
|
def job_template_prompts(project, inventory, machine_credential):
|
||||||
def rf(on_off):
|
def rf(on_off):
|
||||||
return JobTemplate.objects.create(
|
jt = JobTemplate.objects.create(
|
||||||
job_type='run',
|
job_type='run',
|
||||||
project=project,
|
project=project,
|
||||||
inventory=inventory,
|
inventory=inventory,
|
||||||
credential=machine_credential,
|
|
||||||
name='deploy-job-template',
|
name='deploy-job-template',
|
||||||
ask_variables_on_launch=on_off,
|
ask_variables_on_launch=on_off,
|
||||||
ask_tags_on_launch=on_off,
|
ask_tags_on_launch=on_off,
|
||||||
@@ -55,6 +54,8 @@ def job_template_prompts(project, inventory, machine_credential):
|
|||||||
ask_credential_on_launch=on_off,
|
ask_credential_on_launch=on_off,
|
||||||
ask_verbosity_on_launch=on_off,
|
ask_verbosity_on_launch=on_off,
|
||||||
)
|
)
|
||||||
|
jt.credentials.add(machine_credential)
|
||||||
|
return jt
|
||||||
return rf
|
return rf
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +65,6 @@ def job_template_prompts_null(project):
|
|||||||
job_type='run',
|
job_type='run',
|
||||||
project=project,
|
project=project,
|
||||||
inventory=None,
|
inventory=None,
|
||||||
credential=None,
|
|
||||||
name='deploy-job-template',
|
name='deploy-job-template',
|
||||||
ask_variables_on_launch=True,
|
ask_variables_on_launch=True,
|
||||||
ask_tags_on_launch=True,
|
ask_tags_on_launch=True,
|
||||||
@@ -87,7 +87,7 @@ def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, ad
|
|||||||
|
|
||||||
with mocker.patch.object(JobTemplate, 'create_unified_job', return_value=mock_job):
|
with mocker.patch.object(JobTemplate, 'create_unified_job', return_value=mock_job):
|
||||||
with mocker.patch('awx.api.serializers.JobSerializer.to_representation'):
|
with mocker.patch('awx.api.serializers.JobSerializer.to_representation'):
|
||||||
response = post(reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
|
response = post(reverse('api:job_template_launch', kwargs={'pk': job_template.pk}),
|
||||||
runtime_data, admin_user, expect=201)
|
runtime_data, admin_user, expect=201)
|
||||||
assert JobTemplate.create_unified_job.called
|
assert JobTemplate.create_unified_job.called
|
||||||
assert JobTemplate.create_unified_job.call_args == ({'extra_vars':{}},)
|
assert JobTemplate.create_unified_job.call_args == ({'extra_vars':{}},)
|
||||||
@@ -104,7 +104,7 @@ def test_job_ignore_unprompted_vars(runtime_data, job_template_prompts, post, ad
|
|||||||
assert 'job_type' in response.data['ignored_fields']
|
assert 'job_type' in response.data['ignored_fields']
|
||||||
assert 'limit' in response.data['ignored_fields']
|
assert 'limit' in response.data['ignored_fields']
|
||||||
assert 'inventory' in response.data['ignored_fields']
|
assert 'inventory' in response.data['ignored_fields']
|
||||||
assert 'credential' in response.data['ignored_fields']
|
assert 'credentials' in response.data['ignored_fields']
|
||||||
assert 'job_tags' in response.data['ignored_fields']
|
assert 'job_tags' in response.data['ignored_fields']
|
||||||
assert 'skip_tags' in response.data['ignored_fields']
|
assert 'skip_tags' in response.data['ignored_fields']
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ def test_job_accept_prompted_vars_null(runtime_data, job_template_prompts_null,
|
|||||||
job_template.execute_role.members.add(rando)
|
job_template.execute_role.members.add(rando)
|
||||||
|
|
||||||
# Give user permission to use inventory and credential at runtime
|
# Give user permission to use inventory and credential at runtime
|
||||||
credential = Credential.objects.get(pk=runtime_data['credential'])
|
credential = Credential.objects.get(pk=runtime_data['credentials'][0])
|
||||||
credential.use_role.members.add(rando)
|
credential.use_role.members.add(rando)
|
||||||
inventory = Inventory.objects.get(pk=runtime_data['inventory'])
|
inventory = Inventory.objects.get(pk=runtime_data['inventory'])
|
||||||
inventory.use_role.members.add(rando)
|
inventory.use_role.members.add(rando)
|
||||||
@@ -182,11 +182,11 @@ def test_job_reject_invalid_prompted_vars(runtime_data, job_template_prompts, po
|
|||||||
response = post(
|
response = post(
|
||||||
reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
|
reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
|
||||||
dict(job_type='foobicate', # foobicate is not a valid job type
|
dict(job_type='foobicate', # foobicate is not a valid job type
|
||||||
inventory=87865, credential=48474), admin_user, expect=400)
|
inventory=87865, credentials=[48474]), admin_user, expect=400)
|
||||||
|
|
||||||
assert response.data['job_type'] == [u'"foobicate" is not a valid choice.']
|
assert response.data['job_type'] == [u'"foobicate" is not a valid choice.']
|
||||||
assert response.data['inventory'] == [u'Invalid pk "87865" - object does not exist.']
|
assert response.data['inventory'] == [u'Invalid pk "87865" - object does not exist.']
|
||||||
assert response.data['credential'] == [u'Invalid pk "48474" - object does not exist.']
|
assert response.data['credentials'] == [u'Invalid pk "48474" - object does not exist.']
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -235,7 +235,7 @@ def test_job_launch_fails_without_credential_access(job_template_prompts, runtim
|
|||||||
|
|
||||||
# Assure that giving a credential without access blocks the launch
|
# Assure that giving a credential without access blocks the launch
|
||||||
response = post(reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
|
response = post(reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
|
||||||
dict(credential=runtime_data['credential']), rando, expect=403)
|
dict(credentials=runtime_data['credentials']), rando, expect=403)
|
||||||
|
|
||||||
assert response.data['detail'] == u'You do not have permission to perform this action.'
|
assert response.data['detail'] == u'You do not have permission to perform this action.'
|
||||||
|
|
||||||
@@ -258,7 +258,7 @@ def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
|
|||||||
deploy_jobtemplate.ask_credential_on_launch = True
|
deploy_jobtemplate.ask_credential_on_launch = True
|
||||||
deploy_jobtemplate.save()
|
deploy_jobtemplate.save()
|
||||||
|
|
||||||
kv = dict(extra_vars={"job_launch_var": 4}, credential=machine_credential.id)
|
kv = dict(extra_vars={"job_launch_var": 4}, credentials=[machine_credential.id])
|
||||||
serializer = JobLaunchSerializer(
|
serializer = JobLaunchSerializer(
|
||||||
instance=deploy_jobtemplate, data=kv,
|
instance=deploy_jobtemplate, data=kv,
|
||||||
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
||||||
@@ -270,106 +270,15 @@ def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
|
|||||||
final_job_extra_vars = yaml.load(job_obj.extra_vars)
|
final_job_extra_vars = yaml.load(job_obj.extra_vars)
|
||||||
assert 'job_template_var' in final_job_extra_vars
|
assert 'job_template_var' in final_job_extra_vars
|
||||||
assert 'job_launch_var' in final_job_extra_vars
|
assert 'job_launch_var' in final_job_extra_vars
|
||||||
assert job_obj.credential.id == machine_credential.id
|
assert [cred.pk for cred in job_obj.credentials.all()] == [machine_credential.id]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.parametrize('pks, error_msg', [
|
def test_job_launch_with_default_creds(machine_credential, vault_credential, deploy_jobtemplate):
|
||||||
([1], 'must be network or cloud'),
|
|
||||||
([999], 'object does not exist'),
|
|
||||||
])
|
|
||||||
def test_job_launch_JT_with_invalid_extra_credentials(machine_credential, deploy_jobtemplate, pks, error_msg):
|
|
||||||
deploy_jobtemplate.ask_credential_on_launch = True
|
deploy_jobtemplate.ask_credential_on_launch = True
|
||||||
deploy_jobtemplate.save()
|
deploy_jobtemplate.credentials.add(machine_credential)
|
||||||
|
deploy_jobtemplate.credentials.add(vault_credential)
|
||||||
kv = dict(extra_credentials=pks, credential=machine_credential.id)
|
kv = dict()
|
||||||
serializer = JobLaunchSerializer(
|
|
||||||
instance=deploy_jobtemplate, data=kv,
|
|
||||||
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
|
||||||
validated = serializer.is_valid()
|
|
||||||
assert validated is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_job_launch_JT_enforces_unique_extra_credential_kinds(machine_credential, credentialtype_aws, deploy_jobtemplate):
|
|
||||||
"""
|
|
||||||
JT launching should require that extra_credentials have distinct CredentialTypes
|
|
||||||
"""
|
|
||||||
pks = []
|
|
||||||
for i in range(2):
|
|
||||||
aws = Credential.objects.create(
|
|
||||||
name='cred-%d' % i,
|
|
||||||
credential_type=credentialtype_aws,
|
|
||||||
inputs={
|
|
||||||
'username': 'test_user',
|
|
||||||
'password': 'pas4word'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
aws.save()
|
|
||||||
pks.append(aws.pk)
|
|
||||||
|
|
||||||
kv = dict(extra_credentials=pks, credential=machine_credential.id)
|
|
||||||
serializer = JobLaunchSerializer(
|
|
||||||
instance=deploy_jobtemplate, data=kv,
|
|
||||||
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
|
||||||
validated = serializer.is_valid()
|
|
||||||
assert validated is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@pytest.mark.parametrize('ask_credential_on_launch', [True, False])
|
|
||||||
def test_job_launch_with_no_credentials(deploy_jobtemplate, ask_credential_on_launch):
|
|
||||||
deploy_jobtemplate.credential = None
|
|
||||||
deploy_jobtemplate.vault_credential = None
|
|
||||||
deploy_jobtemplate.ask_credential_on_launch = ask_credential_on_launch
|
|
||||||
serializer = JobLaunchSerializer(
|
|
||||||
instance=deploy_jobtemplate, data={},
|
|
||||||
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
|
|
||||||
validated = serializer.is_valid()
|
|
||||||
assert validated is False
|
|
||||||
assert serializer.errors['credential'] == ["Job Template 'credential' is missing or undefined."]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_job_launch_with_only_vault_credential(vault_credential, deploy_jobtemplate):
|
|
||||||
deploy_jobtemplate.credential = None
|
|
||||||
deploy_jobtemplate.vault_credential = vault_credential
|
|
||||||
serializer = JobLaunchSerializer(
|
|
||||||
instance=deploy_jobtemplate, data={},
|
|
||||||
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
|
|
||||||
validated = serializer.is_valid()
|
|
||||||
assert validated
|
|
||||||
|
|
||||||
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**{})
|
|
||||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
|
||||||
|
|
||||||
assert job_obj.vault_credential.pk == vault_credential.pk
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_job_launch_with_vault_credential_ask_for_machine(vault_credential, deploy_jobtemplate):
|
|
||||||
deploy_jobtemplate.credential = None
|
|
||||||
deploy_jobtemplate.ask_credential_on_launch = True
|
|
||||||
deploy_jobtemplate.vault_credential = vault_credential
|
|
||||||
serializer = JobLaunchSerializer(
|
|
||||||
instance=deploy_jobtemplate, data={},
|
|
||||||
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
|
|
||||||
validated = serializer.is_valid()
|
|
||||||
assert validated
|
|
||||||
|
|
||||||
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**{})
|
|
||||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
|
||||||
assert job_obj.credential is None
|
|
||||||
assert job_obj.vault_credential.pk == vault_credential.pk
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_job_launch_with_vault_credential_and_prompted_machine_cred(machine_credential, vault_credential,
|
|
||||||
deploy_jobtemplate):
|
|
||||||
deploy_jobtemplate.credential = None
|
|
||||||
deploy_jobtemplate.ask_credential_on_launch = True
|
|
||||||
deploy_jobtemplate.vault_credential = vault_credential
|
|
||||||
kv = dict(credential=machine_credential.id)
|
|
||||||
serializer = JobLaunchSerializer(
|
serializer = JobLaunchSerializer(
|
||||||
instance=deploy_jobtemplate, data=kv,
|
instance=deploy_jobtemplate, data=kv,
|
||||||
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
||||||
@@ -378,24 +287,26 @@ def test_job_launch_with_vault_credential_and_prompted_machine_cred(machine_cred
|
|||||||
|
|
||||||
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
|
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
|
||||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
||||||
assert job_obj.credential.pk == machine_credential.pk
|
assert job_obj.credential == machine_credential.pk
|
||||||
assert job_obj.vault_credential.pk == vault_credential.pk
|
assert job_obj.vault_credential == vault_credential.pk
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_job_launch_JT_with_default_vault_credential(machine_credential, vault_credential, deploy_jobtemplate):
|
def test_job_launch_with_empty_creds(machine_credential, vault_credential, deploy_jobtemplate):
|
||||||
deploy_jobtemplate.credential = machine_credential
|
deploy_jobtemplate.ask_credential_on_launch = True
|
||||||
deploy_jobtemplate.vault_credential = vault_credential
|
deploy_jobtemplate.credentials.add(machine_credential)
|
||||||
|
deploy_jobtemplate.credentials.add(vault_credential)
|
||||||
|
kv = dict(credentials=[])
|
||||||
serializer = JobLaunchSerializer(
|
serializer = JobLaunchSerializer(
|
||||||
instance=deploy_jobtemplate, data={},
|
instance=deploy_jobtemplate, data=kv,
|
||||||
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
|
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
||||||
validated = serializer.is_valid()
|
validated = serializer.is_valid()
|
||||||
assert validated
|
assert validated
|
||||||
|
|
||||||
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**{})
|
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
|
||||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
||||||
|
assert job_obj.credential is None
|
||||||
assert job_obj.vault_credential.pk == vault_credential.pk
|
assert job_obj.vault_credential is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -403,8 +314,7 @@ def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_
|
|||||||
deploy_jobtemplate, post, rando):
|
deploy_jobtemplate, post, rando):
|
||||||
vault_credential.vault_password = 'ASK'
|
vault_credential.vault_password = 'ASK'
|
||||||
vault_credential.save()
|
vault_credential.save()
|
||||||
deploy_jobtemplate.credential = machine_credential
|
deploy_jobtemplate.credentials.add(vault_credential)
|
||||||
deploy_jobtemplate.vault_credential = vault_credential
|
|
||||||
deploy_jobtemplate.execute_role.members.add(rando)
|
deploy_jobtemplate.execute_role.members.add(rando)
|
||||||
deploy_jobtemplate.save()
|
deploy_jobtemplate.save()
|
||||||
|
|
||||||
@@ -421,7 +331,7 @@ def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_j
|
|||||||
rando):
|
rando):
|
||||||
machine_credential.password = 'ASK'
|
machine_credential.password = 'ASK'
|
||||||
machine_credential.save()
|
machine_credential.save()
|
||||||
deploy_jobtemplate.credential = machine_credential
|
deploy_jobtemplate.credentials.add(machine_credential)
|
||||||
deploy_jobtemplate.execute_role.members.add(rando)
|
deploy_jobtemplate.execute_role.members.add(rando)
|
||||||
deploy_jobtemplate.save()
|
deploy_jobtemplate.save()
|
||||||
|
|
||||||
@@ -440,8 +350,8 @@ def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential
|
|||||||
vault_credential.save()
|
vault_credential.save()
|
||||||
machine_credential.password = 'ASK'
|
machine_credential.password = 'ASK'
|
||||||
machine_credential.save()
|
machine_credential.save()
|
||||||
deploy_jobtemplate.credential = machine_credential
|
deploy_jobtemplate.credentials.add(machine_credential)
|
||||||
deploy_jobtemplate.vault_credential = vault_credential
|
deploy_jobtemplate.credentials.add(vault_credential)
|
||||||
deploy_jobtemplate.execute_role.members.add(rando)
|
deploy_jobtemplate.execute_role.members.add(rando)
|
||||||
deploy_jobtemplate.save()
|
deploy_jobtemplate.save()
|
||||||
|
|
||||||
@@ -458,8 +368,8 @@ def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_
|
|||||||
deploy_jobtemplate, post, rando):
|
deploy_jobtemplate, post, rando):
|
||||||
vault_credential.vault_password = 'ASK'
|
vault_credential.vault_password = 'ASK'
|
||||||
vault_credential.save()
|
vault_credential.save()
|
||||||
deploy_jobtemplate.credential = machine_credential
|
deploy_jobtemplate.credentials.add(machine_credential)
|
||||||
deploy_jobtemplate.vault_credential = vault_credential
|
deploy_jobtemplate.credentials.add(vault_credential)
|
||||||
deploy_jobtemplate.execute_role.members.add(rando)
|
deploy_jobtemplate.execute_role.members.add(rando)
|
||||||
deploy_jobtemplate.save()
|
deploy_jobtemplate.save()
|
||||||
|
|
||||||
@@ -473,27 +383,6 @@ def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_
|
|||||||
signal_start.assert_called_with(vault_password='vault-me')
|
signal_start.assert_called_with(vault_password='vault-me')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_job_launch_JT_with_extra_credentials(machine_credential, credential, net_credential, deploy_jobtemplate):
|
|
||||||
deploy_jobtemplate.ask_credential_on_launch = True
|
|
||||||
deploy_jobtemplate.save()
|
|
||||||
|
|
||||||
kv = dict(extra_credentials=[credential.pk, net_credential.pk], credential=machine_credential.id)
|
|
||||||
serializer = JobLaunchSerializer(
|
|
||||||
instance=deploy_jobtemplate, data=kv,
|
|
||||||
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
|
||||||
validated = serializer.is_valid()
|
|
||||||
assert validated
|
|
||||||
|
|
||||||
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
|
|
||||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
|
||||||
|
|
||||||
extra_creds = job_obj.extra_credentials.all()
|
|
||||||
assert len(extra_creds) == 2
|
|
||||||
assert credential in extra_creds
|
|
||||||
assert net_credential in extra_creds
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.job_runtime_vars
|
@pytest.mark.job_runtime_vars
|
||||||
def test_job_launch_unprompted_vars_with_survey(mocker, survey_spec_factory, job_template_prompts, post, admin_user):
|
def test_job_launch_unprompted_vars_with_survey(mocker, survey_spec_factory, job_template_prompts, post, admin_user):
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def test_create(post, project, machine_credential, inventory, alice, grant_proje
|
|||||||
post(reverse('api:job_template_list'), {
|
post(reverse('api:job_template_list'), {
|
||||||
'name': 'Some name',
|
'name': 'Some name',
|
||||||
'project': project.id,
|
'project': project.id,
|
||||||
'credential': machine_credential.id,
|
'credentials': [machine_credential.id],
|
||||||
'inventory': inventory.id,
|
'inventory': inventory.id,
|
||||||
'playbook': 'helloworld.yml',
|
'playbook': 'helloworld.yml',
|
||||||
}, alice, expect=expect)
|
}, alice, expect=expect)
|
||||||
@@ -184,7 +184,7 @@ def test_detach_extra_credential(get, post, organization_factory, job_template_f
|
|||||||
objs = organization_factory("org", superusers=['admin'])
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
jt = job_template_factory("jt", organization=objs.organization,
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
inventory='test_inv', project='test_proj').job_template
|
inventory='test_inv', project='test_proj').job_template
|
||||||
jt.extra_credentials.add(credential)
|
jt.credentials.add(credential)
|
||||||
jt.save()
|
jt.save()
|
||||||
|
|
||||||
url = reverse('api:job_template_extra_credentials_list', kwargs={'version': 'v2', 'pk': jt.pk})
|
url = reverse('api:job_template_extra_credentials_list', kwargs={'version': 'v2', 'pk': jt.pk})
|
||||||
@@ -222,8 +222,8 @@ def test_v1_extra_credentials_detail(get, organization_factory, job_template_fac
|
|||||||
objs = organization_factory("org", superusers=['admin'])
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
jt = job_template_factory("jt", organization=objs.organization,
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
inventory='test_inv', project='test_proj').job_template
|
inventory='test_inv', project='test_proj').job_template
|
||||||
jt.extra_credentials.add(credential)
|
jt.credentials.add(credential)
|
||||||
jt.extra_credentials.add(net_credential)
|
jt.credentials.add(net_credential)
|
||||||
jt.save()
|
jt.save()
|
||||||
|
|
||||||
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
|
url = reverse('api:job_template_detail', kwargs={'version': 'v1', 'pk': jt.pk})
|
||||||
@@ -272,8 +272,8 @@ def test_filter_by_v1(get, organization_factory, job_template_factory, credentia
|
|||||||
objs = organization_factory("org", superusers=['admin'])
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
jt = job_template_factory("jt", organization=objs.organization,
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
inventory='test_inv', project='test_proj').job_template
|
inventory='test_inv', project='test_proj').job_template
|
||||||
jt.extra_credentials.add(credential)
|
jt.credentials.add(credential)
|
||||||
jt.extra_credentials.add(net_credential)
|
jt.credentials.add(net_credential)
|
||||||
jt.save()
|
jt.save()
|
||||||
|
|
||||||
for query in (
|
for query in (
|
||||||
@@ -312,7 +312,7 @@ def test_edit_sensitive_fields(patch, job_template_factory, alice, grant_project
|
|||||||
patch(reverse('api:job_template_detail', kwargs={'pk': objs.job_template.id}), {
|
patch(reverse('api:job_template_detail', kwargs={'pk': objs.job_template.id}), {
|
||||||
'name': 'Some name',
|
'name': 'Some name',
|
||||||
'project': objs.project.id,
|
'project': objs.project.id,
|
||||||
'credential': objs.credential.id,
|
'credentials': [objs.credential.id],
|
||||||
'inventory': objs.inventory.id,
|
'inventory': objs.inventory.id,
|
||||||
'playbook': 'alt-helloworld.yml',
|
'playbook': 'alt-helloworld.yml',
|
||||||
}, alice, expect=expect)
|
}, alice, expect=expect)
|
||||||
@@ -459,8 +459,7 @@ def test_launch_with_extra_credentials(get, post, organization_factory,
|
|||||||
resp = post(
|
resp = post(
|
||||||
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
|
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
|
||||||
dict(
|
dict(
|
||||||
credential=machine_credential.pk,
|
credentials=[machine_credential.pk, credential.pk, net_credential.pk]
|
||||||
extra_credentials=[credential.pk, net_credential.pk]
|
|
||||||
),
|
),
|
||||||
objs.superusers.admin, expect=201
|
objs.superusers.admin, expect=201
|
||||||
)
|
)
|
||||||
@@ -480,83 +479,24 @@ def test_launch_with_extra_credentials_not_allowed(get, post, organization_facto
|
|||||||
objs = organization_factory("org", superusers=['admin'])
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
jt = job_template_factory("jt", organization=objs.organization,
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
inventory='test_inv', project='test_proj').job_template
|
inventory='test_inv', project='test_proj').job_template
|
||||||
jt.credential = machine_credential
|
jt.credentials.add(machine_credential)
|
||||||
jt.ask_credential_on_launch = False
|
jt.ask_credential_on_launch = False
|
||||||
jt.save()
|
jt.save()
|
||||||
|
|
||||||
resp = post(
|
resp = post(
|
||||||
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
|
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
|
||||||
dict(
|
dict(
|
||||||
credential=machine_credential.pk,
|
credentials=[machine_credential.pk, credential.pk, net_credential.pk]
|
||||||
extra_credentials=[credential.pk, net_credential.pk]
|
|
||||||
),
|
),
|
||||||
objs.superusers.admin
|
objs.superusers.admin
|
||||||
)
|
)
|
||||||
assert 'credential' in resp.data['ignored_fields'].keys()
|
assert 'credentials' in resp.data['ignored_fields'].keys()
|
||||||
assert 'extra_credentials' in resp.data['ignored_fields'].keys()
|
|
||||||
job_pk = resp.data.get('id')
|
job_pk = resp.data.get('id')
|
||||||
|
|
||||||
resp = get(reverse('api:job_extra_credentials_list', kwargs={'pk': job_pk}), objs.superusers.admin)
|
resp = get(reverse('api:job_extra_credentials_list', kwargs={'pk': job_pk}), objs.superusers.admin)
|
||||||
assert resp.data.get('count') == 0
|
assert resp.data.get('count') == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_launch_with_extra_credentials_from_jt(get, post, organization_factory,
|
|
||||||
job_template_factory, machine_credential,
|
|
||||||
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.ask_credential_on_launch = True
|
|
||||||
jt.extra_credentials.add(credential)
|
|
||||||
jt.extra_credentials.add(net_credential)
|
|
||||||
jt.save()
|
|
||||||
|
|
||||||
resp = post(
|
|
||||||
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
|
|
||||||
dict(
|
|
||||||
credential=machine_credential.pk
|
|
||||||
),
|
|
||||||
objs.superusers.admin, expect=201
|
|
||||||
)
|
|
||||||
job_pk = resp.data.get('id')
|
|
||||||
|
|
||||||
resp = get(reverse('api:job_extra_credentials_list', kwargs={'pk': job_pk}), objs.superusers.admin)
|
|
||||||
assert resp.data.get('count') == 2
|
|
||||||
|
|
||||||
resp = get(reverse('api:job_template_extra_credentials_list', kwargs={'pk': jt.pk}), objs.superusers.admin)
|
|
||||||
assert resp.data.get('count') == 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_launch_with_empty_extra_credentials(get, post, organization_factory,
|
|
||||||
job_template_factory, machine_credential,
|
|
||||||
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.ask_credential_on_launch = True
|
|
||||||
jt.extra_credentials.add(credential)
|
|
||||||
jt.extra_credentials.add(net_credential)
|
|
||||||
jt.save()
|
|
||||||
|
|
||||||
resp = post(
|
|
||||||
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
|
|
||||||
dict(
|
|
||||||
credential=machine_credential.pk,
|
|
||||||
extra_credentials=[],
|
|
||||||
),
|
|
||||||
objs.superusers.admin, expect=201
|
|
||||||
)
|
|
||||||
job_pk = resp.data.get('id')
|
|
||||||
|
|
||||||
resp = get(reverse('api:job_extra_credentials_list', kwargs={'pk': job_pk}), objs.superusers.admin)
|
|
||||||
assert resp.data.get('count') == 0
|
|
||||||
|
|
||||||
resp = get(reverse('api:job_template_extra_credentials_list', kwargs={'pk': jt.pk}), objs.superusers.admin)
|
|
||||||
assert resp.data.get('count') == 2
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_v1_launch_with_extra_credentials(get, post, organization_factory,
|
def test_v1_launch_with_extra_credentials(get, post, organization_factory,
|
||||||
job_template_factory, machine_credential,
|
job_template_factory, machine_credential,
|
||||||
|
|||||||
@@ -89,8 +89,7 @@ class TestJobTemplateCopyEdit:
|
|||||||
job_type='run',
|
job_type='run',
|
||||||
project=project,
|
project=project,
|
||||||
inventory=None, ask_inventory_on_launch=False, # not allowed
|
inventory=None, ask_inventory_on_launch=False, # not allowed
|
||||||
credential=None, ask_credential_on_launch=True,
|
ask_credential_on_launch=True, name='deploy-job-template'
|
||||||
name='deploy-job-template'
|
|
||||||
)
|
)
|
||||||
serializer = JobTemplateSerializer(jt_res, context=self.fake_context(admin_user))
|
serializer = JobTemplateSerializer(jt_res, context=self.fake_context(admin_user))
|
||||||
response = serializer.to_representation(jt_res)
|
response = serializer.to_representation(jt_res)
|
||||||
@@ -111,22 +110,6 @@ class TestJobTemplateCopyEdit:
|
|||||||
assert response['summary_fields']['user_capabilities']['copy']
|
assert response['summary_fields']['user_capabilities']['copy']
|
||||||
assert response['summary_fields']['user_capabilities']['edit']
|
assert response['summary_fields']['user_capabilities']['edit']
|
||||||
|
|
||||||
def test_org_admin_foreign_cred_no_copy_edit(self, jt_copy_edit, org_admin, machine_credential):
|
|
||||||
"""
|
|
||||||
Organization admins without access to the 3 related resources:
|
|
||||||
SHOULD NOT be able to copy JT
|
|
||||||
SHOULD be able to edit that job template, for nonsensitive changes
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Attach credential to JT that org admin cannot use
|
|
||||||
jt_copy_edit.credential = machine_credential
|
|
||||||
jt_copy_edit.save()
|
|
||||||
|
|
||||||
serializer = JobTemplateSerializer(jt_copy_edit, context=self.fake_context(org_admin))
|
|
||||||
response = serializer.to_representation(jt_copy_edit)
|
|
||||||
assert not response['summary_fields']['user_capabilities']['copy']
|
|
||||||
assert response['summary_fields']['user_capabilities']['edit']
|
|
||||||
|
|
||||||
def test_jt_admin_copy_edit(self, jt_copy_edit, rando):
|
def test_jt_admin_copy_edit(self, jt_copy_edit, rando):
|
||||||
"""
|
"""
|
||||||
JT admins wihout access to associated resources SHOULD NOT be able to copy
|
JT admins wihout access to associated resources SHOULD NOT be able to copy
|
||||||
@@ -302,27 +285,22 @@ def test_prefetch_group_capabilities(group, rando):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_prefetch_jt_copy_capability(job_template, project, inventory,
|
def test_prefetch_jt_copy_capability(job_template, project, inventory, rando):
|
||||||
machine_credential, vault_credential, rando):
|
|
||||||
job_template.project = project
|
job_template.project = project
|
||||||
job_template.inventory = inventory
|
job_template.inventory = inventory
|
||||||
job_template.credential = machine_credential
|
|
||||||
job_template.vault_credential = vault_credential
|
|
||||||
job_template.save()
|
job_template.save()
|
||||||
|
|
||||||
qs = JobTemplate.objects.all()
|
qs = JobTemplate.objects.all()
|
||||||
cache_list_capabilities(qs, [{'copy': [
|
cache_list_capabilities(qs, [{'copy': [
|
||||||
'project.use', 'inventory.use', 'credential.use', 'vault_credential.use'
|
'project.use', 'inventory.use',
|
||||||
]}], JobTemplate, rando)
|
]}], JobTemplate, rando)
|
||||||
assert qs[0].capabilities_cache == {'copy': False}
|
assert qs[0].capabilities_cache == {'copy': False}
|
||||||
|
|
||||||
project.use_role.members.add(rando)
|
project.use_role.members.add(rando)
|
||||||
inventory.use_role.members.add(rando)
|
inventory.use_role.members.add(rando)
|
||||||
machine_credential.use_role.members.add(rando)
|
|
||||||
vault_credential.use_role.members.add(rando)
|
|
||||||
|
|
||||||
cache_list_capabilities(qs, [{'copy': [
|
cache_list_capabilities(qs, [{'copy': [
|
||||||
'project.use', 'inventory.use', 'credential.use', 'vault_credential.use'
|
'project.use', 'inventory.use',
|
||||||
]}], JobTemplate, rando)
|
]}], JobTemplate, rando)
|
||||||
assert qs[0].capabilities_cache == {'copy': True}
|
assert qs[0].capabilities_cache == {'copy': True}
|
||||||
|
|
||||||
|
|||||||
@@ -81,26 +81,26 @@ def user():
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def check_jobtemplate(project, inventory, credential):
|
def check_jobtemplate(project, inventory, credential):
|
||||||
return \
|
jt = JobTemplate.objects.create(
|
||||||
JobTemplate.objects.create(
|
job_type='check',
|
||||||
job_type='check',
|
project=project,
|
||||||
project=project,
|
inventory=inventory,
|
||||||
inventory=inventory,
|
name='check-job-template'
|
||||||
credential=credential,
|
)
|
||||||
name='check-job-template'
|
jt.credentials.add(credential)
|
||||||
)
|
return jt
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def deploy_jobtemplate(project, inventory, credential):
|
def deploy_jobtemplate(project, inventory, credential):
|
||||||
return \
|
jt = JobTemplate.objects.create(
|
||||||
JobTemplate.objects.create(
|
job_type='run',
|
||||||
job_type='run',
|
project=project,
|
||||||
project=project,
|
inventory=inventory,
|
||||||
inventory=inventory,
|
name='deploy-job-template'
|
||||||
credential=credential,
|
)
|
||||||
name='deploy-job-template'
|
jt.credentials.add(credential)
|
||||||
)
|
return jt
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -54,12 +54,11 @@ class TestImplicitRolesOmitted:
|
|||||||
assert qs[1].operation == 'delete'
|
assert qs[1].operation == 'delete'
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_activity_stream_create_JT(self, project, inventory, credential):
|
def test_activity_stream_create_JT(self, project, inventory):
|
||||||
JobTemplate.objects.create(
|
JobTemplate.objects.create(
|
||||||
name='test-jt',
|
name='test-jt',
|
||||||
project=project,
|
project=project,
|
||||||
inventory=inventory,
|
inventory=inventory,
|
||||||
credential=credential
|
|
||||||
)
|
)
|
||||||
qs = ActivityStream.objects.filter(job_template__isnull=False)
|
qs = ActivityStream.objects.filter(job_template__isnull=False)
|
||||||
assert qs.count() == 1
|
assert qs.count() == 1
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from awx.main.models import Credential
|
from awx.main.models import Credential
|
||||||
|
|
||||||
|
|
||||||
@@ -12,23 +11,10 @@ def test_clean_credential_with_ssh_type(credentialtype_ssh, job_template):
|
|||||||
)
|
)
|
||||||
credential.save()
|
credential.save()
|
||||||
|
|
||||||
job_template.credential = credential
|
job_template.credentials.add(credential)
|
||||||
job_template.full_clean()
|
job_template.full_clean()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_clean_credential_with_invalid_type_xfail(credentialtype_aws, job_template):
|
|
||||||
credential = Credential(
|
|
||||||
name='My Credential',
|
|
||||||
credential_type=credentialtype_aws
|
|
||||||
)
|
|
||||||
credential.save()
|
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
|
||||||
job_template.credential = credential
|
|
||||||
job_template.full_clean()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_clean_credential_with_custom_types(credentialtype_aws, credentialtype_net, job_template):
|
def test_clean_credential_with_custom_types(credentialtype_aws, credentialtype_net, job_template):
|
||||||
aws = Credential(
|
aws = Credential(
|
||||||
@@ -42,6 +28,6 @@ def test_clean_credential_with_custom_types(credentialtype_aws, credentialtype_n
|
|||||||
)
|
)
|
||||||
net.save()
|
net.save()
|
||||||
|
|
||||||
job_template.extra_credentials.add(aws)
|
job_template.credentials.add(aws)
|
||||||
job_template.extra_credentials.add(net)
|
job_template.credentials.add(net)
|
||||||
job_template.full_clean()
|
job_template.full_clean()
|
||||||
|
|||||||
@@ -50,14 +50,15 @@ class TestCreateUnifiedJob:
|
|||||||
mocked.all.assert_called_with()
|
mocked.all.assert_called_with()
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Ensure that extra_credentials m2m field is copied to new relaunched job
|
Ensure that credentials m2m field is copied to new relaunched job
|
||||||
'''
|
'''
|
||||||
def test_job_relaunch_copy_vars(self, machine_credential, inventory,
|
def test_job_relaunch_copy_vars(self, machine_credential, inventory,
|
||||||
deploy_jobtemplate, post, mocker, net_credential):
|
deploy_jobtemplate, post, mocker, net_credential):
|
||||||
job_with_links = Job.objects.create(name='existing-job', credential=machine_credential, inventory=inventory)
|
job_with_links = Job.objects.create(name='existing-job', inventory=inventory)
|
||||||
job_with_links.job_template = deploy_jobtemplate
|
job_with_links.job_template = deploy_jobtemplate
|
||||||
job_with_links.limit = "my_server"
|
job_with_links.limit = "my_server"
|
||||||
job_with_links.extra_credentials.add(net_credential)
|
job_with_links.credentials.add(machine_credential)
|
||||||
|
job_with_links.credentials.add(net_credential)
|
||||||
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate._get_unified_job_field_names',
|
with mocker.patch('awx.main.models.unified_jobs.UnifiedJobTemplate._get_unified_job_field_names',
|
||||||
return_value=['inventory', 'credential', 'limit']):
|
return_value=['inventory', 'credential', 'limit']):
|
||||||
second_job = job_with_links.copy_unified_job()
|
second_job = job_with_links.copy_unified_job()
|
||||||
@@ -66,7 +67,7 @@ class TestCreateUnifiedJob:
|
|||||||
assert second_job.credential == job_with_links.credential
|
assert second_job.credential == job_with_links.credential
|
||||||
assert second_job.inventory == job_with_links.inventory
|
assert second_job.inventory == job_with_links.inventory
|
||||||
assert second_job.limit == 'my_server'
|
assert second_job.limit == 'my_server'
|
||||||
assert net_credential in second_job.extra_credentials.all()
|
assert net_credential in second_job.credentials.all()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -1,331 +0,0 @@
|
|||||||
import mock
|
|
||||||
import pytest
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from copy import deepcopy
|
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
|
|
||||||
from awx.main.models import Credential, CredentialType
|
|
||||||
from awx.main.migrations._credentialtypes import migrate_to_v2_credentials
|
|
||||||
from awx.main.utils import decrypt_field
|
|
||||||
from awx.main.migrations._credentialtypes import _disassociate_non_insights_projects
|
|
||||||
|
|
||||||
EXAMPLE_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nxyz==\n-----END PRIVATE KEY-----'
|
|
||||||
|
|
||||||
# TODO: remove this set of tests when API v1 is removed
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def migrate(credential, kind, is_insights=False):
|
|
||||||
with mock.patch.object(Credential, 'kind', kind), \
|
|
||||||
mock.patch.object(Credential, 'objects', mock.Mock(
|
|
||||||
get=lambda **kw: deepcopy(credential),
|
|
||||||
all=lambda: [credential],
|
|
||||||
)), mock.patch('awx.main.migrations._credentialtypes._is_insights_scm', return_value=is_insights):
|
|
||||||
class Apps(apps.__class__):
|
|
||||||
def get_model(self, app, model):
|
|
||||||
if model == 'Credential':
|
|
||||||
return Credential
|
|
||||||
return apps.get_model(app, model)
|
|
||||||
yield
|
|
||||||
migrate_to_v2_credentials(Apps(), None)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_ssh_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'ssh'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
|
|
||||||
'ssh_key_unlock': 'keypass',
|
|
||||||
'become_method': 'sudo',
|
|
||||||
'become_username': 'superuser',
|
|
||||||
'become_password': 'superpassword',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Machine'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
|
|
||||||
assert cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'ssh_key_unlock') == 'keypass'
|
|
||||||
assert cred.inputs['become_method'] == 'sudo'
|
|
||||||
assert cred.inputs['become_username'] == 'superuser'
|
|
||||||
assert cred.inputs['become_password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'become_password') == 'superpassword'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_scm_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'scm'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
|
|
||||||
'ssh_key_unlock': 'keypass',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Source Control'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
|
|
||||||
assert cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'ssh_key_unlock') == 'keypass'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_vault_only_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'ssh'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'vault_password': 'vault',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Vault'
|
|
||||||
assert cred.inputs['vault_password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'vault_password') == 'vault'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_vault_with_ssh_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'ssh'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'vault_password': 'vault',
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
|
|
||||||
'ssh_key_unlock': 'keypass',
|
|
||||||
'become_method': 'sudo',
|
|
||||||
'become_username': 'superuser',
|
|
||||||
'become_password': 'superpassword',
|
|
||||||
})
|
|
||||||
assert Credential.objects.count() == 2
|
|
||||||
|
|
||||||
assert Credential.objects.filter(credential_type__name='Vault').get() == cred
|
|
||||||
assert cred.inputs.keys() == ['vault_password']
|
|
||||||
assert cred.inputs['vault_password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'vault_password') == 'vault'
|
|
||||||
|
|
||||||
ssh_cred = Credential.objects.filter(credential_type__name='Machine').get()
|
|
||||||
assert sorted(ssh_cred.inputs.keys()) == sorted(CredentialType.from_v1_kind('ssh').defined_fields)
|
|
||||||
assert ssh_cred.credential_type.name == 'Machine'
|
|
||||||
assert ssh_cred.inputs['username'] == 'bob'
|
|
||||||
assert ssh_cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(ssh_cred, 'password') == 'secret'
|
|
||||||
assert ssh_cred.inputs['ssh_key_data'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(ssh_cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
|
|
||||||
assert ssh_cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(ssh_cred, 'ssh_key_unlock') == 'keypass'
|
|
||||||
assert ssh_cred.inputs['become_method'] == 'sudo'
|
|
||||||
assert ssh_cred.inputs['become_username'] == 'superuser'
|
|
||||||
assert ssh_cred.inputs['become_password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(ssh_cred, 'become_password') == 'superpassword'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_net_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'net'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'ssh_key_data': EXAMPLE_PRIVATE_KEY,
|
|
||||||
'ssh_key_unlock': 'keypass',
|
|
||||||
'authorize_password': 'authorize-secret',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Network'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
|
|
||||||
assert cred.inputs['ssh_key_unlock'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'ssh_key_unlock') == 'keypass'
|
|
||||||
assert cred.inputs['authorize_password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'authorize_password') == 'authorize-secret'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_aws_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'aws'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'security_token': 'secret-token'
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Amazon Web Services'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.inputs['security_token'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'security_token') == 'secret-token'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_openstack_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'openstack'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'host': 'https://keystone.example.org/',
|
|
||||||
'project': 'TENANT_ID',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'OpenStack'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.inputs['host'] == 'https://keystone.example.org/'
|
|
||||||
assert cred.inputs['project'] == 'TENANT_ID'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_vmware_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'vmware'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'host': 'https://example.org/',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'VMware vCenter'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.inputs['host'] == 'https://example.org/'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_satellite6_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'satellite6'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'host': 'https://example.org/',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Red Hat Satellite 6'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.inputs['host'] == 'https://example.org/'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_cloudforms_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'cloudforms'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'secret',
|
|
||||||
'host': 'https://example.org/',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Red Hat CloudForms'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'secret'
|
|
||||||
assert cred.inputs['host'] == 'https://example.org/'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_gce_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'gce'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'project': 'PROJECT-123',
|
|
||||||
'ssh_key_data': EXAMPLE_PRIVATE_KEY
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Google Compute Engine'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['project'] == 'PROJECT-123'
|
|
||||||
assert cred.inputs['ssh_key_data'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'ssh_key_data') == EXAMPLE_PRIVATE_KEY
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_azure_rm_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
with migrate(cred, 'azure_rm'):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'subscription': 'some-subscription',
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'some-password',
|
|
||||||
'client': 'some-client',
|
|
||||||
'secret': 'some-secret',
|
|
||||||
'tenant': 'some-tenant',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Microsoft Azure Resource Manager'
|
|
||||||
assert cred.inputs['subscription'] == 'some-subscription'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'password') == 'some-password'
|
|
||||||
assert cred.inputs['client'] == 'some-client'
|
|
||||||
assert cred.inputs['secret'].startswith('$encrypted$')
|
|
||||||
assert decrypt_field(cred, 'secret') == 'some-secret'
|
|
||||||
assert cred.inputs['tenant'] == 'some-tenant'
|
|
||||||
assert Credential.objects.count() == 1
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_insights_migration():
|
|
||||||
cred = Credential(name='My Credential')
|
|
||||||
|
|
||||||
with migrate(cred, 'scm', is_insights=True):
|
|
||||||
cred.__dict__.update({
|
|
||||||
'username': 'bob',
|
|
||||||
'password': 'some-password',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert cred.credential_type.name == 'Insights'
|
|
||||||
assert cred.inputs['username'] == 'bob'
|
|
||||||
assert cred.inputs['password'].startswith('$encrypted$')
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Need some more mocking here or something.")
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_insights_project_migration():
|
|
||||||
cred1 = apps.get_model('main', 'Credential').objects.create(name='My Credential')
|
|
||||||
cred2 = apps.get_model('main', 'Credential').objects.create(name='My Credential')
|
|
||||||
projA1 = apps.get_model('main', 'Project').objects.create(name='Insights Project A1', scm_type='insights', credential=cred1)
|
|
||||||
|
|
||||||
projB1 = apps.get_model('main', 'Project').objects.create(name='Git Project B1', scm_type='git', credential=cred1)
|
|
||||||
projB2 = apps.get_model('main', 'Project').objects.create(name='Git Project B2', scm_type='git', credential=cred1)
|
|
||||||
|
|
||||||
projC1 = apps.get_model('main', 'Project').objects.create(name='Git Project C1', scm_type='git', credential=cred2)
|
|
||||||
|
|
||||||
_disassociate_non_insights_projects(apps, cred1)
|
|
||||||
_disassociate_non_insights_projects(apps, cred2)
|
|
||||||
|
|
||||||
assert apps.get_model('main', 'Project').objects.get(pk=projA1).credential is None
|
|
||||||
assert apps.get_model('main', 'Project').objects.get(pk=projB1).credential is None
|
|
||||||
assert apps.get_model('main', 'Project').objects.get(pk=projB2).credential is None
|
|
||||||
assert apps.get_model('main', 'Project').objects.get(pk=projC1).credential == cred2
|
|
||||||
|
|
||||||
@@ -138,28 +138,29 @@ def test_project_org_admin_delete_allowed(normal_job, org_admin):
|
|||||||
class TestJobRelaunchAccess:
|
class TestJobRelaunchAccess:
|
||||||
|
|
||||||
def test_job_relaunch_normal_resource_access(self, user, inventory, machine_credential):
|
def test_job_relaunch_normal_resource_access(self, user, inventory, machine_credential):
|
||||||
job_with_links = Job.objects.create(name='existing-job', credential=machine_credential, inventory=inventory)
|
job_with_links = Job.objects.create(name='existing-job', inventory=inventory)
|
||||||
|
job_with_links.credentials.add(machine_credential)
|
||||||
inventory_user = user('user1', False)
|
inventory_user = user('user1', False)
|
||||||
credential_user = user('user2', False)
|
credential_user = user('user2', False)
|
||||||
both_user = user('user3', False)
|
both_user = user('user3', False)
|
||||||
|
|
||||||
# Confirm that a user with inventory & credential access can launch
|
# Confirm that a user with inventory & credential access can launch
|
||||||
job_with_links.credential.use_role.members.add(both_user)
|
machine_credential.use_role.members.add(both_user)
|
||||||
job_with_links.inventory.use_role.members.add(both_user)
|
job_with_links.inventory.use_role.members.add(both_user)
|
||||||
assert both_user.can_access(Job, 'start', job_with_links, validate_license=False)
|
assert both_user.can_access(Job, 'start', job_with_links, validate_license=False)
|
||||||
|
|
||||||
# Confirm that a user with credential access alone cannot launch
|
# Confirm that a user with credential access alone cannot launch
|
||||||
job_with_links.credential.use_role.members.add(credential_user)
|
machine_credential.use_role.members.add(credential_user)
|
||||||
assert not credential_user.can_access(Job, 'start', job_with_links, validate_license=False)
|
assert not credential_user.can_access(Job, 'start', job_with_links, validate_license=False)
|
||||||
|
|
||||||
# Confirm that a user with inventory access alone cannot launch
|
# Confirm that a user with inventory access alone cannot launch
|
||||||
job_with_links.inventory.use_role.members.add(inventory_user)
|
job_with_links.inventory.use_role.members.add(inventory_user)
|
||||||
assert not inventory_user.can_access(Job, 'start', job_with_links, validate_license=False)
|
assert not inventory_user.can_access(Job, 'start', job_with_links, validate_license=False)
|
||||||
|
|
||||||
def test_job_relaunch_extra_credential_access(
|
def test_job_relaunch_credential_access(
|
||||||
self, inventory, project, credential, net_credential):
|
self, inventory, project, credential, net_credential):
|
||||||
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project)
|
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project)
|
||||||
jt.extra_credentials.add(credential)
|
jt.credentials.add(credential)
|
||||||
job = jt.create_unified_job()
|
job = jt.create_unified_job()
|
||||||
|
|
||||||
# Job is unchanged from JT, user has ability to launch
|
# Job is unchanged from JT, user has ability to launch
|
||||||
@@ -168,11 +169,11 @@ class TestJobRelaunchAccess:
|
|||||||
assert jt_user in job.job_template.execute_role
|
assert jt_user in job.job_template.execute_role
|
||||||
assert jt_user.can_access(Job, 'start', job, validate_license=False)
|
assert jt_user.can_access(Job, 'start', job, validate_license=False)
|
||||||
|
|
||||||
# Job has prompted extra_credential, launch denied w/ message
|
# Job has prompted net credential, launch denied w/ message
|
||||||
job.extra_credentials.add(net_credential)
|
job.credentials.add(net_credential)
|
||||||
assert not jt_user.can_access(Job, 'start', job, validate_license=False)
|
assert not jt_user.can_access(Job, 'start', job, validate_license=False)
|
||||||
|
|
||||||
def test_prompted_extra_credential_relaunch_denied(
|
def test_prompted_credential_relaunch_denied(
|
||||||
self, inventory, project, net_credential, rando):
|
self, inventory, project, net_credential, rando):
|
||||||
jt = JobTemplate.objects.create(
|
jt = JobTemplate.objects.create(
|
||||||
name='testjt', inventory=inventory, project=project,
|
name='testjt', inventory=inventory, project=project,
|
||||||
@@ -180,11 +181,11 @@ class TestJobRelaunchAccess:
|
|||||||
job = jt.create_unified_job()
|
job = jt.create_unified_job()
|
||||||
jt.execute_role.members.add(rando)
|
jt.execute_role.members.add(rando)
|
||||||
|
|
||||||
# Job has prompted extra_credential, rando lacks permission to use it
|
# Job has prompted net credential, rando lacks permission to use it
|
||||||
job.extra_credentials.add(net_credential)
|
job.credentials.add(net_credential)
|
||||||
assert not rando.can_access(Job, 'start', job, validate_license=False)
|
assert not rando.can_access(Job, 'start', job, validate_license=False)
|
||||||
|
|
||||||
def test_prompted_extra_credential_relaunch_allowed(
|
def test_prompted_credential_relaunch_allowed(
|
||||||
self, inventory, project, net_credential, rando):
|
self, inventory, project, net_credential, rando):
|
||||||
jt = JobTemplate.objects.create(
|
jt = JobTemplate.objects.create(
|
||||||
name='testjt', inventory=inventory, project=project,
|
name='testjt', inventory=inventory, project=project,
|
||||||
@@ -192,23 +193,24 @@ class TestJobRelaunchAccess:
|
|||||||
job = jt.create_unified_job()
|
job = jt.create_unified_job()
|
||||||
jt.execute_role.members.add(rando)
|
jt.execute_role.members.add(rando)
|
||||||
|
|
||||||
# Job has prompted extra_credential, but rando can use it
|
# Job has prompted net credential, but rando can use it
|
||||||
net_credential.use_role.members.add(rando)
|
net_credential.use_role.members.add(rando)
|
||||||
job.extra_credentials.add(net_credential)
|
job.credentials.add(net_credential)
|
||||||
assert rando.can_access(Job, 'start', job, validate_license=False)
|
assert rando.can_access(Job, 'start', job, validate_license=False)
|
||||||
|
|
||||||
def test_extra_credential_relaunch_recreation_permission(
|
def test_credential_relaunch_recreation_permission(
|
||||||
self, inventory, project, net_credential, credential, rando):
|
self, inventory, project, net_credential, credential, rando):
|
||||||
jt = JobTemplate.objects.create(
|
jt = JobTemplate.objects.create(
|
||||||
name='testjt', inventory=inventory, project=project,
|
name='testjt', inventory=inventory, project=project,
|
||||||
credential=credential, ask_credential_on_launch=True)
|
ask_credential_on_launch=True)
|
||||||
job = jt.create_unified_job()
|
job = jt.create_unified_job()
|
||||||
project.admin_role.members.add(rando)
|
project.admin_role.members.add(rando)
|
||||||
inventory.admin_role.members.add(rando)
|
inventory.admin_role.members.add(rando)
|
||||||
credential.admin_role.members.add(rando)
|
credential.admin_role.members.add(rando)
|
||||||
|
|
||||||
# Relaunch blocked by the extra credential
|
# Relaunch blocked by the net credential
|
||||||
job.extra_credentials.add(net_credential)
|
job.credentials.add(credential)
|
||||||
|
job.credentials.add(net_credential)
|
||||||
assert not rando.can_access(Job, 'start', job, validate_license=False)
|
assert not rando.can_access(Job, 'start', job, validate_license=False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,16 +47,18 @@ def test_inventory_use_access(inventory, user):
|
|||||||
class TestJobRelaunchAccess:
|
class TestJobRelaunchAccess:
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def job_no_prompts(self, machine_credential, inventory):
|
def job_no_prompts(self, machine_credential, inventory):
|
||||||
jt = JobTemplate.objects.create(name='test-job_template', credential=machine_credential, inventory=inventory)
|
jt = JobTemplate.objects.create(name='test-job_template', inventory=inventory)
|
||||||
|
jt.credentials.add(machine_credential)
|
||||||
return jt.create_unified_job()
|
return jt.create_unified_job()
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def job_with_prompts(self, machine_credential, inventory, organization, credentialtype_ssh):
|
def job_with_prompts(self, machine_credential, inventory, organization, credentialtype_ssh):
|
||||||
jt = JobTemplate.objects.create(
|
jt = JobTemplate.objects.create(
|
||||||
name='test-job-template-prompts', credential=machine_credential, inventory=inventory,
|
name='test-job-template-prompts', inventory=inventory,
|
||||||
ask_tags_on_launch=True, ask_variables_on_launch=True, ask_skip_tags_on_launch=True,
|
ask_tags_on_launch=True, ask_variables_on_launch=True, ask_skip_tags_on_launch=True,
|
||||||
ask_limit_on_launch=True, ask_job_type_on_launch=True, ask_verbosity_on_launch=True,
|
ask_limit_on_launch=True, ask_job_type_on_launch=True, ask_verbosity_on_launch=True,
|
||||||
ask_inventory_on_launch=True, ask_credential_on_launch=True)
|
ask_inventory_on_launch=True, ask_credential_on_launch=True)
|
||||||
|
jt.credentials.add(machine_credential)
|
||||||
new_cred = Credential.objects.create(
|
new_cred = Credential.objects.create(
|
||||||
name='new-cred',
|
name='new-cred',
|
||||||
credential_type=credentialtype_ssh,
|
credential_type=credentialtype_ssh,
|
||||||
@@ -65,8 +67,9 @@ class TestJobRelaunchAccess:
|
|||||||
'password': 'pas4word'
|
'password': 'pas4word'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
new_cred.save()
|
||||||
new_inv = Inventory.objects.create(name='new-inv', organization=organization)
|
new_inv = Inventory.objects.create(name='new-inv', organization=organization)
|
||||||
return jt.create_unified_job(credential=new_cred, inventory=new_inv)
|
return jt.create_unified_job(credentials=[new_cred.pk], inventory=new_inv)
|
||||||
|
|
||||||
def test_normal_relaunch_via_job_template(self, job_no_prompts, rando):
|
def test_normal_relaunch_via_job_template(self, job_no_prompts, rando):
|
||||||
"Has JT execute_role, job unchanged relative to JT"
|
"Has JT execute_role, job unchanged relative to JT"
|
||||||
@@ -81,7 +84,8 @@ class TestJobRelaunchAccess:
|
|||||||
def test_can_relaunch_with_prompted_fields_access(self, job_with_prompts, rando):
|
def test_can_relaunch_with_prompted_fields_access(self, job_with_prompts, rando):
|
||||||
"Has use_role on the prompted inventory & credential - allow relaunch"
|
"Has use_role on the prompted inventory & credential - allow relaunch"
|
||||||
job_with_prompts.job_template.execute_role.members.add(rando)
|
job_with_prompts.job_template.execute_role.members.add(rando)
|
||||||
job_with_prompts.credential.use_role.members.add(rando)
|
for cred in job_with_prompts.credentials.all():
|
||||||
|
cred.use_role.members.add(rando)
|
||||||
job_with_prompts.inventory.use_role.members.add(rando)
|
job_with_prompts.inventory.use_role.members.add(rando)
|
||||||
assert rando.can_access(Job, 'start', job_with_prompts)
|
assert rando.can_access(Job, 'start', job_with_prompts)
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ def jt_linked(job_template_factory, credential, net_credential, vault_credential
|
|||||||
'testJT', organization='org1', project='proj1', inventory='inventory1',
|
'testJT', organization='org1', project='proj1', inventory='inventory1',
|
||||||
credential='cred1')
|
credential='cred1')
|
||||||
jt = objects.job_template
|
jt = objects.job_template
|
||||||
jt.vault_credential = vault_credential
|
jt.credentials.add(vault_credential)
|
||||||
jt.save()
|
jt.save()
|
||||||
# Add AWS cloud credential and network credential
|
# Add AWS cloud credential and network credential
|
||||||
jt.extra_credentials.add(credential)
|
jt.credentials.add(credential)
|
||||||
jt.extra_credentials.add(net_credential)
|
jt.credentials.add(net_credential)
|
||||||
return jt
|
return jt
|
||||||
|
|
||||||
|
|
||||||
@@ -47,15 +47,15 @@ def test_job_template_access_read_level(jt_linked, rando):
|
|||||||
access = JobTemplateAccess(rando)
|
access = JobTemplateAccess(rando)
|
||||||
jt_linked.project.read_role.members.add(rando)
|
jt_linked.project.read_role.members.add(rando)
|
||||||
jt_linked.inventory.read_role.members.add(rando)
|
jt_linked.inventory.read_role.members.add(rando)
|
||||||
jt_linked.credential.read_role.members.add(rando)
|
jt_linked.get_deprecated_credential('ssh').read_role.members.add(rando)
|
||||||
|
|
||||||
proj_pk = jt_linked.project.pk
|
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(inventory=jt_linked.inventory.pk, project=proj_pk))
|
||||||
assert not access.can_add(dict(credential=jt_linked.credential.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.pk, project=proj_pk))
|
assert not access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk))
|
||||||
|
|
||||||
for cred in jt_linked.extra_credentials.all():
|
for cred in jt_linked.credentials.all():
|
||||||
assert not access.can_unattach(jt_linked, cred, 'extra_credentials', {})
|
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -64,16 +64,16 @@ def test_job_template_access_use_level(jt_linked, rando):
|
|||||||
access = JobTemplateAccess(rando)
|
access = JobTemplateAccess(rando)
|
||||||
jt_linked.project.use_role.members.add(rando)
|
jt_linked.project.use_role.members.add(rando)
|
||||||
jt_linked.inventory.use_role.members.add(rando)
|
jt_linked.inventory.use_role.members.add(rando)
|
||||||
jt_linked.credential.use_role.members.add(rando)
|
jt_linked.get_deprecated_credential('ssh').use_role.members.add(rando)
|
||||||
jt_linked.vault_credential.use_role.members.add(rando)
|
jt_linked.get_deprecated_credential('vault').use_role.members.add(rando)
|
||||||
|
|
||||||
proj_pk = jt_linked.project.pk
|
proj_pk = jt_linked.project.pk
|
||||||
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
||||||
assert access.can_add(dict(credential=jt_linked.credential.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.pk, project=proj_pk))
|
assert access.can_add(dict(vault_credential=jt_linked.vault_credential, project=proj_pk))
|
||||||
|
|
||||||
for cred in jt_linked.extra_credentials.all():
|
for cred in jt_linked.credentials.all():
|
||||||
assert not access.can_unattach(jt_linked, cred, 'extra_credentials', {})
|
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -83,14 +83,14 @@ def test_job_template_access_org_admin(jt_linked, rando):
|
|||||||
jt_linked.inventory.organization.admin_role.members.add(rando)
|
jt_linked.inventory.organization.admin_role.members.add(rando)
|
||||||
# Assign organization permission in the same way the create view does
|
# Assign organization permission in the same way the create view does
|
||||||
organization = jt_linked.inventory.organization
|
organization = jt_linked.inventory.organization
|
||||||
jt_linked.credential.admin_role.parents.add(organization.admin_role)
|
jt_linked.get_deprecated_credential('ssh').admin_role.parents.add(organization.admin_role)
|
||||||
|
|
||||||
proj_pk = jt_linked.project.pk
|
proj_pk = jt_linked.project.pk
|
||||||
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
||||||
assert access.can_add(dict(credential=jt_linked.credential.pk, project=proj_pk))
|
assert access.can_add(dict(credential=jt_linked.credential, project=proj_pk))
|
||||||
|
|
||||||
for cred in jt_linked.extra_credentials.all():
|
for cred in jt_linked.credentials.all():
|
||||||
assert access.can_unattach(jt_linked, cred, 'extra_credentials', {})
|
assert access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||||
|
|
||||||
assert access.can_read(jt_linked)
|
assert access.can_read(jt_linked)
|
||||||
assert access.can_delete(jt_linked)
|
assert access.can_delete(jt_linked)
|
||||||
@@ -104,9 +104,9 @@ def test_job_template_extra_credentials_prompts_access(
|
|||||||
project = project,
|
project = project,
|
||||||
playbook = 'helloworld.yml',
|
playbook = 'helloworld.yml',
|
||||||
inventory = inventory,
|
inventory = inventory,
|
||||||
credential = machine_credential,
|
|
||||||
ask_credential_on_launch = True
|
ask_credential_on_launch = True
|
||||||
)
|
)
|
||||||
|
jt.credentials.add(machine_credential)
|
||||||
jt.execute_role.members.add(rando)
|
jt.execute_role.members.add(rando)
|
||||||
r = post(
|
r = post(
|
||||||
reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}),
|
reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}),
|
||||||
@@ -123,24 +123,24 @@ class TestJobTemplateCredentials:
|
|||||||
credential.read_role.members.add(rando)
|
credential.read_role.members.add(rando)
|
||||||
# without permission to credential, user can not attach it
|
# without permission to credential, user can not attach it
|
||||||
assert not JobTemplateAccess(rando).can_attach(
|
assert not JobTemplateAccess(rando).can_attach(
|
||||||
job_template, credential, 'extra_credentials', {})
|
job_template, credential, 'credentials', {})
|
||||||
|
|
||||||
def test_job_template_can_add_extra_credentials(self, job_template, credential, rando):
|
def test_job_template_can_add_extra_credentials(self, job_template, credential, rando):
|
||||||
job_template.admin_role.members.add(rando)
|
job_template.admin_role.members.add(rando)
|
||||||
credential.use_role.members.add(rando)
|
credential.use_role.members.add(rando)
|
||||||
# user has permission to apply credential
|
# user has permission to apply credential
|
||||||
assert JobTemplateAccess(rando).can_attach(
|
assert JobTemplateAccess(rando).can_attach(
|
||||||
job_template, credential, 'extra_credentials', {})
|
job_template, credential, 'credentials', {})
|
||||||
|
|
||||||
def test_job_template_vault_cred_check(self, job_template, vault_credential, rando):
|
def test_job_template_vault_cred_check(self, job_template, vault_credential, rando):
|
||||||
job_template.admin_role.members.add(rando)
|
job_template.admin_role.members.add(rando)
|
||||||
# not allowed to use the vault cred
|
# not allowed to use the vault cred
|
||||||
assert not JobTemplateAccess(rando).can_change(
|
assert not JobTemplateAccess(rando).can_change(
|
||||||
job_template, {'vault_credential': vault_credential})
|
job_template, {'credentials': [vault_credential.pk]})
|
||||||
|
|
||||||
def test_new_jt_with_vault(self, vault_credential, project, rando):
|
def test_new_jt_with_vault(self, vault_credential, project, rando):
|
||||||
project.admin_role.members.add(rando)
|
project.admin_role.members.add(rando)
|
||||||
assert not JobTemplateAccess(rando).can_add({'vault_credential': vault_credential, 'project': project.pk})
|
assert not JobTemplateAccess(rando).can_add({'credentials': [vault_credential.pk], 'project': project.pk})
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ class TestJobTemplateSerializerGetSummaryFields():
|
|||||||
with mocker.patch("awx.api.serializers.role_summary_fields_generator", return_value='Can eat pie'):
|
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_change", return_value='foobar'):
|
||||||
with mocker.patch("awx.main.access.JobTemplateAccess.can_add", return_value='foo'):
|
with mocker.patch("awx.main.access.JobTemplateAccess.can_add", return_value='foo'):
|
||||||
response = serializer.get_summary_fields(jt_obj)
|
with mock.patch.object(jt_obj.__class__, 'get_deprecated_credential', return_value=None):
|
||||||
|
response = serializer.get_summary_fields(jt_obj)
|
||||||
|
|
||||||
assert response['user_capabilities']['copy'] == 'foo'
|
assert response['user_capabilities']['copy'] == 'foo'
|
||||||
assert response['user_capabilities']['edit'] == 'foobar'
|
assert response['user_capabilities']['edit'] == 'foobar'
|
||||||
|
|||||||
@@ -272,4 +272,4 @@ class TestResourceAccessList:
|
|||||||
def test_related_search_reverse_FK_field():
|
def test_related_search_reverse_FK_field():
|
||||||
view = ListAPIView()
|
view = ListAPIView()
|
||||||
view.model = Credential
|
view.model = Credential
|
||||||
assert 'jobtemplates__search' in view.related_search_fields
|
assert 'unifiedjobtemplates__search' in view.related_search_fields
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
|
||||||
def test_missing_project_error(job_template_factory):
|
def test_missing_project_error(job_template_factory):
|
||||||
objects = job_template_factory(
|
objects = job_template_factory(
|
||||||
'missing-project-jt',
|
'missing-project-jt',
|
||||||
organization='org1',
|
organization='org1',
|
||||||
inventory='inventory1',
|
inventory='inventory1',
|
||||||
credential='cred1',
|
|
||||||
persisted=False)
|
persisted=False)
|
||||||
obj = objects.job_template
|
obj = objects.job_template
|
||||||
assert 'project' in obj.resources_needed_to_start
|
assert 'project' in obj.resources_needed_to_start
|
||||||
@@ -15,27 +16,24 @@ def test_missing_project_error(job_template_factory):
|
|||||||
assert 'project' in validation_errors
|
assert 'project' in validation_errors
|
||||||
|
|
||||||
|
|
||||||
def test_inventory_credential_need_to_start(job_template_factory):
|
def test_inventory_need_to_start(job_template_factory):
|
||||||
objects = job_template_factory(
|
objects = job_template_factory(
|
||||||
'job-template-few-resources',
|
'job-template-few-resources',
|
||||||
project='project1',
|
project='project1',
|
||||||
persisted=False)
|
persisted=False)
|
||||||
obj = objects.job_template
|
obj = objects.job_template
|
||||||
assert 'inventory' in obj.resources_needed_to_start
|
assert 'inventory' in obj.resources_needed_to_start
|
||||||
assert 'credential' in obj.resources_needed_to_start
|
|
||||||
|
|
||||||
|
|
||||||
def test_inventory_credential_contradictions(job_template_factory):
|
def test_inventory_contradictions(job_template_factory):
|
||||||
objects = job_template_factory(
|
objects = job_template_factory(
|
||||||
'job-template-paradox',
|
'job-template-paradox',
|
||||||
project='project1',
|
project='project1',
|
||||||
persisted=False)
|
persisted=False)
|
||||||
obj = objects.job_template
|
obj = objects.job_template
|
||||||
obj.ask_inventory_on_launch = False
|
obj.ask_inventory_on_launch = False
|
||||||
obj.ask_credential_on_launch = False
|
|
||||||
validation_errors, resources_needed_to_start = obj.resource_validation_data()
|
validation_errors, resources_needed_to_start = obj.resource_validation_data()
|
||||||
assert 'inventory' in validation_errors
|
assert 'inventory' in validation_errors
|
||||||
assert 'credential' in validation_errors
|
|
||||||
|
|
||||||
|
|
||||||
def test_survey_answers_as_string(job_template_factory):
|
def test_survey_answers_as_string(job_template_factory):
|
||||||
@@ -142,4 +140,5 @@ def test_job_template_can_start_with_callback_extra_vars_provided(job_template_f
|
|||||||
)
|
)
|
||||||
obj = objects.job_template
|
obj = objects.job_template
|
||||||
obj.ask_variables_on_launch = True
|
obj.ask_variables_on_launch = True
|
||||||
assert obj.can_start_without_user_input(callback_extra_vars='{"foo": "bar"}') is True
|
with mock.patch.object(obj.__class__, 'passwords_needed_to_start', []):
|
||||||
|
assert obj.can_start_without_user_input(callback_extra_vars='{"foo": "bar"}') is True
|
||||||
|
|||||||
@@ -194,18 +194,18 @@ class TestWorkflowJobNodeJobKWARGS:
|
|||||||
extra_vars={'a': 84}, **self.kwargs_base)
|
extra_vars={'a': 84}, **self.kwargs_base)
|
||||||
|
|
||||||
def test_char_prompts_and_res_node_prompts(self, job_node_with_prompts):
|
def test_char_prompts_and_res_node_prompts(self, job_node_with_prompts):
|
||||||
|
# TBD: properly handle multicred credential assignment
|
||||||
expect_kwargs = dict(
|
expect_kwargs = dict(
|
||||||
inventory=job_node_with_prompts.inventory.pk,
|
inventory=job_node_with_prompts.inventory.pk,
|
||||||
credential=job_node_with_prompts.credential.pk,
|
|
||||||
**example_prompts)
|
**example_prompts)
|
||||||
expect_kwargs.update(self.kwargs_base)
|
expect_kwargs.update(self.kwargs_base)
|
||||||
assert job_node_with_prompts.get_job_kwargs() == expect_kwargs
|
assert job_node_with_prompts.get_job_kwargs() == expect_kwargs
|
||||||
|
|
||||||
def test_reject_some_node_prompts(self, job_node_with_prompts):
|
def test_reject_some_node_prompts(self, job_node_with_prompts):
|
||||||
|
# TBD: properly handle multicred credential assignment
|
||||||
job_node_with_prompts.unified_job_template.ask_inventory_on_launch = False
|
job_node_with_prompts.unified_job_template.ask_inventory_on_launch = False
|
||||||
job_node_with_prompts.unified_job_template.ask_job_type_on_launch = False
|
job_node_with_prompts.unified_job_template.ask_job_type_on_launch = False
|
||||||
expect_kwargs = dict(inventory=job_node_with_prompts.inventory.pk,
|
expect_kwargs = dict(inventory=job_node_with_prompts.inventory.pk,
|
||||||
credential=job_node_with_prompts.credential.pk,
|
|
||||||
**example_prompts)
|
**example_prompts)
|
||||||
expect_kwargs.update(self.kwargs_base)
|
expect_kwargs.update(self.kwargs_base)
|
||||||
expect_kwargs.pop('inventory')
|
expect_kwargs.pop('inventory')
|
||||||
@@ -239,6 +239,5 @@ class TestWorkflowWarnings:
|
|||||||
job_node_with_prompts.unified_job_template.ask_job_type_on_launch = False
|
job_node_with_prompts.unified_job_template.ask_job_type_on_launch = False
|
||||||
assert 'ignored' in job_node_with_prompts.get_prompts_warnings()
|
assert 'ignored' in job_node_with_prompts.get_prompts_warnings()
|
||||||
assert 'job_type' in job_node_with_prompts.get_prompts_warnings()['ignored']
|
assert 'job_type' in job_node_with_prompts.get_prompts_warnings()['ignored']
|
||||||
assert 'credential' in job_node_with_prompts.get_prompts_warnings()['ignored']
|
assert len(job_node_with_prompts.get_prompts_warnings()['ignored']) == 1
|
||||||
assert len(job_node_with_prompts.get_prompts_warnings()['ignored']) == 2
|
|
||||||
|
|
||||||
|
|||||||
@@ -128,9 +128,6 @@ def job_template_with_ids(job_template_factory):
|
|||||||
cloud_type = CredentialType(kind='aws')
|
cloud_type = CredentialType(kind='aws')
|
||||||
cloud_cred = Credential(id=3, pk=3, name='testcloudcred', credential_type=cloud_type)
|
cloud_cred = Credential(id=3, pk=3, name='testcloudcred', credential_type=cloud_type)
|
||||||
|
|
||||||
vault_type = CredentialType(kind='vault')
|
|
||||||
vault_cred = Credential(id=4, pk=4, name='testnetcred', credential_type=vault_type)
|
|
||||||
|
|
||||||
inv = Inventory(id=11, pk=11, name='testinv')
|
inv = Inventory(id=11, pk=11, name='testinv')
|
||||||
proj = Project(id=14, pk=14, name='testproj')
|
proj = Project(id=14, pk=14, name='testproj')
|
||||||
|
|
||||||
@@ -138,7 +135,6 @@ def job_template_with_ids(job_template_factory):
|
|||||||
'testJT', project=proj, inventory=inv, credential=credential,
|
'testJT', project=proj, inventory=inv, credential=credential,
|
||||||
cloud_credential=cloud_cred, network_credential=net_cred,
|
cloud_credential=cloud_cred, network_credential=net_cred,
|
||||||
persisted=False)
|
persisted=False)
|
||||||
jt_objects.job_template.vault_credential = vault_cred
|
|
||||||
return jt_objects.job_template
|
return jt_objects.job_template
|
||||||
|
|
||||||
|
|
||||||
@@ -184,9 +180,7 @@ def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit):
|
|||||||
|
|
||||||
mock_add.assert_called_once_with({
|
mock_add.assert_called_once_with({
|
||||||
'inventory': data['inventory'],
|
'inventory': data['inventory'],
|
||||||
'project': job_template_with_ids.project.id,
|
'project': job_template_with_ids.project.id
|
||||||
'credential': job_template_with_ids.credential.id,
|
|
||||||
'vault_credential': job_template_with_ids.vault_credential.id
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -243,12 +243,13 @@ class TestJobExecution:
|
|||||||
verbosity=3
|
verbosity=3
|
||||||
)
|
)
|
||||||
|
|
||||||
# mock the job.extra_credentials M2M relation so we can avoid DB access
|
# mock the job.credentials M2M relation so we can avoid DB access
|
||||||
job._extra_credentials = []
|
job._credentials = []
|
||||||
patch = mock.patch.object(Job, 'extra_credentials', mock.Mock(
|
patch = mock.patch.object(UnifiedJob, 'credentials', mock.Mock(
|
||||||
all=lambda: job._extra_credentials,
|
all=lambda: job._credentials,
|
||||||
add=job._extra_credentials.append,
|
add=job._credentials.append,
|
||||||
spec_set=['all', 'add']
|
filter=mock.Mock(return_value=job._credentials),
|
||||||
|
spec_set=['all', 'add', 'filter']
|
||||||
))
|
))
|
||||||
self.patches.append(patch)
|
self.patches.append(patch)
|
||||||
patch.start()
|
patch.start()
|
||||||
@@ -328,7 +329,7 @@ class TestIsolatedExecution(TestJobExecution):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||||
self.instance.credential = credential
|
self.instance.credentials.add(credential)
|
||||||
|
|
||||||
private_data = tempfile.mkdtemp(prefix='awx_')
|
private_data = tempfile.mkdtemp(prefix='awx_')
|
||||||
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
|
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
|
||||||
@@ -370,7 +371,7 @@ class TestIsolatedExecution(TestJobExecution):
|
|||||||
credential_type=ssh,
|
credential_type=ssh,
|
||||||
inputs = {'username': 'bob',}
|
inputs = {'username': 'bob',}
|
||||||
)
|
)
|
||||||
self.instance.credential = credential
|
self.instance.credentials.add(credential)
|
||||||
|
|
||||||
private_data = tempfile.mkdtemp(prefix='awx_')
|
private_data = tempfile.mkdtemp(prefix='awx_')
|
||||||
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
|
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
|
||||||
@@ -414,7 +415,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
inputs = {'username': 'bob', field: 'secret'}
|
inputs = {'username': 'bob', field: 'secret'}
|
||||||
)
|
)
|
||||||
credential.inputs[field] = encrypt_field(credential, field)
|
credential.inputs[field] = encrypt_field(credential, field)
|
||||||
self.instance.credential = credential
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -434,7 +435,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
inputs={'vault_password': 'vault-me'}
|
inputs={'vault_password': 'vault-me'}
|
||||||
)
|
)
|
||||||
credential.inputs['vault_password'] = encrypt_field(credential, 'vault_password')
|
credential.inputs['vault_password'] = encrypt_field(credential, 'vault_password')
|
||||||
self.instance.vault_credential = credential
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -457,7 +458,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
|
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
|
||||||
self.instance.credential = credential
|
self.instance.credentials.add(credential)
|
||||||
|
|
||||||
def run_pexpect_side_effect(private_data, *args, **kwargs):
|
def run_pexpect_side_effect(private_data, *args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -485,7 +486,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
inputs = {'username': 'bob', 'password': 'secret'}
|
inputs = {'username': 'bob', 'password': 'secret'}
|
||||||
)
|
)
|
||||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -505,7 +506,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
)
|
)
|
||||||
for key in ('password', 'security_token'):
|
for key in ('password', 'security_token'):
|
||||||
credential.inputs[key] = encrypt_field(credential, key)
|
credential.inputs[key] = encrypt_field(credential, key)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -528,7 +529,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
|
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -554,7 +555,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
credential.inputs['secret'] = encrypt_field(credential, 'secret')
|
credential.inputs['secret'] = encrypt_field(credential, 'secret')
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
|
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
@@ -579,7 +580,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
|
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
@@ -599,7 +600,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
|
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
|
||||||
)
|
)
|
||||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -623,7 +624,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -658,7 +659,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
)
|
)
|
||||||
for field in ('password', 'ssh_key_data', 'authorize_password'):
|
for field in ('password', 'ssh_key_data', 'authorize_password'):
|
||||||
credential.inputs[field] = encrypt_field(credential, field)
|
credential.inputs[field] = encrypt_field(credential, field)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
@@ -695,7 +696,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
credential_type=some_cloud,
|
credential_type=some_cloud,
|
||||||
inputs = {'api_token': 'ABC123'}
|
inputs = {'api_token': 'ABC123'}
|
||||||
)
|
)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
@@ -722,7 +723,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
credential_type=some_cloud,
|
credential_type=some_cloud,
|
||||||
inputs = {'api_token': 'ABC123'}
|
inputs = {'api_token': 'ABC123'}
|
||||||
)
|
)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -754,7 +755,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
credential_type=some_cloud,
|
credential_type=some_cloud,
|
||||||
inputs={'turbo_button': True}
|
inputs={'turbo_button': True}
|
||||||
)
|
)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -785,7 +786,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
credential_type=some_cloud,
|
credential_type=some_cloud,
|
||||||
inputs = {'api_token': 'ABC123'}
|
inputs = {'api_token': 'ABC123'}
|
||||||
)
|
)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -819,7 +820,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
inputs = {'password': 'SUPER-SECRET-123'}
|
inputs = {'password': 'SUPER-SECRET-123'}
|
||||||
)
|
)
|
||||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -852,7 +853,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
credential_type=some_cloud,
|
credential_type=some_cloud,
|
||||||
inputs = {'api_token': 'ABC123'}
|
inputs = {'api_token': 'ABC123'}
|
||||||
)
|
)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -884,7 +885,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
credential_type=some_cloud,
|
credential_type=some_cloud,
|
||||||
inputs={'turbo_button': True}
|
inputs={'turbo_button': True}
|
||||||
)
|
)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -915,7 +916,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
credential_type=some_cloud,
|
credential_type=some_cloud,
|
||||||
inputs={'turbo_button': True}
|
inputs={'turbo_button': True}
|
||||||
)
|
)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -951,7 +952,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
inputs = {'password': 'SUPER-SECRET-123'}
|
inputs = {'password': 'SUPER-SECRET-123'}
|
||||||
)
|
)
|
||||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
assert self.run_pexpect.call_count == 1
|
assert self.run_pexpect.call_count == 1
|
||||||
@@ -987,7 +988,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
credential_type=some_cloud,
|
credential_type=some_cloud,
|
||||||
inputs = {'api_token': 'ABC123'}
|
inputs = {'api_token': 'ABC123'}
|
||||||
)
|
)
|
||||||
self.instance.extra_credentials.add(credential)
|
self.instance.credentials.add(credential)
|
||||||
self.task.run(self.pk)
|
self.task.run(self.pk)
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
@@ -1010,7 +1011,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
gce_credential.inputs['ssh_key_data'] = encrypt_field(gce_credential, 'ssh_key_data')
|
gce_credential.inputs['ssh_key_data'] = encrypt_field(gce_credential, 'ssh_key_data')
|
||||||
self.instance.extra_credentials.add(gce_credential)
|
self.instance.credentials.add(gce_credential)
|
||||||
|
|
||||||
azure_rm = CredentialType.defaults['azure_rm']()
|
azure_rm = CredentialType.defaults['azure_rm']()
|
||||||
azure_rm_credential = Credential(
|
azure_rm_credential = Credential(
|
||||||
@@ -1023,7 +1024,7 @@ class TestJobCredentials(TestJobExecution):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
azure_rm_credential.inputs['secret'] = encrypt_field(azure_rm_credential, 'secret')
|
azure_rm_credential.inputs['secret'] = encrypt_field(azure_rm_credential, 'secret')
|
||||||
self.instance.extra_credentials.add(azure_rm_credential)
|
self.instance.credentials.add(azure_rm_credential)
|
||||||
|
|
||||||
def run_pexpect_side_effect(*args, **kwargs):
|
def run_pexpect_side_effect(*args, **kwargs):
|
||||||
args, cwd, env, stdout = args
|
args, cwd, env, stdout = args
|
||||||
|
|||||||
163
docs/multi_credential_assignment.md
Normal file
163
docs/multi_credential_assignment.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
Multi-Credential Assignment
|
||||||
|
===========================
|
||||||
|
|
||||||
|
awx has added support for assigning zero or more credentials to
|
||||||
|
a JobTemplate via a singular, unified interface.
|
||||||
|
|
||||||
|
Background
|
||||||
|
----------
|
||||||
|
|
||||||
|
Prior to awx (Tower 3.2), Job Templates had a certain set of requirements
|
||||||
|
surrounding their relation to Credentials:
|
||||||
|
|
||||||
|
* All Job Templates (and Jobs) were required to have exactly *one* Machine/SSH
|
||||||
|
or Vault credential (or one of both).
|
||||||
|
* All Job Templates (and Jobs) could have zero or more "extra" Credentials.
|
||||||
|
* These extra Credentials represented "Cloud" and "Network" credentials that
|
||||||
|
* could be used to provide authentication to external services via environment
|
||||||
|
* variables (e.g., AWS_ACCESS_KEY_ID).
|
||||||
|
|
||||||
|
This model required a variety of disjoint interfaces for specifying Credentials
|
||||||
|
on a JobTemplate. For example, to modify assignment of Machine/SSH and Vault
|
||||||
|
credentials, you would change the Credential key itself:
|
||||||
|
|
||||||
|
`PATCH /api/v2/job_templates/N/ {'credential': X, 'vault_credential': Y}`
|
||||||
|
|
||||||
|
Modifying `extra_credentials` was accomplished on a separate API endpoint
|
||||||
|
via association/disassociation actions:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v2/job_templates/N/extra_credentials {'associate': true, 'id': Z}
|
||||||
|
POST /api/v2/job_templates/N/extra_credentials {'disassociate': true, 'id': Z}
|
||||||
|
```
|
||||||
|
|
||||||
|
This model lacked the ability associate multiple Vault credentials with
|
||||||
|
a playbook run, a use case supported by Ansible core from Ansible 2.4 onwards.
|
||||||
|
|
||||||
|
This model also was a stumbling block for certain playbook execution workflows.
|
||||||
|
For example, some users wanted to run playbooks with `connection:local` that
|
||||||
|
only interacted with some cloud service via a cloud Credential. In this
|
||||||
|
scenario, users often generated a "dummy" Machine/SSH Credential to attach to
|
||||||
|
the Job Template simply to satisfy the requirement on the model.
|
||||||
|
|
||||||
|
Important Changes
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
JobTemplates now have a single interface for Credential assignment:
|
||||||
|
|
||||||
|
`GET /api/v2/job_templates/N/credentials/`
|
||||||
|
|
||||||
|
Users can associate and disassociate credentials using `POST` requests to this
|
||||||
|
interface, similar to the behavior in the now-deprecated `extra_credentials`
|
||||||
|
endpoint:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v2/job_templates/N/credentials/ {'associate': true, 'id': X'}
|
||||||
|
POST /api/v2/job_templates/N/credentials/ {'disassociate': true, 'id': Y'}
|
||||||
|
```
|
||||||
|
|
||||||
|
Under this model, a JobTemplate is considered valid even when it has _zero_
|
||||||
|
Credentials assigned to it.
|
||||||
|
|
||||||
|
Launch Time Considerations
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Prior to this change, JobTemplates had a configurable attribute,
|
||||||
|
`ask_credential_on_launch`. This value was used at launch time to determine
|
||||||
|
which missing credential values were necessary for launch - this was primarily
|
||||||
|
used as a mechanism for users to specify an SSH (or Vault) credential to satisfy
|
||||||
|
the minimum Credential requirement.
|
||||||
|
|
||||||
|
Under the new unified Credential list model, this attribute still exists, but it
|
||||||
|
is no longer bound to a notion of "requiring" a Credential. Now when
|
||||||
|
`ask_credential_on_launch` is `True`, it signifies that users may (if they
|
||||||
|
wish) specify a list of credentials at launch time to override those defined on
|
||||||
|
the JobTemplate:
|
||||||
|
|
||||||
|
`POST /api/v2/job_templates/N/launch/ {'credentials': [A, B, C]}`
|
||||||
|
|
||||||
|
If `ask_credential_on_launch` is `False`, it signifies that custom `credentials`
|
||||||
|
provided in the payload to `POST /api/v2/job_templates/N/launch/` will be
|
||||||
|
ignored.
|
||||||
|
|
||||||
|
Under this model, the only purpose for `ask_credential_on_launch` is to signal
|
||||||
|
that API clients should prompt the user for (optional) changes at launch time.
|
||||||
|
|
||||||
|
Backwards Compatability Concerns
|
||||||
|
--------------------------------
|
||||||
|
A variety of API clients rely on now-deprecated mechanisms for Credential
|
||||||
|
retrieval and assignment, and those are still supported in a backwards
|
||||||
|
compatible way under this new API change. Requests to update
|
||||||
|
`JobTemplate.credential` and `JobTemplate.vault_credential` will still behave
|
||||||
|
as they did before:
|
||||||
|
|
||||||
|
`PATCH /api/v2/job_templates/N/ {'credential': X, 'vault_credential': Y}`
|
||||||
|
|
||||||
|
Under this model, when a JobTemplate with multiple vault Credentials is updated
|
||||||
|
in this way, the new underlying list will _only_ contain the single Vault
|
||||||
|
Credential specified in the deprecated request.
|
||||||
|
|
||||||
|
`GET` requests to `/api/v2/job_templates/N/` and `/api/v2/jobs/N/`
|
||||||
|
have traditionally included a variety of metadata in the response via
|
||||||
|
`related_fields`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"related": {
|
||||||
|
...
|
||||||
|
"credential": "/api/v2/credentials/1/",
|
||||||
|
"vault_credential": "/api/v2/credentials/3/",
|
||||||
|
"extra_credentials": "/api/v2/job_templates/5/extra_credentials/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
...and `summary_fields`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"summary_fields": {
|
||||||
|
"credential": {
|
||||||
|
"description": "",
|
||||||
|
"credential_type_id": 1,
|
||||||
|
"id": 1,
|
||||||
|
"kind": "ssh",
|
||||||
|
"name": "Demo Credential"
|
||||||
|
},
|
||||||
|
"vault_credential": {
|
||||||
|
"description": "",
|
||||||
|
"credential_type_id": 3,
|
||||||
|
"id": 3,
|
||||||
|
"kind": "vault",
|
||||||
|
"name": "some-vault"
|
||||||
|
},
|
||||||
|
"extra_credentials": [
|
||||||
|
{
|
||||||
|
"description": "",
|
||||||
|
"credential_type_id": 5,
|
||||||
|
"id": 2,
|
||||||
|
"kind": "aws",
|
||||||
|
"name": "some-aws"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "",
|
||||||
|
"credential_type_id": 10,
|
||||||
|
"id": 4,
|
||||||
|
"kind": "gce",
|
||||||
|
"name": "some-gce"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
These metadata will continue to exist and function in a backwards-compatible way.
|
||||||
|
|
||||||
|
The `/api/v2/job_templates/N/extra_credentials` endpoint has been deprecated, but
|
||||||
|
will also continue to exist and function in the same manner for multiple releases.
|
||||||
|
|
||||||
|
The `/api/v2/job_templates/N/launch/` endpoint also provides
|
||||||
|
deprecated,backwards compatible support for specifying credentials at launch time
|
||||||
|
via the `credential`, `vault_credential`, and `extra_credentials` fields:
|
||||||
|
|
||||||
|
`POST /api/v2/job_templates/N/launch/ {'credential': A, 'vault_credential': B, 'extra_credentials': [C, D]}`
|
||||||
@@ -490,16 +490,16 @@ def make_the_data():
|
|||||||
defaults=dict(
|
defaults=dict(
|
||||||
inventory=inventory,
|
inventory=inventory,
|
||||||
project=project,
|
project=project,
|
||||||
credential=next(credential_gen),
|
|
||||||
created_by=next(creator_gen),
|
created_by=next(creator_gen),
|
||||||
modified_by=next(modifier_gen),
|
modified_by=next(modifier_gen),
|
||||||
playbook="debug.yml",
|
playbook="debug.yml",
|
||||||
**extra_kwargs)
|
**extra_kwargs)
|
||||||
)
|
)
|
||||||
|
job_template.credentials.add(next(credential_gen))
|
||||||
if ids['job_template'] % 7 == 0:
|
if ids['job_template'] % 7 == 0:
|
||||||
job_template.extra_credentials.add(next(credential_gen))
|
job_template.credentials.add(next(credential_gen))
|
||||||
if ids['job_template'] % 5 == 0: # formerly cloud credential
|
if ids['job_template'] % 5 == 0: # formerly cloud credential
|
||||||
job_template.extra_credentials.add(next(credential_gen))
|
job_template.credentials.add(next(credential_gen))
|
||||||
job_template._is_new = _
|
job_template._is_new = _
|
||||||
job_templates.append(job_template)
|
job_templates.append(job_template)
|
||||||
inv_idx += 1
|
inv_idx += 1
|
||||||
@@ -649,10 +649,9 @@ def make_the_data():
|
|||||||
job_template=job_template,
|
job_template=job_template,
|
||||||
status=job_stat, name="%s-%d" % (job_template.name, job_i),
|
status=job_stat, name="%s-%d" % (job_template.name, job_i),
|
||||||
project=job_template.project, inventory=job_template.inventory,
|
project=job_template.project, inventory=job_template.inventory,
|
||||||
credential=job_template.credential,
|
|
||||||
)
|
)
|
||||||
for ec in job_template.extra_credentials.all():
|
for ec in job_template.credentials.all():
|
||||||
job.extra_credentials.add(ec)
|
job.credentials.add(ec)
|
||||||
job._is_new = _
|
job._is_new = _
|
||||||
jobs.append(job)
|
jobs.append(job)
|
||||||
job_i += 1
|
job_i += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user