summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py2
-rwxr-xr-xsrc/conf_mode/http-api.py12
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py92
-rwxr-xr-xsrc/conf_mode/interfaces-erspan.py108
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py49
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py12
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py39
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py18
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py17
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py16
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py12
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py131
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py19
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py8
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py11
-rwxr-xr-xsrc/conf_mode/lldp.py9
-rwxr-xr-xsrc/conf_mode/nat.py11
-rwxr-xr-xsrc/conf_mode/nat66.py175
-rwxr-xr-xsrc/conf_mode/ntp.py11
-rwxr-xr-xsrc/conf_mode/policy-lists.py117
-rwxr-xr-xsrc/conf_mode/policy-local-route.py2
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py230
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py233
-rwxr-xr-xsrc/conf_mode/protocols_isis.py249
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py216
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py107
-rwxr-xr-xsrc/conf_mode/protocols_rip.py330
-rwxr-xr-xsrc/conf_mode/protocols_ripng.py131
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py110
-rwxr-xr-xsrc/conf_mode/protocols_static.py113
-rwxr-xr-xsrc/conf_mode/service_console-server.py45
-rwxr-xr-xsrc/conf_mode/service_webproxy.py3
-rwxr-xr-xsrc/conf_mode/ssh.py32
-rwxr-xr-xsrc/conf_mode/system-login.py480
-rwxr-xr-xsrc/conf_mode/system-option.py14
-rwxr-xr-xsrc/conf_mode/system_console.py25
-rwxr-xr-xsrc/conf_mode/vrf.py255
-rwxr-xr-xsrc/conf_mode/vrrp.py6
38 files changed, 2046 insertions, 1404 deletions
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 6d39c6644..c979feca7 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -114,7 +114,7 @@ def verify(dyndns):
raise ConfigError(f'"password" {error_msg}')
if 'zone' in config:
- if service != 'cloudflare':
+ if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'):
raise ConfigError(f'"zone" option only supported with CloudFlare')
if 'custom' in config:
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 7af3e3d7c..fd4ffed9a 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -41,26 +41,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
@@ -74,18 +54,12 @@ def get_config(config=None):
bridge = get_interface_dict(conf, base)
# determine which members have been removed
- tmp = node_changed(conf, ['member', 'interface'])
+ tmp = node_changed(conf, ['member', 'interface'], key_mangling=('-', '_'))
if tmp:
if 'member' in bridge:
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 +73,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 +93,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 +107,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 +131,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-erspan.py b/src/conf_mode/interfaces-erspan.py
new file mode 100755
index 000000000..97ae3cf55
--- /dev/null
+++ b/src/conf_mode/interfaces-erspan.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from copy import deepcopy
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configdict import get_interface_dict
+from vyos.configdict import node_changed
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_mtu_ipv6
+from vyos.configverify import verify_tunnel
+from vyos.ifconfig import Interface
+from vyos.ifconfig import ERSpanIf
+from vyos.ifconfig import ER6SpanIf
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
+from vyos.util import dict_search
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least
+ the interface name will be added or a deleted flag
+ """
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['interfaces', 'erspan']
+ erspan = get_interface_dict(conf, base)
+
+ tmp = leaf_node_changed(conf, ['encapsulation'])
+ if tmp:
+ erspan.update({'encapsulation_changed': {}})
+
+ return erspan
+
+def verify(erspan):
+ if 'deleted' in erspan:
+ return None
+
+ if 'encapsulation' not in erspan:
+ raise ConfigError('Unable to detect the following ERSPAN tunnel encapsulation'\
+ '{ifname}!'.format(**erspan))
+
+ verify_mtu_ipv6(erspan)
+ verify_tunnel(erspan)
+
+ key = dict_search('parameters.ip.key',erspan)
+ if key == None:
+ raise ConfigError('parameters.ip.key is mandatory for ERSPAN tunnel')
+
+
+def generate(erspan):
+ return None
+
+def apply(erspan):
+ if 'deleted' in erspan or 'encapsulation_changed' in erspan:
+ if erspan['ifname'] in interfaces():
+ tmp = Interface(erspan['ifname'])
+ tmp.remove()
+ if 'deleted' in erspan:
+ return None
+
+ dispatch = {
+ 'erspan': ERSpanIf,
+ 'ip6erspan': ER6SpanIf
+ }
+
+ # We need to re-map the tunnel encapsulation proto to a valid interface class
+ encap = erspan['encapsulation']
+ klass = dispatch[encap]
+
+ erspan_tunnel = klass(**erspan)
+ erspan_tunnel.change_options()
+ erspan_tunnel.update(erspan)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ generate(c)
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index bc102826f..378f400b8 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -23,13 +23,14 @@ from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_address
from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_eapol
from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_mirror
from vyos.configverify import verify_mtu
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
-from vyos.configverify import verify_eapol
-from vyos.configverify import verify_mirror
+from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
from vyos.template import render
from vyos.util import call
@@ -59,15 +60,13 @@ def verify(ethernet):
if 'deleted' in ethernet:
return None
- verify_interface_exists(ethernet)
-
- if ethernet.get('speed', None) == 'auto':
- if ethernet.get('duplex', None) != 'auto':
- raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too')
+ ifname = ethernet['ifname']
+ verify_interface_exists(ifname)
- if ethernet.get('duplex', None) == 'auto':
- if ethernet.get('speed', None) != 'auto':
- raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too')
+ # 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)
@@ -77,12 +76,38 @@ def verify(ethernet):
verify_eapol(ethernet)
verify_mirror(ethernet)
- ifname = ethernet['ifname']
# verify offloading capabilities
- if 'offload' in ethernet and 'rps' in ethernet['offload']:
+ 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 = 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')
+
+ ethtool = Ethtool(ifname)
+ if 'ring_buffer' in ethernet:
+ max_rx = ethtool.get_rx_buffer()
+ if not max_rx:
+ raise ConfigError('Driver does not support RX ring-buffer configuration!')
+
+ max_tx = ethtool.get_tx_buffer()
+ if not max_tx:
+ raise ConfigError('Driver does not support TX ring-buffer configuration!')
+
+ rx = dict_search('ring_buffer.rx', ethernet)
+ if rx and int(rx) > int(max_rx):
+ raise ConfigError(f'Driver only supports a maximum RX ring-buffer '\
+ f'size of "{max_rx}" bytes!')
+
+ tx = dict_search('ring_buffer.tx', ethernet)
+ if tx and int(tx) > int(max_tx):
+ raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\
+ f'size of "{max_tx}" bytes!')
+
# XDP requires multiple TX queues
if 'xdp' in ethernet:
queues = glob(f'/sys/class/net/{ifname}/queues/tx-*')
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 979a5612e..2a63b60aa 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -72,18 +72,8 @@ def apply(geneve):
g.remove()
if 'deleted' not in geneve:
- # This is a special type of interface which needs additional parameters
- # when created using iproute2. Instead of passing a ton of arguments,
- # use a dictionary provided by the interface class which holds all the
- # options necessary.
- conf = GeneveIf.get_config()
-
- # Assign GENEVE instance configuration parameters to config dict
- conf['vni'] = geneve['vni']
- conf['remote'] = geneve['remote']
-
# Finally create the new interface
- g = GeneveIf(geneve['ifname'], **conf)
+ g = GeneveIf(**geneve)
g.update(geneve)
return None
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 1118143e4..9b6ddd5aa 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -34,7 +34,6 @@ airbag.enable()
k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
-
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -47,12 +46,6 @@ def get_config(config=None):
base = ['interfaces', 'l2tpv3']
l2tpv3 = get_interface_dict(conf, base)
- # L2TPv3 is "special" the default MTU is 1488 - update accordingly
- # as the config_level is already st in get_interface_dict() - we can use []
- tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
- if 'mtu' not in tmp:
- l2tpv3['mtu'] = '1488'
-
# To delete an l2tpv3 interface we need the current tunnel and session-id
if 'deleted' in l2tpv3:
tmp = leaf_node_changed(conf, ['tunnel-id'])
@@ -71,15 +64,15 @@ def verify(l2tpv3):
interface = l2tpv3['ifname']
- for key in ['local_ip', 'remote_ip', 'tunnel_id', 'peer_tunnel_id',
+ for key in ['source_address', 'remote', 'tunnel_id', 'peer_tunnel_id',
'session_id', 'peer_session_id']:
if key not in l2tpv3:
tmp = key.replace('_', '-')
- raise ConfigError(f'L2TPv3 {tmp} must be configured!')
+ raise ConfigError(f'Missing mandatory L2TPv3 option: "{tmp}"!')
- if not is_addr_assigned(l2tpv3['local_ip']):
- raise ConfigError('L2TPv3 local-ip address '
- '"{local_ip}" is not configured!'.format(**l2tpv3))
+ if not is_addr_assigned(l2tpv3['source_address']):
+ raise ConfigError('L2TPv3 source-address address "{source_address}" '
+ 'not configured on any interface!'.format(**l2tpv3))
verify_mtu_ipv6(l2tpv3)
verify_address(l2tpv3)
@@ -89,34 +82,16 @@ def generate(l2tpv3):
return None
def apply(l2tpv3):
- # This is a special type of interface which needs additional parameters
- # when created using iproute2. Instead of passing a ton of arguments,
- # use a dictionary provided by the interface class which holds all the
- # options necessary.
- conf = L2TPv3If.get_config()
-
# Check if L2TPv3 interface already exists
if l2tpv3['ifname'] in interfaces():
# L2TPv3 is picky when changing tunnels/sessions, thus we can simply
# always delete it first.
- conf['session_id'] = l2tpv3['session_id']
- conf['tunnel_id'] = l2tpv3['tunnel_id']
- l = L2TPv3If(l2tpv3['ifname'], **conf)
+ l = L2TPv3If(**l2tpv3)
l.remove()
if 'deleted' not in l2tpv3:
- conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id']
- conf['local_port'] = l2tpv3['source_port']
- conf['remote_port'] = l2tpv3['destination_port']
- conf['encapsulation'] = l2tpv3['encapsulation']
- conf['local_address'] = l2tpv3['local_ip']
- conf['remote_address'] = l2tpv3['remote_ip']
- conf['session_id'] = l2tpv3['session_id']
- conf['tunnel_id'] = l2tpv3['tunnel_id']
- conf['peer_session_id'] = l2tpv3['peer_session_id']
-
# Finally create the new interface
- l = L2TPv3If(l2tpv3['ifname'], **conf)
+ l = L2TPv3If(**l2tpv3)
l.update(l2tpv3)
return None
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index 2c8367ff3..eab69f36e 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.py
@@ -49,14 +49,6 @@ def get_config(config=None):
base = ['interfaces', 'macsec']
macsec = get_interface_dict(conf, base)
- # MACsec is "special" the default MTU is 1460 - update accordingly
- # as the config_level is already st in get_interface_dict() - we can use []
- tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
- if 'mtu' not in tmp:
- # base MTU for MACsec is 1468 bytes, but we leave room for 802.1ad and
- # 802.1q VLAN tags, thus the limit is 1460 bytes.
- macsec['mtu'] = '1460'
-
# Check if interface has been removed
if 'deleted' in macsec:
source_interface = conf.return_effective_value(['source-interface'])
@@ -123,17 +115,9 @@ def apply(macsec):
os.unlink(wpa_suppl_conf.format(**macsec))
else:
- # This is a special type of interface which needs additional parameters
- # when created using iproute2. Instead of passing a ton of arguments,
- # use a dictionary provided by the interface class which holds all the
- # options necessary.
- conf = MACsecIf.get_config()
- conf['source_interface'] = macsec['source_interface']
- conf['security_cipher'] = macsec['security']['cipher']
-
# It is safe to "re-create" the interface always, there is a sanity
# check that the interface will only be create if its non existent
- i = MACsecIf(macsec['ifname'], **conf)
+ i = MACsecIf(**macsec)
i.update(macsec)
call('systemctl restart wpa_supplicant-macsec@{source_interface}'
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index e4a6a5ec1..4afb85526 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -17,6 +17,7 @@
import os
import re
+from glob import glob
from sys import exit
from ipaddress import IPv4Address
from ipaddress import IPv4Network
@@ -488,14 +489,9 @@ def apply(openvpn):
# Do some cleanup when OpenVPN is disabled/deleted
if 'deleted' in openvpn or 'disable' in openvpn:
- # cleanup old configuration files
- cleanup = []
- cleanup.append(cfg_file.format(**openvpn))
- cleanup.append(openvpn['auth_user_pass_file'])
-
- for file in cleanup:
- if os.path.isfile(file):
- os.unlink(file)
+ for cleanup_file in glob(f'/run/openvpn/{interface}.*'):
+ if os.path.isfile(cleanup_file):
+ os.unlink(cleanup_file)
if interface in interfaces():
VTunIf(interface).remove()
@@ -506,10 +502,7 @@ def apply(openvpn):
# existed - nevertheless, spawn new OpenVPN process
call(f'systemctl start openvpn@{interface}.service')
- conf = VTunIf.get_config()
- conf['device_type'] = openvpn['device_type']
-
- o = VTunIf(interface, **conf)
+ o = VTunIf(**openvpn)
o.update(openvpn)
return None
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index c31e49574..3675db73b 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -43,12 +43,6 @@ def get_config(config=None):
base = ['interfaces', 'pppoe']
pppoe = get_interface_dict(conf, base)
- # PPPoE is "special" the default MTU is 1492 - update accordingly
- # as the config_level is already st in get_interface_dict() - we can use []
- tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
- if 'mtu' not in tmp:
- pppoe['mtu'] = '1492'
-
return pppoe
def verify(pppoe):
@@ -79,7 +73,7 @@ def generate(pppoe):
config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c]
- if 'deleted' in pppoe:
+ if 'deleted' in pppoe or 'disable' in pppoe:
# stop DHCPv6-PD client
call(f'systemctl stop dhcp6c@{ifname}.service')
# Hang-up PPPoE connection
@@ -116,13 +110,11 @@ def generate(pppoe):
return None
def apply(pppoe):
- if 'deleted' in pppoe:
- # bail out early
+ if 'deleted' in pppoe or 'disable' in pppoe:
+ call('systemctl stop ppp@{ifname}.service'.format(**pppoe))
return None
- if 'disable' not in pppoe:
- # Dial PPPoE connection
- call('systemctl restart ppp@{ifname}.service'.format(**pppoe))
+ call('systemctl restart ppp@{ifname}.service'.format(**pppoe))
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index ddbef56ac..34a054837 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -75,19 +75,9 @@ def apply(peth):
if 'mode_old' in peth:
MACVLANIf(peth['ifname']).remove()
- # This is a special type of interface which needs additional parameters
- # when created using iproute2. Instead of passing a ton of arguments,
- # use a dictionary provided by the interface class which holds all the
- # options necessary.
- conf = MACVLANIf.get_config()
-
- # Assign MACVLAN instance configuration parameters to config dict
- conf['source_interface'] = peth['source_interface']
- conf['mode'] = peth['mode']
-
# It is safe to "re-create" the interface always, there is a sanity check
# that the interface will only be create if its non existent
- p = MACVLANIf(peth['ifname'], **conf)
+ p = MACVLANIf(**peth)
p.update(peth)
return None
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 1a7e9a96d..cab94a5b0 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.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
@@ -24,21 +24,17 @@ from vyos.configdict import dict_merge
from vyos.configdict import get_interface_dict
from vyos.configdict import node_changed
from vyos.configdict import leaf_node_changed
-from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_interface_exists
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 GREIf
-from vyos.ifconfig import GRETapIf
-from vyos.ifconfig import IPIPIf
-from vyos.ifconfig import IP6GREIf
-from vyos.ifconfig import IPIP6If
-from vyos.ifconfig import IP6IP6If
-from vyos.ifconfig import SitIf
-from vyos.ifconfig import Sit6RDIf
+from vyos.ifconfig import TunnelIf
from vyos.template import is_ipv4
from vyos.template import is_ipv6
+from vyos.util import get_interface_config
from vyos.util import dict_search
from vyos import ConfigError
from vyos import airbag
@@ -56,12 +52,6 @@ def get_config(config=None):
base = ['interfaces', 'tunnel']
tunnel = get_interface_dict(conf, base)
- # Wireguard is "special" the default MTU is 1420 - update accordingly
- # as the config_level is already st in get_interface_dict() - we can use []
- tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
- if 'mtu' not in tmp:
- tunnel['mtu'] = '1476'
-
tmp = leaf_node_changed(conf, ['encapsulation'])
if tmp: tunnel.update({'encapsulation_changed': {}})
@@ -83,103 +73,50 @@ def verify(tunnel):
return None
if 'encapsulation' not in tunnel:
- raise ConfigError('Must configure the tunnel encapsulation for '\
- '{ifname}!'.format(**tunnel))
+ error = 'Must configure encapsulation for "{ifname}"!'
+ raise ConfigError(error.format(**tunnel))
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
+ verify_tunnel(tunnel)
- if 'local_ip' not in tunnel and 'dhcp_interface' not in tunnel:
- raise ConfigError('local-ip is mandatory for tunnel')
-
- if 'remote_ip' not in tunnel and tunnel['encapsulation'] != 'gre':
- raise ConfigError('remote-ip is mandatory for tunnel')
-
- if {'local_ip', 'dhcp_interface'} <= set(tunnel):
- raise ConfigError('Can not use both local-ip and dhcp-interface')
-
- if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
- error_ipv6 = 'Encapsulation mode requires IPv6'
- if 'local_ip' in tunnel and not is_ipv6(tunnel['local_ip']):
- raise ConfigError(f'{error_ipv6} local-ip')
-
- if 'remote_ip' in tunnel and not is_ipv6(tunnel['remote_ip']):
- raise ConfigError(f'{error_ipv6} remote-ip')
- else:
- error_ipv4 = 'Encapsulation mode requires IPv4'
- if 'local_ip' in tunnel and not is_ipv4(tunnel['local_ip']):
- raise ConfigError(f'{error_ipv4} local-ip')
+ if 'source_interface' in tunnel:
+ verify_interface_exists(tunnel['source_interface'])
- if 'remote_ip' in tunnel and not is_ipv4(tunnel['remote_ip']):
- raise ConfigError(f'{error_ipv4} remote-ip')
+ # TTL != 0 and nopmtudisc are incompatible, parameters and ip use default
+ # values, thus the keys are always present.
+ if dict_search('parameters.ip.no_pmtu_discovery', tunnel) != None:
+ if dict_search('parameters.ip.ttl', tunnel) != '0':
+ raise ConfigError('Disabled PMTU requires TTL set to "0"!')
+ if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
+ raise ConfigError('Can not disable PMTU discovery for given encapsulation')
- if tunnel['encapsulation'] in ['sit', 'gre-bridge']:
- if 'source_interface' in tunnel:
- raise ConfigError('Option source-interface can not be used with ' \
- 'encapsulation "sit" or "gre-bridge"')
- elif tunnel['encapsulation'] == 'gre':
- if 'local_ip' in tunnel and is_ipv6(tunnel['local_ip']):
- raise ConfigError('Can not use local IPv6 address is for mGRE tunnels')
def generate(tunnel):
return None
def apply(tunnel):
- if 'deleted' in tunnel or 'encapsulation_changed' in tunnel:
- if tunnel['ifname'] in interfaces():
- tmp = Interface(tunnel['ifname'])
+ interface = tunnel['ifname']
+ # If a gretap tunnel is already existing we can not "simply" change local or
+ # remote addresses. This returns "Operation not supported" by the Kernel.
+ # There is no other solution to destroy and recreate the tunnel.
+ encap = ''
+ remote = ''
+ tmp = get_interface_config(interface)
+ if tmp:
+ encap = dict_search('linkinfo.info_kind', tmp)
+ remote = dict_search('linkinfo.info_data.remote', tmp)
+
+ if ('deleted' in tunnel or 'encapsulation_changed' in tunnel or
+ encap in ['gretap', 'ip6gretap'] or remote in ['any']):
+ if interface in interfaces():
+ tmp = Interface(interface)
tmp.remove()
if 'deleted' in tunnel:
return None
- dispatch = {
- 'gre': GREIf,
- 'gre-bridge': GRETapIf,
- 'ipip': IPIPIf,
- 'ipip6': IPIP6If,
- 'ip6ip6': IP6IP6If,
- 'ip6gre': IP6GREIf,
- 'sit': SitIf,
- }
-
- # We need to re-map the tunnel encapsulation proto to a valid interface class
- encap = tunnel['encapsulation']
- klass = dispatch[encap]
-
- # This is a special type of interface which needs additional parameters
- # when created using iproute2. Instead of passing a ton of arguments,
- # use a dictionary provided by the interface class which holds all the
- # options necessary.
- conf = klass.get_config()
-
- # Copy/re-assign our dictionary values to values understood by the
- # derived _Tunnel classes
- mapping = {
- # this : get_config()
- 'local_ip' : 'local',
- 'remote_ip' : 'remote',
- 'source_interface' : 'dev',
- 'parameters.ip.ttl' : 'ttl',
- 'parameters.ip.tos' : 'tos',
- 'parameters.ip.key' : 'key',
- 'parameters.ipv6.encaplimit' : 'encaplimit'
- }
-
- # Add additional IPv6 options if tunnel is IPv6 aware
- if tunnel['encapsulation'] in ['ipip6', 'ip6ip6', 'ip6gre']:
- mappingv6 = {
- # this : get_config()
- 'parameters.ipv6.encaplimit' : 'encaplimit'
- }
- mapping.update(mappingv6)
-
- for our_key, their_key in mapping.items():
- if dict_search(our_key, tunnel) and their_key in conf:
- conf[their_key] = dict_search(our_key, tunnel)
-
- tun = klass(tunnel['ifname'], **conf)
- tun.change_options()
+ tun = TunnelIf(**tunnel)
tun.update(tunnel)
return None
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 04e258fcf..8e6247a30 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -42,12 +42,6 @@ def get_config(config=None):
base = ['interfaces', 'vxlan']
vxlan = get_interface_dict(conf, base)
- # VXLAN is "special" the default MTU is 1492 - update accordingly
- # as the config_level is already st in get_interface_dict() - we can use []
- tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
- if 'mtu' not in tmp:
- vxlan['mtu'] = '1450'
-
return vxlan
def verify(vxlan):
@@ -96,19 +90,8 @@ def apply(vxlan):
v.remove()
if 'deleted' not in vxlan:
- # This is a special type of interface which needs additional parameters
- # when created using iproute2. Instead of passing a ton of arguments,
- # use a dictionary provided by the interface class which holds all the
- # options necessary.
- conf = VXLANIf.get_config()
-
- # Assign VXLAN instance configuration parameters to config dict
- for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']:
- if tmp in vxlan:
- conf[tmp] = vxlan[tmp]
-
# Finally create the new interface
- v = VXLANIf(vxlan['ifname'], **conf)
+ v = VXLANIf(**vxlan)
v.update(vxlan)
return None
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 7cfc76aa0..024ab8f59 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -46,19 +46,13 @@ def get_config(config=None):
base = ['interfaces', 'wireguard']
wireguard = get_interface_dict(conf, base)
- # Wireguard is "special" the default MTU is 1420 - update accordingly
- # as the config_level is already st in get_interface_dict() - we can use []
- tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True)
- if 'mtu' not in tmp:
- wireguard['mtu'] = '1420'
-
# Mangle private key - it has a default so its always valid
wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard)
# Determine which Wireguard peer has been removed.
# Peers can only be removed with their public key!
dict = {}
- tmp = node_changed(conf, ['peer'])
+ tmp = node_changed(conf, ['peer'], key_mangling=('-', '_'))
for peer in (tmp or []):
pubkey = leaf_node_changed(conf, ['peer', peer, 'pubkey'])
if pubkey:
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index b25fcd4e0..7b3de6e8a 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -255,17 +255,8 @@ def apply(wifi):
if 'deleted' in wifi:
WiFiIf(interface).remove()
else:
- # This is a special type of interface which needs additional parameters
- # when created using iproute2. Instead of passing a ton of arguments,
- # use a dictionary provided by the interface class which holds all the
- # options necessary.
- conf = WiFiIf.get_config()
-
- # Assign WiFi instance configuration parameters to config dict
- conf['phy'] = wifi['physical_device']
-
# Finally create the new interface
- w = WiFiIf(interface, **conf)
+ w = WiFiIf(**wifi)
w.update(wifi)
# Enable/Disable interface - interface is always placed in
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
index 6b645857a..082c3e128 100755
--- a/src/conf_mode/lldp.py
+++ b/src/conf_mode/lldp.py
@@ -21,7 +21,8 @@ from copy import deepcopy
from sys import exit
from vyos.config import Config
-from vyos.validate import is_addr_assigned,is_loopback_addr
+from vyos.validate import is_addr_assigned
+from vyos.validate import is_loopback_addr
from vyos.version import get_version_data
from vyos import ConfigError
from vyos.util import call
@@ -237,8 +238,10 @@ def apply(lldp):
else:
# LLDP service has been terminated
call('systemctl stop lldpd.service')
- os.unlink(config_file)
- os.unlink(vyos_config_file)
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+ if os.path.isfile(vyos_config_file):
+ os.unlink(vyos_config_file)
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 1ccec3d2e..dae958774 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.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
@@ -26,6 +26,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
+from vyos.template import is_ip_network
from vyos.util import cmd
from vyos.util import check_kmod
from vyos.util import dict_search
@@ -68,9 +69,9 @@ def verify_rule(config, err_msg):
'ports can only be specified when protocol is '\
'either tcp, udp or tcp_udp!')
- if '/' in (dict_search('translation.address', config) or []):
+ if is_ip_network(dict_search('translation.address', config)):
raise ConfigError(f'{err_msg}\n' \
- 'Cannot use ports with an IPv4net type translation address as it\n' \
+ 'Cannot use ports with an IPv4 network as translation address as it\n' \
'statically maps a whole network of addresses onto another\n' \
'network of addresses')
@@ -88,7 +89,7 @@ def get_config(config=None):
for direction in ['source', 'destination']:
if direction in nat:
default_values = defaults(base + [direction, 'rule'])
- for rule in nat[direction]['rule']:
+ for rule in dict_search(f'{direction}.rule', nat) or []:
nat[direction]['rule'][rule] = dict_merge(default_values,
nat[direction]['rule'][rule])
@@ -147,7 +148,7 @@ def verify(nat):
addr = dict_search('translation.address', config)
if addr != None:
- if addr != 'masquerade':
+ if addr != 'masquerade' and not is_ip_network(addr):
for ip in addr.split('-'):
if not is_addr_assigned(ip):
print(f'WARNING: IP address {ip} does not exist on the system!')
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
new file mode 100755
index 000000000..e2bd6417d
--- /dev/null
+++ b/src/conf_mode/nat66.py
@@ -0,0 +1,175 @@
+#!/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 jmespath
+import json
+import os
+
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import cmd
+from vyos.util import check_kmod
+from vyos.util import dict_search
+from vyos.template import is_ipv6
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+k_mod = ['nft_nat', 'nft_chain_nat']
+
+iptables_nat_config = '/tmp/vyos-nat66-rules.nft'
+ndppd_config = '/run/ndppd/ndppd.conf'
+
+def get_handler(json, chain, target):
+ """ Get nftable rule handler number of given chain/target combination.
+ Handler is required when adding NAT66/Conntrack helper targets """
+ for x in json:
+ if x['chain'] != chain:
+ continue
+ if x['target'] != target:
+ continue
+ return x['handle']
+
+ return None
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['nat66']
+ nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ # T2665: we must add the tagNode defaults individually until this is
+ # moved to the base class
+ for direction in ['source', 'destination']:
+ if direction in nat:
+ default_values = defaults(base + [direction, 'rule'])
+ if 'rule' in nat[direction]:
+ for rule in nat[direction]['rule']:
+ nat[direction]['rule'][rule] = dict_merge(default_values,
+ nat[direction]['rule'][rule])
+
+ # read in current nftable (once) for further processing
+ tmp = cmd('nft -j list table ip6 raw')
+ nftable_json = json.loads(tmp)
+
+ # condense the full JSON table into a list with only relevand informations
+ pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}'
+ condensed_json = jmespath.search(pattern, nftable_json)
+
+ if not conf.exists(base):
+ nat['helper_functions'] = 'remove'
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
+ nat['deleted'] = ''
+ return nat
+
+ # check if NAT66 connection tracking helpers need to be set up - this has to
+ # be done only once
+ if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
+ nat['helper_functions'] = 'add'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+ else:
+ nat['helper_functions'] = 'has'
+
+ return nat
+
+def verify(nat):
+ if not nat or 'deleted' in nat:
+ # no need to verify the CLI as NAT66 is going to be deactivated
+ return None
+
+ if 'helper_functions' in nat and nat['helper_functions'] != 'has':
+ if not (nat['pre_ct_conntrack'] or nat['out_ct_conntrack']):
+ raise Exception('could not determine nftable ruleset handlers')
+
+ if dict_search('source.rule', nat):
+ for rule, config in dict_search('source.rule', nat).items():
+ err_msg = f'Source NAT66 configuration error in rule {rule}:'
+ if 'outbound_interface' not in config:
+ raise ConfigError(f'{err_msg}\n' \
+ 'outbound-interface not specified')
+ else:
+ if config['outbound_interface'] not in interfaces():
+ print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
+
+ addr = dict_search('translation.address', config)
+ if addr != None:
+ if addr != 'masquerade' and not is_ipv6(addr):
+ raise ConfigError(f'Warning: IPv6 address {addr} is not a valid address')
+
+ prefix = dict_search('source.prefix', config)
+ if prefix != None:
+ if not is_ipv6(prefix):
+ raise ConfigError(f'{err_msg} source-prefix not specified')
+
+ if dict_search('destination.rule', nat):
+ for rule, config in dict_search('destination.rule', nat).items():
+ err_msg = f'Destination NAT66 configuration error in rule {rule}:'
+
+ if 'inbound_interface' not in config:
+ raise ConfigError(f'{err_msg}\n' \
+ 'inbound-interface not specified')
+ else:
+ if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():
+ print(f'WARNING: rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')
+
+ return None
+
+def generate(nat):
+ render(iptables_nat_config, 'firewall/nftables-nat66.tmpl', nat, permission=0o755)
+ render(ndppd_config, 'proxy-ndp/ndppd.conf.tmpl', nat, permission=0o755)
+ return None
+
+def apply(nat):
+ if not nat:
+ return None
+ cmd(f'{iptables_nat_config}')
+ if 'deleted' in nat or not dict_search('source.rule', nat):
+ cmd('systemctl stop ndppd')
+ if os.path.isfile(ndppd_config):
+ os.unlink(ndppd_config)
+ else:
+ cmd('systemctl restart ndppd')
+ if os.path.isfile(iptables_nat_config):
+ os.unlink(iptables_nat_config)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod(k_mod)
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index b102b3e9e..52070aabc 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.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
@@ -24,7 +24,7 @@ from vyos.template import render
from vyos import airbag
airbag.enable()
-config_file = r'/etc/ntp.conf'
+config_file = r'/run/ntpd/ntpd.conf'
systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf'
def get_config(config=None):
@@ -33,8 +33,11 @@ def get_config(config=None):
else:
conf = Config()
base = ['system', 'ntp']
+ if not conf.exists(base):
+ return None
ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ ntp['config_file'] = config_file
return ntp
def verify(ntp):
@@ -42,7 +45,7 @@ def verify(ntp):
if not ntp:
return None
- if len(ntp.get('allow_clients', {})) and not (len(ntp.get('server', {})) > 0):
+ if 'allow_clients' in ntp and 'server' not in ntp:
raise ConfigError('NTP server not configured')
verify_vrf(ntp)
@@ -53,7 +56,7 @@ def generate(ntp):
if not ntp:
return None
- render(config_file, 'ntp/ntp.conf.tmpl', ntp)
+ render(config_file, 'ntp/ntpd.conf.tmpl', ntp)
render(systemd_override, 'ntp/override.conf.tmpl', ntp)
return None
diff --git a/src/conf_mode/policy-lists.py b/src/conf_mode/policy-lists.py
new file mode 100755
index 000000000..94a020e7b
--- /dev/null
+++ b/src/conf_mode/policy-lists.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.template import render_to_string
+from vyos.util import call
+from vyos.util import dict_search
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+from pprint import pprint
+airbag.enable()
+
+config_file = r'/tmp/policy.frr'
+frr_daemon = 'zebra'
+
+DEBUG = os.path.exists('/tmp/policy.debug')
+if DEBUG:
+ import logging
+ lg = logging.getLogger("vyos.frr")
+ lg.setLevel(logging.DEBUG)
+ ch = logging.StreamHandler()
+ lg.addHandler(ch)
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['npolicy']
+ policy = conf.get_config_dict(base, key_mangling=('-', '_'))
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ return policy
+
+ pprint(policy)
+ exit(1)
+ return policy
+
+def verify(policy):
+ if not policy:
+ return None
+
+ return None
+
+def generate(policy):
+ if not policy:
+ policy['new_frr_config'] = ''
+ return None
+
+ # render(config) not needed, its only for debug
+ # render(config_file, 'frr/policy.frr.tmpl', policy)
+ # policy['new_frr_config'] = render_to_string('frr/policy.frr.tmpl')
+
+ return None
+
+def apply(policy):
+ # Save original configuration prior to starting any commit actions
+ # frr_cfg = frr.FRRConfig()
+ # frr_cfg.load_configuration(frr_daemon)
+ # frr_cfg.modify_section(f'ip', '')
+ # frr_cfg.add_before(r'(line vty)', policy['new_frr_config'])
+
+ # Debugging
+ if DEBUG:
+ from pprint import pprint
+ print('')
+ print('--------- DEBUGGING ----------')
+ pprint(dir(frr_cfg))
+ print('Existing config:\n')
+ for line in frr_cfg.original_config:
+ print(line)
+ print(f'Replacement config:\n')
+ print(f'{policy["new_frr_config"]}')
+ print(f'Modified config:\n')
+ print(f'{frr_cfg}')
+
+ # frr_cfg.commit_configuration(frr_daemon)
+
+ # 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 policy['new_frr_config'] == '':
+ # for a in range(5):
+ # frr_cfg.commit_configuration(frr_daemon)
+
+
+ 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/policy-local-route.py b/src/conf_mode/policy-local-route.py
index c4024dce4..013f22665 100755
--- a/src/conf_mode/policy-local-route.py
+++ b/src/conf_mode/policy-local-route.py
@@ -40,7 +40,7 @@ def get_config(config=None):
# delete policy local-route
dict = {}
- tmp = node_changed(conf, ['policy', 'local-route', 'rule'])
+ tmp = node_changed(conf, ['policy', 'local-route', 'rule'], key_mangling=('-', '_'))
if tmp:
for rule in (tmp or []):
src = leaf_node_changed(conf, ['policy', 'local-route', 'rule', rule, 'source'])
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index d1e551cad..a43eed504 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-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,191 +17,97 @@
import os
from sys import exit
-from copy import deepcopy
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.template import is_ipv6
-from vyos.template import render
+from vyos.template import render_to_string
from vyos.util import call
from vyos.validate import is_ipv6_link_local
+from vyos.xml import defaults
from vyos import ConfigError
+from vyos import frr
from vyos import airbag
airbag.enable()
-config_file = r'/tmp/bfd.frr'
-
-default_config_data = {
- 'new_peers': [],
- 'old_peers' : []
-}
-
-# get configuration for BFD peer from proposed or effective configuration
-def get_bfd_peer_config(peer, conf_mode="proposed"):
- conf = Config()
- conf.set_level('protocols bfd peer {0}'.format(peer))
-
- bfd_peer = {
- 'remote': peer,
- 'shutdown': False,
- 'src_if': '',
- 'src_addr': '',
- 'multiplier': '3',
- 'rx_interval': '300',
- 'tx_interval': '300',
- 'multihop': False,
- 'echo_interval': '',
- 'echo_mode': False,
- }
-
- # Check if individual peer is disabled
- if conf_mode == "effective" and conf.exists_effective('shutdown'):
- bfd_peer['shutdown'] = True
- if conf_mode == "proposed" and conf.exists('shutdown'):
- bfd_peer['shutdown'] = True
-
- # Check if peer has a local source interface configured
- if conf_mode == "effective" and conf.exists_effective('source interface'):
- bfd_peer['src_if'] = conf.return_effective_value('source interface')
- if conf_mode == "proposed" and conf.exists('source interface'):
- bfd_peer['src_if'] = conf.return_value('source interface')
-
- # Check if peer has a local source address configured - this is mandatory for IPv6
- if conf_mode == "effective" and conf.exists_effective('source address'):
- bfd_peer['src_addr'] = conf.return_effective_value('source address')
- if conf_mode == "proposed" and conf.exists('source address'):
- bfd_peer['src_addr'] = conf.return_value('source address')
-
- # Tell BFD daemon that we should expect packets with TTL less than 254
- # (because it will take more than one hop) and to listen on the multihop
- # port (4784)
- if conf_mode == "effective" and conf.exists_effective('multihop'):
- bfd_peer['multihop'] = True
- if conf_mode == "proposed" and conf.exists('multihop'):
- bfd_peer['multihop'] = True
-
- # Configures the minimum interval that this system is capable of receiving
- # control packets. The default value is 300 milliseconds.
- if conf_mode == "effective" and conf.exists_effective('interval receive'):
- bfd_peer['rx_interval'] = conf.return_effective_value('interval receive')
- if conf_mode == "proposed" and conf.exists('interval receive'):
- bfd_peer['rx_interval'] = conf.return_value('interval receive')
-
- # The minimum transmission interval (less jitter) that this system wants
- # to use to send BFD control packets.
- if conf_mode == "effective" and conf.exists_effective('interval transmit'):
- bfd_peer['tx_interval'] = conf.return_effective_value('interval transmit')
- if conf_mode == "proposed" and conf.exists('interval transmit'):
- bfd_peer['tx_interval'] = conf.return_value('interval transmit')
-
- # Configures the detection multiplier to determine packet loss. The remote
- # transmission interval will be multiplied by this value to determine the
- # connection loss detection timer. The default value is 3.
- if conf_mode == "effective" and conf.exists_effective('interval multiplier'):
- bfd_peer['multiplier'] = conf.return_effective_value('interval multiplier')
- if conf_mode == "proposed" and conf.exists('interval multiplier'):
- bfd_peer['multiplier'] = conf.return_value('interval multiplier')
-
- # Configures the minimal echo receive transmission interval that this system is capable of handling
- if conf_mode == "effective" and conf.exists_effective('interval echo-interval'):
- bfd_peer['echo_interval'] = conf.return_effective_value('interval echo-interval')
- if conf_mode == "proposed" and conf.exists('interval echo-interval'):
- bfd_peer['echo_interval'] = conf.return_value('interval echo-interval')
-
- # Enables or disables the echo transmission mode
- if conf_mode == "effective" and conf.exists_effective('echo-mode'):
- bfd_peer['echo_mode'] = True
- if conf_mode == "proposed" and conf.exists('echo-mode'):
- bfd_peer['echo_mode'] = True
-
- return bfd_peer
-
-def get_config():
- bfd = deepcopy(default_config_data)
- conf = Config()
- if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')):
- return None
+def get_config(config=None):
+ if config:
+ conf = config
else:
- conf.set_level('protocols bfd')
-
- # as we have to use vtysh to talk to FRR we also need to know
- # which peers are gone due to a config removal - thus we read in
- # all peers (active or to delete)
- for peer in conf.list_effective_nodes('peer'):
- bfd['old_peers'].append(get_bfd_peer_config(peer, "effective"))
-
- for peer in conf.list_nodes('peer'):
- bfd['new_peers'].append(get_bfd_peer_config(peer))
-
- # find deleted peers
- set_new_peers = set(conf.list_nodes('peer'))
- set_old_peers = set(conf.list_effective_nodes('peer'))
- bfd['deleted_peers'] = set_old_peers - set_new_peers
+ conf = Config()
+ base = ['protocols', 'bfd']
+ bfd = conf.get_config_dict(base, get_first_key=True)
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ return bfd
+
+ # We have gathered the dict representation of the CLI, but there are
+ # default options which we need to update into the dictionary retrived.
+ # XXX: T2665: we currently have no nice way for defaults under tag
+ # nodes, thus we load the defaults "by hand"
+ default_values = defaults(base + ['peer'])
+ if 'peer' in bfd:
+ for peer in bfd['peer']:
+ bfd['peer'][peer] = dict_merge(default_values, bfd['peer'][peer])
+
+ if 'profile' in bfd:
+ for profile in bfd['profile']:
+ bfd['profile'][profile] = dict_merge(default_values, bfd['profile'][profile])
return bfd
def verify(bfd):
- if bfd is None:
+ if not bfd:
return None
- # some variables to use later
- conf = Config()
-
- for peer in bfd['new_peers']:
- # IPv6 link local peers require an explicit local address/interface
- if is_ipv6_link_local(peer['remote']):
- if not (peer['src_if'] and peer['src_addr']):
- raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting')
-
- # IPv6 peers require an explicit local address
- if is_ipv6(peer['remote']):
- if not peer['src_addr']:
- raise ConfigError('BFD IPv6 peers require explicit local address setting')
-
- # multihop require source address
- if peer['multihop'] and not peer['src_addr']:
- raise ConfigError('Multihop require source address')
-
- # multihop and echo-mode cannot be used together
- if peer['multihop'] and peer['echo_mode']:
- raise ConfigError('Multihop and echo-mode cannot be used together')
-
- # multihop doesn't accept interface names
- if peer['multihop'] and peer['src_if']:
- raise ConfigError('Multihop and source interface cannot be used together')
-
- # echo interval can be configured only with enabled echo-mode
- if peer['echo_interval'] != '' and not peer['echo_mode']:
- raise ConfigError('echo-interval can be configured only with enabled echo-mode')
-
- # check if we deleted peers are not used in configuration
- if conf.exists('protocols bgp'):
- bgp_as = conf.list_nodes('protocols bgp')[0]
-
- # check BGP neighbors
- for peer in bfd['deleted_peers']:
- if conf.exists('protocols bgp {0} neighbor {1} bfd'.format(bgp_as, peer)):
- raise ConfigError('Cannot delete BFD peer {0}: it is used in BGP configuration'.format(peer))
- if conf.exists('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer)):
- peer_group = conf.return_value('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer))
- if conf.exists('protocols bgp {0} peer-group {1} bfd'.format(bgp_as, peer_group)):
- raise ConfigError('Cannot delete BFD peer {0}: it belongs to BGP peer-group {1} with enabled BFD'.format(peer, peer_group))
+ if 'peer' in bfd:
+ for peer, peer_config in bfd['peer'].items():
+ # IPv6 link local peers require an explicit local address/interface
+ if is_ipv6_link_local(peer):
+ if 'source' not in peer_config or len(peer_config['source'] < 2):
+ raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting')
+
+ # IPv6 peers require an explicit local address
+ if is_ipv6(peer):
+ if 'source' not in peer_config or 'address' not in peer_config['source']:
+ raise ConfigError('BFD IPv6 peers require explicit local address setting')
+
+ if 'multihop' in peer_config:
+ # multihop require source address
+ if 'source' not in peer_config or 'address' not in peer_config['source']:
+ raise ConfigError('BFD multihop require source address')
+
+ # multihop and echo-mode cannot be used together
+ if 'echo_mode' in peer_config:
+ raise ConfigError('Multihop and echo-mode cannot be used together')
+
+ # multihop doesn't accept interface names
+ if 'source' in peer_config and 'interface' in peer_config['source']:
+ raise ConfigError('Multihop and source interface cannot be used together')
return None
def generate(bfd):
- if bfd is None:
+ if not bfd:
+ bfd['new_frr_config'] = ''
return None
- render(config_file, 'frr/bfd.frr.tmpl', bfd)
- return None
+ bfd['new_frr_config'] = render_to_string('frr/bfd.frr.tmpl', bfd)
def apply(bfd):
- if bfd is None:
- return None
-
- call("vtysh -d bfdd -f " + config_file)
- if os.path.exists(config_file):
- os.remove(config_file)
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration()
+ frr_cfg.modify_section('^bfd', '')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bfd['new_frr_config'])
+ frr_cfg.commit_configuration()
+
+ # 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 bfd['new_frr_config'] == '':
+ for a in range(5):
+ frr_cfg.commit_configuration()
return None
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index d0dfb55ec..6770865ff 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.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
@@ -14,114 +14,207 @@
# 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 sys import argv
from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import is_ip
+from vyos.template import render_to_string
from vyos.util import call
from vyos.util import dict_search
-from vyos.template import render
-from vyos.template import render_to_string
+from vyos.validate import is_addr_assigned
from vyos import ConfigError
from vyos import frr
from vyos import airbag
airbag.enable()
-config_file = r'/tmp/bgp.frr'
+frr_daemon = 'bgpd'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
-def get_config():
- conf = Config()
- base = ['protocols', 'nbgp']
+ base_path = ['protocols', 'bgp']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'bgp'] or base_path
bgp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- # XXX: any reason we can not move this into the FRR template?
- # we shall not call vtysh directly, especially not in get_config()
+ # Assign the name of our VRF context. This MUST be done before the return
+ # statement below, else on deletion we will delete the default instance
+ # instead of the VRF instance.
+ if vrf: bgp.update({'vrf' : vrf})
+
if not conf.exists(base):
- bgp = {}
- call('vtysh -c \"conf t\" -c \"no ip protocol bgp\" ')
+ bgp.update({'deleted' : ''})
+ return bgp
- if not conf.exists(base + ['route-map']):
- call('vtysh -c \"conf t\" -c \"no ip protocol bgp\" ')
+ # We also need some additional information from the config,
+ # prefix-lists and route-maps for instance.
+ base = ['policy']
+ tmp = conf.get_config_dict(base, key_mangling=('-', '_'))
+ # Merge policy dict into bgp dict
+ bgp = dict_merge(tmp, bgp)
return bgp
+def verify_remote_as(peer_config, bgp_config):
+ if 'remote_as' in peer_config:
+ return peer_config['remote_as']
+
+ if 'peer_group' in peer_config:
+ peer_group_name = peer_config['peer_group']
+ tmp = dict_search(f'peer_group.{peer_group_name}.remote_as', bgp_config)
+ if tmp: return tmp
+
+ if 'interface' in peer_config:
+ if 'remote_as' in peer_config['interface']:
+ return peer_config['interface']['remote_as']
+
+ if 'peer_group' in peer_config['interface']:
+ peer_group_name = peer_config['interface']['peer_group']
+ tmp = dict_search(f'peer_group.{peer_group_name}.remote_as', bgp_config)
+ if tmp: return tmp
+
+ return None
+
def verify(bgp):
- if not bgp:
+ if not bgp or 'deleted' in bgp:
return None
- # Check if declared more than one ASN
- if len(bgp) > 1:
- raise ConfigError('Only one BGP AS number can be defined!')
-
- for asn, asn_config in bgp.items():
- import pprint
- pprint.pprint(asn_config)
-
- # Common verification for both peer-group and neighbor statements
- for neighbor in ['neighbor', 'peer_group']:
- # bail out early if there is no neighbor or peer-group statement
- # this also saves one indention level
- if neighbor not in asn_config:
- print(f'no {neighbor} found in config')
- continue
-
- for peer, peer_config in asn_config[neighbor].items():
- # Only regular "neighbor" statement can have a peer-group set
- # Check if the configure peer-group exists
- if 'peer_group' in peer_config:
- peer_group = peer_config['peer_group']
- if peer_group not in asn_config['peer_group']:
- raise ConfigError(f'Specified peer-group "{peer_group}" for '\
- f'neighbor "{neighbor}" does not exist!')
-
- # Some checks can/must only be done on a neighbor and nor a peer-group
- if neighbor == 'neighbor':
- # remote-as must be either set explicitly for the neighbor
- # or for the entire peer-group
- if 'remote_as' not in peer_config:
- peer_group = peer_config['peer_group']
- if 'remote_as' not in asn_config['peer_group'][peer_group]:
- raise ConfigError('Remote AS must be set for neighbor or peer-group!')
+ if 'local_as' not in bgp:
+ raise ConfigError('BGP local-as number must be defined!')
+
+ # Common verification for both peer-group and neighbor statements
+ for neighbor in ['neighbor', 'peer_group']:
+ # bail out early if there is no neighbor or peer-group statement
+ # this also saves one indention level
+ if neighbor not in bgp:
+ continue
+
+ for peer, peer_config in bgp[neighbor].items():
+ # Only regular "neighbor" statement can have a peer-group set
+ # Check if the configure peer-group exists
+ if 'peer_group' in peer_config:
+ peer_group = peer_config['peer_group']
+ if 'peer_group' not in bgp or peer_group not in bgp['peer_group']:
+ raise ConfigError(f'Specified peer-group "{peer_group}" for '\
+ f'neighbor "{neighbor}" does not exist!')
+
+ # ttl-security and ebgp-multihop can't be used in the same configration
+ if 'ebgp_multihop' in peer_config and 'ttl_security' in peer_config:
+ raise ConfigError('You can\'t set both ebgp-multihop and ttl-security hops')
+
+ # Check spaces in the password
+ if 'password' in peer_config and ' ' in peer_config['password']:
+ raise ConfigError('You can\'t use spaces in the password')
+
+ # Some checks can/must only be done on a neighbor and not a peer-group
+ if neighbor == 'neighbor':
+ # remote-as must be either set explicitly for the neighbor
+ # or for the entire peer-group
+ if not verify_remote_as(peer_config, bgp):
+ raise ConfigError(f'Neighbor "{peer}" remote-as must be set!')
+
+ # Only checks for ipv4 and ipv6 neighbors
+ # Check if neighbor address is assigned as system interface address
+ if is_ip(peer) and is_addr_assigned(peer):
+ raise ConfigError(f'Can\'t configure local address as neighbor "{peer}"')
+
+ for afi in ['ipv4_unicast', 'ipv6_unicast', 'l2vpn_evpn']:
+ # Bail out early if address family is not configured
+ if 'address_family' not in peer_config or afi not in peer_config['address_family']:
+ continue
+
+ afi_config = peer_config['address_family'][afi]
+ # Validate if configured Prefix list exists
+ if 'prefix_list' in afi_config:
+ for tmp in ['import', 'export']:
+ if tmp not in afi_config['prefix_list']:
+ # bail out early
+ continue
+ # get_config_dict() mangles all '-' characters to '_' this is legitimate, thus all our
+ # compares will run on '_' as also '_' is a valid name for a prefix-list
+ prefix_list = afi_config['prefix_list'][tmp].replace('-', '_')
+ if afi == 'ipv4_unicast':
+ if dict_search(f'policy.prefix_list.{prefix_list}', bgp) == None:
+ raise ConfigError(f'prefix-list "{prefix_list}" used for "{tmp}" does not exist!')
+ elif afi == 'ipv6_unicast':
+ if dict_search(f'policy.prefix_list6.{prefix_list}', bgp) == None:
+ raise ConfigError(f'prefix-list6 "{prefix_list}" used for "{tmp}" does not exist!')
+
+ if 'route_map' in afi_config:
+ for tmp in ['import', 'export']:
+ if tmp in afi_config['route_map']:
+ # get_config_dict() mangles all '-' characters to '_' this is legitim, thus all our
+ # compares will run on '_' as also '_' is a valid name for a route-map
+ route_map = afi_config['route_map'][tmp].replace('-', '_')
+ if dict_search(f'policy.route_map.{route_map}', bgp) == None:
+ raise ConfigError(f'route-map "{route_map}" used for "{tmp}" does not exist!')
+
+ if 'route_reflector_client' in afi_config:
+ if 'remote_as' in peer_config and bgp['local_as'] != peer_config['remote_as']:
+ raise ConfigError('route-reflector-client only supported for iBGP peers')
+ else:
+ if 'peer_group' in peer_config:
+ peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp)
+ if peer_group_as != None and peer_group_as != bgp['local_as']:
+ raise ConfigError('route-reflector-client only supported for iBGP peers')
+
+ # Throw an error if a peer group is not configured for allow range
+ for prefix in dict_search('listen.range', bgp) or []:
+ # we can not use dict_search() here as prefix contains dots ...
+ if 'peer_group' not in bgp['listen']['range'][prefix]:
+ raise ConfigError(f'Listen range for prefix "{prefix}" has no peer group configured.')
+
+ peer_group = bgp['listen']['range'][prefix]['peer_group']
+ if 'peer_group' not in bgp or peer_group not in bgp['peer_group']:
+ raise ConfigError(f'Peer-group "{peer_group}" for listen range "{prefix}" does not exist!')
+
+ if not verify_remote_as(bgp['listen']['range'][prefix], bgp):
+ raise ConfigError(f'Peer-group "{peer_group}" requires remote-as to be set!')
return None
def generate(bgp):
- if not bgp:
+ if not bgp or 'deleted' in bgp:
bgp['new_frr_config'] = ''
return None
- # only one BGP AS is supported, so we can directly send the first key
- # of the config dict
- asn = list(bgp.keys())[0]
- bgp[asn]['asn'] = asn
-
- # render(config) not needed, its only for debug
- render(config_file, 'frr/bgp.frr.tmpl', bgp[asn])
- bgp['new_frr_config'] = render_to_string('frr/bgp.frr.tmpl', bgp[asn])
-
+ bgp['new_frr_config'] = render_to_string('frr/bgp.frr.tmpl', bgp)
return None
def apply(bgp):
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(daemon='bgpd')
- frr_cfg.modify_section(f'router bgp \S+', '')
+ frr_cfg.load_configuration(frr_daemon)
+
+ if 'vrf' in bgp:
+ vrf = bgp['vrf']
+ frr_cfg.modify_section(f'^router bgp \d+ vrf {vrf}$', '')
+ else:
+ frr_cfg.modify_section('^router bgp \d+$', '')
+
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['new_frr_config'])
- frr_cfg.commit_configuration(daemon='bgpd')
+ frr_cfg.commit_configuration(frr_daemon)
# 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 bgp['new_frr_config'] == '':
for a in range(5):
- frr_cfg.commit_configuration(daemon='bgpd')
-
- # Debugging
- '''
- print('')
- print('--------- DEBUGGING ----------')
- print(f'Existing config:\n{frr_cfg["original_config"]}\n\n')
- print(f'Replacement config:\n{bgp["new_frr_config"]}\n\n')
- print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n')
- '''
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # Save configuration to /run/frr/{daemon}.conf
+ frr.save_configuration(frr_daemon)
return None
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index b7afad473..02cf9970c 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
@@ -17,143 +17,200 @@
import os
from sys import exit
+from sys import argv
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.configdict import node_changed
-from vyos import ConfigError
+from vyos.configverify import verify_interface_exists
from vyos.util import call
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 import ConfigError
from vyos import frr
from vyos import airbag
airbag.enable()
+frr_daemon = 'isisd'
+
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['protocols', 'isis']
- isis = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
+
+ base_path = ['protocols', 'isis']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'isis'] or base_path
+ isis = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+
+ # Assign the name of our VRF context. This MUST be done before the return
+ # statement below, else on deletion we will delete the default instance
+ # instead of the VRF instance.
+ if vrf: isis['vrf'] = vrf
+
+ # As we no re-use this Python handler for both VRF and non VRF instances for
+ # IS-IS we need to find out if any interfaces changed so properly adjust
+ # the FRR configuration and not by acctident change interfaces from a
+ # different VRF.
+ 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 also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify()
+ base = ['policy']
+ tmp = conf.get_config_dict(base, key_mangling=('-', '_'))
+ # Merge policy dict into OSPF 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!')
+
+ # 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)
+ if 'vrf' in isis:
+ # If interface specific options are set, we must ensure that the
+ # interface is bound to our requesting VRF. Due to the VyOS
+ # priorities the interface is bound to the VRF after creation of
+ # the VRF itself, and before any routing protocol is configured.
+ vrf = isis['vrf']
+ tmp = get_interface_config(interface)
+ if 'master' not in tmp or tmp['master'] != vrf:
+ raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!')
+
+ # If md5 and plaintext-password set at the same time
+ if 'area_password' in isis:
+ if {'md5', 'plaintext_password'} <= set(isis['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:
+ 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']:
+ 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}\"')
+
+ if 'route_map' in redistr_config:
+ name = redistr_config['route_map']
+ tmp = name.replace('-', '_')
+ if dict_search(f'policy.route_map.{tmp}', isis) == None:
+ raise ConfigError(f'Route-map {name} does not exist!')
+
+ # 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:
+ if not isis or 'deleted' in isis:
isis['new_frr_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/isis.frr.tmpl',
- isis[process])
-
+ isis['new_frr_config'] = render_to_string('frr/isis.frr.tmpl', isis)
return None
def apply(isis):
# 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.load_configuration(frr_daemon)
+
+ # Generate empty helper string which can be ammended to FRR commands,
+ # it will be either empty (default VRF) or contain the "vrf <name" statement
+ vrf = ''
+ if 'vrf' in isis:
+ vrf = ' vrf ' + isis['vrf']
+
+ frr_cfg.modify_section(f'^router isis VyOS{vrf}$', '')
+ for key in ['interface', 'interface_removed']:
+ if key not in isis:
+ continue
+ for interface in isis[key]:
+ frr_cfg.modify_section(f'^interface {interface}{vrf}$', '')
+
frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['new_frr_config'])
- frr_cfg.commit_configuration(daemon='isisd')
+ frr_cfg.commit_configuration(frr_daemon)
# 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')
- '''
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # Save configuration to /run/frr/{daemon}.conf
+ frr.save_configuration(frr_daemon)
return None
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
new file mode 100755
index 000000000..b4ee8659a
--- /dev/null
+++ b/src/conf_mode/protocols_ospf.py
@@ -0,0 +1,216 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from sys import argv
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configdict import node_changed
+from vyos.configverify import verify_route_maps
+from vyos.configverify import verify_interface_exists
+from vyos.template import render_to_string
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.util import get_interface_config
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+frr_daemon = 'ospfd'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
+
+ base_path = ['protocols', 'ospf']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospf'] or base_path
+ ospf = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+
+ # Assign the name of our VRF context. This MUST be done before the return
+ # statement below, else on deletion we will delete the default instance
+ # instead of the VRF instance.
+ if vrf: ospf['vrf'] = vrf
+
+ # As we no re-use this Python handler for both VRF and non VRF instances for
+ # OSPF we need to find out if any interfaces changed so properly adjust
+ # the FRR configuration and not by acctident change interfaces from a
+ # different VRF.
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ ospf['interface_removed'] = list(interfaces_removed)
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ ospf.update({'deleted' : ''})
+ return ospf
+
+ # 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. As we use the exact same CLI definition for
+ # both the non-vrf and vrf version this is absolutely safe!
+ default_values = defaults(base_path)
+
+ # We have to cleanup the default dict, as default values could enable features
+ # which are not explicitly enabled on the CLI. Example: default-information
+ # originate comes with a default metric-type of 2, which will enable the
+ # entire default-information originate tree, even when not set via CLI so we
+ # need to check this first and probably drop that key.
+ if dict_search('default_information.originate', ospf) is None:
+ del default_values['default_information']
+ if dict_search('area.area_type.nssa', ospf) is None:
+ del default_values['area']['area_type']['nssa']
+ if 'mpls_te' not in ospf:
+ del default_values['mpls_te']
+ for protocol in ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
+ if dict_search(f'redistribute.{protocol}', ospf) is None:
+ del default_values['redistribute'][protocol]
+
+ # XXX: T2665: we currently have no nice way for defaults under tag nodes,
+ # clean them out and add them manually :(
+ del default_values['neighbor']
+ del default_values['area']['virtual_link']
+ del default_values['interface']
+
+ # merge in remaining default values
+ ospf = dict_merge(default_values, ospf)
+
+ if 'neighbor' in ospf:
+ default_values = defaults(base + ['neighbor'])
+ for neighbor in ospf['neighbor']:
+ ospf['neighbor'][neighbor] = dict_merge(default_values, ospf['neighbor'][neighbor])
+
+ if 'area' in ospf:
+ default_values = defaults(base + ['area', 'virtual-link'])
+ for area, area_config in ospf['area'].items():
+ if 'virtual_link' in area_config:
+ print(default_values)
+ for virtual_link in area_config['virtual_link']:
+ ospf['area'][area]['virtual_link'][virtual_link] = dict_merge(
+ default_values, ospf['area'][area]['virtual_link'][virtual_link])
+
+ if 'interface' in ospf:
+ for interface in ospf['interface']:
+ # We need to reload the defaults on every pass b/c of
+ # hello-multiplier dependency on dead-interval
+ default_values = defaults(base + ['interface'])
+ # If hello-multiplier is set, we need to remove the default from
+ # dead-interval.
+ if 'hello_multiplier' in ospf['interface'][interface]:
+ del default_values['dead_interval']
+
+ ospf['interface'][interface] = dict_merge(default_values,
+ ospf['interface'][interface])
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify()
+ base = ['policy']
+ tmp = conf.get_config_dict(base, key_mangling=('-', '_'))
+ # Merge policy dict into OSPF dict
+ ospf = dict_merge(tmp, ospf)
+
+ return ospf
+
+def verify(ospf):
+ if not ospf:
+ return None
+
+ verify_route_maps(ospf)
+
+ if 'interface' in ospf:
+ for interface in ospf['interface']:
+ verify_interface_exists(interface)
+ # One can not use dead-interval and hello-multiplier at the same
+ # time. FRR will only activate the last option set via CLI.
+ if {'hello_multiplier', 'dead_interval'} <= set(ospf['interface'][interface]):
+ raise ConfigError(f'Can not use hello-multiplier and dead-interval ' \
+ f'concurrently for {interface}!')
+
+ if 'vrf' in ospf:
+ # If interface specific options are set, we must ensure that the
+ # interface is bound to our requesting VRF. Due to the VyOS
+ # priorities the interface is bound to the VRF after creation of
+ # the VRF itself, and before any routing protocol is configured.
+ vrf = ospf['vrf']
+ tmp = get_interface_config(interface)
+ if 'master' not in tmp or tmp['master'] != vrf:
+ raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!')
+
+ return None
+
+def generate(ospf):
+ if not ospf or 'deleted' in ospf:
+ ospf['new_frr_config'] = ''
+ return None
+
+ ospf['new_frr_config'] = render_to_string('frr/ospf.frr.tmpl', ospf)
+ return None
+
+def apply(ospf):
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(frr_daemon)
+
+ # Generate empty helper string which can be ammended to FRR commands,
+ # it will be either empty (default VRF) or contain the "vrf <name" statement
+ vrf = ''
+ if 'vrf' in ospf:
+ vrf = ' vrf ' + ospf['vrf']
+
+ frr_cfg.modify_section(f'^router ospf{vrf}$', '')
+ for key in ['interface', 'interface_removed']:
+ if key not in ospf:
+ continue
+ for interface in ospf[key]:
+ frr_cfg.modify_section(f'^interface {interface}{vrf}$', '')
+
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['new_frr_config'])
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # 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 ospf['new_frr_config'] == '':
+ for a in range(5):
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # Save configuration to /run/frr/{daemon}.conf
+ frr.save_configuration(frr_daemon)
+
+ 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/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
new file mode 100755
index 000000000..f3beab204
--- /dev/null
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_maps
+from vyos.template import render_to_string
+from vyos.util import call
+from vyos.ifconfig import Interface
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+frr_daemon = 'ospf6d'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['protocols', 'ospfv3']
+ ospfv3 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ return ospfv3
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify()
+ base = ['policy']
+ tmp = conf.get_config_dict(base, key_mangling=('-', '_'))
+ # Merge policy dict into OSPF dict
+ ospfv3 = dict_merge(tmp, ospfv3)
+
+ return ospfv3
+
+def verify(ospfv3):
+ if not ospfv3:
+ return None
+
+ verify_route_maps(ospfv3)
+
+ if 'interface' in ospfv3:
+ for ifname, if_config in ospfv3['interface'].items():
+ if 'ifmtu' in if_config:
+ mtu = Interface(ifname).get_mtu()
+ if int(if_config['ifmtu']) > int(mtu):
+ raise ConfigError(f'OSPFv3 ifmtu cannot go beyond physical MTU of "{mtu}"')
+
+ return None
+
+def generate(ospfv3):
+ if not ospfv3:
+ ospfv3['new_frr_config'] = ''
+ return None
+
+ ospfv3['new_frr_config'] = render_to_string('frr/ospfv3.frr.tmpl', ospfv3)
+ return None
+
+def apply(ospfv3):
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(frr_daemon)
+ frr_cfg.modify_section(r'^interface \S+', '')
+ frr_cfg.modify_section('^router ospf6$', '')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config'])
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # If FRR config is blank, re-run the blank commit x times due to frr-reload
+ # behavior/bug not properly clearing out on one commit.
+ if ospfv3['new_frr_config'] == '':
+ for a in range(5):
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # Save configuration to /run/frr/{daemon}.conf
+ frr.save_configuration(frr_daemon)
+
+ 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/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 8ddd705f2..34d42d630 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 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
@@ -18,15 +18,19 @@ import os
from sys import exit
-from vyos import ConfigError
from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_maps
from vyos.util import call
-from vyos.template import render
-
+from vyos.util import dict_search
+from vyos.xml import defaults
+from vyos.template import render_to_string
+from vyos import ConfigError
+from vyos import frr
from vyos import airbag
airbag.enable()
-config_file = r'/tmp/ripd.frr'
+frr_daemon = 'ripd'
def get_config(config=None):
if config:
@@ -34,277 +38,86 @@ def get_config(config=None):
else:
conf = Config()
base = ['protocols', 'rip']
- rip_conf = {
- 'rip_conf' : False,
- 'default_distance' : [],
- 'default_originate' : False,
- 'old_rip' : {
- 'default_metric' : [],
- 'distribute' : {},
- 'neighbors' : {},
- 'networks' : {},
- 'net_distance' : {},
- 'passive_iface' : {},
- 'redist' : {},
- 'route' : {},
- 'ifaces' : {},
- 'timer_garbage' : 120,
- 'timer_timeout' : 180,
- 'timer_update' : 30
- },
- 'rip' : {
- 'default_metric' : None,
- 'distribute' : {},
- 'neighbors' : {},
- 'networks' : {},
- 'net_distance' : {},
- 'passive_iface' : {},
- 'redist' : {},
- 'route' : {},
- 'ifaces' : {},
- 'timer_garbage' : 120,
- 'timer_timeout' : 180,
- 'timer_update' : 30
- }
- }
-
- if not (conf.exists(base) or conf.exists_effective(base)):
- return None
-
- if conf.exists(base):
- rip_conf['rip_conf'] = True
-
- conf.set_level(base)
-
- # Get default distance
- if conf.exists_effective('default-distance'):
- rip_conf['old_default_distance'] = conf.return_effective_value('default-distance')
-
- if conf.exists('default-distance'):
- rip_conf['default_distance'] = conf.return_value('default-distance')
-
- # Get default information originate (originate default route)
- if conf.exists_effective('default-information originate'):
- rip_conf['old_default_originate'] = True
-
- if conf.exists('default-information originate'):
- rip_conf['default_originate'] = True
-
- # Get default-metric
- if conf.exists_effective('default-metric'):
- rip_conf['old_rip']['default_metric'] = conf.return_effective_value('default-metric')
-
- if conf.exists('default-metric'):
- rip_conf['rip']['default_metric'] = conf.return_value('default-metric')
-
- # Get distribute list interface old_rip
- for dist_iface in conf.list_effective_nodes('distribute-list interface'):
- # Set level 'distribute-list interface ethX'
- conf.set_level(base + ['distribute-list', 'interface', dist_iface])
- rip_conf['rip']['distribute'].update({
- dist_iface : {
- 'iface_access_list_in': conf.return_effective_value('access-list in'.format(dist_iface)),
- 'iface_access_list_out': conf.return_effective_value('access-list out'.format(dist_iface)),
- 'iface_prefix_list_in': conf.return_effective_value('prefix-list in'.format(dist_iface)),
- 'iface_prefix_list_out': conf.return_effective_value('prefix-list out'.format(dist_iface))
- }
- })
-
- # Access-list in old_rip
- if conf.exists_effective('access-list in'.format(dist_iface)):
- rip_conf['old_rip']['iface_access_list_in'] = conf.return_effective_value('access-list in'.format(dist_iface))
- # Access-list out old_rip
- if conf.exists_effective('access-list out'.format(dist_iface)):
- rip_conf['old_rip']['iface_access_list_out'] = conf.return_effective_value('access-list out'.format(dist_iface))
- # Prefix-list in old_rip
- if conf.exists_effective('prefix-list in'.format(dist_iface)):
- rip_conf['old_rip']['iface_prefix_list_in'] = conf.return_effective_value('prefix-list in'.format(dist_iface))
- # Prefix-list out old_rip
- if conf.exists_effective('prefix-list out'.format(dist_iface)):
- rip_conf['old_rip']['iface_prefix_list_out'] = conf.return_effective_value('prefix-list out'.format(dist_iface))
-
- conf.set_level(base)
-
- # 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])
- rip_conf['rip']['distribute'].update({
- dist_iface : {
- 'iface_access_list_in': conf.return_value('access-list in'.format(dist_iface)),
- 'iface_access_list_out': conf.return_value('access-list out'.format(dist_iface)),
- 'iface_prefix_list_in': conf.return_value('prefix-list in'.format(dist_iface)),
- 'iface_prefix_list_out': conf.return_value('prefix-list out'.format(dist_iface))
- }
- })
-
- # Access-list in
- if conf.exists('access-list in'.format(dist_iface)):
- rip_conf['rip']['iface_access_list_in'] = conf.return_value('access-list in'.format(dist_iface))
- # Access-list out
- if conf.exists('access-list out'.format(dist_iface)):
- rip_conf['rip']['iface_access_list_out'] = conf.return_value('access-list out'.format(dist_iface))
- # Prefix-list in
- if conf.exists('prefix-list in'.format(dist_iface)):
- rip_conf['rip']['iface_prefix_list_in'] = conf.return_value('prefix-list in'.format(dist_iface))
- # Prefix-list out
- if conf.exists('prefix-list out'.format(dist_iface)):
- rip_conf['rip']['iface_prefix_list_out'] = conf.return_value('prefix-list out'.format(dist_iface))
-
- conf.set_level(base + ['distribute-list'])
-
- # Get distribute list, access-list in
- if conf.exists_effective('access-list in'):
- rip_conf['old_rip']['dist_acl_in'] = conf.return_effective_value('access-list in')
-
- if conf.exists('access-list in'):
- rip_conf['rip']['dist_acl_in'] = conf.return_value('access-list in')
-
- # Get distribute list, access-list out
- if conf.exists_effective('access-list out'):
- rip_conf['old_rip']['dist_acl_out'] = conf.return_effective_value('access-list out')
-
- if conf.exists('access-list out'):
- rip_conf['rip']['dist_acl_out'] = conf.return_value('access-list out')
-
- # Get ditstribute list, prefix-list in
- if conf.exists_effective('prefix-list in'):
- rip_conf['old_rip']['dist_prfx_in'] = conf.return_effective_value('prefix-list in')
-
- if conf.exists('prefix-list in'):
- rip_conf['rip']['dist_prfx_in'] = conf.return_value('prefix-list in')
-
- # Get distribute list, prefix-list out
- if conf.exists_effective('prefix-list out'):
- rip_conf['old_rip']['dist_prfx_out'] = conf.return_effective_value('prefix-list out')
+ rip = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- if conf.exists('prefix-list out'):
- rip_conf['rip']['dist_prfx_out'] = conf.return_value('prefix-list out')
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ return rip
- conf.set_level(base)
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ # merge in remaining default values
+ rip = dict_merge(default_values, rip)
- # Get network Interfaces
- if conf.exists_effective('interface'):
- rip_conf['old_rip']['ifaces'] = conf.return_effective_values('interface')
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify()
+ base = ['policy']
+ tmp = conf.get_config_dict(base, key_mangling=('-', '_'))
+ # Merge policy dict into OSPF dict
+ rip = dict_merge(tmp, rip)
- if conf.exists('interface'):
- rip_conf['rip']['ifaces'] = conf.return_values('interface')
+ return rip
- # Get neighbors
- if conf.exists_effective('neighbor'):
- rip_conf['old_rip']['neighbors'] = conf.return_effective_values('neighbor')
-
- if conf.exists('neighbor'):
- rip_conf['rip']['neighbors'] = conf.return_values('neighbor')
-
- # Get networks
- if conf.exists_effective('network'):
- rip_conf['old_rip']['networks'] = conf.return_effective_values('network')
-
- if conf.exists('network'):
- rip_conf['rip']['networks'] = conf.return_values('network')
-
- # Get network-distance old_rip
- for net_dist in conf.list_effective_nodes('network-distance'):
- rip_conf['old_rip']['net_distance'].update({
- net_dist : {
- 'access_list' : conf.return_effective_value('network-distance {0} access-list'.format(net_dist)),
- 'distance' : conf.return_effective_value('network-distance {0} distance'.format(net_dist)),
- }
- })
-
- # Get network-distance
- for net_dist in conf.list_nodes('network-distance'):
- rip_conf['rip']['net_distance'].update({
- net_dist : {
- 'access_list' : conf.return_value('network-distance {0} access-list'.format(net_dist)),
- 'distance' : conf.return_value('network-distance {0} distance'.format(net_dist)),
- }
- })
-
- # Get passive-interface
- if conf.exists_effective('passive-interface'):
- rip_conf['old_rip']['passive_iface'] = conf.return_effective_values('passive-interface')
-
- if conf.exists('passive-interface'):
- rip_conf['rip']['passive_iface'] = conf.return_values('passive-interface')
-
- # Get redistribute for old_rip
- for protocol in conf.list_effective_nodes('redistribute'):
- rip_conf['old_rip']['redist'].update({
- protocol : {
- 'metric' : conf.return_effective_value('redistribute {0} metric'.format(protocol)),
- 'route_map' : conf.return_effective_value('redistribute {0} route-map'.format(protocol)),
- }
- })
-
- # Get redistribute
- for protocol in conf.list_nodes('redistribute'):
- rip_conf['rip']['redist'].update({
- protocol : {
- 'metric' : conf.return_value('redistribute {0} metric'.format(protocol)),
- 'route_map' : conf.return_value('redistribute {0} route-map'.format(protocol)),
- }
- })
-
- conf.set_level(base)
-
- # Get route
- if conf.exists_effective('route'):
- rip_conf['old_rip']['route'] = conf.return_effective_values('route')
-
- if conf.exists('route'):
- rip_conf['rip']['route'] = conf.return_values('route')
-
- # Get timers garbage
- if conf.exists_effective('timers garbage-collection'):
- rip_conf['old_rip']['timer_garbage'] = conf.return_effective_value('timers garbage-collection')
-
- if conf.exists('timers garbage-collection'):
- rip_conf['rip']['timer_garbage'] = conf.return_value('timers garbage-collection')
-
- # Get timers timeout
- if conf.exists_effective('timers timeout'):
- rip_conf['old_rip']['timer_timeout'] = conf.return_effective_value('timers timeout')
+def verify(rip):
+ if not rip:
+ return None
- if conf.exists('timers timeout'):
- rip_conf['rip']['timer_timeout'] = conf.return_value('timers timeout')
+ acl_in = dict_search('distribute_list.access_list.in', rip)
+ if acl_in and acl_in not in (dict_search('policy.access_list', rip) or []):
+ raise ConfigError(f'Inbound ACL "{acl_in}" does not exist!')
- # Get timers update
- if conf.exists_effective('timers update'):
- rip_conf['old_rip']['timer_update'] = conf.return_effective_value('timers update')
+ acl_out = dict_search('distribute_list.access_list.out', rip)
+ if acl_out and acl_out not in (dict_search('policy.access_list', rip) or []):
+ raise ConfigError(f'Outbound ACL "{acl_out}" does not exist!')
- if conf.exists('timers update'):
- rip_conf['rip']['timer_update'] = conf.return_value('timers update')
+ prefix_list_in = dict_search('distribute_list.prefix_list.in', rip)
+ if prefix_list_in and prefix_list_in.replace('-','_') not in (dict_search('policy.prefix_list', rip) or []):
+ raise ConfigError(f'Inbound prefix-list "{prefix_list_in}" does not exist!')
- return rip_conf
+ prefix_list_out = dict_search('distribute_list.prefix_list.out', rip)
+ if prefix_list_out and prefix_list_out.replace('-','_') not in (dict_search('policy.prefix_list', rip) or []):
+ raise ConfigError(f'Outbound prefix-list "{prefix_list_out}" does not exist!')
-def verify(rip):
- if rip is None:
- return None
+ if 'interface' in rip:
+ for interface, interface_options in rip['interface'].items():
+ if 'authentication' in interface_options:
+ if {'md5', 'plaintext_password'} <= set(interface_options['authentication']):
+ raise ConfigError('Can not use both md5 and plaintext-password at the same time!')
+ if 'split_horizon' in interface_options:
+ if {'disable', 'poison_reverse'} <= set(interface_options['split_horizon']):
+ raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \
+ f'with "split-horizon disable" for "{interface}"!')
- # Check for network. If network-distance acl is set and distance not set
- for net in rip['rip']['net_distance']:
- if not rip['rip']['net_distance'][net]['distance']:
- raise ConfigError(f"Must specify distance for network {net}")
+ verify_route_maps(rip)
def generate(rip):
- if rip is None:
+ if not rip:
+ rip['new_frr_config'] = ''
return None
- render(config_file, 'frr/rip.frr.tmpl', rip)
+ rip['new_frr_config'] = render_to_string('frr/rip.frr.tmpl', rip)
+
return None
def apply(rip):
- if rip is None:
- return None
-
- if os.path.exists(config_file):
- call(f'vtysh -d ripd -f {config_file}')
- os.remove(config_file)
- else:
- print("File {0} not found".format(config_file))
-
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(frr_daemon)
+ frr_cfg.modify_section(r'key chain \S+', '')
+ frr_cfg.modify_section(r'interface \S+', '')
+ frr_cfg.modify_section('router rip', '')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rip['new_frr_config'])
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # 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 rip['new_frr_config'] == '':
+ for a in range(5):
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # Save configuration to /run/frr/{daemon}.conf
+ frr.save_configuration(frr_daemon)
return None
@@ -317,4 +130,3 @@ if __name__ == '__main__':
except ConfigError as e:
print(e)
exit(1)
-
diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py
new file mode 100755
index 000000000..eff4297f9
--- /dev/null
+++ b/src/conf_mode/protocols_ripng.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_maps
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.xml import defaults
+from vyos.template import render_to_string
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+frr_daemon = 'ripngd'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['protocols', 'ripng']
+ ripng = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ return ripng
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ # merge in remaining default values
+ ripng = dict_merge(default_values, ripng)
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify()
+ base = ['policy']
+ tmp = conf.get_config_dict(base, key_mangling=('-', '_'))
+ # Merge policy dict into OSPF dict
+ ripng = dict_merge(tmp, ripng)
+
+ return ripng
+
+def verify(ripng):
+ if not ripng:
+ return None
+
+ acl_in = dict_search('distribute_list.access_list.in', ripng)
+ if acl_in and acl_in not in (dict_search('policy.access_list6', ripng) or []):
+ raise ConfigError(f'Inbound access-list6 "{acl_in}" does not exist!')
+
+ acl_out = dict_search('distribute_list.access_list.out', ripng)
+ if acl_out and acl_out not in (dict_search('policy.access_list6', ripng) or []):
+ raise ConfigError(f'Outbound access-list6 "{acl_out}" does not exist!')
+
+ prefix_list_in = dict_search('distribute_list.prefix_list.in', ripng)
+ if prefix_list_in and prefix_list_in.replace('-','_') not in (dict_search('policy.prefix_list6', ripng) or []):
+ raise ConfigError(f'Inbound prefix-list6 "{prefix_list_in}" does not exist!')
+
+ prefix_list_out = dict_search('distribute_list.prefix_list.out', ripng)
+ if prefix_list_out and prefix_list_out.replace('-','_') not in (dict_search('policy.prefix_list6', ripng) or []):
+ raise ConfigError(f'Outbound prefix-list6 "{prefix_list_out}" does not exist!')
+
+ if 'interface' in ripng:
+ for interface, interface_options in ripng['interface'].items():
+ if 'authentication' in interface_options:
+ if {'md5', 'plaintext_password'} <= set(interface_options['authentication']):
+ raise ConfigError('Can not use both md5 and plaintext-password at the same time!')
+ if 'split_horizon' in interface_options:
+ if {'disable', 'poison_reverse'} <= set(interface_options['split_horizon']):
+ raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \
+ f'with "split-horizon disable" for "{interface}"!')
+
+ verify_route_maps(ripng)
+
+def generate(ripng):
+ if not ripng:
+ ripng['new_frr_config'] = ''
+ return None
+
+ ripng['new_frr_config'] = render_to_string('frr/ripng.frr.tmpl', ripng)
+ return None
+
+def apply(ripng):
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(frr_daemon)
+ frr_cfg.modify_section(r'key chain \S+', '')
+ frr_cfg.modify_section(r'interface \S+', '')
+ frr_cfg.modify_section('router ripng', '')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ripng['new_frr_config'])
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # 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 ripng['new_frr_config'] == '':
+ for a in range(5):
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # Save configuration to /run/frr/{daemon}.conf
+ frr.save_configuration(frr_daemon)
+
+ 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/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
new file mode 100755
index 000000000..75b870b05
--- /dev/null
+++ b/src/conf_mode/protocols_rpki.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render_to_string
+from vyos.util import call
+from vyos.util import dict_search
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+frr_daemon = 'bgpd'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['protocols', 'rpki']
+
+ rpki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ if not conf.exists(base):
+ return rpki
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ rpki = dict_merge(default_values, rpki)
+
+ return rpki
+
+def verify(rpki):
+ if not rpki:
+ return None
+
+ if 'cache' in rpki:
+ preferences = []
+ for peer, peer_config in rpki['cache'].items():
+ for mandatory in ['port', 'preference']:
+ if mandatory not in peer_config:
+ raise ConfigError(f'RPKI cache "{peer}" {mandatory} must be defined!')
+
+ if 'preference' in peer_config:
+ preference = peer_config['preference']
+ if preference in preferences:
+ raise ConfigError(f'RPKI cache with preference {preference} already configured!')
+ preferences.append(preference)
+
+ if 'ssh' in peer_config:
+ files = ['private_key_file', 'public_key_file', 'known_hosts_file']
+ for file in files:
+ if file not in peer_config['ssh']:
+ raise ConfigError('RPKI+SSH requires username, public/private ' \
+ 'keys and known-hosts file to be defined!')
+
+ filename = peer_config['ssh'][file]
+ if not os.path.exists(filename):
+ raise ConfigError(f'RPKI SSH {file.replace("-","-")} "{filename}" does not exist!')
+
+ return None
+
+def generate(rpki):
+ rpki['new_frr_config'] = render_to_string('frr/rpki.frr.tmpl', rpki)
+ return None
+
+def apply(rpki):
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(frr_daemon)
+ frr_cfg.modify_section('rpki', '')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rpki['new_frr_config'])
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # If FRR config is blank, re-run the blank commit x times due to frr-reload
+ # behavior/bug not properly clearing out on one commit.
+ if rpki['new_frr_config'] == '':
+ for a in range(5):
+ frr_cfg.commit_configuration(frr_daemon)
+
+ 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/protocols_static.py b/src/conf_mode/protocols_static.py
new file mode 100755
index 000000000..0de073a6d
--- /dev/null
+++ b/src/conf_mode/protocols_static.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from sys import argv
+
+from vyos.config import Config
+from vyos.configverify import verify_route_maps
+from vyos.configverify import verify_vrf
+from vyos.template import render_to_string
+from vyos.util import call
+from vyos import ConfigError
+from vyos import frr
+from vyos import airbag
+airbag.enable()
+
+frr_daemon = 'staticd'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ vrf = None
+ if len(argv) > 1:
+ vrf = argv[1]
+
+ base_path = ['protocols', 'static']
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path
+ static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ # Assign the name of our VRF context
+ if vrf: static['vrf'] = vrf
+
+ return static
+
+def verify(static):
+ verify_route_maps(static)
+
+ for route in ['route', 'route6']:
+ # if there is no route(6) key in the dictionary we can immediately
+ # bail out early
+ if route not in static:
+ continue
+
+ # When leaking routes to other VRFs we must ensure that the destination
+ # VRF exists
+ for prefix, prefix_options in static[route].items():
+ # both the interface and next-hop CLI node can have a VRF subnode,
+ # thus we check this using a for loop
+ for type in ['interface', 'next_hop']:
+ if type in prefix_options:
+ for interface, interface_config in prefix_options[type].items():
+ verify_vrf(interface_config)
+
+ return None
+
+def generate(static):
+ static['new_frr_config'] = render_to_string('frr/static.frr.tmpl', static)
+ return None
+
+def apply(static):
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+ frr_cfg.load_configuration(frr_daemon)
+
+ if 'vrf' in static:
+ vrf = static['vrf']
+ frr_cfg.modify_section(f'^vrf {vrf}$', '')
+ else:
+ frr_cfg.modify_section(r'^ip route .*', '')
+ frr_cfg.modify_section(r'^ipv6 route .*', '')
+
+ frr_cfg.add_before(r'(interface .*|line vty)', static['new_frr_config'])
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # 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 static['new_frr_config'] == '':
+ for a in range(5):
+ frr_cfg.commit_configuration(frr_daemon)
+
+ # Save configuration to /run/frr/{daemon}.conf
+ frr.save_configuration(frr_daemon)
+
+ 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/service_console-server.py b/src/conf_mode/service_console-server.py
index 0e5fc75b0..51050e702 100755
--- a/src/conf_mode/service_console-server.py
+++ b/src/conf_mode/service_console-server.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,6 +17,7 @@
import os
from sys import exit
+from psutil import process_iter
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -25,7 +26,8 @@ from vyos.util import call
from vyos.xml import defaults
from vyos import ConfigError
-config_file = r'/run/conserver/conserver.cf'
+config_file = '/run/conserver/conserver.cf'
+dropbear_systemd_file = '/etc/systemd/system/dropbear@{port}.service.d/override.conf'
def get_config(config=None):
if config:
@@ -59,14 +61,19 @@ def verify(proxy):
if not proxy:
return None
+ processes = process_iter(['name', 'cmdline'])
if 'device' in proxy:
- for device in proxy['device']:
- if 'speed' not in proxy['device'][device]:
- raise ConfigError(f'Serial port speed must be defined for "{device}"!')
+ for device, device_config in proxy['device'].items():
+ for process in processes:
+ if 'agetty' in process.name() and device in process.cmdline():
+ raise ConfigError(f'Port "{device}" already provides a '\
+ 'console used by "system console"!')
+
+ if 'speed' not in device_config:
+ raise ConfigError(f'Port "{device}" requires speed to be set!')
- if 'ssh' in proxy['device'][device]:
- if 'port' not in proxy['device'][device]['ssh']:
- raise ConfigError(f'SSH port must be defined for "{device}"!')
+ if 'ssh' in device_config and 'port' not in device_config['ssh']:
+ raise ConfigError(f'Port "{device}" requires SSH port to be set!')
return None
@@ -75,9 +82,22 @@ def generate(proxy):
return None
render(config_file, 'conserver/conserver.conf.tmpl', proxy)
+ if 'device' in proxy:
+ for device, device_config in proxy['device'].items():
+ if 'ssh' not in device_config:
+ continue
+
+ tmp = {
+ 'device' : device,
+ 'port' : device_config['ssh']['port'],
+ }
+ render(dropbear_systemd_file.format(**tmp),
+ 'conserver/dropbear@.service.tmpl', tmp)
+
return None
def apply(proxy):
+ call('systemctl daemon-reload')
call('systemctl stop dropbear@*.service conserver-server.service')
if not proxy:
@@ -88,10 +108,11 @@ def apply(proxy):
call('systemctl restart conserver-server.service')
if 'device' in proxy:
- for device in proxy['device']:
- if 'ssh' in proxy['device'][device]:
- port = proxy['device'][device]['ssh']['port']
- call(f'systemctl restart dropbear@{device}.service')
+ for device, device_config in proxy['device'].items():
+ if 'ssh' not in device_config:
+ continue
+ port = device_config['ssh']['port']
+ call(f'systemctl restart dropbear@{port}.service')
return None
diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py
index 8dfae348a..cbbd2e0bc 100755
--- a/src/conf_mode/service_webproxy.py
+++ b/src/conf_mode/service_webproxy.py
@@ -123,9 +123,6 @@ def verify(proxy):
ldap_auth = dict_search('authentication.method', proxy) == 'ldap'
for address, config in proxy['listen_address'].items():
- if not is_addr_assigned(address):
- raise ConfigError(
- f'listen-address "{address}" not assigned on any interface!')
if ldap_auth and 'disable_transparent' not in config:
raise ConfigError('Authentication can not be configured when ' \
'proxy is in transparent mode')
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 8f99053d2..67724b043 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.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,6 +17,8 @@
import os
from sys import exit
+from syslog import syslog
+from syslog import LOG_INFO
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -28,9 +30,13 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-config_file = r'/run/ssh/sshd_config'
+config_file = r'/run/sshd/sshd_config'
systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf'
+key_rsa = '/etc/ssh/ssh_host_rsa_key'
+key_dsa = '/etc/ssh/ssh_host_dsa_key'
+key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
+
def get_config(config=None):
if config:
conf = config
@@ -66,8 +72,22 @@ def generate(ssh):
return None
+ # This usually happens only once on a fresh system, SSH keys need to be
+ # freshly generted, one per every system!
+ if not os.path.isfile(key_rsa):
+ syslog(LOG_INFO, 'SSH RSA host key not found, generating new key!')
+ call(f'ssh-keygen -q -N "" -t rsa -f {key_rsa}')
+ if not os.path.isfile(key_dsa):
+ syslog(LOG_INFO, 'SSH DSA host key not found, generating new key!')
+ call(f'ssh-keygen -q -N "" -t dsa -f {key_dsa}')
+ if not os.path.isfile(key_ed25519):
+ syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!')
+ call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}')
+
render(config_file, 'ssh/sshd_config.tmpl', ssh)
render(systemd_override, 'ssh/override.conf.tmpl', ssh)
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
return None
@@ -75,13 +95,9 @@ def apply(ssh):
if not ssh:
# SSH access is removed in the commit
call('systemctl stop ssh.service')
+ return None
- # Reload systemd manager configuration
- call('systemctl daemon-reload')
-
- if ssh:
- call('systemctl restart ssh.service')
-
+ call('systemctl restart ssh.service')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 39bad717d..99af5c757 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.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
@@ -16,33 +16,30 @@
import os
-from crypt import crypt, METHOD_SHA512
-from netifaces import interfaces
+from crypt import crypt
+from crypt import METHOD_SHA512
from psutil import users
-from pwd import getpwall, getpwnam
+from pwd import getpwall
+from pwd import getpwnam
from spwd import getspnam
from sys import exit
from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configverify import verify_vrf
from vyos.template import render
-from vyos.util import cmd, call, DEVNULL, chmod_600, chmod_755
+from vyos.template import is_ipv4
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import DEVNULL
+from vyos.util import dict_search
+from vyos.xml import defaults
from vyos import ConfigError
-
from vyos import airbag
airbag.enable()
radius_config_file = "/etc/pam_radius_auth.conf"
-default_config_data = {
- 'deleted': False,
- 'add_users': [],
- 'del_users': [],
- 'radius_server': [],
- 'radius_source_address': '',
- 'radius_vrf': ''
-}
-
-
def get_local_users():
"""Return list of dynamically allocated users (see Debian Policy Manual)"""
local_users = []
@@ -57,211 +54,131 @@ def get_local_users():
def get_config(config=None):
- login = default_config_data
if config:
conf = config
else:
conf = Config()
- base_level = ['system', 'login']
-
- # We do not need to check if the nodes exist or not and bail out early
- # ... this would interrupt the following logic on determine which users
- # should be deleted and which users should stay.
- #
- # All fine so far!
-
- # Read in all local users and store to list
- for username in conf.list_nodes(base_level + ['user']):
- user = {
- 'name': username,
- 'password_plaintext': '',
- 'password_encrypted': '!',
- 'public_keys': [],
- 'full_name': '',
- 'home_dir': '/home/' + username,
- }
- conf.set_level(base_level + ['user', username])
-
- # Plaintext password
- if conf.exists(['authentication', 'plaintext-password']):
- user['password_plaintext'] = conf.return_value(
- ['authentication', 'plaintext-password'])
-
- # Encrypted password
- if conf.exists(['authentication', 'encrypted-password']):
- user['password_encrypted'] = conf.return_value(
- ['authentication', 'encrypted-password'])
-
- # User real name
- if conf.exists(['full-name']):
- user['full_name'] = conf.return_value(['full-name'])
-
- # User home-directory
- if conf.exists(['home-directory']):
- user['home_dir'] = conf.return_value(['home-directory'])
-
- # Read in public keys
- for id in conf.list_nodes(['authentication', 'public-keys']):
- key = {
- 'name': id,
- 'key': '',
- 'options': '',
- 'type': ''
- }
- conf.set_level(base_level + ['user', username, 'authentication',
- 'public-keys', id])
-
- # Public Key portion
- if conf.exists(['key']):
- key['key'] = conf.return_value(['key'])
-
- # Options for individual public key
- if conf.exists(['options']):
- key['options'] = conf.return_value(['options'])
-
- # Type of public key
- if conf.exists(['type']):
- key['type'] = conf.return_value(['type'])
-
- # Append individual public key to list of user keys
- user['public_keys'].append(key)
-
- login['add_users'].append(user)
-
- #
- # RADIUS configuration
- #
- conf.set_level(base_level + ['radius'])
-
- if conf.exists(['source-address']):
- login['radius_source_address'] = conf.return_value(['source-address'])
-
- # retrieve VRF instance
- if conf.exists(['vrf']):
- login['radius_vrf'] = conf.return_value(['vrf'])
-
- # Read in all RADIUS servers and store to list
- for server in conf.list_nodes(['server']):
- server_cfg = {
- 'address': server,
- 'disabled': False,
- 'key': '',
- 'port': '1812',
- 'timeout': '2',
- 'priority': 255
- }
- conf.set_level(base_level + ['radius', 'server', server])
-
- # Check if RADIUS server was temporary disabled
- if conf.exists(['disable']):
- server_cfg['disabled'] = True
-
- # RADIUS shared secret
- if conf.exists(['key']):
- server_cfg['key'] = conf.return_value(['key'])
-
- # RADIUS authentication port
- if conf.exists(['port']):
- server_cfg['port'] = conf.return_value(['port'])
-
- # RADIUS session timeout
- if conf.exists(['timeout']):
- server_cfg['timeout'] = conf.return_value(['timeout'])
-
- # Check if RADIUS server has priority
- if conf.exists(['priority']):
- server_cfg['priority'] = int(conf.return_value(['priority']))
-
- # Append individual RADIUS server configuration to global server list
- login['radius_server'].append(server_cfg)
+ base = ['system', 'login']
+ login = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
# users no longer existing in the running configuration need to be deleted
local_users = get_local_users()
- cli_users = [tmp['name'] for tmp in login['add_users']]
- # create a list of all users, cli and users
- all_users = list(set(local_users+cli_users))
+ cli_users = []
+ if 'user' in login:
+ cli_users = list(login['user'])
+
+ # XXX: T2665: we can not safely rely on the defaults() when there are
+ # tagNodes in place, it is better to blend in the defaults manually.
+ default_values = defaults(base + ['user'])
+ for user in login['user']:
+ login['user'][user] = dict_merge(default_values, login['user'][user])
+
+ # XXX: T2665: we can not safely rely on the defaults() when there are
+ # tagNodes in place, it is better to blend in the defaults manually.
+ default_values = defaults(base + ['radius', 'server'])
+ for server in dict_search('radius.server', login) or []:
+ login['radius']['server'][server] = dict_merge(default_values,
+ login['radius']['server'][server])
+
+ # XXX: for a yet unknown reason when we only have one source-address
+ # get_config_dict() will show a string over a string
+ if 'radius' in login and 'source_address' in login['radius']:
+ if isinstance(login['radius']['source_address'], str):
+ login['radius']['source_address'] = [login['radius']['source_address']]
- # Remove any normal users that dos not exist in the current configuration.
- # This can happen if user is added but configuration was not saved and
- # system is rebooted.
- login['del_users'] = [tmp for tmp in all_users if tmp not in cli_users]
+ # create a list of all users, cli and users
+ all_users = list(set(local_users + cli_users))
+ # We will remove any normal users that dos not exist in the current
+ # configuration. This can happen if user is added but configuration was not
+ # saved and system is rebooted.
+ rm_users = [tmp for tmp in all_users if tmp not in cli_users]
+ if rm_users: login.update({'rm_users' : rm_users})
return login
-
def verify(login):
- cur_user = os.environ['SUDO_USER']
- if cur_user in login['del_users']:
- raise ConfigError(
- 'Attempting to delete current user: {}'.format(cur_user))
-
- for user in login['add_users']:
- for key in user['public_keys']:
- if not key['type']:
- raise ConfigError(
- 'SSH public key type missing for "{name}"!'.format(**key))
-
- if not key['key']:
- raise ConfigError(
- 'SSH public key for id "{name}" missing!'.format(**key))
+ if 'rm_users' in login:
+ cur_user = os.environ['SUDO_USER']
+ if cur_user in login['rm_users']:
+ raise ConfigError(f'Attempting to delete current user: {cur_user}')
+
+ if 'user' in login:
+ for user, user_config in login['user'].items():
+ for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
+ if 'type' not in pubkey_options:
+ raise ConfigError(f'Missing type for public-key "{pubkey}"!')
+ if 'key' not in pubkey_options:
+ raise ConfigError(f'Missing key for public-key "{pubkey}"!')
# At lease one RADIUS server must not be disabled
- if len(login['radius_server']) > 0:
+ if 'radius' in login:
+ if 'server' not in login['radius']:
+ raise ConfigError('No RADIUS server defined!')
+
fail = True
- for server in login['radius_server']:
- if not server['disabled']:
+ for server, server_config in dict_search('radius.server', login).items():
+ if 'key' not in server_config:
+ raise ConfigError(f'RADIUS server "{server}" requires key!')
+
+ if 'disabled' not in server_config:
fail = False
+ continue
if fail:
- raise ConfigError('At least one RADIUS server must be active.')
+ raise ConfigError('All RADIUS servers are disabled')
+
+ verify_vrf(login['radius'])
- vrf_name = login['radius_vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if 'source_address' in login['radius']:
+ ipv4_count = 0
+ ipv6_count = 0
+ for address in login['radius']['source_address']:
+ if is_ipv4(address): ipv4_count += 1
+ else: ipv6_count += 1
+
+ if ipv4_count > 1:
+ raise ConfigError('Only one IPv4 source-address can be set!')
+ if ipv6_count > 1:
+ raise ConfigError('Only one IPv6 source-address can be set!')
return None
def generate(login):
# calculate users encrypted password
- for user in login['add_users']:
- if user['password_plaintext']:
- user['password_encrypted'] = crypt(
- user['password_plaintext'], METHOD_SHA512)
- user['password_plaintext'] = ''
-
- # remove old plaintext password and set new encrypted password
- env = os.environ.copy()
- env['vyos_libexec_dir'] = '/usr/libexec/vyos'
-
- call("/opt/vyatta/sbin/my_delete system login user '{name}' "
- "authentication plaintext-password"
- .format(**user), env=env)
-
- call("/opt/vyatta/sbin/my_set system login user '{name}' "
- "authentication encrypted-password '{password_encrypted}'"
- .format(**user), env=env)
-
- else:
- try:
- if getspnam(user['name']).sp_pwdp == user['password_encrypted']:
- # If the current encrypted bassword matches the encrypted password
- # from the config - do not update it. This will remove the encrypted
- # value from the system logs.
- #
- # The encrypted password will be set only once during the first boot
- # after an image upgrade.
- user['password_encrypted'] = ''
- except:
- pass
-
- if len(login['radius_server']) > 0:
- render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl',
- login)
-
- uid = getpwnam('root').pw_uid
- gid = getpwnam('root').pw_gid
- os.chown(radius_config_file, uid, gid)
- chmod_600(radius_config_file)
+ if 'user' in login:
+ for user, user_config in login['user'].items():
+ tmp = dict_search('authentication.plaintext_password', user_config)
+ if tmp:
+ encrypted_password = crypt(tmp, METHOD_SHA512)
+ login['user'][user]['authentication']['encrypted_password'] = encrypted_password
+ del login['user'][user]['authentication']['plaintext_password']
+
+ # remove old plaintext password and set new encrypted password
+ env = os.environ.copy()
+ env['vyos_libexec_dir'] = '/usr/libexec/vyos'
+
+ call(f"/opt/vyatta/sbin/my_delete system login user '{user}' " \
+ f"authentication plaintext-password", env=env)
+
+ call(f"/opt/vyatta/sbin/my_set system login user '{user}' " \
+ f"authentication encrypted-password '{encrypted_password}'", env=env)
+ else:
+ try:
+ if getspnam(user).sp_pwdp == dict_search('authentication.encrypted_password', user_config):
+ # If the current encrypted bassword matches the encrypted password
+ # from the config - do not update it. This will remove the encrypted
+ # value from the system logs.
+ #
+ # The encrypted password will be set only once during the first boot
+ # after an image upgrade.
+ del login['user'][user]['authentication']['encrypted_password']
+ except:
+ pass
+
+ if 'radius' in login:
+ render(radius_config_file, 'login/pam_radius_auth.conf.tmpl', login,
+ permission=0o600, user='root', group='root')
else:
if os.path.isfile(radius_config_file):
os.unlink(radius_config_file)
@@ -270,95 +187,71 @@ def generate(login):
def apply(login):
- for user in login['add_users']:
- # make new user using vyatta shell and make home directory (-m),
- # default group of 100 (users)
- command = "useradd -m -N"
- # check if user already exists:
- if user['name'] in get_local_users():
- # update existing account
- command = "usermod"
-
- # all accounts use /bin/vbash
- command += " -s /bin/vbash"
- # we need to use '' quotes when passing formatted data to the shell
- # else it will not work as some data parts are lost in translation
- if user['password_encrypted']:
- command += " -p '{}'".format(user['password_encrypted'])
-
- if user['full_name']:
- command += " -c '{}'".format(user['full_name'])
-
- if user['home_dir']:
- command += " -d '{}'".format(user['home_dir'])
-
- command += " -G frrvty,vyattacfg,sudo,adm,dip,disk"
- command += " {}".format(user['name'])
-
- try:
- cmd(command)
-
- uid = getpwnam(user['name']).pw_uid
- gid = getpwnam(user['name']).pw_gid
-
- # we should not rely on the value stored in user['home_dir'], as a
- # crazy user will choose username root or any other system user
- # which will fail. Should we deny using root at all?
- home_dir = getpwnam(user['name']).pw_dir
-
- # install ssh keys
- ssh_key_dir = home_dir + '/.ssh'
- if not os.path.isdir(ssh_key_dir):
- os.mkdir(ssh_key_dir)
- os.chown(ssh_key_dir, uid, gid)
- chmod_755(ssh_key_dir)
-
- ssh_key_file = ssh_key_dir + '/authorized_keys'
- with open(ssh_key_file, 'w') as f:
- f.write("# Automatically generated by VyOS\n")
- f.write("# Do not edit, all changes will be lost\n")
-
- for id in user['public_keys']:
- line = ''
- if id['options']:
- line = '{} '.format(id['options'])
-
- line += '{} {} {}\n'.format(id['type'],
- id['key'], id['name'])
- f.write(line)
-
- os.chown(ssh_key_file, uid, gid)
- chmod_600(ssh_key_file)
-
- except Exception as e:
- print(e)
- raise ConfigError('Adding user "{name}" raised exception'
- .format(**user))
-
- for user in login['del_users']:
- try:
- # Logout user if he is logged in
- if user in list(set([tmp[0] for tmp in users()])):
- print('{} is logged in, forcing logout'.format(user))
- call('pkill -HUP -u {}'.format(user))
-
- # Remove user account but leave home directory to be safe
- call(f'userdel -r {user}', stderr=DEVNULL)
-
- except Exception as e:
- raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
+ if 'user' in login:
+ for user, user_config in login['user'].items():
+ # make new user using vyatta shell and make home directory (-m),
+ # default group of 100 (users)
+ command = 'useradd -m -N'
+ # check if user already exists:
+ if user in get_local_users():
+ # update existing account
+ command = 'usermod'
+
+ # all accounts use /bin/vbash
+ command += ' -s /bin/vbash'
+ # we need to use '' quotes when passing formatted data to the shell
+ # else it will not work as some data parts are lost in translation
+ tmp = dict_search('authentication.encrypted_password', user_config)
+ if tmp: command += f" -p '{tmp}'"
+
+ tmp = dict_search('full_name', user_config)
+ if tmp: command += f" -c '{tmp}'"
+
+ tmp = dict_search('home_directory', user_config)
+ if tmp: command += f" -d '{tmp}'"
+ else: command += f" -d '/home/{user}'"
+
+ command += f' -G frrvty,vyattacfg,sudo,adm,dip,disk {user}'
+ try:
+ cmd(command)
+
+ # we should not rely on the value stored in
+ # user_config['home_directory'], as a crazy user will choose
+ # username root or any other system user which will fail.
+ #
+ # 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')
+
+ except Exception as e:
+ raise ConfigError(f'Adding user "{user}" raised exception: "{e}"')
+
+ if 'rm_users' in login:
+ for user in login['rm_users']:
+ try:
+ # Logout user if he is still logged in
+ if user in list(set([tmp[0] for tmp in users()])):
+ print(f'{user} is logged in, forcing logout!')
+ call(f'pkill -HUP -u {user}')
+
+ # Remove user account but leave home directory to be safe
+ call(f'userdel -r {user}', stderr=DEVNULL)
+
+ except Exception as e:
+ raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
#
# RADIUS configuration
#
- if len(login['radius_server']) > 0:
- try:
- env = os.environ.copy()
- env['DEBIAN_FRONTEND'] = 'noninteractive'
+ env = os.environ.copy()
+ env['DEBIAN_FRONTEND'] = 'noninteractive'
+ try:
+ if 'radius' in login:
# Enable RADIUS in PAM
- cmd("pam-auth-update --package --enable radius", env=env)
-
- # Make NSS system aware of RADIUS, too
+ cmd('pam-auth-update --package --enable radius', env=env)
+ # Make NSS system aware of RADIUS
+ # This fancy snipped was copied from old Vyatta code
command = "sed -i -e \'/\smapname/b\' \
-e \'/^passwd:/s/\s\s*/&mapuid /\' \
-e \'/^passwd:.*#/s/#.*/mapname &/\' \
@@ -366,31 +259,20 @@ def apply(login):
-e \'/^group:.*#/s/#.*/ mapname &/\' \
-e \'/^group:[^#]*$/s/: */&mapname /\' \
/etc/nsswitch.conf"
-
- cmd(command)
-
- except Exception as e:
- raise ConfigError('RADIUS configuration failed: {}'.format(e))
-
- else:
- try:
- env = os.environ.copy()
- env['DEBIAN_FRONTEND'] = 'noninteractive'
-
+ else:
# Disable RADIUS in PAM
- cmd("pam-auth-update --package --remove radius", env=env)
-
+ cmd('pam-auth-update --package --remove radius', env=env)
+ # Drop RADIUS from NSS NSS system
+ # This fancy snipped was copied from old Vyatta code
command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \
-e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \
-e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \
-e \'s/[ \t]*$//\' \
/etc/nsswitch.conf"
- cmd(command)
-
- except Exception as e:
- raise ConfigError(
- 'Removing RADIUS configuration failed.\n{}'.format(e))
+ cmd(command)
+ except Exception as e:
+ raise ConfigError(f'RADIUS configuration failed: {e}')
return None
diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py
index 910c14474..454611c55 100755
--- a/src/conf_mode/system-option.py
+++ b/src/conf_mode/system-option.py
@@ -87,10 +87,10 @@ def apply(options):
# Ctrl-Alt-Delete action
if os.path.exists(systemd_action_file):
os.unlink(systemd_action_file)
- if 'ctrl_alt_del' in options:
- if options['ctrl_alt_del'] == 'reboot':
+ if 'ctrl_alt_delete' in options:
+ if options['ctrl_alt_delete'] == 'reboot':
os.symlink('/lib/systemd/system/reboot.target', systemd_action_file)
- elif options['ctrl_alt_del'] == 'poweroff':
+ elif options['ctrl_alt_delete'] == 'poweroff':
os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file)
# Configure HTTP client
@@ -104,11 +104,11 @@ def apply(options):
os.unlink(ssh_config)
# Reboot system on kernel panic
+ timeout = '0'
+ if 'reboot_on_panic' in options:
+ timeout = '60'
with open('/proc/sys/kernel/panic', 'w') as f:
- if 'reboot_on_panic' in options:
- f.write('60')
- else:
- f.write('0')
+ f.write(timeout)
# tuned - performance tuning
if 'performance' in options:
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index b17818797..33a546bd3 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -17,9 +17,8 @@
import os
import re
-from fileinput import input as replace_in_file
from vyos.config import Config
-from vyos.util import call
+from vyos.util import call, read_file, write_file
from vyos.template import render
from vyos import ConfigError, airbag
airbag.enable()
@@ -98,15 +97,27 @@ def generate(console):
if not os.path.isfile(grub_config):
return None
- # stdin/stdout are redirected in replace_in_file(), thus print() is fine
+ lines = read_file(grub_config).split('\n')
+
p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$')
- for line in replace_in_file(grub_config, inplace=True):
+ write = False
+ newlines = []
+ for line in lines:
if line.startswith('serial --unit'):
- line = f'serial --unit=0 --speed={speed}\n'
+ newline = f'serial --unit=0 --speed={speed}'
elif p.match(line):
- line = '{},{}{}\n'.format(p.search(line)[1], speed, p.search(line)[2])
+ newline = '{},{}{}'.format(p.search(line)[1], speed, p.search(line)[2])
+ else:
+ newline = line
+
+ if newline != line:
+ write = True
+
+ newlines.append(newline)
+ newlines.append('')
- print(line, end='')
+ if write:
+ write_file(grub_config, '\n'.join(newlines))
return None
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index c4ba859b7..414e514c5 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.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
@@ -17,32 +17,22 @@
import os
from sys import exit
-from copy import deepcopy
from json import loads
from vyos.config import Config
-from vyos.configdict import list_diff
+from vyos.configdict import node_changed
from vyos.ifconfig import Interface
-from vyos.util import read_file, cmd
-from vyos import ConfigError
from vyos.template import render
-
+from vyos.util import call
+from vyos.util import cmd
+from vyos.util import dict_search
+from vyos.util import get_interface_config
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf'
-default_config_data = {
- 'bind_to_all': '0',
- 'deleted': False,
- 'vrf_add': [],
- 'vrf_existing': [],
- 'vrf_remove': []
-}
-
-def _cmd(command):
- cmd(command, raising=ConfigError, message='Error changing VRF')
-
def list_rules():
command = 'ip -j -4 rule show'
answer = loads(cmd(command))
@@ -81,112 +71,61 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- vrf_config = deepcopy(default_config_data)
- cfg_base = ['vrf']
- if not conf.exists(cfg_base):
- # get all currently effetive VRFs and mark them for deletion
- vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name'])
- else:
- # set configuration level base
- conf.set_level(cfg_base)
-
- # Should services be allowed to bind to all VRFs?
- if conf.exists(['bind-to-all']):
- vrf_config['bind_to_all'] = '1'
-
- # Determine vrf interfaces (currently effective) - to determine which
- # vrf interface is no longer present and needs to be removed
- eff_vrf = conf.list_effective_nodes(['name'])
- act_vrf = conf.list_nodes(['name'])
- vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf)
-
- # read in individual VRF definition and build up
- # configuration
- for name in conf.list_nodes(['name']):
- vrf_inst = {
- 'description' : '',
- 'members': [],
- 'name' : name,
- 'table' : '',
- 'table_mod': False
- }
- conf.set_level(cfg_base + ['name', name])
-
- if conf.exists(['table']):
- # VRF table can't be changed on demand, thus we need to read in the
- # current and the effective routing table number
- act_table = conf.return_value(['table'])
- eff_table = conf.return_effective_value(['table'])
- vrf_inst['table'] = act_table
- if eff_table and eff_table != act_table:
- vrf_inst['table_mod'] = True
-
- if conf.exists(['description']):
- vrf_inst['description'] = conf.return_value(['description'])
-
- # append individual VRF configuration to global configuration list
- vrf_config['vrf_add'].append(vrf_inst)
-
- # set configuration level base
- conf.set_level(cfg_base)
-
- # check VRFs which need to be removed as they are not allowed to have
- # interfaces attached
- tmp = []
- for name in vrf_config['vrf_remove']:
- vrf_inst = {
- 'interfaces': [],
- 'name': name,
- 'routes': []
- }
-
- # find member interfaces of this particulat VRF
- vrf_inst['interfaces'] = vrf_interfaces(conf, name)
-
- # find routing protocols used by this VRF
- vrf_inst['routes'] = vrf_routing(conf, name)
-
- # append individual VRF configuration to temporary configuration list
- tmp.append(vrf_inst)
-
- # replace values in vrf_remove with list of dictionaries
- # as we need it in verify() - we can't delete a VRF with members attached
- vrf_config['vrf_remove'] = tmp
- return vrf_config
-
-def verify(vrf_config):
- # ensure VRF is not assigned to any interface
- for vrf in vrf_config['vrf_remove']:
- if len(vrf['interfaces']) > 0:
- raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!")
+ base = ['vrf']
+ vrf = conf.get_config_dict(base, get_first_key=True)
- if len(vrf['routes']) > 0:
- raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!")
+ # determine which VRF has been removed
+ for name in node_changed(conf, base + ['name']):
+ if 'vrf_remove' not in vrf:
+ vrf.update({'vrf_remove' : {}})
- table_ids = []
- for vrf in vrf_config['vrf_add']:
- # table id is mandatory
- if not vrf['table']:
- raise ConfigError(f"VRF {vrf['name']} table id is mandatory!")
+ vrf['vrf_remove'][name] = {}
+ # get VRF bound interfaces
+ interfaces = vrf_interfaces(conf, name)
+ if interfaces: vrf['vrf_remove'][name]['interface'] = interfaces
+ # get VRF bound routing instances
+ routes = vrf_routing(conf, name)
+ if routes: vrf['vrf_remove'][name]['route'] = routes
- # routing table id can't be changed - OS restriction
- if vrf['table_mod']:
- raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!")
+ return vrf
- # VRf routing table ID must be unique on the system
- if vrf['table'] in table_ids:
- raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!")
-
- table_ids.append(vrf['table'])
+def verify(vrf):
+ # ensure VRF is not assigned to any interface
+ if 'vrf_remove' in vrf:
+ for name, config in vrf['vrf_remove'].items():
+ if 'interface' in config:
+ raise ConfigError(f'Can not remove VRF "{name}", it still has '\
+ f'member interfaces!')
+ if 'route' in config:
+ raise ConfigError(f'Can not remove VRF "{name}", it still has '\
+ f'static routes installed!')
+
+ if 'name' in vrf:
+ table_ids = []
+ for name, config in vrf['name'].items():
+ # table id is mandatory
+ if 'table' not in config:
+ raise ConfigError(f'VRF "{name}" table id is mandatory!')
+
+ # routing table id can't be changed - OS restriction
+ if os.path.isdir(f'/sys/class/net/{name}'):
+ tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name)))
+ if tmp and tmp != config['table']:
+ raise ConfigError(f'VRF "{name}" table id modification not possible!')
+
+ # VRf routing table ID must be unique on the system
+ if config['table'] in table_ids:
+ raise ConfigError(f'VRF "{name}" table id is not unique!')
+ table_ids.append(config['table'])
return None
-def generate(vrf_config):
- render(config_file, 'vrf/vrf.conf.tmpl', vrf_config)
+def generate(vrf):
+ render(config_file, 'vrf/vrf.conf.tmpl', vrf)
return None
-def apply(vrf_config):
+def apply(vrf):
# Documentation
#
# - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt
@@ -196,40 +135,48 @@ def apply(vrf_config):
# - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf
# set the default VRF global behaviour
- bind_all = vrf_config['bind_to_all']
- if read_file('/proc/sys/net/ipv4/tcp_l3mdev_accept') != bind_all:
- _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}')
- _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}')
-
- for vrf in vrf_config['vrf_remove']:
- name = vrf['name']
- if os.path.isdir(f'/sys/class/net/{name}'):
- _cmd(f'ip -4 route del vrf {name} unreachable default metric 4278198272')
- _cmd(f'ip -6 route del vrf {name} unreachable default metric 4278198272')
- _cmd(f'ip link delete dev {name}')
-
- for vrf in vrf_config['vrf_add']:
- name = vrf['name']
- table = vrf['table']
-
- if not os.path.isdir(f'/sys/class/net/{name}'):
- # For each VRF apart from your default context create a VRF
- # interface with a separate routing table
- _cmd(f'ip link add {name} type vrf table {table}')
- # Start VRf
- _cmd(f'ip link set dev {name} up')
- # The kernel Documentation/networking/vrf.txt also recommends
- # adding unreachable routes to the VRF routing tables so that routes
- # afterwards are taken.
- _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
- _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
- # We also should add proper loopback IP addresses to the newly
- # created VRFs for services bound to the loopback address (SNMP, NTP)
- _cmd(f'ip -4 addr add 127.0.0.1/8 dev {name}')
- _cmd(f'ip -6 addr add ::1/128 dev {name}')
-
- # set VRF description for e.g. SNMP monitoring
- Interface(name).set_alias(vrf['description'])
+ bind_all = '0'
+ if 'bind_to_all' in vrf:
+ bind_all = '1'
+ call(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}')
+ call(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}')
+
+ for tmp in (dict_search('vrf_remove', vrf) or []):
+ if os.path.isdir(f'/sys/class/net/{tmp}'):
+ call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272')
+ call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272')
+ call(f'ip link delete dev {tmp}')
+
+ if 'name' in vrf:
+ for name, config in vrf['name'].items():
+ table = config['table']
+
+ if not os.path.isdir(f'/sys/class/net/{name}'):
+ # For each VRF apart from your default context create a VRF
+ # interface with a separate routing table
+ call(f'ip link add {name} type vrf table {table}')
+ # The kernel Documentation/networking/vrf.txt also recommends
+ # adding unreachable routes to the VRF routing tables so that routes
+ # afterwards are taken.
+ call(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
+ call(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
+ # We also should add proper loopback IP addresses to the newly
+ # created VRFs for services bound to the loopback address (SNMP, NTP)
+ call(f'ip -4 addr add 127.0.0.1/8 dev {name}')
+ call(f'ip -6 addr add ::1/128 dev {name}')
+
+ # set VRF description for e.g. SNMP monitoring
+ vrf_if = Interface(name)
+ vrf_if.set_alias(config.get('description', ''))
+ # Enable/Disable of an interface must always be done at the end of the
+ # derived class to make use of the ref-counting set_admin_state()
+ # function. We will only enable the interface if 'up' was called as
+ # often as 'down'. This is required by some interface implementations
+ # as certain parameters can only be changed when the interface is
+ # in admin-down state. This ensures the link does not flap during
+ # reconfiguration.
+ state = 'down' if 'disable' in config else 'up'
+ vrf_if.set_admin_state(state)
# Linux routing uses rules to find tables - routing targets are then
# looked up in those tables. If the lookup got a matching route, the
@@ -248,20 +195,20 @@ def apply(vrf_config):
local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0]
# change preference when VRFs are enabled and local lookup table is default
- if not local_pref and vrf_config['vrf_add']:
+ if not local_pref and 'name' in vrf:
for af in ['-4', '-6']:
- _cmd(f'ip {af} rule add pref 32765 table local')
- _cmd(f'ip {af} rule del pref 0')
+ call(f'ip {af} rule add pref 32765 table local')
+ call(f'ip {af} rule del pref 0')
# return to default lookup preference when no VRF is configured
- if not vrf_config['vrf_add']:
+ if 'name' not in vrf:
for af in ['-4', '-6']:
- _cmd(f'ip {af} rule add pref 0 table local')
- _cmd(f'ip {af} rule del pref 32765')
+ call(f'ip {af} rule add pref 0 table local')
+ call(f'ip {af} rule del pref 32765')
# clean out l3mdev-table rule if present
if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
- _cmd(f'ip {af} rule del pref 1000')
+ call(f'ip {af} rule del pref 1000')
return None
diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py
index 4510dd3e7..680a80859 100755
--- a/src/conf_mode/vrrp.py
+++ b/src/conf_mode/vrrp.py
@@ -75,6 +75,7 @@ def get_config(config=None):
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
@@ -183,6 +184,11 @@ def verify(data):
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