def run(self, tmp=None, task_vars=None): """ Action Plugins should implement this method to perform their tasks. Everything else in this base class is a helper method for the action plugin to do that. :kwarg tmp: Temporary directory. Sometimes an action plugin sets up a temporary directory and then calls another module. This parameter allows us to reuse the same directory for both. :kwarg task_vars: The variables (host vars, group vars, config vars, etc) associated with this task. :returns: dictionary of results from the module Implementors of action modules may find the following variables especially useful: * Module parameters. These are stored in self._task.args """ result = {} if self._task.async_val and not self._supports_async: raise AnsibleActionFail('async is not supported for this task.') elif self._play_context.check_mode and not self._supports_check_mode: raise AnsibleActionSkip( 'check mode is not supported for this task.') elif self._task.async_val and self._play_context.check_mode: raise AnsibleActionFail( 'check mode and async cannot be used on same task.') return result
def run(self, tmp=None, task_vars=None): if task_vars is None: task_vars = dict() result = super(ActionModule, self).run(tmp, task_vars) del tmp # parse args args = self._get_args() changed = False src_path = args['path'] # check if source file exists file_stat = self._execute_module(module_name='stat', module_args=dict(path=src_path), task_vars=task_vars) timestamp = self._get_date_string(args['date_format']) dest_path = '.'.join([src_path, timestamp]) if file_stat.get('stat', {}).get('exists', False) is False: # file doesn't exist so we're done raise AnsibleActionSkip("{} does not exist.".format(src_path)) # check if destination file exists file_stat = self._execute_module(module_name='stat', module_args=dict(path=dest_path), task_vars=task_vars) if (not args['force'] and file_stat.get('stat', {}).get('exists', False) is True): raise AnsibleActionFail("Destination file {} exists. Use force " "option to proceed.".format(dest_path)) # copy file out of the way copy_result = self._execute_module(module_name='copy', module_args=dict(src=src_path, dest=dest_path, remote_src=True), task_vars=task_vars) if copy_result.get('failed', False): return copy_result changed = True if boolean(args.get('remove', False), strict=False): # cleanup original file as requested file_result = self._execute_module(module_name='file', module_args=dict( path=src_path, state='absent'), task_vars=task_vars) if file_result.get('failed', False): return file_result result['dest'] = copy_result['dest'] result['changed'] = changed return result
class ActionBase(with_metaclass(ABCMeta, object)): ''' This class is the base class for all action plugins, and defines code common to all actions. The base class handles the connection by putting/getting files and executing commands based on the current action in use. ''' def __init__(self, task, connection, play_context, loader, templar, shared_loader_obj): self._task = task self._connection = connection self._play_context = play_context self._loader = loader self._templar = templar self._shared_loader_obj = shared_loader_obj # Backwards compat: self._display isn't really needed, just import the global display and use that. self._display = display self._cleanup_remote_tmp = False self._supports_check_mode = True self._supports_async = False @abstractmethod def run(self, tmp=None, task_vars=None): """ Action Plugins should implement this method to perform their tasks. Everything else in this base class is a helper method for the action plugin to do that. :kwarg tmp: Temporary directory. Sometimes an action plugin sets up a temporary directory and then calls another module. This parameter allows us to reuse the same directory for both. :kwarg task_vars: The variables (host vars, group vars, config vars, etc) associated with this task. :returns: dictionary of results from the module Implementors of action modules may find the following variables especially useful: * Module parameters. These are stored in self._task.args """ result = {} if self._task. async and not self._supports_async: raise AnsibleActionFail('async is not supported for this task.') elif self._play_context.check_mode and not self._supports_check_mode: raise AnsibleActionSkip( 'check mode is not supported for this task.')
def run(self, tmp=None, task_vars=None): """ Action Plugins should implement this method to perform their tasks. Everything else in this base class is a helper method for the action plugin to do that. :kwarg tmp: Deprecated parameter. This is no longer used. An action plugin that calls another one and wants to use the same remote tmp for both should set self._connection._shell.tmpdir rather than this parameter. :kwarg task_vars: The variables (host vars, group vars, config vars, etc) associated with this task. :returns: dictionary of results from the module Implementors of action modules may find the following variables especially useful: * Module parameters. These are stored in self._task.args """ result = {} if tmp is not None: result['warning'] = ['ActionModule.run() no longer honors the tmp parameter. Action' ' plugins should set self._connection._shell.tmpdir to share' ' the tmpdir'] del tmp if self._task.async_val and not self._supports_async: raise AnsibleActionFail('async is not supported for this task.') elif self._play_context.check_mode and not self._supports_check_mode: raise AnsibleActionSkip('check mode is not supported for this task.') elif self._task.async_val and self._play_context.check_mode: raise AnsibleActionFail('check mode and async cannot be used on same task.') # Error if invalid argument is passed if self._VALID_ARGS: task_opts = frozenset(self._task.args.keys()) bad_opts = task_opts.difference(self._VALID_ARGS) if bad_opts: raise AnsibleActionFail('Invalid options for %s: %s' % (self._task.action, ','.join(list(bad_opts)))) if self._connection._shell.tmpdir is None and self._early_needs_tmp_path(): self._make_tmp_path() return result
def run(self, tmp=None, task_vars=None): ''' handler for unarchive operations ''' if task_vars is None: task_vars = dict() result = super(ActionModule, self).run(tmp, task_vars) source = self._task.args.get('src', None) dest = self._task.args.get('dest', None) remote_src = boolean(self._task.args.get('remote_src', False), strict=False) creates = self._task.args.get('creates', None) decrypt = self._task.args.get('decrypt', True) try: # "copy" is deprecated in favor of "remote_src". if 'copy' in self._task.args: # They are mutually exclusive. if 'remote_src' in self._task.args: raise AnsibleActionFail( "parameters are mutually exclusive: ('copy', 'remote_src')" ) # We will take the information from copy and store it in # the remote_src var to use later in this file. self._task.args['remote_src'] = remote_src = not boolean( self._task.args.pop('copy'), strict=False) if source is None or dest is None: raise AnsibleActionFail( "src (or content) and dest are required") if creates: # do not run the command if the line contains creates=filename # and the filename already exists. This allows idempotence # of command executions. creates = self._remote_expand_user(creates) if self._remote_file_exists(creates): raise AnsibleActionSkip("skipped, since %s exists" % creates) dest = self._remote_expand_user( dest) # CCTODO: Fix path for Windows hosts. source = os.path.expanduser(source) if not remote_src: try: source = self._loader.get_real_file(self._find_needle( 'files', source), decrypt=decrypt) except AnsibleError as e: raise AnsibleActionFail(to_text(e)) try: remote_stat = self._execute_remote_stat(dest, all_vars=task_vars, follow=True) except AnsibleError as e: raise AnsibleActionFail(to_text(e)) if not remote_stat['exists'] or not remote_stat['isdir']: raise AnsibleActionFail("dest '%s' must be an existing dir" % dest) if not remote_src: # transfer the file to a remote tmp location tmp_src = self._connection._shell.join_path( self._connection._shell.tempdir, 'source') self._transfer_file(source, tmp_src) # handle diff mode client side # handle check mode client side if not remote_src: # fix file permissions when the copy is done as a different user self._fixup_perms2((self._connection._shell.tempdir, tmp_src)) # Build temporary module_args. new_module_args = self._task.args.copy() new_module_args.update( dict( src=tmp_src, original_basename=os.path.basename(source), ), ) else: new_module_args = self._task.args.copy() new_module_args.update( dict(original_basename=os.path.basename(source), ), ) # remove action plugin only key for key in ('decrypt', ): if key in new_module_args: del new_module_args[key] # execute the unarchive module now, with the updated args result.update( self._execute_module(module_args=new_module_args, task_vars=task_vars)) except AnsibleAction as e: result.update(e.result) finally: self._remove_tmp_path(self._connection._shell.tempdir) return result
def run(self, tmp=None, task_vars=None): ''' handler for fetch operations ''' if task_vars is None: task_vars = dict() result = super(ActionModule, self).run(tmp, task_vars) del tmp # tmp no longer has any effect try: if self._play_context.check_mode: raise AnsibleActionSkip( 'check mode not (yet) supported for this module') source = self._task.args.get('src', None) original_dest = dest = self._task.args.get('dest', None) flat = boolean(self._task.args.get('flat'), strict=False) fail_on_missing = boolean(self._task.args.get( 'fail_on_missing', True), strict=False) validate_checksum = boolean(self._task.args.get( 'validate_checksum', True), strict=False) msg = '' # validate source and dest are strings FIXME: use basic.py and module specs if not isinstance(source, string_types): msg = "Invalid type supplied for source option, it must be a string" if not isinstance(dest, string_types): msg = "Invalid type supplied for dest option, it must be a string" if source is None or dest is None: msg = "src and dest are required" if msg: raise AnsibleActionFail(msg) source = self._connection._shell.join_path(source) source = self._remote_expand_user(source) remote_stat = {} remote_checksum = None if True: # Get checksum for the remote file even using become. Mitogen doesn't need slurp. # Follow symlinks because fetch always follows symlinks try: remote_stat = self._execute_remote_stat(source, all_vars=task_vars, follow=True) except AnsibleError as ae: result['changed'] = False result['file'] = source if fail_on_missing: result['failed'] = True result['msg'] = to_text(ae) else: result['msg'] = "%s, ignored" % to_text( ae, errors='surrogate_or_replace') return result remote_checksum = remote_stat.get('checksum') if remote_stat.get('exists'): if remote_stat.get('isdir'): result['failed'] = True result['changed'] = False result[ 'msg'] = "remote file is a directory, fetch cannot work on directories" # Historically, these don't fail because you may want to transfer # a log file that possibly MAY exist but keep going to fetch other # log files. Today, this is better achieved by adding # ignore_errors or failed_when to the task. Control the behaviour # via fail_when_missing if not fail_on_missing: result['msg'] += ", not transferring, ignored" del result['changed'] del result['failed'] return result # use slurp if permissions are lacking or privilege escalation is needed remote_data = None if remote_checksum in (None, '1', ''): slurpres = self._execute_module( module_name='ansible.legacy.slurp', module_args=dict(src=source), task_vars=task_vars) if slurpres.get('failed'): if not fail_on_missing: result['file'] = source result['changed'] = False else: result.update(slurpres) if 'not found' in slurpres.get('msg', ''): result[ 'msg'] = "the remote file does not exist, not transferring, ignored" elif slurpres.get('msg', '').startswith('source is a directory'): result[ 'msg'] = "remote file is a directory, fetch cannot work on directories" return result else: if slurpres['encoding'] == 'base64': remote_data = base64.b64decode(slurpres['content']) if remote_data is not None: remote_checksum = checksum_s(remote_data) # calculate the destination name if os.path.sep not in self._connection._shell.join_path('a', ''): source = self._connection._shell._unquote(source) source_local = source.replace('\\', '/') else: source_local = source # ensure we only use file name, avoid relative paths if not is_subpath(dest, original_dest): # TODO: ? dest = os.path.expanduser(dest.replace(('../',''))) raise AnsibleActionFail( "Detected directory traversal, expected to be contained in '%s' but got '%s'" % (original_dest, dest)) if flat: if os.path.isdir(to_bytes(dest, errors='surrogate_or_strict') ) and not dest.endswith(os.sep): raise AnsibleActionFail( "dest is an existing directory, use a trailing slash if you want to fetch src into that directory" ) if dest.endswith(os.sep): # if the path ends with "/", we'll use the source filename as the # destination filename base = os.path.basename(source_local) dest = os.path.join(dest, base) if not dest.startswith("/"): # if dest does not start with "/", we'll assume a relative path dest = self._loader.path_dwim(dest) else: # files are saved in dest dir, with a subdir for each host, then the filename if 'inventory_hostname' in task_vars: target_name = task_vars['inventory_hostname'] else: target_name = self._play_context.remote_addr dest = "%s/%s/%s" % (self._loader.path_dwim(dest), target_name, source_local) dest = os.path.normpath(dest) # calculate checksum for the local file local_checksum = checksum(dest) if remote_checksum != local_checksum: # create the containing directories, if needed makedirs_safe(os.path.dirname(dest)) # fetch the file and check for changes if remote_data is None: self._connection.fetch_file(source, dest) else: try: f = open(to_bytes(dest, errors='surrogate_or_strict'), 'wb') f.write(remote_data) f.close() except (IOError, OSError) as e: raise AnsibleActionFail( "Failed to fetch the file: %s" % e) new_checksum = secure_hash(dest) # For backwards compatibility. We'll return None on FIPS enabled systems try: new_md5 = md5(dest) except ValueError: new_md5 = None if validate_checksum and new_checksum != remote_checksum: result.update( dict(failed=True, md5sum=new_md5, msg="checksum mismatch", file=source, dest=dest, remote_md5sum=None, checksum=new_checksum, remote_checksum=remote_checksum)) else: result.update({ 'changed': True, 'md5sum': new_md5, 'dest': dest, 'remote_md5sum': None, 'checksum': new_checksum, 'remote_checksum': remote_checksum }) else: # For backwards compatibility. We'll return None on FIPS enabled systems try: local_md5 = md5(dest) except ValueError: local_md5 = None result.update( dict(changed=False, md5sum=local_md5, file=source, dest=dest, checksum=local_checksum)) finally: self._remove_tmp_path(self._connection._shell.tmpdir) return result
def run(self, tmp=None, task_vars=None): ''' handler for file transfer operations ''' if task_vars is None: task_vars = dict() result = super(ActionModule, self).run(tmp, task_vars) tmp = self._connection._shell.tempdir try: creates = self._task.args.get('creates') if creates: # do not run the command if the line contains creates=filename # and the filename already exists. This allows idempotence # of command executions. if self._remote_file_exists(creates): raise AnsibleActionSkip("%s exists, matching creates option" % creates) removes = self._task.args.get('removes') if removes: # do not run the command if the line contains removes=filename # and the filename does not exist. This allows idempotence # of command executions. if not self._remote_file_exists(removes): raise AnsibleActionSkip("%s does not exist, matching removes option" % removes) # The chdir must be absolute, because a relative path would rely on # remote node behaviour & user config. chdir = self._task.args.get('chdir') if chdir: # Powershell is the only Windows-path aware shell if self._connection._shell.SHELL_FAMILY == 'powershell' and \ not self.windows_absolute_path_detection.matches(chdir): raise AnsibleActionFail('chdir %s must be an absolute path for a Windows remote node' % chdir) # Every other shell is unix-path-aware. if self._connection._shell.SHELL_FAMILY != 'powershell' and not chdir.startswith('/'): raise AnsibleActionFail('chdir %s must be an absolute path for a Unix-aware remote node' % chdir) # Split out the script as the first item in raw_params using # shlex.split() in order to support paths and files with spaces in the name. # Any arguments passed to the script will be added back later. raw_params = to_native(self._task.args.get('_raw_params', ''), errors='surrogate_or_strict') parts = [to_text(s, errors='surrogate_or_strict') for s in shlex.split(raw_params.strip())] source = parts[0] try: source = self._loader.get_real_file(self._find_needle('files', source), decrypt=self._task.args.get('decrypt', True)) except AnsibleError as e: raise AnsibleActionFail(to_native(e)) # now we execute script, always assume changed. result['changed'] = True if not self._play_context.check_mode: # transfer the file to a remote tmp location tmp_src = self._connection._shell.join_path(tmp, os.path.basename(source)) # Convert raw_params to text for the purpose of replacing the script since # parts and tmp_src are both unicode strings and raw_params will be different # depending on Python version. # # Once everything is encoded consistently, replace the script path on the remote # system with the remainder of the raw_params. This preserves quoting in parameters # that would have been removed by shlex.split(). target_command = to_text(raw_params).strip().replace(parts[0], tmp_src) self._transfer_file(source, tmp_src) # set file permissions, more permissive when the copy is done as a different user self._fixup_perms2((tmp_src,), execute=True) # add preparation steps to one ssh roundtrip executing the script env_dict = dict() env_string = self._compute_environment_string(env_dict) script_cmd = ' '.join([env_string, target_command]) if self._play_context.check_mode: raise AnsibleActionDone() script_cmd = self._connection._shell.wrap_for_exec(script_cmd) exec_data = None # WinRM requires a special wrapper to work with environment variables if self._connection.transport == "winrm": pay = self._connection._create_raw_wrapper_payload(script_cmd, env_dict) exec_data = exec_wrapper.replace(b"$json_raw = ''", b"$json_raw = @'\r\n%s\r\n'@" % to_bytes(pay)) script_cmd = "-" result.update(self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir)) if 'rc' in result and result['rc'] != 0: raise AnsibleActionFail('non-zero return code') except AnsibleAction as e: result.update(e.result) finally: self._remove_tmp_path(tmp) return result
def run(self, tmp=None, task_vars=None): ''' handler for file transfer operations ''' if task_vars is None: task_vars = dict() result = super(ActionModule, self).run(tmp, task_vars) del tmp # tmp no longer has any effect try: creates = self._task.args.get('creates') if creates: # do not run the command if the line contains creates=filename # and the filename already exists. This allows idempotence # of command executions. if self._remote_file_exists(creates): raise AnsibleActionSkip( "%s exists, matching creates option" % creates) removes = self._task.args.get('removes') if removes: # do not run the command if the line contains removes=filename # and the filename does not exist. This allows idempotence # of command executions. if not self._remote_file_exists(removes): raise AnsibleActionSkip( "%s does not exist, matching removes option" % removes) # The chdir must be absolute, because a relative path would rely on # remote node behaviour & user config. chdir = self._task.args.get('chdir') if chdir: # Powershell is the only Windows-path aware shell if getattr(self._connection._shell, "_IS_WINDOWS", False) and \ not self.windows_absolute_path_detection.match(chdir): raise AnsibleActionFail( 'chdir %s must be an absolute path for a Windows remote node' % chdir) # Every other shell is unix-path-aware. if not getattr(self._connection._shell, "_IS_WINDOWS", False) and not chdir.startswith('/'): raise AnsibleActionFail( 'chdir %s must be an absolute path for a Unix-aware remote node' % chdir) # Split out the script as the first item in raw_params using # shlex.split() in order to support paths and files with spaces in the name. # Any arguments passed to the script will be added back later. raw_params = to_native(self._task.args.get('_raw_params', ''), errors='surrogate_or_strict') parts = [ to_text(s, errors='surrogate_or_strict') for s in shlex.split(raw_params.strip()) ] source = parts[0] # Support executable paths and files with spaces in the name. executable = to_native(self._task.args.get('executable', ''), errors='surrogate_or_strict') try: source = self._loader.get_real_file( self._find_needle('files', source), decrypt=self._task.args.get('decrypt', True)) except AnsibleError as e: raise AnsibleActionFail(to_native(e)) # now we execute script, always assume changed. result['changed'] = True if not self._play_context.check_mode: # transfer the file to a remote tmp location tmp_src = self._connection._shell.join_path( self._connection._shell.tmpdir, os.path.basename(source)) # Convert raw_params to text for the purpose of replacing the script since # parts and tmp_src are both unicode strings and raw_params will be different # depending on Python version. # # Once everything is encoded consistently, replace the script path on the remote # system with the remainder of the raw_params. This preserves quoting in parameters # that would have been removed by shlex.split(). target_command = to_text(raw_params).strip().replace( parts[0], tmp_src) self._transfer_file(source, tmp_src) # set file permissions, more permissive when the copy is done as a different user self._fixup_perms2((self._connection._shell.tmpdir, tmp_src), execute=True) # add preparation steps to one ssh roundtrip executing the script env_dict = dict() env_string = self._compute_environment_string(env_dict) if executable: script_cmd = ' '.join( [env_string, executable, target_command]) else: script_cmd = ' '.join([env_string, target_command]) if self._play_context.check_mode: raise _AnsibleActionDone() script_cmd = self._connection._shell.wrap_for_exec(script_cmd) exec_data = None # PowerShell runs the script in a special wrapper to enable things # like become and environment args if getattr(self._connection._shell, "_IS_WINDOWS", False): # FUTURE: use a more public method to get the exec payload pc = self._play_context exec_data = ps_manifest._create_powershell_wrapper( to_bytes(script_cmd), {}, env_dict, self._task.async_val, pc.become, pc.become_method, pc.become_user, pc.become_pass, pc.become_flags, substyle="script") # build the necessary exec wrapper command # FUTURE: this still doesn't let script work on Windows with non-pipelined connections or # full manual exec of KEEP_REMOTE_FILES script_cmd = self._connection._shell.build_module_command( env_string='', shebang='#!powershell', cmd='') result.update( self._low_level_execute_command(cmd=script_cmd, in_data=exec_data, sudoable=True, chdir=chdir)) if 'rc' in result and result['rc'] != 0: raise AnsibleActionFail('non-zero return code') except AnsibleAction as e: result.update(e.result) finally: self._remove_tmp_path(self._connection._shell.tmpdir) return result