nxos cliconf plugin refactor (#43203)

* nxos cliconf plugin refactor

Fixes #39056

*  Refactor nxos cliconf plugin as per new api definition
*  Minor changes in ios, eos, vyos cliconf plugin
*  Change nxos httpapi plugin edit_config method to be in sync with
   nxos cliconf edit_config

* Fix CI failure

* Fix unit test failure and review comment
pull/42410/head
Ganesh Nalawade 6 years ago committed by GitHub
parent e215f842ba
commit af3f510316
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -162,9 +162,10 @@ class Cli:
return response return response
def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, replace='line'): def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
conn = self._get_connection() conn = self._get_connection()
return conn.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=diff_ignore_lines, path=path, replace=replace) return conn.get_diff(candidate=candidate, running=running, diff_match=diff_match, diff_ignore_lines=diff_ignore_lines, path=path,
diff_replace=diff_replace)
class Eapi: class Eapi:
@ -361,17 +362,17 @@ class Eapi:
return result return result
# get_diff added here to support connection=local and transport=eapi scenario # 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'): def get_diff(self, candidate, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
diff = {} diff = {}
# prepare candidate configuration # prepare candidate configuration
candidate_obj = NetworkConfig(indent=3) candidate_obj = NetworkConfig(indent=3)
candidate_obj.load(candidate) candidate_obj.load(candidate)
if running and match != 'none' and replace != 'config': if running and diff_match != 'none' and diff_replace != 'config':
# running configuration # running configuration
running_obj = NetworkConfig(indent=3, contents=running, ignore_lines=diff_ignore_lines) running_obj = NetworkConfig(indent=3, contents=running, ignore_lines=diff_ignore_lines)
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=match, replace=replace) configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
else: else:
configdiffobjs = candidate_obj.items configdiffobjs = candidate_obj.items
@ -424,6 +425,6 @@ def load_config(module, config, commit=False, replace=False):
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'): def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
conn = self.get_connection() conn = self.get_connection()
return conn.get_diff(candidate=candidate, running=running, match=match, diff_ignore_lines=diff_ignore_lines, path=path, replace=replace) return conn.get_diff(candidate=candidate, running=running, diff_match=diff_match, diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)

@ -36,6 +36,7 @@ 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, ComplexList from ansible.module_utils.network.common.utils import to_list, ComplexList
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.six import iteritems, string_types from ansible.module_utils.six import iteritems, string_types
from ansible.module_utils.urls import fetch_url from ansible.module_utils.urls import fetch_url
@ -138,7 +139,7 @@ class Cli:
return self._device_configs[cmd] return self._device_configs[cmd]
except KeyError: except KeyError:
connection = self._get_connection() connection = self._get_connection()
out = connection.get_config(flags=flags) out = connection.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
@ -153,37 +154,42 @@ class Cli:
except ConnectionError as exc: except ConnectionError as exc:
self._module.fail_json(msg=to_text(exc)) self._module.fail_json(msg=to_text(exc))
def load_config(self, config, return_error=False, opts=None): def load_config(self, config, return_error=False, opts=None, replace=None):
"""Sends configuration commands to the remote device """Sends configuration commands to the remote device
""" """
if opts is None: if opts is None:
opts = {} opts = {}
connection = self._get_connection() connection = self._get_connection()
responses = []
msgs = []
try: try:
responses = connection.edit_config(config) resp = connection.edit_config(config, replace=replace)
msg = json.loads(responses) if isinstance(resp, collections.Mapping):
resp = resp['response']
except ConnectionError as e: except ConnectionError as e:
code = getattr(e, 'code', 1) code = getattr(e, 'code', 1)
message = getattr(e, 'err', e) message = getattr(e, 'err', e)
err = to_text(message, errors='surrogate_then_replace') err = to_text(message, errors='surrogate_then_replace')
if opts.get('ignore_timeout') and code: if opts.get('ignore_timeout') and code:
msgs.append(code) responses.append(code)
return msgs return responses
elif code and 'no graceful-restart' in err: elif code and 'no graceful-restart' in err:
if 'ISSU/HA will be affected if Graceful Restart is disabled' in err: if 'ISSU/HA will be affected if Graceful Restart is disabled' in err:
msg = [''] msg = ['']
msgs.extend(msg) responses.extend(msg)
return msgs return responses
else: else:
self._module.fail_json(msg=err) self._module.fail_json(msg=err)
elif code: elif code:
self._module.fail_json(msg=err) self._module.fail_json(msg=err)
msgs.extend(msg) responses.extend(resp)
return msgs return responses
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
conn = self._get_connection()
return conn.get_diff(candidate=candidate, running=running, diff_match=diff_match, diff_ignore_lines=diff_ignore_lines, path=path,
diff_replace=diff_replace)
def get_capabilities(self): def get_capabilities(self):
"""Returns platform info of the remove device """Returns platform info of the remove device
@ -371,10 +377,14 @@ class Nxapi:
return responses return responses
def load_config(self, commands, return_error=False, opts=None): def load_config(self, commands, return_error=False, opts=None, replace=None):
"""Sends the ordered set of commands to the device """Sends the ordered set of commands to the device
""" """
if replace:
commands = 'config replace {0}'.format(replace)
commands = to_list(commands) commands = to_list(commands)
msg = self.send_request(commands, output='config', check_status=True, msg = self.send_request(commands, output='config', check_status=True,
return_error=return_error, opts=opts) return_error=return_error, opts=opts)
if return_error: if return_error:
@ -382,6 +392,24 @@ class Nxapi:
else: else:
return [] return []
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
diff = {}
# prepare candidate configuration
candidate_obj = NetworkConfig(indent=2)
candidate_obj.load(candidate)
if running and diff_match != 'none' and diff_replace != 'config':
# running configuration
running_obj = NetworkConfig(indent=2, contents=running, ignore_lines=diff_ignore_lines)
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
else:
configdiffobjs = candidate_obj.items
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
return diff
def get_device_info(self): def get_device_info(self):
device_info = {} device_info = {}
@ -460,9 +488,9 @@ def run_commands(module, commands, check_rc=True):
return conn.run_commands(to_command(module, commands), check_rc) return conn.run_commands(to_command(module, commands), check_rc)
def load_config(module, config, return_error=False, opts=None): def load_config(module, config, return_error=False, opts=None, replace=None):
conn = get_connection(module) conn = get_connection(module)
return conn.load_config(config, return_error, opts) return conn.load_config(config, return_error, opts, replace=replace)
def get_capabilities(module): def get_capabilities(module):
@ -470,6 +498,11 @@ def get_capabilities(module):
return conn.get_capabilities() return conn.get_capabilities()
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
conn = self.get_connection()
return conn.get_diff(candidate=candidate, running=running, diff_match=diff_match, diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)
def normalize_interface(name): def normalize_interface(name):
"""Return the normalized interface name """Return the normalized interface name
""" """

