#!/bin/sh
set -eu

# alpine-qemu-install ISO DISK
#
# Unattended install of Alpine onto DISK (a disk image pre-created with
# qemu-img) using the Alpine virt ISO. DISK appears as /dev/vda inside the
# guest and is erased.

[ "$#" -eq 2 ] || { echo "usage: ROOT_PASSWORD=... $0 ISO DISK" >&2; exit 1; }
: "${ROOT_PASSWORD:?set ROOT_PASSWORD}"

ISO=$1
DISK=$2
[ -f "$ISO" ]  || { echo "error: ISO not found: $ISO"   >&2; exit 1; }
[ -f "$DISK" ] || { echo "error: DISK not found: $DISK" >&2; exit 1; }

ROOT_PUBKEY=${ROOT_PUBKEY:-}
SSHD=${SSHD:-openssh}
VM_HOSTNAME=${VM_HOSTNAME:-alpine}
APK_MIRROR=${APK_MIRROR:-https://dl-cdn.alpinelinux.org/alpine}
RAM_MB=${RAM_MB:-1024}
SMP=${SMP:-2}

for t in 7z tar openssl qemu-system-x86_64; do
    command -v "$t" >/dev/null || { echo "error: need $t" >&2; exit 1; }
done

# Hash the password on the host so the plaintext never reaches the guest,
# the apkovl, or the set -x install log. openssl reads one line on stdin,
# so it never appears in /proc/*/cmdline and a newline would truncate it.
case $ROOT_PASSWORD in
    *'
'*) echo "error: ROOT_PASSWORD must not contain a newline" >&2; exit 1 ;;
esac
ROOT_HASH=$(printf '%s\n' "$ROOT_PASSWORD" | openssl passwd -6 -stdin)
unset ROOT_PASSWORD

# Shell-quote a value for embedding into generated shell source.
shq() {
    printf "'"
    printf '%s' "$1" | sed "s/'/'\\\\''/g"
    printf "'"
}

# 0755 overlay dirs; they are unpacked over / in the guest and their modes
# survive onto the installed /etc. The workdir itself is 0700 from mktemp.
umask 022
WORK=$(mktemp -d "${TMPDIR:-/tmp}/alpine-qemu-install.XXXXXX")
trap 'rm -rf "$WORK"' EXIT
trap 'exit 1' HUP INT TERM

7z x -y -o"$WORK" "$ISO" boot/vmlinuz-virt boot/initramfs-virt boot/modloop-virt >/dev/null
for f in boot/vmlinuz-virt boot/initramfs-virt boot/modloop-virt; do
    [ -s "$WORK/$f" ] || { echo "error: $f missing from ISO" >&2; exit 1; }
done

# Alpine's normal boot mounts modloop-virt to populate /lib/modules, but
# QEMU's vvfat is unreliable for a ~150 MB file and the initramfs silently
# fails to find it. Without /lib/modules setup-alpine can't modprobe, can't
# partition, can't run apk post-install scripts. Pre-populating /lib/modules
# from the modloop squashfs side-steps the modloop mount path entirely.
7z x -y -o"$WORK/mods" "$WORK/boot/modloop-virt" >/dev/null
[ -d "$WORK/mods/modules" ] || {
    echo "error: 7z could not extract modules/ from modloop-virt" >&2
    echo "       (install 7zip or p7zip-full, or the modloop layout changed)" >&2
    exit 1
}

OVL=$WORK/ovl
mkdir -p "$OVL/etc/local.d" \
         "$OVL/etc/runlevels/default" \
         "$OVL/lib"
mv "$WORK/mods/modules" "$OVL/lib/modules"
# The modloop nests firmware/ inside modules/. The virt kernel loads no
# firmware under QEMU, so drop it rather than ship it to a path the kernel
# never searches.
rm -rf "$OVL/lib/modules/firmware"

# Run autoinstall.start on first boot via the default-runlevel "local" service.
ln -s ../../init.d/local "$OVL/etc/runlevels/default/local"

cat >"$OVL/etc/local.d/autoinstall.start" <<EOF
#!/bin/sh
set -eu
PATH=/sbin:/bin:/usr/sbin:/usr/bin

# Baked in at build time, assigned before set -x to keep the password hash
# out of the install log.
ROOT_HASH=$(shq "$ROOT_HASH")
ROOT_PUBKEY=$(shq "$ROOT_PUBKEY")
APK_MIRROR=$(shq "$APK_MIRROR")
VM_HOSTNAME=$(shq "$VM_HOSTNAME")
SSHD=$(shq "$SSHD")

exec >/dev/ttyS0 2>&1
set -x

# Any exit path reports an explicit result through the isa-debug-exit port
# (0xf4 = 244) and powers off; QEMU has -no-reboot so the user sees the
# full log. Writing value v makes qemu exit 2v+1: 0x10 -> 33 for success,
# 0x11 -> 35 for failure, both away from qemu's own exit codes. A qemu exit
# of 0 therefore means the install never finished.
on_exit() {
    rc=\$?
    trap - EXIT INT TERM HUP
    sync
    if [ "\$rc" -eq 0 ]; then
        printf '\\020' | dd of=/dev/port bs=1 seek=244 || true
    else
        printf '\\021' | dd of=/dev/port bs=1 seek=244 || true
    fi
    poweroff -f
}
trap on_exit EXIT
trap 'exit 1' INT TERM HUP

# Don't re-trigger on the installed system's first boot.
rm -f /etc/local.d/autoinstall.start /etc/runlevels/default/local

i=0
while [ ! -b /dev/vda ]; do
    i=\$((i+1))
    [ "\$i" -ge 30 ] && { echo 'fatal: /dev/vda did not appear within 30s'; exit 1; }
    sleep 1
done

# Bring up eth0 before setup-alpine starts. setup-alpine's very first step
# (keymap) needs to apk-add kbd-bkeymaps even when KEYMAPOPTS=none, which
# means the mirror must already be reachable. setup-alpine's own interfaces
# step runs later and will bounce the link; the DHCP re-negotiation is fine.
ip link set lo up
ip link set eth0 up
udhcpc -i eth0 -q -n -T 3 -t 5

# Write /etc/apk/repositories with the live system's version so apk works
# for every setup-alpine sub-step. setup-disk copies this file into the
# target, so the installed system gets the same repos.
ALPINE_VER=\$(awk -F. '{print "v"\$1"."\$2; exit}' /etc/alpine-release)
cat >/etc/apk/repositories <<EOF2
\$APK_MIRROR/\$ALPINE_VER/main
\$APK_MIRROR/\$ALPINE_VER/community
EOF2
apk update

# Generate setup-alpine's answer file here so APKREPOSOPTS can carry the
# version-qualified URLs directly. Passing full repo URLs bypasses
# setup-apkrepos's path-appending logic (which produced a bare URL)
# and avoids the "APKREPOSOPTS=none" footgun (it wrote the literal "none"
# into /etc/apk/repositories and broke every later apk fetch).
cat >/root/setup.answer <<EOF2
KEYMAPOPTS=none
HOSTNAMEOPTS='\$VM_HOSTNAME'
DEVDOPTS=mdev
INTERFACESOPTS='auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
'
DNSOPTS=none
TIMEZONEOPTS=none
PROXYOPTS=none
APKREPOSOPTS='\$APK_MIRROR/\$ALPINE_VER/main \$APK_MIRROR/\$ALPINE_VER/community'
USEROPTS=none
SSHDOPTS='\$SSHD'
ROOTSSHKEY=none
NTPOPTS=none
DISKOPTS='-m sys /dev/vda'
LBUOPTS=none
APKCACHEOPTS=none
EOF2

export KERNELOPTS='console=ttyS0,115200'
ERASE_DISKS=/dev/vda setup-alpine -e -f /root/setup.answer

# setup-disk umounts /mnt when it finishes. Remount the root partition to
# patch /etc/shadow, /etc/inittab, /etc/securetty. With DISKOPTS='-m sys
# /dev/vda' the layout is vda1=boot, vda2=swap, vda3=root.
mount /dev/vda3 /mnt
[ -e /mnt/etc/shadow ] || { echo 'fatal: /mnt/etc/shadow missing after remount'; exit 1; }

# Alpine's busybox has no chpasswd applet (it's in the separate shadow
# package), so the crypt hash goes straight into /etc/shadow. Keep it out
# of the set -x log; it is still crackable.
set +x
sed -i "s|^root:[^:]*:|root:\$ROOT_HASH:|" /mnt/etc/shadow
set -x

# Alpine's default sshd_config has PermitRootLogin=prohibit-password, so just
# dropping the key in authorized_keys is enough to enable SSH root logins.
if [ -n "\$ROOT_PUBKEY" ]; then
    mkdir -p /mnt/root/.ssh
    chmod 0700 /mnt/root/.ssh
    printf '%s\\n' "\$ROOT_PUBKEY" >/mnt/root/.ssh/authorized_keys
    chmod 0600 /mnt/root/.ssh/authorized_keys
fi

# Alpine's live initramfs adds its own ttyS0 getty to the live inittab
# (console=ttyS0), and setup-disk copies that file to the target. Append a
# getty only if no active ttyS0 entry exists; two gettys on one tty split
# the input between them and console login always fails.
grep -q '^ttyS0:' /mnt/etc/inittab \\
    || echo 'ttyS0::respawn:/sbin/getty -L 115200 ttyS0 vt100' >>/mnt/etc/inittab
grep -qxF ttyS0 /mnt/etc/securetty 2>/dev/null \\
    || echo ttyS0 >>/mnt/etc/securetty

umount /mnt
EOF
chmod 0755 "$OVL/etc/local.d/autoinstall.start"

# vvfat is reliable for a single small file like the apkovl.
mkdir -p "$WORK/fat"
tar -C "$OVL" --owner=0 --group=0 --numeric-owner \
    -czf "$WORK/fat/localhost.apkovl.tar.gz" etc lib

CMDLINE='console=ttyS0,115200 modules=loop,squashfs,sd-mod,sr-mod,cdrom,iso9660,virtio_pci,virtio_blk,virtio_net,fat,vfat,nls_cp437,nls_ascii'

# QEMU splits -drive options on commas, so a comma in a path must be doubled.
qesc() {
    printf '%s' "$1" | sed 's/,/,,/g'
}

set -- qemu-system-x86_64 \
    -m "$RAM_MB" \
    -smp "$SMP" \
    -nographic \
    -no-reboot \
    -device isa-debug-exit,iobase=0xf4,iosize=1 \
    -kernel "$WORK/boot/vmlinuz-virt" \
    -initrd "$WORK/boot/initramfs-virt" \
    -append "$CMDLINE" \
    -drive file="$(qesc "$ISO")",media=cdrom,readonly=on \
    -drive file="$(qesc "$DISK")",if=virtio \
    -drive file=fat:rw:"$(qesc "$WORK/fat")",format=raw,if=virtio \
    -nic user,model=virtio-net-pci

# KVM when /dev/kvm is writable; plain emulation otherwise (slower, works).
[ -w /dev/kvm ] && set -- "$@" -enable-kvm -cpu host

set +e
"$@"
rc=$?
set -e

# qemu maps the guest's isa-debug-exit writes to 33 (ok) and 35 (failed).
case $rc in
    33) exit 0 ;;
    35) exit 3 ;;
    0) echo "error: qemu stopped without an install result" >&2; exit 1 ;;
    *) exit "$rc" ;;
esac
