#!/bin/bash

VERBOSITY=1
TEMP_D=""

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 [packages]
  Patch image, upgrading [packages].
    --kernel FILE             copy kernel out to FILE
    --initrd FILE             copy initrd out to FILE

    --krd-only                only copy out kernel/initrd do not change image.
                              this is incompatible with 'packages'
                              it is a short-cut to specifying all the '--no-*'
                              options below.

    --no-copy-apt             do not copy host's apt repos in.

  image modifications:
    --no-update-fstab         do not modify fstab (LP: #1732028)

  if no packages are provided, and ADT_TEST_TRIGGERS is set
  in environment, then it will be read for the list of packages.
  to override that behavior pass package 'none'.
EOF

}

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

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

adt_test_triggers_to_bin_pkgs() {
    # ADT_TEST_TRIGGERS is space delimited <src>/version
    # returns a scalar '_RET', caller has to expand
    local tok src="" ver="" binpkgs=""
    for tok in "$@"; do
        src=${tok%/*}
        ver=${tok#*/}
        bin_packages_from_source_pkg "$src" "$ver" || return
        if [ -z "${_RET}" ]; then
            error "$tok: (source=$src ver=$ver) No packages available!"
            return 1
        fi
        binpkgs="${binpkgs} ${_RET}"
    done
    _RET=${binpkgs# }
}

bin_packages_from_source_pkg() {
    local pkg=$1 ver=${2} cmd="" out="" p="" ret=""
    if ! command -v grep-aptavail >/dev/null; then
        error "No command grep-aptavail: apt-get install dctrl-tools"
        return 1
    fi
    cmd=( grep-aptavail --show-field=Package
        --exact-match --field=Source:Package "$pkg" )
    if [ -n "$ver" ]; then
       cmd=( "${cmd[@]}" --and --field=Version "$ver" )
    fi
    out=$("${cmd[@]}" | sed 's,Package: ,,')
    ret=""
    for p in $out; do
        ret="$ret ${p#Package: }${ver:+=${ver}}"
    done
    # if no version was provided, we could have multiple results
    _RET=$(set -f; for i in ${ret}; do echo "$i"; done | sort -u)
}

update_fstab() {
    # update_fstab(path) modify the fstab at path
    # to remove problematic entries (LP: #1732028)
    local fstab="$1"
    if [ ! -e "$fstab.patch-image-dist" ]; then
        cp "$fstab" "$fstab.patch-image-dist" ||
            { error "failed backing up $fstab to $fstab.dist"; return 1; }
    fi
    sed -i '/^LABEL=UEFI/s/^/#/' "$fstab"
}

cat_debug_script() {
    cat <<"EOF"
#!/bin/sh
set -f
NAME=${1:-debug-pid-$$}
[ "$(id -u)" = "0" ] && log="/run/${NAME}.log" || log="/tmp/${NAME}.log"

msg() {
   echo "===" "$@" "===" 1>&2
}
showcmd() {
    msg "showcmd$" "$@"
    "$@"
}

main() {
    issues=$(journalctl | egrep "[.]mount: ")
    if [ -z "$issues" ]; then
        msg "$NAME:" "MOUNT ISSUES FOUND: NO"
    else
        msg "$NAME:" "MOUNT ISSUES FOUND: YES"
    fi
    showcmd systemctl list-units --no-pager --all --full "*.mount"
    showcmd systemctl cat --no-pager --all --full "*.mount"
    showcmd systemctl status --no-pager --all --full "*.mount"
    showcmd cat /etc/fstab
    showcmd cat /proc/1/mountinfo
}

main > "$log" 2>&1
echo "============ $NAME ============="
cat "$log"
EOF
}

add_systemd_job() {
    local target="$1" name="$2" after="$3" start="$4"
    local spath="lib/systemd/system/xdebug-$name.service"
    debug 1 "writing $name to $target/$spath"
    cat > "$target/$spath" <<EOF
[Unit]
Description=Execute xdebug ${name}
After=${after}

[Service]
Type=oneshot
ExecStart=${start} ${name}
RemainAfterExit=yes
StandardOutput=journal+console

[Install]
WantedBy=multi-user.target
EOF
    [ $? -eq 0 ] || { error "failed writing $target/$spath"; return 1; }
    local wantsdir="$target/etc/systemd/system/multi-user.target.wants"
    ln -s "/$spath" "$wantsdir/" ||
        { error "failed ln -s '/$spath' '$wantsdir/'"; return 1; }
}

insert_debug_1788188() {
    # This is an attempt to collect more debug information in the
    # event that the tests trigger LP: #1788188.
    local target="$1"
    [ "$target" = "/" -o -z "$target" ] &&
        { error "target was / in insert_debug"; return 1; }
    local script="usr/local/bin/debug-mounts"
    cat_debug_script > "$target/$script" &&
        chmod 755 "$target/$script" ||
       { error "failed writing $target/script"; return 1; }
    local name=""
    ( set +f;
      rm -f "$target/lib/systemd/system/xdebug"-*.service \
            "$target/etc/systemd/system/"*.wants/xdebug*.service )
    add_systemd_job "$target" network network-online.target "/$script" &&
        add_systemd_job "$target" local-fs local-fs.target "/$script" ||
        { error "failed adding systemd jobs"; return 1; }
}

main() {
    local short_opts="hv"
    local long_opts="help,no-copy-apt,initrd:,kernel:,verbose"
    local getopt_out="" orig_args=""
    orig_args=( "$@" )
    getopt_out=$(getopt --name "${0##*/}" \
        --options "${short_opts}" --long "${long_opts}" -- "$@") &&
        eval set -- "${getopt_out}" ||
        { bad_Usage; return; }

    local cur="" next=""
    local kernel="" initrd="" copy_apt=true krd_only=false
    local update_fstab=true

    while [ $# -ne 0 ]; do
        cur="$1"; next="$2";
        case "$cur" in
            -h|--help) Usage ; exit 0;;
               --krd-only) krd_only=true; shift;;
               --no-copy-apt) copy_apt=false; shift;;
               --no-update-fstab) update_fstab=false; shift;;
               --kernel) kernel=$next; shift;;
               --initrd) initrd=$next; shift;;
            -v|--verbose) VERBOSITY=$((${VERBOSITY}+1));;
            --) shift; break;;
        esac
        shift;
    done

    [ $# -ne 0 ] || { bad_Usage "must provide at least image"; return; }

    [ "$(id -u)" = "0" ] || fail "not root"
    target="$1"
    shift
    if [ -f "$target" ]; then
        local nargs=""
        nargs=( "${orig_args[@]}" )
        # replace the first occurance of target in orig args with _MOUNTPOINT_
        for((i=0;i<${#nargs[@]};i++)); do
            [ "${nargs[$i]}" = "$target" ] && nargs[$i]=_MOUNTPOINT_ && break
        done
        debug 1 "mic $target -- $0 ${nargs[*]}"
        IMAGE="$target" exec mount-image-callback \
            --system-resolvconf "$target" -- "$0" "${nargs[@]}"
    fi

    if [ ! -d "$target" ]; then
        fail "$target: not a directory or file"
    fi

    local tsrc=${IMAGE:-${target}}
    local packages=( )
    packages=( "$@" )

    if $krd_only; then
        if [ "${#packages[@]}" != 0 -a "${packages[*]}" != "none" ]; then
            error "--krd-only is incompatible with packages."
            return 1
        fi
        debug 1 "--krd-only provided no changes will be done."
        copy_apt=false
        update_fstab=false
        packages=( "none" )
    fi

    if [ "${#packages[@]}" = "1" -a "${packages[0]}" = "none" ]; then
        packages=( )
    elif [ "${#packages[@]}" -eq 0 -a -n "${ADT_TEST_TRIGGERS}" ]; then
        adt_test_triggers_to_bin_pkgs ${ADT_TEST_TRIGGERS} ||
            fail "failed to find binary packages " \
                "from ADT_TEST_TRIGGERS=$ADT_TEST_TRIGGERS"
        packages=( $_RET )
        debug 1 "read ADT_TEST_TRIGGERS=$ADT_TEST_TRIGGERS to set" \
            "packages=${packages[*]}"
    else
        local opackages="" debs="" pkg=""
        opackages=( "${packages[@]}" )
        packages=( )
        debs=( )
        for pkg in "${opackages[@]}"; do
            [ -f "${pkg}" ] && debs[${#debs[@]}]="$pkg" ||
                packages[${packages[@]}]="$pkg"
        done
        if [ "${#packages[@]}" -eq 0 -a "${#debs[@]}" -eq 0 ]; then
            packages=( open-iscsi )
            debug 1 "no packages or debs given, using packages=${packages[*]}"
        fi
    fi

    # if open-iscsi is not in the packages list above, we handle it specifically
    # so that even if the image did not have open-iscsi, it will get it.
    local pkg="" mypkg="open-iscsi"
    for pkg in "${packages[@]}"; do
        case "$pkg" in
            open-iscsi|open-iscsi/*) mypkg="none";;
        esac
    done

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

    if $copy_apt; then
        local distdir="${target}/etc/apt.dist.${0##*/}"
        if [ ! -d "$distdir" ]; then
            mv "$target/etc/apt" "$distdir" ||
                fail "failed to backup /etc/apt in $tsrc"
        fi
        cp -a /etc/apt "$target/etc/" ||
            fail "failed to replace /etc/apt in $tsrc"

        # find file:// repos in apt sources and copy them in
        local file_repos=""
        file_repos=$(
            fm='s,.*[[:space:]]file:///*\([^[[:space:]]*\).*,/\1,p'
            shopt -s nullglob
            for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list; do
                sed -n -e 's/#.*//' -e "$fm" "$f"
            done
        )
        local dir="" bdir="" tdir=""
        for dir in ${file_repos}; do
            tdir="${target}$(dirname $dir)/$(basename ${dir})"
            if [ -d "$dir" ]; then
                debug 1 "copying $dir -> $tdir"
                mkdir -p "$tdir" ||
                    fail "failed to create $tdir"
                rsync --delete -a "$dir/" "$tdir" ||
                    fail "failed copy from $dir/ to $tdir"
            fi
        done
    fi

    if $update_fstab; then
        update_fstab "$target/etc/fstab" ||
            { error "failed updating /etc/fstab in target"; return 1; }
    fi

    insert_debug_1788188 "$target" || return 1

    mount -o bind /sys "$target"/sys || :
    mount -o bind /proc "$target"/proc || :
    mount -o bind /dev "$target"/dev || :
    mount -o bind /dev/pts "$target"/dev/pts || :

    debug 1 "${#packages[@]} packages: ${packages[*]}"
    if [ "${#packages[@]}" != "0" ]; then
        DEBIAN_FRONTEND=noninteractive chroot "$target" sh -exc '
            mypkg=$1
            shift
            apt-get update -q
            if [ "$mypkg" != "none" ]; then
                apt-get install -qy "$mypkg"
            fi
            apt-get install -qy --only-upgrade "$@"' \
                chroot-inst "$mypkg" "${packages[@]}" ||
            fail "failed to install ${packages[*]} in $target"
    fi

    debug 1 "${#debs[@]} debs: ${debs[*]}"
    if [ "${#debs[@]}" != "0" ]; then
        local tmpd=""
        tmpd=$(mktemp -d "${target}/tmp/${0##*/}.XXXXXX")
        cp "${debs[@]}" "$tmpd"
        DEBIAN_FRONTEND=noninteractive chroot "$target" sh -exc \
            'cd "$1"; shift; dpkg -i *.deb' debinstalls "${tmpd#${target}}"
        rm -Rf "${tmpd}"
    fi

    umount "$target"/dev/pts || :
    umount "$target"/dev || :
    umount "$target"/proc || :
    umount "$target"/sys || :

    local kpath="" ipath=""
    for f in "$target/boot/"*; do
        case "${f##*/}" in
            *.signed) continue;;
            vmlinu?-*) kpath="$f";;
            initrd.img-*) ipath="$f";;
        esac
    done

    if [ -n "$kernel" ]; then
        [ -n "$kpath" ] || fail "unable to find kernel in $tsrc"
        debug 1 "using kernel ${kpath#${target}}"
        cp "$kpath" "$kernel" ||
            fail "failed copy $kpath to $kernel"
        [ -z "$IMAGE" ] || chown "--reference=$IMAGE" "$kernel"
    fi

    if [ -n "$initrd" ]; then
        [ -n "$ipath" ] || fail "unable to find initrd in $tsrc"
        debug 1 "using initrd ${ipath#${target}}"
        cp "$ipath" "$initrd" ||
            fail "failed copy $ipath to $initrd"
        [ -z "$IMAGE" ] || chown "--reference=$IMAGE" "$initrd"
    fi

}

main "$@"

# vi: ts=4 expandtab
