#!/bin/sh
# shellcheck disable=SC3043
#
# autopkgtest-build-lxd is part of autopkgtest
# autopkgtest is a tool for testing Debian binary packages
#
# autopkgtest is Copyright (C) 2006-2015 Canonical Ltd.
#
# Build or update an LXD image autopkgtest/<distro>/<release>/<arch> with
# autopkgtest customizations from an arbitrary existing image.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# See the file CREDITS for a full list of credits information (often
# installed as /usr/share/doc/autopkgtest/CREDITS).
set -eu

# generate temporary instance name
generate_instance_name() {
    while true; do
        AUTOPKGTEST_TEMP_INSTANCE=$(mktemp autopkgtest-prepare-XXX -u)
        "$COMMAND" info "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" >/dev/null 2>&1 || break
    done
}

# detect apt proxy
proxy_detect() {
    # support backwards compatible env var too
    AUTOPKGTEST_APT_PROXY=${AUTOPKGTEST_APT_PROXY:-${ADT_APT_PROXY:-}}
    if [ -z "$AUTOPKGTEST_APT_PROXY" ]; then
        RES=$(apt-config shell proxy Acquire::http::Proxy)
        eval "$RES"
        if echo "${proxy:-}" | grep -E -q '(localhost|127\.0\.0\.[0-9]*)'; then
            # translate proxy address to one that can be accessed from the
            # running instance
            local bridge_interface
            bridge_interface=$("$COMMAND" profile show default | sed -n '/parent:/ { s/^.*: *//; p; q }') || true
            if [ -n "$bridge_interface" ]; then
                local bridge_ip
                bridge_ip=$(ip -4 a show dev "$bridge_interface" | awk '/ inet / {sub(/\/.*$/, "", $2); print $2}') || true
                if [ -n "$bridge_ip" ]; then
                    AUTOPKGTEST_APT_PROXY=$(echo "$proxy" | sed -r "s#localhost|127\.0\.0\.[0-9]*#$bridge_ip#")
                fi
            fi
            if [ -n "$AUTOPKGTEST_APT_PROXY" ]; then
                echo "Detected local apt proxy, using $AUTOPKGTEST_APT_PROXY as instance proxy"
            fi
        elif [ -n "${proxy:-}" ]; then
            AUTOPKGTEST_APT_PROXY="$proxy"
            echo "Using $AUTOPKGTEST_APT_PROXY as instance proxy"
        fi
    fi
}

get_boot_id() {
    if boot_id=$("$COMMAND" exec -n "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- cat /proc/sys/kernel/random/boot_id); then
        echo "$boot_id"
    else
        echo unknown
    fi
}

safe_reboot() {
    previous_boot_id="$(get_boot_id)"
    echo "Issuing reboot (current boot id: $previous_boot_id)"
    "$COMMAND" exec -n "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- reboot || true
    current_boot_id="$(get_boot_id)"
    timeout=36  # 3 minutes should be enough
    while [ $timeout -ge 0 ] && { [ "$current_boot_id" = "unknown" ] || [ "$current_boot_id" = "$previous_boot_id" ] ; }; do
        sleep 5;
        timeout=$((timeout - 1))
        current_boot_id="$(get_boot_id)"
    done
    [ $timeout -ge 0 ] || {
        echo "Timed out waiting for instance to reboot" >&2
        exit 1
    }
    wait_booted
    echo "Reboot successful (current boot id: $current_boot_id)"
}

wait_booted() {
    # find await-boot script
    for script in $(dirname "$(dirname "$0")")/lib/in-testbed/await-boot.sh \
                  /usr/share/autopkgtest/lib/in-testbed/await-boot.sh; do
        if [ -r "$script" ]; then
            await_boot=$script
            break
        fi
    done

    if ! timeout 600 retry -d 1 -- timeout 5 "$COMMAND" exec -n "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- true 2>/dev/null; then
        echo "Timeout waiting for instance to start" >&2
        exit 1
    fi

    # shellcheck disable=SC2002
    if ! cat "$await_boot" | timeout 900 "$COMMAND" exec "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- sh -s; then
        echo "Error or timeout waiting for instance to boot" >&2
        exit 1
    fi
}

setup() {
    # set up apt proxy for the instance
    if [ -n "$AUTOPKGTEST_APT_PROXY" ] && [ "$AUTOPKGTEST_APT_PROXY" != "none" ]; then
        echo "Acquire::http::Proxy \"$AUTOPKGTEST_APT_PROXY\";" | "$COMMAND" file push - "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE/etc/apt/apt.conf.d/01proxy"
        # work around LP#1548878
        "$COMMAND" exec -n "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- chmod 644 /etc/apt/apt.conf.d/01proxy
    fi

    sleep 5
    if "$COMMAND" exec -n "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- systemctl mask serial-getty@getty.service; then
        safe_reboot
    fi

    # shellcheck disable=SC2016
    DISTRO=$("$COMMAND" exec -n "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- sh -ec 'lsb_release -si 2>/dev/null || . /etc/os-release; echo "${NAME% *}"')
    # shellcheck disable=SC2016
    CRELEASE=$("$COMMAND" exec -n "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- sh -ec 'lsb_release -sc 2>/dev/null || awk "/^deb/ {sub(/\\[.*\\]/, \"\", \$0); print \$3; exit}" /etc/apt/sources.list')
    echo "Instance finished booting. Distribution $DISTRO, release $CRELEASE"
    RELEASE=${RELEASE:-${CRELEASE}}

    if [ -z "${AUTOPKGTEST_KEEP_APT_SOURCES:-}" ] && [ -n "${AUTOPKGTEST_APT_SOURCES_FILE:-}" ]; then
        # Transfer the apt sources from the host system to the environment
        AUTOPKGTEST_APT_SOURCES="$(cat "$AUTOPKGTEST_APT_SOURCES_FILE")"
        unset AUTOPKGTEST_APT_SOURCES_FILE
    fi

    # Workaround for LP: #1989603, which affects images that come with snapd installed (Ubuntu).
    "$COMMAND" exec -n "$AUTOPKGTEST_TEMP_INSTANCE" -- umount /var/snap/lxd/common/var/lib/lxcfs || true

    # find setup-testbed script
    for script in $(dirname "$(dirname "$0")")/setup-commands/setup-testbed \
                  /usr/share/autopkgtest/setup-commands/setup-testbed; do
        if [ -r "$script" ]; then
            echo "Running setup script $script..."
            # shellcheck disable=SC2002
            cat "$script" | "$COMMAND" exec "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- env \
                AUTOPKGTEST_KEEP_APT_SOURCES="${AUTOPKGTEST_KEEP_APT_SOURCES:-}" \
                AUTOPKGTEST_APT_SOURCES="${AUTOPKGTEST_APT_SOURCES:-}" \
                MIRROR="${MIRROR:-}" \
                RELEASE="${RELEASE}" \
                sh -s
            break
        fi
    done

    # The base image should not have a populated machine-id
    "$COMMAND" exec -n "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" -- truncate -s0 /etc/machine-id

    "$COMMAND" stop "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE"
}

print_usage() {
    echo "Usage: $0 [--vm] [--lxd|--incus] [--remote REMOTE] <image>" >&2
}

#
# main
#

short_opts="hr:"
long_opts="help,vm,lxd,incus,remote:"
# shellcheck disable=SC2046
eval set -- $(getopt --name "${0##*/}" --options "${short_opts}" --long "${long_opts}" -- "$@") || { print_usage; exit 1; }
unset short_opts long_opt

COMMAND=

while true; do
    case "$1" in
        -h|--help)
            print_usage
            exit
        ;;
        --vm)
            USE_VM=true
            shift
            continue
        ;;
        --lxd)
            COMMAND=lxc
            shift
            continue
        ;;
        --incus)
            COMMAND=incus
            shift
            continue
        ;;
        -r|--remote)
            REMOTE=$2
            shift 2
            continue
        ;;
        --)
            shift
            break
        ;;
    esac
done

if [ -z "${1:-}" ] || [ -z "$COMMAND" ]; then
    print_usage
    exit 1
fi

IMAGE="$1"

# Extract the image architecture from the image properties YAML.
# Do some simple parsing with awk to not add a dependency on yq(1).
ARCH=$("$COMMAND" image show "$IMAGE" ${USE_VM:+--vm} | awk '/^properties:/ {in_prop=1; next} in_prop && $1=="architecture:" {print $2; exit}')

# generate a random temp instance name if one has not been
# specified from the environment.
if [ -z "${AUTOPKGTEST_TEMP_INSTANCE-}" ]; then
    generate_instance_name
fi

if [ -n "${REMOTE:-}" ]; then
    echo "Using remote ${REMOTE}"
fi

