Allow creating chroots on a loopback device.
If the new --useimage flag is passed, instead of creating the chroot
inside a directory, create a loopback device containing an LVM thin LV
and mount it onto the chroot. This will enable a subsequent CL to set
up snapshots of the chroot state.
BUG=chromium:730144
TEST=Created/deleted/replaced a bunch of chroots with cros_sdk and a
local cbuildbot.
Change-Id: Ia79a944d2848c3e1ed00346b389b9f66fd5be4b7
Reviewed-on: https://chromium-review.googlesource.com/553404
Commit-Ready: Benjamin Gordon <bmgordon@chromium.org>
Tested-by: Benjamin Gordon <bmgordon@chromium.org>
Reviewed-by: Benjamin Gordon <bmgordon@chromium.org>
diff --git a/common.sh b/common.sh
index a6fa4e4..9c45adb 100644
--- a/common.sh
+++ b/common.sh
@@ -722,7 +722,8 @@
# will). As such, we have to unmount in reverse order to cleanly
# unmount submounts (think /dev/pts and /dev).
awk -v path="$1" -v len="${#1}" \
- '(substr($2, 1, len) == path) { print $2 }' /proc/mounts | \
+ '(substr($2, 1, len+1) == path ||
+ substr($2, 1, len+1) == (path "/")) { print $2 }' /proc/mounts | \
tac | \
sed -e 's/\\040(deleted)$//'
# Hack(zbehan): If a bind mount's source is mysteriously removed,
diff --git a/sdk_lib/enter_chroot.sh b/sdk_lib/enter_chroot.sh
index 19bfe1d..f573700 100755
--- a/sdk_lib/enter_chroot.sh
+++ b/sdk_lib/enter_chroot.sh
@@ -281,6 +281,10 @@
debug "Mounting chroot environment."
mount --make-rslave /
+ if grep -q -e "${FLAGS_chroot}[[:space:]]" /proc/mounts; then
+ debug "Changing $FLAGS_chroot to a private subtree."
+ mount --make-private "$FLAGS_chroot"
+ fi
MOUNT_CACHE=$(echo $(awk '{print $2}' /proc/mounts))
setup_mount none "-t proc" /proc
setup_mount none "-t sysfs" /sys
diff --git a/sdk_lib/make_chroot.sh b/sdk_lib/make_chroot.sh
index dccb872..85e3593 100755
--- a/sdk_lib/make_chroot.sh
+++ b/sdk_lib/make_chroot.sh
@@ -46,6 +46,7 @@
DEFINE_string toolchains_overlay_path "" \
"Use the toolchains overlay located on this path."
DEFINE_string cache_dir "" "Directory to store caches within."
+DEFINE_boolean useimage $FLAGS_FALSE "Mount the chroot on a loopback image."
# Parse command line flags.
FLAGS_HELP="usage: $SCRIPT_NAME [flags]"
@@ -121,17 +122,37 @@
cleanup() {
# Clean up mounts
safe_umount_tree "${FLAGS_chroot}"
+
+ # Destroy LVM loopback setup if we can find a VG associated with our path.
+ local chroot_img="${FLAGS_chroot}.img"
+ [[ -f "$chroot_img" ]] || return 0
+
+ local chroot_dev=$(losetup -j "$chroot_img" | cut -f1 -d:)
+ local chroot_vg=$(find_vg_name "$FLAGS_chroot" "$chroot_dev")
+ if [ -n "$chroot_vg" ] && vgs "$chroot_vg" >&/dev/null; then
+ info "Removing VG $chroot_vg."
+ vgremove -f "$chroot_vg" --noudevsync
+ fi
+ if [ -n "$chroot_dev" ]; then
+ info "Detaching $chroot_dev."
+ losetup -d "$chroot_dev"
+ fi
}
delete_existing() {
# Delete old chroot dir.
- if [[ ! -e "$FLAGS_chroot" ]]; then
+ local chroot_img="${FLAGS_chroot}.img"
+ if [[ ! -e "$FLAGS_chroot" && ! -f "$chroot_img" ]]; then
return
fi
- info "Cleaning up old mount points..."
+ info "Cleaning up old mount points and loopback device..."
cleanup
info "Deleting $FLAGS_chroot..."
rm -rf "$FLAGS_chroot"
+ if [[ -f "$chroot_img" ]]; then
+ info "Deleting $chroot_img..."
+ rm -f "$chroot_img"
+ fi
info "Done."
}
@@ -325,6 +346,119 @@
${decompress} -dc "${tarball_path}" | tar -xp -C "${dest_dir}"
}
+# Find a usable VG name for a given path and device. If there is an existing
+# VG associated with the device, it will be returned. If not, find an unused
+# name in the format cros_<safe_path>_NNN, where safe_path is an escaped version
+# of the last 90 characters of the path and NNN is a counter. Example:
+# /home/user/chromiumos/chroot/ -> cros_home+user+chromiumos+chroot_000.
+# If no unused name with this pattern can be found, return an empty string.
+find_vg_name() {
+ local chroot_path="$1"
+ local chroot_dev="$2"
+ chroot_path=${chroot_path##/}
+ chroot_path=${chroot_path%%/}
+ chroot_path=${chroot_path//[^A-Za-z0-9_+.-]/+}
+ chroot_path=${chroot_path: -$((${#chroot_path} < 90 ? ${#chroot_path} : 90))}
+ local vg_name=""
+ if [ -n "$chroot_dev" ]; then
+ vg_name=$(pvs -q --noheadings -o vg_name "$chroot_dev" 2>/dev/null | \
+ sed -e 's/^ *//')
+ fi
+ if [ -z "$vg_name" ]; then
+ local counter=0
+ vg_name=$(printf "cros_%s_%03d" "$chroot_path" "$counter")
+ while [ "$counter" -lt 1000 ] && vgs "$vg_name" >&/dev/null; do
+ counter=$((counter + 1))
+ vg_name=$(printf "cros_%s_%03d" "$chroot_path" "$counter")
+ done
+ if [ "$counter" -gt 999 ]; then
+ vg_name=""
+ fi
+ fi
+ echo "$vg_name"
+}
+
+# Create a loopback image and mount it on the chroot path so that we can take
+# snapshots before building. If an image already exists, try to mount it. The
+# chroot is initially mounted inside a temporary shared chroot.build subtree
+# that should have already been set up by the parent process, and then bind
+# mounted into the correct final location. The purpose of this indirection is
+# so that processes outside our mount namespace can see the top-level chroot
+# after we finish.
+mount_chroot_image() {
+ local chroot_image="$1"
+ local mount_path="$2"
+
+ # Make sure there's an image.
+ local existing_chroot=0
+ local chroot_dev=""
+ if [ -f "$chroot_image" ]; then
+ info "Attempting to reuse existing image file ${chroot_image}"
+ chroot_dev=$(losetup -j "$chroot_image" | cut -f1 -d:)
+ existing_chroot=1
+ else
+ dd if=/dev/null of="$chroot_image" bs=1G seek=500 >&/dev/null
+ fi
+
+ # Get/scan a loopback device attached to our image.
+ if [ -n "$chroot_dev" ]; then
+ pvscan -q "$chroot_dev" >&/dev/null
+ else
+ chroot_dev=$(losetup -f "$chroot_image" --show)
+ fi
+
+ # Find/create a VG on the loopback device.
+ chroot_vg=$(find_vg_name "$mount_path" "$chroot_dev")
+ if [ -z "$chroot_vg" ]; then
+ die_notrace "Unable to find usable VG name for ${mount_path}."
+ fi
+ if vgs "$chroot_vg" >&/dev/null; then
+ vgchange -q -a y --noudevsync "$chroot_vg" >/dev/null
+ else
+ vgcreate -q "$chroot_vg" "$chroot_dev" >/dev/null
+ fi
+
+ # Find/create an LV inside our VG. If the LV is new, also create the FS.
+ # We need to pass --noudevsync to lvcreate because we're running inside
+ # a separate IPC namespace from the udev process.
+ if lvs "$chroot_vg/chroot" >&/dev/null; then
+ lvchange -q -ay "$chroot_vg/chroot" --noudevsync >/dev/null
+ else
+ lvcreate -q -L 499G -T "${chroot_vg}/thinpool" -V500G -n chroot \
+ --noudevsync >/dev/null
+ mke2fs -q -m 0 -t ext4 "/dev/${chroot_vg}/chroot"
+ fi
+
+ # Mount the FS into a directory that should have been set up as a shared
+ # subtree by our parent process, then bind mount it into the place where
+ # it belongs. The parent will take care of moving the mount to the correct
+ # final place on the outside of our mount namespace after we exit.
+ local temp_chroot="${FLAGS_chroot}.build/chroot"
+ if ! mount -text4 -onoatime "/dev/${chroot_vg}/chroot" "$temp_chroot"; then
+ local chroot_example_opt=""
+ if [[ "$mount_path" != "$DEFAULT_CHROOT_DIR" ]]; then
+ chroot_example_opt="--chroot=$FLAGS_chroot"
+ fi
+
+ die_notrace <<EOF
+
+Unable to mount ${chroot_vg}/chroot on ${temp_chroot}. Check for corrupted
+image ${chroot_image}, or run
+
+cros_sdk --delete $chroot_example_opt
+
+to clean up an old chroot first.
+
+EOF
+ fi
+ mount --make-private "$temp_chroot"
+ mount --bind "$temp_chroot" "$mount_path"
+ mount --make-private "$mount_path"
+ if [ "$existing_chroot" = "1" ]; then
+ info "Mounted existing chroot image."
+ fi
+}
+
# Handle deleting an existing environment.
if [[ $FLAGS_delete -eq $FLAGS_TRUE || \
$FLAGS_replace -eq $FLAGS_TRUE ]]; then
@@ -344,6 +478,7 @@
CHROOT_OVERLAY="${OVERLAYS_ROOT}/chromiumos"
CHROOT_STATE="${FLAGS_chroot}/etc/debian_chroot"
CHROOT_VERSION="${FLAGS_chroot}/etc/cros_chroot_version"
+CHROOT_IMAGE="${FLAGS_chroot}.img"
# Pass proxy variables into the environment.
for type in http ftp all; do
@@ -356,6 +491,13 @@
# Create the destination directory.
mkdir -p "$FLAGS_chroot"
+[[ $FLAGS_useimage -eq $FLAGS_TRUE ]] && \
+ mount_chroot_image "$CHROOT_IMAGE" "$FLAGS_chroot"
+
+# If the version contains something non-zero, we were already created and this
+# is just a re-mount.
+[[ -f "$CHROOT_VERSION" && "$(<$CHROOT_VERSION)" != "0" ]] && exit 0
+
echo
if [[ -f "${CHROOT_STATE}" ]]; then
info "stage3 already set up. Skipping..."