#!/bin/bash
#
# This file is part of VIRL 2
# Copyright (c) 2019-2023, Cisco Systems, Inc.
# All rights reserved.
#

source /etc/default/virl2

# Version of this script in semver 2.0 format.
_VERSION="2.0.4"

# The branch from which to grab the canonical, stable latest version of the script.
GITHUB_BRANCH="master"

# This is the link to the raw GitHub source for the script itself.
GITHUB_URL="https://raw.githubusercontent.com/CiscoDevNet/cml-community/${GITHUB_BRANCH}/scripts/migration-tool/virl2-migrate-data.sh"

SRC_DIRS="${BASE_DIR}/images ${CFG_DIR}"
KEY_FILE="migration_key"
SSH_PORT="1122"
DOING_MIGRATION=0
DEFAULT_USER="sysadmin"
MIGRATION_MAP="/var/lib/libvirt/images:${LIBVIRT_IMAGES}"
HOSTNAME=$(hostname --short)

# Compute a timestamp, avoiding colons so that it's easier to use in filenames.
TIMESTAMP=$(printf '%(%Y-%m-%d_%H-%M-%S_%Z)T')

check_for_updates() {
    gh_version=$(curl -s ${GITHUB_URL} | grep "^_VERSION")
    if [ -z "${gh_version}" ]; then
        echo "Unable to find version from GitHub source.  Not updating."
        return 1
    fi

    gh_version="_GH${gh_version}"
    eval "${gh_version}"

    if [ "${_GH_VERSION}" = "${_VERSION}" ]; then
        echo "GitHub source version is the same as the current version.  Not updating."
        return 0
    fi

    curl -s --output ${LOCAL_ME} ${GITHUB_URL}
    rc=$?
    if [ ${rc} != 0 ]; then
        echo "Failed to download GitHub source.  Not updating."
        return ${rc}
    fi

    chmod +x ${LOCAL_ME}

    echo "Successfully updated to ${_GH_VERSION}.  Please re-run ${LOCAL_ME}."

    return 0
}

cleanup_backup() {
    echo
    echo "Cleaning up..."
    echo

    if [ -n "${tempd}" ]; then
        rm -rf "${tempd}"
    fi
    if [ -n "${ddir}" ]; then
        rm -rf "${ddir}"
    fi
    if [ -n "${ndir}" ]; then
        rm -rf "${ndir}"
    fi
    if [ -n "${idir}" ]; then
        rm -rf "${idir}"
    fi

    if [ ${DOING_MIGRATION} -eq 1 ]; then
        umount_refplat_overlay 2>/dev/null
    fi

    restart_cml_services

    if [ -z "${rc}" ]; then
        rc=1
    fi

    if [ ${rc} != 0 ]; then
        rm -f "${BACKUP_FILE}"
    fi

    exit ${rc}
}

cleanup_failed_restore() {
    echo
    echo "Cleaning up..."
    echo

    if [ -n "${ddir}" ]; then
        rm -rf "${ddir}"
    fi
    if [ -n "${ndir}" ]; then
        rm -rf "${ndir}"
    fi
    if [ -n "${idir}" ]; then
        rm -rf "${idir}"
    fi

    restart_cml_services

    if [ -z "${rc}" ]; then
        rc=1
    fi

    exit ${rc}
}

cleanup_failed_from_host() {
    echo
    echo "Cleaning up..."
    echo

    if [ ${DOING_MIGRATION} -eq 1 ]; then
        ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --umount-overlay"
    fi

    ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --start && sudo rm -rf ${libvirt_domains} && sudo rm -f /tmp/${ME} && sudo rm -f /etc/sudoers.d/cml-migrate && (cp -fa ~/.ssh/authorized_keys.migration ~/.ssh/authorized_keys >/dev/null 2>&1 || true)"

    if [ -z "${rc}" ]; then
        rc=1
    fi

    cleanup_from_host "${key_dir}"

    exit ${rc}
}

