nas_scripts/zfs_send.zsh

465 lines
19 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
progress_only=false
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 ;;
-ru|--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 ;;
--progress-only) progress_only=true ;;
--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 " --progress-only";
echo " --no-check";
echo " --stop";
echo "-i | --incremental" ;
echo "-p | --progress" ;;
*) echo "Неизвестные параметры: $1"; exit 1 ;;
esac
shift
done
# Функция для записи сообщения в файл и на экран
echo_log () {
local text=$1
echo $text
echo $text >> "$LOGFILE"
}
# Функция для определения SSH-ключа через конфигурацию
find_ssh_key() {
local key
# Получаем IdentityFile из конфигурации
identity_files=$(ssh -G "$backup_user@$backup_server" 2>/dev/null | awk '/^IdentityFile / {print $2}')
# Проверяем каждый IdentityFile из конфига
for file in $identity_files; do
if [[ -f "$file" ]]; then
echo "$file"
return
fi
done
# Если ничего не найдено, ищем стандартные ключи
for candidate in ~/.ssh/id_rsa ~/.ssh/id_ecdsa ~/.ssh/id_ed25519; do
if [[ -f "$candidate" ]]; then
echo "$candidate"
return
fi
done
# Если ключей нет, создаем новый
# echo "Создаём новый ключ..."
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 >&2
echo "~/.ssh/id_ed25519"
}
# Определение ключа
key_path=$(find_ssh_key)
key_pub_path="${key_path}.pub"
check_ssh_connection() {
ssh -o "BatchMode=yes" -o "ConnectTimeout=5" "$backup_user@$backup_server" exit 2>/dev/null
return $?
}
setup_ssh_key() {
key_content=$(cat "$key_pub_path")
echo "Установка SSH-ключа на $backup_user@$backup_server..."
# Передача публичного ключа вручную
ssh $backup_user@$backup_server "mkdir -p ~/.ssh && echo '$key_content' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && chmod 700 ~/.ssh"
echo "
host $backup_server
hostname $backup_server
user $backup_user
identityFile $key_path
identitiesOnly yes
" >> ~/.ssh/config
if [[ $? -ne 0 ]]; then
echo "Ошибка: Не удалось установить SSH-ключ. Попробуйте прописать его на сервер вручную."
echo "----------"
echo $key_content
echo "----------"
exit 1
else
echo "SSH-ключ успешно установлен!"
fi
}
# Если нужно вывести только прогресс, то пропускаем все этапы подготовки и резервирования
if [[ $progress_only = false ]]; then
# Завершение запущенного процесса резервирования
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 "Резервирование на $backup_user@$backup_server"
if check_ssh_connection; then
echo "Connection OK"
else
echo "SSH-ключ не обнаружен на сервере. Начинаем настройку..."
setup_ssh_key
if check_ssh_connection; then
echo "Connection OK"
else
echo "Ошибка: SSH-ключ не работает. Проверьте настройки."
exit 1
fi
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_start # Сохраняем отметку о времени начала
echo $TS0 > /dev/shm/backup_time_circle # Сохраняем отметку о времени круга
echo "Результат работы записываю в файл $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_circle; 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 & # Запустить резервирование в фоне
fi
# Конец блока условия progress_only
fi
# Вывод прогресса
if [[ $progress == true ]]; then
echo ""
pids=$(ps -t $(tty | sed 's|/dev/||') -o pid,args | grep -e zfs_send.zsh -e "zfs send" ) # Список 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" )
fi
fi
# Выводить прогресс запрашивая список запущенных процессов
if [[ $pids = "" ]]; then
echo "Нет запущенного процесса резервирования"
else
echo ""
last_percent=0
time_changed_percent_value=$(date +%s)
while [[ $i_task != $n_tasks ]]; do # выводим статус пока не завершены все задачи
progress=$(ps -u | grep "sending" | 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_circle)
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
fi
if [[ -f /dev/shm/backup_time_start ]]; then
TS0=$(< /dev/shm/backup_time_start)
echo_log "\n--- Все завершено за $(date -d@$(( $(date +%s) - $TS0 )) -u '+%H:%M.%S') ---\n"
else
echo_log "\n--- Все задания завершены ---\n"
fi
# Удалить временные файлы состояний, если они существуют
[ -f /dev/shm/backup_i_task ] && rm /dev/shm/backup_i_task
[ -f /dev/shm//dev/shm/backup_time_start ] && rm /dev/shm/backup_time_start
[ -f /dev/shm//dev/shm/backup_time_circle ] && rm /dev/shm/backup_time_circle
fi