Merge lp:~mpontillo/maas/bug-1553617-bonds-with-no-parents into lp:~maas-committers/maas/trunk
- bug-1553617-bonds-with-no-parents
- Merge into trunk
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 | ||||
Related bugs: |
|
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/
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.)
Description of the change
Mike Pontillo (mpontillo) wrote : | # |
Blake Rouse (blake-rouse) wrote : | # |
Looks good. Well sleuthed! Just one question on the filter on bonds that don't have any members.
Mike Pontillo (mpontillo) : | # |
MAAS Lander (maas-lander) wrote : | # |
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://
Get:2 http://
Hit:3 http://
Hit:4 http://
Get:5 http://
Get:6 http://
Fetched 2,648 kB in 0s (2,703 kB/s)
Reading package lists...
sudo DEBIAN_
--no-
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.
bind9utils is already the newest version (1:9.10.
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.20160115ubun
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.
firefox is already the newest version (45.0+build2-
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
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`.""" |
Updated this branch to fix the second issue blocking rack registration.