Merge ~smoser/curtin:bug/1767979-support-tmpfs-mounts into curtin:master

Proposed by Scott Moser
Status: Merged
Approved by: Ryan Harper
Approved revision: 3689a17896504548ce350b3914e433783a178c37
Merge reported by: Ryan Harper
Merged at revision: 9e6e66af224132d8af524f3f408f73d769adf3e4
Proposed branch: ~smoser/curtin:bug/1767979-support-tmpfs-mounts
Merge into: curtin:master
Diff against target: 841 lines (+629/-76)
6 files modified
curtin/commands/block_meta.py (+145/-69)
doc/topics/storage.rst (+53/-3)
examples/tests/filesystem_battery.yaml (+23/-0)
tests/unittests/helpers.py (+3/-1)
tests/unittests/test_commands_block_meta.py (+356/-3)
tests/vmtests/test_fs_battery.py (+49/-0)
Reviewer Review Type Date Requested Status
Ryan Harper (community) Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+345107@code.launchpad.net

Commit message

Support mount entries not tied to a device, including bind and tmpfs.

Previously an entry in the storage configuration of type 'mount'
was required to have a 'device' entry that was a reference to
an id of type 'format'. That meant that there was no way to add
a mount for anything that wasn't a traditional filesystem.

The change here allows a type 'mount' entry to not have a 'device'
as long as it has a 'spec' and a 'fstype' entry. The word 'spec'
comes from fstab(5).

It also adds support for the mount type to have a 'passno' and 'freq'
which are entries in a fstab file.

LP: #1767979

Description of the change

see commit message

To post a comment you must log in.
Revision history for this message
Scott Moser (smoser) wrote :

I still have doc to add for sure. And i'll probably add vmtests for a few different types (bind, tmpfs)

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

mount with -t. ramfs and tmpfs will need -t to be understood.

others (bind) specifically need *no* -t. fun.

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

Create source dir in bind mounts.

b14e065... by Scott Moser

add some mounts (ramfs, tmpfs, bind) to fsbattery.

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

add some vmtests to fsbattery

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

add documentation

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

fix fat32 and add unit test

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

swap entries do not have to have a path.

Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Alberto Donato (ack) wrote :

Tested this branch together with MAAS from https://code.launchpad.net/~ack/maas/+git/maas/+merge/345233 and this branch.
Adding special mounts (tmpfs and ramfs) works as expected when deploying nodes.

Revision history for this message
Ryan Harper (raharper) :
5c2fea4... by Scott Moser

address review feedback from Ryan

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

Looks great. Ill approve with

a) nits on test docstrings
b) passing vmtest run (previous debug run failed in TestNvme)

ERROR: test suite for <class 'vmtests.test_nvme.XenialGATestNvmeBcache'>

with an untranslated fat32 -> vfat

[ 62.788688] cloud-init[1244]: Unexpected error while running command.
[ 62.789453] cloud-init[1244]: Command: ['mount', '-t', 'fat32', '-o', 'defaults', '/dev/vda1', '/tmp/tmpnbodfn6u/target/boot/efi']
[ 62.792585] cloud-init[1244]: Stderr: mount: unknown filesystem type 'fat32'

199f25c... by Scott Moser

Add docstrings to all the test cases.

8fb88ed... by Scott Moser

when calling mount_data from tests, do so possibly more obviously.

instead of
 block_meta.mount_data(mnt, scfg))
use
 block_meta.mount_data(scfg['m1'], scfg))

as 'mnt' was part of scfg which is the OrderedDict of mounts.

3689a17... by Scott Moser

get test coverage to 100% of new lines.

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

ryan's inline comments are addressed.
I also added some assertRaises tests and got coverage of the new lines to 100%.

I'm running on diglett from 3689a17896504548ce350b3914e433783a178c37 right now.

I'll post a log here.

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 :

I successfully ran on diglett.

$ git rev-parse HEAD
3689a17896504548ce350b3914e433783a178c37
$ git branch | grep '^*'
* (HEAD detached at smoser/bug/1767979-support-tmpfs-mounts)

$ ( ./tools/jenkins-runner -p4 2>&1 ; echo $?; ) | tee out.log
CURTIN_VMTEST_IMAGE_SYNC=0
CURTIN_VMTEST_ISCSI_PORTAL=10.245.168.20:25468
CURTIN_VMTEST_KEEP_DATA_FAIL=logs,collect
CURTIN_VMTEST_KEEP_DATA_PASS=logs,collect
CURTIN_VMTEST_LOG=/srv/smoser/curtin/curtin/output/debug.log
CURTIN_VMTEST_PARALLEL=4
CURTIN_VMTEST_REUSE_TOPDIR=0
CURTIN_VMTEST_TAR_DISKS=0
CURTIN_VMTEST_TOPDIR=/srv/smoser/curtin/curtin/output
TGT_IPC_SOCKET=/srv/smoser/curtin/curtin/output/tgt.d/socket
TGT_LOG_D=/srv/smoser/curtin/curtin/output/tgt.d
TGT_PID=14464
TGT_PORTAL=10.245.168.20:25468
http_proxy=
https_proxy=
no_proxy=
Quering synced ephemeral images/kernels in /srv/images
======================================================================================
 Release Codename ImageDate Arch/SubArch Path
