#!/bin/bash

set -o pipefail

VERSION=4.1.1
API_SERVER='proxy.timmehosting.de'
ANSIBLE_SERVER='tf1.meinserver.io'

export SESSION="${SESSION:-kvmmove_$(date +%Y%m%d%H%M%S)_${RANDOM}}"

DEBUG_LOG="/tmp/${SESSION}_debug.log"
TERM_LOG="/tmp/${SESSION}_term.log"
VM_XML="/tmp/${SESSION}_libvirt.xml"

SSH="ssh -o BatchMode=yes -o ControlMaster=auto -o ControlPath=/tmp/${SESSION}_%h.sock -o ControlPersist=yes -o LogLevel=error -o ServerAliveCountMax=30 -o ServerAliveInterval=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"

export MULTIPLEXER="${MULTIPLEXER:-0}"
ARGS="${*}"

[[ ${MULTIPLEXER} -ne 1 ]] || {
    exec 3>>"${DEBUG_LOG}"
    export BASH_XTRACEFD=3
    set -x
}

_prnt() {
    printf '%s\n' "${1}"
}

_ul() {
    printf '\033[4m%s\033[0m' "${1}"
}

_info() {
    printf 'INFO: %s\n' "${1}"
}

_warn() {
    printf '\033[0;92mWARN:\033[0m %s\n' "${1}" >&2
}

_erro() {
    printf '\033[0;91mERROR:\033[0m %s\n' "${1}" >&2
}

_dbug() {
    printf '\nDEBUG:\n' >&2
    while (($#)); do
        printf ' %s\n' "${1}" >&2
        shift
    done
    printf '\n' >&2
}

_ask() {
    [[ -z ${2:-} ]] || printf '?: %s\n' "${2}"
    case ${1} in
    --read)
        read -rp '=> '
        ;;
    --continue)
        read -rsp $'?: Press ENTER to continue or CTRL+C to exit.\n'
        read -rsp $'?: Are you sure?\n'
        ;;
    *)
        return 1
        ;;
    esac
}

_panic() {
    local OUT
    _erro "(${1}) Manual intervention required, for more info visit:"
    _erro "https://kb.timmehosting.de/books/kvm/page/kvmmove#bkmrk-${1}"
    read -rep "?: Open a GitLab issue? ($(_ul y)es/$(_ul n)o) [no]: "
    [[ ! $(_format --graph <<<"${REPLY}") =~ ^y(es)?$ ]] || {
        _info 'Opening GitLab issue...'
        ${SSH/#ssh/scp} "${DEBUG_LOG}" "${TERM_LOG}" "${USR}@${API_SERVER}:/tmp/" &>/dev/null
        OUT=$( (${SSH} "${USR}@${API_SERVER}" "sudo /opt/kvmmove/./report.sh ${USR} ${VM_FQDN} ${SOURCE_HOST_FQDN} ${TARGET_HOST_FQDN} ${1} ${DEBUG_LOG} ${TERM_LOG}") 2>&1) || {
            _dbug "${OUT}"
            _erro 'Failed to open GitLab issue. Please open one manually.'
            OUT=
        }
        [[ -z ${OUT} ]] || _info "Issue opened: ${OUT}"
    }
    _ask --continue
}

_qa_exec() {
    local OUT VAR CMD TRY=0
    CMD=$(jq -cn --arg cmd "${3}" '{"execute":"guest-exec","arguments":{"path":"/bin/bash","arg":["-c",$cmd],"capture-output":true}}')
    case ${1} in
    --local)
        VAR=""
        ;;
    --remote)
        VAR="${SSH} ${USR}@${TARGET_HOST_FQDN} "
        CMD=$(printf '%q' "${CMD}")
        ;;
    *)
        return 1
        ;;
    esac
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo virsh qemu-agent-command --block --domain "${2}" --cmd "${CMD}") 2>&1) || {
        _prnt "${OUT}"
        return 1
    }
    CMD=$(jq -cn --argjson pid "$(jq -er '.return.pid' <<<"${OUT}")" '{"execute":"guest-exec-status","arguments":{"pid":$pid}}')
    [[ -z ${VAR} ]] || CMD=$(printf '%q' "${CMD}")
    while true; do
        ((TRY++))
        [[ ${TRY} -ge 60 ]] || {
            # shellcheck disable=SC2086
            OUT=$( (${VAR}sudo virsh qemu-agent-command --block --domain "${2}" --cmd "${CMD}") 2>&1) || {
                _prnt "${OUT}"
                return 1
            }
            [[ $(jq -er '.return.exited' <<<"${OUT}") != 'true' ]] || break
            sleep 1
            continue
        }
        _prnt "${OUT}"
        return 1
    done
    jq -er '.return["out-data"] // empty' <<<"${OUT}" | base64 -d
    jq -er '.return["err-data"] // empty' <<<"${OUT}" | base64 -d >&2
    return "$(jq -er '.return.exitcode' <<<"${OUT}")"
}

_format() {
    case ${1} in
    --num)
        tr -dc '[:digit:]'
        ;;
    --graph)
        tr -dc '[:graph:]'
        ;;
    *)
        return 1
        ;;
    esac
}

_conv() {
    local NUM
    case ${1} in
    --nmft)
        numfmt --from=iec
        ;;
    --byte-stg)
        read -r NUM
        printf '%s' "$((NUM * 1073741824))"
        ;;
    --gb-stg)
        read -r NUM
        printf '%s' "$((NUM / 1073741824))"
        ;;
    --byte-ram)
        read -r NUM
        printf '%s' "$((NUM * 1048576000))"
        ;;
    --gb-ram)
        read -r NUM
        printf '%s' "$((NUM / 1048576000))"
        ;;
    --downgrade)
        read -r NUM
        printf '%s' "$((NUM * 1000 - 100))"
        ;;
    *)
        return 1
        ;;
    esac
}

