Connection plugins network_cli and netconf (#32521)

* implements jsonrpc message passing for ansible-connection

* implements more generic mechanism for persistent connections
* starts persistent connection in task_executor if enabled and supported
* supports using network_cli as top level connection plugin
* enhances logging for persistent connection to stdout

* Update action plugins

* Fix Python3 RPC

* Fix Junos bytes<-->str issues

* supports using netconf as top level connection plugin

* Error message when running netconf on an unsupported platform
* Update tests

* Fix `authorize: yes` for `connection: local`

* Handle potentially JSON data in terminal

* Add clarifying detail if possible on ConnectionError
This commit is contained in:
Nathaniel Case 2017-11-09 15:04:40 -05:00 committed by GitHub
parent 897b31f249
commit 9c0275a879
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 722 additions and 798 deletions

View file

@ -1,21 +1,7 @@
# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.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/>.
# Make coding more python3-ish
# (c) 2017, Peter Sprygada <psprygad@redhat.com>
# (c) 2017 Ansible Project
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
@ -69,6 +55,11 @@ class ConnectionBase(AnsiblePlugin):
module_implementation_preferences = ('',)
allow_executable = True
# the following control whether or not the connection supports the
# persistent connection framework or not
supports_persistence = False
force_persistence = False
def __init__(self, play_context, new_stdin, *args, **kwargs):
super(ConnectionBase, self).__init__()
@ -88,6 +79,8 @@ class ConnectionBase(AnsiblePlugin):
self.prompt = None
self._connected = False
self._socket_path = None
# load the shell plugin for this action/connection
if play_context.shell:
shell_type = play_context.shell
@ -110,6 +103,11 @@ class ConnectionBase(AnsiblePlugin):
'''Read-only property holding whether the connection to the remote host is active or closed.'''
return self._connected
@property
def socket_path(self):
'''Read-only property holding the connection socket path for this remote host'''
return self._socket_path
def _become_method_supported(self):
''' Checks if the current class supports this privilege escalation method '''

View file

@ -71,15 +71,14 @@ DOCUMENTATION = """
import os
import logging
import json
from ansible import constants as C
from ansible.errors import AnsibleConnectionFailure, AnsibleError
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.module_utils._text import to_bytes, to_native
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE
from ansible.plugins.loader import netconf_loader
from ansible.plugins.connection import ConnectionBase, ensure_connect
from ansible.utils.jsonrpc import Rpc
from ansible.plugins.connection.local import Connection as LocalConnection
try:
from ncclient import manager
@ -98,11 +97,12 @@ except ImportError:
logging.getLogger('ncclient').setLevel(logging.INFO)
class Connection(Rpc, ConnectionBase):
class Connection(ConnectionBase):
"""NetConf connections"""
transport = 'netconf'
has_pipelining = False
force_persistence = True
def __init__(self, play_context, new_stdin, *args, **kwargs):
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
@ -113,18 +113,50 @@ class Connection(Rpc, ConnectionBase):
self._manager = None
self._connected = False
self._local = LocalConnection(play_context, new_stdin, *args, **kwargs)
def exec_command(self, request, in_data=None, sudoable=True):
"""Sends the request to the node and returns the reply
The method accepts two forms of request. The first form is as a byte
string that represents xml string be send over netconf session.
The second form is a json-rpc (2.0) byte string.
"""
if self._manager:
# to_ele operates on native strings
request = to_ele(to_native(request, errors='surrogate_or_strict'))
if request is None:
return 'unable to parse request'
try:
reply = self._manager.rpc(request)
except RPCError as exc:
return to_xml(exc.xml)
return reply.data_xml
else:
return self._local.exec_command(request, in_data, sudoable)
def put_file(self, in_path, out_path):
"""Transfer a file from local to remote"""
return self._local.put_file(in_path, out_path)
def fetch_file(self, in_path, out_path):
"""Fetch a file from remote to local"""
return self._local.fetch_file(in_path, out_path)
def _connect(self):
super(Connection, self)._connect()
display.display('ssh connection done, stating ncclient', log_only=True)
display.display('ssh connection done, starting ncclient', log_only=True)
self.allow_agent = True
allow_agent = True
if self._play_context.password is not None:
self.allow_agent = False
allow_agent = False
self.key_filename = None
key_filename = None
if self._play_context.private_key_file:
self.key_filename = os.path.expanduser(self._play_context.private_key_file)
key_filename = os.path.expanduser(self._play_context.private_key_file)
network_os = self._play_context.network_os
@ -149,16 +181,18 @@ class Connection(Rpc, ConnectionBase):
port=self._play_context.port or 830,
username=self._play_context.remote_user,
password=self._play_context.password,
key_filename=str(self.key_filename),
key_filename=str(key_filename),
hostkey_verify=C.HOST_KEY_CHECKING,
look_for_keys=C.PARAMIKO_LOOK_FOR_KEYS,
allow_agent=self.allow_agent,
allow_agent=allow_agent,
timeout=self._play_context.timeout,
device_params={'name': network_os},
ssh_config=ssh_config
)
except SSHUnknownHostError as exc:
raise AnsibleConnectionFailure(str(exc))
except ImportError as exc:
raise AnsibleError("connection=netconf is not supported on {0}".format(network_os))
if not self._manager.connected:
return 1, b'', b'not connected'
@ -169,7 +203,6 @@ class Connection(Rpc, ConnectionBase):
self._netconf = netconf_loader.get(network_os, self)
if self._netconf:
self._rpc.add(self._netconf)
display.display('loaded netconf plugin for network_os %s' % network_os, log_only=True)
else:
display.display('unable to load netconf for network_os %s' % network_os)
@ -181,46 +214,3 @@ class Connection(Rpc, ConnectionBase):
self._manager.close_session()
self._connected = False
super(Connection, self).close()
@ensure_connect
def exec_command(self, request):
"""Sends the request to the node and returns the reply
The method accepts two forms of request. The first form is as a byte
string that represents xml string be send over netconf session.
The second form is a json-rpc (2.0) byte string.
"""
try:
obj = json.loads(to_text(request, errors='surrogate_or_strict'))
if 'jsonrpc' in obj:
if self._netconf:
out = self._exec_rpc(obj)
else:
out = self.internal_error("netconf plugin is not supported for network_os %s" % self._play_context.network_os)
return 0, to_bytes(out, errors='surrogate_or_strict'), b''
else:
err = self.invalid_request(obj)
return 1, b'', to_bytes(err, errors='surrogate_or_strict')
except (ValueError, TypeError):
# to_ele operates on native strings
request = to_native(request, errors='surrogate_or_strict')
req = to_ele(request)
if req is None:
return 1, b'', b'unable to parse request'
try:
reply = self._manager.rpc(req)
except RPCError as exc:
return 1, b'', to_bytes(to_xml(exc.xml), errors='surrogate_or_strict')
return 0, to_bytes(reply.data_xml, errors='surrogate_or_strict'), b''
def put_file(self, in_path, out_path):
"""Transfer a file from local to remote"""
pass
def fetch_file(self, in_path, out_path):
"""Fetch a file from remote to local"""
pass

View file

@ -47,6 +47,7 @@ DOCUMENTATION = """
import json
import logging
import re
import os
import signal
import socket
import traceback
@ -57,9 +58,11 @@ from ansible import constants as C
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils.six import BytesIO, binary_type
from ansible.module_utils._text import to_bytes, to_text
from ansible.plugins.loader import cliconf_loader, terminal_loader
from ansible.plugins.connection.paramiko_ssh import Connection as _Connection
from ansible.utils.jsonrpc import Rpc
from ansible.plugins.loader import cliconf_loader, terminal_loader, connection_loader
from ansible.plugins.connection import ConnectionBase
from ansible.plugins.connection.local import Connection as LocalConnection
from ansible.plugins.connection.paramiko_ssh import Connection as ParamikoSshConnection
from ansible.utils.path import unfrackpath, makedirs_safe
try:
from __main__ import display
@ -68,31 +71,73 @@ except ImportError:
display = Display()
class Connection(Rpc, _Connection):
class Connection(ConnectionBase):
''' CLI (shell) SSH connections on Paramiko '''
transport = 'network_cli'
has_pipelining = True
force_persistence = True
def __init__(self, play_context, new_stdin, *args, **kwargs):
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
self._terminal = None
self._cliconf = None
self._shell = None
self.ssh = None
self._ssh_shell = None
self._matched_prompt = None
self._matched_pattern = None
self._last_response = None
self._history = list()
self._play_context = play_context
if play_context.verbosity > 3:
self._local = LocalConnection(play_context, new_stdin, *args, **kwargs)
self._terminal = None
self._cliconf = None
if self._play_context.verbosity > 3:
logging.getLogger('paramiko').setLevel(logging.DEBUG)
# reconstruct the socket_path and set instance values accordingly
self._update_connection_state()
def __getattr__(self, name):
try:
return self.__dict__[name]
except KeyError:
if name.startswith('_'):
raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))
return getattr(self._cliconf, name)
def exec_command(self, cmd, in_data=None, sudoable=True):
# this try..except block is just to handle the transition to supporting
# network_cli as a toplevel connection. Once connection=local is gone,
# this block can be removed as well and all calls passed directly to
# the local connection
if self._ssh_shell:
try:
cmd = json.loads(to_text(cmd, errors='surrogate_or_strict'))
kwargs = {'command': to_bytes(cmd['command'], errors='surrogate_or_strict')}
for key in ('prompts', 'answer', 'send_only'):
if key in cmd:
kwargs[key] = to_bytes(cmd[key], errors='surrogate_or_strict')
return self.send(**kwargs)
except ValueError:
cmd = to_bytes(cmd, errors='surrogate_or_strict')
return self.send(command=cmd)
else:
return self._local.exec_command(cmd, in_data, sudoable)
def put_file(self, in_path, out_path):
return self._local.put_file(in_path, out_path)
def fetch_file(self, in_path, out_path):
return self._local.fetch_file(in_path, out_path)
def update_play_context(self, play_context):
"""Updates the play context information for the connection"""
display.display('updating play_context for connection', log_only=True)
display.vvvv('updating play_context for connection', host=self._play_context.remote_addr)
if self._play_context.become is False and play_context.become is True:
auth_pass = play_context.become_pass
@ -104,17 +149,22 @@ class Connection(Rpc, _Connection):
self._play_context = play_context
def _connect(self):
"""Connections to the device and sets the terminal type"""
'''
Connects to the remote device and starts the terminal
'''
if self.connected:
return
if self._play_context.password and not self._play_context.private_key_file:
C.PARAMIKO_LOOK_FOR_KEYS = False
super(Connection, self)._connect()
ssh = ParamikoSshConnection(self._play_context, '/dev/null')._connect()
self.ssh = ssh.ssh
display.display('ssh connection done, setting terminal', log_only=True)
display.vvvv('ssh connection done, setting terminal', host=self._play_context.remote_addr)
self._shell = self.ssh.invoke_shell()
self._shell.settimeout(self._play_context.timeout)
self._ssh_shell = self.ssh.invoke_shell()
self._ssh_shell.settimeout(self._play_context.timeout)
network_os = self._play_context.network_os
if not network_os:
@ -127,53 +177,83 @@ class Connection(Rpc, _Connection):
if not self._terminal:
raise AnsibleConnectionFailure('network os %s is not supported' % network_os)
display.display('loaded terminal plugin for network_os %s' % network_os, log_only=True)
display.vvvv('loaded terminal plugin for network_os %s' % network_os, host=self._play_context.remote_addr)
self._cliconf = cliconf_loader.get(network_os, self)
if self._cliconf:
self._rpc.add(self._cliconf)
display.display('loaded cliconf plugin for network_os %s' % network_os, log_only=True)
display.vvvv('loaded cliconf plugin for network_os %s' % network_os, host=self._play_context.remote_addr)
else:
display.display('unable to load cliconf for network_os %s' % network_os)
display.vvvv('unable to load cliconf for network_os %s' % network_os)
self.receive()
display.display('firing event: on_open_shell()', log_only=True)
display.vvvv('firing event: on_open_shell()', host=self._play_context.remote_addr)
self._terminal.on_open_shell()
if getattr(self._play_context, 'become', None):
display.display('firing event: on_authorize', log_only=True)
if self._play_context.become and self._play_context.become_method == 'enable':
display.vvvv('firing event: on_authorize', host=self._play_context.remote_addr)
auth_pass = self._play_context.become_pass
self._terminal.on_authorize(passwd=auth_pass)
display.vvvv('ssh connection has completed successfully', host=self._play_context.remote_addr)
self._connected = True
display.display('ssh connection has completed successfully', log_only=True)
return self
def _update_connection_state(self):
'''
Reconstruct the connection socket_path and check if it exists
If the socket path exists then the connection is active and set
both the _socket_path value to the path and the _connected value
to True. If the socket path doesn't exist, leave the socket path
value to None and the _connected value to False
'''
ssh = connection_loader.get('ssh', class_only=True)
cp = ssh._create_control_path(self._play_context.remote_addr, self._play_context.port, self._play_context.remote_user)
tmp_path = unfrackpath(C.PERSISTENT_CONTROL_PATH_DIR)
socket_path = unfrackpath(cp % dict(directory=tmp_path))
if os.path.exists(socket_path):
self._connected = True
self._socket_path = socket_path
def reset(self):
'''
Reset the connection
'''
if self._socket_path:
display.vvvv('resetting persistent connection for socket_path %s' % self._socket_path, host=self._play_context.remote_addr)
self.shutdown()
def close(self):
"""Close the active connection to the device
"""
display.display("closing ssh connection to device", log_only=True)
if self._shell:
display.display("firing event: on_close_shell()", log_only=True)
self._terminal.on_close_shell()
self._shell.close()
self._shell = None
display.display("cli session is now closed", log_only=True)
super(Connection, self).close()
self._connected = False
display.display("ssh connection has been closed successfully", log_only=True)
'''
Close the active connection to the device
'''
# only close the connection if its connected.
if self._connected:
display.debug("closing ssh connection to device")
if self._ssh_shell:
display.debug("firing event: on_close_shell()")
self._terminal.on_close_shell()
self._ssh_shell.close()
self._ssh_shell = None
display.debug("cli session is now closed")
self._connected = False
display.debug("ssh connection has been closed successfully")
def receive(self, command=None, prompts=None, answer=None):
"""Handles receiving of output from command"""
'''
Handles receiving of output from command
'''
recv = BytesIO()
handled = False
self._matched_prompt = None
while True:
data = self._shell.recv(256)
data = self._ssh_shell.recv(256)
recv.write(data)
offset = recv.tell() - 256 if recv.tell() > 256 else 0
@ -190,25 +270,30 @@ class Connection(Rpc, _Connection):
return self._sanitize(resp, command)
def send(self, command, prompts=None, answer=None, send_only=False):
"""Sends the command to the device in the opened shell"""
'''
Sends the command to the device in the opened shell
'''
try:
self._history.append(command)
self._shell.sendall(b'%s\r' % command)
self._ssh_shell.sendall(b'%s\r' % command)
if send_only:
return
return self.receive(command, prompts, answer)
response = self.receive(command, prompts, answer)
return to_text(response, errors='surrogate_or_strict')
except (socket.timeout, AttributeError):
display.display(traceback.format_exc(), log_only=True)
display.vvvv(traceback.format_exc(), host=self._play_context.remote_addr)
raise AnsibleConnectionFailure("timeout trying to send command: %s" % command.strip())
def _strip(self, data):
"""Removes ANSI codes from device response"""
'''
Removes ANSI codes from device response
'''
for regex in self._terminal.ansi_re:
data = regex.sub(b'', data)
return data
def _handle_prompt(self, resp, prompts, answer):
"""
'''
Matches the command prompt and responds
:arg resp: Byte string containing the raw response from the remote
@ -216,17 +301,19 @@ class Connection(Rpc, _Connection):
:arg answer: Byte string to send back to the remote if we find a prompt.
A carriage return is automatically appended to this string.
:returns: True if a prompt was found in ``resp``. False otherwise
"""
'''
prompts = [re.compile(r, re.I) for r in prompts]
for regex in prompts:
match = regex.search(resp)
if match:
self._shell.sendall(b'%s\r' % answer)
self._ssh_shell.sendall(b'%s\r' % answer)
return True
return False
def _sanitize(self, resp, command=None):
"""Removes elements from the response before returning to the caller"""
'''
Removes elements from the response before returning to the caller
'''
cleaned = []
for line in resp.splitlines():
if (command and line.strip() == command.strip()) or self._matched_prompt.strip() in line:
@ -235,7 +322,8 @@ class Connection(Rpc, _Connection):
return b'\n'.join(cleaned).strip()
def _find_prompt(self, response):
"""Searches the buffered response for a matching command prompt"""
'''Searches the buffered response for a matching command prompt
'''
errored_response = None
is_error_message = False
for regex in self._terminal.terminal_stderr_re:
@ -264,64 +352,3 @@ class Connection(Rpc, _Connection):
raise AnsibleConnectionFailure(errored_response)
return False
def alarm_handler(self, signum, frame):
"""Alarm handler raised in case of command timeout """
display.display('closing shell due to sigalarm', log_only=True)
self.close()
def exec_command(self, cmd):
"""Executes the cmd on in the shell and returns the output
The method accepts three forms of cmd. The first form is as a byte
string that represents the command to be executed in the shell. The
second form is as a utf8 JSON byte string with additional keywords.
The third form is a json-rpc (2.0)
Keywords supported for cmd:
:command: the command string to execute
:prompt: the expected prompt generated by executing command.
This can be a string or a list of strings
:answer: the string to respond to the prompt with
:sendonly: bool to disable waiting for response
:arg cmd: the byte string that represents the command to be executed
which can be a single command or a json encoded string.
:returns: a tuple of (return code, stdout, stderr). The return
code is an integer and stdout and stderr are byte strings
"""
try:
obj = json.loads(to_text(cmd, errors='surrogate_or_strict'))
except (ValueError, TypeError):
obj = {'command': to_bytes(cmd.strip(), errors='surrogate_or_strict')}
obj = dict((k, to_bytes(v, errors='surrogate_or_strict', nonstring='passthru')) for k, v in obj.items())
if 'prompt' in obj:
if isinstance(obj['prompt'], binary_type):
# Prompt was a string
obj['prompt'] = [obj['prompt']]
elif not isinstance(obj['prompt'], Sequence):
# Convert nonstrings into byte strings (to_bytes(5) => b'5')
if obj['prompt'] is not None:
obj['prompt'] = [to_bytes(obj['prompt'], errors='surrogate_or_strict')]
else:
# Prompt was a Sequence of strings. Make sure they're byte strings
obj['prompt'] = [to_bytes(p, errors='surrogate_or_strict') for p in obj['prompt'] if p is not None]
if 'jsonrpc' in obj:
if self._cliconf:
out = self._exec_rpc(obj)
else:
out = self.internal_error("cliconf is not supported for network_os %s" % self._play_context.network_os)
return 0, to_bytes(out, errors='surrogate_or_strict'), b''
if obj['command'] == b'prompt()':
return 0, self._matched_prompt, b''
try:
if not signal.getsignal(signal.SIGALRM):
signal.signal(signal.SIGALRM, self.alarm_handler)
signal.alarm(self._play_context.timeout)
out = self.send(obj['command'], obj.get('prompt'), obj.get('answer'), obj.get('sendonly'))
signal.alarm(0)
return 0, out, b''
except (AnsibleConnectionFailure, ValueError) as exc:
return 1, b'', to_bytes(exc)

View file

@ -100,6 +100,7 @@ with warnings.catch_warnings():
class MyAddPolicy(object):
"""
Based on AutoAddPolicy in paramiko so we can determine when keys are added
and also prompt for input.
Policy for automatically adding the hostname and new host key to the
@ -114,8 +115,13 @@ class MyAddPolicy(object):
if all((C.HOST_KEY_CHECKING, not C.PARAMIKO_HOST_KEY_AUTO_ADD)):
fingerprint = hexlify(key.get_fingerprint())
ktype = key.get_name()
if C.USE_PERSISTENT_CONNECTIONS:
raise AnsibleConnectionFailure('rejected %s host key for host %s: %s' % (key.get_name(), hostname, hexlify(key.get_fingerprint())))
# don't print the prompt string since the user cannot respond
# to the question anyway
raise AnsibleError(AUTHENTICITY_MSG[1:92] % (hostname, ktype, fingerprint))
self.connection.connection_lock()
@ -125,9 +131,6 @@ class MyAddPolicy(object):
# clear out any premature input on sys.stdin
tcflush(sys.stdin, TCIFLUSH)
fingerprint = hexlify(key.get_fingerprint())
ktype = key.get_name()
inp = input(AUTHENTICITY_MSG % (hostname, ktype, fingerprint))
sys.stdin = old_stdin

View file

@ -1,4 +1,4 @@
# (c) 2017 Red Hat Inc.
# 2017 Red Hat Inc.
# (c) 2017 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
@ -13,15 +13,19 @@ DOCUMENTATION = """
- This is a helper plugin to allow making other connections persistent.
version_added: "2.3"
"""
import re
import os
import sys
import pty
import json
import subprocess
from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.six.moves import cPickle
from ansible import constants as C
from ansible.plugins.loader import connection_loader
from ansible.plugins.connection import ConnectionBase
from ansible.module_utils._text import to_text
from ansible.module_utils.six.moves import cPickle
from ansible.module_utils.connection import Connection as SocketConnection
from ansible.errors import AnsibleError
try:
from __main__ import display
@ -40,8 +44,38 @@ class Connection(ConnectionBase):
self._connected = True
return self
def _do_it(self, action):
def exec_command(self, cmd, in_data=None, sudoable=True):
display.vvvv('exec_command(), socket_path=%s' % self.socket_path, host=self._play_context.remote_addr)
connection = SocketConnection(self.socket_path)
out = connection.exec_command(cmd, in_data=in_data, sudoable=sudoable)
return 0, out, ''
def put_file(self, in_path, out_path):
pass
def fetch_file(self, in_path, out_path):
pass
def close(self):
self._connected = False
def run(self):
"""Returns the path of the persistent connection socket.
Attempts to ensure (within playcontext.timeout seconds) that the
socket path exists. If the path exists (or the timeout has expired),
returns the socket path.
"""
display.vvvv('starting connection from persistent connection plugin', host=self._play_context.remote_addr)
socket_path = self._start_connection()
display.vvvv('local domain socket path is %s' % socket_path, host=self._play_context.remote_addr)
setattr(self, '_socket_path', socket_path)
return socket_path
def _start_connection(self):
'''
Starts the persistent connection
'''
master, slave = pty.openpty()
p = subprocess.Popen(["ansible-connection"], stdin=slave, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdin = os.fdopen(master, 'wb', 0)
@ -56,40 +90,23 @@ class Connection(ConnectionBase):
stdin.write(src)
stdin.write(b'\n#END_INIT#\n')
stdin.write(to_bytes(action))
stdin.write(b'\n\n')
(stdout, stderr) = p.communicate()
stdin.close()
return (p.returncode, stdout, stderr)
if p.returncode == 0:
result = json.loads(to_text(stdout, errors='surrogate_then_replace'))
else:
result = json.loads(to_text(stderr, errors='surrogate_then_replace'))
def exec_command(self, cmd, in_data=None, sudoable=True):
super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)
return self._do_it('EXEC: ' + cmd)
if 'messages' in result:
for msg in result.get('messages'):
display.vvvv('%s' % msg, host=self._play_context.remote_addr)
def put_file(self, in_path, out_path):
super(Connection, self).put_file(in_path, out_path)
self._do_it('PUT: %s %s' % (in_path, out_path))
if 'error' in result:
if self._play_context.verbosity > 2:
msg = "The full traceback is:\n" + result['exception']
display.display(result['exception'], color=C.COLOR_ERROR)
raise AnsibleError(result['error'])
def fetch_file(self, in_path, out_path):
super(Connection, self).fetch_file(in_path, out_path)
self._do_it('FETCH: %s %s' % (in_path, out_path))
def close(self):
self._connected = False
def run(self):
"""Returns the path of the persistent connection socket.
Attempts to ensure (within playcontext.timeout seconds) that the
socket path exists. If the path exists (or the timeout has expired),
returns the socket path.
"""
socket_path = None
rc, out, err = self._do_it('RUN:')
match = re.search(br"#SOCKET_PATH#: (\S+)", out)
if match:
socket_path = to_text(match.group(1).strip(), errors='surrogate_or_strict')
return socket_path
return result['socket_path']