--------------------------------------------------------------------------------------
   12.04 precise 20170424 amd64/hwe-t precise/amd64/20170424/root-image.gz
   12.04 precise 20170424.1 amd64/hwe-p precise/amd64/20170424/squashfs
   14.04 trusty 20180502 amd64/hwe-t trusty/amd64/20180502/squashfs
   14.04 trusty 20180502 amd64/hwe-x trusty/amd64/20180502/squashfs
   14.04 trusty 20180502 i386/hwe-t trusty/i386/20180502/squashfs
   14.04 trusty 20180502 i386/hwe-x trusty/i386/20180502/squashfs
   16.04 xenial 20180511 amd64/ga-16.04 xenial/amd64/20180511/squashfs
   16.04 xenial 20180511 amd64/hwe-16.04 xenial/amd64/20180511/squashfs
   16.04 xenial 20180511 amd64/hwe-16.04-edge xenial/amd64/20180511/squashfs
   16.04 xenial 20180511 i386/ga-16.04 xenial/i386/20180511/squashfs
   16.04 xenial 20180511 i386/hwe-16.04 xenial/i386/20180511/squashfs
   16.04 xenial 20180511 i386/hwe-16.04-edge xenial/i386/20180511/squashfs
   17.04 zesty 20171219 amd64/ga-17.04 zesty/amd64/20171219/squashfs
   17.10 artful 20180516 amd64/ga-17.10 artful/amd64/20180516/squashfs
   18.04 bionic 20180509 amd64/ga-18.04 bionic/amd64/20180509/squashfs
   18.04 bionic 20180509 i386/ga-18.04 bionic/i386/20180509/squashfs
======================================================================================

Thu, 17 May 2018 12:33:54 +0000: vmtest start: nosetests3 --process-timeout=86400 --processes=4 -vv --nologcapture tests/vmtests/

...

Ran 3089 tests in 13381.016s

OK (SKIP=229)
Thu, 17 May 2018 17:44:56 +0000: vmtest end [0] in 13382s
0

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

Thanks Scott!

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

An upstream commit landed for this bug.

