1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

Exos httpapi (#54405)

* Modify EXOS module utils to utilize 'httpapi' or 'network-cli' connection

* Changes to cliconf plugin to support 'json' or 'text' output for compatibility between network-cli and httpapi

* Add HTTPAPI plugin supportng JSONRPC and RESTCONF for EXOS

* exos_facts modify commands with run script cli2json.py to command dictionary specifying 'json' output
Load appropriate fixtures

* Update exos_config module to utilize the get_diff and get_default_flag functionality.
JSONRPC doesn't work well with pipes, regex MULTILINE

* Support for NOS agnostic 'cli_config' module by implementing 'get_default_flag' and 'get_diff' functionality

* Update Ansible Documentation regarding the connections available for EXOS
This commit is contained in:
Ujwal Komarla 2019-03-27 09:44:04 -04:00 committed by Ricardo Carrillo Cruz
parent b002f3e416
commit 9384773e1e
14 changed files with 735 additions and 200 deletions

View file

@ -4,37 +4,37 @@
EXOS Platform Options EXOS Platform Options
*************************************** ***************************************
Extreme EXOS Ansible modules only support CLI connections today. This page offers details on how to Extreme EXOS Ansible modules support multiple connections. This page offers details on how each connection works in Ansible and how to use it.
use ``network_cli`` on EXOS in Ansible.
.. contents:: Topics .. contents:: Topics
Connections Available Connections Available
================================================================================ ================================================================================
+---------------------------+-----------------------------------------------+ +---------------------------+-----------------------------------------------+-----------------------------------------+
|.. | CLI | |.. | CLI | EXOS-API |
+===========================+===============================================+ +===========================+===============================================+=========================================+
| **Protocol** | SSH | | **Protocol** | SSH | HTTP(S) |
+---------------------------+-----------------------------------------------+ +---------------------------+-----------------------------------------------+-----------------------------------------+
| | **Credentials** | | uses SSH keys / SSH-agent if present | | | **Credentials** | | uses SSH keys / SSH-agent if present | | uses HTTPS certificates if present |
| | | | accepts ``-u myuser -k`` if using password | | | | | accepts ``-u myuser -k`` if using password | | |
+---------------------------+-----------------------------------------------+ +---------------------------+-----------------------------------------------+-----------------------------------------+
| **Indirect Access** | via a bastion (jump host) | | **Indirect Access** | via a bastion (jump host) | via a web proxy |
+---------------------------+-----------------------------------------------+ +---------------------------+-----------------------------------------------+-----------------------------------------+
| | **Connection Settings** | | ``ansible_connection: network_cli`` | | | **Connection Settings** | | ``ansible_connection: network_cli`` | | ``ansible_connection: httpapi`` |
| | | | | | | | | | | |
| | | | | | | | | | | |
| | | | | | | | | | | |
| | | | | | | | | | | |
+---------------------------+-----------------------------------------------+ +---------------------------+-----------------------------------------------+-----------------------------------------+
| | **Enable Mode** | | not supported by EXOS | | | **Enable Mode** | | not supported by EXOS | | not supported by EXOS |
| | (Privilege Escalation) | | | | | (Privilege Escalation) | | | | |
+---------------------------+-----------------------------------------------+ | | | | | | |
| **Returned Data Format** | ``stdout[0].`` | +---------------------------+-----------------------------------------------+-----------------------------------------+
+---------------------------+-----------------------------------------------+ | **Returned Data Format** | ``stdout[0].`` | ``stdout[0].messages[0].`` |
+---------------------------+-----------------------------------------------+-----------------------------------------+
EXOS does not support ``ansible_connection: local``. You must use ``ansible_connection: network_cli``. EXOS does not support ``ansible_connection: local``. You must use ``ansible_connection: network_cli`` or ``ansible_connection: httpapi``
Using CLI in Ansible Using CLI in Ansible
==================== ====================
@ -65,4 +65,37 @@ Example CLI Task
commands: show version commands: show version
when: ansible_network_os == 'exos' when: ansible_network_os == 'exos'
Using EXOS-API in Ansible
=========================
Example EXOS-API ``group_vars/exos.yml``
----------------------------------------
.. code-block:: yaml
ansible_connection: httpapi
ansible_network_os: exos
ansible_user: myuser
ansible_password: !vault...
proxy_env:
http_proxy: http://proxy.example.com:8080
- If you are accessing your host directly (not through a web proxy) you can remove the ``proxy_env`` configuration.
- If you are accessing your host through a web proxy using ``https``, change ``http_proxy`` to ``https_proxy``.
Example EXOS-API Task
---------------------
.. code-block:: yaml
- name: Retrieve EXOS OS version
exos_command:
commands: show version
when: ansible_network_os == 'exos'
In this example the ``proxy_env`` variable defined in ``group_vars`` gets passed to the ``environment`` option of the module used in the task.
.. include:: shared_snippets/SSH_warning.txt .. include:: shared_snippets/SSH_warning.txt

View file

@ -54,7 +54,7 @@ Settings by Platform
+-------------------+-------------------------+-------------+---------+---------+----------+ +-------------------+-------------------------+-------------+---------+---------+----------+
| Dell OS10 | ``dellos10`` | ✓ | | | ✓ | | Dell OS10 | ``dellos10`` | ✓ | | | ✓ |
+-------------------+-------------------------+-------------+---------+---------+----------+ +-------------------+-------------------------+-------------+---------+---------+----------+
| Extreme EXOS | ``exos`` | ✓ | | | | | Extreme EXOS | ``exos`` | ✓ | | | |
+-------------------+-------------------------+-------------+---------+---------+----------+ +-------------------+-------------------------+-------------+---------+---------+----------+
| Extreme IronWare | ``ironware`` | ✓ | | | ✓ | | Extreme IronWare | ``ironware`` | ✓ | | | ✓ |
+-------------------+-------------------------+-------------+---------+---------+----------+ +-------------------+-------------------------+-------------+---------+---------+----------+

View file

@ -28,74 +28,192 @@
import json import json
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.module_utils.basic import env_fallback from ansible.module_utils.basic import env_fallback
from ansible.module_utils.network.common.utils import to_list from ansible.module_utils.network.common.utils import to_list, ComplexList
from ansible.module_utils.connection import Connection from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.connection import Connection, ConnectionError
_DEVICE_CONFIGS = {} _DEVICE_CONNECTION = None
def get_connection(module): class Cli:
if hasattr(module, '_exos_connection'): def __init__(self, module):
return module._exos_connection self._module = module
self._device_configs = {}
self._connection = None
capabilities = get_capabilities(module) def get_capabilities(self):
network_api = capabilities.get('network_api') """Returns platform info of the remove device
"""
connection = self._get_connection()
return json.loads(connection.get_capabilities())
if network_api == 'cliconf': def _get_connection(self):
module._exos_connection = Connection(module._socket_path) if not self._connection:
else: self._connection = Connection(self._module._socket_path)
module.fail_json(msg='Invalid connection type %s' % network_api) return self._connection
return module._exos_connection def get_config(self, flags=None):
"""Retrieves the current config from the device or cache
"""
flags = [] if flags is None else flags
if self._device_configs == {}:
connection = self._get_connection()
try:
out = connection.get_config(flags=flags)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
self._device_configs = to_text(out, errors='surrogate_then_replace').strip()
return self._device_configs
def run_commands(self, commands, check_rc=True):
"""Runs list of commands on remote device and returns results
"""
connection = self._get_connection()
try:
response = connection.run_commands(commands=commands, check_rc=check_rc)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
return response
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
conn = self._get_connection()
try:
diff = conn.get_diff(candidate=candidate, running=running, diff_match=diff_match,
diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
return diff
class HttpApi:
def __init__(self, module):
self._module = module
self._device_configs = {}
self._connection_obj = None
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)
@property
def _connection(self):
if not self._connection_obj:
self._connection_obj = Connection(self._module._socket_path)
return self._connection_obj
def get_config(self, flags=None):
"""Retrieves the current config from the device or cache
"""
flags = [] if flags is None else flags
if self._device_configs == {}:
try:
out = self._connection.get_config(flags=flags)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
self._device_configs = to_text(out, errors='surrogate_then_replace').strip()
return self._device_configs
def run_commands(self, commands, check_rc=True):
"""Runs list of commands on remote device and returns results
"""
try:
response = self._connection.run_commands(commands=commands, check_rc=check_rc)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
return response
def send_requests(self, requests):
"""Send a list of http requests to remote device and return results
"""
if requests is None:
raise ValueError("'requests' value is required")
responses = list()
for req in to_list(requests):
try:
response = self._connection.send_request(**req)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
responses.append(response)
return responses
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
try:
diff = self._connection.get_diff(candidate=candidate, running=running, diff_match=diff_match,
diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)
except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
return diff
def get_capabilities(module): def get_capabilities(module):
if hasattr(module, '_exos_capabilities'): conn = get_connection(module)
return module._exos_capabilities return conn.get_capabilities()
capabilities = Connection(module._socket_path).get_capabilities()
module._exos_capabilities = json.loads(capabilities)
return module._exos_capabilities def get_connection(module):
global _DEVICE_CONNECTION
if not _DEVICE_CONNECTION:
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'] == 'exosapi':
conn = HttpApi(module)
else:
module.fail_json(msg='Invalid connection type %s' % cap['network_api'])
_DEVICE_CONNECTION = conn
return _DEVICE_CONNECTION
def get_config(module, flags=None): def get_config(module, flags=None):
global _DEVICE_CONFIGS flags = None if flags is None else flags
conn = get_connection(module)
if _DEVICE_CONFIGS != {}: return conn.get_config(flags)
return _DEVICE_CONFIGS
else:
connection = get_connection(module)
out = connection.get_config()
cfg = to_text(out, errors='surrogate_then_replace').strip()
_DEVICE_CONFIGS = cfg
return cfg
def run_commands(module, commands, check_rc=True):
responses = list()
connection = get_connection(module)
for cmd in to_list(commands):
if isinstance(cmd, dict):
command = cmd['command']
prompt = cmd['prompt']
answer = cmd['answer']
else:
command = cmd
prompt = None
answer = None
out = connection.get(command, prompt, answer)
try:
out = to_text(out, errors='surrogate_or_strict')
except UnicodeError:
module.fail_json(msg=u'Failed to decode output from %s: %s' % (cmd, to_text(out)))
responses.append(out)
return responses
def load_config(module, commands): def load_config(module, commands):
connection = get_connection(module) conn = get_connection(module)
out = connection.edit_config(commands) return conn.run_commands(to_command(module, commands))
def run_commands(module, commands, check_rc=True):
conn = get_connection(module)
return conn.run_commands(to_command(module, commands), check_rc=check_rc)
def to_command(module, commands):
transform = ComplexList(dict(
command=dict(key=True),
output=dict(default='text'),
prompt=dict(type='list'),
answer=dict(type='list'),
sendonly=dict(type='bool', default=False),
check_all=dict(type='bool', default=False),
), module)
return transform(to_list(commands))
def send_requests(module, requests):
conn = get_connection(module)
return conn.send_requests(to_request(module, requests))
def to_request(module, requests):
transform = ComplexList(dict(
path=dict(key=True),
method=dict(),
data=dict(),
), module)
return transform(to_list(requests))
def get_diff(module, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
conn = get_connection(module)
return conn.get_diff(candidate=candidate, running=running, diff_match=diff_match, diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)

View file

@ -126,6 +126,7 @@ options:
- When this option is configured as I(running), the module will - When this option is configured as I(running), the module will
return the before and after diff of the running-config with respect return the before and after diff of the running-config with respect
to any changes made to the device configuration. to any changes made to the device configuration.
default: running
choices: ['running', 'startup', 'intended'] choices: ['running', 'startup', 'intended']
diff_ignore_lines: diff_ignore_lines:
description: description:
@ -222,50 +223,48 @@ backup_path:
""" """
import re import re
from ansible.module_utils.network.exos.exos import run_commands, get_config, load_config from ansible.module_utils.network.exos.exos import run_commands, get_config, load_config, get_diff
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.common.config import NetworkConfig, dumps from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.module_utils.network.common.utils import to_list
__metaclass__ = type __metaclass__ = type
def get_running_config(module, current_config=None): def get_running_config(module, current_config=None, flags=None):
contents = module.params['running_config'] contents = module.params['running_config']
if not contents: if not contents:
if current_config: if current_config:
contents = current_config.config_text contents = current_config.config_text
else: else:
contents = get_config(module) contents = get_config(module, flags=flags)
return NetworkConfig(indent=1, contents=contents) return contents
def get_startup_config_text(module): def get_startup_config(module, flags=None):
reply = run_commands(module, ['show switch | include "Config Selected"']) reply = run_commands(module, {'command': 'show switch', 'output': 'text'})
match = re.search(r': +(\S+)\.cfg', to_text(reply, errors='surrogate_or_strict').strip()) match = re.search(r'Config Selected: +(\S+)\.cfg', to_text(reply, errors='surrogate_or_strict').strip(), re.MULTILINE)
if match: if match:
cfgname = match.group(1).strip() cfgname = match.group(1).strip()
reply = run_commands(module, ['debug cfgmgr show configuration file ' + cfgname]) command = ' '.join(['debug cfgmgr show configuration file', cfgname])
if flags:
command += ' '.join(to_list(flags)).strip()
reply = run_commands(module, {'command': command, 'output': 'text'})
data = reply[0] data = reply[0]
else: else:
data = '' data = ''
return data return data
def get_startup_config(module):
data = get_startup_config_text(module)
return NetworkConfig(indent=1, contents=data)
def get_candidate(module): def get_candidate(module):
candidate = NetworkConfig(indent=1) candidate = NetworkConfig(indent=1)
if module.params['src']: if module.params['src']:
candidate.load(module.params['src']) candidate.load(module.params['src'])
elif module.params['lines']: elif module.params['lines']:
candidate.add(module.params['lines']) candidate.add(module.params['lines'])
candidate = dumps(candidate, 'raw')
return candidate return candidate
@ -308,7 +307,7 @@ def main():
save_when=dict(choices=['always', 'never', 'modified', 'changed'], default='never'), save_when=dict(choices=['always', 'never', 'modified', 'changed'], default='never'),
diff_against=dict(choices=['startup', 'intended', 'running']), diff_against=dict(choices=['startup', 'intended', 'running'], default='running'),
diff_ignore_lines=dict(type='list'), diff_ignore_lines=dict(type='list'),
) )
@ -327,12 +326,15 @@ def main():
result = {'changed': False} result = {'changed': False}
warnings = list() warnings = list()
result['warnings'] = warnings if warnings:
result['warnings'] = warnings
config = None config = None
flags = ['detail'] if module.params['defaults'] else []
diff_ignore_lines = module.params['diff_ignore_lines']
if module.params['backup'] or (module._diff and module.params['diff_against'] == 'running'): if module.params['backup'] or (module._diff and module.params['diff_against'] == 'running'):
contents = get_config(module) contents = get_config(module, flags=flags)
config = NetworkConfig(indent=1, contents=contents) config = NetworkConfig(indent=1, contents=contents)
if module.params['backup']: if module.params['backup']:
result['__backup__'] = contents result['__backup__'] = contents
@ -342,15 +344,17 @@ def main():
replace = module.params['replace'] replace = module.params['replace']
candidate = get_candidate(module) candidate = get_candidate(module)
running = get_running_config(module, config)
if match != 'none': try:
config = get_running_config(module, config) response = get_diff(module, candidate=candidate, running=running, diff_match=match, diff_ignore_lines=diff_ignore_lines, diff_replace=replace)
configobjs = candidate.difference(config, match=match, replace=replace) except ConnectionError as exc:
else: module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
configobjs = candidate.items
if configobjs: config_diff = response.get('config_diff')
commands = dumps(configobjs, 'commands').split('\n')
if config_diff:
commands = config_diff.split('\n')
if module.params['before']: if module.params['before']:
commands[:0] = module.params['before'] commands[:0] = module.params['before']
@ -372,13 +376,11 @@ def main():
running_config = None running_config = None
startup_config = None startup_config = None
diff_ignore_lines = module.params['diff_ignore_lines']
if module.params['save_when'] == 'always': if module.params['save_when'] == 'always':
save_config(module, result) save_config(module, result)
elif module.params['save_when'] == 'modified': elif module.params['save_when'] == 'modified':
running = get_running_config(module).config_text running = get_running_config(module)
startup = get_startup_config(module).config_text startup = get_startup_config(module)
running_config = NetworkConfig(indent=1, contents=running, ignore_lines=diff_ignore_lines) running_config = NetworkConfig(indent=1, contents=running, ignore_lines=diff_ignore_lines)
startup_config = NetworkConfig(indent=1, contents=startup, ignore_lines=diff_ignore_lines) startup_config = NetworkConfig(indent=1, contents=startup, ignore_lines=diff_ignore_lines)
@ -390,7 +392,7 @@ def main():
if module._diff: if module._diff:
if not running_config: if not running_config:
contents = get_running_config(module).config_text contents = get_running_config(module)
else: else:
contents = running_config.config_text contents = running_config.config_text
@ -406,7 +408,7 @@ def main():
elif module.params['diff_against'] == 'startup': elif module.params['diff_against'] == 'startup':
if not startup_config: if not startup_config:
contents = get_startup_config(module).config_text contents = get_startup_config(module)
else: else:
contents = startup_config.config_text contents = startup_config.config_text