set_virl_version() {
    product_file=/PRODUCT
    if [ $# = 1 ]; then
        product_file=$1
    fi

    vers=$(jq -r ".PRODUCT_VERSION" <"${product_file}")

    if [ $# = 1 ]; then
        echo ${vers}
    else
        VIRL_VERSION=${vers}
    fi
}

needs_overlay() {
    major_ver=$(echo ${VIRL_VERSION} | cut -d'.' -f1)
    minor_ver=$(echo ${VIRL_VERSION} | cut -d'.' -f2)
    if [ "${major_ver}" -gt 2 ] || ([ "${major_ver}" -eq 2 ] && [ "${minor_ver}" -ge 3 ]); then
        return 1
    fi

    return 0
}

mount_refplat_overlay() {
    if ! needs_overlay; then
        return 0
    fi

    # create required directories, if needed
    [ -d $REF_PLAT/cdrom ] || mkdir -p $REF_PLAT/cdrom
    [ -d $REF_PLAT/diff ] || mkdir -p $REF_PLAT/diff
    [ -d $REF_PLAT/work ] || mkdir -p $REF_PLAT/work

    if [ $REF_PLAT != $LIBVIRT_IMAGES ]; then
        MOUNT_POINT=$REF_PLAT/cdrom
    else
        MOUNT_POINT=$LIBVIRT_IMAGES
    fi

    # if overlayfs is mounted, do nothing
    mount | grep -q "^overlay on $LIBVIRT_IMAGES"
    if [ $? -eq 0 ]; then
        return 0
    fi

    # try to mount the CDROM. If none is present it will continue
    # it will just give an error for the mount like
    # "mount: /var/local/virl2/refplat/cdrom: no medium found on /dev/sr0."
    if [ -f "$REF_PLAT_ISO_IMAGE" ]; then
        ! test -d $MOUNT_POINT && mkdir $MOUNT_POINT
        if ! mount -t iso9660 -oloop,fscontext=$CONTEXT $REF_PLAT_ISO_IMAGE $MOUNT_POINT; then
            echo "no refplat ISO image present / mounted!"
            return 1
        fi
    elif [ -d "$REF_PLAT_DIR" ]; then
        if test -d "$REF_PLAT_DIR"; then
            test -d $MOUNT_POINT && rmdir $MOUNT_POINT
            ln -s $REF_PLAT_DIR $MOUNT_POINT
        else
            echo "no refplat ISO image present / mounted!"
            return 1
        fi
    else
        ! test -d $MOUNT_POINT && mkdir $MOUNT_POINT
        if ! mount $CDROM_DEVICE; then
            echo "no refplat CD-ROM present / mounted!"
            return 1
        fi
    fi

    # mount the libvirt image directory in an OverlayFS
    if [ $REF_PLAT != $LIBVIRT_IMAGES ]; then
        mount -t overlay overlay \
            -ofscontext=$CONTEXT,lowerdir=$REF_PLAT/cdrom,upperdir=$REF_PLAT/diff,workdir=$REF_PLAT/work \
            $LIBVIRT_IMAGES
        if [ $? != 0 ]; then
            echo "error mounting overlay filesystem"
            return 1
        fi
    fi

    return 0
}

umount_refplat_overlay() {
    if ! needs_overlay; then
        return 0
    fi

    umount ${LIBVIRT_IMAGES}
    return $?
}

build_local_src_dirs() {
    if [ ${DOING_MIGRATION} -eq 1 ]; then
        return
    fi

    # In CML 2.3+ we no longer have the overlay.
    if ! needs_overlay; then
        SRC_DIRS="${SRC_DIRS} ${LIBVIRT_IMAGES}"
        return
    fi

    if [ "${REF_PLAT}" != "${LIBVIRT_IMAGES}" ]; then
        MOUNT_POINT=${REF_PLAT}/cdrom
    else
        MOUNT_POINT=${LIBVIRT_IMAGES}
    fi

    if [ -d "${REF_PLAT_DIR}" ]; then
        MOUNT_POINT="${REF_PLAT_DIR}"
    fi

    WRKDIR="${REF_PLAT}"/diff

    # Find all custom node defs
    new_node_defs=$(find "${WRKDIR}"/node-definitions/ -maxdepth 1 -type f -name "*.yaml")
    old_IFS=${IFS}
    IFS='
'
    for nd in ${new_node_defs}; do
        fname=$(basename "${nd}")
        if [ ! -f "${MOUNT_POINT}"/node-definitions/"${fname}" ]; then
            SRC_DIRS="${SRC_DIRS} ${nd}"
        else
            if ! diff -q "${nd}" "${MOUNT_POINT}"/node-definitions/"${fname}" >/dev/null 2>&1; then
                SRC_DIRS="${SRC_DIRS} ${nd}"
            fi
        fi
    done

    # Find all custom image defs
    new_image_defs=$(find "${WRKDIR}"/virl-base-images/ -maxdepth 1 -type d)

    for id in ${new_image_defs}; do
        if [ "${id}" = "${WRKDIR}"/virl-base-images/ ]; then
            continue
        fi
        dname=$(basename "${id}")
        if [ ! -d "${MOUNT_POINT}"/virl-base-images/"${dname}" ]; then
            SRC_DIRS="${SRC_DIRS} ${id}"
        fi
    done

    IFS=${old_IFS}
}

export_network_configuration() {
    outfile=${CFG_DIR}/migration_info.txt
    printf "# CML2 migration on $(date).\n" >> ${outfile}
    printf "\n## nmcli con show \n" >> ${outfile}
    nmcli con show >> ${outfile}
    printf "\n## ip link show \n" >> ${outfile}
    ip link show >> ${outfile}
    printf "\n## nmcli con show bridge0 \n" >> ${outfile}
    nmcli con show bridge0 >> ${outfile}
    printf "\n## nmcli \n" >> ${outfile}
    nmcli >> ${outfile}
    printf "\n## hostname \n" >> ${outfile}
    hostname >> ${outfile}
}

export_libvirt_domains() {
    ddir=$(mktemp -d /tmp/libvirt_domains.XXXXX)
    domains=$(virsh list --all --name)
    if [ $? != 0 ]; then
        echo "${ddir}"
        return $?
    fi

    old_IFS=${IFS}
    IFS='
'

    for domain in ${domains}; do
        virsh dumpxml "${domain}" >"${ddir}"/"${domain}".xml
    done

    IFS=${old_IFS}

    echo "${ddir}"
}

export_libvirt_networks() {
    ndir=$(mktemp -d /tmp/libvirt_networks.XXXXX)
    networks=$(virsh net-list --all --name | grep -v "^default")
    if [ $? != 0 ]; then
        echo "${ndir}"
        return $?
    fi

    old_IFS=${IFS}
    IFS='
'

    for network in ${networks}; do
        virsh net-dumpxml "${network}" >"${ndir}"/"${network}".xml
    done

    IFS=${old_IFS}

    echo "${ndir}"
}

export_libvirt_ifaces() {
    idir=$(mktemp -d /tmp/libvirt_ifaces.XXXXX)
    ifaces=$(virsh net-list --all | grep "^[[:space:]]*bridge" | awk '{print $1}' | grep -v "^bridge0")
    if [ $? != 0 ]; then
        echo "${idir}"
        return $?
    fi

    old_IFS=${IFS}
    IFS='
'

    for iface in ${ifaces}; do
        virsh iface-dumpxml "${iface}" --inactive >"${idir}"/"${iface}".xml
    done

    IFS=${old_IFS}

    echo "${idir}"
}

define_domains() {
    ddir=$1

    if [ ! -d "${ddir}" ]; then
        echo "${ddir}: No such directory"
        return 1
    fi

    ls -1 "${ddir}"/*.xml >/dev/null 2>&1
    if [ $? != 0 ]; then
        return 0
    fi

    old_IFS=${IFS}
    IFS='
'

    for domain in "${ddir}"/*.xml; do
        virsh define "${domain}"
        if [ $? != 0 ]; then
            rc=$?
            IFS=${old_IFS}
            return ${rc}
        fi
    done

    IFS=${old_IFS}
}

define_networks() {
    ndir=$1

    if [ ! -d "${ndir}" ]; then
        echo "${ndir}: No such directory"
        return 1
    fi

    ls -1 "${ndir}"/*.xml >/dev/null 2>&1
    if [ $? != 0 ]; then
        return 0
    fi

    old_IFS=${IFS}
    IFS='
'

    for network in "${ndir}"/*.xml; do
        virsh net-define "${network}"
        if [ $? != 0 ]; then
            rc=$?
            IFS=${old_IFS}
            return ${rc}
        fi
    done

    IFS=${old_IFS}
}

define_ifaces() {
    idir=$1

    if [ ! -d "${idir}" ]; then
        echo "${idir}: No such directory"
        return 1
    fi

    ls -1 "${idir}"/*.xml >/dev/null 2>&1
    if [ $? != 0 ]; then
        return 0
    fi

    old_IFS=${IFS}
    IFS='
'

    for iface in "${idir}"/*.xml; do
        virsh iface-define "${iface}"
        if [ $? != 0 ]; then
            rc=$?
            IFS=${old_IFS}
            return ${rc}
        fi
    done

    IFS=${old_IFS}
}

delete_libvirt_domains() {
    for domain in $(virsh list --all --name); do
        virsh undefine "${domain}" >/dev/null
    done
}

delete_libvirt_networks() {
    for network in $(virsh net-list --all --name | grep -v "^default"); do
        virsh net-undefine "${network}" >/dev/null
    done
}

delete_libvirt_ifaces() {
    for iface in $(virsh iface-list --all | grep "^[[:space:]]*bridge" | awk '{print $1}' | grep -v "^bridge0"); do
        virsh iface-undefine "${iface}" >/dev/null
    done
}

check_disk_space() {
    source=$1
    target=$2

    if [ -z "${source}" ] && [ $# = 3 ]; then
        total_needed=$3
    else
        total_needed=$(du -B1 -sc ${source} | grep total | sed -E -s 's|\s+total||')
    fi

    total_available=$(df -B1 --output=avail "${target}" | grep -E '[0-9]')

    if [ "${total_needed}" -gt "${total_available}" ]; then
        echo "Insufficient disk space required in ${target}; ${total_needed} bytes required but only ${total_available} bytes available."
        return 1
    fi

    return 0
}

wait_for_vms_to_stop() {
    # this is only needed if VMs are running on the
    # controller where the ISO is mounted.
    loops=5 # max 5x5 = 25s
    done=0
    while ((loops > 0 && !done)); do
        done=1
        for vm in $(virsh -c qemu:///system list --all --name); do
            if [[ "$vm" =~ ^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12} ]]; then
                vm_state=$(virsh dominfo "$vm")
                if [[ "$vm_state" =~ State:\ +running ]]; then
                    echo "still running: $vm"
                    done=0
                fi
            fi
        done
        ((loops -= 1))
        ((!done)) && sleep 5
    done
    if ((!done)); then
        echo "Some labs are still running; please stop all labs before continuing"
        return 1
    fi
}

stop_cml_services() {
    #wait_for_vms_to_stop
    rc=$?
    if [ ${rc} != 0 ]; then
        return ${rc}
    fi
    systemctl stop virl2.target
    rc=$?
    if [ ${rc} != 0 ]; then
        return ${rc}
    fi

    echo "Waiting for controller to become inactive..."
    while [ "$(systemctl is-active virl2-controller)" = "active" ]; do
        echo "  Still active..."
        sleep 5
    done
    return ${rc}
}

restart_cml_services() {
    echo "Restarting CML services..."
    systemctl start virl2.target
}

delete_local_dirs() {
    dirs=$1

    from=""
    if [ $# -ge 2 ]; then
        from=$2
    fi

    for dir in ${dirs}; do
        echo "Deleting local directory ${from}${dir}"
        rm -rf "${from}""${dir}"
        if [ $# -lt 3 ]; then
            echo "Creating directory ${from}${dir}"
            mkdir -p "${from}""${dir}"
        fi
    done
}

generate_ssh_key() {
    # Generate an SSH key we can use to avoid re-prompting for a password.
    tempd=$(mktemp -d /tmp/migration.XXXXX)
    ssh-keygen -b 4096 -q -N "" -f "${tempd}"/${KEY_FILE}
    echo "${tempd}"
}

cleanup_from_host() {
    key_dir=$1

    rm -rf "${key_dir}"
    restart_cml_services
}

check_cml_versions() {
    curr_ver=$1
    src_ver=$2

    if [ "${curr_ver}" = "${src_ver}" ]; then
        return 0
    fi

    # Allow one to migrate from 2.2.x to 2.3+.
    if echo "${curr_ver}" | grep -qE '^2\.[3-9]\.[0-9]' && echo "${src_ver}" | grep -qE '^2\.2\.'; then
        DOING_MIGRATION=1
        return 0
    fi

    return 1
}

sync_from_host() {
    host=$1
    host_ip="${host}"
    # used in scp/rsync commands to support ipv6
    scp_rsync_host="${host}"
    if echo "${host_ip}" | grep -qE '[a-zA-Z]'; then
        host_ip=$(host -t A "${host_ip}" | grep -oE '[0-9][0-9.]+')
        if [ $? != 0 ]; then
            echo "Failed to lookup host ${host}.  Please specify a valid IP or hostname."
            return 1
        fi
    elif echo "${host_ip}" | grep -qE '([0-9a-fA-F:]{1,5}){3,8}'; then
        scp_rsync_host=\["${host}"\]
    fi
    if ip addr show | grep -q "inet6* ${host_ip}/"; then
        echo "You are trying to migrate from the local host.  Please specify another host from which to migrate."
        return 1
    fi

    if ! nc -z "${host}" ${SSH_PORT}; then
        echo "Remote host does not have OpenSSH enabled; start the OpenSSH service from Cockpit on the source host."
        return 1
    fi

    if [ ${NO_CONFIRM} -eq 0 ]; then
        read -r -p "Do you wish to import data from ${host}? [y/N] " confirm
        echo
        if echo "${confirm}" | grep -viq '^y'; then
            echo "Terminating import."
            return 0
        fi
    fi

    stop_cml_services || (
        echo "Failed to stop CML services."
        exit $?
    )
    delete_libvirt_domains
    delete_libvirt_networks
    delete_libvirt_ifaces

    key_dir=$(generate_ssh_key)
    key=$(cat "${key_dir}"/${KEY_FILE}.pub)

    printf "\nThe next prompt will be for %s's password on %s.\n" "${sysadmin_user}" "${host}"
    printf "The prompt following that will be for %s's password on %s to enter sudo mode.\n\n" "${sysadmin_user}" "${host}"
    # Stop the service on the remote host and make sure we don't need
    # a password for sudo.  We also install the SSH pubkey for subsequent logins.
    if ! ssh -o "StrictHostKeyChecking=no" -t -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "echo '%${sysadmin_user}        ALL=(ALL)       NOPASSWD: ALL' | sudo tee /etc/sudoers.d/cml-migrate >/dev/null 2>&1 && mkdir -p ~/.ssh && \
        chmod 0700 ~/.ssh && (cp -fa ~/.ssh/authorized_keys ~/.ssh/authorized_keys.migration >/dev/null 2>&1 || true) && echo ${key} | tee -a ~/.ssh/authorized_keys >/dev/null 2>&1 && chmod 0600 ~/.ssh/authorized_keys"; then
        rc=$?
        cleanup_from_host "${key_dir}"
        echo "Error preparing ${host} for migration."
        return ${rc}
    fi

    # Install this script on the remote host.
    if ! scp -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -P ${SSH_PORT} "${LOCAL_ME}" "${sysadmin_user}"@"${scp_rsync_host}":"/tmp/${ME}" >/dev/null; then
        rc=$?
        cleanup_from_host "${key_dir}"
        echo "Error copying /usr/local/bin/${ME} to source host."
        return ${rc}
    fi

    # Stop CML services on remote host.
    if ! ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo chmod +x /tmp/${ME} && sudo /tmp/${ME} --stop" 2>/dev/null; then
        rc=$?
        cleanup_from_host "${key_dir}"
        echo "Failed to stop source CML services."
        return ${rc}
    fi

    trap cleanup_failed_from_host SIGINT

    # Check CML versions on both hosts.
    output=$( (ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --cml-version") 2>/dev/null)
    if [ $? != 0 ]; then
        rc=$?
        cleanup_from_host "${key_dir}"
        echo "Failed to determine remote CML version."
        return ${rc}
    fi

    if ! check_cml_versions ${VIRL_VERSION} ${output}; then
        cleanup_from_host "${key_dir}"
        echo "Migration between versions is not supported.  Source server version: ${output}, Dest server version: ${VIRL_VERSION}."
        return 1
    fi

    if [ ${DOING_MIGRATION} -eq 1 ]; then
        (ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --mount-overlay") 2>/dev/null
        if [ $? != 0 ]; then
            rc=$?
            cleanup_from_host "${key_dir}"
            return ${rc}
        fi
    fi

    migr_arg=""
    if [ ${DOING_MIGRATION} -eq 1 ]; then
        migr_arg="--migration"
    fi
    # Build the list of remote src dirs.
    output=$( (ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} ${migr_arg} --src-dirs") 2>/dev/null)
    if [ $? != 0 ]; then
        rc=$?
        cleanup_from_host "${key_dir}"
        echo "Failed to obtain remote src dirs."
        return ${rc}
    fi

    SRC_DIRS=${output}
    delete_local_dirs "${SRC_DIRS}"

    # Get the list of domains from virsh.
    libvirt_domains=$( (ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --get-domains") 2>/dev/null)
    if [ $? != 0 ]; then
        rc=$?
        cleanup_from_host "${key_dir}"
        echo "Failed to get libvirt domains from ${host}: ${libvirt_domains}"
        return ${rc}
    fi

    # Get the list of networks from virsh
    libvirt_networks=$( (ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --get-networks") 2>/dev/null)
    if [ $? != 0 ]; then
        rc=$?
        cleanup_from_host "${key_dir}"
        echo "Failed to get libvirt networks from ${host}: ${libvirt_networks}"
        return ${rc}
    fi

    # Get the list of ifaces from virsh
    libvirt_ifaces=$( (ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --get-ifaces") 2>/dev/null)
    if [ $? != 0 ]; then
        rc=$?
        cleanup_from_host "${key_dir}"
        echo "Failed to get libvirt ifaces from ${host}: ${libvirt_ifaces}"
        return ${rc}
    fi

    SRC_DIRS="${SRC_DIRS} ${libvirt_domains} ${libvirt_networks} ${libvirt_ifaces}"

    echo "Migrating ${SRC_DIRS} to this CML server..."
    space_dirs=""
    if [ ${DOING_MIGRATION} -eq 1 ]; then
        for map_dir in ${MIGRATION_MAP}; do
            space_dirs="${space_dirs} $(echo "${map_dir}" | cut -d':' -f1)"
        done
    fi

    # Get required disk space from remote host
    output=$( (ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo du -B1 -sc ${SRC_DIRS} ${space_dirs} | grep total | sed -E -s 's|\s+total||'") 2>/dev/null)
    if check_disk_space "" ${BASE_DIR} "${output}"; then
        printf "Starting migration.  Please be patient, migration may take a while....\n\n\n"
        sdirs=""
        for src_dir in ${SRC_DIRS}; do
            sdirs="${sdirs} ${sysadmin_user}"@"${scp_rsync_host}":"${src_dir}"
        done
        rsync -aAvzXR --progress --checksum --rsync-path="sudo rsync" --exclude="*.rej" --exclude="*.orig" -e "ssh -o StrictHostKeyChecking=no -i ${key_dir}/${KEY_FILE} -p ${SSH_PORT}" ${sdirs} /
        #        output=$( (ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} sysadmin@"${host}" "sudo tar --acls --selinux -cpf - ${SRC_DIRS}" | tar -C / --acls --selinux -xpf -) 2>&1 )
        rc=$?
        if [ ${rc} != 0 ]; then
            echo "Migration completed with errors.  See the output above for details."
        else
            # Migrate each mapped src dir to its target dir.
            success=1
            if [ ${DOING_MIGRATION} -eq 1 ]; then
                for map_dir in ${MIGRATION_MAP}; do
                    src_dir=${sysadmin_user}@${scp_rsync_host}:$(echo "${map_dir}" | cut -d':' -f1)
                    dest_dir=$(dirname $(echo "${map_dir}" | cut -d':' -f2))
                    delete_local_dirs "$(echo "${map_dir}" | cut -d':' -f2)"
                    rsync -aAvzX --progress --checksum --rsync-path="sudo rsync" --exclude="*.rej" --exclude="*.orig" -e "ssh -o StrictHostKeyChecking=no -i ${key_dir}/${KEY_FILE} -p ${SSH_PORT}" "${src_dir}" "${dest_dir}"
                    rc=$?
                    if [ ${rc} != 0 ]; then
                        echo "Migration completed with errors.  See the output above for details."
                        success=0
                        break
                    fi
                done
            fi
            if [ ${success} = 1 ]; then
                if [ ${DOING_MIGRATION} -eq 0 ]; then
                    # For each of the libvirt domains, migrate the XML.
                    echo "Migrating libvirt domains..."
                    output=$(define_domains "${libvirt_domains}" 2>&1)
                    rc=$?
                    echo "Libvirt domain output is '${output}'"
                    if [ ${rc} != 0 ]; then
                        echo "Libvirt domain import completed with errors:"
                        printf '%s\n\n' "${output}"
                    else
                        # For each of the libvirt networks, migrate the XML.
                        echo "Migrating libvirt networks..."
                        output=$(define_networks "${libvirt_networks}" 2>&1)
                        rc=$?
                        echo "Libvirt network output is '${output}'"
                        if [ ${rc} != 0 ]; then
                            echo "Libvirt network import completed with errors:"
                            printf '%s\n\n' "${output}"
                        else
                            # For each of the libvirt ifaces, migrate the XML.
                            echo "Migrating libvirt ifaces..."
                            output=$(define_ifaces "${libvirt_ifaces}" 2>&1)
                            rc=$?
                            echo "Libvirt iface output is '${output}'"
                            if [ ${rc} != 0 ]; then
                                echo "Libvirt network import completed with errors:"
                                printf '%s\n\n' "${output}"
                            else
                                echo "Migration completed SUCCESSFULLY."
                                echo "Please make sure you have either mounted the same refplat ISO on or copied its contents to this CML server."
                            fi
                        fi
                    fi
                else
                    printf "\nData transfer completed SUCCESSFULLY.\n"
                    echo "Performing configuration migration..."
                    output=$( (cd "${BASE_DIR}"/db_migrations && env CFG_DIR="${CFG_DIR}" BASE_DIR="${BASE_DIR}" VIRL2_DIR="${BASE_DIR}" HOME="${BASE_DIR}" MIGRATION_LIBVIRT_DOM_XML_DIR="${libvirt_domains}" MIGRATION_LIBVIRT_NET_XML_DIR="${libvirt_networks}" MIGRATION_LIBVIRT_IF_XML_DIR="${libvirt_ifaces}" COMPUTE_ID="${COMPUTE_ID}" MIGRATION=1 HOSTNAME="${HOSTNAME}" "${BASE_DIR}"/.local/bin/alembic upgrade head) 2>&1)
                    rc=$?
                    # This feels hacky, but we need to do it.
                    chown -R www-data:www-data "${CFG_DIR}"
                    find "${BASE_DIR}"/images -type f \( -name "*.img" -or -name "nodedisk*" \) -print0 | xargs -0 chown libvirt-qemu:kvm 2>/dev/null
                    if [ ${rc} != 0 ]; then
                        echo "Failed to execute data upgrade script:"
                        printf '%s\n\n' "${output}"
                    else
                        echo "Migration completed SUCCESSFULLY."
                    fi
                fi
            fi
        fi
    else
        rc=$?
    fi

    printf "\nFinishing up with the remote host..."
    if [ ${DOING_MIGRATION} -eq 1 ]; then
        ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --umount-overlay"
    fi

    if ! ssh -o "StrictHostKeyChecking=no" -i "${key_dir}"/${KEY_FILE} -p ${SSH_PORT} "${sysadmin_user}"@"${host}" "sudo /tmp/${ME} --start && sudo rm -rf ${libvirt_domains} && sudo rm -rf ${libvirt_networks} && sudo rm -rf ${libvirt_ifaces} && sudo rm -f /tmp/${ME} && sudo rm -f /etc/sudoers.d/cml-migrate && (cp -fa ~/.ssh/authorized_keys.migration ~/.ssh/authorized_keys >/dev/null 2>&1 || true)"; then
        printf "FAILED.\n"
        echo "Error finishing up on remote host.  Check to make sure the CML services are running on ${host}."
        rc=$?
    else
        printf "DONE.\n"
    fi

    rm -rf "${key_dir}"
    rm -rf "${libvirt_domains}"
    rm -rf "${libvirt_networks}"
    rm -rf "${libvirt_ifaces}"

    restart_cml_services

    return ${rc}
}

ME=$(basename "$0")
LOCAL_ME=$0
set_virl_version

if [ "$EUID" != 0 ]; then
    echo "This script must be run as root.  Use 'sudo' to run it."
    exit 1
fi

opts=$(getopt -o brf:h:vdu: --long host:,file:,backup,restore,cml-version,src-dirs,stop,start,get-domains,get-networks,get-ifaces,mount-overlay,umount-overlay,migration,no-confirm,user,version,update -- "$@")
if [ $? != 0 ]; then
    echo "usage: $0 -h|--host HOST_TO_MIGRATE_FROM"
    echo "       OR"
    echo "       $0 -b|--backup|-r|--restore -f|--file PATH_TO_BACKUP_FILE"
    exit 1
fi

REMOTE_HOST=
BACKUP_FILE=
BACKUP=0
RESTORE=0
GET_SRC_DIRS=0
NO_CONFIRM=0

sysadmin_user=${DEFAULT_USER}

eval set -- "$opts"
while true; do
    case "$1" in
    -h | --host)
        shift
        REMOTE_HOST=$1
        ;;
    -f | --file)
        shift
        BACKUP_FILE=$1
        ;;
    -b | --backup)
        BACKUP=1
        ;;
    -m | --migration)
        DOING_MIGRATION=1
        ;;
    -r | --restore)
        RESTORE=1
        ;;
    --cml-version)
        echo ${VIRL_VERSION}
        exit 0
        ;;
    -u | --user)
        shift
        sysadmin_user=$1
        ;;
    -d | --src-dirs)
        GET_SRC_DIRS=1
        ;;
    --stop)
        stop_cml_services
        exit $?
        ;;
    --start)
        restart_cml_services
        exit $?
        ;;
    --get-domains)
        export_libvirt_domains
        exit $?
        ;;
    --get-networks)
        export_libvirt_networks
        exit $?
        ;;
    --get-ifaces)
        export_libvirt_ifaces
        exit $?
        ;;
    --mount-overlay)
        mount_refplat_overlay
        exit $?
        ;;
    --umount-overlay)
        umount_refplat_overlay
        exit $?
        ;;
    --no-confirm)
        NO_CONFIRM=1
        ;;
    -v | --version)
        echo "${_VERSION}"
        exit 0
        ;;
    --update)
        check_for_updates
        exit $?
        ;;
    --)
        shift
        break
        ;;
    esac
    shift
done

if [ ${GET_SRC_DIRS} -eq 1 ]; then
    build_local_src_dirs
    echo ${SRC_DIRS}
    exit 0
fi

if [ -n "${REMOTE_HOST}" ] && [ -n "${BACKUP_FILE}" ]; then
    echo "Only one of --host or --file may be specified."
    exit 1
fi

if [ -z "${REMOTE_HOST}" ] && [ -z "${BACKUP_FILE}" ]; then
    echo "One of --host or --file must be specified."
    exit 1
fi

if [ -n "${REMOTE_HOST}" ]; then
    sync_from_host "${REMOTE_HOST}"
    exit $?
fi

if [ ${BACKUP} = 1 ] && [ ${RESTORE} = 1 ]; then
    echo "Only one of --backup or --restore can be specified."
    exit 1
fi

if [ ${BACKUP} = 0 ] && [ ${RESTORE} = 0 ]; then
    echo "One of --backup or --restore must be specified."
    exit 1
fi

if [ ${RESTORE} = 1 ]; then
    if [ ! -f "${BACKUP_FILE}" ]; then
        echo "Backup file ${BACKUP_FILE} does not exist or is not a file."
        exit 1
    fi

    DOING_MIGRATION=0

    # While not all data may go to the product root, the way CML's file system
    # is laid out means it's almost certainly going to be on the same FS.
    if ! check_disk_space "${BACKUP_FILE}" ${BASE_DIR}; then
        exit $?
    fi

    if [ ${NO_CONFIRM} -eq 0 ]; then
        read -r -p "Do you wish to restore from ${BACKUP_FILE}?  This will overwrite current local data. [y/N] " confirm
        echo
        if echo "${confirm}" | grep -qiv '^y'; then
            echo "Terminating restore."
            exit 0
        fi
    fi

    # First, extract /PRODUCT from the backup and check that the version matches the current version.
    tempd=$(mktemp -d /tmp/migration.XXXXX)
    output=$(tar -C "${tempd}" --acls --selinux -xpf "${BACKUP_FILE}" PRODUCT libvirt_domains.dat libvirt_networks.dat libvirt_ifaces.dat 2>&1)
    rc=$?
    if [ ${rc} != 0 ]; then
        echo "Failed to extract /PRODUCT and libvirt data from backup:"
        printf '%s\n\n' "${output}"
        exit ${rc}
    fi

    virl_version=$(set_virl_version "${tempd}"/PRODUCT)
    # This will set DOING_MIGRATION if needed.
    if ! check_cml_versions ${VIRL_VERSION} "${virl_version}"; then
        rm -rf "${tempd}"
        echo "Versions do not match or migration path not supported.  Source server version: ${virl_version}, Dest server version: ${VIRL_VERSION}."
        exit 1
    fi

    ddir=$(cat "${tempd}"/libvirt_domains.dat)
    ndir=$(cat "${tempd}"/libvirt_networks.dat)
    idir=$(cat "${tempd}"/libvirt_ifaces.dat)

    rm -rf "${tempd}"

    stop_cml_services || (
        echo "Failed to stop CML services."
        exit $?
    )
    delete_libvirt_domains
    delete_libvirt_networks
    delete_libvirt_ifaces

    trap cleanup_failed_restore SIGINT

    echo "Restoring ${BACKUP_FILE} to local CML.  Please be patient, this may take a while..."
    sdirs=""
    if [ ${DOING_MIGRATION} -eq 1 ]; then
        for sdir in ${SRC_DIRS}; do
            # Remove the leading '/' to match what's in the tar file.
            sdirs="${sdirs} $(echo "${sdir}" | cut -d'/' -f2-)"
        done
        # Add the directory that contains the libvirt domains.
        sdirs="${sdirs} $(echo "${ddir}" | cut -d'/' -f2-)"
        # Add the directory that contains the libvirt networks.
        sdirs="${sdirs} $(echo "${ndir}" | cut -d'/' -f2-)"
        # Add the directory that contains the libvirt ifaces.
        sdirs="${sdirs} $(echo "${idir}" | cut -d'/' -f2-)"
        echo "Extracting ${sdirs} from the backup..."
    fi

    delete_local_dirs "$(tar -tf "${BACKUP_FILE}" | grep '/$')" "/" 0
    export total=$(du -shc ${BACKUP_FILE} -B10k --apparent-size | tail -1 | cut -f1)
    tar -C / --acls --selinux --checkpoint=2000 --checkpoint-action=exec=' printf "\e[1;31m%s of %s extracted  %d/100%% complete  \e[0m\r" $(numfmt --to=iec-i $((10000*${TAR_CHECKPOINT})) ) $(numfmt --to=iec-i $((10000*${total})) )\t$((100*${TAR_CHECKPOINT}/${total})) ' --exclude=PRODUCT --exclude="libvirt*.dat" -xvpf "${BACKUP_FILE}"
    rc=$?
    if [ ${rc} != 0 ]; then
        echo "Restore failed with error.  See output above for the error details."
    else
        if [ ${DOING_MIGRATION} -eq 1 ]; then
            printf "\nRestore completed SUCCESSFULLY.\n"
            echo "Performing configuration migration..."
            output=$( (cd "${BASE_DIR}"/db_migrations && env CFG_DIR="${CFG_DIR}" BASE_DIR="${BASE_DIR}" VIRL2_DIR="${BASE_DIR}" HOME="${BASE_DIR}" MIGRATION_LIBVIRT_DOM_XML_DIR="${ddir}" MIGRATION_LIBVIRT_NET_XML_DIR="${ndir}" MIGRATION_LIBVIRT_IF_XML_DIR="${idir}" COMPUTE_ID="${COMPUTE_ID}" MIGRATION=1 HOSTNAME="${HOSTNAME}" "${BASE_DIR}"/.local/bin/alembic upgrade head) 2>&1)
            rc=$?
            # This feels hacky, but we need to do it.
            chown -R www-data:www-data "${CFG_DIR}"
            find "${BASE_DIR}"/images -type f \( -name "*.img" -or -name "nodedisk*" \) -print0 | xargs -0 chown libvirt-qemu:kvm 2>/dev/null
            if [ ${rc} != 0 ]; then
                echo "Failed to execute data upgrade script:"
                printf '%s\n\n' "${output}"
            else
                echo "Migration completed SUCCESSFULLY."
            fi
        else
            output=$(define_domains "${ddir}" 2>&1)
            rc=$?
            if [ ${rc} != 0 ]; then
                echo "Libvirt domain import completed with errors:"
                printf '%s\n\n' "${output}"
            else
                output=$(define_networks "${ndir}" 2>&1)
                rc=$?
                if [ ${rc} != 0 ]; then
                    echo "Libvirt network import completed with errors:"
                    printf '%s\n\n' "${output}"
                else
                    output=$(define_ifaces "${idir}" 2>&1)
                    rc=$?
                    if [ ${rc} != 0 ]; then
                        echo "Libvirt iface import completed with errors:"
                        printf '%s\n\n' "${output}"
                    else
                        echo "Restore completed SUCCESSFULLY."
                        echo "Please make sure you have either mounted the same refplat ISO on or copied its contents to this CML server."
                    fi
                fi
            fi
        fi
    fi

    rm -rf "${ddir}"
    rm -rf "${ndir}"
    rm -rf "${idir}"

    restart_cml_services

    exit ${rc}
fi

if [ ${DOING_MIGRATION} -eq 0 ]; then
    build_local_src_dirs
else
    for mig_dir in ${MIGRATION_MAP}; do
        SRC_DIRS="${SRC_DIRS} $(echo "${mig_dir}" | cut -d':' -f1)"
    done
fi

BACKUP_FILE=$(realpath "${BACKUP_FILE}")

# We are doing a dump to a single tar file.
if ! check_disk_space "${SRC_DIRS}" "$(dirname "${BACKUP_FILE}")"; then
    exit $?
fi

if [ ${NO_CONFIRM} -eq 0 ]; then
    read -r -p "Are you sure you want to backup?  Doing so will restart the CML services. [y/N] " confirm
    echo
    if echo "${confirm}" | grep -qiv '^y'; then
        echo "Terminating backup."
        exit 0
    fi
fi

# Before stopping the services, fetch the diagnostics and put it in a folder to include in the backup.
/usr/bin/curl -sk ip6-localhost:8001/api/internal/diagnostics > $CFG_DIR/diagnostics-$TIMESTAMP.log

stop_cml_services || (
    echo "Failed to stop CML services."
    exit $?
)

trap cleanup_backup SIGINT

if [ ${DOING_MIGRATION} -eq 1 ]; then
    mount_refplat_overlay
    if [ $? != 0 ]; then
        exit $?
    fi
fi

ddir=$(export_libvirt_domains)
ndir=$(export_libvirt_networks)
idir=$(export_libvirt_ifaces)
tempd=$(mktemp -d /tmp/migration.XXXXX)
cd "${tempd}" || (
    echo "Failed to cd to ${tempd}"
    exit $?
)
echo "${ddir}" >libvirt_domains.dat
echo "${ndir}" >libvirt_networks.dat
echo "${idir}" >libvirt_ifaces.dat

# Try to preserve some info about the server's network configuration.
export_network_configuration

SRC_DIRS="${SRC_DIRS} ${ddir} ${ndir} ${idir}"

echo "Backing up ${SRC_DIRS}..."

echo "Backing up CML data to ${BACKUP_FILE}.  Please be patient, this may take a while..."
export total=$(du -shc /PRODUCT ${SRC_DIRS} libvirt_domains.dat libvirt_networks.dat libvirt_ifaces.dat -B10k --apparent-size | tail -1 | cut -f1)
tar -C "${tempd}" --acls --selinux --checkpoint=2000 --exclude="*.rej" --exclude="*.orig" --checkpoint-action=exec=' printf "\e[1;31m%s of %s copied  %d/100%% complete  \e[0m\r" $(numfmt --to=iec-i $((10000*${TAR_CHECKPOINT})) ) $(numfmt --to=iec-i $((10000*${total})) )\t$((100*${TAR_CHECKPOINT}/${total})) ' -cvpf "${BACKUP_FILE}" /PRODUCT libvirt_domains.dat libvirt_networks.dat libvirt_ifaces.dat ${SRC_DIRS}
rc=$?
echo
if [ ${rc} != 0 ]; then
    echo "Backup completed with errors.  See the output above for the error details."
else
    echo "Backup completed SUCCESSFULLY.  Backup file is ${BACKUP_FILE}."
fi

cleanup_backup
