Replace Job/JT cloud/network credentials with a single M2M relation.

The following fields:

    * (Job | JobTemplate).cloud_credential
    * (Job | JobTemplate).network_credential

...are replaced by M2M relationships:

    * Job.extra_credentials
    * JobTemplate.extra_credentials

Includes support for task execution with multiple cloud credentials.

see: #5807
This commit is contained in:
Ryan Petrello
2017-04-27 11:26:35 -04:00
parent 465d620629
commit accf7cdea2
12 changed files with 463 additions and 395 deletions

View File

@@ -86,8 +86,6 @@ SUMMARIZABLE_FK_FIELDS = {
'scm_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'), 'scm_project': DEFAULT_SUMMARY_FIELDS + ('status', 'scm_type'),
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',), 'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'), 'credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
'cloud_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'cloud'),
'network_credential': DEFAULT_SUMMARY_FIELDS + ('kind', 'net'),
'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed'), 'job': DEFAULT_SUMMARY_FIELDS + ('status', 'failed', 'elapsed'),
'job_template': DEFAULT_SUMMARY_FIELDS, 'job_template': DEFAULT_SUMMARY_FIELDS,
'workflow_job_template': DEFAULT_SUMMARY_FIELDS, 'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
@@ -2096,7 +2094,7 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
class Meta: class Meta:
fields = ('*', 'job_type', 'inventory', 'project', 'playbook', fields = ('*', 'job_type', 'inventory', 'project', 'playbook',
'credential', 'cloud_credential', 'network_credential', 'forks', 'limit', 'credential', 'forks', 'limit',
'verbosity', 'extra_vars', 'job_tags', 'force_handlers', 'verbosity', 'extra_vars', 'job_tags', 'force_handlers',
'skip_tags', 'start_at_task', 'timeout', 'store_facts',) 'skip_tags', 'start_at_task', 'timeout', 'store_facts',)
@@ -2109,12 +2107,7 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk}) res['project'] = self.reverse('api:project_detail', kwargs={'pk': obj.project.pk})
if obj.credential: if obj.credential:
res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential.pk}) res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential.pk})
if obj.cloud_credential: # TODO: add related links for `extra_credentials`
res['cloud_credential'] = self.reverse('api:credential_detail',
kwargs={'pk': obj.cloud_credential.pk})
if obj.network_credential:
res['network_credential'] = self.reverse('api:credential_detail',
kwargs={'pk': obj.network_credential.pk})
return res return res
def to_representation(self, obj): def to_representation(self, obj):
@@ -2129,10 +2122,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
ret['playbook'] = '' ret['playbook'] = ''
if 'credential' in ret and not obj.credential: if 'credential' in ret and not obj.credential:
ret['credential'] = None ret['credential'] = None
if 'cloud_credential' in ret and not obj.cloud_credential:
ret['cloud_credential'] = None
if 'network_credential' in ret and not obj.network_credential:
ret['network_credential'] = None
return ret return ret
def validate(self, attrs): def validate(self, attrs):
@@ -2296,10 +2285,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
data.setdefault('playbook', job_template.playbook) data.setdefault('playbook', job_template.playbook)
if job_template.credential: if job_template.credential:
data.setdefault('credential', job_template.credential.pk) data.setdefault('credential', job_template.credential.pk)
if job_template.cloud_credential:
data.setdefault('cloud_credential', job_template.cloud_credential.pk)
if job_template.network_credential:
data.setdefault('network_credential', job_template.network_credential.pk)
data.setdefault('forks', job_template.forks) data.setdefault('forks', job_template.forks)
data.setdefault('limit', job_template.limit) data.setdefault('limit', job_template.limit)
data.setdefault('verbosity', job_template.verbosity) data.setdefault('verbosity', job_template.verbosity)

View File

@@ -2432,7 +2432,7 @@ class JobTemplateList(ListCreateAPIView):
always_allow_superuser = False always_allow_superuser = False
capabilities_prefetch = [ capabilities_prefetch = [
'admin', 'execute', 'admin', 'execute',
{'copy': ['project.use', 'inventory.use', 'credential.use', 'cloud_credential.use', 'network_credential.use']} {'copy': ['project.use', 'inventory.use', 'credential.use']}
] ]
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
@@ -3906,7 +3906,6 @@ class UnifiedJobTemplateList(ListAPIView):
capabilities_prefetch = [ capabilities_prefetch = [
'admin', 'execute', 'admin', 'execute',
{'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use', 'jobtemplate.credential.use', {'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use', 'jobtemplate.credential.use',
'jobtemplate.cloud_credential.use', 'jobtemplate.network_credential.use',
'workflowjobtemplate.organization.admin']} 'workflowjobtemplate.organization.admin']}
] ]

View File

@@ -1072,7 +1072,7 @@ class JobTemplateAccess(BaseAccess):
else: else:
qs = self.model.accessible_objects(self.user, 'read_role') qs = self.model.accessible_objects(self.user, 'read_role')
return qs.select_related('created_by', 'modified_by', 'inventory', 'project', return qs.select_related('created_by', 'modified_by', 'inventory', 'project',
'credential', 'cloud_credential', 'next_schedule').all() 'credential', 'next_schedule').all()
def can_add(self, data): def can_add(self, data):
''' '''
@@ -1113,13 +1113,8 @@ class JobTemplateAccess(BaseAccess):
if not self.check_related('credential', Credential, data, role_field='use_role'): if not self.check_related('credential', Credential, data, role_field='use_role'):
return False return False
# If a cloud credential is provided, the user should have use access. # TODO: If a vault credential is provided, the user should have use access to it.
if not self.check_related('cloud_credential', Credential, data, role_field='use_role'): # TODO: If any credential in extra_credentials, the user must have access
return False
# If a network credential is provided, the user should have use access.
if not self.check_related('network_credential', Credential, data, role_field='use_role'):
return False
# If an inventory is provided, the user should have use access. # If an inventory is provided, the user should have use access.
inventory = get_value(Inventory, 'inventory') inventory = get_value(Inventory, 'inventory')
@@ -1185,7 +1180,8 @@ class JobTemplateAccess(BaseAccess):
self.check_license(feature='surveys') self.check_license(feature='surveys')
return True return True
for required_field in ('credential', 'cloud_credential', 'network_credential', 'inventory', 'project'): # TODO: handle vault_credential and extra_credentials
for required_field in ('credential', 'inventory', 'project'):
required_obj = getattr(obj, required_field, None) required_obj = getattr(obj, required_field, None)
if required_field not in data_for_change and required_obj is not None: if required_field not in data_for_change and required_obj is not None:
data_for_change[required_field] = required_obj.pk data_for_change[required_field] = required_obj.pk
@@ -1219,8 +1215,6 @@ class JobTemplateAccess(BaseAccess):
project_id = data.get('project', obj.project.id if obj.project else None) project_id = data.get('project', obj.project.id if obj.project else None)
inventory_id = data.get('inventory', obj.inventory.id if obj.inventory else None) inventory_id = data.get('inventory', obj.inventory.id if obj.inventory else None)
credential_id = data.get('credential', obj.credential.id if obj.credential else None) credential_id = data.get('credential', obj.credential.id if obj.credential else None)
cloud_credential_id = data.get('cloud_credential', obj.cloud_credential.id if obj.cloud_credential else None)
network_credential_id = data.get('network_credential', obj.network_credential.id if obj.network_credential else None)
if project_id and self.user not in Project.objects.get(pk=project_id).use_role: if project_id and self.user not in Project.objects.get(pk=project_id).use_role:
return False return False
@@ -1228,10 +1222,7 @@ class JobTemplateAccess(BaseAccess):
return False return False
if credential_id and self.user not in Credential.objects.get(pk=credential_id).use_role: if credential_id and self.user not in Credential.objects.get(pk=credential_id).use_role:
return False return False
if cloud_credential_id and self.user not in Credential.objects.get(pk=cloud_credential_id).use_role: # TODO: handle vault_credential and extra_credentials
return False
if network_credential_id and self.user not in Credential.objects.get(pk=network_credential_id).use_role:
return False
return True return True
@@ -1271,7 +1262,7 @@ class JobAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.objects qs = self.model.objects
qs = qs.select_related('created_by', 'modified_by', 'job_template', 'inventory', qs = qs.select_related('created_by', 'modified_by', 'job_template', 'inventory',
'project', 'credential', 'cloud_credential', 'job_template') 'project', 'credential', 'job_template')
qs = qs.prefetch_related('unified_job_template') qs = qs.prefetch_related('unified_job_template')
if self.user.is_superuser or self.user.is_system_auditor: if self.user.is_superuser or self.user.is_system_auditor:
return qs.all() return qs.all()
@@ -1907,7 +1898,6 @@ class UnifiedJobTemplateAccess(BaseAccess):
# 'project', # 'project',
# 'inventory', # 'inventory',
# 'credential', # 'credential',
# 'cloud_credential',
#) #)
return qs.all() return qs.all()
@@ -1957,14 +1947,12 @@ class UnifiedJobAccess(BaseAccess):
# 'credential', # 'credential',
# 'job_template', # 'job_template',
# 'inventory_source', # 'inventory_source',
# 'cloud_credential',
# 'project___credential', # 'project___credential',
# 'inventory_source___credential', # 'inventory_source___credential',
# 'inventory_source___inventory', # 'inventory_source___inventory',
# 'job_template__inventory', # 'job_template__inventory',
# 'job_template__project', # 'job_template__project',
# 'job_template__credential', # 'job_template__credential',
# 'job_template__cloud_credential',
#) #)
return qs.all() return qs.all()

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from awx.main.migrations import _credentialtypes as credentialtypes
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0042_v320_drop_v1_credential_fields'),
]
operations = [
migrations.AddField(
model_name='job',
name='extra_credentials',
field=models.ManyToManyField(related_name='_job_extra_credentials_+', to='main.Credential'),
),
migrations.AddField(
model_name='jobtemplate',
name='extra_credentials',
field=models.ManyToManyField(related_name='_jobtemplate_extra_credentials_+', to='main.Credential'),
),
migrations.RunPython(credentialtypes.migrate_job_credentials),
migrations.RemoveField(
model_name='job',
name='cloud_credential',
),
migrations.RemoveField(
model_name='job',
name='network_credential',
),
migrations.RemoveField(
model_name='jobtemplate',
name='cloud_credential',
),
migrations.RemoveField(
model_name='jobtemplate',
name='network_credential',
),
]