@ -382,7 +382,8 @@ def main():
candidate = get_candidate(module) candidate = get_candidate(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=diff_ignore_lines, path=path, replace=replace) response = connection.get_diff(candidate=candidate, running=running, diff_match=match, diff_ignore_lines=diff_ignore_lines, path=path,
diff_replace=replace)
config_diff = response['config_diff'] config_diff = response['config_diff']
if config_diff: if config_diff:

@ -420,7 +420,8 @@ 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=diff_ignore_lines, path=path, replace=replace) response = connection.get_diff(candidate=candidate, running=running, diff_match=match, diff_ignore_lines=diff_ignore_lines, path=path,
diff_replace=replace)
config_diff = response['config_diff'] config_diff = response['config_diff']
banner_diff = response['banner_diff'] banner_diff = response['banner_diff']

@ -100,7 +100,7 @@ options:
the modified lines are pushed to the device in configuration the modified lines are pushed to the device in configuration
mode. If the replace argument is set to I(block) then the entire mode. If the replace argument is set to I(block) then the entire
command block is pushed to the device in configuration mode if any command block is pushed to the device in configuration mode if any
line is not correct. I(replace config) is supported only on Nexus 9K device. line is not correct. replace I(config) is supported only on Nexus 9K device.
default: line default: line
choices: ['line', 'block', 'config'] choices: ['line', 'block', 'config']
force: force:
@ -281,7 +281,7 @@ backup_path:
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.connection import ConnectionError from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.network.common.config import NetworkConfig, dumps from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.module_utils.network.nxos.nxos import get_config, load_config, run_commands from ansible.module_utils.network.nxos.nxos import get_config, load_config, run_commands, get_connection
from ansible.module_utils.network.nxos.nxos import get_capabilities from ansible.module_utils.network.nxos.nxos import get_capabilities
from ansible.module_utils.network.nxos.nxos import nxos_argument_spec from ansible.module_utils.network.nxos.nxos import nxos_argument_spec
from ansible.module_utils.network.nxos.nxos import check_args as nxos_check_args from ansible.module_utils.network.nxos.nxos import check_args as nxos_check_args
@ -296,19 +296,21 @@ def get_running_config(module, config=None):
else: else:
flags = ['all'] flags = ['all']
contents = get_config(module, flags=flags) contents = get_config(module, flags=flags)
return NetworkConfig(indent=2, contents=contents) return contents
def get_candidate(module): def get_candidate(module):
candidate = NetworkConfig(indent=2) candidate = ''
if module.params['src']: if module.params['src']:
if module.params['replace'] != 'config': if module.params['replace'] != 'config':
candidate.load(module.params['src']) candidate = module.params['src']
if module.params['replace'] == 'config': if module.params['replace'] == 'config':
candidate.load('config replace {0}'.format(module.params['replace_src'])) candidate = 'config replace {0}'.format(module.params['replace_src'])
elif module.params['lines']: elif module.params['lines']:
candidate_obj = NetworkConfig(indent=2)
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
@ -404,7 +406,12 @@ def main():
if '9K' not in os_platform: if '9K' not in os_platform:
module.fail_json(msg='replace: config is supported only on Nexus 9K series switches') module.fail_json(msg='replace: config is supported only on Nexus 9K series switches')
if module.params['replace_src']: diff_ignore_lines = module.params['diff_ignore_lines']
path = module.params['parents']
connection = get_connection(module)
contents = None
replace_src = module.params['replace_src']
if replace_src:
if module.params['replace'] != 'config': if module.params['replace'] != 'config':
module.fail_json(msg='replace: config is required with replace_src') module.fail_json(msg='replace: config is required with replace_src')
@ -414,48 +421,51 @@ def main():
if module.params['backup']: if module.params['backup']:
result['__backup__'] = contents result['__backup__'] = contents
if any((module.params['src'], module.params['lines'], module.params['replace_src'])): if any((module.params['src'], module.params['lines'], replace_src)):
match = module.params['match'] match = module.params['match']
replace = module.params['replace'] replace = module.params['replace']
commit = not module.check_mode
candidate = get_candidate(module) candidate = get_candidate(module)
running = get_running_config(module, contents)
if replace_src:
commands = candidate.split('\n')
result['commands'] = result['updates'] = commands
if commit:
load_config(module, commands, replace=replace_src)
if match != 'none' and replace != 'config': result['changed'] = True
config = get_running_config(module, config)
path = module.params['parents']
configobjs = candidate.difference(config, match=match, replace=replace, path=path)
else: else:
configobjs = candidate.items response = connection.get_diff(candidate=candidate, running=running, diff_match=match, diff_ignore_lines=diff_ignore_lines, path=path,
diff_replace=replace)
config_diff = response['config_diff']
if config_diff:
commands = config_diff.split('\n')
if configobjs: if module.params['before']:
commands = dumps(configobjs, 'commands').split('\n') commands[:0] = module.params['before']
if module.params['before']: if module.params['after']:
commands[:0] = module.params['before'] commands.extend(module.params['after'])
if module.params['after']: result['commands'] = commands
commands.extend(module.params['after']) result['updates'] = commands
result['commands'] = commands if commit:
result['updates'] = commands load_config(module, commands, replace=replace_src)
if not module.check_mode: result['changed'] = True
load_config(module, commands)
result['changed'] = True
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 = execute_show_commands(module, ['show running-config', 'show startup-config']) output = execute_show_commands(module, ['show running-config', 'show startup-config'])
running_config = NetworkConfig(indent=1, contents=output[0], ignore_lines=diff_ignore_lines) running_config = NetworkConfig(indent=2, 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=2, 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)
@ -470,7 +480,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=2, 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:
@ -484,14 +494,13 @@ def main():
output = execute_show_commands(module, 'show startup-config') output = execute_show_commands(module, 'show startup-config')
contents = output[0] contents = output[0]
else: else:
contents = output[0]
contents = startup_config.config_text contents = startup_config.config_text
elif module.params['diff_against'] == 'intended': elif module.params['diff_against'] == 'intended':
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=2, 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':

