#!/bin/bash

set -eu
# __SILENT=TRUE
. /opt/fox_utils/crab_sys.sh
[ "${1:-}" = "--help" ] && sys::usage "$@"
### --help Info: info по vz7
### --help Usage: vz_cli|kvm_cli backup $VM_NAME [node_name|node_ip|--local]
### --help Usage: [ --online ] [-i/root/.ssh/id_rsa] [--skip-check_node]
### --help Example:
sys::arg_parse "$@"

. /opt/fox_utils/fox_conf vm get "$ARG_1"

if [ -f /tmp/vz_backup.$$ ]; then
	VM_NODE_BACKUP=$(</tmp/vz_backup.$$)
	rm -f /tmp/vz_backup.$$
fi
[ -n "${ARG_2:-}" ] && VM_NODE_BACKUP="$ARG_2"
[ "${ARG_LOCAL:-}" = TRUE ] && VM_NODE_BACKUP="$VM_NODE_LOCATION"

if [ -z "${VM_NODE_BACKUP:-}" ]; then
	echo 'Укажите VM_NODE_BACKUP в vm.conf или в $2'
	exit 255
fi
if [ -f "/opt/fox_conf/node/${VM_NODE_BACKUP}/node.conf" ]; then
	(
		NODE_STATE=
		NODE_REDIRECT=
		. "/opt/fox_conf/node/${VM_NODE_BACKUP}/node.conf"
		if [ "${NODE_STATE:-}" = "failed" ]; then
			echo "WARNING $VM_NODE_BACKUP is ${NODE_STATE:-} redirect to $NODE_REDIRECT"
			echo "$NODE_REDIRECT" > /tmp/vz_backup.$$
		fi
	)
fi


__exit(){
	local t pgid
	/opt/fox_utils/crab_unlock "/tmp/node_disk_io.lock" $$
	if [ -x /opt/fox_node/centos7fox/storage_wcache_zram_init ]; then
		read -r t t t t pgid t< /proc/self/stat
		/opt/fox_node/centos7fox/storage_wcache_zram_init nocache_remove_pid $pgid
		/opt/fox_node/centos7fox/storage_ssd_init nocache_remove_pid $pgid
	fi
	exit $1
	return 0
}
echo "TODO1 /fox_utils/fox_ploop_backup 1.snapshot 2.backup node4"

# разрешаем запускать только 1 бекап тк последовательно быстрее чем параллельно
if ! /opt/fox_utils/crab_lock "/tmp/node_disk_io.lock" $$ 300; then
	echo "Disk busy /tmp/vm_disk.lock, plz try later"
	__exit 255
fi

if [ -x /opt/fox_node/centos7fox/storage_wcache_zram_init ]; then
	read -r t t t t pgid t< /proc/self/stat
	/opt/fox_node/centos7fox/storage_wcache_zram_init nocache_add_pid $pgid
	/opt/fox_node/centos7fox/storage_ssd_init nocache_add_pid $pgid
fi

TMP_IP_FIRST="${VM_IP%% *}"
VM_STATUS=""
VM_BACKUP_ONLINE="${VM_BACKUP_ONLINE:-TRUE}"
if [ "$VM_TYPE" = "vz7" ]; then
	PLOOP_NAME="$VM_NAME"
	PLOOP_PRIVATE="$VM_DISK"
	status="$(vzctl status $PLOOP_NAME)"
	if [[ "$status" == *' running'* ]]; then
		VM_STATUS=running
	fi
elif [ "$VM_TYPE" = "kvm" ]; then
	PLOOP_NAME="kvm-$VM_NAME"
	PLOOP_PRIVATE="${VM_DISK/root/private}"
	dominfo=$( virsh dominfo "$VM_NAME" ) || true
	if [[ "$dominfo" == *running* ]]; then
		VM_STATUS=running
	fi
fi

# TODO0 сделать в случае онлайн бекапа без CRIO sync внутри контейнера
# CRIO не умеет сохранять вложенные cgroups, встречается у докера
declare SKIP_SUSPEND="${VM_BACKUP_SKIP_SUSPEND:-FALSE}"

if [ "${ARG_ONLINE:-}" = "TRUE" ] ;then
	VM_BACKUP_ONLINE=TRUE
	SKIP_SUSPEND=FALSE
fi

if [ -d "/sys/fs/cgroup/pids/machine.slice/${VM_UUID:-}/docker" ]; then
	SKIP_SUSPEND=TRUE
fi

BK_DATESEC=$(date +%s)

# ssh -oStrictHostKeyChecking=no 127.0.0.1 -o 'BatchMode=yes' 'ls'
# не используте здесь пробел -oBatchMode=yes
SSH_OPT='-oStrictHostKeyChecking=no -oBatchMode=yes '
if [ -n "${ARG_I:-}" ]; then
	SSH_OPT="${SSH_OPT} -i${ARG_I} "
elif [ -s "${VM_DIR}/backup_id_rsa" ]; then
	echo "TODO0 вернуть работу через ${VM_DIR}/backup_id_rsa"
	# chmod 0600 ${VM_DIR}/backup_id_rsa
	# SSH_OPT="${SSH_OPT} -i${VM_DIR}/backup_id_rsa "
fi

if [[ "$VM_NODE_BACKUP" == *:* ]]; then
	SSH_PORT="${VM_NODE_BACKUP#*:}"
	VM_NODE_BACKUP="${VM_NODE_BACKUP%:*}"
	SSH_OPT="${SSH_OPT} -p$SSH_PORT "
fi

BACKUP_SERVER="${VM_NODE_BACKUP}"
if [ -f "/opt/fox_conf/node/${VM_NODE_BACKUP}/node.conf" ]; then
	BACKUP_SERVER=$( . "/opt/fox_conf/node/${VM_NODE_BACKUP}/node.conf"; echo $NODE_IP; )
fi

# Для локального бекапа используем старый алгоритм
# ssh заменяется хаком
if [ "$VM_NODE_BACKUP" = "$VM_NODE_LOCATION" ]; then
	BACKUP_DST="/vz/backup/$PLOOP_NAME"
	__ssh(){
		set -e
		shift
		eval "$@"
		return 0
	}
	__rsync(){
		rsync "$@"
		return 0
	}

else
	BACKUP_DST="${BACKUP_SERVER}:/vz/backup/$PLOOP_NAME"
	__ssh(){
		set -e
		/opt/fox_utils/crab_ssh ${SSH_OPT} "$@"
		return 0
	}
	__rsync(){
		rsync -e "ssh $SSH_OPT" "$@"
		return 0
	}
fi

ssh_ok=$(__ssh $BACKUP_SERVER "echo TRUE" || true)
if [ "$ssh_ok" != TRUE ] ; then
	echo "ssh $BACKUP_SERVER $SSH_OPT \"touch /tmp/vz_backup\""
	echo "Не настроен ssh use: ssh-copy-id -i ${VM_DIR}/backup_id_rsa $BACKUP_SERVER"
	__exit 255
fi
set -o pipefail
VM_CUR_SIZE="$( du -s -B G $PLOOP_PRIVATE | awk '{print $1}' )"
set +o pipefail

__ssh $BACKUP_SERVER 'mkdir -p /vz/backup/'

if [ "${ARG_SKIP_CHECK_NODE:-}" != TRUE ]; then
	exists_resource_check=$(__ssh $BACKUP_SERVER \
		'test -x /opt/fox_node/centos7fox/node_resource_check && echo TRUE; exit 0' )
	if [ "$exists_resource_check" = TRUE ]; then
		__ssh $BACKUP_SERVER /opt/fox_node/centos7fox/node_resource_check \
			--alloc-size="$VM_CUR_SIZE" --disk=/vz/backup/
	else
		node_backup_freespace=$(__ssh $BACKUP_SERVER \
			'echo $((`stat -f -c %a /vz/backup/` * 4096 /1024/1024/1024 ))' )
		if ((${VM_CUR_SIZE/G/} > $node_backup_freespace)); then
			echo "No Free space on $BACKUP_SERVER "\
				"${VM_CUR_SIZE/G/} > $node_backup_freespace"
			__exit 255
		fi
	fi
fi

__convert_format(){
	local exists_old_backup
	exists_old_backup=$(__ssh $BACKUP_SERVER \
		"test -d /vz/backup/$PLOOP_NAME/private && echo TRUE; exit 0;" )
	if [ "$exists_old_backup" = TRUE ]; then
		__ssh $BACKUP_SERVER "mv /vz/backup/$PLOOP_NAME /vz/backup/${PLOOP_NAME}.delme;
		mkdir -p /vz/backup/$PLOOP_NAME;
		mv /vz/backup/${PLOOP_NAME}.delme/private /vz/backup/${PLOOP_NAME}/disk"
	fi
	return 0
}

__exists_snap(){
	set -e
	local ret
	# name дважды специально чтоб пробел получить
	ret=$(vzctl snapshot-list $PLOOP_NAME -H -o uuid,name,name)
	[[ " $ret " == *" $1 "* ]] && echo TRUE || echo FALSE
	return 0
}

__get_uuid_by_snap(){
	set -e
	vzctl snapshot-list $PLOOP_NAME -H -o uuid,name | grep -m1 "$1\$" \
		| head -n 1 | sed 's/{\(.*\)} .*/\1/' || true
	return 0
}

delete_snap(){
	local uuid=$( __get_uuid_by_snap $1 )
	rm -f $PLOOP_PRIVATE/dump/*\{${uuid}\}.tar.lzo
	vzctl snapshot-delete "$PLOOP_NAME" --id=$uuid
	return 0
}

__get_known_errors_pattern(){
	set -e
	local known_errors_pattern
	known_errors_pattern="Can't dump nested .* namespace"
	# (00.037500) Error (criu/namespaces.c:419): Can't dump nested ipc namespace for 236454
	# (00.047999) Error (criu/namespaces.c:419): Can't dump nested pid namespace for 52666
	known_errors_pattern+="\|Can't make .* id"
	# (00.037502) Error (criu/namespaces.c:669): Can't make ipcns id
	# (00.048002) Error (criu/namespaces.c:655): Can't make pidns id
	known_errors_pattern+="\|Unseizable non-zombie"
	# (00.017648) Error (compel/src/lib/infect.c:236):
	#   Unseizable non-zombie 1040362 found, state T, err -1/10
	# (00.017682) Error (compel/src/lib/infect.c:236):
	#   Unseizable non-zombie 954 found, state T, err -1/10
	known_errors_pattern+="\|unsupported stop signal"
	# (00.017560) Error (compel/src/lib/infect.c:303):
	#   SEIZE 1040362: unsupported stop signal 20
	# (00.017602) Error (compel/src/lib/infect.c:303):
	#   SEIZE 954: unsupported stop signal 20
	known_errors_pattern+="\|Can't lookup mount"
	# (02.711742) Error (criu/files-reg.c:1680):
	#   Can't lookup mount=4145 for fd=-3 path=/usr/lib/rtkit/rtkit-daemon
	# (02.711747) Error (criu/cr-dump.c:1592):
	#   Collect mappings (pid: 595129) failed with -1
	# Есть информация, что в 2020 году, исправили в криу.
	# https://github.com/checkpoint-restore/criu/issues/860
	known_errors_pattern+="\|Can't dump file"
	# (02.775788) Error (criu/files-ext.c:96):
	#   Can't dump file 10 of that type [20620] (chr 4:0)
	# (02.775820) Error (criu/cr-dump.c:1695): Dump files (pid: 632720) failed with -1
	# (02.792994) Error (criu/cr-dump.c:2108): Dumping FAILED.
	# https://github.com/checkpoint-restore/criu/issues/441
	known_errors_pattern+="\|tty: Found slave peer index"
	# При запуске тасков, например так:
	# setsid ./venv/bin/python main.py --ch_url='http://127.0.0.1:8123' --ip=10.10.18.223 --port=5140
	#   &>>/var/log/nginx_clickhouse_log.log & disown -a
	# у criu есть опция --shell-job, но не факт, что поможет.
	# (03.517183) Error (criu/tty.c:911): tty: Found slave peer index 1 without correspond master peer
	known_errors_pattern+="\|Can't wait or bad status"
	# (00.100329) Error (criu/util.c:1497): Can't wait or bad status: errno=0, status=65280
	known_errors_pattern+="\|Unsupported link 3 (type 1 kind dummy)"
	# (03.280061) Error (criu/net.c:951): net: Unsupported link 3 (type 1 kind dummy)
	# (03.300670) Error (criu/namespaces.c:1519): Namespaces dumping finished with error 65280
	known_errors_pattern+="\|Can't dump nested uts namespace for"
	# (03.179680) mnt: Dumping mountpoints
	# (03.179683) mnt:        2368: 8e:/ @ ./run/docker/netns/97354fe8b82f
	# (03.179702) Error (criu/filesystems.c:412): Can't readlink 1/ns/pid at mount 2368
	# : Not a directory
	# TODO: нужно разобраться, почему /run/docker/netns/97354fe8b82f есть в /proc/mounts
	# но при этом является файлом, а не директорией
	known_errors_pattern+="\|Can't readlink 1/ns/pid at mount"
	# TODO1: мб можно как-то запатчить контейнеры, чтобы они не использовали неймспейсы внутри
	# TODO0: нужно собирать статистику, сколько виртуалок в итоге бекапятся наживую
	known_errors_pattern+="\|External socket is used"
	# (00.864773) Error (criu/sk-unix.c:842): unix: External socket is used.
	# Consider using --ext-unix-sk option.
	# (00.876561) Error (criu/cr-dump.c:2164): Dumping FAILED.
	# рекомендуется использовать опцию --ext-unix-sk, но возможно будет проблема с рестором
	known_errors_pattern+="\|Can't resolve name for socket"
	# (02.076698) Error (criu/sk-unix.c:343): unix: Can't resolve name for socket 0x1c5
	# (02.076710) Error (criu/cr-dump.c:1851): Dump files (pid: 620544) failed with -1
	# (02.094256) Error (criu/cr-dump.c:2275): Dumping FAILED.
	echo "$known_errors_pattern"
	return 0
}

create_snap(){
	local skip_suspend_flag="" ret known_errors_pattern
	if [ "$VM_STATUS" = running ] && [ "${VM_BACKUP_ONLINE:-}" = "FALSE" ]; then
		echo TODO --skip-umount
		echo TODO echo 2  hostd/rootfs/proc/sys/fs/fsync-enable
		echo TODO exec sync
		echo TODO echo 0 cache_all
		# для поддержки kvm используем vm
		/opt/fox_utils/vm stop "$VM_NAME"
		echo "TODO2 /opt/fox_node/bin/node_storage cache-stop"
	elif [ "$VM_STATUS" = running ] && [ "${VM_BACKUP_ONLINE:-}" = "TRUE" ] \
		&& [ "$SKIP_SUSPEND" == "FALSE" ]; then
		# на время снапшота дропаем пакеты, чтобы не показывать nginx 500 error
		local ipt_rules=$(iptables -nvL OUTPUT)
		if [[ "$ipt_rules" != *" $TMP_IP_FIRST "* ]]; then
			iptables -I OUTPUT -d "$TMP_IP_FIRST" -m state --state NEW -j DROP
		fi
	fi
	if [ "$SKIP_SUSPEND" == "TRUE" ]; then
		skip_suspend_flag="--skip-suspend"
	fi

	# Делаем 2 попытки если нет skip-suspend
	if vzctl snapshot "$PLOOP_NAME" ${skip_suspend_flag} --name "$@" &>/tmp/vz_create_snap.$$; then
		cat /tmp/vz_create_snap.$$
		rm -f /tmp/vz_create_snap.$$
	else
		ret=$?
		cat /tmp/vz_create_snap.$$ >&2
		if [ "$SKIP_SUSPEND" == "TRUE" ]; then
			echo "LOG_ERROR Не удаётся создать снапшот." >&2
			exit $ret
		fi
		# Часто бывает, что снапшот не создаётся, если контейнер не сериализуется
		known_errors_pattern="$(__get_known_errors_pattern)"

		echo "Не удалось создать снапшот"
		if grep -qm1 "$known_errors_pattern" /tmp/vz_create_snap.$$; then
			echo "Отключаем создания дампа памяти"
			# Для известных ошибок делаем вторую попытку без сериализации
			skip_suspend_flag="--skip-suspend"
		fi
		rm -f /tmp/vz_create_snap.$$
		# Удалим неудавшийся дамп, по маске, т.к. имя случайное.
		# Могут удалиться и старые фейлы, но бодет ещё попытка и она останется.
		rm -rf /vz/private/*/dump/*.fail
		echo "Пробуем создать снапшот второй раз"
		# Здесь можем криво упасть, оставив dump/...fail
		vzctl snapshot "$PLOOP_NAME" ${skip_suspend_flag} --name "$@" && ret=0 || ret=$?
		if [[ "$ret" != 0 ]]; then
			# Удаляем дропы безусловно - хуже не будет.
			iptables -D OUTPUT -d "$TMP_IP_FIRST" -m state --state NEW -j DROP || true
			iptables -D OUTPUT -d "$TMP_IP_FIRST" -m state --state NEW -j DROP || true
			echo "LOG_ERROR Не удаётся создать снапшот." >&2
			exit $ret
		fi
	fi
	if [ "$VM_STATUS" = running ] && [ "${VM_BACKUP_ONLINE:-}" = "FALSE" ]; then
		/opt/fox_utils/vm start "$VM_NAME"
	elif [ "$VM_STATUS" = running ] && [ "${VM_BACKUP_ONLINE:-}" = "TRUE" ] \
		&& [ "$SKIP_SUSPEND" == "FALSE" ]; then
		iptables -D OUTPUT -d "$TMP_IP_FIRST" -m state --state NEW -j DROP || true
		iptables -D OUTPUT -d "$TMP_IP_FIRST" -m state --state NEW -j DROP || true
	fi
	return 0
}

__get_file_by_snap(){
	set -e
	local uuid fuuid
	uuid=$( __get_uuid_by_snap "$1" )
	# parent_uuid используется здесь правильно тк немного путаницы в ploop->vzctl
	fuuid=$( ploop snapshot-list -H -o parent_uuid,FNAME \
		$PLOOP_PRIVATE/root.hdd/DiskDescriptor.xml | grep $uuid | awk '{ print $2 }' || true )
	fuuid="${fuuid##*/}"
	echo "$fuuid"
	return 0
}
__convert_format


# чтобы все бекапы были одинаковые и скрипты проще делаем backup_snap и для stopped
if [ "$(__exists_snap backup_cur_state)" = TRUE ]; then
	delete_snap backup_cur_state
fi
if [ "$(__exists_snap backup_snap)" = FALSE ]; then
	create_snap backup_snap
fi
if [ "$VM_STATUS" = running ]; then
	# для стабилизации нижних снепшотов сброс write кеша происходит
	# todo возможно устарело и синк не нужен
	# а возможно нужен и без онлайн
	if [ "$VM_BACKUP_ONLINE" = "TRUE" ]; then
		# В некоторых случаях после удаления снапшота он не мержится в backup_snap
		# из-за этого в бекапах на определенную дату может более старая версия.
		# Небольшой хак, создаем промежуточный снапшот и удаляем в обратном порядке
		# по отношению к backup_snap.
		create_snap backup_cur_state --skip-suspend
		create_snap backup_cur_state_tmp --skip-suspend
		delete_snap backup_cur_state
		delete_snap backup_cur_state_tmp
	fi
	# после синхронизации создаем снепшот на время бекапа
	create_snap backup_cur_state
fi

BK_DATESEC=$(date +%s)

# чтобы tmp_snap не попал xml
if [ "$VM_STATUS" = running ]; then
# todo проверить вымывание кеша
# по идее не должно быть сильно тк последовательно делаем бекапы
# и старые dump памяти удаляем, и свободную память юзает новый бекап
	backup_cur_state_uuid=$( __get_uuid_by_snap backup_cur_state )
	backup_cur_state_file=$( __get_file_by_snap backup_cur_state )
else
	backup_cur_state_uuid=FALSE
	backup_cur_state_file=FALSE
fi
for d in ${PLOOP_PRIVATE}/dump/\{*\}; do
	[ ! -d "$d" ] && continue;
	(
		cd "${PLOOP_PRIVATE}/dump";
		sh_name="${d##*/}";
		if [ ! -f *.${sh_name}.tar.lzo ]; then
			set -o pipefail
			tar -c "${sh_name}" | lzop -1 >"${BK_DATESEC}.${sh_name}.tar.lzo"
			set +o pipefail
		fi
		rm -rf --one-file-system "${sh_name}";
	)
done
if [ -d "${PLOOP_PRIVATE}/dump/Dump" ]; then
	(
		cd "${PLOOP_PRIVATE}/dump"
		set -o pipefail
		rm -f Dump.tar Dump.tar.lzo
		tar -c Dump | lzop -1 > Dump.tar.lzo
		set +o pipefail
		rm -rf --one-file-system Dump
	)
fi
/opt/fox_utils/crab_sync -O "${SSH_OPT// /,}" -r \
	--exclude="*$backup_cur_state_file,*.restore/*,*.fail/*,*.lck,*.statfs,*/tmp/*,*/dump/*.fail" \
	-B $((4*1024*1024)) -D "$BK_DATESEC" "$PLOOP_PRIVATE" "$BACKUP_DST/disk/"
# если снепшот больше 16г то это слишком долго
# почему не сделали это перед бекапом?
# потому что перед чисткой снепшотов надо иметь бекап на крайний случай
if [ "$VM_STATUS" = running ]; then
	backup_snap_file=$(__get_file_by_snap backup_snap)
	backup_snap_file_size=$(stat -c %s $PLOOP_PRIVATE/root.hdd/$backup_snap_file)
# todo если виртуалка дает большие дельты, то бекапы делать реже c пропусками
# и новый снеп раз в мес только предложить чистку vm analyze или придумать более хитрый алгоритм
# todo после нового алгоритма
	backup_snap_max_size="${VM_BACKUP_SNAP_SIZE:-16g}"
	backup_snap_max_size="${backup_snap_max_size//[kK]/*1024}"
	backup_snap_max_size="${backup_snap_max_size//[mM]/*1024*1024}"
	backup_snap_max_size="${backup_snap_max_size//[gG]/*1024*1024*1024}"
	backup_snap_max_size="$(($backup_snap_max_size))"
	echo "LOG_DEBUG backup_snap_max_size = $backup_snap_max_size"
	if [[ $backup_snap_file_size -gt $backup_snap_max_size ]]; then
		# time /root/crb_cache/adlermap ./root.hdd $((4096*1024)) 50G=real    3m42.405s
# 16gb почти сразу залетает в flashcache_write (при memtotal32гб) и не мешает другим
# больше можно при memtotal64гб, но обсудить с осв.
# для большой скорости разрешаем кеширование записи zram
		if [ -x /opt/fox_node/centos7fox/storage_wcache_zram_init ]; then
			read -r t t t t pgid t< /proc/self/stat
			/opt/fox_node/centos7fox/storage_wcache_zram_init nocache_remove_pid $pgid
			# на ssd кеширование не включаем, тк для скорости достаточно zram
			# а вымывать ssd нет смысла
			# /opt/fox_node/centos7fox/storage_ssd_init nocache_remove_pid $pgid
		fi

		echo "LOG_WARNING backup snap too large, merge it:" \
			"size $backup_snap_file_size -gt $backup_snap_max_size"
		delete_snap backup_cur_state
		delete_snap backup_snap
# Не алярмим, т.к. отслеживаем частые разрастания снапшотов с помощью еженедельного report
# /opt/fox_utils/fox_alarm "backup_snap_size "\
#	"$((backup_snap_file_size/1024/1024/1024))GB more 16GB $VM_DIR"
	else
		delete_snap backup_cur_state
	fi
fi
__rsync --delete -a "$VM_DIR/" "$BACKUP_DST/conf/"
__rsync -a  "$BACKUP_DST/disk/.INFO/" "$PLOOP_PRIVATE/.BACKUP/"
/opt/fox_utils/crab_conf commit "$VM_DIR/" "vm_backup: "

/opt/fox_utils/crab_unlock "/tmp/node_disk_io.lock" $$
if [ -f "${PLOOP_PRIVATE}/dump/Dump.tar.lzo" ]; then
	(
		cd "${PLOOP_PRIVATE}/dump"
		rm -rf "Dump.tar"
		lzop -d  "Dump.tar.lzo"
		tar -xf "Dump.tar"
		rm -f Dump.tar Dump.tar.lzo
	)
fi

exit 0
