mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-24 11:21:25 -07:00
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:
parent
897b31f249
commit
9c0275a879
26 changed files with 722 additions and 798 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue