Update eos, ios, vyos cliconf plugin (#42300)

* Update eos cliconf plugin methods

*  Refactor eos cliconf plugin
*  Changes in eos module_utils as per cliconf plugin refactor

* Fix unit test and sanity failures

* Fix review comment
pull/42526/head
Ganesh Nalawade 6 years ago committed by GitHub
parent 2aa81bf05d
commit c068b88b38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -104,7 +104,7 @@ DEFAULT_REMOTE_PASS = None
DEFAULT_SUBSET = None DEFAULT_SUBSET = None
DEFAULT_SU_PASS = None DEFAULT_SU_PASS = None
# FIXME: expand to other plugins, but never doc fragments # FIXME: expand to other plugins, but never doc fragments
CONFIGURABLE_PLUGINS = ('cache', 'callback', 'connection', 'inventory', 'lookup', 'shell') CONFIGURABLE_PLUGINS = ('cache', 'callback', 'connection', 'inventory', 'lookup', 'shell', 'cliconf')
# NOTE: always update the docs/docsite/Makefile to match # NOTE: always update the docs/docsite/Makefile to match
DOCUMENTABLE_PLUGINS = CONFIGURABLE_PLUGINS + ('module', 'strategy', 'vars') DOCUMENTABLE_PLUGINS = CONFIGURABLE_PLUGINS + ('module', 'strategy', 'vars')
IGNORE_FILES = ("COPYING", "CONTRIBUTING", "LICENSE", "README", "VERSION", "GUIDELINES") # ignore during module search IGNORE_FILES = ("COPYING", "CONTRIBUTING", "LICENSE", "README", "VERSION", "GUIDELINES") # ignore during module search

@ -33,6 +33,7 @@ import time
from ansible.module_utils._text import to_text, to_native from ansible.module_utils._text import to_text, to_native
from ansible.module_utils.basic import env_fallback, return_values from ansible.module_utils.basic import env_fallback, return_values
from ansible.module_utils.connection import Connection, ConnectionError from ansible.module_utils.connection import Connection, ConnectionError
from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.module_utils.network.common.utils import to_list, ComplexList from ansible.module_utils.network.common.utils import to_list, ComplexList
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible.module_utils.urls import fetch_url from ansible.module_utils.urls import fetch_url
@ -121,27 +122,6 @@ class Cli:
return self._connection return self._connection
def close_session(self, session):
conn = self._get_connection()
# to close session gracefully execute abort in top level session prompt.
conn.get('end')
conn.get('configure session %s' % session)
conn.get('abort')
@property
def supports_sessions(self):
if self._session_support is not None:
return self._session_support
conn = self._get_connection()
self._session_support = True
try:
out = conn.get('show configuration sessions')
except:
self._session_support = False
return self._session_support
def get_config(self, flags=None): def get_config(self, flags=None):
"""Retrieves the current config from the device or cache """Retrieves the current config from the device or cache
""" """
@ -155,7 +135,7 @@ class Cli:
return self._device_configs[cmd] return self._device_configs[cmd]
except KeyError: except KeyError:
conn = self._get_connection() conn = self._get_connection()
out = conn.get_config(flags=flags) out = conn.get_config(filter=flags)
cfg = to_text(out, errors='surrogate_then_replace').strip() cfg = to_text(out, errors='surrogate_then_replace').strip()
self._device_configs[cmd] = cfg self._device_configs[cmd] = cfg
return cfg return cfg
@ -164,48 +144,27 @@ class Cli:
"""Run list of commands on remote device and return results """Run list of commands on remote device and return results
""" """
connection = self._get_connection() connection = self._get_connection()
return connection.run_commands(commands, check_rc) return connection.run_commands(commands=commands, check_rc=check_rc)
def configure(self, commands):
"""Sends configuration commands to the remote device
"""
conn = get_connection(self)
out = conn.get('configure')
try:
self.send_config(commands)
except ConnectionError as exc:
conn.get('end')
message = getattr(exc, 'err', exc)
self._module.fail_json(msg="Error on executing commands %s" % commands, data=to_text(message, errors='surrogate_then_replace'))
conn.get('end')
return {}
def load_config(self, commands, commit=False, replace=False): def load_config(self, commands, commit=False, replace=False):
"""Loads the config commands onto the remote device """Loads the config commands onto the remote device
""" """
use_session = os.getenv('ANSIBLE_EOS_USE_SESSIONS', True)
try:
use_session = int(use_session)
except ValueError:
pass
if not all((bool(use_session), self.supports_sessions)):
if commit:
return self.configure(commands)
else:
self._module.warn("EOS can not check config without config session")
result = {'changed': True}
return result
conn = self._get_connection() conn = self._get_connection()
try: try:
return conn.load_config(commands, commit, replace) response = conn.edit_config(commands, commit, replace)
except ConnectionError as exc: except ConnectionError as exc:
message = getattr(exc, 'err', exc) message = getattr(exc, 'err', exc)
self._module.fail_json(msg="%s" % message, data=to_text(message, errors='surrogate_then_replace')) if "check mode is not supported without configuration session" in message:
self._module.warn("EOS can not check config without config session")
response = {'changed': True}
else:
self._module.fail_json(msg="%s" % message, data=to_text(message, errors='surrogate_then_replace'))
return response
def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, replace='line'):
conn = self._get_connection()
return conn.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=diff_ignore_lines, path=path, replace=replace)
class Eapi: class Eapi:
@ -397,6 +356,26 @@ class Eapi:
return result return result
# get_diff added here to support connection=local and transport=eapi scenario
def get_diff(self, candidate, running=None, match='line', diff_ignore_lines=None, path=None, replace='line'):
diff = {}
# prepare candidate configuration
candidate_obj = NetworkConfig(indent=3)
candidate_obj.load(candidate)
if running and match != 'none' and 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=match, replace=replace)
else:
configdiffobjs = candidate_obj.items
configdiff = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
diff['config_diff'] = configdiff if configdiffobjs else {}
return diff
def is_json(cmd): def is_json(cmd):
return to_native(cmd, errors='surrogate_then_replace').endswith('| json') return to_native(cmd, errors='surrogate_then_replace').endswith('| json')
@ -431,11 +410,16 @@ def get_config(module, flags=None):
return conn.get_config(flags) return conn.get_config(flags)
def run_commands(module, commands): def run_commands(module, commands, check_rc=True):
conn = get_connection(module) conn = get_connection(module)
return conn.run_commands(to_command(module, commands)) return conn.run_commands(to_command(module, commands), check_rc=check_rc)
def load_config(module, config, commit=False, replace=False): def load_config(module, config, commit=False, replace=False):
conn = get_connection(module) conn = get_connection(module)
return conn.load_config(config, commit, replace) return conn.load_config(config, commit, replace)
def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, replace='line'):
conn = self.get_connection()
return conn.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=diff_ignore_lines, path=path, replace=replace)

