#!/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