diff --git a/lib/ansible/module_utils/iosxr.py b/lib/ansible/module_utils/iosxr.py index b89d1febfef..a3f4183816f 100644 --- a/lib/ansible/module_utils/iosxr.py +++ b/lib/ansible/module_utils/iosxr.py @@ -55,7 +55,7 @@ def run_commands(module, commands, check_rc=True): responses.append(out) return responses -def load_config(module, commands, commit=False, replace=False): +def load_config(module, commands, commit=False, replace=False, comment=None): assert isinstance(commands, list), 'commands must be a list' rc, out, err = module.exec_command('configure terminal') @@ -79,8 +79,11 @@ def load_config(module, commands, commit=False, replace=False): rc, diff, err = module.exec_command('show commit changes diff') if commit: cmd = 'commit' + if comment: + cmd += ' comment {0}'.format(comment) else: cmd = 'abort' + diff = None module.exec_command(cmd) - return {'diff': diff} + return diff diff --git a/lib/ansible/modules/network/iosxr/iosxr_config.py b/lib/ansible/modules/network/iosxr/iosxr_config.py index d686f83111a..6e21e022eb0 100644 --- a/lib/ansible/modules/network/iosxr/iosxr_config.py +++ b/lib/ansible/modules/network/iosxr/iosxr_config.py @@ -24,7 +24,7 @@ DOCUMENTATION = """ --- module: iosxr_config version_added: "2.1" -author: "Peter Sprygada (@privateip)" +author: "Ricardo Carrillo Cruz (@rcarrillocruz)" short_description: Manage Cisco IOS XR configuration sections description: - Cisco IOS XR configurations use a simple block indent file syntax @@ -148,19 +148,9 @@ options: """ EXAMPLES = """ -# Note: examples below use the following provider dict to handle -# transport and authentication to the node. -vars: - cli: - host: "{{ inventory_hostname }}" - username: cisco - password: cisco - transport: cli - - name: configure top level configuration iosxr_config: lines: hostname {{ inventory_hostname }} - provider: "{{ cli }}" - name: configure interface settings iosxr_config: @@ -168,14 +158,12 @@ vars: - description test interface - ip address 172.31.1.1 255.255.255.0 parents: interface GigabitEthernet0/0/0/0 - provider: "{{ cli }}" - name: load a config from disk and replace the current config iosxr_config: src: config.cfg update: replace backup: yes - provider: "{{ cli }}" """ RETURN = """ @@ -189,10 +177,26 @@ backup_path: returned: when backup is yes type: path sample: /playbooks/ansible/backup/iosxr01.2016-07-16@22:28:34 +start: + description: The time the job started + returned: always + type: str + sample: "2016-11-16 10:38:15.126146" +end: + description: The time the job ended + returned: always + type: str + sample: "2016-11-16 10:38:25.595612" +delta: + description: The time elapsed to perform all operations + returned: always + type: str + sample: "0:00:10.469466" """ -from ansible.module_utils.basic import get_exception +from ansible.module_utils.local import LocalAnsibleModule from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.iosxr import NetworkModule, NetworkError +from ansible.module_utils.iosxr import load_config,get_config +from ansible.module_utils.network import NET_TRANSPORT_ARGS, _transitional_argument_spec DEFAULT_COMMIT_COMMENT = 'configured by iosxr_config' @@ -206,11 +210,18 @@ def check_args(module, warnings): 'match=none instead. This argument will be ' 'removed in the future') + for key in NET_TRANSPORT_ARGS: + if module.params[key]: + warnings.append( + 'network provider arguments are no longer supported. Please ' + 'use connection: network_cli for the task' + ) + break -def get_config(module, result): +def get_running_config(module): contents = module.params['config'] if not contents: - contents = module.config.get_config() + contents = get_config(module) return NetworkConfig(indent=1, contents=contents) def get_candidate(module): @@ -222,36 +233,28 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate -def load_config(module, commands, result): - replace = module.params['replace'] == 'config' - comment = module.params['comment'] - commit = not module.check_mode - - diff = module.config.load_config(commands, replace=replace, commit=commit, - comment=comment) - - if diff: - result['diff'] = dict(prepared=diff) - result['changed'] = True - def run(module, result): match = module.params['match'] replace = module.params['replace'] + replace_config = replace == 'config' path = module.params['parents'] + comment = module.params['comment'] + check_mode = module.check_mode candidate = get_candidate(module) if match != 'none' and replace != 'config': - config = get_config(module, result) - configobjs = candidate.difference(config, path=path, match=match, + contents = get_running_config(module) + configobj = NetworkConfig(contents=contents, indent=1) + commands = candidate.difference(configobj, path=path, match=match, replace=replace) else: - configobjs = candidate.items + commands = candidate.items - if configobjs: - commands = dumps(configobjs, 'commands').split('\n') + if commands: + commands = dumps(commands, 'commands').split('\n') - if module.params['lines']: + if any((module.params['lines'], module.params['src'])): if module.params['before']: commands[:0] = module.params['before'] @@ -260,7 +263,11 @@ def run(module, result): result['updates'] = commands - load_config(module, commands, result) + diff = load_config(module, commands, not check_mode, + replace_config, comment) + if diff: + result['diff'] = dict(prepared=diff) + result['changed'] = True def main(): """main entry point for module execution @@ -286,6 +293,8 @@ def main(): comment=dict(default=DEFAULT_COMMIT_COMMENT), ) + argument_spec.update(_transitional_argument_spec()) + mutually_exclusive = [('lines', 'src')] required_if = [('match', 'strict', ['lines']), @@ -293,11 +302,10 @@ def main(): ('replace', 'block', ['lines']), ('replace', 'config', ['src'])] - module = NetworkModule(argument_spec=argument_spec, - connect_on_load=False, - mutually_exclusive=mutually_exclusive, - required_if=required_if, - supports_check_mode=True) + module = LocalAnsibleModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + required_if=required_if, + supports_check_mode=True) if module.params['force'] is True: module.params['match'] = 'none' @@ -308,13 +316,9 @@ def main(): result = dict(changed=False, warnings=warnings) if module.params['backup']: - result['__backup__'] = module.config.get_config() + result['__backup__'] = get_config(module) - try: - run(module, result) - except NetworkError: - exc = get_exception() - module.fail_json(msg=str(exc), **exc.kwargs) + run(module, result) module.exit_json(**result) diff --git a/test/units/modules/network/iosxr/fixtures/iosxr_config_config.cfg b/test/units/modules/network/iosxr/fixtures/iosxr_config_config.cfg new file mode 100644 index 00000000000..afad9d08aa7 --- /dev/null +++ b/test/units/modules/network/iosxr/fixtures/iosxr_config_config.cfg @@ -0,0 +1,12 @@ +! +hostname router +! +interface GigabitEthernet0/0 + ip address 1.2.3.4 255.255.255.0 + description test string +! +interface GigabitEthernet0/1 + ip address 6.7.8.9 255.255.255.0 + description test string + shutdown +! diff --git a/test/units/modules/network/iosxr/fixtures/iosxr_config_src.cfg b/test/units/modules/network/iosxr/fixtures/iosxr_config_src.cfg new file mode 100644 index 00000000000..b3d8961a99c --- /dev/null +++ b/test/units/modules/network/iosxr/fixtures/iosxr_config_src.cfg @@ -0,0 +1,11 @@ +! +hostname foo +! +interface GigabitEthernet0/0 + no ip address +! +interface GigabitEthernet0/1 + ip address 6.7.8.9 255.255.255.0 + description test string + shutdown +! diff --git a/test/units/modules/network/iosxr/test_iosxr_config.py b/test/units/modules/network/iosxr/test_iosxr_config.py new file mode 100644 index 00000000000..c77c040bde3 --- /dev/null +++ b/test/units/modules/network/iosxr/test_iosxr_config.py @@ -0,0 +1,178 @@ +# +# (c) 2016 Red Hat Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import json + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch, MagicMock +from ansible.errors import AnsibleModuleExit +from ansible.modules.network.iosxr import iosxr_config +from ansible.module_utils import basic +from ansible.module_utils._text import to_bytes + + +def set_module_args(args): + args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + basic._ANSIBLE_ARGS = to_bytes(args) + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + +def load_fixture(name): + path = os.path.join(fixture_path, name) + + if path in fixture_data: + return fixture_data[path] + + with open(path) as f: + data = f.read() + + try: + data = json.loads(data) + except: + pass + + fixture_data[path] = data + return data + + +class TestIosxrConfigModule(unittest.TestCase): + + def setUp(self): + self.patcher_get_config = patch('ansible.modules.network.iosxr.iosxr_config.get_config') + self.mock_get_config = self.patcher_get_config.start() + self.patcher_exec_command = patch('ansible.modules.network.iosxr.iosxr_config.LocalAnsibleModule.exec_command') + self.mock_exec_command = self.patcher_exec_command.start() + + def tearDown(self): + self.patcher_get_config.stop() + self.patcher_exec_command.stop() + + def execute_module(self, failed=False, changed=False, commands=None, + sort=True): + + config_file = 'iosxr_config_config.cfg' + self.mock_get_config.return_value = load_fixture(config_file) + self.mock_exec_command.return_value = (0, 'dummy diff', None) + + with self.assertRaises(AnsibleModuleExit) as exc: + iosxr_config.main() + + result = exc.exception.result + + if failed: + self.assertTrue(result['failed'], result) + else: + self.assertEqual(result.get('changed'), changed, result) + + if commands: + if sort: + self.assertEqual(sorted(commands), sorted(result['updates']), result['updates']) + else: + self.assertEqual(commands, result['updates'], result['updates']) + + return result + + def test_iosxr_config_unchanged(self): + src = load_fixture('iosxr_config_config.cfg') + set_module_args(dict(src=src)) + self.execute_module() + + def test_iosxr_config_src(self): + src = load_fixture('iosxr_config_src.cfg') + set_module_args(dict(src=src)) + commands = ['hostname foo', 'interface GigabitEthernet0/0', + 'no ip address'] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_config_backup(self): + set_module_args(dict(backup=True)) + result = self.execute_module() + self.assertIn('__backup__', result) + + def test_iosxr_config_lines_wo_parents(self): + set_module_args(dict(lines=['hostname foo'])) + commands = ['hostname foo'] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_config_lines_w_parents(self): + set_module_args(dict(lines=['shutdown'], parents=['interface GigabitEthernet0/0'])) + commands = ['interface GigabitEthernet0/0', 'shutdown'] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_config_before(self): + set_module_args(dict(lines=['hostname foo'], before=['test1','test2'])) + commands = ['test1', 'test2', 'hostname foo'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_iosxr_config_after(self): + set_module_args(dict(lines=['hostname foo'], after=['test1','test2'])) + commands = ['hostname foo', 'test1', 'test2'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_iosxr_config_before_after_no_change(self): + set_module_args(dict(lines=['hostname router'], + before=['test1', 'test2'], + after=['test3','test4'])) + self.execute_module() + + def test_iosxr_config_config(self): + config = 'hostname localhost' + set_module_args(dict(lines=['hostname router'], config=config)) + commands = ['hostname router'] + self.execute_module(changed=True, commands=commands) + + def test_iosxr_config_replace_block(self): + lines = ['description test string', 'test string'] + parents = ['interface GigabitEthernet0/0'] + set_module_args(dict(lines=lines, replace='block', parents=parents)) + commands = parents + lines + self.execute_module(changed=True, commands=commands) + + def test_iosxr_config_force(self): + lines = ['hostname router'] + set_module_args(dict(lines=lines, force=True)) + self.execute_module(changed=True, commands=lines) + + def test_iosxr_config_match_none(self): + lines = ['ip address 1.2.3.4 255.255.255.0', 'description test string'] + parents = ['interface GigabitEthernet0/0'] + set_module_args(dict(lines=lines, parents=parents, match='none')) + commands = parents + lines + self.execute_module(changed=True, commands=commands, sort=False) + + def test_iosxr_config_match_strict(self): + lines = ['ip address 1.2.3.4 255.255.255.0', 'description test string', + 'shutdown'] + parents = ['interface GigabitEthernet0/0'] + set_module_args(dict(lines=lines, parents=parents, match='strict')) + commands = parents + ['shutdown'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_iosxr_config_match_exact(self): + lines = ['ip address 1.2.3.4 255.255.255.0', 'description test string', + 'shutdown'] + parents = ['interface GigabitEthernet0/0'] + set_module_args(dict(lines=lines, parents=parents, match='exact')) + commands = parents + lines + self.execute_module(changed=True, commands=commands, sort=False)