From 5a14f1d7057db381b3f96fc4aaba9d67c5a6356c Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 5 Jan 2017 09:02:29 -0500 Subject: [PATCH] adds new module ios_system (#19916) adds new module ios_system --- lib/ansible/modules/network/ios/ios_system.py | 386 ++++++++++++++++++ .../ios/fixtures/ios_system_config.cfg | 14 + .../modules/network/ios/test_ios_system.py | 170 ++++++++ 3 files changed, 570 insertions(+) create mode 100644 lib/ansible/modules/network/ios/ios_system.py create mode 100644 test/units/modules/network/ios/fixtures/ios_system_config.cfg create mode 100644 test/units/modules/network/ios/test_ios_system.py diff --git a/lib/ansible/modules/network/ios/ios_system.py b/lib/ansible/modules/network/ios/ios_system.py new file mode 100644 index 00000000000..9fe2610f554 --- /dev/null +++ b/lib/ansible/modules/network/ios/ios_system.py @@ -0,0 +1,386 @@ +#!/usr/bin/python +# +# 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 . +# + +ANSIBLE_METADATA = { + 'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0' +} + +DOCUMENTATION = """ +--- +module: ios_system +version_added: "2.3" +author: "Peter Sprygada (@privateip)" +short_description: Manage the system attributes on Cisco IOS devices +description: + - This module provides declarative management of node system attributes + on Cisco IOS devices. It provides an option to configure host system + parameters or remove those parameters from the device active + configuration. +options: + hostname: + description: + - The C(hostname) argument will configure the device hostname + parameter on Cisco IOS devices. The C(hostname) value is an + ASCII string value. + required: false + default: null + domain_name: + description: + - The C(description) argument will configure the IP domain name + on the remote device to the provided value. The C(domain_name) + argument should be in the dotted name form and will be + appended to the C(hostname) to create a fully-qualified + domain name + required: false + default: null + domain_search: + description: + - The C(domain_list) provides the list of domain suffixes to + append to the hostname for the purpose of doing name resolution. + This argument accepts a list of names and will be reconciled + with the current active configuration on the running node. + required: false + default: null + lookup_source: + description: + - The C(lookup_source) argument provides one or more source + interfaces to use for performing DNS lookups. The interface + provided in C(lookup_source) must be a valid interface configured + on the device. + required: false + default: null + lookup_enabled: + description: + - The C(lookup_enabled) argument provides administrative control + for enabling or disabling DNS lookups. When this argument is + set to True, lookups are performed and when it is set to False, + lookups are not performed. + required: false + default: null + choices: ['true', 'false'] + name_servers: + description: + - The C(name_serves) argument accepts a list of DNS name servers by + way of either FQDN or IP address to use to perform name resolution + lookups. This argument accepts wither a list of DNS servers See + examples. + required: false + default: null + state: + description: + - The C(state) argument configures the state of the configuration + values in the device's current active configuration. When set + to I(present), the values should be configured in the device active + configuration and when set to I(absent) the values should not be + in the device active configuration + required: false + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = """ +- name: configure hostname and domain name + ios_system: + hostname: ios01 + domain_name: eng.ansible.com + domain-search: + - ansible.com + - redhat.com + - cisco.com +- name: remove configuration + ios_system: + state: absent +- name: configure DNS lookup sources + ios_system: + lookup_source: MgmtEth0/0/CPU0/0 + lookup_enabled: yes +- name: configure name servers + ios_system: + name_servers: + - 8.8.8.8 + - 8.8.4.4 +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - hostname ios01 + - ip domain name eng.ansible.com +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 re + +from ansible.module_utils.local import LocalAnsibleModule +from ansible.module_utils.ios import get_config, load_config +from ansible.module_utils.network_common import ComplexList + +_CONFIGURED_VRFS = None + +def has_vrf(module, vrf): + global _CONFIGURED_VRFS + if _CONFIGURED_VRFS is not None: + return vrf in _CONFIGURED_VRFS + config = get_config(module) + _CONFIGURED_VRFS = re.findall('vrf definition (\S+)', config) + return vrf in _CONFIGURED_VRFS + +def requires_vrf(module, vrf): + if not has_vrf(module, vrf): + module.fail_json(msg='vrf %s is not configured' % vrf) + +def diff_list(want, have): + adds = [w for w in want if w not in have] + removes = [h for h in have if h not in want] + return (adds, removes) + +def map_obj_to_commands(want, have, module): + commands = list() + state = module.params['state'] + + needs_update = lambda x: want.get(x) and (want.get(x) != have.get(x)) + + if state == 'absent': + if have['hostname'] != 'Router': + commands.append('no hostname') + + if have['lookup_source']: + commands.append('no ip domain lookup source-interface %s' % have['lookup_source']) + + if have['lookup_enabled'] is False: + commands.append('ip domain lookup') + + vrfs = set() + for item in have['domain_name']: + if item['vrf'] and item['vrf'] not in vrfs: + vrfs.add(item['vrf']) + commands.append('no ip domain name vrf %s' % item['vrf']) + elif None not in vrfs: + vrfs.add(None) + commands.append('no ip domain name') + + vrfs = set() + for item in have['domain_search']: + if item['vrf'] and item['vrf'] not in vrfs: + vrfs.add(item['vrf']) + commands.append('no ip domain list vrf %s' % item['vrf']) + elif None not in vrfs: + vrfs.add(None) + commands.append('no ip domain list') + + vrfs = set() + for item in have['name_servers']: + if item['vrf'] and item['vrf'] not in vrfs: + vrfs.add(item['vrf']) + commands.append('no ip name-server vrf %s' % item['vrf']) + elif None not in vrfs: + vrfs.add(None) + commands.append('no ip name-server') + + elif state == 'present': + if needs_update('hostname'): + commands.append('hostname %s' % want['hostname']) + + if needs_update('lookup_source'): + commands.append('ip domain lookup source-interface %s' % want['lookup_source']) + + if needs_update('lookup_enabled'): + cmd = 'ip domain lookup' + if want['lookup_enabled'] is False: + cmd = 'no %s' % cmd + commands.append(cmd) + + if want['domain_name']: + adds, removes = diff_list(want['domain_name'], have['domain_name']) + for item in removes: + if item['vrf']: + commands.append('no ip domain name vrf %s %s' % (item['vrf'], item['name'])) + else: + commands.append('no ip domain name %s' % item['name']) + for item in adds: + if item['vrf']: + requires_vrf(module, item['vrf']) + commands.append('ip domain name vrf %s %s' % (item['vrf'], item['name'])) + else: + commands.append('ip domain name %s' % item['name']) + + if want['domain_search']: + adds, removes = diff_list(want['domain_search'], have['domain_search']) + for item in removes: + if item['vrf']: + commands.append('no ip domain list vrf %s %s' % (item['vrf'], item['name'])) + else: + commands.append('no ip domain list %s' % item['name']) + for item in adds: + if item['vrf']: + requires_vrf(module, item['vrf']) + commands.append('ip domain list vrf %s %s' % (item['vrf'], item['name'])) + else: + commands.append('ip domain list %s' % item['name']) + + + if want['name_servers']: + adds, removes = diff_list(want['name_servers'], have['name_servers']) + for item in removes: + if item['vrf']: + commands.append('no ip name-server vrf %s %s' % (item['vrf'], item['server'])) + else: + commands.append('no ip name-server %s' % item['server']) + for item in adds: + if item['vrf']: + requires_vrf(module, item['vrf']) + commands.append('ip name-server vrf %s %s' % (item['vrf'], item['server'])) + else: + commands.append('ip name-server %s' % item['server']) + + return commands + +def parse_hostname(config): + match = re.search('^hostname (\S+)', config, re.M) + return match.group(1) + +def parse_domain_name(config): + match = re.findall('^ip domain name (?:vrf (\S+) )*(\S+)', config, re.M) + matches = list() + for vrf, name in match: + if not vrf: + vrf = None + matches.append({'name': name, 'vrf': vrf}) + return matches + +def parse_domain_search(config): + match = re.findall('^ip domain list (?:vrf (\S+) )*(\S+)', config, re.M) + matches = list() + for vrf, name in match: + if not vrf: + vrf = None + matches.append({'name': name, 'vrf': vrf}) + return matches + +def parse_name_servers(config): + match = re.findall('^ip name-server (?:vrf (\S+) )*(\S+)', config, re.M) + matches = list() + for vrf, server in match: + if not vrf: + vrf = None + matches.append({'server': server, 'vrf': vrf}) + return matches + +def parse_lookup_source(config): + match = re.search('ip domain lookup source-interface (\S+)', config, re.M) + if match: + return match.group(1) + +def map_config_to_obj(module): + config = get_config(module) + return { + 'hostname': parse_hostname(config), + 'domain_name': parse_domain_name(config), + 'domain_search': parse_domain_search(config), + 'lookup_source': parse_lookup_source(config), + 'lookup_enabled': 'no ip domain lookup' not in config, + 'name_servers': parse_name_servers(config) + } + +def map_params_to_obj(module): + obj = { + 'hostname': module.params['hostname'], + 'lookup_source': module.params['lookup_source'], + 'lookup_enabled': module.params['lookup_enabled'], + } + + domain_name = ComplexList(dict( + name=dict(key=True), + vrf=dict() + )) + + domain_search = ComplexList(dict( + name=dict(key=True), + vrf=dict() + )) + + name_servers = ComplexList(dict( + server=dict(key=True), + vrf=dict() + )) + + for arg, cast in [('domain_name', domain_name), + ('domain_search', domain_search), + ('name_servers', name_servers)]: + + if module.params[arg]: + obj[arg] = cast(module.params[arg]) + else: + obj[arg] = None + + return obj + +def main(): + """ Main entry point for Ansible module execution + """ + argument_spec = dict( + hostname=dict(), + + domain_name=dict(type='list'), + domain_search=dict(type='list'), + name_servers=dict(type='list'), + + lookup_source=dict(), + lookup_enabled=dict(type='bool'), + + state=dict(choices=['present', 'absent'], default='present') + ) + + module = LocalAnsibleModule(argument_spec=argument_spec, + supports_check_mode=True) + + result = {'changed': False} + + want = map_params_to_obj(module) + have = map_config_to_obj(module) + + commands = map_obj_to_commands(want, have, module) + result['commands'] = commands + + if commands: + if not module.check_mode: + load_config(module, commands) + result['changed'] = True + + module.exit_json(**result) + +if __name__ == "__main__": + main() diff --git a/test/units/modules/network/ios/fixtures/ios_system_config.cfg b/test/units/modules/network/ios/fixtures/ios_system_config.cfg new file mode 100644 index 00000000000..3330b4aa345 --- /dev/null +++ b/test/units/modules/network/ios/fixtures/ios_system_config.cfg @@ -0,0 +1,14 @@ +! +hostname ios01 +! +ip domain list vrf management example.net +ip domain list example.net +ip domain list example.com +ip domain lookup source-interface GigabitEthernet0/0 +ip domain name vrf management eng.example.net +ip domain name eng.example.net +ip name-server vrf management 8.8.8.8 +ip name-server 8.8.8.8 +! +vrf definition test +! diff --git a/test/units/modules/network/ios/test_ios_system.py b/test/units/modules/network/ios/test_ios_system.py new file mode 100644 index 00000000000..5933461c094 --- /dev/null +++ b/test/units/modules/network/ios/test_ios_system.py @@ -0,0 +1,170 @@ +# +# (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_system +from ansible.module_utils import basic +#from ansible.module_utils.local import LocalAnsibleModule +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 TestIosSystemModule(unittest.TestCase): + + def setUp(self): + self.mock_get_config = patch('ansible.modules.network.ios.ios_system.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch('ansible.modules.network.ios.ios_system.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): + + self.get_config.return_value = load_fixture('ios_system_config.cfg') + self.load_config.return_value = None + + with self.assertRaises(AnsibleModuleExit) as exc: + ios_system.main() + + result = exc.exception.result + + if failed: + self.assertTrue(result['failed'], result) + else: + self.assertEqual(result['changed'], changed, result) + + if commands: + if sort: + self.assertEqual(sorted(commands), sorted(result['commands']), result['commands']) + else: + self.assertEqual(commands, result['commands']) + + return result + + def test_ios_system_hostname_changed(self): + set_module_args(dict(hostname='foo')) + commands = ['hostname foo'] + self.execute_module(changed=True, commands=commands) + + + def test_ios_system_domain_name(self): + set_module_args(dict(domain_name=['test.com'])) + commands = ['ip domain name test.com', + 'no ip domain name eng.example.net', + 'no ip domain name vrf management eng.example.net'] + self.execute_module(changed=True, commands=commands) + + def test_ios_system_domain_name_complex(self): + set_module_args(dict(domain_name=[{'name': 'test.com', 'vrf': 'test'}, + {'name': 'eng.example.net'}])) + commands = ['ip domain name vrf test test.com', + 'no ip domain name vrf management eng.example.net'] + self.execute_module(changed=True, commands=commands) + + def test_ios_system_domain_search(self): + set_module_args(dict(domain_search=['ansible.com', 'redhat.com'])) + commands = ['no ip domain list vrf management example.net', + 'no ip domain list example.net', + 'no ip domain list example.com', + 'ip domain list ansible.com', + 'ip domain list redhat.com'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_ios_system_domain_search_complex(self): + set_module_args(dict(domain_search=[{'name': 'ansible.com', 'vrf': 'test'}])) + commands = ['no ip domain list vrf management example.net', + 'no ip domain list example.net', + 'no ip domain list example.com', + 'ip domain list vrf test ansible.com'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_ios_system_lookup_source(self): + set_module_args(dict(lookup_source='Ethernet1')) + commands = ['ip domain lookup source-interface Ethernet1'] + self.execute_module(changed=True, commands=commands) + + def test_ios_system_name_servers(self): + name_servers = ['8.8.8.8', '8.8.4.4'] + set_module_args(dict(name_servers=name_servers)) + commands = ['no ip name-server vrf management 8.8.8.8', + 'ip name-server 8.8.4.4'] + self.execute_module(changed=True, commands=commands, sort=False) + + def rest_ios_system_name_servers_complex(self): + name_servers = dict(server='8.8.8.8', vrf='test') + set_module_args(dict(name_servers=name_servers)) + commands = ['no name-server 8.8.8.8', + 'no name-server vrf management 8.8.8.8', + 'ip name-server vrf test 8.8.8.8'] + self.execute_module(changed=True, commands=commands, sort=False) + + def test_ios_system_state_absent(self): + set_module_args(dict(state='absent')) + commands = ['no hostname', + 'no ip domain lookup source-interface GigabitEthernet0/0', + 'no ip domain list vrf management', 'no ip domain list', + 'no ip domain name vrf management', 'no ip domain name', + 'no ip name-server vrf management', 'no ip name-server'] + self.execute_module(changed=True, commands=commands) + + def test_ios_system_no_change(self): + set_module_args(dict(hostname='ios01')) + self.execute_module(commands=[]) + + def test_ios_system_missing_vrf(self): + name_servers = dict(server='8.8.8.8', vrf='missing') + set_module_args(dict(name_servers=name_servers)) + self.execute_module(failed=True) +