Merge lp:~wesley-wiedenmeier/curtin/without-environment into lp:~curtin-dev/curtin/trunk

Proposed by Wesley Wiedenmeier
Status: Work in progress
Proposed branch: lp:~wesley-wiedenmeier/curtin/without-environment
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 1851 lines (+567/-472)
19 files modified
curtin/commands/__init__.py (+8/-0)
curtin/commands/apply_net.py (+18/-30)
curtin/commands/apt_config.py (+7/-15)
curtin/commands/block_meta.py (+70/-92)
curtin/commands/curthooks.py (+53/-63)
curtin/commands/extract.py (+10/-12)
curtin/commands/hook.py (+4/-6)
curtin/commands/in_target.py (+8/-15)
curtin/commands/install.py (+83/-127)
curtin/commands/main.py (+8/-7)
curtin/commands/net_meta.py (+10/-11)
curtin/commands/swap.py (+12/-20)
curtin/commands/system_install.py (+2/-5)
curtin/commands/system_upgrade.py (+2/-5)
curtin/config.py (+11/-11)
curtin/util.py (+50/-36)
curtin/workingdir.py (+106/-0)
tests/unittests/test_make_dname.py (+10/-17)
tests/unittests/test_workingdir.py (+95/-0)
To merge this branch: bzr merge lp:~wesley-wiedenmeier/curtin/without-environment
Reviewer Review Type Date Requested Status
curtin developers Pending
Review via email: mp+303384@code.launchpad.net

Description of the change

Remove global state and dependance on os.environ from curtin for simplification.
Branch isn't ready for merge yet, still need to debug. Everything should be in place though.

To post a comment you must log in.
451. By Wesley Wiedenmeier

In workingdir.WorkingDir, fix attrs used for env

452. By Wesley Wiedenmeier

Handle non existant config files from workingdir in config.load_command_config

453. By Wesley Wiedenmeier

In workingdir.WorkingDir, don't try to clean up dirs that haven't been created

454. By Wesley Wiedenmeier

Merge from trunk

455. By Wesley Wiedenmeier

In commands.extract, fix typo in test for missing target in args

456. By Wesley Wiedenmeier

Don't export environment during install

457. By Wesley Wiedenmeier

Update commands.hook to use target from commandline rather than environment

458. By Wesley Wiedenmeier

In commands.block_meta and commands.curt_hooks, use WorkingDir.mdadm_conf and
WorkingDir.crypttab rather than getting paths via WorkingDir.fstab

459. By Wesley Wiedenmeier

Add mdadm_conf and crypttab to state files in WorkingDir

460. By Wesley Wiedenmeier

Add util.has_content to test if a file is non empty and use it in curthooks
instead of os.path.exists as WorkingDir will cause the files to exist

461. By Wesley Wiedenmeier

Fixed use of WorkingDir.mdadm_config in commands.block_meta

Revision history for this message
Wesley Wiedenmeier (wesley-wiedenmeier) wrote :

I think this should be pretty much ready to go. I've run through several vmtests on it and it looks like everything works. It might still be good to run the full suite though, I haven't done that yet.

This merge ensures that all curtin subcommands support the flags '--target', '--scratch', '--state', and '--report-stack-prefix'. If these flags are already present in the COMMAND_ARGS for the subcommand, then it will not modify the arguments. If the flags are not present yet it will add them and mark them as hidden.

Instead of putting information about curtin's current state during an installation into the system environemnt, which is hard to mimic in tests and can be slightly inconsistant depending on how the system is set up, the flags for target, state, and scratch will always be set when running any curtin subcommands as part of the commands for an installation stage.

If an installation stage includes commands that are not curtin subcommands, then the command will be run normally.

The files under the state dir have paths defined centrally in curtin.workingdir, so that no path manipulation needs to be done to get their paths. curtin.workingdir also handles the creation and removal of temporary directories and the logic to unmount everything under the target directory so that it can be removed at the end of installation has been cleaned up and moved there.

This is a bit simpler than passing information in the system environment was too, because it does not require the subcommands to look for information about what they should be doing in multiple places, everything they need to run is in args.

Revision history for this message
Wesley Wiedenmeier (wesley-wiedenmeier) wrote :

There aren't any unittests for curtin.workingdir yet, I'll mark this needs review once I have those.

462. By Wesley Wiedenmeier

Started unittests for curtin.workingdir

463. By Wesley Wiedenmeier

Added unittests for curtin.workingdir

Unmerged revisions

463. By Wesley Wiedenmeier

Added unittests for curtin.workingdir

462. By Wesley Wiedenmeier

Started unittests for curtin.workingdir

461. By Wesley Wiedenmeier

Fixed use of WorkingDir.mdadm_config in commands.block_meta

460. By Wesley Wiedenmeier

Add util.has_content to test if a file is non empty and use it in curthooks
instead of os.path.exists as WorkingDir will cause the files to exist

459. By Wesley Wiedenmeier

Add mdadm_conf and crypttab to state files in WorkingDir

458. By Wesley Wiedenmeier

In commands.block_meta and commands.curt_hooks, use WorkingDir.mdadm_conf and
WorkingDir.crypttab rather than getting paths via WorkingDir.fstab

457. By Wesley Wiedenmeier

Update commands.hook to use target from commandline rather than environment

456. By Wesley Wiedenmeier

Don't export environment during install

455. By Wesley Wiedenmeier

In commands.extract, fix typo in test for missing target in args

454. By Wesley Wiedenmeier

