#!/usr/bin/env bash
# vm backup script

SYNOSHARE="/usr/syno/sbin/synoshare"
LS="/bin/ls"
RM="/bin/rm"
AWK="/bin/awk"
SED="/bin/sed"
TR="/bin/tr"
SLEEP="/bin/sleep"
DATE="/bin/date"
JQ="/bin/jq"
CUT="/bin/cut"
MV="/bin/mv"
CAT="/bin/cat"
WC="/bin/wc"
MKDIR="/bin/mkdir"
HEAD="/bin/head"
XARGS="/bin/xargs"
GREP="/bin/grep"

webapi_func_format="/usr/syno/bin/synowebapi --exec api=SYNO.Virtualization.%s version=%d method=%s %s"
tmp_folder="/tmp/vmm_backup_ova/"
tmp_file_format="${tmp_folder}tmp_%s_%s_bak"
log="/tmp/vmm_backup_ova.log"

trap "cleanup" INT TERM EXIT

setup()
{
	${RM} -f ${log}
	if [[ ! -d ${tmp_folder} ]]; then
		${MKDIR} ${tmp_folder}
	else
		${RM} -rf "${tmp_folder}"*
	fi
	start_time=$(${DATE})
}

cleanup()
{
	local ret=$?

	if [[ ${ret} -eq 1 ]]; then
		echo "[Fail] Please check ${log} for detail information"
	elif [[ ${ret} -eq 2 ]]; then
	#	${CAT} << EOF > ${log}
		exit 0
====================================
[Host Target] ${host_name}
[Batch Size] ${batch_size}
[Time] ${start_time} - $(${DATE})
====================================
EOF
		echo "[Success] Please check ${log} for backup report"
	fi
	if [[ ${ret} -ne 2 && ! -z ${batch_ids} ]]; then
		for id in ${batch_ids}; do
			webapi_exec "Guest" "delete" 1 "guest_id='\"${id}\"'"
		done
		batch_ids=""
	fi
	${RM} -rf ${tmp_folder}
	exit ${ret}
}

backup_prepare()
{
	share_path=$(${SYNOSHARE} --get ${dst_folder} | ${TR} '[]' '\n' | ${SED} -n "9p")
	target_name_list=""
	for id in ${target_id_list}; do
		local guest_name
		local tmp_guest_setting
		webapi_exec "Guest" "get_setting" 1 "guest_id='\"${id}\"'" tmp_guest_setting
		json_parser ${tmp_guest_setting} ".data.name" guest_name 1
		${MKDIR} -p ${share_path}/${guest_name}
		target_name_list="${target_name_list} ${guest_name}"
	done
}

backup_retention()
{
	for name in ${target_name_list}; do
		${LS} -tr ${share_path}/${name} | ${HEAD} -n -${retent_num} | ${XARGS} -i ${RM} ${share_path}/${name}/{}
	done
}

progress()
{
	local count=0

	echo -ne "\r$1 "
	until [ "${count}" -ge "3" ]; do
		echo -n "."
		${SLEEP} 5
		((count=count+1))
	done
	echo -ne "\b\b\b   \b\b\b"
}

webapi_exec()
{
	local api=$1
	local method=$2
	local version=$3
	local parameter=$4
	local api_cmd=$(printf "${webapi_func_format}" ${api} ${version} ${method} "${parameter}")
	local tmp_file=$(printf ${tmp_file_format} ${api} ${method})
	local isSuccess

	eval "${api_cmd} > ${tmp_file} 2> /dev/null"
	if [[ ! -z $5 ]]; then
		json_parser ${tmp_file} ".success" isSuccess 0
		if [[ ${isSuccess} == "false" ]]; then
			${CAT} << EOF > ${log}
[Time] ${start_time} - $(${DATE})
[Command] $api_cmd
[Result] $(${CAT} ${tmp_file})
EOF
			${RM} -f ${tmp_file}
			return 1
		fi
		eval "$5=\"${tmp_file}\""
	else
		${RM} -f ${tmp_file}
	fi
}

json_parser()
{
	local src_file=$1
	local parse_format=$2
	local end=$4
	local result=$(${CAT} ${src_file} | ${JQ} "${parse_format}" | ${TR} -d '"')

	eval "$3=\"${result}\""
	if [[ ${end} -eq 1 ]]; then
		${RM} -f ${src_file}
	fi
}

usage()
{
	echo -e "\nUsage: $0 [--dst] [--batch] [--host] [--guests] [--retent] [--retry]"
	echo -e "\tbackup VM to shared folder on VMM\n"
	echo "Options:"
	echo -e "\t--default\tuse default options to backup"
	echo -e "\t--dst\t\tshared folder path for storing backup OVA"
	echo -e "\t--batch\t\tthe number of VMs exporting at a time (default: 5)"
	echo -e "\t--host|--guests\tmutually exclusive options"
	echo -e "\t\t\t'--host' only backup VMs which repository is on the specified host (default: all)"
	echo -e "\t\t\t'--guests' only backup specified VMs (default: not specified, use | for seperator if there are multiple targets)"
	echo -e "\t--retent\tthe number of backups for retention (default: 3)"
	echo -e "\t--retry\t\tthe number of times for backup retrying (default: 3)"
	echo -e ""
	echo "Examples:"
	echo -e "\tRun backup script by default"
	echo -e "\t\t./vmm_backup_ova --default"
	echo -e "\tBackup all guests which repository is on the host and store OVAs in certain shared folder"
	echo -e "\t\t./vmm_backup_ova --dst=<share-name> --host=\"<host-name>\""
	echo -e "\tBackup all guests which repository is on the host and limit the number of VMs exporting at a time to avoid affecting performance"
	echo -e "\t\t./vmm_backup_ova --batch=2 --host=\"<host-name>\""
	echo -e "\tBackup certain guests and store the last two OVAs per VM"
	echo -e "\t\t./vmm_backup_ova --guests=\"<guest_name_1>|<guest_name_2>\" --retent=2"
	exit 0
}

host_check()
{
	local guest_list_ori=$1
	local guest_list_filt
	local tmp_repo_list

	webapi_exec "Repo" "list" 1 "" tmp_repo_list
	for guest_id in ${guest_list_ori}; do
		local tmp_guest_setting
		local repo_id
		local guest_name
		local host_name_cur

		webapi_exec "Guest" "get_setting" 1 "guest_id='\"${guest_id}\"'" tmp_guest_setting
		json_parser ${tmp_guest_setting} ".data.repo_id" repo_id 0
		json_parser ${tmp_guest_setting} ".data.name" guest_name 1
		json_parser ${tmp_repo_list} ".data.repos[]|select(.repo_id==\"${repo_id}\")|.host_name" host_name_cur 0
		if [[ ${host_name} == "all" || ${host_name} == ${host_name_cur} ]]; then
			echo -e "\t${guest_name}"
			guest_list_filt="${guest_list_filt} ${guest_id}"
		fi
	done
	echo -e "Total count:\t$(echo ${guest_list_filt} | ${WC} -w)"
	eval "$2=\"${guest_list_filt}\""
	${RM} -f ${tmp_repo_list}
}

backup()
{
	local batch_list=$1
	local retry_list
	local batch_names
	local batch_size_cur

	for id in ${batch_list}; do
		local name
		local new_bak

		json_parser ${tmp_guest_list} ".data.guests[]|select(.guest_id==\"${id}\")|.name" name 0
		name=$(echo "${name}" | ${TR} ' ' '_')
		new_bak=$(printf "%s_%s_bak" ${name} $(date +"%s"))
		webapi_exec "Guest.Action" "clone" 1 "guest_id='\"${id}\"' name='\"${new_bak}\"'"
		batch_names="${batch_names} ${new_bak}"
	done
	${SLEEP} 5	# avoid guest clone return too fast, dashboard does not ready yet
	local list_idx
	local arr_ids
	local tmp_names

	IFS=' ' read -r -a arr_ids <<< "${batch_list}"
	list_idx=$(echo ${batch_list} | ${WC} -w)
	webapi_exec "Guest" "list_basic" 1 "" tmp_guest_list
	for name in ${batch_names}; do
		local guest_id

		((list_idx--))
		json_parser ${tmp_guest_list} ".data.guests[]|select(.name==\"${name}\")|.guest_id" guest_id 0
		if [[ -z ${guest_id} ]]; then
			retry_list="${retry_list} ${arr_ids[$list_idx]}"
		else
			batch_ids="${batch_ids} ${guest_id}"
			tmp_names="${tmp_names} ${name}"
		fi
	done
	batch_names=${tmp_names}
	eval "$2=\"${retry_list}\""
	batch_size_cur=$(echo ${batch_ids} | ${WC} -w)
	local clone_count

	if [[ ${batch_size_cur} -eq 0 ]]; then
		return
	fi

	clone_count=0
	list_idx=$(echo ${batch_list} | ${WC} -w)
	echo "Starting to clone ${batch_size_cur} VMs ..."
	while [[ ${batch_size_cur} -ne ${clone_count} ]]; do
		for guest in ${batch_ids}; do
			local tmp_guest_basic_file
			local guest_state

			webapi_exec "Guest" "get_basic" 1 "guest_id='\"${guest}\"'" tmp_guest_basic_file
			if [[ $? -ne 0 ]]; then
				local name

				((list_idx--))
				retry_list="${retry_list} ${arr_ids[${list_idx}]}"
				batch_ids="$(echo $batch_ids | ${SED} -e "s/${guest}//g" | ${SED} -e "s/  / /g")"
				json_parser ${tmp_guest_list} ".data.guests[]|select(.guest_id==\"${guest}\")|.name" name 0
				batch_names="$(echo $batch_names | ${SED} -e "s/${name}//g" | ${SED} -e "s/  / /g")"
				((clone_count++))
				continue
			fi
			json_parser ${tmp_guest_basic_file} ".data.guest.status" guest_state 1
			if [[ ${guest_state} == "shutdown" ]]; then
				((clone_count++))
			fi
		done
	done
	eval "$2=\"${retry_list}\""
	batch_size_cur=$(echo ${batch_ids} | ${WC} -w)
	if [[ ${batch_size_cur} -eq 0 ]]; then
		return
	fi
	echo "[Clone] Done"
	local idx=0
	local arr_names
	local task_ids

	IFS=' ' read -r -a arr_names <<< "${batch_names}"
	for id in ${batch_ids}; do
		local name=${arr_names[$idx]}
		local sub_folder=${name:0:-15} # remove postfix
		local tmp_guest_export_file
		local task_id

		((idx++))
		webapi_exec "Guest.Action" "export" 1 "guest_id='\"${id}\"' target_ova_path='\"/${dst_folder}/${sub_folder}\"' name='\"${name}\"' login_user='\"$USER\"'" tmp_guest_export_file
		json_parser ${tmp_guest_export_file} ".data.task_id" task_id 1
		task_ids="${task_ids} ${task_id}"
		${SLEEP} 1
	done
	local export_count=0

	echo "Starting to export ova to the backup folder..."
	while [[ ${batch_size_cur} -ne ${export_count} ]]; do
		local text="[Export] Progress: ${export_count}/${batch_size_cur}"
		progress "${text}"
		export_count=0
		for id in ${task_ids}; do
			local tmp_cluster_progress_file
			local finish

			webapi_exec "Cluster" "get_progress" 1 "task_id='\"${id}\"' prefix='\"virtualization_guest_export\"'" tmp_cluster_progress_file
			if [[ -f ${tmp_cluster_progress_file} ]]; then
				json_parser ${tmp_cluster_progress_file} ".data.finish" finish 1
			else
				finish="true"
			fi

			if [[ ${finish} == "true" ]]; then
				((export_count++))
			fi
		done
	done

	idx=0
	list_idx=$(echo ${batch_list} | ${WC} -w)
	for id in ${batch_ids}; do
		local name=${arr_names[$idx]}
		local sub_folder=${name:0:-15} # remove postfix
		local ova_file="/${dst_full_path}/${sub_folder}/${name}.ova"

		((list_idx--))
		if [[ ! -f ${ova_file} ]]; then
			retry_list="${retry_list} ${arr_ids[${list_idx}]}"
		fi
		((idx++))
	done

	if [[ -z ${retry_list} ]]; then
		echo -e "\n[Export] Done"
	else
		eval "$2=\"${retry_list}\""
	fi

	for id in ${batch_ids}; do
		webapi_exec "Guest" "delete" 1 "guest_id='\"${id}\"'"
	done
	batch_ids=""
}

main()
{
	setup
	# list guest first
	webapi_exec "Guest" "list_basic" 1 "" tmp_guest_list
	echo "Selected backup list:"
	if [[ -z ${guests_spec} ]]; then
		json_parser ${tmp_guest_list} ".data.guests[].guest_id" guest_list 0
		guest_list=$(echo "${guest_list}" | ${TR} '\n' ' ')
		host_check "${guest_list}" guest_list
	else
		local guest_id
		local oldifs=$IFS

		IFS=$'\n'
		for name in ${guests_spec}; do
			json_parser ${tmp_guest_list} ".data.guests[]|select(.name==\"${name}\")|.guest_id" guest_id 0
			if [[ ! -z ${guest_id} ]]; then
				echo -e "\t${name}"
				guest_list="${guest_list} ${guest_id}"
			fi
		done
		guest_list=$(echo ${guest_list} | ${SED} 's/^ *//g')
		IFS=$oldifs
	fi
	if [[ -z ${guest_list} ]]; then
		echo -e "\t[empty]"
		echo "VM Backup list is empty!"
		exit 0
	fi
	target_id_list=${guest_list}
	backup_prepare
	guest_list="${guest_list}ReTrYnOw"
	local next=$((batch_size+1))
	local guest_retry_list=""
	local round=0

	while true; do
		local retry_glist=""
		local list
		local retry=0
		list=$(echo "${guest_list}" | ${CUT} -d' ' -f1-${batch_size})
		if [[ ${list} == *"ReTrYnOw"* ]]; then
			retry=1
			list=$(echo "${list}" | ${SED} 's/ReTrYnOw//g')
		fi
		guest_list=$(echo "${guest_list}" | ${CUT} -d' ' -f${next}-)
		if [[ ! -z ${list} ]]; then
			backup "${list}" retry_glist
		fi
		if [[ ! -z ${retry_glist} ]]; then
			guest_retry_list="${retry_glist}${guest_retry_list}"
		fi
		if [[ ${retry} -eq 1 ]]; then
			((round++))
			if [[ ${retry_max} -lt ${round} ]]; then
				break;
			fi
			if [[ -z ${guest_retry_list} ]]; then
				break;
			else
				guest_list="${guest_retry_list}ReTrYnOw"
				guest_retry_list=""
				echo "Failed to backup some guests, sleep 5 minutes and retry... Round [${round}]"
				${SLEEP} 300
			fi
		fi
	done
	backup_retention
	if [[ ${retry_max} -lt ${round} ]]; then
		local guest_fail_list=""

		for fail_id in ${guest_retry_list}; do
			local fail_guest_name

			json_parser ${tmp_guest_list} ".data.guests[]|select(.guest_id==\"${fail_id}\")|.name" fail_guest_name 0
			guest_fail_list="${guest_fail_list} ${fail_guest_name}"
		done
		${CAT} << EOF > ${log}
====================================
[Time] ${start_time} - $(${DATE})
[Result] Failed to backup following VMs
	$(echo ${guest_fail_list} | ${SED} 's/ /\n\t/g')
====================================
EOF
		exit 1
	else
		exit 2
	fi
}

if [[ -z $@ ]]; then
	usage
fi

for arg in "$@"; do
	key=$(echo ${arg} | ${CUT} -d"=" -f1)
	val=$(echo ${arg} | ${CUT} -d"=" -f2)

	case ${key} in
		--default)
			unset dst_folder
			unset dst_full_path
			unset batch_size
			unset host_name
			unset guests_spec
			unset retent_num
			unset retry_max
			break;
			;;
		--dst)
			dst_folder="${val}"
			;;
		--batch)
			batch_size=${val}
			;;
		--host)
			host_name="${val}"
			;;
		--guests)
			guests_spec=$(echo ${val} | ${TR} '|' '\n')
			;;
		--retent)
			retent_num=${val}
			;;
		--retry)
			retry_max=${val}
			;;
		*)
			usage
			;;
	esac
done

if [[ ! -z ${guests_spec} && ! -z ${host_name} ]]; then
	echo "Options [--host] and [--guests] cannot be specified simultaneously"
	exit 0
fi

IFS=' ' read -r -a arr_share <<< $(${SYNOSHARE} --enum ALL | ${AWK} '2 < NR')
if [[ -z ${arr_share[*]} ]]; then
	echo "Please prepare a shared folder for backup usage first"
	exit 0
elif [[ ! -z ${dst_folder} ]]; then
	if [[ ${arr_share[*]} != *"${dst_folder}"* ]]; then
		echo "The shared folder does not exist"
		echo "Please select one folder below:"
		echo -e "\t${arr_share[*]}"
		exit 0
	fi
else
	dst_folder=${arr_share[0]}
	echo "Does not assign any share folder"
	echo -e "Use default [${dst_folder}]\n"
fi
if [[ -z ${batch_size} ]]; then
	batch_size=5
elif [[ ${batch_size} -eq 0 ]]; then
	echo "batch size cannot be zero"
	exit 0
fi
if [[ -z ${host_name} ]]; then
	host_name="all"
fi
if [[ -z ${retent_num} ]]; then
	retent_num=3
fi
if [[ -z ${retry_max} ]]; then
	retry_max=3
fi

dst_full_path=$(${SYNOSHARE} --get ${dst_folder} | ${GREP} -E 'Path.*volume' | ${CUT} -d'[' -f2 | ${CUT} -d']' -f1)
main

