# Copyright 2018-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this library. If not, see <http://www.gnu.org/licenses/>. import re from vyos.configtree import ConfigTree # Convert the old VRRP syntax to the new syntax # The old approach was to put VRRP groups inside interfaces, # as in "interfaces ethernet eth0 vrrp vrrp-group 10 ...". # It was supported only under ethernet and bonding and their # respective vif, vif-s, and vif-c subinterfaces def get_vrrp_group(path): group = {"preempt": True, "rfc_compatibility": False, "disable": False} if config.exists(path + ["advertise-interval"]): group["advertise_interval"] = config.return_value(path + ["advertise-interval"]) if config.exists(path + ["description"]): group["description"] = config.return_value(path + ["description"]) if config.exists(path + ["disable"]): group["disable"] = True if config.exists(path + ["hello-source-address"]): group["hello_source"] = config.return_value(path + ["hello-source-address"]) # 1.1.8 didn't have it, but earlier 1.2.0 did, we don't want to break # configs of early adopters! if config.exists(path + ["peer-address"]): group["peer_address"] = config.return_value(path + ["peer-address"]) if config.exists(path + ["preempt"]): preempt = config.return_value(path + ["preempt"]) if preempt == "false": group["preempt"] = False if config.exists(path + ["rfc3768-compatibility"]): group["rfc_compatibility"] = True if config.exists(path + ["preempt-delay"]): group["preempt_delay"] = config.return_value(path + ["preempt-delay"]) if config.exists(path + ["priority"]): group["priority"] = config.return_value(path + ["priority"]) if config.exists(path + ["sync-group"]): group["sync_group"] = config.return_value(path + ["sync-group"]) if config.exists(path + ["authentication", "type"]): group["auth_type"] = config.return_value(path + ["authentication", "type"]) if config.exists(path + ["authentication", "password"]): group["auth_password"] = config.return_value(path + ["authentication", "password"]) if config.exists(path + ["virtual-address"]): group["virtual_addresses"] = config.return_values(path + ["virtual-address"]) if config.exists(path + ["run-transition-scripts"]): if config.exists(path + ["run-transition-scripts", "master"]): group["master_script"] = config.return_value(path + ["run-transition-scripts", "master"]) if config.exists(path + ["run-transition-scripts", "backup"]): group["backup_script"] = config.return_value(path + ["run-transition-scripts", "backup"]) if config.exists(path + ["run-transition-scripts", "fault"]): group["fault_script"] = config.return_value(path + ["run-transition-scripts", "fault"]) # Also not present in 1.1.8, but supported by earlier 1.2.0 if config.exists(path + ["health-check"]): if config.exists(path + ["health-check", "interval"]): group["health_check_interval"] = config.return_value(path + ["health-check", "interval"]) if config.exists(path + ["health-check", "failure-count"]): group["health_check_count"] = config.return_value(path + ["health-check", "failure-count"]) if config.exists(path + ["health-check", "script"]): group["health_check_script"] = config.return_value(path + ["health-check", "script"]) return group # Since VRRP is all over the place, there's no way to just check a path and exit early # if it doesn't exist, we have to walk all interfaces and collect VRRP settings from them. # Only if no data is collected from any interface we can conclude that VRRP is not configured # and exit. def migrate(config: ConfigTree) -> None: groups = [] base_paths = [] if config.exists(["interfaces", "ethernet"]): base_paths.append("ethernet") if config.exists(["interfaces", "bonding"]): base_paths.append("bonding") for bp in base_paths: parent_path = ["interfaces", bp] parent_intfs = config.list_nodes(parent_path) for pi in parent_intfs: # Extract VRRP groups from the parent interface vg_path =[pi, "vrrp", "vrrp-group"] if config.exists(parent_path + vg_path): pgroups = config.list_nodes(parent_path + vg_path) for pg in pgroups: g = get_vrrp_group(parent_path + vg_path + [pg]) g["interface"] = pi g["vrid"] = pg groups.append(g) # Delete the VRRP subtree # If left in place, configs will not load correctly config.delete(parent_path + [pi, "vrrp"]) # Extract VRRP groups from 802.1q VLAN interfaces if config.exists(parent_path + [pi, "vif"]): vifs = config.list_nodes(parent_path + [pi, "vif"]) for vif in vifs: vif_vg_path = [pi, "vif", vif, "vrrp", "vrrp-group"] if config.exists(parent_path + vif_vg_path): vifgroups = config.list_nodes(parent_path + vif_vg_path) for vif_group in vifgroups: g = get_vrrp_group(parent_path + vif_vg_path + [vif_group]) g["interface"] = "{0}.{1}".format(pi, vif) g["vrid"] = vif_group groups.append(g) config.delete(parent_path + [pi, "vif", vif, "vrrp"]) # Extract VRRP groups from 802.3ad QinQ service VLAN interfaces if config.exists(parent_path + [pi, "vif-s"]): vif_ss = config.list_nodes(parent_path + [pi, "vif-s"]) for vif_s in vif_ss: vifs_vg_path = [pi, "vif-s", vif_s, "vrrp", "vrrp-group"] if config.exists(parent_path + vifs_vg_path): vifsgroups = config.list_nodes(parent_path + vifs_vg_path) for vifs_group in vifsgroups: g = get_vrrp_group(parent_path + vifs_vg_path + [vifs_group]) g["interface"] = "{0}.{1}".format(pi, vif_s) g["vrid"] = vifs_group groups.append(g) config.delete(parent_path + [pi, "vif-s", vif_s, "vrrp"]) # Extract VRRP groups from QinQ client VLAN interfaces nested in the vif-s if config.exists(parent_path + [pi, "vif-s", vif_s, "vif-c"]): vif_cs = config.list_nodes(parent_path + [pi, "vif-s", vif_s, "vif-c"]) for vif_c in vif_cs: vifc_vg_path = [pi, "vif-s", vif_s, "vif-c", vif_c, "vrrp", "vrrp-group"] vifcgroups = config.list_nodes(parent_path + vifc_vg_path) for vifc_group in vifcgroups: g = get_vrrp_group(parent_path + vifc_vg_path + [vifc_group]) g["interface"] = "{0}.{1}.{2}".format(pi, vif_s, vif_c) g["vrid"] = vifc_group groups.append(g) config.delete(parent_path + [pi, "vif-s", vif_s, "vif-c", vif_c, "vrrp"]) # If nothing was collected before this point, it means the config has no VRRP setup if not groups: return # Otherwise, there is VRRP to convert # Now convert the collected groups to the new syntax base_group_path = ["high-availability", "vrrp", "group"] sync_path = ["high-availability", "vrrp", "sync-group"] for g in groups: group_name = "{0}-{1}".format(g["interface"], g["vrid"]) group_path = base_group_path + [group_name] config.set(group_path + ["interface"], value=g["interface"]) config.set(group_path + ["vrid"], value=g["vrid"]) if "advertise_interval" in g: config.set(group_path + ["advertise-interval"], value=g["advertise_interval"]) if "priority" in g: config.set(group_path + ["priority"], value=g["priority"]) if not g["preempt"]: config.set(group_path + ["no-preempt"], value=None) if "preempt_delay" in g: config.set(group_path + ["preempt-delay"], value=g["preempt_delay"]) if g["rfc_compatibility"]: config.set(group_path + ["rfc3768-compatibility"], value=None) if g["disable"]: config.set(group_path + ["disable"], value=None) if "hello_source" in g: config.set(group_path + ["hello-source-address"], value=g["hello_source"]) if "peer_address" in g: config.set(group_path + ["peer-address"], value=g["peer_address"]) if "auth_password" in g: config.set(group_path + ["authentication", "password"], value=g["auth_password"]) if "auth_type" in g: config.set(group_path + ["authentication", "type"], value=g["auth_type"]) if "master_script" in g: config.set(group_path + ["transition-script", "master"], value=g["master_script"]) if "backup_script" in g: config.set(group_path + ["transition-script", "backup"], value=g["backup_script"]) if "fault_script" in g: config.set(group_path + ["transition-script", "fault"], value=g["fault_script"]) if "health_check_interval" in g: config.set(group_path + ["health-check", "interval"], value=g["health_check_interval"]) if "health_check_count" in g: config.set(group_path + ["health-check", "failure-count"], value=g["health_check_count"]) if "health_check_script" in g: config.set(group_path + ["health-check", "script"], value=g["health_check_script"]) # Not that it should ever be absent... if "virtual_addresses" in g: # The new CLI disallows addresses without prefix length # Pre-rewrite configs didn't support IPv6 VRRP, but handle it anyway for va in g["virtual_addresses"]: if not re.search(r'/', va): if re.search(r':', va): va = "{0}/128".format(va) else: va = "{0}/32".format(va) config.set(group_path + ["virtual-address"], value=va, replace=False) # Sync group if "sync_group" in g: config.set(sync_path + [g["sync_group"], "member"], value=group_name, replace=False) # Set the tag flag config.set_tag(base_group_path) if config.exists(sync_path): config.set_tag(sync_path)