_head() {
    clear
    _prnt "
╔╦╗╦ ╦  ┬┌─┬  ┬┌┬┐┌┬┐┌─┐┬  ┬┌─┐
 ║ ╠═╣  ├┴┐└┐┌┘│││││││ │└┐┌┘├┤
 ╩ ╩ ╩  ┴ ┴ └┘ ┴ ┴┴ ┴└─┘ └┘ └─┘${VERSION}

https://kb.timmehosting.de/books/kvm/page/kvmmove
"
}

_help() {
    _prnt '
kvmmove [flag]

  flags:
    -m | --move             Move a VM to another host without making any changes
    -v | --version          Print version number
    -h | --help             Print help
'
}

_captcha() {
    local -A FONT=(
        [0]='OOOO|O  O|O  O|O  O|OOOO' [1]=' O  |OO  | O  | O  |OOOO'
        [2]='OOOO|   O|OOOO|O   |OOOO' [3]='OOOO|   O|OOOO|   O|OOOO'
        [4]='O  O|O  O|OOOO|   O|   O' [5]='OOOO|O   |OOOO|   O|OOOO'
        [6]='OOOO|O   |OOOO|O  O|OOOO' [7]='OOOO|   O|  O | O  | O  '
        [8]='OOOO|O  O|OOOO|O  O|OOOO' [9]='OOOO|O  O|OOOO|   O|OOOO'
    )
    local NUM ROW DIGIT STR I NOISE
    NUM=$((RANDOM % 90 + 10))
    NOISE='.,:;~-=*'
    for ROW in 1 2 3 4 5; do
        printf '    '
        for DIGIT in "${NUM:0:1}" "${NUM:1:1}"; do
            printf -v STR '%-4s' "$(cut -d'|' -f"${ROW}" <<<"${FONT[$DIGIT]}")"
            for ((I = 0; I < ${#STR}; I++)); do
                [[ ${STR:I:1} == O ]] && printf '█' || printf '%s' "${NOISE:RANDOM%${#NOISE}:1}"
            done
            printf '  '
        done
        printf '\n'
    done
    printf '\n'
    while true; do
        read -rp '?: Type the number shown above to confirm: '
        [[ $(_format --num <<<"${REPLY}") == "${NUM}" ]] || {
            _warn 'Wrong! Try again.'
            continue
        }
        break
    done
}

_summary() {
    _prnt "
Username: ${USR}

# Host:
IP: ${SOURCE_HOST_IP} -> ${TARGET_HOST_IP}
FQDN: ${SOURCE_HOST_FQDN} -> ${TARGET_HOST_FQDN}

# VM:
IP: ${VM_IP}
FQDN: ${VM_FQDN}
Name (Auftragsnummer): ${SOURCE_ID} -> ${TARGET_ID}
Disk: ${SOURCE_STORAGE_H}G -> ${TARGET_STORAGE_H}G
RAM: ${SOURCE_RAM_H}G -> ${TARGET_RAM_H}G
vCPU: ${SOURCE_CPU} -> ${TARGET_CPU}
"
    read -rep "?: Continue ? ($(_ul y)es/$(_ul n)o) [no]: "
    [[ $(_format --graph <<<"${REPLY}") =~ ^y(es)?$ ]] || exit 151
    _prnt
    _prnt 'You are about to make changes that may be difficult/impossible to reverse.'
    _prnt 'The output above is your last chance to verify everything is correct.'
    _prnt 'PLEASE VERIFY EVERYTHING ABOVE BEFORE CONTINUING!'
    _prnt
    _prnt '- Does the output look OK or is it malformed?'
    _prnt '- Are there any error messages in the output?'
    _prnt '- Does each machine have only one IP?'
    _prnt '- Are the IPs correct and belong to the server in question?'
    _prnt '- Are the host/vm/disk/vcpu/ram values correct?'
    _prnt
    _prnt "IF ANYTHING LOOKS WRONG OR YOU'RE NOT SURE, DO NOT CONTINUE!!!"
    _prnt
    _captcha
    _prnt
}

_get_source_id() {
    sudo virsh list --all --title
    while true; do
        _ask --read 'Enter the current name (Auftragsnummer) of the VM.'
        REPLY=$(_format --num <<<"${REPLY}")
        sudo virsh list --name | grep -qw "${REPLY:-nonexistent123}" || {
            _warn 'The entry does not look right! Try again.'
            continue
        }
        SOURCE_ID=${REPLY}
        break
    done
}

_get_target_id() {
    while true; do
        _ask --read 'Enter the new name (Auftragsnummer) of the VM.'
        REPLY=$(_format --num <<<"${REPLY}")
        [[ ${#REPLY} -gt 1 && ${#REPLY} -le 7 ]] || {
            _warn 'The entry does not look right! Try again.'
            continue
        }
        TARGET_ID=${REPLY}
        break
    done
}

_get_target_host_fqdn() {
    while true; do
        _ask --read 'Enter the FQDN of the target (new) host.'
        REPLY=$(_format --graph <<<"${REPLY}")
        [[ ${REPLY} == *[.]**[.]* && -n $(getent ahostsv4 "${REPLY}") && ${REPLY} != "${SOURCE_HOST_FQDN}" ]] || {
            _warn 'The entry does not look right! Try again.'
            continue
        }
        TARGET_HOST_FQDN=${REPLY}
        break
    done
}

_get_target_storage() {
    while true; do
        _ask --read "Enter the target storage in GB. [${SOURCE_STORAGE_H}]"
        REPLY=$(_format --num <<<"${REPLY:-${SOURCE_STORAGE_H}}")
        [[ ${#REPLY} -ge 2 && ${#REPLY} -le 5 ]] || {
            _warn 'The entry does not look right! Try again.'
            continue
        }
        TARGET_STORAGE=$(_conv --byte-stg <<<"${REPLY}")
        TARGET_STORAGE_H=${REPLY}
        break
    done
}

_get_target_cpu() {
    while true; do
        _ask --read "Enter the target vCPU count. [${SOURCE_CPU}]"
        REPLY=$(_format --num <<<"${REPLY:-${SOURCE_CPU}}")
        [[ ${#REPLY} -ge 1 && ${#REPLY} -le 3 ]] || {
            _warn 'The entry does not look right! Try again.'
            continue
        }
        TARGET_CPU=${REPLY}
        break
    done
}

_get_target_ram() {
    while true; do
        _ask --read "Enter the target RAM in GB. [${SOURCE_RAM_H}]"
        REPLY=$(_format --num <<<"${REPLY:-${SOURCE_RAM_H}}")
        [[ ${#REPLY} -ge 1 && ${#REPLY} -le 3 ]] || {
            _warn 'The entry does not look right! Try again.'
            continue
        }
        TARGET_RAM=$(_conv --byte-ram <<<"${REPLY}")
        TARGET_RAM_H=${REPLY}
        break
    done
}

_get_timme_username() {
    USR=${SUDO_USER:-${USER}}
    grep -qE '^timmeg?[0-9]+$' <<<"${USR}" || {
        _erro 'Connect as timme-user and try again.'
        exit 151
    }
}

_get_source_host_fqdn() {
    SOURCE_HOST_FQDN=$( (hostname -f) 2>&1) || {
        _dbug "${SOURCE_HOST_FQDN}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_get_source_host_ip() {
    SOURCE_HOST_IP=$( (ip -j -4 route get 8.8.8.8 | jq -er '.[0].prefsrc') 2>&1) || {
        _dbug "${SOURCE_HOST_IP}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_get_source_storage() {
    SOURCE_STORAGE=$( (sudo qemu-img info --output=json "/dev/vgvm/${SOURCE_ID}-root" | jq -er '."virtual-size"') 2>&1) || {
        _dbug "${SOURCE_STORAGE}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
    SOURCE_STORAGE_H=$( (_conv --gb-stg <<<"${SOURCE_STORAGE}") 2>&1) || {
        _dbug "${SOURCE_STORAGE_H}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_get_target_host_ip() {
    TARGET_HOST_IP=$( (getent ahostsv4 "${TARGET_HOST_FQDN}" | awk 'NR==1{print $1}') 2>&1) || {
        _dbug "${TARGET_HOST_IP}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_get_vm_fqdn() {
    VM_FQDN=$( (sudo virsh desc --title --domain "${SOURCE_ID}" | awk '{print $3}') 2>&1) || {
        _dbug "${VM_FQDN}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_get_vm_ip() {
    VM_IP=$( (getent ahostsv4 "${VM_FQDN}" | awk 'NR==1{print $1}') 2>&1) || {
        _dbug "${VM_IP}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_get_source_cpu() {
    SOURCE_CPU=$( (sudo virsh dominfo --domain "${SOURCE_ID}" | awk '/CPU\(s\):/ {print $2}') 2>&1) || {
        _dbug "${SOURCE_CPU}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_get_source_ram() {
    SOURCE_RAM=$( (sudo virsh dominfo --domain "${SOURCE_ID}" | awk '/Max memory/ {print $3"K"}' | _conv --nmft) 2>&1) || {
        _dbug "${SOURCE_RAM}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
    SOURCE_RAM_H=$( (_conv --gb-ram <<<"${SOURCE_RAM}") 2>&1) || {
        _dbug "${SOURCE_RAM_H}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_get_downgrade_temp_size() {
    DOWNGRADE_TEMP_SIZE=$( (_conv --downgrade <<<"${TARGET_STORAGE_H}") 2>&1) || {
        _dbug "${DOWNGRADE_TEMP_SIZE}"
        _erro "Data gathering failed: ${FUNCNAME[0]}"
        exit 167
    }
}

_check_data() {
    local OUT
    _info 'Checking gathered data...'
    OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" "ip -j -4 route get 8.8.8.8 | jq -er '.[0].prefsrc'") 2>&1) || {
        _dbug "${OUT}"
        _erro 'Checking target host IP failed.'
        exit 167
    }
    [[ ${OUT} == "${TARGET_HOST_IP}" ]] || {
        _erro "Target host IP mismatch! Expected: ${TARGET_HOST_IP}, got: ${OUT}"
        exit 167
    }
    OUT=$( (_qa_exec --local "${SOURCE_ID}" "ip -j -4 route get 8.8.8.8 | jq -er '.[0].prefsrc'") 2>&1) || {
        _dbug "${OUT}"
        _erro 'Checking VM IP failed.'
        exit 167
    }
    [[ ${OUT} == "${VM_IP}" ]] || {
        _erro "VM IP mismatch! Expected: ${VM_IP}, got: ${OUT}"
        exit 167
    }
}

_start_multiplexer() {
    [[ ${MULTIPLEXER} -ne 1 ]] || return 0
    (
        umask 0077
        : >"${DEBUG_LOG}"
        : >"${TERM_LOG}"
    )
    while true; do
        read -rep "?: Which multiplexer to use? ($(_ul s)creen/$(_ul t)mux) [screen]: "
        REPLY=$(_format --graph <<<"${REPLY:-screen}")
        case ${REPLY} in
        screen | s)
            MULTIPLEXER=1 exec screen -L -Logfile "${TERM_LOG}" -S "${SESSION}" bash "${0}" "${ARGS}"
            ;;
        tmux | t)
            MULTIPLEXER=1 exec tmux new-session -s "${SESSION}" \; pipe-pane -o "cat >> ${TERM_LOG}" \; send-keys "bash ${0} ${ARGS}" Enter
            ;;
        *)
            _warn 'The entry does not look right! Try again.'
            continue
            ;;
        esac
    done
}

_check_ssh_agent() {
    ssh-add -l &>/dev/null || {
        _erro 'Connect using "ssh -A" and try again.'
        exit 151
    }
}

_get_root_partition_number() {
    _info "Determining the number of the VM's root partition..."
    PART_NUM=$( (_qa_exec --local "${SOURCE_ID}" "$(typeset -f _format); lsblk -Jpo PATH,MOUNTPOINT | jq -er --arg mp '/' '.blockdevices[] | select(.mountpoint == \$mp) | .path' | _format --num") 2>&1)
    [[ ${PART_NUM} =~ ^(2|3)$ ]] || {
        _dbug "${PART_NUM}"
        _panic 180
    }
    ! [[ ${PART_NUM} -eq 2 && ${TARGET_STORAGE_H} -ge 2000 ]] || {
        _erro "VMs with only two partitions and a size of ${TARGET_STORAGE_H}GB can be problematic."
        _erro 'Fixing this is not possible with kvmmove, for more info see:'
        _erro 'https://kb.timmehosting.de/books/kvm/page/kvm-umzug-mit-rsync-wechsel-2-partitionen-3-partitionen'
        exit 151
    }
}

_check_vm_storage() {
    local OUT
    _info 'Checking the amount of disk space used by the VM...'
    OUT=$( (_qa_exec --local "${SOURCE_ID}" "lsblk -Jbo PATH,FSUSED | jq -er --arg dev '/dev/sda${PART_NUM}' '.blockdevices[] | select(.path == \$dev) | .fsused'") 2>&1) || {
        _dbug "${OUT}"
        _panic 196
    }
    [[ ${OUT} -le $(((TARGET_STORAGE / 100) * 90)) ]] || {
        _dbug "${OUT}"
        _panic 197
    }
}

_check_target_host_storage() {
    local OUT
    _info "Checking the amount of disk space available in the target host's pool..."
    OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" 'sudo pvs --units b --nosuffix --reportformat json') 2>&1) || {
        _dbug "${OUT}"
        _panic 198
    }
    [[ $(jq -er '[.report[].pv[] | select(.vg_name=="vgvm") | .pv_free | tonumber] | add' <<<"${OUT}") -ge $((TARGET_STORAGE + 16106127360)) ]] || {
        _dbug "${OUT}"
        _panic 199
    }
}

_check_source_host_snapshots() {
    local OUT
    _info 'Checking for existing snapshots on the source host...'
    OUT=$( (sudo lvs --reportformat json -o lv_name,origin -S 'origin!=""' vgvm) 2>&1) || {
        _dbug "${OUT}"
        _panic 194
    }
    [[ $(jq -er '.report[].lv | length' <<<"${OUT}") -eq 0 ]] || {
        _dbug "${OUT}"
        _panic 194
    }
}

_notify() {
    local OUT
    if [[ ${API_ERROR} -ne 1 ]]; then
        _info 'Sending a Mattermost notification...'
        OUT=$( (${SSH} "${USR}@${API_SERVER}" "sudo /opt/kvmmove/./notify.sh ${USR} ${VM_FQDN} ${SOURCE_HOST_FQDN} ${TARGET_HOST_FQDN}") 2>&1) || {
            _dbug "${OUT}"
            _panic 204
        }
    else
        _ask --continue 'Mattermost notification should now be sent.'
    fi
}

_test_ga() {
    local OUT
    _info "Checking if the VM is responsive..."
    OUT=$( (_qa_exec "${1}" "${2}" 'true') 2>&1) || {
        _dbug "${OUT}"
        _panic 190
    }
}

_test_ssh() {
    local OUT
    _info "Testing the SSH connection... (${1})"
    OUT=$( (${SSH} "${USR}@${1}" 'true') 2>&1) || {
        _dbug "${OUT}"
        case ${1} in
        "${API_SERVER}")
            API_ERROR=1
            _panic 165
            ;;
        "${ANSIBLE_SERVER}")
            ANSIBLE_ERROR=1
            _panic 165
            ;;
        *)
            exit 151
            ;;
        esac
    }
}

_trim_vm() {
    local OUT
    _info "Trimming the VM's filesystem..."
    OUT=$( (sudo virsh domfstrim --domain "${SOURCE_ID}") 2>&1) || {
        _dbug "${OUT}"
        _panic 189
    }
}

_shutdown_vm() {
    local OUT TRY=0
    _info 'Disabling VM autostart...'
    OUT=$( (sudo virsh autostart --disable --domain "${SOURCE_ID}") 2>&1) || {
        _dbug "${OUT}"
        _panic 181
    }
    _info 'Shutting down the VM...'
    OUT=$( (sudo virsh shutdown --mode agent --domain "${SOURCE_ID}") 2>&1) || {
        _dbug "${OUT}"
        _panic 181
    }
    _info 'Ensuring that the VM is completely shut down...'
    while true; do
        ((TRY++))
        [[ ${TRY} -ge 60 ]] || {
            sudo virsh list --name --state-shutoff | grep -qw "${SOURCE_ID}" || {
                sleep 5
                continue
            }
            sleep 30
            break
        }
        _panic 188
        sudo virsh list --name --state-shutoff | grep -qw "${SOURCE_ID}" || {
            _erro 'The VM is running!'
            continue
        }
        break
    done
}

_start_vm() {
    local OUT VAR TRY=0
    _info 'Starting the VM...'
    case ${1} in
    --local)
        VAR=""
        ;;
    --remote)
        VAR="${SSH} ${USR}@${TARGET_HOST_FQDN} "
        ;;
    *)
        return 1
        ;;
    esac
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo virsh start --domain ${2}) 2>&1) || {
        _dbug "${OUT}"
        _panic 178
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo virsh autostart --domain ${2}) 2>&1) || {
        _dbug "${OUT}"
        _panic 178
    }
    _info 'Waiting for the VM to start...'
    while true; do
        ((TRY++))
        [[ ${TRY} -ge 60 ]] || {
            # shellcheck disable=SC2086
            OUT=$( (${VAR}sudo virsh list --name --state-running) 2>&1) || {
                _dbug "${OUT}"
                _panic 190
            }
            grep -qw "${2}" <<<"${OUT}" || {
                sleep 2
                continue
            }
            break
        }
        _panic 190
        # shellcheck disable=SC2086
        OUT=$( (${VAR}sudo virsh list --name --state-running) 2>&1) || {
            _dbug "${OUT}"
            _panic 190
        }
        grep -qw "${2}" <<<"${OUT}" || {
            _erro 'The VM is not running!'
            continue
        }
        break
    done
    sleep 60
}

_vm_configs_make() {
    local OUT VAR
    _info 'Dumping and adjusting the VM configuration...'
    (
        umask 0077
        : >"${VM_XML}"
    )
    # shellcheck disable=SC2024
    OUT=$( (sudo virsh dumpxml --domain "${SOURCE_ID}" >>"${VM_XML}") 2>&1) || {
        _dbug "${OUT}"
        _panic 157
    }
    VAR="<memballoon model='virtio' autodeflate='on' freePageReporting='on'>"
    OUT=$( (sed -Ei "s/<memballoon model=.+/${VAR}/g" "${VM_XML}") 2>&1)
    grep -q "${VAR}" "${VM_XML}" || {
        _dbug "${OUT}"
        _panic 195
    }
    VAR="<audio id='[0-9]' type='spice'"
    OUT=$( (sed -Ei "/${VAR}.+/d" "${VM_XML}") 2>&1)
    ! grep -q "${VAR}" "${VM_XML}" || {
        _dbug "${OUT}"
        _panic 155
    }
    [[ ${SOURCE_ID} != "${TARGET_ID}" ]] || return 0
    OUT=$( (sed -i "s/${SOURCE_ID}/${TARGET_ID}/g" "${VM_XML}") 2>&1)
    ! grep -q "${SOURCE_ID}" "${VM_XML}" || {
        _dbug "${OUT}"
        _panic 158
    }
}

_vm_configs_move() {
    local OUT
    _info 'Move the configuration files to the target host...'
    OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo bash -c 'umask 0077 && tee ${VM_XML}'" <"${VM_XML}") 2>&1) || {
        _dbug "${OUT}"
        _panic 159
    }
    OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo tee /etc/grommet/${TARGET_ID}.yml" <"/etc/grommet/${SOURCE_ID}.yml") 2>&1) || {
        _dbug "${OUT}"
        _panic 160
    }
}

_create_volume() {
    local OUT VAR
    _info "Creating the VM's volume on the target host..."
    if [[ ${TARGET_STORAGE_H} -lt ${SOURCE_STORAGE_H} ]]; then
        VAR=${TARGET_STORAGE}
    else
        VAR=${SOURCE_STORAGE}
    fi
    OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo virsh vol-create-as --pool vgvm --name ${TARGET_ID}-root --capacity ${VAR}") 2>&1) || {
        _dbug "${OUT}"
        _panic 161
    }
}

_create_pretransfer_snapshot() {
    local OUT
    _info "Creating a snapshot of the VM's volume on the source host..."
    OUT=$( (sudo lvcreate --snapshot -l100%FREE -n "${SOURCE_ID}-pretransfer" "/dev/vgvm/${SOURCE_ID}-root") 2>&1) || {
        _dbug "${OUT}"
        _panic 162
    }
}

_move_pretransfer_snapshot() {
    local VAR
    _info "Overwriting the VM's volume on the target host with the contents of the snapshot..."
    if [[ ${TARGET_STORAGE_H} -lt ${SOURCE_STORAGE_H} ]]; then
        VAR=${TARGET_STORAGE}
    else
        VAR=${SOURCE_STORAGE}
    fi
    sudo cat "/dev/vgvm/${SOURCE_ID}-pretransfer" | pv -ptrbes "${VAR}" | lzop | ${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo bash -c 'lzop -d > /dev/vgvm/${TARGET_ID}-root'" || _panic 163
}

_sync_changes() {
    local OUT
    _info "Syncing the VM's volume on the target host with the snapshot..."
    OUT=$( (sudo lvmsync --stdout "/dev/vgvm/${SOURCE_ID}-pretransfer" | ${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo lvmsync --apply - /dev/vgvm/${TARGET_ID}-root") 2>&1) || {
        _dbug "${OUT}"
        _panic 164
    }
}

_adjust_network_conf_vm() {
    local OUT
    _info "Adjusting VM's network configuration for the target host..."
    OUT=$( (_qa_exec --local "${SOURCE_ID}" "grep -Frl ${SOURCE_HOST_IP} /etc/systemd/network/ | xargs sed -i 's/${SOURCE_HOST_IP}/${TARGET_HOST_IP}/g'") 2>&1) || {
        _dbug "${OUT}"
        _panic 166
    }
    OUT=$( (_qa_exec --local "${SOURCE_ID}" "! grep -Frq ${SOURCE_HOST_IP} /etc/systemd/network/") 2>&1) || {
        _dbug "${OUT}"
        _panic 166
    }
}

_check_failover_ip() {
    local OUT
    if [[ ${API_ERROR} -ne 1 ]]; then
        _info "Gathering IPs from grommet config..."
        IPS=$( (grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' "/etc/grommet/${SOURCE_ID}.yml") 2>&1) || {
            _dbug "${IPS}"
            _panic 202
        }
        grep -qw "${VM_IP}" <<<"${IPS}" || {
            _erro "VM IP ${VM_IP} is not present in /etc/grommet/${SOURCE_ID}.yml."
            exit 151
        }
        _info "Checking if the VM's IPs are failover IPs..."
        OUT=$( (${SSH} "${USR}@${API_SERVER}" "sudo /opt/kvmmove/./failover.sh --check ${SOURCE_HOST_IP} ${IPS//$'\n'/ }") 2>&1) || {
            [[ ${?} -ne 151 ]] || {
                _dbug "${OUT}"
                _erro "One or more IPs in /etc/grommet/${SOURCE_ID}.yml are not failover IPs or not routed to the source host."
                exit 151
            }
            _dbug "${OUT}"
            _panic 203
        }
    else
        _ask --continue "Verify that the VM's IPs are failover IPs."
    fi
}

_route_failover_ip() {
    local OUT
    if [[ ${API_ERROR} -ne 1 ]]; then
        _info "Routing the VM's IPs to the target host... (this may take some time)"
        OUT=$( (${SSH} "${USR}@${API_SERVER}" "sudo /opt/kvmmove/./failover.sh --route ${TARGET_HOST_IP} ${IPS//$'\n'/ }") 2>&1) || {
            _dbug "${OUT}"
            _panic 187
        }
    else
        _ask --continue "VM's IPs should now be routed to the target host."
    fi
}

_icinga_downtime() {
    local OUT VAR
    [[ ${API_ERROR} -ne 1 ]] || {
        _ask --continue 'Downtime should now be scheduled.'
        return 0
    }
    _info 'Scheduling downtime...'
    case ${1} in
    --vm)
        OUT=$( (${SSH} "${USR}@${API_SERVER}" "sudo /opt/kvmmove/./downtime.sh --vm ${VM_FQDN}") 2>&1) || {
            _dbug "${OUT}"
            _panic 182
        }
        ;;
    --hosts)
        if [[ ${TARGET_STORAGE_H} -lt ${SOURCE_STORAGE_H} ]]; then
            VAR=${TARGET_STORAGE_H}
        else
            VAR=${SOURCE_STORAGE_H}
        fi
        OUT=$( (${SSH} "${USR}@${API_SERVER}" "sudo /opt/kvmmove/./downtime.sh --hosts ${SOURCE_HOST_FQDN} ${TARGET_HOST_FQDN} ${VAR}") 2>&1) || {
            _dbug "${OUT}"
            _panic 182
        }
        ;;
    *)
        return 1
        ;;
    esac
}

_check_pool() {
    local OUT
    _info "Ensuring that the target host's pool is active..."
    OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo virsh pool-list --name") 2>&1) || {
        _dbug "${OUT}"
        _panic 153
    }
    grep -qw 'vgvm' <<<"${OUT}" || {
        _info 'The pool is inactive, activating...'
        OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" 'sudo virsh pool-start --pool vgvm') 2>&1) || {
            _dbug "${OUT}"
            _panic 153
        }
        sleep 3
        OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo virsh pool-list --name") 2>&1) || {
            _dbug "${OUT}"
            _panic 153
        }
        grep -qw 'vgvm' <<<"${OUT}" || {
            _dbug "${OUT}"
            _panic 153
        }
    }
}

_define_vm() {
    local OUT
    _info 'Defining the VM using the configuration file...'
    OUT=$( (${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo virsh define --file ${VM_XML}") 2>&1) || {
        _dbug "${OUT}"
        _panic 168
    }
}

_configure_cpu() {
    local OUT VAR
    _info 'Configuring the vCPU...'
    case ${1} in
    --local)
        VAR=""
        ;;
    --remote)
        VAR="${SSH} ${USR}@${TARGET_HOST_FQDN} "
        ;;
    *)
        return 1
        ;;
    esac
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo virsh setvcpus --maximum --config --domain ${2} --count ${TARGET_CPU}) 2>&1) || {
        _dbug "${OUT}"
        _panic 169
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo virsh setvcpus --config --domain ${2} --count ${TARGET_CPU}) 2>&1) || {
        _dbug "${OUT}"
        _panic 169
    }
}

