cros_sdk: Create chroots on a loopback device.
We want to move the chroot to a loopback filesystem running on LVM so
that it can be snapshotted/restored when builds fail. Loopback setup is
handled in make_chroot.sh as of crrev.com/c/553404, but cros_sdk needs
to be modified to pass the new flag and make the new chroot mount
visible outside the unshared mount namespace.
The new behavior can be disabled by passing --nouse-image to cros_sdk.
BUG=chromium:730144
TEST=Created/deleted/replaced a bunch of chroots with cros_sdk and a
local cbuildbot.
Change-Id: I612642a1b396791caa130b87b79632e5cd9bec35
Reviewed-on: https://chromium-review.googlesource.com/553462
Commit-Ready: Benjamin Gordon <bmgordon@chromium.org>
Tested-by: Benjamin Gordon <bmgordon@chromium.org>
Reviewed-by: Simon Glass <sjg@chromium.org>
diff --git a/scripts/cros_sdk.py b/scripts/cros_sdk.py
index e0790e2..603b1e5 100644
--- a/scripts/cros_sdk.py
+++ b/scripts/cros_sdk.py
@@ -65,6 +65,10 @@
# Tools needed for --proxy-sim only.
PROXY_NEEDED_TOOLS = ('ip',)
+# Tools needed when use_image is true (the default).
+IMAGE_NEEDED_TOOLS = ('losetup', 'lvchange', 'lvcreate', 'lvs', 'mke2fs',
+ 'pvscan', 'vgchange', 'vgcreate', 'vgs')
+
def GetArchStageTarballs(version):
"""Returns the URL for a given arch/version"""
@@ -186,7 +190,7 @@
def CreateChroot(chroot_path, sdk_tarball, toolchains_overlay_tarball,
- cache_dir, nousepkg=False):
+ cache_dir, nousepkg=False, useimage=False):
"""Creates a new chroot from a given SDK"""
cmd = MAKE_CHROOT + ['--stage3_path', sdk_tarball,
@@ -199,6 +203,9 @@
if nousepkg:
cmd.append('--nousepkg')
+ if useimage:
+ cmd.append('--useimage')
+
logging.notice('Creating chroot. This may take a few minutes...')
try:
cros_build_lib.RunCommand(cmd, print_cmd=False)
@@ -254,6 +261,45 @@
raise SystemExit(ret.returncode)
+def _FindChrootDevice(chroot_path):
+ """Find the VG and LV mounted on chroot_path.
+
+ Returns:
+ A tuple containing the VG and LV names, or (None, None) if an appropriately-
+ name device mounted on |chroot_path| isn't found.
+ """
+
+ mount = [m for m in osutils.IterateMountPoints()
+ if m.destination == chroot_path]
+ if not mount:
+ return (None, None)
+
+ # Take the last mount entry because it's the one currently visible.
+ mount_source = mount[-1].source
+ match = re.match(r'/dev.*/(cros[^-]*)-(.*)', mount_source)
+ if not match:
+ return (None, None)
+
+ return (match.group(1), match.group(2))
+
+
+def _FindSubmounts(*args):
+ """Find all mounts matching each of the paths in |args| and any submounts.
+
+ Returns:
+ A list of all matching mounts in the order found in /proc/mounts.
+ """
+ mounts = []
+ paths = [p.rstrip('/') for p in args]
+ for mtab in osutils.IterateMountPoints():
+ for path in paths:
+ if mtab.destination == path or mtab.destination.startswith(path + '/'):
+ mounts.append(mtab.destination)
+ break
+
+ return mounts
+
+
def _SudoCommand():
"""Get the 'sudo' command, along with all needed environment variables."""
@@ -470,7 +516,6 @@
# We must set up the cgroups mounts before we enter our own namespace.
# This way it is a shared resource in the root mount namespace.
cgroups.Cgroup.InitSystem()
- namespaces.SimpleUnshare()
def _CreateParser(sdk_latest_version, bootstrap_latest_version):
@@ -486,6 +531,10 @@
parser.add_argument(
'--chroot', dest='chroot', default=default_chroot, type='path',
help=('SDK chroot dir name [%s]' % constants.DEFAULT_CHROOT_DIR))
+ parser.add_argument('--nouse-image', dest='use_image', action='store_false',
+ default=True,
+ help='Do not mount the chroot on a loopback image; '
+ 'instead, create it directly in a directory.')
parser.add_argument('--chrome_root', type='path',
help='Mount this chrome root into the SDK chroot')
@@ -595,6 +644,8 @@
_ReportMissing(osutils.FindMissingBinaries(NEEDED_TOOLS))
if options.proxy_sim:
_ReportMissing(osutils.FindMissingBinaries(PROXY_NEEDED_TOOLS))
+ if options.use_image:
+ _ReportMissing(osutils.FindMissingBinaries(IMAGE_NEEDED_TOOLS))
if (sdk_latest_version == '<unknown>' or
bootstrap_latest_version == '<unknown>'):
@@ -606,10 +657,6 @@
' http://www.chromium.org/chromium-os/developer-guide')
_ReExecuteIfNeeded([sys.argv[0]] + argv)
- if options.ns_pid:
- first_pid = namespaces.CreatePidNs()
- else:
- first_pid = None
# Expand out the aliases...
if options.replace:
@@ -645,6 +692,43 @@
if options.enter:
options.create |= not chroot_exists
+ # This dance is to support mounting the chroot inside a separate mount
+ # namespace. While we're here in the original namespace, we set up a
+ # temporary shared subtree
+ # (https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) and
+ # then let CreateChroot operate inside there. Since it's shared, we can see
+ # the mount that gets created. make_chroot.sh marks its mount private inside
+ # the namespace, so none of the inner mounts leak out. After the chroot has
+ # been created, we will return to this mount namespace (using an fd that we
+ # open here and save) and move the mount from the temporary spot to the
+ # requested final location. Once this is done, future uses of the same chroot
+ # don't have to jump through any of these hoops until it gets unmounted. If
+ # we're running on a system where things are mounted shared by default then
+ # all this isn't necessary, but it seems safer to assume we need this setup
+ # rather than try to detect it.
+ if options.use_image:
+ chroot_temp_parent = options.chroot + '.build'
+ chroot_temp_mount = os.path.join(chroot_temp_parent, 'chroot')
+ already_mounted = [m for m in osutils.IterateMountPoints()
+ if m.destination == chroot_temp_parent]
+ if not already_mounted and options.create:
+ osutils.SafeMakedirsNonRoot(chroot_temp_mount)
+ osutils.Mount(chroot_temp_parent, chroot_temp_parent, '', osutils.MS_BIND)
+ osutils.Mount('', chroot_temp_parent, '', osutils.MS_SHARED)
+ parent_ns_file = open('/proc/%d/ns/mnt' % (os.getppid(),))
+ parent_ns = parent_ns_file.fileno()
+
+ # If we're going to delete, also make sure the chroot isn't mounted
+ # before we enter the new mount namespace.
+ if options.delete:
+ osutils.UmountTree(options.chroot)
+
+ namespaces.SimpleUnshare()
+ if options.ns_pid:
+ first_pid = namespaces.CreatePidNs()
+ else:
+ first_pid = None
+
if not options.sdk_version:
sdk_version = (bootstrap_latest_version if options.bootstrap
else sdk_latest_version)
@@ -684,7 +768,8 @@
if options.proxy_sim:
_ProxySimSetup(options)
- if options.delete and os.path.exists(options.chroot):
+ if options.delete and (os.path.exists(options.chroot) or
+ os.path.exists(options.chroot + '.img')):
lock.write_lock()
DeleteChroot(options.chroot)
@@ -730,7 +815,8 @@
lock.write_lock()
CreateChroot(options.chroot, sdk_tarball, toolchains_overlay_tarball,
options.cache_dir,
- nousepkg=(options.bootstrap or options.nousepkg))
+ nousepkg=(options.bootstrap or options.nousepkg),
+ useimage=options.use_image)
if options.enter:
lock.read_lock()
@@ -738,3 +824,30 @@
options.chrome_root_mount, options.workspace,
options.goma_dir, options.goma_client_json,
chroot_command)
+
+ # Remount the inner chroot mount back up to the original namespace. See above
+ # for details.
+ if options.use_image and options.create:
+ vg, lv = _FindChrootDevice(chroot_temp_mount)
+
+ # Clean up inside the child mount namespace. Normally these will disappear
+ # as soon as the last process exits the mount namespace, but we want to be
+ # able to clean up the underlying directories without waiting for the forked
+ # "init" copes of cros_sdk to exit the namespace. This is safe to do even
+ # with multiple processes in the same chroot because the other cros_sdk
+ # copies will have their own mount namespace.
+ chroot_mounts = _FindSubmounts(chroot_temp_parent, options.chroot)
+ osutils.UmountTree(chroot_temp_parent)
+ osutils.UmountTree(options.chroot)
+
+ namespaces.SetNS(parent_ns, 0)
+ chroot_mounts = _FindSubmounts(chroot_temp_parent, options.chroot)
+ if not options.chroot in chroot_mounts:
+ if not vg or not lv:
+ cros_build_lib.Die('Unable to find VG/LV mounted on %s after building '
+ 'chroot.' % chroot_temp_mount)
+ osutils.UmountTree(chroot_temp_parent)
+ osutils.RmDir(chroot_temp_parent, ignore_missing=True)
+
+ chroot_dev_path = '/dev/mapper/%s-%s' % (vg, lv)
+ osutils.Mount(chroot_dev_path, options.chroot, 'ext4', osutils.MS_NOATIME)