summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
authorLulu Cathrinus Grimalkin <me@erkin.party>2021-11-24 14:27:08 +0300
committerGitHub <noreply@github.com>2021-11-24 14:27:08 +0300
commitc0eff50f7be4ee365d0b5ce828f64a66574c4f06 (patch)
treebcffc2c52f29d12ef20854fbea2d2e60b7f7627e /src/conf_mode
parentc0b09fe341c7ddced42479e0192a28ca553e30d6 (diff)
parent771301fea060467945e6c55379dd8e761aa9ad9d (diff)
downloadvyos-1x-c0eff50f7be4ee365d0b5ce828f64a66574c4f06.tar.gz
vyos-1x-c0eff50f7be4ee365d0b5ce828f64a66574c4f06.zip
Merge branch 'vyos:equuleus' into equuleus
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/dhcp_server.py55
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py2
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py15
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py2
-rwxr-xr-xsrc/conf_mode/host_name.py49
-rwxr-xr-xsrc/conf_mode/http-api.py12
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py91
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py50
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py47
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py37
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py4
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py39
-rwxr-xr-xsrc/conf_mode/protocols_isis.py258
-rwxr-xr-xsrc/conf_mode/protocols_rip.py3
-rwxr-xr-xsrc/conf_mode/snmp.py19
-rwxr-xr-xsrc/conf_mode/system-login-banner.py15
-rwxr-xr-xsrc/conf_mode/system-login.py6
-rwxr-xr-xsrc/conf_mode/system_console.py70
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py67
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py2
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py6
-rwxr-xr-xsrc/conf_mode/vrrp.py348
22 files changed, 578 insertions, 619 deletions
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index cdee72e09..a8cef5ebf 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -148,20 +148,35 @@ def verify(dhcp):
'At least one DHCP shared network must be configured.')
# Inspect shared-network/subnet
- failover_names = []
listen_ok = False
subnets = []
+ failover_ok = False
+ shared_networks = len(dhcp['shared_network_name'])
+ disabled_shared_networks = 0
+
# A shared-network requires a subnet definition
for network, network_config in dhcp['shared_network_name'].items():
+ if 'disable' in network_config:
+ disabled_shared_networks += 1
+
if 'subnet' not in network_config:
raise ConfigError(f'No subnets defined for {network}. At least one\n' \
'lease subnet must be configured.')
for subnet, subnet_config in network_config['subnet'].items():
- if 'static_route' in subnet_config and len(subnet_config['static_route']) != 2:
- raise ConfigError('Missing DHCP static-route parameter(s):\n' \
- 'destination-subnet | router must be defined!')
+ # All delivered static routes require a next-hop to be set
+ if 'static_route' in subnet_config:
+ for route, route_option in subnet_config['static_route'].items():
+ if 'next_hop' not in route_option:
+ raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!')
+
+ # DHCP failover needs at least one subnet that uses it
+ if 'enable_failover' in subnet_config:
+ if 'failover' not in dhcp:
+ raise ConfigError(f'Can not enable failover for "{subnet}" in "{network}".\n' \
+ 'Failover is not configured globally!')
+ failover_ok = True
# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
@@ -191,23 +206,6 @@ def verify(dhcp):
tmp = IPRange(range_config['start'], range_config['stop'])
networks.append(tmp)
- if 'failover' in subnet_config:
- for key in ['local_address', 'peer_address', 'name', 'status']:
- if key not in subnet_config['failover']:
- raise ConfigError(f'Missing DHCP failover parameter "{key}"!')
-
- # Failover names must be uniquie
- if subnet_config['failover']['name'] in failover_names:
- name = subnet_config['failover']['name']
- raise ConfigError(f'DHCP failover names must be unique:\n' \
- f'{name} has already been configured!')
- failover_names.append(subnet_config['failover']['name'])
-
- # Failover requires start/stop ranges for pool
- if 'range' not in subnet_config:
- raise ConfigError(f'DHCP failover requires at least one start-stop range to be configured\n'\
- f'within shared-network "{network}, {subnet}" for using failover!')
-
# Exclude addresses must be in bound
if 'exclude' in subnet_config:
for exclude in subnet_config['exclude']:
@@ -234,7 +232,7 @@ def verify(dhcp):
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if 'disable' not in network_config:
- if is_subnet_connected(subnet, primary=True):
+ if is_subnet_connected(subnet, primary=False):
listen_ok = True
# Subnets must be non overlapping
@@ -251,6 +249,19 @@ def verify(dhcp):
if net.overlaps(net2):
raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ # Prevent 'disable' for shared-network if only one network is configured
+ if (shared_networks - disabled_shared_networks) < 1:
+ raise ConfigError(f'At least one shared network must be active!')
+
+ if 'failover' in dhcp:
+ if not failover_ok:
+ raise ConfigError('DHCP failover must be enabled for at least one subnet!')
+
+ for key in ['name', 'remote', 'source_address', 'status']:
+ if key not in dhcp['failover']:
+ tmp = key.replace('_', '-')
+ raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!')
+
for address in (dict_search('listen_address', dhcp) or []):
if is_addr_assigned(address):
listen_ok = True
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 175300bb0..e6a2e4486 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -128,7 +128,7 @@ def verify(dhcpv6):
# Subnets must be unique
if subnet in subnets:
- raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network']))
+ raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!')
subnets.append(subnet)
# DHCPv6 requires at least one configured address range or one static mapping
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index c44e6c974..06366362a 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -66,21 +66,6 @@ def get_config(config=None):
if conf.exists(base_nameservers_dhcp):
dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)})
- # Split the source_address property into separate IPv4 and IPv6 lists
- # NOTE: In future versions of pdns-recursor (> 4.4.0), this logic can be removed
- # as both IPv4 and IPv6 addresses can be specified in a single setting.
- source_address_v4 = []
- source_address_v6 = []
-
- for source_address in dns['source_address']:
- if is_ipv6(source_address):
- source_address_v6.append(source_address)
- else:
- source_address_v4.append(source_address)
-
- dns.update({'source_address_v4': source_address_v4})
- dns.update({'source_address_v6': source_address_v6})
-
return dns
def verify(dns):
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 9cae29481..0a4559ade 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -306,7 +306,7 @@ def verify(config):
source_ip_presented = True
break
if not source_ip_presented:
- raise ConfigError("Your \"netflow source-ip\" does not exist in the system")
+ print("Warning: your \"netflow source-ip\" does not exist in the system")
# check if engine-id compatible with selected protocol version
if config['netflow']['engine-id']:
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index f4c75c257..a7135911d 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# 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
@@ -14,10 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-conf-mode script for 'system host-name' and 'system domain-name'.
-"""
-
import re
import sys
import copy
@@ -25,10 +21,13 @@ import copy
import vyos.util
import vyos.hostsd_client
-from vyos.config import Config
from vyos import ConfigError
-from vyos.util import cmd, call, process_named_running
-
+from vyos.config import Config
+from vyos.ifconfig import Section
+from vyos.template import is_ip
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import process_named_running
from vyos import airbag
airbag.enable()
@@ -37,7 +36,7 @@ default_config_data = {
'domain_name': '',
'domain_search': [],
'nameserver': [],
- 'nameservers_dhcp_interfaces': [],
+ 'nameservers_dhcp_interfaces': {},
'static_host_mapping': {}
}
@@ -51,29 +50,37 @@ def get_config(config=None):
hosts = copy.deepcopy(default_config_data)
- hosts['hostname'] = conf.return_value("system host-name")
+ hosts['hostname'] = conf.return_value(['system', 'host-name'])
# This may happen if the config is not loaded yet,
# e.g. if run by cloud-init
if not hosts['hostname']:
hosts['hostname'] = default_config_data['hostname']
- if conf.exists("system domain-name"):
- hosts['domain_name'] = conf.return_value("system domain-name")
+ if conf.exists(['system', 'domain-name']):
+ hosts['domain_name'] = conf.return_value(['system', 'domain-name'])
hosts['domain_search'].append(hosts['domain_name'])
- for search in conf.return_values("system domain-search domain"):
+ for search in conf.return_values(['system', 'domain-search', 'domain']):
hosts['domain_search'].append(search)
- hosts['nameserver'] = conf.return_values("system name-server")
+ if conf.exists(['system', 'name-server']):
+ for ns in conf.return_values(['system', 'name-server']):
+ if is_ip(ns):
+ hosts['nameserver'].append(ns)
+ else:
+ tmp = ''
+ if_type = Section.section(ns)
+ if conf.exists(['interfaces', if_type, ns, 'address']):
+ tmp = conf.return_values(['interfaces', if_type, ns, 'address'])
- hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp")
+ hosts['nameservers_dhcp_interfaces'].update({ ns : tmp })
# system static-host-mapping
- for hn in conf.list_nodes('system static-host-mapping host-name'):
+ for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']):
hosts['static_host_mapping'][hn] = {}
- hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet')
- hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias')
+ hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet'])
+ hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias'])
return hosts
@@ -103,8 +110,10 @@ def verify(hosts):
if not hostname_regex.match(a) and len(a) != 0:
raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"')
- # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't
- # exist or doesn't have address dhcp(v6)
+ for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items():
+ # Warnin user if interface does not have DHCP or DHCPv6 configured
+ if not set(interface_config).intersection(['dhcp', 'dhcpv6']):
+ print(f'WARNING: "{interface}" is not a DHCP interface but uses DHCP name-server option!')
return None
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 472eb77e4..7e4b117c8 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -19,6 +19,7 @@
import sys
import os
import json
+import time
from copy import deepcopy
import vyos.defaults
@@ -34,11 +35,6 @@ config_file = '/etc/vyos/http-api.conf'
vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode']
-# XXX: this model will need to be extended for tag nodes
-dependencies = [
- 'https.py',
-]
-
def get_config(config=None):
http_api = deepcopy(vyos.defaults.api_data)
x = http_api.get('api_keys')
@@ -103,8 +99,10 @@ def apply(http_api):
else:
call('systemctl stop vyos-http-api.service')
- for dep in dependencies:
- cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
+ # Let uvicorn settle before restarting Nginx
+ time.sleep(2)
+
+ cmd(f'{vyos_conf_scripts_dir}/https.py', raising=ConfigError)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 5b0046a72..4d3ebc587 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -18,7 +18,6 @@ import os
from sys import exit
from netifaces import interfaces
-import re
from vyos.config import Config
from vyos.configdict import get_interface_dict
@@ -41,26 +40,6 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def helper_check_removed_vlan(conf,bridge,key,key_mangling):
- key_update = re.sub(key_mangling[0], key_mangling[1], key)
- if dict_search('member.interface', bridge):
- for interface in bridge['member']['interface']:
- tmp = leaf_node_changed(conf, ['member', 'interface',interface,key])
- if tmp:
- if 'member' in bridge:
- if 'interface' in bridge['member']:
- if interface in bridge['member']['interface']:
- bridge['member']['interface'][interface].update({f'{key_update}_removed': tmp })
- else:
- bridge['member']['interface'].update({interface: {f'{key_update}_removed': tmp }})
- else:
- bridge['member'].update({ 'interface': {interface: {f'{key_update}_removed': tmp }}})
- else:
- bridge.update({'member': { 'interface': {interface: {f'{key_update}_removed': tmp }}}})
-
- return bridge
-
-
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -80,12 +59,6 @@ def get_config(config=None):
bridge['member'].update({'interface_remove': tmp })
else:
bridge.update({'member': {'interface_remove': tmp }})
-
-
- # determine which members vlan have been removed
-
- bridge = helper_check_removed_vlan(conf,bridge,'native-vlan',('-', '_'))
- bridge = helper_check_removed_vlan(conf,bridge,'allowed-vlan',('-', '_'))
if dict_search('member.interface', bridge):
# XXX: T2665: we need a copy of the dict keys for iteration, else we will get:
@@ -99,7 +72,6 @@ def get_config(config=None):
# the default dictionary is not properly paged into the dict (see T2665)
# thus we will ammend it ourself
default_member_values = defaults(base + ['member', 'interface'])
- vlan_aware = False
for interface,interface_config in bridge['member']['interface'].items():
bridge['member']['interface'][interface] = dict_merge(
default_member_values, bridge['member']['interface'][interface])
@@ -120,19 +92,11 @@ def get_config(config=None):
# Bridge members must not have an assigned address
tmp = has_address_configured(conf, interface)
if tmp: bridge['member']['interface'][interface].update({'has_address' : ''})
-
+
# VLAN-aware bridge members must not have VLAN interface configuration
- if 'native_vlan' in interface_config:
- vlan_aware = True
-
- if 'allowed_vlan' in interface_config:
- vlan_aware = True
-
-
- if vlan_aware:
- tmp = has_vlan_subinterface_configured(conf,interface)
- if tmp:
- if tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''})
+ tmp = has_vlan_subinterface_configured(conf,interface)
+ if 'enable_vlan' in bridge and tmp:
+ bridge['member']['interface'][interface].update({'has_vlan' : ''})
return bridge
@@ -142,8 +106,8 @@ def verify(bridge):
verify_dhcpv6(bridge)
verify_vrf(bridge)
-
- vlan_aware = False
+
+ ifname = bridge['ifname']
if dict_search('member.interface', bridge):
for interface, interface_config in bridge['member']['interface'].items():
@@ -166,31 +130,24 @@ def verify(bridge):
if 'has_address' in interface_config:
raise ConfigError(error_msg + 'it has an address assigned!')
-
- if 'has_vlan' in interface_config:
- raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
-
- # VLAN-aware bridge members must not have VLAN interface configuration
- if 'native_vlan' in interface_config:
- vlan_aware = True
-
- if 'allowed_vlan' in interface_config:
- vlan_aware = True
-
- if vlan_aware and 'wlan' in interface:
- raise ConfigError(error_msg + 'VLAN aware cannot be set!')
-
- if 'allowed_vlan' in interface_config:
- for vlan in interface_config['allowed_vlan']:
- if re.search('[0-9]{1,4}-[0-9]{1,4}', vlan):
- vlan_range = vlan.split('-')
- if int(vlan_range[0]) <1 and int(vlan_range[0])>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
- if int(vlan_range[1]) <1 and int(vlan_range[1])>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
- else:
- if int(vlan) <1 and int(vlan)>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
+
+ if 'enable_vlan' in bridge:
+ if 'has_vlan' in interface_config:
+ raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
+
+ if 'wlan' in interface:
+ raise ConfigError(error_msg + 'VLAN aware cannot be set!')
+ else:
+ for option in ['allowed_vlan', 'native_vlan']:
+ if option in interface_config:
+ raise ConfigError('Can not use VLAN options on non VLAN aware bridge')
+
+ if 'enable_vlan' in bridge:
+ if dict_search('vif.1', bridge):
+ raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface')
+ else:
+ if dict_search('vif', bridge):
+ raise ConfigError(f'You must first activate "enable-vlan" of {ifname} bridge to use "vif"')
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 349b0e7a3..de851262b 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -62,12 +62,6 @@ def verify(ethernet):
ifname = ethernet['ifname']
verify_interface_exists(ifname)
-
- # No need to check speed and duplex keys as both have default values.
- if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
- (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
- raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
-
verify_mtu(ethernet)
verify_mtu_ipv6(ethernet)
verify_dhcpv6(ethernet)
@@ -76,25 +70,31 @@ def verify(ethernet):
verify_eapol(ethernet)
verify_mirror(ethernet)
- # verify offloading capabilities
- if dict_search('offload.rps', ethernet) != None:
- if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
- raise ConfigError('Interface does not suport RPS!')
+ ethtool = Ethtool(ifname)
+ # No need to check speed and duplex keys as both have default values.
+ if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
+ (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
+ raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
- driver = EthernetIf(ifname).get_driver_name()
- # T3342 - Xen driver requires special treatment
- if driver == 'vif':
- if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
- raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
- 'for MTU size larger then 1500 bytes')
+ if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
+ # We need to verify if the requested speed and duplex setting is
+ # supported by the underlaying NIC.
+ speed = ethernet['speed']
+ duplex = ethernet['duplex']
+ if not ethtool.check_speed_duplex(speed, duplex):
+ raise ConfigError(f'Adapter does not support changing speed and duplex '\
+ f'settings to: {speed}/{duplex}!')
+
+ if 'disable_flow_control' in ethernet:
+ if not ethtool.check_flow_control():
+ raise ConfigError('Adapter does not support changing flow-control settings!')
- ethtool = Ethtool(ifname)
if 'ring_buffer' in ethernet:
- max_rx = ethtool.get_rx_buffer()
+ max_rx = ethtool.get_ring_buffer_max('rx')
if not max_rx:
raise ConfigError('Driver does not support RX ring-buffer configuration!')
- max_tx = ethtool.get_tx_buffer()
+ max_tx = ethtool.get_ring_buffer_max('tx')
if not max_tx:
raise ConfigError('Driver does not support TX ring-buffer configuration!')
@@ -108,6 +108,18 @@ def verify(ethernet):
raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\
f'size of "{max_tx}" bytes!')
+ # verify offloading capabilities
+ if dict_search('offload.rps', ethernet) != None:
+ if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
+ raise ConfigError('Interface does not suport RPS!')
+
+ driver = ethtool.get_driver_name()
+ # T3342 - Xen driver requires special treatment
+ if driver == 'vif':
+ if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
+ raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
+ 'for MTU size larger then 1500 bytes')
+
if {'is_bond_member', 'mac'} <= set(ethernet):
print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" '
f'is a member of bond "{is_bond_member}"'.format(**ethernet))
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 0a420f7bf..ae35ed3c4 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -40,6 +40,7 @@ from vyos.util import call
from vyos.util import chown
from vyos.util import chmod_600
from vyos.util import dict_search
+from vyos.util import makedir
from vyos.validate import is_addr_assigned
from vyos import ConfigError
@@ -50,6 +51,7 @@ user = 'openvpn'
group = 'openvpn'
cfg_file = '/run/openvpn/{ifname}.conf'
+service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf'
def checkCertHeader(header, filename):
"""
@@ -79,9 +81,6 @@ def get_config(config=None):
openvpn = get_interface_dict(conf, base)
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
- openvpn['daemon_user'] = user
- openvpn['daemon_group'] = group
-
return openvpn
def verify(openvpn):
@@ -425,6 +424,10 @@ def verify(openvpn):
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
+ # create base config directory on demand
+ makedir(directory, user, group)
+ # enforce proper permissions on /run/openvpn
+ chown(directory, user, group)
# we can't know in advance which clients have been removed,
# thus all client configs will be removed and re-added on demand
@@ -432,22 +435,28 @@ def generate(openvpn):
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
+ # Remove systemd directories with overrides
+ service_dir = os.path.dirname(service_file.format(**openvpn))
+ if os.path.isdir(service_dir):
+ rmtree(service_dir, ignore_errors=True)
+
if 'deleted' in openvpn or 'disable' in openvpn:
return None
# create client config directory on demand
- if not os.path.exists(ccd_dir):
- os.makedirs(ccd_dir, 0o755)
- chown(ccd_dir, user, group)
+ makedir(ccd_dir, user, group)
- # Fix file permissons for keys
- fix_permissions = []
+ # Fix file permissons for site2site shared secret
+ if dict_search('shared_secret_key_file', openvpn):
+ chmod_600(openvpn['shared_secret_key_file'])
+ chown(openvpn['shared_secret_key_file'], user, group)
- tmp = dict_search('shared_secret_key_file', openvpn)
- if tmp: fix_permissions.append(openvpn['shared_secret_key_file'])
-
- tmp = dict_search('tls.key_file', openvpn)
- if tmp: fix_permissions.append(tmp)
+ # Fix file permissons for TLS certificate and keys
+ for tls in ['auth_file', 'ca_cert_file', 'cert_file', 'crl_file',
+ 'crypt_file', 'dh_file', 'key_file']:
+ if dict_search(f'tls.{tls}', openvpn):
+ chmod_600(openvpn['tls'][tls])
+ chown(openvpn['tls'][tls], user, group)
# Generate User/Password authentication file
if 'authentication' in openvpn:
@@ -474,18 +483,20 @@ def generate(openvpn):
render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn,
formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
- # Fixup file permissions
- for file in fix_permissions:
- chmod_600(file)
+ # Render 20-override.conf for OpenVPN service
+ render(service_file.format(**openvpn), 'openvpn/service-override.conf.tmpl', openvpn,
+ formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
+ # Reload systemd services config to apply an override
+ call(f'systemctl daemon-reload')
return None
def apply(openvpn):
interface = openvpn['ifname']
- call(f'systemctl stop openvpn@{interface}.service')
# Do some cleanup when OpenVPN is disabled/deleted
if 'deleted' in openvpn or 'disable' in openvpn:
+ call(f'systemctl stop openvpn@{interface}.service')
for cleanup_file in glob(f'/run/openvpn/{interface}.*'):
if os.path.isfile(cleanup_file):
os.unlink(cleanup_file)
@@ -497,7 +508,7 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- call(f'systemctl start openvpn@{interface}.service')
+ call(f'systemctl reload-or-restart openvpn@{interface}.service')
conf = VTunIf.get_config()
conf['device_type'] = openvpn['device_type']
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index e5958e9ae..2798d321f 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -18,6 +18,7 @@ import os
from sys import exit
from netifaces import interfaces
+from ipaddress import IPv4Address
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -31,6 +32,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vrf
from vyos.configverify import verify_tunnel
from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -74,6 +76,41 @@ def verify(tunnel):
verify_tunnel(tunnel)
+ # If tunnel source address any and key not set
+ if tunnel['encapsulation'] in ['gre'] and \
+ dict_search('source_address', tunnel) == '0.0.0.0' and \
+ dict_search('parameters.ip.key', tunnel) == None:
+ raise ConfigError('Tunnel parameters ip key must be set!')
+
+ if tunnel['encapsulation'] in ['gre', 'gretap']:
+ if dict_search('parameters.ip.key', tunnel) != None:
+ # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+ # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+ for tunnel_if in Section.interfaces('tunnel'):
+ # It makes no sense to run the test for re-used GRE keys on our
+ # own interface we are currently working on
+ if tunnel['ifname'] == tunnel_if:
+ continue
+ tunnel_cfg = get_interface_config(tunnel_if)
+ # no match on encapsulation - bail out
+ if dict_search('linkinfo.info_kind', tunnel_cfg) != tunnel['encapsulation']:
+ continue
+ new_source_address = dict_search('source_address', tunnel)
+ # Convert tunnel key to ip key, format "ip -j link show"
+ # 1 => 0.0.0.1, 999 => 0.0.3.231
+ orig_new_key = dict_search('parameters.ip.key', tunnel)
+ new_key = IPv4Address(int(orig_new_key))
+ new_key = str(new_key)
+ if dict_search('address', tunnel_cfg) == new_source_address and \
+ dict_search('linkinfo.info_data.ikey', tunnel_cfg) == new_key:
+ raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
+ f'is already used for tunnel "{tunnel_if}"!')
+
+ # Keys are not allowed with ipip and sit tunnels
+ if tunnel['encapsulation'] in ['ipip', 'sit']:
+ if dict_search('parameters.ip.key', tunnel) != None:
+ raise ConfigError('Keys are not allowed with ipip and sit tunnels!')
+
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index 31c599145..cb46b3723 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -25,7 +25,9 @@ from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_vrf
from vyos.ifconfig import WWANIf
from vyos.util import cmd
+from vyos.util import call
from vyos.util import dict_search
+from vyos.util import DEVNULL
from vyos.template import render
from vyos import ConfigError
from vyos import airbag
@@ -89,7 +91,7 @@ def apply(wwan):
options += ',user={user},password={password}'.format(**wwan['authentication'])
command = f'{base_cmd} --simple-connect="{options}"'
- cmd(command)
+ call(command, stdout=DEVNULL)
w.update(wwan)
return None
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 7ca2d9b44..771f635a0 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -18,7 +18,9 @@ import re
import os
from time import sleep
-from sys import exit
+
+# Top level import so that configd can override it
+from sys import argv
from vyos.config import Config
from vyos import ConfigError
@@ -216,6 +218,20 @@ def generate(data):
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
+def is_charon_responsive():
+ # Check if charon responds to strokes
+ #
+ # Sometimes it takes time to fully initialize,
+ # so waiting for the process to come to live isn't always enough
+ #
+ # There's no official "no-op" stroke so we use the "memusage" stroke as a substitute
+ from os import system
+ res = system("ipsec stroke memusage >&/dev/null")
+ if res == 0:
+ return True
+ else:
+ return False
+
def restart_ipsec():
try:
# Restart the IPsec daemon when it's running.
@@ -223,17 +239,28 @@ def restart_ipsec():
# there's a chance that this script will run before charon is up,
# so we can't assume it's running and have to check and wait if needed.
- # First, wait for charon to get started by the old ipsec.pl script.
+ # But before everything else, there's a catch!
+ # This script is run from _two_ places: "vpn ipsec options" and the top level "vpn" node
+ # When IPsec isn't set up yet, and a user wants to commit an IPsec config with some
+ # "vpn ipsec settings", this script will first be called before StrongSWAN is started by vpn-config.pl!
+ # Thus if this script is run from "settings" _and_ charon is unresponsive,
+ # we shouldn't wait for it, else there will be a deadlock.
+ # We indicate that by running the script under vyshim from "vpn ipsec options" (which sets a variable named "argv")
+ # and running it without configd from "vpn ipsec"
+ if "from-options" in argv:
+ if not is_charon_responsive():
+ return
+
+ # If we got this far, then we actually need to restart StrongSWAN
+
+ # First, wait for charon to get started by the old vpn-config.pl script.
from time import sleep, time
from os import system
now = time()
while True:
if (time() - now) > 60:
raise OSError("Timeout waiting for the IPsec process to become responsive")
- # There's no oficial "no-op" stroke,
- # so we use memusage to check if charon is alive and responsive
- res = system("ipsec stroke memusage >&/dev/null")
- if res == 0:
+ if is_charon_responsive():
break
sleep(5)
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index da91f3b11..0c179b724 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-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
@@ -19,12 +19,16 @@ import os
from sys import exit
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.configdict import node_changed
-from vyos import ConfigError
-from vyos.util import call
+from vyos.configverify import verify_common_route_maps
+from vyos.configverify import verify_interface_exists
+from vyos.ifconfig import Interface
from vyos.util import dict_search
-from vyos.template import render
+from vyos.util import get_interface_config
from vyos.template import render_to_string
+from vyos.xml import defaults
+from vyos import ConfigError
from vyos import frr
from vyos import airbag
airbag.enable()
@@ -34,126 +38,172 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'isis']
- isis = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ base = ['protocols', 'isis']
+ isis = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ isis['interface_removed'] = list(interfaces_removed)
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ isis.update({'deleted' : ''})
+ return isis
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ # XXX: Note that we can not call defaults(base), as defaults does not work
+ # on an instance of a tag node.
+ default_values = defaults(base)
+ # merge in default values
+ isis = dict_merge(default_values, isis)
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify().
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = conf.get_config_dict(['policy'])
+ # Merge policy dict into "regular" config dict
+ isis = dict_merge(tmp, isis)
return isis
def verify(isis):
# bail out early - looks like removal from running config
- if not isis:
+ if not isis or 'deleted' in isis:
return None
- for process, isis_config in isis.items():
- # If more then one isis process is defined (Frr only supports one)
- # http://docs.frrouting.org/en/latest/isisd.html#isis-router
- if len(isis) > 1:
- raise ConfigError('Only one isis process can be defined')
-
- # If network entity title (net) not defined
- if 'net' not in isis_config:
- raise ConfigError('ISIS net format iso is mandatory!')
-
- # If interface not set
- if 'interface' not in isis_config:
- raise ConfigError('ISIS interface is mandatory!')
-
- # If md5 and plaintext-password set at the same time
- if 'area_password' in isis_config:
- if {'md5', 'plaintext_password'} <= set(isis_config['encryption']):
- raise ConfigError('Can not use both md5 and plaintext-password for ISIS area-password!')
-
- # If one param from delay set, but not set others
- if 'spf_delay_ietf' in isis_config:
- required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn']
- exist_timers = []
- for elm_timer in required_timers:
- if elm_timer in isis_config['spf_delay_ietf']:
- exist_timers.append(elm_timer)
-
- exist_timers = set(required_timers).difference(set(exist_timers))
- if len(exist_timers) > 0:
- raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-'))
-
- # If Redistribute set, but level don't set
- if 'redistribute' in isis_config:
- proc_level = isis_config.get('level','').replace('-','_')
- for proto, proto_config in isis_config.get('redistribute', {}).get('ipv4', {}).items():
+ if 'net' not in isis:
+ raise ConfigError('Network entity is mandatory!')
+
+ # last byte in IS-IS area address must be 0
+ tmp = isis['net'].split('.')
+ if int(tmp[-1]) != 0:
+ raise ConfigError('Last byte of IS-IS network entity title must always be 0!')
+
+ verify_common_route_maps(isis)
+
+ # If interface not set
+ if 'interface' not in isis:
+ raise ConfigError('Interface used for routing updates is mandatory!')
+
+ for interface in isis['interface']:
+ verify_interface_exists(interface)
+ # Interface MTU must be >= configured lsp-mtu
+ mtu = Interface(interface).get_mtu()
+ area_mtu = isis['lsp_mtu']
+ # Recommended maximum PDU size = interface MTU - 3 bytes
+ recom_area_mtu = mtu - 3
+ if mtu < int(area_mtu) or int(area_mtu) > recom_area_mtu:
+ raise ConfigError(f'Interface {interface} has MTU {mtu}, ' \
+ f'current area MTU is {area_mtu}! \n' \
+ f'Recommended area lsp-mtu {recom_area_mtu} or less ' \
+ '(calculated on MTU size).')
+
+ # If md5 and plaintext-password set at the same time
+ for password in ['area_password', 'domain_password']:
+ if password in isis:
+ if {'md5', 'plaintext_password'} <= set(isis[password]):
+ tmp = password.replace('_', '-')
+ raise ConfigError(f'Can use either md5 or plaintext-password for {tmp}!')
+
+ # If one param from delay set, but not set others
+ if 'spf_delay_ietf' in isis:
+ required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn']
+ exist_timers = []
+ for elm_timer in required_timers:
+ if elm_timer in isis['spf_delay_ietf']:
+ exist_timers.append(elm_timer)
+
+ exist_timers = set(required_timers).difference(set(exist_timers))
+ if len(exist_timers) > 0:
+ raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-'))
+
+ # If Redistribute set, but level don't set
+ if 'redistribute' in isis:
+ proc_level = isis.get('level','').replace('-','_')
+ for afi in ['ipv4', 'ipv6']:
+ if afi not in isis['redistribute']:
+ continue
+
+ for proto, proto_config in isis['redistribute'][afi].items():
if 'level_1' not in proto_config and 'level_2' not in proto_config:
- raise ConfigError('Redistribute level-1 or level-2 should be specified in \"protocols isis {} redistribute ipv4 {}\"'.format(process, proto))
- for redistribute_level in proto_config.keys():
- if proc_level and proc_level != 'level_1_2' and proc_level != redistribute_level:
- raise ConfigError('\"protocols isis {0} redistribute ipv4 {2} {3}\" cannot be used with \"protocols isis {0} level {1}\"'.format(process, proc_level, proto, redistribute_level))
-
- # Segment routing checks
- if dict_search('segment_routing', isis_config):
- if dict_search('segment_routing.global_block', isis_config):
- high_label_value = dict_search('segment_routing.global_block.high_label_value', isis_config)
- low_label_value = dict_search('segment_routing.global_block.low_label_value', isis_config)
- # If segment routing global block high value is blank, throw error
- if low_label_value and not high_label_value:
- raise ConfigError('Segment routing global block high value must not be left blank')
- # If segment routing global block low value is blank, throw error
- if high_label_value and not low_label_value:
- raise ConfigError('Segment routing global block low value must not be left blank')
- # If segment routing global block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing global block low value must be lower than high value')
-
- if dict_search('segment_routing.local_block', isis_config):
- high_label_value = dict_search('segment_routing.local_block.high_label_value', isis_config)
- low_label_value = dict_search('segment_routing.local_block.low_label_value', isis_config)
- # If segment routing local block high value is blank, throw error
- if low_label_value and not high_label_value:
- raise ConfigError('Segment routing local block high value must not be left blank')
- # If segment routing local block low value is blank, throw error
- if high_label_value and not low_label_value:
- raise ConfigError('Segment routing local block low value must not be left blank')
- # If segment routing local block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing local block low value must be lower than high value')
+ raise ConfigError(f'Redistribute level-1 or level-2 should be specified in ' \
+ f'"protocols isis {process} redistribute {afi} {proto}"!')
+
+ for redistr_level, redistr_config in proto_config.items():
+ if proc_level and proc_level != 'level_1_2' and proc_level != redistr_level:
+ raise ConfigError(f'"protocols isis {process} redistribute {afi} {proto} {redistr_level}" ' \
+ f'can not be used with \"protocols isis {process} level {proc_level}\"')
+
+ # Segment routing checks
+ if dict_search('segment_routing.global_block', isis):
+ high_label_value = dict_search('segment_routing.global_block.high_label_value', isis)
+ low_label_value = dict_search('segment_routing.global_block.low_label_value', isis)
+
+ # If segment routing global block high value is blank, throw error
+ if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
+ raise ConfigError('Segment routing global block requires both low and high value!')
+
+ # If segment routing global block low value is higher than the high value, throw error
+ if int(low_label_value) > int(high_label_value):
+ raise ConfigError('Segment routing global block low value must be lower than high value')
+
+ if dict_search('segment_routing.local_block', isis):
+ high_label_value = dict_search('segment_routing.local_block.high_label_value', isis)
+ low_label_value = dict_search('segment_routing.local_block.low_label_value', isis)
+
+ # If segment routing local block high value is blank, throw error
+ if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
+ raise ConfigError('Segment routing local block requires both high and low value!')
+
+ # If segment routing local block low value is higher than the high value, throw error
+ if int(low_label_value) > int(high_label_value):
+ raise ConfigError('Segment routing local block low value must be lower than high value')
return None
def generate(isis):
- if not isis:
- isis['new_frr_config'] = ''
+ if not isis or 'deleted' in isis:
+ isis['frr_isisd_config'] = ''
+ isis['frr_zebra_config'] = ''
return None
- # only one ISIS process is supported, so we can directly send the first key
- # of the config dict
- process = list(isis.keys())[0]
- isis[process]['process'] = process
-
- isis['new_frr_config'] = render_to_string('frr/isisd.frr.tmpl',
- isis[process])
-
+ isis['protocol'] = 'isis' # required for frr/route-map.frr.tmpl
+ isis['frr_zebra_config'] = render_to_string('frr/route-map.frr.tmpl', isis)
+ isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis)
return None
def apply(isis):
+ isis_daemon = 'isisd'
+ zebra_daemon = 'zebra'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(daemon='isisd')
- frr_cfg.modify_section(r'interface \S+', '')
- frr_cfg.modify_section(f'router isis \S+', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['new_frr_config'])
- frr_cfg.commit_configuration(daemon='isisd')
-
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if isis['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(daemon='isisd')
-
- # Debugging
- '''
- print('')
- print('--------- DEBUGGING ----------')
- print(f'Existing config:\n{frr_cfg["original_config"]}\n\n')
- print(f'Replacement config:\n{isis["new_frr_config"]}\n\n')
- print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n')
- '''
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section(r'(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
+ frr_cfg.load_configuration(isis_daemon)
+ frr_cfg.modify_section(f'^router isis VyOS$', '')
+
+ for key in ['interface', 'interface_removed']:
+ if key not in isis:
+ continue
+ for interface in isis[key]:
+ frr_cfg.modify_section(f'^interface {interface}$', '')
+
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config'])
+ frr_cfg.commit_configuration(isis_daemon)
+
+ # Save configuration to /run/frr/config/frr.conf
+ frr.save_configuration()
return None
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 8ddd705f2..f36abbf90 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -125,7 +125,7 @@ def get_config(config=None):
conf.set_level(base)
- # Get distribute list interface
+ # Get distribute list interface
for dist_iface in conf.list_nodes('distribute-list interface'):
# Set level 'distribute-list interface ethX'
conf.set_level(base + ['distribute-list', 'interface', dist_iface])
@@ -301,6 +301,7 @@ def apply(rip):
if os.path.exists(config_file):
call(f'vtysh -d ripd -f {config_file}')
+ call('sudo vtysh --writeconfig --noerror')
os.remove(config_file)
else:
print("File {0} not found".format(config_file))
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 3990e5735..0fbe90cce 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -20,13 +20,17 @@ from sys import exit
from vyos.config import Config
from vyos.configverify import verify_vrf
-from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random
+from vyos.snmpv3_hashgen import plaintext_to_md5
+from vyos.snmpv3_hashgen import plaintext_to_sha1
+from vyos.snmpv3_hashgen import random
from vyos.template import render
from vyos.template import is_ipv4
-from vyos.util import call, chmod_755
+from vyos.util import call
+from vyos.util import chmod_755
from vyos.validate import is_addr_assigned
from vyos.version import get_version_data
-from vyos import ConfigError, airbag
+from vyos import ConfigError
+from vyos import airbag
airbag.enable()
config_file_client = r'/etc/snmp/snmp.conf'
@@ -401,19 +405,20 @@ def verify(snmp):
addr = listen[0]
port = listen[1]
+ tmp = None
if is_ipv4(addr):
# example: udp:127.0.0.1:161
- listen = 'udp:' + addr + ':' + port
+ tmp = f'udp:{addr}:{port}'
elif snmp['ipv6_enabled']:
# example: udp6:[::1]:161
- listen = 'udp6:' + '[' + addr + ']' + ':' + port
+ tmp = f'udp6:[{addr}]:{port}'
# We only wan't to configure addresses that exist on the system.
# Hint the user if they don't exist
if is_addr_assigned(addr):
- snmp['listen_on'].append(listen)
+ if tmp: snmp['listen_on'].append(tmp)
else:
- print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+ print(f'WARNING: SNMP listen address {addr} not configured!')
verify_vrf(snmp)
diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py
index 569010735..2220d7b66 100755
--- a/src/conf_mode/system-login-banner.py
+++ b/src/conf_mode/system-login-banner.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-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
@@ -22,12 +22,13 @@ from vyos import airbag
airbag.enable()
motd="""
-The programs included with the Debian GNU/Linux system are free software;
-the exact distribution terms for each program are described in the
-individual files in /usr/share/doc/*/copyright.
+Check out project news at https://blog.vyos.io
+and feel free to report bugs at https://phabricator.vyos.net
-Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
-permitted by applicable law.
+You can change this banner using "set system login banner post-login" command.
+
+VyOS is a free software distribution that includes multiple components,
+you can check individual component licenses under /usr/share/doc/*/copyright
"""
@@ -36,7 +37,7 @@ PRELOGIN_NET_FILE = r'/etc/issue.net'
POSTLOGIN_FILE = r'/etc/motd'
default_config_data = {
- 'issue': 'Welcome to VyOS - \n \l\n',
+ 'issue': 'Welcome to VyOS - \\n \\l\n\n',
'issue_net': 'Welcome to VyOS\n',
'motd': motd
}
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 59ea1d34b..8aa43dd32 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -59,7 +59,7 @@ def get_config(config=None):
conf = Config()
base = ['system', 'login']
login = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
+ no_tag_node_value_mangle=True, get_first_key=True)
# users no longer existing in the running configuration need to be deleted
local_users = get_local_users()
@@ -246,7 +246,9 @@ def apply(login):
# XXX: Should we deny using root at all?
home_dir = getpwnam(user).pw_dir
render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.tmpl',
- user_config, permission=0o600, user=user, group='users')
+ user_config, permission=0o600,
+ formater=lambda _: _.replace("&quot;", '"'),
+ user=user, group='users')
except Exception as e:
raise ConfigError(f'Adding user "{user}" raised exception: "{e}"')
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index 33a546bd3..19b252513 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -18,9 +18,14 @@ import os
import re
from vyos.config import Config
-from vyos.util import call, read_file, write_file
+from vyos.configdict import dict_merge
+from vyos.util import call
+from vyos.util import read_file
+from vyos.util import write_file
from vyos.template import render
-from vyos import ConfigError, airbag
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
airbag.enable()
by_bus_dir = '/dev/serial/by-bus'
@@ -36,21 +41,27 @@ def get_config(config=None):
console = conf.get_config_dict(base, get_first_key=True)
# bail out early if no serial console is configured
- if 'device' not in console.keys():
+ if 'device' not in console:
return console
# convert CLI values to system values
- for device in console['device'].keys():
- # no speed setting has been configured - use default value
- if not 'speed' in console['device'][device].keys():
- tmp = { 'speed': '' }
- if device.startswith('hvc'):
- tmp['speed'] = 38400
- else:
- tmp['speed'] = 115200
+ default_values = defaults(base + ['device'])
+ for device, device_config in console['device'].items():
+ if 'speed' not in device_config and device.startswith('hvc'):
+ # XEN console has a different default console speed
+ console['device'][device]['speed'] = 38400
+ else:
+ # Merge in XML defaults - the proper way to do it
+ console['device'][device] = dict_merge(default_values,
+ console['device'][device])
+
+ return console
- console['device'][device].update(tmp)
+def verify(console):
+ if not console or 'device' not in console:
+ return None
+ for device in console['device']:
if device.startswith('usb'):
# It is much easiert to work with the native ttyUSBn name when using
# getty, but that name may change across reboots - depending on the
@@ -58,13 +69,13 @@ def get_config(config=None):
# to its dynamic device file - and create a new dict entry for it.
by_bus_device = f'{by_bus_dir}/{device}'
if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
- tmp = os.path.basename(os.readlink(by_bus_device))
- # updating the dict must come as last step in the loop!
- console['device'][tmp] = console['device'].pop(device)
+ device = os.path.basename(os.readlink(by_bus_device))
- return console
+ # If the device name still starts with usbXXX no matching tty was found
+ # and it can not be used as a serial interface
+ if device.startswith('usb'):
+ raise ConfigError(f'Device {device} does not support beeing used as tty')
-def verify(console):
return None
def generate(console):
@@ -76,20 +87,29 @@ def generate(console):
call(f'systemctl stop {basename}')
os.unlink(os.path.join(root, basename))
- if not console:
+ if not console or 'device' not in console:
return None
- for device in console['device'].keys():
+ for device, device_config in console['device'].items():
+ if device.startswith('usb'):
+ # It is much easiert to work with the native ttyUSBn name when using
+ # getty, but that name may change across reboots - depending on the
+ # amount of connected devices. We will resolve the fixed device name
+ # to its dynamic device file - and create a new dict entry for it.
+ by_bus_device = f'{by_bus_dir}/{device}'
+ if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
+ device = os.path.basename(os.readlink(by_bus_device))
+
config_file = base_dir + f'/serial-getty@{device}.service'
getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service'
- render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device])
+ render(config_file, 'getty/serial-getty.service.tmpl', device_config)
os.symlink(config_file, getty_wants_symlink)
# GRUB
# For existing serial line change speed (if necessary)
# Only applys to ttyS0
- if 'ttyS0' not in console['device'].keys():
+ if 'ttyS0' not in console['device']:
return None
speed = console['device']['ttyS0']['speed']
@@ -98,7 +118,6 @@ def generate(console):
return None
lines = read_file(grub_config).split('\n')
-
p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$')
write = False
newlines = []
@@ -122,9 +141,8 @@ def generate(console):
return None
def apply(console):
- # reset screen blanking
+ # Reset screen blanking
call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1')
-
# Reload systemd manager configuration
call('systemctl daemon-reload')
@@ -136,11 +154,11 @@ def apply(console):
call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1')
# Start getty process on configured serial interfaces
- for device in console['device'].keys():
+ for device in console['device']:
# Only start console if it exists on the running system. If a user
# detaches a USB serial console and reboots - it should not fail!
if os.path.exists(f'/dev/{device}'):
- call(f'systemctl start serial-getty@{device}.service')
+ call(f'systemctl restart serial-getty@{device}.service')
return None
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
deleted file mode 100755
index 969266c30..000000000
--- a/src/conf_mode/vpn_ipsec.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 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 vyos.config import Config
-from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
-from vyos import ConfigError
-from vyos import airbag
-from pprint import pprint
-airbag.enable()
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
- base = ['vpn', 'nipsec']
- if not conf.exists(base):
- return None
-
- # retrieve common dictionary keys
- ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- return ipsec
-
-def verify(ipsec):
- if not ipsec:
- return None
-
-def generate(ipsec):
- if not ipsec:
- return None
-
- return ipsec
-
-def apply(ipsec):
- if not ipsec:
- return None
-
- pprint(ipsec)
-
-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/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index e970d2ef5..86aa9af09 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -291,6 +291,8 @@ def get_config(config=None):
# LNS secret
if conf.exists(['lns', 'shared-secret']):
l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret'])
+ if conf.exists(['lns', 'host-name']):
+ l2tp['lns_host_name'] = conf.return_value(['lns', 'host-name'])
if conf.exists(['ccp-disable']):
l2tp['ccp_disable'] = True
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index 47367f125..070009722 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -50,16 +50,14 @@ def verify(sstp):
verify_accel_ppp_base_service(sstp)
- if not sstp['client_ip_pool']:
+ if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp:
raise ConfigError('Client IP subnet required')
#
# SSL certificate checks
#
tmp = dict_search('ssl.ca_cert_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL CA certificate file required!')
- else:
+ if tmp:
if not os.path.isfile(tmp):
raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!')
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index 3ccc7d66b..ad38adaec 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# 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
@@ -17,244 +17,144 @@
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 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):
- vrrp_groups = []
- sync_groups = []
-
if config:
- config = config
+ conf = config
else:
- 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["virtual_addresses_excluded"] = config.return_values("virtual-address-excluded")
-
- 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")
- group["script_mode_force"] = config.exists("transition-script mode-force")
-
- 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"]))
-
- # Warn the user about the deprecated mode-force option
- if group["script_mode_force"]:
- print("""Warning: "transition-script mode-force" VRRP option is deprecated and will be removed in VyOS 1.4.""")
- print("""It's no longer necessary, so you can safely remove it from your config now.""")
-
- # 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
+ 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 'virtual_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['virtual_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!')
+
+
+ # Warn the user about the deprecated mode-force option
+ if 'transition_script' in group_config and 'mode_force' in group_config['transition_script']:
+ print('Warning: "transition-script mode-force" VRRP option is deprecated and will be removed in VyOS 1.4.')
+ print('It is no longer necessary, so you can safely remove it from your config now.')
# 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', {})
+ 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
-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")
+ # 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:
- # VRRP is removed in the commit
- print("Stopping the VRRP process")
- call("systemctl stop keepalived.service")
- os.unlink(VRRP.location['daemon'])
-
+ call(f'systemctl restart {service_name}')
return None
-
if __name__ == '__main__':
try:
c = get_config()
@@ -262,5 +162,5 @@ if __name__ == '__main__':
generate(c)
apply(c)
except ConfigError as e:
- print("VRRP error: {0}".format(str(e)))
+ print(e)
exit(1)