#!/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 . PROG=`basename $0` PREFIX="${PROG}" INSTALLER_IMAGE_SIZE_M=2048 # 2G image RSYNC_OPTS="-aXHAS --one-file-system" MKFS_OPTS="-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 </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}; set -x # 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]} kernel=( `${USQUERY} --output-format="%(version_name)s %(item_url)s %(sha256)s" --max=1 maas-daily release=$release arch=$arch item_name=boot-kernel` ) initrd=( `${USQUERY} --output-format="%(version_name)s %(item_url)s %(sha256)s" --max=1 maas-daily release=$release arch=$arch item_name=boot-initrd` ) local kernel_url=${kernel[1]} local initrd_url=${initrd[1]} ;; 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]} kernel=( `${USQUERY} --output-format="%(version_name)s %(item_url)s %(sha256)s" --max=1 maas-daily release=$release arch=$arch item_name=boot-kernel` ) initrd=( `${USQUERY} --output-format="%(version_name)s %(item_url)s %(sha256)s" --max=1 maas-daily release=$release arch=$arch item_name=boot-initrd` ) local kernel_url=${kernel[1]} local initrd_url=${initrd[1]} ;; *) 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 kernel_img=${cachedir}/vmlinuz local initrd_img=${cachedir}/initrd.img local roottar=${cachedir}/root.tar.gz local rootfs=${cachedir}/rootfs set +x # 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; } wget --progress=bar -c "${kernel_url}" -O "${kernel_img}" || { log "ERROR: failed to download: ${kernel_url}"; return 1; } wget --progress=bar -c "${initrd_url}" -O "${initrd_img}" || { log "ERROR: failed to download: ${initrd_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 set -x # unpack rootfs tar log "Unpacking roottar: $label $release $arch"; local kernel_img=${cachedir}/vmlinuz 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 if [ -e "${kernel_img}" ] && [ -e "${initrd_img}" ]; then log " Using cached $label $release $arch kernel and initrd:" sudo cp "${kernel_img}" "${initrd_img}" "${rootfs}" fi set +x _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} -cvpf $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/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-install --force --removable --no-floppy \ --target=x86_64-efi \ --efi-directory=${efimnt} \ --boot-directory=${efimnt}/boot $LOOPDEV && 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 \ --target=i386-pc \ --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)" set -x (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 } set +x 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 CURTIN_FEATURES=disabled if echo ${CURTIN_FEATURES} | grep -q SUBCOMMAND_SYSTEM_INSTALL; then log "Using curtin to install packages in-target" set -x (cd $dldir/curtin && sudo ./bin/curtin system-install -t $mnt -- $packages) || { log "Failed to install packages on rootfs"; return 1; } set +x 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 # -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 $?