diff --git a/changelogs/fragments/T7391_domain_search.yaml b/changelogs/fragments/T7391_domain_search.yaml new file mode 100644 index 00000000..17f2c4be --- /dev/null +++ b/changelogs/fragments/T7391_domain_search.yaml @@ -0,0 +1,4 @@ +--- +trivial: + - vyos_system - Added support for domain_search for 1.4+ + - test_vyos_system - Added test for domain_search diff --git a/plugins/module_utils/network/vyos/vyos.py b/plugins/module_utils/network/vyos/vyos.py index 5c157818..6bd8daee 100644 --- a/plugins/module_utils/network/vyos/vyos.py +++ b/plugins/module_utils/network/vyos/vyos.py @@ -1,113 +1,114 @@ # This code is part of Ansible, but is an independent component. # This particular file snippet, and this file snippet only, is BSD licensed. # Modules you write using this snippet, which is embedded dynamically by Ansible # still belong to the author of the module, and may assign their own license # to the complete work. # # (c) 2016 Red Hat Inc. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # 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. # from __future__ import absolute_import, division, print_function __metaclass__ = type import json from ansible.module_utils._text import to_text from ansible.module_utils.connection import Connection, ConnectionError _DEVICE_CONFIGS = {} def get_connection(module): if hasattr(module, "_vyos_connection"): return module._vyos_connection capabilities = get_capabilities(module) network_api = capabilities.get("network_api") if network_api == "cliconf": module._vyos_connection = Connection(module._socket_path) else: module.fail_json(msg="Invalid connection type %s" % network_api) return module._vyos_connection def get_capabilities(module): if hasattr(module, "_vyos_capabilities"): return module._vyos_capabilities try: capabilities = Connection(module._socket_path).get_capabilities() except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) module._vyos_capabilities = json.loads(capabilities) return module._vyos_capabilities def get_config(module, flags=None, format=None): flags = [] if flags is None else flags global _DEVICE_CONFIGS + # If _DEVICE_CONFIGS is non-empty and module.params["match"] is "none", # return the cached device configurations. This avoids redundant calls # to the connection when no specific match criteria are provided. if _DEVICE_CONFIGS != {} and ( module.params["match"] is not None and module.params["match"] == "none" ): return to_text(_DEVICE_CONFIGS) else: connection = get_connection(module) try: out = connection.get_config(flags=flags, format=format) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) cfg = to_text(out, errors="surrogate_then_replace").strip() _DEVICE_CONFIGS = cfg return cfg def run_commands(module, commands, check_rc=True): connection = get_connection(module) try: response = connection.run_commands(commands=commands, check_rc=check_rc) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) return response def load_config(module, commands, commit=False, comment=None): connection = get_connection(module) try: response = connection.edit_config(candidate=commands, commit=commit, comment=comment) except ConnectionError as exc: module.fail_json(msg=to_text(exc, errors="surrogate_then_replace")) return response.get("diff") def get_os_version(module): connection = get_connection(module) if connection.get_device_info(): os_version = connection.get_device_info()["network_os_major_version"] return os_version diff --git a/plugins/modules/vyos_system.py b/plugins/modules/vyos_system.py index 96a0e9bc..d8d676e8 100644 --- a/plugins/modules/vyos_system.py +++ b/plugins/modules/vyos_system.py @@ -1,216 +1,224 @@ #!/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 . # + from __future__ import absolute_import, division, print_function __metaclass__ = type DOCUMENTATION = """ module: vyos_system 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. version_added: 1.0.0 extends_documentation_fragment: - vyos.vyos.vyos notes: - Tested against VyOS 1.1.8 (helium). - This module works with connection C(ansible.netcommon.network_cli). See L(the VyOS OS Platform Options,../network/user_guide/platform_vyos.html). options: host_name: description: - Configure the device hostname parameter. This option takes an ASCII string value. type: str domain_name: description: - The new domain name to apply to the device. type: str name_server: description: - A list of name servers to use with the device. Mutually exclusive with I(domain_search) type: list elements: str aliases: - name_servers domain_search: description: - A list of domain names to search. Mutually exclusive with I(name_server) type: list elements: str state: description: - Whether to apply (C(present)) or remove (C(absent)) the settings. default: present type: str 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 """ EXAMPLES = """ - name: configure hostname and domain-name vyos.vyos.vyos_system: host_name: vyos01 domain_name: test.example.com - name: remove all configuration vyos.vyos.vyos_system: state: absent - name: configure name servers vyos.vyos.vyos_system: name_servers - 8.8.8.8 - 8.8.4.4 - name: configure domain search suffixes vyos.vyos.vyos_system: domain_search: - sub1.example.com - sub2.example.com """ +from re import M, findall from ansible.module_utils.basic import AnsibleModule +from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.version import ( + LooseVersion, +) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.vyos import ( get_config, + get_os_version, load_config, ) -def spec_key_to_device_key(key): +def spec_key_to_device_key(key, module): device_key = key.replace("_", "-") - # domain-search is longer than just it's key + # domain-search differs in 1.3- and 1.4+ if device_key == "domain-search": - device_key += " domain" + if LooseVersion(get_os_version(module)) <= LooseVersion("1.3"): + 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]) - + config_line = findall(r"^set system\s+(\S+)(?:\s+domain)?\s+'([^']+)'", line, M) + if config_line: + if config_line[0][0] == "host-name": + config["host_name"] = config_line[0][1] + elif config_line[0][0] == "domain-name": + config["domain_name"] = config_line[0][1] + elif config_line[0][0] == "domain-search": + config["domain_search"].append(config_line[0][1]) + elif config_line[0][0] == "name-server": + config["name_server"].append(config_line[0][1]) return config -def spec_to_commands(want, have): +def spec_to_commands(want, have, module): 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)) + commands.append("delete system %s" % spec_key_to_device_key(key, module)) 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) + device_key = spec_key_to_device_key(key, module) # 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 map_param_to_obj(module): return { "host_name": module.params["host_name"], "domain_name": module.params["domain_name"], "domain_search": module.params["domain_search"], "name_server": module.params["name_server"], "state": module.params["state"], } def main(): argument_spec = dict( host_name=dict(type="str"), domain_name=dict(type="str"), domain_search=dict(type="list", elements="str"), name_server=dict(type="list", aliases=["name_servers"], elements="str"), state=dict(type="str", default="present", choices=["present", "absent"]), ) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, mutually_exclusive=[("domain_name", "domain_search")], ) warnings = list() result = {"changed": False, "warnings": warnings} want = map_param_to_obj(module) have = config_to_dict(module) - commands = spec_to_commands(want, have) + commands = spec_to_commands(want, have, module) result["commands"] = commands if commands: commit = not module.check_mode load_config(module, commands, commit=commit) result["changed"] = True module.exit_json(**result) if __name__ == "__main__": main() diff --git a/tests/integration/targets/vyos_system/tests/cli/domain_search.yaml b/tests/integration/targets/vyos_system/tests/cli/domain_search.yaml new file mode 100644 index 00000000..2422d2c7 --- /dev/null +++ b/tests/integration/targets/vyos_system/tests/cli/domain_search.yaml @@ -0,0 +1,43 @@ +--- +- debug: msg="START cli/domain_search.yaml on connection={{ ansible_connection }}" + +- name: ensure facts + include_tasks: _get_version.yaml + +- name: setup + ignore_errors: true + vyos.vyos.vyos_system: + domain_search: + - nbg.bufanda.ke + state: absent + +- name: configure domain search setting + register: result + vyos.vyos.vyos_system: + domain_search: + - nbg.bufanda.ke + +- assert: + that: + - result.changed == true + - result.commands|length == 1 + - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + +- name: configure domain search setting + register: result + vyos.vyos.vyos_system: + domain_search: + - nbg.bufanda.ke + +- assert: + that: + - result.changed == false + +- name: teardown + ignore_errors: true + vyos.vyos.vyos_system: + domain_search: + - nbg.bufanda.ke + state: absent + +- debug: msg="END cli/basic.yaml on connection={{ ansible_connection }}" diff --git a/tests/integration/targets/vyos_system/vars/pre-v1_4.yaml b/tests/integration/targets/vyos_system/vars/pre-v1_4.yaml new file mode 100644 index 00000000..cb41c9c6 --- /dev/null +++ b/tests/integration/targets/vyos_system/vars/pre-v1_4.yaml @@ -0,0 +1,4 @@ +--- +merged: + commands: + - set system domain-search domain 'nbg.bufanda.ke' diff --git a/tests/integration/targets/vyos_system/vars/v1_4.yaml b/tests/integration/targets/vyos_system/vars/v1_4.yaml new file mode 100644 index 00000000..96f0b7c9 --- /dev/null +++ b/tests/integration/targets/vyos_system/vars/v1_4.yaml @@ -0,0 +1,4 @@ +--- +merged: + commands: + - set system domain-search 'nbg.bufanda.ke' diff --git a/tests/unit/modules/network/vyos/test_vyos_system.py b/tests/unit/modules/network/vyos/test_vyos_system.py index cf405cab..5edfa0df 100644 --- a/tests/unit/modules/network/vyos/test_vyos_system.py +++ b/tests/unit/modules/network/vyos/test_vyos_system.py @@ -1,114 +1,193 @@ # (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 from unittest.mock import patch from ansible_collections.vyos.vyos.plugins.modules import vyos_system from ansible_collections.vyos.vyos.tests.unit.modules.utils import set_module_args from .vyos_module import TestVyosModule, load_fixture class TestVyosSystemModule(TestVyosModule): module = vyos_system def setUp(self): super(TestVyosSystemModule, self).setUp() self.mock_get_config = patch( "ansible_collections.vyos.vyos.plugins.modules.vyos_system.get_config", ) self.get_config = self.mock_get_config.start() self.mock_load_config = patch( "ansible_collections.vyos.vyos.plugins.modules.vyos_system.load_config", ) self.load_config = self.mock_load_config.start() + self.mock_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.modules.vyos_system.get_os_version", + ) + self.test_version = "1.2" + self.get_os_version = self.mock_get_os_version.start() + self.get_os_version.return_value = self.test_version + self.mock_facts_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.modules.vyos_system.get_os_version", + ) + self.get_facts_os_version = self.mock_facts_get_os_version.start() + self.get_facts_os_version.return_value = self.test_version + self.maxDiff = None + def tearDown(self): super(TestVyosSystemModule, self).tearDown() self.mock_get_config.stop() self.mock_load_config.stop() + self.mock_get_os_version.stop() + self.mock_facts_get_os_version.stop() def load_fixtures(self, commands=None, filename=None): self.get_config.return_value = load_fixture("vyos_config_config.cfg") def test_vyos_system_hostname(self): set_module_args(dict(host_name="foo")) commands = ["set system host-name 'foo'"] self.execute_module(changed=True, commands=commands) def test_vyos_system_clear_hostname(self): set_module_args(dict(host_name="foo", state="absent")) commands = ["delete system host-name"] self.execute_module(changed=True, commands=commands) def test_vyos_remove_single_name_server(self): set_module_args(dict(name_server=["8.8.4.4"], state="absent")) commands = ["delete system name-server '8.8.4.4'"] self.execute_module(changed=True, commands=commands) def test_vyos_system_domain_name(self): set_module_args(dict(domain_name="example2.com")) commands = ["set system domain-name 'example2.com'"] self.execute_module(changed=True, commands=commands) def test_vyos_system_clear_domain_name(self): set_module_args(dict(domain_name="example.com", state="absent")) commands = ["delete system domain-name"] self.execute_module(changed=True, commands=commands) def test_vyos_system_domain_search(self): set_module_args(dict(domain_search=["foo.example.com", "bar.example.com"])) commands = [ "set system domain-search domain 'foo.example.com'", "set system domain-search domain 'bar.example.com'", ] self.execute_module(changed=True, commands=commands) def test_vyos_system_clear_domain_search(self): set_module_args(dict(domain_search=[])) commands = ["delete system domain-search domain"] self.execute_module(changed=True, commands=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")) commands = [ "delete system host-name", "delete system domain-search domain", "delete system domain-name", "delete system name-server", ] self.execute_module(changed=True, commands=commands) + + +class TestVyosSystemModule14(TestVyosModule): + module = vyos_system + + def setUp(self): + super(TestVyosSystemModule14, self).setUp() + + self.mock_get_config = patch( + "ansible_collections.vyos.vyos.plugins.modules.vyos_system.get_config", + ) + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch( + "ansible_collections.vyos.vyos.plugins.modules.vyos_system.load_config", + ) + self.load_config = self.mock_load_config.start() + + self.mock_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.modules.vyos_system.get_os_version", + ) + self.test_version = "1.4" + self.get_os_version = self.mock_get_os_version.start() + self.get_os_version.return_value = self.test_version + self.mock_facts_get_os_version = patch( + "ansible_collections.vyos.vyos.plugins.modules.vyos_system.get_os_version", + ) + self.get_facts_os_version = self.mock_facts_get_os_version.start() + self.get_facts_os_version.return_value = self.test_version + self.maxDiff = None + + def tearDown(self): + super(TestVyosSystemModule14, self).tearDown() + + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_get_os_version.stop() + self.mock_facts_get_os_version.stop() + + def load_fixtures(self, commands=None, filename=None): + self.get_config.return_value = load_fixture("vyos_config_config.cfg") + + def test_vyos_system_domain_search(self): + set_module_args(dict(domain_search=["foo.example.com", "bar.example.com"])) + commands = [ + "set system domain-search 'foo.example.com'", + "set system domain-search 'bar.example.com'", + ] + self.execute_module(changed=True, commands=commands) + + def test_vyos_system_clear_domain_search(self): + set_module_args(dict(domain_search=[])) + commands = ["delete system domain-search"] + self.execute_module(changed=True, commands=commands) + + def test_vyos_system_clear_all(self): + set_module_args(dict(state="absent")) + commands = [ + "delete system host-name", + "delete system domain-search", + "delete system domain-name", + "delete system name-server", + ] + self.execute_module(changed=True, commands=commands)