#!/bin/bash

# enable debug output
[ -n "${L3VM_DEBUG}" ] && set -x

source /usr/lib/l3vm/l3vm_lib.sh

MYSELF="${0##*/}"

# create unique disk name from VG and LV
# $1 disk VG
# $2 disk LV
# return name for backup file
get_disk_backup_name()
{
	if is_vg_system $1; then
		echo $2
		return 0
	fi
	echo "${1}#${2}"
	return 0
}

# Use path to nvram to find backup file name
# $1 full path
# return nvram backup file name (without path)
get_nvram_backup_name()
{
	local nvram=$1
	echo "$(basename $nvram)#nvram"
}

# get list of all templates modified since their last backup
# Check all templates in L3VM_BACKUP_LIST if it's defined
# otherwise check all templates in L3VM_TEMPLATES
get_modified_templates()
{
	local -a modified
	local vm
	local xml_date
	local last_action
	local last_action_date
	local now
	local checked_templates

	now=$(date +%s)

	if [ ${#L3VM_BACKUP_LIST[@]} -ne 0 ]; then
		checked_templates=( "${L3VM_BACKUP_LIST[@]}" )
	else
		checked_templates=( "${L3VM_TEMPLATES[@]}" )
	fi

	for vm in ${checked_templates[@]}; do
		last_action=$(lifespan_get_last_domain_event $vm)
		last_action_date=${last_action##*|}
		last_action_date=${last_action_date:-${now}}
		xml_date=$(stat --printf %X "${L3VM_BKP_DIR}/${vm}.xml")
		xml_date=${xml_date:-0}

		# running machines were modified since last backup run
	    # we can't backup running machines and they will be reported and dropped later
		if is_running "$vm"; then
			last_action_date=${now}
		fi

		if [ ${last_action_date} -ge ${xml_date} ]; then
			# vm was touched since last backup
			modified[${#modified[@]}]="$vm"
		fi

	done
	echo ${modified[@]}
}

# Revert failed backup process by moving old backup from
# temporary directory back to main backup directory
# and delete the temporary dir
# $1 temporary directory
do_backup_template_revert()
{
	mv "${old_backups}/*" "${L3VM_BKP_DIR}/"
	rm -rf "${old_backups}"
}

# backup template LV to raw image and save .xml
# $1 image name
do_backup_template() {
	local vm="$1"
	local XML="${L3VM_BKP_DIR}/${vm}.xml"
	local -a disks

	if ! is_machine "$1"; then
		error "Machine '$1' is not defined, sorry."
		return 1
	fi

	if ! is_shutoff  "$1";  then
		error "Machine '$vm' must be shutdown for backup."
		return 1
	fi

	local old_backups=$(mktemp -d ${L3VM_BKP_DIR}/tmp.${vm}.XXXXXX)

	if [ -z "${old_backups}" ] || [ ! -d "${old_backups}" ]; then
		error "Failed to create temp directory for previous backup"
		return 1
	fi

	# rename previous backup
	# do not overwrite the old one - if .old exists, the previous run was not successful and the image
	# is possibly broken.
	[ -f "${XML}" ] && mv "${XML}" "${old_backups}/"

	# dump xml
	virsh dumpxml "${vm}" > "${XML}"
	if [ $? -ne 0 ]; then
		error "Failed  to backup xml to ${XML}"
		do_backup_template_revert ${old_backups}
		return 1
	fi

	readarray -t disks < <(xml sel -t -v /domain/devices/disk/source/@dev "$XML")
	for disk in "${disks[@]}"; do
		local disk_vg=$(get_vg ${disk})
		local disk_lv=$(get_lv ${disk})
		local disk_backup_name=$(get_disk_backup_name $disk_vg $disk_lv)

		# save previous backup if exists to tmp dir
		if [ -f "${L3VM_BKP_DIR}/${disk_backup_name}" ]; then
			mv -f "${L3VM_BKP_DIR}/${disk_backup_name}" "${old_backups}/"
		fi

		# dump disk image
		dd if="/dev/${disk_vg}/${disk_lv}" of="${L3VM_BKP_DIR}/${disk_backup_name}" bs=4K conv=sparse
		if [ $? -ne 0 ]; then
			error "Failed  to backup /dev/${disk_vg}/${disk_lv} to ${L3VM_BKP_DIR}/${disk_backup_name}"
			do_backup_template_revert ${old_backups}
			return 1
		fi
	done

	if nvram=$(xml sel -t -v /domain/os/nvram "$XML"); then
		# backup nvram
		nvram_backup_name=$(get_nvram_backup_name "${nvram}")
		dd if="$nvram" of="${L3VM_BKP_DIR}/${nvram_backup_name}"
	fi

	# remove old backup files
	rm -rf "${old_backups}"

	return 0
}

# fill array of templates to backup backup_set according to selected method
# all - all templates from L3VM_TEMPLATES set
# selected - backup only templates listed in L3VM_BACKUP_LIST set
# supported - all updated templates (templates not in L3VM_UPDATE_SKIP set)
# modified - backup only templates started since last backup
# none - do not run backup at all
backup_get_templates() {
	case "${1}" in
		all)
			backup_set=( "${L3VM_TEMPLATES[@]}" ) ;;
		selected)
			backup_set=( "${L3VM_BACKUP_LIST[@]}" ) ;;
		supported)
			backup_set=( $(list_supported_templates) ) ;;
		modified)
			backup_set=( $(get_modified_templates) );;
		none)
			backup_set=( ) ;;
		*)
			error "Invalid value for selecting backuped templates: \"${L3VM_BACKUP}\""
			;;
	esac
}

