Merge lp:~raharper/curtin/trunk.passthrough-netconfig into lp:~curtin-dev/curtin/trunk

Proposed by Ryan Harper
Status: Rejected
Rejected by: Scott Moser
Proposed branch: lp:~raharper/curtin/trunk.passthrough-netconfig
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 1549 lines (+1116/-93)
14 files modified
curtin/block/__init__.py (+67/-0)
curtin/commands/apply_net.py (+32/-8)
curtin/commands/curthooks.py (+47/-53)
curtin/net/__init__.py (+111/-0)
curtin/util.py (+3/-0)
examples/network-ipv6-bond-vlan.yaml (+2/-2)
examples/tests/network_v2_passthrough.yaml (+8/-0)
tests/unittests/test_commands_apply_net.py (+351/-0)
tests/unittests/test_curthooks.py (+184/-0)
tests/unittests/test_net.py (+137/-1)
tests/vmtests/test_network.py (+147/-24)
tests/vmtests/test_network_alias.py (+6/-0)
tests/vmtests/test_network_bridging.py (+15/-5)
tests/vmtests/test_network_ipv6_enisource.py (+6/-0)
To merge this branch: bzr merge lp:~raharper/curtin/trunk.passthrough-netconfig
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
curtin developers Pending
Review via email: mp+324801@code.launchpad.net

Description of the change

Enable curtin to pass network configuration through to capable targets

Curtin can detect if the cloud-init package in a target is capable of
accepting network configuration, and if so, write out the network-config
into the target instead of rendering it during installation.

To post a comment you must log in.
Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

well, first few comments
i didn't get very far. sorry.

Revision history for this message
Ryan Harper (raharper) wrote :
Download full text (4.2 KiB)

On Wed, May 31, 2017 at 4:11 PM, Scott Moser <email address hidden> wrote:

> well, first few comments
> i didn't get very far. sorry.
>
>
> Diff comments:
>
> > === modified file 'curtin/__init__.py'
> > --- curtin/__init__.py 2017-03-21 16:41:33 +0000
> > +++ curtin/__init__.py 2017-05-31 00:24:53 +0000
> > @@ -37,6 +37,8 @@
> > 'SUBCOMMAND_SYSTEM_UPGRADE',
> > # supports new format of apt configuration
> > 'APT_CONFIG_V1',
> > + # supports passing network config to target
> > + 'NETWORK_PASSTHROUGH',
>
> Why would someone need to know this ?
> i think it is related to our plan on implementing images that have builtin
> hooks but dont want to use them.
> that is worth an explanation in the commit message.
>

It's worth comment and *documentation*; will add. I think we might also
want to allow caller to
control whether we render eni or passthrough; ie, MaaS may want to *force*
an eni rendering
even if passthrough is possible.

