summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/container.py41
-rwxr-xr-xsrc/conf_mode/firewall.py98
-rwxr-xr-xsrc/conf_mode/interfaces_bonding.py66
-rwxr-xr-xsrc/conf_mode/interfaces_bridge.py5
-rwxr-xr-xsrc/conf_mode/interfaces_ethernet.py37
-rwxr-xr-xsrc/conf_mode/interfaces_geneve.py2
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py28
-rwxr-xr-xsrc/conf_mode/interfaces_tunnel.py12
-rwxr-xr-xsrc/conf_mode/interfaces_vxlan.py2
-rwxr-xr-xsrc/conf_mode/interfaces_wireguard.py74
-rw-r--r--src/conf_mode/load-balancing_haproxy.py7
-rwxr-xr-xsrc/conf_mode/load-balancing_wan.py119
-rwxr-xr-xsrc/conf_mode/nat.py8
-rwxr-xr-xsrc/conf_mode/pki.py12
-rwxr-xr-xsrc/conf_mode/policy.py134
-rwxr-xr-xsrc/conf_mode/protocols_babel.py81
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py43
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py162
-rwxr-xr-xsrc/conf_mode/protocols_eigrp.py93
-rwxr-xr-xsrc/conf_mode/protocols_isis.py105
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py46
-rwxr-xr-xsrc/conf_mode/protocols_nhrp.py118
-rw-r--r--src/conf_mode/protocols_openfabric.py67
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py135
-rwxr-xr-xsrc/conf_mode/protocols_ospfv3.py125
-rwxr-xr-xsrc/conf_mode/protocols_pim.py107
-rwxr-xr-xsrc/conf_mode/protocols_pim6.py71
-rwxr-xr-xsrc/conf_mode/protocols_rip.py82
-rwxr-xr-xsrc/conf_mode/protocols_ripng.py67
-rwxr-xr-xsrc/conf_mode/protocols_rpki.py53
-rwxr-xr-xsrc/conf_mode/protocols_segment-routing.py96
-rwxr-xr-xsrc/conf_mode/protocols_static.py89
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py135
-rwxr-xr-xsrc/conf_mode/service_console-server.py8
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py270
-rwxr-xr-xsrc/conf_mode/service_dns_forwarding.py7
-rw-r--r--[-rwxr-xr-x]src/conf_mode/service_monitoring_network_event.py (renamed from src/conf_mode/service_monitoring_frr-exporter.py)64
-rwxr-xr-xsrc/conf_mode/service_monitoring_node-exporter.py101
-rwxr-xr-xsrc/conf_mode/service_monitoring_prometheus.py206
-rwxr-xr-xsrc/conf_mode/service_monitoring_zabbix-agent.py23
-rwxr-xr-xsrc/conf_mode/service_snmp.py17
-rwxr-xr-xsrc/conf_mode/service_ssh.py57
-rwxr-xr-xsrc/conf_mode/system_flow-accounting.py53
-rwxr-xr-xsrc/conf_mode/system_host-name.py9
-rwxr-xr-xsrc/conf_mode/system_ip.py71
-rwxr-xr-xsrc/conf_mode/system_ipv6.py71
-rwxr-xr-xsrc/conf_mode/system_login.py31
-rwxr-xr-xsrc/conf_mode/system_login_banner.py8
-rwxr-xr-xsrc/conf_mode/system_option.py2
-rwxr-xr-xsrc/conf_mode/system_sflow.py2
-rwxr-xr-xsrc/conf_mode/system_syslog.py86
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py56
-rwxr-xr-xsrc/conf_mode/vrf.py53
53 files changed, 1526 insertions, 1989 deletions
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index a7dc33d9d..18d660a4e 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -22,6 +22,7 @@ from ipaddress import ip_address
from ipaddress import ip_network
from json import dumps as json_write
+import psutil
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -148,6 +149,9 @@ def verify(container):
if network_name not in container.get('network', {}):
raise ConfigError(f'Container network "{network_name}" does not exist!')
+ if 'name_server' in container_config and 'no_name_server' not in container['network'][network_name]:
+ raise ConfigError(f'Setting name server has no effect when attached container network has DNS enabled!')
+
if 'address' in container_config['network'][network_name]:
cnt_ipv4 = 0
cnt_ipv6 = 0
@@ -220,6 +224,21 @@ def verify(container):
if not os.path.exists(source):
raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!')
+ if 'tmpfs' in container_config:
+ for tmpfs, tmpfs_config in container_config['tmpfs'].items():
+ if 'destination' not in tmpfs_config:
+ raise ConfigError(f'tmpfs "{tmpfs}" has no destination path configured!')
+ if 'size' in tmpfs_config:
+ free_mem_mb: int = psutil.virtual_memory().available / 1024 / 1024
+ if int(tmpfs_config['size']) > free_mem_mb:
+ Warning(f'tmpfs "{tmpfs}" size is greater than the current free memory!')
+
+ total_mem_mb: int = (psutil.virtual_memory().total / 1024 / 1024) / 2
+ if int(tmpfs_config['size']) > total_mem_mb:
+ raise ConfigError(f'tmpfs "{tmpfs}" size should not be more than 50% of total system memory!')
+ else:
+ raise ConfigError(f'tmpfs "{tmpfs}" has no size configured!')
+
if 'port' in container_config:
for tmp in container_config['port']:
if not {'source', 'destination'} <= set(container_config['port'][tmp]):
@@ -270,6 +289,13 @@ def verify(container):
if 'registry' in container:
for registry, registry_config in container['registry'].items():
+ if 'mirror' in registry_config:
+ if 'host_name' in registry_config['mirror'] and 'address' in registry_config['mirror']:
+ raise ConfigError(f'Container registry mirror address/host-name are mutually exclusive!')
+
+ if 'path' in registry_config['mirror'] and not registry_config['mirror']['path'].startswith('/'):
+ raise ConfigError('Container registry mirror path must start with "/"!')
+
if 'authentication' not in registry_config:
continue
if not {'username', 'password'} <= set(registry_config['authentication']):
@@ -359,13 +385,26 @@ def generate_run_arguments(name, container_config):
prop = vol_config['propagation']
volume += f' --volume {svol}:{dvol}:{mode},{prop}'
+ # Mount tmpfs
+ tmpfs = ''
+ if 'tmpfs' in container_config:
+ for tmpfs_config in container_config['tmpfs'].values():
+ dest = tmpfs_config['destination']
+ size = tmpfs_config['size']
+ tmpfs += f' --mount=type=tmpfs,tmpfs-size={size}M,destination={dest}'
+
host_pid = ''
if 'allow_host_pid' in container_config:
host_pid = '--pid host'
+ name_server = ''
+ if 'name_server' in container_config:
+ for ns in container_config['name_server']:
+ name_server += f'--dns {ns}'
+
container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} {sysctl_opt} ' \
f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid} {host_pid}'
+ f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid}'
entrypoint = ''
if 'entrypoint' in container_config:
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index ffbd915a2..cebe57092 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -18,7 +18,6 @@ import os
import re
from sys import exit
-
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import is_node_changed
@@ -34,6 +33,8 @@ from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import call
from vyos.utils.process import cmd
from vyos.utils.process import rc_cmd
+from vyos.utils.network import get_vrf_members
+from vyos.utils.network import get_interface_vrf
from vyos import ConfigError
from vyos import airbag
from pathlib import Path
@@ -43,7 +44,7 @@ airbag.enable()
nftables_conf = '/run/nftables.conf'
domain_resolver_usage = '/run/use-vyos-domain-resolver-firewall'
-domain_resolver_usage_nat = '/run/use-vyos-domain-resolver-nat'
+firewall_config_dir = "/config/firewall"
sysctl_file = r'/run/sysctl/10-vyos-firewall.conf'
@@ -53,7 +54,8 @@ valid_groups = [
'network_group',
'port_group',
'interface_group',
- ## Added for group ussage in bridge firewall
+ 'remote_group',
+ ## Added for group usage in bridge firewall
'ipv4_address_group',
'ipv6_address_group',
'ipv4_network_group',
@@ -134,6 +136,27 @@ def get_config(config=None):
fqdn_config_parse(firewall, '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:
+ # Get physical interfaces assigned to the zone if vrf is used:
+ if 'vrf' in local_zone_conf['member']:
+ local_zone_conf['vrf_interfaces'] = {}
+ for vrf_name in local_zone_conf['member']['vrf']:
+ local_zone_conf['vrf_interfaces'][vrf_name] = ','.join(get_vrf_members(vrf_name))
+ continue
+
+ 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]
+
set_dependents('conntrack', conf)
return firewall
@@ -290,8 +313,8 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
raise ConfigError('Only one of address, fqdn or geoip can be specified')
if 'group' in side_conf:
- if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
- raise ConfigError('Only one address-group, network-group or domain-group can be specified')
+ if len({'address_group', 'network_group', 'domain_group', 'remote_group'} & set(side_conf['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group, remote-group or domain-group can be specified')
for group in valid_groups:
if group in side_conf['group']:
@@ -311,7 +334,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
error_group = fw_group.replace("_", "-")
- if group in ['address_group', 'network_group', 'domain_group']:
+ if group in ['address_group', 'network_group', 'domain_group', 'remote_group']:
types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf]
if types:
raise ConfigError(f'{error_group} and {types[0]} cannot both be defined')
@@ -421,6 +444,11 @@ def verify(firewall):
for group_name, group in groups.items():
verify_nested_group(group_name, group, groups, [])
+ if 'remote_group' in firewall['group']:
+ for group_name, group in firewall['group']['remote_group'].items():
+ if 'url' not in group:
+ raise ConfigError(f'remote-group {group_name} must have a url configured')
+
for family in ['ipv4', 'ipv6', 'bridge']:
if family in firewall:
for chain in ['name','forward','input','output', 'prerouting']:
@@ -442,28 +470,45 @@ def verify(firewall):
local_zone = False
zone_interfaces = []
+ zone_vrf = []
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:
+ if 'local_zone' not in zone_conf and 'member' 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:
+ if 'member' 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 'member' in zone_conf:
+ if 'interface' in zone_conf['member']:
+ for iface in zone_conf['member']['interface']:
+
+ if iface in zone_interfaces:
+ raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
- if found_duplicates:
- raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
+ iface_vrf = get_interface_vrf(iface)
+ if iface_vrf != 'default':
+ Warning(f"Interface {iface} assigned to zone {zone} is in VRF {iface_vrf}. This might not work as expected.")
+ zone_interfaces.append(iface)
- zone_interfaces += zone_conf['interface']
+ if 'vrf' in zone_conf['member']:
+ for vrf in zone_conf['member']['vrf']:
+ if vrf in zone_vrf:
+ raise ConfigError(f'VRF cannot be assigned to multiple zones')
+ zone_vrf.append(vrf)
+
+ if 'vrf_interfaces' in zone_conf:
+ for vrf_name, vrf_interfaces in zone_conf['vrf_interfaces'].items():
+ if not vrf_interfaces:
+ raise ConfigError(
+ f'VRF "{vrf_name}" cannot be a member of any zone. It does not contain any interfaces.')
if 'intra_zone_filtering' in zone_conf:
intra_zone = zone_conf['intra_zone_filtering']
@@ -499,24 +544,17 @@ def verify(firewall):
return None
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
-
- 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]
-
render(nftables_conf, 'firewall/nftables.j2', firewall)
render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall)
+
+ # Cleanup remote-group cache files
+ if os.path.exists(firewall_config_dir):
+ for fw_file in os.listdir(firewall_config_dir):
+ # Delete matching files in 'config/firewall' that no longer exist as a remote-group in config
+ if fw_file.startswith("R_") and fw_file.endswith(".txt"):
+ if 'group' not in firewall or 'remote_group' not in firewall['group'] or fw_file[2:-4] not in firewall['group']['remote_group'].keys():
+ os.unlink(os.path.join(firewall_config_dir, fw_file))
+
return None
def parse_firewall_error(output):
@@ -576,7 +614,7 @@ def apply(firewall):
## DOMAIN RESOLVER
domain_action = 'restart'
- if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items():
+ if dict_search_args(firewall, 'group', 'remote_group') or dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items():
text = f'# Automatically generated by firewall.py\nThis file indicates that vyos-domain-resolver service is used by the firewall.\n'
Path(domain_resolver_usage).write_text(text)
else:
diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py
index bbbfb0385..84316c16e 100755
--- a/src/conf_mode/interfaces_bonding.py
+++ b/src/conf_mode/interfaces_bonding.py
@@ -30,19 +30,21 @@ from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.ifconfig import BondIf
from vyos.ifconfig.ethernet import EthernetIf
from vyos.ifconfig import Section
-from vyos.template import render_to_string
from vyos.utils.assertion import assert_mac
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_to_paths_values
from vyos.utils.network import interface_exists
+from vyos.utils.process import is_systemd_service_running
from vyos.configdict import has_address_configured
from vyos.configdict import has_vrf_configured
-from vyos.configdep import set_dependents, call_dependents
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -87,10 +89,13 @@ def get_config(config=None):
bond['mode'] = get_bond_mode(bond['mode'])
tmp = is_node_changed(conf, base + [ifname, 'mode'])
- if tmp: bond['shutdown_required'] = {}
+ if tmp: bond.update({'shutdown_required' : {}})
tmp = is_node_changed(conf, base + [ifname, 'lacp-rate'])
- if tmp: bond['shutdown_required'] = {}
+ if tmp: bond.update({'shutdown_required' : {}})
+
+ tmp = is_node_changed(conf, base + [ifname, 'evpn'])
+ if tmp: bond.update({'frr_dict' : get_frrender_dict(conf)})
# determine which members have been removed
interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface'])
@@ -121,9 +126,8 @@ def get_config(config=None):
# Restore existing config level
conf.set_level(old_level)
- if dict_search('member.interface', bond):
- for interface, interface_config in bond['member']['interface'].items():
-
+ if dict_search('member.interface', bond) is not None:
+ for interface in bond['member']['interface']:
interface_ethernet_config = conf.get_config_dict(
['interfaces', 'ethernet', interface],
key_mangling=('-', '_'),
@@ -132,44 +136,45 @@ def get_config(config=None):
with_defaults=False,
with_recursive_defaults=False)
- interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config)
+ bond['member']['interface'][interface].update({'config_paths' :
+ dict_to_paths_values(interface_ethernet_config)})
# Check if member interface is a new member
if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]):
bond['shutdown_required'] = {}
- interface_config['new_added'] = {}
+ bond['member']['interface'][interface].update({'new_added' : {}})
# Check if member interface is disabled
conf.set_level(['interfaces'])
section = Section.section(interface) # this will be 'ethernet' for 'eth0'
if conf.exists([section, interface, 'disable']):
- interface_config['disable'] = ''
+ if tmp: bond['member']['interface'][interface].update({'disable': ''})
conf.set_level(old_level)
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp: interface_config['is_bridge_member'] = tmp
+ if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp})
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
- for tmp in is_member(conf, interface, 'bonding'):
- if bond['ifname'] == tmp:
- continue
- interface_config['is_bond_member'] = tmp
+ if ifname in tmp:
+ del tmp[ifname]
+ if tmp: bond['member']['interface'][interface].update({'is_bond_member' : tmp})
# Check if member interface is used as source-interface on another interface
tmp = is_source_interface(conf, interface)
- if tmp: interface_config['is_source_interface'] = tmp
+ if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp})
# bond members must not have an assigned address
tmp = has_address_configured(conf, interface)
- if tmp: interface_config['has_address'] = {}
+ if tmp: bond['member']['interface'][interface].update({'has_address' : ''})
# bond members must not have a VRF attached
tmp = has_vrf_configured(conf, interface)
- if tmp: interface_config['has_vrf'] = {}
+ if tmp: bond['member']['interface'][interface].update({'has_vrf' : ''})
+
return bond
@@ -260,16 +265,16 @@ def verify(bond):
return None
def generate(bond):
- bond['frr_zebra_config'] = ''
- if 'deleted' not in bond:
- bond['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', bond)
+ if 'frr_dict' in bond and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(bond['frr_dict'])
return None
def apply(bond):
- ifname = bond['ifname']
- b = BondIf(ifname)
+ if 'frr_dict' in bond and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
+
+ b = BondIf(bond['ifname'])
if 'deleted' in bond:
- # delete interface
b.remove()
else:
b.update(bond)
@@ -281,17 +286,6 @@ def apply(bond):
raise ConfigError('Error in updating ethernet interface '
'after deleting it from bond')
- zebra_daemon = 'zebra'
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # The route-map used for the FIB (zebra) is part of the zebra daemon
- frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True)
- if 'frr_zebra_config' in bond:
- frr_cfg.add_before(frr.default_add_before, bond['frr_zebra_config'])
- frr_cfg.commit_configuration(zebra_daemon)
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py
index 637db442a..aff93af2a 100755
--- a/src/conf_mode/interfaces_bridge.py
+++ b/src/conf_mode/interfaces_bridge.py
@@ -74,8 +74,9 @@ def get_config(config=None):
for interface in list(bridge['member']['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:
- bridge['member']['interface'][interface].update({'is_bridge_member' : tmp})
+ if ifname in tmp:
+ del tmp[ifname]
+ if tmp: bridge['member']['interface'][interface].update({'is_bridge_member' : tmp})
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py
index 34ce7bc47..41c89fdf8 100755
--- a/src/conf_mode/interfaces_ethernet.py
+++ b/src/conf_mode/interfaces_ethernet.py
@@ -33,15 +33,16 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_bond_bridge_member
from vyos.configverify import verify_eapol
from vyos.ethtool import Ethtool
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.ifconfig import EthernetIf
from vyos.ifconfig import BondIf
-from vyos.template import render_to_string
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_to_paths_values
from vyos.utils.dict import dict_set
from vyos.utils.dict import dict_delete
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -164,6 +165,9 @@ def get_config(config=None):
tmp = is_node_changed(conf, base + [ifname, 'duplex'])
if tmp: ethernet.update({'speed_duplex_changed': {}})
+ tmp = is_node_changed(conf, base + [ifname, 'evpn'])
+ if tmp: ethernet.update({'frr_dict' : get_frrender_dict(conf)})
+
return ethernet
def verify_speed_duplex(ethernet: dict, ethtool: Ethtool):
@@ -318,42 +322,25 @@ def verify_ethernet(ethernet):
return None
def generate(ethernet):
- if 'deleted' in ethernet:
- return None
-
- ethernet['frr_zebra_config'] = ''
- if 'deleted' not in ethernet:
- ethernet['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', ethernet)
-
+ if 'frr_dict' in ethernet and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(ethernet['frr_dict'])
return None
def apply(ethernet):
- ifname = ethernet['ifname']
-
- e = EthernetIf(ifname)
+ if 'frr_dict' in ethernet and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
+ e = EthernetIf(ethernet['ifname'])
if 'deleted' in ethernet:
- # delete interface
e.remove()
else:
e.update(ethernet)
-
- zebra_daemon = 'zebra'
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # The route-map used for the FIB (zebra) is part of the zebra daemon
- frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True)
- if 'frr_zebra_config' in ethernet:
- frr_cfg.add_before(frr.default_add_before, ethernet['frr_zebra_config'])
- frr_cfg.commit_configuration(zebra_daemon)
+ return None
if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
-
apply(c)
except ConfigError as e:
print(e)
diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py
index 007708d4a..1c5b4d0e7 100755
--- a/src/conf_mode/interfaces_geneve.py
+++ b/src/conf_mode/interfaces_geneve.py
@@ -47,7 +47,7 @@ def get_config(config=None):
# GENEVE interfaces are picky and require recreation if certain parameters
# change. But a GENEVE interface should - of course - not be re-created if
# it's description or IP address is adjusted. Feels somehow logic doesn't it?
- for cli_option in ['remote', 'vni', 'parameters']:
+ for cli_option in ['remote', 'vni', 'parameters', 'port']:
if is_node_changed(conf, base + [ifname, cli_option]):
geneve.update({'rebuild_required': {}})
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index 8c1213e2b..a9b4e570d 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -32,6 +32,7 @@ from vyos.base import DeprecationWarning
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
+from vyos.configdiff import get_config_diff
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_mirror_redirect
@@ -94,6 +95,23 @@ def get_config(config=None):
if 'deleted' in openvpn:
return openvpn
+ if not is_node_changed(conf, base) and dict_search_args(openvpn, 'tls'):
+ diff = get_config_diff(conf)
+ if diff.get_child_nodes_diff(['pki'], recursive=True).get('add') == ['ca', 'certificate']:
+ crl_path = os.path.join(cfg_dir, f'{ifname}_crl.pem')
+ if os.path.exists(crl_path):
+ # do not restart service when changed only CRL and crl file already exist
+ openvpn.update({'no_restart_crl': True})
+ for rec in diff.get_child_nodes_diff(['pki', 'ca'], recursive=True).get('add'):
+ if diff.get_child_nodes_diff(['pki', 'ca', rec], recursive=True).get('add') != ['crl']:
+ openvpn.update({'no_restart_crl': False})
+ break
+ if openvpn.get('no_restart_crl'):
+ for rec in diff.get_child_nodes_diff(['pki', 'certificate'], recursive=True).get('add'):
+ if diff.get_child_nodes_diff(['pki', 'certificate', rec], recursive=True).get('add') != ['revoke']:
+ openvpn.update({'no_restart_crl': False})
+ break
+
if is_node_changed(conf, base + [ifname, 'openvpn-option']):
openvpn.update({'restart_required': {}})
if is_node_changed(conf, base + [ifname, 'enable-dco']):
@@ -786,10 +804,12 @@ def apply(openvpn):
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
- action = 'reload-or-restart'
- if 'restart_required' in openvpn:
- action = 'restart'
- call(f'systemctl {action} openvpn@{interface}.service')
+
+ if not openvpn.get('no_restart_crl'):
+ action = 'reload-or-restart'
+ if 'restart_required' in openvpn:
+ action = 'restart'
+ call(f'systemctl {action} openvpn@{interface}.service')
o = VTunIf(**openvpn)
o.update(openvpn)
diff --git a/src/conf_mode/interfaces_tunnel.py b/src/conf_mode/interfaces_tunnel.py
index 98ef98d12..ee1436e49 100755
--- a/src/conf_mode/interfaces_tunnel.py
+++ b/src/conf_mode/interfaces_tunnel.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 yOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -13,9 +13,8 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
from sys import exit
-
+import ipaddress
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
@@ -89,6 +88,13 @@ def verify(tunnel):
raise ConfigError('Tunnel used for NHRP, it can not be deleted!')
return None
+ if 'nhrp' in tunnel:
+ if 'address' in tunnel:
+ address_list = dict_search('address', tunnel)
+ for tunip in address_list:
+ if ipaddress.ip_network(tunip, strict=False).prefixlen != 32:
+ raise ConfigError(
+ 'Tunnel is used for NHRP, Netmask should be /32!')
verify_tunnel(tunnel)
diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py
index 68646e8ff..256b65708 100755
--- a/src/conf_mode/interfaces_vxlan.py
+++ b/src/conf_mode/interfaces_vxlan.py
@@ -95,6 +95,8 @@ def verify(vxlan):
if 'group' in vxlan:
if 'source_interface' not in vxlan:
raise ConfigError('Multicast VXLAN requires an underlaying interface')
+ if 'remote' in vxlan:
+ raise ConfigError('Both group and remote cannot be specified')
verify_source_interface(vxlan)
if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan):
diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
index b6fd6b0b2..192937dba 100755
--- a/src/conf_mode/interfaces_wireguard.py
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -19,6 +19,9 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
+from vyos.configdict import is_source_interface
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
@@ -29,8 +32,10 @@ from vyos.ifconfig import WireGuardIf
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.utils.process import call
from vyos import ConfigError
from vyos import airbag
+from pathlib import Path
airbag.enable()
@@ -54,11 +59,31 @@ def get_config(config=None):
if is_node_changed(conf, base + [ifname, 'peer']):
wireguard.update({'rebuild_required': {}})
+ wireguard['peers_need_resolve'] = []
+ if 'peer' in wireguard:
+ for peer, peer_config in wireguard['peer'].items():
+ if 'disable' not in peer_config and 'host_name' in peer_config:
+ wireguard['peers_need_resolve'].append(peer)
+
+ # Check if interface is used as source-interface on VXLAN interface
+ tmp = is_source_interface(conf, ifname, 'vxlan')
+ if tmp:
+ if 'deleted' not in wireguard:
+ set_dependents('vxlan', conf, tmp)
+ else:
+ wireguard['is_source_interface'] = tmp
+
return wireguard
+
def verify(wireguard):
if 'deleted' in wireguard:
verify_bridge_delete(wireguard)
+ if 'is_source_interface' in wireguard:
+ raise ConfigError(
+ f'Interface "{wireguard["ifname"]}" cannot be deleted as it is used '
+ f'as source interface for "{wireguard["is_source_interface"]}"!'
+ )
return None
verify_mtu_ipv6(wireguard)
@@ -82,28 +107,41 @@ def verify(wireguard):
for tmp in wireguard['peer']:
peer = wireguard['peer'][tmp]
+ base_error = f'WireGuard peer "{tmp}":'
+
+ if 'host_name' in peer and 'address' in peer:
+ raise ConfigError(f'{base_error} address/host-name are mutually exclusive!')
+
if 'allowed_ips' not in peer:
- raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
+ raise ConfigError(f'{base_error} missing mandatory allowed-ips!')
if 'public_key' not in peer:
- raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
-
- if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
- raise ConfigError('Both Wireguard port and address must be defined '
- f'for peer "{tmp}" if either one of them is set!')
+ raise ConfigError(f'{base_error} missing mandatory public-key!')
if peer['public_key'] in public_keys:
- raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"')
+ raise ConfigError(f'{base_error} duplicate public-key!')
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"]}"')
+ tmp = wireguard["ifname"]
+ raise ConfigError(f'{base_error} identical public key as interface "{tmp}"!')
+
+ port_addr_error = f'{base_error} both port and address/host-name must '\
+ 'be defined if either one of them is set!'
+ if 'port' not in peer:
+ if 'host_name' in peer or 'address' in peer:
+ raise ConfigError(port_addr_error)
+ else:
+ if 'host_name' not in peer and 'address' not in peer:
+ raise ConfigError(port_addr_error)
public_keys.append(peer['public_key'])
+
def generate(wireguard):
return None
+
def apply(wireguard):
check_kmod('wireguard')
@@ -122,8 +160,28 @@ def apply(wireguard):
wg = WireGuardIf(**wireguard)
wg.update(wireguard)
+ domain_resolver_usage = '/run/use-vyos-domain-resolver-interfaces-wireguard-' + wireguard['ifname']
+
+ ## DOMAIN RESOLVER
+ domain_action = 'restart'
+ if 'peers_need_resolve' in wireguard and len(wireguard['peers_need_resolve']) > 0 and 'disable' not in wireguard:
+ from vyos.utils.file import write_file
+
+ text = f'# Automatically generated by interfaces_wireguard.py\nThis file indicates that vyos-domain-resolver service is used by the interfaces_wireguard.\n'
+ text += "intefaces:\n" + "".join([f" - {peer}\n" for peer in wireguard['peers_need_resolve']])
+ Path(domain_resolver_usage).write_text(text)
+ write_file(domain_resolver_usage, text)
+ else:
+ Path(domain_resolver_usage).unlink(missing_ok=True)
+ if not Path('/run').glob('use-vyos-domain-resolver*'):
+ domain_action = 'stop'
+ call(f'systemctl {domain_action} vyos-domain-resolver.service')
+
+ call_dependents()
+
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/load-balancing_haproxy.py b/src/conf_mode/load-balancing_haproxy.py
index 45042dd52..5fd1beec9 100644
--- a/src/conf_mode/load-balancing_haproxy.py
+++ b/src/conf_mode/load-balancing_haproxy.py
@@ -78,6 +78,13 @@ def verify(lb):
not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
+ if 'http_compression' in front_config:
+ if front_config['mode'] != 'http':
+ raise ConfigError(f'service {front} must be set to http mode to use http-compression!')
+ if len(front_config['http_compression']['mime_type']) == 0:
+ raise ConfigError(f'service {front} must have at least one mime-type configured to use'
+ f'http_compression!')
+
for back, back_config in lb['backend'].items():
if 'http_check' in back_config:
http_check = back_config['http_check']
diff --git a/src/conf_mode/load-balancing_wan.py b/src/conf_mode/load-balancing_wan.py
index 5da0b906b..92d9acfba 100755
--- a/src/conf_mode/load-balancing_wan.py
+++ b/src/conf_mode/load-balancing_wan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2023-2025 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,24 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
-
from sys import exit
-from shutil import rmtree
-from vyos.base import Warning
from vyos.config import Config
from vyos.configdep import set_dependents, call_dependents
from vyos.utils.process import cmd
-from vyos.template import render
from vyos import ConfigError
from vyos import airbag
airbag.enable()
-load_balancing_dir = '/run/load-balance'
-load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf'
-systemd_service = 'vyos-wan-load-balance.service'
-
+service = 'vyos-wan-load-balance.service'
def get_config(config=None):
if config:
@@ -40,6 +32,7 @@ def get_config(config=None):
conf = Config()
base = ['load-balancing', 'wan']
+
lb = conf.get_config_dict(base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
get_first_key=True,
@@ -59,87 +52,61 @@ def verify(lb):
if not lb:
return None
- if 'interface_health' not in lb:
- raise ConfigError(
- 'A valid WAN load-balance configuration requires an interface with a nexthop!'
- )
-
- for interface, interface_config in lb['interface_health'].items():
- if 'nexthop' not in interface_config:
- raise ConfigError(
- f'interface-health {interface} nexthop must be specified!')
-
- if 'test' in interface_config:
- for test_rule, test_config in interface_config['test'].items():
- if 'type' in test_config:
- if test_config['type'] == 'user-defined' and 'test_script' not in test_config:
- raise ConfigError(
- f'test {test_rule} script must be defined for test-script!'
- )
-
- if 'rule' not in lb:
- Warning(
- 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!'
- )
+ if 'interface_health' in lb:
+ for ifname, health_conf in lb['interface_health'].items():
+ if 'nexthop' not in health_conf:
+ raise ConfigError(f'Nexthop must be configured for interface {ifname}')
+
+ if 'test' not in health_conf:
+ continue
+
+ for test_id, test_conf in health_conf['test'].items():
+ if 'type' not in test_conf:
+ raise ConfigError(f'No type configured for health test on interface {ifname}')
+
+ if test_conf['type'] == 'user-defined' and 'test_script' not in test_conf:
+ raise ConfigError(f'Missing user-defined script for health test on interface {ifname}')
else:
- for rule, rule_config in lb['rule'].items():
- if 'inbound_interface' not in rule_config:
- raise ConfigError(f'rule {rule} inbound-interface must be specified!')
- if {'failover', 'exclude'} <= set(rule_config):
- raise ConfigError(f'rule {rule} failover cannot be configured with exclude!')
- if {'limit', 'exclude'} <= set(rule_config):
- raise ConfigError(f'rule {rule} limit cannot be used with exclude!')
- if 'interface' not in rule_config:
- if 'exclude' not in rule_config:
- Warning(
- f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule'
- )
- for direction in {'source', 'destination'}:
- if direction in rule_config:
- if 'protocol' in rule_config and 'port' in rule_config[
- direction]:
- if rule_config['protocol'] not in {'tcp', 'udp'}:
- raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"')
+ raise ConfigError('Interface health tests must be configured')
+ if 'rule' in lb:
+ for rule_id, rule_conf in lb['rule'].items():
+ if 'interface' not in rule_conf and 'exclude' not in rule_conf:
+ raise ConfigError(f'Interface or exclude not specified on load-balancing wan rule {rule_id}')
-def generate(lb):
- if not lb:
- # Delete /run/load-balance/wlb.conf
- if os.path.isfile(load_balancing_conf_file):
- os.unlink(load_balancing_conf_file)
- # Delete old directories
- if os.path.isdir(load_balancing_dir):
- rmtree(load_balancing_dir, ignore_errors=True)
- if os.path.exists('/var/run/load-balance/wlb.out'):
- os.unlink('/var/run/load-balance/wlb.out')
+ if 'failover' in rule_conf and 'exclude' in rule_conf:
+ raise ConfigError(f'Failover cannot be configured with exclude on load-balancing wan rule {rule_id}')
- return None
+ if 'limit' in rule_conf:
+ if 'exclude' in rule_conf:
+ raise ConfigError(f'Limit cannot be configured with exclude on load-balancing wan rule {rule_id}')
- # Create load-balance dir
- if not os.path.isdir(load_balancing_dir):
- os.mkdir(load_balancing_dir)
+ if 'rate' in rule_conf['limit'] and 'period' not in rule_conf['limit']:
+ raise ConfigError(f'Missing "limit period" on load-balancing wan rule {rule_id}')
- render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb)
+ if 'period' in rule_conf['limit'] and 'rate' not in rule_conf['limit']:
+ raise ConfigError(f'Missing "limit rate" on load-balancing wan rule {rule_id}')
- return None
+ for direction in ['source', 'destination']:
+ if direction in rule_conf:
+ if 'port' in rule_conf[direction]:
+ if 'protocol' not in rule_conf:
+ raise ConfigError(f'Protocol required to specify port on load-balancing wan rule {rule_id}')
+
+ if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ raise ConfigError(f'Protocol must be tcp, udp or tcp_udp to specify port on load-balancing wan rule {rule_id}')
+def generate(lb):
+ return None
def apply(lb):
if not lb:
- try:
- cmd(f'systemctl stop {systemd_service}')
- except Exception as e:
- print(f"Error message: {e}")
-
+ cmd(f'sudo systemctl stop {service}')
else:
- cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1')
- cmd(f'systemctl restart {systemd_service}')
+ cmd(f'sudo systemctl restart {service}')
call_dependents()
- return None
-
-
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 98b2f3f29..504b3e82a 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -17,6 +17,7 @@
import os
from sys import exit
+from pathlib import Path
from vyos.base import Warning
from vyos.config import Config
@@ -43,7 +44,6 @@ k_mod = ['nft_nat', 'nft_chain_nat']
nftables_nat_config = '/run/nftables_nat.conf'
nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft'
domain_resolver_usage = '/run/use-vyos-domain-resolver-nat'
-domain_resolver_usage_firewall = '/run/use-vyos-domain-resolver-firewall'
valid_groups = [
'address_group',
@@ -265,9 +265,9 @@ def apply(nat):
text = f'# Automatically generated by nat.py\nThis file indicates that vyos-domain-resolver service is used by nat.\n'
write_file(domain_resolver_usage, text)
elif os.path.exists(domain_resolver_usage):
- os.unlink(domain_resolver_usage)
- if not os.path.exists(domain_resolver_usage_firewall):
- # Firewall not using domain resolver
+ Path(domain_resolver_usage).unlink(missing_ok=True)
+
+ if not Path('/run').glob('use-vyos-domain-resolver*'):
domain_action = 'stop'
call(f'systemctl {domain_action} vyos-domain-resolver.service')
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index acea2c9be..724f97555 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -440,13 +440,21 @@ def generate(pki):
for name, cert_conf in pki['certificate'].items():
if 'acme' in cert_conf:
certbot_list.append(name)
- # generate certificate if not found on disk
+ # There is no ACME/certbot managed certificate presend on the
+ # system, generate it
if name not in certbot_list_on_disk:
certbot_request(name, cert_conf['acme'], dry_run=False)
+ # Now that the certificate was properly generated we have
+ # the PEM files on disk. We need to add the certificate to
+ # certbot_list_on_disk to automatically import the CA chain
+ certbot_list_on_disk.append(name)
+ # We alredy had an ACME managed certificate on the system, but
+ # something changed in the configuration
elif changed_certificates != None and name in changed_certificates:
- # when something for the certificate changed, we should delete it
+ # Delete old ACME certificate first
if name in certbot_list_on_disk:
certbot_delete(name)
+ # Request new certificate via certbot
certbot_request(name, cert_conf['acme'], dry_run=False)
# Cleanup certbot configuration and certificates if no longer in use by CLI
diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py
index a5963e72c..a90e33e81 100755
--- a/src/conf_mode/policy.py
+++ b/src/conf_mode/policy.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2022 VyOS maintainers and contributors
+# Copyright (C) 2021-2024 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,16 +17,16 @@
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.template import render_to_string
+from vyos.configverify import has_frr_protocol_in_dict
+from vyos.frrender import FRRender
+from vyos.frrender import frr_protocols
+from vyos.frrender import get_frrender_dict
from vyos.utils.dict import dict_search
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
-
airbag.enable()
-
def community_action_compatibility(actions: dict) -> bool:
"""
Check compatibility of values in community and large community sections
@@ -87,31 +87,27 @@ def get_config(config=None):
else:
conf = Config()
- base = ['policy']
- policy = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
-
- # We also need some additional information from the config, prefix-lists
- # and route-maps for instance. They will be used in verify().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['protocols'], key_mangling=('-', '_'),
- no_tag_node_value_mangle=True)
- # Merge policy dict into "regular" config dict
- policy = dict_merge(tmp, policy)
- return policy
-
-
-def verify(policy):
- if not policy:
+ return get_frrender_dict(conf)
+
+
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'policy'):
return None
- for policy_type in ['access_list', 'access_list6', 'as_path_list',
- 'community_list', 'extcommunity_list',
- 'large_community_list',
- 'prefix_list', 'prefix_list6', 'route_map']:
+ policy_types = ['access_list', 'access_list6', 'as_path_list',
+ 'community_list', 'extcommunity_list',
+ 'large_community_list', 'prefix_list',
+ 'prefix_list6', 'route_map']
+
+ policy = config_dict['policy']
+ for protocol in frr_protocols:
+ if protocol not in config_dict:
+ continue
+ if 'protocol' not in policy:
+ policy.update({'protocol': {}})
+ policy['protocol'].update({protocol : config_dict[protocol]})
+
+ for policy_type in policy_types:
# Bail out early and continue with next policy type
if policy_type not in policy:
continue
@@ -246,72 +242,36 @@ def verify(policy):
# When the "routing policy" changes and policies, route-maps etc. are deleted,
# it is our responsibility to verify that the policy can not be deleted if it
# is used by any routing protocol
- if 'protocols' in policy:
- for policy_type in ['access_list', 'access_list6', 'as_path_list',
- 'community_list',
- 'extcommunity_list', 'large_community_list',
- 'prefix_list', 'route_map']:
- if policy_type in policy:
- for policy_name in list(set(routing_policy_find(policy_type,
- policy[
- 'protocols']))):
- found = False
- if policy_name in policy[policy_type]:
- found = True
- # BGP uses prefix-list for selecting both an IPv4 or IPv6 AFI related
- # list - we need to go the extra mile here and check both prefix-lists
- if policy_type == 'prefix_list' and 'prefix_list6' in policy and policy_name in \
- policy['prefix_list6']:
- found = True
- if not found:
- tmp = policy_type.replace('_', '-')
- raise ConfigError(
- f'Can not delete {tmp} "{policy_name}", still in use!')
+ # Check if any routing protocol is activated
+ if 'protocol' in policy:
+ for policy_type in policy_types:
+ for policy_name in list(set(routing_policy_find(policy_type, policy['protocol']))):
+ found = False
+ if policy_type in policy and policy_name in policy[policy_type]:
+ found = True
+ # BGP uses prefix-list for selecting both an IPv4 or IPv6 AFI related
+ # list - we need to go the extra mile here and check both prefix-lists
+ if policy_type == 'prefix_list' and 'prefix_list6' in policy and policy_name in \
+ policy['prefix_list6']:
+ found = True
+ if not found:
+ tmp = policy_type.replace('_', '-')
+ raise ConfigError(
+ f'Can not delete {tmp} "{policy_name}", still in use!')
return None
-def generate(policy):
- if not policy:
- return None
- policy['new_frr_config'] = render_to_string('frr/policy.frr.j2', policy)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-
-def apply(policy):
- bgp_daemon = 'bgpd'
- zebra_daemon = 'zebra'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # The route-map used for the FIB (zebra) is part of the zebra daemon
- frr_cfg.load_configuration(bgp_daemon)
- frr_cfg.modify_section(r'^bgp as-path access-list .*')
- frr_cfg.modify_section(r'^bgp community-list .*')
- frr_cfg.modify_section(r'^bgp extcommunity-list .*')
- frr_cfg.modify_section(r'^bgp large-community-list .*')
- frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit',
- remove_stop_mark=True)
- if 'new_frr_config' in policy:
- frr_cfg.add_before(frr.default_add_before, policy['new_frr_config'])
- frr_cfg.commit_configuration(bgp_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'^access-list .*')
- frr_cfg.modify_section(r'^ipv6 access-list .*')
- frr_cfg.modify_section(r'^ip prefix-list .*')
- frr_cfg.modify_section(r'^ipv6 prefix-list .*')
- frr_cfg.modify_section(r'^route-map .*', stop_pattern='^exit',
- remove_stop_mark=True)
- if 'new_frr_config' in policy:
- frr_cfg.add_before(frr.default_add_before, policy['new_frr_config'])
- frr_cfg.commit_configuration(zebra_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
-
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py
index 90b6e4a31..80a847af8 100755
--- a/src/conf_mode/protocols_babel.py
+++ b/src/conf_mode/protocols_babel.py
@@ -17,15 +17,14 @@
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 has_frr_protocol_in_dict
from vyos.configverify import verify_access_list
from vyos.configverify import verify_prefix_list
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.utils.dict import dict_search
-from vyos.template import render_to_string
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -34,46 +33,16 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'babel']
- babel = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
- # FRR has VRF support for different routing daemons. As interfaces belong
- # to VRFs - or the global VRF, we need to check for changed interfaces so
- # that they will be properly rendered for the FRR config. Also this eases
- # removal of interfaces from the running configuration.
- interfaces_removed = node_changed(conf, base + ['interface'])
- if interfaces_removed:
- babel['interface_removed'] = list(interfaces_removed)
+ return get_frrender_dict(conf)
- # Bail out early if configuration tree does not exist
- if not conf.exists(base):
- babel.update({'deleted' : ''})
- return babel
-
- # We have gathered the dict representation of the CLI, but there are default
- # 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)
-
- # 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().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- babel = dict_merge(tmp, babel)
- return babel
-
-def verify(babel):
- if not babel:
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'babel'):
return None
+ babel = config_dict['babel']
+ babel['policy'] = config_dict['policy']
+
# verify distribute_list
if "distribute_list" in babel:
acl_keys = {
@@ -120,32 +89,14 @@ def verify(babel):
verify_prefix_list(prefix_list, babel, version='6' if address_family == 'ipv6' else '')
-def generate(babel):
- if not babel or 'deleted' in babel:
- return None
-
- babel['new_frr_config'] = render_to_string('frr/babeld.frr.j2', babel)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(babel):
- babel_daemon = 'babeld'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- frr_cfg.load_configuration(babel_daemon)
- frr_cfg.modify_section('^router babel', stop_pattern='^exit', remove_stop_mark=True)
-
- for key in ['interface', 'interface_removed']:
- if key not in babel:
- continue
- for interface in babel[key]:
- frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
-
- if 'new_frr_config' in babel:
- frr_cfg.add_before(frr.default_add_before, babel['new_frr_config'])
- frr_cfg.commit_configuration(babel_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index 1361bb1a9..d3bc3e961 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -16,11 +16,13 @@
from vyos.config import Config
from vyos.configverify import verify_vrf
+from vyos.configverify import has_frr_protocol_in_dict
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.template import is_ipv6
-from vyos.template import render_to_string
from vyos.utils.network import is_ipv6_link_local
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -29,22 +31,14 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'bfd']
- bfd = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
- # Bail out early if configuration tree does not exist
- if not conf.exists(base):
- return bfd
- bfd = conf.merge_defaults(bfd, recursive=True)
+ return get_frrender_dict(conf)
- return bfd
-
-def verify(bfd):
- if not bfd:
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'bfd'):
return None
+ bfd = config_dict['bfd']
if 'peer' in bfd:
for peer, peer_config in bfd['peer'].items():
# IPv6 link local peers require an explicit local address/interface
@@ -83,22 +77,13 @@ def verify(bfd):
return None
-def generate(bfd):
- if not bfd:
- return None
- bfd['new_frr_config'] = render_to_string('frr/bfdd.frr.j2', bfd)
-
-def apply(bfd):
- bfd_daemon = 'bfdd'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(bfd_daemon)
- frr_cfg.modify_section('^bfd', stop_pattern='^exit', remove_stop_mark=True)
- if 'new_frr_config' in bfd:
- frr_cfg.add_before(frr.default_add_before, bfd['new_frr_config'])
- frr_cfg.commit_configuration(bfd_daemon)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 22f020099..53e83c3b4 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -19,21 +19,20 @@ 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 has_frr_protocol_in_dict
from vyos.configverify import verify_prefix_list
from vyos.configverify import verify_route_map
from vyos.configverify import verify_vrf
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.template import is_ip
from vyos.template import is_interface
-from vyos.template import render_to_string
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.utils.process import is_systemd_service_running
from vyos.utils.process import process_named_running
-from vyos.utils.process import call
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -43,68 +42,7 @@ def get_config(config=None):
else:
conf = Config()
- vrf = None
- if len(argv) > 1:
- vrf = argv[1]
-
- base_path = ['protocols', 'bgp']
-
- # eqivalent of the C foo ? 'a' : 'b' statement
- base = vrf and ['vrf', 'name', vrf, 'protocols', 'bgp'] or base_path
- bgp = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
- bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
- key_mangling=('-', '_'),
- 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.
- if vrf:
- bgp.update({'vrf' : vrf})
- # We can not delete the BGP VRF instance if there is a L3VNI configured
- # FRR L3VNI must be deleted first otherwise we will see error:
- # "FRR error: Please unconfigure l3vni 3000"
- tmp = ['vrf', 'name', vrf, 'vni']
- if conf.exists_effective(tmp):
- bgp.update({'vni' : conf.return_effective_value(tmp)})
- # We can safely delete ourself from the dependent vrf list
- if vrf in bgp['dependent_vrfs']:
- del bgp['dependent_vrfs'][vrf]
-
- bgp['dependent_vrfs'].update({'default': {'protocols': {
- 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)}}})
-
- if not conf.exists(base):
- # If bgp instance is deleted then mark it
- bgp.update({'deleted' : ''})
- return bgp
-
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- bgp = conf.merge_defaults(bgp, recursive=True)
-
- # We also need some additional information from the config, prefix-lists
- # and route-maps for instance. They will be used in verify().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- bgp = dict_merge(tmp, bgp)
-
- return bgp
-
+ return get_frrender_dict(conf, argv)
def verify_vrf_as_import(search_vrf_name: str, afi_name: str, vrfs_config: dict) -> bool:
"""
@@ -237,13 +175,24 @@ def verify_afi(peer_config, bgp_config):
if tmp: return True
return False
-def verify(bgp):
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'bgp'):
+ return None
+
+ vrf = None
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
+
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ bgp = vrf and config_dict['vrf']['name'][vrf]['protocols']['bgp'] or config_dict['bgp']
+ bgp['policy'] = config_dict['policy']
+
if 'deleted' in bgp:
- if 'vrf' in bgp:
+ if vrf:
# Cannot delete vrf if it exists in import vrf list in other vrfs
for tmp_afi in ['ipv4_unicast', 'ipv6_unicast']:
- if verify_vrf_as_import(bgp['vrf'], tmp_afi, bgp['dependent_vrfs']):
- raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \
+ if verify_vrf_as_import(vrf, tmp_afi, bgp['dependent_vrfs']):
+ raise ConfigError(f'Cannot delete VRF instance "{vrf}", ' \
'unconfigure "import vrf" commands!')
else:
# We are running in the default VRF context, thus we can not delete
@@ -252,8 +201,9 @@ def verify(bgp):
for vrf, vrf_options in bgp['dependent_vrfs'].items():
if vrf != 'default':
if dict_search('protocols.bgp', vrf_options):
- raise ConfigError('Cannot delete default BGP instance, ' \
- 'dependent VRF instance(s) exist(s)!')
+ dependent_vrfs = ', '.join(bgp['dependent_vrfs'].keys())
+ raise ConfigError(f'Cannot delete default BGP instance, ' \
+ f'dependent VRF instance(s): {dependent_vrfs}')
if 'vni' in vrf_options:
raise ConfigError('Cannot delete default BGP instance, ' \
'dependent L3VNI exists!')
@@ -281,9 +231,8 @@ def verify(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']
+ if vrf:
+ if vrf != tmp:
raise ConfigError(f'{error_msg} "{vrf}"!')
elif tmp != 'default':
raise ConfigError(f'{error_msg} "{tmp}"!')
@@ -384,10 +333,8 @@ def verify(bgp):
# Only checks for ipv4 and ipv6 neighbors
# Check if neighbor address is assigned as system interface address
- vrf = None
vrf_error_msg = f' in default VRF!'
- if 'vrf' in bgp:
- vrf = bgp['vrf']
+ if vrf:
vrf_error_msg = f' in VRF "{vrf}"!'
if is_ip(peer) and is_addr_assigned(peer, vrf):
@@ -529,7 +476,7 @@ def verify(bgp):
f'{afi} administrative distance {key}!')
if afi in ['ipv4_unicast', 'ipv6_unicast']:
- vrf_name = bgp['vrf'] if dict_search('vrf', bgp) else 'default'
+ vrf_name = vrf if vrf else 'default'
# Verify if currant VRF contains rd and route-target options
# and does not exist in import list in other VRFs
if dict_search(f'rd.vpn.export', afi_config):
@@ -576,12 +523,21 @@ def verify(bgp):
raise ConfigError(
'Please unconfigure import vrf commands before using vpn commands in dependent VRFs!')
+ if (dict_search('route_map.vrf.import', afi_config) is not None
+ or dict_search('import.vrf', afi_config) is not None):
# FRR error: please unconfigure vpn to vrf commands before
# using import vrf commands
- if 'vpn' in afi_config['import'] or dict_search('export.vpn', afi_config) != None:
+ if ('vpn' in afi_config['import']
+ or dict_search('export.vpn', afi_config) is not None):
raise ConfigError('Please unconfigure VPN to VRF commands before '\
'using "import vrf" commands!')
+ if (dict_search('route_map.vpn.import', afi_config) is not None
+ or dict_search('route_map.vpn.export', afi_config) is not None) :
+ raise ConfigError('Please unconfigure route-map VPN to VRF commands before '\
+ 'using "import vrf" commands!')
+
+
# Verify that the export/import route-maps do exist
for export_import in ['export', 'import']:
tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)
@@ -602,46 +558,14 @@ def verify(bgp):
return None
-def generate(bgp):
- if not bgp or 'deleted' in bgp:
- return None
-
- bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.j2', bgp)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(bgp):
- if 'deleted' in bgp:
- # We need to ensure that the L3VNI is deleted first.
- # This is not possible with old config backend
- # priority bug
- if {'vrf', 'vni'} <= set(bgp):
- call('vtysh -c "conf t" -c "vrf {vrf}" -c "no vni {vni}"'.format(**bgp))
-
- bgp_daemon = 'bgpd'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # Generate empty helper string which can be ammended to FRR commands, it
- # will be either empty (default VRF) or contain the "vrf <name" statement
- vrf = ''
- if 'vrf' in 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'])
- frr_cfg.commit_configuration(bgp_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py
index c13e52a3d..324ff883f 100755
--- a/src/conf_mode/protocols_eigrp.py
+++ b/src/conf_mode/protocols_eigrp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2022-2024 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,94 +18,49 @@ from sys import exit
from sys import argv
from vyos.config import Config
-from vyos.configdict import dict_merge
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.configverify import verify_vrf
-from vyos.template import render_to_string
+from vyos.utils.process import is_systemd_service_running
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
-
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- vrf = None
- if len(argv) > 1:
- vrf = argv[1]
-
- base_path = ['protocols', 'eigrp']
-
- # eqivalent of the C foo ? 'a' : 'b' statement
- base = vrf and ['vrf', 'name', vrf, 'protocols', 'eigrp'] or base_path
- eigrp = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ return get_frrender_dict(conf, argv)
- # Assign the name of our VRF context. This MUST be done before the return
- # statement below, else on deletion we will delete the default instance
- # instead of the VRF instance.
- if vrf: eigrp.update({'vrf' : vrf})
-
- if not conf.exists(base):
- eigrp.update({'deleted' : ''})
- if not vrf:
- # We are running in the default VRF context, thus we can not delete
- # our main EIGRP instance if there are dependent EIGRP VRF instances.
- eigrp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
- key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
-
- return eigrp
-
- # We also need some additional information from the config, prefix-lists
- # and route-maps for instance. They will be used in verify().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- eigrp = dict_merge(tmp, eigrp)
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'eigrp'):
+ return None
- return eigrp
+ vrf = None
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
-def verify(eigrp):
- if not eigrp or 'deleted' in eigrp:
- return
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ eigrp = vrf and config_dict['vrf']['name'][vrf]['protocols']['eigrp'] or config_dict['eigrp']
+ eigrp['policy'] = config_dict['policy']
if 'system_as' not in eigrp:
raise ConfigError('EIGRP system-as must be defined!')
- if 'vrf' in eigrp:
- verify_vrf(eigrp)
-
-def generate(eigrp):
- if not eigrp or 'deleted' in eigrp:
- return None
-
- eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp)
+ if vrf:
+ verify_vrf({'vrf': vrf})
-def apply(eigrp):
- eigrp_daemon = 'eigrpd'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # Generate empty helper string which can be ammended to FRR commands, it
- # will be either empty (default VRF) or contain the "vrf <name" statement
- vrf = ''
- if 'vrf' in eigrp:
- vrf = ' vrf ' + eigrp['vrf']
-
- frr_cfg.load_configuration(eigrp_daemon)
- frr_cfg.modify_section(f'^router eigrp \d+{vrf}', stop_pattern='^exit', remove_stop_mark=True)
- if 'frr_eigrpd_config' in eigrp:
- frr_cfg.add_before(frr.default_add_before, eigrp['frr_eigrpd_config'])
- frr_cfg.commit_configuration(eigrp_daemon)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
+ return None
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py
index ba2f3cf0d..1c994492e 100755
--- a/src/conf_mode/protocols_isis.py
+++ b/src/conf_mode/protocols_isis.py
@@ -18,16 +18,16 @@ from sys import exit
from sys import argv
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.configdict import node_changed
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_interface_exists
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.ifconfig import Interface
from vyos.utils.dict import dict_search
from vyos.utils.network import get_interface_config
-from vyos.template import render_to_string
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -37,54 +37,21 @@ def get_config(config=None):
else:
conf = Config()
- vrf = None
- if len(argv) > 1:
- vrf = argv[1]
+ return get_frrender_dict(conf, argv)
+
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'isis'):
+ return None
- base_path = ['protocols', 'isis']
+ vrf = None
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
# eqivalent of the C foo ? 'a' : 'b' statement
- base = vrf and ['vrf', 'name', vrf, 'protocols', 'isis'] or base_path
- isis = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
-
- # Assign the name of our VRF context. This MUST be done before the return
- # statement below, else on deletion we will delete the default instance
- # instead of the VRF instance.
- if vrf: isis['vrf'] = vrf
-
- # FRR has VRF support for different routing daemons. As interfaces belong
- # to VRFs - or the global VRF, we need to check for changed interfaces so
- # that they will be properly rendered for the FRR config. Also this eases
- # removal of interfaces from the running configuration.
- interfaces_removed = node_changed(conf, base + ['interface'])
- if interfaces_removed:
- isis['interface_removed'] = list(interfaces_removed)
-
- # 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
-
- # merge in default values
- 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().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- isis = dict_merge(tmp, isis)
-
- return isis
-
-def verify(isis):
- # bail out early - looks like removal from running config
- if not isis or 'deleted' in isis:
+ isis = vrf and config_dict['vrf']['name'][vrf]['protocols']['isis'] or config_dict['isis']
+ isis['policy'] = config_dict['policy']
+
+ if 'deleted' in isis:
return None
if 'net' not in isis:
@@ -114,12 +81,11 @@ def verify(isis):
f'Recommended area lsp-mtu {recom_area_mtu} or less ' \
'(calculated on MTU size).')
- if 'vrf' in isis:
+ if vrf:
# If interface specific options are set, we must ensure that the
# interface is bound to our requesting VRF. Due to the VyOS
# priorities the interface is bound to the VRF after creation of
# the VRF itself, and before any routing protocol is configured.
- vrf = isis['vrf']
tmp = get_interface_config(interface)
if 'master' not in tmp or tmp['master'] != vrf:
raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')
@@ -266,39 +232,14 @@ def verify(isis):
return None
-def generate(isis):
- if not isis or 'deleted' in isis:
- return None
-
- isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.j2', isis)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(isis):
- isis_daemon = 'isisd'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # Generate empty helper string which can be ammended to FRR commands, it
- # will be either empty (default VRF) or contain the "vrf <name" statement
- vrf = ''
- if 'vrf' in isis:
- vrf = ' vrf ' + isis['vrf']
-
- frr_cfg.load_configuration(isis_daemon)
- frr_cfg.modify_section(f'^router isis VyOS{vrf}', stop_pattern='^exit', remove_stop_mark=True)
-
- for key in ['interface', 'interface_removed']:
- if key not in isis:
- continue
- for interface in isis[key]:
- 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'])
-
- frr_cfg.commit_configuration(isis_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index ad164db9f..33d9a6dae 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2022 VyOS maintainers and contributors
+# Copyright (C) 2020-2024 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,33 +20,32 @@ from sys import exit
from glob import glob
from vyos.config import Config
-from vyos.template import render_to_string
+from vyos.configverify import has_frr_protocol_in_dict
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.utils.dict import dict_search
from vyos.utils.file import read_file
+from vyos.utils.process import is_systemd_service_running
from vyos.utils.system import sysctl_write
from vyos.configverify import verify_interface_exists
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
-config_file = r'/tmp/ldpd.frr'
-
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['protocols', 'mpls']
- mpls = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- return mpls
+ return get_frrender_dict(conf)
-def verify(mpls):
- # If no config, then just bail out early.
- if not mpls:
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'mpls'):
return None
+ mpls = config_dict['mpls']
+
if 'interface' in mpls:
for interface in mpls['interface']:
verify_interface_exists(mpls, interface)
@@ -68,26 +67,19 @@ def verify(mpls):
return None
-def generate(mpls):
- # If there's no MPLS config generated, create dictionary key with no value.
- if not mpls or 'deleted' in mpls:
- return None
-
- mpls['frr_ldpd_config'] = render_to_string('frr/ldpd.frr.j2', mpls)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(mpls):
- ldpd_damon = 'ldpd'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
- frr_cfg.load_configuration(ldpd_damon)
- frr_cfg.modify_section(f'^mpls ldp', stop_pattern='^exit', remove_stop_mark=True)
+ if not has_frr_protocol_in_dict(config_dict, 'mpls'):
+ return None
- if 'frr_ldpd_config' in mpls:
- frr_cfg.add_before(frr.default_add_before, mpls['frr_ldpd_config'])
- frr_cfg.commit_configuration(ldpd_damon)
+ mpls = config_dict['mpls']
# Set number of entries in the platform label tables
labels = '0'
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index 0bd68b7d8..ac92c9d99 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-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,95 +14,112 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
+from sys import exit
+from sys import argv
+import ipaddress
from vyos.config import Config
-from vyos.configdict import node_changed
from vyos.template import render
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.utils.process import run
+from vyos.utils.dict import dict_search
from vyos import ConfigError
from vyos import airbag
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
+from vyos.utils.process import is_systemd_service_running
+
airbag.enable()
-opennhrp_conf = '/run/opennhrp/opennhrp.conf'
+nflog_redirect = 1
+nflog_multicast = 2
nhrp_nftables_conf = '/run/nftables_nhrp.conf'
+
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['protocols', 'nhrp']
-
- nhrp = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- nhrp['del_tunnels'] = node_changed(conf, base + ['tunnel'])
-
- if not conf.exists(base):
- return nhrp
- nhrp['if_tunnel'] = conf.get_config_dict(['interfaces', 'tunnel'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
+ return get_frrender_dict(conf, argv)
- nhrp['profile_map'] = {}
- profile = conf.get_config_dict(['vpn', 'ipsec', 'profile'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
- for name, profile_conf in profile.items():
- if 'bind' in profile_conf and 'tunnel' in profile_conf['bind']:
- interfaces = profile_conf['bind']['tunnel']
- if isinstance(interfaces, str):
- interfaces = [interfaces]
- for interface in interfaces:
- nhrp['profile_map'][interface] = name
-
- return nhrp
-
-def verify(nhrp):
- if 'tunnel' in nhrp:
- for name, nhrp_conf in nhrp['tunnel'].items():
- if not nhrp['if_tunnel'] or name not in nhrp['if_tunnel']:
+def verify(config_dict):
+ if not config_dict or 'deleted' in config_dict:
+ return None
+ if 'tunnel' in config_dict:
+ for name, nhrp_conf in config_dict['tunnel'].items():
+ if not config_dict['if_tunnel'] or name not in config_dict['if_tunnel']:
raise ConfigError(f'Tunnel interface "{name}" does not exist')
- tunnel_conf = nhrp['if_tunnel'][name]
+ tunnel_conf = config_dict['if_tunnel'][name]
+ if 'address' in tunnel_conf:
+ address_list = dict_search('address', tunnel_conf)
+ for tunip in address_list:
+ if ipaddress.ip_network(tunip,
+ strict=False).prefixlen != 32:
+ raise ConfigError(
+ f'Tunnel {name} is used for NHRP, Netmask should be /32!')
if 'encapsulation' not in tunnel_conf or tunnel_conf['encapsulation'] != 'gre':
raise ConfigError(f'Tunnel "{name}" is not an mGRE tunnel')
+ if 'network_id' not in nhrp_conf:
+ raise ConfigError(f'network-id is not specified in tunnel "{name}"')
+
if 'remote' in tunnel_conf:
raise ConfigError(f'Tunnel "{name}" cannot have a remote address defined')
- if 'map' in nhrp_conf:
- for map_name, map_conf in nhrp_conf['map'].items():
- if 'nbma_address' not in map_conf:
+ map_tunnelip = dict_search('map.tunnel_ip', nhrp_conf)
+ if map_tunnelip:
+ for map_name, map_conf in map_tunnelip.items():
+ if 'nbma' not in map_conf:
raise ConfigError(f'nbma-address missing on map {map_name} on tunnel {name}')
- if 'dynamic_map' in nhrp_conf:
- for map_name, map_conf in nhrp_conf['dynamic_map'].items():
- if 'nbma_domain_name' not in map_conf:
- raise ConfigError(f'nbma-domain-name missing on dynamic-map {map_name} on tunnel {name}')
+ nhs_tunnelip = dict_search('nhs.tunnel_ip', nhrp_conf)
+ nbma_list = []
+ if nhs_tunnelip:
+ for nhs_name, nhs_conf in nhs_tunnelip.items():
+ if 'nbma' not in nhs_conf:
+ raise ConfigError(f'nbma-address missing on map nhs {nhs_name} on tunnel {name}')
+ if nhs_name != 'dynamic':
+ if len(list(dict_search('nbma', nhs_conf))) > 1:
+ raise ConfigError(
+ f'Static nhs tunnel-ip {nhs_name} cannot contain multiple nbma-addresses')
+ for nbma_ip in dict_search('nbma', nhs_conf):
+ if nbma_ip not in nbma_list:
+ nbma_list.append(nbma_ip)
+ else:
+ raise ConfigError(
+ f'Nbma address {nbma_ip} cannot be maped to several tunnel-ip')
return None
-def generate(nhrp):
- if not os.path.exists(nhrp_nftables_conf):
- nhrp['first_install'] = True
- render(opennhrp_conf, 'nhrp/opennhrp.conf.j2', nhrp)
- render(nhrp_nftables_conf, 'nhrp/nftables.conf.j2', nhrp)
+def generate(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'nhrp'):
+ return None
+
+ if 'deleted' in config_dict['nhrp']:
+ return None
+ render(nhrp_nftables_conf, 'frr/nhrpd_nftables.conf.j2', config_dict['nhrp'])
+
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(nhrp):
+
+def apply(config_dict):
+
nft_rc = run(f'nft --file {nhrp_nftables_conf}')
if nft_rc != 0:
raise ConfigError('Failed to apply NHRP tunnel firewall rules')
- action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
- service_rc = run(f'systemctl {action} opennhrp.service')
- if service_rc != 0:
- raise ConfigError(f'Failed to {action} the NHRP service')
-
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
+
if __name__ == '__main__':
try:
c = get_config()
@@ -112,3 +129,4 @@ if __name__ == '__main__':
except ConfigError as e:
print(e)
exit(1)
+
diff --git a/src/conf_mode/protocols_openfabric.py b/src/conf_mode/protocols_openfabric.py
index 8e8c50c06..7df11fb20 100644
--- a/src/conf_mode/protocols_openfabric.py
+++ b/src/conf_mode/protocols_openfabric.py
@@ -18,13 +18,13 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import node_changed
from vyos.configverify import verify_interface_exists
-from vyos.template import render_to_string
+from vyos.configverify import has_frr_protocol_in_dict
+from vyos.utils.process import is_systemd_service_running
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
-
airbag.enable()
def get_config(config=None):
@@ -33,32 +33,14 @@ def get_config(config=None):
else:
conf = Config()
- base_path = ['protocols', 'openfabric']
-
- openfabric = conf.get_config_dict(base_path, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
-
- # Remove per domain MPLS configuration - get a list of all changed Openfabric domains
- # (removed and added) so that they will be properly rendered for the FRR config.
- openfabric['domains_all'] = list(conf.list_nodes(' '.join(base_path) + f' domain') +
- node_changed(conf, base_path + ['domain']))
-
- # Get a list of all interfaces
- openfabric['interfaces_all'] = []
- for domain in openfabric['domains_all']:
- interfaces_modified = list(node_changed(conf, base_path + ['domain', domain, 'interface']) +
- conf.list_nodes(' '.join(base_path) + f' domain {domain} interface'))
- openfabric['interfaces_all'].extend(interfaces_modified)
+ return get_frrender_dict(conf)
- if not conf.exists(base_path):
- openfabric.update({'deleted': ''})
-
- return openfabric
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'openfabric'):
+ return None
-def verify(openfabric):
- # bail out early - looks like removal from running config
- if not openfabric or 'deleted' in openfabric:
+ openfabric = config_dict['openfabric']
+ if 'deleted' in openfabric:
return None
if 'net' not in openfabric:
@@ -107,31 +89,14 @@ def verify(openfabric):
return None
-def generate(openfabric):
- if not openfabric or 'deleted' in openfabric:
- return None
-
- openfabric['frr_fabricd_config'] = render_to_string('frr/fabricd.frr.j2', openfabric)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(openfabric):
- openfabric_daemon = 'fabricd'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- frr_cfg.load_configuration(openfabric_daemon)
- for domain in openfabric['domains_all']:
- frr_cfg.modify_section(f'^router openfabric {domain}', stop_pattern='^exit', remove_stop_mark=True)
-
- for interface in openfabric['interfaces_all']:
- frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
-
- if 'frr_fabricd_config' in openfabric:
- frr_cfg.add_before(frr.default_add_before, openfabric['frr_fabricd_config'])
-
- frr_cfg.commit_configuration(openfabric_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py
index 7347c4faa..c06c0aafc 100755
--- a/src/conf_mode/protocols_ospf.py
+++ b/src/conf_mode/protocols_ospf.py
@@ -18,18 +18,17 @@ 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
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.configverify import has_frr_protocol_in_dict
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.utils.dict import dict_search
from vyos.utils.network import get_interface_config
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -39,85 +38,19 @@ def get_config(config=None):
else:
conf = Config()
- vrf = None
- if len(argv) > 1:
- vrf = argv[1]
-
- base_path = ['protocols', 'ospf']
-
- # eqivalent of the C foo ? 'a' : 'b' statement
- base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospf'] or base_path
- ospf = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True)
-
- # Assign the name of our VRF context. This MUST be done before the return
- # statement below, else on deletion we will delete the default instance
- # instead of the VRF instance.
- if vrf: ospf['vrf'] = vrf
-
- # FRR has VRF support for different routing daemons. As interfaces belong
- # to VRFs - or the global VRF, we need to check for changed interfaces so
- # that they will be properly rendered for the FRR config. Also this eases
- # removal of interfaces from the running configuration.
- interfaces_removed = node_changed(conf, base + ['interface'])
- if interfaces_removed:
- ospf['interface_removed'] = list(interfaces_removed)
-
- # 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
+ return get_frrender_dict(conf, argv)
- # 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 = 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
- # originate comes with a default metric-type of 2, which will enable the
- # entire default-information originate tree, even when not set via CLI so we
- # need to check this first and probably drop that key.
- if dict_search('default_information.originate', ospf) is None:
- del default_values['default_information']
- if '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']:
- if dict_search(f'redistribute.{protocol}', ospf) is None:
- del default_values['redistribute'][protocol]
- if not bool(default_values['redistribute']):
- del default_values['redistribute']
-
- 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']
-
- 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().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- ospf = dict_merge(tmp, ospf)
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'ospf'):
+ return None
- return ospf
+ vrf = None
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
-def verify(ospf):
- if not ospf:
- return None
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ ospf = vrf and config_dict['vrf']['name'][vrf]['protocols']['ospf'] or config_dict['ospf']
+ ospf['policy'] = config_dict['policy']
verify_common_route_maps(ospf)
@@ -164,8 +97,7 @@ def verify(ospf):
# interface is bound to our requesting VRF. Due to the VyOS
# priorities the interface is bound to the VRF after creation of
# the VRF itself, and before any routing protocol is configured.
- if 'vrf' in ospf:
- vrf = ospf['vrf']
+ if vrf:
tmp = get_interface_config(interface)
if 'master' not in tmp or tmp['master'] != vrf:
raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')
@@ -244,39 +176,14 @@ def verify(ospf):
return None
-def generate(ospf):
- if not ospf or 'deleted' in ospf:
- return None
-
- ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(ospf):
- ospf_daemon = 'ospfd'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # Generate empty helper string which can be ammended to FRR commands, it
- # will be either empty (default VRF) or contain the "vrf <name" statement
- vrf = ''
- if 'vrf' in ospf:
- vrf = ' vrf ' + ospf['vrf']
-
- frr_cfg.load_configuration(ospf_daemon)
- frr_cfg.modify_section(f'^router ospf{vrf}', stop_pattern='^exit', remove_stop_mark=True)
-
- for key in ['interface', 'interface_removed']:
- if key not in ospf:
- continue
- for interface in ospf[key]:
- 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'])
-
- frr_cfg.commit_configuration(ospf_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py
index 60c2a9b16..2563eb7d5 100755
--- a/src/conf_mode/protocols_ospfv3.py
+++ b/src/conf_mode/protocols_ospfv3.py
@@ -18,18 +18,17 @@ 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
from vyos.configverify import verify_route_map
from vyos.configverify import verify_interface_exists
-from vyos.template import render_to_string
+from vyos.configverify import has_frr_protocol_in_dict
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.ifconfig import Interface
from vyos.utils.dict import dict_search
from vyos.utils.network import get_interface_config
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -39,75 +38,19 @@ def get_config(config=None):
else:
conf = Config()
- vrf = None
- if len(argv) > 1:
- vrf = argv[1]
+ return get_frrender_dict(conf, argv)
+
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'ospfv3'):
+ return None
- base_path = ['protocols', 'ospfv3']
+ vrf = None
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
# eqivalent of the C foo ? 'a' : 'b' statement
- base = vrf and ['vrf', 'name', vrf, 'protocols', 'ospfv3'] or base_path
- ospfv3 = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-
- # Assign the name of our VRF context. This MUST be done before the return
- # statement below, else on deletion we will delete the default instance
- # instead of the VRF instance.
- if vrf: ospfv3['vrf'] = vrf
-
- # FRR has VRF support for different routing daemons. As interfaces belong
- # to VRFs - or the global VRF, we need to check for changed interfaces so
- # that they will be properly rendered for the FRR config. Also this eases
- # removal of interfaces from the running configuration.
- interfaces_removed = node_changed(conf, base + ['interface'])
- if interfaces_removed:
- ospfv3['interface_removed'] = list(interfaces_removed)
-
- # 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.
- 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
- # originate comes with a default metric-type of 2, which will enable the
- # entire default-information originate tree, even when not set via CLI so we
- # need to check this first and probably drop that key.
- if dict_search('default_information.originate', ospfv3) is None:
- del default_values['default_information']
- if 'graceful_restart' not in ospfv3:
- del default_values['graceful_restart']
-
- for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'ripng', 'static']:
- if dict_search(f'redistribute.{protocol}', ospfv3) is None:
- del default_values['redistribute'][protocol]
- if not bool(default_values['redistribute']):
- del default_values['redistribute']
-
- default_values.pop('interface', {})
-
- # merge in remaining default values
- 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().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- ospfv3 = dict_merge(tmp, ospfv3)
-
- return ospfv3
-
-def verify(ospfv3):
- if not ospfv3:
- return None
+ ospfv3 = vrf and config_dict['vrf']['name'][vrf]['protocols']['ospfv3'] or config_dict['ospfv3']
+ ospfv3['policy'] = config_dict['policy']
verify_common_route_maps(ospfv3)
@@ -137,47 +80,21 @@ def verify(ospfv3):
# interface is bound to our requesting VRF. Due to the VyOS
# priorities the interface is bound to the VRF after creation of
# the VRF itself, and before any routing protocol is configured.
- if 'vrf' in ospfv3:
- vrf = ospfv3['vrf']
+ if vrf:
tmp = get_interface_config(interface)
if 'master' not in tmp or tmp['master'] != vrf:
raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!')
return None
-def generate(ospfv3):
- if not ospfv3 or 'deleted' in ospfv3:
- return None
-
- ospfv3['new_frr_config'] = render_to_string('frr/ospf6d.frr.j2', ospfv3)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(ospfv3):
- ospf6_daemon = 'ospf6d'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # Generate empty helper string which can be ammended to FRR commands, it
- # will be either empty (default VRF) or contain the "vrf <name" statement
- vrf = ''
- if 'vrf' in ospfv3:
- vrf = ' vrf ' + ospfv3['vrf']
-
- frr_cfg.load_configuration(ospf6_daemon)
- frr_cfg.modify_section(f'^router ospf6{vrf}', stop_pattern='^exit', remove_stop_mark=True)
-
- for key in ['interface', 'interface_removed']:
- if key not in ospfv3:
- continue
- for interface in ospfv3[key]:
- 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'])
-
- frr_cfg.commit_configuration(ospf6_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
index 79294a1f0..632099964 100755
--- a/src/conf_mode/protocols_pim.py
+++ b/src/conf_mode/protocols_pim.py
@@ -22,72 +22,33 @@ from signal import SIGTERM
from sys import exit
from vyos.config import Config
-from vyos.config import config_dict_merge
-from vyos.configdict import node_changed
from vyos.configverify import verify_interface_exists
+from vyos.configverify import has_frr_protocol_in_dict
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
+from vyos.frrender import pim_daemon
+from vyos.utils.process import is_systemd_service_running
from vyos.utils.process import process_named_running
from vyos.utils.process import call
-from vyos.template import render_to_string
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
-RESERVED_MC_NET = '224.0.0.0/24'
-
-
def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['protocols', 'pim']
-
- pim = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
- # We can not run both IGMP proxy and PIM at the same time - get IGMP
- # proxy status
- if conf.exists(['protocols', 'igmp-proxy']):
- pim.update({'igmp_proxy_enabled' : {}})
-
- # FRR has VRF support for different routing daemons. As interfaces belong
- # to VRFs - or the global VRF, we need to check for changed interfaces so
- # that they will be properly rendered for the FRR config. Also this eases
- # removal of interfaces from the running configuration.
- interfaces_removed = node_changed(conf, base + ['interface'])
- if interfaces_removed:
- pim['interface_removed'] = list(interfaces_removed)
-
- # 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):
- pim.update({'deleted' : ''})
- return pim
-
- # 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 = conf.get_config_defaults(**pim.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
- # originate comes with a default metric-type of 2, which will enable the
- # entire default-information originate tree, even when not set via CLI so we
- # need to check this first and probably drop that key.
- for interface in pim.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 'igmp' not in pim['interface'][interface]:
- del default_values['interface'][interface]['igmp']
-
- pim = config_dict_merge(default_values, pim)
- return pim
-
-def verify(pim):
- if not pim or 'deleted' in pim:
+ return get_frrender_dict(conf)
+
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'pim'):
+ return None
+
+ pim = config_dict['pim']
+
+ if 'deleted' in pim:
return None
if 'igmp_proxy_enabled' in pim:
@@ -96,6 +57,7 @@ def verify(pim):
if 'interface' not in pim:
raise ConfigError('PIM require defined interfaces!')
+ RESERVED_MC_NET = '224.0.0.0/24'
for interface, interface_config in pim['interface'].items():
verify_interface_exists(pim, interface)
@@ -124,41 +86,26 @@ def verify(pim):
raise ConfigError(f'{pim_base_error} must be unique!')
unique.append(gr_addr)
-def generate(pim):
- if not pim or 'deleted' in pim:
- return None
- pim['frr_pimd_config'] = render_to_string('frr/pimd.frr.j2', pim)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(pim):
- pim_daemon = 'pimd'
- pim_pid = process_named_running(pim_daemon)
-
- if not pim or 'deleted' in pim:
- if 'deleted' in pim:
- os.kill(int(pim_pid), SIGTERM)
+def apply(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'pim'):
+ return None
+ pim_pid = process_named_running(pim_daemon)
+ pim = config_dict['pim']
+ if 'deleted' in pim:
+ os.kill(int(pim_pid), SIGTERM)
return None
if not pim_pid:
call('/usr/lib/frr/pimd -d -F traditional --daemon -A 127.0.0.1')
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- frr_cfg.load_configuration(pim_daemon)
- frr_cfg.modify_section(f'^ip pim')
- frr_cfg.modify_section(f'^ip igmp')
-
- for key in ['interface', 'interface_removed']:
- if key not in pim:
- continue
- for interface in pim[key]:
- frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
-
- if 'frr_pimd_config' in pim:
- frr_cfg.add_before(frr.default_add_before, pim['frr_pimd_config'])
- frr_cfg.commit_configuration(pim_daemon)
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_pim6.py b/src/conf_mode/protocols_pim6.py
index 581ffe238..03a79139a 100755
--- a/src/conf_mode/protocols_pim6.py
+++ b/src/conf_mode/protocols_pim6.py
@@ -19,12 +19,12 @@ from ipaddress import IPv6Network
from sys import exit
from vyos.config import Config
-from vyos.config import config_dict_merge
-from vyos.configdict import node_changed
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.configverify import verify_interface_exists
-from vyos.template import render_to_string
+from vyos.utils.process import is_systemd_service_running
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -33,34 +33,15 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'pim6']
- pim6 = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, with_recursive_defaults=True)
+ return get_frrender_dict(conf)
- # FRR has VRF support for different routing daemons. As interfaces belong
- # to VRFs - or the global VRF, we need to check for changed interfaces so
- # that they will be properly rendered for the FRR config. Also this eases
- # removal of interfaces from the running configuration.
- interfaces_removed = node_changed(conf, base + ['interface'])
- if interfaces_removed:
- pim6['interface_removed'] = list(interfaces_removed)
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'pim6'):
+ return None
- # 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):
- pim6.update({'deleted' : ''})
- return pim6
-
- # 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 = conf.get_config_defaults(**pim6.kwargs, recursive=True)
-
- pim6 = config_dict_merge(default_values, pim6)
- return pim6
-
-def verify(pim6):
- if not pim6 or 'deleted' in pim6:
- return
+ pim6 = config_dict['pim6']
+ if 'deleted' in pim6:
+ return None
for interface, interface_config in pim6.get('interface', {}).items():
verify_interface_exists(pim6, interface)
@@ -94,32 +75,14 @@ def verify(pim6):
raise ConfigError(f'{pim_base_error} must be unique!')
unique.append(gr_addr)
-def generate(pim6):
- if not pim6 or 'deleted' in pim6:
- return
- pim6['new_frr_config'] = render_to_string('frr/pim6d.frr.j2', pim6)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(pim6):
- if pim6 is None:
- return
-
- pim6_daemon = 'pim6d'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- frr_cfg.load_configuration(pim6_daemon)
-
- for key in ['interface', 'interface_removed']:
- if key not in pim6:
- continue
- for interface in pim6[key]:
- frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
-
- if 'new_frr_config' in pim6:
- frr_cfg.add_before(frr.default_add_before, pim6['new_frr_config'])
- frr_cfg.commit_configuration(pim6_daemon)
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py
index 9afac544d..ec9dfbb8b 100755
--- a/src/conf_mode/protocols_rip.py
+++ b/src/conf_mode/protocols_rip.py
@@ -17,15 +17,15 @@
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.configdict import node_changed
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_access_list
from vyos.configverify import verify_prefix_list
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.utils.dict import dict_search
-from vyos.template import render_to_string
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -34,41 +34,16 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'rip']
- rip = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- # FRR has VRF support for different routing daemons. As interfaces belong
- # to VRFs - or the global VRF, we need to check for changed interfaces so
- # that they will be properly rendered for the FRR config. Also this eases
- # removal of interfaces from the running configuration.
- interfaces_removed = node_changed(conf, base + ['interface'])
- if interfaces_removed:
- rip['interface_removed'] = list(interfaces_removed)
+ return get_frrender_dict(conf)
- # Bail out early if configuration tree does not exist
- if not conf.exists(base):
- rip.update({'deleted' : ''})
- return rip
-
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- 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().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- rip = dict_merge(tmp, rip)
-
- return rip
-
-def verify(rip):
- if not rip:
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'rip'):
return None
+ rip = config_dict['rip']
+ rip['policy'] = config_dict['policy']
+
verify_common_route_maps(rip)
acl_in = dict_search('distribute_list.access_list.in', rip)
@@ -93,39 +68,14 @@ def verify(rip):
raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \
f'with "split-horizon disable" for "{interface}"!')
-def generate(rip):
- if not rip or 'deleted' in rip:
- return None
-
- rip['new_frr_config'] = render_to_string('frr/ripd.frr.j2', rip)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(rip):
- rip_daemon = 'ripd'
- 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('^ip protocol rip route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
- frr_cfg.commit_configuration(zebra_daemon)
-
- frr_cfg.load_configuration(rip_daemon)
- frr_cfg.modify_section('^key chain \S+', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.modify_section('^router rip', stop_pattern='^exit', remove_stop_mark=True)
-
- for key in ['interface', 'interface_removed']:
- if key not in rip:
- continue
- for interface in rip[key]:
- frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True)
-
- if 'new_frr_config' in rip:
- frr_cfg.add_before(frr.default_add_before, rip['new_frr_config'])
- frr_cfg.commit_configuration(rip_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py
index 23416ff96..9a9ac8ec8 100755
--- a/src/conf_mode/protocols_ripng.py
+++ b/src/conf_mode/protocols_ripng.py
@@ -17,14 +17,15 @@
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_access_list
from vyos.configverify import verify_prefix_list
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.utils.dict import dict_search
-from vyos.template import render_to_string
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -33,32 +34,16 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'ripng']
- ripng = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
- # Bail out early if configuration tree does not exist
- if not conf.exists(base):
- return ripng
+ return get_frrender_dict(conf)
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- 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().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- ripng = dict_merge(tmp, ripng)
-
- return ripng
-
-def verify(ripng):
- if not ripng:
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'ripng'):
return None
+ ripng = config_dict['ripng']
+ ripng['policy'] = config_dict['policy']
+
verify_common_route_maps(ripng)
acl_in = dict_search('distribute_list.access_list.in', ripng)
@@ -83,34 +68,14 @@ def verify(ripng):
raise ConfigError(f'You can not have "split-horizon poison-reverse" enabled ' \
f'with "split-horizon disable" for "{interface}"!')
-def generate(ripng):
- if not ripng:
- ripng['new_frr_config'] = ''
- return None
-
- ripng['new_frr_config'] = render_to_string('frr/ripngd.frr.j2', ripng)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(ripng):
- ripng_daemon = 'ripngd'
- 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('^ipv6 protocol ripng route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)')
- frr_cfg.commit_configuration(zebra_daemon)
-
- frr_cfg.load_configuration(ripng_daemon)
- frr_cfg.modify_section('key chain \S+', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.modify_section('interface \S+', stop_pattern='^exit', remove_stop_mark=True)
- frr_cfg.modify_section('^router ripng', stop_pattern='^exit', remove_stop_mark=True)
- if 'new_frr_config' in ripng:
- frr_cfg.add_before(frr.default_add_before, ripng['new_frr_config'])
- frr_cfg.commit_configuration(ripng_daemon)
-
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py
index a59ecf3e4..ef0250e3d 100755
--- a/src/conf_mode/protocols_rpki.py
+++ b/src/conf_mode/protocols_rpki.py
@@ -20,13 +20,15 @@ from glob import glob
from sys import exit
from vyos.config import Config
+from vyos.configverify import has_frr_protocol_in_dict
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.pki import wrap_openssh_public_key
from vyos.pki import wrap_openssh_private_key
-from vyos.template import render_to_string
from vyos.utils.dict import dict_search_args
from vyos.utils.file import write_file
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -37,25 +39,14 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['protocols', 'rpki']
+ return get_frrender_dict(conf)
- rpki = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True, with_pki=True)
- # Bail out early if configuration tree does not exist
- if not conf.exists(base):
- rpki.update({'deleted' : ''})
- return rpki
-
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- rpki = conf.merge_defaults(rpki, recursive=True)
-
- return rpki
-
-def verify(rpki):
- if not rpki:
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'rpki'):
return None
+ rpki = config_dict['rpki']
+
if 'cache' in rpki:
preferences = []
for peer, peer_config in rpki['cache'].items():
@@ -81,12 +72,14 @@ def verify(rpki):
return None
-def generate(rpki):
+def generate(config_dict):
for key in glob(f'{rpki_ssh_key_base}*'):
os.unlink(key)
- if not rpki:
- return
+ if not has_frr_protocol_in_dict(config_dict, 'rpki'):
+ return None
+
+ rpki = config_dict['rpki']
if 'cache' in rpki:
for cache, cache_config in rpki['cache'].items():
@@ -102,21 +95,13 @@ def generate(rpki):
write_file(cache_config['ssh']['public_key_file'], wrap_openssh_public_key(public_key_data, public_key_type))
write_file(cache_config['ssh']['private_key_file'], wrap_openssh_private_key(private_key_data))
- rpki['new_frr_config'] = render_to_string('frr/rpki.frr.j2', rpki)
-
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(rpki):
- bgp_daemon = 'bgpd'
-
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(bgp_daemon)
- frr_cfg.modify_section('^rpki', stop_pattern='^exit', remove_stop_mark=True)
- if 'new_frr_config' in rpki:
- frr_cfg.add_before(frr.default_add_before, rpki['new_frr_config'])
-
- frr_cfg.commit_configuration(bgp_daemon)
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_segment-routing.py b/src/conf_mode/protocols_segment-routing.py
index b36c2ca11..f2bd42a79 100755
--- a/src/conf_mode/protocols_segment-routing.py
+++ b/src/conf_mode/protocols_segment-routing.py
@@ -17,12 +17,15 @@
from sys import exit
from vyos.config import Config
-from vyos.configdict import node_changed
-from vyos.template import render_to_string
+from vyos.configdict import list_diff
+from vyos.configverify import has_frr_protocol_in_dict
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
+from vyos.ifconfig import Section
from vyos.utils.dict import dict_search
+from vyos.utils.process import is_systemd_service_running
from vyos.utils.system import sysctl_write
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -32,25 +35,14 @@ def get_config(config=None):
else:
conf = Config()
- base = ['protocols', 'segment-routing']
- sr = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True,
- with_recursive_defaults=True)
+ return get_frrender_dict(conf)
- # FRR has VRF support for different routing daemons. As interfaces belong
- # to VRFs - or the global VRF, we need to check for changed interfaces so
- # that they will be properly rendered for the FRR config. Also this eases
- # removal of interfaces from the running configuration.
- interfaces_removed = node_changed(conf, base + ['interface'])
- if interfaces_removed:
- sr['interface_removed'] = list(interfaces_removed)
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'segment_routing'):
+ return None
- import pprint
- pprint.pprint(sr)
- return sr
+ sr = config_dict['segment_routing']
-def verify(sr):
if 'srv6' in sr:
srv6_enable = False
if 'interface' in sr:
@@ -62,47 +54,43 @@ def verify(sr):
raise ConfigError('SRv6 should be enabled on at least one interface!')
return None
-def generate(sr):
- if not sr:
- return None
-
- sr['new_frr_config'] = render_to_string('frr/zebra.segment_routing.frr.j2', sr)
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
return None
-def apply(sr):
- zebra_daemon = 'zebra'
+def apply(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'segment_routing'):
+ return None
- if 'interface_removed' in sr:
- for interface in sr['interface_removed']:
- # Disable processing of IPv6-SR packets
- sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0')
+ sr = config_dict['segment_routing']
+
+ current_interfaces = Section.interfaces()
+ sr_interfaces = list(sr.get('interface', {}).keys())
- if 'interface' in sr:
- for interface, interface_config in sr['interface'].items():
- # Accept or drop SR-enabled IPv6 packets on this interface
- if 'srv6' in interface_config:
- sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1')
- # Define HMAC policy for ingress SR-enabled packets on this interface
- # It's a redundant check as HMAC has a default value - but better safe
- # then sorry
- tmp = dict_search('srv6.hmac', interface_config)
- if tmp == 'accept':
- sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0')
- elif tmp == 'drop':
- sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1')
- elif tmp == 'ignore':
- sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1')
- else:
- sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0')
+ for interface in list_diff(current_interfaces, sr_interfaces):
+ # Disable processing of IPv6-SR packets
+ sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0')
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(r'^segment-routing')
- if 'new_frr_config' in sr:
- frr_cfg.add_before(frr.default_add_before, sr['new_frr_config'])
- frr_cfg.commit_configuration(zebra_daemon)
+ for interface, interface_config in sr.get('interface', {}).items():
+ # Accept or drop SR-enabled IPv6 packets on this interface
+ if 'srv6' in interface_config:
+ sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '1')
+ # Define HMAC policy for ingress SR-enabled packets on this interface
+ # It's a redundant check as HMAC has a default value - but better safe
+ # then sorry
+ tmp = dict_search('srv6.hmac', interface_config)
+ if tmp == 'accept':
+ sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '0')
+ elif tmp == 'drop':
+ sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '1')
+ elif tmp == 'ignore':
+ sysctl_write(f'net.ipv6.conf.{interface}.seg6_require_hmac', '-1')
+ else:
+ sysctl_write(f'net.ipv6.conf.{interface}.seg6_enabled', '0')
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py
index 430cc69d4..1b9e51167 100755
--- a/src/conf_mode/protocols_static.py
+++ b/src/conf_mode/protocols_static.py
@@ -14,19 +14,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+from ipaddress import IPv4Network
from sys import exit
from sys import argv
from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.configdict import get_dhcp_interfaces
-from vyos.configdict import get_pppoe_interfaces
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.configverify import verify_common_route_maps
from vyos.configverify import verify_vrf
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
+from vyos.utils.process import is_systemd_service_running
from vyos.template import render
-from vyos.template import render_to_string
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -38,36 +38,20 @@ def get_config(config=None):
else:
conf = Config()
+ return get_frrender_dict(conf, argv)
+
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'static'):
+ return None
+
vrf = None
- if len(argv) > 1:
- vrf = argv[1]
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
- 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, no_tag_node_value_mangle=True)
-
- # Assign the name of our VRF context
- if vrf: static['vrf'] = vrf
-
- # We also need some additional information from the config, prefix-lists
- # and route-maps for instance. They will be used in verify().
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = conf.get_config_dict(['policy'])
- # Merge policy dict into "regular" config dict
- static = dict_merge(tmp, static)
-
- # T3680 - get a list of all interfaces currently configured to use DHCP
- tmp = get_dhcp_interfaces(conf, vrf)
- if tmp: static.update({'dhcp' : tmp})
- tmp = get_pppoe_interfaces(conf, vrf)
- if tmp: static.update({'pppoe' : tmp})
-
- return static
-
-def verify(static):
+ static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static']
+ static['policy'] = config_dict['policy']
+
verify_common_route_maps(static)
for route in ['route', 'route6']:
@@ -90,35 +74,34 @@ def verify(static):
raise ConfigError(f'Can not use both blackhole and reject for '\
f'prefix "{prefix}"!')
+ if 'multicast' in static and 'route' in static['multicast']:
+ for prefix, prefix_options in static['multicast']['route'].items():
+ if not IPv4Network(prefix).is_multicast:
+ raise ConfigError(f'{prefix} is not a multicast network!')
+
return None
-def generate(static):
- if not static:
+def generate(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'static'):
return None
- # Put routing table names in /etc/iproute2/rt_tables
- render(config_file, 'iproute2/static.conf.j2', static)
- static['new_frr_config'] = render_to_string('frr/staticd.frr.j2', static)
- return None
-
-def apply(static):
- static_daemon = 'staticd'
+ vrf = None
+ if 'vrf_context' in config_dict:
+ vrf = config_dict['vrf_context']
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(static_daemon)
+ # eqivalent of the C foo ? 'a' : 'b' statement
+ static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static']
- if 'vrf' in static:
- vrf = static['vrf']
- frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit-vrf', remove_stop_mark=True)
- else:
- frr_cfg.modify_section(r'^ip route .*')
- frr_cfg.modify_section(r'^ipv6 route .*')
+ # Put routing table names in /etc/iproute2/rt_tables
+ render(config_file, 'iproute2/static.conf.j2', static)
- if 'new_frr_config' in static:
- frr_cfg.add_before(frr.default_add_before, static['new_frr_config'])
- frr_cfg.commit_configuration(static_daemon)
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
+ return None
+def apply(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
deleted file mode 100755
index c8894fd41..000000000
--- a/src/conf_mode/protocols_static_multicast.py
+++ /dev/null
@@ -1,135 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2020-2024 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/>.
-
-
-from ipaddress import IPv4Address
-from sys import exit
-
-from vyos import ConfigError
-from vyos import frr
-from vyos.config import Config
-from vyos.template import render_to_string
-
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/tmp/static_mcast.frr'
-
-# Get configuration for static multicast route
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
- mroute = {
- 'old_mroute' : {},
- 'mroute' : {}
- }
-
- base_path = "protocols static multicast"
-
- if not (conf.exists(base_path) or conf.exists_effective(base_path)):
- return None
-
- conf.set_level(base_path)
-
- # Get multicast effective routes
- for route in conf.list_effective_nodes('route'):
- mroute['old_mroute'][route] = {}
- for next_hop in conf.list_effective_nodes('route {0} next-hop'.format(route)):
- mroute['old_mroute'][route].update({
- next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop))
- })
-
- # Get multicast effective interface-routes
- for route in conf.list_effective_nodes('interface-route'):
- if not route in mroute['old_mroute']:
- mroute['old_mroute'][route] = {}
- for next_hop in conf.list_effective_nodes('interface-route {0} next-hop-interface'.format(route)):
- mroute['old_mroute'][route].update({
- next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop))
- })
-
- # Get multicast routes
- for route in conf.list_nodes('route'):
- mroute['mroute'][route] = {}
- for next_hop in conf.list_nodes('route {0} next-hop'.format(route)):
- mroute['mroute'][route].update({
- next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop))
- })
-
- # Get multicast interface-routes
- for route in conf.list_nodes('interface-route'):
- if not route in mroute['mroute']:
- mroute['mroute'][route] = {}
- for next_hop in conf.list_nodes('interface-route {0} next-hop-interface'.format(route)):
- mroute['mroute'][route].update({
- next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop))
- })
-
- return mroute
-
-def verify(mroute):
- if mroute is None:
- return None
-
- for mcast_route in mroute['mroute']:
- route = mcast_route.split('/')
- if IPv4Address(route[0]) < IPv4Address('224.0.0.0'):
- raise ConfigError(f'{mcast_route} not a multicast network')
-
-
-def generate(mroute):
- if mroute is None:
- return None
-
- mroute['new_frr_config'] = render_to_string('frr/static_mcast.frr.j2', mroute)
- return None
-
-
-def apply(mroute):
- if mroute is None:
- return None
- static_daemon = 'staticd'
-
- frr_cfg = frr.FRRConfig()
- frr_cfg.load_configuration(static_daemon)
-
- if 'old_mroute' in mroute:
- for route_gr in mroute['old_mroute']:
- for nh in mroute['old_mroute'][route_gr]:
- if mroute['old_mroute'][route_gr][nh]:
- frr_cfg.modify_section(f'^ip mroute {route_gr} {nh} {mroute["old_mroute"][route_gr][nh]}')
- else:
- frr_cfg.modify_section(f'^ip mroute {route_gr} {nh}')
-
- if 'new_frr_config' in mroute:
- frr_cfg.add_before(frr.default_add_before, mroute['new_frr_config'])
-
- frr_cfg.commit_configuration(static_daemon)
-
- return None
-
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
index b112add3f..b83c6dfb1 100755
--- a/src/conf_mode/service_console-server.py
+++ b/src/conf_mode/service_console-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -98,6 +98,12 @@ def generate(proxy):
return None
def apply(proxy):
+ if not os.path.exists('/etc/dropbear/dropbear_rsa_host_key'):
+ call('dropbearkey -t rsa -s 4096 -f /etc/dropbear/dropbear_rsa_host_key')
+
+ if not os.path.exists('/etc/dropbear/dropbear_ecdsa_host_key'):
+ call('dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key')
+
call('systemctl daemon-reload')
call('systemctl stop dropbear@*.service conserver-server.service')
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index 9c59aa63d..5a729af74 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -38,6 +38,7 @@ 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()
ctrl_config_file = '/run/kea/kea-ctrl-agent.conf'
@@ -45,13 +46,13 @@ ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
config_file = '/run/kea/kea-dhcp4.conf'
lease_file = '/config/dhcp/dhcp4-leases.csv'
lease_file_glob = '/config/dhcp/dhcp4-leases*'
-systemd_override = r'/run/systemd/system/kea-ctrl-agent.service.d/10-override.conf'
user_group = '_kea'
ca_cert_file = '/run/kea/kea-failover-ca.pem'
cert_file = '/run/kea/kea-failover.pem'
cert_key_file = '/run/kea/kea-failover-key.pem'
+
def dhcp_slice_range(exclude_list, range_dict):
"""
This function is intended to slice a DHCP range. What does it mean?
@@ -74,19 +75,17 @@ def dhcp_slice_range(exclude_list, range_dict):
range_last_exclude = ''
for e in exclude_list:
- if (ip_address(e) >= ip_address(range_start)) and \
- (ip_address(e) <= ip_address(range_stop)):
+ if (ip_address(e) >= ip_address(range_start)) and (
+ ip_address(e) <= ip_address(range_stop)
+ ):
range_last_exclude = e
for e in exclude_list:
- if (ip_address(e) >= ip_address(range_start)) and \
- (ip_address(e) <= ip_address(range_stop)):
-
+ if (ip_address(e) >= ip_address(range_start)) and (
+ ip_address(e) <= ip_address(range_stop)
+ ):
# Build new address range ending one address before exclude address
- r = {
- 'start' : range_start,
- 'stop' : str(ip_address(e) -1)
- }
+ r = {'start': range_start, 'stop': str(ip_address(e) - 1)}
if 'option' in range_dict:
r['option'] = range_dict['option']
@@ -104,10 +103,7 @@ def dhcp_slice_range(exclude_list, range_dict):
# Take care of last IP address range spanning from the last exclude
# address (+1) to the end of the initial configured range
if ip_address(e) == ip_address(range_last_exclude):
- r = {
- 'start': str(ip_address(e) + 1),
- 'stop': str(range_stop)
- }
+ r = {'start': str(ip_address(e) + 1), 'stop': str(range_stop)}
if 'option' in range_dict:
r['option'] = range_dict['option']
@@ -115,14 +111,15 @@ def dhcp_slice_range(exclude_list, range_dict):
if not (ip_address(r['start']) > ip_address(r['stop'])):
output.append(r)
else:
- # if the excluded address was not part of the range, we simply return
- # the entire ranga again
- if not range_last_exclude:
- if range_dict not in output:
- output.append(range_dict)
+ # if the excluded address was not part of the range, we simply return
+ # the entire ranga again
+ if not range_last_exclude:
+ if range_dict not in output:
+ output.append(range_dict)
return output
+
def get_config(config=None):
if config:
conf = config
@@ -132,10 +129,13 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dhcp = conf.get_config_dict(base, key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True,
- with_recursive_defaults=True)
+ 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():
@@ -147,22 +147,31 @@ def get_config(config=None):
new_range_id = 0
new_range_dict = {}
for r, r_config in subnet_config['range'].items():
- for slice in dhcp_slice_range(subnet_config['exclude'], r_config):
- new_range_dict.update({new_range_id : slice})
- new_range_id +=1
+ for slice in dhcp_slice_range(
+ subnet_config['exclude'], r_config
+ ):
+ new_range_dict.update({new_range_id: slice})
+ new_range_id += 1
dhcp['shared_network_name'][network]['subnet'][subnet].update(
- {'range' : new_range_dict})
+ {'range': new_range_dict}
+ )
if len(dhcp['high_availability']) == 1:
## only default value for mode is set, need to remove ha node
del dhcp['high_availability']
else:
if dict_search('high_availability.certificate', dhcp):
- dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)
+ dhcp['pki'] = conf.get_config_dict(
+ ['pki'],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ )
return dhcp
+
def verify(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
@@ -170,13 +179,15 @@ def verify(dhcp):
# If DHCP is enabled we need one share-network
if 'shared_network_name' not in dhcp:
- raise ConfigError('No DHCP shared networks configured.\n' \
- 'At least one DHCP shared network must be configured.')
+ raise ConfigError(
+ 'No DHCP shared networks configured.\n'
+ 'At least one DHCP shared network must be configured.'
+ )
# Inspect shared-network/subnet
listen_ok = False
subnets = []
- shared_networks = len(dhcp['shared_network_name'])
+ shared_networks = len(dhcp['shared_network_name'])
disabled_shared_networks = 0
subnet_ids = []
@@ -187,12 +198,16 @@ def verify(dhcp):
disabled_shared_networks += 1
if 'subnet' not in network_config:
- raise ConfigError(f'No subnets defined for {network}. At least one\n' \
- 'lease subnet must be configured.')
+ raise ConfigError(
+ f'No subnets defined for {network}. At least one\n'
+ 'lease subnet must be configured.'
+ )
for subnet, subnet_config in network_config['subnet'].items():
if 'subnet_id' not in subnet_config:
- raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"')
+ raise ConfigError(
+ f'Unique subnet ID not specified for subnet "{subnet}"'
+ )
if subnet_config['subnet_id'] in subnet_ids:
raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique')
@@ -203,32 +218,46 @@ def verify(dhcp):
if 'static_route' in subnet_config:
for route, route_option in subnet_config['static_route'].items():
if 'next_hop' not in route_option:
- raise ConfigError(f'DHCP static-route "{route}" requires router to be defined!')
+ raise ConfigError(
+ f'DHCP static-route "{route}" requires router to be defined!'
+ )
# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
networks = []
for range, range_config in subnet_config['range'].items():
if not {'start', 'stop'} <= set(range_config):
- raise ConfigError(f'DHCP range "{range}" start and stop address must be defined!')
+ raise ConfigError(
+ f'DHCP range "{range}" start and stop address must be defined!'
+ )
# Start/Stop address must be inside network
for key in ['start', 'stop']:
if ip_address(range_config[key]) not in ip_network(subnet):
- raise ConfigError(f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'DHCP range "{range}" {key} address not within shared-network "{network}, {subnet}"!'
+ )
# Stop address must be greater or equal to start address
- if ip_address(range_config['stop']) < ip_address(range_config['start']):
- raise ConfigError(f'DHCP range "{range}" stop address must be greater or equal\n' \
- 'to the ranges start address!')
+ if ip_address(range_config['stop']) < ip_address(
+ range_config['start']
+ ):
+ raise ConfigError(
+ f'DHCP range "{range}" stop address must be greater or equal\n'
+ 'to the ranges start address!'
+ )
for network in networks:
start = range_config['start']
stop = range_config['stop']
if start in network:
- raise ConfigError(f'Range "{range}" start address "{start}" already part of another range!')
+ raise ConfigError(
+ f'Range "{range}" start address "{start}" already part of another range!'
+ )
if stop in network:
- raise ConfigError(f'Range "{range}" stop address "{stop}" already part of another range!')
+ raise ConfigError(
+ f'Range "{range}" stop address "{stop}" already part of another range!'
+ )
tmp = IPRange(range_config['start'], range_config['stop'])
networks.append(tmp)
@@ -237,12 +266,16 @@ def verify(dhcp):
if 'exclude' in subnet_config:
for exclude in subnet_config['exclude']:
if ip_address(exclude) not in ip_network(subnet):
- raise ConfigError(f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'Excluded IP address "{exclude}" not within shared-network "{network}, {subnet}"!'
+ )
# At least one DHCP address range or static-mapping required
if 'range' not in subnet_config and 'static_mapping' not in subnet_config:
- raise ConfigError(f'No DHCP address range or active static-mapping configured\n' \
- f'within shared-network "{network}, {subnet}"!')
+ raise ConfigError(
+ f'No DHCP address range or active static-mapping configured\n'
+ f'within shared-network "{network}, {subnet}"!'
+ )
if 'static_mapping' in subnet_config:
# Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
@@ -251,29 +284,42 @@ def verify(dhcp):
used_duid = []
for mapping, mapping_config in subnet_config['static_mapping'].items():
if 'ip_address' in mapping_config:
- if ip_address(mapping_config['ip_address']) not in ip_network(subnet):
- raise ConfigError(f'Configured static lease address for mapping "{mapping}" is\n' \
- f'not within shared-network "{network}, {subnet}"!')
-
- if ('mac' not in mapping_config and 'duid' not in mapping_config) or \
- ('mac' in mapping_config and 'duid' in mapping_config):
- raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
- f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
+ if ip_address(mapping_config['ip_address']) not in ip_network(
+ subnet
+ ):
+ raise ConfigError(
+ f'Configured static lease address for mapping "{mapping}" is\n'
+ f'not within shared-network "{network}, {subnet}"!'
+ )
+
+ if (
+ 'mac' not in mapping_config and 'duid' not in mapping_config
+ ) or ('mac' in mapping_config and 'duid' in mapping_config):
+ raise ConfigError(
+ f'Either MAC address or Client identifier (DUID) is required for '
+ f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!'
+ )
if 'disable' not in mapping_config:
if mapping_config['ip_address'] in used_ips:
- raise ConfigError(f'Configured IP address for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured IP address for static mapping "{mapping}" already exists on another static mapping'
+ )
used_ips.append(mapping_config['ip_address'])
if 'disable' not in mapping_config:
if 'mac' in mapping_config:
if mapping_config['mac'] in used_mac:
- raise ConfigError(f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping'
+ )
used_mac.append(mapping_config['mac'])
if 'duid' in mapping_config:
if mapping_config['duid'] in used_duid:
- raise ConfigError(f'Configured DUID for static mapping "{mapping}" already exists on another static mapping')
+ raise ConfigError(
+ f'Configured DUID for static mapping "{mapping}" already exists on another static mapping'
+ )
used_duid.append(mapping_config['duid'])
# There must be one subnet connected to a listen interface.
@@ -284,73 +330,102 @@ def verify(dhcp):
# Subnets must be non overlapping
if subnet in subnets:
- raise ConfigError(f'Configured subnets must be unique! Subnet "{subnet}"\n'
- 'defined multiple times!')
+ raise ConfigError(
+ f'Configured subnets must be unique! Subnet "{subnet}"\n'
+ 'defined multiple times!'
+ )
subnets.append(subnet)
# Check for overlapping subnets
net = ip_network(subnet)
for n in subnets:
net2 = ip_network(n)
- if (net != net2):
+ if net != net2:
if net.overlaps(net2):
- raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!')
+ raise ConfigError(
+ f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!'
+ )
# Prevent 'disable' for shared-network if only one network is configured
if (shared_networks - disabled_shared_networks) < 1:
- raise ConfigError(f'At least one shared network must be active!')
+ raise ConfigError('At least one shared network must be active!')
if 'high_availability' in dhcp:
for key in ['name', 'remote', 'source_address', 'status']:
if key not in dhcp['high_availability']:
tmp = key.replace('_', '-')
- raise ConfigError(f'DHCP high-availability requires "{tmp}" to be specified!')
+ raise ConfigError(
+ f'DHCP high-availability requires "{tmp}" to be specified!'
+ )
if len({'certificate', 'ca_certificate'} & set(dhcp['high_availability'])) == 1:
- raise ConfigError(f'DHCP secured high-availability requires both certificate and CA certificate')
+ raise ConfigError(
+ 'DHCP secured high-availability requires both certificate and CA certificate'
+ )
if 'certificate' in dhcp['high_availability']:
cert_name = dhcp['high_availability']['certificate']
if cert_name not in dhcp['pki']['certificate']:
- raise ConfigError(f'Invalid certificate specified for DHCP high-availability')
-
- if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'):
- raise ConfigError(f'Invalid certificate specified for DHCP high-availability')
-
- if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'):
- raise ConfigError(f'Missing private key on certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid certificate specified for DHCP high-availability'
+ )
+
+ if not dict_search_args(
+ dhcp['pki']['certificate'], cert_name, 'certificate'
+ ):
+ raise ConfigError(
+ 'Invalid certificate specified for DHCP high-availability'
+ )
+
+ if not dict_search_args(
+ dhcp['pki']['certificate'], cert_name, 'private', 'key'
+ ):
+ raise ConfigError(
+ 'Missing private key on certificate specified for DHCP high-availability'
+ )
if 'ca_certificate' in dhcp['high_availability']:
ca_cert_name = dhcp['high_availability']['ca_certificate']
if ca_cert_name not in dhcp['pki']['ca']:
- raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid CA certificate specified for DHCP high-availability'
+ )
if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'):
- raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability')
+ raise ConfigError(
+ 'Invalid CA certificate specified for DHCP high-availability'
+ )
- for address in (dict_search('listen_address', dhcp) or []):
+ for address in dict_search('listen_address', dhcp) or []:
if is_addr_assigned(address, include_vrf=True):
listen_ok = True
# no need to probe further networks, we have one that is valid
continue
else:
- raise ConfigError(f'listen-address "{address}" not configured on any interface')
+ raise ConfigError(
+ f'listen-address "{address}" not configured on any interface'
+ )
if not listen_ok:
- raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
- 'broadcast interface configured, nor was there an explicit listen-address\n'
- 'configured for serving DHCP relay packets!')
+ raise ConfigError(
+ 'None of the configured subnets have an appropriate primary IP address on any\n'
+ 'broadcast interface configured, nor was there an explicit listen-address\n'
+ 'configured for serving DHCP relay packets!'
+ )
if 'listen_address' in dhcp and 'listen_interface' in dhcp:
- raise ConfigError(f'Cannot define listen-address and listen-interface at the same time')
+ raise ConfigError(
+ 'Cannot define listen-address and listen-interface at the same time'
+ )
- for interface in (dict_search('listen_interface', dhcp) or []):
+ for interface in dict_search('listen_interface', dhcp) or []:
if not interface_exists(interface):
raise ConfigError(f'listen-interface "{interface}" does not exist')
return None
+
def generate(dhcp):
# bail out early - looks like removal from running config
if not dhcp or 'disable' in dhcp:
@@ -382,8 +457,12 @@ def generate(dhcp):
cert_name = dhcp['high_availability']['certificate']
cert_data = dhcp['pki']['certificate'][cert_name]['certificate']
key_data = dhcp['pki']['certificate'][cert_name]['private']['key']
- write_file(cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600)
- write_file(cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600)
+ write_file(
+ cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600
+ )
+ write_file(
+ cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600
+ )
dhcp['high_availability']['cert_file'] = cert_file
dhcp['high_availability']['cert_key_file'] = cert_key_file
@@ -391,17 +470,33 @@ def generate(dhcp):
if 'ca_certificate' in dhcp['high_availability']:
ca_cert_name = dhcp['high_availability']['ca_certificate']
ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate']
- write_file(ca_cert_file, wrap_certificate(ca_cert_data), user=user_group, mode=0o600)
+ write_file(
+ ca_cert_file,
+ wrap_certificate(ca_cert_data),
+ user=user_group,
+ mode=0o600,
+ )
dhcp['high_availability']['ca_cert_file'] = ca_cert_file
- render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp)
-
- render(ctrl_config_file, 'dhcp-server/kea-ctrl-agent.conf.j2', dhcp, user=user_group, group=user_group)
- render(config_file, 'dhcp-server/kea-dhcp4.conf.j2', dhcp, user=user_group, group=user_group)
+ render(
+ ctrl_config_file,
+ 'dhcp-server/kea-ctrl-agent.conf.j2',
+ dhcp,
+ user=user_group,
+ group=user_group,
+ )
+ render(
+ config_file,
+ 'dhcp-server/kea-dhcp4.conf.j2',
+ dhcp,
+ user=user_group,
+ group=user_group,
+ )
return None
+
def apply(dhcp):
services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
@@ -427,6 +522,7 @@ def apply(dhcp):
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py
index e3bdbc9f8..5636d6f83 100755
--- a/src/conf_mode/service_dns_forwarding.py
+++ b/src/conf_mode/service_dns_forwarding.py
@@ -366,6 +366,13 @@ def apply(dns):
hc.add_name_server_tags_recursor(['dhcp-' + interface,
'dhcpv6-' + interface ])
+ # add dhcp interfaces
+ if 'dhcp' in dns:
+ for interface in dns['dhcp']:
+ if interface_exists(interface):
+ hc.add_name_server_tags_recursor(['dhcp-' + interface,
+ 'dhcpv6-' + interface ])
+
# hostsd will generate the forward-zones file
# the list and keys() are required as get returns a dict, not list
hc.delete_forward_zones(list(hc.get_forward_zones().keys()))
diff --git a/src/conf_mode/service_monitoring_frr-exporter.py b/src/conf_mode/service_monitoring_network_event.py
index 01527d579..104e6ce23 100755..100644
--- a/src/conf_mode/service_monitoring_frr-exporter.py
+++ b/src/conf_mode/service_monitoring_network_event.py
@@ -15,22 +15,18 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import json
from sys import exit
from vyos.config import Config
-from vyos.configdict import is_node_changed
-from vyos.configverify import verify_vrf
-from vyos.template import render
+from vyos.utils.file import write_file
from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
-
-
airbag.enable()
-service_file = '/etc/systemd/system/frr_exporter.service'
-systemd_service = 'frr_exporter.service'
+vyos_network_event_logger_config = r'/run/vyos-network-event-logger.conf'
def get_config(config=None):
@@ -38,56 +34,52 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['service', 'monitoring', 'frr-exporter']
+ base = ['service', 'monitoring', 'network-event']
if not conf.exists(base):
return None
- config_data = conf.get_config_dict(
- base, key_mangling=('-', '_'), get_first_key=True
- )
- config_data = conf.merge_defaults(config_data, recursive=True)
+ monitoring = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)
- tmp = is_node_changed(conf, base + ['vrf'])
- if tmp:
- config_data.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.
+ monitoring = conf.merge_defaults(monitoring, recursive=True)
- return config_data
+ return monitoring
-def verify(config_data):
- # bail out early - looks like removal from running config
- if not config_data:
+def verify(monitoring):
+ if not monitoring:
return None
- verify_vrf(config_data)
return None
-def generate(config_data):
- if not config_data:
- # Delete systemd files
- if os.path.isfile(service_file):
- os.unlink(service_file)
+def generate(monitoring):
+ if not monitoring:
+ # Delete config
+ if os.path.exists(vyos_network_event_logger_config):
+ os.unlink(vyos_network_event_logger_config)
+
return None
- # Render frr_exporter service_file
- render(service_file, 'frr_exporter/frr_exporter.service.j2', config_data)
+ # Create config
+ log_conf_json = json.dumps(monitoring, indent=4)
+ write_file(vyos_network_event_logger_config, log_conf_json)
+
return None
-def apply(config_data):
+def apply(monitoring):
# Reload systemd manager configuration
- call('systemctl daemon-reload')
- if not config_data:
+ systemd_service = 'vyos-network-event-logger.service'
+
+ if not monitoring:
call(f'systemctl stop {systemd_service}')
return
- # we need to restart the service if e.g. the VRF name changed
- systemd_action = 'reload-or-restart'
- if 'restart_required' in config_data:
- systemd_action = 'restart'
-
- call(f'systemctl {systemd_action} {systemd_service}')
+ call(f'systemctl restart {systemd_service}')
if __name__ == '__main__':
diff --git a/src/conf_mode/service_monitoring_node-exporter.py b/src/conf_mode/service_monitoring_node-exporter.py
deleted file mode 100755
index db34bb5d0..000000000
--- a/src/conf_mode/service_monitoring_node-exporter.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2024 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 is_node_changed
-from vyos.configverify import verify_vrf
-from vyos.template import render
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-
-
-airbag.enable()
-
-service_file = '/etc/systemd/system/node_exporter.service'
-systemd_service = 'node_exporter.service'
-
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
- base = ['service', 'monitoring', 'node-exporter']
- if not conf.exists(base):
- return None
-
- config_data = conf.get_config_dict(
- base, key_mangling=('-', '_'), get_first_key=True
- )
- config_data = conf.merge_defaults(config_data, recursive=True)
-
- tmp = is_node_changed(conf, base + ['vrf'])
- if tmp:
- config_data.update({'restart_required': {}})
-
- return config_data
-
-
-def verify(config_data):
- # bail out early - looks like removal from running config
- if not config_data:
- return None
-
- verify_vrf(config_data)
- return None
-
-
-def generate(config_data):
- if not config_data:
- # Delete systemd files
- if os.path.isfile(service_file):
- os.unlink(service_file)
- return None
-
- # Render node_exporter service_file
- render(service_file, 'node_exporter/node_exporter.service.j2', config_data)
- return None
-
-
-def apply(config_data):
- # Reload systemd manager configuration
- call('systemctl daemon-reload')
- if not config_data:
- call(f'systemctl stop {systemd_service}')
- return
-
- # we need to restart the service if e.g. the VRF name changed
- systemd_action = 'reload-or-restart'
- if 'restart_required' in config_data:
- systemd_action = 'restart'
-
- call(f'systemctl {systemd_action} {systemd_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_monitoring_prometheus.py b/src/conf_mode/service_monitoring_prometheus.py
new file mode 100755
index 000000000..9a07d8593
--- /dev/null
+++ b/src/conf_mode/service_monitoring_prometheus.py
@@ -0,0 +1,206 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 is_node_changed
+from vyos.configverify import verify_vrf
+from vyos.template import render
+from vyos.utils.process import call
+from vyos import ConfigError
+from vyos import airbag
+
+airbag.enable()
+
+node_exporter_service_file = '/etc/systemd/system/node_exporter.service'
+node_exporter_systemd_service = 'node_exporter.service'
+node_exporter_collector_path = '/run/node_exporter/collector'
+
+frr_exporter_service_file = '/etc/systemd/system/frr_exporter.service'
+frr_exporter_systemd_service = 'frr_exporter.service'
+
+blackbox_exporter_service_file = '/etc/systemd/system/blackbox_exporter.service'
+blackbox_exporter_systemd_service = 'blackbox_exporter.service'
+
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['service', 'monitoring', 'prometheus']
+ if not conf.exists(base):
+ return None
+
+ monitoring = conf.get_config_dict(
+ base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True
+ )
+
+ tmp = is_node_changed(conf, base + ['node-exporter', 'vrf'])
+ if tmp:
+ monitoring.update({'node_exporter_restart_required': {}})
+
+ tmp = is_node_changed(conf, base + ['frr-exporter', 'vrf'])
+ if tmp:
+ monitoring.update({'frr_exporter_restart_required': {}})
+
+ tmp = False
+ for node in ['vrf', 'config-file']:
+ tmp = tmp or is_node_changed(conf, base + ['blackbox-exporter', node])
+ if tmp:
+ monitoring.update({'blackbox_exporter_restart_required': {}})
+
+ return monitoring
+
+
+def verify(monitoring):
+ if not monitoring:
+ return None
+
+ if 'node_exporter' in monitoring:
+ verify_vrf(monitoring['node_exporter'])
+
+ if 'frr_exporter' in monitoring:
+ verify_vrf(monitoring['frr_exporter'])
+
+ if 'blackbox_exporter' in monitoring:
+ verify_vrf(monitoring['blackbox_exporter'])
+
+ if (
+ 'modules' in monitoring['blackbox_exporter']
+ and 'dns' in monitoring['blackbox_exporter']['modules']
+ and 'name' in monitoring['blackbox_exporter']['modules']['dns']
+ ):
+ for mod_name, mod_config in monitoring['blackbox_exporter']['modules'][
+ 'dns'
+ ]['name'].items():
+ if 'query_name' not in mod_config:
+ raise ConfigError(
+ f'query name not specified in dns module {mod_name}'
+ )
+
+ return None
+
+
+def generate(monitoring):
+ if not monitoring or 'node_exporter' not in monitoring:
+ # Delete systemd files
+ if os.path.isfile(node_exporter_service_file):
+ os.unlink(node_exporter_service_file)
+
+ if not monitoring or 'frr_exporter' not in monitoring:
+ # Delete systemd files
+ if os.path.isfile(frr_exporter_service_file):
+ os.unlink(frr_exporter_service_file)
+
+ if not monitoring or 'blackbox_exporter' not in monitoring:
+ # Delete systemd files
+ if os.path.isfile(blackbox_exporter_service_file):
+ os.unlink(blackbox_exporter_service_file)
+
+ if not monitoring:
+ return None
+
+ if 'node_exporter' in monitoring:
+ # Render node_exporter node_exporter_service_file
+ render(
+ node_exporter_service_file,
+ 'prometheus/node_exporter.service.j2',
+ monitoring['node_exporter'],
+ )
+ if (
+ 'collectors' in monitoring['node_exporter']
+ and 'textfile' in monitoring['node_exporter']['collectors']
+ ):
+ # Create textcollector folder
+ if not os.path.isdir(node_exporter_collector_path):
+ os.makedirs(node_exporter_collector_path)
+
+ if 'frr_exporter' in monitoring:
+ # Render frr_exporter service_file
+ render(
+ frr_exporter_service_file,
+ 'prometheus/frr_exporter.service.j2',
+ monitoring['frr_exporter'],
+ )
+
+ if 'blackbox_exporter' in monitoring:
+ # Render blackbox_exporter service_file
+ render(
+ blackbox_exporter_service_file,
+ 'prometheus/blackbox_exporter.service.j2',
+ monitoring['blackbox_exporter'],
+ )
+ # Render blackbox_exporter config file
+ render(
+ '/run/blackbox_exporter/config.yml',
+ 'prometheus/blackbox_exporter.yml.j2',
+ monitoring['blackbox_exporter'],
+ )
+
+ return None
+
+
+def apply(monitoring):
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+ if not monitoring or 'node_exporter' not in monitoring:
+ call(f'systemctl stop {node_exporter_systemd_service}')
+ if not monitoring or 'frr_exporter' not in monitoring:
+ call(f'systemctl stop {frr_exporter_systemd_service}')
+ if not monitoring or 'blackbox_exporter' not in monitoring:
+ call(f'systemctl stop {blackbox_exporter_systemd_service}')
+
+ if not monitoring:
+ return
+
+ if 'node_exporter' in monitoring:
+ # we need to restart the service if e.g. the VRF name changed
+ systemd_action = 'reload-or-restart'
+ if 'node_exporter_restart_required' in monitoring:
+ systemd_action = 'restart'
+
+ call(f'systemctl {systemd_action} {node_exporter_systemd_service}')
+
+ if 'frr_exporter' in monitoring:
+ # we need to restart the service if e.g. the VRF name changed
+ systemd_action = 'reload-or-restart'
+ if 'frr_exporter_restart_required' in monitoring:
+ systemd_action = 'restart'
+
+ call(f'systemctl {systemd_action} {frr_exporter_systemd_service}')
+
+ if 'blackbox_exporter' in monitoring:
+ # we need to restart the service if e.g. the VRF name changed
+ systemd_action = 'reload-or-restart'
+ if 'blackbox_exporter_restart_required' in monitoring:
+ systemd_action = 'restart'
+
+ call(f'systemctl {systemd_action} {blackbox_exporter_systemd_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_monitoring_zabbix-agent.py b/src/conf_mode/service_monitoring_zabbix-agent.py
index 98d8a32ca..f17146a8d 100755
--- a/src/conf_mode/service_monitoring_zabbix-agent.py
+++ b/src/conf_mode/service_monitoring_zabbix-agent.py
@@ -18,6 +18,8 @@ import os
from vyos.config import Config
from vyos.template import render
+from vyos.utils.dict import dict_search
+from vyos.utils.file import write_file
from vyos.utils.process import call
from vyos import ConfigError
from vyos import airbag
@@ -26,6 +28,7 @@ airbag.enable()
service_name = 'zabbix-agent2'
service_conf = f'/run/zabbix/{service_name}.conf'
+service_psk_file = f'/run/zabbix/{service_name}.psk'
systemd_override = r'/run/systemd/system/zabbix-agent2.service.d/10-override.conf'
@@ -49,6 +52,8 @@ def get_config(config=None):
if 'directory' in config and config['directory'].endswith('/'):
config['directory'] = config['directory'][:-1]
+ config['service_psk_file'] = service_psk_file
+
return config
@@ -60,18 +65,34 @@ def verify(config):
if 'server' not in config:
raise ConfigError('Server is required!')
+ if 'authentication' in config and dict_search("authentication.mode",
+ config) == 'pre_shared_secret':
+ if 'id' not in config['authentication']['psk']:
+ raise ConfigError(
+ 'PSK identity is required for pre-shared-secret authentication mode')
+
+ if 'secret' not in config['authentication']['psk']:
+ raise ConfigError(
+ 'PSK secret is required for pre-shared-secret authentication mode')
+
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]
+ config_files = [service_conf, systemd_override, service_psk_file]
for file in config_files:
if os.path.isfile(file):
os.unlink(file)
return None
+ if not dict_search("authentication.psk.secret", config):
+ if os.path.isfile(service_psk_file):
+ os.unlink(service_psk_file)
+ else:
+ write_file(service_psk_file, config["authentication"]["psk"]["secret"])
+
# Write configuration file
render(service_conf, 'zabbix-agent/zabbix-agent.conf.j2', config)
render(systemd_override, 'zabbix-agent/10-override.conf.j2', config)
diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py
index c9c0ed9a0..c64c59af7 100755
--- a/src/conf_mode/service_snmp.py
+++ b/src/conf_mode/service_snmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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,6 +22,7 @@ from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configverify import verify_vrf
+from vyos.defaults import systemd_services
from vyos.snmpv3_hashgen import plaintext_to_md5
from vyos.snmpv3_hashgen import plaintext_to_sha1
from vyos.snmpv3_hashgen import random
@@ -43,7 +44,7 @@ config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user = r'/var/lib/snmp/snmpd.conf'
default_script_dir = r'/config/user-data/'
systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf'
-systemd_service = 'snmpd.service'
+systemd_service = systemd_services['snmpd']
def get_config(config=None):
if config:
@@ -146,6 +147,9 @@ def verify(snmp):
return None
if 'user' in snmp['v3']:
+ if 'engineid' not in snmp['v3']:
+ raise ConfigError(f'EngineID must be configured for SNMPv3!')
+
for user, user_config in snmp['v3']['user'].items():
if 'group' not in user_config:
raise ConfigError(f'Group membership required for user "{user}"!')
@@ -260,15 +264,6 @@ def apply(snmp):
# start SNMP daemon
call(f'systemctl reload-or-restart {systemd_service}')
-
- # Enable AgentX in FRR
- # This should be done for each daemon individually because common command
- # works only if all the daemons started with SNMP support
- # Following daemons from FRR 9.0/stable have SNMP module compiled in VyOS
- frr_daemons_list = ['zebra', 'bgpd', 'ospf6d', 'ospfd', 'ripd', 'isisd', 'ldpd']
- for frr_daemon in frr_daemons_list:
- call(f'vtysh -c "configure terminal" -d {frr_daemon} -c "agentx" >/dev/null')
-
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/service_ssh.py b/src/conf_mode/service_ssh.py
index 9abdd33dc..759f87bb2 100755
--- a/src/conf_mode/service_ssh.py
+++ b/src/conf_mode/service_ssh.py
@@ -23,10 +23,16 @@ from syslog import LOG_INFO
from vyos.config import Config
from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_pki_ca_certificate
from vyos.utils.process import call
from vyos.template import render
from vyos import ConfigError
from vyos import airbag
+from vyos.pki import find_chain
+from vyos.pki import encode_certificate
+from vyos.pki import load_certificate
+from vyos.utils.file import write_file
+
airbag.enable()
config_file = r'/run/sshd/sshd_config'
@@ -38,6 +44,9 @@ key_rsa = '/etc/ssh/ssh_host_rsa_key'
key_dsa = '/etc/ssh/ssh_host_dsa_key'
key_ed25519 = '/etc/ssh/ssh_host_ed25519_key'
+trusted_user_ca_key = '/etc/ssh/trusted_user_ca_key'
+
+
def get_config(config=None):
if config:
conf = config
@@ -47,10 +56,13 @@ def get_config(config=None):
if not conf.exists(base):
return None
- ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ ssh = conf.get_config_dict(
+ base, key_mangling=('-', '_'), get_first_key=True, with_pki=True
+ )
tmp = is_node_changed(conf, base + ['vrf'])
- if tmp: ssh.update({'restart_required': {}})
+ if tmp:
+ ssh.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.
@@ -62,20 +74,32 @@ def get_config(config=None):
# Ignore default XML values if config doesn't exists
# Delete key from dict
if not conf.exists(base + ['dynamic-protection']):
- del ssh['dynamic_protection']
+ del ssh['dynamic_protection']
return ssh
+
def verify(ssh):
if not ssh:
return None
if 'rekey' in ssh and 'data' not in ssh['rekey']:
- raise ConfigError(f'Rekey data is required!')
+ raise ConfigError('Rekey data is required!')
+
+ if 'trusted_user_ca_key' in ssh:
+ if 'ca_certificate' not in ssh['trusted_user_ca_key']:
+ raise ConfigError('CA certificate is required for TrustedUserCAKey')
+
+ ca_key_name = ssh['trusted_user_ca_key']['ca_certificate']
+ verify_pki_ca_certificate(ssh, ca_key_name)
+ pki_ca_cert = ssh['pki']['ca'][ca_key_name]
+ if 'certificate' not in pki_ca_cert or not pki_ca_cert['certificate']:
+ raise ConfigError(f"CA certificate '{ca_key_name}' is not valid or missing")
verify_vrf(ssh)
return None
+
def generate(ssh):
if not ssh:
if os.path.isfile(config_file):
@@ -95,6 +119,24 @@ def generate(ssh):
syslog(LOG_INFO, 'SSH ed25519 host key not found, generating new key!')
call(f'ssh-keygen -q -N "" -t ed25519 -f {key_ed25519}')
+ if 'trusted_user_ca_key' in ssh:
+ ca_key_name = ssh['trusted_user_ca_key']['ca_certificate']
+ pki_ca_cert = ssh['pki']['ca'][ca_key_name]
+
+ loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
+ loaded_ca_certs = {
+ load_certificate(c['certificate'])
+ for c in ssh['pki']['ca'].values()
+ if 'certificate' in c
+ }
+
+ ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
+ write_file(
+ trusted_user_ca_key, '\n'.join(encode_certificate(c) for c in ca_full_chain)
+ )
+ elif os.path.exists(trusted_user_ca_key):
+ os.unlink(trusted_user_ca_key)
+
render(config_file, 'ssh/sshd_config.j2', ssh)
if 'dynamic_protection' in ssh:
@@ -103,12 +145,12 @@ def generate(ssh):
return None
+
def apply(ssh):
- systemd_service_ssh = 'ssh.service'
systemd_service_sshguard = 'sshguard.service'
if not ssh:
# SSH access is removed in the commit
- call(f'systemctl stop ssh@*.service')
+ call('systemctl stop ssh@*.service')
call(f'systemctl stop {systemd_service_sshguard}')
return None
@@ -122,13 +164,14 @@ def apply(ssh):
if 'restart_required' in ssh:
# this is only true if something for the VRFs changed, thus we
# stop all VRF services and only restart then new ones
- call(f'systemctl stop ssh@*.service')
+ call('systemctl stop ssh@*.service')
systemd_action = 'restart'
for vrf in ssh['vrf']:
call(f'systemctl {systemd_action} ssh@{vrf}.service')
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/system_flow-accounting.py b/src/conf_mode/system_flow-accounting.py
index a12ee363d..925c4a562 100755
--- a/src/conf_mode/system_flow-accounting.py
+++ b/src/conf_mode/system_flow-accounting.py
@@ -18,7 +18,6 @@ import os
import re
from sys import exit
-from ipaddress import ip_address
from vyos.config import Config
from vyos.config import config_dict_merge
@@ -159,9 +158,9 @@ def get_config(config=None):
# delete individual flow type defaults - should only be added if user
# sets this feature
- for flow_type in ['sflow', 'netflow']:
- if flow_type not in flow_accounting and flow_type in default_values:
- del default_values[flow_type]
+ flow_type = 'netflow'
+ if flow_type not in flow_accounting and flow_type in default_values:
+ del default_values[flow_type]
flow_accounting = config_dict_merge(default_values, flow_accounting)
@@ -171,9 +170,9 @@ def verify(flow_config):
if not flow_config:
return None
- # check if at least one collector is enabled
- if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config:
- raise ConfigError('You need to configure at least sFlow or NetFlow, ' \
+ # check if collector is enabled
+ if 'netflow' not in flow_config and 'disable_imt' in flow_config:
+ raise ConfigError('You need to configure NetFlow, ' \
'or not set "disable-imt" for flow-accounting!')
# Check if at least one interface is configured
@@ -185,45 +184,7 @@ def verify(flow_config):
for interface in flow_config['interface']:
verify_interface_exists(flow_config, interface, warning_only=True)
- # check sFlow configuration
- if 'sflow' in flow_config:
- # check if at least one sFlow collector is configured
- if 'server' not in flow_config['sflow']:
- raise ConfigError('You need to configure at least one sFlow server!')
-
- # check that all sFlow collectors use the same IP protocol version
- sflow_collector_ipver = None
- for server in flow_config['sflow']['server']:
- if sflow_collector_ipver:
- if sflow_collector_ipver != ip_address(server).version:
- raise ConfigError("All sFlow servers must use the same IP protocol")
- else:
- 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']
-
- # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
- for server in flow_config['sflow']['server']:
- if 'agent_address' in flow_config['sflow']:
- if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version:
- raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\
- 'server". You need to set the same IP version for both "agent-address" and '\
- 'all sFlow servers')
-
- if 'agent_address' in flow_config['sflow']:
- tmp = flow_config['sflow']['agent_address']
- 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 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']
- raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!')
+ verify_vrf(flow_config)
# check NetFlow configuration
if 'netflow' in flow_config:
diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py
index 3f245f166..fef034d1c 100755
--- a/src/conf_mode/system_host-name.py
+++ b/src/conf_mode/system_host-name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -23,6 +23,7 @@ import vyos.hostsd_client
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import leaf_node_changed
+from vyos.defaults import systemd_services
from vyos.ifconfig import Section
from vyos.template import is_ip
from vyos.utils.process import cmd
@@ -174,11 +175,13 @@ def apply(config):
# Restart services that use the hostname
if hostname_new != hostname_old:
- call("systemctl restart rsyslog.service")
+ tmp = systemd_services['rsyslog']
+ call(f'systemctl restart {tmp}')
# If SNMP is running, restart it too
if process_named_running('snmpd') and config['snmpd_restart_reqired']:
- call('systemctl restart snmpd.service')
+ tmp = systemd_services['snmpd']
+ call(f'systemctl restart {tmp}')
return None
diff --git a/src/conf_mode/system_ip.py b/src/conf_mode/system_ip.py
index c8a91fd2f..7f3796168 100755
--- a/src/conf_mode/system_ip.py
+++ b/src/conf_mode/system_ip.py
@@ -17,17 +17,17 @@
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.configverify import verify_route_map
-from vyos.template import render_to_string
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.utils.dict import dict_search
-from vyos.utils.file import write_file
from vyos.utils.process import is_systemd_service_active
+from vyos.utils.process import is_systemd_service_running
from vyos.utils.system import sysctl_write
-from vyos.configdep import set_dependents
-from vyos.configdep import call_dependents
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -36,42 +36,36 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['system', 'ip']
-
- 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'
-
- # We also need the route-map information from the config
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
- get_first_key=True)}}
- # Merge policy dict into "regular" config dict
- opt = dict_merge(tmp, opt)
# If IPv4 ARP table size is set here and also manually in sysctl, the more
# fine grained value from sysctl must win
set_dependents('sysctl', conf)
+ return get_frrender_dict(conf)
+
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'ip'):
+ return None
- return opt
+ opt = config_dict['ip']
+ opt['policy'] = config_dict['policy']
-def verify(opt):
if 'protocol' in opt:
for protocol, protocol_options in opt['protocol'].items():
if 'route_map' in protocol_options:
verify_route_map(protocol_options['route_map'], opt)
return
-def generate(opt):
- opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
- return
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
+ return None
+
+def apply(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'ip'):
+
+ return None
+ opt = config_dict['ip']
-def apply(opt):
# Apply ARP threshold values
# table_size has a default value - thus the key always exists
size = int(dict_search('arp.table_size', opt))
@@ -82,11 +76,6 @@ def apply(opt):
# Minimum number of stored records is indicated which is not cleared
sysctl_write('net.ipv4.neigh.default.gc_thresh1', size // 8)
- # enable/disable IPv4 forwarding
- tmp = dict_search('disable_forwarding', opt)
- value = '0' if (tmp != None) else '1'
- write_file('/proc/sys/net/ipv4/conf/all/forwarding', value)
-
# configure multipath
tmp = dict_search('multipath.ignore_unreachable_nexthops', opt)
value = '1' if (tmp != None) else '0'
@@ -121,19 +110,11 @@ def apply(opt):
# running when this script is called first. Skip this part and wait for initial
# commit of the configuration to trigger this statement
if is_systemd_service_active('frr.service'):
- 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'no ip nht resolve-via-default')
- 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 config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
call_dependents()
+ return None
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system_ipv6.py b/src/conf_mode/system_ipv6.py
index a2442d009..309869b2f 100755
--- a/src/conf_mode/system_ipv6.py
+++ b/src/conf_mode/system_ipv6.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# Copyright (C) 2019-2024 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,17 +18,18 @@ import os
from sys import exit
from vyos.config import Config
-from vyos.configdict import dict_merge
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
+from vyos.configverify import has_frr_protocol_in_dict
from vyos.configverify import verify_route_map
-from vyos.template import render_to_string
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.utils.dict import dict_search
from vyos.utils.file import write_file
from vyos.utils.process import is_systemd_service_active
+from vyos.utils.process import is_systemd_service_running
from vyos.utils.system import sysctl_write
-from vyos.configdep import set_dependents
-from vyos.configdep import call_dependents
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -37,42 +38,35 @@ def get_config(config=None):
conf = config
else:
conf = Config()
- base = ['system', 'ipv6']
-
- 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'
-
- # We also need the route-map information from the config
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
- get_first_key=True)}}
- # Merge policy dict into "regular" config dict
- opt = dict_merge(tmp, opt)
# If IPv6 neighbor table size is set here and also manually in sysctl, the more
# fine grained value from sysctl must win
set_dependents('sysctl', conf)
+ return get_frrender_dict(conf)
+
+def verify(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'ipv6'):
+ return None
- return opt
+ opt = config_dict['ipv6']
+ opt['policy'] = config_dict['policy']
-def verify(opt):
if 'protocol' in opt:
for protocol, protocol_options in opt['protocol'].items():
if 'route_map' in protocol_options:
verify_route_map(protocol_options['route_map'], opt)
return
-def generate(opt):
- opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt)
- return
+def generate(config_dict):
+ if config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(config_dict)
+ return None
+
+def apply(config_dict):
+ if not has_frr_protocol_in_dict(config_dict, 'ipv6'):
+ return None
+ opt = config_dict['ipv6']
-def apply(opt):
# configure multipath
tmp = dict_search('multipath.layer4_hashing', opt)
value = '1' if (tmp != None) else '0'
@@ -88,11 +82,6 @@ def apply(opt):
# Minimum number of stored records is indicated which is not cleared
sysctl_write('net.ipv6.neigh.default.gc_thresh1', size // 8)
- # enable/disable IPv6 forwarding
- tmp = dict_search('disable_forwarding', opt)
- value = '0' if (tmp != None) else '1'
- write_file('/proc/sys/net/ipv6/conf/all/forwarding', value)
-
# configure IPv6 strict-dad
tmp = dict_search('strict_dad', opt)
value = '2' if (tmp != None) else '1'
@@ -105,19 +94,11 @@ def apply(opt):
# running when this script is called first. Skip this part and wait for initial
# commit of the configuration to trigger this statement
if is_systemd_service_active('frr.service'):
- 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'no ipv6 nht resolve-via-default')
- 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 config_dict and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
call_dependents()
+ return None
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py
index 439fa645b..4febb6494 100755
--- a/src/conf_mode/system_login.py
+++ b/src/conf_mode/system_login.py
@@ -24,10 +24,13 @@ from pwd import getpwuid
from sys import exit
from time import sleep
+from vyos.base import Warning
from vyos.config import Config
from vyos.configverify import verify_vrf
from vyos.template import render
from vyos.template import is_ipv4
+from vyos.utils.auth import EPasswdStrength
+from vyos.utils.auth import evaluate_strength
from vyos.utils.auth import get_current_user
from vyos.utils.configfs import delete_cli_node
from vyos.utils.configfs import add_cli_node
@@ -58,20 +61,21 @@ MAX_RADIUS_TIMEOUT: int = 50
MAX_RADIUS_COUNT: int = 8
# Maximum number of supported TACACS servers
MAX_TACACS_COUNT: int = 8
-
+# Minimum USER id for TACACS users
+MIN_TACACS_UID = 900
# 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():
+def get_local_users(min_uid=MIN_USER_UID, max_uid=MAX_USER_UID):
"""Return list of dynamically allocated users (see Debian Policy Manual)"""
local_users = []
for s_user in getpwall():
- if getpwnam(s_user.pw_name).pw_uid < MIN_USER_UID:
+ if getpwnam(s_user.pw_name).pw_uid < min_uid:
continue
- if getpwnam(s_user.pw_name).pw_uid > MAX_USER_UID:
+ if getpwnam(s_user.pw_name).pw_uid > max_uid:
continue
if s_user.pw_name in SYSTEM_USER_SKIP_LIST:
continue
@@ -119,6 +123,12 @@ def get_config(config=None):
rm_users = [tmp for tmp in all_users if tmp not in cli_users]
if rm_users: login.update({'rm_users' : rm_users})
+ # Build TACACS user mapping
+ if 'tacacs' in login:
+ login['exclude_users'] = get_local_users(min_uid=0,
+ max_uid=MIN_TACACS_UID) + cli_users
+ login['tacacs_min_uid'] = MIN_TACACS_UID
+
return login
def verify(login):
@@ -139,6 +149,19 @@ def verify(login):
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!')
+ # T6353: Check password for complexity using cracklib.
+ # A user password should be sufficiently complex
+ plaintext_password = dict_search(
+ path='authentication.plaintext_password',
+ dict_object=user_config
+ ) or None
+
+ failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR]
+ if plaintext_password is not None:
+ result = evaluate_strength(plaintext_password)
+ if result['strength'] in failed_check_status:
+ Warning(result['error'])
+
for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
if 'type' not in pubkey_options:
raise ConfigError(f'Missing type for public-key "{pubkey}"!')
diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py
index 5826d8042..cdd066649 100755
--- a/src/conf_mode/system_login_banner.py
+++ b/src/conf_mode/system_login_banner.py
@@ -95,8 +95,12 @@ def apply(banner):
render(POSTLOGIN_FILE, 'login/default_motd.j2', banner,
permission=0o644, user='root', group='root')
- render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', banner,
- permission=0o644, user='root', group='root')
+ if banner['version_data']['build_type'] != 'release':
+ render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2',
+ banner,
+ permission=0o644,
+ user='root',
+ group='root')
return None
diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py
index e2832cde6..064a1aa91 100755
--- a/src/conf_mode/system_option.py
+++ b/src/conf_mode/system_option.py
@@ -86,7 +86,7 @@ def verify(options):
if 'source_address' in config:
if not is_addr_assigned(config['source_address']):
- raise ConfigError('No interface with give address specified!')
+ raise ConfigError('No interface with given address specified!')
if 'ssh_client' in options:
config = options['ssh_client']
diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py
index 41119b494..a22dac36f 100755
--- a/src/conf_mode/system_sflow.py
+++ b/src/conf_mode/system_sflow.py
@@ -54,7 +54,7 @@ def verify(sflow):
# Check if configured sflow agent-address exist in the system
if 'agent_address' in sflow:
tmp = sflow['agent_address']
- if not is_addr_assigned(tmp):
+ if not is_addr_assigned(tmp, include_vrf=True):
raise ConfigError(
f'Configured "sflow agent-address {tmp}" does not exist in the system!'
)
diff --git a/src/conf_mode/system_syslog.py b/src/conf_mode/system_syslog.py
index eb2f02eb3..414bd4b6b 100755
--- a/src/conf_mode/system_syslog.py
+++ b/src/conf_mode/system_syslog.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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,17 +20,22 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
+from vyos.defaults import systemd_services
+from vyos.utils.network import is_addr_assigned
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 import ConfigError
from vyos import airbag
airbag.enable()
-rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf'
+rsyslog_conf = '/run/rsyslog/rsyslog.conf'
logrotate_conf = '/etc/logrotate.d/vyos-rsyslog'
-systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf'
+
+systemd_socket = 'syslog.socket'
+systemd_service = systemd_services['rsyslog']
def get_config(config=None):
if config:
@@ -46,23 +51,17 @@ def get_config(config=None):
syslog.update({ 'logrotate' : logrotate_conf })
- tmp = is_node_changed(conf, base + ['vrf'])
- if tmp: syslog.update({'restart_required': {}})
-
syslog = conf.merge_defaults(syslog, recursive=True)
- if syslog.from_defaults(['global']):
- del syslog['global']
-
- if (
- 'global' in syslog
- and 'preserve_fqdn' in syslog['global']
- and conf.exists(['system', 'host-name'])
- and conf.exists(['system', 'domain-name'])
- ):
- hostname = conf.return_value(['system', 'host-name'])
- domain = conf.return_value(['system', 'domain-name'])
- fqdn = f'{hostname}.{domain}'
- syslog['global']['local_host_name'] = fqdn
+ if syslog.from_defaults(['local']):
+ del syslog['local']
+
+ if 'preserve_fqdn' in syslog:
+ if conf.exists(['system', 'host-name']):
+ tmp = conf.return_value(['system', 'host-name'])
+ syslog['preserve_fqdn']['host_name'] = tmp
+ if conf.exists(['system', 'domain-name']):
+ tmp = conf.return_value(['system', 'domain-name'])
+ syslog['preserve_fqdn']['domain_name'] = tmp
return syslog
@@ -70,13 +69,33 @@ def verify(syslog):
if not syslog:
return None
- if 'host' in syslog:
- for host, host_options in syslog['host'].items():
- if 'protocol' in host_options and host_options['protocol'] == 'udp':
- if 'format' in host_options and 'octet_counted' in host_options['format']:
- Warning(f'Syslog UDP transport for "{host}" should not use octet-counted format!')
-
- verify_vrf(syslog)
+ if 'preserve_fqdn' in syslog:
+ if 'host_name' not in syslog['preserve_fqdn']:
+ Warning('No "system host-name" defined - cannot set syslog FQDN!')
+ if 'domain_name' not in syslog['preserve_fqdn']:
+ Warning('No "system domain-name" defined - cannot set syslog FQDN!')
+
+ if 'remote' in syslog:
+ for remote, remote_options in syslog['remote'].items():
+ if 'protocol' in remote_options and remote_options['protocol'] == 'udp':
+ if 'format' in remote_options and 'octet_counted' in remote_options['format']:
+ Warning(f'Syslog UDP transport for "{remote}" should not use octet-counted format!')
+
+ if 'vrf' in remote_options:
+ verify_vrf(remote_options)
+
+ if 'source_address' in remote_options:
+ vrf = None
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']
+ if not is_addr_assigned(remote_options['source_address'], vrf):
+ raise ConfigError('No interface with given address specified!')
+
+ source_address = remote_options['source_address']
+ if ((is_ipv4(remote) and is_ipv6(source_address)) or
+ (is_ipv6(remote) and is_ipv4(source_address))):
+ raise ConfigError(f'Source-address "{source_address}" does not match '\
+ f'address-family of remote "{remote}"!')
def generate(syslog):
if not syslog:
@@ -88,26 +107,15 @@ def generate(syslog):
return None
render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog)
- render(systemd_override, 'rsyslog/override.conf.j2', syslog)
render(logrotate_conf, 'rsyslog/logrotate.j2', syslog)
-
- # Reload systemd manager configuration
- call('systemctl daemon-reload')
return None
def apply(syslog):
- systemd_socket = 'syslog.socket'
- systemd_service = 'syslog.service'
if not syslog:
call(f'systemctl stop {systemd_service} {systemd_socket}')
return None
- # we need to restart the service if e.g. the VRF name changed
- systemd_action = 'reload-or-restart'
- if 'restart_required' in syslog:
- systemd_action = 'restart'
-
- call(f'systemctl {systemd_action} {systemd_service}')
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index e22b7550c..2754314f7 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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
@@ -64,6 +64,7 @@ swanctl_dir = '/etc/swanctl'
charon_conf = '/etc/strongswan.d/charon.conf'
charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf'
+charon_systemd_conf = '/etc/strongswan.d/charon-systemd.conf'
interface_conf = '/etc/strongswan.d/interfaces_use.conf'
swanctl_conf = f'{swanctl_dir}/swanctl.conf'
@@ -86,8 +87,6 @@ def get_config(config=None):
conf = Config()
base = ['vpn', 'ipsec']
l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings']
- if not conf.exists(base):
- return None
# retrieve common dictionary keys
ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
@@ -95,6 +94,14 @@ def get_config(config=None):
get_first_key=True,
with_pki=True)
+ ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
+ if ipsec['nhrp_exists']:
+ set_dependents('nhrp', conf)
+
+ if not conf.exists(base):
+ ipsec.update({'deleted' : ''})
+ return ipsec
+
# We have to cleanup the default dict, as default values could
# enable features which are not explicitly enabled on the
# CLI. E.g. dead-peer-detection defaults should not be injected
@@ -115,7 +122,6 @@ def get_config(config=None):
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'])
if ipsec['nhrp_exists']:
set_dependents('nhrp', conf)
@@ -151,6 +157,8 @@ def get_config(config=None):
_, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
ipsec['vti_interface_dicts'][vti_interface] = vti
+ ipsec['vpp_ipsec_exists'] = conf.exists(['vpp', 'settings', 'ipsec'])
+
return ipsec
def get_dhcp_address(iface):
@@ -196,8 +204,8 @@ def verify_pki_rsa(pki, rsa_conf):
return True
def verify(ipsec):
- if not ipsec:
- return None
+ if not ipsec or 'deleted' in ipsec:
+ return
if 'authentication' in ipsec:
if 'psk' in ipsec['authentication']:
@@ -479,6 +487,17 @@ def verify(ipsec):
else:
raise ConfigError(f"Missing ike-group on site-to-site peer {peer}")
+ # verify encryption algorithm compatibility for IKE with VPP
+ if ipsec['vpp_ipsec_exists']:
+ ike_group = ipsec['ike_group'][peer_conf['ike_group']]
+ for proposal, proposal_config in ike_group.get('proposal', {}).items():
+ algs = ['gmac', 'serpent', 'twofish']
+ if any(alg in proposal_config['encryption'] for alg in algs):
+ raise ConfigError(
+ f'Encryption algorithm {proposal_config["encryption"]} cannot be used '
+ f'for IKE proposal {proposal} for site-to-site peer {peer} with VPP'
+ )
+
if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']:
raise ConfigError(f"Missing authentication on site-to-site peer {peer}")
@@ -557,7 +576,7 @@ def verify(ipsec):
esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group']
- if esp_group_name not in ipsec['esp_group']:
+ if esp_group_name not in ipsec.get('esp_group'):
raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}")
esp_group = ipsec['esp_group'][esp_group_name]
@@ -569,6 +588,18 @@ def verify(ipsec):
if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']):
raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
+ # verify ESP encryption algorithm compatibility with VPP
+ # because Marvel plugin for VPP doesn't support all algorithms that Strongswan does
+ if ipsec['vpp_ipsec_exists']:
+ for proposal, proposal_config in esp_group.get('proposal', {}).items():
+ algs = ['aes128', 'aes192', 'aes256', 'aes128gcm128', 'aes192gcm128', 'aes256gcm128']
+ if proposal_config['encryption'] not in algs:
+ raise ConfigError(
+ f'Encryption algorithm {proposal_config["encryption"]} cannot be used '
+ f'for ESP proposal {proposal} on tunnel {tunnel} for site-to-site peer {peer} with VPP'
+ )
+
+
def cleanup_pki_files():
for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]:
if not os.path.exists(path):
@@ -624,7 +655,7 @@ def generate_pki_files_rsa(pki, rsa_conf):
def generate(ipsec):
cleanup_pki_files()
- if not ipsec:
+ if not ipsec or 'deleted' in ipsec:
for config_file in [charon_dhcp_conf, charon_radius_conf, interface_conf, swanctl_conf]:
if os.path.isfile(config_file):
os.unlink(config_file)
@@ -715,21 +746,19 @@ def generate(ipsec):
render(charon_conf, 'ipsec/charon.j2', ipsec)
render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec)
render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec)
+ render(charon_systemd_conf, 'ipsec/charon_systemd.conf.j2', ipsec)
render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec)
render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec)
def apply(ipsec):
systemd_service = 'strongswan.service'
- if not ipsec:
+ if not ipsec or 'deleted' in ipsec:
call(f'systemctl stop {systemd_service}')
-
if vti_updown_db_exists():
remove_vti_updown_db()
-
else:
call(f'systemctl reload-or-restart {systemd_service}')
-
if ipsec['enabled_vti_interfaces']:
with open_vti_updown_db_for_create_or_update() as db:
db.removeAllOtherInterfaces(ipsec['enabled_vti_interfaces'])
@@ -737,7 +766,7 @@ def apply(ipsec):
db.commit(lambda interface: ipsec['vti_interface_dicts'][interface])
elif vti_updown_db_exists():
remove_vti_updown_db()
-
+ if ipsec:
if ipsec.get('nhrp_exists', False):
try:
call_dependents()
@@ -746,7 +775,6 @@ def apply(ipsec):
# ConfigError("ConfigError('Interface ethN requires an IP address!')")
pass
-
if __name__ == '__main__':
try:
ipsec = get_config()
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 72b178c89..8baf55857 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -19,23 +19,23 @@ from jmespath import search
from json import loads
from vyos.config import Config
-from vyos.configdict import dict_merge
from vyos.configdict import node_changed
from vyos.configverify import verify_route_map
from vyos.firewall import conntrack_required
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
from vyos.ifconfig import Interface
from vyos.template import render
-from vyos.template import render_to_string
from vyos.utils.dict import dict_search
from vyos.utils.network import get_vrf_tableid
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 is_systemd_service_running
from vyos.utils.process import popen
from vyos.utils.system import sysctl_write
from vyos import ConfigError
-from vyos import frr
from vyos import airbag
airbag.enable()
@@ -132,15 +132,9 @@ def get_config(config=None):
if 'name' in vrf:
vrf['conntrack'] = conntrack_required(conf)
- # We also need the route-map information from the config
- #
- # XXX: one MUST always call this without the key_mangling() option! See
- # vyos.configverify.verify_common_route_maps() for more information.
- tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'],
- get_first_key=True)}}
-
- # Merge policy dict into "regular" config dict
- vrf = dict_merge(tmp, vrf)
+ # We need to merge the FRR rendering dict into the VRF dict
+ # this is required to get the route-map information to FRR
+ vrf.update({'frr_dict' : get_frrender_dict(conf)})
return vrf
def verify(vrf):
@@ -155,9 +149,11 @@ def verify(vrf):
f'static routes installed!')
if 'name' in vrf:
- reserved_names = ["add", "all", "broadcast", "default", "delete", "dev",
- "get", "inet", "mtu", "link", "type", "vrf"]
+ reserved_names = ['add', 'all', 'broadcast', 'default', 'delete', 'dev',
+ 'down', 'get', 'inet', 'link', 'mtu', 'type', 'up', 'vrf']
+
table_ids = []
+ vnis = []
for name, vrf_config in vrf['name'].items():
# Reserved VRF names
if name in reserved_names:
@@ -178,17 +174,24 @@ def verify(vrf):
raise ConfigError(f'VRF "{name}" table id is not unique!')
table_ids.append(vrf_config['table'])
+ # VRF VNIs must be unique on the system
+ if 'vni' in vrf_config:
+ vni = vrf_config['vni']
+ if vni in vnis:
+ raise ConfigError(f'VRF "{name}" VNI "{vni}" is not unique!')
+ vnis.append(vni)
+
tmp = dict_search('ip.protocol', vrf_config)
if tmp != None:
for protocol, protocol_options in tmp.items():
if 'route_map' in protocol_options:
- verify_route_map(protocol_options['route_map'], vrf)
+ verify_route_map(protocol_options['route_map'], vrf['frr_dict'])
tmp = dict_search('ipv6.protocol', vrf_config)
if tmp != None:
for protocol, protocol_options in tmp.items():
if 'route_map' in protocol_options:
- verify_route_map(protocol_options['route_map'], vrf)
+ verify_route_map(protocol_options['route_map'], vrf['frr_dict'])
return None
@@ -196,8 +199,9 @@ def verify(vrf):
def generate(vrf):
# Render iproute2 VR helper names
render(config_file, 'iproute2/vrf.conf.j2', vrf)
- # Render VRF Kernel/Zebra route-map filters
- vrf['frr_zebra_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf)
+
+ if 'frr_dict' in vrf and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().generate(vrf['frr_dict'])
return None
@@ -339,17 +343,8 @@ def apply(vrf):
if has_rule(afi, 2000, 'l3mdev'):
call(f'ip {afi} rule del pref 2000 l3mdev unreachable')
- # Apply FRR filters
- zebra_daemon = 'zebra'
- # Save original configuration prior to starting any commit actions
- frr_cfg = frr.FRRConfig()
-
- # The route-map used for the FIB (zebra) is part of the zebra daemon
- frr_cfg.load_configuration(zebra_daemon)
- frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True)
- if 'frr_zebra_config' in vrf:
- frr_cfg.add_before(frr.default_add_before, vrf['frr_zebra_config'])
- frr_cfg.commit_configuration(zebra_daemon)
+ if 'frr_dict' in vrf and not is_systemd_service_running('vyos-configd.service'):
+ FRRender().apply()
return None