#!/usr/bin/env bash
# Copyright 2012 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

# Exit immediately if a command exits with a non-zero status.
set -o errexit
# Treat unset variables as an error when substituting.
set -o nounset

# This should mirror what's in odev's setup.py, except here x86_64 is called
# amd64, not x86_64 because that is Ubuntu's selected name for the arch.
GUEST_ARCHITECTURE=$(uname -m)
case "$GUEST_ARCHITECTURE" in
    i?86) GUEST_ARCHITECTURE="i386" ;;
    x86_64) GUEST_ARCHITECTURE="amd64" ;;
esac

GUEST_RELEASE=${ZIMMER_GUEST_RELEASE:-quantal}
DEF_ZIMG="http://cloud-images.ubuntu.com/server/${GUEST_RELEASE}/current/${GUEST_RELEASE}-server-cloudimg-${GUEST_ARCHITECTURE}-disk1.img"
DEF_SAVE_D="pristine"
DEF_UD_FILE="ud-build.txt"
ZIMMER_SSH_FORWARD=${ZIMMER_SSH_FORWARD:-""} # hostfwd=tcp::2222-:22
ZIMMER_MEM="${ZIMMER_MEM:-1024}"
KVM_PID=""
TAIL_PID=""
LOG="output.log"

case $(uname -m) in
    i?86) DEF_ZIMG=${DEF_ZIMG//amd64/i386};;
esac

VERBOSITY=0
TEMP_D=""

error() { echo "$@" 1>&2; }
errorp() { printf "$@" 1>&2; }
fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
failp() { [ $# -eq 0 ] || errorp "$@"; exit 1; }

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

   build a zimmer server from a cloud image, and put it in 'output'

   options:
     --zimg   Z          url or path to compressed cloud image
                         (will be uncompressed)
                         def: $DEF_ZIMG
     --img    I          url or path to uncompressed cloud image
                         expected to be uncompressed.
                         default: create from zimg
     --log    L          log items to LOG
                         default: $LOG
     --save   D          put pristine copies of things in D
                         default: $DEF_SAVE_D
     --ud-file F         use user-data file F
                         default: $DEF_UD_FILE
     --import-keys K     import ssh keys
                          values are 'auto', 'lp:<id>', or path to file
EOF
}

bad_Usage() { Usage 1>&2; [ $# -eq 0 ] || error "$@"; exit 1; }
cleanup() {
    [ -z "${TEMP_D}" -o ! -d "${TEMP_D}" ] || rm -Rf "${TEMP_D}"
    [ -z "$KVM_PID" ] || kill "$KVM_PID"
    [ -z "$TAIL_PID" ] || kill "$TAIL_PID"
}

log() {
    [ -n "$LOG" ] || return
    echo "$(date -R):" "$@" >> "$LOG"
}
debug() {
    local level=${1}; shift;
    log "$@"
    [ "${level}" -gt "${VERBOSITY}" ] && return
    error "${@}"
}

# Download image file.
# Parameters: source URL, filename to save to
download() {
    local src="$1" dest="$2"

    debug 0 "downloading $src to $dest"
    wget --progress=dot:mega "$src" -O "$dest.partial" &&
        mv -- "$dest.partial" "$dest" ||
        fail "failed to get $src"
}

short_opts="ho:v"
long_opts="help,img:,import-keys:,log:,ud-file:,verbose,zimg:"
getopt_out=$(getopt --name "${0##*/}" \
    --options "${short_opts}" --long "${long_opts}" -- "$@") &&
    eval set -- "${getopt_out}" ||
    bad_Usage

img=""
zimg=""
save_d="$DEF_SAVE_D"
ud_file="$DEF_UD_FILE"
import_keys=""

while [ $# -ne 0 ]; do
    cur=${1}; next=${2};
    case "$cur" in
        -h|--help) Usage ; exit 0;;
           --img) img=${2}; shift;;
           --log) LOG=${2}; shift;;
           --save) save_d=${2}; shift;;
           --ud-file) ud_file=${2}; shift;;
        -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
           --zimg) zimg=${2}; shift;;
           --import-keys) import_keys=${2}; shift;;
        --) shift; break;;
    esac
    shift;
done

