diff --git a/lib/ansible/modules/network/vyos/vyos_system.py b/lib/ansible/modules/network/vyos/vyos_system.py new file mode 100644 index 0000000000..ec1c9dc241 --- /dev/null +++ b/lib/ansible/modules/network/vyos/vyos_system.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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': 'community', + 'version': '1.0' +} + +DOCUMENTATION = """ +--- +module: "vyos_system" +version_added: "2.3" +author: "Nathaniel Case (@qalthos)" +short_description: Run `set system` commands on VyOS devices +description: + - Runs one or more commands on remote devices running VyOS. + This module can also be introspected to validate key parameters before + returning successfully. +options: + hostname: + description: + - The new hostname to apply to the device. + required: false + default: null + domain_name: + description: + - The new domain name to apply to the device. + required: false + default: null + name_server: + description: + - A list of name servers to use with the device. Mutually exclusive with + I(domain_search) + required: false + default: null + domain_search: + description: + - A list of domain names to search. Mutually exclusive with + I(name_server) + required: false + default: null + state: + description: + - Whether to apply (C(present)) or remove (C(absent)) the settings. + required: false + default: present + choices: ['present', 'absent'] +""" + +RETURN = """ +commands: + description: The list of configuration mode commands to send to the device + returned: always + type: list + sample: + - set system hostname vyos01 + - set system domain-name foo.example.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" +""" + +EXAMPLES = """ +- name: configure hostname and domain-name + vyos_system: + hostname: vyos01 + domain_name: foo.example.com + +- name: remove all configuration + vyos_system: + state: absent + +- name: configure name servers + vyos_system: + name_server: + - 8.8.8.8 + - 8.8.4.4 + +- name: configure domain search suffixes + vyos_system: + domain_search: + - sub1.example.com + - sub2.example.com +""" + +from ansible.module_utils.local import LocalAnsibleModule +from ansible.module_utils.vyos import get_config, load_config + + +def spec_key_to_device_key(key): + device_key = key.replace('_', '-') + + # domain-search is longer than just it's key + if device_key == 'domain-search': + device_key += ' domain' + + return device_key + + +def config_to_dict(module): + data = get_config(module) + + config = {'domain_search': [], 'name_server': []} + + for line in data.split('\n'): + if line.startswith('set system host-name'): + config['host_name'] = line[22:-1] + elif line.startswith('set system domain-name'): + config['domain_name'] = line[24:-1] + elif line.startswith('set system domain-search domain'): + config['domain_search'].append(line[33:-1]) + elif line.startswith('set system name-server'): + config['name_server'].append(line[24:-1]) + + return config + + +def spec_to_commands(want, have): + commands = [] + + state = want.pop('state') + + # state='absent' by itself has special meaning + if state == 'absent' and all(v is None for v in want.values()): + # Clear everything + for key in have: + commands.append('delete system %s' % spec_key_to_device_key(key)) + + for key in want: + if want[key] is None: + continue + + current = have.get(key) + proposed = want[key] + device_key = spec_key_to_device_key(key) + + # These keys are lists which may need to be reconciled with the device + if key in ['domain_search', 'name_server']: + if not proposed: + # Empty list was passed, delete all values + commands.append("delete system %s" % device_key) + for config in proposed: + if state == 'absent' and config in current: + commands.append("delete system %s '%s'" % (device_key, config)) + elif state == 'present' and config not in current: + commands.append("set system %s '%s'" % (device_key, config)) + else: + if state == 'absent' and current and proposed: + commands.append('delete system %s' % device_key) + elif state == 'present' and proposed and proposed != current: + commands.append("set system %s '%s'" % (device_key, proposed)) + + return commands + + +def main(): + argument_spec = dict( + host_name=dict(type='str'), + domain_name=dict(type='str'), + domain_search=dict(type='list'), + name_server=dict(type='list'), + state=dict(type='str', default='present', choices=['present', 'absent']), + ) + + module = LocalAnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[('domain_name', 'domain_search')], + ) + + result = {'changed': False} + want = dict(module.params) + have = config_to_dict(module) + + commands = spec_to_commands(want, have) + result['commands'] = commands + + if commands: + commit = not module.check_mode + response = load_config(module, commands, commit=commit) + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/network/vyos/__init__.py b/test/units/modules/network/vyos/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/units/modules/network/vyos/fixtures/vyos_config_config.cfg b/test/units/modules/network/vyos/fixtures/vyos_config_config.cfg index e6d642c36c..fcef8ebdda 100644 --- a/test/units/modules/network/vyos/fixtures/vyos_config_config.cfg +++ b/test/units/modules/network/vyos/fixtures/vyos_config_config.cfg @@ -1,4 +1,8 @@ -set system host-name router +set system host-name 'router' +set system domain-name 'example.com' +set system domain-search domain 'example.com' +set system name-server '8.8.8.8' +set system name-server '8.8.4.4' set interfaces ethernet eth0 address '1.2.3.4/24' set interfaces ethernet eth0 description 'test string' set interfaces ethernet eth1 address '6.7.8.9/24' diff --git a/test/units/modules/network/vyos/test_vyos_system.py b/test/units/modules/network/vyos/test_vyos_system.py new file mode 100644 index 0000000000..da0f5b0fc6 --- /dev/null +++ b/test/units/modules/network/vyos/test_vyos_system.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# +# (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 + +import ansible.module_utils.basic +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch, MagicMock +from ansible.errors import AnsibleModuleExit +from ansible.modules.network.vyos import vyos_system +from ansible.module_utils._text import to_bytes + + +fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures') +fixture_data = {} + + +def set_module_args(args): + json_args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) + ansible.module_utils.basic._ANSIBLE_ARGS = to_bytes(json_args) + + +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 TestVyosSystemModule(unittest.TestCase): + + def setUp(self): + self.mock_get_config = patch('ansible.modules.network.vyos.vyos_system.get_config') + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch('ansible.modules.network.vyos.vyos_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('vyos_config_config.cfg') + + with self.assertRaises(AnsibleModuleExit) as exc: + vyos_system.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['commands']), result['commands']) + else: + self.assertEqual(commands, result['commands'], result['commands']) + + return result + + def test_vyos_system_hostname(self): + set_module_args(dict(host_name='foo')) + result = self.execute_module(changed=True) + self.assertIn("set system host-name 'foo'", result['commands']) + self.assertEqual(1, len(result['commands'])) + + def test_vyos_system_clear_hostname(self): + set_module_args(dict(host_name='foo', state='absent')) + result = self.execute_module(changed=True) + self.assertIn('delete system host-name', result['commands']) + self.assertEqual(1, len(result['commands'])) + + def test_vyos_remove_single_name_server(self): + set_module_args(dict(name_server=['8.8.4.4'], state='absent')) + result = self.execute_module(changed=True) + self.assertIn("delete system name-server '8.8.4.4'", result['commands']) + self.assertEqual(1, len(result['commands'])) + + def test_vyos_system_domain_name(self): + set_module_args(dict(domain_name='example2.com')) + result = self.execute_module(changed=True) + self.assertIn("set system domain-name 'example2.com'", result['commands']) + self.assertEqual(1, len(result['commands'])) + + def test_vyos_system_clear_domain_name(self): + set_module_args(dict(domain_name='example.com', state='absent')) + result = self.execute_module(changed=True) + self.assertIn('delete system domain-name', result['commands']) + self.assertEqual(1, len(result['commands'])) + + def test_vyos_system_domain_search(self): + set_module_args(dict(domain_search=['foo.example.com', 'bar.example.com'])) + result = self.execute_module(changed=True) + self.assertIn("set system domain-search domain 'foo.example.com'", result['commands']) + self.assertIn("set system domain-search domain 'bar.example.com'", result['commands']) + self.assertEqual(2, len(result['commands'])) + + def test_vyos_system_clear_domain_search(self): + set_module_args(dict(domain_search=[])) + result = self.execute_module(changed=True) + self.assertIn('delete system domain-search domain', result['commands']) + self.assertEqual(1, len(result['commands'])) + + def test_vyos_system_no_change(self): + set_module_args(dict(host_name='router', domain_name='example.com', name_server=['8.8.8.8', '8.8.4.4'])) + result = self.execute_module() + self.assertEqual([], result['commands']) + + def test_vyos_system_clear_all(self): + set_module_args(dict(state='absent')) + result = self.execute_module(changed=True) + self.assertEqual(4, len(result['commands']))