Skip to content

Commit 03bc049

Browse files
author
Igor Mineev
committed
Add bond interfaces supporting
1 parent abd30a5 commit 03bc049

File tree

7 files changed

+291
-4
lines changed

7 files changed

+291
-4
lines changed

debinterface/adapter.py

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,9 +402,99 @@ def appendPostDown(self, cmd):
402402
"""
403403
self._ensure_list(self._ifAttributes, "post-down", cmd)
404404

405+
def setBondMaster(self, master_adapter_name):
406+
"""Set bond-master reference on slave.
407+
408+
Args:
409+
master_adapter_name (str): name of master iface
410+
"""
411+
self._ifAttributes['bond-master'] = master_adapter_name
412+
413+
def setBondMode(self, mode):
414+
"""Set bond-mode.
415+
416+
Args:
417+
mode (str, int): mode of bonding
418+
"""
419+
if isinstance(mode, str):
420+
mode = mode.lower().replace('_', '-')
421+
self._ifAttributes['bond-mode'] = mode
422+
423+
def setBondMiimon(self, miimon):
424+
"""Set bond-miimon parameter.
425+
Specifies the MII link monitoring frequency in milliseconds.
426+
427+
Args:
428+
miimon (int): miimon
429+
"""
430+
self._ifAttributes['bond-miimon'] = miimon
431+
432+
def setBondUpDelay(self, delay):
433+
"""Set bond-updelay parameter.
434+
Specifies the time, in milliseconds, to wait before enabling a slave after a link recovery has been detected.
435+
This option is only valid for the miimon link monitor. The updelay value should be a multiple of the miimon value.
436+
437+
Args:
438+
delay (int): delay in milliseconds
439+
"""
440+
miimon = self._ifAttributes.get('bond-miimon', 0)
441+
if miimon and delay % miimon != 0:
442+
raise ValueError("Updelay should be multiple of miimon value")
443+
self._ifAttributes['bond-updelay'] = delay
444+
445+
def setBondDownDelay(self, delay):
446+
"""Set bond-downdelay parameter.
447+
Specifies the time, in milliseconds, to wait before disabling a slave after a link failure has been detected.
448+
This option is only valid for the miimon link monitor. The updelay value should be a multiple of the miimon value.
449+
450+
Args:
451+
delay (int): delay in milliseconds
452+
"""
453+
miimon = self._ifAttributes.get('bond-miimon', 0)
454+
if miimon and delay % miimon != 0:
455+
raise ValueError("Downdelay should be multiple of miimon value")
456+
self._ifAttributes['bond-downdelay'] = delay
457+
458+
def setBondPrimary(self, primary_name):
459+
"""Set bond-primary parameter.
460+
A string specifying which slave is the primary device.
461+
The specified device will always be the active slave while it is available.
462+
Only when the primary is off-line will alternate devices be used.
463+
This is useful when one slave is preferred over another, e.g., when one slave has higher throughput than another.
464+
The primary option is only valid for active-backup (1) mode.
465+
466+
Args:
467+
primary_name (str): name of primary slave
468+
"""
469+
self._ifAttributes['bond-primary'] = primary_name
470+
471+
def setBondSlaves(self, slaves):
472+
"""Set bond-primary parameter.
473+
Slave interfaces names
474+
475+
Args:
476+
slaves (list, optional): names of slaves
477+
"""
478+
if not isinstance(slaves, list):
479+
slaves = [slaves]
480+
self._ifAttributes['bond-slaves'] = slaves
481+
405482
def setUnknown(self, key, val):
406483
"""Stores uncommon options as there are with no special handling
407484
It's impossible to know about all available options
485+
Format key with lower case. Replaces '_' with '-'
486+
487+
Args:
488+
key (str): the option name
489+
val (any): the option value
490+
"""
491+
key = key.lower().replace('_', '-')
492+
self.setUnknownUnformatted(key, val)
493+
494+
def setUnknownUnformatted(self, key, val):
495+
"""Stores uncommon options as there are with no special handling
496+
It's impossible to know about all available options
497+
Stores key as is
408498
409499
Args:
410500
key (str): the option name
@@ -515,7 +605,14 @@ def set_options(self, options):
515605
'hostapd': self.setHostapd,
516606
'dns-nameservers': self.setDnsNameservers,
517607
'dns-search': self.setDnsSearch,
518-
'wpa-conf': self.setWpaConf
608+
'wpa-conf': self.setWpaConf,
609+
'bond-master': self.setBondMaster,
610+
'bond-slaves': self.setBondSlaves,
611+
'bond-miimon': self.setBondMiimon,
612+
'bond-mode': self.setBondMode,
613+
'bond-updelay': self.setBondUpDelay,
614+
'bond-downdelay': self.setBondDownDelay,
615+
'bond-primary': self.setBondPrimary
519616
}
520617
for key, value in options.items():
521618
if key in roseta:

