#!/bin/sh

# Copyright 2019 Johannes 'josch' Schauer <josch@debian.org>
# Copyright 2021 Guilhem Moulin <guilhem@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.

set -eux
PATH="/usr/bin:/bin"
export PATH

TESTNAME="$(basename -- "$0")"

# get package version/distribution and local autopkgtest repo URI to
# pass along to mmdebstrap
VERSION="$(dpkg-parsechangelog -SVersion)"
apt-cache show "dropbear=$VERSION" >/dev/null || exit 1

# try to create /dev/kvm if missing, for instance in a schroot where /dev isn't managed by udev
if [ ! -c /dev/kvm ] && mknod -m0600 /dev/kvm c 10 232; then
    echo "INFO: Created character special file /dev/kvm" >&2
    if getent group kvm >/dev/null && chgrp kvm /dev/kvm; then
        # kvm group is created by udev.postinst
        chmod 0640 /dev/kvm
    fi
fi

if [ -c /dev/kvm ] && dd if=/dev/kvm count=0 status=none; then
    CPU_MODEL="host"
else
    CPU_MODEL="max"
    echo "WARN: KVM is not available, guests will be slow!" >&2
fi


# mkinitramfs(8) uses the default kernel by default, but we might be running an
# old kernel and need to use what's defined as dependency for this test anyway
DEB_HOST_ARCH="$(dpkg-architecture -qDEB_HOST_ARCH)"
KERNEL_VERSION="$(find /lib/modules -mindepth 1 -maxdepth 1 -name "*-$DEB_HOST_ARCH" -type d -printf "%P\\n" | sort -V | tail -n1)"
KERNEL="/boot/vmlinuz-$KERNEL_VERSION"
if [ -z "$KERNEL_VERSION" ] || [ ! -f "$KERNEL" ]; then
    echo "ERROR: Could not determine kernel to use, aborting" >&2
    exit 1
fi

# generate key file used to format and unlock the root FS
head -c32 </dev/urandom >"$AUTOPKGTEST_TMP/rootfs.key"

# rebuild initramfs with custom hook and boot scripts
# (note: cryptsetup-initramfs might spew warnings depending on the host's device stack -- these are irrelevant here)
install -m0755 "$0.d/initramfs-hook"   "/etc/initramfs-tools/hooks/autopkgtest-$TESTNAME-setup"
install -m0755 "$0.d/initramfs-script" "/etc/initramfs-tools/scripts/init-premount/autopkgtest-$TESTNAME-setup"
/usr/sbin/mkinitramfs -o "$AUTOPKGTEST_TMP/initrd.img" "$KERNEL_VERSION"
rm -f "/etc/initramfs-tools/hooks/autopkgtest-$TESTNAME-setup" "/etc/initramfs-tools/scripts/init-premount/autopkgtest-$TESTNAME-setup"


# generate SSH key used to log in to the guest
ssh-keygen -t rsa -C "$TESTNAME" -f "$AUTOPKGTEST_TMP/id_rsa" -N ""

# prepare chroot tarball with a minimal system (to find out missing
# dependencies); we do not use debootstrap because it's unable to create
# a system containing only essential:yes packages and their dependencies,
# and don't use multistrap because it's unmaintained
#
# aside from obious packages (kernel, boot loader, cryptsetup-initramfs, dropbear*)
# we need the following:
#   - e2fsprogs: for /sbin/fsck.ext4
#   - systemd-sysv: for /sbin/init
#   - systemd-cryptsetup: to unlock the swap device at boot time
#   - ifupdown: for networking
# (note: cryptsetup-initramfs and/or dropbear-initramfs might spew warnings depending on
# the host's device stack -- these are irrelevant here)
apt-get indextargets --format "deb \$(SITE) \$(SUITE) \$(COMPONENT)" \
    "Created-By: Packages" "Origin: Debian" >"$AUTOPKGTEST_TMP/sources.list"
AUTOPKGTEST_LOCAL_ARCHIVE="$(apt-get indextargets --format "\$(REPO_URI)" "Created-By: Packages" | \
    sed -nr '\,^(file|copy):((/+[^/[:blank:]]+)*/autopkgtest[.-].*[^/])/?$, {s//\2/p;q}')"
if [ -n "$AUTOPKGTEST_LOCAL_ARCHIVE" ]; then
    echo "deb [trusted=yes] file://$AUTOPKGTEST_LOCAL_ARCHIVE /" >>"$AUTOPKGTEST_TMP/sources.list"
fi
mmdebstrap --verbose --variant="apt" \
    --include="linux-image-$KERNEL_VERSION" \
    --include="grub2" \
    --include="cryptsetup-initramfs" \
    --include="dropbear=$VERSION,dropbear-initramfs=$VERSION" \
    --include="e2fsprogs" \
    --include="systemd-sysv" \
    --include="systemd-cryptsetup" \
    --include="ifupdown" \
    --customize-hook="upload \"$AUTOPKGTEST_TMP/id_rsa.pub\" /etc/dropbear/initramfs/authorized_keys" \
    ${AUTOPKGTEST_LOCAL_ARCHIVE:+--setup-hook="mkdir -p -- \"\$1${AUTOPKGTEST_LOCAL_ARCHIVE%/*}\"" \
                                 --setup-hook="copy-in $AUTOPKGTEST_LOCAL_ARCHIVE ${AUTOPKGTEST_LOCAL_ARCHIVE%/*}" \
                                 --customize-hook="rm -rf -- \"\$1${AUTOPKGTEST_LOCAL_ARCHIVE%/*}\"" \
                                 --customize-hook="sed -i 's,.*\\bfile://,#&,' \"\$1/etc/apt/sources.list\"" } \
    <"$AUTOPKGTEST_TMP/sources.list" >"$AUTOPKGTEST_TMP/debian.tar"