@ -208,7 +208,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, diff_match=module.params['match'])
commands = response.get('config_diff') commands = response.get('config_diff')
sanitize_config(commands, result) sanitize_config(commands, result)

@ -189,7 +189,7 @@ class CliconfBase(AnsiblePlugin):
pass pass
@abstractmethod @abstractmethod
def edit_config(self, candidate=None, commit=True, replace=False, diff=False, comment=None): def edit_config(self, candidate=None, commit=True, replace=None, diff=False, comment=None):
"""Loads the candidate configuration into the network device """Loads the candidate configuration into the network device
This method will load the specified candidate config into the device This method will load the specified candidate config into the device
@ -203,8 +203,10 @@ class CliconfBase(AnsiblePlugin):
:param commit: Boolean value that indicates if the device candidate :param commit: Boolean value that indicates if the device candidate
configuration should be pushed in the running configuration or discarded. configuration should be pushed in the running configuration or discarded.
:param replace: Boolean flag to indicate if running configuration should be completely :param replace: If the value is True/False it indicates if running configuration should be completely
replace by candidate configuration. replace by candidate configuration. If can also take configuration file path as value,
the file in this case should be present on the remote host in the mentioned path as a
prerequisite.
:param comment: Commit comment provided it is supported by remote host :param comment: Commit comment provided it is supported by remote host
:return: Returns a json string with contains configuration applied on remote host, the returned :return: Returns a json string with contains configuration applied on remote host, the returned
response on executing configuration commands and platform relevant data. response on executing configuration commands and platform relevant data.
@ -341,7 +343,7 @@ class CliconfBase(AnsiblePlugin):
with ssh.open_sftp() as sftp: with ssh.open_sftp() as sftp:
sftp.get(source, destination) sftp.get(source, destination)
def get_diff(self, candidate=None, running=None, match=None, diff_ignore_lines=None, path=None, replace=None): def get_diff(self, candidate=None, running=None, diff_match=None, diff_ignore_lines=None, path=None, diff_replace=None):
""" """
Generate diff between candidate and running configuration. If the Generate diff between candidate and running configuration. If the
remote host supports onbox diff capabilities ie. supports_onbox_diff in that case remote host supports onbox diff capabilities ie. supports_onbox_diff in that case
@ -350,7 +352,7 @@ class CliconfBase(AnsiblePlugin):
and running argument is optional. and running argument is optional.
:param candidate: The configuration which is expected to be present on remote host. :param candidate: The configuration which is expected to be present on remote host.
:param running: The base configuration which is used to generate diff. :param running: The base configuration which is used to generate diff.
:param match: Instructs how to match the candidate configuration with current device configuration :param diff_match: Instructs how to match the candidate configuration with current device configuration
Valid values are 'line', 'strict', 'exact', 'none'. Valid values are 'line', 'strict', 'exact', 'none'.
'line' - commands are matched line by line 'line' - commands are matched line by line
'strict' - command lines are matched with respect to position 'strict' - command lines are matched with respect to position
@ -364,7 +366,7 @@ class CliconfBase(AnsiblePlugin):
the commands should be checked against. If the parents argument the commands should be checked against. If the parents argument
is omitted, the commands are checked against the set of top is omitted, the commands are checked against the set of top
level or global commands. level or global commands.
:param replace: Instructs on the way to perform the configuration on the device. :param diff_replace: Instructs on the way to perform the configuration on the device.
If the replace argument is set to I(line) then the modified lines are If the replace argument is set to I(line) then the modified lines are
pushed to the device in configuration mode. If the replace argument is pushed to the device in configuration mode. If the replace argument is
set to I(block) then the entire command block is pushed to the device in set to I(block) then the entire command block is pushed to the device in
@ -396,3 +398,20 @@ class CliconfBase(AnsiblePlugin):
:return: List of returned response :return: List of returned response
""" """
pass pass
def check_edit_config_capabiltiy(self, operations, candidate=None, commit=True, replace=None, comment=None):
if not candidate and not replace:
raise ValueError("must provide a candidate or replace to load configuration")
if commit not in (True, False):
raise ValueError("'commit' must be a bool, got %s" % commit)
if replace and not operations['supports_replace']:
raise ValueError("configuration replace is not supported")
if comment and not operations.get('supports_commit_comment', False):
raise ValueError("commit comment is not supported")
if replace and not operations.get('supports_replace', False):
raise ValueError("configuration replace is not supported")