Merge from trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'curtin/commands/__init__.py'
2--- curtin/commands/__init__.py 2013-08-26 14:09:15 +0000
3+++ curtin/commands/__init__.py 2016-08-22 05:07:32 +0000
4@@ -15,6 +15,10 @@
5 # You should have received a copy of the GNU Affero General Public License
6 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
7
8+import argparse
9+
10+HIDDEN_OPTS = ('--target', '--state', '--scratch', '--report-stack-prefix')
11+
12
13 def populate_one_subcmd(parser, options_dict, handler):
14 for ent in options_dict:
15@@ -22,6 +26,10 @@
16 if not isinstance(args, (list, tuple)):
17 args = (args,)
18 parser.add_argument(*args, **ent[1])
19+ for optname in HIDDEN_OPTS:
20+ if optname not in parser._option_string_actions.keys():
21+ parser.add_argument(optname, help=argparse.SUPPRESS,
22+ action='store', default=None)
23 parser.set_defaults(func=handler)
24
25 # vi: ts=4 expandtab syntax=python
26
27=== modified file 'curtin/commands/apply_net.py'
28--- curtin/commands/apply_net.py 2016-07-13 07:50:49 +0000
29+++ curtin/commands/apply_net.py 2016-08-22 05:07:32 +0000
30@@ -15,12 +15,11 @@
31 # You should have received a copy of the GNU Affero General Public License
32 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
33
34-import os
35 import sys
36
37 from .. import log
38 import curtin.net as net
39-import curtin.util as util
40+from curtin.workingdir import WorkingDir
41 from . import populate_one_subcmd
42
43
44@@ -49,33 +48,28 @@
45 def apply_net_main(args):
46 # curtin apply_net [--net-state=/config/netstate.yml] [--target=/]
47 # [--net-config=/config/maas_net.yml]
48- state = util.load_command_environment()
49-
50 log.basicConfig(stream=args.log_file, verbosity=1)
51
52- if args.target is not None:
53- state['target'] = args.target
54-
55- if args.net_state is not None:
56- state['network_state'] = args.net_state
57-
58- if args.net_config is not None:
59- state['network_config'] = args.net_config
60-
61- if state['target'] is None:
62+ if args.target is None:
63 sys.stderr.write("Unable to find target. "
64 "Use --target or set TARGET_MOUNT_POINT\n")
65 sys.exit(2)
66
67- if not state['network_config'] and not state['network_state']:
68+ if args.state and (not args.network_config and not args.network_state):
69+ wd = WorkingDir(target=args.target, state=args.state,
70+ scratch=args.scratch)
71+ args.network_state = wd.network_state
72+ args.network_config = wd.network_config
73+
74+ if not args.network_config and not args.network_state:
75 sys.stderr.write("Must provide at least config or state\n")
76 sys.exit(2)
77
78 LOG.info('Applying network configuration')
79 try:
80- apply_net(target=state['target'],
81- network_state=state['network_state'],
82- network_config=state['network_config'])
83+ apply_net(target=args.target,
84+ network_state=args.network_state,
85+ network_config=args.network_config)
86 except Exception:
87 LOG.exception('failed to apply network config')
88
89@@ -85,20 +79,14 @@
90
91 CMD_ARGUMENTS = (
92 ((('-s', '--net-state'),
93- {'help': ('file to read containing network state. '
94- 'defaults to env["OUTPUT_NETWORK_STATE"]'),
95- 'metavar': 'NETSTATE', 'action': 'store',
96- 'default': os.environ.get('OUTPUT_NETWORK_STATE')}),
97+ {'help': ('file to read containing network state.'),
98+ 'metavar': 'NETSTATE', 'action': 'store', 'default': None}),
99 (('-t', '--target'),
100- {'help': ('target filesystem root to add swap file to. '
101- 'default is env["TARGET_MOUNT_POINT"]'),
102- 'metavar': 'TARGET', 'action': 'store',
103- 'default': os.environ.get('TARGET_MOUNT_POINT')}),
104+ {'help': ('target filesystem to write data too.'),
105+ 'metavar': 'TARGET', 'action': 'store', 'default': None}),
106 (('-c', '--net-config'),
107- {'help': ('file to read containing curtin network config.'
108- 'defaults to env["OUTPUT_NETWORK_CONFIG"]'),
109- 'metavar': 'NETCONFIG', 'action': 'store',
110- 'default': os.environ.get('OUTPUT_NETWORK_CONFIG')})))
111+ {'help': ('file to read containing curtin network config.'),
112+ 'metavar': 'NETCONFIG', 'action': 'store', 'default': None})))
113
114
115 def POPULATE_SUBCMD(parser):
116
117=== modified file 'curtin/commands/apt_config.py'
118--- curtin/commands/apt_config.py 2016-08-18 14:53:38 +0000
119+++ curtin/commands/apt_config.py 2016-08-22 05:07:32 +0000
120@@ -552,25 +552,18 @@
121 """
122 cfg = config.load_command_config(args, {})
123
124- if args.target is not None:
125- target = args.target
126- else:
127- state = util.load_command_environment()
128- target = state['target']
129-
130- if target is None:
131- sys.stderr.write("Unable to find target. "
132- "Use --target or set TARGET_MOUNT_POINT\n")
133+ if args.target is None:
134+ sys.stderr.write("Unable to find target. Use --target.\n")
135 sys.exit(2)
136
137 apt_cfg = cfg.get("apt")
138 # if no apt config section is available, do nothing
139 if apt_cfg is not None:
140 LOG.debug("Handling apt to target %s with config %s",
141- target, apt_cfg)
142+ args.target, apt_cfg)
143 try:
144- with util.ChrootableTarget(target, sys_resolvconf=True):
145- handle_apt(apt_cfg, target)
146+ with util.ChrootableTarget(args.target, sys_resolvconf=True):
147+ handle_apt(apt_cfg, args.target)
148 except (RuntimeError, TypeError, ValueError, IOError):
149 LOG.exception("Failed to configure apt features '%s'", apt_cfg)
150 sys.exit(1)
151@@ -651,9 +644,8 @@
152 'metavar': 'FILE', 'type': argparse.FileType("rb"),
153 'dest': 'cfgopts', 'default': []}),
154 (('-t', '--target'),
155- {'help': 'chroot to target. default is env[TARGET_MOUNT_POINT]',
156- 'action': 'store', 'metavar': 'TARGET',
157- 'default': os.environ.get('TARGET_MOUNT_POINT')}),)
158+ {'help': 'chroot to target.', 'action': 'store',
159+ 'metavar': 'TARGET', 'default': None}),)
160 )
161
162
163
164=== modified file 'curtin/commands/block_meta.py'
165--- curtin/commands/block_meta.py 2016-08-02 07:59:23 +0000
166+++ curtin/commands/block_meta.py 2016-08-22 05:07:32 +0000
167@@ -16,7 +16,7 @@
168 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
169
170 from collections import OrderedDict
171-from curtin import (block, config, util)
172+from curtin import (block, config, util, workingdir)
173 from curtin.block import mdadm
174 from curtin.log import LOG
175 from curtin.block import mkfs
176@@ -57,14 +57,15 @@
177
178 def block_meta(args):
179 # main entry point for the block-meta command.
180- state = util.load_command_environment()
181- cfg = config.load_command_config(args, state)
182- if args.mode == CUSTOM or cfg.get("storage") is not None:
183- meta_custom(args)
184- elif args.mode in (SIMPLE, SIMPLE_BOOT):
185- meta_simple(args)
186- else:
187- raise NotImplementedError("mode=%s is not implemented" % args.mode)
188+ cfg = config.load_command_config(args)
189+ with workingdir.WorkingDir(state=args.state, target=args.target,
190+ scratch=args.scratch) as wd:
191+ if args.mode == CUSTOM or cfg.get("storage") is not None:
192+ meta_custom(args, wd)
193+ elif args.mode in (SIMPLE, SIMPLE_BOOT):
194+ meta_simple(args, wd)
195+ else:
196+ raise NotImplementedError("mode=%s is not implemented" % args.mode)
197
198
199 def logtime(msg, func, *args, **kwargs):
200@@ -275,9 +276,7 @@
201 return ''.join(c if c in valid else '-' for c in dname)
202
203
204-def make_dname(volume, storage_config):
205- state = util.load_command_environment()
206- rules_dir = os.path.join(state['scratch'], "rules.d")
207+def make_dname(volume, storage_config, rules_dir):
208 vol = storage_config.get(volume)
209 path = get_path_to_storage_volume(volume, storage_config)
210 ptuuid = None
211@@ -427,7 +426,7 @@
212 return volume_path
213
214
215-def disk_handler(info, storage_config):
216+def disk_handler(info, storage_config, wd):
217 ptable = info.get('ptable')
218
219 disk = get_path_to_storage_volume(info.get('id'), storage_config)
220@@ -497,7 +496,7 @@
221
222 # Make the name if needed
223 if info.get('name'):
224- make_dname(info.get('id'), storage_config)
225+ make_dname(info.get('id'), storage_config, wd.rules_dir)
226
227
228 def getnumberoflogicaldisks(device, storage_config):
229@@ -525,7 +524,7 @@
230 return last_partnum
231
232
233-def partition_handler(info, storage_config):
234+def partition_handler(info, storage_config, wd):
235 device = info.get('device')
236 size = info.get('size')
237 flag = info.get('flag')
238@@ -673,10 +672,10 @@
239 mode=info.get('wipe'))
240 # Make the name if needed
241 if storage_config.get(device).get('name') and partition_type != 'extended':
242- make_dname(info.get('id'), storage_config)
243-
244-
245-def format_handler(info, storage_config):
246+ make_dname(info.get('id'), storage_config, wd.rules_dir)
247+
248+
249+def format_handler(info, storage_config, wd):
250 volume = info.get('volume')
251 if not volume:
252 raise ValueError("volume must be specified for partition '%s'" %
253@@ -695,8 +694,7 @@
254 mkfs.mkfs_from_config(volume_path, info)
255
256
257-def mount_handler(info, storage_config):
258- state = util.load_command_environment()
259+def mount_handler(info, storage_config, wd):
260 path = info.get('path')
261 filesystem = storage_config.get(info.get('device'))
262 if not path and filesystem.get('fstype') != "swap":
263@@ -711,7 +709,7 @@
264 # Figure out what point should be
265 while len(path) > 0 and path[0] == "/":
266 path = path[1:]
267- mount_point = os.path.join(state['target'], path)
268+ mount_point = os.path.join(wd.target, path)
269
270 # Create mount point if does not exist
271 util.ensure_dir(mount_point)
272@@ -720,8 +718,8 @@
273 util.subp(['mount', volume_path, mount_point])
274
275 # Add volume to fstab
276- if state['fstab']:
277- with open(state['fstab'], "a") as fp:
278+ if wd.fstab:
279+ with open(wd.fstab, "a") as fp:
280 if volume.get('type') in ["raid", "bcache",
281 "disk", "lvm_partition"]:
282 location = get_path_to_storage_volume(volume.get('id'),
283@@ -749,7 +747,7 @@
284 LOG.info("fstab not in environment, so not writing")
285
286
287-def lvm_volgroup_handler(info, storage_config):
288+def lvm_volgroup_handler(info, storage_config, wd):
289 devices = info.get('devices')
290 device_paths = []
291 name = info.get('name')
292@@ -790,7 +788,7 @@
293 util.subp(cmd)
294
295
296-def lvm_partition_handler(info, storage_config):
297+def lvm_partition_handler(info, storage_config, wd):
298 volgroup = storage_config.get(info.get('volgroup')).get('name')
299 name = info.get('name')
300 if not volgroup:
301@@ -832,11 +830,10 @@
302 raise ValueError("Partition tables on top of lvm logical volumes is \
303 not supported")
304
305- make_dname(info.get('id'), storage_config)
306-
307-
308-def dm_crypt_handler(info, storage_config):
309- state = util.load_command_environment()
310+ make_dname(info.get('id'), storage_config, wd.rules_dir)
311+
312+
313+def dm_crypt_handler(info, storage_config, wd):
314 volume = info.get('volume')
315 key = info.get('key')
316 keysize = info.get('keysize')
317@@ -874,22 +871,21 @@
318
319 os.remove(tmp_keyfile)
320
321- # A crypttab will be created in the same directory as the fstab in the
322- # configuration. This will then be copied onto the system later
323- if state['fstab']:
324- crypt_tab_location = os.path.join(os.path.split(state['fstab'])[0],
325- "crypttab")
326- uuid = block.get_volume_uuid(volume_path)
327- with open(crypt_tab_location, "a") as fp:
328- fp.write("%s UUID=%s none luks\n" % (dm_name, uuid))
329- else:
330- LOG.info("fstab configuration is not present in environment, so \
331- cannot locate an appropriate directory to write crypttab in \
332- so not writing crypttab")
333-
334-
335-def raid_handler(info, storage_config):
336- state = util.load_command_environment()
337+ # the WorkingDir instance will ensure that there is a state dir to write
338+ # to, even if this is being used outside of install and it will be removed
339+ # immediately
340+ mdadm_scan_data = mdadm.mdadm_detail_scan()
341+ util.write_file(wd.crypttab, mdadm_scan_data)
342+
343+ # the WorkingDir instance will ensure that there is a state dir to write
344+ # to, even if this is being used outside of install and it will be removed
345+ # immediately
346+ uuid = block.get_volume_uuid(volume_path)
347+ util.write_file(wd.crypttab,
348+ "{} UUID={} none luks\n".format(dm_name, uuid))
349+
350+
351+def raid_handler(info, storage_config, wd):
352 devices = info.get('devices')
353 raidlevel = info.get('raidlevel')
354 spare_devices = info.get('spare_devices')
355@@ -939,22 +935,13 @@
356 info.get('mdname', ''))
357
358 # Make dname rule for this dev
359- make_dname(info.get('id'), storage_config)
360+ make_dname(info.get('id'), storage_config, wd.rules_dir)
361
362- # A mdadm.conf will be created in the same directory as the fstab in the
363- # configuration. This will then be copied onto the installed system later.
364- # The file must also be written onto the running system to enable it to run
365- # mdadm --assemble and continue installation
366- if state['fstab']:
367- mdadm_location = os.path.join(os.path.split(state['fstab'])[0],
368- "mdadm.conf")
369- mdadm_scan_data = mdadm.mdadm_detail_scan()
370- with open(mdadm_location, "w") as fp:
371- fp.write(mdadm_scan_data)
372- else:
373- LOG.info("fstab configuration is not present in the environment, so \
374- cannot locate an appropriate directory to write mdadm.conf in, \
375- so not writing mdadm.conf")
376+ # the WorkingDir instance will ensure that there is a state dir to write
377+ # to, even if this is being used outside of install and it will be removed
378+ # immediately
379+ mdadm_scan_data = mdadm.mdadm_detail_scan()
380+ util.write_file(wd.mdadm_config, mdadm_scan_data)
381
382 # If ptable is specified, call disk_handler on this mdadm device to create
383 # the table
384@@ -962,7 +949,7 @@
385 disk_handler(info, storage_config)
386
387
388-def bcache_handler(info, storage_config):
389+def bcache_handler(info, storage_config, wd):
390 backing_device = get_path_to_storage_volume(info.get('backing_device'),
391 storage_config)
392 cache_device = get_path_to_storage_volume(info.get('cache_device'),
393@@ -1099,7 +1086,7 @@
394
395 if info.get('name'):
396 # Make dname rule for this dev
397- make_dname(info.get('id'), storage_config)
398+ make_dname(info.get('id'), storage_config, wd.rules_dir)
399
400 if info.get('ptable'):
401 raise ValueError("Partition tables on top of lvm logical volumes is \
402@@ -1123,7 +1110,7 @@
403 return OrderedDict((d["id"], d) for (i, d) in enumerate(scfg))
404
405
406-def meta_custom(args):
407+def meta_custom(args, wd):
408 """Does custom partitioning based on the layout provided in the config
409 file. Section with the name storage contains information on which
410 partitions on which disks to create. It also contains information about
411@@ -1142,13 +1129,10 @@
412 'bcache': bcache_handler
413 }
414
415- state = util.load_command_environment()
416- cfg = config.load_command_config(args, state)
417-
418+ cfg = config.load_command_config(args)
419 storage_config_dict = extract_storage_ordered_dict(cfg)
420-
421- # set up reportstack
422- stack_prefix = state.get('report_stack_prefix', '')
423+ stack_prefix = (args.report_stack_prefix
424+ if args.report_stack_prefix else '')
425
426 for item_id, command in storage_config_dict.items():
427 handler = command_handlers.get(command['type'])
428@@ -1156,10 +1140,10 @@
429 raise ValueError("unknown command type '%s'" % command['type'])
430 with events.ReportEventStack(
431 name=stack_prefix, reporting_enabled=True, level="INFO",
432- description="configuring %s: %s" % (command['type'],
433- command['id'])):
434+ description="configuring %s: %s" % (
435+ command['type'], command['id'])):
436 try:
437- handler(command, storage_config_dict)
438+ handler(command, storage_config_dict, wd)
439 except Exception as error:
440 LOG.error("An error occured handling '%s': %s - %s" %
441 (item_id, type(error).__name__, error))
442@@ -1168,20 +1152,14 @@
443 return 0
444
445
446-def meta_simple(args):
447+def meta_simple(args, wd):
448 """Creates a root partition. If args.mode == SIMPLE_BOOT, it will also
449 create a separate /boot partition.
450 """
451- state = util.load_command_environment()
452-
453- cfg = config.load_command_config(args, state)
454-
455- if args.target is not None:
456- state['target'] = args.target
457-
458- if state['target'] is None:
459- sys.stderr.write("Unable to find target. "
460- "Use --target or set TARGET_MOUNT_POINT\n")
461+ cfg = config.load_command_config(args)
462+
463+ if wd.target is None:
464+ sys.stderr.write("Unable to find target. Use --target\n")
465 sys.exit(2)
466
467 devices = args.devices
468@@ -1250,7 +1228,7 @@
469 # we have at least one dd-able image
470 # we will only take the first one
471 rootdev = write_image_to_disk(dd_images[0], devname)
472- util.subp(['mount', rootdev, state['target']])
473+ util.subp(['mount', rootdev, wd.target])
474 return 0
475
476 # helper partition will forcibly set up partition there
477@@ -1303,11 +1281,11 @@
478 # mkfs for root partition first and mount
479 cmd = ['mkfs.%s' % args.fstype, '-q', '-L', 'cloudimg-rootfs', rootdev]
480 logtime(' '.join(cmd), util.subp, cmd)
481- util.subp(['mount', rootdev, state['target']])
482+ util.subp(['mount', rootdev, wd.target])
483
484 if bootpt['enabled']:
485- # create 'boot' directory in state['target']
486- boot_dir = os.path.join(state['target'], 'boot')
487+ # create 'boot' directory in target
488+ boot_dir = os.path.join(wd.target, 'boot')
489 util.subp(['mkdir', boot_dir])
490 # mkfs for boot partition and mount
491 cmd = ['mkfs.%s' % bootpt['fstype'],
492@@ -1316,12 +1294,12 @@
493 util.subp(['mount', bootdev, boot_dir])
494
495 if ptfmt == "uefi":
496- uefi_dir = os.path.join(state['target'], 'boot', 'efi')
497+ uefi_dir = os.path.join(target, 'boot', 'efi')
498 util.ensure_dir(uefi_dir)
499 util.subp(['mount', uefi_dev, uefi_dir])
500
501- if state['fstab']:
502- with open(state['fstab'], "w") as fp:
503+ if wd.fstab:
504+ with open(wd.fstab, "w") as fp:
505 if bootpt['enabled']:
506 fp.write("LABEL=%s /boot %s defaults 0 0\n" %
507 (bootpt['label'], bootpt['fstype']))
508
509=== modified file 'curtin/commands/curthooks.py'
510--- curtin/commands/curthooks.py 2016-07-29 17:19:20 +0000
511+++ curtin/commands/curthooks.py 2016-08-22 05:07:32 +0000
512@@ -31,16 +31,17 @@
513 from curtin import net
514 from curtin.reporter import events
515 from curtin.commands import apt_config
516+from curtin.workingdir import WorkingDir
517
518 from . import populate_one_subcmd
519
520 CMD_ARGUMENTS = (
521 ((('-t', '--target'),
522- {'help': 'operate on target. default is env[TARGET_MOUNT_POINT]',
523- 'action': 'store', 'metavar': 'TARGET', 'default': None}),
524+ {'help': 'operate on target.', 'action': 'store',
525+ 'metavar': 'TARGET', 'default': None}),
526 (('-c', '--config'),
527- {'help': 'operate on config. default is env[CONFIG]',
528- 'action': 'store', 'metavar': 'CONFIG', 'default': None}),
529+ {'help': 'operate on config.', 'action': 'store',
530+ 'metavar': 'CONFIG', 'default': None}),
531 )
532 )
533
534@@ -386,7 +387,7 @@
535
536 def copy_fstab(fstab, target):
537 if not fstab:
538- LOG.warn("fstab variable not in state, not copying fstab")
539+ LOG.warn("no fstab provided to copy, not copying fstab")
540 return
541
542 shutil.copy(fstab, os.path.sep.join([target, 'etc/fstab']))
543@@ -410,10 +411,7 @@
544 'etc/mdadm/mdadm.conf']))
545
546
547-def apply_networking(target, state):
548- netstate = state.get('network_state')
549- netconf = state.get('network_config')
550- interfaces = state.get('interfaces')
551+def apply_networking(target, network_state, network_config, interfaces):
552
553 def is_valid_src(infile):
554 with open(infile, 'r') as fp:
555@@ -423,12 +421,14 @@
556 return False
557
558 ns = None
559- if is_valid_src(netstate):
560+ if is_valid_src(network_state):
561 LOG.debug("applying network_state")
562- ns = net.network_state.from_state_file(netstate)
563- elif is_valid_src(netconf):
564+ ns = net.network_state.from_state_file(network_state)
565+ elif is_valid_src(network_config):
566 LOG.debug("applying network_config")
567- ns = net.parse_net_config(netconf)
568+ ns = net.parse_net_config(network_config)
569+ else:
570+ LOG.debug("no available network_state or network_config, none applied")
571
572 if ns is not None:
573 net.render_network_state(target=target, network_state=ns)
574@@ -593,7 +593,7 @@
575 update_initramfs(target, all_kernels=True)
576
577
578-def install_missing_packages(cfg, target):
579+def install_missing_packages(cfg, target, stack_prefix):
580 ''' describe which operation types will require specific packages
581
582 'custom_config_key': {
583@@ -642,10 +642,8 @@
584 needed_packages.append(pkg)
585
586 if needed_packages:
587- state = util.load_command_environment()
588 with events.ReportEventStack(
589- name=state.get('report_stack_prefix'),
590- reporting_enabled=True, level="INFO",
591+ name=stack_prefix, reporting_enabled=True, level="INFO",
592 description="Installing packages on target system: " +
593 str(needed_packages)):
594 util.install_packages(needed_packages, target=target)
595@@ -674,95 +672,87 @@
596
597
598 def curthooks(args):
599- state = util.load_command_environment()
600-
601- if args.target is not None:
602- target = args.target
603- else:
604- target = state['target']
605-
606- if target is None:
607- sys.stderr.write("Unable to find target. "
608- "Use --target or set TARGET_MOUNT_POINT\n")
609+ """
610+ configure installed system
611+ """
612+ wd = WorkingDir(target=args.target, state=args.state, scratch=args.scratch)
613+
614+ if wd.target is None:
615+ sys.stderr.write("Unable to find target. Use --target \n")
616 sys.exit(2)
617
618+ stack_prefix = (args.report_stack_prefix
619+ if hasattr(args, 'report_stack_prefix') else '')
620+
621 # if network-config hook exists in target,
622 # we do not run the builtin
623- if util.run_hook_if_exists(target, 'curtin-hooks'):
624+ if util.run_hook_if_exists(wd.target, 'curtin-hooks'):
625 sys.exit(0)
626
627- cfg = config.load_command_config(args, state)
628- stack_prefix = state.get('report_stack_prefix', '')
629+ cfg = config.load_command_config(args)
630
631 with events.ReportEventStack(
632 name=stack_prefix, reporting_enabled=True, level="INFO",
633 description="writing config files and configuring apt"):
634- write_files(cfg, target)
635- do_apt_config(cfg, target)
636- disable_overlayroot(cfg, target)
637+ write_files(cfg, wd.target)
638+ do_apt_config(cfg, wd.target)
639+ disable_overlayroot(cfg, wd.target)
640
641 # packages may be needed prior to installing kernel
642- install_missing_packages(cfg, target)
643+ install_missing_packages(cfg, wd.target, stack_prefix)
644
645 # If a mdadm.conf file was created by block_meta than it needs to be copied
646 # onto the target system
647- mdadm_location = os.path.join(os.path.split(state['fstab'])[0],
648- "mdadm.conf")
649- if os.path.exists(mdadm_location):
650- copy_mdadm_conf(mdadm_location, target)
651+ if util.has_content(wd.mdadm_config):
652+ copy_mdadm_conf(wd.mdadm_config, wd.target)
653 # as per https://bugs.launchpad.net/ubuntu/+source/mdadm/+bug/964052
654 # reconfigure mdadm
655 util.subp(['dpkg-reconfigure', '--frontend=noninteractive', 'mdadm'],
656- data=None, target=target)
657+ data=None, target=wd.target)
658+
659+ # copy crypttab if any was written. this is done before install_kernel so
660+ # that the initramfs will be built by when the kernel is installed
661+ if util.has_content(wd.crypttab):
662+ copy_crypttab(wd.crypttab, wd.target)
663
664 with events.ReportEventStack(
665 name=stack_prefix, reporting_enabled=True, level="INFO",
666 description="installing kernel"):
667- setup_zipl(cfg, target)
668- install_kernel(cfg, target)
669- run_zipl(cfg, target)
670+ setup_zipl(cfg, wd.target)
671+ install_kernel(cfg, wd.target)
672+ run_zipl(cfg, wd.target)
673
674- restore_dist_interfaces(cfg, target)
675+ restore_dist_interfaces(cfg, wd.target)
676
677 with events.ReportEventStack(
678 name=stack_prefix, reporting_enabled=True, level="INFO",
679 description="setting up swap"):
680- add_swap(cfg, target, state.get('fstab'))
681+ add_swap(cfg, wd.target, wd.fstab)
682
683 with events.ReportEventStack(
684 name=stack_prefix, reporting_enabled=True, level="INFO",
685 description="apply networking"):
686- apply_networking(target, state)
687+ apply_networking(wd.target, wd.network_state,
688+ wd.network_config, wd.interfaces)
689
690 with events.ReportEventStack(
691 name=stack_prefix, reporting_enabled=True, level="INFO",
692 description="writing etc/fstab"):
693- copy_fstab(state.get('fstab'), target)
694+ copy_fstab(wd.fstab, wd.target)
695
696 with events.ReportEventStack(
697 name=stack_prefix, reporting_enabled=True, level="INFO",
698 description="configuring multipath"):
699- detect_and_handle_multipath(cfg, target)
700+ detect_and_handle_multipath(cfg, wd.target)
701
702 with events.ReportEventStack(
703 name=stack_prefix, reporting_enabled=True, level="INFO",
704 description="updating packages on target system"):
705- system_upgrade(cfg, target)
706-
707- # If a crypttab file was created by block_meta than it needs to be copied
708- # onto the target system, and update_initramfs() needs to be run, so that
709- # the cryptsetup hooks are properly configured on the installed system and
710- # it will be able to open encrypted volumes at boot.
711- crypttab_location = os.path.join(os.path.split(state['fstab'])[0],
712- "crypttab")
713- if os.path.exists(crypttab_location):
714- copy_crypttab(crypttab_location, target)
715- update_initramfs(target)
716+ system_upgrade(cfg, wd.target)
717
718 # If udev dname rules were created, copy them to target
719- udev_rules_d = os.path.join(state['scratch'], "rules.d")
720- if os.path.isdir(udev_rules_d):
721- copy_dname_rules(udev_rules_d, target)
722+ if os.path.isdir(wd.rules_dir):
723+ copy_dname_rules(wd.rules_dir, wd.target)
724
725 # As a rule, ARMv7 systems don't use grub. This may change some
726 # day, but for now, assume no. They do require the initramfs
727@@ -772,9 +762,9 @@
728 if (machine.startswith('armv7') or
729 machine.startswith('s390x') or
730 machine.startswith('aarch64') and not util.is_uefi_bootable()):
731- update_initramfs(target)
732+ update_initramfs(wd.target)
733 else:
734- setup_grub(cfg, target)
735+ setup_grub(cfg, wd.target)
736
737 sys.exit(0)
738
739
740=== modified file 'curtin/commands/extract.py'
741--- curtin/commands/extract.py 2016-07-13 07:50:49 +0000
742+++ curtin/commands/extract.py 2016-08-22 05:07:32 +0000
743@@ -27,9 +27,8 @@
744
745 CMD_ARGUMENTS = (
746 ((('-t', '--target'),
747- {'help': ('target directory to extract to (root) '
748- '[default TARGET_MOUNT_POINT]'),
749- 'action': 'store', 'default': os.environ.get('TARGET_MOUNT_POINT')}),
750+ {'help': ('target directory to extract to (root)'),
751+ 'action': 'store', 'default': None}),
752 (('sources',),
753 {'help': 'the sources to install [default read from CONFIG]',
754 'nargs': '*'}),
755@@ -83,11 +82,9 @@
756 if not args.target:
757 raise ValueError("Target must be defined or set in environment")
758
759- state = curtin.util.load_command_environment()
760- cfg = curtin.config.load_command_config(args, state)
761+ cfg = curtin.config.load_command_config(args)
762
763 sources = args.sources
764- target = args.target
765 if not sources:
766 if not cfg.get('sources'):
767 raise ValueError("'sources' must be on cmdline or in config")
768@@ -96,8 +93,9 @@
769 if isinstance(sources, dict):
770 sources = [sources[k] for k in sorted(sources.keys())]
771
772- LOG.debug("Installing sources: %s to target at %s" % (sources, target))
773- stack_prefix = state.get('report_stack_prefix', '')
774+ LOG.debug("Installing sources: %s to target at %s" %
775+ (sources, args.target))
776+ stack_prefix = args.report_stack_prefix if args.report_stack_prefix else ''
777
778 for source in sources:
779 with events.ReportEventStack(
780@@ -107,16 +105,16 @@
781 if source['type'].startswith('dd-'):
782 continue
783 if source['uri'].startswith("cp://"):
784- copy_to_target(source['uri'], target)
785+ copy_to_target(source['uri'], args.target)
786 elif os.path.isfile(source['uri']):
787- extract_root_tgz_file(source['uri'], target)
788+ extract_root_tgz_file(source['uri'], args.target)
789 elif source['uri'].startswith("file://"):
790 extract_root_tgz_file(
791 source['uri'][len("file://"):],
792- target)
793+ args.target)
794 elif (source['uri'].startswith("http://") or
795 source['uri'].startswith("https://")):
796- extract_root_tgz_url(source['uri'], target)
797+ extract_root_tgz_url(source['uri'], args.target)
798 else:
799 raise TypeError(
800 "do not know how to extract '%s'" %
801
802=== modified file 'curtin/commands/hook.py'
803--- curtin/commands/hook.py 2016-07-13 07:50:49 +0000
804+++ curtin/commands/hook.py 2016-08-22 05:07:32 +0000
805@@ -15,7 +15,6 @@
806 # You should have received a copy of the GNU Affero General Public License
807 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
808
809-import os
810 import sys
811
812 import curtin.config
813@@ -25,17 +24,16 @@
814 from . import populate_one_subcmd
815
816 CMD_ARGUMENTS = (
817- ((('target',),
818- {'help': 'finalize the provided directory [default TARGET_MOUNT_POINT]',
819- 'action': 'store', 'default': os.environ.get('TARGET_MOUNT_POINT'),
820- 'nargs': '?'}),
821+ ((('-t', '--target'),
822+ {'help': 'finalize the provided directory',
823+ 'action': 'store', 'default': None, 'nargs': '?'}),
824 )
825 )
826
827
828 def hook(args):
829 if not args.target:
830- raise ValueError("Target must be provided or set in environment")
831+ raise ValueError("Target must be provided")
832
833 LOG.debug("Finalizing %s" % args.target)
834 curtin.util.run_hook_if_exists(args.target, "finalize")
835
836=== modified file 'curtin/commands/in_target.py'
837--- curtin/commands/in_target.py 2016-07-13 07:50:49 +0000
838+++ curtin/commands/in_target.py 2016-08-22 05:07:32 +0000
839@@ -34,9 +34,8 @@
840 {'help': 'capture/swallow output of command',
841 'action': 'store_true', 'default': False}),
842 (('-t', '--target'),
843- {'help': 'chroot to target. default is env[TARGET_MOUNT_POINT]',
844- 'action': 'store', 'metavar': 'TARGET',
845- 'default': os.environ.get('TARGET_MOUNT_POINT')}),
846+ {'help': 'chroot to target.', 'action': 'store',
847+ 'metavar': 'TARGET', 'default': None}),
848 ('command_args',
849 {'help': 'run a command chrooted in the target', 'nargs': '*'}),
850 )
851@@ -56,26 +55,20 @@
852
853
854 def in_target_main(args):
855- if args.target is not None:
856- target = args.target
857- else:
858- state = util.load_command_environment()
859- target = state['target']
860-
861 if args.target is None:
862- sys.stderr.write("Unable to find target. "
863- "Use --target or set TARGET_MOUNT_POINT\n")
864+ sys.stderr.write("Unable to find target. Use --target.\n")
865 sys.exit(2)
866
867- if os.path.abspath(target) == "/":
868+ if os.path.abspath(args.target) == "/":
869 cmd = args.command_args
870 else:
871- cmd = ['chroot', target] + args.command_args
872+ cmd = ['chroot', args.target] + args.command_args
873
874- if target == "/" and args.allow_daemons:
875+ if args.target == "/" and args.allow_daemons:
876 ret = run_command(cmd, args.interactive, capture=args.capture)
877 else:
878- with util.ChrootableTarget(target, allow_daemons=args.allow_daemons):
879+ with util.ChrootableTarget(
880+ args.target, allow_daemons=args.allow_daemons):
881 ret = run_command(cmd, args.interactive)
882
883 sys.exit(ret)
884
885=== modified file 'curtin/commands/install.py'
886--- curtin/commands/install.py 2016-07-13 07:57:01 +0000
887+++ curtin/commands/install.py 2016-08-22 05:07:32 +0000
888@@ -16,21 +16,18 @@
889 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
890
891 import argparse
892-import json
893 import os
894 import re
895 import shlex
896-import shutil
897 import subprocess
898 import sys
899-import tempfile
900
901-from curtin import block
902 from curtin import config
903 from curtin import util
904 from curtin.log import LOG
905 from curtin.reporter.legacy import load_reporter
906 from curtin.reporter import events
907+from curtin.workingdir import WorkingDir
908 from . import populate_one_subcmd
909
910 INSTALL_LOG = "/var/log/curtin/install.log"
911@@ -83,54 +80,13 @@
912 pass
913
914
915-class WorkingDir(object):
916- def __init__(self, config):
917- top_d = tempfile.mkdtemp()
918- state_d = os.path.join(top_d, 'state')
919- target_d = os.path.join(top_d, 'target')
920- scratch_d = os.path.join(top_d, 'scratch')
921- for p in (state_d, target_d, scratch_d):
922- os.mkdir(p)
923-
924- netconf_f = os.path.join(state_d, 'network_config')
925- netstate_f = os.path.join(state_d, 'network_state')
926- interfaces_f = os.path.join(state_d, 'interfaces')
927- config_f = os.path.join(state_d, 'config')
928- fstab_f = os.path.join(state_d, 'fstab')
929-
930- with open(config_f, "w") as fp:
931- json.dump(config, fp)
932-
933- # just touch these files to make sure they exist
934- for f in (interfaces_f, config_f, fstab_f, netconf_f, netstate_f):
935- with open(f, "ab") as fp:
936- pass
937-
938- self.scratch = scratch_d
939- self.target = target_d
940- self.top = top_d
941- self.interfaces = interfaces_f
942- self.netconf = netconf_f
943- self.netstate = netstate_f
944- self.fstab = fstab_f
945- self.config = config
946- self.config_file = config_f
947-
948- def env(self):
949- return ({'WORKING_DIR': self.scratch, 'OUTPUT_FSTAB': self.fstab,
950- 'OUTPUT_INTERFACES': self.interfaces,
951- 'OUTPUT_NETWORK_CONFIG': self.netconf,
952- 'OUTPUT_NETWORK_STATE': self.netstate,
953- 'TARGET_MOUNT_POINT': self.target,
954- 'CONFIG': self.config_file})
955-
956-
957 class Stage(object):
958
959- def __init__(self, name, commands, env, reportstack=None, logfile=None):
960+ def __init__(self, name, commands, workingdir,
961+ reportstack=None, logfile=None):
962 self.name = name
963 self.commands = commands
964- self.env = env
965+ self.workingdir = workingdir
966 if logfile is None:
967 logfile = INSTALL_LOG
968 self.install_log = self._open_install_log(logfile)
969@@ -179,35 +135,42 @@
970 name=cmdname, description="running '%s'" % ' '.join(cmd),
971 parent=self.reportstack, level="DEBUG")
972
973- env = self.env.copy()
974+ env = self.workingdir.env
975 env['CURTIN_REPORTSTACK'] = cur_res.fullname
976
977+ extra_args = ['='.join(arg) for arg in {
978+ '--target': self.workingdir.target,
979+ '--scratch': self.workingdir.scratch,
980+ '--state': self.workingdir.state,
981+ '--report-stack-prefix': cur_res.fullname
982+ }.items()]
983 shell = not isinstance(cmd, list)
984- with util.LogTimer(LOG.debug, cmdname):
985- with cur_res:
986- try:
987- sp = subprocess.Popen(
988- cmd, stdout=subprocess.PIPE,
989- stderr=subprocess.STDOUT,
990- env=env, shell=shell)
991- except OSError as e:
992- LOG.warn("%s command failed", cmdname)
993- raise util.ProcessExecutionError(cmd=cmd, reason=e)
994-
995- output = b""
996- while True:
997- data = sp.stdout.read(1)
998- if not data and sp.poll() is not None:
999- break
1000- self.write(data)
1001- output += data
1002-
1003- rc = sp.returncode
1004- if rc != 0:
1005- LOG.warn("%s command failed", cmdname)
1006- raise util.ProcessExecutionError(
1007- stdout=output, stderr="",
1008- exit_code=rc, cmd=cmd)
1009+ if cmd.startswith('curtin') if shell else cmd[0] == 'curtin':
1010+ cmd += ' '.join(extra_args) if shell else extra_args
1011+
1012+ with util.LogTimer(LOG.debug, cmdname), cur_res:
1013+ try:
1014+ sp = subprocess.Popen(cmd, stdout=subprocess.PIPE,
1015+ stderr=subprocess.STDOUT,
1016+ shell=shell)
1017+ except OSError as e:
1018+ LOG.warn("%s command failed", cmdname)
1019+ raise util.ProcessExecutionError(cmd=cmd, reason=e)
1020+
1021+ output = b""
1022+ while True:
1023+ data = sp.stdout.read(1)
1024+ if not data and sp.poll() is not None:
1025+ break
1026+ self.write(data)
1027+ output += data
1028+
1029+ rc = sp.returncode
1030+ if rc != 0:
1031+ LOG.warn("%s command failed", cmdname)
1032+ raise util.ProcessExecutionError(
1033+ stdout=output, stderr="",
1034+ exit_code=rc, cmd=cmd)
1035
1036
1037 def apply_power_state(pstate):
1038@@ -375,60 +338,53 @@
1039 post_files = cfg.get('post_files', [logfile])
1040 legacy_reporter = load_reporter(cfg)
1041 legacy_reporter.files = post_files
1042-
1043 args.reportstack.post_files = post_files
1044- try:
1045- dd_images = util.get_dd_images(cfg.get('sources', {}))
1046- if len(dd_images) > 1:
1047- raise ValueError("You may not use more then one disk image")
1048-
1049- workingd = WorkingDir(cfg)
1050- LOG.debug(workingd.env())
1051- env = os.environ.copy()
1052- env.update(workingd.env())
1053-
1054- for name in cfg.get('stages'):
1055- desc = STAGE_DESCRIPTIONS.get(name, "stage %s" % name)
1056- reportstack = events.ReportEventStack(
1057- "stage-%s" % name, description=desc,
1058- parent=args.reportstack)
1059- env['CURTIN_REPORTSTACK'] = reportstack.fullname
1060-
1061- with reportstack:
1062- commands_name = '%s_commands' % name
1063- with util.LogTimer(LOG.debug, 'stage_%s' % name):
1064- stage = Stage(name, cfg.get(commands_name, {}), env,
1065- reportstack=reportstack, logfile=logfile)
1066- stage.run()
1067-
1068- if apply_kexec(cfg.get('kexec'), workingd.target):
1069- cfg['power_state'] = {'mode': 'reboot', 'delay': 'now',
1070- 'message': "'rebooting with kexec'"}
1071-
1072- writeline(logfile, INSTALL_PASS_MSG)
1073- out = sys.stdout
1074- msg = "%s\n" % INSTALL_PASS_MSG
1075- if hasattr(out, 'buffer'):
1076- out = out.buffer
1077- msg = msg.encode()
1078- out.write(msg)
1079- out.flush()
1080- legacy_reporter.report_success()
1081- except Exception as e:
1082- exp_msg = "Installation failed with exception: %s" % e
1083- writeline(logfile, exp_msg)
1084- LOG.error(exp_msg)
1085- legacy_reporter.report_failure(exp_msg)
1086- raise e
1087- finally:
1088- for d in ('sys', 'dev', 'proc'):
1089- util.do_umount(os.path.join(workingd.target, d))
1090- mounted = block.get_mountpoints()
1091- mounted.sort(key=lambda x: -1 * x.count("/"))
1092- for d in filter(lambda x: workingd.target in x, mounted):
1093- util.do_umount(d)
1094- util.do_umount(workingd.target)
1095- shutil.rmtree(workingd.top)
1096+
1097+ # create tmp dir
1098+ with WorkingDir(config=cfg) as workingd:
1099+ try:
1100+ dd_images = util.get_dd_images(cfg.get('sources', {}))
1101+ if len(dd_images) > 1:
1102+ raise ValueError("You may not use more then one disk image")
1103+
1104+ LOG.debug(workingd.env)
1105+ env = os.environ.copy()
1106+ env.update(workingd.env)
1107+
1108+ for name in cfg.get('stages'):
1109+ desc = STAGE_DESCRIPTIONS.get(name, "stage %s" % name)
1110+ reportstack = events.ReportEventStack(
1111+ "stage-%s" % name, description=desc,
1112+ parent=args.reportstack)
1113+ env['CURTIN_REPORTSTACK'] = reportstack.fullname
1114+
1115+ with reportstack:
1116+ commands_name = '%s_commands' % name
1117+ with util.LogTimer(LOG.debug, 'stage_%s' % name):
1118+ stage = Stage(name, cfg.get(commands_name, {}),
1119+ workingd, reportstack=reportstack,
1120+ logfile=logfile)
1121+ stage.run()
1122+
1123+ if apply_kexec(cfg.get('kexec'), workingd.target):
1124+ cfg['power_state'] = {'mode': 'reboot', 'delay': 'now',
1125+ 'message': "'rebooting with kexec'"}
1126+
1127+ writeline(logfile, INSTALL_PASS_MSG)
1128+ out = sys.stdout
1129+ msg = "%s\n" % INSTALL_PASS_MSG
1130+ if hasattr(out, 'buffer'):
1131+ out = out.buffer
1132+ msg = msg.encode()
1133+ out.write(msg)
1134+ out.flush()
1135+ legacy_reporter.report_success()
1136+ except Exception as e:
1137+ exp_msg = "Installation failed with exception: %s" % e
1138+ writeline(logfile, exp_msg)
1139+ LOG.error(exp_msg)
1140+ legacy_reporter.report_failure(exp_msg)
1141+ raise e
1142
1143 apply_power_state(cfg.get('power_state'))
1144
1145
1146=== modified file 'curtin/commands/main.py'
1147--- curtin/commands/main.py 2016-07-26 13:43:15 +0000
1148+++ curtin/commands/main.py 2016-08-22 05:07:32 +0000
1149@@ -157,7 +157,7 @@
1150 elif flag in ('--set'):
1151 config.merge_cmdarg(cfg, val)
1152 else:
1153- cfg = config.load_command_config(args, util.load_command_environment())
1154+ cfg = config.load_command_config(args)
1155
1156 args.config = cfg
1157
1158@@ -196,13 +196,14 @@
1159 # set up the reportstack
1160 update_configuration(cfg.get('reporting', {}))
1161
1162- stack_prefix = (os.environ.get("CURTIN_REPORTSTACK", "") +
1163- "/cmd-%s" % args.subcmd)
1164- if stack_prefix.startswith("/"):
1165- stack_prefix = stack_prefix[1:]
1166- os.environ["CURTIN_REPORTSTACK"] = stack_prefix
1167+ if not args.report_stack_prefix:
1168+ args.report_stack_prefix = ''
1169+ args.report_stack_prefix = (args.report_stack_prefix +
1170+ '/cmd-{}'.format(args.subcmd))
1171+ if args.report_stack_prefix.startswith("/"):
1172+ args.report_stack_prefix = args.report_stack_prefix[1:]
1173 args.reportstack = events.ReportEventStack(
1174- name=stack_prefix, reporting_enabled=True, level="DEBUG",
1175+ name=args.report_stack_prefix, reporting_enabled=True, level="DEBUG",
1176 description="curtin command %s" % args.subcmd)
1177
1178 try:
1179
1180=== modified file 'curtin/commands/net_meta.py'
1181--- curtin/commands/net_meta.py 2016-07-13 07:50:49 +0000
1182+++ curtin/commands/net_meta.py 2016-08-22 05:07:32 +0000
1183@@ -21,6 +21,7 @@
1184
1185 from curtin import net
1186 from curtin.log import LOG
1187+from curtin.workingdir import WorkingDir
1188 import curtin.util as util
1189 import curtin.config as config
1190
1191@@ -71,8 +72,7 @@
1192
1193
1194 def interfaces_custom(args):
1195- state = util.load_command_environment()
1196- cfg = config.load_command_config(args, state)
1197+ cfg = config.load_command_config(args)
1198
1199 network_config = cfg.get('network', [])
1200 if not network_config:
1201@@ -93,8 +93,7 @@
1202 if util.run_hook_if_exists(args.target, 'network-config'):
1203 sys.exit(0)
1204
1205- state = util.load_command_environment()
1206- cfg = config.load_command_config(args, state)
1207+ cfg = config.load_command_config(args)
1208 if cfg.get("network") is not None:
1209 args.mode = "custom"
1210
1211@@ -124,7 +123,9 @@
1212
1213 LOG.debug("net-meta mode is '%s'. devices=%s", args.mode, devices)
1214
1215- output_network_config = os.environ.get("OUTPUT_NETWORK_CONFIG", "")
1216+ wd = WorkingDir(state=args.state)
1217+ output_network_config = wd.network_config
1218+
1219 if args.mode == "copy":
1220 if not args.target:
1221 raise argparse.ArgumentTypeError("mode 'copy' requires --target")
1222@@ -167,13 +168,11 @@
1223 {'help': 'which devices to operate on', 'action': 'append',
1224 'metavar': 'DEVICE', 'type': network_device}),
1225 (('-o', '--output'),
1226- {'help': 'file to write to. defaults to env["OUTPUT_INTERFACES"] or "-"',
1227- 'metavar': 'IFILE', 'action': 'store',
1228- 'default': os.environ.get('OUTPUT_INTERFACES', "-")}),
1229+ {'help': 'file to write to.', 'metavar': 'IFILE',
1230+ 'action': 'store', 'default': None}),
1231 (('-t', '--target'),
1232- {'help': 'operate on target. default is env[TARGET_MOUNT_POINT]',
1233- 'action': 'store', 'metavar': 'TARGET',
1234- 'default': os.environ.get('TARGET_MOUNT_POINT')}),
1235+ {'help': 'operate on target.', 'action': 'store',
1236+ 'metavar': 'TARGET', 'default': None}),
1237 ('mode', {'help': 'meta-mode to use',
1238 'choices': ['dhcp', 'copy', 'auto', 'custom']})
1239 )
1240
1241=== modified file 'curtin/commands/swap.py'
1242--- curtin/commands/swap.py 2016-07-13 07:50:49 +0000
1243+++ curtin/commands/swap.py 2016-08-22 05:07:32 +0000
1244@@ -15,28 +15,23 @@
1245 # You should have received a copy of the GNU Affero General Public License
1246 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
1247
1248-import os
1249 import sys
1250
1251 import curtin.swap as swap
1252 import curtin.util as util
1253+import curtin.workingdir as workingdir
1254
1255 from . import populate_one_subcmd
1256
1257
1258 def swap_main(args):
1259 # curtin swap [--size=4G] [--target=/] [--fstab=/etc/fstab] [swap]
1260- state = util.load_command_environment()
1261-
1262- if args.target is not None:
1263- state['target'] = args.target
1264-
1265- if args.fstab is not None:
1266- state['fstab'] = args.fstab
1267-
1268- if state['target'] is None:
1269- sys.stderr.write("Unable to find target. "
1270- "Use --target or set TARGET_MOUNT_POINT\n")
1271+ if not args.fstab and args.state:
1272+ wd = workingdir.WorkingDir(state=args.state)
1273+ args.fstab = wd.fstab
1274+
1275+ if args.target is None:
1276+ sys.stderr.write("Unable to find target. Use --target\n")
1277 sys.exit(2)
1278
1279 size = args.size
1280@@ -53,7 +48,7 @@
1281 if args.maxsize is not None:
1282 args.maxsize = util.human2bytes(args.maxsize)
1283
1284- swap.setup_swapfile(target=state['target'], fstab=state['fstab'],
1285+ swap.setup_swapfile(target=args.target, fstab=args.fstab,
1286 swapfile=args.swapfile, size=size,
1287 maxsize=args.maxsize)
1288 sys.exit(2)
1289@@ -61,14 +56,11 @@
1290
1291 CMD_ARGUMENTS = (
1292 ((('-f', '--fstab'),
1293- {'help': 'file to write to. defaults to env["OUTPUT_FSTAB"]',
1294- 'metavar': 'FSTAB', 'action': 'store',
1295- 'default': os.environ.get('OUTPUT_FSTAB')}),
1296+ {'help': 'file to write to.', 'metavar': 'FSTAB', 'action': 'store',
1297+ 'default': None}),
1298 (('-t', '--target'),
1299- {'help': ('target filesystem root to add swap file to. '
1300- 'default is env[TARGET_MOUNT_POINT]'),
1301- 'action': 'store', 'metavar': 'TARGET',
1302- 'default': os.environ.get('TARGET_MOUNT_POINT')}),
1303+ {'help': ('target filesystem root to add swap file to.'),
1304+ 'action': 'store', 'metavar': 'TARGET', 'default': None}),
1305 (('-s', '--size'),
1306 {'help': 'size of swap file (eg: 1G, 1500M, 1024K, 100000. def: "auto")',
1307 'default': None, 'action': 'store'}),
1308
1309=== modified file 'curtin/commands/system_install.py'
1310--- curtin/commands/system_install.py 2016-07-13 07:50:49 +0000
1311+++ curtin/commands/system_install.py 2016-08-22 05:07:32 +0000
1312@@ -15,7 +15,6 @@
1313 # You should have received a copy of the GNU Affero General Public License
1314 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
1315
1316-import os
1317 import sys
1318
1319 import curtin.util as util
1320@@ -46,10 +45,8 @@
1321 {'help': ('do not disable running of daemons during upgrade.'),
1322 'action': 'store_true', 'default': False}),
1323 (('-t', '--target'),
1324- {'help': ('target root to upgrade. '
1325- 'default is env[TARGET_MOUNT_POINT]'),
1326- 'action': 'store', 'metavar': 'TARGET',
1327- 'default': os.environ.get('TARGET_MOUNT_POINT')}),
1328+ {'help': ('target root to upgrade.'),
1329+ 'action': 'store', 'metavar': 'TARGET', 'default': None}),
1330 ('packages',
1331 {'help': 'the list of packages to install',
1332 'metavar': 'PACKAGES', 'action': 'store', 'nargs': '+'}),
1333
1334=== modified file 'curtin/commands/system_upgrade.py'
1335--- curtin/commands/system_upgrade.py 2016-07-13 07:50:49 +0000
1336+++ curtin/commands/system_upgrade.py 2016-08-22 05:07:32 +0000
1337@@ -15,7 +15,6 @@
1338 # You should have received a copy of the GNU Affero General Public License
1339 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
1340
1341-import os
1342 import sys
1343
1344 import curtin.util as util
1345@@ -45,10 +44,8 @@
1346 {'help': ('do not disable running of daemons during upgrade.'),
1347 'action': 'store_true', 'default': False}),
1348 (('-t', '--target'),
1349- {'help': ('target root to upgrade. '
1350- 'default is env[TARGET_MOUNT_POINT]'),
1351- 'action': 'store', 'metavar': 'TARGET',
1352- 'default': os.environ.get('TARGET_MOUNT_POINT')}),
1353+ {'help': ('target root to upgrade.'),
1354+ 'action': 'store', 'metavar': 'TARGET', 'default': None}),
1355 )
1356 )
1357
1358
1359=== modified file 'curtin/config.py'
1360--- curtin/config.py 2016-03-11 15:14:46 +0000
1361+++ curtin/config.py 2016-08-22 05:07:32 +0000
1362@@ -15,8 +15,11 @@
1363 # You should have received a copy of the GNU Affero General Public License
1364 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
1365
1366+import os
1367+import json
1368 import yaml
1369-import json
1370+
1371+from curtin import workingdir
1372
1373 ARCHIVE_HEADER = "#curtin-config-archive"
1374 ARCHIVE_TYPE = "text/curtin-config-archive"
1375@@ -119,17 +122,14 @@
1376 return load_config_archive(content)
1377
1378
1379-def load_command_config(args, state):
1380+def load_command_config(args):
1381+ cfg = {}
1382 if hasattr(args, 'config') and args.config:
1383- return args.config
1384- else:
1385- # state 'config' points to a file with fully rendered config
1386- cfg_file = state.get('config')
1387-
1388- if not cfg_file:
1389- cfg = {}
1390- else:
1391- cfg = load_config(cfg_file)
1392+ cfg = args.config
1393+ elif hasattr(args, 'state') and args.state is not None:
1394+ wd = workingdir.WorkingDir(state=args.state)
1395+ if os.path.exists(wd.config):
1396+ cfg = load_config(wd.config)
1397 return cfg
1398
1399
1400
1401=== modified file 'curtin/util.py'
1402--- curtin/util.py 2016-07-29 17:19:20 +0000
1403+++ curtin/util.py 2016-08-22 05:07:32 +0000
1404@@ -254,42 +254,6 @@
1405 (self.msg, time.time() - self.start))
1406
1407
1408-def is_mounted(target, src=None, opts=None):
1409- # return whether or not src is mounted on target
1410- mounts = ""
1411- with open("/proc/mounts", "r") as fp:
1412- mounts = fp.read()
1413-
1414- for line in mounts.splitlines():
1415- if line.split()[1] == os.path.abspath(target):
1416- return True
1417- return False
1418-
1419-
1420-def do_mount(src, target, opts=None):
1421- # mount src at target with opts and return True
1422- # if already mounted, return False
1423- if opts is None:
1424- opts = []
1425- if isinstance(opts, str):
1426- opts = [opts]
1427-
1428- if is_mounted(target, src, opts):
1429- return False
1430-
1431- ensure_dir(target)
1432- cmd = ['mount'] + opts + [src, target]
1433- subp(cmd)
1434- return True
1435-
1436-
1437-def do_umount(mountpoint):
1438- if not is_mounted(mountpoint):
1439- return False
1440- subp(['umount', mountpoint])
1441- return True
1442-
1443-
1444 def ensure_dir(path, mode=None):
1445 try:
1446 os.makedirs(path)
1447@@ -323,6 +287,13 @@
1448 raise e
1449
1450
1451+def has_content(path):
1452+ """
1453+ check if file exists and is not empty
1454+ """
1455+ return os.path.exists(path) and os.stat(path).st_size > 0
1456+
1457+
1458 def disable_daemons_in_root(target):
1459 contents = "\n".join(
1460 ['#!/bin/sh',
1461@@ -355,6 +326,49 @@
1462 return True
1463
1464
1465+def is_mounted(target, src=None, opts=None):
1466+ """
1467+ return whether or not src is mounted on target
1468+ """
1469+ mounts = ""
1470+ with open("/proc/mounts", "r") as fp:
1471+ mounts = fp.read()
1472+
1473+ for line in mounts.splitlines():
1474+ if line.split()[1] == os.path.abspath(target):
1475+ return True
1476+ return False
1477+
1478+
1479+def do_mount(src, target, opts=None):
1480+ """
1481+ mount src at target with opts and return True
1482+ if already mounted, return False
1483+ """
1484+ if opts is None:
1485+ opts = []
1486+ if isinstance(opts, str):
1487+ opts = [opts]
1488+
1489+ if is_mounted(target, src, opts):
1490+ return False
1491+
1492+ ensure_dir(target)
1493+ cmd = ['mount'] + opts + [src, target]
1494+ subp(cmd)
1495+ return True
1496+
1497+
1498+def do_umount(mountpoint):
1499+ """
1500+ unmount specified mountpoint
1501+ """
1502+ if not is_mounted(mountpoint):
1503+ return False
1504+ subp(['umount', mountpoint])
1505+ return True
1506+
1507+
1508 class ChrootableTarget(object):
1509 def __init__(self, target, allow_daemons=False, sys_resolvconf=True):
1510 if target is None:
1511
1512=== added file 'curtin/workingdir.py'
1513--- curtin/workingdir.py 1970-01-01 00:00:00 +0000
1514+++ curtin/workingdir.py 2016-08-22 05:07:32 +0000
1515@@ -0,0 +1,106 @@
1516+# Copyright (C) 2016 Canonical Ltd.
1517+#
1518+# Author: Wesley Wiedenmeier <wesley.wiedenmeier@canonical.com>
1519+#
1520+# Curtin is free software: you can redistribute it and/or modify it under
1521+# the terms of the GNU Affero General Public License as published by the
1522+# Free Software Foundation, either version 3 of the License, or (at your
1523+# option) any later version.
1524+#
1525+# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
1526+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
1527+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
1528+# more details.
1529+#
1530+# You should have received a copy of the GNU Affero General Public License
1531+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
1532+
1533+# this has to be in a seperate module from util because curtin.block does a
1534+# relative import from util before the name 'block' exists in sys.modules. this
1535+# is permitted in python3 but not python2.7
1536+
1537+from curtin import (block, util)
1538+from curtin.log import LOG
1539+
1540+import os
1541+import tempfile
1542+import shutil
1543+
1544+
1545+def umount_all(basedir):
1546+ """
1547+ unmount all mountpoints under base dir
1548+ """
1549+ mounts = sorted((m for m in block.get_mountpoints() if basedir in m),
1550+ key=lambda x: -1 * x.count(os.path.sep))
1551+ LOG.debug('removing mounts: %s', mounts)
1552+ for mount in mounts:
1553+ util.do_umount(mount)
1554+
1555+
1556+class WorkingDir(object):
1557+ def __init__(self, target=None, state=None, scratch=None, config=None):
1558+ self._config_dict = config
1559+
1560+ # base tmpdir will not be used if state target and scratch specified
1561+ self.base = os.path.join(tempfile.gettempdir(),
1562+ next(tempfile._get_candidate_names()))
1563+ self.target = target if target else os.path.join(self.base, 'target')
1564+ self.state = state if state else os.path.join(self.base, 'state')
1565+ self.scratch = (scratch if scratch else
1566+ os.path.join(self.base, 'scratch'))
1567+ self.rules_dir = os.path.join(self.scratch, 'rules.d')
1568+
1569+ # only dirs which are not passed in should be removed on exit
1570+ self._dirs_to_clean = []
1571+ if not state:
1572+ self._dirs_to_clean.append(self.state)
1573+ if not scratch:
1574+ self._dirs_to_clean.append(self.scratch)
1575+ if not target:
1576+ self._dirs_to_clean.append(self.target)
1577+ self._dirs_to_clean.append(self.base)
1578+
1579+ # set state files
1580+ self._state_files = ('fstab', 'config', 'interfaces',
1581+ 'network_state', 'network_config',
1582+ 'mdadm_config', 'crypttab')
1583+ for state_file in self._state_files:
1584+ setattr(self, state_file, os.path.join(self.state, state_file))
1585+
1586+ def __enter__(self):
1587+ # create subdirs, basedir is not created if not needed
1588+ for path in (self.target, self.state, self.scratch, self.rules_dir):
1589+ util.ensure_dir(path)
1590+
1591+ # touch state files to make sure they exist
1592+ for state_file in self._state_files:
1593+ with open(getattr(self, state_file), 'ab'):
1594+ pass
1595+
1596+ # dump config
1597+ if self._config_dict is not None:
1598+ util.write_file(
1599+ self.config, util.json_dumps(self._config_dict), omode='wb')
1600+
1601+ return self
1602+
1603+ def __exit__(self, etype, value, trace):
1604+ for path in [p for p in self._dirs_to_clean if os.path.exists(p)]:
1605+ umount_all(path)
1606+ shutil.rmtree(path)
1607+
1608+ # raise all encountered errors
1609+ return False
1610+
1611+ @property
1612+ def env(self):
1613+ return {
1614+ 'WORKING_DIR': self.scratch,
1615+ 'OUTPUT_FSTAB': self.fstab,
1616+ 'OUTPUT_INTERFACES': self.interfaces,
1617+ 'OUTPUT_NETWORK_CONFIG': self.network_config,
1618+ 'OUTPUT_NETWORK_STATE': self.network_state,
1619+ 'TARGET_MOUNT_POINT': self.target,
1620+ 'CONFIG': self.config
1621+ }
1622
1623=== modified file 'tests/unittests/test_make_dname.py'
1624--- tests/unittests/test_make_dname.py 2016-07-13 17:07:11 +0000
1625+++ tests/unittests/test_make_dname.py 2016-08-22 05:07:32 +0000
1626@@ -8,7 +8,6 @@
1627
1628
1629 class TestMakeDname(TestCase):
1630- state = {'scratch': '/tmp/null'}
1631 rules_d = '/tmp/null/rules.d'
1632 rule_file = '/tmp/null/rules.d/{}.rules'
1633 storage_config = {
1634@@ -56,7 +55,6 @@
1635 disk_ptuuid = str(uuid.uuid1())
1636 mock_util.subp.side_effect = self._make_mock_subp_blkid(
1637 disk_ptuuid, self.disk_blkid)
1638- mock_util.load_command_environment.return_value = self.state
1639 rule_identifiers = [
1640 ('DEVTYPE', 'disk'),
1641 ('ID_PART_TABLE_UUID', disk_ptuuid)
1642@@ -64,7 +62,7 @@
1643
1644 # simple run
1645 res_dname = 'main_disk'
1646- block_meta.make_dname('disk1', self.storage_config)
1647+ block_meta.make_dname('disk1', self.storage_config, self.rules_d)
1648 mock_util.ensure_dir.assert_called_with(self.rules_d)
1649 self.assertTrue(mock_log.debug.called)
1650 self.assertFalse(mock_log.warning.called)
1651@@ -74,7 +72,7 @@
1652
1653 # run invalid dname
1654 res_dname = 'in_valid-name----------disk'
1655- block_meta.make_dname('disk2', self.storage_config)
1656+ block_meta.make_dname('disk2', self.storage_config, self.rules_d)
1657 self.assertTrue(mock_log.warning.called)
1658 mock_util.write_file.assert_called_with(
1659 self.rule_file.format(res_dname),
1660@@ -86,17 +84,16 @@
1661 def test_make_dname_failures(self, mock_util, mock_get_path, mock_log):
1662 mock_util.subp.side_effect = self._make_mock_subp_blkid(
1663 '', self.trusty_blkid)
1664- mock_util.load_command_environment.return_value = self.state
1665
1666 warning_msg = "Can't find a uuid for volume: {}. Skipping dname."
1667
1668 # disk with no PT_UUID
1669- block_meta.make_dname('disk1', self.storage_config)
1670+ block_meta.make_dname('disk1', self.storage_config, self.rules_d)
1671 mock_log.warning.assert_called_with(warning_msg.format('disk1'))
1672 self.assertFalse(mock_util.write_file.called)
1673
1674 # partition with no PART_UUID
1675- block_meta.make_dname('disk1p1', self.storage_config)
1676+ block_meta.make_dname('disk1p1', self.storage_config, self.rules_d)
1677 mock_log.warning.assert_called_with(warning_msg.format('disk1p1'))
1678 self.assertFalse(mock_util.write_file.called)
1679
1680@@ -107,7 +104,6 @@
1681 part_uuid = str(uuid.uuid1())
1682 mock_util.subp.side_effect = self._make_mock_subp_blkid(
1683 part_uuid, self.part_blkid)
1684- mock_util.load_command_environment.return_value = self.state
1685
1686 rule_identifiers = [
1687 ('DEVTYPE', 'partition'),
1688@@ -116,7 +112,7 @@
1689
1690 # simple run
1691 res_dname = 'main_disk-part1'
1692- block_meta.make_dname('disk1p1', self.storage_config)
1693+ block_meta.make_dname('disk1p1', self.storage_config, self.rules_d)
1694 mock_util.ensure_dir.assert_called_with(self.rules_d)
1695 self.assertTrue(mock_log.debug.called)
1696 self.assertFalse(mock_log.warning.called)
1697@@ -126,7 +122,7 @@
1698
1699 # run invalid dname
1700 res_dname = 'in_valid-name----------disk-part1'
1701- block_meta.make_dname('disk2p1', self.storage_config)
1702+ block_meta.make_dname('disk2p1', self.storage_config, self.rules_d)
1703 self.assertTrue(mock_log.warning.called)
1704 mock_util.write_file.assert_called_with(
1705 self.rule_file.format(res_dname),
1706@@ -140,12 +136,11 @@
1707 mock_mdadm):
1708 md_uuid = str(uuid.uuid1())
1709 mock_mdadm.mdadm_query_detail.return_value = {'MD_UUID': md_uuid}
1710- mock_util.load_command_environment.return_value = self.state
1711 rule_identifiers = [('MD_UUID', md_uuid)]
1712
1713 # simple
1714 res_dname = 'mdadm_name'
1715- block_meta.make_dname('md_id', self.storage_config)
1716+ block_meta.make_dname('md_id', self.storage_config, self.rules_d)
1717 self.assertTrue(mock_log.debug.called)
1718 self.assertFalse(mock_log.warning.called)
1719 mock_util.write_file.assert_called_with(
1720@@ -154,7 +149,7 @@
1721
1722 # invalid name
1723 res_dname = 'mdadm-name'
1724- block_meta.make_dname('md_id2', self.storage_config)
1725+ block_meta.make_dname('md_id2', self.storage_config, self.rules_d)
1726 self.assertTrue(mock_log.warning.called)
1727 mock_util.write_file.assert_called_with(
1728 self.rule_file.format(res_dname),
1729@@ -165,12 +160,10 @@
1730 @mock.patch('curtin.commands.block_meta.util')
1731 def test_make_dname_lvm_partition(self, mock_util, mock_get_path,
1732 mock_log):
1733- mock_util.load_command_environment.return_value = self.state
1734-
1735 # simple
1736 res_dname = 'vg1-lpartition1'
1737 rule_identifiers = [('DM_NAME', res_dname)]
1738- block_meta.make_dname('lpart_id', self.storage_config)
1739+ block_meta.make_dname('lpart_id', self.storage_config, self.rules_d)
1740 self.assertTrue(mock_log.debug.called)
1741 self.assertFalse(mock_log.warning.called)
1742 mock_util.write_file.assert_called_with(
1743@@ -180,7 +173,7 @@
1744 # with invalid name
1745 res_dname = 'vg1-lvm-part-2'
1746 rule_identifiers = [('DM_NAME', 'vg1-lvm part/2')]
1747- block_meta.make_dname('lpart2_id', self.storage_config)
1748+ block_meta.make_dname('lpart2_id', self.storage_config, self.rules_d)
1749 self.assertTrue(mock_log.warning.called)
1750 mock_util.write_file.assert_called_with(
1751 self.rule_file.format(res_dname),
1752
1753=== added file 'tests/unittests/test_workingdir.py'
1754--- tests/unittests/test_workingdir.py 1970-01-01 00:00:00 +0000
1755+++ tests/unittests/test_workingdir.py 2016-08-22 05:07:32 +0000
1756@@ -0,0 +1,95 @@
1757+from unittest import TestCase
1758+from curtin import workingdir
1759+import mock
1760+import os
1761+
1762+
1763+class TestWorkingDir(TestCase):
1764+ base_tmp = '/tmp'
1765+ tmp_name = 'asdf'
1766+ mock_cfg = {'testkey': 'value'}
1767+
1768+ @mock.patch('curtin.workingdir.tempfile')
1769+ def test_attributes(self, mock_tempfile):
1770+ mock_tempfile.gettempdir.return_value = self.base_tmp
1771+ mock_tempfile._get_candidate_names.return_value = (
1772+ (name for name in (self.tmp_name,)))
1773+ wd = workingdir.WorkingDir()
1774+ self.assertIsNone(wd._config_dict)
1775+ self.assertTrue(mock_tempfile.gettempdir.called)
1776+ self.assertTrue(mock_tempfile._get_candidate_names.called)
1777+ for name in ('target', 'state', 'scratch'):
1778+ self.assertEqual(getattr(wd, name), os.path.sep.join(
1779+ (self.base_tmp, self.tmp_name, name)))
1780+ self.assertEqual(wd.base, os.path.sep.join((self.base_tmp,
1781+ self.tmp_name)))
1782+ for name in ('fstab', 'config', 'interfaces', 'network_state',
1783+ 'network_config', 'mdadm_config', 'crypttab'):
1784+ self.assertEqual(getattr(wd, name), os.path.sep.join(
1785+ (self.base_tmp, self.tmp_name, 'state', name)))
1786+
1787+ @mock.patch('curtin.workingdir.tempfile')
1788+ @mock.patch('curtin.workingdir.open')
1789+ @mock.patch('curtin.workingdir.util')
1790+ def test_enter_creates_files(self, mock_util, mock_open, mock_tempfile):
1791+ """
1792+ ensure that tmp directories are created and state files are touched
1793+ on __enter__ when using WorkingDir as a context manager
1794+ """
1795+ mock_tempfile.gettempdir.return_value = self.base_tmp
1796+ mock_tempfile._get_candidate_names.return_value = (
1797+ (name for name in (self.tmp_name,)))
1798+ mock_util.json_dumps.side_effect = lambda x: str(x)
1799+ with workingdir.WorkingDir(config=self.mock_cfg) as wd:
1800+ self.assertIsInstance(wd, workingdir.WorkingDir)
1801+ self.assertEqual(wd._config_dict, self.mock_cfg)
1802+ self.assertEqual(wd.base, os.path.sep.join((self.base_tmp,
1803+ self.tmp_name)))
1804+ for (count, dir_name) in enumerate(('target', 'state', 'scratch',
1805+ 'scratch/rules.d')):
1806+ self.assertEqual(mock_util.ensure_dir.call_args_list[count],
1807+ mock.call(os.path.sep.join(
1808+ (self.base_tmp, self.tmp_name, dir_name))))
1809+ for (count, target_file) in enumerate(
1810+ ('fstab', 'config', 'interfaces', 'network_state',
1811+ 'network_config', 'mdadm_config', 'crypttab')):
1812+ self.assertEqual(
1813+ mock_open.call_args_list[count],
1814+ mock.call(os.path.sep.join((self.base_tmp, self.tmp_name,
1815+ 'state', target_file)),
1816+ 'ab'))
1817+ mock_util.json_dumps.assert_called_with(self.mock_cfg)
1818+ mock_util.write_file.assert_called_once_with(
1819+ os.path.sep.join(
1820+ (self.base_tmp, self.tmp_name, 'state', 'config')),
1821+ str(self.mock_cfg), omode='wb')
1822+
1823+ @mock.patch('curtin.workingdir.tempfile')
1824+ @mock.patch('curtin.workingdir.open')
1825+ @mock.patch('curtin.workingdir.util')
1826+ @mock.patch('curtin.workingdir.os')
1827+ @mock.patch('curtin.workingdir.umount_all')
1828+ @mock.patch('curtin.workingdir.shutil')
1829+ def test_exit_removes_unspecified(self, mock_shutil, mock_umount_all,
1830+ mock_os, mock_util, mock_open,
1831+ mock_tempfile):
1832+ """
1833+ test that workingdir exit removes only dirs that were not specified
1834+ """
1835+ mock_tempfile.gettempdir.return_value = self.base_tmp
1836+ mock_tempfile._get_candidate_names.return_value = (
1837+ (name for name in (self.tmp_name,)))
1838+ target_dir = '/tmp/target'
1839+ mock_os.path.exists.return_value = True
1840+ mock_os.path.join.side_effect = os.path.join
1841+ with workingdir.WorkingDir(target=target_dir):
1842+ pass
1843+ for (count, dir_name) in enumerate(('state', 'scratch', '')):
1844+ path = os.path.sep.join((self.base_tmp, self.tmp_name, dir_name))
1845+ path = path.rstrip('/')
1846+ self.assertEqual(mock_os.path.exists.call_args_list[count],
1847+ mock.call(path))
1848+ self.assertEqual(mock_umount_all.call_args_list[count],
1849+ mock.call(path))
1850+ self.assertEqual(mock_shutil.rmtree.call_args_list[count],
1851+ mock.call(path))

Subscribers

People subscribed via source and target branches