# pre-allocate disk image for the guest
DRIVE_IMG="$AUTOPKGTEST_TMP/disk.img"
dd if=/dev/zero of="$DRIVE_IMG" bs=1M count=2048 conv=fsync,excl status=none

# wrapper around qemu-system, passing common options and an optional timeout
SMP="cpus=2"
MEMORY="1G"
unset TIMEOUT
qemu_system() {
    local exe="qemu-system-x86_64" rv
    ${TIMEOUT:+timeout -k60s -sTERM "$TIMEOUT"} "$exe" -no-user-config -nodefaults \
        -name "autopkgtest-$TESTNAME" -machine "type=q35,accel=kvm:tcg,graphics=off" \
        -cpu "$CPU_MODEL" -smp "$SMP" -m "$MEMORY" \
        -serial stdio -vga virtio -display none -monitor none \
        -object "rng-random,id=rng0,filename=/dev/urandom" -device "virtio-rng-pci,rng=rng0" \
        -drive "file=$DRIVE_IMG,format=raw,if=virtio,cache=unsafe" \
        "$@" </dev/null && return 0 || rv=$?

    # timeout(1) exits with status 124 on timeout, or with status 128+9 if
    # the command is still running after 60s and a SIGKILL needs to be sent
    if [ -n ${TIMEOUT:+x} ] && [ $rv -eq 124 -o $rv -eq $((128+9)) ]; then
        echo "ERROR: $TIMEOUT timeout reached while running $exe" >&2
    else
        echo "ERROR: $exe exited with status $rv" >&2
    fi
    trap - EXIT # don't try to kill the terminated QEMU process
    exit 1
}

# prepare the target system from our custom initrd, passing the chroot tarball as secondary disk
TIMEOUT=1800s qemu_system \
    -drive "file=$AUTOPKGTEST_TMP/debian.tar,format=raw,if=virtio,readonly=on" \
    -kernel "$KERNEL" -initrd "$AUTOPKGTEST_TMP/initrd.img" -append "console=ttyS0"

# boot the target system -- without the chroot tarball but with a network card instead, forward
# connections from host port 12222 resp. 10022 to the guest's SSHd at initramfs stage resp.
# after pivoting
HOST_FWD="hostfwd=tcp:127.0.0.1:10022-10.0.2.129:22,hostfwd=tcp:127.0.0.1:12222-10.0.2.130:2222"
TIMEOUT=600s qemu_system \
    -netdev "user,restrict=on,id=net0,$HOST_FWD" -device "virtio-net-pci,netdev=net0" &

QEMU_PID=$!
trap "kill $QEMU_PID" EXIT # terminate the guest if this script exits first


# wrapper around ssh(1), passing common options
ssh() {
    command ssh -oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no \
                -oConnectTimeout=5 -i "$AUTOPKGTEST_TMP/id_rsa" -l root "$@"
}

# keep trying to ssh into QEMU up to $MAX_TRIES times
unset MAX_TRIES
ssh_retry() {
    local i=${MAX_TRIES:-24} rv
    while :; do
        ssh "$@" && rv=0 || rv=$?
        if [ $rv -ne 255 ]; then
            # the remote command was run, returns its exit status
            return $rv
        elif [ $i -le 1 ]; then
            echo "ERROR: Couldn't SSH into QEMU: maximum number of tries exceeded" >&2
            exit 1
        fi
        i=$((i - 1))
        sleep 5
    done
}

# wait for dropbear to start on the guest, then remotely unlock the system
ssh_retry -Tp 12222 "127.0.0.1" /nonexistent <"$AUTOPKGTEST_TMP/rootfs.key"

# wait for the main system to finish booting and its SSHd to start, then
# make sure the root FS and swap are held by dm-crypt devices
# XXX we have to use cryptsetup(8)'s full path here, see #903403
ssh_retry -nTp 10022 "127.0.0.1" echo ping
ssh -nTp 10022 "127.0.0.1" lsblk -no"MOUNTPOINT" /dev/mapper/root_crypt | grep -Fx "/"
#ssh -nTp 10022 "127.0.0.1" lsblk -no"MOUNTPOINT" /dev/mapper/swap_crypt | grep -Fx "[SWAP]"
ssh  -Tp 10022 "127.0.0.1" /sbin/cryptsetup luksOpen --key-file=- \
    --test-passphrase /dev/vda4 <"$AUTOPKGTEST_TMP/rootfs.key"

# power the guest down and wait for QEMU to gracefully terminate
ssh -nTp 10022 "127.0.0.1" systemctl poweroff

trap - EXIT
wait $QEMU_PID
