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'),
'project_update': DEFAULT_SUMMARY_FIELDS + ('status', 'failed',),
'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_template': DEFAULT_SUMMARY_FIELDS,
'workflow_job_template': DEFAULT_SUMMARY_FIELDS,
@ -2096,7 +2094,7 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
class Meta:
fields = ('*', 'job_type', 'inventory', 'project', 'playbook',
'credential', 'cloud_credential', 'network_credential', 'forks', 'limit',
'credential', 'forks', 'limit',
'verbosity', 'extra_vars', 'job_tags', 'force_handlers',
'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})
if obj.credential:
res['credential'] = self.reverse('api:credential_detail', kwargs={'pk': obj.credential.pk})
if obj.cloud_credential:
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})
# TODO: add related links for `extra_credentials`
return res
def to_representation(self, obj):
@ -2129,10 +2122,6 @@ class JobOptionsSerializer(LabelsListMixin, BaseSerializer):
ret['playbook'] = ''
if 'credential' in ret and not obj.credential:
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
def validate(self, attrs):
@ -2296,10 +2285,6 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
data.setdefault('playbook', job_template.playbook)
if job_template.credential:
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('limit', job_template.limit)
data.setdefault('verbosity', job_template.verbosity)

View File

@ -2432,7 +2432,7 @@ class JobTemplateList(ListCreateAPIView):
always_allow_superuser = False
capabilities_prefetch = [
'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):
@ -3906,7 +3906,6 @@ class UnifiedJobTemplateList(ListAPIView):
capabilities_prefetch = [
'admin', 'execute',
{'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use', 'jobtemplate.credential.use',
'jobtemplate.cloud_credential.use', 'jobtemplate.network_credential.use',
'workflowjobtemplate.organization.admin']}
]

View File

@ -1072,7 +1072,7 @@ class JobTemplateAccess(BaseAccess):
else:
qs = self.model.accessible_objects(self.user, 'read_role')
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):
'''
@ -1113,13 +1113,8 @@ class JobTemplateAccess(BaseAccess):
if not self.check_related('credential', Credential, data, role_field='use_role'):
return False
# If a cloud credential is provided, the user should have use access.
if not self.check_related('cloud_credential', Credential, data, role_field='use_role'):
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
# TODO: If a vault credential is provided, the user should have use access to it.
# TODO: If any credential in extra_credentials, the user must have access
# If an inventory is provided, the user should have use access.
inventory = get_value(Inventory, 'inventory')
@ -1185,7 +1180,8 @@ class JobTemplateAccess(BaseAccess):
self.check_license(feature='surveys')
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)
if required_field not in data_for_change and required_obj is not None:
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)
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)
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:
return False
@ -1228,10 +1222,7 @@ class JobTemplateAccess(BaseAccess):
return False
if credential_id and self.user not in Credential.objects.get(pk=credential_id).use_role:
return False
if cloud_credential_id and self.user not in Credential.objects.get(pk=cloud_credential_id).use_role:
return False
if network_credential_id and self.user not in Credential.objects.get(pk=network_credential_id).use_role:
return False
# TODO: handle vault_credential and extra_credentials
return True
@ -1271,7 +1262,7 @@ class JobAccess(BaseAccess):
def get_queryset(self):
qs = self.model.objects
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')
if self.user.is_superuser or self.user.is_system_auditor:
return qs.all()
@ -1907,7 +1898,6 @@ class UnifiedJobTemplateAccess(BaseAccess):
# 'project',
# 'inventory',
# 'credential',
# 'cloud_credential',
#)
return qs.all()
@ -1957,14 +1947,12 @@ class UnifiedJobAccess(BaseAccess):
# 'credential',
# 'job_template',
# 'inventory_source',
# 'cloud_credential',
# 'project___credential',
# 'inventory_source___credential',
# 'inventory_source___inventory',
# 'job_template__inventory',
# 'job_template__project',
# 'job_template__credential',
# 'job_template__cloud_credential',
#)
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()
finally:
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,
on_delete=models.SET_NULL,
)
cloud_credential = models.ForeignKey(
extra_credentials = models.ManyToManyField(
'Credential',
related_name='%(class)ss_as_cloud_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,
related_name='%(class)ss_as_extra_credential+',
)
forks = models.PositiveIntegerField(
blank=True,
@ -170,26 +158,44 @@ class JobOptions(BaseModel):
cred = self.credential
if cred and cred.kind != 'ssh':
raise ValidationError(
_('You must provide a machine / SSH credential.'),
_('You must provide an SSH credential.'),
)
return cred
def clean_network_credential(self):
cred = self.network_credential
if cred and cred.kind != 'net':
def clean_vault_credential(self):
cred = self.vault_credential
if cred and cred.kind != 'vault':
raise ValidationError(
_('You must provide a network credential.'),
_('You must provide a Vault credential.'),
)
return cred
def clean_cloud_credential(self):
cred = self.cloud_credential
if cred and cred.kind not in CLOUD_PROVIDERS + ('aws',):
raise ValidationError(
_('Must provide a credential for a cloud provider, such as '
'Amazon Web Services or Rackspace.'),
)
return cred
def clean(self):
super(JobOptions, self).clean()
# extra_credentials M2M can't be accessed until a primary key exists
if self.pk:
for cred in self.extra_credentials.all():
if cred.credential_type.kind not in ('net', 'cloud'):
raise ValidationError(
_('Extra credentials must be network or cloud.'),
)
@property
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
def passwords_needed_to_start(self):
@ -262,11 +268,11 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
@classmethod
def _get_unified_job_field_names(cls):
return ['name', 'description', 'job_type', 'inventory', 'project',
'playbook', 'credential', 'cloud_credential', 'network_credential', 'forks', 'schedule',
'limit', 'verbosity', 'job_tags', 'extra_vars', 'launch_type',
'force_handlers', 'skip_tags', 'start_at_task', 'become_enabled',
'labels', 'survey_passwords', 'allow_simultaneous', 'timeout',
'store_facts',]
'playbook', 'credential', 'extra_credentials', 'forks',
'schedule', 'limit', 'verbosity', 'job_tags', 'extra_vars',
'launch_type', 'force_handlers', 'skip_tags', 'start_at_task',
'become_enabled', 'labels', 'survey_passwords',
'allow_simultaneous', 'timeout', 'store_facts',]
def resource_validation_data(self):
'''

