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