From b9d489c788e9a926daf17f3dd85987ba8b33d08f Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 29 Jan 2019 11:53:05 -0500 Subject: [PATCH] Use randomized file names for injector credential files --- awx/main/models/credential/__init__.py | 2 +- awx/main/models/credential/injectors.py | 8 +- awx/main/models/inventory.py | 127 ++++++++++++------ awx/main/tasks.py | 13 +- ...E_CREDENTIALS_FILE_PATH => file_reference} | 0 .../inventory/plugins/gce/gcp_compute.yml | 2 +- .../test_inventory_source_injectors.py | 6 - 7 files changed, 101 insertions(+), 57 deletions(-) rename awx/main/tests/data/inventory/plugins/gce/{GCE_CREDENTIALS_FILE_PATH => file_reference} (100%) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index e67d1492d7..1db1cedc44 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -718,8 +718,8 @@ class CredentialType(CommonModelNameNotUnique): os.chmod(path, stat.S_IRUSR) return path - path = build_extra_vars_file(extra_vars, private_data_dir) if extra_vars: + path = build_extra_vars_file(extra_vars, private_data_dir) args.extend(['-e', '@%s' % path]) diff --git a/awx/main/models/credential/injectors.py b/awx/main/models/credential/injectors.py index 4fb642da0b..9bf7edc4a9 100644 --- a/awx/main/models/credential/injectors.py +++ b/awx/main/models/credential/injectors.py @@ -30,11 +30,13 @@ def gce(cred, env, private_data_dir): 'token_uri': 'https://accounts.google.com/o/oauth2/token', } - path = os.path.join(private_data_dir, 'creds.json') - with open(path, 'w') as f: - json.dump(json_cred, f, indent=2) + handle, path = tempfile.mkstemp(dir=private_data_dir) + f = os.fdopen(handle, 'w') + json.dump(json_cred, f, indent=2) + f.close() os.chmod(path, stat.S_IRUSR | stat.S_IWUSR) env['GCE_CREDENTIALS_FILE_PATH'] = path + return path def azure_rm(cred, env, private_data_dir): diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 899851c49e..a2f74caa5c 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1324,20 +1324,27 @@ class InventorySourceOptions(BaseModel): # in other cases we do not specify which plugin to use return None - def get_deprecated_credential(self, kind): - for cred in self.credentials.all(): - if cred.credential_type.kind == kind: - return cred - else: - return None - def get_cloud_credential(self): + """Return the credential which is directly tied to the inventory source type. + """ credential = None - for cred in self.credentials.all(): - if cred.credential_type.kind != 'vault': - credential = cred + if self.source in CLOUD_PROVIDERS: + cred_kind = self.source.replace('ec2', 'aws') + for cred in self.credentials.all(): + if cred.kind == cred_kind: + credential = cred return credential + def get_extra_credentials(self): + """Return all credentials that are not used by the inventory source injector. + """ + primary_cred = self.get_cloud_credential() + extra_creds = [] + for cred in self.credentials.all(): + if primary_cred and cred.pk != primary_cred.pk: + extra_creds.append(cred) + return extra_creds + @property def credential(self): cred = self.get_cloud_credential() @@ -1711,14 +1718,6 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin, def get_ui_url(self): return urljoin(settings.TOWER_URL_BASE, "/#/jobs/inventory/{}".format(self.pk)) - @property - def ansible_virtualenv_path(self): - if self.inventory and self.inventory.organization: - virtualenv = self.inventory.organization.custom_virtualenv - if virtualenv: - return virtualenv - return settings.ANSIBLE_VENV_PATH - def get_actual_source_path(self): '''Alias to source_path that combines with project path for for SCM file based sources''' if self.inventory_source_id is None or self.inventory_source.source_project_id is None: @@ -1834,8 +1833,8 @@ class PluginFileInjector(object): def filename(self): return '{0}.yml'.format(self.plugin_name) - def inventory_contents(self, inventory_source): - return yaml.safe_dump(self.inventory_as_dict(inventory_source), default_flow_style=False) + def inventory_contents(self, inventory_update, private_data_dir): + return yaml.safe_dump(self.inventory_as_dict(inventory_update, private_data_dir), default_flow_style=False) def should_use_plugin(self): return bool( @@ -1843,36 +1842,59 @@ class PluginFileInjector(object): Version(self.ansible_version) >= Version(self.initial_version) ) - def build_env(self, *args, **kwargs): - if self.should_use_plugin(): - return self.build_plugin_env(*args, **kwargs) - else: - return self.build_script_env(*args, **kwargs) + @staticmethod + def get_builtin_injector(source): + from awx.main.models.credential import injectors as builtin_injectors + cred_kind = source.replace('ec2', 'aws') + if cred_kind not in dir(builtin_injectors): + return None + return getattr(builtin_injectors, cred_kind) - def build_plugin_env(self, inventory_update, env, private_data_dir): + def build_env(self, inventory_update, env, private_data_dir): + if self.should_use_plugin(): + injector_env = self.get_plugin_env(inventory_update, private_data_dir) + else: + injector_env = self.get_script_env(inventory_update, private_data_dir) + env.update(injector_env) return env - def build_script_env(self, inventory_update, env, private_data_dir): - return env + def get_plugin_env(self, inventory_update, private_data_dir, safe=False): + return self.get_script_env(inventory_update, private_data_dir, safe) - def build_private_data(self, *args, **kwargs): + def get_script_env(self, inventory_update, private_data_dir, safe=False): + """By default, we will apply the standard managed_by_tower injectors + for the script injection + """ + injected_env = {} + credential = inventory_update.get_cloud_credential() + builtin_injector = self.get_builtin_injector(inventory_update.source) + if builtin_injector is None: + return {} + builtin_injector(credential, injected_env, private_data_dir) + if safe: + from awx.main.models.credential import build_safe_env + injected_env = build_safe_env(injected_env) + return injected_env + + def build_private_data(self, inventory_update, private_data_dir): if self.should_use_plugin(): - return self.build_private_data(*args, **kwargs) + return self.build_plugin_private_data(inventory_update, private_data_dir) else: - return self.build_private_data(*args, **kwargs) + return self.build_script_private_data(inventory_update, private_data_dir) - def build_script_private_data(self, *args, **kwargs): - pass + def build_script_private_data(self, inventory_update, private_data_dir): + return None - def build_plugin_private_data(self, *args, **kwargs): - pass + def build_plugin_private_data(self, inventory_update, private_data_dir): + return None class gce(PluginFileInjector): plugin_name = 'gcp_compute' initial_version = '2.6' - def build_script_env(self, inventory_update, env, private_data_dir): + def get_script_env(self, inventory_update, private_data_dir): + env = super(gce, self).get_script_env(inventory_update, private_data_dir) env['GCE_ZONE'] = inventory_update.source_regions if inventory_update.source_regions != 'all' else '' # noqa # by default, the GCE inventory source caches results on disk for @@ -1886,21 +1908,44 @@ class gce(PluginFileInjector): env['GCE_INI_PATH'] = path return env - def inventory_as_dict(self, inventory_source): + def inventory_as_dict(self, inventory_update, private_data_dir): # NOTE: generalizing this to be use templating like credential types would be nice # but with YAML content that need to inject list parameters into the YAML, # it is hard to see any clean way we can possibly do this + credential = inventory_update.get_cloud_credential() + builtin_injector = self.get_builtin_injector(inventory_update.source) + creds_path = builtin_injector(credential, {}, private_data_dir) ret = dict( plugin='gcp_compute', - projects=[inventory_source.get_cloud_credential().project], + projects=[credential.get_input('project', default='')], filters=None, # necessary cruft, see: https://github.com/ansible/ansible/pull/50025 - service_account_file="creds.json", + service_account_file=creds_path, auth_kind="serviceaccount" ) - if inventory_source.source_regions and 'all' not in inventory_source.source_regions: - ret['zones'] = inventory_source.source_regions.split(',') + if inventory_update.source_regions and 'all' not in inventory_update.source_regions: + ret['zones'] = inventory_update.source_regions.split(',') return ret + def get_plugin_env(self, inventory_update, private_data_dir, safe=False): + # gce wants everything defined in inventory & cred files + return {} + + +class rhv(PluginFileInjector): + + def get_script_env(self, inventory_update, private_data_dir): + """Unlike the others, ovirt uses the custom credential templating + """ + env = {'INVENTORY_UPDATE_ID': inventory_update.pk} + safe_env = env.copy() + args = [] + safe_args = [] + credential = inventory_update.get_cloud_credential() + credential.credential_type.inject_credential( + credential, env, safe_env, args, safe_args, private_data_dir + ) + return env + for cls in PluginFileInjector.__subclasses__(): InventorySourceOptions.injectors[cls.__name__] = cls diff --git a/awx/main/tasks.py b/awx/main/tasks.py index b188414da6..bf542050ee 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1939,6 +1939,10 @@ class RunInventoryUpdate(BaseTask): If no private data is needed, return None. """ + if inventory_update.source in InventorySource.injectors: + injector = InventorySource.injectors[inventory_update.source](kwargs['ansible_version']) + return injector.build_private_data(inventory_update, kwargs.get('private_data_dir', None)) + private_data = {'credentials': {}} credential = inventory_update.get_cloud_credential() @@ -2283,10 +2287,9 @@ class RunInventoryUpdate(BaseTask): src = inventory_update.source if src in CLOUD_PROVIDERS: if src in InventorySource.injectors: - cloud_cred = inventory_update.get_cloud_credential() - injector = InventorySource.injectors[cloud_cred.kind](self.get_ansible_version(inventory_update)) + injector = InventorySource.injectors[inventory_update.source](self.get_ansible_version(inventory_update)) if injector.should_use_plugin(): - content = injector.inventory_contents(inventory_update) + content = injector.inventory_contents(inventory_update, kwargs['private_data_dir']) # must be a statically named file inventory_path = os.path.join(private_data_dir, injector.filename) with open(inventory_path, 'w') as f: @@ -2333,8 +2336,8 @@ class RunInventoryUpdate(BaseTask): return None def build_credentials_list(self, inventory_update): - # TODO: allow multiple custom creds for inv updates - return [inventory_update.get_cloud_credential()] + # All credentials not used by inventory source injector + return [inventory_update.get_extra_credentials()] def get_idle_timeout(self): return getattr(settings, 'INVENTORY_UPDATE_IDLE_TIMEOUT', None) diff --git a/awx/main/tests/data/inventory/plugins/gce/GCE_CREDENTIALS_FILE_PATH b/awx/main/tests/data/inventory/plugins/gce/file_reference similarity index 100% rename from awx/main/tests/data/inventory/plugins/gce/GCE_CREDENTIALS_FILE_PATH rename to awx/main/tests/data/inventory/plugins/gce/file_reference diff --git a/awx/main/tests/data/inventory/plugins/gce/gcp_compute.yml b/awx/main/tests/data/inventory/plugins/gce/gcp_compute.yml index 04bc7dcbe8..d94cc9cb8f 100644 --- a/awx/main/tests/data/inventory/plugins/gce/gcp_compute.yml +++ b/awx/main/tests/data/inventory/plugins/gce/gcp_compute.yml @@ -3,7 +3,7 @@ filters: null plugin: gcp_compute projects: - fooo -service_account_file: creds.json +service_account_file: {{ file_reference }} zones: - us-east4-a - us-west1-b diff --git a/awx/main/tests/functional/test_inventory_source_injectors.py b/awx/main/tests/functional/test_inventory_source_injectors.py index 2277f597ca..68ab8ee354 100644 --- a/awx/main/tests/functional/test_inventory_source_injectors.py +++ b/awx/main/tests/functional/test_inventory_source_injectors.py @@ -137,8 +137,6 @@ def read_content(private_data_dir, env, inventory_update): references[abs_file_path] = filename # plugin filenames are universal except IsADirectoryError: dir_contents[abs_file_path] = '' - print('dir contents') - print(dir_contents) # Declare cross-file references, also use special keywords if it is the cache cache_referenced = False @@ -169,8 +167,6 @@ def read_content(private_data_dir, env, inventory_update): raise AssertionError( 'A cache file was referenced but never created, files:\n{}'.format( json.dumps(dir_contents, indent=4))) - print('dir contents') - print(dir_contents) content = {} for abs_file_path, file_content in dir_contents.items(): @@ -181,8 +177,6 @@ def read_content(private_data_dir, env, inventory_update): reference_key = references[abs_file_path] file_content = private_key_regex.sub('{{private_key}}', file_content) content[reference_key] = file_content - print(' contents') - print(content) return content