2014-11-14 22:14:08 +00:00
|
|
|
# (c) 2014, Chris Church <chris@ninemoreminutes.com>
|
|
|
|
#
|
|
|
|
# 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/>.
|
2015-04-13 20:28:01 +00:00
|
|
|
from __future__ import (absolute_import, division, print_function)
|
|
|
|
__metaclass__ = type
|
2014-11-14 22:14:08 +00:00
|
|
|
|
|
|
|
import base64
|
|
|
|
import os
|
|
|
|
import re
|
|
|
|
import random
|
|
|
|
import shlex
|
|
|
|
import time
|
|
|
|
|
2015-07-24 16:39:54 +00:00
|
|
|
from ansible.utils.unicode import to_bytes, to_unicode
|
|
|
|
|
|
|
|
_common_args = ['PowerShell', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Unrestricted']
|
2014-11-14 22:14:08 +00:00
|
|
|
|
|
|
|
# Primarily for testing, allow explicitly specifying PowerShell version via
|
|
|
|
# an environment variable.
|
|
|
|
_powershell_version = os.environ.get('POWERSHELL_VERSION', None)
|
|
|
|
if _powershell_version:
|
|
|
|
_common_args = ['PowerShell', '-Version', _powershell_version] + _common_args[1:]
|
|
|
|
|
|
|
|
class ShellModule(object):
|
|
|
|
|
|
|
|
def env_prefix(self, **kwargs):
|
|
|
|
return ''
|
|
|
|
|
|
|
|
def join_path(self, *args):
|
2015-07-24 16:39:54 +00:00
|
|
|
parts = []
|
|
|
|
for arg in args:
|
|
|
|
arg = self._unquote(arg).replace('/', '\\')
|
|
|
|
parts.extend([a for a in arg.split('\\') if a])
|
|
|
|
path = '\\'.join(parts)
|
|
|
|
if path.startswith('~'):
|
|
|
|
return path
|
|
|
|
return '"%s"' % path
|
2014-11-14 22:14:08 +00:00
|
|
|
|
|
|
|
def path_has_trailing_slash(self, path):
|
|
|
|
# Allow Windows paths to be specified using either slash.
|
2015-07-24 16:39:54 +00:00
|
|
|
path = self._unquote(path)
|
2014-11-14 22:14:08 +00:00
|
|
|
return path.endswith('/') or path.endswith('\\')
|
|
|
|
|
|
|
|
def chmod(self, mode, path):
|
|
|
|
return ''
|
|
|
|
|
|
|
|
def remove(self, path, recurse=False):
|
2015-07-24 16:39:54 +00:00
|
|
|
path = self._escape(self._unquote(path))
|
2014-11-14 22:14:08 +00:00
|
|
|
if recurse:
|
2015-04-27 05:28:25 +00:00
|
|
|
return self._encode_script('''Remove-Item "%s" -Force -Recurse;''' % path)
|
2014-11-14 22:14:08 +00:00
|
|
|
else:
|
2015-04-27 05:28:25 +00:00
|
|
|
return self._encode_script('''Remove-Item "%s" -Force;''' % path)
|
2014-11-14 22:14:08 +00:00
|
|
|
|
|
|
|
def mkdtemp(self, basefile, system=False, mode=None):
|
2015-07-24 16:39:54 +00:00
|
|
|
basefile = self._escape(self._unquote(basefile))
|
2014-11-14 22:14:08 +00:00
|
|
|
# FIXME: Support system temp path!
|
2015-04-27 05:28:25 +00:00
|
|
|
return self._encode_script('''(New-Item -Type Directory -Path $env:temp -Name "%s").FullName | Write-Host -Separator '';''' % basefile)
|
2014-11-14 22:14:08 +00:00
|
|
|
|
2015-06-29 19:41:51 +00:00
|
|
|
def expand_user(self, user_home_path):
|
|
|
|
# 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.
|
2015-07-24 16:39:54 +00:00
|
|
|
user_home_path = self._unquote(user_home_path)
|
2015-06-29 19:41:51 +00:00
|
|
|
if user_home_path == '~':
|
|
|
|
script = 'Write-Host (Get-Location).Path'
|
|
|
|
elif user_home_path.startswith('~\\'):
|
2015-07-24 16:39:54 +00:00
|
|
|
script = 'Write-Host ((Get-Location).Path + "%s")' % self._escape(user_home_path[1:])
|
2015-06-29 19:41:51 +00:00
|
|
|
else:
|
2015-07-24 16:39:54 +00:00
|
|
|
script = 'Write-Host "%s"' % self._escape(user_home_path)
|
2015-06-29 19:41:51 +00:00
|
|
|
return self._encode_script(script)
|
|
|
|
|
|
|
|
def checksum(self, path, *args, **kwargs):
|
2015-07-24 16:39:54 +00:00
|
|
|
path = self._escape(self._unquote(path))
|
2014-11-14 22:14:08 +00:00
|
|
|
script = '''
|
|
|
|
If (Test-Path -PathType Leaf "%(path)s")
|
|
|
|
{
|
2015-06-29 19:41:51 +00:00
|
|
|
$sp = new-object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider;
|
2014-11-14 22:14:08 +00:00
|
|
|
$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-Host "3";
|
|
|
|
}
|
|
|
|
Else
|
|
|
|
{
|
|
|
|
Write-Host "1";
|
|
|
|
}
|
|
|
|
''' % dict(path=path)
|
2015-04-27 05:28:25 +00:00
|
|
|
return self._encode_script(script)
|
2014-11-14 22:14:08 +00:00
|
|
|
|
Make sudo+requiretty and ANSIBLE_PIPELINING work together
Pipelining is a *significant* performance benefit, because each task can
be completed with a single SSH connection (vs. one ssh connection at the
start to mkdir, plus one sftp and one ssh per task).
Pipelining is disabled by default in Ansible because it conflicts with
the use of sudo if 'Defaults requiretty' is set in /etc/sudoers (as it
is on Red Hat) and su (which always requires a tty).
We can (and already do) make sudo/su happy by using "ssh -t" to allocate
a tty, but then the python interpreter goes into interactive mode and is
unhappy with module source being written to its stdin, per the following
comment from connections/ssh.py:
# we can only use tty when we are not pipelining the modules.
# piping data into /usr/bin/python inside a tty automatically
# invokes the python interactive-mode but the modules are not
# compatible with the interactive-mode ("unexpected indent"
# mainly because of empty lines)
Instead of the (current) drastic solution of turning off pipelining when
we use a tty, we can instead use a tty but suppress the behaviour of the
Python interpreter to switch to interactive mode. The easiest way to do
this is to make its stdin *not* be a tty, e.g. with cat|python.
This works, but there's a problem: ssh will ignore -t if its input isn't
really a tty. So we could open a pseudo-tty and use that as ssh's stdin,
but if we then write Python source into it, it's all echoed back to us
(because we're a tty). So we have to use -tt to force tty allocation; in
that case, however, ssh puts the tty into "raw" mode (~ICANON), so there
is no good way for the process on the other end to detect EOF on stdin.
So if we do:
echo -e "print('hello world')\n"|ssh -tt someho.st "cat|python"
…it hangs forever, because cat keeps on reading input even after we've
closed our pipe into ssh's stdin. We can get around this by writing a
special __EOF__ marker after writing in_data, and doing this:
echo -e "print('hello world')\n__EOF__\n"|ssh -tt someho.st "sed -ne '/__EOF__/q' -e p|python"
This works fine, but in fact I use a clever python one-liner by mgedmin
to achieve the same effect without depending on sed (at the expense of a
much longer command line, alas; Python really isn't one-liner-friendly).
We also enable pipelining by default as a consequence.
2015-11-05 12:01:31 +00:00
|
|
|
def build_module_command(self, env_string, shebang, cmd, arg_path=None, rm_tmp=None, python_interpreter=None):
|
2015-07-24 16:39:54 +00:00
|
|
|
cmd_parts = shlex.split(to_bytes(cmd), posix=False)
|
|
|
|
cmd_parts = map(to_unicode, cmd_parts)
|
|
|
|
if shebang and shebang.lower() == '#!powershell':
|
|
|
|
if not self._unquote(cmd_parts[0]).lower().endswith('.ps1'):
|
|
|
|
cmd_parts[0] = '"%s.ps1"' % self._unquote(cmd_parts[0])
|
|
|
|
cmd_parts.insert(0, '&')
|
|
|
|
elif shebang and shebang.startswith('#!'):
|
|
|
|
cmd_parts.insert(0, shebang[2:])
|
2015-08-22 22:19:43 +00:00
|
|
|
script = '''
|
|
|
|
Try
|
|
|
|
{
|
|
|
|
%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
|
|
|
|
}
|
|
|
|
''' % (' '.join(cmd_parts))
|
2014-11-14 22:14:08 +00:00
|
|
|
if rm_tmp:
|
2015-07-24 16:39:54 +00:00
|
|
|
rm_tmp = self._escape(self._unquote(rm_tmp))
|
|
|
|
rm_cmd = 'Remove-Item "%s" -Force -Recurse -ErrorAction SilentlyContinue' % rm_tmp
|
|
|
|
script = '%s\nFinally { %s }' % (script, rm_cmd)
|
2015-04-27 05:28:25 +00:00
|
|
|
return self._encode_script(script)
|
|
|
|
|
2015-07-24 16:39:54 +00:00
|
|
|
def _unquote(self, value):
|
|
|
|
'''Remove any matching quotes that wrap the given value.'''
|
2015-08-02 22:38:29 +00:00
|
|
|
value = to_unicode(value or '')
|
2015-07-24 16:39:54 +00:00
|
|
|
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
|
|
|
|
|
2015-04-27 05:28:25 +00:00
|
|
|
def _escape(self, value, include_vars=False):
|
|
|
|
'''Return value escaped for use in PowerShell command.'''
|
|
|
|
# http://www.techotopia.com/index.php/Windows_PowerShell_1.0_String_Quoting_and_Escape_Sequences
|
|
|
|
# http://stackoverflow.com/questions/764360/a-list-of-string-replacements-in-python
|
|
|
|
subs = [('\n', '`n'), ('\r', '`r'), ('\t', '`t'), ('\a', '`a'),
|
|
|
|
('\b', '`b'), ('\f', '`f'), ('\v', '`v'), ('"', '`"'),
|
|
|
|
('\'', '`\''), ('`', '``'), ('\x00', '`0')]
|
|
|
|
if include_vars:
|
|
|
|
subs.append(('$', '`$'))
|
|
|
|
pattern = '|'.join('(%s)' % re.escape(p) for p, s in subs)
|
|
|
|
substs = [s for p, s in subs]
|
|
|
|
replace = lambda m: substs[m.lastindex - 1]
|
|
|
|
return re.sub(pattern, replace, value)
|
|
|
|
|
2015-08-22 22:19:43 +00:00
|
|
|
def _encode_script(self, script, as_list=False, strict_mode=True):
|
2015-04-27 05:28:25 +00:00
|
|
|
'''Convert a PowerShell script to a single base64-encoded command.'''
|
2015-07-24 16:39:54 +00:00
|
|
|
script = to_unicode(script)
|
2015-08-22 22:19:43 +00:00
|
|
|
if strict_mode:
|
|
|
|
script = u'Set-StrictMode -Version Latest\r\n%s' % script
|
2015-04-27 05:28:25 +00:00
|
|
|
script = '\n'.join([x.strip() for x in script.splitlines() if x.strip()])
|
|
|
|
encoded_script = base64.b64encode(script.encode('utf-16-le'))
|
|
|
|
cmd_parts = _common_args + ['-EncodedCommand', encoded_script]
|
|
|
|
if as_list:
|
|
|
|
return cmd_parts
|
|
|
|
return ' '.join(cmd_parts)
|