trap '[ -z "$("$COMMAND" ls "${REMOTE:+$REMOTE:}" "^${AUTOPKGTEST_TEMP_INSTANCE}\$" --format=csv)" ] || retry -t 5 -d 5 -- "$COMMAND" delete -f "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE"' EXIT INT QUIT PIPE HUP TERM

# make sure the instance does not exist
if [ "$("$COMMAND" ls "${REMOTE:+$REMOTE:}" "^${AUTOPKGTEST_TEMP_INSTANCE}\$" --format=csv)" ]; then
    retry -t 5 -d 5 -- "$COMMAND" delete --force "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE"
fi

proxy_detect

# we must redirect stdin when calling lxc/incus init due to lp #1845037
"$COMMAND" init "$IMAGE" "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" ${USE_VM:+--vm} < /dev/null

# Install lxd-agent in older Ubuntu VM images, see:
# https://documentation.ubuntu.com/lxd/latest/reference/devices_disk/#vm-cloud-init-config
if [ "$COMMAND" = lxc ] && [ "${USE_VM-}" ]; then
    case "$("$COMMAND" image show "$IMAGE")" in
        *"ubuntu 20.04"*)
            "$COMMAND" config device add "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" config disk source=cloud-init:config
            "$COMMAND" config set "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" cloud-init.user-data - <<EOF
#cloud-config
datasource_list: [ LXD ]
EOF
            ;;
        *"ubuntu 18.04"*)
            "$COMMAND" config device add "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" config disk source=cloud-init:config
            "$COMMAND" config set "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" cloud-init.user-data - <<EOF
#cloud-config
datasource_list: [ LXD ]
runcmd:
  - mount -t 9p config /mnt
  - cd /mnt
  - ./install.sh
  - cd /
  - umount /mnt
  - systemctl start lxd-agent
EOF
            if [ "$ARCH" = "amd64" ]; then
                "$COMMAND" config set "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" boot.mode=bios
            fi
            ;;
        *"ubuntu 16.04"*)
            "$COMMAND" config device add "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" config disk source=cloud-init:config
            "$COMMAND" config set "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" cloud-init.user-data - <<EOF
#cloud-config
datasource_list: [ LXD ]
apt:
  http_proxy: ${AUTOPKGTEST_APT_PROXY:-DIRECT}
packages:
  - linux-image-virtual-hwe-16.04
runcmd:
  # s390x quirk: upgrading the hwe kernel breaks the netcork configuration; keep the initial one
  - '[ "$ARCH" = "s390x" ] && echo "network: {config: disabled}" > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg'
  # drop proxy settings configured by cloud-init; rely on 01proxy as configured by this script
  - rm -f /etc/apt/apt.conf.d/90cloud-init-aptproxy
  # install lxd-agent as normal
  - mount -t 9p config /mnt
  - cd /mnt
  - ./install.sh
  - cd /
  - umount /mnt
  - systemctl start lxd-agent
EOF
            if [ "$ARCH" = "amd64" ]; then
                "$COMMAND" config set "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" boot.mode=bios
            fi
            ;;
    esac
fi

"$COMMAND" start "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE"
wait_booted
setup

# if there already is a published image, get its fingerprint to clean it up
# afterwards
DISTROLC=$(echo "$DISTRO" | tr '[:upper:]' '[:lower:]')
ALIAS="autopkgtest/$DISTROLC/$RELEASE/$ARCH${USE_VM:+/vm}"
DESCRIPTION="autopkgtest $DISTRO $RELEASE $ARCH"
if [ -n "${USE_VM:-}" ]; then
    TYPE=virtual-machine
else
    TYPE=container
fi

REUSE=""
if [ "$COMMAND" = incus ] || dpkg --compare-versions "$(lxc version | awk '/Client version:/{print$3}')" ge 5.14; then
    REUSE="--reuse"
fi
"$COMMAND" publish "${REMOTE:+$REMOTE:}$AUTOPKGTEST_TEMP_INSTANCE" "${REMOTE:+$REMOTE:}" $REUSE --alias "$ALIAS" --public description="$DESCRIPTION" distribution="$DISTROLC" release="$RELEASE" architecture="$ARCH"

# clean up old images
# columns: l=shortest image alias, F=long fingerprint
for i in $("$COMMAND" image list "${REMOTE:+$REMOTE:}" --columns=lF --format=csv description="$DESCRIPTION" type="$TYPE" | grep -v "^$ALIAS," | cut -d, -f2); do
    echo "Removing previous image $i"
    "$COMMAND" image delete "${REMOTE:+$REMOTE:}$i"
done
