summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/host_name.py49
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py91
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py50
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py12
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py34
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py39
-rwxr-xr-xsrc/conf_mode/protocols_isis.py258
-rwxr-xr-xsrc/conf_mode/protocols_rip.py3
-rwxr-xr-xsrc/conf_mode/system-login.py6
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py67
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py4
11 files changed, 318 insertions, 295 deletions
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index f4c75c257..a7135911d 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,10 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-"""
-conf-mode script for 'system host-name' and 'system domain-name'.
-"""
-
import re
import sys
import copy
@@ -25,10 +21,13 @@ import copy
import vyos.util
import vyos.hostsd_client
-from vyos.config import Config
from vyos import ConfigError
-from vyos.util import cmd, call, process_named_running
-
+from vyos.config import Config
+from vyos.ifconfig import Section
+from vyos.template import is_ip
+from vyos.util import cmd
+from vyos.util import call
+from vyos.util import process_named_running
from vyos import airbag
airbag.enable()
@@ -37,7 +36,7 @@ default_config_data = {
'domain_name': '',
'domain_search': [],
'nameserver': [],
- 'nameservers_dhcp_interfaces': [],
+ 'nameservers_dhcp_interfaces': {},
'static_host_mapping': {}
}
@@ -51,29 +50,37 @@ def get_config(config=None):
hosts = copy.deepcopy(default_config_data)
- hosts['hostname'] = conf.return_value("system host-name")
+ hosts['hostname'] = conf.return_value(['system', 'host-name'])
# This may happen if the config is not loaded yet,
# e.g. if run by cloud-init
if not hosts['hostname']:
hosts['hostname'] = default_config_data['hostname']
- if conf.exists("system domain-name"):
- hosts['domain_name'] = conf.return_value("system domain-name")
+ if conf.exists(['system', 'domain-name']):
+ hosts['domain_name'] = conf.return_value(['system', 'domain-name'])
hosts['domain_search'].append(hosts['domain_name'])
- for search in conf.return_values("system domain-search domain"):
+ for search in conf.return_values(['system', 'domain-search', 'domain']):
hosts['domain_search'].append(search)
- hosts['nameserver'] = conf.return_values("system name-server")
+ if conf.exists(['system', 'name-server']):
+ for ns in conf.return_values(['system', 'name-server']):
+ if is_ip(ns):
+ hosts['nameserver'].append(ns)
+ else:
+ tmp = ''
+ if_type = Section.section(ns)
+ if conf.exists(['interfaces', if_type, ns, 'address']):
+ tmp = conf.return_values(['interfaces', if_type, ns, 'address'])
- hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp")
+ hosts['nameservers_dhcp_interfaces'].update({ ns : tmp })
# system static-host-mapping
- for hn in conf.list_nodes('system static-host-mapping host-name'):
+ for hn in conf.list_nodes(['system', 'static-host-mapping', 'host-name']):
hosts['static_host_mapping'][hn] = {}
- hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet')
- hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias')
+ hosts['static_host_mapping'][hn]['address'] = conf.return_value(['system', 'static-host-mapping', 'host-name', hn, 'inet'])
+ hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(['system', 'static-host-mapping', 'host-name', hn, 'alias'])
return hosts
@@ -103,8 +110,10 @@ def verify(hosts):
if not hostname_regex.match(a) and len(a) != 0:
raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"')
- # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't
- # exist or doesn't have address dhcp(v6)
+ for interface, interface_config in hosts['nameservers_dhcp_interfaces'].items():
+ # Warnin user if interface does not have DHCP or DHCPv6 configured
+ if not set(interface_config).intersection(['dhcp', 'dhcpv6']):
+ print(f'WARNING: "{interface}" is not a DHCP interface but uses DHCP name-server option!')
return None
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 5b0046a72..4d3ebc587 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -18,7 +18,6 @@ import os
from sys import exit
from netifaces import interfaces
-import re
from vyos.config import Config
from vyos.configdict import get_interface_dict
@@ -41,26 +40,6 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-def helper_check_removed_vlan(conf,bridge,key,key_mangling):
- key_update = re.sub(key_mangling[0], key_mangling[1], key)
- if dict_search('member.interface', bridge):
- for interface in bridge['member']['interface']:
- tmp = leaf_node_changed(conf, ['member', 'interface',interface,key])
- if tmp:
- if 'member' in bridge:
- if 'interface' in bridge['member']:
- if interface in bridge['member']['interface']:
- bridge['member']['interface'][interface].update({f'{key_update}_removed': tmp })
- else:
- bridge['member']['interface'].update({interface: {f'{key_update}_removed': tmp }})
- else:
- bridge['member'].update({ 'interface': {interface: {f'{key_update}_removed': tmp }}})
- else:
- bridge.update({'member': { 'interface': {interface: {f'{key_update}_removed': tmp }}}})
-
- return bridge
-
-
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -80,12 +59,6 @@ def get_config(config=None):
bridge['member'].update({'interface_remove': tmp })
else:
bridge.update({'member': {'interface_remove': tmp }})
-
-
- # determine which members vlan have been removed
-
- bridge = helper_check_removed_vlan(conf,bridge,'native-vlan',('-', '_'))
- bridge = helper_check_removed_vlan(conf,bridge,'allowed-vlan',('-', '_'))
if dict_search('member.interface', bridge):
# XXX: T2665: we need a copy of the dict keys for iteration, else we will get:
@@ -99,7 +72,6 @@ def get_config(config=None):
# the default dictionary is not properly paged into the dict (see T2665)
# thus we will ammend it ourself
default_member_values = defaults(base + ['member', 'interface'])
- vlan_aware = False
for interface,interface_config in bridge['member']['interface'].items():
bridge['member']['interface'][interface] = dict_merge(
default_member_values, bridge['member']['interface'][interface])
@@ -120,19 +92,11 @@ def get_config(config=None):
# Bridge members must not have an assigned address
tmp = has_address_configured(conf, interface)
if tmp: bridge['member']['interface'][interface].update({'has_address' : ''})
-
+
# VLAN-aware bridge members must not have VLAN interface configuration
- if 'native_vlan' in interface_config:
- vlan_aware = True
-
- if 'allowed_vlan' in interface_config:
- vlan_aware = True
-
-
- if vlan_aware:
- tmp = has_vlan_subinterface_configured(conf,interface)
- if tmp:
- if tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''})
+ tmp = has_vlan_subinterface_configured(conf,interface)
+ if 'enable_vlan' in bridge and tmp:
+ bridge['member']['interface'][interface].update({'has_vlan' : ''})
return bridge
@@ -142,8 +106,8 @@ def verify(bridge):
verify_dhcpv6(bridge)
verify_vrf(bridge)
-
- vlan_aware = False
+
+ ifname = bridge['ifname']
if dict_search('member.interface', bridge):
for interface, interface_config in bridge['member']['interface'].items():
@@ -166,31 +130,24 @@ def verify(bridge):
if 'has_address' in interface_config:
raise ConfigError(error_msg + 'it has an address assigned!')
-
- if 'has_vlan' in interface_config:
- raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
-
- # VLAN-aware bridge members must not have VLAN interface configuration
- if 'native_vlan' in interface_config:
- vlan_aware = True
-
- if 'allowed_vlan' in interface_config:
- vlan_aware = True
-
- if vlan_aware and 'wlan' in interface:
- raise ConfigError(error_msg + 'VLAN aware cannot be set!')
-
- if 'allowed_vlan' in interface_config:
- for vlan in interface_config['allowed_vlan']:
- if re.search('[0-9]{1,4}-[0-9]{1,4}', vlan):
- vlan_range = vlan.split('-')
- if int(vlan_range[0]) <1 and int(vlan_range[0])>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
- if int(vlan_range[1]) <1 and int(vlan_range[1])>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
- else:
- if int(vlan) <1 and int(vlan)>4094:
- raise ConfigError('VLAN ID must be between 1 and 4094')
+
+ if 'enable_vlan' in bridge:
+ if 'has_vlan' in interface_config:
+ raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!')
+
+ if 'wlan' in interface:
+ raise ConfigError(error_msg + 'VLAN aware cannot be set!')
+ else:
+ for option in ['allowed_vlan', 'native_vlan']:
+ if option in interface_config:
+ raise ConfigError('Can not use VLAN options on non VLAN aware bridge')
+
+ if 'enable_vlan' in bridge:
+ if dict_search('vif.1', bridge):
+ raise ConfigError(f'VLAN 1 sub interface cannot be set for VLAN aware bridge {ifname}, and VLAN 1 is always the parent interface')
+ else:
+ if dict_search('vif', bridge):
+ raise ConfigError(f'You must first activate "enable-vlan" of {ifname} bridge to use "vif"')
return None
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 349b0e7a3..de851262b 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -62,12 +62,6 @@ def verify(ethernet):
ifname = ethernet['ifname']
verify_interface_exists(ifname)
-
- # No need to check speed and duplex keys as both have default values.
- if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
- (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
- raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
-
verify_mtu(ethernet)
verify_mtu_ipv6(ethernet)
verify_dhcpv6(ethernet)
@@ -76,25 +70,31 @@ def verify(ethernet):
verify_eapol(ethernet)
verify_mirror(ethernet)
- # verify offloading capabilities
- if dict_search('offload.rps', ethernet) != None:
- if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
- raise ConfigError('Interface does not suport RPS!')
+ ethtool = Ethtool(ifname)
+ # No need to check speed and duplex keys as both have default values.
+ if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
+ (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
+ raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
- driver = EthernetIf(ifname).get_driver_name()
- # T3342 - Xen driver requires special treatment
- if driver == 'vif':
- if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
- raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
- 'for MTU size larger then 1500 bytes')
+ if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
+ # We need to verify if the requested speed and duplex setting is
+ # supported by the underlaying NIC.
+ speed = ethernet['speed']
+ duplex = ethernet['duplex']
+ if not ethtool.check_speed_duplex(speed, duplex):
+ raise ConfigError(f'Adapter does not support changing speed and duplex '\
+ f'settings to: {speed}/{duplex}!')
+
+ if 'disable_flow_control' in ethernet:
+ if not ethtool.check_flow_control():
+ raise ConfigError('Adapter does not support changing flow-control settings!')
- ethtool = Ethtool(ifname)
if 'ring_buffer' in ethernet:
- max_rx = ethtool.get_rx_buffer()
+ max_rx = ethtool.get_ring_buffer_max('rx')
if not max_rx:
raise ConfigError('Driver does not support RX ring-buffer configuration!')
- max_tx = ethtool.get_tx_buffer()
+ max_tx = ethtool.get_ring_buffer_max('tx')
if not max_tx:
raise ConfigError('Driver does not support TX ring-buffer configuration!')
@@ -108,6 +108,18 @@ def verify(ethernet):
raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\
f'size of "{max_tx}" bytes!')
+ # verify offloading capabilities
+ if dict_search('offload.rps', ethernet) != None:
+ if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
+ raise ConfigError('Interface does not suport RPS!')
+
+ driver = ethtool.get_driver_name()
+ # T3342 - Xen driver requires special treatment
+ if driver == 'vif':
+ if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
+ raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
+ 'for MTU size larger then 1500 bytes')
+
if {'is_bond_member', 'mac'} <= set(ethernet):
print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" '
f'is a member of bond "{is_bond_member}"'.format(**ethernet))
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 0a420f7bf..c3620d690 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -40,6 +40,7 @@ from vyos.util import call
from vyos.util import chown
from vyos.util import chmod_600
from vyos.util import dict_search
+from vyos.util import makedir
from vyos.validate import is_addr_assigned
from vyos import ConfigError
@@ -79,9 +80,6 @@ def get_config(config=None):
openvpn = get_interface_dict(conf, base)
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
- openvpn['daemon_user'] = user
- openvpn['daemon_group'] = group
-
return openvpn
def verify(openvpn):
@@ -425,6 +423,10 @@ def verify(openvpn):
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
+ # create base config directory on demand
+ makedir(directory, user, group)
+ # enforce proper permissions on /run/openvpn
+ chown(directory, user, group)
# we can't know in advance which clients have been removed,
# thus all client configs will be removed and re-added on demand
@@ -436,9 +438,7 @@ def generate(openvpn):
return None
# create client config directory on demand
- if not os.path.exists(ccd_dir):
- os.makedirs(ccd_dir, 0o755)
- chown(ccd_dir, user, group)
+ makedir(ccd_dir, user, group)
# Fix file permissons for keys
fix_permissions = []
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index e5958e9ae..22a9f0e18 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -18,6 +18,7 @@ import os
from sys import exit
from netifaces import interfaces
+from ipaddress import IPv4Address
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -31,6 +32,7 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vrf
from vyos.configverify import verify_tunnel
from vyos.ifconfig import Interface
+from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -74,6 +76,38 @@ def verify(tunnel):
verify_tunnel(tunnel)
+ # If tunnel source address any and key not set
+ if tunnel['encapsulation'] in ['gre'] and \
+ tunnel['source_address'] == '0.0.0.0' and \
+ dict_search('parameters.ip.key', tunnel) == None:
+ raise ConfigError('Tunnel parameters ip key must be set!')
+
+ if tunnel['encapsulation'] in ['gre', 'gretap']:
+ if dict_search('parameters.ip.key', tunnel) != None:
+ # Check pairs tunnel source-address/encapsulation/key with exists tunnels.
+ # Prevent the same key for 2 tunnels with same source-address/encap. T2920
+ for tunnel_if in Section.interfaces('tunnel'):
+ tunnel_cfg = get_interface_config(tunnel_if)
+ exist_encap = tunnel_cfg['linkinfo']['info_kind']
+ exist_source_address = tunnel_cfg['address']
+ exist_key = tunnel_cfg['linkinfo']['info_data']['ikey']
+ new_source_address = tunnel['source_address']
+ # Convert tunnel key to ip key, format "ip -j link show"
+ # 1 => 0.0.0.1, 999 => 0.0.3.231
+ orig_new_key = int(tunnel['parameters']['ip']['key'])
+ new_key = IPv4Address(orig_new_key)
+ new_key = str(new_key)
+ if tunnel['encapsulation'] == exist_encap and \
+ new_source_address == exist_source_address and \
+ new_key == exist_key:
+ raise ConfigError(f'Key "{orig_new_key}" for source-address "{new_source_address}" ' \
+ f'is already used for tunnel "{tunnel_if}"!')
+
+ # Keys are not allowed with ipip and sit tunnels
+ if tunnel['encapsulation'] in ['ipip', 'sit']:
+ if dict_search('parameters.ip.key', tunnel) != None:
+ raise ConfigError('Keys are not allowed with ipip and sit tunnels!')
+
verify_mtu_ipv6(tunnel)
verify_address(tunnel)
verify_vrf(tunnel)
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 7ca2d9b44..771f635a0 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -18,7 +18,9 @@ import re
import os
from time import sleep
-from sys import exit
+
+# Top level import so that configd can override it
+from sys import argv
from vyos.config import Config
from vyos import ConfigError
@@ -216,6 +218,20 @@ def generate(data):
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file)
remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file)
+def is_charon_responsive():
+ # Check if charon responds to strokes
+ #
+ # Sometimes it takes time to fully initialize,
+ # so waiting for the process to come to live isn't always enough
+ #
+ # There's no official "no-op" stroke so we use the "memusage" stroke as a substitute
+ from os import system
+ res = system("ipsec stroke memusage >&/dev/null")
+ if res == 0:
+ return True
+ else:
+ return False
+
def restart_ipsec():
try:
# Restart the IPsec daemon when it's running.
@@ -223,17 +239,28 @@ def restart_ipsec():
# there's a chance that this script will run before charon is up,
# so we can't assume it's running and have to check and wait if needed.
- # First, wait for charon to get started by the old ipsec.pl script.
+ # But before everything else, there's a catch!
+ # This script is run from _two_ places: "vpn ipsec options" and the top level "vpn" node
+ # When IPsec isn't set up yet, and a user wants to commit an IPsec config with some
+ # "vpn ipsec settings", this script will first be called before StrongSWAN is started by vpn-config.pl!
+ # Thus if this script is run from "settings" _and_ charon is unresponsive,
+ # we shouldn't wait for it, else there will be a deadlock.
+ # We indicate that by running the script under vyshim from "vpn ipsec options" (which sets a variable named "argv")
+ # and running it without configd from "vpn ipsec"
+ if "from-options" in argv:
+ if not is_charon_responsive():
+ return
+
+ # If we got this far, then we actually need to restart StrongSWAN
+
+ # First, wait for charon to get started by the old vpn-config.pl script.
from time import sleep, time
from os import system
now = time()
while True:
if (time() - now) > 60:
raise OSError("Timeout waiting for the IPsec process to become responsive")
- # There's no oficial "no-op" stroke,
- # so we use memusage to check if charon is alive and responsive
- res = system("ipsec stroke memusage >&/dev/null")
- if res == 0:
+ if is_charon_responsive():
break
sleep(5)
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index da91f3b11..0c179b724 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 VyOS maintainers and contributors
+# Copyright (C) 2020-2021 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -19,12 +19,16 @@ import os
from sys import exit
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.configdict import node_changed
-from vyos import ConfigError
-from vyos.util import call
+from vyos.configverify import verify_common_route_maps
+from vyos.configverify import verify_interface_exists
+from vyos.ifconfig import Interface
from vyos.util import dict_search
-from vyos.template import render
+from vyos.util import get_interface_config
from vyos.template import render_to_string
+from vyos.xml import defaults
+from vyos import ConfigError
from vyos import frr
from vyos import airbag
airbag.enable()
@@ -34,126 +38,172 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'isis']
- isis = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ base = ['protocols', 'isis']
+ isis = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ isis['interface_removed'] = list(interfaces_removed)
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ isis.update({'deleted' : ''})
+ return isis
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ # XXX: Note that we can not call defaults(base), as defaults does not work
+ # on an instance of a tag node.
+ default_values = defaults(base)
+ # merge in default values
+ isis = dict_merge(default_values, isis)
+
+ # We also need some additional information from the config, prefix-lists
+ # and route-maps for instance. They will be used in verify().
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = conf.get_config_dict(['policy'])
+ # Merge policy dict into "regular" config dict
+ isis = dict_merge(tmp, isis)
return isis
def verify(isis):
# bail out early - looks like removal from running config
- if not isis:
+ if not isis or 'deleted' in isis:
return None
- for process, isis_config in isis.items():
- # If more then one isis process is defined (Frr only supports one)
- # http://docs.frrouting.org/en/latest/isisd.html#isis-router
- if len(isis) > 1:
- raise ConfigError('Only one isis process can be defined')
-
- # If network entity title (net) not defined
- if 'net' not in isis_config:
- raise ConfigError('ISIS net format iso is mandatory!')
-
- # If interface not set
- if 'interface' not in isis_config:
- raise ConfigError('ISIS interface is mandatory!')
-
- # If md5 and plaintext-password set at the same time
- if 'area_password' in isis_config:
- if {'md5', 'plaintext_password'} <= set(isis_config['encryption']):
- raise ConfigError('Can not use both md5 and plaintext-password for ISIS area-password!')
-
- # If one param from delay set, but not set others
- if 'spf_delay_ietf' in isis_config:
- required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn']
- exist_timers = []
- for elm_timer in required_timers:
- if elm_timer in isis_config['spf_delay_ietf']:
- exist_timers.append(elm_timer)
-
- exist_timers = set(required_timers).difference(set(exist_timers))
- if len(exist_timers) > 0:
- raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-'))
-
- # If Redistribute set, but level don't set
- if 'redistribute' in isis_config:
- proc_level = isis_config.get('level','').replace('-','_')
- for proto, proto_config in isis_config.get('redistribute', {}).get('ipv4', {}).items():
+ if 'net' not in isis:
+ raise ConfigError('Network entity is mandatory!')
+
+ # last byte in IS-IS area address must be 0
+ tmp = isis['net'].split('.')
+ if int(tmp[-1]) != 0:
+ raise ConfigError('Last byte of IS-IS network entity title must always be 0!')
+
+ verify_common_route_maps(isis)
+
+ # If interface not set
+ if 'interface' not in isis:
+ raise ConfigError('Interface used for routing updates is mandatory!')
+
+ for interface in isis['interface']:
+ verify_interface_exists(interface)
+ # Interface MTU must be >= configured lsp-mtu
+ mtu = Interface(interface).get_mtu()
+ area_mtu = isis['lsp_mtu']
+ # Recommended maximum PDU size = interface MTU - 3 bytes
+ recom_area_mtu = mtu - 3
+ if mtu < int(area_mtu) or int(area_mtu) > recom_area_mtu:
+ raise ConfigError(f'Interface {interface} has MTU {mtu}, ' \
+ f'current area MTU is {area_mtu}! \n' \
+ f'Recommended area lsp-mtu {recom_area_mtu} or less ' \
+ '(calculated on MTU size).')
+
+ # If md5 and plaintext-password set at the same time
+ for password in ['area_password', 'domain_password']:
+ if password in isis:
+ if {'md5', 'plaintext_password'} <= set(isis[password]):
+ tmp = password.replace('_', '-')
+ raise ConfigError(f'Can use either md5 or plaintext-password for {tmp}!')
+
+ # If one param from delay set, but not set others
+ if 'spf_delay_ietf' in isis:
+ required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn']
+ exist_timers = []
+ for elm_timer in required_timers:
+ if elm_timer in isis['spf_delay_ietf']:
+ exist_timers.append(elm_timer)
+
+ exist_timers = set(required_timers).difference(set(exist_timers))
+ if len(exist_timers) > 0:
+ raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-'))
+
+ # If Redistribute set, but level don't set
+ if 'redistribute' in isis:
+ proc_level = isis.get('level','').replace('-','_')
+ for afi in ['ipv4', 'ipv6']:
+ if afi not in isis['redistribute']:
+ continue
+
+ for proto, proto_config in isis['redistribute'][afi].items():
if 'level_1' not in proto_config and 'level_2' not in proto_config:
- raise ConfigError('Redistribute level-1 or level-2 should be specified in \"protocols isis {} redistribute ipv4 {}\"'.format(process, proto))
- for redistribute_level in proto_config.keys():
- if proc_level and proc_level != 'level_1_2' and proc_level != redistribute_level:
- raise ConfigError('\"protocols isis {0} redistribute ipv4 {2} {3}\" cannot be used with \"protocols isis {0} level {1}\"'.format(process, proc_level, proto, redistribute_level))
-
- # Segment routing checks
- if dict_search('segment_routing', isis_config):
- if dict_search('segment_routing.global_block', isis_config):
- high_label_value = dict_search('segment_routing.global_block.high_label_value', isis_config)
- low_label_value = dict_search('segment_routing.global_block.low_label_value', isis_config)
- # If segment routing global block high value is blank, throw error
- if low_label_value and not high_label_value:
- raise ConfigError('Segment routing global block high value must not be left blank')
- # If segment routing global block low value is blank, throw error
- if high_label_value and not low_label_value:
- raise ConfigError('Segment routing global block low value must not be left blank')
- # If segment routing global block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing global block low value must be lower than high value')
-
- if dict_search('segment_routing.local_block', isis_config):
- high_label_value = dict_search('segment_routing.local_block.high_label_value', isis_config)
- low_label_value = dict_search('segment_routing.local_block.low_label_value', isis_config)
- # If segment routing local block high value is blank, throw error
- if low_label_value and not high_label_value:
- raise ConfigError('Segment routing local block high value must not be left blank')
- # If segment routing local block low value is blank, throw error
- if high_label_value and not low_label_value:
- raise ConfigError('Segment routing local block low value must not be left blank')
- # If segment routing local block low value is higher than the high value, throw error
- if int(low_label_value) > int(high_label_value):
- raise ConfigError('Segment routing local block low value must be lower than high value')
+ raise ConfigError(f'Redistribute level-1 or level-2 should be specified in ' \
+ f'"protocols isis {process} redistribute {afi} {proto}"!')
+
+ for redistr_level, redistr_config in proto_config.items():
+ if proc_level and proc_level != 'level_1_2' and proc_level != redistr_level:
+ raise ConfigError(f'"protocols isis {process} redistribute {afi} {proto} {redistr_level}" ' \
+ f'can not be used with \"protocols isis {process} level {proc_level}\"')
+
+ # Segment routing checks
+ if dict_search('segment_routing.global_block', isis):
+ high_label_value = dict_search('segment_routing.global_block.high_label_value', isis)
+ low_label_value = dict_search('segment_routing.global_block.low_label_value', isis)
+
+ # If segment routing global block high value is blank, throw error
+ if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
+ raise ConfigError('Segment routing global block requires both low and high value!')
+
+ # If segment routing global block low value is higher than the high value, throw error
+ if int(low_label_value) > int(high_label_value):
+ raise ConfigError('Segment routing global block low value must be lower than high value')
+
+ if dict_search('segment_routing.local_block', isis):
+ high_label_value = dict_search('segment_routing.local_block.high_label_value', isis)
+ low_label_value = dict_search('segment_routing.local_block.low_label_value', isis)
+
+ # If segment routing local block high value is blank, throw error
+ if (low_label_value and not high_label_value) or (high_label_value and not low_label_value):
+ raise ConfigError('Segment routing local block requires both high and low value!')
+
+ # If segment routing local block low value is higher than the high value, throw error
+ if int(low_label_value) > int(high_label_value):
+ raise ConfigError('Segment routing local block low value must be lower than high value')
return None
def generate(isis):
- if not isis:
- isis['new_frr_config'] = ''
+ if not isis or 'deleted' in isis:
+ isis['frr_isisd_config'] = ''
+ isis['frr_zebra_config'] = ''
return None
- # only one ISIS process is supported, so we can directly send the first key
- # of the config dict
- process = list(isis.keys())[0]
- isis[process]['process'] = process
-
- isis['new_frr_config'] = render_to_string('frr/isisd.frr.tmpl',
- isis[process])
-
+ isis['protocol'] = 'isis' # required for frr/route-map.frr.tmpl
+ isis['frr_zebra_config'] = render_to_string('frr/route-map.frr.tmpl', isis)
+ isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis)
return None
def apply(isis):
+ isis_daemon = 'isisd'
+ zebra_daemon = 'zebra'
+
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(daemon='isisd')
- frr_cfg.modify_section(r'interface \S+', '')
- frr_cfg.modify_section(f'router isis \S+', '')
- frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['new_frr_config'])
- frr_cfg.commit_configuration(daemon='isisd')
-
- # If FRR config is blank, rerun the blank commit x times due to frr-reload
- # behavior/bug not properly clearing out on one commit.
- if isis['new_frr_config'] == '':
- for a in range(5):
- frr_cfg.commit_configuration(daemon='isisd')
-
- # Debugging
- '''
- print('')
- print('--------- DEBUGGING ----------')
- print(f'Existing config:\n{frr_cfg["original_config"]}\n\n')
- print(f'Replacement config:\n{isis["new_frr_config"]}\n\n')
- print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n')
- '''
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section(r'(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+$', '', '(\s|!)')
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
+ frr_cfg.load_configuration(isis_daemon)
+ frr_cfg.modify_section(f'^router isis VyOS$', '')
+
+ for key in ['interface', 'interface_removed']:
+ if key not in isis:
+ continue
+ for interface in isis[key]:
+ frr_cfg.modify_section(f'^interface {interface}$', '')
+
+ frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config'])
+ frr_cfg.commit_configuration(isis_daemon)
+
+ # Save configuration to /run/frr/config/frr.conf
+ frr.save_configuration()
return None
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 8ddd705f2..f36abbf90 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -125,7 +125,7 @@ def get_config(config=None):
conf.set_level(base)
- # Get distribute list interface
+ # Get distribute list interface
for dist_iface in conf.list_nodes('distribute-list interface'):
# Set level 'distribute-list interface ethX'
conf.set_level(base + ['distribute-list', 'interface', dist_iface])
@@ -301,6 +301,7 @@ def apply(rip):
if os.path.exists(config_file):
call(f'vtysh -d ripd -f {config_file}')
+ call('sudo vtysh --writeconfig --noerror')
os.remove(config_file)
else:
print("File {0} not found".format(config_file))
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 59ea1d34b..8aa43dd32 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -59,7 +59,7 @@ def get_config(config=None):
conf = Config()
base = ['system', 'login']
login = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
+ no_tag_node_value_mangle=True, get_first_key=True)
# users no longer existing in the running configuration need to be deleted
local_users = get_local_users()
@@ -246,7 +246,9 @@ def apply(login):
# XXX: Should we deny using root at all?
home_dir = getpwnam(user).pw_dir
render(f'{home_dir}/.ssh/authorized_keys', 'login/authorized_keys.tmpl',
- user_config, permission=0o600, user=user, group='users')
+ user_config, permission=0o600,
+ formater=lambda _: _.replace("&quot;", '"'),
+ user=user, group='users')
except Exception as e:
raise ConfigError(f'Adding user "{user}" raised exception: "{e}"')
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
deleted file mode 100755
index 969266c30..000000000
--- a/src/conf_mode/vpn_ipsec.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
-from vyos import ConfigError
-from vyos import airbag
-from pprint import pprint
-airbag.enable()
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
- base = ['vpn', 'nipsec']
- if not conf.exists(base):
- return None
-
- # retrieve common dictionary keys
- ipsec = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- return ipsec
-
-def verify(ipsec):
- if not ipsec:
- return None
-
-def generate(ipsec):
- if not ipsec:
- return None
-
- return ipsec
-
-def apply(ipsec):
- if not ipsec:
- return None
-
- pprint(ipsec)
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index 47367f125..11925dfa4 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -57,9 +57,7 @@ def verify(sstp):
# SSL certificate checks
#
tmp = dict_search('ssl.ca_cert_file', sstp)
- if not tmp:
- raise ConfigError(f'SSL CA certificate file required!')
- else:
+ if tmp:
if not os.path.isfile(tmp):
raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!')