View file

@ -240,10 +240,10 @@ class Interfaces(FactsBase):
COMMANDS = [ COMMANDS = [
'show switch', 'show switch',
'run script cli2json.py show port config', {'command': 'show port config', 'output': 'json'},
'run script cli2json.py show port description', {'command': 'show port description', 'output': 'json'},
'run script cli2json.py show vlan detail', {'command': 'show vlan detail', 'output': 'json'},
'run script cli2json.py show lldp neighbors' {'command': 'show lldp neighbors', 'output': 'json'}
] ]
def populate(self): def populate(self):
@ -256,19 +256,19 @@ class Interfaces(FactsBase):
if data: if data:
sysmac = self.parse_sysmac(data) sysmac = self.parse_sysmac(data)
data = json.loads(self.responses[1]) data = self.responses[1]
if data: if data:
self.facts['interfaces'] = self.populate_interfaces(data, sysmac) self.facts['interfaces'] = self.populate_interfaces(data, sysmac)
data = json.loads(self.responses[2]) data = self.responses[2]
if data: if data:
self.populate_interface_descriptions(data) self.populate_interface_descriptions(data)
data = json.loads(self.responses[3]) data = self.responses[3]
if data: if data:
self.populate_vlan_interfaces(data, sysmac) self.populate_vlan_interfaces(data, sysmac)
data = json.loads(self.responses[4]) data = self.responses[4]
if data: if data:
self.facts['neighbors'] = self.parse_neighbors(data) self.facts['neighbors'] = self.parse_neighbors(data)

