#!/bin/bash
# This file is part of curtin. See LICENSE file for copyright and license info.

set -o pipefail

VERBOSITY=1
CONTAINER=""

error() { echo "$@" 1>&2; }
fail() { local r=$?;  [ $r -eq 0 ] && r=1; failrc "$r" "$@"; }
failrc() { local r=$1; shift; [ $# -eq 0 ] || error "$@"; exit $r; }

Usage() {
    cat <<EOF
Usage: ${0##*/} [ options ] <image> name

   start a container of image (ubuntu-daily:xenial) and install curtin.

   options:
      --no-patch-version  do not patch source dir/curtin/version.py
                          by default, --source will have version.py updated
                          with dpkg's version.
      --proposed          enable proposed
      --daily             enable daily curtin archive
      --source   D        grab the source deb, unpack inside, and copy unpacked
                          source out to 'D'
EOF
}

bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; return 1; }
cleanup() {
    if [ -n "$CONTAINER" ]; then
        debug 1 "deleting container $CONTAINER"
        lxc delete --force "$CONTAINER"
    fi
}

inside() {
    local n="$1" close_in=true
    shift
    [ "$1" = "-" ] && { close_in=false; shift; }
    set -- lxc exec --mode=non-interactive "$n" -- "$@"
    if ${close_in}; then
        debug 1 "$* </dev/null"
        "$@" </dev/null
    else
        debug 1 "$*"
        "$@"
    fi
}

install() {
    local name="$1"
    shift
    inside "$name" $eatmydata \
        env DEBIAN_FRONTEND=noninteractive \
        apt-get install --no-install-recommends -qy "$@" || {
            error "failed apt-get install --no-install-recommends -qy $*"
            return 1
        }
}

wait_for_ready() {
    local n="$1" max="${2:-30}" debug=${3}
    inside "$n" - /bin/sh -s $max $debug <<"EOF"
max=$1; debug=${2:-0};
i=0;
while [ ! -e /run/cloud-init/result.json ] && i=$(($i+1)); do
    [ $i -ge $max ] && exit 1
    [ "$debug" = "0" ] || echo -n .
    sleep 1
done
[ "$debug" = "0" ] || echo "[done after $i]"
exit 0
}
EOF
}

debug() {
    local level=${1}; shift;
    [ "${level}" -gt "${VERBOSITY}" ] && return
    error "${@}"
}

get_source() {
    local target="$1" pkg="$2" ver="$3"
    local tmpd="" x=""
    tmpd=$(mktemp -d) || fail "failed to mktemp"
    mkdir "$tmpd/extract"
    cd "$tmpd/extract"
    debug 1 "Inside. getting source for $pkg${ver:+=${ver}}"
    if ! apt-get source "${pkg}${ver:+=${ver}}"; then
        [ -n "$ver" ] || fail "Failed to get source for $pkg"
        # Getting the specific version failed.
        # Assume 'pkg' is a binary package and source package.
        # Ask apt for the url to the binary, and assume source in same dir.
        debug 1 "Failed to apt-get source ${pkg}=$ver. Trying workaround."
        url=$(apt-get -qq download --print-uris "$pkg=${ver}" |
            awk '{ gsub(/'\''/, ""); print $1}')
        local dsc_url="${url%_*.deb}.dsc"
        debug 1 "Binary package came from $url."
        debug 1 "Trying dsc from $dsc_url"
        dget --allow-unauthenticated "$dsc_url" || fail "Failed dget $dsc_url"
    fi

    # dget or apt-get source of pkg/ver produces pkg-<upstream-ver>/
    x="${pkg}-${ver%-*}"
    [ -d "$x" ] || {
        error "getting source for '$pkg/$ver' did not produce directory '$x'"
        error "ls -l:"
        ls -l 1>&2
        fail
    }
    cp -a "$x" "$target" || fail "failed copying $x to $target"
    rm -Rf "$tmpd"
}

