summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--interface-definitions/interfaces-bridge.xml.in33
-rw-r--r--op-mode-definitions/show-interfaces-bridge.xml6
-rw-r--r--python/vyos/configdict.py22
-rw-r--r--python/vyos/ifconfig/bridge.py58
-rw-r--r--python/vyos/ifconfig/l2tpv3.py25
-rw-r--r--python/vyos/validate.py1
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bridge.py92
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py60
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>&lt;n&gt;</format>
+ <description>Virtual Local Area Network (VLAN) ID</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;n-m&gt;-&lt;1-4096&gt;</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