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,7 +900,7 @@ class TaskExecutor:
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)
for plugin in connection._sub_plugins:
plugin = connection._sub_plugin
if plugin['type'] != 'external':
option_vars.extend(C.config.get_plugin_vars(plugin['type'], plugin['name']))

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

@ -27,6 +27,7 @@
# 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.
#
import json
import os
import time
@ -99,10 +100,15 @@ def get_connection(module):
global _DEVICE_CONNECTION
if not _DEVICE_CONNECTION:
load_params(module)
if is_eapi(module):
conn = Eapi(module)
if is_local_eapi(module):
conn = LocalEapi(module)
else:
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
return _DEVICE_CONNECTION
@ -180,7 +186,7 @@ class Cli:
return diff
class Eapi:
class LocalEapi:
def __init__(self, module):
self._module = module
@ -394,18 +400,187 @@ class Eapi:
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):
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']
provider_transport = (module.params['provider'] or {}).get('transport')
return 'eapi' in (transport, provider_transport)
def to_command(module, commands):
if is_eapi(module):
if is_local_eapi(module):
default_output = 'json'
else:
default_output = 'text'

@ -105,10 +105,15 @@ def get_connection(module):
global _DEVICE_CONNECTION
if not _DEVICE_CONNECTION:
load_params(module)
if is_nxapi(module):
conn = Nxapi(module)
if is_local_nxapi(module):
conn = LocalNxapi(module)
else:
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
return _DEVICE_CONNECTION
@ -244,7 +249,7 @@ class Cli:
return None
class Nxapi:
class LocalNxapi:
OUTPUT_TO_COMMAND_TYPE = {
'text': 'cli_show_ascii',
@ -496,22 +501,178 @@ class Nxapi:
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):
return str(cmd).endswith('| json')
return to_text(cmd).endswith('| json')
def is_text(cmd):
return not is_json(cmd)
def is_nxapi(module):
def is_local_nxapi(module):
transport = module.params['transport']
provider_transport = (module.params['provider'] or {}).get('transport')
return 'nxapi' in (transport, provider_transport)
def to_command(module, commands):
if is_nxapi(module):
if is_local_nxapi(module):
default_output = 'json'
else:
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.config import NetworkConfig, dumps
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):
@ -60,20 +58,6 @@ class Cliconf(CliconfBase):
super(Cliconf, self).__init__(*args, **kwargs)
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
def get_config(self, source='running', format='text', flags=None):
options_values = self.get_option_values()
@ -294,13 +278,8 @@ class Cliconf(CliconfBase):
result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations()
result.update(self.get_option_values())
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)
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.utils import to_list
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):
@ -50,20 +48,6 @@ class Cliconf(CliconfBase):
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):
device_info = {}
@ -261,13 +245,8 @@ class Cliconf(CliconfBase):
result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations()
result.update(self.get_option_values())
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)
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.set_options()
self._sub_plugins = []
self._sub_plugin = {}
self._cached_variables = (None, None, None)
# reconstruct the socket_path and set instance values accordingly
@ -309,8 +309,9 @@ class NetworkConnectionBase(ConnectionBase):
return self.__dict__[name]
except KeyError:
if not name.startswith('_'):
for plugin in self._sub_plugins:
method = getattr(plugin['obj'], name, None)
plugin = self._sub_plugin.get('obj')
if plugin:
method = getattr(plugin, name, None)
if method is not None:
return method
raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))
@ -342,10 +343,9 @@ class NetworkConnectionBase(ConnectionBase):
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)
for plugin in self._sub_plugins:
if plugin['type'] != 'external':
if self._sub_plugin.get('obj') and self._sub_plugin.get('type') != 'external':
try:
plugin['obj'].set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self._sub_plugin['obj'].set_options(task_keys=task_keys, var_options=var_options, direct=direct)
except AttributeError:
pass

@ -37,8 +37,8 @@ options:
network_os:
description:
- Configures the device platform network operating system. This value is
used to load the correct httpapi and cliconf plugins to communicate
with the remote device
used to load the correct httpapi plugin to communicate with the remote
device
vars:
- name: ansible_network_os
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.urls import open_url
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.utils.display import Display
@ -177,17 +177,11 @@ class Connection(NetworkConnectionBase):
self.httpapi = httpapi_loader.get(self._network_os, self)
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)
else:
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:
raise AnsibleConnectionFailure(
'Unable to automatically determine host network os. Please '

@ -183,7 +183,7 @@ class Connection(NetworkConnectionBase):
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)
self._connected = True