@ -59,23 +59,6 @@ class Cliconf(CliconfBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(Cliconf, self).__init__(*args, **kwargs) super(Cliconf, self).__init__(*args, **kwargs)
self._session_support = None 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): def send_command(self, command, **kwargs):
"""Executes a cli command and returns the results """Executes a cli command and returns the results
@ -83,10 +66,12 @@ class Cliconf(CliconfBase):
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
""" """
if self.network_api == 'network_cli': if isinstance(self._connection, NetworkCli):
resp = super(Cliconf, self).send_command(command, **kwargs) resp = super(Cliconf, self).send_command(command, **kwargs)
else: elif isinstance(self._connection, HttpApi):
resp = self._connection.send_request(command, **kwargs) resp = self._connection.send_request(command, **kwargs)
else:
raise ValueError("Invalid connection type")
return resp return resp
@enable_mode @enable_mode
@ -108,32 +93,19 @@ class Cliconf(CliconfBase):
return self.send_command(cmd) return self.send_command(cmd)
@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=None, comment=None):
if not candidate:
raise ValueError("must provide a candidate config to load")
if commit not in (True, False):
raise ValueError("'commit' must be a bool, got %s" % commit)
operations = self.get_device_operations() operations = self.get_device_operations()
if replace not in (True, False): self.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment)
raise ValueError("'replace' must be a bool, got %s" % replace)
if replace and not operations['supports_replace']:
raise ValueError("configuration replace is supported only with configuration session")
if comment and not operations['supports_commit_comment']:
raise ValueError("commit comment is not supported")
if (commit is False) and (not self.supports_sessions): if (commit is False) and (not self.supports_sessions):
raise ValueError('check mode is not supported without configuration session') raise ValueError('check mode is not supported without configuration session')
response = {} resp = {}
session = None session = None
if self.supports_sessions: if self.supports_sessions:
session = 'ansible_%s' % int(time.time()) session = 'ansible_%s' % int(time.time())
response.update({'session': session}) resp.update({'session': session})
self.send_command('configure session %s' % session) self.send_command('configure session %s' % session)
if replace: if replace:
self.send_command('rollback clean-config') self.send_command('rollback clean-config')
@ -141,6 +113,7 @@ class Cliconf(CliconfBase):
self.send_command('configure') self.send_command('configure')
results = [] results = []
requests = []
multiline = False multiline = False
for line in to_list(candidate): for line in to_list(candidate):
if not isinstance(line, collections.Mapping): if not isinstance(line, collections.Mapping):
@ -160,15 +133,17 @@ class Cliconf(CliconfBase):
if cmd != 'end' and cmd[0] != '!': if cmd != 'end' and cmd[0] != '!':
try: try:
results.append(self.send_command(**line)) results.append(self.send_command(**line))
requests.append(cmd)
except AnsibleConnectionFailure as e: except AnsibleConnectionFailure as e:
self.discard_changes(session) self.discard_changes(session)
raise AnsibleConnectionFailure(e.message) raise AnsibleConnectionFailure(e.message)
response['response'] = results resp['request'] = requests
resp['response'] = results
if self.supports_sessions: if self.supports_sessions:
out = self.send_command('show session-config diffs') out = self.send_command('show session-config diffs')
if out: if out:
response['diff'] = out.strip() resp['diff'] = out.strip()
if commit: if commit:
self.commit() self.commit()
@ -176,7 +151,7 @@ class Cliconf(CliconfBase):
self.discard_changes(session) self.discard_changes(session)
else: else:
self.send_command('end') self.send_command('end')
return response return resp
def get(self, command, prompt=None, answer=None, sendonly=False, output=None): def get(self, command, prompt=None, answer=None, sendonly=False, output=None):
if output: if output:
@ -224,7 +199,7 @@ class Cliconf(CliconfBase):
responses.append(out) responses.append(out)
return responses return responses
def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, replace='line'): def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
diff = {} diff = {}
device_operations = self.get_device_operations() device_operations = self.get_device_operations()
option_values = self.get_option_values() option_values = self.get_option_values()
@ -232,26 +207,25 @@ class Cliconf(CliconfBase):
if candidate is None and device_operations['supports_generate_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 diff_match not in option_values['diff_match']:
raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match']))) raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
if replace not in option_values['diff_replace']: if diff_replace not in option_values['diff_replace']:
raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, ', '.join(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 # prepare candidate configuration
candidate_obj = NetworkConfig(indent=3) candidate_obj = NetworkConfig(indent=3)
candidate_obj.load(candidate) candidate_obj.load(candidate)
if running and match != 'none' and replace != 'config': if running and diff_match != 'none' and diff_replace != 'config':
# running configuration # running configuration
running_obj = NetworkConfig(indent=3, contents=running, ignore_lines=diff_ignore_lines) running_obj = NetworkConfig(indent=3, contents=running, ignore_lines=diff_ignore_lines)
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=match, replace=replace) configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
else: else:
configdiffobjs = candidate_obj.items configdiffobjs = candidate_obj.items
configdiff = dumps(configdiffobjs, 'commands') if configdiffobjs else '' diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
diff['config_diff'] = configdiff if configdiffobjs else {}
return diff return diff
@property @property
@ -317,8 +291,25 @@ class Cliconf(CliconfBase):
result = {} result = {}
result['rpc'] = self.get_base_rpc() result['rpc'] = self.get_base_rpc()
result['device_info'] = self.get_device_info() result['device_info'] = self.get_device_info()
result['network_api'] = self.network_api
result['device_info'] = self.get_device_info() result['device_info'] = self.get_device_info()
result['device_operations'] = self.get_device_operations() result['device_operations'] = self.get_device_operations()
result.update(self.get_option_values()) result.update(self.get_option_values())
if isinstance(self._connection, NetworkCli):
result['network_api'] = 'cliconf'
elif isinstance(self._connection, HttpApi):
result['network_api'] = 'eapi'
else:
raise ValueError("Invalid connection type")
return json.dumps(result) return json.dumps(result)
def _get_command_with_output(self, command, output):
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

@ -56,7 +56,7 @@ class Cliconf(CliconfBase):
return self.send_command(cmd) return self.send_command(cmd)
def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, replace='line'): def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
""" """
Generate diff between candidate and running configuration. If the Generate diff between candidate and running configuration. If the
remote host supports onbox diff capabilities ie. supports_onbox_diff in that case remote host supports onbox diff capabilities ie. supports_onbox_diff in that case
@ -65,7 +65,7 @@ class Cliconf(CliconfBase):
and running argument is optional. and running argument is optional.
:param candidate: The configuration which is expected to be present on remote host. :param candidate: The configuration which is expected to be present on remote host.
:param running: The base configuration which is used to generate diff. :param running: The base configuration which is used to generate diff.
:param match: Instructs how to match the candidate configuration with current device configuration :param diff_match: Instructs how to match the candidate configuration with current device configuration
Valid values are 'line', 'strict', 'exact', 'none'. Valid values are 'line', 'strict', 'exact', 'none'.
'line' - commands are matched line by line 'line' - commands are matched line by line
'strict' - command lines are matched with respect to position 'strict' - command lines are matched with respect to position
@ -79,7 +79,7 @@ class Cliconf(CliconfBase):
the commands should be checked against. If the parents argument the commands should be checked against. If the parents argument
is omitted, the commands are checked against the set of top is omitted, the commands are checked against the set of top
level or global commands. level or global commands.
:param replace: Instructs on the way to perform the configuration on the device. :param diff_replace: Instructs on the way to perform the configuration on the device.
If the replace argument is set to I(line) then the modified lines are If the replace argument is set to I(line) then the modified lines are
pushed to the device in configuration mode. If the replace argument is pushed to the device in configuration mode. If the replace argument is
set to I(block) then the entire command block is pushed to the device in set to I(block) then the entire command block is pushed to the device in
@ -87,7 +87,7 @@ class Cliconf(CliconfBase):
:return: Configuration diff in json format. :return: Configuration diff in json format.
{ {
'config_diff': '', 'config_diff': '',
'banner_diff': '' 'banner_diff': {}
} }
""" """
@ -98,71 +98,57 @@ class Cliconf(CliconfBase):
if candidate is None and device_operations['supports_generate_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 diff_match not in option_values['diff_match']:
raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match']))) raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
if replace not in option_values['diff_replace']: if diff_replace not in option_values['diff_replace']:
raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, ', '.join(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 # prepare candidate configuration
candidate_obj = NetworkConfig(indent=1) candidate_obj = NetworkConfig(indent=1)
want_src, want_banners = self._extract_banners(candidate) want_src, want_banners = self._extract_banners(candidate)
candidate_obj.load(want_src) candidate_obj.load(want_src)
if running and match != 'none': if running and diff_match != 'none':
# running configuration # running configuration
have_src, have_banners = self._extract_banners(running) have_src, have_banners = self._extract_banners(running)
running_obj = NetworkConfig(indent=1, contents=have_src, ignore_lines=diff_ignore_lines) running_obj = NetworkConfig(indent=1, contents=have_src, ignore_lines=diff_ignore_lines)
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=match, replace=replace) configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
else: else:
configdiffobjs = candidate_obj.items configdiffobjs = candidate_obj.items
have_banners = {} have_banners = {}
configdiff = dumps(configdiffobjs, 'commands') if configdiffobjs else '' diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
diff['config_diff'] = configdiff if configdiffobjs else {}
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 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=None, comment=None):
resp = {} resp = {}
operations = self.get_device_operations() operations = self.get_device_operations()
self.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment)
if not candidate:
raise ValueError("must provide a candidate config to load")
if commit not in (True, False):
raise ValueError("'commit' must be a bool, got %s" % commit)
if replace not in (True, False):
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()
if replace and not operations['supports_replace']:
raise ValueError("configuration replace is not supported")
results = [] results = []
requests = []
if commit: if commit:
for line in chain(['configure terminal'], to_list(candidate)): self.send_command('configure terminal')
for line in to_list(candidate):
if not isinstance(line, collections.Mapping): if not isinstance(line, collections.Mapping):
line = {'command': line} line = {'command': line}
cmd = line['command'] cmd = line['command']
if cmd != 'end' and cmd[0] != '!': if cmd != 'end' and cmd[0] != '!':
results.append(self.send_command(**line)) results.append(self.send_command(**line))
requests.append(cmd)
results.append(self.send_command('end')) self.send_command('end')
else: else:
raise ValueError('check mode is not supported') raise ValueError('check mode is not supported')
resp['response'] = results[1:-1] resp['request'] = requests
resp['response'] = results
return 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):
@ -241,17 +227,23 @@ class Cliconf(CliconfBase):
resp = {} resp = {}
banners_obj = json.loads(candidate) banners_obj = json.loads(candidate)
results = [] results = []
requests = []
if commit: if commit:
for key, value in iteritems(banners_obj): for key, value in iteritems(banners_obj):
key += ' %s' % multiline_delimiter key += ' %s' % multiline_delimiter
for cmd in ['config terminal', key, value, multiline_delimiter, 'end']: self.send_commad('config terminal', sendonly=True)
for cmd in [key, value, multiline_delimiter]:
obj = {'command': cmd, 'sendonly': True} obj = {'command': cmd, 'sendonly': True}
results.append(self.send_command(**obj)) results.append(self.send_command(**obj))
requests.append(cmd)
self.send_commad('end', sendonly=True)
time.sleep(0.1) time.sleep(0.1)
results.append(self.send_command('\n')) results.append(self.send_command('\n'))
requests.append('\n')
resp['response'] = results[1:-1] resp['request'] = requests
resp['response'] = results
return resp return resp

