diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i index fdd62b63d..02e7ab057 100644 --- a/interface-definitions/include/interface/vif-s.xml.i +++ b/interface-definitions/include/interface/vif-s.xml.i @@ -1,75 +1,55 @@ <!-- include start from interface/vif-s.xml.i --> <tagNode name="vif-s"> <properties> <help>QinQ TAG-S Virtual Local Area Network (VLAN) ID</help> <valueHelp> <format>u32:0-4094</format> <description>QinQ Virtual Local Area Network (VLAN) ID</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4094"/> </constraint> <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> </properties> <children> #include <include/generic-description.xml.i> #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> - <leafNode name="protocol"> - <properties> - <help>Protocol used for service VLAN (default: 802.1ad)</help> - <completionHelp> - <list>802.1ad 802.1q</list> - </completionHelp> - <valueHelp> - <format>802.1ad</format> - <description>Provider Bridging (IEEE 802.1ad, Q-inQ), ethertype 0x88a8</description> - </valueHelp> - <valueHelp> - <format>802.1q</format> - <description>VLAN-tagged frame (IEEE 802.1q), ethertype 0x8100</description> - </valueHelp> - <constraint> - <regex>(802.1q|802.1ad)</regex> - </constraint> - <constraintErrorMessage>Ethertype must be 802.1ad or 802.1q</constraintErrorMessage> - </properties> - <defaultValue>802.1ad</defaultValue> - </leafNode> + #include <include/interface/vlan-protocol.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> #include <include/interface/mtu-68-16000.xml.i> <tagNode name="vif-c"> <properties> <help>QinQ TAG-C Virtual Local Area Network (VLAN) ID</help> <constraint> <validator name="numeric" argument="--range 0-4094"/> </constraint> <constraintErrorMessage>VLAN ID must be between 0 and 4094</constraintErrorMessage> </properties> <children> #include <include/generic-description.xml.i> #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> #include <include/interface/mtu-68-16000.xml.i> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> </children> </tagNode> #include <include/interface/redirect.xml.i> #include <include/interface/vrf.xml.i> </children> </tagNode> <!-- include end --> diff --git a/interface-definitions/include/interface/vlan-protocol.xml.i b/interface-definitions/include/interface/vlan-protocol.xml.i new file mode 100644 index 000000000..2fe8d65d7 --- /dev/null +++ b/interface-definitions/include/interface/vlan-protocol.xml.i @@ -0,0 +1,23 @@ +<!-- include start from interface/vif.xml.i --> +<leafNode name="protocol"> + <properties> + <help>Protocol used for service VLAN (default: 802.1ad)</help> + <completionHelp> + <list>802.1ad 802.1q</list> + </completionHelp> + <valueHelp> + <format>802.1ad</format> + <description>Provider Bridging (IEEE 802.1ad, Q-inQ), ethertype 0x88a8</description> + </valueHelp> + <valueHelp> + <format>802.1q</format> + <description>VLAN-tagged frame (IEEE 802.1q), ethertype 0x8100</description> + </valueHelp> + <constraint> + <regex>(802.1q|802.1ad)</regex> + </constraint> + <constraintErrorMessage>Ethertype must be 802.1ad or 802.1q</constraintErrorMessage> + </properties> + <defaultValue>802.1ad</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/interfaces_bridge.xml.in b/interface-definitions/interfaces_bridge.xml.in index d4d277cfc..7fb5f121a 100644 --- a/interface-definitions/interfaces_bridge.xml.in +++ b/interface-definitions/interfaces_bridge.xml.in @@ -1,226 +1,230 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="interfaces"> <children> <tagNode name="bridge" owner="${vyos_conf_scripts_dir}/interfaces_bridge.py"> <properties> <help>Bridge Interface</help> <priority>310</priority> <constraint> <regex>br[0-9]+</regex> </constraint> <constraintErrorMessage>Bridge interface must be named brN</constraintErrorMessage> <valueHelp> <format>brN</format> <description>Bridge interface name</description> </valueHelp> </properties> <children> #include <include/interface/address-ipv4-ipv6-dhcp.xml.i> <leafNode name="aging"> <properties> <help>MAC address aging interval</help> <valueHelp> <format>u32:0</format> <description>Disable MAC address learning (always flood)</description> </valueHelp> <valueHelp> <format>u32:10-1000000</format> <description>MAC address aging time in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-0 --range 10-1000000"/> </constraint> </properties> <defaultValue>300</defaultValue> </leafNode> #include <include/generic-description.xml.i> #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> #include <include/interface/mtu-68-16000.xml.i> <leafNode name="forwarding-delay"> <properties> <help>Forwarding delay</help> <valueHelp> <format>u32:0-200</format> <description>Spanning Tree Protocol forwarding delay in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-200"/> </constraint> <constraintErrorMessage>Forwarding delay must be between 0 and 200 seconds</constraintErrorMessage> </properties> <defaultValue>14</defaultValue> </leafNode> <leafNode name="hello-time"> <properties> <help>Hello packet advertisement interval</help> <valueHelp> <format>u32:1-10</format> <description>Spanning Tree Protocol hello advertisement interval in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-10"/> </constraint> <constraintErrorMessage>Bridge Hello interval must be between 1 and 10 seconds</constraintErrorMessage> </properties> <defaultValue>2</defaultValue> </leafNode> <node name="igmp"> <properties> <help>Internet Group Management Protocol (IGMP) and Multicast Listener Discovery (MLD) settings</help> </properties> <children> <leafNode name="querier"> <properties> <help>Enable IGMP/MLD querier</help> <valueless/> </properties> </leafNode> <leafNode name="snooping"> <properties> <help>Enable IGMP/MLD snooping</help> <valueless/> </properties> </leafNode> </children> </node> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mirror.xml.i> <leafNode name="enable-vlan"> <properties> <help>Enable VLAN aware bridge</help> <valueless/> </properties> </leafNode> + #include <include/interface/vlan-protocol.xml.i> + <leafNode name="protocol"> + <defaultValue>802.1q</defaultValue> + </leafNode> <leafNode name="max-age"> <properties> <help>Interval at which neighbor bridges are removed</help> <valueHelp> <format>u32:1-40</format> <description>Bridge maximum aging time in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-40"/> </constraint> <constraintErrorMessage>Bridge max aging value must be between 1 and 40 seconds</constraintErrorMessage> </properties> <defaultValue>20</defaultValue> </leafNode> <node name="member"> <properties> <help>Bridge member interfaces</help> </properties> <children> <tagNode name="interface"> <properties> <help>Member interface name</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces --bridgeable</script> </completionHelp> <constraint> #include <include/constraint/interface-name.xml.i> </constraint> </properties> <children> <leafNode name="native-vlan"> <properties> <help>Specify VLAN id which should natively be present on the link</help> <valueHelp> <format>u32: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>Specify VLAN id which is allowed in this trunk interface</help> <valueHelp> <format><id></format> <description>VLAN id allowed to pass this interface</description> </valueHelp> <valueHelp> <format><idN>-<idM></format> <description>VLAN id range allowed on this interface (use '-' as delimiter)</description> </valueHelp> <constraint> <validator name="numeric" argument="--allow-range --range 1-4094"/> </constraint> <constraintErrorMessage>not a valid VLAN ID value or range</constraintErrorMessage> <multi/> </properties> </leafNode> <leafNode name="cost"> <properties> <help>Bridge port cost</help> <valueHelp> <format>u32:1-65535</format> <description>Path cost value for Spanning Tree Protocol</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-65535"/> </constraint> <constraintErrorMessage>Path cost value must be between 1 and 65535</constraintErrorMessage> </properties> <defaultValue>100</defaultValue> </leafNode> <leafNode name="priority"> <properties> <help>Bridge port priority</help> <valueHelp> <format>u32:0-63</format> <description>Bridge port priority</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-63"/> </constraint> <constraintErrorMessage>Port priority value must be between 0 and 63</constraintErrorMessage> </properties> <defaultValue>32</defaultValue> </leafNode> <leafNode name="isolated"> <properties> <help>Port is isolated (also known as Private-VLAN)</help> <valueless/> </properties> </leafNode> </children> </tagNode> </children> </node> <leafNode name="priority"> <properties> <help>Priority for this bridge</help> <valueHelp> <format>u32:0-65535</format> <description>Bridge priority</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-65535"/> </constraint> <constraintErrorMessage>Bridge priority must be between 0 and 65535 (multiples of 4096)</constraintErrorMessage> </properties> <defaultValue>32768</defaultValue> </leafNode> <leafNode name="stp"> <properties> <help>Enable spanning tree protocol</help> <valueless/> </properties> </leafNode> #include <include/interface/redirect.xml.i> #include <include/interface/vif.xml.i> </children> </tagNode> </children> </node> </interfaceDefinition> diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index b29e71394..7936e3da5 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -1,388 +1,414 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. from netifaces import interfaces -import json from vyos.ifconfig.interface import Interface from vyos.utils.assertion import assert_boolean +from vyos.utils.assertion import assert_list from vyos.utils.assertion import assert_positive -from vyos.utils.process import cmd from vyos.utils.dict import dict_search from vyos.configdict import get_vlan_ids from vyos.configdict import list_diff @Interface.register class BridgeIf(Interface): """ A bridge is a way to connect two Ethernet segments together in a protocol independent way. Packets are forwarded based on Ethernet address, rather than IP address (like a router). Since forwarding is done at Layer 2, all protocols can go transparently through a bridge. The Linux bridge code implements a subset of the ANSI/IEEE 802.1d standard. """ iftype = 'bridge' definition = { **Interface.definition, **{ 'section': 'bridge', 'prefixes': ['br', ], 'broadcast': True, 'vlan': True, }, } _sysfs_get = { **Interface._sysfs_get,**{ 'vlan_filter': { 'location': '/sys/class/net/{ifname}/bridge/vlan_filtering' } } } _sysfs_set = {**Interface._sysfs_set, **{ 'ageing_time': { 'validate': assert_positive, 'convert': lambda t: int(t) * 100, 'location': '/sys/class/net/{ifname}/bridge/ageing_time', }, 'forward_delay': { 'validate': assert_positive, 'convert': lambda t: int(t) * 100, 'location': '/sys/class/net/{ifname}/bridge/forward_delay', }, 'hello_time': { 'validate': assert_positive, 'convert': lambda t: int(t) * 100, 'location': '/sys/class/net/{ifname}/bridge/hello_time', }, 'max_age': { 'validate': assert_positive, 'convert': lambda t: int(t) * 100, 'location': '/sys/class/net/{ifname}/bridge/max_age', }, 'priority': { 'validate': assert_positive, 'location': '/sys/class/net/{ifname}/bridge/priority', }, 'stp': { 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/stp_state', }, 'vlan_filter': { 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/vlan_filtering', }, + 'vlan_protocol': { + 'validate': lambda v: assert_list(v, ['0x88a8', '0x8100']), + 'location': '/sys/class/net/{ifname}/bridge/vlan_protocol', + }, 'multicast_querier': { 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', }, 'multicast_snooping': { 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_snooping', }, }} _command_set = {**Interface._command_set, **{ 'add_port': { 'shellcmd': 'ip link set dev {value} master {ifname}', }, 'del_port': { 'shellcmd': 'ip link set dev {value} nomaster', }, }} def get_vlan_filter(self): """ Get the status of the bridge VLAN filter """ return self.get_interface('vlan_filter') def set_ageing_time(self, time): """ Set bridge interface MAC address aging time in seconds. Internal kernel representation is in centiseconds. Kernel default is 300 seconds. Example: >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').ageing_time(2) """ self.set_interface('ageing_time', time) def set_forward_delay(self, time): """ Set bridge forwarding delay in seconds. Internal Kernel representation is in centiseconds. Example: >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').forward_delay(15) """ self.set_interface('forward_delay', time) def set_hello_time(self, time): """ Set bridge hello time in seconds. Internal Kernel representation is in centiseconds. Example: >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').set_hello_time(2) """ self.set_interface('hello_time', time) def set_max_age(self, time): """ Set bridge max message age in seconds. Internal Kernel representation is in centiseconds. Example: >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').set_max_age(30) """ self.set_interface('max_age', time) def set_priority(self, priority): """ Set bridge max aging time in seconds. Example: >>> from vyos.ifconfig import BridgeIf >>> BridgeIf('br0').set_priority(8192) """ self.set_interface('priority', priority) def set_stp(self, state): """ Set bridge STP (Spanning Tree) state. 0 -> STP disabled, 1 -> STP enabled Example: >>> from vyos.ifconfig import BridgeIf >>> 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) # VLAN of bridge parent interface is always 1 # VLAN 1 is the default VLAN for all unlabeled packets cmd = f'bridge vlan add dev {self.ifname} vid 1 pvid untagged self' self._cmd(cmd) def set_multicast_querier(self, enable): """ Sets whether the bridge actively runs a multicast querier or not. When a bridge receives a 'multicast host membership' query from another network host, that host is tracked based on the time that the query was received plus the multicast query interval time. Use enable=1 to enable or enable=0 to disable Example: >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').set_multicast_querier(1) """ self.set_interface('multicast_querier', enable) def set_multicast_snooping(self, enable): """ Enable or disable multicast snooping on the bridge. Use enable=1 to enable or enable=0 to disable Example: >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').set_multicast_snooping(1) """ self.set_interface('multicast_snooping', enable) def add_port(self, interface): """ Add physical interface to bridge (member port) Example: >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').add_port('eth0') >>> BridgeIf('br0').add_port('eth1') """ # Bridge port handling of wireless interfaces is done by hostapd. if 'wlan' in interface: return try: return self.set_interface('add_port', interface) except: from vyos import ConfigError raise ConfigError('Error: Device does not allow enslaving to a bridge.') def del_port(self, interface): """ Remove member port from bridge instance. Example: >>> from vyos.ifconfig import Interface >>> BridgeIf('br0').del_port('eth1') """ return self.set_interface('del_port', interface) + def set_vlan_protocol(self, protocol): + """ + Set protocol used for VLAN filtering. + The valid values are 0x8100(802.1q) or 0x88A8(802.1ad). + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + + if protocol not in ['802.1q', '802.1ad']: + raise ValueError() + + map = { + '802.1ad': '0x88a8', + '802.1q' : '0x8100' + } + + return self.set_interface('vlan_protocol', map[protocol]) + 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. """ # Set ageing time value = config.get('aging') self.set_ageing_time(value) # set bridge forward delay value = config.get('forwarding_delay') self.set_forward_delay(value) # set hello time value = config.get('hello_time') self.set_hello_time(value) # set max message age value = config.get('max_age') self.set_max_age(value) # set bridge priority value = config.get('priority') self.set_priority(value) # enable/disable spanning tree value = '1' if 'stp' in config else '0' self.set_stp(value) # enable or disable multicast snooping tmp = dict_search('igmp.snooping', config) value = '1' if (tmp != None) else '0' self.set_multicast_snooping(value) # enable or disable IGMP querier tmp = dict_search('igmp.querier', config) value = '1' if (tmp != None) else '0' self.set_multicast_querier(value) # remove interface from bridge tmp = dict_search('member.interface_remove', config) for member in (tmp or []): if member in interfaces(): self.del_port(member) - # enable/disable Vlan Filter + # enable/disable VLAN Filter tmp = '1' if 'enable_vlan' in config else '0' self.set_vlan_filter(tmp) + tmp = config.get('protocol') + self.set_vlan_protocol(tmp) + # add VLAN interfaces to local 'parent' bridge to allow forwarding if 'enable_vlan' in config: for vlan in config.get('vif_remove', {}): # Remove old VLANs from the bridge cmd = f'bridge vlan del dev {self.ifname} vid {vlan} self' self._cmd(cmd) for vlan in config.get('vif', {}): cmd = f'bridge vlan add dev {self.ifname} vid {vlan} self' self._cmd(cmd) # VLAN of bridge parent interface is always 1. VLAN 1 is the default # VLAN for all unlabeled packets cmd = f'bridge vlan add dev {self.ifname} vid 1 pvid untagged self' self._cmd(cmd) tmp = dict_search('member.interface', config) if tmp: for interface, interface_config in tmp.items(): # if interface does yet not exist bail out early and # add it later if interface not in interfaces(): continue # Bridge lower "physical" interface lower = Interface(interface) # If we've come that far we already verified the interface does # not have any addresses configured by CLI so just flush any # remaining ones lower.flush_addrs() # enslave interface port to bridge self.add_port(interface) if not interface.startswith('wlan'): # always set private-vlan/port isolation - this can not be # done when lower link is a wifi link, as it will trigger: # RTNETLINK answers: Operation not supported tmp = dict_search('isolated', interface_config) value = 'on' if (tmp != None) else 'off' lower.set_port_isolation(value) # set bridge port path cost if 'cost' in interface_config: lower.set_path_cost(interface_config['cost']) # set bridge port path priority if 'priority' in interface_config: lower.set_path_priority(interface_config['priority']) if 'enable_vlan' in config: add_vlan = [] native_vlan_id = None allowed_vlan_ids= [] cur_vlan_ids = get_vlan_ids(interface) if 'native_vlan' in interface_config: vlan_id = interface_config['native_vlan'] add_vlan.append(vlan_id) native_vlan_id = vlan_id if 'allowed_vlan' in interface_config: for vlan in interface_config['allowed_vlan']: vlan_range = vlan.split('-') if len(vlan_range) == 2: for vlan_add in range(int(vlan_range[0]),int(vlan_range[1]) + 1): add_vlan.append(str(vlan_add)) allowed_vlan_ids.append(str(vlan_add)) else: add_vlan.append(vlan) allowed_vlan_ids.append(vlan) # Remove redundant VLANs from the system for vlan in list_diff(cur_vlan_ids, add_vlan): cmd = f'bridge vlan del dev {interface} vid {vlan} master' self._cmd(cmd) for vlan in allowed_vlan_ids: cmd = f'bridge vlan add dev {interface} vid {vlan} master' self._cmd(cmd) # Setting native VLAN to system if native_vlan_id: cmd = f'bridge vlan add dev {interface} vid {native_vlan_id} pvid untagged master' self._cmd(cmd) super().update(config) diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 3500e97d6..124c1fbcb 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -1,446 +1,464 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2024 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/>. import os import json import unittest from base_interfaces_test import BasicInterfaceTest from copy import deepcopy from glob import glob from vyos.ifconfig import Section from vyos.template import ip_from_cidr from vyos.utils.process import cmd from vyos.utils.file import read_file from vyos.utils.network import get_interface_config from vyos.utils.network import interface_exists class BridgeInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): cls._base_path = ['interfaces', 'bridge'] cls._mirror_interfaces = ['dum21354'] cls._members = [] # we need to filter out VLAN interfaces identified by a dot (.) # in their name - just in case! if 'TEST_ETH' in os.environ: cls._members = os.environ['TEST_ETH'].split() else: for tmp in Section.interfaces('ethernet', vlan=False): cls._members.append(tmp) cls._options['br0'] = [] for member in cls._members: cls._options['br0'].append(f'member interface {member}') cls._interfaces = list(cls._options) # call base-classes classmethod super(BridgeInterfaceTest, cls).setUpClass() def tearDown(self): for intf in self._interfaces: self.cli_delete(self._base_path + [intf]) super().tearDown() def test_isolated_interfaces(self): # Add member interfaces to bridge and set STP cost/priority for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['stp']) # assign members to bridge interface for member in self._members: base_member = base + ['member', 'interface', member] self.cli_set(base_member + ['isolated']) # commit config self.cli_commit() for interface in self._interfaces: tmp = get_interface_config(interface) # STP must be enabled as configured above self.assertEqual(1, tmp['linkinfo']['info_data']['stp_state']) # validate member interface configuration for member in self._members: tmp = get_interface_config(member) # verify member is assigned to the bridge self.assertEqual(interface, tmp['master']) # Isolated must be enabled as configured above self.assertTrue(tmp['linkinfo']['info_slave_data']['isolated']) def test_igmp_querier_snooping(self): # Add member interfaces to bridge for interface in self._interfaces: base = self._base_path + [interface] # assign members to bridge interface for member in self._members: base_member = base + ['member', 'interface', member] self.cli_set(base_member) # commit config self.cli_commit() for interface in self._interfaces: # Verify IGMP default configuration tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') self.assertEqual(tmp, '0') tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') self.assertEqual(tmp, '0') # Enable IGMP snooping for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['igmp', 'snooping']) # commit config self.cli_commit() for interface in self._interfaces: # Verify IGMP snooping configuration # Verify IGMP default configuration tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') self.assertEqual(tmp, '1') tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') self.assertEqual(tmp, '0') # Enable IGMP querieer for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['igmp', 'querier']) # commit config self.cli_commit() for interface in self._interfaces: # Verify IGMP snooping & querier configuration tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') self.assertEqual(tmp, '1') tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') self.assertEqual(tmp, '1') # Disable IGMP for interface in self._interfaces: base = self._base_path + [interface] self.cli_delete(base + ['igmp']) # commit config self.cli_commit() for interface in self._interfaces: # Verify IGMP snooping & querier configuration tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') self.assertEqual(tmp, '0') tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') self.assertEqual(tmp, '0') # validate member interface configuration for member in self._members: tmp = get_interface_config(member) # verify member is assigned to the bridge self.assertEqual(interface, tmp['master']) def test_add_remove_bridge_member(self): # Add member interfaces to bridge and set STP cost/priority for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['stp']) self.cli_set(base + ['address', '192.0.2.1/24']) cost = 1000 priority = 10 # assign members to bridge interface for member in self._members: base_member = base + ['member', 'interface', member] self.cli_set(base_member + ['cost', str(cost)]) self.cli_set(base_member + ['priority', str(priority)]) cost += 1 priority += 1 # commit config self.cli_commit() # Add member interfaces to bridge and set STP cost/priority for interface in self._interfaces: cost = 1000 priority = 10 + + tmp = get_interface_config(interface) + self.assertEqual('802.1Q', tmp['linkinfo']['info_data']['vlan_protocol']) # default VLAN protocol + for member in self._members: tmp = get_interface_config(member) self.assertEqual(interface, tmp['master']) self.assertFalse( tmp['linkinfo']['info_slave_data']['isolated']) self.assertEqual(cost, tmp['linkinfo']['info_slave_data']['cost']) self.assertEqual(priority, tmp['linkinfo']['info_slave_data']['priority']) cost += 1 priority += 1 def test_vif_8021q_interfaces(self): for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['enable-vlan']) super().test_vif_8021q_interfaces() def test_vif_8021q_lower_up_down(self): for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['enable-vlan']) super().test_vif_8021q_lower_up_down() def test_vif_8021q_qos_change(self): for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['enable-vlan']) super().test_vif_8021q_qos_change() def test_vif_8021q_mtu_limits(self): for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['enable-vlan']) super().test_vif_8021q_mtu_limits() def test_bridge_vlan_filter(self): vifs = ['10', '20', '30', '40'] native_vlan = '20' # Add member interface to bridge and set VLAN filter for interface in self._interfaces: base = self._base_path + [interface] self.cli_set(base + ['enable-vlan']) self.cli_set(base + ['address', '192.0.2.1/24']) for vif in vifs: self.cli_set(base + ['vif', vif, 'address', f'192.0.{vif}.1/24']) self.cli_set(base + ['vif', vif, 'mtu', self._mtu]) for member in self._members: base_member = base + ['member', 'interface', member] self.cli_set(base_member + ['native-vlan', native_vlan]) for vif in vifs: self.cli_set(base_member + ['allowed-vlan', vif]) # commit config self.cli_commit() def _verify_members(interface, members) -> None: # check member interfaces are added on the bridge bridge_members = [] for tmp in glob(f'/sys/class/net/{interface}/lower_*'): bridge_members.append(os.path.basename(tmp).replace('lower_', '')) self.assertListEqual(sorted(members), sorted(bridge_members)) def _check_vlan_filter(interface, vifs) -> None: configured_vlan_ids = [] bridge_json = cmd(f'bridge -j vlan show dev {interface}') bridge_json = json.loads(bridge_json) self.assertIsNotNone(bridge_json) for tmp in bridge_json: self.assertIn('vlans', tmp) for vlan in tmp['vlans']: self.assertIn('vlan', vlan) configured_vlan_ids.append(str(vlan['vlan'])) # Verify native VLAN ID has 'PVID' flag set on individual member ports if not interface.startswith('br') and str(vlan['vlan']) == native_vlan: self.assertIn('flags', vlan) self.assertIn('PVID', vlan['flags']) self.assertListEqual(sorted(configured_vlan_ids), sorted(vifs)) # Verify correct setting of VLAN filter function for interface in self._interfaces: tmp = read_file(f'/sys/class/net/{interface}/bridge/vlan_filtering') self.assertEqual(tmp, '1') # Obtain status information and verify proper VLAN filter setup. # First check if all members are present, second check if all VLANs # are assigned on the parend bridge interface, third verify all the # VLANs are properly setup on the downstream "member" ports for interface in self._interfaces: # check member interfaces are added on the bridge _verify_members(interface, self._members) # Check if all VLAN ids are properly set up. Bridge interface always # has native VLAN 1 tmp = deepcopy(vifs) tmp.append('1') _check_vlan_filter(interface, tmp) for member in self._members: _check_vlan_filter(member, vifs) # change member interface description to trigger config update, # VLANs must still exist (T4565) for interface in self._interfaces: for member in self._members: self.cli_set(['interfaces', Section.section(member), member, 'description', f'foo {member}']) # commit config self.cli_commit() # Obtain status information and verify proper VLAN filter setup. # First check if all members are present, second check if all VLANs # are assigned on the parend bridge interface, third verify all the # VLANs are properly setup on the downstream "member" ports for interface in self._interfaces: # check member interfaces are added on the bridge _verify_members(interface, self._members) # Check if all VLAN ids are properly set up. Bridge interface always # has native VLAN 1 tmp = deepcopy(vifs) tmp.append('1') _check_vlan_filter(interface, tmp) for member in self._members: _check_vlan_filter(member, vifs) # delete all members for interface in self._interfaces: self.cli_delete(self._base_path + [interface, 'member']) # commit config self.cli_commit() # verify member interfaces are no longer assigned 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_', '')) self.assertNotEqual(len(self._members), len(bridge_members)) for member in self._members: self.assertNotIn(member, bridge_members) def test_bridge_vif_members(self): # T2945: ensure that VIFs are not dropped from bridge vifs = ['300', '400'] for interface in self._interfaces: for member in self._members: for vif in vifs: self.cli_set(['interfaces', 'ethernet', member, 'vif', vif]) self.cli_set(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif}']) self.cli_commit() # Verify config for interface in self._interfaces: for member in self._members: for vif in vifs: # member interface must be assigned to the bridge self.assertTrue(os.path.exists(f'/sys/class/net/{interface}/lower_{member}.{vif}')) # delete all members for interface in self._interfaces: for member in self._members: for vif in vifs: self.cli_delete(['interfaces', 'ethernet', member, 'vif', vif]) self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif}']) def test_bridge_vif_s_vif_c_members(self): # T2945: ensure that VIFs are not dropped from bridge vifs = ['300', '400'] vifc = ['301', '401'] for interface in self._interfaces: for member in self._members: for vif_s in vifs: for vif_c in vifc: self.cli_set(['interfaces', 'ethernet', member, 'vif-s', vif_s, 'vif-c', vif_c]) self.cli_set(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}']) self.cli_commit() # Verify config for interface in self._interfaces: for member in self._members: for vif_s in vifs: for vif_c in vifc: # member interface must be assigned to the bridge self.assertTrue(os.path.exists(f'/sys/class/net/{interface}/lower_{member}.{vif_s}.{vif_c}')) # delete all members for interface in self._interfaces: for member in self._members: for vif_s in vifs: self.cli_delete(['interfaces', 'ethernet', member, 'vif-s', vif_s]) for vif_c in vifc: self.cli_delete(['interfaces', 'bridge', interface, 'member', 'interface', f'{member}.{vif_s}.{vif_c}']) def test_bridge_tunnel_vxlan_multicast(self): # Testcase for T6043 running VXLAN over gretap br_if = 'br0' tunnel_if = 'tun0' eth_if = 'eth1' vxlan_if = 'vxlan0' multicast_group = '239.0.0.241' vni = '123' eth0_addr = '192.0.2.2/30' self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', eth_if]) self.cli_set(['interfaces', 'bridge', br_if, 'member', 'interface', vxlan_if]) self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'address', '10.0.0.2/24']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'enable-multicast']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'encapsulation', 'gretap']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'mtu', '1500']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'parameters', 'ip', 'ignore-df']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'parameters', 'ip', 'key', '1']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'parameters', 'ip', 'no-pmtu-discovery']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'parameters', 'ip', 'ttl', '0']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'remote', '203.0.113.2']) self.cli_set(['interfaces', 'tunnel', tunnel_if, 'source-address', ip_from_cidr(eth0_addr)]) self.cli_set(['interfaces', 'vxlan', vxlan_if, 'group', multicast_group]) self.cli_set(['interfaces', 'vxlan', vxlan_if, 'mtu', '1426']) self.cli_set(['interfaces', 'vxlan', vxlan_if, 'source-interface', tunnel_if]) self.cli_set(['interfaces', 'vxlan', vxlan_if, 'vni', vni]) self.cli_commit() self.assertTrue(interface_exists(eth_if)) self.assertTrue(interface_exists(vxlan_if)) self.assertTrue(interface_exists(tunnel_if)) tmp = get_interface_config(vxlan_if) self.assertEqual(tmp['ifname'], vxlan_if) self.assertEqual(tmp['linkinfo']['info_data']['link'], tunnel_if) self.assertEqual(tmp['linkinfo']['info_data']['group'], multicast_group) self.assertEqual(tmp['linkinfo']['info_data']['id'], int(vni)) bridge_members = [] for tmp in glob(f'/sys/class/net/{br_if}/lower_*'): bridge_members.append(os.path.basename(tmp).replace('lower_', '')) self.assertIn(eth_if, bridge_members) self.assertIn(vxlan_if, bridge_members) self.cli_delete(['interfaces', 'bridge', br_if]) self.cli_delete(['interfaces', 'vxlan', vxlan_if]) self.cli_delete(['interfaces', 'tunnel', tunnel_if]) self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) + def test_bridge_vlan_protocol(self): + protocol = '802.1ad' + + # Add member interface to bridge and set VLAN filter + for interface in self._interfaces: + self.cli_set(self._base_path + [interface, 'protocol', protocol]) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + tmp = get_interface_config(interface) + self.assertEqual(protocol, tmp['linkinfo']['info_data']['vlan_protocol']) + if __name__ == '__main__': unittest.main(verbosity=2)