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

Proposed by Wesley Wiedenmeier
Status: Merged
Merged at revision: 343
Proposed branch: lp:~wesley-wiedenmeier/curtin/mkfs
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 588 lines (+363/-131)
5 files modified
curtin/block/mkfs.py (+197/-0)
curtin/commands/block_meta.py (+3/-49)
curtin/commands/mkfs.py (+20/-81)
curtin/util.py (+9/-1)
tests/unittests/test_block_mkfs.py (+134/-0)
To merge this branch: bzr merge lp:~wesley-wiedenmeier/curtin/mkfs
Reviewer Review Type Date Requested Status
Ryan Harper (community) Needs Fixing
Review via email: mp+281529@code.launchpad.net

Description of the change

Move mkfs logic into block.mkfs to allow for more flexible support for different format types and unit tests

To post a comment you must log in.
Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for this cleanup. In general looks good. A few comments below.
I'd also like to see some negative unit tests for each place you raise a ValueError exception, you should provide bogus data and self.assertRaises(ValueError, fn) etc.

Also, we know of at least one edge case (btrfs on precise), we should have a test-case specifically for that one. You'll need to mock out the lsb_release call for that. I used:

@patch('curtin.block.mkfs.util')
mock_util.lsb_release.return_value = {'codename': 'precise'}

review: Needs Fixing
lp:~wesley-wiedenmeier/curtin/mkfs updated
336. By Wesley Wiedenmeier

Comment typo fixes

337. By Wesley Wiedenmeier

Verify that path for curtin.block.mkfs.mkfs is a valid block dev

338. By Wesley Wiedenmeier

General cleanup, and comment addition

339. By Wesley Wiedenmeier

Renamed curtin.block.mkfs.mkfs to curtin.block.mkfs.run_mkfs and changed import
in curtin.commands.mkfs to avoid name conflict between block.mkfs and
commands.mkfs

340. By Wesley Wiedenmeier

Add check for precise + btrfs edge case in mkfs unit tests

341. By Wesley Wiedenmeier

Added negative unit tests for all errors that mkfs can raise

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

This looks really good. Some comments in line.
One request is to make curtin/commands/mkfs be a simple wrapper around this.

So we'd then have:
  curtin mkfs --label=FOO /dev/vdb1 ext4
I think I like that better than :
  curtin mkfs /dev/vdb1 ext4 label=FOO uuid=BAR

but I'm not opposed to arguments for either.

lp:~wesley-wiedenmeier/curtin/mkfs updated
342. By Wesley Wiedenmeier

Rename block.mkfs.run_mkfs to block.mkfs.mkfs, and use import as in
commands.mkfs to avoid conflict

343. By Wesley Wiedenmeier

Switched to long form flags where available

344. By Wesley Wiedenmeier

Improve docstring

345. By Wesley Wiedenmeier

Check that mkfs command exists on system before trying to run it

346. By Wesley Wiedenmeier

Moved negative unit tests for block.mkfs into separate functions for better
readibility/more informative errors

347. By Wesley Wiedenmeier

Do not add quiet flag by default, as command output is captured

348. By Wesley Wiedenmeier

Removed -q from expected flags

349. By Wesley Wiedenmeier

Added strict flag to control mkfs behavior when an invalid format flag is
specified or when label is too long

 - With strict=True, raise error if flag specified that is not supported by
   current fs family, raise error if filesystem label is too long
 - With strict=False, silently ignore flag if not supported by current fs
   family (default behavior of previous code in block_meta) and truncate
   filesystem label if it is too long (many of the mkfs commands do this
   anyway)

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

Looking again at this, it looks like you've addressed most of my concerns.
The remaining things I see are:
1.)
| please improve the docstring here to cover what is in 'flags' at least.
|
| Would it work to give this a signature like:
| run_mkfs(path, fstype, label=None, uuid=None, ...)
|
| then you could call run_mkfs from your 'mkfs_from_config' via:
| flags = {i: info.get(i) for i in info if i in family_flag_mappings)}
| run_mkfs(dev, fstype, **flags)

2.) fix mkfs command to be user usable wrapper around mkfs()
  curtin mkfs --label=FOO /dev/vdb1 ext4
