Merge lp:~raharper/curtin/iodo_network_rebase_v2 into lp:~curtin-dev/curtin/trunk
- iodo_network_rebase_v2
- Merge into trunk
Proposed by
Ryan Harper
Status: | Merged |
---|---|
Merged at revision: | 257 |
Proposed branch: | lp:~raharper/curtin/iodo_network_rebase_v2 |
Merge into: | lp:~curtin-dev/curtin/trunk |
Diff against target: |
2046 lines (+1307/-455) 20 files modified
curtin/commands/apply_net.py (+96/-0) curtin/commands/block_meta.py (+1/-1) curtin/commands/curthooks.py (+29/-1) curtin/commands/install.py (+8/-1) curtin/commands/main.py (+3/-3) curtin/commands/net_meta.py (+7/-256) curtin/config.py (+4/-0) curtin/net/__init__.py (+166/-0) curtin/net/network_state.py (+360/-0) curtin/udev.py (+54/-0) curtin/util.py (+3/-1) examples/network-all.yaml (+85/-83) examples/network-bond.yaml (+42/-40) examples/network-bridge.yaml (+24/-22) examples/network-simple.yaml (+25/-20) examples/network-vlan.yaml (+18/-16) examples/tests/basic_network.yaml (+22/-0) tests/unittests/test_net.py (+74/-0) tests/vmtests/__init__.py (+62/-11) tests/vmtests/test_network.py (+224/-0) |
To merge this branch: | bzr merge lp:~raharper/curtin/iodo_network_rebase_v2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser | Pending | ||
Review via email: mp+269506@code.launchpad.net |
Commit message
Description of the change
vmtest: Add network testing
Update the base vmtests class to handle network tests. If the
class configration includes a network section, parse and use it
to extend the VM launch and run commands.
To post a comment you must log in.
- 256. By Ryan Harper
-
Handle when we don't have network config. Fix bug in expected_interfaces method.
- 257. By Ryan Harper
-
from trunk
- 258. By Ryan Harper
-
Inject versioned network config
Use the same dictionary layout as storage, including a version field and then
a config dict within the outer Network config. Fix example files. Adjust
loading of data and fixup unittests.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'curtin/commands/apply_net.py' |
2 | --- curtin/commands/apply_net.py 1970-01-01 00:00:00 +0000 |
3 | +++ curtin/commands/apply_net.py 2015-09-02 14:28:32 +0000 |
4 | @@ -0,0 +1,96 @@ |
5 | +# Copyright (C) 2015 Canonical Ltd. |
6 | +# |
7 | +# Author: Ryan Harper <ryan.harper@canonical.com> |
8 | +# |
9 | +# Curtin is free software: you can redistribute it and/or modify it under |
10 | +# the terms of the GNU Affero General Public License as published by the |
11 | +# Free Software Foundation, either version 3 of the License, or (at your |
12 | +# option) any later version. |
13 | +# |
14 | +# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY |
15 | +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
16 | +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for |
17 | +# more details. |
18 | +# |
19 | +# You should have received a copy of the GNU Affero General Public License |
20 | +# along with Curtin. If not, see <http://www.gnu.org/licenses/>. |
21 | + |
22 | +import os |
23 | +import sys |
24 | + |
25 | +import curtin.net as net |
26 | +import curtin.util as util |
27 | +from . import populate_one_subcmd |
28 | + |
29 | + |
30 | +def apply_net(target, network_state=None, network_config=None): |
31 | + if network_state is None and network_config is None: |
32 | + msg = "Must provide at least config or state" |
33 | + sys.stderr.write(msg + "\n") |
34 | + raise Exception(msg) |
35 | + |
36 | + if target is None: |
37 | + msg = "Must provide target" |
38 | + sys.stderr.write(msg + "\n") |
39 | + raise Exception(msg) |
40 | + |
41 | + if network_state: |
42 | + ns = net.network_state.from_state_file(network_state) |
43 | + elif network_config: |
44 | + ns = net.parse_net_config(network_config) |
45 | + |
46 | + net.render_network_state(target=target, network_state=ns) |
47 | + |
48 | + |
49 | +def apply_net_main(args): |
50 | + # curtin apply_net [--net-state=/config/netstate.yml] [--target=/] |
51 | + # [--net-config=/config/maas_net.yml] |
52 | + state = util.load_command_environment() |
53 | + |
54 | + if args.target is not None: |
55 | + state['target'] = args.target |
56 | + |
57 | + if args.net_state is not None: |
58 | + state['network_state'] = args.net_state |
59 | + |
60 | + if args.net_config is not None: |
61 | + state['network_config'] = args.net_config |
62 | + |
63 | + if state['target'] is None: |
64 | + sys.stderr.write("Unable to find target. " |
65 | + "Use --target or set TARGET_MOUNT_POINT\n") |
66 | + sys.exit(2) |
67 | + |
68 | + if not state['network_config'] and not state['network_state']: |
69 | + sys.stderr.write("Must provide at least config or state\n") |
70 | + sys.exit(2) |
71 | + |
72 | + apply_net(target=state['target'], |
73 | + network_state=state['network_state'], |
74 | + network_config=state['network_config']) |
75 | + |
76 | + sys.exit(0) |
77 | + |
78 | + |
79 | +CMD_ARGUMENTS = ( |
80 | + ((('-s', '--net-state'), |
81 | + {'help': ('file to read containing network state. ' |
82 | + 'defaults to env["OUTPUT_NETWORK_STATE"]'), |
83 | + 'metavar': 'NETSTATE', 'action': 'store', |
84 | + 'default': os.environ.get('OUTPUT_NETWORK_STATE')}), |
85 | + (('-t', '--target'), |
86 | + {'help': ('target filesystem root to add swap file to. ' |
87 | + 'default is env["TARGET_MOUNT_POINT"]'), |
88 | + 'metavar': 'TARGET', 'action': 'store', |
89 | + 'default': os.environ.get('TARGET_MOUNT_POINT')}), |
90 | + (('-c', '--net-config'), |
91 | + {'help': ('file to read containing curtin network config.' |
92 | + 'defaults to env["OUTPUT_NETWORK_CONFIG"]'), |
93 | + 'metavar': 'NETCONFIG', 'action': 'store', |
94 | + 'default': os.environ.get('OUTPUT_NETWORK_CONFIG')}))) |
95 | + |
96 | + |
97 | +def POPULATE_SUBCMD(parser): |
98 | + populate_one_subcmd(parser, CMD_ARGUMENTS, apply_net_main) |
99 | + |
100 | +# vi: ts=4 expandtab syntax=python |
101 | |
102 | === modified file 'curtin/commands/block_meta.py' |
103 | --- curtin/commands/block_meta.py 2015-08-27 20:27:04 +0000 |
104 | +++ curtin/commands/block_meta.py 2015-09-02 14:28:32 +0000 |
105 | @@ -21,7 +21,7 @@ |
106 | from curtin.log import LOG |
107 | |
108 | from . import populate_one_subcmd |
109 | -from curtin.commands.net_meta import compose_udev_equality |
110 | +from curtin.udev import compose_udev_equality |
111 | |
112 | import glob |
113 | import os |
114 | |
115 | === modified file 'curtin/commands/curthooks.py' |
116 | --- curtin/commands/curthooks.py 2015-08-11 16:56:18 +0000 |
117 | +++ curtin/commands/curthooks.py 2015-09-02 14:28:32 +0000 |
118 | @@ -30,6 +30,7 @@ |
119 | from curtin.log import LOG |
120 | from curtin import swap |
121 | from curtin import util |
122 | +from curtin import net |
123 | |
124 | from . import populate_one_subcmd |
125 | |
126 | @@ -407,6 +408,33 @@ |
127 | 'etc/mdadm/mdadm.conf'])) |
128 | |
129 | |
130 | +def apply_networking(target, state): |
131 | + netstate = state.get('network_state') |
132 | + netconf = state.get('network_config') |
133 | + interfaces = state.get('interfaces') |
134 | + |
135 | + def is_valid_src(infile): |
136 | + with open(infile, 'r') as fp: |
137 | + content = fp.read() |
138 | + if len(content.split('\n')) > 1: |
139 | + return True |
140 | + return False |
141 | + |
142 | + ns = None |
143 | + if is_valid_src(netstate): |
144 | + LOG.debug("applying network_state") |
145 | + ns = net.network_state.from_state_file(netstate) |
146 | + elif is_valid_src(netconf): |
147 | + LOG.debug("applying network_config") |
148 | + ns = net.parse_net_config(netconf) |
149 | + |
150 | + if ns is not None: |
151 | + net.render_network_state(target=target, network_state=ns) |
152 | + else: |
153 | + LOG.debug("copying interfaces") |
154 | + copy_interfaces(interfaces, target) |
155 | + |
156 | + |
157 | def copy_interfaces(interfaces, target): |
158 | if not interfaces: |
159 | LOG.warn("no interfaces file to copy!") |
160 | @@ -597,7 +625,7 @@ |
161 | |
162 | add_swap(cfg, target, state.get('fstab')) |
163 | |
164 | - copy_interfaces(state.get('interfaces'), target) |
165 | + apply_networking(target, state) |
166 | copy_fstab(state.get('fstab'), target) |
167 | |
168 | detect_and_handle_multipath(cfg, target) |
169 | |
170 | === modified file 'curtin/commands/install.py' |
171 | --- curtin/commands/install.py 2015-08-27 14:27:20 +0000 |
172 | +++ curtin/commands/install.py 2015-09-02 14:28:32 +0000 |
173 | @@ -56,6 +56,7 @@ |
174 | 'curthooks_commands': {'builtin': ['curtin', 'curthooks']}, |
175 | 'late_commands': {'builtin': []}, |
176 | 'network_commands': {'builtin': ['curtin', 'net-meta', 'auto']}, |
177 | + 'apply_net_commands': {'builtin': []}, |
178 | 'install': {'log_file': INSTALL_LOG}, |
179 | } |
180 | |
181 | @@ -89,6 +90,8 @@ |
182 | for p in (state_d, target_d, scratch_d): |
183 | os.mkdir(p) |
184 | |
185 | + netconf_f = os.path.join(state_d, 'network_config') |
186 | + netstate_f = os.path.join(state_d, 'network_state') |
187 | interfaces_f = os.path.join(state_d, 'interfaces') |
188 | config_f = os.path.join(state_d, 'config') |
189 | fstab_f = os.path.join(state_d, 'fstab') |
190 | @@ -97,7 +100,7 @@ |
191 | json.dump(config, fp) |
192 | |
193 | # just touch these files to make sure they exist |
194 | - for f in (interfaces_f, config_f, fstab_f): |
195 | + for f in (interfaces_f, config_f, fstab_f, netconf_f, netstate_f): |
196 | with open(f, "ab") as fp: |
197 | pass |
198 | |
199 | @@ -105,6 +108,8 @@ |
200 | self.target = target_d |
201 | self.top = top_d |
202 | self.interfaces = interfaces_f |
203 | + self.netconf = netconf_f |
204 | + self.netstate = netstate_f |
205 | self.fstab = fstab_f |
206 | self.config = config |
207 | self.config_file = config_f |
208 | @@ -112,6 +117,8 @@ |
209 | def env(self): |
210 | return ({'WORKING_DIR': self.scratch, 'OUTPUT_FSTAB': self.fstab, |
211 | 'OUTPUT_INTERFACES': self.interfaces, |
212 | + 'OUTPUT_NETWORK_CONFIG': self.netconf, |
213 | + 'OUTPUT_NETWORK_STATE': self.netstate, |
214 | 'TARGET_MOUNT_POINT': self.target, |
215 | 'CONFIG': self.config_file}) |
216 | |
217 | |
218 | === modified file 'curtin/commands/main.py' |
219 | --- curtin/commands/main.py 2015-08-11 20:23:35 +0000 |
220 | +++ curtin/commands/main.py 2015-09-02 14:28:32 +0000 |
221 | @@ -26,9 +26,9 @@ |
222 | from .. import config |
223 | from ..reporter import (events, update_configuration) |
224 | |
225 | -SUB_COMMAND_MODULES = ['block-meta', 'curthooks', 'extract', 'hook', |
226 | - 'in-target', 'install', 'mkfs', 'net-meta', 'pack', |
227 | - 'swap'] |
228 | +SUB_COMMAND_MODULES = ['apply_net', 'block-meta', 'curthooks', 'extract', |
229 | + 'hook', 'in-target', 'install', 'mkfs', 'net-meta', |
230 | + 'pack', 'swap'] |
231 | |
232 | |
233 | def add_subcmd(subparser, subcmd): |
234 | |
235 | === modified file 'curtin/commands/net_meta.py' |
236 | --- curtin/commands/net_meta.py 2015-08-07 15:49:19 +0000 |
237 | +++ curtin/commands/net_meta.py 2015-09-02 14:28:32 +0000 |
238 | @@ -21,6 +21,7 @@ |
239 | |
240 | from curtin import net |
241 | import curtin.util as util |
242 | +import curtin.config as config |
243 | |
244 | from . import populate_one_subcmd |
245 | |
246 | @@ -72,24 +73,6 @@ |
247 | |
248 | |
249 | def interfaces_custom(args): |
250 | - content = '\n'.join( |
251 | - [("# Autogenerated interfaces from net-meta custom mode"), |
252 | - "", |
253 | - "# The loopback network interface", |
254 | - "auto lo", |
255 | - "iface lo inet loopback", |
256 | - "", |
257 | - ]) |
258 | - |
259 | - command_handlers = { |
260 | - 'physical': handle_physical, |
261 | - 'vlan': handle_vlan, |
262 | - 'bond': handle_bond, |
263 | - 'bridge': handle_bridge, |
264 | - 'route': handle_route, |
265 | - 'nameserver': handle_nameserver, |
266 | - } |
267 | - |
268 | state = util.load_command_environment() |
269 | cfg = util.load_command_config(args, state) |
270 | |
271 | @@ -98,244 +81,7 @@ |
272 | raise Exception("network configuration is required by mode '%s' " |
273 | "but not provided in the config file" % 'custom') |
274 | |
275 | - for command in network_config: |
276 | - handler = command_handlers.get(command['type']) |
277 | - if not handler: |
278 | - raise ValueError("unknown command type '%s'" % command['type']) |
279 | - content += handler(command, args) |
280 | - content = content.replace('\n\n\n', '\n\n') |
281 | - |
282 | - return content |
283 | - |
284 | - |
285 | -def handle_vlan(command, args): |
286 | - ''' |
287 | - auto eth0.222 |
288 | - iface eth0.222 inet static |
289 | - address 10.10.10.1 |
290 | - netmask 255.255.255.0 |
291 | - vlan-raw-device eth0 |
292 | - ''' |
293 | - content = handle_physical(command, args)[:-1] |
294 | - content += " vlan-raw-device {}".format(command['vlan_link']) |
295 | - |
296 | - return content |
297 | - |
298 | - |
299 | -def handle_bond(command, args): |
300 | - ''' |
301 | -#/etc/network/interfaces |
302 | -auto eth0 |
303 | -iface eth0 inet manual |
304 | - |
305 | -auto eth1 |
306 | -iface eth1 inet manual |
307 | - |
308 | -auto bond0 |
309 | -iface bond0 inet static |
310 | - address 192.168.0.10 |
311 | - gateway 192.168.0.1 |
312 | - netmask 255.255.255.0 |
313 | - bond-mode 802.3ad |
314 | - bond-miimon 100 |
315 | - bond-downdelay 200 |
316 | - bond-updelay 200 |
317 | - bond-lacp-rate 4 |
318 | - ''' |
319 | - # write out bondX iface stanza and options |
320 | - content = handle_physical(command, args)[:-1] |
321 | - params = command.get('params', []) |
322 | - for param, value in params.items(): |
323 | - content += " {} {}\n".format(param, value) |
324 | - |
325 | - content += "\n" |
326 | - |
327 | - # now write out slaved iface stanzas |
328 | - for slave in command['bond_interfaces']: |
329 | - content += "auto {}\n".format(slave) |
330 | - content += "iface {} inet manual\n".format(slave) |
331 | - content += " bond-master {}\n\n".format(command['name']) |
332 | - |
333 | - return content |
334 | - |
335 | - |
336 | -def handle_bridge(command, args): |
337 | - ''' |
338 | - auto br0 |
339 | - iface br0 inet static |
340 | - address 10.10.10.1 |
341 | - netmask 255.255.255.0 |
342 | - bridge_ports eth0 eth1 |
343 | - bridge_stp off |
344 | - bridge_fd 0 |
345 | - bridge_maxwait 0 |
346 | - |
347 | - ''' |
348 | - bridge_params = [ |
349 | - "bridge_ports", |
350 | - "bridge_ageing", |
351 | - "bridge_bridgeprio", |
352 | - "bridge_fd", |
353 | - "bridge_gcint", |
354 | - "bridge_hello", |
355 | - "bridge_hw", |
356 | - "bridge_maxage", |
357 | - "bridge_maxwait", |
358 | - "bridge_pathcost", |
359 | - "bridge_portprio", |
360 | - "bridge_stp", |
361 | - "bridge_waitport", |
362 | - ] |
363 | - |
364 | - content = handle_physical(command, args)[:-1] |
365 | - content += " bridge_ports %s\n" % ( |
366 | - " ".join(command['bridge_interfaces'])) |
367 | - params = command.get('params', []) |
368 | - for param, value in params.items(): |
369 | - if param in bridge_params: |
370 | - content += " {} {}\n".format(param, value) |
371 | - |
372 | - return content |
373 | - |
374 | - |
375 | -def cidr2mask(cidr): |
376 | - mask = [0, 0, 0, 0] |
377 | - for i in list(range(0, cidr)): |
378 | - idx = int(i / 8) |
379 | - mask[idx] = mask[idx] + (1 << (7 - i % 8)) |
380 | - return ".".join([str(x) for x in mask]) |
381 | - |
382 | - |
383 | -def handle_route(command, args): |
384 | - content = "\n" |
385 | - network, cidr = command['destination'].split("/") |
386 | - netmask = cidr2mask(int(cidr)) |
387 | - command['network'] = network |
388 | - command['netmask'] = netmask |
389 | - content += "up route add" |
390 | - mapping = { |
391 | - 'network': '-net', |
392 | - 'netmask': 'netmask', |
393 | - 'gateway': 'gw', |
394 | - 'metric': 'metric', |
395 | - } |
396 | - for k in ['network', 'netmask', 'gateway', 'metric']: |
397 | - if k in command: |
398 | - content += " %s %s" % (mapping[k], command[k]) |
399 | - |
400 | - content += '\n' |
401 | - return content |
402 | - |
403 | - |
404 | -def handle_nameserver(command, args): |
405 | - content = "\n" |
406 | - if 'address' in command: |
407 | - content += "dns-nameserver {address}\n".format(**command) |
408 | - if 'search' in command: |
409 | - content += "dns-search {search}\n".format(**command) |
410 | - |
411 | - return content |
412 | - |
413 | - |
414 | -def handle_physical(command, args): |
415 | - ''' |
416 | - command = { |
417 | - 'type': 'physical', |
418 | - 'mac_address': 'c0:d6:9f:2c:e8:80', |
419 | - 'name': 'eth0', |
420 | - 'subnets': [ |
421 | - {'type': 'dhcp4'} |
422 | - ] |
423 | - } |
424 | - ''' |
425 | - ctxt = { |
426 | - 'name': command.get('name'), |
427 | - 'inet': 'inet', |
428 | - 'mode': 'manual', |
429 | - 'mtu': command.get('mtu'), |
430 | - 'address': None, |
431 | - 'gateway': None, |
432 | - 'subnets': command.get('subnets'), |
433 | - } |
434 | - |
435 | - content = "" |
436 | - content += "auto {name}\n".format(**ctxt) |
437 | - subnets = command.get('subnets', {}) |
438 | - if subnets: |
439 | - for index, subnet in zip(range(0, len(subnets)), subnets): |
440 | - ctxt['index'] = index |
441 | - ctxt['mode'] = subnet['type'] |
442 | - if ctxt['mode'].endswith('6'): |
443 | - ctxt['inet'] += '6' |
444 | - elif ctxt['mode'] == 'static' and ":" in subnet['address']: |
445 | - ctxt['inet'] += '6' |
446 | - if ctxt['mode'].startswith('dhcp'): |
447 | - ctxt['mode'] = 'dhcp' |
448 | - |
449 | - if index == 0: |
450 | - content += "iface {name} {inet} {mode}\n".format(**ctxt) |
451 | - else: |
452 | - content += \ |
453 | - "iface {name}:{index} {inet} {mode}\n".format(**ctxt) |
454 | - |
455 | - if 'mtu' in ctxt and ctxt['mtu'] and index == 0: |
456 | - content += " mtu {mtu}\n".format(**ctxt) |
457 | - if 'address' in subnet: |
458 | - content += " address {address}\n".format(**subnet) |
459 | - if 'gateway' in subnet: |
460 | - content += " gateway {gateway}\n".format(**subnet) |
461 | - content += "\n" |
462 | - else: |
463 | - content += "iface {name} {inet} {mode}\n\n".format(**ctxt) |
464 | - |
465 | - # for physical interfaces ,write out a persist net udev rule |
466 | - if command['type'] == 'physical' and \ |
467 | - 'name' in command and 'mac_address' in command: |
468 | - udev_line = generate_udev_rule(command['name'], |
469 | - command['mac_address']) |
470 | - persist_net = 'etc/udev/rules.d/70-persistent-net.rules' |
471 | - netrules = os.path.sep.join((args.target, persist_net,)) |
472 | - util.ensure_dir(os.path.dirname(netrules)) |
473 | - with open(netrules, 'a+') as f: |
474 | - f.write(udev_line) |
475 | - |
476 | - return content |
477 | - |
478 | - |
479 | -def compose_udev_equality(key, value): |
480 | - """Return a udev comparison clause, like `ACTION=="add"`.""" |
481 | - assert key == key.upper() |
482 | - return '%s=="%s"' % (key, value) |
483 | - |
484 | - |
485 | -def compose_udev_attr_equality(attribute, value): |
486 | - """Return a udev attribute comparison clause, like `ATTR{type}=="1"`.""" |
487 | - assert attribute == attribute.lower() |
488 | - return 'ATTR{%s}=="%s"' % (attribute, value) |
489 | - |
490 | - |
491 | -def compose_udev_setting(key, value): |
492 | - """Return a udev assignment clause, like `NAME="eth0"`.""" |
493 | - assert key == key.upper() |
494 | - return '%s="%s"' % (key, value) |
495 | - |
496 | - |
497 | -def generate_udev_rule(interface, mac): |
498 | - """Return a udev rule to set the name of network interface with `mac`. |
499 | - |
500 | - The rule ends up as a single line looking something like: |
501 | - |
502 | - SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", |
503 | - ATTR{address}="ff:ee:dd:cc:bb:aa", NAME="eth0" |
504 | - """ |
505 | - rule = ', '.join([ |
506 | - compose_udev_equality('SUBSYSTEM', 'net'), |
507 | - compose_udev_equality('ACTION', 'add'), |
508 | - compose_udev_equality('DRIVERS', '?*'), |
509 | - compose_udev_attr_equality('address', mac), |
510 | - compose_udev_setting('NAME', interface), |
511 | - ]) |
512 | - return '%s\n' % rule |
513 | + return config.dump_config({'network': network_config}) |
514 | |
515 | |
516 | def net_meta(args): |
517 | @@ -390,6 +136,11 @@ |
518 | content = interfaces_basic_dhcp(devices) |
519 | elif args.mode == 'custom': |
520 | content = interfaces_custom(args) |
521 | + # if we have a config, write it out to OUTPUT_NETWORK_CONFIG |
522 | + output_network_config = os.environ.get("OUTPUT_NETWORK_CONFIG", "") |
523 | + if output_network_config: |
524 | + with open(output_network_config, "w") as fp: |
525 | + fp.write(content) |
526 | |
527 | if args.output == "-": |
528 | sys.stdout.write(content) |
529 | |
530 | === modified file 'curtin/config.py' |
531 | --- curtin/config.py 2014-03-24 18:57:19 +0000 |
532 | +++ curtin/config.py 2015-09-02 14:28:32 +0000 |
533 | @@ -98,3 +98,7 @@ |
534 | return yaml.safe_load(content) |
535 | else: |
536 | return load_config_archive(content) |
537 | + |
538 | + |
539 | +def dump_config(config): |
540 | + return yaml.dump(config, default_flow_style=False) |
541 | |
542 | === modified file 'curtin/net/__init__.py' |
543 | --- curtin/net/__init__.py 2014-07-21 17:07:39 +0000 |
544 | +++ curtin/net/__init__.py 2015-09-02 14:28:32 +0000 |
545 | @@ -20,6 +20,10 @@ |
546 | import os |
547 | |
548 | from curtin.log import LOG |
549 | +from curtin.udev import generate_udev_rule |
550 | +import curtin.util as util |
551 | +import curtin.config as config |
552 | +from . import network_state |
553 | |
554 | SYS_CLASS_NET = "/sys/class/net/" |
555 | |
556 | @@ -208,4 +212,166 @@ |
557 | os.path.dirname(os.path.abspath(path))) |
558 | return ifaces |
559 | |
560 | + |
561 | +def parse_net_config_data(net_config): |
562 | + """Parses the config, returns NetworkState dictionary |
563 | + |
564 | + :param net_config: curtin network config dict |
565 | + """ |
566 | + state = None |
567 | + if 'version' in net_config and 'config' in net_config: |
568 | + ns = network_state.NetworkState(version=net_config.get('version'), |
569 | + config=net_config.get('config')) |
570 | + ns.parse_config() |
571 | + state = ns.network_state |
572 | + |
573 | + return state |
574 | + |
575 | + |
576 | +def parse_net_config(path): |
577 | + """Parses a curtin network configuration file and |
578 | + return network state""" |
579 | + ns = None |
580 | + net_config = config.load_config(path) |
581 | + if 'network' in net_config: |
582 | + ns = parse_net_config_data(net_config.get('network')) |
583 | + |
584 | + return ns |
585 | + |
586 | + |
587 | +def render_persistent_net(network_state): |
588 | + ''' Given state, emit udev rules to map |
589 | + mac to ifname |
590 | + ''' |
591 | + content = "" |
592 | + interfaces = network_state.get('interfaces') |
593 | + for iface in interfaces.values(): |
594 | + # for physical interfaces write out a persist net udev rule |
595 | + if iface['type'] == 'physical' and \ |
596 | + 'name' in iface and 'mac_address' in iface: |
597 | + content += generate_udev_rule(iface['name'], |
598 | + iface['mac_address']) |
599 | + |
600 | + return content |
601 | + |
602 | + |
603 | +# TODO: switch valid_map based on mode inet/inet6 |
604 | +def iface_add_subnet(iface, subnet): |
605 | + content = "" |
606 | + valid_map = [ |
607 | + 'address', |
608 | + 'netmask', |
609 | + 'broadcast', |
610 | + 'metric', |
611 | + 'gateway', |
612 | + 'pointopoint', |
613 | + 'hwaddress', |
614 | + 'mtu', |
615 | + 'scope', |
616 | + ] |
617 | + for key, value in subnet.items(): |
618 | + if value and key in valid_map: |
619 | + if type(value) == list: |
620 | + value = " ".join(value) |
621 | + content += " {} {}\n".format(key, value) |
622 | + |
623 | + return content |
624 | + |
625 | + |
626 | +# TODO: switch to valid_map for attrs |
627 | +def iface_add_attrs(iface): |
628 | + content = "" |
629 | + ignore_map = [ |
630 | + 'type', |
631 | + 'name', |
632 | + 'inet', |
633 | + 'mode', |
634 | + 'index', |
635 | + 'subnets', |
636 | + ] |
637 | + for key, value in iface.items(): |
638 | + if value and key not in ignore_map: |
639 | + if type(value) == list: |
640 | + value = " ".join(value) |
641 | + content += " {} {}\n".format(key, value) |
642 | + |
643 | + return content |
644 | + |
645 | + |
646 | +def render_route(route): |
647 | + content = "up route add" |
648 | + mapping = { |
649 | + 'network': '-net', |
650 | + 'netmask': 'netmask', |
651 | + 'gateway': 'gw', |
652 | + 'metric': 'metric', |
653 | + } |
654 | + for k in ['network', 'netmask', 'gateway', 'metric']: |
655 | + if k in route: |
656 | + content += " %s %s" % (mapping[k], route[k]) |
657 | + |
658 | + content += '\n' |
659 | + return content |
660 | + |
661 | + |
662 | +def render_interfaces(network_state): |
663 | + ''' Given state, emit etc/network/interfaces content ''' |
664 | + |
665 | + content = "" |
666 | + interfaces = network_state.get('interfaces') |
667 | + for iface in interfaces.values(): |
668 | + content += "auto {name}\n".format(**iface) |
669 | + |
670 | + subnets = iface.get('subnets', {}) |
671 | + if subnets: |
672 | + for index, subnet in zip(range(0, len(subnets)), subnets): |
673 | + iface['index'] = index |
674 | + iface['mode'] = subnet['type'] |
675 | + if iface['mode'].endswith('6'): |
676 | + iface['inet'] += '6' |
677 | + elif iface['mode'] == 'static' and ":" in subnet['address']: |
678 | + iface['inet'] += '6' |
679 | + if iface['mode'].startswith('dhcp'): |
680 | + iface['mode'] = 'dhcp' |
681 | + |
682 | + if index == 0: |
683 | + content += "iface {name} {inet} {mode}\n".format(**iface) |
684 | + else: |
685 | + content += "auto {name}:{index}\n".format(**iface) |
686 | + content += \ |
687 | + "iface {name}:{index} {inet} {mode}\n".format(**iface) |
688 | + |
689 | + content += iface_add_subnet(iface, subnet) |
690 | + content += iface_add_attrs(iface) |
691 | + content += "\n" |
692 | + else: |
693 | + content += "iface {name} {inet} {mode}\n".format(**iface) |
694 | + content += iface_add_attrs(iface) |
695 | + content += "\n" |
696 | + |
697 | + for (addr, dns) in network_state.get('nameservers').items(): |
698 | + content += "{}\n".format(dns) |
699 | + |
700 | + for route in network_state.get('routes'): |
701 | + content += render_route(route) |
702 | + |
703 | + # global replacements until v2 format |
704 | + content = content.replace('mac_address', 'hwaddress') |
705 | + return content |
706 | + |
707 | + |
708 | +def render_network_state(target, network_state): |
709 | + eni = 'etc/network/interfaces' |
710 | + netrules = 'etc/udev/rules.d/70-persistent-net.rules' |
711 | + |
712 | + eni = os.path.sep.join((target, eni,)) |
713 | + util.ensure_dir(os.path.dirname(eni)) |
714 | + with open(eni, 'w+') as f: |
715 | + f.write(render_interfaces(network_state)) |
716 | + |
717 | + netrules = os.path.sep.join((target, netrules,)) |
718 | + util.ensure_dir(os.path.dirname(netrules)) |
719 | + with open(netrules, 'w+') as f: |
720 | + f.write(render_persistent_net(network_state)) |
721 | + |
722 | # vi: ts=4 expandtab syntax=python |
723 | |
724 | === added file 'curtin/net/network_state.py' |
725 | --- curtin/net/network_state.py 1970-01-01 00:00:00 +0000 |
726 | +++ curtin/net/network_state.py 2015-09-02 14:28:32 +0000 |
727 | @@ -0,0 +1,360 @@ |
728 | +# Copyright (C) 2013-2014 Canonical Ltd. |
729 | +# |
730 | +# Author: Ryan Harper <ryan.harper@canonical.com> |
731 | +# |
732 | +# Curtin is free software: you can redistribute it and/or modify it under |
733 | +# the terms of the GNU Affero General Public License as published by the |
734 | +# Free Software Foundation, either version 3 of the License, or (at your |
735 | +# option) any later version. |
736 | +# |
737 | +# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY |
738 | +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
739 | +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for |
740 | +# more details. |
741 | +# |
742 | +# You should have received a copy of the GNU Affero General Public License |
743 | +# along with Curtin. If not, see <http://www.gnu.org/licenses/>. |
744 | + |
745 | +from curtin.log import LOG |
746 | +import curtin.config as curtin_config |
747 | + |
748 | +NETWORK_STATE_VERSION = 1 |
749 | +NETWORK_STATE_REQUIRED_KEYS = { |
750 | + 1: ['version', 'config', 'network_state'], |
751 | +} |
752 | + |
753 | + |
754 | +def from_state_file(state_file): |
755 | + network_state = None |
756 | + state = curtin_config.load_config(state_file) |
757 | + network_state = NetworkState() |
758 | + network_state.load(state) |
759 | + |
760 | + return network_state |
761 | + |
762 | + |
763 | +class NetworkState: |
764 | + def __init__(self, version=NETWORK_STATE_VERSION, config=None): |
765 | + self.version = version |
766 | + self.config = config |
767 | + self.network_state = { |
768 | + 'interfaces': {}, |
769 | + 'routes': [], |
770 | + 'nameservers': {}, |
771 | + } |
772 | + self.command_handlers = self.get_command_handlers() |
773 | + |
774 | + def get_command_handlers(self): |
775 | + METHOD_PREFIX = 'handle_' |
776 | + methods = filter(lambda x: callable(getattr(self, x)) and |
777 | + x.startswith(METHOD_PREFIX), dir(self)) |
778 | + handlers = {} |
779 | + for m in methods: |
780 | + key = m.replace(METHOD_PREFIX, '') |
781 | + handlers[key] = getattr(self, m) |
782 | + |
783 | + return handlers |
784 | + |
785 | + def dump(self): |
786 | + state = { |
787 | + 'version': self.version, |
788 | + 'config': self.config, |
789 | + 'network_state': self.network_state, |
790 | + } |
791 | + return curtin_config.dump_config(state) |
792 | + |
793 | + def load(self, state): |
794 | + if 'version' not in state: |
795 | + LOG.error('Invalid state, missing version field') |
796 | + raise Exception('Invalid state, missing version field') |
797 | + |
798 | + required_keys = NETWORK_STATE_REQUIRED_KEYS[state['version']] |
799 | + if not self.valid_command(state, required_keys): |
800 | + msg = 'Invalid state, missing keys: {}'.format(required_keys) |
801 | + LOG.error(msg) |
802 | + raise Exception(msg) |
803 | + |
804 | + # v1 - direct attr mapping, except version |
805 | + for key in [k for k in required_keys if k not in ['version']]: |
806 | + setattr(self, key, state[key]) |
807 | + self.command_handlers = self.get_command_handlers() |
808 | + |
809 | + def dump_network_state(self): |
810 | + return curtin_config.dump_config(self.network_state) |
811 | + |
812 | + def parse_config(self): |
813 | + # rebuild network state |
814 | + for command in self.config: |
815 | + handler = self.command_handlers.get(command['type']) |
816 | + handler(command) |
817 | + |
818 | + def valid_command(self, command, required_keys): |
819 | + if not required_keys: |
820 | + return False |
821 | + |
822 | + found_keys = [key for key in command.keys() if key in required_keys] |
823 | + return len(found_keys) == len(required_keys) |
824 | + |
825 | + def handle_physical(self, command): |
826 | + ''' |
827 | + command = { |
828 | + 'type': 'physical', |
829 | + 'mac_address': 'c0:d6:9f:2c:e8:80', |
830 | + 'name': 'eth0', |
831 | + 'subnets': [ |
832 | + {'type': 'dhcp4'} |
833 | + ] |
834 | + } |
835 | + ''' |
836 | + required_keys = [ |
837 | + 'name', |
838 | + ] |
839 | + if not self.valid_command(command, required_keys): |
840 | + LOG.warn('Skipping Invalid command: {}'.format(command)) |
841 | + LOG.debug(self.dump_network_state()) |
842 | + return |
843 | + |
844 | + interfaces = self.network_state.get('interfaces') |
845 | + iface = interfaces.get(command['name'], {}) |
846 | + iface.update({ |
847 | + 'name': command.get('name'), |
848 | + 'type': command.get('type'), |
849 | + 'mac_address': command.get('mac_address'), |
850 | + 'inet': 'inet', |
851 | + 'mode': 'manual', |
852 | + 'mtu': command.get('mtu'), |
853 | + 'address': None, |
854 | + 'gateway': None, |
855 | + 'subnets': command.get('subnets'), |
856 | + }) |
857 | + self.network_state['interfaces'].update({command.get('name'): iface}) |
858 | + self.dump_network_state() |
859 | + |
860 | + def handle_vlan(self, command): |
861 | + ''' |
862 | + auto eth0.222 |
863 | + iface eth0.222 inet static |
864 | + address 10.10.10.1 |
865 | + netmask 255.255.255.0 |
866 | + vlan-raw-device eth0 |
867 | + ''' |
868 | + required_keys = [ |
869 | + 'name', |
870 | + 'vlan_link', |
871 | + 'vlan_id', |
872 | + ] |
873 | + if not self.valid_command(command, required_keys): |
874 | + print('Skipping Invalid command: {}'.format(command)) |
875 | + print(self.dump_network_state()) |
876 | + return |
877 | + |
878 | + interfaces = self.network_state.get('interfaces') |
879 | + self.handle_physical(command) |
880 | + iface = interfaces.get(command.get('name'), {}) |
881 | + iface['vlan-raw-device'] = command.get('vlan_link') |
882 | + iface['vlan_id'] = command.get('vlan_id') |
883 | + interfaces.update({iface['name']: iface}) |
884 | + |
885 | + def handle_bond(self, command): |
886 | + ''' |
887 | + #/etc/network/interfaces |
888 | + auto eth0 |
889 | + iface eth0 inet manual |
890 | + |
891 | + auto eth1 |
892 | + iface eth1 inet manual |
893 | + |
894 | + auto bond0 |
895 | + iface bond0 inet static |
896 | + address 192.168.0.10 |
897 | + gateway 192.168.0.1 |
898 | + netmask 255.255.255.0 |
899 | + bond-mode 802.3ad |
900 | + bond-miimon 100 |
901 | + bond-downdelay 200 |
902 | + bond-updelay 200 |
903 | + bond-lacp-rate 4 |
904 | + ''' |
905 | + required_keys = [ |
906 | + 'name', |
907 | + 'bond_interfaces', |
908 | + 'params', |
909 | + ] |
910 | + if not self.valid_command(command, required_keys): |
911 | + print('Skipping Invalid command: {}'.format(command)) |
912 | + print(self.dump_network_state()) |
913 | + return |
914 | + |
915 | + self.handle_physical(command) |
916 | + interfaces = self.network_state.get('interfaces') |
917 | + iface = interfaces.get(command.get('name'), {}) |
918 | + for param, val in command.get('params').items(): |
919 | + iface.update({param: val}) |
920 | + self.network_state['interfaces'].update({iface['name']: iface}) |
921 | + |
922 | + # handle bond slaves |
923 | + for ifname in command.get('bond_interfaces'): |
924 | + if ifname not in interfaces: |
925 | + cmd = { |
926 | + 'name': ifname, |
927 | + 'type': 'bond', |
928 | + } |
929 | + # inject placeholder |
930 | + self.handle_physical(cmd) |
931 | + |
932 | + interfaces = self.network_state.get('interfaces') |
933 | + bond_if = interfaces.get(ifname) |
934 | + bond_if['bond-master'] = command.get('name') |
935 | + self.network_state['interfaces'].update({ifname: bond_if}) |
936 | + |
937 | + def handle_bridge(self, command): |
938 | + ''' |
939 | + auto br0 |
940 | + iface br0 inet static |
941 | + address 10.10.10.1 |
942 | + netmask 255.255.255.0 |
943 | + bridge_ports eth0 eth1 |
944 | + bridge_stp off |
945 | + bridge_fd 0 |
946 | + bridge_maxwait 0 |
947 | + |
948 | + bridge_params = [ |
949 | + "bridge_ports", |
950 | + "bridge_ageing", |
951 | + "bridge_bridgeprio", |
952 | + "bridge_fd", |
953 | + "bridge_gcint", |
954 | + "bridge_hello", |
955 | + "bridge_hw", |
956 | + "bridge_maxage", |
957 | + "bridge_maxwait", |
958 | + "bridge_pathcost", |
959 | + "bridge_portprio", |
960 | + "bridge_stp", |
961 | + "bridge_waitport", |
962 | + ] |
963 | + ''' |
964 | + required_keys = [ |
965 | + 'name', |
966 | + 'bridge_interfaces', |
967 | + 'params', |
968 | + ] |
969 | + if not self.valid_command(command, required_keys): |
970 | + print('Skipping Invalid command: {}'.format(command)) |
971 | + print(self.dump_network_state()) |
972 | + return |
973 | + |
974 | + # find one of the bridge port ifaces to get mac_addr |
975 | + # handle bridge_slaves |
976 | + interfaces = self.network_state.get('interfaces') |
977 | + for ifname in command.get('bridge_interfaces'): |
978 | + if ifname in interfaces: |
979 | + continue |
980 | + |
981 | + cmd = { |
982 | + 'name': ifname, |
983 | + } |
984 | + # inject placeholder |
985 | + self.handle_physical(cmd) |
986 | + |
987 | + interfaces = self.network_state.get('interfaces') |
988 | + self.handle_physical(command) |
989 | + iface = interfaces.get(command.get('name'), {}) |
990 | + iface['bridge_ports'] = command['bridge_interfaces'] |
991 | + for param, val in command.get('params').items(): |
992 | + iface.update({param: val}) |
993 | + |
994 | + interfaces.update({iface['name']: iface}) |
995 | + |
996 | + def handle_nameserver(self, command): |
997 | + required_keys = [ |
998 | + 'address', |
999 | + ] |
1000 | + if not self.valid_command(command, required_keys): |
1001 | + print('Skipping Invalid command: {}'.format(command)) |
1002 | + print(self.dump_network_state()) |
1003 | + return |
1004 | + |
1005 | + nameservers = self.network_state.get('nameservers') |
1006 | + if 'address' in command: |
1007 | + nameservers[command['address']] = \ |
1008 | + "dns-nameserver {address}".format(**command) |
1009 | + |
1010 | + def handle_route(self, command): |
1011 | + required_keys = [ |
1012 | + 'destination', |
1013 | + ] |
1014 | + if not self.valid_command(command, required_keys): |
1015 | + print('Skipping Invalid command: {}'.format(command)) |
1016 | + print(self.dump_network_state()) |
1017 | + return |
1018 | + |
1019 | + routes = self.network_state.get('routes') |
1020 | + network, cidr = command['destination'].split("/") |
1021 | + netmask = cidr2mask(int(cidr)) |
1022 | + route = { |
1023 | + 'network': network, |
1024 | + 'netmask': netmask, |
1025 | + 'gateway': command.get('gateway'), |
1026 | + 'metric': command.get('metric'), |
1027 | + } |
1028 | + routes.append(route) |
1029 | + |
1030 | + |
1031 | +def cidr2mask(cidr): |
1032 | + mask = [0, 0, 0, 0] |
1033 | + for i in list(range(0, cidr)): |
1034 | + idx = int(i / 8) |
1035 | + mask[idx] = mask[idx] + (1 << (7 - i % 8)) |
1036 | + return ".".join([str(x) for x in mask]) |
1037 | + |
1038 | + |
1039 | +if __name__ == '__main__': |
1040 | + import sys |
1041 | + import random |
1042 | + from curtin import net |
1043 | + |
1044 | + def load_config(nc): |
1045 | + version = nc.get('version') |
1046 | + config = nc.get('config') |
1047 | + return (version, config) |
1048 | + |
1049 | + def test_parse(network_config): |
1050 | + (version, config) = load_config(network_config) |
1051 | + ns1 = NetworkState(version=version, config=config) |
1052 | + ns1.parse_config() |
1053 | + random.shuffle(config) |
1054 | + ns2 = NetworkState(version=version, config=config) |
1055 | + ns2.parse_config() |
1056 | + print("----NS1-----") |
1057 | + print(ns1.dump_network_state()) |
1058 | + print() |
1059 | + print("----NS2-----") |
1060 | + print(ns2.dump_network_state()) |
1061 | + print("NS1 == NS2 ?=> {}".format( |
1062 | + ns1.network_state == ns2.network_state)) |
1063 | + eni = net.render_interfaces(ns2.network_state) |
1064 | + print(eni) |
1065 | + udev_rules = net.render_persistent_net(ns2.network_state) |
1066 | + print(udev_rules) |
1067 | + |
1068 | + def test_dump_and_load(network_config): |
1069 | + print("Loading network_config into NetworkState") |
1070 | + (version, config) = load_config(network_config) |
1071 | + ns1 = NetworkState(version=version, config=config) |
1072 | + ns1.parse_config() |
1073 | + print("Dumping state to file") |
1074 | + ns1_dump = ns1.dump() |
1075 | + ns1_state = "/tmp/ns1.state" |
1076 | + with open(ns1_state, "w+") as f: |
1077 | + f.write(ns1_dump) |
1078 | + |
1079 | + print("Loading state from file") |
1080 | + ns2 = from_state_file(ns1_state) |
1081 | + print("NS1 == NS2 ?=> {}".format( |
1082 | + ns1.network_state == ns2.network_state)) |
1083 | + |
1084 | + y = curtin_config.load_config(sys.argv[1]) |
1085 | + network_config = y.get('network') |
1086 | + test_parse(network_config) |
1087 | + test_dump_and_load(network_config) |
1088 | |
1089 | === added file 'curtin/udev.py' |
1090 | --- curtin/udev.py 1970-01-01 00:00:00 +0000 |
1091 | +++ curtin/udev.py 2015-09-02 14:28:32 +0000 |
1092 | @@ -0,0 +1,54 @@ |
1093 | +# Copyright (C) 2015 Canonical Ltd. |
1094 | +# |
1095 | +# Author: Ryan Harper <ryan.harper@canonical.com> |
1096 | +# |
1097 | +# Curtin is free software: you can redistribute it and/or modify it under |
1098 | +# the terms of the GNU Affero General Public License as published by the |
1099 | +# Free Software Foundation, either version 3 of the License, or (at your |
1100 | +# option) any later version. |
1101 | +# |
1102 | +# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY |
1103 | +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
1104 | +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for |
1105 | +# more details. |
1106 | +# |
1107 | +# You should have received a copy of the GNU Affero General Public License |
1108 | +# along with Curtin. If not, see <http://www.gnu.org/licenses/>. |
1109 | + |
1110 | + |
1111 | +def compose_udev_equality(key, value): |
1112 | + """Return a udev comparison clause, like `ACTION=="add"`.""" |
1113 | + assert key == key.upper() |
1114 | + return '%s=="%s"' % (key, value) |
1115 | + |
1116 | + |
1117 | +def compose_udev_attr_equality(attribute, value): |
1118 | + """Return a udev attribute comparison clause, like `ATTR{type}=="1"`.""" |
1119 | + assert attribute == attribute.lower() |
1120 | + return 'ATTR{%s}=="%s"' % (attribute, value) |
1121 | + |
1122 | + |
1123 | +def compose_udev_setting(key, value): |
1124 | + """Return a udev assignment clause, like `NAME="eth0"`.""" |
1125 | + assert key == key.upper() |
1126 | + return '%s="%s"' % (key, value) |
1127 | + |
1128 | + |
1129 | +def generate_udev_rule(interface, mac): |
1130 | + """Return a udev rule to set the name of network interface with `mac`. |
1131 | + |
1132 | + The rule ends up as a single line looking something like: |
1133 | + |
1134 | + SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", |
1135 | + ATTR{address}="ff:ee:dd:cc:bb:aa", NAME="eth0" |
1136 | + """ |
1137 | + rule = ', '.join([ |
1138 | + compose_udev_equality('SUBSYSTEM', 'net'), |
1139 | + compose_udev_equality('ACTION', 'add'), |
1140 | + compose_udev_equality('DRIVERS', '?*'), |
1141 | + compose_udev_attr_equality('address', mac), |
1142 | + compose_udev_setting('NAME', interface), |
1143 | + ]) |
1144 | + return '%s\n' % rule |
1145 | + |
1146 | +# vi: ts=4 expandtab syntax=python |
1147 | |
1148 | === modified file 'curtin/util.py' |
1149 | --- curtin/util.py 2015-08-11 13:30:17 +0000 |
1150 | +++ curtin/util.py 2015-09-02 14:28:32 +0000 |
1151 | @@ -101,7 +101,9 @@ |
1152 | |
1153 | mapping = {'scratch': 'WORKING_DIR', 'fstab': 'OUTPUT_FSTAB', |
1154 | 'interfaces': 'OUTPUT_INTERFACES', 'config': 'CONFIG', |
1155 | - 'target': 'TARGET_MOUNT_POINT'} |
1156 | + 'target': 'TARGET_MOUNT_POINT', |
1157 | + 'network_state': 'OUTPUT_NETWORK_STATE', |
1158 | + 'network_config': 'OUTPUT_NETWORK_CONFIG'} |
1159 | |
1160 | if strict: |
1161 | missing = [k for k in mapping if k not in env] |
1162 | |
1163 | === modified file 'examples/network-all.yaml' |
1164 | --- examples/network-all.yaml 2015-08-07 00:16:25 +0000 |
1165 | +++ examples/network-all.yaml 2015-09-02 14:28:32 +0000 |
1166 | @@ -4,88 +4,90 @@ |
1167 | |
1168 | # YAML example of a network config. |
1169 | network: |
1170 | - # Physical interfaces. |
1171 | - - type: physical |
1172 | - name: eth0 |
1173 | - mac_address: "c0:d6:9f:2c:e8:80" |
1174 | - - type: physical |
1175 | - name: eth1 |
1176 | - mac_address: "aa:d6:9f:2c:e8:80" |
1177 | - - type: physical |
1178 | - name: eth2 |
1179 | - mac_address: "c0:bb:9f:2c:e8:80" |
1180 | - - type: physical |
1181 | - name: eth3 |
1182 | - mac_address: "66:bb:9f:2c:e8:80" |
1183 | - - type: physical |
1184 | - name: eth4 |
1185 | - mac_address: "98:bb:9f:2c:e8:80" |
1186 | - # VLAN interface. |
1187 | - - type: vlan |
1188 | - name: eth0.101 |
1189 | - vlan_link: eth0 |
1190 | - vlan_id: 101 |
1191 | - mtu: 1500 |
1192 | - subnets: |
1193 | - - type: static |
1194 | - address: 192.168.0.2/24 |
1195 | - gateway: 192.168.0.1 |
1196 | - dns_nameservers: |
1197 | - - 192.168.0.10 |
1198 | - - type: static |
1199 | - address: 192.168.2.10/24 |
1200 | - # Bond. |
1201 | - - type: bond |
1202 | - name: bond0 |
1203 | - # if 'mac_address' is omitted, the MAC is taken from |
1204 | - # the first slave. |
1205 | - mac_address: "aa:bb:cc:dd:ee:ff" |
1206 | - bond_interfaces: |
1207 | - - eth1 |
1208 | - - eth2 |
1209 | - params: |
1210 | - bond-mode: active-backup |
1211 | - subnets: |
1212 | - - type: dhcp6 |
1213 | - # A Bond VLAN. |
1214 | - - type: vlan |
1215 | - name: bond0.200 |
1216 | - vlan_link: bond0 |
1217 | - vlan_id: 200 |
1218 | - subnets: |
1219 | - - type: dhcp4 |
1220 | - # A bridge. |
1221 | - - type: bridge |
1222 | - name: br0 |
1223 | - bridge_interfaces: |
1224 | - - eth3 |
1225 | - - eth4 |
1226 | - ipv4_conf: |
1227 | - rp_filter: 1 |
1228 | - proxy_arp: 0 |
1229 | - forwarding: 1 |
1230 | - ipv6_conf: |
1231 | - autoconf: 1 |
1232 | - disable_ipv6: 1 |
1233 | - use_tempaddr: 1 |
1234 | - forwarding: 1 |
1235 | - # basically anything in /proc/sys/net/ipv6/conf/.../ |
1236 | - params: |
1237 | - bridge_stp: 'off' |
1238 | - bridge_fd: 0 |
1239 | - bridge_maxwait: 0 |
1240 | - subnets: |
1241 | - - type: static |
1242 | - address: 192.168.14.2/24 |
1243 | - - type: static |
1244 | - address: 2001:1::1/64 # default to /64 |
1245 | - # A global nameserver. |
1246 | - - type: nameserver |
1247 | - address: 8.8.8.8 |
1248 | - # A global route. |
1249 | - - type: route |
1250 | - destination: 10.0.0.0/8 |
1251 | - gateway: 11.0.0.1 |
1252 | - metric: 3 |
1253 | + version: 1 |
1254 | + config: |
1255 | + # Physical interfaces. |
1256 | + - type: physical |
1257 | + name: eth0 |
1258 | + mac_address: "c0:d6:9f:2c:e8:80" |
1259 | + - type: physical |
1260 | + name: eth1 |
1261 | + mac_address: "aa:d6:9f:2c:e8:80" |
1262 | + - type: physical |
1263 | + name: eth2 |
1264 | + mac_address: "c0:bb:9f:2c:e8:80" |
1265 | + - type: physical |
1266 | + name: eth3 |
1267 | + mac_address: "66:bb:9f:2c:e8:80" |
1268 | + - type: physical |
1269 | + name: eth4 |
1270 | + mac_address: "98:bb:9f:2c:e8:80" |
1271 | + # VLAN interface. |
1272 | + - type: vlan |
1273 | + name: eth0.101 |
1274 | + vlan_link: eth0 |
1275 | + vlan_id: 101 |
1276 | + mtu: 1500 |
1277 | + subnets: |
1278 | + - type: static |
1279 | + address: 192.168.0.2/24 |
1280 | + gateway: 192.168.0.1 |
1281 | + dns_nameservers: |
1282 | + - 192.168.0.10 |
1283 | + - type: static |
1284 | + address: 192.168.2.10/24 |
1285 | + # Bond. |
1286 | + - type: bond |
1287 | + name: bond0 |
1288 | + # if 'mac_address' is omitted, the MAC is taken from |
1289 | + # the first slave. |
1290 | + mac_address: "aa:bb:cc:dd:ee:ff" |
1291 | + bond_interfaces: |
1292 | + - eth1 |
1293 | + - eth2 |
1294 | + params: |
1295 | + bond-mode: active-backup |
1296 | + subnets: |
1297 | + - type: dhcp6 |
1298 | + # A Bond VLAN. |
1299 | + - type: vlan |
1300 | + name: bond0.200 |
1301 | + vlan_link: bond0 |
1302 | + vlan_id: 200 |
1303 | + subnets: |
1304 | + - type: dhcp4 |
1305 | + # A bridge. |
1306 | + - type: bridge |
1307 | + name: br0 |
1308 | + bridge_interfaces: |
1309 | + - eth3 |
1310 | + - eth4 |
1311 | + ipv4_conf: |
1312 | + rp_filter: 1 |
1313 | + proxy_arp: 0 |
1314 | + forwarding: 1 |
1315 | + ipv6_conf: |
1316 | + autoconf: 1 |
1317 | + disable_ipv6: 1 |
1318 | + use_tempaddr: 1 |
1319 | + forwarding: 1 |
1320 | + # basically anything in /proc/sys/net/ipv6/conf/.../ |
1321 | + params: |
1322 | + bridge_stp: 'off' |
1323 | + bridge_fd: 0 |
1324 | + bridge_maxwait: 0 |
1325 | + subnets: |
1326 | + - type: static |
1327 | + address: 192.168.14.2/24 |
1328 | + - type: static |
1329 | + address: 2001:1::1/64 # default to /64 |
1330 | + # A global nameserver. |
1331 | + - type: nameserver |
1332 | + address: 8.8.8.8 |
1333 | + # A global route. |
1334 | + - type: route |
1335 | + destination: 10.0.0.0/8 |
1336 | + gateway: 11.0.0.1 |
1337 | + metric: 3 |
1338 | |
1339 | |
1340 | |
1341 | === modified file 'examples/network-bond.yaml' |
1342 | --- examples/network-bond.yaml 2015-08-07 00:16:25 +0000 |
1343 | +++ examples/network-bond.yaml 2015-09-02 14:28:32 +0000 |
1344 | @@ -4,43 +4,45 @@ |
1345 | |
1346 | # YAML example of a network config. |
1347 | network: |
1348 | - # Physical interfaces. |
1349 | - - type: physical |
1350 | - name: eth0 |
1351 | - mac_address: "c0:d6:9f:2c:e8:80" |
1352 | - - type: physical |
1353 | - name: eth1 |
1354 | - mac_address: "aa:d6:9f:2c:e8:80" |
1355 | - - type: physical |
1356 | - name: eth2 |
1357 | - mac_address: "c0:bb:9f:2c:e8:80" |
1358 | - - type: physical |
1359 | - name: eth3 |
1360 | - mac_address: "66:bb:9f:2c:e8:80" |
1361 | - - type: physical |
1362 | - name: eth4 |
1363 | - mac_address: "98:bb:9f:2c:e8:80" |
1364 | - # Bond. |
1365 | - - type: bond |
1366 | - name: bond0 |
1367 | - # if 'mac_address' is omitted, the MAC is taken from |
1368 | - # the first slave. |
1369 | - mac_address: "aa:bb:cc:dd:ee:ff" |
1370 | - bond_interfaces: |
1371 | - - eth1 |
1372 | - - eth2 |
1373 | - params: |
1374 | - bond-mode: active-backup |
1375 | - subnets: |
1376 | - - type: dhcp6 |
1377 | - # A Bond VLAN. |
1378 | - - type: vlan |
1379 | - name: bond0.200 |
1380 | - vlan_link: bond0 |
1381 | - vlan_id: 200 |
1382 | - subnets: |
1383 | - - type: static |
1384 | - address: 192.168.0.2/24 |
1385 | - gateway: 192.168.0.1 |
1386 | - dns_nameservers: |
1387 | - - 192.168.0.10 |
1388 | + version: 1 |
1389 | + config: |
1390 | + # Physical interfaces. |
1391 | + - type: physical |
1392 | + name: eth0 |
1393 | + mac_address: "c0:d6:9f:2c:e8:80" |
1394 | + - type: physical |
1395 | + name: eth1 |
1396 | + mac_address: "aa:d6:9f:2c:e8:80" |
1397 | + - type: physical |
1398 | + name: eth2 |
1399 | + mac_address: "c0:bb:9f:2c:e8:80" |
1400 | + - type: physical |
1401 | + name: eth3 |
1402 | + mac_address: "66:bb:9f:2c:e8:80" |
1403 | + - type: physical |
1404 | + name: eth4 |
1405 | + mac_address: "98:bb:9f:2c:e8:80" |
1406 | + # Bond. |
1407 | + - type: bond |
1408 | + name: bond0 |
1409 | + # if 'mac_address' is omitted, the MAC is taken from |
1410 | + # the first slave. |
1411 | + mac_address: "aa:bb:cc:dd:ee:ff" |
1412 | + bond_interfaces: |
1413 | + - eth1 |
1414 | + - eth2 |
1415 | + params: |
1416 | + bond-mode: active-backup |
1417 | + subnets: |
1418 | + - type: dhcp6 |
1419 | + # A Bond VLAN. |
1420 | + - type: vlan |
1421 | + name: bond0.200 |
1422 | + vlan_link: bond0 |
1423 | + vlan_id: 200 |
1424 | + subnets: |
1425 | + - type: static |
1426 | + address: 192.168.0.2/24 |
1427 | + gateway: 192.168.0.1 |
1428 | + dns_nameservers: |
1429 | + - 192.168.0.10 |
1430 | |
1431 | === modified file 'examples/network-bridge.yaml' |
1432 | --- examples/network-bridge.yaml 2015-08-07 00:16:25 +0000 |
1433 | +++ examples/network-bridge.yaml 2015-09-02 14:28:32 +0000 |
1434 | @@ -4,25 +4,27 @@ |
1435 | |
1436 | # YAML example of a network config. |
1437 | network: |
1438 | - # Physical interfaces. |
1439 | - - type: physical |
1440 | - name: eth0 |
1441 | - mac_address: "c0:d6:9f:2c:e8:80" |
1442 | - - type: physical |
1443 | - name: eth1 |
1444 | - mac_address: "aa:d6:9f:2c:e8:80" |
1445 | - # A bridge. |
1446 | - - type: bridge |
1447 | - name: br0 |
1448 | - bridge_interfaces: |
1449 | - - eth0 |
1450 | - - eth1 |
1451 | - params: |
1452 | - bridge_stp: 'off' |
1453 | - bridge_fd: 0 |
1454 | - bridge_maxwait: 0 |
1455 | - subnets: |
1456 | - - type: static |
1457 | - address: 192.168.14.2/24 |
1458 | - - type: static |
1459 | - address: 2001:1::1/64 # default to /64 |
1460 | + version: 1 |
1461 | + config: |
1462 | + # Physical interfaces. |
1463 | + - type: physical |
1464 | + name: eth0 |
1465 | + mac_address: "c0:d6:9f:2c:e8:80" |
1466 | + - type: physical |
1467 | + name: eth1 |
1468 | + mac_address: "aa:d6:9f:2c:e8:80" |
1469 | + # A bridge. |
1470 | + - type: bridge |
1471 | + name: br0 |
1472 | + bridge_interfaces: |
1473 | + - eth0 |
1474 | + - eth1 |
1475 | + params: |
1476 | + bridge_stp: 'off' |
1477 | + bridge_fd: 0 |
1478 | + bridge_maxwait: 0 |
1479 | + subnets: |
1480 | + - type: static |
1481 | + address: 192.168.14.2/24 |
1482 | + - type: static |
1483 | + address: 2001:1::1/64 # default to /64 |
1484 | |
1485 | === modified file 'examples/network-simple.yaml' |
1486 | --- examples/network-simple.yaml 2015-08-07 00:16:25 +0000 |
1487 | +++ examples/network-simple.yaml 2015-09-02 14:28:32 +0000 |
1488 | @@ -1,25 +1,30 @@ |
1489 | network_commands: |
1490 | builtin: null |
1491 | - 10_network: curtin net-meta custom |
1492 | + 10_network: |
1493 | + - curtin |
1494 | + - net-meta |
1495 | + - custom |
1496 | |
1497 | # YAML example of a simple network config |
1498 | network: |
1499 | - # Physical interfaces. |
1500 | - - type: physical |
1501 | - name: eth0 |
1502 | - mac_address: "c0:d6:9f:2c:e8:80" |
1503 | - subnets: |
1504 | - - type: dhcp4 |
1505 | - - type: physical |
1506 | - name: eth1 |
1507 | - mtu: 1492 |
1508 | - mac_address: "aa:d6:9f:2c:e8:80" |
1509 | - subnets: |
1510 | - - type: static |
1511 | - address: 192.168.14.2/24 |
1512 | - gateway: 192.168.14.1 |
1513 | - - type: static |
1514 | - address: 192.168.14.4/24 |
1515 | - - type: physical |
1516 | - name: eth2 |
1517 | - mac_address: "cf:d6:af:48:e8:80" |
1518 | + version: 1 |
1519 | + config: |
1520 | + # Physical interfaces. |
1521 | + - type: physical |
1522 | + name: eth0 |
1523 | + mac_address: "c0:d6:9f:2c:e8:80" |
1524 | + subnets: |
1525 | + - type: dhcp4 |
1526 | + - type: physical |
1527 | + name: eth1 |
1528 | + mtu: 1492 |
1529 | + mac_address: "aa:d6:9f:2c:e8:80" |
1530 | + subnets: |
1531 | + - type: static |
1532 | + address: 192.168.14.2/24 |
1533 | + gateway: 192.168.14.1 |
1534 | + - type: static |
1535 | + address: 192.168.14.4/24 |
1536 | + - type: physical |
1537 | + name: eth2 |
1538 | + mac_address: "cf:d6:af:48:e8:80" |
1539 | |
1540 | === modified file 'examples/network-vlan.yaml' |
1541 | --- examples/network-vlan.yaml 2015-08-07 00:16:25 +0000 |
1542 | +++ examples/network-vlan.yaml 2015-09-02 14:28:32 +0000 |
1543 | @@ -4,19 +4,21 @@ |
1544 | |
1545 | # YAML example of a network config. |
1546 | network: |
1547 | - # Physical interfaces. |
1548 | - - type: physical |
1549 | - name: eth0 |
1550 | - mac_address: "c0:d6:9f:2c:e8:80" |
1551 | - # VLAN interface. |
1552 | - - type: vlan |
1553 | - name: eth0.101 |
1554 | - vlan_link: eth0 |
1555 | - vlan_id: 101 |
1556 | - mtu: 1500 |
1557 | - subnets: |
1558 | - - type: static |
1559 | - address: 192.168.0.2/24 |
1560 | - gateway: 192.168.0.1 |
1561 | - dns_nameservers: |
1562 | - - 192.168.0.10 |
1563 | + version: 1 |
1564 | + config: |
1565 | + # Physical interfaces. |
1566 | + - type: physical |
1567 | + name: eth0 |
1568 | + mac_address: "c0:d6:9f:2c:e8:80" |
1569 | + # VLAN interface. |
1570 | + - type: vlan |
1571 | + name: eth0.101 |
1572 | + vlan_link: eth0 |
1573 | + vlan_id: 101 |
1574 | + mtu: 1500 |
1575 | + subnets: |
1576 | + - type: static |
1577 | + address: 192.168.0.2/24 |
1578 | + gateway: 192.168.0.1 |
1579 | + dns_nameservers: |
1580 | + - 192.168.0.10 |
1581 | |
1582 | === added file 'examples/tests/basic_network.yaml' |
1583 | --- examples/tests/basic_network.yaml 1970-01-01 00:00:00 +0000 |
1584 | +++ examples/tests/basic_network.yaml 2015-09-02 14:28:32 +0000 |
1585 | @@ -0,0 +1,22 @@ |
1586 | +network: |
1587 | + version: 1 |
1588 | + config: |
1589 | + # Physical interfaces. |
1590 | + - type: physical |
1591 | + name: eth0 |
1592 | + mac_address: "52:54:00:12:34:00" |
1593 | + subnets: |
1594 | + - type: dhcp4 |
1595 | + - type: physical |
1596 | + name: eth1 |
1597 | + mtu: 1492 |
1598 | + mac_address: "52:54:00:12:34:02" |
1599 | + subnets: |
1600 | + - type: static |
1601 | + address: 10.0.2.100/24 |
1602 | + gateway: 10.0.2.1 |
1603 | + - type: static |
1604 | + address: 10.0.2.200/24 |
1605 | + - type: physical |
1606 | + name: eth2 |
1607 | + mac_address: "52:54:00:12:34:04" |
1608 | |
1609 | === modified file 'tests/unittests/test_net.py' |
1610 | --- tests/unittests/test_net.py 2014-07-21 17:07:39 +0000 |
1611 | +++ tests/unittests/test_net.py 2015-09-02 14:28:32 +0000 |
1612 | @@ -2,8 +2,10 @@ |
1613 | import os |
1614 | import shutil |
1615 | import tempfile |
1616 | +import yaml |
1617 | |
1618 | from curtin import net |
1619 | +import curtin.net.network_state as network_state |
1620 | from textwrap import dedent |
1621 | |
1622 | |
1623 | @@ -241,4 +243,76 @@ |
1624 | expected = net.parse_deb_config(i_path) |
1625 | self.assertEqual(data, expected) |
1626 | |
1627 | + |
1628 | +class TestNetConfig(TestCase): |
1629 | + def setUp(self): |
1630 | + self.target = tempfile.mkdtemp() |
1631 | + self.config_f = os.path.join(self.target, 'config') |
1632 | + self.config = ''' |
1633 | +# YAML example of a simple network config |
1634 | +network: |
1635 | + version: 1 |
1636 | + config: |
1637 | + # Physical interfaces. |
1638 | + - type: physical |
1639 | + name: eth0 |
1640 | + mac_address: "c0:d6:9f:2c:e8:80" |
1641 | + subnets: |
1642 | + - type: dhcp4 |
1643 | + - type: physical |
1644 | + name: eth1 |
1645 | + mac_address: "cf:d6:af:48:e8:80" |
1646 | +''' |
1647 | + with open(self.config_f, 'w') as fp: |
1648 | + fp.write(self.config) |
1649 | + |
1650 | + def get_net_config(self): |
1651 | + cfg = yaml.safe_load(self.config) |
1652 | + return cfg.get('network') |
1653 | + |
1654 | + def get_net_state(self): |
1655 | + net_cfg = self.get_net_config() |
1656 | + version = net_cfg.get('version') |
1657 | + config = net_cfg.get('config') |
1658 | + ns = network_state.NetworkState(version=version, config=config) |
1659 | + ns.parse_config() |
1660 | + return ns |
1661 | + |
1662 | + def tearDown(self): |
1663 | + shutil.rmtree(self.target) |
1664 | + |
1665 | + def test_parse_net_config_data(self): |
1666 | + ns = self.get_net_state() |
1667 | + net_state_from_cls = ns.network_state |
1668 | + |
1669 | + net_state_from_fn = net.parse_net_config_data(self.get_net_config()) |
1670 | + self.assertEqual(net_state_from_cls, net_state_from_fn) |
1671 | + |
1672 | + def test_parse_net_config(self): |
1673 | + ns = self.get_net_state() |
1674 | + net_state_from_cls = ns.network_state |
1675 | + |
1676 | + net_state_from_fn = net.parse_net_config(self.config_f) |
1677 | + self.assertEqual(net_state_from_cls, net_state_from_fn) |
1678 | + |
1679 | + def test_render_persistent_net(self): |
1680 | + ns = self.get_net_state() |
1681 | + udev_rules = ('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' + |
1682 | + 'ATTR{address}=="cf:d6:af:48:e8:80", NAME="eth1"\n' + |
1683 | + 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' + |
1684 | + 'ATTR{address}=="c0:d6:9f:2c:e8:80", NAME="eth0"\n') |
1685 | + persist_net_rules = net.render_persistent_net(ns.network_state) |
1686 | + self.assertEqual(sorted(udev_rules.split('\n')), |
1687 | + sorted(persist_net_rules.split('\n'))) |
1688 | + |
1689 | + def test_render_interfaces(self): |
1690 | + ns = self.get_net_state() |
1691 | + ifaces = ('auto eth1\n' + 'iface eth1 inet manual\n' + |
1692 | + ' hwaddress cf:d6:af:48:e8:80\n\n' + |
1693 | + 'auto eth0\n' + 'iface eth0 inet dhcp\n' + |
1694 | + ' hwaddress c0:d6:9f:2c:e8:80\n\n') |
1695 | + net_ifaces = net.render_interfaces(ns.network_state) |
1696 | + self.assertEqual(sorted(ifaces.split('\n')), |
1697 | + sorted(net_ifaces.split('\n'))) |
1698 | + |
1699 | # vi: ts=4 expandtab syntax=python |
1700 | |
1701 | === modified file 'tests/vmtests/__init__.py' |
1702 | --- tests/vmtests/__init__.py 2015-08-27 20:16:43 +0000 |
1703 | +++ tests/vmtests/__init__.py 2015-09-02 14:28:32 +0000 |
1704 | @@ -5,6 +5,7 @@ |
1705 | import shutil |
1706 | import subprocess |
1707 | import tempfile |
1708 | +import curtin.net as curtin_net |
1709 | |
1710 | IMAGE_DIR = "/srv/images" |
1711 | |
1712 | @@ -22,9 +23,12 @@ |
1713 | print('Query simplestreams for root image: ' |
1714 | 'release={release} arch={arch}'.format(release=release, |
1715 | arch=arch)) |
1716 | - out = subprocess.check_output( |
1717 | - ["tools/usquery", "--max=1", repo, "release=%s" % release, |
1718 | - "arch=%s" % arch, "item_name=root-image.gz"]) |
1719 | + cmd = ["tools/usquery", "--max=1", repo, "release=%s" % release, |
1720 | + "krel=%s" % release, "arch=%s" % arch, |
1721 | + "item_name=root-image.gz"] |
1722 | + print(" ".join(cmd)) |
1723 | + out = subprocess.check_output(cmd) |
1724 | + print(out) |
1725 | sstream_data = ast.literal_eval(bytes.decode(out)) |
1726 | |
1727 | # Check if we already have the image |
1728 | @@ -127,18 +131,38 @@ |
1729 | if not self.interactive: |
1730 | cmd.extend(["--silent", "--power=off"]) |
1731 | |
1732 | + # check for network configuration |
1733 | + self.network_state = curtin_net.parse_net_config(self.conf_file) |
1734 | + print(self.network_state) |
1735 | + |
1736 | + # build -n arg list with macaddrs from net_config physical config |
1737 | + macs = [] |
1738 | + interfaces = {} |
1739 | + if self.network_state: |
1740 | + interfaces = self.network_state.get('interfaces') |
1741 | + for ifname in interfaces: |
1742 | + print(ifname) |
1743 | + iface = interfaces.get(ifname) |
1744 | + hwaddr = iface.get('mac_address') |
1745 | + if hwaddr: |
1746 | + macs.append(hwaddr) |
1747 | + netdevs = [] |
1748 | + if len(macs) > 0: |
1749 | + for mac in macs: |
1750 | + netdevs.extend(["--netdev=user,mac={}".format(mac)]) |
1751 | + else: |
1752 | + netdevs.extend(["--netdev=user"]) |
1753 | + |
1754 | # build disk arguments |
1755 | extra_disks = [] |
1756 | for (disk_no, disk_sz) in enumerate(self.extra_disks): |
1757 | dpath = os.path.join(self.td.tmpdir, 'extra_disk_%d.img' % disk_no) |
1758 | extra_disks.extend(['--disk', '{}:{}'.format(dpath, disk_sz)]) |
1759 | |
1760 | - cmd.extend(["--netdev=user", "--disk", self.td.target_disk] + |
1761 | - extra_disks + |
1762 | - [boot_img, "--kernel=%s" % boot_kernel, |
1763 | - "--initrd=%s" % boot_initrd, |
1764 | - "--", "curtin", "install", |
1765 | - "--config=%s" % self.conf_file, "cp:///"]) |
1766 | + cmd.extend(netdevs + ["--disk", self.td.target_disk] + extra_disks + |
1767 | + [boot_img, "--kernel=%s" % boot_kernel, "--initrd=%s" % |
1768 | + boot_initrd, "--", "curtin", "install", "--config=%s" % |
1769 | + self.conf_file, "cp:///"]) |
1770 | |
1771 | # run vm with installer |
1772 | try: |
1773 | @@ -172,8 +196,8 @@ |
1774 | extra_disks = [x if ":" not in x else x.split(':')[0] |
1775 | for x in extra_disks] |
1776 | # create xkvm cmd |
1777 | - cmd = (["tools/xkvm", "--netdev=user", "--disk", self.td.target_disk, |
1778 | - "--disk", self.td.output_disk] + extra_disks + |
1779 | + cmd = (["tools/xkvm"] + netdevs + ["--disk", self.td.target_disk, |
1780 | + "--disk", self.td.output_disk] + extra_disks + |
1781 | ["--", "-drive", |
1782 | "file=%s,if=virtio,media=cdrom" % self.td.seed_disk, |
1783 | "-m", "1024"]) |
1784 | @@ -208,6 +232,33 @@ |
1785 | if os.path.exists("./serial.log"): |
1786 | os.remove("./serial.log") |
1787 | |
1788 | + @classmethod |
1789 | + def expected_interfaces(self): |
1790 | + expected = [] |
1791 | + interfaces = {} |
1792 | + if self.network_state: |
1793 | + interfaces = self.network_state.get('interfaces') |
1794 | + # handle interface aliases when subnets have multiple entries |
1795 | + for iface in interfaces.values(): |
1796 | + subnets = iface.get('subnets', {}) |
1797 | + if subnets: |
1798 | + for index, subnet in zip(range(0, len(subnets)), subnets): |
1799 | + if index == 0: |
1800 | + expected.append(iface) |
1801 | + else: |
1802 | + expected.append("{}:{}".format(iface, index)) |
1803 | + else: |
1804 | + expected.append(iface) |
1805 | + return expected |
1806 | + |
1807 | + @classmethod |
1808 | + def get_network_state(self): |
1809 | + return self.network_state |
1810 | + |
1811 | + @classmethod |
1812 | + def get_expected_etc_network_interfaces(self): |
1813 | + return curtin_net.render_interfaces(self.network_state) |
1814 | + |
1815 | # Misc functions that are useful for many tests |
1816 | def output_files_exist(self, files): |
1817 | for f in files: |
1818 | |
1819 | === added file 'tests/vmtests/test_network.py' |
1820 | --- tests/vmtests/test_network.py 1970-01-01 00:00:00 +0000 |
1821 | +++ tests/vmtests/test_network.py 2015-09-02 14:28:32 +0000 |
1822 | @@ -0,0 +1,224 @@ |
1823 | +from . import VMBaseClass |
1824 | +from unittest import TestCase |
1825 | + |
1826 | +import ipaddress |
1827 | +import os |
1828 | +import re |
1829 | +import textwrap |
1830 | +import yaml |
1831 | + |
1832 | + |
1833 | +def iface_extract(input): |
1834 | + mo = re.search(r'^(?P<interface>\w+|\w+:\d+)\s+' + |
1835 | + r'Link encap:(?P<link_encap>\S+)\s+' + |
1836 | + r'(HWaddr\s+(?P<mac_address>\S+))?' + |
1837 | + r'(\s+inet addr:(?P<address>\S+))?' + |
1838 | + r'(\s+Bcast:(?P<broadcast>\S+)\s+)?' + |
1839 | + r'(Mask:(?P<netmask>\S+)\s+)?', |
1840 | + input, re.MULTILINE) |
1841 | + |
1842 | + mtu = re.search(r'(\s+MTU:(?P<mtu>\d+)\s+)\s+', input, re.MULTILINE) |
1843 | + mtu_info = mtu.groupdict('') |
1844 | + mtu_info['mtu'] = int(mtu_info['mtu']) |
1845 | + |
1846 | + if mo: |
1847 | + info = mo.groupdict('') |
1848 | + info['running'] = False |
1849 | + info['up'] = False |
1850 | + info['multicast'] = False |
1851 | + if 'RUNNING' in input: |
1852 | + info['running'] = True |
1853 | + if 'UP' in input: |
1854 | + info['up'] = True |
1855 | + if 'MULTICAST' in input: |
1856 | + info['multicast'] = True |
1857 | + info.update(mtu_info) |
1858 | + return info |
1859 | + return {} |
1860 | + |
1861 | + |
1862 | +def ifconfig_to_dict(ifconfig): |
1863 | + interfaces = {} |
1864 | + for iface in [iface_extract(iface) for iface in ifconfig.split('\n\n') |
1865 | + if iface.strip()]: |
1866 | + interfaces[iface['interface']] = iface |
1867 | + |
1868 | + return interfaces |
1869 | + |
1870 | + |
1871 | +class TestNetworkAbs(VMBaseClass): |
1872 | + __test__ = False |
1873 | + interactive = False |
1874 | + conf_file = "examples/tests/basic_network.yaml" |
1875 | + install_timeout = 600 |
1876 | + boot_timeout = 600 |
1877 | + extra_disks = [] |
1878 | + extra_nics = [] |
1879 | + user_data = textwrap.dedent("""\ |
1880 | + #cloud-config |
1881 | + password: passw0rd |
1882 | + chpasswd: { expire: False } |
1883 | + bootcmd: |
1884 | + - mkdir -p /media/output |
1885 | + - mount /dev/vdb /media/output |
1886 | + runcmd: |
1887 | + - ifconfig -a > /media/output/ifconfig_a |
1888 | + - cp -av /etc/network/interfaces /media/output |
1889 | + - cp -av /etc/udev/rules.d/70-persistent-net.rules /media/output |
1890 | + - ip -o route show > /media/output/ip_route_show |
1891 | + - route -n > /media/output/route_n |
1892 | + power_state: |
1893 | + mode: poweroff |
1894 | + """) |
1895 | + |
1896 | + def test_output_files_exist(self): |
1897 | + self.output_files_exist(["ifconfig_a", |
1898 | + "interfaces", |
1899 | + "70-persistent-net.rules", |
1900 | + "ip_route_show", |
1901 | + "route_n"]) |
1902 | + |
1903 | + def test_etc_network_interfaces(self): |
1904 | + with open(os.path.join(self.td.mnt, "interfaces")) as fp: |
1905 | + eni = fp.read() |
1906 | + print('etc/network/interfaces:\n{}'.format(eni)) |
1907 | + |
1908 | + expected_eni = self.get_expected_etc_network_interfaces() |
1909 | + eni_lines = eni.split('\n') |
1910 | + for line in expected_eni.split('\n'): |
1911 | + self.assertTrue(line in eni_lines) |
1912 | + |
1913 | + def test_ifconfig_output(self): |
1914 | + '''check ifconfig output with test input''' |
1915 | + network_state = self.get_network_state() |
1916 | + print('expected_network_state:\n{}'.format( |
1917 | + yaml.dump(network_state, default_flow_style=False, indent=4))) |
1918 | + |
1919 | + with open(os.path.join(self.td.mnt, "ifconfig_a")) as fp: |
1920 | + ifconfig_a = fp.read() |
1921 | + print('ifconfig -a:\n{}'.format(ifconfig_a)) |
1922 | + |
1923 | + ifconfig_dict = ifconfig_to_dict(ifconfig_a) |
1924 | + print('parsed ifcfg dict:\n{}'.format( |
1925 | + yaml.dump(ifconfig_dict, default_flow_style=False, indent=4))) |
1926 | + |
1927 | + with open(os.path.join(self.td.mnt, "ip_route_show")) as fp: |
1928 | + ip_route_show = fp.read() |
1929 | + print("ip route show:\n{}".format(ip_route_show)) |
1930 | + for line in [line for line in ip_route_show.split('\n') |
1931 | + if 'src' in line]: |
1932 | + m = re.search(r'^(?P<network>\S+)\sdev\s' + |
1933 | + r'(?P<devname>\S+)\s+' + |
1934 | + r'proto kernel\s+scope link' + |
1935 | + r'\s+src\s(?P<src_ip>\S+)', |
1936 | + line) |
1937 | + route_info = m.groupdict('') |
1938 | + print(route_info) |
1939 | + |
1940 | + with open(os.path.join(self.td.mnt, "route_n")) as fp: |
1941 | + route_n = fp.read() |
1942 | + print("route -n:\n{}".format(route_n)) |
1943 | + |
1944 | + interfaces = network_state.get('interfaces') |
1945 | + for iface in interfaces.values(): |
1946 | + subnets = iface.get('subnets', {}) |
1947 | + if subnets: |
1948 | + for index, subnet in zip(range(0, len(subnets)), subnets): |
1949 | + iface['index'] = index |
1950 | + if index == 0: |
1951 | + ifname = "{name}".format(**iface) |
1952 | + else: |
1953 | + ifname = "{name}:{index}".format(**iface) |
1954 | + |
1955 | + self.check_interface(iface, |
1956 | + ifconfig_dict.get(ifname), |
1957 | + route_n) |
1958 | + else: |
1959 | + iface['index'] = 0 |
1960 | + self.check_interface(iface, |
1961 | + ifconfig_dict.get(iface['name']), |
1962 | + route_n) |
1963 | + |
1964 | + def check_interface(self, iface, ifconfig, route_n): |
1965 | + print('testing iface:\n{}\n\nifconfig:\n{}'.format( |
1966 | + iface, ifconfig)) |
1967 | + subnets = iface.get('subnets', {}) |
1968 | + if subnets and iface['index'] != 0: |
1969 | + ifname = "{name}:{index}".format(**iface) |
1970 | + else: |
1971 | + ifname = "{name}".format(**iface) |
1972 | + |
1973 | + # initial check, do we have the correct iface ? |
1974 | + print('ifname={}'.format(ifname)) |
1975 | + print("ifconfig['interface']={}".format(ifconfig['interface'])) |
1976 | + self.assertEqual(ifname, ifconfig['interface']) |
1977 | + |
1978 | + # check physical interface attributes |
1979 | + for key in ['mac_address', 'mtu']: |
1980 | + if key in iface and iface[key]: |
1981 | + self.assertEqual(iface[key], |
1982 | + ifconfig[key]) |
1983 | + |
1984 | + def __get_subnet(subnets, subidx): |
1985 | + for index, subnet in zip(range(0, len(subnets)), subnets): |
1986 | + if index == subidx: |
1987 | + break |
1988 | + return subnet |
1989 | + |
1990 | + # check subnet related attributes, and specifically only |
1991 | + # the subnet specified by iface['index'] |
1992 | + subnets = iface.get('subnets', {}) |
1993 | + if subnets: |
1994 | + subnet = __get_subnet(subnets, iface['index']) |
1995 | + if 'address' in subnet and subnet['address']: |
1996 | + if ':' in subnet['address']: |
1997 | + inet_iface = ipaddress.IPv6Interface( |
1998 | + subnet['address']) |
1999 | + else: |
2000 | + inet_iface = ipaddress.IPv4Interface( |
2001 | + subnet['address']) |
2002 | + |
2003 | + # check ip addr |
2004 | + self.assertEqual(str(inet_iface.ip), |
2005 | + ifconfig['address']) |
2006 | + |
2007 | + self.assertEqual(str(inet_iface.netmask), |
2008 | + ifconfig['netmask']) |
2009 | + |
2010 | + self.assertEqual( |
2011 | + str(inet_iface.network.broadcast_address), |
2012 | + ifconfig['broadcast']) |
2013 | + |
2014 | + # handle gateway by looking at routing table |
2015 | + if 'gateway' in subnet and subnet['gateway']: |
2016 | + gw_ip = subnet['gateway'] |
2017 | + gateways = [line for line in route_n.split('\n') |
2018 | + if 'UG' in line and gw_ip in line] |
2019 | + print('matching gateways:\n{}'.format(gateways)) |
2020 | + self.assertEqual(len(gateways), 1) |
2021 | + [gateways] = gateways |
2022 | + (dest, gw, genmask, flags, metric, ref, use, iface) = \ |
2023 | + gateways.split() |
2024 | + print('expected gw:{} found gw:{}'.format(gw_ip, gw)) |
2025 | + self.assertEqual(gw_ip, gw) |
2026 | + |
2027 | + |
2028 | +class TrustyTestBasic(TestNetworkAbs, TestCase): |
2029 | + __test__ = True |
2030 | + repo = "maas-daily" |
2031 | + release = "trusty" |
2032 | + arch = "amd64" |
2033 | + |
2034 | + |
2035 | +class WilyTestBasic(TestNetworkAbs, TestCase): |
2036 | + __test__ = True |
2037 | + repo = "maas-daily" |
2038 | + release = "wily" |
2039 | + arch = "amd64" |
2040 | + |
2041 | + |
2042 | +class VividTestBasic(TestNetworkAbs, TestCase): |
2043 | + __test__ = True |
2044 | + repo = "maas-daily" |
2045 | + release = "vivid" |
2046 | + arch = "amd64" |