diff options
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-x | src/conf_mode/dns_dynamic.py | 38 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bonding.py | 23 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bridge.py | 18 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireguard.py | 33 | ||||
-rwxr-xr-x | src/conf_mode/service_pppoe-server.py | 7 | ||||
-rwxr-xr-x | src/conf_mode/vpp.py | 207 |
6 files changed, 82 insertions, 244 deletions
diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 874c4b689..d6ef620fe 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,16 +30,21 @@ config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' # Protocols that require zone -zone_necessary = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1'] # Protocols that do not require username -username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla'] +username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2', + 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla', + 'regfishde'] # Protocols that support TTL -ttl_supported = ['cloudflare', 'gandi', 'hetzner', 'dnsexit', 'godaddy', 'nfsn'] +ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn'] # Protocols that support both IPv4 and IPv6 -dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla'] +dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns', + 'dyndns2', 'easydns', 'freedns', 'hetzner', 'infomaniak', + 'njalla'] # dyndns2 protocol in ddclient honors dual stack for selective servers # because of the way it is implemented in ddclient @@ -82,34 +87,37 @@ def verify(dyndns): f'based Dynamic DNS service on "{address}"') # Dynamic DNS service provider - configuration validation + if 'web_options' in dyndns['address'][address] and address != 'web': + raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') + + # Dynamic DNS service provider - configuration validation if 'service' in dyndns['address'][address]: for service, config in dyndns['address'][address]['service'].items(): - error_msg = f'is required for Dynamic DNS service "{service}" on "{address}"' + error_msg_req = f'is required for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' + error_msg_uns = f'is not supported for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' for field in ['host_name', 'password', 'protocol']: if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}') + raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') if config['protocol'] in zone_necessary and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg}') + raise ConfigError(f'"zone" {error_msg_req}') - if config['protocol'] not in zone_necessary and 'zone' in config: - raise ConfigError(f'"{config["protocol"]}" does not support "zone"') + if config['protocol'] not in zone_supported and 'zone' in config: + raise ConfigError(f'"zone" {error_msg_uns}') if config['protocol'] not in username_unnecessary and 'username' not in config: - raise ConfigError(f'"username" {error_msg}') + raise ConfigError(f'"username" {error_msg_req}') if config['protocol'] not in ttl_supported and 'ttl' in config: - raise ConfigError(f'"{config["protocol"]}" does not support "ttl"') + raise ConfigError(f'"ttl" {error_msg_uns}') if config['ip_version'] == 'both': if config['protocol'] not in dualstack_supported: - raise ConfigError(f'"{config["protocol"]}" does not support ' - f'both IPv4 and IPv6 at the same time') + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns}') # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org) if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers: - raise ConfigError(f'"{config["protocol"]}" does not support ' - f'both IPv4 and IPv6 at the same time for "{config["server"]}"') + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}"') if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']): raise ConfigError(f'"expiry-time" must be greater than "wait-time"') diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index 1179e3e4f..8184d8415 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -35,12 +35,14 @@ from vyos.configverify import verify_vrf 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.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured from vyos.configdep import set_dependents, call_dependents from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() @@ -247,21 +249,38 @@ 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) return None def apply(bond): - b = BondIf(bond['ifname']) + ifname = bond['ifname'] + b = BondIf(ifname) if 'deleted' in bond: # delete interface b.remove() else: b.update(bond) + if dict_search('member.interface_remove', bond): try: call_dependents() except ConfigError: 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 c82f01e53..31508a3c5 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -28,7 +28,8 @@ from vyos.configverify import verify_vrf from vyos.ifconfig import BridgeIf from vyos.configdict import has_address_configured from vyos.configdict import has_vrf_configured - +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.utils.dict import dict_search from vyos import ConfigError @@ -83,6 +84,12 @@ def get_config(config=None): if 'enable_vlan' in bridge and tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''}) + # When using VXLAN member interfaces that are configured for Single + # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to re-create + # VLAN to VNI mappings if required + if interface.startswith('vxlan'): + set_dependents('vxlan', conf, interface) + # delete empty dictionary keys - no need to run code paths if nothing is there to do if 'member' in bridge: if 'interface' in bridge['member'] and len(bridge['member']['interface']) == 0: @@ -159,6 +166,13 @@ def apply(bridge): else: br.update(bridge) + for interface in dict_search('member.interface', bridge) or []: + if interface.startswith('vxlan'): + try: + call_dependents() + except ConfigError: + raise ConfigError('Error in updating VXLAN interface after changing bridge!') + return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 122d9589a..79e5d3f44 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -51,17 +51,9 @@ def get_config(config=None): tmp = is_node_changed(conf, base + [ifname, 'port']) if tmp: wireguard['port_changed'] = {} - # Determine which Wireguard peer has been removed. - # Peers can only be removed with their public key! - if 'peer' in wireguard: - peer_remove = {} - for peer, peer_config in wireguard['peer'].items(): - # T4702: If anything on a peer changes we remove the peer first and re-add it - if is_node_changed(conf, base + [ifname, 'peer', peer]): - if 'public_key' in peer_config: - peer_remove = dict_merge({'peer_remove' : {peer : peer_config['public_key']}}, peer_remove) - if peer_remove: - wireguard.update(peer_remove) + # T4702: If anything on a peer changes we remove the peer first and re-add it + if is_node_changed(conf, base + [ifname, 'peer']): + wireguard.update({'rebuild_required': {}}) return wireguard @@ -113,12 +105,21 @@ def verify(wireguard): public_keys.append(peer['public_key']) def apply(wireguard): - tmp = WireGuardIf(wireguard['ifname']) - if 'deleted' in wireguard: - tmp.remove() - return None + if 'rebuild_required' in wireguard or 'deleted' in wireguard: + wg = WireGuardIf(**wireguard) + # WireGuard only supports peer removal based on the configured public-key, + # by deleting the entire interface this is the shortcut instead of parsing + # out all peers and removing them one by one. + # + # Peer reconfiguration will always come with a short downtime while the + # WireGuard interface is recreated (see below) + wg.remove() + + # Create the new interface if required + if 'deleted' not in wireguard: + wg = WireGuardIf(**wireguard) + wg.update(wireguard) - tmp.update(wireguard) return None if __name__ == '__main__': diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index aace267a7..87660c127 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -48,9 +48,12 @@ def get_config(config=None): # reload-or-restart does not implemented in accel-ppp # use this workaround until it will be implemented # https://phabricator.accel-ppp.org/T3 - if is_node_changed(conf, base + ['client-ip-pool']) or is_node_changed( - conf, base + ['client-ipv6-pool']): + conditions = [is_node_changed(conf, base + ['client-ip-pool']), + is_node_changed(conf, base + ['client-ipv6-pool']), + is_node_changed(conf, base + ['interface'])] + if any(conditions): pppoe.update({'restart_required': {}}) + return pppoe def verify(pppoe): diff --git a/src/conf_mode/vpp.py b/src/conf_mode/vpp.py deleted file mode 100755 index 82c2f236e..000000000 --- a/src/conf_mode/vpp.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2023 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os -from psutil import virtual_memory - -from pathlib import Path -from re import search as re_search, MULTILINE as re_M - -from vyos.config import Config -from vyos.configdep import set_dependents, call_dependents -from vyos.configdict import node_changed -from vyos.ifconfig import Section -from vyos.utils.boot import boot_configuration_complete -from vyos.utils.process import call -from vyos.utils.process import rc_cmd -from vyos.utils.system import sysctl_read -from vyos.utils.system import sysctl_apply -from vyos.template import render - -from vyos import ConfigError -from vyos import airbag -from vyos.vpp import VPPControl -from vyos.vpp import HostControl - -airbag.enable() - -service_name = 'vpp' -service_conf = Path(f'/run/vpp/{service_name}.conf') -systemd_override = '/run/systemd/system/vpp.service.d/10-override.conf' - -# Free memory required for VPP -# 2 GB for hugepages + 1 GB for other services -MIN_AVAILABLE_MEMORY: int = 3 * 1024**3 - - -def _get_pci_address_by_interface(iface) -> str: - rc, out = rc_cmd(f'ethtool -i {iface}') - # if ethtool command was successful - if rc == 0 and out: - regex_filter = r'^bus-info: (?P<address>\w+:\w+:\w+\.\w+)$' - re_obj = re_search(regex_filter, out, re_M) - # if bus-info with PCI address found - if re_obj: - address = re_obj.groupdict().get('address', '') - return address - # use VPP - maybe interface already attached to it - vpp_control = VPPControl(attempts=20, interval=500) - pci_addr = vpp_control.get_pci_addr(iface) - if pci_addr: - return pci_addr - # raise error if PCI address was not found - raise ConfigError(f'Cannot find PCI address for interface {iface}') - - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base = ['vpp'] - base_ethernet = ['interfaces', 'ethernet'] - - # find interfaces removed from VPP - removed_ifaces = [] - tmp = node_changed(conf, base + ['interface']) - if tmp: - for removed_iface in tmp: - pci_address: str = _get_pci_address_by_interface(removed_iface) - removed_ifaces.append({ - 'iface_name': removed_iface, - 'iface_pci_addr': pci_address - }) - # add an interface to a list of interfaces that need - # to be reinitialized after the commit - set_dependents('ethernet', conf, removed_iface) - - if not conf.exists(base): - return {'removed_ifaces': removed_ifaces} - - config = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) - - if 'interface' in config: - for iface, iface_config in config['interface'].items(): - # add an interface to a list of interfaces that need - # to be reinitialized after the commit - set_dependents('ethernet', conf, iface) - - # Get PCI address auto - if iface_config['pci'] == 'auto': - config['interface'][iface]['pci'] = _get_pci_address_by_interface(iface) - - config['other_interfaces'] = conf.get_config_dict(base_ethernet, key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) - - if removed_ifaces: - config['removed_ifaces'] = removed_ifaces - - return config - - -def verify(config): - # bail out early - looks like removal from running config - if not config or (len(config) == 1 and 'removed_ifaces' in config): - return None - - if 'interface' not in config: - raise ConfigError('"interface" is required but not set!') - - if 'cpu' in config: - if 'corelist_workers' in config['cpu'] and 'main_core' not in config[ - 'cpu']: - raise ConfigError('"cpu main-core" is required but not set!') - - memory_available: int = virtual_memory().available - if memory_available < MIN_AVAILABLE_MEMORY: - raise ConfigError( - 'Not enough free memory to start VPP:\n' - f'available: {round(memory_available / 1024**3, 1)}GB\n' - f'required: {round(MIN_AVAILABLE_MEMORY / 1024**3, 1)}GB') - - -def generate(config): - if not config or (len(config) == 1 and 'removed_ifaces' in config): - # Remove old config and return - service_conf.unlink(missing_ok=True) - return None - - render(service_conf, 'vpp/startup.conf.j2', config) - render(systemd_override, 'vpp/override.conf.j2', config) - - # apply default sysctl values from - # https://github.com/FDio/vpp/blob/v23.06/src/vpp/conf/80-vpp.conf - sysctl_config: dict[str, str] = { - 'vm.nr_hugepages': '1024', - 'vm.max_map_count': '3096', - 'vm.hugetlb_shm_group': '0', - 'kernel.shmmax': '2147483648' - } - # we do not want to reduce `kernel.shmmax` - kernel_shmnax_current: str = sysctl_read('kernel.shmmax') - if int(kernel_shmnax_current) > int(sysctl_config['kernel.shmmax']): - sysctl_config['kernel.shmmax'] = kernel_shmnax_current - - if not sysctl_apply(sysctl_config): - raise ConfigError('Cannot configure sysctl parameters for VPP') - - return None - - -def apply(config): - if not config or (len(config) == 1 and 'removed_ifaces' in config): - call(f'systemctl stop {service_name}.service') - else: - call('systemctl daemon-reload') - call(f'systemctl restart {service_name}.service') - - # Initialize interfaces removed from VPP - for iface in config.get('removed_ifaces', []): - host_control = HostControl() - # rescan PCI to use a proper driver - host_control.pci_rescan(iface['iface_pci_addr']) - # rename to the proper name - iface_new_name: str = host_control.get_eth_name(iface['iface_pci_addr']) - host_control.rename_iface(iface_new_name, iface['iface_name']) - - if 'interface' in config: - # connect to VPP - # must be performed multiple attempts because API is not available - # immediately after the service restart - vpp_control = VPPControl(attempts=20, interval=500) - for iface, _ in config['interface'].items(): - # Create lcp - if iface not in Section.interfaces(): - vpp_control.lcp_pair_add(iface, iface) - - # reinitialize interfaces, but not during the first boot - if boot_configuration_complete(): - call_dependents() - - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) |