summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/container.py221
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py15
-rwxr-xr-xsrc/conf_mode/dhcp_server.py4
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py60
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py3
-rwxr-xr-xsrc/conf_mode/firewall.py22
-rwxr-xr-xsrc/conf_mode/high-availability.py43
-rwxr-xr-xsrc/conf_mode/http-api.py7
-rwxr-xr-xsrc/conf_mode/https.py2
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py6
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py2
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py2
-rwxr-xr-xsrc/conf_mode/load-balancing-wan.py132
-rwxr-xr-xsrc/conf_mode/protocols_babel.py163
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py59
-rwxr-xr-xsrc/conf_mode/protocols_eigrp.py12
-rwxr-xr-xsrc/conf_mode/protocols_isis.py16
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py17
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py4
-rwxr-xr-xsrc/conf_mode/protocols_static.py8
-rwxr-xr-xsrc/conf_mode/qos.py31
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py115
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py11
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py5
-rwxr-xr-xsrc/conf_mode/snmp.py5
-rwxr-xr-xsrc/conf_mode/system-ip.py38
-rwxr-xr-xsrc/conf_mode/system-ipv6.py38
-rwxr-xr-xsrc/conf_mode/system-login.py60
-rwxr-xr-xsrc/conf_mode/system-syslog.py324
-rwxr-xr-xsrc/conf_mode/system_sflow.py124
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py41
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py6
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py106
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py27
-rwxr-xr-xsrc/conf_mode/vrf.py61
-rw-r--r--[-rwxr-xr-x]src/conf_mode/vrf_vni.py55
36 files changed, 1354 insertions, 491 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 08861053d..aceb27fb0 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2022 VyOS maintainers and contributors
+# Copyright (C) 2021-2023 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,18 +16,22 @@
import os
+from hashlib import sha256
from ipaddress import ip_address
from ipaddress import ip_network
-from time import sleep
from json import dumps as json_write
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import Interface
from vyos.util import call
from vyos.util import cmd
from vyos.util import run
+from vyos.util import rc_cmd
from vyos.util import write_file
from vyos.template import inc_ip
from vyos.template import is_ipv4
@@ -38,8 +42,9 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
-config_containers_registry = '/etc/containers/registries.conf'
-config_containers_storage = '/etc/containers/storage.conf'
+config_containers = '/etc/containers/containers.conf'
+config_registry = '/etc/containers/registries.conf'
+config_storage = '/etc/containers/storage.conf'
systemd_unit_path = '/run/systemd/system'
def _cmd(command):
@@ -68,6 +73,9 @@ def get_config(config=None):
# container base default values can not be merged here - remove and add them later
if 'name' in default_values:
del default_values['name']
+ # registry will be handled below
+ if 'registry' in default_values:
+ del default_values['registry']
container = dict_merge(default_values, container)
# Merge per-container default values
@@ -80,20 +88,38 @@ def get_config(config=None):
for name in container['name']:
container['name'][name] = dict_merge(default_values, container['name'][name])
+ # T5047: Any container related configuration changed? We only
+ # wan't to restart the required containers and not all of them ...
+ tmp = is_node_changed(conf, base + ['name', name])
+ if tmp:
+ if 'container_restart' not in container:
+ container['container_restart'] = [name]
+ else:
+ container['container_restart'].append(name)
+
# 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.
if 'port' in container['name'][name]:
for port in container['name'][name]['port']:
- default_values = defaults(base + ['name', 'port'])
+ default_values_port = defaults(base + ['name', 'port'])
container['name'][name]['port'][port] = dict_merge(
- default_values, container['name'][name]['port'][port])
+ default_values_port, container['name'][name]['port'][port])
# 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.
if 'volume' in container['name'][name]:
for volume in container['name'][name]['volume']:
- default_values = defaults(base + ['name', 'volume'])
+ default_values_volume = defaults(base + ['name', 'volume'])
container['name'][name]['volume'][volume] = dict_merge(
- default_values, container['name'][name]['volume'][volume])
+ default_values_volume, container['name'][name]['volume'][volume])
+
+ # registry is a tagNode with default values - merge the list from
+ # default_values['registry'] into the tagNode variables
+ if 'registry' not in container:
+ container.update({'registry' : {}})
+ default_values = defaults(base)
+ for registry in default_values['registry'].split():
+ tmp = {registry : {}}
+ container['registry'] = dict_merge(tmp, container['registry'])
# Delete container network, delete containers
tmp = node_changed(conf, base + ['network'])
@@ -142,21 +168,29 @@ def verify(container):
raise ConfigError(f'Container network "{network_name}" does not exist!')
if 'address' in container_config['network'][network_name]:
- address = container_config['network'][network_name]['address']
- network = None
- if is_ipv4(address):
- network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0]
- elif is_ipv6(address):
- network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0]
-
- # Specified container IP address must belong to network prefix
- if ip_address(address) not in ip_network(network):
- raise ConfigError(f'Used container address "{address}" not in network "{network}"!')
-
- # We can not use the first IP address of a network prefix as this is used by podman
- if ip_address(address) == ip_network(network)[1]:
- raise ConfigError(f'IP address "{address}" can not be used for a container, '\
- 'reserved for the container engine!')
+ cnt_ipv4 = 0
+ cnt_ipv6 = 0
+ for address in container_config['network'][network_name]['address']:
+ network = None
+ if is_ipv4(address):
+ network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0]
+ cnt_ipv4 += 1
+ elif is_ipv6(address):
+ network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0]
+ cnt_ipv6 += 1
+
+ # Specified container IP address must belong to network prefix
+ if ip_address(address) not in ip_network(network):
+ raise ConfigError(f'Used container address "{address}" not in network "{network}"!')
+
+ # We can not use the first IP address of a network prefix as this is used by podman
+ if ip_address(address) == ip_network(network)[1]:
+ raise ConfigError(f'IP address "{address}" can not be used for a container, '\
+ 'reserved for the container engine!')
+
+ if cnt_ipv4 > 1 or cnt_ipv6 > 1:
+ raise ConfigError(f'Only one IP address per address family can be used for '\
+ f'container "{name}". {cnt_ipv4} IPv4 and {cnt_ipv6} IPv6 address(es)!')
if 'device' in container_config:
for dev, dev_config in container_config['device'].items():
@@ -218,6 +252,8 @@ def verify(container):
if v6_prefix > 1:
raise ConfigError(f'Only one IPv6 prefix can be defined for network "{network}"!')
+ # Verify VRF exists
+ verify_vrf(network_config)
# A network attached to a container can not be deleted
if {'network_remove', 'name'} <= set(container):
@@ -226,6 +262,13 @@ def verify(container):
if 'network' in container_config and network in container_config['network']:
raise ConfigError(f'Can not remove network "{network}", used by container "{container}"!')
+ if 'registry' in container:
+ for registry, registry_config in container['registry'].items():
+ if 'authentication' not in registry_config:
+ continue
+ if not {'username', 'password'} <= set(registry_config['authentication']):
+ raise ConfigError('If registry username or or password is defined, so must be the other!')
+
return None
def generate_run_arguments(name, container_config):
@@ -256,6 +299,11 @@ def generate_run_arguments(name, container_config):
for k, v in container_config['environment'].items():
env_opt += f" --env \"{k}={v['value']}\""
+ hostname = ''
+ if 'host_name' in container_config:
+ hostname = container_config['host_name']
+ hostname = f'--hostname {hostname}'
+
# Publish ports
port = ''
if 'port' in container_config:
@@ -277,62 +325,96 @@ def generate_run_arguments(name, container_config):
container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \
f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {device} {port} {volume} {env_opt}'
+ f'--name {name} {hostname} {device} {port} {volume} {env_opt}'
+
+ entrypoint = ''
+ if 'entrypoint' in container_config:
+ # it needs to be json-formatted with single quote on the outside
+ entrypoint = json_write(container_config['entrypoint'].split()).replace('"', "&quot;")
+ entrypoint = f'--entrypoint &apos;{entrypoint}&apos;'
+
+ hostname = ''
+ if 'host_name' in container_config:
+ hostname = container_config['host_name']
+ hostname = f'--hostname {hostname}'
+
+ command = ''
+ if 'command' in container_config:
+ command = container_config['command'].strip()
+
+ command_arguments = ''
+ if 'arguments' in container_config:
+ command_arguments = container_config['arguments'].strip()
if 'allow_host_networks' in container_config:
- return f'{container_base_cmd} --net host {image}'
+ return f'{container_base_cmd} --net host {entrypoint} {image} {command} {command_arguments}'.strip()
ip_param = ''
networks = ",".join(container_config['network'])
for network in container_config['network']:
- if 'address' in container_config['network'][network]:
- address = container_config['network'][network]['address']
- ip_param = f'--ip {address}'
+ if 'address' not in container_config['network'][network]:
+ continue
+ for address in container_config['network'][network]['address']:
+ if is_ipv6(address):
+ ip_param += f' --ip6 {address}'
+ else:
+ ip_param += f' --ip {address}'
- return f'{container_base_cmd} --net {networks} {ip_param} {image}'
+ return f'{container_base_cmd} --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip()
def generate(container):
# bail out early - looks like removal from running config
if not container:
- if os.path.exists(config_containers_registry):
- os.unlink(config_containers_registry)
- if os.path.exists(config_containers_storage):
- os.unlink(config_containers_storage)
+ for file in [config_containers, config_registry, config_storage]:
+ if os.path.exists(file):
+ os.unlink(file)
return None
if 'network' in container:
for network, network_config in container['network'].items():
tmp = {
- 'cniVersion' : '0.4.0',
- 'name' : network,
- 'plugins' : [{
- 'type': 'bridge',
- 'bridge': f'cni-{network}',
- 'isGateway': True,
- 'ipMasq': False,
- 'hairpinMode': False,
- 'ipam' : {
- 'type': 'host-local',
- 'routes': [],
- 'ranges' : [],
- },
- }]
+ 'name': network,
+ 'id' : sha256(f'{network}'.encode()).hexdigest(),
+ 'driver': 'bridge',
+ 'network_interface': f'pod-{network}',
+ 'subnets': [],
+ 'ipv6_enabled': False,
+ 'internal': False,
+ 'dns_enabled': True,
+ 'ipam_options': {
+ 'driver': 'host-local'
+ }
}
-
for prefix in network_config['prefix']:
- net = [{'gateway' : inc_ip(prefix, 1), 'subnet' : prefix}]
- tmp['plugins'][0]['ipam']['ranges'].append(net)
+ net = {'subnet' : prefix, 'gateway' : inc_ip(prefix, 1)}
+ tmp['subnets'].append(net)
- # install per address-family default orutes
- default_route = '0.0.0.0/0'
if is_ipv6(prefix):
- default_route = '::/0'
- tmp['plugins'][0]['ipam']['routes'].append({'dst': default_route})
+ tmp['ipv6_enabled'] = True
- write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2))
+ write_file(f'/etc/containers/networks/{network}.json', json_write(tmp, indent=2))
- render(config_containers_registry, 'container/registries.conf.j2', container)
- render(config_containers_storage, 'container/storage.conf.j2', container)
+ if 'registry' in container:
+ cmd = f'podman logout --all'
+ rc, out = rc_cmd(cmd)
+ if rc != 0:
+ raise ConfigError(out)
+
+ for registry, registry_config in container['registry'].items():
+ if 'disable' in registry_config:
+ continue
+ if 'authentication' in registry_config:
+ if {'username', 'password'} <= set(registry_config['authentication']):
+ username = registry_config['authentication']['username']
+ password = registry_config['authentication']['password']
+ cmd = f'podman login --username {username} --password {password} {registry}'
+ rc, out = rc_cmd(cmd)
+ if rc != 0:
+ raise ConfigError(out)
+
+ render(config_containers, 'container/containers.conf.j2', container)
+ render(config_registry, 'container/registries.conf.j2', container)
+ render(config_storage, 'container/storage.conf.j2', container)
if 'name' in container:
for name, container_config in container['name'].items():
@@ -341,7 +423,8 @@ def generate(container):
file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service')
run_args = generate_run_arguments(name, container_config)
- render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args})
+ render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args,},
+ formater=lambda _: _.replace("&quot;", '"').replace("&apos;", "'"))
return None
@@ -360,10 +443,7 @@ def apply(container):
# Delete old networks if needed
if 'network_remove' in container:
for network in container['network_remove']:
- call(f'podman network rm {network}')
- tmp = f'/etc/cni/net.d/{network}.conflist'
- if os.path.exists(tmp):
- os.unlink(tmp)
+ call(f'podman network rm {network} >/dev/null 2>&1')
# Add container
disabled_new = False
@@ -387,11 +467,26 @@ def apply(container):
os.unlink(file_path)
continue
- cmd(f'systemctl restart vyos-container-{name}.service')
+ if 'container_restart' in container and name in container['container_restart']:
+ cmd(f'systemctl restart vyos-container-{name}.service')
if disabled_new:
call('systemctl daemon-reload')
+ # Start network and assign it to given VRF if requested. this can only be done
+ # after the containers got started as the podman network interface will
+ # only be enabled by the first container and yet I do not know how to enable
+ # the network interface in advance
+ if 'network' in container:
+ for network, network_config in container['network'].items():
+ network_name = f'pod-{network}'
+ # T5147: Networks are started only as soon as there is a consumer.
+ # If only a network is created in the first place, no need to assign
+ # it to a VRF as there's no consumer, yet.
+ if os.path.exists(f'/sys/class/net/{network_name}'):
+ tmp = Interface(network_name)
+ tmp.set_vrf(network_config.get('vrf', ''))
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index 4de2ca2f3..7e702a446 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -18,9 +18,11 @@ import os
from sys import exit
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render
+from vyos.base import Warning
from vyos.util import call
from vyos.util import dict_search
from vyos.xml import defaults
@@ -59,6 +61,19 @@ def verify(relay):
raise ConfigError('No DHCP relay server(s) configured.\n' \
'At least one DHCP relay server required.')
+ if 'interface' in relay:
+ Warning('DHCP relay interface is DEPRECATED - please use upstream-interface and listen-interface instead!')
+ if 'upstream_interface' in relay or 'listen_interface' in relay:
+ raise ConfigError('<interface> configuration is not compatible with upstream/listen interface')
+ else:
+ Warning('<interface> is going to be deprecated.\n' \
+ 'Please use <listen-interface> and <upstream-interface>')
+
+ if 'upstream_interface' in relay and 'listen_interface' not in relay:
+ raise ConfigError('No listen-interface configured')
+ if 'listen_interface' in relay and 'upstream_interface' not in relay:
+ raise ConfigError('No upstream-interface configured')
+
return None
def generate(relay):
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index 52b682d6d..2b2af252d 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -247,7 +247,7 @@ def verify(dhcp):
net2 = ip_network(n)
if (net != net2):
if net.overlaps(net2):
- raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
# Prevent 'disable' for shared-network if only one network is configured
if (shared_networks - disabled_shared_networks) < 1:
@@ -283,7 +283,7 @@ def generate(dhcp):
if not dhcp or 'disable' in dhcp:
return None
- # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw
+ # Please see: https://vyos.dev/T1129 for quoting of the raw
# parameters we can pass to ISC DHCPd
tmp_file = '/tmp/dhcpd.conf'
render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp,
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index d0d87d73e..0d86c6a52 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -24,7 +24,7 @@ from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.hostsd_client import Client as hostsd_client
from vyos.template import render
-from vyos.template import is_ipv6
+from vyos.template import bracketize_ipv6
from vyos.util import call
from vyos.util import chown
from vyos.util import dict_search
@@ -58,8 +58,26 @@ def get_config(config=None):
default_values = defaults(base)
# T2665 due to how defaults under tag nodes work, we must clear these out before we merge
del default_values['authoritative_domain']
+ del default_values['name_server']
+ del default_values['domain']['name_server']
dns = dict_merge(default_values, dns)
+ # T2665: we cleared default values for tag node 'name_server' above.
+ # We now need to add them back back in a granular way.
+ if 'name_server' in dns:
+ default_values = defaults(base + ['name-server'])
+ for server in dns['name_server']:
+ dns['name_server'][server] = dict_merge(default_values, dns['name_server'][server])
+
+ # T2665: we cleared default values for tag node 'domain' above.
+ # We now need to add them back back in a granular way.
+ if 'domain' in dns:
+ default_values = defaults(base + ['domain', 'name-server'])
+ for domain in dns['domain'].keys():
+ for server in dns['domain'][domain]['name_server']:
+ dns['domain'][domain]['name_server'][server] = dict_merge(
+ default_values, dns['domain'][domain]['name_server'][server])
+
# some additions to the default dictionary
if 'system' in dns:
base_nameservers = ['system', 'name-server']
@@ -81,7 +99,7 @@ def get_config(config=None):
recorddata = zonedata['records']
- for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]:
+ for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ns', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]:
if rtype not in recorddata:
continue
for subnode in recorddata[rtype]:
@@ -95,7 +113,7 @@ def get_config(config=None):
rdata = dict_merge(rdefaults, rdata)
if not 'address' in rdata:
- dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one address is required')
continue
if subnode == 'any':
@@ -108,12 +126,12 @@ def get_config(config=None):
'ttl': rdata['ttl'],
'value': address
})
- elif rtype in ['cname', 'ptr']:
+ elif rtype in ['cname', 'ptr', 'ns']:
rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
rdata = dict_merge(rdefaults, rdata)
if not 'target' in rdata:
- dns['authoritative_zone_errors'].append('{}.{}: target is required'.format(subnode, node))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: target is required')
continue
zone['records'].append({
@@ -128,7 +146,7 @@ def get_config(config=None):
rdata = dict_merge(rdefaults, rdata)
if not 'server' in rdata:
- dns['authoritative_zone_errors'].append('{}.{}: at least one server is required'.format(subnode, node))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one server is required')
continue
for servername in rdata['server']:
@@ -146,7 +164,7 @@ def get_config(config=None):
rdata = dict_merge(rdefaults, rdata)
if not 'value' in rdata:
- dns['authoritative_zone_errors'].append('{}.{}: at least one value is required'.format(subnode, node))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one value is required')
continue
for value in rdata['value']:
@@ -161,7 +179,7 @@ def get_config(config=None):
rdata = dict_merge(rdefaults, rdata)
if not 'value' in rdata:
- dns['authoritative_zone_errors'].append('{}.{}: value is required'.format(subnode, node))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: value is required')
continue
zone['records'].append({
@@ -176,7 +194,7 @@ def get_config(config=None):
rdata = dict_merge(rdefaults, rdata)
if not 'entry' in rdata:
- dns['authoritative_zone_errors'].append('{}.{}: at least one entry is required'.format(subnode, node))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one entry is required')
continue
for entryno in rdata['entry']:
@@ -185,11 +203,11 @@ def get_config(config=None):
entrydata = dict_merge(entrydefaults, entrydata)
if not 'hostname' in entrydata:
- dns['authoritative_zone_errors'].append('{}.{}: hostname is required for entry {}'.format(subnode, node, entryno))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: hostname is required for entry {entryno}')
continue
if not 'port' in entrydata:
- dns['authoritative_zone_errors'].append('{}.{}: port is required for entry {}'.format(subnode, node, entryno))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: port is required for entry {entryno}')
continue
zone['records'].append({
@@ -205,7 +223,7 @@ def get_config(config=None):
if not 'rule' in rdata:
- dns['authoritative_zone_errors'].append('{}.{}: at least one rule is required'.format(subnode, node))
+ dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one rule is required')
continue
for ruleno in rdata['rule']:
@@ -263,7 +281,7 @@ def verify(dns):
# as a domain will contains dot's which is out dictionary delimiter.
if 'domain' in dns:
for domain in dns['domain']:
- if 'server' not in dns['domain'][domain]:
+ if 'name_server' not in dns['domain'][domain]:
raise ConfigError(f'No server configured for domain {domain}!')
if 'dns64_prefix' in dns:
@@ -329,7 +347,12 @@ def apply(dns):
# sources
hc.delete_name_servers([hostsd_tag])
if 'name_server' in dns:
- hc.add_name_servers({hostsd_tag: dns['name_server']})
+ # 'name_server' is of the form
+ # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...}
+ # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...]
+ nslist = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p)
+ for (h, p) in dns['name_server'].items()]
+ hc.add_name_servers({hostsd_tag: nslist})
# delete all nameserver tags
hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor())
@@ -358,7 +381,14 @@ def apply(dns):
# the list and keys() are required as get returns a dict, not list
hc.delete_forward_zones(list(hc.get_forward_zones().keys()))
if 'domain' in dns:
- hc.add_forward_zones(dns['domain'])
+ zones = dns['domain']
+ for domain in zones.keys():
+ # 'name_server' is of the form
+ # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...}
+ # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...]
+ zones[domain]['name_server'] = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p)
+ for (h, p) in zones[domain]['name_server'].items()]
+ hc.add_forward_zones(zones)
# hostsd generates NTAs for the authoritative zones
# the list and keys() are required as get returns a dict, not list
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 06a2f7e15..426e3d693 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -108,7 +108,8 @@ def verify(dyndns):
raise ConfigError(f'"host-name" {error_msg}')
if 'login' not in config:
- raise ConfigError(f'"login" (username) {error_msg}')
+ if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'):
+ raise ConfigError(f'"login" (username) {error_msg}, unless using CloudFlare')
if 'password' not in config:
raise ConfigError(f'"password" {error_msg}')
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 20cf1ead1..190587980 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -197,6 +197,15 @@ def verify_rule(firewall, rule_conf, ipv6):
if target not in dict_search_args(firewall, 'ipv6_name'):
raise ConfigError(f'Invalid jump-target. Firewall ipv6-name {target} does not exist on the system')
+ if 'queue_options' in rule_conf:
+ if 'queue' not in rule_conf['action']:
+ raise ConfigError('queue-options defined, but action queue needed and it is not defined')
+ if 'fanout' in rule_conf['queue_options'] and ('queue' not in rule_conf or '-' not in rule_conf['queue']):
+ raise ConfigError('queue-options fanout defined, then queue needs to be defined as a range')
+
+ if 'queue' in rule_conf and 'queue' not in rule_conf['action']:
+ raise ConfigError('queue defined, but action queue needed and it is not defined')
+
if 'fragment' in rule_conf:
if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']):
raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"')
@@ -273,6 +282,19 @@ def verify_rule(firewall, rule_conf, ipv6):
if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group')
+ if 'port' in side_conf and dict_search_args(side_conf, 'group', 'port_group'):
+ raise ConfigError(f'{side} port-group and port cannot both be defined')
+
+ if 'log_options' in rule_conf:
+ if 'log' not in rule_conf or 'enable' not in rule_conf['log']:
+ raise ConfigError('log-options defined, but log is not enable')
+
+ if 'snapshot_length' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']:
+ raise ConfigError('log-options snapshot-length defined, but log group is not define')
+
+ if 'queue_threshold' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']:
+ raise ConfigError('log-options queue-threshold defined, but log group is not define')
+
def verify_nested_group(group_name, group, groups, seen):
if 'include' not in group:
return
diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py
index bc3e67b40..e18b426b1 100755
--- a/src/conf_mode/high-availability.py
+++ b/src/conf_mode/high-availability.py
@@ -21,6 +21,7 @@ from ipaddress import ip_interface
from ipaddress import IPv4Interface
from ipaddress import IPv6Interface
+from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.ifconfig.vrrp import VRRP
@@ -28,6 +29,7 @@ from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.util import call
+from vyos.util import dict_search
from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
@@ -49,12 +51,27 @@ def get_config(config=None):
# We have gathered the dict representation of the CLI, but there are default
# options which we need to update into the dictionary retrived.
if 'vrrp' in ha:
+ if dict_search('vrrp.global_parameters.garp', ha) != None:
+ default_values = defaults(base_vrrp + ['global-parameters', 'garp'])
+ ha['vrrp']['global_parameters']['garp'] = dict_merge(
+ default_values, ha['vrrp']['global_parameters']['garp'])
+
if 'group' in ha['vrrp']:
- default_values_vrrp = defaults(base_vrrp + ['group'])
- if 'garp' in default_values_vrrp:
- del default_values_vrrp['garp']
+ default_values = defaults(base_vrrp + ['group'])
+ default_values_garp = defaults(base_vrrp + ['group', 'garp'])
+
+ # 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.
+ if 'garp' in default_values:
+ del default_values['garp']
for group in ha['vrrp']['group']:
- ha['vrrp']['group'][group] = dict_merge(default_values_vrrp, ha['vrrp']['group'][group])
+ ha['vrrp']['group'][group] = dict_merge(default_values, ha['vrrp']['group'][group])
+
+ # 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.
+ if 'garp' in ha['vrrp']['group'][group]:
+ ha['vrrp']['group'][group]['garp'] = dict_merge(
+ default_values_garp, ha['vrrp']['group'][group]['garp'])
# Merge per virtual-server default values
if 'virtual_server' in ha:
@@ -70,7 +87,7 @@ def get_config(config=None):
return ha
def verify(ha):
- if not ha:
+ if not ha or 'disable' in ha:
return None
used_vrid_if = []
@@ -90,6 +107,18 @@ def verify(ha):
if not {'password', 'type'} <= set(group_config['authentication']):
raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"')
+ if 'health_check' in group_config:
+ health_check_types = ["script", "ping"]
+ from vyos.utils.dict import check_mutually_exclusive_options
+ try:
+ check_mutually_exclusive_options(group_config["health_check"], health_check_types, required=True)
+ except ValueError as e:
+ Warning(f'Health check configuration for VRRP group "{group}" will remain unused ' \
+ f'until it has one of the following options: {health_check_types}')
+ # XXX: health check has default options so we need to remove it
+ # to avoid generating useless config statements in keepalived.conf
+ del group_config["health_check"]
+
# Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
# We also need to make sure VRID is not used twice on the same interface with the
# same address family.
@@ -159,7 +188,7 @@ def verify(ha):
def generate(ha):
- if not ha:
+ if not ha or 'disable' in ha:
return None
render(VRRP.location['config'], 'high-availability/keepalived.conf.j2', ha)
@@ -167,7 +196,7 @@ def generate(ha):
def apply(ha):
service_name = 'keepalived.service'
- if not ha:
+ if not ha or 'disable' in ha:
call(f'systemctl stop {service_name}')
return None
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 6328294c1..7e801eb26 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -79,9 +79,10 @@ def get_config(config=None):
# http-api.conf format for api_keys:
if 'keys' in api_dict:
api_dict['api_keys'] = []
- for el in list(api_dict['keys']['id']):
- key = api_dict['keys']['id'][el]['key']
- api_dict['api_keys'].append({'id': el, 'key': key})
+ for el in list(api_dict['keys'].get('id', {})):
+ key = api_dict['keys']['id'][el].get('key', '')
+ if key:
+ api_dict['api_keys'].append({'id': el, 'key': key})
del api_dict['keys']
# Do we run inside a VRF context?
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index ce5e63928..b0c38e8d3 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -159,6 +159,8 @@ def generate(https):
server_block['port'] = data.get('listen-port', '443')
name = data.get('server-name', ['_'])
server_block['name'] = name
+ allow_client = data.get('allow-client', {})
+ server_block['allow_client'] = allow_client.get('address', [])
server_block_list.append(server_block)
# get certificate data
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 8155f36c2..6f227b0d1 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2022 VyOS maintainers and contributors
+# Copyright (C) 2019-2023 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
@@ -597,7 +597,7 @@ def generate_pki_files(openvpn):
def generate(openvpn):
interface = openvpn['ifname']
directory = os.path.dirname(cfg_file.format(**openvpn))
- plugin_dir = '/usr/lib/openvpn'
+ openvpn['plugin_dir'] = '/usr/lib/openvpn'
# create base config directory on demand
makedir(directory, user, group)
# enforce proper permissions on /run/openvpn
@@ -645,7 +645,7 @@ def generate(openvpn):
user=user, group=group)
# we need to support quoting of raw parameters from OpenVPN CLI
- # see https://phabricator.vyos.net/T1632
+ # see https://vyos.dev/T1632
render(cfg_file.format(**openvpn), 'openvpn/server.conf.j2', openvpn,
formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index e2701d9d3..0a3726e94 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -136,7 +136,7 @@ def verify(tunnel):
if our_key != None:
if their_address == our_address and their_key == our_key:
raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \
- f'is already used for tunnel "{tunnel_if}"!')
+ f'is already used for tunnel "{o_tunnel}"!')
else:
our_source_if = dict_search('source_interface', tunnel)
their_source_if = dict_search('source_interface', o_tunnel_conf)
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index a14a992ae..9ca495476 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -171,7 +171,7 @@ def apply(wwan):
options = f'ip-type={ip_type},apn=' + wwan['apn']
if 'authentication' in wwan:
- options += ',user={user},password={password}'.format(**wwan['authentication'])
+ options += ',user={username},password={password}'.format(**wwan['authentication'])
command = f'{base_cmd} --simple-connect="{options}"'
call(command, stdout=DEVNULL)
diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py
index 11840249f..7086aaf8b 100755
--- a/src/conf_mode/load-balancing-wan.py
+++ b/src/conf_mode/load-balancing-wan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2023 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,17 +14,25 @@
# 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 shutil import rmtree
+from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import node_changed
-from vyos.util import call
+from vyos.configdict import dict_merge
+from vyos.util import cmd
+from vyos.template import render
+from vyos.xml import defaults
from vyos import ConfigError
-from pprint import pprint
from vyos import airbag
airbag.enable()
+load_balancing_dir = '/run/load-balance'
+load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf'
+systemd_service = 'vyos-wan-load-balance.service'
+
def get_config(config=None):
if config:
@@ -33,27 +41,135 @@ def get_config(config=None):
conf = Config()
base = ['load-balancing', 'wan']
- lb = conf.get_config_dict(base, get_first_key=True,
- no_tag_node_value_mangle=True)
+ lb = conf.get_config_dict(base,
+ get_first_key=True,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ # lb base default values can not be merged here - remove and add them later
+ if 'interface_health' in default_values:
+ del default_values['interface_health']
+ if 'rule' in default_values:
+ del default_values['rule']
+ lb = dict_merge(default_values, lb)
+
+ if 'interface_health' in lb:
+ for iface in lb.get('interface_health'):
+ default_values_iface = defaults(base + ['interface-health'])
+ if 'test' in default_values_iface:
+ del default_values_iface['test']
+ lb['interface_health'][iface] = dict_merge(
+ default_values_iface, lb['interface_health'][iface])
+ if 'test' in lb['interface_health'][iface]:
+ for node_test in lb['interface_health'][iface]['test']:
+ default_values_test = defaults(base +
+ ['interface-health', 'test'])
+ lb['interface_health'][iface]['test'][node_test] = dict_merge(
+ default_values_test,
+ lb['interface_health'][iface]['test'][node_test])
+
+ if 'rule' in lb:
+ for rule in lb.get('rule'):
+ default_values_rule = defaults(base + ['rule'])
+ if 'interface' in default_values_rule:
+ del default_values_rule['interface']
+ lb['rule'][rule] = dict_merge(default_values_rule, lb['rule'][rule])
+ if not conf.exists(base + ['rule', rule, 'limit']):
+ del lb['rule'][rule]['limit']
+ if 'interface' in lb['rule'][rule]:
+ for iface in lb['rule'][rule]['interface']:
+ default_values_rule_iface = defaults(base + ['rule', 'interface'])
+ lb['rule'][rule]['interface'][iface] = dict_merge(default_values_rule_iface, lb['rule'][rule]['interface'][iface])
- pprint(lb)
return lb
+
def verify(lb):
- return None
+ if not lb:
+ return None
+
+ if 'interface_health' not in lb:
+ raise ConfigError(
+ 'A valid WAN load-balance configuration requires an interface with a nexthop!'
+ )
+
+ for interface, interface_config in lb['interface_health'].items():
+ if 'nexthop' not in interface_config:
+ raise ConfigError(
+ f'interface-health {interface} nexthop must be specified!')
+
+ if 'test' in interface_config:
+ for test_rule, test_config in interface_config['test'].items():
+ if 'type' in test_config:
+ if test_config['type'] == 'user-defined' and 'test_script' not in test_config:
+ raise ConfigError(
+ f'test {test_rule} script must be defined for test-script!'
+ )
+
+ if 'rule' not in lb:
+ Warning(
+ 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!'
+ )
+ else:
+ for rule, rule_config in lb['rule'].items():
+ if 'inbound_interface' not in rule_config:
+ raise ConfigError(f'rule {rule} inbound-interface must be specified!')
+ if {'failover', 'exclude'} <= set(rule_config):
+ raise ConfigError(f'rule {rule} failover cannot be configured with exclude!')
+ if {'limit', 'exclude'} <= set(rule_config):
+ raise ConfigError(f'rule {rule} limit cannot be used with exclude!')
+ if 'interface' not in rule_config:
+ if 'exclude' not in rule_config:
+ Warning(
+ f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule'
+ )
+ for direction in {'source', 'destination'}:
+ if direction in rule_config:
+ if 'protocol' in rule_config and 'port' in rule_config[
+ direction]:
+ if rule_config['protocol'] not in {'tcp', 'udp'}:
+ raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"')
def generate(lb):
if not lb:
+ # Delete /run/load-balance/wlb.conf
+ if os.path.isfile(load_balancing_conf_file):
+ os.unlink(load_balancing_conf_file)
+ # Delete old directories
+ if os.path.isdir(load_balancing_dir):
+ rmtree(load_balancing_dir, ignore_errors=True)
+ if os.path.exists('/var/run/load-balance/wlb.out'):
+ os.unlink('/var/run/load-balance/wlb.out')
+
return None
+ # Create load-balance dir
+ if not os.path.isdir(load_balancing_dir):
+ os.mkdir(load_balancing_dir)
+
+ render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb)
+
return None
def apply(lb):
+ if not lb:
+ try:
+ cmd(f'systemctl stop {systemd_service}')
+ except Exception as e:
+ print(f"Error message: {e}")
+
+ else:
+ cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1')
+ cmd(f'systemctl restart {systemd_service}')
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py
new file mode 100755
index 000000000..20821c7f2
--- /dev/null
+++ b/src/conf_mode/protocols_babel.py
@@ -0,0 +1,163 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2023 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.configdict import node_changed
+from vyos.configverify import verify_common_route_maps
+from vyos.configverify import verify_access_list
+from vyos.configverify import verify_prefix_list
+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()
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['protocols', 'babel']
+ babel = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+
+ # FRR has VRF support for different routing daemons. As interfaces belong
+ # to VRFs - or the global VRF, we need to check for changed interfaces so
+ # that they will be properly rendered for the FRR config. Also this eases
+ # removal of interfaces from the running configuration.
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ babel['interface_removed'] = list(interfaces_removed)
+
+ # Bail out early if configuration tree does not exist
+ if not conf.exists(base):
+ babel.update({'deleted' : ''})
+ return babel
+
+ # 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)
+
+ # XXX: T2665: we currently have no nice way for defaults under tag nodes,
+ # clean them out and add them manually :(
+ del default_values['interface']
+
+ # merge in remaining default values
+ babel = dict_merge(default_values, babel)
+
+ # 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
+ babel = dict_merge(tmp, babel)
+ return babel
+
+def verify(babel):
+ if not babel:
+ return None
+
+ # verify distribute_list
+ if "distribute_list" in babel:
+ acl_keys = {
+ "ipv4": [
+ "distribute_list.ipv4.access_list.in",
+ "distribute_list.ipv4.access_list.out",
+ ],
+ "ipv6": [
+ "distribute_list.ipv6.access_list.in",
+ "distribute_list.ipv6.access_list.out",
+ ]
+ }
+ prefix_list_keys = {
+ "ipv4": [
+ "distribute_list.ipv4.prefix_list.in",
+ "distribute_list.ipv4.prefix_list.out",
+ ],
+ "ipv6":[
+ "distribute_list.ipv6.prefix_list.in",
+ "distribute_list.ipv6.prefix_list.out",
+ ]
+ }
+ for address_family in ["ipv4", "ipv6"]:
+ for iface_key in babel["distribute_list"].get(address_family, {}).get("interface", {}).keys():
+ acl_keys[address_family].extend([
+ f"distribute_list.{address_family}.interface.{iface_key}.access_list.in",
+ f"distribute_list.{address_family}.interface.{iface_key}.access_list.out"
+ ])
+ prefix_list_keys[address_family].extend([
+ f"distribute_list.{address_family}.interface.{iface_key}.prefix_list.in",
+ f"distribute_list.{address_family}.interface.{iface_key}.prefix_list.out"
+ ])
+
+ for address_family, keys in acl_keys.items():
+ for key in keys:
+ acl = dict_search(key, babel)
+ if acl:
+ verify_access_list(acl, babel, version='6' if address_family == 'ipv6' else '')
+
+ for address_family, keys in prefix_list_keys.items():
+ for key in keys:
+ prefix_list = dict_search(key, babel)
+ if prefix_list:
+ verify_prefix_list(prefix_list, babel, version='6' if address_family == 'ipv6' else '')
+
+
+def generate(babel):
+ if not babel or 'deleted' in babel:
+ return None
+
+ babel['new_frr_config'] = render_to_string('frr/babeld.frr.j2', babel)
+ return None
+
+def apply(babel):
+ babel_daemon = 'babeld'
+
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+
+ frr_cfg.load_configuration(babel_daemon)
+ frr_cfg.modify_section('^router babel', stop_pattern='^exit', remove_stop_mark=True)
+
+ for key in ['interface', 'interface_removed']:
+ if key not in babel:
+ continue
+ for interface in babel[key]:
+ frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
+
+ if 'new_frr_config' in babel:
+ frr_cfg.add_before(frr.default_add_before, babel['new_frr_config'])
+ frr_cfg.commit_configuration(babel_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_bgp.py b/src/conf_mode/protocols_bgp.py
index c410258ee..b23584bdb 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -50,16 +50,24 @@ def get_config(config=None):
bgp = conf.get_config_dict(base, key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=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: bgp.update({'vrf' : vrf})
-
bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
key_mangling=('-', '_'),
get_first_key=True,
no_tag_node_value_mangle=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:
+ bgp.update({'vrf' : vrf})
+ # We can not delete the BGP VRF instance if there is a L3VNI configured
+ tmp = ['vrf', 'name', vrf, 'vni']
+ if conf.exists(tmp):
+ bgp.update({'vni' : conf.return_value(tmp)})
+ # We can safely delete ourself from the dependent vrf list
+ if vrf in bgp['dependent_vrfs']:
+ del bgp['dependent_vrfs'][vrf]
+
bgp['dependent_vrfs'].update({'default': {'protocols': {
'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'),
get_first_key=True,
@@ -202,9 +210,13 @@ def verify(bgp):
if 'vrf' in bgp:
# Cannot delete vrf if it exists in import vrf list in other vrfs
for tmp_afi in ['ipv4_unicast', 'ipv6_unicast']:
- if verify_vrf_as_import(bgp['vrf'],tmp_afi,bgp['dependent_vrfs']):
- raise ConfigError(f'Cannot delete vrf {bgp["vrf"]} instance, ' \
- 'Please unconfigure import vrf commands!')
+ if verify_vrf_as_import(bgp['vrf'], tmp_afi, bgp['dependent_vrfs']):
+ raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \
+ 'unconfigure "import vrf" commands!')
+ # We can not delete the BGP instance if a L3VNI instance exists
+ if 'vni' in bgp:
+ raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \
+ f'unconfigure VNI "{bgp["vni"]}" first!')
else:
# We are running in the default VRF context, thus we can not delete
# our main BGP instance if there are dependent BGP VRF instances.
@@ -235,6 +247,11 @@ def verify(bgp):
raise ConfigError(f'Specified peer-group "{peer_group}" for '\
f'neighbor "{neighbor}" does not exist!')
+ if 'local_role' in peer_config:
+ #Ensure Local Role has only one value.
+ if len(peer_config['local_role']) > 1:
+ raise ConfigError(f'Only one local role can be specified for peer "{peer}"!')
+
if 'local_as' in peer_config:
if len(peer_config['local_as']) > 1:
raise ConfigError(f'Only one local-as number can be specified for peer "{peer}"!')
@@ -407,6 +424,11 @@ def verify(bgp):
raise ConfigError('Missing mandatory configuration option for '\
f'global administrative distance {key}!')
+ # TCP keepalive requires all three parameters to be set
+ if dict_search('parameters.tcp_keepalive', bgp) != None:
+ if not {'idle', 'interval', 'probes'} <= set(bgp['parameters']['tcp_keepalive']):
+ raise ConfigError('TCP keepalive incomplete - idle, keepalive and probes must be set')
+
# Address Family specific validation
if 'address_family' in bgp:
for afi, afi_config in bgp['address_family'].items():
@@ -419,7 +441,6 @@ def verify(bgp):
f'{afi} administrative distance {key}!')
if afi in ['ipv4_unicast', 'ipv6_unicast']:
-
vrf_name = bgp['vrf'] if dict_search('vrf', bgp) else 'default'
# Verify if currant VRF contains rd and route-target options
# and does not exist in import list in other VRFs
@@ -468,32 +489,30 @@ def verify(bgp):
tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)
if tmp: verify_route_map(tmp, bgp)
+ # Checks only required for L2VPN EVPN
+ if afi in ['l2vpn_evpn']:
+ if 'vni' in afi_config:
+ for vni, vni_config in afi_config['vni'].items():
+ if 'rd' in vni_config and 'advertise_all_vni' not in afi_config:
+ raise ConfigError('BGP EVPN "rd" requires "advertise-all-vni" to be set!')
+ if 'route_target' in vni_config and 'advertise_all_vni' not in afi_config:
+ raise ConfigError('BGP EVPN "route-target" requires "advertise-all-vni" to be set!')
+
return None
def generate(bgp):
if not bgp or 'deleted' in bgp:
return None
- bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.j2
- bgp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', bgp)
bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.j2', bgp)
-
return None
def apply(bgp):
bgp_daemon = 'bgpd'
- zebra_daemon = 'zebra'
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- # 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 bgp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
- if 'frr_zebra_config' in bgp:
- frr_cfg.add_before(frr.default_add_before, bgp['frr_zebra_config'])
- frr_cfg.commit_configuration(zebra_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 = ''
diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py
index c1a1a45e1..609b39065 100755
--- a/src/conf_mode/protocols_eigrp.py
+++ b/src/conf_mode/protocols_eigrp.py
@@ -69,8 +69,6 @@ def get_config(config=None):
# Merge policy dict into "regular" config dict
eigrp = dict_merge(tmp, eigrp)
- import pprint
- pprint.pprint(eigrp)
return eigrp
def verify(eigrp):
@@ -80,24 +78,14 @@ def generate(eigrp):
if not eigrp or 'deleted' in eigrp:
return None
- eigrp['protocol'] = 'eigrp' # required for frr/vrf.route-map.frr.j2
- eigrp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', eigrp)
eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp)
def apply(eigrp):
eigrp_daemon = 'eigrpd'
- zebra_daemon = 'zebra'
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- # 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 eigrp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
- if 'frr_zebra_config' in eigrp:
- frr_cfg.add_before(frr.default_add_before, eigrp['frr_zebra_config'])
- frr_cfg.commit_configuration(zebra_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 = ''
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index cb8ea3be4..ecca87db0 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -129,7 +129,7 @@ def verify(isis):
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}!')
+ raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')
# If md5 and plaintext-password set at the same time
for password in ['area_password', 'domain_password']:
@@ -203,7 +203,7 @@ def verify(isis):
if list(set(global_range) & set(local_range)):
raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\
f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!')
-
+
# Check for a blank or invalid value per prefix
if dict_search('segment_routing.prefix', isis):
for prefix, prefix_config in isis['segment_routing']['prefix'].items():
@@ -218,7 +218,7 @@ def verify(isis):
if dict_search('segment_routing.prefix', isis):
for prefix, prefix_config in isis['segment_routing']['prefix'].items():
if 'absolute' in prefix_config:
- if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']):
+ if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']):
raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\
f'and no-php-flag configured at the same time.')
elif 'index' in prefix_config:
@@ -232,25 +232,15 @@ def generate(isis):
if not isis or 'deleted' in isis:
return None
- isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.j2
- isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', isis)
isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.j2', 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()
- # The route-map used for the FIB (zebra) is part of the zebra daemon
- frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section('(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
- if 'frr_zebra_config' in isis:
- frr_cfg.add_before(frr.default_add_before, isis['frr_zebra_config'])
- frr_cfg.commit_configuration(zebra_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 = ''
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 0582d32be..b73483470 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -89,7 +89,7 @@ def get_config(config=None):
if 'mpls_te' not in ospf:
del default_values['mpls_te']
- for protocol in ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']:
+ for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']:
# table is a tagNode thus we need to clean out all occurances for the
# default values and load them in later individually
if protocol == 'table':
@@ -196,7 +196,7 @@ def verify(ospf):
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}!')
+ raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')
# Segment routing checks
if dict_search('segment_routing.global_block', ospf):
@@ -234,7 +234,7 @@ def verify(ospf):
if list(set(global_range) & set(local_range)):
raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\
f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!')
-
+
# Check for a blank or invalid value per prefix
if dict_search('segment_routing.prefix', ospf):
for prefix, prefix_config in ospf['segment_routing']['prefix'].items():
@@ -256,25 +256,15 @@ def generate(ospf):
if not ospf or 'deleted' in ospf:
return None
- ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.j2
- ospf['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', ospf)
ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf)
return None
def apply(ospf):
ospf_daemon = 'ospfd'
- zebra_daemon = 'zebra'
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
- # The route-map used for the FIB (zebra) is part of the zebra daemon
- frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section('(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
- if 'frr_zebra_config' in ospf:
- frr_cfg.add_before(frr.default_add_before, ospf['frr_zebra_config'])
- frr_cfg.commit_configuration(zebra_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 = ''
@@ -292,6 +282,7 @@ def apply(ospf):
if 'frr_ospfd_config' in ospf:
frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config'])
+
frr_cfg.commit_configuration(ospf_daemon)
return None
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index ed0a8fba2..cb21bd83c 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2023 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
@@ -138,7 +138,7 @@ def verify(ospfv3):
vrf = ospfv3['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}!')
+ raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')
return None
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index 3e5ebb805..7b6150696 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -105,20 +105,14 @@ def generate(static):
def apply(static):
static_daemon = 'staticd'
- zebra_daemon = 'zebra'
# Save original configuration prior to starting any commit actions
frr_cfg = frr.FRRConfig()
-
- # 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'^ip protocol static route-map [-a-zA-Z0-9.]+', '')
- frr_cfg.commit_configuration(zebra_daemon)
frr_cfg.load_configuration(static_daemon)
if 'vrf' in static:
vrf = static['vrf']
- frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit-vrf', remove_stop_mark=True)
else:
frr_cfg.modify_section(r'^ip route .*')
frr_cfg.modify_section(r'^ipv6 route .*')
diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py
index 0418e8d82..1be2c283f 100755
--- a/src/conf_mode/qos.py
+++ b/src/conf_mode/qos.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2023 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,12 +14,16 @@
# 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 netifaces import interfaces
+from vyos.base import Warning
from vyos.config import Config
+from vyos.configdep import set_dependents, call_dependents
from vyos.configdict import dict_merge
-from vyos.configverify import verify_interface_exists
+from vyos.ifconfig import Section
from vyos.qos import CAKE
from vyos.qos import DropTail
from vyos.qos import FairQueue
@@ -81,6 +85,18 @@ def get_config(config=None):
get_first_key=True,
no_tag_node_value_mangle=True)
+ if 'interface' in qos:
+ for ifname, if_conf in qos['interface'].items():
+ if_node = Section.get_config_path(ifname)
+
+ if not if_node:
+ continue
+
+ path = f'interfaces {if_node}'
+ if conf.exists(f'{path} mirror') or conf.exists(f'{path} redirect'):
+ type_node = path.split(" ")[1] # return only interface type node
+ set_dependents(type_node, conf, ifname)
+
if 'policy' in qos:
for policy in qos['policy']:
# when calling defaults() we need to use the real CLI node, thus we
@@ -194,8 +210,6 @@ def verify(qos):
# we should check interface ingress/egress configuration after verifying that
# the policy name is used only once - this makes the logic easier!
for interface, interface_config in qos['interface'].items():
- verify_interface_exists(interface)
-
for direction in ['egress', 'ingress']:
# bail out early if shaper for given direction is not used at all
if direction not in interface_config:
@@ -229,6 +243,13 @@ def apply(qos):
return None
for interface, interface_config in qos['interface'].items():
+ if not os.path.exists(f'/sys/class/net/{interface}'):
+ # When shaper is bound to a dialup (e.g. PPPoE) interface it is
+ # possible that it is yet not availbale when to QoS code runs.
+ # Skip the configuration and inform the user
+ Warning(f'Interface "{interface}" does not exist!')
+ continue
+
for direction in ['egress', 'ingress']:
# bail out early if shaper for given direction is not used at all
if direction not in interface_config:
@@ -238,6 +259,8 @@ def apply(qos):
tmp = shaper_type(interface)
tmp.update(shaper_config, direction)
+ call_dependents()
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index e9afd6a55..95c72df47 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
+# Copyright (C) 2018-2023 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
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import jmespath
from sys import exit
@@ -29,9 +30,92 @@ from vyos import ConfigError
from vyos import airbag
airbag.enable()
+
ipoe_conf = '/run/accel-pppd/ipoe.conf'
ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets'
+
+def get_pools_in_order(data: dict) -> list:
+ """Return a list of dictionaries representing pool data in the order
+ in which they should be allocated. Pool must be defined before we can
+ use it with 'next-pool' option.
+
+ Args:
+ data: A dictionary of pool data, where the keys are pool names and the
+ values are dictionaries containing the 'subnet' key and the optional
+ 'next_pool' key.
+
+ Returns:
+ list: A list of dictionaries
+
+ Raises:
+ ValueError: If a 'next_pool' key references a pool name that
+ has not been defined.
+ ValueError: If a circular reference is found in the 'next_pool' keys.
+
+ Example:
+ config_data = {
+ ... 'first-pool': {
+ ... 'next_pool': 'second-pool',
+ ... 'subnet': '192.0.2.0/25'
+ ... },
+ ... 'second-pool': {
+ ... 'next_pool': 'third-pool',
+ ... 'subnet': '203.0.113.0/25'
+ ... },
+ ... 'third-pool': {
+ ... 'subnet': '198.51.100.0/24'
+ ... },
+ ... 'foo': {
+ ... 'subnet': '100.64.0.0/24',
+ ... 'next_pool': 'second-pool'
+ ... }
+ ... }
+
+ % get_pools_in_order(config_data)
+ [{'third-pool': {'subnet': '198.51.100.0/24'}},
+ {'second-pool': {'next_pool': 'third-pool', 'subnet': '203.0.113.0/25'}},
+ {'first-pool': {'next_pool': 'second-pool', 'subnet': '192.0.2.0/25'}},
+ {'foo': {'next_pool': 'second-pool', 'subnet': '100.64.0.0/24'}}]
+ """
+ pools = []
+ unresolved_pools = {}
+
+ for pool, pool_config in data.items():
+ if 'next_pool' not in pool_config:
+ pools.insert(0, {pool: pool_config})
+ else:
+ unresolved_pools[pool] = pool_config
+
+ while unresolved_pools:
+ resolved_pools = []
+
+ for pool, pool_config in unresolved_pools.items():
+ next_pool_name = pool_config['next_pool']
+
+ if any(p for p in pools if next_pool_name in p):
+ index = next(
+ (i for i, p in enumerate(pools) if next_pool_name in p),
+ None)
+ pools.insert(index + 1, {pool: pool_config})
+ resolved_pools.append(pool)
+ elif next_pool_name in unresolved_pools:
+ # next pool not yet resolved
+ pass
+ else:
+ raise ValueError(
+ f"Pool '{next_pool_name}' not defined in configuration data"
+ )
+
+ if not resolved_pools:
+ raise ValueError("Circular reference in configuration data")
+
+ for pool in resolved_pools:
+ unresolved_pools.pop(pool)
+
+ return pools
+
+
def get_config(config=None):
if config:
conf = config
@@ -43,6 +127,19 @@ def get_config(config=None):
# retrieve common dictionary keys
ipoe = get_accel_dict(conf, base, ipoe_chap_secrets)
+
+ if jmespath.search('client_ip_pool.name', ipoe):
+ dict_named_pools = jmespath.search('client_ip_pool.name', ipoe)
+ # Multiple named pools require ordered values T5099
+ ipoe['ordered_named_pools'] = get_pools_in_order(dict_named_pools)
+ # T5099 'next-pool' option
+ if jmespath.search('client_ip_pool.name.*.next_pool', ipoe):
+ for pool, pool_config in ipoe['client_ip_pool']['name'].items():
+ if 'next_pool' in pool_config:
+ ipoe['first_named_pool'] = pool
+ ipoe['first_named_pool_subnet'] = pool_config
+ break
+
return ipoe
@@ -53,10 +150,24 @@ def verify(ipoe):
if 'interface' not in ipoe:
raise ConfigError('No IPoE interface configured')
- for interface in ipoe['interface']:
+ for interface, iface_config in ipoe['interface'].items():
verify_interface_exists(interface)
+ if 'client_subnet' in iface_config and 'vlan' in iface_config:
+ raise ConfigError('Option "client-subnet" incompatible with "vlan"!'
+ 'Use "ipoe client-ip-pool" instead.')
#verify_accel_ppp_base_service(ipoe, local_users=False)
+ # IPoE server does not have 'gateway' option in the CLI
+ # we cannot use configverify.py verify_accel_ppp_base_service for ipoe-server
+
+ if dict_search('authentication.mode', ipoe) == 'radius':
+ if not dict_search('authentication.radius.server', ipoe):
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for server in dict_search('authentication.radius.server', ipoe):
+ radius_config = ipoe['authentication']['radius']['server'][server]
+ if 'key' not in radius_config:
+ raise ConfigError(f'Missing RADIUS secret key for server "{server}"')
if 'client_ipv6_pool' in ipoe:
if 'delegate' in ipoe['client_ipv6_pool'] and 'prefix' not in ipoe['client_ipv6_pool']:
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index 363408679..47510ce80 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2022 VyOS maintainers and contributors
+# Copyright (C) 2021-2023 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
@@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import socket
import json
from sys import exit
@@ -57,6 +58,13 @@ def get_nft_filter_chains():
return chain_list
+def get_hostname() -> str:
+ try:
+ hostname = socket.getfqdn()
+ except socket.gaierror:
+ hostname = socket.gethostname()
+ return hostname
+
def get_config(config=None):
if config:
conf = config
@@ -79,6 +87,7 @@ def get_config(config=None):
monitoring = dict_merge(default_values, monitoring)
monitoring['custom_scripts_dir'] = custom_scripts_dir
+ monitoring['hostname'] = get_hostname()
monitoring['interfaces_ethernet'] = Section.interfaces('ethernet', vlan=False)
monitoring['nft_chains'] = get_nft_filter_chains()
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 600ba4e92..adeefaa37 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
+# Copyright (C) 2018-2023 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
@@ -71,8 +71,9 @@ def verify(pppoe):
# local ippool and gateway settings config checks
if not (dict_search('client_ip_pool.subnet', pppoe) or
+ (dict_search('client_ip_pool.name', pppoe) or
(dict_search('client_ip_pool.start', pppoe) and
- dict_search('client_ip_pool.stop', pppoe))):
+ dict_search('client_ip_pool.stop', pppoe)))):
print('Warning: No PPPoE client pool defined')
if dict_search('authentication.radius.dynamic_author.server', pppoe):
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index ab2ccf99e..9b7c04eb0 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -92,7 +92,7 @@ def get_config(config=None):
# Always listen on localhost if an explicit address has been configured
# This is a safety measure to not end up with invalid listen addresses
- # that are not configured on this system. See https://phabricator.vyos.net/T850
+ # that are not configured on this system. See https://vyos.dev/T850
if '127.0.0.1' not in snmp['listen_address']:
tmp = {'127.0.0.1': {'port': '161'}}
snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
@@ -103,6 +103,9 @@ def get_config(config=None):
if 'community' in snmp:
default_values = defaults(base + ['community'])
+ if 'network' in default_values:
+ # convert multiple default networks to list
+ default_values['network'] = default_values['network'].split()
for community in snmp['community']:
snmp['community'][community] = dict_merge(
default_values, snmp['community'][community])
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 0c5063ed3..95865c690 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2022 VyOS maintainers and contributors
+# Copyright (C) 2019-2023 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,12 +18,15 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_map
+from vyos.template import render_to_string
from vyos.util import call
from vyos.util import dict_search
from vyos.util import sysctl_write
from vyos.util import write_file
from vyos.xml import defaults
from vyos import ConfigError
+from vyos import frr
from vyos import airbag
airbag.enable()
@@ -40,13 +43,30 @@ def get_config(config=None):
default_values = defaults(base)
opt = dict_merge(default_values, opt)
+ # When working with FRR we need to know the corresponding address-family
+ opt['afi'] = 'ip'
+
+ # We also need the route-map information from the config
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
+ get_first_key=True)}}
+ # Merge policy dict into "regular" config dict
+ opt = dict_merge(tmp, opt)
return opt
def verify(opt):
- pass
+ if 'protocol' in opt:
+ for protocol, protocol_options in opt['protocol'].items():
+ if 'route_map' in protocol_options:
+ verify_route_map(protocol_options['route_map'], opt)
+ return
def generate(opt):
- pass
+ if 'protocol' in opt:
+ opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
+ return
def apply(opt):
# Apply ARP threshold values
@@ -78,6 +98,18 @@ def apply(opt):
value = '1' if (tmp != None) else '0'
sysctl_write('net.ipv4.fib_multipath_hash_policy', value)
+ if 'protocol' in opt:
+ zebra_daemon = 'zebra'
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+
+ # 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'ip protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in opt:
+ frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index 26aacf46b..b6d3a79c3 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2022 VyOS maintainers and contributors
+# Copyright (C) 2019-2023 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,11 +19,14 @@ import os
from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configverify import verify_route_map
+from vyos.template import render_to_string
from vyos.util import dict_search
from vyos.util import sysctl_write
from vyos.util import write_file
from vyos.xml import defaults
from vyos import ConfigError
+from vyos import frr
from vyos import airbag
airbag.enable()
@@ -41,13 +44,30 @@ def get_config(config=None):
default_values = defaults(base)
opt = dict_merge(default_values, opt)
+ # When working with FRR we need to know the corresponding address-family
+ opt['afi'] = 'ipv6'
+
+ # We also need the route-map information from the config
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
+ get_first_key=True)}}
+ # Merge policy dict into "regular" config dict
+ opt = dict_merge(tmp, opt)
return opt
def verify(opt):
- pass
+ if 'protocol' in opt:
+ for protocol, protocol_options in opt['protocol'].items():
+ if 'route_map' in protocol_options:
+ verify_route_map(protocol_options['route_map'], opt)
+ return
def generate(opt):
- pass
+ if 'protocol' in opt:
+ opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
+ return
def apply(opt):
# configure multipath
@@ -78,6 +98,18 @@ def apply(opt):
if name == 'accept_dad':
write_file(os.path.join(root, name), value)
+ if 'protocol' in opt:
+ zebra_daemon = 'zebra'
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+
+ # 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'ipv6 protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
+ if 'frr_zebra_config' in opt:
+ frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index e26b81e3d..fbb013cf3 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-2022 VyOS maintainers and contributors
+# Copyright (C) 2020-2023 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,22 +16,21 @@
import os
-from crypt import crypt
-from crypt import METHOD_SHA512
+from passlib.hosts import linux_context
from psutil import users
from pwd import getpwall
from pwd import getpwnam
-from spwd import getspnam
from sys import exit
from time import sleep
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configverify import verify_vrf
+from vyos.defaults import directories
from vyos.template import render
from vyos.template import is_ipv4
from vyos.util import cmd
-from vyos.util import call
+from vyos.util import call, rc_cmd
from vyos.util import run
from vyos.util import DEVNULL
from vyos.util import dict_search
@@ -41,8 +40,14 @@ from vyos import airbag
airbag.enable()
autologout_file = "/etc/profile.d/autologout.sh"
+limits_file = "/etc/security/limits.d/10-vyos.conf"
radius_config_file = "/etc/pam_radius_auth.conf"
+# LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec
+MAX_RADIUS_TIMEOUT: int = 50
+# MAX_RADIUS_TIMEOUT divided by 2 sec (minimum recomended timeout)
+MAX_RADIUS_COUNT: int = 25
+
def get_local_users():
"""Return list of dynamically allocated users (see Debian Policy Manual)"""
local_users = []
@@ -54,6 +59,13 @@ def get_local_users():
return local_users
+def get_shadow_password(username):
+ with open('/etc/shadow') as f:
+ for user in f.readlines():
+ items = user.split(":")
+ if username == items[0]:
+ return items[1]
+ return None
def get_config(config=None):
if config:
@@ -118,18 +130,27 @@ def verify(login):
if 'radius' in login:
if 'server' not in login['radius']:
raise ConfigError('No RADIUS server defined!')
-
+ sum_timeout: int = 0
+ radius_servers_count: int = 0
fail = True
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:
+ if 'disable' not in server_config:
+ sum_timeout += int(server_config['timeout'])
+ radius_servers_count += 1
fail = False
- continue
+
if fail:
raise ConfigError('All RADIUS servers are disabled')
+ if radius_servers_count > MAX_RADIUS_COUNT:
+ raise ConfigError('Number of RADIUS servers more than 25 ')
+
+ if sum_timeout > MAX_RADIUS_TIMEOUT:
+ raise ConfigError('Sum of RADIUS servers timeouts '
+ 'has to be less or eq 50 sec')
+
verify_vrf(login['radius'])
if 'source_address' in login['radius']:
@@ -144,6 +165,9 @@ def verify(login):
if ipv6_count > 1:
raise ConfigError('Only one IPv6 source-address can be set!')
+ if 'max_login_session' in login and 'timeout' not in login:
+ raise ConfigError('"login timeout" must be configured!')
+
return None
@@ -153,13 +177,13 @@ def generate(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)
+ encrypted_password = linux_context.hash(tmp)
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'
+ env['vyos_libexec_dir'] = directories['base']
# Set default commands for re-adding user with encrypted password
del_user_plain = f"system login user '{user}' authentication plaintext-password"
@@ -183,10 +207,12 @@ def generate(login):
add_user_encrypt = " ".join(add_user_encrypt)
call(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env)
- call(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env)
+ ret, out = rc_cmd(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env)
+ if ret:
+ raise ConfigError(out)
else:
try:
- if getspnam(user).sp_pwdp == dict_search('authentication.encrypted_password', user_config):
+ if get_shadow_password(user) == 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.
@@ -204,6 +230,14 @@ def generate(login):
if os.path.isfile(radius_config_file):
os.unlink(radius_config_file)
+ # /etc/security/limits.d/10-vyos.conf
+ if 'max_login_session' in login:
+ render(limits_file, 'login/limits.j2', login,
+ permission=0o644, user='root', group='root')
+ else:
+ if os.path.isfile(limits_file):
+ os.unlink(limits_file)
+
if 'timeout' in login:
render(autologout_file, 'login/autologout.j2', login,
permission=0o755, user='root', group='root')
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index 20132456c..e646fb0ae 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2023 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
@@ -15,253 +15,129 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
-import re
-from pathlib import Path
from sys import exit
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import run
+from vyos.configdict import dict_merge
+from vyos.configdict import is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.util import call
from vyos.template import render
-
+from vyos.xml import defaults
+from vyos import ConfigError
from vyos import airbag
airbag.enable()
+rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf'
+logrotate_conf = '/etc/logrotate.d/vyos-rsyslog'
+systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf'
+
def get_config(config=None):
if config:
- c = config
+ conf = config
else:
- c = Config()
- if not c.exists('system syslog'):
+ conf = Config()
+ base = ['system', 'syslog']
+ if not conf.exists(base):
return None
- c.set_level('system syslog')
-
- config_data = {
- 'files': {},
- 'console': {},
- 'hosts': {},
- 'user': {}
- }
-
- #
- # /etc/rsyslog.d/vyos-rsyslog.conf
- # 'set system syslog global'
- #
- config_data['files'].update(
- {
- 'global': {
- 'log-file': '/var/log/messages',
- 'selectors': '*.notice;local7.debug',
- 'max-files': '5',
- 'preserver_fqdn': False
- }
- }
- )
-
- if c.exists('global marker'):
- config_data['files']['global']['marker'] = True
- if c.exists('global marker interval'):
- config_data['files']['global'][
- 'marker-interval'] = c.return_value('global marker interval')
- if c.exists('global facility'):
- config_data['files']['global'][
- 'selectors'] = generate_selectors(c, 'global facility')
- if c.exists('global archive size'):
- config_data['files']['global']['max-size'] = int(
- c.return_value('global archive size')) * 1024
- if c.exists('global archive file'):
- config_data['files']['global'][
- 'max-files'] = c.return_value('global archive file')
- if c.exists('global preserve-fqdn'):
- config_data['files']['global']['preserver_fqdn'] = True
-
- #
- # set system syslog file
- #
-
- if c.exists('file'):
- filenames = c.list_nodes('file')
- for filename in filenames:
- config_data['files'].update(
- {
- filename: {
- 'log-file': '/var/log/user/' + filename,
- 'max-files': '5',
- 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog-generated-' + filename,
- 'selectors': '*.err',
- 'max-size': 262144
- }
- }
- )
-
- if c.exists('file ' + filename + ' facility'):
- config_data['files'][filename]['selectors'] = generate_selectors(
- c, 'file ' + filename + ' facility')
- if c.exists('file ' + filename + ' archive size'):
- config_data['files'][filename]['max-size'] = int(
- c.return_value('file ' + filename + ' archive size')) * 1024
- if c.exists('file ' + filename + ' archive files'):
- config_data['files'][filename]['max-files'] = c.return_value(
- 'file ' + filename + ' archive files')
-
- # set system syslog console
- if c.exists('console'):
- config_data['console'] = {
- '/dev/console': {
- 'selectors': '*.err'
- }
- }
-
- for f in c.list_nodes('console facility'):
- if c.exists('console facility ' + f + ' level'):
- config_data['console'] = {
- '/dev/console': {
- 'selectors': generate_selectors(c, 'console facility')
- }
- }
- # set system syslog host
- if c.exists('host'):
- rhosts = c.list_nodes('host')
- proto = 'udp'
- for rhost in rhosts:
- for fac in c.list_nodes('host ' + rhost + ' facility'):
- if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'):
- proto = c.return_value(
- 'host ' + rhost + ' facility ' + fac + ' protocol')
- else:
- proto = 'udp'
-
- config_data['hosts'].update(
- {
- rhost: {
- 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'),
- 'proto': proto
- }
- }
- )
- if c.exists('host ' + rhost + ' port'):
- config_data['hosts'][rhost][
- 'port'] = c.return_value(['host', rhost, 'port'])
-
- # set system syslog host x.x.x.x format octet-counted
- if c.exists('host ' + rhost + ' format octet-counted'):
- config_data['hosts'][rhost]['oct_count'] = True
- else:
- config_data['hosts'][rhost]['oct_count'] = False
-
- # set system syslog user
- if c.exists('user'):
- usrs = c.list_nodes('user')
- for usr in usrs:
- config_data['user'].update(
- {
- usr: {
- 'selectors': generate_selectors(c, 'user ' + usr + ' facility')
- }
- }
- )
-
- return config_data
-
-
-def generate_selectors(c, config_node):
-# protocols and security are being mapped here
-# for backward compatibility with old configs
-# security and protocol mappings can be removed later
- nodes = c.list_nodes(config_node)
- selectors = ""
- for node in nodes:
- lvl = c.return_value(config_node + ' ' + node + ' level')
- if lvl == None:
- lvl = "err"
- if lvl == 'all':
- lvl = '*'
- if node == 'all' and node != nodes[-1]:
- selectors += "*." + lvl + ";"
- elif node == 'all':
- selectors += "*." + lvl
- elif node != nodes[-1]:
- if node == 'protocols':
- node = 'local7'
- if node == 'security':
- node = 'auth'
- selectors += node + "." + lvl + ";"
- else:
- if node == 'protocols':
- node = 'local7'
- if node == 'security':
- node = 'auth'
- selectors += node + "." + lvl
- return selectors
-
-
-def generate(c):
- if c == None:
+ syslog = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ syslog.update({ 'logrotate' : logrotate_conf })
+ tmp = is_node_changed(conf, base + ['vrf'])
+ if tmp: syslog.update({'restart_required': {}})
+
+ # 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)
+ # XXX: some syslog default values can not be merged here (originating from
+ # a tagNode - remove and add them later per individual tagNode instance
+ if 'console' in default_values:
+ del default_values['console']
+ for entity in ['global', 'user', 'host', 'file']:
+ if entity in default_values:
+ del default_values[entity]
+
+ syslog = dict_merge(default_values, syslog)
+
+ # XXX: add defaults for "console" tree
+ if 'console' in syslog and 'facility' in syslog['console']:
+ default_values = defaults(base + ['console', 'facility'])
+ for facility in syslog['console']['facility']:
+ syslog['console']['facility'][facility] = dict_merge(default_values,
+ syslog['console']['facility'][facility])
+
+ # XXX: add defaults for "host" tree
+ if 'host' in syslog:
+ default_values_host = defaults(base + ['host'])
+ if 'facility' in default_values_host:
+ del default_values_host['facility']
+ default_values_facility = defaults(base + ['host', 'facility'])
+
+ for host, host_config in syslog['host'].items():
+ syslog['host'][host] = dict_merge(default_values_host, syslog['host'][host])
+ if 'facility' in host_config:
+ for facility in host_config['facility']:
+ syslog['host'][host]['facility'][facility] = dict_merge(default_values_facility,
+ syslog['host'][host]['facility'][facility])
+
+ # XXX: add defaults for "user" tree
+ if 'user' in syslog:
+ default_values = defaults(base + ['user', 'facility'])
+ for user, user_config in syslog['user'].items():
+ if 'facility' in user_config:
+ for facility in user_config['facility']:
+ syslog['user'][user]['facility'][facility] = dict_merge(default_values,
+ syslog['user'][user]['facility'][facility])
+
+ # XXX: add defaults for "file" tree
+ if 'file' in syslog:
+ default_values = defaults(base + ['file'])
+ for file, file_config in syslog['file'].items():
+ for facility in file_config['facility']:
+ syslog['file'][file]['facility'][facility] = dict_merge(default_values,
+ syslog['file'][file]['facility'][facility])
+
+ return syslog
+
+def verify(syslog):
+ if not syslog:
return None
- conf = '/etc/rsyslog.d/vyos-rsyslog.conf'
- render(conf, 'syslog/rsyslog.conf.j2', c)
-
- # cleanup current logrotate config files
- logrotate_files = Path('/etc/logrotate.d/').glob('vyos-rsyslog-generated-*')
- for file in logrotate_files:
- file.unlink()
+ verify_vrf(syslog)
- # eventually write for each file its own logrotate file, since size is
- # defined it shouldn't matter
- for filename, fileconfig in c.get('files', {}).items():
- if fileconfig['log-file'].startswith('/var/log/user/'):
- conf = '/etc/logrotate.d/vyos-rsyslog-generated-' + filename
- render(conf, 'syslog/logrotate.j2', { 'config_render': fileconfig })
+def generate(syslog):
+ if not syslog:
+ if os.path.exists(rsyslog_conf):
+ os.path.unlink(rsyslog_conf)
+ if os.path.exists(logrotate_conf):
+ os.path.unlink(logrotate_conf)
-
-def verify(c):
- if c == None:
return None
- # may be obsolete
- # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf)
- # it interferes with the global logging, to make sure we are using a single base, template is enforced here
- #
- if not os.path.islink('/etc/rsyslog.conf'):
- os.remove('/etc/rsyslog.conf')
- os.symlink(
- '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf')
+ render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog)
+ render(systemd_override, 'rsyslog/override.conf.j2', syslog)
+ render(logrotate_conf, 'rsyslog/logrotate.j2', syslog)
- # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there
- # is a chance that someone still needs it, so I don't automatically remove
- # them
- #
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+ return None
- if c == None:
+def apply(syslog):
+ systemd_service = 'syslog.service'
+ if not syslog:
+ call(f'systemctl stop {systemd_service}')
return None
- fac = [
- '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security',
- 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7']
- lvl = ['emerg', 'alert', 'crit', 'err',
- 'warning', 'notice', 'info', 'debug', '*']
-
- for conf in c:
- if c[conf]:
- for item in c[conf]:
- for s in c[conf][item]['selectors'].split(";"):
- f = re.sub("\..*$", "", s)
- if f not in fac:
- raise ConfigError(
- 'Invalid facility ' + s + ' set in ' + conf + ' ' + item)
- l = re.sub("^.+\.", "", s)
- if l not in lvl:
- raise ConfigError(
- 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item)
-
+ # we need to restart the service if e.g. the VRF name changed
+ systemd_action = 'reload-or-restart'
+ if 'restart_required' in syslog:
+ systemd_action = 'restart'
-def apply(c):
- if not c:
- return run('systemctl stop syslog.service')
- return run('systemctl restart syslog.service')
+ call(f'systemctl {systemd_action} {systemd_service}')
+ return None
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py
new file mode 100755
index 000000000..a0c3fca7f
--- /dev/null
+++ b/src/conf_mode/system_sflow.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2023 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.util import call
+from vyos.validate import is_addr_assigned
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+hsflowd_conf_path = '/run/sflow/hsflowd.conf'
+systemd_service = 'hsflowd.service'
+systemd_override = f'/run/systemd/system/{systemd_service}.d/override.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['system', 'sflow']
+ if not conf.exists(base):
+ return None
+
+ sflow = conf.get_config_dict(base,
+ key_mangling=('-', '_'),
+ get_first_key=True)
+
+ # 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)
+
+ sflow = dict_merge(default_values, sflow)
+
+ # Ignore default XML values if config doesn't exists
+ # Delete key from dict
+ if 'port' in sflow['server']:
+ del sflow['server']['port']
+
+ # Set default values per server
+ if 'server' in sflow:
+ for server in sflow['server']:
+ default_values = defaults(base + ['server'])
+ sflow['server'][server] = dict_merge(default_values, sflow['server'][server])
+
+ return sflow
+
+
+def verify(sflow):
+ if not sflow:
+ return None
+
+ # Check if configured sflow agent-address exist in the system
+ if 'agent_address' in sflow:
+ tmp = sflow['agent_address']
+ if not is_addr_assigned(tmp):
+ raise ConfigError(
+ f'Configured "sflow agent-address {tmp}" does not exist in the system!'
+ )
+
+ # Check if at least one interface is configured
+ if 'interface' not in sflow:
+ raise ConfigError(
+ 'sFlow requires at least one interface to be configured!')
+
+ # Check if at least one server is configured
+ if 'server' not in sflow:
+ raise ConfigError('You need to configure at least one sFlow server!')
+
+ # return True if all checks were passed
+ return True
+
+
+def generate(sflow):
+ if not sflow:
+ return None
+
+ render(hsflowd_conf_path, 'sflow/hsflowd.conf.j2', sflow)
+ render(systemd_override, 'sflow/override.conf.j2', sflow)
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+
+def apply(sflow):
+ if not sflow:
+ # Stop flow-accounting daemon and remove configuration file
+ call(f'systemctl stop {systemd_service}')
+ if os.path.exists(hsflowd_conf_path):
+ os.unlink(hsflowd_conf_path)
+ return
+
+ # Start/reload flow-accounting daemon
+ call(f'systemctl restart {systemd_service}')
+
+
+if __name__ == '__main__':
+ try:
+ config = get_config()
+ verify(config)
+ generate(config)
+ apply(config)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 3af2af4d9..63887b278 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2022 VyOS maintainers and contributors
+# Copyright (C) 2021-2023 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 ipaddress
import os
import re
+import jmespath
from sys import exit
from time import sleep
@@ -52,8 +53,6 @@ dhcp_wait_attempts = 2
dhcp_wait_sleep = 1
swanctl_dir = '/etc/swanctl'
-ipsec_conf = '/etc/ipsec.conf'
-ipsec_secrets = '/etc/ipsec.secrets'
charon_conf = '/etc/strongswan.d/charon.conf'
charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf'
@@ -219,6 +218,12 @@ def verify(ipsec):
if not ipsec:
return None
+ if 'authentication' in ipsec:
+ if 'psk' in ipsec['authentication']:
+ for psk, psk_config in ipsec['authentication']['psk'].items():
+ if 'id' not in psk_config or 'secret' not in psk_config:
+ raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"')
+
if 'interfaces' in ipsec :
for ifname in ipsec['interface']:
verify_interface_exists(ifname)
@@ -535,8 +540,7 @@ def generate(ipsec):
cleanup_pki_files()
if not ipsec:
- for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf,
- charon_radius_conf, interface_conf, swanctl_conf]:
+ for config_file in [charon_dhcp_conf, charon_radius_conf, interface_conf, swanctl_conf]:
if os.path.isfile(config_file):
os.unlink(config_file)
render(charon_conf, 'ipsec/charon.j2', {'install_routes': default_install_routes})
@@ -545,6 +549,8 @@ def generate(ipsec):
if ipsec['dhcp_no_address']:
with open(DHCP_HOOK_IFLIST, 'w') as f:
f.write(" ".join(ipsec['dhcp_no_address'].values()))
+ elif os.path.exists(DHCP_HOOK_IFLIST):
+ os.unlink(DHCP_HOOK_IFLIST)
for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH, PUBKEY_PATH]:
if not os.path.exists(path):
@@ -602,9 +608,15 @@ def generate(ipsec):
ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough
+ # auth psk <tag> dhcp-interface <xxx>
+ if jmespath.search('authentication.psk.*.dhcp_interface', ipsec):
+ for psk, psk_config in ipsec['authentication']['psk'].items():
+ if 'dhcp_interface' in psk_config:
+ for iface in psk_config['dhcp_interface']:
+ id = get_dhcp_address(iface)
+ if id:
+ ipsec['authentication']['psk'][psk]['id'].append(id)
- render(ipsec_conf, 'ipsec/ipsec.conf.j2', ipsec)
- render(ipsec_secrets, 'ipsec/ipsec.secrets.j2', ipsec)
render(charon_conf, 'ipsec/charon.j2', ipsec)
render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec)
render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec)
@@ -619,25 +631,12 @@ def resync_nhrp(ipsec):
if tmp > 0:
print('ERROR: failed to reapply NHRP settings!')
-def wait_for_vici_socket(timeout=5, sleep_interval=0.1):
- start_time = time()
- test_command = f'sudo socat -u OPEN:/dev/null UNIX-CONNECT:{vici_socket}'
- while True:
- if (start_time + timeout) < time():
- return None
- result = run(test_command)
- if result == 0:
- return True
- sleep(sleep_interval)
-
def apply(ipsec):
- systemd_service = 'strongswan-starter.service'
+ systemd_service = 'strongswan.service'
if not ipsec:
call(f'systemctl stop {systemd_service}')
else:
call(f'systemctl reload-or-restart {systemd_service}')
- if wait_for_vici_socket():
- call('sudo swanctl -q')
resync_nhrp(ipsec)
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index 65623c2b1..ffac3b023 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2020 VyOS maintainers and contributors
+# Copyright (C) 2019-2023 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
@@ -63,6 +63,7 @@ default_config_data = {
'ppp_ipv6_peer_intf_id': None,
'radius_server': [],
'radius_acct_inter_jitter': '',
+ 'radius_acct_interim_interval': None,
'radius_acct_tmo': '3',
'radius_max_try': '3',
'radius_timeout': '3',
@@ -190,6 +191,9 @@ def get_config(config=None):
# advanced radius-setting
conf.set_level(base_path + ['authentication', 'radius'])
+ if conf.exists(['accounting-interim-interval']):
+ l2tp['radius_acct_interim_interval'] = conf.return_value(['accounting-interim-interval'])
+
if conf.exists(['acct-interim-jitter']):
l2tp['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter'])
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index 16a5b5952..7b3325d95 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
+# Copyright (C) 2018-2023 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
@@ -46,8 +46,70 @@ radius_servers = cfg_dir + '/radius_servers'
def get_hash(password):
return crypt(password, mksalt(METHOD_SHA512))
-def get_config():
- conf = Config()
+
+def _default_dict_cleanup(origin: dict, default_values: dict) -> dict:
+ """
+ https://vyos.dev/T2665
+ Clear unnecessary key values in merged config by dict_merge function
+ :param origin: config
+ :type origin: dict
+ :param default_values: default values
+ :type default_values: dict
+ :return: merged dict
+ :rtype: dict
+ """
+ if 'mode' in origin["authentication"] and "local" in \
+ origin["authentication"]["mode"]:
+ del origin['authentication']['local_users']['username']['otp']
+ if not origin["authentication"]["local_users"]["username"]:
+ raise ConfigError(
+ 'Openconnect authentication mode local requires at least one user')
+ default_ocserv_usr_values = \
+ default_values['authentication']['local_users']['username']['otp']
+ for user, params in origin['authentication']['local_users'][
+ 'username'].items():
+ # Not every configuration requires OTP settings
+ if origin['authentication']['local_users']['username'][user].get(
+ 'otp'):
+ origin['authentication']['local_users']['username'][user][
+ 'otp'] = dict_merge(default_ocserv_usr_values,
+ origin['authentication'][
+ 'local_users']['username'][user][
+ 'otp'])
+
+ if 'mode' in origin["authentication"] and "radius" in \
+ origin["authentication"]["mode"]:
+ del origin['authentication']['radius']['server']['port']
+ if not origin["authentication"]['radius']['server']:
+ raise ConfigError(
+ 'Openconnect authentication mode radius requires at least one RADIUS server')
+ default_values_radius_port = \
+ default_values['authentication']['radius']['server']['port']
+ for server, params in origin['authentication']['radius'][
+ 'server'].items():
+ if 'port' not in params:
+ params['port'] = default_values_radius_port
+
+ if 'mode' in origin["accounting"] and "radius" in \
+ origin["accounting"]["mode"]:
+ del origin['accounting']['radius']['server']['port']
+ if not origin["accounting"]['radius']['server']:
+ raise ConfigError(
+ 'Openconnect accounting mode radius requires at least one RADIUS server')
+ default_values_radius_port = \
+ default_values['accounting']['radius']['server']['port']
+ for server, params in origin['accounting']['radius'][
+ 'server'].items():
+ if 'port' not in params:
+ params['port'] = default_values_radius_port
+ return origin
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
base = ['vpn', 'openconnect']
if not conf.exists(base):
return None
@@ -57,18 +119,8 @@ def get_config():
# options which we need to update into the dictionary retrived.
default_values = defaults(base)
ocserv = dict_merge(default_values, ocserv)
-
- if 'mode' in ocserv["authentication"] and "local" in ocserv["authentication"]["mode"]:
- # workaround a "know limitation" - https://phabricator.vyos.net/T2665
- del ocserv['authentication']['local_users']['username']['otp']
- if not ocserv["authentication"]["local_users"]["username"]:
- raise ConfigError('openconnect mode local required at least one user')
- default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp']
- for user, params in ocserv['authentication']['local_users']['username'].items():
- # Not every configuration requires OTP settings
- if ocserv['authentication']['local_users']['username'][user].get('otp'):
- ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp'])
-
+ # workaround a "know limitation" - https://vyos.dev/T2665
+ ocserv = _default_dict_cleanup(ocserv, default_values)
if ocserv:
ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
@@ -85,6 +137,14 @@ def verify(ocserv):
not is_listen_port_bind_service(int(port), 'ocserv-main'):
raise ConfigError(f'"{proto}" port "{port}" is used by another service')
+ # Check accounting
+ if "accounting" in ocserv:
+ if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]:
+ if "authentication" not in ocserv or "mode" not in ocserv["authentication"]:
+ raise ConfigError('Accounting depends on OpenConnect authentication configuration')
+ elif "radius" not in ocserv["authentication"]["mode"]:
+ raise ConfigError('RADIUS accounting must be used with RADIUS authentication')
+
# Check authentication
if "authentication" in ocserv:
if "mode" in ocserv["authentication"]:
@@ -177,10 +237,18 @@ def generate(ocserv):
return None
if "radius" in ocserv["authentication"]["mode"]:
- # Render radius client configuration
- render(radius_cfg, 'ocserv/radius_conf.j2', ocserv["authentication"]["radius"])
- # Render radius servers
- render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"])
+ if dict_search(ocserv, 'accounting.mode.radius'):
+ # Render radius client configuration
+ render(radius_cfg, 'ocserv/radius_conf.j2', ocserv)
+ merged_servers = ocserv["accounting"]["radius"]["server"] | ocserv["authentication"]["radius"]["server"]
+ # Render radius servers
+ # Merge the accounting and authentication servers into a single dictionary
+ render(radius_servers, 'ocserv/radius_servers.j2', {'server': merged_servers})
+ else:
+ # Render radius client configuration
+ render(radius_cfg, 'ocserv/radius_conf.j2', ocserv)
+ # Render radius servers
+ render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"])
elif "local" in ocserv["authentication"]["mode"]:
# if mode "OTP", generate OTP users file parameters
if "otp" in ocserv["authentication"]["mode"]["local"]:
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index 7550c411e..b9d18110a 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
+# Copyright (C) 2018-2023 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
@@ -37,6 +37,7 @@ default_pptp = {
'local_users' : [],
'radius_server' : [],
'radius_acct_inter_jitter': '',
+ 'radius_acct_interim_interval': None,
'radius_acct_tmo' : '30',
'radius_max_try' : '3',
'radius_timeout' : '30',
@@ -44,6 +45,8 @@ default_pptp = {
'radius_nas_ip' : '',
'radius_source_address' : '',
'radius_shaper_attr' : '',
+ 'radius_shaper_enable': False,
+ 'radius_shaper_multiplier': '',
'radius_shaper_vendor': '',
'radius_dynamic_author' : '',
'chap_secrets_file': pptp_chap_secrets, # used in Jinja2 template
@@ -143,6 +146,9 @@ def get_config(config=None):
# advanced radius-setting
conf.set_level(base_path + ['authentication', 'radius'])
+ if conf.exists(['accounting-interim-interval']):
+ pptp['radius_acct_interim_interval'] = conf.return_value(['accounting-interim-interval'])
+
if conf.exists(['acct-interim-jitter']):
pptp['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter'])
@@ -183,15 +189,18 @@ def get_config(config=None):
pptp['radius_dynamic_author'] = dae
+ # Rate limit
+ if conf.exists(['rate-limit', 'attribute']):
+ pptp['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute'])
+
if conf.exists(['rate-limit', 'enable']):
- pptp['radius_shaper_attr'] = 'Filter-Id'
- c_attr = ['rate-limit', 'enable', 'attribute']
- if conf.exists(c_attr):
- pptp['radius_shaper_attr'] = conf.return_value(c_attr)
-
- c_vendor = ['rate-limit', 'enable', 'vendor']
- if conf.exists(c_vendor):
- pptp['radius_shaper_vendor'] = conf.return_value(c_vendor)
+ pptp['radius_shaper_enable'] = True
+
+ if conf.exists(['rate-limit', 'multiplier']):
+ pptp['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier'])
+
+ if conf.exists(['rate-limit', 'vendor']):
+ pptp['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor'])
conf.set_level(base_path)
if conf.exists(['client-ip-pool']):
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index c17cca3bd..0b983293e 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -20,9 +20,12 @@ from sys import exit
from json import loads
from vyos.config import Config
+from vyos.configdict import dict_merge
from vyos.configdict import node_changed
+from vyos.configverify import verify_route_map
from vyos.ifconfig import Interface
from vyos.template import render
+from vyos.template import render_to_string
from vyos.util import call
from vyos.util import cmd
from vyos.util import dict_search
@@ -99,6 +102,20 @@ def get_config(config=None):
routes = vrf_routing(conf, name)
if routes: vrf['vrf_remove'][name]['route'] = routes
+ # We also need the route-map information from the config
+ #
+ # XXX: one MUST always call this without the key_mangling() option! See
+ # vyos.configverify.verify_common_route_maps() for more information.
+ tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
+ get_first_key=True)}}
+
+ # L3VNI setup is done via vrf_vni.py as it must be de-configured (on node
+ # deletetion prior to the BGP process. Tell the Jinja2 template no VNI
+ # setup is needed
+ vrf.update({'no_vni' : ''})
+
+ # Merge policy dict into "regular" config dict
+ vrf = dict_merge(tmp, vrf)
return vrf
def verify(vrf):
@@ -113,38 +130,53 @@ def verify(vrf):
f'static routes installed!')
if 'name' in vrf:
- reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type",
- "vrf"]
+ reserved_names = ["add", "all", "broadcast", "default", "delete", "dev",
+ "get", "inet", "mtu", "link", "type", "vrf"]
table_ids = []
- for name, config in vrf['name'].items():
+ for name, vrf_config in vrf['name'].items():
# Reserved VRF names
if name in reserved_names:
raise ConfigError(f'VRF name "{name}" is reserved and connot be used!')
# table id is mandatory
- if 'table' not in config:
+ if 'table' not in vrf_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']:
+ if tmp and tmp != vrf_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:
+ # VRF routing table ID must be unique on the system
+ if 'table' in vrf_config and vrf_config['table'] in table_ids:
raise ConfigError(f'VRF "{name}" table id is not unique!')
- table_ids.append(config['table'])
+ table_ids.append(vrf_config['table'])
+
+ tmp = dict_search('ip.protocol', vrf_config)
+ if tmp != None:
+ for protocol, protocol_options in tmp.items():
+ if 'route_map' in protocol_options:
+ verify_route_map(protocol_options['route_map'], vrf)
+
+ tmp = dict_search('ipv6.protocol', vrf_config)
+ if tmp != None:
+ for protocol, protocol_options in tmp.items():
+ if 'route_map' in protocol_options:
+ verify_route_map(protocol_options['route_map'], vrf)
return None
def generate(vrf):
+ # Render iproute2 VR helper names
render(config_file, 'iproute2/vrf.conf.j2', vrf)
# Render nftables zones config
render(nft_vrf_config, 'firewall/nftables-vrf-zones.j2', vrf)
- return None
+ # Render VRF Kernel/Zebra route-map filters
+ vrf['frr_zebra_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf)
+ return None
def apply(vrf):
# Documentation
@@ -249,6 +281,17 @@ def apply(vrf):
nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}'
cmd(f'nft {nft_add_element}')
+ # Apply FRR filters
+ zebra_daemon = 'zebra'
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True)
+ if 'frr_zebra_config' in vrf:
+ frr_cfg.add_before(frr.default_add_before, vrf['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
# return to default lookup preference when no VRF is configured
if 'name' not in vrf:
diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py
index 585fdbebf..9f33536e5 100755..100644
--- a/src/conf_mode/vrf_vni.py
+++ b/src/conf_mode/vrf_vni.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2021 VyOS maintainers and contributors
+# Copyright (C) 2023 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,36 +19,75 @@ from sys import exit
from vyos.config import Config
from vyos.template import render_to_string
+from vyos.util import dict_search
from vyos import ConfigError
from vyos import frr
from vyos import airbag
airbag.enable()
-frr_daemon = 'zebra'
-
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['vrf']
- vrf = conf.get_config_dict(base, get_first_key=True)
+ vrf_name = None
+ if len(argv) > 1:
+ vrf_name = argv[1]
+ else:
+ return None
+
+ # Using duplicate L3VNIs makes no sense - it's also forbidden in FRR,
+ # thus VyOS CLI must deny this, too. Instead of getting only the dict for
+ # the requested VRF and den comparing it with depenent VRfs to not have any
+ # duplicate we will just grad ALL VRFs by default but only render/apply
+ # the configuration for the requested VRF - that makes the code easier and
+ # hopefully less error prone
+ vrf = conf.get_config_dict(['vrf'], key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
+
+ # Store name of VRF we are interested in for FRR config rendering
+ vrf.update({'only_vrf' : vrf_name})
+
return vrf
def verify(vrf):
+ if not vrf:
+ return
+
+ if len(argv) < 2:
+ raise ConfigError('VRF parameter not specified when valling vrf_vni.py')
+
+ if 'name' in vrf:
+ vni_ids = []
+ for name, vrf_config in vrf['name'].items():
+ # VRF VNI (Virtual Network Identifier) must be unique on the system
+ if 'vni' in vrf_config:
+ if vrf_config['vni'] in vni_ids:
+ raise ConfigError(f'VRF "{name}" VNI is not unique!')
+ vni_ids.append(vrf_config['vni'])
+
return None
def generate(vrf):
- vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.j2', vrf)
+ if not vrf:
+ return
+
+ vrf['new_frr_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf)
return None
def apply(vrf):
+ frr_daemon = 'zebra'
+
# add configuration to FRR
frr_cfg = frr.FRRConfig()
frr_cfg.load_configuration(frr_daemon)
- frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True)
- if 'new_frr_config' in vrf:
+ # There is only one VRF inside the dict as we read only one in get_config()
+ if vrf and 'only_vrf' in vrf:
+ vrf_name = vrf['only_vrf']
+ frr_cfg.modify_section(f'^vrf {vrf_name}', stop_pattern='^exit-vrf', remove_stop_mark=True)
+ if vrf and 'new_frr_config' in vrf:
frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config'])
frr_cfg.commit_configuration(frr_daemon)