I think I like that better than :
  curtin mkfs /dev/vdb1 ext4 label=FOO uuid=BAR

lp:~wesley-wiedenmeier/curtin/mkfs updated
350. By Wesley Wiedenmeier

Remove config parsing from curtin.commands.mkfs and add flags to pass in label
and uuid

351. By Wesley Wiedenmeier

Added uuid and label kwargs to mkfs.mkfs to simplify usage

352. By Wesley Wiedenmeier

Added force flag as kwarg in block.mkfs

353. By Wesley Wiedenmeier

Removed extra_flags parameter from block.mkfs and rewrote logic to append flags

354. By Wesley Wiedenmeier

Updated docstring for mkfs to remove explaination of extra_flags

355. By Wesley Wiedenmeier

Added strict flag to mkfs command to enforce filesystem label length limits and
flags not supported by some some filesystems

356. By Wesley Wiedenmeier

Move check for btrfs+precise into mkfs

357. By Wesley Wiedenmeier

Sorted command tables in block.mkfs

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

Hey.
One more thing on this. I just gave it a try.
$ rm -f /tmp/my.img; truncate --size 1G /tmp/my.img ; ./bin/curtin mkfs /tmp/my.img
invalid block dev path '/tmp/my.img'

I'd like that fixed, so I changed it to just verify that the path exists.

Then:
$ rm -f /tmp/my.img; truncate --size 1G /tmp/my.img ; ./bin/curtin mkfs /tmp/my.img
<hang>

I hit enter and I see:
Unexpected error while running command.
Command: ['mkfs.ext4', '/tmp/my.img']
Exit code: 1
Reason: -
Stdout: '/tmp/my.img is not a block special device.\nProceed anyway? (y,n) '
Stderr: 'mke2fs 1.42.9 (4-Feb-2014)\n'

It was expecting input from me but was hiding/capturing output.

We need to change the subp implementation to not use 'subprocess.pipe'.

Below is the patch for that. Then we get the same output as above, without the hang.. which is good.

=== modified file 'curtin/block/mkfs.py'
--- curtin/block/mkfs.py 2016-01-12 15:02:45 +0000
+++ curtin/block/mkfs.py 2016-01-15 21:49:20 +0000
@@ -22,6 +22,7 @@
 from curtin import block

 import string
