From b63a577b33a6a71bb9a5646efe9fa9c315e0069c Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 23 Apr 2015 10:08:49 -0400 Subject: [PATCH 1/2] allow machine and cloud credentials to co-exist. --- awx/main/tasks.py | 122 ++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 69 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index a263f9ed61..eace5bd618 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -230,11 +230,25 @@ class BaseTask(Task): ''' return os.path.abspath(os.path.join(os.path.dirname(__file__), *args)) - def build_private_data(self, instance, **kwargs): + def build_private_data(self, job, **kwargs): ''' - Return any private data that needs to be written to a temporary file - for this task. + Return SSH private key data (only if stored in DB as ssh_key_data). + Return structure is a dict of the form: + dict['credential'] = + dict['cloud_credential'] = ''' + job_credentials = [ 'credential', 'cloud_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 None + + return private_data def build_private_data_dir(self, instance, **kwargs): ''' @@ -244,20 +258,23 @@ class BaseTask(Task): os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) return path - def build_private_data_file(self, instance, **kwargs): + def build_private_data_files(self, instance, **kwargs): ''' - Create a temporary file containing the private data. + Create a temporary files containing the private data. + Returns a dictionary with keys from build_private_data + (i.e. 'credential', 'cloud_credential') and values the file path. ''' private_data = self.build_private_data(instance, **kwargs) + private_data_files = {} if private_data is not None: - handle, path = tempfile.mkstemp(dir=kwargs.get('private_data_dir', None)) - f = os.fdopen(handle, 'w') - f.write(private_data) - f.close() - os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) - return path - else: - return '' + for name, data in private_data.iteritems(): + handle, path = tempfile.mkstemp(dir=kwargs.get('private_data_dir', None)) + f = os.fdopen(handle, 'w') + f.write(data) + f.close() + os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) + private_data_files[name] = path + return private_data_files def build_passwords(self, instance, **kwargs): ''' @@ -319,6 +336,12 @@ class BaseTask(Task): ''' return '' + def get_cloud_credential_path(self, instance, **kwargs): + ''' + Return the path to the cloud key file, if present. + ''' + return kwargs.get('private_data_files', {}).get('cloud_credential', '') + 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), @@ -434,7 +457,8 @@ class BaseTask(Task): # Fetch ansible version once here to support version-dependent features. kwargs['ansible_version'] = get_ansible_version() kwargs['private_data_dir'] = self.build_private_data_dir(instance, **kwargs) - kwargs['private_data_file'] = self.build_private_data_file(instance, **kwargs) + # May have to serialize the value + kwargs['private_data_files'] = self.build_private_data_files(instance, **kwargs) kwargs['passwords'] = self.build_passwords(instance, **kwargs) args = self.build_args(instance, **kwargs) safe_args = self.build_safe_args(instance, **kwargs) @@ -503,28 +527,6 @@ class RunJob(BaseTask): name = 'awx.main.tasks.run_job' model = Job - def build_private_data(self, job, **kwargs): - ''' - Return SSH private key data needed for this job (only if stored in DB - as ssh_key_data). - ''' - # If we were sent SSH credentials, decrypt them and send them - # back (they will be written to a temporary file). - credential = getattr(job, 'credential', None) - if credential: - return decrypt_field(credential, 'ssh_key_data') or None - - # We might also have been sent a cloud credential. If so, send it. - # - # This sets up an either/or situation with credential and cloud - # credential when it comes to SSH data. This should be fine, as if - # you're running against cloud instances, you'll be using the cloud - # credentials to do so. I assert that no situation currently exists - # where we need both. - cloud_credential = getattr(job, 'cloud_credential', None) - if cloud_credential: - return decrypt_field(cloud_credential, 'ssh_key_data') or None - def build_passwords(self, job, **kwargs): ''' Build a dictionary of passwords for SSH private key, SSH user, sudo/su @@ -583,10 +585,10 @@ class RunJob(BaseTask): 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['private_data_file'] + env['GCE_PEM_FILE_PATH'] = self.get_cloud_credential_path(job, **kwargs) elif cloud_cred and cloud_cred.kind == 'azure': env['AZURE_SUBSCRIPTION_ID'] = cloud_cred.username - env['AZURE_CERT_PATH'] = kwargs['private_data_file'] + env['AZURE_CERT_PATH'] = self.get_cloud_credential_path(job, **kwargs) elif cloud_cred and cloud_cred.kind == 'vmware': env['VMWARE_USER'] = cloud_cred.username env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password') @@ -729,7 +731,7 @@ class RunJob(BaseTask): ''' If using an SSH key, return the path for use by ssh-agent. ''' - return kwargs.get('private_data_file', '') + return kwargs.get('private_data_files', {}).get('credential', '') def should_use_proot(self, instance, **kwargs): ''' @@ -760,13 +762,6 @@ class RunProjectUpdate(BaseTask): name = 'awx.main.tasks.run_project_update' model = ProjectUpdate - def build_private_data(self, project_update, **kwargs): - ''' - Return SSH private key data needed for this project update. - ''' - if project_update.credential: - return decrypt_field(project_update.credential, 'ssh_key_data') or None - def build_passwords(self, project_update, **kwargs): ''' Build a dictionary of passwords for SSH private key unlock and SCM @@ -916,7 +911,7 @@ class RunProjectUpdate(BaseTask): ''' If using an SSH key, return the path for use by ssh-agent. ''' - return kwargs.get('private_data_file', '') + return kwargs.get('private_data_files', {}).get('credential', '') class RunInventoryUpdate(BaseTask): @@ -929,8 +924,7 @@ class RunInventoryUpdate(BaseTask): """ # If this is Microsoft Azure or GCE, return the RSA key if inventory_update.source in ('azure', 'gce'): - credential = inventory_update.credential - return decrypt_field(credential, 'ssh_key_data') + return super(RunInventoryUpdate, self).build_private_data(inventory_update, **kwargs) if inventory_update.source == 'openstack': credential = inventory_update.credential @@ -939,7 +933,7 @@ class RunInventoryUpdate(BaseTask): password=decrypt_field(credential, "password"), project_name=credential.project) openstack_data = {"clouds": {"devstack": {"auth": openstack_auth}}} - return yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True) + return dict(cloud_credential=yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True)) cp = ConfigParser.ConfigParser() # Build custom ec2.ini for ec2 inventory script to use. @@ -993,7 +987,7 @@ class RunInventoryUpdate(BaseTask): if cp.sections(): f = cStringIO.StringIO() cp.write(f) - return f.getvalue() + return dict(cloud_credential=f.getvalue()) def build_passwords(self, inventory_update, **kwargs): """Build a dictionary of authentication/credential information for @@ -1038,31 +1032,32 @@ 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 = self.get_cloud_credential_path(inventory_update, **kwargs) if inventory_update.source == 'ec2': if passwords.get('source_username', '') and passwords.get('source_password', ''): env['AWS_ACCESS_KEY_ID'] = passwords['source_username'] env['AWS_SECRET_ACCESS_KEY'] = passwords['source_password'] - env['EC2_INI_PATH'] = kwargs.get('private_data_file', '') + env['EC2_INI_PATH'] = cloud_credential elif inventory_update.source == 'rax': - env['RAX_CREDS_FILE'] = kwargs.get('private_data_file', '') + env['RAX_CREDS_FILE'] = cloud_credential env['RAX_REGION'] = inventory_update.source_regions or 'all' # Set this environment variable so the vendored package won't # complain about not being able to determine its version number. env['PBR_VERSION'] = '0.5.21' elif inventory_update.source == 'vmware': - env['VMWARE_INI'] = kwargs.get('private_data_file', '') + env['VMWARE_INI'] = cloud_credential env['VMWARE_HOST'] = passwords.get('source_host', '') env['VMWARE_USER'] = passwords.get('source_username', '') env['VMWARE_PASSWORD'] = passwords.get('source_password', '') elif inventory_update.source == 'azure': env['AZURE_SUBSCRIPTION_ID'] = passwords.get('source_username', '') - env['AZURE_CERT_PATH'] = kwargs['private_data_file'] + env['AZURE_CERT_PATH'] = cloud_credential elif inventory_update.source == 'gce': env['GCE_EMAIL'] = passwords.get('source_username', '') env['GCE_PROJECT'] = passwords.get('source_project', '') - env['GCE_PEM_FILE_PATH'] = kwargs['private_data_file'] + env['GCE_PEM_FILE_PATH'] = cloud_credential elif inventory_update.source == 'openstack': - env['OPENSTACK_CONFIG_FILE'] = kwargs.get('private_data_file', '') + env['OPENSTACK_CONFIG_FILE'] = cloud_credential elif inventory_update.source == 'file': # FIXME: Parse source_env to dict, update env. pass @@ -1172,17 +1167,6 @@ class RunAdHocCommand(BaseTask): name = 'awx.main.tasks.run_ad_hoc_command' model = AdHocCommand - def build_private_data(self, ad_hoc_command, **kwargs): - ''' - Return SSH private key data needed for this ad hoc command (only if - stored in DB as 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 - if creds: - return decrypt_field(creds, 'ssh_key_data') or None - def build_passwords(self, ad_hoc_command, **kwargs): ''' Build a dictionary of passwords for SSH private key, SSH user and @@ -1321,7 +1305,7 @@ class RunAdHocCommand(BaseTask): ''' If using an SSH key, return the path for use by ssh-agent. ''' - return kwargs.get('private_data_file', '') + return kwargs.get('private_data_files', {}).get('credential', '') def should_use_proot(self, instance, **kwargs): ''' From 360aae8c255b6a1a37f75e940ff9c19de7bdb9b7 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Thu, 23 Apr 2015 13:01:43 -0400 Subject: [PATCH 2/2] multiple credentials implementation style more inline with existing code --- awx/main/tasks.py | 92 +++++++++++++++++++++++++++++------------------ 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index eace5bd618..b5289a6718 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -234,21 +234,7 @@ class BaseTask(Task): ''' Return SSH private key data (only if stored in DB as ssh_key_data). Return structure is a dict of the form: - dict['credential'] = - dict['cloud_credential'] = ''' - job_credentials = [ 'credential', 'cloud_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 None - - return private_data def build_private_data_dir(self, instance, **kwargs): ''' @@ -336,12 +322,6 @@ class BaseTask(Task): ''' return '' - def get_cloud_credential_path(self, instance, **kwargs): - ''' - Return the path to the cloud key file, if present. - ''' - return kwargs.get('private_data_files', {}).get('cloud_credential', '') - 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), @@ -527,6 +507,25 @@ class RunJob(BaseTask): name = 'awx.main.tasks.run_job' model = Job + def build_private_data(self, job, **kwargs): + ''' + Returns a dict of the form + dict['credential'] = + dict['cloud_credential'] = + ''' + job_credentials = ['credential', 'cloud_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 '' + + return private_data + def build_passwords(self, job, **kwargs): ''' Build a dictionary of passwords for SSH private key, SSH user, sudo/su @@ -585,10 +584,10 @@ class RunJob(BaseTask): 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'] = self.get_cloud_credential_path(job, **kwargs) + 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'] = self.get_cloud_credential_path(job, **kwargs) + env['AZURE_CERT_PATH'] = kwargs.get('private_data_files', {}).get('cloud_credential', '') elif cloud_cred and cloud_cred.kind == 'vmware': env['VMWARE_USER'] = cloud_cred.username env['VMWARE_PASSWORD'] = decrypt_field(cloud_cred, 'password') @@ -762,6 +761,16 @@ class RunProjectUpdate(BaseTask): name = 'awx.main.tasks.run_project_update' model = ProjectUpdate + + def build_private_data(self, project_update, **kwargs): + ''' + Return SSH private key data needed for this project update. + ''' + private_data = {} + if project_update.credential: + private_data['scm_credential'] = decrypt_field(project_update.credential, 'ssh_key_data') or None + return private_data + def build_passwords(self, project_update, **kwargs): ''' Build a dictionary of passwords for SSH private key unlock and SCM @@ -911,7 +920,7 @@ class RunProjectUpdate(BaseTask): ''' If using an SSH key, return the path for use by ssh-agent. ''' - return kwargs.get('private_data_files', {}).get('credential', '') + return kwargs.get('private_data_files', {}).get('scm_credential', '') class RunInventoryUpdate(BaseTask): @@ -924,7 +933,8 @@ class RunInventoryUpdate(BaseTask): """ # If this is Microsoft Azure or GCE, return the RSA key if inventory_update.source in ('azure', 'gce'): - return super(RunInventoryUpdate, self).build_private_data(inventory_update, **kwargs) + credential = inventory_update.credential + return dict(scm_credential=decrypt_field(credential, 'ssh_key_data')) if inventory_update.source == 'openstack': credential = inventory_update.credential @@ -933,7 +943,7 @@ class RunInventoryUpdate(BaseTask): password=decrypt_field(credential, "password"), project_name=credential.project) openstack_data = {"clouds": {"devstack": {"auth": openstack_auth}}} - return dict(cloud_credential=yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True)) + return dict(scm_credential=yaml.safe_dump(openstack_data, default_flow_style=False, allow_unicode=True)) cp = ConfigParser.ConfigParser() # Build custom ec2.ini for ec2 inventory script to use. @@ -987,7 +997,7 @@ class RunInventoryUpdate(BaseTask): if cp.sections(): f = cStringIO.StringIO() cp.write(f) - return dict(cloud_credential=f.getvalue()) + return dict(scm_credential=f.getvalue()) def build_passwords(self, inventory_update, **kwargs): """Build a dictionary of authentication/credential information for @@ -1032,32 +1042,32 @@ 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 = self.get_cloud_credential_path(inventory_update, **kwargs) + scm_credential = kwargs.get('private_data_files', {}).get('scm_credential', '') if inventory_update.source == 'ec2': if passwords.get('source_username', '') and passwords.get('source_password', ''): env['AWS_ACCESS_KEY_ID'] = passwords['source_username'] env['AWS_SECRET_ACCESS_KEY'] = passwords['source_password'] - env['EC2_INI_PATH'] = cloud_credential + env['EC2_INI_PATH'] = scm_credential elif inventory_update.source == 'rax': - env['RAX_CREDS_FILE'] = cloud_credential + env['RAX_CREDS_FILE'] = scm_credential env['RAX_REGION'] = inventory_update.source_regions or 'all' # Set this environment variable so the vendored package won't # complain about not being able to determine its version number. env['PBR_VERSION'] = '0.5.21' elif inventory_update.source == 'vmware': - env['VMWARE_INI'] = cloud_credential + env['VMWARE_INI'] = scm_credential env['VMWARE_HOST'] = passwords.get('source_host', '') env['VMWARE_USER'] = passwords.get('source_username', '') env['VMWARE_PASSWORD'] = passwords.get('source_password', '') elif inventory_update.source == 'azure': env['AZURE_SUBSCRIPTION_ID'] = passwords.get('source_username', '') - env['AZURE_CERT_PATH'] = cloud_credential + env['AZURE_CERT_PATH'] = scm_credential elif inventory_update.source == 'gce': env['GCE_EMAIL'] = passwords.get('source_username', '') env['GCE_PROJECT'] = passwords.get('source_project', '') - env['GCE_PEM_FILE_PATH'] = cloud_credential + env['GCE_PEM_FILE_PATH'] = scm_credential elif inventory_update.source == 'openstack': - env['OPENSTACK_CONFIG_FILE'] = cloud_credential + env['OPENSTACK_CONFIG_FILE'] = scm_credential elif inventory_update.source == 'file': # FIXME: Parse source_env to dict, update env. pass @@ -1158,7 +1168,6 @@ class RunInventoryUpdate(BaseTask): def get_idle_timeout(self): return getattr(settings, 'INVENTORY_UPDATE_IDLE_TIMEOUT', None) - class RunAdHocCommand(BaseTask): ''' Celery task to run an ad hoc command using ansible. @@ -1167,6 +1176,19 @@ class RunAdHocCommand(BaseTask): name = 'awx.main.tasks.run_ad_hoc_command' model = AdHocCommand + def build_private_data(self, ad_hoc_command, **kwargs): + ''' + Return SSH private key data needed for this ad hoc command (only if + stored in DB as 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 = {} + if creds and creds.ssh_key_data not in (None, ''): + private_data['ad_hoc_credential'] = decrypt_field(creds, 'ssh_key_data') or '' + return private_data + def build_passwords(self, ad_hoc_command, **kwargs): ''' Build a dictionary of passwords for SSH private key, SSH user and @@ -1305,7 +1327,7 @@ class RunAdHocCommand(BaseTask): ''' If using an SSH key, return the path for use by ssh-agent. ''' - return kwargs.get('private_data_files', {}).get('credential', '') + return kwargs.get('private_data_files', {}).get('ad_hoc_credential', '') def should_use_proot(self, instance, **kwargs): '''