## check arguments here
## how many args do you expect?
[ $# -gt 1 ] && bad_Usage "too many arguments"
[ $# -eq 0 ] && bad_Usage "need an output argument"
output="$1"
[ "${output%.zimg}" = "${output}" ] || fail "do not name output with .zimg"

command -v genisoimage >/dev/null ||
    fail "you do not have genisoimage installed. install genisoimage package"

: > "$LOG"

[ -f "$ud_file" ] ||
    fail "user data file $ud_file" is not a file

TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") ||
    fail "failed to make tempdir"
trap cleanup EXIT

mkdir -p "$save_d" || fail "failed to mkdir $save_d"

# if --import-keys was specified, get the keys into a local file
keyf="$TEMP_D/keys"
if [ "$import_keys" = "auto" ]; then
    ssh-add -L > "${keyf}" 2>/dev/null ||
        cat $HOME/.ssh/id*.pub > "$keyf" 2>/dev/null ||
        error "Warning: unable to find 'auto' keys"
elif [ -f "$import_keys" ]; then
    cat "$import_keys" > "$keyf"
elif [ "${import_keys#lp:}" != "${import_keys}" ]; then
    ssh-import-id -o - ${import_keys#lp:} > "$keyf" 2>/dev/null ||
        error "Warning: failed to ssh-import ${import_keys#lp:}"
fi

if [ -n "$img" ]; then
    # if img was given, then we assume good, its the backing image
    [ -f "$img" ] || fail "$img (--img) is not a file"
    debug 0 "using $img as uncompressed image"
else
    if [ -z "$zimg" ]; then
        zimg="$DEF_ZIMG"
    fi
    case "$zimg" in
        http://*|https://*)
            o_zimg="${zimg}"
            zimg=${save_d}/$(basename "$o_zimg" ".img").zimg
            [ -f "$zimg" ] &&
                fail "please delete $zimg first or use --zimg|--img"
            download "$o_zimg" "$zimg"
            ;;
        file://*)
            o_zimg=${zimg}
            zimg=${zimg#file://}
            debug 0 "using file $o_zimg as zimg"
            [ -f "$zimg" ] || fail "$zimg is not a file"
            ;;
        *)  [ -f "$zimg" ] || fail "$zimg is not a file"
            debug 0 "using file $zimg as zimg"
            ;;
    esac
    img=${zimg%.zimg}.img
    debug 0 "creating uncompressed img $img from $zimg"
    qemu-img convert -O qcow2 "$zimg" "$img"
    qemu-img resize "$img" 4G
fi

debug 0 "making nocloud data source in iso"
seed_d="$TEMP_D/seed"
mkdir "$seed_d" || fail "failed to make 'seed' in tempdir"

cp "$ud_file" "$seed_d/user-data" || fail "failed to copy $ud_file to $seed_d"
cat > "$seed_d/meta-data" <<EOF
instance-id: i-zimmer-build
local-hostname: zimmer-build
EOF

# if keys were specified, dump them into meta-data
if [ -s "$keyf" ]; then
    {
    echo "public-keys:"
    echo " zimmer-build:"
    while read line; do
        echo "  - \"$line\""
    done < "$keyf"
    } >> "$seed_d/meta-data"
fi

( cd "$seed_d" &&
    genisoimage -output "$TEMP_D/build.iso" \
        -volid cidata -joliet -rock user-data meta-data 2>/dev/null ) ||
    fail "failed to create iso for user-data from $ud_file"

build0="$TEMP_D/build0.img"
img_fp=$(readlink -f "$img") || fail "failed to get fullpath for $img"
qemu-img create -f qcow2 -b "$img_fp" "${build0}" ||
    fail "failed to create qcow image backed by $img"

## on release newer than oneiric, do not give 'boot=on' in kvm cmdline
[ "$(lsb_release -sc)" ">" "oneiric" ] && bton="" || bton="boot=on"

serial_out="$TEMP_D/serial.output"
monitor="${TEMP_D}/monitor.fifo" && mkfifo "$monitor" ||
    fail "failed to mkfifo for monitor"

debug 0 "booting kvm guest to turn cloud-image into zimmer"
kvm_start=$SECONDS
MONITOR="-monitor null"
NOGRAPHIC="-nographic"
kvm \
    -drive file=${build0},if=virtio,cache=unsafe${bton:+,${bton}} \
    -boot c -cdrom "$TEMP_D/build.iso" \
    -net nic,model=virtio \
    -net user${ZIMMER_SSH_FORWARD:+,${ZIMMER_SSH_FORWARD}} \
    -m "${ZIMMER_MEM}" \
    $NOGRAPHIC \
    $MONITOR \
    -serial "file:$serial_out" \
    2>&1 &

KVM_PID=$!
tail -F "$serial_out" 2>/dev/null &
TAIL_PID=$!

sleep 20
[ -s "$serial_out" ] ||
    fail "no output in serial console output after 20 seconds"

wait $KVM_PID
ret=$?
KVM_PID=""

{ kill $TAIL_PID ; } >/dev/null 2>&1
TAIL_PID=""

{
    echo ===== begin serial console ====
    cat "$serial_out"
    echo ===== end serial console ====
} >> "$LOG"
[ $ret -eq 0 ] || fail "failed to build via kvm guest"
grep -q "ZIMMER BUILD FINISHED" "$serial_out" ||
    fail "did not find finished message in $serial_out"

debug 0 "kvm image built in $(($SECONDS-$kvm_start))s"
debug 0 "creating dist image in $output"
## create a re-shrunk image of build0.img into 'zimmer-disk0.img.dist'
[ ! -f "$output" ] || rm -f "$output" ||
    fail "failed to remove existing $output"
qemu-img convert -O qcow2 "$TEMP_D/build0.img" "$output" &&
    chmod 444 "$output" ||
    fail "failed to create $output from build0.img"

debug 0 "creating pristine compressed zimmer-disk0.zimg"
## optionally create a zip'd image for transmission
qemu-img convert -f qcow2 -O qcow2 -c "$output" "${output%.img}.zimg"

debug 0 "done. took $SECONDS seconds"
