#!/bin/bash
# Copyright 2015 Canonical, Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

PROG=`basename $0`
PREFIX="${PROG}"
INSTALLER_IMAGE_SIZE_M=2048  # 2G image
RSYNC_OPTS="-aXHAS --one-file-system"
MKFS_OPTS="-E packed_meta_blocks=1 -J size=4 -m 0 -q"
LOGFILE="geninstaller.log"
TOPDIR=./
USQUERY=installer/usquery
RESOURCES=installer/resources
PKG_DEPS="
qemu-utils
kpartx
parted
gdisk
extlinux
simplestreams
syslinux-common
grub2-common
shim
shim-signed
grub-efi-amd64-signed
probert
"
# "URL" "GPGKEY"
# ppa:foo/bar ""
PPAS=(
  "https://raharper:Q9F9bRlSxg70BGv8m6dc@private-ppa.launchpad.net/subiquity/subiquity-dev/ubuntu" "3D2F6C3B"
)
SRC_DEPS=(
  "bzr" "lp:curtin" "curtin"
)
INSTALLER_DEPS=(
  "bcache-tools"
  "lvm2"
  "mdadm"
  "petname"
  "python3-urwid"
  "python3-pyudev"
  "python-urwid"
  "python3-tornado"
  "probert"
  "xfsprogs"
)
CACHEDIR=""
GRUB_MODS="configfile fat part_gpt part_msdos cat echo test search search_label search_fs_uuid boot chain linux reboot halt normal efi_gop efi_uga font gfxterm gfxterm_menu gfxterm_background gfxmenu serial"


cleanup_noexit() {
    [ -n "${CACHEDIR}" ] && {
        sync
        sudo umount -l ${CACHEDIR}/mnt/{dev,proc,sys}
        sudo umount -l ${CACHEDIR}/mnt
        sudo umount -l ${CACHEDIR}/lower
        sudo umount -l ${CACHEDIR}/upper
        sudo umount -l ${CACHEDIR}/efimnt
        sudo kpartx -d ${CACHEDIR}/installer.img &>/dev/null || exit
        for DEV in $EFI_DEV $ROOTFS_DEV $OVERLAY_DEV; do
            [ -e "/dev/mapper/`basename $DEV`" ] && {
                sudo dmsetup remove $DEV &>/dev/null || exit
            }
        done
        # it's ok to fail here, it means kpartx did it for us
        sudo losetup -d $LOOPDEV &>/dev/null | :
    }
}

cleanup() {
    cleanup_noexit &>/dev/null
    exit
}

trap cleanup EXIT HUP INT TERM

log() {
    echo "`date +%s`: $@" | tee -a ${LOGFILE}
}

blockalign_up() {
    local block_size=$((1 << 20))  # 1M
    local new_size=$(($1 + ($block_size - ($1 % $block_size))))
    echo $(($new_size >> 20))  # return in MB
}

write_metadata() {
    cat <<EOF
instance-id: 'inst-${RANDOM}'
local-hostname: ubuntu-server-installer
EOF
}

userdata_write_file() {
    local path=${1}; shift;
    local owner=${1}; shift;
    local permissions=${1}; shift;
    local encoding=${1}; shift;
    case $encoding in
        none)
            echo "-   content: |"
            for x in "$@"; do
                sed 's,^,        ,' "$x" || return
            done
            ;;
        b64)
            echo "-   encoding: $encoding"
            echo "    content: |"
            base64 "$@" | sed 's,^,        ,'
            ;;
        *)
            log "ERROR: unsupported encoding $encoding"
            return 1;
            ;;
    esac
    echo "    path: $path"
    echo "    owner: $owner"
    echo "    permissions: '$permissions'"
}

usage() {
    cat << EOF

usage: $PROG [PARAMS] [ARGS]
 -a, --arch=ARCH                For ARCH in [i386, amd64, ppc64el, armf, arm64]
 -b, --bootloader=TYPE          For TYPE in [syslinux, grub2, uboot]
 -h, --help                     This output.
 -r, --release=RELEASE          For RELEASE in  [trusty, utopic, vivid, wily]
 -s, --stream=STREAM            For STREAM in [daily, released]
 -V, --version=VERSION          VERSION=YYYYMMDD , 20150623
 -v, --verbose


Example usage:
  # generate an grub2-based install image for wily on amd64
  $PROG --arch=amd64 --release=wily --bootloader=grub2

EOF
}

