diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 68 | ||||
| -rwxr-xr-x | src/conf_mode/interfaces-wwan.py | 1 | ||||
| -rwxr-xr-x | src/conf_mode/nat.py | 3 | ||||
| -rwxr-xr-x | src/conf_mode/ntp.py | 34 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_bgp.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_ospf.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_ospfv3.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_static.py | 2 | ||||
| -rwxr-xr-x | src/conf_mode/system-login.py | 2 | ||||
| -rw-r--r-- | src/etc/modprobe.d/openvpn.conf | 1 | ||||
| -rwxr-xr-x | src/migration-scripts/container/0-to-1 | 8 | ||||
| -rwxr-xr-x | src/migration-scripts/ntp/2-to-3 | 62 | ||||
| -rwxr-xr-x | src/op_mode/bridge.py | 32 | ||||
| -rwxr-xr-x | src/op_mode/show_ntp.sh | 2 | 
14 files changed, 180 insertions, 41 deletions
| diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 607a19385..3bef9b8f6 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -56,6 +56,8 @@ from vyos.utils.list import is_list_equal  from vyos.utils.file import makedir  from vyos.utils.file import read_file  from vyos.utils.file import write_file +from vyos.utils.kernel import check_kmod +from vyos.utils.kernel import unload_kmod  from vyos.utils.process import call  from vyos.utils.permission import chown  from vyos.utils.process import cmd @@ -86,30 +88,45 @@ def get_config(config=None):          conf = Config()      base = ['interfaces', 'openvpn'] -    tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'), -                                get_first_key=True, no_tag_node_value_mangle=True) -      ifname, openvpn = get_interface_dict(conf, base) - -    if 'deleted' not in openvpn: -        openvpn['pki'] = tmp_pki -        if is_node_changed(conf, base + [ifname, 'openvpn-option']): -            openvpn.update({'restart_required': {}}) - -        # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict' -        # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there. -        tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True) - -        # We have to cleanup the config dict, as default values could enable features -        # which are not explicitly enabled on the CLI. Example: server mfa totp -        # originate comes with defaults, which will enable the -        # totp plugin, even when not set via CLI so we -        # need to check this first and drop those keys -        if dict_search('server.mfa.totp', tmp) == None: -            del openvpn['server']['mfa'] -      openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) +    if 'deleted' in openvpn: +        return openvpn + +    openvpn['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), +                                        get_first_key=True, +                                        no_tag_node_value_mangle=True) + +    if is_node_changed(conf, base + [ifname, 'openvpn-option']): +        openvpn.update({'restart_required': {}}) +    if is_node_changed(conf, base + [ifname, 'enable-dco']): +        openvpn.update({'restart_required': {}}) + +    # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict' +    # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there. +    tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True) + +    # We have to cleanup the config dict, as default values could enable features +    # which are not explicitly enabled on the CLI. Example: server mfa totp +    # originate comes with defaults, which will enable the +    # totp plugin, even when not set via CLI so we +    # need to check this first and drop those keys +    if dict_search('server.mfa.totp', tmp) == None: +        del openvpn['server']['mfa'] + +    # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all +    # OpenVPN interfaces. Check if DCO is used by any other interface instance. +    tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) +    for interface, interface_config in tmp.items(): +        # If one interface has DCO configured, enable it. No need to further check +        # all other OpenVPN interfaces. We must use a dedicated key to indicate +        # the Kernel module must be loaded or not. The per interface "offload.dco" +        # key is required per OpenVPN interface instance. +        if dict_search('offload.dco', interface_config) != None: +            openvpn['module_load_dco'] = {} +            break +      return openvpn  def is_ec_private_key(pki, cert_name): @@ -670,6 +687,15 @@ def apply(openvpn):          if interface in interfaces():              VTunIf(interface).remove() +    # dynamically load/unload DCO Kernel extension if requested +    dco_module = 'ovpn_dco_v2' +    if 'module_load_dco' in openvpn: +        check_kmod(dco_module) +    else: +        unload_kmod(dco_module) + +    # Now bail out early if interface is disabled or got deleted +    if 'deleted' in openvpn or 'disable' in openvpn:          return None      # verify specified IP address is present on any interface on this system diff --git a/src/conf_mode/interfaces-wwan.py b/src/conf_mode/interfaces-wwan.py index 6658ca86a..2515dc838 100755 --- a/src/conf_mode/interfaces-wwan.py +++ b/src/conf_mode/interfaces-wwan.py @@ -75,7 +75,6 @@ def get_config(config=None):      # We need to know the amount of other WWAN interfaces as ModemManager needs      # to be started or stopped. -    conf.set_level(base)      wwan['other_interfaces'] = conf.get_config_dict([], key_mangling=('-', '_'),                                                         get_first_key=True,                                                         no_tag_node_value_mangle=True) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 5f4b658f8..e19b12937 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -72,6 +72,7 @@ def verify_rule(config, err_msg, groups_dict):      """ Common verify steps used for both source and destination NAT """      if (dict_search('translation.port', config) != None or +        dict_search('translation.redirect.port', config) != None or          dict_search('destination.port', config) != None or          dict_search('source.port', config)): @@ -221,7 +222,7 @@ def verify(nat):              elif config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():                  Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') -            if not dict_search('translation.address', config) and not dict_search('translation.port', config): +            if not dict_search('translation.address', config) and not dict_search('translation.port', config) and not dict_search('translation.redirect.port', config):                  if 'exclude' not in config:                      raise ConfigError(f'{err_msg} translation requires address and/or port') diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 917f6e058..1cc23a7df 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -24,6 +24,7 @@ from vyos.utils.process import call  from vyos.utils.permission import chmod_750  from vyos.utils.network import get_interface_config  from vyos.template import render +from vyos.template import is_ipv4  from vyos import ConfigError  from vyos import airbag  airbag.enable() @@ -62,16 +63,29 @@ def verify(ntp):      if 'interface' in ntp:          # If ntpd should listen on a given interface, ensure it exists -        for interface in ntp['interface']: -            verify_interface_exists(interface) - -            # If we run in a VRF, our interface must belong to this VRF, too -            if 'vrf' in ntp: -                tmp = get_interface_config(interface) -                vrf_name = ntp['vrf'] -                if 'master' not in tmp or tmp['master'] != vrf_name: -                    raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\ -                                      f'does not belong to this VRF!') +        interface = ntp['interface'] +        verify_interface_exists(interface) + +        # If we run in a VRF, our interface must belong to this VRF, too +        if 'vrf' in ntp: +            tmp = get_interface_config(interface) +            vrf_name = ntp['vrf'] +            if 'master' not in tmp or tmp['master'] != vrf_name: +                raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\ +                                  f'does not belong to this VRF!') + +    if 'listen_address' in ntp: +        ipv4_addresses = 0 +        ipv6_addresses = 0 +        for address in ntp['listen_address']: +            if is_ipv4(address): +                ipv4_addresses += 1 +            else: +                ipv6_addresses += 1 +        if ipv4_addresses > 1: +            raise ConfigError(f'NTP Only admits one ipv4 value for listen-address parameter ') +        if ipv6_addresses > 1: +            raise ConfigError(f'NTP Only admits one ipv6 value for listen-address parameter ')      return None diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index cec025fea..7b9f15505 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -475,6 +475,8 @@ def verify(bgp):                      if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']):                          raise ConfigError(                              'Command "import vrf" conflicts with "rd vpn export" command!') +                    if not dict_search('parameters.router_id', bgp): +                        Warning(f'BGP "router-id" is required when using "rd" and "route-target"!')                  if dict_search('route_target.vpn.both', afi_config):                      if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 509d4f501..f2075d25b 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -88,6 +88,8 @@ def get_config(config=None):          del default_values['area']['area_type']['nssa']      if 'mpls_te' not in ospf:          del default_values['mpls_te'] +    if 'graceful_restart' not in ospf: +        del default_values['graceful_restart']      for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static', 'table']:          # table is a tagNode thus we need to clean out all occurances for the diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index 7f50d8624..fbea51f56 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -83,6 +83,8 @@ def get_config(config=None):      # need to check this first and probably drop that key.      if dict_search('default_information.originate', ospfv3) is None:          del default_values['default_information'] +    if 'graceful_restart' not in ospfv3: +        del default_values['graceful_restart']      # XXX: T2665: we currently have no nice way for defaults under tag nodes,      # clean them out and add them manually :( diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 7b6150696..5def8d645 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -47,7 +47,7 @@ def get_config(config=None):      base_path = ['protocols', 'static']      # eqivalent of the C foo ? 'a' : 'b' statement      base = vrf and ['vrf', 'name', vrf, 'protocols', 'static'] or base_path -    static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) +    static = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True)      # Assign the name of our VRF context      if vrf: static['vrf'] = vrf diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 273475c18..afd75913e 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -389,7 +389,7 @@ def apply(login):                  # command until user is removed - userdel might return 8 as                  # SSH sessions are not all yet properly cleaned away, thus we                  # simply re-run the command until the account wen't away -                while run(f'userdel --remove {user}', stderr=DEVNULL): +                while run(f'userdel {user}', stderr=DEVNULL):                      sleep(0.250)              except Exception as e: diff --git a/src/etc/modprobe.d/openvpn.conf b/src/etc/modprobe.d/openvpn.conf new file mode 100644 index 000000000..a9259fea2 --- /dev/null +++ b/src/etc/modprobe.d/openvpn.conf @@ -0,0 +1 @@ +blacklist ovpn-dco-v2 diff --git a/src/migration-scripts/container/0-to-1 b/src/migration-scripts/container/0-to-1 index 9fcf295e8..86f89ee04 100755 --- a/src/migration-scripts/container/0-to-1 +++ b/src/migration-scripts/container/0-to-1 @@ -39,12 +39,12 @@ config = ConfigTree(config_file)  if config.exists(base):      for container in config.list_nodes(base):          # Stop any given container first -        call(f'systemctl stop vyos-container-{container}.service') +        call(f'sudo systemctl stop vyos-container-{container}.service')          # Export container image for later re-import to new filesystem. We store          # the backup on a real disk as a tmpfs (like /tmp) could probably lack          # memory if a host has too many containers stored.          image_name = config.return_value(base + [container, 'image']) -        call(f'podman image save --quiet --output /root/{container}.tar --format oci-archive {image_name}') +        call(f'sudo podman image save --quiet --output /root/{container}.tar --format oci-archive {image_name}')  # No need to adjust the strage driver online (this is only used for testing and  # debugging on a live system) - it is already overlay2 when the migration script @@ -66,10 +66,10 @@ if config.exists(base):          # Export container image for later re-import to new filesystem          image_name = config.return_value(base + [container, 'image'])          image_path = f'/root/{container}.tar' -        call(f'podman image load --quiet --input {image_path}') +        call(f'sudo podman image load --quiet --input {image_path}')          # Start any given container first -        call(f'systemctl start vyos-container-{container}.service') +        call(f'sudo systemctl start vyos-container-{container}.service')          # Delete temporary container image          if os.path.exists(image_path): diff --git a/src/migration-scripts/ntp/2-to-3 b/src/migration-scripts/ntp/2-to-3 new file mode 100755 index 000000000..7d4e0bd83 --- /dev/null +++ b/src/migration-scripts/ntp/2-to-3 @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2023 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program.  If not, see <http://www.gnu.org/licenses/>. + +# T5154: allow only one ip address per family for parameter 'listen-address' +# Allow only one interface for parameter 'interface' +# If more than one are specified, remove such entries + +import sys + +from vyos.configtree import ConfigTree +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 + +if (len(sys.argv) < 1): +    print("Must specify file name!") +    sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +config = ConfigTree(config_file) + +base_path = ['service', 'ntp'] +if not config.exists(base_path): +    # Nothing to do +    sys.exit(0) + +if config.exists(base_path + ['listen-address']) and (len([addr for addr in config.return_values(base_path + ['listen-address']) if is_ipv4(addr)]) > 1): +    for addr in config.return_values(base_path + ['listen-address']): +        if is_ipv4(addr): +            config.delete_value(base_path + ['listen-address'], addr) + +if config.exists(base_path + ['listen-address']) and (len([addr for addr in config.return_values(base_path + ['listen-address']) if is_ipv6(addr)]) > 1): +    for addr in config.return_values(base_path + ['listen-address']): +        if is_ipv6(addr): +            config.delete_value(base_path + ['listen-address'], addr) + +if config.exists(base_path + ['interface']): +    if len(config.return_values(base_path + ['interface'])) > 1: +        config.delete(base_path + ['interface']) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    sys.exit(1) diff --git a/src/op_mode/bridge.py b/src/op_mode/bridge.py index 5531c41d0..1834b9cc9 100755 --- a/src/op_mode/bridge.py +++ b/src/op_mode/bridge.py @@ -24,6 +24,7 @@ from tabulate import tabulate  from vyos.utils.process import cmd  from vyos.utils.process import rc_cmd +from vyos.utils.process	import call  from vyos.utils.dict import dict_search  import vyos.opmode @@ -129,7 +130,8 @@ def _get_formatted_output_vlan(data):              if vlan_entry.get('vlanEnd'):                  vlan_end = vlan_entry.get('vlanEnd')                  vlan = f'{vlan}-{vlan_end}' -            flags = ', '.join(vlan_entry.get('flags')).lower() +            flags_raw = vlan_entry.get('flags') +            flags = ', '.join(flags_raw if isinstance(flags_raw,list) else "").lower()              data_entries.append([interface, vlan, flags])      headers = ["Interface", "Vlan", "Flags"] @@ -164,6 +166,23 @@ def _get_formatted_output_mdb(data):      output = tabulate(data_entries, headers)      return output +def _get_bridge_detail(iface): +    """Get interface detail statistics""" +    return call(f'vtysh -c "show interface {iface}"') + +def _get_bridge_detail_nexthop_group(iface): +    """Get interface detail nexthop_group statistics""" +    return call(f'vtysh -c "show interface {iface} nexthop-group"') + +def _get_bridge_detail_nexthop_group_raw(iface): +    out = cmd(f'vtysh -c "show interface {iface} nexthop-group"') +    return out + +def _get_bridge_detail_raw(iface): +    """Get interface detail json statistics""" +    data =  cmd(f'vtysh -c "show interface {iface} json"') +    data_dict = json.loads(data) +    return data_dict  def show(raw: bool):      bridge_data = _get_raw_data_summary() @@ -196,6 +215,17 @@ def show_mdb(raw: bool, interface: str):      else:          return _get_formatted_output_mdb(mdb_data) +def show_detail(raw: bool, nexthop_group: typing.Optional[bool], interface: str): +    if raw: +        if nexthop_group: +            return _get_bridge_detail_nexthop_group_raw(interface) +        else: +            return _get_bridge_detail_raw(interface) +    else: +        if nexthop_group: +            return _get_bridge_detail_nexthop_group(interface) +        else: +            return _get_bridge_detail(interface)  if __name__ == '__main__':      try: diff --git a/src/op_mode/show_ntp.sh b/src/op_mode/show_ntp.sh index 85f8eda15..4b59b801e 100755 --- a/src/op_mode/show_ntp.sh +++ b/src/op_mode/show_ntp.sh @@ -18,7 +18,7 @@ if ! ps -C chronyd &>/dev/null; then  fi  PID=$(pgrep chronyd | head -n1) -VRF_NAME=$(ip vrf identify ) +VRF_NAME=$(ip vrf identify ${PID})  if [ ! -z ${VRF_NAME} ]; then      VRF_CMD="sudo ip vrf exec ${VRF_NAME}" | 