_configure_ram() {
    local OUT VAR
    _info 'Configuring the RAM...'
    case ${1} in
    --local)
        VAR=""
        ;;
    --remote)
        VAR="${SSH} ${USR}@${TARGET_HOST_FQDN} "
        ;;
    *)
        return 1
        ;;
    esac
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo virsh setmaxmem --config --domain ${2} --size ${TARGET_RAM}b) 2>&1) || {
        _dbug "${OUT}"
        _panic 170
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo virsh setmem --config --domain ${2} --size ${TARGET_RAM}b) 2>&1) || {
        _dbug "${OUT}"
        _panic 170
    }
}

_e2fsck_exec() {
    local OUT
    # shellcheck disable=SC2086
    OUT=$( (${1}sudo e2fsck -fy ${2}) 2>&1) || {
        [[ $? -le 3 ]] || {
            _dbug "${OUT}"
            _panic 175
        }
    }
}

_resize_disk_up() {
    local OUT VAR
    _info 'Configuring the disk... (this may take some time)'
    case ${1} in
    --local)
        VAR=""
        ;;
    --remote)
        VAR="${SSH} ${USR}@${TARGET_HOST_FQDN} "
        ;;
    *)
        return 1
        ;;
    esac
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo lvresize -fL ${TARGET_STORAGE}b /dev/vgvm/${2}-root) 2>&1) || {
        _dbug "${OUT}"
        _panic 171
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo kpartx -sa /dev/vgvm/${2}-root) 2>&1) || {
        _dbug "${OUT}"
        _panic 172
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo sgdisk -e /dev/vgvm/${2}-root) 2>&1) || {
        _dbug "${OUT}"
        _panic 173
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo parted -s /dev/mapper/vgvm-${2}--root resizepart ${PART_NUM} 100%) 2>&1) || {
        _dbug "${OUT}"
        _panic 174
    }
    _e2fsck_exec "${VAR}" "/dev/mapper/vgvm-${2}--root${PART_NUM}"
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo resize2fs /dev/mapper/vgvm-${2}--root${PART_NUM}) 2>&1) || {
        _dbug "${OUT}"
        _panic 176
    }
    _e2fsck_exec "${VAR}" "/dev/mapper/vgvm-${2}--root${PART_NUM}"
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo kpartx -sd /dev/vgvm/${2}-root) 2>&1) || {
        _dbug "${OUT}"
        _panic 177
    }
}