To view that commit see the following URL:
https://git.launchpad.net/curtin/commit/?id=9e6e66af

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/curtin/commands/block_meta.py b/curtin/commands/block_meta.py
2index c77f787..ff2ab80 100644
3--- a/curtin/commands/block_meta.py
4+++ b/curtin/commands/block_meta.py
5@@ -1,6 +1,6 @@
6 # This file is part of curtin. See LICENSE file for copyright and license info.
7
8-from collections import OrderedDict
9+from collections import OrderedDict, namedtuple
10 from curtin import (block, config, util)
11 from curtin.block import (bcache, mdadm, mkfs, clear_holders, lvm, iscsi, zfs)
12 from curtin.log import LOG
13@@ -17,6 +17,12 @@ import sys
14 import tempfile
15 import time
16
17+FstabData = namedtuple(
18+ "FstabData", ('spec', 'path', 'fstype', 'options', 'freq', 'passno',
19+ 'device'))
20+FstabData.__new__.__defaults__ = (None, None, None, "", "0", "0", None)
21+
22+
23 SIMPLE = 'simple'
24 SIMPLE_BOOT = 'simple-boot'
25 CUSTOM = 'custom'
26@@ -628,6 +634,142 @@ def format_handler(info, storage_config):
27 udevadm_trigger([volume_path])
28
29
30+def mount_data(info, storage_config):
31+ """Return information necessary for a mount or fstab entry.
32+
33+ :param info: a 'mount' type from storage config.
34+ :param storage_config: related storage_config ordered dict by id.
35+
36+ :return FstabData type."""
37+ if info.get('type') != "mount":
38+ raise ValueError("entry is not type 'mount' (%s)" % info)
39+
40+ spec = info.get('spec')
41+ fstype = info.get('fstype')
42+ path = info.get('path')
43+ freq = str(info.get('freq', 0))
44+ passno = str(info.get('passno', 0))
45+
46+ # turn empty options into "defaults", which works in fstab and mount -o.
47+ if not info.get('options'):
48+ options = ["defaults"]
49+ else:
50+ options = info.get('options').split(",")
51+
52+ volume_path = None
53+
54+ if 'device' not in info:
55+ missing = [m for m in ('spec', 'fstype') if not info.get(m)]
56+ if not (fstype and spec):
57+ raise ValueError(
58+ "mount entry without 'device' missing: %s. (%s)" %
59+ (missing, info))
60+
61+ else:
62+ if info['device'] not in storage_config:
63+ raise ValueError(
64+ "mount entry refers to non-existant device %s: (%s)" %
65+ (info['device'], info))
66+ if not (fstype and spec):
67+ format_info = storage_config.get(info['device'])
68+ if not fstype:
69+ fstype = format_info['fstype']
70+ if not spec:
71+ if format_info.get('volume') not in storage_config:
72+ raise ValueError(
73+ "format type refers to non-existant id %s: (%s)" %
74+ (format_info.get('volume'), format_info))
75+ volume_path = get_path_to_storage_volume(
76+ format_info['volume'], storage_config)
77+ if "_netdev" not in options:
78+ if iscsi.volpath_is_iscsi(volume_path):
79+ options.append("_netdev")
80+
81+ if fstype in ("fat", "fat12", "fat16", "fat32", "fat64"):
82+ fstype = "vfat"
83+
84+ return FstabData(
85+ spec, path, fstype, ",".join(options), freq, passno, volume_path)
86+
87+
88+def fstab_line_for_data(fdata):
89+ """Return a string representing fdata in /etc/fstab format.
90+
91+ :param fdata: a FstabData type
92+ :return a newline terminated string for /etc/fstab."""
93+ path = fdata.path
94+ if not path:
95+ if fdata.fstype == "swap":
96+ path = "none"
97+ else:
98+ raise ValueError("empty path in %s." % str(fdata))
99+
100+ if fdata.spec is None:
101+ if not fdata.device:
102+ raise ValueError("FstabData missing both spec and device.")
103+ uuid = block.get_volume_uuid(fdata.device)
104+ spec = ("UUID=%s" % uuid) if uuid else fdata.device
105+ else:
106+ spec = fdata.spec
107+
108+ if fdata.options in (None, "", "defaults"):
109+ if fdata.fstype == "swap":
110+ options = "sw"
111+ else:
112+ options = "defaults"
113+ else:
114+ options = fdata.options
115+
116+ return ' '.join((spec, path, fdata.fstype, options,
117+ fdata.freq, fdata.passno)) + "\n"
118+
119+
120+def mount_fstab_data(fdata, target=None):
121+ """mount the FstabData fdata with root at target.
122+
123+ :param fdata: a FstabData type
124+ :return None."""
125+ mp = util.target_path(target, fdata.path)
126+ if fdata.device:
127+ device = fdata.device
128+ else:
129+ if fdata.spec.startswith("/") and not fdata.spec.startswith("/dev/"):
130+ device = util.target_path(target, fdata.spec)
131+ else:
132+ device = fdata.spec
133+
134+ options = fdata.options if fdata.options else "defaults"
135+
136+ mcmd = ['mount']
137+ if fdata.fstype not in ("bind", None, "none"):
138+ mcmd.extend(['-t', fdata.fstype])
139+ mcmd.extend(['-o', options, device, mp])
140+
141+ if fdata.fstype == "bind" or "bind" in options.split(","):
142+ # for bind mounts, create the 'src' dir (mount -o bind src target)
143+ util.ensure_dir(device)
144+ util.ensure_dir(mp)
145+
146+ try:
147+ util.subp(mcmd, capture=True)
148+ except util.ProcessExecutionError as e:
149+ LOG.exception(e)
150+ msg = 'Mount failed: %s @ %s with options %s' % (device, mp, options)
151+ LOG.error(msg)
152+ raise RuntimeError(msg)
153+
154+
155+def mount_apply(fdata, target=None, fstab=None):
156+ if fdata.fstype != "swap":
157+ mount_fstab_data(fdata, target=target)
158+
159+ # Add volume to fstab
160+ if fstab:
161+ util.write_file(fstab, fstab_line_for_data(fdata), omode="a")
162+ else:
163+ LOG.info("fstab not in environment, so not writing")
164+
165+
166 def mount_handler(info, storage_config):
167 """ Handle storage config type: mount
168
169@@ -643,74 +785,8 @@ def mount_handler(info, storage_config):
170 fstab entry.
171 """
172 state = util.load_command_environment()
173- path = info.get('path')
174- filesystem = storage_config.get(info.get('device'))
175- mount_options = info.get('options')
176- # handle unset, or empty('') strings
177- if not mount_options:
178- mount_options = 'defaults'
179-
180- if not path and filesystem.get('fstype') != "swap":
181- raise ValueError("path to mountpoint must be specified")
182- volume = storage_config.get(filesystem.get('volume'))
183-
184- # Get path to volume
185- volume_path = get_path_to_storage_volume(filesystem.get('volume'),
186- storage_config)
187-
188- if filesystem.get('fstype') != "swap":
189- # Figure out what point should be
190- while len(path) > 0 and path[0] == "/":
191- path = path[1:]
192- mount_point = os.path.sep.join([state['target'], path])
193- mount_point = os.path.normpath(mount_point)
194-
195- options = mount_options.split(",")
196- # If the volume_path's kname is backed by iSCSI or (in the case of
197- # LVM/DM) if any of its slaves are backed by iSCSI, then we need to
198- # append _netdev to the fstab line
199- if iscsi.volpath_is_iscsi(volume_path):
200- LOG.debug("Marking volume_path:%s as '_netdev'", volume_path)
201- options.append("_netdev")
202-
203- # Create mount point if does not exist
204- util.ensure_dir(mount_point)
205-
206- # Mount volume, with options
207- try:
208- opts = ['-o', ','.join(options)]
209- util.subp(['mount', volume_path, mount_point] + opts, capture=True)
210- except util.ProcessExecutionError as e:
211- LOG.exception(e)
212- msg = ('Mount failed: %s @ %s with options %s' % (volume_path,
213- mount_point,
214- ",".join(opts)))
215- LOG.error(msg)
216- raise RuntimeError(msg)
217-
218- # set path
219- path = "/%s" % path
220-
221- else:
222- path = "none"
223- options = ["sw"]
224-
225- # Add volume to fstab
226- if state['fstab']:
227- uuid = block.get_volume_uuid(volume_path)
228- location = ("UUID=%s" % uuid) if uuid else (
229- get_path_to_storage_volume(volume.get('id'),
230- storage_config))
231-
232- fstype = filesystem.get('fstype')
233- if fstype in ["fat", "fat12", "fat16", "fat32", "fat64"]:
234- fstype = "vfat"
235-
236- fstab_entry = "%s %s %s %s 0 0\n" % (location, path, fstype,
237- ",".join(options))
238- util.write_file(state['fstab'], fstab_entry, omode='a')
239- else:
240- LOG.info("fstab not in environment, so not writing")
241+ mount_apply(mount_data(info, storage_config),
242+ target=state.get('target'), fstab=state.get('fstab'))
243
244
245 def lvm_volgroup_handler(info, storage_config):
246diff --git a/doc/topics/storage.rst b/doc/topics/storage.rst
247index 04823b9..ca6253c 100644
248--- a/doc/topics/storage.rst
249+++ b/doc/topics/storage.rst
250@@ -277,6 +277,8 @@ exists and will not modify the partition.
251 device: disk0
252 flag: boot
253
254+.. _format:
255+
256 Format Command
257 ~~~~~~~~~~~~~~
258 The format command makes filesystems on a volume. The filesystem type and
259@@ -369,9 +371,8 @@ in ``/dev``.
260
261 **device**: *<device id>*
262
263-The ``device`` key refers to the ``id`` of the target device in the storage
264-config. The target device must already contain a valid filesystem and be
265-accessible.
266+The ``device`` key refers to the ``id`` of a :ref:`Format <format>` entry.
267+One of ``device`` or ``spec`` must be present.
268
269 .. note::
270
271@@ -379,6 +380,12 @@ accessible.
272 fstab entry will contain ``_netdev`` to indicate networking is
273 required to mount this filesystem.
274
275+**fstype**: *<fileystem type>*
276+
277+``fstype`` is only required if ``device`` is not present. It indicates
278+the filesystem type and will be used for mount operations and written
279+to ``/etc/fstab``
280+
281 **options**: *<mount(8) comma-separated options string>*
282
283 The ``options`` key will replace the default options value of ``defaults``.
284@@ -396,6 +403,14 @@ The ``options`` key will replace the default options value of ``defaults``.
285 If either of the environments (install or target) do not have support for
286 the provided options, the behavior is undefined.
287
288+**spec**: *<fs_spec>*
289+
290+The ``spec`` attribute defines the fsspec as defined in fstab(5).
291+If ``spec`` is present with ``device``, then mounts will be done
292+according to ``spec`` rather than determined via inspection of ``device``.
293+If ``spec`` is present without ``device`` then ``fstype`` must be present.
294+
295+
296 **Config Example**::
297
298 - id: disk0-part1-fs1-mount0
299@@ -404,6 +419,41 @@ The ``options`` key will replace the default options value of ``defaults``.
300 device: disk0-part1-fs1
301 options: 'noatime,errors=remount-ro'
302
303+**Bind Mount**
304+
305+Below is an example of configuring a bind mount.
306+
307+.. code-block:: yaml
308+
309+ - id: bind1
310+ fstype: "none"
311+ options: "bind"
312+ path: "/var/lib"
313+ spec: "/my/bind-over-var-lib"
314+ type: mount
315+
316+That would result in a fstab entry like::
317+
318+ /my/bind-over-var-lib /var/lib none bind 0 0
319+
320+**Tmpfs Mount**
321+
322+Below is an example of configuring a tmpfsbind mount.
323+
324+.. code-block:: yaml
325+
326+ - id: tmpfs1
327+ type: mount
328+ spec: "none"
329+ path: "/my/tmpfs"
330+ options: size=4194304
331+ fstype: "tmpfs"
332+
333+That would result in a fstab entry like::
334+
335+ none /my/tmpfs tmpfs size=4194304 0 0
336+
337+
338 Lvm Volgroup Command
339 ~~~~~~~~~~~~~~~~~~~~
340 The lvm_volgroup command creates LVM Physical Volumes (PV) and connects them in
341diff --git a/examples/tests/filesystem_battery.yaml b/examples/tests/filesystem_battery.yaml
342index ba4fcac..3b1edbf 100644
343--- a/examples/tests/filesystem_battery.yaml
344+++ b/examples/tests/filesystem_battery.yaml
345@@ -99,3 +99,26 @@ storage:
346 label: myxfs
347 volume: d2p10
348 uuid: 9c537621-f2f4-4e24-a071-e05012a1a997
349+ - id: tmpfs1
350+ type: mount
351+ spec: "none"
352+ path: "/my/tmpfs"
353+ options: size=4194304
354+ fstype: "tmpfs"
355+ - id: ramfs1
356+ type: mount
357+ spec: "none"
358+ path: "/my/ramfs"
359+ fstype: "ramfs"
360+ - id: bind1
361+ fstype: "none"
362+ options: "bind"
363+ path: "/var/lib"
364+ spec: "/my/bind-over-var-lib"
365+ type: mount
366+ - id: bind2
367+ fstype: "none"
368+ options: "bind,ro"
369+ path: "/my/bind-ro-etc"
370+ spec: "/etc"
371+ type: mount
372diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
373index bd07708..58e068b 100644
374--- a/tests/unittests/helpers.py
375+++ b/tests/unittests/helpers.py
376@@ -63,7 +63,9 @@ class CiTestCase(TestCase):
377 # the file is not created or modified.
378 if _dir is None:
379 _dir = self.tmp_dir()
380- return os.path.normpath(os.path.abspath(os.path.join(_dir, path)))
381+
382+ return os.path.normpath(
383+ os.path.abspath(os.path.sep.join((_dir, path))))
384
385
386 def dir2dict(startdir, prefix=None):
387diff --git a/tests/unittests/test_commands_block_meta.py b/tests/unittests/test_commands_block_meta.py
388index 5eefbd3..17186d4 100644
389--- a/tests/unittests/test_commands_block_meta.py
390+++ b/tests/unittests/test_commands_block_meta.py
391@@ -2,14 +2,14 @@
392
393 from argparse import Namespace
394 from collections import OrderedDict
395+import copy
396 from mock import patch, call
397+import os
398
399 from curtin.commands import block_meta
400 from curtin import util
401 from .helpers import CiTestCase
402
403-import copy
404-
405
406 class TestBlockMetaSimple(CiTestCase):
407 def setUp(self):
408@@ -323,7 +323,7 @@ class TestBlockMeta(CiTestCase):
409 rendered_fstab = fh.read()
410
411 print(rendered_fstab)
412- self.assertEqual(rendered_fstab, expected)
413+ self.assertEqual(expected, rendered_fstab)
414
415
416 class TestZpoolHandler(CiTestCase):
417@@ -413,4 +413,357 @@ class TestZFSRootUpdates(CiTestCase):
418 with self.assertRaises(ValueError):
419 block_meta.zfsroot_update_storage_config(scfg)
420
421+
422+class TestFstabData(CiTestCase):
423+ mnt = {'id': 'm1', 'type': 'mount', 'device': 'fs1', 'path': '/',
424+ 'options': 'noatime'}
425+ base_cfg = [
426+ {'id': 'xda', 'type': 'disk', 'ptable': 'msdos'},
427+ {'id': 'xda1', 'type': 'partition', 'size': '3GB',
428+ 'device': 'xda'},
429+ {'id': 'fs1', 'type': 'format', 'fstype': 'ext4',
430+ 'volume': 'xda1', 'label': 'rfs'},
431+ ]
432+
433+ def _my_gptsv(self, d_id, _scfg):
434+ """local test replacement for get_path_to_storage_volume."""
435+ if d_id in ("xda", "xda1"):
436+ return "/dev/" + d_id
437+ raise RuntimeError("Unexpected call to gptsv with %s" % d_id)
438+
439+ def test_mount_data_raises_valueerror_if_not_mount(self):
440+ """mount_data on non-mount type raises ValueError."""
441+ mnt = self.mnt.copy()
442+ mnt['type'] = "not-mount"
443+ with self.assertRaisesRegexp(ValueError, r".*not type 'mount'"):
444+ block_meta.mount_data(mnt, {mnt['id']: mnt})
445+
446+ def test_mount_data_no_device_or_spec_raises_valueerror(self):
447+ """test_mount_data raises ValueError if no device or spec."""
448+ mnt = self.mnt.copy()
449+ del mnt['device']
450+ with self.assertRaisesRegexp(ValueError, r".*mount.*missing.*"):
451+ block_meta.mount_data(mnt, {mnt['id']: mnt})
452+
453+ def test_mount_data_invalid_device_ref_raises_valueerror(self):
454+ """test_mount_data raises ValueError if device is invalid ref."""
455+ mnt = self.mnt.copy()
456+ mnt['device'] = 'myinvalid'
457+ scfg = OrderedDict([(i['id'], i) for i in self.base_cfg + [mnt]])
458+ with self.assertRaisesRegexp(ValueError, r".*refers.*myinvalid"):
459+ block_meta.mount_data(mnt, scfg)
460+
461+ def test_mount_data_invalid_format_ref_raises_valueerror(self):
462+ """test_mount_data raises ValueError if format.volume is invalid."""
463+ mycfg = copy.deepcopy(self.base_cfg) + [self.mnt.copy()]
464+ scfg = OrderedDict([(i['id'], i) for i in mycfg])
465+ # change the 'volume' entry for the 'format' type.
466+ scfg['fs1']['volume'] = 'myinvalidvol'
467+ with self.assertRaisesRegexp(ValueError, r".*refers.*myinvalidvol"):
468+ block_meta.mount_data(scfg['m1'], scfg)
469+
470+ def test_non_device_mount_with_spec(self):
471+ """mount_info with a spec does not need device."""
472+ info = {'id': 'xm1', 'spec': 'none', 'type': 'mount',
473+ 'fstype': 'tmpfs', 'path': '/tmpfs'}
474+ self.assertEqual(
475+ block_meta.FstabData(
476+ spec="none", fstype="tmpfs", path="/tmpfs",
477+ options="defaults", freq="0", passno="0", device=None),
478+ block_meta.mount_data(info, {'xm1': info}))
479+
480+ @patch('curtin.block.iscsi.volpath_is_iscsi')
481+ @patch('curtin.commands.block_meta.get_path_to_storage_volume')
482+ def test_device_mount_basic(self, m_gptsv, m_is_iscsi):
483+ """Test mount_data for FstabData with a device."""
484+ m_gptsv.side_effect = self._my_gptsv
485+ m_is_iscsi.return_value = False
486+
487+ scfg = OrderedDict(
488+ [(i['id'], i) for i in self.base_cfg + [self.mnt]])
489+ self.assertEqual(
490+ block_meta.FstabData(
491+ spec=None, fstype="ext4", path="/",
492+ options="noatime", freq="0", passno="0", device="/dev/xda1"),
493+ block_meta.mount_data(scfg['m1'], scfg))
494+
495+ @patch('curtin.block.iscsi.volpath_is_iscsi', return_value=False)
496+ @patch('curtin.commands.block_meta.get_path_to_storage_volume')
497+ def test_device_mount_boot_efi(self, m_gptsv, m_is_iscsi):
498+ """Test mount_data fat fs gets converted to vfat."""
499+ bcfg = copy.deepcopy(self.base_cfg)
500+ bcfg[2]['fstype'] = 'fat32'
501+ mnt = {'id': 'm1', 'type': 'mount', 'device': 'fs1',
502+ 'path': '/boot/efi'}
503+ m_gptsv.side_effect = self._my_gptsv
504+
505+ scfg = OrderedDict(
506+ [(i['id'], i) for i in bcfg + [mnt]])
507+ self.assertEqual(
508+ block_meta.FstabData(
509+ spec=None, fstype="vfat", path="/boot/efi",
510+ options="defaults", freq="0", passno="0", device="/dev/xda1"),
511+ block_meta.mount_data(scfg['m1'], scfg))
512+
513+ @patch('curtin.block.iscsi.volpath_is_iscsi')
514+ @patch('curtin.commands.block_meta.get_path_to_storage_volume')
515+ def test_device_mount_iscsi(self, m_gptsv, m_is_iscsi):
516+ """mount_data for a iscsi device should have _netdev in opts."""
517+ m_gptsv.side_effect = self._my_gptsv
518+ m_is_iscsi.return_value = True
519+
520+ scfg = OrderedDict([(i['id'], i) for i in self.base_cfg + [self.mnt]])
521+ self.assertEqual(
522+ block_meta.FstabData(
523+ spec=None, fstype="ext4", path="/",
524+ options="noatime,_netdev", freq="0", passno="0",
525+ device="/dev/xda1"),
526+ block_meta.mount_data(scfg['m1'], scfg))
527+
528+ @patch('curtin.block.iscsi.volpath_is_iscsi')
529+ @patch('curtin.commands.block_meta.get_path_to_storage_volume')
530+ def test_spec_fstype_override_inline(self, m_gptsv, m_is_iscsi):
531+ """spec and fstype are preferred over lookups from 'device' ref.
532+
533+ If a mount entry has 'fstype' and 'spec', those are prefered over
534+ values looked up via the 'device' reference present in the entry.
535+ The test here enforces that the device reference present in
536+ the mount entry is not looked up, that isn't strictly necessary.
537+ """
538+ m_gptsv.side_effect = Exception(
539+ "Unexpected Call to get_path_to_storage_volume")
540+ m_is_iscsi.return_value = Exception(
541+ "Unexpected Call to volpath_is_iscsi")
542+
543+ myspec = '/dev/disk/by-label/LABEL=rfs'
544+ mnt = {'id': 'm1', 'type': 'mount', 'device': 'fs1', 'path': '/',
545+ 'options': 'noatime', 'spec': myspec, 'fstype': 'ext3'}
546+ scfg = OrderedDict([(i['id'], i) for i in self.base_cfg + [mnt]])
547+ self.assertEqual(
548+ block_meta.FstabData(
549+ spec=myspec, fstype="ext3", path="/",
550+ options="noatime", freq="0", passno="0",
551+ device=None),
552+ block_meta.mount_data(mnt, scfg))
553+
554+ @patch('curtin.commands.block_meta.mount_fstab_data')
555+ def test_mount_apply_skips_mounting_swap(self, m_mount_fstab_data):
556+ """mount_apply does not mount swap fs, but should write fstab."""
557+ fdata = block_meta.FstabData(
558+ spec="/dev/xxxx1", path="none", fstype='swap')
559+ fstab = self.tmp_path("fstab")
560+ block_meta.mount_apply(fdata, fstab=fstab)
561+ contents = util.load_file(fstab)
562+ self.assertEqual(0, m_mount_fstab_data.call_count)
563+ self.assertIn("/dev/xxxx1", contents)
564+ self.assertIn("swap", contents)
565+
566+ @patch('curtin.commands.block_meta.mount_fstab_data')
567+ def test_mount_apply_calls_mount_fstab_data(self, m_mount_fstab_data):
568+ """mount_apply should call mount_fstab_data to mount."""
569+ fdata = block_meta.FstabData(
570+ spec="/dev/xxxx1", path="none", fstype='ext3')
571+ target = self.tmp_dir()
572+ block_meta.mount_apply(fdata, target=target, fstab=None)
573+ self.assertEqual([call(fdata, target=target)],
574+ m_mount_fstab_data.call_args_list)
575+
576+ @patch('curtin.commands.block_meta.mount_fstab_data')
577+ def test_mount_apply_appends_to_fstab(self, m_mount_fstab_data):
578+ """mount_apply should append to fstab."""
579+ fdslash = block_meta.FstabData(
580+ spec="/dev/disk2", path="/", fstype='ext4')
581+ fdboot = block_meta.FstabData(
582+ spec="/dev/disk1", path="/boot", fstype='ext3')
583+ fstab = self.tmp_path("fstab")
584+ existing_line = "# this is my line"
585+ util.write_file(fstab, existing_line + "\n")
586+ block_meta.mount_apply(fdslash, fstab=fstab)
587+ block_meta.mount_apply(fdboot, fstab=fstab)
588+
589+ self.assertEqual(2, m_mount_fstab_data.call_count)
590+ lines = util.load_file(fstab).splitlines()
591+ self.assertEqual(existing_line, lines[0])
592+ self.assertIn("/dev/disk2", lines[1])
593+ self.assertIn("/dev/disk1", lines[2])
594+
595+ def test_fstab_line_for_data_swap(self):
596+ """fstab_line_for_data return value for swap fstab line."""
597+ fdata = block_meta.FstabData(
598+ spec="/dev/disk2", path="none", fstype='swap')
599+ self.assertEqual(
600+ ["/dev/disk2", "none", "swap", "sw", "0", "0"],
601+ block_meta.fstab_line_for_data(fdata).split())
602+
603+ def test_fstab_line_for_data_swap_no_path(self):
604+ """fstab_line_for_data return value for swap with path=None."""
605+ fdata = block_meta.FstabData(
606+ spec="/dev/disk2", path=None, fstype='swap')
607+ self.assertEqual(
608+ ["/dev/disk2", "none", "swap", "sw", "0", "0"],
609+ block_meta.fstab_line_for_data(fdata).split())
610+
611+ def test_fstab_line_for_data_not_swap_and_no_path(self):
612+ """fstab_line_for_data raises ValueError if no path and not swap."""
613+ fdata = block_meta.FstabData(
614+ spec="/dev/disk2", device=None, path="", fstype='ext3')
615+ with self.assertRaisesRegexp(ValueError, r".*empty.*path"):
616+ block_meta.fstab_line_for_data(fdata)
617+
618+ def test_fstab_line_for_data_with_options(self):
619+ """fstab_line_for_data return value with options."""
620+ fdata = block_meta.FstabData(
621+ spec="/dev/disk2", path="/mnt", fstype='btrfs', options='noatime')
622+ self.assertEqual(
623+ ["/dev/disk2", "/mnt", "btrfs", "noatime", "0", "0"],
624+ block_meta.fstab_line_for_data(fdata).split())
625+
626+ def test_fstab_line_for_data_with_passno_and_freq(self):
627+ """fstab_line_for_data should respect passno and freq."""
628+ fdata = block_meta.FstabData(
629+ spec="/dev/d1", path="/mnt", fstype='ext4', freq="1", passno="2")
630+ self.assertEqual(
631+ ["1", "2"], block_meta.fstab_line_for_data(fdata).split()[4:6])
632+
633+ def test_fstab_line_for_data_raises_error_without_spec_or_device(self):
634+ """fstab_line_for_data should raise ValueError if no spec or device."""
635+ fdata = block_meta.FstabData(
636+ spec=None, device=None, path="/", fstype='ext3')
637+ match = r".*missing.*spec.*device"
638+ with self.assertRaisesRegexp(ValueError, match):
639+ block_meta.fstab_line_for_data(fdata)
640+
641+ @patch('curtin.block.get_volume_uuid')
642+ def test_fstab_line_for_data_uses_uuid(self, m_get_uuid):
643+ """fstab_line_for_data with a device mounts by uuid."""
644+ fdata = block_meta.FstabData(
645+ device="/dev/disk2", path="/mnt", fstype='ext4')
646+ uuid = 'b30d2389-5152-4fbc-8f18-0385ef3046c5'
647+ m_get_uuid.side_effect = lambda d: uuid if d == "/dev/disk2" else None
648+ self.assertEqual(
649+ ["UUID=%s" % uuid, "/mnt", "ext4", "defaults", "0", "0"],
650+ block_meta.fstab_line_for_data(fdata).split())
651+ self.assertEqual(1, m_get_uuid.call_count)
652+
653+ @patch('curtin.block.get_volume_uuid')
654+ def test_fstab_line_for_data_uses_device_if_no_uuid(self, m_get_uuid):
655+ """fstab_line_for_data with a device and no uuid uses device."""
656+ fdata = block_meta.FstabData(
657+ device="/dev/disk2", path="/mnt", fstype='ext4')
658+ m_get_uuid.return_value = None
659+ self.assertEqual(
660+ ["/dev/disk2", "/mnt", "ext4", "defaults", "0", "0"],
661+ block_meta.fstab_line_for_data(fdata).split())
662+ self.assertEqual(1, m_get_uuid.call_count)
663+
664+ @patch('curtin.block.get_volume_uuid')
665+ def test_fstab_line_for_data__spec_and_dev_prefers_spec(self, m_get_uuid):
666+ """fstab_line_for_data should prefer spec over device."""
667+ spec = "/dev/xvda1"
668+ fdata = block_meta.FstabData(
669+ spec=spec, device="/dev/disk/by-uuid/7AC9-DEFF",
670+ path="/mnt", fstype='ext4')
671+ m_get_uuid.return_value = None
672+ self.assertEqual(
673+ ["/dev/xvda1", "/mnt", "ext4", "defaults", "0", "0"],
674+ block_meta.fstab_line_for_data(fdata).split())
675+ self.assertEqual(0, m_get_uuid.call_count)
676+
677+ @patch('curtin.util.ensure_dir')
678+ @patch('curtin.util.subp')
679+ def test_mount_fstab_data_without_target(self, m_subp, m_ensure_dir):
680+ """mount_fstab_data with no target param does the right thing."""
681+ fdata = block_meta.FstabData(
682+ device="/dev/disk1", path="/mnt", fstype='ext4')
683+ block_meta.mount_fstab_data(fdata)
684+ self.assertEqual(
685+ call(['mount', "-t", "ext4", "-o", "defaults",
686+ "/dev/disk1", "/mnt"], capture=True),
687+ m_subp.call_args)
688+ m_ensure_dir.assert_called()
689+
690+ def _check_mount_fstab_subp(self, fdata, expected, target=None):
691+ # expected currently is like: mount <device> <mp>
692+ # and thus mp will always be target + fdata.path
693+ if target is None:
694+ target = self.tmp_dir()
695+
696+ expected = [a if a != "_T_MP" else util.target_path(target, fdata.path)
697+ for a in expected]
698+ with patch("curtin.util.subp") as m_subp:
699+ block_meta.mount_fstab_data(fdata, target=target)
700+
701+ self.assertEqual(call(expected, capture=True), m_subp.call_args)
702+ self.assertTrue(os.path.isdir(self.tmp_path(fdata.path, target)))
703+
704+ def test_mount_fstab_data_with_spec_and_device(self):
705+ """mount_fstab_data with spec and device should use device."""
706+ self._check_mount_fstab_subp(
707+ block_meta.FstabData(
708+ spec="LABEL=foo", device="/dev/disk1", path="/mnt",
709+ fstype='ext4'),
710+ ['mount', "-t", "ext4", "-o", "defaults", "/dev/disk1", "_T_MP"])
711+
712+ def test_mount_fstab_data_with_spec_that_is_path(self):
713+ """If spec is a path outside of /dev, then prefix target."""
714+ target = self.tmp_dir()
715+ spec = "/mydata"
716+ self._check_mount_fstab_subp(
717+ block_meta.FstabData(
718+ spec=spec, path="/var/lib", fstype="none", options="bind"),
719+ ['mount', "-o", "bind", self.tmp_path(spec, target), "_T_MP"],
720+ target)
721+
722+ def test_mount_fstab_data_bind_type_creates_src(self):
723+ """Bind mounts should have both src and target dir created."""
724+ target = self.tmp_dir()
725+ spec = "/mydata"
726+ self._check_mount_fstab_subp(
727+ block_meta.FstabData(
728+ spec=spec, path="/var/lib", fstype="none", options="bind"),
729+ ['mount', "-o", "bind", self.tmp_path(spec, target), "_T_MP"],
730+ target)
731+ self.assertTrue(os.path.isdir(self.tmp_path(spec, target)))
732+
733+ def test_mount_fstab_data_with_spec_that_is_device(self):
734+ """If spec looks like a path to a device, then use it."""
735+ spec = "/dev/xxda1"
736+ self._check_mount_fstab_subp(
737+ block_meta.FstabData(spec=spec, path="/var/", fstype="ext3"),
738+ ['mount', "-t", "ext3", "-o", "defaults", spec, "_T_MP"])
739+
740+ def test_mount_fstab_data_with_device_no_spec(self):
741+ """mount_fstab_data mounts by spec if present, not require device."""
742+ spec = "/dev/xxda1"
743+ self._check_mount_fstab_subp(
744+ block_meta.FstabData(spec=spec, path="/home", fstype="ext3"),
745+ ['mount', "-t", "ext3", "-o", "defaults", spec, "_T_MP"])
746+
747+ def test_mount_fstab_data_with_uses_options(self):
748+ """mount_fstab_data mounts with -o options."""
749+ device = "/dev/xxda1"
750+ opts = "option1,option2,x=4"
751+ self._check_mount_fstab_subp(
752+ block_meta.FstabData(
753+ device=device, path="/var", fstype="ext3", options=opts),
754+ ['mount', "-t", "ext3", "-o", opts, device, "_T_MP"])
755+
756+ @patch('curtin.util.subp')
757+ def test_mount_fstab_data_does_not_swallow_subp_exception(self, m_subp):
758+ """verify that subp exception gets raised.
759+
760+ The implementation there could/should change to raise the
761+ ProcessExecutionError directly. Currently raises a RuntimeError."""
762+ my_error = util.ProcessExecutionError(
763+ stdout="", stderr="BOOM", exit_code=4)
764+ m_subp.side_effect = my_error
765+
766+ mp = self.tmp_path("my-mountpoint")
767+ with self.assertRaisesRegexp(RuntimeError, r"Mount failed.*"):
768+ block_meta.mount_fstab_data(
769+ block_meta.FstabData(device="/dev/disk1", path="/var"),
770+ target=mp)
771+ # dir should be created before call to subp failed.
772+ self.assertTrue(os.path.isdir(mp))
773+
774 # vi: ts=4 expandtab syntax=python
775diff --git a/tests/vmtests/test_fs_battery.py b/tests/vmtests/test_fs_battery.py
776index 5798d48..423cc1e 100644
777--- a/tests/vmtests/test_fs_battery.py
778+++ b/tests/vmtests/test_fs_battery.py
779@@ -52,6 +52,12 @@ class TestFsBattery(VMBaseClass):
780 cat /proc/partitions > proc_partitions
781 find /etc/network/interfaces.d > find_interfacesd
782 cat /proc/cmdline > cmdline
783+ cat /etc/fstab > fstab
784+ cat /proc/1/mountinfo > mountinfo
785+
786+ for p in /my/bind-over-var-lib/apt /my/bind-ro-etc/passwd; do
787+ [ -e "$p" ] && echo "$p: present" || echo "$p: missing"
788+ done > my-path-checks
789
790 set +x
791 serial="fsbattery"
792@@ -151,6 +157,49 @@ class TestFsBattery(VMBaseClass):
793 ["%s umount: PASS" % k for k in entries])
794 self.assertEqual(sorted(expected), sorted(results))
795
796+ def test_fstab_has_mounts(self):
797+ """Verify each of the expected "my" mounts got into fstab."""
798+ expected = [
799+ "none /my/tmpfs tmpfs size=4194304 0 0".split(),
800+ "none /my/ramfs ramfs defaults 0 0".split(),
801+ "/my/bind-over-var-lib /var/lib none bind 0 0".split(),
802+ "/etc /my/bind-ro-etc none bind,ro 0 0".split(),
803+ ]
804+ fstab_found = [
805+ l.split() for l in self.load_collect_file("fstab").splitlines()]
806+ self.assertEqual(expected, [e for e in expected if e in fstab_found])
807+
808+ def test_mountinfo_has_mounts(self):
809+ """Verify the my mounts got into mountinfo.
810+
811+ This is a light check that things got mounted. We do not check
812+ options as to not break on different kernel behavior.
813+ Maybe it could/should."""
814+ # mountinfo has src and path as 4th and 5th field.
815+ data = self.load_collect_file("mountinfo").splitlines()
816+ dest_src = {}
817+ for line in data:
818+ toks = line.split()
819+ if not (toks[3].startswith("/my/") or toks[4].startswith("/my/")):
820+ continue
821+ dest_src[toks[4]] = toks[3]
822+ self.assertTrue("/my/ramfs" in dest_src)
823+ self.assertTrue("/my/tmpfs" in dest_src)
824+ self.assertEqual(dest_src.get("/var/lib"), "/my/bind-over-var-lib")
825+ self.assertEqual(dest_src.get("/my/bind-ro-etc"), "/etc")
826+
827+ def test_expected_files_from_bind_mounts(self):
828+ data = self.load_collect_file("my-path-checks")
829+ # this file is <path>: (present|missing)
830+ paths = {}
831+ for line in data.splitlines():
832+ path, _, val = line.partition(":")
833+ paths[path] = val.strip()
834+
835+ self.assertEqual(
836+ {'/my/bind-over-var-lib/apt': 'present',
837+ '/my/bind-ro-etc/passwd': 'present'}, paths)
838+
839
840 class TrustyTestFsBattery(relbase.trusty, TestFsBattery):
841 __test__ = True

Subscribers

People subscribed via source and target branches