#!/bin/bash
#
# This script can be used as a workaround for systemd autofs mount migration.
# The problem is that systemd is a clever guy: before mounting of actual file
# system on top of autofs mount, it first checks that device number of autofs
# mount is equal to the one, stored in systemd internals. If they do not match,
# systemd ignores kernel request.
# The problem happens each time autofs is restored (new device number for
# autofs superblock) and can't be properly solved without some kind of "device
# namespaces", where device number can be preserved.
# But some of systemd services can be painlessly restarted. Like
# proc-sys-fs-binfmt_misc.
#
# Usage:
# criu restore <options> --action-script $(pwd)/scripts/systemd-autofs-restart.sh
#
[ "$CRTOOLS_SCRIPT_ACTION" == "post-resume" ] || exit 0

if [ -z "$CRTOOLS_INIT_PID" ]; then
	echo "CRTOOLS_INIT_PID environment variable is not set"
	exit 1
fi

if [ ! -d "/proc/$CRTOOLS_INIT_PID" ]; then
	echo "Process with CRTOOLS_INIT_PID=$CRTOOLS_INIT_PID doesn't exist"
	exit 1
fi

NS_ENTER=/bin/nsenter
[ ! -x $NS_ENTER ] || NS_ENTER=/usr/bin/nsenter

if [ ! -x $NS_ENTER ]; then
	echo "$NS_ENTER binary not found"
	exit 2
fi

JOIN_CT="$NS_ENTER -t $CRTOOLS_INIT_PID -m -u -p"

# Skip container, if it's not systemd based
[ "$($JOIN_CT basename -- "$($JOIN_CT readlink /proc/1/exe)")" == "systemd" ] || exit 0

AUTOFS_SERVICES="proc-sys-fs-binfmt_misc.automount"

bindmount=""

function remove_bindmount {
	if [ -n "$bindmount" ]; then
		$JOIN_CT umount "$bindmount"
		$JOIN_CT rm -rf "$bindmount"
		bindmount=""
	fi
}
trap remove_bindmount EXIT

function get_fs_type {
	local mountpoint=$1

	local top_mount_id=""
	local top_mount_fs_type=""

	while IFS='' read -r line; do
		# Skip those entries which do not match the mountpoint
		[ "$(echo "$line" | awk '{print $5;}')" = "$mountpoint" ] || continue

		local mnt_id
		mnt_id=$(echo "$line" | awk '{print $1;}')
		local mnt_parent_id
		mnt_parent_id=$(echo "$line" | awk '{print $2;}')
		local mnt_fs_type
		mnt_fs_type=$(echo "$line" | sed 's/.* - //g' | awk '{print $1;}')

		# Skip mount entry, if not the first one and not a child
		[ -n "$top_mount_id" ] && [ "$mnt_parent_id" != "$top_mount_id" ] && continue

		top_mount_id=$mnt_id
		top_mount_fs_type=$mnt_fs_type
	done < "/proc/$CRTOOLS_INIT_PID/mountinfo"

	if [ -z "$top_mount_fs_type" ]; then
		echo "Failed to find $mountpoint mountpoint"
		return 1
	fi

	echo "$top_mount_fs_type"
	return 0
}

function bind_mount {
	local from=$1
	local to=$2

	$JOIN_CT mount --bind "$from" "$to" && return 0

	echo "Failed to bind mount $from to $to"
	return 1
}

function save_mountpoint {
	local mountpoint=$1
	local top_mount_fs_type=""

	if ! top_mount_fs_type=$(get_fs_type "$mountpoint"); then
		echo "$top_mount_fs_type"
		return
	fi

	# Nothing to do, if no file system is on top of autofs
	[ "$top_mount_fs_type" = "autofs" ] && return

	bindmount="$($JOIN_CT mktemp -d)"
	if [ -z "$bindmount" ]; then
		echo "Failed to create temporary directory"
		return 1
	fi

	# No need to unmount fs on top of autofs:
	# systemd will does it for us on service restart
	bind_mount "$mountpoint" "$bindmount" || $JOIN_CT rm -rf "$bindmount"
}

function restore_mountpoint {
	local mountpoint=$1

	[ -n "$bindmount" ] || return

	# Umount file system, remounted by systemd, if any
	if ! top_mount_fs_type=$(get_fs_type "$mountpoint"); then
		echo "$top_mount_fs_type"
		return
	fi

	# Nothing to do, if no file system is on top of autofs
	if [ "$top_mount_fs_type" != "autofs" ]; then
		$JOIN_CT umount "$mountpoint" || echo "Failed to umount $mountpoint"
	fi

	# Restore origin file system even if we failed to unmount the new one
	bind_mount "$bindmount" "$mountpoint"
	remove_bindmount
}

function restart_service {
	local service=$1
	local mountpoint
	mountpoint=$($JOIN_CT systemctl show "$service" -p Where | sed 's/.*=//g')

	if [ -z "$mountpoint" ]; then
		echo "Failed to discover $service mountpoint"
		return
	fi

	# Try to move restored bind-mount aside and exit if Failed
	# Nothing to do, if we Failed
	save_mountpoint "$mountpoint" || return

	if ! $JOIN_CT systemctl restart "$service"; then
		echo "Failed to restart $service service"
		return
	fi
	echo "$service restarted"

	# Try to move saved monutpoint back on top of autofs
	restore_mountpoint "$mountpoint"
}

for service in $AUTOFS_SERVICES; do
	status=$($JOIN_CT systemctl is-active "$service")

	if [ "$status" == "active" ]; then
		restart_service "$service"
	else
		echo "$service skipped ($status)"
	fi
done

exit 0