@ -19,6 +19,7 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
import collections
import json import json
import re import re
@ -27,30 +28,30 @@ from itertools import chain
from ansible.errors import AnsibleConnectionFailure 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.connection import ConnectionError from ansible.module_utils.connection import ConnectionError
from ansible.module_utils.network.common.config import NetworkConfig, dumps
from ansible.module_utils.network.common.utils import to_list from ansible.module_utils.network.common.utils import to_list
from ansible.plugins.cliconf import CliconfBase 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)
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,
'newline': newline, 'prompt_retry_check': prompt_retry_check}
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): if isinstance(self._connection, NetworkCli):
resp = self._connection.send(**kwargs) resp = super(Cliconf, self).send_command(command, **kwargs)
else: elif isinstance(self._connection, HttpApi):
resp = self._connection.send_request(command, **kwargs) resp = self._connection.send_request(command, **kwargs)
else:
raise ValueError("Invalid connection type")
return resp return resp
def get_device_info(self): def get_device_info(self):
@ -101,66 +102,169 @@ class Cliconf(CliconfBase):
return device_info return device_info
def get_config(self, source='running', format='text', flags=None): 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=2)
candidate_obj.load(candidate)
if running and diff_match != 'none' and diff_replace != 'config':
# running configuration
running_obj = NetworkConfig(indent=2, contents=running, ignore_lines=diff_ignore_lines)
configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace)
else:
configdiffobjs = candidate_obj.items
diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else ''
return diff
def 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:
return self.invalid_params("fetching configuration from %s is not supported" % source)
cmd = 'show {0} '.format(lookup[source]) cmd = 'show {0} '.format(lookup[source])
if flags: if format and format is not 'text':
cmd += ' '.join(flags) cmd += '| %s ' % format
if filter:
cmd += ' '.join(to_list(filter))
cmd = cmd.strip() cmd = cmd.strip()
return self.send_command(cmd) return self.send_command(cmd)
def edit_config(self, command): def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
responses = [] resp = {}
for cmd in chain(['configure'], to_list(command), ['end']): operations = self.get_device_operations()
responses.append(self.send_command(cmd)) self.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment)
resp = responses[1:-1] results = []
return json.dumps(resp) requests = []
def get(self, command, prompt=None, answer=None, sendonly=False): if replace:
return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) candidate = 'config replace {0}'.format(replace)
def get_capabilities(self): if commit:
result = {} self.send_command('configure terminal')
result['rpc'] = self.get_base_rpc()
result['device_info'] = self.get_device_info() for line in to_list(candidate):
if isinstance(self._connection, NetworkCli): if not isinstance(line, collections.Mapping):
result['network_api'] = 'cliconf' line = {'command': line}
cmd = line['command']
if cmd != 'end':
results.append(self.send_command(**line))
requests.append(cmd)
self.send_command('end')
else: else:
result['network_api'] = 'nxapi' raise ValueError('check mode is not supported')
return json.dumps(result)
resp['request'] = requests
resp['response'] = results
return resp
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 run_commands(self, commands=None, check_rc=True):
if commands is None:
raise ValueError("'commands' value is required")
# Migrated from module_utils
def run_commands(self, commands, check_rc=True):
"""Run list of commands on remote device and return results
"""
responses = list() responses = list()
for cmd in to_list(commands):
if not isinstance(cmd, collections.Mapping):
cmd = {'command': cmd}
for item in to_list(commands): output = cmd.pop('output', None)
if item['output'] == 'json' and not item['command'].endswith('| json'): if output:
cmd = '%s | json' % item['command'] cmd['command'] = self._get_command_with_output(cmd['command'], output)
elif item['output'] == 'text' and item['command'].endswith('| json'):
cmd = item['command'].rsplit('|', 1)[0]
else:
cmd = item['command']
try: try:
out = self.get(cmd) out = self.send_command(**cmd)
except AnsibleConnectionFailure as e: except AnsibleConnectionFailure as e:
if check_rc: if check_rc:
raise raise
out = getattr(e, 'err', e) out = getattr(e, 'err', e)
try: if out is not None:
out = to_text(out, errors='surrogate_or_strict').strip() try:
except UnicodeError: out = to_text(out, errors='surrogate_or_strict').strip()
raise ConnectionError(msg=u'Failed to decode output from %s: %s' % (cmd, to_text(out))) except UnicodeError:
raise ConnectionError(msg=u'Failed to decode output from %s: %s' % (cmd, to_text(out)))
try: try:
out = json.loads(out) out = json.loads(out)
except ValueError: except ValueError:
pass out = to_text(out, errors='surrogate_or_strict').strip()
responses.append(out) responses.append(out)
return responses return responses
def get_device_operations(self):
return {
'supports_diff_replace': True,
'supports_commit': False,
'supports_rollback': False,
'supports_defaults': True,
'supports_onbox_diff': False,
'supports_commit_comment': False,
'supports_multiline_delimiter': False,
'supports_diff_match': True,
'supports_diff_ignore_lines': True,
'supports_generate_diff': True,
'supports_replace': True
}
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.update(self.get_option_values())
if isinstance(self._connection, NetworkCli):
result['network_api'] = 'cliconf'
elif isinstance(self._connection, HttpApi):
result['network_api'] = 'nxapi'
else:
raise ValueError("Invalid connection type")
return json.dumps(result)
def _get_command_with_output(self, command, output):
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
elif output == 'text' and command.endswith('| json'):
cmd = command.rsplit('|', 1)[0]
else:
cmd = command
return cmd

