mirror of
https://github.com/thegeeklab/ansible-later.git
synced 2024-11-18 10:50:40 +00:00
727 lines
31 KiB
Python
727 lines
31 KiB
Python
|
# (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 = """
|
||
|
author: Ansible Core Team
|
||
|
connection: winrm
|
||
|
short_description: Run tasks over Microsoft's WinRM
|
||
|
description:
|
||
|
- Run commands or put/fetch on a target via WinRM
|
||
|
- This plugin allows extra arguments to be passed that are supported by the protocol but not explicitly defined here.
|
||
|
They should take the form of variables declared with the following pattern `ansible_winrm_<option>`.
|
||
|
version_added: "2.0"
|
||
|
requirements:
|
||
|
- pywinrm (python library)
|
||
|
options:
|
||
|
# figure out more elegant 'delegation'
|
||
|
remote_addr:
|
||
|
description:
|
||
|
- Address of the windows machine
|
||
|
default: inventory_hostname
|
||
|
vars:
|
||
|
- name: ansible_host
|
||
|
- name: ansible_winrm_host
|
||
|
remote_user:
|
||
|
keywords:
|
||
|
- name: user
|
||
|
- name: remote_user
|
||
|
description:
|
||
|
- The user to log in as to the Windows machine
|
||
|
vars:
|
||
|
- name: ansible_user
|
||
|
- name: ansible_winrm_user
|
||
|
port:
|
||
|
description:
|
||
|
- port for winrm to connect on remote target
|
||
|
- The default is the https (5986) port, if using http it should be 5985
|
||
|
vars:
|
||
|
- name: ansible_port
|
||
|
- name: ansible_winrm_port
|
||
|
default: 5986
|
||
|
keywords:
|
||
|
- name: port
|
||
|
type: integer
|
||
|
scheme:
|
||
|
description:
|
||
|
- URI scheme to use
|
||
|
- If not set, then will default to C(https) or C(http) if I(port) is
|
||
|
C(5985).
|
||
|
choices: [http, https]
|
||
|
vars:
|
||
|
- name: ansible_winrm_scheme
|
||
|
path:
|
||
|
description: URI path to connect to
|
||
|
default: '/wsman'
|
||
|
vars:
|
||
|
- name: ansible_winrm_path
|
||
|
transport:
|
||
|
description:
|
||
|
- List of winrm transports to attempt to to use (ssl, plaintext, kerberos, etc)
|
||
|
- If None (the default) the plugin will try to automatically guess the correct list
|
||
|
- The choices avialable depend on your version of pywinrm
|
||
|
type: list
|
||
|
vars:
|
||
|
- name: ansible_winrm_transport
|
||
|
kerberos_command:
|
||
|
description: kerberos command to use to request a authentication ticket
|
||
|
default: kinit
|
||
|
vars:
|
||
|
- name: ansible_winrm_kinit_cmd
|
||
|
kerberos_mode:
|
||
|
description:
|
||
|
- kerberos usage mode.
|
||
|
- The managed option means Ansible will obtain kerberos ticket.
|
||
|
- While the manual one means a ticket must already have been obtained by the user.
|
||
|
- If having issues with Ansible freezing when trying to obtain the
|
||
|
Kerberos ticket, you can either set this to C(manual) and obtain
|
||
|
it outside Ansible or install C(pexpect) through pip and try
|
||
|
again.
|
||
|
choices: [managed, manual]
|
||
|
vars:
|
||
|
- name: ansible_winrm_kinit_mode
|
||
|
connection_timeout:
|
||
|
description:
|
||
|
- Sets the operation and read timeout settings for the WinRM
|
||
|
connection.
|
||
|
- Corresponds to the C(operation_timeout_sec) and
|
||
|
C(read_timeout_sec) args in pywinrm so avoid setting these vars
|
||
|
with this one.
|
||
|
- The default value is whatever is set in the installed version of
|
||
|
pywinrm.
|
||
|
vars:
|
||
|
- name: ansible_winrm_connection_timeout
|
||
|
"""
|
||
|
|
||
|
import base64
|
||
|
import os
|
||
|
import re
|
||
|
import traceback
|
||
|
import json
|
||
|
import tempfile
|
||
|
import subprocess
|
||
|
import xml.etree.ElementTree as ET
|
||
|
|
||
|
HAVE_KERBEROS = False
|
||
|
try:
|
||
|
import kerberos
|
||
|
HAVE_KERBEROS = True
|
||
|
except ImportError:
|
||
|
pass
|
||
|
|
||
|
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
||
|
from ansible.errors import AnsibleFileNotFound
|
||
|
from ansible.module_utils.json_utils import _filter_non_json_lines
|
||
|
from ansible.module_utils.parsing.convert_bool import boolean
|
||
|
from ansible.module_utils.six.moves.urllib.parse import urlunsplit
|
||
|
from ansible.module_utils._text import to_bytes, to_native, to_text
|
||
|
from ansible.module_utils.six import binary_type, PY3
|
||
|
from ansible.plugins.connection import ConnectionBase
|
||
|
from ansible.plugins.shell.powershell import leaf_exec
|
||
|
from ansible.utils.hashing import secure_hash
|
||
|
from ansible.utils.path import makedirs_safe
|
||
|
|
||
|
# getargspec is deprecated in favour of getfullargspec in Python 3 but
|
||
|
# getfullargspec is not available in Python 2
|
||
|
if PY3:
|
||
|
from inspect import getfullargspec as getargspec
|
||
|
else:
|
||
|
from inspect import getargspec
|
||
|
|
||
|
try:
|
||
|
import winrm
|
||
|
from winrm import Response
|
||
|
from winrm.protocol import Protocol
|
||
|
import requests.exceptions
|
||
|
HAS_WINRM = True
|
||
|
except ImportError as e:
|
||
|
HAS_WINRM = False
|
||
|
WINRM_IMPORT_ERR = e
|
||
|
|
||
|
try:
|
||
|
import xmltodict
|
||
|
HAS_XMLTODICT = True
|
||
|
except ImportError as e:
|
||
|
HAS_XMLTODICT = False
|
||
|
XMLTODICT_IMPORT_ERR = e
|
||
|
|
||
|
HAS_PEXPECT = False
|
||
|
try:
|
||
|
import pexpect
|
||
|
# echo was added in pexpect 3.3+ which is newer than the RHEL package
|
||
|
# we can only use pexpect for kerb auth if echo is a valid kwarg
|
||
|
# https://github.com/ansible/ansible/issues/43462
|
||
|
if hasattr(pexpect, 'spawn'):
|
||
|
argspec = getargspec(pexpect.spawn.__init__)
|
||
|
if 'echo' in argspec.args:
|
||
|
HAS_PEXPECT = True
|
||
|
except ImportError as e:
|
||
|
pass
|
||
|
|
||
|
# used to try and parse the hostname and detect if IPv6 is being used
|
||
|
try:
|
||
|
import ipaddress
|
||
|
HAS_IPADDRESS = True
|
||
|
except ImportError:
|
||
|
HAS_IPADDRESS = False
|
||
|
|
||
|
try:
|
||
|
from __main__ import display
|
||
|
except ImportError:
|
||
|
from ansible.utils.display import Display
|
||
|
display = Display()
|
||
|
|
||
|
|
||
|
class Connection(ConnectionBase):
|
||
|
'''WinRM connections over HTTP/HTTPS.'''
|
||
|
|
||
|
transport = 'winrm'
|
||
|
module_implementation_preferences = ('.ps1', '.exe', '')
|
||
|
become_methods = ['runas']
|
||
|
allow_executable = False
|
||
|
has_pipelining = True
|
||
|
allow_extras = True
|
||
|
|
||
|
def __init__(self, *args, **kwargs):
|
||
|
|
||
|
self.always_pipeline_modules = True
|
||
|
self.has_native_async = True
|
||
|
|
||
|
self.protocol = None
|
||
|
self.shell_id = None
|
||
|
self.delegate = None
|
||
|
self._shell_type = 'powershell'
|
||
|
|
||
|
super(Connection, self).__init__(*args, **kwargs)
|
||
|
|
||
|
def _build_winrm_kwargs(self):
|
||
|
# this used to be in set_options, as win_reboot needs to be able to
|
||
|
# override the conn timeout, we need to be able to build the args
|
||
|
# after setting individual options. This is called by _connect before
|
||
|
# starting the WinRM connection
|
||
|
self._winrm_host = self.get_option('remote_addr')
|
||
|
self._winrm_user = self.get_option('remote_user')
|
||
|
self._winrm_pass = self._play_context.password
|
||
|
|
||
|
self._become_method = self._play_context.become_method
|
||
|
self._become_user = self._play_context.become_user
|
||
|
self._become_pass = self._play_context.become_pass
|
||
|
|
||
|
self._winrm_port = self.get_option('port')
|
||
|
|
||
|
self._winrm_scheme = self.get_option('scheme')
|
||
|
# old behaviour, scheme should default to http if not set and the port
|
||
|
# is 5985 otherwise https
|
||
|
if self._winrm_scheme is None:
|
||
|
self._winrm_scheme = 'http' if self._winrm_port == 5985 else 'https'
|
||
|
|
||
|
self._winrm_path = self.get_option('path')
|
||
|
self._kinit_cmd = self.get_option('kerberos_command')
|
||
|
self._winrm_transport = self.get_option('transport')
|
||
|
self._winrm_connection_timeout = self.get_option('connection_timeout')
|
||
|
|
||
|
if hasattr(winrm, 'FEATURE_SUPPORTED_AUTHTYPES'):
|
||
|
self._winrm_supported_authtypes = set(winrm.FEATURE_SUPPORTED_AUTHTYPES)
|
||
|
else:
|
||
|
# for legacy versions of pywinrm, use the values we know are supported
|
||
|
self._winrm_supported_authtypes = set(['plaintext', 'ssl', 'kerberos'])
|
||
|
|
||
|
# calculate transport if needed
|
||
|
if self._winrm_transport is None or self._winrm_transport[0] is None:
|
||
|
# TODO: figure out what we want to do with auto-transport selection in the face of NTLM/Kerb/CredSSP/Cert/Basic
|
||
|
transport_selector = ['ssl'] if self._winrm_scheme == 'https' else ['plaintext']
|
||
|
|
||
|
if HAVE_KERBEROS and ((self._winrm_user and '@' in self._winrm_user)):
|
||
|
self._winrm_transport = ['kerberos'] + transport_selector
|
||
|
else:
|
||
|
self._winrm_transport = transport_selector
|
||
|
|
||
|
unsupported_transports = set(self._winrm_transport).difference(self._winrm_supported_authtypes)
|
||
|
|
||
|
if unsupported_transports:
|
||
|
raise AnsibleError('The installed version of WinRM does not support transport(s) %s' %
|
||
|
to_native(list(unsupported_transports), nonstring='simplerepr'))
|
||
|
|
||
|
# if kerberos is among our transports and there's a password specified, we're managing the tickets
|
||
|
kinit_mode = self.get_option('kerberos_mode')
|
||
|
if kinit_mode is None:
|
||
|
# HACK: ideally, remove multi-transport stuff
|
||
|
self._kerb_managed = "kerberos" in self._winrm_transport and (self._winrm_pass is not None and self._winrm_pass != "")
|
||
|
elif kinit_mode == "managed":
|
||
|
self._kerb_managed = True
|
||
|
elif kinit_mode == "manual":
|
||
|
self._kerb_managed = False
|
||
|
|
||
|
# arg names we're going passing directly
|
||
|
internal_kwarg_mask = set(['self', 'endpoint', 'transport', 'username', 'password', 'scheme', 'path', 'kinit_mode', 'kinit_cmd'])
|
||
|
|
||
|
self._winrm_kwargs = dict(username=self._winrm_user, password=self._winrm_pass)
|
||
|
argspec = getargspec(Protocol.__init__)
|
||
|
supported_winrm_args = set(argspec.args)
|
||
|
supported_winrm_args.update(internal_kwarg_mask)
|
||
|
passed_winrm_args = set([v.replace('ansible_winrm_', '') for v in self.get_option('_extras')])
|
||
|
unsupported_args = passed_winrm_args.difference(supported_winrm_args)
|
||
|
|
||
|
# warn for kwargs unsupported by the installed version of pywinrm
|
||
|
for arg in unsupported_args:
|
||
|
display.warning("ansible_winrm_{0} unsupported by pywinrm (is an up-to-date version of pywinrm installed?)".format(arg))
|
||
|
|
||
|
# pass through matching extras, excluding the list we want to treat specially
|
||
|
for arg in passed_winrm_args.difference(internal_kwarg_mask).intersection(supported_winrm_args):
|
||
|
self._winrm_kwargs[arg] = self.get_option('_extras')['ansible_winrm_%s' % arg]
|
||
|
|
||
|
# Until pykerberos has enough goodies to implement a rudimentary kinit/klist, simplest way is to let each connection
|
||
|
# auth itself with a private CCACHE.
|
||
|
def _kerb_auth(self, principal, password):
|
||
|
if password is None:
|
||
|
password = ""
|
||
|
|
||
|
self._kerb_ccache = tempfile.NamedTemporaryFile()
|
||
|
display.vvvvv("creating Kerberos CC at %s" % self._kerb_ccache.name)
|
||
|
krb5ccname = "FILE:%s" % self._kerb_ccache.name
|
||
|
os.environ["KRB5CCNAME"] = krb5ccname
|
||
|
krb5env = dict(KRB5CCNAME=krb5ccname)
|
||
|
|
||
|
# stores various flags to call with kinit, we currently only use this
|
||
|
# to set -f so we can get a forward-able ticket (cred delegation)
|
||
|
kinit_flags = []
|
||
|
if boolean(self.get_option('_extras').get('ansible_winrm_kerberos_delegation', False)):
|
||
|
kinit_flags.append('-f')
|
||
|
|
||
|
kinit_cmdline = [self._kinit_cmd]
|
||
|
kinit_cmdline.extend(kinit_flags)
|
||
|
kinit_cmdline.append(principal)
|
||
|
|
||
|
# pexpect runs the process in its own pty so it can correctly send
|
||
|
# the password as input even on MacOS which blocks subprocess from
|
||
|
# doing so. Unfortunately it is not available on the built in Python
|
||
|
# so we can only use it if someone has installed it
|
||
|
if HAS_PEXPECT:
|
||
|
proc_mechanism = "pexpect"
|
||
|
command = kinit_cmdline.pop(0)
|
||
|
password = to_text(password, encoding='utf-8',
|
||
|
errors='surrogate_or_strict')
|
||
|
|
||
|
display.vvvv("calling kinit with pexpect for principal %s"
|
||
|
% principal)
|
||
|
try:
|
||
|
child = pexpect.spawn(command, kinit_cmdline, timeout=60,
|
||
|
env=krb5env, echo=False)
|
||
|
except pexpect.ExceptionPexpect as err:
|
||
|
err_msg = "Kerberos auth failure when calling kinit cmd " \
|
||
|
"'%s': %s" % (command, to_native(err))
|
||
|
raise AnsibleConnectionFailure(err_msg)
|
||
|
|
||
|
try:
|
||
|
child.expect(".*:")
|
||
|
child.sendline(password)
|
||
|
except OSError as err:
|
||
|
# child exited before the pass was sent, Ansible will raise
|
||
|
# error based on the rc below, just display the error here
|
||
|
display.vvvv("kinit with pexpect raised OSError: %s"
|
||
|
% to_native(err))
|
||
|
|
||
|
# technically this is the stdout + stderr but to match the
|
||
|
# subprocess error checking behaviour, we will call it stderr
|
||
|
stderr = child.read()
|
||
|
child.wait()
|
||
|
rc = child.exitstatus
|
||
|
else:
|
||
|
proc_mechanism = "subprocess"
|
||
|
password = to_bytes(password, encoding='utf-8',
|
||
|
errors='surrogate_or_strict')
|
||
|
|
||
|
display.vvvv("calling kinit with subprocess for principal %s"
|
||
|
% principal)
|
||
|
try:
|
||
|
p = subprocess.Popen(kinit_cmdline, stdin=subprocess.PIPE,
|
||
|
stdout=subprocess.PIPE,
|
||
|
stderr=subprocess.PIPE,
|
||
|
env=krb5env)
|
||
|
|
||
|
except OSError as err:
|
||
|
err_msg = "Kerberos auth failure when calling kinit cmd " \
|
||
|
"'%s': %s" % (self._kinit_cmd, to_native(err))
|
||
|
raise AnsibleConnectionFailure(err_msg)
|
||
|
|
||
|
stdout, stderr = p.communicate(password + b'\n')
|
||
|
rc = p.returncode != 0
|
||
|
|
||
|
if rc != 0:
|
||
|
# one last attempt at making sure the password does not exist
|
||
|
# in the output
|
||
|
exp_msg = to_native(stderr.strip())
|
||
|
exp_msg = exp_msg.replace(to_native(password), "<redacted>")
|
||
|
|
||
|
err_msg = "Kerberos auth failure for principal %s with %s: %s" \
|
||
|
% (principal, proc_mechanism, exp_msg)
|
||
|
raise AnsibleConnectionFailure(err_msg)
|
||
|
|
||
|
display.vvvvv("kinit succeeded for principal %s" % principal)
|
||
|
|
||
|
def _winrm_connect(self):
|
||
|
'''
|
||
|
Establish a WinRM connection over HTTP/HTTPS.
|
||
|
'''
|
||
|
display.vvv("ESTABLISH WINRM CONNECTION FOR USER: %s on PORT %s TO %s" %
|
||
|
(self._winrm_user, self._winrm_port, self._winrm_host), host=self._winrm_host)
|
||
|
|
||
|
winrm_host = self._winrm_host
|
||
|
if HAS_IPADDRESS:
|
||
|
display.vvvv("checking if winrm_host %s is an IPv6 address" % winrm_host)
|
||
|
try:
|
||
|
ipaddress.IPv6Address(winrm_host)
|
||
|
except ipaddress.AddressValueError:
|
||
|
pass
|
||
|
else:
|
||
|
winrm_host = "[%s]" % winrm_host
|
||
|
|
||
|
netloc = '%s:%d' % (winrm_host, self._winrm_port)
|
||
|
endpoint = urlunsplit((self._winrm_scheme, netloc, self._winrm_path, '', ''))
|
||
|
errors = []
|
||
|
for transport in self._winrm_transport:
|
||
|
if transport == 'kerberos':
|
||
|
if not HAVE_KERBEROS:
|
||
|
errors.append('kerberos: the python kerberos library is not installed')
|
||
|
continue
|
||
|
if self._kerb_managed:
|
||
|
self._kerb_auth(self._winrm_user, self._winrm_pass)
|
||
|
display.vvvvv('WINRM CONNECT: transport=%s endpoint=%s' % (transport, endpoint), host=self._winrm_host)
|
||
|
try:
|
||
|
winrm_kwargs = self._winrm_kwargs.copy()
|
||
|
if self._winrm_connection_timeout:
|
||
|
winrm_kwargs['operation_timeout_sec'] = self._winrm_connection_timeout
|
||
|
winrm_kwargs['read_timeout_sec'] = self._winrm_connection_timeout + 1
|
||
|
protocol = Protocol(endpoint, transport=transport, **winrm_kwargs)
|
||
|
|
||
|
# open the shell from connect so we know we're able to talk to the server
|
||
|
if not self.shell_id:
|
||
|
self.shell_id = protocol.open_shell(codepage=65001) # UTF-8
|
||
|
display.vvvvv('WINRM OPEN SHELL: %s' % self.shell_id, host=self._winrm_host)
|
||
|
|
||
|
return protocol
|
||
|
except Exception as e:
|
||
|
err_msg = to_text(e).strip()
|
||
|
if re.search(to_text(r'Operation\s+?timed\s+?out'), err_msg, re.I):
|
||
|
raise AnsibleError('the connection attempt timed out')
|
||
|
m = re.search(to_text(r'Code\s+?(\d{3})'), err_msg)
|
||
|
if m:
|
||
|
code = int(m.groups()[0])
|
||
|
if code == 401:
|
||
|
err_msg = 'the specified credentials were rejected by the server'
|
||
|
elif code == 411:
|
||
|
return protocol
|
||
|
errors.append(u'%s: %s' % (transport, err_msg))
|
||
|
display.vvvvv(u'WINRM CONNECTION ERROR: %s\n%s' % (err_msg, to_text(traceback.format_exc())), host=self._winrm_host)
|
||
|
if errors:
|
||
|
raise AnsibleConnectionFailure(', '.join(map(to_native, errors)))
|
||
|
else:
|
||
|
raise AnsibleError('No transport found for WinRM connection')
|
||
|
|
||
|
def _winrm_send_input(self, protocol, shell_id, command_id, stdin, eof=False):
|
||
|
rq = {'env:Envelope': protocol._get_soap_header(
|
||
|
resource_uri='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd',
|
||
|
action='http://schemas.microsoft.com/wbem/wsman/1/windows/shell/Send',
|
||
|
shell_id=shell_id)}
|
||
|
stream = rq['env:Envelope'].setdefault('env:Body', {}).setdefault('rsp:Send', {})\
|
||
|
.setdefault('rsp:Stream', {})
|
||
|
stream['@Name'] = 'stdin'
|
||
|
stream['@CommandId'] = command_id
|
||
|
stream['#text'] = base64.b64encode(to_bytes(stdin))
|
||
|
if eof:
|
||
|
stream['@End'] = 'true'
|
||
|
protocol.send_message(xmltodict.unparse(rq))
|
||
|
|
||
|
def _winrm_exec(self, command, args=(), from_exec=False, stdin_iterator=None):
|
||
|
if not self.protocol:
|
||
|
self.protocol = self._winrm_connect()
|
||
|
self._connected = True
|
||
|
if from_exec:
|
||
|
display.vvvvv("WINRM EXEC %r %r" % (command, args), host=self._winrm_host)
|
||
|
else:
|
||
|
display.vvvvvv("WINRM EXEC %r %r" % (command, args), host=self._winrm_host)
|
||
|
command_id = None
|
||
|
try:
|
||
|
stdin_push_failed = False
|
||
|
command_id = self.protocol.run_command(self.shell_id, to_bytes(command), map(to_bytes, args), console_mode_stdin=(stdin_iterator is None))
|
||
|
|
||
|
try:
|
||
|
if stdin_iterator:
|
||
|
for (data, is_last) in stdin_iterator:
|
||
|
self._winrm_send_input(self.protocol, self.shell_id, command_id, data, eof=is_last)
|
||
|
|
||
|
except Exception as ex:
|
||
|
display.warning("ERROR DURING WINRM SEND INPUT - attempting to recover: %s %s"
|
||
|
% (type(ex).__name__, to_text(ex)))
|
||
|
display.debug(traceback.format_exc())
|
||
|
stdin_push_failed = True
|
||
|
|
||
|
# NB: this can hang if the receiver is still running (eg, network failed a Send request but the server's still happy).
|
||
|
# FUTURE: Consider adding pywinrm status check/abort operations to see if the target is still running after a failure.
|
||
|
resptuple = self.protocol.get_command_output(self.shell_id, command_id)
|
||
|
# ensure stdout/stderr are text for py3
|
||
|
# FUTURE: this should probably be done internally by pywinrm
|
||
|
response = Response(tuple(to_text(v) if isinstance(v, binary_type) else v for v in resptuple))
|
||
|
|
||
|
# TODO: check result from response and set stdin_push_failed if we have nonzero
|
||
|
if from_exec:
|
||
|
display.vvvvv('WINRM RESULT %r' % to_text(response), host=self._winrm_host)
|
||
|
else:
|
||
|
display.vvvvvv('WINRM RESULT %r' % to_text(response), host=self._winrm_host)
|
||
|
|
||
|
display.vvvvvv('WINRM STDOUT %s' % to_text(response.std_out), host=self._winrm_host)
|
||
|
display.vvvvvv('WINRM STDERR %s' % to_text(response.std_err), host=self._winrm_host)
|
||
|
|
||
|
if stdin_push_failed:
|
||
|
# There are cases where the stdin input failed but the WinRM service still processed it. We attempt to
|
||
|
# see if stdout contains a valid json return value so we can ignore this error
|
||
|
try:
|
||
|
filtered_output, dummy = _filter_non_json_lines(response.std_out)
|
||
|
json.loads(filtered_output)
|
||
|
except ValueError:
|
||
|
# stdout does not contain a return response, stdin input was a fatal error
|
||
|
stderr = to_bytes(response.std_err, encoding='utf-8')
|
||
|
if self.is_clixml(stderr):
|
||
|
stderr = self.parse_clixml_stream(stderr)
|
||
|
|
||
|
raise AnsibleError('winrm send_input failed; \nstdout: %s\nstderr %s'
|
||
|
% (to_native(response.std_out), to_native(stderr)))
|
||
|
|
||
|
return response
|
||
|
except requests.exceptions.Timeout as exc:
|
||
|
raise AnsibleConnectionFailure('winrm connection error: %s' % to_native(exc))
|
||
|
finally:
|
||
|
if command_id:
|
||
|
self.protocol.cleanup_command(self.shell_id, command_id)
|
||
|
|
||
|
def _connect(self):
|
||
|
|
||
|
if not HAS_WINRM:
|
||
|
raise AnsibleError("winrm or requests is not installed: %s" % to_native(WINRM_IMPORT_ERR))
|
||
|
elif not HAS_XMLTODICT:
|
||
|
raise AnsibleError("xmltodict is not installed: %s" % to_native(XMLTODICT_IMPORT_ERR))
|
||
|
|
||
|
super(Connection, self)._connect()
|
||
|
if not self.protocol:
|
||
|
self._build_winrm_kwargs() # build the kwargs from the options set
|
||
|
self.protocol = self._winrm_connect()
|
||
|
self._connected = True
|
||
|
return self
|
||
|
|
||
|
def reset(self):
|
||
|
self.protocol = None
|
||
|
self.shell_id = None
|
||
|
self._connect()
|
||
|
|
||
|
def _create_raw_wrapper_payload(self, cmd, environment=None):
|
||
|
environment = {} if environment is None else environment
|
||
|
|
||
|
payload = {
|
||
|
'module_entry': to_text(base64.b64encode(to_bytes(cmd))),
|
||
|
'powershell_modules': {},
|
||
|
'actions': ['exec'],
|
||
|
'exec': to_text(base64.b64encode(to_bytes(leaf_exec))),
|
||
|
'environment': environment,
|
||
|
'min_ps_version': None,
|
||
|
'min_os_version': None
|
||
|
}
|
||
|
|
||
|
return json.dumps(payload)
|
||
|
|
||
|
def _wrapper_payload_stream(self, payload, buffer_size=200000):
|
||
|
payload_bytes = to_bytes(payload)
|
||
|
byte_count = len(payload_bytes)
|
||
|
for i in range(0, byte_count, buffer_size):
|
||
|
yield payload_bytes[i:i + buffer_size], i + buffer_size >= byte_count
|
||
|
|
||
|
def exec_command(self, cmd, in_data=None, sudoable=True):
|
||
|
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
|
||
|
cmd_parts = self._shell._encode_script(cmd, as_list=True, strict_mode=False, preserve_rc=False)
|
||
|
|
||
|
# TODO: display something meaningful here
|
||
|
display.vvv("EXEC (via pipeline wrapper)")
|
||
|
|
||
|
stdin_iterator = None
|
||
|
|
||
|
if in_data:
|
||
|
stdin_iterator = self._wrapper_payload_stream(in_data)
|
||
|
|
||
|
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True, stdin_iterator=stdin_iterator)
|
||
|
|
||
|
result.std_out = to_bytes(result.std_out)
|
||
|
result.std_err = to_bytes(result.std_err)
|
||
|
|
||
|
# parse just stderr from CLIXML output
|
||
|
if self.is_clixml(result.std_err):
|
||
|
try:
|
||
|
result.std_err = self.parse_clixml_stream(result.std_err)
|
||
|
except Exception:
|
||
|
# unsure if we're guaranteed a valid xml doc- use raw output in case of error
|
||
|
pass
|
||
|
|
||
|
return (result.status_code, result.std_out, result.std_err)
|
||
|
|
||
|
def is_clixml(self, value):
|
||
|
return value.startswith(b"#< CLIXML\r\n")
|
||
|
|
||
|
# hacky way to get just stdout- not always sure of doc framing here, so use with care
|
||
|
def parse_clixml_stream(self, clixml_doc, stream_name='Error'):
|
||
|
clixml = ET.fromstring(clixml_doc.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_name]
|
||
|
return to_bytes('\r\n'.join(lines))
|
||
|
|
||
|
# FUTURE: determine buffer size at runtime via remote winrm config?
|
||
|
def _put_file_stdin_iterator(self, in_path, out_path, buffer_size=250000):
|
||
|
in_size = os.path.getsize(to_bytes(in_path, errors='surrogate_or_strict'))
|
||
|
offset = 0
|
||
|
with open(to_bytes(in_path, errors='surrogate_or_strict'), 'rb') as in_file:
|
||
|
for out_data in iter((lambda: in_file.read(buffer_size)), b''):
|
||
|
offset += len(out_data)
|
||
|
self._display.vvvvv('WINRM PUT "%s" to "%s" (offset=%d size=%d)' % (in_path, out_path, offset, len(out_data)), host=self._winrm_host)
|
||
|
# yes, we're double-encoding over the wire in this case- we want to ensure that the data shipped to the end PS pipeline is still b64-encoded
|
||
|
b64_data = base64.b64encode(out_data) + b'\r\n'
|
||
|
# cough up the data, as well as an indicator if this is the last chunk so winrm_send knows to set the End signal
|
||
|
yield b64_data, (in_file.tell() == in_size)
|
||
|
|
||
|
if offset == 0: # empty file, return an empty buffer + eof to close it
|
||
|
yield "", True
|
||
|
|
||
|
def put_file(self, in_path, out_path):
|
||
|
super(Connection, self).put_file(in_path, out_path)
|
||
|
out_path = self._shell._unquote(out_path)
|
||
|
display.vvv('PUT "%s" TO "%s"' % (in_path, out_path), host=self._winrm_host)
|
||
|
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
|
||
|
raise AnsibleFileNotFound('file or module does not exist: "%s"' % to_native(in_path))
|
||
|
|
||
|
script_template = u'''
|
||
|
begin {{
|
||
|
$path = '{0}'
|
||
|
|
||
|
$DebugPreference = "Continue"
|
||
|
$ErrorActionPreference = "Stop"
|
||
|
Set-StrictMode -Version 2
|
||
|
|
||
|
$fd = [System.IO.File]::Create($path)
|
||
|
|
||
|
$sha1 = [System.Security.Cryptography.SHA1CryptoServiceProvider]::Create()
|
||
|
|
||
|
$bytes = @() #initialize for empty file case
|
||
|
}}
|
||
|
process {{
|
||
|
$bytes = [System.Convert]::FromBase64String($input)
|
||
|
$sha1.TransformBlock($bytes, 0, $bytes.Length, $bytes, 0) | Out-Null
|
||
|
$fd.Write($bytes, 0, $bytes.Length)
|
||
|
}}
|
||
|
end {{
|
||
|
$sha1.TransformFinalBlock($bytes, 0, 0) | Out-Null
|
||
|
|
||
|
$hash = [System.BitConverter]::ToString($sha1.Hash).Replace("-", "").ToLowerInvariant()
|
||
|
|
||
|
$fd.Close()
|
||
|
|
||
|
Write-Output "{{""sha1"":""$hash""}}"
|
||
|
}}
|
||
|
'''
|
||
|
|
||
|
script = script_template.format(self._shell._escape(out_path))
|
||
|
cmd_parts = self._shell._encode_script(script, as_list=True, strict_mode=False, preserve_rc=False)
|
||
|
|
||
|
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], stdin_iterator=self._put_file_stdin_iterator(in_path, out_path))
|
||
|
# TODO: improve error handling
|
||
|
if result.status_code != 0:
|
||
|
raise AnsibleError(to_native(result.std_err))
|
||
|
|
||
|
put_output = json.loads(result.std_out)
|
||
|
remote_sha1 = put_output.get("sha1")
|
||
|
|
||
|
if not remote_sha1:
|
||
|
raise AnsibleError("Remote sha1 was not returned")
|
||
|
|
||
|
local_sha1 = secure_hash(in_path)
|
||
|
|
||
|
if not remote_sha1 == local_sha1:
|
||
|
raise AnsibleError("Remote sha1 hash {0} does not match local hash {1}".format(to_native(remote_sha1), to_native(local_sha1)))
|
||
|
|
||
|
def fetch_file(self, in_path, out_path):
|
||
|
super(Connection, self).fetch_file(in_path, out_path)
|
||
|
in_path = self._shell._unquote(in_path)
|
||
|
out_path = out_path.replace('\\', '/')
|
||
|
display.vvv('FETCH "%s" TO "%s"' % (in_path, out_path), host=self._winrm_host)
|
||
|
buffer_size = 2**19 # 0.5MB chunks
|
||
|
makedirs_safe(os.path.dirname(out_path))
|
||
|
out_file = None
|
||
|
try:
|
||
|
offset = 0
|
||
|
while True:
|
||
|
try:
|
||
|
script = '''
|
||
|
$path = "%(path)s"
|
||
|
If (Test-Path -Path $path -PathType Leaf)
|
||
|
{
|
||
|
$buffer_size = %(buffer_size)d
|
||
|
$offset = %(offset)d
|
||
|
|
||
|
$stream = New-Object -TypeName IO.FileStream($path, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
|
||
|
$stream.Seek($offset, [System.IO.SeekOrigin]::Begin) > $null
|
||
|
$buffer = New-Object -TypeName byte[] $buffer_size
|
||
|
$bytes_read = $stream.Read($buffer, 0, $buffer_size)
|
||
|
if ($bytes_read -gt 0) {
|
||
|
$bytes = $buffer[0..($bytes_read - 1)]
|
||
|
[System.Convert]::ToBase64String($bytes)
|
||
|
}
|
||
|
$stream.Close() > $null
|
||
|
}
|
||
|
ElseIf (Test-Path -Path $path -PathType Container)
|
||
|
{
|
||
|
Write-Host "[DIR]";
|
||
|
}
|
||
|
Else
|
||
|
{
|
||
|
Write-Error "$path does not exist";
|
||
|
Exit 1;
|
||
|
}
|
||
|
''' % dict(buffer_size=buffer_size, path=self._shell._escape(in_path), offset=offset)
|
||
|
display.vvvvv('WINRM FETCH "%s" to "%s" (offset=%d)' % (in_path, out_path, offset), host=self._winrm_host)
|
||
|
cmd_parts = self._shell._encode_script(script, as_list=True, preserve_rc=False)
|
||
|
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:])
|
||
|
if result.status_code != 0:
|
||
|
raise IOError(to_native(result.std_err))
|
||
|
if result.std_out.strip() == '[DIR]':
|
||
|
data = None
|
||
|
else:
|
||
|
data = base64.b64decode(result.std_out.strip())
|
||
|
if data is None:
|
||
|
makedirs_safe(out_path)
|
||
|
break
|
||
|
else:
|
||
|
if not out_file:
|
||
|
# If out_path is a directory and we're expecting a file, bail out now.
|
||
|
if os.path.isdir(to_bytes(out_path, errors='surrogate_or_strict')):
|
||
|
break
|
||
|
out_file = open(to_bytes(out_path, errors='surrogate_or_strict'), 'wb')
|
||
|
out_file.write(data)
|
||
|
if len(data) < buffer_size:
|
||
|
break
|
||
|
offset += len(data)
|
||
|
except Exception:
|
||
|
traceback.print_exc()
|
||
|
raise AnsibleError('failed to transfer file to "%s"' % to_native(out_path))
|
||
|
finally:
|
||
|
if out_file:
|
||
|
out_file.close()
|
||
|
|
||
|
def close(self):
|
||
|
if self.protocol and self.shell_id:
|
||
|
display.vvvvv('WINRM CLOSE SHELL: %s' % self.shell_id, host=self._winrm_host)
|
||
|
self.protocol.close_shell(self.shell_id)
|
||
|
self.shell_id = None
|
||
|
self.protocol = None
|
||
|
self._connected = False
|