@ -217,11 +217,11 @@ class Connection(NetworkConnectionBase):
netconf = netconf_loader.get(self._network_os, self)
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)
else:
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('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)
if self.cliconf:
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:
display.vvvv('unable to load cliconf for network_os %s' % self._network_os)
else:

@ -4,8 +4,29 @@
from __future__ import (absolute_import, division, print_function)
__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 time
from ansible.module_utils._text import to_text
from ansible.module_utils.connection import ConnectionError
@ -16,7 +37,39 @@ from ansible.utils.display import Display
display = Display()
OPTIONS = {
'format': ['text', 'json'],
'diff_match': ['line', 'strict', 'exact', 'none'],
'diff_replace': ['line', 'block', 'config'],
'output': ['text', 'json']
}
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):
data = to_list(data)
if self._become:
@ -45,117 +98,51 @@ class HttpApi(HttpApiBase):
return results
def get_prompt(self):
# Fake a prompt for @enable_mode
if self._become:
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:
response = self.send_request(commands)
except Exception:
commands = ['configure session %s' % session, 'abort']
response = self.send_request(commands, output='text')
raise
commands = ['configure session %s' % session, 'show session-config diffs']
if commit:
commands.append('commit')
else:
commands.append('abort')
def get_device_info(self):
if self._device_info:
return self._device_info
response = self.send_request(commands, output='text')
diff = response[1].strip()
if diff:
result['diff'] = diff
device_info = {}
return result
device_info['network_os'] = 'eos'
reply = self.send_request('show version | json')
data = json.loads(reply)
def run_commands(self, commands, check_rc=True):
"""Runs list of commands on remote device and returns results
"""
output = None
queue = list()
responses = list()
device_info['network_os_version'] = data['version']
device_info['network_os_model'] = data['modelName']
def run_queue(queue, output):
try:
response = to_list(self.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 command.endswith('| json'):
command = command.rsplit('|', 1)[0]
cmd_output = 'json'
reply = self.send_request('show hostname | json')
data = json.loads(reply)
if output and output != cmd_output:
responses.extend(run_queue(queue, output))
queue = list()
device_info['network_os_hostname'] = data['hostname']
output = cmd_output
queue.append(command)
self._device_info = device_info
return self._device_info
if queue:
responses.extend(run_queue(queue, output))
def get_device_operations(self):
return {
'supports_diff_replace': True,
'supports_commit': bool(self.supports_sessions),
'supports_rollback': False,
'supports_defaults': False,
'supports_onbox_diff': bool(self.supports_sessions),
'supports_commit_comment': False,
'supports_multiline_delimiter': False,
'supports_diff_match': True,
'supports_diff_ignore_lines': True,
'supports_generate_diff': not bool(self.supports_sessions),
'supports_replace': bool(self.supports_sessions),
}
return responses
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'] = 'eapi'
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)
return json.dumps(result)
def handle_response(response):
@ -170,6 +157,7 @@ def handle_response(response):
raise ConnectionError(error_text, code=error['code'])
results = []
for result in response['result']:
if 'messages' in result:
results.append(result['messages'][0])

@ -4,7 +4,19 @@
from __future__ import (absolute_import, division, print_function)
__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 re
from ansible.module_utils._text import to_text
from ansible.module_utils.connection import ConnectionError
@ -15,29 +27,18 @@ from ansible.utils.display import Display
display = Display()
class HttpApi(HttpApiBase):
def _run_queue(self, queue, output):
if self._become:
display.vvvv('firing event: on_become')
queue.insert(0, 'enable')
request = request_builder(queue, output)
headers = {'Content-Type': 'application/json'}
response, response_data = self.connection.send('/ins', request, headers=headers, method='POST')
try:
response_data = json.loads(to_text(response_data.getvalue()))
except ValueError:
raise ConnectionError('Response was not valid JSON, got {0}'.format(
to_text(response_data.getvalue())
))
OPTIONS = {
'format': ['text', 'json'],
'diff_match': ['line', 'strict', 'exact', 'none'],
'diff_replace': ['line', 'block', 'config'],
'output': ['text', 'json']
}
results = handle_response(response_data)
if self._become:
results = results[1:]
return results
class HttpApi(HttpApiBase):
def __init__(self, *args, **kwargs):
super(HttpApi, self).__init__(*args, **kwargs)
self._device_info = None
def send_request(self, data, **message_kwargs):
output = None
@ -72,46 +73,93 @@ class HttpApi(HttpApiBase):
return responses[0]
return responses
def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
resp = list()
def _run_queue(self, queue, output):
if self._become:
display.vvvv('firing event: on_become')
queue.insert(0, 'enable')
operations = self.connection.get_device_operations()
self.connection.check_edit_config_capability(operations, candidate, commit, replace, comment)
request = request_builder(queue, output)
headers = {'Content-Type': 'application/json'}
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)
response, response_data = self.connection.send('/ins', request, headers=headers, method='POST')
responses = self.send_request(candidate, output='config')
for response in to_list(responses):
if response != '{}':
resp.append(response)
if not resp:
resp = ['']
try:
response_data = json.loads(to_text(response_data.getvalue()))
except ValueError:
raise ConnectionError('Response was not valid JSON, got {0}'.format(
to_text(response_data.getvalue())
))
return resp
results = handle_response(response_data)
def run_commands(self, commands, check_rc=True):
"""Runs list of commands on remote device and returns results
"""
try:
out = self.send_request(commands)
except ConnectionError as exc:
if check_rc is True:
raise
out = to_text(exc)
if self._become:
results = results[1:]
return results
out = to_list(out)
if not out[0]:
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,
}
for index, response in enumerate(out):
if response[0] == '{':
out[index] = json.loads(response)
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 out
return json.dumps(result)
def handle_response(response):

@ -27,17 +27,9 @@ class JsonRpcServer(object):
error = self.invalid_request()
return json.dumps(error)
params = request.get('params')
args, kwargs = request.get('params')
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
for obj in self._objects:
rpc_method = getattr(obj, method, None)

Loading…
Cancel
Save