diff --git a/changelogs/fragments/l3_interfaces-support.yaml b/changelogs/fragments/l3_interfaces-support.yaml new file mode 100644 index 0000000..41bcd07 --- /dev/null +++ b/changelogs/fragments/l3_interfaces-support.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - vyos-l3_interface_support - Add support for Tunnel, Bridge and Dummy interfaces. (https://github.com/ansible-collections/vyos.vyos/issues/265) diff --git a/galaxy.yml b/galaxy.yml index fb02e07..d2d6ce3 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,14 +1,14 @@ --- authors: - Ansible Network Community (ansible-network) dependencies: "ansible.netcommon": ">=2.5.1" license_file: LICENSE name: vyos description: Ansible Network Collection for VYOS devices. namespace: vyos readme: README.md repository: https://github.com/ansible-collections/vyos.vyos issues: https://github.com/ansible-collections/vyos.vyos/issues tags: [vyos, networking] -version: 4.0.3-dev +version: 4.1.0-dev diff --git a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py index 9fd00c1..51c3a10 100644 --- a/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py +++ b/plugins/module_utils/network/vyos/facts/interfaces/interfaces.py @@ -1,133 +1,133 @@ # # -*- 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 copy import deepcopy from re import M, findall 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 = self.get_device_data(connection) objs = [] interface_names = findall( - r"^set interfaces (?:ethernet|bonding|vti|loopback|vxlan|openvpn|wireguard) (?:\'*)(\S+)(?:\'*)", + r"^set interfaces (?:ethernet|bonding|bridge|dummy|tunnel|vti|loopback|vxlan|openvpn|wireguard) (?:\'*)(\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/facts/l3_interfaces/l3_interfaces.py b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py index ebfab03..845d79e 100644 --- a/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py +++ b/plugins/module_utils/network/vyos/facts/l3_interfaces/l3_interfaces.py @@ -1,144 +1,144 @@ # # -*- 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 l3_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 import re from copy import deepcopy from ansible.module_utils.six import iteritems from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.argspec.l3_interfaces.l3_interfaces import ( L3_interfacesArgs, ) from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( get_ip_address_version, ) class L3_interfacesFacts(object): """The vyos l3_interfaces fact class""" def __init__(self, module, subspec="config", options="options"): self._module = module self.argument_spec = L3_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 populate_facts(self, connection, ansible_facts, data=None): """Populate the facts for l3_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() # operate on a collection of resource x objs = [] interface_names = re.findall( - r"set interfaces (?:ethernet|bonding|vti|vxlan) (?:\'*)(\S+)(?:\'*)", + r"set interfaces (?:ethernet|bonding|bridge|dummy|tunnel|vti|vxlan) (?:\'*)(\S+)(?:\'*)", data, re.M, ) if interface_names: for interface in set(interface_names): intf_regex = r" %s .+$" % interface cfg = re.findall(intf_regex, data, re.M) obj = self.render_config(cfg) obj["name"] = interface.strip("'") if obj: objs.append(obj) ansible_facts["ansible_network_resources"].pop("l3_interfaces", None) facts = {} if objs: facts["l3_interfaces"] = [] params = utils.validate_config(self.argument_spec, {"config": objs}) for cfg in params["config"]: facts["l3_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(eth_conf) config["vifs"] = self.parse_vifs(vif_conf) return utils.remove_empties(config) def parse_vifs(self, conf): vif_names = re.findall(r"vif (\d+)", conf, re.M) vifs_list = None if vif_names: vifs_list = [] for vif in set(vif_names): vif_regex = r" %s .+$" % vif cfg = "\n".join(re.findall(vif_regex, conf, re.M)) obj = self.parse_attribs(cfg) obj["vlan_id"] = vif if obj: vifs_list.append(obj) return vifs_list def parse_attribs(self, conf): config = {} ipaddrs = re.findall(r"address (\S+)", conf, re.M) config["ipv4"] = [] config["ipv6"] = [] for item in ipaddrs: item = item.strip("'") if item == "dhcp": config["ipv4"].append({"address": item}) elif item == "dhcpv6": config["ipv6"].append({"address": item}) elif item == "no-default-link-local": config["ipv6"].append({"address": item}) elif item == "autoconf": config["ipv6"].append({"address": item}) else: ip_version = get_ip_address_version(item.split("/")[0]) if ip_version == 4: config["ipv4"].append({"address": item}) else: config["ipv6"].append({"address": item}) for key, value in iteritems(config): if value == []: config[key] = None 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 4d44744..d2ca703 100644 --- a/plugins/module_utils/network/vyos/utils/utils.py +++ b/plugins/module_utils/network/vyos/utils/utils.py @@ -1,258 +1,264 @@ # -*- 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.basic import missing_required_lib from ansible.module_utils.six import iteritems try: import ipaddress HAS_IPADDRESS = True except ImportError: HAS_IPADDRESS = False 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" elif interface.startswith("wg"): return "wireguard" + elif interface.startswith("tun"): + return "tunnel" + elif interface.startswith("br"): + return "bridge" + elif interface.startswith("dum"): + return "dummy" 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: """ if not HAS_IPADDRESS: raise Exception(missing_required_lib("ipaddress")) 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