From 2817f86a0faf0e1034ac985c4300f14d84e4fb61 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Mon, 27 Dec 2021 07:49:56 +0000 Subject: conntrack-sync: T4109: Change script name for vrrp The script vrrp.py was moved to high-availability.py As all logic are handle by root 'high-avalability' node --- src/conf_mode/conntrack_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py index f82a077e6..8f9837c2b 100755 --- a/src/conf_mode/conntrack_sync.py +++ b/src/conf_mode/conntrack_sync.py @@ -36,7 +36,7 @@ airbag.enable() config_file = '/run/conntrackd/conntrackd.conf' def resync_vrrp(): - tmp = run('/usr/libexec/vyos/conf_mode/vrrp.py') + tmp = run('/usr/libexec/vyos/conf_mode/high-availability.py') if tmp > 0: print('ERROR: error restarting VRRP daemon!') -- cgit v1.2.3 From 3628121505658fd4c588960136d5645afc791c59 Mon Sep 17 00:00:00 2001 From: Viacheslav Date: Mon, 27 Dec 2021 07:58:30 +0000 Subject: keepalived: T4109: Add high-availability virtual-server Add new feature, high-availability virtual-server Change XML, python and templates Move vrrp to root node 'high-availability' as all logic are handler by root node 'high-availability' --- .../high-availability/keepalived.conf.tmpl | 157 +++++++++++++ interface-definitions/high-availability.xml.in | 12 +- interface-definitions/vrrp.xml.in | 257 --------------------- smoketest/scripts/cli/test_ha_virtual_server.py | 146 ++++++++++++ src/conf_mode/high-availability.py | 180 +++++++++++++++ src/conf_mode/vrrp.py | 159 ------------- 6 files changed, 489 insertions(+), 422 deletions(-) create mode 100644 data/templates/high-availability/keepalived.conf.tmpl delete mode 100644 interface-definitions/vrrp.xml.in create mode 100755 smoketest/scripts/cli/test_ha_virtual_server.py create mode 100755 src/conf_mode/high-availability.py delete mode 100755 src/conf_mode/vrrp.py (limited to 'src/conf_mode') diff --git a/data/templates/high-availability/keepalived.conf.tmpl b/data/templates/high-availability/keepalived.conf.tmpl new file mode 100644 index 000000000..817c65ff0 --- /dev/null +++ b/data/templates/high-availability/keepalived.conf.tmpl @@ -0,0 +1,157 @@ +# Autogenerated by VyOS +# Do not edit this file, all your changes will be lost +# on next commit or reboot + +global_defs { + dynamic_interfaces + script_user root + notify_fifo /run/keepalived/keepalived_notify_fifo + notify_fifo_script /usr/libexec/vyos/system/keepalived-fifo.py +} + +{% if vrrp is defined and vrrp.group is defined and vrrp.group is not none %} +{% for name, group_config in vrrp.group.items() if group_config.disable is not defined %} +{% if group_config.health_check is defined and group_config.health_check.script is defined and group_config.health_check.script is not none %} +vrrp_script healthcheck_{{ name }} { + script "{{ group_config.health_check.script }}" + interval {{ group_config.health_check.interval }} + fall {{ group_config.health_check.failure_count }} + rise 1 +} +{% endif %} +vrrp_instance {{ name }} { +{% if group_config.description is defined and group_config.description is not none %} + # {{ group_config.description }} +{% endif %} + state BACKUP + interface {{ group_config.interface }} + virtual_router_id {{ group_config.vrid }} + priority {{ group_config.priority }} + advert_int {{ group_config.advertise_interval }} +{% if group_config.no_preempt is not defined and group_config.preempt_delay is defined and group_config.preempt_delay is not none %} + preempt_delay {{ group_config.preempt_delay }} +{% elif group_config.no_preempt is defined %} + nopreempt +{% endif %} +{% if group_config.peer_address is defined and group_config.peer_address is not none %} + unicast_peer { {{ group_config.peer_address }} } +{% endif %} +{% if group_config.hello_source_address is defined and group_config.hello_source_address is not none %} +{% if group_config.peer_address is defined and group_config.peer_address is not none %} + unicast_src_ip {{ group_config.hello_source_address }} +{% else %} + mcast_src_ip {{ group_config.hello_source_address }} +{% endif %} +{% endif %} +{% if group_config.rfc3768_compatibility is defined and group_config.peer_address is defined %} + use_vmac {{ group_config.interface }}v{{ group_config.vrid }} + vmac_xmit_base +{% elif group_config.rfc3768_compatibility is defined %} + use_vmac {{ group_config.interface }}v{{ group_config.vrid }} +{% endif %} +{% if group_config.authentication is defined and group_config.authentication is not none %} + authentication { + auth_pass "{{ group_config.authentication.password }}" +{% if group_config.authentication.type == 'plaintext-password' %} + auth_type PASS +{% else %} + auth_type {{ group_config.authentication.type | upper }} +{% endif %} + } +{% endif %} +{% if group_config.address is defined and group_config.address is not none %} + virtual_ipaddress { +{% for addr in group_config.address %} + {{ addr }} +{% endfor %} + } +{% endif %} +{% if group_config.excluded_address is defined and group_config.excluded_address is not none %} + virtual_ipaddress_excluded { +{% for addr in group_config.excluded_address %} + {{ addr }} +{% endfor %} + } +{% endif %} +{% if group_config.health_check is defined and group_config.health_check.script is defined and group_config.health_check.script is not none %} + track_script { + healthcheck_{{ name }} + } +{% endif %} +} +{% endfor %} +{% endif %} + +{% if vrrp is defined and vrrp.sync_group is defined and vrrp.sync_group is not none %} +{% for name, sync_group_config in vrrp.sync_group.items() if sync_group_config.disable is not defined %} +vrrp_sync_group {{ name }} { + group { +{% if sync_group_config.member is defined and sync_group_config.member is not none %} +{% for member in sync_group_config.member %} + {{ member }} +{% endfor %} +{% endif %} + } + +{# Health-check scripts should be in section sync-group if member is part of the sync-group T4081 #} +{% for name, group_config in vrrp.group.items() if group_config.disable is not defined %} +{% if group_config.health_check is defined and group_config.health_check.script is defined and group_config.health_check.script is not none and name in sync_group_config.member %} + track_script { + healthcheck_{{ name }} + } +{% endif %} +{% endfor %} +{% if vrrp.conntrack_sync_group is defined and vrrp.conntrack_sync_group == name %} +{% set vyos_helper = "/usr/libexec/vyos/vyos-vrrp-conntracksync.sh" %} + notify_master "{{ vyos_helper }} master {{ name }}" + notify_backup "{{ vyos_helper }} backup {{ name }}" + notify_fault "{{ vyos_helper }} fault {{ name }}" +{% endif %} +} +{% endfor %} +{% endif %} + +# Virtual-server configuration +{% if virtual_server is defined and virtual_server is not none %} +{% for vserver, vserver_config in virtual_server.items() %} +virtual_server {{ vserver }} {{ vserver_config.port }} { + delay_loop {{ vserver_config.delay_loop }} +{% if vserver_config.algorithm == 'round-robin' %} + lb_algo rr +{% elif vserver_config.algorithm == 'weighted-round-robin' %} + lb_algo wrr +{% elif vserver_config.algorithm == 'least-connection' %} + lb_algo lc +{% elif vserver_config.algorithm == 'weighted-least-connection' %} + lb_algo wlc +{% elif vserver_config.algorithm == 'source-hashing' %} + lb_algo sh +{% elif vserver_config.algorithm == 'destination-hashing' %} + lb_algo dh +{% elif vserver_config.algorithm == 'locality-based-least-connection' %} + lb_algo lblc +{% endif %} +{% if vserver_config.forward_method == "nat" %} + lb_kind NAT +{% elif vserver_config.forward_method == "direct" %} + lb_kind DR +{% elif vserver_config.forward_method == "tunnel" %} + lb_kind TUN +{% endif %} + persistence_timeout {{ vserver_config.persistence_timeout }} + protocol {{ vserver_config.protocol | upper }} +{% if vserver_config.real_server is defined and vserver_config.real_server is not none %} +{% for rserver, rserver_config in vserver_config.real_server.items() %} + real_server {{ rserver }} {{ rserver_config.port }} { + weight 1 + {{ vserver_config.protocol | upper }}_CHECK { +{% if rserver_config.connection_timeout is defined and rserver_config.connection_timeout is not none %} + connect_timeout {{ rserver_config.connection_timeout }} +{% endif %} + } + } +{% endfor %} +{% endif %} +} +{% endfor %} +{% endif %} diff --git a/interface-definitions/high-availability.xml.in b/interface-definitions/high-availability.xml.in index 42cdceed1..f46343c76 100644 --- a/interface-definitions/high-availability.xml.in +++ b/interface-definitions/high-availability.xml.in @@ -312,7 +312,7 @@ - Forwarding method (default - NAT) + Forwarding method (default: NAT) direct nat tunnel @@ -340,7 +340,7 @@ Timeout for persistent connections u32:1-86400 - Timeout for persistent connections (default 300) + Timeout for persistent connections (default: 300) @@ -350,17 +350,17 @@ - Protocol for port checks (default TCP) + Protocol for port checks (default: TCP) tcp udp tcp - Protocol TCP + TCP udp - Protocol UDP + UDP ^(tcp|udp)$ @@ -376,7 +376,7 @@ #include - Connection timeout to remote server + Server connection timeout u32:1-86400 Connection timeout to remote server diff --git a/interface-definitions/vrrp.xml.in b/interface-definitions/vrrp.xml.in deleted file mode 100644 index 53d79caac..000000000 --- a/interface-definitions/vrrp.xml.in +++ /dev/null @@ -1,257 +0,0 @@ - - - - - High availability settings - - - - - 800 - Virtual Router Redundancy Protocol settings - - - - - VRRP group - - - #include - - - Advertise interval - - u32:1-255 - Advertise interval in seconds (default: 1) - - - - - - 1 - - - - VRRP authentication - - - - - VRRP password - - txt - Password string (up to 8 characters) - - - .{1,8} - - Password must not be longer than 8 characters - - - - - Authentication type - - plaintext-password ah - - - plaintext-password - Simple password string - - - ah - AH - IPSEC (not recommended) - - - ^(plaintext-password|ah)$ - - Authentication type must be plaintext-password or ah - - - - - #include - #include - - - Health check script - - - - - Health check failure count required for transition to fault (default: 3) - - - - - 3 - - - - Health check execution interval in seconds (default: 60) - - - - - 60 - - - - Health check script file - - - - - - - - - - VRRP hello source address - - ipv4 - IPv4 hello source address - - - ipv6 - IPv6 hello source address - - - - - - - - - - Unicast VRRP peer address - - ipv4 - IPv4 unicast peer address - - - ipv6 - IPv6 unicast peer address - - - - - - - - - - - Disable master preemption - - - - - Preempt delay (in seconds) - - u32:0-1000 - preempt delay - - - - - - 0 - - - - Router priority (default: 100) - - u32:1-255 - Router priority - - - - - - 100 - - - - Use VRRP virtual MAC address as per RFC3768 - - - - #include - - - Virtual IP address - - ipv4 - IPv4 virtual address - - - ipv6 - IPv6 virtual address - - - - - - - - - - - Virtual address (If you need additional IPv4 and IPv6 in same group) - - ipv4 - IP address - - - ipv6 - IPv6 address - - - - - - - Virtual address must be a valid IPv4 or IPv6 address with prefix length (e.g. 192.0.2.3/24 or 2001:db8:ff::10/64) - - - - - Virtual router identifier - - u32:1-255 - Virtual router identifier - - - - - - - - - - - VRRP sync group - - - - - - Sync group member - - txt - VRRP group name - - - high-availability vrrp group - - - - #include - - - - - - - diff --git a/smoketest/scripts/cli/test_ha_virtual_server.py b/smoketest/scripts/cli/test_ha_virtual_server.py new file mode 100755 index 000000000..e3a91283e --- /dev/null +++ b/smoketest/scripts/cli/test_ha_virtual_server.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2022 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 . + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.ifconfig.vrrp import VRRP +from vyos.util import cmd +from vyos.util import process_named_running +from vyos.util import read_file +from vyos.template import inc_ip + +PROCESS_NAME = 'keepalived' +KEEPALIVED_CONF = VRRP.location['config'] +base_path = ['high-availability'] +vrrp_interface = 'eth1' + +class TestHAVirtualServer(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + + self.cli_delete(['interfaces', 'ethernet', vrrp_interface, 'address']) + self.cli_delete(base_path) + self.cli_commit() + + # Process must be terminated after deleting the config + self.assertFalse(process_named_running(PROCESS_NAME)) + + def test_01_ha_virtual_server(self): + algo = 'least-connection' + delay = '10' + method = 'nat' + persistence_timeout = '600' + vip = '203.0.113.111' + vport = '2222' + rservers = ['192.0.2.21', '192.0.2.22', '192.0.2.23'] + rport = '22' + proto = 'tcp' + connection_timeout = '30' + + vserver_base = base_path + ['virtual-server'] + + self.cli_set(vserver_base + [vip, 'algorithm', algo]) + self.cli_set(vserver_base + [vip, 'delay-loop', delay]) + self.cli_set(vserver_base + [vip, 'forward-method', method]) + self.cli_set(vserver_base + [vip, 'persistence-timeout', persistence_timeout]) + self.cli_set(vserver_base + [vip, 'port', vport]) + self.cli_set(vserver_base + [vip, 'protocol', proto]) + for rs in rservers: + self.cli_set(vserver_base + [vip, 'real-server', rs, 'connection-timeout', connection_timeout]) + self.cli_set(vserver_base + [vip, 'real-server', rs, 'port', rport]) + + # commit changes + self.cli_commit() + + config = read_file(KEEPALIVED_CONF) + + self.assertIn(f'delay_loop {delay}', config) + self.assertIn(f'lb_algo lc', config) + self.assertIn(f'lb_kind {method.upper()}', config) + self.assertIn(f'persistence_timeout {persistence_timeout}', config) + self.assertIn(f'protocol {proto.upper()}', config) + for rs in rservers: + self.assertIn(f'real_server {rs} {rport}', config) + self.assertIn(f'{proto.upper()}_CHECK', config) + self.assertIn(f'connect_timeout {connection_timeout}', config) + + def test_02_ha_virtual_server_and_vrrp(self): + algo = 'least-connection' + delay = '15' + method = 'nat' + persistence_timeout = '300' + vip = '203.0.113.222' + vport = '22322' + rservers = ['192.0.2.11', '192.0.2.12'] + rport = '222' + proto = 'tcp' + connection_timeout = '23' + group = 'VyOS' + vrid = '99' + + vrrp_base = base_path + ['vrrp', 'group'] + vserver_base = base_path + ['virtual-server'] + + self.cli_set(['interfaces', 'ethernet', vrrp_interface, 'address', '203.0.113.10/24']) + + # VRRP config + self.cli_set(vrrp_base + [group, 'description', group]) + self.cli_set(vrrp_base + [group, 'interface', vrrp_interface]) + self.cli_set(vrrp_base + [group, 'address', vip + '/24']) + self.cli_set(vrrp_base + [group, 'vrid', vrid]) + + # Virtual-server config + self.cli_set(vserver_base + [vip, 'algorithm', algo]) + self.cli_set(vserver_base + [vip, 'delay-loop', delay]) + self.cli_set(vserver_base + [vip, 'forward-method', method]) + self.cli_set(vserver_base + [vip, 'persistence-timeout', persistence_timeout]) + self.cli_set(vserver_base + [vip, 'port', vport]) + self.cli_set(vserver_base + [vip, 'protocol', proto]) + for rs in rservers: + self.cli_set(vserver_base + [vip, 'real-server', rs, 'connection-timeout', connection_timeout]) + self.cli_set(vserver_base + [vip, 'real-server', rs, 'port', rport]) + + # commit changes + self.cli_commit() + + config = read_file(KEEPALIVED_CONF) + + # Keepalived vrrp + self.assertIn(f'# {group}', config) + self.assertIn(f'interface {vrrp_interface}', config) + self.assertIn(f'virtual_router_id {vrid}', config) + self.assertIn(f'priority 100', config) # default value + self.assertIn(f'advert_int 1', config) # default value + self.assertIn(f'preempt_delay 0', config) # default value + + # Keepalived virtual-server + self.assertIn(f'delay_loop {delay}', config) + self.assertIn(f'lb_algo lc', config) + self.assertIn(f'lb_kind {method.upper()}', config) + self.assertIn(f'persistence_timeout {persistence_timeout}', config) + self.assertIn(f'protocol {proto.upper()}', config) + for rs in rservers: + self.assertIn(f'real_server {rs} {rport}', config) + self.assertIn(f'{proto.upper()}_CHECK', config) + self.assertIn(f'connect_timeout {connection_timeout}', config) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py new file mode 100755 index 000000000..7d51bb393 --- /dev/null +++ b/src/conf_mode/high-availability.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2022 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 . + +import os + +from sys import exit +from ipaddress import ip_interface +from ipaddress import IPv4Interface +from ipaddress import IPv6Interface + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.ifconfig.vrrp import VRRP +from vyos.template import render +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 +from vyos.util import call +from vyos.util import is_systemd_service_running +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base = ['high-availability'] + base_vrrp = ['high-availability', 'vrrp'] + if not conf.exists(base): + return None + + ha = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + if 'vrrp' in ha: + if 'group' in ha['vrrp']: + default_values_vrrp = defaults(base_vrrp + ['group']) + for group in ha['vrrp']['group']: + ha['vrrp']['group'][group] = dict_merge(default_values_vrrp, ha['vrrp']['group'][group]) + + # Merge per virtual-server default values + if 'virtual_server' in ha: + default_values = defaults(base + ['virtual-server']) + for vs in ha['virtual_server']: + ha['virtual_server'][vs] = dict_merge(default_values, ha['virtual_server'][vs]) + + ## Get the sync group used for conntrack-sync + conntrack_path = ['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group'] + if conf.exists(conntrack_path): + ha['conntrack_sync_group'] = conf.return_value(conntrack_path) + + return ha + +def verify(ha): + if not ha: + return None + + used_vrid_if = [] + if 'vrrp' in ha and 'group' in ha['vrrp']: + for group, group_config in ha['vrrp']['group'].items(): + # Check required fields + if 'vrid' not in group_config: + raise ConfigError(f'VRID is required but not set in VRRP group "{group}"') + + if 'interface' not in group_config: + raise ConfigError(f'Interface is required but not set in VRRP group "{group}"') + + if 'address' not in group_config: + raise ConfigError(f'Virtual IP address is required but not set in VRRP group "{group}"') + + if 'authentication' in group_config: + if not {'password', 'type'} <= set(group_config['authentication']): + raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"') + + # We can not use a VRID once per interface + interface = group_config['interface'] + vrid = group_config['vrid'] + tmp = {'interface': interface, 'vrid': vrid} + if tmp in used_vrid_if: + raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface}"!') + used_vrid_if.append(tmp) + + # 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_config['address'])) + vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs)) + vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs)) + + if vaddrs4 and vaddrs6: + raise ConfigError(f'VRRP group "{group}" mixes IPv4 and IPv6 virtual addresses, this is not allowed.\n' \ + 'Create individual groups for IPv4 and IPv6!') + if vaddrs4: + if 'hello_source_address' in group_config: + if is_ipv6(group_config['hello_source_address']): + raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!') + + if 'peer_address' in group_config: + if is_ipv6(group_config['peer_address']): + raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!') + + if vaddrs6: + if 'hello_source_address' in group_config: + if is_ipv4(group_config['hello_source_address']): + raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!') + + if 'peer_address' in group_config: + if is_ipv4(group_config['peer_address']): + raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!') + # Check sync groups + if 'vrrp' in ha and 'sync_group' in ha['vrrp']: + for sync_group, sync_config in ha['vrrp']['sync_group'].items(): + if 'member' in sync_config: + for member in sync_config['member']: + if member not in ha['vrrp']['group']: + raise ConfigError(f'VRRP sync-group "{sync_group}" refers to VRRP group "{member}", '\ + 'but it does not exist!') + + # Virtual-server + if 'virtual_server' in ha: + for vs, vs_config in ha['virtual_server'].items(): + if 'port' not in vs_config: + raise ConfigError(f'Port is required but not set for virtual-server "{vs}"') + if 'real_server' not in vs_config: + raise ConfigError(f'Real-server ip is required but not set for virtual-server "{vs}"') + # Real-server + for rs, rs_config in vs_config['real_server'].items(): + if 'port' not in rs_config: + raise ConfigError(f'Port is required but not set for virtual-server "{vs}" real-server "{rs}"') + + +def generate(ha): + if not ha: + return None + + render(VRRP.location['config'], 'high-availability/keepalived.conf.tmpl', ha) + return None + +def apply(ha): + service_name = 'keepalived.service' + if not ha: + call(f'systemctl stop {service_name}') + return None + + # XXX: T3944 - reload keepalived configuration if service is already running + # to not cause any service disruption when applying changes. + if is_systemd_service_running(service_name): + call(f'systemctl reload {service_name}') + else: + call(f'systemctl restart {service_name}') + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py deleted file mode 100755 index c72efc61f..000000000 --- a/src/conf_mode/vrrp.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2021 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 . - -import os - -from sys import exit -from ipaddress import ip_interface -from ipaddress import IPv4Interface -from ipaddress import IPv6Interface - -from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.ifconfig.vrrp import VRRP -from vyos.template import render -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 -from vyos.util import call -from vyos.util import is_systemd_service_running -from vyos.xml import defaults -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base = ['high-availability', 'vrrp'] - if not conf.exists(base): - return None - - vrrp = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - if 'group' in vrrp: - default_values = defaults(base + ['group']) - for group in vrrp['group']: - vrrp['group'][group] = dict_merge(default_values, vrrp['group'][group]) - - ## Get the sync group used for conntrack-sync - conntrack_path = ['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group'] - if conf.exists(conntrack_path): - vrrp['conntrack_sync_group'] = conf.return_value(conntrack_path) - - return vrrp - -def verify(vrrp): - if not vrrp: - return None - - used_vrid_if = [] - if 'group' in vrrp: - for group, group_config in vrrp['group'].items(): - # Check required fields - if 'vrid' not in group_config: - raise ConfigError(f'VRID is required but not set in VRRP group "{group}"') - - if 'interface' not in group_config: - raise ConfigError(f'Interface is required but not set in VRRP group "{group}"') - - if 'address' not in group_config: - raise ConfigError(f'Virtual IP address is required but not set in VRRP group "{group}"') - - if 'authentication' in group_config: - if not {'password', 'type'} <= set(group_config['authentication']): - raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"') - - # We can not use a VRID once per interface - interface = group_config['interface'] - vrid = group_config['vrid'] - tmp = {'interface': interface, 'vrid': vrid} - if tmp in used_vrid_if: - raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface}"!') - used_vrid_if.append(tmp) - - # 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_config['address'])) - vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs)) - vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs)) - - if vaddrs4 and vaddrs6: - raise ConfigError(f'VRRP group "{group}" mixes IPv4 and IPv6 virtual addresses, this is not allowed.\n' \ - 'Create individual groups for IPv4 and IPv6!') - if vaddrs4: - if 'hello_source_address' in group_config: - if is_ipv6(group_config['hello_source_address']): - raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!') - - if 'peer_address' in group_config: - if is_ipv6(group_config['peer_address']): - raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!') - - if vaddrs6: - if 'hello_source_address' in group_config: - if is_ipv4(group_config['hello_source_address']): - raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!') - - if 'peer_address' in group_config: - if is_ipv4(group_config['peer_address']): - raise ConfigError(f'VRRP group "{group}" uses IPv6 but peer-address is IPv4!') - # Check sync groups - if 'sync_group' in vrrp: - for sync_group, sync_config in vrrp['sync_group'].items(): - if 'member' in sync_config: - for member in sync_config['member']: - if member not in vrrp['group']: - raise ConfigError(f'VRRP sync-group "{sync_group}" refers to VRRP group "{member}", '\ - 'but it does not exist!') - -def generate(vrrp): - if not vrrp: - return None - - render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl', vrrp) - return None - -def apply(vrrp): - service_name = 'keepalived.service' - if not vrrp: - call(f'systemctl stop {service_name}') - return None - - # XXX: T3944 - reload keepalived configuration if service is already running - # to not cause any service disruption when applying changes. - if is_systemd_service_running(service_name): - call(f'systemctl reload {service_name}') - else: - call(f'systemctl restart {service_name}') - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) -- cgit v1.2.3