@ -131,25 +131,8 @@ def to_commands(module, commands):
def run_commands(module, commands, check_rc=True): def run_commands(module, commands, check_rc=True):
responses = list()
connection = get_connection(module) connection = get_connection(module)
return connection.run_commands(commands=commands, check_rc=check_rc)
try:
outputs = connection.run_commands(commands)
except ConnectionError as exc:
if check_rc:
module.fail_json(msg=to_text(exc))
else:
outputs = exc
for item in to_list(outputs):
try:
item = to_text(item, errors='surrogate_or_strict')
except UnicodeError:
module.fail_json(msg=u'Failed to decode output from %s: %s' % (item, to_text(item)))
responses.append(item)
return responses
def load_config(module, commands): def load_config(module, commands):

@ -26,9 +26,9 @@
# 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 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, return_values from ansible.module_utils.basic import env_fallback, return_values
from ansible.module_utils.network.common.utils import to_list
from ansible.module_utils.connection import Connection, ConnectionError from ansible.module_utils.connection import Connection, ConnectionError
_DEVICE_CONFIGS = {} _DEVICE_CONFIGS = {}
@ -100,26 +100,8 @@ def get_config(module):
def run_commands(module, commands, check_rc=True): def run_commands(module, commands, check_rc=True):
responses = list()
connection = get_connection(module) connection = get_connection(module)
return connection.run_commands(commands=commands, check_rc=check_rc)
try:
outputs = connection.run_commands(commands)
except ConnectionError as exc:
if check_rc:
module.fail_json(msg=to_text(exc))
else:
outputs = exc
for item in to_list(outputs):
try:
item = to_text(item, errors='surrogate_or_strict')
except UnicodeError:
module.fail_json(msg=u'Failed to decode output from %s: %s' % (item, to_text(item)))
responses.append(item)
return responses
def load_config(module, commands, commit=False, comment=None): def load_config(module, commands, commit=False, comment=None):
@ -127,7 +109,6 @@ def load_config(module, commands, commit=False, comment=None):
try: try:
resp = connection.edit_config(candidate=commands, commit=commit, comment=comment) resp = connection.edit_config(candidate=commands, commit=commit, comment=comment)
resp = json.loads(resp)
except ConnectionError as exc: except ConnectionError as exc:
module.fail_json(msg=to_text(exc)) module.fail_json(msg=to_text(exc))

