diff options
Diffstat (limited to 'python')
| -rw-r--r-- | python/vyos/accel_ppp_util.py | 41 | ||||
| -rw-r--r-- | python/vyos/config.py | 51 | ||||
| -rw-r--r-- | python/vyos/configdict.py | 13 | ||||
| -rw-r--r-- | python/vyos/configverify.py | 7 | ||||
| -rw-r--r-- | python/vyos/defaults.py | 12 | ||||
| -rw-r--r-- | python/vyos/firewall.py | 24 | ||||
| -rw-r--r-- | python/vyos/ifconfig/ethernet.py | 7 | ||||
| -rw-r--r-- | python/vyos/kea.py | 117 | ||||
| -rw-r--r-- | python/vyos/nat.py | 10 | ||||
| -rw-r--r-- | python/vyos/opmode.py | 5 | ||||
| -rw-r--r-- | python/vyos/qos/base.py | 50 | ||||
| -rw-r--r-- | python/vyos/qos/trafficshaper.py | 101 | ||||
| -rw-r--r-- | python/vyos/remote.py | 2 | ||||
| -rw-r--r-- | python/vyos/system/compat.py | 25 | ||||
| -rw-r--r-- | python/vyos/system/disk.py | 2 | ||||
| -rw-r--r-- | python/vyos/system/grub.py | 78 | ||||
| -rw-r--r-- | python/vyos/system/grub_util.py | 70 | ||||
| -rw-r--r-- | python/vyos/system/image.py | 13 | ||||
| -rw-r--r-- | python/vyos/template.py | 39 | ||||
| -rw-r--r-- | python/vyos/utils/network.py | 6 | ||||
| -rw-r--r-- | python/vyos/utils/process.py | 27 | 
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): | 