main() {
    local short_opts="hv"
    local long_opts="help,daily,no-patch-version,proposed,source:,verbose"
    local getopt_out=""
    getopt_out=$(getopt --name "${0##*/}" \
        --options "${short_opts}" --long "${long_opts}" -- "$@") &&
        eval set -- "${getopt_out}" ||
        { bad_Usage; return; }

    local cur="" next=""
    local proposed=false daily=false src="" name="" maxwait=30
    local eatmydata="eatmydata" getsource="none" patch_version=true

    while [ $# -ne 0 ]; do
        cur="$1"; next="$2";
        case "$cur" in
            -h|--help) Usage ; exit 0;;
               --source) getsource="$next"; shift;;
               --no-patch-version) patch_version=false;;
               --proposed) proposed=true;;
               --daily) daily=true;;
            -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
            --) shift; break;;
        esac
        shift;
    done

    [ $# -eq 2 ] || { bad_Usage "expected 2 args, got $#: $*"; return; }

    trap cleanup EXIT
    src="$1"
    name="$2"

    if [ "$getsource" != "none" ]; then
        [ ! -e "$getsource" ] || fail "source output '$getsource' exists."
    fi
    getsource="${getsource%/}"

    # launch container; mask snapd.seeded.service; not needed
    {
        lxc init "$src" "$name" &&
        lxc file push \
            /dev/null ${name}/etc/systemd/system/snapd.seeded.service &&
        lxc start ${name}
    } || fail "failed lxc launch $src $name"
    CONTAINER=$name

    wait_for_ready "$name" $maxwait $VERBOSITY ||
        fail "$name did not become ready after $maxwait"

    inside "$name" which eatmydata >/dev/null || eatmydata=""
    release=$(inside $name lsb_release -sc) ||
        fail "$name did not have a lsb release codename"

    # curtin depends on zfsutils-linux via probert-storage, but zfsutils-linux
    # can't be installed in an unprivileged container as it fails to start
    # the zfs-mount and zfs-share services as /dev/zfs is missing. We do
    # not actually need ZFS to work in the container, so the problem can be
    # worked around by masking the services before the package is installed.
    inside "$name" systemctl mask zfs-mount || fail "failed to mask zfs-mount"
    inside "$name" systemctl mask zfs-share || fail "failed to mask zfs-share"

    if $proposed; then
        mirror=$(inside $name awk '$1 == "deb" { print $2; exit(0); }' \
            /etc/apt/sources.list) ||
            fail "failed to get mirror in $name"
        line="$mirror $release-proposed main universe"
        local fname="/etc/apt/sources.list.d/proposed.list"
        debug 1 "enabling proposed in $fname: deb $line"
        inside "$name" sh -c "echo deb $line > $fname" ||
            fail "failed adding proposed to $fname"
        if [ "$getsource" != "none" ]; then
            inside "$name" sh -c "echo deb-src $line >> $fname" ||
                fail "failed adding proposed deb-src to $fname"
        fi
    fi
    if $daily; then
        local daily_ppa="ppa:curtin-dev/daily"
        debug 1 "enabling daily: $daily_ppa"
        local addaptrepo="add-apt-repository"
        inside "$name" which $addaptrepo >/dev/null || addaptrepo=""
        if [ -n "${addaptrepo}" ]; then
            inside "$name" ${addaptrepo} --enable-source --yes --no-update \
                "${daily_ppa}" ||
                fail "failed add-apt-repository for daily."
        else
            # https://launchpad.net/~curtin-dev/+archive/ubuntu/daily
            local url="http://ppa.launchpad.net/curtin-dev/daily/ubuntu"
            local lfile="/etc/apt/sources.list.d/curtin-daily-ppa.list"
            local kfile="/etc/apt/trusted.gpg.d/curtin-daily-ppa.gpg"
            local key="0x1bc30f715a3b861247a81a5e55fe7c8c0165013e"
            local keyserver="keyserver.ubuntu.com"
            local keyurl="https://${keyserver}/pks/lookup?op=get&search=${key}"
            inside "$name" sh -c "
                echo deb $url $release main > $lfile &&
                wget -q \"$keyurl\" -O - | gpg --dearmour --output $kfile" ||
                fail "failed to add $daily_ppa repository manually"
            if [ "$getsource" != "none" ]; then
                inside "$name" sh -c "
                    echo deb-src $url $release main >> $lfile" ||
                    fail "failed adding daily ppa deb-src to $lfile"
            fi
        fi
    fi

    line="Acquire::Languages \"none\";"
    fname="/etc/apt/apt.conf.d/99notranslations"
    inside "$name" sh -c '
        rm -f /var/lib/apt/lists/*Translation*;
        echo "$1" > "$2"' -- "$line" "$fname" ||
        error "failed to disable translations"

    pkgs="curtin"
    if [ "${getsource}" = "none" ]; then
        inside "$name" sed -i '/^deb-src/s/^/#/' /etc/apt/sources.list ||
            error "failed to disable deb-src entries"
    else
        pkgs="${pkgs} dpkg-dev devscripts"
    fi

    inside "$name" $eatmydata apt-get -q update ||
        fail "failed apt-get update"
    install "$name" $pkgs || fail "failed install of $pkgs"
    local pkg_ver="" src_ver=""
    pkg_ver=$(inside "$name" \
        dpkg-query --show --showformat='${Version}\n' curtin)
    debug 1 "installed curtin at $pkg_ver"

    if [ "${getsource}" != "none" ]; then
        local isrcd="/tmp/curtin-source"
        debug 1 "getting source for curtin at $pkg_ver to $getsource"
        inside "$name" - $eatmydata /bin/bash -s \
            get_source "$isrcd" "curtin" "$pkg_ver" < "$0" ||
            fail "Failed getting source in $name"
        mkdir "$getsource" || fail "failed to create dir '$getsource'"
        inside "$name" tar -C "$isrcd" -cf - . |
            tar -C "$getsource" -xf - ||
            fail "failed to copy source out to $getsource"
        # 14.04 cannot take --file=<file>. Has to be 2 arguments.
        src_ver=$(inside "$name" dpkg-parsechangelog \
            "--file" "$isrcd/debian/changelog" "--show-field=version")
        if [ "$src_ver" != "$pkg_ver" ]; then
            fail "source version ($src_ver) != package version ($pkg_ver)"
        fi
        if "${patch_version}"; then
            local verfile="$getsource/curtin/version.py"
            grep -q "@@PACKAGED_VERSION@@" "$verfile" ||
                fail "failed patching version: " \
                    "@@PACKAGED_VERSION@@ not found in $verfile"
            sed -i.curtainer-dist \
                "s,@@PACKAGED_VERSION@@,${pkg_ver}," "$verfile" ||
                fail "failed modifying $verfile"
            debug 1 "patched $verfile pkg version to $pkg_ver."
        fi
        inside "$name" rm -Rf "$isrcd" ||
            fail "failed removal of extract dir"
        debug 1 "put source for curtin at $src_ver in $getsource"
    fi

    CONTAINER=""
}


if [ "$1" = "get_source" ]; then
    shift
    get_source "$@"
else
    main "$@"
fi

# vi: ts=4 expandtab syntax=sh