@ -266,33 +266,32 @@ backup_path:
""" """
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.network.eos.eos import get_config, load_config from ansible.module_utils.network.eos.eos import get_config, load_config, get_connection
from ansible.module_utils.network.eos.eos import run_commands from ansible.module_utils.network.eos.eos import run_commands
from ansible.module_utils.network.eos.eos import eos_argument_spec from ansible.module_utils.network.eos.eos import eos_argument_spec
from ansible.module_utils.network.eos.eos import check_args from ansible.module_utils.network.eos.eos import check_args
def get_candidate(module): def get_candidate(module):
candidate = NetworkConfig(indent=3) candidate = ''
if module.params['src']: if module.params['src']:
candidate.load(module.params['src']) candidate = module.params['src']
elif module.params['lines']: elif module.params['lines']:
candidate_obj = NetworkConfig(indent=3)
parents = module.params['parents'] or list() parents = module.params['parents'] or list()
candidate.add(module.params['lines'], parents=parents) candidate_obj.add(module.params['lines'], parents=parents)
candidate = dumps(candidate_obj, 'raw')
return candidate return candidate
def get_running_config(module, config=None): def get_running_config(module, config=None, flags=None):
contents = module.params['running_config'] contents = module.params['running_config']
if not contents: if not contents:
if config: if config:
contents = config contents = config
else: else:
flags = []
if module.params['defaults']:
flags.append('all')
contents = get_config(module, flags=flags) contents = get_config(module, flags=flags)
return NetworkConfig(indent=3, contents=contents) return contents
def save_config(module, result): def save_config(module, result):
@ -363,30 +362,31 @@ def main():
if warnings: if warnings:
result['warnings'] = warnings result['warnings'] = warnings
diff_ignore_lines = module.params['diff_ignore_lines']
config = None config = None
contents = None
flags = ['all'] if module.params['defaults'] else []
connection = get_connection(module)
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=3, contents=contents) config = NetworkConfig(indent=1, contents=contents)
if module.params['backup']: if module.params['backup']:
result['__backup__'] = contents result['__backup__'] = contents
if any((module.params['src'], module.params['lines'])): if any((module.params['src'], module.params['lines'])):
match = module.params['match'] match = module.params['match']
replace = module.params['replace'] replace = module.params['replace']
path = module.params['parents']
candidate = get_candidate(module) candidate = get_candidate(module)
running = get_running_config(module, contents, flags=flags)
if match != 'none' and replace != 'config': response = connection.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=diff_ignore_lines, path=path, replace=replace)
config_text = get_running_config(module) config_diff = response['config_diff']
config = NetworkConfig(indent=3, contents=config_text)
path = module.params['parents']
configobjs = candidate.difference(config, match=match, replace=replace, path=path)
else:
configobjs = candidate.items
if configobjs: if config_diff:
commands = dumps(configobjs, 'commands').split('\n') commands = config_diff.split('\n')
if module.params['before']: if module.params['before']:
commands[:0] = module.params['before'] commands[:0] = module.params['before']
@ -413,16 +413,14 @@ def main():
running_config = module.params['running_config'] running_config = module.params['running_config']
startup_config = None startup_config = None
diff_ignore_lines = module.params['diff_ignore_lines']
if module.params['save_when'] == 'always' or module.params['save']: if module.params['save_when'] == 'always' or module.params['save']:
save_config(module, result) save_config(module, result)
elif module.params['save_when'] == 'modified': elif module.params['save_when'] == 'modified':
output = run_commands(module, [{'command': 'show running-config', 'output': 'text'}, output = run_commands(module, [{'command': 'show running-config', 'output': 'text'},
{'command': 'show startup-config', 'output': 'text'}]) {'command': 'show startup-config', 'output': 'text'}])
running_config = NetworkConfig(indent=1, contents=output[0], ignore_lines=diff_ignore_lines) running_config = NetworkConfig(indent=3, contents=output[0], ignore_lines=diff_ignore_lines)
startup_config = NetworkConfig(indent=1, contents=output[1], ignore_lines=diff_ignore_lines) startup_config = NetworkConfig(indent=3, contents=output[1], ignore_lines=diff_ignore_lines)
if running_config.sha1 != startup_config.sha1: if running_config.sha1 != startup_config.sha1:
save_config(module, result) save_config(module, result)
@ -438,7 +436,7 @@ def main():
contents = running_config contents = running_config
# recreate the object in order to process diff_ignore_lines # recreate the object in order to process diff_ignore_lines
running_config = NetworkConfig(indent=1, contents=contents, ignore_lines=diff_ignore_lines) running_config = NetworkConfig(indent=3, contents=contents, ignore_lines=diff_ignore_lines)
if module.params['diff_against'] == 'running': if module.params['diff_against'] == 'running':
if module.check_mode: if module.check_mode:
@ -458,7 +456,7 @@ def main():
contents = module.params['intended_config'] contents = module.params['intended_config']
if contents is not None: if contents is not None:
base_config = NetworkConfig(indent=1, contents=contents, ignore_lines=diff_ignore_lines) base_config = NetworkConfig(indent=3, contents=contents, ignore_lines=diff_ignore_lines)
if running_config.sha1 != base_config.sha1: if running_config.sha1 != base_config.sha1:
if module.params['diff_against'] == 'intended': if module.params['diff_against'] == 'intended':

@ -400,6 +400,7 @@ def main():
check_args(module, warnings) check_args(module, warnings)
result['warnings'] = warnings result['warnings'] = warnings
diff_ignore_lines = module.params['diff_ignore_lines']
config = None config = None
contents = None contents = None
flags = get_defaults_flag(module) if module.params['defaults'] else [] flags = get_defaults_flag(module) if module.params['defaults'] else []
@ -419,10 +420,9 @@ def main():
candidate = get_candidate_config(module) candidate = get_candidate_config(module)
running = get_running_config(module, contents, flags=flags) running = get_running_config(module, contents, flags=flags)
response = connection.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=None, path=path, replace=replace) response = connection.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=diff_ignore_lines, path=path, replace=replace)
diff = json.loads(response) config_diff = response['config_diff']
config_diff = diff['config_diff'] banner_diff = response['banner_diff']
banner_diff = diff['banner_diff']
if config_diff or banner_diff: if config_diff or banner_diff:
commands = config_diff.split('\n') commands = config_diff.split('\n')
@ -450,8 +450,6 @@ def main():
running_config = module.params['running_config'] running_config = module.params['running_config']
startup_config = None startup_config = None
diff_ignore_lines = module.params['diff_ignore_lines']
if module.params['save_when'] == 'always' or module.params['save']: if module.params['save_when'] == 'always' or module.params['save']:
save_config(module, result) save_config(module, result)
elif module.params['save_when'] == 'modified': elif module.params['save_when'] == 'modified':

@ -130,7 +130,6 @@ backup_path:
sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34 sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34
""" """
import re import re
import json
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.vyos.vyos import load_config, get_config, run_commands from ansible.module_utils.network.vyos.vyos import load_config, get_config, run_commands
@ -205,8 +204,7 @@ def run(module, result):
# create loadable config that includes only the configuration updates # create loadable config that includes only the configuration updates
connection = get_connection(module) connection = get_connection(module)
response = connection.get_diff(candidate=candidate, running=config, match=module.params['match']) response = connection.get_diff(candidate=candidate, running=config, match=module.params['match'])
diff_obj = json.loads(response) commands = response.get('config_diff')
commands = diff_obj.get('config_diff')
sanitize_config(commands, result) sanitize_config(commands, result)
result['commands'] = commands result['commands'] = commands

@ -19,12 +19,13 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
from abc import ABCMeta, abstractmethod from abc import abstractmethod
from functools import wraps from functools import wraps
from ansible.plugins import AnsiblePlugin
from ansible.errors import AnsibleError, AnsibleConnectionFailure from ansible.errors import AnsibleError, 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.six import with_metaclass
try: try:
from scp import SCPClient from scp import SCPClient
@ -49,7 +50,7 @@ def enable_mode(func):
return wrapped return wrapped
class CliconfBase(with_metaclass(ABCMeta, object)): class CliconfBase(AnsiblePlugin):
""" """
A base class for implementing cli connections A base class for implementing cli connections
@ -84,6 +85,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
__rpc__ = ['get_config', 'edit_config', 'get_capabilities', 'get', 'enable_response_logging', 'disable_response_logging'] __rpc__ = ['get_config', 'edit_config', 'get_capabilities', 'get', 'enable_response_logging', 'disable_response_logging']
def __init__(self, connection): def __init__(self, connection):
super(CliconfBase, self).__init__()
self._connection = connection self._connection = connection
self.history = list() self.history = list()
self.response_logging = False self.response_logging = False
@ -375,7 +377,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
""" """
pass pass
def run_commands(self, commands): def run_commands(self, commands=None, check_rc=True):
""" """
Execute a list of commands on remote host and return the list of response Execute a list of commands on remote host and return the list of response
:param commands: The list of command that needs to be executed on remote host. :param commands: The list of command that needs to be executed on remote host.
@ -385,10 +387,12 @@ class CliconfBase(with_metaclass(ABCMeta, object)):
'command': <command to be executed> 'command': <command to be executed>
'prompt': <expected prompt on executing the command>, 'prompt': <expected prompt on executing the command>,
'answer': <answer for the prompt>, 'answer': <answer for the prompt>,
'output': <the format in which command output should be rendered eg: 'json', 'text', if supported by platform>, 'output': <the format in which command output should be rendered eg: 'json', 'text'>,
'sendonly': <Boolean flag to indicate if it command execution response should be ignored or not> 'sendonly': <Boolean flag to indicate if it command execution response should be ignored or not>
} }
:param check_rc: Boolean flag to check if returned response should be checked for error or not.
If check_rc is False the error output is appended in return response list, else if the
value is True an exception is raised.
:return: List of returned response :return: List of returned response
""" """
pass pass

@ -19,58 +19,82 @@
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
cliconf: eos
short_description: Use eos cliconf to run command on eos platform
description:
- This eos plugin provides low level abstraction api's for
sending and receiving CLI commands from eos network devices.
version_added: "2.7"
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.7'
"""
import collections
import json import json
import time import time
from itertools import chain
from ansible.errors import AnsibleConnectionFailure from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils._text import to_bytes from ansible.module_utils._text import 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.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.network_cli import Connection as NetworkCli
from ansible.plugins.connection.httpapi import Connection as HttpApi
class Cliconf(CliconfBase): class Cliconf(CliconfBase):
def send_command(self, command, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False): def __init__(self, *args, **kwargs):
super(Cliconf, self).__init__(*args, **kwargs)
self._session_support = None
if isinstance(self._connection, NetworkCli):
self.network_api = 'network_cli'
elif isinstance(self._connection, HttpApi):
self.network_api = 'eapi'
else:
raise ValueError("Invalid connection type")
def _get_command_with_output(self, command, output):
options_values = self.get_option_values()
if output not in options_values['output']:
raise ValueError("'output' value %s is invalid. Valid values are %s" % (output, ','.join(options_values['output'])))
if output == 'json' and not command.endswith('| json'):
cmd = '%s | json' % command
else:
cmd = command
return cmd
def send_command(self, command, **kwargs):
"""Executes a cli command and returns the results """Executes a cli command and returns the results
This method will execute the CLI command on the connection and return 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 the results to the caller. The command output will be returned as a
string string
""" """
kwargs = {'command': to_bytes(command), 'sendonly': sendonly, if self.network_api == 'network_cli':
'newline': newline, 'prompt_retry_check': prompt_retry_check} resp = super(Cliconf, self).send_command(command, **kwargs)
if prompt is not None:
kwargs['prompt'] = to_bytes(prompt)
if answer is not None:
kwargs['answer'] = to_bytes(answer)
if isinstance(self._connection, NetworkCli):
resp = self._connection.send(**kwargs)
else: else:
resp = self._connection.send_request(command, **kwargs) resp = self._connection.send_request(command, **kwargs)
return resp return resp
def get_device_info(self):
device_info = {}
device_info['network_os'] = 'eos'
reply = self.get('show version | json')
data = json.loads(reply)
device_info['network_os_version'] = data['version']
device_info['network_os_model'] = data['modelName']
reply = self.get('show hostname | json')
data = json.loads(reply)
device_info['network_os_hostname'] = data['hostname']
return device_info
@enable_mode @enable_mode
def get_config(self, source='running', format='text', flags=None): def get_config(self, source='running', format='text', filter=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': 'running-config', 'startup': 'startup-config'} lookup = {'running': 'running-config', 'startup': 'startup-config'}
if source not in lookup: if source not in lookup:
return self.invalid_params("fetching configuration from %s is not supported" % source) return self.invalid_params("fetching configuration from %s is not supported" % source)
@ -79,60 +103,112 @@ class Cliconf(CliconfBase):
if format and format is not 'text': if format and format is not 'text':
cmd += '| %s ' % format cmd += '| %s ' % format
cmd += ' '.join(to_list(flags)) cmd += ' '.join(to_list(filter))
cmd = cmd.strip() cmd = cmd.strip()
return self.send_command(cmd) return self.send_command(cmd)
@enable_mode @enable_mode
def edit_config(self, command): def edit_config(self, candidate=None, commit=True, replace=False, comment=None):
for cmd in chain(['configure'], to_list(command), ['end']):
self.send_command(cmd)
def get(self, command, prompt=None, answer=None, sendonly=False): if not candidate:
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) raise ValueError("must provide a candidate config to load")
def get_capabilities(self): if commit not in (True, False):
result = {} raise ValueError("'commit' must be a bool, got %s" % commit)
result['rpc'] = self.get_base_rpc()
result['device_info'] = self.get_device_info()
if isinstance(self._connection, NetworkCli):
result['network_api'] = 'cliconf'
else:
result['network_api'] = 'eapi'
return json.dumps(result)
# Imported from module_utils operations = self.get_device_operations()
def close_session(self, session): if replace not in (True, False):
# to close session gracefully execute abort in top level session prompt. raise ValueError("'replace' must be a bool, got %s" % replace)
self.get('end')
self.get('configure session %s' % session)
self.get('abort')
def run_commands(self, commands, check_rc=True): if replace and not operations['supports_replace']:
"""Run list of commands on remote device and return results raise ValueError("configuration replace is supported only with configuration session")
"""
responses = list()
multiline = False
for cmd in to_list(commands): if comment and not operations['supports_commit_comment']:
if isinstance(cmd, dict): raise ValueError("commit comment is not supported")
command = cmd['command']
prompt = cmd['prompt'] if (commit is False) and (not self.supports_sessions):
answer = cmd['answer'] raise ValueError('check mode is not supported without configuration session')
else:
command = cmd response = {}
prompt = None session = None
answer = None if self.supports_sessions:
session = 'ansible_%s' % int(time.time())
response.update({'session': session})
self.send_command('configure session %s' % session)
if replace:
self.send_command('rollback clean-config')
else:
self.send_command('configure')
results = []
multiline = False
for line in to_list(candidate):
if not isinstance(line, collections.Mapping):
line = {'command': line}
if command == 'end': cmd = line['command']
if cmd == 'end':
continue continue
elif command.startswith('banner') or multiline: elif cmd.startswith('banner') or multiline:
multiline = True multiline = True
elif command == 'EOF' and multiline: elif cmd == 'EOF' and multiline:
multiline = False multiline = False
if multiline:
line['sendonly'] = True
if cmd != 'end' and cmd[0] != '!':
try:
results.append(self.send_command(**line))
except AnsibleConnectionFailure as e:
self.discard_changes(session)
raise AnsibleConnectionFailure(e.message)
response['response'] = results
if self.supports_sessions:
out = self.send_command('show session-config diffs')
if out:
response['diff'] = out.strip()
if commit:
self.commit()
else:
self.discard_changes(session)
else:
self.send_command('end')
return response
def get(self, command, prompt=None, answer=None, sendonly=False, output=None):
if output:
command = self._get_command_with_output(command, output)
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
def commit(self):
self.send_command('commit')
def discard_changes(self, session=None):
commands = ['end']
if self.supports_sessions:
# to close session gracefully execute abort in top level session prompt.
commands.extend(['configure session %s' % session, 'abort'])
for cmd in commands:
self.send_command(cmd)
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, collections.Mapping):
cmd = {'command': cmd}
output = cmd.pop('output', None)
if output:
cmd['command'] = self._get_command_with_output(cmd['command'], output)
try: try:
out = self.get(command, prompt, answer, multiline) out = self.send_command(**cmd)
except AnsibleConnectionFailure as e: except AnsibleConnectionFailure as e:
if check_rc: if check_rc:
raise raise
@ -142,35 +218,106 @@ class Cliconf(CliconfBase):
try: try:
out = json.loads(out) out = json.loads(out)
except ValueError: except ValueError:
out = str(out).strip() out = to_text(out, errors='surrogate_or_strict').strip()
responses.append(out) responses.append(out)
return responses return responses
def load_config(self, commands, commit=False, replace=False): def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, replace='line'):
"""Loads the config commands onto the remote device diff = {}
""" device_operations = self.get_device_operations()
session = 'ansible_%s' % int(time.time()) option_values = self.get_option_values()
result = {'session': session}
self.get('configure session %s' % session) if candidate is None and device_operations['supports_generate_diff']:
if replace: raise ValueError("candidate configuration is required to generate diff")
self.get('rollback clean-config')
try: if match not in option_values['diff_match']:
self.run_commands(commands) raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match'])))
except AnsibleConnectionFailure:
self.close_session(session) if replace not in option_values['diff_replace']:
raise raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, ', '.join(option_values['diff_replace'])))
# prepare candidate configuration
candidate_obj = NetworkConfig(indent=3)
candidate_obj.load(candidate)
if running and match != 'none' and 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=match, replace=replace)
out = self.get('show session-config diffs') else:
if out: configdiffobjs = candidate_obj.items
result['diff'] = out.strip()
configdiff = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
diff['config_diff'] = configdiff if configdiffobjs else {}
return diff
@property
def supports_sessions(self):
use_session = self.get_option('eos_use_sessions')
try:
use_session = int(use_session)
except ValueError:
pass
if commit: if not bool(use_session):
self.get('commit') self._session_support = False
else: else:
self.close_session(session) if self._session_support:
return self._session_support
response = self.get('show configuration sessions')
self._session_support = 'error' not in response
return self._session_support
return result def get_device_info(self):
device_info = {}
device_info['network_os'] = 'eos'
reply = self.get('show version | json')
data = json.loads(reply)
device_info['network_os_version'] = data['version']
device_info['network_os_model'] = data['modelName']
reply = self.get('show hostname | json')
data = json.loads(reply)
device_info['network_os_hostname'] = data['hostname']
return device_info
def get_device_operations(self):
return {
'supports_diff_replace': True,
'supports_commit': True if self.supports_sessions else False,
'supports_rollback': True if self.supports_sessions else False,
'supports_defaults': False,
'supports_onbox_diff': True if self.supports_sessions else False,
'supports_commit_comment': False,
'supports_multiline_delimiter': False,
'support_diff_match': True,
'support_diff_ignore_lines': True,
'supports_generate_diff': True,
'supports_replace': True if self.supports_sessions else False
}
def get_option_values(self):
return {
'format': ['text', 'json'],
'diff_match': ['line', 'strict', 'exact', 'none'],
'diff_replace': ['line', 'block', 'config'],
'output': ['text', 'json']
}
def get_capabilities(self):
result = {}
result['rpc'] = self.get_base_rpc()
result['device_info'] = self.get_device_info()
result['network_api'] = self.network_api
result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations()
result.update(self.get_option_values())
return json.dumps(result)

@ -26,6 +26,7 @@ import json
from itertools import chain from itertools import chain
from ansible.errors import AnsibleConnectionFailure
from ansible.module_utils._text import to_text from ansible.module_utils._text import to_text
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible.module_utils.network.common.config import NetworkConfig, dumps from ansible.module_utils.network.common.config import NetworkConfig, dumps
@ -41,7 +42,7 @@ class Cliconf(CliconfBase):
return self.invalid_params("fetching configuration from %s is not supported" % source) return self.invalid_params("fetching configuration from %s is not supported" % source)
if format: if format:
raise ValueError("'format' value %s is not supported on ios" % format) raise ValueError("'format' value %s is not supported for get_config" % format)
if not filter: if not filter:
filter = [] filter = []
@ -94,14 +95,14 @@ class Cliconf(CliconfBase):
device_operations = self.get_device_operations() device_operations = self.get_device_operations()
option_values = self.get_option_values() option_values = self.get_option_values()
if candidate is None and not device_operations['supports_onbox_diff']: if candidate is None and device_operations['supports_generate_diff']:
raise ValueError("candidate configuration is required to generate diff") raise ValueError("candidate configuration is required to generate diff")
if match not in option_values['diff_match']: if match not in option_values['diff_match']:
raise ValueError("'match' value %s in invalid, valid values are %s" % (match, option_values['diff_match'])) raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match'])))
if replace not in option_values['diff_replace']: if replace not in option_values['diff_replace']:
raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, option_values['diff_replace'])) raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, ', '.join(option_values['diff_replace'])))
# prepare candidate configuration # prepare candidate configuration
candidate_obj = NetworkConfig(indent=1) candidate_obj = NetworkConfig(indent=1)
@ -124,11 +125,13 @@ class Cliconf(CliconfBase):
banners = self._diff_banners(want_banners, have_banners) banners = self._diff_banners(want_banners, have_banners)
diff['banner_diff'] = banners if banners else {} diff['banner_diff'] = banners if banners else {}
return json.dumps(diff) return diff
@enable_mode @enable_mode
def edit_config(self, candidate=None, commit=True, replace=False, comment=None): def edit_config(self, candidate=None, commit=True, replace=False, comment=None):
resp = {} resp = {}
operations = self.get_device_operations()
if not candidate: if not candidate:
raise ValueError("must provide a candidate config to load") raise ValueError("must provide a candidate config to load")
@ -138,9 +141,12 @@ class Cliconf(CliconfBase):
if replace not in (True, False): if replace not in (True, False):
raise ValueError("'replace' must be a bool, got %s" % replace) raise ValueError("'replace' must be a bool, got %s" % replace)
if comment and not operations['supports_commit_comment']:
raise ValueError("commit comment is not supported")
operations = self.get_device_operations() operations = self.get_device_operations()
if replace and not operations['supports_replace']: if replace and not operations['supports_replace']:
raise ValueError("configuration replace is not supported on ios") raise ValueError("configuration replace is not supported")
results = [] results = []
if commit: if commit:
@ -153,6 +159,8 @@ class Cliconf(CliconfBase):
results.append(self.send_command(**line)) results.append(self.send_command(**line))
results.append(self.send_command('end')) results.append(self.send_command('end'))
else:
raise ValueError('check mode is not supported')
resp['response'] = results[1:-1] resp['response'] = results[1:-1]
return resp return resp
@ -161,7 +169,7 @@ class Cliconf(CliconfBase):
if not command: if not command:
raise ValueError('must provide value of command to execute') raise ValueError('must provide value of command to execute')
if output: if output:
raise ValueError("'output' value %s is not supported on ios" % output) raise ValueError("'output' value %s is not supported for get" % output)
return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly) return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly)
@ -247,7 +255,10 @@ class Cliconf(CliconfBase):
return resp return resp
def run_commands(self, commands): def run_commands(self, commands=None, check_rc=True):
if commands is None:
raise ValueError("'commands' value is required")
responses = list() responses = list()
for cmd in to_list(commands): for cmd in to_list(commands):
if not isinstance(cmd, collections.Mapping): if not isinstance(cmd, collections.Mapping):
@ -255,9 +266,17 @@ class Cliconf(CliconfBase):
output = cmd.pop('output', None) output = cmd.pop('output', None)
if output: if output:
raise ValueError("'output' value %s is not supported on ios" % output) raise ValueError("'output' value %s is not supported for run_commands" % output)
try:
out = self.send_command(**cmd)
except AnsibleConnectionFailure as e:
if check_rc:
raise
out = getattr(e, 'err', e)
responses.append(out)
responses.append(self.send_command(**cmd))
return responses return responses
def _extract_banners(self, config): def _extract_banners(self, config):

@ -58,7 +58,7 @@ class Cliconf(CliconfBase):
if format: if format:
option_values = self.get_option_values() option_values = self.get_option_values()
if format not in option_values['format']: if format not in option_values['format']:
raise ValueError("'format' value %s is invalid. Valid values of format are %s" % (format, ','.join(option_values['format']))) raise ValueError("'format' value %s is invalid. Valid values of format are %s" % (format, ', '.join(option_values['format'])))
if format == 'text': if format == 'text':
out = self.send_command('show configuration') out = self.send_command('show configuration')
@ -79,7 +79,7 @@ class Cliconf(CliconfBase):
operations = self.get_device_operations() operations = self.get_device_operations()
if replace and not operations['supports_replace']: if replace and not operations['supports_replace']:
raise ValueError("configuration replace is not supported on vyos") raise ValueError("configuration replace is not supported")
results = [] results = []
@ -110,14 +110,14 @@ class Cliconf(CliconfBase):
resp['diff'] = diff_config resp['diff'] = diff_config
resp['response'] = results[1:-1] resp['response'] = results[1:-1]
return json.dumps(resp) return resp
def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None): def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None):
if not command: if not command:
raise ValueError('must provide value of command to execute') raise ValueError('must provide value of command to execute')
if output: if output:
raise ValueError("'output' value %s is not supported on vyos" % output) raise ValueError("'output' value %s is not supported for get" % output)
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly)
@ -136,20 +136,20 @@ class Cliconf(CliconfBase):
device_operations = self.get_device_operations() device_operations = self.get_device_operations()
option_values = self.get_option_values() option_values = self.get_option_values()
if candidate is None and not device_operations['supports_onbox_diff']: if candidate is None and device_operations['supports_generate_diff']:
raise ValueError("candidate configuration is required to generate diff") raise ValueError("candidate configuration is required to generate diff")
if match not in option_values['diff_match']: if match not in option_values['diff_match']:
raise ValueError("'match' value %s in invalid, valid values are %s" % (match, option_values['diff_match'])) raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match'])))
if replace: if replace:
raise ValueError("'replace' in diff is not supported on vyos") raise ValueError("'replace' in diff is not supported")
if diff_ignore_lines: if diff_ignore_lines:
raise ValueError("'diff_ignore_lines' in diff is not supported on vyos") raise ValueError("'diff_ignore_lines' in diff is not supported")
if path: if path:
raise ValueError("'path' in diff is not supported on vyos") raise ValueError("'path' in diff is not supported")
set_format = candidate.startswith('set') or candidate.startswith('delete') set_format = candidate.startswith('set') or candidate.startswith('delete')
candidate_obj = NetworkConfig(indent=4, contents=candidate) candidate_obj = NetworkConfig(indent=4, contents=candidate)
@ -171,7 +171,7 @@ class Cliconf(CliconfBase):
if match == 'none': if match == 'none':
diff['config_diff'] = list(candidate_commands) diff['config_diff'] = list(candidate_commands)
return json.dumps(diff) return diff
running_commands = [str(c).replace("'", '') for c in running.splitlines()] running_commands = [str(c).replace("'", '') for c in running.splitlines()]
@ -198,9 +198,12 @@ class Cliconf(CliconfBase):
visited.add(line) visited.add(line)
diff['config_diff'] = list(updates) diff['config_diff'] = list(updates)
return json.dumps(diff) return diff
def run_commands(self, commands=None, check_rc=True):
if commands is None:
raise ValueError("'commands' value is required")
def run_commands(self, commands):
responses = list() responses = list()
for cmd in to_list(commands): for cmd in to_list(commands):
if not isinstance(cmd, collections.Mapping): if not isinstance(cmd, collections.Mapping):
@ -208,9 +211,17 @@ class Cliconf(CliconfBase):
output = cmd.pop('output', None) output = cmd.pop('output', None)
if output: if output:
raise ValueError("'output' value %s is not supported on vyos" % output) raise ValueError("'output' value %s is not supported for run_commands" % output)
try:
out = self.send_command(**cmd)
except AnsibleConnectionFailure as e:
if check_rc:
raise
out = getattr(e, 'err', e)
responses.append(out)
responses.append(self.send_command(**cmd))
return responses return responses
def get_device_operations(self): def get_device_operations(self):
@ -219,7 +230,7 @@ class Cliconf(CliconfBase):
'supports_commit': True, 'supports_commit': True,
'supports_rollback': True, 'supports_rollback': True,
'supports_defaults': False, 'supports_defaults': False,
'supports_onbox_diff': False, 'supports_onbox_diff': True,
'supports_commit_comment': True, 'supports_commit_comment': True,
'supports_multiline_delimiter': False, 'supports_multiline_delimiter': False,
'support_diff_match': True, 'support_diff_match': True,

@ -198,6 +198,7 @@ class Connection(NetworkConnectionBase):
self._history = list() self._history = list()
self._terminal = None self._terminal = None
self.cliconf = None
self.paramiko_conn = None self.paramiko_conn = None
if self._play_context.verbosity > 3: if self._play_context.verbosity > 3:
@ -258,7 +259,6 @@ class Connection(NetworkConnectionBase):
self.reset_history() self.reset_history()
self.disable_response_logging() self.disable_response_logging()
return messages return messages
def _connect(self): def _connect(self):
@ -291,10 +291,11 @@ class Connection(NetworkConnectionBase):
display.vvvv('loaded terminal plugin for network_os %s' % self._network_os, host=host) display.vvvv('loaded terminal plugin for network_os %s' % self._network_os, host=host)
cliconf = cliconf_loader.get(self._network_os, self) self.cliconf = cliconf_loader.get(self._network_os, self)
if cliconf: if self.cliconf:
display.vvvv('loaded cliconf plugin for network_os %s' % self._network_os, host=host) display.vvvv('loaded cliconf plugin for network_os %s' % self._network_os, host=host)
self._implementation_plugins.append(cliconf) self._implementation_plugins.append(self.cliconf)
self.cliconf.set_options()
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)

@ -8,8 +8,8 @@
parents: interface Loopback911 parents: interface Loopback911
become: yes become: yes
check_mode: 1 check_mode: 1
environment: vars:
ANSIBLE_EOS_USE_SESSIONS: 1 ansible_eos_use_sessions: 1
register: result register: result
ignore_errors: yes ignore_errors: yes
@ -46,8 +46,8 @@
parents: interface Loopback911 parents: interface Loopback911
become: yes become: yes
check_mode: 1 check_mode: 1
environment: vars:
ANSIBLE_EOS_USE_SESSIONS: 0 ansible_eos_use_sessions: 0
register: result register: result
ignore_errors: yes ignore_errors: yes
@ -63,8 +63,8 @@
become: yes become: yes
check_mode: yes check_mode: yes
register: result register: result
environment: vars:
ANSIBLE_EOS_USE_SESSIONS: 0 ansible_eos_use_sessions: 0
- assert: - assert:
that: that:

@ -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 ansible.compat.tests.mock import patch from ansible.compat.tests.mock import patch, MagicMock
from ansible.modules.network.eos import eos_config from ansible.modules.network.eos import eos_config
from ansible.plugins.cliconf.ios import Cliconf
from units.modules.utils import set_module_args from units.modules.utils import set_module_args
from .eos_module import TestEosModule, load_fixture from .eos_module import TestEosModule, load_fixture
@ -34,62 +35,78 @@ class TestEosConfigModule(TestEosModule):
self.mock_get_config = patch('ansible.modules.network.eos.eos_config.get_config') self.mock_get_config = patch('ansible.modules.network.eos.eos_config.get_config')
self.get_config = self.mock_get_config.start() self.get_config = self.mock_get_config.start()
self.mock_get_connection = patch('ansible.modules.network.eos.eos_config.get_connection')
self.get_connection = self.mock_get_connection.start()
self.mock_load_config = patch('ansible.modules.network.eos.eos_config.load_config') self.mock_load_config = patch('ansible.modules.network.eos.eos_config.load_config')
self.load_config = self.mock_load_config.start() self.load_config = self.mock_load_config.start()
self.mock_run_commands = patch('ansible.modules.network.eos.eos_config.run_commands') self.mock_run_commands = patch('ansible.modules.network.eos.eos_config.run_commands')
self.run_commands = self.mock_run_commands.start() self.run_commands = self.mock_run_commands.start()
self.conn = self.get_connection()
self.conn.edit_config = MagicMock()
self.cliconf_obj = Cliconf(MagicMock())
self.running_config = load_fixture('eos_config_config.cfg')
def tearDown(self): def tearDown(self):
super(TestEosConfigModule, self).tearDown() super(TestEosConfigModule, self).tearDown()
self.mock_get_config.stop() self.mock_get_config.stop()
self.mock_load_config.stop() self.mock_load_config.stop()
self.mock_get_connection.stop()
def load_fixtures(self, commands=None, transport='cli'): def load_fixtures(self, commands=None, transport='cli'):
self.get_config.return_value = load_fixture('eos_config_config.cfg') self.get_config.return_value = load_fixture('eos_config_config.cfg')
self.load_config.return_value = dict(diff=None, session='session') self.load_config.return_value = dict(diff=None, session='session')
def test_eos_config_no_change(self): def test_eos_config_no_change(self):
args = dict(lines=['hostname localhost']) lines = ['hostname localhost']
config = '\n'.join(lines)
args = dict(lines=lines)
set_module_args(args) set_module_args(args)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(config, config))
result = self.execute_module() result = self.execute_module()
def test_eos_config_src(self): def test_eos_config_src(self):
args = dict(src=load_fixture('eos_config_candidate.cfg')) src = load_fixture('eos_config_candidate.cfg')
args = dict(src=src)
set_module_args(args) set_module_args(args)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, self.running_config))
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
config = ['hostname switch01', 'interface Ethernet1', config = ['hostname switch01', 'interface Ethernet1',
'description test interface', 'no shutdown', 'ip routing'] 'description test interface', 'no shutdown', 'ip routing']
self.assertEqual(sorted(config), sorted(result['commands']), result['commands']) self.assertEqual(sorted(config), sorted(result['commands']), result['commands'])
def test_eos_config_lines(self): def test_eos_config_lines(self):
args = dict(lines=['hostname switch01', 'ip domain-name eng.ansible.com']) lines = ['hostname switch01', 'ip domain-name eng.ansible.com']
args = dict(lines=lines)
set_module_args(args) set_module_args(args)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
config = ['hostname switch01'] config = ['hostname switch01']
self.assertEqual(sorted(config), sorted(result['commands']), result['commands']) self.assertEqual(sorted(config), sorted(result['commands']), result['commands'])
def test_eos_config_before(self): def test_eos_config_before(self):
args = dict(lines=['hostname switch01', 'ip domain-name eng.ansible.com'], lines = ['hostname switch01', 'ip domain-name eng.ansible.com']
before=['before command']) before = ['before command']
args = dict(lines=lines,
before=before)
set_module_args(args) set_module_args(args)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
config = ['before command', 'hostname switch01'] config = ['before command', 'hostname switch01']
self.assertEqual(sorted(config), sorted(result['commands']), result['commands']) self.assertEqual(sorted(config), sorted(result['commands']), result['commands'])
self.assertEqual('before command', result['commands'][0]) self.assertEqual('before command', result['commands'][0])
def test_eos_config_after(self): def test_eos_config_after(self):
args = dict(lines=['hostname switch01', 'ip domain-name eng.ansible.com'], lines = ['hostname switch01', 'ip domain-name eng.ansible.com']
args = dict(lines=lines,
after=['after command']) after=['after command'])
set_module_args(args) set_module_args(args)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
config = ['after command', 'hostname switch01'] config = ['after command', 'hostname switch01']
@ -97,8 +114,12 @@ class TestEosConfigModule(TestEosModule):
self.assertEqual('after command', result['commands'][-1]) self.assertEqual('after command', result['commands'][-1])
def test_eos_config_parents(self): def test_eos_config_parents(self):
args = dict(lines=['ip address 1.2.3.4/5', 'no shutdown'], parents=['interface Ethernet10']) lines = ['ip address 1.2.3.4/5', 'no shutdown']
parents = ['interface Ethernet10']
args = dict(lines=lines, parents=parents)
candidate = parents + lines
set_module_args(args) set_module_args(args)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(candidate), self.running_config))
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
config = ['interface Ethernet10', 'ip address 1.2.3.4/5', 'no shutdown'] config = ['interface Ethernet10', 'ip address 1.2.3.4/5', 'no shutdown']

Loading…
Cancel
Save