summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/accel_ppp_util.py41
-rw-r--r--python/vyos/config.py51
-rw-r--r--python/vyos/configdict.py13
-rw-r--r--python/vyos/configverify.py7
-rw-r--r--python/vyos/defaults.py12
-rw-r--r--python/vyos/firewall.py24
-rw-r--r--python/vyos/ifconfig/ethernet.py7
-rw-r--r--python/vyos/kea.py117
-rw-r--r--python/vyos/nat.py10
-rw-r--r--python/vyos/opmode.py5
-rw-r--r--python/vyos/qos/base.py50
-rw-r--r--python/vyos/qos/trafficshaper.py101
-rw-r--r--python/vyos/remote.py2
-rw-r--r--python/vyos/system/compat.py25
-rw-r--r--python/vyos/system/disk.py2
-rw-r--r--python/vyos/system/grub.py78
-rw-r--r--python/vyos/system/grub_util.py70
-rw-r--r--python/vyos/system/image.py13
-rw-r--r--python/vyos/template.py39
-rw-r--r--python/vyos/utils/network.py6
-rw-r--r--python/vyos/utils/process.py27
21 files changed, 555 insertions, 145 deletions
diff --git a/python/vyos/accel_ppp_util.py b/python/vyos/accel_ppp_util.py
index 757d447a2..d60402e48 100644
--- a/python/vyos/accel_ppp_util.py
+++ b/python/vyos/accel_ppp_util.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 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
@@ -22,9 +22,9 @@
# makes use of it!
from vyos import ConfigError
+from vyos.base import Warning
from vyos.utils.dict import dict_search
-
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
@@ -156,38 +156,47 @@ def verify_accel_ppp_base_service(config, local_users=True):
"Not more then three IPv6 DNS name-servers " "can be configured"
)
- if "client_ipv6_pool" in config:
- ipv6_pool = config["client_ipv6_pool"]
- if "delegate" in ipv6_pool:
- if "prefix" not in ipv6_pool:
- raise ConfigError(
- 'IPv6 "delegate" also requires "prefix" to be defined!'
- )
-
- for delegate in ipv6_pool["delegate"]:
- if "delegation_prefix" not in ipv6_pool["delegate"][delegate]:
- raise ConfigError("delegation-prefix length required!")
def verify_accel_ppp_ip_pool(vpn_config):
"""
Common helper function which must be used by Accel-PPP
services (pptp, l2tp, sstp, pppoe) to verify client-ip-pool
+ and client-ipv6-pool
"""
if dict_search("client_ip_pool", vpn_config):
for pool_name, pool_config in vpn_config["client_ip_pool"].items():
next_pool = dict_search(f"next_pool", pool_config)
if next_pool:
if next_pool not in vpn_config["client_ip_pool"]:
- raise ConfigError(f'Next pool "{next_pool}" does not exist')
+ raise ConfigError(
+ f'Next pool "{next_pool}" does not exist')
if not dict_search(f"range", pool_config):
raise ConfigError(
f'Pool "{pool_name}" does not contain range but next-pool exists'
)
-
if not dict_search("gateway_address", vpn_config):
- raise ConfigError("Server requires gateway-address to be configured!")
+ Warning("IPv4 Server requires gateway-address to be configured!")
+
default_pool = dict_search("default_pool", vpn_config)
if default_pool:
if default_pool not in dict_search("client_ip_pool", vpn_config):
raise ConfigError(f'Default pool "{default_pool}" does not exists')
+
+ if 'client_ipv6_pool' in vpn_config:
+ for ipv6_pool, ipv6_pool_config in vpn_config['client_ipv6_pool'].items():
+ if 'delegate' in ipv6_pool_config and 'prefix' not in ipv6_pool_config:
+ raise ConfigError(
+ f'IPv6 delegate-prefix requires IPv6 prefix to be configured in "{ipv6_pool}"!')
+
+ if dict_search('authentication.mode', vpn_config) in ['local', 'noauth']:
+ if not dict_search('client_ip_pool', vpn_config) and not dict_search(
+ 'client_ipv6_pool', vpn_config):
+ raise ConfigError(
+ "Local auth mode requires local client-ip-pool or client-ipv6-pool to be configured!")
+ if dict_search('client_ip_pool', vpn_config) and not dict_search(
+ 'default_pool', vpn_config):
+ Warning("'default-pool' is not defined")
+ if dict_search('client_ipv6_pool', vpn_config) and not dict_search(
+ 'default_ipv6_pool', vpn_config):
+ Warning("'default-ipv6-pool' is not defined")
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 0ca41718f..bee85315d 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -29,7 +29,7 @@ There are multiple types of config tree nodes in VyOS, each requires
its own set of operations.
*Leaf nodes* (such as "address" in interfaces) can have values, but cannot
-have children.
+have children.
Leaf nodes can have one value, multiple values, or no values at all.
For example, "system host-name" is a single-value leaf node,
@@ -92,6 +92,38 @@ def config_dict_merge(src: dict, dest: Union[dict, ConfigDict]) -> ConfigDict:
dest = ConfigDict(dest)
return ext_dict_merge(src, dest)
+def config_dict_mangle_acme(name, cli_dict):
+ """
+ Load CLI PKI dictionary and if an ACME certificate is used, load it's content
+ and place it into the CLI dictionary as it would be a "regular" CLI PKI based
+ certificate with private key
+ """
+ from vyos.base import ConfigError
+ from vyos.defaults import directories
+ from vyos.utils.file import read_file
+ from vyos.pki import encode_certificate
+ from vyos.pki import encode_private_key
+ from vyos.pki import load_certificate
+ from vyos.pki import load_private_key
+
+ try:
+ vyos_certbot_dir = directories['certbot']
+
+ if 'acme' in cli_dict:
+ tmp = read_file(f'{vyos_certbot_dir}/live/{name}/cert.pem')
+ tmp = load_certificate(tmp, wrap_tags=False)
+ cert_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1])
+
+ tmp = read_file(f'{vyos_certbot_dir}/live/{name}/privkey.pem')
+ tmp = load_private_key(tmp, wrap_tags=False)
+ key_base64 = "".join(encode_private_key(tmp).strip().split("\n")[1:-1])
+ # install ACME based PEM keys into "regular" CLI config keys
+ cli_dict.update({'certificate' : cert_base64, 'private' : {'key' : key_base64}})
+ except:
+ raise ConfigError(f'Unable to load ACME certificates for "{name}"!')
+
+ return cli_dict
+
class Config(object):
"""
The class of config access objects.
@@ -258,7 +290,9 @@ class Config(object):
def get_config_dict(self, path=[], effective=False, key_mangling=None,
get_first_key=False, no_multi_convert=False,
no_tag_node_value_mangle=False,
- with_defaults=False, with_recursive_defaults=False):
+ with_defaults=False,
+ with_recursive_defaults=False,
+ with_pki=False):
"""
Args:
path (str list): Configuration tree path, can be empty
@@ -274,6 +308,7 @@ class Config(object):
del kwargs['no_multi_convert']
del kwargs['with_defaults']
del kwargs['with_recursive_defaults']
+ del kwargs['with_pki']
lpath = self._make_path(path)
root_dict = self.get_cached_root_dict(effective)
@@ -298,6 +333,18 @@ class Config(object):
else:
conf_dict = ConfigDict(conf_dict)
+ if with_pki and conf_dict:
+ pki_dict = self.get_config_dict(['pki'], key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True)
+ if pki_dict:
+ if 'certificate' in pki_dict:
+ for certificate in pki_dict['certificate']:
+ pki_dict['certificate'][certificate] = config_dict_mangle_acme(
+ certificate, pki_dict['certificate'][certificate])
+
+ conf_dict['pki'] = pki_dict
+
# save optional args for a call to get_config_defaults
setattr(conf_dict, '_dict_kwargs', kwargs)
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 6a421485f..4111d7271 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -163,6 +163,9 @@ def node_changed(conf, path, key_mangling=None, recursive=False, expand_nodes=No
output.extend(list(tmp['delete'].keys()))
if expand_nodes & Diff.ADD:
output.extend(list(tmp['add'].keys()))
+
+ # remove duplicate keys from list, this happens when a node (e.g. description) is altered
+ output = list(dict.fromkeys(output))
return output
def get_removed_vlans(conf, path, dict):
@@ -424,7 +427,7 @@ def get_pppoe_interfaces(conf, vrf=None):
return pppoe_interfaces
-def get_interface_dict(config, base, ifname='', recursive_defaults=True):
+def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pki=False):
"""
Common utility function to retrieve and mangle the interfaces configuration
from the CLI input nodes. All interfaces have a common base where value
@@ -456,7 +459,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True):
get_first_key=True,
no_tag_node_value_mangle=True,
with_defaults=True,
- with_recursive_defaults=recursive_defaults)
+ with_recursive_defaults=recursive_defaults,
+ with_pki=with_pki)
# If interface does not request an IPv4 DHCP address there is no need
# to keep the dhcp-options key
@@ -620,7 +624,7 @@ def get_vlan_ids(interface):
return vlan_ids
-def get_accel_dict(config, base, chap_secrets):
+def get_accel_dict(config, base, chap_secrets, with_pki=False):
"""
Common utility function to retrieve and mangle the Accel-PPP configuration
from different CLI input nodes. All Accel-PPP services have a common base
@@ -635,7 +639,8 @@ def get_accel_dict(config, base, chap_secrets):
dict = config.get_config_dict(base, key_mangling=('-', '_'),
get_first_key=True,
no_tag_node_value_mangle=True,
- with_recursive_defaults=True)
+ with_recursive_defaults=True,
+ with_pki=with_pki)
# set CPUs cores to process requests
dict.update({'thread_count' : get_half_cpus()})
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 85423142d..5d3723876 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -1,4 +1,4 @@
-# Copyright 2020-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2020-2024 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
@@ -25,6 +25,9 @@ from vyos import ConfigError
from vyos.utils.dict import dict_search
from vyos.utils.dict import dict_search_recursive
+# pattern re-used in ipsec migration script
+dynamic_interface_pattern = r'(ppp|pppoe|sstpc|l2tp|ipoe)[0-9]+'
+
def verify_mtu(config):
"""
Common helper function used by interface implementations to perform
@@ -290,7 +293,7 @@ def verify_source_interface(config):
src_ifname = config['source_interface']
# We do not allow sourcing other interfaces (e.g. tunnel) from dynamic interfaces
- tmp = re.compile(r'(ppp|pppoe|sstpc|l2tp|ipoe)[0-9]+')
+ tmp = re.compile(dynamic_interface_pattern)
if tmp.match(src_ifname):
raise ConfigError(f'Can not source "{ifname}" from dynamic interface "{src_ifname}"!')
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 2f3580571..64145a42e 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -37,6 +37,7 @@ directories = {
}
config_status = '/tmp/vyos-config-status'
+api_config_state = '/run/http-api-state'
cfg_group = 'vyattacfg'
@@ -45,14 +46,3 @@ cfg_vintage = 'vyos'
commit_lock = '/opt/vyatta/config/.lock'
component_version_json = os.path.join(directories['data'], 'component-versions.json')
-
-https_data = {
- 'listen_addresses' : { '*': ['_'] }
-}
-
-vyos_cert_data = {
- 'conf' : '/etc/nginx/snippets/vyos-cert.conf',
- 'crt' : '/etc/ssl/certs/vyos-selfsigned.crt',
- 'key' : '/etc/ssl/private/vyos-selfsign',
- 'lifetime' : '365',
-}
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index a2622fa00..eee11bd2d 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -226,6 +226,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!=' if exclude else '=='
operator = f'& {address_mask} {operator}'
output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}')
+ elif 'dynamic_address_group' in group:
+ group_name = group['dynamic_address_group']
+ operator = ''
+ exclude = group_name[0] == "!"
+ if exclude:
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_name} {prefix}addr {operator} @DA{def_suffix}_{group_name}')
# Generate firewall group domain-group
elif 'domain_group' in group:
group_name = group['domain_group']
@@ -280,7 +288,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!='
iiface = iiface[1:]
output.append(f'iifname {operator} {{{iiface}}}')
- else:
+ elif 'group' in rule_conf['inbound_interface']:
iiface = rule_conf['inbound_interface']['group']
if iiface[0] == '!':
operator = '!='
@@ -295,7 +303,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!='
oiface = oiface[1:]
output.append(f'oifname {operator} {{{oiface}}}')
- else:
+ elif 'group' in rule_conf['outbound_interface']:
oiface = rule_conf['outbound_interface']['group']
if oiface[0] == '!':
operator = '!='
@@ -419,6 +427,18 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
output.append('counter')
+ if 'add_address_to_group' in rule_conf:
+ for side in ['destination_address', 'source_address']:
+ if side in rule_conf['add_address_to_group']:
+ prefix = side[0]
+ side_conf = rule_conf['add_address_to_group'][side]
+ dyn_group = side_conf['address_group']
+ if 'timeout' in side_conf:
+ timeout_value = side_conf['timeout']
+ output.append(f'set update ip{def_suffix} {prefix}addr timeout {timeout_value} @DA{def_suffix}_{dyn_group}')
+ else:
+ output.append(f'set update ip{def_suffix} saddr @DA{def_suffix}_{dyn_group}')
+
if 'set' in rule_conf:
output.append(parse_policy_set(rule_conf['set'], def_suffix))
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index aaf903acd..c3f5bbf47 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -19,6 +19,7 @@ from glob import glob
from vyos.base import Warning
from vyos.ethtool import Ethtool
+from vyos.ifconfig import Section
from vyos.ifconfig.interface import Interface
from vyos.utils.dict import dict_search
from vyos.utils.file import read_file
@@ -128,6 +129,10 @@ class EthernetIf(Interface):
# will remain visible for the operating system.
self.set_admin_state('down')
+ # Remove all VLAN subinterfaces - filter with the VLAN dot
+ for vlan in [x for x in Section.interfaces(self.iftype) if x.startswith(f'{self.ifname}.')]:
+ Interface(vlan).remove()
+
super().remove()
def set_flow_control(self, enable):
@@ -447,7 +452,7 @@ class EthernetIf(Interface):
self.set_gso(dict_search('offload.gso', config) != None)
# GSO (generic segmentation offload)
- self.set_hw_tc_offload(dict_search('offload.hw-tc-offload', config) != None)
+ self.set_hw_tc_offload(dict_search('offload.hw_tc_offload', config) != None)
# LRO (large receive offload)
self.set_lro(dict_search('offload.lro', config) != None)
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 819fe16a9..720bebec3 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -25,7 +25,7 @@ from vyos.template import netmask_from_cidr
from vyos.utils.dict import dict_search_args
from vyos.utils.file import file_permissions
from vyos.utils.file import read_file
-from vyos.utils.process import cmd
+from vyos.utils.process import run
kea4_options = {
'name_server': 'domain-name-servers',
@@ -92,17 +92,28 @@ def kea_parse_options(config):
options.append({'name': 'pcode', 'data': tz_string})
options.append({'name': 'tcode', 'data': config['time_zone']})
+ unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
+ if unifi_controller:
+ options.append({
+ 'name': 'unifi-controller',
+ 'data': unifi_controller,
+ 'space': 'ubnt'
+ })
+
return options
def kea_parse_subnet(subnet, config):
- out = {'subnet': subnet}
- options = kea_parse_options(config)
+ out = {'subnet': subnet, 'id': int(config['subnet_id'])}
+ options = []
+
+ if 'option' in config:
+ out['option-data'] = kea_parse_options(config['option'])
- if 'bootfile_name' in config:
- out['boot-file-name'] = config['bootfile_name']
+ if 'bootfile_name' in config['option']:
+ out['boot-file-name'] = config['option']['bootfile_name']
- if 'bootfile_server' in config:
- out['next-server'] = config['bootfile_server']
+ if 'bootfile_server' in config['option']:
+ out['next-server'] = config['option']['bootfile_server']
if 'lease' in config:
out['valid-lifetime'] = int(config['lease'])
@@ -112,7 +123,20 @@ def kea_parse_subnet(subnet, config):
pools = []
for num, range_config in config['range'].items():
start, stop = range_config['start'], range_config['stop']
- pools.append({'pool': f'{start} - {stop}'})
+ pool = {
+ 'pool': f'{start} - {stop}'
+ }
+
+ if 'option' in range_config:
+ pool['option-data'] = kea_parse_options(range_config['option'])
+
+ if 'bootfile_name' in range_config['option']:
+ pool['boot-file-name'] = range_config['option']['bootfile_name']
+
+ if 'bootfile_server' in range_config['option']:
+ pool['next-server'] = range_config['option']['bootfile_server']
+
+ pools.append(pool)
out['pools'] = pools
if 'static_mapping' in config:
@@ -134,35 +158,23 @@ def kea_parse_subnet(subnet, config):
if 'ip_address' in host_config:
reservation['ip-address'] = host_config['ip_address']
- reservations.append(reservation)
- out['reservations'] = reservations
+ if 'option' in host_config:
+ reservation['option-data'] = kea_parse_options(host_config['option'])
- unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
- if unifi_controller:
- options.append({
- 'name': 'unifi-controller',
- 'data': unifi_controller,
- 'space': 'ubnt'
- })
+ if 'bootfile_name' in host_config['option']:
+ reservation['boot-file-name'] = host_config['option']['bootfile_name']
+
+ if 'bootfile_server' in host_config['option']:
+ reservation['next-server'] = host_config['option']['bootfile_server']
- if options:
- out['option-data'] = options
+ reservations.append(reservation)
+ out['reservations'] = reservations
return out
def kea6_parse_options(config):
options = []
- if 'common_options' in config:
- common_opt = config['common_options']
-
- for node, option_name in kea6_options.items():
- if node not in common_opt:
- continue
-
- value = ", ".join(common_opt[node]) if isinstance(common_opt[node], list) else common_opt[node]
- options.append({'name': option_name, 'data': value})
-
for node, option_name in kea6_options.items():
if node not in config:
continue
@@ -195,21 +207,28 @@ def kea6_parse_options(config):
return options
def kea6_parse_subnet(subnet, config):
- out = {'subnet': subnet}
- options = kea6_parse_options(config)
+ out = {'subnet': subnet, 'id': int(config['subnet_id'])}
+
+ if 'option' in config:
+ out['option-data'] = kea6_parse_options(config['option'])
- if 'address_range' in config:
- addr_range = config['address_range']
+ if 'range' in config:
pools = []
+ for num, range_config in config['range'].items():
+ pool = {}
+
+ if 'prefix' in range_config:
+ pool['pool'] = range_config['prefix']
- if 'prefix' in addr_range:
- for prefix in addr_range['prefix']:
- pools.append({'pool': prefix})
+ if 'start' in range_config:
+ start = range_config['start']
+ stop = range_config['stop']
+ pool['pool'] = f'{start} - {stop}'
- if 'start' in addr_range:
- for start, range_conf in addr_range['start'].items():
- stop = range_conf['stop']
- pools.append({'pool': f'{start} - {stop}'})
+ if 'option' in range_config:
+ pool['option-data'] = kea6_parse_options(range_config['option'])
+
+ pools.append(pool)
out['pools'] = pools
@@ -218,11 +237,17 @@ def kea6_parse_subnet(subnet, config):
if 'prefix' in config['prefix_delegation']:
for prefix, pd_conf in config['prefix_delegation']['prefix'].items():
- pd_pools.append({
+ pd_pool = {
'prefix': prefix,
'prefix-len': int(pd_conf['prefix_length']),
'delegated-len': int(pd_conf['delegated_length'])
- })
+ }
+
+ if 'excluded_prefix' in pd_conf:
+ pd_pool['excluded-prefix'] = pd_conf['excluded_prefix']
+ pd_pool['excluded-prefix-len'] = int(pd_conf['excluded_prefix_length'])
+
+ pd_pools.append(pd_pool)
out['pd-pools'] = pd_pools
@@ -256,13 +281,13 @@ def kea6_parse_subnet(subnet, config):
if 'ipv6_prefix' in host_config:
reservation['prefixes'] = [ host_config['ipv6_prefix'] ]
+ if 'option' in host_config:
+ reservation['option-data'] = kea6_parse_options(host_config['option'])
+
reservations.append(reservation)
out['reservations'] = reservations
- if options:
- out['option-data'] = options
-
return out
def kea_parse_leases(lease_path):
@@ -293,7 +318,7 @@ def _ctrl_socket_command(path, command, args=None):
return None
if file_permissions(path) != '0775':
- cmd(f'sudo chmod 775 {path}')
+ run(f'sudo chmod 775 {path}')
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(path)
diff --git a/python/vyos/nat.py b/python/vyos/nat.py
index 392d38772..7215aac88 100644
--- a/python/vyos/nat.py
+++ b/python/vyos/nat.py
@@ -89,7 +89,10 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
if addr and is_ip_network(addr):
if not ipv6:
map_addr = dict_search_args(rule_conf, nat_type, 'address')
- translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} }}')
+ if port:
+ translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} . {port} }}')
+ else:
+ translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} }}')
ignore_type_addr = True
else:
translation_output.append(f'prefix to {addr}')
@@ -112,7 +115,10 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False):
if port_mapping and port_mapping != 'none':
options.append(port_mapping)
- translation_str = " ".join(translation_output) + (f':{port}' if port else '')
+ if ((not addr) or (addr and not is_ip_network(addr))) and port:
+ translation_str = " ".join(translation_output) + (f':{port}')
+ else:
+ translation_str = " ".join(translation_output)
if options:
translation_str += f' {",".join(options)}'
diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py
index 230a85541..e1af1a682 100644
--- a/python/vyos/opmode.py
+++ b/python/vyos/opmode.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2022-2024 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
@@ -81,7 +81,7 @@ class InternalError(Error):
def _is_op_mode_function_name(name):
- if re.match(r"^(show|clear|reset|restart|add|delete|generate|set)", name):
+ if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set)", name):
return True
else:
return False
@@ -275,4 +275,3 @@ def run(module):
# Other functions should not return anything,
# although they may print their own warnings or status messages
func(**args)
-
diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py
index d8bbfe970..a22039e52 100644
--- a/python/vyos/qos/base.py
+++ b/python/vyos/qos/base.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2022-2024 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
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import jmespath
from vyos.base import Warning
from vyos.utils.process import cmd
@@ -166,14 +167,17 @@ class QoSBase:
}
if rate == 'auto' or rate.endswith('%'):
- speed = 10
+ speed = 1000
+ default_speed = speed
# Not all interfaces have valid entries in the speed file. PPPoE
# interfaces have the appropriate speed file, but you can not read it:
# cat: /sys/class/net/pppoe7/speed: Invalid argument
try:
speed = read_file(f'/sys/class/net/{self._interface}/speed')
if not speed.isnumeric():
- Warning('Interface speed cannot be determined (assuming 10 Mbit/s)')
+ Warning('Interface speed cannot be determined (assuming 1000 Mbit/s)')
+ if int(speed) < 1:
+ speed = default_speed
if rate.endswith('%'):
percent = rate.rstrip('%')
speed = int(speed) * int(percent) // 100
@@ -223,6 +227,9 @@ class QoSBase:
if 'mark' in match_config:
mark = match_config['mark']
filter_cmd += f' handle {mark} fw'
+ if 'vif' in match_config:
+ vif = match_config['vif']
+ filter_cmd += f' basic match "meta(vlan mask 0xfff eq {vif})"'
for af in ['ip', 'ipv6']:
tc_af = af
@@ -298,23 +305,28 @@ class QoSBase:
filter_cmd += f' flowid {self._parent:x}:{cls:x}'
self._cmd(filter_cmd)
+ vlan_expression = "match.*.vif"
+ match_vlan = jmespath.search(vlan_expression, cls_config)
+
if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config):
- filter_cmd += f' action police'
-
- if 'exceed' in cls_config:
- action = cls_config['exceed']
- filter_cmd += f' conform-exceed {action}'
- if 'not_exceed' in cls_config:
- action = cls_config['not_exceed']
- filter_cmd += f'/{action}'
-
- if 'bandwidth' in cls_config:
- rate = self._rate_convert(cls_config['bandwidth'])
- filter_cmd += f' rate {rate}'
-
- if 'burst' in cls_config:
- burst = cls_config['burst']
- filter_cmd += f' burst {burst}'
+ # For "vif" "basic match" is used instead of "action police" T5961
+ if not match_vlan:
+ filter_cmd += f' action police'
+
+ if 'exceed' in cls_config:
+ action = cls_config['exceed']
+ filter_cmd += f' conform-exceed {action}'
+ if 'not_exceed' in cls_config:
+ action = cls_config['not_exceed']
+ filter_cmd += f'/{action}'
+
+ if 'bandwidth' in cls_config:
+ rate = self._rate_convert(cls_config['bandwidth'])
+ filter_cmd += f' rate {rate}'
+
+ if 'burst' in cls_config:
+ burst = cls_config['burst']
+ filter_cmd += f' burst {burst}'
cls = int(cls)
filter_cmd += f' flowid {self._parent:x}:{cls:x}'
self._cmd(filter_cmd)
diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py
index 0d5f9a8a1..d6705cc77 100644
--- a/python/vyos/qos/trafficshaper.py
+++ b/python/vyos/qos/trafficshaper.py
@@ -1,4 +1,4 @@
-# Copyright 2022-2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2022-2024 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
@@ -99,7 +99,11 @@ class TrafficShaper(QoSBase):
self._cmd(tmp)
if 'default' in config:
- rate = self._rate_convert(config['default']['bandwidth'])
+ if config['default']['bandwidth'].endswith('%'):
+ percent = config['default']['bandwidth'].rstrip('%')
+ rate = self._rate_convert(config['bandwidth']) * int(percent) // 100
+ else:
+ rate = self._rate_convert(config['default']['bandwidth'])
burst = config['default']['burst']
quantum = config['default']['codel_quantum']
tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} htb rate {rate} burst {burst} quantum {quantum}'
@@ -107,7 +111,11 @@ class TrafficShaper(QoSBase):
priority = config['default']['priority']
tmp += f' prio {priority}'
if 'ceiling' in config['default']:
- f_ceil = self._rate_convert(config['default']['ceiling'])
+ if config['default']['ceiling'].endswith('%'):
+ percent = config['default']['ceiling'].rstrip('%')
+ f_ceil = self._rate_convert(config['bandwidth']) * int(percent) // 100
+ else:
+ f_ceil = self._rate_convert(config['default']['ceiling'])
tmp += f' ceil {f_ceil}'
self._cmd(tmp)
@@ -117,8 +125,91 @@ class TrafficShaper(QoSBase):
# call base class
super().update(config, direction)
-class TrafficShaperHFSC(TrafficShaper):
+class TrafficShaperHFSC(QoSBase):
+ _parent = 1
+ qostype = 'shaper_hfsc'
+
+ # https://man7.org/linux/man-pages/man8/tc-hfsc.8.html
def update(self, config, direction):
+ class_id_max = 0
+ if 'class' in config:
+ tmp = list(config['class'])
+ tmp.sort()
+ class_id_max = tmp[-1]
+
+ r2q = 10
+ # bandwidth is a mandatory CLI node
+ speed = self._rate_convert(config['bandwidth'])
+ speed_bps = int(speed) // 8
+
+ # need a bigger r2q if going fast than 16 mbits/sec
+ if (speed_bps // r2q) >= MAXQUANTUM: # integer division
+ r2q = ceil(speed_bps // MAXQUANTUM)
+ else:
+ # if there is a slow class then may need smaller value
+ if 'class' in config:
+ min_speed = speed_bps
+ for cls, cls_options in config['class'].items():
+ # find class with the lowest bandwidth used
+ if 'bandwidth' in cls_options:
+ bw_bps = int(self._rate_convert(cls_options['bandwidth'])) // 8 # bandwidth in bytes per second
+ if bw_bps < min_speed:
+ min_speed = bw_bps
+
+ while (r2q > 1) and (min_speed // r2q) < MINQUANTUM:
+ tmp = r2q -1
+ if (speed_bps // tmp) >= MAXQUANTUM:
+ break
+ r2q = tmp
+
+ default_minor_id = int(class_id_max) +1
+ tmp = f'tc qdisc replace dev {self._interface} root handle {self._parent:x}: hfsc default {default_minor_id:x}' # default is in hex
+ self._cmd(tmp)
+
+ tmp = f'tc class replace dev {self._interface} parent {self._parent:x}: classid {self._parent:x}:1 hfsc sc rate {speed} ul rate {speed}'
+ self._cmd(tmp)
+
+ if 'class' in config:
+ for cls, cls_config in config['class'].items():
+ # class id is used later on and passed as hex, thus this needs to be an int
+ cls = int(cls)
+ # ls m1
+ if cls_config.get('linkshare', {}).get('m1').endswith('%'):
+ percent = cls_config['linkshare']['m1'].rstrip('%')
+ m_one_rate = self._rate_convert(config['bandwidth']) * int(percent) // 100
+ else:
+ m_one_rate = cls_config['linkshare']['m1']
+ # ls m2
+ if cls_config.get('linkshare', {}).get('m2').endswith('%'):
+ percent = cls_config['linkshare']['m2'].rstrip('%')
+ m_two_rate = self._rate_convert(config['bandwidth']) * int(percent) // 100
+ else:
+ m_two_rate = self._rate_convert(cls_config['linkshare']['m2'])
+
+ tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} hfsc ls m1 {m_one_rate} m2 {m_two_rate} '
+ self._cmd(tmp)
+
+ tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{cls:x} sfq perturb 10'
+ self._cmd(tmp)
+
+ if 'default' in config:
+ # ls m1
+ if config.get('default', {}).get('linkshare', {}).get('m1').endswith('%'):
+ percent = config['default']['linkshare']['m1'].rstrip('%')
+ m_one_rate = self._rate_convert(config['default']['linkshare']['m1']) * int(percent) // 100
+ else:
+ m_one_rate = config['default']['linkshare']['m1']
+ # ls m2
+ if config.get('default', {}).get('linkshare', {}).get('m2').endswith('%'):
+ percent = config['default']['linkshare']['m2'].rstrip('%')
+ m_two_rate = self._rate_convert(config['default']['linkshare']['m2']) * int(percent) // 100
+ else:
+ m_two_rate = self._rate_convert(config['default']['linkshare']['m2'])
+ tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} hfsc ls m1 {m_one_rate} m2 {m_two_rate} '
+ self._cmd(tmp)
+
+ tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq perturb 10'
+ self._cmd(tmp)
+
# call base class
super().update(config, direction)
-
diff --git a/python/vyos/remote.py b/python/vyos/remote.py
index b1efcd10b..830770d11 100644
--- a/python/vyos/remote.py
+++ b/python/vyos/remote.py
@@ -148,7 +148,7 @@ class FtpC:
# Almost all FTP servers support the `SIZE' command.
size = conn.size(self.path)
if self.check_space:
- check_storage(path, size)
+ check_storage(location, size)
# No progressbar if we can't determine the size or if the file is too small.
if self.progressbar and size and size > CHUNK_SIZE:
with Progressbar(CHUNK_SIZE / size) as p:
diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py
index 319c3dabf..37b834ad6 100644
--- a/python/vyos/system/compat.py
+++ b/python/vyos/system/compat.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 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
@@ -27,7 +27,7 @@ TMPL_GRUB_COMPAT: str = 'grub/grub_compat.j2'
# define regexes and variables
REGEX_VERSION = r'^menuentry "[^\n]*{\n[^}]*\s+linux /boot/(?P<version>\S+)/[^}]*}'
REGEX_MENUENTRY = r'^menuentry "[^\n]*{\n[^}]*\s+linux /boot/(?P<version>\S+)/vmlinuz (?P<options>[^\n]+)\n[^}]*}'
-REGEX_CONSOLE = r'^.*console=(?P<console_type>[^\s\d]+)(?P<console_num>[\d]+).*$'
+REGEX_CONSOLE = r'^.*console=(?P<console_type>[^\s\d]+)(?P<console_num>[\d]+)(,(?P<console_speed>[\d]+))?.*$'
REGEX_SANIT_CONSOLE = r'\ ?console=[^\s\d]+[\d]+(,\d+)?\ ?'
REGEX_SANIT_INIT = r'\ ?init=\S*\ ?'
REGEX_SANIT_QUIET = r'\ ?quiet\ ?'
@@ -131,6 +131,8 @@ def parse_entry(entry: tuple) -> dict:
# find console type and number
regex_filter = compile(REGEX_CONSOLE)
entry_dict.update(regex_filter.match(entry[1]).groupdict())
+ speed = entry_dict.get('console_speed', None)
+ entry_dict['console_speed'] = speed if speed is not None else '115200'
entry_dict['boot_opts'] = sanitize_boot_opts(entry[1])
return entry_dict
@@ -168,9 +170,12 @@ def prune_vyos_versions(root_dir: str = '') -> None:
if not root_dir:
root_dir = disk.find_persistence()
- for version in grub.version_list():
+ version_files = Path(f'{root_dir}/{grub.GRUB_DIR_VYOS_VERS}').glob('*.cfg')
+
+ for file in version_files:
+ version = Path(file).stem
if not Path(f'{root_dir}/boot/{version}').is_dir():
- grub.version_del(version)
+ grub.version_del(version, root_dir)
def update_cfg_ver(root_dir:str = '') -> int:
@@ -244,13 +249,17 @@ def update_version_list(root_dir: str = '') -> list[dict]:
menu_entries = list(filter(lambda x: x.get('version') != ver,
menu_entries))
+ # reset boot_opts in case of config update
+ for entry in menu_entries:
+ entry['boot_opts'] = grub.get_boot_opts(entry['version'])
+
add = list(set(current_versions) - set(menu_versions))
for ver in add:
last = menu_entries[0].get('version')
new = deepcopy(list(filter(lambda x: x.get('version') == last,
menu_entries)))
for e in new:
- boot_opts = e.get('boot_opts').replace(last, ver)
+ boot_opts = grub.get_boot_opts(ver)
e.update({'version': ver, 'boot_opts': boot_opts})
menu_entries = new + menu_entries
@@ -271,9 +280,11 @@ def grub_cfg_fields(root_dir: str = '') -> dict:
root_dir = disk.find_persistence()
grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}'
+ grub_vars = f'{root_dir}/{grub.CFG_VYOS_VARS}'
- fields = {'default': 0, 'timeout': 5}
- # 'default' and 'timeout' from legacy grub.cfg
+ fields = grub.vars_read(grub_vars)
+ # 'default' and 'timeout' from legacy grub.cfg resets 'default' to
+ # index, rather than uuid
fields |= grub.vars_read(grub_cfg_main)
fields['tools_version'] = SYSTEM_CFG_VER
diff --git a/python/vyos/system/disk.py b/python/vyos/system/disk.py
index b8a2c0f35..7860d719f 100644
--- a/python/vyos/system/disk.py
+++ b/python/vyos/system/disk.py
@@ -78,7 +78,7 @@ def parttable_create(drive_path: str, root_size: int) -> None:
run(command)
# update partitons in kernel
sync()
- run(f'partprobe {drive_path}')
+ run(f'partx -u {drive_path}')
partitions: list[str] = partition_list(drive_path)
diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py
index a94729964..2e8b20972 100644
--- a/python/vyos/system/grub.py
+++ b/python/vyos/system/grub.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 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
@@ -45,10 +45,14 @@ TMPL_GRUB_MODULES: str = 'grub/grub_modules.j2'
TMPL_GRUB_OPTS: str = 'grub/grub_options.j2'
TMPL_GRUB_COMMON: str = 'grub/grub_common.j2'
+# default boot options
+BOOT_OPTS_STEM: str = 'boot=live rootdelay=5 noautologin net.ifnames=0 biosdevname=0 vyos-union=/boot/'
+
# prepare regexes
REGEX_GRUB_VARS: str = r'^set (?P<variable_name>.+)=[\'"]?(?P<variable_value>.*)(?<![\'"])[\'"]?$'
REGEX_GRUB_MODULES: str = r'^insmod (?P<module_name>.+)$'
REGEX_KERNEL_CMDLINE: str = r'^BOOT_IMAGE=/(?P<boot_type>boot|live)/((?P<image_version>.+)/)?vmlinuz.*$'
+REGEX_GRUB_BOOT_OPTS: str = r'^\s*set boot_opts="(?P<boot_opts>[^$]+)"$'
def install(drive_path: str, boot_dir: str, efi_dir: str, id: str = 'VyOS') -> None:
@@ -95,7 +99,8 @@ def gen_version_uuid(version_name: str) -> str:
def version_add(version_name: str,
root_dir: str = '',
- boot_opts: str = '') -> None:
+ boot_opts: str = '',
+ boot_opts_config = None) -> None:
"""Add a new VyOS version to GRUB loader configuration
Args:
@@ -112,7 +117,9 @@ def version_add(version_name: str,
version_config, TMPL_VYOS_VERSION, {
'version_name': version_name,
'version_uuid': gen_version_uuid(version_name),
- 'boot_opts': boot_opts
+ 'boot_opts_default': BOOT_OPTS_STEM + version_name,
+ 'boot_opts': boot_opts,
+ 'boot_opts_config': boot_opts_config
})
@@ -294,12 +301,43 @@ def vars_write(grub_cfg: str, grub_vars: dict[str, str]) -> None:
"""
render(grub_cfg, TMPL_GRUB_VARS, {'vars': grub_vars})
+def get_boot_opts(version_name: str, root_dir: str = '') -> str:
+ """Read boot_opts setting from version file; return default setting on
+ any failure.
+
+ Args:
+ version_name (str): version name
+ root_dir (str, optional): an optional path to the root directory.
+ Defaults to empty.
+ """
+ if not root_dir:
+ root_dir = disk.find_persistence()
+
+ boot_opts_default: str = BOOT_OPTS_STEM + version_name
+ boot_opts: str = ''
+ regex_filter = re_compile(REGEX_GRUB_BOOT_OPTS)
+ version_config: str = f'{root_dir}/{GRUB_DIR_VYOS_VERS}/{version_name}.cfg'
+ try:
+ config_text: list[str] = Path(version_config).read_text().splitlines()
+ except FileNotFoundError:
+ return boot_opts_default
+ for line in config_text:
+ search_result = regex_filter.fullmatch(line)
+ if search_result:
+ search_dict = search_result.groupdict()
+ boot_opts = search_dict.get('boot_opts', '')
+ break
+
+ if not boot_opts:
+ boot_opts = boot_opts_default
+
+ return boot_opts
def set_default(version_name: str, root_dir: str = '') -> None:
"""Set version as default boot entry
Args:
- version_name (str): versio name
+ version_name (str): version name
root_dir (str, optional): an optional path to the root directory.
Defaults to empty.
"""
@@ -354,5 +392,33 @@ def set_console_type(console_type: str, root_dir: str = '') -> None:
vars_current['console_type'] = str(console_type)
vars_write(vars_file, vars_current)
-def set_raid(root_dir: str = '') -> None:
- pass
+def set_console_speed(console_speed: str, root_dir: str = '') -> None:
+ """Write default console speed to GRUB configuration
+
+ Args:
+ console_speed (str): default console speed
+ root_dir (str, optional): an optional path to the root directory.
+ Defaults to empty.
+ """
+ if not root_dir:
+ root_dir = disk.find_persistence()
+
+ vars_file: str = f'{root_dir}/{CFG_VYOS_VARS}'
+ vars_current: dict[str, str] = vars_read(vars_file)
+ vars_current['console_speed'] = str(console_speed)
+ vars_write(vars_file, vars_current)
+
+def set_kernel_cmdline_options(cmdline_options: str, version_name: str,
+ root_dir: str = '') -> None:
+ """Write additional cmdline options to GRUB configuration
+
+ Args:
+ cmdline_options (str): cmdline options to add to default boot line
+ version_name (str): image version name
+ root_dir (str, optional): an optional path to the root directory.
+ """
+ if not root_dir:
+ root_dir = disk.find_persistence()
+
+ version_add(version_name=version_name, root_dir=root_dir,
+ boot_opts_config=cmdline_options)
diff --git a/python/vyos/system/grub_util.py b/python/vyos/system/grub_util.py
new file mode 100644
index 000000000..4a3d8795e
--- /dev/null
+++ b/python/vyos/system/grub_util.py
@@ -0,0 +1,70 @@
+# Copyright 2024 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/>.
+
+from vyos.system import disk, grub, image, compat
+
+@compat.grub_cfg_update
+def set_console_speed(console_speed: str, root_dir: str = '') -> None:
+ """Write default console speed to GRUB configuration
+
+ Args:
+ console_speed (str): default console speed
+ root_dir (str, optional): an optional path to the root directory.
+ Defaults to empty.
+ """
+ if not root_dir:
+ root_dir = disk.find_persistence()
+
+ grub.set_console_speed(console_speed, root_dir)
+
+@image.if_not_live_boot
+def update_console_speed(console_speed: str, root_dir: str = '') -> None:
+ """Update console_speed if different from current value"""
+
+ if not root_dir:
+ root_dir = disk.find_persistence()
+
+ vars_file: str = f'{root_dir}/{grub.CFG_VYOS_VARS}'
+ vars_current: dict[str, str] = grub.vars_read(vars_file)
+ console_speed_current = vars_current.get('console_speed', None)
+ if console_speed != console_speed_current:
+ set_console_speed(console_speed, root_dir)
+
+@compat.grub_cfg_update
+def set_kernel_cmdline_options(cmdline_options: str, version: str = '',
+ root_dir: str = '') -> None:
+ """Write Kernel CLI cmdline options to GRUB configuration"""
+ if not root_dir:
+ root_dir = disk.find_persistence()
+
+ if not version:
+ version = image.get_running_image()
+
+ grub.set_kernel_cmdline_options(cmdline_options, version, root_dir)
+
+@image.if_not_live_boot
+def update_kernel_cmdline_options(cmdline_options: str,
+ root_dir: str = '') -> None:
+ """Update Kernel custom cmdline options"""
+ if not root_dir:
+ root_dir = disk.find_persistence()
+
+ version = image.get_running_image()
+
+ boot_opts_current = grub.get_boot_opts(version, root_dir)
+ boot_opts_proposed = grub.BOOT_OPTS_STEM + f'{version} {cmdline_options}'
+
+ if boot_opts_proposed != boot_opts_current:
+ set_kernel_cmdline_options(cmdline_options, version, root_dir)
diff --git a/python/vyos/system/image.py b/python/vyos/system/image.py
index 514275654..5460e6a36 100644
--- a/python/vyos/system/image.py
+++ b/python/vyos/system/image.py
@@ -1,4 +1,4 @@
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2024 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
@@ -15,6 +15,7 @@
from pathlib import Path
from re import compile as re_compile
+from functools import wraps
from tempfile import TemporaryDirectory
from typing import TypedDict
@@ -262,6 +263,16 @@ def is_live_boot() -> bool:
return True
return False
+def if_not_live_boot(func):
+ """Decorator to call function only if not live boot"""
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ if not is_live_boot():
+ ret = func(*args, **kwargs)
+ return ret
+ return None
+ return wrapper
+
def is_running_as_container() -> bool:
if Path('/.dockerenv').exists():
return True
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 29ea0889b..456239568 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -786,6 +786,23 @@ def range_to_regex(num_range):
regex = range_to_regex(num_range)
return f'({regex})'
+@register_filter('kea_address_json')
+def kea_address_json(addresses):
+ from json import dumps
+ from vyos.utils.network import is_addr_assigned
+
+ out = []
+
+ for address in addresses:
+ ifname = is_addr_assigned(address, return_ifname=True)
+
+ if not ifname:
+ continue
+
+ out.append(f'{ifname}/{address}')
+
+ return dumps(out)
+
@register_filter('kea_failover_json')
def kea_failover_json(config):
from json import dumps
@@ -842,15 +859,22 @@ def kea_shared_network_json(shared_networks):
'authoritative': ('authoritative' in config),
'subnet4': []
}
- options = kea_parse_options(config)
+
+ if 'option' in config:
+ network['option-data'] = kea_parse_options(config['option'])
+
+ if 'bootfile_name' in config['option']:
+ network['boot-file-name'] = config['option']['bootfile_name']
+
+ if 'bootfile_server' in config['option']:
+ network['next-server'] = config['option']['bootfile_server']
if 'subnet' in config:
for subnet, subnet_config in config['subnet'].items():
+ if 'disable' in subnet_config:
+ continue
network['subnet4'].append(kea_parse_subnet(subnet, subnet_config))
- if options:
- network['option-data'] = options
-
out.append(network)
return dumps(out, indent=4)
@@ -870,7 +894,9 @@ def kea6_shared_network_json(shared_networks):
'name': name,
'subnet6': []
}
- options = kea6_parse_options(config)
+
+ if 'common_options' in config:
+ network['option-data'] = kea6_parse_options(config['common_options'])
if 'interface' in config:
network['interface'] = config['interface']
@@ -879,9 +905,6 @@ def kea6_shared_network_json(shared_networks):
for subnet, subnet_config in config['subnet'].items():
network['subnet6'].append(kea6_parse_subnet(subnet, subnet_config))
- if options:
- network['option-data'] = options
-
out.append(network)
return dumps(out, indent=4)
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 997ee6309..cac59475d 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -159,7 +159,9 @@ def is_wwan_connected(interface):
""" Determine if a given WWAN interface, e.g. wwan0 is connected to the
carrier network or not """
import json
+ from vyos.utils.dict import dict_search
from vyos.utils.process import cmd
+ from vyos.utils.process import is_systemd_service_active
if not interface.startswith('wwan'):
raise ValueError(f'Specified interface "{interface}" is not a WWAN interface')
@@ -308,7 +310,7 @@ def is_ipv6_link_local(addr):
return False
-def is_addr_assigned(ip_address, vrf=None) -> bool:
+def is_addr_assigned(ip_address, vrf=None, return_ifname=False) -> bool | str:
""" Verify if the given IPv4/IPv6 address is assigned to any interface """
from netifaces import interfaces
from vyos.utils.network import get_interface_config
@@ -323,7 +325,7 @@ def is_addr_assigned(ip_address, vrf=None) -> bool:
continue
if is_intf_addr_assigned(interface, ip_address):
- return True
+ return interface if return_ifname else True
return False
diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py
index e09c7d86d..bd0644bc0 100644
--- a/python/vyos/utils/process.py
+++ b/python/vyos/utils/process.py
@@ -204,17 +204,32 @@ def process_running(pid_file):
pid = f.read().strip()
return pid_exists(int(pid))
-def process_named_running(name, cmdline: str=None):
+def process_named_running(name: str, cmdline: str=None, timeout: int=0):
""" Checks if process with given name is running and returns its PID.
If Process is not running, return None
"""
from psutil import process_iter
- for p in process_iter(['name', 'pid', 'cmdline']):
- if cmdline:
- if p.info['name'] == name and cmdline in p.info['cmdline']:
+ def check_process(name, cmdline):
+ for p in process_iter(['name', 'pid', 'cmdline']):
+ if cmdline:
+ if name in p.info['name'] and cmdline in p.info['cmdline']:
+ return p.info['pid']
+ elif name in p.info['name']:
return p.info['pid']
- elif p.info['name'] == name:
- return p.info['pid']
+ return None
+ if timeout:
+ import time
+ time_expire = time.time() + timeout
+ while True:
+ tmp = check_process(name, cmdline)
+ if not tmp:
+ if time.time() > time_expire:
+ break
+ time.sleep(0.100) # wait 250ms
+ continue
+ return tmp
+ else:
+ return check_process(name, cmdline)
return None
def is_systemd_service_active(service):