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-23 23:54:48 +00:00
2015-04-27 15:46:26 +00:00
from __future__ import ( absolute_import , division , print_function )
__metaclass__ = type
2014-11-14 22:14:08 +00:00
import base64
2015-08-23 05:54:46 +00:00
import inspect
2014-11-14 22:14:08 +00:00
import os
import re
import shlex
import traceback
2015-10-23 21:37:50 +00:00
import json
import xmltodict
2014-11-14 22:14:08 +00:00
2015-10-16 00:55:23 +00:00
from ansible . compat . six . moves . urllib . parse import urlunsplit
2015-09-17 14:44:26 +00:00
from ansible . errors import AnsibleError
2014-11-14 22:14:08 +00:00
try :
from winrm import Response
from winrm . exceptions import WinRMTransportError
from winrm . protocol import Protocol
except ImportError :
2015-04-27 05:28:25 +00:00
raise AnsibleError ( " winrm is not installed " )
2014-11-14 22:14:08 +00:00
2015-04-23 23:54:48 +00:00
HAVE_KERBEROS = False
try :
import kerberos
HAVE_KERBEROS = True
except ImportError :
pass
2014-11-14 22:14:08 +00:00
2015-11-11 15:10:14 +00:00
from ansible . errors import AnsibleFileNotFound
2015-09-15 15:57:54 +00:00
from ansible . plugins . connection import ConnectionBase
2015-10-23 21:37:50 +00:00
from ansible . utils . hashing import secure_hash
2015-06-03 03:42:00 +00:00
from ansible . utils . path import makedirs_safe
2015-10-21 14:59:46 +00:00
from ansible . utils . unicode import to_bytes , to_unicode , to_str
2015-10-09 00:37:24 +00:00
from ansible . utils . vars import combine_vars
2014-11-14 22:14:08 +00:00
2015-11-11 15:10:14 +00:00
try :
from __main__ import display
except ImportError :
from ansible . utils . display import Display
display = Display ( )
2015-04-27 05:28:25 +00:00
class Connection ( ConnectionBase ) :
2014-11-14 22:14:08 +00:00
''' WinRM connections over HTTP/HTTPS. '''
2015-09-10 19:55:59 +00:00
module_implementation_preferences = ( ' .ps1 ' , ' ' )
2015-04-27 05:28:25 +00:00
def __init__ ( self , * args , * * kwargs ) :
self . has_pipelining = False
self . protocol = None
self . shell_id = None
self . delegate = None
2015-06-29 19:41:51 +00:00
self . _shell_type = ' powershell '
2014-11-14 22:14:08 +00:00
2015-04-27 05:28:25 +00:00
# TODO: Add runas support
2015-04-23 23:54:48 +00:00
self . become_methods_supported = [ ]
2015-04-27 05:28:25 +00:00
super ( Connection , self ) . __init__ ( * args , * * kwargs )
@property
def transport ( self ) :
''' used to identify this connection object from other classes '''
return ' winrm '
2015-08-23 05:54:46 +00:00
def set_host_overrides ( self , host ) :
2014-11-14 22:14:08 +00:00
'''
2015-08-23 05:54:46 +00:00
Override WinRM - specific options from host variables .
2014-11-14 22:14:08 +00:00
'''
2015-10-09 00:37:24 +00:00
host_vars = combine_vars ( host . get_group_vars ( ) , host . get_vars ( ) )
2015-04-27 05:28:25 +00:00
2015-09-14 19:59:40 +00:00
self . _winrm_host = self . _play_context . remote_addr
self . _winrm_port = int ( self . _play_context . port or 5986 )
2015-08-23 05:54:46 +00:00
self . _winrm_scheme = host_vars . get ( ' ansible_winrm_scheme ' , ' http ' if self . _winrm_port == 5985 else ' https ' )
self . _winrm_path = host_vars . get ( ' ansible_winrm_path ' , ' /wsman ' )
2015-09-14 19:59:40 +00:00
self . _winrm_user = self . _play_context . remote_user
self . _winrm_pass = self . _play_context . password
2015-08-23 05:54:46 +00:00
if ' @ ' in self . _winrm_user :
self . _winrm_realm = self . _winrm_user . split ( ' @ ' , 1 ) [ 1 ] . strip ( ) or None
else :
self . _winrm_realm = None
self . _winrm_realm = host_vars . get ( ' ansible_winrm_realm ' , self . _winrm_realm ) or None
2015-04-27 05:28:25 +00:00
2015-10-09 00:37:24 +00:00
transport_selector = ' ssl ' if self . _winrm_scheme == ' https ' else ' plaintext '
2015-08-23 05:54:46 +00:00
if HAVE_KERBEROS and ( ' @ ' in self . _winrm_user or self . _winrm_realm ) :
2015-10-09 00:37:24 +00:00
self . _winrm_transport = ' kerberos, %s ' % transport_selector
2015-08-23 05:54:46 +00:00
else :
2015-10-09 00:37:24 +00:00
self . _winrm_transport = transport_selector
2015-08-23 05:54:46 +00:00
self . _winrm_transport = host_vars . get ( ' ansible_winrm_transport ' , self . _winrm_transport )
if isinstance ( self . _winrm_transport , basestring ) :
self . _winrm_transport = [ x . strip ( ) for x in self . _winrm_transport . split ( ' , ' ) if x . strip ( ) ]
2015-04-27 05:28:25 +00:00
2015-08-23 05:54:46 +00:00
self . _winrm_kwargs = dict ( username = self . _winrm_user , password = self . _winrm_pass , realm = self . _winrm_realm )
argspec = inspect . getargspec ( Protocol . __init__ )
for arg in argspec . args :
if arg in ( ' self ' , ' endpoint ' , ' transport ' , ' username ' , ' password ' , ' realm ' ) :
continue
if ' ansible_winrm_ %s ' % arg in host_vars :
self . _winrm_kwargs [ arg ] = host_vars [ ' ansible_winrm_ %s ' % arg ]
2015-04-27 05:28:25 +00:00
2015-08-23 05:54:46 +00:00
def _winrm_connect ( self ) :
'''
Establish a WinRM connection over HTTP / HTTPS .
'''
2015-11-11 15:10:14 +00:00
display . vvv ( " ESTABLISH WINRM CONNECTION FOR USER: %s on PORT %s TO %s " %
2015-08-23 05:54:46 +00:00
( self . _winrm_user , self . _winrm_port , self . _winrm_host ) , host = self . _winrm_host )
netloc = ' %s : %d ' % ( self . _winrm_host , self . _winrm_port )
2015-09-17 14:44:26 +00:00
endpoint = urlunsplit ( ( self . _winrm_scheme , netloc , self . _winrm_path , ' ' , ' ' ) )
2015-09-14 19:59:40 +00:00
errors = [ ]
2015-08-23 05:54:46 +00:00
for transport in self . _winrm_transport :
2015-09-14 19:59:40 +00:00
if transport == ' kerberos ' and not HAVE_KERBEROS :
errors . append ( ' kerberos: the python kerberos library is not installed ' )
continue
2015-11-11 15:10:14 +00:00
display . vvvvv ( ' WINRM CONNECT: transport= %s endpoint= %s ' % ( transport , endpoint ) , host = self . _winrm_host )
2014-11-14 22:14:08 +00:00
try :
2015-09-14 19:59:40 +00:00
protocol = Protocol ( endpoint , transport = transport , * * self . _winrm_kwargs )
2014-11-14 22:14:08 +00:00
protocol . send_message ( ' ' )
return protocol
2015-09-14 19:59:40 +00:00
except Exception as e :
err_msg = ( str ( e ) or repr ( e ) ) . strip ( )
2014-11-14 22:14:08 +00:00
if re . search ( r ' Operation \ s+?timed \ s+?out ' , err_msg , re . I ) :
2015-09-14 19:59:40 +00:00
raise AnsibleError ( ' the connection attempt timed out ' )
2014-11-14 22:14:08 +00:00
m = re . search ( r ' Code \ s+?( \ d {3} ) ' , err_msg )
if m :
code = int ( m . groups ( ) [ 0 ] )
if code == 401 :
2015-09-14 19:59:40 +00:00
err_msg = ' the username/password specified for this server was incorrect '
2014-11-14 22:14:08 +00:00
elif code == 411 :
return protocol
2015-09-14 19:59:40 +00:00
errors . append ( ' %s : %s ' % ( transport , err_msg ) )
2015-11-11 15:10:14 +00:00
display . vvvvv ( ' WINRM CONNECTION ERROR: %s \n %s ' % ( err_msg , traceback . format_exc ( ) ) , host = self . _winrm_host )
2015-09-14 19:59:40 +00:00
if errors :
raise AnsibleError ( ' , ' . join ( errors ) )
else :
raise AnsibleError ( ' No transport found for WinRM connection ' )
2014-11-14 22:14:08 +00:00
2015-10-23 21:37:50 +00:00
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 '
rs = protocol . send_message ( xmltodict . unparse ( rq ) )
def _winrm_exec ( self , command , args = ( ) , from_exec = False , stdin_iterator = None ) :
2014-11-14 22:14:08 +00:00
if not self . protocol :
self . protocol = self . _winrm_connect ( )
2015-11-23 16:53:05 +00:00
self . _connected = True
2014-11-14 22:14:08 +00:00
if not self . shell_id :
2015-07-24 16:39:54 +00:00
self . shell_id = self . protocol . open_shell ( codepage = 65001 ) # UTF-8
2015-11-23 16:53:05 +00:00
display . vvvvv ( ' WINRM OPEN SHELL: %s ' % self . shell_id , host = self . _winrm_host )
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 )
2014-11-14 22:14:08 +00:00
command_id = None
try :
2015-11-19 07:09:16 +00:00
stdin_push_failed = False
2015-10-23 21:37:50 +00:00
command_id = self . protocol . run_command ( self . shell_id , to_bytes ( command ) , map ( to_bytes , args ) , console_mode_stdin = ( stdin_iterator == None ) )
# TODO: try/except around this, so we can get/return the command result on a broken pipe or other failure (probably more useful than the 500 that comes from this)
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 :
2015-11-19 07:09:16 +00:00
stdin_push_failed = True
2015-10-23 21:37:50 +00:00
# NB: this could hang if the receiver is still running (eg, network failed a Send request but the server's still happy).
2015-11-19 07:09:16 +00:00
# FUTURE: Consider adding pywinrm status check/abort operations to see if the target is still running after a failure.
2014-11-14 22:14:08 +00:00
response = Response ( self . protocol . get_command_output ( self . shell_id , command_id ) )
if from_exec :
2015-11-11 15:10:14 +00:00
display . vvvvv ( ' WINRM RESULT %r ' % to_unicode ( response ) , host = self . _winrm_host )
2014-11-14 22:14:08 +00:00
else :
2015-11-11 15:10:14 +00:00
display . vvvvvv ( ' WINRM RESULT %r ' % to_unicode ( response ) , 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 )
2015-11-19 07:09:16 +00:00
if stdin_push_failed :
raise AnsibleError ( ' winrm send_input failed; \n stdout: %s \n stderr %s ' % ( response . std_out , response . std_err ) )
2014-11-14 22:14:08 +00:00
return response
finally :
if command_id :
self . protocol . cleanup_command ( self . shell_id , command_id )
2015-04-27 05:28:25 +00:00
def _connect ( self ) :
2014-11-14 22:14:08 +00:00
if not self . protocol :
self . protocol = self . _winrm_connect ( )
2015-11-23 16:53:05 +00:00
self . _connected = True
2014-11-14 22:14:08 +00:00
return self
2015-09-24 20:29:36 +00:00
def exec_command ( self , cmd , in_data = None , sudoable = True ) :
super ( Connection , self ) . exec_command ( cmd , in_data = in_data , sudoable = sudoable )
2015-07-24 16:39:54 +00:00
cmd_parts = shlex . split ( to_bytes ( cmd ) , posix = False )
cmd_parts = map ( to_unicode , cmd_parts )
script = None
cmd_ext = cmd_parts and self . _shell . _unquote ( cmd_parts [ 0 ] ) . lower ( ) [ - 4 : ] or ' '
# Support running .ps1 files (via script/raw).
if cmd_ext == ' .ps1 ' :
2015-08-23 00:18:44 +00:00
script = ' & %s ' % cmd
2015-07-24 16:39:54 +00:00
# Support running .bat/.cmd files; change back to the default system encoding instead of UTF-8.
elif cmd_ext in ( ' .bat ' , ' .cmd ' ) :
2015-08-23 00:18:44 +00:00
script = ' [System.Console]::OutputEncoding = [System.Text.Encoding]::Default; & %s ' % cmd
2015-07-24 16:39:54 +00:00
# Encode the command if not already encoded; supports running simple PowerShell commands via raw.
elif ' -EncodedCommand ' not in cmd_parts :
2015-08-23 00:18:44 +00:00
script = cmd
2015-07-24 16:39:54 +00:00
if script :
2015-08-22 22:19:43 +00:00
cmd_parts = self . _shell . _encode_script ( script , as_list = True , strict_mode = False )
2014-11-14 22:14:08 +00:00
if ' -EncodedCommand ' in cmd_parts :
encoded_cmd = cmd_parts [ cmd_parts . index ( ' -EncodedCommand ' ) + 1 ]
2015-08-23 00:18:44 +00:00
decoded_cmd = to_unicode ( base64 . b64decode ( encoded_cmd ) . decode ( ' utf-16-le ' ) )
2015-11-11 15:10:14 +00:00
display . vvv ( " EXEC %s " % decoded_cmd , host = self . _winrm_host )
2014-11-14 22:14:08 +00:00
else :
2015-11-11 15:10:14 +00:00
display . vvv ( " EXEC %s " % cmd , host = self . _winrm_host )
2014-11-14 22:14:08 +00:00
try :
result = self . _winrm_exec ( cmd_parts [ 0 ] , cmd_parts [ 1 : ] , from_exec = True )
2015-11-11 15:10:14 +00:00
except Exception :
2014-11-14 22:14:08 +00:00
traceback . print_exc ( )
2015-04-27 05:28:25 +00:00
raise AnsibleError ( " failed to exec cmd %s " % cmd )
2015-10-21 14:59:46 +00:00
result . std_out = to_bytes ( result . std_out )
result . std_err = to_bytes ( result . std_err )
2015-09-24 15:56:20 +00:00
return ( result . status_code , result . std_out , result . std_err )
2014-11-14 22:14:08 +00:00
2015-10-23 21:37:50 +00:00
# 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 ( in_path )
offset = 0
with open ( in_path , ' rb ' ) as in_file :
for out_data in iter ( ( lambda : in_file . read ( buffer_size ) ) , ' ' ) :
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 ) + ' \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
2014-11-14 22:14:08 +00:00
def put_file ( self , in_path , out_path ) :
2015-06-04 18:27:18 +00:00
super ( Connection , self ) . put_file ( in_path , out_path )
2015-07-24 16:39:54 +00:00
out_path = self . _shell . _unquote ( out_path )
2015-11-11 15:10:14 +00:00
display . vvv ( ' PUT " %s " TO " %s " ' % ( in_path , out_path ) , host = self . _winrm_host )
2014-11-14 22:14:08 +00:00
if not os . path . exists ( in_path ) :
2015-07-24 16:39:54 +00:00
raise AnsibleFileNotFound ( ' file or module does not exist: " %s " ' % in_path )
2015-10-23 21:37:50 +00:00
script_template = '''
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 " " }} "
} }
'''
# FUTURE: this sucks- why can't the module/shell stuff do this?
with open ( in_path , ' r ' ) as temp_file :
if temp_file . read ( 15 ) . lower ( ) . startswith ( ' #!powershell ' ) and not out_path . lower ( ) . endswith ( ' .ps1 ' ) :
out_path = out_path + ' .ps1 '
script = script_template . format ( self . _shell . _escape ( out_path ) )
cmd_parts = self . _shell . _encode_script ( script , as_list = True , strict_mode = 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 :
2015-11-19 07:09:16 +00:00
raise AnsibleError ( to_str ( result . std_err ) )
2015-10-23 21:37:50 +00:00
put_output = json . loads ( result . std_out )
remote_sha1 = put_output . get ( " sha1 " )
if not remote_sha1 :
2015-11-19 07:09:16 +00:00
raise AnsibleError ( " Remote sha1 was not returned " )
2015-10-23 21:37:50 +00:00
local_sha1 = secure_hash ( in_path )
if not remote_sha1 == local_sha1 :
2015-11-19 07:09:16 +00:00
raise AnsibleError ( " Remote sha1 hash {0} does not match local hash {1} " . format ( remote_sha1 , local_sha1 ) )
2015-10-23 21:37:50 +00:00
2014-11-14 22:14:08 +00:00
def fetch_file ( self , in_path , out_path ) :
2015-06-04 18:27:18 +00:00
super ( Connection , self ) . fetch_file ( in_path , out_path )
2015-07-24 16:39:54 +00:00
in_path = self . _shell . _unquote ( in_path )
2014-11-14 22:14:08 +00:00
out_path = out_path . replace ( ' \\ ' , ' / ' )
2015-11-11 15:10:14 +00:00
display . vvv ( ' FETCH " %s " TO " %s " ' % ( in_path , out_path ) , host = self . _winrm_host )
2015-04-23 23:54:48 +00:00
buffer_size = 2 * * 19 # 0.5MB chunks
2015-06-02 15:41:30 +00:00
makedirs_safe ( os . path . dirname ( out_path ) )
2014-11-14 22:14:08 +00:00
out_file = None
try :
offset = 0
while True :
try :
script = '''
If ( Test - Path - PathType Leaf " %(path)s " )
{
$ stream = [ System . IO . File ] : : OpenRead ( " %(path)s " ) ;
$ stream . Seek ( % ( offset ) d , [ System . IO . SeekOrigin ] : : Begin ) | Out - Null ;
$ buffer = New - Object Byte [ ] % ( buffer_size ) d ;
$ bytesRead = $ stream . Read ( $ buffer , 0 , % ( buffer_size ) d ) ;
$ bytes = $ buffer [ 0. . ( $ bytesRead - 1 ) ] ;
[ System . Convert ] : : ToBase64String ( $ bytes ) ;
$ stream . Close ( ) | Out - Null ;
}
ElseIf ( Test - Path - PathType Container " %(path)s " )
{
Write - Host " [DIR] " ;
}
Else
{
Write - Error " %(path)s does not exist " ;
Exit 1 ;
}
2015-04-27 05:28:25 +00:00
''' % d ict(buffer_size=buffer_size, path=self._shell._escape(in_path), offset=offset)
2015-11-11 15:10:14 +00:00
display . vvvvv ( ' WINRM FETCH " %s " to " %s " (offset= %d ) ' % ( in_path , out_path , offset ) , host = self . _winrm_host )
2015-04-27 05:28:25 +00:00
cmd_parts = self . _shell . _encode_script ( script , as_list = True )
2014-11-14 22:14:08 +00:00
result = self . _winrm_exec ( cmd_parts [ 0 ] , cmd_parts [ 1 : ] )
if result . status_code != 0 :
2015-10-21 14:59:46 +00:00
raise IOError ( to_str ( result . std_err ) )
2014-11-14 22:14:08 +00:00
if result . std_out . strip ( ) == ' [DIR] ' :
data = None
else :
data = base64 . b64decode ( result . std_out . strip ( ) )
if data is None :
2015-06-02 15:41:30 +00:00
makedirs_safe ( out_path )
2014-11-14 22:14:08 +00:00
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 ( out_path ) :
break
out_file = open ( out_path , ' wb ' )
out_file . write ( data )
if len ( data ) < buffer_size :
break
offset + = len ( data )
except Exception :
traceback . print_exc ( )
2015-07-24 16:39:54 +00:00
raise AnsibleError ( ' failed to transfer file to " %s " ' % out_path )
2014-11-14 22:14:08 +00:00
finally :
if out_file :
out_file . close ( )
def close ( self ) :
if self . protocol and self . shell_id :
2015-11-23 16:53:05 +00:00
display . vvvvv ( ' WINRM CLOSE SHELL: %s ' % self . shell_id , host = self . _winrm_host )
2014-11-14 22:14:08 +00:00
self . protocol . close_shell ( self . shell_id )
2015-11-23 16:53:05 +00:00
self . shell_id = None
self . protocol = None
self . _connected = False