Merge lp:~raharper/curtin/fix-network-package-install into lp:~curtin-dev/curtin/trunk

Proposed by Ryan Harper
Status: Merged
Merged at revision: 269
Proposed branch: lp:~raharper/curtin/fix-network-package-install
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 476 lines (+339/-23)
6 files modified
curtin/commands/curthooks.py (+32/-17)
curtin/net/__init__.py (+15/-2)
curtin/net/network_state.py (+28/-0)
examples/tests/bonding_network.yaml (+27/-0)
tests/unittests/test_net.py (+2/-4)
tests/vmtests/test_bonding.py (+235/-0)
To merge this branch: bzr merge lp:~raharper/curtin/fix-network-package-install
Reviewer Review Type Date Requested Status
Scott Moser (community) Approve
Review via email: mp+270212@code.launchpad.net

Description of the change

- Fixes lp:1491994
- Enforce order on rendering of /etc/network/interfaces (physical ifaces before bonds)
- Don't emit hwaddress for anything except bonds or bridges
- Introduce a bonding vmtest
- lint and unit-test fixes.

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

llooks good.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'curtin/commands/curthooks.py'
--- curtin/commands/curthooks.py 2015-09-04 17:01:28 +0000
+++ curtin/commands/curthooks.py 2015-09-04 19:23:11 +0000
@@ -573,24 +573,39 @@
573 update_initramfs(target, all_kernels=True)573 update_initramfs(target, all_kernels=True)
574574
575575
576def install_missing_storage_packages(cfg, target):576def install_missing_packages(cfg, target):
577 # Do nothing if no custom storage.577 ''' describe which operation types will require specific packages
578 if 'storage' not in cfg:578
579 return579 'custom_config_key': {
580580 'pkg1': ['op_name_1', 'op_name_2', ...]
581 all_types = set(581 }
582 operation['type']582 '''
583 for operation in cfg['storage']['config']583 custom_configs = {
584 )584 'storage': {
585 'lvm2': ['lvm_volgroup', 'lvm_partition'],
586 'mdadm': ['raid'],
587 'bcache-tools': ['bcache']},
588 'network': {
589 'vlan': ['vlan'],
590 'ifenslave': ['bond'],
591 'bridge-utils': ['bridge']},
592 }
593
585 needed_packages = []594 needed_packages = []
586 installed_packages = get_installed_packages(target)595 installed_packages = get_installed_packages(target)
587 if ('lvm_volgroup' in all_types or 'lvm_partition' in all_types) and \596 for cust_cfg, pkg_reqs in custom_configs.items():
588 'lvm2' not in installed_packages:597 if cust_cfg not in cfg:
589 needed_packages.append('lvm2')598 continue
590 if 'raid' in all_types and 'mdadm' not in installed_packages:599
591 needed_packages.append('mdadm')600 all_types = set(
592 if 'bcache' in all_types and 'bcache-tools' not in installed_packages:601 operation['type']
593 needed_packages.append('bcache-tools')602 for operation in cfg[cust_cfg]['config']
603 )
604 for pkg, types in pkg_reqs.items():
605 if set(types).intersection(all_types) and \
606 pkg not in installed_packages:
607 needed_packages.append(pkg)
608
594 if needed_packages:609 if needed_packages:
595 util.install_packages(needed_packages, target=target)610 util.install_packages(needed_packages, target=target)
596611
@@ -630,7 +645,7 @@
630645
631 detect_and_handle_multipath(cfg, target)646 detect_and_handle_multipath(cfg, target)
632647
633 install_missing_storage_packages(cfg, target)648 install_missing_packages(cfg, target)
634649
635 # If a crypttab file was created by block_meta than it needs to be copied650 # If a crypttab file was created by block_meta than it needs to be copied
636 # onto the target system, and update_initramfs() needs to be run, so that651 # onto the target system, and update_initramfs() needs to be run, so that
637652
=== modified file 'curtin/net/__init__.py'
--- curtin/net/__init__.py 2015-09-02 13:55:39 +0000
+++ curtin/net/__init__.py 2015-09-04 19:23:11 +0000
@@ -265,7 +265,6 @@
265 'metric',265 'metric',
266 'gateway',266 'gateway',
267 'pointopoint',267 'pointopoint',
268 'hwaddress',
269 'mtu',268 'mtu',
270 'scope',269 'scope',
271 ]270 ]
@@ -289,6 +288,9 @@
289 'index',288 'index',
290 'subnets',289 'subnets',
291 ]290 ]
291 if iface['type'] not in ['bond', 'bridge']:
292 ignore_map.append('mac_address')
293
292 for key, value in iface.items():294 for key, value in iface.items():
293 if value and key not in ignore_map:295 if value and key not in ignore_map:
294 if type(value) == list:296 if type(value) == list:
@@ -319,7 +321,18 @@
319321
320 content = ""322 content = ""
321 interfaces = network_state.get('interfaces')323 interfaces = network_state.get('interfaces')
322 for iface in interfaces.values():324 ''' Apply a sort order to ensure that we write out
325 the physical interfaces first; this is critical for
326 bonding
327 '''
328 order = {
329 'physical': 0,
330 'bond': 1,
331 'bridge': 2,
332 'vlan': 3,
333 }
334 for iface in sorted(interfaces.values(),
335 key=lambda k: (order[k['type']], k['name'])):
323 content += "auto {name}\n".format(**iface)336 content += "auto {name}\n".format(**iface)
324337
325 subnets = iface.get('subnets', {})338 subnets = iface.get('subnets', {})
326339
=== modified file 'curtin/net/network_state.py'
--- curtin/net/network_state.py 2015-09-02 13:55:39 +0000
+++ curtin/net/network_state.py 2015-09-04 19:23:11 +0000
@@ -116,6 +116,8 @@
116116
117 interfaces = self.network_state.get('interfaces')117 interfaces = self.network_state.get('interfaces')
118 iface = interfaces.get(command['name'], {})118 iface = interfaces.get(command['name'], {})
119 for param, val in command.get('params', {}).items():
120 iface.update({param: val})
119 iface.update({121 iface.update({
120 'name': command.get('name'),122 'name': command.get('name'),
121 'type': command.get('type'),123 'type': command.get('type'),
@@ -160,15 +162,20 @@
160 #/etc/network/interfaces162 #/etc/network/interfaces
161 auto eth0163 auto eth0
162 iface eth0 inet manual164 iface eth0 inet manual
165 bond-master bond0
166 bond-mode 802.3ad
163167
164 auto eth1168 auto eth1
165 iface eth1 inet manual169 iface eth1 inet manual
170 bond-master bond0
171 bond-mode 802.3ad
166172
167 auto bond0173 auto bond0
168 iface bond0 inet static174 iface bond0 inet static
169 address 192.168.0.10175 address 192.168.0.10
170 gateway 192.168.0.1176 gateway 192.168.0.1
171 netmask 255.255.255.0177 netmask 255.255.255.0
178 bond-slaves none
172 bond-mode 802.3ad179 bond-mode 802.3ad
173 bond-miimon 100180 bond-miimon 100
174 bond-downdelay 200181 bond-downdelay 200
@@ -190,6 +197,7 @@
190 iface = interfaces.get(command.get('name'), {})197 iface = interfaces.get(command.get('name'), {})
191 for param, val in command.get('params').items():198 for param, val in command.get('params').items():
192 iface.update({param: val})199 iface.update({param: val})
200 iface.update({'bond-slaves': 'none'})
193 self.network_state['interfaces'].update({iface['name']: iface})201 self.network_state['interfaces'].update({iface['name']: iface})
194202
195 # handle bond slaves203 # handle bond slaves
@@ -205,6 +213,9 @@
205 interfaces = self.network_state.get('interfaces')213 interfaces = self.network_state.get('interfaces')
206 bond_if = interfaces.get(ifname)214 bond_if = interfaces.get(ifname)
207 bond_if['bond-master'] = command.get('name')215 bond_if['bond-master'] = command.get('name')
216 # copy in bond config into slave
217 for param, val in command.get('params').items():
218 bond_if.update({param: val})
208 self.network_state['interfaces'].update({ifname: bond_if})219 self.network_state['interfaces'].update({ifname: bond_if})
209220
210 def handle_bridge(self, command):221 def handle_bridge(self, command):
@@ -354,7 +365,24 @@
354 print("NS1 == NS2 ?=> {}".format(365 print("NS1 == NS2 ?=> {}".format(
355 ns1.network_state == ns2.network_state))366 ns1.network_state == ns2.network_state))
356367
368 def test_output(network_config):
369 (version, config) = load_config(network_config)
370 ns1 = NetworkState(version=version, config=config)
371 ns1.parse_config()
372 random.shuffle(config)
373 ns2 = NetworkState(version=version, config=config)
374 ns2.parse_config()
375 print("NS1 == NS2 ?=> {}".format(
376 ns1.network_state == ns2.network_state))
377 eni_1 = net.render_interfaces(ns1.network_state)
378 eni_2 = net.render_interfaces(ns2.network_state)
379 print(eni_1)
380 print(eni_2)
381 print("eni_1 == eni_2 ?=> {}".format(
382 eni_1 == eni_2))
383
357 y = curtin_config.load_config(sys.argv[1])384 y = curtin_config.load_config(sys.argv[1])
358 network_config = y.get('network')385 network_config = y.get('network')
359 test_parse(network_config)386 test_parse(network_config)
360 test_dump_and_load(network_config)387 test_dump_and_load(network_config)
388 test_output(network_config)
361389
=== added file 'examples/tests/bonding_network.yaml'
--- examples/tests/bonding_network.yaml 1970-01-01 00:00:00 +0000
+++ examples/tests/bonding_network.yaml 2015-09-04 19:23:11 +0000
@@ -0,0 +1,27 @@
1network:
2 version: 1
3 config:
4 # Physical interfaces.
5 - type: physical
6 name: eth0
7 mac_address: "52:54:00:12:34:00"
8 subnets:
9 - type: dhcp4
10 - type: physical
11 name: eth1
12 mac_address: "52:54:00:12:34:02"
13 - type: physical
14 name: eth2
15 mac_address: "52:54:00:12:34:04"
16 # Bond.
17 - type: bond
18 name: bond0
19 mac_address: "52:54:00:12:34:06"
20 bond_interfaces:
21 - eth1
22 - eth2
23 params:
24 bond-mode: active-backup
25 subnets:
26 - type: static
27 address: 10.23.23.2/24
028
=== modified file 'tests/unittests/test_net.py'
--- tests/unittests/test_net.py 2015-09-02 13:55:39 +0000
+++ tests/unittests/test_net.py 2015-09-04 19:23:11 +0000
@@ -307,10 +307,8 @@
307307
308 def test_render_interfaces(self):308 def test_render_interfaces(self):
309 ns = self.get_net_state()309 ns = self.get_net_state()
310 ifaces = ('auto eth1\n' + 'iface eth1 inet manual\n' +310 ifaces = ('auto eth0\n' + 'iface eth0 inet dhcp\n\n' +
311 ' hwaddress cf:d6:af:48:e8:80\n\n' +311 'auto eth1\n' + 'iface eth1 inet manual\n\n')
312 'auto eth0\n' + 'iface eth0 inet dhcp\n' +
313 ' hwaddress c0:d6:9f:2c:e8:80\n\n')
314 net_ifaces = net.render_interfaces(ns.network_state)312 net_ifaces = net.render_interfaces(ns.network_state)
315 self.assertEqual(sorted(ifaces.split('\n')),313 self.assertEqual(sorted(ifaces.split('\n')),
316 sorted(net_ifaces.split('\n')))314 sorted(net_ifaces.split('\n')))
317315
=== added file 'tests/vmtests/test_bonding.py'
--- tests/vmtests/test_bonding.py 1970-01-01 00:00:00 +0000
+++ tests/vmtests/test_bonding.py 2015-09-04 19:23:11 +0000
@@ -0,0 +1,235 @@
1from . import VMBaseClass
2from unittest import TestCase
3
4import ipaddress
5import os
6import re
7import textwrap
8import yaml
9
10
11def iface_extract(input):
12 mo = re.search(r'^(?P<interface>\w+|\w+:\d+)\s+' +
13 r'Link encap:(?P<link_encap>\S+)\s+' +
14 r'(HWaddr\s+(?P<mac_address>\S+))?' +
15 r'(\s+inet addr:(?P<address>\S+))?' +
16 r'(\s+Bcast:(?P<broadcast>\S+)\s+)?' +
17 r'(Mask:(?P<netmask>\S+)\s+)?',
18 input, re.MULTILINE)
19
20 mtu = re.search(r'(\s+MTU:(?P<mtu>\d+)\s+)\s+', input, re.MULTILINE)
21 mtu_info = mtu.groupdict('')
22 mtu_info['mtu'] = int(mtu_info['mtu'])
23
24 if mo:
25 info = mo.groupdict('')
26 info['running'] = False
27 info['up'] = False
28 info['multicast'] = False
29 if 'RUNNING' in input:
30 info['running'] = True
31 if 'UP' in input:
32 info['up'] = True
33 if 'MULTICAST' in input:
34 info['multicast'] = True
35 info.update(mtu_info)
36 return info
37 return {}
38
39
40def ifconfig_to_dict(ifconfig):
41 interfaces = {}
42 for iface in [iface_extract(iface) for iface in ifconfig.split('\n\n')
43 if iface.strip()]:
44 interfaces[iface['interface']] = iface
45
46 return interfaces
47
48
49class TestNetworkAbs(VMBaseClass):
50 __test__ = False
51 interactive = False
52 conf_file = "examples/tests/bonding_network.yaml"
53 install_timeout = 600
54 boot_timeout = 600
55 extra_disks = []
56 extra_nics = []
57 user_data = textwrap.dedent("""\
58 #cloud-config
59 password: passw0rd
60 chpasswd: { expire: False }
61 bootcmd:
62 - mkdir -p /media/output
63 - mount /dev/vdb /media/output
64 runcmd:
65 - ifconfig -a > /media/output/ifconfig_a
66 - cp -av /etc/network/interfaces /media/output
67 - cp -av /etc/udev/rules.d/70-persistent-net.rules /media/output
68 - ip -o route show > /media/output/ip_route_show
69 - route -n > /media/output/route_n
70 - dpkg-query -W -f '${Status}' ifenslave > \
71 /media/output/ifenslave_installed
72 power_state:
73 mode: poweroff
74 """)
75
76 def test_output_files_exist(self):
77 self.output_files_exist(["ifconfig_a",
78 "interfaces",
79 "70-persistent-net.rules",
80 "ip_route_show",
81 "ifenslave_installed",
82 "route_n"])
83
84 def test_ifenslave_installed(self):
85 with open(os.path.join(self.td.mnt, "ifenslave_installed")) as fp:
86 status = fp.read().strip()
87 print('ifenslave installed: {}'.format(status))
88 self.assertEqual('install ok installed', status)
89
90 def test_etc_network_interfaces(self):
91 with open(os.path.join(self.td.mnt, "interfaces")) as fp:
92 eni = fp.read()
93 print('etc/network/interfaces:\n{}'.format(eni))
94
95 expected_eni = self.get_expected_etc_network_interfaces()
96 eni_lines = eni.split('\n')
97 for line in expected_eni.split('\n'):
98 self.assertTrue(line in eni_lines)
99
100 def test_ifconfig_output(self):
101 '''check ifconfig output with test input'''
102 network_state = self.get_network_state()
103 print('expected_network_state:\n{}'.format(
104 yaml.dump(network_state, default_flow_style=False, indent=4)))
105
106 with open(os.path.join(self.td.mnt, "ifconfig_a")) as fp:
107 ifconfig_a = fp.read()
108 print('ifconfig -a:\n{}'.format(ifconfig_a))
109
110 ifconfig_dict = ifconfig_to_dict(ifconfig_a)
111 print('parsed ifcfg dict:\n{}'.format(
112 yaml.dump(ifconfig_dict, default_flow_style=False, indent=4)))
113
114 with open(os.path.join(self.td.mnt, "ip_route_show")) as fp:
115 ip_route_show = fp.read()
116 print("ip route show:\n{}".format(ip_route_show))
117 for line in [line for line in ip_route_show.split('\n')
118 if 'src' in line]:
119 m = re.search(r'^(?P<network>\S+)\sdev\s' +
120 r'(?P<devname>\S+)\s+' +
121 r'proto kernel\s+scope link' +
122 r'\s+src\s(?P<src_ip>\S+)',
123 line)
124 route_info = m.groupdict('')
125 print(route_info)
126
127 with open(os.path.join(self.td.mnt, "route_n")) as fp:
128 route_n = fp.read()
129 print("route -n:\n{}".format(route_n))
130
131 interfaces = network_state.get('interfaces')
132 for iface in interfaces.values():
133 subnets = iface.get('subnets', {})
134 if subnets:
135 for index, subnet in zip(range(0, len(subnets)), subnets):
136 iface['index'] = index
137 if index == 0:
138 ifname = "{name}".format(**iface)
139 else:
140 ifname = "{name}:{index}".format(**iface)
141
142 self.check_interface(iface,
143 ifconfig_dict.get(ifname),
144 route_n)
145 else:
146 iface['index'] = 0
147 self.check_interface(iface,
148 ifconfig_dict.get(iface['name']),
149 route_n)
150
151 def check_interface(self, iface, ifconfig, route_n):
152 print('testing iface:\n{}\n\nifconfig:\n{}'.format(
153 iface, ifconfig))
154 subnets = iface.get('subnets', {})
155 if subnets and iface['index'] != 0:
156 ifname = "{name}:{index}".format(**iface)
157 else:
158 ifname = "{name}".format(**iface)
159
160 # initial check, do we have the correct iface ?
161 print('ifname={}'.format(ifname))
162 print("ifconfig['interface']={}".format(ifconfig['interface']))
163 self.assertEqual(ifname, ifconfig['interface'])
164
165 # check physical interface attributes
166 # FIXME: can't check mac_addr under bonding since
167 # the bond might change slave mac addrs
168 for key in ['mtu']:
169 if key in iface and iface[key]:
170 self.assertEqual(iface[key],
171 ifconfig[key])
172
173 def __get_subnet(subnets, subidx):
174 for index, subnet in zip(range(0, len(subnets)), subnets):
175 if index == subidx:
176 break
177 return subnet
178
179 # check subnet related attributes, and specifically only
180 # the subnet specified by iface['index']
181 subnets = iface.get('subnets', {})
182 if subnets:
183 subnet = __get_subnet(subnets, iface['index'])
184 if 'address' in subnet and subnet['address']:
185 if ':' in subnet['address']:
186 inet_iface = ipaddress.IPv6Interface(
187 subnet['address'])
188 else:
189 inet_iface = ipaddress.IPv4Interface(
190 subnet['address'])
191
192 # check ip addr
193 self.assertEqual(str(inet_iface.ip),
194 ifconfig['address'])
195
196 self.assertEqual(str(inet_iface.netmask),
197 ifconfig['netmask'])
198
199 self.assertEqual(
200 str(inet_iface.network.broadcast_address),
201 ifconfig['broadcast'])
202
203 # handle gateway by looking at routing table
204 if 'gateway' in subnet and subnet['gateway']:
205 gw_ip = subnet['gateway']
206 gateways = [line for line in route_n.split('\n')
207 if 'UG' in line and gw_ip in line]
208 print('matching gateways:\n{}'.format(gateways))
209 self.assertEqual(len(gateways), 1)
210 [gateways] = gateways
211 (dest, gw, genmask, flags, metric, ref, use, iface) = \
212 gateways.split()
213 print('expected gw:{} found gw:{}'.format(gw_ip, gw))
214 self.assertEqual(gw_ip, gw)
215
216
217class TrustyTestBasic(TestNetworkAbs, TestCase):
218 __test__ = False
219 repo = "maas-daily"
220 release = "trusty"
221 arch = "amd64"
222
223
224class WilyTestBasic(TestNetworkAbs, TestCase):
225 __test__ = True
226 repo = "maas-daily"
227 release = "wily"
228 arch = "amd64"
229
230
231class VividTestBasic(TestNetworkAbs, TestCase):
232 __test__ = True
233 repo = "maas-daily"
234 release = "vivid"
235 arch = "amd64"

Subscribers

People subscribed via source and target branches