View file

@ -32,20 +32,51 @@ version_added: "2.6"
import re import re
import json import json
from itertools import chain from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.network.common.utils import to_list from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.plugins.cliconf import CliconfBase from ansible.plugins.cliconf import CliconfBase
class Cliconf(CliconfBase): class Cliconf(CliconfBase):
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
diff = {}
device_operations = self.get_device_operations()
option_values = self.get_option_values()
if candidate is None and device_operations['supports_generate_diff']:
raise ValueError("candidate configuration is required to generate diff")
if diff_match not in option_values['diff_match']:
raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
if diff_replace not in option_values['diff_replace']:
raise ValueError("'replace' value %s in invalid, valid values are %s" % (diff_replace, ', '.join(option_values['diff_replace'])))
# prepare candidate configuration
candidate_obj = NetworkConfig(indent=1)
candidate_obj.load(candidate)
if running and diff_match != 'none' and diff_replace != 'config':
# running configuration
running_obj = NetworkConfig(indent=1, 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 get_device_info(self): def get_device_info(self):
device_info = {} device_info = {}
device_info['network_os'] = 'exos' device_info['network_os'] = 'exos'
reply = self.get('show switch detail') reply = self.run_commands({'command': 'show switch detail', 'output': 'text'})
data = to_text(reply, errors='surrogate_or_strict').strip() data = to_text(reply, errors='surrogate_or_strict').strip()
match = re.search(r'ExtremeXOS version (\S+)', data) match = re.search(r'ExtremeXOS version (\S+)', data)
@ -62,69 +93,138 @@ class Cliconf(CliconfBase):
return device_info return device_info
def get_config(self, source='running', flags=None): def get_default_flag(self):
if source not in ('running', 'startup'): # The flag to modify the command to collect configuration with defaults
return 'detail'
def get_config(self, source='running', format='text', flags=None):
options_values = self.get_option_values()
if format not in options_values['format']:
raise ValueError("'format' value %s is invalid. Valid values are %s" % (format, ','.join(options_values['format'])))
lookup = {'running': 'show configuration', 'startup': 'debug cfgmgr show configuration file'}
if source not in lookup:
raise ValueError("fetching configuration from %s is not supported" % source) raise ValueError("fetching configuration from %s is not supported" % source)
if source == 'running':
cmd = 'show configuration' cmd = {'command': lookup[source], 'output': 'text'}
else:
cmd = 'debug cfgmgr show configuration file' if source == 'startup':
reply = self.get('show switch | include "Config Selected"') reply = self.run_commands({'command': 'show switch', 'format': 'text'})
data = to_text(reply, errors='surrogate_or_strict').strip() data = to_text(reply, errors='surrogate_or_strict').strip()
match = re.search(r': +(\S+)\.cfg', data) match = re.search(r'Config Selected: +(\S+)\.cfg', data, re.MULTILINE)
if match: if match:
cmd += ' '.join(match.group(1)) cmd['command'] += match.group(1)
cmd = cmd.strip()
flags = [] if flags is None else flags
cmd += ' '.join(flags)
cmd = cmd.strip()
return self.send_command(cmd)
def edit_config(self, command):
for cmd in chain(to_list(command)):
if isinstance(cmd, dict):
command = cmd['command']
prompt = cmd['prompt']
answer = cmd['answer']
newline = cmd.get('newline', True)
else: else:
command = cmd # No Startup(/Selected) Config
prompt = None return {}
answer = None
newline = True
self.send_command(to_bytes(command), to_bytes(prompt), to_bytes(answer),
False, newline)
def get(self, command, prompt=None, answer=None, sendonly=False, check_all=False): cmd['command'] += ' '.join(to_list(flags))
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all) cmd['command'] = cmd['command'].strip()
return self.run_commands(cmd)[0]
def edit_config(self, candidate=None, commit=True, replace=None, diff=False, comment=None):
resp = {}
operations = self.get_device_operations()
self.check_edit_config_capability(operations, candidate, commit, replace, comment)
results = []
requests = []
if commit:
for line in to_list(candidate):
if not isinstance(line, Mapping):
line = {'command': line}
results.append(self.send_command(**line))
requests.append(line['command'])
else:
raise ValueError('check mode is not supported')
resp['request'] = requests
resp['response'] = results
return resp
def get(self, command, prompt=None, answer=None, sendonly=False, output=None, check_all=False):
if output:
command = self._get_command_with_output(command, output)
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly, check_all=check_all)
def run_commands(self, commands=None, check_rc=True):
if commands is None:
raise ValueError("'commands' value is required")
responses = list()
for cmd in to_list(commands):
if not isinstance(cmd, Mapping):
cmd = {'command': cmd}
output = cmd.pop('output', None)
if output:
cmd['command'] = self._get_command_with_output(cmd['command'], output)
try:
out = self.send_command(**cmd)
except AnsibleConnectionFailure as e:
if check_rc is True:
raise
out = getattr(e, 'err', e)
if out is not None:
try:
out = to_text(out, errors='surrogate_or_strict').strip()
except UnicodeError:
raise ConnectionError(message=u'Failed to decode output from %s: %s' % (cmd, to_text(out)))
if output and output == 'json':
try:
out = json.loads(out)
except ValueError:
raise ConnectionError('Response was not valid JSON, got {0}'.format(
to_text(out)
))
responses.append(out)
return responses
def get_device_operations(self): def get_device_operations(self):
return { return {
'supports_diff_replace': True, 'supports_diff_replace': False, # identify if config should be merged or replaced is supported
'supports_commit': False, 'supports_commit': False, # identify if commit is supported by device or not
'supports_rollback': False, 'supports_rollback': False, # identify if rollback is supported or not
'supports_defaults': True, 'supports_defaults': True, # identify if fetching running config with default is supported
'supports_onbox_diff': False, 'supports_commit_comment': False, # identify if adding comment to commit is supported of not
'supports_commit_comment': False, 'supports_onbox_diff': False, # identify if on box diff capability is supported or not
'supports_multiline_delimiter': False, 'supports_generate_diff': True, # identify if diff capability is supported within plugin
'supports_diff_match': True, 'supports_multiline_delimiter': False, # identify if multiline delimiter is supported within config
'supports_diff_ignore_lines': True, 'supports_diff_match': True, # identify if match is supported
'supports_generate_diff': True, 'supports_diff_ignore_lines': True, # identify if ignore line in diff is supported
'supports_replace': True 'supports_config_replace': False, # identify if running config replace with candidate config is supported
'supports_admin': False, # identify if admin configure mode is supported or not
'supports_commit_label': False, # identify if commit label is supported or not
'supports_replace': False
} }
def get_option_values(self): def get_option_values(self):
return { return {
'format': ['text'], 'format': ['text', 'json'],
'diff_match': ['line', 'strict', 'exact', 'none'], 'diff_match': ['line', 'strict', 'exact', 'none'],
'diff_replace': ['line', 'block'], 'diff_replace': ['line', 'block'],
'output': ['text'] 'output': ['text', 'json']
} }
def get_capabilities(self): def get_capabilities(self):
result = super(Cliconf, self).get_capabilities() result = super(Cliconf, self).get_capabilities()
result['rpc'] += ['run_commmands', 'get_default_flag', 'get_diff']
result['device_operations'] = self.get_device_operations() result['device_operations'] = self.get_device_operations()
result['device_info'] = self.get_device_info()
result.update(self.get_option_values()) result.update(self.get_option_values())
return json.dumps(result) return json.dumps(result)
def _get_command_with_output(self, command, output):
if output not in self.get_option_values().get('output'):
raise ValueError("'output' value is %s is invalid. Valid values are %s" % (output, ','.join(self.get_option_values().get('output'))))
if output == 'json' and not command.startswith('run script cli2json.py'):
cmd = 'run script cli2json.py %s' % command
else:
cmd = command
return cmd

