mirror of
https://github.com/ansible/awx.git
synced 2026-02-12 23:24:48 -03:30
Merge pull request #6232 from ryanpetrello/jt_promptable_extra_creds
add prompting for JT.extra_credentials
This commit is contained in:
@@ -2184,7 +2184,7 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
|
|||||||
res['cloud_credential'] = self.reverse('api:credential_detail', kwargs={'pk': cloud_cred})
|
res['cloud_credential'] = self.reverse('api:credential_detail', kwargs={'pk': cloud_cred})
|
||||||
net_cred = obj.network_credential
|
net_cred = obj.network_credential
|
||||||
if net_cred:
|
if net_cred:
|
||||||
res['cloud_credential'] = self.reverse('api:credential_detail', kwargs={'pk': net_cred})
|
res['network_credential'] = self.reverse('api:credential_detail', kwargs={'pk': net_cred})
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -2302,7 +2302,15 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO
|
|||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
fields = ('*', 'host_config_key', 'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch',
|
fields = ('*', 'host_config_key', 'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch',
|
||||||
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch', 'ask_inventory_on_launch',
|
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch', 'ask_inventory_on_launch',
|
||||||
'ask_credential_on_launch', 'survey_enabled', 'become_enabled', 'allow_simultaneous')
|
'ask_credential_on_launch', 'ask_extra_credentials_on_launch', 'survey_enabled', 'become_enabled',
|
||||||
|
'allow_simultaneous')
|
||||||
|
|
||||||
|
# TODO: remove in 3.3
|
||||||
|
def get_fields(self):
|
||||||
|
ret = super(JobTemplateSerializer, self).get_fields()
|
||||||
|
if self.version == 1:
|
||||||
|
ret.pop('ask_extra_credentials_on_launch')
|
||||||
|
return ret
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
res = super(JobTemplateSerializer, self).get_related(obj)
|
res = super(JobTemplateSerializer, self).get_related(obj)
|
||||||
@@ -2972,18 +2980,19 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
model = JobTemplate
|
model = JobTemplate
|
||||||
fields = ('can_start_without_user_input', 'passwords_needed_to_start',
|
fields = ('can_start_without_user_input', 'passwords_needed_to_start',
|
||||||
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
|
'extra_vars', 'limit', 'job_tags', 'skip_tags', 'job_type', 'inventory',
|
||||||
'credential', 'ask_variables_on_launch', 'ask_tags_on_launch',
|
'credential', 'extra_credentials', 'ask_variables_on_launch', 'ask_tags_on_launch',
|
||||||
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_limit_on_launch',
|
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_limit_on_launch',
|
||||||
'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch',
|
'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch',
|
||||||
'survey_enabled', 'variables_needed_to_start',
|
'ask_extra_credentials_on_launch', 'survey_enabled', 'variables_needed_to_start',
|
||||||
'credential_needed_to_start', 'inventory_needed_to_start',
|
'credential_needed_to_start', 'inventory_needed_to_start',
|
||||||
'job_template_data', 'defaults')
|
'job_template_data', 'defaults')
|
||||||
read_only_fields = (
|
read_only_fields = (
|
||||||
'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch',
|
'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch',
|
||||||
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch',
|
'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch',
|
||||||
'ask_inventory_on_launch', 'ask_credential_on_launch')
|
'ask_inventory_on_launch', 'ask_credential_on_launch', 'ask_extra_credentials_on_launch')
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'credential': {'write_only': True,},
|
'credential': {'write_only': True,},
|
||||||
|
'extra_credentials': {'write_only': True, 'default': [], 'allow_empty': True},
|
||||||
'limit': {'write_only': True,},
|
'limit': {'write_only': True,},
|
||||||
'job_tags': {'write_only': True,},
|
'job_tags': {'write_only': True,},
|
||||||
'skip_tags': {'write_only': True,},
|
'skip_tags': {'write_only': True,},
|
||||||
@@ -2991,6 +3000,14 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
'inventory': {'write_only': True,}
|
'inventory': {'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')
|
||||||
|
ret.pop('ask_extra_credentials_on_launch')
|
||||||
|
return ret
|
||||||
|
|
||||||
def get_credential_needed_to_start(self, obj):
|
def get_credential_needed_to_start(self, obj):
|
||||||
return not (obj and obj.credential)
|
return not (obj and obj.credential)
|
||||||
|
|
||||||
@@ -3010,6 +3027,9 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
defaults_dict[field] = dict(
|
defaults_dict[field] = dict(
|
||||||
name=getattrd(obj, '%s.name' % field, None),
|
name=getattrd(obj, '%s.name' % field, None),
|
||||||
id=getattrd(obj, '%s.pk' % field, None))
|
id=getattrd(obj, '%s.pk' % field, None))
|
||||||
|
elif field == 'extra_credentials':
|
||||||
|
if self.version > 1:
|
||||||
|
defaults_dict[field] = [cred.id for cred in obj.extra_credentials.all()]
|
||||||
else:
|
else:
|
||||||
defaults_dict[field] = getattr(obj, field)
|
defaults_dict[field] = getattr(obj, field)
|
||||||
return defaults_dict
|
return defaults_dict
|
||||||
@@ -3060,6 +3080,15 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
if validation_errors:
|
if validation_errors:
|
||||||
errors['variables_needed_to_start'] = validation_errors
|
errors['variables_needed_to_start'] = validation_errors
|
||||||
|
|
||||||
|
extra_cred_kinds = []
|
||||||
|
for cred in data.get('extra_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)
|
||||||
|
|
||||||
# Special prohibited cases for scan jobs
|
# Special prohibited cases for scan jobs
|
||||||
errors.update(obj._extra_job_type_errors(data))
|
errors.update(obj._extra_job_type_errors(data))
|
||||||
|
|
||||||
@@ -3073,6 +3102,7 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
JT_skip_tags = obj.skip_tags
|
JT_skip_tags = obj.skip_tags
|
||||||
JT_inventory = obj.inventory
|
JT_inventory = obj.inventory
|
||||||
JT_credential = obj.credential
|
JT_credential = obj.credential
|
||||||
|
extra_credentials = attrs.pop('extra_credentials', None)
|
||||||
attrs = super(JobLaunchSerializer, self).validate(attrs)
|
attrs = super(JobLaunchSerializer, self).validate(attrs)
|
||||||
obj.extra_vars = JT_extra_vars
|
obj.extra_vars = JT_extra_vars
|
||||||
obj.limit = JT_limit
|
obj.limit = JT_limit
|
||||||
@@ -3081,6 +3111,8 @@ class JobLaunchSerializer(BaseSerializer):
|
|||||||
obj.job_tags = JT_job_tags
|
obj.job_tags = JT_job_tags
|
||||||
obj.inventory = JT_inventory
|
obj.inventory = JT_inventory
|
||||||
obj.credential = JT_credential
|
obj.credential = JT_credential
|
||||||
|
if extra_credentials is not None:
|
||||||
|
attrs['extra_credentials'] = extra_credentials
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2526,29 +2526,41 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
|
|||||||
data['extra_vars'] = extra_vars
|
data['extra_vars'] = extra_vars
|
||||||
ask_for_vars_dict = obj._ask_for_vars_dict()
|
ask_for_vars_dict = obj._ask_for_vars_dict()
|
||||||
ask_for_vars_dict.pop('extra_vars')
|
ask_for_vars_dict.pop('extra_vars')
|
||||||
|
if get_request_version(self.request) == 1: # TODO: remove in 3.3
|
||||||
|
ask_for_vars_dict.pop('extra_credentials')
|
||||||
for field in ask_for_vars_dict:
|
for field in ask_for_vars_dict:
|
||||||
if not ask_for_vars_dict[field]:
|
if not ask_for_vars_dict[field]:
|
||||||
data.pop(field, None)
|
data.pop(field, None)
|
||||||
elif field == 'inventory' or field == 'credential':
|
elif field == 'inventory' or field == 'credential':
|
||||||
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
|
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
|
||||||
|
elif field == 'extra_credentials':
|
||||||
|
data[field] = [cred.id for cred in obj.extra_credentials.all()]
|
||||||
else:
|
else:
|
||||||
data[field] = getattr(obj, field)
|
data[field] = getattr(obj, field)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
obj = self.get_object()
|
obj = self.get_object()
|
||||||
|
ignored_fields = {}
|
||||||
|
|
||||||
if 'credential' not in request.data and 'credential_id' in request.data:
|
if 'credential' not in request.data and 'credential_id' in request.data:
|
||||||
request.data['credential'] = request.data['credential_id']
|
request.data['credential'] = request.data['credential_id']
|
||||||
if 'inventory' not in request.data and 'inventory_id' in request.data:
|
if 'inventory' not in request.data and 'inventory_id' in request.data:
|
||||||
request.data['inventory'] = request.data['inventory_id']
|
request.data['inventory'] = request.data['inventory_id']
|
||||||
|
|
||||||
|
if get_request_version(self.request) == 1: # TODO: remove in 3.3
|
||||||
|
extra_creds = request.data.pop('extra_credentials', None)
|
||||||
|
if extra_creds is not None:
|
||||||
|
ignored_fields['extra_credentials'] = extra_creds
|
||||||
|
|
||||||
passwords = {}
|
passwords = {}
|
||||||
serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
|
serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
|
||||||
if not serializer.is_valid():
|
if not serializer.is_valid():
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
prompted_fields, ignored_fields = obj._accept_or_ignore_job_kwargs(**request.data)
|
_accepted_or_ignored = obj._accept_or_ignore_job_kwargs(**request.data)
|
||||||
|
prompted_fields = _accepted_or_ignored[0]
|
||||||
|
ignored_fields.update(_accepted_or_ignored[1])
|
||||||
|
|
||||||
if 'credential' in prompted_fields and prompted_fields['credential'] != getattrd(obj, 'credential.pk', None):
|
if 'credential' in prompted_fields and prompted_fields['credential'] != getattrd(obj, 'credential.pk', None):
|
||||||
new_credential = get_object_or_400(Credential, pk=get_pk_from_dict(prompted_fields, 'credential'))
|
new_credential = get_object_or_400(Credential, pk=get_pk_from_dict(prompted_fields, 'credential'))
|
||||||
@@ -2560,6 +2572,11 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
|
|||||||
if request.user not in new_inventory.use_role:
|
if request.user not in new_inventory.use_role:
|
||||||
raise PermissionDenied()
|
raise PermissionDenied()
|
||||||
|
|
||||||
|
for cred in prompted_fields.get('extra_credentials', []):
|
||||||
|
new_credential = get_object_or_400(Credential, pk=cred)
|
||||||
|
if request.user not in new_credential.use_role:
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
new_job = obj.create_unified_job(**prompted_fields)
|
new_job = obj.create_unified_job(**prompted_fields)
|
||||||
result = new_job.signal_start(**passwords)
|
result = new_job.signal_start(**passwords)
|
||||||
|
|
||||||
|
|||||||
@@ -1192,7 +1192,8 @@ class JobTemplateAccess(BaseAccess):
|
|||||||
'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars',
|
'name', 'description', 'forks', 'limit', 'verbosity', 'extra_vars',
|
||||||
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
|
'job_tags', 'force_handlers', 'skip_tags', 'ask_variables_on_launch',
|
||||||
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_skip_tags_on_launch',
|
'ask_tags_on_launch', 'ask_job_type_on_launch', 'ask_skip_tags_on_launch',
|
||||||
'ask_inventory_on_launch', 'ask_credential_on_launch', 'survey_enabled',
|
'ask_inventory_on_launch', 'ask_credential_on_launch',
|
||||||
|
'ask_extra_credentials_on_launch', 'survey_enabled',
|
||||||
|
|
||||||
# These fields are ignored, but it is convenient for QA to allow clients to post them
|
# These fields are ignored, but it is convenient for QA to allow clients to post them
|
||||||
'last_job_run', 'created', 'modified',
|
'last_job_run', 'created', 'modified',
|
||||||
@@ -1352,7 +1353,11 @@ class JobAccess(BaseAccess):
|
|||||||
job_fields[fd] = getattr(obj, fd)
|
job_fields[fd] = getattr(obj, fd)
|
||||||
accepted_fields, ignored_fields = obj.job_template._accept_or_ignore_job_kwargs(**job_fields)
|
accepted_fields, ignored_fields = obj.job_template._accept_or_ignore_job_kwargs(**job_fields)
|
||||||
for fd in ignored_fields:
|
for fd in ignored_fields:
|
||||||
if fd != 'extra_vars' and job_fields[fd] != getattr(obj.job_template, fd):
|
if fd == 'extra_credentials':
|
||||||
|
if set(job_fields[fd].all()) != set(getattr(obj.job_template, fd).all()):
|
||||||
|
# Job has field that is not promptable
|
||||||
|
prompts_access = False
|
||||||
|
elif fd != 'extra_vars' and job_fields[fd] != getattr(obj.job_template, fd):
|
||||||
# Job has field that is not promptable
|
# Job has field that is not promptable
|
||||||
prompts_access = False
|
prompts_access = False
|
||||||
if obj.credential != obj.job_template.credential and not credential_access:
|
if obj.credential != obj.job_template.credential and not credential_access:
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from awx.main.migrations import _credentialtypes as credentialtypes
|
from awx.main.migrations import _credentialtypes as credentialtypes
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -23,6 +22,11 @@ class Migration(migrations.Migration):
|
|||||||
name='extra_credentials',
|
name='extra_credentials',
|
||||||
field=models.ManyToManyField(related_name='_jobtemplate_extra_credentials_+', to='main.Credential'),
|
field=models.ManyToManyField(related_name='_jobtemplate_extra_credentials_+', to='main.Credential'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobtemplate',
|
||||||
|
name='ask_extra_credentials_on_launch',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
migrations.RunPython(credentialtypes.migrate_job_credentials),
|
migrations.RunPython(credentialtypes.migrate_job_credentials),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='job',
|
model_name='job',
|
||||||
|
|||||||
@@ -169,16 +169,6 @@ class JobOptions(BaseModel):
|
|||||||
)
|
)
|
||||||
return cred
|
return cred
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super(JobOptions, self).clean()
|
|
||||||
# extra_credentials M2M can't be accessed until a primary key exists
|
|
||||||
if self.pk:
|
|
||||||
for cred in self.extra_credentials.all():
|
|
||||||
if cred.credential_type.kind not in ('net', 'cloud'):
|
|
||||||
raise ValidationError(
|
|
||||||
_('Extra credentials must be network or cloud.'),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_credentials(self):
|
def all_credentials(self):
|
||||||
credentials = list(self.extra_credentials.all())
|
credentials = list(self.extra_credentials.all())
|
||||||
@@ -269,6 +259,10 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
blank=True,
|
blank=True,
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
ask_extra_credentials_on_launch = models.BooleanField(
|
||||||
|
blank=True,
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
admin_role = ImplicitRoleField(
|
admin_role = ImplicitRoleField(
|
||||||
parent_role=['project.organization.admin_role', 'inventory.organization.admin_role']
|
parent_role=['project.organization.admin_role', 'inventory.organization.admin_role']
|
||||||
)
|
)
|
||||||
@@ -369,7 +363,8 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
|||||||
job_type=self.ask_job_type_on_launch,
|
job_type=self.ask_job_type_on_launch,
|
||||||
verbosity=self.ask_verbosity_on_launch,
|
verbosity=self.ask_verbosity_on_launch,
|
||||||
inventory=self.ask_inventory_on_launch,
|
inventory=self.ask_inventory_on_launch,
|
||||||
credential=self.ask_credential_on_launch
|
credential=self.ask_credential_on_launch,
|
||||||
|
extra_credentials=self.ask_extra_credentials_on_launch
|
||||||
)
|
)
|
||||||
|
|
||||||
def _accept_or_ignore_job_kwargs(self, **kwargs):
|
def _accept_or_ignore_job_kwargs(self, **kwargs):
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
|||||||
unified_job_class = self._get_unified_job_class()
|
unified_job_class = self._get_unified_job_class()
|
||||||
fields = self._get_unified_job_field_names()
|
fields = self._get_unified_job_field_names()
|
||||||
unified_job = copy_model_by_class(self, unified_job_class, fields, kwargs)
|
unified_job = copy_model_by_class(self, unified_job_class, fields, kwargs)
|
||||||
|
|
||||||
eager_fields = kwargs.get('_eager_fields', None)
|
eager_fields = kwargs.get('_eager_fields', None)
|
||||||
if eager_fields:
|
if eager_fields:
|
||||||
for fd, val in eager_fields.items():
|
for fd, val in eager_fields.items():
|
||||||
@@ -351,7 +352,8 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, Notificatio
|
|||||||
unified_job.survey_passwords = hide_password_dict
|
unified_job.survey_passwords = hide_password_dict
|
||||||
|
|
||||||
unified_job.save()
|
unified_job.save()
|
||||||
# Labels coppied here
|
|
||||||
|
# Labels and extra credentials copied here
|
||||||
copy_m2m_relationships(self, unified_job, fields, kwargs=kwargs)
|
copy_m2m_relationships(self, unified_job, fields, kwargs=kwargs)
|
||||||
return unified_job
|
return unified_job
|
||||||
|
|
||||||
|
|||||||
@@ -329,6 +329,70 @@ def test_job_launch_JT_with_validation(machine_credential, deploy_jobtemplate):
|
|||||||
assert job_obj.credential.id == machine_credential.id
|
assert job_obj.credential.id == 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):
|
||||||
|
deploy_jobtemplate.ask_extra_credentials_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
|
||||||
|
def test_job_launch_JT_with_extra_credentials(machine_credential, credential, net_credential, deploy_jobtemplate):
|
||||||
|
deploy_jobtemplate.ask_extra_credentials_on_launch = True
|
||||||
|
deploy_jobtemplate.save()
|
||||||
|
|
||||||
|
kv = dict(extra_credentials=[credential.pk, net_credential.pk], credential=machine_credential.id)
|
||||||
|
serializer = JobLaunchSerializer(
|
||||||
|
instance=deploy_jobtemplate, data=kv,
|
||||||
|
context={'obj': deploy_jobtemplate, 'data': kv, 'passwords': {}})
|
||||||
|
validated = serializer.is_valid()
|
||||||
|
assert validated
|
||||||
|
|
||||||
|
prompted_fields, ignored_fields = deploy_jobtemplate._accept_or_ignore_job_kwargs(**kv)
|
||||||
|
job_obj = deploy_jobtemplate.create_unified_job(**prompted_fields)
|
||||||
|
|
||||||
|
extra_creds = job_obj.extra_credentials.all()
|
||||||
|
assert len(extra_creds) == 2
|
||||||
|
assert credential in extra_creds
|
||||||
|
assert net_credential in extra_creds
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@pytest.mark.job_runtime_vars
|
@pytest.mark.job_runtime_vars
|
||||||
def test_job_launch_unprompted_vars_with_survey(mocker, survey_spec_factory, job_template_prompts, post, admin_user):
|
def test_job_launch_unprompted_vars_with_survey(mocker, survey_spec_factory, job_template_prompts, post, admin_user):
|
||||||
|
|||||||
@@ -452,6 +452,145 @@ def test_scan_jt_surveys(inventory):
|
|||||||
assert "survey_enabled" in serializer.errors
|
assert "survey_enabled" in serializer.errors
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_launch_with_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_extra_credentials_on_launch = True
|
||||||
|
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]
|
||||||
|
),
|
||||||
|
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') == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_launch_with_extra_credentials_no_allowed(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_extra_credentials_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]
|
||||||
|
),
|
||||||
|
objs.superusers.admin, expect=201
|
||||||
|
)
|
||||||
|
assert 'extra_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_extra_credentials_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_extra_credentials_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,
|
||||||
|
credential, net_credential):
|
||||||
|
# launch requests to `/api/v1/job_templates/N/launch/` should ignore
|
||||||
|
# `extra_credentials`, as they're only supported in v2 of the API.
|
||||||
|
objs = organization_factory("org", superusers=['admin'])
|
||||||
|
jt = job_template_factory("jt", organization=objs.organization,
|
||||||
|
inventory='test_inv', project='test_proj').job_template
|
||||||
|
jt.ask_extra_credentials_on_launch = True
|
||||||
|
jt.save()
|
||||||
|
|
||||||
|
resp = post(
|
||||||
|
reverse('api:job_template_launch', kwargs={'pk': jt.pk, 'version': 'v1'}),
|
||||||
|
dict(
|
||||||
|
credential=machine_credential.pk,
|
||||||
|
extra_credentials=[credential.pk, net_credential.pk]
|
||||||
|
),
|
||||||
|
objs.superusers.admin, expect=201
|
||||||
|
)
|
||||||
|
job_pk = resp.data.get('id')
|
||||||
|
assert resp.data.get('ignored_fields').keys() == ['extra_credentials']
|
||||||
|
|
||||||
|
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') == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_jt_without_project(inventory):
|
def test_jt_without_project(inventory):
|
||||||
data = dict(name="Test", job_type="run",
|
data = dict(name="Test", job_type="run",
|
||||||
|
|||||||
@@ -45,16 +45,3 @@ def test_clean_credential_with_custom_types(credentialtype_aws, credentialtype_n
|
|||||||
job_template.extra_credentials.add(aws)
|
job_template.extra_credentials.add(aws)
|
||||||
job_template.extra_credentials.add(net)
|
job_template.extra_credentials.add(net)
|
||||||
job_template.full_clean()
|
job_template.full_clean()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_clean_credential_with_custom_types_xfail(credentialtype_ssh, job_template):
|
|
||||||
ssh = Credential(
|
|
||||||
name='SSH Credential',
|
|
||||||
credential_type=credentialtype_ssh
|
|
||||||
)
|
|
||||||
ssh.save()
|
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
|
||||||
job_template.extra_credentials.add(ssh)
|
|
||||||
job_template.full_clean()
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import six
|
|||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.db.models import ManyToManyField
|
from django.db.models.fields.related import ForeignObjectRel, ManyToManyField
|
||||||
|
|
||||||
# Django REST Framework
|
# Django REST Framework
|
||||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||||
@@ -459,9 +459,7 @@ def copy_model_by_class(obj1, Class2, fields, kwargs):
|
|||||||
elif field_name in kwargs:
|
elif field_name in kwargs:
|
||||||
if field_name == 'extra_vars' and isinstance(kwargs[field_name], dict):
|
if field_name == 'extra_vars' and isinstance(kwargs[field_name], dict):
|
||||||
create_kwargs[field_name] = json.dumps(kwargs['extra_vars'])
|
create_kwargs[field_name] = json.dumps(kwargs['extra_vars'])
|
||||||
# We can't get a hold of django.db.models.fields.related.ManyRelatedManager to compare
|
elif not isinstance(Class2._meta.get_field(field_name), (ForeignObjectRel, ManyToManyField)):
|
||||||
# so this is the next best thing.
|
|
||||||
elif kwargs[field_name].__class__.__name__ is not 'ManyRelatedManager':
|
|
||||||
create_kwargs[field_name] = kwargs[field_name]
|
create_kwargs[field_name] = kwargs[field_name]
|
||||||
elif hasattr(obj1, field_name):
|
elif hasattr(obj1, field_name):
|
||||||
field_obj = obj1._meta.get_field_by_name(field_name)[0]
|
field_obj = obj1._meta.get_field_by_name(field_name)[0]
|
||||||
@@ -491,6 +489,9 @@ def copy_m2m_relationships(obj1, obj2, fields, kwargs=None):
|
|||||||
src_field_value = getattr(obj1, field_name)
|
src_field_value = getattr(obj1, field_name)
|
||||||
if kwargs and field_name in kwargs:
|
if kwargs and field_name in kwargs:
|
||||||
override_field_val = kwargs[field_name]
|
override_field_val = kwargs[field_name]
|
||||||
|
if isinstance(override_field_val, list):
|
||||||
|
getattr(obj2, field_name).add(*override_field_val)
|
||||||
|
continue
|
||||||
if override_field_val.__class__.__name__ is 'ManyRelatedManager':
|
if override_field_val.__class__.__name__ is 'ManyRelatedManager':
|
||||||
src_field_value = override_field_val
|
src_field_value = override_field_val
|
||||||
dest_field = getattr(obj2, field_name)
|
dest_field = getattr(obj2, field_name)
|
||||||
|
|||||||
@@ -14,3 +14,18 @@
|
|||||||
[[#5741](https://github.com/ansible/ansible-tower/issues/5741)]
|
[[#5741](https://github.com/ansible/ansible-tower/issues/5741)]
|
||||||
* support sourcing inventory from a file inside of a project's source
|
* support sourcing inventory from a file inside of a project's source
|
||||||
tree [[#2477](https://github.com/ansible/ansible-tower/issues/2477)]
|
tree [[#2477](https://github.com/ansible/ansible-tower/issues/2477)]
|
||||||
|
* added support for custom cloud and network credential types, which give the
|
||||||
|
customer the ability to modify environment variables, extra vars, and
|
||||||
|
generate file-based credentials (such as file-based certificates or .ini
|
||||||
|
files) at `ansible-playbook` runtime
|
||||||
|
[[#5876](https://github.com/ansible/ansible-tower/issues/5876)]
|
||||||
|
* added support for assigning multiple cloud and network credential types on
|
||||||
|
`JobTemplates`. ``JobTemplates`` can prompt for "extra credentials" at
|
||||||
|
launch time in the same manner as promptable machine credentials
|
||||||
|
[[#5807](https://github.com/ansible/ansible-tower/issues/5807)]
|
||||||
|
[[#2913](https://github.com/ansible/ansible-tower/issues/2913)]
|
||||||
|
* custom inventory sources can now specify a ``Credential``; you
|
||||||
|
can store third-party credentials encrypted within Tower and use their
|
||||||
|
values from within your custom inventory script (by - for example - reading
|
||||||
|
an environment variable or a file's contents)
|
||||||
|
[[#5879](https://github.com/ansible/ansible-tower/issues/5879)]
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ Important Changes
|
|||||||
Engine credential. You cannot, however, create a ``Job Template`` that uses
|
Engine credential. You cannot, however, create a ``Job Template`` that uses
|
||||||
two OpenStack credentials.
|
two OpenStack credentials.
|
||||||
|
|
||||||
|
* In the same manner as "promptable SSH credentials", ``Job Templates`` can now
|
||||||
|
be flagged with ``ask_extra_credentials_on_launch = true``. When this flag
|
||||||
|
is enabled, ``extra_credentials`` for a ``Job Template`` can be specified in
|
||||||
|
the launch payload.
|
||||||
|
|
||||||
* Custom inventory sources can now utilize a ``Credential``; you
|
* Custom inventory sources can now utilize a ``Credential``; you
|
||||||
can store third-party credentials encrypted within Tower and use their
|
can store third-party credentials encrypted within Tower and use their
|
||||||
values from within your custom inventory script (by - for example - reading
|
values from within your custom inventory script (by - for example - reading
|
||||||
|
|||||||
Reference in New Issue
Block a user