mirror of https://github.com/ansible/ansible.git
psrp: Added new Windows connection plugin (#41729)
* psrp: Added new Windows connection plugin * Tweaks to connection options from reviewpull/44490/merge
parent
07a011cd6f
commit
6982dfc756
@ -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
|
@ -0,0 +1,3 @@
|
||||
windows
|
||||
shippable/windows/group1
|
||||
shippable/windows/smoketest
|
@ -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 \
|
||||
"$@"
|
@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# (c) 2018, Jordan Borean <jborean@redhat.com>
|
||||
# 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)
|
Loading…
Reference in New Issue