diff --git a/lib/ansible/module_utils/netcfg.py b/lib/ansible/module_utils/netcfg.py index 139064d972..7b32eb4c59 100644 --- a/lib/ansible/module_utils/netcfg.py +++ b/lib/ansible/module_utils/netcfg.py @@ -244,11 +244,11 @@ class NetworkConfig(object): for item in updates: for p in item._parents: - if p not in visited: - visited.add(p) + if p.line not in visited: + visited.add(p.line) expanded.append(p) expanded.append(item) - visited.add(item) + visited.add(item.line) return expanded diff --git a/lib/ansible/module_utils/network.py b/lib/ansible/module_utils/network.py index 03ba6ea289..c8c75e2713 100644 --- a/lib/ansible/module_utils/network.py +++ b/lib/ansible/module_utils/network.py @@ -25,13 +25,11 @@ # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE # USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # - -import itertools - from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback, get_exception from ansible.module_utils.netcli import Cli, Command from ansible.module_utils._text import to_native +from ansible.module_utils.six import iteritems NET_TRANSPORT_ARGS = dict( host=dict(required=True), @@ -54,6 +52,12 @@ NET_CONNECTION_ARGS = dict() NET_CONNECTIONS = dict() +def _transitional_argument_spec(): + argument_spec = {} + for key, value in iteritems(NET_TRANSPORT_ARGS): + value['required'] = False + argument_spec[key] = value + return argument_spec def to_list(val): if isinstance(val, (list, tuple)): diff --git a/lib/ansible/modules/network/ios/_ios_template.py b/lib/ansible/modules/network/ios/_ios_template.py index f6ffb1fe3b..0b00a406f6 100644 --- a/lib/ansible/modules/network/ios/_ios_template.py +++ b/lib/ansible/modules/network/ios/_ios_template.py @@ -15,10 +15,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # -ANSIBLE_METADATA = {'status': ['deprecated'], - 'supported_by': 'community', - 'version': '1.0'} - +ANSIBLE_METADATA = { + 'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0' +} DOCUMENTATION = """ --- @@ -34,7 +35,9 @@ description: commands that are not already configured. The config source can be a set of commands or a template. deprecated: Deprecated in 2.2. Use M(ios_config) instead. -extends_documentation_fragment: ios +notes: + - Provider arguments are no longer supported. Network tasks should now + specify connection plugin network_cli instead. options: src: description: @@ -88,21 +91,15 @@ options: EXAMPLES = """ - name: push a configuration onto the device ios_template: - host: hostname - username: foo src: config.j2 - name: forceable push a configuration onto the device ios_template: - host: hostname - username: foo src: config.j2 force: yes - name: provide the base configuration for comparison ios_template: - host: hostname - username: foo src: candidate_config.txt config: current_config.txt """ @@ -113,28 +110,51 @@ updates: returned: always type: list sample: ['...', '...'] - -responses: - description: The set of responses from issuing the commands on the device - returned: when not check_mode - type: list - sample: ['...', '...'] +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" """ -import ansible.module_utils.ios +from ansible.module_utils.local import LocalAnsibleModule from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.ios import NetworkModule +from ansible.module_utils.ios import get_config, load_config +from ansible.module_utils.network import NET_TRANSPORT_ARGS, _transitional_argument_spec -def get_config(module): - config = module.params['config'] or dict() - defaults = module.params['include_defaults'] - if not config and not module.params['force']: - config = module.config.get_config(include_defaults=defaults) - return config + +def check_args(module): + warnings = list() + 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 + return warnings + +def get_current_config(module): + if module.params['config']: + return module.params['config'] + if module.params['include_defaults']: + flags = ['all'] + else: + flags = [] + return get_config(flags=flags) def main(): """ main entry point for module execution """ - argument_spec = dict( src=dict(), force=dict(default=False, type='bool'), @@ -143,37 +163,44 @@ def main(): config=dict(), ) + # Removed the use of provider arguments in 2.3 due to network_cli + # connection plugin. To be removed in 2.5 + argument_spec.update(_transitional_argument_spec()) + mutually_exclusive = [('config', 'backup'), ('config', 'force')] - module = NetworkModule(argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) + module = LocalAnsibleModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) - result = dict(changed=False) + warnings = check_args(module) + + result = dict(changed=False, warnings=warnings) candidate = NetworkConfig(contents=module.params['src'], indent=1) - contents = get_config(module) - if contents: - config = NetworkConfig(contents=contents, indent=1) - result['_backup'] = str(contents) + result = {'changed': False} + + if module.params['backup']: + result['__backup__'] = get_config() if not module.params['force']: - commands = candidate.difference(config) + contents = get_current_config(module) + configobj = NetworkConfig(contents=contents, indent=1) + commands = candidate.difference(configobj) commands = dumps(commands, 'commands').split('\n') - commands = [str(c) for c in commands if c] + commands = [str(c).strip() for c in commands if c] else: - commands = str(candidate).split('\n') + commands = [c.strip() for c in str(candidate).split('\n')] if commands: if not module.check_mode: - response = module.config(commands) - result['responses'] = response + load_config(commands) result['changed'] = True result['updates'] = commands - module.exit_json(**result) + module.exit_json(**result) if __name__ == '__main__': main() diff --git a/lib/ansible/plugins/action/ios_template.py b/lib/ansible/plugins/action/ios_template.py index 5334b644d3..39d87ece50 100644 --- a/lib/ansible/plugins/action/ios_template.py +++ b/lib/ansible/plugins/action/ios_template.py @@ -1,5 +1,5 @@ # -# Copyright 2015 Peter Sprygada +# (c) 2016 Red Hat Inc. # # This file is part of Ansible # @@ -19,10 +19,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from ansible.plugins.action import ActionBase -from ansible.plugins.action.net_template import ActionModule as NetActionModule +from ansible.plugins.action.net_template import ActionModule as _ActionModule -class ActionModule(NetActionModule, ActionBase): +class ActionModule(_ActionModule): pass - - diff --git a/test/units/modules/network/ios/fixtures/ios_template_config.cfg b/test/units/modules/network/ios/fixtures/ios_template_config.cfg new file mode 100644 index 0000000000..afad9d08aa --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_template_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/ios/fixtures/ios_template_defaults.cfg b/test/units/modules/network/ios/fixtures/ios_template_defaults.cfg new file mode 100644 index 0000000000..e54645ab14 --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_template_defaults.cfg @@ -0,0 +1,13 @@ +! +hostname router +! +interface GigabitEthernet0/0 + ip address 1.2.3.4 255.255.255.0 + description test string + no shutdown +! +interface GigabitEthernet0/1 + ip address 6.7.8.9 255.255.255.0 + description test string + shutdown +! diff --git a/test/units/modules/network/ios/fixtures/ios_template_src.cfg b/test/units/modules/network/ios/fixtures/ios_template_src.cfg new file mode 100644 index 0000000000..b3d8961a99 --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_template_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/ios/test_ios_template.py b/test/units/modules/network/ios/test_ios_template.py new file mode 100644 index 0000000000..d1e59aafdf --- /dev/null +++ b/test/units/modules/network/ios/test_ios_template.py @@ -0,0 +1,140 @@ +# +# (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.ios import _ios_template +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 TestIosTemplateModule(unittest.TestCase): + + def setUp(self): + self.mock_get_config = patch('ansible.modules.network.ios._ios_template.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch('ansible.modules.network.ios._ios_template.load_config') + self.load_config = self.mock_load_config.start() + + def tearDown(self): + self.mock_get_config.stop() + self.mock_load_config.stop() + + def execute_module(self, failed=False, changed=False, commands=None, + sort=True, defaults=False): + + config_file = 'ios_template_defaults.cfg' if defaults else 'ios_template_config.cfg' + self.get_config.return_value = load_fixture(config_file) + self.load_config.return_value = None + + with self.assertRaises(AnsibleModuleExit) as exc: + _ios_template.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_ios_template_unchanged(self): + src = load_fixture('ios_template_config.cfg') + set_module_args(dict(src=src)) + self.execute_module() + + def test_ios_template_simple(self): + src = load_fixture('ios_template_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_ios_template_force(self): + src = load_fixture('ios_template_config.cfg') + set_module_args(dict(src=src, force=True)) + commands = [str(s).strip() for s in src.split('\n') if s and s != '!'] + self.execute_module(changed=True, commands=commands) + self.assertFalse(self.get_config.called) + + def test_ios_template_include_defaults_false(self): + src = load_fixture('ios_template_config.cfg') + set_module_args(dict(src=src, include_defaults=False)) + self.execute_module() + self.get_config.assert_called_with(flags=[]) + + def test_ios_template_backup(self): + set_module_args(dict(backup=True)) + result = self.execute_module() + self.assertIn('__backup__', result) + + def test_ios_template_config(self): + src = load_fixture('ios_template_config.cfg') + config = 'hostname router' + set_module_args(dict(src=src, config=config)) + commands = ['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'] + self.execute_module(changed=True, commands=commands) + self.assertFalse(self.get_config.called)