From 6072dad15fe26614356221864a790760ca343213 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 15 Feb 2016 17:11:49 -0500 Subject: [PATCH] use stat module instead of checksum code - added new function for action plugins this avoids the very fragile checksum code that is shell dependant. - ported copy module to it - converted assemble to new stat function - some corrections and ported temlpate - updated old checksum function to use new stat one under the hood - documented revamped remote checksum method --- lib/ansible/plugins/action/__init__.py | 58 +++++++++++++++++++------- lib/ansible/plugins/action/assemble.py | 5 ++- lib/ansible/plugins/action/copy.py | 17 ++++---- lib/ansible/plugins/action/template.py | 19 ++++----- 4 files changed, 61 insertions(+), 38 deletions(-) diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index cacf6a83c52..643e8c9daef 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -291,28 +291,54 @@ class ActionBase(with_metaclass(ABCMeta, object)): res = self._low_level_execute_command(cmd, sudoable=sudoable) return res - def _remote_checksum(self, path, all_vars): + def _execute_remote_stat(self, path, all_vars, follow): ''' - Takes a remote checksum and returns 1 if no file + Get information from remote file. ''' + module_args=dict( + path=path, + follow=follow, + get_md5=False, + get_checksum=True, + checksum_algo='sha1', + ) + mystat = self._execute_module(module_name='stat', module_args=module_args, task_vars=all_vars) + + if 'failed' in mystat and mystat['failed']: + raise AnsibleError('Failed to get information on remote file (%s): %s' % (path, mystat['msg'])) + + if not mystat['stat']['exists']: + # empty might be matched, 1 should never match, also backwards compatible + mystat['stat']['checksum'] = '1' - python_interp = all_vars.get('ansible_python_interpreter', 'python') + return mystat['stat'] - cmd = self._connection._shell.checksum(path, python_interp) - data = self._low_level_execute_command(cmd, sudoable=True) + def _remote_checksum(self, path, all_vars): + ''' + Produces a remote checksum given a path, + Returns a number 0-4 for specific errors instead of checksum, also ensures it is different + 0 = unknown error + 1 = file does not exist, this might not be an error + 2 = permissions issue + 3 = its a directory, not a file + 4 = stat module failed, likely due to not finding python + ''' + x = "0" # unknown error has occured try: - data2 = data['stdout'].strip().splitlines()[-1] - if data2 == u'': - # this may happen if the connection to the remote server - # failed, so just return "INVALIDCHECKSUM" to avoid errors - return "INVALIDCHECKSUM" + remote_stat = self._execute_remote_stat(path, all_vars, follow=False) + if remote_stat['exists'] and remote_stat['isdir']: + x = "3" # its a directory not a file else: - return data2.split()[0] - except IndexError: - display.warning(u"Calculating checksum failed unusually, please report this to " - u"the list so it can be fixed\ncommand: %s\n----\noutput: %s\n----\n" % (to_unicode(cmd), data)) - # this will signal that it changed and allow things to keep going - return "INVALIDCHECKSUM" + x = remote_stat['checksum'] # if 1, file is missing + except AnsibleError as e: + errormsg = to_bytes(e) + if errormsg.endswith('Permission denied'): + x = "2" # cannot read file + elif errormsg.endswith('MODULE FAILURE'): + x = "4" # python not found or module uncaught exception + finally: + return x + def _remote_expand_user(self, path): ''' takes a remote path and performs tilde expansion on the remote host ''' diff --git a/lib/ansible/plugins/action/assemble.py b/lib/ansible/plugins/action/assemble.py index aae105400fd..4bbecbd25a0 100644 --- a/lib/ansible/plugins/action/assemble.py +++ b/lib/ansible/plugins/action/assemble.py @@ -89,6 +89,7 @@ class ActionModule(ActionBase): delimiter = self._task.args.get('delimiter', None) remote_src = self._task.args.get('remote_src', 'yes') regexp = self._task.args.get('regexp', None) + follow = self._task.args.get('follow', False) ignore_hidden = self._task.args.get('ignore_hidden', False) if src is None or dest is None: @@ -119,10 +120,10 @@ class ActionModule(ActionBase): path_checksum = checksum_s(path) dest = self._remote_expand_user(dest) - remote_checksum = self._remote_checksum(dest, all_vars=task_vars) + dest_stat = self._execute_remote_stat(dest, all_vars=task_vars, follow=follow) diff = {} - if path_checksum != remote_checksum: + if path_checksum != dest_stat['checksum']: resultant = file(path).read() if self._play_context.diff: diff --git a/lib/ansible/plugins/action/copy.py b/lib/ansible/plugins/action/copy.py index 17c22865300..f9cd4c59030 100644 --- a/lib/ansible/plugins/action/copy.py +++ b/lib/ansible/plugins/action/copy.py @@ -46,6 +46,7 @@ class ActionModule(ActionBase): force = boolean(self._task.args.get('force', 'yes')) faf = self._task.first_available_file remote_src = boolean(self._task.args.get('remote_src', False)) + follow = boolean(self._task.args.get('follow', False)) if (source is None and content is None and faf is None) or dest is None: result['failed'] = True @@ -167,11 +168,11 @@ class ActionModule(ActionBase): else: dest_file = self._connection._shell.join_path(dest) - # Attempt to get the remote checksum - remote_checksum = self._remote_checksum(dest_file, all_vars=task_vars) + # Attempt to get remote file info + dest_status = self._execute_remote_stat(dest_file, all_vars=task_vars, follow=follow) - if remote_checksum == '3': - # The remote_checksum was executed on a directory. + if dest_status['exists'] and dest_status['isdir']: + # The dest is a directory. if content is not None: # If source was defined as content remove the temporary file and fail out. self._remove_tempfile_if_content_defined(content, content_tempfile) @@ -179,15 +180,15 @@ class ActionModule(ActionBase): result['msg'] = "can not use content with a dir as dest" return result else: - # Append the relative source location to the destination and retry remote_checksum + # Append the relative source location to the destination and get remote stats again dest_file = self._connection._shell.join_path(dest, source_rel) - remote_checksum = self._remote_checksum(dest_file, all_vars=task_vars) + dest_status = self._execute_remote_stat(dest_file, all_vars=task_vars, follow=follow) - if remote_checksum != '1' and not force: + if not dest_status['exists'] and not force: # remote_file does not exist so continue to next iteration. continue - if local_checksum != remote_checksum: + if local_checksum != dest_status['checksum']: # The checksums don't match and we will change or error out. changed = True diff --git a/lib/ansible/plugins/action/template.py b/lib/ansible/plugins/action/template.py index d8339e57b90..c5c98861fb9 100644 --- a/lib/ansible/plugins/action/template.py +++ b/lib/ansible/plugins/action/template.py @@ -34,23 +34,18 @@ class ActionModule(ActionBase): TRANSFERS_FILES = True def get_checksum(self, dest, all_vars, try_directory=False, source=None): - remote_checksum = self._remote_checksum(dest, all_vars=all_vars) + try: + dest_stat = self._execute_remote_stat(dest, all_vars=all_vars, follow=False) - if remote_checksum in ('0', '2', '3', '4'): - # Note: 1 means the file is not present which is fine; template - # will create it. 3 means directory was specified instead of file - if try_directory and remote_checksum == '3' and source: + if dest_stat['exists'] and dest_stat['isdir'] and try_directory and source: base = os.path.basename(source) dest = os.path.join(dest, base) - remote_checksum = self.get_checksum(dest, all_vars=all_vars, try_directory=False) - if remote_checksum not in ('0', '2', '3', '4'): - return remote_checksum + dest_stat = self._execute_remote_stat(dest, all_vars=all_vars, follow=False) - result = dict(failed=True, msg="failed to checksum remote file." - " Checksum error code: %s" % remote_checksum) - return result + except Exception as e: + return dict(failed=True, msg=to_bytes(e)) - return remote_checksum + return dest_stat['checksum'] def run(self, tmp=None, task_vars=None): ''' handler for template operations '''