diff options
Diffstat (limited to 'src')
126 files changed, 4516 insertions, 1727 deletions
diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh new file mode 100755 index 000000000..75fb0cf44 --- /dev/null +++ b/src/completion/list_ddclient_protocols.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# +# 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/>. + +echo -n $(ddclient -list-protocols) diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py deleted file mode 100755 index b19b90156..000000000 --- a/src/completion/list_interfaces.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import sys -import argparse -from vyos.ifconfig import Section - -def matching(feature): - for section in Section.feature(feature): - for intf in Section.interfaces(section): - yield intf - -parser = argparse.ArgumentParser() -group = parser.add_mutually_exclusive_group() -group.add_argument("-t", "--type", type=str, help="List interfaces of specific type") -group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces") -group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces") -group.add_argument("-bo", "--bondable", action="store_true", help="List all bondable interfaces") - -args = parser.parse_args() - -if args.type: - try: - interfaces = Section.interfaces(args.type) - print(" ".join(interfaces)) - except ValueError as e: - print(e, file=sys.stderr) - print("") - -elif args.broadcast: - print(" ".join(matching("broadcast"))) - -elif args.bridgeable: - print(" ".join(matching("bridgeable"))) - -elif args.bondable: - # we need to filter out VLAN interfaces identified by a dot (.) in their name - print(" ".join([intf for intf in matching("bondable") if '.' not in intf])) - -else: - print(" ".join(Section.interfaces())) diff --git a/src/completion/list_ipsec_profile_tunnels.py b/src/completion/list_ipsec_profile_tunnels.py new file mode 100644 index 000000000..df6c52f6d --- /dev/null +++ b/src/completion/list_ipsec_profile_tunnels.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +# +# 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import sys +import argparse + +from vyos.config import Config +from vyos.util import dict_search + +def get_tunnels_from_ipsecprofile(profile): + config = Config() + base = ['vpn', 'ipsec', 'profile', profile, 'bind'] + profile_conf = config.get_config_dict(base, effective=True, key_mangling=('-', '_')) + tunnels = [] + + try: + for tunnel in (dict_search('bind.tunnel', profile_conf) or []): + tunnels.append(tunnel) + except: + pass + + return tunnels + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--profile", type=str, help="List tunnels per profile") + args = parser.parse_args() + + tunnels = [] + + tunnels = get_tunnels_from_ipsecprofile(args.profile) + + print(" ".join(tunnels)) + diff --git a/src/completion/list_ntp_servers.sh b/src/completion/list_ntp_servers.sh deleted file mode 100755 index d0977fbd6..000000000 --- a/src/completion/list_ntp_servers.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -# Completion script used to select specific NTP server -/bin/cli-shell-api -- listEffectiveNodes system ntp server | sed "s/'//g" diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 08861053d..aceb27fb0 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -16,18 +16,22 @@ import os +from hashlib import sha256 from ipaddress import ip_address from ipaddress import ip_network -from time import sleep from json import dumps as json_write from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configdict import node_changed +from vyos.configdict import is_node_changed +from vyos.configverify import verify_vrf +from vyos.ifconfig import Interface from vyos.util import call from vyos.util import cmd from vyos.util import run +from vyos.util import rc_cmd from vyos.util import write_file from vyos.template import inc_ip from vyos.template import is_ipv4 @@ -38,8 +42,9 @@ from vyos import ConfigError from vyos import airbag airbag.enable() -config_containers_registry = '/etc/containers/registries.conf' -config_containers_storage = '/etc/containers/storage.conf' +config_containers = '/etc/containers/containers.conf' +config_registry = '/etc/containers/registries.conf' +config_storage = '/etc/containers/storage.conf' systemd_unit_path = '/run/systemd/system' def _cmd(command): @@ -68,6 +73,9 @@ def get_config(config=None): # container base default values can not be merged here - remove and add them later if 'name' in default_values: del default_values['name'] + # registry will be handled below + if 'registry' in default_values: + del default_values['registry'] container = dict_merge(default_values, container) # Merge per-container default values @@ -80,20 +88,38 @@ def get_config(config=None): for name in container['name']: container['name'][name] = dict_merge(default_values, container['name'][name]) + # T5047: Any container related configuration changed? We only + # wan't to restart the required containers and not all of them ... + tmp = is_node_changed(conf, base + ['name', name]) + if tmp: + if 'container_restart' not in container: + container['container_restart'] = [name] + else: + container['container_restart'].append(name) + # XXX: T2665: we can not safely rely on the defaults() when there are # tagNodes in place, it is better to blend in the defaults manually. if 'port' in container['name'][name]: for port in container['name'][name]['port']: - default_values = defaults(base + ['name', 'port']) + default_values_port = defaults(base + ['name', 'port']) container['name'][name]['port'][port] = dict_merge( - default_values, container['name'][name]['port'][port]) + default_values_port, container['name'][name]['port'][port]) # XXX: T2665: we can not safely rely on the defaults() when there are # tagNodes in place, it is better to blend in the defaults manually. if 'volume' in container['name'][name]: for volume in container['name'][name]['volume']: - default_values = defaults(base + ['name', 'volume']) + default_values_volume = defaults(base + ['name', 'volume']) container['name'][name]['volume'][volume] = dict_merge( - default_values, container['name'][name]['volume'][volume]) + default_values_volume, container['name'][name]['volume'][volume]) + + # registry is a tagNode with default values - merge the list from + # default_values['registry'] into the tagNode variables + if 'registry' not in container: + container.update({'registry' : {}}) + default_values = defaults(base) + for registry in default_values['registry'].split(): + tmp = {registry : {}} + container['registry'] = dict_merge(tmp, container['registry']) # Delete container network, delete containers tmp = node_changed(conf, base + ['network']) @@ -142,21 +168,29 @@ def verify(container): raise ConfigError(f'Container network "{network_name}" does not exist!') if 'address' in container_config['network'][network_name]: - address = container_config['network'][network_name]['address'] - network = None - if is_ipv4(address): - network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0] - elif is_ipv6(address): - network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0] - - # Specified container IP address must belong to network prefix - if ip_address(address) not in ip_network(network): - raise ConfigError(f'Used container address "{address}" not in network "{network}"!') - - # We can not use the first IP address of a network prefix as this is used by podman - if ip_address(address) == ip_network(network)[1]: - raise ConfigError(f'IP address "{address}" can not be used for a container, '\ - 'reserved for the container engine!') + cnt_ipv4 = 0 + cnt_ipv6 = 0 + for address in container_config['network'][network_name]['address']: + network = None + if is_ipv4(address): + network = [x for x in container['network'][network_name]['prefix'] if is_ipv4(x)][0] + cnt_ipv4 += 1 + elif is_ipv6(address): + network = [x for x in container['network'][network_name]['prefix'] if is_ipv6(x)][0] + cnt_ipv6 += 1 + + # Specified container IP address must belong to network prefix + if ip_address(address) not in ip_network(network): + raise ConfigError(f'Used container address "{address}" not in network "{network}"!') + + # We can not use the first IP address of a network prefix as this is used by podman + if ip_address(address) == ip_network(network)[1]: + raise ConfigError(f'IP address "{address}" can not be used for a container, '\ + 'reserved for the container engine!') + + if cnt_ipv4 > 1 or cnt_ipv6 > 1: + raise ConfigError(f'Only one IP address per address family can be used for '\ + f'container "{name}". {cnt_ipv4} IPv4 and {cnt_ipv6} IPv6 address(es)!') if 'device' in container_config: for dev, dev_config in container_config['device'].items(): @@ -218,6 +252,8 @@ def verify(container): if v6_prefix > 1: raise ConfigError(f'Only one IPv6 prefix can be defined for network "{network}"!') + # Verify VRF exists + verify_vrf(network_config) # A network attached to a container can not be deleted if {'network_remove', 'name'} <= set(container): @@ -226,6 +262,13 @@ def verify(container): if 'network' in container_config and network in container_config['network']: raise ConfigError(f'Can not remove network "{network}", used by container "{container}"!') + if 'registry' in container: + for registry, registry_config in container['registry'].items(): + if 'authentication' not in registry_config: + continue + if not {'username', 'password'} <= set(registry_config['authentication']): + raise ConfigError('If registry username or or password is defined, so must be the other!') + return None def generate_run_arguments(name, container_config): @@ -256,6 +299,11 @@ def generate_run_arguments(name, container_config): for k, v in container_config['environment'].items(): env_opt += f" --env \"{k}={v['value']}\"" + hostname = '' + if 'host_name' in container_config: + hostname = container_config['host_name'] + hostname = f'--hostname {hostname}' + # Publish ports port = '' if 'port' in container_config: @@ -277,62 +325,96 @@ def generate_run_arguments(name, container_config): container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ - f'--name {name} {device} {port} {volume} {env_opt}' + f'--name {name} {hostname} {device} {port} {volume} {env_opt}' + + entrypoint = '' + if 'entrypoint' in container_config: + # it needs to be json-formatted with single quote on the outside + entrypoint = json_write(container_config['entrypoint'].split()).replace('"', """) + entrypoint = f'--entrypoint '{entrypoint}'' + + hostname = '' + if 'host_name' in container_config: + hostname = container_config['host_name'] + hostname = f'--hostname {hostname}' + + command = '' + if 'command' in container_config: + command = container_config['command'].strip() + + command_arguments = '' + if 'arguments' in container_config: + command_arguments = container_config['arguments'].strip() if 'allow_host_networks' in container_config: - return f'{container_base_cmd} --net host {image}' + return f'{container_base_cmd} --net host {entrypoint} {image} {command} {command_arguments}'.strip() ip_param = '' networks = ",".join(container_config['network']) for network in container_config['network']: - if 'address' in container_config['network'][network]: - address = container_config['network'][network]['address'] - ip_param = f'--ip {address}' + if 'address' not in container_config['network'][network]: + continue + for address in container_config['network'][network]['address']: + if is_ipv6(address): + ip_param += f' --ip6 {address}' + else: + ip_param += f' --ip {address}' - return f'{container_base_cmd} --net {networks} {ip_param} {image}' + return f'{container_base_cmd} --net {networks} {ip_param} {entrypoint} {image} {command} {command_arguments}'.strip() def generate(container): # bail out early - looks like removal from running config if not container: - if os.path.exists(config_containers_registry): - os.unlink(config_containers_registry) - if os.path.exists(config_containers_storage): - os.unlink(config_containers_storage) + for file in [config_containers, config_registry, config_storage]: + if os.path.exists(file): + os.unlink(file) return None if 'network' in container: for network, network_config in container['network'].items(): tmp = { - 'cniVersion' : '0.4.0', - 'name' : network, - 'plugins' : [{ - 'type': 'bridge', - 'bridge': f'cni-{network}', - 'isGateway': True, - 'ipMasq': False, - 'hairpinMode': False, - 'ipam' : { - 'type': 'host-local', - 'routes': [], - 'ranges' : [], - }, - }] + 'name': network, + 'id' : sha256(f'{network}'.encode()).hexdigest(), + 'driver': 'bridge', + 'network_interface': f'pod-{network}', + 'subnets': [], + 'ipv6_enabled': False, + 'internal': False, + 'dns_enabled': True, + 'ipam_options': { + 'driver': 'host-local' + } } - for prefix in network_config['prefix']: - net = [{'gateway' : inc_ip(prefix, 1), 'subnet' : prefix}] - tmp['plugins'][0]['ipam']['ranges'].append(net) + net = {'subnet' : prefix, 'gateway' : inc_ip(prefix, 1)} + tmp['subnets'].append(net) - # install per address-family default orutes - default_route = '0.0.0.0/0' if is_ipv6(prefix): - default_route = '::/0' - tmp['plugins'][0]['ipam']['routes'].append({'dst': default_route}) + tmp['ipv6_enabled'] = True - write_file(f'/etc/cni/net.d/{network}.conflist', json_write(tmp, indent=2)) + write_file(f'/etc/containers/networks/{network}.json', json_write(tmp, indent=2)) - render(config_containers_registry, 'container/registries.conf.j2', container) - render(config_containers_storage, 'container/storage.conf.j2', container) + if 'registry' in container: + cmd = f'podman logout --all' + rc, out = rc_cmd(cmd) + if rc != 0: + raise ConfigError(out) + + for registry, registry_config in container['registry'].items(): + if 'disable' in registry_config: + continue + if 'authentication' in registry_config: + if {'username', 'password'} <= set(registry_config['authentication']): + username = registry_config['authentication']['username'] + password = registry_config['authentication']['password'] + cmd = f'podman login --username {username} --password {password} {registry}' + rc, out = rc_cmd(cmd) + if rc != 0: + raise ConfigError(out) + + render(config_containers, 'container/containers.conf.j2', container) + render(config_registry, 'container/registries.conf.j2', container) + render(config_storage, 'container/storage.conf.j2', container) if 'name' in container: for name, container_config in container['name'].items(): @@ -341,7 +423,8 @@ def generate(container): file_path = os.path.join(systemd_unit_path, f'vyos-container-{name}.service') run_args = generate_run_arguments(name, container_config) - render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args}) + render(file_path, 'container/systemd-unit.j2', {'name': name, 'run_args': run_args,}, + formater=lambda _: _.replace(""", '"').replace("'", "'")) return None @@ -360,10 +443,7 @@ def apply(container): # Delete old networks if needed if 'network_remove' in container: for network in container['network_remove']: - call(f'podman network rm {network}') - tmp = f'/etc/cni/net.d/{network}.conflist' - if os.path.exists(tmp): - os.unlink(tmp) + call(f'podman network rm {network} >/dev/null 2>&1') # Add container disabled_new = False @@ -387,11 +467,26 @@ def apply(container): os.unlink(file_path) continue - cmd(f'systemctl restart vyos-container-{name}.service') + if 'container_restart' in container and name in container['container_restart']: + cmd(f'systemctl restart vyos-container-{name}.service') if disabled_new: call('systemctl daemon-reload') + # Start network and assign it to given VRF if requested. this can only be done + # after the containers got started as the podman network interface will + # only be enabled by the first container and yet I do not know how to enable + # the network interface in advance + if 'network' in container: + for network, network_config in container['network'].items(): + network_name = f'pod-{network}' + # T5147: Networks are started only as soon as there is a consumer. + # If only a network is created in the first place, no need to assign + # it to a VRF as there's no consumer, yet. + if os.path.exists(f'/sys/class/net/{network_name}'): + tmp = Interface(network_name) + tmp.set_vrf(network_config.get('vrf', '')) + return None if __name__ == '__main__': diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py index 4de2ca2f3..7e702a446 100755 --- a/src/conf_mode/dhcp_relay.py +++ b/src/conf_mode/dhcp_relay.py @@ -18,9 +18,11 @@ import os from sys import exit +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render +from vyos.base import Warning from vyos.util import call from vyos.util import dict_search from vyos.xml import defaults @@ -59,6 +61,19 @@ def verify(relay): raise ConfigError('No DHCP relay server(s) configured.\n' \ 'At least one DHCP relay server required.') + if 'interface' in relay: + Warning('DHCP relay interface is DEPRECATED - please use upstream-interface and listen-interface instead!') + if 'upstream_interface' in relay or 'listen_interface' in relay: + raise ConfigError('<interface> configuration is not compatible with upstream/listen interface') + else: + Warning('<interface> is going to be deprecated.\n' \ + 'Please use <listen-interface> and <upstream-interface>') + + if 'upstream_interface' in relay and 'listen_interface' not in relay: + raise ConfigError('No listen-interface configured') + if 'listen_interface' in relay and 'upstream_interface' not in relay: + raise ConfigError('No upstream-interface configured') + return None def generate(relay): diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 52b682d6d..2b2af252d 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -247,7 +247,7 @@ def verify(dhcp): net2 = ip_network(n) if (net != net2): if net.overlaps(net2): - raise ConfigError('Conflicting subnet ranges: "{net}" overlaps "{net2}"!') + raise ConfigError(f'Conflicting subnet ranges: "{net}" overlaps "{net2}"!') # Prevent 'disable' for shared-network if only one network is configured if (shared_networks - disabled_shared_networks) < 1: @@ -283,7 +283,7 @@ def generate(dhcp): if not dhcp or 'disable' in dhcp: return None - # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw + # Please see: https://vyos.dev/T1129 for quoting of the raw # parameters we can pass to ISC DHCPd tmp_file = '/tmp/dhcpd.conf' render(tmp_file, 'dhcp-server/dhcpd.conf.j2', dhcp, diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index d0d87d73e..0d86c6a52 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -24,7 +24,7 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.hostsd_client import Client as hostsd_client from vyos.template import render -from vyos.template import is_ipv6 +from vyos.template import bracketize_ipv6 from vyos.util import call from vyos.util import chown from vyos.util import dict_search @@ -58,8 +58,26 @@ def get_config(config=None): default_values = defaults(base) # T2665 due to how defaults under tag nodes work, we must clear these out before we merge del default_values['authoritative_domain'] + del default_values['name_server'] + del default_values['domain']['name_server'] dns = dict_merge(default_values, dns) + # T2665: we cleared default values for tag node 'name_server' above. + # We now need to add them back back in a granular way. + if 'name_server' in dns: + default_values = defaults(base + ['name-server']) + for server in dns['name_server']: + dns['name_server'][server] = dict_merge(default_values, dns['name_server'][server]) + + # T2665: we cleared default values for tag node 'domain' above. + # We now need to add them back back in a granular way. + if 'domain' in dns: + default_values = defaults(base + ['domain', 'name-server']) + for domain in dns['domain'].keys(): + for server in dns['domain'][domain]['name_server']: + dns['domain'][domain]['name_server'][server] = dict_merge( + default_values, dns['domain'][domain]['name_server'][server]) + # some additions to the default dictionary if 'system' in dns: base_nameservers = ['system', 'name-server'] @@ -81,7 +99,7 @@ def get_config(config=None): recorddata = zonedata['records'] - for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]: + for rtype in [ 'a', 'aaaa', 'cname', 'mx', 'ns', 'ptr', 'txt', 'spf', 'srv', 'naptr' ]: if rtype not in recorddata: continue for subnode in recorddata[rtype]: @@ -95,7 +113,7 @@ def get_config(config=None): rdata = dict_merge(rdefaults, rdata) if not 'address' in rdata: - dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one address is required') continue if subnode == 'any': @@ -108,12 +126,12 @@ def get_config(config=None): 'ttl': rdata['ttl'], 'value': address }) - elif rtype in ['cname', 'ptr']: + elif rtype in ['cname', 'ptr', 'ns']: rdefaults = defaults(base + ['authoritative-domain', 'records', rtype]) # T2665 rdata = dict_merge(rdefaults, rdata) if not 'target' in rdata: - dns['authoritative_zone_errors'].append('{}.{}: target is required'.format(subnode, node)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: target is required') continue zone['records'].append({ @@ -128,7 +146,7 @@ def get_config(config=None): rdata = dict_merge(rdefaults, rdata) if not 'server' in rdata: - dns['authoritative_zone_errors'].append('{}.{}: at least one server is required'.format(subnode, node)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one server is required') continue for servername in rdata['server']: @@ -146,7 +164,7 @@ def get_config(config=None): rdata = dict_merge(rdefaults, rdata) if not 'value' in rdata: - dns['authoritative_zone_errors'].append('{}.{}: at least one value is required'.format(subnode, node)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one value is required') continue for value in rdata['value']: @@ -161,7 +179,7 @@ def get_config(config=None): rdata = dict_merge(rdefaults, rdata) if not 'value' in rdata: - dns['authoritative_zone_errors'].append('{}.{}: value is required'.format(subnode, node)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: value is required') continue zone['records'].append({ @@ -176,7 +194,7 @@ def get_config(config=None): rdata = dict_merge(rdefaults, rdata) if not 'entry' in rdata: - dns['authoritative_zone_errors'].append('{}.{}: at least one entry is required'.format(subnode, node)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one entry is required') continue for entryno in rdata['entry']: @@ -185,11 +203,11 @@ def get_config(config=None): entrydata = dict_merge(entrydefaults, entrydata) if not 'hostname' in entrydata: - dns['authoritative_zone_errors'].append('{}.{}: hostname is required for entry {}'.format(subnode, node, entryno)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: hostname is required for entry {entryno}') continue if not 'port' in entrydata: - dns['authoritative_zone_errors'].append('{}.{}: port is required for entry {}'.format(subnode, node, entryno)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: port is required for entry {entryno}') continue zone['records'].append({ @@ -205,7 +223,7 @@ def get_config(config=None): if not 'rule' in rdata: - dns['authoritative_zone_errors'].append('{}.{}: at least one rule is required'.format(subnode, node)) + dns['authoritative_zone_errors'].append(f'{subnode}.{node}: at least one rule is required') continue for ruleno in rdata['rule']: @@ -263,7 +281,7 @@ def verify(dns): # as a domain will contains dot's which is out dictionary delimiter. if 'domain' in dns: for domain in dns['domain']: - if 'server' not in dns['domain'][domain]: + if 'name_server' not in dns['domain'][domain]: raise ConfigError(f'No server configured for domain {domain}!') if 'dns64_prefix' in dns: @@ -329,7 +347,12 @@ def apply(dns): # sources hc.delete_name_servers([hostsd_tag]) if 'name_server' in dns: - hc.add_name_servers({hostsd_tag: dns['name_server']}) + # 'name_server' is of the form + # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...} + # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...] + nslist = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p) + for (h, p) in dns['name_server'].items()] + hc.add_name_servers({hostsd_tag: nslist}) # delete all nameserver tags hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor()) @@ -358,7 +381,14 @@ def apply(dns): # the list and keys() are required as get returns a dict, not list hc.delete_forward_zones(list(hc.get_forward_zones().keys())) if 'domain' in dns: - hc.add_forward_zones(dns['domain']) + zones = dns['domain'] + for domain in zones.keys(): + # 'name_server' is of the form + # {'192.0.2.1': {'port': 53}, '2001:db8::1': {'port': 853}, ...} + # canonicalize them as ['192.0.2.1:53', '[2001:db8::1]:853', ...] + zones[domain]['name_server'] = [(lambda h, p: f"{bracketize_ipv6(h)}:{p['port']}")(h, p) + for (h, p) in zones[domain]['name_server'].items()] + hc.add_forward_zones(zones) # hostsd generates NTAs for the authoritative zones # the list and keys() are required as get returns a dict, not list diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 06a2f7e15..426e3d693 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -108,7 +108,8 @@ def verify(dyndns): raise ConfigError(f'"host-name" {error_msg}') if 'login' not in config: - raise ConfigError(f'"login" (username) {error_msg}') + if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'): + raise ConfigError(f'"login" (username) {error_msg}, unless using CloudFlare') if 'password' not in config: raise ConfigError(f'"password" {error_msg}') diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 20cf1ead1..190587980 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -197,6 +197,15 @@ def verify_rule(firewall, rule_conf, ipv6): if target not in dict_search_args(firewall, 'ipv6_name'): raise ConfigError(f'Invalid jump-target. Firewall ipv6-name {target} does not exist on the system') + if 'queue_options' in rule_conf: + if 'queue' not in rule_conf['action']: + raise ConfigError('queue-options defined, but action queue needed and it is not defined') + if 'fanout' in rule_conf['queue_options'] and ('queue' not in rule_conf or '-' not in rule_conf['queue']): + raise ConfigError('queue-options fanout defined, then queue needs to be defined as a range') + + if 'queue' in rule_conf and 'queue' not in rule_conf['action']: + raise ConfigError('queue defined, but action queue needed and it is not defined') + if 'fragment' in rule_conf: if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']): raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"') @@ -273,6 +282,19 @@ def verify_rule(firewall, rule_conf, ipv6): if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group') + if 'port' in side_conf and dict_search_args(side_conf, 'group', 'port_group'): + raise ConfigError(f'{side} port-group and port cannot both be defined') + + if 'log_options' in rule_conf: + if 'log' not in rule_conf or 'enable' not in rule_conf['log']: + raise ConfigError('log-options defined, but log is not enable') + + if 'snapshot_length' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']: + raise ConfigError('log-options snapshot-length defined, but log group is not define') + + if 'queue_threshold' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']: + raise ConfigError('log-options queue-threshold defined, but log group is not define') + def verify_nested_group(group_name, group, groups, seen): if 'include' not in group: return diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index bc3e67b40..e18b426b1 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -21,6 +21,7 @@ from ipaddress import ip_interface from ipaddress import IPv4Interface from ipaddress import IPv6Interface +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.ifconfig.vrrp import VRRP @@ -28,6 +29,7 @@ from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call +from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError from vyos import airbag @@ -49,12 +51,27 @@ def get_config(config=None): # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. if 'vrrp' in ha: + if dict_search('vrrp.global_parameters.garp', ha) != None: + default_values = defaults(base_vrrp + ['global-parameters', 'garp']) + ha['vrrp']['global_parameters']['garp'] = dict_merge( + default_values, ha['vrrp']['global_parameters']['garp']) + if 'group' in ha['vrrp']: - default_values_vrrp = defaults(base_vrrp + ['group']) - if 'garp' in default_values_vrrp: - del default_values_vrrp['garp'] + default_values = defaults(base_vrrp + ['group']) + default_values_garp = defaults(base_vrrp + ['group', 'garp']) + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'garp' in default_values: + del default_values['garp'] for group in ha['vrrp']['group']: - ha['vrrp']['group'][group] = dict_merge(default_values_vrrp, ha['vrrp']['group'][group]) + ha['vrrp']['group'][group] = dict_merge(default_values, ha['vrrp']['group'][group]) + + # XXX: T2665: we can not safely rely on the defaults() when there are + # tagNodes in place, it is better to blend in the defaults manually. + if 'garp' in ha['vrrp']['group'][group]: + ha['vrrp']['group'][group]['garp'] = dict_merge( + default_values_garp, ha['vrrp']['group'][group]['garp']) # Merge per virtual-server default values if 'virtual_server' in ha: @@ -70,7 +87,7 @@ def get_config(config=None): return ha def verify(ha): - if not ha: + if not ha or 'disable' in ha: return None used_vrid_if = [] @@ -90,6 +107,18 @@ def verify(ha): if not {'password', 'type'} <= set(group_config['authentication']): raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"') + if 'health_check' in group_config: + health_check_types = ["script", "ping"] + from vyos.utils.dict import check_mutually_exclusive_options + try: + check_mutually_exclusive_options(group_config["health_check"], health_check_types, required=True) + except ValueError as e: + Warning(f'Health check configuration for VRRP group "{group}" will remain unused ' \ + f'until it has one of the following options: {health_check_types}') + # XXX: health check has default options so we need to remove it + # to avoid generating useless config statements in keepalived.conf + del group_config["health_check"] + # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction # We also need to make sure VRID is not used twice on the same interface with the # same address family. @@ -159,7 +188,7 @@ def verify(ha): def generate(ha): - if not ha: + if not ha or 'disable' in ha: return None render(VRRP.location['config'], 'high-availability/keepalived.conf.j2', ha) @@ -167,7 +196,7 @@ def generate(ha): def apply(ha): service_name = 'keepalived.service' - if not ha: + if not ha or 'disable' in ha: call(f'systemctl stop {service_name}') return None diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 6328294c1..7e801eb26 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -79,9 +79,10 @@ def get_config(config=None): # http-api.conf format for api_keys: if 'keys' in api_dict: api_dict['api_keys'] = [] - for el in list(api_dict['keys']['id']): - key = api_dict['keys']['id'][el]['key'] - api_dict['api_keys'].append({'id': el, 'key': key}) + for el in list(api_dict['keys'].get('id', {})): + key = api_dict['keys']['id'][el].get('key', '') + if key: + api_dict['api_keys'].append({'id': el, 'key': key}) del api_dict['keys'] # Do we run inside a VRF context? diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index ce5e63928..b0c38e8d3 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -159,6 +159,8 @@ def generate(https): server_block['port'] = data.get('listen-port', '443') name = data.get('server-name', ['_']) server_block['name'] = name + allow_client = data.get('allow-client', {}) + server_block['allow_client'] = allow_client.get('address', []) server_block_list.append(server_block) # get certificate data diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 8155f36c2..6f227b0d1 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -597,7 +597,7 @@ def generate_pki_files(openvpn): def generate(openvpn): interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) - plugin_dir = '/usr/lib/openvpn' + openvpn['plugin_dir'] = '/usr/lib/openvpn' # create base config directory on demand makedir(directory, user, group) # enforce proper permissions on /run/openvpn @@ -645,7 +645,7 @@ def generate(openvpn): user=user, group=group) # we need to support quoting of raw parameters from OpenVPN CLI - # see https://phabricator.vyos.net/T1632 + # see https://vyos.dev/T1632 render(cfg_file.format(**openvpn), 'openvpn/server.conf.j2', openvpn, formater=lambda _: _.replace(""", '"'), user=user, group=group) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index e2701d9d3..0a3726e94 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -136,7 +136,7 @@ def verify(tunnel): if our_key != None: if their_address == our_address and their_key == our_key: raise ConfigError(f'Key "{our_key}" for source-address "{our_address}" ' \ - f'is already used for tunnel "{tunnel_if}"!') + f'is already used for tunnel "{o_tunnel}"!') else: our_source_if = dict_search('source_interface', tunnel) their_source_if = dict_search('source_interface', o_tunnel_conf) diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index a14a992ae..9ca495476 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -171,7 +171,7 @@ def apply(wwan): options = f'ip-type={ip_type},apn=' + wwan['apn'] if 'authentication' in wwan: - options += ',user={user},password={password}'.format(**wwan['authentication']) + options += ',user={username},password={password}'.format(**wwan['authentication']) command = f'{base_cmd} --simple-connect="{options}"' call(command, stdout=DEVNULL) diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py index 11840249f..7086aaf8b 100755 --- a/src/conf_mode/load-balancing-wan.py +++ b/src/conf_mode/load-balancing-wan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,17 +14,25 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os from sys import exit +from shutil import rmtree +from vyos.base import Warning from vyos.config import Config -from vyos.configdict import node_changed -from vyos.util import call +from vyos.configdict import dict_merge +from vyos.util import cmd +from vyos.template import render +from vyos.xml import defaults from vyos import ConfigError -from pprint import pprint from vyos import airbag airbag.enable() +load_balancing_dir = '/run/load-balance' +load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf' +systemd_service = 'vyos-wan-load-balance.service' + def get_config(config=None): if config: @@ -33,27 +41,135 @@ def get_config(config=None): conf = Config() base = ['load-balancing', 'wan'] - lb = conf.get_config_dict(base, get_first_key=True, - no_tag_node_value_mangle=True) + lb = conf.get_config_dict(base, + get_first_key=True, + key_mangling=('-', '_'), + no_tag_node_value_mangle=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + # lb base default values can not be merged here - remove and add them later + if 'interface_health' in default_values: + del default_values['interface_health'] + if 'rule' in default_values: + del default_values['rule'] + lb = dict_merge(default_values, lb) + + if 'interface_health' in lb: + for iface in lb.get('interface_health'): + default_values_iface = defaults(base + ['interface-health']) + if 'test' in default_values_iface: + del default_values_iface['test'] + lb['interface_health'][iface] = dict_merge( + default_values_iface, lb['interface_health'][iface]) + if 'test' in lb['interface_health'][iface]: + for node_test in lb['interface_health'][iface]['test']: + default_values_test = defaults(base + + ['interface-health', 'test']) + lb['interface_health'][iface]['test'][node_test] = dict_merge( + default_values_test, + lb['interface_health'][iface]['test'][node_test]) + + if 'rule' in lb: + for rule in lb.get('rule'): + default_values_rule = defaults(base + ['rule']) + if 'interface' in default_values_rule: + del default_values_rule['interface'] + lb['rule'][rule] = dict_merge(default_values_rule, lb['rule'][rule]) + if not conf.exists(base + ['rule', rule, 'limit']): + del lb['rule'][rule]['limit'] + if 'interface' in lb['rule'][rule]: + for iface in lb['rule'][rule]['interface']: + default_values_rule_iface = defaults(base + ['rule', 'interface']) + lb['rule'][rule]['interface'][iface] = dict_merge(default_values_rule_iface, lb['rule'][rule]['interface'][iface]) - pprint(lb) return lb + def verify(lb): - return None + if not lb: + return None + + if 'interface_health' not in lb: + raise ConfigError( + 'A valid WAN load-balance configuration requires an interface with a nexthop!' + ) + + for interface, interface_config in lb['interface_health'].items(): + if 'nexthop' not in interface_config: + raise ConfigError( + f'interface-health {interface} nexthop must be specified!') + + if 'test' in interface_config: + for test_rule, test_config in interface_config['test'].items(): + if 'type' in test_config: + if test_config['type'] == 'user-defined' and 'test_script' not in test_config: + raise ConfigError( + f'test {test_rule} script must be defined for test-script!' + ) + + if 'rule' not in lb: + Warning( + 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!' + ) + else: + for rule, rule_config in lb['rule'].items(): + if 'inbound_interface' not in rule_config: + raise ConfigError(f'rule {rule} inbound-interface must be specified!') + if {'failover', 'exclude'} <= set(rule_config): + raise ConfigError(f'rule {rule} failover cannot be configured with exclude!') + if {'limit', 'exclude'} <= set(rule_config): + raise ConfigError(f'rule {rule} limit cannot be used with exclude!') + if 'interface' not in rule_config: + if 'exclude' not in rule_config: + Warning( + f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule' + ) + for direction in {'source', 'destination'}: + if direction in rule_config: + if 'protocol' in rule_config and 'port' in rule_config[ + direction]: + if rule_config['protocol'] not in {'tcp', 'udp'}: + raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"') def generate(lb): if not lb: + # Delete /run/load-balance/wlb.conf + if os.path.isfile(load_balancing_conf_file): + os.unlink(load_balancing_conf_file) + # Delete old directories + if os.path.isdir(load_balancing_dir): + rmtree(load_balancing_dir, ignore_errors=True) + if os.path.exists('/var/run/load-balance/wlb.out'): + os.unlink('/var/run/load-balance/wlb.out') + return None + # Create load-balance dir + if not os.path.isdir(load_balancing_dir): + os.mkdir(load_balancing_dir) + + render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb) + return None def apply(lb): + if not lb: + try: + cmd(f'systemctl stop {systemd_service}') + except Exception as e: + print(f"Error message: {e}") + + else: + cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1') + cmd(f'systemctl restart {systemd_service}') return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/protocols_babel.py b/src/conf_mode/protocols_babel.py new file mode 100755 index 000000000..20821c7f2 --- /dev/null +++ b/src/conf_mode/protocols_babel.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021-2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import node_changed +from vyos.configverify import verify_common_route_maps +from vyos.configverify import verify_access_list +from vyos.configverify import verify_prefix_list +from vyos.util import dict_search +from vyos.xml import defaults +from vyos.template import render_to_string +from vyos import ConfigError +from vyos import frr +from vyos import airbag +airbag.enable() + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['protocols', 'babel'] + babel = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + # FRR has VRF support for different routing daemons. As interfaces belong + # to VRFs - or the global VRF, we need to check for changed interfaces so + # that they will be properly rendered for the FRR config. Also this eases + # removal of interfaces from the running configuration. + interfaces_removed = node_changed(conf, base + ['interface']) + if interfaces_removed: + babel['interface_removed'] = list(interfaces_removed) + + # Bail out early if configuration tree does not exist + if not conf.exists(base): + babel.update({'deleted' : ''}) + return babel + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + + # XXX: T2665: we currently have no nice way for defaults under tag nodes, + # clean them out and add them manually :( + del default_values['interface'] + + # merge in remaining default values + babel = dict_merge(default_values, babel) + + # We also need some additional information from the config, prefix-lists + # and route-maps for instance. They will be used in verify(). + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = conf.get_config_dict(['policy']) + # Merge policy dict into "regular" config dict + babel = dict_merge(tmp, babel) + return babel + +def verify(babel): + if not babel: + return None + + # verify distribute_list + if "distribute_list" in babel: + acl_keys = { + "ipv4": [ + "distribute_list.ipv4.access_list.in", + "distribute_list.ipv4.access_list.out", + ], + "ipv6": [ + "distribute_list.ipv6.access_list.in", + "distribute_list.ipv6.access_list.out", + ] + } + prefix_list_keys = { + "ipv4": [ + "distribute_list.ipv4.prefix_list.in", + "distribute_list.ipv4.prefix_list.out", + ], + "ipv6":[ + "distribute_list.ipv6.prefix_list.in", + "distribute_list.ipv6.prefix_list.out", + ] + } + for address_family in ["ipv4", "ipv6"]: + for iface_key in babel["distribute_list"].get(address_family, {}).get("interface", {}).keys(): + acl_keys[address_family].extend([ + f"distribute_list.{address_family}.interface.{iface_key}.access_list.in", + f"distribute_list.{address_family}.interface.{iface_key}.access_list.out" + ]) + prefix_list_keys[address_family].extend([ + f"distribute_list.{address_family}.interface.{iface_key}.prefix_list.in", + f"distribute_list.{address_family}.interface.{iface_key}.prefix_list.out" + ]) + + for address_family, keys in acl_keys.items(): + for key in keys: + acl = dict_search(key, babel) + if acl: + verify_access_list(acl, babel, version='6' if address_family == 'ipv6' else '') + + for address_family, keys in prefix_list_keys.items(): + for key in keys: + prefix_list = dict_search(key, babel) + if prefix_list: + verify_prefix_list(prefix_list, babel, version='6' if address_family == 'ipv6' else '') + + +def generate(babel): + if not babel or 'deleted' in babel: + return None + + babel['new_frr_config'] = render_to_string('frr/babeld.frr.j2', babel) + return None + +def apply(babel): + babel_daemon = 'babeld' + + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + frr_cfg.load_configuration(babel_daemon) + frr_cfg.modify_section('^router babel', stop_pattern='^exit', remove_stop_mark=True) + + for key in ['interface', 'interface_removed']: + if key not in babel: + continue + for interface in babel[key]: + frr_cfg.modify_section(f'^interface {interface}', stop_pattern='^exit', remove_stop_mark=True) + + if 'new_frr_config' in babel: + frr_cfg.add_before(frr.default_add_before, babel['new_frr_config']) + frr_cfg.commit_configuration(babel_daemon) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index c410258ee..b23584bdb 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -50,16 +50,24 @@ def get_config(config=None): bgp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - # Assign the name of our VRF context. This MUST be done before the return - # statement below, else on deletion we will delete the default instance - # instead of the VRF instance. - if vrf: bgp.update({'vrf' : vrf}) - bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + # Assign the name of our VRF context. This MUST be done before the return + # statement below, else on deletion we will delete the default instance + # instead of the VRF instance. + if vrf: + bgp.update({'vrf' : vrf}) + # We can not delete the BGP VRF instance if there is a L3VNI configured + tmp = ['vrf', 'name', vrf, 'vni'] + if conf.exists(tmp): + bgp.update({'vni' : conf.return_value(tmp)}) + # We can safely delete ourself from the dependent vrf list + if vrf in bgp['dependent_vrfs']: + del bgp['dependent_vrfs'][vrf] + bgp['dependent_vrfs'].update({'default': {'protocols': { 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'), get_first_key=True, @@ -202,9 +210,13 @@ def verify(bgp): if 'vrf' in bgp: # Cannot delete vrf if it exists in import vrf list in other vrfs for tmp_afi in ['ipv4_unicast', 'ipv6_unicast']: - if verify_vrf_as_import(bgp['vrf'],tmp_afi,bgp['dependent_vrfs']): - raise ConfigError(f'Cannot delete vrf {bgp["vrf"]} instance, ' \ - 'Please unconfigure import vrf commands!') + if verify_vrf_as_import(bgp['vrf'], tmp_afi, bgp['dependent_vrfs']): + raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \ + 'unconfigure "import vrf" commands!') + # We can not delete the BGP instance if a L3VNI instance exists + if 'vni' in bgp: + raise ConfigError(f'Cannot delete VRF instance "{bgp["vrf"]}", ' \ + f'unconfigure VNI "{bgp["vni"]}" first!') else: # We are running in the default VRF context, thus we can not delete # our main BGP instance if there are dependent BGP VRF instances. @@ -235,6 +247,11 @@ def verify(bgp): raise ConfigError(f'Specified peer-group "{peer_group}" for '\ f'neighbor "{neighbor}" does not exist!') + if 'local_role' in peer_config: + #Ensure Local Role has only one value. + if len(peer_config['local_role']) > 1: + raise ConfigError(f'Only one local role can be specified for peer "{peer}"!') + if 'local_as' in peer_config: if len(peer_config['local_as']) > 1: raise ConfigError(f'Only one local-as number can be specified for peer "{peer}"!') @@ -407,6 +424,11 @@ def verify(bgp): raise ConfigError('Missing mandatory configuration option for '\ f'global administrative distance {key}!') + # TCP keepalive requires all three parameters to be set + if dict_search('parameters.tcp_keepalive', bgp) != None: + if not {'idle', 'interval', 'probes'} <= set(bgp['parameters']['tcp_keepalive']): + raise ConfigError('TCP keepalive incomplete - idle, keepalive and probes must be set') + # Address Family specific validation if 'address_family' in bgp: for afi, afi_config in bgp['address_family'].items(): @@ -419,7 +441,6 @@ def verify(bgp): f'{afi} administrative distance {key}!') if afi in ['ipv4_unicast', 'ipv6_unicast']: - vrf_name = bgp['vrf'] if dict_search('vrf', bgp) else 'default' # Verify if currant VRF contains rd and route-target options # and does not exist in import list in other VRFs @@ -468,32 +489,30 @@ def verify(bgp): tmp = dict_search(f'route_map.vpn.{export_import}', afi_config) if tmp: verify_route_map(tmp, bgp) + # Checks only required for L2VPN EVPN + if afi in ['l2vpn_evpn']: + if 'vni' in afi_config: + for vni, vni_config in afi_config['vni'].items(): + if 'rd' in vni_config and 'advertise_all_vni' not in afi_config: + raise ConfigError('BGP EVPN "rd" requires "advertise-all-vni" to be set!') + if 'route_target' in vni_config and 'advertise_all_vni' not in afi_config: + raise ConfigError('BGP EVPN "route-target" requires "advertise-all-vni" to be set!') + return None def generate(bgp): if not bgp or 'deleted' in bgp: return None - bgp['protocol'] = 'bgp' # required for frr/vrf.route-map.frr.j2 - bgp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', bgp) bgp['frr_bgpd_config'] = render_to_string('frr/bgpd.frr.j2', bgp) - return None def apply(bgp): bgp_daemon = 'bgpd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'(\s+)?ip protocol bgp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in bgp: - frr_cfg.add_before(frr.default_add_before, bgp['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' diff --git a/src/conf_mode/protocols_eigrp.py b/src/conf_mode/protocols_eigrp.py index c1a1a45e1..609b39065 100755 --- a/src/conf_mode/protocols_eigrp.py +++ b/src/conf_mode/protocols_eigrp.py @@ -69,8 +69,6 @@ def get_config(config=None): # Merge policy dict into "regular" config dict eigrp = dict_merge(tmp, eigrp) - import pprint - pprint.pprint(eigrp) return eigrp def verify(eigrp): @@ -80,24 +78,14 @@ def generate(eigrp): if not eigrp or 'deleted' in eigrp: return None - eigrp['protocol'] = 'eigrp' # required for frr/vrf.route-map.frr.j2 - eigrp['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', eigrp) eigrp['frr_eigrpd_config'] = render_to_string('frr/eigrpd.frr.j2', eigrp) def apply(eigrp): eigrp_daemon = 'eigrpd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'(\s+)?ip protocol eigrp route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in eigrp: - frr_cfg.add_before(frr.default_add_before, eigrp['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index cb8ea3be4..ecca87db0 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -129,7 +129,7 @@ def verify(isis): vrf = isis['vrf'] tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: - raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') + raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') # If md5 and plaintext-password set at the same time for password in ['area_password', 'domain_password']: @@ -203,7 +203,7 @@ def verify(isis): if list(set(global_range) & set(local_range)): raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!') - + # Check for a blank or invalid value per prefix if dict_search('segment_routing.prefix', isis): for prefix, prefix_config in isis['segment_routing']['prefix'].items(): @@ -218,7 +218,7 @@ def verify(isis): if dict_search('segment_routing.prefix', isis): for prefix, prefix_config in isis['segment_routing']['prefix'].items(): if 'absolute' in prefix_config: - if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']): + if ("explicit_null" in prefix_config['absolute']) and ("no_php_flag" in prefix_config['absolute']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') elif 'index' in prefix_config: @@ -232,25 +232,15 @@ def generate(isis): if not isis or 'deleted' in isis: return None - isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.j2 - isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', isis) isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.j2', isis) return None def apply(isis): isis_daemon = 'isisd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section('(\s+)?ip protocol isis route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in isis: - frr_cfg.add_before(frr.default_add_before, isis['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 0582d32be..b73483470 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -89,7 +89,7 @@ def get_config(config=None): if 'mpls_te' not in ospf: del default_values['mpls_te'] - for protocol in ['bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']: + for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']: # table is a tagNode thus we need to clean out all occurances for the # default values and load them in later individually if protocol == 'table': @@ -196,7 +196,7 @@ def verify(ospf): vrf = ospf['vrf'] tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: - raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') + raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') # Segment routing checks if dict_search('segment_routing.global_block', ospf): @@ -234,7 +234,7 @@ def verify(ospf): if list(set(global_range) & set(local_range)): raise ConfigError(f'Segment-Routing Global Block ({g_low_label_value}/{g_high_label_value}) '\ f'conflicts with Local Block ({l_low_label_value}/{l_high_label_value})!') - + # Check for a blank or invalid value per prefix if dict_search('segment_routing.prefix', ospf): for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): @@ -256,25 +256,15 @@ def generate(ospf): if not ospf or 'deleted' in ospf: return None - ospf['protocol'] = 'ospf' # required for frr/vrf.route-map.frr.j2 - ospf['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.j2', ospf) ospf['frr_ospfd_config'] = render_to_string('frr/ospfd.frr.j2', ospf) return None def apply(ospf): ospf_daemon = 'ospfd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section('(\s+)?ip protocol ospf route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') - if 'frr_zebra_config' in ospf: - frr_cfg.add_before(frr.default_add_before, ospf['frr_zebra_config']) - frr_cfg.commit_configuration(zebra_daemon) - # Generate empty helper string which can be ammended to FRR commands, it # will be either empty (default VRF) or contain the "vrf <name" statement vrf = '' @@ -292,6 +282,7 @@ def apply(ospf): if 'frr_ospfd_config' in ospf: frr_cfg.add_before(frr.default_add_before, ospf['frr_ospfd_config']) + frr_cfg.commit_configuration(ospf_daemon) return None diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index ed0a8fba2..cb21bd83c 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -138,7 +138,7 @@ def verify(ospfv3): vrf = ospfv3['vrf'] tmp = get_interface_config(interface) if 'master' not in tmp or tmp['master'] != vrf: - raise ConfigError(f'Interface {interface} is not a member of VRF {vrf}!') + raise ConfigError(f'Interface "{interface}" is not a member of VRF "{vrf}"!') return None diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 3e5ebb805..7b6150696 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -105,20 +105,14 @@ def generate(static): def apply(static): static_daemon = 'staticd' - zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions frr_cfg = frr.FRRConfig() - - # The route-map used for the FIB (zebra) is part of the zebra daemon - frr_cfg.load_configuration(zebra_daemon) - frr_cfg.modify_section(r'^ip protocol static route-map [-a-zA-Z0-9.]+', '') - frr_cfg.commit_configuration(zebra_daemon) frr_cfg.load_configuration(static_daemon) if 'vrf' in static: vrf = static['vrf'] - frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit', remove_stop_mark=True) + frr_cfg.modify_section(f'^vrf {vrf}', stop_pattern='^exit-vrf', remove_stop_mark=True) else: frr_cfg.modify_section(r'^ip route .*') frr_cfg.modify_section(r'^ipv6 route .*') diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index 0418e8d82..1be2c283f 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,12 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os + from sys import exit from netifaces import interfaces +from vyos.base import Warning from vyos.config import Config +from vyos.configdep import set_dependents, call_dependents from vyos.configdict import dict_merge -from vyos.configverify import verify_interface_exists +from vyos.ifconfig import Section from vyos.qos import CAKE from vyos.qos import DropTail from vyos.qos import FairQueue @@ -81,6 +85,18 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) + if 'interface' in qos: + for ifname, if_conf in qos['interface'].items(): + if_node = Section.get_config_path(ifname) + + if not if_node: + continue + + path = f'interfaces {if_node}' + if conf.exists(f'{path} mirror') or conf.exists(f'{path} redirect'): + type_node = path.split(" ")[1] # return only interface type node + set_dependents(type_node, conf, ifname) + if 'policy' in qos: for policy in qos['policy']: # when calling defaults() we need to use the real CLI node, thus we @@ -194,8 +210,6 @@ def verify(qos): # we should check interface ingress/egress configuration after verifying that # the policy name is used only once - this makes the logic easier! for interface, interface_config in qos['interface'].items(): - verify_interface_exists(interface) - for direction in ['egress', 'ingress']: # bail out early if shaper for given direction is not used at all if direction not in interface_config: @@ -229,6 +243,13 @@ def apply(qos): return None for interface, interface_config in qos['interface'].items(): + if not os.path.exists(f'/sys/class/net/{interface}'): + # When shaper is bound to a dialup (e.g. PPPoE) interface it is + # possible that it is yet not availbale when to QoS code runs. + # Skip the configuration and inform the user + Warning(f'Interface "{interface}" does not exist!') + continue + for direction in ['egress', 'ingress']: # bail out early if shaper for given direction is not used at all if direction not in interface_config: @@ -238,6 +259,8 @@ def apply(qos): tmp = shaper_type(interface) tmp.update(shaper_config, direction) + call_dependents() + return None if __name__ == '__main__': diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index e9afd6a55..95c72df47 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import jmespath from sys import exit @@ -29,9 +30,92 @@ from vyos import ConfigError from vyos import airbag airbag.enable() + ipoe_conf = '/run/accel-pppd/ipoe.conf' ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' + +def get_pools_in_order(data: dict) -> list: + """Return a list of dictionaries representing pool data in the order + in which they should be allocated. Pool must be defined before we can + use it with 'next-pool' option. + + Args: + data: A dictionary of pool data, where the keys are pool names and the + values are dictionaries containing the 'subnet' key and the optional + 'next_pool' key. + + Returns: + list: A list of dictionaries + + Raises: + ValueError: If a 'next_pool' key references a pool name that + has not been defined. + ValueError: If a circular reference is found in the 'next_pool' keys. + + Example: + config_data = { + ... 'first-pool': { + ... 'next_pool': 'second-pool', + ... 'subnet': '192.0.2.0/25' + ... }, + ... 'second-pool': { + ... 'next_pool': 'third-pool', + ... 'subnet': '203.0.113.0/25' + ... }, + ... 'third-pool': { + ... 'subnet': '198.51.100.0/24' + ... }, + ... 'foo': { + ... 'subnet': '100.64.0.0/24', + ... 'next_pool': 'second-pool' + ... } + ... } + + % get_pools_in_order(config_data) + [{'third-pool': {'subnet': '198.51.100.0/24'}}, + {'second-pool': {'next_pool': 'third-pool', 'subnet': '203.0.113.0/25'}}, + {'first-pool': {'next_pool': 'second-pool', 'subnet': '192.0.2.0/25'}}, + {'foo': {'next_pool': 'second-pool', 'subnet': '100.64.0.0/24'}}] + """ + pools = [] + unresolved_pools = {} + + for pool, pool_config in data.items(): + if 'next_pool' not in pool_config: + pools.insert(0, {pool: pool_config}) + else: + unresolved_pools[pool] = pool_config + + while unresolved_pools: + resolved_pools = [] + + for pool, pool_config in unresolved_pools.items(): + next_pool_name = pool_config['next_pool'] + + if any(p for p in pools if next_pool_name in p): + index = next( + (i for i, p in enumerate(pools) if next_pool_name in p), + None) + pools.insert(index + 1, {pool: pool_config}) + resolved_pools.append(pool) + elif next_pool_name in unresolved_pools: + # next pool not yet resolved + pass + else: + raise ValueError( + f"Pool '{next_pool_name}' not defined in configuration data" + ) + + if not resolved_pools: + raise ValueError("Circular reference in configuration data") + + for pool in resolved_pools: + unresolved_pools.pop(pool) + + return pools + + def get_config(config=None): if config: conf = config @@ -43,6 +127,19 @@ def get_config(config=None): # retrieve common dictionary keys ipoe = get_accel_dict(conf, base, ipoe_chap_secrets) + + if jmespath.search('client_ip_pool.name', ipoe): + dict_named_pools = jmespath.search('client_ip_pool.name', ipoe) + # Multiple named pools require ordered values T5099 + ipoe['ordered_named_pools'] = get_pools_in_order(dict_named_pools) + # T5099 'next-pool' option + if jmespath.search('client_ip_pool.name.*.next_pool', ipoe): + for pool, pool_config in ipoe['client_ip_pool']['name'].items(): + if 'next_pool' in pool_config: + ipoe['first_named_pool'] = pool + ipoe['first_named_pool_subnet'] = pool_config + break + return ipoe @@ -53,10 +150,24 @@ def verify(ipoe): if 'interface' not in ipoe: raise ConfigError('No IPoE interface configured') - for interface in ipoe['interface']: + for interface, iface_config in ipoe['interface'].items(): verify_interface_exists(interface) + if 'client_subnet' in iface_config and 'vlan' in iface_config: + raise ConfigError('Option "client-subnet" incompatible with "vlan"!' + 'Use "ipoe client-ip-pool" instead.') #verify_accel_ppp_base_service(ipoe, local_users=False) + # IPoE server does not have 'gateway' option in the CLI + # we cannot use configverify.py verify_accel_ppp_base_service for ipoe-server + + if dict_search('authentication.mode', ipoe) == 'radius': + if not dict_search('authentication.radius.server', ipoe): + raise ConfigError('RADIUS authentication requires at least one server') + + for server in dict_search('authentication.radius.server', ipoe): + radius_config = ipoe['authentication']['radius']['server'][server] + if 'key' not in radius_config: + raise ConfigError(f'Missing RADIUS secret key for server "{server}"') if 'client_ipv6_pool' in ipoe: if 'delegate' in ipoe['client_ipv6_pool'] and 'prefix' not in ipoe['client_ipv6_pool']: diff --git a/src/conf_mode/service_monitoring_telegraf.py b/src/conf_mode/service_monitoring_telegraf.py index 363408679..47510ce80 100755 --- a/src/conf_mode/service_monitoring_telegraf.py +++ b/src/conf_mode/service_monitoring_telegraf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import socket import json from sys import exit @@ -57,6 +58,13 @@ def get_nft_filter_chains(): return chain_list +def get_hostname() -> str: + try: + hostname = socket.getfqdn() + except socket.gaierror: + hostname = socket.gethostname() + return hostname + def get_config(config=None): if config: conf = config @@ -79,6 +87,7 @@ def get_config(config=None): monitoring = dict_merge(default_values, monitoring) monitoring['custom_scripts_dir'] = custom_scripts_dir + monitoring['hostname'] = get_hostname() monitoring['interfaces_ethernet'] = Section.interfaces('ethernet', vlan=False) monitoring['nft_chains'] = get_nft_filter_chains() diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 600ba4e92..adeefaa37 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -71,8 +71,9 @@ def verify(pppoe): # local ippool and gateway settings config checks if not (dict_search('client_ip_pool.subnet', pppoe) or + (dict_search('client_ip_pool.name', pppoe) or (dict_search('client_ip_pool.start', pppoe) and - dict_search('client_ip_pool.stop', pppoe))): + dict_search('client_ip_pool.stop', pppoe)))): print('Warning: No PPPoE client pool defined') if dict_search('authentication.radius.dynamic_author.server', pppoe): diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index ab2ccf99e..9b7c04eb0 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -92,7 +92,7 @@ def get_config(config=None): # Always listen on localhost if an explicit address has been configured # This is a safety measure to not end up with invalid listen addresses - # that are not configured on this system. See https://phabricator.vyos.net/T850 + # that are not configured on this system. See https://vyos.dev/T850 if '127.0.0.1' not in snmp['listen_address']: tmp = {'127.0.0.1': {'port': '161'}} snmp['listen_address'] = dict_merge(tmp, snmp['listen_address']) @@ -103,6 +103,9 @@ def get_config(config=None): if 'community' in snmp: default_values = defaults(base + ['community']) + if 'network' in default_values: + # convert multiple default networks to list + default_values['network'] = default_values['network'].split() for community in snmp['community']: snmp['community'][community] = dict_merge( default_values, snmp['community'][community]) diff --git a/src/conf_mode/system-ip.py b/src/conf_mode/system-ip.py index 0c5063ed3..95865c690 100755 --- a/src/conf_mode/system-ip.py +++ b/src/conf_mode/system-ip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,12 +18,15 @@ from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_route_map +from vyos.template import render_to_string from vyos.util import call from vyos.util import dict_search from vyos.util import sysctl_write from vyos.util import write_file from vyos.xml import defaults from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() @@ -40,13 +43,30 @@ def get_config(config=None): default_values = defaults(base) opt = dict_merge(default_values, opt) + # When working with FRR we need to know the corresponding address-family + opt['afi'] = 'ip' + + # We also need the route-map information from the config + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], + get_first_key=True)}} + # Merge policy dict into "regular" config dict + opt = dict_merge(tmp, opt) return opt def verify(opt): - pass + if 'protocol' in opt: + for protocol, protocol_options in opt['protocol'].items(): + if 'route_map' in protocol_options: + verify_route_map(protocol_options['route_map'], opt) + return def generate(opt): - pass + if 'protocol' in opt: + opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt) + return def apply(opt): # Apply ARP threshold values @@ -78,6 +98,18 @@ def apply(opt): value = '1' if (tmp != None) else '0' sysctl_write('net.ipv4.fib_multipath_hash_policy', value) + if 'protocol' in opt: + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(r'ip protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + if 'frr_zebra_config' in opt: + frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system-ipv6.py b/src/conf_mode/system-ipv6.py index 26aacf46b..b6d3a79c3 100755 --- a/src/conf_mode/system-ipv6.py +++ b/src/conf_mode/system-ipv6.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,11 +19,14 @@ import os from sys import exit from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_route_map +from vyos.template import render_to_string from vyos.util import dict_search from vyos.util import sysctl_write from vyos.util import write_file from vyos.xml import defaults from vyos import ConfigError +from vyos import frr from vyos import airbag airbag.enable() @@ -41,13 +44,30 @@ def get_config(config=None): default_values = defaults(base) opt = dict_merge(default_values, opt) + # When working with FRR we need to know the corresponding address-family + opt['afi'] = 'ipv6' + + # We also need the route-map information from the config + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], + get_first_key=True)}} + # Merge policy dict into "regular" config dict + opt = dict_merge(tmp, opt) return opt def verify(opt): - pass + if 'protocol' in opt: + for protocol, protocol_options in opt['protocol'].items(): + if 'route_map' in protocol_options: + verify_route_map(protocol_options['route_map'], opt) + return def generate(opt): - pass + if 'protocol' in opt: + opt['frr_zebra_config'] = render_to_string('frr/zebra.route-map.frr.j2', opt) + return def apply(opt): # configure multipath @@ -78,6 +98,18 @@ def apply(opt): if name == 'accept_dad': write_file(os.path.join(root, name), value) + if 'protocol' in opt: + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(r'ipv6 protocol \w+ route-map [-a-zA-Z0-9.]+', stop_pattern='(\s|!)') + if 'frr_zebra_config' in opt: + frr_cfg.add_before(frr.default_add_before, opt['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index e26b81e3d..fbb013cf3 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -16,22 +16,21 @@ import os -from crypt import crypt -from crypt import METHOD_SHA512 +from passlib.hosts import linux_context from psutil import users from pwd import getpwall from pwd import getpwnam -from spwd import getspnam from sys import exit from time import sleep from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_vrf +from vyos.defaults import directories from vyos.template import render from vyos.template import is_ipv4 from vyos.util import cmd -from vyos.util import call +from vyos.util import call, rc_cmd from vyos.util import run from vyos.util import DEVNULL from vyos.util import dict_search @@ -41,8 +40,14 @@ from vyos import airbag airbag.enable() autologout_file = "/etc/profile.d/autologout.sh" +limits_file = "/etc/security/limits.d/10-vyos.conf" radius_config_file = "/etc/pam_radius_auth.conf" +# LOGIN_TIMEOUT from /etc/loign.defs minus 10 sec +MAX_RADIUS_TIMEOUT: int = 50 +# MAX_RADIUS_TIMEOUT divided by 2 sec (minimum recomended timeout) +MAX_RADIUS_COUNT: int = 25 + def get_local_users(): """Return list of dynamically allocated users (see Debian Policy Manual)""" local_users = [] @@ -54,6 +59,13 @@ def get_local_users(): return local_users +def get_shadow_password(username): + with open('/etc/shadow') as f: + for user in f.readlines(): + items = user.split(":") + if username == items[0]: + return items[1] + return None def get_config(config=None): if config: @@ -118,18 +130,27 @@ def verify(login): if 'radius' in login: if 'server' not in login['radius']: raise ConfigError('No RADIUS server defined!') - + sum_timeout: int = 0 + radius_servers_count: int = 0 fail = True for server, server_config in dict_search('radius.server', login).items(): if 'key' not in server_config: raise ConfigError(f'RADIUS server "{server}" requires key!') - - if 'disabled' not in server_config: + if 'disable' not in server_config: + sum_timeout += int(server_config['timeout']) + radius_servers_count += 1 fail = False - continue + if fail: raise ConfigError('All RADIUS servers are disabled') + if radius_servers_count > MAX_RADIUS_COUNT: + raise ConfigError('Number of RADIUS servers more than 25 ') + + if sum_timeout > MAX_RADIUS_TIMEOUT: + raise ConfigError('Sum of RADIUS servers timeouts ' + 'has to be less or eq 50 sec') + verify_vrf(login['radius']) if 'source_address' in login['radius']: @@ -144,6 +165,9 @@ def verify(login): if ipv6_count > 1: raise ConfigError('Only one IPv6 source-address can be set!') + if 'max_login_session' in login and 'timeout' not in login: + raise ConfigError('"login timeout" must be configured!') + return None @@ -153,13 +177,13 @@ def generate(login): for user, user_config in login['user'].items(): tmp = dict_search('authentication.plaintext_password', user_config) if tmp: - encrypted_password = crypt(tmp, METHOD_SHA512) + encrypted_password = linux_context.hash(tmp) login['user'][user]['authentication']['encrypted_password'] = encrypted_password del login['user'][user]['authentication']['plaintext_password'] # remove old plaintext password and set new encrypted password env = os.environ.copy() - env['vyos_libexec_dir'] = '/usr/libexec/vyos' + env['vyos_libexec_dir'] = directories['base'] # Set default commands for re-adding user with encrypted password del_user_plain = f"system login user '{user}' authentication plaintext-password" @@ -183,10 +207,12 @@ def generate(login): add_user_encrypt = " ".join(add_user_encrypt) call(f"/opt/vyatta/sbin/my_delete {del_user_plain}", env=env) - call(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env) + ret, out = rc_cmd(f"/opt/vyatta/sbin/my_set {add_user_encrypt}", env=env) + if ret: + raise ConfigError(out) else: try: - if getspnam(user).sp_pwdp == dict_search('authentication.encrypted_password', user_config): + if get_shadow_password(user) == dict_search('authentication.encrypted_password', user_config): # If the current encrypted bassword matches the encrypted password # from the config - do not update it. This will remove the encrypted # value from the system logs. @@ -204,6 +230,14 @@ def generate(login): if os.path.isfile(radius_config_file): os.unlink(radius_config_file) + # /etc/security/limits.d/10-vyos.conf + if 'max_login_session' in login: + render(limits_file, 'login/limits.j2', login, + permission=0o644, user='root', group='root') + else: + if os.path.isfile(limits_file): + os.unlink(limits_file) + if 'timeout' in login: render(autologout_file, 'login/autologout.j2', login, permission=0o755, user='root', group='root') diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index 20132456c..e646fb0ae 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,253 +15,129 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re -from pathlib import Path from sys import exit from vyos.config import Config -from vyos import ConfigError -from vyos.util import run +from vyos.configdict import dict_merge +from vyos.configdict import is_node_changed +from vyos.configverify import verify_vrf +from vyos.util import call from vyos.template import render - +from vyos.xml import defaults +from vyos import ConfigError from vyos import airbag airbag.enable() +rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf' +logrotate_conf = '/etc/logrotate.d/vyos-rsyslog' +systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf' + def get_config(config=None): if config: - c = config + conf = config else: - c = Config() - if not c.exists('system syslog'): + conf = Config() + base = ['system', 'syslog'] + if not conf.exists(base): return None - c.set_level('system syslog') - - config_data = { - 'files': {}, - 'console': {}, - 'hosts': {}, - 'user': {} - } - - # - # /etc/rsyslog.d/vyos-rsyslog.conf - # 'set system syslog global' - # - config_data['files'].update( - { - 'global': { - 'log-file': '/var/log/messages', - 'selectors': '*.notice;local7.debug', - 'max-files': '5', - 'preserver_fqdn': False - } - } - ) - - if c.exists('global marker'): - config_data['files']['global']['marker'] = True - if c.exists('global marker interval'): - config_data['files']['global'][ - 'marker-interval'] = c.return_value('global marker interval') - if c.exists('global facility'): - config_data['files']['global'][ - 'selectors'] = generate_selectors(c, 'global facility') - if c.exists('global archive size'): - config_data['files']['global']['max-size'] = int( - c.return_value('global archive size')) * 1024 - if c.exists('global archive file'): - config_data['files']['global'][ - 'max-files'] = c.return_value('global archive file') - if c.exists('global preserve-fqdn'): - config_data['files']['global']['preserver_fqdn'] = True - - # - # set system syslog file - # - - if c.exists('file'): - filenames = c.list_nodes('file') - for filename in filenames: - config_data['files'].update( - { - filename: { - 'log-file': '/var/log/user/' + filename, - 'max-files': '5', - 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog-generated-' + filename, - 'selectors': '*.err', - 'max-size': 262144 - } - } - ) - - if c.exists('file ' + filename + ' facility'): - config_data['files'][filename]['selectors'] = generate_selectors( - c, 'file ' + filename + ' facility') - if c.exists('file ' + filename + ' archive size'): - config_data['files'][filename]['max-size'] = int( - c.return_value('file ' + filename + ' archive size')) * 1024 - if c.exists('file ' + filename + ' archive files'): - config_data['files'][filename]['max-files'] = c.return_value( - 'file ' + filename + ' archive files') - - # set system syslog console - if c.exists('console'): - config_data['console'] = { - '/dev/console': { - 'selectors': '*.err' - } - } - - for f in c.list_nodes('console facility'): - if c.exists('console facility ' + f + ' level'): - config_data['console'] = { - '/dev/console': { - 'selectors': generate_selectors(c, 'console facility') - } - } - # set system syslog host - if c.exists('host'): - rhosts = c.list_nodes('host') - proto = 'udp' - for rhost in rhosts: - for fac in c.list_nodes('host ' + rhost + ' facility'): - if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): - proto = c.return_value( - 'host ' + rhost + ' facility ' + fac + ' protocol') - else: - proto = 'udp' - - config_data['hosts'].update( - { - rhost: { - 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'), - 'proto': proto - } - } - ) - if c.exists('host ' + rhost + ' port'): - config_data['hosts'][rhost][ - 'port'] = c.return_value(['host', rhost, 'port']) - - # set system syslog host x.x.x.x format octet-counted - if c.exists('host ' + rhost + ' format octet-counted'): - config_data['hosts'][rhost]['oct_count'] = True - else: - config_data['hosts'][rhost]['oct_count'] = False - - # set system syslog user - if c.exists('user'): - usrs = c.list_nodes('user') - for usr in usrs: - config_data['user'].update( - { - usr: { - 'selectors': generate_selectors(c, 'user ' + usr + ' facility') - } - } - ) - - return config_data - - -def generate_selectors(c, config_node): -# protocols and security are being mapped here -# for backward compatibility with old configs -# security and protocol mappings can be removed later - nodes = c.list_nodes(config_node) - selectors = "" - for node in nodes: - lvl = c.return_value(config_node + ' ' + node + ' level') - if lvl == None: - lvl = "err" - if lvl == 'all': - lvl = '*' - if node == 'all' and node != nodes[-1]: - selectors += "*." + lvl + ";" - elif node == 'all': - selectors += "*." + lvl - elif node != nodes[-1]: - if node == 'protocols': - node = 'local7' - if node == 'security': - node = 'auth' - selectors += node + "." + lvl + ";" - else: - if node == 'protocols': - node = 'local7' - if node == 'security': - node = 'auth' - selectors += node + "." + lvl - return selectors - - -def generate(c): - if c == None: + syslog = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + syslog.update({ 'logrotate' : logrotate_conf }) + tmp = is_node_changed(conf, base + ['vrf']) + if tmp: syslog.update({'restart_required': {}}) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + # XXX: some syslog default values can not be merged here (originating from + # a tagNode - remove and add them later per individual tagNode instance + if 'console' in default_values: + del default_values['console'] + for entity in ['global', 'user', 'host', 'file']: + if entity in default_values: + del default_values[entity] + + syslog = dict_merge(default_values, syslog) + + # XXX: add defaults for "console" tree + if 'console' in syslog and 'facility' in syslog['console']: + default_values = defaults(base + ['console', 'facility']) + for facility in syslog['console']['facility']: + syslog['console']['facility'][facility] = dict_merge(default_values, + syslog['console']['facility'][facility]) + + # XXX: add defaults for "host" tree + if 'host' in syslog: + default_values_host = defaults(base + ['host']) + if 'facility' in default_values_host: + del default_values_host['facility'] + default_values_facility = defaults(base + ['host', 'facility']) + + for host, host_config in syslog['host'].items(): + syslog['host'][host] = dict_merge(default_values_host, syslog['host'][host]) + if 'facility' in host_config: + for facility in host_config['facility']: + syslog['host'][host]['facility'][facility] = dict_merge(default_values_facility, + syslog['host'][host]['facility'][facility]) + + # XXX: add defaults for "user" tree + if 'user' in syslog: + default_values = defaults(base + ['user', 'facility']) + for user, user_config in syslog['user'].items(): + if 'facility' in user_config: + for facility in user_config['facility']: + syslog['user'][user]['facility'][facility] = dict_merge(default_values, + syslog['user'][user]['facility'][facility]) + + # XXX: add defaults for "file" tree + if 'file' in syslog: + default_values = defaults(base + ['file']) + for file, file_config in syslog['file'].items(): + for facility in file_config['facility']: + syslog['file'][file]['facility'][facility] = dict_merge(default_values, + syslog['file'][file]['facility'][facility]) + + return syslog + +def verify(syslog): + if not syslog: return None - conf = '/etc/rsyslog.d/vyos-rsyslog.conf' - render(conf, 'syslog/rsyslog.conf.j2', c) - - # cleanup current logrotate config files - logrotate_files = Path('/etc/logrotate.d/').glob('vyos-rsyslog-generated-*') - for file in logrotate_files: - file.unlink() + verify_vrf(syslog) - # eventually write for each file its own logrotate file, since size is - # defined it shouldn't matter - for filename, fileconfig in c.get('files', {}).items(): - if fileconfig['log-file'].startswith('/var/log/user/'): - conf = '/etc/logrotate.d/vyos-rsyslog-generated-' + filename - render(conf, 'syslog/logrotate.j2', { 'config_render': fileconfig }) +def generate(syslog): + if not syslog: + if os.path.exists(rsyslog_conf): + os.path.unlink(rsyslog_conf) + if os.path.exists(logrotate_conf): + os.path.unlink(logrotate_conf) - -def verify(c): - if c == None: return None - # may be obsolete - # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) - # it interferes with the global logging, to make sure we are using a single base, template is enforced here - # - if not os.path.islink('/etc/rsyslog.conf'): - os.remove('/etc/rsyslog.conf') - os.symlink( - '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') + render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog) + render(systemd_override, 'rsyslog/override.conf.j2', syslog) + render(logrotate_conf, 'rsyslog/logrotate.j2', syslog) - # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there - # is a chance that someone still needs it, so I don't automatically remove - # them - # + # Reload systemd manager configuration + call('systemctl daemon-reload') + return None - if c == None: +def apply(syslog): + systemd_service = 'syslog.service' + if not syslog: + call(f'systemctl stop {systemd_service}') return None - fac = [ - '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security', - 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7'] - lvl = ['emerg', 'alert', 'crit', 'err', - 'warning', 'notice', 'info', 'debug', '*'] - - for conf in c: - if c[conf]: - for item in c[conf]: - for s in c[conf][item]['selectors'].split(";"): - f = re.sub("\..*$", "", s) - if f not in fac: - raise ConfigError( - 'Invalid facility ' + s + ' set in ' + conf + ' ' + item) - l = re.sub("^.+\.", "", s) - if l not in lvl: - raise ConfigError( - 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item) - + # we need to restart the service if e.g. the VRF name changed + systemd_action = 'reload-or-restart' + if 'restart_required' in syslog: + systemd_action = 'restart' -def apply(c): - if not c: - return run('systemctl stop syslog.service') - return run('systemctl restart syslog.service') + call(f'systemctl {systemd_action} {systemd_service}') + return None if __name__ == '__main__': try: diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py new file mode 100755 index 000000000..a0c3fca7f --- /dev/null +++ b/src/conf_mode/system_sflow.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import call +from vyos.validate import is_addr_assigned +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +hsflowd_conf_path = '/run/sflow/hsflowd.conf' +systemd_service = 'hsflowd.service' +systemd_override = f'/run/systemd/system/{systemd_service}.d/override.conf' + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['system', 'sflow'] + if not conf.exists(base): + return None + + sflow = conf.get_config_dict(base, + key_mangling=('-', '_'), + get_first_key=True) + + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = defaults(base) + + sflow = dict_merge(default_values, sflow) + + # Ignore default XML values if config doesn't exists + # Delete key from dict + if 'port' in sflow['server']: + del sflow['server']['port'] + + # Set default values per server + if 'server' in sflow: + for server in sflow['server']: + default_values = defaults(base + ['server']) + sflow['server'][server] = dict_merge(default_values, sflow['server'][server]) + + return sflow + + +def verify(sflow): + if not sflow: + return None + + # Check if configured sflow agent-address exist in the system + if 'agent_address' in sflow: + tmp = sflow['agent_address'] + if not is_addr_assigned(tmp): + raise ConfigError( + f'Configured "sflow agent-address {tmp}" does not exist in the system!' + ) + + # Check if at least one interface is configured + if 'interface' not in sflow: + raise ConfigError( + 'sFlow requires at least one interface to be configured!') + + # Check if at least one server is configured + if 'server' not in sflow: + raise ConfigError('You need to configure at least one sFlow server!') + + # return True if all checks were passed + return True + + +def generate(sflow): + if not sflow: + return None + + render(hsflowd_conf_path, 'sflow/hsflowd.conf.j2', sflow) + render(systemd_override, 'sflow/override.conf.j2', sflow) + # Reload systemd manager configuration + call('systemctl daemon-reload') + + +def apply(sflow): + if not sflow: + # Stop flow-accounting daemon and remove configuration file + call(f'systemctl stop {systemd_service}') + if os.path.exists(hsflowd_conf_path): + os.unlink(hsflowd_conf_path) + return + + # Start/reload flow-accounting daemon + call(f'systemctl restart {systemd_service}') + + +if __name__ == '__main__': + try: + config = get_config() + verify(config) + generate(config) + apply(config) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 3af2af4d9..63887b278 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,6 +17,7 @@ import ipaddress import os import re +import jmespath from sys import exit from time import sleep @@ -52,8 +53,6 @@ dhcp_wait_attempts = 2 dhcp_wait_sleep = 1 swanctl_dir = '/etc/swanctl' -ipsec_conf = '/etc/ipsec.conf' -ipsec_secrets = '/etc/ipsec.secrets' charon_conf = '/etc/strongswan.d/charon.conf' charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf' @@ -219,6 +218,12 @@ def verify(ipsec): if not ipsec: return None + if 'authentication' in ipsec: + if 'psk' in ipsec['authentication']: + for psk, psk_config in ipsec['authentication']['psk'].items(): + if 'id' not in psk_config or 'secret' not in psk_config: + raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"') + if 'interfaces' in ipsec : for ifname in ipsec['interface']: verify_interface_exists(ifname) @@ -535,8 +540,7 @@ def generate(ipsec): cleanup_pki_files() if not ipsec: - for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, - charon_radius_conf, interface_conf, swanctl_conf]: + for config_file in [charon_dhcp_conf, charon_radius_conf, interface_conf, swanctl_conf]: if os.path.isfile(config_file): os.unlink(config_file) render(charon_conf, 'ipsec/charon.j2', {'install_routes': default_install_routes}) @@ -545,6 +549,8 @@ def generate(ipsec): if ipsec['dhcp_no_address']: with open(DHCP_HOOK_IFLIST, 'w') as f: f.write(" ".join(ipsec['dhcp_no_address'].values())) + elif os.path.exists(DHCP_HOOK_IFLIST): + os.unlink(DHCP_HOOK_IFLIST) for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH, PUBKEY_PATH]: if not os.path.exists(path): @@ -602,9 +608,15 @@ def generate(ipsec): ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough + # auth psk <tag> dhcp-interface <xxx> + if jmespath.search('authentication.psk.*.dhcp_interface', ipsec): + for psk, psk_config in ipsec['authentication']['psk'].items(): + if 'dhcp_interface' in psk_config: + for iface in psk_config['dhcp_interface']: + id = get_dhcp_address(iface) + if id: + ipsec['authentication']['psk'][psk]['id'].append(id) - render(ipsec_conf, 'ipsec/ipsec.conf.j2', ipsec) - render(ipsec_secrets, 'ipsec/ipsec.secrets.j2', ipsec) render(charon_conf, 'ipsec/charon.j2', ipsec) render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec) render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec) @@ -619,25 +631,12 @@ def resync_nhrp(ipsec): if tmp > 0: print('ERROR: failed to reapply NHRP settings!') -def wait_for_vici_socket(timeout=5, sleep_interval=0.1): - start_time = time() - test_command = f'sudo socat -u OPEN:/dev/null UNIX-CONNECT:{vici_socket}' - while True: - if (start_time + timeout) < time(): - return None - result = run(test_command) - if result == 0: - return True - sleep(sleep_interval) - def apply(ipsec): - systemd_service = 'strongswan-starter.service' + systemd_service = 'strongswan.service' if not ipsec: call(f'systemctl stop {systemd_service}') else: call(f'systemctl reload-or-restart {systemd_service}') - if wait_for_vici_socket(): - call('sudo swanctl -q') resync_nhrp(ipsec) diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 65623c2b1..ffac3b023 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -63,6 +63,7 @@ default_config_data = { 'ppp_ipv6_peer_intf_id': None, 'radius_server': [], 'radius_acct_inter_jitter': '', + 'radius_acct_interim_interval': None, 'radius_acct_tmo': '3', 'radius_max_try': '3', 'radius_timeout': '3', @@ -190,6 +191,9 @@ def get_config(config=None): # advanced radius-setting conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['accounting-interim-interval']): + l2tp['radius_acct_interim_interval'] = conf.return_value(['accounting-interim-interval']) + if conf.exists(['acct-interim-jitter']): l2tp['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter']) diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 16a5b5952..7b3325d95 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -46,8 +46,70 @@ radius_servers = cfg_dir + '/radius_servers' def get_hash(password): return crypt(password, mksalt(METHOD_SHA512)) -def get_config(): - conf = Config() + +def _default_dict_cleanup(origin: dict, default_values: dict) -> dict: + """ + https://vyos.dev/T2665 + Clear unnecessary key values in merged config by dict_merge function + :param origin: config + :type origin: dict + :param default_values: default values + :type default_values: dict + :return: merged dict + :rtype: dict + """ + if 'mode' in origin["authentication"] and "local" in \ + origin["authentication"]["mode"]: + del origin['authentication']['local_users']['username']['otp'] + if not origin["authentication"]["local_users"]["username"]: + raise ConfigError( + 'Openconnect authentication mode local requires at least one user') + default_ocserv_usr_values = \ + default_values['authentication']['local_users']['username']['otp'] + for user, params in origin['authentication']['local_users'][ + 'username'].items(): + # Not every configuration requires OTP settings + if origin['authentication']['local_users']['username'][user].get( + 'otp'): + origin['authentication']['local_users']['username'][user][ + 'otp'] = dict_merge(default_ocserv_usr_values, + origin['authentication'][ + 'local_users']['username'][user][ + 'otp']) + + if 'mode' in origin["authentication"] and "radius" in \ + origin["authentication"]["mode"]: + del origin['authentication']['radius']['server']['port'] + if not origin["authentication"]['radius']['server']: + raise ConfigError( + 'Openconnect authentication mode radius requires at least one RADIUS server') + default_values_radius_port = \ + default_values['authentication']['radius']['server']['port'] + for server, params in origin['authentication']['radius'][ + 'server'].items(): + if 'port' not in params: + params['port'] = default_values_radius_port + + if 'mode' in origin["accounting"] and "radius" in \ + origin["accounting"]["mode"]: + del origin['accounting']['radius']['server']['port'] + if not origin["accounting"]['radius']['server']: + raise ConfigError( + 'Openconnect accounting mode radius requires at least one RADIUS server') + default_values_radius_port = \ + default_values['accounting']['radius']['server']['port'] + for server, params in origin['accounting']['radius'][ + 'server'].items(): + if 'port' not in params: + params['port'] = default_values_radius_port + return origin + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() base = ['vpn', 'openconnect'] if not conf.exists(base): return None @@ -57,18 +119,8 @@ def get_config(): # options which we need to update into the dictionary retrived. default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) - - if 'mode' in ocserv["authentication"] and "local" in ocserv["authentication"]["mode"]: - # workaround a "know limitation" - https://phabricator.vyos.net/T2665 - del ocserv['authentication']['local_users']['username']['otp'] - if not ocserv["authentication"]["local_users"]["username"]: - raise ConfigError('openconnect mode local required at least one user') - default_ocserv_usr_values = default_values['authentication']['local_users']['username']['otp'] - for user, params in ocserv['authentication']['local_users']['username'].items(): - # Not every configuration requires OTP settings - if ocserv['authentication']['local_users']['username'][user].get('otp'): - ocserv['authentication']['local_users']['username'][user]['otp'] = dict_merge(default_ocserv_usr_values, ocserv['authentication']['local_users']['username'][user]['otp']) - + # workaround a "know limitation" - https://vyos.dev/T2665 + ocserv = _default_dict_cleanup(ocserv, default_values) if ocserv: ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) @@ -85,6 +137,14 @@ def verify(ocserv): not is_listen_port_bind_service(int(port), 'ocserv-main'): raise ConfigError(f'"{proto}" port "{port}" is used by another service') + # Check accounting + if "accounting" in ocserv: + if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]: + if "authentication" not in ocserv or "mode" not in ocserv["authentication"]: + raise ConfigError('Accounting depends on OpenConnect authentication configuration') + elif "radius" not in ocserv["authentication"]["mode"]: + raise ConfigError('RADIUS accounting must be used with RADIUS authentication') + # Check authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: @@ -177,10 +237,18 @@ def generate(ocserv): return None if "radius" in ocserv["authentication"]["mode"]: - # Render radius client configuration - render(radius_cfg, 'ocserv/radius_conf.j2', ocserv["authentication"]["radius"]) - # Render radius servers - render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"]) + if dict_search(ocserv, 'accounting.mode.radius'): + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.j2', ocserv) + merged_servers = ocserv["accounting"]["radius"]["server"] | ocserv["authentication"]["radius"]["server"] + # Render radius servers + # Merge the accounting and authentication servers into a single dictionary + render(radius_servers, 'ocserv/radius_servers.j2', {'server': merged_servers}) + else: + # Render radius client configuration + render(radius_cfg, 'ocserv/radius_conf.j2', ocserv) + # Render radius servers + render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"]) elif "local" in ocserv["authentication"]["mode"]: # if mode "OTP", generate OTP users file parameters if "otp" in ocserv["authentication"]["mode"]["local"]: diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 7550c411e..b9d18110a 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -37,6 +37,7 @@ default_pptp = { 'local_users' : [], 'radius_server' : [], 'radius_acct_inter_jitter': '', + 'radius_acct_interim_interval': None, 'radius_acct_tmo' : '30', 'radius_max_try' : '3', 'radius_timeout' : '30', @@ -44,6 +45,8 @@ default_pptp = { 'radius_nas_ip' : '', 'radius_source_address' : '', 'radius_shaper_attr' : '', + 'radius_shaper_enable': False, + 'radius_shaper_multiplier': '', 'radius_shaper_vendor': '', 'radius_dynamic_author' : '', 'chap_secrets_file': pptp_chap_secrets, # used in Jinja2 template @@ -143,6 +146,9 @@ def get_config(config=None): # advanced radius-setting conf.set_level(base_path + ['authentication', 'radius']) + if conf.exists(['accounting-interim-interval']): + pptp['radius_acct_interim_interval'] = conf.return_value(['accounting-interim-interval']) + if conf.exists(['acct-interim-jitter']): pptp['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter']) @@ -183,15 +189,18 @@ def get_config(config=None): pptp['radius_dynamic_author'] = dae + # Rate limit + if conf.exists(['rate-limit', 'attribute']): + pptp['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) + if conf.exists(['rate-limit', 'enable']): - pptp['radius_shaper_attr'] = 'Filter-Id' - c_attr = ['rate-limit', 'enable', 'attribute'] - if conf.exists(c_attr): - pptp['radius_shaper_attr'] = conf.return_value(c_attr) - - c_vendor = ['rate-limit', 'enable', 'vendor'] - if conf.exists(c_vendor): - pptp['radius_shaper_vendor'] = conf.return_value(c_vendor) + pptp['radius_shaper_enable'] = True + + if conf.exists(['rate-limit', 'multiplier']): + pptp['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) + + if conf.exists(['rate-limit', 'vendor']): + pptp['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) conf.set_level(base_path) if conf.exists(['client-ip-pool']): diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index c17cca3bd..0b983293e 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -20,9 +20,12 @@ from sys import exit from json import loads from vyos.config import Config +from vyos.configdict import dict_merge from vyos.configdict import node_changed +from vyos.configverify import verify_route_map from vyos.ifconfig import Interface from vyos.template import render +from vyos.template import render_to_string from vyos.util import call from vyos.util import cmd from vyos.util import dict_search @@ -99,6 +102,20 @@ def get_config(config=None): routes = vrf_routing(conf, name) if routes: vrf['vrf_remove'][name]['route'] = routes + # We also need the route-map information from the config + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = {'policy' : {'route-map' : conf.get_config_dict(['policy', 'route-map'], + get_first_key=True)}} + + # L3VNI setup is done via vrf_vni.py as it must be de-configured (on node + # deletetion prior to the BGP process. Tell the Jinja2 template no VNI + # setup is needed + vrf.update({'no_vni' : ''}) + + # Merge policy dict into "regular" config dict + vrf = dict_merge(tmp, vrf) return vrf def verify(vrf): @@ -113,38 +130,53 @@ def verify(vrf): f'static routes installed!') if 'name' in vrf: - reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", "get", "inet", "mtu", "link", "type", - "vrf"] + reserved_names = ["add", "all", "broadcast", "default", "delete", "dev", + "get", "inet", "mtu", "link", "type", "vrf"] table_ids = [] - for name, config in vrf['name'].items(): + for name, vrf_config in vrf['name'].items(): # Reserved VRF names if name in reserved_names: raise ConfigError(f'VRF name "{name}" is reserved and connot be used!') # table id is mandatory - if 'table' not in config: + if 'table' not in vrf_config: raise ConfigError(f'VRF "{name}" table id is mandatory!') # routing table id can't be changed - OS restriction if os.path.isdir(f'/sys/class/net/{name}'): tmp = str(dict_search('linkinfo.info_data.table', get_interface_config(name))) - if tmp and tmp != config['table']: + if tmp and tmp != vrf_config['table']: raise ConfigError(f'VRF "{name}" table id modification not possible!') - # VRf routing table ID must be unique on the system - if config['table'] in table_ids: + # VRF routing table ID must be unique on the system + if 'table' in vrf_config and vrf_config['table'] in table_ids: raise ConfigError(f'VRF "{name}" table id is not unique!') - table_ids.append(config['table']) + table_ids.append(vrf_config['table']) + + tmp = dict_search('ip.protocol', vrf_config) + if tmp != None: + for protocol, protocol_options in tmp.items(): + if 'route_map' in protocol_options: + verify_route_map(protocol_options['route_map'], vrf) + + tmp = dict_search('ipv6.protocol', vrf_config) + if tmp != None: + for protocol, protocol_options in tmp.items(): + if 'route_map' in protocol_options: + verify_route_map(protocol_options['route_map'], vrf) return None def generate(vrf): + # Render iproute2 VR helper names render(config_file, 'iproute2/vrf.conf.j2', vrf) # Render nftables zones config render(nft_vrf_config, 'firewall/nftables-vrf-zones.j2', vrf) - return None + # Render VRF Kernel/Zebra route-map filters + vrf['frr_zebra_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) + return None def apply(vrf): # Documentation @@ -249,6 +281,17 @@ def apply(vrf): nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}' cmd(f'nft {nft_add_element}') + # Apply FRR filters + zebra_daemon = 'zebra' + # Save original configuration prior to starting any commit actions + frr_cfg = frr.FRRConfig() + + # The route-map used for the FIB (zebra) is part of the zebra daemon + frr_cfg.load_configuration(zebra_daemon) + frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True) + if 'frr_zebra_config' in vrf: + frr_cfg.add_before(frr.default_add_before, vrf['frr_zebra_config']) + frr_cfg.commit_configuration(zebra_daemon) # return to default lookup preference when no VRF is configured if 'name' not in vrf: diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py index 585fdbebf..9f33536e5 100755..100644 --- a/src/conf_mode/vrf_vni.py +++ b/src/conf_mode/vrf_vni.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,36 +19,75 @@ from sys import exit from vyos.config import Config from vyos.template import render_to_string +from vyos.util import dict_search from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() -frr_daemon = 'zebra' - def get_config(config=None): if config: conf = config else: conf = Config() - base = ['vrf'] - vrf = conf.get_config_dict(base, get_first_key=True) + vrf_name = None + if len(argv) > 1: + vrf_name = argv[1] + else: + return None + + # Using duplicate L3VNIs makes no sense - it's also forbidden in FRR, + # thus VyOS CLI must deny this, too. Instead of getting only the dict for + # the requested VRF and den comparing it with depenent VRfs to not have any + # duplicate we will just grad ALL VRFs by default but only render/apply + # the configuration for the requested VRF - that makes the code easier and + # hopefully less error prone + vrf = conf.get_config_dict(['vrf'], key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True) + + # Store name of VRF we are interested in for FRR config rendering + vrf.update({'only_vrf' : vrf_name}) + return vrf def verify(vrf): + if not vrf: + return + + if len(argv) < 2: + raise ConfigError('VRF parameter not specified when valling vrf_vni.py') + + if 'name' in vrf: + vni_ids = [] + for name, vrf_config in vrf['name'].items(): + # VRF VNI (Virtual Network Identifier) must be unique on the system + if 'vni' in vrf_config: + if vrf_config['vni'] in vni_ids: + raise ConfigError(f'VRF "{name}" VNI is not unique!') + vni_ids.append(vrf_config['vni']) + return None def generate(vrf): - vrf['new_frr_config'] = render_to_string('frr/vrf-vni.frr.j2', vrf) + if not vrf: + return + + vrf['new_frr_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) return None def apply(vrf): + frr_daemon = 'zebra' + # add configuration to FRR frr_cfg = frr.FRRConfig() frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(f'^vrf .+', stop_pattern='^exit-vrf', remove_stop_mark=True) - if 'new_frr_config' in vrf: + # There is only one VRF inside the dict as we read only one in get_config() + if vrf and 'only_vrf' in vrf: + vrf_name = vrf['only_vrf'] + frr_cfg.modify_section(f'^vrf {vrf_name}', stop_pattern='^exit-vrf', remove_stop_mark=True) + if vrf and 'new_frr_config' in vrf: frr_cfg.add_before(frr.default_add_before, vrf['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) diff --git a/src/etc/commit/post-hooks.d/00vyos-sync b/src/etc/commit/post-hooks.d/00vyos-sync new file mode 100755 index 000000000..8ec732df0 --- /dev/null +++ b/src/etc/commit/post-hooks.d/00vyos-sync @@ -0,0 +1,7 @@ +#!/bin/sh +# When power is lost right after a commit modified files, the +# system can be corrupted and e.g. login is no longer possible. +# Always sync files to the backend storage after a commit. +# https://vyos.dev/T4975 +sync + diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks b/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks new file mode 100644 index 000000000..b4b4d516d --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/99-run-user-hooks @@ -0,0 +1,5 @@ +#!/bin/bash +DHCP_PRE_HOOKS="/config/scripts/dhcp-client/pre-hooks.d/" +if [ -d "${DHCP_PRE_HOOKS}" ] ; then + run-parts "${DHCP_PRE_HOOKS}" +fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/03-vyatta-dhclient-hook index 49bb18372..49bb18372 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/vyatta-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/03-vyatta-dhclient-hook diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks b/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks new file mode 100755 index 000000000..442419d79 --- /dev/null +++ b/src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks @@ -0,0 +1,5 @@ +#!/bin/bash +DHCP_POST_HOOKS="/config/scripts/dhcp-client/post-hooks.d/" +if [ -d "${DHCP_POST_HOOKS}" ] ; then + run-parts "${DHCP_POST_HOOKS}" +fi diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook index 1f1926e17..1f1926e17 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/ipsec-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py index bf25a7331..688c7af2a 100755 --- a/src/etc/opennhrp/opennhrp-script.py +++ b/src/etc/opennhrp/opennhrp-script.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,7 +17,7 @@ import os import re import sys -import vici +import vyos.ipsec from json import loads from pathlib import Path @@ -51,9 +51,8 @@ def vici_get_ipsec_uniqueid(conn: str, src_nbma: str, logger.info( f'Resolving IKE unique ids for: conn: {conn}, ' f'src_nbma: {src_nbma}, dst_nbma: {dst_nbma}') - session: vici.Session = vici.Session() list_ikeid: list[str] = [] - list_sa = session.list_sas({'ike': conn}) + list_sa: list = vyos.ipsec.get_vici_sas_by_name(conn, None) for sa in list_sa: if sa[conn]['local-host'].decode('ascii') == src_nbma \ and sa[conn]['remote-host'].decode('ascii') == dst_nbma: @@ -78,16 +77,7 @@ def vici_ike_terminate(list_ikeid: list[str]) -> bool: return False try: - session = vici.Session() - for ikeid in list_ikeid: - logger.info(f'Terminating IKE SA with id {ikeid}') - session_generator = session.terminate( - {'ike-id': ikeid, 'timeout': '-1'}) - # a dummy `for` loop is required because of requirements - # from vici. Without a full iteration on the output, the - # command to vici may not be executed completely - for _ in session_generator: - pass + vyos.ipsec.terminate_vici_ikeid_list(list_ikeid) return True except Exception as err: logger.error(f'Failed to terminate SA for IKE ids {list_ikeid}: {err}') @@ -180,19 +170,7 @@ def vici_initiate(conn: str, child_sa: str, src_addr: str, f'Trying to initiate connection. Name: {conn}, child sa: {child_sa}, ' f'src_addr: {src_addr}, dst_addr: {dest_addr}') try: - session = vici.Session() - session_generator = session.initiate({ - 'ike': conn, - 'child': child_sa, - 'timeout': '-1', - 'my-host': src_addr, - 'other-host': dest_addr - }) - # a dummy `for` loop is required because of requirements - # from vici. Without a full iteration on the output, the - # command to vici may not be executed completely - for _ in session_generator: - pass + vyos.ipsec.vici_initiate(conn, child_sa, src_addr, dest_addr) return True except Exception as err: logger.error(f'Unable to initiate connection {err}') @@ -218,8 +196,11 @@ def vici_terminate(conn: str, src_addr: str, dest_addr: str) -> None: f'No active sessions found for IKE profile {conn}, ' f'local NBMA {src_addr}, remote NBMA {dest_addr}') else: - vici_ike_terminate(ikeid_list) - + try: + vyos.ipsec.terminate_vici_ikeid_list(ikeid_list) + except Exception as err: + logger.error( + f'Failed to terminate SA for IKE ids {ikeid_list}: {err}') def iface_up(interface: str) -> None: """Proceed tunnel interface UP event diff --git a/src/etc/rsyslog.conf b/src/etc/rsyslog.conf new file mode 100644 index 000000000..c28e9b537 --- /dev/null +++ b/src/etc/rsyslog.conf @@ -0,0 +1,67 @@ +################# +#### MODULES #### +################# + +$ModLoad imuxsock # provides support for local system logging +$ModLoad imklog # provides kernel logging support (previously done by rklogd) +#$ModLoad immark # provides --MARK-- message capability + +$OmitLocalLogging off +$SystemLogSocketName /run/systemd/journal/syslog + +$KLogPath /proc/kmsg + +########################### +#### GLOBAL DIRECTIVES #### +########################### + +# The lines below cause all listed daemons/processes to be logged into +# /var/log/auth.log, then drops the message so it does not also go to the +# regular syslog so that messages are not duplicated + +$outchannel auth_log,/var/log/auth.log +if $programname == 'CRON' or + $programname == 'sudo' or + $programname == 'su' + then :omfile:$auth_log + +if $programname == 'CRON' or + $programname == 'sudo' or + $programname == 'su' + then stop + +# Use traditional timestamp format. +# To enable high precision timestamps, comment out the following line. +# A modern-style logfile format similar to TraditionalFileFormat, buth with high-precision timestamps and timezone information +#$ActionFileDefaultTemplate RSYSLOG_FileFormat +# The "old style" default log file format with low-precision timestamps +$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat + +# Filter duplicated messages +$RepeatedMsgReduction on + +# +# Set the default permissions for all log files. +# +$FileOwner root +$FileGroup adm +$FileCreateMode 0640 +$DirCreateMode 0755 +$Umask 0022 + +# +# Stop excessive logging of sudo +# +:msg, contains, " pam_unix(sudo:session): session opened for user root(uid=0) by" ~ +:msg, contains, "pam_unix(sudo:session): session closed for user root" ~ + +# +# Include all config files in /etc/rsyslog.d/ +# +$IncludeConfig /etc/rsyslog.d/*.conf + +############### +#### RULES #### +############### +# Emergencies are sent to everybody logged in. +*.emerg :omusrmsg:*
\ No newline at end of file diff --git a/src/etc/rsyslog.d/01-auth.conf b/src/etc/rsyslog.d/01-auth.conf deleted file mode 100644 index cc64099d6..000000000 --- a/src/etc/rsyslog.d/01-auth.conf +++ /dev/null @@ -1,14 +0,0 @@ -# The lines below cause all listed daemons/processes to be logged into -# /var/log/auth.log, then drops the message so it does not also go to the -# regular syslog so that messages are not duplicated - -$outchannel auth_log,/var/log/auth.log -if $programname == 'CRON' or - $programname == 'sudo' or - $programname == 'su' - then :omfile:$auth_log - -if $programname == 'CRON' or - $programname == 'sudo' or - $programname == 'su' - then stop diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 411429510..f5d84be4b 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -19,7 +19,7 @@ kernel.core_pattern=/var/core/core-%e-%p-%t # arp_filter defaults to 1 so set all to 0 so vrrp interfaces can override it. net.ipv4.conf.all.arp_filter=0 -# https://phabricator.vyos.net/T300 +# https://vyos.dev/T300 net.ipv4.conf.all.arp_ignore=0 net.ipv4.conf.all.arp_announce=2 @@ -98,9 +98,6 @@ net.ipv6.route.skip_notify_on_dev_down=1 # Default value of 20 seems to interfere with larger OSPF and VRRP setups net.ipv4.igmp_max_memberships = 512 -# Enable conntrack helper by default -net.netfilter.nf_conntrack_helper=1 - # Increase default garbage collection thresholds net.ipv4.neigh.default.gc_thresh1 = 1024 net.ipv4.neigh.default.gc_thresh2 = 4096 diff --git a/src/etc/systemd/system/ddclient.service.d/override.conf b/src/etc/systemd/system/ddclient.service.d/override.conf index d9c9963b0..09d929d39 100644 --- a/src/etc/systemd/system/ddclient.service.d/override.conf +++ b/src/etc/systemd/system/ddclient.service.d/override.conf @@ -8,4 +8,4 @@ WorkingDirectory=/run/ddclient PIDFile= PIDFile=/run/ddclient/ddclient.pid ExecStart= -ExecStart=/usr/sbin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf +ExecStart=/usr/bin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf diff --git a/src/etc/systemd/system/frr.service.d/override.conf b/src/etc/systemd/system/frr.service.d/override.conf index 69eb1a86a..2e4b6e295 100644 --- a/src/etc/systemd/system/frr.service.d/override.conf +++ b/src/etc/systemd/system/frr.service.d/override.conf @@ -3,6 +3,7 @@ Before= Before=vyos-router.service [Service] +LimitNOFILE=4096 ExecStartPre=/bin/bash -c 'mkdir -p /run/frr/config; \ echo "log syslog" > /run/frr/config/frr.conf; \ echo "log facility local7" >> /run/frr/config/frr.conf; \ diff --git a/src/etc/systemd/system/hostapd@.service.d/override.conf b/src/etc/systemd/system/hostapd@.service.d/override.conf index bb8e81d7a..926c07f94 100644 --- a/src/etc/systemd/system/hostapd@.service.d/override.conf +++ b/src/etc/systemd/system/hostapd@.service.d/override.conf @@ -1,6 +1,8 @@ [Unit] After= After=vyos-router.service +ConditionFileNotEmpty= +ConditionFileNotEmpty=/run/hostapd/%i.conf [Service] WorkingDirectory=/run/hostapd diff --git a/src/systemd/keepalived.service b/src/etc/systemd/system/keepalived.service.d/override.conf index a462d8614..d91a824b9 100644 --- a/src/systemd/keepalived.service +++ b/src/etc/systemd/system/keepalived.service.d/override.conf @@ -1,13 +1,14 @@ [Unit] -Description=Keepalive Daemon (LVS and VRRP) After=vyos-router.service -# Only start if there is a configuration file +# Only start if there is our configuration file - remove Debian default +# config file from the condition list +ConditionFileNotEmpty= ConditionFileNotEmpty=/run/keepalived/keepalived.conf [Service] KillMode=process Type=simple # Read configuration variable file if it is present +ExecStart= ExecStart=/usr/sbin/keepalived --use-file /run/keepalived/keepalived.conf --pid /run/keepalived/keepalived.pid --dont-fork --snmp -ExecReload=/bin/kill -HUP $MAINPID PIDFile=/run/keepalived/keepalived.pid diff --git a/src/helpers/vyos-failover.py b/src/helpers/vyos-failover.py index 0de945f20..ce4cf8fa4 100755 --- a/src/helpers/vyos-failover.py +++ b/src/helpers/vyos-failover.py @@ -30,7 +30,7 @@ my_name = Path(__file__).stem def is_route_exists(route, gateway, interface, metric): """Check if route with expected gateway, dev and metric exists""" - rc, data = rc_cmd(f'sudo ip --json route show protocol failover {route} ' + rc, data = rc_cmd(f'ip --json route show protocol failover {route} ' f'via {gateway} dev {interface} metric {metric}') if rc == 0: data = json.loads(data) @@ -72,6 +72,7 @@ def get_best_route_options(route, debug=False): f'best_metric: {best_metric}, best_iface: {best_interface}') return best_gateway, best_interface, best_metric + def is_port_open(ip, port): """ Check connection to remote host and port @@ -91,32 +92,70 @@ def is_port_open(ip, port): finally: s.close() -def is_target_alive(target=None, iface='', proto='icmp', port=None, debug=False): - """ - Host availability check by ICMP, ARP, TCP - Return True if target checks is successful - % is_target_alive('192.0.2.1', 'eth1', proto='arp') - True +def is_target_alive(target_list=None, + iface='', + proto='icmp', + port=None, + debug=False, + policy='any-available') -> bool: + """Check the availability of each target in the target_list using + the specified protocol ICMP, ARP, TCP + + Args: + target_list (list): A list of IP addresses or hostnames to check. + iface (str): The name of the network interface to use for the check. + proto (str): The protocol to use for the check. Options are 'icmp', 'arp', or 'tcp'. + port (int): The port number to use for the TCP check. Only applicable if proto is 'tcp'. + debug (bool): If True, print debug information during the check. + policy (str): The policy to use for the check. Options are 'any-available' or 'all-available'. + + Returns: + bool: True if all targets are reachable according to the policy, False otherwise. + + Example: + % is_target_alive(['192.0.2.1', '192.0.2.5'], 'eth1', proto='arp', policy='all-available') + True """ if iface != '': iface = f'-I {iface}' - if proto == 'icmp': - command = f'/usr/bin/ping -q {target} {iface} -n -c 2 -W 1' - rc, response = rc_cmd(command) - if debug: print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') - if rc == 0: - return True - elif proto == 'arp': - command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}' - rc, response = rc_cmd(command) - if debug: print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') - if rc == 0: + + num_reachable_targets = 0 + for target in target_list: + match proto: + case 'icmp': + command = f'/usr/bin/ping -q {target} {iface} -n -c 2 -W 1' + rc, response = rc_cmd(command) + if debug: + print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') + if rc == 0: + num_reachable_targets += 1 + if policy == 'any-available': + return True + + case 'arp': + command = f'/usr/bin/arping -b -c 2 -f -w 1 -i 1 {iface} {target}' + rc, response = rc_cmd(command) + if debug: + print(f' [ CHECK-TARGET ]: [{command}] -- return-code [RC: {rc}]') + if rc == 0: + num_reachable_targets += 1 + if policy == 'any-available': + return True + + case _ if proto == 'tcp' and port is not None: + if is_port_open(target, port): + num_reachable_targets += 1 + if policy == 'any-available': + return True + + case _: + return False + + if policy == 'all-available' and num_reachable_targets == len(target_list): return True - elif proto == 'tcp' and port is not None: - return True if is_port_open(target, port) else False - else: - return False + + return False if __name__ == '__main__': @@ -155,6 +194,7 @@ if __name__ == '__main__': conf_metric = int(nexthop_config.get('metric')) port = nexthop_config.get('check').get('port') port_opt = f'port {port}' if port else '' + policy = nexthop_config.get('check').get('policy') proto = nexthop_config.get('check').get('type') target = nexthop_config.get('check').get('target') timeout = nexthop_config.get('check').get('timeout') @@ -163,7 +203,7 @@ if __name__ == '__main__': if not is_route_exists(route, next_hop, conf_iface, conf_metric): if debug: print(f" [NEW_ROUTE_DETECTED] route: [{route}]") # Add route if check-target alive - if is_target_alive(target, conf_iface, proto, port, debug=debug): + if is_target_alive(target, conf_iface, proto, port, debug=debug, policy=policy): if debug: print(f' [ ADD ] -- ip route add {route} via {next_hop} dev {conf_iface} ' f'metric {conf_metric} proto failover\n###') rc, command = rc_cmd(f'ip route add {route} via {next_hop} dev {conf_iface} ' @@ -182,7 +222,7 @@ if __name__ == '__main__': # Route was added, check if the target is alive # We should delete route if check fails only if route exists in the routing table - if not is_target_alive(target, conf_iface, proto, port, debug=debug) and \ + if not is_target_alive(target, conf_iface, proto, port, debug=debug, policy=policy) and \ is_route_exists(route, next_hop, conf_iface, conf_metric): if debug: print(f'Nexh_hop {next_hop} fail, target not response') diff --git a/src/migration-scripts/bgp/3-to-4 b/src/migration-scripts/bgp/3-to-4 new file mode 100755 index 000000000..0df2fbec4 --- /dev/null +++ b/src/migration-scripts/bgp/3-to-4 @@ -0,0 +1,64 @@ +#!/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/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +bgp_base = ['protocols', 'bgp'] +# Check if BGP is configured - if so, migrate the CLI node +if config.exists(bgp_base): + if config.exists(bgp_base + ['route-map']): + tmp = config.return_value(bgp_base + ['route-map']) + + config.set(['system', 'ip', 'protocol', 'bgp', 'route-map'], value=tmp) + config.set_tag(['system', 'ip', 'protocol']) + config.delete(bgp_base + ['route-map']) + + +# Check if vrf names are configured. Check if BGP is configured - if so, migrate +# the CLI node(s) +if config.exists(['vrf', 'name']): + for vrf in config.list_nodes(['vrf', 'name']): + vrf_base = ['vrf', 'name', vrf] + if config.exists(vrf_base + ['protocols', 'bgp', 'route-map']): + tmp = config.return_value(vrf_base + ['protocols', 'bgp', 'route-map']) + + config.set(vrf_base + ['ip', 'protocol', 'bgp', 'route-map'], value=tmp) + config.set_tag(vrf_base + ['ip', 'protocol', 'bgp']) + config.delete(vrf_base + ['protocols', 'bgp', 'route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/dns-forwarding/3-to-4 b/src/migration-scripts/dns-forwarding/3-to-4 new file mode 100755 index 000000000..55165c2c5 --- /dev/null +++ b/src/migration-scripts/dns-forwarding/3-to-4 @@ -0,0 +1,49 @@ +#!/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/>. + +# T5115: migrate "service dns forwarding domain example.com server" to +# "service dns forwarding domain example.com name-server" + +import sys +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'dns', 'forwarding', 'domain'] +if not config.exists(base): + # Nothing to do + sys.exit(0) + +for domain in config.list_nodes(base): + if config.exists(base + [domain, 'server']): + config.copy(base + [domain, 'server'], base + [domain, 'name-server']) + config.delete(base + [domain, 'server']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/firewall/9-to-10 b/src/migration-scripts/firewall/9-to-10 new file mode 100755 index 000000000..6f67cc512 --- /dev/null +++ b/src/migration-scripts/firewall/9-to-10 @@ -0,0 +1,80 @@ +#!/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/>. + +# T5050: Log options +# cli changes from: +# set firewall [name | ipv6-name] <name> rule <number> log-level <log_level> +# To +# set firewall [name | ipv6-name] <name> rule <number> log-options level <log_level> + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree +from vyos.ifconfig import Section + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['firewall'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['name']): + for name in config.list_nodes(base + ['name']): + if not config.exists(base + ['name', name, 'rule']): + continue + + for rule in config.list_nodes(base + ['name', name, 'rule']): + log_options_base = base + ['name', name, 'rule', rule, 'log-options'] + rule_log_level = base + ['name', name, 'rule', rule, 'log-level'] + + if config.exists(rule_log_level): + tmp = config.return_value(rule_log_level) + config.delete(rule_log_level) + config.set(log_options_base + ['level'], value=tmp) + +if config.exists(base + ['ipv6-name']): + for name in config.list_nodes(base + ['ipv6-name']): + if not config.exists(base + ['ipv6-name', name, 'rule']): + continue + + for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): + log_options_base = base + ['ipv6-name', name, 'rule', rule, 'log-options'] + rule_log_level = base + ['ipv6-name', name, 'rule', rule, 'log-level'] + + if config.exists(rule_log_level): + tmp = config.return_value(rule_log_level) + config.delete(rule_log_level) + config.set(log_options_base + ['level'], value=tmp) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1)
\ No newline at end of file diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1 index ee4d6b82c..c7f324661 100755 --- a/src/migration-scripts/interfaces/0-to-1 +++ b/src/migration-scripts/interfaces/0-to-1 @@ -3,7 +3,7 @@ # Change syntax of bridge interface # - move interface based bridge-group to actual bridge (de-nest) # - make stp and igmp-snooping nodes valueless -# https://phabricator.vyos.net/T1556 +# https://vyos.dev/T1556 import sys from vyos.configtree import ConfigTree diff --git a/src/migration-scripts/interfaces/1-to-2 b/src/migration-scripts/interfaces/1-to-2 index 050137318..c75404d85 100755 --- a/src/migration-scripts/interfaces/1-to-2 +++ b/src/migration-scripts/interfaces/1-to-2 @@ -2,7 +2,7 @@ # Change syntax of bond interface # - move interface based bond-group to actual bond (de-nest) -# https://phabricator.vyos.net/T1614 +# https://vyos.dev/T1614 import sys from vyos.configtree import ConfigTree @@ -40,7 +40,7 @@ else: # some combinations were allowed in the past from a CLI perspective # but the kernel overwrote them - remove from CLI to not confuse the users. # In addition new consitency checks are in place so users can't repeat the - # mistake. One of those nice issues is https://phabricator.vyos.net/T532 + # mistake. One of those nice issues is https://vyos.dev/T532 for bond in config.list_nodes(base): if config.exists(base + [bond, 'arp-monitor', 'interval']) and config.exists(base + [bond, 'mode']): mode = config.return_value(base + [bond, 'mode']) diff --git a/src/migration-scripts/interfaces/16-to-17 b/src/migration-scripts/interfaces/16-to-17 index a6b4c7663..d123be06f 100755 --- a/src/migration-scripts/interfaces/16-to-17 +++ b/src/migration-scripts/interfaces/16-to-17 @@ -15,7 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # Command line migration of port mirroring -# https://phabricator.vyos.net/T3089 +# https://vyos.dev/T3089 import sys from vyos.configtree import ConfigTree diff --git a/src/migration-scripts/interfaces/2-to-3 b/src/migration-scripts/interfaces/2-to-3 index a63a54cdf..68d41de39 100755 --- a/src/migration-scripts/interfaces/2-to-3 +++ b/src/migration-scripts/interfaces/2-to-3 @@ -2,7 +2,7 @@ # Change syntax of openvpn encryption settings # - move cipher from encryption to encryption cipher -# https://phabricator.vyos.net/T1704 +# https://vyos.dev/T1704 import sys from vyos.configtree import ConfigTree diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21 index 0bd858760..cb1c36882 100755 --- a/src/migration-scripts/interfaces/20-to-21 +++ b/src/migration-scripts/interfaces/20-to-21 @@ -15,7 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # T3619: mirror Linux Kernel defaults for ethernet offloading options into VyOS -# CLI. See https://phabricator.vyos.net/T3619#102254 for all the details. +# CLI. See https://vyos.dev/T3619#102254 for all the details. # T3787: Remove deprecated UDP fragmentation offloading option from sys import argv diff --git a/src/migration-scripts/interfaces/26-to-27 b/src/migration-scripts/interfaces/26-to-27 new file mode 100755 index 000000000..949cc55b6 --- /dev/null +++ b/src/migration-scripts/interfaces/26-to-27 @@ -0,0 +1,49 @@ +#!/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/>. + +# T4995: pppoe, wwan, sstpc-client rename "authentication user" CLI node +# to "authentication username" + +from sys import argv + +from vyos.ethtool import Ethtool +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +for type in ['pppoe', 'sstpc-client', 'wwam']: + base = ['interfaces', type] + if not config.exists(base): + continue + for interface in config.list_nodes(base): + auth_base = base + [interface, 'authentication', 'user'] + if config.exists(auth_base): + config.rename(auth_base, 'username') + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/interfaces/27-to-28 b/src/migration-scripts/interfaces/27-to-28 new file mode 100755 index 000000000..6225d6414 --- /dev/null +++ b/src/migration-scripts/interfaces/27-to-28 @@ -0,0 +1,55 @@ +#!/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/>. + +# T5034: tunnel: rename "multicast enable" CLI node to "enable-multicast" +# valueless node. + +from sys import argv + +from vyos.ethtool import Ethtool +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['interfaces', 'tunnel'] +config = ConfigTree(config_file) + +if not config.exists(base): + exit(0) + +for ifname in config.list_nodes(base): + print(ifname) + multicast_base = base + [ifname, 'multicast'] + if config.exists(multicast_base): + tmp = config.return_value(multicast_base) + print(tmp) + # Delete old Config node + config.delete(multicast_base) + if tmp == 'enable': + config.set(base + [ifname, 'enable-multicast']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/interfaces/4-to-5 b/src/migration-scripts/interfaces/4-to-5 index 2a42c60ff..f645c5aeb 100755 --- a/src/migration-scripts/interfaces/4-to-5 +++ b/src/migration-scripts/interfaces/4-to-5 @@ -50,7 +50,7 @@ def migrate_dialer(config, tree, intf): # Remove IPv6 router-advert nodes as this makes no sense on a # client diale rinterface to send RAs back into the network - # https://phabricator.vyos.net/T2055 + # https://vyos.dev/T2055 ipv6_ra = pppoe_base + ['ipv6', 'router-advert'] if config.exists(ipv6_ra): config.delete(ipv6_ra) diff --git a/src/migration-scripts/ipsec/10-to-11 b/src/migration-scripts/ipsec/10-to-11 new file mode 100755 index 000000000..0707a5e3c --- /dev/null +++ b/src/migration-scripts/ipsec/10-to-11 @@ -0,0 +1,83 @@ +#!/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/>. + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['vpn', 'ipsec'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# PEER changes +if config.exists(base + ['site-to-site', 'peer']): + for peer in config.list_nodes(base + ['site-to-site', 'peer']): + peer_base = base + ['site-to-site', 'peer', peer] + + # replace: 'ipsec site-to-site peer <tag> authentication pre-shared-secret xxx' + # => 'ipsec authentication psk <tag> secret xxx' + if config.exists(peer_base + ['authentication', 'pre-shared-secret']): + tmp = config.return_value(peer_base + ['authentication', 'pre-shared-secret']) + config.delete(peer_base + ['authentication', 'pre-shared-secret']) + config.set(base + ['authentication', 'psk', peer, 'secret'], value=tmp) + # format as tag node to avoid loading problems + config.set_tag(base + ['authentication', 'psk']) + + # Get id's from peers for "ipsec auth psk <tag> id xxx" + if config.exists(peer_base + ['authentication', 'local-id']): + local_id = config.return_value(peer_base + ['authentication', 'local-id']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=local_id, replace=False) + if config.exists(peer_base + ['authentication', 'remote-id']): + remote_id = config.return_value(peer_base + ['authentication', 'remote-id']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=remote_id, replace=False) + + if config.exists(peer_base + ['local-address']): + tmp = config.return_value(peer_base + ['local-address']) + config.set(base + ['authentication', 'psk', peer, 'id'], value=tmp, replace=False) + if config.exists(peer_base + ['remote-address']): + tmp = config.return_values(peer_base + ['remote-address']) + if tmp: + for remote_addr in tmp: + if remote_addr == 'any': + remote_addr = '%any' + config.set(base + ['authentication', 'psk', peer, 'id'], value=remote_addr, replace=False) + + # get DHCP peer interface as psk dhcp-interface + if config.exists(peer_base + ['dhcp-interface']): + tmp = config.return_value(peer_base + ['dhcp-interface']) + config.set(base + ['authentication', 'psk', peer, 'dhcp-interface'], value=tmp) + + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/ipsec/11-to-12 b/src/migration-scripts/ipsec/11-to-12 new file mode 100755 index 000000000..8bbde5efa --- /dev/null +++ b/src/migration-scripts/ipsec/11-to-12 @@ -0,0 +1,53 @@ +#!/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/>. + +# Remove legacy ipsec.conf and ipsec.secrets - Not supported with swanctl + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['vpn', 'ipsec'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['include-ipsec-conf']): + config.delete(base + ['include-ipsec-conf']) + +if config.exists(base + ['include-ipsec-secrets']): + config.delete(base + ['include-ipsec-secrets']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/isis/2-to-3 b/src/migration-scripts/isis/2-to-3 new file mode 100755 index 000000000..4490feb0a --- /dev/null +++ b/src/migration-scripts/isis/2-to-3 @@ -0,0 +1,63 @@ +#!/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/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +isis_base = ['protocols', 'isis'] +# Check if IS-IS is configured - if so, migrate the CLI node +if config.exists(isis_base): + if config.exists(isis_base + ['route-map']): + tmp = config.return_value(isis_base + ['route-map']) + + config.set(['system', 'ip', 'protocol', 'isis', 'route-map'], value=tmp) + config.set_tag(['system', 'ip', 'protocol']) + config.delete(isis_base + ['route-map']) + +# Check if vrf names are configured. Check if IS-IS is configured - if so, +# migrate the CLI node(s) +if config.exists(['vrf', 'name']): + for vrf in config.list_nodes(['vrf', 'name']): + vrf_base = ['vrf', 'name', vrf] + if config.exists(vrf_base + ['protocols', 'isis', 'route-map']): + tmp = config.return_value(vrf_base + ['protocols', 'isis', 'route-map']) + + config.set(vrf_base + ['ip', 'protocol', 'isis', 'route-map'], value=tmp) + config.set_tag(vrf_base + ['ip', 'protocol', 'isis']) + config.delete(vrf_base + ['protocols', 'isis', 'route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/ntp/1-to-2 b/src/migration-scripts/ntp/1-to-2 index 4a701e7e5..d1e510e4c 100755 --- a/src/migration-scripts/ntp/1-to-2 +++ b/src/migration-scripts/ntp/1-to-2 @@ -37,6 +37,11 @@ if not config.exists(base_path): # Nothing to do sys.exit(0) +# config.copy does not recursively create a path, so create ['service'] if +# it doesn't yet exist, such as for config.boot.default +if not config.exists(['service']): + config.set(['service']) + # copy "system ntp" to "service ntp" config.copy(base_path, new_base_path) config.delete(base_path) diff --git a/src/migration-scripts/ospf/1-to-2 b/src/migration-scripts/ospf/1-to-2 new file mode 100755 index 000000000..a6beaf04e --- /dev/null +++ b/src/migration-scripts/ospf/1-to-2 @@ -0,0 +1,80 @@ +#!/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/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +ospf_base = ['protocols', 'ospf'] +# Check if OSPF is configured - if so, migrate the CLI node +if config.exists(ospf_base): + if config.exists(ospf_base + ['route-map']): + tmp = config.return_value(ospf_base + ['route-map']) + + config.set(['system', 'ip', 'protocol', 'ospf', 'route-map'], value=tmp) + config.set_tag(['system', 'ip', 'protocol']) + config.delete(ospf_base + ['route-map']) + +ospfv3_base = ['protocols', 'ospfv3'] +# Check if OSPFv3 is configured - if so, migrate the CLI node +if config.exists(ospfv3_base): + if config.exists(ospfv3_base + ['route-map']): + tmp = config.return_value(ospfv3_base + ['route-map']) + + config.set(['system', 'ipv6', 'protocol', 'ospfv3', 'route-map'], value=tmp) + config.set_tag(['system', 'ipv6', 'protocol']) + config.delete(ospfv3_base + ['route-map']) + +# Check if vrf names are configured. Check if OSPF/OSPFv3 is configured - if so, +# migrate the CLI node(s) +if config.exists(['vrf', 'name']): + for vrf in config.list_nodes(['vrf', 'name']): + vrf_base = ['vrf', 'name', vrf] + if config.exists(vrf_base + ['protocols', 'ospf', 'route-map']): + tmp = config.return_value(vrf_base + ['protocols', 'ospf', 'route-map']) + + config.set(vrf_base + ['ip', 'protocol', 'ospf', 'route-map'], value=tmp) + config.set_tag(vrf_base + ['ip', 'protocol', 'ospf']) + config.delete(vrf_base + ['protocols', 'ospf', 'route-map']) + + if config.exists(vrf_base + ['protocols', 'ospfv3', 'route-map']): + tmp = config.return_value(vrf_base + ['protocols', 'ospfv3', 'route-map']) + + config.set(vrf_base + ['ipv6', 'protocol', 'ospfv3', 'route-map'], value=tmp) + config.set_tag(vrf_base + ['ipv6', 'protocol', 'ospfv6']) + config.delete(vrf_base + ['protocols', 'ospfv3', 'route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/qos/1-to-2 b/src/migration-scripts/qos/1-to-2 index 41026cbd6..14d3a6e0a 100755 --- a/src/migration-scripts/qos/1-to-2 +++ b/src/migration-scripts/qos/1-to-2 @@ -118,6 +118,28 @@ if config.exists(netem_base): if config.exists(netem_base + [policy_name, 'burst']): config.delete(netem_base + [policy_name, 'burst']) +# Change bandwidth unit MBit -> mbit as tc only supports mbit +base = ['qos', 'policy'] +if config.exists(base): + for policy_type in config.list_nodes(base): + for policy in config.list_nodes(base + [policy_type]): + policy_base = base + [policy_type, policy] + if config.exists(policy_base + ['bandwidth']): + tmp = config.return_value(policy_base + ['bandwidth']) + config.set(policy_base + ['bandwidth'], value=tmp.lower()) + + if config.exists(policy_base + ['class']): + for cls in config.list_nodes(policy_base + ['class']): + cls_base = policy_base + ['class', cls] + if config.exists(cls_base + ['bandwidth']): + tmp = config.return_value(cls_base + ['bandwidth']) + config.set(cls_base + ['bandwidth'], value=tmp.lower()) + + if config.exists(policy_base + ['default', 'bandwidth']): + if config.exists(policy_base + ['default', 'bandwidth']): + tmp = config.return_value(policy_base + ['default', 'bandwidth']) + config.set(policy_base + ['default', 'bandwidth'], value=tmp.lower()) + try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/migration-scripts/quagga/10-to-11 b/src/migration-scripts/quagga/10-to-11 new file mode 100755 index 000000000..04fc16f79 --- /dev/null +++ b/src/migration-scripts/quagga/10-to-11 @@ -0,0 +1,51 @@ +#!/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/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +static_base = ['protocols', 'static'] +# Check if static routes are configured - if so, migrate the CLI node +if config.exists(static_base): + if config.exists(static_base + ['route-map']): + tmp = config.return_value(static_base + ['route-map']) + + config.set(['system', 'ip', 'protocol', 'static', 'route-map'], value=tmp) + config.set_tag(['system', 'ip', 'protocol']) + config.delete(static_base + ['route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/rip/0-to-1 b/src/migration-scripts/rip/0-to-1 new file mode 100755 index 000000000..60d510001 --- /dev/null +++ b/src/migration-scripts/rip/0-to-1 @@ -0,0 +1,51 @@ +#!/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/>. + +# T5150: Rework CLI definitions to apply route-maps between routing daemons +# and zebra/kernel + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +ripng_base = ['protocols', 'ripng'] +# Check if RIPng is configured - if so, migrate the CLI node +if config.exists(ripng_base): + if config.exists(ripng_base + ['route-map']): + tmp = config.return_value(ripng_base + ['route-map']) + + config.set(['system', 'ipv6', 'protocol', 'ripng', 'route-map'], value=tmp) + config.set_tag(['system', 'ipv6', 'protocol']) + config.delete(ripng_base + ['route-map']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/snmp/0-to-1 b/src/migration-scripts/snmp/0-to-1 index a836f7011..096ba779d 100755 --- a/src/migration-scripts/snmp/0-to-1 +++ b/src/migration-scripts/snmp/0-to-1 @@ -33,18 +33,18 @@ if not config.exists(config_base): # Nothing to do sys.exit(0) else: - # we no longer support a per trap target engine ID (https://phabricator.vyos.net/T818) + # we no longer support a per trap target engine ID (https://vyos.dev/T818) if config.exists(config_base + ['v3', 'trap-target']): for target in config.list_nodes(config_base + ['v3', 'trap-target']): config.delete(config_base + ['v3', 'trap-target', target, 'engineid']) - # we no longer support a per user engine ID (https://phabricator.vyos.net/T818) + # we no longer support a per user engine ID (https://vyos.dev/T818) if config.exists(config_base + ['v3', 'user']): for user in config.list_nodes(config_base + ['v3', 'user']): config.delete(config_base + ['v3', 'user', user, 'engineid']) # we drop TSM support as there seem to be no users and this code is untested - # https://phabricator.vyos.net/T1769 + # https://vyos.dev/T1769 if config.exists(config_base + ['v3', 'tsm']): config.delete(config_base + ['v3', 'tsm']) diff --git a/src/migration-scripts/system/25-to-26 b/src/migration-scripts/system/25-to-26 new file mode 100755 index 000000000..615274430 --- /dev/null +++ b/src/migration-scripts/system/25-to-26 @@ -0,0 +1,82 @@ +#!/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/>. +# +# syslog: migrate deprecated CLI options +# - protocols -> local7 +# - security -> auth + +from sys import exit, argv +from vyos.configtree import ConfigTree + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['system', 'syslog'] +config = ConfigTree(config_file) + +if not config.exists(base): + exit(0) + +def rename_facilities(config, base_tree, facility, facility_new) -> None: + if config.exists(base + [base_tree, 'facility', facility]): + # do not overwrite already existing replacement facility + if not config.exists(base + [base_tree, 'facility', facility_new]): + config.rename(base + [base_tree, 'facility', facility], facility_new) + else: + # delete old duplicate facility config + config.delete(base + [base_tree, 'facility', facility]) + +# +# Rename protocols and securityy facility to common ones +# +replace = { + 'protocols' : 'local7', + 'security' : 'auth' +} +for facility, facility_new in replace.items(): + rename_facilities(config, 'console', facility, facility_new) + rename_facilities(config, 'global', facility, facility_new) + + if config.exists(base + ['host']): + for host in config.list_nodes(base + ['host']): + rename_facilities(config, f'host {host}', facility, facility_new) + +# +# It makes no sense to configure udp/tcp transport per individual facility +# +if config.exists(base + ['host']): + for host in config.list_nodes(base + ['host']): + protocol = None + for facility in config.list_nodes(base + ['host', host, 'facility']): + tmp_path = base + ['host', host, 'facility', facility, 'protocol'] + if config.exists(tmp_path): + # We can only change the first one + if protocol == None: + protocol = config.return_value(tmp_path) + config.set(base + ['host', host, 'protocol'], value=protocol) + config.delete(tmp_path) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/op_mode/accelppp.py b/src/op_mode/accelppp.py index 2fd045dc3..00de45fc8 100755 --- a/src/op_mode/accelppp.py +++ b/src/op_mode/accelppp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -27,34 +27,56 @@ from vyos.util import rc_cmd accel_dict = { 'ipoe': { 'port': 2002, - 'path': 'service ipoe-server' + 'path': 'service ipoe-server', + 'base_path': 'service ipoe-server' }, 'pppoe': { 'port': 2001, - 'path': 'service pppoe-server' + 'path': 'service pppoe-server', + 'base_path': 'service pppoe-server' }, 'pptp': { 'port': 2003, - 'path': 'vpn pptp' + 'path': 'vpn pptp', + 'base_path': 'vpn pptp' }, 'l2tp': { 'port': 2004, - 'path': 'vpn l2tp' + 'path': 'vpn l2tp', + 'base_path': 'vpn l2tp remote-access' }, 'sstp': { 'port': 2005, - 'path': 'vpn sstp' + 'path': 'vpn sstp', + 'base_path': 'vpn sstp' } } -def _get_raw_statistics(accel_output, pattern): - return vyos.accel_ppp.get_server_statistics(accel_output, pattern, sep=':') +def _get_config_settings(protocol): + '''Get config dict from VyOS configuration''' + conf = ConfigTreeQuery() + base_path = accel_dict[protocol]['base_path'] + data = conf.get_config_dict(base_path, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + if conf.exists(f'{base_path} authentication local-users'): + # Delete sensitive data + del data['authentication']['local_users'] + return {'config_option': data} + + +def _get_raw_statistics(accel_output, pattern, protocol): + return { + **vyos.accel_ppp.get_server_statistics(accel_output, pattern, sep=':'), + **_get_config_settings(protocol) + } def _get_raw_sessions(port): - cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,state,' \ - 'uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \ + cmd_options = 'show sessions ifname,username,ip,ip6,ip6-dp,type,rate-limit,' \ + 'state,uptime-raw,calling-sid,called-sid,sid,comp,rx-bytes-raw,' \ 'tx-bytes-raw,rx-pkts,tx-pkts' output = vyos.accel_ppp.accel_cmd(port, cmd_options) parsed_data: list[dict[str, str]] = vyos.accel_ppp.accel_out_parse( @@ -103,7 +125,7 @@ def show_statistics(raw: bool, protocol: str): rc, output = rc_cmd(f'/usr/bin/accel-cmd -p {port} show stat') if raw: - return _get_raw_statistics(output, pattern) + return _get_raw_statistics(output, pattern, protocol) return output diff --git a/src/op_mode/bgp.py b/src/op_mode/bgp.py index 23001a9d7..af9ea788b 100755 --- a/src/op_mode/bgp.py +++ b/src/op_mode/bgp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,100 +15,133 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # # Purpose: -# Displays bgp neighbors information. -# Used by the "show bgp (vrf <tag>) ipv4|ipv6 neighbors" commands. +# Displays BGP neighbors and tables information. import re import sys import typing -import jmespath from jinja2 import Template -from humps import decamelize - -from vyos.configquery import ConfigTreeQuery import vyos.opmode - frr_command_template = Template(""" -{% if family %} - show bgp - {{ 'vrf ' ~ vrf if vrf else '' }} - {{ 'ipv6' if family == 'inet6' else 'ipv4'}} - {{ 'neighbor ' ~ peer if peer else 'summary' }} +show bgp + +{## VRF and family modifiers that may precede any options ##} + +{% if vrf %} + vrf {{vrf}} +{% endif %} + +{% if family == "inet" %} + ipv4 +{% elif family == "inet6" %} + ipv6 +{% elif family == "l2vpn" %} + l2vpn evpn +{% endif %} + +{% if family_modifier == "unicast" %} + unicast +{% elif family_modifier == "multicast" %} + multicast +{% elif family_modifier == "flowspec" %} + flowspec +{% elif family_modifier == "vpn" %} + vpn +{% endif %} + +{## Mutually exclusive query parameters ##} + +{# Network prefix #} +{% if prefix %} + {{prefix}} + + {% if longer_prefixes %} + longer-prefixes + {% elif best_path %} + bestpath + {% endif %} {% endif %} +{# Regex #} +{% if regex %} + regex {{regex}} +{% endif %} + +{## Raw modifier ##} + {% if raw %} json {% endif %} """) +ArgFamily = typing.Literal['inet', 'inet6', 'l2vpn'] +ArgFamilyModifier = typing.Literal['unicast', 'labeled_unicast', 'multicast', 'vpn', 'flowspec'] + +def show_summary(raw: bool): + from vyos.util import cmd + + if raw: + from json import loads + + output = cmd(f"vtysh -c 'show bgp summary json'").strip() -def _verify(func): - """Decorator checks if BGP config exists - BGP configuration can be present under vrf <tag> - If we do npt get arg 'peer' then it can be 'bgp summary' - """ - from functools import wraps - - @wraps(func) - def _wrapper(*args, **kwargs): - config = ConfigTreeQuery() - afi = 'ipv6' if kwargs.get('family') == 'inet6' else 'ipv4' - global_vrfs = ['all', 'default'] - peer = kwargs.get('peer') - vrf = kwargs.get('vrf') - unconf_message = f'BGP or neighbor is not configured' - # Add option to check the specific neighbor if we have arg 'peer' - peer_opt = f'neighbor {peer} address-family {afi}-unicast' if peer else '' - vrf_opt = '' - if vrf and vrf not in global_vrfs: - vrf_opt = f'vrf name {vrf}' - # Check if config does not exist - if not config.exists(f'{vrf_opt} protocols bgp {peer_opt}'): - raise vyos.opmode.UnconfiguredSubsystem(unconf_message) - return func(*args, **kwargs) - - return _wrapper - - -@_verify -def show_neighbors(raw: bool, - family: str, - peer: typing.Optional[str], - vrf: typing.Optional[str]): - kwargs = dict(locals()) - frr_command = frr_command_template.render(kwargs) - frr_command = re.sub(r'\s+', ' ', frr_command) + # FRR 8.5 correctly returns an empty object when BGP is not running, + # we don't need to do anything special here + return loads(output) + else: + output = cmd(f"vtysh -c 'show bgp summary'") + return output +def show_neighbors(raw: bool): from vyos.util import cmd - output = cmd(f"vtysh -c '{frr_command}'") + from vyos.utils.dict import dict_to_list if raw: from json import loads - data = loads(output) - # Get list of the peers - peers = jmespath.search('*.peers | [0]', data) - if peers: - # Create new dict, delete old key 'peers' - # add key 'peers' neighbors to the list - list_peers = [] - new_dict = jmespath.search('* | [0]', data) - if 'peers' in new_dict: - new_dict.pop('peers') - - for neighbor, neighbor_options in peers.items(): - neighbor_options['neighbor'] = neighbor - list_peers.append(neighbor_options) - new_dict['peers'] = list_peers - return decamelize(new_dict) - data = jmespath.search('* | [0]', data) - return decamelize(data) + output = cmd(f"vtysh -c 'show bgp neighbors json'").strip() + d = loads(output) + return dict_to_list(d, save_key_to="neighbor") else: + output = cmd(f"vtysh -c 'show bgp neighbors'") return output +def show(raw: bool, + family: ArgFamily, + family_modifier: ArgFamilyModifier, + prefix: typing.Optional[str], + longer_prefixes: typing.Optional[bool], + best_path: typing.Optional[bool], + regex: typing.Optional[str], + vrf: typing.Optional[str]): + from vyos.utils.dict import dict_to_list + + if (longer_prefixes or best_path) and (prefix is None): + raise ValueError("longer_prefixes and best_path can only be used when prefix is given") + elif (family == "l2vpn") and (family_modifier is not None): + raise ValueError("l2vpn family does not accept any modifiers") + else: + kwargs = dict(locals()) + + frr_command = frr_command_template.render(kwargs) + frr_command = re.sub(r'\s+', ' ', frr_command) + + from vyos.util import cmd + output = cmd(f"vtysh -c '{frr_command}'") + + if raw: + from json import loads + d = loads(output) + if not ("routes" in d): + raise vyos.opmode.InternalError("FRR returned a BGP table with no routes field") + d = d["routes"] + routes = dict_to_list(d, save_key_to="route_key") + return routes + else: + return output if __name__ == '__main__': try: diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py index df213cc5a..ea7c4c208 100755 --- a/src/op_mode/conntrack.py +++ b/src/op_mode/conntrack.py @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys +import typing import xmltodict from tabulate import tabulate @@ -23,6 +24,7 @@ from vyos.util import run import vyos.opmode +ArgFamily = typing.Literal['inet', 'inet6'] def _get_xml_data(family): """ @@ -126,7 +128,7 @@ def get_formatted_output(dict_data): return output -def show(raw: bool, family: str): +def show(raw: bool, family: ArgFamily): family = 'ipv6' if family == 'inet6' else 'ipv4' conntrack_data = _get_raw_data(family) if raw: diff --git a/src/op_mode/conntrack_sync.py b/src/op_mode/conntrack_sync.py index 54ecd6d0e..c3345a936 100755 --- a/src/op_mode/conntrack_sync.py +++ b/src/op_mode/conntrack_sync.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,9 +15,12 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import sys import syslog import xmltodict +import vyos.opmode + from argparse import ArgumentParser from vyos.configquery import CliShellApiConfigQuery from vyos.configquery import ConfigTreeQuery @@ -31,36 +34,23 @@ conntrackd_bin = '/usr/sbin/conntrackd' conntrackd_config = '/run/conntrackd/conntrackd.conf' failover_state_file = '/var/run/vyatta-conntrackd-failover-state' -parser = ArgumentParser(description='Conntrack Sync') -group = parser.add_mutually_exclusive_group() -group.add_argument('--restart', help='Restart connection tracking synchronization service', action='store_true') -group.add_argument('--reset-cache-internal', help='Reset internal cache', action='store_true') -group.add_argument('--reset-cache-external', help='Reset external cache', action='store_true') -group.add_argument('--show-internal', help='Show internal (main) tracking cache', action='store_true') -group.add_argument('--show-external', help='Show external (main) tracking cache', action='store_true') -group.add_argument('--show-internal-expect', help='Show internal (expect) tracking cache', action='store_true') -group.add_argument('--show-external-expect', help='Show external (expect) tracking cache', action='store_true') -group.add_argument('--show-statistics', help='Show connection syncing statistics', action='store_true') -group.add_argument('--show-status', help='Show conntrack-sync status', action='store_true') - def is_configured(): """ Check if conntrack-sync service is configured """ config = CliShellApiConfigQuery() if not config.exists(['service', 'conntrack-sync']): - print('Service conntrackd-sync not configured!') - exit(1) + raise vyos.opmode.UnconfiguredSubsystem("conntrack-sync is not configured!") def send_bulk_update(): """ send bulk update of internal-cache to other systems """ tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -B') if tmp > 0: - print('ERROR: failed to send bulk update to other conntrack-sync systems') + raise vyos.opmode.Error('Failed to send bulk update to other conntrack-sync systems') def request_sync(): """ request resynchronization with other systems """ tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -n') if tmp > 0: - print('ERROR: failed to request resynchronization of external cache') + raise vyos.opmode.Error('Failed to request resynchronization of external cache') def flush_cache(direction): """ flush conntrackd cache (internal or external) """ @@ -68,9 +58,9 @@ def flush_cache(direction): raise ValueError() tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -f {direction}') if tmp > 0: - print('ERROR: failed to clear {direction} cache') + raise vyos.opmode.Error('Failed to clear {direction} cache') -def xml_to_stdout(xml): +def from_xml(raw, xml): out = [] for line in xml.splitlines(): if line == '\n': @@ -78,108 +68,131 @@ def xml_to_stdout(xml): parsed = xmltodict.parse(line) out.append(parsed) - print(render_to_string('conntrackd/conntrackd.op-mode.j2', {'data' : out})) - -if __name__ == '__main__': - args = parser.parse_args() - syslog.openlog(ident='conntrack-tools', logoption=syslog.LOG_PID, - facility=syslog.LOG_INFO) + if raw: + return out + else: + return render_to_string('conntrackd/conntrackd.op-mode.j2', {'data' : out}) + +def restart(): + is_configured() + if commit_in_progress(): + raise vyos.opmode.CommitInProgress('Cannot restart conntrackd while a commit is in progress') + + syslog.syslog('Restarting conntrack sync service...') + cmd('systemctl restart conntrackd.service') + # request resynchronization with other systems + request_sync() + # send bulk update of internal-cache to other systems + send_bulk_update() + +def reset_external_cache(): + is_configured() + syslog.syslog('Resetting external cache of conntrack sync service...') + + # flush the external cache + flush_cache('external') + # request resynchronization with other systems + request_sync() + +def reset_internal_cache(): + is_configured() + syslog.syslog('Resetting internal cache of conntrack sync service...') + # flush the internal cache + flush_cache('internal') + + # request resynchronization of internal cache with kernel conntrack table + tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -R') + if tmp > 0: + print('ERROR: failed to resynchronize internal cache with kernel conntrack table') - if args.restart: - is_configured() - if commit_in_progress(): - print('Cannot restart conntrackd while a commit is in progress') - exit(1) - - syslog.syslog('Restarting conntrack sync service...') - cmd('systemctl restart conntrackd.service') - # request resynchronization with other systems - request_sync() - # send bulk update of internal-cache to other systems - send_bulk_update() - - elif args.reset_cache_external: - is_configured() - syslog.syslog('Resetting external cache of conntrack sync service...') + # send bulk update of internal-cache to other systems + send_bulk_update() - # flush the external cache - flush_cache('external') - # request resynchronization with other systems - request_sync() +def _show_cache(raw, opts): + is_configured() + out = cmd(f'{conntrackd_bin} -C {conntrackd_config} {opts} -x') + return from_xml(raw, out) - elif args.reset_cache_internal: - is_configured() - syslog.syslog('Resetting internal cache of conntrack sync service...') - # flush the internal cache - flush_cache('internal') +def show_external_cache(raw: bool): + opts = '-e ct' + return _show_cache(raw, opts) - # request resynchronization of internal cache with kernel conntrack table - tmp = run(f'{conntrackd_bin} -C {conntrackd_config} -R') - if tmp > 0: - print('ERROR: failed to resynchronize internal cache with kernel conntrack table') +def show_external_expect(raw: bool): + opts = '-e expect' + return _show_cache(raw, opts) - # send bulk update of internal-cache to other systems - send_bulk_update() +def show_internal_cache(raw: bool): + opts = '-i ct' + return _show_cache(raw, opts) - elif args.show_external or args.show_internal or args.show_external_expect or args.show_internal_expect: - is_configured() - opt = '' - if args.show_external: - opt = '-e ct' - elif args.show_external_expect: - opt = '-e expect' - elif args.show_internal: - opt = '-i ct' - elif args.show_internal_expect: - opt = '-i expect' - - if args.show_external or args.show_internal: - print('Main Table Entries:') - else: - print('Expect Table Entries:') - out = cmd(f'sudo {conntrackd_bin} -C {conntrackd_config} {opt} -x') - xml_to_stdout(out) +def show_internal_expect(raw: bool): + opts = '-i expect' + return _show_cache(raw, opts) - elif args.show_statistics: +def show_statistics(raw: bool): + if raw: + raise vyos.opmode.UnsupportedOperation("Machine-readable conntrack-sync statistics are not available yet") + else: is_configured() config = ConfigTreeQuery() print('\nMain Table Statistics:\n') - call(f'sudo {conntrackd_bin} -C {conntrackd_config} -s') + call(f'{conntrackd_bin} -C {conntrackd_config} -s') print() if config.exists(['service', 'conntrack-sync', 'expect-sync']): print('\nExpect Table Statistics:\n') - call(f'sudo {conntrackd_bin} -C {conntrackd_config} -s exp') + call(f'{conntrackd_bin} -C {conntrackd_config} -s exp') print() - elif args.show_status: - is_configured() - config = ConfigTreeQuery() - ct_sync_intf = config.list_nodes(['service', 'conntrack-sync', 'interface']) - ct_sync_intf = ', '.join(ct_sync_intf) - failover_state = "no transition yet!" - expect_sync_protocols = "disabled" - - if config.exists(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp']): - failover_mechanism = "vrrp" - vrrp_sync_grp = config.value(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group']) - - if os.path.isfile(failover_state_file): - with open(failover_state_file, "r") as f: - failover_state = f.readline() - - if config.exists(['service', 'conntrack-sync', 'expect-sync']): - expect_sync_protocols = config.values(['service', 'conntrack-sync', 'expect-sync']) - if 'all' in expect_sync_protocols: - expect_sync_protocols = ["ftp", "sip", "h323", "nfs", "sqlnet"] +def show_status(raw: bool): + is_configured() + config = ConfigTreeQuery() + ct_sync_intf = config.list_nodes(['service', 'conntrack-sync', 'interface']) + ct_sync_intf = ', '.join(ct_sync_intf) + failover_state = "no transition yet!" + expect_sync_protocols = [] + + if config.exists(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp']): + failover_mechanism = "vrrp" + vrrp_sync_grp = config.value(['service', 'conntrack-sync', 'failover-mechanism', 'vrrp', 'sync-group']) + + if os.path.isfile(failover_state_file): + with open(failover_state_file, "r") as f: + failover_state = f.readline() + + if config.exists(['service', 'conntrack-sync', 'expect-sync']): + expect_sync_protocols = config.values(['service', 'conntrack-sync', 'expect-sync']) + if 'all' in expect_sync_protocols: + expect_sync_protocols = ["ftp", "sip", "h323", "nfs", "sqlnet"] + + if raw: + status_data = { + "sync_interface": ct_sync_intf, + "failover_mechanism": failover_mechanism, + "sync_group": vrrp_sync_grp, + "last_transition": failover_state, + "sync_protocols": expect_sync_protocols + } + + return status_data + else: + if expect_sync_protocols: expect_sync_protocols = ', '.join(expect_sync_protocols) - + else: + expect_sync_protocols = "disabled" show_status = (f'\nsync-interface : {ct_sync_intf}\n' f'failover-mechanism : {failover_mechanism} [sync-group {vrrp_sync_grp}]\n' - f'last state transition : {failover_state}' + f'last state transition : {failover_state}\n' f'ExpectationSync : {expect_sync_protocols}') - print(show_status) + return show_status - else: - parser.print_help() - exit(1) +if __name__ == '__main__': + syslog.openlog(ident='conntrack-tools', logoption=syslog.LOG_PID, facility=syslog.LOG_INFO) + + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index b9e6e7bc9..fe7f252ba 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-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 @@ -36,6 +36,9 @@ lease_valid_states = ['all', 'active', 'free', 'expired', 'released', 'abandoned sort_valid_inet = ['end', 'mac', 'hostname', 'ip', 'pool', 'remaining', 'start', 'state'] sort_valid_inet6 = ['end', 'iaid_duid', 'ip', 'last_communication', 'pool', 'remaining', 'state', 'type'] +ArgFamily = typing.Literal['inet', 'inet6'] +ArgState = typing.Literal['all', 'active', 'free', 'expired', 'released', 'abandoned', 'reset', 'backup'] + def _utc_to_local(utc_dt): return datetime.fromtimestamp((datetime.fromtimestamp(utc_dt) - datetime(1970, 1, 1)).total_seconds()) @@ -82,7 +85,7 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[]) -> l data_lease['ip'] = lease.ip data_lease['state'] = lease.binding_state data_lease['pool'] = lease.sets.get('shared-networkname', '') - data_lease['end'] = lease.end.timestamp() + data_lease['end'] = lease.end.timestamp() if lease.end else None if family == 'inet': data_lease['mac'] = lease.ethernet @@ -95,17 +98,18 @@ def _get_raw_server_leases(family='inet', pool=None, sorted=None, state=[]) -> l lease_types_long = {'na': 'non-temporary', 'ta': 'temporary', 'pd': 'prefix delegation'} data_lease['type'] = lease_types_long[lease.type] - data_lease['remaining'] = lease.end - datetime.utcnow() + data_lease['remaining'] = '-' - if data_lease['remaining'].days >= 0: - # substraction gives us a timedelta object which can't be formatted with strftime - # so we use str(), split gets rid of the microseconds - data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] - else: - data_lease['remaining'] = '' + if lease.end: + data_lease['remaining'] = lease.end - datetime.utcnow() + + if data_lease['remaining'].days >= 0: + # substraction gives us a timedelta object which can't be formatted with strftime + # so we use str(), split gets rid of the microseconds + data_lease['remaining'] = str(data_lease["remaining"]).split('.')[0] # Do not add old leases - if data_lease['remaining'] != '' and data_lease['pool'] in pool: + if data_lease['remaining'] != '' and data_lease['pool'] in pool and data_lease['state'] != 'free': if not state or data_lease['state'] in state: data.append(data_lease) @@ -137,7 +141,7 @@ def _get_formatted_server_leases(raw_data, family='inet'): start = lease.get('start') start = _utc_to_local(start).strftime('%Y/%m/%d %H:%M:%S') end = lease.get('end') - end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') + end = _utc_to_local(end).strftime('%Y/%m/%d %H:%M:%S') if end else '-' remain = lease.get('remaining') pool = lease.get('pool') hostname = lease.get('hostname') @@ -248,7 +252,7 @@ def _verify(func): @_verify -def show_pool_statistics(raw: bool, family: str, pool: typing.Optional[str]): +def show_pool_statistics(raw: bool, family: ArgFamily, pool: typing.Optional[str]): pool_data = _get_raw_pool_statistics(family=family, pool=pool) if raw: return pool_data @@ -257,11 +261,13 @@ def show_pool_statistics(raw: bool, family: str, pool: typing.Optional[str]): @_verify -def show_server_leases(raw: bool, family: str, pool: typing.Optional[str], - sorted: typing.Optional[str], state: typing.Optional[str]): +def show_server_leases(raw: bool, family: ArgFamily, pool: typing.Optional[str], + sorted: typing.Optional[str], state: typing.Optional[ArgState]): # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if not is_systemd_service_running('isc-dhcp-server.service'): - Warning('DHCP server is configured but not started. Data may be stale.') + v = '6' if family == 'inet6' else '' + service_name = 'DHCPv6' if family == 'inet6' else 'DHCP' + if not is_systemd_service_running(f'isc-dhcp-server{v}.service'): + Warning(f'{service_name} server is configured but not started. Data may be stale.') v = 'v6' if family == 'inet6' else '' if pool and pool not in _get_dhcp_pools(family=family): diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py index a0e47d7ad..f8863c530 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -17,7 +17,6 @@ import sys -from sys import exit from tabulate import tabulate from vyos.configquery import ConfigTreeQuery @@ -75,8 +74,7 @@ def show_forwarding_statistics(raw: bool): config = ConfigTreeQuery() if not config.exists('service dns forwarding'): - print("DNS forwarding is not configured") - exit(0) + raise vyos.opmode.UnconfiguredSubsystem('DNS forwarding is not configured') dns_data = _get_raw_forwarding_statistics() if raw: diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py index 263a3b6a5..d41a74db3 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dynamic_dns.py @@ -16,69 +16,75 @@ import os import argparse -import jinja2 import sys import time +from tabulate import tabulate from vyos.config import Config +from vyos.template import is_ipv4, is_ipv6 from vyos.util import call cache_file = r'/run/ddclient/ddclient.cache' -OUT_TMPL_SRC = """ -{% for entry in hosts %} -ip address : {{ entry.ip }} -host-name : {{ entry.host }} -last update : {{ entry.time }} -update-status: {{ entry.status }} +columns = { + 'host': 'Hostname', + 'ipv4': 'IPv4 address', + 'status-ipv4': 'IPv4 status', + 'ipv6': 'IPv6 address', + 'status-ipv6': 'IPv6 status', + 'mtime': 'Last update', +} + + +def _get_formatted_host_records(host_data): + data_entries = [] + for entry in host_data: + data_entries.append([entry.get(key) for key in columns.keys()]) + + header = columns.values() + output = tabulate(data_entries, header, numalign='left') + return output -{% endfor %} -""" def show_status(): - # A ddclient status file must not always exist + # A ddclient status file might not always exist if not os.path.exists(cache_file): sys.exit(0) - data = { - 'hosts': [] - } + data = [] with open(cache_file, 'r') as f: for line in f: if line.startswith('#'): continue - outp = { - 'host': '', - 'ip': '', - 'time': '' - } - - if 'host=' in line: - host = line.split('host=')[1] - if host: - outp['host'] = host.split(',')[0] - - if 'ip=' in line: - ip = line.split('ip=')[1] - if ip: - outp['ip'] = ip.split(',')[0] - - if 'mtime=' in line: - mtime = line.split('mtime=')[1] - if mtime: - outp['time'] = time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(int(mtime.split(',')[0], base=10))) - - if 'status=' in line: - status = line.split('status=')[1] - if status: - outp['status'] = status.split(',')[0] - - data['hosts'].append(outp) - - tmpl = jinja2.Template(OUT_TMPL_SRC) - print(tmpl.render(data)) + props = {} + # ddclient cache rows have properties in 'key=value' format separated by comma + # we pick up the ones we are interested in + for kvraw in line.split(' ')[0].split(','): + k, v = kvraw.split('=') + if k in list(columns.keys()) + ['ip', 'status']: # ip and status are legacy keys + props[k] = v + + # Extract IPv4 and IPv6 address and status from legacy keys + # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6 + if 'ip' in props: + if is_ipv4(props['ip']): + props['ipv4'] = props['ip'] + props['status-ipv4'] = props['status'] + elif is_ipv6(props['ip']): + props['ipv6'] = props['ip'] + props['status-ipv6'] = props['status'] + del props['ip'] + + # Convert mtime to human readable format + if 'mtime' in props: + props['mtime'] = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) + + data.append(props) + + print(_get_formatted_host_records(data)) def update_ddns(): diff --git a/src/op_mode/generate_public_key_command.py b/src/op_mode/generate_public_key_command.py index f071ae350..8ba55c901 100755 --- a/src/op_mode/generate_public_key_command.py +++ b/src/op_mode/generate_public_key_command.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,28 +19,51 @@ import sys import urllib.parse import vyos.remote +from vyos.template import generate_uuid4 -def get_key(path): + +def get_key(path) -> list: + """Get public keys from a local file or remote URL + + Args: + path: Path to the public keys file + + Returns: list of public keys split by new line + + """ url = urllib.parse.urlparse(path) if url.scheme == 'file' or url.scheme == '': with open(os.path.expanduser(path), 'r') as f: key_string = f.read() else: key_string = vyos.remote.get_remote_config(path) - return key_string.split() - -try: - username = sys.argv[1] - algorithm, key, identifier = get_key(sys.argv[2]) -except Exception as e: - print("Failed to retrieve the public key: {}".format(e)) - sys.exit(1) - -print('# To add this key as an embedded key, run the following commands:') -print('configure') -print(f'set system login user {username} authentication public-keys {identifier} key {key}') -print(f'set system login user {username} authentication public-keys {identifier} type {algorithm}') -print('commit') -print('save') -print('exit') + return key_string.split('\n') + + +if __name__ == "__main__": + first_loop = True + + for k in get_key(sys.argv[2]): + k = k.split() + # Skip empty list entry + if k == []: + continue + + try: + username = sys.argv[1] + # Github keys don't have identifier for example 'vyos@localhost' + # 'ssh-rsa AAAA... vyos@localhost' + # Generate uuid4 identifier + identifier = f'github@{generate_uuid4("")}' if sys.argv[2].startswith('https://github.com') else k[2] + algorithm, key = k[0], k[1] + except Exception as e: + print("Failed to retrieve the public key: {}".format(e)) + sys.exit(1) + + if first_loop: + print('# To add this key as an embedded key, run the following commands:') + print('configure') + print(f'set system login user {username} authentication public-keys {identifier} key {key}') + print(f'set system login user {username} authentication public-keys {identifier} type {algorithm}') + first_loop = False diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py index 678c74980..f38b95a71 100755 --- a/src/op_mode/interfaces.py +++ b/src/op_mode/interfaces.py @@ -207,7 +207,11 @@ def _get_raw_data(ifname: typing.Optional[str], res_intf['description'] = interface.get_alias() - res_intf['stats'] = interface.operational.get_stats() + stats = interface.operational.get_stats() + for k in list(stats): + stats[k] = _get_counter_val(cache[k], stats[k]) + + res_intf['stats'] = stats ret.append(res_intf) @@ -273,6 +277,10 @@ def _get_counter_data(ifname: typing.Optional[str], res_intf['rx_bytes'] = _get_counter_val(cache['rx_bytes'], stats['rx_bytes']) res_intf['tx_packets'] = _get_counter_val(cache['tx_packets'], stats['tx_packets']) res_intf['tx_bytes'] = _get_counter_val(cache['tx_bytes'], stats['tx_bytes']) + res_intf['rx_dropped'] = _get_counter_val(cache['rx_dropped'], stats['rx_dropped']) + res_intf['tx_dropped'] = _get_counter_val(cache['tx_dropped'], stats['tx_dropped']) + res_intf['rx_over_errors'] = _get_counter_val(cache['rx_over_errors'], stats['rx_over_errors']) + res_intf['tx_carrier_errors'] = _get_counter_val(cache['tx_carrier_errors'], stats['tx_carrier_errors']) ret.append(res_intf) @@ -364,19 +372,23 @@ def _format_show_summary(data): @catch_broken_pipe def _format_show_counters(data: list): - formatting = '%-12s %10s %10s %10s %10s' - print(formatting % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes')) - - for intf in data: - print(formatting % ( - intf['ifname'], - intf['rx_packets'], - intf['rx_bytes'], - intf['tx_packets'], - intf['tx_bytes'] - )) - - return 0 + data_entries = [] + for entry in data: + interface = entry.get('ifname') + rx_packets = entry.get('rx_packets') + rx_bytes = entry.get('rx_bytes') + tx_packets = entry.get('tx_packets') + tx_bytes = entry.get('tx_bytes') + rx_dropped = entry.get('rx_dropped') + tx_dropped = entry.get('tx_dropped') + rx_errors = entry.get('rx_over_errors') + tx_errors = entry.get('tx_carrier_errors') + data_entries.append([interface, rx_packets, rx_bytes, tx_packets, tx_bytes, rx_dropped, tx_dropped, rx_errors, tx_errors]) + + headers = ['Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes', 'Rx Dropped', 'Tx Dropped', 'Rx Errors', 'Tx Errors'] + output = tabulate(data_entries, headers, numalign="left") + print (output) + return output def show(raw: bool, intf_name: typing.Optional[str], intf_type: typing.Optional[str], @@ -402,6 +414,18 @@ def show_counters(raw: bool, intf_name: typing.Optional[str], return data return _format_show_counters(data) +def clear_counters(intf_name: typing.Optional[str], + intf_type: typing.Optional[str], + vif: bool, vrrp: bool): + for interface in filtered_interfaces(intf_name, intf_type, vif, vrrp): + interface.operational.clear_counters() + +def reset_counters(intf_name: typing.Optional[str], + intf_type: typing.Optional[str], + vif: bool, vrrp: bool): + for interface in filtered_interfaces(intf_name, intf_type, vif, vrrp): + interface.operational.reset_counters() + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index f6417764a..db4948d7a 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -13,26 +13,21 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os import re import sys import typing -from collections import OrderedDict from hurry import filesize from re import split as re_split from tabulate import tabulate -from subprocess import TimeoutExpired -from vyos.util import call from vyos.util import convert_data from vyos.util import seconds_to_human +from vyos.util import cmd +from vyos.configquery import ConfigTreeQuery import vyos.opmode - - -SWANCTL_CONF = '/etc/swanctl/swanctl.conf' +import vyos.ipsec def _convert(text): @@ -43,21 +38,31 @@ def _alphanum_key(key): return [_convert(c) for c in re_split('([0-9]+)', str(key))] -def _get_vici_sas(): - from vici import Session as vici_session - +def _get_raw_data_sas(): try: - session = vici_session() - except Exception: - raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") - sas = list(session.list_sas()) - return sas + get_sas = vyos.ipsec.get_vici_sas() + sas = convert_data(get_sas) + return sas + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) -def _get_raw_data_sas(): - get_sas = _get_vici_sas() - sas = convert_data(get_sas) - return sas +def _get_output_swanctl_sas_from_list(ra_output_list: list) -> str: + """ + Template for output for VICI + Inserts \n after each IKE SA + :param ra_output_list: IKE SAs list + :type ra_output_list: list + :return: formatted string + :rtype: str + """ + output = ''; + for sa_val in ra_output_list: + for sa in sa_val.values(): + swanctl_output: str = cmd( + f'sudo swanctl -l --ike-id {sa["uniqueid"]}') + output = f'{output}{swanctl_output}\n\n' + return output def _get_formatted_output_sas(sas): @@ -139,22 +144,14 @@ def _get_formatted_output_sas(sas): # Connections block -def _get_vici_connections(): - from vici import Session as vici_session - - try: - session = vici_session() - except Exception: - raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") - connections = list(session.list_conns()) - return connections - def _get_convert_data_connections(): - get_connections = _get_vici_connections() - connections = convert_data(get_connections) - return connections - + try: + get_connections = vyos.ipsec.get_vici_connections() + connections = convert_data(get_connections) + return connections + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) def _get_parent_sa_proposal(connection_name: str, data: list) -> dict: """Get parent SA proposals by connection name @@ -239,7 +236,8 @@ def _get_child_sa_state(connection_name: str, tunnel_name: str, # Get all child SA states # there can be multiple SAs per tunnel child_sa_states = [ - v['state'] for k, v in child_sas.items() if v['name'] == tunnel_name + v['state'] for k, v in child_sas.items() if + v['name'] == tunnel_name ] return 'up' if 'INSTALLED' in child_sa_states else child_sa @@ -406,39 +404,170 @@ def _get_formatted_output_conections(data): # Connections block end -def get_peer_connections(peer, tunnel): - search = rf'^[\s]*({peer}-(tunnel-[\d]+|vti)).*' - matches = [] - if not os.path.exists(SWANCTL_CONF): - raise vyos.opmode.UnconfiguredSubsystem("IPsec not initialized") - suffix = None if tunnel is None else (f'tunnel-{tunnel}' if - tunnel.isnumeric() else tunnel) - with open(SWANCTL_CONF, 'r') as f: - for line in f.readlines(): - result = re.match(search, line) - if result: - if tunnel is None: - matches.append(result[1]) +def _get_childsa_id_list(ike_sas: list) -> list: + """ + Generate list of CHILD SA ids based on list of OrderingDict + wich is returned by vici + :param ike_sas: list of IKE SAs generated by vici + :type ike_sas: list + :return: list of IKE SAs ids + :rtype: list + """ + list_childsa_id: list = [] + for ike in ike_sas: + for ike_sa in ike.values(): + for child_sa in ike_sa['child-sas'].values(): + list_childsa_id.append(child_sa['uniqueid'].decode('ascii')) + return list_childsa_id + + +def _get_all_sitetosite_peers_name_list() -> list: + """ + Return site-to-site peers configuration + :return: site-to-site peers configuration + :rtype: list + """ + conf: ConfigTreeQuery = ConfigTreeQuery() + config_path = ['vpn', 'ipsec', 'site-to-site', 'peer'] + peers_config = conf.get_config_dict(config_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + peers_list: list = [] + for name in peers_config: + peers_list.append(name) + return peers_list + + +def reset_peer(peer: str, tunnel: typing.Optional[str] = None): + # Convert tunnel to Strongwan format of CHILD_SA + tunnel_sw = None + if tunnel: + if tunnel.isnumeric(): + tunnel_sw = f'{peer}-tunnel-{tunnel}' + elif tunnel == 'vti': + tunnel_sw = f'{peer}-vti' + try: + sa_list: list = vyos.ipsec.get_vici_sas_by_name(peer, tunnel_sw) + if not sa_list: + raise vyos.opmode.IncorrectValue( + f'Peer\'s {peer} SA(s) not found, aborting') + if tunnel and sa_list: + childsa_id_list: list = _get_childsa_id_list(sa_list) + if not childsa_id_list: + raise vyos.opmode.IncorrectValue( + f'Peer {peer} tunnel {tunnel} SA(s) not found, aborting') + vyos.ipsec.terminate_vici_by_name(peer, tunnel_sw) + print(f'Peer {peer} reset result: success') + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) + except (vyos.ipsec.ViciCommandError) as err: + raise vyos.opmode.IncorrectValue(err) + + +def reset_all_peers(): + sitetosite_list = _get_all_sitetosite_peers_name_list() + if sitetosite_list: + for peer_name in sitetosite_list: + try: + reset_peer(peer_name) + except (vyos.opmode.IncorrectValue) as err: + print(err) + print('Peers reset result: success') + else: + raise vyos.opmode.UnconfiguredSubsystem( + 'VPN IPSec site-to-site is not configured, aborting') + + +def _get_ra_session_list_by_username(username: typing.Optional[str] = None): + """ + Return list of remote-access IKE_SAs uniqueids + :param username: + :type username: + :return: + :rtype: + """ + list_sa_id = [] + sa_list = _get_raw_data_sas() + for sa_val in sa_list: + for sa in sa_val.values(): + if 'remote-eap-id' in sa: + if username: + if username == sa['remote-eap-id']: + list_sa_id.append(sa['uniqueid']) else: - if result[2] == suffix: - matches.append(result[1]) - return matches + list_sa_id.append(sa['uniqueid']) + return list_sa_id -def reset_peer(peer: str, tunnel:typing.Optional[str]): - conns = get_peer_connections(peer, tunnel) +def reset_ra(username: typing.Optional[str] = None): + #Reset remote-access ipsec sessions + if username: + list_sa_id = _get_ra_session_list_by_username(username) + else: + list_sa_id = _get_ra_session_list_by_username() + if list_sa_id: + vyos.ipsec.terminate_vici_ikeid_list(list_sa_id) - if not conns: - raise vyos.opmode.IncorrectValue('Peer or tunnel(s) not found, aborting') - for conn in conns: +def reset_profile_dst(profile: str, tunnel: str, nbma_dst: str): + if profile and tunnel and nbma_dst: + ike_sa_name = f'dmvpn-{profile}-{tunnel}' try: - call(f'sudo /usr/sbin/ipsec down {conn}{{*}}', timeout = 10) - call(f'sudo /usr/sbin/ipsec up {conn}', timeout = 10) - except TimeoutExpired as e: - raise vyos.opmode.InternalError(f'Timed out while resetting {conn}') - - print('Peer reset result: success') + # Get IKE SAs + sa_list = convert_data( + vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)) + if not sa_list: + raise vyos.opmode.IncorrectValue( + f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting') + sa_nbma_list = list([x for x in sa_list if + ike_sa_name in x and x[ike_sa_name][ + 'remote-host'] == nbma_dst]) + if not sa_nbma_list: + raise vyos.opmode.IncorrectValue( + f'SA(s) for profile {profile} tunnel {tunnel} remote-host {nbma_dst} not found, aborting') + # terminate IKE SAs + vyos.ipsec.terminate_vici_ikeid_list(list( + [x[ike_sa_name]['uniqueid'] for x in sa_nbma_list if + ike_sa_name in x])) + # initiate IKE SAs + for ike in sa_nbma_list: + if ike_sa_name in ike: + vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn', + ike[ike_sa_name]['local-host'], + ike[ike_sa_name]['remote-host']) + print( + f'Profile {profile} tunnel {tunnel} remote-host {nbma_dst} reset result: success') + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) + except (vyos.ipsec.ViciCommandError) as err: + raise vyos.opmode.IncorrectValue(err) + + +def reset_profile_all(profile: str, tunnel: str): + if profile and tunnel: + ike_sa_name = f'dmvpn-{profile}-{tunnel}' + try: + # Get IKE SAs + sa_list: list = convert_data( + vyos.ipsec.get_vici_sas_by_name(ike_sa_name, None)) + if not sa_list: + raise vyos.opmode.IncorrectValue( + f'SA(s) for profile {profile} tunnel {tunnel} not found, aborting') + # terminate IKE SAs + vyos.ipsec.terminate_vici_by_name(ike_sa_name, None) + # initiate IKE SAs + for ike in sa_list: + if ike_sa_name in ike: + vyos.ipsec.vici_initiate(ike_sa_name, 'dmvpn', + ike[ike_sa_name]['local-host'], + ike[ike_sa_name]['remote-host']) + print( + f'Profile {profile} tunnel {tunnel} remote-host {ike[ike_sa_name]["remote-host"]} reset result: success') + print(f'Profile {profile} tunnel {tunnel} reset result: success') + except (vyos.ipsec.ViciInitiateError) as err: + raise vyos.opmode.UnconfiguredSubsystem(err) + except (vyos.ipsec.ViciCommandError) as err: + raise vyos.opmode.IncorrectValue(err) def show_sa(raw: bool): @@ -448,6 +577,24 @@ def show_sa(raw: bool): return _get_formatted_output_sas(sa_data) +def _get_output_sas_detail(ra_output_list: list) -> str: + """ + Formate all IKE SAs detail output + :param ra_output_list: IKE SAs list + :type ra_output_list: list + :return: formatted RA IKE SAs detail output + :rtype: str + """ + return _get_output_swanctl_sas_from_list(ra_output_list) + + +def show_sa_detail(raw: bool): + sa_data = _get_raw_data_sas() + if raw: + return sa_data + return _get_output_sas_detail(sa_data) + + def show_connections(raw: bool): list_conns = _get_convert_data_connections() list_sas = _get_raw_data_sas() @@ -465,6 +612,173 @@ def show_connections_summary(raw: bool): return _get_raw_connections_summary(list_conns, list_sas) +def _get_ra_sessions(username: typing.Optional[str] = None) -> list: + """ + Return list of remote-access IKE_SAs from VICI by username. + If username unspecified, return all remote-access IKE_SAs + :param username: Username of RA connection + :type username: str + :return: list of ra remote-access IKE_SAs + :rtype: list + """ + list_sa = [] + sa_list = _get_raw_data_sas() + for conn in sa_list: + for sa in conn.values(): + if 'remote-eap-id' in sa: + if username: + if username == sa['remote-eap-id']: + list_sa.append(conn) + else: + list_sa.append(conn) + return list_sa + + +def _filter_ikesas(list_sa: list, filter_key: str, filter_value: str) -> list: + """ + Filter IKE SAs by specifice key + :param list_sa: list of IKE SAs + :type list_sa: list + :param filter_key: Filter Key + :type filter_key: str + :param filter_value: Filter Value + :type filter_value: str + :return: Filtered list of IKE SAs + :rtype: list + """ + filtered_sa_list = [] + for conn in list_sa: + for sa in conn.values(): + if sa[filter_key] and sa[filter_key] == filter_value: + filtered_sa_list.append(conn) + return filtered_sa_list + + +def _get_last_installed_childsa(sa: dict) -> str: + """ + Return name of last installed active Child SA + :param sa: Dictionary with Child SAs + :type sa: dict + :return: Name of the Last installed active Child SA + :rtype: str + """ + child_sa_name = None + child_sa_id = 0 + for sa_name, child_sa in sa['child-sas'].items(): + if child_sa['state'] == 'INSTALLED': + if child_sa_id == 0 or int(child_sa['uniqueid']) > child_sa_id: + child_sa_id = int(child_sa['uniqueid']) + child_sa_name = sa_name + return child_sa_name + + +def _get_formatted_ike_proposal(sa: dict) -> str: + """ + Return IKE proposal string in format + EncrALG-EncrKeySize/PFR/HASH/DH-GROUP + :param sa: IKE SA + :type sa: dict + :return: IKE proposal string + :rtype: str + """ + proposal = '' + proposal = f'{proposal}{sa["encr-alg"]}' if 'encr-alg' in sa else proposal + proposal = f'{proposal}-{sa["encr-keysize"]}' if 'encr-keysize' in sa else proposal + proposal = f'{proposal}/{sa["prf-alg"]}' if 'prf-alg' in sa else proposal + proposal = f'{proposal}/{sa["integ-alg"]}' if 'integ-alg' in sa else proposal + proposal = f'{proposal}/{sa["dh-group"]}' if 'dh-group' in sa else proposal + return proposal + + +def _get_formatted_ipsec_proposal(sa: dict) -> str: + """ + Return IPSec proposal string in format + Protocol: EncrALG-EncrKeySize/HASH/PFS + :param sa: Child SA + :type sa: dict + :return: IPSec proposal string + :rtype: str + """ + proposal = '' + proposal = f'{proposal}{sa["protocol"]}' if 'protocol' in sa else proposal + proposal = f'{proposal}:{sa["encr-alg"]}' if 'encr-alg' in sa else proposal + proposal = f'{proposal}-{sa["encr-keysize"]}' if 'encr-keysize' in sa else proposal + proposal = f'{proposal}/{sa["integ-alg"]}' if 'integ-alg' in sa else proposal + proposal = f'{proposal}/{sa["dh-group"]}' if 'dh-group' in sa else proposal + return proposal + + +def _get_output_ra_sas_detail(ra_output_list: list) -> str: + """ + Formate RA IKE SAs detail output + :param ra_output_list: IKE SAs list + :type ra_output_list: list + :return: formatted RA IKE SAs detail output + :rtype: str + """ + return _get_output_swanctl_sas_from_list(ra_output_list) + + +def _get_formatted_output_ra_summary(ra_output_list: list): + sa_data = [] + for conn in ra_output_list: + for sa in conn.values(): + sa_id = sa['uniqueid'] if 'uniqueid' in sa else '' + sa_username = sa['remote-eap-id'] if 'remote-eap-id' in sa else '' + sa_protocol = f'IKEv{sa["version"]}' if 'version' in sa else '' + sa_remotehost = sa['remote-host'] if 'remote-host' in sa else '' + sa_remoteid = sa['remote-id'] if 'remote-id' in sa else '' + sa_ike_proposal = _get_formatted_ike_proposal(sa) + sa_tunnel_ip = sa['remote-vips'] + child_sa_key = _get_last_installed_childsa(sa) + if child_sa_key: + child_sa = sa['child-sas'][child_sa_key] + sa_ipsec_proposal = _get_formatted_ipsec_proposal(child_sa) + sa_state = "UP" + sa_uptime = seconds_to_human(sa['established']) + else: + sa_ipsec_proposal = '' + sa_state = "DOWN" + sa_uptime = '' + sa_data.append( + [sa_id, sa_username, sa_protocol, sa_state, sa_uptime, + sa_tunnel_ip, + sa_remotehost, sa_remoteid, sa_ike_proposal, + sa_ipsec_proposal]) + + headers = ["Connection ID", "Username", "Protocol", "State", "Uptime", + "Tunnel IP", "Remote Host", "Remote ID", "IKE Proposal", + "IPSec Proposal"] + sa_data = sorted(sa_data, key=_alphanum_key) + output = tabulate(sa_data, headers) + return output + + +def show_ra_detail(raw: bool, username: typing.Optional[str] = None, + conn_id: typing.Optional[str] = None): + list_sa: list = _get_ra_sessions() + if username: + list_sa = _filter_ikesas(list_sa, 'remote-eap-id', username) + elif conn_id: + list_sa = _filter_ikesas(list_sa, 'uniqueid', conn_id) + if not list_sa: + raise vyos.opmode.IncorrectValue( + f'No active connections found, aborting') + if raw: + return list_sa + return _get_output_ra_sas_detail(list_sa) + + +def show_ra_summary(raw: bool): + list_sa: list = _get_ra_sessions() + if not list_sa: + raise vyos.opmode.IncorrectValue( + f'No active connections found, aborting') + if raw: + return list_sa + return _get_formatted_output_ra_summary(list_sa) + + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py index cf06de0e9..c92795745 100755 --- a/src/op_mode/nat.py +++ b/src/op_mode/nat.py @@ -31,6 +31,8 @@ from vyos.util import dict_search base = 'nat' unconf_message = 'NAT is not configured' +ArgDirection = typing.Literal['source', 'destination'] +ArgFamily = typing.Literal['inet', 'inet6'] def _get_xml_translation(direction, family, address=None): """ @@ -298,7 +300,7 @@ def _verify(func): @_verify -def show_rules(raw: bool, direction: str, family: str): +def show_rules(raw: bool, direction: ArgDirection, family: ArgFamily): nat_rules = _get_raw_data_rules(direction, family) if raw: return nat_rules @@ -307,7 +309,7 @@ def show_rules(raw: bool, direction: str, family: str): @_verify -def show_statistics(raw: bool, direction: str, family: str): +def show_statistics(raw: bool, direction: ArgDirection, family: ArgFamily): nat_statistics = _get_raw_data_rules(direction, family) if raw: return nat_statistics @@ -316,8 +318,8 @@ def show_statistics(raw: bool, direction: str, family: str): @_verify -def show_translations(raw: bool, direction: - str, family: str, +def show_translations(raw: bool, direction: ArgDirection, + family: ArgFamily, address: typing.Optional[str], verbose: typing.Optional[bool]): family = 'ipv6' if family == 'inet6' else 'ipv4' diff --git a/src/op_mode/neighbor.py b/src/op_mode/neighbor.py index 264dbdc72..b329ea280 100755 --- a/src/op_mode/neighbor.py +++ b/src/op_mode/neighbor.py @@ -32,6 +32,9 @@ import typing import vyos.opmode +ArgFamily = typing.Literal['inet', 'inet6'] +ArgState = typing.Literal['reachable', 'stale', 'failed', 'permanent'] + def interface_exists(interface): import os return os.path.exists(f'/sys/class/net/{interface}') @@ -88,7 +91,8 @@ def format_neighbors(neighs, interface=None): headers = ["Address", "Interface", "Link layer address", "State"] return tabulate(neighs, headers) -def show(raw: bool, family: str, interface: typing.Optional[str], state: typing.Optional[str]): +def show(raw: bool, family: ArgFamily, interface: typing.Optional[str], + state: typing.Optional[ArgState]): """ Display neighbor table contents """ data = get_raw_data(family, interface, state=state) @@ -97,7 +101,7 @@ def show(raw: bool, family: str, interface: typing.Optional[str], state: typing. else: return format_neighbors(data, interface) -def reset(family: str, interface: typing.Optional[str], address: typing.Optional[str]): +def reset(family: ArgFamily, interface: typing.Optional[str], address: typing.Optional[str]): from vyos.util import run if address and interface: diff --git a/src/op_mode/nhrp.py b/src/op_mode/nhrp.py new file mode 100755 index 000000000..5ff91a59c --- /dev/null +++ b/src/op_mode/nhrp.py @@ -0,0 +1,101 @@ +#!/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 sys +import tabulate +import vyos.opmode + +from vyos.util import cmd +from vyos.util import process_named_running +from vyos.util import colon_separated_to_dict + + +def _get_formatted_output(output_dict: dict) -> str: + """ + Create formatted table for CLI output + :param output_dict: dictionary for API + :type output_dict: dict + :return: tabulate string + :rtype: str + """ + print(f"Status: {output_dict['Status']}") + output: str = tabulate.tabulate(output_dict['routes'], headers='keys', + numalign="left") + return output + + +def _get_formatted_dict(output_string: str) -> dict: + """ + Format string returned from CMD to API list + :param output_string: String received by CMD + :type output_string: str + :return: dictionary for API + :rtype: dict + """ + formatted_dict: dict = { + 'Status': '', + 'routes': [] + } + output_list: list = output_string.split('\n\n') + for list_a in output_list: + output_dict = colon_separated_to_dict(list_a, True) + if 'Status' in output_dict: + formatted_dict['Status'] = output_dict['Status'] + else: + formatted_dict['routes'].append(output_dict) + return formatted_dict + + +def show_interface(raw: bool): + """ + Command 'show nhrp interface' + :param raw: if API + :type raw: bool + """ + if not process_named_running('opennhrp'): + raise vyos.opmode.UnconfiguredSubsystem('OpenNHRP is not running.') + interface_string: str = cmd('sudo opennhrpctl interface show') + interface_dict: dict = _get_formatted_dict(interface_string) + if raw: + return interface_dict + else: + return _get_formatted_output(interface_dict) + + +def show_tunnel(raw: bool): + """ + Command 'show nhrp tunnel' + :param raw: if API + :type raw: bool + """ + if not process_named_running('opennhrp'): + raise vyos.opmode.UnconfiguredSubsystem('OpenNHRP is not running.') + tunnel_string: str = cmd('sudo opennhrpctl show') + tunnel_dict: list = _get_formatted_dict(tunnel_string) + if raw: + return tunnel_dict + else: + return _get_formatted_output(tunnel_dict) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/openvpn.py b/src/op_mode/openvpn.py index 3797a7153..d9ae965c5 100755 --- a/src/op_mode/openvpn.py +++ b/src/op_mode/openvpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -16,16 +16,21 @@ # # +import json import os import sys +import typing from tabulate import tabulate import vyos.opmode from vyos.util import bytes_to_human from vyos.util import commit_in_progress from vyos.util import call +from vyos.util import rc_cmd from vyos.config import Config +ArgMode = typing.Literal['client', 'server', 'site_to_site'] + def _get_tunnel_address(peer_host, peer_port, status_file): peer = peer_host + ':' + peer_port lst = [] @@ -50,7 +55,7 @@ def _get_tunnel_address(peer_host, peer_port, status_file): def _get_interface_status(mode: str, interface: str) -> dict: status_file = f'/run/openvpn/{interface}.status' - data = { + data: dict = { 'mode': mode, 'intf': interface, 'local_host': '', @@ -60,7 +65,7 @@ def _get_interface_status(mode: str, interface: str) -> dict: } if not os.path.exists(status_file): - raise vyos.opmode.DataUnavailable('No information for interface {interface}') + return data with open(status_file, 'r') as f: lines = f.readlines() @@ -139,30 +144,54 @@ def _get_interface_status(mode: str, interface: str) -> dict: return data -def _get_raw_data(mode: str) -> dict: - data = {} + +def _get_interface_state(iface): + rc, out = rc_cmd(f'ip --json link show dev {iface}') + try: + data = json.loads(out) + except: + return 'DOWN' + return data[0].get('operstate', 'DOWN') + + +def _get_interface_description(iface): + rc, out = rc_cmd(f'ip --json link show dev {iface}') + try: + data = json.loads(out) + except: + return '' + return data[0].get('ifalias', '') + + +def _get_raw_data(mode: str) -> list: + data: list = [] conf = Config() conf_dict = conf.get_config_dict(['interfaces', 'openvpn'], get_first_key=True) if not conf_dict: return data - interfaces = [x for x in list(conf_dict) if conf_dict[x]['mode'] == mode] + interfaces = [x for x in list(conf_dict) if + conf_dict[x]['mode'].replace('-', '_') == mode] for intf in interfaces: - data[intf] = _get_interface_status(mode, intf) - d = data[intf] + d = _get_interface_status(mode, intf) + d['state'] = _get_interface_state(intf) + d['description'] = _get_interface_description(intf) d['local_host'] = conf_dict[intf].get('local-host', '') d['local_port'] = conf_dict[intf].get('local-port', '') - if mode in ['client', 'site-to-site']: + if conf.exists(f'interfaces openvpn {intf} server client'): + d['configured_clients'] = conf.list_nodes(f'interfaces openvpn {intf} server client') + if mode in ['client', 'site_to_site']: for client in d['clients']: if 'shared-secret-key-file' in list(conf_dict[intf]): client['name'] = 'None (PSK)' client['remote_host'] = conf_dict[intf].get('remote-host', [''])[0] client['remote_port'] = conf_dict[intf].get('remote-port', '1194') + data.append(d) return data -def _format_openvpn(data: dict) -> str: +def _format_openvpn(data: list) -> str: if not data: out = 'No OpenVPN interfaces configured' return out @@ -171,11 +200,12 @@ def _format_openvpn(data: dict) -> str: 'TX bytes', 'RX bytes', 'Connected Since'] out = '' - data_out = [] - for intf in list(data): - l_host = data[intf]['local_host'] - l_port = data[intf]['local_port'] - for client in list(data[intf]['clients']): + for d in data: + data_out = [] + intf = d['intf'] + l_host = d['local_host'] + l_port = d['local_port'] + for client in d['clients']: r_host = client['remote_host'] r_port = client['remote_port'] @@ -190,11 +220,13 @@ def _format_openvpn(data: dict) -> str: data_out.append([name, remote, tunnel, local, tx_bytes, rx_bytes, online_since]) - out += tabulate(data_out, headers) + if data_out: + out += tabulate(data_out, headers) + out += "\n" return out -def show(raw: bool, mode: str) -> str: +def show(raw: bool, mode: ArgMode) -> typing.Union[list,str]: openvpn_data = _get_raw_data(mode) if raw: diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 1e78c3a03..b054690b0 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -87,6 +87,9 @@ def get_config_certificate(name=None): def get_certificate_ca(cert, ca_certs): # Find CA certificate for given certificate + if not ca_certs: + return None + for ca_name, ca_dict in ca_certs.items(): if 'certificate' not in ca_dict: continue diff --git a/src/op_mode/reset_vpn.py b/src/op_mode/reset_vpn.py index 3a0ad941c..46195d6cd 100755 --- a/src/op_mode/reset_vpn.py +++ b/src/op_mode/reset_vpn.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 VyOS maintainers and contributors +# Copyright (C) 2022-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 @@ -13,60 +13,49 @@ # # 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 sys -import argparse +import typing from vyos.util import run +import vyos.opmode + cmd_dict = { - 'cmd_base' : '/usr/bin/accel-cmd -p {} terminate {} {}', - 'vpn_types' : { - 'pptp' : 2003, - 'l2tp' : 2004, - 'sstp' : 2005 + 'cmd_base': '/usr/bin/accel-cmd -p {} terminate {} {}', + 'vpn_types': { + 'pptp': 2003, + 'l2tp': 2004, + 'sstp': 2005 } } -def terminate_sessions(username='', interface='', protocol=''): - # Reset vpn connections by username +def reset_conn(protocol: str, username: typing.Optional[str] = None, + interface: typing.Optional[str] = None): if protocol in cmd_dict['vpn_types']: - if username == "all_users": - run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'all', '')) - else: - run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], 'username', username)) - - # Reset vpn connections by ifname - elif interface: - for proto in cmd_dict['vpn_types']: - run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'if', interface)) - - elif username: - # Reset all vpn connections - if username == "all_users": - for proto in cmd_dict['vpn_types']: - run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'all', '')) + # Reset by Interface + if interface: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], + 'if', interface)) + return + # Reset by username + if username: + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], + 'username', username)) + # Reset all else: - for proto in cmd_dict['vpn_types']: - run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][proto], 'username', username)) - -def main(): - #parese args - parser = argparse.ArgumentParser() - parser.add_argument('--username', help='Terminate by username (all_users used for disconnect all users)', required=False) - parser.add_argument('--interface', help='Terminate by interface', required=False) - parser.add_argument('--protocol', help='Set protocol (pptp|l2tp|sstp)', required=False) - args = parser.parse_args() - - if args.username or args.interface: - terminate_sessions(username=args.username, interface=args.interface, protocol=args.protocol) + run(cmd_dict['cmd_base'].format(cmd_dict['vpn_types'][protocol], + 'all', + '')) else: - print("Param --username or --interface required") - sys.exit(1) - - terminate_sessions() + vyos.opmode.IncorrectValue('Unknown VPN Protocol, aborting') if __name__ == '__main__': - main() + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/restart_frr.py b/src/op_mode/restart_frr.py index 91b25567a..680d9f8cc 100755 --- a/src/op_mode/restart_frr.py +++ b/src/op_mode/restart_frr.py @@ -139,7 +139,7 @@ def _reload_config(daemon): # define program arguments cmd_args_parser = argparse.ArgumentParser(description='restart frr daemons') cmd_args_parser.add_argument('--action', choices=['restart'], required=True, help='action to frr daemons') -cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ldpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra'], required=False, nargs='*', help='select single or multiple daemons') +cmd_args_parser.add_argument('--daemon', choices=['bfdd', 'bgpd', 'ldpd', 'ospfd', 'ospf6d', 'isisd', 'ripd', 'ripngd', 'staticd', 'zebra', 'babeld'], required=False, nargs='*', help='select single or multiple daemons') # parse arguments cmd_args = cmd_args_parser.parse_args() diff --git a/src/op_mode/route.py b/src/op_mode/route.py index 7f0f9cbac..d6d6b7d6f 100755 --- a/src/op_mode/route.py +++ b/src/op_mode/route.py @@ -54,7 +54,9 @@ frr_command_template = Template(""" {% endif %} """) -def show_summary(raw: bool, family: str, table: typing.Optional[int], vrf: typing.Optional[str]): +ArgFamily = typing.Literal['inet', 'inet6'] + +def show_summary(raw: bool, family: ArgFamily, table: typing.Optional[int], vrf: typing.Optional[str]): from vyos.util import cmd if family == 'inet': @@ -94,7 +96,7 @@ def show_summary(raw: bool, family: str, table: typing.Optional[int], vrf: typin return output def show(raw: bool, - family: str, + family: ArgFamily, net: typing.Optional[str], table: typing.Optional[int], protocol: typing.Optional[str], diff --git a/src/op_mode/sflow.py b/src/op_mode/sflow.py new file mode 100755 index 000000000..88f70d6bd --- /dev/null +++ b/src/op_mode/sflow.py @@ -0,0 +1,108 @@ +#!/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 dbus +import sys + +from tabulate import tabulate + +from vyos.configquery import ConfigTreeQuery +from vyos.util import cmd + +import vyos.opmode + + +def _get_raw_sflow(): + bus = dbus.SystemBus() + config = ConfigTreeQuery() + + interfaces = config.values('system sflow interface') + servers = config.list_nodes('system sflow server') + + sflow = bus.get_object('net.sflow.hsflowd', '/net/sflow/hsflowd') + sflow_telemetry = dbus.Interface( + sflow, dbus_interface='net.sflow.hsflowd.telemetry') + agent_address = sflow_telemetry.GetAgent() + samples_dropped = int(sflow_telemetry.Get('dropped_samples')) + packet_drop_sent = int(sflow_telemetry.Get('event_samples')) + samples_packet_sent = int(sflow_telemetry.Get('flow_samples')) + samples_counter_sent = int(sflow_telemetry.Get('counter_samples')) + datagrams_sent = int(sflow_telemetry.Get('datagrams')) + rtmetric_samples = int(sflow_telemetry.Get('rtmetric_samples')) + event_samples_suppressed = int(sflow_telemetry.Get('event_samples_suppressed')) + samples_suppressed = int(sflow_telemetry.Get('flow_samples_suppressed')) + counter_samples_suppressed = int( + sflow_telemetry.Get("counter_samples_suppressed")) + version = sflow_telemetry.GetVersion() + + sflow_dict = { + 'agent_address': agent_address, + 'sflow_interfaces': interfaces, + 'sflow_servers': servers, + 'counter_samples_sent': samples_counter_sent, + 'datagrams_sent': datagrams_sent, + 'packet_drop_sent': packet_drop_sent, + 'packet_samples_dropped': samples_dropped, + 'packet_samples_sent': samples_packet_sent, + 'rtmetric_samples': rtmetric_samples, + 'event_samples_suppressed': event_samples_suppressed, + 'flow_samples_suppressed': samples_suppressed, + 'counter_samples_suppressed': counter_samples_suppressed, + 'hsflowd_version': version + } + return sflow_dict + + +def _get_formatted_sflow(data): + table = [ + ['Agent address', f'{data.get("agent_address")}'], + ['sFlow interfaces', f'{data.get("sflow_interfaces", "n/a")}'], + ['sFlow servers', f'{data.get("sflow_servers", "n/a")}'], + ['Counter samples sent', f'{data.get("counter_samples_sent")}'], + ['Datagrams sent', f'{data.get("datagrams_sent")}'], + ['Packet samples sent', f'{data.get("packet_samples_sent")}'], + ['Packet samples dropped', f'{data.get("packet_samples_dropped")}'], + ['Packet drops sent', f'{data.get("packet_drop_sent")}'], + ['Packet drops suppressed', f'{data.get("event_samples_suppressed")}'], + ['Flow samples suppressed', f'{data.get("flow_samples_suppressed")}'], + ['Counter samples suppressed', f'{data.get("counter_samples_suppressed")}'] + ] + + return tabulate(table) + + +def show(raw: bool): + + config = ConfigTreeQuery() + if not config.exists('system sflow'): + raise vyos.opmode.UnconfiguredSubsystem( + '"system sflow" is not configured!') + + sflow_data = _get_raw_sflow() + if raw: + return sflow_data + else: + return _get_formatted_sflow(sflow_data) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/show_interfaces.py b/src/op_mode/show_interfaces.py deleted file mode 100755 index eac068274..000000000 --- a/src/op_mode/show_interfaces.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2017-2021 VyOS maintainers and contributors <maintainers@vyos.io> -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library 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 -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library. If not, see <http://www.gnu.org/licenses/>. - -import os -import re -import sys -import glob -import argparse - -from vyos.ifconfig import Section -from vyos.ifconfig import Interface -from vyos.ifconfig import VRRP -from vyos.util import cmd, call - - -# interfaces = Sections.reserved() -interfaces = ['eno', 'ens', 'enp', 'enx', 'eth', 'vmnet', 'lo', 'tun', 'wan', 'pppoe'] -glob_ifnames = '/sys/class/net/({})*'.format('|'.join(interfaces)) - - -actions = {} -def register(name): - """ - Decorator to register a function into actions with a name. - `actions[name]' can be used to call the registered functions. - We wrap each function in a SIGPIPE handler as all registered functions - can be subject to a broken pipe if there are a lot of interfaces. - """ - def _register(function): - def handled_function(*args, **kwargs): - try: - function(*args, **kwargs) - except BrokenPipeError: - # Flush output to /dev/null and bail out. - os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) - sys.exit(1) - actions[name] = handled_function - return handled_function - return _register - - -def filtered_interfaces(ifnames, iftypes, vif, vrrp): - """ - get all the interfaces from the OS and returns them - ifnames can be used to filter which interfaces should be considered - - ifnames: a list of interfaces names to consider, empty do not filter - return an instance of the interface class - """ - if isinstance(iftypes, list): - for iftype in iftypes: - yield from filtered_interfaces(ifnames, iftype, vif, vrrp) - - for ifname in Section.interfaces(iftypes): - # Bail out early if interface name not part of our search list - if ifnames and ifname not in ifnames: - continue - - # As we are only "reading" from the interface - we must use the - # generic base class which exposes all the data via a common API - interface = Interface(ifname, create=False, debug=False) - - # VLAN interfaces have a '.' in their name by convention - if vif and not '.' in ifname: - continue - - if vrrp: - vrrp_interfaces = VRRP.active_interfaces() - if ifname not in vrrp_interfaces: - continue - - yield interface - - -def split_text(text, used=0): - """ - take a string and attempt to split it to fit with the width of the screen - - text: the string to split - used: number of characted already used in the screen - """ - no_tty = call('tty -s') - - returned = cmd('stty size') if not no_tty else '' - if len(returned) == 2: - rows, columns = [int(_) for _ in returned] - else: - rows, columns = (40, 80) - - desc_len = columns - used - - line = '' - for word in text.split(): - if len(line) + len(word) < desc_len: - line = f'{line} {word}' - continue - if line: - yield line[1:] - else: - line = f'{line} {word}' - - yield line[1:] - - -def get_counter_val(clear, now): - """ - attempt to correct a counter if it wrapped, copied from perl - - clear: previous counter - now: the current counter - """ - # This function has to deal with both 32 and 64 bit counters - if clear == 0: - return now - - # device is using 64 bit values assume they never wrap - value = now - clear - if (now >> 32) != 0: - return value - - # The counter has rolled. If the counter has rolled - # multiple times since the clear value, then this math - # is meaningless. - if (value < 0): - value = (4294967296 - clear) + now - - return value - - -@register('help') -def usage(*args): - print(f"Usage: {sys.argv[0]} [intf=NAME|intf-type=TYPE|vif|vrrp] action=ACTION") - print(f" NAME = " + ' | '.join(Section.interfaces())) - print(f" TYPE = " + ' | '.join(Section.sections())) - print(f" ACTION = " + ' | '.join(actions)) - sys.exit(1) - - -@register('allowed') -def run_allowed(**kwarg): - sys.stdout.write(' '.join(Section.interfaces())) - - -def pppoe(ifname): - out = cmd(f'ps -C pppd -f') - if ifname in out: - return 'C' - elif ifname in [_.split('/')[-1] for _ in glob.glob('/etc/ppp/peers/pppoe*')]: - return 'D' - return '' - - -@register('show') -def run_show_intf(ifnames, iftypes, vif, vrrp): - handled = [] - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - handled.append(interface.ifname) - cache = interface.operational.load_counters() - - out = cmd(f'ip addr show {interface.ifname}') - out = re.sub(f'^\d+:\s+','',out) - if re.search('link/tunnel6', out): - tunnel = cmd(f'ip -6 tun show {interface.ifname}') - # tun0: ip/ipv6 remote ::2 local ::1 encaplimit 4 hoplimit 64 tclass inherit flowlabel inherit (flowinfo 0x00000000) - tunnel = re.sub('.*encap', 'encap', tunnel) - out = re.sub('(\n\s+)(link/tunnel6)', f'\g<1>{tunnel}\g<1>\g<2>', out) - - print(out) - - timestamp = int(cache.get('timestamp', 0)) - if timestamp: - when = interface.operational.strtime(timestamp) - print(f' Last clear: {when}') - - description = interface.get_alias() - if description: - print(f' Description: {description}') - - print() - print(interface.operational.formated_stats()) - - for ifname in ifnames: - if ifname not in handled and ifname.startswith('pppoe'): - state = pppoe(ifname) - if not state: - continue - string = { - 'C': 'Coming up', - 'D': 'Link down', - }[state] - print('{}: {}'.format(ifname, string)) - - -@register('show-brief') -def run_show_intf_brief(ifnames, iftypes, vif, vrrp): - format1 = '%-16s %-33s %-4s %s' - format2 = '%-16s %s' - - print('Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down') - print(format1 % ("Interface", "IP Address", "S/L", "Description")) - print(format1 % ("---------", "----------", "---", "-----------")) - - handled = [] - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - handled.append(interface.ifname) - - oper_state = interface.operational.get_state() - admin_state = interface.get_admin_state() - - intf = [interface.ifname,] - - oper = ['u', ] if oper_state in ('up', 'unknown') else ['D', ] - admin = ['u', ] if admin_state in ('up', 'unknown') else ['A', ] - addrs = [_ for _ in interface.get_addr() if not _.startswith('fe80::')] or ['-', ] - descs = list(split_text(interface.get_alias(),0)) - - while intf or oper or admin or addrs or descs: - i = intf.pop(0) if intf else '' - a = addrs.pop(0) if addrs else '' - d = descs.pop(0) if descs else '' - s = [admin.pop(0)] if admin else [] - l = [oper.pop(0)] if oper else [] - if len(a) < 33: - print(format1 % (i, a, '/'.join(s+l), d)) - else: - print(format2 % (i, a)) - print(format1 % ('', '', '/'.join(s+l), d)) - - for ifname in ifnames: - if ifname not in handled and ifname.startswith('pppoe'): - state = pppoe(ifname) - if not state: - continue - string = { - 'C': 'u/D', - 'D': 'A/D', - }[state] - print(format1 % (ifname, '', string, '')) - - -@register('show-count') -def run_show_counters(ifnames, iftypes, vif, vrrp): - formating = '%-12s %10s %10s %10s %10s' - print(formating % ('Interface', 'Rx Packets', 'Rx Bytes', 'Tx Packets', 'Tx Bytes')) - - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - oper = interface.operational.get_state() - - if oper not in ('up','unknown'): - continue - - stats = interface.operational.get_stats() - cache = interface.operational.load_counters() - print(formating % ( - interface.ifname, - get_counter_val(cache['rx_packets'], stats['rx_packets']), - get_counter_val(cache['rx_bytes'], stats['rx_bytes']), - get_counter_val(cache['tx_packets'], stats['tx_packets']), - get_counter_val(cache['tx_bytes'], stats['tx_bytes']), - )) - - -@register('clear') -def run_clear_intf(ifnames, iftypes, vif, vrrp): - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - print(f'Clearing {interface.ifname}') - interface.operational.clear_counters() - - -@register('reset') -def run_reset_intf(ifnames, iftypes, vif, vrrp): - for interface in filtered_interfaces(ifnames, iftypes, vif, vrrp): - interface.operational.reset_counters() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(add_help=False, description='Show interface information') - parser.add_argument('--intf', action="store", type=str, default='', help='only show the specified interface(s)') - parser.add_argument('--intf-type', action="store", type=str, default='', help='only show the specified interface type') - parser.add_argument('--action', action="store", type=str, default='show', help='action to perform') - parser.add_argument('--vif', action='store_true', default=False, help="only show vif interfaces") - parser.add_argument('--vrrp', action='store_true', default=False, help="only show vrrp interfaces") - parser.add_argument('--help', action='store_true', default=False, help="show help") - - args = parser.parse_args() - - def missing(*args): - print('Invalid action [{args.action}]') - usage() - - actions.get(args.action, missing)( - [_ for _ in args.intf.split(' ') if _], - [_ for _ in args.intf_type.split(' ') if _], - args.vif, - args.vrrp - ) diff --git a/src/op_mode/show_openconnect_otp.py b/src/op_mode/show_openconnect_otp.py index ae532ccc9..88982c50b 100755 --- a/src/op_mode/show_openconnect_otp.py +++ b/src/op_mode/show_openconnect_otp.py @@ -46,7 +46,7 @@ def get_otp_ocserv(username): # options which we need to update into the dictionary retrived. default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) - # workaround a "know limitation" - https://phabricator.vyos.net/T2665 + # workaround a "know limitation" - https://vyos.dev/T2665 del ocserv['authentication']['local_users']['username']['otp'] if not ocserv["authentication"]["local_users"]["username"]: return None diff --git a/src/op_mode/show_techsupport_report.py b/src/op_mode/show_techsupport_report.py new file mode 100644 index 000000000..782004144 --- /dev/null +++ b/src/op_mode/show_techsupport_report.py @@ -0,0 +1,303 @@ +#!/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 typing import List +from vyos.util import rc_cmd +from vyos.ifconfig import Section +from vyos.ifconfig import Interface + + +def print_header(command: str) -> None: + """Prints a command with headers '-'. + + Example: + + % print_header('Example command') + + --------------- + Example command + --------------- + """ + header_length = len(command) * '-' + print(f"\n{header_length}\n{command}\n{header_length}") + + +def execute_command(command: str, header_text: str) -> None: + """Executes a command and prints the output with a header. + + Example: + % execute_command('uptime', "Uptime of the system") + + -------------------- + Uptime of the system + -------------------- + 20:21:57 up 9:04, 5 users, load average: 0.00, 0.00, 0.0 + + """ + print_header(header_text) + try: + rc, output = rc_cmd(command) + print(output) + except Exception as e: + print(f"Error executing command: {command}") + print(f"Error message: {e}") + + +def op(cmd: str) -> str: + """Returns a command with the VyOS operational mode wrapper.""" + return f'/opt/vyatta/bin/vyatta-op-cmd-wrapper {cmd}' + + +def get_ethernet_interfaces() -> List[Interface]: + """Returns a list of Ethernet interfaces.""" + return Section.interfaces('ethernet') + + +def show_version() -> None: + """Prints the VyOS version and package changes.""" + execute_command(op('show version'), 'VyOS Version and Package Changes') + + +def show_config_file() -> None: + """Prints the contents of a configuration file with a header.""" + execute_command('cat /opt/vyatta/etc/config/config.boot', 'Configuration file') + + +def show_running_config() -> None: + """Prints the running configuration.""" + execute_command(op('show configuration'), 'Running configuration') + + +def show_package_repository_config() -> None: + """Prints the package repository configuration file.""" + execute_command('cat /etc/apt/sources.list', 'Package Repository Configuration File') + execute_command('ls -l /etc/apt/sources.list.d/', 'Repositories') + + +def show_user_startup_scripts() -> None: + """Prints the user startup scripts.""" + execute_command('cat /config/scripts/vyos-postconfig-bootup.script', 'User Startup Scripts') + + +def show_frr_config() -> None: + """Prints the FRR configuration.""" + execute_command('vtysh -c "show run"', 'FRR configuration') + + +def show_interfaces() -> None: + """Prints the interfaces.""" + execute_command(op('show interfaces'), 'Interfaces') + + +def show_interface_statistics() -> None: + """Prints the interface statistics.""" + execute_command('ip -s link show', 'Interface statistics') + + +def show_physical_interface_statistics() -> None: + """Prints the physical interface statistics.""" + execute_command('/usr/bin/true', 'Physical Interface statistics') + for iface in get_ethernet_interfaces(): + # Exclude vlans + if '.' in iface: + continue + execute_command(f'ethtool --driver {iface}', f'ethtool --driver {iface}') + execute_command(f'ethtool --statistics {iface}', f'ethtool --statistics {iface}') + execute_command(f'ethtool --show-ring {iface}', f'ethtool --show-ring {iface}') + execute_command(f'ethtool --show-coalesce {iface}', f'ethtool --show-coalesce {iface}') + execute_command(f'ethtool --pause {iface}', f'ethtool --pause {iface}') + execute_command(f'ethtool --show-features {iface}', f'ethtool --show-features {iface}') + execute_command(f'ethtool --phy-statistics {iface}', f'ethtool --phy-statistics {iface}') + execute_command('netstat --interfaces', 'netstat --interfaces') + execute_command('netstat --listening', 'netstat --listening') + execute_command('cat /proc/net/dev', 'cat /proc/net/dev') + + +def show_bridge() -> None: + """Show bridge interfaces.""" + execute_command(op('show bridge'), 'Show bridge') + + +def show_arp() -> None: + """Prints ARP entries.""" + execute_command(op('show arp'), 'ARP Table (Total entries)') + execute_command(op('show ipv6 neighbors'), 'show ipv6 neighbors') + + +def show_route() -> None: + """Prints routing information.""" + + cmd_list_route = [ + "show ip route bgp | head -108", + "show ip route cache", + "show ip route connected", + "show ip route forward", + "show ip route isis | head -108", + "show ip route kernel", + "show ip route ospf | head -108", + "show ip route rip", + "show ip route static", + "show ip route summary", + "show ip route supernets-only", + "show ip route table all", + "show ip route vrf all", + "show ipv6 route bgp | head 108", + "show ipv6 route cache", + "show ipv6 route connected", + "show ipv6 route forward", + "show ipv6 route isis", + "show ipv6 route kernel", + "show ipv6 route ospf", + "show ipv6 route rip", + "show ipv6 route static", + "show ipv6 route summary", + "show ipv6 route table all", + "show ipv6 route vrf all", + ] + for command in cmd_list_route: + execute_command(op(command), command) + + +def show_firewall() -> None: + """Prints firweall information.""" + execute_command('sudo nft list ruleset', 'nft list ruleset') + + +def show_system() -> None: + """Prints system parameters.""" + execute_command(op('show system image version'), 'Show System Image Version') + execute_command(op('show system image storage'), 'Show System Image Storage') + + +def show_date() -> None: + """Print the current date.""" + execute_command('date', 'Current Time') + + +def show_installed_packages() -> None: + """Prints installed packages.""" + execute_command('dpkg --list', 'Installed Packages') + + +def show_loaded_modules() -> None: + """Prints loaded modules /proc/modules""" + execute_command('cat /proc/modules', 'Loaded Modules') + + +def show_cpu_statistics() -> None: + """Prints CPU statistics.""" + execute_command('/usr/bin/true', 'CPU') + execute_command('lscpu', 'Installed CPU\'s') + execute_command('top --iterations 1 --batch-mode --accum-time-toggle', 'Cumulative CPU Time Used by Running Processes') + execute_command('cat /proc/loadavg', 'Load Average') + + +def show_system_interrupts() -> None: + """Prints system interrupts.""" + execute_command('cat /proc/interrupts', 'Hardware Interrupt Counters') + + +def show_soft_irqs() -> None: + """Prints soft IRQ's.""" + execute_command('cat /proc/softirqs', 'Soft IRQ\'s') + + +def show_softnet_statistics() -> None: + """Prints softnet statistics.""" + execute_command('cat /proc/net/softnet_stat', 'cat /proc/net/softnet_stat') + + +def show_running_processes() -> None: + """Prints current running processes""" + execute_command('ps -ef', 'Running Processes') + + +def show_memory_usage() -> None: + """Prints memory usage""" + execute_command('/usr/bin/true', 'Memory') + execute_command('cat /proc/meminfo', 'Installed Memory') + execute_command('free', 'Memory Usage') + + +def list_disks(): + disks = set() + with open('/proc/partitions') as partitions_file: + for line in partitions_file: + fields = line.strip().split() + if len(fields) == 4 and fields[3].isalpha() and fields[3] != 'name': + disks.add(fields[3]) + return disks + + +def show_storage() -> None: + """Prints storage information.""" + execute_command('cat /proc/devices', 'Devices') + execute_command('cat /proc/partitions', 'Partitions') + + for disk in list_disks(): + execute_command(f'fdisk --list /dev/{disk}', f'Partitioning for disk {disk}') + + +def main(): + # Configuration data + show_version() + show_config_file() + show_running_config() + show_package_repository_config() + show_user_startup_scripts() + show_frr_config() + + # Interfaces + show_interfaces() + show_interface_statistics() + show_physical_interface_statistics() + show_bridge() + show_arp() + + # Routing + show_route() + + # Firewall + show_firewall() + + # System + show_system() + show_date() + show_installed_packages() + show_loaded_modules() + + # CPU + show_cpu_statistics() + show_system_interrupts() + show_soft_irqs() + show_softnet_statistics() + + # Memory + show_memory_usage() + + # Storage + show_storage() + + # Processes + show_running_processes() + + # TODO: Get information from clouds + + +if __name__ == "__main__": + main() diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py deleted file mode 100755 index 73688c4ea..000000000 --- a/src/op_mode/show_vpn_ra.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os -import sys -import re - -from vyos.util import popen - -# chech connection to pptp and l2tp daemon -def get_sessions(): - absent_pptp = False - absent_l2tp = False - pptp_cmd = "accel-cmd -p 2003 show sessions" - l2tp_cmd = "accel-cmd -p 2004 show sessions" - err_pattern = "^Connection.+failed$" - # This value for chack only output header without sessions. - len_def_header = 170 - - # Check pptp - output, err = popen(pptp_cmd, decode='utf-8') - if not err and len(output) > len_def_header and not re.search(err_pattern, output): - print(output) - else: - absent_pptp = True - - # Check l2tp - output, err = popen(l2tp_cmd, decode='utf-8') - if not err and len(output) > len_def_header and not re.search(err_pattern, output): - print(output) - else: - absent_l2tp = True - - if absent_l2tp and absent_pptp: - print("No active remote access VPN sessions") - - -def main(): - get_sessions() - - -if __name__ == '__main__': - main() diff --git a/src/op_mode/show_wwan.py b/src/op_mode/show_wwan.py index 529b5bd0f..eb601a456 100755 --- a/src/op_mode/show_wwan.py +++ b/src/op_mode/show_wwan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,6 +17,7 @@ import argparse from sys import exit +from vyos.configquery import ConfigTreeQuery from vyos.util import cmd parser = argparse.ArgumentParser() @@ -49,6 +50,11 @@ def qmi_cmd(device, command, silent=False): if __name__ == '__main__': args = parser.parse_args() + tmp = ConfigTreeQuery() + if not tmp.exists(['interfaces', 'wwan', args.interface]): + print(f'Interface "{args.interface}" unconfigured!') + exit(1) + # remove the WWAN prefix from the interface, required for the CDC interface if_num = args.interface.replace('wwan','') cdc = f'/dev/cdc-wdm{if_num}' diff --git a/src/op_mode/vpn_ipsec.py b/src/op_mode/vpn_ipsec.py index 2392cfe92..b81d1693e 100755 --- a/src/op_mode/vpn_ipsec.py +++ b/src/op_mode/vpn_ipsec.py @@ -16,12 +16,12 @@ import re import argparse -from subprocess import TimeoutExpired from vyos.util import call SWANCTL_CONF = '/etc/swanctl/swanctl.conf' + def get_peer_connections(peer, tunnel, return_all = False): search = rf'^[\s]*(peer_{peer}_(tunnel_[\d]+|vti)).*' matches = [] @@ -34,57 +34,6 @@ def get_peer_connections(peer, tunnel, return_all = False): matches.append(result[1]) return matches -def reset_peer(peer, tunnel): - if not peer: - print('Invalid peer, aborting') - return - - conns = get_peer_connections(peer, tunnel, return_all = (not tunnel or tunnel == 'all')) - - if not conns: - print('Tunnel(s) not found, aborting') - return - - result = True - for conn in conns: - try: - call(f'/usr/sbin/ipsec down {conn}{{*}}', timeout = 10) - call(f'/usr/sbin/ipsec up {conn}', timeout = 10) - except TimeoutExpired as e: - print(f'Timed out while resetting {conn}') - result = False - - - print('Peer reset result: ' + ('success' if result else 'failed')) - -def get_profile_connection(profile, tunnel = None): - search = rf'(dmvpn-{profile}-[\w]+)' if tunnel == 'all' else rf'(dmvpn-{profile}-{tunnel})' - with open(SWANCTL_CONF, 'r') as f: - for line in f.readlines(): - result = re.search(search, line) - if result: - return result[1] - return None - -def reset_profile(profile, tunnel): - if not profile: - print('Invalid profile, aborting') - return - - if not tunnel: - print('Invalid tunnel, aborting') - return - - conn = get_profile_connection(profile) - - if not conn: - print('Profile not found, aborting') - return - - call(f'/usr/sbin/ipsec down {conn}') - result = call(f'/usr/sbin/ipsec up {conn}') - - print('Profile reset result: ' + ('success' if result == 0 else 'failed')) def debug_peer(peer, tunnel): peer = peer.replace(':', '-') @@ -119,6 +68,7 @@ def debug_peer(peer, tunnel): for conn in conns: call(f'/usr/sbin/ipsec statusall | grep {conn}') + if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--action', help='Control action', required=True) @@ -127,9 +77,6 @@ if __name__ == '__main__': args = parser.parse_args() - if args.action == 'reset-peer': - reset_peer(args.name, args.tunnel) - elif args.action == "reset-profile": - reset_profile(args.name, args.tunnel) - elif args.action == "vpn-debug": + + if args.action == "vpn-debug": debug_peer(args.name, args.tunnel) diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index aa1ba0eb0..ef4966466 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -19,9 +19,6 @@ from . graphql.mutations import mutation from . graphql.directives import directives_dict from . graphql.errors import op_mode_error from . graphql.auth_token_mutation import auth_token_mutation -from . generate.schema_from_op_mode import generate_op_mode_definitions -from . generate.schema_from_config_session import generate_config_session_definitions -from . generate.schema_from_composite import generate_composite_definitions from . libs.token_auth import init_secret from . import state from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers @@ -29,10 +26,6 @@ from ariadne import make_executable_schema, load_schema_from_path, snake_case_fa def generate_schema(): api_schema_dir = vyos.defaults.directories['api_schema'] - generate_op_mode_definitions() - generate_config_session_definitions() - generate_composite_definitions() - if state.settings['app'].state.vyos_auth_type == 'token': init_secret() diff --git a/src/services/api/graphql/generate/config_session_function.py b/src/services/api/graphql/generate/config_session_function.py index fc0dd7a87..4ebb47a7e 100644 --- a/src/services/api/graphql/generate/config_session_function.py +++ b/src/services/api/graphql/generate/config_session_function.py @@ -8,8 +8,12 @@ def show_config(path: list[str], configFormat: typing.Optional[str]): def show(path: list[str]): pass +def show_user_info(user: str): + pass + queries = {'show_config': show_config, - 'show': show} + 'show': show, + 'show_user_info': show_user_info} def save_config_file(fileName: typing.Optional[str]): pass @@ -24,5 +28,3 @@ mutations = {'save_config_file': save_config_file, 'load_config_file': load_config_file, 'add_system_image': add_system_image, 'delete_system_image': delete_system_image} - - diff --git a/src/services/api/graphql/generate/generate_schema.py b/src/services/api/graphql/generate/generate_schema.py new file mode 100755 index 000000000..dd5e7ea56 --- /dev/null +++ b/src/services/api/graphql/generate/generate_schema.py @@ -0,0 +1,26 @@ +#!/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/>. +# +# + +from schema_from_op_mode import generate_op_mode_definitions +from schema_from_config_session import generate_config_session_definitions +from schema_from_composite import generate_composite_definitions + +if __name__ == '__main__': + generate_op_mode_definitions() + generate_config_session_definitions() + generate_composite_definitions() diff --git a/src/services/api/graphql/generate/schema_from_composite.py b/src/services/api/graphql/generate/schema_from_composite.py index 61a08cb2f..06e74032d 100755 --- a/src/services/api/graphql/generate/schema_from_composite.py +++ b/src/services/api/graphql/generate/schema_from_composite.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -20,59 +20,31 @@ import os import sys -import json -from inspect import signature, getmembers, isfunction, isclass, getmro +from inspect import signature from jinja2 import Template from vyos.defaults import directories if __package__ is None or __package__ == '': - sys.path.append("/usr/libexec/vyos/services/api") + sys.path.append(os.path.join(directories['services'], 'api')) from graphql.libs.op_mode import snake_to_pascal_case, map_type_name from composite_function import queries, mutations - from vyos.config import Config - from vyos.configdict import dict_merge - from vyos.xml import defaults else: from .. libs.op_mode import snake_to_pascal_case, map_type_name from . composite_function import queries, mutations - from .. import state SCHEMA_PATH = directories['api_schema'] +CLIENT_OP_PATH = directories['api_client_op'] -if __package__ is None or __package__ == '': - # allow running stand-alone - conf = Config() - base = ['service', 'https', 'api'] - graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True) - if 'graphql' not in graphql_dict: - exit("graphql is not configured") - - graphql_dict = dict_merge(defaults(base), graphql_dict) - auth_type = graphql_dict['graphql']['authentication']['type'] -else: - auth_type = state.settings['app'].state.vyos_auth_type - -schema_data: dict = {'auth_type': auth_type, - 'schema_name': '', +schema_data: dict = {'schema_name': '', 'schema_fields': []} query_template = """ -{%- if auth_type == 'key' %} input {{ schema_name }}Input { - key: String! + key: String {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } -{%- elif schema_fields %} -input {{ schema_name }}Input { - {%- for field_entry in schema_fields %} - {{ field_entry }} - {%- endfor %} -} -{%- endif %} type {{ schema_name }} { result: Generic @@ -85,29 +57,17 @@ type {{ schema_name }}Result { } extend type Query { -{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositequery -{%- else %} - {{ schema_name }} : {{ schema_name }}Result @compositequery -{%- endif %} } """ mutation_template = """ -{%- if auth_type == 'key' %} -input {{ schema_name }}Input { - key: String! - {%- for field_entry in schema_fields %} - {{ field_entry }} - {%- endfor %} -} -{%- elif schema_fields %} input {{ schema_name }}Input { + key: String {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } -{%- endif %} type {{ schema_name }} { result: Generic @@ -120,11 +80,31 @@ type {{ schema_name }}Result { } extend type Mutation { -{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @compositemutation -{%- else %} - {{ schema_name }} : {{ schema_name }}Result @compositemutation -{%- endif %} +} +""" + +op_query_template = """ +query {{ op_name }} ({{ op_sig }}) { + {{ op_name }} (data: { {{ op_arg }} }) { + success + errors + data { + result + } + } +} +""" + +op_mutation_template = """ +mutation {{ op_name }} ({{ op_sig }}) { + {{ op_name }} (data: { {{ op_arg }} }) { + success + errors + data { + result + } + } } """ @@ -147,19 +127,52 @@ def create_schema(func_name: str, func: callable, template: str) -> str: return res +def create_client_op(func_name: str, func: callable, template: str) -> str: + sig = signature(func) + + field_dict = {} + for k in sig.parameters: + field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation) + + op_sig = ['$key: String'] + op_arg = ['key: $key'] + for k,v in field_dict.items(): + op_sig.append('$'+k+': '+v) + op_arg.append(k+': $'+k) + + op_data = {} + op_data['op_name'] = snake_to_pascal_case(func_name) + op_data['op_sig'] = ', '.join(op_sig) + op_data['op_arg'] = ', '.join(op_arg) + + j2_template = Template(template) + + res = j2_template.render(op_data) + + return res + def generate_composite_definitions(): - results = [] + schema = [] + client_op = [] for name,func in queries.items(): res = create_schema(name, func, query_template) - results.append(res) + schema.append(res) + res = create_client_op(name, func, op_query_template) + client_op.append(res) for name,func in mutations.items(): res = create_schema(name, func, mutation_template) - results.append(res) + schema.append(res) + res = create_client_op(name, func, op_mutation_template) + client_op.append(res) - out = '\n'.join(results) + out = '\n'.join(schema) with open(f'{SCHEMA_PATH}/composite.graphql', 'w') as f: f.write(out) + out = '\n'.join(client_op) + with open(f'{CLIENT_OP_PATH}/composite.graphql', 'w') as f: + f.write(out) + if __name__ == '__main__': generate_composite_definitions() diff --git a/src/services/api/graphql/generate/schema_from_config_session.py b/src/services/api/graphql/generate/schema_from_config_session.py index 49bf2440e..1d5ff1e53 100755 --- a/src/services/api/graphql/generate/schema_from_config_session.py +++ b/src/services/api/graphql/generate/schema_from_config_session.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -20,59 +20,31 @@ import os import sys -import json -from inspect import signature, getmembers, isfunction, isclass, getmro +from inspect import signature from jinja2 import Template from vyos.defaults import directories if __package__ is None or __package__ == '': - sys.path.append("/usr/libexec/vyos/services/api") + sys.path.append(os.path.join(directories['services'], 'api')) from graphql.libs.op_mode import snake_to_pascal_case, map_type_name from config_session_function import queries, mutations - from vyos.config import Config - from vyos.configdict import dict_merge - from vyos.xml import defaults else: from .. libs.op_mode import snake_to_pascal_case, map_type_name from . config_session_function import queries, mutations - from .. import state SCHEMA_PATH = directories['api_schema'] +CLIENT_OP_PATH = directories['api_client_op'] -if __package__ is None or __package__ == '': - # allow running stand-alone - conf = Config() - base = ['service', 'https', 'api'] - graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True) - if 'graphql' not in graphql_dict: - exit("graphql is not configured") - - graphql_dict = dict_merge(defaults(base), graphql_dict) - auth_type = graphql_dict['graphql']['authentication']['type'] -else: - auth_type = state.settings['app'].state.vyos_auth_type - -schema_data: dict = {'auth_type': auth_type, - 'schema_name': '', +schema_data: dict = {'schema_name': '', 'schema_fields': []} query_template = """ -{%- if auth_type == 'key' %} input {{ schema_name }}Input { - key: String! + key: String {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } -{%- elif schema_fields %} -input {{ schema_name }}Input { - {%- for field_entry in schema_fields %} - {{ field_entry }} - {%- endfor %} -} -{%- endif %} type {{ schema_name }} { result: Generic @@ -85,29 +57,17 @@ type {{ schema_name }}Result { } extend type Query { -{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionquery -{%- else %} - {{ schema_name }} : {{ schema_name }}Result @configsessionquery -{%- endif %} } """ mutation_template = """ -{%- if auth_type == 'key' %} -input {{ schema_name }}Input { - key: String! - {%- for field_entry in schema_fields %} - {{ field_entry }} - {%- endfor %} -} -{%- elif schema_fields %} input {{ schema_name }}Input { + key: String {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } -{%- endif %} type {{ schema_name }} { result: Generic @@ -120,11 +80,31 @@ type {{ schema_name }}Result { } extend type Mutation { -{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @configsessionmutation -{%- else %} - {{ schema_name }} : {{ schema_name }}Result @configsessionmutation -{%- endif %} +} +""" + +op_query_template = """ +query {{ op_name }} ({{ op_sig }}) { + {{ op_name }} (data: { {{ op_arg }} }) { + success + errors + data { + result + } + } +} +""" + +op_mutation_template = """ +mutation {{ op_name }} ({{ op_sig }}) { + {{ op_name }} (data: { {{ op_arg }} }) { + success + errors + data { + result + } + } } """ @@ -147,19 +127,52 @@ def create_schema(func_name: str, func: callable, template: str) -> str: return res +def create_client_op(func_name: str, func: callable, template: str) -> str: + sig = signature(func) + + field_dict = {} + for k in sig.parameters: + field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation) + + op_sig = ['$key: String'] + op_arg = ['key: $key'] + for k,v in field_dict.items(): + op_sig.append('$'+k+': '+v) + op_arg.append(k+': $'+k) + + op_data = {} + op_data['op_name'] = snake_to_pascal_case(func_name) + op_data['op_sig'] = ', '.join(op_sig) + op_data['op_arg'] = ', '.join(op_arg) + + j2_template = Template(template) + + res = j2_template.render(op_data) + + return res + def generate_config_session_definitions(): - results = [] + schema = [] + client_op = [] for name,func in queries.items(): res = create_schema(name, func, query_template) - results.append(res) + schema.append(res) + res = create_client_op(name, func, op_query_template) + client_op.append(res) for name,func in mutations.items(): res = create_schema(name, func, mutation_template) - results.append(res) + schema.append(res) + res = create_client_op(name, func, op_mutation_template) + client_op.append(res) - out = '\n'.join(results) + out = '\n'.join(schema) with open(f'{SCHEMA_PATH}/configsession.graphql', 'w') as f: f.write(out) + out = '\n'.join(client_op) + with open(f'{CLIENT_OP_PATH}/configsession.graphql', 'w') as f: + f.write(out) + if __name__ == '__main__': generate_config_session_definitions() diff --git a/src/services/api/graphql/generate/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py index b320a529e..229ccf90f 100755 --- a/src/services/api/graphql/generate/schema_from_op_mode.py +++ b/src/services/api/graphql/generate/schema_from_op_mode.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-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 @@ -26,60 +26,35 @@ from jinja2 import Template from vyos.defaults import directories from vyos.opmode import _is_op_mode_function_name as is_op_mode_function_name +from vyos.opmode import _get_literal_values as get_literal_values from vyos.util import load_as_module if __package__ is None or __package__ == '': - sys.path.append("/usr/libexec/vyos/services/api") + sys.path.append(os.path.join(directories['services'], 'api')) from graphql.libs.op_mode import is_show_function_name from graphql.libs.op_mode import snake_to_pascal_case, map_type_name - from vyos.config import Config - from vyos.configdict import dict_merge - from vyos.xml import defaults else: from .. libs.op_mode import is_show_function_name from .. libs.op_mode import snake_to_pascal_case, map_type_name - from .. import state OP_MODE_PATH = directories['op_mode'] SCHEMA_PATH = directories['api_schema'] +CLIENT_OP_PATH = directories['api_client_op'] DATA_DIR = directories['data'] + op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json') op_mode_error_schema = 'op_mode_error.graphql' -if __package__ is None or __package__ == '': - # allow running stand-alone - conf = Config() - base = ['service', 'https', 'api'] - graphql_dict = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True) - if 'graphql' not in graphql_dict: - exit("graphql is not configured") - - graphql_dict = dict_merge(defaults(base), graphql_dict) - auth_type = graphql_dict['graphql']['authentication']['type'] -else: - auth_type = state.settings['app'].state.vyos_auth_type - -schema_data: dict = {'auth_type': auth_type, - 'schema_name': '', +schema_data: dict = {'schema_name': '', 'schema_fields': []} query_template = """ -{%- if auth_type == 'key' %} -input {{ schema_name }}Input { - key: String! - {%- for field_entry in schema_fields %} - {{ field_entry }} - {%- endfor %} -} -{%- elif schema_fields %} input {{ schema_name }}Input { + key: String {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } -{%- endif %} type {{ schema_name }} { result: Generic @@ -93,29 +68,17 @@ type {{ schema_name }}Result { } extend type Query { -{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopquery -{%- else %} - {{ schema_name }} : {{ schema_name }}Result @genopquery -{%- endif %} } """ mutation_template = """ -{%- if auth_type == 'key' %} -input {{ schema_name }}Input { - key: String! - {%- for field_entry in schema_fields %} - {{ field_entry }} - {%- endfor %} -} -{%- elif schema_fields %} input {{ schema_name }}Input { + key: String {%- for field_entry in schema_fields %} {{ field_entry }} {%- endfor %} } -{%- endif %} type {{ schema_name }} { result: Generic @@ -129,11 +92,15 @@ type {{ schema_name }}Result { } extend type Mutation { -{%- if auth_type == 'key' or schema_fields %} {{ schema_name }}(data: {{ schema_name }}Input) : {{ schema_name }}Result @genopmutation -{%- else %} - {{ schema_name }} : {{ schema_name }}Result @genopquery -{%- endif %} +} +""" + +enum_template = """ +enum {{ enum_name }} { + {%- for field_entry in enum_fields %} + {{ field_entry }} + {%- endfor %} } """ @@ -152,12 +119,52 @@ type {{ name }} implements OpModeError { {%- endfor %} """ -def create_schema(func_name: str, base_name: str, func: callable) -> str: +op_query_template = """ +query {{ op_name }} ({{ op_sig }}) { + {{ op_name }} (data: { {{ op_arg }} }) { + success + errors + op_mode_error { + name + message + vyos_code + } + data { + result + } + } +} +""" + +op_mutation_template = """ +mutation {{ op_name }} ({{ op_sig }}) { + {{ op_name }} (data: { {{ op_arg }} }) { + success + errors + op_mode_error { + name + message + vyos_code + } + data { + result + } + } +} +""" + +def create_schema(func_name: str, base_name: str, func: callable, + enums: dict) -> str: sig = signature(func) + for k in sig.parameters: + t = get_literal_values(sig.parameters[k].annotation) + if t: + enums[t] = snake_to_pascal_case(sig.parameters[k].name + '_' + base_name) + field_dict = {} for k in sig.parameters: - field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation) + field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation, enums) # It is assumed that if one is generating a schema for a 'show_*' # function, that 'get_raw_data' is present and 'raw' is desired. @@ -180,6 +187,58 @@ def create_schema(func_name: str, base_name: str, func: callable) -> str: return res +def create_client_op(func_name: str, base_name: str, func: callable, + enums: dict) -> str: + sig = signature(func) + + for k in sig.parameters: + t = get_literal_values(sig.parameters[k].annotation) + if t: + enums[t] = snake_to_pascal_case(sig.parameters[k].name + '_' + base_name) + + field_dict = {} + for k in sig.parameters: + field_dict[sig.parameters[k].name] = map_type_name(sig.parameters[k].annotation, enums) + + # It is assumed that if one is generating a schema for a 'show_*' + # function, that 'get_raw_data' is present and 'raw' is desired. + if 'raw' in list(field_dict): + del field_dict['raw'] + + op_sig = ['$key: String'] + op_arg = ['key: $key'] + for k,v in field_dict.items(): + op_sig.append('$'+k+': '+v) + op_arg.append(k+': $'+k) + + op_data = {} + op_data['op_name'] = snake_to_pascal_case(func_name + '_' + base_name) + op_data['op_sig'] = ', '.join(op_sig) + op_data['op_arg'] = ', '.join(op_arg) + + if is_show_function_name(func_name): + j2_template = Template(op_query_template) + else: + j2_template = Template(op_mutation_template) + + res = j2_template.render(op_data) + + return res + +def create_enums(enums: dict) -> str: + enum_data = [] + for k, v in enums.items(): + enum = {'enum_name': v, 'enum_fields': list(k)} + enum_data.append(enum) + + out = '' + j2_template = Template(enum_template) + for el in enum_data: + out += j2_template.render(el) + out += '\n' + + return out + def create_error_schema(): from vyos import opmode @@ -200,6 +259,8 @@ def create_error_schema(): return res def generate_op_mode_definitions(): + os.makedirs(CLIENT_OP_PATH, exist_ok=True) + out = create_error_schema() with open(f'{SCHEMA_PATH}/{op_mode_error_schema}', 'w') as f: f.write(out) @@ -218,14 +279,23 @@ def generate_op_mode_definitions(): for (name, thunk) in funcs: funcs_dict[name] = thunk - results = [] + schema = [] + client_op = [] + enums = {} # gather enums from function Literal type args for name,func in funcs_dict.items(): - res = create_schema(name, basename, func) - results.append(res) + res = create_schema(name, basename, func, enums) + schema.append(res) + res = create_client_op(name, basename, func, enums) + client_op.append(res) - out = '\n'.join(results) + out = create_enums(enums) + out += '\n'.join(schema) with open(f'{SCHEMA_PATH}/{basename}.graphql', 'w') as f: f.write(out) + out = '\n'.join(client_op) + with open(f'{CLIENT_OP_PATH}/{basename}.graphql', 'w') as f: + f.write(out) + if __name__ == '__main__': generate_op_mode_definitions() diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py index 21ac40094..603a13758 100644 --- a/src/services/api/graphql/graphql/auth_token_mutation.py +++ b/src/services/api/graphql/graphql/auth_token_mutation.py @@ -20,6 +20,7 @@ from ariadne import ObjectType, UnionType from graphql import GraphQLResolveInfo from .. libs.token_auth import generate_token +from .. session.session import get_user_info from .. import state auth_token_mutation = ObjectType("Mutation") @@ -36,13 +37,24 @@ def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict): datetime.timedelta(seconds=exp_interval)) res = generate_token(user, passwd, secret, expiration) - if res: + try: + res |= get_user_info(user) + except ValueError: + # non-existent user already caught + pass + if 'token' in res: data['result'] = res return { "success": True, "data": data } + if 'errors' in res: + return { + "success": False, + "errors": res['errors'] + } + return { "success": False, "errors": ['token generation failed'] diff --git a/src/services/api/graphql/graphql/client_op/auth_token.graphql b/src/services/api/graphql/graphql/client_op/auth_token.graphql new file mode 100644 index 000000000..5ea2ecc1c --- /dev/null +++ b/src/services/api/graphql/graphql/client_op/auth_token.graphql @@ -0,0 +1,10 @@ + +mutation AuthToken ($username: String!, $password: String!) { + AuthToken (data: { username: $username, password: $password }) { + success + errors + data { + result + } + } +} diff --git a/src/services/api/graphql/libs/op_mode.py b/src/services/api/graphql/libs/op_mode.py index c553bbd67..e91d8bd0f 100644 --- a/src/services/api/graphql/libs/op_mode.py +++ b/src/services/api/graphql/libs/op_mode.py @@ -16,13 +16,13 @@ import os import re import typing -import importlib.util -from typing import Union +from typing import Union, Tuple, Optional from humps import decamelize from vyos.defaults import directories from vyos.util import load_as_module from vyos.opmode import _normalize_field_names +from vyos.opmode import _is_literal_type, _get_literal_values def load_op_mode_as_module(name: str): path = os.path.join(directories['op_mode'], name) @@ -73,7 +73,7 @@ def snake_to_pascal_case(name: str) -> str: res = ''.join(map(str.title, name.split('_'))) return res -def map_type_name(type_name: type, optional: bool = False) -> str: +def map_type_name(type_name: type, enums: Optional[dict] = None, optional: bool = False) -> str: if type_name == str: return 'String!' if not optional else 'String = null' if type_name == int: @@ -82,12 +82,17 @@ def map_type_name(type_name: type, optional: bool = False) -> str: return 'Boolean = false' if typing.get_origin(type_name) == list: if not optional: - return f'[{map_type_name(typing.get_args(type_name)[0])}]!' - return f'[{map_type_name(typing.get_args(type_name)[0])}]' + return f'[{map_type_name(typing.get_args(type_name)[0], enums=enums)}]!' + return f'[{map_type_name(typing.get_args(type_name)[0], enums=enums)}]' + if _is_literal_type(type_name): + mapped = enums.get(_get_literal_values(type_name), '') + if not mapped: + raise ValueError(typing.get_args(type_name)) + return f'{mapped}!' if not optional else mapped # typing.Optional is typing.Union[_, NoneType] if (typing.get_origin(type_name) is typing.Union and typing.get_args(type_name)[1] == type(None)): - return f'{map_type_name(typing.get_args(type_name)[0], optional=True)}' + return f'{map_type_name(typing.get_args(type_name)[0], enums=enums, optional=True)}' # scalar 'Generic' is defined in schema.graphql return 'Generic' diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py index 2100eba7f..8585485c9 100644 --- a/src/services/api/graphql/libs/token_auth.py +++ b/src/services/api/graphql/libs/token_auth.py @@ -29,14 +29,13 @@ def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict: payload_data = {'iss': user, 'sub': user_id, 'exp': exp} secret = state.settings.get('secret') if secret is None: - return { - "success": False, - "errors": ['failed secret generation'] - } + return {"errors": ['missing secret']} token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256") users |= {user_id: user} return {'token': token} + else: + return {"errors": ['failed pam authentication']} def get_user_context(request): context = {} diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py index 0b77b1433..3c5a062b6 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -29,6 +29,28 @@ from api.graphql.libs.op_mode import normalize_output op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') +def get_config_dict(path=[], effective=False, key_mangling=None, + get_first_key=False, no_multi_convert=False, + no_tag_node_value_mangle=False): + config = Config() + return config.get_config_dict(path=path, effective=effective, + key_mangling=key_mangling, + get_first_key=get_first_key, + no_multi_convert=no_multi_convert, + no_tag_node_value_mangle=no_tag_node_value_mangle) + +def get_user_info(user): + user_info = {} + info = get_config_dict(['system', 'login', 'user', user], + get_first_key=True) + if not info: + raise ValueError("No such user") + + user_info['user'] = user + user_info['full_name'] = info.get('full-name', '') + + return user_info + class Session: """ Wrapper for calling configsession functions based on GraphQL requests. @@ -116,6 +138,19 @@ class Session: return res + def show_user_info(self): + session = self._session + data = self._data + + user_info = {} + user = data['user'] + try: + user_info = get_user_info(user) + except Exception as error: + raise error + + return user_info + def system_status(self): import api.graphql.session.composite.system_status as system_status diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index a380f2e66..894f9e24d 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -329,7 +329,7 @@ tag_regex_schema = op_type_schema.extend({ forward_zone_add_schema = op_type_schema.extend({ 'data': { str: { - 'server': [str], + 'name_server': [str], 'addnta': Any({}, None), 'recursion_desired': Any({}, None), } diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index f59e089ae..acaa383b4 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -283,7 +283,7 @@ class MultipartRequest(Request): return self._headers async def form(self) -> FormData: - if not hasattr(self, "_form"): + if self._form is None: assert ( parse_options_header is not None ), "The `python-multipart` library must be installed to use form parsing." @@ -425,7 +425,7 @@ async def validation_exception_handler(request, exc): return error(400, str(exc.errors()[0])) @app.post('/configure') -def configure_op(data: Union[ConfigureModel, ConfigureListModel]): +async def configure_op(data: Union[ConfigureModel, ConfigureListModel]): session = app.state.vyos_session env = session.get_session_env() config = vyos.config.Config(session_env=env) @@ -494,7 +494,7 @@ def configure_op(data: Union[ConfigureModel, ConfigureListModel]): return success(None) @app.post("/retrieve") -def retrieve_op(data: RetrieveModel): +async def retrieve_op(data: RetrieveModel): session = app.state.vyos_session env = session.get_session_env() config = vyos.config.Config(session_env=env) diff --git a/src/system/vyos-config-cloud-init.py b/src/system/vyos-config-cloud-init.py new file mode 100755 index 000000000..0a6c1f9bc --- /dev/null +++ b/src/system/vyos-config-cloud-init.py @@ -0,0 +1,169 @@ +#!/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 logging +from concurrent.futures import ProcessPoolExecutor +from pathlib import Path +from subprocess import run, TimeoutExpired +from sys import exit + +from psutil import net_if_addrs, AF_LINK +from systemd.journal import JournalHandler +from yaml import safe_load + +from vyos.template import render + +# define a path to the configuration file and template +config_file = '/etc/cloud/cloud.cfg.d/20_vyos_network.cfg' +template_file = 'system/cloud_init_networking.j2' + + +def check_interface_dhcp(iface_name: str) -> bool: + """Check DHCP client can work on an interface + + Args: + iface_name (str): interface name + + Returns: + bool: check result + """ + dhclient_command: list[str] = [ + 'dhclient', '-4', '-1', '-q', '--no-pid', '-sf', '/bin/true', iface_name + ] + check_result = False + # try to get an IP address + # we use dhclient behavior here to speedup detection + # if dhclient receives a configuration and configure an interface + # it switch to background + # If no - it will keep running in foreground + try: + run(['ip', 'l', 'set', iface_name, 'up']) + run(dhclient_command, timeout=5) + check_result = True + except TimeoutExpired: + pass + finally: + run(['ip', 'l', 'set', iface_name, 'down']) + + logger.info(f'DHCP server was found on {iface_name}: {check_result}') + return check_result + + +def dhclient_cleanup() -> None: + """Clean up after dhclients + """ + run(['killall', 'dhclient']) + leases_file: Path = Path('/var/lib/dhcp/dhclient.leases') + leases_file.unlink(missing_ok=True) + logger.debug('cleaned up after dhclients') + + +def dict_interfaces() -> dict[str, str]: + """Return list of available network interfaces except loopback + + Returns: + list[str]: a list of interfaces + """ + interfaces_dict: dict[str, str] = {} + ifaces = net_if_addrs() + for iface_name, iface_addresses in ifaces.items(): + # we do not need loopback interface + if iface_name == 'lo': + continue + # check other interfaces for MAC addresses + for iface_addr in iface_addresses: + if iface_addr.family == AF_LINK and iface_addr.address: + interfaces_dict[iface_name] = iface_addr.address + continue + + logger.debug(f'found interfaces: {interfaces_dict}') + return interfaces_dict + + +def need_to_check() -> bool: + """Check if we need to perform DHCP checks + + Returns: + bool: check result + """ + # if cloud-init config does not exist, we do not need to do anything + ci_config_vyos = Path('/etc/cloud/cloud.cfg.d/20_vyos_custom.cfg') + if not ci_config_vyos.exists(): + logger.info( + 'No need to check interfaces: Cloud-init config file was not found') + return False + + # load configuration file + try: + config = safe_load(ci_config_vyos.read_text()) + except: + logger.error('Cloud-init config file has a wrong format') + return False + + # check if we have in config configured option + # vyos_config_options: + # network_preconfigure: true + if not config.get('vyos_config_options', {}).get('network_preconfigure'): + logger.info( + 'No need to check interfaces: Cloud-init config option "network_preconfigure" is not set' + ) + return False + + return True + + +if __name__ == '__main__': + # prepare logger + logger = logging.getLogger(__name__) + logger.addHandler(JournalHandler(SYSLOG_IDENTIFIER=Path(__file__).name)) + logger.setLevel(logging.INFO) + + # we need to give udev some time to rename all interfaces + # this is placed before need_to_check() call, because we are not always + # need to preconfigure cloud-init, but udev always need to finish its work + # before cloud-init start + run(['udevadm', 'settle']) + logger.info('udev finished its work, we continue') + + # do not perform any checks if this is not required + if not need_to_check(): + exit() + + # get list of interfaces and check them + interfaces_dhcp: list[dict[str, str]] = [] + interfaces_dict: dict[str, str] = dict_interfaces() + + with ProcessPoolExecutor(max_workers=len(interfaces_dict)) as executor: + iface_check_results = [{ + 'dhcp': executor.submit(check_interface_dhcp, iface_name), + 'append': { + 'name': iface_name, + 'mac': iface_mac + } + } for iface_name, iface_mac in interfaces_dict.items()] + + dhclient_cleanup() + + for iface_check_result in iface_check_results: + if iface_check_result.get('dhcp').result(): + interfaces_dhcp.append(iface_check_result.get('append')) + + # render cloud-init config + if interfaces_dhcp: + logger.debug('rendering cloud-init network configuration') + render(config_file, template_file, {'ifaces_list': interfaces_dhcp}) + + exit() diff --git a/src/systemd/vyos-config-cloud-init.service b/src/systemd/vyos-config-cloud-init.service new file mode 100644 index 000000000..ba6f90e6d --- /dev/null +++ b/src/systemd/vyos-config-cloud-init.service @@ -0,0 +1,19 @@ +[Unit] +Description=Pre-configure Cloud-init +DefaultDependencies=no +Requires=systemd-remount-fs.service +Requires=systemd-udevd.service +Wants=network-pre.target +After=systemd-remount-fs.service +After=systemd-udevd.service +Before=cloud-init-local.service + +[Service] +Type=oneshot +ExecStart=/usr/libexec/vyos/system/vyos-config-cloud-init.py +TimeoutSec=120 +KillMode=process +StandardOutput=journal+console + +[Install] +WantedBy=cloud-init-local.service diff --git a/src/systemd/vyos-wan-load-balance.service b/src/systemd/vyos-wan-load-balance.service new file mode 100644 index 000000000..7d62a2ff6 --- /dev/null +++ b/src/systemd/vyos-wan-load-balance.service @@ -0,0 +1,15 @@ +[Unit] +Description=VyOS WAN load-balancing service +After=vyos-router.service + +[Service] +ExecStart=/opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid +ExecReload=/bin/kill -s SIGTERM $MAINPID && sleep 5 && /opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid +ExecStop=/bin/kill -s SIGTERM $MAINPID +PIDFile=/var/run/vyatta/wlb.pid +KillMode=process +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target diff --git a/src/tests/test_config_diff.py b/src/tests/test_config_diff.py new file mode 100644 index 000000000..f61cbc4a2 --- /dev/null +++ b/src/tests/test_config_diff.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import vyos.configtree + +from unittest import TestCase + +class TestConfigDiff(TestCase): + def setUp(self): + with open('tests/data/config.left', 'r') as f: + config_string = f.read() + self.config_left = vyos.configtree.ConfigTree(config_string) + + with open('tests/data/config.right', 'r') as f: + config_string = f.read() + self.config_right = vyos.configtree.ConfigTree(config_string) + + self.config_null = vyos.configtree.ConfigTree('') + + def test_unit(self): + diff = vyos.configtree.DiffTree(self.config_left, self.config_null) + sub = diff.sub + self.assertEqual(sub.to_string(), self.config_left.to_string()) + + diff = vyos.configtree.DiffTree(self.config_null, self.config_left) + add = diff.add + self.assertEqual(add.to_string(), self.config_left.to_string()) + + def test_symmetry(self): + lr_diff = vyos.configtree.DiffTree(self.config_left, + self.config_right) + rl_diff = vyos.configtree.DiffTree(self.config_right, + self.config_left) + + sub = lr_diff.sub + add = rl_diff.add + self.assertEqual(sub.to_string(), add.to_string()) + add = lr_diff.add + sub = rl_diff.sub + self.assertEqual(add.to_string(), sub.to_string()) + + def test_identity(self): + lr_diff = vyos.configtree.DiffTree(self.config_left, + self.config_right) + + sub = lr_diff.sub + inter = lr_diff.inter + add = lr_diff.add + + r_union = vyos.configtree.union(add, inter) + l_union = vyos.configtree.union(sub, inter) + + self.assertEqual(r_union.to_string(), + self.config_right.to_string(ordered_values=True)) + self.assertEqual(l_union.to_string(), + self.config_left.to_string(ordered_values=True)) diff --git a/src/tests/test_config_parser.py b/src/tests/test_config_parser.py index 6e0a071f8..8148aa79b 100644 --- a/src/tests/test_config_parser.py +++ b/src/tests/test_config_parser.py @@ -34,8 +34,8 @@ class TestConfigParser(TestCase): def test_top_level_tag(self): self.assertTrue(self.config.exists(["top-level-tag-node"])) - # No sorting is intentional, child order must be preserved - self.assertEqual(self.config.list_nodes(["top-level-tag-node"]), ["foo", "bar"]) + # Sorting is now intentional, during parsing of config + self.assertEqual(self.config.list_nodes(["top-level-tag-node"]), ["bar", "foo"]) def test_copy(self): self.config.copy(["top-level-tag-node", "bar"], ["top-level-tag-node", "baz"]) diff --git a/src/tests/test_configverify.py b/src/tests/test_configverify.py index ad7e053db..6fb43ece2 100644 --- a/src/tests/test_configverify.py +++ b/src/tests/test_configverify.py @@ -27,11 +27,6 @@ class TestDictSearch(TestCase): def test_dh_key_none(self): self.assertFalse(verify_diffie_hellman_length('/tmp/non_existing_file', '1024')) - def test_dh_key_256(self): - key_len = '256' - cmd(f'openssl dhparam -out {dh_file} {key_len}') - self.assertTrue(verify_diffie_hellman_length(dh_file, key_len)) - def test_dh_key_512(self): key_len = '512' cmd(f'openssl dhparam -out {dh_file} {key_len}') diff --git a/src/tests/test_util.py b/src/tests/test_util.py index d8b2b7940..473052bef 100644 --- a/src/tests/test_util.py +++ b/src/tests/test_util.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -26,17 +26,3 @@ class TestVyOSUtil(TestCase): def test_sysctl_read(self): self.assertEqual(sysctl_read('net.ipv4.conf.lo.forwarding'), '1') - - def test_camel_to_snake_case(self): - self.assertEqual(camel_to_snake_case('ConnectionTimeout'), - 'connection_timeout') - self.assertEqual(camel_to_snake_case('connectionTimeout'), - 'connection_timeout') - self.assertEqual(camel_to_snake_case('TCPConnectionTimeout'), - 'tcp_connection_timeout') - self.assertEqual(camel_to_snake_case('TCPPort'), - 'tcp_port') - self.assertEqual(camel_to_snake_case('UseHTTPProxy'), - 'use_http_proxy') - self.assertEqual(camel_to_snake_case('CustomerID'), - 'customer_id') diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol new file mode 100755 index 000000000..6f927927b --- /dev/null +++ b/src/validators/ddclient-protocol @@ -0,0 +1,24 @@ +#!/bin/sh +# +# 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/>. + +ddclient -list-protocols | grep -qw $1 + +if [ $? -gt 0 ]; then + echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols" + exit 1 +fi + +exit 0 diff --git a/src/validators/timezone b/src/validators/timezone index baf5abca2..107571181 100755 --- a/src/validators/timezone +++ b/src/validators/timezone @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019 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 @@ -25,7 +25,7 @@ if __name__ == '__main__': parser.add_argument("--validate", action="store", required=True, help="Check if timezone is valid") args = parser.parse_args() - tz_data = cmd('find /usr/share/zoneinfo/posix -type f -or -type l | sed -e s:/usr/share/zoneinfo/posix/::') + tz_data = cmd('timedatectl list-timezones') tz_data = tz_data.split('\n') if args.validate not in tz_data: diff --git a/src/xdp/common/common_libbpf.c b/src/xdp/common/common_libbpf.c index 5788ecd9e..443ca4c66 100644 --- a/src/xdp/common/common_libbpf.c +++ b/src/xdp/common/common_libbpf.c @@ -24,10 +24,6 @@ static inline bool IS_ERR_OR_NULL(const void *ptr) int bpf_prog_load_xattr_maps(const struct bpf_prog_load_attr_maps *attr, struct bpf_object **pobj, int *prog_fd) { - struct bpf_object_open_attr open_attr = { - .file = attr->file, - .prog_type = attr->prog_type, - }; struct bpf_program *prog, *first_prog = NULL; enum bpf_attach_type expected_attach_type; enum bpf_prog_type prog_type; @@ -41,10 +37,13 @@ int bpf_prog_load_xattr_maps(const struct bpf_prog_load_attr_maps *attr, if (!attr->file) return -EINVAL; + obj = bpf_object__open_file(attr->file, NULL); - obj = bpf_object__open_xattr(&open_attr); - if (IS_ERR_OR_NULL(obj)) - return -ENOENT; + if (libbpf_get_error(obj)) + return -EINVAL; + + prog = bpf_object__next_program(obj, NULL); + bpf_program__set_type(prog, attr->prog_type); bpf_object__for_each_program(prog, obj) { /* @@ -82,7 +81,7 @@ int bpf_prog_load_xattr_maps(const struct bpf_prog_load_attr_maps *attr, bpf_map__for_each(map, obj) { const char* mapname = bpf_map__name(map); - if (!bpf_map__is_offload_neutral(map)) + if (bpf_map__type(map) != BPF_MAP_TYPE_PERF_EVENT_ARRAY) bpf_map__set_ifindex(map, attr->ifindex); /* Was: map->map_ifindex = attr->ifindex; */ diff --git a/src/xdp/common/common_user_bpf_xdp.c b/src/xdp/common/common_user_bpf_xdp.c index faf7f4f91..524f08c9d 100644 --- a/src/xdp/common/common_user_bpf_xdp.c +++ b/src/xdp/common/common_user_bpf_xdp.c @@ -21,7 +21,7 @@ int xdp_link_attach(int ifindex, __u32 xdp_flags, int prog_fd) int err; /* libbpf provide the XDP net_device link-level hook attach helper */ - err = bpf_set_link_xdp_fd(ifindex, prog_fd, xdp_flags); + err = bpf_xdp_attach(ifindex, prog_fd, xdp_flags, NULL); if (err == -EEXIST && !(xdp_flags & XDP_FLAGS_UPDATE_IF_NOEXIST)) { /* Force mode didn't work, probably because a program of the * opposite type is loaded. Let's unload that and try loading @@ -32,9 +32,9 @@ int xdp_link_attach(int ifindex, __u32 xdp_flags, int prog_fd) xdp_flags &= ~XDP_FLAGS_MODES; xdp_flags |= (old_flags & XDP_FLAGS_SKB_MODE) ? XDP_FLAGS_DRV_MODE : XDP_FLAGS_SKB_MODE; - err = bpf_set_link_xdp_fd(ifindex, -1, xdp_flags); + err = bpf_xdp_detach(ifindex, xdp_flags, NULL); if (!err) - err = bpf_set_link_xdp_fd(ifindex, prog_fd, old_flags); + err = bpf_xdp_attach(ifindex, prog_fd, old_flags, NULL); } if (err < 0) { fprintf(stderr, "ERR: " @@ -65,7 +65,7 @@ int xdp_link_detach(int ifindex, __u32 xdp_flags, __u32 expected_prog_id) __u32 curr_prog_id; int err; - err = bpf_get_link_xdp_id(ifindex, &curr_prog_id, xdp_flags); + err = bpf_xdp_query_id(ifindex, xdp_flags, &curr_prog_id); if (err) { fprintf(stderr, "ERR: get link xdp id failed (err=%d): %s\n", -err, strerror(-err)); @@ -86,7 +86,7 @@ int xdp_link_detach(int ifindex, __u32 xdp_flags, __u32 expected_prog_id) return EXIT_FAIL; } - if ((err = bpf_set_link_xdp_fd(ifindex, -1, xdp_flags)) < 0) { + if ((err = bpf_xdp_detach(ifindex, xdp_flags, NULL)) < 0) { fprintf(stderr, "ERR: %s() link set xdp failed (err=%d): %s\n", __func__, err, strerror(-err)); return EXIT_FAIL_XDP; @@ -109,22 +109,28 @@ struct bpf_object *load_bpf_object_file(const char *filename, int ifindex) * hardware offloading XDP programs (note this sets libbpf * bpf_program->prog_ifindex and foreach bpf_map->map_ifindex). */ - struct bpf_prog_load_attr prog_load_attr = { - .prog_type = BPF_PROG_TYPE_XDP, - .ifindex = ifindex, - }; - prog_load_attr.file = filename; + struct bpf_program *prog; + obj = bpf_object__open_file(filename, NULL); + + if (libbpf_get_error(obj)) + return NULL; + + prog = bpf_object__next_program(obj, NULL); + bpf_program__set_type(prog, BPF_PROG_TYPE_XDP); + bpf_program__set_ifindex(prog, ifindex); /* Use libbpf for extracting BPF byte-code from BPF-ELF object, and * loading this into the kernel via bpf-syscall */ - err = bpf_prog_load_xattr(&prog_load_attr, &obj, &first_prog_fd); + err = bpf_object__load(obj); if (err) { fprintf(stderr, "ERR: loading BPF-OBJ file(%s) (%d): %s\n", filename, err, strerror(-err)); return NULL; } + first_prog_fd = bpf_program__fd(prog); + /* Notice how a pointer to a libbpf bpf_object is returned */ return obj; } @@ -136,12 +142,15 @@ static struct bpf_object *open_bpf_object(const char *file, int ifindex) struct bpf_map *map; struct bpf_program *prog, *first_prog = NULL; - struct bpf_object_open_attr open_attr = { - .file = file, - .prog_type = BPF_PROG_TYPE_XDP, - }; + obj = bpf_object__open_file(file, NULL); - obj = bpf_object__open_xattr(&open_attr); + if (libbpf_get_error(obj)) + return NULL; + + prog = bpf_object__next_program(obj, NULL); + bpf_program__set_type(prog, BPF_PROG_TYPE_XDP); + + err = bpf_object__load(obj); if (IS_ERR_OR_NULL(obj)) { err = -PTR_ERR(obj); fprintf(stderr, "ERR: opening BPF-OBJ file(%s) (%d): %s\n", @@ -157,7 +166,7 @@ static struct bpf_object *open_bpf_object(const char *file, int ifindex) } bpf_object__for_each_map(map, obj) { - if (!bpf_map__is_offload_neutral(map)) + if (bpf_map__type(map) != BPF_MAP_TYPE_PERF_EVENT_ARRAY) bpf_map__set_ifindex(map, ifindex); } @@ -264,10 +273,10 @@ struct bpf_object *load_bpf_and_xdp_attach(struct config *cfg) if (cfg->progsec[0]) /* Find a matching BPF prog section name */ - bpf_prog = bpf_object__find_program_by_title(bpf_obj, cfg->progsec); + bpf_prog = bpf_object__find_program_by_name(bpf_obj, cfg->progsec); else /* Find the first program */ - bpf_prog = bpf_program__next(NULL, bpf_obj); + bpf_prog = bpf_object__next_program(bpf_obj, NULL); if (!bpf_prog) { fprintf(stderr, "ERR: couldn't find a program in ELF section '%s'\n", cfg->progsec); diff --git a/src/xdp/common/xdp_stats_kern.h b/src/xdp/common/xdp_stats_kern.h index 4e08551a0..c061a149d 100644 --- a/src/xdp/common/xdp_stats_kern.h +++ b/src/xdp/common/xdp_stats_kern.h @@ -13,12 +13,12 @@ #endif /* Keeps stats per (enum) xdp_action */ -struct bpf_map_def SEC("maps") xdp_stats_map = { - .type = BPF_MAP_TYPE_PERCPU_ARRAY, - .key_size = sizeof(__u32), - .value_size = sizeof(struct datarec), - .max_entries = XDP_ACTION_MAX, -}; +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __type(key, __u32); + __type(value, struct datarec); + __uint(max_entries, XDP_ACTION_MAX); +} xdp_stats_map SEC(".maps"); static __always_inline __u32 xdp_stats_record_action(struct xdp_md *ctx, __u32 action) diff --git a/src/xdp/xdp_prog_kern.c b/src/xdp/xdp_prog_kern.c index a1eb395af..59308325d 100644 --- a/src/xdp/xdp_prog_kern.c +++ b/src/xdp/xdp_prog_kern.c @@ -16,19 +16,19 @@ #define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n)) #endif -struct bpf_map_def SEC("maps") tx_port = { - .type = BPF_MAP_TYPE_DEVMAP, - .key_size = sizeof(int), - .value_size = sizeof(int), - .max_entries = 256, -}; - -struct bpf_map_def SEC("maps") redirect_params = { - .type = BPF_MAP_TYPE_HASH, - .key_size = ETH_ALEN, - .value_size = ETH_ALEN, - .max_entries = 1, -}; +struct { + __uint(type, BPF_MAP_TYPE_DEVMAP); + __type(key, int); + __type(value, int); + __uint(max_entries, 256); +} tx_port SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, ETH_ALEN); + __type(value, ETH_ALEN); + __uint(max_entries, 1); +} redirect_params SEC(".maps"); static __always_inline __u16 csum_fold_helper(__u32 csum) { @@ -208,8 +208,12 @@ out: return xdp_stats_record_action(ctx, action); } +#ifndef AF_INET #define AF_INET 2 +#endif +#ifndef AF_INET6 #define AF_INET6 10 +#endif #define IPV6_FLOWINFO_MASK bpf_htonl(0x0FFFFFFF) /* from include/net/ip.h */ |