Merge lp:~mpontillo/maas/bug-1553617-bonds-with-no-parents into lp:~maas-committers/maas/trunk

Proposed by Mike Pontillo
Status: Merged
Approved by: Mike Pontillo
Approved revision: no longer in the source branch.
Merged at revision: 4741
Proposed branch: lp:~mpontillo/maas/bug-1553617-bonds-with-no-parents
Merge into: lp:~maas-committers/maas/trunk
Diff against target: 368 lines (+180/-26)
2 files modified
src/provisioningserver/utils/ipaddr.py (+73/-7)
src/provisioningserver/utils/tests/test_ipaddr.py (+107/-19)
To merge this branch: bzr merge lp:~mpontillo/maas/bug-1553617-bonds-with-no-parents
Reviewer Review Type Date Requested Status
Blake Rouse (community) Approve
Review via email: mp+288211@code.launchpad.net

Commit message

Fix bug #1553617: rack registration fails when bond interfaces are present.

 * When parsing '/sbin/ip addr' and gathering driver information, ignore
   bonds with no backing interfaces.
 * After parsing '/sbin/ip addr', cross-reference data from
   /proc/net/bonding/* and update interface MACs with the original
   hardware MAC. (After a bond interface is added, the interface MAC is
   updated to be the same as the bond MAC, which was throwing off the data
   MAAS gathered.)

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Updated this branch to fix the second issue blocking rack registration.

Revision history for this message
Blake Rouse (blake-rouse) wrote :

Looks good. Well sleuthed! Just one question on the filter on bonds that don't have any members.

review: Approve
Revision history for this message
Mike Pontillo (mpontillo) :
Revision history for this message
MAAS Lander (maas-lander) wrote :
Download full text (1.0 MiB)

The attempt to merge lp:~mpontillo/maas/bug-1553617-bonds-with-no-parents into lp:maas failed. Below is the output from the failed tests.

Hit:1 http://security.ubuntu.com/ubuntu xenial-security InRelease
Get:2 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial InRelease [95.8 kB]
Hit:3 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-updates InRelease
Hit:4 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial-backports InRelease
Get:5 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main Sources [1,109 kB]
Get:6 http://prodstack-zone-2.clouds.archive.ubuntu.com/ubuntu xenial/main amd64 Packages [1,443 kB]
Fetched 2,648 kB in 0s (2,703 kB/s)
Reading package lists...
sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    --no-install-recommends install apache2 archdetect-deb authbind bind9 bind9utils build-essential bzr-builddeb chromium-browser chromium-chromedriver curl daemontools debhelper dh-apport dh-systemd distro-info dnsutils firefox freeipmi-tools git gjs ipython isc-dhcp-common libjs-angularjs libjs-jquery libjs-jquery-hotkeys libjs-yui3-full libjs-yui3-min libpq-dev make nodejs-legacy npm postgresql pxelinux python3-all python3-apt python3-bson python3-convoy python3-coverage python3-crochet python3-cssselect python3-curtin python3-dev python3-distro-info python3-django python3-django-nose python3-django-piston3 python3-docutils python3-formencode python3-hivex python3-httplib2 python3-jinja2 python3-jsonschema python3-lxml python3-mock python3-netaddr python3-netifaces python3-oauth python3-oauthlib python3-openssl python3-paramiko python3-petname python3-pexpect python3-psycopg2 python3-pyinotify python3-pyparsing python3-pyvmomi python3-requests python3-seamicroclient python3-setuptools python3-simplestreams python3-sphinx python3-tempita python3-twisted python3-txtftp python3-tz python3-yaml python3-zope.interface python-bson python-crochet python-django python-django-piston python-djorm-ext-pgarray python-formencode python-lxml python-netaddr python-netifaces python-pocket-lint python-psycopg2 python-tempita python-twisted python-yaml socat syslinux-common tgt ubuntu-cloudimage-keyring wget xvfb
Reading package lists...
Building dependency tree...
Reading state information...
apache2 is already the newest version (2.4.18-1ubuntu1).
archdetect-deb is already the newest version (1.114ubuntu3).
authbind is already the newest version (2.1.1+nmu1).
bind9 is already the newest version (1:9.10.3.dfsg.P2-5).
bind9utils is already the newest version (1:9.10.3.dfsg.P2-5).
build-essential is already the newest version (12.1ubuntu2).
curl is already the newest version (7.47.0-1ubuntu2).
debhelper is already the newest version (9.20160115ubuntu2).
dh-apport is already the newest version (2.20-0ubuntu3).
dh-systemd is already the newest version (1.29ubuntu1).
distro-info is already the newest version (0.14build1).
dnsutils is already the newest version (1:9.10.3.dfsg.P2-5).
firefox is already the newest version (45.0+build2-0ubuntu1).
freeipmi-tools is already the newest version (1.4.11-1ubuntu1).
git is already the newest version (1:2.7.0-1).
isc-dhcp-common is already the newest version ...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/provisioningserver/utils/ipaddr.py'
2--- src/provisioningserver/utils/ipaddr.py 2016-03-03 20:40:10 +0000
3+++ src/provisioningserver/utils/ipaddr.py 2016-03-07 19:23:24 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2015 Canonical Ltd. This software is licensed under the
6+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Utility to parse 'ip addr [show]'.
10@@ -221,7 +221,7 @@
11
12 def get_interface_type(
13 ifname, sys_class_net="/sys/class/net",
14- proc_net_vlan='/proc/net/vlan'):
15+ proc_net="/proc/net"):
16 """Heuristic to return the type of the given interface.
17
18 The given interface must be able to be found in /sys/class/net/ifname.
19@@ -270,7 +270,7 @@
20 bond_dir = os.path.join(sys_path, 'bonding')
21 if os.path.isdir(bond_dir):
22 return 'ethernet.bond'
23- if os.path.isfile('%s/%s' % (proc_net_vlan, ifname)):
24+ if os.path.isfile(os.path.join(proc_net, "vlan", ifname)):
25 return 'ethernet.vlan'
26 device_path = os.path.join(sys_path, 'device')
27 if os.path.islink(device_path):
28@@ -291,22 +291,88 @@
29 return 'unknown-%d' % iftype
30
31
32-def annotate_with_driver_information(interfaces):
33+def _parse_proc_net_bonding(file):
34+ """Parse the given file, which must be a path to a file in the format
35+ that is used for file in `/proc/net/bonding/<interface>`.
36+
37+ Returns a dictionary mapping each interface name found in the file to
38+ its original MAC address.
39+ """
40+ interfaces = {}
41+ current_iface = None
42+ with open(file) as f:
43+ for line in f.readlines():
44+ line = line.strip()
45+ slave_iface = line.split("Slave Interface: ")
46+ if len(slave_iface) == 2:
47+ current_iface = slave_iface[1]
48+ hw_addr = line.split("Permanent HW addr: ")
49+ if len(hw_addr) == 2:
50+ interfaces[current_iface] = hw_addr[1]
51+ return interfaces
52+
53+
54+def annotate_with_proc_net_bonding_original_macs(
55+ interfaces, proc_net="/proc/net"):
56+ """Repairs the MAC addresses of bond members in the specified structure.
57+
58+ Given the specified interfaces structure, uses the data in
59+ `/proc/net/bonding/*` to determine if any of the interfaces
60+ in the structure are bond members. If so, modifies their MAC address,
61+ setting it back to the original hardware MAC. (When an interface is added
62+ to a bond, its MAC address is set to the bond MAC, and subsequently
63+ reported in commands like "ip addr".)
64+ """
65+ proc_net_bonding = os.path.join(proc_net, "bonding")
66+ if os.path.isdir(proc_net_bonding):
67+ bonds = os.listdir(proc_net_bonding)
68+ for bond in bonds:
69+ parent_macs = _parse_proc_net_bonding(
70+ os.path.join(proc_net_bonding, bond))
71+ for interface in parent_macs:
72+ if interface in interfaces:
73+ interfaces[interface]['mac'] = parent_macs[interface]
74+ return interfaces
75+
76+
77+def annotate_with_driver_information(
78+ interfaces, sys_class_net="/sys/class/net", proc_net="/proc/net"):
79 """Determines driver information for each of the given interfaces.
80
81 Annotates the given dictionary to update it with driver information
82 (if found) for each interface.
83+
84+ Deletes bond interfaces if they are not configured.
85+
86+ :param interfaces: interfaces dictionary from `parse_ip_addr()`.
87+ :param proc_net: path to /proc/net
88+ :param sys_class_net: path to /sys/class/net
89 """
90+ interfaces = annotate_with_proc_net_bonding_original_macs(
91+ interfaces, proc_net=proc_net)
92+ bogus_interfaces = []
93 for name in interfaces:
94 iface = interfaces[name]
95- iftype = get_interface_type(name)
96+ iftype = get_interface_type(
97+ name, sys_class_net=sys_class_net, proc_net=proc_net)
98 interfaces[name]['type'] = iftype
99 if iftype == 'ethernet.bond':
100- iface['bonded_interfaces'] = get_bonded_interfaces(name)
101+ bond_parents = get_bonded_interfaces(
102+ name, sys_class_net=sys_class_net)
103+ if len(bond_parents) > 0:
104+ iface['bonded_interfaces'] = bond_parents
105+ else:
106+ # If we found a bond interface with no parents, just pretend
107+ # it doesn't exist. The MAAS model assumes bonds must have
108+ # backing interfaces.
109+ bogus_interfaces.append(name)
110 elif iftype == 'ethernet.vlan':
111 iface['vid'] = get_vid_from_ifname(name)
112 elif iftype == 'ethernet.bridge':
113- iface['bridged_interfaces'] = get_bridged_interfaces(name)
114+ iface['bridged_interfaces'] = get_bridged_interfaces(
115+ name, sys_class_net=sys_class_net)
116+ for name in bogus_interfaces:
117+ del interfaces[name]
118 return interfaces
119
120
121
122=== modified file 'src/provisioningserver/utils/tests/test_ipaddr.py'
123--- src/provisioningserver/utils/tests/test_ipaddr.py 2016-03-04 00:56:41 +0000
124+++ src/provisioningserver/utils/tests/test_ipaddr.py 2016-03-07 19:23:24 +0000
125@@ -21,6 +21,7 @@
126 _add_additional_interface_properties,
127 _parse_interface_definition,
128 annotate_with_driver_information,
129+ annotate_with_proc_net_bonding_original_macs,
130 get_bonded_interfaces,
131 get_interface_type,
132 get_ip_addr,
133@@ -33,6 +34,7 @@
134 from testtools.matchers import (
135 Contains,
136 Equals,
137+ HasLength,
138 Not,
139 )
140
141@@ -347,17 +349,19 @@
142 self.assertThat(ip_link['ens11'], Not(Contains('inet')))
143
144
145-class TestGetInterfaceType(MAASTestCase):
146+class FakeSysProcTestCase(MAASTestCase):
147
148 def setUp(self):
149- super(TestGetInterfaceType, self).setUp()
150+ super().setUp()
151 self.tmp_sys_net = mkdtemp('maas-unit-tests.sys-class-net')
152- self.tmp_proc_net_vlan = mkdtemp('maas-unit-tests.proc-net-vlan')
153+ self.tmp_proc_net = mkdtemp('maas-unit-tests.proc-net')
154+ os.mkdir(os.path.join(self.tmp_proc_net, "vlan"))
155+ os.mkdir(os.path.join(self.tmp_proc_net, "bonding"))
156
157 def tearDown(self):
158- super(TestGetInterfaceType, self).tearDown()
159+ super().tearDown()
160 rmtree(self.tmp_sys_net)
161- rmtree(self.tmp_proc_net_vlan)
162+ rmtree(self.tmp_proc_net)
163
164 def createInterfaceType(
165 self, ifname, iftype, is_bridge=False, is_vlan=False,
166@@ -371,7 +375,7 @@
167 if is_bridge:
168 os.mkdir(os.path.join(ifdir, 'bridge'))
169 if is_vlan:
170- with open(os.path.join(self.tmp_proc_net_vlan, ifname), 'w'):
171+ with open(os.path.join(self.tmp_proc_net, "vlan", ifname), 'w'):
172 pass # Just touch.
173 if is_bond:
174 os.mkdir(os.path.join(ifdir, 'bonding'))
175@@ -395,6 +399,9 @@
176 def createEthernetInterface(self, ifname, **kwargs):
177 self.createInterfaceType(ifname, 1, **kwargs)
178
179+
180+class TestGetInterfaceType(FakeSysProcTestCase):
181+
182 def test__identifies_missing_interface(self):
183 self.assertThat(get_interface_type(
184 'eth0', sys_class_net=self.tmp_sys_net),
185@@ -405,7 +412,7 @@
186 self.createEthernetInterface('br0', is_bridge=True)
187 self.assertThat(get_interface_type(
188 'br0', sys_class_net=self.tmp_sys_net,
189- proc_net_vlan=self.tmp_proc_net_vlan),
190+ proc_net=self.tmp_proc_net),
191 Equals('ethernet.bridge')
192 )
193
194@@ -413,7 +420,7 @@
195 self.createEthernetInterface('bond0', is_bond=True)
196 self.assertThat(get_interface_type(
197 'bond0', sys_class_net=self.tmp_sys_net,
198- proc_net_vlan=self.tmp_proc_net_vlan),
199+ proc_net=self.tmp_proc_net),
200 Equals('ethernet.bond')
201 )
202
203@@ -429,7 +436,7 @@
204 self.createEthernetInterface('vlan42', is_vlan=True)
205 self.assertThat(get_interface_type(
206 'vlan42', sys_class_net=self.tmp_sys_net,
207- proc_net_vlan=self.tmp_proc_net_vlan),
208+ proc_net=self.tmp_proc_net),
209 Equals('ethernet.vlan')
210 )
211
212@@ -437,7 +444,7 @@
213 self.createEthernetInterface('eth0', is_physical=True)
214 self.assertThat(get_interface_type(
215 'eth0', sys_class_net=self.tmp_sys_net,
216- proc_net_vlan=self.tmp_proc_net_vlan),
217+ proc_net=self.tmp_proc_net),
218 Equals('ethernet.physical')
219 )
220
221@@ -445,7 +452,7 @@
222 self.createEthernetInterface('wlan0', is_wireless=True)
223 self.assertThat(get_interface_type(
224 'wlan0', sys_class_net=self.tmp_sys_net,
225- proc_net_vlan=self.tmp_proc_net_vlan),
226+ proc_net=self.tmp_proc_net),
227 Equals('ethernet.wireless')
228 )
229
230@@ -453,7 +460,7 @@
231 self.createEthernetInterface('eth1')
232 self.assertThat(get_interface_type(
233 'eth1', sys_class_net=self.tmp_sys_net,
234- proc_net_vlan=self.tmp_proc_net_vlan),
235+ proc_net=self.tmp_proc_net),
236 Equals('ethernet')
237 )
238
239@@ -461,7 +468,7 @@
240 self.createLoopbackInterface('lo')
241 self.assertThat(get_interface_type(
242 'lo', sys_class_net=self.tmp_sys_net,
243- proc_net_vlan=self.tmp_proc_net_vlan),
244+ proc_net=self.tmp_proc_net),
245 Equals('loopback')
246 )
247
248@@ -469,7 +476,7 @@
249 self.createIpIpInterface('tun0')
250 self.assertThat(get_interface_type(
251 'tun0', sys_class_net=self.tmp_sys_net,
252- proc_net_vlan=self.tmp_proc_net_vlan),
253+ proc_net=self.tmp_proc_net),
254 Equals('ipip')
255 )
256
257@@ -477,18 +484,16 @@
258 self.createInterfaceType('avian0', 1149)
259 self.assertThat(get_interface_type(
260 'avian0', sys_class_net=self.tmp_sys_net,
261- proc_net_vlan=self.tmp_proc_net_vlan),
262+ proc_net=self.tmp_proc_net),
263 Equals('unknown-1149')
264 )
265
266
267-class TestAnnotateWithDriverInformation(MAASTestCase):
268+class TestAnnotateWithDriverInformation(FakeSysProcTestCase):
269
270 def test__populates_interface_type_for_each_interface(self):
271 # Note: this is more of an end-to-end test, since we call
272- # "/sbin/ip addr" on the host running the tests. This is necessary
273- # because we don't have dependency injection for the directory names
274- # all the way through.
275+ # "/sbin/ip addr" on the host running the tests.
276 ip_addr_output = check_output(['/sbin/ip', 'addr'])
277 interfaces = parse_ip_addr(ip_addr_output)
278 interfaces_with_types = annotate_with_driver_information(interfaces)
279@@ -502,6 +507,89 @@
280 elif iface['type'] == 'ethernet.bridge':
281 self.expectThat(iface, Contains('bridged_interfaces'))
282
283+ def test__ignores_bond_interfaces_with_no_parents(self):
284+ interfaces = {
285+ 'eth0': {},
286+ 'eth1': {},
287+ 'bond0': {},
288+ 'bond1': {},
289+ }
290+ self.createEthernetInterface('eth0', is_physical=True)
291+ self.createEthernetInterface('eth1', is_physical=True)
292+ self.createEthernetInterface(
293+ 'bond0', is_bond=True, bonded_interfaces=['eth0, eth1'])
294+ self.createEthernetInterface(
295+ 'bond1', is_bond=True, bonded_interfaces=[])
296+ interfaces = annotate_with_driver_information(
297+ interfaces, sys_class_net=self.tmp_sys_net,
298+ proc_net=self.tmp_proc_net)
299+ self.expectThat(interfaces, HasLength(3))
300+ self.expectThat(interfaces, Contains('eth0'))
301+ self.expectThat(interfaces, Contains('eth1'))
302+ self.expectThat(interfaces, Contains('bond0'))
303+
304+ def test__finds_bond_members_original_mac_addresses(self):
305+ testdata = dedent("""\
306+ Ethernet Channel Bonding Driver: v3.7.1 (April 27, 2011)
307+
308+ Bonding Mode: fault-tolerance (active-backup)
309+ Primary Slave: None
310+ Currently Active Slave: ens11
311+ MII Status: up
312+ MII Polling Interval (ms): 100
313+ Up Delay (ms): 200
314+ Down Delay (ms): 0
315+
316+ Slave Interface: ens11
317+ MII Status: up
318+ Speed: Unknown
319+ Duplex: Unknown
320+ Link Failure Count: 0
321+ Permanent HW addr: 52:54:00:ea:1c:fc
322+ Slave queue ID: 0
323+
324+ Slave Interface: ens3
325+ MII Status: up
326+ Speed: Unknown
327+ Duplex: Unknown
328+ Link Failure Count: 0
329+ Permanent HW addr: 52:54:00:13:0e:6f
330+ Slave queue ID: 0
331+ """)
332+ proc_net_bonding_bond0 = os.path.join(
333+ self.tmp_proc_net, "bonding", "bond0")
334+ with open(proc_net_bonding_bond0, mode='w') as f:
335+ f.write(testdata)
336+ interfaces = {
337+ "ens3": {"mac": "00:01:02:03:04:05"},
338+ "ens11": {"mac": "01:02:03:04:05:06"},
339+ }
340+ annotate_with_proc_net_bonding_original_macs(
341+ interfaces, proc_net=self.tmp_proc_net)
342+ self.assertEqual(
343+ {
344+ "ens3": {"mac": "52:54:00:13:0e:6f"},
345+ "ens11": {"mac": "52:54:00:ea:1c:fc"},
346+ },
347+ interfaces
348+ )
349+
350+ def test__ignores_missing_proc_net_bonding(self):
351+ os.rmdir(os.path.join(self.tmp_proc_net, "bonding"))
352+ interfaces = {
353+ "ens3": {"mac": "00:01:02:03:04:05"},
354+ "ens11": {"mac": "01:02:03:04:05:06"},
355+ }
356+ annotate_with_proc_net_bonding_original_macs(
357+ interfaces, proc_net=self.tmp_proc_net)
358+ self.assertEqual(
359+ {
360+ "ens3": {"mac": "00:01:02:03:04:05"},
361+ "ens11": {"mac": "01:02:03:04:05:06"},
362+ },
363+ interfaces
364+ )
365+
366
367 class TestGetIPAddr(MAASTestCase):
368 """Tests for `get_ip_addr`, `get_ip_addr_json`, `get_mac_addresses`."""