diff --git a/changelogs/fragments/86-openvpn_vtu_interface.yaml b/changelogs/fragments/86-openvpn_vtu_interface.yaml new file mode 100644 index 0000000..287f05b --- /dev/null +++ b/changelogs/fragments/86-openvpn_vtu_interface.yaml @@ -0,0 +1,3 @@ +--- +bugfixes: + - Added openvpn vtu interface support. diff --git a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py index 358dd9d..13f22e1 100644 --- a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py @@ -1,133 +1,138 @@ # # -*- coding: utf-8 -*- # Copyright 2019 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """ The vyos interfaces fact class It is in this file the configuration is collected from the device for a given resource, parsed, and the facts tree is populated based on the configuration. """ from __future__ import absolute_import, division, print_function __metaclass__ = type from re import findall, M from copy import deepcopy from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import ( utils, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.interfaces.interfaces import ( InterfacesArgs, ) class InterfacesFacts(object): """The vyos interfaces fact class""" def __init__(self, module, subspec="config", options="options"): self._module = module self.argument_spec = InterfacesArgs.argument_spec spec = deepcopy(self.argument_spec) if subspec: if options: facts_argument_spec = spec[subspec][options] else: facts_argument_spec = spec[subspec] else: facts_argument_spec = spec self.generated_spec = utils.generate_dict(facts_argument_spec) + def get_device_data(self, connection): + + data = connection.get_config(flags=["| grep interfaces"]) + return data + def populate_facts(self, connection, ansible_facts, data=None): """Populate the facts for interfaces :param connection: the device connection :param ansible_facts: Facts dictionary :param data: previously collected conf :rtype: dictionary :returns: facts """ if not data: - data = connection.get_config(flags=["| grep interfaces"]) + data = self.get_device_data(connection) objs = [] interface_names = findall( - r"^set interfaces (?:ethernet|bonding|vti|loopback|vxlan) (?:\'*)(\S+)(?:\'*)", + r"^set interfaces (?:ethernet|bonding|vti|loopback|vxlan|openvpn) (?:\'*)(\S+)(?:\'*)", data, M, ) if interface_names: for interface in set(interface_names): intf_regex = r" %s .+$" % interface.strip("'") cfg = findall(intf_regex, data, M) obj = self.render_config(cfg) obj["name"] = interface.strip("'") if obj: objs.append(obj) facts = {} if objs: facts["interfaces"] = [] params = utils.validate_config( self.argument_spec, {"config": objs} ) for cfg in params["config"]: facts["interfaces"].append(utils.remove_empties(cfg)) ansible_facts["ansible_network_resources"].update(facts) return ansible_facts def render_config(self, conf): """ Render config as dictionary structure and delete keys from spec for null values :param spec: The facts tree, generated from the argspec :param conf: The configuration :rtype: dictionary :returns: The generated config """ vif_conf = "\n".join(filter(lambda x: ("vif" in x), conf)) eth_conf = "\n".join(filter(lambda x: ("vif" not in x), conf)) config = self.parse_attribs( ["description", "speed", "mtu", "duplex"], eth_conf ) config["vifs"] = self.parse_vifs(vif_conf) return utils.remove_empties(config) def parse_vifs(self, conf): vif_names = findall(r"vif (?:\'*)(\d+)(?:\'*)", conf, M) vifs_list = None if vif_names: vifs_list = [] for vif in set(vif_names): vif_regex = r" %s .+$" % vif cfg = "\n".join(findall(vif_regex, conf, M)) obj = self.parse_attribs(["description", "mtu"], cfg) obj["vlan_id"] = int(vif) if obj: vifs_list.append(obj) vifs_list = sorted(vifs_list, key=lambda i: i["vlan_id"]) return vifs_list def parse_attribs(self, attribs, conf): config = {} for item in attribs: value = utils.parse_conf_arg(conf, item) if value and item == "mtu": config[item] = int(value.strip("'")) elif value: config[item] = value.strip("'") else: config[item] = None if "disable" in conf: config["enabled"] = False else: config["enabled"] = True return utils.remove_empties(config) diff --git a/plugins/module_utils/network/vyos/utils/utils.py b/plugins/module_utils/network/vyos/utils/utils.py index bcf6fc2..7ce4688 100644 --- a/plugins/module_utils/network/vyos/utils/utils.py +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -1,268 +1,270 @@ # -*- coding: utf-8 -*- # Copyright 2019 Red Hat # GNU General Public License v3.0+ # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # utils from __future__ import absolute_import, division, print_function __metaclass__ = type from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.compat import ( ipaddress, ) def search_obj_in_list(name, lst, key="name"): if lst: for item in lst: if item[key] == name: return item return None def get_interface_type(interface): """Gets the type of interface""" if interface.startswith("eth"): return "ethernet" elif interface.startswith("bond"): return "bonding" elif interface.startswith("vti"): return "vti" elif interface.startswith("lo"): return "loopback" + elif interface.startswith("vtun"): + return "openvpn" def dict_delete(base, comparable): """ This function generates a dict containing key, value pairs for keys that are present in the `base` dict but not present in the `comparable` dict. :param base: dict object to base the diff on :param comparable: dict object to compare against base :returns: new dict object with key, value pairs that needs to be deleted. """ to_delete = dict() for key in base: if isinstance(base[key], dict): sub_diff = dict_delete(base[key], comparable.get(key, {})) if sub_diff: to_delete[key] = sub_diff else: if key not in comparable: to_delete[key] = base[key] return to_delete def diff_list_of_dicts(want, have): diff = [] set_w = set(tuple(d.items()) for d in want) set_h = set(tuple(d.items()) for d in have) difference = set_w.difference(set_h) for element in difference: diff.append(dict((x, y) for x, y in element)) return diff def get_lst_diff_for_dicts(want, have, lst): """ This function generates a list containing values that are only in want and not in list in have dict :param want: dict object to want :param have: dict object to have :param lst: list the diff on :return: new list object with values which are only in want. """ if not have: diff = want.get(lst) or [] else: want_elements = want.get(lst) or {} have_elements = have.get(lst) or {} diff = list_diff_want_only(want_elements, have_elements) return diff def get_lst_same_for_dicts(want, have, lst): """ This function generates a list containing values that are common for list in want and list in have dict :param want: dict object to want :param have: dict object to have :param lst: list the comparison on :return: new list object with values which are common in want and have. """ diff = None if want and have: want_list = want.get(lst) or {} have_list = have.get(lst) or {} diff = [ i for i in want_list and have_list if i in have_list and i in want_list ] return diff def list_diff_have_only(want_list, have_list): """ This function generated the list containing values that are only in have list. :param want_list: :param have_list: :return: new list with values which are only in have list """ if have_list and not want_list: diff = have_list elif not have_list: diff = None else: diff = [ i for i in have_list + want_list if i in have_list and i not in want_list ] return diff def list_diff_want_only(want_list, have_list): """ This function generated the list containing values that are only in want list. :param want_list: :param have_list: :return: new list with values which are only in want list """ if have_list and not want_list: diff = None elif not have_list: diff = want_list else: diff = [ i for i in have_list + want_list if i in want_list and i not in have_list ] return diff def search_dict_tv_in_list(d_val1, d_val2, lst, key1, key2): """ This function return the dict object if it exist in list. :param d_val1: :param d_val2: :param lst: :param key1: :param key2: :return: """ obj = next( ( item for item in lst if item[key1] == d_val1 and item[key2] == d_val2 ), None, ) if obj: return obj else: return None def key_value_in_dict(have_key, have_value, want_dict): """ This function checks whether the key and values exist in dict :param have_key: :param have_value: :param want_dict: :return: """ for key, value in iteritems(want_dict): if key == have_key and value == have_value: return True return False def is_dict_element_present(dict, key): """ This function checks whether the key is present in dict. :param dict: :param key: :return: """ for item in dict: if item == key: return True return False def get_ip_address_version(address): """ This function returns the version of IP address :param address: IP address :return: """ try: address = unicode(address) except NameError: address = str(address) version = ipaddress.ip_address(address.split("/")[0]).version return version def get_route_type(address): """ This function returns the route type based on IP address :param address: :return: """ version = get_ip_address_version(address) if version == 6: return "route6" elif version == 4: return "route" def _bool_to_str(val): """ This function converts the bool value into string. :param val: bool value. :return: enable/disable. """ return ( "enable" if str(val) == "True" else "disable" if str(val) == "False" else val ) def _is_w_same(w, h, key): """ This function checks whether the key value is same in desired and target config dictionary. :param w: base config. :param h: target config. :param key:attribute name. :return: True/False. """ return True if h and key in h and h[key] == w[key] else False def _in_target(h, key): """ This function checks whether the target exist and key present in target config. :param h: target config. :param key: attribute name. :return: True/False. """ return True if h and key in h else False diff --git a/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config.cfg b/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config.cfg new file mode 100644 index 0000000..0876916 --- /dev/null +++ b/tests/unit/modules/network/vyos/fixtures/vyos_interfaces_config.cfg @@ -0,0 +1,6 @@ +set interfaces ethernet eth0 address 'dhcp' +set interfaces ethernet eth0 hw-id '08:00:27:7c:85:05' +set interfaces ethernet eth1 description 'test-interface' +set interfaces ethernet eth2 hw-id '08:00:27:04:85:99' +set interfaces ethernet eth3 hw-id '08:00:27:1c:82:d1' +set interfaces loopback 'lo' diff --git a/tests/unit/modules/network/vyos/test_vyos_interfaces.py b/tests/unit/modules/network/vyos/test_vyos_interfaces.py new file mode 100644 index 0000000..9b832a0 --- /dev/null +++ b/tests/unit/modules/network/vyos/test_vyos_interfaces.py @@ -0,0 +1,100 @@ +# (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 ansible_collections.vyos.vyos.tests.unit.compat.mock import patch +from ansible_collections.vyos.vyos.plugins.modules import ( + vyos_interfaces, +) +from ansible_collections.vyos.vyos.tests.unit.modules.utils import ( + set_module_args, +) +from .vyos_module import TestVyosModule, load_fixture + + +class TestVyosFirewallInterfacesModule(TestVyosModule): + + module = vyos_interfaces + + def setUp(self): + super(TestVyosFirewallInterfacesModule, self).setUp() + self.mock_get_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.get_config" + ) + self.get_config = self.mock_get_config.start() + + self.mock_load_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.network.Config.load_config" + ) + self.load_config = self.mock_load_config.start() + + self.mock_get_resource_connection_config = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base.get_resource_connection" + ) + self.get_resource_connection_config = ( + self.mock_get_resource_connection_config.start() + ) + + self.mock_get_resource_connection_facts = patch( + "ansible_collections.ansible.netcommon.plugins.module_utils.network.common.facts.facts.get_resource_connection" + ) + self.get_resource_connection_facts = ( + self.mock_get_resource_connection_facts.start() + ) + + self.mock_execute_show_command = patch( + "ansible_collections.vyos.vyos.plugins.module_utils.network.vyos." + "facts.interfaces.interfaces.InterfacesFacts.get_device_data" + ) + self.execute_show_command = self.mock_execute_show_command.start() + + def tearDown(self): + super(TestVyosFirewallInterfacesModule, self).tearDown() + self.mock_get_resource_connection_config.stop() + self.mock_get_resource_connection_facts.stop() + self.mock_get_config.stop() + self.mock_load_config.stop() + self.mock_execute_show_command.stop() + + def load_fixtures(self, commands=None): + def load_from_file(*args, **kwargs): + return load_fixture("vyos_interfaces_config.cfg") + + self.execute_show_command.side_effect = load_from_file + + def test_vyos_interfaces_merged(self): + set_module_args( + dict( + config=[ + dict(name="bond1", description="Bond - 1", enabled=True), + dict(name="vtun1", description="vtun - 1", enabled=True), + ], + state="merged", + ) + ) + + commands = [ + "set interfaces bonding bond1 description 'Bond - 1'", + "delete interfaces bonding bond1 disable", + "set interfaces openvpn vtun1 description 'vtun - 1'", + "delete interfaces openvpn vtun1 disable", + ] + self.execute_module(changed=True, commands=commands)