>
> > # has version module
> > 'HAS_VERSION_MODULE',
> > ]
> >
> > === modified file 'curtin/block/__init__.py'
> > --- curtin/block/__init__.py 2017-05-19 18:52:21 +0000
> > +++ curtin/block/__init__.py 2017-05-31 00:24:53 +0000
> > @@ -978,4 +978,71 @@
> > else:
> > raise ValueError("wipe mode %s not supported" % mode)
> >
> > +
> > +def storage_config_required_packages(storage_config, mapping):
> > + """Read storage configuration dictionary and determine
> > + which packages are required for the supplied configuration
> > + to function. Return a list of packaged to install.
> > + """
> > +
> > + if not storage_config or not isinstance(storage_config, dict):
> > + raise ValueError('Invalid storage configuration. '
> > + 'Must be a dict:\n %s' % storage_config)
> > +
> > + if not mapping or not isinstance(mapping, dict):
> > + raise ValueError('Invalid storage mapping. Must be a dict')
> > +
> > + if 'storage' in storage_config:
> > + storage_config = storage_config.get('storage')
> > +
> > + needed_packages = []
> > +
> > + # get reqs by device operation type
> > + dev_configs = set(operation['type']
> > + for operation in storage_config['config'])
> > +
> > + for dev_type in dev_configs:
> > + if dev_type in mapping:
> > + needed_packages.extend(mapping[dev_type])
> > +
> > + # for any format operations, check the fstype and
> > + # determine if we need any mkfs tools as well.
> > + format_configs = set([operation['fstype']
> > + for operation in storage_config['config']
> > + if operation['type'] == 'format'])
> > + for format_type in format_configs:
> > + if format_type in mapping:
> > + needed_packages.extend(mapping[format_type])
> > +
> > + return needed_packages
> > +
> > +
> > +def detect_required_packages_mapping():
> > + """Return a dictionary providing a versioned configuration which
> maps
> > + storage configuration elements to the packages which are required
> > + for functionality.
> > +
> > + The mapping key is either...

Read more...

Revision history for this message
Scott Moser (smoser) wrote :

Summary of review:
 - your commit message doesn't mention that you've modified the block/fs depends (i dont think)
 - more unit tests would be good.
 - I don't love the 'passthrough'. if we dont have an actual need for it i think i'd drop it for now.

but over all, seems sane.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the review, will fix ack'ed items.

Revision history for this message
Scott Moser (smoser) wrote :

Moved this to 'Work in progress'. Move it back to 'Needs Review' when you "fix ack'ed items.".

Thanks

Revision history for this message
Ryan Harper (raharper) wrote :

Updated branch with fixes for items marked ACK. Custom config deps discussion still needed.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

inline comments, also:
 * Chad has started requesting Human readable """This unit tests does..."""
   in each unit test, and I generally agree.

Revision history for this message
Scott Moser (smoser) wrote :

shoot. hit submit before i was done... continuing.

Revision history for this message
Scott Moser (smoser) wrote :

a lot of prints in test cases.

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the review, will update with ACK'ed fixes. I think the items that remain for discussion:

1) parsing shebang for which python interpreter to use for cloud-init in-target
2) side-effect = (Exception) which it appears there was some confusion with the assert_not_called (which I agree need to be replaced with call_count checks).

Revision history for this message
Ryan Harper (raharper) wrote :

Fixed all of the ACK's here, filed a bug in cloud-init for UpperCase MAC issue, and applied these fixes to centos-passthrough branch, mp here (https://code.launchpad.net/~raharper/curtin/centos-passthrough/+merge/327536)

Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for the follow-up. I'll fix up (1) and (2) as discussed below.

Revision history for this message
Scott Moser (smoser) wrote :

marking this as rejected.
the changes here all went into
 https://code.launchpad.net/~raharper/curtin/centos-passthrough/+merge/327536
which got merged (see comments there).

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'curtin/block/__init__.py'
2--- curtin/block/__init__.py 2017-05-19 18:52:21 +0000
3+++ curtin/block/__init__.py 2017-06-19 16:35:41 +0000
4@@ -978,4 +978,71 @@
5 else:
6 raise ValueError("wipe mode %s not supported" % mode)
7
8+
9+def storage_config_required_packages(storage_config, mapping):
10+ """Read storage configuration dictionary and determine
11+ which packages are required for the supplied configuration
12+ to function. Return a list of packaged to install.
13+ """
14+
15+ if not storage_config or not isinstance(storage_config, dict):
16+ raise ValueError('Invalid storage configuration. '
17+ 'Must be a dict:\n %s' % storage_config)
18+
19+ if not mapping or not isinstance(mapping, dict):
20+ raise ValueError('Invalid storage mapping. Must be a dict')
21+
22+ if 'storage' in storage_config:
23+ storage_config = storage_config.get('storage')
24+
25+ needed_packages = []
26+
27+ # get reqs by device operation type
28+ dev_configs = set(operation['type']
29+ for operation in storage_config['config'])
30+
31+ for dev_type in dev_configs:
32+ if dev_type in mapping:
33+ needed_packages.extend(mapping[dev_type])
34+
35+ # for any format operations, check the fstype and
36+ # determine if we need any mkfs tools as well.
37+ format_configs = set([operation['fstype']
38+ for operation in storage_config['config']
39+ if operation['type'] == 'format'])
40+ for format_type in format_configs:
41+ if format_type in mapping:
42+ needed_packages.extend(mapping[format_type])
43+
44+ return needed_packages
45+
46+
47+def detect_required_packages_mapping():
48+ """Return a dictionary providing a versioned configuration which maps
49+ storage configuration elements to the packages which are required
50+ for functionality.
51+
52+ The mapping key is either a config type value, or an fstype value.
53+
54+ """
55+ version = 1
56+ mapping = {
57+ version: {
58+ 'handler': storage_config_required_packages,
59+ 'mapping': {
60+ 'bcache': ['bcache-tools'],
61+ 'btrfs': ['btrfs-tools'],
62+ 'ext2': ['e2fsprogs'],
63+ 'ext3': ['e2fsprogs'],
64+ 'ext4': ['e2fsprogs'],
65+ 'lvm_partition': ['lvm2'],
66+ 'lvm_volgroup': ['lvm2'],
67+ 'raid': ['mdadm'],
68+ 'xfs': ['xfsprogs']
69+ },
70+ },
71+ }
72+ return mapping
73+
74+
75 # vi: ts=4 expandtab syntax=python
76
77=== modified file 'curtin/commands/apply_net.py'
78--- curtin/commands/apply_net.py 2017-02-08 05:56:12 +0000
79+++ curtin/commands/apply_net.py 2017-06-19 16:35:41 +0000
80@@ -21,6 +21,7 @@
81 from .. import log
82 import curtin.net as net
83 import curtin.util as util
84+from curtin import config
85 from . import populate_one_subcmd
86
87
88@@ -89,15 +90,36 @@
89 sys.stderr.write(msg + "\n")
90 raise Exception(msg)
91
92+ passthrough = False
93 if network_state:
94+ # NB: we cannot support passthrough until curtin can convert from
95+ # network_state to network-config yaml
96 ns = net.network_state.from_state_file(network_state)
97+ raise ValueError('Not Supported; curtin lacks a network_state to '
98+ 'network_config converter.')
99 elif network_config:
100- ns = net.parse_net_config(network_config)
101-
102- net.render_network_state(target=target, network_state=ns)
103+ netcfg = config.load_config(network_config)
104+
105+ # curtin will pass-through the netconfig into the target
106+ # for rendering at runtime, unless:
107+ # 1) target OS does not support (cloud-init too old)
108+ LOG.info('Checking cloud-init in target [%s] for network '
109+ 'configuration passthrough support.', target)
110+ passthrough = net.netconfig_passthrough_available(target)
111+ LOG.debug('passthrough available via in-target: %s', passthrough)
112+
113+ if passthrough:
114+ LOG.info('Passing network configuration through to target: %s',
115+ target)
116+ net.render_netconfig_passthrough(target, netconfig=netcfg)
117+ else:
118+ ns = net.parse_net_config_data(netcfg.get('network', {}))
119+
120+ if not passthrough:
121+ LOG.info('Rendering network configuration in target')
122+ net.render_network_state(target=target, network_state=ns)
123
124 _maybe_remove_legacy_eth0(target)
125- LOG.info('Attempting to remove ipv6 privacy extensions')
126 _disable_ipv6_privacy_extensions(target)
127 _patch_ifupdown_ipv6_mtu_hook(target)
128
129@@ -130,6 +152,7 @@
130 by default; this races with the cloud-image desire to disable them.
131 Resolve this by allowing the cloud-image setting to win. """
132
133+ LOG.debug('Attempting to remove ipv6 privacy extensions')
134 cfg = util.target_path(target, path=path)
135 if not os.path.exists(cfg):
136 LOG.warn('Failed to find ipv6 privacy conf file %s', cfg)
137@@ -143,7 +166,7 @@
138 lines = [f.strip() for f in contents.splitlines()
139 if not f.startswith("#")]
140 if lines == known_contents:
141- LOG.info('deleting file: %s', cfg)
142+ LOG.info('Found expected contents, deleting file: %s', cfg)
143 util.del_file(cfg)
144 msg = "removed %s with known contents" % cfg
145 curtin_contents = '\n'.join(
146@@ -153,9 +176,10 @@
147 "# net.ipv6.conf.default.use_tempaddr = 2"])
148 util.write_file(cfg, curtin_contents)
149 else:
150- LOG.info('skipping, content didnt match')
151- LOG.debug("found content:\n%s", lines)
152- LOG.debug("expected contents:\n%s", known_contents)
153+ LOG.debug('skipping removal of %s, expected content not found',
154+ cfg)
155+ LOG.debug("Found content in file %s:\n%s", cfg, lines)
156+ LOG.debug("Expected contents in file %s:\n%s", cfg, known_contents)
157 msg = (bmsg + " '%s' exists with user configured content." % cfg)
158 except Exception as e:
159 msg = bmsg + " %s exists, but could not be read. %s" % (cfg, e)
160
161=== modified file 'curtin/commands/curthooks.py'
162--- curtin/commands/curthooks.py 2017-05-12 00:49:43 +0000
163+++ curtin/commands/curthooks.py 2017-06-19 16:35:41 +0000
164@@ -25,6 +25,7 @@
165
166 from curtin import config
167 from curtin import block
168+from curtin import net
169 from curtin import futil
170 from curtin.log import LOG
171 from curtin import swap
172@@ -648,6 +649,38 @@
173 update_initramfs(target, all_kernels=True)
174
175
176+def detect_required_packages(cfg):
177+ """
178+ detect packages that will be required in-target by custom config items
179+ """
180+
181+ mapping = {
182+ 'storage': block.detect_required_packages_mapping(),
183+ 'network': net.detect_required_packages_mapping(),
184+ }
185+
186+ needed_packages = []
187+ for cfg_type, cfg_map in mapping.items():
188+
189+ # skip missing or invalid config items, configs may
190+ # only have network or storage, not always both
191+ if not isinstance(cfg.get(cfg_type), dict):
192+ continue
193+
194+ cfg_version = cfg[cfg_type].get('version')
195+ if not isinstance(cfg_version, int) or cfg_version not in cfg_map:
196+ msg = ('Supplied configuration version "%s", for config type'
197+ '"%s" is not present in the known mapping.' % (cfg_version,
198+ cfg_type))
199+ raise ValueError(msg)
200+
201+ mapped_config = cfg_map[cfg_version]
202+ found_reqs = mapped_config['handler'](cfg, mapped_config['mapping'])
203+ needed_packages.extend(found_reqs)
204+
205+ return needed_packages
206+
207+
208 def install_missing_packages(cfg, target):
209 ''' describe which operation types will require specific packages
210
211@@ -655,46 +688,10 @@
212 'pkg1': ['op_name_1', 'op_name_2', ...]
213 }
214 '''
215- custom_configs = {
216- 'storage': {
217- 'lvm2': ['lvm_volgroup', 'lvm_partition'],
218- 'mdadm': ['raid'],
219- 'bcache-tools': ['bcache']},
220- 'network': {
221- 'vlan': ['vlan'],
222- 'ifenslave': ['bond'],
223- 'bridge-utils': ['bridge']},
224- }
225-
226- format_configs = {
227- 'xfsprogs': ['xfs'],
228- 'e2fsprogs': ['ext2', 'ext3', 'ext4'],
229- 'btrfs-tools': ['btrfs'],
230- }
231-
232- needed_packages = []
233+
234 installed_packages = util.get_installed_packages(target)
235- for cust_cfg, pkg_reqs in custom_configs.items():
236- if cust_cfg not in cfg:
237- continue
238-
239- all_types = set(
240- operation['type']
241- for operation in cfg[cust_cfg]['config']
242- )
243- for pkg, types in pkg_reqs.items():
244- if set(types).intersection(all_types) and \
245- pkg not in installed_packages:
246- needed_packages.append(pkg)
247-
248- format_types = set(
249- [operation['fstype']
250- for operation in cfg[cust_cfg]['config']
251- if operation['type'] == 'format'])
252- for pkg, fstypes in format_configs.items():
253- if set(fstypes).intersection(format_types) and \
254- pkg not in installed_packages:
255- needed_packages.append(pkg)
256+ needed_packages = [pkg for pkg in detect_required_packages(cfg)
257+ if pkg not in installed_packages]
258
259 arch_packages = {
260 's390x': [('s390-tools', 'zipl')],
261@@ -852,7 +849,11 @@
262 disable_overlayroot(cfg, target)
263
264 # packages may be needed prior to installing kernel
265- install_missing_packages(cfg, target)
266+ with events.ReportEventStack(
267+ name=stack_prefix + '/installing-missing-packages',
268+ reporting_enabled=True, level="INFO",
269+ description="installing missing packages"):
270+ install_missing_packages(cfg, target)
271
272 # If a /etc/iscsi/nodes/... file was created by block_meta then it
273 # needs to be copied onto the target system
274@@ -880,10 +881,15 @@
275 setup_zipl(cfg, target)
276 install_kernel(cfg, target)
277 run_zipl(cfg, target)
278-
279 restore_dist_interfaces(cfg, target)
280
281 with events.ReportEventStack(
282+ name=stack_prefix + '/system-upgrade',
283+ reporting_enabled=True, level="INFO",
284+ description="updating packages on target system"):
285+ system_upgrade(cfg, target)
286+
287+ with events.ReportEventStack(
288 name=stack_prefix + '/setting-up-swap',
289 reporting_enabled=True, level="INFO",
290 description="setting up swap"):
291@@ -907,18 +913,6 @@
292 description="configuring multipath"):
293 detect_and_handle_multipath(cfg, target)
294
295- with events.ReportEventStack(
296- name=stack_prefix + '/installing-missing-packages',
297- reporting_enabled=True, level="INFO",
298- description="installing missing packages"):
299- install_missing_packages(cfg, target)
300-
301- with events.ReportEventStack(
302- name=stack_prefix + '/system-upgrade',
303- reporting_enabled=True, level="INFO",
304- description="updating packages on target system"):
305- system_upgrade(cfg, target)
306-
307 # If a crypttab file was created by block_meta than it needs to be copied
308 # onto the target system, and update_initramfs() needs to be run, so that
309 # the cryptsetup hooks are properly configured on the installed system and
310
311=== modified file 'curtin/net/__init__.py'
312--- curtin/net/__init__.py 2017-02-06 20:58:37 +0000
313+++ curtin/net/__init__.py 2017-06-19 16:35:41 +0000
314@@ -520,7 +520,63 @@
315 return content
316
317
318+def netconfig_passthrough_available(target, feature='NETWORK_CONFIG_V2'):
319+ """
320+ Determine if curtin can pass v2 network config to in target cloud-init
321+ """
322+ LOG.debug('Checking in-target cloud-init features')
323+ cmd = ("from cloudinit import version;"
324+ "print('{}' in getattr(version, 'FEATURES', []))"
325+ .format(feature))
326+ with util.ChrootableTarget(target) as in_chroot:
327+
328+ def run_cmd(cmd):
329+ (out, _) = in_chroot.subp(cmd, capture=True)
330+ return out.strip()
331+
332+ cloudinit = util.which('cloud-init', target=target)
333+ if not cloudinit:
334+ LOG.warning('Target does not have cloud-init installed')
335+ return False
336+
337+ # here we read shebang from cloud-init and extract python path as we
338+ # cannot use the presence of the python package to determine which
339+ # python cloud-init will use. E.g trusty has python3 support but
340+ # cloud-init uses python2.7
341+ python = util.load_file(util.target_path(target, path=cloudinit))
342+ python = python.splitlines()[0].split("#!")[-1].strip()
343+ try:
344+ feature_available = run_cmd([python, '-c', cmd])
345+ except util.ProcessExecutionError:
346+ LOG.exception("An error occurred while probing cloudinit features")
347+ return False
348+
349+ available = config.value_as_boolean(feature_available)
350+ LOG.debug('%s available? %s', feature, available)
351+ return available
352+
353+
354+def render_netconfig_passthrough(target, netconfig=None):
355+ """
356+ Extract original network config and pass it
357+ through to cloud-init in target
358+ """
359+ LOG.debug("rendering passthrough netconfig")
360+ cc = 'etc/cloud/cloud.cfg.d/curtin-networking.cfg'
361+ if not isinstance(netconfig, dict):
362+ raise ValueError('Network config must be a dictionary')
363+
364+ if 'network' not in netconfig:
365+ raise ValueError("Network config must contain the key 'network'")
366+
367+ content = config.dump_config(netconfig)
368+ cc_passthrough = os.path.sep.join((target, cc,))
369+ LOG.info('Writing ' + cc_passthrough)
370+ util.write_file(cc_passthrough, content=content)
371+
372+
373 def render_network_state(target, network_state):
374+ LOG.debug("rendering eni from netconfig")
375 eni = 'etc/network/interfaces'
376 netrules = 'etc/udev/rules.d/70-persistent-net.rules'
377 cc = 'etc/cloud/cloud.cfg.d/curtin-disable-cloudinit-networking.cfg'
378@@ -542,4 +598,59 @@
379 """Returns the string value of an interface's MAC Address"""
380 return read_sys_net(ifname, "address", enoent=False)
381
382+
383+def network_config_required_packages(network_config, mapping=None):
384+
385+ if not network_config or not isinstance(network_config, dict):
386+ raise ValueError('Invalid network configuration. Must be a dict')
387+
388+ if not mapping or not isinstance(mapping, dict):
389+ raise ValueError('Invalid network mapping. Must be a dict')
390+
391+ # allow top-level 'network' key
392+ if 'network' in network_config:
393+ network_config = network_config.get('network')
394+
395+ # v1 has 'config' key and uses type: devtype elements
396+ if 'config' in network_config:
397+ dev_configs = set(device['type']
398+ for device in network_config['config'])
399+ else:
400+ # v2 has no config key
401+ dev_configs = set(cfgtype for (cfgtype, cfg) in
402+ network_config.items() if cfgtype not in ['version'])
403+
404+ needed_packages = []
405+ for dev_type in dev_configs:
406+ if dev_type in mapping:
407+ needed_packages.extend(mapping[dev_type])
408+
409+ return needed_packages
410+
411+
412+def detect_required_packages_mapping():
413+ """Return a dictionary providing a versioned configuration which maps
414+ network configuration elements to the packages which are required
415+ for functionality.
416+ """
417+ mapping = {
418+ 1: {
419+ 'handler': network_config_required_packages,
420+ 'mapping': {
421+ 'bond': ['ifenslave'],
422+ 'bridge': ['bridge-utils'],
423+ 'vlan': ['vlan']},
424+ },
425+ 2: {
426+ 'handler': network_config_required_packages,
427+ 'mapping': {
428+ 'bonds': ['ifenslave'],
429+ 'bridges': ['bridge-utils'],
430+ 'vlans': ['vlan']}
431+ },
432+ }
433+
434+ return mapping
435+
436+
437 # vi: ts=4 expandtab syntax=python
438
439=== modified file 'curtin/util.py'
440--- curtin/util.py 2017-06-05 02:55:31 +0000
441+++ curtin/util.py 2017-06-19 16:35:41 +0000
442@@ -1347,6 +1347,9 @@
443 if not path:
444 return target
445
446+ if not isinstance(path, string_types):
447+ raise ValueError("Unexpected input for path: %s" % path)
448+
449 # os.path.join("/etc", "/foo") returns "/foo". Chomp all leading /.
450 while len(path) and path[0] == "/":
451 path = path[1:]
452
453=== modified file 'examples/network-ipv6-bond-vlan.yaml'
454--- examples/network-ipv6-bond-vlan.yaml 2016-08-12 17:24:13 +0000
455+++ examples/network-ipv6-bond-vlan.yaml 2017-06-19 16:35:41 +0000
456@@ -3,10 +3,10 @@
457 config:
458 - name: interface0
459 type: physical
460- mac_address: BC:76:4E:06:96:B3
461+ mac_address: bc:76:4e:06:96:b3
462 - name: interface1
463 type: physical
464- mac_address: BC:76:4E:04:88:41
465+ mac_address: bc:76:4e:04:88:41
466 - type: bond
467 bond_interfaces:
468 - interface0
469
470=== added file 'examples/tests/network_v2_passthrough.yaml'
471--- examples/tests/network_v2_passthrough.yaml 1970-01-01 00:00:00 +0000
472+++ examples/tests/network_v2_passthrough.yaml 2017-06-19 16:35:41 +0000
473@@ -0,0 +1,8 @@
474+showtrace: true
475+network:
476+ version: 2
477+ ethernets:
478+ interface0:
479+ match:
480+ mac_address: "52:54:00:12:34:00"
481+ dhcp4: true
482
483=== added file 'tests/unittests/test_commands_apply_net.py'
484--- tests/unittests/test_commands_apply_net.py 1970-01-01 00:00:00 +0000
485+++ tests/unittests/test_commands_apply_net.py 2017-06-19 16:35:41 +0000
486@@ -0,0 +1,351 @@
487+from unittest import TestCase
488+from mock import patch, call
489+import copy
490+import os
491+
492+from curtin.commands import apply_net
493+from curtin import util
494+
495+
496+class ApplyNetTestBase(TestCase):
497+ def setUp(self):
498+ super(ApplyNetTestBase, self).setUp()
499+
500+ def add_patch(self, target, attr):
501+ """Patches specified target object and sets it as attr on test
502+ instance also schedules cleanup"""
503+ m = patch(target, autospec=True)
504+ p = m.start()
505+ self.addCleanup(m.stop)
506+ setattr(self, attr, p)
507+
508+
509+class TestApplyNet(ApplyNetTestBase):
510+ def setUp(self):
511+ super(TestApplyNet, self).setUp()
512+
513+ basepath = 'curtin.commands.apply_net.'
514+ self.add_patch(basepath + '_maybe_remove_legacy_eth0', 'mock_legacy')
515+ self.add_patch(basepath + '_disable_ipv6_privacy_extensions',
516+ 'mock_ipv6_priv')
517+ self.add_patch(basepath + '_patch_ifupdown_ipv6_mtu_hook',
518+ 'mock_ipv6_mtu')
519+ self.add_patch('curtin.net.netconfig_passthrough_available',
520+ 'mock_netpass_avail')
521+ self.add_patch('curtin.net.render_netconfig_passthrough',
522+ 'mock_netpass_render')
523+ self.add_patch('curtin.net.parse_net_config_data',
524+ 'mock_net_parsedata')
525+ self.add_patch('curtin.net.render_network_state',
526+ 'mock_net_renderstate')
527+ self.add_patch('curtin.net.network_state.from_state_file',
528+ 'mock_ns_from_file')
529+ self.add_patch('curtin.config.load_config', 'mock_load_config')
530+
531+ self.target = "my_target"
532+ self.network_config = {
533+ 'network': {
534+ 'version': 1,
535+ 'config': {},
536+ }
537+ }
538+ self.ns = {
539+ 'interfaces': {},
540+ 'routes': [],
541+ 'dns': {
542+ 'nameservers': [],
543+ 'search': [],
544+ }
545+ }
546+
547+ def test_apply_net_notarget(self):
548+ self.assertRaises(Exception,
549+ apply_net.apply_net, None, "", "")
550+
551+ def test_apply_net_nostate_or_config(self):
552+ self.assertRaises(Exception,
553+ apply_net.apply_net, "")
554+
555+ def test_apply_net_target_and_state(self):
556+ self.mock_ns_from_file.return_value = self.ns
557+
558+ self.assertRaises(ValueError,
559+ apply_net.apply_net, self.target,
560+ network_state=self.ns, network_config=None)
561+
562+ def test_apply_net_target_and_config(self):
563+ self.mock_load_config.return_value = self.network_config
564+ self.mock_netpass_avail.return_value = False
565+ self.mock_net_parsedata.return_value = self.ns
566+
567+ apply_net.apply_net(self.target, network_state=None,
568+ network_config=self.network_config)
569+
570+ self.mock_netpass_avail.assert_called_with(self.target)
571+
572+ self.mock_net_renderstate.assert_called_with(target=self.target,
573+ network_state=self.ns)
574+ self.mock_legacy.assert_called_with(self.target)
575+ self.mock_ipv6_priv.assert_called_with(self.target)
576+ self.mock_ipv6_mtu.assert_called_with(self.target)
577+
578+ def test_apply_net_target_and_config_passthrough(self):
579+ self.mock_load_config.return_value = self.network_config
580+ self.mock_netpass_avail.return_value = True
581+
582+ netcfg = "network_config.yaml"
583+ apply_net.apply_net(self.target, network_state=None,
584+ network_config=netcfg)
585+
586+ self.assertFalse(self.mock_ns_from_file.called)
587+ self.mock_load_config.assert_called_with(netcfg)
588+ self.mock_netpass_avail.assert_called_with(self.target)
589+ nc = self.network_config
590+ self.mock_netpass_render.assert_called_with(self.target, netconfig=nc)
591+
592+ self.assertFalse(self.mock_net_renderstate.called)
593+ self.mock_legacy.assert_called_with(self.target)
594+ self.mock_ipv6_priv.assert_called_with(self.target)
595+ self.mock_ipv6_mtu.assert_called_with(self.target)
596+
597+ def test_apply_net_target_and_config_passthrough_nonet(self):
598+ nc = {'storage': {}}
599+ self.mock_load_config.return_value = nc
600+ self.mock_netpass_avail.return_value = True
601+
602+ netcfg = "network_config.yaml"
603+
604+ apply_net.apply_net(self.target, network_state=None,
605+ network_config=netcfg)
606+
607+ self.assertFalse(self.mock_ns_from_file.called)
608+ self.mock_load_config.assert_called_with(netcfg)
609+ self.mock_netpass_avail.assert_called_with(self.target)
610+ self.mock_netpass_render.assert_called_with(self.target, netconfig=nc)
611+
612+ self.assertFalse(self.mock_net_renderstate.called)
613+ self.mock_legacy.assert_called_with(self.target)
614+ self.mock_ipv6_priv.assert_called_with(self.target)
615+ self.mock_ipv6_mtu.assert_called_with(self.target)
616+
617+ def test_apply_net_target_and_config_passthrough_v2_not_available(self):
618+ nc = copy.deepcopy(self.network_config)
619+ nc['network']['version'] = 2
620+ self.mock_load_config.return_value = nc
621+ self.mock_netpass_avail.return_value = False
622+ self.mock_net_parsedata.return_value = self.ns
623+
624+ netcfg = "network_config.yaml"
625+
626+ apply_net.apply_net(self.target, network_state=None,
627+ network_config=netcfg)
628+
629+ self.assertFalse(self.mock_ns_from_file.called)
630+ self.mock_load_config.assert_called_with(netcfg)
631+ self.mock_netpass_avail.assert_called_with(self.target)
632+ self.assertFalse(self.mock_netpass_render.called)
633+ self.mock_net_parsedata.assert_called_with(nc['network'])
634+
635+ self.mock_net_renderstate.assert_called_with(
636+ target=self.target, network_state=self.ns)
637+ self.mock_legacy.assert_called_with(self.target)
638+ self.mock_ipv6_priv.assert_called_with(self.target)
639+ self.mock_ipv6_mtu.assert_called_with(self.target)
640+
641+
642+class TestApplyNetPatchIfupdown(ApplyNetTestBase):
643+
644+ @patch('curtin.util.write_file')
645+ def test_apply_ipv6_mtu_hook(self, mock_write):
646+ target = 'mytarget'
647+ prehookfn = 'if-pre-up.d/mtuipv6'
648+ posthookfn = 'if-up.d/mtuipv6'
649+ mode = 0o755
650+
651+ apply_net._patch_ifupdown_ipv6_mtu_hook(target,
652+ prehookfn=prehookfn,
653+ posthookfn=posthookfn)
654+
655+ precfg = util.target_path(target, path=prehookfn)
656+ postcfg = util.target_path(target, path=posthookfn)
657+ precontents = apply_net.IFUPDOWN_IPV6_MTU_PRE_HOOK
658+ postcontents = apply_net.IFUPDOWN_IPV6_MTU_POST_HOOK
659+
660+ hook_calls = [
661+ call(precfg, precontents, mode=mode),
662+ call(postcfg, postcontents, mode=mode),
663+ ]
664+ mock_write.assert_has_calls(hook_calls)
665+
666+ @patch('curtin.util.write_file')
667+ def test_apply_ipv6_mtu_hook_write_fail(self, mock_write):
668+ target = 'mytarget'
669+ prehookfn = 'if-pre-up.d/mtuipv6'
670+ posthookfn = 'if-up.d/mtuipv6'
671+ mock_write.side_effect = (Exception)
672+
673+ self.assertRaises(Exception,
674+ apply_net._patch_ifupdown_ipv6_mtu_hook,
675+ target,
676+ prehookfn=prehookfn,
677+ posthookfn=posthookfn)
678+
679+ @patch('curtin.util.write_file')
680+ def test_apply_ipv6_mtu_hook_invalid_target(self, mock_write):
681+ """ Test that an invalid target will fail to build a
682+ proper path for util.write_file
683+ """
684+ target = {}
685+ prehookfn = 'if-pre-up.d/mtuipv6'
686+ posthookfn = 'if-up.d/mtuipv6'
687+ mock_write.side_effect = (Exception)
688+
689+ self.assertRaises(ValueError,
690+ apply_net._patch_ifupdown_ipv6_mtu_hook,
691+ target,
692+ prehookfn=prehookfn,
693+ posthookfn=posthookfn)
694+
695+ @patch('curtin.util.write_file')
696+ def test_apply_ipv6_mtu_hook_invalid_prepost_fn(self, mock_write):
697+ """ Test that invalid prepost filenames will fail to build a
698+ proper path for util.write_file
699+ """
700+ target = "mytarget"
701+ prehookfn = {'a': 1}
702+ posthookfn = {'b': 2}
703+ mock_write.side_effect = (Exception)
704+
705+ self.assertRaises(ValueError,
706+ apply_net._patch_ifupdown_ipv6_mtu_hook,
707+ target,
708+ prehookfn=prehookfn,
709+ posthookfn=posthookfn)
710+
711+
712+class TestApplyNetPatchIpv6Priv(ApplyNetTestBase):
713+
714+ @patch('curtin.util.del_file')
715+ @patch('curtin.util.load_file')
716+ @patch('os.path')
717+ @patch('curtin.util.write_file')
718+ def test_disable_ipv6_priv_extentions(self, mock_write, mock_ospath,
719+ mock_load, mock_del):
720+ target = 'mytarget'
721+ path = 'etc/sysctl.d/10-ipv6-privacy.conf'
722+ ipv6_priv_contents = (
723+ 'net.ipv6.conf.all.use_tempaddr = 2\n'
724+ 'net.ipv6.conf.default.use_tempaddr = 2')
725+ expected_ipv6_priv_contents = '\n'.join(
726+ ["# IPv6 Privacy Extensions (RFC 4941)",
727+ "# Disabled by curtin",
728+ "# net.ipv6.conf.all.use_tempaddr = 2",
729+ "# net.ipv6.conf.default.use_tempaddr = 2"])
730+ mock_ospath.exists.return_value = True
731+ mock_load.side_effect = [ipv6_priv_contents]
732+
733+ apply_net._disable_ipv6_privacy_extensions(target)
734+
735+ cfg = util.target_path(target, path=path)
736+ mock_write.assert_called_with(cfg, expected_ipv6_priv_contents)
737+
738+ @patch('curtin.util.load_file')
739+ @patch('os.path')
740+ def test_disable_ipv6_priv_extentions_decoderror(self, mock_ospath,
741+ mock_load):
742+ target = 'mytarget'
743+ mock_ospath.exists.return_value = True
744+
745+ # simulate loading of binary data
746+ mock_load.side_effect = (Exception)
747+
748+ self.assertRaises(Exception,
749+ apply_net._disable_ipv6_privacy_extensions,
750+ target)
751+
752+ @patch('curtin.util.load_file')
753+ @patch('os.path')
754+ def test_disable_ipv6_priv_extentions_notfound(self, mock_ospath,
755+ mock_load):
756+ target = 'mytarget'
757+ path = 'foo.conf'
758+ mock_ospath.exists.return_value = False
759+
760+ apply_net._disable_ipv6_privacy_extensions(target, path=path)
761+
762+ # source file not found
763+ cfg = util.target_path(target, path)
764+ mock_ospath.exists.assert_called_with(cfg)
765+ mock_load.assert_not_called()
766+
767+
768+class TestApplyNetRemoveLegacyEth0(ApplyNetTestBase):
769+
770+ @patch('curtin.util.del_file')
771+ @patch('curtin.util.load_file')
772+ @patch('os.path')
773+ def test_remove_legacy_eth0(self, mock_ospath, mock_load, mock_del):
774+ target = 'mytarget'
775+ path = 'eth0.cfg'
776+ cfg = util.target_path(target, path)
777+ legacy_eth0_contents = (
778+ 'auto eth0\n'
779+ 'iface eth0 inet dhcp')
780+
781+ mock_ospath.exists.return_value = True
782+ mock_load.side_effect = [legacy_eth0_contents]
783+
784+ apply_net._maybe_remove_legacy_eth0(target, path)
785+
786+ mock_del.assert_called_with(cfg)
787+
788+ @patch('curtin.util.del_file')
789+ @patch('curtin.util.load_file')
790+ @patch('os.path')
791+ def test_remove_legacy_eth0_nomatch(self, mock_ospath, mock_load,
792+ mock_del):
793+ target = 'mytarget'
794+ path = 'eth0.cfg'
795+ legacy_eth0_contents = "nomatch"
796+ mock_ospath.join.side_effect = os.path.join
797+ mock_ospath.exists.return_value = True
798+ mock_load.side_effect = [legacy_eth0_contents]
799+
800+ self.assertRaises(Exception,
801+ apply_net._maybe_remove_legacy_eth0,
802+ target, path)
803+
804+ mock_del.assert_not_called()
805+
806+ @patch('curtin.util.del_file')
807+ @patch('curtin.util.load_file')
808+ @patch('os.path')
809+ def test_remove_legacy_eth0_badload(self, mock_ospath, mock_load,
810+ mock_del):
811+ target = 'mytarget'
812+ path = 'eth0.cfg'
813+ mock_ospath.exists.return_value = True
814+ mock_load.side_effect = (Exception)
815+
816+ self.assertRaises(Exception,
817+ apply_net._maybe_remove_legacy_eth0,
818+ target, path)
819+
820+ mock_del.assert_not_called()
821+
822+ @patch('curtin.util.del_file')
823+ @patch('curtin.util.load_file')
824+ @patch('os.path')
825+ def test_remove_legacy_eth0_notfound(self, mock_ospath, mock_load,
826+ mock_del):
827+ target = 'mytarget'
828+ path = 'eth0.conf'
829+ mock_ospath.exists.return_value = False
830+
831+ apply_net._maybe_remove_legacy_eth0(target, path)
832+
833+ # source file not found
834+ cfg = util.target_path(target, path)
835+ mock_ospath.exists.assert_called_with(cfg)
836+ mock_load.assert_not_called()
837+ mock_del.assert_not_called()
838
839=== modified file 'tests/unittests/test_curthooks.py'
840--- tests/unittests/test_curthooks.py 2017-05-12 14:35:53 +0000
841+++ tests/unittests/test_curthooks.py 2017-06-19 16:35:41 +0000
842@@ -577,4 +577,188 @@
843 with self.assertRaises(ValueError):
844 curthooks.handle_cloudconfig([], target="foobar")
845
846+
847+class TestDetectRequiredPackages(TestCase):
848+ test_config = {
849+ 'storage': {
850+ 1: {
851+ 'bcache': {
852+ 'type': 'bcache', 'name': 'bcache0', 'id': 'cache0',
853+ 'backing_device': 'sda3', 'cache_device': 'sdb'},
854+ 'lvm_partition': {
855+ 'id': 'lvol1', 'name': 'lv1', 'volgroup': 'vg1',
856+ 'type': 'lvm_partition'},
857+ 'lvm_volgroup': {
858+ 'id': 'vol1', 'name': 'vg1', 'devices': ['sda', 'sdb'],
859+ 'type': 'lvm_volgroup'},
860+ 'raid': {
861+ 'id': 'mddevice', 'name': 'md0', 'type': 'raid',
862+ 'raidlevel': 5, 'devices': ['sda1', 'sdb1', 'sdc1']},
863+ 'ext2': {
864+ 'id': 'format0', 'fstype': 'ext2', 'type': 'format'},
865+ 'ext3': {
866+ 'id': 'format1', 'fstype': 'ext3', 'type': 'format'},
867+ 'ext4': {
868+ 'id': 'format2', 'fstype': 'ext4', 'type': 'format'},
869+ 'btrfs': {
870+ 'id': 'format3', 'fstype': 'btrfs', 'type': 'format'},
871+ 'xfs': {
872+ 'id': 'format4', 'fstype': 'xfs', 'type': 'format'}}
873+ },
874+ 'network': {
875+ 1: {
876+ 'bond': {
877+ 'name': 'bond0', 'type': 'bond',
878+ 'bond_interfaces': ['interface0', 'interface1'],
879+ 'params': {'bond-mode': 'active-backup'},
880+ 'subnets': [
881+ {'type': 'static', 'address': '10.23.23.2/24'},
882+ {'type': 'static', 'address': '10.23.24.2/24'}]},
883+ 'vlan': {
884+ 'id': 'interface1.2667', 'mtu': 1500, 'name':
885+ 'interface1.2667', 'type': 'vlan', 'vlan_id': 2667,
886+ 'vlan_link': 'interface1',
887+ 'subnets': [{'address': '10.245.184.2/24',
888+ 'dns_nameservers': [], 'type': 'static'}]},
889+ 'bridge': {
890+ 'name': 'br0', 'bridge_interfaces': ['eth0', 'eth1'],
891+ 'type': 'bridge', 'params': {
892+ 'bridge_stp': 'off', 'bridge_fd': 0,
893+ 'bridge_maxwait': 0},
894+ 'subnets': [
895+ {'type': 'static', 'address': '192.168.14.2/24'},
896+ {'type': 'static', 'address': '2001:1::1/64'}]}},
897+ 2: {
898+ 'vlan': {
899+ 'vlans': {
900+ 'en-intra': {'id': 1, 'link': 'eno1', 'dhcp4': 'yes'},
901+ 'en-vpn': {'id': 2, 'link': 'eno1'}}},
902+ 'bridge': {
903+ 'bridges': {
904+ 'br0': {
905+ 'interfaces': ['wlp1s0', 'switchports'],
906+ 'dhcp4': True}}}}
907+ },
908+ }
909+
910+ def _fmt_config(self, config_items):
911+ res = {}
912+ for item, item_confs in config_items.items():
913+ version = item_confs['version']
914+ res[item] = {'version': version}
915+ if version == 1:
916+ res[item]['config'] = [self.test_config[item][version][i]
917+ for i in item_confs['items']]
918+ elif version == 2 and item == 'network':
919+ for cfg_item in item_confs['items']:
920+ res[item].update(self.test_config[item][version][cfg_item])
921+ else:
922+ raise NotImplementedError
923+ return res
924+
925+ def _test_req_mappings(self, req_mappings):
926+ for (config_items, expected_reqs) in req_mappings:
927+ cfg = self._fmt_config(config_items)
928+ actual_reqs = curthooks.detect_required_packages(cfg)
929+ self.assertEqual(set(actual_reqs), set(expected_reqs),
930+ 'failed for config: {}'.format(config_items))
931+
932+ def test_storage_v1_detect(self):
933+ self._test_req_mappings((
934+ ({'storage': {
935+ 'version': 1,
936+ 'items': ('lvm_partition', 'lvm_volgroup', 'btrfs', 'xfs')}},
937+ ('lvm2', 'xfsprogs', 'btrfs-tools')),
938+ ({'storage': {
939+ 'version': 1,
940+ 'items': ('raid', 'bcache', 'ext3', 'xfs')}},
941+ ('mdadm', 'bcache-tools', 'e2fsprogs', 'xfsprogs')),
942+ ({'storage': {
943+ 'version': 1,
944+ 'items': ('raid', 'lvm_volgroup', 'lvm_partition', 'ext3',
945+ 'ext4', 'btrfs')}},
946+ ('lvm2', 'mdadm', 'e2fsprogs', 'btrfs-tools')),
947+ ({'storage': {
948+ 'version': 1,
949+ 'items': ('bcache', 'lvm_volgroup', 'lvm_partition', 'ext2')}},
950+ ('bcache-tools', 'lvm2', 'e2fsprogs')),
951+ ))
952+
953+ def test_network_v1_detect(self):
954+ self._test_req_mappings((
955+ ({'network': {
956+ 'version': 1,
957+ 'items': ('bridge',)}},
958+ ('bridge-utils',)),
959+ ({'network': {
960+ 'version': 1,
961+ 'items': ('vlan', 'bond')}},
962+ ('vlan', 'ifenslave')),
963+ ({'network': {
964+ 'version': 1,
965+ 'items': ('bond', 'bridge')}},
966+ ('ifenslave', 'bridge-utils')),
967+ ({'network': {
968+ 'version': 1,
969+ 'items': ('vlan', 'bridge', 'bond')}},
970+ ('ifenslave', 'bridge-utils', 'vlan')),
971+ ))
972+
973+ def test_mixed_v1_detect(self):
974+ self._test_req_mappings((
975+ ({'storage': {
976+ 'version': 1,
977+ 'items': ('raid', 'bcache', 'ext4')},
978+ 'network': {
979+ 'version': 1,
980+ 'items': ('vlan',)}},
981+ ('mdadm', 'bcache-tools', 'e2fsprogs', 'vlan')),
982+ ({'storage': {
983+ 'version': 1,
984+ 'items': ('lvm_partition', 'lvm_volgroup', 'xfs')},
985+ 'network': {
986+ 'version': 1,
987+ 'items': ('bridge', 'bond')}},
988+ ('lvm2', 'xfsprogs', 'bridge-utils', 'ifenslave')),
989+ ({'storage': {
990+ 'version': 1,
991+ 'items': ('ext3', 'ext4', 'btrfs')},
992+ 'network': {
993+ 'version': 1,
994+ 'items': ('bond', 'vlan')}},
995+ ('e2fsprogs', 'btrfs-tools', 'vlan', 'ifenslave')),
996+ ))
997+
998+ def test_network_v2_detect(self):
999+ self._test_req_mappings((
1000+ ({'network': {
1001+ 'version': 2,
1002+ 'items': ('bridge',)}},
1003+ ('bridge-utils',)),
1004+ ({'network': {
1005+ 'version': 2,
1006+ 'items': ('vlan',)}},
1007+ ('vlan',)),
1008+ ({'network': {
1009+ 'version': 2,
1010+ 'items': ('vlan', 'bridge')}},
1011+ ('vlan', 'bridge-utils')),
1012+ ))
1013+
1014+ def test_mixed_storage_v1_network_v2_detect(self):
1015+ self._test_req_mappings((
1016+ ({'network': {
1017+ 'version': 2,
1018+ 'items': ('bridge', 'vlan')},
1019+ 'storage': {
1020+ 'version': 1,
1021+ 'items': ('raid', 'bcache', 'ext4')}},
1022+ ('vlan', 'bridge-utils', 'mdadm', 'bcache-tools', 'e2fsprogs')),
1023+ ))
1024+
1025+ def test_invalid_version_in_config(self):
1026+ with self.assertRaises(ValueError):
1027+ curthooks.detect_required_packages({'network': {'version': 3}})
1028+
1029+
1030 # vi: ts=4 expandtab syntax=python
1031
1032=== modified file 'tests/unittests/test_net.py'
1033--- tests/unittests/test_net.py 2017-02-09 16:58:23 +0000
1034+++ tests/unittests/test_net.py 2017-06-19 16:35:41 +0000
1035@@ -1,10 +1,11 @@
1036 from unittest import TestCase
1037+import mock
1038 import os
1039 import shutil
1040 import tempfile
1041 import yaml
1042
1043-from curtin import net
1044+from curtin import config, net, util
1045 import curtin.net.network_state as network_state
1046 from textwrap import dedent
1047
1048@@ -654,6 +655,141 @@
1049 self.assertEqual(sorted(ifaces.split('\n')),
1050 sorted(net_ifaces.split('\n')))
1051
1052+ @mock.patch('curtin.util.load_file')
1053+ @mock.patch('curtin.util.subp')
1054+ @mock.patch('curtin.util.which')
1055+ @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
1056+ def test_netconfig_passthrough_available(self, mock_which, mock_subp,
1057+ mock_load_file):
1058+ cloud_init = '/usr/bin/cloud-init'
1059+ python = '/usr/bin/python3'
1060+ mock_which.return_value = cloud_init
1061+ mock_load_file.return_value = "#! %s" % python
1062+ mock_subp.return_value = ('True', '')
1063+ feature = 'NETWORK_CONFIG_V2'
1064+ expected_cmd = (
1065+ "from cloudinit import version;"
1066+ "print('%s' in getattr(version, 'FEATURES', []))" % feature)
1067+
1068+ available = net.netconfig_passthrough_available(self.target)
1069+
1070+ self.assertEqual(True, available,
1071+ "netconfig passthrough was NOT available")
1072+ mock_which.assert_called_with('cloud-init', target=self.target)
1073+ mock_load_file.assert_called_with(self.target + cloud_init)
1074+ mock_subp.assert_called_with([python, '-c', expected_cmd],
1075+ capture=True, target=self.target)
1076+
1077+ @mock.patch('curtin.util.load_file')
1078+ @mock.patch('curtin.util.subp')
1079+ @mock.patch('curtin.util.which')
1080+ @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
1081+ def test_netconfig_passthrough_available_no_py3(self, mock_which,
1082+ mock_subp, mock_load_file):
1083+ cloud_init = '/usr/bin/cloud-init'
1084+ python = '/usr/bin/python'
1085+ mock_which.return_value = cloud_init
1086+ mock_load_file.return_value = "#! %s" % python
1087+ mock_subp.return_value = ('True', '')
1088+ feature = 'NETWORK_CONFIG_V2'
1089+ expected_cmd = (
1090+ "from cloudinit import version;"
1091+ "print('%s' in getattr(version, 'FEATURES', []))" % feature)
1092+
1093+ available = net.netconfig_passthrough_available(self.target)
1094+
1095+ self.assertEqual(True, available,
1096+ "netconfig passthrough was available")
1097+ mock_which.assert_called_with('cloud-init', target=self.target)
1098+ mock_load_file.assert_called_with(self.target + cloud_init)
1099+ mock_subp.assert_called_with([python, '-c', expected_cmd],
1100+ capture=True, target=self.target)
1101+
1102+ @mock.patch('curtin.net.LOG')
1103+ @mock.patch('curtin.util.subp')
1104+ @mock.patch('curtin.util.which')
1105+ @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
1106+ def test_netconfig_passthrough_available_no_cloudinit(self, mock_which,
1107+ mock_subp, mock_log):
1108+ mock_which.return_value = None
1109+
1110+ available = net.netconfig_passthrough_available(self.target)
1111+
1112+ self.assertEqual(False, available,
1113+ "netconfig passthrough was available")
1114+ self.assertTrue(mock_log.warning.called)
1115+ self.assertFalse(mock_subp.called)
1116+
1117+ @mock.patch('curtin.util.load_file')
1118+ @mock.patch('curtin.util.subp')
1119+ @mock.patch('curtin.util.which')
1120+ @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
1121+ def test_netconfig_passthrough_available_feature_not_found(self,
1122+ mock_which,
1123+ mock_subp,
1124+ mock_load_file):
1125+ cloud_init = '/usr/bin/cloud-init'
1126+ python = '/usr/bin/python3'
1127+ mock_which.return_value = cloud_init
1128+ mock_load_file.return_value = "#! %s" % python
1129+ mock_subp.return_value = ('False', '')
1130+ feature = 'NETWORK_CONFIG_V2'
1131+ expected_cmd = (
1132+ "from cloudinit import version;"
1133+ "print('%s' in getattr(version, 'FEATURES', []))" % feature)
1134+
1135+ available = net.netconfig_passthrough_available(self.target)
1136+
1137+ self.assertEqual(False, available,
1138+ "netconfig passthrough was available")
1139+ mock_which.assert_called_with('cloud-init', target=self.target)
1140+ mock_load_file.assert_called_with(self.target + cloud_init)
1141+ mock_subp.assert_called_with([python, '-c', expected_cmd],
1142+ capture=True, target=self.target)
1143+
1144+ @mock.patch('curtin.util.load_file')
1145+ @mock.patch('curtin.net.LOG')
1146+ @mock.patch('curtin.util.subp')
1147+ @mock.patch('curtin.util.which')
1148+ @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
1149+ def test_netconfig_passthrough_available_exc(self, mock_which, mock_subp,
1150+ mock_log, mock_load_file):
1151+ cloud_init = '/usr/bin/cloud-init'
1152+ python = '/usr/bin/python3'
1153+ mock_which.return_value = cloud_init
1154+ mock_load_file.return_value = "#! %s" % python
1155+ mock_subp.side_effect = util.ProcessExecutionError
1156+ feature = 'NETWORK_CONFIG_V2'
1157+ expected_cmd = (
1158+ "from cloudinit import version;"
1159+ "print('%s' in getattr(version, 'FEATURES', []))" % feature)
1160+
1161+ available = net.netconfig_passthrough_available(self.target)
1162+
1163+ self.assertEqual(False, available,
1164+ "netconfig passthrough was available")
1165+ mock_which.assert_called_with('cloud-init', target=self.target)
1166+ mock_subp.assert_called_with([python, '-c', expected_cmd],
1167+ capture=True, target=self.target)
1168+ self.assertTrue(mock_log.exception.called)
1169+
1170+ @mock.patch('curtin.util.write_file')
1171+ def test_render_netconfig_passthrough(self, mock_writefile):
1172+ netcfg = yaml.safe_load(self.config)
1173+ pt_config = 'etc/cloud/cloud.cfg.d/curtin-networking.cfg'
1174+ target_config = os.path.sep.join((self.target, pt_config),)
1175+
1176+ net.render_netconfig_passthrough(self.target, netconfig=netcfg)
1177+
1178+ content = config.dump_config(netcfg)
1179+ mock_writefile.assert_called_with(target_config, content=content)
1180+
1181+ def test_render_netconfig_passthrough_nonetcfg(self):
1182+ netcfg = None
1183+ self.assertRaises(ValueError,
1184+ net.render_netconfig_passthrough,
1185+ self.target, netconfig=netcfg)
1186+
1187 def test_routes_rendered(self):
1188 # as reported in bug 1649652
1189 conf = [
1190
1191=== modified file 'tests/vmtests/test_network.py'
1192--- tests/vmtests/test_network.py 2017-05-19 21:40:48 +0000
1193+++ tests/vmtests/test_network.py 2017-06-19 16:35:41 +0000
1194@@ -1,7 +1,12 @@
1195 from . import VMBaseClass, logger, helpers
1196 from .releases import base_vm_classes as relbase
1197
1198+from unittest import SkipTest
1199+from curtin import config
1200+
1201+import glob
1202 import ipaddress
1203+import os
1204 import re
1205 import textwrap
1206 import yaml
1207@@ -14,26 +19,41 @@
1208 collect_scripts = [textwrap.dedent("""
1209 cd OUTPUT_COLLECT_D
1210 echo "waiting for ipv6 to settle" && sleep 5
1211- ifconfig -a > ifconfig_a
1212- ip link show > ip_link_show
1213- ip a > ip_a
1214+ route -n | tee first_route_n
1215+ ifconfig -a | tee ifconfig_a
1216+ ip link show | tee ip_link_show
1217+ ip a | tee ip_a
1218 find /etc/network/interfaces.d > find_interfacesd
1219 cp -av /etc/network/interfaces .
1220 cp -av /etc/network/interfaces.d .
1221 cp /etc/resolv.conf .
1222- cp -av /etc/udev/rules.d/70-persistent-net.rules .
1223- ip -o route show > ip_route_show
1224- ip -6 -o route show > ip_6_route_show
1225- route -n > route_n
1226- route -6 -n > route_6_n
1227+ cp -av /etc/udev/rules.d/70-persistent-net.rules . ||:
1228+ ip -o route show | tee ip_route_show
1229+ ip -6 -o route show | tee ip_6_route_show
1230+ route -n |tee route_n
1231+ route -6 -n |tee route_6_n
1232 cp -av /run/network ./run_network
1233 cp -av /var/log/upstart ./upstart ||:
1234- sleep 10 && ip a > ip_a
1235+ cp -av /etc/cloud ./etc_cloud
1236+ cp -av /var/log/cloud*.log ./
1237+ dpkg-query -W -f '${Version}' cloud-init |tee dpkg_cloud-init_version
1238+ dpkg-query -W -f '${Version}' nplan |tee dpkg_nplan_version
1239+ dpkg-query -W -f '${Version}' systemd |tee dpkg_systemd_version
1240+ V=/usr/lib/python*/dist-packages/cloudinit/version.py;
1241+ grep -c NETWORK_CONFIG_V2 $V > cloudinit_passthrough_available
1242+ mkdir -p etc_netplan
1243+ cp -av /etc/netplan/* ./etc_netplan/ ||:
1244+ networkctl |tee networkctl
1245+ mkdir -p run_systemd_network
1246+ cp -a /run/systemd/network/* ./run_systemd_network/ ||:
1247+ cp -a /run/systemd/netif ./run_systemd_netif ||:
1248+ cp -a /run/systemd/resolve ./run_systemd_resolve ||:
1249+ cp -a /etc/systemd ./etc_systemd ||:
1250+ journalctl --no-pager -b -x > journalctl_out
1251 """)]
1252
1253 def test_output_files_exist(self):
1254 self.output_files_exist([
1255- "70-persistent-net.rules",
1256 "find_interfacesd",
1257 "ifconfig_a",
1258 "interfaces",
1259@@ -44,17 +64,109 @@
1260 "route_n",
1261 ])
1262
1263- def test_etc_network_interfaces(self):
1264+ def read_eni(self):
1265+ eni = ""
1266+ eni_cfg = ""
1267+
1268 eni = self.load_collect_file("interfaces")
1269 logger.debug('etc/network/interfaces:\n{}'.format(eni))
1270
1271+ # we don't use collect_path as we're building a glob
1272+ eni_dir = os.path.join(self.td.collect, "interfaces.d", "*.cfg")
1273+ for cfg in glob.glob(eni_dir):
1274+ with open(cfg) as fp:
1275+ eni_cfg += fp.read()
1276+
1277+ return (eni, eni_cfg)
1278+
1279+ def _network_renderer(self):
1280+ """ Determine if target uses eni/ifupdown or netplan/networkd """
1281+
1282+ etc_netplan = self.collect_path('etc_netplan')
1283+ networkd = self.collect_path('run_systemd_network')
1284+
1285+ if len(os.listdir(etc_netplan)) > 0 and len(os.listdir(networkd)) > 0:
1286+ print('Network Renderer: systemd-networkd')
1287+ return 'systemd-networkd'
1288+
1289+ print('Network Renderer: ifupdown')
1290+ return 'ifupdown'
1291+
1292+ def test_etc_network_interfaces(self):
1293+ if self._network_renderer() != "ifupdown":
1294+ reason = ("{}: using net-passthrough; "
1295+ "deferring to cloud-init".format(self.__class__))
1296+ raise SkipTest(reason)
1297+
1298+ eni, eni_cfg = self.read_eni()
1299 expected_eni = self.get_expected_etc_network_interfaces()
1300- eni_lines = eni.split('\n')
1301- for line in expected_eni.split('\n'):
1302- self.assertTrue(line in eni_lines, msg="missing line: %s" % line)
1303+
1304+ eni_lines = eni.split('\n') + eni_cfg.split('\n')
1305+ print("\n".join(eni_lines))
1306+ for line in [l for l in expected_eni.split('\n') if len(l) > 0]:
1307+ if line.startswith("#"):
1308+ continue
1309+ if "hwaddress ether" in line:
1310+ continue
1311+ print('expected line:\n%s' % line)
1312+ self.assertTrue(line in eni_lines, "not in eni: %s" % line)
1313+
1314+ def test_cloudinit_network_passthrough(self):
1315+ cc_passthrough = "cloud.cfg.d/curtin-networking.cfg"
1316+
1317+ avail_str = self.load_collect_file('cloudinit_passthrough_available')
1318+ available = int(avail_str) == 1
1319+ print('avail_str=%s available=%s' % (avail_str, available))
1320+
1321+ if not available:
1322+ raise SkipTest('not available on %s' % self.__class__)
1323+
1324+ print('passthrough was available')
1325+ pt_file = os.path.join(self.td.collect, 'etc_cloud',
1326+ cc_passthrough)
1327+ print('checking if passthrough file written: %s' % pt_file)
1328+ self.assertTrue(os.path.exists(pt_file))
1329+
1330+ # compare
1331+ original = {'network':
1332+ config.load_config(self.conf_file).get('network')}
1333+ intarget = config.load_config(pt_file)
1334+ self.assertEqual(original, intarget)
1335+
1336+ def test_cloudinit_network_disabled(self):
1337+ cc_disabled = 'cloud.cfg.d/curtin-disable-cloudinit-networking.cfg'
1338+
1339+ avail_str = self.load_collect_file('cloudinit_passthrough_available')
1340+ available = int(avail_str) == 1
1341+ print('avail_str=%s available=%s' % (avail_str, available))
1342+
1343+ if available:
1344+ raise SkipTest('passthrough available on %s' % self.__class__)
1345+
1346+ print('passthrough not available')
1347+ cc_disable_file = os.path.join(self.td.collect, 'etc_cloud',
1348+ cc_disabled)
1349+ print('checking if network:disable file written: %s' %
1350+ cc_disable_file)
1351+ self.assertTrue(os.path.exists(cc_disable_file))
1352+
1353+ # compare
1354+ original = {'network': {'config': 'disabled'}}
1355+ intarget = config.load_config(cc_disable_file)
1356+
1357+ print('checking cloud-init network-cfg content')
1358+ self.assertEqual(original, intarget)
1359
1360 def test_etc_resolvconf(self):
1361- resolvconf = self.load_collect_file("resolv.conf")
1362+ render2resolvconf = {
1363+ 'ifupdown': "resolv.conf",
1364+ 'systemd-networkd': "run_systemd_resolve/resolv.conf"
1365+ }
1366+ resolvconfpath = render2resolvconf.get(self._network_renderer(), None)
1367+ self.assertTrue(resolvconfpath is not None)
1368+ logger.debug('Selected path to resolvconf: %s', resolvconfpath)
1369+
1370+ resolvconf = self.load_collect_file(resolvconfpath)
1371 logger.debug('etc/resolv.conf:\n{}'.format(resolvconf))
1372
1373 resolv_lines = resolvconf.split('\n')
1374@@ -111,12 +223,21 @@
1375 def test_static_routes(self):
1376 '''check routing table'''
1377 network_state = self.get_network_state()
1378+
1379+ # if we're using passthrough then we can't load state
1380+ cc_passthrough = "cloud.cfg.d/curtin-networking.cfg"
1381+ pt_file = os.path.join(self.td.collect, 'etc_cloud', cc_passthrough)
1382+ print('checking if passthrough file written: %s' % pt_file)
1383+ if not network_state and os.path.exists(pt_file):
1384+ raise SkipTest('passthrough enabled, skipping %s' % self.__class__)
1385+
1386 ip_route_show = self.load_collect_file("ip_route_show")
1387 route_n = self.load_collect_file("route_n")
1388
1389 print("ip route show:\n%s" % ip_route_show)
1390 print("route -n:\n%s" % route_n)
1391- routes = network_state.get('routes')
1392+ routes = network_state.get('routes', [])
1393+ print("found routes: [%s]" % routes)
1394 for route in routes:
1395 print('Checking static route: %s' % route)
1396 destnet = (
1397@@ -144,6 +265,12 @@
1398 print('parsed ip_a dict:\n{}'.format(
1399 yaml.dump(ip_dict, default_flow_style=False, indent=4)))
1400
1401+ route_n = self.load_collect_file("route_n")
1402+ logger.debug("route -n:\n{}".format(route_n))
1403+
1404+ route_6_n = self.load_collect_file("route_6_n")
1405+ logger.debug("route -6 -n:\n{}".format(route_6_n))
1406+
1407 ip_route_show = self.load_collect_file("ip_route_show")
1408 logger.debug("ip route show:\n{}".format(ip_route_show))
1409 for line in [line for line in ip_route_show.split('\n')
1410@@ -156,12 +283,6 @@
1411 route_info = m.groupdict('')
1412 logger.debug(route_info)
1413
1414- route_n = self.load_collect_file("route_n")
1415- logger.debug("route -n:\n{}".format(route_n))
1416-
1417- route_6_n = self.load_collect_file("route_6_n")
1418- logger.debug("route -6 -n:\n{}".format(route_6_n))
1419-
1420 routes = {
1421 '4': route_n,
1422 '6': route_6_n,
1423@@ -170,9 +291,10 @@
1424 for iface in interfaces.values():
1425 print("\nnetwork_state iface: %s" % (
1426 yaml.dump(iface, default_flow_style=False, indent=4)))
1427+ ipcfg = ip_dict.get(iface['name'], {})
1428 self.check_interface(iface['name'],
1429 iface,
1430- ip_dict.get(iface['name']),
1431+ ipcfg,
1432 routes)
1433
1434 def check_interface(self, ifname, iface, ipcfg, routes):
1435@@ -182,8 +304,9 @@
1436 # FIXME: remove check?
1437 # initial check, do we have the correct iface ?
1438 print('ifname={}'.format(ifname))
1439+ self.assertTrue(type(ipcfg) == dict, "%s is not dict" % (ipcfg))
1440+ print("ipcfg['interface']={}".format(ipcfg['interface']))
1441 self.assertEqual(ifname, ipcfg['interface'])
1442- print("ipcfg['interface']={}".format(ipcfg['interface']))
1443
1444 # check physical interface attributes (skip bond members, macs change)
1445 if iface['type'] in ['physical'] and 'bond-master' not in iface:
1446
1447=== modified file 'tests/vmtests/test_network_alias.py'
1448--- tests/vmtests/test_network_alias.py 2017-04-26 16:14:04 +0000
1449+++ tests/vmtests/test_network_alias.py 2017-06-19 16:35:41 +0000
1450@@ -1,5 +1,6 @@
1451 from .releases import base_vm_classes as relbase
1452 from .test_network import TestNetworkBaseTestsAbs
1453+from unittest import SkipTest
1454
1455
1456 class TestNetworkAliasAbs(TestNetworkBaseTestsAbs):
1457@@ -7,6 +8,11 @@
1458 """
1459 conf_file = "examples/tests/network_alias.yaml"
1460
1461+ def test_etc_network_interfaces(self):
1462+ reason = ("%s: cloud-init and curtin eni rendering"
1463+ " differ" % (self.__class__))
1464+ raise SkipTest(reason)
1465+
1466
1467 class PreciseHWETTestNetworkAlias(relbase.precise_hwe_t, TestNetworkAliasAbs):
1468 # FIXME: off due to hang at test: Starting execute cloud user/final scripts
1469
1470=== modified file 'tests/vmtests/test_network_bridging.py'
1471--- tests/vmtests/test_network_bridging.py 2017-05-19 21:40:48 +0000
1472+++ tests/vmtests/test_network_bridging.py 2017-06-19 16:35:41 +0000
1473@@ -111,7 +111,6 @@
1474 self.assertEqual('install ok installed', status)
1475
1476 def test_bridge_params(self):
1477- """ Test if configure bridge params match values on the device """
1478
1479 def _load_sysfs_bridge_data():
1480 sysfs_br0 = sysfs_to_dict(self.collect_path("sysfs_br0"))
1481@@ -138,6 +137,7 @@
1482 p not in bridge_params_uncheckable)]
1483
1484 def _check_bridge_param(sysfs_vals, p, br):
1485+ print('Checking bridge %s param %s' % (br, p))
1486 value = br.get(param)
1487 if param in ['bridge_stp']:
1488 if value in ['off', '0']:
1489@@ -146,25 +146,35 @@
1490 value = 1
1491 else:
1492 print('bridge_stp not in known val list')
1493+ elif param in ['bridge_portprio']:
1494+ if self._network_renderer() == "systemd-networkd":
1495+ reason = ("%s: skip until lp#1668347"
1496+ " is fixed" % self.__class__)
1497+ logger.warn('Skipping: %s', reason)
1498+ print(reason)
1499+ return
1500
1501 print('key=%s value=%s' % (param, value))
1502 if type(value) == list:
1503 for subval in value:
1504 (port, pval) = subval.split(" ")
1505- print('key=%s port=%s pval=%s' % (param, port, pval))
1506+ print('param=%s port=%s pval=%s' % (param, port, pval))
1507 sys_file_val = _get_sysfs_value(sysfs_vals, br0['name'],
1508 param, port)
1509
1510- self.assertEqual(int(pval), int(sys_file_val))
1511+ msg = "Source cfg: %s=%s on port %s" % (param, value, port)
1512+ self.assertEqual(int(pval), int(sys_file_val), msg)
1513 else:
1514 sys_file_val = _get_sysfs_value(sysfs_vals, br0['name'],
1515 param, port=None)
1516- self.assertEqual(int(value), int(sys_file_val))
1517+ self.assertEqual(int(value), int(sys_file_val),
1518+ "Source cfg: %s=%s" % (param, value))
1519
1520 sysfs_vals = _load_sysfs_bridge_data()
1521- print(sysfs_vals)
1522+ # print(sysfs_vals)
1523 br0 = _get_bridge_config()
1524 for param in _get_bridge_params(br0):
1525+ print('Checking param %s' % param)
1526 _check_bridge_param(sysfs_vals, param, br0)
1527
1528
1529
1530=== modified file 'tests/vmtests/test_network_ipv6_enisource.py'
1531--- tests/vmtests/test_network_ipv6_enisource.py 2017-04-26 16:14:04 +0000
1532+++ tests/vmtests/test_network_ipv6_enisource.py 2017-06-19 16:35:41 +0000
1533@@ -1,10 +1,16 @@
1534 from .releases import base_vm_classes as relbase
1535 from .test_network_enisource import TestNetworkENISource
1536
1537+import unittest
1538+
1539
1540 class TestNetworkIPV6ENISource(TestNetworkENISource):
1541 conf_file = "examples/tests/network_source_ipv6.yaml"
1542
1543+ @unittest.skip("FIXME: cloud-init.net needs update")
1544+ def test_etc_network_interfaces(self):
1545+ pass
1546+
1547
1548 class PreciseTestNetworkIPV6ENISource(relbase.precise,
1549 TestNetworkIPV6ENISource):

Subscribers

People subscribed via source and target branches