summaryrefslogtreecommitdiff
path: root/src/conf_mode/vrrp.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode/vrrp.py')
-rwxr-xr-xsrc/conf_mode/vrrp.py256
1 files changed, 256 insertions, 0 deletions
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
new file mode 100755
index 000000000..292eb0c78
--- /dev/null
+++ b/src/conf_mode/vrrp.py
@@ -0,0 +1,256 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address
+from json import dumps
+from pathlib import Path
+
+import vyos.config
+
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos.ifconfig.vrrp import VRRP
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ vrrp_groups = []
+ sync_groups = []
+
+ config = vyos.config.Config()
+
+ # Get the VRRP groups
+ for group_name in config.list_nodes("high-availability vrrp group"):
+ config.set_level("high-availability vrrp group {0}".format(group_name))
+
+ # Retrieve the values
+ group = {"preempt": True, "use_vmac": False, "disable": False}
+
+ if config.exists("disable"):
+ group["disable"] = True
+
+ group["name"] = group_name
+ group["vrid"] = config.return_value("vrid")
+ group["interface"] = config.return_value("interface")
+ group["description"] = config.return_value("description")
+ group["advertise_interval"] = config.return_value("advertise-interval")
+ group["priority"] = config.return_value("priority")
+ group["hello_source"] = config.return_value("hello-source-address")
+ group["peer_address"] = config.return_value("peer-address")
+ group["sync_group"] = config.return_value("sync-group")
+ group["preempt_delay"] = config.return_value("preempt-delay")
+ group["virtual_addresses"] = config.return_values("virtual-address")
+
+ group["auth_password"] = config.return_value("authentication password")
+ group["auth_type"] = config.return_value("authentication type")
+
+ group["health_check_script"] = config.return_value("health-check script")
+ group["health_check_interval"] = config.return_value("health-check interval")
+ group["health_check_count"] = config.return_value("health-check failure-count")
+
+ group["master_script"] = config.return_value("transition-script master")
+ group["backup_script"] = config.return_value("transition-script backup")
+ group["fault_script"] = config.return_value("transition-script fault")
+ group["stop_script"] = config.return_value("transition-script stop")
+
+ if config.exists("no-preempt"):
+ group["preempt"] = False
+ if config.exists("rfc3768-compatibility"):
+ group["use_vmac"] = True
+
+ # Substitute defaults where applicable
+ if not group["advertise_interval"]:
+ group["advertise_interval"] = 1
+ if not group["priority"]:
+ group["priority"] = 100
+ if not group["preempt_delay"]:
+ group["preempt_delay"] = 0
+ if not group["health_check_interval"]:
+ group["health_check_interval"] = 60
+ if not group["health_check_count"]:
+ group["health_check_count"] = 3
+
+ # FIXUP: translate our option for auth type to keepalived's syntax
+ # for simplicity
+ if group["auth_type"]:
+ if group["auth_type"] == "plaintext-password":
+ group["auth_type"] = "PASS"
+ else:
+ group["auth_type"] = "AH"
+
+ vrrp_groups.append(group)
+
+ config.set_level("")
+
+ # Get the sync group used for conntrack-sync
+ conntrack_sync_group = None
+ if config.exists("service conntrack-sync failover-mechanism vrrp"):
+ conntrack_sync_group = config.return_value("service conntrack-sync failover-mechanism vrrp sync-group")
+
+ # Get the sync groups
+ for sync_group_name in config.list_nodes("high-availability vrrp sync-group"):
+ config.set_level("high-availability vrrp sync-group {0}".format(sync_group_name))
+
+ sync_group = {"conntrack_sync": False}
+ sync_group["name"] = sync_group_name
+ sync_group["members"] = config.return_values("member")
+ if conntrack_sync_group:
+ if conntrack_sync_group == sync_group_name:
+ sync_group["conntrack_sync"] = True
+
+ # add transition script configuration
+ sync_group["master_script"] = config.return_value("transition-script master")
+ sync_group["backup_script"] = config.return_value("transition-script backup")
+ sync_group["fault_script"] = config.return_value("transition-script fault")
+ sync_group["stop_script"] = config.return_value("transition-script stop")
+
+ sync_groups.append(sync_group)
+
+ # create a file with dict with proposed configuration
+ with open("{}.temp".format(VRRP.location['vyos']), 'w') as dict_file:
+ dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups}))
+
+ return (vrrp_groups, sync_groups)
+
+
+def verify(data):
+ vrrp_groups, sync_groups = data
+
+ for group in vrrp_groups:
+ # Check required fields
+ if not group["vrid"]:
+ raise ConfigError("vrid is required but not set in VRRP group {0}".format(group["name"]))
+ if not group["interface"]:
+ raise ConfigError("interface is required but not set in VRRP group {0}".format(group["name"]))
+ if not group["virtual_addresses"]:
+ raise ConfigError("virtual-address is required but not set in VRRP group {0}".format(group["name"]))
+
+ if group["auth_password"] and (not group["auth_type"]):
+ raise ConfigError("authentication type is required but not set in VRRP group {0}".format(group["name"]))
+
+ # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
+
+ # XXX: filter on map object is destructive, so we force it to list.
+ # Additionally, filter objects always evaluate to True, empty or not,
+ # so we force them to lists as well.
+ vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"]))
+ vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs))
+ vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs))
+
+ if vaddrs4 and vaddrs6:
+ raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"]))
+
+ if vaddrs4:
+ if group["hello_source"]:
+ hsa = ip_address(group["hello_source"])
+ if isinstance(hsa, IPv6Address):
+ raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"]))
+ if group["peer_address"]:
+ pa = ip_address(group["peer_address"])
+ if isinstance(pa, IPv6Address):
+ raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"]))
+
+ if vaddrs6:
+ if group["hello_source"]:
+ hsa = ip_address(group["hello_source"])
+ if isinstance(hsa, IPv4Address):
+ raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"]))
+ if group["peer_address"]:
+ pa = ip_address(group["peer_address"])
+ if isinstance(pa, IPv4Address):
+ raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"]))
+
+ # Disallow same VRID on multiple interfaces
+ _groups = sorted(vrrp_groups, key=(lambda x: x["interface"]))
+ count = len(_groups) - 1
+ index = 0
+ while (index < count):
+ if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]):
+ raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format(
+ _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"]))
+ else:
+ index += 1
+
+ # Check sync groups
+ vrrp_group_names = list(map(lambda x: x["name"], vrrp_groups))
+
+ for sync_group in sync_groups:
+ for m in sync_group["members"]:
+ if not (m in vrrp_group_names):
+ raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m))
+
+
+def generate(data):
+ vrrp_groups, sync_groups = data
+
+ # Remove disabled groups from the sync group member lists
+ for sync_group in sync_groups:
+ for member in sync_group["members"]:
+ g = list(filter(lambda x: x["name"] == member, vrrp_groups))[0]
+ if g["disable"]:
+ print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"]))
+ # Filter out disabled groups
+ vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups))
+
+ render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl',
+ {"groups": vrrp_groups, "sync_groups": sync_groups})
+ render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {})
+ return None
+
+
+def apply(data):
+ vrrp_groups, sync_groups = data
+ if vrrp_groups:
+ # safely rename a temporary file with configuration dict
+ try:
+ dict_file = Path("{}.temp".format(VRRP.location['vyos']))
+ dict_file.rename(Path(VRRP.location['vyos']))
+ except Exception as err:
+ print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err))
+
+ if not VRRP.is_running():
+ print("Starting the VRRP process")
+ ret = call("systemctl restart keepalived.service")
+ else:
+ print("Reloading the VRRP process")
+ ret = call("systemctl reload keepalived.service")
+
+ if ret != 0:
+ raise ConfigError("keepalived failed to start")
+ else:
+ # VRRP is removed in the commit
+ print("Stopping the VRRP process")
+ call("systemctl stop keepalived.service")
+ os.unlink(VRRP.location['daemon'])
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print("VRRP error: {0}".format(str(e)))
+ exit(1)