Merge lp:~raharper/curtin/trunk.lp1709284-mount-options into lp:~curtin-dev/curtin/trunk

Proposed by Ryan Harper
Status: Merged
Merged at revision: 544
Proposed branch: lp:~raharper/curtin/trunk.lp1709284-mount-options
Merge into: lp:~curtin-dev/curtin/trunk
Diff against target: 349 lines (+191/-26)
6 files modified
curtin/commands/block_meta.py (+50/-23)
doc/topics/storage.rst (+18/-0)
examples/tests/basic.yaml (+1/-0)
examples/tests/basic_scsi.yaml (+1/-0)
tests/unittests/test_commands_block_meta.py (+118/-2)
tests/vmtests/test_basic.py (+3/-1)
To merge this branch: bzr merge lp:~raharper/curtin/trunk.lp1709284-mount-options
Reviewer Review Type Date Requested Status
Scott Moser (community) Approve
Server Team CI bot continuous-integration Approve
Chad Smith Approve
Review via email: mp+328814@code.launchpad.net

Description of the change

storage: add 'options' key to the mount storage type to specify mount parameters in fstab

Add support, documentation, unittests and vmtests to deliver a new parameter to the mount configuration dictionary. This parameter if set will modify the the mount options field in the rendered /etc/fstab.

To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
522. By Ryan Harper

Handle empty mount option string

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

The CI failure looks to be related to infrastructure, not the branch:

+ tox
/tmp/hudson2703813631282842425.sh: line 9: tox: command not found
Build step 'Execute shell' marked build as failure

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Chad Smith (chad.smith) wrote :

looks pretty good, checking a few other things but here are some inline comments.

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

Thanks for the review, will fix and update.

Revision history for this message
Chad Smith (chad.smith) wrote :

additional unittest/doc tweaks inline and we are a +1.

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

Will fix up a few more things suggested.

523. By Ryan Harper

Address MP feedback

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
524. By Ryan Harper

merge from trunk

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

generally fine with this, but i would like some warnings on this section.
its basically impossible to do this without somewhat deep knowledge of your target OS.

Also, I suppose that curtin probably *should* mount the filesystem with the options that are provided when it mounts the filesystem. Other wise, it would mount the filesystem during install with one set of options and then the boot would mount with a different set. That could potentially be hazardous.

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

Note, that if curtin did mount with the options provided, then it would require the ephemeral' environment's kernel to support these options which could fail. (in maas, any custom OS such as centos will be installed with the ephemeral environment of Ubuntu)

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

On Fri, Aug 18, 2017 at 3:21 PM, Scott Moser <email address hidden> wrote:

> generally fine with this, but i would like some warnings on this section.
>

Something generic along mount options might be incompatible between OS
releases
or more specific about certain ones?

> its basically impossible to do this without somewhat deep knowledge of
> your target OS.
>

Indeed.

>
> Also, I suppose that curtin probably *should* mount the filesystem with
> the options that are provided when it mounts the filesystem. Other wise,
> it would mount the filesystem during install with one set of options and
> then the boot would mount with a different set. That could potentially be
> hazardous.
>

That's a really good point.