debinterface/adapterValidation.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@
5050
'post-up': {'type': list},
5151
'down': {'type': list},
5252
'pre-down': {'type': list},
53-
'post-down': {'type': list}
53+
'post-down': {'type': list},
54+
'bond-mode': {'in': ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb', 0, 1, 2, 3, 4, 5, 6]},
55+
'bond-miimon': {'type': int},
56+
'bond-updelay': {'type': int},
57+
'bond-downdelay': {'type': int}
5458
}
5559
REQUIRED_FAMILY_OPTS = {
5660
"inet": {

debinterface/interfaces.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,82 @@ def _set_paths(self, interfaces_path, backup_path):
166166
else:
167167
# self._interfaces_path is never None
168168
self._backup_path = self._interfaces_path + ".bak"
169+
170+
def addBond(self, name="bond0", mode=0, slaves=None):
171+
"""Add new bond interface
172+
173+
Args:
174+
name (str): name of new interfaces
175+
mode (str/int): mode of bonding
176+
slaves (list, optional): list of names of bond slaves
177+
178+
Returns:
179+
NetworkAdapter: the new adapter
180+
"""
181+
if not slaves:
182+
slaves = []
183+
184+
if self.getAdapter(name) is not None:
185+
raise ValueError("Interface {} already exists".format(name))
186+
187+
for slave in slaves:
188+
adapter = self.getAdapter(slave)
189+
if adapter is None:
190+
raise ValueError("Interface {} does not exists".format(slave))
191+
if 'bond-master' in adapter.attributes and adapter.attributes['bond-master'] != name:
192+
raise ValueError("Interface {} already has master iface {}".format(slave, adapter.attributes['bond-master']))
193+
adapter.attributes['bond-master'] = name
194+
195+
return self.addAdapter({"name": name, 'bond-mode': mode, 'bond-slaves': ' '.join(slaves)})
196+
197+
def validateBondSettings(self):
198+
"""Validate bond network settings with debian bonding standard (https://wiki.debian.org/Bonding)
199+
200+
Raises:
201+
ValueError: human-readable description of error
202+
"""
203+
for adapter in self._adapters:
204+
attrs = adapter.attributes
205+
206+
if 'bond-mode' in attrs:
207+
mode = attrs['bond-mode']
208+
if isinstance(mode, str):
209+
mode = mode.lower().replace('_', '-')
210+
211+
if not 'bond-slaves' in attrs:
212+
raise ValueError("Interface {} have no slaves".format(attrs['name']))
213+
214+
if mode == 1 or mode == 'active-backup':
215+
if not 'bond-primary' in attrs:
216+
raise ValueError("Interface {} have no primary".format(attrs['name']))
217+
primary = self.getAdapter(attrs['bond-primary'])
218+
if not primary:
219+
raise ValueError(
220+
"Interface {0} have {1} as primary, but {1} was not found".format(attrs['name'],
221+
attrs['bond-primary']))
222+
if primary.attributes['name'] not in attrs['bond-slaves']:
223+
raise ValueError(
224+
"Primary interface {} is not bond slave of {}".format(attrs['bond-primary'], attrs['name']))
225+
226+
if 'bond-slaves' in attrs:
227+
slaves = attrs['bond-slaves']
228+
if slaves != ['none']:
229+
for slave in slaves:
230+
slave_adapter = self.getAdapter(slave)
231+
if not slave_adapter or slave_adapter.attributes.get('bond-master', None) != attrs['name']:
232+
raise ValueError("Interface {} have no {} as master".format(slave, attrs['name']))
233+
234+
if 'bond-master' in attrs:
235+
master = self.getAdapter(attrs['bond-master'])
236+
if not master:
237+
raise ValueError("Interface {} have no {} as master".format(attrs['name'], attrs['bond-master']))
238+
master_mode = master.attributes['bond-mode']
239+
if master_mode == 1 or master_mode == 'active-backup':
240+
if not 'bond-primary' in attrs:
241+
raise ValueError(
242+
"Interface {} have no primary when bond master mode is active-backup".format(attrs['name']))
243+
primary = self.getAdapter(attrs['bond-primary'])
244+
if not primary:
245+
raise ValueError(
246+
"Interface {0} have {1} as primary, but {1} was not found".format(attrs['name'],
247+
attrs['bond-primary']))

debinterface/interfacesReader.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ def _parse_iface(self, line):
8282

8383
def _parse_details(self, line):
8484
if line[0].isspace() is True:
85-
sline = [x.strip() for x in line.split()]
85+
sline = [x.strip() for x in line.split(None, 1)]
86+
87+
sline[0] = sline[0].lower().replace('_', '-')
8688

8789
if sline[0] == 'address':
8890
self._adapters[self._context].setAddress(sline[1])
@@ -98,6 +100,25 @@ def _parse_details(self, line):
98100
self._adapters[self._context].setHostapd(sline[1])
99101
elif sline[0] == 'wpa-conf':
100102
self._adapters[self._context].setWpaConf(sline[1])
103+
elif sline[0] == 'bond-mode':
104+
mode = sline[1]
105+
try:
106+
mode = int(mode)
107+
except ValueError:
108+
pass
109+
self._adapters[self._context].setBondMode(mode)
110+
elif sline[0] == 'bond-miimon':
111+
self._adapters[self._context].setBondMiimon(int(sline[1]))
112+
elif sline[0] == 'bond-updelay':
113+
self._adapters[self._context].setBondUpDelay(int(sline[1]))
114+
elif sline[0] == 'bond-downdelay':
115+
self._adapters[self._context].setBondDownDelay(int(sline[1]))
116+
elif sline[0] == 'bond-primary':
117+
self._adapters[self._context].setBondPrimary(sline[1])
118+
elif sline[0] == 'bond-master':
119+
self._adapters[self._context].setBondMaster(sline[1])
120+
elif sline[0] == 'bond-slaves':
121+
self._adapters[self._context].setBondSlaves(sline[1].split())
101122
elif sline[0] == 'dns-nameservers':
102123
nameservers = sline
103124
del nameservers[0]

debinterface/interfacesWriter.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ class InterfacesWriter(object):
2424
]
2525
_prepFields = ['pre-up', 'pre-down', 'up', 'down', 'post-up', 'post-down']
2626
_bridgeFields = ['ports', 'fd', 'hello', 'maxage', 'stp', 'maxwait']
27+
_bondFields = ['bond-master', 'bond-slaves', 'bond-miimon', 'bond-updelay', 'bond-downdelay',
28+
'bond-primary', 'bond-mode']
2729
_plugins = ['hostapd', 'wpa-conf']
2830

2931
def __init__(self, adapters, interfaces_path, backup_path=None,
@@ -140,6 +142,7 @@ def _write_adapter(self, interfaces, adapter):
140142
self._write_bridge(interfaces, adapter, ifAttributes)
141143
self._write_plugins(interfaces, adapter, ifAttributes)
142144
self._write_callbacks(interfaces, adapter, ifAttributes)
145+
self._write_bond(interfaces, adapter, ifAttributes)
143146
self._write_unknown(interfaces, adapter, ifAttributes)
144147
interfaces.write("\n")
145148

@@ -152,6 +155,21 @@ def _write_auto(self, interfaces, adapter, ifAttributes):
152155
except KeyError:
153156
pass
154157

158+
def _write_bond(self, interfaces, adapter, ifAttributes):
159+
for field in self._bondFields:
160+
try:
161+
value = ifAttributes[field]
162+
if value is not None:
163+
if isinstance(value, list):
164+
d = dict(varient=field,
165+
value=" ".join(ifAttributes[field]))
166+
else:
167+
d = dict(varient=field, value=ifAttributes[field])
168+
interfaces.write(self._cmd.substitute(d))
169+
# Keep going if a field isn't provided.
170+
except KeyError:
171+
pass
172+
155173
def _write_hotplug(self, interfaces, adapter, ifAttributes):
156174
""" Write if applicable """
157175
try:

test/interfaces_bond1.txt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Example of bond interfaces
2+
3+
auto bond0
4+
iface bond0 inet static
5+
address 0.0.0.0
6+
netmask 0.0.0.0
7+
bond-slaves enp1s0 enp2s0
8+
bond-mode 1
9+
bond-primary enp1s0
10+
up /sbin/ifenslave bond0 enp1s0 enp2s0
11+
down /sbin/ifenslave -d bond0 enp1s0 enp2s0
12+
13+
auto enp1s0
14+
iface enp1s0 inet manual
15+
bond-master bond0
16+
bond-primary enp1s0
17+
18+
auto enp2s0
19+
iface enp2s0 inet manual
20+
bond-master bond0
21+
bond-primary enp1s0
22+
23+
auto bond0.10
24+
iface bond0.10 inet static
25+
address 192.168.10.1
26+
netmask 255.255.255.0
27+
vlan_raw_device bond0
28+
29+
auto bond0.20
30+
iface bond0.20 inet static
31+
address 192.168.20.1
32+
netmask 255.255.255.0
33+
vlan_raw_device bond0

test/test_interfaces.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# -*- coding: utf-8 -*-
22
import os
33
import unittest
4-
from ..debinterface import Interfaces
4+
from debinterface import Interfaces
55

66

77
INF_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "interfaces.txt")
88
INF2_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "interfaces2.txt")
9+
INFBOND1_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)), "interfaces_bond1.txt")
10+
911

