Remove cliconf from httpapi connection (#46813)

* Bare minimum rip out cliconf

* nxapi changeover

* Update documentation, move options

* Memoize device_info

* Gratuitous rename to underscore use of local api implementation

Fixup eos module_utils like nxos

* Streamline version and image scans

* Expose get_capabilities through module_utils

* Add load_config to module_utils

* Support rpcs using both args and kwargs

* Add get_config for nxos

* Add get_diff

* module context, pulled from nxapi

We could probably do this correctly later

* Fix eos issues

* Limit connection._sub_plugin to only one plugin
pull/49799/head
Nathaniel Case 6 years ago committed by GitHub
parent 32dbb99bb8
commit 02432565cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -900,9 +900,9 @@ class TaskExecutor:
final_vars = combine_vars(variables, variables.get('ansible_delegated_vars', dict()).get(self._task.delegate_to, dict())) final_vars = combine_vars(variables, variables.get('ansible_delegated_vars', dict()).get(self._task.delegate_to, dict()))
option_vars = C.config.get_plugin_vars('connection', connection._load_name) option_vars = C.config.get_plugin_vars('connection', connection._load_name)
for plugin in connection._sub_plugins: plugin = connection._sub_plugin
if plugin['type'] != 'external': if plugin['type'] != 'external':
option_vars.extend(C.config.get_plugin_vars(plugin['type'], plugin['name'])) option_vars.extend(C.config.get_plugin_vars(plugin['type'], plugin['name']))
options = {} options = {}
for k in option_vars: for k in option_vars:

@ -100,10 +100,7 @@ def exec_command(module, command):
def request_builder(method_, *args, **kwargs): def request_builder(method_, *args, **kwargs):
reqid = str(uuid.uuid4()) reqid = str(uuid.uuid4())
req = {'jsonrpc': '2.0', 'method': method_, 'id': reqid} req = {'jsonrpc': '2.0', 'method': method_, 'id': reqid}
req['params'] = (args, kwargs)
params = args or kwargs or None
if params:
req['params'] = params
return req return req

@ -27,6 +27,7 @@
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# #
import json
import os import os
import time import time
@ -99,10 +100,15 @@ def get_connection(module):
global _DEVICE_CONNECTION global _DEVICE_CONNECTION
if not _DEVICE_CONNECTION: if not _DEVICE_CONNECTION:
load_params(module) load_params(module)
if is_eapi(module): if is_local_eapi(module):
conn = Eapi(module) conn = LocalEapi(module)
else: else:
conn = Cli(module) connection_proxy = Connection(module._socket_path)
cap = json.loads(connection_proxy.get_capabilities())
if cap['network_api'] == 'cliconf':
conn = Cli(module)
elif cap['network_api'] == 'eapi':
conn = HttpApi(module)
_DEVICE_CONNECTION = conn _DEVICE_CONNECTION = conn
return _DEVICE_CONNECTION return _DEVICE_CONNECTION
@ -180,7 +186,7 @@ class Cli:
return diff return diff
class Eapi: class LocalEapi:
def __init__(self, module): def __init__(self, module):
self._module = module self._module = module
@ -394,18 +400,187 @@ class Eapi:
return diff return diff
class HttpApi:
def __init__(self, module):
self._module = module
self._device_configs = {}
self._session_support = None
self._connection_obj = None
@property
def _connection(self):
if not self._connection_obj:
self._connection_obj = Connection(self._module._socket_path)
return self._connection_obj
def run_commands(self, commands, check_rc=True):
"""Runs list of commands on remote device and returns results
"""
output = None
queue = list()
responses = list()
def run_queue(queue, output):
try:
response = to_list(self._connection.send_request(queue, output=output))
except Exception as exc:
if check_rc:
raise
return to_text(exc)
if output == 'json':
response = [json.loads(item) for item in response]
return response
for item in to_list(commands):
cmd_output = 'text'
if isinstance(item, dict):
command = item['command']
if 'output' in item:
cmd_output = item['output']
else:
command = item
# Emulate '| json' from CLI
if is_json(command):
command = command.rsplit('|', 1)[0]
cmd_output = 'json'
if output and output != cmd_output:
responses.extend(run_queue(queue, output))
queue = list()
output = cmd_output
queue.append(command)
if queue:
responses.extend(run_queue(queue, output))
return responses
def get_config(self, flags=None):
"""Retrieves the current config from the device or cache
"""
flags = [] if flags is None else flags
cmd = 'show running-config '
cmd += ' '.join(flags)
cmd = cmd.strip()
try:
return self._device_configs[cmd]
except KeyError:
try:
out = self._connection.send_request(cmd)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
cfg = to_text(out).strip()
self._device_configs[cmd] = cfg
return cfg
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
diff = {}
# prepare candidate configuration
candidate_obj = NetworkConfig(indent=3)
candidate_obj.load(candidate)
if running and diff_match != 'none' and diff_replace != 'config':
# running configuration
running_obj = NetworkConfig(indent=3, contents=running, ignore_lines=diff_ignore_lines)
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
else:
configdiffobjs = candidate_obj.items
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else {}
return diff
def load_config(self, config, commit=False, replace=False):
"""Loads the configuration onto the remote devices
If the device doesn't support configuration sessions, this will
fallback to using configure() to load the commands. If that happens,
there will be no returned diff or session values
"""
return self.edit_config(config, commit, replace)
def edit_config(self, config, commit=False, replace=False):
"""Loads the configuration onto the remote devices
If the device doesn't support configuration sessions, this will
fallback to using configure() to load the commands. If that happens,
there will be no returned diff or session values
"""
session = 'ansible_%s' % int(time.time())
result = {'session': session}
banner_cmd = None
banner_input = []
commands = ['configure session %s' % session]
if replace:
commands.append('rollback clean-config')
for command in config:
if command.startswith('banner'):
banner_cmd = command
banner_input = []
elif banner_cmd:
if command == 'EOF':
command = {'cmd': banner_cmd, 'input': '\n'.join(banner_input)}
banner_cmd = None
commands.append(command)
else:
banner_input.append(command)
continue
else:
commands.append(command)
try:
response = self._connection.send_request(commands)
except Exception:
commands = ['configure session %s' % session, 'abort']
response = self._connection.send_request(commands, output='text')
raise
commands = ['configure session %s' % session, 'show session-config diffs']
if commit:
commands.append('commit')
else:
commands.append('abort')
response = self._connection.send_request(commands, output='text')
diff = response[1].strip()
if diff:
result['diff'] = diff
return result
def get_capabilities(self):
"""Returns platform info of the remove device
"""
try:
capabilities = self._connection.get_capabilities()
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
return json.loads(capabilities)
def is_json(cmd): def is_json(cmd):
return to_native(cmd, errors='surrogate_then_replace').endswith('| json') return to_text(cmd, errors='surrogate_then_replace').endswith('| json')
def is_eapi(module): def is_local_eapi(module):
transport = module.params['transport'] transport = module.params['transport']
provider_transport = (module.params['provider'] or {}).get('transport') provider_transport = (module.params['provider'] or {}).get('transport')
return 'eapi' in (transport, provider_transport) return 'eapi' in (transport, provider_transport)
def to_command(module, commands): def to_command(module, commands):
if is_eapi(module): if is_local_eapi(module):
default_output = 'json' default_output = 'json'
else: else:
default_output = 'text' default_output = 'text'

@ -105,10 +105,15 @@ def get_connection(module):
global _DEVICE_CONNECTION global _DEVICE_CONNECTION
if not _DEVICE_CONNECTION: if not _DEVICE_CONNECTION:
load_params(module) load_params(module)
if is_nxapi(module): if is_local_nxapi(module):
conn = Nxapi(module) conn = LocalNxapi(module)
else: else:
conn = Cli(module) connection_proxy = Connection(module._socket_path)
cap = json.loads(connection_proxy.get_capabilities())
if cap['network_api'] == 'cliconf':
conn = Cli(module)
elif cap['network_api'] == 'nxapi':
conn = HttpApi(module)
_DEVICE_CONNECTION = conn _DEVICE_CONNECTION = conn
return _DEVICE_CONNECTION return _DEVICE_CONNECTION
@ -244,7 +249,7 @@ class Cli:
return None return None
class Nxapi: class LocalNxapi:
OUTPUT_TO_COMMAND_TYPE = { OUTPUT_TO_COMMAND_TYPE = {
'text': 'cli_show_ascii', 'text': 'cli_show_ascii',
@ -496,22 +501,178 @@ class Nxapi:
return None return None
class HttpApi:
def __init__(self, module):
self._module = module
self._device_configs = {}
self._module_context = {}
self._connection_obj = None
@property
def _connection(self):
if not self._connection_obj:
self._connection_obj = Connection(self._module._socket_path)
return self._connection_obj
def run_commands(self, commands, check_rc=True):
"""Runs list of commands on remote device and returns results
"""
try:
out = self._connection.send_request(commands)
except ConnectionError as exc:
if check_rc is True:
raise
out = to_text(exc)
out = to_list(out)
if not out[0]:
return out
for index, response in enumerate(out):
if response[0] == '{':
out[index] = json.loads(response)
return out
def get_config(self, flags=None):
"""Retrieves the current config from the device or cache
"""
flags = [] if flags is None else flags
cmd = 'show running-config '
cmd += ' '.join(flags)
cmd = cmd.strip()
try:
return self._device_configs[cmd]
except KeyError:
try:
out = self._connection.send_request(cmd)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
cfg = to_text(out).strip()
self._device_configs[cmd] = cfg
return cfg
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
diff = {}
# prepare candidate configuration
candidate_obj = NetworkConfig(indent=2)
candidate_obj.load(candidate)
if running and diff_match != 'none' and diff_replace != 'config':
# running configuration
running_obj = NetworkConfig(indent=2, contents=running, ignore_lines=diff_ignore_lines)
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
else:
configdiffobjs = candidate_obj.items
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
return diff
def load_config(self, commands, return_error=False, opts=None, replace=None):
"""Sends the ordered set of commands to the device
"""
if opts is None:
opts = {}
responses = []
try:
resp = self.edit_config(commands, replace=replace)
except ConnectionError as exc:
code = getattr(exc, 'code', 1)
message = getattr(exc, 'err', exc)
err = to_text(message, errors='surrogate_then_replace')
if opts.get('ignore_timeout') and code:
responses.append(code)
return responses
elif code and 'no graceful-restart' in err:
if 'ISSU/HA will be affected if Graceful Restart is disabled' in err:
msg = ['']
responses.extend(msg)
return responses
else:
self._module.fail_json(msg=err)
elif code:
self._module.fail_json(msg=err)
responses.extend(resp)
return responses
def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
resp = list()
self.check_edit_config_capability(candidate, commit, replace, comment)
if replace:
candidate = 'config replace {0}'.format(replace)
responses = self._connection.send_request(candidate, output='config')
for response in to_list(responses):
if response != '{}':
resp.append(response)
if not resp:
resp = ['']
return resp
def get_capabilities(self):
"""Returns platform info of the remove device
"""
try:
capabilities = self._connection.get_capabilities()
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
return json.loads(capabilities)
def check_edit_config_capability(self, candidate=None, commit=True, replace=None, comment=None):
operations = self._connection.get_device_operations()
if not candidate and not replace:
raise ValueError("must provide a candidate or replace to load configuration")
if commit not in (True, False):
raise ValueError("'commit' must be a bool, got %s" % commit)
if replace and not operations.get('supports_replace'):
raise ValueError("configuration replace is not supported")
if comment and not operations.get('supports_commit_comment', False):
raise ValueError("commit comment is not supported")
def read_module_context(self, module_key):
if self._module_context.get(module_key):
return self._module_context[module_key]
return None
def save_module_context(self, module_key, module_context):
self._module_context[module_key] = module_context
return None
def is_json(cmd): def is_json(cmd):
return str(cmd).endswith('| json') return to_text(cmd).endswith('| json')
def is_text(cmd): def is_text(cmd):
return not is_json(cmd) return not is_json(cmd)
def is_nxapi(module): def is_local_nxapi(module):
transport = module.params['transport'] transport = module.params['transport']
provider_transport = (module.params['provider'] or {}).get('transport') provider_transport = (module.params['provider'] or {}).get('transport')
return 'nxapi' in (transport, provider_transport) return 'nxapi' in (transport, provider_transport)
def to_command(module, commands): def to_command(module, commands):
if is_nxapi(module): if is_local_nxapi(module):
default_output = 'json' default_output = 'json'
else: else:
default_output = 'text' default_output = 'text'

@ -50,8 +50,6 @@ from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.network.common.utils import to_list from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.network.common.config import NetworkConfig, dumps from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.plugins.cliconf import CliconfBase, enable_mode from ansible.plugins.cliconf import CliconfBase, enable_mode
from ansible.plugins.connection.network_cli import Connection as NetworkCli
from ansible.plugins.connection.httpapi import Connection as HttpApi
class Cliconf(CliconfBase): class Cliconf(CliconfBase):
@ -60,20 +58,6 @@ class Cliconf(CliconfBase):
super(Cliconf, self).__init__(*args, **kwargs) super(Cliconf, self).__init__(*args, **kwargs)
self._session_support = None self._session_support = None
def send_command(self, command, **kwargs):
"""Executes a cli command and returns the results
This method will execute the CLI command on the connection and return
the results to the caller. The command output will be returned as a
string
"""
if isinstance(self._connection, NetworkCli):
resp = super(Cliconf, self).send_command(command, **kwargs)
elif isinstance(self._connection, HttpApi):
resp = self._connection.send_request(command, **kwargs)
else:
raise ValueError("Invalid connection type")
return resp
@enable_mode @enable_mode
def get_config(self, source='running', format='text', flags=None): def get_config(self, source='running', format='text', flags=None):
options_values = self.get_option_values() options_values = self.get_option_values()
@ -294,13 +278,8 @@ class Cliconf(CliconfBase):
result['device_info'] = self.get_device_info() result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations() result['device_operations'] = self.get_device_operations()
result.update(self.get_option_values()) result.update(self.get_option_values())
result['network_api'] = 'cliconf'
if isinstance(self._connection, NetworkCli):
result['network_api'] = 'cliconf'
elif isinstance(self._connection, HttpApi):
result['network_api'] = 'eapi'
else:
raise ValueError("Invalid connection type")
return json.dumps(result) return json.dumps(result)
def _get_command_with_output(self, command, output): def _get_command_with_output(self, command, output):

@ -29,8 +29,6 @@ from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.network.common.config import NetworkConfig, dumps from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.module_utils.network.common.utils import to_list from ansible.module_utils.network.common.utils import to_list
from ansible.plugins.cliconf import CliconfBase, enable_mode from ansible.plugins.cliconf import CliconfBase, enable_mode
from ansible.plugins.connection.network_cli import Connection as NetworkCli
from ansible.plugins.connection.httpapi import Connection as HttpApi
class Cliconf(CliconfBase): class Cliconf(CliconfBase):
@ -50,20 +48,6 @@ class Cliconf(CliconfBase):
return None return None
def send_command(self, command, **kwargs):
"""Executes a cli command and returns the results
This method will execute the CLI command on the connection and return
the results to the caller. The command output will be returned as a
string
"""
if isinstance(self._connection, NetworkCli):
resp = super(Cliconf, self).send_command(command, **kwargs)
elif isinstance(self._connection, HttpApi):
resp = self._connection.send_request(command, **kwargs)
else:
raise ValueError("Invalid connection type")
return resp
def get_device_info(self): def get_device_info(self):
device_info = {} device_info = {}
@ -261,13 +245,8 @@ class Cliconf(CliconfBase):
result['device_info'] = self.get_device_info() result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations() result['device_operations'] = self.get_device_operations()
result.update(self.get_option_values()) result.update(self.get_option_values())
result['network_api'] = 'cliconf'
if isinstance(self._connection, NetworkCli):
result['network_api'] = 'cliconf'
elif isinstance(self._connection, HttpApi):
result['network_api'] = 'nxapi'
else:
raise ValueError("Invalid connection type")
return json.dumps(result) return json.dumps(result)
def _get_command_with_output(self, command, output): def _get_command_with_output(self, command, output):

@ -297,7 +297,7 @@ class NetworkConnectionBase(ConnectionBase):
self._local = connection_loader.get('local', play_context, '/dev/null') self._local = connection_loader.get('local', play_context, '/dev/null')
self._local.set_options() self._local.set_options()
self._sub_plugins = [] self._sub_plugin = {}
self._cached_variables = (None, None, None) self._cached_variables = (None, None, None)
# reconstruct the socket_path and set instance values accordingly # reconstruct the socket_path and set instance values accordingly
@ -309,8 +309,9 @@ class NetworkConnectionBase(ConnectionBase):
return self.__dict__[name] return self.__dict__[name]
except KeyError: except KeyError:
if not name.startswith('_'): if not name.startswith('_'):
for plugin in self._sub_plugins: plugin = self._sub_plugin.get('obj')
method = getattr(plugin['obj'], name, None) if plugin:
method = getattr(plugin, name, None)
if method is not None: if method is not None:
return method return method
raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name)) raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))
@ -342,12 +343,11 @@ class NetworkConnectionBase(ConnectionBase):
def set_options(self, task_keys=None, var_options=None, direct=None): def set_options(self, task_keys=None, var_options=None, direct=None):
super(NetworkConnectionBase, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) super(NetworkConnectionBase, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
for plugin in self._sub_plugins: if self._sub_plugin.get('obj') and self._sub_plugin.get('type') != 'external':
if plugin['type'] != 'external': try:
try: self._sub_plugin['obj'].set_options(task_keys=task_keys, var_options=var_options, direct=direct)
plugin['obj'].set_options(task_keys=task_keys, var_options=var_options, direct=direct) except AttributeError:
except AttributeError: pass
pass
def _update_connection_state(self): def _update_connection_state(self):
''' '''

@ -37,8 +37,8 @@ options:
network_os: network_os:
description: description:
- Configures the device platform network operating system. This value is - Configures the device platform network operating system. This value is
used to load the correct httpapi and cliconf plugins to communicate used to load the correct httpapi plugin to communicate with the remote
with the remote device device
vars: vars:
- name: ansible_network_os - name: ansible_network_os
remote_user: remote_user:
@ -154,7 +154,7 @@ from ansible.module_utils.six.moves import cPickle
from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError
from ansible.module_utils.urls import open_url from ansible.module_utils.urls import open_url
from ansible.playbook.play_context import PlayContext from ansible.playbook.play_context import PlayContext
from ansible.plugins.loader import cliconf_loader, httpapi_loader from ansible.plugins.loader import httpapi_loader
from ansible.plugins.connection import NetworkConnectionBase from ansible.plugins.connection import NetworkConnectionBase
from ansible.utils.display import Display from ansible.utils.display import Display
@ -177,17 +177,11 @@ class Connection(NetworkConnectionBase):
self.httpapi = httpapi_loader.get(self._network_os, self) self.httpapi = httpapi_loader.get(self._network_os, self)
if self.httpapi: if self.httpapi:
self._sub_plugins.append({'type': 'httpapi', 'name': self._network_os, 'obj': self.httpapi}) self._sub_plugin = {'type': 'httpapi', 'name': self._network_os, 'obj': self.httpapi}
display.vvvv('loaded API plugin for network_os %s' % self._network_os) display.vvvv('loaded API plugin for network_os %s' % self._network_os)
else: else:
raise AnsibleConnectionFailure('unable to load API plugin for network_os %s' % self._network_os) raise AnsibleConnectionFailure('unable to load API plugin for network_os %s' % self._network_os)
self.cliconf = cliconf_loader.get(self._network_os, self)
if self.cliconf:
self._sub_plugins.append({'type': 'cliconf', 'name': self._network_os, 'obj': self.cliconf})
display.vvvv('loaded cliconf plugin for network_os %s' % self._network_os)
else:
display.vvvv('unable to load cliconf for network_os %s' % self._network_os)
else: else:
raise AnsibleConnectionFailure( raise AnsibleConnectionFailure(
'Unable to automatically determine host network os. Please ' 'Unable to automatically determine host network os. Please '

@ -183,7 +183,7 @@ class Connection(NetworkConnectionBase):
self.napalm.open() self.napalm.open()
self._sub_plugins.append({'type': 'external', 'name': 'napalm', 'obj': self.napalm}) self._sub_plugin = {'type': 'external', 'name': 'napalm', 'obj': self.napalm}
display.vvvv('created napalm device for network_os %s' % self._network_os, host=host) display.vvvv('created napalm device for network_os %s' % self._network_os, host=host)
self._connected = True self._connected = True

@ -217,11 +217,11 @@ class Connection(NetworkConnectionBase):
netconf = netconf_loader.get(self._network_os, self) netconf = netconf_loader.get(self._network_os, self)
if netconf: if netconf:
self._sub_plugins.append({'type': 'netconf', 'name': self._network_os, 'obj': netconf}) self._sub_plugin = {'type': 'netconf', 'name': self._network_os, 'obj': netconf}
display.display('loaded netconf plugin for network_os %s' % self._network_os, log_only=True) display.display('loaded netconf plugin for network_os %s' % self._network_os, log_only=True)
else: else:
netconf = netconf_loader.get("default", self) netconf = netconf_loader.get("default", self)
self._sub_plugins.append({'type': 'netconf', 'name': 'default', 'obj': netconf}) self._sub_plugin = {'type': 'netconf', 'name': 'default', 'obj': netconf}
display.display('unable to load netconf plugin for network_os %s, falling back to default plugin' % self._network_os) display.display('unable to load netconf plugin for network_os %s, falling back to default plugin' % self._network_os)
display.display('network_os is set to %s' % self._network_os, log_only=True) display.display('network_os is set to %s' % self._network_os, log_only=True)

@ -231,7 +231,7 @@ class Connection(NetworkConnectionBase):
self.cliconf = cliconf_loader.get(self._network_os, self) self.cliconf = cliconf_loader.get(self._network_os, self)
if self.cliconf: if self.cliconf:
display.vvvv('loaded cliconf plugin for network_os %s' % self._network_os) display.vvvv('loaded cliconf plugin for network_os %s' % self._network_os)
self._sub_plugins.append({'type': 'cliconf', 'name': self._network_os, 'obj': self.cliconf}) self._sub_plugin = {'type': 'cliconf', 'name': self._network_os, 'obj': self.cliconf}
else: else:
display.vvvv('unable to load cliconf for network_os %s' % self._network_os) display.vvvv('unable to load cliconf for network_os %s' % self._network_os)
else: else:

@ -4,8 +4,29 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = """
---
author: Ansible Networking Team
httpapi: eos
short_description: Use eAPI to run command on eos platform
description:
- This eos plugin provides low level abstraction api's for
sending and receiving CLI commands with eos network devices.
version_added: "2.6"
options:
eos_use_sessions:
type: int
default: 1
description:
- Specifies if sessions should be used on remote host or not
env:
- name: ANSIBLE_EOS_USE_SESSIONS
vars:
- name: ansible_eos_use_sessions
version_added: '2.8'
"""
import json import json
import time
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.module_utils.connection import ConnectionError from ansible.module_utils.connection import ConnectionError
@ -16,7 +37,39 @@ from ansible.utils.display import Display
display = Display() display = Display()
OPTIONS = {
'format': ['text', 'json'],
'diff_match': ['line', 'strict', 'exact', 'none'],
'diff_replace': ['line', 'block', 'config'],
'output': ['text', 'json']
}
class HttpApi(HttpApiBase): class HttpApi(HttpApiBase):
def __init__(self, *args, **kwargs):
super(HttpApi, self).__init__(*args, **kwargs)
self._device_info = None
self._session_support = None
@property
def supports_sessions(self):
use_session = self.get_option('eos_use_sessions')
try:
use_session = int(use_session)
except ValueError:
pass
if not bool(use_session):
self._session_support = False
else:
if self._session_support:
return self._session_support
response = self.send_request('show configuration sessions')
self._session_support = 'error' not in response
return self._session_support
def send_request(self, data, **message_kwargs): def send_request(self, data, **message_kwargs):
data = to_list(data) data = to_list(data)
if self._become: if self._become:
@ -45,117 +98,51 @@ class HttpApi(HttpApiBase):
return results return results
def get_prompt(self): def get_device_info(self):
# Fake a prompt for @enable_mode if self._device_info:
if self._become: return self._device_info
return '#'
return '>'
# Imported from module_utils
def edit_config(self, config, commit=False, replace=False):
"""Loads the configuration onto the remote devices
If the device doesn't support configuration sessions, this will
fallback to using configure() to load the commands. If that happens,
there will be no returned diff or session values
"""
session = 'ansible_%s' % int(time.time())
result = {'session': session}
banner_cmd = None
banner_input = []
commands = ['configure session %s' % session]
if replace:
commands.append('rollback clean-config')
for command in config:
if command.startswith('banner'):
banner_cmd = command
banner_input = []
elif banner_cmd:
if command == 'EOF':
command = {'cmd': banner_cmd, 'input': '\n'.join(banner_input)}
banner_cmd = None
commands.append(command)
else:
banner_input.append(command)
continue
else:
commands.append(command)
try: device_info = {}
response = self.send_request(commands)
except Exception: device_info['network_os'] = 'eos'
commands = ['configure session %s' % session, 'abort'] reply = self.send_request('show version | json')
response = self.send_request(commands, output='text') data = json.loads(reply)
raise
device_info['network_os_version'] = data['version']
commands = ['configure session %s' % session, 'show session-config diffs'] device_info['network_os_model'] = data['modelName']
if commit:
commands.append('commit') reply = self.send_request('show hostname | json')
else: data = json.loads(reply)
commands.append('abort')
device_info['network_os_hostname'] = data['hostname']
response = self.send_request(commands, output='text')
diff = response[1].strip() self._device_info = device_info
if diff: return self._device_info
result['diff'] = diff
def get_device_operations(self):
return result return {
'supports_diff_replace': True,
def run_commands(self, commands, check_rc=True): 'supports_commit': bool(self.supports_sessions),
"""Runs list of commands on remote device and returns results 'supports_rollback': False,
""" 'supports_defaults': False,
output = None 'supports_onbox_diff': bool(self.supports_sessions),
queue = list() 'supports_commit_comment': False,
responses = list() 'supports_multiline_delimiter': False,
'supports_diff_match': True,
def run_queue(queue, output): 'supports_diff_ignore_lines': True,
try: 'supports_generate_diff': not bool(self.supports_sessions),
response = to_list(self.send_request(queue, output=output)) 'supports_replace': bool(self.supports_sessions),
except Exception as exc: }
if check_rc:
raise def get_capabilities(self):
return to_text(exc) result = {}
result['rpc'] = []
if output == 'json': result['device_info'] = self.get_device_info()
response = [json.loads(item) for item in response] result['device_operations'] = self.get_device_operations()
return response result.update(OPTIONS)
result['network_api'] = 'eapi'
for item in to_list(commands):
cmd_output = 'text' return json.dumps(result)
if isinstance(item, dict):
command = item['command']
if 'output' in item:
cmd_output = item['output']
else:
command = item
# Emulate '| json' from CLI
if command.endswith('| json'):
command = command.rsplit('|', 1)[0]
cmd_output = 'json'
if output and output != cmd_output:
responses.extend(run_queue(queue, output))
queue = list()
output = cmd_output
queue.append(command)
if queue:
responses.extend(run_queue(queue, output))
return responses
def load_config(self, config, commit=False, replace=False):
"""Loads the configuration onto the remote devices
If the device doesn't support configuration sessions, this will
fallback to using configure() to load the commands. If that happens,
there will be no returned diff or session values
"""
return self.edit_config(config, commit, replace)
def handle_response(response): def handle_response(response):
@ -170,6 +157,7 @@ def handle_response(response):
raise ConnectionError(error_text, code=error['code']) raise ConnectionError(error_text, code=error['code'])
results = [] results = []
for result in response['result']: for result in response['result']:
if 'messages' in result: if 'messages' in result:
results.append(result['messages'][0]) results.append(result['messages'][0])

@ -4,7 +4,19 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = """
---
author: Ansible Networking Team
httpapi: nxos
short_description: Use NX-API to run command on nxos platform
description:
- This eos plugin provides low level abstraction api's for
sending and receiving CLI commands with nxos network devices.
version_added: "2.6"
"""
import json import json
import re
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.module_utils.connection import ConnectionError from ansible.module_utils.connection import ConnectionError
@ -15,29 +27,18 @@ from ansible.utils.display import Display
display = Display() display = Display()
class HttpApi(HttpApiBase): OPTIONS = {
def _run_queue(self, queue, output): 'format': ['text', 'json'],
if self._become: 'diff_match': ['line', 'strict', 'exact', 'none'],
display.vvvv('firing event: on_become') 'diff_replace': ['line', 'block', 'config'],
queue.insert(0, 'enable') 'output': ['text', 'json']
}
request = request_builder(queue, output)
headers = {'Content-Type': 'application/json'}
response, response_data = self.connection.send('/ins', request, headers=headers, method='POST') class HttpApi(HttpApiBase):
def __init__(self, *args, **kwargs):
try: super(HttpApi, self).__init__(*args, **kwargs)
response_data = json.loads(to_text(response_data.getvalue())) self._device_info = None
except ValueError:
raise ConnectionError('Response was not valid JSON, got {0}'.format(
to_text(response_data.getvalue())
))
results = handle_response(response_data)
if self._become:
results = results[1:]
return results
def send_request(self, data, **message_kwargs): def send_request(self, data, **message_kwargs):
output = None output = None
@ -72,46 +73,93 @@ class HttpApi(HttpApiBase):
return responses[0] return responses[0]
return responses return responses
def edit_config(self, candidate=None, commit=True, replace=None, comment=None): def _run_queue(self, queue, output):
resp = list() if self._become:
display.vvvv('firing event: on_become')
operations = self.connection.get_device_operations() queue.insert(0, 'enable')
self.connection.check_edit_config_capability(operations, candidate, commit, replace, comment)
if replace:
device_info = self.connection.get_device_info()
if '9K' not in device_info.get('network_os_platform', ''):
raise ConnectionError(msg=u'replace is supported only on Nexus 9K devices')
candidate = 'config replace {0}'.format(replace)
responses = self.send_request(candidate, output='config') request = request_builder(queue, output)
for response in to_list(responses): headers = {'Content-Type': 'application/json'}
if response != '{}':
resp.append(response)
if not resp:
resp = ['']
return resp response, response_data = self.connection.send('/ins', request, headers=headers, method='POST')
def run_commands(self, commands, check_rc=True):
"""Runs list of commands on remote device and returns results
"""
try: try:
out = self.send_request(commands) response_data = json.loads(to_text(response_data.getvalue()))
except ConnectionError as exc: except ValueError:
if check_rc is True: raise ConnectionError('Response was not valid JSON, got {0}'.format(
raise to_text(response_data.getvalue())
out = to_text(exc) ))
out = to_list(out) results = handle_response(response_data)
if not out[0]:
return out
for index, response in enumerate(out): if self._become:
if response[0] == '{': results = results[1:]
out[index] = json.loads(response) return results
return out def get_device_info(self):
if self._device_info:
return self._device_info
device_info = {}
device_info['network_os'] = 'nxos'
reply = self.send_request('show version')
platform_reply = self.send_request('show inventory')
find_os_version = [r'\s+system:\s+version\s*(\S+)', r'\s+kickstart:\s+version\s*(\S+)', r'\s+NXOS:\s+version\s*(\S+)']
for regex in find_os_version:
match_ver = re.search(regex, reply, re.M)
if match_ver:
device_info['network_os_version'] = match_ver.group(1)
break
match_chassis_id = re.search(r'Hardware\n\s+cisco\s*(\S+\s+\S+)', reply, re.M)
if match_chassis_id:
device_info['network_os_model'] = match_chassis_id.group(1)
match_host_name = re.search(r'\s+Device name:\s*(\S+)', reply, re.M)
if match_host_name:
device_info['network_os_hostname'] = match_host_name.group(1)
find_os_image = [r'\s+system image file is:\s*(\S+)', r'\s+kickstart image file is:\s*(\S+)', r'\s+NXOS image file is:\s*(\S+)']
for regex in find_os_image:
match_file_name = re.search(regex, reply, re.M)
if match_file_name:
device_info['network_os_image'] = match_file_name.group(1)
break
match_os_platform = re.search(r'NAME: "Chassis",\s*DESCR:.*\nPID:\s*(\S+)', platform_reply, re.M)
if match_os_platform:
device_info['network_os_platform'] = match_os_platform.group(1)
self._device_info = device_info
return self._device_info
def get_device_operations(self):
platform = self.get_device_info().get('network_os_platform', '')
return {
'supports_diff_replace': True,
'supports_commit': False,
'supports_rollback': False,
'supports_defaults': True,
'supports_onbox_diff': False,
'supports_commit_comment': False,
'supports_multiline_delimiter': False,
'supports_diff_match': True,
'supports_diff_ignore_lines': True,
'supports_generate_diff': True,
'supports_replace': True if '9K' in platform else False,
}
def get_capabilities(self):
result = {}
result['rpc'] = []
result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations()
result.update(OPTIONS)
result['network_api'] = 'nxapi'
return json.dumps(result)
def handle_response(response): def handle_response(response):

@ -27,17 +27,9 @@ class JsonRpcServer(object):
error = self.invalid_request() error = self.invalid_request()
return json.dumps(error) return json.dumps(error)
params = request.get('params') args, kwargs = request.get('params')
setattr(self, '_identifier', request.get('id')) setattr(self, '_identifier', request.get('id'))
args = []
kwargs = {}
if all((params, isinstance(params, list))):
args = params
elif all((params, isinstance(params, dict))):
kwargs = params
rpc_method = None rpc_method = None
for obj in self._objects: for obj in self._objects:
rpc_method = getattr(obj, method, None) rpc_method = getattr(obj, method, None)

Loading…
Cancel
Save