@ -66,29 +66,20 @@ class Cliconf(CliconfBase):
out = self.send_command('show configuration commands') out = self.send_command('show configuration commands')
return out return out
def edit_config(self, candidate=None, commit=True, replace=False, comment=None): def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
resp = {} resp = {}
if not candidate:
raise ValueError('must provide a candidate config to load')
if commit not in (True, False):
raise ValueError("'commit' must be a bool, got %s" % commit)
if replace not in (True, False):
raise ValueError("'replace' must be a bool, got %s" % replace)
operations = self.get_device_operations() operations = self.get_device_operations()
if replace and not operations['supports_replace']: self.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment)
raise ValueError("configuration replace is not supported")
results = [] results = []
requests = []
for cmd in chain(['configure'], to_list(candidate)): self.send_command('configure')
for cmd in to_list(candidate):
if not isinstance(cmd, collections.Mapping): if not isinstance(cmd, collections.Mapping):
cmd = {'command': cmd} cmd = {'command': cmd}
results.append(self.send_command(**cmd)) results.append(self.send_command(**cmd))
requests.append(cmd['command'])
out = self.get('compare') out = self.get('compare')
out = to_text(out, errors='surrogate_or_strict') out = to_text(out, errors='surrogate_or_strict')
diff_config = out if not out.startswith('No changes') else None diff_config = out if not out.startswith('No changes') else None
@ -109,7 +100,8 @@ class Cliconf(CliconfBase):
self.send_command('exit') self.send_command('exit')
resp['diff'] = diff_config resp['diff'] = diff_config
resp['response'] = results[1:-1] resp['response'] = results
resp['request'] = requests
return 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):
@ -131,7 +123,7 @@ class Cliconf(CliconfBase):
def discard_changes(self): def discard_changes(self):
self.send_command('exit discard') self.send_command('exit discard')
def get_diff(self, candidate=None, running=None, match='line', diff_ignore_lines=None, path=None, replace=None): def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace=None):
diff = {} diff = {}
device_operations = self.get_device_operations() device_operations = self.get_device_operations()
option_values = self.get_option_values() option_values = self.get_option_values()
@ -139,10 +131,10 @@ class Cliconf(CliconfBase):
if candidate is None and device_operations['supports_generate_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 diff_match not in option_values['diff_match']:
raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match']))) raise ValueError("'match' value %s in invalid, valid values are %s" % (diff_match, ', '.join(option_values['diff_match'])))
if replace: if diff_replace:
raise ValueError("'replace' in diff is not supported") raise ValueError("'replace' in diff is not supported")
if diff_ignore_lines: if diff_ignore_lines:
@ -169,7 +161,7 @@ class Cliconf(CliconfBase):
else: else:
candidate_commands = str(candidate).strip().split('\n') candidate_commands = str(candidate).strip().split('\n')
if match == 'none': if diff_match == 'none':
diff['config_diff'] = list(candidate_commands) diff['config_diff'] = list(candidate_commands)
return diff return diff

@ -72,17 +72,22 @@ class HttpApi(HttpApiBase):
return responses[0] return responses[0]
return responses return responses
# Migrated from module_utils def edit_config(self, candidate=None, commit=True, replace=None, comment=None):
def edit_config(self, command):
resp = list() resp = list()
responses = self.send_request(command, output='config')
operations = self.connection.get_device_operations()
self.connection.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment)
if replace:
candidate = 'config replace {0}'.format(replace)
responses = self.send_request(candidate, output='config')
for response in to_list(responses): for response in to_list(responses):
if response != '{}': if response != '{}':
resp.append(response) resp.append(response)
if not resp: if not resp:
resp = [''] resp = ['']
return json.dumps(resp) return resp
def run_commands(self, commands, check_rc=True): def run_commands(self, commands, check_rc=True):
"""Runs list of commands on remote device and returns results """Runs list of commands on remote device and returns results

