Fixes for WinRM/PowerShell support in v2.

- Add support for inserting module args into PowerShell modules.  Fixes #11661.
- Support Windows paths containing spaces.  Applies changes from #10727 to v2.  Fixes #9999.  Should also fix ansible/ansible-modules-core#944 and ansible/ansible-modules-core#1007.
- Change how execution policy is set for running remote scripts.  Applies changes from #11092 to v2.  Also fixes ansible/ansible-modules-core#1776.
- Use codepage 65001 (UTF-8) for WinRM connection instead of default (CP437), convert command to UTF-8 and results from UTF-8.  Replaces changes from #10024.  Fixes #11198.
- Close WinRM connection when task completes.
- Use win_stat, win_file and win_copy modules instead of stat, file and copy when called from within other action plugins (only when using WinRM+PowerShell).
- Unquote Windows path arguments before passing to win_stat, win_file, win_copy and slurp modules (only when using WinRM/PowerShell).
- Check for win_ping module to determine if core modules are missing (only when using WinRM/PowerShell).
- Add stdout_lines to result from running low level commands (so stdout_lines is available when using raw/script).
- Update copy action plugin to use shell functions for joining paths and checking for trailing slash.
- Update fetch action plugin to unquote source path when using Windows paths.
- Add win_copy and win_template action plugins that inherit from copy and template.
- Support running .bat and .cmd scripts using default system encoding instead of UTF-8.
- Always send PowerShell commands as base64-encoded blobs to allow for running simple PowerShell commands via raw.
- Support running modules on Windows with interpreters other than PowerShell.
- Update integration tests to support above changes and test unicode fixes.
- Add test for win_user error from ansible/ansible-modules-core#1241 (fixed by ansible/ansible-modules-core#1774).
- Add test for additional win_stat output values (implemented by ansible/ansible-modules-core#1473).
- Add test for OS architecture and name from setup.ps1 (implemented by ansible/ansible-modules-core#1100).

All WinRM integration tests pass for me with these changes.
This commit is contained in:
Chris Church 2015-07-24 12:39:54 -04:00 committed by James Cammarata
parent fa8043c0ba
commit e87cf4a3cc
25 changed files with 356 additions and 118 deletions

View file

@ -24,7 +24,9 @@ import random
import shlex
import time
_common_args = ['PowerShell', '-NoProfile', '-NonInteractive']
from ansible.utils.unicode import to_bytes, to_unicode
_common_args = ['PowerShell', '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Unrestricted']
# Primarily for testing, allow explicitly specifying PowerShell version via
# an environment variable.
@ -38,24 +40,32 @@ class ShellModule(object):
return ''
def join_path(self, *args):
return os.path.join(*args).replace('/', '\\')
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
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, mode, path):
return ''
def remove(self, path, recurse=False):
path = self._escape(path)
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, system=False, mode=None):
basefile = self._escape(basefile)
basefile = self._escape(self._unquote(basefile))
# FIXME: Support system temp path!
return self._encode_script('''(New-Item -Type Directory -Path $env:temp -Name "%s").FullName | Write-Host -Separator '';''' % basefile)
@ -63,16 +73,17 @@ class ShellModule(object):
# 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-Host (Get-Location).Path'
elif user_home_path.startswith('~\\'):
script = 'Write-Host ((Get-Location).Path + "%s")' % _escape(user_home_path[1:])
script = 'Write-Host ((Get-Location).Path + "%s")' % self._escape(user_home_path[1:])
else:
script = 'Write-Host "%s"' % _escape(user_home_path)
script = 'Write-Host "%s"' % self._escape(user_home_path)
return self._encode_script(script)
def checksum(self, path, *args, **kwargs):
path = self._escape(path)
path = self._escape(self._unquote(path))
script = '''
If (Test-Path -PathType Leaf "%(path)s")
{
@ -93,16 +104,36 @@ class ShellModule(object):
return self._encode_script(script)
def build_module_command(self, env_string, shebang, cmd, rm_tmp=None):
cmd = cmd.encode('utf-8')
cmd_parts = shlex.split(cmd, posix=False)
if not cmd_parts[0].lower().endswith('.ps1'):
cmd_parts[0] = '%s.ps1' % cmd_parts[0]
script = self._build_file_cmd(cmd_parts)
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:])
catch = '''
$_obj = @{ failed = $true; $msg = $_ }
echo $_obj | ConvertTo-Json -Compress -Depth 99
Exit 1
'''
script = 'Try { %s }\nCatch { %s }' % (' '.join(cmd_parts), 'throw')
if rm_tmp:
rm_tmp = self._escape(rm_tmp)
script = '%s; Remove-Item "%s" -Force -Recurse;' % (script, rm_tmp)
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)
return self._encode_script(script)
def _unquote(self, value):
'''Remove any matching quotes that wrap the given value.'''
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, 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
@ -119,14 +150,10 @@ class ShellModule(object):
def _encode_script(self, script, as_list=False):
'''Convert a PowerShell script to a single base64-encoded command.'''
script = to_unicode(script)
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)
def _build_file_cmd(self, cmd_parts):
'''Build command line to run a file, given list of file name plus args.'''
return ' '.join(_common_args + ['-ExecutionPolicy', 'Unrestricted', '-File'] + ['"%s"' % x for x in cmd_parts])