summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorViacheslav Hletenko <v.gletenko@vyos.io>2026-06-23 13:36:44 +0300
committerGitHub <noreply@github.com>2026-06-23 13:36:44 +0300
commitd1e320047348d90202a23a598cd771e41cb0be24 (patch)
treeb64a18c06ed6f94ab04ad8dbe7acb3c4645deedd /python
parentd935b5a83b0fa81e3fc9d727cedbda7575f60d85 (diff)
parentd90693fd0da03f8de4ccc4a5a85e2f2c9823e682 (diff)
downloadvyos-1x-d1e320047348d90202a23a598cd771e41cb0be24.tar.gz
vyos-1x-d1e320047348d90202a23a598cd771e41cb0be24.zip
Merge pull request #5156 from alexandr-san4ez/T8540-current
pseudo-ethernet: T8540: Add anycast-gateway support for EVPN
Diffstat (limited to 'python')
-rw-r--r--python/vyos/ifconfig/bridge.py51
-rw-r--r--python/vyos/ifconfig/macvlan.py70
-rw-r--r--python/vyos/utils/network.py35
3 files changed, 156 insertions, 0 deletions
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index f1cc7c0e0..92596d07c 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -13,6 +13,8 @@
# 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/>.
+import json
+
from vyos.ifconfig.interface import Interface
from vyos.utils.assertion import assert_boolean
from vyos.utils.assertion import assert_list
@@ -97,6 +99,13 @@ class BridgeIf(Interface):
},
}}
+ _command_get = {**Interface._command_get, **{
+ 'fdb_entries': {
+ 'shellcmd': 'bridge -json -detail fdb show dev {ifname}',
+ 'format': json.loads,
+ },
+ }}
+
_command_set = {**Interface._command_set, **{
'add_port': {
'shellcmd': 'ip link set dev {value} master {ifname}',
@@ -104,6 +113,12 @@ class BridgeIf(Interface):
'del_port': {
'shellcmd': 'ip link set dev {value} nomaster',
},
+ 'add_local_fdb_entry': {
+ 'shellcmd': 'bridge fdb add {value} dev {ifname} self local',
+ },
+ 'del_local_fdb_entry': {
+ 'shellcmd': 'bridge fdb del {value} dev {ifname} self local',
+ },
}}
def _create(self):
@@ -272,6 +287,32 @@ class BridgeIf(Interface):
return self.set_interface('vlan_protocol', map[protocol])
+ def get_fdb_entries(self):
+ """
+ Get the bridge forwarding database (FDB) entries
+ """
+ return self.get_interface('fdb_entries')
+
+ def add_local_fdb_entry(self, mac_address: str):
+ """
+ Add a local FDB entry for the given MAC address on the bridge interface.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').add_local_fdb_entry('cc:38:2e:bf:7b:0d')
+ """
+ self.set_interface('add_local_fdb_entry', mac_address.lower())
+
+ def del_local_fdb_entry(self, mac_address: str):
+ """
+ Remove a local FDB entry for the given MAC address on the bridge interface.
+
+ Example:
+ >>> from vyos.ifconfig import BridgeIf
+ >>> BridgeIf('br0').del_local_fdb_entry('cc:38:2e:bf:7b:0d')
+ """
+ self.set_interface('del_local_fdb_entry', mac_address.lower())
+
def update(self, config):
""" General helper function which works on a dictionary retrieved by
get_config_dict(). It's main intention is to consolidate the scattered
@@ -332,6 +373,16 @@ class BridgeIf(Interface):
cmd = f'bridge vlan del dev {self.ifname} vid {vlan} self'
self._cmd(cmd)
+ # After deleting vif, the FDB entry for that VLAN can linger.
+ # We should remove it manually:
+ fdb_entries = self.get_fdb_entries()
+ for entry in fdb_entries:
+ mac = entry.get('mac')
+ entry_vlan = entry.get('vlan')
+ if entry_vlan and mac and str(entry_vlan) == vlan:
+ cmd = f'bridge fdb del {mac} dev {self.ifname} vlan {vlan}'
+ self._cmd(cmd)
+
for vlan in config.get('vif', {}):
cmd = f'bridge vlan add dev {self.ifname} vid {vlan} self'
self._cmd(cmd)
diff --git a/python/vyos/ifconfig/macvlan.py b/python/vyos/ifconfig/macvlan.py
index 7a26f9ef5..45fd1d351 100644
--- a/python/vyos/ifconfig/macvlan.py
+++ b/python/vyos/ifconfig/macvlan.py
@@ -14,6 +14,10 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
from vyos.ifconfig.interface import Interface
+from vyos.ifconfig import BridgeIf
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import interface_exists
+from vyos.utils.network import split_interface_vlans
@Interface.register
class MACVLANIf(Interface):
@@ -40,6 +44,72 @@ class MACVLANIf(Interface):
# interface is always A/D down. It needs to be enabled explicitly
self.set_admin_state('down')
+ def _get_bridge_by_source(self, source_interface: str) -> BridgeIf | None:
+ """
+ Resolve a source interface name to its root BridgeIf object.
+
+ Handles plain bridge names (e.g. 'br0') and bridge sub-interfaces
+ with one or two VLAN suffixes (e.g. 'br0.100', 'br0.100.200').
+ """
+
+ bridge = None
+ if source_interface.startswith('br'):
+ # We only need the root bridge name, so unpack with *_ to
+ # discard the VLAN parts.
+ bridge_ifname, *_ = split_interface_vlans(source_interface)
+ if interface_exists(bridge_ifname):
+ bridge = BridgeIf(bridge_ifname)
+
+ return bridge
+
+ def _create_anycast_gateway(self, source_interface, mac):
+ """Install a local FDB entry on the parent bridge for the anycast MAC"""
+
+ if source_interface and mac:
+ bridge = self._get_bridge_by_source(source_interface)
+ if bridge:
+ bridge.add_local_fdb_entry(mac)
+
+ def _delete_anycast_gateway(self, source_interface, mac):
+ """Remove the local FDB entry from the parent bridge for the anycast MAC"""
+
+ if source_interface and mac:
+ bridge = self._get_bridge_by_source(source_interface)
+ if bridge:
+ try:
+ bridge.del_local_fdb_entry(mac)
+ except OSError:
+ pass # Bridge may already be gone, that is fine
+
+ def update(self, config):
+ # Always attempt to remove any existing anycast FDB entry before
+ # applying the new config. It reads the currently live
+ # state of the interface (before update).
+ self._delete_anycast_gateway(self.get_source_interface(), self.get_mac())
+
+ # Apply all standard interface configuration
+ super().update(config)
+
+ # After the interface is fully updated, install the new FDB entry if
+ # anycast-gateway is set in the incoming config.
+ if 'anycast_gateway' in self.config:
+ self._create_anycast_gateway(
+ self.config.get('source_interface'), self.get_mac()
+ )
+
+ def remove(self, skip_delete=False):
+ # Before tearing down the MACVLAN interface, clean up the anycast
+ # gateway FDB entry from the parent bridge.
+ self._delete_anycast_gateway(
+ self.get_source_interface(), self.get_mac()
+ )
+
+ return super().remove(skip_delete=skip_delete)
+
def set_mode(self, mode):
cmd = f'ip link set dev {self.ifname} type macvlan mode {mode}'
return self._cmd(cmd)
+
+ def get_source_interface(self):
+ interface_config = get_interface_config(self.ifname)
+ return interface_config['link'] if interface_config is not None else None
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 8544b2163..cde374d03 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -158,6 +158,41 @@ def get_vrf_tableid(interface: str):
table = tmp['linkinfo']['info_slave_data']['table']
return table
+
+def split_interface_vlans(interface: str) -> tuple:
+ """
+ Parse a interface name (with optional VLAN suffixes) into its
+ component parts.
+
+ Handles three input forms:
+ - 'br0' -> root bridge interface only
+ - 'br0.100' -> root bridge interface + one VLAN sub-interface level
+ - 'br0.100.200' -> root bridge interface + two VLAN sub-interface levels (QinQ)
+
+ Returns a tuple with the following parts on success:
+ (
+ 'br0', # root interface (always present)
+ '100', # first VLAN suffix (None if not present)
+ '200', # second VLAN suffix (None if not present)
+ )
+ """
+
+ parts = interface.split('.')
+
+ # Guard: we support at most bridge + 2 VLAN levels (e.g. br0.100.200)
+ if len(parts) > 3:
+ raise ValueError(
+ f'Interface "{interface}" has too many VLAN suffixes. '
+ 'Only up to two levels are supported (e.g. br0.100.200).'
+ )
+
+ iface = parts[0]
+ vlan_id = parts[1] if len(parts) >= 2 else None
+ inner_vlan_id = parts[2] if len(parts) == 3 else None
+
+ return iface, vlan_id, inner_vlan_id
+
+
def get_interface_config(interface):
""" Returns the used encapsulation protocol for given interface.
If interface does not exist, None is returned.