nas_scripts/zfs_send.zsh

346 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/zsh
backup_server="192.168.0.162" # Сервер для резервирования
backup_user="root" # Пользователь на сервере
inc_snapshot="manual-$(date +%Y%m%d)" # Новый создаваемый снисмок
incremental=false
silent=false
stop=false
progress=true
check=true
LOGFILE="log_bak_${inc_snapshot}.log"
unset local_dataset
unset remote_dataset
echo -ne "Обработка аргументов...\033[2K\r"
while [[ "$#" -gt 0 ]]; do
case $1 in
-rs|--remote-server) backup_server="$2"; shift ;;
-гu|--remote-user) backup_user="$2"; shift ;;
-s|--snapshot) inc_snapshot="$2"; shift ;;
-ld|--local-dataset) local_dataset="$2"; shift ;;
-rd|--remote-dataset) remote_dataset="$2"; shift ;;
-l|--log-file) LOGFILE="$2"; shift ;;
-i|--incremental) incremental=true ;;
--no-progress) progress=false ;;
--no-check) check=false ;;
--stop) stop=true ;;
-h|--help) echo "Использование: ./zfs_send.zsh [OPTIONS]";
echo "-rs | --remote-server <ip/dns>";
echo "-гu | --remote-user <user>";
echo "-s | --snapshot <snapshot_name>";
echo "-ld | --local-dataset <local/dataset>";
echo "-rd | --remote-dataset <remote/dataset>";
echo "-l | --log-file <path/filename>";
echo " --no-progress";
echo " --no-check";
echo " --stop";
echo "-i | --incremental" ;;
*) echo "Неизвестные параметры: $1"; exit 1 ;;
esac
shift
done
# Завершение запущенного процесса резервирования
if [[ $stop = true ]]; then
pids=$(ps -t $(tty | sed 's|/dev/||') -o pid,args | grep -e zfs_send.zsh -e "zfs send" | grep -v -- '--stop') # Список PIDs с командой запуска
if [[ $pids = "" ]]; then
echo "Нет запущенного процесса резервирования в этом терминале."
read "srch?Икать во всех процессах системы? (y/N) "
if [[ $srch = y ]]; then
pids=$(ps -o pid,args | grep -e zfs_send.zsh -e "zfs send" | grep -v -- '--stop' )
fi
fi
# Подтверждение
if [[ $pids = "" ]]; then
echo "Нет запущенного процесса резервирования"
else
echo "Будут остановлены следующие процессы:"
echo $pids
read "work? Продолжать (y/N)"
if [[ $work = y ]]; then
pids=$(echo $pids | awk '{print $1}')
kill $pids
echo "ok"
else
echo "Операция прервана"
fi
fi
exit 0
fi
echo -n "Резервирование на "
# Проверка соединения
if ssh $backup_user@$backup_server 'exit'; then;
echo "$backup_user@$backup_server ok";
else
echo "Настройте ssh и запустите скрипт повторно"
exit 1
fi
echo "Снимок файловой системы $inc_snapshot"
case $incremental in
true) echo "Режим инкрементной копии";;
false) echo "Режим полной копии";;
esac
echo "Дополнительные опции: -h|--help"
# --------- Собираем данные ----------
echo -ne "Собираю данные...\033[2K\r"
# Получить список датасетов на локальной машине
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}) # Конвертирование в массив
# Получить список датасетов на удалённой машине
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}) # Конвертирование в массив
declare -A work_datasets # Словарь ключ = резервируемый локальный датасет, значение = имя датасета на удалённом сервере
# Если заданы local_dataset & remote_dataset
if [[ -v local_dataset && -v remote_dataset ]]; then
work_datasets[$local_dataset]=$remote_dataset
else # Если нет, то вычислить
if [[ $incremental = true ]]; then
# Высчитать что на что резервировать
for ((i = 1; i <= ${#local_datasets}; i++)); do
for ((j = 1; j <= ${#backup_datasets}; j++)); do
a=$(sed -r "s/(.*)\/(.*)/\2/" <<<"$local_datasets[i]") # Определить нижний по иерархии
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
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
loc_name_ds=$(sed -r "s/(.*)\/(.*)/\2/" <<<"$local_datasets[$i]") # Определить нижний по иерархии
bak_ds="$bak_ds/$loc_name_ds" # Добавить имя создаваемого датасета
work_datasets[$local_datasets[$i]]=$bak_ds
else; exit 1; fi ;;
esac
done
fi # Конец блока запроса места резервирования
fi # Конец блока наличия переданных параметров датасетов в скрипт
# Словарь со списком последких снапшотов для датасетов
declare -A last_loc_snaps
echo -ne "Проверяю на наличие $inc_snapshot...\033[2K\r"
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=${loc_snap:1} # удалить @ в начале строки
if [[ $incremental = true ]]; then
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 [[ $incremental = true ]] && [[ "$loc_snap" = "$bak_snap" ]]; then
# Если последние снимки на обеих машинах совпадают в инкрементном режиме
echo "${loc_ds}@${loc_snap} * существует на обеих машинах. Исключено из списка резервируемых"
unset "work_datasets[$loc_ds]"
else
# Если на удалённой машине нет такого снимка, то предложить удалить.
echo "Датасет $loc_ds уже имеет последний снимок $inc_snapshot"
read "act?ok: резервировать | re: пересоздать (удаляет сразу!) | исключить из работы по умолчанию : "
case $act in
re) zfs destroy "${loc_ds}@${loc_snap}";
loc_snap="$(zfs list -t snapshot -o name ${loc_ds} | grep 'manual' | tail -n1 | egrep -o '@.+' )";
loc_snap="${loc_snap:1}";
last_loc_snaps[$loc_ds]=$loc_snap ;;
ok) last_loc_snaps[$loc_ds]=$loc_snap ;;
*) echo "$loc_ds Исключено из списка";
unset "work_datasets[$loc_ds]" ;;
esac
fi
else
# добавить в список снапшотов
if [[ $loc_snap != "" ]]; then
last_loc_snaps[$loc_ds]=$loc_snap
else
if [[ $incremental = true ]]; then
unset "work_datasets[$loc_ds]"
fi
fi
fi
done
# --------- Запрашиваем подтверждение пользователя ----------
echo ""
echo "Список резервируемых датасетов:"
column_widths="%-4s %-35s %-20s -> %-35s\n"
if [[ $incremental = true ]]; then
i=0; for loc_ds bak_ds in ${(kv)work_datasets}; do
(( i++ ))
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
# --------- Выбор датасетов ----------
if [[ $check = true ]]; then
read 'work?Утвердите данные. "y" в работу | "1 2.." выбрать через пробел | НЕТ по умолчанию : '
if [[ $work = *[[:digit:]]* ]]; then
indx_list=(${=work})
# Если нет индекса в списке, то удалить из словаря для резервирования
i=0; for loc_ds bak_ds in ${(kv)work_datasets}; do
i=$((i+1))
if [[ ${indx_list[(r)$i]} != $i ]]; then # Если нет $i в списке $indx_list
unset "work_datasets[$loc_ds]"
else
echo "$i - $loc_ds \t ${last_loc_snaps[$loc_ds]} \t -> \t${bak_ds}"
fi
done
fi
# Если был выведен список, то запросить запуск в работу нового списка
if [[ ${#indx_list} > 0 ]]; then
work="N"
read "work?В работу (y/N) "
fi
else
work="y"
fi
# --------- Резервирование ----------
if [[ $work = "y" || $work = "Y" ]]; then
TS0=$(date +%s)
echo $TS0 > /dev/shm/backup_time_stamp # Сохраняем отметку о времени начала
echo "Результат работы записываю в файл $LOGFILE"
# Функция для записи сообщения в файл и на экран
echo_log () { # $1 = Сообщение
echo $1
echo $1 >> "$LOGFILE"
}
# Записать лог список резервируемых датасетов
echo "--- $(date +'%Y.%m.%d %H:%M.%S') ---" >> "$LOGFILE"
echo "Список резервируемых датасетов:" >> "$LOGFILE"
n_tasks=0
for loc_ds in ${(k)work_datasets}; do
(( n_tasks++ ))
echo "$key \t ${last_loc_snaps[$loc_ds]} \t -> \t${work_datasets[$loc_ds]}" >> "$LOGFILE"
done
echo_log
echo_log "--- Резервирую данные ---"
i_task=0 # Выполненных зачач
if [[ $progress == true ]]; then echo $i_task > /dev/shm/backup_i_task; fi # Сохраняем количество готовых задач в память
for loc_ds bak_ds in ${(kv)work_datasets}; do
TS1=$(date +%s)
if [[ $progress == true ]]; then echo $TS1 > /dev/shm/backup_time_stamp; fi # Сохраняем отметку о времени начала
echo_log " * snapshot ${loc_ds}@${inc_snapshot}"
zfs snapshot ${loc_ds}@${inc_snapshot}; #FIXME выдаёт ошибку если мы оставили существующий снимок
echo_log " * Start sending ${loc_ds} at $(date +'%Y.%m.%d %H:%M.%S')"
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) zfs send -V ${loc_ds}@${inc_snapshot} | ssh "$backup_user@${backup_server}" zfs receive ${bak_ds} ;;
esac
TS2=$(date +%s)
(( i_task++ ))
if [[ $progress == true ]]; then echo $i_task > /dev/shm/backup_i_task; fi
echo_log "--- готово за: $(date -d@$(($TS2-$TS1)) -u '+%H:%M.%S') ---"
done & # Запустить резервирование в фоне
# Выводить прогресс запрашивая список запущенных процессов
if [[ $progress == true ]]; then
echo ""
last_percent=0
time_changed_percent_value=$(date +%s)
while [[ $i_task != $n_tasks ]]; do # выводим статус пока не завершены все задачи
progress=$(ps -u | grep "send" | grep -v "grep" | sed -r "s/(.*) zfs: (.*)/\2/")
percent=$(echo $progress | cut -d "(" -f2 | cut -d "%" -f1 )
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
# Удалить временные файлы состояний
rm /dev/shm/backup_i_task
rm /dev/shm/backup_time_stamp
fi
echo_log "\n--- Все завершено за $(date -d@$(( $(date +%s) - $TS0 )) -u '+%H:%M.%S') ---\n"
fi