summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/completion/list_esi.sh20
-rwxr-xr-xsrc/completion/list_vni.sh20
-rwxr-xr-xsrc/conf_mode/container.py11
-rwxr-xr-xsrc/conf_mode/interfaces_bonding.py11
-rwxr-xr-xsrc/conf_mode/interfaces_bridge.py31
-rwxr-xr-xsrc/conf_mode/interfaces_ethernet.py17
-rwxr-xr-xsrc/conf_mode/load-balancing_reverse-proxy.py6
-rwxr-xr-xsrc/conf_mode/nat.py18
-rwxr-xr-xsrc/conf_mode/nat66.py22
-rwxr-xr-xsrc/conf_mode/nat_cgnat.py121
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py48
-rwxr-xr-xsrc/conf_mode/qos.py29
-rwxr-xr-xsrc/conf_mode/service_dhcpv6-server.py8
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py17
-rwxr-xr-xsrc/conf_mode/service_upnp.py157
-rwxr-xr-xsrc/conf_mode/system_host-name.py10
-rwxr-xr-xsrc/conf_mode/vrf.py5
-rw-r--r--src/conf_mode/vrf_vni.py103
-rwxr-xr-xsrc/helpers/vyos_config_sync.py23
-rwxr-xr-xsrc/migration-scripts/nat/7-to-862
-rwxr-xr-xsrc/migration-scripts/pppoe-server/9-to-1056
-rwxr-xr-xsrc/op_mode/bonding.py103
-rwxr-xr-xsrc/op_mode/cgnat.py96
-rw-r--r--src/op_mode/evpn.py46
-rwxr-xr-xsrc/op_mode/firewall.py12
-rwxr-xr-xsrc/op_mode/image_installer.py24
-rwxr-xr-xsrc/op_mode/image_manager.py28
-rwxr-xr-xsrc/op_mode/ipoe-control.py6
-rwxr-xr-xsrc/op_mode/nat.py2
-rw-r--r--src/op_mode/ntp.py164
-rwxr-xr-xsrc/op_mode/version.py6
-rw-r--r--src/systemd/miniupnpd.service13
32 files changed, 888 insertions, 407 deletions
diff --git a/src/completion/list_esi.sh b/src/completion/list_esi.sh
new file mode 100755
index 000000000..b8373fa57
--- /dev/null
+++ b/src/completion/list_esi.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# This script is completion helper to list all valid ESEs that are visible to FRR
+
+esiJson=$(vtysh -c 'show evpn es json')
+echo "$(echo "$esiJson" | jq -r '.[] | .esi')"
diff --git a/src/completion/list_vni.sh b/src/completion/list_vni.sh
new file mode 100755
index 000000000..f8bd4a993
--- /dev/null
+++ b/src/completion/list_vni.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# This script is completion helper to list all configured VNIs that are visible to FRR
+
+vniJson=$(vtysh -c 'show evpn vni json')
+echo "$(echo "$vniJson" | jq -r 'keys | .[]')"
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index a73a18ffa..91a10e891 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -329,9 +329,13 @@ def generate_run_arguments(name, container_config):
prop = vol_config['propagation']
volume += f' --volume {svol}:{dvol}:{mode},{prop}'
+ host_pid = ''
+ if 'allow_host_pid' in container_config:
+ host_pid = '--pid host'
+
container_base_cmd = f'--detach --interactive --tty --replace {capabilities} ' \
f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid}'
+ f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid} {host_pid}'
entrypoint = ''
if 'entrypoint' in container_config:
@@ -339,11 +343,6 @@ def generate_run_arguments(name, container_config):
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()
diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py
index 371b219c0..5e5d5fba1 100755
--- a/src/conf_mode/interfaces_bonding.py
+++ b/src/conf_mode/interfaces_bonding.py
@@ -33,6 +33,7 @@ from vyos.ifconfig import BondIf
from vyos.ifconfig.ethernet import EthernetIf
from vyos.ifconfig import Section
from vyos.template import render_to_string
+from vyos.utils.assertion import assert_mac
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_to_paths_values
from vyos.utils.network import interface_exists
@@ -244,6 +245,16 @@ def verify(bond):
raise ConfigError('primary interface only works for mode active-backup, ' \
'transmit-load-balance or adaptive-load-balance')
+ if 'system_mac' in bond:
+ if bond['mode'] != '802.3ad':
+ raise ConfigError('Actor MAC address only available in 802.3ad mode!')
+
+ system_mac = bond['system_mac']
+ try:
+ assert_mac(system_mac, test_all_zero=False)
+ except:
+ raise ConfigError(f'Cannot use a multicast MAC address "{system_mac}" as system-mac!')
+
return None
def generate(bond):
diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py
index 9789f7bd3..7b2c1ee0b 100755
--- a/src/conf_mode/interfaces_bridge.py
+++ b/src/conf_mode/interfaces_bridge.py
@@ -56,6 +56,17 @@ def get_config(config=None):
bridge['member'].update({'interface_remove' : tmp })
else:
bridge.update({'member' : {'interface_remove' : tmp }})
+ for interface in tmp:
+ # When using VXLAN member interfaces that are configured for Single
+ # VXLAN Device (SVD) we need to call the VXLAN conf-mode script to
+ # re-create VLAN to VNI mappings if required, but only if the interface
+ # is already live on the system - this must not be done on first commit
+ if interface.startswith('vxlan') and interface_exists(interface):
+ set_dependents('vxlan', conf, interface)
+ # When using Wireless member interfaces we need to inform hostapd
+ # to properly set-up the bridge
+ elif interface.startswith('wlan') and interface_exists(interface):
+ set_dependents('wlan', conf, interface)
if dict_search('member.interface', bridge) is not None:
for interface in list(bridge['member']['interface']):
@@ -91,6 +102,10 @@ def get_config(config=None):
# is already live on the system - this must not be done on first commit
if interface.startswith('vxlan') and interface_exists(interface):
set_dependents('vxlan', conf, interface)
+ # When using Wireless member interfaces we need to inform hostapd
+ # to properly set-up the bridge
+ elif interface.startswith('wlan') and interface_exists(interface):
+ set_dependents('wlan', conf, interface)
# delete empty dictionary keys - no need to run code paths if nothing is there to do
if 'member' in bridge:
@@ -140,9 +155,6 @@ def verify(bridge):
if 'enable_vlan' in bridge:
if 'has_vlan' in interface_config:
raise ConfigError(error_msg + 'it has VLAN subinterface(s) assigned!')
-
- if 'wlan' in interface:
- raise ConfigError(error_msg + 'VLAN aware cannot be set!')
else:
for option in ['allowed_vlan', 'native_vlan']:
if option in interface_config:
@@ -168,12 +180,19 @@ def apply(bridge):
else:
br.update(bridge)
- for interface in dict_search('member.interface', bridge) or []:
- if interface.startswith('vxlan') and interface_exists(interface):
+ tmp = []
+ if 'member' in bridge:
+ if 'interface_remove' in bridge['member']:
+ tmp.extend(bridge['member']['interface_remove'])
+ if 'interface' in bridge['member']:
+ tmp.extend(bridge['member']['interface'])
+
+ for interface in tmp:
+ if interface.startswith(tuple(['vxlan', 'wlan'])) and interface_exists(interface):
try:
call_dependents()
except ConfigError:
- raise ConfigError('Error in updating VXLAN interface after changing bridge!')
+ raise ConfigError('Error updating member interface configuration after changing bridge!')
return None
diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py
index 6da7e6a69..54d0669cb 100755
--- a/src/conf_mode/interfaces_ethernet.py
+++ b/src/conf_mode/interfaces_ethernet.py
@@ -41,6 +41,7 @@ from vyos.pki import encode_certificate
from vyos.pki import load_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
+from vyos.template import render_to_string
from vyos.utils.process import call
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_to_paths_values
@@ -48,6 +49,7 @@ from vyos.utils.dict import dict_set
from vyos.utils.dict import dict_delete
from vyos.utils.file import write_file
from vyos import ConfigError
+from vyos import frr
from vyos import airbag
airbag.enable()
@@ -389,6 +391,10 @@ def generate(ethernet):
write_file(ca_cert_file_path, '\n'.join(ca_chains))
+ ethernet['frr_zebra_config'] = ''
+ if 'deleted' not in ethernet:
+ ethernet['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', ethernet)
+
return None
def apply(ethernet):
@@ -407,6 +413,17 @@ def apply(ethernet):
call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}')
+ zebra_daemon = 'zebra'
+ # Save original configuration prior to starting any commit actions
+ frr_cfg = frr.FRRConfig()
+
+ # The route-map used for the FIB (zebra) is part of the zebra daemon
+ frr_cfg.load_configuration(zebra_daemon)
+ frr_cfg.modify_section(f'^interface {ifname}', stop_pattern='^exit', remove_stop_mark=True)
+ if 'frr_zebra_config' in ethernet:
+ frr_cfg.add_before(frr.default_add_before, ethernet['frr_zebra_config'])
+ frr_cfg.commit_configuration(zebra_daemon)
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py
index 1569d8d71..a4efb1cd8 100755
--- a/src/conf_mode/load-balancing_reverse-proxy.py
+++ b/src/conf_mode/load-balancing_reverse-proxy.py
@@ -88,6 +88,12 @@ def verify(lb):
if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf):
raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"')
+ # Check if http-response-headers are configured in any frontend/backend where mode != http
+ for group in ['service', 'backend']:
+ for config_name, config in lb[group].items():
+ if 'http_response_headers' in config and ('mode' not in config or config['mode'] != 'http'):
+ raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!')
+
if 'ssl' in back_config:
if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']):
raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!')
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 4cd9b570d..f74bb217e 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -17,7 +17,6 @@
import os
from sys import exit
-from netifaces import interfaces
from vyos.base import Warning
from vyos.config import Config
@@ -30,6 +29,7 @@ from vyos.utils.dict import dict_search_args
from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.utils.network import is_addr_assigned
+from vyos.utils.network import interface_exists
from vyos import ConfigError
from vyos import airbag
@@ -149,8 +149,12 @@ def verify(nat):
if 'name' in config['outbound_interface'] and 'group' in config['outbound_interface']:
raise ConfigError(f'{err_msg} cannot specify both interface group and interface name for nat source rule "{rule}"')
elif 'name' in config['outbound_interface']:
- if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces():
- Warning(f'NAT interface "{config["outbound_interface"]["name"]}" for source NAT rule "{rule}" does not exist!')
+ interface_name = config['outbound_interface']['name']
+ if interface_name not in 'any':
+ if interface_name.startswith('!'):
+ interface_name = interface_name[1:]
+ if not interface_exists(interface_name):
+ Warning(f'Interface "{interface_name}" for source NAT rule "{rule}" does not exist!')
else:
group_name = config['outbound_interface']['group']
if group_name[0] == '!':
@@ -182,8 +186,12 @@ def verify(nat):
if 'name' in config['inbound_interface'] and 'group' in config['inbound_interface']:
raise ConfigError(f'{err_msg} cannot specify both interface group and interface name for destination nat rule "{rule}"')
elif 'name' in config['inbound_interface']:
- if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces():
- Warning(f'NAT interface "{config["inbound_interface"]["name"]}" for destination NAT rule "{rule}" does not exist!')
+ interface_name = config['inbound_interface']['name']
+ if interface_name not in 'any':
+ if interface_name.startswith('!'):
+ interface_name = interface_name[1:]
+ if not interface_exists(interface_name):
+ Warning(f'Interface "{interface_name}" for destination NAT rule "{rule}" does not exist!')
else:
group_name = config['inbound_interface']['group']
if group_name[0] == '!':
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index fe017527d..075738dad 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -17,15 +17,15 @@
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.template import render
-from vyos.utils.process import cmd
-from vyos.utils.kernel import check_kmod
from vyos.utils.dict import dict_search
+from vyos.utils.kernel import check_kmod
+from vyos.utils.network import interface_exists
+from vyos.utils.process import cmd
from vyos.template import is_ipv6
from vyos import ConfigError
from vyos import airbag
@@ -64,8 +64,12 @@ def verify(nat):
if 'name' in config['outbound_interface'] and 'group' in config['outbound_interface']:
raise ConfigError(f'{err_msg} cannot specify both interface group and interface name for nat source rule "{rule}"')
elif 'name' in config['outbound_interface']:
- if config['outbound_interface']['name'] not in 'any' and config['outbound_interface']['name'] not in interfaces():
- Warning(f'NAT66 interface "{config["outbound_interface"]["name"]}" for source NAT66 rule "{rule}" does not exist!')
+ interface_name = config['outbound_interface']['name']
+ if interface_name not in 'any':
+ if interface_name.startswith('!'):
+ interface_name = interface_name[1:]
+ if not interface_exists(interface_name):
+ Warning(f'Interface "{interface_name}" for source NAT66 rule "{rule}" does not exist!')
addr = dict_search('translation.address', config)
if addr != None:
@@ -88,8 +92,12 @@ def verify(nat):
if 'name' in config['inbound_interface'] and 'group' in config['inbound_interface']:
raise ConfigError(f'{err_msg} cannot specify both interface group and interface name for destination nat rule "{rule}"')
elif 'name' in config['inbound_interface']:
- if config['inbound_interface']['name'] not in 'any' and config['inbound_interface']['name'] not in interfaces():
- Warning(f'NAT66 interface "{config["inbound_interface"]["name"]}" for destination NAT66 rule "{rule}" does not exist!')
+ interface_name = config['inbound_interface']['name']
+ if interface_name not in 'any':
+ if interface_name.startswith('!'):
+ interface_name = interface_name[1:]
+ if not interface_exists(interface_name):
+ Warning(f'Interface "{interface_name}" for destination NAT66 rule "{rule}" does not exist!')
return None
diff --git a/src/conf_mode/nat_cgnat.py b/src/conf_mode/nat_cgnat.py
index f41d66c66..5ad65de80 100755
--- a/src/conf_mode/nat_cgnat.py
+++ b/src/conf_mode/nat_cgnat.py
@@ -189,11 +189,6 @@ def verify(config):
if 'rule' not in config:
raise ConfigError(f'Rule must be defined!')
- # As PoC allow only one rule for CGNAT translations
- # one internal pool and one external pool
- if len(config['rule']) > 1:
- raise ConfigError(f'Only one rule is allowed for translations!')
-
for pool in ('external', 'internal'):
if pool not in config['pool']:
raise ConfigError(f'{pool} pool must be defined!')
@@ -203,6 +198,13 @@ def verify(config):
f'Range for "{pool} pool {pool_name}" must be defined!'
)
+ external_pools_query = "keys(pool.external)"
+ external_pools: list = jmespath.search(external_pools_query, config)
+ internal_pools_query = "keys(pool.internal)"
+ internal_pools: list = jmespath.search(internal_pools_query, config)
+
+ used_external_pools = {}
+ used_internal_pools = {}
for rule, rule_config in config['rule'].items():
if 'source' not in rule_config:
raise ConfigError(f'Rule "{rule}" source pool must be defined!')
@@ -212,49 +214,82 @@ def verify(config):
if 'translation' not in rule_config:
raise ConfigError(f'Rule "{rule}" translation pool must be defined!')
+ # Check if pool exists
+ internal_pool = rule_config['source']['pool']
+ if internal_pool not in internal_pools:
+ raise ConfigError(f'Internal pool "{internal_pool}" does not exist!')
+ external_pool = rule_config['translation']['pool']
+ if external_pool not in external_pools:
+ raise ConfigError(f'External pool "{external_pool}" does not exist!')
+
+ # Check pool duplication in different rules
+ if external_pool in used_external_pools:
+ raise ConfigError(
+ f'External pool "{external_pool}" is already used in rule '
+ f'{used_external_pools[external_pool]} and cannot be used in '
+ f'rule {rule}!'
+ )
+
+ if internal_pool in used_internal_pools:
+ raise ConfigError(
+ f'Internal pool "{internal_pool}" is already used in rule '
+ f'{used_internal_pools[internal_pool]} and cannot be used in '
+ f'rule {rule}!'
+ )
+
+ used_external_pools[external_pool] = rule
+ used_internal_pools[internal_pool] = rule
+
def generate(config):
if not config:
return None
- # first external pool as we allow only one as PoC
- ext_pool_name = jmespath.search("rule.*.translation | [0]", config).get('pool')
- int_pool_name = jmespath.search("rule.*.source | [0]", config).get('pool')
- ext_query = f"pool.external.{ext_pool_name}.range | keys(@)"
- int_query = f"pool.internal.{int_pool_name}.range"
- external_ranges = jmespath.search(ext_query, config)
- internal_ranges = [jmespath.search(int_query, config)]
-
- external_list_count = []
- external_list_hosts = []
- internal_list_count = []
- internal_list_hosts = []
- for ext_range in external_ranges:
- # External hosts count
- e_count = IPOperations(ext_range).get_ips_count()
- external_list_count.append(e_count)
- # External hosts list
- e_hosts = IPOperations(ext_range).convert_prefix_to_list_ips()
- external_list_hosts.extend(e_hosts)
- for int_range in internal_ranges:
- # Internal hosts count
- i_count = IPOperations(int_range).get_ips_count()
- internal_list_count.append(i_count)
- # Internal hosts list
- i_hosts = IPOperations(int_range).convert_prefix_to_list_ips()
- internal_list_hosts.extend(i_hosts)
-
- external_host_count = sum(external_list_count)
- internal_host_count = sum(internal_list_count)
- ports_per_user = int(
- jmespath.search(f'pool.external.{ext_pool_name}.per_user_limit.port', config)
- )
- external_port_range: str = jmespath.search(
- f'pool.external.{ext_pool_name}.external_port_range', config
- )
- proto_maps, other_maps = generate_port_rules(
- external_list_hosts, internal_list_hosts, ports_per_user, external_port_range
- )
+ proto_maps = []
+ other_maps = []
+
+ for rule, rule_config in config['rule'].items():
+ ext_pool_name: str = rule_config['translation']['pool']
+ int_pool_name: str = rule_config['source']['pool']
+
+ external_ranges: list = [range for range in config['pool']['external'][ext_pool_name]['range']]
+ internal_ranges: list = [range for range in config['pool']['internal'][int_pool_name]['range']]
+ external_list_hosts_count = []
+ external_list_hosts = []
+ internal_list_hosts_count = []
+ internal_list_hosts = []
+
+ for ext_range in external_ranges:
+ # External hosts count
+ e_count = IPOperations(ext_range).get_ips_count()
+ external_list_hosts_count.append(e_count)
+ # External hosts list
+ e_hosts = IPOperations(ext_range).convert_prefix_to_list_ips()
+ external_list_hosts.extend(e_hosts)
+
+ for int_range in internal_ranges:
+ # Internal hosts count
+ i_count = IPOperations(int_range).get_ips_count()
+ internal_list_hosts_count.append(i_count)
+ # Internal hosts list
+ i_hosts = IPOperations(int_range).convert_prefix_to_list_ips()
+ internal_list_hosts.extend(i_hosts)
+
+ external_host_count = sum(external_list_hosts_count)
+ internal_host_count = sum(internal_list_hosts_count)
+ ports_per_user = int(
+ jmespath.search(f'pool.external."{ext_pool_name}".per_user_limit.port', config)
+ )
+ external_port_range: str = jmespath.search(
+ f'pool.external."{ext_pool_name}".external_port_range', config
+ )
+
+ rule_proto_maps, rule_other_maps = generate_port_rules(
+ external_list_hosts, internal_list_hosts, ports_per_user, external_port_range
+ )
+
+ proto_maps.extend(rule_proto_maps)
+ other_maps.extend(rule_other_maps)
config['proto_map_elements'] = ', '.join(proto_maps)
config['other_map_elements'] = ', '.join(other_maps)
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index 4df97d133..22f020099 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -31,6 +31,7 @@ from vyos.utils.dict import dict_search
from vyos.utils.network import get_interface_vrf
from vyos.utils.network import is_addr_assigned
from vyos.utils.process import process_named_running
+from vyos.utils.process import call
from vyos import ConfigError
from vyos import frr
from vyos import airbag
@@ -50,13 +51,8 @@ def get_config(config=None):
# eqivalent of the C foo ? 'a' : 'b' statement
base = vrf and ['vrf', 'name', vrf, 'protocols', 'bgp'] or base_path
- bgp = conf.get_config_dict(
- base,
- key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True,
- with_recursive_defaults=True,
- )
+ bgp = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'],
key_mangling=('-', '_'),
@@ -75,22 +71,29 @@ def get_config(config=None):
if vrf:
bgp.update({'vrf' : vrf})
# We can not delete the BGP VRF instance if there is a L3VNI configured
+ # FRR L3VNI must be deleted first otherwise we will see error:
+ # "FRR error: Please unconfigure l3vni 3000"
tmp = ['vrf', 'name', vrf, 'vni']
- if conf.exists(tmp):
- bgp.update({'vni' : conf.return_value(tmp)})
+ if conf.exists_effective(tmp):
+ bgp.update({'vni' : conf.return_effective_value(tmp)})
# We can safely delete ourself from the dependent vrf list
if vrf in bgp['dependent_vrfs']:
del bgp['dependent_vrfs'][vrf]
- bgp['dependent_vrfs'].update({'default': {'protocols': {
- 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)}}})
+ bgp['dependent_vrfs'].update({'default': {'protocols': {
+ 'bgp': conf.get_config_dict(base_path, key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True)}}})
+
if not conf.exists(base):
# If bgp instance is deleted then mark it
bgp.update({'deleted' : ''})
return bgp
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ bgp = conf.merge_defaults(bgp, recursive=True)
+
# We also need some additional information from the config, prefix-lists
# and route-maps for instance. They will be used in verify().
#
@@ -242,10 +245,6 @@ def verify(bgp):
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.
@@ -254,7 +253,11 @@ def verify(bgp):
if vrf != 'default':
if dict_search('protocols.bgp', vrf_options):
raise ConfigError('Cannot delete default BGP instance, ' \
- 'dependent VRF instance(s) exist!')
+ 'dependent VRF instance(s) exist(s)!')
+ if 'vni' in vrf_options:
+ raise ConfigError('Cannot delete default BGP instance, ' \
+ 'dependent L3VNI exists!')
+
return None
if 'system_as' not in bgp:
@@ -330,7 +333,7 @@ def verify(bgp):
raise ConfigError('Cannot have local-as same as system-as number')
# Neighbor AS specified for local-as and remote-as can not be the same
- if dict_search('remote_as', peer_config) == asn:
+ if dict_search('remote_as', peer_config) == asn and neighbor != 'peer_group':
raise ConfigError(f'Neighbor "{peer}" has local-as specified which is '\
'the same as remote-as, this is not allowed!')
@@ -607,6 +610,13 @@ def generate(bgp):
return None
def apply(bgp):
+ if 'deleted' in bgp:
+ # We need to ensure that the L3VNI is deleted first.
+ # This is not possible with old config backend
+ # priority bug
+ if {'vrf', 'vni'} <= set(bgp):
+ call('vtysh -c "conf t" -c "vrf {vrf}" -c "no vni {vni}"'.format(**bgp))
+
bgp_daemon = 'bgpd'
# Save original configuration prior to starting any commit actions
diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py
index ccfc8f6b8..8a590cbc6 100755
--- a/src/conf_mode/qos.py
+++ b/src/conf_mode/qos.py
@@ -39,6 +39,9 @@ from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import run
from vyos import ConfigError
from vyos import airbag
+from vyos.xml_ref import relative_defaults
+
+
airbag.enable()
map_vyops_tc = {
@@ -115,8 +118,18 @@ def get_config(config=None):
for rd_name in list(qos['policy'][policy]):
# There are eight precedence levels - ensure all are present
# to be filled later down with the appropriate default values
- default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {},
- '4' : {}, '5' : {}, '6' : {}, '7' : {} }}
+ default_p_val = relative_defaults(
+ ['qos', 'policy', 'random-detect', rd_name, 'precedence'],
+ {'precedence': {'0': {}}},
+ get_first_key=True, recursive=True
+ )['0']
+ default_p_val = {key.replace('-', '_'): value for key, value in default_p_val.items()}
+ default_precedence = {
+ 'precedence': {'0': default_p_val, '1': default_p_val,
+ '2': default_p_val, '3': default_p_val,
+ '4': default_p_val, '5': default_p_val,
+ '6': default_p_val, '7': default_p_val}}
+
qos['policy']['random_detect'][rd_name] = dict_merge(
default_precedence, qos['policy']['random_detect'][rd_name])
@@ -124,18 +137,6 @@ def get_config(config=None):
for policy in qos.get('policy', []):
for p_name, p_config in qos['policy'][policy].items():
- if 'precedence' in p_config:
- # precedence settings are a bit more complex as they are
- # calculated under specific circumstances:
- for precedence in p_config['precedence']:
- max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold'])
- if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]:
- qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str(
- int((9 + int(precedence)) * max_thr) // 18);
-
- if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]:
- qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \
- str(int(4 * max_thr))
# cleanup empty match config
if 'class' in p_config:
for cls, cls_config in p_config['class'].items():
diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py
index c7333dd3a..7af88007c 100755
--- a/src/conf_mode/service_dhcpv6-server.py
+++ b/src/conf_mode/service_dhcpv6-server.py
@@ -106,14 +106,14 @@ def verify(dhcpv6):
# Stop address must be greater or equal to start address
if not ip_address(stop) >= ip_address(start):
- raise ConfigError(f'Range stop address "{stop}" must be greater then or equal ' \
+ raise ConfigError(f'Range stop address "{stop}" must be greater than or equal ' \
f'to the range start address "{start}"!')
# DHCPv6 range start address must be unique - two ranges can't
# start with the same address - makes no sense
if start in range6_start:
raise ConfigError(f'Conflicting DHCPv6 lease range: '\
- f'Pool start address "{start}" defined multipe times!')
+ f'Pool start address "{start}" defined multiple times!')
range6_start.append(start)
@@ -121,7 +121,7 @@ def verify(dhcpv6):
# end with the same address - makes no sense
if stop in range6_stop:
raise ConfigError(f'Conflicting DHCPv6 lease range: '\
- f'Pool stop address "{stop}" defined multipe times!')
+ f'Pool stop address "{stop}" defined multiple times!')
range6_stop.append(stop)
@@ -180,7 +180,7 @@ def verify(dhcpv6):
if 'option' in subnet_config:
if 'vendor_option' in subnet_config['option']:
if len(dict_search('option.vendor_option.cisco.tftp_server', subnet_config)) > 2:
- raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!')
+ raise ConfigError(f'No more than two Cisco tftp-servers should be defined for subnet "{subnet}"!')
# Subnets must be unique
if subnet in subnets:
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 328487985..c95f976d3 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -84,12 +84,29 @@ def verify_pado_delay(pppoe):
pado_delay = pppoe['pado_delay']
delays_without_sessions = pado_delay['delays_without_sessions']
+ if 'disable' in delays_without_sessions:
+ raise ConfigError(
+ 'Number of sessions must be specified for "pado-delay disable"'
+ )
+
if len(delays_without_sessions) > 1:
raise ConfigError(
f'Cannot add more then ONE pado-delay without sessions, '
f'but {len(delays_without_sessions)} were set'
)
+ if 'disable' in [delay[0] for delay in pado_delay['delays_with_sessions']]:
+ # need to sort delays by sessions to verify if there is no delay
+ # for sessions after disabling
+ sorted_pado_delay = sorted(pado_delay['delays_with_sessions'], key=lambda k_v: k_v[1])
+ last_delay = sorted_pado_delay[-1]
+
+ if last_delay[0] != 'disable':
+ raise ConfigError(
+ f'Cannot add pado-delay after disabled sessions, but '
+ f'"pado-delay {last_delay[0]} sessions {last_delay[1]}" was set'
+ )
+
def verify(pppoe):
if not pppoe:
return None
diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py
deleted file mode 100755
index 0df8dc09e..000000000
--- a/src/conf_mode/service_upnp.py
+++ /dev/null
@@ -1,157 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2021-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
-# 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
-import uuid
-import netifaces
-from ipaddress import IPv4Network
-from ipaddress import IPv6Network
-
-from vyos.config import Config
-from vyos.utils.process import call
-from vyos.template import render
-from vyos.template import is_ipv4
-from vyos.template import is_ipv6
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/upnp/miniupnp.conf'
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
-
- base = ['service', 'upnp']
- upnpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
-
- if not upnpd:
- return None
-
- upnpd = conf.merge_defaults(upnpd, recursive=True)
-
- uuidgen = uuid.uuid1()
- upnpd.update({'uuid': uuidgen})
-
- return upnpd
-
-def get_all_interface_addr(prefix, filter_dev, filter_family):
- list_addr = []
- for interface in netifaces.interfaces():
- if filter_dev and interface in filter_dev:
- continue
- addrs = netifaces.ifaddresses(interface)
- if netifaces.AF_INET in addrs.keys():
- if netifaces.AF_INET in filter_family:
- for addr in addrs[netifaces.AF_INET]:
- if prefix:
- # we need to manually assemble a list of IPv4 address/prefix
- prefix = '/' + \
- str(IPv4Network('0.0.0.0/' + addr['netmask']).prefixlen)
- list_addr.append(addr['addr'] + prefix)
- else:
- list_addr.append(addr['addr'])
- if netifaces.AF_INET6 in addrs.keys():
- if netifaces.AF_INET6 in filter_family:
- for addr in addrs[netifaces.AF_INET6]:
- if prefix:
- # we need to manually assemble a list of IPv4 address/prefix
- bits = bin(int(addr['netmask'].replace(':', '').split('/')[0], 16)).count('1')
- prefix = '/' + str(bits)
- list_addr.append(addr['addr'] + prefix)
- else:
- list_addr.append(addr['addr'])
-
- return list_addr
-
-def verify(upnpd):
- if not upnpd:
- return None
-
- if 'wan_interface' not in upnpd:
- raise ConfigError('To enable UPNP, you must have the "wan-interface" option!')
-
- if 'rule' in upnpd:
- for rule, rule_config in upnpd['rule'].items():
- for option in ['external_port_range', 'internal_port_range', 'ip', 'action']:
- if option not in rule_config:
- tmp = option.replace('_', '-')
- raise ConfigError(f'Every UPNP rule requires "{tmp}" to be set!')
-
- if 'stun' in upnpd:
- for option in ['host', 'port']:
- if option not in upnpd['stun']:
- raise ConfigError(f'A UPNP stun support must have an "{option}" option!')
-
- # Check the validity of the IP address
- listen_dev = []
- system_addrs_cidr = get_all_interface_addr(True, [], [netifaces.AF_INET, netifaces.AF_INET6])
- system_addrs = get_all_interface_addr(False, [], [netifaces.AF_INET, netifaces.AF_INET6])
- if 'listen' not in upnpd:
- raise ConfigError(f'Listen address or interface is required!')
- for listen_if_or_addr in upnpd['listen']:
- if listen_if_or_addr not in netifaces.interfaces():
- listen_dev.append(listen_if_or_addr)
- if (listen_if_or_addr not in system_addrs) and (listen_if_or_addr not in system_addrs_cidr) and \
- (listen_if_or_addr not in netifaces.interfaces()):
- if is_ipv4(listen_if_or_addr) and IPv4Network(listen_if_or_addr).is_multicast:
- raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed'
- f'to listen on. It is not an interface address nor a multicast address!')
- if is_ipv6(listen_if_or_addr) and IPv6Network(listen_if_or_addr).is_multicast:
- raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed'
- f'to listen on. It is not an interface address nor a multicast address!')
-
- system_listening_dev_addrs_cidr = get_all_interface_addr(True, listen_dev, [netifaces.AF_INET6])
- system_listening_dev_addrs = get_all_interface_addr(False, listen_dev, [netifaces.AF_INET6])
- for listen_if_or_addr in upnpd['listen']:
- if listen_if_or_addr not in netifaces.interfaces() and \
- (listen_if_or_addr not in system_listening_dev_addrs_cidr) and \
- (listen_if_or_addr not in system_listening_dev_addrs) and \
- is_ipv6(listen_if_or_addr) and \
- (not IPv6Network(listen_if_or_addr).is_multicast):
- raise ConfigError(f'{listen_if_or_addr} must listen on the interface of the network card')
-
-def generate(upnpd):
- if not upnpd:
- return None
-
- if os.path.isfile(config_file):
- os.unlink(config_file)
-
- render(config_file, 'firewall/upnpd.conf.j2', upnpd)
-
-def apply(upnpd):
- systemd_service_name = 'miniupnpd.service'
- if not upnpd:
- # Stop the UPNP service
- call(f'systemctl stop {systemd_service_name}')
- else:
- # Start the UPNP service
- call(f'systemctl restart {systemd_service_name}')
-
-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/system_host-name.py b/src/conf_mode/system_host-name.py
index 8975cadb6..3f245f166 100755
--- a/src/conf_mode/system_host-name.py
+++ b/src/conf_mode/system_host-name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2023 VyOS maintainers and contributors
+# Copyright (C) 2018-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -22,6 +22,7 @@ import vyos.hostsd_client
from vyos.base import Warning
from vyos.config import Config
+from vyos.configdict import leaf_node_changed
from vyos.ifconfig import Section
from vyos.template import is_ip
from vyos.utils.process import cmd
@@ -37,6 +38,7 @@ default_config_data = {
'domain_search': [],
'nameserver': [],
'nameservers_dhcp_interfaces': {},
+ 'snmpd_restart_reqired': False,
'static_host_mapping': {}
}
@@ -52,6 +54,10 @@ def get_config(config=None):
hosts['hostname'] = conf.return_value(['system', 'host-name'])
+ base = ['system']
+ if leaf_node_changed(conf, base + ['host-name']) or leaf_node_changed(conf, base + ['domain-name']):
+ hosts['snmpd_restart_reqired'] = True
+
# This may happen if the config is not loaded yet,
# e.g. if run by cloud-init
if not hosts['hostname']:
@@ -171,7 +177,7 @@ def apply(config):
call("systemctl restart rsyslog.service")
# If SNMP is running, restart it too
- if process_named_running('snmpd'):
+ if process_named_running('snmpd') and config['snmpd_restart_reqired']:
call('systemctl restart snmpd.service')
return None
diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py
index 587309005..8d8c234c0 100755
--- a/src/conf_mode/vrf.py
+++ b/src/conf_mode/vrf.py
@@ -130,11 +130,6 @@ def get_config(config=None):
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
diff --git a/src/conf_mode/vrf_vni.py b/src/conf_mode/vrf_vni.py
deleted file mode 100644
index 8dab164d7..000000000
--- a/src/conf_mode/vrf_vni.py
+++ /dev/null
@@ -1,103 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-from sys import argv
-from sys import exit
-
-from vyos.config import Config
-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()
-
- 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):
- 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)
- # 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)
-
- 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/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py
index 0604b2837..9d9aec376 100755
--- a/src/helpers/vyos_config_sync.py
+++ b/src/helpers/vyos_config_sync.py
@@ -93,7 +93,8 @@ def set_remote_config(
key: str,
op: str,
mask: Dict[str, Any],
- config: Dict[str, Any]) -> Optional[Dict[str, Any]]:
+ config: Dict[str, Any],
+ port: int) -> Optional[Dict[str, Any]]:
"""Loads the VyOS configuration in JSON format to a remote host.
Args:
@@ -102,6 +103,7 @@ def set_remote_config(
op (str): The operation to perform (set or load).
mask (dict): The dict of paths in sections.
config (dict): The dict of masked config data.
+ port (int): The remote API port
Returns:
Optional[Dict[str, Any]]: The response from the remote host as a
@@ -113,7 +115,7 @@ def set_remote_config(
# Disable the InsecureRequestWarning
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
- url = f'https://{address}/configure-section'
+ url = f'https://{address}:{port}/configure-section'
data = json.dumps({
'op': op,
'mask': mask,
@@ -138,7 +140,8 @@ def is_section_revised(section: List[str]) -> bool:
def config_sync(secondary_address: str,
secondary_key: str,
sections: List[list[str]],
- mode: str):
+ mode: str,
+ secondary_port: int):
"""Retrieve a config section from primary router in JSON format and send it to
secondary router
"""
@@ -158,7 +161,8 @@ def config_sync(secondary_address: str,
key=secondary_key,
op=mode,
mask=mask_dict,
- config=config_dict)
+ config=config_dict,
+ port=secondary_port)
logger.debug(f"Set config for sections '{sections}': {set_config}")
@@ -178,14 +182,12 @@ if __name__ == '__main__':
secondary_address = config.get('secondary', {}).get('address')
secondary_address = bracketize_ipv6(secondary_address)
secondary_key = config.get('secondary', {}).get('key')
+ secondary_port = int(config.get('secondary', {}).get('port', 443))
sections = config.get('section')
timeout = int(config.get('secondary', {}).get('timeout'))
- if not all([
- mode, secondary_address, secondary_key, sections
- ]):
- logger.error(
- "Missing required configuration data for config synchronization.")
+ if not all([mode, secondary_address, secondary_key, sections]):
+ logger.error("Missing required configuration data for config synchronization.")
exit(0)
# Generate list_sections of sections/subsections
@@ -200,5 +202,4 @@ if __name__ == '__main__':
else:
list_sections.append([section])
- config_sync(secondary_address, secondary_key,
- list_sections, mode)
+ config_sync(secondary_address, secondary_key, list_sections, mode, secondary_port)
diff --git a/src/migration-scripts/nat/7-to-8 b/src/migration-scripts/nat/7-to-8
new file mode 100755
index 000000000..ab2ffa6d3
--- /dev/null
+++ b/src/migration-scripts/nat/7-to-8
@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# T6345: random - In kernel 5.0 and newer this is the same as fully-random.
+# In earlier kernels the port mapping will be randomized using a seeded
+# MD5 hash mix using source and destination address and destination port.
+# drop fully-random from CLI
+
+from sys import argv,exit
+from vyos.configtree import ConfigTree
+
+if len(argv) < 2:
+ 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)
+
+if not config.exists(['nat']):
+ # Nothing to do
+ exit(0)
+
+for direction in ['source', 'destination']:
+ # If a node doesn't exist, we obviously have nothing to do.
+ if not config.exists(['nat', direction]):
+ continue
+
+ # However, we also need to handle the case when a 'source' or 'destination' sub-node does exist,
+ # but there are no rules under it.
+ if not config.list_nodes(['nat', direction]):
+ continue
+
+ for rule in config.list_nodes(['nat', direction, 'rule']):
+ port_mapping = ['nat', direction, 'rule', rule, 'translation', 'options', 'port-mapping']
+ if config.exists(port_mapping):
+ tmp = config.return_value(port_mapping)
+ if tmp == 'fully-random':
+ config.set(port_mapping, value='random')
+
+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/pppoe-server/9-to-10 b/src/migration-scripts/pppoe-server/9-to-10
new file mode 100755
index 000000000..e0c782f04
--- /dev/null
+++ b/src/migration-scripts/pppoe-server/9-to-10
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Migration of pado-delay options
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+
+if len(argv) < 2:
+ 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)
+base = ['service', 'pppoe-server', 'pado-delay']
+if not config.exists(base):
+ exit(0)
+
+pado_delay = {}
+for delay in config.list_nodes(base):
+ sessions = config.return_value(base + [delay, 'sessions'])
+ pado_delay[delay] = sessions
+
+# need to define delay for latest sessions
+sorted_delays = dict(sorted(pado_delay.items(), key=lambda k_v: int(k_v[1])))
+last_delay = list(sorted_delays)[-1]
+
+# Rename last delay -> disable
+tmp = base + [last_delay]
+if config.exists(tmp):
+ config.rename(tmp, 'disable')
+
+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)
diff --git a/src/op_mode/bonding.py b/src/op_mode/bonding.py
new file mode 100755
index 000000000..07bccbd4b
--- /dev/null
+++ b/src/op_mode/bonding.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016-2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# This script will parse 'sudo cat /proc/net/bonding/<interface name>' and return table output for lacp related info
+
+import subprocess
+import re
+import sys
+import typing
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+
+def list_to_dict(data, headers, basekey):
+ data_list = {basekey: []}
+
+ for row in data:
+ row_dict = {headers[i]: row[i] for i in range(len(headers))}
+ data_list[basekey].append(row_dict)
+
+ return data_list
+
+def show_lacp_neighbors(raw: bool, interface: typing.Optional[str]):
+ headers = ["Interface", "Member", "Local ID", "Remote ID"]
+ data = subprocess.run(f"cat /proc/net/bonding/{interface}", stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True, text=False).stdout.decode('utf-8')
+ if 'Bonding Mode: IEEE 802.3ad Dynamic link aggregation' not in data:
+ raise vyos.opmode.DataUnavailable(f"{interface} is not present or not configured with mode 802.3ad")
+
+ pattern = re.compile(
+ r"Slave Interface: (?P<member>\w+\d+).*?"
+ r"system mac address: (?P<local_id>[0-9a-f:]+).*?"
+ r"details partner lacp pdu:.*?"
+ r"system mac address: (?P<remote_id>[0-9a-f:]+)",
+ re.DOTALL
+ )
+
+ interfaces = []
+
+ for match in re.finditer(pattern, data):
+ member = match.group("member")
+ local_id = match.group("local_id")
+ remote_id = match.group("remote_id")
+ interfaces.append([interface, member, local_id, remote_id])
+
+ if raw:
+ return list_to_dict(interfaces, headers, 'lacp')
+ else:
+ return tabulate(interfaces, headers)
+
+def show_lacp_detail(raw: bool, interface: typing.Optional[str]):
+ headers = ["Interface", "Members", "Mode", "Rate", "System-MAC", "Hash"]
+ query = ConfigTreeQuery()
+
+ if interface:
+ intList = [interface]
+ else:
+ intList = query.list_nodes(['interfaces', 'bonding'])
+
+ bondList = []
+
+ for interface in intList:
+ data = subprocess.run(f"cat /proc/net/bonding/{interface}", stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, shell=True, text=False).stdout.decode('utf-8')
+ if 'Bonding Mode: IEEE 802.3ad Dynamic link aggregation' not in data:
+ continue
+
+ mode_active = "active" if "LACP active: on" in data else "passive"
+ lacp_rate = re.search(r"LACP rate: (\w+)", data).group(1) if re.search(r"LACP rate: (\w+)", data) else "N/A"
+ hash_policy = re.search(r"Transmit Hash Policy: (.+?) \(\d+\)", data).group(1) if re.search(r"Transmit Hash Policy: (.+?) \(\d+\)", data) else "N/A"
+ system_mac = re.search(r"System MAC address: ([0-9a-f:]+)", data).group(1) if re.search(r"System MAC address: ([0-9a-f:]+)", data) else "N/A"
+ if raw:
+ members = re.findall(r"Slave Interface: ([a-zA-Z0-9:_-]+)", data)
+ else:
+ members = ",".join(set(re.findall(r"Slave Interface: ([a-zA-Z0-9:_-]+)", data)))
+
+ bondList.append([interface, members, mode_active, lacp_rate, system_mac, hash_policy])
+
+ if raw:
+ return list_to_dict(bondList, headers, 'lacp')
+ else:
+ return tabulate(bondList, headers)
+
+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/cgnat.py b/src/op_mode/cgnat.py
new file mode 100755
index 000000000..9ad8f92f9
--- /dev/null
+++ b/src/op_mode/cgnat.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import sys
+import typing
+
+from tabulate import tabulate
+
+import vyos.opmode
+
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import cmd
+
+CGNAT_TABLE = 'cgnat'
+
+
+def _get_raw_data(external_address: str = '', internal_address: str = '') -> list[dict]:
+ """Get CGNAT dictionary and filter by external or internal address if provided."""
+ cmd_output = cmd(f'nft --json list table ip {CGNAT_TABLE}')
+ data = json.loads(cmd_output)
+
+ elements = data['nftables'][2]['map']['elem']
+ allocations = []
+ for elem in elements:
+ internal = elem[0] # internal
+ external = elem[1]['concat'][0] # external
+ start_port = elem[1]['concat'][1]['range'][0]
+ end_port = elem[1]['concat'][1]['range'][1]
+ port_range = f'{start_port}-{end_port}'
+
+ if (internal_address and internal != internal_address) or (
+ external_address and external != external_address
+ ):
+ continue
+
+ allocations.append(
+ {
+ 'internal_address': internal,
+ 'external_address': external,
+ 'port_range': port_range,
+ }
+ )
+
+ return allocations
+
+
+def _get_formatted_output(allocations: list[dict]) -> str:
+ # Convert the list of dictionaries to a list of tuples for tabulate
+ headers = ['Internal IP', 'External IP', 'Port range']
+ data = [
+ (alloc['internal_address'], alloc['external_address'], alloc['port_range'])
+ for alloc in allocations
+ ]
+ output = tabulate(data, headers, numalign="left")
+ return output
+
+
+def show_allocation(
+ raw: bool,
+ external_address: typing.Optional[str],
+ internal_address: typing.Optional[str],
+) -> str:
+ config = ConfigTreeQuery()
+ if not config.exists('nat cgnat'):
+ raise vyos.opmode.UnconfiguredSubsystem('CGNAT is not configured')
+
+ if raw:
+ return _get_raw_data(external_address, internal_address)
+
+ else:
+ raw_data = _get_raw_data(external_address, internal_address)
+ return _get_formatted_output(raw_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/evpn.py b/src/op_mode/evpn.py
new file mode 100644
index 000000000..cae4ab9f5
--- /dev/null
+++ b/src/op_mode/evpn.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2016-2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# This script is a helper to run VTYSH commands for "show evpn", allowing for the --raw flag to output JSON
+
+import sys
+import typing
+import json
+
+import vyos.opmode
+from vyos.utils.process import cmd
+
+def show_evpn(raw: bool, command: typing.Optional[str]):
+ if raw:
+ command = f"{command} json"
+ evpnDict = {}
+ try:
+ evpnDict['evpn'] = json.loads(cmd(f"vtysh -c '{command}'"))
+ except:
+ raise vyos.opmode.DataUnavailable(f"\"{command.replace(' json', '')}\" is invalid or has no JSON option")
+
+ return evpnDict
+ else:
+ return cmd(f"vtysh -c '{command}'")
+
+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/firewall.py b/src/op_mode/firewall.py
index 442c186cc..15fbb65a2 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -531,9 +531,15 @@ def show_firewall_group(name=None):
continue
for idx, member in enumerate(members):
- val = member.get('val', 'N/D')
- timeout = str(member.get('timeout', 'N/D'))
- expires = str(member.get('expires', 'N/D'))
+ if isinstance(member, str):
+ # Only member, and no timeout:
+ val = member
+ timeout = "N/D"
+ expires = "N/D"
+ else:
+ val = member.get('val', 'N/D')
+ timeout = str(member.get('timeout', 'N/D'))
+ expires = str(member.get('expires', 'N/D'))
if args.detail:
row.append(f'{val} (timeout: {timeout}, expires: {expires})')
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index ba0e3b6db..0d2d7076c 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -23,6 +23,8 @@ from shutil import copy, chown, rmtree, copytree
from glob import glob
from sys import exit
from os import environ
+from os import readlink
+from os import getpid, getppid
from typing import Union
from urllib.parse import urlparse
from passlib.hosts import linux_context
@@ -65,7 +67,7 @@ MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:'
MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:'
MSG_INPUT_ROOT_SIZE_ALL: str = 'Would you like to use all the free space on the drive?'
MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root partition (min is 1.5 GB)?'
-MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?'
+MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial)?'
MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?'
MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?'
MSG_INPUT_COPY_ENC_DATA: str = 'Would you like to copy the encrypted config to the new image?'
@@ -614,6 +616,20 @@ def copy_ssh_host_keys() -> bool:
return False
+def console_hint() -> str:
+ pid = getppid() if 'SUDO_USER' in environ else getpid()
+ try:
+ path = readlink(f'/proc/{pid}/fd/1')
+ except OSError:
+ path = '/dev/tty'
+
+ name = Path(path).name
+ if name == 'ttyS0':
+ return 'S'
+ else:
+ return 'K'
+
+
def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None:
"""Clean up after installation
@@ -709,9 +725,9 @@ def install_image() -> None:
# ask for default console
console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE,
- default='K',
- valid_responses=['K', 'S', 'U'])
- console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS', 'U': 'ttyUSB'}
+ default=console_hint(),
+ valid_responses=['K', 'S'])
+ console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS'}
config_boot_list = ['/opt/vyatta/etc/config/config.boot',
'/opt/vyatta/etc/config.boot.default']
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index 1cfb5f5a1..fb4286dbc 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -21,7 +21,7 @@ from argparse import ArgumentParser, Namespace
from pathlib import Path
from shutil import rmtree
from sys import exit
-from typing import Optional
+from typing import Optional, Literal, TypeAlias, get_args
from vyos.system import disk, grub, image, compat
from vyos.utils.io import ask_yes_no, select_entry
@@ -33,6 +33,8 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:'
MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first'
MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first'
+ConsoleType: TypeAlias = Literal['tty', 'ttyS']
+
def annotate_list(images_list: list[str]) -> list[str]:
"""Annotate list of images with additional info
@@ -202,6 +204,15 @@ def rename_image(name_old: str, name_new: str) -> None:
exit(f'Unable to rename the encrypted config for "{name_old}" to "{name_new}": {err}')
+@compat.grub_cfg_update
+def set_console_type(console_type: ConsoleType) -> None:
+ console_choice = get_args(ConsoleType)
+ if console_type not in console_choice:
+ exit(f'console type \'{console_type}\' not available')
+
+ grub.set_console_type(console_type)
+
+
def list_images() -> None:
"""Print list of available images for CLI hints"""
images_list: list[str] = grub.version_list()
@@ -209,6 +220,13 @@ def list_images() -> None:
print(image_name)
+def list_console_types() -> None:
+ """Print list of console types for CLI hints"""
+ console_types: list[str] = list(get_args(ConsoleType))
+ for console_type in console_types:
+ print(console_type)
+
+
def parse_arguments() -> Namespace:
"""Parse arguments
@@ -217,7 +235,8 @@ def parse_arguments() -> Namespace:
"""
parser: ArgumentParser = ArgumentParser(description='Manage system images')
parser.add_argument('--action',
- choices=['delete', 'set', 'rename', 'list'],
+ choices=['delete', 'set', 'set_console_type',
+ 'rename', 'list', 'list_console_types'],
required=True,
help='action to perform with an image')
parser.add_argument('--no-prompt', action='store_true',
@@ -227,6 +246,7 @@ def parse_arguments() -> Namespace:
help=
'a name of an image to add, delete, install, rename, or set as default')
parser.add_argument('--image-new-name', help='a new name for image')
+ parser.add_argument('--console-type', help='console type for boot')
args: Namespace = parser.parse_args()
# Validate arguments
if args.action == 'rename' and (not args.image_name or
@@ -243,10 +263,14 @@ if __name__ == '__main__':
delete_image(args.image_name, args.no_prompt)
if args.action == 'set':
set_image(args.image_name)
+ if args.action == 'set_console_type':
+ set_console_type(args.console_type)
if args.action == 'rename':
rename_image(args.image_name, args.image_new_name)
if args.action == 'list':
list_images()
+ if args.action == 'list_console_types':
+ list_console_types()
exit()
diff --git a/src/op_mode/ipoe-control.py b/src/op_mode/ipoe-control.py
index 0f33beca7..b7d6a0c43 100755
--- a/src/op_mode/ipoe-control.py
+++ b/src/op_mode/ipoe-control.py
@@ -56,7 +56,11 @@ def main():
if args.selector in cmd_dict['selector'] and args.target:
run(cmd_dict['cmd_base'] + "{0} {1} {2}".format(args.action, args.selector, args.target))
else:
- output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action], decode='utf-8')
+ if args.action == "show_sessions":
+ ses_pattern = " ifname,username,calling-sid,ip,ip6,ip6-dp,rate-limit,type,comp,state,uptime"
+ else:
+ ses_pattern = ""
+ output, err = popen(cmd_dict['cmd_base'] + cmd_dict['actions'][args.action] + ses_pattern, decode='utf-8')
if not err:
print(output)
else:
diff --git a/src/op_mode/nat.py b/src/op_mode/nat.py
index 2bc7e24fe..4ab524fb7 100755
--- a/src/op_mode/nat.py
+++ b/src/op_mode/nat.py
@@ -263,7 +263,7 @@ def _get_formatted_translation(dict_data, nat_direction, family, verbose):
proto = meta['layer4']['protoname']
if direction == 'independent':
conn_id = meta['id']
- timeout = meta['timeout']
+ timeout = meta.get('timeout', 'n/a')
orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src
orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst
reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src
diff --git a/src/op_mode/ntp.py b/src/op_mode/ntp.py
new file mode 100644
index 000000000..e14cc46d0
--- /dev/null
+++ b/src/op_mode/ntp.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import csv
+import sys
+from itertools import chain
+
+import vyos.opmode
+from vyos.configquery import ConfigTreeQuery
+from vyos.utils.process import cmd
+
+def _get_raw_data(command: str) -> dict:
+ # Returns returns chronyc output as a dictionary
+
+ # Initialize dictionary keys to align with output of
+ # chrony -c. From some commands, its -c switch outputs
+ # more parameters, make sure to include them all below.
+ # See to chronyc(1) for definition of key variables
+ match command:
+ case "chronyc -c activity":
+ keys: list = [
+ 'sources_online',
+ 'sources_offline',
+ 'sources_doing_burst_return_online',
+ 'sources_doing_burst_return_offline',
+ 'sources_with_unknown_address'
+ ]
+
+ case "chronyc -c sources":
+ keys: list = [
+ 'm',
+ 's',
+ 'name_ip_address',
+ 'stratum',
+ 'poll',
+ 'reach',
+ 'last_rx',
+ 'last_sample_adj_offset',
+ 'last_sample_mes_offset',
+ 'last_sample_est_error'
+ ]
+
+ case "chronyc -c sourcestats":
+ keys: list = [
+ 'name_ip_address',
+ 'np',
+ 'nr',
+ 'span',
+ 'frequency',
+ 'freq_skew',
+ 'offset',
+ 'std_dev'
+ ]
+
+ case "chronyc -c tracking":
+ keys: list = [
+ 'ref_id',
+ 'ref_id_name',
+ 'stratum',
+ 'ref_time',
+ 'system_time',
+ 'last_offset',
+ 'rms_offset',
+ 'frequency',
+ 'residual_freq',
+ 'skew',
+ 'root_delay',
+ 'root_dispersion',
+ 'update_interval',
+ 'leap_status'
+ ]
+
+ case _:
+ raise ValueError(f"Raw mode: of {command} is not implemented")
+
+ # Get -c option command line output, splitlines,
+ # and save comma-separated values as a flat list
+ output = cmd(command).splitlines()
+ values = csv.reader(output)
+ values = list(chain.from_iterable(values))
+
+ # Divide values into chunks of size keys and transpose
+ if len(values) > len(keys):
+ values = _chunk_list(values,keys)
+ values = zip(*values)
+
+ return dict(zip(keys, values))
+
+def _chunk_list(in_list, n):
+ # Yields successive n-sized chunks from in_list
+ for i in range(0, len(in_list), len(n)):
+ yield in_list[i:i + len(n)]
+
+def _is_configured():
+ # Check if ntp is configured
+ config = ConfigTreeQuery()
+ if not config.exists("service ntp"):
+ raise vyos.opmode.UnconfiguredSubsystem("NTP service is not enabled.")
+
+def show_activity(raw: bool):
+ _is_configured()
+ command = f'chronyc'
+
+ if raw:
+ command += f" -c activity"
+ return _get_raw_data(command)
+ else:
+ command += f" activity"
+ return cmd(command)
+
+def show_sources(raw: bool):
+ _is_configured()
+ command = f'chronyc'
+
+ if raw:
+ command += f" -c sources"
+ return _get_raw_data(command)
+ else:
+ command += f" sources -v"
+ return cmd(command)
+
+def show_tracking(raw: bool):
+ _is_configured()
+ command = f'chronyc'
+
+ if raw:
+ command += f" -c tracking"
+ return _get_raw_data(command)
+ else:
+ command += f" tracking"
+ return cmd(command)
+
+def show_sourcestats(raw: bool):
+ _is_configured()
+ command = f'chronyc'
+
+ if raw:
+ command += f" -c sourcestats"
+ return _get_raw_data(command)
+ else:
+ command += f" sourcestats -v"
+ return cmd(command)
+
+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/version.py b/src/op_mode/version.py
index ad0293aca..09d69ad1d 100755
--- a/src/op_mode/version.py
+++ b/src/op_mode/version.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2016-2022 VyOS maintainers and contributors
+# Copyright (C) 2016-2024 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -30,11 +30,15 @@ from jinja2 import Template
version_output_tmpl = """
Version: VyOS {{version}}
Release train: {{release_train}}
+Release flavor: {{flavor}}
Built by: {{built_by}}
Built on: {{built_on}}
Build UUID: {{build_uuid}}
Build commit ID: {{build_git}}
+{%- if build_comment %}
+Build comment: {{build_comment}}
+{% endif %}
Architecture: {{system_arch}}
Boot via: {{boot_via}}
diff --git a/src/systemd/miniupnpd.service b/src/systemd/miniupnpd.service
deleted file mode 100644
index 51cb2eed8..000000000
--- a/src/systemd/miniupnpd.service
+++ /dev/null
@@ -1,13 +0,0 @@
-[Unit]
-Description=UPnP service
-ConditionPathExists=/run/upnp/miniupnp.conf
-After=vyos-router.service
-StartLimitIntervalSec=0
-
-[Service]
-WorkingDirectory=/run/upnp
-Type=simple
-ExecStart=/usr/sbin/miniupnpd -d -f /run/upnp/miniupnp.conf
-PrivateTmp=yes
-PIDFile=/run/miniupnpd.pid
-Restart=on-failure