_resize_disk_down() {
    local OUT VAR
    _info 'Configuring the disk... (this may take some time)'
    case ${1} in
    --local)
        VAR=""
        ;;
    --remote)
        VAR="${SSH} ${USR}@${TARGET_HOST_FQDN} "
        ;;
    *)
        return 1
        ;;
    esac
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo kpartx -sa /dev/vgvm/${2}-root) 2>&1) || {
        _dbug "${OUT}"
        _panic 172
    }
    _e2fsck_exec "${VAR}" "/dev/mapper/vgvm-${2}--root${PART_NUM}"
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo resize2fs /dev/mapper/vgvm-${2}--root${PART_NUM} ${DOWNGRADE_TEMP_SIZE}M) 2>&1) || {
        _dbug "${OUT}"
        _panic 191
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo sgdisk /dev/vgvm/${2}-root -d ${PART_NUM}) 2>&1) || {
        _dbug "${OUT}"
        _panic 192
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo sgdisk /dev/vgvm/${2}-root -n ${PART_NUM}:0:${DOWNGRADE_TEMP_SIZE}M) 2>&1) || {
        _dbug "${OUT}"
        _panic 193
    }
    _e2fsck_exec "${VAR}" "/dev/mapper/vgvm-${2}--root${PART_NUM}"
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo lvresize -fL ${TARGET_STORAGE}b /dev/vgvm/${2}-root) 2>&1) || {
        _dbug "${OUT}"
        _panic 171
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo sgdisk -e /dev/vgvm/${2}-root) 2>&1) || {
        _dbug "${OUT}"
        _panic 173
    }
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo parted -s /dev/vgvm/${2}-root resizepart ${PART_NUM} 100%) 2>&1) || {
        _dbug "${OUT}"
        _panic 174
    }
    _e2fsck_exec "${VAR}" "/dev/mapper/vgvm-${2}--root${PART_NUM}"
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo resize2fs /dev/mapper/vgvm-${2}--root${PART_NUM}) 2>&1) || {
        _dbug "${OUT}"
        _panic 176
    }
    _e2fsck_exec "${VAR}" "/dev/mapper/vgvm-${2}--root${PART_NUM}"
    sleep 3
    # shellcheck disable=SC2086
    OUT=$( (${VAR}sudo kpartx -sd /dev/vgvm/${2}-root) 2>&1) || {
        _dbug "${OUT}"
        _panic 177
    }
}