+import os

 mkfs_commands = {
         "btrfs": "mkfs.btrfs",
@@ -119,8 +120,10 @@
        finds old data or filesystems on the partition.
        """

- if path is None or not block.is_valid_device(path):
+ if path is None:
         raise ValueError("invalid block dev path '%s'" % path)
+ if not os.path.exists(path):
+ raise ValueError("'%s': no such file or directory" % path)

     fs_family = specific_to_family.get(fstype, fstype)
     mkfs_cmd = mkfs_commands.get(fstype)

=== modified file 'curtin/util.py'
--- curtin/util.py 2015-12-18 19:47:48 +0000
+++ curtin/util.py 2016-01-15 21:51:07 +0000
@@ -39,6 +39,7 @@
     if rcs is None:
         rcs = [0]

+ devnull_fp = None
     try:
         if not logstring:
             LOG.debug(("Running command %s with allowed return codes %s"
@@ -52,7 +53,10 @@
         if capture:
             stdout = subprocess.PIPE
             stderr = subprocess.PIPE
- if data is not None:
+ if data is None:
+ devnull_fp = open(os.devnull)
+ stdin = devnull_fp
+ else:
             stdin = subprocess.PIPE
         sp = subprocess.Popen(args, stdout=stdout,
                               stderr=stderr, stdin=stdin,
@@ -74,6 +78,10 @@
             err = ldecode(err)
     except OSError as e:
         raise ProcessExecutionError(cmd=args, reason=e)
+ finally:
+ if devnull_fp:
+ devnull_fp.close()
+
     rc = sp.returncode # pylint: disable=E1101
     if rc not in rcs:
         raise ProcessExecutionError(stdout=out, stderr=err,

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

So that fix in pastebin is http://paste.ubuntu.com/14511520/

The result then we can actually:
$ rm -f /tmp/my.img; truncate --size 1G /tmp/my.img ; ./bin/curtin mkfs --force /tmp/my.img
$ blkid /tmp/my.img
/tmp/my.img: UUID="3268e5cc-ac13-4086-8786-e45c0927385f" TYPE="ext4"

lp:~wesley-wiedenmeier/curtin/mkfs updated
358. By Wesley Wiedenmeier

Applied patch from http://paste.ubuntu.com/14511520/

 - will operate on non block devices such as disk images
 - will not hang silently if waiting for confirmation

359. By Wesley Wiedenmeier

Updated unit tests to mock os.path.exists instead of block.is_valid_device

360. By Wesley Wiedenmeier

block.mkfs returns uuid of created filesystem

 - if no uuid is passed into command uuid.uuid1() is used to generate one
 - if the mkfs command for the filesystem type does not support passing in uuid
   then blkid will be used to determine uuid after fs is created
 - if blkid fails no exceptions will be thrown as mkfs command did still
   succeed

361. By Wesley Wiedenmeier

Updated unit tests to verify that mkfs is generating a uuid and to avoid
potential errors from not mocking block

362. By Wesley Wiedenmeier

Print message after creating filesystem in commands.mkfs

363. By Wesley Wiedenmeier

Added block.mkfs.valid_fstypes() to list fstypes we know how to support

364. By Wesley Wiedenmeier

Use 'choices': valid_fstypes() in commands.mkfs arguments so that help message
lists valid fstypes

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'curtin/block/mkfs.py'
2--- curtin/block/mkfs.py 1970-01-01 00:00:00 +0000
3+++ curtin/block/mkfs.py 2016-01-15 23:10:42 +0000
4@@ -0,0 +1,197 @@
5+# Copyright (C) 2016 Canonical Ltd.
6+#
7+# Author: Wesley Wiedenmeier <wesley.wiedenmeier@canonical.com>
8+#
9+# Curtin is free software: you can redistribute it and/or modify it under
10+# the terms of the GNU Affero General Public License as published by the
11+# Free Software Foundation, either version 3 of the License, or (at your
12+# option) any later version.
13+#
14+# Curtin is distributed in the hope that it will be useful, but WITHOUT ANY
15+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
17+# more details.
18+#
19+# You should have received a copy of the GNU Affero General Public License
20+# along with Curtin. If not, see <http://www.gnu.org/licenses/>.
21+
22+# This module wraps calls to mkfs.<fstype> and determines the appropriate flags
23+# for each filesystem type
24+
25+from curtin import util
26+from curtin import block
27+
28+import string
29+import os
30+from uuid import uuid1
31+
32+mkfs_commands = {
33+ "btrfs": "mkfs.btrfs",
34+ "ext2": "mkfs.ext2",
35+ "ext3": "mkfs.ext3",
36+ "ext4": "mkfs.ext4",
37+ "fat": "mkfs.fat",
38+ "fat12": "mkfs.fat",
39+ "fat16": "mkfs.fat",
40+ "fat32": "mkfs.fat",
41+ "jfs": "jfs_mkfs",
42+ "ntfs": "mkntfs",
43+ "reiserfs": "mkfs.reiserfs",
44+ "swap": "mkswap",
45+ "xfs": "mkfs.xfs"
46+ }
47+
48+specific_to_family = {
49+ "ext2": "ext",
50+ "ext3": "ext",
51+ "ext4": "ext",
52+ "fat12": "fat",
53+ "fat16": "fat",
54+ "fat32": "fat",
55+ }
56+
57+label_length_limits = {
58+ "btrfs": 256,
59+ "ext": 16,
60+ "fat": 11,
61+ "jfs": 16, # see jfs_tune manpage
62+ "ntfs": 32,
63+ "reiserfs": 16,
64+ "swap": 15, # not in manpages, found experimentally
65+ "xfs": 12
66+ }
67+
68+family_flag_mappings = {
69+ "label": {"btrfs": "--label",
70+ "ext": "-L",
71+ "fat": "-n",
72+ "jfs": "-L",
73+ "ntfs": "--label",
74+ "reiserfs": "--label",
75+ "swap": "--label",
76+ "xfs": "-L"},
77+ "uuid": {"btrfs": "--uuid",
78+ "ext": "-U",
79+ "reiserfs": "--uuid",
80+ "swap": "--uuid"},
81+ "force": {"btrfs": "--force",
82+ "ext": "-F",
83+ "ntfs": "--force",
84+ "reiserfs": "-f",
85+ "swap": "--force",
86+ "xfs": "-f"},
87+ "fatsize": {"fat": "-F"},
88+ "quiet": {"ext": "-q",
89+ "ntfs": "-q",
90+ "reiserfs": "-q",
91+ "xfs": "--quiet"}
92+ }
93+
94+
95+def valid_fstypes():
96+ return list(mkfs_commands.keys())
97+
98+
99+def get_flag_mapping(flag_name, fs_family, param=None, strict=False):
100+ ret = []
101+ flag_sym_families = family_flag_mappings.get(flag_name)
102+ if flag_sym_families is None:
103+ raise ValueError("unsupported flag '%s'" % flag_name)
104+ flag_sym = flag_sym_families.get(fs_family)
105+ if flag_sym is None:
106+ if strict:
107+ raise ValueError("flag '%s' not supported by fs family '%s'" %
108+ flag_name, fs_family)
109+ else:
110+ ret = [flag_sym]
111+ if param is not None:
112+ ret.append(param)
113+ return ret
114+
115+
116+def mkfs(path, fstype, strict=False, label=None, uuid=None, force=False):
117+ """Make filesystem on block device with given path using given fstype and
118+ appropriate flags for filesystem family.
119+
120+ Filesystem uuid and label can be passed in as kwargs. By default no
121+ label or uuid will be used. If a filesystem label is too long curtin
122+ will raise a ValueError if the strict flag is true or will truncate
123+ it to the maximum possible length.
124+
125+ If a flag is not supported by a filesystem family mkfs will raise a
126+ ValueError if the strict flag is true or silently ignore it otherwise.
127+
128+ Force can be specified to force the mkfs command to continue even if it
129+ finds old data or filesystems on the partition.
130+ """
131+
132+ if path is None:
133+ raise ValueError("invalid block dev path '%s'" % path)
134+ if not os.path.exists(path):
135+ raise ValueError("'%s': no such file or directory" % path)
136+
137+ fs_family = specific_to_family.get(fstype, fstype)
138+ mkfs_cmd = mkfs_commands.get(fstype)
139+ if not mkfs_cmd:
140+ raise ValueError("unsupported fs type '%s'" % fstype)
141+
142+ if util.which(mkfs_cmd) is None:
143+ raise ValueError("need '%s' but it could not be found" % mkfs_cmd)
144+
145+ cmd = [mkfs_cmd]
146+
147+ if force:
148+ # On precise mkfs.btrfs does not have a force flag, though it does in
149+ # later releases
150+ if not (util.lsb_release()['codename'] == "precise" and fstype ==
151+ "btrfs"):
152+ cmd.extend(get_flag_mapping("force", fs_family, strict=strict))
153+ if label is not None:
154+ limit = label_length_limits.get(fs_family)
155+ if len(label) > limit:
156+ if strict:
157+ raise ValueError("length of fs label for '%s' exceeds max \
158+ allowed for fstype '%s'. max is '%s'"
159+ % (path, fstype, limit))
160+ else:
161+ label = label[:limit]
162+ cmd.extend(get_flag_mapping("label", fs_family, param=label,
163+ strict=strict))
164+
165+ # If uuid is not specified, generate one and try to use it
166+ if uuid is None:
167+ uuid = str(uuid1())
168+ cmd.extend(get_flag_mapping("uuid", fs_family, param=uuid, strict=strict))
169+
170+ if fs_family == "fat":
171+ fat_size = fstype.strip(string.ascii_letters)
172+ if fat_size in ["12", "16", "32"]:
173+ cmd.extend(get_flag_mapping("fatsize", fs_family, param=fat_size,
174+ strict=strict))
175+
176+ cmd.append(path)
177+ util.subp(cmd, capture=True)
178+
179+ # if fs_family does not support specifying uuid then use blkid to find it
180+ # if blkid is unable to then just return None for uuid
181+ if fs_family not in family_flag_mappings['uuid']:
182+ try:
183+ uuid = block.blkid()[path]['UUID']
184+ except:
185+ pass
186+
187+ # return uuid, may be none if it could not be specified and blkid could not
188+ # find it
189+ return uuid
190+
191+
192+def mkfs_from_config(path, info, strict=False):
193+ """Make filesystem on block device with given path according to storage
194+ config given"""
195+ fstype = info.get('fstype')
196+ if fstype is None:
197+ raise ValueError("fstype must be specified")
198+ # NOTE: Since old metadata on partitions that have not been wiped can cause
199+ # some mkfs commands to refuse to work, it's best to use force=True
200+ mkfs(path, fstype, strict=strict, force=True, uuid=info.get('uuid'),
201+ label=info.get('label'))
202
203=== modified file 'curtin/commands/block_meta.py'
204--- curtin/commands/block_meta.py 2016-01-07 18:53:54 +0000
205+++ curtin/commands/block_meta.py 2016-01-15 23:10:42 +0000
206@@ -19,6 +19,7 @@
207 from curtin import (block, config, util)
208 from curtin.block import mdadm
209 from curtin.log import LOG
210+from curtin.block import mkfs
211
212 from . import populate_one_subcmd
213 from curtin.udev import compose_udev_equality
214@@ -26,7 +27,6 @@
215 import glob
216 import os
217 import platform
218-import string
219 import sys
220 import tempfile
221 import time
222@@ -635,10 +635,7 @@
223
224
225 def format_handler(info, storage_config):
226- fstype = info.get('fstype')
227 volume = info.get('volume')
228- part_label = info.get('label')
229- uuid = info.get('uuid')
230 if not volume:
231 raise ValueError("volume must be specified for partition '%s'" %
232 info.get('id'))
233@@ -651,51 +648,8 @@
234 # Volume marked to be preserved, not formatting
235 return
236
237- # Generate mkfs command and run
238- if fstype in ["ext4", "ext3"]:
239- cmd = ['mkfs.%s' % fstype, '-F', '-q']
240- if part_label:
241- if len(part_label) > 16:
242- raise ValueError(
243- "ext3/4 partition labels cannot be longer than "
244- "16 characters")
245- else:
246- cmd.extend(["-L", part_label])
247- if uuid:
248- cmd.extend(["-U", uuid])
249- cmd.append(volume_path)
250- elif fstype in ["btrfs"]:
251- cmd = ['mkfs.%s' % fstype]
252- if util.lsb_release()['codename'] != "precise":
253- cmd.extend(['-f'])
254- if part_label:
255- cmd.extend(["-L", part_label])
256- if uuid:
257- cmd.extend(["-U", uuid])
258- cmd.append(volume_path)
259- elif fstype in ["fat12", "fat16", "fat32", "fat"]:
260- cmd = ["mkfs.fat"]
261- fat_size = fstype.strip(string.ascii_letters)
262- if fat_size in ["12", "16", "32"]:
263- cmd.extend(["-F", fat_size])
264- if part_label:
265- if len(part_label) > 11:
266- raise ValueError(
267- "fat partition names cannot be longer than "
268- "11 characters")
269- cmd.extend(["-n", part_label])
270- cmd.append(volume_path)
271- elif fstype == "swap":
272- cmd = ["mkswap", volume_path]
273- else:
274- # See if mkfs.<fstype> exists. If so try to run it.
275- try:
276- util.subp(["which", "mkfs.%s" % fstype])
277- cmd = ["mkfs.%s" % fstype, volume_path]
278- except util.ProcessExecutionError:
279- raise ValueError("fstype '%s' not supported" % fstype)
280- LOG.info("formatting volume '%s' with format '%s'" % (volume_path, fstype))
281- logtime(' '.join(cmd), util.subp, cmd)
282+ # Make filesystem using block library
283+ mkfs.mkfs_from_config(volume_path, info)
284
285
286 def mount_handler(info, storage_config):
287
288=== modified file 'curtin/commands/mkfs.py'
289--- curtin/commands/mkfs.py 2015-11-18 17:15:08 +0000
290+++ curtin/commands/mkfs.py 2016-01-15 23:10:42 +0000
291@@ -15,18 +15,11 @@
292 # You should have received a copy of the GNU Affero General Public License
293 # along with Curtin. If not, see <http://www.gnu.org/licenses/>.
294
295-import curtin.config
296-from curtin.log import LOG
297-from curtin import config
298-from curtin import util
299 from . import populate_one_subcmd
300-from curtin.commands.block_meta import get_path_to_storage_volume
301-
302-from collections import OrderedDict
303-
304-import os
305+from curtin.block.mkfs import mkfs as run_mkfs
306+from curtin.block.mkfs import valid_fstypes
307+
308 import sys
309-import string
310
311 CMD_ARGUMENTS = (
312 (('devices',
313@@ -35,84 +28,30 @@
314 'metavar': 'DEVICE', 'action': 'store', 'nargs': '+'}),
315 (('-f', '--fstype'),
316 {'help': 'filesystem type to use. default is ext4',
317+ 'choices': valid_fstypes(),
318 'default': 'ext4', 'action': 'store'}),
319- (('-c', '--config'),
320- {'help': 'read configuration from cfg', 'action': 'append',
321- 'metavar': 'CONFIG', 'dest': 'cfgopts', 'default': []})
322+ (('-l', '--label'),
323+ {'help': 'label to use for filesystem', 'action': 'store'}),
324+ (('-u', '--uuid'),
325+ {'help': 'uuid to use for filesystem', 'action': 'store'}),
326+ (('-s', '--strict'),
327+ {'help': 'exit if mkfs cannot do exactly what is specified',
328+ 'action': 'store_true', 'default': False}),
329+ (('-F', '--force'),
330+ {'help': 'continue if some data already exists on device',
331+ 'action': 'store_true', 'default': False})
332 )
333 )
334
335
336-def format_blockdev(volume_path, fstype, part_id=None):
337- if not part_id:
338- part_id = volume_path.split("/")[-1]
339-
340- # Generate mkfs command and run
341- if fstype in ["ext4", "ext3"]:
342- if len(part_id) > 16:
343- raise ValueError("ext3/4 partition labels cannot be longer than \
344- 16 characters")
345- cmd = ['mkfs.%s' % fstype, '-F', '-q', '-L', part_id, volume_path]
346- elif fstype in ["fat12", "fat16", "fat32", "fat"]:
347- cmd = ["mkfs.fat"]
348- fat_size = fstype.strip(string.ascii_letters)
349- if fat_size in ["12", "16", "32"]:
350- cmd.extend(["-F", fat_size])
351- if len(part_id) > 11:
352- raise ValueError("fat partition names cannot be longer than \
353- 11 characters")
354- cmd.extend(["-n", part_id, volume_path])
355- else:
356- # See if mkfs.<fstype> exists. If so try to run it.
357- try:
358- util.subp(["which", "mkfs.%s" % fstype])
359- cmd = ["mkfs.%s" % fstype, volume_path]
360- except util.ProcessExecutionError:
361- raise ValueError("fstype '%s' not supported" % fstype)
362- LOG.info("formatting volume '%s' with format '%s'" % (volume_path, fstype))
363- util.subp(cmd)
364-
365-
366-def format_storage_item(info, storage_config):
367- fstype = info.get('fstype')
368- volume = info.get('volume')
369- part_id = info.get('id')
370- if not volume:
371- raise ValueError("volume must be specified for partition '%s'" %
372- info.get('id'))
373-
374- # Get path to volume
375- volume_path = get_path_to_storage_volume(volume, storage_config)
376-
377- # Call format_blockdev
378- format_blockdev(volume_path, fstype, part_id=part_id)
379-
380-
381 def mkfs(args):
382- state = util.load_command_environment()
383- cfg = config.load_command_config(args, state)
384-
385- if args.cfgopts:
386- for filename in args.cfgopts:
387- curtin.config.merge_config_fp(cfg, open(filename))
388-
389- if "storage" in cfg:
390- storage_config = OrderedDict((d["id"], d) for (i, d) in
391- enumerate(cfg.get("storage")))
392- else:
393- storage_config = {}
394-
395 for device in args.devices:
396- if device in storage_config:
397- # Device is in storage config
398- format_storage_item(storage_config.get(device), storage_config)
399- elif "/dev/" in device and os.path.exists(device):
400- # Device is path to block dev
401- format_blockdev(device, args.fstype)
402- else:
403- # Bad argument
404- raise ValueError("device '%s' is neither an item in storage "
405- "config nor a block device" % device)
406+ uuid = run_mkfs(device, args.fstype, strict=args.strict,
407+ uuid=args.uuid, label=args.label,
408+ force=args.force)
409+
410+ print("Created '%s' filesystem in '%s' with uuid '%s' and label '%s'" %
411+ (args.fstype, device, uuid, args.label))
412
413 sys.exit(0)
414
415
416=== modified file 'curtin/util.py'
417--- curtin/util.py 2015-12-18 19:47:48 +0000
418+++ curtin/util.py 2016-01-15 23:10:42 +0000
419@@ -39,6 +39,7 @@
420 if rcs is None:
421 rcs = [0]
422
423+ devnull_fp = None
424 try:
425 if not logstring:
426 LOG.debug(("Running command %s with allowed return codes %s"
427@@ -52,7 +53,10 @@
428 if capture:
429 stdout = subprocess.PIPE
430 stderr = subprocess.PIPE
431- if data is not None:
432+ if data is None:
433+ devnull_fp = open(os.devnull)
434+ stdin = devnull_fp
435+ else:
436 stdin = subprocess.PIPE
437 sp = subprocess.Popen(args, stdout=stdout,
438 stderr=stderr, stdin=stdin,
439@@ -74,6 +78,10 @@
440 err = ldecode(err)
441 except OSError as e:
442 raise ProcessExecutionError(cmd=args, reason=e)
443+ finally:
444+ if devnull_fp:
445+ devnull_fp.close()
446+
447 rc = sp.returncode # pylint: disable=E1101
448 if rc not in rcs:
449 raise ProcessExecutionError(stdout=out, stderr=err,
450
451=== added file 'tests/unittests/test_block_mkfs.py'
452--- tests/unittests/test_block_mkfs.py 1970-01-01 00:00:00 +0000
453+++ tests/unittests/test_block_mkfs.py 2016-01-15 23:10:42 +0000
454@@ -0,0 +1,134 @@
455+from curtin.block import mkfs
456+
457+from unittest import TestCase
458+import mock
459+
460+
461+class TestBlockMkfs(TestCase):
462+ test_uuid = "fb26cc6c-ae73-11e5-9e38-2fb63f0c3155"
463+
464+ def _get_config(self, fstype):
465+ return {"fstype": fstype, "type": "format", "id": "testfmt",
466+ "volume": "null", "label": "format1",
467+ "uuid": self.test_uuid}
468+
469+ def _assert_same_flags(self, call, expected):
470+ for flag in expected:
471+ if type(flag) == list:
472+ flag_name = flag[0]
473+ flag_val = flag[1]
474+ self.assertIn(flag_name, call)
475+ flag_index = call.index(flag_name)
476+ self.assertTrue(len(call) > flag_index)
477+ self.assertEquals(call[flag_index + 1], flag_val)
478+ call.remove(flag_name)
479+ call.remove(flag_val)
480+ else:
481+ self.assertIn(flag, call)
482+ call.remove(flag)
483+ # Only remaining vals in call should be mkfs.fstype and dev path
484+ self.assertEquals(len(call), 2)
485+
486+ @mock.patch("curtin.block.mkfs.block")
487+ @mock.patch("curtin.block.mkfs.os")
488+ @mock.patch("curtin.block.mkfs.util")
489+ def _run_mkfs_with_config(self, config, expected_cmd, expected_flags,
490+ mock_util, mock_os, mock_block,
491+ release="wily", strict=False):
492+ # Pretend we are on wily as there are no known edge cases for it
493+ mock_util.lsb_release.return_value = {"codename": release}
494+ mock_os.path.exists.return_value = True
495+
496+ mkfs.mkfs_from_config("/dev/null", config, strict=strict)
497+ self.assertTrue(mock_util.subp.called)
498+ calls = mock_util.subp.call_args_list
499+ self.assertEquals(len(calls), 1)
500+
501+ # Get first function call, tuple of first positional arg and its
502+ # (nonexistant) keyword arg, and unpack to get cmd
503+ call = calls[0][0][0]
504+ self.assertEquals(call[0], expected_cmd)
505+ self._assert_same_flags(call, expected_flags)
506+
507+ def test_mkfs_ext(self):
508+ conf = self._get_config("ext4")
509+ expected_flags = [["-L", "format1"], "-F",
510+ ["-U", self.test_uuid]]
511+ self._run_mkfs_with_config(conf, "mkfs.ext4", expected_flags)
512+
513+ def test_mkfs_btrfs(self):
514+ conf = self._get_config("btrfs")
515+ expected_flags = [["--label", "format1"], "--force",
516+ ["--uuid", self.test_uuid]]
517+ self._run_mkfs_with_config(conf, "mkfs.btrfs", expected_flags)
518+
519+ # Test precise+btrfs edge case, force should not be used
520+ expected_flags.remove("--force")
521+ self._run_mkfs_with_config(conf, "mkfs.btrfs", expected_flags,
522+ release="precise")
523+
524+ def test_mkfs_fat(self):
525+ conf = self._get_config("fat32")
526+ expected_flags = [["-n", "format1"], ["-F", "32"]]
527+ self._run_mkfs_with_config(conf, "mkfs.fat", expected_flags)
528+
529+ def test_mkfs_invalid_fstype(self):
530+ """Do not proceed if fstype is None or invalid"""
531+ with self.assertRaises(ValueError):
532+ conf = self._get_config(None)
533+ self._run_mkfs_with_config(conf, "mkfs.ext4", [])
534+ with self.assertRaises(ValueError):
535+ conf = self._get_config("fakefilesystemtype")
536+ self._run_mkfs_with_config(conf, "mkfs.ext3", [])
537+
538+ def test_mkfs_invalid_label(self):
539+ """Do not proceed if filesystem label is too long"""
540+ with self.assertRaises(ValueError):
541+ conf = self._get_config("ext4")
542+ conf['label'] = "thislabelislongerthan16chars"
543+ self._run_mkfs_with_config(conf, "mkfs.ext4", [], strict=True)
544+
545+ conf = self._get_config("swap")
546+ expected_flags = ["--force", ["--label", "abcdefghijklmno"],
547+ ["--uuid", conf['uuid']]]
548+ conf['label'] = "abcdefghijklmnop" # 16 chars, 15 is max
549+
550+ # Raise error, do not truncate with strict = True
551+ with self.assertRaises(ValueError):
552+ self._run_mkfs_with_config(conf, "mkswap", expected_flags,
553+ strict=True)
554+
555+ # Do not raise with strict = False
556+ self._run_mkfs_with_config(conf, "mkswap", expected_flags)
557+
558+ @mock.patch("curtin.block.mkfs.block")
559+ @mock.patch("curtin.block.mkfs.util")
560+ @mock.patch("curtin.block.mkfs.os")
561+ def test_mkfs_kwargs(self, mock_os, mock_util, mock_block):
562+ """Ensure that kwargs are being followed"""
563+ mkfs.mkfs("/dev/null", "ext4", [], uuid=self.test_uuid,
564+ label="testlabel", force=True)
565+ expected_flags = ["-F", ["-L", "testlabel"], ["-U", self.test_uuid]]
566+ calls = mock_util.subp.call_args_list
567+ self.assertEquals(len(calls), 1)
568+ call = calls[0][0][0]
569+ self.assertEquals(call[0], "mkfs.ext4")
570+ self._assert_same_flags(call, expected_flags)
571+
572+ @mock.patch("curtin.block.mkfs.os")
573+ def test_mkfs_invalid_block_device(self, mock_os):
574+ """Do not proceed if block device is none or is not valid block dev"""
575+ with self.assertRaises(ValueError):
576+ mock_os.path.exists.return_value = False
577+ mkfs.mkfs("/dev/null", "ext4")
578+ with self.assertRaises(ValueError):
579+ mock_os.path.exists.return_value = True
580+ mkfs.mkfs(None, "ext4")
581+
582+ @mock.patch("curtin.block.mkfs.util")
583+ @mock.patch("curtin.block.mkfs.os")
584+ def test_mkfs_generates_uuid(self, mock_os, mock_util):
585+ """Ensure that block.mkfs generates and returns a uuid if None is
586+ provided"""
587+ uuid = mkfs.mkfs("/dev/null", "ext4")
588+ self.assertIsNotNone(uuid)

Subscribers

People subscribed via source and target branches