From 6982dfc7560eec6a68beb5402d7a047079325803 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 22 Aug 2018 09:43:13 +1000 Subject: [PATCH] psrp: Added new Windows connection plugin (#41729) * psrp: Added new Windows connection plugin * Tweaks to connection options from review --- lib/ansible/modules/windows/win_copy.ps1 | 1 + lib/ansible/plugins/connection/psrp.py | 600 ++++++++++++++++++ lib/ansible/plugins/shell/powershell.py | 47 +- .../targets/connection_psrp/aliases | 3 + .../targets/connection_psrp/runme.sh | 15 + test/runner/lib/classification.py | 2 +- test/units/plugins/connection/test_psrp.py | 147 +++++ 7 files changed, 793 insertions(+), 22 deletions(-) create mode 100644 lib/ansible/plugins/connection/psrp.py create mode 100644 test/integration/targets/connection_psrp/aliases create mode 100755 test/integration/targets/connection_psrp/runme.sh create mode 100644 test/units/plugins/connection/test_psrp.py diff --git a/lib/ansible/modules/windows/win_copy.ps1 b/lib/ansible/modules/windows/win_copy.ps1 index e3ac14a6890..12cfbc9515f 100644 --- a/lib/ansible/modules/windows/win_copy.ps1 +++ b/lib/ansible/modules/windows/win_copy.ps1 @@ -182,6 +182,7 @@ Function Extract-Zip($src, $dest) { } } } + $archive.Dispose() # release the handle of the zip file } Function Extract-ZipLegacy($src, $dest) { diff --git a/lib/ansible/plugins/connection/psrp.py b/lib/ansible/plugins/connection/psrp.py new file mode 100644 index 00000000000..e6dfcfa1600 --- /dev/null +++ b/lib/ansible/plugins/connection/psrp.py @@ -0,0 +1,600 @@ +# Copyright (c) 2018 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: psrp +short_description: Run tasks over Microsoft PowerShell Remoting Protocol +description: +- Run commands or put/fetch on a target via PSRP (WinRM plugin) +- This is similar to the I(winrm) connection plugin which uses the same + underlying transport but instead runs in a PowerShell interpreter. +version_added: "2.7" +requirements: +- pypsrp (Python library) +options: + # transport options + remote_addr: + description: + - The hostname or IP address of the remote host. + default: inventory_hostname + vars: + - name: ansible_host + - name: ansible_psrp_host + remote_user: + description: + - The user to log in as. + vars: + - name: ansible_user + - name: ansible_psrp_user + port: + description: + - The port for PSRP to connect on the remote target. + - Default is C(5986) if I(protocol) is not defined or is C(https), + otherwise the port is C(5985). + vars: + - name: ansible_port + - name: ansible_psrp_port + protocol: + description: + - Set the protocol to use for the connection. + - Default is C(https) if I(port) is not defined or I(port) is not C(5985). + choices: + - http + - https + vars: + - name: ansible_psrp_protocol + path: + description: + - The URI path to connect to. + vars: + - name: ansible_psrp_path + default: 'wsman' + auth: + description: + - The authentication protocol to use when authenticating the remote user. + - The default, C(negotiate), will attempt to use C(Kerberos) if it is + available and fall back to C(NTLM) if it isn't. + vars: + - name: ansible_psrp_auth + choices: + - basic + - certificate + - negotiate + - kerberos + - ntlm + - credssp + default: negotiate + cert_validation: + description: + - Whether to validate the remote server's certificate or not. + - Set to C(ignore) to not validate any certificates. + - I(cert_trust_path) can be set to the path of a PEM certificate chain to + use in the validation. + choices: + - validate + - ignore + default: validate + vars: + - name: ansible_psrp_cert_validation + cert_trust_path: + description: + - The path to a PEM certificate chain to use when validating the server's + certificate. + - This value is ignored if I(cert_validation) is set to C(ignore). + vars: + - name: ansible_psrp_cert_trust_path + connection_timeout: + description: + - The connection timeout for making the request to the remote host. + - This is measured in seconds. + vars: + - name: ansible_psrp_connection_timeout + default: 30 + message_encryption: + description: + - Controls the message encryption settings, this is different from TLS + encryption when I(ansible_psrp_protocol) is C(https). + - Only the auth protocols C(negotiate), C(kerberos), C(ntlm), and + C(credssp) can do message encryption. The other authentication protocols + only support encryption when C(protocol) is set to C(https). + - C(auto) means means message encryption is only used when not using + TLS/HTTPS. + - C(always) is the same as C(auto) but message encryption is always used + even when running over TLS/HTTPS. + - C(never) disables any encryption checks that are in place when running + over HTTP and disables any authentication encryption processes. + vars: + - name: ansible_psrp_message_encryption + choices: + - auto + - always + - never + default: auto + proxy: + description: + - Set the proxy URL to use when connecting to the remote host. + vars: + - name: ansible_psrp_proxy + ignore_proxy: + description: + - Will disable any environment proxy settings and connect directly to the + remote host. + - This option is ignored if C(proxy) is set. + vars: + - name: ansible_psrp_ignore_proxy + type: bool + default: 'no' + + # protocol options + operation_timeout: + description: + - Sets the WSMan timeout for each operation. + - This is measured in seconds. + - This should not exceed the value for C(connection_timeout). + vars: + - name: ansible_psrp_operation_timeout + default: 20 + max_envelope_size: + description: + - Sets the maximum size of each WSMan message sent to the remote host. + - This is measured in bytes. + - Defaults to C(150KiB) for compatibility with older hosts. + vars: + - name: ansible_psrp_max_envelope_size + default: 153600 + configuration_name: + description: + - The name of the PowerShell configuration endpoint to connect to. + vars: + - name: ansible_psrp_configuration_name + default: Microsoft.PowerShell +""" + +import base64 +import json +import os + +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.errors import AnsibleFileNotFound +from ansible.module_utils.parsing.convert_bool import boolean +from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.plugins.connection import ConnectionBase +from ansible.plugins.shell.powershell import _common_args +from ansible.utils.hashing import secure_hash +from ansible.utils.path import makedirs_safe + +HAS_PYPSRP = True +PYPSRP_IMP_ERR = None +try: + from pypsrp.complex_objects import GenericComplexObject, RunspacePoolState + from pypsrp.exceptions import AuthenticationError, WinRMError + from pypsrp.host import PSHost, PSHostUserInterface + from pypsrp.powershell import PowerShell, RunspacePool + from pypsrp.shell import Process, SignalCode, WinRS + from pypsrp.wsman import WSMan, AUTH_KWARGS +except ImportError as err: + HAS_PYPSRP = False + PYPSRP_IMP_ERR = err + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() + + +class Connection(ConnectionBase): + + transport = 'psrp' + 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.runspace = None + self.host = None + + self._shell_type = 'powershell' + super(Connection, self).__init__(*args, **kwargs) + + def _connect(self): + if not HAS_PYPSRP: + raise AnsibleError("pypsrp or dependencies are not installed: %s" + % to_native(PYPSRP_IMP_ERR)) + super(Connection, self)._connect() + self._build_kwargs() + display.vvv("ESTABLISH PSRP CONNECTION FOR USER: %s ON PORT %s TO %s" % + (self._psrp_user, self._psrp_port, self._psrp_host), + host=self._psrp_host) + + if not self.runspace: + connection = WSMan(**self._psrp_conn_kwargs) + + # create our psuedo host to capture the exit code and host output + host_ui = PSHostUserInterface() + self.host = PSHost(None, None, False, "Ansible PSRP Host", None, + host_ui, None) + + self.runspace = RunspacePool( + connection, host=self.host, + configuration_name=self._psrp_configuration_name + ) + display.vvvvv( + "PSRP OPEN RUNSPACE: auth=%s configuration=%s endpoint=%s" % + (self._psrp_auth, self._psrp_configuration_name, + connection.transport.endpoint), host=self._psrp_host + ) + try: + self.runspace.open() + except AuthenticationError as e: + raise AnsibleConnectionFailure("failed to authenticate with " + "the server: %s" % to_native(e)) + except WinRMError as e: + raise AnsibleConnectionFailure( + "psrp connection failure during runspace open: %s" + % to_native(e) + ) + self._connected = True + return self + + def reset(self): + display.vvvvv("PSRP: Reset Connection", host=self._psrp_host) + self.runspace = None + self._connect() + + def exec_command(self, cmd, in_data=None, sudoable=True): + super(Connection, self).exec_command(cmd, in_data=in_data, + sudoable=sudoable) + + if cmd == "-" and not in_data.startswith(b"#!"): + # The powershell plugin sets cmd to '-' when we are executing a + # PowerShell script with in_data being the script to execute. + script = in_data + in_data = None + display.vvv("PSRP: EXEC (via pipeline wrapper)", + host=self._psrp_host) + elif cmd == "-": + # ANSIBALLZ wrapper, we need to get the interpreter and execute + # that as the script - note this won't work as basic.py relies + # on packages not available on Windows, once fixed we can enable + # this path + interpreter = to_native(in_data.splitlines()[0][2:]) + # script = "$input | &'%s' -" % interpreter + # in_data = to_text(in_data) + raise AnsibleError("cannot run the interpreter '%s' on the psrp " + "connection plugin" % interpreter) + elif cmd.startswith(" ".join(_common_args) + " -EncodedCommand"): + # This is a PowerShell script encoded by the shell plugin, we will + # decode the script and execute it in the runspace instead of + # starting a new interpreter to save on time + b_command = base64.b64decode(cmd.split(" ")[-1]) + script = to_text(b_command, 'utf-16-le') + display.vvv("PSRP: EXEC %s" % script, host=self._psrp_host) + else: + # in other cases we want to execute the cmd as the script + script = cmd + display.vvv("PSRP: EXEC %s" % script, host=self._psrp_host) + + rc, stdout, stderr = self._exec_psrp_script(script, in_data) + return rc, stdout, stderr + + def put_file(self, in_path, out_path): + super(Connection, self).put_file(in_path, out_path) + display.vvv("PUT %s TO %s" % (in_path, out_path), host=self._psrp_host) + + out_path = self._shell._unquote(out_path) + script = u'''begin { + $ErrorActionPreference = "Stop" + + $path = '%s' + $fd = [System.IO.File]::Create($path) + $algo = [System.Security.Cryptography.SHA1CryptoServiceProvider]::Create() + $bytes = @() +} process { + $bytes = [System.Convert]::FromBase64String($input) + $algo.TransformBlock($bytes, 0, $bytes.Length, $bytes, 0) > $null + $fd.Write($bytes, 0, $bytes.Length) +} end { + $fd.Close() + $algo.TransformFinalBlock($bytes, 0, 0) > $null + $hash = [System.BitConverter]::ToString($algo.Hash) + $hash = $hash.Replace("-", "").ToLowerInvariant() + + Write-Output -InputObject "{`"sha1`":`"$hash`"}" +}''' % self._shell._escape(out_path) + + cmd_parts = self._shell._encode_script(script, as_list=True, + strict_mode=False, + preserve_rc=False) + b_in_path = to_bytes(in_path, errors='surrogate_or_strict') + if not os.path.exists(b_in_path): + raise AnsibleFileNotFound('file or module does not exist: "%s"' + % to_native(in_path)) + + in_size = os.path.getsize(b_in_path) + buffer_size = int(self.runspace.connection.max_payload_size / 4 * 3) + + # copying files is faster when using the raw WinRM shell and not PSRP + # we will create a WinRS shell just for this process + # TODO: speed this up as there is overhead creating a shell for this + with WinRS(self.runspace.connection, codepage=65001) as shell: + process = Process(shell, cmd_parts[0], cmd_parts[1:]) + process.begin_invoke() + + offset = 0 + with open(b_in_path, 'rb') as src_file: + for data in iter((lambda: src_file.read(buffer_size)), b""): + offset += len(data) + display.vvvvv("PSRP PUT %s to %s (offset=%d, size=%d" % + (in_path, out_path, offset, len(data)), + host=self._psrp_host) + b64_data = base64.b64encode(data) + b"\r\n" + process.send(b64_data, end=(src_file.tell() == in_size)) + + # the file was empty, return empty buffer + if offset == 0: + process.send(b"", end=True) + + process.end_invoke() + process.signal(SignalCode.CTRL_C) + + if process.rc != 0: + raise AnsibleError(to_native(process.stderr)) + + put_output = json.loads(process.stdout) + remote_sha1 = put_output.get("sha1") + + if not remote_sha1: + raise AnsibleError("Remote sha1 was not returned, stdout: '%s', " + "stderr: '%s'" % (to_native(process.stdout), + to_native(process.stderr))) + + local_sha1 = secure_hash(in_path) + if not remote_sha1 == local_sha1: + raise AnsibleError("Remote sha1 hash %s does not match local hash " + "%s" % (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) + display.vvv("FETCH %s TO %s" % (in_path, out_path), + host=self._psrp_host) + + in_path = self._shell._unquote(in_path) + out_path = out_path.replace('\\', '/') + + # because we are dealing with base64 data we need to get the max size + # of the bytes that the base64 size would equal + max_b64_size = int(self.runspace.connection.max_payload_size - + (self.runspace.connection.max_payload_size / 4 * 3)) + buffer_size = max_b64_size - (max_b64_size % 1024) + + # setup the file stream with read only mode + setup_script = '''$ErrorActionPreference = "Stop" +$path = "%s" + +if (Test-Path -Path $path -PathType Leaf) { + $fs = New-Object -TypeName System.IO.FileStream -ArgumentList @( + $path, + [System.IO.FileMode]::Open, + [System.IO.FileAccess]::Read, + [System.IO.FileShare]::Read + ) + $buffer_size = %d +} elseif (Test-Path -Path $path -PathType Container) { + Write-Output -InputObject "[DIR]" +} else { + Write-Error -Message "$path does not exist" + $host.SetShouldExit(1) +}''' % (self._shell._escape(in_path), buffer_size) + + # read the file stream at the offset and return the b64 string + read_script = '''$ErrorActionPreference = "Stop" +$fs.Seek(%d, [System.IO.SeekOrigin]::Begin) > $null +$buffer = New-Object -TypeName byte[] -ArgumentList $buffer_size +$bytes_read = $fs.Read($buffer, 0, $buffer_size) + +if ($bytes_read -gt 0) { + $bytes = $buffer[0..($bytes_read - 1)] + Write-Output -InputObject ([System.Convert]::ToBase64String($bytes)) +}''' + + # need to run the setup script outside of the local scope so the + # file stream stays active between fetch operations + rc, stdout, stderr = self._exec_psrp_script(setup_script, + use_local_scope=False) + if rc != 0: + raise AnsibleError("failed to setup file stream for fetch '%s': %s" + % (out_path, to_native(stderr))) + elif stdout.strip() == '[DIR]': + # in_path was a dir so we need to create the dir locally + makedirs_safe(out_path) + return + + b_out_path = to_bytes(out_path, errors='surrogate_or_strict') + makedirs_safe(os.path.dirname(b_out_path)) + offset = 0 + with open(b_out_path, 'wb') as out_file: + while True: + display.vvvvv("PSRP FETCH %s to %s (offset=%d" % + (in_path, out_path, offset), host=self._psrp_host) + rc, stdout, stderr = \ + self._exec_psrp_script(read_script % offset) + if rc != 0: + raise AnsibleError("failed to transfer file to '%s': %s" + % (out_path, to_native(stderr))) + + data = base64.b64decode(stdout.strip()) + out_file.write(data) + if len(data) < buffer_size: + break + + rc, stdout, stderr = self._exec_psrp_script("$fs.Close()") + if rc != 0: + display.warning("failed to close remote file stream of file " + "'%s': %s" % (in_path, to_native(stderr))) + + def close(self): + if self.runspace and self.runspace.state == RunspacePoolState.OPENED: + display.vvvvv("PSRP CLOSE RUNSPACE: %s" % (self.runspace.id), + host=self._psrp_host) + self.runspace.close() + self.runspace = None + self._connected = False + + def _build_kwargs(self): + 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._psrp_host = self.get_option('remote_addr') + self._psrp_user = self.get_option('remote_user') + self._psrp_pass = self._play_context.password + + protocol = self.get_option('protocol') + port = self.get_option('port') + if protocol is None and port is None: + protocol = 'https' + port = 5986 + elif protocol is None: + protocol = 'https' if int(port) != 5985 else 'http' + elif port is None: + port = 5986 if protocol == 'https' else 5985 + + self._psrp_protocol = protocol + self._psrp_port = int(port) + + self._psrp_path = self.get_option('path') + self._psrp_auth = self.get_option('auth') + # cert validation can either be a bool or a path to the cert + cert_validation = self.get_option('cert_validation') + cert_trust_path = self.get_option('cert_trust_path') + if cert_validation == 'ignore': + self._psrp_cert_validation = False + elif cert_trust_path is not None: + self._psrp_cert_validation = cert_trust_path + else: + self._psrp_cert_validation = True + + self._psrp_connection_timeout = int(self.get_option('connection_timeout')) + self._psrp_message_encryption = self.get_option('message_encryption') + self._psrp_proxy = self.get_option('proxy') + self._psrp_ignore_proxy = boolean(self.get_option('ignore_proxy')) + self._psrp_operation_timeout = int(self.get_option('operation_timeout')) + self._psrp_max_envelope_size = int(self.get_option('max_envelope_size')) + self._psrp_configuration_name = self.get_option('configuration_name') + + supported_args = [] + for auth_kwarg in AUTH_KWARGS.values(): + supported_args.extend(auth_kwarg) + extra_args = set([v.replace('ansible_psrp_', '') for v in + self.get_option('_extras')]) + unsupported_args = extra_args.difference(supported_args) + + for arg in unsupported_args: + display.warning("ansible_psrp_%s is unsupported by the current " + "psrp version installed" % arg) + + self._psrp_conn_kwargs = dict( + server=self._psrp_host, port=self._psrp_port, + username=self._psrp_user, password=self._psrp_pass, + ssl=self._psrp_protocol == 'https', path=self._psrp_path, + auth=self._psrp_auth, cert_validation=self._psrp_cert_validation, + connection_timeout=self._psrp_connection_timeout, + encryption=self._psrp_message_encryption, proxy=self._psrp_proxy, + no_proxy=self._psrp_ignore_proxy, + max_envelope_size=self._psrp_max_envelope_size, + operation_timeout=self._psrp_operation_timeout, + ) + # add in the extra args that were set + for arg in extra_args.intersection(supported_args): + option = self.get_option('_extras')['ansible_psrp_%s' % arg] + self._psrp_conn_kwargs[arg] = option + + def _exec_psrp_script(self, script, input_data=None, use_local_scope=True): + ps = PowerShell(self.runspace) + ps.add_script(script, use_local_scope=use_local_scope) + ps.invoke(input=input_data) + + rc, stdout, stderr = self._parse_pipeline_result(ps) + return rc, stdout, stderr + + def _parse_pipeline_result(self, pipeline): + """ + PSRP doesn't have the same concept as other protocols with its output. + We need some extra logic to convert the pipeline streams and host + output into the format that Ansible understands. + + :param pipeline: The finished PowerShell pipeline that invoked our + commands + :return: rc, stdout, stderr based on the pipeline output + """ + # we try and get the rc from our host implementation, this is set if + # exit or $host.SetShouldExit() is called in our pipeline, if not we + # set to 0 if the pipeline had not errors and 1 if it did + rc = self.host.rc or (1 if pipeline.had_errors else 0) + + # TODO: figure out a better way of merging this with the host output + stdout_list = [] + for output in pipeline.output: + # not all pipeline outputs can be casted to a string, we will + # create our own output based on the properties if that is the + # case+ + try: + output_msg = str(output) + except TypeError: + if isinstance(output, GenericComplexObject): + obj_lines = output.property_sets + for key, value in output.adapted_properties.items(): + obj_lines.append("%s: %s" % (key, value)) + for key, value in output.extended_properties.items(): + obj_lines.append("%s: %s" % (key, value)) + output_msg = "\n".join(obj_lines) + else: + output_msg = "" + stdout_list.append(output_msg) + + stdout = "\r\n".join(stdout_list) + if len(self.host.ui.stdout) > 0: + stdout += "\r\n" + "".join(self.host.ui.stdout) + + stderr_list = [] + for error in pipeline.streams.error: + # the error record is not as fully fleshed out like we usually get + # in PS, we will manually create it here + error_msg = "%s : %s\r\n" \ + "%s\r\n" \ + " + CategoryInfo : %s\r\n" \ + " + FullyQualifiedErrorId : %s" \ + % (error.command_name, str(error), + error.invocation_position_message, error.message, + error.fq_error) + stacktrace = error.script_stacktrace + if self._play_context.verbosity >= 3 and stacktrace is not None: + error_msg += "\r\nStackTrace:\r\n%s" % stacktrace + stderr_list.append(error_msg) + + stderr = "\r\n".join(stderr_list) + if len(self.host.ui.stderr) > 0: + stderr += "\r\n" + "".join(self.host.ui.stderr) + + display.vvvvv("PSRP RC: %d" % rc, host=self._psrp_host) + display.vvvvv("PSRP STDOUT: %s" % stdout, host=self._psrp_host) + display.vvvvv("PSRP STDERR: %s" % stderr, host=self._psrp_host) + + # reset the host back output back to defaults, needed if running + # multiple pipelines on the same RunspacePool + self.host.rc = 0 + self.host.ui.stdout = [] + self.host.ui.stderr = [] + + return rc, stdout, stderr diff --git a/lib/ansible/plugins/shell/powershell.py b/lib/ansible/plugins/shell/powershell.py index c4e9d20b5aa..6b1208ef470 100644 --- a/lib/ansible/plugins/shell/powershell.py +++ b/lib/ansible/plugins/shell/powershell.py @@ -171,8 +171,12 @@ Function Run($payload) { $ps.AddCommand("Out-Null") | Out-Null } - # force input encoding to preamble-free UTF8 so PS sub-processes (eg, Start-Job) don't blow up - $ps.AddStatement().AddScript("[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false") | Out-Null + # force input encoding to preamble-free UTF8 so PS sub-processes (eg, + # Start-Job) don't blow up. This is only required for WinRM, a PSRP + # runspace doesn't have a host console and this will bomb out + if ($host.Name -eq "ConsoleHost") { + $ps.AddStatement().AddScript("[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false") | Out-Null + } $ps.AddStatement().AddScript($entrypoint) | Out-Null @@ -182,7 +186,7 @@ Function Run($payload) { # PS3 doesn't properly set HadErrors in many cases, inspect the error stream as a fallback If ($ps.HadErrors -or ($PSVersionTable.PSVersion.Major -lt 4 -and $ps.Streams.Error.Count -gt 0)) { - [System.Console]::Error.WriteLine($($ps.Streams.Error | Out-String)) + $host.UI.WriteErrorLine($($ps.Streams.Error | Out-String)) $exit_code = $ps.Runspace.SessionStateProxy.GetVariable("LASTEXITCODE") If(-not $exit_code) { $exit_code = 1 @@ -210,7 +214,7 @@ using System.Security.Principal; using System.Text; using System.Threading; -namespace Ansible +namespace AnsibleBecome { [StructLayout(LayoutKind.Sequential)] public class SECURITY_ATTRIBUTES @@ -931,6 +935,7 @@ $become_exec_wrapper = { } $exec_wrapper = { + &chcp.com 65001 > $null Set-StrictMode -Version 2 $DebugPreference = "Continue" $ErrorActionPreference = "Stop" @@ -1011,8 +1016,8 @@ Function Parse-EnumValue($enum, $flag_type, $value, $prefix) { } Function Parse-BecomeFlags($flags) { - $logon_type = [Ansible.LogonType]::LOGON32_LOGON_INTERACTIVE - $logon_flags = [Ansible.LogonFlags]::LOGON_WITH_PROFILE + $logon_type = [AnsibleBecome.LogonType]::LOGON32_LOGON_INTERACTIVE + $logon_flags = [AnsibleBecome.LogonFlags]::LOGON_WITH_PROFILE if ($flags -eq $null -or $flags -eq "") { $flag_split = @() @@ -1031,7 +1036,7 @@ Function Parse-BecomeFlags($flags) { $flag_value = $split[1] if ($flag_key -eq "logon_type") { $enum_details = @{ - enum = [Ansible.LogonType] + enum = [AnsibleBecome.LogonType] flag_type = $flag_key value = $flag_value prefix = "LOGON32_LOGON_" @@ -1039,13 +1044,13 @@ Function Parse-BecomeFlags($flags) { $logon_type = Parse-EnumValue @enum_details } elseif ($flag_key -eq "logon_flags") { $logon_flag_values = $flag_value.Split(",") - $logon_flags = 0 -as [Ansible.LogonFlags] + $logon_flags = 0 -as [AnsibleBecome.LogonFlags] foreach ($logon_flag_value in $logon_flag_values) { if ($logon_flag_value -eq "") { continue } $enum_details = @{ - enum = [Ansible.LogonFlags] + enum = [AnsibleBecome.LogonFlags] flag_type = $flag_key value = $logon_flag_value prefix = "LOGON_" @@ -1058,7 +1063,7 @@ Function Parse-BecomeFlags($flags) { } } - return $logon_type, [Ansible.LogonFlags]$logon_flags + return $logon_type, [AnsibleBecome.LogonFlags]$logon_flags } Function Run($payload) { @@ -1098,14 +1103,14 @@ Function Run($payload) { $lp_current_directory = "$env:SystemRoot" Try { - $result = [Ansible.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $exec_wrapper, $logon_flags, $logon_type) + $result = [AnsibleBecome.BecomeUtil]::RunAsUser($username, $password, $lp_command_line, $lp_current_directory, $exec_wrapper, $logon_flags, $logon_type) $stdout = $result.StandardOut $stdout = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($stdout.Trim())) $stderr = $result.StandardError $rc = $result.ExitCode - [Console]::Out.WriteLine($stdout) - [Console]::Error.WriteLine($stderr.Trim()) + $host.UI.WriteLine($stdout) + $host.UI.WriteErrorLine($stderr.Trim()) } Catch { $excep = $_ Dump-Error -excep $excep -msg "Failed to become user $username" @@ -1521,7 +1526,7 @@ class ShellModule(ShellBase): script = ''' $tmp_path = [System.Environment]::ExpandEnvironmentVariables('%s') $tmp = New-Item -Type Directory -Path $tmp_path -Name '%s' - $tmp.FullName | Write-Host -Separator '' + Write-Output -InputObject $tmp.FullName ''' % (basetmpdir, basefile) return self._encode_script(script.strip()) @@ -1531,11 +1536,11 @@ class ShellModule(ShellBase): # in the user's home directory. user_home_path = self._unquote(user_home_path) if user_home_path == '~': - script = 'Write-Host (Get-Location).Path' + script = 'Write-Output (Get-Location).Path' elif user_home_path.startswith('~\\'): - script = 'Write-Host ((Get-Location).Path + "%s")' % self._escape(user_home_path[1:]) + script = 'Write-Output ((Get-Location).Path + "%s")' % self._escape(user_home_path[1:]) else: - script = 'Write-Host "%s"' % self._escape(user_home_path) + script = 'Write-Output "%s"' % self._escape(user_home_path) return self._encode_script(script) def exists(self, path): @@ -1549,7 +1554,7 @@ class ShellModule(ShellBase): { $res = 1; } - Write-Host "$res"; + Write-Output "$res"; Exit $res; ''' % path return self._encode_script(script) @@ -1566,11 +1571,11 @@ class ShellModule(ShellBase): } ElseIf (Test-Path -PathType Container "%(path)s") { - Write-Host "3"; + Write-Output "3"; } Else { - Write-Host "1"; + Write-Output "1"; } ''' % dict(path=path) return self._encode_script(script) @@ -1633,7 +1638,7 @@ class ShellModule(ShellBase): return self._encode_script(script, preserve_rc=False) def wrap_for_exec(self, cmd): - return '& %s' % cmd + return '& %s; exit $LASTEXITCODE' % cmd def _unquote(self, value): '''Remove any matching quotes that wrap the given value.''' diff --git a/test/integration/targets/connection_psrp/aliases b/test/integration/targets/connection_psrp/aliases new file mode 100644 index 00000000000..cf714783f5c --- /dev/null +++ b/test/integration/targets/connection_psrp/aliases @@ -0,0 +1,3 @@ +windows +shippable/windows/group1 +shippable/windows/smoketest diff --git a/test/integration/targets/connection_psrp/runme.sh b/test/integration/targets/connection_psrp/runme.sh new file mode 100755 index 00000000000..61058c73cd8 --- /dev/null +++ b/test/integration/targets/connection_psrp/runme.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -eux + +pip install pypsrp +cd ../connection + +INVENTORY=../../inventory.winrm ./test.sh \ + -e target_hosts=winrm \ + -e action_prefix=win_ \ + -e local_tmp=/tmp/ansible-local \ + -e remote_tmp=c:/windows/temp/ansible-remote \ + -e ansible_psrp_cert_validation=False \ + -c psrp \ + "$@" diff --git a/test/runner/lib/classification.py b/test/runner/lib/classification.py index bbb3a66cdf3..5f1979dbe57 100644 --- a/test/runner/lib/classification.py +++ b/test/runner/lib/classification.py @@ -395,7 +395,7 @@ class PathMapper(object): # entire integration test commands depend on these connection plugins - if name == 'winrm': + if name in ['winrm', 'psrp']: return { 'windows-integration': self.integration_all_target, 'units': units_path, diff --git a/test/units/plugins/connection/test_psrp.py b/test/units/plugins/connection/test_psrp.py new file mode 100644 index 00000000000..2ea1d79225c --- /dev/null +++ b/test/units/plugins/connection/test_psrp.py @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +# (c) 2018, Jordan Borean +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from io import StringIO + +from ansible.playbook.play_context import PlayContext +from ansible.plugins.loader import connection_loader + +pytest.importorskip("pypsrp") + + +class TestConnectionWinRM(object): + + OPTIONS_DATA = ( + # default options + ( + {'_extras': {}}, + { + '_psrp_auth': 'negotiate', + '_psrp_cert_validation': True, + '_psrp_configuration_name': 'Microsoft.PowerShell', + '_psrp_connection_timeout': 30, + '_psrp_message_encryption': 'auto', + '_psrp_host': 'inventory_hostname', + '_psrp_conn_kwargs': { + 'server': 'inventory_hostname', + 'port': 5986, + 'username': None, + 'password': '', + 'ssl': True, + 'path': 'wsman', + 'auth': 'negotiate', + 'cert_validation': True, + 'connection_timeout': 30, + 'encryption': 'auto', + 'proxy': None, + 'no_proxy': False, + 'max_envelope_size': 153600, + 'operation_timeout': 20, + }, + '_psrp_max_envelope_size': 153600, + '_psrp_ignore_proxy': False, + '_psrp_operation_timeout': 20, + '_psrp_pass': '', + '_psrp_path': 'wsman', + '_psrp_port': 5986, + '_psrp_proxy': None, + '_psrp_protocol': 'https', + '_psrp_user': None + }, + ), + # ssl=False when port defined to 5985 + ( + {'_extras': {}, 'ansible_port': '5985'}, + { + '_psrp_port': 5985, + '_psrp_protocol': 'http' + }, + ), + # ssl=True when port defined to not 5985 + ( + {'_extras': {}, 'ansible_port': 1234}, + { + '_psrp_port': 1234, + '_psrp_protocol': 'https' + }, + ), + # port 5986 when ssl=True + ( + {'_extras': {}, 'ansible_psrp_protocol': 'https'}, + { + '_psrp_port': 5986, + '_psrp_protocol': 'https' + }, + ), + # port 5985 when ssl=False + ( + {'_extras': {}, 'ansible_psrp_protocol': 'http'}, + { + '_psrp_port': 5985, + '_psrp_protocol': 'http' + }, + ), + # psrp extras + ( + {'_extras': {'ansible_psrp_negotiate_delegate': True}}, + { + '_psrp_conn_kwargs': { + 'server': 'inventory_hostname', + 'port': 5986, + 'username': None, + 'password': '', + 'ssl': True, + 'path': 'wsman', + 'auth': 'negotiate', + 'cert_validation': True, + 'connection_timeout': 30, + 'encryption': 'auto', + 'proxy': None, + 'no_proxy': False, + 'max_envelope_size': 153600, + 'operation_timeout': 20, + 'negotiate_delegate': True, + + }, + }, + ), + # cert validation through string repr of bool + ( + {'_extras': {}, 'ansible_psrp_cert_validation': 'ignore'}, + { + '_psrp_cert_validation': False + }, + ), + # cert validation path + ( + {'_extras': {}, 'ansible_psrp_cert_trust_path': '/path/cert.pem'}, + { + '_psrp_cert_validation': '/path/cert.pem' + }, + ), + ) + + # pylint bug: https://github.com/PyCQA/pylint/issues/511 + # pylint: disable=undefined-variable + @pytest.mark.parametrize('options, expected', + ((o, e) for o, e in OPTIONS_DATA)) + def test_set_options(self, options, expected): + pc = PlayContext() + new_stdin = StringIO() + + conn = connection_loader.get('psrp', pc, new_stdin) + conn.set_options(var_options=options) + conn._build_kwargs() + + for attr, expected in expected.items(): + actual = getattr(conn, attr) + assert actual == expected, \ + "psrp attr '%s', actual '%s' != expected '%s'"\ + % (attr, actual, expected)