1012
class TestInterfaces(unittest.TestCase):
1113
def test_interfaces_paths(self):
@@ -78,3 +80,36 @@ def test_remove_adapter_name(self):
7880
self.assertEqual(len(itfs.adapters), nb_adapters - 1)
7981
for adapter in itfs.adapters:
8082
self.assertNotEqual("eth0", adapter.attributes["name"])
83+
84+
def test_bond_remove_bond(self):
85+
itfs = Interfaces(interfaces_path=INFBOND1_PATH)
86+
itfs.validateBondSettings()
87+
itfs.removeAdapterByName("bond0")
88+
with self.assertRaises(ValueError):
89+
itfs.validateBondSettings()
90+
91+
def test_bond_configure_bond(self):
92+
itfs = Interfaces(interfaces_path=INFBOND1_PATH)
93+
# Slave has another interface as master
94+
with self.assertRaises(ValueError):
95+
itfs.addBond(name="bond1", slaves=["enp1s0"])
96+
# Slave does not exist
97+
with self.assertRaises(ValueError):
98+
itfs.addBond(name="bond1", slaves=["enp3s0"])
99+
100+
itfs.addAdapter({"name": "enp3s0"})
101+
itfs.addBond(name="bond1", slaves=["enp3s0"], mode=0)
102+
itfs.validateBondSettings()
103+
104+
def test_bond_miimon(self):
105+
itfs = Interfaces(interfaces_path=INFBOND1_PATH)
106+
bond0 = itfs.getAdapter("bond0")
107+
self.assertIsNotNone(bond0)
108+
bond0.setBondMode("bAlAnCe_XOR")
109+
itfs.validateBondSettings()
110+
bond0.setBondMiimon(100)
111+
bond0.setBondUpDelay(200)
112+
# Delay should be multiple of the miimon value
113+
with self.assertRaises(ValueError):
114+
bond0.setBondDownDelay(205)
115+

0 commit comments

Comments
 (0)