mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
winrm connection tweaks for pywinrm (#15584)
added warnings for invalid kwargs sniff supported authtypes (for new pywinrm) use default authtypes (for old pywinrm) error on unsupported authtype allow no username/password to be specified (kerb SSO) tested w/ old and new pywinrm hacky CLIXML parsing of stderr
This commit is contained in:
parent
4e0013d161
commit
8bf1c53b21
1 changed files with 68 additions and 21 deletions
|
@ -29,8 +29,9 @@ import xmltodict
|
||||||
|
|
||||||
from ansible.compat.six.moves.urllib.parse import urlunsplit
|
from ansible.compat.six.moves.urllib.parse import urlunsplit
|
||||||
|
|
||||||
from ansible.errors import AnsibleError
|
from ansible.errors import AnsibleError, AnsibleConnectionFailure
|
||||||
try:
|
try:
|
||||||
|
import winrm
|
||||||
from winrm import Response
|
from winrm import Response
|
||||||
from winrm.exceptions import WinRMTransportError
|
from winrm.exceptions import WinRMTransportError
|
||||||
from winrm.protocol import Protocol
|
from winrm.protocol import Protocol
|
||||||
|
@ -74,7 +75,7 @@ class Connection(ConnectionBase):
|
||||||
self.delegate = None
|
self.delegate = None
|
||||||
self._shell_type = 'powershell'
|
self._shell_type = 'powershell'
|
||||||
|
|
||||||
# TODO: Add runas support
|
# FUTURE: Add runas support
|
||||||
|
|
||||||
super(Connection, self).__init__(*args, **kwargs)
|
super(Connection, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -91,15 +92,16 @@ class Connection(ConnectionBase):
|
||||||
self._winrm_user = self._play_context.remote_user
|
self._winrm_user = self._play_context.remote_user
|
||||||
self._winrm_pass = self._play_context.password
|
self._winrm_pass = self._play_context.password
|
||||||
|
|
||||||
if '@' in self._winrm_user:
|
if hasattr(winrm, 'FEATURE_SUPPORTED_AUTHTYPES'):
|
||||||
self._winrm_realm = self._winrm_user.split('@', 1)[1].strip() or None
|
self._winrm_supported_authtypes = set(winrm.FEATURE_SUPPORTED_AUTHTYPES)
|
||||||
else:
|
else:
|
||||||
self._winrm_realm = None
|
# for legacy versions of pywinrm, use the values we know are supported
|
||||||
self._winrm_realm = host_vars.get('ansible_winrm_realm', self._winrm_realm) or None
|
self._winrm_supported_authtypes = set(['plaintext','ssl','kerberos'])
|
||||||
|
|
||||||
|
# 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'
|
transport_selector = 'ssl' if self._winrm_scheme == 'https' else 'plaintext'
|
||||||
|
|
||||||
if HAVE_KERBEROS and ('@' in self._winrm_user or self._winrm_realm):
|
if HAVE_KERBEROS and ((self._winrm_user and '@' in self._winrm_user)):
|
||||||
self._winrm_transport = 'kerberos,%s' % transport_selector
|
self._winrm_transport = 'kerberos,%s' % transport_selector
|
||||||
else:
|
else:
|
||||||
self._winrm_transport = transport_selector
|
self._winrm_transport = transport_selector
|
||||||
|
@ -107,12 +109,26 @@ class Connection(ConnectionBase):
|
||||||
if isinstance(self._winrm_transport, basestring):
|
if isinstance(self._winrm_transport, basestring):
|
||||||
self._winrm_transport = [x.strip() for x in self._winrm_transport.split(',') if x.strip()]
|
self._winrm_transport = [x.strip() for x in self._winrm_transport.split(',') if x.strip()]
|
||||||
|
|
||||||
self._winrm_kwargs = dict(username=self._winrm_user, password=self._winrm_pass, realm=self._winrm_realm)
|
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' % list(unsupported_transports))
|
||||||
|
|
||||||
|
self._winrm_kwargs = dict(username=self._winrm_user, password=self._winrm_pass)
|
||||||
argspec = inspect.getargspec(Protocol.__init__)
|
argspec = inspect.getargspec(Protocol.__init__)
|
||||||
for arg in argspec.args:
|
supported_winrm_args = set(argspec.args)
|
||||||
if arg in ('self', 'endpoint', 'transport', 'username', 'password', 'realm'):
|
passed_winrm_args = set([v.replace('ansible_winrm_', '') for v in host_vars if v.startswith('ansible_winrm_')])
|
||||||
continue
|
unsupported_args = passed_winrm_args.difference(supported_winrm_args)
|
||||||
if 'ansible_winrm_%s' % arg in host_vars:
|
|
||||||
|
# warn for kwargs unsupported by the installed version of pywinrm
|
||||||
|
for arg in unsupported_args:
|
||||||
|
display.warning("ansible_winrm_{0} unsupported by pywinrm (are you running the right pywinrm version?)".format(arg))
|
||||||
|
|
||||||
|
# arg names we're going passing directly
|
||||||
|
internal_kwarg_mask = set(['self', 'endpoint', 'transport', 'username', 'password'])
|
||||||
|
|
||||||
|
# pass through matching kwargs, 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] = host_vars['ansible_winrm_%s' % arg]
|
self._winrm_kwargs[arg] = host_vars['ansible_winrm_%s' % arg]
|
||||||
|
|
||||||
def _winrm_connect(self):
|
def _winrm_connect(self):
|
||||||
|
@ -131,7 +147,13 @@ class Connection(ConnectionBase):
|
||||||
display.vvvvv('WINRM CONNECT: transport=%s endpoint=%s' % (transport, endpoint), host=self._winrm_host)
|
display.vvvvv('WINRM CONNECT: transport=%s endpoint=%s' % (transport, endpoint), host=self._winrm_host)
|
||||||
try:
|
try:
|
||||||
protocol = Protocol(endpoint, transport=transport, **self._winrm_kwargs)
|
protocol = Protocol(endpoint, transport=transport, **self._winrm_kwargs)
|
||||||
protocol.send_message('')
|
# send keepalive message to ensure we're awake
|
||||||
|
# TODO: is this necessary?
|
||||||
|
# protocol.send_message(xmltodict.unparse(rq))
|
||||||
|
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
|
return protocol
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg = to_unicode(e).strip()
|
err_msg = to_unicode(e).strip()
|
||||||
|
@ -147,7 +169,7 @@ class Connection(ConnectionBase):
|
||||||
errors.append(u'%s: %s' % (transport, err_msg))
|
errors.append(u'%s: %s' % (transport, err_msg))
|
||||||
display.vvvvv(u'WINRM CONNECTION ERROR: %s\n%s' % (err_msg, to_unicode(traceback.format_exc())), host=self._winrm_host)
|
display.vvvvv(u'WINRM CONNECTION ERROR: %s\n%s' % (err_msg, to_unicode(traceback.format_exc())), host=self._winrm_host)
|
||||||
if errors:
|
if errors:
|
||||||
raise AnsibleError(', '.join(map(to_str, errors)))
|
raise AnsibleConnectionFailure(', '.join(map(to_str, errors)))
|
||||||
else:
|
else:
|
||||||
raise AnsibleError('No transport found for WinRM connection')
|
raise AnsibleError('No transport found for WinRM connection')
|
||||||
|
|
||||||
|
@ -169,9 +191,6 @@ class Connection(ConnectionBase):
|
||||||
if not self.protocol:
|
if not self.protocol:
|
||||||
self.protocol = self._winrm_connect()
|
self.protocol = self._winrm_connect()
|
||||||
self._connected = True
|
self._connected = True
|
||||||
if not self.shell_id:
|
|
||||||
self.shell_id = self.protocol.open_shell(codepage=65001) # UTF-8
|
|
||||||
display.vvvvv('WINRM OPEN SHELL: %s' % self.shell_id, host=self._winrm_host)
|
|
||||||
if from_exec:
|
if from_exec:
|
||||||
display.vvvvv("WINRM EXEC %r %r" % (command, args), host=self._winrm_host)
|
display.vvvvv("WINRM EXEC %r %r" % (command, args), host=self._winrm_host)
|
||||||
else:
|
else:
|
||||||
|
@ -186,12 +205,20 @@ class Connection(ConnectionBase):
|
||||||
if stdin_iterator:
|
if stdin_iterator:
|
||||||
for (data, is_last) in stdin_iterator:
|
for (data, is_last) in stdin_iterator:
|
||||||
self._winrm_send_input(self.protocol, self.shell_id, command_id, data, eof=is_last)
|
self._winrm_send_input(self.protocol, self.shell_id, command_id, data, eof=is_last)
|
||||||
except:
|
|
||||||
|
except Exception as ex:
|
||||||
|
from traceback import format_exc
|
||||||
|
display.warning("FATAL ERROR DURING FILE TRANSFER: %s" % format_exc(ex))
|
||||||
stdin_push_failed = True
|
stdin_push_failed = True
|
||||||
|
|
||||||
# NB: this could hang if the receiver is still running (eg, network failed a Send request but the server's still happy).
|
if stdin_push_failed:
|
||||||
|
raise AnsibleError('winrm send_input failed')
|
||||||
|
|
||||||
|
# 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.
|
# FUTURE: Consider adding pywinrm status check/abort operations to see if the target is still running after a failure.
|
||||||
response = Response(self.protocol.get_command_output(self.shell_id, command_id))
|
response = Response(self.protocol.get_command_output(self.shell_id, command_id))
|
||||||
|
|
||||||
|
# TODO: check result from response and set stdin_push_failed if we have nonzero
|
||||||
if from_exec:
|
if from_exec:
|
||||||
display.vvvvv('WINRM RESULT %r' % to_unicode(response), host=self._winrm_host)
|
display.vvvvv('WINRM RESULT %r' % to_unicode(response), host=self._winrm_host)
|
||||||
else:
|
else:
|
||||||
|
@ -199,6 +226,7 @@ class Connection(ConnectionBase):
|
||||||
display.vvvvvv('WINRM STDOUT %s' % to_unicode(response.std_out), host=self._winrm_host)
|
display.vvvvvv('WINRM STDOUT %s' % to_unicode(response.std_out), host=self._winrm_host)
|
||||||
display.vvvvvv('WINRM STDERR %s' % to_unicode(response.std_err), host=self._winrm_host)
|
display.vvvvvv('WINRM STDERR %s' % to_unicode(response.std_err), host=self._winrm_host)
|
||||||
|
|
||||||
|
|
||||||
if stdin_push_failed:
|
if stdin_push_failed:
|
||||||
raise AnsibleError('winrm send_input failed; \nstdout: %s\nstderr %s' % (response.std_out, response.std_err))
|
raise AnsibleError('winrm send_input failed; \nstdout: %s\nstderr %s' % (response.std_out, response.std_err))
|
||||||
|
|
||||||
|
@ -245,11 +273,30 @@ class Connection(ConnectionBase):
|
||||||
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True)
|
result = self._winrm_exec(cmd_parts[0], cmd_parts[1:], from_exec=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
raise AnsibleError("failed to exec cmd %s" % cmd)
|
raise AnsibleConnectionFailure("failed to exec cmd %s" % cmd)
|
||||||
result.std_out = to_bytes(result.std_out)
|
result.std_out = to_bytes(result.std_out)
|
||||||
result.std_err = to_bytes(result.std_err)
|
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:
|
||||||
|
# unsure if we're guaranteed a valid xml doc- keep original output just in case
|
||||||
|
pass
|
||||||
|
|
||||||
return (result.status_code, result.std_out, result.std_err)
|
return (result.status_code, result.std_out, result.std_err)
|
||||||
|
|
||||||
|
def is_clixml(self, value):
|
||||||
|
return value.startswith("#< CLIXML")
|
||||||
|
|
||||||
|
# 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'):
|
||||||
|
clear_xml = clixml_doc.replace('#< CLIXML\r\n', '')
|
||||||
|
doc = xmltodict.parse(clear_xml)
|
||||||
|
lines = [l.get('#text', '') for l in doc.get('Objs', {}).get('S', {}) if l.get('@S') == stream_name]
|
||||||
|
return '\r\n'.join(lines)
|
||||||
|
|
||||||
# FUTURE: determine buffer size at runtime via remote winrm config?
|
# FUTURE: determine buffer size at runtime via remote winrm config?
|
||||||
def _put_file_stdin_iterator(self, in_path, out_path, buffer_size=250000):
|
def _put_file_stdin_iterator(self, in_path, out_path, buffer_size=250000):
|
||||||
in_size = os.path.getsize(to_bytes(in_path, errors='strict'))
|
in_size = os.path.getsize(to_bytes(in_path, errors='strict'))
|
||||||
|
|
Loading…
Reference in a new issue