#!/bin/sh
#
# Copyright 2021 Johannes Schauer Marin Rodrigues <josch@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# Since Debian bug #898446 was closed, usernamespaces are enabled by default in
# Debian. Unfortunately, salsaci and debci both do not allow processes to
# unshare. To still be able to test software that requires namespace support we
# spin up a qemu virtual machine, install EXTRA_DEPS in it and then run the
# target SCRIPT as a normal user on that machine.
#
# Another advantage of using this wrapper is, that it can be run by any
# unprivileged user without building the package and spinning up autopkgtest
# first.

set -exu

EXTRA_DEPS=gnupg,sbuild,mmdebstrap,build-essential,uidmap,fakeroot,diffoscope
SCRIPT=./debian/tests/unshare

[ -e debian/tests/control ]
SOURCES="$(pwd)"

if [ -z ${AUTOPKGTEST_TMP+x} ]; then
	# if AUTOPKGTEST_TMP is not set, then this script is probably not
	# executed under autopkgtest
	TMPDIR=$(mktemp --directory)
	aptsources=
	MODE=auto
else
	# since AUTOPKGTEST_TMP is set, we assume that this script is executed
	# under autopkgtest --> switch to the temporary directory
	TMPDIR="$AUTOPKGTEST_TMP"
	mkdir -p "$TMPDIR"
	# we need to install the chroot using the same apt sources as used by
	# the autopkgtest chroot so that the packages to be tested are
	# available
	aptsources=
	if [ -e /etc/apt/sources.list ]; then
		aptsources="$aptsources /etc/apt/sources.list"
	fi
	for f in /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
		[ -e "$f" ] || continue
		aptsources="$aptsources $f"
	done
	MODE=root
fi

# make TMPDIR world-readable or otherwise it cannot be accessed in unshare mode
chmod a+rx "$TMPDIR"
cd "$TMPDIR"

# generate a new ssh key for us, so that we can authenticate ourselves to the
# setup system, as well as the cryptsystem (both dropbear and openssh) via
# public key instead of using passwords
if [ ! -e "$TMPDIR/id_rsa" ]; then
ssh-keygen -q -t rsa -f "$TMPDIR/id_rsa" -N ""
fi

cat << SCRIPT > "$TMPDIR/customize.sh"
#!/bin/sh
set -exu

rootfs="\$1"

# setup various files in /etc
echo host > "\$rootfs/etc/hostname"
echo "127.0.0.1 localhost host" > "\$rootfs/etc/hosts"
echo "/dev/vda1 / auto errors=remount-ro 0 1" > "\$rootfs/etc/fstab"
cat /etc/resolv.conf > "\$rootfs/etc/resolv.conf"
echo 'net.ipv4.ip_forward=1' > "\$rootfs/etc/sysctl.conf"

# give a trivial password to the root user for easy debugging in case something fails
echo root:abcdef | chroot "\$rootfs" /usr/sbin/chpasswd

# extlinux config to boot from /dev/vda1 with predictable network interface
# naming and a serial console for logging
cat << END > "\$rootfs/extlinux.conf"
default linux
timeout 0

label linux
kernel /vmlinuz
append initrd=/initrd.img root=/dev/vda1 net.ifnames=0 console=ttyS0
END

# network interface config
# we can use eth0 because we boot with net.ifnames=0 for predictable interface
# names
cat << END > "\$rootfs/etc/network/interfaces"
auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
END

# copy in the public key
mkdir "\$rootfs/root/.ssh"
cp "$TMPDIR/id_rsa.pub" "\$rootfs/root/.ssh/authorized_keys"
chroot "\$rootfs" chown 0:0 /root/.ssh/authorized_keys

chroot "\$rootfs" adduser --gecos user --disabled-password user
SCRIPT
chmod +x "$TMPDIR/customize.sh"

# mmdebstrap will not have access to any file:// apt repositories because the
# path will be outside the chroot. To fix this, copy their contents into the
# chroot. We could also bind-mount the repos but then their contents would only
# be available during installation and not anymore at a later point
cat << 'SCRIPT' > "$TMPDIR/setup.sh"
#!/bin/sh
set -exu

rootfs="$1"

