summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/arp.py2
-rwxr-xr-xsrc/conf_mode/bcast_relay.py20
-rwxr-xr-xsrc/conf_mode/conntrack.py24
-rwxr-xr-xsrc/conf_mode/conntrack_sync.py20
-rwxr-xr-xsrc/conf_mode/container.py100
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py20
-rwxr-xr-xsrc/conf_mode/dhcp_server.py28
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py24
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py6
-rwxr-xr-xsrc/conf_mode/dns_dynamic.py134
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py69
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py156
-rwxr-xr-xsrc/conf_mode/firewall.py283
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py54
-rwxr-xr-xsrc/conf_mode/high-availability.py47
-rwxr-xr-xsrc/conf_mode/host_name.py9
-rwxr-xr-xsrc/conf_mode/http-api.py14
-rwxr-xr-xsrc/conf_mode/https.py8
-rwxr-xr-xsrc/conf_mode/igmp_proxy.py19
-rwxr-xr-xsrc/conf_mode/intel_qat.py5
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py10
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py28
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py25
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py4
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py80
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py125
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py4
-rwxr-xr-xsrc/conf_mode/interfaces-sstpc.py8
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py10
-rwxr-xr-xsrc/conf_mode/interfaces-vti.py2
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py4
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py13
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py61
-rwxr-xr-xsrc/conf_mode/interfaces-wwan.py13
-rwxr-xr-xsrc/conf_mode/le_cert.py6
-rwxr-xr-xsrc/conf_mode/lldp.py41
-rwxr-xr-xsrc/conf_mode/load-balancing-haproxy.py20
-rwxr-xr-xsrc/conf_mode/load-balancing-wan.py53
-rwxr-xr-xsrc/conf_mode/nat.py49
-rwxr-xr-xsrc/conf_mode/nat66.py18
-rwxr-xr-xsrc/conf_mode/netns.py6
-rwxr-xr-xsrc/conf_mode/ntp.py40
-rwxr-xr-xsrc/conf_mode/pki.py9
-rwxr-xr-xsrc/conf_mode/policy-local-route.py2
-rwxr-xr-xsrc/conf_mode/policy-route.py8
-rwxr-xr-xsrc/conf_mode/policy.py2
-rwxr-xr-xsrc/conf_mode/protocols_babel.py21
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py17
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py45
-rwxr-xr-xsrc/conf_mode/protocols_failover.py15
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py5
-rwxr-xr-xsrc/conf_mode/protocols_isis.py18
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py6
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py6
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py86
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py25
-rwxr-xr-xsrc/conf_mode/protocols_pim.py5
-rwxr-xr-xsrc/conf_mode/protocols_rip.py7
-rwxr-xr-xsrc/conf_mode/protocols_ripng.py7
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py7
-rwxr-xr-xsrc/conf_mode/protocols_static.py2
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py2
-rwxr-xr-xsrc/conf_mode/qos.py115
-rwxr-xr-xsrc/conf_mode/salt-minion.py9
-rwxr-xr-xsrc/conf_mode/service_config_sync.py105
-rwxr-xr-xsrc/conf_mode/service_console-server.py10
-rwxr-xr-xsrc/conf_mode/service_event_handler.py3
-rwxr-xr-xsrc/conf_mode/service_ids_fastnetmon.py26
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py4
-rwxr-xr-xsrc/conf_mode/service_mdns-repeater.py2
-rwxr-xr-xsrc/conf_mode/service_monitoring_telegraf.py11
-rwxr-xr-xsrc/conf_mode/service_monitoring_zabbix-agent.py98
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py4
-rwxr-xr-xsrc/conf_mode/service_router-advert.py41
-rwxr-xr-xsrc/conf_mode/service_sla.py12
-rwxr-xr-xsrc/conf_mode/service_upnp.py9
-rwxr-xr-xsrc/conf_mode/service_webproxy.py27
-rwxr-xr-xsrc/conf_mode/snmp.py71
-rwxr-xr-xsrc/conf_mode/ssh.py8
-rwxr-xr-xsrc/conf_mode/system-ip.py62
-rwxr-xr-xsrc/conf_mode/system-ipv6.py38
-rwxr-xr-xsrc/conf_mode/system-login-banner.py2
-rwxr-xr-xsrc/conf_mode/system-login.py173
-rwxr-xr-xsrc/conf_mode/system-logs.py12
-rwxr-xr-xsrc/conf_mode/system-option.py31
-rwxr-xr-xsrc/conf_mode/system-syslog.py58
-rwxr-xr-xsrc/conf_mode/system-timezone.py2
-rwxr-xr-xsrc/conf_mode/system_console.py18
-rwxr-xr-xsrc/conf_mode/system_frr.py6
-rwxr-xr-xsrc/conf_mode/system_lcd.py4
-rwxr-xr-xsrc/conf_mode/system_sflow.py29
-rwxr-xr-xsrc/conf_mode/system_sysctl.py2
-rwxr-xr-xsrc/conf_mode/system_update_check.py2
-rwxr-xr-xsrc/conf_mode/tftp_server.py16
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py100
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py8
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py99
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py3
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py10
-rwxr-xr-xsrc/conf_mode/vpp.py207
-rwxr-xr-xsrc/conf_mode/vrf.py35
-rw-r--r--src/conf_mode/vrf_vni.py2
102 files changed, 1612 insertions, 1819 deletions
diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py
index 7dc5206e0..b141f1141 100755
--- a/src/conf_mode/arp.py
+++ b/src/conf_mode/arp.py
@@ -18,7 +18,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import node_changed
-from vyos.util import call
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
index 39a2971ce..31c552f5a 100755
--- a/src/conf_mode/bcast_relay.py
+++ b/src/conf_mode/bcast_relay.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2017-2022 VyOS maintainers and contributors
+# Copyright (C) 2017-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,12 +17,14 @@
import os
from glob import glob
-from netifaces import interfaces
+from netifaces import AF_INET
from sys import exit
from vyos.config import Config
-from vyos.util import call
+from vyos.configverify import verify_interface_exists
from vyos.template import render
+from vyos.utils.process import call
+from vyos.utils.network import is_afi_configured
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -50,18 +52,16 @@ def verify(relay):
# we certainly require a UDP port to listen to
if 'port' not in config:
- raise ConfigError(f'Port number mandatory for udp broadcast relay "{instance}"')
+ raise ConfigError(f'Port number is mandatory for UDP broadcast relay "{instance}"')
- # if only oone interface is given it's a string -> move to list
- if isinstance(config.get('interface', []), str):
- config['interface'] = [ config['interface'] ]
# Relaying data without two interface is kinda senseless ...
if len(config.get('interface', [])) < 2:
- raise ConfigError('At least two interfaces are required for udp broadcast relay "{instance}"')
+ raise ConfigError('At least two interfaces are required for UDP broadcast relay "{instance}"')
for interface in config.get('interface', []):
- if interface not in interfaces():
- raise ConfigError('Interface "{interface}" does not exist!')
+ verify_interface_exists(interface)
+ if not is_afi_configured(interface, AF_INET):
+ raise ConfigError(f'Interface "{interface}" has no IPv4 address configured!')
return None
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py
index 82289526f..9c43640a9 100755
--- a/src/conf_mode/conntrack.py
+++ b/src/conf_mode/conntrack.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
@@ -20,15 +20,13 @@ import re
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.firewall import find_nftables_rule
from vyos.firewall import remove_nftables_rule
-from vyos.util import cmd
-from vyos.util import run
-from vyos.util import process_named_running
-from vyos.util import dict_search
+from vyos.utils.process import process_named_running
+from vyos.utils.dict import dict_search
+from vyos.utils.process import cmd
+from vyos.utils.process import run
from vyos.template import render
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -77,16 +75,8 @@ def get_config(config=None):
base = ['system', 'conntrack']
conntrack = 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)
- # 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 'timeout' in default_values and 'custom' in default_values['timeout']:
- del default_values['timeout']['custom']
- conntrack = dict_merge(default_values, conntrack)
+ get_first_key=True,
+ with_recursive_defaults=True)
return conntrack
diff --git a/src/conf_mode/conntrack_sync.py b/src/conf_mode/conntrack_sync.py
index c4b2bb488..4fb2ce27f 100755
--- a/src/conf_mode/conntrack_sync.py
+++ b/src/conf_mode/conntrack_sync.py
@@ -18,17 +18,15 @@ import os
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configverify import verify_interface_exists
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.util import process_named_running
-from vyos.util import read_file
-from vyos.util import run
+from vyos.utils.dict import dict_search
+from vyos.utils.process import process_named_running
+from vyos.utils.file import read_file
+from vyos.utils.process import call
+from vyos.utils.process import run
from vyos.template import render
from vyos.template import get_ipv4
-from vyos.validate import is_addr_assigned
-from vyos.xml import defaults
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -50,11 +48,7 @@ def get_config(config=None):
return None
conntrack = 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)
- conntrack = dict_merge(default_values, conntrack)
+ get_first_key=True, with_defaults=True)
conntrack['hash_size'] = read_file('/sys/module/nf_conntrack/parameters/hashsize')
conntrack['table_size'] = read_file('/proc/sys/net/netfilter/nf_conntrack_max')
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index aceb27fb0..46eb10714 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -28,16 +28,17 @@ 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.utils.file import write_file
+from vyos.utils.process import call
+from vyos.utils.process import cmd
+from vyos.utils.process import run
+from vyos.utils.process import rc_cmd
+from vyos.template import bracketize_ipv6
from vyos.template import inc_ip
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.template import render
-from vyos.xml import defaults
+from vyos.xml_ref import default_value
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -66,58 +67,26 @@ def get_config(config=None):
base = ['container']
container = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- default_values = defaults(base)
- # 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
- if 'name' in container:
- default_values = defaults(base + ['name'])
- if 'port' in default_values:
- del default_values['port']
- if 'volume' in default_values:
- del default_values['volume']
- 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_port = defaults(base + ['name', 'port'])
- container['name'][name]['port'][port] = dict_merge(
- 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_volume = defaults(base + ['name', 'volume'])
- container['name'][name]['volume'][volume] = dict_merge(
- default_values_volume, container['name'][name]['volume'][volume])
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
+
+ for name in container.get('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)
# 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():
+ default_values = default_value(base + ['registry'])
+ for registry in default_values:
tmp = {registry : {}}
container['registry'] = dict_merge(tmp, container['registry'])
@@ -209,6 +178,11 @@ def verify(container):
if 'value' not in cfg:
raise ConfigError(f'Environment variable {var} has no value assigned!')
+ if 'label' in container_config:
+ for var, cfg in container_config['label'].items():
+ if 'value' not in cfg:
+ raise ConfigError(f'Label variable {var} has no value assigned!')
+
if 'volume' in container_config:
for volume, volume_config in container_config['volume'].items():
if 'source' not in volume_config:
@@ -299,6 +273,12 @@ def generate_run_arguments(name, container_config):
for k, v in container_config['environment'].items():
env_opt += f" --env \"{k}={v['value']}\""
+ # Check/set label options "--label foo=bar"
+ env_opt = ''
+ if 'label' in container_config:
+ for k, v in container_config['label'].items():
+ env_opt += f" --label \"{k}={v['value']}\""
+
hostname = ''
if 'host_name' in container_config:
hostname = container_config['host_name']
@@ -312,7 +292,15 @@ def generate_run_arguments(name, container_config):
protocol = container_config['port'][portmap]['protocol']
sport = container_config['port'][portmap]['source']
dport = container_config['port'][portmap]['destination']
- port += f' --publish {sport}:{dport}/{protocol}'
+ listen_addresses = container_config['port'][portmap].get('listen_address', [])
+
+ # If listen_addresses is not empty, include them in the publish command
+ if listen_addresses:
+ for listen_address in listen_addresses:
+ port += f' --publish {bracketize_ipv6(listen_address)}:{sport}:{dport}/{protocol}'
+ else:
+ # If listen_addresses is empty, just include the standard publish command
+ port += f' --publish {sport}:{dport}/{protocol}'
# Bind volume
volume = ''
@@ -321,7 +309,8 @@ def generate_run_arguments(name, container_config):
svol = vol_config['source']
dvol = vol_config['destination']
mode = vol_config['mode']
- volume += f' --volume {svol}:{dvol}:{mode}'
+ prop = vol_config['propagation']
+ volume += f' --volume {svol}:{dvol}:{mode},{prop}'
container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \
f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \
@@ -485,6 +474,7 @@ def apply(container):
# 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.add_ipv6_eui64_address('fe80::/64')
tmp.set_vrf(network_config.get('vrf', ''))
return None
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index 7e702a446..37d708847 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -20,12 +20,10 @@ 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
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -41,17 +39,15 @@ def get_config(config=None):
if not conf.exists(base):
return None
- relay = 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)
- relay = dict_merge(default_values, relay)
+ relay = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
return relay
def verify(relay):
# bail out early - looks like removal from running config
- if not relay:
+ if not relay or 'disable' in relay:
return None
if 'lo' in (dict_search('interface', relay) or []):
@@ -78,7 +74,7 @@ def verify(relay):
def generate(relay):
# bail out early - looks like removal from running config
- if not relay:
+ if not relay or 'disable' in relay:
return None
render(config_file, 'dhcp-relay/dhcrelay.conf.j2', relay)
@@ -87,7 +83,7 @@ def generate(relay):
def apply(relay):
# bail out early - looks like removal from running config
service_name = 'isc-dhcp-relay.service'
- if not relay:
+ if not relay or 'disable' in relay:
call(f'systemctl stop {service_name}')
if os.path.exists(config_file):
os.unlink(config_file)
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index 2b2af252d..c4c72aae9 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -23,14 +23,12 @@ from netaddr import IPRange
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.util import dict_search
-from vyos.util import run
-from vyos.validate import is_subnet_connected
-from vyos.validate import is_addr_assigned
-from vyos.xml import defaults
+from vyos.utils.dict import dict_search
+from vyos.utils.process import call
+from vyos.utils.process import run
+from vyos.utils.network import is_subnet_connected
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -109,19 +107,15 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dhcp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
- # T2665: defaults include lease time per TAG node which need to be added to
- # individual subnet definitions
- default_values = defaults(base + ['shared-network-name', 'subnet'])
+ dhcp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
if 'shared_network_name' in dhcp:
for network, network_config in dhcp['shared_network_name'].items():
if 'subnet' in network_config:
for subnet, subnet_config in network_config['subnet'].items():
- if 'lease' not in subnet_config:
- dhcp['shared_network_name'][network]['subnet'][subnet] = dict_merge(
- default_values, dhcp['shared_network_name'][network]['subnet'][subnet])
-
# If exclude IP addresses are defined we need to slice them out of
# the defined ranges
if {'exclude', 'range'} <= set(subnet_config):
@@ -302,6 +296,10 @@ def generate(dhcp):
render(config_file, 'dhcp-server/dhcpd.conf.j2', dhcp,
formater=lambda _: _.replace("&quot;", '"'))
+ # Clean up configuration test file
+ if os.path.exists(tmp_file):
+ os.unlink(tmp_file)
+
return None
def apply(dhcp):
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
index c1bd51f62..6537ca3c2 100755
--- a/src/conf_mode/dhcpv6_relay.py
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -19,13 +19,11 @@ import os
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.ifconfig import Interface
from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.validate import is_ipv6_link_local
-from vyos.xml import defaults
+from vyos.template import is_ipv6
+from vyos.utils.process import call
+from vyos.utils.network import is_ipv6_link_local
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -41,17 +39,15 @@ def get_config(config=None):
if not conf.exists(base):
return None
- relay = 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)
- relay = dict_merge(default_values, relay)
+ relay = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
return relay
def verify(relay):
# bail out early - looks like removal from running config
- if not relay:
+ if not relay or 'disable' in relay:
return None
if 'upstream_interface' not in relay:
@@ -69,7 +65,7 @@ def verify(relay):
for interface in relay['listen_interface']:
has_global = False
for addr in Interface(interface).get_addr():
- if not is_ipv6_link_local(addr):
+ if is_ipv6(addr) and not is_ipv6_link_local(addr):
has_global = True
if not has_global:
raise ConfigError(f'Interface {interface} does not have global '\
@@ -79,7 +75,7 @@ def verify(relay):
def generate(relay):
# bail out early - looks like removal from running config
- if not relay:
+ if not relay or 'disable' in relay:
return None
render(config_file, 'dhcp-relay/dhcrelay6.conf.j2', relay)
@@ -88,7 +84,7 @@ def generate(relay):
def apply(relay):
# bail out early - looks like removal from running config
service_name = 'isc-dhcp-relay6.service'
- if not relay:
+ if not relay or 'disable' in relay:
# DHCPv6 relay support is removed in the commit
call(f'systemctl stop {service_name}')
if os.path.exists(config_file):
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index 078ff327c..427001609 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -23,9 +23,9 @@ from sys import exit
from vyos.config import Config
from vyos.template import render
from vyos.template import is_ipv6
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.validate import is_subnet_connected
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.network import is_subnet_connected
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py
new file mode 100755
index 000000000..ab80defe8
--- /dev/null
+++ b/src/conf_mode/dns_dynamic.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+#
+# 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
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/ddclient/ddclient.conf'
+systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf'
+
+# Protocols that require zone
+zone_allowed = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn']
+
+# Protocols that do not require username
+username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla']
+
+# Protocols that support both IPv4 and IPv6
+dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla']
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base_level = ['service', 'dns', 'dynamic']
+ if not conf.exists(base_level):
+ return None
+
+ dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
+
+ dyndns['config_file'] = config_file
+ return dyndns
+
+def verify(dyndns):
+ # bail out early - looks like removal from running config
+ if not dyndns or 'address' not in dyndns:
+ return None
+
+ for address in dyndns['address']:
+ # RFC2136 - configuration validation
+ if 'rfc2136' in dyndns['address'][address]:
+ for config in dyndns['address'][address]['rfc2136'].values():
+ for field in ['host_name', 'zone', 'server', 'key']:
+ if field not in config:
+ raise ConfigError(f'"{field.replace("_", "-")}" is required for RFC2136 '
+ f'based Dynamic DNS service on "{address}"')
+
+ # Dynamic DNS service provider - configuration validation
+ if 'service' in dyndns['address'][address]:
+ for service, config in dyndns['address'][address]['service'].items():
+ error_msg = f'is required for Dynamic DNS service "{service}" on "{address}"'
+
+ for field in ['host_name', 'password', 'protocol']:
+ if field not in config:
+ raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}')
+
+ if config['protocol'] in zone_allowed and 'zone' not in config:
+ raise ConfigError(f'"zone" {error_msg}')
+
+ if config['protocol'] not in zone_allowed and 'zone' in config:
+ raise ConfigError(f'"{config["protocol"]}" does not support "zone"')
+
+ if config['protocol'] not in username_unnecessary:
+ if 'username' not in config:
+ raise ConfigError(f'"username" {error_msg}')
+
+ if config['ip_version'] == 'both':
+ if config['protocol'] not in dualstack_supported:
+ raise ConfigError(f'"{config["protocol"]}" does not support '
+ f'both IPv4 and IPv6 at the same time')
+ # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org)
+ if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] != 'members.dyndns.org':
+ raise ConfigError(f'"{config["protocol"]}" does not support '
+ f'both IPv4 and IPv6 at the same time for "{config["server"]}"')
+
+ return None
+
+def generate(dyndns):
+ # bail out early - looks like removal from running config
+ if not dyndns or 'address' not in dyndns:
+ return None
+
+ render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns)
+ render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns)
+ return None
+
+def apply(dyndns):
+ systemd_service = 'ddclient.service'
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ # bail out early - looks like removal from running config
+ if not dyndns or 'address' not in dyndns:
+ call(f'systemctl stop {systemd_service}')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ else:
+ call(f'systemctl reload-or-restart {systemd_service}')
+
+ 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/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 0d86c6a52..c186f47af 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -21,14 +21,12 @@ from sys import exit
from glob import glob
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 bracketize_ipv6
-from vyos.util import call
-from vyos.util import chown
-from vyos.util import dict_search
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.permission import chown
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
@@ -52,31 +50,10 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrieved.
- 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])
+ dns = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
# some additions to the default dictionary
if 'system' in dns:
@@ -109,9 +86,6 @@ def get_config(config=None):
rdata = recorddata[rtype][subnode]
if rtype in [ 'a', 'aaaa' ]:
- rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
- rdata = dict_merge(rdefaults, rdata)
-
if not 'address' in rdata:
dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one address is required')
continue
@@ -127,9 +101,6 @@ def get_config(config=None):
'value': address
})
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(f'{subnode}.{node}: target is required')
continue
@@ -141,18 +112,12 @@ def get_config(config=None):
'value': '{}.'.format(rdata['target'])
})
elif rtype == 'mx':
- rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
- del rdefaults['server']
- rdata = dict_merge(rdefaults, rdata)
-
if not 'server' in rdata:
dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one server is required')
continue
for servername in rdata['server']:
serverdata = rdata['server'][servername]
- serverdefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'server']) # T2665
- serverdata = dict_merge(serverdefaults, serverdata)
zone['records'].append({
'name': subnode,
'type': rtype.upper(),
@@ -160,9 +125,6 @@ def get_config(config=None):
'value': '{} {}.'.format(serverdata['priority'], servername)
})
elif rtype == 'txt':
- rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
- rdata = dict_merge(rdefaults, rdata)
-
if not 'value' in rdata:
dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one value is required')
continue
@@ -175,9 +137,6 @@ def get_config(config=None):
'value': "\"{}\"".format(value.replace("\"", "\\\""))
})
elif rtype == 'spf':
- rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
- rdata = dict_merge(rdefaults, rdata)
-
if not 'value' in rdata:
dns['authoritative_zone_errors'].append(f'{subnode}.{node}: value is required')
continue
@@ -189,19 +148,12 @@ def get_config(config=None):
'value': '"{}"'.format(rdata['value'].replace("\"", "\\\""))
})
elif rtype == 'srv':
- rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
- del rdefaults['entry']
- rdata = dict_merge(rdefaults, rdata)
-
if not 'entry' in rdata:
dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one entry is required')
continue
for entryno in rdata['entry']:
entrydata = rdata['entry'][entryno]
- entrydefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'entry']) # T2665
- entrydata = dict_merge(entrydefaults, entrydata)
-
if not 'hostname' in entrydata:
dns['authoritative_zone_errors'].append(f'{subnode}.{node}: hostname is required for entry {entryno}')
continue
@@ -217,19 +169,12 @@ def get_config(config=None):
'value': '{} {} {} {}.'.format(entrydata['priority'], entrydata['weight'], entrydata['port'], entrydata['hostname'])
})
elif rtype == 'naptr':
- rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665
- del rdefaults['rule']
- rdata = dict_merge(rdefaults, rdata)
-
-
if not 'rule' in rdata:
dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one rule is required')
continue
for ruleno in rdata['rule']:
ruledata = rdata['rule'][ruleno]
- ruledefaults = defaults(base + ['authoritative-domain', 'records', rtype, 'rule']) # T2665
- ruledata = dict_merge(ruledefaults, ruledata)
flags = ""
if 'lookup-srv' in ruledata:
flags += "S"
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
deleted file mode 100755
index 426e3d693..000000000
--- a/src/conf_mode/dynamic_dns.py
+++ /dev/null
@@ -1,156 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.template import render
-from vyos.util import call
-from vyos.xml import defaults
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/ddclient/ddclient.conf'
-
-# Mapping of service name to service protocol
-default_service_protocol = {
- 'afraid': 'freedns',
- 'changeip': 'changeip',
- 'cloudflare': 'cloudflare',
- 'dnspark': 'dnspark',
- 'dslreports': 'dslreports1',
- 'dyndns': 'dyndns2',
- 'easydns': 'easydns',
- 'namecheap': 'namecheap',
- 'noip': 'noip',
- 'sitelutions': 'sitelutions',
- 'zoneedit': 'zoneedit1'
-}
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
-
- base_level = ['service', 'dns', 'dynamic']
- if not conf.exists(base_level):
- return None
-
- dyndns = conf.get_config_dict(base_level, 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.
- for interface in dyndns['interface']:
- if 'service' in dyndns['interface'][interface]:
- # 'Autodetect' protocol used by DynDNS service
- for service in dyndns['interface'][interface]['service']:
- if service in default_service_protocol:
- dyndns['interface'][interface]['service'][service].update(
- {'protocol' : default_service_protocol.get(service)})
- else:
- dyndns['interface'][interface]['service'][service].update(
- {'custom': ''})
-
- if 'rfc2136' in dyndns['interface'][interface]:
- default_values = defaults(base_level + ['interface', 'rfc2136'])
- for rfc2136 in dyndns['interface'][interface]['rfc2136']:
- dyndns['interface'][interface]['rfc2136'][rfc2136] = dict_merge(
- default_values, dyndns['interface'][interface]['rfc2136'][rfc2136])
-
- return dyndns
-
-def verify(dyndns):
- # bail out early - looks like removal from running config
- if not dyndns:
- return None
-
- # A 'node' corresponds to an interface
- if 'interface' not in dyndns:
- return None
-
- for interface in dyndns['interface']:
- # RFC2136 - configuration validation
- if 'rfc2136' in dyndns['interface'][interface]:
- for rfc2136, config in dyndns['interface'][interface]['rfc2136'].items():
-
- for tmp in ['record', 'zone', 'server', 'key']:
- if tmp not in config:
- raise ConfigError(f'"{tmp}" required for rfc2136 based '
- f'DynDNS service on "{interface}"')
-
- if not os.path.isfile(config['key']):
- raise ConfigError(f'"key"-file not found for rfc2136 based '
- f'DynDNS service on "{interface}"')
-
- # DynDNS service provider - configuration validation
- if 'service' in dyndns['interface'][interface]:
- for service, config in dyndns['interface'][interface]['service'].items():
- error_msg = f'required for DynDNS service "{service}" on "{interface}"'
- if 'host_name' not in config:
- raise ConfigError(f'"host-name" {error_msg}')
-
- if 'login' not in config:
- 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}')
-
- if 'zone' in config:
- if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'):
- raise ConfigError(f'"zone" option only supported with CloudFlare')
-
- if 'custom' in config:
- if 'protocol' not in config:
- raise ConfigError(f'"protocol" {error_msg}')
-
- if 'server' not in config:
- raise ConfigError(f'"server" {error_msg}')
-
- return None
-
-def generate(dyndns):
- # bail out early - looks like removal from running config
- if not dyndns:
- return None
-
- render(config_file, 'dynamic-dns/ddclient.conf.j2', dyndns)
- return None
-
-def apply(dyndns):
- if not dyndns:
- call('systemctl stop ddclient.service')
- if os.path.exists(config_file):
- os.unlink(config_file)
- else:
- call('systemctl restart ddclient.service')
-
- 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/firewall.py b/src/conf_mode/firewall.py
index 190587980..c3b1ee015 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -23,7 +23,6 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configdiff import get_config_diff, Diff
from vyos.configdep import set_dependents, call_dependents
@@ -31,13 +30,12 @@ from vyos.configdep import set_dependents, call_dependents
from vyos.firewall import fqdn_config_parse
from vyos.firewall import geoip_update
from vyos.template import render
-from vyos.util import call
-from vyos.util import cmd
-from vyos.util import dict_search_args
-from vyos.util import dict_search_recursive
-from vyos.util import process_named_running
-from vyos.util import rc_cmd
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.process import cmd
+from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search_recursive
+from vyos.utils.process import process_named_running
+from vyos.utils.process import rc_cmd
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -56,7 +54,6 @@ sysfs_config = {
'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians'},
'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects'},
'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects'},
- 'source_validation': {'sysfs': '/proc/sys/net/ipv4/conf/*/rp_filter', 'disable': '0', 'strict': '1', 'loose': '2'},
'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies'},
'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'}
}
@@ -97,19 +94,22 @@ def geoip_updated(conf, firewall):
updated = False
for key, path in dict_search_recursive(firewall, 'geoip'):
- set_name = f'GEOIP_CC_{path[1]}_{path[3]}'
- if path[0] == 'name':
+ set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
+ if (path[0] == 'ipv4'):
out['name'].append(set_name)
- elif path[0] == 'ipv6_name':
+ elif (path[0] == 'ipv6'):
+ set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}'
out['ipv6_name'].append(set_name)
+
updated = True
if 'delete' in node_diff:
for key, path in dict_search_recursive(node_diff['delete'], 'geoip'):
- set_name = f'GEOIP_CC_{path[1]}_{path[3]}'
- if path[0] == 'name':
+ set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
+ if (path[0] == 'ipv4'):
out['deleted_name'].append(set_name)
- elif path[0] == 'ipv6-name':
+ elif (path[0] == 'ipv6'):
+ set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
out['deleted_ipv6_name'].append(set_name)
updated = True
@@ -125,54 +125,17 @@ def get_config(config=None):
conf = Config()
base = ['firewall']
- firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
- # We have gathered the dict representation of the CLI, but there are
- # default options which we need to update into the dictionary retrived.
- # XXX: T2665: we currently have no nice way for defaults under tag
- # nodes, thus we load the defaults "by hand"
- default_values = defaults(base)
- for tmp in ['name', 'ipv6_name']:
- if tmp in default_values:
- del default_values[tmp]
-
- if 'zone' in default_values:
- del default_values['zone']
-
- firewall = dict_merge(default_values, firewall)
-
- # Merge in defaults for IPv4 ruleset
- if 'name' in firewall:
- default_values = defaults(base + ['name'])
- for name in firewall['name']:
- firewall['name'][name] = dict_merge(default_values,
- firewall['name'][name])
-
- # Merge in defaults for IPv6 ruleset
- if 'ipv6_name' in firewall:
- default_values = defaults(base + ['ipv6-name'])
- for ipv6_name in firewall['ipv6_name']:
- firewall['ipv6_name'][ipv6_name] = dict_merge(default_values,
- firewall['ipv6_name'][ipv6_name])
-
- if 'zone' in firewall:
- default_values = defaults(base + ['zone'])
- for zone in firewall['zone']:
- firewall['zone'][zone] = dict_merge(default_values, firewall['zone'][zone])
+ firewall = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
+
firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group']))
if firewall['group_resync']:
# Update nat and policy-route as firewall groups were updated
set_dependents('group_resync', conf)
- if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
- diff = get_config_diff(conf)
- firewall['trap_diff'] = diff.get_child_nodes_diff_str(base)
- firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'],
- key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
-
firewall['geoip_updated'] = geoip_updated(conf, firewall)
fqdn_config_parse(firewall)
@@ -191,11 +154,11 @@ def verify_rule(firewall, rule_conf, ipv6):
raise ConfigError('jump-target defined, but action jump needed and it is not defined')
target = rule_conf['jump_target']
if not ipv6:
- if target not in dict_search_args(firewall, 'name'):
+ if target not in dict_search_args(firewall, 'ipv4', 'name'):
raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
else:
- 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 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']:
@@ -295,6 +258,11 @@ def verify_rule(firewall, rule_conf, ipv6):
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')
+ for direction in ['inbound_interface','outbound_interface']:
+ if direction in rule_conf:
+ if 'interface_name' in rule_conf[direction] and 'interface_group' in rule_conf[direction]:
+ raise ConfigError(f'Cannot specify both interface-group and interface-name for {direction}')
+
def verify_nested_group(group_name, group, groups, seen):
if 'include' not in group:
return
@@ -312,10 +280,6 @@ def verify_nested_group(group_name, group, groups, seen):
verify_nested_group(g, groups[g], groups, seen)
def verify(firewall):
- if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
- if not firewall['trap_targets']:
- raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined')
-
if 'group' in firewall:
for group_type in nested_group_types:
if group_type in firewall['group']:
@@ -323,95 +287,45 @@ def verify(firewall):
for group_name, group in groups.items():
verify_nested_group(group_name, group, groups, [])
- for name in ['name', 'ipv6_name']:
- if name in firewall:
- for name_id, name_conf in firewall[name].items():
- if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
- raise ConfigError('default-action set to jump, but no default-jump-target specified')
- if 'default_jump_target' in name_conf:
- target = name_conf['default_jump_target']
- if 'jump' not in name_conf['default_action']:
- raise ConfigError('default-jump-target defined,but default-action jump needed and it is not defined')
- if name_conf['default_jump_target'] == name_id:
- raise ConfigError(f'Loop detected on default-jump-target.')
- ## Now need to check that default-jump-target exists (other firewall chain/name)
- if target not in dict_search_args(firewall, name):
- raise ConfigError(f'Invalid jump-target. Firewall {name} {target} does not exist on the system')
-
- if 'rule' in name_conf:
- for rule_id, rule_conf in name_conf['rule'].items():
- verify_rule(firewall, rule_conf, name == 'ipv6_name')
-
- if 'interface' in firewall:
- for ifname, if_firewall in firewall['interface'].items():
- # verify ifname needs to be disabled, dynamic devices come up later
- # verify_interface_exists(ifname)
-
- for direction in ['in', 'out', 'local']:
- name = dict_search_args(if_firewall, direction, 'name')
- ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
-
- if name and dict_search_args(firewall, 'name', name) == None:
- raise ConfigError(f'Invalid firewall name "{name}" referenced on interface {ifname}')
-
- if ipv6_name and dict_search_args(firewall, 'ipv6_name', ipv6_name) == None:
- raise ConfigError(f'Invalid firewall ipv6-name "{ipv6_name}" referenced on interface {ifname}')
-
- local_zone = False
- zone_interfaces = []
-
- if 'zone' in firewall:
- for zone, zone_conf in firewall['zone'].items():
- if 'local_zone' not in zone_conf and 'interface' not in zone_conf:
- raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone')
-
- if 'local_zone' in zone_conf:
- if local_zone:
- raise ConfigError('There cannot be multiple local zones')
- if 'interface' in zone_conf:
- raise ConfigError('Local zone cannot have interfaces assigned')
- if 'intra_zone_filtering' in zone_conf:
- raise ConfigError('Local zone cannot use intra-zone-filtering')
- local_zone = True
-
- if 'interface' in zone_conf:
- found_duplicates = [intf for intf in zone_conf['interface'] if intf in zone_interfaces]
-
- if found_duplicates:
- raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
-
- zone_interfaces += zone_conf['interface']
-
- if 'intra_zone_filtering' in zone_conf:
- intra_zone = zone_conf['intra_zone_filtering']
-
- if len(intra_zone) > 1:
- raise ConfigError('Only one intra-zone-filtering action must be specified')
-
- if 'firewall' in intra_zone:
- v4_name = dict_search_args(intra_zone, 'firewall', 'name')
- if v4_name and not dict_search_args(firewall, 'name', v4_name):
- raise ConfigError(f'Firewall name "{v4_name}" does not exist')
-
- v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6_name')
- if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name):
- raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
-
- if not v4_name and not v6_name:
- raise ConfigError('No firewall names specified for intra-zone-filtering')
-
- if 'from' in zone_conf:
- for from_zone, from_conf in zone_conf['from'].items():
- if from_zone not in firewall['zone']:
- raise ConfigError(f'Zone "{zone}" refers to a non-existent or deleted zone "{from_zone}"')
-
- v4_name = dict_search_args(from_conf, 'firewall', 'name')
- if v4_name and not dict_search_args(firewall, 'name', v4_name):
- raise ConfigError(f'Firewall name "{v4_name}" does not exist')
-
- v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name')
- if v6_name and not dict_search_args(firewall, 'ipv6_name', v6_name):
- raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+ if 'ipv4' in firewall:
+ for name in ['name','forward','input','output']:
+ if name in firewall['ipv4']:
+ for name_id, name_conf in firewall['ipv4'][name].items():
+ if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
+ raise ConfigError('default-action set to jump, but no default-jump-target specified')
+ if 'default_jump_target' in name_conf:
+ target = name_conf['default_jump_target']
+ if 'jump' not in name_conf['default_action']:
+ raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
+ if name_conf['default_jump_target'] == name_id:
+ raise ConfigError(f'Loop detected on default-jump-target.')
+ ## Now need to check that default-jump-target exists (other firewall chain/name)
+ if target not in dict_search_args(firewall['ipv4'], 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+
+ if 'rule' in name_conf:
+ for rule_id, rule_conf in name_conf['rule'].items():
+ verify_rule(firewall, rule_conf, False)
+
+ if 'ipv6' in firewall:
+ for name in ['name','forward','input','output']:
+ if name in firewall['ipv6']:
+ for name_id, name_conf in firewall['ipv6'][name].items():
+ if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf:
+ raise ConfigError('default-action set to jump, but no default-jump-target specified')
+ if 'default_jump_target' in name_conf:
+ target = name_conf['default_jump_target']
+ if 'jump' not in name_conf['default_action']:
+ raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined')
+ if name_conf['default_jump_target'] == name_id:
+ raise ConfigError(f'Loop detected on default-jump-target.')
+ ## Now need to check that default-jump-target exists (other firewall chain/name)
+ if target not in dict_search_args(firewall['ipv6'], 'name'):
+ raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system')
+
+ if 'rule' in name_conf:
+ for rule_id, rule_conf in name_conf['rule'].items():
+ verify_rule(firewall, rule_conf, True)
return None
@@ -419,18 +333,16 @@ def generate(firewall):
if not os.path.exists(nftables_conf):
firewall['first_install'] = True
- if 'zone' in firewall:
- for local_zone, local_zone_conf in firewall['zone'].items():
- if 'local_zone' not in local_zone_conf:
- continue
+ # Determine if conntrack is needed
+ firewall['ipv4_conntrack_action'] = 'return'
+ firewall['ipv6_conntrack_action'] = 'return'
- local_zone_conf['from_local'] = {}
-
- for zone, zone_conf in firewall['zone'].items():
- if zone == local_zone or 'from' not in zone_conf:
- continue
- if local_zone in zone_conf['from']:
- local_zone_conf['from_local'][zone] = zone_conf['from'][local_zone]
+ for rules, path in dict_search_recursive(firewall, 'rule'):
+ if any(('state' in rule_conf or 'connection_status' in rule_conf) for rule_conf in rules.values()):
+ if path[0] == 'ipv4':
+ firewall['ipv4_conntrack_action'] = 'accept'
+ elif path[0] == 'ipv6':
+ firewall['ipv6_conntrack_action'] = 'accept'
render(nftables_conf, 'firewall/nftables.j2', firewall)
return None
@@ -440,9 +352,8 @@ def apply_sysfs(firewall):
paths = glob(conf['sysfs'])
value = None
- if name in firewall:
- conf_value = firewall[name]
-
+ if name in firewall['global_options']:
+ conf_value = firewall['global_options'][name]
if conf_value in conf:
value = conf[conf_value]
elif conf_value == 'enable':
@@ -455,42 +366,6 @@ def apply_sysfs(firewall):
with open(path, 'w') as f:
f.write(value)
-def post_apply_trap(firewall):
- if 'first_install' in firewall:
- return None
-
- if 'config_trap' not in firewall or firewall['config_trap'] != 'enable':
- return None
-
- if not process_named_running('snmpd'):
- return None
-
- trap_username = os.getlogin()
-
- for host, target_conf in firewall['trap_targets'].items():
- community = target_conf['community'] if 'community' in target_conf else 'public'
- port = int(target_conf['port']) if 'port' in target_conf else 162
-
- base_cmd = f'snmptrap -v2c -c {community} {host}:{port} 0 {snmp_trap_mib}::{snmp_trap_name} '
-
- for change_type, changes in firewall['trap_diff'].items():
- for path_str, value in changes.items():
- objects = [
- f'mgmtEventUser s "{trap_username}"',
- f'mgmtEventSource i {snmp_event_source}',
- f'mgmtEventType i {snmp_change_type[change_type]}'
- ]
-
- if change_type == 'add':
- objects.append(f'mgmtEventCurrCfg s "{path_str} {value}"')
- elif change_type == 'delete':
- objects.append(f'mgmtEventPrevCfg s "{path_str} {value}"')
- elif change_type == 'change':
- objects.append(f'mgmtEventPrevCfg s "{path_str} {value[0]}"')
- objects.append(f'mgmtEventCurrCfg s "{path_str} {value[1]}"')
-
- cmd(base_cmd + ' '.join(objects))
-
def apply(firewall):
install_result, output = rc_cmd(f'nft -f {nftables_conf}')
if install_result == 1:
@@ -515,8 +390,6 @@ def apply(firewall):
print('Updating GeoIP. Please wait...')
geoip_update(firewall)
- post_apply_trap(firewall)
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index f67f1710e..71acd69fa 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.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
@@ -18,20 +18,17 @@ import os
import re
from sys import exit
-import ipaddress
-
from ipaddress import ip_address
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
+from vyos.config import config_dict_merge
+from vyos.configverify import verify_vrf
from vyos.ifconfig import Section
-from vyos.ifconfig import Interface
from vyos.template import render
-from vyos.util import call
-from vyos.util import cmd
-from vyos.validate import is_addr_assigned
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.process import cmd
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -130,30 +127,19 @@ def get_config(config=None):
flow_accounting = 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)
+ # We have gathered the dict representation of the CLI, but there are
+ # default values which we need to conditionally update into the
+ # dictionary retrieved.
+ default_values = conf.get_config_defaults(**flow_accounting.kwargs,
+ recursive=True)
- # delete individual flow type default - should only be added if user uses
- # this feature
+ # delete individual flow type defaults - should only be added if user
+ # sets this feature
for flow_type in ['sflow', 'netflow']:
- if flow_type in default_values:
+ if flow_type not in flow_accounting and flow_type in default_values:
del default_values[flow_type]
- flow_accounting = dict_merge(default_values, flow_accounting)
- for flow_type in ['sflow', 'netflow']:
- if flow_type in flow_accounting:
- default_values = defaults(base + [flow_type])
- # we need to merge individual server configurations
- if 'server' in default_values:
- del default_values['server']
- flow_accounting[flow_type] = dict_merge(default_values, flow_accounting[flow_type])
-
- if 'server' in flow_accounting[flow_type]:
- default_values = defaults(base + [flow_type, 'server'])
- for server in flow_accounting[flow_type]['server']:
- flow_accounting[flow_type]['server'][server] = dict_merge(
- default_values,flow_accounting[flow_type]['server'][server])
+ flow_accounting = config_dict_merge(default_values, flow_accounting)
return flow_accounting
@@ -194,6 +180,7 @@ def verify(flow_config):
sflow_collector_ipver = ip_address(server).version
# check if vrf is defined for Sflow
+ verify_vrf(flow_config)
sflow_vrf = None
if 'vrf' in flow_config:
sflow_vrf = flow_config['vrf']
@@ -211,7 +198,7 @@ def verify(flow_config):
if not is_addr_assigned(tmp, sflow_vrf):
raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!')
- # Check if configured netflow source-address exist in the system
+ # Check if configured sflow source-address exist in the system
if 'source_address' in flow_config['sflow']:
if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf):
tmp = flow_config['sflow']['source_address']
@@ -219,13 +206,18 @@ def verify(flow_config):
# check NetFlow configuration
if 'netflow' in flow_config:
+ # check if vrf is defined for netflow
+ netflow_vrf = None
+ if 'vrf' in flow_config:
+ netflow_vrf = flow_config['vrf']
+
# check if at least one NetFlow collector is configured if NetFlow configuration is presented
if 'server' not in flow_config['netflow']:
raise ConfigError('You need to configure at least one NetFlow server!')
# Check if configured netflow source-address exist in the system
if 'source_address' in flow_config['netflow']:
- if not is_addr_assigned(flow_config['netflow']['source_address']):
+ if not is_addr_assigned(flow_config['netflow']['source_address'], netflow_vrf):
tmp = flow_config['netflow']['source_address']
raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!')
diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py
index e18b426b1..626a3757e 100755
--- a/src/conf_mode/high-availability.py
+++ b/src/conf_mode/high-availability.py
@@ -14,7 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
from sys import exit
from ipaddress import ip_interface
@@ -23,14 +22,11 @@ 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
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.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -42,42 +38,12 @@ def get_config(config=None):
conf = Config()
base = ['high-availability']
- base_vrrp = ['high-availability', 'vrrp']
if not conf.exists(base):
return None
ha = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- if 'vrrp' in ha:
- if 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 = 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, 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:
- default_values = defaults(base + ['virtual-server'])
- for vs in ha['virtual_server']:
- ha['virtual_server'][vs] = dict_merge(default_values, ha['virtual_server'][vs])
+ no_tag_node_value_mangle=True,
+ get_first_key=True, with_defaults=True)
## Get the sync group used for conntrack-sync
conntrack_path = ['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group']
@@ -112,7 +78,7 @@ def verify(ha):
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:
+ except ValueError:
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
@@ -175,6 +141,11 @@ def verify(ha):
# Virtual-server
if 'virtual_server' in ha:
for vs, vs_config in ha['virtual_server'].items():
+
+ if 'address' not in vs_config and 'fwmark' not in vs_config:
+ raise ConfigError('Either address or fwmark is required '
+ f'but not set for virtual-server "{vs}"')
+
if 'port' not in vs_config and 'fwmark' not in vs_config:
raise ConfigError(f'Port or fwmark is required but not set for virtual-server "{vs}"')
if 'port' in vs_config and 'fwmark' in vs_config:
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
index 93f244f42..36d1f6493 100755
--- a/src/conf_mode/host_name.py
+++ b/src/conf_mode/host_name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 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
@@ -18,16 +18,15 @@ import re
import sys
import copy
-import vyos.util
import vyos.hostsd_client
from vyos.base import Warning
from vyos.config import Config
from vyos.ifconfig import Section
from vyos.template import is_ip
-from vyos.util import cmd
-from vyos.util import call
-from vyos.util import process_named_running
+from vyos.utils.process import cmd
+from vyos.utils.process import call
+from vyos.utils.process import process_named_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py
index 7e801eb26..793a90d88 100755
--- a/src/conf_mode/http-api.py
+++ b/src/conf_mode/http-api.py
@@ -24,12 +24,9 @@ from copy import deepcopy
import vyos.defaults
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configdep import set_dependents, call_dependents
from vyos.template import render
-from vyos.util import cmd
-from vyos.util import call
-from vyos.xml import defaults
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -72,8 +69,9 @@ def get_config(config=None):
return None
api_dict = conf.get_config_dict(base, key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True)
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
# One needs to 'flatten' the keys dict from the config into the
# http-api.conf format for api_keys:
@@ -93,8 +91,8 @@ def get_config(config=None):
if 'api_keys' in api_dict:
keys_added = True
- if 'graphql' in api_dict:
- api_dict = dict_merge(defaults(base), api_dict)
+ if api_dict.from_defaults(['graphql']):
+ del api_dict['graphql']
http_api.update(api_dict)
diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py
index b0c38e8d3..010490c7e 100755
--- a/src/conf_mode/https.py
+++ b/src/conf_mode/https.py
@@ -28,10 +28,10 @@ from vyos import ConfigError
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
-from vyos.util import call
-from vyos.util import check_port_availability
-from vyos.util import is_listen_port_bind_service
-from vyos.util import write_file
+from vyos.utils.process import call
+from vyos.utils.network import check_port_availability
+from vyos.utils.network import is_listen_port_bind_service
+from vyos.utils.file import write_file
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py
index de6a51c64..40db417dd 100755
--- a/src/conf_mode/igmp_proxy.py
+++ b/src/conf_mode/igmp_proxy.py
@@ -21,11 +21,9 @@ from netifaces import interfaces
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -39,16 +37,9 @@ def get_config(config=None):
conf = Config()
base = ['protocols', 'igmp-proxy']
- igmp_proxy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-
- if 'interface' in igmp_proxy:
- # T2665: we must add the tagNode defaults individually until this is
- # moved to the base class
- default_values = defaults(base + ['interface'])
- for interface in igmp_proxy['interface']:
- igmp_proxy['interface'][interface] = dict_merge(default_values,
- igmp_proxy['interface'][interface])
-
+ igmp_proxy = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_defaults=True)
if conf.exists(['protocols', 'igmp']):
igmp_proxy.update({'igmp_configured': ''})
diff --git a/src/conf_mode/intel_qat.py b/src/conf_mode/intel_qat.py
index dd04a002d..e4b248675 100755
--- a/src/conf_mode/intel_qat.py
+++ b/src/conf_mode/intel_qat.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
@@ -20,7 +20,8 @@ import re
from sys import exit
from vyos.config import Config
-from vyos.util import popen, run
+from vyos.utils.process import popen
+from vyos.utils.process import run
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 9936620c8..0bd306ed0 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -35,9 +35,9 @@ from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ifconfig import BondIf
from vyos.ifconfig import Section
-from vyos.util import dict_search
-from vyos.validate import has_address_configured
-from vyos.validate import has_vrf_configured
+from vyos.utils.dict import dict_search
+from vyos.configdict import has_address_configured
+from vyos.configdict import has_vrf_configured
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -195,11 +195,11 @@ def verify(bond):
raise ConfigError(error_msg + 'it does not exist!')
if 'is_bridge_member' in interface_config:
- tmp = interface_config['is_bridge_member']
+ tmp = next(iter(interface_config['is_bridge_member']))
raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
if 'is_bond_member' in interface_config:
- tmp = interface_config['is_bond_member']
+ tmp = next(iter(interface_config['is_bond_member']))
raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
if 'is_source_interface' in interface_config:
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 4da3b097f..c82f01e53 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -14,10 +14,7 @@
# 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.config import Config
from vyos.configdict import get_interface_dict
@@ -25,17 +22,14 @@ from vyos.configdict import node_changed
from vyos.configdict import is_member
from vyos.configdict import is_source_interface
from vyos.configdict import has_vlan_subinterface_configured
-from vyos.configdict import dict_merge
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import BridgeIf
-from vyos.validate import has_address_configured
-from vyos.validate import has_vrf_configured
-from vyos.xml import defaults
+from vyos.configdict import has_address_configured
+from vyos.configdict import has_vrf_configured
-from vyos.util import cmd
-from vyos.util import dict_search
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
@@ -61,22 +55,8 @@ def get_config(config=None):
else:
bridge.update({'member' : {'interface_remove' : tmp }})
- if dict_search('member.interface', bridge) != None:
- # XXX: T2665: we need a copy of the dict keys for iteration, else we will get:
- # RuntimeError: dictionary changed size during iteration
+ if dict_search('member.interface', bridge) is not None:
for interface in list(bridge['member']['interface']):
- for key in ['cost', 'priority']:
- if interface == key:
- del bridge['member']['interface'][key]
- continue
-
- # the default dictionary is not properly paged into the dict (see T2665)
- # thus we will ammend it ourself
- default_member_values = defaults(base + ['member', 'interface'])
- for interface,interface_config in bridge['member']['interface'].items():
- bridge['member']['interface'][interface] = dict_merge(
- default_member_values, bridge['member']['interface'][interface])
-
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
if tmp and bridge['ifname'] not in tmp:
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 31cfab368..f3e65ad5e 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -40,9 +40,9 @@ from vyos.pki import encode_certificate
from vyos.pki import load_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.util import write_file
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.file import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -145,12 +145,6 @@ def verify(ethernet):
raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
'for MTU size larger then 1500 bytes')
- # XDP requires multiple TX queues
- if 'xdp' in ethernet:
- queues = glob(f'/sys/class/net/{ifname}/queues/tx-*')
- if len(queues) < 2:
- raise ConfigError('XDP requires additional TX queues, too few available!')
-
if {'is_bond_member', 'mac'} <= set(ethernet):
Warning(f'changing mac address "{mac}" will be ignored as "{ifname}" ' \
f'is a member of bond "{is_bond_member}"'.format(**ethernet))
@@ -192,14 +186,15 @@ def generate(ethernet):
if 'ca_certificate' in ethernet['eapol']:
ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
- ca_cert_name = ethernet['eapol']['ca_certificate']
- pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
+ ca_chains = []
- loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
- ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+ for ca_cert_name in ethernet['eapol']['ca_certificate']:
+ pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
+ loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+ ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+ ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain))
- write_file(ca_cert_file_path,
- '\n'.join(encode_certificate(c) for c in ca_full_chain))
+ write_file(ca_cert_file_path, '\n'.join(ca_chains))
return None
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index ca321e01d..e1db3206e 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -28,8 +28,8 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import L2TPv3If
-from vyos.util import check_kmod
-from vyos.validate import is_addr_assigned
+from vyos.utils.kernel import check_kmod
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py
index 649ea8d50..0a927ac88 100755
--- a/src/conf_mode/interfaces-macsec.py
+++ b/src/conf_mode/interfaces-macsec.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
@@ -33,9 +33,9 @@ from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import MACsecIf
from vyos.ifconfig import Interface
from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.util import is_systemd_service_running
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -43,6 +43,14 @@ airbag.enable()
# XXX: wpa_supplicant works on the source interface
wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf'
+# Constants
+## gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit
+GCM_AES_128_LEN: int = 32
+GCM_128_KEY_ERROR = 'gcm-aes-128 requires a 128bit long key!'
+## gcm-aes-256 requires a 256bit long key - 64 characters (string) = 32byte = 256bit
+GCM_AES_256_LEN: int = 64
+GCM_256_KEY_ERROR = 'gcm-aes-256 requires a 256bit long key!'
+
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -89,18 +97,54 @@ def verify(macsec):
raise ConfigError('Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
if dict_search('security.encrypt', macsec) != None:
- if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None:
- raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!')
+ # Check that only static or MKA config is present
+ if dict_search('security.static', macsec) != None and (dict_search('security.mka.cak', macsec) != None or dict_search('security.mka.ckn', macsec) != None):
+ raise ConfigError('Only static or MKA can be used!')
+
+ # Logic to check static configuration
+ if dict_search('security.static', macsec) != None:
+ # tx-key must be defined
+ if dict_search('security.static.key', macsec) == None:
+ raise ConfigError('Static MACsec tx-key must be defined.')
+
+ tx_len = len(dict_search('security.static.key', macsec))
+
+ if dict_search('security.cipher', macsec) == 'gcm-aes-128' and tx_len != GCM_AES_128_LEN:
+ raise ConfigError(GCM_128_KEY_ERROR)
+
+ if dict_search('security.cipher', macsec) == 'gcm-aes-256' and tx_len != GCM_AES_256_LEN:
+ raise ConfigError(GCM_256_KEY_ERROR)
+
+ # Make sure at least one peer is defined
+ if 'peer' not in macsec['security']['static']:
+ raise ConfigError('Must have at least one peer defined for static MACsec')
+
+ # For every enabled peer, make sure a MAC and rx-key is defined
+ for peer, peer_config in macsec['security']['static']['peer'].items():
+ if 'disable' not in peer_config and ('mac' not in peer_config or 'key' not in peer_config):
+ raise ConfigError('Every enabled MACsec static peer must have a MAC address and rx-key defined.')
+
+ # check rx-key length against cipher suite
+ rx_len = len(peer_config['key'])
+
+ if dict_search('security.cipher', macsec) == 'gcm-aes-128' and rx_len != GCM_AES_128_LEN:
+ raise ConfigError(GCM_128_KEY_ERROR)
+
+ if dict_search('security.cipher', macsec) == 'gcm-aes-256' and rx_len != GCM_AES_256_LEN:
+ raise ConfigError(GCM_256_KEY_ERROR)
+
+ # Logic to check MKA configuration
+ else:
+ if dict_search('security.mka.cak', macsec) == None or dict_search('security.mka.ckn', macsec) == None:
+ raise ConfigError('Missing mandatory MACsec security keys as encryption is enabled!')
- cak_len = len(dict_search('security.mka.cak', macsec))
+ cak_len = len(dict_search('security.mka.cak', macsec))
- if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != 32:
- # gcm-aes-128 requires a 128bit long key - 32 characters (string) = 16byte = 128bit
- raise ConfigError('gcm-aes-128 requires a 128bit long key!')
+ if dict_search('security.cipher', macsec) == 'gcm-aes-128' and cak_len != GCM_AES_128_LEN:
+ raise ConfigError(GCM_128_KEY_ERROR)
- elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != 64:
- # gcm-aes-128 requires a 128bit long key - 64 characters (string) = 32byte = 256bit
- raise ConfigError('gcm-aes-128 requires a 256bit long key!')
+ elif dict_search('security.cipher', macsec) == 'gcm-aes-256' and cak_len != GCM_AES_256_LEN:
+ raise ConfigError(GCM_256_KEY_ERROR)
if 'source_interface' in macsec:
# MACsec adds a 40 byte overhead (32 byte MACsec + 8 bytes VLAN 802.1ad
@@ -115,7 +159,9 @@ def verify(macsec):
def generate(macsec):
- render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)
+ # Only generate wpa_supplicant config if using MKA
+ if dict_search('security.mka.cak', macsec):
+ render(wpa_suppl_conf.format(**macsec), 'macsec/wpa_supplicant.conf.j2', macsec)
return None
@@ -142,8 +188,10 @@ def apply(macsec):
i = MACsecIf(**macsec)
i.update(macsec)
- if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec:
- call(f'systemctl reload-or-restart {systemd_service}')
+ # Only reload/restart if using MKA
+ if dict_search('security.mka.cak', macsec):
+ if not is_systemd_service_running(systemd_service) or 'shutdown_required' in macsec:
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 6f227b0d1..1d0feb56f 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -50,16 +50,18 @@ from vyos.pki import wrap_private_key
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 chown
-from vyos.util import cmd
-from vyos.util import dict_search
-from vyos.util import dict_search_args
-from vyos.util import is_list_equal
-from vyos.util import makedir
-from vyos.util import read_file
-from vyos.util import write_file
-from vyos.validate import is_addr_assigned
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_search_args
+from vyos.utils.list import is_list_equal
+from vyos.utils.file import makedir
+from vyos.utils.file import read_file
+from vyos.utils.file import write_file
+from vyos.utils.kernel import check_kmod
+from vyos.utils.kernel import unload_kmod
+from vyos.utils.process import call
+from vyos.utils.permission import chown
+from vyos.utils.process import cmd
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
@@ -86,30 +88,45 @@ def get_config(config=None):
conf = Config()
base = ['interfaces', 'openvpn']
- tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
ifname, openvpn = get_interface_dict(conf, base)
-
- if 'deleted' not in openvpn:
- openvpn['pki'] = tmp_pki
- if is_node_changed(conf, base + [ifname, 'openvpn-option']):
- openvpn.update({'restart_required': {}})
-
- # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
- # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
- tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True)
-
- # We have to cleanup the config dict, as default values could enable features
- # which are not explicitly enabled on the CLI. Example: server mfa totp
- # originate comes with defaults, which will enable the
- # totp plugin, even when not set via CLI so we
- # need to check this first and drop those keys
- if dict_search('server.mfa.totp', tmp) == None:
- del openvpn['server']['mfa']
-
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
+ if 'deleted' in openvpn:
+ return openvpn
+
+ openvpn['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ if is_node_changed(conf, base + [ifname, 'openvpn-option']):
+ openvpn.update({'restart_required': {}})
+ if is_node_changed(conf, base + [ifname, 'enable-dco']):
+ openvpn.update({'restart_required': {}})
+
+ # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
+ # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
+ tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True)
+
+ # We have to cleanup the config dict, as default values could enable features
+ # which are not explicitly enabled on the CLI. Example: server mfa totp
+ # originate comes with defaults, which will enable the
+ # totp plugin, even when not set via CLI so we
+ # need to check this first and drop those keys
+ if dict_search('server.mfa.totp', tmp) == None:
+ del openvpn['server']['mfa']
+
+ # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all
+ # OpenVPN interfaces. Check if DCO is used by any other interface instance.
+ tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ for interface, interface_config in tmp.items():
+ # If one interface has DCO configured, enable it. No need to further check
+ # all other OpenVPN interfaces. We must use a dedicated key to indicate
+ # the Kernel module must be loaded or not. The per interface "offload.dco"
+ # key is required per OpenVPN interface instance.
+ if dict_search('offload.dco', interface_config) != None:
+ openvpn['module_load_dco'] = {}
+ break
+
return openvpn
def is_ec_private_key(pki, cert_name):
@@ -149,17 +166,23 @@ def verify_pki(openvpn):
raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}')
if tls:
- if 'ca_certificate' not in tls:
- raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}')
+ if (mode in ['server', 'client']) and ('ca_certificate' not in tls):
+ raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface},\
+ it is required in server and client modes')
+ else:
+ if ('ca_certificate' not in tls) and ('peer_fingerprint' not in tls):
+ raise ConfigError('Either "tls ca-certificate" or "tls peer-fingerprint" is required\
+ on openvpn interface {interface} in site-to-site mode')
- for ca_name in tls['ca_certificate']:
- if ca_name not in pki['ca']:
- raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
+ if 'ca_certificate' in tls:
+ for ca_name in tls['ca_certificate']:
+ if ca_name not in pki['ca']:
+ raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
- if len(tls['ca_certificate']) > 1:
- sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca'])
- if not verify_ca_chain(sorted_chain, pki['ca']):
- raise ConfigError(f'CA certificates are not a valid chain')
+ if len(tls['ca_certificate']) > 1:
+ sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca'])
+ if not verify_ca_chain(sorted_chain, pki['ca']):
+ raise ConfigError(f'CA certificates are not a valid chain')
if mode != 'client' and 'auth_key' not in tls:
if 'certificate' not in tls:
@@ -172,16 +195,7 @@ def verify_pki(openvpn):
if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected') is not None:
raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}')
- if mode == 'server' and 'dh_params' not in tls and not is_ec_private_key(pki, tls['certificate']):
- raise ConfigError('Must specify "tls dh-params" when not using EC keys in server mode')
-
if 'dh_params' in tls:
- if 'dh' not in pki:
- raise ConfigError('There are no DH parameters in PKI configuration')
-
- if tls['dh_params'] not in pki['dh']:
- raise ConfigError(f'Invalid dh-params on openvpn interface {interface}')
-
pki_dh = pki['dh'][tls['dh_params']]
dh_params = load_dh_parameters(pki_dh['parameters'])
dh_numbers = dh_params.parameter_numbers()
@@ -190,6 +204,7 @@ def verify_pki(openvpn):
if dh_bits < 2048:
raise ConfigError(f'Minimum DH key-size is 2048 bits')
+
if 'auth_key' in tls or 'crypt_key' in tls:
if not dict_search_args(pki, 'openvpn', 'shared_secret'):
raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
@@ -478,9 +493,6 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-active':
raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
- if not dict_search('tls.dh_params', openvpn):
- raise ConfigError('Must specify "tls dh-params" when "tls role" is "passive"')
-
if 'certificate' in openvpn['tls'] and is_ec_private_key(openvpn['pki'], openvpn['tls']['certificate']):
if 'dh_params' in openvpn['tls']:
print('Warning: using dh-params and EC keys simultaneously will ' \
@@ -670,6 +682,15 @@ def apply(openvpn):
if interface in interfaces():
VTunIf(interface).remove()
+ # dynamically load/unload DCO Kernel extension if requested
+ dco_module = 'ovpn_dco_v2'
+ if 'module_load_dco' in openvpn:
+ check_kmod(dco_module)
+ else:
+ unload_kmod(dco_module)
+
+ # Now bail out early if interface is disabled or got deleted
+ if 'deleted' in openvpn or 'disable' in openvpn:
return None
# verify specified IP address is present on any interface on this system
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index 5f0b76f90..fca91253c 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -32,8 +32,8 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import PPPoEIf
from vyos.template import render
-from vyos.util import call
-from vyos.util import is_systemd_service_running
+from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/interfaces-sstpc.py b/src/conf_mode/interfaces-sstpc.py
index b5cc4cf4e..b588910dc 100755
--- a/src/conf_mode/interfaces-sstpc.py
+++ b/src/conf_mode/interfaces-sstpc.py
@@ -27,10 +27,10 @@ from vyos.pki import encode_certificate
from vyos.pki import find_chain
from vyos.pki import load_certificate
from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.util import is_systemd_service_running
-from vyos.util import write_file
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.file import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 0a3726e94..91aed9cc3 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -33,8 +33,8 @@ from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import Interface
from vyos.ifconfig import Section
from vyos.ifconfig import TunnelIf
-from vyos.util import get_interface_config
-from vyos.util import dict_search
+from vyos.utils.network import get_interface_config
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -55,6 +55,9 @@ def get_config(config=None):
tmp = is_node_changed(conf, base + [ifname, 'encapsulation'])
if tmp: tunnel.update({'encapsulation_changed': {}})
+ tmp = is_node_changed(conf, base + [ifname, 'parameters', 'ip', 'key'])
+ if tmp: tunnel.update({'key_changed': {}})
+
# We also need to inspect other configured tunnels as there are Kernel
# restrictions where we need to comply. E.g. GRE tunnel key can't be used
# twice, or with multiple GRE tunnels to the same location we must specify
@@ -197,7 +200,8 @@ def apply(tunnel):
remote = dict_search('linkinfo.info_data.remote', tmp)
if ('deleted' in tunnel or 'encapsulation_changed' in tunnel or encap in
- ['gretap', 'ip6gretap', 'erspan', 'ip6erspan'] or remote in ['any']):
+ ['gretap', 'ip6gretap', 'erspan', 'ip6erspan'] or remote in ['any'] or
+ 'key_changed' in tunnel):
if interface in interfaces():
tmp = Interface(interface)
tmp.remove()
diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py
index f4b0436af..9871810ae 100755
--- a/src/conf_mode/interfaces-vti.py
+++ b/src/conf_mode/interfaces-vti.py
@@ -21,7 +21,7 @@ from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import VTIIf
-from vyos.util import dict_search
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index b1536148c..a3b0867e0 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -87,8 +87,8 @@ def verify(vxlan):
raise ConfigError('Multicast VXLAN requires an underlaying interface')
verify_source_interface(vxlan)
- if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan):
- raise ConfigError('Group, remote or source-address must be configured')
+ if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan):
+ raise ConfigError('Group, remote, source-address or source-interface must be configured')
if 'vni' not in vxlan and 'external' not in vxlan:
raise ConfigError(
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 762bad94f..122d9589a 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.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
@@ -27,12 +27,14 @@ from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import WireGuardIf
-from vyos.util import check_kmod
-from vyos.util import check_port_availability
+from vyos.utils.kernel import check_kmod
+from vyos.utils.network import check_port_availability
+from vyos.utils.network import is_wireguard_key_pair
from vyos import ConfigError
from vyos import airbag
airbag.enable()
+
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -88,7 +90,6 @@ def verify(wireguard):
# run checks on individual configured WireGuard peer
public_keys = []
-
for tmp in wireguard['peer']:
peer = wireguard['peer'][tmp]
@@ -105,6 +106,10 @@ def verify(wireguard):
if peer['public_key'] in public_keys:
raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
+ if 'disable' not in peer:
+ if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']):
+ raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"')
+
public_keys.append(peer['public_key'])
def apply(wireguard):
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index dd798b5a2..02b4a2500 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -25,16 +25,14 @@ from vyos.configdict import get_interface_dict
from vyos.configdict import dict_merge
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
-from vyos.configverify import verify_dhcpv6
-from vyos.configverify import verify_source_interface
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bond_bridge_member
from vyos.ifconfig import WiFiIf
from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -42,6 +40,8 @@ airbag.enable()
# XXX: wpa_supplicant works on the source interface
wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
hostapd_conf = '/run/hostapd/{ifname}.conf'
+hostapd_accept_station_conf = '/run/hostapd/{ifname}_station_accept.conf'
+hostapd_deny_station_conf = '/run/hostapd/{ifname}_station_deny.conf'
def find_other_stations(conf, base, ifname):
"""
@@ -79,30 +79,14 @@ def get_config(config=None):
ifname, wifi = get_interface_dict(conf, base)
- # Cleanup "delete" default values when required user selectable values are
- # not defined at all
- tmp = conf.get_config_dict(base + [ifname], key_mangling=('-', '_'),
- get_first_key=True)
- if not (dict_search('security.wpa.passphrase', tmp) or
- dict_search('security.wpa.radius', tmp)):
- if 'deleted' not in wifi:
+ if 'deleted' not in wifi:
+ # then get_interface_dict provides default keys
+ if wifi.from_defaults(['security', 'wep']): # if not set by user
+ del wifi['security']['wep']
+ if wifi.from_defaults(['security', 'wpa']): # if not set by user
del wifi['security']['wpa']
- # if 'security' key is empty, drop it too
- if len(wifi['security']) == 0:
- del wifi['security']
-
- # defaults include RADIUS server specifics per TAG node which need to be
- # added to individual RADIUS servers instead - so we can simply delete them
- if dict_search('security.wpa.radius.server.port', wifi) != None:
- del wifi['security']['wpa']['radius']['server']['port']
- if not len(wifi['security']['wpa']['radius']['server']):
- del wifi['security']['wpa']['radius']
- if not len(wifi['security']['wpa']):
- del wifi['security']['wpa']
- if not len(wifi['security']):
- del wifi['security']
- if 'security' in wifi and 'wpa' in wifi['security']:
+ if dict_search('security.wpa', wifi) != None:
wpa_cipher = wifi['security']['wpa'].get('cipher')
wpa_mode = wifi['security']['wpa'].get('mode')
if not wpa_cipher:
@@ -120,13 +104,9 @@ def get_config(config=None):
tmp = find_other_stations(conf, base, wifi['ifname'])
if tmp: wifi['station_interfaces'] = tmp
- # Add individual RADIUS server default values
- if dict_search('security.wpa.radius.server', wifi):
- default_values = defaults(base + ['security', 'wpa', 'radius', 'server'])
-
- for server in dict_search('security.wpa.radius.server', wifi):
- wifi['security']['wpa']['radius']['server'][server] = dict_merge(
- default_values, wifi['security']['wpa']['radius']['server'][server])
+ # used in hostapt.conf.j2
+ wifi['hostapd_accept_station_conf'] = hostapd_accept_station_conf.format(**wifi)
+ wifi['hostapd_deny_station_conf'] = hostapd_deny_station_conf.format(**wifi)
return wifi
@@ -142,7 +122,7 @@ def verify(wifi):
raise ConfigError('You must specify a WiFi mode')
if 'ssid' not in wifi and wifi['type'] != 'monitor':
- raise ConfigError('SSID must be configured')
+ raise ConfigError('SSID must be configured unless type is set to "monitor"!')
if wifi['type'] == 'access-point':
if 'country_code' not in wifi:
@@ -215,7 +195,10 @@ def generate(wifi):
if 'deleted' in wifi:
if os.path.isfile(hostapd_conf.format(**wifi)):
os.unlink(hostapd_conf.format(**wifi))
-
+ if os.path.isfile(hostapd_accept_station_conf.format(**wifi)):
+ os.unlink(hostapd_accept_station_conf.format(**wifi))
+ if os.path.isfile(hostapd_deny_station_conf.format(**wifi)):
+ os.unlink(hostapd_deny_station_conf.format(**wifi))
if os.path.isfile(wpa_suppl_conf.format(**wifi)):
os.unlink(wpa_suppl_conf.format(**wifi))
@@ -250,12 +233,12 @@ def generate(wifi):
# render appropriate new config files depending on access-point or station mode
if wifi['type'] == 'access-point':
- render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2',
- wifi)
+ render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.j2', wifi)
+ render(hostapd_accept_station_conf.format(**wifi), 'wifi/hostapd_accept_station.conf.j2', wifi)
+ render(hostapd_deny_station_conf.format(**wifi), 'wifi/hostapd_deny_station.conf.j2', wifi)
elif wifi['type'] == 'station':
- render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.j2',
- wifi)
+ render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.j2', wifi)
return None
diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py
index 9ca495476..2515dc838 100755
--- a/src/conf_mode/interfaces-wwan.py
+++ b/src/conf_mode/interfaces-wwan.py
@@ -27,12 +27,12 @@ from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
from vyos.ifconfig import WWANIf
-from vyos.util import cmd
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.util import DEVNULL
-from vyos.util import is_systemd_service_active
-from vyos.util import write_file
+from vyos.utils.dict import dict_search
+from vyos.utils.process import cmd
+from vyos.utils.process import call
+from vyos.utils.process import DEVNULL
+from vyos.utils.process import is_systemd_service_active
+from vyos.utils.file import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -75,7 +75,6 @@ def get_config(config=None):
# We need to know the amount of other WWAN interfaces as ModemManager needs
# to be started or stopped.
- conf.set_level(base)
wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),
get_first_key=True,
no_tag_node_value_mangle=True)
diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py
index 6e169a3d5..06c7e7b72 100755
--- a/src/conf_mode/le_cert.py
+++ b/src/conf_mode/le_cert.py
@@ -20,9 +20,9 @@ import os
import vyos.defaults
from vyos.config import Config
from vyos import ConfigError
-from vyos.util import cmd
-from vyos.util import call
-from vyos.util import is_systemd_service_running
+from vyos.utils.process import cmd
+from vyos.utils.process import call
+from vyos.utils.process import is_systemd_service_running
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/lldp.py b/src/conf_mode/lldp.py
index c703c1fe0..c2e87d171 100755
--- a/src/conf_mode/lldp.py
+++ b/src/conf_mode/lldp.py
@@ -20,13 +20,11 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.validate import is_addr_assigned
-from vyos.validate import is_loopback_addr
+from vyos.utils.network import is_addr_assigned
+from vyos.utils.network import is_loopback_addr
from vyos.version import get_version_data
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
from vyos.template import render
from vyos import ConfigError
from vyos import airbag
@@ -46,7 +44,9 @@ def get_config(config=None):
return {}
lldp = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
if conf.exists(['service', 'snmp']):
lldp['system_snmp_enabled'] = ''
@@ -54,27 +54,12 @@ def get_config(config=None):
version_data = get_version_data()
lldp['version'] = version_data['version']
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- # location coordinates have a default value
- if 'interface' in lldp:
- for interface, interface_config in lldp['interface'].items():
- default_values = defaults(base + ['interface'])
- if dict_search('location.coordinate_based', interface_config) == None:
- # no location specified - no need to add defaults
- del default_values['location']['coordinate_based']['datum']
- del default_values['location']['coordinate_based']['altitude']
-
- # cleanup default_values dictionary from inner to outer
- # this might feel overkill here, but it does support easy extension
- # in the future with additional default values
- if len(default_values['location']['coordinate_based']) == 0:
- del default_values['location']['coordinate_based']
- if len(default_values['location']) == 0:
- del default_values['location']
-
- lldp['interface'][interface] = dict_merge(default_values,
- lldp['interface'][interface])
+ # prune location information if not set by user
+ for interface in lldp.get('interface', []):
+ if lldp.from_defaults(['interface', interface, 'location']):
+ del lldp['interface'][interface]['location']
+ elif lldp.from_defaults(['interface', interface, 'location','coordinate_based']):
+ del lldp['interface'][interface]['location']['coordinate_based']
return lldp
diff --git a/src/conf_mode/load-balancing-haproxy.py b/src/conf_mode/load-balancing-haproxy.py
index b29fdffc7..8fe429653 100755
--- a/src/conf_mode/load-balancing-haproxy.py
+++ b/src/conf_mode/load-balancing-haproxy.py
@@ -20,14 +20,12 @@ from sys import exit
from shutil import rmtree
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.util import call
-from vyos.util import check_port_availability
-from vyos.util import is_listen_port_bind_service
+from vyos.utils.process import call
+from vyos.utils.network import check_port_availability
+from vyos.utils.network import is_listen_port_bind_service
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -54,18 +52,8 @@ def get_config(config=None):
lb['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- default_values = defaults(base)
- if 'backend' in default_values:
- del default_values['backend']
if lb:
- lb = dict_merge(default_values, lb)
-
- if 'backend' in lb:
- for backend in lb['backend']:
- default_balues_backend = defaults(base + ['backend'])
- lb['backend'][backend] = dict_merge(default_balues_backend, lb['backend'][backend])
+ lb = conf.merge_defaults(lb, recursive=True)
return lb
diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py
index 7086aaf8b..ad9c80d72 100755
--- a/src/conf_mode/load-balancing-wan.py
+++ b/src/conf_mode/load-balancing-wan.py
@@ -21,10 +21,8 @@ from shutil import rmtree
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.util import cmd
+from vyos.utils.process import cmd
from vyos.template import render
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -41,48 +39,15 @@ def get_config(config=None):
conf = Config()
base = ['load-balancing', 'wan']
- lb = conf.get_config_dict(base,
+ lb = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
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])
+ with_recursive_defaults=True)
+
+ # prune limit key if not set by user
+ for rule in lb.get('rule', []):
+ if lb.from_defaults(['rule', rule, 'limit']):
+ del lb['rule'][rule]['limit']
return lb
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 9f8221514..9da7fbe80 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.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
@@ -25,16 +25,14 @@ from netifaces import interfaces
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.template import render
from vyos.template import is_ip_network
-from vyos.util import cmd
-from vyos.util import run
-from vyos.util import check_kmod
-from vyos.util import dict_search
-from vyos.util import dict_search_args
-from vyos.validate import is_addr_assigned
-from vyos.xml import defaults
+from vyos.utils.kernel import check_kmod
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_search_args
+from vyos.utils.process import cmd
+from vyos.utils.process import run
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
@@ -72,6 +70,7 @@ def verify_rule(config, err_msg, groups_dict):
""" Common verify steps used for both source and destination NAT """
if (dict_search('translation.port', config) != None or
+ dict_search('translation.redirect.port', config) != None or
dict_search('destination.port', config) != None or
dict_search('source.port', config)):
@@ -125,6 +124,18 @@ def verify_rule(config, err_msg, groups_dict):
if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port-group')
+ if 'load_balance' in config:
+ for item in ['source-port', 'destination-port']:
+ if item in config['load_balance']['hash'] and config['protocol'] not in ['tcp', 'udp']:
+ raise ConfigError('Protocol must be tcp or udp when specifying hash ports')
+ count = 0
+ if 'backend' in config['load_balance']:
+ for member in config['load_balance']['backend']:
+ weight = config['load_balance']['backend'][member]['weight']
+ count = count + int(weight)
+ if count != 100:
+ Warning(f'Sum of weight for nat load balance rule is not 100. You may get unexpected behaviour')
+
def get_config(config=None):
if config:
conf = config
@@ -132,16 +143,9 @@ def get_config(config=None):
conf = Config()
base = ['nat']
- nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-
- # T2665: we must add the tagNode defaults individually until this is
- # moved to the base class
- for direction in ['source', 'destination', 'static']:
- if direction in nat:
- default_values = defaults(base + [direction, 'rule'])
- for rule in dict_search(f'{direction}.rule', nat) or []:
- nat[direction]['rule'][rule] = dict_merge(default_values,
- nat[direction]['rule'][rule])
+ nat = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
# read in current nftable (once) for further processing
tmp = cmd('nft -j list table raw')
@@ -198,7 +202,7 @@ def verify(nat):
Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
if not dict_search('translation.address', config) and not dict_search('translation.port', config):
- if 'exclude' not in config:
+ if 'exclude' not in config and 'backend' not in config['load_balance']:
raise ConfigError(f'{err_msg} translation requires address and/or port')
addr = dict_search('translation.address', config)
@@ -210,7 +214,6 @@ def verify(nat):
# common rule verification
verify_rule(config, err_msg, nat['firewall_group'])
-
if dict_search('destination.rule', nat):
for rule, config in dict_search('destination.rule', nat).items():
err_msg = f'Destination NAT configuration error in rule {rule}:'
@@ -221,8 +224,8 @@ def verify(nat):
elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():
Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')
- if not dict_search('translation.address', config) and not dict_search('translation.port', config):
- if 'exclude' not in config:
+ if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']:
+ if 'exclude' not in config and 'backend' not in config['load_balance']:
raise ConfigError(f'{err_msg} translation requires address and/or port')
# common rule verification
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index d8f913b0c..4c12618bc 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -23,13 +23,11 @@ from netifaces import interfaces
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.template import render
-from vyos.util import cmd
-from vyos.util import check_kmod
-from vyos.util import dict_search
+from vyos.utils.process import cmd
+from vyos.utils.kernel import check_kmod
+from vyos.utils.dict import dict_search
from vyos.template import is_ipv6
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -60,16 +58,6 @@ def get_config(config=None):
base = ['nat66']
nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- # T2665: we must add the tagNode defaults individually until this is
- # moved to the base class
- for direction in ['source', 'destination']:
- if direction in nat:
- default_values = defaults(base + [direction, 'rule'])
- if 'rule' in nat[direction]:
- for rule in nat[direction]['rule']:
- nat[direction]['rule'][rule] = dict_merge(default_values,
- nat[direction]['rule'][rule])
-
# read in current nftable (once) for further processing
tmp = cmd('nft -j list table ip6 raw')
nftable_json = json.loads(tmp)
diff --git a/src/conf_mode/netns.py b/src/conf_mode/netns.py
index 20129ce65..95ab83dbc 100755
--- a/src/conf_mode/netns.py
+++ b/src/conf_mode/netns.py
@@ -22,9 +22,9 @@ from tempfile import NamedTemporaryFile
from vyos.config import Config
from vyos.configdict import node_changed
from vyos.ifconfig import Interface
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.util import get_interface_config
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.network import get_interface_config
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py
index 92cb73aab..1cc23a7df 100755
--- a/src/conf_mode/ntp.py
+++ b/src/conf_mode/ntp.py
@@ -20,10 +20,11 @@ from vyos.config import Config
from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.configverify import verify_interface_exists
-from vyos.util import call
-from vyos.util import chmod_750
-from vyos.util import get_interface_config
+from vyos.utils.process import call
+from vyos.utils.permission import chmod_750
+from vyos.utils.network import get_interface_config
from vyos.template import render
+from vyos.template import is_ipv4
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -62,16 +63,29 @@ def verify(ntp):
if 'interface' in ntp:
# If ntpd should listen on a given interface, ensure it exists
- for interface in ntp['interface']:
- verify_interface_exists(interface)
-
- # If we run in a VRF, our interface must belong to this VRF, too
- if 'vrf' in ntp:
- tmp = get_interface_config(interface)
- vrf_name = ntp['vrf']
- if 'master' not in tmp or tmp['master'] != vrf_name:
- raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\
- f'does not belong to this VRF!')
+ interface = ntp['interface']
+ verify_interface_exists(interface)
+
+ # If we run in a VRF, our interface must belong to this VRF, too
+ if 'vrf' in ntp:
+ tmp = get_interface_config(interface)
+ vrf_name = ntp['vrf']
+ if 'master' not in tmp or tmp['master'] != vrf_name:
+ raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\
+ f'does not belong to this VRF!')
+
+ if 'listen_address' in ntp:
+ ipv4_addresses = 0
+ ipv6_addresses = 0
+ for address in ntp['listen_address']:
+ if is_ipv4(address):
+ ipv4_addresses += 1
+ else:
+ ipv6_addresses += 1
+ if ipv4_addresses > 1:
+ raise ConfigError(f'NTP Only admits one ipv4 value for listen-address parameter ')
+ if ipv6_addresses > 1:
+ raise ConfigError(f'NTP Only admits one ipv6 value for listen-address parameter ')
return None
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index 54de467ca..34ba2fe69 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -18,7 +18,6 @@ from sys import exit
from vyos.config import Config
from vyos.configdep import set_dependents, call_dependents
-from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.pki import is_ca_certificate
from vyos.pki import load_certificate
@@ -26,9 +25,8 @@ from vyos.pki import load_public_key
from vyos.pki import load_private_key
from vyos.pki import load_crl
from vyos.pki import load_dh_parameters
-from vyos.util import dict_search_args
-from vyos.util import dict_search_recursive
-from vyos.xml import defaults
+from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search_recursive
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -113,8 +111,7 @@ def get_config(config=None):
# We only merge on the defaults of there is a configuration at all
if conf.exists(base):
- default_values = defaults(base)
- pki = dict_merge(default_values, pki)
+ pki = conf.merge_defaults(pki, recursive=True)
# We need to get the entire system configuration to verify that we are not
# deleting a certificate that is still referenced somewhere!
diff --git a/src/conf_mode/policy-local-route.py b/src/conf_mode/policy-local-route.py
index 3f834f55c..79526f82a 100755
--- a/src/conf_mode/policy-local-route.py
+++ b/src/conf_mode/policy-local-route.py
@@ -24,7 +24,7 @@ from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configdict import leaf_node_changed
from vyos.template import render
-from vyos.util import call
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py
index 40a32efb3..adad012de 100755
--- a/src/conf_mode/policy-route.py
+++ b/src/conf_mode/policy-route.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
@@ -22,9 +22,9 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
from vyos.template import render
-from vyos.util import cmd
-from vyos.util import dict_search_args
-from vyos.util import run
+from vyos.utils.dict import dict_search_args
+from vyos.utils.process import cmd
+from vyos.utils.process import run
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index 331194fec..4df893ebf 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -19,7 +19,7 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.template import render_to_string
-from vyos.util import dict_search
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import frr
from vyos import airbag
diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py
index 20821c7f2..104711b55 100755
--- a/src/conf_mode/protocols_babel.py
+++ b/src/conf_mode/protocols_babel.py
@@ -19,13 +19,13 @@ import os
from sys import exit
from vyos.config import Config
+from vyos.config import config_dict_merge
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.utils.dict import dict_search
from vyos.template import render_to_string
from vyos import ConfigError
from vyos import frr
@@ -38,7 +38,8 @@ def get_config(config=None):
else:
conf = Config()
base = ['protocols', 'babel']
- babel = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ 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
@@ -54,15 +55,13 @@ def get_config(config=None):
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)
+ # values which we need to update into the dictionary retrieved.
+ default_values = conf.get_config_defaults(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ recursive=True)
- # 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)
+ # merge in default values
+ babel = config_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().
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index 0436abaf9..dab784662 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -17,12 +17,10 @@
import os
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configverify import verify_vrf
from vyos.template import is_ipv6
from vyos.template import render_to_string
-from vyos.validate import is_ipv6_link_local
-from vyos.xml import defaults
+from vyos.utils.network import is_ipv6_link_local
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -41,18 +39,7 @@ def get_config(config=None):
if not conf.exists(base):
return bfd
- # We have gathered the dict representation of the CLI, but there are
- # default options which we need to update into the dictionary retrived.
- # XXX: T2665: we currently have no nice way for defaults under tag
- # nodes, thus we load the defaults "by hand"
- default_values = defaults(base + ['peer'])
- if 'peer' in bfd:
- for peer in bfd['peer']:
- bfd['peer'][peer] = dict_merge(default_values, bfd['peer'][peer])
-
- if 'profile' in bfd:
- for profile in bfd['profile']:
- bfd['profile'][profile] = dict_merge(default_values, bfd['profile'][profile])
+ bfd = conf.merge_defaults(bfd, recursive=True)
return bfd
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index b23584bdb..00015023c 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-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
@@ -20,14 +20,16 @@ from sys import argv
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.configdict import node_changed
from vyos.configverify import verify_prefix_list
from vyos.configverify import verify_route_map
from vyos.configverify import verify_vrf
from vyos.template import is_ip
from vyos.template import is_interface
from vyos.template import render_to_string
-from vyos.util import dict_search
-from vyos.validate import is_addr_assigned
+from vyos.utils.dict import dict_search
+from vyos.utils.network import get_interface_vrf
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -55,6 +57,12 @@ def get_config(config=None):
get_first_key=True,
no_tag_node_value_mangle=True)
+ # Remove per interface MPLS configuration - get a list if changed
+ # nodes under the interface tagNode
+ interfaces_removed = node_changed(conf, base + ['interface'])
+ if interfaces_removed:
+ bgp['interface_removed'] = list(interfaces_removed)
+
# 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.
@@ -195,14 +203,21 @@ def verify_remote_as(peer_config, bgp_config):
return None
def verify_afi(peer_config, bgp_config):
+ # If address_family configured under neighboor
if 'address_family' in peer_config:
return True
+ # If address_family configured under peer-group
+ # if neighbor interface configured
+ peer_group_name = ''
+ if dict_search('interface.peer_group', peer_config):
+ peer_group_name = peer_config['interface']['peer_group']
+ # if neighbor IP configured.
if 'peer_group' in peer_config:
peer_group_name = peer_config['peer_group']
+ if peer_group_name:
tmp = dict_search(f'peer_group.{peer_group_name}.address_family', bgp_config)
if tmp: return True
-
return False
def verify(bgp):
@@ -231,6 +246,18 @@ def verify(bgp):
if 'system_as' not in bgp:
raise ConfigError('BGP system-as number must be defined!')
+ # Verify vrf on interface and bgp section
+ if 'interface' in bgp:
+ for interface in bgp['interface']:
+ error_msg = f'Interface "{interface}" belongs to different VRF instance'
+ tmp = get_interface_vrf(interface)
+ if 'vrf' in bgp:
+ if bgp['vrf'] != tmp:
+ vrf = bgp['vrf']
+ raise ConfigError(f'{error_msg} "{vrf}"!')
+ elif tmp != 'default':
+ raise ConfigError(f'{error_msg} "{tmp}"!')
+
# Common verification for both peer-group and neighbor statements
for neighbor in ['neighbor', 'peer_group']:
# bail out early if there is no neighbor or peer-group statement
@@ -448,6 +475,8 @@ def verify(bgp):
if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']):
raise ConfigError(
'Command "import vrf" conflicts with "rd vpn export" command!')
+ if not dict_search('parameters.router_id', bgp):
+ Warning(f'BGP "router-id" is required when using "rd" and "route-target"!')
if dict_search('route_target.vpn.both', afi_config):
if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']):
@@ -520,6 +549,14 @@ def apply(bgp):
vrf = ' vrf ' + bgp['vrf']
frr_cfg.load_configuration(bgp_daemon)
+
+ # Remove interface specific config
+ for key in ['interface', 'interface_removed']:
+ if key not in bgp:
+ continue
+ for interface in bgp[key]:
+ frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
+
frr_cfg.modify_section(f'^router bgp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True)
if 'frr_bgpd_config' in bgp:
frr_cfg.add_before(frr.default_add_before, bgp['frr_bgpd_config'])
diff --git a/src/conf_mode/protocols_failover.py b/src/conf_mode/protocols_failover.py
index 85e984afe..e7e44db84 100755
--- a/src/conf_mode/protocols_failover.py
+++ b/src/conf_mode/protocols_failover.py
@@ -19,10 +19,8 @@ import json
from pathlib import Path
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.template import render
-from vyos.util import call
-from vyos.xml import defaults
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
@@ -42,15 +40,12 @@ def get_config(config=None):
conf = Config()
base = ['protocols', 'failover']
- failover = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ failover = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
# Set default values only if we set config
- if failover.get('route'):
- for route, route_config in failover.get('route').items():
- for next_hop, next_hop_config in route_config.get('next_hop').items():
- default_values = defaults(base + ['route'])
- failover['route'][route]['next_hop'][next_hop] = dict_merge(
- default_values['next_hop'], failover['route'][route]['next_hop'][next_hop])
+ if failover.get('route') is not None:
+ failover = conf.merge_defaults(failover, recursive=True)
return failover
diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py
index 65cc2beba..f6097e282 100755
--- a/src/conf_mode/protocols_igmp.py
+++ b/src/conf_mode/protocols_igmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 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
@@ -21,7 +21,8 @@ from sys import exit
from vyos import ConfigError
from vyos.config import Config
-from vyos.util import call, process_named_running
+from vyos.utils.process import process_named_running
+from vyos.utils.process import call
from vyos.template import render
from signal import SIGTERM
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index ecca87db0..e00c58ee4 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -25,10 +25,9 @@ from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_interface_exists
from vyos.ifconfig import Interface
-from vyos.util import dict_search
-from vyos.util import get_interface_config
+from vyos.utils.dict import dict_search
+from vyos.utils.network import get_interface_config
from vyos.template import render_to_string
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -64,19 +63,14 @@ def get_config(config=None):
if interfaces_removed:
isis['interface_removed'] = list(interfaces_removed)
- # Bail out early if configuration tree does not exist
+ # Bail out early if configuration tree does no longer exist. this must
+ # be done after retrieving the list of interfaces to be removed.
if not conf.exists(base):
isis.update({'deleted' : ''})
return isis
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- # XXX: Note that we can not call defaults(base), as defaults does not work
- # on an instance of a tag node. As we use the exact same CLI definition for
- # both the non-vrf and vrf version this is absolutely safe!
- default_values = defaults(base_path)
# merge in default values
- isis = dict_merge(default_values, isis)
+ isis = conf.merge_defaults(isis, recursive=True)
# We also need some additional information from the config, prefix-lists
# and route-maps for instance. They will be used in verify().
@@ -254,7 +248,7 @@ def apply(isis):
if key not in isis:
continue
for interface in isis[key]:
- frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
if 'frr_isisd_config' in isis:
frr_cfg.add_before(frr.default_add_before, isis['frr_isisd_config'])
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index 73af6595b..177a43444 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -21,9 +21,9 @@ from sys import exit
from glob import glob
from vyos.config import Config
from vyos.template import render_to_string
-from vyos.util import dict_search
-from vyos.util import read_file
-from vyos.util import sysctl_write
+from vyos.utils.dict import dict_search
+from vyos.utils.file import read_file
+from vyos.utils.system import sysctl_write
from vyos.configverify import verify_interface_exists
from vyos import ConfigError
from vyos import frr
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index d28ced4fd..5ec0bc9e5 100755
--- a/src/conf_mode/protocols_nhrp.py
+++ b/src/conf_mode/protocols_nhrp.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
@@ -19,8 +19,8 @@ import os
from vyos.config import Config
from vyos.configdict import node_changed
from vyos.template import render
-from vyos.util import process_named_running
-from vyos.util import run
+from vyos.utils.process import process_named_running
+from vyos.utils.process import run
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index b73483470..cddd3765e 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.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
@@ -20,6 +20,7 @@ from sys import exit
from sys import argv
from vyos.config import Config
+from vyos.config import config_dict_merge
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
@@ -27,9 +28,8 @@ from vyos.configverify import verify_route_map
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_access_list
from vyos.template import render_to_string
-from vyos.util import dict_search
-from vyos.util import get_interface_config
-from vyos.xml import defaults
+from vyos.utils.dict import dict_search
+from vyos.utils.network import get_interface_config
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -65,17 +65,15 @@ def get_config(config=None):
if interfaces_removed:
ospf['interface_removed'] = list(interfaces_removed)
- # Bail out early if configuration tree does not exist
+ # Bail out early if configuration tree does no longer exist. this must
+ # be done after retrieving the list of interfaces to be removed.
if not conf.exists(base):
ospf.update({'deleted' : ''})
return ospf
# We have gathered the dict representation of the CLI, but there are default
# options which we need to update into the dictionary retrived.
- # XXX: Note that we can not call defaults(base), as defaults does not work
- # on an instance of a tag node. As we use the exact same CLI definition for
- # both the non-vrf and vrf version this is absolutely safe!
- default_values = defaults(base_path)
+ default_values = conf.get_config_defaults(**ospf.kwargs, recursive=True)
# We have to cleanup the default dict, as default values could enable features
# which are not explicitly enabled on the CLI. Example: default-information
@@ -84,60 +82,27 @@ def get_config(config=None):
# need to check this first and probably drop that key.
if dict_search('default_information.originate', ospf) is None:
del default_values['default_information']
- if dict_search('area.area_type.nssa', ospf) is None:
- del default_values['area']['area_type']['nssa']
if 'mpls_te' not in ospf:
del default_values['mpls_te']
+ if 'graceful_restart' not in ospf:
+ del default_values['graceful_restart']
+ for area_num in default_values.get('area', []):
+ if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None:
+ del default_values['area'][area_num]['area_type']['nssa']
- 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':
- del default_values['redistribute']['table']
- continue
+ for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
if dict_search(f'redistribute.{protocol}', ospf) is None:
del default_values['redistribute'][protocol]
- # XXX: T2665: we currently have no nice way for defaults under tag nodes,
- # clean them out and add them manually :(
- del default_values['neighbor']
- del default_values['area']['virtual_link']
- del default_values['interface']
-
- # merge in remaining default values
- ospf = dict_merge(default_values, ospf)
+ for interface in ospf.get('interface', []):
+ # We need to reload the defaults on every pass b/c of
+ # hello-multiplier dependency on dead-interval
+ # If hello-multiplier is set, we need to remove the default from
+ # dead-interval.
+ if 'hello_multiplier' in ospf['interface'][interface]:
+ del default_values['interface'][interface]['dead_interval']
- if 'neighbor' in ospf:
- default_values = defaults(base + ['neighbor'])
- for neighbor in ospf['neighbor']:
- ospf['neighbor'][neighbor] = dict_merge(default_values, ospf['neighbor'][neighbor])
-
- if 'area' in ospf:
- default_values = defaults(base + ['area', 'virtual-link'])
- for area, area_config in ospf['area'].items():
- if 'virtual_link' in area_config:
- for virtual_link in area_config['virtual_link']:
- ospf['area'][area]['virtual_link'][virtual_link] = dict_merge(
- default_values, ospf['area'][area]['virtual_link'][virtual_link])
-
- if 'interface' in ospf:
- for interface in ospf['interface']:
- # We need to reload the defaults on every pass b/c of
- # hello-multiplier dependency on dead-interval
- default_values = defaults(base + ['interface'])
- # If hello-multiplier is set, we need to remove the default from
- # dead-interval.
- if 'hello_multiplier' in ospf['interface'][interface]:
- del default_values['dead_interval']
-
- ospf['interface'][interface] = dict_merge(default_values,
- ospf['interface'][interface])
-
- if 'redistribute' in ospf and 'table' in ospf['redistribute']:
- default_values = defaults(base + ['redistribute', 'table'])
- for table in ospf['redistribute']['table']:
- ospf['redistribute']['table'][table] = dict_merge(default_values,
- ospf['redistribute']['table'][table])
+ ospf = config_dict_merge(default_values, ospf)
# We also need some additional information from the config, prefix-lists
# and route-maps for instance. They will be used in verify().
@@ -250,6 +215,13 @@ def verify(ospf):
raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\
f'and no-php-flag configured at the same time.')
+ # Check route summarisation
+ if 'summary_address' in ospf:
+ for prefix, prefix_options in ospf['summary_address'].items():
+ if {'tag', 'no_advertise'} <= set(prefix_options):
+ raise ConfigError(f'Can not set both "tag" and "no-advertise" for Type-5 '\
+ f'and Type-7 route summarisation of "{prefix}"!')
+
return None
def generate(ospf):
@@ -278,7 +250,7 @@ def apply(ospf):
if key not in ospf:
continue
for interface in ospf[key]:
- frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
if 'frr_ospfd_config' in ospf:
frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config'])
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index cb21bd83c..5b1adce30 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -20,6 +20,7 @@ from sys import exit
from sys import argv
from vyos.config import Config
+from vyos.config import config_dict_merge
from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configverify import verify_common_route_maps
@@ -27,9 +28,8 @@ from vyos.configverify import verify_route_map
from vyos.configverify import verify_interface_exists
from vyos.template import render_to_string
from vyos.ifconfig import Interface
-from vyos.util import dict_search
-from vyos.util import get_interface_config
-from vyos.xml import defaults
+from vyos.utils.dict import dict_search
+from vyos.utils.network import get_interface_config
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -64,17 +64,16 @@ def get_config(config=None):
if interfaces_removed:
ospfv3['interface_removed'] = list(interfaces_removed)
- # Bail out early if configuration tree does not exist
+ # Bail out early if configuration tree does no longer exist. this must
+ # be done after retrieving the list of interfaces to be removed.
if not conf.exists(base):
ospfv3.update({'deleted' : ''})
return ospfv3
# We have gathered the dict representation of the CLI, but there are default
# options which we need to update into the dictionary retrived.
- # XXX: Note that we can not call defaults(base), as defaults does not work
- # on an instance of a tag node. As we use the exact same CLI definition for
- # both the non-vrf and vrf version this is absolutely safe!
- default_values = defaults(base_path)
+ default_values = conf.get_config_defaults(**ospfv3.kwargs,
+ recursive=True)
# We have to cleanup the default dict, as default values could enable features
# which are not explicitly enabled on the CLI. Example: default-information
@@ -83,13 +82,13 @@ def get_config(config=None):
# need to check this first and probably drop that key.
if dict_search('default_information.originate', ospfv3) is None:
del default_values['default_information']
+ if 'graceful_restart' not in ospfv3:
+ del default_values['graceful_restart']
- # XXX: T2665: we currently have no nice way for defaults under tag nodes,
- # clean them out and add them manually :(
- del default_values['interface']
+ default_values.pop('interface', {})
# merge in remaining default values
- ospfv3 = dict_merge(default_values, ospfv3)
+ ospfv3 = config_dict_merge(default_values, ospfv3)
# We also need some additional information from the config, prefix-lists
# and route-maps for instance. They will be used in verify().
@@ -168,7 +167,7 @@ def apply(ospfv3):
if key not in ospfv3:
continue
for interface in ospfv3[key]:
- frr_cfg.modify_section(f'^interface {interface}{vrf}', stop_pattern='^exit', remove_stop_mark=True)
+ frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
if 'new_frr_config' in ospfv3:
frr_cfg.add_before(frr.default_add_before, ospfv3['new_frr_config'])
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
index 78df9b6f8..0aaa0d2c6 100755
--- a/src/conf_mode/protocols_pim.py
+++ b/src/conf_mode/protocols_pim.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 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
@@ -21,7 +21,8 @@ from sys import exit
from vyos.config import Config
from vyos import ConfigError
-from vyos.util import call, process_named_running
+from vyos.utils.process import process_named_running
+from vyos.utils.process import call
from vyos.template import render
from signal import SIGTERM
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index c78d90396..bd47dfd00 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -24,8 +24,7 @@ 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.utils.dict import dict_search
from vyos.template import render_to_string
from vyos import ConfigError
from vyos import frr
@@ -55,9 +54,7 @@ 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.
- default_values = defaults(base)
- # merge in remaining default values
- rip = dict_merge(default_values, rip)
+ rip = conf.merge_defaults(rip, recursive=True)
# We also need some additional information from the config, prefix-lists
# and route-maps for instance. They will be used in verify().
diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py
index 21ff710b3..dd1550033 100755
--- a/src/conf_mode/protocols_ripng.py
+++ b/src/conf_mode/protocols_ripng.py
@@ -23,8 +23,7 @@ from vyos.configdict import dict_merge
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.utils.dict import dict_search
from vyos.template import render_to_string
from vyos import ConfigError
from vyos import frr
@@ -45,9 +44,7 @@ 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.
- default_values = defaults(base)
- # merge in remaining default values
- ripng = dict_merge(default_values, ripng)
+ ripng = conf.merge_defaults(ripng, recursive=True)
# We also need some additional information from the config, prefix-lists
# and route-maps for instance. They will be used in verify().
diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
index 62ea9c878..05e876f3b 100755
--- a/src/conf_mode/protocols_rpki.py
+++ b/src/conf_mode/protocols_rpki.py
@@ -19,10 +19,8 @@ import os
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.template import render_to_string
-from vyos.util import dict_search
-from vyos.xml import defaults
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -43,8 +41,7 @@ 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.
- default_values = defaults(base)
- rpki = dict_merge(default_values, rpki)
+ rpki = conf.merge_defaults(rpki, recursive=True)
return rpki
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index 7b6150696..5def8d645 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -47,7 +47,7 @@ def get_config(config=None):
base_path = ['protocols', 'static']
# eqivalent of the C foo ? 'a' : 'b' statement
base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path
- static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
# Assign the name of our VRF context
if vrf: static['vrf'] = vrf
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
index 6afdf31f3..7f6ae3680 100755
--- a/src/conf_mode/protocols_static_multicast.py
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -21,7 +21,7 @@ from sys import exit
from vyos import ConfigError
from vyos.config import Config
-from vyos.util import call
+from vyos.utils.process import call
from vyos.template import render
from vyos import airbag
diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py
index 1be2c283f..ad4121a49 100755
--- a/src/conf_mode/qos.py
+++ b/src/conf_mode/qos.py
@@ -36,9 +36,8 @@ from vyos.qos import RateLimiter
from vyos.qos import RoundRobin
from vyos.qos import TrafficShaper
from vyos.qos import TrafficShaperHFSC
-from vyos.util import call
-from vyos.util import dict_search_recursive
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search_recursive
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -85,75 +84,43 @@ 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)
+ for ifname in interfaces():
+ if_node = Section.get_config_path(ifname)
- if not if_node:
- continue
+ 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
- # need a hyphen
- policy_hyphen = policy.replace('_', '-')
-
- if policy in ['random_detect']:
- for rd_name, rd_config in qos['policy'][policy].items():
- # There are eight precedence levels - ensure all are present
- # to be filled later down with the appropriate default values
- default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {},
- '4' : {}, '5' : {}, '6' : {}, '7' : {} }}
- qos['policy']['random_detect'][rd_name] = dict_merge(
- default_precedence, qos['policy']['random_detect'][rd_name])
-
- for p_name, p_config in qos['policy'][policy].items():
- default_values = defaults(base + ['policy', policy_hyphen])
-
- if policy in ['priority_queue']:
- if 'default' not in p_config:
- raise ConfigError(f'QoS policy {p_name} misses "default" class!')
-
- # 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 'class' in default_values:
- del default_values['class']
- if 'precedence' in default_values:
- del default_values['precedence']
-
- qos['policy'][policy][p_name] = dict_merge(
- default_values, qos['policy'][policy][p_name])
-
- # class is another tag node which requires individual handling
- if 'class' in p_config:
- default_values = defaults(base + ['policy', policy_hyphen, 'class'])
- for p_class in p_config['class']:
- qos['policy'][policy][p_name]['class'][p_class] = dict_merge(
- default_values, qos['policy'][policy][p_name]['class'][p_class])
-
- if 'precedence' in p_config:
- default_values = defaults(base + ['policy', policy_hyphen, 'precedence'])
- # precedence values are a bit more complex as they are calculated
- # under specific circumstances - thus we need to iterate two times.
- # first blend in the defaults from XML / CLI
- for precedence in p_config['precedence']:
- qos['policy'][policy][p_name]['precedence'][precedence] = dict_merge(
- default_values, qos['policy'][policy][p_name]['precedence'][precedence])
- # second calculate defaults based on actual dictionary
- for precedence in p_config['precedence']:
- max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold'])
- if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]:
- qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str(
- int((9 + int(precedence)) * max_thr) // 18);
-
- if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]:
- qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \
- str(int(4 * max_thr))
+ 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.split(".")[0])
+
+ for policy in qos.get('policy', []):
+ if policy in ['random_detect']:
+ for rd_name in list(qos['policy'][policy]):
+ # There are eight precedence levels - ensure all are present
+ # to be filled later down with the appropriate default values
+ default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {},
+ '4' : {}, '5' : {}, '6' : {}, '7' : {} }}
+ qos['policy']['random_detect'][rd_name] = dict_merge(
+ default_precedence, qos['policy']['random_detect'][rd_name])
+
+ qos = conf.merge_defaults(qos, recursive=True)
+
+ for policy in qos.get('policy', []):
+ for p_name, p_config in qos['policy'][policy].items():
+ if 'precedence' in p_config:
+ # precedence settings are a bit more complex as they are
+ # calculated under specific circumstances:
+ for precedence in p_config['precedence']:
+ max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold'])
+ if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]:
+ qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str(
+ int((9 + int(precedence)) * max_thr) // 18);
+
+ if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]:
+ qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \
+ str(int(4 * max_thr))
return qos
@@ -202,7 +169,9 @@ def verify(qos):
queue_lim = int(precedence_config['queue_limit'])
if queue_lim < max_tr:
raise ConfigError(f'Policy "{policy}" uses queue-limit "{queue_lim}" < max-threshold "{max_tr}"!')
-
+ if policy_type in ['priority_queue']:
+ if 'default' not in policy_config:
+ raise ConfigError(f'Policy {policy} misses "default" class!')
if 'default' in policy_config:
if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin']:
raise ConfigError('Bandwidth not defined for default traffic!')
@@ -239,6 +208,8 @@ def apply(qos):
call(f'tc qdisc del dev {interface} parent ffff:')
call(f'tc qdisc del dev {interface} root')
+ call_dependents()
+
if not qos or 'interface' not in qos:
return None
@@ -259,8 +230,6 @@ 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/salt-minion.py b/src/conf_mode/salt-minion.py
index 00b889a11..a8fce8e01 100755
--- a/src/conf_mode/salt-minion.py
+++ b/src/conf_mode/salt-minion.py
@@ -22,12 +22,10 @@ from urllib3 import PoolManager
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configverify import verify_interface_exists
from vyos.template import render
-from vyos.util import call
-from vyos.util import chown
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.permission import chown
from vyos import ConfigError
from vyos import airbag
@@ -55,8 +53,7 @@ def get_config(config=None):
salt['id'] = gethostname()
# 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)
- salt = dict_merge(default_values, salt)
+ salt = conf.merge_defaults(salt, recursive=True)
if not conf.exists(base):
return None
diff --git a/src/conf_mode/service_config_sync.py b/src/conf_mode/service_config_sync.py
new file mode 100755
index 000000000..4b8a7f6ee
--- /dev/null
+++ b/src/conf_mode/service_config_sync.py
@@ -0,0 +1,105 @@
+#!/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
+import json
+from pathlib import Path
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos import airbag
+
+airbag.enable()
+
+
+service_conf = Path(f'/run/config_sync_conf.conf')
+post_commit_dir = '/run/scripts/commit/post-hooks.d'
+post_commit_file_src = '/usr/libexec/vyos/vyos_config_sync.py'
+post_commit_file = f'{post_commit_dir}/vyos_config_sync'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['service', 'config-sync']
+ if not conf.exists(base):
+ return None
+ config = conf.get_config_dict(base, get_first_key=True,
+ with_recursive_defaults=True)
+
+ return config
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if not config:
+ return None
+
+ if 'mode' not in config:
+ raise ConfigError(f'config-sync mode is mandatory!')
+
+ for option in ['secondary', 'section']:
+ if option not in config:
+ raise ConfigError(f"config-sync '{option}' is not configured!")
+
+ if 'address' not in config['secondary']:
+ raise ConfigError(f'secondary address is mandatory!')
+ if 'key' not in config['secondary']:
+ raise ConfigError(f'secondary key is mandatory!')
+
+
+def generate(config):
+ if not config:
+
+ if os.path.exists(post_commit_file):
+ os.unlink(post_commit_file)
+
+ if service_conf.exists():
+ service_conf.unlink()
+
+ return None
+
+ # Write configuration file
+ conf_json = json.dumps(config, indent=4)
+ service_conf.write_text(conf_json)
+
+ # Create post commit dir
+ if not os.path.isdir(post_commit_dir):
+ os.makedirs(post_commit_dir)
+
+ # Symlink from helpers to post-commit
+ if not os.path.exists(post_commit_file):
+ os.symlink(post_commit_file_src, post_commit_file)
+
+ return None
+
+
+def apply(config):
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
index 60eff6543..b112add3f 100755
--- a/src/conf_mode/service_console-server.py
+++ b/src/conf_mode/service_console-server.py
@@ -20,10 +20,8 @@ from sys import exit
from psutil import process_iter
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.template import render
-from vyos.util import call
-from vyos.xml import defaults
+from vyos.utils.process import call
from vyos import ConfigError
config_file = '/run/conserver/conserver.cf'
@@ -49,11 +47,7 @@ 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.
- default_values = defaults(base + ['device'])
- if 'device' in proxy:
- for device in proxy['device']:
- tmp = dict_merge(default_values, proxy['device'][device])
- proxy['device'][device] = tmp
+ proxy = conf.merge_defaults(proxy, recursive=True)
return proxy
diff --git a/src/conf_mode/service_event_handler.py b/src/conf_mode/service_event_handler.py
index 5440d1056..5028ef52f 100755
--- a/src/conf_mode/service_event_handler.py
+++ b/src/conf_mode/service_event_handler.py
@@ -18,7 +18,8 @@ import json
from pathlib import Path
from vyos.config import Config
-from vyos.util import call, dict_search
+from vyos.utils.dict import dict_search
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py
index c58f8db9a..276a71fcb 100755
--- a/src/conf_mode/service_ids_fastnetmon.py
+++ b/src/conf_mode/service_ids_fastnetmon.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
@@ -19,10 +19,8 @@ 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.xml import defaults
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -30,6 +28,7 @@ airbag.enable()
config_file = r'/run/fastnetmon/fastnetmon.conf'
networks_list = r'/run/fastnetmon/networks_list'
excluded_networks_list = r'/run/fastnetmon/excluded_networks_list'
+attack_dir = '/var/log/fastnetmon_attacks'
def get_config(config=None):
if config:
@@ -40,11 +39,9 @@ def get_config(config=None):
if not conf.exists(base):
return None
- fastnetmon = 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)
- fastnetmon = dict_merge(default_values, fastnetmon)
+ fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
return fastnetmon
@@ -55,8 +52,11 @@ def verify(fastnetmon):
if 'mode' not in fastnetmon:
raise ConfigError('Specify operating mode!')
- if 'listen_interface' not in fastnetmon:
- raise ConfigError('Specify interface(s) for traffic capture')
+ if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon:
+ raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring")
+
+ if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}):
+ raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'")
if 'alert_script' in fastnetmon:
if os.path.isfile(fastnetmon['alert_script']):
@@ -74,6 +74,10 @@ def generate(fastnetmon):
return None
+ # Create dir for log attack details
+ if not os.path.exists(attack_dir):
+ os.mkdir(attack_dir)
+
render(config_file, 'ids/fastnetmon.j2', fastnetmon)
render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon)
render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon)
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 95c72df47..b70e32373 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -24,8 +24,8 @@ from vyos.configdict import get_accel_dict
from vyos.configverify import verify_accel_ppp_base_service
from vyos.configverify import verify_interface_exists
from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/service_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py
index 2383a53fb..a2c90b537 100755
--- a/src/conf_mode/service_mdns-repeater.py
+++ b/src/conf_mode/service_mdns-repeater.py
@@ -23,7 +23,7 @@ from netifaces import ifaddresses, interfaces, AF_INET
from vyos.config import Config
from vyos.ifconfig.vrrp import VRRP
from vyos.template import render
-from vyos.util import call
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py
index 47510ce80..40eb13e23 100755
--- a/src/conf_mode/service_monitoring_telegraf.py
+++ b/src/conf_mode/service_monitoring_telegraf.py
@@ -22,15 +22,13 @@ from sys import exit
from shutil import rmtree
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
from vyos.ifconfig import Section
from vyos.template import render
-from vyos.util import call
-from vyos.util import chown
-from vyos.util import cmd
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.permission import chown
+from vyos.utils.process import cmd
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -83,8 +81,7 @@ 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.
- default_values = defaults(base)
- monitoring = dict_merge(default_values, monitoring)
+ monitoring = conf.merge_defaults(monitoring, recursive=True)
monitoring['custom_scripts_dir'] = custom_scripts_dir
monitoring['hostname'] = get_hostname()
diff --git a/src/conf_mode/service_monitoring_zabbix-agent.py b/src/conf_mode/service_monitoring_zabbix-agent.py
new file mode 100755
index 000000000..98d8a32ca
--- /dev/null
+++ b/src/conf_mode/service_monitoring_zabbix-agent.py
@@ -0,0 +1,98 @@
+#!/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 vyos.config import Config
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+
+service_name = 'zabbix-agent2'
+service_conf = f'/run/zabbix/{service_name}.conf'
+systemd_override = r'/run/systemd/system/zabbix-agent2.service.d/10-override.conf'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['service', 'monitoring', 'zabbix-agent']
+
+ if not conf.exists(base):
+ return None
+
+ config = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True)
+
+ # Cut the / from the end, /tmp/ => /tmp
+ if 'directory' in config and config['directory'].endswith('/'):
+ config['directory'] = config['directory'][:-1]
+
+ return config
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if config is None:
+ return
+
+ if 'server' not in config:
+ raise ConfigError('Server is required!')
+
+
+def generate(config):
+ # bail out early - looks like removal from running config
+ if config is None:
+ # Remove old config and return
+ config_files = [service_conf, systemd_override]
+ for file in config_files:
+ if os.path.isfile(file):
+ os.unlink(file)
+
+ return None
+
+ # Write configuration file
+ render(service_conf, 'zabbix-agent/zabbix-agent.conf.j2', config)
+ render(systemd_override, 'zabbix-agent/10-override.conf.j2', config)
+
+ return None
+
+
+def apply(config):
+ call('systemctl daemon-reload')
+ if config:
+ call(f'systemctl restart {service_name}.service')
+ else:
+ call(f'systemctl stop {service_name}.service')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index adeefaa37..aace267a7 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -24,8 +24,8 @@ from vyos.configdict import is_node_changed
from vyos.configverify import verify_accel_ppp_base_service
from vyos.configverify import verify_interface_exists
from vyos.template import render
-from vyos.util import call
-from vyos.util import dict_search
+from vyos.utils.process import call
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py
index 1dd973d67..dbb47de4e 100755
--- a/src/conf_mode/service_router-advert.py
+++ b/src/conf_mode/service_router-advert.py
@@ -19,10 +19,8 @@ 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.util import call
-from vyos.xml import defaults
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -35,40 +33,9 @@ def get_config(config=None):
else:
conf = Config()
base = ['service', 'router-advert']
- rtradv = 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_interface_values = defaults(base + ['interface'])
- # we deal with prefix, route defaults later on
- if 'prefix' in default_interface_values:
- del default_interface_values['prefix']
- if 'route' in default_interface_values:
- del default_interface_values['route']
-
- default_prefix_values = defaults(base + ['interface', 'prefix'])
- default_route_values = defaults(base + ['interface', 'route'])
-
- if 'interface' in rtradv:
- for interface in rtradv['interface']:
- rtradv['interface'][interface] = dict_merge(
- default_interface_values, rtradv['interface'][interface])
-
- if 'prefix' in rtradv['interface'][interface]:
- for prefix in rtradv['interface'][interface]['prefix']:
- rtradv['interface'][interface]['prefix'][prefix] = dict_merge(
- default_prefix_values, rtradv['interface'][interface]['prefix'][prefix])
-
- if 'route' in rtradv['interface'][interface]:
- for route in rtradv['interface'][interface]['route']:
- rtradv['interface'][interface]['route'][route] = dict_merge(
- default_route_values, rtradv['interface'][interface]['route'][route])
-
- if 'name_server' in rtradv['interface'][interface]:
- # always use a list when dealing with nameservers - eases the template generation
- if isinstance(rtradv['interface'][interface]['name_server'], str):
- rtradv['interface'][interface]['name_server'] = [
- rtradv['interface'][interface]['name_server']]
+ rtradv = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
return rtradv
diff --git a/src/conf_mode/service_sla.py b/src/conf_mode/service_sla.py
index b1e22f37b..ba5e645f0 100755
--- a/src/conf_mode/service_sla.py
+++ b/src/conf_mode/service_sla.py
@@ -19,10 +19,8 @@ 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.xml import defaults
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -44,11 +42,9 @@ def get_config(config=None):
if not conf.exists(base):
return None
- sla = 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)
- sla = dict_merge(default_values, sla)
+ sla = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
# Ignore default XML values if config doesn't exists
# Delete key from dict
diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py
index c798fd515..cf26bf9ce 100755
--- a/src/conf_mode/service_upnp.py
+++ b/src/conf_mode/service_upnp.py
@@ -23,12 +23,10 @@ from ipaddress import IPv4Network
from ipaddress import IPv6Network
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.util import call
+from vyos.utils.process import call
from vyos.template import render
from vyos.template import is_ipv4
from vyos.template import is_ipv6
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -47,10 +45,7 @@ def get_config(config=None):
if not upnpd:
return None
- if 'rule' in upnpd:
- default_member_values = defaults(base + ['rule'])
- for rule,rule_config in upnpd['rule'].items():
- upnpd['rule'][rule] = dict_merge(default_member_values, upnpd['rule'][rule])
+ upnpd = conf.merge_defaults(upnpd, recursive=True)
uuidgen = uuid.uuid1()
upnpd.update({'uuid': uuidgen})
diff --git a/src/conf_mode/service_webproxy.py b/src/conf_mode/service_webproxy.py
index 658e496a6..12ae4135e 100755
--- a/src/conf_mode/service_webproxy.py
+++ b/src/conf_mode/service_webproxy.py
@@ -20,14 +20,13 @@ from shutil import rmtree
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
+from vyos.config import config_dict_merge
from vyos.template import render
-from vyos.util import call
-from vyos.util import chmod_755
-from vyos.util import dict_search
-from vyos.util import write_file
-from vyos.validate import is_addr_assigned
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.permission import chmod_755
+from vyos.utils.dict import dict_search
+from vyos.utils.file import write_file
+from vyos.utils.network import is_addr_assigned
from vyos.base import Warning
from vyos import ConfigError
from vyos import airbag
@@ -125,7 +124,8 @@ def get_config(config=None):
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)
+ default_values = conf.get_config_defaults(**proxy.kwargs,
+ recursive=True)
# if no authentication method is supplied, no need to add defaults
if not dict_search('authentication.method', proxy):
@@ -138,16 +138,7 @@ def get_config(config=None):
proxy['squidguard_conf'] = squidguard_config_file
proxy['squidguard_db_dir'] = squidguard_db_dir
- # XXX: T2665: blend in proper cache-peer default values later
- default_values.pop('cache_peer')
- proxy = dict_merge(default_values, proxy)
-
- # XXX: T2665: blend in proper cache-peer default values
- if 'cache_peer' in proxy:
- default_values = defaults(base + ['cache-peer'])
- for peer in proxy['cache_peer']:
- proxy['cache_peer'][peer] = dict_merge(default_values,
- proxy['cache_peer'][peer])
+ proxy = config_dict_merge(default_values, proxy)
return proxy
diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py
index 9b7c04eb0..7882f8510 100755
--- a/src/conf_mode/snmp.py
+++ b/src/conf_mode/snmp.py
@@ -26,12 +26,11 @@ from vyos.snmpv3_hashgen import plaintext_to_md5
from vyos.snmpv3_hashgen import plaintext_to_sha1
from vyos.snmpv3_hashgen import random
from vyos.template import render
-from vyos.util import call
-from vyos.util import chmod_755
-from vyos.util import dict_search
-from vyos.validate import is_addr_assigned
+from vyos.utils.process import call
+from vyos.utils.permission import chmod_755
+from vyos.utils.dict import dict_search
+from vyos.utils.network import is_addr_assigned
from vyos.version import get_version_data
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -70,26 +69,9 @@ 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.
- default_values = defaults(base)
-
- # We can not merge defaults for tagNodes - those need to be blended in
- # per tagNode instance
- if 'listen_address' in default_values:
- del default_values['listen_address']
- if 'community' in default_values:
- del default_values['community']
- if 'trap_target' in default_values:
- del default_values['trap_target']
- if 'v3' in default_values:
- del default_values['v3']
- snmp = dict_merge(default_values, snmp)
+ snmp = conf.merge_defaults(snmp, recursive=True)
if 'listen_address' in snmp:
- default_values = defaults(base + ['listen-address'])
- for address in snmp['listen_address']:
- snmp['listen_address'][address] = dict_merge(
- default_values, snmp['listen_address'][address])
-
# 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://vyos.dev/T850
@@ -101,41 +83,6 @@ def get_config(config=None):
tmp = {'::1': {'port': '161'}}
snmp['listen_address'] = dict_merge(tmp, snmp['listen_address'])
- 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])
-
- if 'trap_target' in snmp:
- default_values = defaults(base + ['trap-target'])
- for trap in snmp['trap_target']:
- snmp['trap_target'][trap] = dict_merge(
- default_values, snmp['trap_target'][trap])
-
- if 'v3' in snmp:
- default_values = defaults(base + ['v3'])
- # tagNodes need to be merged in individually later on
- for tmp in ['user', 'group', 'trap_target']:
- del default_values[tmp]
- snmp['v3'] = dict_merge(default_values, snmp['v3'])
-
- for user_group in ['user', 'group']:
- if user_group in snmp['v3']:
- default_values = defaults(base + ['v3', user_group])
- for tmp in snmp['v3'][user_group]:
- snmp['v3'][user_group][tmp] = dict_merge(
- default_values, snmp['v3'][user_group][tmp])
-
- if 'trap_target' in snmp['v3']:
- default_values = defaults(base + ['v3', 'trap-target'])
- for trap in snmp['v3']['trap_target']:
- snmp['v3']['trap_target'][trap] = dict_merge(
- default_values, snmp['v3']['trap_target'][trap])
-
return snmp
def verify(snmp):
@@ -161,8 +108,12 @@ def verify(snmp):
for address in snmp['listen_address']:
# We only wan't to configure addresses that exist on the system.
# Hint the user if they don't exist
- if not is_addr_assigned(address):
- Warning(f'SNMP listen address "{address}" not configured!')
+ if 'vrf' in snmp:
+ vrf_name = snmp['vrf']
+ if not is_addr_assigned(address, vrf_name) and address not in ['::1','127.0.0.1']:
+ raise ConfigError(f'SNMP listen address "{address}" not configured in vrf "{vrf_name}"!')
+ elif not is_addr_assigned(address):
+ raise ConfigError(f'SNMP listen address "{address}" not configured in default vrf!')
if 'trap_target' in snmp:
for trap, trap_config in snmp['trap_target'].items():
diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py
index 8de0617af..ee5e1eca2 100755
--- a/src/conf_mode/ssh.py
+++ b/src/conf_mode/ssh.py
@@ -21,12 +21,10 @@ from syslog import syslog
from syslog import LOG_INFO
from vyos.config import Config
-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.utils.process import call
from vyos.template import render
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -57,8 +55,8 @@ 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.
- default_values = defaults(base)
- ssh = dict_merge(default_values, ssh)
+ ssh = conf.merge_defaults(ssh, recursive=True)
+
# pass config file path - used in override template
ssh['config_file'] = config_file
diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py
index 95865c690..5e4e5ec28 100755
--- a/src/conf_mode/system-ip.py
+++ b/src/conf_mode/system-ip.py
@@ -20,11 +20,10 @@ 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.utils.process import call
+from vyos.utils.dict import dict_search
+from vyos.utils.file import write_file
+from vyos.utils.system import sysctl_write
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -37,11 +36,9 @@ def get_config(config=None):
conf = Config()
base = ['system', 'ip']
- opt = 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)
- opt = dict_merge(default_values, opt)
+ opt = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
# When working with FRR we need to know the corresponding address-family
opt['afi'] = 'ip'
@@ -64,8 +61,7 @@ def verify(opt):
return
def generate(opt):
- if 'protocol' in opt:
- opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
+ opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
return
def apply(opt):
@@ -98,17 +94,37 @@ 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)
+ # configure TCP options (defaults as of Linux 6.4)
+ tmp = dict_search('tcp.mss.probing', opt)
+ if tmp is None:
+ value = 0
+ elif tmp == 'on-icmp-black-hole':
+ value = 1
+ elif tmp == 'force':
+ value = 2
+ else:
+ # Shouldn't happen
+ raise ValueError("TCP MSS probing is neither 'on-icmp-black-hole' nor 'force'!")
+ sysctl_write('net.ipv4.tcp_mtu_probing', value)
+
+ tmp = dict_search('tcp.mss.base', opt)
+ value = '1024' if (tmp is None) else tmp
+ sysctl_write('net.ipv4.tcp_base_mss', value)
+
+ tmp = dict_search('tcp.mss.floor', opt)
+ value = '48' if (tmp is None) else tmp
+ sysctl_write('net.ipv4.tcp_mtu_probe_floor', value)
+
+ 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:
diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py
index b6d3a79c3..e40ed38e2 100755
--- a/src/conf_mode/system-ipv6.py
+++ b/src/conf_mode/system-ipv6.py
@@ -21,10 +21,9 @@ 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.utils.dict import dict_search
+from vyos.utils.system import sysctl_write
+from vyos.utils.file import write_file
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -37,12 +36,9 @@ def get_config(config=None):
conf = Config()
base = ['system', 'ipv6']
- opt = 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)
- opt = dict_merge(default_values, opt)
+ opt = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
# When working with FRR we need to know the corresponding address-family
opt['afi'] = 'ipv6'
@@ -65,8 +61,7 @@ def verify(opt):
return
def generate(opt):
- if 'protocol' in opt:
- opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
+ opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
return
def apply(opt):
@@ -98,17 +93,16 @@ 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()
+ 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)
+ # 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:
diff --git a/src/conf_mode/system-login-banner.py b/src/conf_mode/system-login-banner.py
index a521c9834..65fa04417 100755
--- a/src/conf_mode/system-login-banner.py
+++ b/src/conf_mode/system-login-banner.py
@@ -18,7 +18,7 @@ from sys import exit
from copy import deepcopy
from vyos.config import Config
-from vyos.util import write_file
+from vyos.utils.file import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index fbb013cf3..02c97afaa 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -24,17 +24,16 @@ 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, rc_cmd
-from vyos.util import run
-from vyos.util import DEVNULL
-from vyos.util import dict_search
-from vyos.xml import defaults
+from vyos.utils.dict import dict_search
+from vyos.utils.process import cmd
+from vyos.utils.process import call
+from vyos.utils.process import rc_cmd
+from vyos.utils.process import run
+from vyos.utils.process import DEVNULL
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -42,20 +41,38 @@ 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"
-
+tacacs_pam_config_file = "/etc/tacplus_servers"
+tacacs_nss_config_file = "/etc/tacplus_nss.conf"
+nss_config_file = "/etc/nsswitch.conf"
+
+# Minimum UID used when adding system users
+MIN_USER_UID: int = 1000
+# Maximim UID used when adding system users
+MAX_USER_UID: int = 59999
# 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
+MAX_RADIUS_COUNT: int = 8
+# Maximum number of supported TACACS servers
+MAX_TACACS_COUNT: int = 8
+
+# List of local user accounts that must be preserved
+SYSTEM_USER_SKIP_LIST: list = ['radius_user', 'radius_priv_user', 'tacacs0', 'tacacs1',
+ 'tacacs2', 'tacacs3', 'tacacs4', 'tacacs5', 'tacacs6',
+ 'tacacs7', 'tacacs8', 'tacacs9', 'tacacs10',' tacacs11',
+ 'tacacs12', 'tacacs13', 'tacacs14', 'tacacs15']
def get_local_users():
"""Return list of dynamically allocated users (see Debian Policy Manual)"""
local_users = []
for s_user in getpwall():
- uid = getpwnam(s_user.pw_name).pw_uid
- if uid in range(1000, 29999):
- if s_user.pw_name not in ['radius_user', 'radius_priv_user']:
- local_users.append(s_user.pw_name)
+ if getpwnam(s_user.pw_name).pw_uid < MIN_USER_UID:
+ continue
+ if getpwnam(s_user.pw_name).pw_uid > MAX_USER_UID:
+ continue
+ if s_user.pw_name in SYSTEM_USER_SKIP_LIST:
+ continue
+ local_users.append(s_user.pw_name)
return local_users
@@ -74,7 +91,9 @@ def get_config(config=None):
conf = Config()
base = ['system', 'login']
login = conf.get_config_dict(base, key_mangling=('-', '_'),
- no_tag_node_value_mangle=True, get_first_key=True)
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
# users no longer existing in the running configuration need to be deleted
local_users = get_local_users()
@@ -82,18 +101,9 @@ def get_config(config=None):
if 'user' in login:
cli_users = list(login['user'])
- # XXX: T2665: we can not safely rely on the defaults() when there are
- # tagNodes in place, it is better to blend in the defaults manually.
- default_values = defaults(base + ['user'])
- for user in login['user']:
- login['user'][user] = dict_merge(default_values, login['user'][user])
-
- # XXX: T2665: we can not safely rely on the defaults() when there are
- # tagNodes in place, it is better to blend in the defaults manually.
- default_values = defaults(base + ['radius', 'server'])
- for server in dict_search('radius.server', login) or []:
- login['radius']['server'][server] = dict_merge(default_values,
- login['radius']['server'][server])
+ # prune TACACS global defaults if not set by user
+ if login.from_defaults(['tacacs']):
+ del login['tacacs']
# create a list of all users, cli and users
all_users = list(set(local_users + cli_users))
@@ -107,9 +117,13 @@ def get_config(config=None):
def verify(login):
if 'rm_users' in login:
- cur_user = os.environ['SUDO_USER']
- if cur_user in login['rm_users']:
- raise ConfigError(f'Attempting to delete current user: {cur_user}')
+ # This check is required as the script is also executed from vyos-router
+ # init script and there is no SUDO_USER environment variable available
+ # during system boot.
+ if 'SUDO_USER' in os.environ:
+ cur_user = os.environ['SUDO_USER']
+ if cur_user in login['rm_users']:
+ raise ConfigError(f'Attempting to delete current user: {cur_user}')
if 'user' in login:
system_users = getpwall()
@@ -117,7 +131,7 @@ def verify(login):
# Linux system users range up until UID 1000, we can not create a
# VyOS CLI user which already exists as system user
for s_user in system_users:
- if s_user.pw_name == user and s_user.pw_uid < 1000:
+ if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID:
raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
@@ -126,6 +140,9 @@ def verify(login):
if 'key' not in pubkey_options:
raise ConfigError(f'Missing key for public-key "{pubkey}"!')
+ if {'radius', 'tacacs'} <= set(login):
+ raise ConfigError('Using both RADIUS and TACACS at the same time is not supported!')
+
# At lease one RADIUS server must not be disabled
if 'radius' in login:
if 'server' not in login['radius']:
@@ -145,7 +162,7 @@ def verify(login):
raise ConfigError('All RADIUS servers are disabled')
if radius_servers_count > MAX_RADIUS_COUNT:
- raise ConfigError('Number of RADIUS servers more than 25 ')
+ raise ConfigError(f'Number of RADIUS servers exceeded maximum of {MAX_RADIUS_COUNT}!')
if sum_timeout > MAX_RADIUS_TIMEOUT:
raise ConfigError('Sum of RADIUS servers timeouts '
@@ -165,6 +182,24 @@ def verify(login):
if ipv6_count > 1:
raise ConfigError('Only one IPv6 source-address can be set!')
+ if 'tacacs' in login:
+ tacacs_servers_count: int = 0
+ fail = True
+ for server, server_config in dict_search('tacacs.server', login).items():
+ if 'key' not in server_config:
+ raise ConfigError(f'TACACS server "{server}" requires key!')
+ if 'disable' not in server_config:
+ tacacs_servers_count += 1
+ fail = False
+
+ if fail:
+ raise ConfigError('All RADIUS servers are disabled')
+
+ if tacacs_servers_count > MAX_TACACS_COUNT:
+ raise ConfigError(f'Number of TACACS servers exceeded maximum of {MAX_TACACS_COUNT}!')
+
+ verify_vrf(login['tacacs'])
+
if 'max_login_session' in login and 'timeout' not in login:
raise ConfigError('"login timeout" must be configured!')
@@ -186,8 +221,8 @@ def generate(login):
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"
- add_user_encrypt = f"system login user '{user}' authentication encrypted-password '{encrypted_password}'"
+ del_user_plain = f"system login user {user} authentication plaintext-password"
+ add_user_encrypt = f"system login user {user} authentication encrypted-password '{encrypted_password}'"
lvl = env['VYATTA_EDIT_LEVEL']
# We're in config edit level, for example "edit system login"
@@ -206,10 +241,10 @@ def generate(login):
add_user_encrypt = add_user_encrypt[len(lvl):]
add_user_encrypt = " ".join(add_user_encrypt)
- call(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env)
+ ret, out = rc_cmd(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env)
+ if ret: raise ConfigError(out)
ret, out = rc_cmd(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env)
- if ret:
- raise ConfigError(out)
+ if ret: raise ConfigError(out)
else:
try:
if get_shadow_password(user) == dict_search('authentication.encrypted_password', user_config):
@@ -223,6 +258,7 @@ def generate(login):
except:
pass
+ ### RADIUS based user authentication
if 'radius' in login:
render(radius_config_file, 'login/pam_radius_auth.conf.j2', login,
permission=0o600, user='root', group='root')
@@ -230,6 +266,24 @@ def generate(login):
if os.path.isfile(radius_config_file):
os.unlink(radius_config_file)
+ ### TACACS+ based user authentication
+ if 'tacacs' in login:
+ render(tacacs_pam_config_file, 'login/tacplus_servers.j2', login,
+ permission=0o644, user='root', group='root')
+ render(tacacs_nss_config_file, 'login/tacplus_nss.conf.j2', login,
+ permission=0o644, user='root', group='root')
+ else:
+ if os.path.isfile(tacacs_pam_config_file):
+ os.unlink(tacacs_pam_config_file)
+ if os.path.isfile(tacacs_nss_config_file):
+ os.unlink(tacacs_nss_config_file)
+
+
+
+ # NSS must always be present on the system
+ render(nss_config_file, 'login/nsswitch.conf.j2', login,
+ permission=0o644, user='root', group='root')
+
# /etc/security/limits.d/10-vyos.conf
if 'max_login_session' in login:
render(limits_file, 'login/limits.j2', login,
@@ -253,7 +307,7 @@ def apply(login):
for user, user_config in login['user'].items():
# make new user using vyatta shell and make home directory (-m),
# default group of 100 (users)
- command = 'useradd --create-home --no-user-group'
+ command = 'useradd --create-home --no-user-group '
# check if user already exists:
if user in get_local_users():
# update existing account
@@ -317,44 +371,23 @@ def apply(login):
# command until user is removed - userdel might return 8 as
# SSH sessions are not all yet properly cleaned away, thus we
# simply re-run the command until the account wen't away
- while run(f'userdel --remove {user}', stderr=DEVNULL):
+ while run(f'userdel {user}', stderr=DEVNULL):
sleep(0.250)
except Exception as e:
raise ConfigError(f'Deleting user "{user}" raised exception: {e}')
- #
- # RADIUS configuration
- #
- env = os.environ.copy()
- env['DEBIAN_FRONTEND'] = 'noninteractive'
- try:
- if 'radius' in login:
- # Enable RADIUS in PAM
- cmd('pam-auth-update --package --enable radius', env=env)
- # Make NSS system aware of RADIUS
- # This fancy snipped was copied from old Vyatta code
- command = "sed -i -e \'/\smapname/b\' \
- -e \'/^passwd:/s/\s\s*/&mapuid /\' \
- -e \'/^passwd:.*#/s/#.*/mapname &/\' \
- -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \
- -e \'/^group:.*#/s/#.*/ mapname &/\' \
- -e \'/^group:[^#]*$/s/: */&mapname /\' \
- /etc/nsswitch.conf"
- else:
- # Disable RADIUS in PAM
- cmd('pam-auth-update --package --remove radius', env=env)
- # Drop RADIUS from NSS NSS system
- # This fancy snipped was copied from old Vyatta code
- command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \
- -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \
- -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \
- -e \'s/[ \t]*$//\' \
- /etc/nsswitch.conf"
-
- cmd(command)
- except Exception as e:
- raise ConfigError(f'RADIUS configuration failed: {e}')
+ # Enable RADIUS in PAM configuration
+ pam_cmd = '--remove'
+ if 'radius' in login:
+ pam_cmd = '--enable'
+ cmd(f'pam-auth-update --package {pam_cmd} radius')
+
+ # Enable/Disable TACACS in PAM configuration
+ pam_cmd = '--remove'
+ if 'tacacs' in login:
+ pam_cmd = '--enable'
+ cmd(f'pam-auth-update --package {pam_cmd} tacplus')
return None
diff --git a/src/conf_mode/system-logs.py b/src/conf_mode/system-logs.py
index c71938a79..8ad4875d4 100755
--- a/src/conf_mode/system-logs.py
+++ b/src/conf_mode/system-logs.py
@@ -19,11 +19,9 @@ from sys import exit
from vyos import ConfigError
from vyos import airbag
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.logger import syslog
from vyos.template import render
-from vyos.util import dict_search
-from vyos.xml import defaults
+from vyos.utils.dict import dict_search
airbag.enable()
# path to logrotate configs
@@ -38,11 +36,9 @@ def get_config(config=None):
conf = Config()
base = ['system', 'logs']
- default_values = defaults(base)
- logs_config = conf.get_config_dict(base,
- key_mangling=('-', '_'),
- get_first_key=True)
- logs_config = dict_merge(default_values, logs_config)
+ logs_config = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
return logs_config
diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py
index e6c7a0ed2..d92121b3d 100755
--- a/src/conf_mode/system-option.py
+++ b/src/conf_mode/system-option.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
@@ -21,14 +21,12 @@ from sys import exit
from time import sleep
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configverify import verify_source_interface
from vyos.template import render
-from vyos.util import cmd
-from vyos.util import is_systemd_service_running
-from vyos.validate import is_addr_assigned
-from vyos.validate import is_intf_addr_assigned
-from vyos.xml import defaults
+from vyos.utils.process import cmd
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.network import is_addr_assigned
+from vyos.utils.network import is_intf_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -36,6 +34,11 @@ airbag.enable()
curlrc_config = r'/etc/curlrc'
ssh_config = r'/etc/ssh/ssh_config.d/91-vyos-ssh-client-options.conf'
systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target'
+time_format_to_locale = {
+ '12-hour': 'en_US.UTF-8',
+ '24-hour': 'en_GB.UTF-8'
+}
+
def get_config(config=None):
if config:
@@ -43,12 +46,9 @@ def get_config(config=None):
else:
conf = Config()
base = ['system', 'option']
- options = 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)
- options = dict_merge(default_values, options)
+ options = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
return options
@@ -143,6 +143,11 @@ def apply(options):
else:
cmd('systemctl disable root-partition-auto-resize.service')
+ # Time format 12|24-hour
+ if 'time_format' in options:
+ time_format = time_format_to_locale.get(options['time_format'])
+ cmd(f'localectl set-locale LC_TIME={time_format}')
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
index cf34bad2e..07fbb0734 100755
--- a/src/conf_mode/system-syslog.py
+++ b/src/conf_mode/system-syslog.py
@@ -19,12 +19,10 @@ import os
from sys import exit
from vyos.config import Config
-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.utils.process import call
from vyos.template import render
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -46,59 +44,13 @@ def get_config(config=None):
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])
+ syslog = conf.merge_defaults(syslog, recursive=True)
+ if syslog.from_defaults(['global']):
+ del syslog['global']
return syslog
diff --git a/src/conf_mode/system-timezone.py b/src/conf_mode/system-timezone.py
index 3d98ba774..cd3d4b229 100755
--- a/src/conf_mode/system-timezone.py
+++ b/src/conf_mode/system-timezone.py
@@ -20,7 +20,7 @@ import os
from copy import deepcopy
from vyos.config import Config
from vyos import ConfigError
-from vyos.util import call
+from vyos.utils.process import call
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py
index e922edc4e..ebf9a113b 100755
--- a/src/conf_mode/system_console.py
+++ b/src/conf_mode/system_console.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020 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
@@ -19,12 +19,10 @@ import re
from pathlib import Path
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.util import call
-from vyos.util import read_file
-from vyos.util import write_file
+from vyos.utils.process import call
+from vyos.utils.file import read_file
+from vyos.utils.file import write_file
from vyos.template import render
-from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -45,16 +43,12 @@ def get_config(config=None):
if 'device' not in console:
return console
- # convert CLI values to system values
- default_values = defaults(base + ['device'])
for device, device_config in console['device'].items():
if 'speed' not in device_config and device.startswith('hvc'):
# XEN console has a different default console speed
console['device'][device]['speed'] = 38400
- else:
- # Merge in XML defaults - the proper way to do it
- console['device'][device] = dict_merge(default_values,
- console['device'][device])
+
+ console = conf.merge_defaults(console, recursive=True)
return console
diff --git a/src/conf_mode/system_frr.py b/src/conf_mode/system_frr.py
index 1af0055f6..fb252238a 100755
--- a/src/conf_mode/system_frr.py
+++ b/src/conf_mode/system_frr.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
@@ -22,7 +22,9 @@ from vyos import airbag
from vyos.config import Config
from vyos.logger import syslog
from vyos.template import render_to_string
-from vyos.util import read_file, write_file, run
+from vyos.utils.file import read_file
+from vyos.utils.file import write_file
+from vyos.utils.process import run
airbag.enable()
# path to daemons config and config status files
diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py
index 3341dd738..eb88224d1 100755
--- a/src/conf_mode/system_lcd.py
+++ b/src/conf_mode/system_lcd.py
@@ -19,8 +19,8 @@ import os
from sys import exit
from vyos.config import Config
-from vyos.util import call
-from vyos.util import find_device_file
+from vyos.utils.process import call
+from vyos.utils.system import find_device_file
from vyos.template import render
from vyos import ConfigError
from vyos import airbag
diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py
index a0c3fca7f..2df1bbb7a 100755
--- a/src/conf_mode/system_sflow.py
+++ b/src/conf_mode/system_sflow.py
@@ -19,11 +19,9 @@ 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.utils.process import call
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -42,26 +40,9 @@ def get_config(config=None):
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])
+ sflow = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
return sflow
diff --git a/src/conf_mode/system_sysctl.py b/src/conf_mode/system_sysctl.py
index 2e0004ffa..f6b02023d 100755
--- a/src/conf_mode/system_sysctl.py
+++ b/src/conf_mode/system_sysctl.py
@@ -20,7 +20,7 @@ from sys import exit
from vyos.config import Config
from vyos.template import render
-from vyos.util import cmd
+from vyos.utils.process import cmd
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/system_update_check.py b/src/conf_mode/system_update_check.py
index 08ecfcb81..8d641a97d 100755
--- a/src/conf_mode/system_update_check.py
+++ b/src/conf_mode/system_update_check.py
@@ -22,7 +22,7 @@ from pathlib import Path
from sys import exit
from vyos.config import Config
-from vyos.util import call
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py
index c5daccb7f..3ad346e2e 100755
--- a/src/conf_mode/tftp_server.py
+++ b/src/conf_mode/tftp_server.py
@@ -24,14 +24,12 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configverify import verify_vrf
from vyos.template import render
from vyos.template import is_ipv4
-from vyos.util import call
-from vyos.util import chmod_755
-from vyos.validate import is_addr_assigned
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.permission import chmod_755
+from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -48,11 +46,9 @@ def get_config(config=None):
if not conf.exists(base):
return None
- tftpd = 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)
- tftpd = dict_merge(default_values, tftpd)
+ tftpd = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
return tftpd
def verify(tftpd):
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 63887b278..fa271cbdb 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -27,7 +27,7 @@ from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import leaf_node_changed
from vyos.configverify import verify_interface_exists
-from vyos.configdict import dict_merge
+from vyos.defaults import directories
from vyos.ifconfig import Interface
from vyos.pki import encode_public_key
from vyos.pki import load_private_key
@@ -39,12 +39,11 @@ from vyos.template import ip_from_cidr
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.template import render
-from vyos.validate import is_ipv6_link_local
-from vyos.util import call
-from vyos.util import dict_search
-from vyos.util import dict_search_args
-from vyos.util import run
-from vyos.xml import defaults
+from vyos.utils.network import is_ipv6_link_local
+from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_search_args
+from vyos.utils.process import call
+from vyos.utils.process import run
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -69,7 +68,6 @@ KEY_PATH = f'{swanctl_dir}/private/'
CA_PATH = f'{swanctl_dir}/x509ca/'
CRL_PATH = f'{swanctl_dir}/x509crl/'
-DHCP_BASE = '/var/lib/dhcp/dhclient'
DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting'
def get_config(config=None):
@@ -84,88 +82,23 @@ def get_config(config=None):
# retrieve common dictionary keys
ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- default_values = defaults(base)
- # XXX: T2665: we must safely remove default values for tag nodes, those are
- # added in a more fine grained way later on
- del default_values['esp_group']
- del default_values['ike_group']
- del default_values['remote_access']
- del default_values['site_to_site']
- ipsec = dict_merge(default_values, ipsec)
-
- if 'esp_group' in ipsec:
- default_values = defaults(base + ['esp-group'])
- for group in ipsec['esp_group']:
- ipsec['esp_group'][group] = dict_merge(default_values,
- ipsec['esp_group'][group])
- if 'ike_group' in ipsec:
- default_values = defaults(base + ['ike-group'])
- # proposal is a tag node which may come with individual defaults per node
- if 'proposal' in default_values:
- del default_values['proposal']
-
- for group in ipsec['ike_group']:
- ipsec['ike_group'][group] = dict_merge(default_values,
- ipsec['ike_group'][group])
-
- if 'proposal' in ipsec['ike_group'][group]:
- default_values = defaults(base + ['ike-group', 'proposal'])
- for proposal in ipsec['ike_group'][group]['proposal']:
- ipsec['ike_group'][group]['proposal'][proposal] = dict_merge(default_values,
- ipsec['ike_group'][group]['proposal'][proposal])
-
- # 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 dict_search('remote_access.connection', ipsec):
- default_values = defaults(base + ['remote-access', 'connection'])
- for rw in ipsec['remote_access']['connection']:
- ipsec['remote_access']['connection'][rw] = dict_merge(default_values,
- ipsec['remote_access']['connection'][rw])
-
- # 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 dict_search('remote_access.radius.server', ipsec):
- # Fist handle the "base" stuff like RADIUS timeout
- default_values = defaults(base + ['remote-access', 'radius'])
- if 'server' in default_values:
- del default_values['server']
- ipsec['remote_access']['radius'] = dict_merge(default_values,
- ipsec['remote_access']['radius'])
-
- # Take care about individual RADIUS servers implemented as tagNodes - this
- # requires special treatment
- default_values = defaults(base + ['remote-access', 'radius', 'server'])
- for server in ipsec['remote_access']['radius']['server']:
- ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values,
- ipsec['remote_access']['radius']['server'][server])
-
- # 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 dict_search('site_to_site.peer', ipsec):
- default_values = defaults(base + ['site-to-site', 'peer'])
- for peer in ipsec['site_to_site']['peer']:
- ipsec['site_to_site']['peer'][peer] = dict_merge(default_values,
- ipsec['site_to_site']['peer'][peer])
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
if tmp:
- ipsec['l2tp'] = tmp
- l2tp_defaults = defaults(l2tp_base)
- ipsec['l2tp'] = dict_merge(l2tp_defaults, ipsec['l2tp'])
+ ipsec['l2tp'] = conf.merge_defaults(tmp, recursive=True)
ipsec['l2tp_outside_address'] = conf.return_value(['vpn', 'l2tp', 'remote-access', 'outside-address'])
ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024'
ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1'
@@ -433,8 +366,9 @@ def verify(ipsec):
dhcp_interface = peer_conf['dhcp_interface']
verify_interface_exists(dhcp_interface)
+ dhcp_base = directories['isc_dhclient_dir']
- if not os.path.exists(f'{DHCP_BASE}_{dhcp_interface}.conf'):
+ if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
address = get_dhcp_address(dhcp_interface)
@@ -455,7 +389,7 @@ def verify(ipsec):
if dict_search('options.disable_route_autoinstall',
ipsec) == None:
- Warning('It\'s recommended to use ipsec vty with the next command\n[set vpn ipsec option disable-route-autoinstall]')
+ Warning('It\'s recommended to use ipsec vti with the next command\n[set vpn ipsec option disable-route-autoinstall]')
if 'bind' in peer_conf['vti']:
vti_interface = peer_conf['vti']['bind']
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index ffac3b023..6232ce64a 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -26,10 +26,10 @@ from ipaddress import ip_network
from vyos.config import Config
from vyos.template import is_ipv4
from vyos.template import render
-from vyos.util import call
-from vyos.util import get_half_cpus
-from vyos.util import check_port_availability
-from vyos.util import is_listen_port_bind_service
+from vyos.utils.process import call
+from vyos.utils.system import get_half_cpus
+from vyos.utils.network import check_port_availability
+from vyos.utils.network import is_listen_port_bind_service
from vyos import ConfigError
from vyos import airbag
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index 83021a3e6..a039172c4 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -19,18 +19,16 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
-from vyos.util import call
-from vyos.util import check_port_availability
-from vyos.util import is_systemd_service_running
-from vyos.util import is_listen_port_bind_service
-from vyos.util import dict_search
-from vyos.xml import defaults
+from vyos.utils.process import call
+from vyos.utils.network import check_port_availability
+from vyos.utils.process import is_systemd_service_running
+from vyos.utils.network import is_listen_port_bind_service
+from vyos.utils.dict import dict_search
from vyos import ConfigError
-from crypt import crypt, mksalt, METHOD_SHA512
+from passlib.hash import sha512_crypt
from time import sleep
from vyos import airbag
@@ -45,66 +43,7 @@ radius_servers = cfg_dir + '/radius_servers'
# Generate hash from user cleartext password
def get_hash(password):
- return crypt(password, mksalt(METHOD_SHA512))
-
-
-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
-
+ return sha512_crypt.hash(password)
def get_config(config=None):
if config:
@@ -115,16 +54,14 @@ def get_config(config=None):
if not conf.exists(base):
return None
- ocserv = 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)
- ocserv = dict_merge(default_values, ocserv)
- # workaround a "know limitation" - https://vyos.dev/T2665
- ocserv = _default_dict_cleanup(ocserv, default_values)
+ ocserv = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ with_recursive_defaults=True)
+
if ocserv:
ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
return ocserv
@@ -141,6 +78,8 @@ def verify(ocserv):
# Check accounting
if "accounting" in ocserv:
if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]:
+ if not origin["accounting"]['radius']['server']:
+ raise ConfigError('Openconnect accounting mode radius requires at least one RADIUS server')
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"]:
@@ -149,9 +88,13 @@ def verify(ocserv):
# Check authentication
if "authentication" in ocserv:
if "mode" in ocserv["authentication"]:
- if "local" in ocserv["authentication"]["mode"]:
- if "radius" in ocserv["authentication"]["mode"]:
+ if ("local" in ocserv["authentication"]["mode"] and
+ "radius" in ocserv["authentication"]["mode"]):
raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration')
+ if "radius" in ocserv["authentication"]["mode"]:
+ if not ocserv["authentication"]['radius']['server']:
+ raise ConfigError('Openconnect authentication mode radius requires at least one RADIUS server')
+ if "local" in ocserv["authentication"]["mode"]:
if not ocserv["authentication"]["local_users"]:
raise ConfigError('openconnect mode local required at least one user')
if not ocserv["authentication"]["local_users"]["username"]:
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index b9d18110a..d542f57fe 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -23,7 +23,8 @@ from sys import exit
from vyos.config import Config
from vyos.template import render
-from vyos.util import call, get_half_cpus
+from vyos.utils.system import get_half_cpus
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index 2949ab290..e98d8385b 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -25,11 +25,11 @@ from vyos.configverify import verify_accel_ppp_base_service
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
-from vyos.util import call
-from vyos.util import check_port_availability
-from vyos.util import dict_search
-from vyos.util import is_listen_port_bind_service
-from vyos.util import write_file
+from vyos.utils.process import call
+from vyos.utils.network import check_port_availability
+from vyos.utils.dict import dict_search
+from vyos.utils.network import is_listen_port_bind_service
+from vyos.utils.file import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py
new file mode 100755
index 000000000..82c2f236e
--- /dev/null
+++ b/src/conf_mode/vpp.py
@@ -0,0 +1,207 @@
+#!/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 psutil import virtual_memory
+
+from pathlib import Path
+from re import search as re_search, MULTILINE as re_M
+
+from vyos.config import Config
+from vyos.configdep import set_dependents, call_dependents
+from vyos.configdict import node_changed
+from vyos.ifconfig import Section
+from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.process import call
+from vyos.utils.process import rc_cmd
+from vyos.utils.system import sysctl_read
+from vyos.utils.system import sysctl_apply
+from vyos.template import render
+
+from vyos import ConfigError
+from vyos import airbag
+from vyos.vpp import VPPControl
+from vyos.vpp import HostControl
+
+airbag.enable()
+
+service_name = 'vpp'
+service_conf = Path(f'/run/vpp/{service_name}.conf')
+systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf'
+
+# Free memory required for VPP
+# 2 GB for hugepages + 1 GB for other services
+MIN_AVAILABLE_MEMORY: int = 3 * 1024**3
+
+
+def _get_pci_address_by_interface(iface) -> str:
+ rc, out = rc_cmd(f'ethtool -i {iface}')
+ # if ethtool command was successful
+ if rc == 0 and out:
+ regex_filter = r'^bus-info: (?P<address>\w+:\w+:\w+\.\w+)$'
+ re_obj = re_search(regex_filter, out, re_M)
+ # if bus-info with PCI address found
+ if re_obj:
+ address = re_obj.groupdict().get('address', '')
+ return address
+ # use VPP - maybe interface already attached to it
+ vpp_control = VPPControl(attempts=20, interval=500)
+ pci_addr = vpp_control.get_pci_addr(iface)
+ if pci_addr:
+ return pci_addr
+ # raise error if PCI address was not found
+ raise ConfigError(f'Cannot find PCI address for interface {iface}')
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base = ['vpp']
+ base_ethernet = ['interfaces', 'ethernet']
+
+ # find interfaces removed from VPP
+ removed_ifaces = []
+ tmp = node_changed(conf, base + ['interface'])
+ if tmp:
+ for removed_iface in tmp:
+ pci_address: str = _get_pci_address_by_interface(removed_iface)
+ removed_ifaces.append({
+ 'iface_name': removed_iface,
+ 'iface_pci_addr': pci_address
+ })
+ # add an interface to a list of interfaces that need
+ # to be reinitialized after the commit
+ set_dependents('ethernet', conf, removed_iface)
+
+ if not conf.exists(base):
+ return {'removed_ifaces': removed_ifaces}
+
+ config = conf.get_config_dict(base, key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True)
+
+ if 'interface' in config:
+ for iface, iface_config in config['interface'].items():
+ # add an interface to a list of interfaces that need
+ # to be reinitialized after the commit
+ set_dependents('ethernet', conf, iface)
+
+ # Get PCI address auto
+ if iface_config['pci'] == 'auto':
+ config['interface'][iface]['pci'] = _get_pci_address_by_interface(iface)
+
+ config['other_interfaces'] = conf.get_config_dict(base_ethernet, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ if removed_ifaces:
+ config['removed_ifaces'] = removed_ifaces
+
+ return config
+
+
+def verify(config):
+ # bail out early - looks like removal from running config
+ if not config or (len(config) == 1 and 'removed_ifaces' in config):
+ return None
+
+ if 'interface' not in config:
+ raise ConfigError('"interface" is required but not set!')
+
+ if 'cpu' in config:
+ if 'corelist_workers' in config['cpu'] and 'main_core' not in config[
+ 'cpu']:
+ raise ConfigError('"cpu main-core" is required but not set!')
+
+ memory_available: int = virtual_memory().available
+ if memory_available < MIN_AVAILABLE_MEMORY:
+ raise ConfigError(
+ 'Not enough free memory to start VPP:\n'
+ f'available: {round(memory_available / 1024**3, 1)}GB\n'
+ f'required: {round(MIN_AVAILABLE_MEMORY / 1024**3, 1)}GB')
+
+
+def generate(config):
+ if not config or (len(config) == 1 and 'removed_ifaces' in config):
+ # Remove old config and return
+ service_conf.unlink(missing_ok=True)
+ return None
+
+ render(service_conf, 'vpp/startup.conf.j2', config)
+ render(systemd_override, 'vpp/override.conf.j2', config)
+
+ # apply default sysctl values from
+ # https://github.com/FDio/vpp/blob/v23.06/src/vpp/conf/80-vpp.conf
+ sysctl_config: dict[str, str] = {
+ 'vm.nr_hugepages': '1024',
+ 'vm.max_map_count': '3096',
+ 'vm.hugetlb_shm_group': '0',
+ 'kernel.shmmax': '2147483648'
+ }
+ # we do not want to reduce `kernel.shmmax`
+ kernel_shmnax_current: str = sysctl_read('kernel.shmmax')
+ if int(kernel_shmnax_current) > int(sysctl_config['kernel.shmmax']):
+ sysctl_config['kernel.shmmax'] = kernel_shmnax_current
+
+ if not sysctl_apply(sysctl_config):
+ raise ConfigError('Cannot configure sysctl parameters for VPP')
+
+ return None
+
+
+def apply(config):
+ if not config or (len(config) == 1 and 'removed_ifaces' in config):
+ call(f'systemctl stop {service_name}.service')
+ else:
+ call('systemctl daemon-reload')
+ call(f'systemctl restart {service_name}.service')
+
+ # Initialize interfaces removed from VPP
+ for iface in config.get('removed_ifaces', []):
+ host_control = HostControl()
+ # rescan PCI to use a proper driver
+ host_control.pci_rescan(iface['iface_pci_addr'])
+ # rename to the proper name
+ iface_new_name: str = host_control.get_eth_name(iface['iface_pci_addr'])
+ host_control.rename_iface(iface_new_name, iface['iface_name'])
+
+ if 'interface' in config:
+ # connect to VPP
+ # must be performed multiple attempts because API is not available
+ # immediately after the service restart
+ vpp_control = VPPControl(attempts=20, interval=500)
+ for iface, _ in config['interface'].items():
+ # Create lcp
+ if iface not in Section.interfaces():
+ vpp_control.lcp_pair_add(iface, iface)
+
+ # reinitialize interfaces, but not during the first boot
+ if boot_configuration_complete():
+ call_dependents()
+
+
+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/vrf.py b/src/conf_mode/vrf.py
index 0b983293e..37625142c 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -26,13 +26,15 @@ 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
-from vyos.util import get_interface_config
-from vyos.util import popen
-from vyos.util import run
-from vyos.util import sysctl_write
+from vyos.utils.dict import dict_search
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vrf_members
+from vyos.utils.network import interface_exists
+from vyos.utils.process import call
+from vyos.utils.process import cmd
+from vyos.utils.process import popen
+from vyos.utils.process import run
+from vyos.utils.system import sysctl_write
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -143,7 +145,7 @@ def verify(vrf):
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}'):
+ if interface_exists(name):
tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name)))
if tmp and tmp != vrf_config['table']:
raise ConfigError(f'VRF "{name}" table id modification not possible!')
@@ -195,12 +197,23 @@ def apply(vrf):
sysctl_write('net.ipv4.udp_l3mdev_accept', bind_all)
for tmp in (dict_search('vrf_remove', vrf) or []):
- if os.path.isdir(f'/sys/class/net/{tmp}'):
- call(f'ip link delete dev {tmp}')
+ if interface_exists(tmp):
+ # T5492: deleting a VRF instance may leafe processes running
+ # (e.g. dhclient) as there is a depedency ordering issue in the CLI.
+ # We need to ensure that we stop the dhclient processes first so
+ # a proper DHCLP RELEASE message is sent
+ for interface in get_vrf_members(tmp):
+ vrf_iface = Interface(interface)
+ vrf_iface.set_dhcp(False)
+ vrf_iface.set_dhcpv6(False)
+
# Remove nftables conntrack zone map item
nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}'
cmd(f'nft {nft_del_element}')
+ # Delete the VRF Kernel interface
+ call(f'ip link delete dev {tmp}')
+
if 'name' in vrf:
# Separate VRFs in conntrack table
# check if table already exists
@@ -245,7 +258,7 @@ def apply(vrf):
for name, config in vrf['name'].items():
table = config['table']
- if not os.path.isdir(f'/sys/class/net/{name}'):
+ if not interface_exists(name):
# For each VRF apart from your default context create a VRF
# interface with a separate routing table
call(f'ip link add {name} type vrf table {table}')
diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py
index 9f33536e5..23b341079 100644
--- a/src/conf_mode/vrf_vni.py
+++ b/src/conf_mode/vrf_vni.py
@@ -19,7 +19,7 @@ from sys import exit
from vyos.config import Config
from vyos.template import render_to_string
-from vyos.util import dict_search
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import frr
from vyos import airbag