From af3f510316f64e26a143f3cb6e2701bee3ed2b51 Mon Sep 17 00:00:00 2001 From: Ganesh Nalawade Date: Fri, 27 Jul 2018 11:05:40 +0530 Subject: [PATCH] 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 --- lib/ansible/module_utils/network/eos/eos.py | 15 +- lib/ansible/module_utils/network/nxos/nxos.py | 63 ++++-- lib/ansible/modules/network/eos/eos_config.py | 3 +- lib/ansible/modules/network/ios/ios_config.py | 3 +- .../modules/network/nxos/nxos_config.py | 75 ++++--- .../modules/network/vyos/vyos_config.py | 2 +- lib/ansible/plugins/cliconf/__init__.py | 31 ++- lib/ansible/plugins/cliconf/eos.py | 89 ++++---- lib/ansible/plugins/cliconf/ios.py | 64 +++--- lib/ansible/plugins/cliconf/nxos.py | 206 +++++++++++++----- lib/ansible/plugins/cliconf/vyos.py | 34 ++- lib/ansible/plugins/httpapi/nxos.py | 13 +- .../modules/network/eos/test_eos_config.py | 7 +- .../modules/network/ios/test_ios_config.py | 10 +- .../modules/network/nxos/test_nxos_config.py | 44 +++- .../modules/network/vyos/test_vyos_config.py | 2 +- 16 files changed, 421 insertions(+), 240 deletions(-) diff --git a/lib/ansible/module_utils/network/eos/eos.py b/lib/ansible/module_utils/network/eos/eos.py index 424b73ebb01..4dcb1d4d8a6 100644 --- a/lib/ansible/module_utils/network/eos/eos.py +++ b/lib/ansible/module_utils/network/eos/eos.py @@ -162,9 +162,10 @@ class Cli: 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() - 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: @@ -361,17 +362,17 @@ class Eapi: 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'): + def get_diff(self, candidate, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'): diff = {} # prepare candidate configuration candidate_obj = NetworkConfig(indent=3) candidate_obj.load(candidate) - if running and match != 'none' and replace != 'config': + if running and diff_match != 'none' and diff_replace != 'config': # running configuration running_obj = NetworkConfig(indent=3, contents=running, ignore_lines=diff_ignore_lines) - configdiffobjs = candidate_obj.difference(running_obj, path=path, match=match, replace=replace) + configdiffobjs = candidate_obj.difference(running_obj, path=path, match=diff_match, replace=diff_replace) else: configdiffobjs = candidate_obj.items @@ -424,6 +425,6 @@ def load_config(module, config, commit=False, replace=False): 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() - 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) diff --git a/lib/ansible/module_utils/network/nxos/nxos.py b/lib/ansible/module_utils/network/nxos/nxos.py index c0887163983..304e36b8116 100644 --- a/lib/ansible/module_utils/network/nxos/nxos.py +++ b/lib/ansible/module_utils/network/nxos/nxos.py @@ -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.network.common.utils import to_list, ComplexList 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.urls import fetch_url @@ -138,7 +139,7 @@ class Cli: return self._device_configs[cmd] except KeyError: 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() self._device_configs[cmd] = cfg return cfg @@ -153,37 +154,42 @@ class Cli: except ConnectionError as 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 """ if opts is None: opts = {} connection = self._get_connection() - - msgs = [] + responses = [] try: - responses = connection.edit_config(config) - msg = json.loads(responses) + resp = connection.edit_config(config, replace=replace) + if isinstance(resp, collections.Mapping): + resp = resp['response'] except ConnectionError as e: code = getattr(e, 'code', 1) message = getattr(e, 'err', e) err = to_text(message, errors='surrogate_then_replace') if opts.get('ignore_timeout') and code: - msgs.append(code) - return msgs + responses.append(code) + return responses elif code and 'no graceful-restart' in err: if 'ISSU/HA will be affected if Graceful Restart is disabled' in err: msg = [''] - msgs.extend(msg) - return msgs + responses.extend(msg) + return responses else: self._module.fail_json(msg=err) elif code: self._module.fail_json(msg=err) - msgs.extend(msg) - return msgs + responses.extend(resp) + 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): """Returns platform info of the remove device @@ -371,10 +377,14 @@ class Nxapi: 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 """ + if replace: + commands = 'config replace {0}'.format(replace) + commands = to_list(commands) + msg = self.send_request(commands, output='config', check_status=True, return_error=return_error, opts=opts) if return_error: @@ -382,6 +392,24 @@ class Nxapi: else: 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): device_info = {} @@ -460,9 +488,9 @@ def run_commands(module, commands, check_rc=True): 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) - return conn.load_config(config, return_error, opts) + return conn.load_config(config, return_error, opts, replace=replace) def get_capabilities(module): @@ -470,6 +498,11 @@ def get_capabilities(module): 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): """Return the normalized interface name """ diff --git a/lib/ansible/modules/network/eos/eos_config.py b/lib/ansible/modules/network/eos/eos_config.py index db28e9131f1..d9093d01a23 100644 --- a/lib/ansible/modules/network/eos/eos_config.py +++ b/lib/ansible/modules/network/eos/eos_config.py @@ -382,7 +382,8 @@ def main(): candidate = get_candidate(module) 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'] if config_diff: diff --git a/lib/ansible/modules/network/ios/ios_config.py b/lib/ansible/modules/network/ios/ios_config.py index e858d936441..7e9745d91b9 100644 --- a/lib/ansible/modules/network/ios/ios_config.py +++ b/lib/ansible/modules/network/ios/ios_config.py @@ -420,7 +420,8 @@ def main(): candidate = get_candidate_config(module) 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'] banner_diff = response['banner_diff'] diff --git a/lib/ansible/modules/network/nxos/nxos_config.py b/lib/ansible/modules/network/nxos/nxos_config.py index 448f7cbc1e4..5782061e67e 100644 --- a/lib/ansible/modules/network/nxos/nxos_config.py +++ b/lib/ansible/modules/network/nxos/nxos_config.py @@ -100,7 +100,7 @@ options: the modified lines are 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 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 choices: ['line', 'block', 'config'] force: @@ -281,7 +281,7 @@ backup_path: from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import ConnectionError 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 nxos_argument_spec 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: flags = ['all'] contents = get_config(module, flags=flags) - return NetworkConfig(indent=2, contents=contents) + return contents def get_candidate(module): - candidate = NetworkConfig(indent=2) + candidate = '' if module.params['src']: if module.params['replace'] != 'config': - candidate.load(module.params['src']) + candidate = module.params['src'] 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']: + candidate_obj = NetworkConfig(indent=2) 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 @@ -404,7 +406,12 @@ def main(): if '9K' not in os_platform: 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': module.fail_json(msg='replace: config is required with replace_src') @@ -414,48 +421,51 @@ def main(): if module.params['backup']: 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'] replace = module.params['replace'] + commit = not module.check_mode 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': - config = get_running_config(module, config) - path = module.params['parents'] - configobjs = candidate.difference(config, match=match, replace=replace, path=path) + result['changed'] = True 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: - commands = dumps(configobjs, 'commands').split('\n') + if module.params['before']: + commands[:0] = module.params['before'] - if module.params['before']: - commands[:0] = module.params['before'] + if module.params['after']: + commands.extend(module.params['after']) - if module.params['after']: - commands.extend(module.params['after']) + result['commands'] = commands + result['updates'] = commands - result['commands'] = commands - result['updates'] = commands + if commit: + load_config(module, commands, replace=replace_src) - if not module.check_mode: - load_config(module, commands) - - result['changed'] = True + result['changed'] = True running_config = module.params['running_config'] startup_config = None - diff_ignore_lines = module.params['diff_ignore_lines'] - if module.params['save_when'] == 'always' or module.params['save']: save_config(module, result) elif module.params['save_when'] == 'modified': output = execute_show_commands(module, ['show running-config', 'show startup-config']) - running_config = NetworkConfig(indent=1, contents=output[0], ignore_lines=diff_ignore_lines) - startup_config = NetworkConfig(indent=1, contents=output[1], ignore_lines=diff_ignore_lines) + running_config = NetworkConfig(indent=2, contents=output[0], 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: save_config(module, result) @@ -470,7 +480,7 @@ def main(): contents = running_config # 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.check_mode: @@ -484,14 +494,13 @@ def main(): output = execute_show_commands(module, 'show startup-config') contents = output[0] else: - contents = output[0] contents = startup_config.config_text elif module.params['diff_against'] == 'intended': contents = module.params['intended_config'] 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 module.params['diff_against'] == 'intended': diff --git a/lib/ansible/modules/network/vyos/vyos_config.py b/lib/ansible/modules/network/vyos/vyos_config.py index 8937bf10329..c4d776b96bb 100644 --- a/lib/ansible/modules/network/vyos/vyos_config.py +++ b/lib/ansible/modules/network/vyos/vyos_config.py @@ -208,7 +208,7 @@ def run(module, result): # create loadable config that includes only the configuration updates 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') sanitize_config(commands, result) diff --git a/lib/ansible/plugins/cliconf/__init__.py b/lib/ansible/plugins/cliconf/__init__.py index ff211ce02e5..31e67390e17 100644 --- a/lib/ansible/plugins/cliconf/__init__.py +++ b/lib/ansible/plugins/cliconf/__init__.py @@ -189,7 +189,7 @@ class CliconfBase(AnsiblePlugin): pass @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 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 configuration should be pushed in the running configuration or discarded. - :param replace: Boolean flag to indicate if running configuration should be completely - replace by candidate configuration. + :param replace: If the value is True/False it indicates if running configuration should be completely + 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 :return: Returns a json string with contains configuration applied on remote host, the returned response on executing configuration commands and platform relevant data. @@ -341,7 +343,7 @@ class CliconfBase(AnsiblePlugin): with ssh.open_sftp() as sftp: 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 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. :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 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'. 'line' - commands are matched line by line '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 is omitted, the commands are checked against the set of top 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 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 @@ -396,3 +398,20 @@ class CliconfBase(AnsiblePlugin): :return: List of returned response """ 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") diff --git a/lib/ansible/plugins/cliconf/eos.py b/lib/ansible/plugins/cliconf/eos.py index ef960d0319f..6267ec8e806 100644 --- a/lib/ansible/plugins/cliconf/eos.py +++ b/lib/ansible/plugins/cliconf/eos.py @@ -59,23 +59,6 @@ class Cliconf(CliconfBase): 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 @@ -83,10 +66,12 @@ class Cliconf(CliconfBase): the results to the caller. The command output will be returned as a string """ - if self.network_api == 'network_cli': + if isinstance(self._connection, NetworkCli): resp = super(Cliconf, self).send_command(command, **kwargs) - else: + elif isinstance(self._connection, HttpApi): resp = self._connection.send_request(command, **kwargs) + else: + raise ValueError("Invalid connection type") return resp @enable_mode @@ -108,32 +93,19 @@ class Cliconf(CliconfBase): return self.send_command(cmd) @enable_mode - def edit_config(self, candidate=None, commit=True, replace=False, 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) + def edit_config(self, candidate=None, commit=True, replace=None, comment=None): operations = self.get_device_operations() - if replace not in (True, False): - 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") + self.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment) if (commit is False) and (not self.supports_sessions): raise ValueError('check mode is not supported without configuration session') - response = {} + resp = {} session = None if self.supports_sessions: session = 'ansible_%s' % int(time.time()) - response.update({'session': session}) + resp.update({'session': session}) self.send_command('configure session %s' % session) if replace: self.send_command('rollback clean-config') @@ -141,6 +113,7 @@ class Cliconf(CliconfBase): self.send_command('configure') results = [] + requests = [] multiline = False for line in to_list(candidate): if not isinstance(line, collections.Mapping): @@ -160,15 +133,17 @@ class Cliconf(CliconfBase): if cmd != 'end' and cmd[0] != '!': try: results.append(self.send_command(**line)) + requests.append(cmd) except AnsibleConnectionFailure as e: self.discard_changes(session) raise AnsibleConnectionFailure(e.message) - response['response'] = results + resp['request'] = requests + resp['response'] = results if self.supports_sessions: out = self.send_command('show session-config diffs') if out: - response['diff'] = out.strip() + resp['diff'] = out.strip() if commit: self.commit() @@ -176,7 +151,7 @@ class Cliconf(CliconfBase): self.discard_changes(session) else: self.send_command('end') - return response + return resp def get(self, command, prompt=None, answer=None, sendonly=False, output=None): if output: @@ -224,7 +199,7 @@ class Cliconf(CliconfBase): responses.append(out) 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 = {} device_operations = self.get_device_operations() option_values = self.get_option_values() @@ -232,26 +207,25 @@ class Cliconf(CliconfBase): if candidate is None and device_operations['supports_generate_diff']: raise ValueError("candidate configuration is required to generate diff") - if match not in option_values['diff_match']: - raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match']))) + 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 replace not in option_values['diff_replace']: - raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, ', '.join(option_values['diff_replace']))) + 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=3) 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_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: configdiffobjs = candidate_obj.items - configdiff = dumps(configdiffobjs, 'commands') if configdiffobjs else '' - diff['config_diff'] = configdiff if configdiffobjs else {} + diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else '' return diff @property @@ -317,8 +291,25 @@ class Cliconf(CliconfBase): 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()) + + if isinstance(self._connection, NetworkCli): + result['network_api'] = 'cliconf' + elif isinstance(self._connection, HttpApi): + result['network_api'] = 'eapi' + else: + raise ValueError("Invalid connection type") return json.dumps(result) + + def _get_command_with_output(self, command, output): + 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 diff --git a/lib/ansible/plugins/cliconf/ios.py b/lib/ansible/plugins/cliconf/ios.py index 7a4faa96eee..61379c0250d 100644 --- a/lib/ansible/plugins/cliconf/ios.py +++ b/lib/ansible/plugins/cliconf/ios.py @@ -56,7 +56,7 @@ class Cliconf(CliconfBase): 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 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. :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 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'. 'line' - commands are matched line by line '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 is omitted, the commands are checked against the set of top 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 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 @@ -87,7 +87,7 @@ class Cliconf(CliconfBase): :return: Configuration diff in json format. { 'config_diff': '', - 'banner_diff': '' + 'banner_diff': {} } """ @@ -98,71 +98,57 @@ class Cliconf(CliconfBase): if candidate is None and device_operations['supports_generate_diff']: raise ValueError("candidate configuration is required to generate diff") - if match not in option_values['diff_match']: - raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match']))) + 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 replace not in option_values['diff_replace']: - raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, ', '.join(option_values['diff_replace']))) + if diff_replace not in option_values['diff_replace']: + raise ValueError("'replace' value %s in invalid, valid values are %s" % (diff_replace, ', '.join(option_values['diff_replace']))) # prepare candidate configuration candidate_obj = NetworkConfig(indent=1) want_src, want_banners = self._extract_banners(candidate) candidate_obj.load(want_src) - if running and match != 'none': + if running and diff_match != 'none': # running configuration have_src, have_banners = self._extract_banners(running) 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: configdiffobjs = candidate_obj.items have_banners = {} - configdiff = dumps(configdiffobjs, 'commands') if configdiffobjs else '' - diff['config_diff'] = configdiff if configdiffobjs else {} - + diff['config_diff'] = dumps(configdiffobjs, 'commands') if configdiffobjs else '' banners = self._diff_banners(want_banners, have_banners) - diff['banner_diff'] = banners if banners else {} return diff @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 = {} operations = self.get_device_operations() - - 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") + self.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment) results = [] + requests = [] 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): line = {'command': line} cmd = line['command'] if cmd != 'end' and cmd[0] != '!': results.append(self.send_command(**line)) + requests.append(cmd) - results.append(self.send_command('end')) + self.send_command('end') else: raise ValueError('check mode is not supported') - resp['response'] = results[1:-1] + resp['request'] = requests + resp['response'] = results return resp def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None): @@ -241,17 +227,23 @@ class Cliconf(CliconfBase): resp = {} banners_obj = json.loads(candidate) results = [] + requests = [] if commit: for key, value in iteritems(banners_obj): 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} results.append(self.send_command(**obj)) + requests.append(cmd) + self.send_commad('end', sendonly=True) time.sleep(0.1) results.append(self.send_command('\n')) + requests.append('\n') - resp['response'] = results[1:-1] + resp['request'] = requests + resp['response'] = results return resp diff --git a/lib/ansible/plugins/cliconf/nxos.py b/lib/ansible/plugins/cliconf/nxos.py index b901ceeccfe..0e999bde323 100644 --- a/lib/ansible/plugins/cliconf/nxos.py +++ b/lib/ansible/plugins/cliconf/nxos.py @@ -19,6 +19,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type +import collections import json import re @@ -27,30 +28,30 @@ from itertools import chain from ansible.errors import AnsibleConnectionFailure from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils.connection import ConnectionError +from ansible.module_utils.network.common.config import NetworkConfig, dumps from ansible.module_utils.network.common.utils import to_list -from ansible.plugins.cliconf import CliconfBase +from ansible.plugins.cliconf import CliconfBase, enable_mode from ansible.plugins.connection.network_cli import Connection as NetworkCli +from ansible.plugins.connection.httpapi import Connection as HttpApi class Cliconf(CliconfBase): - 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 This method will execute the CLI command on the connection and return the results to the caller. The command output will be returned as a string """ - 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): - resp = self._connection.send(**kwargs) - else: + resp = super(Cliconf, self).send_command(command, **kwargs) + elif isinstance(self._connection, HttpApi): resp = self._connection.send_request(command, **kwargs) + else: + raise ValueError("Invalid connection type") return resp def get_device_info(self): @@ -101,66 +102,169 @@ class Cliconf(CliconfBase): 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'} + if source not in lookup: + return self.invalid_params("fetching configuration from %s is not supported" % source) cmd = 'show {0} '.format(lookup[source]) - if flags: - cmd += ' '.join(flags) + if format and format is not 'text': + cmd += '| %s ' % format + + if filter: + cmd += ' '.join(to_list(filter)) cmd = cmd.strip() return self.send_command(cmd) - def edit_config(self, command): - responses = [] - for cmd in chain(['configure'], to_list(command), ['end']): - responses.append(self.send_command(cmd)) - resp = responses[1:-1] - return json.dumps(resp) + def edit_config(self, candidate=None, commit=True, replace=None, comment=None): + resp = {} + operations = self.get_device_operations() + self.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment) + results = [] + requests = [] - def get(self, command, prompt=None, answer=None, sendonly=False): - return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) + if replace: + candidate = 'config replace {0}'.format(replace) - def get_capabilities(self): - result = {} - result['rpc'] = self.get_base_rpc() - result['device_info'] = self.get_device_info() - if isinstance(self._connection, NetworkCli): - result['network_api'] = 'cliconf' + if commit: + self.send_command('configure terminal') + + for line in to_list(candidate): + if not isinstance(line, collections.Mapping): + line = {'command': line} + + cmd = line['command'] + if cmd != 'end': + results.append(self.send_command(**line)) + requests.append(cmd) + + self.send_command('end') else: - result['network_api'] = 'nxapi' - return json.dumps(result) + raise ValueError('check mode is not supported') + + resp['request'] = requests + resp['response'] = results + return resp + + def get(self, command, prompt=None, answer=None, sendonly=False, output=None): + 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() + for cmd in to_list(commands): + if not isinstance(cmd, collections.Mapping): + cmd = {'command': cmd} - for item in to_list(commands): - if item['output'] == 'json' and not item['command'].endswith('| json'): - cmd = '%s | json' % item['command'] - elif item['output'] == 'text' and item['command'].endswith('| json'): - cmd = item['command'].rsplit('|', 1)[0] - else: - cmd = item['command'] + output = cmd.pop('output', None) + if output: + cmd['command'] = self._get_command_with_output(cmd['command'], output) try: - out = self.get(cmd) + out = self.send_command(**cmd) except AnsibleConnectionFailure as e: if check_rc: raise out = getattr(e, 'err', e) - try: - out = to_text(out, errors='surrogate_or_strict').strip() - except UnicodeError: - raise ConnectionError(msg=u'Failed to decode output from %s: %s' % (cmd, to_text(out))) + if out is not None: + try: + out = to_text(out, errors='surrogate_or_strict').strip() + except UnicodeError: + raise ConnectionError(msg=u'Failed to decode output from %s: %s' % (cmd, to_text(out))) - try: - out = json.loads(out) - except ValueError: - pass + try: + out = json.loads(out) + except ValueError: + out = to_text(out, errors='surrogate_or_strict').strip() - responses.append(out) + responses.append(out) 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 diff --git a/lib/ansible/plugins/cliconf/vyos.py b/lib/ansible/plugins/cliconf/vyos.py index 0af5589234c..15569fa4c02 100644 --- a/lib/ansible/plugins/cliconf/vyos.py +++ b/lib/ansible/plugins/cliconf/vyos.py @@ -66,29 +66,20 @@ class Cliconf(CliconfBase): out = self.send_command('show configuration commands') 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 = {} - 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() - if replace and not operations['supports_replace']: - raise ValueError("configuration replace is not supported") + self.check_edit_config_capabiltiy(operations, candidate, commit, replace, comment) results = [] - - for cmd in chain(['configure'], to_list(candidate)): + requests = [] + self.send_command('configure') + for cmd in to_list(candidate): if not isinstance(cmd, collections.Mapping): cmd = {'command': cmd} results.append(self.send_command(**cmd)) - + requests.append(cmd['command']) out = self.get('compare') out = to_text(out, errors='surrogate_or_strict') diff_config = out if not out.startswith('No changes') else None @@ -109,7 +100,8 @@ class Cliconf(CliconfBase): self.send_command('exit') resp['diff'] = diff_config - resp['response'] = results[1:-1] + resp['response'] = results + resp['request'] = requests return resp def get(self, command=None, prompt=None, answer=None, sendonly=False, output=None): @@ -131,7 +123,7 @@ class Cliconf(CliconfBase): def discard_changes(self): 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 = {} device_operations = self.get_device_operations() option_values = self.get_option_values() @@ -139,10 +131,10 @@ class Cliconf(CliconfBase): if candidate is None and device_operations['supports_generate_diff']: raise ValueError("candidate configuration is required to generate diff") - if match not in option_values['diff_match']: - raise ValueError("'match' value %s in invalid, valid values are %s" % (match, ', '.join(option_values['diff_match']))) + 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 replace: + if diff_replace: raise ValueError("'replace' in diff is not supported") if diff_ignore_lines: @@ -169,7 +161,7 @@ class Cliconf(CliconfBase): else: candidate_commands = str(candidate).strip().split('\n') - if match == 'none': + if diff_match == 'none': diff['config_diff'] = list(candidate_commands) return diff diff --git a/lib/ansible/plugins/httpapi/nxos.py b/lib/ansible/plugins/httpapi/nxos.py index 45958bb7f80..1afc883b635 100644 --- a/lib/ansible/plugins/httpapi/nxos.py +++ b/lib/ansible/plugins/httpapi/nxos.py @@ -72,17 +72,22 @@ class HttpApi(HttpApiBase): return responses[0] return responses - # Migrated from module_utils - def edit_config(self, command): + def edit_config(self, candidate=None, commit=True, replace=None, comment=None): 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): if response != '{}': resp.append(response) if not resp: resp = [''] - return json.dumps(resp) + return resp def run_commands(self, commands, check_rc=True): """Runs list of commands on remote device and returns results diff --git a/test/units/modules/network/eos/test_eos_config.py b/test/units/modules/network/eos/test_eos_config.py index 1691bcd81fb..f280e94c235 100644 --- a/test/units/modules/network/eos/test_eos_config.py +++ b/test/units/modules/network/eos/test_eos_config.py @@ -21,7 +21,7 @@ __metaclass__ = type from ansible.compat.tests.mock import patch, MagicMock 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 .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.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.edit_config = MagicMock() @@ -54,6 +58,7 @@ class TestEosConfigModule(TestEosModule): self.mock_get_config.stop() self.mock_load_config.stop() self.mock_get_connection.stop() + self.mock_supports_sessions.stop() def load_fixtures(self, commands=None, transport='cli'): self.get_config.return_value = load_fixture('eos_config_config.cfg') diff --git a/test/units/modules/network/ios/test_ios_config.py b/test/units/modules/network/ios/test_ios_config.py index 4311057e18e..ef87fb055af 100644 --- a/test/units/modules/network/ios/test_ios_config.py +++ b/test/units/modules/network/ios/test_ios_config.py @@ -177,7 +177,7 @@ class TestIosConfigModule(TestIosModule): module.params = {'lines': lines, 'parents': parents, 'src': None} 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 self.execute_module(changed=True, commands=commands) @@ -185,7 +185,7 @@ class TestIosConfigModule(TestIosModule): def test_ios_config_match_none(self): lines = ['hostname router'] 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) def test_ios_config_match_none(self): @@ -196,7 +196,7 @@ class TestIosConfigModule(TestIosModule): module = MagicMock() module.params = {'lines': lines, 'parents': parents, 'src': None} 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 self.execute_module(changed=True, commands=commands, sort=False) @@ -210,7 +210,7 @@ class TestIosConfigModule(TestIosModule): module = MagicMock() module.params = {'lines': lines, 'parents': parents, 'src': None} 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'] self.execute_module(changed=True, commands=commands, sort=False) @@ -224,7 +224,7 @@ class TestIosConfigModule(TestIosModule): module = MagicMock() module.params = {'lines': lines, 'parents': parents, 'src': None} 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 self.execute_module(changed=True, commands=commands, sort=False) diff --git a/test/units/modules/network/nxos/test_nxos_config.py b/test/units/modules/network/nxos/test_nxos_config.py index 14dc1131bae..d06121d61c8 100644 --- a/test/units/modules/network/nxos/test_nxos_config.py +++ b/test/units/modules/network/nxos/test_nxos_config.py @@ -19,8 +19,9 @@ from __future__ import (absolute_import, division, print_function) __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.plugins.cliconf.nxos import Cliconf 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.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): super(TestNxosConfigModule, self).tearDown() self.mock_get_config.stop() self.mock_load_config.stop() self.mock_get_capabilities.stop() + self.mock_run_commands.stop() + self.mock_get_connection.stop() def load_fixtures(self, commands=None, device=''): self.get_config.return_value = load_fixture('nxos_config', 'config.cfg') self.load_config.return_value = None 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) result = self.execute_module() 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) result = self.execute_module(changed=True) @@ -71,11 +90,14 @@ class TestNxosConfigModule(TestNxosModule): def test_nxos_config_replace_src(self): 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) self.assertEqual(result['commands'], ['config replace config.txt']) 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) result = self.execute_module(changed=True) @@ -84,9 +106,10 @@ class TestNxosConfigModule(TestNxosModule): self.assertEqual(sorted(config), sorted(result['commands']), result['commands']) 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']) - + self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config)) set_module_args(args) result = self.execute_module(changed=True) @@ -96,9 +119,11 @@ class TestNxosConfigModule(TestNxosModule): self.assertEqual('before command', result['commands'][0]) 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']) + self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff('\n'.join(lines), self.running_config)) set_module_args(args) result = self.execute_module(changed=True) @@ -108,7 +133,10 @@ class TestNxosConfigModule(TestNxosModule): self.assertEqual('after command', result['commands'][-1]) 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) result = self.execute_module(changed=True) diff --git a/test/units/modules/network/vyos/test_vyos_config.py b/test/units/modules/network/vyos/test_vyos_config.py index a923193af92..c4cc347138c 100644 --- a/test/units/modules/network/vyos/test_vyos_config.py +++ b/test/units/modules/network/vyos/test_vyos_config.py @@ -113,5 +113,5 @@ class TestVyosConfigModule(TestVyosModule): 'set system interfaces ethernet eth0 description test string'] set_module_args(dict(lines=lines, match='none')) 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)