View File

@@ -81,3 +81,21 @@ def migrate_to_v2_credentials(apps, schema_editor):
new_cred.save() new_cred.save()
finally: finally:
utils.get_current_apps = orig_current_apps utils.get_current_apps = orig_current_apps
def migrate_job_credentials(apps, schema_editor):
# this monkey-patch is necessary to make the implicit role generation save
# signal use the correct Role model (the version active at this point in
# migration, not the one at HEAD)
orig_current_apps = utils.get_current_apps
try:
utils.get_current_apps = lambda: apps
for type_ in ('Job', 'JobTemplate'):
for obj in apps.get_model('main', type_).objects.all():
if obj.cloud_credential:
obj.extra_credentials.add(obj.cloud_credential)
if obj.network_credential:
obj.extra_credentials.add(obj.network_credential)
obj.save()
finally:
utils.get_current_apps = orig_current_apps

View File

@@ -96,21 +96,9 @@ class JobOptions(BaseModel):
default=None, default=None,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
) )
cloud_credential = models.ForeignKey( extra_credentials = models.ManyToManyField(
'Credential', 'Credential',
related_name='%(class)ss_as_cloud_credential+', related_name='%(class)ss_as_extra_credential+',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
)
network_credential = models.ForeignKey(
'Credential',
related_name='%(class)ss_as_network_credential+',
blank=True,
null=True,
default=None,
on_delete=models.SET_NULL,
) )
forks = models.PositiveIntegerField( forks = models.PositiveIntegerField(
blank=True, blank=True,
@@ -170,26 +158,44 @@ class JobOptions(BaseModel):
cred = self.credential cred = self.credential
if cred and cred.kind != 'ssh': if cred and cred.kind != 'ssh':
raise ValidationError( raise ValidationError(
_('You must provide a machine / SSH credential.'), _('You must provide an SSH credential.'),
) )
return cred return cred
def clean_network_credential(self): def clean_vault_credential(self):
cred = self.network_credential cred = self.vault_credential
if cred and cred.kind != 'net': if cred and cred.kind != 'vault':
raise ValidationError( raise ValidationError(
_('You must provide a network credential.'), _('You must provide a Vault credential.'),
) )
return cred return cred
def clean_cloud_credential(self): def clean(self):
cred = self.cloud_credential super(JobOptions, self).clean()
if cred and cred.kind not in CLOUD_PROVIDERS + ('aws',): # extra_credentials M2M can't be accessed until a primary key exists
raise ValidationError( if self.pk:
_('Must provide a credential for a cloud provider, such as ' for cred in self.extra_credentials.all():
'Amazon Web Services or Rackspace.'), if cred.credential_type.kind not in ('net', 'cloud'):
) raise ValidationError(
return cred _('Extra credentials must be network or cloud.'),
)
@property
def all_credentials(self):
credentials = 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']
@property
def cloud_credentials(self):
return [cred for cred in self.extra_credentials.all() if cred.credential_type.kind == 'cloud']
@property @property
def passwords_needed_to_start(self): def passwords_needed_to_start(self):
@@ -262,11 +268,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
@classmethod @classmethod
def _get_unified_job_field_names(cls): def _get_unified_job_field_names(cls):
return ['name', 'description', 'job_type', 'inventory', 'project', return ['name', 'description', 'job_type', 'inventory', 'project',
'playbook', 'credential', 'cloud_credential', 'network_credential', 'forks', 'schedule', 'playbook', 'credential', 'extra_credentials', 'forks',
'limit', 'verbosity', 'job_tags', 'extra_vars', 'launch_type', 'schedule', 'limit', 'verbosity', 'job_tags', 'extra_vars',
'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled', 'launch_type', 'force_handlers', 'skip_tags', 'start_at_task',
'labels', 'survey_passwords', 'allow_simultaneous', 'timeout', 'become_enabled', 'labels', 'survey_passwords',
'store_facts',] 'allow_simultaneous', 'timeout', 'store_facts',]
def resource_validation_data(self): def resource_validation_data(self):
''' '''

View File

@@ -397,31 +397,40 @@ class BaseTask(Task):
def build_private_data_files(self, instance, **kwargs): def build_private_data_files(self, instance, **kwargs):
''' '''
Create a temporary files containing the private data. Creates temporary files containing the private data.
Returns a dictionary with keys from build_private_data Returns a dictionary i.e.,
(i.e. 'credential', 'cloud_credential', 'network_credential') and values the file path.
{
'credentials': {
<awx.main.models.Credential>: '/path/to/decrypted/data',
<awx.main.models.Credential>: '/path/to/decrypted/data',
<awx.main.models.Credential>: '/path/to/decrypted/data',
}
}
''' '''
private_data = self.build_private_data(instance, **kwargs) private_data = self.build_private_data(instance, **kwargs)
private_data_files = {} private_data_files = {'credentials': {}}
if private_data is not None: if private_data is not None:
ssh_ver = get_ssh_version() ssh_ver = get_ssh_version()
ssh_too_old = True if ssh_ver == "unknown" else Version(ssh_ver) < Version("6.0") ssh_too_old = True if ssh_ver == "unknown" else Version(ssh_ver) < Version("6.0")
openssh_keys_supported = ssh_ver != "unknown" and Version(ssh_ver) >= Version("6.5") openssh_keys_supported = ssh_ver != "unknown" and Version(ssh_ver) >= Version("6.5")
for name, data in private_data.iteritems(): for credential, data in private_data.get('credentials', {}).iteritems():
# Bail out now if a private key was provided in OpenSSH format # Bail out now if a private key was provided in OpenSSH format
# and we're running an earlier version (<6.5). # and we're running an earlier version (<6.5).
if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported: if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported:
raise RuntimeError(OPENSSH_KEY_ERROR) raise RuntimeError(OPENSSH_KEY_ERROR)
for name, data in private_data.iteritems(): for credential, data in private_data.get('credentials', {}).iteritems():
name = 'credential_%d' % credential.pk
# OpenSSH formatted keys must have a trailing newline to be # OpenSSH formatted keys must have a trailing newline to be
# accepted by ssh-add. # accepted by ssh-add.
if 'OPENSSH PRIVATE KEY' in data and not data.endswith('\n'): if 'OPENSSH PRIVATE KEY' in data and not data.endswith('\n'):
data += '\n' data += '\n'
# For credentials used with ssh-add, write to a named pipe which # For credentials used with ssh-add, write to a named pipe which
# will be read then closed, instead of leaving the SSH key on disk. # will be read then closed, instead of leaving the SSH key on disk.
if name in ('credential', 'scm_credential', 'ad_hoc_credential') and not ssh_too_old: if credential.kind in ('ssh', 'scm') and not ssh_too_old:
path = os.path.join(kwargs.get('private_data_dir', tempfile.gettempdir()), name) path = os.path.join(kwargs.get('private_data_dir', tempfile.gettempdir()), name)
self.open_fifo_write(path, data) self.open_fifo_write(path, data)
private_data_files['credentials']['ssh'] = path
# Ansible network modules do not yet support ssh-agent. # Ansible network modules do not yet support ssh-agent.
# Instead, ssh private key file is explicitly passed via an # Instead, ssh private key file is explicitly passed via an
# env variable. # env variable.
@@ -431,7 +440,7 @@ class BaseTask(Task):
f.write(data) f.write(data)
f.close() f.close()
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
private_data_files[name] = path private_data_files['credentials'][credential] = path
return private_data_files return private_data_files
def open_fifo_write(self, path, data): def open_fifo_write(self, path, data):
@@ -515,12 +524,6 @@ class BaseTask(Task):
def args2cmdline(self, *args): def args2cmdline(self, *args):
return ' '.join([pipes.quote(a) for a in args]) return ' '.join([pipes.quote(a) for a in args])
def get_ssh_key_path(self, instance, **kwargs):
'''
Return the path to the SSH key file, if present.
'''
return ''
def wrap_args_with_ssh_agent(self, args, ssh_key_path, ssh_auth_sock=None): def wrap_args_with_ssh_agent(self, args, ssh_key_path, ssh_auth_sock=None):
if ssh_key_path: if ssh_key_path:
cmd = ' && '.join([self.args2cmdline('ssh-add', ssh_key_path), cmd = ' && '.join([self.args2cmdline('ssh-add', ssh_key_path),
@@ -724,8 +727,11 @@ class BaseTask(Task):
safe_env = self.build_safe_env(env, **kwargs) safe_env = self.build_safe_env(env, **kwargs)
# handle custom injectors specified on the CredentialType # handle custom injectors specified on the CredentialType
for type_ in ('credential', 'cloud_credential', 'network_credential'): if hasattr(instance, 'all_credentials'):
credential = getattr(instance, type_, None) credentials = instance.all_credentials
else:
credentials = [instance.credential]
for credential in credentials:
if credential: if credential:
credential.credential_type.inject_credential( credential.credential_type.inject_credential(
credential, env, safe_env, args, safe_args, kwargs['private_data_dir'] credential, env, safe_env, args, safe_args, kwargs['private_data_dir']
@@ -788,6 +794,22 @@ class BaseTask(Task):
if not hasattr(settings, 'CELERY_UNIT_TEST'): if not hasattr(settings, 'CELERY_UNIT_TEST'):
self.signal_finished(pk) self.signal_finished(pk)
def get_ssh_key_path(self, instance, **kwargs):
'''
If using an SSH key, return the path for use by ssh-agent.
'''
private_data_files = kwargs.get('private_data_files', {})
if 'ssh' in private_data_files.get('credentials', {}):
return private_data_files['credentials']['ssh']
'''
Note: Don't inject network ssh key data into ssh-agent for network
credentials because the ansible modules do not yet support it.
We will want to add back in support when/if Ansible network modules
support this.
'''
return ''
class RunJob(BaseTask): class RunJob(BaseTask):
''' '''
@@ -800,36 +822,36 @@ class RunJob(BaseTask):
def build_private_data(self, job, **kwargs): def build_private_data(self, job, **kwargs):
''' '''
Returns a dict of the form Returns a dict of the form
dict['credential'] = <credential_decrypted_ssh_key_data> {
dict['cloud_credential'] = <cloud_credential_decrypted_ssh_key_data> 'credentials': {
dict['network_credential'] = <network_credential_decrypted_ssh_key_data> <awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
''' <awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
job_credentials = ['credential', 'cloud_credential', 'network_credential'] <awx.main.models.Credential>: <credential_decrypted_ssh_key_data>
private_data = {}
# If we were sent SSH credentials, decrypt them and send them
# back (they will be written to a temporary file).
for cred_name in job_credentials:
credential = getattr(job, cred_name, None)
if credential:
if credential.ssh_key_data not in (None, ''):
private_data[cred_name] = decrypt_field(credential, 'ssh_key_data') or ''
if job.cloud_credential and job.cloud_credential.kind == 'openstack':
credential = job.cloud_credential
openstack_auth = dict(auth_url=credential.host,
username=credential.username,
password=decrypt_field(credential, "password"),
project_name=credential.project)
if credential.domain not in (None, ''):
openstack_auth['domain_name'] = credential.domain
openstack_data = {
'clouds': {
'devstack': {
'auth': openstack_auth,
},
},
} }
private_data['cloud_credential'] = yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True) }
'''
private_data = {'credentials': {}}
for credential in job.all_credentials:
# 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, ''):
private_data['credentials'][credential] = decrypt_field(credential, 'ssh_key_data') or ''
if credential.kind == 'openstack':
openstack_auth = dict(auth_url=credential.host,
username=credential.username,
password=decrypt_field(credential, "password"),
project_name=credential.project)
if credential.domain not in (None, ''):
openstack_auth['domain_name'] = credential.domain
openstack_data = {
'clouds': {
'devstack': {
'auth': openstack_auth,
},
},
}
private_data['credentials'][credential] = yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True)
return private_data return private_data
@@ -894,47 +916,47 @@ class RunJob(BaseTask):
env['INVENTORY_HOSTVARS'] = str(True) env['INVENTORY_HOSTVARS'] = str(True)
# Set environment variables for cloud credentials. # Set environment variables for cloud credentials.
cloud_cred = job.cloud_credential cred_files = kwargs.get('private_data_files', {}).get('credentials', {})
if cloud_cred and cloud_cred.kind == 'aws': for cloud_cred in job.cloud_credentials:
env['AWS_ACCESS_KEY'] = cloud_cred.username if cloud_cred and cloud_cred.kind == 'aws':
env['AWS_SECRET_KEY'] = decrypt_field(cloud_cred, 'password') env['AWS_ACCESS_KEY'] = cloud_cred.username
if len(cloud_cred.security_token) > 0: env['AWS_SECRET_KEY'] = decrypt_field(cloud_cred, 'password')
env['AWS_SECURITY_TOKEN'] = decrypt_field(cloud_cred, 'security_token') if len(cloud_cred.security_token) > 0:
# FIXME: Add EC2_URL, maybe EC2_REGION! env['AWS_SECURITY_TOKEN'] = decrypt_field(cloud_cred, 'security_token')
elif cloud_cred and cloud_cred.kind == 'rax': # FIXME: Add EC2_URL, maybe EC2_REGION!
env['RAX_USERNAME'] = cloud_cred.username elif cloud_cred and cloud_cred.kind == 'rax':
env['RAX_API_KEY'] = decrypt_field(cloud_cred, 'password') env['RAX_USERNAME'] = cloud_cred.username
env['CLOUD_VERIFY_SSL'] = str(False) env['RAX_API_KEY'] = decrypt_field(cloud_cred, 'password')
elif cloud_cred and cloud_cred.kind == 'gce': env['CLOUD_VERIFY_SSL'] = str(False)
env['GCE_EMAIL'] = cloud_cred.username elif cloud_cred and cloud_cred.kind == 'gce':
env['GCE_PROJECT'] = cloud_cred.project env['GCE_EMAIL'] = cloud_cred.username
env['GCE_PEM_FILE_PATH'] = kwargs.get('private_data_files', {}).get('cloud_credential', '') env['GCE_PROJECT'] = cloud_cred.project
elif cloud_cred and cloud_cred.kind == 'azure': env['GCE_PEM_FILE_PATH'] = cred_files.get(cloud_cred, '')
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.username elif cloud_cred and cloud_cred.kind == 'azure':
env['AZURE_CERT_PATH'] = kwargs.get('private_data_files', {}).get('cloud_credential', '') env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.username
elif cloud_cred and cloud_cred.kind == 'azure_rm': env['AZURE_CERT_PATH'] = cred_files.get(cloud_cred, '')
if len(cloud_cred.client) and len(cloud_cred.tenant): elif cloud_cred and cloud_cred.kind == 'azure_rm':
env['AZURE_CLIENT_ID'] = cloud_cred.client if len(cloud_cred.client) and len(cloud_cred.tenant):
env['AZURE_SECRET'] = decrypt_field(cloud_cred, 'secret') env['AZURE_CLIENT_ID'] = cloud_cred.client
env['AZURE_TENANT'] = cloud_cred.tenant env['AZURE_SECRET'] = decrypt_field(cloud_cred, 'secret')
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.subscription env['AZURE_TENANT'] = cloud_cred.tenant
else: env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.subscription
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.subscription else:
env['AZURE_AD_USER'] = cloud_cred.username env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.subscription
env['AZURE_PASSWORD'] = decrypt_field(cloud_cred, 'password') env['AZURE_AD_USER'] = cloud_cred.username
elif cloud_cred and cloud_cred.kind == 'vmware': env['AZURE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
env['VMWARE_USER'] = cloud_cred.username elif cloud_cred and cloud_cred.kind == 'vmware':
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password') env['VMWARE_USER'] = cloud_cred.username
env['VMWARE_HOST'] = cloud_cred.host env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
elif cloud_cred and cloud_cred.kind == 'openstack': env['VMWARE_HOST'] = cloud_cred.host
env['OS_CLIENT_CONFIG_FILE'] = kwargs.get('private_data_files', {}).get('cloud_credential', '') elif cloud_cred and cloud_cred.kind == 'openstack':
env['OS_CLIENT_CONFIG_FILE'] = cred_files.get(cloud_cred, '')
network_cred = job.network_credential for network_cred in job.network_credentials:
if network_cred:
env['ANSIBLE_NET_USERNAME'] = network_cred.username env['ANSIBLE_NET_USERNAME'] = network_cred.username
env['ANSIBLE_NET_PASSWORD'] = decrypt_field(network_cred, 'password') env['ANSIBLE_NET_PASSWORD'] = decrypt_field(network_cred, 'password')
ssh_keyfile = kwargs.get('private_data_files', {}).get('network_credential', '') ssh_keyfile = cred_files.get(network_cred, '')
if ssh_keyfile: if ssh_keyfile:
env['ANSIBLE_NET_SSH_KEYFILE'] = ssh_keyfile env['ANSIBLE_NET_SSH_KEYFILE'] = ssh_keyfile
@@ -1099,24 +1121,6 @@ class RunJob(BaseTask):
return OutputEventFilter(stdout_handle, job_event_callback) return OutputEventFilter(stdout_handle, job_event_callback)
def get_ssh_key_path(self, instance, **kwargs):
'''
If using an SSH key, return the path for use by ssh-agent.
'''
private_data_files = kwargs.get('private_data_files', {})
if 'credential' in private_data_files:
return private_data_files.get('credential')
'''
Note: Don't inject network ssh key data into ssh-agent for network
credentials because the ansible modules do not yet support it.
We will want to add back in support when/if Ansible network modules
support this.
'''
#elif 'network_credential' in private_data_files:
# return private_data_files.get('network_credential')
return ''
def should_use_proot(self, instance, **kwargs): def should_use_proot(self, instance, **kwargs):
''' '''
Return whether this task should use proot. Return whether this task should use proot.
@@ -1169,13 +1173,22 @@ class RunProjectUpdate(BaseTask):
def build_private_data(self, project_update, **kwargs): def build_private_data(self, project_update, **kwargs):
''' '''
Return SSH private key data needed for this project update. Return SSH private key data needed for this project update.
Returns a dict of the form
{
'credentials': {
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>
}
}
''' '''
handle, self.revision_path = tempfile.mkstemp() handle, self.revision_path = tempfile.mkstemp()
private_data = {} private_data = {'credentials': {}}
if project_update.credential: if project_update.credential:
credential = project_update.credential credential = project_update.credential
if credential.ssh_key_data not in (None, ''): if credential.ssh_key_data not in (None, ''):
private_data['scm_credential'] = decrypt_field(project_update.credential, 'ssh_key_data') private_data['credentials'][credential] = decrypt_field(credential, 'ssh_key_data')
return private_data return private_data
def build_passwords(self, project_update, **kwargs): def build_passwords(self, project_update, **kwargs):
@@ -1334,12 +1347,6 @@ class RunProjectUpdate(BaseTask):
def get_idle_timeout(self): def get_idle_timeout(self):
return getattr(settings, 'PROJECT_UPDATE_IDLE_TIMEOUT', None) return getattr(settings, 'PROJECT_UPDATE_IDLE_TIMEOUT', None)
def get_ssh_key_path(self, instance, **kwargs):
'''
If using an SSH key, return the path for use by ssh-agent.
'''
return kwargs.get('private_data_files', {}).get('scm_credential', '')
def get_stdout_handle(self, instance): def get_stdout_handle(self, instance):
stdout_handle = super(RunProjectUpdate, self).get_stdout_handle(instance) stdout_handle = super(RunProjectUpdate, self).get_stdout_handle(instance)
@@ -1450,13 +1457,26 @@ class RunInventoryUpdate(BaseTask):
model = InventoryUpdate model = InventoryUpdate
def build_private_data(self, inventory_update, **kwargs): def build_private_data(self, inventory_update, **kwargs):
"""Return private data needed for inventory update. """
Return private data needed for inventory update.
Returns a dict of the form
{
'credentials': {
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>
}
}
If no private data is needed, return None. If no private data is needed, return None.
""" """
private_data = {'credentials': {}}
# If this is Microsoft Azure or GCE, return the RSA key # If this is Microsoft Azure or GCE, return the RSA key
if inventory_update.source in ('azure', 'gce'): if inventory_update.source in ('azure', 'gce'):
credential = inventory_update.credential credential = inventory_update.credential
return dict(cloud_credential=decrypt_field(credential, 'ssh_key_data')) private_data['credentials'][credential] = decrypt_field(credential, 'ssh_key_data')
return private_data
if inventory_update.source == 'openstack': if inventory_update.source == 'openstack':
credential = inventory_update.credential credential = inventory_update.credential
@@ -1484,7 +1504,10 @@ class RunInventoryUpdate(BaseTask):
}, },
'cache': cache, 'cache': cache,
} }
return dict(cloud_credential=yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True)) private_data['credentials'][credential] = yaml.safe_dump(
openstack_data, default_flow_style=False, allow_unicode=True
)
return private_data
cp = ConfigParser.ConfigParser() cp = ConfigParser.ConfigParser()
# Build custom ec2.ini for ec2 inventory script to use. # Build custom ec2.ini for ec2 inventory script to use.
@@ -1600,7 +1623,8 @@ class RunInventoryUpdate(BaseTask):
if cp.sections(): if cp.sections():
f = cStringIO.StringIO() f = cStringIO.StringIO()
cp.write(f) cp.write(f)
return dict(cloud_credential=f.getvalue()) private_data['credentials'][inventory_update.credential] = f.getvalue()
return private_data
def build_passwords(self, inventory_update, **kwargs): def build_passwords(self, inventory_update, **kwargs):
"""Build a dictionary of authentication/credential information for """Build a dictionary of authentication/credential information for
@@ -1646,7 +1670,8 @@ class RunInventoryUpdate(BaseTask):
# `awx/plugins/inventory` directory; those files should be kept in # `awx/plugins/inventory` directory; those files should be kept in
# sync with those in Ansible core at all times. # sync with those in Ansible core at all times.
passwords = kwargs.get('passwords', {}) passwords = kwargs.get('passwords', {})
cloud_credential = kwargs.get('private_data_files', {}).get('cloud_credential', '') cred_data = kwargs.get('private_data_files', {}).get('credentials', '')
cloud_credential = cred_data.get(inventory_update.credential, '')
if inventory_update.source == 'ec2': if inventory_update.source == 'ec2':
if passwords.get('source_username', '') and passwords.get('source_password', ''): if passwords.get('source_username', '') and passwords.get('source_password', ''):
env['AWS_ACCESS_KEY_ID'] = passwords['source_username'] env['AWS_ACCESS_KEY_ID'] = passwords['source_username']
@@ -1823,13 +1848,22 @@ class RunAdHocCommand(BaseTask):
''' '''
Return SSH private key data needed for this ad hoc command (only if Return SSH private key data needed for this ad hoc command (only if
stored in DB as ssh_key_data). stored in DB as ssh_key_data).
Returns a dict of the form
{
'credentials': {
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>,
<awx.main.models.Credential>: <credential_decrypted_ssh_key_data>
}
}
''' '''
# If we were sent SSH credentials, decrypt them and send them # If we were sent SSH credentials, decrypt them and send them
# back (they will be written to a temporary file). # back (they will be written to a temporary file).
creds = ad_hoc_command.credential creds = ad_hoc_command.credential
private_data = {} private_data = {'credentials': {}}
if creds and creds.ssh_key_data not in (None, ''): if creds and creds.ssh_key_data not in (None, ''):
private_data['ad_hoc_credential'] = decrypt_field(creds, 'ssh_key_data') or '' private_data['credentials'][creds] = decrypt_field(creds, 'ssh_key_data') or ''
return private_data return private_data
def build_passwords(self, ad_hoc_command, **kwargs): def build_passwords(self, ad_hoc_command, **kwargs):
@@ -1986,12 +2020,6 @@ class RunAdHocCommand(BaseTask):
return OutputEventFilter(stdout_handle, ad_hoc_command_event_callback) return OutputEventFilter(stdout_handle, ad_hoc_command_event_callback)
def get_ssh_key_path(self, instance, **kwargs):
'''
If using an SSH key, return the path for use by ssh-agent.
'''
return kwargs.get('private_data_files', {}).get('ad_hoc_credential', '')
def should_use_proot(self, instance, **kwargs): def should_use_proot(self, instance, **kwargs):
''' '''
Return whether this task should use proot. Return whether this task should use proot.

View File

@@ -316,7 +316,6 @@ def test_prefetch_jt_copy_capability(job_template, project, inventory, machine_c
qs = JobTemplate.objects.all() qs = JobTemplate.objects.all()
cache_list_capabilities(qs, [{'copy': [ cache_list_capabilities(qs, [{'copy': [
'project.use', 'inventory.use', 'credential.use', 'project.use', 'inventory.use', 'credential.use',
'cloud_credential.use', 'network_credential.use'
]}], JobTemplate, rando) ]}], JobTemplate, rando)
assert qs[0].capabilities_cache == {'copy': False} assert qs[0].capabilities_cache == {'copy': False}
@@ -326,7 +325,6 @@ def test_prefetch_jt_copy_capability(job_template, project, inventory, machine_c
cache_list_capabilities(qs, [{'copy': [ cache_list_capabilities(qs, [{'copy': [
'project.use', 'inventory.use', 'credential.use', 'project.use', 'inventory.use', 'credential.use',
'cloud_credential.use', 'network_credential.use'
]}], JobTemplate, rando) ]}], JobTemplate, rando)
assert qs[0].capabilities_cache == {'copy': True} assert qs[0].capabilities_cache == {'copy': True}

View File

@@ -0,0 +1,60 @@
import pytest
from django.core.exceptions import ValidationError
from awx.main.models import Credential
@pytest.mark.django_db
def test_clean_credential_with_ssh_type(credentialtype_ssh, job_template):
credential = Credential(
name='My Credential',
credential_type=credentialtype_ssh
)
credential.save()
job_template.credential = 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(
name='AWS Credential',
credential_type=credentialtype_aws
)
aws.save()
net = Credential(
name='Net Credential',
credential_type=credentialtype_net
)
net.save()
job_template.extra_credentials.add(aws)
job_template.extra_credentials.add(net)
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()

View File

@@ -159,6 +159,7 @@ def test_jt_existing_values_are_nonsensitive(job_template_with_ids, user_unit):
assert access.changes_are_non_sensitive(job_template_with_ids, data) assert access.changes_are_non_sensitive(job_template_with_ids, data)
@pytest.mark.xfail # TODO: update this to respect JT.extra_credentials
def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit): def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit):
"""Assure that can_add is called with all ForeignKeys.""" """Assure that can_add is called with all ForeignKeys."""

View File

@@ -1,133 +0,0 @@
import pytest
from awx.main.models.credential import CredentialType, Credential
from awx.main.models.jobs import Job
from awx.main.models.inventory import Inventory
from awx.main.tasks import RunJob
def test_aws_cred_parse(mocker):
with mocker.patch('django.db.ConnectionRouter.db_for_write'):
job = Job(id=1)
job.inventory = mocker.MagicMock(spec=Inventory, id=2)
aws = CredentialType.defaults['aws']()
options = {
'credential_type': aws,
'inputs': {
'username': 'aws_user',
'password': 'aws_passwd',
'security_token': 'token',
}
}
job.cloud_credential = Credential(**options)
run_job = RunJob()
mocker.patch.object(run_job, 'should_use_proot', return_value=False)
env = run_job.build_env(job, private_data_dir='/tmp')
assert env['AWS_ACCESS_KEY'] == options['inputs']['username']
assert env['AWS_SECRET_KEY'] == options['inputs']['password']
assert env['AWS_SECURITY_TOKEN'] == options['inputs']['security_token']
def test_net_cred_parse(mocker):
with mocker.patch('django.db.ConnectionRouter.db_for_write'):
job = Job(id=1)
job.inventory = mocker.MagicMock(spec=Inventory, id=2)
net = CredentialType.defaults['aws']()
options = {
'credential_type': net,
'inputs': {
'username':'test',
'password':'test',
'authorize': True,
'authorize_password': 'passwd',
'ssh_key_data': """-----BEGIN PRIVATE KEY-----\nstuff==\n-----END PRIVATE KEY-----""",
}
}
private_data_files = {
'network_credential': '/tmp/this_file_does_not_exist_during_test_but_the_path_is_real',
}
job.network_credential = Credential(**options)
run_job = RunJob()
mocker.patch.object(run_job, 'should_use_proot', return_value=False)
env = run_job.build_env(job, private_data_dir='/tmp', private_data_files=private_data_files)
assert env['ANSIBLE_NET_USERNAME'] == options['inputs']['username']
assert env['ANSIBLE_NET_PASSWORD'] == options['inputs']['password']
assert env['ANSIBLE_NET_AUTHORIZE'] == '1'
assert env['ANSIBLE_NET_AUTH_PASS'] == options['inputs']['authorize_password']
assert env['ANSIBLE_NET_SSH_KEYFILE'] == private_data_files['network_credential']
@pytest.fixture
def mock_job(mocker):
ssh = CredentialType.defaults['ssh']()
options = {
'credential_type': ssh,
'inputs': {
'username':'test',
'password':'test',
'ssh_key_data': """-----BEGIN PRIVATE KEY-----\nstuff==\n-----END PRIVATE KEY-----""",
'authorize': True,
'authorize_password': 'passwd',
}
}
mock_job_attrs = {'forks': False, 'id': 1, 'cancel_flag': False, 'status': 'running', 'job_type': 'normal',
'credential': None, 'cloud_credential': None, 'network_credential': Credential(**options),
'become_enabled': False, 'become_method': None, 'become_username': None,
'inventory': mocker.MagicMock(spec=Inventory, id=2), 'force_handlers': False,
'limit': None, 'verbosity': None, 'job_tags': None, 'skip_tags': None,
'start_at_task': None, 'pk': 1, 'launch_type': 'normal', 'job_template':None,
'created_by': None, 'extra_vars_dict': None, 'project':None, 'playbook': 'test.yml',
'store_facts': False,}
mock_job = mocker.MagicMock(spec=Job, **mock_job_attrs)
return mock_job
@pytest.fixture
def run_job_net_cred(mocker, get_ssh_version, mock_job):
mocker.patch('django.db.ConnectionRouter.db_for_write')
run_job = RunJob()
mocker.patch.object(run_job, 'update_model', return_value=mock_job)
mocker.patch.object(run_job, 'build_cwd', return_value='/tmp')
mocker.patch.object(run_job, 'should_use_proot', return_value=False)
mocker.patch.object(run_job, 'run_pexpect', return_value=('successful', 0))
mocker.patch.object(run_job, 'open_fifo_write', return_value=None)
mocker.patch.object(run_job, 'post_run_hook', return_value=None)
return run_job
@pytest.mark.skip(reason="Note: Ansible network modules don't yet support ssh-agent added keys.")
def test_net_cred_ssh_agent(run_job_net_cred, mock_job):
run_job = run_job_net_cred
run_job.run(mock_job.id)
assert run_job.update_model.call_count == 4
job_args = run_job.update_model.call_args_list[1][1].get('job_args')
assert 'ssh-add' in job_args
assert 'ssh-agent' in job_args
assert 'network_credential' in job_args
def test_net_cred_job_model_env(run_job_net_cred, mock_job):
run_job = run_job_net_cred
run_job.run(mock_job.id)
assert run_job.update_model.call_count == 4
job_args = run_job.update_model.call_args_list[1][1].get('job_env')
assert 'ANSIBLE_NET_USERNAME' in job_args
assert 'ANSIBLE_NET_PASSWORD' in job_args
assert 'ANSIBLE_NET_AUTHORIZE' in job_args
assert 'ANSIBLE_NET_AUTH_PASS' in job_args
assert 'ANSIBLE_NET_SSH_KEYFILE' in job_args

View File

@@ -126,7 +126,9 @@ def test_openstack_client_config_generation(mocker):
'source_vars_dict': {} 'source_vars_dict': {}
}) })
cloud_config = update.build_private_data(inventory_update) cloud_config = update.build_private_data(inventory_update)
cloud_credential = yaml.load(cloud_config['cloud_credential']) cloud_credential = yaml.load(
cloud_config.get('credentials')[inventory_update.credential]
)
assert cloud_credential['clouds'] == { assert cloud_credential['clouds'] == {
'devstack': { 'devstack': {
'auth': { 'auth': {
@@ -155,7 +157,9 @@ def test_openstack_client_config_generation_with_private_source_vars(mocker, sou
'source_vars_dict': {'private': source} 'source_vars_dict': {'private': source}
}) })
cloud_config = update.build_private_data(inventory_update) cloud_config = update.build_private_data(inventory_update)
cloud_credential = yaml.load(cloud_config['cloud_credential']) cloud_credential = yaml.load(
cloud_config.get('credentials')[inventory_update.credential]
)
assert cloud_credential['clouds'] == { assert cloud_credential['clouds'] == {
'devstack': { 'devstack': {
'auth': { 'auth': {
@@ -227,18 +231,27 @@ class TestJobExecution:
p.stop() p.stop()
def get_instance(self): def get_instance(self):
return Job( job = Job(
pk=1, pk=1,
created=datetime.utcnow(), created=datetime.utcnow(),
status='new', status='new',
job_type='run', job_type='run',
cancel_flag=False, cancel_flag=False,
credential=None,
cloud_credential=None,
network_credential=None,
project=Project() project=Project()
) )
# 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']
))
self.patches.append(patch)
patch.start()
return job
@property @property
def pk(self): def pk(self):
return self.instance.pk return self.instance.pk
@@ -278,13 +291,13 @@ class TestJobCredentials(TestJobExecution):
def test_ssh_passwords(self, field, password_name, expected_flag): def test_ssh_passwords(self, field, password_name, expected_flag):
ssh = CredentialType.defaults['ssh']() ssh = CredentialType.defaults['ssh']()
self.instance.credential = Credential( credential = Credential(
pk=1,
credential_type=ssh, credential_type=ssh,
inputs = {'username': 'bob', field: 'secret'} inputs = {'username': 'bob', field: 'secret'}
) )
self.instance.credential.inputs[field] = encrypt_field( credential.inputs[field] = encrypt_field(credential, field)
self.instance.credential, field self.instance.credential = credential
)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -298,20 +311,20 @@ class TestJobCredentials(TestJobExecution):
def test_ssh_key_with_agent(self): def test_ssh_key_with_agent(self):
ssh = CredentialType.defaults['ssh']() ssh = CredentialType.defaults['ssh']()
self.instance.credential = Credential( credential = Credential(
pk=1,
credential_type=ssh, credential_type=ssh,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY 'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
} }
) )
self.instance.credential.inputs['ssh_key_data'] = encrypt_field( credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
self.instance.credential, 'ssh_key_data' self.instance.credential = credential
)
def run_pexpect_side_effect(private_data, *args, **kwargs): def run_pexpect_side_effect(private_data, *args, **kwargs):
job, args, cwd, env, passwords, stdout = args job, args, cwd, env, passwords, stdout = args
ssh_key_data_fifo = '/'.join([private_data, 'credential']) ssh_key_data_fifo = '/'.join([private_data, 'credential_1'])
assert open(ssh_key_data_fifo, 'r').read() == self.EXAMPLE_PRIVATE_KEY assert open(ssh_key_data_fifo, 'r').read() == self.EXAMPLE_PRIVATE_KEY
assert ' '.join(args).startswith( assert ' '.join(args).startswith(
'ssh-agent -a %s sh -c ssh-add %s && rm -f %s' % ( 'ssh-agent -a %s sh -c ssh-add %s && rm -f %s' % (
@@ -331,13 +344,13 @@ class TestJobCredentials(TestJobExecution):
def test_aws_cloud_credential(self): def test_aws_cloud_credential(self):
aws = CredentialType.defaults['aws']() aws = CredentialType.defaults['aws']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=aws, credential_type=aws,
inputs = {'username': 'bob', 'password': 'secret'} inputs = {'username': 'bob', 'password': 'secret'}
) )
self.instance.cloud_credential.inputs['password'] = encrypt_field( credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.cloud_credential, 'password' self.instance.extra_credentials.add(credential)
)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -350,14 +363,14 @@ class TestJobCredentials(TestJobExecution):
def test_aws_cloud_credential_with_sts_token(self): def test_aws_cloud_credential_with_sts_token(self):
aws = CredentialType.defaults['aws']() aws = CredentialType.defaults['aws']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=aws, credential_type=aws,
inputs = {'username': 'bob', 'password': 'secret', 'security_token': 'token'} inputs = {'username': 'bob', 'password': 'secret', 'security_token': 'token'}
) )
for key in ('password', 'security_token'): for key in ('password', 'security_token'):
self.instance.cloud_credential.inputs[key] = encrypt_field( credential.inputs[key] = encrypt_field(credential, key)
self.instance.cloud_credential, key self.instance.extra_credentials.add(credential)
)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -370,13 +383,13 @@ class TestJobCredentials(TestJobExecution):
def test_rax_credential(self): def test_rax_credential(self):
rax = CredentialType.defaults['rackspace']() rax = CredentialType.defaults['rackspace']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=rax, credential_type=rax,
inputs = {'username': 'bob', 'password': 'secret'} inputs = {'username': 'bob', 'password': 'secret'}
) )
self.instance.cloud_credential.inputs['password'] = encrypt_field( credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.cloud_credential, 'password' self.instance.extra_credentials.add(credential)
)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -389,7 +402,8 @@ class TestJobCredentials(TestJobExecution):
def test_gce_credentials(self): def test_gce_credentials(self):
gce = CredentialType.defaults['gce']() gce = CredentialType.defaults['gce']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=gce, credential_type=gce,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
@@ -397,9 +411,8 @@ class TestJobCredentials(TestJobExecution):
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY 'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
} }
) )
self.instance.cloud_credential.inputs['ssh_key_data'] = encrypt_field( credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
self.instance.cloud_credential, 'ssh_key_data' self.instance.extra_credentials.add(credential)
)
def run_pexpect_side_effect(*args, **kwargs): def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args job, args, cwd, env, passwords, stdout = args
@@ -414,16 +427,16 @@ class TestJobCredentials(TestJobExecution):
def test_azure_credentials(self): def test_azure_credentials(self):
azure = CredentialType.defaults['azure']() azure = CredentialType.defaults['azure']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=azure, credential_type=azure,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY 'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
} }
) )
self.instance.cloud_credential.inputs['ssh_key_data'] = encrypt_field( credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
self.instance.cloud_credential, 'ssh_key_data' self.instance.extra_credentials.add(credential)
)
def run_pexpect_side_effect(*args, **kwargs): def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args job, args, cwd, env, passwords, stdout = args
@@ -437,7 +450,8 @@ class TestJobCredentials(TestJobExecution):
def test_azure_rm_with_tenant(self): def test_azure_rm_with_tenant(self):
azure = CredentialType.defaults['azure_rm']() azure = CredentialType.defaults['azure_rm']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=azure, credential_type=azure,
inputs = { inputs = {
'client': 'some-client', 'client': 'some-client',
@@ -446,9 +460,8 @@ class TestJobCredentials(TestJobExecution):
'subscription': 'some-subscription' 'subscription': 'some-subscription'
} }
) )
self.instance.cloud_credential.inputs['secret'] = encrypt_field( credential.inputs['secret'] = encrypt_field(credential, 'secret')
self.instance.cloud_credential, 'secret' self.instance.extra_credentials.add(credential)
)
self.task.run(self.pk) self.task.run(self.pk)
@@ -463,7 +476,8 @@ class TestJobCredentials(TestJobExecution):
def test_azure_rm_with_password(self): def test_azure_rm_with_password(self):
azure = CredentialType.defaults['azure_rm']() azure = CredentialType.defaults['azure_rm']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=azure, credential_type=azure,
inputs = { inputs = {
'subscription': 'some-subscription', 'subscription': 'some-subscription',
@@ -471,9 +485,8 @@ class TestJobCredentials(TestJobExecution):
'password': 'secret' 'password': 'secret'
} }
) )
self.instance.cloud_credential.inputs['password'] = encrypt_field( credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.cloud_credential, 'password' self.instance.extra_credentials.add(credential)
)
self.task.run(self.pk) self.task.run(self.pk)
@@ -487,13 +500,13 @@ class TestJobCredentials(TestJobExecution):
def test_vmware_credentials(self): def test_vmware_credentials(self):
vmware = CredentialType.defaults['vmware']() vmware = CredentialType.defaults['vmware']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=vmware, credential_type=vmware,
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'} inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
) )
self.instance.cloud_credential.inputs['password'] = encrypt_field( credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.cloud_credential, 'password' self.instance.extra_credentials.add(credential)
)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -506,7 +519,8 @@ class TestJobCredentials(TestJobExecution):
def test_openstack_credentials(self): def test_openstack_credentials(self):
openstack = CredentialType.defaults['openstack']() openstack = CredentialType.defaults['openstack']()
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=openstack, credential_type=openstack,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
@@ -515,9 +529,8 @@ class TestJobCredentials(TestJobExecution):
'host': 'https://keystone.example.org' 'host': 'https://keystone.example.org'
} }
) )
self.instance.cloud_credential.inputs['password'] = encrypt_field( credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.cloud_credential, 'password' self.instance.extra_credentials.add(credential)
)
def run_pexpect_side_effect(*args, **kwargs): def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args job, args, cwd, env, passwords, stdout = args
@@ -539,7 +552,8 @@ class TestJobCredentials(TestJobExecution):
def test_net_credentials(self): def test_net_credentials(self):
net = CredentialType.defaults['net']() net = CredentialType.defaults['net']()
self.instance.network_credential = Credential( credential = Credential(
pk=1,
credential_type=net, credential_type=net,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
@@ -550,9 +564,8 @@ class TestJobCredentials(TestJobExecution):
} }
) )
for field in ('password', 'ssh_key_data', 'authorize_password'): for field in ('password', 'ssh_key_data', 'authorize_password'):
self.instance.network_credential.inputs[field] = encrypt_field( credential.inputs[field] = encrypt_field(credential, field)
self.instance.network_credential, field self.instance.extra_credentials.add(credential)
)
def run_pexpect_side_effect(*args, **kwargs): def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args job, args, cwd, env, passwords, stdout = args
@@ -584,10 +597,12 @@ class TestJobCredentials(TestJobExecution):
} }
} }
) )
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=some_cloud, credential_type=some_cloud,
inputs = {'api_token': 'ABC123'} inputs = {'api_token': 'ABC123'}
) )
self.instance.extra_credentials.add(credential)
with pytest.raises(Exception): with pytest.raises(Exception):
self.task.run(self.pk) self.task.run(self.pk)
@@ -609,10 +624,12 @@ class TestJobCredentials(TestJobExecution):
} }
} }
) )
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=some_cloud, credential_type=some_cloud,
inputs = {'api_token': 'ABC123'} inputs = {'api_token': 'ABC123'}
) )
self.instance.extra_credentials.add(credential)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -639,10 +656,12 @@ class TestJobCredentials(TestJobExecution):
} }
} }
) )
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=some_cloud, credential_type=some_cloud,
inputs = {'api_token': 'ABC123'} inputs = {'api_token': 'ABC123'}
) )
self.instance.extra_credentials.add(credential)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -670,13 +689,13 @@ class TestJobCredentials(TestJobExecution):
} }
} }
) )
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=some_cloud, credential_type=some_cloud,
inputs = {'password': 'SUPER-SECRET-123'} inputs = {'password': 'SUPER-SECRET-123'}
) )
self.instance.cloud_credential.inputs['password'] = encrypt_field( credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.cloud_credential, 'password' self.instance.extra_credentials.add(credential)
)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -704,10 +723,12 @@ class TestJobCredentials(TestJobExecution):
} }
} }
) )
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=some_cloud, credential_type=some_cloud,
inputs = {'api_token': 'ABC123'} inputs = {'api_token': 'ABC123'}
) )
self.instance.extra_credentials.add(credential)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -738,13 +759,13 @@ class TestJobCredentials(TestJobExecution):
} }
} }
) )
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=some_cloud, credential_type=some_cloud,
inputs = {'password': 'SUPER-SECRET-123'} inputs = {'password': 'SUPER-SECRET-123'}
) )
self.instance.cloud_credential.inputs['password'] = encrypt_field( credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.cloud_credential, 'password' self.instance.extra_credentials.add(credential)
)
self.task.run(self.pk) self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1 assert self.task.run_pexpect.call_count == 1
@@ -775,10 +796,12 @@ class TestJobCredentials(TestJobExecution):
} }
} }
) )
self.instance.cloud_credential = Credential( credential = Credential(
pk=1,
credential_type=some_cloud, credential_type=some_cloud,
inputs = {'api_token': 'ABC123'} inputs = {'api_token': 'ABC123'}
) )
self.instance.extra_credentials.add(credential)
self.task.run(self.pk) self.task.run(self.pk)
def run_pexpect_side_effect(*args, **kwargs): def run_pexpect_side_effect(*args, **kwargs):
@@ -789,6 +812,49 @@ class TestJobCredentials(TestJobExecution):
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect) self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.task.run(self.pk) self.task.run(self.pk)
def test_multi_cloud(self):
gce = CredentialType.defaults['gce']()
gce_credential = Credential(
pk=1,
credential_type=gce,
inputs = {
'username': 'bob',
'project': 'some-project',
'ssh_key_data': 'GCE: %s' % self.EXAMPLE_PRIVATE_KEY
}
)
gce_credential.inputs['ssh_key_data'] = encrypt_field(gce_credential, 'ssh_key_data')
self.instance.extra_credentials.add(gce_credential)
azure = CredentialType.defaults['azure']()
azure_credential = Credential(
pk=2,
credential_type=azure,
inputs = {
'username': 'joe',
'ssh_key_data': 'AZURE: %s' % self.EXAMPLE_PRIVATE_KEY
}
)
azure_credential.inputs['ssh_key_data'] = encrypt_field(azure_credential, 'ssh_key_data')
self.instance.extra_credentials.add(azure_credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
assert env['GCE_EMAIL'] == 'bob'
assert env['GCE_PROJECT'] == 'some-project'
ssh_key_data = env['GCE_PEM_FILE_PATH']
assert open(ssh_key_data, 'rb').read() == 'GCE: %s' % self.EXAMPLE_PRIVATE_KEY
assert env['AZURE_SUBSCRIPTION_ID'] == 'joe'
ssh_key_data = env['AZURE_CERT_PATH']
assert open(ssh_key_data, 'rb').read() == 'AZURE: %s' % self.EXAMPLE_PRIVATE_KEY
return ['successful', 0]
self.task.run_pexpect = mock.Mock(side_effect=run_pexpect_side_effect)
self.task.run(self.pk)
class TestProjectUpdateCredentials(TestJobExecution): class TestProjectUpdateCredentials(TestJobExecution):
@@ -817,6 +883,7 @@ class TestProjectUpdateCredentials(TestJobExecution):
ssh = CredentialType.defaults['ssh']() ssh = CredentialType.defaults['ssh']()
self.instance.scm_type = scm_type self.instance.scm_type = scm_type
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=ssh, credential_type=ssh,
inputs = {'username': 'bob', 'password': 'secret'} inputs = {'username': 'bob', 'password': 'secret'}
) )
@@ -836,6 +903,7 @@ class TestProjectUpdateCredentials(TestJobExecution):
ssh = CredentialType.defaults['ssh']() ssh = CredentialType.defaults['ssh']()
self.instance.scm_type = scm_type self.instance.scm_type = scm_type
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=ssh, credential_type=ssh,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
@@ -848,7 +916,7 @@ class TestProjectUpdateCredentials(TestJobExecution):
def run_pexpect_side_effect(private_data, *args, **kwargs): def run_pexpect_side_effect(private_data, *args, **kwargs):
job, args, cwd, env, passwords, stdout = args job, args, cwd, env, passwords, stdout = args
ssh_key_data_fifo = '/'.join([private_data, 'scm_credential']) ssh_key_data_fifo = '/'.join([private_data, 'credential_1'])
assert open(ssh_key_data_fifo, 'r').read() == self.EXAMPLE_PRIVATE_KEY assert open(ssh_key_data_fifo, 'r').read() == self.EXAMPLE_PRIVATE_KEY
assert ' '.join(args).startswith( assert ' '.join(args).startswith(
'ssh-agent -a %s sh -c ssh-add %s && rm -f %s' % ( 'ssh-agent -a %s sh -c ssh-add %s && rm -f %s' % (
@@ -885,6 +953,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
aws = CredentialType.defaults['aws']() aws = CredentialType.defaults['aws']()
self.instance.source = 'ec2' self.instance.source = 'ec2'
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=aws, credential_type=aws,
inputs = {'username': 'bob', 'password': 'secret'} inputs = {'username': 'bob', 'password': 'secret'}
) )
@@ -911,6 +980,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
vmware = CredentialType.defaults['vmware']() vmware = CredentialType.defaults['vmware']()
self.instance.source = 'vmware' self.instance.source = 'vmware'
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=vmware, credential_type=vmware,
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'} inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
) )
@@ -935,6 +1005,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
azure = CredentialType.defaults['azure']() azure = CredentialType.defaults['azure']()
self.instance.source = 'azure' self.instance.source = 'azure'
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=azure, credential_type=azure,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
@@ -959,6 +1030,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
gce = CredentialType.defaults['gce']() gce = CredentialType.defaults['gce']()
self.instance.source = 'gce' self.instance.source = 'gce'
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=gce, credential_type=gce,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
@@ -985,6 +1057,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
openstack = CredentialType.defaults['openstack']() openstack = CredentialType.defaults['openstack']()
self.instance.source = 'openstack' self.instance.source = 'openstack'
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=openstack, credential_type=openstack,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
@@ -1019,6 +1092,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
satellite6 = CredentialType.defaults['satellite6']() satellite6 = CredentialType.defaults['satellite6']()
self.instance.source = 'satellite6' self.instance.source = 'satellite6'
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=satellite6, credential_type=satellite6,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',
@@ -1046,6 +1120,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
cloudforms = CredentialType.defaults['cloudforms']() cloudforms = CredentialType.defaults['cloudforms']()
self.instance.source = 'cloudforms' self.instance.source = 'cloudforms'
self.instance.credential = Credential( self.instance.credential = Credential(
pk=1,
credential_type=cloudforms, credential_type=cloudforms,
inputs = { inputs = {
'username': 'bob', 'username': 'bob',