diff options
-rw-r--r-- | interface-definitions/interfaces-bridge.xml.in | 33 | ||||
-rw-r--r-- | op-mode-definitions/show-interfaces-bridge.xml | 6 | ||||
-rw-r--r-- | python/vyos/configdict.py | 22 | ||||
-rw-r--r-- | python/vyos/ifconfig/bridge.py | 58 | ||||
-rw-r--r-- | python/vyos/ifconfig/l2tpv3.py | 25 | ||||
-rw-r--r-- | python/vyos/validate.py | 1 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_bridge.py | 92 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-bridge.py | 60 |
8 files changed, 292 insertions, 5 deletions
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index ccd6db9e4..97f548252 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -111,6 +111,37 @@ </completionHelp> </properties> <children> + <leafNode name="native-vlan"> + <properties> + <help>Set the specific VLAN ID in the interface to ACCESS mode</help> + <valueHelp> + <format>1-4094</format> + <description>Virtual Local Area Network (VLAN) ID</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4094"/> + </constraint> + <constraintErrorMessage>VLAN ID must be between 1 and 4094</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="allowed-vlan"> + <properties> + <help>Allow the bridge to pass the tagged VLAN tag on this interface</help> + <valueHelp> + <format><n></format> + <description>Virtual Local Area Network (VLAN) ID</description> + </valueHelp> + <valueHelp> + <format><n-m>-<1-4096></format> + <description>Virtual Local Area Network (VLAN) ID Range</description> + </valueHelp> + <constraint> + <regex>^([0-9]{1,4}-[0-9]{1,4})|([0-9]{1,4})$</regex> + </constraint> + <constraintErrorMessage>not a valid VLAN ID value or range</constraintErrorMessage> + <multi/> + </properties> + </leafNode> <leafNode name="cost"> <properties> <help>Bridge port cost</help> @@ -163,6 +194,8 @@ <valueless/> </properties> </leafNode> + #include <include/vif-s.xml.i> + #include <include/vif.xml.i> </children> </tagNode> </children> diff --git a/op-mode-definitions/show-interfaces-bridge.xml b/op-mode-definitions/show-interfaces-bridge.xml index 85fde95b5..cc4b248b6 100644 --- a/op-mode-definitions/show-interfaces-bridge.xml +++ b/op-mode-definitions/show-interfaces-bridge.xml @@ -33,6 +33,12 @@ </properties> <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show</command> </leafNode> + <leafNode name="vlan"> + <properties> + <help>View the VLAN filter settings of the bridge</help> + </properties> + <command>/usr/sbin/bridge -c vlan show</command> + </leafNode> </children> </node> </children> diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index e43b68f6f..0b03dfc7d 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -219,6 +219,28 @@ def is_member(conf, interface, intftype=None): old_level = conf.set_level(old_level) return ret_val +def has_vlan_subinterface_configured(conf, intf): + """ + Checks if interface has an VLAN subinterface configured. + Checks the following config nodes: + 'vif', 'vif-s' + + Returns True if interface has VLAN subinterface 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} vif') or + conf.exists(f'{intfpath} vif-s')): + ret = True + + conf.set_level(old_level) + return ret + def is_source_interface(conf, interface, intftype=None): """ Checks if passed interface is configured as source-interface of other diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index bd43d4874..772db3543 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -41,6 +41,7 @@ class BridgeIf(Interface): 'section': 'bridge', 'prefixes': ['br', ], 'broadcast': True, + 'vlan': True, }, } @@ -73,6 +74,10 @@ class BridgeIf(Interface): 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/stp_state', }, + 'vlan_filter': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/vlan_filtering', + }, 'multicast_querier': { 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', @@ -152,6 +157,16 @@ class BridgeIf(Interface): >>> BridgeIf('br0').set_stp(1) """ self.set_interface('stp', state) + + def set_vlan_filter(self, state): + """ + Set bridge Vlan Filter state. 0 -> Vlan Filter disabled, 1 -> Vlan Filter enabled + + Example: + >>> from vyos.ifconfig import BridgeIf + >>> BridgeIf('br0').set_vlan_filter(1) + """ + self.set_interface('vlan_filter', state) def set_multicast_querier(self, enable): """ @@ -183,6 +198,7 @@ class BridgeIf(Interface): return self.set_interface('add_port', interface) + def del_port(self, interface): """ Remove member port from bridge instance. @@ -201,6 +217,8 @@ class BridgeIf(Interface): # call base class first super().update(config) + + ifname = config['ifname'] # Set ageing time value = config.get('aging') @@ -236,6 +254,7 @@ class BridgeIf(Interface): for member in (tmp or []): if member in interfaces(): self.del_port(member) + vlan_filter = 0 tmp = dict_search('member.interface', config) if tmp: @@ -264,7 +283,44 @@ class BridgeIf(Interface): if 'priority' in interface_config: value = interface_config.get('priority') lower.set_path_priority(value) - + + tmp = dict_search('native_vlan_removed', interface_config) + + if tmp and 'native_vlan_removed' not in interface_config: + vlan_id = tmp + cmd = f'bridge vlan add dev {interface} vid 1 pvid untagged master' + self._cmd(cmd) + cmd = f'bridge vlan del dev {interface} vid {vlan_id}' + self._cmd(cmd) + + tmp = dict_search('allowed_vlan_removed', interface_config) + + + for vlan_id in (tmp or []): + cmd = f'bridge vlan del dev {interface} vid {vlan_id}' + self._cmd(cmd) + + if 'native_vlan' in interface_config: + vlan_filter = 1 + cmd = f'bridge vlan del dev {interface} vid 1' + self._cmd(cmd) + vlan_id = interface_config['native_vlan'] + cmd = f'bridge vlan add dev {interface} vid {vlan_id} pvid untagged master' + self._cmd(cmd) + else: + cmd = f'bridge vlan del dev {interface} vid 1' + self._cmd(cmd) + + if 'allowed_vlan' in interface_config: + vlan_filter = 1 + for vlan in interface_config['allowed_vlan']: + cmd = f'bridge vlan add dev {interface} vid {vlan} master' + self._cmd(cmd) + + # enable/disable Vlan Filter + self.set_vlan_filter(vlan_filter) + + # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index 5fd90f9cf..8ed3d5afb 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -68,8 +68,9 @@ class L2TPv3If(Interface): cmd += ' peer_session_id {peer_session_id}' self._cmd(cmd.format(**self.config)) - # interface is always A/D down. It needs to be enabled explicitly - self.set_admin_state('down') + # No need for interface shut down. There exist no function to permanently enable tunnel. + # But you can disable interface permanently with shutdown/disable command. + self.set_admin_state('up') def remove(self): """ @@ -93,4 +94,24 @@ class L2TPv3If(Interface): if self.config['tunnel_id']: cmd = 'ip l2tp del tunnel tunnel_id {tunnel_id}' self._cmd(cmd.format(**self.config)) + + + def update(self, config): + """ General helper function which works on a dictionary retrived by + get_config_dict(). It's main intention is to consolidate the scattered + interface setup code and provide a single point of entry when workin + on any interface. """ + + # call base class first + super().update(config) + + # Enable/Disable of an interface must always be done at the end of the + # derived class to make use of the ref-counting set_admin_state() + # function. We will only enable the interface if 'up' was called as + # often as 'down'. This is required by some interface implementations + # as certain parameters can only be changed when the interface is + # in admin-down state. This ensures the link does not flap during + # reconfiguration. + state = 'down' if 'disable' in config else 'up' + self.set_admin_state(state) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 74488bed6..74b8adcfc 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -239,7 +239,6 @@ def assert_mac(m): if octets[:5] == (0, 0, 94, 0, 1): raise ValueError(f'{m} is a VRRP MAC address') - def has_address_configured(conf, intf): """ Checks if interface has an address configured. diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index a1359680b..3b7f1bc9a 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -21,12 +21,16 @@ from base_interfaces_test import BasicInterfaceTest from glob import glob from netifaces import interfaces from vyos.ifconfig import Section +from vyos.util import cmd +import json class BridgeInterfaceTest(BasicInterfaceTest.BaseTest): def setUp(self): super().setUp() self._test_ipv6 = True + self._test_vlan = True + self._test_qinq = True self._base_path = ['interfaces', 'bridge'] self._interfaces = ['br0'] @@ -78,6 +82,94 @@ class BridgeInterfaceTest(BasicInterfaceTest.BaseTest): self.session.delete(self._base_path + [interface, 'member']) self.session.commit() + + def test_vlan_filter(self): + """ Add member interface to bridge and set VLAN filter """ + for interface in self._interfaces: + base = self._base_path + [interface] + self.session.set(base + ['address', '192.0.2.1/24']) + self.session.set(base + ['vif', '2','address','192.0.3.1/24']) + + vlan_id = 101 + allowed_vlan = 2 + allowed_vlan_range = '4-9' + # assign members to bridge interface + for member in self._members: + base_member = base + ['member', 'interface', member] + self.session.set(base_member + ['allowed-vlan', str(allowed_vlan)]) + self.session.set(base_member + ['allowed-vlan', allowed_vlan_range]) + self.session.set(base_member + ['native-vlan', str(vlan_id)]) + vlan_id += 1 + + # commit config + self.session.commit() + + # Detect the vlan filter function + for interface in self._interfaces: + with open(f'/sys/class/net/{interface}/bridge/vlan_filtering', 'r') as f: + flags = f.read() + self.assertEqual(int(flags), 1) + + # Execute the program to obtain status information + + json_data = cmd('bridge -j vlan show', shell=True) + + vlan_filter_status = None + + vlan_filter_status = json.loads(json_data) + + + if vlan_filter_status is not None: + for interface_status in vlan_filter_status: + ifname = interface_status['ifname'] + for interface in self._members: + vlan_success = 0; + if interface == ifname: + vlans_status = interface_status['vlans'] + for vlan_status in vlans_status: + vlan_id = vlan_status['vlan'] + flag_num = 0 + if 'flags' in vlan_status: + flags = vlan_status['flags'] + for flag in flags: + flag_num = flag_num +1 + if vlan_id == 2: + if flag_num == 0: + vlan_success = vlan_success + 1 + else: + for id in range(4,10): + if vlan_id == id: + if flag_num == 0: + vlan_success = vlan_success + 1 + if vlan_id >= 101: + if flag_num == 2: + vlan_success = vlan_success + 1 + if vlan_success >= 7: + self.assertTrue(True) + else: + self.assertTrue(False) + + else: + self.assertTrue(False) + + + + + # check member interfaces are added on the bridge + + for interface in self._interfaces: + bridge_members = [] + for tmp in glob(f'/sys/class/net/{interface}/lower_*'): + bridge_members.append(os.path.basename(tmp).replace('lower_', '')) + + for member in self._members: + self.assertIn(member, bridge_members) + + # delete all members + for interface in self._interfaces: + self.session.delete(self._base_path + [interface, 'member']) + + self.session.commit() def test_vlan_members(self): """ T2945: ensure that VIFs are not dropped from bridge """ diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index 258f9ec79..076bdb63e 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -18,12 +18,15 @@ import os from sys import exit from netifaces import interfaces +import re from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import node_changed +from vyos.configdict import leaf_node_changed from vyos.configdict import is_member from vyos.configdict import is_source_interface +from vyos.configdict import has_vlan_subinterface_configured from vyos.configdict import dict_merge from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_vrf @@ -38,6 +41,26 @@ from vyos import ConfigError from vyos import airbag airbag.enable() +def helper_check_removed_vlan(conf,bridge,key,key_mangling): + key_update = re.sub(key_mangling[0], key_mangling[1], key) + if dict_search('member.interface', bridge): + for interface in bridge['member']['interface']: + tmp = leaf_node_changed(conf, ['member', 'interface',interface,key]) + if tmp: + if 'member' in bridge: + if 'interface' in bridge['member']: + if interface in bridge['member']['interface']: + bridge['member']['interface'][interface].update({f'{key_update}_removed': tmp }) + else: + bridge['member']['interface'].update({interface: {f'{key_update}_removed': tmp }}) + else: + bridge['member'].update({ 'interface': {interface: {f'{key_update}_removed': tmp }}}) + else: + bridge.update({'member': { 'interface': {interface: {f'{key_update}_removed': tmp }}}}) + + return bridge + + def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -57,6 +80,12 @@ def get_config(config=None): bridge['member'].update({'interface_remove': tmp }) else: bridge.update({'member': {'interface_remove': tmp }}) + + + # determine which members vlan have been removed + + bridge = helper_check_removed_vlan(conf,bridge,'native-vlan',('-', '_')) + bridge = helper_check_removed_vlan(conf,bridge,'allowed-vlan',('-', '_')) if dict_search('member.interface', bridge): # XXX: T2665: we need a copy of the dict keys for iteration, else we will get: @@ -70,7 +99,8 @@ def get_config(config=None): # the default dictionary is not properly paged into the dict (see T2665) # thus we will ammend it ourself default_member_values = defaults(base + ['member', 'interface']) - for interface in bridge['member']['interface']: + vlan_aware = False + for interface,interface_config in bridge['member']['interface'].items(): bridge['member']['interface'][interface] = dict_merge( default_member_values, bridge['member']['interface'][interface]) @@ -90,6 +120,19 @@ def get_config(config=None): # Bridge members must not have an assigned address tmp = has_address_configured(conf, interface) if tmp: bridge['member']['interface'][interface].update({'has_address' : ''}) + + # VLAN-aware bridge members must not have VLAN interface configuration + if 'native_vlan' in interface_config: + if 'disable' not in interface_config['native_vlan']: + vlan_aware = True + + if 'allowed_vlan' in interface_config: + vlan_aware = True + + if vlan_aware: + tmp = has_vlan_subinterface_configured(conf,interface) + if tmp: + if tmp: bridge['member']['interface'][interface].update({'has_vlan' : ''}) return bridge @@ -121,6 +164,21 @@ def verify(bridge): if 'has_address' in interface_config: raise ConfigError(error_msg + 'it has an address assigned!') + + if 'has_vlan' in interface_config: + raise ConfigError(error_msg + 'it has an VLAN subinterface assigned!') + + if 'allowed_vlan' in interface_config: + for vlan in interface_config['allowed_vlan']: + if re.search('[0-9]{1,4}-[0-9]{1,4}', vlan): + vlan_range = vlan.split('-') + if int(vlan_range[0]) <1 and int(vlan_range[0])>4094: + raise ConfigError('VLAN ID must be between 1 and 4094') + if int(vlan_range[1]) <1 and int(vlan_range[1])>4094: + raise ConfigError('VLAN ID must be between 1 and 4094') + else: + if int(vlan) <1 and int(vlan)>4094: + raise ConfigError('VLAN ID must be between 1 and 4094') return None |