install_ppas() {
    while [ $# -gt 0 ]; do
        local url=${1}; shift
        local gpgkey=${1}; shift;
        if [ -n ${gpgkey} ]; then
            echo sudo apt-key adv --keyserver keyserver.ubuntu.com --recv ${gpgkey}
        fi
        echo sudo apt-add-repository -y ${url}
    done
    return 0
}

remove_ppas() {
    while [ $# -gt 0 ]; do
        local url=${1}; shift
        local gpgkey=${1}; shift;

        if [ -n ${gpgkey} ]; then
            echo sudo apt-key del ${gpgkey}
        fi
        echo sudo apt-add-repository --remove ${url}
    done
    return 0
}

install_deps() {
    local packages="$1"
    local to_install=""

    log "Checking for build package dependencies"
    for p in $packages; do
        if ! dpkg-query -s "$p" &>/dev/null; then
            to_install="$to_install $p";
        fi
    done

    [ -n "$to_install" ] && {
        log "Installing dependencies: $to_install";
        sudo apt-get install -q -y $to_install || {
            log "Failed to install one or more dependencies in $to_install";
            return 1;
        }
   }
   return 0
}

install_src() {
    local dldir=${1}; shift;
    mkdir -p ${dldir} || {
        log "ERROR: failed to mkdir $dldir";
        return 1;
    }

    log "Acquiring src packages..."
    while [ $# -gt 0 ]; do
        local proto=${1}; shift
        local url=${1}; shift
        local localdir=${1}; shift

        [ -z "${proto}" -o -z "${url}" ] && {
            log "ERROR installing source with args: $@"
            return 1;
        }

        local target="$dldir/$localdir"
        case "$proto" in
            git)
                cmd="git clone $url $target";;
            bzr)
                cmd="bzr branch $url $target";;
            *)
                log "ERROR: unsupported src protocol: $proto $url";;
        esac

        if [ ! -d "${target}" ]; then
            log "Acquiring src @ $url with $proto"
            $cmd || {
                log "ERROR: failed to fetch src: $proto $url into $target";
                return 1;
            }
        else
            log "  Using cached src for repo $url @ $target"
        fi
    done
}

acquire_image() {
    _RETVAL=""
    [ $# -lt 1 ] && {
        log "ERROR: not enough arguments passed to $FUNCNAME";
        return 1;
    }

    # what to get from maas
    local item_name="root-image.gz"
    local dldir=${1}; shift;

    local label=${1:-"daily"}
    local release=${2-"wily"};
    local arch=${3-"amd64"};
    local version=${4};

    # run a query unless they specify all params
    if [ $# -le 4 -a -z "${version}" ]; then
        log "Querying simplestreams for latest image: $label $release $arch"
        case "$label" in
            daily)
             ssresult=( `${USQUERY} --output-format="%(version_name)s %(item_url)s %(sha256)s" --max=1 maas-daily release=$release arch=$arch item_name=$item_name` )
             local version=${ssresult[0]}
             local item_url=${ssresult[1]}
             local sha256=${ssresult[2]}
             ;;
           release)
             ssresult=( `${USQUERY} --output-format="%(version_name)s %(item_url)s %(sha256)s" --max=1 maas-release release=$release arch=$arch item_name=$item_name` )
             local version=${ssresult[0]}
             local item_url=${ssresult[1]}
             local sha256=${ssresult[2]}
             ;;
           *)
             log "ERROR: simplestream label must be one of: [daily, release]"
             return 1;
             ;;
        esac
    fi
    local cachedir=$dldir/maas/${label}/${release}/${arch}/${version}
    local ephimg=${cachedir}/${item_name}
    local roottar=${cachedir}/root.tar.gz
    local rootfs=${cachedir}/rootfs

    # cache policy:
    #   $ephimg must be a file and it must checksum to $sha256 value otherwise
    #   we will nuke the cachedir and reacquire $item_name @ $item_url
    #   and re-assemble roottar and rootfs which was based on ephimg

    [ -r "$ephimg" ] && CACHE_SUM=( `sha256sum $ephimg 2>/dev/null` )
    if [ -z "${sha256}" -a -f "${ephimg}.sha256" ]; then
        log "Using sha256sum from cached file";
        sha256=`cat ${ephimg}.sha256 | cut -d' ' -f1`
    fi
    if [ "${sha256}" != "${CACHE_SUM[0]}" ]; then
        log "WARNING: sha256 csum mismatch"
        log "WARNING: expected: [$sha256]"
        log "            found: [${CACHE_SUM[0]}]"
        # didn't match so nuke the cache
        log "WARNING: removing old cache"
        sudo rm -fr "${cachedir}" || {
            log "ERROR: failed to remove stale cachedir: $cachedir";
            return 1;
        }
    fi

    # download the image if it's not in the cache
    log "Downloading installer root image @ $item_url"
    if [ ! -r "${ephimg}" ]; then
        mkdir -p $cachedir &&
        wget --progress=bar -c "${item_url}" -O "${ephimg}" || {
            log "ERROR: failed to download: ${item_url}";
            return 1;
        }
        (cd `dirname ${ephimg}` &&
         sha256sum `basename ${ephimg}` > ${ephimg}.sha256)
    else
        log "  Using cached $label $release $arch $item_name:"
        log "  $ephimg"
    fi

    # convert to root.tar.gz
    log "Converting mass ephemeral image to roottar"
    if [ ! -r ${roottar} ]; then
        $dldir/curtin/tools/maas2roottar $ephimg $roottar
        [ "$?" != "0" ] && {
            log "ERROR: Failed to convert ephemeral to roottar";
            return 1;
        }
    else
        log "  Using cached $label $release $arch root.tar.gz"
        log "  $roottar"
    fi

    # unpack rootfs tar
    log "Unpacking roottar: $label $release $arch";
    if [ ! -e ${rootfs}/vmlinuz ]; then
        mkdir -p ${rootfs} &&
        sudo tar -C $rootfs -xz --numeric-owner --xattrs -f $roottar
    else
        log "  Using cached $label $release $arch rootfs:"
        log "  $rootfs"
    fi

    _RETVAL=${cachedir}
}