# backup given templates
# handle input arguments and call backup for each template
backup_templates() {
	local -a backup_set=()
	local method_selected=0 

	while [ "${1:0:1}" = - ]; do
		case "$1" in
			--backup-dir|-b)
				shift
				L3VM_BKP_DIR=$1
				shift ;;
			--all|-a)
				shift
				method_selected=1
				backup_get_templates "all"
				;;
			--list|-l)
				shift
				method_selected=1
				backup_get_templates "selected"
				;;
			--modified|-m)
				shift
				method_selected=1
				backup_get_templates "modified"
				;;
			--supported|-s)
				shift
				method_selected=1
				backup_get_templates "supported"
				;;
			*)
				error "Unknown parameter '$1'"; exit 1;;
		esac
	done

	backup_set=( ${backup_set[@]} $@ )

	if [ ${#backup_set[@]} -eq 0 ] && [ ${method_selected} -eq 0 ]; then
		# if template was specified and no method selected default to config file
		inform "No machine selected, defaulting to method \"${L3VM_BACKUP}\" from config file."
		backup_get_templates "${L3VM_BACKUP}"
	fi

	if [ ! -d "${L3VM_BKP_DIR}" ]; then
		warn  "Target backup directory \"${L3VM_BKP_DIR}\" does not exists. Exiting."
		return 1
	fi

	local TOTAL=${#backup_set[@]}
	if [ "${TOTAL}" -eq 0 ]; then
		warn 'No machines to backup.'
		return 0
	fi

	inform "Selected templates: ${backup_set[@]}"
	local NUM=0
	for template in "${backup_set[@]}"; do
		NUM=$((NUM+1))
		inform "Backing up \"${template}\" ${NUM}/${TOTAL}"
		do_backup_template "${template}"
	done

    return 0
}

# input:
# array $restore_disks_old - source files to copy from
# array $restore_disks_new - targed names of new LVs to create
do_restore_disks() {

	if [ ${#restore_disks_old[@]} -ne ${#restore_disks_new[@]} ]; then
		error "Internal error: lists of old and new volume names should have the same number of elements!"
		return 1
	fi

	# check all disks in xml that disk backup exists and the target in LVM not.
	for d in "${restore_disks_new[@]}"; do
		if [ -f "${d}" ]; then
			error "Disk \"${d}\" already exists, cannot restore."
			return 1
		fi
	done

	for d in "${restore_disks_old[@]}"; do
		if [ ! -f "${d}" ]; then
			error "Backup image ${d} does not exists"
			return 1
		fi
	done

	# FIXME: delete already created volumes on error
	for i in "${!restore_disks_old[@]}"; do
		inform "Restoring disk \"${restore_disks_old[i]}\""
		# get image size
		size=$(stat --printf='%s' "${restore_disks_old[i]}")

		# create volume
		inform -l "Creating new volume '${restore_disks_new[i]}'"
		lvm_create_volume "${restore_disks_new[i]}" "$size"B
		if [ $? -ne 0 ]; then
			error "Cannot create new volume "${restore_disks_new[i]}" of size $size for template derivation"
			return 1
		fi

		# write image to LV (without sparse this time, we write directly to block device)
		dd if="${restore_disks_old[i]}" of="${restore_disks_new[i]}" bs=4M
		if [ $? -ne 0 ]; then
			error "Failed to restore machine ${from_image} volume "${restore_disks_old[i]}" from backup."
			return 1
		fi

	done
	libvirtd_pool_refresh
}

do_restore_template() {
	local from_image="$1"
	local to_image="$2"
	local change_mac="${3}"

	inform -l "Restore backup \"$from_image\" to \"$to_image\""

	if [ ! -f "${L3VM_BKP_DIR}/${from_image}.xml" ]; then
		error "Backup xml \"${L3VM_BKP_DIR}/${from_image}.xml\" does not exists"
		return 1
	fi

	XML="$(mktemp /tmp/l3vm.restore.XXXXXXXXXX)"
	cp "${L3VM_BKP_DIR}/${from_image}.xml" ${XML}

	if is_machine "${to_image}"; then
		error "Machine '${to_image}' already exists. Destroy or rename it before restoring."
		return 1
	fi

	# backup original nvram file name before possible renaming
	local orig_nvram=$(xml sel -t -v /domain/os/nvram "$XML")

	if [ "${from_image}" = "${to_image}" ]; then
		# restore under the same name, just prepare disks and define machine

		if [ "${change_mac}" == "1" ]; then
			do_change_mac_xml "$XML"
		fi

		# get disks configuration
		readarray -t disks < <(xml sel -t -v /domain/devices/disk/source/@dev "$XML")

		for d in "${disks[@]}"; do
			local new_lv="$(get_lv ${d})"
			local new_vg=$(get_vg ${d})
			local disk_backup_name=$(get_disk_backup_name ${new_vg} ${new_lv})

			restore_disks_old+=("${L3VM_BKP_DIR}/${disk_backup_name}")
			restore_disks_new+=("/dev/${new_vg}/${new_lv}")
		done

		do_restore_disks
		if [ $? -ne 0 ]; then
			error "Failed to restore disks. Some of them might be already created, please purge that manually."
			return 1
		fi
	else
		# we have to redefine MAC, rename disks etc...
		do_clone_xml "${XML}" "${from_image}" "${to_image}"
		if [ $? != 0 ]; then
			error "Failed to clone \"${from_image}\" xml to \"${to_image}\""
			return 1
		fi

		# get disks configuration
		readarray -t disks < <(xml sel -t -v /domain/devices/disk/source/@dev "$XML")

		for d in "${disks[@]}"; do
			local old_lv=$(get_lv ${d})
			local new_name="$(find_new_lvm_name "${d}" "${from_image}" "${to_image}")"
			local new_vg=$(get_vg ${d})
			local disk_backup_name=$(get_disk_backup_name ${new_vg} ${old_lv})

			restore_disks_old+=("${L3VM_BKP_DIR}/${disk_backup_name}")
			restore_disks_new+=("/dev/${new_vg}/${new_name}")
			xml ed -L -u /domain/devices/disk/source[@dev=\"${d}\"]/@dev -v "/dev/${new_vg}/${new_name}" "$XML"
		done

	        do_restore_disks
		if [ $? -ne 0 ]; then
			error "Failed to restore disks. Some of them might be already created, please purge that manually."
			return 1
		fi
	fi

	if [ -n "${orig_nvram}" ]; then
		# nvram is used, find new file name
		local new_nvram=$(xml sel -t -v /domain/os/nvram "$XML")
		# use old name to find backup file name
		local nvram_backup_name=$(get_nvram_backup_name "${orig_nvram}")
		# FIXME: what about some checks?
		dd if="${L3VM_BKP_DIR}/${nvram_backup_name}" of="${new_nvram}"
		langc_virsh pool-refresh --pool nvram &>/dev/null
	fi

	# create new machine
	inform "Defining new machine ${to_image}..."
	if ! langc_virsh define "$XML"; then
		error -l "There was problem with creating new virtual machine. You can check ${XML} file which was used."
		return 1
	fi

	rm -f "$XML"

    return 0
}

# $1 - backup name
restore_template() {
	local restore_name=""
	while [ "${1:0:1}" = - ]; do
		case "$1" in
			--backup-dir|-b)
				shift
				L3VM_BKP_DIR=$1
				shift ;;
			--mac|-m)
				change_mac="1"
				shift ;;
			--name|-n)
				shift
				restore_name=$1
				shift ;;
			*)
				error "Unknown parameter '$1'"; exit 1;;
		esac
	done

	backup=$1

	[ -z "${restore_name}" ] && restore_name=${backup}

	if [ ! -d "${L3VM_BKP_DIR}" ]; then
		inform  "Target backup directory \"${L3VM_BKP_DIR}\" does not exists. Exiting."
		exit 1
	fi

	if [ -z "${backup}" ]; then
		error "No template to restore."
		return 1
	fi

	do_restore_template "${backup}" "${restore_name}" "${change_mac}"
}

usage() {
	local args=" $* "
	local help_regex=' (--help|-h|help) '
	if [[ $args =~ $help_regex ]]; then
	    case "$1" in
			backup)
				echo "$MYSELF         backup <option> | [machine ...]"
				echo
				echo "   -a, --all                  backup all templates"
				echo "   -b, --backup-dir           set backup directory"
				echo "   -l, --list                 backup templates specified by L3VM_BACKUP_LIST in config file"
				echo "   -m, --modified             backup templates modified since last backup run"
				echo "                              If L3VM_BACKUP_LIST is set, check only those for modifications."
				echo "   -s, --supported            backup only supported templates (regulary updated)"
				echo "Backup templates selected by listing them or by option."
				echo "If no option is set only listed machines are chosen."
				echo "If no option is set and no machine listed, config file will be used to speficy them.";;
			restore)
				echo "$MYSELF         restore < backup name >"
				echo
				echo "   -b, --backup-dir           backup directory"
				echo "   -m, --mac                  change mac addresses"
				echo "   -n, --name                 restore under new name"
				echo "Restore template from backup.";;
	        *)
				echo "$MYSELF         < backup | restore | help | --help | -h >"
				echo "   backup                 backup templates"
				echo "   restore                restore template"
				echo "   help, --help, -h       show this menu";;
	    esac
	    echo
	fi
}

case "$1" in
	--help|-h|help)
		usage "help";;
	"")
		usage "help";;
	backup)
		usage backup "$@"
		shift; backup_templates "$@";;
	restore)
		usage restore "$@"
		shift; restore_template "$@";;
	*)
		error "Unknown command \"$1\", use ${MYSELF} help";;
esac

