Add support for Windows hosts in the SSH connection plugin (#47732)

* Add support for Windows hosts in the SSH connection plugin

* fix Python 2.6 unit test and sanity issues

* fix up connection tests in CI, disable SCP for now

* ensure we don't pollute the existing environment during the test

* Add connection_windows_ssh to classifier

* use test dir for inventory file

* Required powershell as default shell and fix tests

* Remove exlicit become_methods on connection

* clarify console encoding comment

* ignore recent SCP errors in integration tests

* Add cmd shell type and added more tests

* Fix some doc issues

* revises windows faq

* add anchors for windows links

* revises windows setup page

* Update changelogs/fragments/windows-ssh.yaml

Co-Authored-By: jborean93 <jborean93@gmail.com>
This commit is contained in:
Jordan Borean
2019-03-08 10:38:02 +10:00
committed by Matt Davis
parent cdf475e830
commit 8ef2e6da05
24 changed files with 657 additions and 143 deletions

View File

@@ -221,3 +221,7 @@ class ShellBase(AnsiblePlugin):
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)

View File

@@ -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
plugin_type: shell
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()
# 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:
s = s.replace(c, "^" + c)
return '^"' + s + '^"'

View File

@@ -5,60 +5,26 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
name: powershell
plugin_type: shell
version_added: ""
short_description: Windows Powershell
description:
- The only option when using 'winrm' as a connection plugin
options:
async_dir:
description:
- Directory in which ansible will keep async job information.
- Before Ansible 2.8, this was set to C(remote_tmp + "\\.ansible_async").
default: '%USERPROFILE%\\.ansible_async'
ini:
- section: powershell
key: async_dir
vars:
- name: ansible_async_dir
version_added: '2.8'
remote_tmp:
description:
- Temporary directory to use on targets when copying files to the host.
default: '%TEMP%'
ini:
- section: powershell
key: remote_tmp
vars:
- name: ansible_remote_tmp
set_module_language:
description:
- Controls if we set the locale for moduels when executing on the
target.
- Windows only supports C(no) as an option.
type: bool
default: 'no'
choices:
- 'no'
environment:
description:
- Dictionary of environment variables and their values to use when
executing commands.
type: dict
default: {}
name: powershell
plugin_type: shell
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
'''
# FIXME: admin_users and set_module_language don't belong here but must be set
# so they don't failk when someone get_option('admin_users') on this plugin
import base64
import os
import re
import shlex
import pkgutil
import xml.etree.ElementTree as ET
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_text
from ansible.module_utils._text import to_bytes, to_text
from ansible.plugins.shell import ShellBase
@@ -71,6 +37,21 @@ if _powershell_version:
_common_args = ['PowerShell', '-Version', _powershell_version] + _common_args[1:]
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.
"""
clixml = ET.fromstring(data.split(b"\r\n", 1)[-1])
namespace_match = re.match(r'{(.*)}', clixml.tag)
namespace = "{%s}" % namespace_match.group(1) if namespace_match else ""
strings = clixml.findall("./%sS" % namespace)
lines = [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
@@ -80,6 +61,12 @@ class ShellModule(ShellBase):
# 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
env = dict()
# We're being overly cautious about which keys to accept (more so than