View file

@ -0,0 +1,253 @@
# Copyright (c) 2019 Extreme Networks.
#
# 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/>.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = """
---
author:
- "Ujwal Komarla (@ujwalkomarla)"
httpapi: exos
short_description: Use EXOS REST APIs to communicate with EXOS platform
description:
- This plugin provides low level abstraction api's to send REST API
requests to EXOS network devices and receive JSON responses.
version_added: "2.8"
"""
import json
import re
from ansible.module_utils._text import to_text
from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.network.common.utils import to_list
from ansible.plugins.httpapi import HttpApiBase
import ansible.module_utils.six.moves.http_cookiejar as cookiejar
from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.network.common.config import NetworkConfig, dumps
class HttpApi(HttpApiBase):
def __init__(self, *args, **kwargs):
super(HttpApi, self).__init__(*args, **kwargs)
self._device_info = None
self._auth_token = cookiejar.CookieJar()
def login(self, username, password):
auth_path = '/auth/token'
credentials = {'username': username, 'password': password}
self.send_request(path=auth_path, data=json.dumps(credentials), method='POST')
def logout(self):
pass
def handle_httperror(self, exc):
return False
def send_request(self, path, data=None, method='GET', **message_kwargs):
headers = {'Content-Type': 'application/json'}
response, response_data = self.connection.send(path, data, method=method, cookies=self._auth_token, headers=headers, **message_kwargs)
try:
if response.status == 204:
response_data = {}
else:
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 response_data
def run_commands(self, commands, check_rc=True):
if commands is None:
raise ValueError("'commands' value is required")
headers = {'Content-Type': 'application/json'}
responses = list()
for cmd in to_list(commands):
if not isinstance(cmd, Mapping):
cmd = {'command': cmd}
cmd['command'] = strip_run_script_cli2json(cmd['command'])
output = cmd.pop('output', None)
if output and output not in self.get_option_values().get('output'):
raise ValueError("'output' value is %s is invalid. Valid values are %s" % (output, ','.join(self.get_option_values().get('output'))))
data = request_builder(cmd['command'])
response, response_data = self.connection.send('/jsonrpc', data, cookies=self._auth_token, 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())
))
if response_data.get('error', None):
raise ConnectionError("Request Error, got {0}".format(response_data['error']))
if not response_data.get('result', None):
raise ConnectionError("Request Error, got {0}".format(response_data))
response_data = response_data['result']
if output and output == 'text':
statusOut = getKeyInResponse(response_data, 'status')
cliOut = getKeyInResponse(response_data, 'CLIoutput')
if statusOut == "ERROR":
raise ConnectionError("Command error({1}) for request {0}".format(cmd['command'], cliOut))
if cliOut is None:
raise ValueError("Response for request {0} doesn't have the CLIoutput field, got {1}".format(cmd['command'], response_data))
response_data = cliOut
responses.append(response_data)
return responses
def get_device_info(self):
device_info = {}
device_info['network_os'] = 'exos'
reply = self.run_commands({'command': 'show switch detail', 'output': 'text'})
data = to_text(reply, errors='surrogate_or_strict').strip()
match = re.search(r'ExtremeXOS version (\S+)', data)
if match:
device_info['network_os_version'] = match.group(1)
match = re.search(r'System Type: +(\S+)', data)
if match:
device_info['network_os_model'] = match.group(1)
match = re.search(r'SysName: +(\S+)', data)
if match:
device_info['network_os_hostname'] = match.group(1)
return device_info
def get_device_operations(self):
return {
'supports_diff_replace': False, # identify if config should be merged or replaced is supported
'supports_commit': False, # identify if commit is supported by device or not
'supports_rollback': False, # identify if rollback is supported or not
'supports_defaults': True, # identify if fetching running config with default is supported
'supports_commit_comment': False, # identify if adding comment to commit is supported of not
'supports_onbox_diff': False, # identify if on box diff capability is supported or not
'supports_generate_diff': True, # identify if diff capability is supported within plugin
'supports_multiline_delimiter': False, # identify if multiline demiliter is supported within config
'supports_diff_match': True, # identify if match is supported
'supports_diff_ignore_lines': True, # identify if ignore line in diff is supported
'supports_config_replace': False, # identify if running config replace with candidate config is supported
'supports_admin': False, # identify if admin configure mode is supported or not
'supports_commit_label': False # identify if commit label is supported or not
}
def get_option_values(self):
return {
'format': ['text', 'json'],
'diff_match': ['line', 'strict', 'exact', 'none'],
'diff_replace': ['line', 'block'],
'output': ['text', 'json']
}
def get_capabilities(self):
result = {}
result['rpc'] = ['get_default_flag', 'run_commands', 'get_config', 'send_request', 'get_capabilities', 'get_diff']
result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations()
result.update(self.get_option_values())
result['network_api'] = 'exosapi'
return json.dumps(result)
def get_default_flag(self):
# The flag to modify the command to collect configuration with defaults
return 'detail'
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
diff = {}
device_operations = self.get_device_operations()
option_values = self.get_option_values()
if candidate is None and device_operations['supports_generate_diff']:
raise ValueError("candidate configuration is required to generate diff")
if diff_match not in option_values['diff_match']:
raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
if diff_replace not in option_values['diff_replace']:
raise ValueError("'replace' value %s in invalid, valid values are %s" % (diff_replace, ', '.join(option_values['diff_replace'])))
# prepare candidate configuration
candidate_obj = NetworkConfig(indent=1)
candidate_obj.load(candidate)
if running and diff_match != 'none' and diff_replace != 'config':
# running configuration
running_obj = NetworkConfig(indent=1, 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 get_config(self, source='running', format='text', flags=None):
options_values = self.get_option_values()
if format not in options_values['format']:
raise ValueError("'format' value %s is invalid. Valid values are %s" % (format, ','.join(options_values['format'])))
lookup = {'running': 'show configuration', 'startup': 'debug cfgmgr show configuration file'}
if source not in lookup:
raise ValueError("fetching configuration from %s is not supported" % source)
cmd = {'command': lookup[source], 'output': 'text'}
if source == 'startup':
reply = self.run_commands({'command': 'show switch', 'format': 'text'})
data = to_text(reply, errors='surrogate_or_strict').strip()
match = re.search(r'Config Selected: +(\S+)\.cfg', data, re.MULTILINE)
if match:
cmd['command'] += match.group(1)
else:
# No Startup(/Selected) Config
return {}
cmd['command'] += ' '.join(to_list(flags))
cmd['command'] = cmd['command'].strip()
return self.run_commands(cmd)[0]
def request_builder(command, reqid=""):
return json.dumps(dict(jsonrpc='2.0', id=reqid, method='cli', params=to_list(command)))
def strip_run_script_cli2json(command):
if to_text(command, errors="surrogate_then_replace").startswith('run script cli2json.py'):
command = str(command).replace('run script cli2json.py', '')
return command
def getKeyInResponse(response, key):
keyOut = None
for item in response:
if key in item:
keyOut = item[key]
break
return keyOut

View file

@ -19,8 +19,9 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from units.compat.mock import patch from units.compat.mock import patch, MagicMock
from units.modules.utils import set_module_args from units.modules.utils import set_module_args
from ansible.plugins.cliconf.exos import Cliconf
from ansible.modules.network.exos import exos_config from ansible.modules.network.exos import exos_config
from .exos_module import TestExosModule, load_fixture from .exos_module import TestExosModule, load_fixture
@ -41,11 +42,22 @@ class TestExosConfigModule(TestExosModule):
self.mock_run_commands = patch('ansible.modules.network.exos.exos_config.run_commands') self.mock_run_commands = patch('ansible.modules.network.exos.exos_config.run_commands')
self.run_commands = self.mock_run_commands.start() self.run_commands = self.mock_run_commands.start()
self.mock_get_startup_config = patch('ansible.modules.network.exos.exos_config.get_startup_config')
self.get_startup_config = self.mock_get_startup_config.start()
self.cliconf_obj = Cliconf(MagicMock())
self.mock_get_diff = patch('ansible.modules.network.exos.exos_config.get_diff')
self.get_diff = self.mock_get_diff.start()
self.running_config = load_fixture('exos_config_config.cfg')
def tearDown(self): def tearDown(self):
super(TestExosConfigModule, self).tearDown() super(TestExosConfigModule, self).tearDown()
self.mock_get_config.stop() self.mock_get_config.stop()
self.mock_load_config.stop() self.mock_load_config.stop()
self.mock_run_commands.stop() self.mock_run_commands.stop()
self.mock_get_startup_config.stop()
def load_fixtures(self, commands=None): def load_fixtures(self, commands=None):
config_file = 'exos_config_config.cfg' config_file = 'exos_config_config.cfg'
@ -55,6 +67,7 @@ class TestExosConfigModule(TestExosModule):
def test_exos_config_unchanged(self): def test_exos_config_unchanged(self):
src = load_fixture('exos_config_config.cfg') src = load_fixture('exos_config_config.cfg')
set_module_args(dict(src=src)) set_module_args(dict(src=src))
self.get_diff.return_value = self.cliconf_obj.get_diff(src, src)
self.execute_module() self.execute_module()
def test_exos_config_src(self): def test_exos_config_src(self):
@ -62,6 +75,7 @@ class TestExosConfigModule(TestExosModule):
set_module_args(dict(src=src)) set_module_args(dict(src=src))
commands = ['configure ports 1 description-string "IDS"', commands = ['configure ports 1 description-string "IDS"',
'configure snmp sysName "marble"'] 'configure snmp sysName "marble"']
self.get_diff.return_value = self.cliconf_obj.get_diff(src, self.running_config)
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
def test_exos_config_backup(self): def test_exos_config_backup(self):
@ -84,6 +98,7 @@ class TestExosConfigModule(TestExosModule):
set_module_args(dict(src=src, save_when='changed')) set_module_args(dict(src=src, save_when='changed'))
commands = ['configure ports 1 description-string "IDS"', commands = ['configure ports 1 description-string "IDS"',
'configure snmp sysName "marble"'] 'configure snmp sysName "marble"']
self.get_diff.return_value = self.cliconf_obj.get_diff(src, self.running_config)
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
self.assertEqual(self.run_commands.call_count, 1) self.assertEqual(self.run_commands.call_count, 1)
self.assertEqual(self.get_config.call_count, 1) self.assertEqual(self.get_config.call_count, 1)
@ -96,6 +111,7 @@ class TestExosConfigModule(TestExosModule):
set_module_args(dict(src=src, save_when='changed', _ansible_check_mode=True)) set_module_args(dict(src=src, save_when='changed', _ansible_check_mode=True))
commands = ['configure ports 1 description-string "IDS"', commands = ['configure ports 1 description-string "IDS"',
'configure snmp sysName "marble"'] 'configure snmp sysName "marble"']
self.get_diff.return_value = self.cliconf_obj.get_diff(src, self.running_config)
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
self.assertEqual(self.run_commands.call_count, 0) self.assertEqual(self.run_commands.call_count, 0)
self.assertEqual(self.get_config.call_count, 1) self.assertEqual(self.get_config.call_count, 1)
@ -109,65 +125,64 @@ class TestExosConfigModule(TestExosModule):
self.assertEqual(self.load_config.call_count, 0) self.assertEqual(self.load_config.call_count, 0)
def test_exos_config_save_modified_false(self): def test_exos_config_save_modified_false(self):
mock_get_startup_config_text = patch('ansible.modules.network.exos.exos_config.get_startup_config_text') self.get_startup_config.return_value = load_fixture('exos_config_config.cfg')
get_startup_config_text = mock_get_startup_config_text.start()
get_startup_config_text.return_value = load_fixture('exos_config_config.cfg')
set_module_args(dict(save_when='modified')) set_module_args(dict(save_when='modified'))
self.execute_module(changed=False) self.execute_module(changed=False)
self.assertEqual(self.run_commands.call_count, 0) self.assertEqual(self.run_commands.call_count, 0)
self.assertEqual(self.get_config.call_count, 1) self.assertEqual(self.get_config.call_count, 1)
self.assertEqual(get_startup_config_text.call_count, 1) self.assertEqual(self.get_startup_config.call_count, 1)
self.assertEqual(self.load_config.call_count, 0) self.assertEqual(self.load_config.call_count, 0)
mock_get_startup_config_text.stop()
def test_exos_config_save_modified_true(self): def test_exos_config_save_modified_true(self):
mock_get_startup_config_text = patch('ansible.modules.network.exos.exos_config.get_startup_config_text') self.get_startup_config.return_value = load_fixture('exos_config_modified.cfg')
get_startup_config_text = mock_get_startup_config_text.start()
get_startup_config_text.return_value = load_fixture('exos_config_modified.cfg')
set_module_args(dict(save_when='modified')) set_module_args(dict(save_when='modified'))
self.execute_module(changed=True) self.execute_module(changed=True)
self.assertEqual(self.run_commands.call_count, 1) self.assertEqual(self.run_commands.call_count, 1)
self.assertTrue(self.get_config.call_count > 0) self.assertTrue(self.get_config.call_count > 0)
self.assertEqual(get_startup_config_text.call_count, 1) self.assertEqual(self.get_startup_config.call_count, 1)
self.assertEqual(self.load_config.call_count, 0) self.assertEqual(self.load_config.call_count, 0)
mock_get_startup_config_text.stop()
def test_exos_config_lines(self): def test_exos_config_lines(self):
set_module_args(dict(lines=['configure snmp sysName "marble"'])) lines = ['configure snmp sysName "marble"']
set_module_args(dict(lines=lines))
commands = ['configure snmp sysName "marble"'] commands = ['configure snmp sysName "marble"']
self.get_diff.return_value = self.cliconf_obj.get_diff('\n'.join(lines), self.running_config)
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
def test_exos_config_before(self): def test_exos_config_before(self):
set_module_args(dict(lines=['configure snmp sysName "marble"'], before=['test1', 'test2'])) lines = ['configure snmp sysName "marble"']
set_module_args(dict(lines=lines, before=['test1', 'test2']))
commands = ['test1', 'test2', 'configure snmp sysName "marble"'] commands = ['test1', 'test2', 'configure snmp sysName "marble"']
self.get_diff.return_value = self.cliconf_obj.get_diff('\n'.join(lines), self.running_config)
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)
def test_exos_config_after(self): def test_exos_config_after(self):
set_module_args(dict(lines=['hostname foo'], after=['test1', 'test2'])) lines = ['configure snmp sysName "marble"']
commands = ['hostname foo', 'test1', 'test2'] set_module_args(dict(lines=lines, after=['test1', 'test2']))
set_module_args(dict(lines=['configure snmp sysName "marble"'], after=['test1', 'test2']))
commands = ['configure snmp sysName "marble"', 'test1', 'test2'] commands = ['configure snmp sysName "marble"', 'test1', 'test2']
self.get_diff.return_value = self.cliconf_obj.get_diff('\n'.join(lines), self.running_config)
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)
def test_exos_config_before_after_no_change(self): def test_exos_config_before_after_no_change(self):
set_module_args(dict(lines=['configure snmp sysName "x870"'], lines = ['configure snmp sysName "x870"']
set_module_args(dict(lines=lines,
before=['test1', 'test2'], before=['test1', 'test2'],
after=['test3', 'test4'])) after=['test3', 'test4']))
self.get_diff.return_value = self.cliconf_obj.get_diff('\n'.join(lines), self.running_config)
self.execute_module() self.execute_module()
def test_exos_config_config(self): def test_exos_config_config(self):
config = 'hostname localhost' config = 'hostname localhost'
set_module_args(dict(lines=['configure snmp sysName "x870"'], config=config)) lines = ['configure snmp sysName "x870"']
set_module_args(dict(lines=lines, config=config))
commands = ['configure snmp sysName "x870"'] commands = ['configure snmp sysName "x870"']
self.get_diff.return_value = self.cliconf_obj.get_diff('\n'.join(lines), config)
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
def test_exos_config_match_none(self): def test_exos_config_match_none(self):
lines = ['configure snmp sysName "x870"'] lines = ['configure snmp sysName "x870"']
set_module_args(dict(lines=lines, match='none')) set_module_args(dict(lines=lines, match='none'))
self.get_diff.return_value = self.cliconf_obj.get_diff('\n'.join(lines), self.running_config, diff_match='none')
self.execute_module(changed=True, commands=lines) self.execute_module(changed=True, commands=lines)
def test_exos_config_src_and_lines_fails(self): def test_exos_config_src_and_lines_fails(self):
@ -208,39 +223,43 @@ class TestExosConfigModule(TestExosModule):
self.execute_module(changed=False) self.execute_module(changed=False)
def test_exos_diff_startup_unchanged(self): def test_exos_diff_startup_unchanged(self):
mock_get_startup_config_text = patch('ansible.modules.network.exos.exos_config.get_startup_config_text') mock_get_startup_config = patch('ansible.modules.network.exos.exos_config.get_startup_config')
get_startup_config_text = mock_get_startup_config_text.start() get_startup_config = mock_get_startup_config.start()
get_startup_config_text.return_value = load_fixture('exos_config_config.cfg') get_startup_config.return_value = load_fixture('exos_config_config.cfg')
args = dict(diff_against='startup', _ansible_diff=True) args = dict(diff_against='startup', _ansible_diff=True)
set_module_args(args) set_module_args(args)
self.execute_module(changed=False) self.execute_module(changed=False)
self.assertEqual(get_startup_config_text.call_count, 1) self.assertEqual(get_startup_config.call_count, 1)
mock_get_startup_config_text.stop() mock_get_startup_config.stop()
def test_exos_diff_startup_changed(self): def test_exos_diff_startup_changed(self):
mock_get_startup_config_text = patch('ansible.modules.network.exos.exos_config.get_startup_config_text') mock_get_startup_config = patch('ansible.modules.network.exos.exos_config.get_startup_config')
get_startup_config_text = mock_get_startup_config_text.start() get_startup_config = mock_get_startup_config.start()
get_startup_config_text.return_value = load_fixture('exos_config_modified.cfg') get_startup_config.return_value = load_fixture('exos_config_modified.cfg')
args = dict(diff_against='startup', _ansible_diff=True) args = dict(diff_against='startup', _ansible_diff=True)
set_module_args(args) set_module_args(args)
self.execute_module(changed=True) self.execute_module(changed=True)
self.assertEqual(get_startup_config_text.call_count, 1) self.assertEqual(get_startup_config.call_count, 1)
mock_get_startup_config_text.stop() mock_get_startup_config.stop()
def test_exos_diff_intended_unchanged(self): def test_exos_diff_intended_unchanged(self):
intended_config = load_fixture('exos_config_config.cfg')
args = dict(diff_against='intended', args = dict(diff_against='intended',
intended_config=load_fixture('exos_config_config.cfg'), intended_config=intended_config,
_ansible_diff=True) _ansible_diff=True)
set_module_args(args) set_module_args(args)
self.get_diff = self.cliconf_obj.get_diff(intended_config, self.running_config)
self.execute_module(changed=False) self.execute_module(changed=False)
def test_exos_diff_intended_modified(self): def test_exos_diff_intended_modified(self):
intended_config = load_fixture('exos_config_modified.cfg')
args = dict(diff_against='intended', args = dict(diff_against='intended',
intended_config=load_fixture('exos_config_modified.cfg'), intended_config=intended_config,
_ansible_diff=True) _ansible_diff=True)
set_module_args(args) set_module_args(args)
self.get_diff = self.cliconf_obj.get_diff(intended_config, self.running_config)
self.execute_module(changed=True) self.execute_module(changed=True)

View file

@ -20,9 +20,11 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import os import os
import json
from units.compat.mock import patch from units.compat.mock import patch
from units.modules.utils import set_module_args from units.modules.utils import set_module_args
from ansible.module_utils.common._collections_compat import Mapping
from ansible.modules.network.exos import exos_facts from ansible.modules.network.exos import exos_facts
from .exos_module import TestExosModule from .exos_module import TestExosModule
@ -49,10 +51,18 @@ class TestExosFactsModule(TestExosModule):
fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
for command in commands: for command in commands:
if isinstance(command, Mapping):
command = command['command']
filename = str(command).replace(' ', '_') filename = str(command).replace(' ', '_')
filename = os.path.join(fixture_path, filename) filename = os.path.join(fixture_path, filename)
with open(filename) as f: with open(filename) as f:
data = f.read() data = f.read()
try:
data = json.loads(data)
except Exception:
pass
output.append(data) output.append(data)
return output return output