diff options
Diffstat (limited to 'lib/ansible/plugins/shell')
-rw-r--r-- | lib/ansible/plugins/shell/__init__.py | 239 | ||||
-rw-r--r-- | lib/ansible/plugins/shell/cmd.py | 57 | ||||
-rw-r--r-- | lib/ansible/plugins/shell/powershell.py | 287 | ||||
-rw-r--r-- | lib/ansible/plugins/shell/sh.py | 79 |
4 files changed, 662 insertions, 0 deletions
diff --git a/lib/ansible/plugins/shell/__init__.py b/lib/ansible/plugins/shell/__init__.py new file mode 100644 index 0000000..d5db261 --- /dev/null +++ b/lib/ansible/plugins/shell/__init__.py @@ -0,0 +1,239 @@ +# (c) 2016 RedHat +# +# This file is part of Ansible. +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import os.path +import random +import re +import shlex +import time + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible.module_utils.six import text_type, string_types +from ansible.module_utils.common._collections_compat import Mapping, Sequence +from ansible.plugins import AnsiblePlugin + +_USER_HOME_PATH_RE = re.compile(r'^~[_.A-Za-z0-9][-_.A-Za-z0-9]*$') + + +class ShellBase(AnsiblePlugin): + def __init__(self): + + super(ShellBase, self).__init__() + + self.env = {} + self.tmpdir = None + self.executable = None + + def _normalize_system_tmpdirs(self): + # Normalize the tmp directory strings. We don't use expanduser/expandvars because those + # can vary between remote user and become user. Therefore the safest practice will be for + # this to always be specified as full paths) + normalized_paths = [d.rstrip('/') for d in self.get_option('system_tmpdirs')] + + # Make sure all system_tmpdirs are absolute otherwise they'd be relative to the login dir + # which is almost certainly going to fail in a cornercase. + if not all(os.path.isabs(d) for d in normalized_paths): + raise AnsibleError('The configured system_tmpdirs contains a relative path: {0}. All' + ' system_tmpdirs must be absolute'.format(to_native(normalized_paths))) + + self.set_option('system_tmpdirs', normalized_paths) + + def set_options(self, task_keys=None, var_options=None, direct=None): + + super(ShellBase, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) + + # set env if needed, deal with environment's 'dual nature' list of dicts or dict + # TODO: config system should already resolve this so we should be able to just iterate over dicts + env = self.get_option('environment') + if isinstance(env, string_types): + raise AnsibleError('The "envirionment" keyword takes a list of dictionaries or a dictionary, not a string') + if not isinstance(env, Sequence): + env = [env] + for env_dict in env: + if not isinstance(env_dict, Mapping): + raise AnsibleError('The "envirionment" keyword takes a list of dictionaries (or single dictionary), but got a "%s" instead' % type(env_dict)) + self.env.update(env_dict) + + # We can remove the try: except in the future when we make ShellBase a proper subset of + # *all* shells. Right now powershell and third party shells which do not use the + # shell_common documentation fragment (and so do not have system_tmpdirs) will fail + try: + self._normalize_system_tmpdirs() + except KeyError: + pass + + @staticmethod + def _generate_temp_dir_name(): + return 'ansible-tmp-%s-%s-%s' % (time.time(), os.getpid(), random.randint(0, 2**48)) + + def env_prefix(self, **kwargs): + return ' '.join(['%s=%s' % (k, shlex.quote(text_type(v))) for k, v in kwargs.items()]) + + def join_path(self, *args): + return os.path.join(*args) + + # some shells (eg, powershell) are snooty about filenames/extensions, this lets the shell plugin have a say + def get_remote_filename(self, pathname): + base_name = os.path.basename(pathname.strip()) + return base_name.strip() + + def path_has_trailing_slash(self, path): + return path.endswith('/') + + def chmod(self, paths, mode): + cmd = ['chmod', mode] + cmd.extend(paths) + cmd = [shlex.quote(c) for c in cmd] + + return ' '.join(cmd) + + def chown(self, paths, user): + cmd = ['chown', user] + cmd.extend(paths) + cmd = [shlex.quote(c) for c in cmd] + + return ' '.join(cmd) + + def chgrp(self, paths, group): + cmd = ['chgrp', group] + cmd.extend(paths) + cmd = [shlex.quote(c) for c in cmd] + + return ' '.join(cmd) + + def set_user_facl(self, paths, user, mode): + """Only sets acls for users as that's really all we need""" + cmd = ['setfacl', '-m', 'u:%s:%s' % (user, mode)] + cmd.extend(paths) + cmd = [shlex.quote(c) for c in cmd] + + return ' '.join(cmd) + + def remove(self, path, recurse=False): + path = shlex.quote(path) + cmd = 'rm -f ' + if recurse: + cmd += '-r ' + return cmd + "%s %s" % (path, self._SHELL_REDIRECT_ALLNULL) + + def exists(self, path): + cmd = ['test', '-e', shlex.quote(path)] + return ' '.join(cmd) + + def mkdtemp(self, basefile=None, system=False, mode=0o700, tmpdir=None): + if not basefile: + basefile = self.__class__._generate_temp_dir_name() + + # When system is specified we have to create this in a directory where + # other users can read and access the tmp directory. + # This is because we use system to create tmp dirs for unprivileged users who are + # sudo'ing to a second unprivileged user. + # The 'system_tmpdirs' setting defines dirctories we can use for this purpose + # the default are, /tmp and /var/tmp. + # So we only allow one of those locations if system=True, using the + # passed in tmpdir if it is valid or the first one from the setting if not. + + if system: + if tmpdir: + tmpdir = tmpdir.rstrip('/') + + if tmpdir in self.get_option('system_tmpdirs'): + basetmpdir = tmpdir + else: + basetmpdir = self.get_option('system_tmpdirs')[0] + else: + if tmpdir is None: + basetmpdir = self.get_option('remote_tmp') + else: + basetmpdir = tmpdir + + basetmp = self.join_path(basetmpdir, basefile) + + # use mkdir -p to ensure parents exist, but mkdir fullpath to ensure last one is created by us + cmd = 'mkdir -p %s echo %s %s' % (self._SHELL_SUB_LEFT, basetmpdir, self._SHELL_SUB_RIGHT) + cmd += '%s mkdir %s echo %s %s' % (self._SHELL_AND, self._SHELL_SUB_LEFT, basetmp, self._SHELL_SUB_RIGHT) + cmd += ' %s echo %s=%s echo %s %s' % (self._SHELL_AND, basefile, self._SHELL_SUB_LEFT, basetmp, self._SHELL_SUB_RIGHT) + + # change the umask in a subshell to achieve the desired mode + # also for directories created with `mkdir -p` + if mode: + tmp_umask = 0o777 & ~mode + cmd = '%s umask %o %s %s %s' % (self._SHELL_GROUP_LEFT, tmp_umask, self._SHELL_AND, cmd, self._SHELL_GROUP_RIGHT) + + return cmd + + def expand_user(self, user_home_path, username=''): + ''' Return a command to expand tildes in a path + + It can be either "~" or "~username". We just ignore $HOME + We use the POSIX definition of a username: + http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap03.html#tag_03_426 + http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap03.html#tag_03_276 + + Falls back to 'current working directory' as we assume 'home is where the remote user ends up' + ''' + + # Check that the user_path to expand is safe + if user_home_path != '~': + if not _USER_HOME_PATH_RE.match(user_home_path): + # shlex.quote will make the shell return the string verbatim + user_home_path = shlex.quote(user_home_path) + elif username: + # if present the user name is appended to resolve "that user's home" + user_home_path += username + + return 'echo %s' % user_home_path + + def pwd(self): + """Return the working directory after connecting""" + return 'echo %spwd%s' % (self._SHELL_SUB_LEFT, self._SHELL_SUB_RIGHT) + + def build_module_command(self, env_string, shebang, cmd, arg_path=None): + # don't quote the cmd if it's an empty string, because this will break pipelining mode + if cmd.strip() != '': + cmd = shlex.quote(cmd) + + cmd_parts = [] + if shebang: + shebang = shebang.replace("#!", "").strip() + else: + shebang = "" + cmd_parts.extend([env_string.strip(), shebang, cmd]) + if arg_path is not None: + cmd_parts.append(arg_path) + new_cmd = " ".join(cmd_parts) + return new_cmd + + def append_command(self, cmd, cmd_to_append): + """Append an additional command if supported by the shell""" + + if self._SHELL_AND: + cmd += ' %s %s' % (self._SHELL_AND, cmd_to_append) + + return cmd + + def wrap_for_exec(self, cmd): + """wrap script execution with any necessary decoration (eg '&' for quoted powershell script paths)""" + return cmd + + def quote(self, cmd): + """Returns a shell-escaped string that can be safely used as one token in a shell command line""" + return shlex.quote(cmd) diff --git a/lib/ansible/plugins/shell/cmd.py b/lib/ansible/plugins/shell/cmd.py new file mode 100644 index 0000000..c1083dc --- /dev/null +++ b/lib/ansible/plugins/shell/cmd.py @@ -0,0 +1,57 @@ +# Copyright (c) 2019 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' +name: cmd +version_added: '2.8' +short_description: Windows Command Prompt +description: +- Used with the 'ssh' connection plugin and no C(DefaultShell) has been set on the Windows host. +extends_documentation_fragment: +- shell_windows +''' + +import re + +from ansible.plugins.shell.powershell import ShellModule as PSShellModule + +# these are the metachars that have a special meaning in cmd that we want to escape when quoting +_find_unsafe = re.compile(r'[\s\(\)\%\!^\"\<\>\&\|]').search + + +class ShellModule(PSShellModule): + + # Common shell filenames that this plugin handles + COMPATIBLE_SHELLS = frozenset() # type: frozenset[str] + # Family of shells this has. Must match the filename without extension + SHELL_FAMILY = 'cmd' + + _SHELL_REDIRECT_ALLNULL = '>nul 2>&1' + _SHELL_AND = '&&' + + # Used by various parts of Ansible to do Windows specific changes + _IS_WINDOWS = True + + def quote(self, s): + # cmd does not support single quotes that the shlex_quote uses. We need to override the quoting behaviour to + # better match cmd.exe. + # https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/ + + # Return an empty argument + if not s: + return '""' + + if _find_unsafe(s) is None: + return s + + # Escape the metachars as we are quoting the string to stop cmd from interpreting that metachar. For example + # 'file &whoami.exe' would result in 'file $(whoami.exe)' instead of the literal string + # https://stackoverflow.com/questions/3411771/multiple-character-replace-with-python + for c in '^()%!"<>&|': # '^' must be the first char that we scan and replace + if c in s: + # I can't find any docs that explicitly say this but to escape ", it needs to be prefixed with \^. + s = s.replace(c, ("\\^" if c == '"' else "^") + c) + + return '^"' + s + '^"' diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py new file mode 100644 index 0000000..de5e705 --- /dev/null +++ b/lib/ansible/plugins/shell/powershell.py @@ -0,0 +1,287 @@ +# Copyright (c) 2014, Chris Church <chris@ninemoreminutes.com> +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' +name: powershell +version_added: historical +short_description: Windows PowerShell +description: +- The only option when using 'winrm' or 'psrp' as a connection plugin. +- Can also be used when using 'ssh' as a connection plugin and the C(DefaultShell) has been configured to PowerShell. +extends_documentation_fragment: +- shell_windows +''' + +import base64 +import os +import re +import shlex +import pkgutil +import xml.etree.ElementTree as ET +import ntpath + +from ansible.module_utils._text import to_bytes, to_text +from ansible.plugins.shell import ShellBase + + +_common_args = ['PowerShell', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Unrestricted'] + + +def _parse_clixml(data, stream="Error"): + """ + Takes a byte string like '#< CLIXML\r\n<Objs...' and extracts the stream + message encoded in the XML data. CLIXML is used by PowerShell to encode + multiple objects in stderr. + """ + lines = [] + + # There are some scenarios where the stderr contains a nested CLIXML element like + # '<# CLIXML\r\n<# CLIXML\r\n<Objs>...</Objs><Objs>...</Objs>'. + # Parse each individual <Objs> element and add the error strings to our stderr list. + # https://github.com/ansible/ansible/issues/69550 + while data: + end_idx = data.find(b"</Objs>") + 7 + current_element = data[data.find(b"<Objs "):end_idx] + data = data[end_idx:] + + clixml = ET.fromstring(current_element) + namespace_match = re.match(r'{(.*)}', clixml.tag) + namespace = "{%s}" % namespace_match.group(1) if namespace_match else "" + + strings = clixml.findall("./%sS" % namespace) + lines.extend([e.text.replace('_x000D__x000A_', '') for e in strings if e.attrib.get('S') == stream]) + + return to_bytes('\r\n'.join(lines)) + + +class ShellModule(ShellBase): + + # Common shell filenames that this plugin handles + # Powershell is handled differently. It's selected when winrm is the + # connection + COMPATIBLE_SHELLS = frozenset() # type: frozenset[str] + # Family of shells this has. Must match the filename without extension + SHELL_FAMILY = 'powershell' + + _SHELL_REDIRECT_ALLNULL = '> $null' + _SHELL_AND = ';' + + # Used by various parts of Ansible to do Windows specific changes + _IS_WINDOWS = True + + # TODO: add binary module support + + def env_prefix(self, **kwargs): + # powershell/winrm env handling is handled in the exec wrapper + return "" + + def join_path(self, *args): + # use normpath() to remove doubled slashed and convert forward to backslashes + parts = [ntpath.normpath(self._unquote(arg)) for arg in args] + + # Because ntpath.join treats any component that begins with a backslash as an absolute path, + # we have to strip slashes from at least the beginning, otherwise join will ignore all previous + # path components except for the drive. + return ntpath.join(parts[0], *[part.strip('\\') for part in parts[1:]]) + + def get_remote_filename(self, pathname): + # powershell requires that script files end with .ps1 + base_name = os.path.basename(pathname.strip()) + name, ext = os.path.splitext(base_name.strip()) + if ext.lower() not in ['.ps1', '.exe']: + return name + '.ps1' + + return base_name.strip() + + def path_has_trailing_slash(self, path): + # Allow Windows paths to be specified using either slash. + path = self._unquote(path) + return path.endswith('/') or path.endswith('\\') + + def chmod(self, paths, mode): + raise NotImplementedError('chmod is not implemented for Powershell') + + def chown(self, paths, user): + raise NotImplementedError('chown is not implemented for Powershell') + + def set_user_facl(self, paths, user, mode): + raise NotImplementedError('set_user_facl is not implemented for Powershell') + + def remove(self, path, recurse=False): + path = self._escape(self._unquote(path)) + if recurse: + return self._encode_script('''Remove-Item '%s' -Force -Recurse;''' % path) + else: + return self._encode_script('''Remove-Item '%s' -Force;''' % path) + + def mkdtemp(self, basefile=None, system=False, mode=None, tmpdir=None): + # Windows does not have an equivalent for the system temp files, so + # the param is ignored + if not basefile: + basefile = self.__class__._generate_temp_dir_name() + basefile = self._escape(self._unquote(basefile)) + basetmpdir = tmpdir if tmpdir else self.get_option('remote_tmp') + + script = ''' + $tmp_path = [System.Environment]::ExpandEnvironmentVariables('%s') + $tmp = New-Item -Type Directory -Path $tmp_path -Name '%s' + Write-Output -InputObject $tmp.FullName + ''' % (basetmpdir, basefile) + return self._encode_script(script.strip()) + + def expand_user(self, user_home_path, username=''): + # PowerShell only supports "~" (not "~username"). Resolve-Path ~ does + # not seem to work remotely, though by default we are always starting + # in the user's home directory. + user_home_path = self._unquote(user_home_path) + if user_home_path == '~': + script = 'Write-Output (Get-Location).Path' + elif user_home_path.startswith('~\\'): + script = "Write-Output ((Get-Location).Path + '%s')" % self._escape(user_home_path[1:]) + else: + script = "Write-Output '%s'" % self._escape(user_home_path) + return self._encode_script(script) + + def exists(self, path): + path = self._escape(self._unquote(path)) + script = ''' + If (Test-Path '%s') + { + $res = 0; + } + Else + { + $res = 1; + } + Write-Output '$res'; + Exit $res; + ''' % path + return self._encode_script(script) + + def checksum(self, path, *args, **kwargs): + path = self._escape(self._unquote(path)) + script = ''' + If (Test-Path -PathType Leaf '%(path)s') + { + $sp = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider; + $fp = [System.IO.File]::Open('%(path)s', [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read); + [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower(); + $fp.Dispose(); + } + ElseIf (Test-Path -PathType Container '%(path)s') + { + Write-Output "3"; + } + Else + { + Write-Output "1"; + } + ''' % dict(path=path) + return self._encode_script(script) + + def build_module_command(self, env_string, shebang, cmd, arg_path=None): + bootstrap_wrapper = pkgutil.get_data("ansible.executor.powershell", "bootstrap_wrapper.ps1") + + # pipelining bypass + if cmd == '': + return self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False) + + # non-pipelining + + cmd_parts = shlex.split(cmd, posix=False) + cmd_parts = list(map(to_text, cmd_parts)) + if shebang and shebang.lower() == '#!powershell': + if not self._unquote(cmd_parts[0]).lower().endswith('.ps1'): + # we're running a module via the bootstrap wrapper + cmd_parts[0] = '"%s.ps1"' % self._unquote(cmd_parts[0]) + wrapper_cmd = "type " + cmd_parts[0] + " | " + self._encode_script(script=bootstrap_wrapper, strict_mode=False, preserve_rc=False) + return wrapper_cmd + elif shebang and shebang.startswith('#!'): + cmd_parts.insert(0, shebang[2:]) + elif not shebang: + # The module is assumed to be a binary + cmd_parts[0] = self._unquote(cmd_parts[0]) + cmd_parts.append(arg_path) + script = ''' + Try + { + %s + %s + } + Catch + { + $_obj = @{ failed = $true } + If ($_.Exception.GetType) + { + $_obj.Add('msg', $_.Exception.Message) + } + Else + { + $_obj.Add('msg', $_.ToString()) + } + If ($_.InvocationInfo.PositionMessage) + { + $_obj.Add('exception', $_.InvocationInfo.PositionMessage) + } + ElseIf ($_.ScriptStackTrace) + { + $_obj.Add('exception', $_.ScriptStackTrace) + } + Try + { + $_obj.Add('error_record', ($_ | ConvertTo-Json | ConvertFrom-Json)) + } + Catch + { + } + Echo $_obj | ConvertTo-Json -Compress -Depth 99 + Exit 1 + } + ''' % (env_string, ' '.join(cmd_parts)) + return self._encode_script(script, preserve_rc=False) + + def wrap_for_exec(self, cmd): + return '& %s; exit $LASTEXITCODE' % cmd + + def _unquote(self, value): + '''Remove any matching quotes that wrap the given value.''' + value = to_text(value or '') + m = re.match(r'^\s*?\'(.*?)\'\s*?$', value) + if m: + return m.group(1) + m = re.match(r'^\s*?"(.*?)"\s*?$', value) + if m: + return m.group(1) + return value + + def _escape(self, value): + '''Return value escaped for use in PowerShell single quotes.''' + # There are 5 chars that need to be escaped in a single quote. + # https://github.com/PowerShell/PowerShell/blob/b7cb335f03fe2992d0cbd61699de9d9aafa1d7c1/src/System.Management.Automation/engine/parser/CharTraits.cs#L265-L272 + return re.compile(u"(['\u2018\u2019\u201a\u201b])").sub(u'\\1\\1', value) + + def _encode_script(self, script, as_list=False, strict_mode=True, preserve_rc=True): + '''Convert a PowerShell script to a single base64-encoded command.''' + script = to_text(script) + + if script == u'-': + cmd_parts = _common_args + ['-Command', '-'] + + else: + if strict_mode: + script = u'Set-StrictMode -Version Latest\r\n%s' % script + # try to propagate exit code if present- won't work with begin/process/end-style scripts (ala put_file) + # NB: the exit code returned may be incorrect in the case of a successful command followed by an invalid command + if preserve_rc: + script = u'%s\r\nIf (-not $?) { If (Get-Variable LASTEXITCODE -ErrorAction SilentlyContinue) { exit $LASTEXITCODE } Else { exit 1 } }\r\n'\ + % script + script = '\n'.join([x.strip() for x in script.splitlines() if x.strip()]) + encoded_script = to_text(base64.b64encode(script.encode('utf-16-le')), 'utf-8') + cmd_parts = _common_args + ['-EncodedCommand', encoded_script] + + if as_list: + return cmd_parts + return ' '.join(cmd_parts) diff --git a/lib/ansible/plugins/shell/sh.py b/lib/ansible/plugins/shell/sh.py new file mode 100644 index 0000000..146c466 --- /dev/null +++ b/lib/ansible/plugins/shell/sh.py @@ -0,0 +1,79 @@ +# Copyright (c) 2014, Chris Church <chris@ninemoreminutes.com> +# Copyright (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' +name: sh +short_description: "POSIX shell (/bin/sh)" +version_added: historical +description: + - This shell plugin is the one you want to use on most Unix systems, it is the most compatible and widely installed shell. +extends_documentation_fragment: + - shell_common +''' + +import shlex + +from ansible.plugins.shell import ShellBase + + +class ShellModule(ShellBase): + + # Common shell filenames that this plugin handles. + # Note: sh is the default shell plugin so this plugin may also be selected + # This code needs to be SH-compliant. BASH-isms will not work if /bin/sh points to a non-BASH shell. + + # if the filename is not listed in any Shell plugin. + COMPATIBLE_SHELLS = frozenset(('sh', 'zsh', 'bash', 'dash', 'ksh')) + # Family of shells this has. Must match the filename without extension + SHELL_FAMILY = 'sh' + + # commonly used + ECHO = 'echo' + COMMAND_SEP = ';' + + # How to end lines in a python script one-liner + _SHELL_EMBEDDED_PY_EOL = '\n' + _SHELL_REDIRECT_ALLNULL = '> /dev/null 2>&1' + _SHELL_AND = '&&' + _SHELL_OR = '||' + _SHELL_SUB_LEFT = '"`' + _SHELL_SUB_RIGHT = '`"' + _SHELL_GROUP_LEFT = '(' + _SHELL_GROUP_RIGHT = ')' + + def checksum(self, path, python_interp): + # In the following test, each condition is a check and logical + # comparison (|| or &&) that sets the rc value. Every check is run so + # the last check in the series to fail will be the rc that is returned. + # + # If a check fails we error before invoking the hash functions because + # hash functions may successfully take the hash of a directory on BSDs + # (UFS filesystem?) which is not what the rest of the ansible code expects + # + # If all of the available hashing methods fail we fail with an rc of 0. + # This logic is added to the end of the cmd at the bottom of this function. + + # Return codes: + # checksum: success! + # 0: Unknown error + # 1: Remote file does not exist + # 2: No read permissions on the file + # 3: File is a directory + # 4: No python interpreter + + # Quoting gets complex here. We're writing a python string that's + # used by a variety of shells on the remote host to invoke a python + # "one-liner". + shell_escaped_path = shlex.quote(path) + test = "rc=flag; [ -r %(p)s ] %(shell_or)s rc=2; [ -f %(p)s ] %(shell_or)s rc=1; [ -d %(p)s ] %(shell_and)s rc=3; %(i)s -V 2>/dev/null %(shell_or)s rc=4; [ x\"$rc\" != \"xflag\" ] %(shell_and)s echo \"${rc} \"%(p)s %(shell_and)s exit 0" % dict(p=shell_escaped_path, i=python_interp, shell_and=self._SHELL_AND, shell_or=self._SHELL_OR) # NOQA + csums = [ + u"({0} -c 'import hashlib; BLOCKSIZE = 65536; hasher = hashlib.sha1();{2}afile = open(\"'{1}'\", \"rb\"){2}buf = afile.read(BLOCKSIZE){2}while len(buf) > 0:{2}\thasher.update(buf){2}\tbuf = afile.read(BLOCKSIZE){2}afile.close(){2}print(hasher.hexdigest())' 2>/dev/null)".format(python_interp, shell_escaped_path, self._SHELL_EMBEDDED_PY_EOL), # NOQA Python > 2.4 (including python3) + u"({0} -c 'import sha; BLOCKSIZE = 65536; hasher = sha.sha();{2}afile = open(\"'{1}'\", \"rb\"){2}buf = afile.read(BLOCKSIZE){2}while len(buf) > 0:{2}\thasher.update(buf){2}\tbuf = afile.read(BLOCKSIZE){2}afile.close(){2}print(hasher.hexdigest())' 2>/dev/null)".format(python_interp, shell_escaped_path, self._SHELL_EMBEDDED_PY_EOL), # NOQA Python == 2.4 + ] + + cmd = (" %s " % self._SHELL_OR).join(csums) + cmd = "%s; %s %s (echo \'0 \'%s)" % (test, cmd, self._SHELL_OR, shell_escaped_path) + return cmd |