summaryrefslogtreecommitdiffstats
path: root/lib/ansible/plugins/shell
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/plugins/shell')
-rw-r--r--lib/ansible/plugins/shell/__init__.py239
-rw-r--r--lib/ansible/plugins/shell/cmd.py57
-rw-r--r--lib/ansible/plugins/shell/powershell.py287
-rw-r--r--lib/ansible/plugins/shell/sh.py79
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