Merge lp:~wesley-wiedenmeier/curtin/without-environment into lp:~curtin-dev/curtin/trunk
- without-environment
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
curtin developers | Pending | ||
Review via email:
|
Commit message
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.
- 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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
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)) |
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.