diff options
author | Christian Poessinger <christian@poessinger.com> | 2020-05-05 07:21:34 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-05 07:21:34 +0200 |
commit | 9afb06a0187c437f01e5de498851b94025cf9dd8 (patch) | |
tree | cf76b8e4c4d866b4ad9ee467692bebdfa873a382 | |
parent | 66ecfc6ecada4e50562dcfa4635b83daa5b0b1ff (diff) | |
parent | b16dc5044eb9735c51ea211ea00fa35297d921f3 (diff) | |
download | vyos-1x-9afb06a0187c437f01e5de498851b94025cf9dd8.tar.gz vyos-1x-9afb06a0187c437f01e5de498851b94025cf9dd8.zip |
Merge pull request #384 from jjakob/bridge-fix-T2241
T2241: fix interfaces falling out of bridge
-rw-r--r-- | data/templates/openvpn/server.conf.tmpl | 2 | ||||
-rw-r--r-- | python/vyos/configdict.py | 29 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 20 | ||||
-rw-r--r-- | python/vyos/ifconfig/section.py | 21 | ||||
-rw-r--r-- | python/vyos/ifconfig_vlan.py | 68 | ||||
-rw-r--r-- | python/vyos/util.py | 71 | ||||
-rw-r--r-- | python/vyos/validate.py | 66 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bonding.py | 121 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bridge.py | 83 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-dummy.py | 36 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-ethernet.py | 52 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-geneve.py | 22 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-l2tpv3.py | 28 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 41 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-pseudo-ethernet.py | 48 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-tunnel.py | 43 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-vxlan.py | 28 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireguard.py | 53 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireless.py | 43 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wirelessmodem.py | 37 |
20 files changed, 638 insertions, 274 deletions
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index 75ab602f8..401f8e04b 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -74,7 +74,7 @@ nobind topology {% if server_topology == 'point-to-point' %}p2p{% else %}{{ server_topology }}{% endif %} {%- endif %} -{%- if bridge_member %} +{%- if is_bridge_member %} mode server tls-server {%- else %} diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index e1b704a31..5ab831258 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -23,7 +23,8 @@ from copy import deepcopy from vyos import ConfigError from vyos.ifconfig import Interface - +from vyos.validate import is_member +from vyos.util import ifname_from_config def retrieve_config(path_hash, base_path, config): """ @@ -128,6 +129,7 @@ vlan_default = { 'ipv6_dup_addr_detect': 1, 'ingress_qos': '', 'ingress_qos_changed': False, + 'is_bridge_member': False, 'mac': '', 'mtu': 1500, 'vif_c': [], @@ -197,6 +199,7 @@ def intf_to_dict(conf, default): """ intf = deepcopy(default) + intf['intf'] = ifname_from_config(conf) # retrieve configured interface addresses if conf.exists('address'): @@ -263,15 +266,18 @@ def intf_to_dict(conf, default): if conf.exists('ipv6 disable-forwarding'): intf['ipv6_forwarding'] = 0 - # Media Access Control (MAC) address - if conf.exists('mac'): - intf['mac'] = conf.return_value('mac') + # check if interface is member of a bridge + intf['is_bridge_member'] = is_member(conf, intf['intf'], 'bridge') # IPv6 Duplicate Address Detection (DAD) tries if conf.exists('ipv6 dup-addr-detect-transmits'): intf['ipv6_dup_addr_detect'] = int( conf.return_value('ipv6 dup-addr-detect-transmits')) + # Media Access Control (MAC) address + if conf.exists('mac'): + intf['mac'] = conf.return_value('mac') + # Maximum Transmission Unit (MTU) if conf.exists('mtu'): intf['mtu'] = int(conf.return_value('mtu')) @@ -338,14 +344,15 @@ def intf_to_dict(conf, default): intf['ipv6_eui64_prefix'] = act_eui intf['ipv6_eui64_prefix_remove'] = list_diff(eff_eui, act_eui) - # Remove the default link-local address if set. - if conf.exists('ipv6 address no-default-link-local'): + # Remove the default link-local address if set or if member of a bridge + if ( conf.exists('ipv6 address no-default-link-local') + or intf.get('is_bridge_member') ): intf['ipv6_eui64_prefix_remove'].append('fe80::/64') else: # add the link-local by default to make IPv6 work intf['ipv6_eui64_prefix'].append('fe80::/64') - # Find out if MAC has changed + # If MAC has changed, remove and re-add all IPv6 EUI64 addresses try: interface = Interface(intf['intf'], create=False) if intf['mac'] and intf['mac'] != interface.get_mac(): @@ -416,13 +423,13 @@ def add_to_dict(conf, disabled, ifdict, section, key): def vlan_to_dict(conf, default=vlan_default): vlan, disabled = intf_to_dict(conf, default) - # get the '100' in 'interfaces bonding bond0 vif-s 100 - vlan['id'] = conf.get_level()[-1] - current_level = conf.get_level() + level = conf.get_level() + # get the '100' in 'interfaces bonding bond0 vif-s 100' + vlan['id'] = level[-1] # if this is a not within vif-s node, we are done - if current_level[-2] != 'vif-s': + if level[-2] != 'vif-s': return vlan # ethertype is mandatory on vif-s nodes and only exists here! diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 5b3da228e..7b42e3399 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -42,6 +42,7 @@ from vyos.ifconfig.control import Control from vyos.ifconfig.dhcp import DHCP from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.operational import Operational +from vyos.ifconfig import Section class Interface(Control): @@ -718,3 +719,22 @@ class Interface(Control): # flush all addresses self._cmd(f'ip addr flush dev "{self.ifname}"') + + def add_to_bridge(self, br): + """ + Adds the interface to the bridge with the passed port config. + + Returns False if bridge doesn't exist. + """ + + # check if the bridge exists (on boot it doesn't) + if br not in Section.interfaces('bridge'): + return False + + self.flush_addrs() + # add interface to bridge - use Section.klass to get BridgeIf class + Section.klass(br)(br, create=False).add_port(self.ifname) + + # TODO: port config (STP) + + return True diff --git a/python/vyos/ifconfig/section.py b/python/vyos/ifconfig/section.py index 092236fef..926c22e8a 100644 --- a/python/vyos/ifconfig/section.py +++ b/python/vyos/ifconfig/section.py @@ -54,7 +54,7 @@ class Section: name = name.rstrip('0123456789') name = name.rstrip('.') if vlan: - name = name.rstrip('0123456789') + name = name.rstrip('0123456789.') return name @classmethod @@ -137,3 +137,22 @@ class Section: eth, lo, vxlan, dum, ... """ return list(cls._prefixes.keys()) + + @classmethod + def get_config_path(cls, name): + """ + get config path to interface with .vif or .vif-s.vif-c + example: eth0.1.2 -> 'ethernet eth0 vif-s 1 vif-c 2' + Returns False if interface name is invalid (not found in sections) + """ + sect = cls.section(name) + if sect: + splinterface = name.split('.') + intfpath = f'{sect} {splinterface[0]}' + if len(splinterface) == 2: + intfpath += f' vif {splinterface[1]}' + elif len(splinterface) == 3: + intfpath += f' vif-s {splinterface[1]} vif-c {splinterface[2]}' + return intfpath + else: + return False diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py index ee009f7f9..eb7a369ec 100644 --- a/python/vyos/ifconfig_vlan.py +++ b/python/vyos/ifconfig_vlan.py @@ -63,8 +63,10 @@ def apply_vlan_config(vlan, config): # Maximum Transmission Unit (MTU) vlan.set_mtu(config['mtu']) - # assign/remove VRF - vlan.set_vrf(config['vrf']) + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not config['is_bridge_member']: + vlan.set_vrf(config['vrf']) # Delete old IPv6 EUI64 addresses before changing MAC for addr in config['ipv6_eui64_prefix_remove']: @@ -92,6 +94,10 @@ def apply_vlan_config(vlan, config): for addr in config['address']: vlan.add_addr(addr) + # re-add ourselves to any bridge we might have fallen out of + if config['is_bridge_member']: + vlan.add_to_bridge(config['is_bridge_member']) + def verify_vlan_config(config): """ Generic function to verify VLAN config consistency. Instead of re- @@ -103,9 +109,22 @@ def verify_vlan_config(config): if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - vrf_name = vif['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + if ( vif['is_bridge_member'] + and ( vif['address'] + or vif['ipv6_eui64_prefix'] + or vif['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to vif interface {vif["intf"]} ' + f'which is a member of bridge {vif["is_bridge_member"]}')) + + if vif['vrf']: + if vif['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{vif["vrf"]}" does not exist') + + if vif['is_bridge_member']: + raise ConfigError(( + f'vif {vif["intf"]} cannot be member of VRF {vif["vrf"]} ' + f'and bridge {vif["is_bridge_member"]} at the same time!')) # e.g. wireless interface has no vif_s support # thus we bail out eraly. @@ -121,17 +140,42 @@ def verify_vlan_config(config): if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - vrf_name = vif_s['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + if ( vif_s['is_bridge_member'] + and ( vif_s['address'] + or vif_s['ipv6_eui64_prefix'] + or vif_s['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to vif-s interface {vif_s["intf"]} ' + f'which is a member of bridge {vif_s["is_bridge_member"]}')) + + if vif_s['vrf']: + if vif_s['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{vif_s["vrf"]}" does not exist') + + if vif_s['is_bridge_member']: + raise ConfigError(( + f'vif-s {vif_s["intf"]} cannot be member of VRF {vif_s["vrf"]} ' + f'and bridge {vif_s["is_bridge_member"]} at the same time!')) for vif_c in vif_s['vif_c']: # DHCPv6 parameters-only and temporary address are mutually exclusive if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - vrf_name = vif_c['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') - + if ( vif_c['is_bridge_member'] + and ( vif_c['address'] + or vif_c['ipv6_eui64_prefix'] + or vif_c['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to vif-c interface {vif_c["intf"]} ' + f'which is a member of bridge {vif_c["is_bridge_member"]}')) + + if vif_c['vrf']: + if vif_c['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{vif_c["vrf"]}" does not exist') + + if vif_c['is_bridge_member']: + raise ConfigError(( + f'vif-c {vif_c["intf"]} cannot be member of VRF {vif_c["vrf"]} ' + f'and bridge {vif_c["is_bridge_member"]} at the same time!')) diff --git a/python/vyos/util.py b/python/vyos/util.py index 92b6f7992..e598e0ff3 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -498,3 +498,74 @@ def get_half_cpus(): if cpu > 1: cpu /= 2 return int(cpu) + +def ifname_from_config(conf): + """ + Gets interface name with VLANs from current config level. + Level must be at the interface whose name we want. + + Example: + >>> from vyos.util import ifname_from_config + >>> from vyos.config import Config + >>> conf = Config() + >>> conf.set_level('interfaces ethernet eth0 vif-s 1 vif-c 2') + >>> ifname_from_config(conf) + 'eth0.1.2' + """ + level = conf.get_level() + + # vlans + if level[-2] == 'vif' or level[-2] == 'vif-s': + return level[-3] + '.' + level[-1] + if level[-2] == 'vif-c': + return level[-5] + '.' + level[-3] + '.' + level[-1] + + # no vlans + return level[-1] + +def get_bridge_member_config(conf, br, intf): + """ + Gets bridge port (member) configuration + + Arguments: + conf: Config + br: bridge name + intf: interface name + + Returns: + dict with the configuration + False if bridge or bridge port doesn't exist + """ + old_level = conf.get_level() + conf.set_level([]) + + bridge = f'interfaces bridge {br}' + member = f'{bridge} member interface {intf}' + if not ( conf.exists(bridge) and conf.exists(member) ): + return False + + # default bridge port configuration + # cost and priority initialized with linux defaults + # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority} + # after adding interface to bridge after reboot + memberconf = { + 'cost': 100, + 'priority': 32, + 'arp_cache_tmo': 30, + 'disable_link_detect': 1, + } + + if conf.exists(f'{member} cost'): + memberconf['cost'] = int(conf.return_value(f'{member} cost')) + + if conf.exists(f'{member} priority'): + memberconf['priority'] = int(conf.return_value(f'{member} priority')) + + if conf.exists(f'{bridge} ip arp-cache-timeout'): + memberconf['arp_cache_tmo'] = int(conf.return_value(f'{bridge} ip arp-cache-timeout')) + + if conf.exists(f'{bridge} disable-link-detect'): + memberconf['disable_link_detect'] = 2 + + conf.set_level(old_level) + return memberconf diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 446f6e4ca..e083604c5 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -241,26 +241,66 @@ def assert_mac(m): if octets[:5] == (0, 0, 94, 0, 1): raise ValueError(f'{m} is a VRRP MAC address') -def is_bridge_member(conf, interface): +def is_member(conf, interface, intftype=None): """ - Checks if passed interfaces is part of a bridge device or not. - - Returns a tuple: - None -> Interface not a bridge member - Bridge -> Interface is a member of this bridge + Checks if passed interface is member of other interface of specified type. + intftype is optional, if not passed it will search all known types + (currently bridge and bonding) + + Returns: + None -> Interface is not a member + interface name -> Interface is a member of this interface + False -> interface type cannot have members """ ret_val = None - old_level = conf.get_level() + + if intftype not in ['bonding', 'bridge', None]: + raise ValueError(( + f'unknown interface type "{intftype}" or it cannot ' + f'have member interfaces')) + + intftype = ['bonding', 'bridge'] if intftype == None else [intftype] # set config level to root + old_level = conf.get_level() conf.set_level([]) - base = ['interfaces', 'bridge'] - for bridge in conf.list_nodes(base): - members = conf.list_nodes(base + [bridge, 'member', 'interface']) - if interface in members: - ret_val = bridge - break + + for it in intftype: + base = 'interfaces ' + it + for intf in conf.list_nodes(base): + memberintf = f'{base} {intf} member interface' + if conf.is_tag(memberintf): + if interface in conf.list_nodes(memberintf): + ret_val = intf + break + elif conf.is_leaf(memberintf): + if ( conf.exists(memberintf) and + interface in conf.return_values(memberintf) ): + ret_val = intf + break old_level = conf.set_level(old_level) return ret_val +def has_address_configured(conf, intf): + """ + Checks if interface has an address configured. + Checks the following config nodes: + 'address', 'ipv6 address eui64', 'ipv6 address autoconf' + + Returns True if interface has address configured, False if it doesn't. + """ + from vyos.ifconfig import Section + ret = False + + old_level = conf.get_level() + conf.set_level([]) + + intfpath = 'interfaces ' + Section.get_config_path(intf) + if ( conf.exists(f'{intfpath} address') or + conf.exists(f'{intfpath} ipv6 address autoconf') or + conf.exists(f'{intfpath} ipv6 address eui64') ): + ret = True + + conf.set_level(old_level) + return ret diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py index a174e33e4..82b7c0f2a 100755 --- a/src/conf_mode/interfaces-bonding.py +++ b/src/conf_mode/interfaces-bonding.py @@ -20,12 +20,12 @@ from copy import deepcopy from sys import exit from netifaces import interfaces -from vyos.ifconfig import BondIf, Section +from vyos.ifconfig import BondIf from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, intf_to_dict, add_to_dict from vyos.config import Config from vyos.util import call, cmd -from vyos.validate import is_bridge_member +from vyos.validate import is_member, has_address_configured from vyos import ConfigError default_config_data = { @@ -111,15 +111,12 @@ def get_config(): bond = deepcopy(default_config_data) bond['intf'] = ifname bond['deleted'] = True - # check if interface is member if a bridge - bond['is_bridge_member'] = is_bridge_member(conf, ifname) return bond # set new configuration level conf.set_level(cfg_base) bond, disabled = intf_to_dict(conf, default_config_data) - bond['intf'] = ifname # ARP link monitoring frequency in milliseconds if conf.exists('arp-monitor interval'): @@ -175,22 +172,38 @@ def get_config(): def verify(bond): if bond['deleted']: if bond['is_bridge_member']: - interface = bond['intf'] - bridge = bond['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Cannot delete interface "{bond["intf"]}" as it is a ' + f'member of bridge "{bond["is_bridge_member"]}"!')) + return None - if len (bond['arp_mon_tgt']) > 16: - raise ConfigError('The maximum number of targets that can be specified is 16') + if len(bond['arp_mon_tgt']) > 16: + raise ConfigError('The maximum number of arp-monitor targets is 16') if bond['primary']: if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: - raise ConfigError('Mode dependency failed, primary not supported ' \ - 'in mode "{}"!'.format(bond['mode'])) + raise ConfigError(( + 'Mode dependency failed, primary not supported in mode ' + f'"{bond["mode"]}"!')) + + if ( bond['is_bridge_member'] + and ( bond['address'] + or bond['ipv6_eui64_prefix'] + or bond['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to interface "{bond["intf"]}" ' + f'as it is a member of bridge "{bond["is_bridge_member"]}"!')) + + if bond['vrf']: + if bond['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{bond["vrf"]}" does not exist') - vrf_name = bond['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + if bond['is_bridge_member']: + raise ConfigError(( + f'Interface "{bond["intf"]}" cannot be member of VRF ' + f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} ' + f'at the same time!')) # use common function to verify VLAN configuration verify_vlan_config(bond) @@ -199,51 +212,55 @@ def verify(bond): for intf in bond['member']: # check if member interface is "real" if intf not in interfaces(): - raise ConfigError('interface {} does not exist!'.format(intf)) + raise ConfigError(f'Interface {intf} does not exist!') # a bonding member interface is only allowed to be assigned to one bond! all_bonds = conf.list_nodes('interfaces bonding') # We do not need to check our own bond all_bonds.remove(bond['intf']) for tmp in all_bonds: - if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf): - raise ConfigError('can not enslave interface {} which already ' \ - 'belongs to {}'.format(intf, tmp)) + if conf.exists('interfaces bonding {tmp} member interface {intf}'): + raise ConfigError(( + f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' + f'it is already a member of bond "{tmp}"!')) # can not add interfaces with an assigned address to a bond - if conf.exists('interfaces ethernet ' + intf + ' address'): - raise ConfigError('can not enslave interface {} which has an address ' \ - 'assigned'.format(intf)) - - # bond members are not allowed to be bridge members, too - for tmp in conf.list_nodes('interfaces bridge'): - if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf): - raise ConfigError('can not enslave interface {} which belongs to ' \ - 'bridge {}'.format(intf, tmp)) - - # bond members are not allowed to be vrrp members, too + if has_address_configured(conf, intf): + raise ConfigError(( + f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' + f'it has an address assigned!')) + + # bond members are not allowed to be bridge members + tmp = is_member(conf, intf, 'bridge') + if tmp: + raise ConfigError(( + f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' + f'it is already a member of bridge "{tmp}"!')) + + # bond members are not allowed to be vrrp members for tmp in conf.list_nodes('high-availability vrrp group'): - if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf): - raise ConfigError('can not enslave interface {} which belongs to ' \ - 'VRRP group {}'.format(intf, tmp)) + if conf.exists('high-availability vrrp group {tmp} interface {intf}'): + raise ConfigError(( + f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' + f'it is already a member of VRRP group "{tmp}"!')) # bond members are not allowed to be underlaying psuedo-ethernet devices for tmp in conf.list_nodes('interfaces pseudo-ethernet'): - if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf): - raise ConfigError('can not enslave interface {} which belongs to ' \ - 'pseudo-ethernet {}'.format(intf, tmp)) + if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'): + raise ConfigError(( + f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' + f'it is already the link of pseudo-ethernet "{tmp}"!')) # bond members are not allowed to be underlaying vxlan devices for tmp in conf.list_nodes('interfaces vxlan'): - if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf): - raise ConfigError('can not enslave interface {} which belongs to ' \ - 'vxlan {}'.format(intf, tmp)) - + if conf.exists('interfaces vxlan {tmp} link {intf}'): + raise ConfigError(( + f'Cannot add interface "{intf}" to bond "{bond["intf"]}", ' + f'it is already the link of VXLAN "{tmp}"!')) if bond['primary']: if bond['primary'] not in bond['member']: - raise ConfigError('primary interface must be a member interface of {}' \ - .format(bond['intf'])) + raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member') if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']: raise ConfigError('primary interface only works for mode active-backup, ' \ @@ -256,7 +273,6 @@ def verify(bond): return None - def generate(bond): return None @@ -365,12 +381,9 @@ def apply(bond): # Add (enslave) interfaces to bond for intf in bond['member']: - # flushes only children of Interfaces class (e.g. vlan are not) - if intf in Section.interfaces(): - klass = Section.klass(intf, vlan=False) - klass(intf, create=False).flush_addrs() - # flushes also vlan interfaces - call(f'ip addr flush dev "{intf}"') + # if we've come here we already verified the interface doesn't + # have addresses configured so just flush any remaining ones + cmd(f'ip addr flush dev "{intf}"') b.add_port(intf) # As the bond interface is always disabled first when changing @@ -389,8 +402,14 @@ def apply(bond): for addr in bond['address']: b.add_addr(addr) - # assign/remove VRF - b.set_vrf(bond['vrf']) + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not bond['is_bridge_member']: + b.set_vrf(bond['vrf']) + + # re-add ourselves to any bridge we might have fallen out of + if bond['is_bridge_member']: + b.add_to_bridge(bond['is_bridge_member']) # remove no longer required service VLAN interfaces (vif-s) for vif_s in bond['vif_s_remove']: diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 9d638653c..c43fae78b 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -23,8 +23,9 @@ from netifaces import interfaces from vyos.ifconfig import BridgeIf, Section from vyos.ifconfig.stp import STP from vyos.configdict import list_diff +from vyos.validate import is_member, has_address_configured from vyos.config import Config -from vyos.util import cmd +from vyos.util import cmd, get_bridge_member_config from vyos import ConfigError default_config_data = { @@ -202,22 +203,12 @@ def get_config(): # Determine bridge member interface (currently configured) for intf in conf.list_nodes('member interface'): - # cost and priority initialized with linux defaults - # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority} - # after adding interface to bridge after reboot - iface = { - 'name': intf, - 'cost': 100, - 'priority': 32 - } - - if conf.exists('member interface {} cost'.format(intf)): - iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf))) - - if conf.exists('member interface {} priority'.format(intf)): - iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf))) - - bridge['member'].append(iface) + # defaults are stored in util.py (they can't be here as all interface + # scripts use the function) + memberconf = get_bridge_member_config(conf, bridge['intf'], intf) + if memberconf: + memberconf['name'] = intf + bridge['member'].append(memberconf) # Determine bridge member interface (currently effective) - to determine which # interfaces is no longer assigend to the bridge and thus can be removed @@ -248,30 +239,40 @@ def verify(bridge): raise ConfigError(f'VRF "{vrf_name}" does not exist') conf = Config() - for br in conf.list_nodes('interfaces bridge'): - # it makes no sense to verify ourself in this case - if br == bridge['intf']: - continue - - for intf in bridge['member']: - tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br)) - if intf['name'] in tmp: - raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf'])) - - # the interface must exist prior adding it to a bridge for intf in bridge['member']: + # the interface must exist prior adding it to a bridge if intf['name'] not in interfaces(): - raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf'])) + raise ConfigError(( + f'Cannot add nonexistent interface "{intf["name"]}" ' + f'to bridge "{bridge["intf"]}"')) if intf['name'] == 'lo': raise ConfigError('Loopback interface "lo" can not be added to a bridge') - # bridge members are not allowed to be bond members, too - for intf in bridge['member']: - for bond in conf.list_nodes('interfaces bonding'): - if conf.exists('interfaces bonding ' + bond + ' member interface'): - if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'): - raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf'])) + # bridge members aren't allowed to be members of another bridge + for br in conf.list_nodes('interfaces bridge'): + # it makes no sense to verify ourself in this case + if br == bridge['intf']: + continue + + tmp = conf.list_nodes(f'interfaces bridge {br} member interface') + if intf['name'] in tmp: + raise ConfigError(( + f'Cannot add interface "{intf["name"]}" to bridge ' + f'"{bridge["intf"]}", it is already a member of bridge "{br}"!')) + + # bridge members are not allowed to be bond members + tmp = is_member(conf, intf['name'], 'bonding') + if tmp: + raise ConfigError(( + f'Cannot add interface "{intf["name"]}" to bridge ' + f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!')) + + # bridge members must not have an assigned address + if has_address_configured(conf, intf['name']): + raise ConfigError(( + f'Cannot add interface "{intf["name"]}" to bridge ' + f'"{bridge["intf"]}", it has an address assigned!')) return None @@ -347,12 +348,8 @@ def apply(bridge): # add interfaces to bridge for member in bridge['member']: - # flushes address of only children of Interfaces class - # (e.g. vlan are not) - if member['name'] in Section.interfaces(): - klass = Section.klass(member['name'], vlan=False) - klass(member['name'], create=False).flush_addrs() - # flushes all interfaces + # if we've come here we already verified the interface doesn't + # have addresses configured so just flush any remaining ones cmd(f'ip addr flush dev "{member["name"]}"') br.add_port(member['name']) @@ -382,9 +379,9 @@ def apply(bridge): for member in bridge['member']: i = STPBridgeIf(member['name']) # configure ARP cache timeout - i.set_arp_cache_tmo(bridge['arp_cache_tmo']) + i.set_arp_cache_tmo(member['arp_cache_tmo']) # ignore link state changes - i.set_link_detect(bridge['disable_link_detect']) + i.set_link_detect(member['disable_link_detect']) # set bridge port path cost i.set_path_cost(member['cost']) # set bridge port path priority diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py index 23eaa4ecb..4a77b0c1a 100755 --- a/src/conf_mode/interfaces-dummy.py +++ b/src/conf_mode/interfaces-dummy.py @@ -23,7 +23,7 @@ from netifaces import interfaces from vyos.ifconfig import DummyIf from vyos.configdict import list_diff from vyos.config import Config -from vyos.validate import is_bridge_member +from vyos.validate import is_member from vyos import ConfigError default_config_data = { @@ -47,11 +47,12 @@ def get_config(): dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + # check if we are a member of any bridge + dummy['is_bridge_member'] = is_member(conf, dummy['intf'], 'bridge') + # Check if interface has been removed if not conf.exists('interfaces dummy ' + dummy['intf']): dummy['deleted'] = True - # check if interface is member if a bridge - dummy['is_bridge_member'] = is_bridge_member(conf, dummy['intf']) return dummy # set new configuration level @@ -84,15 +85,26 @@ def get_config(): def verify(dummy): if dummy['deleted']: if dummy['is_bridge_member']: - interface = dummy['intf'] - bridge = dummy['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Interface "{dummy["intf"]}" cannot be deleted as it is a ' + f'member of bridge "{dummy["is_bridge_member"]}"!')) return None - vrf_name = dummy['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + if dummy['vrf']: + if dummy['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{dummy["vrf"]}" does not exist') + + if dummy['is_bridge_member']: + raise ConfigError(( + f'Interface "{dummy["intf"]}" cannot be member of VRF ' + f'"{dummy["vrf"]}" and bridge "{dummy["is_bridge_member"]}" ' + f'at the same time!')) + + if dummy['is_bridge_member'] and dummy['address']: + raise ConfigError(( + f'Cannot assign address to interface "{dummy["intf"]}" ' + f'as it is a member of bridge "{dummy["is_bridge_member"]}"!')) return None @@ -117,8 +129,10 @@ def apply(dummy): for addr in dummy['address']: d.add_addr(addr) - # assign/remove VRF - d.set_vrf(dummy['vrf']) + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not dummy['is_bridge_member']: + d.set_vrf(dummy['vrf']) # disable interface on demand if dummy['disable']: diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 3ddd394d7..9b9ae931c 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -20,9 +20,10 @@ from sys import exit from copy import deepcopy from netifaces import interfaces -from vyos.ifconfig import EthernetIf, Section +from vyos.ifconfig import EthernetIf from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.configdict import list_diff, intf_to_dict, add_to_dict +from vyos.validate import is_member from vyos.config import Config from vyos import ConfigError @@ -53,6 +54,8 @@ default_config_data = { 'ipv6_eui64_prefix_remove': [], 'ipv6_forwarding': 1, 'ipv6_dup_addr_detect': 1, + 'is_bridge_member': False, + 'is_bond_member': False, 'intf': '', 'mac': '', 'mtu': 1500, @@ -92,7 +95,6 @@ def get_config(): conf.set_level(cfg_base) eth, disabled = intf_to_dict(conf, default_config_data) - eth['intf'] = ifname # disable ethernet flow control (pause frames) if conf.exists('disable-flow-control'): @@ -114,6 +116,9 @@ def get_config(): if conf.exists('ip proxy-arp-pvlan'): eth['ip_proxy_arp_pvlan'] = 1 + # check if we are a member of any bond + eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding') + # GRO (generic receive offload) if conf.exists('offload-options generic-receive'): eth['offload_gro'] = conf.return_value('offload-options generic-receive') @@ -138,6 +143,11 @@ def get_config(): if conf.exists('speed'): eth['speed'] = conf.return_value('speed') + # remove default IPv6 link-local address if member of a bond + if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']: + eth['ipv6_eui64_prefix'].remove('fe80::/64') + eth['ipv6_eui64_prefix_remove'].append('fe80::/64') + add_to_dict(conf, disabled, eth, 'vif', 'vif') add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s') @@ -162,18 +172,24 @@ def verify(eth): if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']: raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!') - vrf_name = eth['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member'] - conf = Config() - # some options can not be changed when interface is enslaved to a bond - for bond in conf.list_nodes('interfaces bonding'): - if conf.exists('interfaces bonding ' + bond + ' member interface'): - bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface') - if eth['intf'] in bond_member: - if eth['address']: - raise ConfigError(f"Can not assign address to interface {eth['intf']} which is a member of {bond}") + if ( memberof + and ( eth['address'] + or eth['ipv6_eui64_prefix'] + or eth['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to interface "{eth["intf"]}" ' + f'as it is a member of "{memberof}"!')) + + if eth['vrf']: + if eth['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{eth["vrf"]}" does not exist') + + if memberof: + raise ConfigError(( + f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" ' + f'and "{memberof}" at the same time!')) # use common function to verify VLAN configuration verify_vlan_config(eth) @@ -281,8 +297,14 @@ def apply(eth): for addr in eth['address']: e.add_addr(addr) - # assign/remove VRF - e.set_vrf(eth['vrf']) + # assign/remove VRF (ONLY when not a member of a bridge or bond, + # otherwise 'nomaster' removes it from it) + if not ( eth['is_bridge_member'] or eth['is_bond_member'] ): + e.set_vrf(eth['vrf']) + + # re-add ourselves to any bridge we might have fallen out of + if eth['is_bridge_member']: + e.add_to_bridge(eth['is_bridge_member']) # remove no longer required service VLAN interfaces (vif-s) for vif_s in eth['vif_s_remove']: diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py index 708a64474..e4109a221 100755 --- a/src/conf_mode/interfaces-geneve.py +++ b/src/conf_mode/interfaces-geneve.py @@ -22,7 +22,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.ifconfig import GeneveIf -from vyos.validate import is_bridge_member +from vyos.validate import is_member from vyos import ConfigError default_config_data = { @@ -49,11 +49,12 @@ def get_config(): geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + # check if interface is member if a bridge + geneve['is_bridge_member'] = is_member(conf, geneve['intf'], 'bridge') + # Check if interface has been removed if not conf.exists('interfaces geneve ' + geneve['intf']): geneve['deleted'] = True - # check if interface is member if a bridge - geneve['is_bridge_member'] = is_bridge_member(conf, geneve['intf']) return geneve # set new configuration level @@ -97,12 +98,17 @@ def get_config(): def verify(geneve): if geneve['deleted']: if geneve['is_bridge_member']: - interface = geneve['intf'] - bridge = geneve['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Cannot delete interface "{geneve["intf"]}" as it is a ' + f'member of bridge "{geneve["is_bridge_member"]}"!')) return None + if geneve['is_bridge_member'] and geneve['address']: + raise ConfigError(( + f'Cannot assign address to interface "{geneve["intf"]}" ' + f'as it is a member of bridge "{geneve["is_bridge_member"]}"!')) + if not geneve['remote']: raise ConfigError('GENEVE remote must be configured') @@ -158,6 +164,10 @@ def apply(geneve): if not geneve['disable']: g.set_admin_state('up') + # re-add ourselves to any bridge we might have fallen out of + if geneve['is_bridge_member']: + g.add_to_bridge(geneve['is_bridge_member']) + return None diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py index 33cf62f70..26bb537e5 100755 --- a/src/conf_mode/interfaces-l2tpv3.py +++ b/src/conf_mode/interfaces-l2tpv3.py @@ -24,7 +24,7 @@ from vyos.config import Config from vyos.ifconfig import L2TPv3If, Interface from vyos import ConfigError from vyos.util import call -from vyos.validate import is_bridge_member, is_addr_assigned +from vyos.validate import is_member, is_addr_assigned default_config_data = { 'address': [], @@ -66,12 +66,13 @@ def get_config(): l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + # check if interface is member of a bridge + l2tpv3['is_bridge_member'] = is_member(conf, l2tpv3['intf'], 'bridge') + # Check if interface has been removed if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']): l2tpv3['deleted'] = True interface = l2tpv3['intf'] - # check if interface is member if a bridge - l2tpv3['is_bridge_member'] = is_bridge_member(conf, interface) # to delete the l2tpv3 interface we need the current tunnel_id and session_id if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'): @@ -118,7 +119,8 @@ def get_config(): l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') # Remove the default link-local address if set. - if not conf.exists('ipv6 address no-default-link-local'): + if not ( conf.exists('ipv6 address no-default-link-local') or + l2tpv3['is_bridge_member'] ): # add the link-local by default to make IPv6 work l2tpv3['ipv6_eui64_prefix'].append('fe80::/64') @@ -166,9 +168,9 @@ def verify(l2tpv3): if l2tpv3['deleted']: if l2tpv3['is_bridge_member']: - interface = l2tpv3['intf'] - bridge = l2tpv3['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Interface "{l2tpv3["intf"]}" cannot be deleted as it is a ' + f'member of bridge "{l2tpv3["is_bridge_member"]}"!')) return None @@ -193,6 +195,14 @@ def verify(l2tpv3): if not l2tpv3['peer_session_id']: raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}') + if ( l2tpv3['is_bridge_member'] + and ( l2tpv3['address'] + or l2tpv3['ipv6_eui64_prefix'] + or l2tpv3['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to interface "{l2tpv3["intf"]}" ' + f'as it is a member of bridge "{l2tpv3["is_bridge_member"]}"!')) + return None @@ -254,6 +264,10 @@ def apply(l2tpv3): if not l2tpv3['disable']: l.set_admin_state('up') + # re-add ourselves to any bridge we might have fallen out of + if l2tpv3['is_bridge_member']: + l.add_to_bridge(l2tpv3['is_bridge_member']) + return None if __name__ == '__main__': diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 029bc1d69..c0678764f 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -29,7 +29,7 @@ from vyos.configdict import list_diff from vyos.ifconfig import VTunIf from vyos.template import render from vyos.util import call, chown, chmod_600, chmod_755 -from vyos.validate import is_addr_assigned, is_bridge_member, is_ipv4 +from vyos.validate import is_addr_assigned, is_member, is_ipv4 from vyos import ConfigError user = 'openvpn' @@ -41,7 +41,6 @@ default_config_data = { 'auth_pass': '', 'auth_user_pass_file': '', 'auth': False, - 'bridge_member': [], 'compress_lzo': False, 'deleted': False, 'description': '', @@ -199,21 +198,16 @@ def get_config(): openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE'] openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw" + # check if interface is member of a bridge + openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge') + # Check if interface instance has been removed if not conf.exists('interfaces openvpn ' + openvpn['intf']): openvpn['deleted'] = True - # check if interface is member if a bridge - openvpn['is_bridge_member'] = is_bridge_member(conf, openvpn['intf']) return openvpn - # Check if we belong to any bridge interface - for bridge in conf.list_nodes('interfaces bridge'): - for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): - if intf == openvpn['intf']: - openvpn['bridge_member'].append(intf) - # bridged server should not have a pool by default (but can be specified manually) - if openvpn['bridge_member']: + if openvpn['is_bridge_member']: openvpn['server_pool'] = False openvpn['server_ipv6_pool'] = False @@ -597,7 +591,7 @@ def get_config(): default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type']) if default_server: # server-bridge doesn't require a pool so don't set defaults for it - if openvpn['server_pool'] and not openvpn['bridge_member']: + if openvpn['server_pool'] and not openvpn['is_bridge_member']: if not openvpn['server_pool_start']: openvpn['server_pool_start'] = default_server['pool_start'] @@ -635,22 +629,15 @@ def get_config(): def verify(openvpn): if openvpn['deleted']: if openvpn['is_bridge_member']: - interface = openvpn['intf'] - bridge = openvpn['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') - - return None + raise ConfigError(( + f'Cannot delete interface "{openvpn["intf"]}" as it is a ' + f'member of bridge "{openvpn["is_bridge_menber"]}"!')) + return None if not openvpn['mode']: raise ConfigError('Must specify OpenVPN operation mode') - # Checks which need to be performed on interface rmeoval - if openvpn['deleted']: - # OpenVPN interface can not be deleted if it's still member of a bridge - if openvpn['bridge_member']: - raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge)) - # Check if we have disabled ncp and at the same time specified ncp-ciphers if openvpn['disable_ncp'] and openvpn['ncp_ciphers']: raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"') @@ -680,9 +667,9 @@ def verify(openvpn): if openvpn['ncp_ciphers']: raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client') - if openvpn['mode'] == 'site-to-site' and not openvpn['bridge_member']: + if openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']: if not (openvpn['local_address'] or openvpn['ipv6_local_address']): - raise ConfigError('Must specify "local-address" or "bridge member interface"') + raise ConfigError('Must specify "local-address" or add interface to bridge') if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1: raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"') @@ -761,8 +748,8 @@ def verify(openvpn): raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}') else: - if not openvpn['bridge_member']: - raise ConfigError('Must specify "server subnet" or "bridge member interface" in server mode') + if not openvpn['is_bridge_member']: + raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode') if openvpn['server_pool']: if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']): diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index f0f893b44..1d04d6dbe 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -21,10 +21,9 @@ from sys import exit from netifaces import interfaces from vyos.config import Config -from vyos.configdict import list_diff, vlan_to_dict, intf_to_dict, add_to_dict +from vyos.configdict import list_diff, intf_to_dict, add_to_dict from vyos.ifconfig import MACVLANIf, Section from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config -from vyos.validate import is_bridge_member from vyos import ConfigError default_config_data = { @@ -77,15 +76,12 @@ def get_config(): if not conf.exists(cfg_base): peth = deepcopy(default_config_data) peth['deleted'] = True - # check if interface is member if a bridge - peth['is_bridge_member'] = is_bridge_member(conf, ifname) return peth # set new configuration level conf.set_level(cfg_base) peth, disabled = intf_to_dict(conf, default_config_data) - peth['intf'] = ifname # ARP cache entry timeout in seconds if conf.exists(['ip', 'arp-cache-timeout']): @@ -114,21 +110,37 @@ def get_config(): def verify(peth): if peth['deleted']: if peth['is_bridge_member']: - interface = peth['intf'] - bridge = peth['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Cannot delete interface "{peth["intf"]}" as it is a ' + f'member of bridge "{peth["is_bridge_member"]}"!')) return None if not peth['source_interface']: - raise ConfigError('source-interface must be set for virtual ethernet {}'.format(peth['intf'])) + raise ConfigError(( + f'Link device must be set for pseudo-ethernet "{peth["intf"]}"')) if not peth['source_interface'] in interfaces(): - raise ConfigError('Pseudo-ethernet source-interface does not exist') + raise ConfigError(( + f'Pseudo-ethernet "{peth["intf"]}" link device does not exist') - vrf_name = peth['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + if ( peth['is_bridge_member'] + and ( peth['address'] + or peth['ipv6_eui64_prefix'] + or peth['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to interface "{peth["intf"]}" ' + f'as it is a member of bridge "{peth["is_bridge_member"]}"!')) + + if peth['vrf']: + if peth['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{peth["vrf"]}" does not exist') + + if peth['is_bridge_member']: + raise ConfigError(( + f'Interface "{peth["intf"]}" cannot be member of VRF ' + f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} ' + f'at the same time!')) # use common function to verify VLAN configuration verify_vlan_config(peth) @@ -203,8 +215,10 @@ def apply(peth): # IPv6 Duplicate Address Detection (DAD) tries p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect']) - # assign/remove VRF - p.set_vrf(peth['vrf']) + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not peth['is_bridge_member']: + p.set_vrf(peth['vrf']) # Delete old IPv6 EUI64 addresses before changing MAC for addr in peth['ipv6_eui64_prefix_remove']: @@ -235,6 +249,10 @@ def apply(peth): for addr in peth['address']: p.add_addr(addr) + # re-add ourselves to any bridge we might have fallen out of + if peth['is_bridge_member']: + p.add_to_bridge(peth['is_bridge_member']) + # remove no longer required service VLAN interfaces (vif-s) for vif_s in peth['vif_s_remove']: p.del_vlan(vif_s) diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index fc084814a..f4cd53981 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -25,7 +25,7 @@ from vyos.config import Config from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf from vyos.ifconfig.afi import IP4, IP6 from vyos.configdict import list_diff -from vyos.validate import is_ipv4, is_ipv6, is_bridge_member +from vyos.validate import is_ipv4, is_ipv6, is_member from vyos import ConfigError from vyos.dicts import FixedDict @@ -410,7 +410,7 @@ def get_config(): options['tunnel'] = {} # check for bridges - options['bridge'] = is_bridge_member(conf, ifname) + options['bridge'] = is_member(conf, ifname, 'bridge') options['interfaces'] = interfaces() for name in ct: @@ -436,11 +436,14 @@ def verify(conf): if changes['section'] == 'delete': if ifname in options['nhrp']: - raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp') + raise ConfigError(( + f'Cannot delete interface tunnel {iftype} {ifname}, ' + 'it is used by NHRP')) - bridge = options['bridge'] - if bridge: - raise ConfigError(f'Interface "{ifname}" can not be deleted as it belongs to bridge "{bridge}"!') + if options['bridge']: + raise ConfigError(( + f'Cannot delete interface "{options["ifname"]}" as it is a ' + f'member of bridge "{options["bridge"]}"!')) # done, bail out early return None @@ -525,10 +528,23 @@ def verify(conf): print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}') # vrf check - - vrf = options['vrf'] - if vrf and vrf not in options['interfaces']: - raise ConfigError(f'VRF "{vrf}" does not exist') + if options['vrf']: + if options['vrf'] not in options['interfaces']: + raise ConfigError(f'VRF "{options["vrf"]}" does not exist') + + if options['bridge']: + raise ConfigError(( + f'Interface "{options["ifname"]}" cannot be member of VRF ' + f'"{options["vrf"]}" and bridge {options["bridge"]} ' + f'at the same time!')) + + # bridge and address check + if ( options['bridge'] + and ( options['addresses-add'] + or options['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to interface "{options["name"]}" ' + f'as it is a member of bridge "{options["bridge"]}"!')) # source-interface check @@ -620,12 +636,17 @@ def apply(conf): # set other interface properties for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast', - 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'): + 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'): if not options[option]: # should never happen but better safe continue tunnel.set_interface(option, options[option]) + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not options['bridge']: + tunnel.set_vrf(options['vrf']) + # Configure interface address(es) for addr in options['addresses-del']: tunnel.del_addr(addr) diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 74eae4281..fabfaa9df 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -22,7 +22,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.ifconfig import VXLANIf, Interface -from vyos.validate import is_bridge_member +from vyos.validate import is_member from vyos import ConfigError default_config_data = { @@ -62,11 +62,12 @@ def get_config(): vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + # check if interface is member if a bridge + vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge') + # Check if interface has been removed if not conf.exists('interfaces vxlan ' + vxlan['intf']): vxlan['deleted'] = True - # check if interface is member if a bridge - vxlan['is_bridge_member'] = is_bridge_member(conf, vxlan['intf']) return vxlan # set new configuration level @@ -121,7 +122,8 @@ def get_config(): vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64') # Remove the default link-local address if set. - if not conf.exists('ipv6 address no-default-link-local'): + if not ( conf.exists('ipv6 address no-default-link-local') + or vxlan['is_bridge_member'] ): # add the link-local by default to make IPv6 work vxlan['ipv6_eui64_prefix'].append('fe80::/64') @@ -163,9 +165,9 @@ def get_config(): def verify(vxlan): if vxlan['deleted']: if vxlan['is_bridge_member']: - interface = vxlan['intf'] - bridge = vxlan['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Cannot delete interface "{vxlan["intf"]}" as it is a ' + f'member of bridge "{vxlan["is_bridge_member"]}"!') return None @@ -193,6 +195,14 @@ def verify(vxlan): raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ 'MTU is to small ({})'.format(underlay_mtu)) + if ( vxlan['is_bridge_member'] + and ( vxlan['address'] + or vxlan['ipv6_eui64_prefix'] + or vxlan['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to interface "{vxlan["intf"]}" ' + f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!')) + return None @@ -264,6 +274,10 @@ def apply(vxlan): if not vxlan['disable']: v.set_admin_state('up') + # re-add ourselves to any bridge we might have fallen out of + if vxlan['is_bridge_member']: + v.add_to_bridge(vxlan['is_bridge_member']) + return None diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 01f84260d..820b0a724 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -25,7 +25,7 @@ from vyos.config import Config from vyos.configdict import list_diff from vyos.ifconfig import WireGuardIf from vyos.util import chown, chmod_750, call -from vyos.validate import is_bridge_member +from vyos.validate import is_member from vyos import ConfigError kdir = r'/config/auth/wireguard' @@ -38,8 +38,8 @@ default_config_data = { 'listen_port': '', 'deleted': False, 'disable': False, - 'is_bridge_member': False, 'fwmark': 0, + 'is_bridge_member': False, 'mtu': 1420, 'peer': [], 'peer_remove': [], # stores public keys of peers to remove @@ -78,11 +78,12 @@ def get_config(): wg = deepcopy(default_config_data) wg['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + # check if interface is member if a bridge + wg['is_bridge_member'] = is_member(conf, wg['intf'], 'bridge') + # Check if interface has been removed if not conf.exists(base + [wg['intf']]): wg['deleted'] = True - # check if interface is member if a bridge - wg['is_bridge_member'] = is_bridge_member(conf, wg['intf']) return wg conf.set_level(base + [wg['intf']]) @@ -189,44 +190,52 @@ def get_config(): def verify(wg): - interface = wg['intf'] - if wg['deleted']: if wg['is_bridge_member']: - interface = wg['intf'] - bridge = wg['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Cannot delete interface "{wg["intf"]}" as it is a member ' + f'of bridge "{wg["is_bridge_member"]}"!')) return None - vrf_name = wg['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + if wg['is_bridge_member'] and wg['address']: + raise ConfigError(( + f'Cannot assign address to interface "{wg["intf"]}" ' + f'as it is a member of bridge "{wg["is_bridge_member"]}"!')) + + if wg['vrf']: + if wg['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{wg["vrf"]}" does not exist') + + if wg['is_bridge_member']: + raise ConfigError(( + f'Interface "{wg["intf"]}" cannot be member of VRF ' + f'"{wg["vrf"]}" and bridge {wg["is_bridge_member"]} ' + f'at the same time!')) if not os.path.exists(wg['pk']): raise ConfigError('No keys found, generate them by executing:\n' \ '"run generate wireguard [keypair|named-keypairs]"') if not wg['address']: - raise ConfigError(f'IP address required for interface "{interface}"!') + raise ConfigError(f'IP address required for interface "{wg["intf"]}"!') if not wg['peer']: - raise ConfigError(f'Peer required for interface "{interface}"!') + raise ConfigError(f'Peer required for interface "{wg["intf"]}"!') # run checks on individual configured WireGuard peer for peer in wg['peer']: - peer_name = peer['name'] if not peer['allowed-ips']: - raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!') + raise ConfigError(f'Peer allowed-ips required for peer "{peer["name"]}"!') if not peer['pubkey']: - raise ConfigError(f'Peer public-key required for peer "{peer_name}"!') + raise ConfigError(f'Peer public-key required for peer "{peer["name"]}"!') if peer['address'] and not peer['port']: - raise ConfigError(f'Peer "{peer_name}" port must be defined if address is defined!') + raise ConfigError(f'Peer "{peer["name"]}" port must be defined if address is defined!') if not peer['address'] and peer['port']: - raise ConfigError(f'Peer "{peer_name}" address must be defined if port is defined!') + raise ConfigError(f'Peer "{peer["name"]}" address must be defined if port is defined!') def apply(wg): @@ -252,8 +261,10 @@ def apply(wg): # update interface description used e.g. within SNMP w.set_alias(wg['description']) - # assign/remove VRF - w.set_vrf(wg['vrf']) + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not wg['is_bridge_member']: + w.set_vrf(wg['vrf']) # remove peers for pub_key in wg['peer_remove']: diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 148a7f6e0..4d61dc303 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -29,7 +29,7 @@ from vyos.ifconfig import WiFiIf, Section from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config from vyos.template import render from vyos.util import chown, call -from vyos.validate import is_bridge_member +from vyos.validate import is_member from vyos import ConfigError default_config_data = { @@ -134,12 +134,13 @@ def get_config(): wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + # check if interface is member if a bridge + wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge') + # check if wireless interface has been removed cfg_base = 'interfaces wireless ' + wifi['intf'] if not conf.exists(cfg_base): wifi['deleted'] = True - # check if interface is member if a bridge - wifi['is_bridge_member'] = is_bridge_member(conf, wifi['intf']) # we can not bail out early as wireless interface can not be removed # Kernel will complain with: RTNETLINK answers: Operation not supported. # Thus we need to remove individual settings @@ -378,8 +379,8 @@ def get_config(): eff_addr = conf.return_effective_values('ipv6 address eui64') wifi['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, wifi['ipv6_eui64_prefix']) - # Remove the default link-local address if set. - if conf.exists('ipv6 address no-default-link-local'): + # Remove the default link-local address if set or if member of a bridge + if conf.exists('ipv6 address no-default-link-local') or wifi['is_bridge_member']: wifi['ipv6_eui64_prefix_remove'].append('fe80::/64') else: # add the link-local by default to make IPv6 work @@ -551,9 +552,9 @@ def get_config(): def verify(wifi): if wifi['deleted']: if wifi['is_bridge_member']: - interface = wifi['intf'] - bridge = wifi['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Cannot delete interface "{wifi["intf"]}" as it is a ' + f'member of bridge "{wifi["is_bridge_member"]}"!')) return None @@ -598,9 +599,23 @@ def verify(wifi): if not radius['key']: raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server'])) - vrf_name = wifi['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF "{vrf_name}" does not exist') + if ( wifi['is_bridge_member'] + and ( wifi['address'] + or wifi['ipv6_eui64_prefix'] + or wifi['ipv6_autoconf'] ) ): + raise ConfigError(( + f'Cannot assign address to interface "{wifi["intf"]}" ' + f'as it is a member of bridge "{wifi["is_bridge_member"]}"!')) + + if wifi['vrf']: + if wifi['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist') + + if wifi['is_bridge_member']: + raise ConfigError(( + f'Interface "{wifi["intf"]}" cannot be member of VRF ' + f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} ' + f'at the same time!')) # use common function to verify VLAN configuration verify_vlan_config(wifi) @@ -691,8 +706,10 @@ def apply(wifi): # Finally create the new interface w = WiFiIf(interface, **conf) - # assign/remove VRF - w.set_vrf(wifi['vrf']) + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not wifi['is_bridge_member']: + w.set_vrf(wifi['vrf']) # update interface description used e.g. within SNMP w.set_alias(wifi['description']) diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index a3a2a2648..975e21d9f 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -21,9 +21,10 @@ from copy import deepcopy from netifaces import interfaces from vyos.config import Config +from vyos.ifconfig import BridgeIf, Section from vyos.template import render from vyos.util import chown, chmod_755, cmd, call -from vyos.validate import is_bridge_member +from vyos.validate import is_member from vyos import ConfigError default_config_data = { @@ -64,11 +65,12 @@ def get_config(): wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log" wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}" + # check if interface is member if a bridge + wwan['is_bridge_member'] = is_member(conf, wwan['intf'], 'bridge') + # Check if interface has been removed if not conf.exists('interfaces wirelessmodem ' + wwan['intf']): wwan['deleted'] = True - # check if interface is member if a bridge - wwan['is_bridge_member'] = is_bridge_member(conf, wwan['intf']) return wwan # set new configuration level @@ -119,9 +121,9 @@ def get_config(): def verify(wwan): if wwan['deleted']: if wwan['is_bridge_member']: - interface = wwan['intf'] - bridge = wwan['is_bridge_member'] - raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!') + raise ConfigError(( + f'Cannot delete interface "{wwan["intf"]}" as it is a ' + f'member of bridge "{wwan["is_bridge_member"]}"!')) return None @@ -133,9 +135,20 @@ def verify(wwan): if not os.path.exists(f"/dev/{wwan['device']}"): raise ConfigError(f"Device {wwan['device']} does not exist") - vrf_name = wwan['vrf'] - if vrf_name and vrf_name not in interfaces(): - raise ConfigError(f'VRF {vrf_name} does not exist') + if wwan['is_bridge_member'] and wwan['address']: + raise ConfigError(( + f'Cannot assign address to interface "{wwan["intf"]}" ' + f'as it is a member of bridge "{wwan["is_bridge_member"]}"!')) + + if wwan['vrf']: + if wwan['vrf'] not in interfaces(): + raise ConfigError(f'VRF "{wwan["vrf"]}" does not exist') + + if wwan['is_bridge_member']: + raise ConfigError(( + f'Interface "{wwan["intf"]}" cannot be member of VRF ' + f'"{wwan["vrf"]}" and bridge {wwan["is_bridge_member"]} ' + f'at the same time!')) return None @@ -193,6 +206,12 @@ def apply(wwan): # make logfile owned by root / vyattacfg chown(wwan['logfile'], 'root', 'vyattacfg') + # re-add ourselves to any bridge we might have fallen out of + # FIXME: wwan isn't under vyos.ifconfig so we can't call + # Interfaces.add_to_bridge() so STP settings won't get applied + if wwan['is_bridge_member'] in Section.interfaces('bridge'): + BridgeIf(wwan['is_bridge_member'], create=False).add_port(wwan['intf']) + return None if __name__ == '__main__': |