#!/bin/bash

# IMPLEMENTATION NOTE: It was not possible to implement this script using
# virt-customize because of below ubuntu bugs:
#  - https://bugs.launchpad.net/ubuntu/+source/libguestfs/+bug/1632405
#  - https://bugs.launchpad.net/ubuntu/+source/isc-dhcp/+bug/1650740
#
# It has therefore been adopted a more low level strategy performing below
# steps:
#  - mount guest image to a temporary folder
#  - set up an environment suitable for executing chroot
#  - execute customize_image function inside chroot environment
#  - cleanup chroot environment

# Array of packages to be installed of guest image
INSTALL_GUEST_PACKAGES=(
   socat  # used to replace nc for testing advanced network features like
          # multicast
   iperf3
   iputils-ping
   ncat
   psmisc  # provides killall command
   python3
   tcpdump
   vlan
)

# Function to be executed once after chroot on guest image
# Add more customization steps here
function customize_image {
    # dhclient-script requires to read /etc/fstab for setting up network
    touch /etc/fstab
    chmod ugo+r /etc/fstab

    # Ubuntu guest image _apt user could require access to below folders
    local apt_user_folders=( /var/lib/apt/lists/partial )
    mkdir -p "${apt_user_folders[@]}"
    chown _apt.root -fR "${apt_user_folders[@]}"

    # Install desired packages to Ubuntu guest image
    (
        DEBIAN_FRONTEND=noninteractive
        sudo apt-get update -y
        sudo apt-get install -y "${INSTALL_GUEST_PACKAGES[@]}"
    )
}

function main {
    set -eux
    trap cleanup EXIT
    "${ENTRY_POINT:-chroot_image}" "$@"
}

# Chroot to guest image then executes customize_image function inside it
function chroot_image {
    local image_file=$1
    local temp_dir=${TEMP_DIR:-$(make_temp -d)}

    # Mount guest image into a temporary directory
    local mount_dir=${temp_dir}/mount
    mkdir -p "${mount_dir}"
    mount_image "${mount_dir}" "${temp_dir}/pid"

    # Mount system directories
    bind_dir "/dev" "${mount_dir}/dev"
    bind_dir "/dev/pts" "${mount_dir}/dev/pts"
    bind_dir "/proc" "${mount_dir}/proc"
    bind_dir "/sys" "${mount_dir}/sys"

    # Mount to keep temporary files out of guest image
    mkdir -p "${temp_dir}/apt" "${temp_dir}/cache" "${temp_dir}/tmp"
    bind_dir "${temp_dir}/cache" "${mount_dir}/var/cache"
    bind_dir "${temp_dir}/tmp" "${mount_dir}/tmp"
    bind_dir "${temp_dir}/tmp" "${mount_dir}/var/tmp"
    bind_dir "${temp_dir}/apt" "${mount_dir}/var/lib/apt"

    # Temporarly replace /etc/resolv.conf symlink to use the same DNS as this
    # host
    local resolv_file=${mount_dir}/etc/resolv.conf
    sudo mv -f "${resolv_file}" "${resolv_file}.orig"
    sudo cp /etc/resolv.conf "${resolv_file}"
    add_cleanup sudo mv -f "${resolv_file}.orig" "${resolv_file}"

    # Makesure /etc/fstab exists and it is readable because it is required by
    # /sbin/dhclient-script
    sudo touch /etc/fstab
    sudo chmod 644 /etc/fstab

    # Copy this script to mount dir
    local script_name=$(basename "$0")
    local script_file=${mount_dir}/${script_name}
    sudo cp "$0" "${script_file}"
    sudo chmod 500 "${script_file}"
    add_cleanup sudo rm -f "'${script_file}'"

    # Execute customize_image inside chroot environment
    local command_line=( ${CHROOT_COMMAND:-customize_image} )
    local entry_point=${command_line[0]}
    unset command_line[0]
    sudo -E "ENTRY_POINT=${entry_point}" \
        chroot "${mount_dir}" "/${script_name}" "${command_line[@]:-}"
}

# Mounts guest image to $1 directory writing pid to $1 pid file
# Then registers umount of such directory for final cleanup
function mount_image {
    local mount_dir=$1
    local pid_file=$2

    # export libguest settings
    export LIBGUESTFS_BACKEND=${LIBGUESTFS_BACKEND:-direct}
    export LIBGUESTFS_BACKEND_SETTINGS=${LIBGUESTFS_BACKEND_SETTINGS:-force_tcg}

    # Mount guest image
    sudo -E guestmount -i \
        --add "${image_file}" \
        --pid-file "${pid_file}" \
        "${mount_dir}"

    add_cleanup \
        'ENTRY_POINT=umount_image' \
        "'$0'" "'${mount_dir}'" "'${pid_file}'"
}

# Unmounts guest image directory
function umount_image {
    local mount_dir=$1
    local pid_file=$2
    local timeout=10

    # Take PID just before unmounting
    local pid=$(cat ${pid_file} || true)
    sudo -E guestunmount "${mount_dir}"

    if [ "${pid:-}" != "" ]; then
        # Make sure guestmount process is not running before using image
        # file again
        local count=${timeout}
        while sudo kill -0 "${pid}" 2> /dev/null && (( count-- > 0 )); do
            sleep 1
        done
        if [ ${count} == 0 ]; then
            # It is not safe to use image file at this point
            echo "Wait for guestmount to exit failed after ${timeout} seconds"
        fi
    fi
}

# Creates a temporary file or directory and register removal for final cleanup
function make_temp {
    local temporary=$(mktemp "$@")
    add_cleanup sudo rm -fR "'${temporary}'"
    echo "${temporary}"
}

# Bind directory $1 to directory $2 and register umount for final cleanup
function bind_dir {
    local source_dir=$1
    local target_dir=$2
    sudo mount --bind "${source_dir}" "${target_dir}"
    add_cleanup sudo umount "'${target_dir}'"
}

# Registers a command line to be executed for final cleanup
function add_cleanup {
    CLEANUP_FILE=${CLEANUP_FILE:-$(mktemp)}

    echo -e "$*" >> ${CLEANUP_FILE}
}

# Execute command lines for final cleanup in reversed order
function cleanup {
    error=$?

    local cleanup_file=${CLEANUP_FILE:-}
    if [ -r "${cleanup_file}" ]; then
        tac "${cleanup_file}" | bash +e -x
        CLEANUP_FILE=
        rm -fR "${cleanup_file}"
    fi

    exit ${error}
}

main "$@"