generate_seed() {
    _RETVAL=""
    [ $# -lt 2 ] && {
        log "ERROR: not enough arguments passed to $FUNCNAME";
        return 1;
    }

    local dldir=${1};
    local cachedir=${2};
    local seed=$cachedir/seed/nocloud-net
    local installer_user_data=${RESOURCES}/user-data/installer-user-data

    # inject user-data/meta-data into seed
    log "Writing seed meta-data"
    mkdir -p ${seed} && write_metadata > $seed/meta-data || {
        log "Failed to write meta-data into $seed";
        return 1;
    }
    log "Writing seed user-data (curtin)"
    # remove the old seed; copy in the base template and
    # append the curtin-cmd file
    rm -f ${seed}/user-data &&
    cp $installer_user_data $seed/user-data &&

    if [ "${OFFLINE}" == "no" ]; then
        log "Enabling cloud-init package installation"
        local packages=""
        for pkg in ${INSTALLER_DEPS[@]}; do
            packages="$packages - $pkg\n"
        done
        sed -i "s/#packages/packages:\n$packages/" ${seed}/user-data
    fi

    log "Writing seed user-data (subiquity)"
    local subiquity_tar=$dldir/subiquity.tar
    local tar_cmd="tar -C ${TOPDIR} -cpf $subiquity_tar subiquity subiquitycore"
    if [[ ${TOPDIR} = /usr/share/subiquity* ]]; then
        log "Using installed subiquity paths"
        tar_cmd="$tar_cmd subiquity-tui"
    else
        log "Using source subiquity paths"
        tar_cmd="$tar_cmd bin"
    fi
    log "subiquity_tar cmd: ${tar_cmd}"
    $tar_cmd || {
        log "ERROR: Failed to package subiquity installer";
        return 1;
    }
    [ -e ${subiquity_tar} ] || {
        log "ERROR: failed to package subiquity installer";
        return 1
    }
    userdata_write_file "/tmp/subiquity.tar" \
                        "root:root" "0644" "b64" \
                        "$subiquity_tar" >> $seed/user-data || {
        log "Failed to subiquity into $seed";
        return 1;
    }
    return 0
}

disable_daemons_in_root() {
    local target=${1};
    [ -z "${target}" ] && {
        log "ERROR: $FUNCNAME was not passed a target"
        return 1;
    }

    log "Disabling deamons in target"
    fpath="${target}/usr/sbin/policy-rc.d"
    sudo tee $fpath << EOF
#!/bin/sh
# see invoke-rc.d for exit codes. 101 is "do not run"
while true; do
   case "\$1" in
      -*) shift;;
      makedev|x11-common) exit 0;;
      *) exit 101;;
   esac