@ -21,7 +21,7 @@ __metaclass__ = type
from ansible.compat.tests.mock import patch, MagicMock 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 ansible.plugins.cliconf.eos 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
@ -43,6 +43,10 @@ class TestEosConfigModule(TestEosModule):
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.mock_supports_sessions = patch('ansible.plugins.cliconf.eos.Cliconf.supports_sessions')
self.supports_sessions = self.mock_supports_sessions.start()
self.mock_supports_sessions.return_value = True
self.conn = self.get_connection() self.conn = self.get_connection()
self.conn.edit_config = MagicMock() self.conn.edit_config = MagicMock()
@ -54,6 +58,7 @@ class TestEosConfigModule(TestEosModule):
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() self.mock_get_connection.stop()
self.mock_supports_sessions.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')

@ -177,7 +177,7 @@ class TestIosConfigModule(TestIosModule):
module.params = {'lines': lines, 'parents': parents, 'src': None} module.params = {'lines': lines, 'parents': parents, 'src': None}
candidate_config = ios_config.get_candidate_config(module) candidate_config = ios_config.get_candidate_config(module)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, replace='block', path=parents)) self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, diff_replace='block', path=parents))
commands = parents + lines commands = parents + lines
self.execute_module(changed=True, commands=commands) self.execute_module(changed=True, commands=commands)
@ -185,7 +185,7 @@ class TestIosConfigModule(TestIosModule):
def test_ios_config_match_none(self): def test_ios_config_match_none(self):
lines = ['hostname router'] lines = ['hostname router']
set_module_args(dict(lines=lines, match='none')) set_module_args(dict(lines=lines, match='none'))
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config, match='none')) self.conn.get_diff = MagicMock(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_ios_config_match_none(self): def test_ios_config_match_none(self):
@ -196,7 +196,7 @@ class TestIosConfigModule(TestIosModule):
module = MagicMock() module = MagicMock()
module.params = {'lines': lines, 'parents': parents, 'src': None} module.params = {'lines': lines, 'parents': parents, 'src': None}
candidate_config = ios_config.get_candidate_config(module) candidate_config = ios_config.get_candidate_config(module)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, match='none', path=parents)) self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, diff_match='none', path=parents))
commands = parents + lines commands = parents + lines
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)
@ -210,7 +210,7 @@ class TestIosConfigModule(TestIosModule):
module = MagicMock() module = MagicMock()
module.params = {'lines': lines, 'parents': parents, 'src': None} module.params = {'lines': lines, 'parents': parents, 'src': None}
candidate_config = ios_config.get_candidate_config(module) candidate_config = ios_config.get_candidate_config(module)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, match='strict', path=parents)) self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, diff_match='strict', path=parents))
commands = parents + ['shutdown'] commands = parents + ['shutdown']
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)
@ -224,7 +224,7 @@ class TestIosConfigModule(TestIosModule):
module = MagicMock() module = MagicMock()
module.params = {'lines': lines, 'parents': parents, 'src': None} module.params = {'lines': lines, 'parents': parents, 'src': None}
candidate_config = ios_config.get_candidate_config(module) candidate_config = ios_config.get_candidate_config(module)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, match='exact', path=parents)) self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate_config, self.running_config, diff_match='exact', path=parents))
commands = parents + lines commands = parents + lines
self.execute_module(changed=True, commands=commands, sort=False) self.execute_module(changed=True, commands=commands, sort=False)