>
>
> Diff comments:
>
> > === modified file 'curtin/commands/block_meta.py'
> > --- curtin/commands/block_meta.py 2017-05-26 14:07:29 +0000
> > +++ curtin/commands/block_meta.py 2017-08-15 19:45:43 +0000
> > @@ -656,20 +674,18 @@
> >
> > # Add volume to fstab
> > if state['fstab']:
> > - with open(state['fstab'], "a") as fp:
> > - location = get_path_to_storage_volume(volume.get('id'),
> > - storage_config)
> > - uuid = block.get_volume_uuid(volume_path)
> > - if len(uuid) > 0:
> > - location = "UUID=%s" % uuid
> > -
> > - if filesystem.get('fstype') in ["fat", "fat12", "fat16",
> "fat32",
> > - "fat64"]:
> > - fstype = "vfat"
> > - else:
> > - fstype = filesystem.get('fstype')
> > - fp.write("%s %s %s %s 0 0\n" % (location, path, fstype,
> > - ",".join(options)))
> > + uuid = block.get_volume_uuid(volume_path)
> > + location = ("UUID=%s" % uuid) if uuid else (
> > + get_path_to_storage_volume(volume.get('id'),
> > + storage_config))
> > +
> > + fstype = filesystem.get('fstype')
> > + if fstype in ["fat", "fat12", "fat16", "fat32", "fat64"]:
> > + fstype = "vfat"
> > +
> > + fstab_entry = "%s %s %s %s 0 0\n" % (location, path, fstype,
>
> seems better as ' '.join([location, path, fstype, ",".join(options), "0",
> "0")
>
> then you could also '\t'.join()
>
> > + ",".join(options))
> > + util.write_file(state['fstab'], fstab_entry, omode='a')
> > else:
> > LOG.info("fstab not in environment, so not writing")
> >
> >
> > === modified file 'doc/topics/storage.rst'
> > --- doc/topics/storage.rst 2017-04-21 17:36:27 +0000
> > +++ doc/topics/storage.rst 2017-08-15 19:45:43 +0000
> > @@ -358,6 +358,10 @@
> > config. The target device must already contain a valid filesystem and be
> > accessible.
> >
> > +**options**: *<mount(8) comma-separated options string>*
> > +
> > +The ``options`` key will replace the default options value of
> ``defaults``.
> > +
>
> lets mention here that options are often times OS or OS version specific,
> and behavior of non-implemented options is not well defined. ...

Read more...

525. By Ryan Harper

attempt to mount storage with options applied to catch possible failures

526. By Ryan Harper

Add options via extend

527. By Ryan Harper

merge from trunk

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

2 comments inline, then I think I approve.

528. By Ryan Harper

merge from trunk

529. By Ryan Harper

Log mount exception as string, add warning to documentation

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

Ran a vmtest over test_basic which now checks/validates mount options.

Ran 96 tests in 303.208s

OK
Mon, 27 Nov 2017 18:14:53 +0000: vmtest end [0] in 305s

https://jenkins.ubuntu.com/server/view/curtin/job/curtin-vmtest-devel-debug/22/console

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

please fix one typo-ish
 - will have both of the following effects.
 + will have both of the following effects:

other than that approve.

review: Approve
530. By Ryan Harper

doc: replace period with colon because punctuation.

531. By Ryan Harper

merge from trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'curtin/commands/block_meta.py'
--- curtin/commands/block_meta.py 2017-10-27 16:29:07 +0000
+++ curtin/commands/block_meta.py 2017-11-27 19:41:30 +0000
@@ -620,9 +620,27 @@
620620
621621
622def mount_handler(info, storage_config):622def mount_handler(info, storage_config):
623 """ Handle storage config type: mount
624
625 info = {
626 'id': 'rootfs_mount',
627 'type': 'mount',
628 'path': '/',
629 'options': 'defaults,errors=remount-ro',
630 'device': 'rootfs',
631 }
632
633 Mount specified device under target at 'path' and generate
634 fstab entry.
635 """
623 state = util.load_command_environment()636 state = util.load_command_environment()
624 path = info.get('path')637 path = info.get('path')
625 filesystem = storage_config.get(info.get('device'))638 filesystem = storage_config.get(info.get('device'))
639 mount_options = info.get('options')
640 # handle unset, or empty('') strings
641 if not mount_options:
642 mount_options = 'defaults'
643
626 if not path and filesystem.get('fstype') != "swap":644 if not path and filesystem.get('fstype') != "swap":
627 raise ValueError("path to mountpoint must be specified")645 raise ValueError("path to mountpoint must be specified")
628 volume = storage_config.get(filesystem.get('volume'))646 volume = storage_config.get(filesystem.get('volume'))
@@ -638,41 +656,50 @@
638 mount_point = os.path.sep.join([state['target'], path])656 mount_point = os.path.sep.join([state['target'], path])
639 mount_point = os.path.normpath(mount_point)657 mount_point = os.path.normpath(mount_point)
640658
641 # Create mount point if does not exist659 options = mount_options.split(",")
642 util.ensure_dir(mount_point)
643
644 # Mount volume
645 util.subp(['mount', volume_path, mount_point])
646
647 path = "/%s" % path
648
649 options = ["defaults"]
650 # If the volume_path's kname is backed by iSCSI or (in the case of660 # If the volume_path's kname is backed by iSCSI or (in the case of
651 # LVM/DM) if any of its slaves are backed by iSCSI, then we need to661 # LVM/DM) if any of its slaves are backed by iSCSI, then we need to
652 # append _netdev to the fstab line662 # append _netdev to the fstab line
653 if iscsi.volpath_is_iscsi(volume_path):663 if iscsi.volpath_is_iscsi(volume_path):
654 LOG.debug("Marking volume_path:%s as '_netdev'", volume_path)664 LOG.debug("Marking volume_path:%s as '_netdev'", volume_path)
655 options.append("_netdev")665 options.append("_netdev")
666
667 # Create mount point if does not exist
668 util.ensure_dir(mount_point)
669
670 # Mount volume, with options
671 try:
672 opts = ['-o', ','.join(options)]
673 util.subp(['mount', volume_path, mount_point] + opts, capture=True)
674 except util.ProcessExecutionError as e:
675 LOG.exception(e)
676 msg = ('Mount failed: %s @ %s with options %s' % (volume_path,
677 mount_point,
678 ",".join(opts)))
679 LOG.error(msg)
680 raise RuntimeError(msg)
681
682 # set path
683 path = "/%s" % path
684
656 else:685 else:
657 path = "none"686 path = "none"
658 options = ["sw"]687 options = ["sw"]
659688
660 # Add volume to fstab689 # Add volume to fstab
661 if state['fstab']:690 if state['fstab']:
662 with open(state['fstab'], "a") as fp:691 uuid = block.get_volume_uuid(volume_path)
663 location = get_path_to_storage_volume(volume.get('id'),692 location = ("UUID=%s" % uuid) if uuid else (
664 storage_config)693 get_path_to_storage_volume(volume.get('id'),
665 uuid = block.get_volume_uuid(volume_path)694 storage_config))
666 if len(uuid) > 0:695
667 location = "UUID=%s" % uuid696 fstype = filesystem.get('fstype')
668697 if fstype in ["fat", "fat12", "fat16", "fat32", "fat64"]:
669 if filesystem.get('fstype') in ["fat", "fat12", "fat16", "fat32",698 fstype = "vfat"
670 "fat64"]:699
671 fstype = "vfat"700 fstab_entry = "%s %s %s %s 0 0\n" % (location, path, fstype,
672 else:701 ",".join(options))
673 fstype = filesystem.get('fstype')702 util.write_file(state['fstab'], fstab_entry, omode='a')
674 fp.write("%s %s %s %s 0 0\n" % (location, path, fstype,
675 ",".join(options)))
676 else:703 else:
677 LOG.info("fstab not in environment, so not writing")704 LOG.info("fstab not in environment, so not writing")
678705
679706
=== modified file 'doc/topics/storage.rst'
--- doc/topics/storage.rst 2017-09-29 16:38:59 +0000
+++ doc/topics/storage.rst 2017-11-27 19:41:30 +0000
@@ -366,12 +366,30 @@
366 fstab entry will contain ``_netdev`` to indicate networking is366 fstab entry will contain ``_netdev`` to indicate networking is
367 required to mount this filesystem.367 required to mount this filesystem.
368368
369**options**: *<mount(8) comma-separated options string>*
370
371The ``options`` key will replace the default options value of ``defaults``.
372
373.. warning::
374 The kernel and user-space utilities may differ between the install
375 environment and the runtime environment. Not all kernels and user-space
376 combinations will support all options. Providing options for a mount point
377 will have both of the following effects:
378
379 - ``curtin`` will mount the filesystems with the provided options during the installation.
380
381 - ``curtin`` will ensure the target OS uses the provided mount options by updating the target OS (/etc/fstab).
382
383 If either of the environments (install or target) do not have support for
384 the provided options, the behavior is undefined.
385
369**Config Example**::386**Config Example**::
370387
371 - id: disk0-part1-fs1-mount0388 - id: disk0-part1-fs1-mount0
372 type: mount389 type: mount
373 path: /home390 path: /home
374 device: disk0-part1-fs1391 device: disk0-part1-fs1
392 options: 'noatime,errors=remount-ro'
375393
376Lvm Volgroup Command394Lvm Volgroup Command
377~~~~~~~~~~~~~~~~~~~~395~~~~~~~~~~~~~~~~~~~~
378396
=== modified file 'examples/tests/basic.yaml'
--- examples/tests/basic.yaml 2017-10-11 14:47:43 +0000
+++ examples/tests/basic.yaml 2017-11-27 19:41:30 +0000
@@ -59,6 +59,7 @@
59 - id: btrfs_disk_mnt_id59 - id: btrfs_disk_mnt_id
60 type: mount60 type: mount
61 path: /btrfs61 path: /btrfs
62 options: 'defaults,noatime'
62 device: btrfs_disk_fmt_id63 device: btrfs_disk_fmt_id
63 - id: pnum_disk64 - id: pnum_disk
64 type: disk65 type: disk
6566
=== modified file 'examples/tests/basic_scsi.yaml'
--- examples/tests/basic_scsi.yaml 2016-07-30 22:42:26 +0000
+++ examples/tests/basic_scsi.yaml 2017-11-27 19:41:30 +0000
@@ -53,6 +53,7 @@
53 - id: btrfs_disk_mnt_id53 - id: btrfs_disk_mnt_id
54 type: mount54 type: mount
55 path: /btrfs55 path: /btrfs
56 options: 'defaults,noatime'
56 device: btrfs_disk_fmt_id57 device: btrfs_disk_fmt_id
57 - id: pnum_disk58 - id: pnum_disk
58 type: disk59 type: disk
5960
=== modified file 'tests/unittests/test_commands_block_meta.py'
--- tests/unittests/test_commands_block_meta.py 2017-10-27 16:29:07 +0000
+++ tests/unittests/test_commands_block_meta.py 2017-11-27 19:41:30 +0000
@@ -115,7 +115,10 @@
115 basepath = 'curtin.commands.block_meta.'115 basepath = 'curtin.commands.block_meta.'
116 self.add_patch(basepath + 'get_path_to_storage_volume', 'mock_getpath')116 self.add_patch(basepath + 'get_path_to_storage_volume', 'mock_getpath')
117 self.add_patch(basepath + 'make_dname', 'mock_make_dname')117 self.add_patch(basepath + 'make_dname', 'mock_make_dname')
118 self.add_patch('curtin.util.load_command_environment',
119 'mock_load_env')
118 self.add_patch('curtin.util.subp', 'mock_subp')120 self.add_patch('curtin.util.subp', 'mock_subp')
121 self.add_patch('curtin.util.ensure_dir', 'mock_ensure_dir')
119 self.add_patch('curtin.block.get_part_table_type',122 self.add_patch('curtin.block.get_part_table_type',
120 'mock_block_get_part_table_type')123 'mock_block_get_part_table_type')
121 self.add_patch('curtin.block.wipe_volume',124 self.add_patch('curtin.block.wipe_volume',
@@ -130,6 +133,10 @@
130 'mock_clear_holders')133 'mock_clear_holders')
131 self.add_patch('curtin.block.clear_holders.assert_clear',134 self.add_patch('curtin.block.clear_holders.assert_clear',
132 'mock_assert_clear')135 'mock_assert_clear')
136 self.add_patch('curtin.block.iscsi.volpath_is_iscsi',
137 'mock_volpath_is_iscsi')
138 self.add_patch('curtin.block.get_volume_uuid',
139 'mock_block_get_volume_uuid')
133 self.add_patch('curtin.block.zero_file_at_offsets',140 self.add_patch('curtin.block.zero_file_at_offsets',
134 'mock_block_zero_file')141 'mock_block_zero_file')
135142
@@ -154,7 +161,20 @@
154 'size': '511705088B',161 'size': '511705088B',
155 'type': 'partition',162 'type': 'partition',
156 'uuid': 'fc7ab24c-b6bf-460f-8446-d3ac362c0625',163 'uuid': 'fc7ab24c-b6bf-460f-8446-d3ac362c0625',
157 'wipe': 'superblock'}164 'wipe': 'superblock'},
165 {'id': 'sda1-root',
166 'type': 'format',
167 'fstype': 'xfs',
168 'volume': 'sda-part1'},
169 {'id': 'sda-part1-mnt-root',
170 'type': 'mount',
171 'path': '/',
172 'device': 'sda1-root'},
173 {'id': 'sda-part1-mnt-root-ro',
174 'type': 'mount',
175 'path': '/readonly',
176 'options': 'ro',
177 'device': 'sda1-root'}
158 ],178 ],
159 }179 }
160 }180 }
@@ -195,10 +215,106 @@
195 self.mock_block_sys_block_path.return_value = '/sys/class/block/xxx'215 self.mock_block_sys_block_path.return_value = '/sys/class/block/xxx'
196216
197 block_meta.partition_handler(part_info, self.storage_config)217 block_meta.partition_handler(part_info, self.storage_config)
198
199 part_offset = 2048 * 512218 part_offset = 2048 * 512
200 self.mock_block_zero_file.assert_called_with(disk_kname, [part_offset],219 self.mock_block_zero_file.assert_called_with(disk_kname, [part_offset],
201 exclusive=False)220 exclusive=False)
202 self.mock_subp.assert_called_with(['parted', disk_kname, '--script',221 self.mock_subp.assert_called_with(['parted', disk_kname, '--script',
203 'mkpart', 'primary', '2048s',222 'mkpart', 'primary', '2048s',
204 '1001471s'], capture=True)223 '1001471s'], capture=True)
224
225 @patch('curtin.util.write_file')
226 def test_mount_handler_defaults(self, mock_write_file):
227 """Test mount_handler has defaults to 'defaults' for mount options"""
228 fstab = self.tmp_path('fstab')
229 self.mock_load_env.return_value = {'fstab': fstab,
230 'target': self.target}
231 disk_info = self.storage_config.get('sda')
232 fs_info = self.storage_config.get('sda1-root')
233 mount_info = self.storage_config.get('sda-part1-mnt-root')
234
235 self.mock_getpath.return_value = '/wark/xxx'
236 self.mock_volpath_is_iscsi.return_value = False
237 self.mock_block_get_volume_uuid.return_value = None
238
239 block_meta.mount_handler(mount_info, self.storage_config)
240 options = 'defaults'
241 expected = "%s %s %s %s 0 0\n" % (disk_info['path'],
242 mount_info['path'],
243 fs_info['fstype'], options)
244
245 mock_write_file.assert_called_with(fstab, expected, omode='a')
246
247 @patch('curtin.util.write_file')
248 def test_mount_handler_uses_mount_options(self, mock_write_file):
249 """Test mount_handler 'options' string is present in fstab entry"""
250 fstab = self.tmp_path('fstab')
251 self.mock_load_env.return_value = {'fstab': fstab,
252 'target': self.target}
253 disk_info = self.storage_config.get('sda')
254 fs_info = self.storage_config.get('sda1-root')
255 mount_info = self.storage_config.get('sda-part1-mnt-root-ro')
256
257 self.mock_getpath.return_value = '/wark/xxx'
258 self.mock_volpath_is_iscsi.return_value = False
259 self.mock_block_get_volume_uuid.return_value = None
260
261 block_meta.mount_handler(mount_info, self.storage_config)
262 options = 'ro'
263 expected = "%s %s %s %s 0 0\n" % (disk_info['path'],
264 mount_info['path'],
265 fs_info['fstype'], options)
266
267 mock_write_file.assert_called_with(fstab, expected, omode='a')
268
269 @patch('curtin.util.write_file')
270 def test_mount_handler_empty_options_string(self, mock_write_file):
271 """Test mount_handler with empty 'options' string, selects defaults"""
272 fstab = self.tmp_path('fstab')
273 self.mock_load_env.return_value = {'fstab': fstab,
274 'target': self.target}
275 disk_info = self.storage_config.get('sda')
276 fs_info = self.storage_config.get('sda1-root')
277 mount_info = self.storage_config.get('sda-part1-mnt-root-ro')
278 mount_info['options'] = ''
279
280 self.mock_getpath.return_value = '/wark/xxx'
281 self.mock_volpath_is_iscsi.return_value = False
282 self.mock_block_get_volume_uuid.return_value = None
283
284 block_meta.mount_handler(mount_info, self.storage_config)
285 options = 'defaults'
286 expected = "%s %s %s %s 0 0\n" % (disk_info['path'],
287 mount_info['path'],
288 fs_info['fstype'], options)
289
290 mock_write_file.assert_called_with(fstab, expected, omode='a')
291
292 def test_mount_handler_appends_to_fstab(self):
293 """Test mount_handler appends fstab lines to existing file"""
294 fstab = self.tmp_path('fstab')
295 with open(fstab, 'w') as fh:
296 fh.write("#curtin-test\n")
297
298 self.mock_load_env.return_value = {'fstab': fstab,
299 'target': self.target}
300 disk_info = self.storage_config.get('sda')
301 fs_info = self.storage_config.get('sda1-root')
302 mount_info = self.storage_config.get('sda-part1-mnt-root-ro')
303 mount_info['options'] = ''
304
305 self.mock_getpath.return_value = '/wark/xxx'
306 self.mock_volpath_is_iscsi.return_value = False
307 self.mock_block_get_volume_uuid.return_value = None
308
309 block_meta.mount_handler(mount_info, self.storage_config)
310 options = 'defaults'
311 expected = "#curtin-test\n%s %s %s %s 0 0\n" % (disk_info['path'],
312 mount_info['path'],
313 fs_info['fstype'],
314 options)
315
316 with open(fstab, 'r') as fh:
317 rendered_fstab = fh.read()
318
319 print(rendered_fstab)
320 self.assertEqual(rendered_fstab, expected)
205321
=== modified file 'tests/vmtests/test_basic.py'
--- tests/vmtests/test_basic.py 2017-11-03 21:27:17 +0000
+++ tests/vmtests/test_basic.py 2017-11-27 19:41:30 +0000
@@ -104,6 +104,7 @@
104 break104 break
105 self.assertIsNotNone(fstab_entry)105 self.assertIsNotNone(fstab_entry)
106 self.assertEqual(fstab_entry.split(' ')[1], "/btrfs")106 self.assertEqual(fstab_entry.split(' ')[1], "/btrfs")
107 self.assertEqual(fstab_entry.split(' ')[3], "defaults,noatime")
107108
108 def test_whole_disk_format(self):109 def test_whole_disk_format(self):
109 # confirm the whole disk format is the expected device110 # confirm the whole disk format is the expected device
@@ -237,7 +238,7 @@
237 self.assertIsNotNone(fstab_entry)238 self.assertIsNotNone(fstab_entry)
238 self.assertEqual(fstab_entry.split(' ')[1], "/home")239 self.assertEqual(fstab_entry.split(' ')[1], "/home")
239240
240 # Test whole disk sdc is mounted at /btrfs241 # Test whole disk sdc is mounted at /btrfs, and uses defaults,noatime
241 uuid = self._kname_to_uuid('sdc')242 uuid = self._kname_to_uuid('sdc')
242 fstab_entry = None243 fstab_entry = None
243 for line in fstab_lines:244 for line in fstab_lines:
@@ -246,6 +247,7 @@
246 break247 break
247 self.assertIsNotNone(fstab_entry)248 self.assertIsNotNone(fstab_entry)
248 self.assertEqual(fstab_entry.split(' ')[1], "/btrfs")249 self.assertEqual(fstab_entry.split(' ')[1], "/btrfs")
250 self.assertEqual(fstab_entry.split(' ')[3], "defaults,noatime")
249251
250 def test_whole_disk_format(self):252 def test_whole_disk_format(self):
251 # confirm the whole disk format is the expected device253 # confirm the whole disk format is the expected device

Subscribers

People subscribed via source and target branches