done
EOF
    sudo chmod 0755 $fpath
    return 0
}

undisable_daemons_in_root() {
    local target=${1};
    [ -z "${target}" ] && {
        log "ERROR: $FUNCNAME was not passed a target"
        return 1;
    }

    log "Enabling daemons in target"
    fpath="${target}/usr/sbin/policy-rc.d"
    sudo rm -f $fpath
    return 0
}

generate_img() {
    _RETVAL=""
    [ $# -lt 3 ] && {
        log "ERROR: not enough arguments passed to $FUNCNAME";
        return 1;
    }

    local dldir=${1};
    local cachedir=${2};
    local bootloader=${3};
    local extlinux_conf=${RESOURCES}/menu/extlinux-menu.conf
    local overlay_path=${RESOURCES}/overlay
    local grub_efi_core=${RESOURCES}/grub/bootx64.efi  # FIXME, ARCH
    local grub_conf=${RESOURCES}/grub/grub.cfg
    local embed_conf=${RESOURCES}/grub/embed_efi.cfg
    local efi_grub_conf=${RESOURCES}/grub/efi_grub.cfg
    local gptmbr=$(dpkg -L syslinux-common | grep \/gptmbr.bin | grep -v efi)
    local installimg=${cachedir}/installer.img
    local mnt=${cachedir}/mnt
    local rootfs=${cachedir}/rootfs
    local efimnt=${cachedir}/efimnt
    local lower=${cachedir}/lower
    local upper=${cachedir}/upper
    local work=${cachedir}/upper/overlay-work
    local seed=$cachedir/seed/nocloud-net
    local splash=${RESOURCES}/images/splash.png
    local syslinux_path=$(dpkg -L syslinux-common | grep \/vesamenu.c32 |
                          grep -v efi | xargs -i dirname {})

    # prep image
    log "Generating Installer image file"
    local image_size=${INSTALLER_IMAGE_SIZE_M}
    qemu-img create -f raw $installimg ${image_size}M 2>&1 >> ${LOGFILE} || {
        log "Failed to create empty file: $installimg"
        return 1;
    }

    # Calculate the partition sizes and offsets.  Rootfs size can change
    # from build to build and the total size of the image is no longer
    # encoded here, but set globally.
    local efi_start="0%"
    local efi_end=200  # 200M efi partition
    local rootfs_size=$(blockalign_up `sudo du -s -B1 $rootfs`)
    local rootfs_start=$efi_end
    local fs_overhead=130  # best guess fs metadata and other overhead
    local rootfs_end=$(($rootfs_start + $rootfs_size + $fs_overhead))
    local overlay_start=$rootfs_end
    local overlay_end=$(($image_size - 1))  # save last 1M for GPT
    log "Partitioning Installer image (UEFI)"
    (parted -s $installimg mklabel msdos &&
    parted -s $installimg mkpart primary fat32 ${efi_start} ${efi_end}M &&
    parted -s $installimg mkpart primary ext3 ${efi_end}M ${rootfs_end}M &&
    parted -s $installimg mkpart primary ext3 ${overlay_start}M ${overlay_end}M &&
    parted -s $installimg set 1 boot on) 2>&1 >> ${LOGFILE}  || {
        log "Failed to partition image: $installimg"
     return 1;
    }
    log "$(parted -s $installimg print)"

    if [ "${bootloader}" != "grub2" ]; then
        log "Bootloader ${bootloader} not supported, cannot install"
        usage;
        return 1;
    fi

    log "Syncing rootfs into install image"
    local kpartx_ret=$(sudo kpartx -va $installimg)
    [ -z "$kpartx_ret" ] && {
        log "Failed to map image partitions into LVM"
        return 1;
    }
    sudo udevadm settle

    local loopparts=( `echo ${kpartx_ret} | fmt -w 1 | grep ^loop` )
    EFI_DEV="/dev/mapper/${loopparts[0]}"
    ROOTFS_DEV="/dev/mapper/${loopparts[1]}"
    OVERLAY_DEV="/dev/mapper/${loopparts[2]}"
    LOOPDEV="`losetup | grep "$installimg" | fmt -w 1 | grep ^/dev`"
    [ -z "${LOOPDEV}" ] && {
        log "empty loopdev! aieeeeee!";
        return 1;
    }

    log "Building Grub (EFI and BIOS) boot partition"
    (sudo mkfs.vfat -F32 -n GRUB2EFI ${EFI_DEV} 2>/dev/null &&
     mkdir -p ${efimnt} &&
     sudo mount $EFI_DEV ${efimnt} &&
     sudo mkdir -p ${efimnt}/EFI/BOOT &&
     sudo mkdir -p ${efimnt}/boot/grub &&
     sudo mkdir -p ${efimnt}/grub/fonts &&
     sudo mkdir -p ${efimnt}/grub/x86_64-efi &&
     sudo cp -a /usr/lib/shim/mmx64.efi.signed ${efimnt}/EFI/BOOT/MokManager.efi &&
     sudo cp -a /usr/lib/shim/shimx64.efi.signed ${efimnt}/EFI/BOOT/shimx64.efi &&
     sudo cp -a /usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed ${efimnt}/EFI/BOOT/grubx64.efi &&
     sudo cp -a /usr/share/grub/unicode.pf2 ${efimnt}/grub/fonts/ &&
     sudo cp -a /usr/share/grub/unicode.pf2 ${efimnt}/boot/grub &&
     cat ${efi_grub_conf} | sudo tee ${efimnt}/EFI/BOOT/grub.cfg &&
     cat ${embed_conf} | sudo tee ${efimnt}/grub/embed_efi.cfg &&
     cat ${grub_conf} | sudo tee ${efimnt}/grub/grub.cfg &&
     sudo rsync ${RSYNC_OPTS} /usr/lib/grub/x86_64-efi/ ${efimnt}/grub/x86_64-efi/ &&
     sudo grub-mkimage -O x86_64-efi -p /grub -c ${efimnt}/grub/embed_efi.cfg -o ${efimnt}/EFI/BOOT/bootx64.efi ${GRUB_MODS} &&
     sudo grub-install --force --removable --no-floppy \
                      --efi-directory=${efimnt}/EFI \
                      --boot-directory=${efimnt}/boot $LOOPDEV &&
     sudo cp -v ${splash} ${efimnt}/boot/grub) 2>&1 >> ${LOGFILE} || {
        log "ERROR: failed to create multiboot partition"
         return 1
     }

    log "Creating and syncing filesystem (original cloudimg rootfs)"
    (sudo mkfs.ext4 ${MKFS_OPTS} -L cloudimg-rootfs $ROOTFS_DEV &&
    sudo tune2fs -r 0 $ROOTFS_DEV &&
    mkdir -p ${lower} &&
    sudo mount $ROOTFS_DEV ${lower} &&
    sudo rsync ${RSYNC_OPTS} ${rootfs}/ ${lower}/) 2>&1 >> ${LOGFILE} || {
        log "ERROR: failed to sync rootfs into install image";
        return 1
    }

    log "Creating and syncing filesystem (installer overlay)"
    # mount -t overlay overlay -olowerdir=/lower,upperdir=/upper,\
    #                            workdir=/work /merged
    OVERLAY_VERSION=( `modinfo -V overlayfs` )  # returns: kmod version XX
    if [ ${OVERLAY_VERSION[2]} -le 15 ]; then
        # trusty 3.13 overlayfs version 15 or older doesn't use workdir
        FS=overlayfs
        OPTS="-olowerdir=$lower,upperdir=$upper/overlay"
    else
        # newer kernels than trusty 3.13 have workdir
        FS=overlay
        OPTS="-olowerdir=$lower,upperdir=$upper/overlay,workdir=$work"
    fi
    # load the right overlay module
    if ! lsmod | grep -q ${FS}; then sudo modprobe -v $FS; fi

    (sudo mkfs.ext3 ${MKFS_OPTS} -L overlay-rootfs $OVERLAY_DEV
    sudo tune2fs -r 0 $OVERLAY_DEV &&
    sudo mkdir -p ${work} ${upper} ${mnt} &&
    sudo mount $OVERLAY_DEV ${upper} &&
    sudo mkdir -p ${upper}/overlay ${work} &&
    sudo mount -t $FS $FS $OPTS ${mnt}) 2>&1 >> ${LOGFILE} || {
            log "ERROR: failed to overlay mount installer";
            return 1
    }

    log "Installing bootloader configuration"
    (sudo mkdir -p ${mnt}/proc
    sudo mkdir -p ${mnt}/sys
    sudo mkdir -p ${mnt}/dev
    sudo mount none -t proc ${mnt}/proc &&
    sudo mount none -t sysfs ${mnt}/sys &&
    sudo mount -o bind /dev ${mnt}/dev) 2>&1 >> ${LOGFILE} || {
      log "ERROR: failed to prepare target mounts for sync";
      return 1;
    }

    log "Installing subiquity package dependencies"
    local resolvconf=${mnt}/etc/resolv.conf
    local packages=""
    for installer_package in "${INSTALLER_DEPS[@]}"; do
        packages="$packages $installer_package"
    done
    sudo mv ${resolvconf} ${resolvconf}.old &&
    sudo cp /etc/resolv.conf ${resolvconf} &&

    log "Installing ppas in rootfs"
    # export existing install_ppa and run it in the chroot
    install_ppas_cmds="$(install_ppas ${PPAS[@]})"
    sudo chroot ${mnt} /bin/bash -c "${install_ppas_cmds}; apt-get update" || {
        log "Failed to add installer ppas to chroot";
        return 1;
    }

    log "Installing curtin in rootfs"
    local curtin_debbuild="$(cd $dldir/curtin &&
                             ./tools/build-deb -us -uc || return 1)"
    local curtin_ver=$(echo $curtin_debbuild |
                       awk '/building upstream version/ {print $4}' |
                       sed 's|,||g')
    # no python2 curtin
    local curtin_debs="$(ls $dldir/curtin/*${curtin_ver}*.deb | grep -v 'python-curtin')"
    if [ -z "$curtin_ver" -o -z "$curtin_debs" ]; then
        log "ERROR: failed to build curtin debs and get version";
        return 1;
    fi
    for deb in ${curtin_debs}; do
        cp ${deb} ${mnt}/tmp || {
           log "ERROR: failed copyin ${deb} into ${mnt}/tmp";
           return 1;
        }
    done

    # disable daemons in target
    disable_daemons_in_root $mnt

    apt_opts=" --quiet --assume-yes"
    apt_opts="$apt_opts --option=Dpkg::Options::=--force-unsafe-io"
    apt_opts="$apt_opts --option=Dpkg::Options::=--force-confold"
    sudo chroot ${mnt} /bin/bash -c \
        "dpkg --install /tmp/*${curtin_ver}*.deb;
         export DEBIAN_FRONTEND=noninteractive;
         apt-get ${apt_opts} -y -f install" || {
        log "ERROR: Failed to install curtin in target";
        return 1;
    }

    # restore daemons in target
    undisable_daemons_in_root $mnt

    log "Installing on rootfs: $packages"
    # use curtin's system-install if available
    if echo ${CURTIN_FEATURES} | grep -q SUBCOMMAND_SYSTEM_INSTALL; then
        log "Using curtin to install packages in-target"
        (cd $dldir/curtin &&
         sudo ./bin/curtin system-install -t $mnt -- $packages) || {
            log "Failed to install packages on rootfs";
            return 1;
        }
    else
        log "WARNING: skipping preinstallation of mdadm, lvm2"
        log "WARNING: will install packages during runtime"
        # if we don't have the right curtin features, we can't easily
        # install mdadm/lvm2 into the target as it will start up system
        # daemons and prevent us from unmounting the target
        packages=$(echo $packages | fmt -w1 | egrep -v "(mdadm|lvm2)")
        sudo chroot ${mnt} apt-get -y install $packages || {
            log "Failed to install packages on rootfs";
            return 1;
        }
    fi

    log "Cleaning up overlay apt cache"
    local before=( `sudo du -sk ${upper}` )
    sudo chroot ${mnt} apt-get clean || {
        log "ERROR: failed to run apt-get clean in ${mnt}";
        return 1;
    }
    local after=( `sudo du -sk ${upper}` )
    local delta=$((($before - $after) / 1024))
    log "Saved ${delta} MiB"

    log "Removing ppas in rootfs"
    remove_ppas_cmds="$(remove_ppas ${PPAS[@]})"
    sudo chroot ${mnt} /bin/bash -c "${remove_ppas_cmds}" || {
        log "Failed to remove installer ppas from chroot";
        return 1;
    }

    sudo rm ${resolvconf}
    sudo mv ${resolvconf}.old ${resolvconf}

    if [ "${bootloader}" == "syslinux" ]; then
        sudo mkdir -p ${mnt}/boot/extlinux &&
        sudo extlinux --install ${mnt}/boot/extlinux &&
        sudo cp -av ${syslinux_path}/*.c32 ${mnt}/boot/extlinux &&
        sudo cp -av ${splash} ${mnt}/boot/extlinux &&
        cat ${extlinux_conf} | sudo tee ${mnt}/boot/extlinux/extlinux.conf
    else
        log "Installing grub2"
        cat ${grub_conf} | sudo tee ${efimnt}/boot/grub/grub.cfg 2>&1 >> ${LOGFILE} && true
    fi

    # syncing overlay
    log "Injecting installer configuration/scripts"
    sudo rsync ${RSYNC_OPTS} ${overlay_path}/ ${mnt}/ || {
        log "Failed to sync local installer configuration/scripts";
        return 1;
    }

    log "Installing cloud seed"
    sudo mkdir -p ${mnt}/var/lib/cloud/seed &&
    sudo cp -a ${seed} ${mnt}/var/lib/cloud/seed && sync || {
        log "Failed to install bootloader and configuration";
        return 1;
    }

    _RETVAL="$installimg";
    return 0;
}

parse_args() {
    # -b,--bootloader [syslinux, grub2]
    # -h,--help <help output>
    # -r,--release [trusty, utopic, vivid, wily]
    # -v,--verbose

    # args:
    [ $# -lt 1 ] && { usage; exit 0; }

    OPTS_LONG="arch:,bootloader:,download:,help,release:,stream:,verbose,version:"
    OPTS="a:b:d:hor:s:vV:"
    ARGS=`getopt --name "$PROG" --long $OPTS_LONG --options $OPTS -- "$@"`
    if [ $? -ne 0 ]; then
        echo "$PROG: usage error (use -h for help)" >&2
        exit 2
    fi
    eval set -- $ARGS

    ARCH="amd64"
    BOOTLOADER="syslinux"
    DLDIR=~/download
    RELEASE="wily"
    STREAM="daily"
    VERBOSE="no"
    VERSION=""
    while [ $# -gt 0 ]; do
        case "$1" in
            -a | --arch)        ARCH="$2"; shift;;
            -b | --bootloader)  BOOTLOADER="$2"; shift;;
            -d | --download)    DLDIR="$2"; shift;;
            -h | --help)        usage; exit 0;;
            -r | --release)     RELEASE="$2"; shift;;
            -s | --stream)      STREAM="$2"; shift;;
            -V | --version)     VERSION="$2"; shift;;
            -v | --verbose)     VERBOSE="yes";;
            --)                 shift; break;; # end of options
        esac
        shift
    done
    ARGS="$@"

    [ "${VERBOSE}" == "yes" ] && set -x

    return 0
}

main() {
    log "INFO: Starting $PROG with params: $@"

    parse_args "$@"

    # get prereqs installed first
    install_deps "$PKG_DEPS" || { return 1; }

    [ -z "$OUTPUT" ] && {
        OUTPUT="ubuntu-server-${STREAM}-${RELEASE}-${ARCH}-installer.img"
    }

    install_src ${DLDIR} ${SRC_DEPS[@]} || {
        return 1;
    }

    export CURTIN_FEATURES="$(cd ${DLDIR}/curtin;
                      python3 -c \
                      'import curtin; [print(x) for x in curtin.FEATURES]')"
    log "Detected curtin features: $CURTIN_FEATURES"
    [ -n "${CURTIN_FEATURES}" ] || {
        return 1;
    }

    acquire_image ${DLDIR} "$STREAM" "$RELEASE" "$ARCH" "$VERSION" || {
        return 1;
    }
    CACHEDIR=${_RETVAL}
    log "CACHEDIR=$CACHEDIR"

    generate_seed ${DLDIR} $CACHEDIR || {
        return 1;
    }

    generate_img ${DLDIR} $CACHEDIR $BOOTLOADER || {
        return 1;
    }
    INSTALLIMG=${_RETVAL}
    log "Cleaning up ..."
    cleanup_noexit &&
    mv $INSTALLIMG ${OUTPUT} &&
    ln -fs ${OUTPUT} installer.img || {
        log "ERROR: failed to move $INSTALLIMG to $OUTPUT";
        return 1;
    }
    log "Installer image complete: $OUTPUT"

    return 0
}

main $@
exit $?