@ -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.nxos import nxos_config from ansible.modules.network.nxos import nxos_config
from ansible.plugins.cliconf.nxos import Cliconf
from .nxos_module import TestNxosModule, load_fixture, set_module_args from .nxos_module import TestNxosModule, load_fixture, set_module_args
@ -44,23 +45,41 @@ class TestNxosConfigModule(TestNxosModule):
self.mock_save_config = patch('ansible.modules.network.nxos.nxos_config.save_config') self.mock_save_config = patch('ansible.modules.network.nxos.nxos_config.save_config')
self.save_config = self.mock_save_config.start() self.save_config = self.mock_save_config.start()
self.mock_get_connection = patch('ansible.modules.network.nxos.nxos_config.get_connection')
self.get_connection = self.mock_get_connection.start()
self.conn = self.get_connection()
self.conn.edit_config = MagicMock()
self.mock_run_commands = patch('ansible.modules.network.nxos.nxos_config.run_commands')
self.run_commands = self.mock_run_commands.start()
self.cliconf_obj = Cliconf(MagicMock())
self.running_config = load_fixture('nxos_config', 'config.cfg')
def tearDown(self): def tearDown(self):
super(TestNxosConfigModule, self).tearDown() super(TestNxosConfigModule, 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_capabilities.stop() self.mock_get_capabilities.stop()
self.mock_run_commands.stop()
self.mock_get_connection.stop()
def load_fixtures(self, commands=None, device=''): def load_fixtures(self, commands=None, device=''):
self.get_config.return_value = load_fixture('nxos_config', 'config.cfg') self.get_config.return_value = load_fixture('nxos_config', 'config.cfg')
self.load_config.return_value = None self.load_config.return_value = None
def test_nxos_config_no_change(self): def test_nxos_config_no_change(self):
args = dict(lines=['hostname localhost']) lines = ['hostname localhost']
args = dict(lines=lines)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
set_module_args(args) set_module_args(args)
result = self.execute_module() result = self.execute_module()
def test_nxos_config_src(self): def test_nxos_config_src(self):
args = dict(src=load_fixture('nxos_config', 'candidate.cfg')) src = load_fixture('nxos_config', 'candidate.cfg')
args = dict(src=src)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, self.running_config))
set_module_args(args) set_module_args(args)
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
@ -71,11 +90,14 @@ class TestNxosConfigModule(TestNxosModule):
def test_nxos_config_replace_src(self): def test_nxos_config_replace_src(self):
set_module_args(dict(replace_src='config.txt', replace='config')) set_module_args(dict(replace_src='config.txt', replace='config'))
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(self.running_config, self.running_config, diff_replace='config'))
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
self.assertEqual(result['commands'], ['config replace config.txt']) self.assertEqual(result['commands'], ['config replace config.txt'])
def test_nxos_config_lines(self): def test_nxos_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)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
set_module_args(args) set_module_args(args)
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
@ -84,9 +106,10 @@ class TestNxosConfigModule(TestNxosModule):
self.assertEqual(sorted(config), sorted(result['commands']), result['commands']) self.assertEqual(sorted(config), sorted(result['commands']), result['commands'])
def test_nxos_config_before(self): def test_nxos_config_before(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,
before=['before command']) before=['before command'])
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
set_module_args(args) set_module_args(args)
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
@ -96,9 +119,11 @@ class TestNxosConfigModule(TestNxosModule):
self.assertEqual('before command', result['commands'][0]) self.assertEqual('before command', result['commands'][0])
def test_nxos_config_after(self): def test_nxos_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'])
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config))
set_module_args(args) set_module_args(args)
result = self.execute_module(changed=True) result = self.execute_module(changed=True)
@ -108,7 +133,10 @@ class TestNxosConfigModule(TestNxosModule):
self.assertEqual('after command', result['commands'][-1]) self.assertEqual('after command', result['commands'][-1])
def test_nxos_config_parents(self): def test_nxos_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)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(parents + lines), self.running_config, path=parents))
set_module_args(args) set_module_args(args)
result = self.execute_module(changed=True) result = self.execute_module(changed=True)

@ -113,5 +113,5 @@ class TestVyosConfigModule(TestVyosModule):
'set system interfaces ethernet eth0 description test string'] 'set system interfaces ethernet eth0 description test string']
set_module_args(dict(lines=lines, match='none')) set_module_args(dict(lines=lines, match='none'))
candidate = '\n'.join(lines) candidate = '\n'.join(lines)
self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate, None, match='none')) self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate, None, diff_match='none'))
self.execute_module(changed=True, commands=lines, sort=False) self.execute_module(changed=True, commands=lines, sort=False)

Loading…
Cancel
Save