mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
replace all Job/JT relations with a single M2M credentials relation
Includes backwards compatibility for now-deprecated .credential, .vault_credential, and .extra_credentials This is a building block for multi-vault implementation and Alan's saved launch configurations (both coming soon) see: https://github.com/ansible/awx/issues/352 see: https://github.com/ansible/awx/issues/169
This commit is contained in:
parent
f887aaa71f
commit
28ce9b700e
@ -269,8 +269,10 @@ class FieldLookupBackend(BaseFilterBackend):
|
||||
|
||||
# Make legacy v1 Job/Template fields work for backwards compatability
|
||||
# TODO: remove after API v1 deprecation period
|
||||
if queryset.model._meta.object_name in ('JobTemplate', 'Job') and key in ('cloud_credential', 'network_credential'):
|
||||
key = 'extra_credentials'
|
||||
if queryset.model._meta.object_name in ('JobTemplate', 'Job') and key in (
|
||||
'credential', 'vault_credential', 'cloud_credential', 'network_credential'
|
||||
):
|
||||
key = 'credentials'
|
||||
|
||||
# Make legacy v1 Credential fields work for backwards compatability
|
||||
# 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_310': getattr(self, 'new_in_310', 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),
|
||||
'deprecated': getattr(self, 'deprecated', False),
|
||||
}
|
||||
|
||||
@ -54,6 +54,8 @@ from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, Ver
|
||||
|
||||
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.
|
||||
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')
|
||||
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):
|
||||
pass
|
||||
@ -2286,8 +2295,8 @@ class V1JobOptionsSerializer(BaseSerializer):
|
||||
fields = ('*', 'cloud_credential', 'network_credential')
|
||||
|
||||
V1_FIELDS = {
|
||||
'cloud_credential': models.PositiveIntegerField(blank=True, null=True, default=None),
|
||||
'network_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, help_text=DEPRECATED),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@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 Meta:
|
||||
fields = ('*', 'job_type', 'inventory', 'project', 'playbook',
|
||||
'credential', 'vault_credential', 'forks', 'limit',
|
||||
'verbosity', 'extra_vars', 'job_tags', 'force_handlers',
|
||||
'skip_tags', 'start_at_task', 'timeout', 'use_fact_cache',)
|
||||
'forks', 'limit', 'verbosity', 'extra_vars', 'job_tags',
|
||||
'force_handlers', 'skip_tags', 'start_at_task', 'timeout',
|
||||
'use_fact_cache',)
|
||||
|
||||
def get_fields(self):
|
||||
fields = super(JobOptionsSerializer, self).get_fields()
|
||||
|
||||
# TODO: remove when API v1 is removed
|
||||
if self.version == 1 and 'credential' in self.Meta.fields:
|
||||
if self.version == 1:
|
||||
fields.update(V1JobOptionsSerializer().get_fields())
|
||||
|
||||
fields.update(LegacyCredentialFields().get_fields())
|
||||
return fields
|
||||
|
||||
def get_related(self, obj):
|
||||
@ -2321,17 +2351,22 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||
if obj.project:
|
||||
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
|
||||
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:
|
||||
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 isinstance(obj, UnifiedJobTemplate):
|
||||
res['extra_credentials'] = self.reverse(
|
||||
'api:job_template_extra_credentials_list',
|
||||
kwargs={'pk': obj.pk}
|
||||
)
|
||||
res['credentials'] = self.reverse(
|
||||
'api:job_template_credentials_list',
|
||||
kwargs={'pk': obj.pk}
|
||||
)
|
||||
elif isinstance(obj, UnifiedJob):
|
||||
res['extra_credentials'] = self.reverse('api:job_extra_credentials_list', kwargs={'pk': obj.pk})
|
||||
res['credentials'] = self.reverse('api:job_credentials_list', kwargs={'pk': obj.pk})
|
||||
else:
|
||||
cloud_cred = obj.cloud_credential
|
||||
if cloud_cred:
|
||||
@ -2352,64 +2387,67 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
||||
ret['project'] = None
|
||||
if 'playbook' in ret:
|
||||
ret['playbook'] = ''
|
||||
if 'credential' in ret and not obj.credential:
|
||||
ret['credential'] = None
|
||||
if 'vault_credential' in ret and not obj.vault_credential:
|
||||
ret['vault_credential'] = None
|
||||
if self.version == 1 and 'credential' in self.Meta.fields:
|
||||
ret['credential'] = obj.credential
|
||||
ret['vault_credential'] = obj.vault_credential
|
||||
if self.version == 1:
|
||||
ret['cloud_credential'] = obj.cloud_credential
|
||||
ret['network_credential'] = obj.network_credential
|
||||
return ret
|
||||
|
||||
def create(self, validated_data):
|
||||
deprecated_fields = {}
|
||||
for key in ('cloud_credential', 'network_credential'):
|
||||
for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'):
|
||||
if key in validated_data:
|
||||
deprecated_fields[key] = validated_data.pop(key)
|
||||
obj = super(JobOptionsSerializer, self).create(validated_data)
|
||||
if 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)
|
||||
return obj
|
||||
|
||||
def update(self, obj, validated_data):
|
||||
deprecated_fields = {}
|
||||
for key in ('cloud_credential', 'network_credential'):
|
||||
for key in ('credential', 'vault_credential', 'cloud_credential', 'network_credential'):
|
||||
if key in validated_data:
|
||||
deprecated_fields[key] = validated_data.pop(key)
|
||||
obj = super(JobOptionsSerializer, self).update(obj, validated_data)
|
||||
if 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)
|
||||
return obj
|
||||
|
||||
def _update_deprecated_fields(self, fields, obj):
|
||||
for key, existing in (
|
||||
('credential', obj.credentials.filter(credential_type__kind='ssh')),
|
||||
('vault_credential', obj.credentials.filter(credential_type__kind='vault')),
|
||||
('cloud_credential', obj.cloud_credentials),
|
||||
('network_credential', obj.network_credentials),
|
||||
):
|
||||
if key in fields:
|
||||
for cred in existing:
|
||||
obj.extra_credentials.remove(cred)
|
||||
obj.credentials.remove(cred)
|
||||
if fields[key]:
|
||||
obj.extra_credentials.add(fields[key])
|
||||
obj.credentials.add(fields[key])
|
||||
obj.save()
|
||||
|
||||
def validate(self, attrs):
|
||||
v1_credentials = {}
|
||||
view = self.context.get('view', None)
|
||||
if self.version == 1: # TODO: remove in 3.3
|
||||
for attr, kind, error in (
|
||||
('cloud_credential', 'cloud', _('You must provide a cloud credential.')),
|
||||
('network_credential', 'net', _('You must provide a network credential.'))
|
||||
):
|
||||
if attr in attrs:
|
||||
v1_credentials[attr] = None
|
||||
pk = attrs.pop(attr)
|
||||
if pk:
|
||||
cred = v1_credentials[attr] = Credential.objects.get(pk=pk)
|
||||
if cred.credential_type.kind != kind:
|
||||
raise serializers.ValidationError({attr: error})
|
||||
if (not view) or (not view.request) or (view.request.user not in cred.use_role):
|
||||
raise PermissionDenied()
|
||||
for attr, kind, error in (
|
||||
('cloud_credential', 'cloud', _('You must provide a cloud credential.')),
|
||||
('network_credential', 'net', _('You must provide a network credential.')),
|
||||
('credential', 'ssh', _('You must provide an SSH credential.')),
|
||||
('vault_credential', 'vault', _('You must provide a vault credential.')),
|
||||
):
|
||||
if kind in ('cloud', 'net') and self.version > 1:
|
||||
continue # cloud and net deprecated creds are v1 only
|
||||
if attr in attrs:
|
||||
v1_credentials[attr] = None
|
||||
pk = attrs.pop(attr)
|
||||
if pk:
|
||||
cred = v1_credentials[attr] = Credential.objects.get(pk=pk)
|
||||
if cred.credential_type.kind != kind:
|
||||
raise serializers.ValidationError({attr: error})
|
||||
if view and view.request and view.request.user not in cred.use_role:
|
||||
raise PermissionDenied()
|
||||
|
||||
if 'project' in self.fields and 'playbook' in self.fields:
|
||||
project = attrs.get('project', self.instance and self.instance.project or None)
|
||||
@ -2492,19 +2530,11 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
||||
return attrs.get(fd, self.instance and getattr(self.instance, fd) or None)
|
||||
|
||||
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')
|
||||
|
||||
prompting_error_message = _("Must either set a default value or ask to prompt on launch.")
|
||||
if project is None:
|
||||
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'):
|
||||
raise serializers.ValidationError({'inventory': prompting_error_message})
|
||||
|
||||
@ -2515,17 +2545,27 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
||||
|
||||
def get_summary_fields(self, 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 = []
|
||||
for cred in obj.extra_credentials.all():
|
||||
extra_creds.append({
|
||||
for cred in obj.credentials.all():
|
||||
summarized_cred = {
|
||||
'id': cred.pk,
|
||||
'name': cred.name,
|
||||
'description': cred.description,
|
||||
'kind': cred.kind,
|
||||
'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
|
||||
|
||||
|
||||
@ -2618,17 +2658,27 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
|
||||
def get_summary_fields(self, 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 = []
|
||||
for cred in obj.extra_credentials.all():
|
||||
extra_creds.append({
|
||||
for cred in obj.credentials.all():
|
||||
summarized_cred = {
|
||||
'id': cred.pk,
|
||||
'name': cred.name,
|
||||
'description': cred.description,
|
||||
'kind': cred.kind,
|
||||
'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
|
||||
|
||||
|
||||
@ -3250,7 +3300,7 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
model = JobTemplate
|
||||
fields = ('can_start_without_user_input', 'passwords_needed_to_start',
|
||||
'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_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch',
|
||||
'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_inventory_on_launch', 'ask_credential_on_launch',)
|
||||
extra_kwargs = {
|
||||
'credential': {'write_only': True,},
|
||||
'extra_credentials': {'write_only': True, 'default': [], 'allow_empty': True},
|
||||
'credentials': {'write_only': True, 'default': [], 'allow_empty': True},
|
||||
'limit': {'write_only': True,},
|
||||
'job_tags': {'write_only': True,},
|
||||
'skip_tags': {'write_only': True,},
|
||||
@ -3270,15 +3319,8 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
'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):
|
||||
return not (obj and obj.credential)
|
||||
return False
|
||||
|
||||
def get_inventory_needed_to_start(self, obj):
|
||||
return not (obj and obj.inventory)
|
||||
@ -3293,11 +3335,15 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
ask_for_vars_dict['vault_credential'] = False
|
||||
defaults_dict = {}
|
||||
for field in ask_for_vars_dict:
|
||||
if field in ('inventory', 'credential', 'vault_credential'):
|
||||
if field == 'inventory':
|
||||
defaults_dict[field] = dict(
|
||||
name=getattrd(obj, '%s.name' % 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:
|
||||
defaults_dict[field] = [
|
||||
dict(
|
||||
@ -3305,7 +3351,7 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
name=cred.name,
|
||||
credential_type=cred.credential_type.pk
|
||||
)
|
||||
for cred in obj.extra_credentials.all()
|
||||
for cred in obj.credentials.all()
|
||||
]
|
||||
else:
|
||||
defaults_dict[field] = getattr(obj, field)
|
||||
@ -3326,23 +3372,7 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
if obj.inventory and obj.inventory.pending_deletion is True:
|
||||
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', {})
|
||||
|
||||
try:
|
||||
extra_vars = parse_yaml_or_json(extra_vars, silent_failure=False)
|
||||
except ParseError as e:
|
||||
@ -3354,14 +3384,15 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
if validation_errors:
|
||||
errors['variables_needed_to_start'] = validation_errors
|
||||
|
||||
extra_cred_kinds = []
|
||||
for cred in data.get('extra_credentials', []):
|
||||
# Prohibit credential assign of the same CredentialType.kind
|
||||
# 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)
|
||||
if cred.credential_type.pk in extra_cred_kinds:
|
||||
errors['extra_credentials'] = _('Cannot assign multiple %s credentials.' % cred.credential_type.name)
|
||||
if cred.credential_type.kind not in ('net', 'cloud'):
|
||||
errors['extra_credentials'] = _('Extra credentials must be network or cloud.')
|
||||
extra_cred_kinds.append(cred.credential_type.pk)
|
||||
if cred.credential_type.pk in distinct_cred_kinds:
|
||||
errors['credentials'] = _('Cannot assign multiple %s credentials.' % cred.credential_type.name)
|
||||
distinct_cred_kinds.append(cred.credential_type.pk)
|
||||
|
||||
# Special prohibited cases for scan jobs
|
||||
errors.update(obj._extra_job_type_errors(data))
|
||||
@ -3375,9 +3406,8 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
JT_job_tags = obj.job_tags
|
||||
JT_skip_tags = obj.skip_tags
|
||||
JT_inventory = obj.inventory
|
||||
JT_credential = obj.credential
|
||||
JT_verbosity = obj.verbosity
|
||||
extra_credentials = attrs.pop('extra_credentials', None)
|
||||
credentials = attrs.pop('credentials', None)
|
||||
attrs = super(JobLaunchSerializer, self).validate(attrs)
|
||||
obj.extra_vars = JT_extra_vars
|
||||
obj.limit = JT_limit
|
||||
@ -3385,10 +3415,29 @@ class JobLaunchSerializer(BaseSerializer):
|
||||
obj.skip_tags = JT_skip_tags
|
||||
obj.job_tags = JT_job_tags
|
||||
obj.inventory = JT_inventory
|
||||
obj.credential = JT_credential
|
||||
obj.verbosity = JT_verbosity
|
||||
if extra_credentials is not None:
|
||||
attrs['extra_credentials'] = extra_credentials
|
||||
if credentials is not None:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -10,4 +10,5 @@
|
||||
{% 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_320 %}> _New in Ansible Tower 3.2.0_{% endif %}
|
||||
{% if new_in_330 %}> _New in Ansible Tower 3.3.0_{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@ -26,9 +26,6 @@ The response will include the following fields:
|
||||
job_template (array, read-only)
|
||||
* `survey_enabled`: Flag indicating whether the job_template has an enabled
|
||||
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
|
||||
associated with the job template. If not then one should be supplied when
|
||||
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
|
||||
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
|
||||
escaped parentheses. If `credential_needed_to_start` is `True` then the
|
||||
`credential` field is required and if the `inventory_needed_to_start` is
|
||||
`True` then the `inventory` is required as well.
|
||||
escaped parentheses. If the `inventory_needed_to_start` is `True` then the
|
||||
`inventory` is required.
|
||||
|
||||
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
|
||||
|
||||
@ -18,7 +18,9 @@ from awx.api.views import (
|
||||
UnifiedJobTemplateList,
|
||||
UnifiedJobList,
|
||||
HostAnsibleFactsDetail,
|
||||
JobCredentialsList,
|
||||
JobExtraCredentialsList,
|
||||
JobTemplateCredentialsList,
|
||||
JobTemplateExtraCredentialsList,
|
||||
)
|
||||
|
||||
@ -108,7 +110,9 @@ v2_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'^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]+)/credentials/$', JobTemplateCredentialsList.as_view(), name='job_template_credentials_list'),
|
||||
]
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
125
awx/api/views.py
125
awx/api/views.py
@ -13,7 +13,7 @@ import sys
|
||||
import logging
|
||||
import requests
|
||||
from base64 import b64encode
|
||||
from collections import OrderedDict
|
||||
from collections import OrderedDict, Iterable
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
@ -2669,7 +2669,7 @@ class JobTemplateList(ListCreateAPIView):
|
||||
always_allow_superuser = False
|
||||
capabilities_prefetch = [
|
||||
'admin', 'execute',
|
||||
{'copy': ['project.use', 'inventory.use', 'credential.use', 'vault_credential.use']}
|
||||
{'copy': ['project.use', 'inventory.use']}
|
||||
]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
@ -2711,15 +2711,13 @@ class JobTemplateLaunch(RetrieveAPIView):
|
||||
data['extra_vars'] = extra_vars
|
||||
ask_for_vars_dict = obj._ask_for_vars_dict()
|
||||
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:
|
||||
if not ask_for_vars_dict[field]:
|
||||
data.pop(field, None)
|
||||
elif field == 'inventory' or field == 'credential':
|
||||
elif field == 'inventory':
|
||||
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
|
||||
elif field == 'extra_credentials':
|
||||
data[field] = [cred.id for cred in obj.extra_credentials.all()]
|
||||
elif field == 'credentials':
|
||||
data[field] = [cred.id for cred in obj.credentials.all()]
|
||||
else:
|
||||
data[field] = getattr(obj, field)
|
||||
return data
|
||||
@ -2733,13 +2731,56 @@ class JobTemplateLaunch(RetrieveAPIView):
|
||||
if fd not in request.data and id_fd in request.data:
|
||||
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:
|
||||
request.data._mutable = True
|
||||
extra_creds = request.data.pop('extra_credentials', None)
|
||||
if extra_creds is not None:
|
||||
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 = {}
|
||||
serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
|
||||
if not serializer.is_valid():
|
||||
@ -2749,17 +2790,14 @@ class JobTemplateLaunch(RetrieveAPIView):
|
||||
prompted_fields = _accepted_or_ignored[0]
|
||||
ignored_fields.update(_accepted_or_ignored[1])
|
||||
|
||||
for fd, model in (
|
||||
('credential', Credential),
|
||||
('vault_credential', Credential),
|
||||
('inventory', Inventory)):
|
||||
if fd in prompted_fields and prompted_fields[fd] != getattrd(obj, '{}.pk'.format(fd), None):
|
||||
new_res = get_object_or_400(model, pk=get_pk_from_dict(prompted_fields, fd))
|
||||
use_role = getattr(new_res, 'use_role')
|
||||
if request.user not in use_role:
|
||||
raise PermissionDenied()
|
||||
fd = 'inventory'
|
||||
if fd in prompted_fields and prompted_fields[fd] != getattrd(obj, '{}.pk'.format(fd), None):
|
||||
new_res = get_object_or_400(Inventory, pk=get_pk_from_dict(prompted_fields, fd))
|
||||
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)
|
||||
if request.user not in new_credential.use_role:
|
||||
raise PermissionDenied()
|
||||
@ -2920,17 +2958,17 @@ class JobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIVi
|
||||
new_in_300 = True
|
||||
|
||||
|
||||
class JobTemplateExtraCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
|
||||
model = Credential
|
||||
serializer_class = CredentialSerializer
|
||||
parent_model = JobTemplate
|
||||
relationship = 'extra_credentials'
|
||||
new_in_320 = True
|
||||
relationship = 'credentials'
|
||||
new_in_330 = True
|
||||
new_in_api_v2 = True
|
||||
|
||||
def get_queryset(self):
|
||||
# Return the full list of extra_credentials
|
||||
# Return the full list of credentials
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
sublist_qs = getattrd(parent, self.relationship)
|
||||
@ -2941,15 +2979,29 @@ class JobTemplateExtraCredentialsList(SubListCreateAttachDetachAPIView):
|
||||
return sublist_qs
|
||||
|
||||
def is_valid_relation(self, parent, sub, created=False):
|
||||
current_extra_types = [
|
||||
cred.credential_type.pk for cred in parent.extra_credentials.all()
|
||||
]
|
||||
current_extra_types = [cred.credential_type.pk for cred in parent.credentials.all()]
|
||||
if sub.credential_type.pk in current_extra_types:
|
||||
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 super(JobTemplateExtraCredentialsList, self).is_valid_relation(parent, sub, created)
|
||||
return valid
|
||||
|
||||
|
||||
class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
||||
@ -3720,14 +3772,26 @@ class JobDetail(UnifiedJobDeletionMixin, RetrieveUpdateDestroyAPIView):
|
||||
return super(JobDetail, self).update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class JobExtraCredentialsList(SubListAPIView):
|
||||
class JobCredentialsList(SubListAPIView):
|
||||
|
||||
model = Credential
|
||||
serializer_class = CredentialSerializer
|
||||
parent_model = Job
|
||||
relationship = 'extra_credentials'
|
||||
new_in_320 = True
|
||||
relationship = 'credentials'
|
||||
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):
|
||||
@ -4252,7 +4316,6 @@ class UnifiedJobTemplateList(ListAPIView):
|
||||
capabilities_prefetch = [
|
||||
'admin', 'execute',
|
||||
{'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use',
|
||||
'jobtemplate.credential.use', 'jobtemplate.vault_credential.use',
|
||||
'workflowjobtemplate.organization.admin']}
|
||||
]
|
||||
|
||||
|
||||
@ -1149,7 +1149,7 @@ class JobTemplateAccess(BaseAccess):
|
||||
|
||||
model = JobTemplate
|
||||
select_related = ('created_by', 'modified_by', 'inventory', 'project',
|
||||
'credential', 'next_schedule',)
|
||||
'next_schedule',)
|
||||
|
||||
def filtered_queryset(self):
|
||||
return self.model.accessible_objects(self.user, 'read_role')
|
||||
@ -1187,13 +1187,10 @@ class JobTemplateAccess(BaseAccess):
|
||||
else:
|
||||
return None
|
||||
|
||||
# If a credential is provided, the user should have use access to it.
|
||||
if not self.check_related('credential', Credential, data, role_field='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 credentials is provided, the user should have use access to them.
|
||||
for pk in data.get('credentials', []):
|
||||
if self.user not in get_object_or_400(Credential, pk=pk).use_role:
|
||||
return False
|
||||
|
||||
# If an inventory is provided, the user should have use access.
|
||||
inventory = get_value(Inventory, 'inventory')
|
||||
@ -1239,7 +1236,7 @@ class JobTemplateAccess(BaseAccess):
|
||||
self.check_license(feature='surveys')
|
||||
return True
|
||||
|
||||
for required_field in ('credential', 'inventory', 'project', 'vault_credential'):
|
||||
for required_field in ('inventory', 'project'):
|
||||
required_obj = getattr(obj, required_field, None)
|
||||
if required_field not in data_for_change and required_obj is not None:
|
||||
data_for_change[required_field] = required_obj.pk
|
||||
@ -1288,7 +1285,7 @@ class JobTemplateAccess(BaseAccess):
|
||||
if not obj.project.organization:
|
||||
return False
|
||||
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 super(JobTemplateAccess, self).can_attach(
|
||||
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):
|
||||
if relationship == "instance_groups":
|
||||
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 super(JobTemplateAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs)
|
||||
|
||||
@ -1316,7 +1313,7 @@ class JobAccess(BaseAccess):
|
||||
|
||||
model = Job
|
||||
select_related = ('created_by', 'modified_by', 'job_template', 'inventory',
|
||||
'project', 'credential', 'job_template',)
|
||||
'project', 'job_template',)
|
||||
prefetch_related = (
|
||||
'unified_job_template',
|
||||
'instance_group',
|
||||
@ -1399,29 +1396,27 @@ class JobAccess(BaseAccess):
|
||||
if self.user.is_superuser:
|
||||
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
|
||||
credential_access = obj.credential and self.user in obj.credential.use_role
|
||||
job_extra_credentials = set(obj.extra_credentials.all())
|
||||
if job_extra_credentials:
|
||||
credential_access = False
|
||||
job_credentials = set(obj.credentials.all())
|
||||
|
||||
# Check if JT execute access (and related prompts) is sufficient
|
||||
if obj.job_template is not None:
|
||||
prompts_access = True
|
||||
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():
|
||||
if fd == 'extra_credentials':
|
||||
job_fields[fd] = job_extra_credentials
|
||||
if fd == 'credentials':
|
||||
job_fields[fd] = job_credentials
|
||||
job_fields[fd] = getattr(obj, fd)
|
||||
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
|
||||
for fd in ignored_fields:
|
||||
if fd == 'extra_vars':
|
||||
continue # we cannot yet validate validity of prompted extra_vars
|
||||
elif fd == 'extra_credentials':
|
||||
if job_extra_credentials != jt_extra_credentials:
|
||||
# Job has extra_credentials that are not promptable
|
||||
elif fd == 'credentials':
|
||||
if job_credentials != jt_credentials:
|
||||
# Job has credentials that are not promptable
|
||||
prompts_access = False
|
||||
break
|
||||
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
|
||||
# from JT, assure that user has explicit access to them
|
||||
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:
|
||||
prompts_access = False
|
||||
if prompts_access and job_extra_credentials != jt_extra_credentials:
|
||||
for cred in job_extra_credentials:
|
||||
if prompts_access and job_credentials != jt_credentials:
|
||||
for cred in job_credentials:
|
||||
if self.user not in cred.use_role:
|
||||
prompts_access = False
|
||||
break
|
||||
if prompts_access and self.user in obj.job_template.execute_role:
|
||||
return True
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -45,10 +45,10 @@ class Command(BaseCommand):
|
||||
inventory=i,
|
||||
variables="ansible_connection: local",
|
||||
created_by=superuser)
|
||||
JobTemplate.objects.create(name='Demo Job Template',
|
||||
playbook='hello_world.yml',
|
||||
project=p,
|
||||
inventory=i,
|
||||
credential=c)
|
||||
jt = JobTemplate.objects.create(name='Demo Job Template',
|
||||
playbook='hello_world.yml',
|
||||
project=p,
|
||||
inventory=i)
|
||||
jt.credentials.add(c)
|
||||
print('Default organization 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='',
|
||||
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(
|
||||
blank=True,
|
||||
default=0,
|
||||
@ -184,22 +164,31 @@ class JobOptions(BaseModel):
|
||||
)
|
||||
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
|
||||
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
|
||||
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
|
||||
@property
|
||||
@ -221,10 +210,8 @@ class JobOptions(BaseModel):
|
||||
def passwords_needed_to_start(self):
|
||||
'''Return list of password field names needed to start the job.'''
|
||||
needed = []
|
||||
if self.credential:
|
||||
needed.extend(self.credential.passwords_needed)
|
||||
if self.vault_credential:
|
||||
needed.extend(self.vault_credential.passwords_needed)
|
||||
for cred in self.credentials.all():
|
||||
needed.extend(cred.passwords_needed)
|
||||
return needed
|
||||
|
||||
|
||||
@ -234,6 +221,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
||||
playbook) to an inventory source with a given credential.
|
||||
'''
|
||||
SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name')]
|
||||
PASSWORD_FIELDS = ('credential', 'vault_credential')
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
@ -298,12 +286,12 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
||||
@classmethod
|
||||
def _get_unified_job_field_names(cls):
|
||||
return ['name', 'description', 'job_type', 'inventory', 'project',
|
||||
'playbook', 'credential', 'vault_credential',
|
||||
'extra_credentials', 'forks', 'schedule', 'limit', 'verbosity',
|
||||
'job_tags', 'extra_vars', 'launch_type', 'force_handlers',
|
||||
'skip_tags', 'start_at_task', 'become_enabled', 'labels',
|
||||
'survey_passwords', 'allow_simultaneous', 'timeout',
|
||||
'use_fact_cache', 'diff_mode',]
|
||||
'playbook', 'credentials', 'forks', 'schedule', 'limit',
|
||||
'verbosity', 'job_tags', 'extra_vars', 'launch_type',
|
||||
'force_handlers', 'skip_tags', 'start_at_task',
|
||||
'become_enabled', 'labels', 'survey_passwords',
|
||||
'allow_simultaneous', 'timeout', 'use_fact_cache',
|
||||
'diff_mode',]
|
||||
|
||||
def resource_validation_data(self):
|
||||
'''
|
||||
@ -317,10 +305,6 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
||||
resources_needed_to_start.append('inventory')
|
||||
if not self.ask_inventory_on_launch:
|
||||
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
|
||||
if self.project is None:
|
||||
@ -379,9 +363,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
||||
job_type=self.ask_job_type_on_launch,
|
||||
verbosity=self.ask_verbosity_on_launch,
|
||||
inventory=self.ask_inventory_on_launch,
|
||||
credential=self.ask_credential_on_launch,
|
||||
vault_credential=self.ask_credential_on_launch,
|
||||
extra_credentials=self.ask_credential_on_launch,
|
||||
credentials=self.ask_credential_on_launch,
|
||||
)
|
||||
|
||||
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 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
|
||||
'''
|
||||
|
||||
@ -149,6 +149,10 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
||||
default='ok',
|
||||
editable=False,
|
||||
)
|
||||
credentials = models.ManyToManyField(
|
||||
'Credential',
|
||||
related_name='%(class)ss',
|
||||
)
|
||||
labels = models.ManyToManyField(
|
||||
"Label",
|
||||
blank=True,
|
||||
@ -579,6 +583,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
on_delete=models.SET_NULL,
|
||||
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):
|
||||
real_instance = self.get_real_instance()
|
||||
|
||||
@ -791,12 +791,15 @@ class BaseTask(LogErrorsTask):
|
||||
safe_env = self.build_safe_env(env, **kwargs)
|
||||
|
||||
# handle custom injectors specified on the CredentialType
|
||||
if hasattr(instance, 'all_credentials'):
|
||||
credentials = instance.all_credentials
|
||||
credentials = []
|
||||
if isinstance(instance, Job):
|
||||
credentials = instance.credentials.all()
|
||||
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]
|
||||
else:
|
||||
credentials = []
|
||||
|
||||
for credential in credentials:
|
||||
if credential:
|
||||
credential.credential_type.inject_credential(
|
||||
@ -927,7 +930,7 @@ class RunJob(BaseTask):
|
||||
}
|
||||
'''
|
||||
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
|
||||
# back (they will be written to a temporary file).
|
||||
if credential.ssh_key_data not in (None, ''):
|
||||
@ -957,11 +960,11 @@ class RunJob(BaseTask):
|
||||
and ansible-vault.
|
||||
'''
|
||||
passwords = super(RunJob, self).build_passwords(job, **kwargs)
|
||||
for cred, fields in {
|
||||
'credential': ('ssh_key_unlock', 'ssh_password', 'become_password'),
|
||||
'vault_credential': ('vault_password',)
|
||||
for kind, fields in {
|
||||
'ssh': ('ssh_key_unlock', 'ssh_password', 'become_password'),
|
||||
'vault': ('vault_password',)
|
||||
}.items():
|
||||
cred = getattr(job, cred, None)
|
||||
cred = job.get_deprecated_credential(kind)
|
||||
if cred:
|
||||
for field in fields:
|
||||
if field == 'ssh_password':
|
||||
@ -1072,7 +1075,8 @@ class RunJob(BaseTask):
|
||||
Build command line argument list for running ansible-playbook,
|
||||
optionally using ssh-agent for public/private key authentication.
|
||||
'''
|
||||
creds = job.credential
|
||||
creds = job.get_deprecated_credential('ssh')
|
||||
|
||||
ssh_username, become_username, become_method = '', '', ''
|
||||
if creds:
|
||||
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.inventory = inventory
|
||||
job.credential = credential
|
||||
job.project = project
|
||||
|
||||
if persisted:
|
||||
job.save()
|
||||
job.credentials.add(credential)
|
||||
job.project = project
|
||||
|
||||
return job
|
||||
|
||||
|
||||
@ -164,9 +164,11 @@ def mk_job_template(name, job_type='run',
|
||||
if jt.inventory is None:
|
||||
jt.ask_inventory_on_launch = True
|
||||
|
||||
jt.credential = credential
|
||||
if jt.credential is None:
|
||||
jt.ask_credential_on_launch = True
|
||||
if persisted and credential:
|
||||
jt.save()
|
||||
jt.credentials.add(credential)
|
||||
if jt.credential is None:
|
||||
jt.ask_credential_on_launch = True
|
||||
|
||||
jt.project = project
|
||||
|
||||
@ -178,10 +180,10 @@ def mk_job_template(name, job_type='run',
|
||||
jt.save()
|
||||
if cloud_credential:
|
||||
cloud_credential.save()
|
||||
jt.extra_credentials.add(cloud_credential)
|
||||
jt.credentials.add(cloud_credential)
|
||||
if network_credential:
|
||||
network_credential.save()
|
||||
jt.extra_credentials.add(network_credential)
|
||||
jt.credentials.add(network_credential)
|
||||
jt.save()
|
||||
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'])
|
||||
jt = job_template_factory("jt", organization=objs.organization,
|
||||
inventory='test_inv', project='test_proj').job_template
|
||||
jt.extra_credentials.add(credential)
|
||||
jt.credentials.add(credential)
|
||||
jt.save()
|
||||
job = jt.create_unified_job()
|
||||
|
||||
@ -22,8 +22,8 @@ def test_extra_credentials(get, organization_factory, job_template_factory, cred
|
||||
@pytest.mark.django_db
|
||||
def test_job_relaunch_permission_denied_response(
|
||||
post, get, inventory, project, credential, net_credential, machine_credential):
|
||||
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project,
|
||||
credential=machine_credential)
|
||||
jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project)
|
||||
jt.credentials.add(machine_credential)
|
||||
jt_user = User.objects.create(username='jobtemplateuser')
|
||||
jt.execute_role.members.add(jt_user)
|
||||
job = jt.create_unified_job()
|
||||
@ -33,7 +33,7 @@ def test_job_relaunch_permission_denied_response(
|
||||
assert r.data['summary_fields']['user_capabilities']['start']
|
||||
|
||||
# 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)
|
||||
assert 'launched with prompted fields' 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
|
||||
jt = JobTemplate.objects.create(
|
||||
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.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)
|
||||
|
||||
@ -28,7 +28,7 @@ def runtime_data(organization, credentialtype_ssh):
|
||||
job_tags='provision',
|
||||
skip_tags='restart',
|
||||
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
|
||||
def job_template_prompts(project, inventory, machine_credential):
|
||||
def rf(on_off):
|
||||
return JobTemplate.objects.create(
|
||||
jt = JobTemplate.objects.create(
|
||||
job_type='run',
|
||||
project=project,
|
||||
inventory=inventory,
|
||||
credential=machine_credential,
|
||||
name='deploy-job-template',
|
||||
ask_variables_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_verbosity_on_launch=on_off,
|
||||
)
|
||||
jt.credentials.add(machine_credential)
|
||||
return jt
|
||||
return rf
|
||||
|
||||
|
||||
@ -64,7 +65,6 @@ def job_template_prompts_null(project):
|
||||
job_type='run',
|
||||
project=project,
|
||||
inventory=None,
|
||||
credential=None,
|
||||
name='deploy-job-template',
|
||||
ask_variables_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('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)
|
||||
assert JobTemplate.create_unified_job.called
|
||||
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 'limit' 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 '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)
|
||||
|
||||
# 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)
|
||||
inventory = Inventory.objects.get(pk=runtime_data['inventory'])
|
||||
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(
|
||||
reverse('api:job_template_launch', kwargs={'pk':job_template.pk}),
|
||||
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['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
|
||||
@ -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
|
||||
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.'
|
||||
|
||||
@ -258,7 +258,7 @@ def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
|
||||
deploy_jobtemplate.ask_credential_on_launch = True
|
||||
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(
|
||||
instance=deploy_jobtemplate, data=kv,
|
||||
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)
|
||||
assert 'job_template_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.parametrize('pks, error_msg', [
|
||||
([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):
|
||||
def test_job_launch_with_default_creds(machine_credential, vault_credential, deploy_jobtemplate):
|
||||
deploy_jobtemplate.ask_credential_on_launch = True
|
||||
deploy_jobtemplate.save()
|
||||
|
||||
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
|
||||
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)
|
||||
deploy_jobtemplate.credentials.add(machine_credential)
|
||||
deploy_jobtemplate.credentials.add(vault_credential)
|
||||
kv = dict()
|
||||
serializer = JobLaunchSerializer(
|
||||
instance=deploy_jobtemplate, data=kv,
|
||||
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)
|
||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
||||
assert job_obj.credential.pk == machine_credential.pk
|
||||
assert job_obj.vault_credential.pk == vault_credential.pk
|
||||
assert job_obj.credential == machine_credential.pk
|
||||
assert job_obj.vault_credential == vault_credential.pk
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_job_launch_JT_with_default_vault_credential(machine_credential, vault_credential, deploy_jobtemplate):
|
||||
deploy_jobtemplate.credential = machine_credential
|
||||
deploy_jobtemplate.vault_credential = vault_credential
|
||||
def test_job_launch_with_empty_creds(machine_credential, vault_credential, deploy_jobtemplate):
|
||||
deploy_jobtemplate.ask_credential_on_launch = True
|
||||
deploy_jobtemplate.credentials.add(machine_credential)
|
||||
deploy_jobtemplate.credentials.add(vault_credential)
|
||||
kv = dict(credentials=[])
|
||||
serializer = JobLaunchSerializer(
|
||||
instance=deploy_jobtemplate, data={},
|
||||
context={'obj': deploy_jobtemplate, 'data': {}, 'passwords': {}})
|
||||
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(**{})
|
||||
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
|
||||
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
||||
|
||||
assert job_obj.vault_credential.pk == vault_credential.pk
|
||||
assert job_obj.credential is None
|
||||
assert job_obj.vault_credential is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -403,8 +314,7 @@ def test_job_launch_fails_with_missing_vault_password(machine_credential, vault_
|
||||
deploy_jobtemplate, post, rando):
|
||||
vault_credential.vault_password = 'ASK'
|
||||
vault_credential.save()
|
||||
deploy_jobtemplate.credential = machine_credential
|
||||
deploy_jobtemplate.vault_credential = vault_credential
|
||||
deploy_jobtemplate.credentials.add(vault_credential)
|
||||
deploy_jobtemplate.execute_role.members.add(rando)
|
||||
deploy_jobtemplate.save()
|
||||
|
||||
@ -421,7 +331,7 @@ def test_job_launch_fails_with_missing_ssh_password(machine_credential, deploy_j
|
||||
rando):
|
||||
machine_credential.password = 'ASK'
|
||||
machine_credential.save()
|
||||
deploy_jobtemplate.credential = machine_credential
|
||||
deploy_jobtemplate.credentials.add(machine_credential)
|
||||
deploy_jobtemplate.execute_role.members.add(rando)
|
||||
deploy_jobtemplate.save()
|
||||
|
||||
@ -440,8 +350,8 @@ def test_job_launch_fails_with_missing_vault_and_ssh_password(machine_credential
|
||||
vault_credential.save()
|
||||
machine_credential.password = 'ASK'
|
||||
machine_credential.save()
|
||||
deploy_jobtemplate.credential = machine_credential
|
||||
deploy_jobtemplate.vault_credential = vault_credential
|
||||
deploy_jobtemplate.credentials.add(machine_credential)
|
||||
deploy_jobtemplate.credentials.add(vault_credential)
|
||||
deploy_jobtemplate.execute_role.members.add(rando)
|
||||
deploy_jobtemplate.save()
|
||||
|
||||
@ -458,8 +368,8 @@ def test_job_launch_pass_with_prompted_vault_password(machine_credential, vault_
|
||||
deploy_jobtemplate, post, rando):
|
||||
vault_credential.vault_password = 'ASK'
|
||||
vault_credential.save()
|
||||
deploy_jobtemplate.credential = machine_credential
|
||||
deploy_jobtemplate.vault_credential = vault_credential
|
||||
deploy_jobtemplate.credentials.add(machine_credential)
|
||||
deploy_jobtemplate.credentials.add(vault_credential)
|
||||
deploy_jobtemplate.execute_role.members.add(rando)
|
||||
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')
|
||||
|
||||
|
||||
@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.job_runtime_vars
|
||||
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'), {
|
||||
'name': 'Some name',
|
||||
'project': project.id,
|
||||
'credential': machine_credential.id,
|
||||
'credentials': [machine_credential.id],
|
||||
'inventory': inventory.id,
|
||||
'playbook': 'helloworld.yml',
|
||||
}, 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'])
|
||||
jt = job_template_factory("jt", organization=objs.organization,
|
||||
inventory='test_inv', project='test_proj').job_template
|
||||
jt.extra_credentials.add(credential)
|
||||
jt.credentials.add(credential)
|
||||
jt.save()
|
||||
|
||||
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'])
|
||||
jt = job_template_factory("jt", organization=objs.organization,
|
||||
inventory='test_inv', project='test_proj').job_template
|
||||
jt.extra_credentials.add(credential)
|
||||
jt.extra_credentials.add(net_credential)
|
||||
jt.credentials.add(credential)
|
||||
jt.credentials.add(net_credential)
|
||||
jt.save()
|
||||
|
||||
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'])
|
||||
jt = job_template_factory("jt", organization=objs.organization,
|
||||
inventory='test_inv', project='test_proj').job_template
|
||||
jt.extra_credentials.add(credential)
|
||||
jt.extra_credentials.add(net_credential)
|
||||
jt.credentials.add(credential)
|
||||
jt.credentials.add(net_credential)
|
||||
jt.save()
|
||||
|
||||
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}), {
|
||||
'name': 'Some name',
|
||||
'project': objs.project.id,
|
||||
'credential': objs.credential.id,
|
||||
'credentials': [objs.credential.id],
|
||||
'inventory': objs.inventory.id,
|
||||
'playbook': 'alt-helloworld.yml',
|
||||
}, alice, expect=expect)
|
||||
@ -459,8 +459,7 @@ def test_launch_with_extra_credentials(get, post, organization_factory,
|
||||
resp = post(
|
||||
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
|
||||
dict(
|
||||
credential=machine_credential.pk,
|
||||
extra_credentials=[credential.pk, net_credential.pk]
|
||||
credentials=[machine_credential.pk, credential.pk, net_credential.pk]
|
||||
),
|
||||
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'])
|
||||
jt = job_template_factory("jt", organization=objs.organization,
|
||||
inventory='test_inv', project='test_proj').job_template
|
||||
jt.credential = machine_credential
|
||||
jt.credentials.add(machine_credential)
|
||||
jt.ask_credential_on_launch = False
|
||||
jt.save()
|
||||
|
||||
resp = post(
|
||||
reverse('api:job_template_launch', kwargs={'pk': jt.pk}),
|
||||
dict(
|
||||
credential=machine_credential.pk,
|
||||
extra_credentials=[credential.pk, net_credential.pk]
|
||||
credentials=[machine_credential.pk, credential.pk, net_credential.pk]
|
||||
),
|
||||
objs.superusers.admin
|
||||
)
|
||||
assert 'credential' in resp.data['ignored_fields'].keys()
|
||||
assert 'extra_credentials' in resp.data['ignored_fields'].keys()
|
||||
assert 'credentials' in resp.data['ignored_fields'].keys()
|
||||
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
|
||||
|
||||
|
||||
@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
|
||||
def test_v1_launch_with_extra_credentials(get, post, organization_factory,
|
||||
job_template_factory, machine_credential,
|
||||
|
||||
@ -89,8 +89,7 @@ class TestJobTemplateCopyEdit:
|
||||
job_type='run',
|
||||
project=project,
|
||||
inventory=None, ask_inventory_on_launch=False, # not allowed
|
||||
credential=None, ask_credential_on_launch=True,
|
||||
name='deploy-job-template'
|
||||
ask_credential_on_launch=True, name='deploy-job-template'
|
||||
)
|
||||
serializer = JobTemplateSerializer(jt_res, context=self.fake_context(admin_user))
|
||||
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']['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):
|
||||
"""
|
||||
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
|
||||
def test_prefetch_jt_copy_capability(job_template, project, inventory,
|
||||
machine_credential, vault_credential, rando):
|
||||
def test_prefetch_jt_copy_capability(job_template, project, inventory, rando):
|
||||
job_template.project = project
|
||||
job_template.inventory = inventory
|
||||
job_template.credential = machine_credential
|
||||
job_template.vault_credential = vault_credential
|
||||
job_template.save()
|
||||
|
||||
qs = JobTemplate.objects.all()
|
||||
cache_list_capabilities(qs, [{'copy': [
|
||||
'project.use', 'inventory.use', 'credential.use', 'vault_credential.use'
|
||||
'project.use', 'inventory.use',
|
||||
]}], JobTemplate, rando)
|
||||
assert qs[0].capabilities_cache == {'copy': False}
|
||||
|
||||
project.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': [
|
||||
'project.use', 'inventory.use', 'credential.use', 'vault_credential.use'
|
||||
'project.use', 'inventory.use',
|
||||
]}], JobTemplate, rando)
|
||||
assert qs[0].capabilities_cache == {'copy': True}
|
||||
|
||||
|
||||
@ -81,26 +81,26 @@ def user():
|
||||
|
||||
@pytest.fixture
|
||||
def check_jobtemplate(project, inventory, credential):
|
||||
return \
|
||||
JobTemplate.objects.create(
|
||||
job_type='check',
|
||||
project=project,
|
||||
inventory=inventory,
|
||||
credential=credential,
|
||||
name='check-job-template'
|
||||
)
|
||||
jt = JobTemplate.objects.create(
|
||||
job_type='check',
|
||||
project=project,
|
||||
inventory=inventory,
|
||||
name='check-job-template'
|
||||
)
|
||||
jt.credentials.add(credential)
|
||||
return jt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deploy_jobtemplate(project, inventory, credential):
|
||||
return \
|
||||
JobTemplate.objects.create(
|
||||
job_type='run',
|
||||
project=project,
|
||||
inventory=inventory,
|
||||
credential=credential,
|
||||
name='deploy-job-template'
|
||||
)
|
||||
jt = JobTemplate.objects.create(
|
||||
job_type='run',
|
||||
project=project,
|
||||
inventory=inventory,
|
||||
name='deploy-job-template'
|
||||
)
|
||||
jt.credentials.add(credential)
|
||||
return jt
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@ -54,12 +54,11 @@ class TestImplicitRolesOmitted:
|
||||
assert qs[1].operation == 'delete'
|
||||
|
||||
@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(
|
||||
name='test-jt',
|
||||
project=project,
|
||||
inventory=inventory,
|
||||
credential=credential
|
||||
)
|
||||
qs = ActivityStream.objects.filter(job_template__isnull=False)
|
||||
assert qs.count() == 1
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from awx.main.models import Credential
|
||||
|
||||
|
||||
@ -12,23 +11,10 @@ def test_clean_credential_with_ssh_type(credentialtype_ssh, job_template):
|
||||
)
|
||||
credential.save()
|
||||
|
||||
job_template.credential = credential
|
||||
job_template.credentials.add(credential)
|
||||
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
|
||||
def test_clean_credential_with_custom_types(credentialtype_aws, credentialtype_net, job_template):
|
||||
aws = Credential(
|
||||
@ -42,6 +28,6 @@ def test_clean_credential_with_custom_types(credentialtype_aws, credentialtype_n
|
||||
)
|
||||
net.save()
|
||||
|
||||
job_template.extra_credentials.add(aws)
|
||||
job_template.extra_credentials.add(net)
|
||||
job_template.credentials.add(aws)
|
||||
job_template.credentials.add(net)
|
||||
job_template.full_clean()
|
||||
|
||||
@ -50,14 +50,15 @@ class TestCreateUnifiedJob:
|
||||
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,
|
||||
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.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',
|
||||
return_value=['inventory', 'credential', 'limit']):
|
||||
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.inventory == job_with_links.inventory
|
||||
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
|
||||
|
||||
@ -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:
|
||||
|
||||
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)
|
||||
credential_user = user('user2', False)
|
||||
both_user = user('user3', False)
|
||||
|
||||
# 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)
|
||||
assert both_user.can_access(Job, 'start', job_with_links, validate_license=False)
|
||||
|
||||
# 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)
|
||||
|
||||
# Confirm that a user with inventory access alone cannot launch
|
||||
job_with_links.inventory.use_role.members.add(inventory_user)
|
||||
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):
|
||||
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 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.can_access(Job, 'start', job, validate_license=False)
|
||||
|
||||
# Job has prompted extra_credential, launch denied w/ message
|
||||
job.extra_credentials.add(net_credential)
|
||||
# Job has prompted net credential, launch denied w/ message
|
||||
job.credentials.add(net_credential)
|
||||
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):
|
||||
jt = JobTemplate.objects.create(
|
||||
name='testjt', inventory=inventory, project=project,
|
||||
@ -180,11 +181,11 @@ class TestJobRelaunchAccess:
|
||||
job = jt.create_unified_job()
|
||||
jt.execute_role.members.add(rando)
|
||||
|
||||
# Job has prompted extra_credential, rando lacks permission to use it
|
||||
job.extra_credentials.add(net_credential)
|
||||
# Job has prompted net credential, rando lacks permission to use it
|
||||
job.credentials.add(net_credential)
|
||||
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):
|
||||
jt = JobTemplate.objects.create(
|
||||
name='testjt', inventory=inventory, project=project,
|
||||
@ -192,23 +193,24 @@ class TestJobRelaunchAccess:
|
||||
job = jt.create_unified_job()
|
||||
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)
|
||||
job.extra_credentials.add(net_credential)
|
||||
job.credentials.add(net_credential)
|
||||
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):
|
||||
jt = JobTemplate.objects.create(
|
||||
name='testjt', inventory=inventory, project=project,
|
||||
credential=credential, ask_credential_on_launch=True)
|
||||
ask_credential_on_launch=True)
|
||||
job = jt.create_unified_job()
|
||||
project.admin_role.members.add(rando)
|
||||
inventory.admin_role.members.add(rando)
|
||||
credential.admin_role.members.add(rando)
|
||||
|
||||
# Relaunch blocked by the extra credential
|
||||
job.extra_credentials.add(net_credential)
|
||||
# Relaunch blocked by the net credential
|
||||
job.credentials.add(credential)
|
||||
job.credentials.add(net_credential)
|
||||
assert not rando.can_access(Job, 'start', job, validate_license=False)
|
||||
|
||||
|
||||
|
||||
@ -47,16 +47,18 @@ def test_inventory_use_access(inventory, user):
|
||||
class TestJobRelaunchAccess:
|
||||
@pytest.fixture
|
||||
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()
|
||||
|
||||
@pytest.fixture
|
||||
def job_with_prompts(self, machine_credential, inventory, organization, credentialtype_ssh):
|
||||
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_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)
|
||||
jt.credentials.add(machine_credential)
|
||||
new_cred = Credential.objects.create(
|
||||
name='new-cred',
|
||||
credential_type=credentialtype_ssh,
|
||||
@ -65,8 +67,9 @@ class TestJobRelaunchAccess:
|
||||
'password': 'pas4word'
|
||||
}
|
||||
)
|
||||
new_cred.save()
|
||||
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):
|
||||
"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):
|
||||
"Has use_role on the prompted inventory & credential - allow relaunch"
|
||||
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)
|
||||
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',
|
||||
credential='cred1')
|
||||
jt = objects.job_template
|
||||
jt.vault_credential = vault_credential
|
||||
jt.credentials.add(vault_credential)
|
||||
jt.save()
|
||||
# Add AWS cloud credential and network credential
|
||||
jt.extra_credentials.add(credential)
|
||||
jt.extra_credentials.add(net_credential)
|
||||
jt.credentials.add(credential)
|
||||
jt.credentials.add(net_credential)
|
||||
return jt
|
||||
|
||||
|
||||
@ -47,15 +47,15 @@ def test_job_template_access_read_level(jt_linked, rando):
|
||||
access = JobTemplateAccess(rando)
|
||||
jt_linked.project.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
|
||||
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(vault_credential=jt_linked.vault_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, project=proj_pk))
|
||||
|
||||
for cred in jt_linked.extra_credentials.all():
|
||||
assert not access.can_unattach(jt_linked, cred, 'extra_credentials', {})
|
||||
for cred in jt_linked.credentials.all():
|
||||
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -64,16 +64,16 @@ def test_job_template_access_use_level(jt_linked, rando):
|
||||
access = JobTemplateAccess(rando)
|
||||
jt_linked.project.use_role.members.add(rando)
|
||||
jt_linked.inventory.use_role.members.add(rando)
|
||||
jt_linked.credential.use_role.members.add(rando)
|
||||
jt_linked.vault_credential.use_role.members.add(rando)
|
||||
jt_linked.get_deprecated_credential('ssh').use_role.members.add(rando)
|
||||
jt_linked.get_deprecated_credential('vault').use_role.members.add(rando)
|
||||
|
||||
proj_pk = jt_linked.project.pk
|
||||
assert access.can_add(dict(inventory=jt_linked.inventory.pk, project=proj_pk))
|
||||
assert access.can_add(dict(credential=jt_linked.credential.pk, project=proj_pk))
|
||||
assert access.can_add(dict(vault_credential=jt_linked.vault_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, project=proj_pk))
|
||||
|
||||
for cred in jt_linked.extra_credentials.all():
|
||||
assert not access.can_unattach(jt_linked, cred, 'extra_credentials', {})
|
||||
for cred in jt_linked.credentials.all():
|
||||
assert not access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||
|
||||
|
||||
@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)
|
||||
# Assign organization permission in the same way the create view does
|
||||
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
|
||||
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():
|
||||
assert access.can_unattach(jt_linked, cred, 'extra_credentials', {})
|
||||
for cred in jt_linked.credentials.all():
|
||||
assert access.can_unattach(jt_linked, cred, 'credentials', {})
|
||||
|
||||
assert access.can_read(jt_linked)
|
||||
assert access.can_delete(jt_linked)
|
||||
@ -104,9 +104,9 @@ def test_job_template_extra_credentials_prompts_access(
|
||||
project = project,
|
||||
playbook = 'helloworld.yml',
|
||||
inventory = inventory,
|
||||
credential = machine_credential,
|
||||
ask_credential_on_launch = True
|
||||
)
|
||||
jt.credentials.add(machine_credential)
|
||||
jt.execute_role.members.add(rando)
|
||||
r = post(
|
||||
reverse('api:job_template_launch', kwargs={'version': 'v2', 'pk': jt.id}),
|
||||
@ -123,24 +123,24 @@ class TestJobTemplateCredentials:
|
||||
credential.read_role.members.add(rando)
|
||||
# without permission to credential, user can not attach it
|
||||
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):
|
||||
job_template.admin_role.members.add(rando)
|
||||
credential.use_role.members.add(rando)
|
||||
# user has permission to apply credential
|
||||
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):
|
||||
job_template.admin_role.members.add(rando)
|
||||
# not allowed to use the vault cred
|
||||
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):
|
||||
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
|
||||
|
||||
@ -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.main.access.JobTemplateAccess.can_change", return_value='foobar'):
|
||||
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']['edit'] == 'foobar'
|
||||
|
||||
@ -272,4 +272,4 @@ class TestResourceAccessList:
|
||||
def test_related_search_reverse_FK_field():
|
||||
view = ListAPIView()
|
||||
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 json
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
def test_missing_project_error(job_template_factory):
|
||||
objects = job_template_factory(
|
||||
'missing-project-jt',
|
||||
organization='org1',
|
||||
inventory='inventory1',
|
||||
credential='cred1',
|
||||
persisted=False)
|
||||
obj = objects.job_template
|
||||
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
|
||||
|
||||
|
||||
def test_inventory_credential_need_to_start(job_template_factory):
|
||||
def test_inventory_need_to_start(job_template_factory):
|
||||
objects = job_template_factory(
|
||||
'job-template-few-resources',
|
||||
project='project1',
|
||||
persisted=False)
|
||||
obj = objects.job_template
|
||||
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(
|
||||
'job-template-paradox',
|
||||
project='project1',
|
||||
persisted=False)
|
||||
obj = objects.job_template
|
||||
obj.ask_inventory_on_launch = False
|
||||
obj.ask_credential_on_launch = False
|
||||
validation_errors, resources_needed_to_start = obj.resource_validation_data()
|
||||
assert 'inventory' in validation_errors
|
||||
assert 'credential' in validation_errors
|
||||
|
||||
|
||||
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.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)
|
||||
|
||||
def test_char_prompts_and_res_node_prompts(self, job_node_with_prompts):
|
||||
# TBD: properly handle multicred credential assignment
|
||||
expect_kwargs = dict(
|
||||
inventory=job_node_with_prompts.inventory.pk,
|
||||
credential=job_node_with_prompts.credential.pk,
|
||||
**example_prompts)
|
||||
expect_kwargs.update(self.kwargs_base)
|
||||
assert job_node_with_prompts.get_job_kwargs() == expect_kwargs
|
||||
|
||||
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_job_type_on_launch = False
|
||||
expect_kwargs = dict(inventory=job_node_with_prompts.inventory.pk,
|
||||
credential=job_node_with_prompts.credential.pk,
|
||||
**example_prompts)
|
||||
expect_kwargs.update(self.kwargs_base)
|
||||
expect_kwargs.pop('inventory')
|
||||
@ -239,6 +239,5 @@ class TestWorkflowWarnings:
|
||||
job_node_with_prompts.unified_job_template.ask_job_type_on_launch = False
|
||||
assert 'ignored' in job_node_with_prompts.get_prompts_warnings()
|
||||
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']) == 2
|
||||
assert len(job_node_with_prompts.get_prompts_warnings()['ignored']) == 1
|
||||
|
||||
|
||||
@ -128,9 +128,6 @@ def job_template_with_ids(job_template_factory):
|
||||
cloud_type = CredentialType(kind='aws')
|
||||
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')
|
||||
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,
|
||||
cloud_credential=cloud_cred, network_credential=net_cred,
|
||||
persisted=False)
|
||||
jt_objects.job_template.vault_credential = vault_cred
|
||||
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({
|
||||
'inventory': data['inventory'],
|
||||
'project': job_template_with_ids.project.id,
|
||||
'credential': job_template_with_ids.credential.id,
|
||||
'vault_credential': job_template_with_ids.vault_credential.id
|
||||
'project': job_template_with_ids.project.id
|
||||
})
|
||||
|
||||
|
||||
|
||||
@ -243,12 +243,13 @@ class TestJobExecution:
|
||||
verbosity=3
|
||||
)
|
||||
|
||||
# mock the job.extra_credentials M2M relation so we can avoid DB access
|
||||
job._extra_credentials = []
|
||||
patch = mock.patch.object(Job, 'extra_credentials', mock.Mock(
|
||||
all=lambda: job._extra_credentials,
|
||||
add=job._extra_credentials.append,
|
||||
spec_set=['all', 'add']
|
||||
# mock the job.credentials M2M relation so we can avoid DB access
|
||||
job._credentials = []
|
||||
patch = mock.patch.object(UnifiedJob, 'credentials', mock.Mock(
|
||||
all=lambda: job._credentials,
|
||||
add=job._credentials.append,
|
||||
filter=mock.Mock(return_value=job._credentials),
|
||||
spec_set=['all', 'add', 'filter']
|
||||
))
|
||||
self.patches.append(patch)
|
||||
patch.start()
|
||||
@ -328,7 +329,7 @@ class TestIsolatedExecution(TestJobExecution):
|
||||
}
|
||||
)
|
||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||
self.instance.credential = credential
|
||||
self.instance.credentials.add(credential)
|
||||
|
||||
private_data = tempfile.mkdtemp(prefix='awx_')
|
||||
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
|
||||
@ -370,7 +371,7 @@ class TestIsolatedExecution(TestJobExecution):
|
||||
credential_type=ssh,
|
||||
inputs = {'username': 'bob',}
|
||||
)
|
||||
self.instance.credential = credential
|
||||
self.instance.credentials.add(credential)
|
||||
|
||||
private_data = tempfile.mkdtemp(prefix='awx_')
|
||||
self.task.build_private_data_dir = mock.Mock(return_value=private_data)
|
||||
@ -414,7 +415,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
inputs = {'username': 'bob', field: 'secret'}
|
||||
)
|
||||
credential.inputs[field] = encrypt_field(credential, field)
|
||||
self.instance.credential = credential
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -434,7 +435,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
inputs={'vault_password': 'vault-me'}
|
||||
)
|
||||
credential.inputs['vault_password'] = encrypt_field(credential, 'vault_password')
|
||||
self.instance.vault_credential = credential
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
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')
|
||||
self.instance.credential = credential
|
||||
self.instance.credentials.add(credential)
|
||||
|
||||
def run_pexpect_side_effect(private_data, *args, **kwargs):
|
||||
args, cwd, env, stdout = args
|
||||
@ -485,7 +486,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
inputs = {'username': 'bob', 'password': 'secret'}
|
||||
)
|
||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -505,7 +506,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
)
|
||||
for key in ('password', 'security_token'):
|
||||
credential.inputs[key] = encrypt_field(credential, key)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
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')
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
|
||||
def run_pexpect_side_effect(*args, **kwargs):
|
||||
args, cwd, env, stdout = args
|
||||
@ -554,7 +555,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
}
|
||||
)
|
||||
credential.inputs['secret'] = encrypt_field(credential, 'secret')
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
|
||||
self.task.run(self.pk)
|
||||
|
||||
@ -579,7 +580,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
}
|
||||
)
|
||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
|
||||
self.task.run(self.pk)
|
||||
|
||||
@ -599,7 +600,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
|
||||
)
|
||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -623,7 +624,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
}
|
||||
)
|
||||
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):
|
||||
args, cwd, env, stdout = args
|
||||
@ -658,7 +659,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
)
|
||||
for field in ('password', 'ssh_key_data', 'authorize_password'):
|
||||
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):
|
||||
args, cwd, env, stdout = args
|
||||
@ -695,7 +696,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
credential_type=some_cloud,
|
||||
inputs = {'api_token': 'ABC123'}
|
||||
)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
with pytest.raises(Exception):
|
||||
self.task.run(self.pk)
|
||||
|
||||
@ -722,7 +723,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
credential_type=some_cloud,
|
||||
inputs = {'api_token': 'ABC123'}
|
||||
)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -754,7 +755,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
credential_type=some_cloud,
|
||||
inputs={'turbo_button': True}
|
||||
)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -785,7 +786,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
credential_type=some_cloud,
|
||||
inputs = {'api_token': 'ABC123'}
|
||||
)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -819,7 +820,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
inputs = {'password': 'SUPER-SECRET-123'}
|
||||
)
|
||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -852,7 +853,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
credential_type=some_cloud,
|
||||
inputs = {'api_token': 'ABC123'}
|
||||
)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -884,7 +885,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
credential_type=some_cloud,
|
||||
inputs={'turbo_button': True}
|
||||
)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -915,7 +916,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
credential_type=some_cloud,
|
||||
inputs={'turbo_button': True}
|
||||
)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -951,7 +952,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
inputs = {'password': 'SUPER-SECRET-123'}
|
||||
)
|
||||
credential.inputs['password'] = encrypt_field(credential, 'password')
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
assert self.run_pexpect.call_count == 1
|
||||
@ -987,7 +988,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
credential_type=some_cloud,
|
||||
inputs = {'api_token': 'ABC123'}
|
||||
)
|
||||
self.instance.extra_credentials.add(credential)
|
||||
self.instance.credentials.add(credential)
|
||||
self.task.run(self.pk)
|
||||
|
||||
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')
|
||||
self.instance.extra_credentials.add(gce_credential)
|
||||
self.instance.credentials.add(gce_credential)
|
||||
|
||||
azure_rm = CredentialType.defaults['azure_rm']()
|
||||
azure_rm_credential = Credential(
|
||||
@ -1023,7 +1024,7 @@ class TestJobCredentials(TestJobExecution):
|
||||
}
|
||||
)
|
||||
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):
|
||||
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(
|
||||
inventory=inventory,
|
||||
project=project,
|
||||
credential=next(credential_gen),
|
||||
created_by=next(creator_gen),
|
||||
modified_by=next(modifier_gen),
|
||||
playbook="debug.yml",
|
||||
**extra_kwargs)
|
||||
)
|
||||
job_template.credentials.add(next(credential_gen))
|
||||
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
|
||||
job_template.extra_credentials.add(next(credential_gen))
|
||||
job_template.credentials.add(next(credential_gen))
|
||||
job_template._is_new = _
|
||||
job_templates.append(job_template)
|
||||
inv_idx += 1
|
||||
@ -649,10 +649,9 @@ def make_the_data():
|
||||
job_template=job_template,
|
||||
status=job_stat, name="%s-%d" % (job_template.name, job_i),
|
||||
project=job_template.project, inventory=job_template.inventory,
|
||||
credential=job_template.credential,
|
||||
)
|
||||
for ec in job_template.extra_credentials.all():
|
||||
job.extra_credentials.add(ec)
|
||||
for ec in job_template.credentials.all():
|
||||
job.credentials.add(ec)
|
||||
job._is_new = _
|
||||
jobs.append(job)
|
||||
job_i += 1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user