mirror of
https://github.com/ansible/awx.git
synced 2026-01-15 03:40:42 -03:30
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:
parent
465d620629
commit
accf7cdea2
@ -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)
|
||||
|
||||
@ -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']}
|
||||
]
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
'''
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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}
|
||||
|
||||
|
||||
60
awx/main/tests/functional/models/test_job_options.py
Normal file
60
awx/main/tests/functional/models/test_job_options.py
Normal 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()
|
||||
@ -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."""
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user