View File

@ -397,31 +397,40 @@ class BaseTask(Task):
def build_private_data_files(self, instance, **kwargs):
'''
Create a temporary files containing the private data.
Returns a dictionary with keys from build_private_data
(i.e. 'credential', 'cloud_credential', 'network_credential') and values the file path.
Creates temporary files containing the private data.
Returns a dictionary i.e.,
{
'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_files = {}
private_data_files = {'credentials': {}}
if private_data is not None:
ssh_ver = get_ssh_version()
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")
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
# and we're running an earlier version (<6.5).
if 'OPENSSH PRIVATE KEY' in data and not openssh_keys_supported:
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
# accepted by ssh-add.
if 'OPENSSH PRIVATE KEY' in data and not data.endswith('\n'):
data += '\n'
# 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.
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)
self.open_fifo_write(path, data)
private_data_files['credentials']['ssh'] = path
# Ansible network modules do not yet support ssh-agent.
# Instead, ssh private key file is explicitly passed via an
# env variable.
@ -431,7 +440,7 @@ class BaseTask(Task):
f.write(data)
f.close()
os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)
private_data_files[name] = path
private_data_files['credentials'][credential] = path
return private_data_files
def open_fifo_write(self, path, data):
@ -515,12 +524,6 @@ class BaseTask(Task):
def args2cmdline(self, *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):
if 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)
# handle custom injectors specified on the CredentialType
for type_ in ('credential', 'cloud_credential', 'network_credential'):
credential = getattr(instance, type_, None)
if hasattr(instance, 'all_credentials'):
credentials = instance.all_credentials
else:
credentials = [instance.credential]
for credential in credentials:
if credential:
credential.credential_type.inject_credential(
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'):
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):
'''
@ -800,36 +822,36 @@ class RunJob(BaseTask):
def build_private_data(self, job, **kwargs):
'''
Returns a dict of the form
dict['credential'] = <credential_decrypted_ssh_key_data>
dict['cloud_credential'] = <cloud_credential_decrypted_ssh_key_data>
dict['network_credential'] = <network_credential_decrypted_ssh_key_data>
'''
job_credentials = ['credential', 'cloud_credential', 'network_credential']
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,
},
},
{
'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>
}
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
@ -894,47 +916,47 @@ class RunJob(BaseTask):
env['INVENTORY_HOSTVARS'] = str(True)
# Set environment variables for cloud credentials.
cloud_cred = job.cloud_credential
if cloud_cred and cloud_cred.kind == 'aws':
env['AWS_ACCESS_KEY'] = cloud_cred.username
env['AWS_SECRET_KEY'] = decrypt_field(cloud_cred, 'password')
if len(cloud_cred.security_token) > 0:
env['AWS_SECURITY_TOKEN'] = decrypt_field(cloud_cred, 'security_token')
# FIXME: Add EC2_URL, maybe EC2_REGION!
elif cloud_cred and cloud_cred.kind == 'rax':
env['RAX_USERNAME'] = cloud_cred.username
env['RAX_API_KEY'] = decrypt_field(cloud_cred, 'password')
env['CLOUD_VERIFY_SSL'] = str(False)
elif cloud_cred and cloud_cred.kind == 'gce':
env['GCE_EMAIL'] = cloud_cred.username
env['GCE_PROJECT'] = cloud_cred.project
env['GCE_PEM_FILE_PATH'] = kwargs.get('private_data_files', {}).get('cloud_credential', '')
elif cloud_cred and cloud_cred.kind == 'azure':
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.username
env['AZURE_CERT_PATH'] = kwargs.get('private_data_files', {}).get('cloud_credential', '')
elif cloud_cred and cloud_cred.kind == 'azure_rm':
if len(cloud_cred.client) and len(cloud_cred.tenant):
env['AZURE_CLIENT_ID'] = cloud_cred.client
env['AZURE_SECRET'] = decrypt_field(cloud_cred, 'secret')
env['AZURE_TENANT'] = cloud_cred.tenant
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.subscription
else:
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.subscription
env['AZURE_AD_USER'] = cloud_cred.username
env['AZURE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
elif cloud_cred and cloud_cred.kind == 'vmware':
env['VMWARE_USER'] = cloud_cred.username
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
env['VMWARE_HOST'] = cloud_cred.host
elif cloud_cred and cloud_cred.kind == 'openstack':
env['OS_CLIENT_CONFIG_FILE'] = kwargs.get('private_data_files', {}).get('cloud_credential', '')
cred_files = kwargs.get('private_data_files', {}).get('credentials', {})
for cloud_cred in job.cloud_credentials:
if cloud_cred and cloud_cred.kind == 'aws':
env['AWS_ACCESS_KEY'] = cloud_cred.username
env['AWS_SECRET_KEY'] = decrypt_field(cloud_cred, 'password')
if len(cloud_cred.security_token) > 0:
env['AWS_SECURITY_TOKEN'] = decrypt_field(cloud_cred, 'security_token')
# FIXME: Add EC2_URL, maybe EC2_REGION!
elif cloud_cred and cloud_cred.kind == 'rax':
env['RAX_USERNAME'] = cloud_cred.username
env['RAX_API_KEY'] = decrypt_field(cloud_cred, 'password')
env['CLOUD_VERIFY_SSL'] = str(False)
elif cloud_cred and cloud_cred.kind == 'gce':
env['GCE_EMAIL'] = cloud_cred.username
env['GCE_PROJECT'] = cloud_cred.project
env['GCE_PEM_FILE_PATH'] = cred_files.get(cloud_cred, '')
elif cloud_cred and cloud_cred.kind == 'azure':
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.username
env['AZURE_CERT_PATH'] = cred_files.get(cloud_cred, '')
elif cloud_cred and cloud_cred.kind == 'azure_rm':
if len(cloud_cred.client) and len(cloud_cred.tenant):
env['AZURE_CLIENT_ID'] = cloud_cred.client
env['AZURE_SECRET'] = decrypt_field(cloud_cred, 'secret')
env['AZURE_TENANT'] = cloud_cred.tenant
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.subscription
else:
env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.subscription
env['AZURE_AD_USER'] = cloud_cred.username
env['AZURE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
elif cloud_cred and cloud_cred.kind == 'vmware':
env['VMWARE_USER'] = cloud_cred.username
env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password')
env['VMWARE_HOST'] = cloud_cred.host
elif cloud_cred and cloud_cred.kind == 'openstack':
env['OS_CLIENT_CONFIG_FILE'] = cred_files.get(cloud_cred, '')
network_cred = job.network_credential
if network_cred:
for network_cred in job.network_credentials:
env['ANSIBLE_NET_USERNAME'] = network_cred.username
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:
env['ANSIBLE_NET_SSH_KEYFILE'] = ssh_keyfile
@ -1099,24 +1121,6 @@ class RunJob(BaseTask):
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):
'''
Return whether this task should use proot.
@ -1169,13 +1173,22 @@ class RunProjectUpdate(BaseTask):
def build_private_data(self, project_update, **kwargs):
'''
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()
private_data = {}
private_data = {'credentials': {}}
if project_update.credential:
credential = project_update.credential
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
def build_passwords(self, project_update, **kwargs):
@ -1334,12 +1347,6 @@ class RunProjectUpdate(BaseTask):
def get_idle_timeout(self):
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):
stdout_handle = super(RunProjectUpdate, self).get_stdout_handle(instance)
@ -1450,13 +1457,26 @@ class RunInventoryUpdate(BaseTask):
model = InventoryUpdate
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.
"""
private_data = {'credentials': {}}
# If this is Microsoft Azure or GCE, return the RSA key
if inventory_update.source in ('azure', 'gce'):
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':
credential = inventory_update.credential
@ -1484,7 +1504,10 @@ class RunInventoryUpdate(BaseTask):
},
'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()
# Build custom ec2.ini for ec2 inventory script to use.
@ -1600,7 +1623,8 @@ class RunInventoryUpdate(BaseTask):
if cp.sections():
f = cStringIO.StringIO()
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):
"""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
# sync with those in Ansible core at all times.
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 passwords.get('source_username', '') and passwords.get('source_password', ''):
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
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
# back (they will be written to a temporary file).
creds = ad_hoc_command.credential
private_data = {}
private_data = {'credentials': {}}
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
def build_passwords(self, ad_hoc_command, **kwargs):
@ -1986,12 +2020,6 @@ class RunAdHocCommand(BaseTask):
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):
'''
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()
cache_list_capabilities(qs, [{'copy': [
'project.use', 'inventory.use', 'credential.use',
'cloud_credential.use', 'network_credential.use'
]}], JobTemplate, rando)
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': [
'project.use', 'inventory.use', 'credential.use',
'cloud_credential.use', 'network_credential.use'
]}], JobTemplate, rando)
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)
@pytest.mark.xfail # TODO: update this to respect JT.extra_credentials
def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit):
"""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': {}
})
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'] == {
'devstack': {
'auth': {
@ -155,7 +157,9 @@ def test_openstack_client_config_generation_with_private_source_vars(mocker, sou
'source_vars_dict': {'private': source}
})
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'] == {
'devstack': {
'auth': {
@ -227,18 +231,27 @@ class TestJobExecution:
p.stop()
def get_instance(self):
return Job(
job = Job(
pk=1,
created=datetime.utcnow(),
status='new',
job_type='run',
cancel_flag=False,
credential=None,
cloud_credential=None,
network_credential=None,
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
def pk(self):
return self.instance.pk
@ -278,13 +291,13 @@ class TestJobCredentials(TestJobExecution):
def test_ssh_passwords(self, field, password_name, expected_flag):
ssh = CredentialType.defaults['ssh']()
self.instance.credential = Credential(
credential = Credential(
pk=1,
credential_type=ssh,
inputs = {'username': 'bob', field: 'secret'}
)
self.instance.credential.inputs[field] = encrypt_field(
self.instance.credential, field
)
credential.inputs[field] = encrypt_field(credential, field)
self.instance.credential = credential
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
@ -298,20 +311,20 @@ class TestJobCredentials(TestJobExecution):
def test_ssh_key_with_agent(self):
ssh = CredentialType.defaults['ssh']()
self.instance.credential = Credential(
credential = Credential(
pk=1,
credential_type=ssh,
inputs = {
'username': 'bob',
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
}
)
self.instance.credential.inputs['ssh_key_data'] = encrypt_field(
self.instance.credential, 'ssh_key_data'
)
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
self.instance.credential = credential
def run_pexpect_side_effect(private_data, *args, **kwargs):
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 ' '.join(args).startswith(
'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):
aws = CredentialType.defaults['aws']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=aws,
inputs = {'username': 'bob', 'password': 'secret'}
)
self.instance.cloud_credential.inputs['password'] = encrypt_field(
self.instance.cloud_credential, 'password'
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
@ -350,14 +363,14 @@ class TestJobCredentials(TestJobExecution):
def test_aws_cloud_credential_with_sts_token(self):
aws = CredentialType.defaults['aws']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=aws,
inputs = {'username': 'bob', 'password': 'secret', 'security_token': 'token'}
)
for key in ('password', 'security_token'):
self.instance.cloud_credential.inputs[key] = encrypt_field(
self.instance.cloud_credential, key
)
credential.inputs[key] = encrypt_field(credential, key)
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
@ -370,13 +383,13 @@ class TestJobCredentials(TestJobExecution):
def test_rax_credential(self):
rax = CredentialType.defaults['rackspace']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=rax,
inputs = {'username': 'bob', 'password': 'secret'}
)
self.instance.cloud_credential.inputs['password'] = encrypt_field(
self.instance.cloud_credential, 'password'
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
@ -389,7 +402,8 @@ class TestJobCredentials(TestJobExecution):
def test_gce_credentials(self):
gce = CredentialType.defaults['gce']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=gce,
inputs = {
'username': 'bob',
@ -397,9 +411,8 @@ class TestJobCredentials(TestJobExecution):
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
}
)
self.instance.cloud_credential.inputs['ssh_key_data'] = encrypt_field(
self.instance.cloud_credential, 'ssh_key_data'
)
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
self.instance.extra_credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
@ -414,16 +427,16 @@ class TestJobCredentials(TestJobExecution):
def test_azure_credentials(self):
azure = CredentialType.defaults['azure']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=azure,
inputs = {
'username': 'bob',
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
}
)
self.instance.cloud_credential.inputs['ssh_key_data'] = encrypt_field(
self.instance.cloud_credential, 'ssh_key_data'
)
credential.inputs['ssh_key_data'] = encrypt_field(credential, 'ssh_key_data')
self.instance.extra_credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
@ -437,7 +450,8 @@ class TestJobCredentials(TestJobExecution):
def test_azure_rm_with_tenant(self):
azure = CredentialType.defaults['azure_rm']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=azure,
inputs = {
'client': 'some-client',
@ -446,9 +460,8 @@ class TestJobCredentials(TestJobExecution):
'subscription': 'some-subscription'
}
)
self.instance.cloud_credential.inputs['secret'] = encrypt_field(
self.instance.cloud_credential, 'secret'
)
credential.inputs['secret'] = encrypt_field(credential, 'secret')
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
@ -463,7 +476,8 @@ class TestJobCredentials(TestJobExecution):
def test_azure_rm_with_password(self):
azure = CredentialType.defaults['azure_rm']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=azure,
inputs = {
'subscription': 'some-subscription',
@ -471,9 +485,8 @@ class TestJobCredentials(TestJobExecution):
'password': 'secret'
}
)
self.instance.cloud_credential.inputs['password'] = encrypt_field(
self.instance.cloud_credential, 'password'
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
@ -487,13 +500,13 @@ class TestJobCredentials(TestJobExecution):
def test_vmware_credentials(self):
vmware = CredentialType.defaults['vmware']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=vmware,
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
)
self.instance.cloud_credential.inputs['password'] = encrypt_field(
self.instance.cloud_credential, 'password'
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
assert self.task.run_pexpect.call_count == 1
@ -506,7 +519,8 @@ class TestJobCredentials(TestJobExecution):
def test_openstack_credentials(self):
openstack = CredentialType.defaults['openstack']()
self.instance.cloud_credential = Credential(
credential = Credential(
pk=1,
credential_type=openstack,
inputs = {
'username': 'bob',
@ -515,9 +529,8 @@ class TestJobCredentials(TestJobExecution):
'host': 'https://keystone.example.org'
}
)
self.instance.cloud_credential.inputs['password'] = encrypt_field(
self.instance.cloud_credential, 'password'
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
job, args, cwd, env, passwords, stdout = args
@ -539,7 +552,8 @@ class TestJobCredentials(TestJobExecution):
def test_net_credentials(self):
net = CredentialType.defaults['net']()
self.instance.network_credential = Credential(
credential = Credential(
pk=1,
credential_type=net,
inputs = {
'username': 'bob',
@ -550,9 +564,8 @@ class TestJobCredentials(TestJobExecution):
}
)
for field in ('password', 'ssh_key_data', 'authorize_password'):
self.instance.network_credential.inputs[field] = encrypt_field(
self.instance.network_credential, field
)
credential.inputs[field] = encrypt_field(credential, field)
self.instance.extra_credentials.add(credential)
def run_pexpect_side_effect(*args, **kwargs):
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,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
with pytest.raises(Exception):
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,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
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,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
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,
inputs = {'password': 'SUPER-SECRET-123'}
)
self.instance.cloud_credential.inputs['password'] = encrypt_field(
self.instance.cloud_credential, 'password'
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
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,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
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,
inputs = {'password': 'SUPER-SECRET-123'}
)
self.instance.cloud_credential.inputs['password'] = encrypt_field(
self.instance.cloud_credential, 'password'
)
credential.inputs['password'] = encrypt_field(credential, 'password')
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
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,
inputs = {'api_token': 'ABC123'}
)
self.instance.extra_credentials.add(credential)
self.task.run(self.pk)
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(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):
@ -817,6 +883,7 @@ class TestProjectUpdateCredentials(TestJobExecution):
ssh = CredentialType.defaults['ssh']()
self.instance.scm_type = scm_type
self.instance.credential = Credential(
pk=1,
credential_type=ssh,
inputs = {'username': 'bob', 'password': 'secret'}
)
@ -836,6 +903,7 @@ class TestProjectUpdateCredentials(TestJobExecution):
ssh = CredentialType.defaults['ssh']()
self.instance.scm_type = scm_type
self.instance.credential = Credential(
pk=1,
credential_type=ssh,
inputs = {
'username': 'bob',
@ -848,7 +916,7 @@ class TestProjectUpdateCredentials(TestJobExecution):
def run_pexpect_side_effect(private_data, *args, **kwargs):
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 ' '.join(args).startswith(
'ssh-agent -a %s sh -c ssh-add %s && rm -f %s' % (
@ -885,6 +953,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
aws = CredentialType.defaults['aws']()
self.instance.source = 'ec2'
self.instance.credential = Credential(
pk=1,
credential_type=aws,
inputs = {'username': 'bob', 'password': 'secret'}
)
@ -911,6 +980,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
vmware = CredentialType.defaults['vmware']()
self.instance.source = 'vmware'
self.instance.credential = Credential(
pk=1,
credential_type=vmware,
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
)
@ -935,6 +1005,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
azure = CredentialType.defaults['azure']()
self.instance.source = 'azure'
self.instance.credential = Credential(
pk=1,
credential_type=azure,
inputs = {
'username': 'bob',
@ -959,6 +1030,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
gce = CredentialType.defaults['gce']()
self.instance.source = 'gce'
self.instance.credential = Credential(
pk=1,
credential_type=gce,
inputs = {
'username': 'bob',
@ -985,6 +1057,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
openstack = CredentialType.defaults['openstack']()
self.instance.source = 'openstack'
self.instance.credential = Credential(
pk=1,
credential_type=openstack,
inputs = {
'username': 'bob',
@ -1019,6 +1092,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
satellite6 = CredentialType.defaults['satellite6']()
self.instance.source = 'satellite6'
self.instance.credential = Credential(
pk=1,
credential_type=satellite6,
inputs = {
'username': 'bob',
@ -1046,6 +1120,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
cloudforms = CredentialType.defaults['cloudforms']()
self.instance.source = 'cloudforms'
self.instance.credential = Credential(
pk=1,
credential_type=cloudforms,
inputs = {
'username': 'bob',