Skip to content

Commit 99bbe55

Browse files
Igor MineevIgor Mineev
authored andcommitted
Add bond interfaces supporting
1 parent 5c48bf9 commit 99bbe55

7 files changed

Lines changed: 289 additions & 2 deletions

File tree

debinterface/adapter.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,100 @@ def appendPostDown(self, cmd):
483483
cmd = cmd.split()
484484
self._ensure_list(self._ifAttributes, "post-down", cmd)
485485

486+
def setBondMaster(self, master_adapter_name):
487+
"""Set bond-master reference on slave.
488+
489+
Args:
490+
master_adapter_name (str): name of master iface
491+
"""
492+
self._ifAttributes['bond-master'] = master_adapter_name
493+
494+
def setBondMode(self, mode):
495+
"""Set bond-mode.
496+
497+
Args:
498+
mode (str, int): mode of bonding
499+
"""
500+
if isinstance(mode, str):
501+
mode = mode.lower().replace('_', '-')
502+
self._ifAttributes['bond-mode'] = mode
503+
504+
def setBondMiimon(self, miimon):
505+
"""Set bond-miimon parameter.
506+
Specifies the MII link monitoring frequency in milliseconds.
507+
508+
Args:
509+
miimon (int): miimon
510+
"""
511+
self._ifAttributes['bond-miimon'] = miimon
512+
513+
def setBondUpDelay(self, delay):
514+
"""Set bond-updelay parameter.
515+
Specifies the time, in milliseconds, to wait before enabling a slave after a link recovery has been detected.
516+
This option is only valid for the miimon link monitor. The updelay value should be a multiple of the miimon value.
517+
518+
Args:
519+
delay (int): delay in milliseconds
520+
"""
521+
miimon = self._ifAttributes.get('bond-miimon', 0)
522+
if miimon and delay % miimon != 0:
523+
raise ValueError("Updelay should be multiple of miimon value")
524+
self._ifAttributes['bond-updelay'] = delay
525+
526+
def setBondDownDelay(self, delay):
527+
"""Set bond-downdelay parameter.
528+
Specifies the time, in milliseconds, to wait before disabling a slave after a link failure has been detected.
529+
This option is only valid for the miimon link monitor. The updelay value should be a multiple of the miimon value.
530+
531+
Args:
532+
delay (int): delay in milliseconds
533+
"""
534+
miimon = self._ifAttributes.get('bond-miimon', 0)
535+
if miimon and delay % miimon != 0:
536+
raise ValueError("Downdelay should be multiple of miimon value")
537+
self._ifAttributes['bond-downdelay'] = delay
538+
539+
def setBondPrimary(self, primary_name):
540+
"""Set bond-primary parameter.
541+
A string specifying which slave is the primary device.
542+
The specified device will always be the active slave while it is available.
543+
Only when the primary is off-line will alternate devices be used.
544+
This is useful when one slave is preferred over another, e.g., when one slave has higher throughput than another.
545+
The primary option is only valid for active-backup (1) mode.
546+
547+
Args:
548+
primary_name (str): name of primary slave
549+
"""
550+
self._ifAttributes['bond-primary'] = primary_name
551+
552+
def setBondSlaves(self, slaves):
553+
"""Set bond-primary parameter.
554+
Slave interfaces names
555+
556+
Args:
557+
slaves (list, optional): names of slaves
558+
"""
559+
if not isinstance(slaves, list):
560+
slaves = [slaves]
561+
self._ifAttributes['bond-slaves'] = slaves
562+
486563
def setUnknown(self, key, val):
487564
# type: (str, tp.Any)->None
488565
"""Stores uncommon options as there are with no special handling
489566
It's impossible to know about all available options
567+
Format key with lower case. Replaces '_' with '-'
568+
569+
Args:
570+
key (str): the option name
571+
val (any): the option value
572+
"""
573+
key = key.lower().replace('_', '-')
574+
self.setUnknownUnformatted(key, val)
575+
576+
def setUnknownUnformatted(self, key, val):
577+
"""Stores uncommon options as there are with no special handling
578+
It's impossible to know about all available options
579+
Stores key as is
490580
WARNING: duplicated directives are overwriten. TODO better
491581
492582
Args:
@@ -607,6 +697,13 @@ def set_options(self, options):
607697
'dns-search': self.setDnsSearch,
608698
'nameservers': self.setNameservers,
609699
'wpa-conf': self.setWpaConf,
700+
'bond-master': self.setBondMaster,
701+
'bond-slaves': self.setBondSlaves,
702+
'bond-miimon': self.setBondMiimon,
703+
'bond-mode': self.setBondMode,
704+
'bond-updelay': self.setBondUpDelay,
705+
'bond-downdelay': self.setBondDownDelay,
706+
'bond-primary': self.setBondPrimary
610707
} # type: tp.Dict[str, tp.Callable[[tp.Any], None]]
611708
for key, value in options.items():
612709
if key in roseta:

debinterface/adapterValidation.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@
5454
'post-up': {'type': list},
5555
'down': {'type': list},
5656
'pre-down': {'type': list},
57-
'post-down': {'type': list}
57+
'post-down': {'type': list},
58+
'bond-mode': {'in': ['balance-rr', 'active-backup', 'balance-xor', 'broadcast', '802.3ad', 'balance-tlb', 'balance-alb', 0, 1, 2, 3, 4, 5, 6]},
59+
'bond-miimon': {'type': int},
60+
'bond-updelay': {'type': int},
61+
'bond-downdelay': {'type': int}
5862
} # type: tp.Dict[str, tp.Dict[str, tp.Any]]
5963
REQUIRED_FAMILY_OPTS = {
6064
"inet": {

debinterface/interfaces.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,82 @@ def _set_paths(self, interfaces_path, backup_path):
198198
else:
199199
# self._interfaces_path is never None
200200
self._backup_path = self._interfaces_path + ".bak"
201+
202+
def addBond(self, name="bond0", mode=0, slaves=None):
203+
"""Add new bond interface
204+
205+
Args:
206+
name (str): name of new interfaces
207+
mode (str/int): mode of bonding
208+
slaves (list, optional): list of names of bond slaves
209+
210+
Returns:
211+
NetworkAdapter: the new adapter
212+
"""
213+
if not slaves:
214+
slaves = []
215+
216+
if self.getAdapter(name) is not None:
217+
raise ValueError("Interface {} already exists".format(name))
218+
219+
for slave in slaves:
220+
adapter = self.getAdapter(slave)
221+
if adapter is None:
222+
raise ValueError("Interface {} does not exists".format(slave))
223+
if 'bond-master' in adapter.attributes and adapter.attributes['bond-master'] != name:
224+
raise ValueError("Interface {} already has master iface {}".format(slave, adapter.attributes['bond-master']))
225+
adapter.attributes['bond-master'] = name
226+
227+
return self.addAdapter({"name": name, 'bond-mode': mode, 'bond-slaves': ' '.join(slaves)})
228+
229+
def validateBondSettings(self):
230+
"""Validate bond network settings with debian bonding standard (https://wiki.debian.org/Bonding)
231+
232+
Raises:
233+
ValueError: human-readable description of error
234+
"""
235+
for adapter in self._adapters:
236+
attrs = adapter.attributes
237+
238+
if 'bond-mode' in attrs:
239+
mode = attrs['bond-mode']
240+
if isinstance(mode, str):
241+
mode = mode.lower().replace('_', '-')
242+
243+
if not 'bond-slaves' in attrs:
244+
raise ValueError("Interface {} have no slaves".format(attrs['name']))
245+
246+
if mode == 1 or mode == 'active-backup':
247+
if not 'bond-primary' in attrs:
248+
raise ValueError("Interface {} have no primary".format(attrs['name']))
249+
primary = self.getAdapter(attrs['bond-primary'])
250+
if not primary:
251+
raise ValueError(
252+
"Interface {0} have {1} as primary, but {1} was not found".format(attrs['name'],
253+
attrs['bond-primary']))
254+
if primary.attributes['name'] not in attrs['bond-slaves']:
255+
raise ValueError(
256+
"Primary interface {} is not bond slave of {}".format(attrs['bond-primary'], attrs['name']))
257+
258+
if 'bond-slaves' in attrs:
259+
slaves = attrs['bond-slaves']
260+
if slaves != ['none']:
261+
for slave in slaves:
262+
slave_adapter = self.getAdapter(slave)
263+
if not slave_adapter or slave_adapter.attributes.get('bond-master', None) != attrs['name']:
264+
raise ValueError("Interface {} have no {} as master".format(slave, attrs['name']))
265+
266+
if 'bond-master' in attrs:
267+
master = self.getAdapter(attrs['bond-master'])
268+
if not master:
269+
raise ValueError("Interface {} have no {} as master".format(attrs['name'], attrs['bond-master']))
270+
master_mode = master.attributes['bond-mode']
271+
if master_mode == 1 or master_mode == 'active-backup':
272+
if not 'bond-primary' in attrs:
273+
raise ValueError(
274+
"Interface {} have no primary when bond master mode is active-backup".format(attrs['name']))
275+
primary = self.getAdapter(attrs['bond-primary'])
276+
if not primary:
277+
raise ValueError(
278+
"Interface {0} have {1} as primary, but {1} was not found".format(attrs['name'],
279+
attrs['bond-primary']))

debinterface/interfacesReader.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ def _parse_details(self, line):
118118
if line[0].isspace() is True:
119119
keyword, value = line.strip().split(None, 1)
120120

121+
keyword = keyword.lower().replace('_', '-')
122+
121123
if keyword == 'address':
122124
self._adapters[self._context].setAddress(value)
123125
elif keyword == 'netmask':
@@ -132,6 +134,25 @@ def _parse_details(self, line):
132134
self._adapters[self._context].setHostapd(value)
133135
elif keyword == 'wpa-conf':
134136
self._adapters[self._context].setWpaConf(value)
137+
elif keyword == 'bond-mode':
138+
mode = value
139+
try:
140+
mode = int(mode)
141+
except ValueError:
142+
pass
143+
self._adapters[self._context].setBondMode(mode)
144+
elif keyword == 'bond-miimon':
145+
self._adapters[self._context].setBondMiimon(int(value))
146+
elif keyword == 'bond-updelay':
147+
self._adapters[self._context].setBondUpDelay(int(value))
148+
elif keyword == 'bond-downdelay':
149+
self._adapters[self._context].setBondDownDelay(int(value))
150+
elif keyword == 'bond-primary':
151+
self._adapters[self._context].setBondPrimary(value)
152+
elif keyword == 'bond-master':
153+
self._adapters[self._context].setBondMaster(value)
154+
elif keyword == 'bond-slaves':
155+
self._adapters[self._context].setBondSlaves(value.split())
135156
elif keyword == 'dns-nameservers':
136157
self._adapters[self._context].setDnsNameservers(value)
137158
elif keyword == 'dns-search':

debinterface/interfacesWriter.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class InterfacesWriter(object):
3131
]
3232
_prepFields = ['pre-up', 'pre-down', 'up', 'down', 'post-up', 'post-down']
3333
_bridgeFields = ['ports', 'fd', 'hello', 'maxage', 'stp', 'maxwait']
34+
_bondFields = ['bond-master', 'bond-slaves', 'bond-miimon', 'bond-updelay', 'bond-downdelay',
35+
'bond-primary', 'bond-mode']
3436
_plugins = ['hostapd', 'wpa-conf']
3537

3638
def __init__(self, adapters, interfaces_path, backup_path=None,
@@ -163,6 +165,7 @@ def _write_adapter(self, interfaces, adapter):
163165
self._write_bridge(interfaces, adapter, ifAttributes)
164166
self._write_plugins(interfaces, adapter, ifAttributes)
165167
self._write_callbacks(interfaces, adapter, ifAttributes)
168+
self._write_bond(interfaces, adapter, ifAttributes)
166169
self._write_unknown(interfaces, adapter, ifAttributes)
167170
interfaces.write("\n")
168171

@@ -176,6 +179,21 @@ def _write_auto(self, interfaces, adapter, ifAttributes):
176179
except KeyError:
177180
pass
178181

182+
def _write_bond(self, interfaces, adapter, ifAttributes):
183+
for field in self._bondFields:
184+
try:
185+
value = ifAttributes[field]
186+
if value is not None:
187+
if isinstance(value, list):
188+
d = dict(varient=field,
189+
value=" ".join(ifAttributes[field]))
190+
else:
191+
d = dict(varient=field, value=ifAttributes[field])
192+
interfaces.write(self._cmd.substitute(d))
193+
# Keep going if a field isn't provided.
194+
except KeyError:
195+
pass
196+
179197
def _write_hotplug(self, interfaces, adapter, ifAttributes):
180198
# type: (tp.IO[str], NetworkAdapter, tp.Dict[str, tp.Any])->None
181199
""" Write if applicable """

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

1113
class TestInterfaces(unittest.TestCase):
@@ -98,3 +100,36 @@ def test_remove_adapter_name(self):
98100
self.assertEqual(len(itfs.adapters), nb_adapters - 1)
99101
for adapter in itfs.adapters:
100102
self.assertNotEqual("eth0", adapter.attributes["name"])
103+
104+
def test_bond_remove_bond(self):
105+
itfs = Interfaces(interfaces_path=INFBOND1_PATH)
106+
itfs.validateBondSettings()
107+
itfs.removeAdapterByName("bond0")
108+
with self.assertRaises(ValueError):
109+
itfs.validateBondSettings()
110+
111+
def test_bond_configure_bond(self):
112+
itfs = Interfaces(interfaces_path=INFBOND1_PATH)
113+
# Slave has another interface as master
114+
with self.assertRaises(ValueError):
115+
itfs.addBond(name="bond1", slaves=["enp1s0"])
116+
# Slave does not exist
117+
with self.assertRaises(ValueError):
118+
itfs.addBond(name="bond1", slaves=["enp3s0"])
119+
120+
itfs.addAdapter({"name": "enp3s0"})
121+
itfs.addBond(name="bond1", slaves=["enp3s0"], mode=0)
122+
itfs.validateBondSettings()
123+
124+
def test_bond_miimon(self):
125+
itfs = Interfaces(interfaces_path=INFBOND1_PATH)
126+
bond0 = itfs.getAdapter("bond0")
127+
self.assertIsNotNone(bond0)
128+
bond0.setBondMode("bAlAnCe_XOR")
129+
itfs.validateBondSettings()
130+
bond0.setBondMiimon(100)
131+
bond0.setBondUpDelay(200)
132+
# Delay should be multiple of the miimon value
133+
with self.assertRaises(ValueError):
134+
bond0.setBondDownDelay(205)
135+

0 commit comments

Comments
 (0)