Merge lp:~raharper/curtin/new-artful-upload into lp:~curtin-dev/curtin/artful
- new-artful-upload
- Merge into artful
Proposed by
Scott Moser
Status: | Merged |
---|---|
Merged at revision: | 81 |
Proposed branch: | lp:~raharper/curtin/new-artful-upload |
Merge into: | lp:~curtin-dev/curtin/artful |
Diff against target: |
7045 lines (+3340/-962) 91 files modified
curtin/__init__.py (+2/-0) curtin/block/__init__.py (+69/-20) curtin/block/iscsi.py (+44/-3) curtin/block/mdadm.py (+10/-6) curtin/commands/apply_net.py (+34/-8) curtin/commands/apt_config.py (+0/-9) curtin/commands/curthooks.py (+197/-94) curtin/commands/extract.py (+6/-0) curtin/commands/install.py (+44/-4) curtin/futil.py (+24/-1) curtin/net/__init__.py (+106/-0) curtin/reporter/handlers.py (+42/-0) curtin/util.py (+137/-13) debian/changelog (+33/-0) doc/index.rst (+1/-0) doc/topics/apt_source.rst (+9/-6) doc/topics/config.rst (+18/-0) doc/topics/curthooks.rst (+109/-0) doc/topics/integration-testing.rst (+6/-0) doc/topics/networking.rst (+2/-0) doc/topics/overview.rst (+45/-47) doc/topics/reporting.rst (+29/-0) doc/topics/storage.rst (+2/-0) examples/network-ipv6-bond-vlan.yaml (+2/-2) examples/tests/bonding_network.yaml (+1/-4) examples/tests/centos_basic.yaml (+2/-1) examples/tests/centos_defaults.yaml (+91/-0) examples/tests/journald_reporter.yaml (+20/-0) examples/tests/network_alias.yaml (+29/-31) examples/tests/network_static_routes.yaml (+10/-15) examples/tests/network_v2_passthrough.yaml (+8/-0) setup.py (+16/-2) tests/unittests/helpers.py (+36/-0) tests/unittests/test_apt_custom_sources_list.py (+3/-6) tests/unittests/test_apt_source.py (+4/-7) tests/unittests/test_basic.py (+4/-4) tests/unittests/test_block.py (+20/-36) tests/unittests/test_block_iscsi.py (+187/-18) tests/unittests/test_block_lvm.py (+2/-2) tests/unittests/test_block_mdadm.py (+10/-22) tests/unittests/test_block_mkfs.py (+2/-2) tests/unittests/test_clear_holders.py (+5/-5) tests/unittests/test_commands_apply_net.py (+334/-0) tests/unittests/test_commands_block_meta.py (+6/-19) tests/unittests/test_commands_install.py (+22/-0) tests/unittests/test_config.py (+6/-6) tests/unittests/test_curthooks.py (+241/-57) tests/unittests/test_feature.py (+5/-2) tests/unittests/test_gpg.py (+4/-4) tests/unittests/test_make_dname.py (+4/-4) tests/unittests/test_net.py (+99/-24) tests/unittests/test_partitioning.py (+4/-3) tests/unittests/test_public.py (+54/-0) tests/unittests/test_reporter.py (+29/-38) tests/unittests/test_util.py (+201/-52) tests/unittests/test_version.py (+7/-19) tests/vmtests/__init__.py (+59/-7) tests/vmtests/releases.py (+0/-15) tests/vmtests/test_apt_config_cmd.py (+0/-4) tests/vmtests/test_basic.py (+0/-13) tests/vmtests/test_bcache_basic.py (+0/-4) tests/vmtests/test_centos_basic.py (+35/-0) tests/vmtests/test_iscsi.py (+0/-4) tests/vmtests/test_journald_reporter.py (+52/-0) tests/vmtests/test_lvm.py (+0/-9) tests/vmtests/test_lvm_iscsi.py (+4/-4) tests/vmtests/test_mdadm_bcache.py (+3/-59) tests/vmtests/test_mdadm_iscsi.py (+4/-4) tests/vmtests/test_multipath.py (+0/-4) tests/vmtests/test_network.py (+202/-39) tests/vmtests/test_network_alias.py (+33/-4) tests/vmtests/test_network_bonding.py (+47/-22) tests/vmtests/test_network_bridging.py (+77/-17) tests/vmtests/test_network_enisource.py (+2/-8) tests/vmtests/test_network_ipv6.py (+29/-4) tests/vmtests/test_network_ipv6_enisource.py (+8/-6) tests/vmtests/test_network_ipv6_static.py (+17/-5) tests/vmtests/test_network_ipv6_vlan.py (+17/-5) tests/vmtests/test_network_mtu.py (+61/-8) tests/vmtests/test_network_static.py (+30/-4) tests/vmtests/test_network_static_routes.py (+19/-6) tests/vmtests/test_network_vlan.py (+40/-15) tests/vmtests/test_nvme.py (+0/-9) tests/vmtests/test_raid5_bcache.py (+0/-9) tests/vmtests/test_simple.py (+0/-4) tests/vmtests/test_uefi_basic.py (+0/-19) tools/build-deb (+3/-1) tools/curtainer (+14/-8) tools/find-tgt (+54/-29) tools/jenkins-runner (+47/-10) tools/launch (+46/-7) |
To merge this branch: | bzr merge lp:~raharper/curtin/new-artful-upload |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Scott Moser (community) | Approve | ||
Review via email: mp+331913@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'curtin/__init__.py' |
2 | --- curtin/__init__.py 2017-03-23 17:05:17 +0000 |
3 | +++ curtin/__init__.py 2017-10-06 01:09:48 +0000 |
4 | @@ -23,6 +23,8 @@ |
5 | # can determine which features are supported. Each entry should have |
6 | # a consistent meaning. |
7 | FEATURES = [ |
8 | + # curtin can apply centos networking via centos_apply_network_config |
9 | + 'CENTOS_APPLY_NETWORK_CONFIG', |
10 | # install supports the 'network' config version 1 |
11 | 'NETWORK_CONFIG_V1', |
12 | # reporter supports 'webhook' type |
13 | |
14 | === modified file 'curtin/block/__init__.py' |
15 | --- curtin/block/__init__.py 2017-06-12 19:43:55 +0000 |
16 | +++ curtin/block/__init__.py 2017-10-06 01:09:48 +0000 |
17 | @@ -19,7 +19,6 @@ |
18 | import errno |
19 | import itertools |
20 | import os |
21 | -import shlex |
22 | import stat |
23 | import sys |
24 | import tempfile |
25 | @@ -204,30 +203,13 @@ |
26 | return [path_to_kname(device)] |
27 | |
28 | |
29 | -def _shlex_split(str_in): |
30 | - # shlex.split takes a string |
31 | - # but in python2 if input here is a unicode, encode it to a string. |
32 | - # http://stackoverflow.com/questions/2365411/ |
33 | - # python-convert-unicode-to-ascii-without-errors |
34 | - if sys.version_info.major == 2: |
35 | - try: |
36 | - if isinstance(str_in, unicode): |
37 | - str_in = str_in.encode('utf-8') |
38 | - except NameError: |
39 | - pass |
40 | - |
41 | - return shlex.split(str_in) |
42 | - else: |
43 | - return shlex.split(str_in) |
44 | - |
45 | - |
46 | def _lsblock_pairs_to_dict(lines): |
47 | """ |
48 | parse lsblock output and convert to dict |
49 | """ |
50 | ret = {} |
51 | for line in lines.splitlines(): |
52 | - toks = _shlex_split(line) |
53 | + toks = util.shlex_split(line) |
54 | cur = {} |
55 | for tok in toks: |
56 | k, v = tok.split("=", 1) |
57 | @@ -468,7 +450,7 @@ |
58 | for line in out.splitlines(): |
59 | curdev, curdata = line.split(":", 1) |
60 | data[curdev] = dict(tok.split('=', 1) |
61 | - for tok in _shlex_split(curdata)) |
62 | + for tok in util.shlex_split(curdata)) |
63 | return data |
64 | |
65 | |
66 | @@ -978,4 +960,71 @@ |
67 | else: |
68 | raise ValueError("wipe mode %s not supported" % mode) |
69 | |
70 | + |
71 | +def storage_config_required_packages(storage_config, mapping): |
72 | + """Read storage configuration dictionary and determine |
73 | + which packages are required for the supplied configuration |
74 | + to function. Return a list of packaged to install. |
75 | + """ |
76 | + |
77 | + if not storage_config or not isinstance(storage_config, dict): |
78 | + raise ValueError('Invalid storage configuration. ' |
79 | + 'Must be a dict:\n %s' % storage_config) |
80 | + |
81 | + if not mapping or not isinstance(mapping, dict): |
82 | + raise ValueError('Invalid storage mapping. Must be a dict') |
83 | + |
84 | + if 'storage' in storage_config: |
85 | + storage_config = storage_config.get('storage') |
86 | + |
87 | + needed_packages = [] |
88 | + |
89 | + # get reqs by device operation type |
90 | + dev_configs = set(operation['type'] |
91 | + for operation in storage_config['config']) |
92 | + |
93 | + for dev_type in dev_configs: |
94 | + if dev_type in mapping: |
95 | + needed_packages.extend(mapping[dev_type]) |
96 | + |
97 | + # for any format operations, check the fstype and |
98 | + # determine if we need any mkfs tools as well. |
99 | + format_configs = set([operation['fstype'] |
100 | + for operation in storage_config['config'] |
101 | + if operation['type'] == 'format']) |
102 | + for format_type in format_configs: |
103 | + if format_type in mapping: |
104 | + needed_packages.extend(mapping[format_type]) |
105 | + |
106 | + return needed_packages |
107 | + |
108 | + |
109 | +def detect_required_packages_mapping(): |
110 | + """Return a dictionary providing a versioned configuration which maps |
111 | + storage configuration elements to the packages which are required |
112 | + for functionality. |
113 | + |
114 | + The mapping key is either a config type value, or an fstype value. |
115 | + |
116 | + """ |
117 | + version = 1 |
118 | + mapping = { |
119 | + version: { |
120 | + 'handler': storage_config_required_packages, |
121 | + 'mapping': { |
122 | + 'bcache': ['bcache-tools'], |
123 | + 'btrfs': ['btrfs-tools'], |
124 | + 'ext2': ['e2fsprogs'], |
125 | + 'ext3': ['e2fsprogs'], |
126 | + 'ext4': ['e2fsprogs'], |
127 | + 'lvm_partition': ['lvm2'], |
128 | + 'lvm_volgroup': ['lvm2'], |
129 | + 'raid': ['mdadm'], |
130 | + 'xfs': ['xfsprogs'] |
131 | + }, |
132 | + }, |
133 | + } |
134 | + return mapping |
135 | + |
136 | + |
137 | # vi: ts=4 expandtab syntax=python |
138 | |
139 | === modified file 'curtin/block/iscsi.py' |
140 | --- curtin/block/iscsi.py 2017-05-19 20:56:27 +0000 |
141 | +++ curtin/block/iscsi.py 2017-10-06 01:09:48 +0000 |
142 | @@ -195,6 +195,15 @@ |
143 | return target_nodes_location |
144 | |
145 | |
146 | +def restart_iscsi_service(): |
147 | + LOG.info('restarting iscsi service') |
148 | + if util.uses_systemd(): |
149 | + cmd = ['systemctl', 'reload-or-restart', 'open-iscsi'] |
150 | + else: |
151 | + cmd = ['service', 'open-iscsi', 'restart'] |
152 | + util.subp(cmd, capture=True) |
153 | + |
154 | + |
155 | def save_iscsi_config(iscsi_disk): |
156 | state = util.load_command_environment() |
157 | # A nodes directory will be created in the same directory as the |
158 | @@ -238,11 +247,35 @@ |
159 | return _ISCSI_DISKS |
160 | |
161 | |
162 | +def get_iscsi_disks_from_config(cfg): |
163 | + """Parse a curtin storage config and return a list |
164 | + of iscsi disk objects for each configuration present |
165 | + """ |
166 | + if not cfg: |
167 | + cfg = {} |
168 | + |
169 | + sconfig = cfg.get('storage', {}).get('config', {}) |
170 | + if not sconfig: |
171 | + LOG.warning('Configuration dictionary did not contain' |
172 | + ' a storage configuration') |
173 | + return [] |
174 | + |
175 | + # Construct IscsiDisk objects for each iscsi volume present |
176 | + iscsi_disks = [IscsiDisk(disk['path']) for disk in sconfig |
177 | + if disk['type'] == 'disk' and |
178 | + disk.get('path', "").startswith('iscsi:')] |
179 | + LOG.debug('Found %s iscsi disks in storage config', len(iscsi_disks)) |
180 | + return iscsi_disks |
181 | + |
182 | + |
183 | def disconnect_target_disks(target_root_path=None): |
184 | target_nodes_path = util.target_path(target_root_path, '/etc/iscsi/nodes') |
185 | fails = [] |
186 | if os.path.isdir(target_nodes_path): |
187 | for target in os.listdir(target_nodes_path): |
188 | + if target not in iscsiadm_sessions(): |
189 | + LOG.debug('iscsi target %s not active, skipping', target) |
190 | + continue |
191 | # conn is "host,port,lun" |
192 | for conn in os.listdir( |
193 | os.path.sep.join([target_nodes_path, target])): |
194 | @@ -254,7 +287,9 @@ |
195 | fails.append(target) |
196 | LOG.warn("Unable to logout of iSCSI target %s: %s", |
197 | target, e) |
198 | - |
199 | + else: |
200 | + LOG.warning('Skipping disconnect: failed to find iscsi nodes path: %s', |
201 | + target_nodes_path) |
202 | if fails: |
203 | raise RuntimeError( |
204 | "Unable to logout of iSCSI targets: %s" % ', '.join(fails)) |
205 | @@ -414,9 +449,15 @@ |
206 | |
207 | def disconnect(self): |
208 | if self.target not in iscsiadm_sessions(): |
209 | + LOG.warning('Iscsi target %s not in active iscsi sessions', |
210 | + self.target) |
211 | return |
212 | |
213 | - util.subp(['sync']) |
214 | - iscsiadm_logout(self.target, self.portal) |
215 | + try: |
216 | + util.subp(['sync']) |
217 | + iscsiadm_logout(self.target, self.portal) |
218 | + except util.ProcessExecutionError as e: |
219 | + LOG.warn("Unable to logout of iSCSI target %s from portal %s: %s", |
220 | + self.target, self.portal, e) |
221 | |
222 | # vi: ts=4 expandtab syntax=python |
223 | |
224 | === modified file 'curtin/block/mdadm.py' |
225 | --- curtin/block/mdadm.py 2017-05-19 20:56:27 +0000 |
226 | +++ curtin/block/mdadm.py 2017-10-06 01:09:48 +0000 |
227 | @@ -273,7 +273,11 @@ |
228 | LOG.debug('%s/sync_max = %s', sync_action, val) |
229 | if val != "idle": |
230 | LOG.debug("mdadm: setting array sync_action=idle") |
231 | - util.write_file(sync_action, content="idle") |
232 | + try: |
233 | + util.write_file(sync_action, content="idle") |
234 | + except (IOError, OSError) as e: |
235 | + LOG.debug("mdadm: (non-fatal) write to %s failed %s", |
236 | + sync_action, e) |
237 | |
238 | # Setting the sync_{max,min} may can help prevent the array from |
239 | # changing back to 'resync' which may prevent the array from being |
240 | @@ -283,11 +287,11 @@ |
241 | if val != "0": |
242 | LOG.debug("mdadm: setting array sync_{min,max}=0") |
243 | try: |
244 | - util.write_file(sync_max, content="0") |
245 | - util.write_file(sync_min, content="0") |
246 | - except IOError: |
247 | - LOG.warning('mdadm: failed to set sync_{max,min} values') |
248 | - pass |
249 | + for sync_file in [sync_max, sync_min]: |
250 | + util.write_file(sync_file, content="0") |
251 | + except (IOError, OSError) as e: |
252 | + LOG.debug('mdadm: (non-fatal) write to %s failed %s', |
253 | + sync_file, e) |
254 | |
255 | # one wonders why this command doesn't do any of the above itself? |
256 | out, err = util.subp(["mdadm", "--manage", "--stop", devpath], |
257 | |
258 | === modified file 'curtin/commands/apply_net.py' |
259 | --- curtin/commands/apply_net.py 2017-02-08 20:25:39 +0000 |
260 | +++ curtin/commands/apply_net.py 2017-10-06 01:09:48 +0000 |
261 | @@ -21,6 +21,7 @@ |
262 | from .. import log |
263 | import curtin.net as net |
264 | import curtin.util as util |
265 | +from curtin import config |
266 | from . import populate_one_subcmd |
267 | |
268 | |
269 | @@ -89,15 +90,38 @@ |
270 | sys.stderr.write(msg + "\n") |
271 | raise Exception(msg) |
272 | |
273 | + passthrough = False |
274 | if network_state: |
275 | + # NB: we cannot support passthrough until curtin can convert from |
276 | + # network_state to network-config yaml |
277 | ns = net.network_state.from_state_file(network_state) |
278 | + raise ValueError('Not Supported; curtin lacks a network_state to ' |
279 | + 'network_config converter.') |
280 | elif network_config: |
281 | - ns = net.parse_net_config(network_config) |
282 | - |
283 | - net.render_network_state(target=target, network_state=ns) |
284 | + netcfg = config.load_config(network_config) |
285 | + |
286 | + # curtin will pass-through the netconfig into the target |
287 | + # for rendering at runtime unless the target OS does not |
288 | + # support NETWORK_CONFIG_V2 feature. |
289 | + LOG.info('Checking cloud-init in target [%s] for network ' |
290 | + 'configuration passthrough support.', target) |
291 | + try: |
292 | + passthrough = net.netconfig_passthrough_available(target) |
293 | + except util.ProcessExecutionError: |
294 | + LOG.warning('Failed to determine if passthrough is available') |
295 | + |
296 | + if passthrough: |
297 | + LOG.info('Passing network configuration through to target: %s', |
298 | + target) |
299 | + net.render_netconfig_passthrough(target, netconfig=netcfg) |
300 | + else: |
301 | + ns = net.parse_net_config_data(netcfg.get('network', {})) |
302 | + |
303 | + if not passthrough: |
304 | + LOG.info('Rendering network configuration in target') |
305 | + net.render_network_state(target=target, network_state=ns) |
306 | |
307 | _maybe_remove_legacy_eth0(target) |
308 | - LOG.info('Attempting to remove ipv6 privacy extensions') |
309 | _disable_ipv6_privacy_extensions(target) |
310 | _patch_ifupdown_ipv6_mtu_hook(target) |
311 | |
312 | @@ -130,6 +154,7 @@ |
313 | by default; this races with the cloud-image desire to disable them. |
314 | Resolve this by allowing the cloud-image setting to win. """ |
315 | |
316 | + LOG.debug('Attempting to remove ipv6 privacy extensions') |
317 | cfg = util.target_path(target, path=path) |
318 | if not os.path.exists(cfg): |
319 | LOG.warn('Failed to find ipv6 privacy conf file %s', cfg) |
320 | @@ -143,7 +168,7 @@ |
321 | lines = [f.strip() for f in contents.splitlines() |
322 | if not f.startswith("#")] |
323 | if lines == known_contents: |
324 | - LOG.info('deleting file: %s', cfg) |
325 | + LOG.info('Removing ipv6 privacy extension config file: %s', cfg) |
326 | util.del_file(cfg) |
327 | msg = "removed %s with known contents" % cfg |
328 | curtin_contents = '\n'.join( |
329 | @@ -153,9 +178,10 @@ |
330 | "# net.ipv6.conf.default.use_tempaddr = 2"]) |
331 | util.write_file(cfg, curtin_contents) |
332 | else: |
333 | - LOG.info('skipping, content didnt match') |
334 | - LOG.debug("found content:\n%s", lines) |
335 | - LOG.debug("expected contents:\n%s", known_contents) |
336 | + LOG.debug('skipping removal of %s, expected content not found', |
337 | + cfg) |
338 | + LOG.debug("Found content in file %s:\n%s", cfg, lines) |
339 | + LOG.debug("Expected contents in file %s:\n%s", cfg, known_contents) |
340 | msg = (bmsg + " '%s' exists with user configured content." % cfg) |
341 | except Exception as e: |
342 | msg = bmsg + " %s exists, but could not be read. %s" % (cfg, e) |
343 | |
344 | === modified file 'curtin/commands/apt_config.py' |
345 | --- curtin/commands/apt_config.py 2017-02-28 15:26:03 +0000 |
346 | +++ curtin/commands/apt_config.py 2017-10-06 01:09:48 +0000 |
347 | @@ -24,7 +24,6 @@ |
348 | import os |
349 | import re |
350 | import sys |
351 | -import time |
352 | import yaml |
353 | |
354 | from curtin.log import LOG |
355 | @@ -406,20 +405,12 @@ |
356 | if aa_repo_match(source): |
357 | with util.ChrootableTarget( |
358 | target, sys_resolvconf=True) as in_chroot: |
359 | - time_entered = time.time() |
360 | try: |
361 | in_chroot.subp(["add-apt-repository", source], |
362 | retries=(1, 2, 5, 10)) |
363 | except util.ProcessExecutionError: |
364 | LOG.exception("add-apt-repository failed.") |
365 | raise |
366 | - finally: |
367 | - # workaround to gnupg >=2.x spawning daemons (LP: #1645680) |
368 | - seconds_since = time.time() - time_entered + 1 |
369 | - in_chroot.subp(['killall', '--wait', '--quiet', |
370 | - '--younger-than', '%ds' % seconds_since, |
371 | - '--regexp', '(dirmngr|gpg-agent)'], |
372 | - rcs=[0, 1]) |
373 | continue |
374 | |
375 | sourcefn = util.target_path(target, ent['filename']) |
376 | |
377 | === modified file 'curtin/commands/curthooks.py' |
378 | --- curtin/commands/curthooks.py 2017-06-12 19:43:55 +0000 |
379 | +++ curtin/commands/curthooks.py 2017-10-06 01:09:48 +0000 |
380 | @@ -16,6 +16,7 @@ |
381 | # along with Curtin. If not, see <http://www.gnu.org/licenses/>. |
382 | |
383 | import copy |
384 | +import glob |
385 | import os |
386 | import platform |
387 | import re |
388 | @@ -25,6 +26,7 @@ |
389 | |
390 | from curtin import config |
391 | from curtin import block |
392 | +from curtin import net |
393 | from curtin import futil |
394 | from curtin.log import LOG |
395 | from curtin import swap |
396 | @@ -65,28 +67,18 @@ |
397 | } |
398 | } |
399 | |
400 | - |
401 | -def write_files(cfg, target): |
402 | - # this takes 'write_files' entry in config and writes files in the target |
403 | - # config entry example: |
404 | - # f1: |
405 | - # path: /file1 |
406 | - # content: !!binary | |
407 | - # f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAAwARAAAAAAABAAAAAAAAAAJAVAAAAAAA |
408 | - # f2: {path: /file2, content: "foobar", permissions: '0666'} |
409 | - if 'write_files' not in cfg: |
410 | - return |
411 | - |
412 | - for (key, info) in cfg.get('write_files').items(): |
413 | - if not info.get('path'): |
414 | - LOG.warn("Warning, write_files[%s] had no 'path' entry", key) |
415 | - continue |
416 | - |
417 | - futil.write_finfo(path=target + os.path.sep + info['path'], |
418 | - content=info.get('content', ''), |
419 | - owner=info.get('owner', "-1:-1"), |
420 | - perms=info.get('permissions', |
421 | - info.get('perms', "0644"))) |
422 | +CLOUD_INIT_YUM_REPO_TEMPLATE = """ |
423 | +[group_cloud-init-el-stable] |
424 | +name=Copr repo for el-stable owned by @cloud-init |
425 | +baseurl=https://copr-be.cloud.fedoraproject.org/results/@cloud-init/el-stable/epel-%s-$basearch/ |
426 | +type=rpm-md |
427 | +skip_if_unavailable=True |
428 | +gpgcheck=1 |
429 | +gpgkey=https://copr-be.cloud.fedoraproject.org/results/@cloud-init/el-stable/pubkey.gpg |
430 | +repo_gpgcheck=0 |
431 | +enabled=1 |
432 | +enabled_metadata=1 |
433 | +""" |
434 | |
435 | |
436 | def do_apt_config(cfg, target): |
437 | @@ -142,15 +134,9 @@ |
438 | parameters = root=%s |
439 | |
440 | """ % root_arg |
441 | - zipl_cfg = { |
442 | - "write_files": { |
443 | - "zipl_cfg": { |
444 | - "path": "/etc/zipl.conf", |
445 | - "content": zipl_conf, |
446 | - } |
447 | - } |
448 | - } |
449 | - write_files(zipl_cfg, target) |
450 | + futil.write_files( |
451 | + files={"zipl_conf": {"path": "/etc/zipl.conf", "content": zipl_conf}}, |
452 | + base_dir=target) |
453 | |
454 | |
455 | def run_zipl(cfg, target): |
456 | @@ -648,6 +634,40 @@ |
457 | update_initramfs(target, all_kernels=True) |
458 | |
459 | |
460 | +def detect_required_packages(cfg): |
461 | + """ |
462 | + detect packages that will be required in-target by custom config items |
463 | + """ |
464 | + |
465 | + mapping = { |
466 | + 'storage': block.detect_required_packages_mapping(), |
467 | + 'network': net.detect_required_packages_mapping(), |
468 | + } |
469 | + |
470 | + needed_packages = [] |
471 | + for cfg_type, cfg_map in mapping.items(): |
472 | + |
473 | + # skip missing or invalid config items, configs may |
474 | + # only have network or storage, not always both |
475 | + if not isinstance(cfg.get(cfg_type), dict): |
476 | + continue |
477 | + |
478 | + cfg_version = cfg[cfg_type].get('version') |
479 | + if not isinstance(cfg_version, int) or cfg_version not in cfg_map: |
480 | + msg = ('Supplied configuration version "%s", for config type' |
481 | + '"%s" is not present in the known mapping.' % (cfg_version, |
482 | + cfg_type)) |
483 | + raise ValueError(msg) |
484 | + |
485 | + mapped_config = cfg_map[cfg_version] |
486 | + found_reqs = mapped_config['handler'](cfg, mapped_config['mapping']) |
487 | + needed_packages.extend(found_reqs) |
488 | + |
489 | + LOG.debug('Curtin config dependencies requires additional packages: %s', |
490 | + needed_packages) |
491 | + return needed_packages |
492 | + |
493 | + |
494 | def install_missing_packages(cfg, target): |
495 | ''' describe which operation types will require specific packages |
496 | |
497 | @@ -655,46 +675,10 @@ |
498 | 'pkg1': ['op_name_1', 'op_name_2', ...] |
499 | } |
500 | ''' |
501 | - custom_configs = { |
502 | - 'storage': { |
503 | - 'lvm2': ['lvm_volgroup', 'lvm_partition'], |
504 | - 'mdadm': ['raid'], |
505 | - 'bcache-tools': ['bcache']}, |
506 | - 'network': { |
507 | - 'vlan': ['vlan'], |
508 | - 'ifenslave': ['bond'], |
509 | - 'bridge-utils': ['bridge']}, |
510 | - } |
511 | - |
512 | - format_configs = { |
513 | - 'xfsprogs': ['xfs'], |
514 | - 'e2fsprogs': ['ext2', 'ext3', 'ext4'], |
515 | - 'btrfs-tools': ['btrfs'], |
516 | - } |
517 | - |
518 | - needed_packages = [] |
519 | + |
520 | installed_packages = util.get_installed_packages(target) |
521 | - for cust_cfg, pkg_reqs in custom_configs.items(): |
522 | - if cust_cfg not in cfg: |
523 | - continue |
524 | - |
525 | - all_types = set( |
526 | - operation['type'] |
527 | - for operation in cfg[cust_cfg]['config'] |
528 | - ) |
529 | - for pkg, types in pkg_reqs.items(): |
530 | - if set(types).intersection(all_types) and \ |
531 | - pkg not in installed_packages: |
532 | - needed_packages.append(pkg) |
533 | - |
534 | - format_types = set( |
535 | - [operation['fstype'] |
536 | - for operation in cfg[cust_cfg]['config'] |
537 | - if operation['type'] == 'format']) |
538 | - for pkg, fstypes in format_configs.items(): |
539 | - if set(fstypes).intersection(format_types) and \ |
540 | - pkg not in installed_packages: |
541 | - needed_packages.append(pkg) |
542 | + needed_packages = set([pkg for pkg in detect_required_packages(cfg) |
543 | + if pkg not in installed_packages]) |
544 | |
545 | arch_packages = { |
546 | 's390x': [('s390-tools', 'zipl')], |
547 | @@ -703,16 +687,28 @@ |
548 | for pkg, cmd in arch_packages.get(platform.machine(), []): |
549 | if not util.which(cmd, target=target): |
550 | if pkg not in needed_packages: |
551 | - needed_packages.append(pkg) |
552 | + needed_packages.add(pkg) |
553 | + |
554 | + # FIXME: This needs cleaning up. |
555 | + # do not install certain packages on artful as they are no longer needed. |
556 | + # ifenslave specifically causes issuse due to dependency on ifupdown. |
557 | + codename = util.lsb_release(target=target).get('codename') |
558 | + if codename == 'artful': |
559 | + drops = set(['bridge-utils', 'ifenslave', 'vlan']) |
560 | + if needed_packages.union(drops): |
561 | + LOG.debug("Skipping install of %s. Not needed on artful.", |
562 | + needed_packages.union(drops)) |
563 | + needed_packages = needed_packages.difference(drops) |
564 | |
565 | if needed_packages: |
566 | + to_add = list(sorted(needed_packages)) |
567 | state = util.load_command_environment() |
568 | with events.ReportEventStack( |
569 | name=state.get('report_stack_prefix'), |
570 | reporting_enabled=True, level="INFO", |
571 | description="Installing packages on target system: " + |
572 | - str(needed_packages)): |
573 | - util.install_packages(needed_packages, target=target) |
574 | + str(to_add)): |
575 | + util.install_packages(to_add, target=target) |
576 | |
577 | |
578 | def system_upgrade(cfg, target): |
579 | @@ -737,8 +733,8 @@ |
580 | util.system_upgrade(target=target) |
581 | |
582 | |
583 | -def handle_cloudconfig(cfg, target=None): |
584 | - """write cloud-init configuration files into target |
585 | +def handle_cloudconfig(cfg, base_dir=None): |
586 | + """write cloud-init configuration files into base_dir. |
587 | |
588 | cloudconfig format is a dictionary of keys and values of content |
589 | |
590 | @@ -773,9 +769,9 @@ |
591 | cfgvalue['path'] = cfgpath |
592 | |
593 | # re-use write_files format and adjust target to prepend |
594 | - LOG.debug('Calling write_files with cloudconfig @ %s', target) |
595 | + LOG.debug('Calling write_files with cloudconfig @ %s', base_dir) |
596 | LOG.debug('Injecting cloud-config:\n%s', cfg) |
597 | - write_files({'write_files': cfg}, target) |
598 | + futil.write_files(cfg, base_dir) |
599 | |
600 | |
601 | def ubuntu_core_curthooks(cfg, target=None): |
602 | @@ -795,17 +791,98 @@ |
603 | if os.path.exists(cloudinit_disable): |
604 | util.del_file(cloudinit_disable) |
605 | |
606 | - handle_cloudconfig(cloudconfig, target=cc_target) |
607 | + handle_cloudconfig(cloudconfig, base_dir=cc_target) |
608 | |
609 | netconfig = cfg.get('network', None) |
610 | if netconfig: |
611 | LOG.info('Writing network configuration') |
612 | ubuntu_core_netconfig = os.path.join(cc_target, |
613 | - "50-network-config.cfg") |
614 | + "50-curtin-networking.cfg") |
615 | util.write_file(ubuntu_core_netconfig, |
616 | content=config.dump_config({'network': netconfig})) |
617 | |
618 | |
619 | +def rpm_get_dist_id(target): |
620 | + """Use rpm command to extract the '%rhel' distro macro which returns |
621 | + the major os version id (6, 7, 8). This works for centos or rhel |
622 | + """ |
623 | + with util.ChrootableTarget(target) as in_chroot: |
624 | + dist, _ = in_chroot.subp(['rpm', '-E', '%rhel'], capture=True) |
625 | + return dist.rstrip() |
626 | + |
627 | + |
628 | +def centos_apply_network_config(netcfg, target=None): |
629 | + """ CentOS images execute built-in curthooks which only supports |
630 | + simple networking configuration. This hook enables advanced |
631 | + network configuration via config passthrough to the target. |
632 | + """ |
633 | + |
634 | + def cloud_init_repo(version): |
635 | + if not version: |
636 | + raise ValueError('Missing required version parameter') |
637 | + |
638 | + return CLOUD_INIT_YUM_REPO_TEMPLATE % version |
639 | + |
640 | + if netcfg: |
641 | + LOG.info('Removing embedded network configuration (if present)') |
642 | + ifcfgs = glob.glob(util.target_path(target, |
643 | + 'etc/sysconfig/network-scripts') + |
644 | + '/ifcfg-*') |
645 | + # remove ifcfg-* (except ifcfg-lo) |
646 | + for ifcfg in ifcfgs: |
647 | + if os.path.basename(ifcfg) != "ifcfg-lo": |
648 | + util.del_file(ifcfg) |
649 | + |
650 | + LOG.info('Checking cloud-init in target [%s] for network ' |
651 | + 'configuration passthrough support.', target) |
652 | + passthrough = net.netconfig_passthrough_available(target) |
653 | + LOG.debug('passthrough available via in-target: %s', passthrough) |
654 | + |
655 | + # if in-target cloud-init is not updated, upgrade via cloud-init repo |
656 | + if not passthrough: |
657 | + cloud_init_yum_repo = ( |
658 | + util.target_path(target, |
659 | + 'etc/yum.repos.d/curtin-cloud-init.repo')) |
660 | + # Inject cloud-init daily yum repo |
661 | + util.write_file(cloud_init_yum_repo, |
662 | + content=cloud_init_repo(rpm_get_dist_id(target))) |
663 | + |
664 | + # we separate the installation of repository packages (epel, |
665 | + # cloud-init-el-release) as we need a new invocation of yum |
666 | + # to read the newly installed repo files. |
667 | + YUM_CMD = ['yum', '-y', '--noplugins', 'install'] |
668 | + retries = [1] * 30 |
669 | + with util.ChrootableTarget(target) as in_chroot: |
670 | + # ensure up-to-date ca-certificates to handle https mirror |
671 | + # connections |
672 | + in_chroot.subp(YUM_CMD + ['ca-certificates'], capture=True, |
673 | + log_captured=True, retries=retries) |
674 | + in_chroot.subp(YUM_CMD + ['epel-release'], capture=True, |
675 | + log_captured=True, retries=retries) |
676 | + in_chroot.subp(YUM_CMD + ['cloud-init-el-release'], |
677 | + log_captured=True, capture=True, |
678 | + retries=retries) |
679 | + in_chroot.subp(YUM_CMD + ['cloud-init'], capture=True, |
680 | + log_captured=True, retries=retries) |
681 | + |
682 | + # remove cloud-init el-stable bootstrap repo config as the |
683 | + # cloud-init-el-release package points to the correct repo |
684 | + util.del_file(cloud_init_yum_repo) |
685 | + |
686 | + # install bridge-utils if needed |
687 | + with util.ChrootableTarget(target) as in_chroot: |
688 | + try: |
689 | + in_chroot.subp(['rpm', '-q', 'bridge-utils'], |
690 | + capture=False, rcs=[0]) |
691 | + except util.ProcessExecutionError: |
692 | + LOG.debug('Image missing bridge-utils package, installing') |
693 | + in_chroot.subp(YUM_CMD + ['bridge-utils'], capture=True, |
694 | + log_captured=True, retries=retries) |
695 | + |
696 | + LOG.info('Passing network configuration through to target') |
697 | + net.render_netconfig_passthrough(target, netconfig={'network': netcfg}) |
698 | + |
699 | + |
700 | def target_is_ubuntu_core(target): |
701 | """Check if Ubuntu-Core specific directory is present at target""" |
702 | if target: |
703 | @@ -814,6 +891,22 @@ |
704 | return False |
705 | |
706 | |
707 | +def target_is_centos(target): |
708 | + """Check if CentOS specific file is present at target""" |
709 | + if target: |
710 | + return os.path.exists(util.target_path(target, 'etc/centos-release')) |
711 | + |
712 | + return False |
713 | + |
714 | + |
715 | +def target_is_rhel(target): |
716 | + """Check if RHEL specific file is present at target""" |
717 | + if target: |
718 | + return os.path.exists(util.target_path(target, 'etc/redhat-release')) |
719 | + |
720 | + return False |
721 | + |
722 | + |
723 | def curthooks(args): |
724 | state = util.load_command_environment() |
725 | |
726 | @@ -827,14 +920,28 @@ |
727 | "Use --target or set TARGET_MOUNT_POINT\n") |
728 | sys.exit(2) |
729 | |
730 | - # if network-config hook exists in target, |
731 | - # we do not run the builtin |
732 | - if util.run_hook_if_exists(target, 'curtin-hooks'): |
733 | - sys.exit(0) |
734 | - |
735 | cfg = config.load_command_config(args, state) |
736 | stack_prefix = state.get('report_stack_prefix', '') |
737 | |
738 | + # if curtin-hooks hook exists in target we can defer to the in-target hooks |
739 | + if util.run_hook_if_exists(target, 'curtin-hooks'): |
740 | + # For vmtests to force execute centos_apply_network_config, uncomment |
741 | + # the value in examples/tests/centos_defaults.yaml |
742 | + if cfg.get('_ammend_centos_curthooks'): |
743 | + if cfg.get('cloudconfig'): |
744 | + handle_cloudconfig( |
745 | + cfg['cloudconfig'], |
746 | + base_dir=util.target_path(target, 'etc/cloud/cloud.cfg.d')) |
747 | + |
748 | + if target_is_centos(target) or target_is_rhel(target): |
749 | + LOG.info('Detected RHEL/CentOS image, running extra hooks') |
750 | + with events.ReportEventStack( |
751 | + name=stack_prefix, reporting_enabled=True, |
752 | + level="INFO", |
753 | + description="Configuring CentOS for first boot"): |
754 | + centos_apply_network_config(cfg.get('network', {}), target) |
755 | + sys.exit(0) |
756 | + |
757 | if target_is_ubuntu_core(target): |
758 | LOG.info('Detected Ubuntu-Core image, running hooks') |
759 | with events.ReportEventStack( |
760 | @@ -846,13 +953,16 @@ |
761 | with events.ReportEventStack( |
762 | name=stack_prefix + '/writing-config', |
763 | reporting_enabled=True, level="INFO", |
764 | - description="writing config files and configuring apt"): |
765 | - write_files(cfg, target) |
766 | + description="configuring apt configuring apt"): |
767 | do_apt_config(cfg, target) |
768 | disable_overlayroot(cfg, target) |
769 | |
770 | # packages may be needed prior to installing kernel |
771 | - install_missing_packages(cfg, target) |
772 | + with events.ReportEventStack( |
773 | + name=stack_prefix + '/installing-missing-packages', |
774 | + reporting_enabled=True, level="INFO", |
775 | + description="installing missing packages"): |
776 | + install_missing_packages(cfg, target) |
777 | |
778 | # If a /etc/iscsi/nodes/... file was created by block_meta then it |
779 | # needs to be copied onto the target system |
780 | @@ -880,7 +990,6 @@ |
781 | setup_zipl(cfg, target) |
782 | install_kernel(cfg, target) |
783 | run_zipl(cfg, target) |
784 | - |
785 | restore_dist_interfaces(cfg, target) |
786 | |
787 | with events.ReportEventStack( |
788 | @@ -908,12 +1017,6 @@ |
789 | detect_and_handle_multipath(cfg, target) |
790 | |
791 | with events.ReportEventStack( |
792 | - name=stack_prefix + '/installing-missing-packages', |
793 | - reporting_enabled=True, level="INFO", |
794 | - description="installing missing packages"): |
795 | - install_missing_packages(cfg, target) |
796 | - |
797 | - with events.ReportEventStack( |
798 | name=stack_prefix + '/system-upgrade', |
799 | reporting_enabled=True, level="INFO", |
800 | description="updating packages on target system"): |
801 | |
802 | === modified file 'curtin/commands/extract.py' |
803 | --- curtin/commands/extract.py 2016-05-05 16:43:40 +0000 |
804 | +++ curtin/commands/extract.py 2017-10-06 01:09:48 +0000 |
805 | @@ -21,6 +21,7 @@ |
806 | import curtin.config |
807 | from curtin.log import LOG |
808 | import curtin.util |
809 | +from curtin.futil import write_files |
810 | from curtin.reporter import events |
811 | |
812 | from . import populate_one_subcmd |
813 | @@ -122,6 +123,11 @@ |
814 | "do not know how to extract '%s'" % |
815 | source['uri']) |
816 | |
817 | + if cfg.get('write_files'): |
818 | + LOG.info("Applying write_files from config.") |
819 | + write_files(cfg['write_files'], target) |
820 | + else: |
821 | + LOG.info("No write_files in config.") |
822 | sys.exit(0) |
823 | |
824 | |
825 | |
826 | === modified file 'curtin/commands/install.py' |
827 | --- curtin/commands/install.py 2017-05-19 20:56:27 +0000 |
828 | +++ curtin/commands/install.py 2017-10-06 01:09:48 +0000 |
829 | @@ -366,6 +366,27 @@ |
830 | return True |
831 | |
832 | |
833 | +def migrate_proxy_settings(cfg): |
834 | + """Move the legacy proxy setting 'http_proxy' into cfg['proxy'].""" |
835 | + proxy = cfg.get('proxy', {}) |
836 | + if not isinstance(proxy, dict): |
837 | + raise ValueError("'proxy' in config is not a dictionary: %s" % proxy) |
838 | + |
839 | + if 'http_proxy' in cfg: |
840 | + hp = cfg['http_proxy'] |
841 | + if hp: |
842 | + if proxy.get('http_proxy', hp) != hp: |
843 | + LOG.warn("legacy http_proxy setting (%s) differs from " |
844 | + "proxy/http_proxy (%s), using %s", |
845 | + hp, proxy['http_proxy'], proxy['http_proxy']) |
846 | + else: |
847 | + LOG.debug("legacy 'http_proxy' migrated to proxy/http_proxy") |
848 | + proxy['http_proxy'] = hp |
849 | + del cfg['http_proxy'] |
850 | + |
851 | + cfg['proxy'] = proxy |
852 | + |
853 | + |
854 | def cmd_install(args): |
855 | cfg = CONFIG_BUILTIN.copy() |
856 | config.merge_config(cfg, args.config) |
857 | @@ -384,8 +405,10 @@ |
858 | # we default to tgz for old style sources config |
859 | cfg['sources'][i] = util.sanitize_source(cfg['sources'][i]) |
860 | |
861 | - if cfg.get('http_proxy'): |
862 | - os.environ['http_proxy'] = cfg['http_proxy'] |
863 | + migrate_proxy_settings(cfg) |
864 | + for k in ('http_proxy', 'https_proxy', 'no_proxy'): |
865 | + if k in cfg['proxy']: |
866 | + os.environ[k] = cfg['proxy'][k] |
867 | |
868 | instcfg = cfg.get('install', {}) |
869 | logfile = instcfg.get('log_file') |
870 | @@ -454,9 +477,26 @@ |
871 | '/root/curtin-install.log') |
872 | if log_target_path: |
873 | copy_install_log(logfile, workingd.target, log_target_path) |
874 | + # unmount everything (including iscsi disks) |
875 | util.do_umount(workingd.target, recursive=True) |
876 | - # need to do some processing on iscsi disks to disconnect? |
877 | - iscsi.disconnect_target_disks(workingd.target) |
878 | + |
879 | + # The open-iscsi service in the ephemeral environment handles |
880 | + # disconnecting active sessions. On Artful release the systemd |
881 | + # unit file has conditionals that are not met at boot time and |
882 | + # results in open-iscsi service not being started; This breaks |
883 | + # shutdown on Artful releases. |
884 | + # Additionally, in release < Artful, if the storage configuration |
885 | + # is layered, like RAID over iscsi volumes, then disconnecting iscsi |
886 | + # sessions before stopping the raid device hangs. |
887 | + # As it turns out, letting the open-iscsi service take down the |
888 | + # session last is the cleanest way to handle all releases regardless |
889 | + # of what may be layered on top of the iscsi disks. |
890 | + # |
891 | + # Check if storage configuration has iscsi volumes and if so ensure |
892 | + # iscsi service is active before exiting install |
893 | + if iscsi.get_iscsi_disks_from_config(cfg): |
894 | + iscsi.restart_iscsi_service() |
895 | + |
896 | shutil.rmtree(workingd.top) |
897 | |
898 | apply_power_state(cfg.get('power_state')) |
899 | |
900 | === modified file 'curtin/futil.py' |
901 | --- curtin/futil.py 2014-03-26 17:34:57 +0000 |
902 | +++ curtin/futil.py 2017-10-06 01:09:48 +0000 |
903 | @@ -19,7 +19,8 @@ |
904 | import pwd |
905 | import os |
906 | |
907 | -from .util import write_file |
908 | +from .util import write_file, target_path |
909 | +from .log import LOG |
910 | |
911 | |
912 | def chownbyid(fname, uid=None, gid=None): |
913 | @@ -78,3 +79,25 @@ |
914 | omode = "wb" |
915 | write_file(path, content, mode=decode_perms(perms), omode=omode) |
916 | chownbyname(path, u, g) |
917 | + |
918 | + |
919 | +def write_files(files, base_dir=None): |
920 | + """Write files described in the dictionary 'files' |
921 | + |
922 | + paths are assumed under 'base_dir', which will default to '/'. |
923 | + A trailing '/' will be applied if not present. |
924 | + |
925 | + files is a dictionary where each entry has: |
926 | + path: /file1 |
927 | + content: (bytes or string) |
928 | + permissions: (optional, default=0644) |
929 | + owner: (optional, default -1:-1): string of 'uid:gid'.""" |
930 | + for (key, info) in files.items(): |
931 | + if not info.get('path'): |
932 | + LOG.warn("Warning, write_files[%s] had no 'path' entry", key) |
933 | + continue |
934 | + |
935 | + write_finfo(path=target_path(base_dir, info['path']), |
936 | + content=info.get('content', ''), |
937 | + owner=info.get('owner', "-1:-1"), |
938 | + perms=info.get('permissions', info.get('perms', "0644"))) |
939 | |
940 | === modified file 'curtin/net/__init__.py' |
941 | --- curtin/net/__init__.py 2017-02-28 15:26:03 +0000 |
942 | +++ curtin/net/__init__.py 2017-10-06 01:09:48 +0000 |
943 | @@ -520,7 +520,52 @@ |
944 | return content |
945 | |
946 | |
947 | +def netconfig_passthrough_available(target, feature='NETWORK_CONFIG_V2'): |
948 | + """ |
949 | + Determine if curtin can pass v2 network config to in target cloud-init |
950 | + """ |
951 | + LOG.debug('Checking in-target cloud-init for feature: %s', feature) |
952 | + with util.ChrootableTarget(target) as in_chroot: |
953 | + |
954 | + cloudinit = util.which('cloud-init', target=target) |
955 | + if not cloudinit: |
956 | + LOG.warning('Target does not have cloud-init installed') |
957 | + return False |
958 | + |
959 | + available = False |
960 | + try: |
961 | + out, _ = in_chroot.subp([cloudinit, 'features'], capture=True) |
962 | + available = feature in out.splitlines() |
963 | + except util.ProcessExecutionError: |
964 | + # we explicitly don't dump the exception as this triggers |
965 | + # vmtest failures when parsing the installation log file |
966 | + LOG.warning("Failed to probe cloudinit features") |
967 | + return False |
968 | + |
969 | + LOG.debug('cloud-init feature %s available? %s', feature, available) |
970 | + return available |
971 | + |
972 | + |
973 | +def render_netconfig_passthrough(target, netconfig=None): |
974 | + """ |
975 | + Extract original network config and pass it |
976 | + through to cloud-init in target |
977 | + """ |
978 | + cc = 'etc/cloud/cloud.cfg.d/50-curtin-networking.cfg' |
979 | + if not isinstance(netconfig, dict): |
980 | + raise ValueError('Network config must be a dictionary') |
981 | + |
982 | + if 'network' not in netconfig: |
983 | + raise ValueError("Network config must contain the key 'network'") |
984 | + |
985 | + content = config.dump_config(netconfig) |
986 | + cc_passthrough = os.path.sep.join((target, cc,)) |
987 | + LOG.info('Writing network config to %s: %s', cc, cc_passthrough) |
988 | + util.write_file(cc_passthrough, content=content) |
989 | + |
990 | + |
991 | def render_network_state(target, network_state): |
992 | + LOG.debug("rendering eni from netconfig") |
993 | eni = 'etc/network/interfaces' |
994 | netrules = 'etc/udev/rules.d/70-persistent-net.rules' |
995 | cc = 'etc/cloud/cloud.cfg.d/curtin-disable-cloudinit-networking.cfg' |
996 | @@ -542,4 +587,65 @@ |
997 | """Returns the string value of an interface's MAC Address""" |
998 | return read_sys_net(ifname, "address", enoent=False) |
999 | |
1000 | + |
1001 | +def network_config_required_packages(network_config, mapping=None): |
1002 | + |
1003 | + if network_config is None: |
1004 | + network_config = {} |
1005 | + |
1006 | + if not isinstance(network_config, dict): |
1007 | + raise ValueError('Invalid network configuration. Must be a dict') |
1008 | + |
1009 | + if mapping is None: |
1010 | + mapping = {} |
1011 | + |
1012 | + if not isinstance(mapping, dict): |
1013 | + raise ValueError('Invalid network mapping. Must be a dict') |
1014 | + |
1015 | + # allow top-level 'network' key |
1016 | + if 'network' in network_config: |
1017 | + network_config = network_config.get('network') |
1018 | + |
1019 | + # v1 has 'config' key and uses type: devtype elements |
1020 | + if 'config' in network_config: |
1021 | + dev_configs = set(device['type'] |
1022 | + for device in network_config['config']) |
1023 | + else: |
1024 | + # v2 has no config key |
1025 | + dev_configs = set(cfgtype for (cfgtype, cfg) in |
1026 | + network_config.items() if cfgtype not in ['version']) |
1027 | + |
1028 | + needed_packages = [] |
1029 | + for dev_type in dev_configs: |
1030 | + if dev_type in mapping: |
1031 | + needed_packages.extend(mapping[dev_type]) |
1032 | + |
1033 | + return needed_packages |
1034 | + |
1035 | + |
1036 | +def detect_required_packages_mapping(): |
1037 | + """Return a dictionary providing a versioned configuration which maps |
1038 | + network configuration elements to the packages which are required |
1039 | + for functionality. |
1040 | + """ |
1041 | + mapping = { |
1042 | + 1: { |
1043 | + 'handler': network_config_required_packages, |
1044 | + 'mapping': { |
1045 | + 'bond': ['ifenslave'], |
1046 | + 'bridge': ['bridge-utils'], |
1047 | + 'vlan': ['vlan']}, |
1048 | + }, |
1049 | + 2: { |
1050 | + 'handler': network_config_required_packages, |
1051 | + 'mapping': { |
1052 | + 'bonds': ['ifenslave'], |
1053 | + 'bridges': ['bridge-utils'], |
1054 | + 'vlans': ['vlan']} |
1055 | + }, |
1056 | + } |
1057 | + |
1058 | + return mapping |
1059 | + |
1060 | + |
1061 | # vi: ts=4 expandtab syntax=python |
1062 | |
1063 | === modified file 'curtin/reporter/handlers.py' |
1064 | --- curtin/reporter/handlers.py 2017-02-08 20:25:39 +0000 |
1065 | +++ curtin/reporter/handlers.py 2017-10-06 01:09:48 +0000 |
1066 | @@ -80,7 +80,49 @@ |
1067 | LOG.warn("failed posting event: %s [%s]" % (event.as_string(), e)) |
1068 | |
1069 | |
1070 | +class JournaldHandler(ReportingHandler): |
1071 | + |
1072 | + def __init__(self, level="DEBUG", identifier="curtin_event"): |
1073 | + super(JournaldHandler, self).__init__() |
1074 | + if isinstance(level, int): |
1075 | + pass |
1076 | + else: |
1077 | + input_level = level |
1078 | + try: |
1079 | + level = getattr(logging, level.upper()) |
1080 | + except Exception: |
1081 | + LOG.warn("invalid level '%s', using WARN", input_level) |
1082 | + level = logging.WARN |
1083 | + self.level = level |
1084 | + self.identifier = identifier |
1085 | + |
1086 | + def publish_event(self, event): |
1087 | + # Ubuntu older than precise will not have python-systemd installed. |
1088 | + try: |
1089 | + from systemd import journal |
1090 | + except ImportError: |
1091 | + raise |
1092 | + level = str(getattr(journal, "LOG_" + event.level, journal.LOG_DEBUG)) |
1093 | + extra = {} |
1094 | + if hasattr(event, 'result'): |
1095 | + extra['CURTIN_RESULT'] = event.result |
1096 | + journal.send( |
1097 | + event.as_string(), |
1098 | + PRIORITY=level, |
1099 | + SYSLOG_IDENTIFIER=self.identifier, |
1100 | + CURTIN_EVENT_TYPE=event.event_type, |
1101 | + CURTIN_MESSAGE=event.description, |
1102 | + CURTIN_NAME=event.name, |
1103 | + **extra |
1104 | + ) |
1105 | + |
1106 | + |
1107 | available_handlers = DictRegistry() |
1108 | available_handlers.register_item('log', LogHandler) |
1109 | available_handlers.register_item('print', PrintHandler) |
1110 | available_handlers.register_item('webhook', WebHookHandler) |
1111 | +# only add journald handler on systemd systems |
1112 | +try: |
1113 | + available_handlers.register_item('journald', JournaldHandler) |
1114 | +except ImportError: |
1115 | + print('journald report handler not supported; no systemd module') |
1116 | |
1117 | === modified file 'curtin/util.py' |
1118 | --- curtin/util.py 2017-06-12 19:43:55 +0000 |
1119 | +++ curtin/util.py 2017-10-06 01:09:48 +0000 |
1120 | @@ -23,6 +23,7 @@ |
1121 | import os |
1122 | import platform |
1123 | import re |
1124 | +import shlex |
1125 | import shutil |
1126 | import socket |
1127 | import subprocess |
1128 | @@ -57,6 +58,8 @@ |
1129 | _INSTALLED_MAIN = '/usr/bin/curtin' |
1130 | |
1131 | _LSB_RELEASE = {} |
1132 | +_USES_SYSTEMD = None |
1133 | +_HAS_UNSHARE_PID = None |
1134 | |
1135 | _DNS_REDIRECT_IP = None |
1136 | |
1137 | @@ -66,21 +69,31 @@ |
1138 | |
1139 | def _subp(args, data=None, rcs=None, env=None, capture=False, |
1140 | shell=False, logstring=False, decode="replace", |
1141 | - target=None, cwd=None, log_captured=False): |
1142 | + target=None, cwd=None, log_captured=False, unshare_pid=None): |
1143 | if rcs is None: |
1144 | rcs = [0] |
1145 | - |
1146 | devnull_fp = None |
1147 | - try: |
1148 | - if target_path(target) != "/": |
1149 | - args = ['chroot', target] + list(args) |
1150 | - |
1151 | - if not logstring: |
1152 | - LOG.debug(("Running command %s with allowed return codes %s" |
1153 | - " (shell=%s, capture=%s)"), args, rcs, shell, capture) |
1154 | - else: |
1155 | - LOG.debug(("Running hidden command to protect sensitive " |
1156 | - "input/output logstring: %s"), logstring) |
1157 | + |
1158 | + tpath = target_path(target) |
1159 | + chroot_args = [] if tpath == "/" else ['chroot', target] |
1160 | + sh_args = ['sh', '-c'] if shell else [] |
1161 | + if isinstance(args, string_types): |
1162 | + args = [args] |
1163 | + |
1164 | + try: |
1165 | + unshare_args = _get_unshare_pid_args(unshare_pid, tpath) |
1166 | + except RuntimeError as e: |
1167 | + raise RuntimeError("Unable to unshare pid (cmd=%s): %s" % (args, e)) |
1168 | + |
1169 | + args = unshare_args + chroot_args + sh_args + list(args) |
1170 | + |
1171 | + if not logstring: |
1172 | + LOG.debug(("Running command %s with allowed return codes %s" |
1173 | + " (capture=%s)"), args, rcs, capture) |
1174 | + else: |
1175 | + LOG.debug(("Running hidden command to protect sensitive " |
1176 | + "input/output logstring: %s"), logstring) |
1177 | + try: |
1178 | stdin = None |
1179 | stdout = None |
1180 | stderr = None |
1181 | @@ -94,7 +107,7 @@ |
1182 | stdin = subprocess.PIPE |
1183 | sp = subprocess.Popen(args, stdout=stdout, |
1184 | stderr=stderr, stdin=stdin, |
1185 | - env=env, shell=shell, cwd=cwd) |
1186 | + env=env, shell=False, cwd=cwd) |
1187 | # communicate in python2 returns str, python3 returns bytes |
1188 | (out, err) = sp.communicate(data) |
1189 | |
1190 | @@ -128,6 +141,63 @@ |
1191 | return (out, err) |
1192 | |
1193 | |
1194 | +def _has_unshare_pid(): |
1195 | + global _HAS_UNSHARE_PID |
1196 | + if _HAS_UNSHARE_PID is not None: |
1197 | + return _HAS_UNSHARE_PID |
1198 | + |
1199 | + if not which('unshare'): |
1200 | + _HAS_UNSHARE_PID = False |
1201 | + return False |
1202 | + out, err = subp(["unshare", "--help"], capture=True, decode=False, |
1203 | + unshare_pid=False) |
1204 | + joined = b'\n'.join([out, err]) |
1205 | + _HAS_UNSHARE_PID = b'--fork' in joined and b'--pid' in joined |
1206 | + return _HAS_UNSHARE_PID |
1207 | + |
1208 | + |
1209 | +def _get_unshare_pid_args(unshare_pid=None, target=None, euid=None): |
1210 | + """Get args for calling unshare for a pid. |
1211 | + |
1212 | + If unshare_pid is False, return empty list. |
1213 | + If unshare_pid is True, check if it is usable. If not, raise exception. |
1214 | + if unshare_pid is None, then unshare if |
1215 | + * euid is 0 |
1216 | + * 'unshare' with '--fork' and '--pid' is available. |
1217 | + * target != / |
1218 | + """ |
1219 | + if unshare_pid is not None and not unshare_pid: |
1220 | + # given a false-ish other than None means no. |
1221 | + return [] |
1222 | + |
1223 | + if euid is None: |
1224 | + euid = os.geteuid() |
1225 | + |
1226 | + tpath = target_path(target) |
1227 | + |
1228 | + unshare_pid_in = unshare_pid |
1229 | + if unshare_pid is None: |
1230 | + unshare_pid = False |
1231 | + if tpath != "/" and euid == 0: |
1232 | + if _has_unshare_pid(): |
1233 | + unshare_pid = True |
1234 | + |
1235 | + if not unshare_pid: |
1236 | + return [] |
1237 | + |
1238 | + # either unshare was passed in as True, or None and turned to True. |
1239 | + if euid != 0: |
1240 | + raise RuntimeError( |
1241 | + "given unshare_pid=%s but euid (%s) != 0." % |
1242 | + (unshare_pid_in, euid)) |
1243 | + |
1244 | + if not _has_unshare_pid(): |
1245 | + raise RuntimeError( |
1246 | + "given unshare_pid=%s but no unshare command." % unshare_pid_in) |
1247 | + |
1248 | + return ['unshare', '--fork', '--pid', '--'] |
1249 | + |
1250 | + |
1251 | def subp(*args, **kwargs): |
1252 | """Run a subprocess. |
1253 | |
1254 | @@ -160,6 +230,10 @@ |
1255 | means to run, sleep 1, run, sleep 3, run and then return exit code. |
1256 | :param target: |
1257 | run the command as 'chroot target <args>' |
1258 | + :param unshare_pid: |
1259 | + unshare the pid namespace. |
1260 | + default value (None) is to unshare pid namespace if possible |
1261 | + and target != / |
1262 | |
1263 | :return |
1264 | if not capturing, return is (None, None) |
1265 | @@ -1275,6 +1349,9 @@ |
1266 | if not path: |
1267 | return target |
1268 | |
1269 | + if not isinstance(path, string_types): |
1270 | + raise ValueError("Unexpected input for path: %s" % path) |
1271 | + |
1272 | # os.path.join("/etc", "/foo") returns "/foo". Chomp all leading /. |
1273 | while len(path) and path[0] == "/": |
1274 | path = path[1:] |
1275 | @@ -1290,4 +1367,51 @@ |
1276 | __call__ = ChrootableTarget.subp |
1277 | |
1278 | |
1279 | +def shlex_split(str_in): |
1280 | + # shlex.split takes a string |
1281 | + # but in python2 if input here is a unicode, encode it to a string. |
1282 | + # http://stackoverflow.com/questions/2365411/ |
1283 | + # python-convert-unicode-to-ascii-without-errors |
1284 | + if sys.version_info.major == 2: |
1285 | + try: |
1286 | + if isinstance(str_in, unicode): |
1287 | + str_in = str_in.encode('utf-8') |
1288 | + except NameError: |
1289 | + pass |
1290 | + |
1291 | + return shlex.split(str_in) |
1292 | + else: |
1293 | + return shlex.split(str_in) |
1294 | + |
1295 | + |
1296 | +def load_shell_content(content, add_empty=False, empty_val=None): |
1297 | + """Given shell like syntax (key=value\nkey2=value2\n) in content |
1298 | + return the data in dictionary form. If 'add_empty' is True |
1299 | + then add entries in to the returned dictionary for 'VAR=' |
1300 | + variables. Set their value to empty_val.""" |
1301 | + |
1302 | + data = {} |
1303 | + for line in shlex_split(content): |
1304 | + key, value = line.split("=", 1) |
1305 | + if not value: |
1306 | + value = empty_val |
1307 | + if add_empty or value: |
1308 | + data[key] = value |
1309 | + |
1310 | + return data |
1311 | + |
1312 | + |
1313 | +def uses_systemd(): |
1314 | + """ Check if current enviroment uses systemd by testing if |
1315 | + /run/systemd/system is a directory; only present if |
1316 | + systemd is available on running system. |
1317 | + """ |
1318 | + |
1319 | + global _USES_SYSTEMD |
1320 | + if _USES_SYSTEMD is None: |
1321 | + _USES_SYSTEMD = os.path.isdir('/run/systemd/system') |
1322 | + |
1323 | + return _USES_SYSTEMD |
1324 | + |
1325 | + |
1326 | # vi: ts=4 expandtab syntax=python |
1327 | |
1328 | === modified file 'debian/changelog' |
1329 | --- debian/changelog 2017-06-12 19:45:37 +0000 |
1330 | +++ debian/changelog 2017-10-06 01:09:48 +0000 |
1331 | @@ -1,3 +1,36 @@ |
1332 | +curtin (0.1.0~bzr532-0ubuntu1) artful; urgency=medium |
1333 | + |
1334 | + * New upstream snapshot. |
1335 | + - vmtest: fix artful networking (LP: #1714028) (LP: #1718216) (LP: #1706744) |
1336 | + - docs: Trivial doc fix for enabling proposed. |
1337 | + - setup.py: fix to allow installation into a virtualenv (LP: #1703755) |
1338 | + - doc: update documentation on curtin-hooks and non-ubuntu installation. |
1339 | + - reporter: Add journald reporter to send events to journald |
1340 | + - vmtests: add option to tar disk images after test run |
1341 | + - install: ensure iscsi service is running to handle shutdown properly |
1342 | + - mdadm: handle write failures to sysfs entries when stopping mdadm (LP: #1708052) |
1343 | + - vmtest: catch exceptions in curtin-log-print |
1344 | + - iscsi: use curtin storage config to disconnect iscsi targets (LP: #1713537) |
1345 | + - vmtests: bump skip_by_date values out to give cloud-init SRU more time |
1346 | + - vmtest: get info about collected symlinks and then delete them. |
1347 | + - Update network cloud-init related skiptest dates, SRU still pending |
1348 | + - tests: Add CiTestCase common parent for all curtin tests. |
1349 | + - vmtests: Remove force flag for centos curthooks |
1350 | + - tools/jenkins-runner: improve tgtd cleanup logic |
1351 | + - tests: Drop EOL Wily Vivid and Yakkety tests. |
1352 | + - Disable yum plugins when installing packages, update ca-certs for https |
1353 | + - Rename centos_network_curthooks -> centos_apply_network_config. |
1354 | + - tests: in centos_defaults use write_files for grub serial. |
1355 | + - write_files: write files after extract, change write_files signature. |
1356 | + - pass network configuration through to target for ubuntu and centos |
1357 | + - tests: disable yakkety tests. |
1358 | + - tools/launch: automatically pass on proxy settings to curtin |
1359 | + - Add top level 'proxy' to config, deprecate top level http_proxy. |
1360 | + - tools/curtainer: fix to enable deb-src for -proposed. |
1361 | + - Use unshare to put chroot commands in own pid namespace. |
1362 | + |
1363 | + -- Ryan Harper <ryan.harper@canonical.com> Thu, 05 Oct 2017 19:15:28 -0500 |
1364 | + |
1365 | curtin (0.1.0~bzr505-0ubuntu1) artful; urgency=medium |
1366 | |
1367 | * debian/new-upstream-snapshot: fix issue with whitespace at end of line. |
1368 | |
1369 | === modified file 'doc/index.rst' |
1370 | --- doc/index.rst 2016-09-29 18:31:02 +0000 |
1371 | +++ doc/index.rst 2017-10-06 01:09:48 +0000 |
1372 | @@ -17,6 +17,7 @@ |
1373 | topics/apt_source |
1374 | topics/networking |
1375 | topics/storage |
1376 | + topics/curthooks |
1377 | topics/reporting |
1378 | topics/development |
1379 | topics/integration-testing |
1380 | |
1381 | === modified file 'doc/topics/apt_source.rst' |
1382 | --- doc/topics/apt_source.rst 2016-09-29 18:31:02 +0000 |
1383 | +++ doc/topics/apt_source.rst 2017-10-06 01:09:48 +0000 |
1384 | @@ -135,7 +135,9 @@ |
1385 | |
1386 | apt: |
1387 | sources: |
1388 | - proposed.list: deb $MIRROR $RELEASE-proposed main restricted universe multiverse |
1389 | + proposed.list: |
1390 | + source: | |
1391 | + deb $MIRROR $RELEASE-proposed main restricted universe multiverse |
1392 | |
1393 | * Make debug symbols available |
1394 | |
1395 | @@ -143,11 +145,12 @@ |
1396 | |
1397 | apt: |
1398 | sources: |
1399 | - ddebs.list: | |
1400 | - deb http://ddebs.ubuntu.com $RELEASE main restricted universe multiverse |
1401 | - Â deb http://ddebs.ubuntu.com $RELEASE-updates main restricted universe multiverse |
1402 | - Â deb http://ddebs.ubuntu.com $RELEASE-security main restricted universe multiverse |
1403 | - deb http://ddebs.ubuntu.com $RELEASE-proposed main restricted universe multiverse |
1404 | + ddebs.list: |
1405 | + source: | |
1406 | + deb http://ddebs.ubuntu.com $RELEASE main restricted universe multiverse |
1407 | + Â deb http://ddebs.ubuntu.com $RELEASE-updates main restricted universe multiverse |
1408 | + Â deb http://ddebs.ubuntu.com $RELEASE-security main restricted universe multiverse |
1409 | + deb http://ddebs.ubuntu.com $RELEASE-proposed main restricted universe multiverse |
1410 | |
1411 | Timing |
1412 | ~~~~~~ |
1413 | |
1414 | === modified file 'doc/topics/config.rst' |
1415 | --- doc/topics/config.rst 2016-09-29 18:31:02 +0000 |
1416 | +++ doc/topics/config.rst 2017-10-06 01:09:48 +0000 |
1417 | @@ -24,6 +24,7 @@ |
1418 | - multipath (``multipath``) |
1419 | - network (``network``) |
1420 | - power_state (``power_state``) |
1421 | +- proxy (``proxy``) |
1422 | - reporting (``reporting``) |
1423 | - restore_dist_interfaces: (``restore_dist_interfaces``) |
1424 | - sources (``sources``) |
1425 | @@ -177,6 +178,7 @@ |
1426 | http_proxy |
1427 | ~~~~~~~~~~ |
1428 | Curtin will export ``http_proxy`` value into the installer environment. |
1429 | +**Deprecated**: This setting is deprecated in favor of ``proxy`` below. |
1430 | |
1431 | **http_proxy**: *<HTTP Proxy URL>* |
1432 | |
1433 | @@ -348,6 +350,22 @@ |
1434 | message: Bye Bye |
1435 | |
1436 | |
1437 | +proxy |
1438 | +~~~~~ |
1439 | +Curtin will put ``http_proxy``, ``https_proxy`` and ``no_proxy`` |
1440 | +into its install environment. This is in affect for curtin's process |
1441 | +and subprocesses. |
1442 | + |
1443 | +**proxy**: A dictionary containing http_proxy, https_proxy, and no_proxy. |
1444 | + |
1445 | +**Example**:: |
1446 | + |
1447 | + proxy: |
1448 | + http_proxy: http://squid.proxy:3728/ |
1449 | + https_proxy: http://squid.proxy:3728/ |
1450 | + no_proxy: localhost,127.0.0.1,10.0.2.1 |
1451 | + |
1452 | + |
1453 | reporting |
1454 | ~~~~~~~~~ |
1455 | Configure installation reporting (see Reporting section for details). |
1456 | |
1457 | === added file 'doc/topics/curthooks.rst' |
1458 | --- doc/topics/curthooks.rst 1970-01-01 00:00:00 +0000 |
1459 | +++ doc/topics/curthooks.rst 2017-10-06 01:09:48 +0000 |
1460 | @@ -0,0 +1,109 @@ |
1461 | +======================================== |
1462 | +Curthooks / New OS Support |
1463 | +======================================== |
1464 | +Curtin has built-in support for installation of Ubuntu. |
1465 | +Other operating systems are supported through a mechanism called |
1466 | +'curthooks' or 'curtin-hooks'. |
1467 | + |
1468 | +A curtin install runs through different stages. See the |
1469 | +:ref:`Stages <stages>` |
1470 | +documentation for function of each stage. |
1471 | +The stages communicate with each other via data in a working directory and |
1472 | +environment variables as described in |
1473 | +:ref:`Command Environment`. |
1474 | + |
1475 | +Curtin handles partitioning, filesystem creation and target filesystem |
1476 | +population for all operating systems. Curthooks are the mechanism provided |
1477 | +so that the operating system can customize itself before reboot. This |
1478 | +customization typically would need to include: |
1479 | + |
1480 | + - ensuring that appropriate device drivers are loaded on first boot |
1481 | + - consuming the network interfaces file and applying its declarations. |
1482 | + - ensuring that necessary packages are installed to utilize storage |
1483 | + configuration or networking configuration. |
1484 | + - making the system boot (running grub-install or equivalent). |
1485 | + |
1486 | +Image provided curtin-hooks |
1487 | +--------------------------- |
1488 | +An image provides curtin hooks support by containing a file |
1489 | +``/curtin/curtin-hooks``. |
1490 | + |
1491 | +If an Ubuntu image image contains this path it will override the builtin |
1492 | +curtin support. |
1493 | + |
1494 | +The ``curtin-hooks`` program should be executable in the filesystem and |
1495 | +will be executed without any arguments. It will be executed in the install |
1496 | +environment, *not* the target environment. A change of root to the |
1497 | +target environment can be done with ``curtin in-target``. |
1498 | + |
1499 | +The hook is provided with some environment variables that can be used |
1500 | +to find more information. See the :ref:`Command Environment` doc for |
1501 | +details. Specifically interesting to this stage are: |
1502 | + |
1503 | + - ``OUTPUT_NETWORK_CONFIG``: This is a path to the file created during |
1504 | + network discovery stage. |
1505 | + - ``OUTPUT_FSTAB``: This is a path to the file created during partitioning |
1506 | + stage. |
1507 | + - ``CONFIG``: This is a path to the curtin config file. It is provided so |
1508 | + that additional configuration could be provided through to the OS |
1509 | + customization. |
1510 | + |
1511 | +.. **TODO**: We should add 'PYTHON' or 'CURTIN_PYTHON' to this environment |
1512 | + so that the hook can easily run a python program with the same python |
1513 | + that curtin ran with (ie, python2 or python3). |
1514 | + |
1515 | + |
1516 | +Networking configuration |
1517 | +------------------------ |
1518 | +Access to the network configuration that is desired is inside the config |
1519 | +and is in the format described in :ref:`networking`. |
1520 | + |
1521 | +.. TODO: We should guarantee that the presence |
1522 | + of network config v1 in the file OUTPUT_NETWORK_CONFIG. |
1523 | + |
1524 | +The curtin-hooks program must read the configuration from the |
1525 | +path contained in ``OUTPUT_NETWORK_CONFIG`` and then set up |
1526 | +the installed system to use it. |
1527 | + |
1528 | +If the installed system has cloud-init at version 17.1 or higher, it may |
1529 | +be possible to simply copy this section into the target in |
1530 | +``/etc/cloud/cloud.cfg.d/`` and let cloud-init render the correct |
1531 | +networking on first boot. |
1532 | + |
1533 | +Storage configuration |
1534 | +--------------------- |
1535 | +Access to the storage configuration that was set up is inside the config |
1536 | +and is in the format described in :ref:`storage`. |
1537 | + |
1538 | +.. TODO: We should guarantee that the presence |
1539 | + of storage config v1 in the file OUTPUT_STORAGE_CONFIG. |
1540 | + This would mean the user would not have to pull it out |
1541 | + of CONFIG. We should guarantee its presence and format |
1542 | + even in the 'simple' path. |
1543 | + |
1544 | +To apply this storage configuration, the curthooks may need to: |
1545 | + |
1546 | + * update /etc/fstab to add the expected mounts entries. The environment |
1547 | + variable ``OUTPUT_FSTAB`` contains a path to a file that may be suitable |
1548 | + for use. |
1549 | + |
1550 | + * install any packages that are not already installed that are required |
1551 | + to boot with the provided storage config. For example, if the storage |
1552 | + layout includes raid you may need to install the mdadm package. |
1553 | + |
1554 | + * update or create an initramfs. |
1555 | + |
1556 | + |
1557 | +System boot |
1558 | +----------- |
1559 | +In Ubuntu, curtin will run 'grub-setup' and to install grub. This covers |
1560 | +putting the bootloader onto the disk(s) that are marked as |
1561 | +``grub_device``. The provided hook will need to do the equivalent |
1562 | +operation. |
1563 | + |
1564 | +finalize hook |
1565 | +------------- |
1566 | +There is one other hook that curtin will invoke in an install, called |
1567 | +``finalize``. This program is invoked in the same environment as |
1568 | +``curtin-hooks`` above. It is intended to give the OS a final opportunity |
1569 | +make updates before reboot. It is called before ``late_commands``. |
1570 | |
1571 | === modified file 'doc/topics/integration-testing.rst' |
1572 | --- doc/topics/integration-testing.rst 2017-05-19 20:56:27 +0000 |
1573 | +++ doc/topics/integration-testing.rst 2017-10-06 01:09:48 +0000 |
1574 | @@ -161,6 +161,12 @@ |
1575 | - ``logs``: install and boot logs |
1576 | - ``collect``: data collected by the boot phase |
1577 | |
1578 | +- ``CURTIN_VMTEST_TAR_DISKS``: default 0 |
1579 | + |
1580 | + Vmtest writes out disk image files sparsely into a disks directory |
1581 | + If this flag is set to a non-zero number, vmtest will tar all disks in |
1582 | + the directory into a single disks.tar and remove the sparse disk files. |
1583 | + |
1584 | - ``CURTIN_VMTEST_TOPDIR``: default $TMPDIR/vmtest-<timestamp> |
1585 | |
1586 | Vmtest puts all test data under this value. By default, it creates |
1587 | |
1588 | === modified file 'doc/topics/networking.rst' |
1589 | --- doc/topics/networking.rst 2016-09-29 18:31:02 +0000 |
1590 | +++ doc/topics/networking.rst 2017-10-06 01:09:48 +0000 |
1591 | @@ -1,3 +1,5 @@ |
1592 | +.. _networking: |
1593 | + |
1594 | ========== |
1595 | Networking |
1596 | ========== |
1597 | |
1598 | === modified file 'doc/topics/overview.rst' |
1599 | --- doc/topics/overview.rst 2016-09-29 18:31:02 +0000 |
1600 | +++ doc/topics/overview.rst 2017-10-06 01:09:48 +0000 |
1601 | @@ -4,6 +4,8 @@ |
1602 | |
1603 | Curtin is intended to be a bare bones "installer". Its goal is to take data from a source, and get it onto disk as quick as possible and then boot it. The key difference from traditional package based installers is that curtin assumes the thing its installing is intelligent and will do the right thing. |
1604 | |
1605 | +.. _Stages: |
1606 | + |
1607 | Stages |
1608 | ------ |
1609 | A usage of curtin will go through the following stages: |
1610 | @@ -22,6 +24,32 @@ |
1611 | |
1612 | Curtin's assumption is that a fairly rich Linux (Ubuntu) environment is booted. |
1613 | |
1614 | +.. _Command Environment: |
1615 | + |
1616 | +Command Environment |
1617 | +~~~~~~~~~~~~~~~~~~~ |
1618 | +Stages and commands invoked by curtin always have the following environment |
1619 | +variables defined. |
1620 | + |
1621 | +- ``WORKING_DIR``: This is for inter-command state. It will be the same |
1622 | + directory for each command run and will only be deleted at the end of the |
1623 | + install. Files referenced in other environment variables will be in |
1624 | + this directory. |
1625 | + |
1626 | +- ``TARGET_MOUNT_POINT``: The path in the filesystem where the target |
1627 | + filesystem will be mounted. |
1628 | + |
1629 | +- ``OUTPUT_NETWORK_CONFIG``: After the network discovery stage, this file |
1630 | + should contain networking config information that should then be written |
1631 | + to the target. |
1632 | + |
1633 | +- ``OUTPUT_FSTAB``: After partitioning and filesystem creation, this file |
1634 | + will contain fstab(5) style content representing mounts. |
1635 | + |
1636 | +- ``CONFIG``: This variable contains a path to a yaml formatted file with |
1637 | + the fully rendered config. |
1638 | + |
1639 | + |
1640 | Early Commands |
1641 | ~~~~~~~~~~~~~~ |
1642 | Early commands are executed on the system, and non-zero exit status will terminate the installation process. These commands are intended to be used for things like |
1643 | @@ -48,32 +76,23 @@ |
1644 | 10_wipe_filesystems: curtin wipe --quick --all-unused-disks |
1645 | 50_setup_raid: curtin disk-setup --all-disks raid0 / |
1646 | |
1647 | -**Command environment** |
1648 | - |
1649 | -Partitioning commands have the following environment variables available to them: |
1650 | - |
1651 | -- ``WORKING_DIR``: This is simply for some sort of inter-command state. It will be the same directory for each command run and will only be deleted at the end of all partitioning_commands. |
1652 | -- ``OUTPUT_FSTAB``: This is the target path for a fstab file. After all partitioning commands have been run, a file should exist, formatted per fstab(5) that describes how the filesystems should be mounted. |
1653 | -- ``TARGET_MOUNT_POINT``: |
1654 | - |
1655 | - |
1656 | -Network Discovery and Setup |
1657 | -~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
1658 | -Networking is done in a similar fashion to partitioning. A series of commands, specified in the config are run. At the end of these commands, a interfaces(5) style file is expected to be written to ``OUTPUT_INTERFACES``. |
1659 | - |
1660 | -Note, that as with fstab, this file is not copied verbatim to the target filesystem, but rather made available to the OS customization stage. That stage may just copy the file verbatim, but may also parse it, and use that as input. |
1661 | - |
1662 | -**Config Example**:: |
1663 | - |
1664 | - network_commands: |
1665 | - 10_netconf: curtin network copy-existing |
1666 | - |
1667 | -**Command environment** |
1668 | - |
1669 | -Networking commands have the following environment variables available to them: |
1670 | - |
1671 | -- ``WORKING_DIR``: This is simply for some sort of inter-command state. It will be the same directory for each command run and will only be deleted at the end of all network_commands. |
1672 | -- ``OUTPUT_INTERFACES``: This is the target path for an interfaces style file. After all commands have been run, a file should exist, formatted per interfaces(5) that describes the systems network setup. |
1673 | + |
1674 | +Network Discovery |
1675 | +~~~~~~~~~~~~~~~~~ |
1676 | +Networking configuration is *discovered* in the 'network' stage. |
1677 | +The default command run at this stage is ``curtin net-meta auto``. After |
1678 | +execution, it will write the discovered networking to the file specified |
1679 | +in the environment variable ``OUTPUT_NETWORK_CONFIG``. The format of this |
1680 | +file is as described in :ref:`networking`. |
1681 | + |
1682 | +If curtin's config has a network section, the net-meta will simply parrot the |
1683 | +data to the output file. If there is no network section, then its default |
1684 | +behavior is to copy existing config from the running environment. |
1685 | + |
1686 | +Note, that as with fstab, this file is not copied verbatim to the target |
1687 | +filesystem, but rather made available to the OS customization stage. That |
1688 | +stage may just copy the file verbatim, but may also parse it, and apply the |
1689 | +settings. |
1690 | |
1691 | Extraction of sources |
1692 | ~~~~~~~~~~~~~~~~~~~~~ |
1693 | @@ -88,27 +107,6 @@ |
1694 | |
1695 | wget $URL | tar -Sxvzf |
1696 | |
1697 | -Hook for installed OS to customize itself |
1698 | -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
1699 | -After extraction of sources, the source that was extracted is then given a chance to customize itself for the system. This customization may include: |
1700 | - - ensuring that appropriate device drivers are loaded on first boot |
1701 | - - consuming the network interfaces file and applying its declarations. |
1702 | - - ensuring that necessary packages |
1703 | - |
1704 | -**Config Example**:: |
1705 | - |
1706 | - config_hook: {{TARGET_MP}}/opt/curtin/config-hook |
1707 | - |
1708 | -**Command environment** |
1709 | - - ``INTERFACES``: This is a path to the file created during networking stage |
1710 | - - ``FSTAB``: This is a path to the file created during partitioning stage |
1711 | - - ``CONFIG``: This is a path to the curtin config file. It is provided so that additional configuration could be provided through to the OS customization. |
1712 | - |
1713 | -**Helpers** |
1714 | - |
1715 | -Curtin provides some helpers to make the OS customization easier. |
1716 | - - `curtin in-target`: run the command while chrooted into the target. |
1717 | - |
1718 | Final Commands |
1719 | ~~~~~~~~~~~~~~ |
1720 | |
1721 | |
1722 | === modified file 'doc/topics/reporting.rst' |
1723 | --- doc/topics/reporting.rst 2016-09-29 18:31:02 +0000 |
1724 | +++ doc/topics/reporting.rst 2017-10-06 01:09:48 +0000 |
1725 | @@ -10,6 +10,7 @@ |
1726 | Reporting consists of notification of a series of 'events. Each event has: |
1727 | - **event_type**: 'start' or 'finish' |
1728 | - **description**: human readable text |
1729 | + - **level**: the log level of the event, DEBUG/INFO/WARN etc. |
1730 | - **name**: and id for this event |
1731 | - **result**: only present when event_type is 'finish', its value is one of "SUCCESS", "WARN", or "FAIL". A result of WARN indicates something is likely wrong, but a non-fatal error. A result of "FAIL" is fatal. |
1732 | - **origin**: literal value 'curtin' |
1733 | @@ -75,6 +76,34 @@ |
1734 | is specified then all messages with a lower priority than specified will be |
1735 | ignored. Default is INFO. |
1736 | |
1737 | +Journald Reporter |
1738 | +----------------- |
1739 | + |
1740 | +The journald reporter sends the events to systemd's `journald`_. To enable, |
1741 | +provide curtin with config like:: |
1742 | + |
1743 | + reporting: |
1744 | + mylistener: |
1745 | + type: journald |
1746 | + identifier: "my_identifier" |
1747 | + level: DEBUG |
1748 | + |
1749 | +The event's fields are mapped to fields of the resulting journal entry |
1750 | +as follows: |
1751 | + |
1752 | +- **description** maps to **CURTIN_MESSAGE** |
1753 | +- **level** maps to **PRIORITY** |
1754 | +- **name** maps to **CURTIN_NAME** |
1755 | +- **event_type** maps to **CURTIN_EVENT_TYPE** |
1756 | +- **result**, if present, maps to **CURTIN_RESULT** |
1757 | + |
1758 | +The configured `identifier`, which defaults to "curtin_event", becomes |
1759 | +the entry's **SYSLOG_IDENTIFIER**. |
1760 | + |
1761 | +The python-systemd package must be installed to use this handler. |
1762 | + |
1763 | +.. _`journald`: https://www.freedesktop.org/software/systemd/man/systemd-journald.service.html |
1764 | + |
1765 | Example Events |
1766 | ~~~~~~~~~~~~~~ |
1767 | The following is an example event that would be posted:: |
1768 | |
1769 | === modified file 'doc/topics/storage.rst' |
1770 | --- doc/topics/storage.rst 2017-05-19 20:56:27 +0000 |
1771 | +++ doc/topics/storage.rst 2017-10-06 01:09:48 +0000 |
1772 | @@ -1,3 +1,5 @@ |
1773 | +.. _storage: |
1774 | + |
1775 | ======= |
1776 | Storage |
1777 | ======= |
1778 | |
1779 | === modified file 'examples/network-ipv6-bond-vlan.yaml' |
1780 | --- examples/network-ipv6-bond-vlan.yaml 2016-09-29 18:31:02 +0000 |
1781 | +++ examples/network-ipv6-bond-vlan.yaml 2017-10-06 01:09:48 +0000 |
1782 | @@ -3,10 +3,10 @@ |
1783 | config: |
1784 | - name: interface0 |
1785 | type: physical |
1786 | - mac_address: BC:76:4E:06:96:B3 |
1787 | + mac_address: bc:76:4e:06:96:b3 |
1788 | - name: interface1 |
1789 | type: physical |
1790 | - mac_address: BC:76:4E:04:88:41 |
1791 | + mac_address: bc:76:4e:04:88:41 |
1792 | - type: bond |
1793 | bond_interfaces: |
1794 | - interface0 |
1795 | |
1796 | === modified file 'examples/tests/bonding_network.yaml' |
1797 | --- examples/tests/bonding_network.yaml 2016-07-12 16:28:59 +0000 |
1798 | +++ examples/tests/bonding_network.yaml 2017-10-06 01:09:48 +0000 |
1799 | @@ -16,8 +16,7 @@ |
1800 | mac_address: "52:54:00:12:34:04" |
1801 | # Bond. |
1802 | - type: bond |
1803 | - name: bond0 |
1804 | - mac_address: "52:54:00:12:34:06" |
1805 | + name: bond1 |
1806 | bond_interfaces: |
1807 | - interface1 |
1808 | - interface2 |
1809 | @@ -26,8 +25,6 @@ |
1810 | subnets: |
1811 | - type: static |
1812 | address: 10.23.23.2/24 |
1813 | - - type: static |
1814 | - address: 10.23.24.2/24 |
1815 | |
1816 | curthooks_commands: |
1817 | # use curtin to disable open-iscsi ifupdown hooks for precise; they're |
1818 | |
1819 | === modified file 'examples/tests/centos_basic.yaml' |
1820 | --- examples/tests/centos_basic.yaml 2016-12-02 02:04:27 +0000 |
1821 | +++ examples/tests/centos_basic.yaml 2017-10-06 01:09:48 +0000 |
1822 | @@ -9,5 +9,6 @@ |
1823 | mac_address: "52:54:00:12:34:00" |
1824 | subnets: |
1825 | - type: static |
1826 | - address: 10.0.2.15/24 |
1827 | + address: 10.0.2.15 |
1828 | + netmask: 255.255.255.0 |
1829 | gateway: 10.0.2.2 |
1830 | |
1831 | === added file 'examples/tests/centos_defaults.yaml' |
1832 | --- examples/tests/centos_defaults.yaml 1970-01-01 00:00:00 +0000 |
1833 | +++ examples/tests/centos_defaults.yaml 2017-10-06 01:09:48 +0000 |
1834 | @@ -0,0 +1,91 @@ |
1835 | +hook_commands: |
1836 | + builtin: null |
1837 | + |
1838 | +# To force curtin to run centos_apply_network_config vmtest, uncomment |
1839 | +# _ammend_centos_curthooks: True |
1840 | + |
1841 | +write_files: |
1842 | + grub_serial_console: |
1843 | + path: '/root/curtin-send-console-to-serial' |
1844 | + permissions: '0755' |
1845 | + owner: 'root:root' |
1846 | + content: | |
1847 | + # update grub1 and grub2 configs to write to serial console. |
1848 | + CONPARM="console=ttyS0,115200" |
1849 | + grub1conf="/boot/grub/grub.conf" |
1850 | + grub2conf="/boot/grub2/grub.cfg" |
1851 | + grub2def="/etc/default/grub" |
1852 | + |
1853 | + rerror() { perror "$?" "$@"; return $r; } |
1854 | + perror() { local r="$1"; shift; error "$@"; return $r; } |
1855 | + error() { echo "GRUB_SERIAL:" "ERROR:" "$@" 1>&2; } |
1856 | + info() { echo "GRUB_SERIAL:" "$@" 1>&2; } |
1857 | + fail() { error "$@"; exit 1; } |
1858 | + bk() { |
1859 | + local ofile="$1" bk="$1.dist.curtin" |
1860 | + shift |
1861 | + [ -e "$ofile" ] || return 0 |
1862 | + cp "$ofile" "$bk" || rerror "failed backup ($ofile -> $bk):" "$@"; |
1863 | + } |
1864 | + |
1865 | + update_grub1() { |
1866 | + local cfg="$1" r="" |
1867 | + [ -e "$cfg" ] || |
1868 | + { info "no grub1 cfg '$cfg'"; return 0; } |
1869 | + bk "$cfg" "grub1 config" || return |
1870 | + if ! grep "^serial" "$cfg"; then |
1871 | + cat >> "$cfg" <<EOF |
1872 | + #curtin added |
1873 | + serial --unit=0 --speed=115200 |
1874 | + terminal --timeout=2 serial console |
1875 | + EOF |
1876 | + r=$? |
1877 | + [ $r -eq 0 ] || |
1878 | + { perror $r "failed to append to grub1 cfg '$cfg'"; return; } |
1879 | + fi |
1880 | + sed -i -e '/linux16/n' -e '/console=/n' \ |
1881 | + -e "s/root=\([^ ]*\)/root=\1 ${CONPARM}/" "$cfg" || |
1882 | + { rerror "failed to update grub1 cfg '$cfg'."; return; } |
1883 | + info "updated grub1 cfg '$cfg'." |
1884 | + } |
1885 | + |
1886 | + update_grub2() { |
1887 | + local cfg="$1" defgrub="$2" |
1888 | + [ -e "$cfg" ] || { info "no grub2 config '$cfg'"; return 0; } |
1889 | + bk "$cfg" "grub2 config" || return |
1890 | + sed -i -e '/kernel/n' -e '/console=/n' \ |
1891 | + -e "s/root=\([^ ]*\)/root=\1 ${CONPARM}/" "$cfg" || |
1892 | + { rerror "failed to update grub2 '$cfg'"; return; } |
1893 | + |
1894 | + # update /etc/default/grub. any GRUB_CMDLINE_LINUX remove |
1895 | + # any console= and add conparm at the beginning. |
1896 | + local var="GRUB_CMDLINE_LINUX" msg="updated grub2 '$cfg'." |
1897 | + if [ ! -e "$defgrub" ]; then |
1898 | + msg="$msg. no defaults file '$defgrub'." |
1899 | + else |
1900 | + bk "$defgrub" "grub2 defaults file" || return |
1901 | + msg="$msg. updated defaults file '$defgrub'." |
1902 | + sed -i \ |
1903 | + -e "/$var=/!n" \ |
1904 | + -e 's/console=[^ "]*//g' \ |
1905 | + -e "s/$var=\"/$var=\"${CONPARM}/" "$defgrub" || |
1906 | + { rerror "grub2 default update failed on $defgrub"; return; } |
1907 | + fi |
1908 | + info "$msg" |
1909 | + } |
1910 | + |
1911 | + update_grub1 "$grub1conf" || fail "failed update grub1" |
1912 | + update_grub2 "$grub2conf" "$grub2def" || fail "failed update grub2" |
1913 | + |
1914 | +late_commands: |
1915 | + # centos66 images include grub 0.97 which will detect vmtests' ephemeral disk |
1916 | + # and the install disk which leaves grub configured with two disks. When |
1917 | + # vmtest reboots into installed disk, there is only one disk and the grub |
1918 | + # map is no longer valid. Here in 00_grub, we switch hd1 to hd0. MAAS |
1919 | + # is not affected as their ephemeral image (iscsi or http) is not discovered |
1920 | + # by grub and therefor the device.map doesn't contain a second device. Cent7 |
1921 | + # has grub2 which uses root by UUID |
1922 | + 00_grub1_boot: [curtin, in-target, --, sed, -i.curtin, -e, |
1923 | + 's|(hd1,0)|(hd0,0)|g', /boot/grub/grub.conf] |
1924 | + # vmtest wants output to go to serial console so we update grub inside. |
1925 | + 00_grub_serial: [curtin, in-target, --, '/root/curtin-send-console-to-serial'] |
1926 | |
1927 | === added file 'examples/tests/journald_reporter.yaml' |
1928 | --- examples/tests/journald_reporter.yaml 1970-01-01 00:00:00 +0000 |
1929 | +++ examples/tests/journald_reporter.yaml 2017-10-06 01:09:48 +0000 |
1930 | @@ -0,0 +1,20 @@ |
1931 | +reporting: |
1932 | + journald: |
1933 | + type: journald |
1934 | + level: DEBUG |
1935 | + |
1936 | +journal_cmds: |
1937 | + - ©_journal_log | |
1938 | + journalctl -b -o short-precise --no-pager -t curtin_event \ |
1939 | + > ${TARGET_MOUNT_POINT}/root/journalctl.curtin_events.log |
1940 | + |
1941 | + # use sed to make the json file loadable (listify the json) |
1942 | + - ©_journal_json | |
1943 | + journalctl -b -o json-pretty --no-pager -t curtin_event \ |
1944 | + | sed -e '1i [' -e 's|^}|},|g' -e '$s|^},|}|' -e '$a]' \ |
1945 | + > ${TARGET_MOUNT_POINT}/root/journalctl.curtin_events.json |
1946 | + |
1947 | +# extract the journald entries for curtin |
1948 | +late_commands: |
1949 | + 00_copy_journal__log: [sh, -c, *copy_journal_log] |
1950 | + 01_copy_journal_json: [sh, -c, *copy_journal_json] |
1951 | |
1952 | === modified file 'examples/tests/network_alias.yaml' |
1953 | --- examples/tests/network_alias.yaml 2016-09-29 18:31:02 +0000 |
1954 | +++ examples/tests/network_alias.yaml 2017-10-06 01:09:48 +0000 |
1955 | @@ -8,29 +8,27 @@ |
1956 | mac_address: "52:54:00:12:34:00" |
1957 | subnets: |
1958 | - type: static |
1959 | - address: 192.168.1.2/24 |
1960 | - mtu: 1501 |
1961 | + address: 10.47.98.1/24 |
1962 | - type: static |
1963 | address: 2001:4800:78ff:1b:be76:4eff:fe06:ffac |
1964 | netmask: 'ffff:ffff:ffff:ffff::' |
1965 | - mtu: 1480 |
1966 | # multi_v4_alias: multiple v4 addrs on same interface |
1967 | - type: physical |
1968 | name: interface1 |
1969 | mac_address: "52:54:00:12:34:02" |
1970 | subnets: |
1971 | - type: static |
1972 | - address: 192.168.2.2/22 |
1973 | + address: 192.168.20.2/24 |
1974 | routes: |
1975 | - - network: 192.168.0.0 |
1976 | - netmask: 255.255.252.0 |
1977 | - gateway: 192.168.2.1 |
1978 | + - gateway: 192.168.20.1 |
1979 | + netmask: 255.255.255.0 |
1980 | + network: 10.242.47.0 |
1981 | - type: static |
1982 | - address: 10.23.23.7/23 |
1983 | + address: 10.23.22.7/23 |
1984 | routes: |
1985 | - - gateway: 10.23.23.1 |
1986 | - netmask: 255.255.254.0 |
1987 | - network: 10.23.22.0 |
1988 | + - gateway: 10.23.22.2 |
1989 | + netmask: 255.255.255.0 |
1990 | + network: 10.49.253.0 |
1991 | # multi_v6_alias: multiple v6 addrs on same interface |
1992 | - type: physical |
1993 | name: interface2 |
1994 | @@ -51,17 +49,17 @@ |
1995 | mac_address: "52:54:00:12:34:06" |
1996 | subnets: |
1997 | - type: static |
1998 | - address: 192.168.7.7/22 |
1999 | + address: 192.168.80.8/24 |
2000 | routes: |
2001 | - - network: 192.168.0.0 |
2002 | - netmask: 255.255.252.0 |
2003 | - gateway: 192.168.7.1 |
2004 | + - gateway: 192.168.80.1 |
2005 | + netmask: 255.255.255.0 |
2006 | + network: 10.189.34.0 |
2007 | - type: static |
2008 | - address: 10.99.99.23/23 |
2009 | + address: 10.99.10.23/23 |
2010 | routes: |
2011 | - - gateway: 10.99.99.1 |
2012 | - netmask: 255.255.254.0 |
2013 | - network: 10.99.98.0 |
2014 | + - gateway: 10.99.10.1 |
2015 | + netmask: 255.255.255.0 |
2016 | + network: 10.77.154.0 |
2017 | - type: static |
2018 | address: 2001:4800:78ff:1b:be76:4eff:beef:4000 |
2019 | netmask: 'ffff:ffff:ffff:ffff::' |
2020 | @@ -86,17 +84,17 @@ |
2021 | address: 2001:4800:78ff:1b:be76:4eff:debe:9000 |
2022 | netmask: 'ffff:ffff:ffff:ffff::' |
2023 | - type: static |
2024 | - address: 192.168.100.100/22 |
2025 | + address: 192.168.100.100/24 |
2026 | routes: |
2027 | - - network: 192.168.0.0 |
2028 | - netmask: 255.255.252.0 |
2029 | - gateway: 192.168.100.1 |
2030 | + - gateway: 192.168.100.1 |
2031 | + netmask: 255.255.255.0 |
2032 | + network: 10.28.219.0 |
2033 | - type: static |
2034 | address: 10.17.142.2/23 |
2035 | routes: |
2036 | - gateway: 10.17.142.1 |
2037 | - netmask: 255.255.254.0 |
2038 | - network: 10.17.142.0 |
2039 | + netmask: 255.255.255.0 |
2040 | + network: 10.82.49.0 |
2041 | # multi_v6_and_v4_mix_order: multiple v4 and v6 addr, mixed order |
2042 | - type: physical |
2043 | name: interface5 |
2044 | @@ -109,17 +107,17 @@ |
2045 | address: 2001:4800:78ff:1b:be76:4eff:baaf:c000 |
2046 | netmask: 'ffff:ffff:ffff:ffff::' |
2047 | - type: static |
2048 | - address: 192.168.200.200/22 |
2049 | + address: 192.168.200.200/24 |
2050 | routes: |
2051 | - - network: 192.168.0.0 |
2052 | - netmask: 255.255.252.0 |
2053 | - gateway: 192.168.200.1 |
2054 | + - gateway: 192.168.200.1 |
2055 | + netmask: 255.255.255.0 |
2056 | + network: 10.71.23.0 |
2057 | - type: static |
2058 | address: 10.252.2.2/23 |
2059 | routes: |
2060 | - gateway: 10.252.2.1 |
2061 | - netmask: 255.255.254.0 |
2062 | - network: 10.252.2.0 |
2063 | + netmask: 255.255.255.0 |
2064 | + network: 10.3.7.0 |
2065 | - type: static |
2066 | address: 2001:4800:78ff:1b:be76:4eff:baaf:b000 |
2067 | netmask: 'ffff:ffff:ffff:ffff::' |
2068 | |
2069 | === modified file 'examples/tests/network_static_routes.yaml' |
2070 | --- examples/tests/network_static_routes.yaml 2017-02-08 20:25:39 +0000 |
2071 | +++ examples/tests/network_static_routes.yaml 2017-10-06 01:09:48 +0000 |
2072 | @@ -10,18 +10,13 @@ |
2073 | - address: 172.23.31.42/26 |
2074 | gateway: 172.23.31.2 |
2075 | type: static |
2076 | - - type: route |
2077 | - id: 4 |
2078 | - metric: 0 |
2079 | - destination: 10.0.0.0/12 |
2080 | - gateway: 172.23.31.1 |
2081 | - - type: route |
2082 | - id: 5 |
2083 | - metric: 0 |
2084 | - destination: 192.168.0.0/16 |
2085 | - gateway: 172.23.31.1 |
2086 | - - type: route |
2087 | - id: 6 |
2088 | - metric: 1 |
2089 | - destination: 10.200.0.0/16 |
2090 | - gateway: 172.23.31.1 |
2091 | + routes: |
2092 | + - gateway: 172.23.31.1 |
2093 | + network: 10.0.0.0/12 |
2094 | + metric: 0 |
2095 | + - gateway: 172.23.31.1 |
2096 | + network: 192.168.0.0/16 |
2097 | + metric: 0 |
2098 | + - gateway: 172.23.31.1 |
2099 | + network: 10.200.0.0/16 |
2100 | + metric: 1 |
2101 | |
2102 | === added file 'examples/tests/network_v2_passthrough.yaml' |
2103 | --- examples/tests/network_v2_passthrough.yaml 1970-01-01 00:00:00 +0000 |
2104 | +++ examples/tests/network_v2_passthrough.yaml 2017-10-06 01:09:48 +0000 |
2105 | @@ -0,0 +1,8 @@ |
2106 | +showtrace: true |
2107 | +network: |
2108 | + version: 2 |
2109 | + ethernets: |
2110 | + interface0: |
2111 | + match: |
2112 | + mac_address: "52:54:00:12:34:00" |
2113 | + dhcp4: true |
2114 | |
2115 | === modified file 'setup.py' |
2116 | --- setup.py 2016-09-29 18:31:02 +0000 |
2117 | +++ setup.py 2017-10-06 01:09:48 +0000 |
2118 | @@ -1,6 +1,7 @@ |
2119 | from distutils.core import setup |
2120 | from glob import glob |
2121 | import os |
2122 | +import sys |
2123 | |
2124 | import curtin |
2125 | |
2126 | @@ -8,6 +9,19 @@ |
2127 | def is_f(p): |
2128 | return os.path.isfile(p) |
2129 | |
2130 | + |
2131 | +def in_virtualenv(): |
2132 | + try: |
2133 | + if sys.real_prefix == sys.prefix: |
2134 | + return False |
2135 | + else: |
2136 | + return True |
2137 | + except AttributeError: |
2138 | + return False |
2139 | + |
2140 | + |
2141 | +USR = "usr" if in_virtualenv() else "/usr" |
2142 | + |
2143 | setup( |
2144 | name="curtin", |
2145 | description='The curtin installer', |
2146 | @@ -27,9 +41,9 @@ |
2147 | ], |
2148 | scripts=glob('bin/*'), |
2149 | data_files=[ |
2150 | - ('/usr/share/doc/curtin', |
2151 | + (USR + '/share/doc/curtin', |
2152 | [f for f in glob('doc/*') if is_f(f)]), |
2153 | - ('/usr/lib/curtin/helpers', |
2154 | + (USR + '/lib/curtin/helpers', |
2155 | [f for f in glob('helpers/*') if is_f(f)]) |
2156 | ] |
2157 | ) |
2158 | |
2159 | === modified file 'tests/unittests/helpers.py' |
2160 | --- tests/unittests/helpers.py 2017-02-08 20:25:39 +0000 |
2161 | +++ tests/unittests/helpers.py 2017-10-06 01:09:48 +0000 |
2162 | @@ -19,6 +19,10 @@ |
2163 | import imp |
2164 | import importlib |
2165 | import mock |
2166 | +import os |
2167 | +import shutil |
2168 | +import tempfile |
2169 | +from unittest import TestCase |
2170 | |
2171 | |
2172 | def builtin_module_name(): |
2173 | @@ -43,3 +47,35 @@ |
2174 | m_patch = '{}.open'.format(mod_name) |
2175 | with mock.patch(m_patch, m_open, create=True): |
2176 | yield m_open |
2177 | + |
2178 | + |
2179 | +class CiTestCase(TestCase): |
2180 | + """Common testing class which all curtin unit tests subclass.""" |
2181 | + |
2182 | + def add_patch(self, target, attr, **kwargs): |
2183 | + """Patches specified target object and sets it as attr on test |
2184 | + instance also schedules cleanup""" |
2185 | + if 'autospec' not in kwargs: |
2186 | + kwargs['autospec'] = True |
2187 | + m = mock.patch(target, **kwargs) |
2188 | + p = m.start() |
2189 | + self.addCleanup(m.stop) |
2190 | + setattr(self, attr, p) |
2191 | + |
2192 | + def tmp_dir(self, dir=None, cleanup=True): |
2193 | + """Return a full path to a temporary directory for the test run.""" |
2194 | + if dir is None: |
2195 | + tmpd = tempfile.mkdtemp( |
2196 | + prefix="curtin-ci-%s." % self.__class__.__name__) |
2197 | + else: |
2198 | + tmpd = tempfile.mkdtemp(dir=dir) |
2199 | + self.addCleanup(shutil.rmtree, tmpd) |
2200 | + return tmpd |
2201 | + |
2202 | + def tmp_path(self, path, _dir=None): |
2203 | + # return an absolute path to 'path' under dir. |
2204 | + # if dir is None, one will be created with tmp_dir() |
2205 | + # the file is not created or modified. |
2206 | + if _dir is None: |
2207 | + _dir = self.tmp_dir() |
2208 | + return os.path.normpath(os.path.abspath(os.path.join(_dir, path))) |
2209 | |
2210 | === modified file 'tests/unittests/test_apt_custom_sources_list.py' |
2211 | --- tests/unittests/test_apt_custom_sources_list.py 2016-08-05 20:47:14 +0000 |
2212 | +++ tests/unittests/test_apt_custom_sources_list.py 2017-10-06 01:09:48 +0000 |
2213 | @@ -3,10 +3,7 @@ |
2214 | """ |
2215 | import logging |
2216 | import os |
2217 | -import shutil |
2218 | -import tempfile |
2219 | |
2220 | -from unittest import TestCase |
2221 | |
2222 | import yaml |
2223 | import mock |
2224 | @@ -14,6 +11,7 @@ |
2225 | |
2226 | from curtin import util |
2227 | from curtin.commands import apt_config |
2228 | +from .helpers import CiTestCase |
2229 | |
2230 | LOG = logging.getLogger(__name__) |
2231 | |
2232 | @@ -85,12 +83,11 @@ |
2233 | """) |
2234 | |
2235 | |
2236 | -class TestAptSourceConfigSourceList(TestCase): |
2237 | +class TestAptSourceConfigSourceList(CiTestCase): |
2238 | """TestAptSourceConfigSourceList - Class to test sources list rendering""" |
2239 | def setUp(self): |
2240 | super(TestAptSourceConfigSourceList, self).setUp() |
2241 | - self.new_root = tempfile.mkdtemp() |
2242 | - self.addCleanup(shutil.rmtree, self.new_root) |
2243 | + self.new_root = self.tmp_dir() |
2244 | # self.patchUtils(self.new_root) |
2245 | |
2246 | @staticmethod |
2247 | |
2248 | === modified file 'tests/unittests/test_apt_source.py' |
2249 | --- tests/unittests/test_apt_source.py 2017-02-28 15:26:03 +0000 |
2250 | +++ tests/unittests/test_apt_source.py 2017-10-06 01:09:48 +0000 |
2251 | @@ -4,11 +4,8 @@ |
2252 | import glob |
2253 | import os |
2254 | import re |
2255 | -import shutil |
2256 | import socket |
2257 | -import tempfile |
2258 | |
2259 | -from unittest import TestCase |
2260 | |
2261 | import mock |
2262 | from mock import call |
2263 | @@ -16,6 +13,7 @@ |
2264 | from curtin import util |
2265 | from curtin import gpg |
2266 | from curtin.commands import apt_config |
2267 | +from .helpers import CiTestCase |
2268 | |
2269 | |
2270 | EXPECTEDKEY = u"""-----BEGIN PGP PUBLIC KEY BLOCK----- |
2271 | @@ -62,14 +60,13 @@ |
2272 | ChrootableTargetStr = "curtin.commands.apt_config.util.ChrootableTarget" |
2273 | |
2274 | |
2275 | -class TestAptSourceConfig(TestCase): |
2276 | +class TestAptSourceConfig(CiTestCase): |
2277 | """ TestAptSourceConfig |
2278 | Main Class to test apt configs |
2279 | """ |
2280 | def setUp(self): |
2281 | super(TestAptSourceConfig, self).setUp() |
2282 | - self.tmp = tempfile.mkdtemp() |
2283 | - self.addCleanup(shutil.rmtree, self.tmp) |
2284 | + self.tmp = self.tmp_dir() |
2285 | self.aptlistfile = os.path.join(self.tmp, "single-deb.list") |
2286 | self.aptlistfile2 = os.path.join(self.tmp, "single-deb2.list") |
2287 | self.aptlistfile3 = os.path.join(self.tmp, "single-deb3.list") |
2288 | @@ -930,7 +927,7 @@ |
2289 | orig, apt_config.disable_suites(["proposed"], orig, rel)) |
2290 | |
2291 | |
2292 | -class TestDebconfSelections(TestCase): |
2293 | +class TestDebconfSelections(CiTestCase): |
2294 | |
2295 | @mock.patch("curtin.commands.apt_config.debconf_set_selections") |
2296 | def test_no_set_sel_if_none_to_set(self, m_set_sel): |
2297 | |
2298 | === modified file 'tests/unittests/test_basic.py' |
2299 | --- tests/unittests/test_basic.py 2013-07-29 16:12:09 +0000 |
2300 | +++ tests/unittests/test_basic.py 2017-10-06 01:09:48 +0000 |
2301 | @@ -1,7 +1,7 @@ |
2302 | -from unittest import TestCase |
2303 | - |
2304 | - |
2305 | -class TestImport(TestCase): |
2306 | +from .helpers import CiTestCase |
2307 | + |
2308 | + |
2309 | +class TestImport(CiTestCase): |
2310 | def test_import(self): |
2311 | import curtin |
2312 | self.assertFalse(getattr(curtin, 'BOGUS_ENTRY', None)) |
2313 | |
2314 | === modified file 'tests/unittests/test_block.py' |
2315 | --- tests/unittests/test_block.py 2017-06-12 19:43:55 +0000 |
2316 | +++ tests/unittests/test_block.py 2017-10-06 01:09:48 +0000 |
2317 | @@ -1,19 +1,16 @@ |
2318 | -from unittest import TestCase |
2319 | import functools |
2320 | import os |
2321 | import mock |
2322 | -import tempfile |
2323 | -import shutil |
2324 | import sys |
2325 | |
2326 | from collections import OrderedDict |
2327 | |
2328 | -from .helpers import simple_mocked_open |
2329 | +from .helpers import CiTestCase, simple_mocked_open |
2330 | from curtin import util |
2331 | from curtin import block |
2332 | |
2333 | |
2334 | -class TestBlock(TestCase): |
2335 | +class TestBlock(CiTestCase): |
2336 | |
2337 | @mock.patch("curtin.block.util") |
2338 | def test_get_volume_uuid(self, mock_util): |
2339 | @@ -103,7 +100,7 @@ |
2340 | block.lookup_disk(serial) |
2341 | |
2342 | |
2343 | -class TestSysBlockPath(TestCase): |
2344 | +class TestSysBlockPath(CiTestCase): |
2345 | @mock.patch("curtin.block.get_blockdev_for_partition") |
2346 | @mock.patch("os.path.exists") |
2347 | def test_existing_valid_devname(self, m_os_path_exists, m_get_blk): |
2348 | @@ -177,19 +174,13 @@ |
2349 | block.sys_block_path('/dev/cciss/c0d0p1')) |
2350 | |
2351 | |
2352 | -class TestWipeFile(TestCase): |
2353 | +class TestWipeFile(CiTestCase): |
2354 | def __init__(self, *args, **kwargs): |
2355 | super(TestWipeFile, self).__init__(*args, **kwargs) |
2356 | |
2357 | - def tfile(self, *args): |
2358 | - # return a temp file in a dir that will be cleaned up |
2359 | - tmpdir = tempfile.mkdtemp() |
2360 | - self.addCleanup(shutil.rmtree, tmpdir) |
2361 | - return os.path.sep.join([tmpdir] + list(args)) |
2362 | - |
2363 | def test_non_exist_raises_file_not_found(self): |
2364 | try: |
2365 | - p = self.tfile("enofile") |
2366 | + p = self.tmp_path("enofile") |
2367 | block.wipe_file(p) |
2368 | raise Exception("%s did not raise exception" % p) |
2369 | except Exception as e: |
2370 | @@ -198,7 +189,7 @@ |
2371 | |
2372 | def test_non_exist_dir_raises_file_not_found(self): |
2373 | try: |
2374 | - p = self.tfile("enodir", "file") |
2375 | + p = self.tmp_path(os.path.sep.join(["enodir", "file"])) |
2376 | block.wipe_file(p) |
2377 | raise Exception("%s did not raise exception" % p) |
2378 | except Exception as e: |
2379 | @@ -207,7 +198,7 @@ |
2380 | |
2381 | def test_default_is_zero(self): |
2382 | flen = 1024 |
2383 | - myfile = self.tfile("def_zero") |
2384 | + myfile = self.tmp_path("def_zero") |
2385 | util.write_file(myfile, flen * b'\1', omode="wb") |
2386 | block.wipe_file(myfile) |
2387 | found = util.load_file(myfile, decode=False) |
2388 | @@ -219,7 +210,7 @@ |
2389 | def reader(size): |
2390 | return size * b'\1' |
2391 | |
2392 | - myfile = self.tfile("reader_used") |
2393 | + myfile = self.tmp_path("reader_used") |
2394 | # populate with nulls |
2395 | util.write_file(myfile, flen * b'\0', omode="wb") |
2396 | block.wipe_file(myfile, reader=reader, buflen=flen) |
2397 | @@ -236,15 +227,15 @@ |
2398 | data['x'] = data['x'][size:] |
2399 | return buf |
2400 | |
2401 | - myfile = self.tfile("reader_twice") |
2402 | + myfile = self.tmp_path("reader_twice") |
2403 | util.write_file(myfile, flen * b'\xff', omode="wb") |
2404 | block.wipe_file(myfile, reader=reader, buflen=20) |
2405 | found = util.load_file(myfile, decode=False) |
2406 | self.assertEqual(found, expected) |
2407 | |
2408 | def test_reader_fhandle(self): |
2409 | - srcfile = self.tfile("fhandle_src") |
2410 | - trgfile = self.tfile("fhandle_trg") |
2411 | + srcfile = self.tmp_path("fhandle_src") |
2412 | + trgfile = self.tmp_path("fhandle_trg") |
2413 | data = '\n'.join(["this is source file." for f in range(0, 10)] + []) |
2414 | util.write_file(srcfile, data) |
2415 | util.write_file(trgfile, 'a' * len(data)) |
2416 | @@ -254,7 +245,7 @@ |
2417 | self.assertEqual(data, found) |
2418 | |
2419 | def test_exclusive_open_raise_missing(self): |
2420 | - myfile = self.tfile("no-such-file") |
2421 | + myfile = self.tmp_path("no-such-file") |
2422 | |
2423 | with self.assertRaises(ValueError): |
2424 | with block.exclusive_open(myfile) as fp: |
2425 | @@ -265,7 +256,7 @@ |
2426 | @mock.patch('os.open') |
2427 | def test_exclusive_open(self, mock_os_open, mock_os_fdopen, mock_os_close): |
2428 | flen = 1024 |
2429 | - myfile = self.tfile("my_exclusive_file") |
2430 | + myfile = self.tmp_path("my_exclusive_file") |
2431 | util.write_file(myfile, flen * b'\1', omode="wb") |
2432 | mock_fd = 3 |
2433 | mock_os_open.return_value = mock_fd |
2434 | @@ -288,7 +279,7 @@ |
2435 | mock_os_close, |
2436 | mock_util_fuser): |
2437 | flen = 1024 |
2438 | - myfile = self.tfile("my_exclusive_file") |
2439 | + myfile = self.tmp_path("my_exclusive_file") |
2440 | util.write_file(myfile, flen * b'\1', omode="wb") |
2441 | mock_os_open.side_effect = OSError("NO_O_EXCL") |
2442 | mock_holders.return_value = ['md1'] |
2443 | @@ -310,7 +301,7 @@ |
2444 | def test_exclusive_open_fdopen_failure(self, mock_os_open, |
2445 | mock_os_fdopen, mock_os_close): |
2446 | flen = 1024 |
2447 | - myfile = self.tfile("my_exclusive_file") |
2448 | + myfile = self.tmp_path("my_exclusive_file") |
2449 | util.write_file(myfile, flen * b'\1', omode="wb") |
2450 | mock_fd = 3 |
2451 | mock_os_open.return_value = mock_fd |
2452 | @@ -328,7 +319,7 @@ |
2453 | self.assertEqual([], mock_os_close.call_args_list) |
2454 | |
2455 | |
2456 | -class TestWipeVolume(TestCase): |
2457 | +class TestWipeVolume(CiTestCase): |
2458 | dev = '/dev/null' |
2459 | |
2460 | @mock.patch('curtin.block.lvm') |
2461 | @@ -366,7 +357,7 @@ |
2462 | block.wipe_volume(self.dev, mode='invalidmode') |
2463 | |
2464 | |
2465 | -class TestBlockKnames(TestCase): |
2466 | +class TestBlockKnames(CiTestCase): |
2467 | """Tests for some of the kname functions in block""" |
2468 | def test_determine_partition_kname(self): |
2469 | part_knames = [(('sda', 1), 'sda1'), |
2470 | @@ -430,7 +421,7 @@ |
2471 | block.kname_to_path(kname) |
2472 | |
2473 | |
2474 | -class TestPartTableSignature(TestCase): |
2475 | +class TestPartTableSignature(CiTestCase): |
2476 | blockdev = '/dev/null' |
2477 | dos_content = b'\x00' * 0x1fe + b'\x55\xAA' + b'\x00' * 0xf00 |
2478 | gpt_content = b'\x00' * 0x200 + b'EFI PART' + b'\x00' * (0x200 - 8) |
2479 | @@ -493,7 +484,7 @@ |
2480 | block.check_efi_signature(self.blockdev)) |
2481 | |
2482 | |
2483 | -class TestNonAscii(TestCase): |
2484 | +class TestNonAscii(CiTestCase): |
2485 | @mock.patch('curtin.block.util.subp') |
2486 | def test_lsblk(self, mock_subp): |
2487 | # lsblk can write non-ascii data, causing shlex to blow up |
2488 | @@ -519,14 +510,7 @@ |
2489 | block.blkid() |
2490 | |
2491 | |
2492 | -class TestSlaveKnames(TestCase): |
2493 | - def add_patch(self, target, attr, autospec=True): |
2494 | - """Patches specified target object and sets it as attr on test |
2495 | - instance also schedules cleanup""" |
2496 | - m = mock.patch(target, autospec=autospec) |
2497 | - p = m.start() |
2498 | - self.addCleanup(m.stop) |
2499 | - setattr(self, attr, p) |
2500 | +class TestSlaveKnames(CiTestCase): |
2501 | |
2502 | def setUp(self): |
2503 | super(TestSlaveKnames, self).setUp() |
2504 | |
2505 | === modified file 'tests/unittests/test_block_iscsi.py' |
2506 | --- tests/unittests/test_block_iscsi.py 2017-05-19 20:56:27 +0000 |
2507 | +++ tests/unittests/test_block_iscsi.py 2017-10-06 01:09:48 +0000 |
2508 | @@ -1,23 +1,13 @@ |
2509 | import mock |
2510 | +import os |
2511 | |
2512 | -from unittest import TestCase |
2513 | from curtin.block import iscsi |
2514 | - |
2515 | - |
2516 | -class IscsiTestBase(TestCase): |
2517 | - def setUp(self): |
2518 | - super(IscsiTestBase, self).setUp() |
2519 | - |
2520 | - def add_patch(self, target, attr): |
2521 | - """Patches specified target object and sets it as attr on test |
2522 | - instance also schedules cleanup""" |
2523 | - m = mock.patch(target, autospec=True) |
2524 | - p = m.start() |
2525 | - self.addCleanup(m.stop) |
2526 | - setattr(self, attr, p) |
2527 | - |
2528 | - |
2529 | -class TestBlockIscsiPortalParsing(IscsiTestBase): |
2530 | +from curtin import util |
2531 | +from .helpers import CiTestCase |
2532 | + |
2533 | + |
2534 | +class TestBlockIscsiPortalParsing(CiTestCase): |
2535 | + |
2536 | def test_iscsi_portal_parsing_string(self): |
2537 | with self.assertRaisesRegexp(ValueError, 'not a string'): |
2538 | iscsi.assert_valid_iscsi_portal(1234) |
2539 | @@ -490,7 +480,7 @@ |
2540 | self.assertEquals(i.target, 'iqn.2017-04.com.example.test:target-name') |
2541 | |
2542 | |
2543 | -class TestBlockIscsiVolPath(IscsiTestBase): |
2544 | +class TestBlockIscsiVolPath(CiTestCase): |
2545 | # non-iscsi backed disk returns false |
2546 | # regular iscsi-backed disk returns true |
2547 | # layered setup without an iscsi member returns false |
2548 | @@ -569,4 +559,183 @@ |
2549 | with self.assertRaises(ValueError): |
2550 | iscsi.volpath_is_iscsi(None) |
2551 | |
2552 | + |
2553 | +class TestBlockIscsiDiskFromConfig(CiTestCase): |
2554 | + # Test iscsi parsing of storage config for iscsi configure disks |
2555 | + |
2556 | + def setUp(self): |
2557 | + super(TestBlockIscsiDiskFromConfig, self).setUp() |
2558 | + self.add_patch('curtin.block.iscsi.util.subp', 'mock_subp') |
2559 | + |
2560 | + def test_parse_iscsi_disk_from_config(self): |
2561 | + """Test parsing iscsi volume path creates the same iscsi disk""" |
2562 | + target = 'curtin-659d5f45-4f23-46cb-b826-f2937b896e09' |
2563 | + iscsi_path = 'iscsi:10.245.168.20::20112:1:' + target |
2564 | + cfg = { |
2565 | + 'storage': { |
2566 | + 'config': [{'type': 'disk', |
2567 | + 'id': 'iscsidev1', |
2568 | + 'path': iscsi_path, |
2569 | + 'name': 'iscsi_disk1', |
2570 | + 'ptable': 'msdos', |
2571 | + 'wipe': 'superblock'}] |
2572 | + } |
2573 | + } |
2574 | + expected_iscsi_disk = iscsi.IscsiDisk(iscsi_path) |
2575 | + iscsi_disk = iscsi.get_iscsi_disks_from_config(cfg).pop() |
2576 | + # utilize IscsiDisk str method for equality check |
2577 | + self.assertEqual(str(expected_iscsi_disk), str(iscsi_disk)) |
2578 | + |
2579 | + def test_parse_iscsi_disk_from_config_no_iscsi(self): |
2580 | + """Test parsing storage config with no iscsi disks included""" |
2581 | + cfg = { |
2582 | + 'storage': { |
2583 | + 'config': [{'type': 'disk', |
2584 | + 'id': 'ssd1', |
2585 | + 'path': 'dev/slash/foo1', |
2586 | + 'name': 'the-fast-one', |
2587 | + 'ptable': 'gpt', |
2588 | + 'wipe': 'superblock'}] |
2589 | + } |
2590 | + } |
2591 | + expected_iscsi_disks = [] |
2592 | + iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg) |
2593 | + self.assertEqual(expected_iscsi_disks, iscsi_disks) |
2594 | + |
2595 | + def test_parse_iscsi_disk_from_config_invalid_iscsi(self): |
2596 | + """Test parsing storage config with no iscsi disks included""" |
2597 | + cfg = { |
2598 | + 'storage': { |
2599 | + 'config': [{'type': 'disk', |
2600 | + 'id': 'iscsidev2', |
2601 | + 'path': 'iscsi:garbage', |
2602 | + 'name': 'noob-city', |
2603 | + 'ptable': 'msdos', |
2604 | + 'wipe': 'superblock'}] |
2605 | + } |
2606 | + } |
2607 | + with self.assertRaises(ValueError): |
2608 | + iscsi.get_iscsi_disks_from_config(cfg) |
2609 | + |
2610 | + def test_parse_iscsi_disk_from_config_empty(self): |
2611 | + """Test parse_iscsi_disks handles empty/invalid config""" |
2612 | + expected_iscsi_disks = [] |
2613 | + iscsi_disks = iscsi.get_iscsi_disks_from_config({}) |
2614 | + self.assertEqual(expected_iscsi_disks, iscsi_disks) |
2615 | + |
2616 | + cfg = {'storage': {'config': []}} |
2617 | + iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg) |
2618 | + self.assertEqual(expected_iscsi_disks, iscsi_disks) |
2619 | + |
2620 | + def test_parse_iscsi_disk_from_config_none(self): |
2621 | + """Test parse_iscsi_disks handles no config""" |
2622 | + expected_iscsi_disks = [] |
2623 | + iscsi_disks = iscsi.get_iscsi_disks_from_config({}) |
2624 | + self.assertEqual(expected_iscsi_disks, iscsi_disks) |
2625 | + |
2626 | + cfg = None |
2627 | + iscsi_disks = iscsi.get_iscsi_disks_from_config(cfg) |
2628 | + self.assertEqual(expected_iscsi_disks, iscsi_disks) |
2629 | + |
2630 | + |
2631 | +class TestBlockIscsiDisconnect(CiTestCase): |
2632 | + # test that when disconnecting iscsi targets we |
2633 | + # check that the target has an active session before |
2634 | + # issuing a disconnect command |
2635 | + |
2636 | + def setUp(self): |
2637 | + super(TestBlockIscsiDisconnect, self).setUp() |
2638 | + self.add_patch('curtin.block.iscsi.util.subp', 'mock_subp') |
2639 | + self.add_patch('curtin.block.iscsi.iscsiadm_sessions', |
2640 | + 'mock_iscsi_sessions') |
2641 | + # fake target_root + iscsi nodes dir |
2642 | + self.target_path = self.tmp_dir() |
2643 | + self.iscsi_nodes = os.path.join(self.target_path, 'etc/iscsi/nodes') |
2644 | + util.ensure_dir(self.iscsi_nodes) |
2645 | + |
2646 | + def _fmt_disconnect(self, target, portal): |
2647 | + return ['iscsiadm', '--mode=node', '--targetname=%s' % target, |
2648 | + '--portal=%s' % portal, '--logout'] |
2649 | + |
2650 | + def _setup_nodes(self, sessions, connection): |
2651 | + # setup iscsi_nodes dir (<fakeroot>/etc/iscsi/nodes) with content |
2652 | + for s in sessions: |
2653 | + sdir = os.path.join(self.iscsi_nodes, s) |
2654 | + connpath = os.path.join(sdir, connection) |
2655 | + util.ensure_dir(sdir) |
2656 | + util.write_file(connpath, content="") |
2657 | + |
2658 | + def test_disconnect_target_disk(self): |
2659 | + """Test iscsi disconnecting multiple sessions, all present""" |
2660 | + |
2661 | + sessions = [ |
2662 | + 'curtin-53ab23ff-a887-449a-80a8-288151208091', |
2663 | + 'curtin-94b62de1-c579-42c0-879e-8a28178e64c5', |
2664 | + 'curtin-556aeecd-a227-41b7-83d7-2bb471c574b4', |
2665 | + 'curtin-fd0f644b-7858-420f-9997-3ea2aefe87b9' |
2666 | + ] |
2667 | + connection = '10.245.168.20,16395,1' |
2668 | + self._setup_nodes(sessions, connection) |
2669 | + |
2670 | + self.mock_iscsi_sessions.return_value = "\n".join(sessions) |
2671 | + |
2672 | + iscsi.disconnect_target_disks(self.target_path) |
2673 | + |
2674 | + expected_calls = [] |
2675 | + for session in sessions: |
2676 | + (host, port, _) = connection.split(',') |
2677 | + disconnect = self._fmt_disconnect(session, "%s:%s" % (host, port)) |
2678 | + calls = [ |
2679 | + mock.call(['sync']), |
2680 | + mock.call(disconnect, capture=True, log_captured=True), |
2681 | + mock.call(['udevadm', 'settle']), |
2682 | + ] |
2683 | + expected_calls.extend(calls) |
2684 | + |
2685 | + self.mock_subp.assert_has_calls(expected_calls, any_order=True) |
2686 | + |
2687 | + def test_disconnect_target_disk_skip_disconnected(self): |
2688 | + """Test iscsi does not attempt to disconnect already closed sessions""" |
2689 | + sessions = [ |
2690 | + 'curtin-53ab23ff-a887-449a-80a8-288151208091', |
2691 | + 'curtin-94b62de1-c579-42c0-879e-8a28178e64c5', |
2692 | + 'curtin-556aeecd-a227-41b7-83d7-2bb471c574b4', |
2693 | + 'curtin-fd0f644b-7858-420f-9997-3ea2aefe87b9' |
2694 | + ] |
2695 | + connection = '10.245.168.20,16395,1' |
2696 | + self._setup_nodes(sessions, connection) |
2697 | + # Test with all sessions are already disconnected |
2698 | + self.mock_iscsi_sessions.return_value = "" |
2699 | + |
2700 | + iscsi.disconnect_target_disks(self.target_path) |
2701 | + |
2702 | + self.mock_subp.assert_has_calls([], any_order=True) |
2703 | + |
2704 | + @mock.patch('curtin.block.iscsi.iscsiadm_logout') |
2705 | + def test_disconnect_target_disk_raises_runtime_error(self, mock_logout): |
2706 | + """Test iscsi raises RuntimeError if we fail to logout""" |
2707 | + sessions = [ |
2708 | + 'curtin-53ab23ff-a887-449a-80a8-288151208091', |
2709 | + ] |
2710 | + connection = '10.245.168.20,16395,1' |
2711 | + self._setup_nodes(sessions, connection) |
2712 | + self.mock_iscsi_sessions.return_value = "\n".join(sessions) |
2713 | + mock_logout.side_effect = util.ProcessExecutionError() |
2714 | + |
2715 | + with self.assertRaises(RuntimeError): |
2716 | + iscsi.disconnect_target_disks(self.target_path) |
2717 | + |
2718 | + expected_calls = [] |
2719 | + for session in sessions: |
2720 | + (host, port, _) = connection.split(',') |
2721 | + disconnect = self._fmt_disconnect(session, "%s:%s" % (host, port)) |
2722 | + calls = [ |
2723 | + mock.call(['sync']), |
2724 | + mock.call(disconnect, capture=True, log_captured=True), |
2725 | + mock.call(['udevadm', 'settle']), |
2726 | + ] |
2727 | + expected_calls.extend(calls) |
2728 | + |
2729 | + self.mock_subp.assert_has_calls([], any_order=True) |
2730 | + |
2731 | # vi: ts=4 expandtab syntax=python |
2732 | |
2733 | === modified file 'tests/unittests/test_block_lvm.py' |
2734 | --- tests/unittests/test_block_lvm.py 2016-09-29 18:31:02 +0000 |
2735 | +++ tests/unittests/test_block_lvm.py 2017-10-06 01:09:48 +0000 |
2736 | @@ -1,10 +1,10 @@ |
2737 | from curtin.block import lvm |
2738 | |
2739 | -from unittest import TestCase |
2740 | +from .helpers import CiTestCase |
2741 | import mock |
2742 | |
2743 | |
2744 | -class TestBlockLvm(TestCase): |
2745 | +class TestBlockLvm(CiTestCase): |
2746 | vg_name = 'ubuntu-volgroup' |
2747 | |
2748 | @mock.patch('curtin.block.lvm.util') |
2749 | |
2750 | === modified file 'tests/unittests/test_block_mdadm.py' |
2751 | --- tests/unittests/test_block_mdadm.py 2017-05-19 20:56:27 +0000 |
2752 | +++ tests/unittests/test_block_mdadm.py 2017-10-06 01:09:48 +0000 |
2753 | @@ -1,27 +1,15 @@ |
2754 | -from unittest import TestCase |
2755 | from mock import call, patch |
2756 | from curtin.block import dev_short |
2757 | from curtin.block import mdadm |
2758 | from curtin import util |
2759 | +from .helpers import CiTestCase |
2760 | import os |
2761 | import subprocess |
2762 | import textwrap |
2763 | |
2764 | |
2765 | -class MdadmTestBase(TestCase): |
2766 | - def setUp(self): |
2767 | - super(MdadmTestBase, self).setUp() |
2768 | - |
2769 | - def add_patch(self, target, attr): |
2770 | - """Patches specified target object and sets it as attr on test |
2771 | - instance also schedules cleanup""" |
2772 | - m = patch(target, autospec=True) |
2773 | - p = m.start() |
2774 | - self.addCleanup(m.stop) |
2775 | - setattr(self, attr, p) |
2776 | - |
2777 | - |
2778 | -class TestBlockMdadmAssemble(MdadmTestBase): |
2779 | +class TestBlockMdadmAssemble(CiTestCase): |
2780 | + |
2781 | def setUp(self): |
2782 | super(TestBlockMdadmAssemble, self).setUp() |
2783 | self.add_patch('curtin.block.mdadm.util', 'mock_util') |
2784 | @@ -94,7 +82,7 @@ |
2785 | rcs=[0, 1, 2]) |
2786 | |
2787 | |
2788 | -class TestBlockMdadmCreate(MdadmTestBase): |
2789 | +class TestBlockMdadmCreate(CiTestCase): |
2790 | def setUp(self): |
2791 | super(TestBlockMdadmCreate, self).setUp() |
2792 | self.add_patch('curtin.block.mdadm.util', 'mock_util') |
2793 | @@ -243,7 +231,7 @@ |
2794 | self.mock_util.subp.assert_has_calls(expected_calls) |
2795 | |
2796 | |
2797 | -class TestBlockMdadmExamine(MdadmTestBase): |
2798 | +class TestBlockMdadmExamine(CiTestCase): |
2799 | def setUp(self): |
2800 | super(TestBlockMdadmExamine, self).setUp() |
2801 | self.add_patch('curtin.block.mdadm.util', 'mock_util') |
2802 | @@ -328,7 +316,7 @@ |
2803 | self.assertEqual(data, {}) |
2804 | |
2805 | |
2806 | -class TestBlockMdadmStop(MdadmTestBase): |
2807 | +class TestBlockMdadmStop(CiTestCase): |
2808 | def setUp(self): |
2809 | super(TestBlockMdadmStop, self).setUp() |
2810 | self.add_patch('curtin.block.mdadm.util.lsb_release', 'mock_util_lsb') |
2811 | @@ -495,7 +483,7 @@ |
2812 | self.mock_util_write_file.assert_has_calls(expected_writes) |
2813 | |
2814 | |
2815 | -class TestBlockMdadmRemove(MdadmTestBase): |
2816 | +class TestBlockMdadmRemove(CiTestCase): |
2817 | def setUp(self): |
2818 | super(TestBlockMdadmRemove, self).setUp() |
2819 | self.add_patch('curtin.block.mdadm.util', 'mock_util') |
2820 | @@ -521,7 +509,7 @@ |
2821 | self.mock_util.subp.assert_has_calls(expected_calls) |
2822 | |
2823 | |
2824 | -class TestBlockMdadmQueryDetail(MdadmTestBase): |
2825 | +class TestBlockMdadmQueryDetail(CiTestCase): |
2826 | def setUp(self): |
2827 | super(TestBlockMdadmQueryDetail, self).setUp() |
2828 | self.add_patch('curtin.block.mdadm.util', 'mock_util') |
2829 | @@ -599,7 +587,7 @@ |
2830 | '93a73e10:427f280b:b7076c02:204b8f7a') |
2831 | |
2832 | |
2833 | -class TestBlockMdadmDetailScan(MdadmTestBase): |
2834 | +class TestBlockMdadmDetailScan(CiTestCase): |
2835 | def setUp(self): |
2836 | super(TestBlockMdadmDetailScan, self).setUp() |
2837 | self.add_patch('curtin.block.mdadm.util', 'mock_util') |
2838 | @@ -634,7 +622,7 @@ |
2839 | self.assertEqual(None, data) |
2840 | |
2841 | |
2842 | -class TestBlockMdadmMdHelpers(MdadmTestBase): |
2843 | +class TestBlockMdadmMdHelpers(CiTestCase): |
2844 | def setUp(self): |
2845 | super(TestBlockMdadmMdHelpers, self).setUp() |
2846 | self.add_patch('curtin.block.mdadm.util', 'mock_util') |
2847 | |
2848 | === modified file 'tests/unittests/test_block_mkfs.py' |
2849 | --- tests/unittests/test_block_mkfs.py 2016-08-05 20:47:14 +0000 |
2850 | +++ tests/unittests/test_block_mkfs.py 2017-10-06 01:09:48 +0000 |
2851 | @@ -1,10 +1,10 @@ |
2852 | from curtin.block import mkfs |
2853 | |
2854 | -from unittest import TestCase |
2855 | +from .helpers import CiTestCase |
2856 | import mock |
2857 | |
2858 | |
2859 | -class TestBlockMkfs(TestCase): |
2860 | +class TestBlockMkfs(CiTestCase): |
2861 | test_uuid = "fb26cc6c-ae73-11e5-9e38-2fb63f0c3155" |
2862 | |
2863 | def _get_config(self, fstype): |
2864 | |
2865 | === modified file 'tests/unittests/test_clear_holders.py' |
2866 | --- tests/unittests/test_clear_holders.py 2017-06-12 19:43:55 +0000 |
2867 | +++ tests/unittests/test_clear_holders.py 2017-10-06 01:09:48 +0000 |
2868 | @@ -1,12 +1,12 @@ |
2869 | -from unittest import TestCase |
2870 | import mock |
2871 | - |
2872 | -from curtin.block import clear_holders |
2873 | import os |
2874 | import textwrap |
2875 | |
2876 | - |
2877 | -class TestClearHolders(TestCase): |
2878 | +from curtin.block import clear_holders |
2879 | +from .helpers import CiTestCase |
2880 | + |
2881 | + |
2882 | +class TestClearHolders(CiTestCase): |
2883 | test_blockdev = '/dev/null' |
2884 | test_syspath = '/sys/class/block/null' |
2885 | remove_retries = [0.2] * 150 # clear_holders defaults to 30 seconds |
2886 | |
2887 | === added file 'tests/unittests/test_commands_apply_net.py' |
2888 | --- tests/unittests/test_commands_apply_net.py 1970-01-01 00:00:00 +0000 |
2889 | +++ tests/unittests/test_commands_apply_net.py 2017-10-06 01:09:48 +0000 |
2890 | @@ -0,0 +1,334 @@ |
2891 | +from mock import patch, call |
2892 | +import copy |
2893 | +import os |
2894 | + |
2895 | +from curtin.commands import apply_net |
2896 | +from curtin import util |
2897 | +from .helpers import CiTestCase |
2898 | + |
2899 | + |
2900 | +class TestApplyNet(CiTestCase): |
2901 | + |
2902 | + def setUp(self): |
2903 | + super(TestApplyNet, self).setUp() |
2904 | + |
2905 | + base = 'curtin.commands.apply_net.' |
2906 | + patches = [ |
2907 | + (base + '_maybe_remove_legacy_eth0', 'm_legacy'), |
2908 | + (base + '_disable_ipv6_privacy_extensions', 'm_ipv6_priv'), |
2909 | + (base + '_patch_ifupdown_ipv6_mtu_hook', 'm_ipv6_mtu'), |
2910 | + ('curtin.net.netconfig_passthrough_available', 'm_netpass_avail'), |
2911 | + ('curtin.net.render_netconfig_passthrough', 'm_netpass_render'), |
2912 | + ('curtin.net.parse_net_config_data', 'm_net_parsedata'), |
2913 | + ('curtin.net.render_network_state', 'm_net_renderstate'), |
2914 | + ('curtin.net.network_state.from_state_file', 'm_ns_from_file'), |
2915 | + ('curtin.config.load_config', 'm_load_config'), |
2916 | + ] |
2917 | + for (tgt, attr) in patches: |
2918 | + self.add_patch(tgt, attr) |
2919 | + |
2920 | + self.target = "my_target" |
2921 | + self.network_config = { |
2922 | + 'network': { |
2923 | + 'version': 1, |
2924 | + 'config': {}, |
2925 | + } |
2926 | + } |
2927 | + self.ns = { |
2928 | + 'interfaces': {}, |
2929 | + 'routes': [], |
2930 | + 'dns': { |
2931 | + 'nameservers': [], |
2932 | + 'search': [], |
2933 | + } |
2934 | + } |
2935 | + |
2936 | + def test_apply_net_notarget(self): |
2937 | + self.assertRaises(Exception, |
2938 | + apply_net.apply_net, None, "", "") |
2939 | + |
2940 | + def test_apply_net_nostate_or_config(self): |
2941 | + self.assertRaises(Exception, |
2942 | + apply_net.apply_net, "") |
2943 | + |
2944 | + def test_apply_net_target_and_state(self): |
2945 | + self.m_ns_from_file.return_value = self.ns |
2946 | + |
2947 | + self.assertRaises(ValueError, |
2948 | + apply_net.apply_net, self.target, |
2949 | + network_state=self.ns, network_config=None) |
2950 | + |
2951 | + def test_apply_net_target_and_config(self): |
2952 | + self.m_load_config.return_value = self.network_config |
2953 | + self.m_netpass_avail.return_value = False |
2954 | + self.m_net_parsedata.return_value = self.ns |
2955 | + |
2956 | + apply_net.apply_net(self.target, network_state=None, |
2957 | + network_config=self.network_config) |
2958 | + |
2959 | + self.m_netpass_avail.assert_called_with(self.target) |
2960 | + |
2961 | + self.m_net_renderstate.assert_called_with(target=self.target, |
2962 | + network_state=self.ns) |
2963 | + self.m_legacy.assert_called_with(self.target) |
2964 | + self.m_ipv6_priv.assert_called_with(self.target) |
2965 | + self.m_ipv6_mtu.assert_called_with(self.target) |
2966 | + |
2967 | + def test_apply_net_target_and_config_passthrough(self): |
2968 | + self.m_load_config.return_value = self.network_config |
2969 | + self.m_netpass_avail.return_value = True |
2970 | + |
2971 | + netcfg = "network_config.yaml" |
2972 | + apply_net.apply_net(self.target, network_state=None, |
2973 | + network_config=netcfg) |
2974 | + |
2975 | + self.assertFalse(self.m_ns_from_file.called) |
2976 | + self.m_load_config.assert_called_with(netcfg) |
2977 | + self.m_netpass_avail.assert_called_with(self.target) |
2978 | + nc = self.network_config |
2979 | + self.m_netpass_render.assert_called_with(self.target, netconfig=nc) |
2980 | + |
2981 | + self.assertFalse(self.m_net_renderstate.called) |
2982 | + self.m_legacy.assert_called_with(self.target) |
2983 | + self.m_ipv6_priv.assert_called_with(self.target) |
2984 | + self.m_ipv6_mtu.assert_called_with(self.target) |
2985 | + |
2986 | + def test_apply_net_target_and_config_passthrough_nonet(self): |
2987 | + nc = {'storage': {}} |
2988 | + self.m_load_config.return_value = nc |
2989 | + self.m_netpass_avail.return_value = True |
2990 | + |
2991 | + netcfg = "network_config.yaml" |
2992 | + |
2993 | + apply_net.apply_net(self.target, network_state=None, |
2994 | + network_config=netcfg) |
2995 | + |
2996 | + self.assertFalse(self.m_ns_from_file.called) |
2997 | + self.m_load_config.assert_called_with(netcfg) |
2998 | + self.m_netpass_avail.assert_called_with(self.target) |
2999 | + self.m_netpass_render.assert_called_with(self.target, netconfig=nc) |
3000 | + |
3001 | + self.assertFalse(self.m_net_renderstate.called) |
3002 | + self.m_legacy.assert_called_with(self.target) |
3003 | + self.m_ipv6_priv.assert_called_with(self.target) |
3004 | + self.m_ipv6_mtu.assert_called_with(self.target) |
3005 | + |
3006 | + def test_apply_net_target_and_config_passthrough_v2_not_available(self): |
3007 | + nc = copy.deepcopy(self.network_config) |
3008 | + nc['network']['version'] = 2 |
3009 | + self.m_load_config.return_value = nc |
3010 | + self.m_netpass_avail.return_value = False |
3011 | + self.m_net_parsedata.return_value = self.ns |
3012 | + |
3013 | + netcfg = "network_config.yaml" |
3014 | + |
3015 | + apply_net.apply_net(self.target, network_state=None, |
3016 | + network_config=netcfg) |
3017 | + |
3018 | + self.assertFalse(self.m_ns_from_file.called) |
3019 | + self.m_load_config.assert_called_with(netcfg) |
3020 | + self.m_netpass_avail.assert_called_with(self.target) |
3021 | + self.assertFalse(self.m_netpass_render.called) |
3022 | + self.m_net_parsedata.assert_called_with(nc['network']) |
3023 | + |
3024 | + self.m_net_renderstate.assert_called_with( |
3025 | + target=self.target, network_state=self.ns) |
3026 | + self.m_legacy.assert_called_with(self.target) |
3027 | + self.m_ipv6_priv.assert_called_with(self.target) |
3028 | + self.m_ipv6_mtu.assert_called_with(self.target) |
3029 | + |
3030 | + |
3031 | +class TestApplyNetPatchIfupdown(CiTestCase): |
3032 | + |
3033 | + @patch('curtin.util.write_file') |
3034 | + def test_apply_ipv6_mtu_hook(self, mock_write): |
3035 | + target = 'mytarget' |
3036 | + prehookfn = 'if-pre-up.d/mtuipv6' |
3037 | + posthookfn = 'if-up.d/mtuipv6' |
3038 | + mode = 0o755 |
3039 | + |
3040 | + apply_net._patch_ifupdown_ipv6_mtu_hook(target, |
3041 | + prehookfn=prehookfn, |
3042 | + posthookfn=posthookfn) |
3043 | + |
3044 | + precfg = util.target_path(target, path=prehookfn) |
3045 | + postcfg = util.target_path(target, path=posthookfn) |
3046 | + precontents = apply_net.IFUPDOWN_IPV6_MTU_PRE_HOOK |
3047 | + postcontents = apply_net.IFUPDOWN_IPV6_MTU_POST_HOOK |
3048 | + |
3049 | + hook_calls = [ |
3050 | + call(precfg, precontents, mode=mode), |
3051 | + call(postcfg, postcontents, mode=mode), |
3052 | + ] |
3053 | + mock_write.assert_has_calls(hook_calls) |
3054 | + |
3055 | + @patch('curtin.util.write_file') |
3056 | + def test_apply_ipv6_mtu_hook_write_fail(self, mock_write): |
3057 | + """Write failure raises IOError""" |
3058 | + target = 'mytarget' |
3059 | + prehookfn = 'if-pre-up.d/mtuipv6' |
3060 | + posthookfn = 'if-up.d/mtuipv6' |
3061 | + mock_write.side_effect = (IOError) |
3062 | + |
3063 | + self.assertRaises(IOError, |
3064 | + apply_net._patch_ifupdown_ipv6_mtu_hook, |
3065 | + target, |
3066 | + prehookfn=prehookfn, |
3067 | + posthookfn=posthookfn) |
3068 | + self.assertEqual(1, mock_write.call_count) |
3069 | + |
3070 | + @patch('curtin.util.write_file') |
3071 | + def test_apply_ipv6_mtu_hook_invalid_target(self, mock_write): |
3072 | + """Invalid target path fail before calling util.write_file""" |
3073 | + invalid_target = {} |
3074 | + prehookfn = 'if-pre-up.d/mtuipv6' |
3075 | + posthookfn = 'if-up.d/mtuipv6' |
3076 | + |
3077 | + self.assertRaises(ValueError, |
3078 | + apply_net._patch_ifupdown_ipv6_mtu_hook, |
3079 | + invalid_target, |
3080 | + prehookfn=prehookfn, |
3081 | + posthookfn=posthookfn) |
3082 | + self.assertEqual(0, mock_write.call_count) |
3083 | + |
3084 | + @patch('curtin.util.write_file') |
3085 | + def test_apply_ipv6_mtu_hook_invalid_prepost_fn(self, mock_write): |
3086 | + """Invalid prepost filenames fail before calling util.write_file""" |
3087 | + target = "mytarget" |
3088 | + invalid_prehookfn = {'a': 1} |
3089 | + invalid_posthookfn = {'b': 2} |
3090 | + |
3091 | + self.assertRaises(ValueError, |
3092 | + apply_net._patch_ifupdown_ipv6_mtu_hook, |
3093 | + target, |
3094 | + prehookfn=invalid_prehookfn, |
3095 | + posthookfn=invalid_posthookfn) |
3096 | + self.assertEqual(0, mock_write.call_count) |
3097 | + |
3098 | + |
3099 | +class TestApplyNetPatchIpv6Priv(CiTestCase): |
3100 | + |
3101 | + @patch('curtin.util.del_file') |
3102 | + @patch('curtin.util.load_file') |
3103 | + @patch('os.path') |
3104 | + @patch('curtin.util.write_file') |
3105 | + def test_disable_ipv6_priv_extentions(self, mock_write, mock_ospath, |
3106 | + mock_load, mock_del): |
3107 | + target = 'mytarget' |
3108 | + path = 'etc/sysctl.d/10-ipv6-privacy.conf' |
3109 | + ipv6_priv_contents = ( |
3110 | + 'net.ipv6.conf.all.use_tempaddr = 2\n' |
3111 | + 'net.ipv6.conf.default.use_tempaddr = 2') |
3112 | + expected_ipv6_priv_contents = '\n'.join( |
3113 | + ["# IPv6 Privacy Extensions (RFC 4941)", |
3114 | + "# Disabled by curtin", |
3115 | + "# net.ipv6.conf.all.use_tempaddr = 2", |
3116 | + "# net.ipv6.conf.default.use_tempaddr = 2"]) |
3117 | + mock_ospath.exists.return_value = True |
3118 | + mock_load.side_effect = [ipv6_priv_contents] |
3119 | + |
3120 | + apply_net._disable_ipv6_privacy_extensions(target) |
3121 | + |
3122 | + cfg = util.target_path(target, path=path) |
3123 | + mock_write.assert_called_with(cfg, expected_ipv6_priv_contents) |
3124 | + |
3125 | + @patch('curtin.util.load_file') |
3126 | + @patch('os.path') |
3127 | + def test_disable_ipv6_priv_extentions_decoderror(self, mock_ospath, |
3128 | + mock_load): |
3129 | + target = 'mytarget' |
3130 | + mock_ospath.exists.return_value = True |
3131 | + |
3132 | + # simulate loading of binary data |
3133 | + mock_load.side_effect = (Exception) |
3134 | + |
3135 | + self.assertRaises(Exception, |
3136 | + apply_net._disable_ipv6_privacy_extensions, |
3137 | + target) |
3138 | + |
3139 | + @patch('curtin.util.load_file') |
3140 | + @patch('os.path') |
3141 | + def test_disable_ipv6_priv_extentions_notfound(self, mock_ospath, |
3142 | + mock_load): |
3143 | + target = 'mytarget' |
3144 | + path = 'foo.conf' |
3145 | + mock_ospath.exists.return_value = False |
3146 | + |
3147 | + apply_net._disable_ipv6_privacy_extensions(target, path=path) |
3148 | + |
3149 | + # source file not found |
3150 | + cfg = util.target_path(target, path) |
3151 | + mock_ospath.exists.assert_called_with(cfg) |
3152 | + self.assertEqual(0, mock_load.call_count) |
3153 | + |
3154 | + |
3155 | +class TestApplyNetRemoveLegacyEth0(CiTestCase): |
3156 | + |
3157 | + @patch('curtin.util.del_file') |
3158 | + @patch('curtin.util.load_file') |
3159 | + @patch('os.path') |
3160 | + def test_remove_legacy_eth0(self, mock_ospath, mock_load, mock_del): |
3161 | + target = 'mytarget' |
3162 | + path = 'eth0.cfg' |
3163 | + cfg = util.target_path(target, path) |
3164 | + legacy_eth0_contents = ( |
3165 | + 'auto eth0\n' |
3166 | + 'iface eth0 inet dhcp') |
3167 | + |
3168 | + mock_ospath.exists.return_value = True |
3169 | + mock_load.side_effect = [legacy_eth0_contents] |
3170 | + |
3171 | + apply_net._maybe_remove_legacy_eth0(target, path) |
3172 | + |
3173 | + mock_del.assert_called_with(cfg) |
3174 | + |
3175 | + @patch('curtin.util.del_file') |
3176 | + @patch('curtin.util.load_file') |
3177 | + @patch('os.path') |
3178 | + def test_remove_legacy_eth0_nomatch(self, mock_ospath, mock_load, |
3179 | + mock_del): |
3180 | + target = 'mytarget' |
3181 | + path = 'eth0.cfg' |
3182 | + legacy_eth0_contents = "nomatch" |
3183 | + mock_ospath.join.side_effect = os.path.join |
3184 | + mock_ospath.exists.return_value = True |
3185 | + mock_load.side_effect = [legacy_eth0_contents] |
3186 | + |
3187 | + self.assertRaises(Exception, |
3188 | + apply_net._maybe_remove_legacy_eth0, |
3189 | + target, path) |
3190 | + |
3191 | + self.assertEqual(0, mock_del.call_count) |
3192 | + |
3193 | + @patch('curtin.util.del_file') |
3194 | + @patch('curtin.util.load_file') |
3195 | + @patch('os.path') |
3196 | + def test_remove_legacy_eth0_badload(self, mock_ospath, mock_load, |
3197 | + mock_del): |
3198 | + target = 'mytarget' |
3199 | + path = 'eth0.cfg' |
3200 | + mock_ospath.exists.return_value = True |
3201 | + mock_load.side_effect = (Exception) |
3202 | + |
3203 | + self.assertRaises(Exception, |
3204 | + apply_net._maybe_remove_legacy_eth0, |
3205 | + target, path) |
3206 | + |
3207 | + self.assertEqual(0, mock_del.call_count) |
3208 | + |
3209 | + @patch('curtin.util.del_file') |
3210 | + @patch('curtin.util.load_file') |
3211 | + @patch('os.path') |
3212 | + def test_remove_legacy_eth0_notfound(self, mock_ospath, mock_load, |
3213 | + mock_del): |
3214 | + target = 'mytarget' |
3215 | + path = 'eth0.conf' |
3216 | + mock_ospath.exists.return_value = False |
3217 | + |
3218 | + apply_net._maybe_remove_legacy_eth0(target, path) |
3219 | + |
3220 | + # source file not found |
3221 | + cfg = util.target_path(target, path) |
3222 | + mock_ospath.exists.assert_called_with(cfg) |
3223 | + self.assertEqual(0, mock_load.call_count) |
3224 | + self.assertEqual(0, mock_del.call_count) |
3225 | |
3226 | === modified file 'tests/unittests/test_commands_block_meta.py' |
3227 | --- tests/unittests/test_commands_block_meta.py 2017-03-23 17:05:17 +0000 |
3228 | +++ tests/unittests/test_commands_block_meta.py 2017-10-06 01:09:48 +0000 |
3229 | @@ -1,24 +1,11 @@ |
3230 | -from unittest import TestCase |
3231 | from mock import patch, call |
3232 | from argparse import Namespace |
3233 | |
3234 | from curtin.commands import block_meta |
3235 | - |
3236 | - |
3237 | -class BlockMetaTestBase(TestCase): |
3238 | - def setUp(self): |
3239 | - super(BlockMetaTestBase, self).setUp() |
3240 | - |
3241 | - def add_patch(self, target, attr): |
3242 | - """Patches specified target object and sets it as attr on test |
3243 | - instance also schedules cleanup""" |
3244 | - m = patch(target, autospec=True) |
3245 | - p = m.start() |
3246 | - self.addCleanup(m.stop) |
3247 | - setattr(self, attr, p) |
3248 | - |
3249 | - |
3250 | -class TestBlockMetaSimple(BlockMetaTestBase): |
3251 | +from .helpers import CiTestCase |
3252 | + |
3253 | + |
3254 | +class TestBlockMetaSimple(CiTestCase): |
3255 | def setUp(self): |
3256 | super(TestBlockMetaSimple, self).setUp() |
3257 | self.target = "my_target" |
3258 | @@ -120,10 +107,10 @@ |
3259 | [call(['mount', devname, self.target])]) |
3260 | |
3261 | |
3262 | -class TestBlockMeta(BlockMetaTestBase): |
3263 | +class TestBlockMeta(CiTestCase): |
3264 | + |
3265 | def setUp(self): |
3266 | super(TestBlockMeta, self).setUp() |
3267 | - # self.target = tempfile.mkdtemp() |
3268 | |
3269 | basepath = 'curtin.commands.block_meta.' |
3270 | self.add_patch(basepath + 'get_path_to_storage_volume', 'mock_getpath') |
3271 | |
3272 | === added file 'tests/unittests/test_commands_install.py' |
3273 | --- tests/unittests/test_commands_install.py 1970-01-01 00:00:00 +0000 |
3274 | +++ tests/unittests/test_commands_install.py 2017-10-06 01:09:48 +0000 |
3275 | @@ -0,0 +1,22 @@ |
3276 | +import copy |
3277 | + |
3278 | +from curtin.commands import install |
3279 | +from .helpers import CiTestCase |
3280 | + |
3281 | + |
3282 | +class TestMigrateProxy(CiTestCase): |
3283 | + def test_legacy_moved_over(self): |
3284 | + """Legacy setting should get moved over.""" |
3285 | + proxy = "http://my.proxy:3128" |
3286 | + cfg = {'http_proxy': proxy} |
3287 | + install.migrate_proxy_settings(cfg) |
3288 | + self.assertEqual(cfg, {'proxy': {'http_proxy': proxy}}) |
3289 | + |
3290 | + def test_no_legacy_new_only(self): |
3291 | + """If only new 'proxy', then no change is expected.""" |
3292 | + proxy = "http://my.proxy:3128" |
3293 | + cfg = {'proxy': {'http_proxy': proxy, 'https_proxy': proxy, |
3294 | + 'no_proxy': "10.2.2.2"}} |
3295 | + expected = copy.deepcopy(cfg) |
3296 | + install.migrate_proxy_settings(cfg) |
3297 | + self.assertEqual(expected, cfg) |
3298 | |
3299 | === modified file 'tests/unittests/test_config.py' |
3300 | --- tests/unittests/test_config.py 2015-10-02 16:19:07 +0000 |
3301 | +++ tests/unittests/test_config.py 2017-10-06 01:09:48 +0000 |
3302 | @@ -1,12 +1,12 @@ |
3303 | -from unittest import TestCase |
3304 | import copy |
3305 | import json |
3306 | import textwrap |
3307 | |
3308 | from curtin import config |
3309 | - |
3310 | - |
3311 | -class TestMerge(TestCase): |
3312 | +from .helpers import CiTestCase |
3313 | + |
3314 | + |
3315 | +class TestMerge(CiTestCase): |
3316 | def test_merge_cfg_string(self): |
3317 | d1 = {'str1': 'str_one'} |
3318 | d2 = {'dict1': {'d1.e1': 'd1-e1'}} |
3319 | @@ -16,7 +16,7 @@ |
3320 | self.assertEqual(d1, expected) |
3321 | |
3322 | |
3323 | -class TestCmdArg2Cfg(TestCase): |
3324 | +class TestCmdArg2Cfg(CiTestCase): |
3325 | def test_cmdarg_flat(self): |
3326 | self.assertEqual(config.cmdarg2cfg("foo=bar"), {'foo': 'bar'}) |
3327 | |
3328 | @@ -50,7 +50,7 @@ |
3329 | self.assertEqual(via_merge, via_merge_cmdarg) |
3330 | |
3331 | |
3332 | -class TestConfigArchive(TestCase): |
3333 | +class TestConfigArchive(CiTestCase): |
3334 | def test_archive_dict(self): |
3335 | myarchive = _replace_consts(textwrap.dedent(""" |
3336 | _ARCH_HEAD_ |
3337 | |
3338 | === modified file 'tests/unittests/test_curthooks.py' |
3339 | --- tests/unittests/test_curthooks.py 2017-06-12 19:43:55 +0000 |
3340 | +++ tests/unittests/test_curthooks.py 2017-10-06 01:09:48 +0000 |
3341 | @@ -1,29 +1,14 @@ |
3342 | import os |
3343 | -from unittest import TestCase |
3344 | from mock import call, patch, MagicMock |
3345 | -import shutil |
3346 | -import tempfile |
3347 | |
3348 | from curtin.commands import curthooks |
3349 | from curtin import util |
3350 | from curtin import config |
3351 | from curtin.reporter import events |
3352 | - |
3353 | - |
3354 | -class CurthooksBase(TestCase): |
3355 | - def setUp(self): |
3356 | - super(CurthooksBase, self).setUp() |
3357 | - |
3358 | - def add_patch(self, target, attr, autospec=True): |
3359 | - """Patches specified target object and sets it as attr on test |
3360 | - instance also schedules cleanup""" |
3361 | - m = patch(target, autospec=autospec) |
3362 | - p = m.start() |
3363 | - self.addCleanup(m.stop) |
3364 | - setattr(self, attr, p) |
3365 | - |
3366 | - |
3367 | -class TestGetFlashKernelPkgs(CurthooksBase): |
3368 | +from .helpers import CiTestCase |
3369 | + |
3370 | + |
3371 | +class TestGetFlashKernelPkgs(CiTestCase): |
3372 | def setUp(self): |
3373 | super(TestGetFlashKernelPkgs, self).setUp() |
3374 | self.add_patch('curtin.util.subp', 'mock_subp') |
3375 | @@ -57,7 +42,7 @@ |
3376 | self.mock_is_uefi_bootable.assert_called_once_with() |
3377 | |
3378 | |
3379 | -class TestCurthooksInstallKernel(CurthooksBase): |
3380 | +class TestCurthooksInstallKernel(CiTestCase): |
3381 | def setUp(self): |
3382 | super(TestCurthooksInstallKernel, self).setUp() |
3383 | self.add_patch('curtin.util.has_pkg_available', 'mock_haspkg') |
3384 | @@ -70,7 +55,7 @@ |
3385 | 'fallback-package': 'mock-fallback', |
3386 | 'mapping': {}}} |
3387 | # Tests don't actually install anything so we just need a name |
3388 | - self.target = tempfile.mktemp() |
3389 | + self.target = self.tmp_dir() |
3390 | |
3391 | def test__installs_flash_kernel_packages_when_needed(self): |
3392 | kernel_package = self.kernel_cfg.get('kernel', {}).get('package', {}) |
3393 | @@ -94,14 +79,11 @@ |
3394 | [kernel_package], target=self.target) |
3395 | |
3396 | |
3397 | -class TestUpdateInitramfs(CurthooksBase): |
3398 | +class TestUpdateInitramfs(CiTestCase): |
3399 | def setUp(self): |
3400 | super(TestUpdateInitramfs, self).setUp() |
3401 | self.add_patch('curtin.util.subp', 'mock_subp') |
3402 | - self.target = tempfile.mkdtemp() |
3403 | - |
3404 | - def tearDown(self): |
3405 | - shutil.rmtree(self.target) |
3406 | + self.target = self.tmp_dir() |
3407 | |
3408 | def _mnt_call(self, point): |
3409 | target = os.path.join(self.target, point) |
3410 | @@ -134,7 +116,7 @@ |
3411 | self.mock_subp.assert_has_calls(subp_calls) |
3412 | |
3413 | |
3414 | -class TestInstallMissingPkgs(CurthooksBase): |
3415 | +class TestInstallMissingPkgs(CiTestCase): |
3416 | def setUp(self): |
3417 | super(TestInstallMissingPkgs, self).setUp() |
3418 | self.add_patch('platform.machine', 'mock_machine') |
3419 | @@ -176,11 +158,38 @@ |
3420 | self.assertEqual([], self.mock_install_packages.call_args_list) |
3421 | |
3422 | |
3423 | -class TestSetupGrub(CurthooksBase): |
3424 | +class TestSetupZipl(CiTestCase): |
3425 | + |
3426 | + def setUp(self): |
3427 | + super(TestSetupZipl, self).setUp() |
3428 | + self.target = self.tmp_dir() |
3429 | + |
3430 | + @patch('curtin.block.get_devices_for_mp') |
3431 | + @patch('platform.machine') |
3432 | + def test_noop_non_s390x(self, m_machine, m_get_devices): |
3433 | + m_machine.return_value = 'non-s390x' |
3434 | + curthooks.setup_zipl(None, self.target) |
3435 | + self.assertEqual(0, m_get_devices.call_count) |
3436 | + |
3437 | + @patch('curtin.block.get_devices_for_mp') |
3438 | + @patch('platform.machine') |
3439 | + def test_setup_zipl_writes_etc_zipl_conf(self, m_machine, m_get_devices): |
3440 | + m_machine.return_value = 's390x' |
3441 | + m_get_devices.return_value = ['/dev/mapper/ubuntu--vg-root'] |
3442 | + curthooks.setup_zipl(None, self.target) |
3443 | + m_get_devices.assert_called_with(self.target) |
3444 | + with open(os.path.join(self.target, 'etc', 'zipl.conf')) as stream: |
3445 | + content = stream.read() |
3446 | + self.assertIn( |
3447 | + '# This has been modified by the MAAS curtin installer', |
3448 | + content) |
3449 | + |
3450 | + |
3451 | +class TestSetupGrub(CiTestCase): |
3452 | |
3453 | def setUp(self): |
3454 | super(TestSetupGrub, self).setUp() |
3455 | - self.target = tempfile.mkdtemp() |
3456 | + self.target = self.tmp_dir() |
3457 | self.add_patch('curtin.util.lsb_release', 'mock_lsb_release') |
3458 | self.mock_lsb_release.return_value = { |
3459 | 'codename': 'xenial', |
3460 | @@ -203,9 +212,6 @@ |
3461 | self.mock_in_chroot_subp.side_effect = iter(self.in_chroot_subp_output) |
3462 | self.mock_chroot.return_value = self.mock_in_chroot |
3463 | |
3464 | - def tearDown(self): |
3465 | - shutil.rmtree(self.target) |
3466 | - |
3467 | def test_uses_old_grub_install_devices_in_cfg(self): |
3468 | cfg = { |
3469 | 'grub_install_devices': ['/dev/vdb'] |
3470 | @@ -434,17 +440,13 @@ |
3471 | self.mock_in_chroot_subp.call_args_list[0][0]) |
3472 | |
3473 | |
3474 | -class TestUbuntuCoreHooks(CurthooksBase): |
3475 | +class TestUbuntuCoreHooks(CiTestCase): |
3476 | def setUp(self): |
3477 | super(TestUbuntuCoreHooks, self).setUp() |
3478 | self.target = None |
3479 | |
3480 | - def tearDown(self): |
3481 | - if self.target: |
3482 | - shutil.rmtree(self.target) |
3483 | - |
3484 | def test_target_is_ubuntu_core(self): |
3485 | - self.target = tempfile.mkdtemp() |
3486 | + self.target = self.tmp_dir() |
3487 | ubuntu_core_path = os.path.join(self.target, 'system-data', |
3488 | 'var/lib/snapd') |
3489 | util.ensure_dir(ubuntu_core_path) |
3490 | @@ -457,7 +459,7 @@ |
3491 | self.assertFalse(is_core) |
3492 | |
3493 | def test_target_is_ubuntu_core_noncore_target(self): |
3494 | - self.target = tempfile.mkdtemp() |
3495 | + self.target = self.tmp_dir() |
3496 | non_core_path = os.path.join(self.target, 'curtin') |
3497 | util.ensure_dir(non_core_path) |
3498 | self.assertTrue(os.path.isdir(non_core_path)) |
3499 | @@ -469,7 +471,7 @@ |
3500 | @patch('curtin.commands.curthooks.handle_cloudconfig') |
3501 | def test_curthooks_no_config(self, mock_handle_cc, mock_del_file, |
3502 | mock_write_file): |
3503 | - self.target = tempfile.mkdtemp() |
3504 | + self.target = self.tmp_dir() |
3505 | cfg = {} |
3506 | curthooks.ubuntu_core_curthooks(cfg, target=self.target) |
3507 | self.assertEqual(len(mock_handle_cc.call_args_list), 0) |
3508 | @@ -478,7 +480,7 @@ |
3509 | |
3510 | @patch('curtin.commands.curthooks.handle_cloudconfig') |
3511 | def test_curthooks_cloud_config_remove_disabled(self, mock_handle_cc): |
3512 | - self.target = tempfile.mkdtemp() |
3513 | + self.target = self.tmp_dir() |
3514 | uc_cloud = os.path.join(self.target, 'system-data', 'etc/cloud') |
3515 | cc_disabled = os.path.join(uc_cloud, 'cloud-init.disabled') |
3516 | cc_path = os.path.join(uc_cloud, 'cloud.cfg.d') |
3517 | @@ -496,7 +498,7 @@ |
3518 | curthooks.ubuntu_core_curthooks(cfg, target=self.target) |
3519 | |
3520 | mock_handle_cc.assert_called_with(cfg.get('cloudconfig'), |
3521 | - target=cc_path) |
3522 | + base_dir=cc_path) |
3523 | self.assertFalse(os.path.exists(cc_disabled)) |
3524 | |
3525 | @patch('curtin.util.write_file') |
3526 | @@ -504,7 +506,7 @@ |
3527 | @patch('curtin.commands.curthooks.handle_cloudconfig') |
3528 | def test_curthooks_cloud_config(self, mock_handle_cc, mock_del_file, |
3529 | mock_write_file): |
3530 | - self.target = tempfile.mkdtemp() |
3531 | + self.target = self.tmp_dir() |
3532 | cfg = { |
3533 | 'cloudconfig': { |
3534 | 'file1': { |
3535 | @@ -518,7 +520,7 @@ |
3536 | cc_path = os.path.join(self.target, |
3537 | 'system-data/etc/cloud/cloud.cfg.d') |
3538 | mock_handle_cc.assert_called_with(cfg.get('cloudconfig'), |
3539 | - target=cc_path) |
3540 | + base_dir=cc_path) |
3541 | self.assertEqual(len(mock_write_file.call_args_list), 0) |
3542 | |
3543 | @patch('curtin.util.write_file') |
3544 | @@ -526,7 +528,7 @@ |
3545 | @patch('curtin.commands.curthooks.handle_cloudconfig') |
3546 | def test_curthooks_net_config(self, mock_handle_cc, mock_del_file, |
3547 | mock_write_file): |
3548 | - self.target = tempfile.mkdtemp() |
3549 | + self.target = self.tmp_dir() |
3550 | cfg = { |
3551 | 'network': { |
3552 | 'version': '1', |
3553 | @@ -541,13 +543,13 @@ |
3554 | netcfg_path = os.path.join(self.target, |
3555 | 'system-data', |
3556 | 'etc/cloud/cloud.cfg.d', |
3557 | - '50-network-config.cfg') |
3558 | + '50-curtin-networking.cfg') |
3559 | netcfg = config.dump_config({'network': cfg.get('network')}) |
3560 | mock_write_file.assert_called_with(netcfg_path, |
3561 | content=netcfg) |
3562 | self.assertEqual(len(mock_del_file.call_args_list), 0) |
3563 | |
3564 | - @patch('curtin.commands.curthooks.write_files') |
3565 | + @patch('curtin.commands.curthooks.futil.write_files') |
3566 | def test_handle_cloudconfig(self, mock_write_files): |
3567 | cc_target = "tmpXXXX/systemd-data/etc/cloud/cloud.cfg.d" |
3568 | cloudconfig = { |
3569 | @@ -561,20 +563,202 @@ |
3570 | } |
3571 | |
3572 | expected_cfg = { |
3573 | - 'write_files': { |
3574 | - 'file1': { |
3575 | - 'path': '50-cloudconfig-file1.cfg', |
3576 | - 'content': cloudconfig['file1']['content']}, |
3577 | - 'foobar': { |
3578 | - 'path': '50-cloudconfig-foobar.cfg', |
3579 | - 'content': cloudconfig['foobar']['content']} |
3580 | - } |
3581 | + 'file1': { |
3582 | + 'path': '50-cloudconfig-file1.cfg', |
3583 | + 'content': cloudconfig['file1']['content']}, |
3584 | + 'foobar': { |
3585 | + 'path': '50-cloudconfig-foobar.cfg', |
3586 | + 'content': cloudconfig['foobar']['content']} |
3587 | } |
3588 | - curthooks.handle_cloudconfig(cloudconfig, target=cc_target) |
3589 | + curthooks.handle_cloudconfig(cloudconfig, base_dir=cc_target) |
3590 | mock_write_files.assert_called_with(expected_cfg, cc_target) |
3591 | |
3592 | def test_handle_cloudconfig_bad_config(self): |
3593 | with self.assertRaises(ValueError): |
3594 | - curthooks.handle_cloudconfig([], target="foobar") |
3595 | + curthooks.handle_cloudconfig([], base_dir="foobar") |
3596 | + |
3597 | + |
3598 | +class TestDetectRequiredPackages(CiTestCase): |
3599 | + test_config = { |
3600 | + 'storage': { |
3601 | + 1: { |
3602 | + 'bcache': { |
3603 | + 'type': 'bcache', 'name': 'bcache0', 'id': 'cache0', |
3604 | + 'backing_device': 'sda3', 'cache_device': 'sdb'}, |
3605 | + 'lvm_partition': { |
3606 | + 'id': 'lvol1', 'name': 'lv1', 'volgroup': 'vg1', |
3607 | + 'type': 'lvm_partition'}, |
3608 | + 'lvm_volgroup': { |
3609 | + 'id': 'vol1', 'name': 'vg1', 'devices': ['sda', 'sdb'], |
3610 | + 'type': 'lvm_volgroup'}, |
3611 | + 'raid': { |
3612 | + 'id': 'mddevice', 'name': 'md0', 'type': 'raid', |
3613 | + 'raidlevel': 5, 'devices': ['sda1', 'sdb1', 'sdc1']}, |
3614 | + 'ext2': { |
3615 | + 'id': 'format0', 'fstype': 'ext2', 'type': 'format'}, |
3616 | + 'ext3': { |
3617 | + 'id': 'format1', 'fstype': 'ext3', 'type': 'format'}, |
3618 | + 'ext4': { |
3619 | + 'id': 'format2', 'fstype': 'ext4', 'type': 'format'}, |
3620 | + 'btrfs': { |
3621 | + 'id': 'format3', 'fstype': 'btrfs', 'type': 'format'}, |
3622 | + 'xfs': { |
3623 | + 'id': 'format4', 'fstype': 'xfs', 'type': 'format'}} |
3624 | + }, |
3625 | + 'network': { |
3626 | + 1: { |
3627 | + 'bond': { |
3628 | + 'name': 'bond0', 'type': 'bond', |
3629 | + 'bond_interfaces': ['interface0', 'interface1'], |
3630 | + 'params': {'bond-mode': 'active-backup'}, |
3631 | + 'subnets': [ |
3632 | + {'type': 'static', 'address': '10.23.23.2/24'}, |
3633 | + {'type': 'static', 'address': '10.23.24.2/24'}]}, |
3634 | + 'vlan': { |
3635 | + 'id': 'interface1.2667', 'mtu': 1500, 'name': |
3636 | + 'interface1.2667', 'type': 'vlan', 'vlan_id': 2667, |
3637 | + 'vlan_link': 'interface1', |
3638 | + 'subnets': [{'address': '10.245.184.2/24', |
3639 | + 'dns_nameservers': [], 'type': 'static'}]}, |
3640 | + 'bridge': { |
3641 | + 'name': 'br0', 'bridge_interfaces': ['eth0', 'eth1'], |
3642 | + 'type': 'bridge', 'params': { |
3643 | + 'bridge_stp': 'off', 'bridge_fd': 0, |
3644 | + 'bridge_maxwait': 0}, |
3645 | + 'subnets': [ |
3646 | + {'type': 'static', 'address': '192.168.14.2/24'}, |
3647 | + {'type': 'static', 'address': '2001:1::1/64'}]}}, |
3648 | + 2: { |
3649 | + 'vlan': { |
3650 | + 'vlans': { |
3651 | + 'en-intra': {'id': 1, 'link': 'eno1', 'dhcp4': 'yes'}, |
3652 | + 'en-vpn': {'id': 2, 'link': 'eno1'}}}, |
3653 | + 'bridge': { |
3654 | + 'bridges': { |
3655 | + 'br0': { |
3656 | + 'interfaces': ['wlp1s0', 'switchports'], |
3657 | + 'dhcp4': True}}}} |
3658 | + }, |
3659 | + } |
3660 | + |
3661 | + def _fmt_config(self, config_items): |
3662 | + res = {} |
3663 | + for item, item_confs in config_items.items(): |
3664 | + version = item_confs['version'] |
3665 | + res[item] = {'version': version} |
3666 | + if version == 1: |
3667 | + res[item]['config'] = [self.test_config[item][version][i] |
3668 | + for i in item_confs['items']] |
3669 | + elif version == 2 and item == 'network': |
3670 | + for cfg_item in item_confs['items']: |
3671 | + res[item].update(self.test_config[item][version][cfg_item]) |
3672 | + else: |
3673 | + raise NotImplementedError |
3674 | + return res |
3675 | + |
3676 | + def _test_req_mappings(self, req_mappings): |
3677 | + for (config_items, expected_reqs) in req_mappings: |
3678 | + cfg = self._fmt_config(config_items) |
3679 | + actual_reqs = curthooks.detect_required_packages(cfg) |
3680 | + self.assertEqual(set(actual_reqs), set(expected_reqs), |
3681 | + 'failed for config: {}'.format(config_items)) |
3682 | + |
3683 | + def test_storage_v1_detect(self): |
3684 | + self._test_req_mappings(( |
3685 | + ({'storage': { |
3686 | + 'version': 1, |
3687 | + 'items': ('lvm_partition', 'lvm_volgroup', 'btrfs', 'xfs')}}, |
3688 | + ('lvm2', 'xfsprogs', 'btrfs-tools')), |
3689 | + ({'storage': { |
3690 | + 'version': 1, |
3691 | + 'items': ('raid', 'bcache', 'ext3', 'xfs')}}, |
3692 | + ('mdadm', 'bcache-tools', 'e2fsprogs', 'xfsprogs')), |
3693 | + ({'storage': { |
3694 | + 'version': 1, |
3695 | + 'items': ('raid', 'lvm_volgroup', 'lvm_partition', 'ext3', |
3696 | + 'ext4', 'btrfs')}}, |
3697 | + ('lvm2', 'mdadm', 'e2fsprogs', 'btrfs-tools')), |
3698 | + ({'storage': { |
3699 | + 'version': 1, |
3700 | + 'items': ('bcache', 'lvm_volgroup', 'lvm_partition', 'ext2')}}, |
3701 | + ('bcache-tools', 'lvm2', 'e2fsprogs')), |
3702 | + )) |
3703 | + |
3704 | + def test_network_v1_detect(self): |
3705 | + self._test_req_mappings(( |
3706 | + ({'network': { |
3707 | + 'version': 1, |
3708 | + 'items': ('bridge',)}}, |
3709 | + ('bridge-utils',)), |
3710 | + ({'network': { |
3711 | + 'version': 1, |
3712 | + 'items': ('vlan', 'bond')}}, |
3713 | + ('vlan', 'ifenslave')), |
3714 | + ({'network': { |
3715 | + 'version': 1, |
3716 | + 'items': ('bond', 'bridge')}}, |
3717 | + ('ifenslave', 'bridge-utils')), |
3718 | + ({'network': { |
3719 | + 'version': 1, |
3720 | + 'items': ('vlan', 'bridge', 'bond')}}, |
3721 | + ('ifenslave', 'bridge-utils', 'vlan')), |
3722 | + )) |
3723 | + |
3724 | + def test_mixed_v1_detect(self): |
3725 | + self._test_req_mappings(( |
3726 | + ({'storage': { |
3727 | + 'version': 1, |
3728 | + 'items': ('raid', 'bcache', 'ext4')}, |
3729 | + 'network': { |
3730 | + 'version': 1, |
3731 | + 'items': ('vlan',)}}, |
3732 | + ('mdadm', 'bcache-tools', 'e2fsprogs', 'vlan')), |
3733 | + ({'storage': { |
3734 | + 'version': 1, |
3735 | + 'items': ('lvm_partition', 'lvm_volgroup', 'xfs')}, |
3736 | + 'network': { |
3737 | + 'version': 1, |
3738 | + 'items': ('bridge', 'bond')}}, |
3739 | + ('lvm2', 'xfsprogs', 'bridge-utils', 'ifenslave')), |
3740 | + ({'storage': { |
3741 | + 'version': 1, |
3742 | + 'items': ('ext3', 'ext4', 'btrfs')}, |
3743 | + 'network': { |
3744 | + 'version': 1, |
3745 | + 'items': ('bond', 'vlan')}}, |
3746 | + ('e2fsprogs', 'btrfs-tools', 'vlan', 'ifenslave')), |
3747 | + )) |
3748 | + |
3749 | + def test_network_v2_detect(self): |
3750 | + self._test_req_mappings(( |
3751 | + ({'network': { |
3752 | + 'version': 2, |
3753 | + 'items': ('bridge',)}}, |
3754 | + ('bridge-utils',)), |
3755 | + ({'network': { |
3756 | + 'version': 2, |
3757 | + 'items': ('vlan',)}}, |
3758 | + ('vlan',)), |
3759 | + ({'network': { |
3760 | + 'version': 2, |
3761 | + 'items': ('vlan', 'bridge')}}, |
3762 | + ('vlan', 'bridge-utils')), |
3763 | + )) |
3764 | + |
3765 | + def test_mixed_storage_v1_network_v2_detect(self): |
3766 | + self._test_req_mappings(( |
3767 | + ({'network': { |
3768 | + 'version': 2, |
3769 | + 'items': ('bridge', 'vlan')}, |
3770 | + 'storage': { |
3771 | + 'version': 1, |
3772 | + 'items': ('raid', 'bcache', 'ext4')}}, |
3773 | + ('vlan', 'bridge-utils', 'mdadm', 'bcache-tools', 'e2fsprogs')), |
3774 | + )) |
3775 | + |
3776 | + def test_invalid_version_in_config(self): |
3777 | + with self.assertRaises(ValueError): |
3778 | + curthooks.detect_required_packages({'network': {'version': 3}}) |
3779 | + |
3780 | |
3781 | # vi: ts=4 expandtab syntax=python |
3782 | |
3783 | === modified file 'tests/unittests/test_feature.py' |
3784 | --- tests/unittests/test_feature.py 2017-03-23 17:05:17 +0000 |
3785 | +++ tests/unittests/test_feature.py 2017-10-06 01:09:48 +0000 |
3786 | @@ -1,9 +1,9 @@ |
3787 | -from unittest import TestCase |
3788 | +from .helpers import CiTestCase |
3789 | |
3790 | import curtin |
3791 | |
3792 | |
3793 | -class TestExportsFeatures(TestCase): |
3794 | +class TestExportsFeatures(CiTestCase): |
3795 | def test_has_storage_v1(self): |
3796 | self.assertIn('STORAGE_CONFIG_V1', curtin.FEATURES) |
3797 | |
3798 | @@ -15,3 +15,6 @@ |
3799 | |
3800 | def test_has_reporting_events_webhook(self): |
3801 | self.assertIn('REPORTING_EVENTS_WEBHOOK', curtin.FEATURES) |
3802 | + |
3803 | + def test_has_centos_apply_network_config(self): |
3804 | + self.assertIn('CENTOS_APPLY_NETWORK_CONFIG', curtin.FEATURES) |
3805 | |
3806 | === modified file 'tests/unittests/test_gpg.py' |
3807 | --- tests/unittests/test_gpg.py 2017-02-08 20:25:39 +0000 |
3808 | +++ tests/unittests/test_gpg.py 2017-10-06 01:09:48 +0000 |
3809 | @@ -1,12 +1,12 @@ |
3810 | -from unittest import TestCase |
3811 | from mock import call, patch |
3812 | import textwrap |
3813 | |
3814 | from curtin import gpg |
3815 | from curtin import util |
3816 | - |
3817 | - |
3818 | -class TestCurtinGpg(TestCase): |
3819 | +from .helpers import CiTestCase |
3820 | + |
3821 | + |
3822 | +class TestCurtinGpg(CiTestCase): |
3823 | |
3824 | @patch('curtin.util.subp') |
3825 | def test_export_armour(self, mock_subp): |
3826 | |
3827 | === modified file 'tests/unittests/test_make_dname.py' |
3828 | --- tests/unittests/test_make_dname.py 2016-08-05 20:47:14 +0000 |
3829 | +++ tests/unittests/test_make_dname.py 2017-10-06 01:09:48 +0000 |
3830 | @@ -1,13 +1,13 @@ |
3831 | -from unittest import TestCase |
3832 | import mock |
3833 | |
3834 | import textwrap |
3835 | import uuid |
3836 | |
3837 | from curtin.commands import block_meta |
3838 | - |
3839 | - |
3840 | -class TestMakeDname(TestCase): |
3841 | +from .helpers import CiTestCase |
3842 | + |
3843 | + |
3844 | +class TestMakeDname(CiTestCase): |
3845 | state = {'scratch': '/tmp/null'} |
3846 | rules_d = '/tmp/null/rules.d' |
3847 | rule_file = '/tmp/null/rules.d/{}.rules' |
3848 | |
3849 | === modified file 'tests/unittests/test_net.py' |
3850 | --- tests/unittests/test_net.py 2017-02-28 15:26:03 +0000 |
3851 | +++ tests/unittests/test_net.py 2017-10-06 01:09:48 +0000 |
3852 | @@ -1,15 +1,14 @@ |
3853 | -from unittest import TestCase |
3854 | +import mock |
3855 | import os |
3856 | -import shutil |
3857 | -import tempfile |
3858 | import yaml |
3859 | |
3860 | -from curtin import net |
3861 | +from curtin import config, net, util |
3862 | import curtin.net.network_state as network_state |
3863 | +from .helpers import CiTestCase |
3864 | from textwrap import dedent |
3865 | |
3866 | |
3867 | -class TestNetParserData(TestCase): |
3868 | +class TestNetParserData(CiTestCase): |
3869 | |
3870 | def test_parse_deb_config_data_ignores_comments(self): |
3871 | contents = dedent("""\ |
3872 | @@ -234,13 +233,11 @@ |
3873 | }, ifaces) |
3874 | |
3875 | |
3876 | -class TestNetParser(TestCase): |
3877 | +class TestNetParser(CiTestCase): |
3878 | |
3879 | def setUp(self): |
3880 | - self.target = tempfile.mkdtemp() |
3881 | - |
3882 | - def tearDown(self): |
3883 | - shutil.rmtree(self.target) |
3884 | + super(TestNetParser, self).setUp() |
3885 | + self.target = self.tmp_dir() |
3886 | |
3887 | def make_config(self, path=None, name=None, contents=None, |
3888 | parse=True): |
3889 | @@ -386,9 +383,10 @@ |
3890 | self.assertEqual({}, observed) |
3891 | |
3892 | |
3893 | -class TestNetConfig(TestCase): |
3894 | +class TestNetConfig(CiTestCase): |
3895 | def setUp(self): |
3896 | - self.target = tempfile.mkdtemp() |
3897 | + super(TestNetConfig, self).setUp() |
3898 | + self.target = self.tmp_dir() |
3899 | self.config_f = os.path.join(self.target, 'config') |
3900 | self.config = ''' |
3901 | # YAML example of a simple network config |
3902 | @@ -435,9 +433,6 @@ |
3903 | ns.parse_config() |
3904 | return ns |
3905 | |
3906 | - def tearDown(self): |
3907 | - shutil.rmtree(self.target) |
3908 | - |
3909 | def test_parse_net_config_data(self): |
3910 | ns = self.get_net_state() |
3911 | net_state_from_cls = ns.network_state |
3912 | @@ -503,24 +498,19 @@ |
3913 | auto interface1 |
3914 | iface interface1 inet manual |
3915 | bond-mode active-backup |
3916 | - bond-master bond0 |
3917 | + bond-master bond1 |
3918 | |
3919 | auto interface2 |
3920 | iface interface2 inet manual |
3921 | bond-mode active-backup |
3922 | - bond-master bond0 |
3923 | + bond-master bond1 |
3924 | |
3925 | - auto bond0 |
3926 | - iface bond0 inet static |
3927 | + auto bond1 |
3928 | + iface bond1 inet static |
3929 | address 10.23.23.2/24 |
3930 | bond-mode active-backup |
3931 | - hwaddress ether 52:54:00:12:34:06 |
3932 | bond-slaves none |
3933 | |
3934 | - # control-alias bond0 |
3935 | - iface bond0 inet static |
3936 | - address 10.23.24.2/24 |
3937 | - |
3938 | source /etc/network/interfaces.d/*.cfg |
3939 | """) |
3940 | net_ifaces = net.render_interfaces(ns.network_state) |
3941 | @@ -654,6 +644,91 @@ |
3942 | self.assertEqual(sorted(ifaces.split('\n')), |
3943 | sorted(net_ifaces.split('\n'))) |
3944 | |
3945 | + @mock.patch('curtin.util.subp') |
3946 | + @mock.patch('curtin.util.which') |
3947 | + @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a) |
3948 | + def test_netconfig_passthrough_available(self, mock_which, mock_subp): |
3949 | + cloud_init = '/usr/bin/cloud-init' |
3950 | + mock_which.return_value = cloud_init |
3951 | + mock_subp.return_value = ("NETWORK_CONFIG_V1\nNETWORK_CONFIG_V2\n", '') |
3952 | + |
3953 | + available = net.netconfig_passthrough_available(self.target) |
3954 | + |
3955 | + self.assertEqual(True, available, |
3956 | + "netconfig passthrough was NOT available") |
3957 | + mock_which.assert_called_with('cloud-init', target=self.target) |
3958 | + mock_subp.assert_called_with([cloud_init, 'features'], |
3959 | + capture=True, target=self.target) |
3960 | + |
3961 | + @mock.patch('curtin.net.LOG') |
3962 | + @mock.patch('curtin.util.subp') |
3963 | + @mock.patch('curtin.util.which') |
3964 | + @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a) |
3965 | + def test_netconfig_passthrough_available_no_cloudinit(self, mock_which, |
3966 | + mock_subp, mock_log): |
3967 | + mock_which.return_value = None |
3968 | + |
3969 | + available = net.netconfig_passthrough_available(self.target) |
3970 | + |
3971 | + self.assertEqual(False, available, |
3972 | + "netconfig passthrough was available") |
3973 | + self.assertTrue(mock_log.warning.called) |
3974 | + self.assertFalse(mock_subp.called) |
3975 | + |
3976 | + @mock.patch('curtin.util.subp') |
3977 | + @mock.patch('curtin.util.which') |
3978 | + @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a) |
3979 | + def test_netconfig_passthrough_available_feature_not_found(self, |
3980 | + mock_which, |
3981 | + mock_subp): |
3982 | + cloud_init = '/usr/bin/cloud-init' |
3983 | + mock_which.return_value = cloud_init |
3984 | + mock_subp.return_value = ('NETWORK_CONFIG_V1\n', '') |
3985 | + |
3986 | + available = net.netconfig_passthrough_available(self.target) |
3987 | + |
3988 | + self.assertEqual(False, available, |
3989 | + "netconfig passthrough was available") |
3990 | + mock_which.assert_called_with('cloud-init', target=self.target) |
3991 | + mock_subp.assert_called_with([cloud_init, 'features'], |
3992 | + capture=True, target=self.target) |
3993 | + |
3994 | + @mock.patch('curtin.net.LOG') |
3995 | + @mock.patch('curtin.util.subp') |
3996 | + @mock.patch('curtin.util.which') |
3997 | + @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a) |
3998 | + def test_netconfig_passthrough_available_exc(self, mock_which, mock_subp, |
3999 | + mock_log): |
4000 | + cloud_init = '/usr/bin/cloud-init' |
4001 | + mock_which.return_value = cloud_init |
4002 | + mock_subp.side_effect = util.ProcessExecutionError |
4003 | + |
4004 | + available = net.netconfig_passthrough_available(self.target) |
4005 | + |
4006 | + self.assertEqual(False, available, |
4007 | + "netconfig passthrough was available") |
4008 | + mock_which.assert_called_with('cloud-init', target=self.target) |
4009 | + mock_subp.assert_called_with([cloud_init, 'features'], |
4010 | + capture=True, target=self.target) |
4011 | + self.assertTrue(mock_log.warning.called) |
4012 | + |
4013 | + @mock.patch('curtin.util.write_file') |
4014 | + def test_render_netconfig_passthrough(self, mock_writefile): |
4015 | + netcfg = yaml.safe_load(self.config) |
4016 | + pt_config = 'etc/cloud/cloud.cfg.d/50-curtin-networking.cfg' |
4017 | + target_config = os.path.sep.join((self.target, pt_config),) |
4018 | + |
4019 | + net.render_netconfig_passthrough(self.target, netconfig=netcfg) |
4020 | + |
4021 | + content = config.dump_config(netcfg) |
4022 | + mock_writefile.assert_called_with(target_config, content=content) |
4023 | + |
4024 | + def test_render_netconfig_passthrough_nonetcfg(self): |
4025 | + netcfg = None |
4026 | + self.assertRaises(ValueError, |
4027 | + net.render_netconfig_passthrough, |
4028 | + self.target, netconfig=netcfg) |
4029 | + |
4030 | def test_routes_rendered(self): |
4031 | # as reported in bug 1649652 |
4032 | conf = [ |
4033 | |
4034 | === modified file 'tests/unittests/test_partitioning.py' |
4035 | --- tests/unittests/test_partitioning.py 2015-10-02 16:19:07 +0000 |
4036 | +++ tests/unittests/test_partitioning.py 2017-10-06 01:09:48 +0000 |
4037 | @@ -1,6 +1,7 @@ |
4038 | -import unittest |
4039 | +from unittest import skip |
4040 | import mock |
4041 | import curtin.commands.block_meta |
4042 | +from .helpers import CiTestCase |
4043 | |
4044 | from sys import version_info |
4045 | if version_info.major == 2: |
4046 | @@ -11,8 +12,8 @@ |
4047 | parted = None # FIXME: remove these tests entirely. This is here for flake8 |
4048 | |
4049 | |
4050 | -@unittest.skip |
4051 | -class TestBlock(unittest.TestCase): |
4052 | +@skip |
4053 | +class TestBlock(CiTestCase): |
4054 | storage_config = { |
4055 | "sda": {"id": "sda", "type": "disk", "ptable": "msdos", |
4056 | "serial": "DISK_1", "grub_device": "True"}, |
4057 | |
4058 | === added file 'tests/unittests/test_public.py' |
4059 | --- tests/unittests/test_public.py 1970-01-01 00:00:00 +0000 |
4060 | +++ tests/unittests/test_public.py 2017-10-06 01:09:48 +0000 |
4061 | @@ -0,0 +1,54 @@ |
4062 | + |
4063 | +from curtin import block |
4064 | +from curtin import config |
4065 | +from curtin import futil |
4066 | +from curtin import util |
4067 | + |
4068 | +from curtin.commands import curthooks |
4069 | +from .helpers import CiTestCase |
4070 | + |
4071 | + |
4072 | +class TestPublicAPI(CiTestCase): |
4073 | + """Test entry points known to be used externally. |
4074 | + |
4075 | + Curtin's only known external library user is the curthooks |
4076 | + that are present in the MAAS images. This will test for presense |
4077 | + of the modules and entry points that are used there. |
4078 | + |
4079 | + This unit test is present to just test entry points. Function |
4080 | + behavior should be present elsewhere.""" |
4081 | + |
4082 | + def assert_has_callables(self, module, expected): |
4083 | + self.assertEqual(expected, _module_has(module, expected, callable)) |
4084 | + |
4085 | + def test_block(self): |
4086 | + """Verify expected attributes in curtin.block.""" |
4087 | + self.assert_has_callables( |
4088 | + block, |
4089 | + ['get_devices_for_mp', 'get_blockdev_for_partition', '_lsblock']) |
4090 | + |
4091 | + def test_config(self): |
4092 | + """Verify exported attributes in curtin.config.""" |
4093 | + self.assert_has_callables(config, ['load_config']) |
4094 | + |
4095 | + def test_util(self): |
4096 | + """Verify exported attributes in curtin.util.""" |
4097 | + self.assert_has_callables( |
4098 | + util, ['RunInChroot', 'load_command_environment']) |
4099 | + |
4100 | + def test_centos_apply_network_config(self): |
4101 | + """MAAS images use centos_apply_network_config from cmd.curthooks.""" |
4102 | + self.assert_has_callables(curthooks, ['centos_apply_network_config']) |
4103 | + |
4104 | + def test_futil(self): |
4105 | + """Verify exported attributes in curtin.futil.""" |
4106 | + self.assert_has_callables(futil, ['write_files']) |
4107 | + |
4108 | + |
4109 | +def _module_has(module, names, nfilter=None): |
4110 | + found = [(name, getattr(module, name)) |
4111 | + for name in names if hasattr(module, name)] |
4112 | + if nfilter is not None: |
4113 | + found = [(name, attr) for name, attr in found if nfilter(attr)] |
4114 | + |
4115 | + return [name for name, _ in found] |
4116 | |
4117 | === modified file 'tests/unittests/test_reporter.py' |
4118 | --- tests/unittests/test_reporter.py 2017-02-28 15:26:03 +0000 |
4119 | +++ tests/unittests/test_reporter.py 2017-10-06 01:09:48 +0000 |
4120 | @@ -21,7 +21,6 @@ |
4121 | unicode_literals, |
4122 | ) |
4123 | |
4124 | -from unittest import TestCase |
4125 | from mock import patch |
4126 | |
4127 | from curtin.reporter.legacy import ( |
4128 | @@ -39,13 +38,12 @@ |
4129 | from curtin.reporter import handlers |
4130 | from curtin import url_helper |
4131 | from curtin.reporter import events |
4132 | +from .helpers import CiTestCase |
4133 | |
4134 | -import os |
4135 | -import tempfile |
4136 | import base64 |
4137 | |
4138 | |
4139 | -class TestLegacyReporter(TestCase): |
4140 | +class TestLegacyReporter(CiTestCase): |
4141 | |
4142 | @patch('curtin.reporter.legacy.LOG') |
4143 | def test_load_reporter_logs_empty_cfg(self, mock_LOG): |
4144 | @@ -72,7 +70,7 @@ |
4145 | self.assertTrue(mock_LOG.error.called) |
4146 | |
4147 | |
4148 | -class TestMAASReporter(TestCase): |
4149 | +class TestMAASReporter(CiTestCase): |
4150 | def test_load_factory_raises_exception_wrong_options(self): |
4151 | options = {'wrong': 'wrong'} |
4152 | self.assertRaises( |
4153 | @@ -86,7 +84,7 @@ |
4154 | self.assertIsInstance(reporter, MAASReporter) |
4155 | |
4156 | |
4157 | -class TestReporter(TestCase): |
4158 | +class TestReporter(CiTestCase): |
4159 | config = {'element1': {'type': 'webhook', 'level': 'INFO', |
4160 | 'consumer_key': "ck_foo", |
4161 | 'consumer_secret': 'cs_foo', |
4162 | @@ -175,39 +173,32 @@ |
4163 | @patch('curtin.reporter.events.report_event') |
4164 | def test_report_finished_post_files(self, mock_report_event): |
4165 | test_data = b'abcdefg' |
4166 | - tmp = tempfile.mkstemp() |
4167 | - try: |
4168 | - with open(tmp[1], 'wb') as fp: |
4169 | - fp.write(test_data) |
4170 | - events.report_finish_event(self.ev_name, self.ev_desc, |
4171 | - post_files=[tmp[1]]) |
4172 | - event = self._get_reported_event(mock_report_event) |
4173 | - files = event.as_dict().get('files') |
4174 | - self.assertTrue(len(files) == 1) |
4175 | - self.assertEqual(files[0].get('path'), tmp[1]) |
4176 | - self.assertEqual(files[0].get('encoding'), 'base64') |
4177 | - self.assertEqual(files[0].get('content'), |
4178 | - base64.b64encode(test_data).decode()) |
4179 | - finally: |
4180 | - os.remove(tmp[1]) |
4181 | + tmpfname = self.tmp_path('testfile') |
4182 | + with open(tmpfname, 'wb') as fp: |
4183 | + fp.write(test_data) |
4184 | + events.report_finish_event(self.ev_name, self.ev_desc, |
4185 | + post_files=[tmpfname]) |
4186 | + event = self._get_reported_event(mock_report_event) |
4187 | + files = event.as_dict().get('files') |
4188 | + self.assertTrue(len(files) == 1) |
4189 | + self.assertEqual(files[0].get('path'), tmpfname) |
4190 | + self.assertEqual(files[0].get('encoding'), 'base64') |
4191 | + self.assertEqual(files[0].get('content'), |
4192 | + base64.b64encode(test_data).decode()) |
4193 | |
4194 | @patch('curtin.url_helper.OauthUrlHelper') |
4195 | def test_webhook_handler_post_files(self, mock_url_helper): |
4196 | test_data = b'abcdefg' |
4197 | - tmp = tempfile.mkstemp() |
4198 | - tmpfname = tmp[1] |
4199 | - try: |
4200 | - with open(tmpfname, 'wb') as fp: |
4201 | - fp.write(test_data) |
4202 | - event = events.FinishReportingEvent('test_event_name', |
4203 | - 'test event description', |
4204 | - post_files=[tmpfname], |
4205 | - level='INFO') |
4206 | - webhook_handler = handlers.WebHookHandler('127.0.0.1:8000', |
4207 | - level='INFO') |
4208 | - webhook_handler.publish_event(event) |
4209 | - webhook_handler.oauth_helper.geturl.assert_called_with( |
4210 | - url='127.0.0.1:8000', data=event.as_dict(), |
4211 | - headers=webhook_handler.headers, retries=None) |
4212 | - finally: |
4213 | - os.remove(tmpfname) |
4214 | + tmpfname = self.tmp_path('testfile') |
4215 | + with open(tmpfname, 'wb') as fp: |
4216 | + fp.write(test_data) |
4217 | + event = events.FinishReportingEvent('test_event_name', |
4218 | + 'test event description', |
4219 | + post_files=[tmpfname], |
4220 | + level='INFO') |
4221 | + webhook_handler = handlers.WebHookHandler('127.0.0.1:8000', |
4222 | + level='INFO') |
4223 | + webhook_handler.publish_event(event) |
4224 | + webhook_handler.oauth_helper.geturl.assert_called_with( |
4225 | + url='127.0.0.1:8000', data=event.as_dict(), |
4226 | + headers=webhook_handler.headers, retries=None) |
4227 | |
4228 | === modified file 'tests/unittests/test_util.py' |
4229 | --- tests/unittests/test_util.py 2017-06-12 19:43:55 +0000 |
4230 | +++ tests/unittests/test_util.py 2017-10-06 01:09:48 +0000 |
4231 | @@ -1,16 +1,14 @@ |
4232 | -from unittest import TestCase, skipIf |
4233 | +from unittest import skipIf |
4234 | import mock |
4235 | import os |
4236 | import stat |
4237 | -import shutil |
4238 | -import tempfile |
4239 | from textwrap import dedent |
4240 | |
4241 | from curtin import util |
4242 | -from .helpers import simple_mocked_open |
4243 | - |
4244 | - |
4245 | -class TestLogTimer(TestCase): |
4246 | +from .helpers import CiTestCase, simple_mocked_open |
4247 | + |
4248 | + |
4249 | +class TestLogTimer(CiTestCase): |
4250 | def test_logger_called(self): |
4251 | data = {} |
4252 | |
4253 | @@ -24,16 +22,14 @@ |
4254 | self.assertIn("mymessage", data['msg']) |
4255 | |
4256 | |
4257 | -class TestDisableDaemons(TestCase): |
4258 | +class TestDisableDaemons(CiTestCase): |
4259 | prcpath = "usr/sbin/policy-rc.d" |
4260 | |
4261 | def setUp(self): |
4262 | - self.target = tempfile.mkdtemp() |
4263 | + super(TestDisableDaemons, self).setUp() |
4264 | + self.target = self.tmp_dir() |
4265 | self.temp_prc = os.path.join(self.target, self.prcpath) |
4266 | |
4267 | - def tearDown(self): |
4268 | - shutil.rmtree(self.target) |
4269 | - |
4270 | def test_disable_daemons_in_root_works(self): |
4271 | ret = util.disable_daemons_in_root(self.target) |
4272 | self.assertTrue(ret) |
4273 | @@ -55,8 +51,10 @@ |
4274 | self.assertTrue(os.path.exists(self.temp_prc)) |
4275 | |
4276 | |
4277 | -class TestWhich(TestCase): |
4278 | +class TestWhich(CiTestCase): |
4279 | + |
4280 | def setUp(self): |
4281 | + super(TestWhich, self).setUp() |
4282 | self.orig_is_exe = util.is_exe |
4283 | util.is_exe = self.my_is_exe |
4284 | self.orig_path = os.environ.get("PATH") |
4285 | @@ -103,8 +101,10 @@ |
4286 | self.assertEqual(found, "/usr/bin2/fuzz") |
4287 | |
4288 | |
4289 | -class TestLsbRelease(TestCase): |
4290 | +class TestLsbRelease(CiTestCase): |
4291 | + |
4292 | def setUp(self): |
4293 | + super(TestLsbRelease, self).setUp() |
4294 | self._reset_cache() |
4295 | |
4296 | def _reset_cache(self): |
4297 | @@ -143,7 +143,7 @@ |
4298 | self.assertEqual(util.lsb_release(), expected) |
4299 | |
4300 | |
4301 | -class TestSubp(TestCase): |
4302 | +class TestSubp(CiTestCase): |
4303 | |
4304 | stdin2err = ['bash', '-c', 'cat >&2'] |
4305 | stdin2out = ['cat'] |
4306 | @@ -160,6 +160,12 @@ |
4307 | decode_type = str |
4308 | nodecode_type = bytes |
4309 | |
4310 | + def setUp(self): |
4311 | + super(TestSubp, self).setUp() |
4312 | + self.add_patch( |
4313 | + 'curtin.util._get_unshare_pid_args', 'mock_get_unshare_pid_args', |
4314 | + return_value=[]) |
4315 | + |
4316 | def printf_cmd(self, *args): |
4317 | # bash's printf supports \xaa. So does /usr/bin/printf |
4318 | # but by using bash, we remove dependency on another program. |
4319 | @@ -296,12 +302,29 @@ |
4320 | calls = m_popen.call_args_list |
4321 | popen_args, popen_kwargs = calls[-1] |
4322 | target = util.target_path(kwargs.get('target', None)) |
4323 | + unshcmd = self.mock_get_unshare_pid_args.return_value |
4324 | if target == "/": |
4325 | - self.assertEqual(cmd, popen_args[0]) |
4326 | + self.assertEqual(unshcmd + list(cmd), popen_args[0]) |
4327 | else: |
4328 | - self.assertEqual(['chroot', target] + list(cmd), popen_args[0]) |
4329 | + self.assertEqual(unshcmd + ['chroot', target] + list(cmd), |
4330 | + popen_args[0]) |
4331 | return calls |
4332 | |
4333 | + def test_args_can_be_a_tuple(self): |
4334 | + """subp can take a tuple for cmd rather than a list.""" |
4335 | + my_cmd = tuple(['echo', 'hi', 'mom']) |
4336 | + calls = self._subp_wrap_popen(my_cmd, {}) |
4337 | + args, kwargs = calls[0] |
4338 | + # subp was called with cmd as a tuple. That may get converted to |
4339 | + # a list before subprocess.popen. So only compare as lists. |
4340 | + self.assertEqual(1, len(calls)) |
4341 | + self.assertEqual(list(my_cmd), list(args[0])) |
4342 | + |
4343 | + def test_args_can_be_a_string(self): |
4344 | + """subp("cat") is acceptable, as suprocess.call("cat") works fine.""" |
4345 | + out, err = util.subp("cat", data=b'hi mom', capture=True, decode=False) |
4346 | + self.assertEqual(b'hi mom', out) |
4347 | + |
4348 | def test_with_target_gets_chroot(self): |
4349 | args, kwargs = self._subp_wrap_popen(["my-command"], |
4350 | {'target': "/mytarget"})[0] |
4351 | @@ -342,8 +365,94 @@ |
4352 | # since we fail a few times, it needs to have been called again. |
4353 | self.assertEqual(len(r), len(rcs)) |
4354 | |
4355 | - |
4356 | -class TestHuman2Bytes(TestCase): |
4357 | + def test_unshare_pid_return_is_used(self): |
4358 | + """The return of _get_unshare_pid_return needs to be in command.""" |
4359 | + my_unshare_cmd = ['do-unshare-command', 'arg0', 'arg1', '--'] |
4360 | + self.mock_get_unshare_pid_args.return_value = my_unshare_cmd |
4361 | + my_kwargs = {'target': '/target', 'unshare_pid': True} |
4362 | + r = self._subp_wrap_popen(['apt-get', 'install'], my_kwargs) |
4363 | + self.assertEqual(1, len(r)) |
4364 | + args, kwargs = r[0] |
4365 | + self.assertEqual( |
4366 | + [mock.call(my_kwargs['unshare_pid'], my_kwargs['target'])], |
4367 | + self.mock_get_unshare_pid_args.call_args_list) |
4368 | + expected = (my_unshare_cmd + ['chroot', '/target'] + |
4369 | + ['apt-get', 'install']) |
4370 | + self.assertEqual(expected, args[0]) |
4371 | + |
4372 | + |
4373 | +class TestGetUnsharePidArgs(CiTestCase): |
4374 | + """Test the internal implementation for when to unshare.""" |
4375 | + |
4376 | + def setUp(self): |
4377 | + super(TestGetUnsharePidArgs, self).setUp() |
4378 | + self.add_patch('curtin.util._has_unshare_pid', 'mock_has_unshare_pid', |
4379 | + return_value=True) |
4380 | + # our trusty tox environment with mock 1.0.1 will stack trace |
4381 | + # if autospec is not disabled here. |
4382 | + self.add_patch('curtin.util.os.geteuid', 'mock_geteuid', |
4383 | + autospec=False, return_value=0) |
4384 | + |
4385 | + def assertOff(self, result): |
4386 | + self.assertEqual([], result) |
4387 | + |
4388 | + def assertOn(self, result): |
4389 | + self.assertEqual(['unshare', '--fork', '--pid', '--'], result) |
4390 | + |
4391 | + def test_unshare_pid_none_and_not_root_means_off(self): |
4392 | + """If not root, then expect off.""" |
4393 | + self.assertOff(util._get_unshare_pid_args(None, "/foo", 500)) |
4394 | + self.assertOff(util._get_unshare_pid_args(None, "/", 500)) |
4395 | + |
4396 | + self.mock_geteuid.return_value = 500 |
4397 | + self.assertOff(util._get_unshare_pid_args(None, "/")) |
4398 | + self.assertOff( |
4399 | + util._get_unshare_pid_args(unshare_pid=None, target="/foo")) |
4400 | + |
4401 | + def test_unshare_pid_none_and_no_unshare_pid_means_off(self): |
4402 | + """No unshare support and unshare_pid is None means off.""" |
4403 | + self.mock_has_unshare_pid.return_value = False |
4404 | + self.assertOff(util._get_unshare_pid_args(None, "/target", 0)) |
4405 | + |
4406 | + def test_unshare_pid_true_and_no_unshare_pid_raises(self): |
4407 | + """Passing unshare_pid in as True and no command should raise.""" |
4408 | + self.mock_has_unshare_pid.return_value = False |
4409 | + expected_msg = 'no unshare command' |
4410 | + with self.assertRaisesRegexp(RuntimeError, expected_msg): |
4411 | + util._get_unshare_pid_args(True) |
4412 | + |
4413 | + with self.assertRaisesRegexp(RuntimeError, expected_msg): |
4414 | + util._get_unshare_pid_args(True, "/foo", 0) |
4415 | + |
4416 | + def test_unshare_pid_true_and_not_root_raises(self): |
4417 | + """When unshare_pid is True for non-root an error is raised.""" |
4418 | + expected_msg = 'euid.* != 0' |
4419 | + with self.assertRaisesRegexp(RuntimeError, expected_msg): |
4420 | + util._get_unshare_pid_args(True, "/foo", 500) |
4421 | + |
4422 | + self.mock_geteuid.return_value = 500 |
4423 | + with self.assertRaisesRegexp(RuntimeError, expected_msg): |
4424 | + util._get_unshare_pid_args(True) |
4425 | + |
4426 | + def test_euid0_target_not_slash(self): |
4427 | + """If root and target is not /, then expect on.""" |
4428 | + self.assertOn(util._get_unshare_pid_args(None, target="/foo", euid=0)) |
4429 | + |
4430 | + def test_euid0_target_slash(self): |
4431 | + """If root and target is /, then expect off.""" |
4432 | + self.assertOff(util._get_unshare_pid_args(None, "/", 0)) |
4433 | + self.assertOff(util._get_unshare_pid_args(None, target=None, euid=0)) |
4434 | + |
4435 | + def test_unshare_pid_of_false_means_off(self): |
4436 | + """Any unshare_pid value false-ish other than None means no unshare.""" |
4437 | + self.assertOff( |
4438 | + util._get_unshare_pid_args(unshare_pid=False, target=None)) |
4439 | + self.assertOff(util._get_unshare_pid_args(False, "/target", 1)) |
4440 | + self.assertOff(util._get_unshare_pid_args(False, "/", 0)) |
4441 | + self.assertOff(util._get_unshare_pid_args("", "/target", 0)) |
4442 | + |
4443 | + |
4444 | +class TestHuman2Bytes(CiTestCase): |
4445 | GB = 1024 * 1024 * 1024 |
4446 | MB = 1024 * 1024 |
4447 | |
4448 | @@ -397,52 +506,42 @@ |
4449 | util.bytes2human(util.human2bytes(size_str)), size_str) |
4450 | |
4451 | |
4452 | -class TestSetUnExecutable(TestCase): |
4453 | +class TestSetUnExecutable(CiTestCase): |
4454 | tmpf = None |
4455 | tmpd = None |
4456 | |
4457 | - def tearDown(self): |
4458 | - if self.tmpf: |
4459 | - if os.path.exists(self.tmpf): |
4460 | - os.unlink(self.tmpf) |
4461 | - self.tmpf = None |
4462 | - if self.tmpd: |
4463 | - shutil.rmtree(self.tmpd) |
4464 | - self.tmpd = None |
4465 | - |
4466 | - def tempfile(self, data=None): |
4467 | - fp, self.tmpf = tempfile.mkstemp() |
4468 | - if data: |
4469 | - fp.write(data) |
4470 | - os.close(fp) |
4471 | - return self.tmpf |
4472 | + def setUp(self): |
4473 | + super(CiTestCase, self).setUp() |
4474 | + self.tmpd = self.tmp_dir() |
4475 | |
4476 | def test_change_needed_returns_original_mode(self): |
4477 | - tmpf = self.tempfile() |
4478 | + tmpf = self.tmp_path('testfile') |
4479 | + util.write_file(tmpf, '') |
4480 | os.chmod(tmpf, 0o755) |
4481 | ret = util.set_unexecutable(tmpf) |
4482 | self.assertEqual(ret, 0o0755) |
4483 | |
4484 | def test_no_change_needed_returns_none(self): |
4485 | - tmpf = self.tempfile() |
4486 | + tmpf = self.tmp_path('testfile') |
4487 | + util.write_file(tmpf, '') |
4488 | os.chmod(tmpf, 0o600) |
4489 | ret = util.set_unexecutable(tmpf) |
4490 | self.assertEqual(ret, None) |
4491 | |
4492 | def test_change_does_as_expected(self): |
4493 | - tmpf = self.tempfile() |
4494 | + tmpf = self.tmp_path('testfile') |
4495 | + util.write_file(tmpf, '') |
4496 | os.chmod(tmpf, 0o755) |
4497 | ret = util.set_unexecutable(tmpf) |
4498 | self.assertEqual(ret, 0o0755) |
4499 | self.assertEqual(stat.S_IMODE(os.stat(tmpf).st_mode), 0o0644) |
4500 | |
4501 | def test_strict_no_exists_raises_exception(self): |
4502 | - self.tmpd = tempfile.mkdtemp() |
4503 | bogus = os.path.join(self.tmpd, 'bogus') |
4504 | self.assertRaises(ValueError, util.set_unexecutable, bogus, True) |
4505 | |
4506 | |
4507 | -class TestTargetPath(TestCase): |
4508 | +class TestTargetPath(CiTestCase): |
4509 | def test_target_empty_string(self): |
4510 | self.assertEqual("/etc/passwd", util.target_path("", "/etc/passwd")) |
4511 | |
4512 | @@ -484,7 +583,7 @@ |
4513 | util.target_path("/target/", "///my/path/")) |
4514 | |
4515 | |
4516 | -class TestRunInChroot(TestCase): |
4517 | +class TestRunInChroot(CiTestCase): |
4518 | """Test the legacy 'RunInChroot'. |
4519 | |
4520 | The test works by mocking ChrootableTarget's __enter__ to do nothing. |
4521 | @@ -514,7 +613,7 @@ |
4522 | m_subp.assert_called_with(cmd, target=target) |
4523 | |
4524 | |
4525 | -class TestLoadFile(TestCase): |
4526 | +class TestLoadFile(CiTestCase): |
4527 | """Test utility 'load_file'""" |
4528 | |
4529 | def test_load_file_simple(self): |
4530 | @@ -545,7 +644,7 @@ |
4531 | self.assertEqual(loaded_contents, contents) |
4532 | |
4533 | |
4534 | -class TestIpAddress(TestCase): |
4535 | +class TestIpAddress(CiTestCase): |
4536 | """Test utility 'is_valid_ip{,v4,v6}_address'""" |
4537 | |
4538 | def test_is_valid_ipv6_address(self): |
4539 | @@ -570,10 +669,11 @@ |
4540 | '2002:4559:1FE2:0000:0000:0000:4559:1FE2')) |
4541 | |
4542 | |
4543 | -class TestLoadCommandEnvironment(TestCase): |
4544 | +class TestLoadCommandEnvironment(CiTestCase): |
4545 | + |
4546 | def setUp(self): |
4547 | - self.tmpd = tempfile.mkdtemp() |
4548 | - self.addCleanup(shutil.rmtree, self.tmpd) |
4549 | + super(TestLoadCommandEnvironment, self).setUp() |
4550 | + self.tmpd = self.tmp_dir() |
4551 | all_names = { |
4552 | 'CONFIG', |
4553 | 'OUTPUT_FSTAB', |
4554 | @@ -616,7 +716,7 @@ |
4555 | self.fail("unexpected key error raised: %s" % e) |
4556 | |
4557 | |
4558 | -class TestWaitForRemoval(TestCase): |
4559 | +class TestWaitForRemoval(CiTestCase): |
4560 | def test_wait_for_removal_missing_path(self): |
4561 | with self.assertRaises(ValueError): |
4562 | util.wait_for_removal(None) |
4563 | @@ -684,14 +784,12 @@ |
4564 | ]) |
4565 | |
4566 | |
4567 | -class TestGetEFIBootMGR(TestCase): |
4568 | +class TestGetEFIBootMGR(CiTestCase): |
4569 | |
4570 | def setUp(self): |
4571 | super(TestGetEFIBootMGR, self).setUp() |
4572 | - mock_chroot = mock.patch( |
4573 | - 'curtin.util.ChrootableTarget', autospec=False) |
4574 | - self.mock_chroot = mock_chroot.start() |
4575 | - self.addCleanup(mock_chroot.stop) |
4576 | + self.add_patch( |
4577 | + 'curtin.util.ChrootableTarget', 'mock_chroot', autospec=False) |
4578 | self.mock_in_chroot = mock.MagicMock() |
4579 | self.mock_in_chroot.__enter__.return_value = self.mock_in_chroot |
4580 | self.in_chroot_subp_output = [] |
4581 | @@ -753,4 +851,55 @@ |
4582 | }, observed) |
4583 | |
4584 | |
4585 | +class TestUsesSystemd(CiTestCase): |
4586 | + |
4587 | + def setUp(self): |
4588 | + super(TestUsesSystemd, self).setUp() |
4589 | + self._reset_cache() |
4590 | + self.add_patch('curtin.util.os.path.isdir', 'mock_isdir') |
4591 | + |
4592 | + def _reset_cache(self): |
4593 | + util._USES_SYSTEMD = None |
4594 | + |
4595 | + def test_uses_systemd_on_systemd(self): |
4596 | + """ Test that uses_systemd returns True if sdpath is a dir """ |
4597 | + # systemd_enabled |
4598 | + self.mock_isdir.return_value = True |
4599 | + result = util.uses_systemd() |
4600 | + self.assertEqual(True, result) |
4601 | + self.assertEqual(1, len(self.mock_isdir.call_args_list)) |
4602 | + |
4603 | + def test_uses_systemd_cached(self): |
4604 | + """Test that we cache the uses_systemd result""" |
4605 | + |
4606 | + # reset_cache should ensure it's unset |
4607 | + self.assertEqual(None, util._USES_SYSTEMD) |
4608 | + |
4609 | + # systemd enabled |
4610 | + self.mock_isdir.return_value = True |
4611 | + |
4612 | + # first time |
4613 | + first_result = util.uses_systemd() |
4614 | + |
4615 | + # check the cache value |
4616 | + self.assertEqual(first_result, util._USES_SYSTEMD) |
4617 | + |
4618 | + # second time |
4619 | + second_result = util.uses_systemd() |
4620 | + |
4621 | + # results should match between tries |
4622 | + self.assertEqual(True, first_result) |
4623 | + self.assertEqual(True, second_result) |
4624 | + |
4625 | + # isdir should only be called once |
4626 | + self.assertEqual(1, len(self.mock_isdir.call_args_list)) |
4627 | + |
4628 | + def test_uses_systemd_on_non_systemd(self): |
4629 | + """ Test that uses_systemd returns False if sdpath is not a dir """ |
4630 | + # systemd not available |
4631 | + self.mock_isdir.return_value = False |
4632 | + result = util.uses_systemd() |
4633 | + self.assertEqual(False, result) |
4634 | + |
4635 | + |
4636 | # vi: ts=4 expandtab syntax=python |
4637 | |
4638 | === modified file 'tests/unittests/test_version.py' |
4639 | --- tests/unittests/test_version.py 2017-02-08 20:25:39 +0000 |
4640 | +++ tests/unittests/test_version.py 2017-10-06 01:09:48 +0000 |
4641 | @@ -1,28 +1,16 @@ |
4642 | -from unittest import TestCase |
4643 | import mock |
4644 | import subprocess |
4645 | import os |
4646 | |
4647 | from curtin import version |
4648 | from curtin import __version__ as old_version |
4649 | - |
4650 | - |
4651 | -class CurtinVersionBase(TestCase): |
4652 | - def setUp(self): |
4653 | - super(CurtinVersionBase, self).setUp() |
4654 | - |
4655 | - def add_patch(self, target, attr): |
4656 | - """Patches specified target object and sets it as attr on test |
4657 | - instance also schedules cleanup""" |
4658 | - m = mock.patch(target, autospec=True) |
4659 | - p = m.start() |
4660 | - self.addCleanup(m.stop) |
4661 | - setattr(self, attr, p) |
4662 | - |
4663 | - |
4664 | -class TestCurtinVersion(CurtinVersionBase): |
4665 | - |
4666 | - def setUp(self): |
4667 | +from .helpers import CiTestCase |
4668 | + |
4669 | + |
4670 | +class TestCurtinVersion(CiTestCase): |
4671 | + |
4672 | + def setUp(self): |
4673 | + super(TestCurtinVersion, self).setUp() |
4674 | self.add_patch('subprocess.check_output', 'mock_subp') |
4675 | self.add_patch('os.path', 'mock_path') |
4676 | |
4677 | |
4678 | === modified file 'tests/vmtests/__init__.py' |
4679 | --- tests/vmtests/__init__.py 2017-06-12 19:43:55 +0000 |
4680 | +++ tests/vmtests/__init__.py 2017-10-06 01:09:48 +0000 |
4681 | @@ -38,6 +38,7 @@ |
4682 | CURTIN_VMTEST_IMAGE_SYNC = os.environ.get("CURTIN_VMTEST_IMAGE_SYNC", "1") |
4683 | IMAGE_SYNCS = [] |
4684 | TARGET_IMAGE_FORMAT = "raw" |
4685 | +TAR_DISKS = bool(int(os.environ.get("CURTIN_VMTEST_TAR_DISKS", "0"))) |
4686 | |
4687 | |
4688 | DEFAULT_BRIDGE = os.environ.get("CURTIN_VMTEST_BRIDGE", "user") |
4689 | @@ -335,7 +336,12 @@ |
4690 | __test__ = False |
4691 | arch_skip = [] |
4692 | boot_timeout = BOOT_TIMEOUT |
4693 | - collect_scripts = [] |
4694 | + collect_scripts = [textwrap.dedent(""" |
4695 | + cd OUTPUT_COLLECT_D |
4696 | + dpkg-query --show \ |
4697 | + --showformat='${db:Status-Abbrev}\t${Package}\t${Version}\n' \ |
4698 | + > debian-packages.txt 2> debian-packages.txt.err |
4699 | + """)] |
4700 | conf_file = "examples/tests/basic.yaml" |
4701 | nr_cpus = None |
4702 | dirty_disks = False |
4703 | @@ -368,6 +374,8 @@ |
4704 | target_krel = None |
4705 | target_ftype = "vmtest.root-tgz" |
4706 | |
4707 | + _debian_packages = None |
4708 | + |
4709 | def shortDescription(self): |
4710 | return None |
4711 | |
4712 | @@ -593,7 +601,7 @@ |
4713 | logger.debug("Interface name: {}".format(ifname)) |
4714 | iface = interfaces.get(ifname) |
4715 | hwaddr = iface.get('mac_address') |
4716 | - if hwaddr: |
4717 | + if iface['type'] == 'physical' and hwaddr: |
4718 | macs.append(hwaddr) |
4719 | netdevs = [] |
4720 | if len(macs) > 0: |
4721 | @@ -685,6 +693,12 @@ |
4722 | configs.append(excfg) |
4723 | logger.debug('Added extra config {}'.format(excfg)) |
4724 | |
4725 | + if cls.target_distro == "centos": |
4726 | + centos_default = 'examples/tests/centos_defaults.yaml' |
4727 | + configs.append(centos_default) |
4728 | + logger.info('Detected centos, adding default config %s', |
4729 | + centos_default) |
4730 | + |
4731 | if cls.multipath: |
4732 | disks = disks * cls.multipath_num_paths |
4733 | |
4734 | @@ -871,8 +885,11 @@ |
4735 | raise |
4736 | |
4737 | # capture curtin install log and webhook timings |
4738 | - util.subp(["tools/curtin-log-print", "--dumpfiles", cls.td.logs, |
4739 | - cls.reporting_log], capture=True) |
4740 | + try: |
4741 | + util.subp(["tools/curtin-log-print", "--dumpfiles", cls.td.logs, |
4742 | + cls.reporting_log], capture=True) |
4743 | + except util.ProcessExecutionError as error: |
4744 | + logger.debug('tools/curtin-log-print failed: %s', error) |
4745 | |
4746 | logger.info( |
4747 | "%s: setUpClass finished. took %.02f seconds. Running testcases.", |
4748 | @@ -929,7 +946,8 @@ |
4749 | clean_working_dir(cls.td.tmpdir, success, |
4750 | keep_pass=KEEP_DATA['pass'], |
4751 | keep_fail=KEEP_DATA['fail']) |
4752 | - |
4753 | + if TAR_DISKS: |
4754 | + tar_disks(cls.td.tmpdir) |
4755 | cls.cleanIscsiState(success, |
4756 | keep_pass=KEEP_DATA['pass'], |
4757 | keep_fail=KEEP_DATA['fail']) |
4758 | @@ -1143,6 +1161,18 @@ |
4759 | fp.write(json.dumps(data, indent=2, sort_keys=True, |
4760 | separators=(',', ': ')) + "\n") |
4761 | |
4762 | + @property |
4763 | + def debian_packages(self): |
4764 | + if self._debian_packages is None: |
4765 | + data = self.load_collect_file("debian-packages.txt") |
4766 | + pkgs = {} |
4767 | + for line in data.splitlines(): |
4768 | + # lines are <status>\t< |
4769 | + status, pkg, ver = line.split('\t') |
4770 | + pkgs[pkg] = {'status': status, 'version': ver} |
4771 | + self._debian_packages = pkgs |
4772 | + return self._debian_packages |
4773 | + |
4774 | |
4775 | class PsuedoVMBaseClass(VMBaseClass): |
4776 | # This mimics much of the VMBaseClass just with faster setUpClass |
4777 | @@ -1332,8 +1362,13 @@ |
4778 | output_device = '/dev/disk/by-id/virtio-%s' % OUTPUT_DISK_NAME |
4779 | |
4780 | collect_prep = textwrap.dedent("mkdir -p " + output_dir) |
4781 | - collect_post = textwrap.dedent( |
4782 | - 'tar -C "%s" -cf "%s" .' % (output_dir, output_device)) |
4783 | + collect_post = textwrap.dedent("""\ |
4784 | + cd {output_dir}\n |
4785 | + # remove any symlinks, but archive information about them. |
4786 | + # %Y target's file type, %P = path, %l = target of symlink |
4787 | + find -type l -printf "%Y\t%P\t%l\n" -delete > symlinks.txt |
4788 | + tar -cf "{output_device}" . |
4789 | + """).format(output_dir=output_dir, output_device=output_device) |
4790 | |
4791 | # copy /root for curtin config and install.log |
4792 | copy_rootdir = textwrap.dedent("cp -a /root " + output_dir) |
4793 | @@ -1410,6 +1445,23 @@ |
4794 | KEEP_DATA.update(data) |
4795 | |
4796 | |
4797 | +def tar_disks(tmpdir, outfile="disks.tar", diskmatch=".img"): |
4798 | + """ Tar up files in ``tmpdir``/disks that ends with the pattern supplied""" |
4799 | + |
4800 | + disks_dir = os.path.join(tmpdir, "disks") |
4801 | + if os.path.exists(disks_dir): |
4802 | + outfile = os.path.join(disks_dir, outfile) |
4803 | + disks = [os.path.join(disks_dir, disk) for disk in |
4804 | + os.listdir(disks_dir) if disk.endswith(diskmatch)] |
4805 | + cmd = ["tar", "--create", "--file=%s" % outfile, |
4806 | + "--verbose", "--remove-files", "--sparse"] |
4807 | + cmd.extend(disks) |
4808 | + logger.info('Taring %s disks sparsely to %s', len(disks), outfile) |
4809 | + util.subp(cmd, capture=True) |
4810 | + else: |
4811 | + logger.error('Failed to find "disks" dir under tmpdir: %s', tmpdir) |
4812 | + |
4813 | + |
4814 | def boot_log_wrap(name, func, cmd, console_log, timeout, purpose): |
4815 | logger.debug("%s[%s]: booting with timeout=%s log=%s cmd: %s", |
4816 | name, purpose, timeout, console_log, ' '.join(cmd)) |
4817 | |
4818 | === modified file 'tests/vmtests/releases.py' |
4819 | --- tests/vmtests/releases.py 2017-05-19 20:56:27 +0000 |
4820 | +++ tests/vmtests/releases.py 2017-10-06 01:09:48 +0000 |
4821 | @@ -77,22 +77,10 @@ |
4822 | target_release = "trusty" |
4823 | |
4824 | |
4825 | -class _VividBase(_UbuntuBase): |
4826 | - release = "vivid" |
4827 | - |
4828 | - |
4829 | -class _WilyBase(_UbuntuBase): |
4830 | - release = "wily" |
4831 | - |
4832 | - |
4833 | class _XenialBase(_UbuntuBase): |
4834 | release = "xenial" |
4835 | |
4836 | |
4837 | -class _YakketyBase(_UbuntuBase): |
4838 | - release = "yakkety" |
4839 | - |
4840 | - |
4841 | class _ZestyBase(_UbuntuBase): |
4842 | release = "zesty" |
4843 | |
4844 | @@ -110,10 +98,7 @@ |
4845 | trusty_hwe_w = _TrustyHWEW |
4846 | trusty_hwe_x = _TrustyHWEX |
4847 | trustyfromxenial = _TrustyFromXenial |
4848 | - vivid = _VividBase |
4849 | - wily = _WilyBase |
4850 | xenial = _XenialBase |
4851 | - yakkety = _YakketyBase |
4852 | zesty = _ZestyBase |
4853 | artful = _ArtfulBase |
4854 | |
4855 | |
4856 | === modified file 'tests/vmtests/test_apt_config_cmd.py' |
4857 | --- tests/vmtests/test_apt_config_cmd.py 2017-05-19 20:56:27 +0000 |
4858 | +++ tests/vmtests/test_apt_config_cmd.py 2017-10-06 01:09:48 +0000 |
4859 | @@ -55,10 +55,6 @@ |
4860 | __test__ = True |
4861 | |
4862 | |
4863 | -class YakketyTestAptConfigCMDCMD(relbase.yakkety, TestAptConfigCMD): |
4864 | - __test__ = True |
4865 | - |
4866 | - |
4867 | class ZestyTestAptConfigCMDCMD(relbase.zesty, TestAptConfigCMD): |
4868 | __test__ = True |
4869 | |
4870 | |
4871 | === modified file 'tests/vmtests/test_basic.py' |
4872 | --- tests/vmtests/test_basic.py 2017-05-19 20:56:27 +0000 |
4873 | +++ tests/vmtests/test_basic.py 2017-10-06 01:09:48 +0000 |
4874 | @@ -202,19 +202,10 @@ |
4875 | __test__ = True |
4876 | |
4877 | |
4878 | -class WilyTestBasic(relbase.wily, TestBasicAbs): |
4879 | - # EOL - 2016-07-28 |
4880 | - __test__ = False |
4881 | - |
4882 | - |
4883 | class XenialTestBasic(relbase.xenial, TestBasicAbs): |
4884 | __test__ = True |
4885 | |
4886 | |
4887 | -class YakketyTestBasic(relbase.yakkety, TestBasicAbs): |
4888 | - __test__ = True |
4889 | - |
4890 | - |
4891 | class ZestyTestBasic(relbase.zesty, TestBasicAbs): |
4892 | __test__ = True |
4893 | |
4894 | @@ -323,10 +314,6 @@ |
4895 | __test__ = True |
4896 | |
4897 | |
4898 | -class YakketyTestScsiBasic(relbase.yakkety, TestBasicScsiAbs): |
4899 | - __test__ = True |
4900 | - |
4901 | - |
4902 | class ZestyTestScsiBasic(relbase.zesty, TestBasicScsiAbs): |
4903 | __test__ = True |
4904 | |
4905 | |
4906 | === modified file 'tests/vmtests/test_bcache_basic.py' |
4907 | --- tests/vmtests/test_bcache_basic.py 2017-05-19 20:56:27 +0000 |
4908 | +++ tests/vmtests/test_bcache_basic.py 2017-10-06 01:09:48 +0000 |
4909 | @@ -59,10 +59,6 @@ |
4910 | __test__ = True |
4911 | |
4912 | |
4913 | -class YakketyBcacheBasic(relbase.yakkety, TestBcacheBasic): |
4914 | - __test__ = True |
4915 | - |
4916 | - |
4917 | class ZestyBcacheBasic(relbase.zesty, TestBcacheBasic): |
4918 | __test__ = True |
4919 | |
4920 | |
4921 | === modified file 'tests/vmtests/test_centos_basic.py' |
4922 | --- tests/vmtests/test_centos_basic.py 2016-12-02 02:04:27 +0000 |
4923 | +++ tests/vmtests/test_centos_basic.py 2017-10-06 01:09:48 +0000 |
4924 | @@ -1,5 +1,6 @@ |
4925 | from . import VMBaseClass |
4926 | from .releases import centos_base_vm_classes as relbase |
4927 | +from .test_network import TestNetworkBaseTestsAbs |
4928 | |
4929 | import textwrap |
4930 | |
4931 | @@ -9,10 +10,20 @@ |
4932 | __test__ = False |
4933 | conf_file = "examples/tests/centos_basic.yaml" |
4934 | extra_kern_args = "BOOTIF=eth0-52:54:00:12:34:00" |
4935 | + # XXX: command | tee output is required for Centos under SELinux |
4936 | + # http://danwalsh.livejournal.com/22860.html |
4937 | collect_scripts = [textwrap.dedent( |
4938 | """ |
4939 | cd OUTPUT_COLLECT_D |
4940 | cat /etc/fstab > fstab |
4941 | + rpm -qa | cat >rpm_qa |
4942 | + ifconfig -a | cat >ifconfig_a |
4943 | + ip a | cat >ip_a |
4944 | + cp -a /etc/sysconfig/network-scripts . |
4945 | + cp -a /var/log/messages . |
4946 | + cp -a /var/log/cloud-init* . |
4947 | + cp -a /var/lib/cloud ./var_lib_cloud |
4948 | + cp -a /run/cloud-init ./run_cloud-init |
4949 | """)] |
4950 | fstab_expected = { |
4951 | 'LABEL=cloudimg-rootfs': '/', |
4952 | @@ -40,3 +51,27 @@ |
4953 | # FIXME: test is disabled because the grub config script in target |
4954 | # specifies drive using hd(1,0) syntax, which breaks when the |
4955 | # installation medium is removed. other than this, the install works |
4956 | + |
4957 | + |
4958 | +class CentosTestBasicNetworkAbs(TestNetworkBaseTestsAbs): |
4959 | + conf_file = "examples/tests/centos_basic.yaml" |
4960 | + extra_kern_args = "BOOTIF=eth0-52:54:00:12:34:00" |
4961 | + collect_scripts = TestNetworkBaseTestsAbs.collect_scripts + [ |
4962 | + textwrap.dedent(""" |
4963 | + cd OUTPUT_COLLECT_D |
4964 | + cp -a /etc/sysconfig/network-scripts . |
4965 | + cp -a /var/log/cloud-init* . |
4966 | + cp -a /var/lib/cloud ./var_lib_cloud |
4967 | + cp -a /run/cloud-init ./run_cloud-init |
4968 | + """)] |
4969 | + |
4970 | + def test_etc_network_interfaces(self): |
4971 | + pass |
4972 | + |
4973 | + def test_etc_resolvconf(self): |
4974 | + pass |
4975 | + |
4976 | + |
4977 | +class Centos70BasicNetworkFromXenialTestBasic(relbase.centos70fromxenial, |
4978 | + CentosTestBasicNetworkAbs): |
4979 | + __test__ = True |
4980 | |
4981 | === modified file 'tests/vmtests/test_iscsi.py' |
4982 | --- tests/vmtests/test_iscsi.py 2017-05-19 20:56:27 +0000 |
4983 | +++ tests/vmtests/test_iscsi.py 2017-10-06 01:09:48 +0000 |
4984 | @@ -59,10 +59,6 @@ |
4985 | __test__ = True |
4986 | |
4987 | |
4988 | -class YakketyTestIscsiBasic(relbase.yakkety, TestBasicIscsiAbs): |
4989 | - __test__ = True |
4990 | - |
4991 | - |
4992 | class ZestyTestIscsiBasic(relbase.zesty, TestBasicIscsiAbs): |
4993 | __test__ = True |
4994 | |
4995 | |
4996 | === added file 'tests/vmtests/test_journald_reporter.py' |
4997 | --- tests/vmtests/test_journald_reporter.py 1970-01-01 00:00:00 +0000 |
4998 | +++ tests/vmtests/test_journald_reporter.py 2017-10-06 01:09:48 +0000 |
4999 | @@ -0,0 +1,52 @@ |
5000 | +from . import VMBaseClass |
The diff has been truncated for viewing.
I had failed to push the commits for 0.1.0~bzr519- 0ubuntu1 that was in artful.
thus, ryan's MP didn't have that debian/changelog entry
but the rest of it matched up well.
Thanks for catching the missed LP numbers on the vmtest fixes
vmtest: fix artful networking (LP: #1714028, LP: #1718216, LP: #1706744)
i'm really looking forward to getting curtin to git so we can utilize some of the same machinery that we have in place there for cloud-init.