_clean_up() {
    local OUT
    _info 'Cleaning up...'
    OUT=$( (sudo lvremove -f "/dev/vgvm/${SOURCE_ID}-pretransfer") 2>&1) || {
        _dbug "${OUT}"
        _panic 183
    }
    sudo virsh pool-refresh --pool vgvm &>/dev/null
    OUT=$( (sudo virsh undefine --domain "${SOURCE_ID}") 2>&1) || {
        _dbug "${OUT}"
        _panic 184
    }
    OUT=$( (sudo wipefs -af "/dev/vgvm/${SOURCE_ID}-root") 2>&1) || {
        _dbug "${OUT}"
        _panic 185
    }
    OUT=$( (sudo virsh vol-delete --pool vgvm --vol "${SOURCE_ID}-root") 2>&1) || {
        _dbug "${OUT}"
        _panic 186
    }
    rm -f "${VM_XML}"
    sudo rm -f "/etc/grommet/${SOURCE_ID}.yml"
    sudo rm -f "/etc/vzploopdump.d/${SOURCE_ID}.config"
    ${SSH} "${USR}@${TARGET_HOST_FQDN}" "sudo rm -f ${VM_XML}" &>/dev/null
}

_remove_route() {
    local OUT IN IP
    _info 'Removing routes and routing configuration files on the source host...'
    for IP in ${IPS}; do
        # shellcheck disable=SC2086
        while read -r IN; do
            [[ ${#IN} -gt 1 ]] || continue
            OUT=$( (sudo ip route del ${IN}) 2>&1) || {
                _dbug "${IN}" "${OUT}"
                _panic 154
                break
            }
        done <<<"$(ip route show ${IP})"
        OUT=$( (ip route show "${IP}") 2>&1)
        [[ ${#OUT} -le 1 ]] || {
            _dbug "${OUT}"
            _panic 154
        }
        sudo find /etc/systemd/network/ -type f -name "*${IP//./-}.conf" -print -delete | grep -q . || _panic 156
    done
}

_disable_backups() {
    local OUT
    _info 'Disabling VM backups on the source host...'
    OUT=$( (sudo tee -a "/etc/vzploopdump.d/${SOURCE_ID}.config" <<<'SKIP=y') 2>&1) || {
        _dbug "${OUT}"
        _panic 179
    }
}

_scale_vm() {
    local OUT VAR
    case ${1} in
    --pre)
        VAR="prescale"
        ;;
    --post)
        VAR="scale"
        ;;
    *)
        return 1
        ;;
    esac
    if [[ ${ANSIBLE_ERROR} -ne 1 ]]; then
        _info "Scaling VM... (${VAR}-vm.yml)"
        OUT=$( (${SSH} "${USR}@${ANSIBLE_SERVER}" "sudo ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i ${VM_IP}, /root/ansible2/${VAR}-vm.yml -e 'container=${VM_FQDN%%.*}'") 2>&1)
        tail -n4 <<<"${OUT}" | grep -q 'unreachable=0.*failed=0' || {
            _dbug "${OUT}"
            _panic 200
        }
    else
        _ask --continue "The VM should now be ${VAR}d with ${VAR}-vm.yml."
    fi
}

_run() {
    local SECONDS_SAVE
    _check_ssh_agent
    _get_timme_username
    _start_multiplexer
    _head
    _get_source_id
    _get_source_storage
    _get_source_cpu
    _get_source_ram
    if [[ ${MOVE_MODE} -eq 1 ]]; then
        TARGET_ID=${SOURCE_ID}
        TARGET_STORAGE=${SOURCE_STORAGE}
        TARGET_STORAGE_H=${SOURCE_STORAGE_H}
        TARGET_CPU=${SOURCE_CPU}
        TARGET_RAM=${SOURCE_RAM}
        TARGET_RAM_H=${SOURCE_RAM_H}
    else
        _get_target_id
        _get_target_storage
        _get_target_cpu
        _get_target_ram
        [[ ${TARGET_STORAGE_H} -ge ${SOURCE_STORAGE_H} ]] || _get_downgrade_temp_size
    fi
    _get_source_host_fqdn
    _get_target_host_fqdn
    _get_source_host_ip
    _get_target_host_ip
    _get_vm_fqdn
    _get_vm_ip
    _summary
    trap '
        ${SSH/#ssh/scp} "${DEBUG_LOG}" "${TERM_LOG}" "${USR}@${TARGET_HOST_FQDN}:/tmp/" &>/dev/null
        find /tmp -maxdepth 1 -name "${SESSION}_*.sock" -exec ssh -O exit -S {} foo \; &>/dev/null
    ' EXIT
    SECONDS=0
    _test_ga --local "${SOURCE_ID}"
    _test_ssh "${TARGET_HOST_FQDN}"
    _test_ssh "${API_SERVER}"
    [[ ${TARGET_RAM_H} -eq ${SOURCE_RAM_H} ]] || _test_ssh "${ANSIBLE_SERVER}"
    _check_data
    _check_failover_ip
    [[ ${TARGET_STORAGE_H} -eq ${SOURCE_STORAGE_H} ]] || _get_root_partition_number
    [[ ${TARGET_STORAGE_H} -ge ${SOURCE_STORAGE_H} ]] || _check_vm_storage
    _check_target_host_storage
    _check_source_host_snapshots
    _notify
    _trim_vm
    [[ ${TARGET_RAM_H} -ge ${SOURCE_RAM_H} ]] || _scale_vm --pre
    [[ ${TARGET_STORAGE_H} -ge ${SOURCE_STORAGE_H} ]] || {
        _icinga_downtime --vm
        _shutdown_vm
        _resize_disk_down --local "${SOURCE_ID}"
        _start_vm --local "${SOURCE_ID}"
        _test_ga --local "${SOURCE_ID}"
        SECONDS_SAVE=${SECONDS}
        _ask --continue 'The disk has been resized. Check that everything is working and confirm to continue with the transfer.'
        SECONDS=${SECONDS_SAVE}
    }
    _vm_configs_make
    _vm_configs_move
    _check_pool
    _create_volume
    _icinga_downtime --hosts
    _create_pretransfer_snapshot
    _move_pretransfer_snapshot
    _sync_changes
    SECONDS_SAVE=${SECONDS}
    _ask --continue 'Pre-transfer is complete. Confirm to shut down the VM and complete the final steps.'
    SECONDS=${SECONDS_SAVE}
    _icinga_downtime --vm
    _adjust_network_conf_vm
    _shutdown_vm
    _sync_changes
    _route_failover_ip
    _remove_route
    _define_vm
    [[ ${TARGET_CPU} -eq ${SOURCE_CPU} ]] || _configure_cpu --remote "${TARGET_ID}"
    [[ ${TARGET_RAM_H} -eq ${SOURCE_RAM_H} ]] || _configure_ram --remote "${TARGET_ID}"
    [[ ${TARGET_STORAGE_H} -le ${SOURCE_STORAGE_H} ]] || _resize_disk_up --remote "${TARGET_ID}"
    _start_vm --remote "${TARGET_ID}"
    _test_ga --remote "${TARGET_ID}"
    [[ ${TARGET_RAM_H} -eq ${SOURCE_RAM_H} ]] || _scale_vm --post
    _disable_backups
    SECONDS_SAVE=${SECONDS}
    _ask --continue 'Check that everything works and confirm to start the cleanup. VM data will be irretrievably removed from the source host!'
    SECONDS=${SECONDS_SAVE}
    _clean_up
}

_main() {
    local H M S
    case ${1:-} in
    '') ;;
    -m | --move)
        MOVE_MODE=1
        ;;
    -h | --help)
        _help
        exit 0
        ;;
    -v | --version)
        _prnt "${VERSION}"
        exit 0
        ;;
    *)
        _erro "Unknown argument: ${1}"
        _help
        exit 151
        ;;
    esac

    _run

    sudo virsh list --name | grep -q . || {
        [[ $(lsb_release -sr 2>/dev/null) -ge 12 ]] || {
            _warn 'This host is empty and not running the latest version of Debian.'
            _warn 'Please leave the slot disabled (last step in KB) and write in "Town Square" that you have seen this message.'
        }
    }

    if ((SECONDS > 3600)); then
        ((H = SECONDS / 3600))
        ((M = (SECONDS % 3600) / 60))
        ((S = (SECONDS % 3600) % 60))
        _info "Completed in ${H} hour(s), ${M} minute(s) and ${S} second(s)."
    elif ((SECONDS > 60)); then
        ((M = (SECONDS % 3600) / 60))
        ((S = (SECONDS % 3600) % 60))
        _info "Completed in ${M} minute(s) and ${S} second(s)."
    else
        _info "Completed in ${SECONDS} seconds."
    fi
}

[[ ${#} -le 1 ]] || {
    _erro 'Too many arguments.'
    _help
    exit 151
}
_main "${@}"

# vim: syn=bash ts=4 sw=4 et:
