mirror of
https://github.com/ZwareBear/awx.git
synced 2026-05-04 07:51:58 -05:00
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:
@@ -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']}
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user