From 773c031d334cf723279ea28cc59034155a239f07 Mon Sep 17 00:00:00 2001 From: Ganesh Nalawade Date: Mon, 25 Jun 2018 09:43:37 +0530 Subject: [PATCH] vyos and ios cliconf plugin refactor (#41846) * vyos and ios cliconf plugin refactor * Refactor vyos cliconf plugin * Change vyos module_utils and vyos_config as per refactor * Minor changes in ios cliconf plugin * Fix unit test failure * Fix sanity issues * Add get_diff to rpc list --- lib/ansible/module_utils/network/ios/ios.py | 3 +- lib/ansible/module_utils/network/vyos/vyos.py | 25 +-- lib/ansible/modules/network/ios/ios_config.py | 4 +- .../modules/network/vyos/vyos_config.py | 39 ++--- lib/ansible/plugins/cliconf/__init__.py | 66 ++++++-- lib/ansible/plugins/cliconf/ios.py | 73 ++++++--- lib/ansible/plugins/cliconf/vyos.py | 153 +++++++++++++++++- .../modules/network/vyos/test_vyos_config.py | 23 ++- 8 files changed, 285 insertions(+), 101 deletions(-) diff --git a/lib/ansible/module_utils/network/ios/ios.py b/lib/ansible/module_utils/network/ios/ios.py index af4a1485f2..1b091e75b0 100644 --- a/lib/ansible/module_utils/network/ios/ios.py +++ b/lib/ansible/module_utils/network/ios/ios.py @@ -166,6 +166,7 @@ def load_config(module, commands): connection = get_connection(module) try: - return connection.edit_config(commands) + diff, response = connection.edit_config(commands) + return response except ConnectionError as exc: module.fail_json(msg=to_text(exc)) diff --git a/lib/ansible/module_utils/network/vyos/vyos.py b/lib/ansible/module_utils/network/vyos/vyos.py index 04646d1186..0d9dc41d4e 100644 --- a/lib/ansible/module_utils/network/vyos/vyos.py +++ b/lib/ansible/module_utils/network/vyos/vyos.py @@ -133,29 +133,8 @@ def load_config(module, commands, commit=False, comment=None): connection = get_connection(module) try: - out = connection.edit_config(commands) + diff_config, resp = connection.edit_config(candidate=commands, commit=commit, diff=module._diff, comment=comment) except ConnectionError as exc: module.fail_json(msg=to_text(exc)) - diff = None - if module._diff: - out = connection.get('compare') - out = to_text(out, errors='surrogate_or_strict') - - if not out.startswith('No changes'): - out = connection.get('show') - diff = to_text(out, errors='surrogate_or_strict').strip() - - if commit: - try: - out = connection.commit(comment) - except ConnectionError: - connection.discard_changes() - module.fail_json(msg='commit failed: %s' % out) - else: - connection.get('exit') - else: - connection.discard_changes() - - if diff: - return diff + return diff_config diff --git a/lib/ansible/modules/network/ios/ios_config.py b/lib/ansible/modules/network/ios/ios_config.py index ae1d58ca22..0ddcd42a2d 100644 --- a/lib/ansible/modules/network/ios/ios_config.py +++ b/lib/ansible/modules/network/ios/ios_config.py @@ -441,9 +441,9 @@ def main(): # them with the current running config if not module.check_mode: if commands: - connection.edit_config(commands) + connection.edit_config(candidate=commands) if banner_diff: - connection.edit_banner(json.dumps(banner_diff), multiline_delimiter=module.params['multiline_delimiter']) + connection.edit_banner(candidate=json.dumps(banner_diff), multiline_delimiter=module.params['multiline_delimiter']) result['changed'] = True diff --git a/lib/ansible/modules/network/vyos/vyos_config.py b/lib/ansible/modules/network/vyos/vyos_config.py index 4b0214494e..a639449320 100644 --- a/lib/ansible/modules/network/vyos/vyos_config.py +++ b/lib/ansible/modules/network/vyos/vyos_config.py @@ -130,11 +130,11 @@ backup_path: sample: /playbooks/ansible/backup/vyos_config.2016-07-16@22:28:34 """ import re +import json from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.network.common.config import NetworkConfig from ansible.module_utils.network.vyos.vyos import load_config, get_config, run_commands -from ansible.module_utils.network.vyos.vyos import vyos_argument_spec +from ansible.module_utils.network.vyos.vyos import vyos_argument_spec, get_connection DEFAULT_COMMENT = 'configured by vyos_config' @@ -144,35 +144,13 @@ CONFIG_FILTERS = [ ] -def config_to_commands(config): - set_format = config.startswith('set') or config.startswith('delete') - candidate = NetworkConfig(indent=4, contents=config) - if not set_format: - candidate = [c.line for c in candidate.items] - commands = list() - # this filters out less specific lines - for item in candidate: - for index, entry in enumerate(commands): - if item.startswith(entry): - del commands[index] - break - commands.append(item) - - commands = ['set %s' % cmd.replace(' {', '') for cmd in commands] - - else: - commands = str(candidate).split('\n') - - return commands - - def get_candidate(module): contents = module.params['src'] or module.params['lines'] if module.params['lines']: contents = '\n'.join(contents) - return config_to_commands(contents) + return contents def diff_config(commands, config): @@ -225,7 +203,10 @@ def run(module, result): candidate = get_candidate(module) # create loadable config that includes only the configuration updates - commands = diff_config(candidate, config) + connection = get_connection(module) + response = connection.get_diff(candidate=candidate, running=config, match=module.params['match']) + diff_obj = json.loads(response) + commands = diff_obj.get('config_diff') sanitize_config(commands, result) result['commands'] = commands @@ -233,8 +214,9 @@ def run(module, result): commit = not module.check_mode comment = module.params['comment'] + diff = None if commands: - load_config(module, commands, commit=commit, comment=comment) + diff = load_config(module, commands, commit=commit, comment=comment) if result.get('filtered'): result['warnings'].append('Some configuration commands were ' @@ -242,6 +224,9 @@ def run(module, result): result['changed'] = True + if module._diff: + result['diff'] = diff + def main(): argument_spec = dict( diff --git a/lib/ansible/plugins/cliconf/__init__.py b/lib/ansible/plugins/cliconf/__init__.py index b83d6d9dd8..4713d7e9b2 100644 --- a/lib/ansible/plugins/cliconf/__init__.py +++ b/lib/ansible/plugins/cliconf/__init__.py @@ -93,7 +93,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)): display.display('closing shell due to command timeout (%s seconds).' % self._connection._play_context.timeout, log_only=True) self.close() - def send_command(self, command, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False): + def send_command(self, command=None, prompt=None, answer=None, sendonly=False, newline=True, prompt_retry_check=False): """Executes a command over the device connection This method will execute a command over the device connection and @@ -184,7 +184,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)): pass @abstractmethod - def edit_config(self, candidate, check_mode=False, replace=None): + def edit_config(self, candidate=None, commit=True, replace=False, diff=False, comment=None): """Loads the candidate configuration into the network device This method will load the specified candidate config into the device @@ -195,20 +195,22 @@ class CliconfBase(with_metaclass(ABCMeta, object)): :param candidate: The configuration to load into the device and merge with the current running configuration - :param check_mode: 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. - :param replace: Specifies the way in which provided config value should replace - the configuration running on the remote device. If the device - doesn't support config replace, an error is return. - - :return: Returns response of executing the configuration command received - from remote host + :param replace: Boolean flag to indicate if running configuration should be completely + replace by candidate configuration. + :param diff: Boolean flag to indicate if configuration that is applied on remote host should + generated and returned in response or not + :param comment: Commit comment provided it is supported by remote host + :return: Returns a tuple, the first entry of tupe is configuration diff if diff flag is enable else + it is None. Second entry is the list of response received from remote host on executing + configuration commands. """ pass @abstractmethod - def get(self, command, prompt=None, answer=None, sendonly=False, newline=True): + def get(self, command=None, prompt=None, answer=None, sendonly=False, newline=True): """Execute specified command on remote device This method will retrieve the specified data and return it to the caller as a string. @@ -242,7 +244,7 @@ class CliconfBase(with_metaclass(ABCMeta, object)): 'network_os_platform': , }, 'device_operations': { - 'supports_replace': , # identify if config should be merged or replaced is supported + 'supports_diff_replace': , # identify if config should be merged or replaced is supported 'supports_commit': , # identify if commit is supported by device or not 'supports_rollback': , # identify if rollback is supported or not 'supports_defaults': , # identify if fetching running config with default is supported @@ -250,12 +252,13 @@ class CliconfBase(with_metaclass(ABCMeta, object)): 'supports_onbox_diff: , # identify if on box diff capability is supported or not 'supports_generate_diff: , # identify if diff capability is supported within plugin 'supports_multiline_delimiter: , # identify if multiline demiliter is supported within config - 'support_match: , # identify if match is supported + 'support_diff_match: , # identify if match is supported 'support_diff_ignore_lines: , # identify if ignore line in diff is supported + 'support_config_replace': , # identify if running config replace with candidate config is supported } 'format': [list of supported configuration format], - 'match': ['line', 'strict', 'exact', 'none'], - 'replace': ['line', 'block', 'config'], + 'diff_match': [list of supported match values], + 'diff_replace': [list of supported replace values], } :return: capability as json string """ @@ -326,3 +329,38 @@ class CliconfBase(with_metaclass(ABCMeta, object)): elif proto == 'sftp': 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): + """ + Generate diff between candidate and running configuration. If the + remote host supports onbox diff capabilities ie. supports_onbox_diff in that case + candidate and running configurations are not required to be passed as argument. + In case if onbox diff capability is not supported candidate argument is mandatory + 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 + Valid values are 'line', 'strict', 'exact', 'none'. + 'line' - commands are matched line by line + 'strict' - command lines are matched with respect to position + 'exact' - command lines must be an equal match + 'none' - will not compare the candidate configuration with the running configuration + :param diff_ignore_lines: Use this argument to specify one or more lines that should be + ignored during the diff. This is used for lines in the configuration + that are automatically updated by the system. This argument takes + a list of regular expressions or exact line matches. + :param path: The ordered set of parents that uniquely identify the section or hierarchy + 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. + 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 + configuration mode if any line is not correct. + :return: Configuration and/or banner diff in json format. + { + 'config_diff': '' + } + + """ diff --git a/lib/ansible/plugins/cliconf/ios.py b/lib/ansible/plugins/cliconf/ios.py index d4637bd759..b2e5022947 100644 --- a/lib/ansible/plugins/cliconf/ios.py +++ b/lib/ansible/plugins/cliconf/ios.py @@ -66,8 +66,7 @@ class Cliconf(CliconfBase): 'line' - commands are matched line by line 'strict' - command lines are matched with respect to position 'exact' - command lines must be an equal match - 'none' - will not compare the candidate configuration with - the running configuration on the remote device + 'none' - will not compare the candidate configuration with the running configuration :param diff_ignore_lines: Use this argument to specify one or more lines that should be ignored during the diff. This is used for lines in the configuration that are automatically updated by the system. This argument takes @@ -90,9 +89,16 @@ class Cliconf(CliconfBase): """ diff = {} device_operations = self.get_device_operations() + option_values = self.get_option_values() if candidate is None and not device_operations['supports_onbox_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']: + raise ValueError("'match' value %s in invalid, valid values are %s" % (match, option_values['diff_match'])) + + if replace not in option_values['diff_replace']: + raise ValueError("'replace' value %s in invalid, valid values are %s" % (replace, option_values['diff_replace'])) # prepare candidate configuration candidate_obj = NetworkConfig(indent=1) @@ -118,19 +124,22 @@ class Cliconf(CliconfBase): return json.dumps(diff) @enable_mode - def edit_config(self, candidate, check_mode=False, replace=None): + def edit_config(self, candidate=None, commit=True, replace=False, diff=False, comment=None): if not candidate: - raise ValueError('must provide a candidate config to load') + raise ValueError("must provide a candidate config to load") - if check_mode not in (True, False): - raise ValueError('`check_mode` must be a bool, got %s' % check_mode) + if commit not in (True, False): + raise ValueError("'commit' must be a bool, got %s" % commit) - options = self.get_option_values() - if replace and replace not in options['replace']: - raise ValueError('`replace` value %s in invalid, valid values are %s' % (replace, options['replace'])) + 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 on ios") results = [] - if not check_mode: + if commit: for line in chain(['configure terminal'], to_list(candidate)): if not isinstance(line, collections.Mapping): line = {'command': line} @@ -140,16 +149,23 @@ class Cliconf(CliconfBase): results.append(self.send_command(**line)) results.append(self.send_command('end')) - return results[1:-1] - def get(self, command, prompt=None, answer=None, sendonly=False): - return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) + diff_config = None + if diff: + diff_config = candidate + + return diff_config, results[1:-1] + + def get(self, command=None, prompt=None, answer=None, sendonly=False): + if not command: + raise ValueError('must provide value of command to execute') + return self.send_command(command=command, prompt=prompt, answer=answer, sendonly=sendonly) def get_device_info(self): device_info = {} device_info['network_os'] = 'ios' - reply = self.get('show version') + reply = self.get(command='show version') data = to_text(reply, errors='surrogate_or_strict').strip() match = re.search(r'Version (\S+)', data) @@ -168,47 +184,50 @@ class Cliconf(CliconfBase): def get_device_operations(self): return { - 'supports_replace': True, + '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, - 'support_match': True, + 'support_diff_match': True, 'support_diff_ignore_lines': True, 'supports_generate_diff': True, + 'supports_replace': False } def get_option_values(self): return { 'format': ['text'], - 'match': ['line', 'strict', 'exact', 'none'], - 'replace': ['line', 'block'] + 'diff_match': ['line', 'strict', 'exact', 'none'], + 'diff_replace': ['line', 'block'] } def get_capabilities(self): result = dict() - result['rpc'] = self.get_base_rpc() + ['edit_banner'] + result['rpc'] = self.get_base_rpc() + ['edit_banner', 'get_diff'] result['network_api'] = 'cliconf' result['device_info'] = self.get_device_info() result['device_operations'] = self.get_device_operations() result.update(self.get_option_values()) return json.dumps(result) - def edit_banner(self, banners, multiline_delimiter="@", check_mode=False): + def edit_banner(self, candidate=None, multiline_delimiter="@", commit=True, diff=False): """ Edit banner on remote device :param banners: Banners to be loaded in json format :param multiline_delimiter: Line delimiter for banner - :param check_mode: 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. + :param diff: Boolean flag to indicate if configuration that is applied on remote host should + generated and returned in response or not :return: Returns response of executing the configuration command received from remote host """ - banners_obj = json.loads(banners) + banners_obj = json.loads(candidate) results = [] - if not check_mode: + if commit: for key, value in iteritems(banners_obj): key += ' %s' % multiline_delimiter for cmd in ['config terminal', key, value, multiline_delimiter, 'end']: @@ -218,7 +237,11 @@ class Cliconf(CliconfBase): time.sleep(0.1) results.append(self.send_command('\n')) - return results[1:-1] + diff_banner = None + if diff: + diff_banner = candidate + + return diff_banner, results[1:-1] def _extract_banners(self, config): banners = {} diff --git a/lib/ansible/plugins/cliconf/vyos.py b/lib/ansible/plugins/cliconf/vyos.py index 0b9076e5da..f2dd1270f4 100644 --- a/lib/ansible/plugins/cliconf/vyos.py +++ b/lib/ansible/plugins/cliconf/vyos.py @@ -24,7 +24,9 @@ import json from itertools import chain +from ansible.errors import AnsibleConnectionFailure from ansible.module_utils._text import to_text +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 @@ -51,14 +53,56 @@ class Cliconf(CliconfBase): return device_info - def get_config(self, source='running', format='text'): - return self.send_command('show configuration commands') + def get_config(self, filter=None, format='set'): + if format == 'text': + out = self.send_command('show configuration') + else: + out = self.send_command('show configuration commands') + return out - def edit_config(self, command): - for cmd in chain(['configure'], to_list(command)): - self.send_command(cmd) + def edit_config(self, candidate=None, commit=True, replace=False, diff=False, comment=None): + if not candidate: + raise ValueError('must provide a candidate config to load') - def get(self, command, prompt=None, answer=None, sendonly=False): + 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 on vyos") + + results = [] + + for cmd in chain(['configure'], to_list(candidate)): + results.append(self.send_command(cmd)) + + diff_config = None + if diff: + out = self.get('compare') + out = to_text(out, errors='surrogate_or_strict') + if not out.startswith('No changes'): + diff_config = out + + if commit: + try: + self.commit(comment) + except AnsibleConnectionFailure as e: + msg = 'commit failed: %s' % e.message + self.discard_changes() + raise AnsibleConnectionFailure(msg) + else: + self.get('exit') + else: + self.discard_changes() + + return diff_config, results[1:] + + def get(self, command=None, prompt=None, answer=None, sendonly=False): + if not command: + raise ValueError('must provide value of command to execute') return self.send_command(command, prompt=prompt, answer=answer, sendonly=sendonly) def commit(self, comment=None): @@ -68,12 +112,105 @@ class Cliconf(CliconfBase): command = 'commit' self.send_command(command) - def discard_changes(self, *args, **kwargs): + 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): + diff = {} + device_operations = self.get_device_operations() + option_values = self.get_option_values() + + if candidate is None and not device_operations['supports_onbox_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, option_values['diff_match'])) + + if replace: + raise ValueError("'replace' in diff is not supported on vyos") + + if diff_ignore_lines: + raise ValueError("'diff_ignore_lines' in diff is not supported on vyos") + + if path: + raise ValueError("'path' in diff is not supported on vyos") + + set_format = candidate.startswith('set') or candidate.startswith('delete') + candidate_obj = NetworkConfig(indent=4, contents=candidate) + if not set_format: + config = [c.line for c in candidate_obj.items] + commands = list() + # this filters out less specific lines + for item in config: + for index, entry in enumerate(commands): + if item.startswith(entry): + del commands[index] + break + commands.append(item) + + candidate_commands = ['set %s' % cmd.replace(' {', '') for cmd in commands] + + else: + candidate_commands = str(candidate).strip().split('\n') + + if match == 'none': + diff['config_diff'] = list(candidate_commands) + return json.dumps(diff) + + running_commands = [str(c).replace("'", '') for c in running.splitlines()] + + updates = list() + visited = set() + + for line in candidate_commands: + item = str(line).replace("'", '') + + if not item.startswith('set') and not item.startswith('delete'): + raise ValueError('line must start with either `set` or `delete`') + + elif item.startswith('set') and item not in running_commands: + updates.append(line) + + elif item.startswith('delete'): + if not running_commands: + updates.append(line) + else: + item = re.sub(r'delete', 'set', item) + for entry in running_commands: + if entry.startswith(item) and line not in visited: + updates.append(line) + visited.add(line) + + diff['config_diff'] = list(updates) + return json.dumps(diff) + + def get_device_operations(self): + return { + 'supports_diff_replace': False, + 'supports_commit': True, + 'supports_rollback': True, + 'supports_defaults': False, + 'supports_onbox_diff': False, + 'supports_commit_comment': True, + 'supports_multiline_delimiter': False, + 'support_diff_match': True, + 'support_diff_ignore_lines': False, + 'supports_generate_diff': True, + 'supports_replace': False + } + + def get_option_values(self): + return { + 'format': ['set', 'text'], + 'diff_match': ['line', 'none'], + 'diff_replace': [], + } + def get_capabilities(self): result = {} - result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes'] + result['rpc'] = self.get_base_rpc() + ['commit', 'discard_changes', 'get_diff'] result['network_api'] = 'cliconf' result['device_info'] = self.get_device_info() + result['device_operations'] = self.get_device_operations() + result.update(self.get_option_values()) return json.dumps(result) diff --git a/test/units/modules/network/vyos/test_vyos_config.py b/test/units/modules/network/vyos/test_vyos_config.py index b1bdeab6eb..bd73b7540e 100644 --- a/test/units/modules/network/vyos/test_vyos_config.py +++ b/test/units/modules/network/vyos/test_vyos_config.py @@ -20,8 +20,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.vyos import vyos_config +from ansible.plugins.cliconf.vyos import Cliconf from units.modules.utils import set_module_args from .vyos_module import TestVyosModule, load_fixture @@ -42,12 +43,23 @@ class TestVyosConfigModule(TestVyosModule): self.mock_run_commands = patch('ansible.modules.network.vyos.vyos_config.run_commands') self.run_commands = self.mock_run_commands.start() + self.mock_get_connection = patch('ansible.modules.network.vyos.vyos_config.get_connection') + self.get_connection = self.mock_get_connection.start() + + self.cliconf_obj = Cliconf(MagicMock()) + self.running_config = load_fixture('vyos_config_config.cfg') + + self.conn = self.get_connection() + self.conn.edit_config = MagicMock() + self.running_config = load_fixture('vyos_config_config.cfg') + def tearDown(self): super(TestVyosConfigModule, self).tearDown() self.mock_get_config.stop() self.mock_load_config.stop() self.mock_run_commands.stop() + self.mock_get_connection.stop() def load_fixtures(self, commands=None): config_file = 'vyos_config_config.cfg' @@ -56,6 +68,7 @@ class TestVyosConfigModule(TestVyosModule): def test_vyos_config_unchanged(self): src = load_fixture('vyos_config_config.cfg') + self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, src)) set_module_args(dict(src=src)) self.execute_module() @@ -63,12 +76,14 @@ class TestVyosConfigModule(TestVyosModule): src = load_fixture('vyos_config_src.cfg') set_module_args(dict(src=src)) commands = ['set system host-name foo', 'delete interfaces ethernet eth0 address'] + self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, self.running_config)) self.execute_module(changed=True, commands=commands) def test_vyos_config_src_brackets(self): src = load_fixture('vyos_config_src_brackets.cfg') set_module_args(dict(src=src)) commands = ['set interfaces ethernet eth0 address 10.10.10.10/24', 'set system host-name foo'] + self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(src, self.running_config)) self.execute_module(changed=True, commands=commands) def test_vyos_config_backup(self): @@ -79,16 +94,22 @@ class TestVyosConfigModule(TestVyosModule): def test_vyos_config_lines(self): commands = ['set system host-name foo'] set_module_args(dict(lines=commands)) + candidate = '\n'.join(commands) + self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate, self.running_config)) self.execute_module(changed=True, commands=commands) def test_vyos_config_config(self): config = 'set system host-name localhost' new_config = ['set system host-name router'] set_module_args(dict(lines=new_config, config=config)) + candidate = '\n'.join(new_config) + self.conn.get_diff = MagicMock(return_value=self.cliconf_obj.get_diff(candidate, config)) self.execute_module(changed=True, commands=new_config) def test_vyos_config_match_none(self): lines = ['set system interfaces ethernet eth0 address 1.2.3.4/24', '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.execute_module(changed=True, commands=lines, sort=False)