apt-get indextargets \
	| grep-dctrl \( -F Created-By Packages -a --regex -F Repo-URI '^file://' \) -s Repo-URI -n \
	| while read uri; do
		repo=${uri#file://} # strip prefix
		mkdir -p "$rootfs/$repo"
		mmdebstrap --hook-helper "$rootfs" "$MMDEBSTRAP_MODE" setup env 1 sync-in "$repo" "$repo" <&$MMDEBSTRAP_HOOKSOCK >&$MMDEBSTRAP_HOOKSOCK
done
SCRIPT
chmod +x "$TMPDIR/setup.sh"

if [ ! -e "$TMPDIR/debian-unstable-host.tar" ]; then
mmdebstrap --variant=apt --mode=$MODE --verbose \
	--setup-hook="$TMPDIR/setup.sh" \
	--include=openssh-server,systemd-sysv,ifupdown,netbase,isc-dhcp-client,udev,policykit-1,linux-image-amd64,$EXTRA_DEPS \
	--customize-hook="$TMPDIR/customize.sh" \
	unstable "$TMPDIR/debian-unstable-host.tar" $aptsources
fi

# use guestfish to prepare the host system
#
#  - create a single 2G partition and unpack the rootfs tarball into it
#  - put a syslinux MBR into the first 440 bytes of the drive
#  - install extlinux and make partition bootable
#
# useful stuff to debug any errors:
#   LIBGUESTFS_BACKEND_SETTINGS=force_tcg
#   libguestfs-test-tool || true
#   export LIBGUESTFS_DEBUG=1 LIBGUESTFS_TRACE=1
guestfish -N "$TMPDIR/host.img"=disk:2G -- \
	part-disk /dev/sda mbr : \
	mkfs ext2 /dev/sda1 : \
	mount /dev/sda1 / : \
	tar-in "$TMPDIR/debian-unstable-host.tar" / : \
	mkdir /build : \
	copy-in "$SOURCES/." /build/ : \
	upload /usr/lib/SYSLINUX/mbr.bin /mbr.bin : \
	copy-file-to-device /mbr.bin /dev/sda size:440 : \
	rm /mbr.bin : \
	extlinux / : \
	sync : \
	umount / : \
	part-set-bootable /dev/sda 1 true : \
	shutdown

# start the host system
# prefer using kvm but fall back to tcg if not available
# avoid entropy starvation by feeding the crypt system with random bits from /dev/urandom
# the default memory size of 128 MiB is not enough for Debian, so we go with 1G
# use a virtio network card instead of emulating a real network device
# we don't need any graphics
# this also multiplexes the console and the monitor to stdio
# creates a multiplexed stdio backend connected to the serial port and the qemu
# monitor
# redirect tcp connections on port 10022 localhost to the host system port 22
# redirect all output to a file
# run in the background
qemu-system-x86_64 \
	-M accel=kvm:tcg \
	-no-user-config \
	-object rng-random,filename=/dev/urandom,id=rng0 -device virtio-rng-pci,rng=rng0 \
	-m 1G \
	-net nic,model=virtio \
	-nographic \
	-serial mon:stdio \
	-net user,hostfwd=tcp:127.0.0.1:10022-:22 \
	-drive file="$TMPDIR/host.img",format=raw,if=virtio \
	> "$TMPDIR/qemu.log" </dev/null 2>&1 &

# store the pid
QEMUPID=$!

onerror() {
	# attempt poweroff
	$ssh -o ConnectTimeout=$TIMEOUT root@localhost systemctl poweroff || true
	# give a few seconds for poweroff
	sleep 10
	kill $QEMUPID || true
	# turn off verbose output
	set +x
	echo "script failed -- temporary files are stored in $TMPDIR:"
	echo
	ls -lha "$TMPDIR"
	echo
	echo "to test yourself, run qemu with:"
	echo
	echo "    $ qemu-system-x86_64 -no-user-config -m 1G -net nic,model=virtio -nographic -serial mon:stdio -net user,hostfwd=tcp:127.0.0.1:10022-:22 -drive file=\"$TMPDIR/host.img\",format=raw,if=virtio"
	echo
	echo "and log in using:"
	echo
	echo "    user: root"
	echo "    pass: abcdef"
	echo
	echo "or connect to it via ssh:"
	echo
	echo "    $ $ssh root@localhost"
	echo
	echo "when you are done, cleanup temporary files with:"
	echo
	echo "    $ rm -r \"$TMPDIR\""
}

# show the log and kill qemu in case the script exits first
trap "cat --show-nonprinting $TMPDIR/qemu.log; onerror" EXIT

# the default ssh command does not store known hosts and even ignores host keys
# it identifies itself with the rsa key generated above
# pseudo terminal allocation is disabled or otherwise, programs executed via
# ssh might wait for input on stdin of the ssh process
ssh="ssh -oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no -i "$TMPDIR/id_rsa" -T -p 10022"

# we use sleepenh to make sure that we wait the right number of seconds
# independent on how long the command took beforehand
TIMESTAMP=$(sleepenh 0 || [ $? -eq 1 ])
# the timeout in seconds
TIMEOUT=5
# the maximum number of tries
NUM_TRIES=20
i=0
while true; do
	rv=0
	$ssh -o ConnectTimeout=$TIMEOUT root@localhost echo success || rv=1
	# with an exit code of zero, the ssh connection was successful
	# and we break out of the loop
	[ $rv -eq 0 ] && break
	# if the command before took less than $TIMEOUT seconds, wait the remaining time
	TIMESTAMP=$(sleepenh $TIMESTAMP $TIMEOUT || [ $? -eq 1 ]);
	# increment the counter and break out of the loop if we tried
	# too often
	i=$((i+1))
	if [ $i -ge $NUM_TRIES ]; then
		break
	fi
done

# if all tries were exhausted, the process failed
if [ $i -eq $NUM_TRIES ]; then
	echo "timeout reached: unable to connect to qemu via ssh"
	exit 1
fi

trap onerror EXIT

$ssh root@localhost env --chdir=/build/ AUTOPKGTEST_TMP=/tmp runuser -u user -- "$SCRIPT"

# shut the system off
trap - EXIT
$ssh root@localhost systemctl poweroff || true
wait $QEMUPID

# cleanup
for f in debian-unstable-host.tar id_rsa id_rsa.pub \
	qemu.log host.img customize.sh setup.sh; do
	rm "$TMPDIR/$f"
done
if [ -z ${AUTOPKGTEST_TMP+x} ]; then
	rmdir "$TMPDIR"
fi
