Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F119062213
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
80 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json
index 91a757c16..4b24420e6 100644
--- a/data/config-mode-dependencies/vyos-1x.json
+++ b/data/config-mode-dependencies/vyos-1x.json
@@ -1,35 +1,38 @@
{
"firewall": {"group_resync": ["nat", "policy-route"]},
"http_api": {"https": ["https"]},
+ "interfaces_bonding": {
+ "ethernet": ["interfaces-ethernet"]
+ },
"pki": {
"ethernet": ["interfaces-ethernet"],
"openvpn": ["interfaces-openvpn"],
"https": ["https"],
"ipsec": ["vpn_ipsec"],
"openconnect": ["vpn_openconnect"],
"sstp": ["vpn_sstp"]
},
"qos": {
"bonding": ["interfaces-bonding"],
"bridge": ["interfaces-bridge"],
"dummy": ["interfaces-dummy"],
"ethernet": ["interfaces-ethernet"],
"geneve": ["interfaces-geneve"],
"input": ["interfaces-input"],
"l2tpv3": ["interfaces-l2tpv3"],
"loopback": ["interfaces-loopback"],
"macsec": ["interfaces-macsec"],
"openvpn": ["interfaces-openvpn"],
"pppoe": ["interfaces-pppoe"],
"pseudo-ethernet": ["interfaces-pseudo-ethernet"],
"tunnel": ["interfaces-tunnel"],
"vti": ["interfaces-vti"],
"vxlan": ["interfaces-vxlan"],
"wireguard": ["interfaces-wireguard"],
"wireless": ["interfaces-wireless"],
"wwan": ["interfaces-wwan"]
},
"vpp": {
"ethernet": ["interfaces-ethernet"]
}
}
diff --git a/git b/git
new file mode 100644
index 000000000..e69de29bb
diff --git a/interface-definitions/include/version/interfaces-version.xml.i b/interface-definitions/include/version/interfaces-version.xml.i
index 3d11ce888..76c5d3c05 100644
--- a/interface-definitions/include/version/interfaces-version.xml.i
+++ b/interface-definitions/include/version/interfaces-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/interfaces-version.xml.i -->
-<syntaxVersion component='interfaces' version='30'></syntaxVersion>
+<syntaxVersion component='interfaces' version='31'></syntaxVersion>
<!-- include end -->
diff --git a/python/vyos/ifconfig/bond.py b/python/vyos/ifconfig/bond.py
index d1d7d48c4..45e6e4c16 100644
--- a/python/vyos/ifconfig/bond.py
+++ b/python/vyos/ifconfig/bond.py
@@ -1,464 +1,477 @@
# Copyright 2019-2022 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/>.
import os
from vyos.ifconfig.interface import Interface
from vyos.utils.process import cmd
from vyos.utils.dict import dict_search
from vyos.utils.assertion import assert_list
from vyos.utils.assertion import assert_positive
@Interface.register
class BondIf(Interface):
"""
The Linux bonding driver provides a method for aggregating multiple network
interfaces into a single logical "bonded" interface. The behavior of the
bonded interfaces depends upon the mode; generally speaking, modes provide
either hot standby or load balancing services. Additionally, link integrity
monitoring may be performed.
"""
iftype = 'bond'
definition = {
**Interface.definition,
** {
'section': 'bonding',
'prefixes': ['bond', ],
'broadcast': True,
'bridgeable': True,
},
}
_sysfs_set = {**Interface._sysfs_set, **{
'bond_hash_policy': {
'validate': lambda v: assert_list(v, ['layer2', 'layer2+3', 'layer3+4', 'encap2+3', 'encap3+4']),
'location': '/sys/class/net/{ifname}/bonding/xmit_hash_policy',
},
'bond_min_links': {
'validate': assert_positive,
'location': '/sys/class/net/{ifname}/bonding/min_links',
},
'bond_lacp_rate': {
'validate': lambda v: assert_list(v, ['slow', 'fast']),
'location': '/sys/class/net/{ifname}/bonding/lacp_rate',
},
'bond_miimon': {
'validate': assert_positive,
'location': '/sys/class/net/{ifname}/bonding/miimon'
},
'bond_arp_interval': {
'validate': assert_positive,
'location': '/sys/class/net/{ifname}/bonding/arp_interval'
},
'bond_arp_ip_target': {
# XXX: no validation of the IP
'location': '/sys/class/net/{ifname}/bonding/arp_ip_target',
},
'bond_add_port': {
'location': '/sys/class/net/{ifname}/bonding/slaves',
},
'bond_del_port': {
'location': '/sys/class/net/{ifname}/bonding/slaves',
},
'bond_primary': {
'convert': lambda name: name if name else '\0',
'location': '/sys/class/net/{ifname}/bonding/primary',
},
'bond_mode': {
'validate': lambda v: assert_list(v, ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb']),
'location': '/sys/class/net/{ifname}/bonding/mode',
},
}}
_sysfs_get = {**Interface._sysfs_get, **{
'bond_arp_ip_target': {
'location': '/sys/class/net/{ifname}/bonding/arp_ip_target',
},
'bond_mode': {
'location': '/sys/class/net/{ifname}/bonding/mode',
}
}}
+ @staticmethod
+ def get_inherit_bond_options() -> list:
+ """
+ Returns list of option
+ which are inherited from bond interface to member interfaces
+ :return: List of interface options
+ :rtype: list
+ """
+ options = [
+ 'mtu'
+ ]
+ return options
+
def remove(self):
"""
Remove interface from operating system. Removing the interface
deconfigures all assigned IP addresses and clear possible DHCP(v6)
client processes.
Example:
>>> from vyos.ifconfig import Interface
>>> i = Interface('eth0')
>>> i.remove()
"""
# when a bond member gets deleted, all members are placed in A/D state
# even when they are enabled inside CLI. This will make the config
# and system look async.
slave_list = []
for s in self.get_slaves():
slave = {
'ifname': s,
'state': Interface(s).get_admin_state()
}
slave_list.append(slave)
# remove bond master which places members in disabled state
super().remove()
# replicate previous interface state before bond destruction back to
# physical interface
for slave in slave_list:
i = Interface(slave['ifname'])
i.set_admin_state(slave['state'])
def set_hash_policy(self, mode):
"""
Selects the transmit hash policy to use for slave selection in
balance-xor, 802.3ad, and tlb modes. Possible values are: layer2,
layer2+3, layer3+4, encap2+3, encap3+4.
The default value is layer2
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_hash_policy('layer2+3')
"""
self.set_interface('bond_hash_policy', mode)
def set_min_links(self, number):
"""
Specifies the minimum number of links that must be active before
asserting carrier. It is similar to the Cisco EtherChannel min-links
feature. This allows setting the minimum number of member ports that
must be up (link-up state) before marking the bond device as up
(carrier on). This is useful for situations where higher level services
such as clustering want to ensure a minimum number of low bandwidth
links are active before switchover. This option only affect 802.3ad
mode.
The default value is 0. This will cause carrier to be asserted (for
802.3ad mode) whenever there is an active aggregator, regardless of the
number of available links in that aggregator. Note that, because an
aggregator cannot be active without at least one available link,
setting this option to 0 or to 1 has the exact same effect.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_min_links('0')
"""
self.set_interface('bond_min_links', number)
def set_lacp_rate(self, slow_fast):
"""
Option specifying the rate in which we'll ask our link partner
to transmit LACPDU packets in 802.3ad mode. Possible values
are:
slow or 0
Request partner to transmit LACPDUs every 30 seconds
fast or 1
Request partner to transmit LACPDUs every 1 second
The default is slow.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_lacp_rate('slow')
"""
self.set_interface('bond_lacp_rate', slow_fast)
def set_miimon_interval(self, interval):
"""
Specifies the MII link monitoring frequency in milliseconds. This
determines how often the link state of each slave is inspected for link
failures. A value of zero disables MII link monitoring. A value of 100
is a good starting point.
The default value is 0.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_miimon_interval('100')
"""
return self.set_interface('bond_miimon', interval)
def set_arp_interval(self, interval):
"""
Specifies the ARP link monitoring frequency in milliseconds.
The ARP monitor works by periodically checking the slave devices
to determine whether they have sent or received traffic recently
(the precise criteria depends upon the bonding mode, and the
state of the slave). Regular traffic is generated via ARP probes
issued for the addresses specified by the arp_ip_target option.
If ARP monitoring is used in an etherchannel compatible mode
(modes 0 and 2), the switch should be configured in a mode that
evenly distributes packets across all links. If the switch is
configured to distribute the packets in an XOR fashion, all
replies from the ARP targets will be received on the same link
which could cause the other team members to fail.
value of 0 disables ARP monitoring. The default value is 0.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_arp_interval('100')
"""
return self.set_interface('bond_arp_interval', interval)
def get_arp_ip_target(self):
"""
Specifies the IP addresses to use as ARP monitoring peers when
arp_interval is > 0. These are the targets of the ARP request sent to
determine the health of the link to the targets. Specify these values
in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
a comma. At least one IP address must be given for ARP monitoring to
function. The maximum number of targets that can be specified is 16.
The default value is no IP addresses.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').get_arp_ip_target()
'192.0.2.1'
"""
# As this function might also be called from update() of a VLAN interface
# we must check if the bond_arp_ip_target retrieval worked or not - as this
# can not be set for a bond vif interface
try:
return self.get_interface('bond_arp_ip_target')
except FileNotFoundError:
return ''
def set_arp_ip_target(self, target):
"""
Specifies the IP addresses to use as ARP monitoring peers when
arp_interval is > 0. These are the targets of the ARP request sent to
determine the health of the link to the targets. Specify these values
in ddd.ddd.ddd.ddd format. Multiple IP addresses must be separated by
a comma. At least one IP address must be given for ARP monitoring to
function. The maximum number of targets that can be specified is 16.
The default value is no IP addresses.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_arp_ip_target('192.0.2.1')
>>> BondIf('bond0').get_arp_ip_target()
'192.0.2.1'
"""
return self.set_interface('bond_arp_ip_target', target)
def add_port(self, interface):
"""
Enslave physical interface to bond.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').add_port('eth0')
>>> BondIf('bond0').add_port('eth1')
"""
# From drivers/net/bonding/bond_main.c:
# ...
# bond_set_slave_link_state(new_slave,
# BOND_LINK_UP,
# BOND_SLAVE_NOTIFY_NOW);
# ...
#
# The kernel will ALWAYS place new bond members in "up" state regardless
# what the CLI will tell us!
# Physical interface must be in admin down state before they can be
# enslaved. If this is not the case an error will be shown:
# bond0: eth0 is up - this may be due to an out of date ifenslave
slave = Interface(interface)
slave_state = slave.get_admin_state()
if slave_state == 'up':
slave.set_admin_state('down')
ret = self.set_interface('bond_add_port', f'+{interface}')
# The kernel will ALWAYS place new bond members in "up" state regardless
# what the LI is configured for - thus we place the interface in its
# desired state
slave.set_admin_state(slave_state)
return ret
def del_port(self, interface):
"""
Remove physical port from bond
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').del_port('eth1')
"""
return self.set_interface('bond_del_port', f'-{interface}')
def get_slaves(self):
"""
Return a list with all configured slave interfaces on this bond.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').get_slaves()
['eth1', 'eth2']
"""
enslaved_ifs = []
# retrieve real enslaved interfaces from OS kernel
sysfs_bond = '/sys/class/net/{}'.format(self.config['ifname'])
if os.path.isdir(sysfs_bond):
for directory in os.listdir(sysfs_bond):
if 'lower_' in directory:
enslaved_ifs.append(directory.replace('lower_', ''))
return enslaved_ifs
def get_mode(self):
"""
Return bond operation mode.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').get_mode()
'802.3ad'
"""
mode = self.get_interface('bond_mode')
# mode is now "802.3ad 4", we are only interested in "802.3ad"
return mode.split()[0]
def set_primary(self, interface):
"""
A string (eth0, eth2, etc) specifying which slave is the primary
device. The specified device will always be the active slave while it
is available. Only when the primary is off-line will alternate devices
be used. This is useful when one slave is preferred over another, e.g.,
when one slave has higher throughput than another.
The primary option is only valid for active-backup, balance-tlb and
balance-alb mode.
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_primary('eth2')
"""
return self.set_interface('bond_primary', interface)
def set_mode(self, mode):
"""
Specifies one of the bonding policies. The default is balance-rr
(round robin).
Possible values are: balance-rr, active-backup, balance-xor,
broadcast, 802.3ad, balance-tlb, balance-alb
NOTE: the bonding mode can not be changed when the bond itself has
slaves
Example:
>>> from vyos.ifconfig import BondIf
>>> BondIf('bond0').set_mode('802.3ad')
"""
return self.set_interface('bond_mode', mode)
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. """
# use ref-counting function to place an interface into admin down state.
# set_admin_state_up() must be called the same amount of times else the
# interface won't come up. This can/should be used to prevent link flapping
# when changing interface parameters require the interface to be down.
# We will disable it once before reconfiguration and enable it afterwards.
if 'shutdown_required' in config:
self.set_admin_state('down')
# Specifies the MII link monitoring frequency in milliseconds
value = config.get('mii_mon_interval')
self.set_miimon_interval(value)
# Bonding transmit hash policy
value = config.get('hash_policy')
if value: self.set_hash_policy(value)
# Minimum number of member interfaces
value = config.get('min_links')
if value: self.set_min_links(value)
# Some interface options can only be changed if the interface is
# administratively down
if self.get_admin_state() == 'down':
# Remove ALL bond member interfaces
for interface in self.get_slaves():
self.del_port(interface)
# Restore correct interface status based on config
if dict_search(f'member.interface.{interface}.disable', config) is not None or \
dict_search(f'member.interface_remove.{interface}.disable', config) is not None:
Interface(interface).set_admin_state('down')
else:
Interface(interface).set_admin_state('up')
# Bonding policy/mode - default value, always present
mode = config.get('mode')
self.set_mode(mode)
# LACPDU transmission rate - default value
if mode == '802.3ad':
self.set_lacp_rate(config.get('lacp_rate'))
if mode not in ['802.3ad', 'balance-tlb', 'balance-alb']:
tmp = dict_search('arp_monitor.interval', config)
value = tmp if (tmp != None) else '0'
self.set_arp_interval(value)
# ARP monitor targets need to be synchronized between sysfs and CLI.
# Unfortunately an address can't be send twice to sysfs as this will
# result in the following exception: OSError: [Errno 22] Invalid argument.
#
# We remove ALL addresses prior to adding new ones, this will remove
# addresses manually added by the user too - but as we are limited to 16 adresses
# from the kernel side this looks valid to me. We won't run into an error
# when a user added manual adresses which would result in having more
# then 16 adresses in total.
arp_tgt_addr = list(map(str, self.get_arp_ip_target().split()))
for addr in arp_tgt_addr:
self.set_arp_ip_target('-' + addr)
# Add configured ARP target addresses
value = dict_search('arp_monitor.target', config)
if isinstance(value, str):
value = [value]
if value:
for addr in value:
self.set_arp_ip_target('+' + addr)
# Add (enslave) interfaces to bond
value = dict_search('member.interface', config)
for interface in (value or []):
# if we've come here we already verified the interface
# does not have an addresses configured so just flush
# any remaining ones
Interface(interface).flush_addrs()
self.add_port(interface)
# Primary device interface - must be set after 'mode'
value = config.get('primary')
if value: self.set_primary(value)
# call base class first
super().update(config)
diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py
index 24ce3a803..96e5f513b 100644
--- a/python/vyos/ifconfig/ethernet.py
+++ b/python/vyos/ifconfig/ethernet.py
@@ -1,389 +1,423 @@
# Copyright 2019-2023 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/>.
import os
from glob import glob
from vyos.base import Warning
from vyos.ethtool import Ethtool
from vyos.ifconfig.interface import Interface
from vyos.utils.dict import dict_search
from vyos.utils.file import read_file
from vyos.utils.process import run
from vyos.utils.assertion import assert_list
@Interface.register
class EthernetIf(Interface):
"""
Abstraction of a Linux Ethernet Interface
"""
iftype = 'ethernet'
definition = {
**Interface.definition,
**{
'section': 'ethernet',
'prefixes': ['lan', 'eth', 'eno', 'ens', 'enp', 'enx'],
'bondable': True,
'broadcast': True,
'bridgeable': True,
'eternal': '(lan|eth|eno|ens|enp|enx)[0-9]+$',
}
}
@staticmethod
def feature(ifname, option, value):
run(f'ethtool --features {ifname} {option} {value}')
return False
_command_set = {**Interface._command_set, **{
'gro': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'gro', v),
},
'gso': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'gso', v),
},
'lro': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'lro', v),
},
'sg': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'sg', v),
},
'tso': {
'validate': lambda v: assert_list(v, ['on', 'off']),
'possible': lambda i, v: EthernetIf.feature(i, 'tso', v),
},
}}
+ @staticmethod
+ def get_bond_member_allowed_options() -> list:
+ """
+ Return list of options which are allowed for changing,
+ when interface is a bond member
+ :return: List of interface options
+ :rtype: list
+ """
+ bond_allowed_sections = [
+ 'description',
+ 'disable',
+ 'disable_flow_control',
+ 'disable_link_detect',
+ 'duplex',
+ 'eapol.ca_certificate',
+ 'eapol.certificate',
+ 'eapol.passphrase',
+ 'mirror.egress',
+ 'mirror.ingress',
+ 'offload.gro',
+ 'offload.gso',
+ 'offload.lro',
+ 'offload.rfs',
+ 'offload.rps',
+ 'offload.sg',
+ 'offload.tso',
+ 'redirect',
+ 'ring_buffer.rx',
+ 'ring_buffer.tx',
+ 'speed',
+ 'hw_id'
+ ]
+ return bond_allowed_sections
+
def __init__(self, ifname, **kargs):
super().__init__(ifname, **kargs)
self.ethtool = Ethtool(ifname)
def remove(self):
"""
Remove interface from config. Removing the interface deconfigures all
assigned IP addresses.
Example:
>>> from vyos.ifconfig import WWANIf
>>> i = EthernetIf('eth0')
>>> i.remove()
"""
if self.exists(self.ifname):
# interface is placed in A/D state when removed from config! It
# will remain visible for the operating system.
self.set_admin_state('down')
super().remove()
def set_flow_control(self, enable):
"""
Changes the pause parameters of the specified Ethernet device.
@param enable: true -> enable pause frames, false -> disable pause frames
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_flow_control(True)
"""
ifname = self.config['ifname']
if enable not in ['on', 'off']:
raise ValueError("Value out of range")
if not self.ethtool.check_flow_control():
self._debug_msg(f'NIC driver does not support changing flow control settings!')
return False
current = self.ethtool.get_flow_control()
if current != enable:
# Assemble command executed on system. Unfortunately there is no way
# to change this setting via sysfs
cmd = f'ethtool --pause {ifname} autoneg {enable} tx {enable} rx {enable}'
output, code = self._popen(cmd)
if code:
Warning(f'could not change "{ifname}" flow control setting!')
return output
return None
def set_speed_duplex(self, speed, duplex):
"""
Set link speed in Mbit/s and duplex.
@speed can be any link speed in MBit/s, e.g. 10, 100, 1000 auto
@duplex can be half, full, auto
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_speed_duplex('auto', 'auto')
"""
ifname = self.config['ifname']
if speed not in ['auto', '10', '100', '1000', '2500', '5000', '10000',
'25000', '40000', '50000', '100000', '400000']:
raise ValueError("Value out of range (speed)")
if duplex not in ['auto', 'full', 'half']:
raise ValueError("Value out of range (duplex)")
if not self.ethtool.check_speed_duplex(speed, duplex):
Warning(f'changing speed/duplex setting on "{ifname}" is unsupported!')
return
if not self.ethtool.check_auto_negotiation_supported():
Warning(f'changing auto-negotiation setting on "{ifname}" is unsupported!')
return
# Get current speed and duplex settings:
ifname = self.config['ifname']
if self.ethtool.get_auto_negotiation():
if speed == 'auto' and duplex == 'auto':
# bail out early as nothing is to change
return
else:
# XXX: read in current speed and duplex settings
# There are some "nice" NICs like AX88179 which do not support
# reading the speed thus we simply fallback to the supplied speed
# to not cause any change here and raise an exception.
cur_speed = read_file(f'/sys/class/net/{ifname}/speed', speed)
cur_duplex = read_file(f'/sys/class/net/{ifname}/duplex', duplex)
if (cur_speed == speed) and (cur_duplex == duplex):
# bail out early as nothing is to change
return
cmd = f'ethtool --change {ifname}'
try:
if speed == 'auto' or duplex == 'auto':
cmd += ' autoneg on'
else:
cmd += f' speed {speed} duplex {duplex} autoneg off'
return self._cmd(cmd)
except PermissionError:
# Some NICs do not tell that they don't suppport settings speed/duplex,
# but they do not actually support it either.
# In that case it's probably better to ignore the error
# than end up with a broken config.
print('Warning: could not set speed/duplex settings: operation not permitted!')
def set_gro(self, state):
"""
Enable Generic Receive Offload. State can be either True or False.
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_gro(True)
"""
if not isinstance(state, bool):
raise ValueError('Value out of range')
enabled, fixed = self.ethtool.get_generic_receive_offload()
if enabled != state:
if not fixed:
return self.set_interface('gro', 'on' if state else 'off')
else:
print('Adapter does not support changing generic-receive-offload settings!')
return False
def set_gso(self, state):
"""
Enable Generic Segmentation offload. State can be either True or False.
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_gso(True)
"""
if not isinstance(state, bool):
raise ValueError('Value out of range')
enabled, fixed = self.ethtool.get_generic_segmentation_offload()
if enabled != state:
if not fixed:
return self.set_interface('gso', 'on' if state else 'off')
else:
print('Adapter does not support changing generic-segmentation-offload settings!')
return False
def set_lro(self, state):
"""
Enable Large Receive offload. State can be either True or False.
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_lro(True)
"""
if not isinstance(state, bool):
raise ValueError('Value out of range')
enabled, fixed = self.ethtool.get_large_receive_offload()
if enabled != state:
if not fixed:
return self.set_interface('lro', 'on' if state else 'off')
else:
print('Adapter does not support changing large-receive-offload settings!')
return False
def set_rps(self, state):
if not isinstance(state, bool):
raise ValueError('Value out of range')
rps_cpus = 0
queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*'))
if state:
# Enable RPS on all available CPUs except CPU0 which we will not
# utilize so the system has one spare core when it's under high
# preasure to server other means. Linux sysfs excepts a bitmask
# representation of the CPUs which should participate on RPS, we
# can enable more CPUs that are physically present on the system,
# Linux will clip that internally!
rps_cpus = (1 << os.cpu_count()) -1
# XXX: we should probably reserve one core when the system is under
# high preasure so we can still have a core left for housekeeping.
# This is done by masking out the lowst bit so CPU0 is spared from
# receive packet steering.
rps_cpus &= ~1
for i in range(0, queues):
self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_cpus', f'{rps_cpus:x}')
# send bitmask representation as hex string without leading '0x'
return True
def set_rfs(self, state):
rfs_flow = 0
queues = len(glob(f'/sys/class/net/{self.ifname}/queues/rx-*'))
if state:
global_rfs_flow = 32768
rfs_flow = int(global_rfs_flow/queues)
for i in range(0, queues):
self._write_sysfs(f'/sys/class/net/{self.ifname}/queues/rx-{i}/rps_flow_cnt', rfs_flow)
return True
def set_sg(self, state):
"""
Enable Scatter-Gather support. State can be either True or False.
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_sg(True)
"""
if not isinstance(state, bool):
raise ValueError('Value out of range')
enabled, fixed = self.ethtool.get_scatter_gather()
if enabled != state:
if not fixed:
return self.set_interface('sg', 'on' if state else 'off')
else:
print('Adapter does not support changing scatter-gather settings!')
return False
def set_tso(self, state):
"""
Enable TCP segmentation offloading. State can be either True or False.
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_tso(False)
"""
if not isinstance(state, bool):
raise ValueError('Value out of range')
enabled, fixed = self.ethtool.get_tcp_segmentation_offload()
if enabled != state:
if not fixed:
return self.set_interface('tso', 'on' if state else 'off')
else:
print('Adapter does not support changing tcp-segmentation-offload settings!')
return False
def set_ring_buffer(self, rx_tx, size):
"""
Example:
>>> from vyos.ifconfig import EthernetIf
>>> i = EthernetIf('eth0')
>>> i.set_ring_buffer('rx', '4096')
"""
current_size = self.ethtool.get_ring_buffer(rx_tx)
if current_size == size:
# bail out early if nothing is about to change
return None
ifname = self.config['ifname']
cmd = f'ethtool --set-ring {ifname} {rx_tx} {size}'
output, code = self._popen(cmd)
# ethtool error codes:
# 80 - value already setted
# 81 - does not possible to set value
if code and code != 80:
print(f'could not set "{rx_tx}" ring-buffer for {ifname}')
return output
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. """
# disable ethernet flow control (pause frames)
value = 'off' if 'disable_flow_control' in config else 'on'
self.set_flow_control(value)
# GRO (generic receive offload)
self.set_gro(dict_search('offload.gro', config) != None)
# GSO (generic segmentation offload)
self.set_gso(dict_search('offload.gso', config) != None)
# LRO (large receive offload)
self.set_lro(dict_search('offload.lro', config) != None)
# RPS - Receive Packet Steering
self.set_rps(dict_search('offload.rps', config) != None)
# RFS - Receive Flow Steering
self.set_rfs(dict_search('offload.rfs', config) != None)
# scatter-gather option
self.set_sg(dict_search('offload.sg', config) != None)
# TSO (TCP segmentation offloading)
self.set_tso(dict_search('offload.tso', config) != None)
# Set physical interface speed and duplex
if 'speed_duplex_changed' in config:
if {'speed', 'duplex'} <= set(config):
speed = config.get('speed')
duplex = config.get('duplex')
self.set_speed_duplex(speed, duplex)
# Set interface ring buffer
if 'ring_buffer' in config:
for rx_tx, size in config['ring_buffer'].items():
self.set_ring_buffer(rx_tx, size)
# call base class last
super().update(config)
diff --git a/python/vyos/utils/dict.py b/python/vyos/utils/dict.py
index 9484eacdd..d36b6fcfb 100644
--- a/python/vyos/utils/dict.py
+++ b/python/vyos/utils/dict.py
@@ -1,307 +1,366 @@
# Copyright 2023 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/>.
def colon_separated_to_dict(data_string, uniquekeys=False):
""" Converts a string containing newline-separated entries
of colon-separated key-value pairs into a dict.
Such files are common in Linux /proc filesystem
Args:
data_string (str): data string
uniquekeys (bool): whether to insist that keys are unique or not
Returns: dict
Raises:
ValueError: if uniquekeys=True and the data string has
duplicate keys.
Note:
If uniquekeys=True, then dict entries are always strings,
otherwise they are always lists of strings.
"""
import re
key_value_re = re.compile('([^:]+)\s*\:\s*(.*)')
data_raw = re.split('\n', data_string)
data = {}
for l in data_raw:
l = l.strip()
if l:
match = re.match(key_value_re, l)
if match and (len(match.groups()) == 2):
key = match.groups()[0].strip()
value = match.groups()[1].strip()
else:
raise ValueError(f"""Line "{l}" could not be parsed a colon-separated pair """, l)
if key in data.keys():
if uniquekeys:
raise ValueError("Data string has duplicate keys: {0}".format(key))
else:
data[key].append(value)
else:
if uniquekeys:
data[key] = value
else:
data[key] = [value]
else:
pass
return data
def mangle_dict_keys(data, regex, replacement, abs_path=None, no_tag_node_value_mangle=False):
""" Mangles dict keys according to a regex and replacement character.
Some libraries like Jinja2 do not like certain characters in dict keys.
This function can be used for replacing all offending characters
with something acceptable.
Args:
data (dict): Original dict to mangle
regex, replacement (str): arguments to re.sub(regex, replacement, ...)
abs_path (list): if data is a config dict and no_tag_node_value_mangle is True
then abs_path should be the absolute config path to the first
keys of data, non-inclusive
no_tag_node_value_mangle (bool): do not mangle keys of tag node values
Returns: dict
"""
import re
from vyos.xml_ref import is_tag_value
if abs_path is None:
abs_path = []
new_dict = type(data)()
for k in data.keys():
if no_tag_node_value_mangle and is_tag_value(abs_path + [k]):
new_key = k
else:
new_key = re.sub(regex, replacement, k)
value = data[k]
if isinstance(value, dict):
new_dict[new_key] = mangle_dict_keys(value, regex, replacement,
abs_path=abs_path + [k],
no_tag_node_value_mangle=no_tag_node_value_mangle)
else:
new_dict[new_key] = value
return new_dict
def _get_sub_dict(d, lpath):
k = lpath[0]
if k not in d.keys():
return {}
c = {k: d[k]}
lpath = lpath[1:]
if not lpath:
return c
elif not isinstance(c[k], dict):
return {}
return _get_sub_dict(c[k], lpath)
def get_sub_dict(source, lpath, get_first_key=False):
""" Returns the sub-dict of a nested dict, defined by path of keys.
Args:
source (dict): Source dict to extract from
lpath (list[str]): sequence of keys
Returns: source, if lpath is empty, else
{key : source[..]..[key]} for key the last element of lpath, if exists
{} otherwise
"""
if not isinstance(source, dict):
raise TypeError("source must be of type dict")
if not isinstance(lpath, list):
raise TypeError("path must be of type list")
if not lpath:
return source
ret = _get_sub_dict(source, lpath)
if get_first_key and lpath and ret:
tmp = next(iter(ret.values()))
if not isinstance(tmp, dict):
raise TypeError("Data under node is not of type dict")
ret = tmp
return ret
def dict_search(path, dict_object):
""" Traverse Python dictionary (dict_object) delimited by dot (.).
Return value of key if found, None otherwise.
This is faster implementation then jmespath.search('foo.bar', dict_object)"""
if not isinstance(dict_object, dict) or not path:
return None
parts = path.split('.')
inside = parts[:-1]
if not inside:
if path not in dict_object:
return None
return dict_object[path]
c = dict_object
for p in parts[:-1]:
c = c.get(p, {})
return c.get(parts[-1], None)
def dict_search_args(dict_object, *path):
# Traverse dictionary using variable arguments
# Added due to above function not allowing for '.' in the key names
# Example: dict_search_args(some_dict, 'key', 'subkey', 'subsubkey', ...)
if not isinstance(dict_object, dict) or not path:
return None
for item in path:
if item not in dict_object:
return None
dict_object = dict_object[item]
return dict_object
def dict_search_recursive(dict_object, key, path=[]):
""" Traverse a dictionary recurisvely and return the value of the key
we are looking for.
Thankfully copied from https://stackoverflow.com/a/19871956
Modified to yield optional path to found keys
"""
if isinstance(dict_object, list):
for i in dict_object:
new_path = path + [i]
for x in dict_search_recursive(i, key, new_path):
yield x
elif isinstance(dict_object, dict):
if key in dict_object:
new_path = path + [key]
yield dict_object[key], new_path
for k, j in dict_object.items():
new_path = path + [k]
for x in dict_search_recursive(j, key, new_path):
yield x
+
+def dict_set(key_path, value, dict_object):
+ """ Set value to Python dictionary (dict_object) using path to key delimited by dot (.).
+ The key will be added if it does not exist.
+ """
+ path_list = key_path.split(".")
+ dynamic_dict = dict_object
+ if len(path_list) > 0:
+ for i in range(0, len(path_list)-1):
+ dynamic_dict = dynamic_dict[path_list[i]]
+ dynamic_dict[path_list[len(path_list)-1]] = value
+
+def dict_delete(key_path, dict_object):
+ """ Delete key in Python dictionary (dict_object) using path to key delimited by dot (.).
+ """
+ path_dict = dict_object
+ path_list = key_path.split('.')
+ inside = path_list[:-1]
+ if not inside:
+ del dict_object[path_list]
+ else:
+ for key in path_list[:-1]:
+ path_dict = path_dict[key]
+ del path_dict[path_list[len(path_list)-1]]
+
def dict_to_list(d, save_key_to=None):
""" Convert a dict to a list of dicts.
Optionally, save the original key of the dict inside
dicts stores in that list.
"""
def save_key(i, k):
if isinstance(i, dict):
i[save_key_to] = k
return
elif isinstance(i, list):
for _i in i:
save_key(_i, k)
else:
raise ValueError(f"Cannot save the key: the item is {type(i)}, not a dict")
collect = []
for k,_ in d.items():
item = d[k]
if save_key_to is not None:
save_key(item, k)
if isinstance(item, list):
collect += item
else:
collect.append(item)
return collect
+def dict_to_paths_values(conf: dict) -> dict:
+ """
+ Convert nested dictionary to simple dictionary, where key is a path is delimited by dot (.).
+ """
+ list_of_paths = []
+ dict_of_options ={}
+ for path in dict_to_key_paths(conf):
+ str_path = '.'.join(path)
+ list_of_paths.append(str_path)
+
+ for path in list_of_paths:
+ dict_of_options[path] = dict_search(path,conf)
+
+ return dict_of_options
+def dict_to_key_paths(d: dict) -> list:
+ """ Generator to return list of key paths from dict of list[str]|str
+ """
+ def func(d, path):
+ if isinstance(d, dict):
+ if not d:
+ yield path
+ for k, v in d.items():
+ for r in func(v, path + [k]):
+ yield r
+ elif isinstance(d, list):
+ yield path
+ elif isinstance(d, str):
+ yield path
+ else:
+ raise ValueError('object is not a dict of strings/list of strings')
+ for r in func(d, []):
+ yield r
+
def dict_to_paths(d: dict) -> list:
""" Generator to return list of paths from dict of list[str]|str
"""
def func(d, path):
if isinstance(d, dict):
if not d:
yield path
for k, v in d.items():
for r in func(v, path + [k]):
yield r
elif isinstance(d, list):
for i in d:
for r in func(i, path):
yield r
elif isinstance(d, str):
yield path + [d]
else:
raise ValueError('object is not a dict of strings/list of strings')
for r in func(d, []):
yield r
def check_mutually_exclusive_options(d, keys, required=False):
""" Checks if a dict has at most one or only one of
mutually exclusive keys.
"""
present_keys = []
for k in d:
if k in keys:
present_keys.append(k)
# Un-mangle the keys to make them match CLI option syntax
from re import sub
orig_keys = list(map(lambda s: sub(r'_', '-', s), keys))
orig_present_keys = list(map(lambda s: sub(r'_', '-', s), present_keys))
if len(present_keys) > 1:
raise ValueError(f"Options {orig_keys} are mutually-exclusive but more than one of them is present: {orig_present_keys}")
if required and (len(present_keys) < 1):
raise ValueError(f"At least one of the following options is required: {orig_keys}")
class FixedDict(dict):
"""
FixedDict: A dictionnary not allowing new keys to be created after initialisation.
>>> f = FixedDict(**{'count':1})
>>> f['count'] = 2
>>> f['king'] = 3
File "...", line ..., in __setitem__
raise ConfigError(f'Option "{k}" has no defined default')
"""
from vyos import ConfigError
def __init__(self, **options):
self._allowed = options.keys()
super().__init__(**options)
def __setitem__(self, k, v):
"""
__setitem__ is a builtin which is called by python when setting dict values:
>>> d = dict()
>>> d['key'] = 'value'
>>> d
{'key': 'value'}
is syntaxic sugar for
>>> d = dict()
>>> d.__setitem__('key','value')
>>> d
{'key': 'value'}
"""
if k not in self._allowed:
raise ConfigError(f'Option "{k}" has no defined default')
super().__setitem__(k, v)
+
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 0bd306ed0..1179e3e4f 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -1,247 +1,275 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-2022 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
from sys import exit
from netifaces import interfaces
-
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
from vyos.configdict import leaf_node_changed
from vyos.configdict import is_member
from vyos.configdict import is_source_interface
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.ifconfig import BondIf
+from vyos.ifconfig.ethernet import EthernetIf
from vyos.ifconfig import Section
from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_to_paths_values
from vyos.configdict import has_address_configured
from vyos.configdict import has_vrf_configured
+from vyos.configdep import set_dependents, call_dependents
from vyos import ConfigError
from vyos import airbag
airbag.enable()
def get_bond_mode(mode):
if mode == 'round-robin':
return 'balance-rr'
elif mode == 'active-backup':
return 'active-backup'
elif mode == 'xor-hash':
return 'balance-xor'
elif mode == 'broadcast':
return 'broadcast'
elif mode == '802.3ad':
return '802.3ad'
elif mode == 'transmit-load-balance':
return 'balance-tlb'
elif mode == 'adaptive-load-balance':
return 'balance-alb'
else:
raise ConfigError(f'invalid bond mode "{mode}"')
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
if config:
conf = config
else:
conf = Config()
base = ['interfaces', 'bonding']
ifname, bond = get_interface_dict(conf, base)
# To make our own life easier transfor the list of member interfaces
# into a dictionary - we will use this to add additional information
# later on for each member
if 'member' in bond and 'interface' in bond['member']:
# convert list of member interfaces to a dictionary
bond['member']['interface'] = {k: {} for k in bond['member']['interface']}
if 'mode' in bond:
bond['mode'] = get_bond_mode(bond['mode'])
tmp = is_node_changed(conf, base + [ifname, 'mode'])
if tmp: bond['shutdown_required'] = {}
tmp = is_node_changed(conf, base + [ifname, 'lacp-rate'])
if tmp: bond['shutdown_required'] = {}
# determine which members have been removed
interfaces_removed = leaf_node_changed(conf, base + [ifname, 'member', 'interface'])
-
# Reset config level to interfaces
old_level = conf.get_level()
conf.set_level(['interfaces'])
if interfaces_removed:
bond['shutdown_required'] = {}
if 'member' not in bond:
bond['member'] = {}
tmp = {}
for interface in interfaces_removed:
+ # if member is deleted from bond, add dependencies to call
+ # ethernet commit again in apply function
+ # to apply options under ethernet section
+ set_dependents('ethernet', conf, interface)
section = Section.section(interface) # this will be 'ethernet' for 'eth0'
if conf.exists([section, interface, 'disable']):
tmp[interface] = {'disable': ''}
else:
tmp[interface] = {}
# also present the interfaces to be removed from the bond as dictionary
bond['member']['interface_remove'] = tmp
# Restore existing config level
conf.set_level(old_level)
if dict_search('member.interface', bond):
for interface, interface_config in bond['member']['interface'].items():
+
+ interface_ethernet_config = conf.get_config_dict(
+ ['interfaces', 'ethernet', interface],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=False,
+ with_recursive_defaults=False)
+
+ interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config)
+
# Check if member interface is a new member
if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]):
bond['shutdown_required'] = {}
+ interface_config['new_added'] = {}
# Check if member interface is disabled
conf.set_level(['interfaces'])
section = Section.section(interface) # this will be 'ethernet' for 'eth0'
if conf.exists([section, interface, 'disable']):
interface_config['disable'] = ''
conf.set_level(old_level)
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
if tmp: interface_config['is_bridge_member'] = tmp
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
for tmp in is_member(conf, interface, 'bonding'):
if bond['ifname'] == tmp:
continue
interface_config['is_bond_member'] = tmp
# Check if member interface is used as source-interface on another interface
tmp = is_source_interface(conf, interface)
if tmp: interface_config['is_source_interface'] = tmp
# bond members must not have an assigned address
tmp = has_address_configured(conf, interface)
if tmp: interface_config['has_address'] = {}
# bond members must not have a VRF attached
tmp = has_vrf_configured(conf, interface)
if tmp: interface_config['has_vrf'] = {}
-
return bond
def verify(bond):
if 'deleted' in bond:
verify_bridge_delete(bond)
return None
if 'arp_monitor' in bond:
if 'target' in bond['arp_monitor'] and len(bond['arp_monitor']['target']) > 16:
raise ConfigError('The maximum number of arp-monitor targets is 16')
if 'interval' in bond['arp_monitor'] and int(bond['arp_monitor']['interval']) > 0:
if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
'transmit-load-balance or adaptive-load-balance')
if 'primary' in bond:
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('Option primary - mode dependency failed, not'
'supported in mode {mode}!'.format(**bond))
verify_mtu_ipv6(bond)
verify_address(bond)
verify_dhcpv6(bond)
verify_vrf(bond)
verify_mirror_redirect(bond)
# use common function to verify VLAN configuration
verify_vlan_config(bond)
bond_name = bond['ifname']
if dict_search('member.interface', bond):
for interface, interface_config in bond['member']['interface'].items():
error_msg = f'Can not add interface "{interface}" to bond, '
if interface == 'lo':
raise ConfigError('Loopback interface "lo" can not be added to a bond')
if interface not in interfaces():
raise ConfigError(error_msg + 'it does not exist!')
if 'is_bridge_member' in interface_config:
tmp = next(iter(interface_config['is_bridge_member']))
raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
if 'is_bond_member' in interface_config:
tmp = next(iter(interface_config['is_bond_member']))
raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
if 'is_source_interface' in interface_config:
tmp = interface_config['is_source_interface']
raise ConfigError(error_msg + f'it is the source-interface of "{tmp}"!')
if 'has_address' in interface_config:
raise ConfigError(error_msg + 'it has an address assigned!')
if 'has_vrf' in interface_config:
raise ConfigError(error_msg + 'it has a VRF assigned!')
+ if 'new_added' in interface_config and 'config_paths' in interface_config:
+ for option_path, option_value in interface_config['config_paths'].items():
+ if option_path in EthernetIf.get_bond_member_allowed_options() :
+ continue
+ if option_path in BondIf.get_inherit_bond_options():
+ continue
+ raise ConfigError(error_msg + f'it has a "{option_path.replace(".", " ")}" assigned!')
+
if 'primary' in bond:
if bond['primary'] not in bond['member']['interface']:
raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface')
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('primary interface only works for mode active-backup, ' \
'transmit-load-balance or adaptive-load-balance')
return None
def generate(bond):
return None
def apply(bond):
b = BondIf(bond['ifname'])
-
if 'deleted' in bond:
# delete interface
b.remove()
else:
b.update(bond)
-
+ if dict_search('member.interface_remove', bond):
+ try:
+ call_dependents()
+ except ConfigError:
+ raise ConfigError('Error in updating ethernet interface '
+ 'after deleting it from bond')
return None
if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
apply(c)
except ConfigError as e:
print(e)
exit(1)
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index f3e65ad5e..7374a29f7 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -1,225 +1,400 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-2021 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 pprint
from glob import glob
from sys import exit
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
from vyos.configverify import verify_address
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_eapol
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_mtu
from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_vrf
from vyos.configverify import verify_bond_bridge_member
from vyos.ethtool import Ethtool
from vyos.ifconfig import EthernetIf
+from vyos.ifconfig import BondIf
from vyos.pki import find_chain
from vyos.pki import encode_certificate
from vyos.pki import load_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
from vyos.utils.process import call
from vyos.utils.dict import dict_search
+from vyos.utils.dict import dict_to_paths_values
+from vyos.utils.dict import dict_set
+from vyos.utils.dict import dict_delete
from vyos.utils.file import write_file
from vyos import ConfigError
from vyos import airbag
airbag.enable()
# XXX: wpa_supplicant works on the source interface
cfg_dir = '/run/wpa_supplicant'
wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
+def update_bond_options(conf: Config, eth_conf: dict) -> list:
+ """
+ Return list of blocked options if interface is a bond member
+ :param conf: Config object
+ :type conf: Config
+ :param eth_conf: Ethernet config dictionary
+ :type eth_conf: dict
+ :return: List of blocked options
+ :rtype: list
+ """
+ blocked_list = []
+ bond_name = list(eth_conf['is_bond_member'].keys())[0]
+ config_without_defaults = conf.get_config_dict(
+ ['interfaces', 'ethernet', eth_conf['ifname']],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=False,
+ with_recursive_defaults=False)
+ config_with_defaults = conf.get_config_dict(
+ ['interfaces', 'ethernet', eth_conf['ifname']],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True)
+ bond_config_with_defaults = conf.get_config_dict(
+ ['interfaces', 'bonding', bond_name],
+ key_mangling=('-', '_'),
+ get_first_key=True,
+ no_tag_node_value_mangle=True,
+ with_defaults=True,
+ with_recursive_defaults=True)
+ eth_dict_paths = dict_to_paths_values(config_without_defaults)
+ eth_path_base = ['interfaces', 'ethernet', eth_conf['ifname']]
+
+ #if option is configured under ethernet section
+ for option_path, option_value in eth_dict_paths.items():
+ bond_option_value = dict_search(option_path, bond_config_with_defaults)
+
+ #If option is allowed for changing then continue
+ if option_path in EthernetIf.get_bond_member_allowed_options():
+ continue
+ # if option is inherited from bond then set valued from bond interface
+ if option_path in BondIf.get_inherit_bond_options():
+ # If option equals to bond option then do nothing
+ if option_value == bond_option_value:
+ continue
+ else:
+ # if ethernet has option and bond interface has
+ # then copy it from bond
+ if bond_option_value is not None:
+ if is_node_changed(conf, eth_path_base + option_path.split('.')):
+ Warning(
+ f'Cannot apply "{option_path.replace(".", " ")}" to "{option_value}".' \
+ f' Interface "{eth_conf["ifname"]}" is a bond member.' \
+ f' Option is inherited from bond "{bond_name}"')
+ dict_set(option_path, bond_option_value, eth_conf)
+ continue
+ # if ethernet has option and bond interface does not have
+ # then delete it form dict and do not apply it
+ else:
+ if is_node_changed(conf, eth_path_base + option_path.split('.')):
+ Warning(
+ f'Cannot apply "{option_path.replace(".", " ")}".' \
+ f' Interface "{eth_conf["ifname"]}" is a bond member.' \
+ f' Option is inherited from bond "{bond_name}"')
+ dict_delete(option_path, eth_conf)
+ blocked_list.append(option_path)
+
+ # if inherited option is not configured under ethernet section but configured under bond section
+ for option_path in BondIf.get_inherit_bond_options():
+ bond_option_value = dict_search(option_path, bond_config_with_defaults)
+ if bond_option_value is not None:
+ if option_path not in eth_dict_paths:
+ if is_node_changed(conf, eth_path_base + option_path.split('.')):
+ Warning(
+ f'Cannot apply "{option_path.replace(".", " ")}" to "{dict_search(option_path, config_with_defaults)}".' \
+ f' Interface "{eth_conf["ifname"]}" is a bond member. ' \
+ f'Option is inherited from bond "{bond_name}"')
+ dict_set(option_path, bond_option_value, eth_conf)
+ eth_conf['bond_blocked_changes'] = blocked_list
+ return None
+
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
interface name will be added or a deleted flag
"""
if config:
conf = config
else:
conf = Config()
# This must be called prior to get_interface_dict(), as this function will
# alter the config level (config.set_level())
pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
get_first_key=True, no_tag_node_value_mangle=True)
base = ['interfaces', 'ethernet']
ifname, ethernet = get_interface_dict(conf, base)
+ if 'is_bond_member' in ethernet:
+ update_bond_options(conf, ethernet)
if 'deleted' not in ethernet:
if pki: ethernet['pki'] = pki
tmp = is_node_changed(conf, base + [ifname, 'speed'])
if tmp: ethernet.update({'speed_duplex_changed': {}})
tmp = is_node_changed(conf, base + [ifname, 'duplex'])
if tmp: ethernet.update({'speed_duplex_changed': {}})
return ethernet
-def verify(ethernet):
- if 'deleted' in ethernet:
- return None
- ifname = ethernet['ifname']
- verify_interface_exists(ifname)
- verify_mtu(ethernet)
- verify_mtu_ipv6(ethernet)
- verify_dhcpv6(ethernet)
- verify_address(ethernet)
- verify_vrf(ethernet)
- verify_bond_bridge_member(ethernet)
- verify_eapol(ethernet)
- verify_mirror_redirect(ethernet)
- ethtool = Ethtool(ifname)
- # No need to check speed and duplex keys as both have default values.
+def verify_speed_duplex(ethernet: dict, ethtool: Ethtool):
+ """
+ Verify speed and duplex
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ :param ethtool: Ethernet object
+ :type ethtool: Ethtool
+ """
if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
- (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
- raise ConfigError('Speed/Duplex missmatch. Must be both auto or manually configured')
+ (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
+ raise ConfigError(
+ 'Speed/Duplex missmatch. Must be both auto or manually configured')
if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
# We need to verify if the requested speed and duplex setting is
# supported by the underlaying NIC.
speed = ethernet['speed']
duplex = ethernet['duplex']
if not ethtool.check_speed_duplex(speed, duplex):
- raise ConfigError(f'Adapter does not support changing speed and duplex '\
- f'settings to: {speed}/{duplex}!')
+ raise ConfigError(
+ f'Adapter does not support changing speed ' \
+ f'and duplex settings to: {speed}/{duplex}!')
+
+def verify_flow_control(ethernet: dict, ethtool: Ethtool):
+ """
+ Verify flow control
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ :param ethtool: Ethernet object
+ :type ethtool: Ethtool
+ """
if 'disable_flow_control' in ethernet:
if not ethtool.check_flow_control():
- raise ConfigError('Adapter does not support changing flow-control settings!')
+ raise ConfigError(
+ 'Adapter does not support changing flow-control settings!')
+
+def verify_ring_buffer(ethernet: dict, ethtool: Ethtool):
+ """
+ Verify ring buffer
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ :param ethtool: Ethernet object
+ :type ethtool: Ethtool
+ """
if 'ring_buffer' in ethernet:
max_rx = ethtool.get_ring_buffer_max('rx')
if not max_rx:
- raise ConfigError('Driver does not support RX ring-buffer configuration!')
+ raise ConfigError(
+ 'Driver does not support RX ring-buffer configuration!')
max_tx = ethtool.get_ring_buffer_max('tx')
if not max_tx:
- raise ConfigError('Driver does not support TX ring-buffer configuration!')
+ raise ConfigError(
+ 'Driver does not support TX ring-buffer configuration!')
rx = dict_search('ring_buffer.rx', ethernet)
if rx and int(rx) > int(max_rx):
- raise ConfigError(f'Driver only supports a maximum RX ring-buffer '\
+ raise ConfigError(f'Driver only supports a maximum RX ring-buffer ' \
f'size of "{max_rx}" bytes!')
tx = dict_search('ring_buffer.tx', ethernet)
if tx and int(tx) > int(max_tx):
- raise ConfigError(f'Driver only supports a maximum TX ring-buffer '\
+ raise ConfigError(f'Driver only supports a maximum TX ring-buffer ' \
f'size of "{max_tx}" bytes!')
- # verify offloading capabilities
+
+def verify_offload(ethernet: dict, ethtool: Ethtool):
+ """
+ Verify offloading capabilities
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ :param ethtool: Ethernet object
+ :type ethtool: Ethtool
+ """
if dict_search('offload.rps', ethernet) != None:
- if not os.path.exists(f'/sys/class/net/{ifname}/queues/rx-0/rps_cpus'):
+ if not os.path.exists(f'/sys/class/net/{ethernet["ifname"]}/queues/rx-0/rps_cpus'):
raise ConfigError('Interface does not suport RPS!')
-
driver = ethtool.get_driver_name()
# T3342 - Xen driver requires special treatment
if driver == 'vif':
if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
'for MTU size larger then 1500 bytes')
- if {'is_bond_member', 'mac'} <= set(ethernet):
- Warning(f'changing mac address "{mac}" will be ignored as "{ifname}" ' \
- f'is a member of bond "{is_bond_member}"'.format(**ethernet))
+def verify_allowedbond_changes(ethernet: dict):
+ """
+ Verify changed options if interface is in bonding
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ """
+ if 'bond_blocked_changes' in ethernet:
+ for option in ethernet['bond_blocked_changes']:
+ raise ConfigError(f'Cannot configure "{option.replace(".", " ")}"' \
+ f' on interface "{ethernet["ifname"]}".' \
+ f' Interface is a bond member')
+
+
+def verify(ethernet):
+ if 'deleted' in ethernet:
+ return None
+ if 'is_bond_member' in ethernet:
+ verify_bond_member(ethernet)
+ else:
+ verify_ethernet(ethernet)
+
+
+def verify_bond_member(ethernet):
+ """
+ Verification function for ethernet interface which is in bonding
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ """
+ ifname = ethernet['ifname']
+ verify_interface_exists(ifname)
+ verify_eapol(ethernet)
+ verify_mirror_redirect(ethernet)
+ ethtool = Ethtool(ifname)
+ verify_speed_duplex(ethernet, ethtool)
+ verify_flow_control(ethernet, ethtool)
+ verify_ring_buffer(ethernet, ethtool)
+ verify_offload(ethernet, ethtool)
+ verify_allowedbond_changes(ethernet)
+
+def verify_ethernet(ethernet):
+ """
+ Verification function for simple ethernet interface
+ :param ethernet: dictionary which is received from get_interface_dict
+ :type ethernet: dict
+ """
+ ifname = ethernet['ifname']
+ verify_interface_exists(ifname)
+ verify_mtu(ethernet)
+ verify_mtu_ipv6(ethernet)
+ verify_dhcpv6(ethernet)
+ verify_address(ethernet)
+ verify_vrf(ethernet)
+ verify_bond_bridge_member(ethernet)
+ verify_eapol(ethernet)
+ verify_mirror_redirect(ethernet)
+ ethtool = Ethtool(ifname)
+ # No need to check speed and duplex keys as both have default values.
+ verify_speed_duplex(ethernet, ethtool)
+ verify_flow_control(ethernet, ethtool)
+ verify_ring_buffer(ethernet, ethtool)
+ verify_offload(ethernet, ethtool)
# use common function to verify VLAN configuration
verify_vlan_config(ethernet)
return None
+
def generate(ethernet):
# render real configuration file once
wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet)
if 'deleted' in ethernet:
# delete configuration on interface removal
if os.path.isfile(wpa_supplicant_conf):
os.unlink(wpa_supplicant_conf)
return None
if 'eapol' in ethernet:
ifname = ethernet['ifname']
render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet)
cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem')
cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key')
cert_name = ethernet['eapol']['certificate']
pki_cert = ethernet['pki']['certificate'][cert_name]
loaded_pki_cert = load_certificate(pki_cert['certificate'])
loaded_ca_certs = {load_certificate(c['certificate'])
for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {}
cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
write_file(cert_file_path,
'\n'.join(encode_certificate(c) for c in cert_full_chain))
write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
if 'ca_certificate' in ethernet['eapol']:
ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
ca_chains = []
for ca_cert_name in ethernet['eapol']['ca_certificate']:
pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
- ca_chains.append('\n'.join(encode_certificate(c) for c in ca_full_chain))
+ ca_chains.append(
+ '\n'.join(encode_certificate(c) for c in ca_full_chain))
write_file(ca_cert_file_path, '\n'.join(ca_chains))
return None
def apply(ethernet):
ifname = ethernet['ifname']
# take care about EAPoL supplicant daemon
eapol_action='stop'
e = EthernetIf(ifname)
if 'deleted' in ethernet:
# delete interface
e.remove()
else:
e.update(ethernet)
if 'eapol' in ethernet:
eapol_action='reload-or-restart'
call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}')
if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
+
apply(c)
except ConfigError as e:
print(e)
exit(1)
diff --git a/src/migration-scripts/interfaces/30-to-31 b/src/migration-scripts/interfaces/30-to-31
new file mode 100755
index 000000000..894106ef4
--- /dev/null
+++ b/src/migration-scripts/interfaces/30-to-31
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021-2023 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/>.
+#
+# Deletes Wireguard peers if they have the same public key as the router has.
+
+import json
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+from vyos.ifconfig import EthernetIf
+from vyos.ifconfig import BondIf
+from vyos.utils.dict import dict_to_paths_values
+
+if len(argv) < 2:
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+with open(file_name, 'r') as f:
+ config_file = f.read()
+ base = ['interfaces', 'bonding']
+
+config = ConfigTree(config_file)
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+for bond in config.list_nodes(base):
+ member_base = base + [bond, 'member', 'interface']
+ if config.exists(member_base):
+ for interface in config.return_values(member_base):
+ if_base = ['interfaces', 'ethernet', interface]
+ if config.exists(if_base):
+ config_ethernet = json.loads(config.get_subtree(if_base).to_json())
+ eth_dict_paths = dict_to_paths_values(config_ethernet)
+ for option_path, option_value in eth_dict_paths.items():
+ # If option is allowed for changing then continue
+ converted_path = option_path.replace('-','_')
+ if converted_path in EthernetIf.get_bond_member_allowed_options():
+ continue
+ # if option is inherited from bond then continue
+ if converted_path in BondIf.get_inherit_bond_options():
+ continue
+ option_path_list = option_path.split('.')
+ config.delete(if_base + option_path_list)
+ del option_path_list[-1]
+ # delete empty node from config
+ while len(option_path_list) > 0:
+ if config.list_nodes(if_base + option_path_list):
+ break
+ config.delete(if_base + option_path_list)
+ del option_path_list[-1]
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print(f'Failed to save the modified config: {e}')
+ exit(1)
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 6, 6:24 AM (2 h, 12 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4645725
Default Alt Text
(80 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment