feature: Полный резерв, плавный расчёт времени окончания. +bugfixes

This commit is contained in:
root 2024-10-31 07:25:07 +03:00
parent 59da941114
commit ab244ea756

View File

@ -1,99 +1,177 @@
#!/bin/zsh #!/bin/zsh
backup_server="192.168.0.162" # Сервер для резервирования backup_server="192.168.0.162" # Сервер для резервирования
backup_user="root" # Пользователь на сервере
inc_snapshot="manual-$(date +%Y%m%d)" # Новый создаваемый снисмок inc_snapshot="manual-$(date +%Y%m%d)" # Новый создаваемый снисмок
incremental=false
echo -ne "Обработка аргументов...\033[0K\r" echo -ne "Обработка аргументов...\033[2K\r"
if [[ $# == 1 ]]; then while [[ "$#" -gt 0 ]]; do
backup_server=$1 case $1 in
-r|--backup-server) backup_server="$2"; shift ;;
-г|--baсkup-user) backup_user="$2"; shift ;;
-s|--snapshot) inc_snapshot="$2"; shift ;;
-i|--incremental) incremental=true ;;
*) echo "Неизвестные параметры: $1"; exit 1 ;;
esac
shift
done
echo -n "Резервирование на "
# Проверка соединения
if ssh $backup_user@$backup_server 'exit'; then;
echo "$backup_user@$backup_server ok";
else
echo "Настройте ssh и запустите скрипт повторно"
exit 1
fi fi
if [[ $# == 2 ]]; then
backup_server=$1
inc_snapshot=$2
fi
if [[ $# > 2 ]]; then
echo "Ожидается 0 или 2 аргумента: backup_server или ни одного для сервера по умолчанию: $backup_server"
exit
fi
echo "Резервирование на $backup_server"
echo "Снимок файловой системы $inc_snapshot" echo "Снимок файловой системы $inc_snapshot"
case $incremental in
true) echo "Режим инкрементной копии";;
false) echo "Режим полной копии";;
esac
# --------- Собираем данные ---------- # --------- Собираем данные ----------
echo -ne "Собираю данные...\033[0K\r" echo -ne "Собираю данные...\033[2K\r"
# Получить список датасетов на локальной машине # Получить список датасетов на локальной машине
local_datasets=$(zfs list -o name | grep -v -e ix-app -e boot-pool -e system-data -e jails | grep /) # Отрезаем всё лишнее, отделяем нужные данные по 4 пробелам в выводе. local_datasets=$(zfs list -o name | grep -v -e ix-app -e boot-pool -e system-data -e jails -e NAME)
local_datasets=(${=local_datasets}) # Конвертирование в массив local_datasets=(${=local_datasets}) # Конвертирование в массив
# Получить список датасетов на удалённой машине # Получить список датасетов на удалённой машине
backup_datasets=$(ssh root@$backup_server zfs list -o name | grep -v -e ix-app -e boot-pool -e system-data -e jails | grep /) # Отрезаем всё лишнее, отделяем нужные данные по 4 пробелам в выводе. backup_datasets=$(ssh $backup_user@$backup_server zfs list -o name | grep -v -e ix-app -e boot-pool -e system-data -e jails -e NAME)
backup_datasets=(${=backup_datasets}) # Конвертирование в массив backup_datasets=(${=backup_datasets}) # Конвертирование в массив
# Высчитать что на что резервировать declare -A work_datasets # Словарь ключ = резервируемый локальный датасет, значение = имя датасета на удалённом сервере
declare -A work_datasets
for ((i = 1; i <= ${#local_datasets}; i++)); do if [[ $incremental = true ]]; then
for ((j = 1; j <= ${#backup_datasets}; j++)); do # Высчитать что на что резервировать
a=$(sed -r "s/(.*)\/(.*)/\2/" <<<"$local_datasets[i]") # Определить нижний по иерархии for ((i = 1; i <= ${#local_datasets}; i++)); do
b=$(sed -r "s/(.*)\/(.*)/\2/" <<<"$backup_datasets[j]") # тоже дял второго for ((j = 1; j <= ${#backup_datasets}; j++)); do
if [[ "$a" == "$b" ]]; then a=$(sed -r "s/(.*)\/(.*)/\2/" <<<"$local_datasets[i]") # Определить нижний по иерархии
work_datasets[${local_datasets[i]}]=${backup_datasets[j]} b=$(sed -r "s/(.*)\/(.*)/\2/" <<<"$backup_datasets[j]") # тоже для второго
if [[ "$a" == "$b" ]]; then
work_datasets[${local_datasets[i]}]=${backup_datasets[j]}
fi
done
done
else # Если не инкрементная копия, то запросить мео для резервирования
# Вывести на экран запрос
column_widths="| %-4s%-35s | %-4s%-35s\n"
i_max=$(( ${#local_datasets} > ${#backup_datasets} ? ${#local_datasets} : ${#backup_datasets} )) # Максимальное количество элементов
printf $column_widths "№" "Локальные датасеты" "№" "На $backup_server"
printf $column_widths "----" "------------------" "----" "-------------------"
# echo "№ - Локальные\t\t№ - Удалённые"
for ((i = 1; i <= $i_max; i++)); do
if [[ -v local_datasets[i] ]]; then i1=$i; else; i1=""; fi
if [[ -v backup_datasets[i] ]]; then i2=$i; else; i2=""; fi
printf $column_widths "$i1" "${local_datasets[i]}" "$i2" "${backup_datasets[i]}"
done
wrong_enter=true
while $wrong_enter = true ; do
read "loc_ds_indx_list?Локальные индексы через пробел (0=выход; all=режим исключения): "
if [[ $loc_ds_indx_list = "all" ]]; then
loc_ds_indx_list=({1..${#local_datasets}})
break
fi
if [[ $loc_ds_indx_list = *[[:digit:]]* ]]; then
loc_ds_indx_list=(${=loc_ds_indx_list}) # Convert to list
for i in $loc_ds_indx_list; do
if [[ $i = 0 ]]; then exit; fi
if (( $i < 1 )) || (( $i > ${#local_datasets} )); then
echo "$i ! Выход за диапазон"
wrong_enter=true
break
else
wrong_enter=false
fi
done
fi fi
done done
done
echo "Введите номер датасета в котором создать кописю исходного (- исключить, 0 выйти)"
for i in $loc_ds_indx_list; do
read "bak_ds_indx?$i - ${local_datasets[i]} -> "
case $bak_ds_indx in
0) exit;;
-) ;;
*) if (( $bak_ds_indx >= 1 )) && (( $bak_ds_indx <= ${#backup_datasets} )); then
work_datasets[$local_datasets[$i]]=$backup_datasets[bak_ds_indx]
else; exit 1; fi ;;
esac
done
fi # Конец блока запроса места резервирования
# Словарь со списком последких снапшотов для датасетов # Словарь со списком последких снапшотов для датасетов
declare -A last_loc_snaps declare -A last_loc_snaps
declare -A last_bak_snaps echo -ne "Проверяю на наличие $inc_snapshot...\033[2K\r"
echo -ne "Проверяю на наличие $inc_snapshot...\033[0K\r"
for loc_ds bak_ds in ${(kv)work_datasets}; do for loc_ds bak_ds in ${(kv)work_datasets}; do
# Запросить последние снимки # Запросить последние снимки
loc_snap="$(zfs list -t snapshot -o name ${loc_ds} | grep "manual" | tail -n1 | egrep -o '@.+' )" loc_snap="$(zfs list -t snapshot -o name ${loc_ds} | grep 'manual' | tail -n1 | egrep -o '@.+' )"
loc_snap=${loc_snap:1} # удалить @ в начале строки loc_snap=${loc_snap:1} # удалить @ в начале строки
bak_snap="$(ssh root@$backup_server zfs list -t snapshot -o name ${bak_ds} | grep "manual" | tail -n1 | egrep -o '@.+' )" if [[ $incremental = true ]]; then
bak_snap=${bak_snap:1} # удалить @ в начале строки bak_snap="$(ssh $backup_user@$backup_server zfs list -t snapshot -o name ${bak_ds} | grep 'manual' | tail -n1 | egrep -o '@.+' )"
bak_snap=${bak_snap:1} # удалить @ в начале строки
# Если последний снимок совпадает с сегодняшним fi
# echo "$loc_ds @ $loc_snap -> $bak_ds @ $bak_snap"
# Если последний снимок локального датасета совпадает с сегодняшним
if [[ "$loc_snap" = "$inc_snapshot" ]]; then if [[ "$loc_snap" = "$inc_snapshot" ]]; then
# Если последние снимки на обеих машинах совпадают if [[ $incremental = true ]] && [[ "$loc_snap" = "$bak_snap" ]]; then
if [[ "$loc_snap" = "$bak_snap" ]]; then # Если последние снимки на обеих машинах совпадают в инкрементном режиме
echo "${loc_ds}@${loc_snap} * существует на обеих машинах. Исключено из списка резервируемых" echo "${loc_ds}@${loc_snap} * существует на обеих машинах. Исключено из списка резервируемых"
unset "work_datasets[$loc_ds]" unset "work_datasets[$loc_ds]"
else else
# Если на удалённой машине нет такого снимка, то предложить удалить. # Если на удалённой машине нет такого снимка, то предложить удалить.
read "del_snapshot?Датасет $loc_ds уже имеет последний снимок $inc_snapshot. Удалить? (y/N)" echo "Датасет $loc_ds уже имеет последний снимок $inc_snapshot"
if [ "$del_snapshot" = "y" ]; then read "act?ok: резервировать | re: пересоздать (удаляет сразу!) | исключить из работы по умолчанию : "
zfs destroy "${loc_ds}@${loc_snap}" case $act in
loc_snap="$(zfs list -t snapshot -o name ${loc_ds} | grep "manual" | tail -n1 | egrep -o '@.+' )" re) zfs destroy "${loc_ds}@${loc_snap}";
loc_snap="${loc_snap:1}" loc_snap="$(zfs list -t snapshot -o name ${loc_ds} | grep 'manual' | tail -n1 | egrep -o '@.+' )";
last_loc_snaps[$loc_ds]=$loc_snap loc_snap="${loc_snap:1}";
# last_bak_snaps[$bak_ds]=$bak_snap # не нужно, нигде не используется last_loc_snaps[$loc_ds]=$loc_snap ;;
else ok) last_loc_snaps[$loc_ds]=$loc_snap ;;
# Если не удалять, то просто исключить из резервирования *) echo "$loc_ds Исключено из списка";
echo "$ds Исключено из списка" unset "work_datasets[$loc_ds]" ;;
unset "work_datasets[$ds]" esac
fi
fi fi
else else
# добавить в список снапшотов # добавить в список снапшотов
last_loc_snaps[$loc_ds]=$loc_snap if [[ $loc_snap != "" ]]; then
# last_bak_snaps[$bak_ds]=$bak_snap # не нужно, нигде не используется last_loc_snaps[$loc_ds]=$loc_snap
else
if [[ $incremental = true ]]; then
unset "work_datasets[$loc_ds]"
fi
fi
fi fi
done done
# --------- Запрашиваем подтверждение пользователя ---------- # --------- Запрашиваем подтверждение пользователя ----------
echo " " echo ""
echo "Список резервируемых датасетов:" echo "Список резервируемых датасетов:"
i=0; for loc_ds bak_ds in ${(kv)work_datasets}; do column_widths="%-4s %-35s %-20s -> %-35s\n"
i=$((i+1))
echo "$i - $loc_ds \t ${last_loc_snaps[$loc_ds]} \t -> \t${bak_ds}" if [[ $incremental = true ]]; then
done i=0; for loc_ds bak_ds in ${(kv)work_datasets}; do
echo (( i++ ))
echo "Инкрементный снимок:\t$inc_snapshot" printf $column_widths $i $loc_ds "${last_loc_snaps[$loc_ds]}" $bak_ds
done
printf "%-4s %-35s %-20s\n" "" "Инкрементный снимок:" "$inc_snapshot (*)"
else
i=0; for loc_ds bak_ds in ${(kv)work_datasets}; do
(( i++ ))
printf $column_widths $i $loc_ds "$inc_snapshot (*)" $bak_ds
done
fi
# --------- Выбор датасетов ---------- # --------- Выбор датасетов ----------
read "work?Утвердите данные. В работу (y), или перечислить через пробел номера, НЕТ по умолчанию? " read 'work?Утвердите данные. "y" в работу | "1 2.." выбрать через пробел | НЕТ по умолчанию : '
if [[ $work = *[[:digit:]]* ]]; then if [[ $work = *[[:digit:]]* ]]; then
indx_list=(${=work}) indx_list=(${=work})
# Если нет индекса в списке, то удалить из словаря для резервирования # Если нет индекса в списке, то удалить из словаря для резервирования
@ -118,7 +196,8 @@ fi
if [[ $work = "y" || $work = "Y" ]]; then if [[ $work = "y" || $work = "Y" ]]; then
TS0=$(date +%s) TS0=$(date +%s)
LOGFILE="backup_${inc_snapshot}.log" echo $TS0 > /dev/shm/backup_time_stamp # Сохраняем отметку о времени начала
LOGFILE="log_bak_${inc_snapshot}.log"
echo "Результат работы записываю в файл $LOGFILE" echo "Результат работы записываю в файл $LOGFILE"
# Записать в файл список резервируемых датасетов # Записать в файл список резервируемых датасетов
@ -126,7 +205,7 @@ if [[ $work = "y" || $work = "Y" ]]; then
echo "Список резервируемых датасетов:" >> "${LOGFILE}" echo "Список резервируемых датасетов:" >> "${LOGFILE}"
n_tasks=0 n_tasks=0
for loc_ds in ${(k)work_datasets}; do for loc_ds in ${(k)work_datasets}; do
n_tasks=$((n_tasks+1)) (( n_tasks++ ))
echo "$key \t ${last_loc_snaps[$loc_ds]} \t -> \t${work_datasets[$loc_ds]}" >> "${LOGFILE}" echo "$key \t ${last_loc_snaps[$loc_ds]} \t -> \t${work_datasets[$loc_ds]}" >> "${LOGFILE}"
done done
@ -134,30 +213,65 @@ if [[ $work = "y" || $work = "Y" ]]; then
exec 6>&1 # Saves stdout exec 6>&1 # Saves stdout
exec >&1 >>$LOGFILE 2>&1 # Вывод на экран и в файл exec >&1 >>$LOGFILE 2>&1 # Вывод на экран и в файл
echo "\n--- Резервирую данные ---" echo "--- Резервирую данные ---"
i_task=0 # Выполненных зачач i_task=0 # Выполненных зачач
echo $i_task > /dev/shm/backup_i_task # Сохраняем количество готовых задач в память
for loc_ds bak_ds in ${(kv)work_datasets}; do for loc_ds bak_ds in ${(kv)work_datasets}; do
TS1=$(date +%s) TS1=$(date +%s)
echo $TS1 > /dev/shm/backup_time_stamp # Сохраняем отметку о времени начала
echo " * snapshot ${loc_ds}@${inc_snapshot}" echo " * snapshot ${loc_ds}@${inc_snapshot}"
zfs snapshot ${loc_ds}@${inc_snapshot}; zfs snapshot ${loc_ds}@${inc_snapshot}; #FIXME выдаёт ошибку если мы оставили существующий снимок
echo " * Start sending ${loc_ds} at $(date +'%Y.%m.%d %H:%M.%S')" echo " * Start sending ${loc_ds} at $(date +'%Y.%m.%d %H:%M.%S')"
zfs send -V -i ${loc_ds}@${last_loc_snaps[$loc_ds]} ${loc_ds}@${inc_snapshot} | ssh "root@${backup_server}" zfs receive ${bak_ds}@${inc_snapshot} case $incremental in
true) zfs send -V -i ${loc_ds}@${last_loc_snaps[$loc_ds]} ${loc_ds}@${inc_snapshot} | ssh "$backup_user@${backup_server}" zfs receive ${bak_ds}@${inc_snapshot} ;;
false) loc_name_ds=(${(@s:/:)loc_ds}); loc_name_ds=$loc_name_ds[-1] # Получить имя датасета без пути
bak_ds="$bak_ds/$loc_name_ds"
zfs send -V ${loc_ds}@${inc_snapshot} | ssh "$backup_user@${backup_server}" zfs receive ${bak_ds} ;;
esac
TS2=$(date +%s) TS2=$(date +%s)
i_task=$((i_task+1)) (( i_task++ ))
echo "------ готово за: $(date -d@$(($TS2-$TS1)) -u '+%H:%M:%S') " echo $i_task > /dev/shm/backup_i_task
echo "------ готово за: $(date -d@$(($TS2-$TS1)) -u '+%H:%M.%S') ------"
done & # Запустить резервирование в фоне done & # Запустить резервирование в фоне
exec 1>&6 # Restore stdout. Не писать в файл лога отследивание процента exec 1>&6 # Restore stdout. Не писать в файл лога отследивание процента
# Выводить прогресс запрашивая список запущенныйх процессов # Выводить прогресс запрашивая список запущенныйх процессов
echo ""
while [[ $i_task != $n_tasks ]]; do last_percent=0
PROGR=$(ps -u | grep "send" | grep -v "grep" | sed -r "s/(.*) zfs: (.*)/\2/") time_changed_percent_value=$(date +%s)
echo -ne "$PROGR" while [[ $i_task != $n_tasks ]]; do # выводим статус пока не завершены все задачи
dots_per_min=10 progress=$(ps -u | grep "send" | grep -v "grep" | sed -r "s/(.*) zfs: (.*)/\2/")
for ((i = 0; i < dots_per_min; i++)); do echo -n "."; sleep ((60 / dots_per_min)); done percent=$(echo $progress | cut -d "(" -f2 | cut -d "%" -f1 )
echo -e "\r"; if (( percent > 0 )); then
now=$(date +%s)
if (( percent != last_percent )); then
time_changed_percent_value=$now
last_percent=$percent
TS1=$(< /dev/shm/backup_time_stamp)
elapsed=$(( $(date +%s) - TS1 ))
fi
time_part_of_percent=$(( now - time_changed_percent_value )) # Время с последнего изменения процента
estimated_total=$(( 100 / percent * elapsed )) # Всего времени на задачу
estimated_remain=$(( estimated_total - elapsed - time_part_of_percent )) # Осталось времени на задачу
if (( estimated_remain >= 86400 )) || (( estimated_total >= 86400 )); then
estimated_remain_days=$(( estimated_remain / 86400 )); estimated_remain_time=$(( estimated_remain % 86400 ))
estimated_total_days=$(( estimated_total / 86400 )); estimated_total_time=$(( estimated_total % 86400 ))
echo -n "\033[2K\r$progress $estimated_remain_days д. $(date -d@$estimated_remain_time -u '+%H:%M.%S') ост. / $estimated_total_days д. $(date -d@$estimated_total_time -u '+%H:%M.%S') "
else
echo -n "\033[2K\r$progress $(date -d@$estimated_remain -u '+%H:%M.%S') ост. / $(date -d@$estimated_total -u '+%H:%M.%S') "
fi
sleep 1
else
echo -ne "\033[2K\r$progress "
sleep 1
for ((i = 1; i < 3 ; i++)); do echo -n "."; sleep 1; done
fi
i_task=$(< /dev/shm/backup_i_task)
done done
# Удалить временные файлы состояний
rm /dev/shm/backup_i_task
rm /dev/shm/backup_time_stamp
exec >&1 >>$LOGFILE 2>&1 # Вывод на экран и в файл exec >&1 >>$LOGFILE 2>&1 # Вывод на экран и в файл
echo "\n--- Все завершено за $(date -d@$(($TS2-$TS0)) -u '+%H:%M.%S') ---" echo "\n--- Все завершено за $(date -d@$(($TS2-$TS0)) -u '+%H:%M.%S') ---"