Compare commits
12 Commits
3120cfc2d0
...
6af17fcb8e
| Author | SHA1 | Date | |
|---|---|---|---|
| 6af17fcb8e | |||
|
|
909b5bbb49 | ||
|
|
1738c35abb | ||
|
|
f55223926f | ||
|
|
874662feb4 | ||
| 5b1b327e22 | |||
| 4ec0277802 | |||
| 2541094bdf | |||
| 7658041e3d | |||
| 7706403ff5 | |||
| 6d0056657a | |||
| 8a01f1ef8c |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
log*
|
||||||
55
README.md
Normal file
55
README.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Скрипт для резервирования данных
|
||||||
|
Скрипт используется для резервирования датасетов файловой системы zfs.
|
||||||
|
Необходим запуск от суперпользователя.
|
||||||
|
|
||||||
|
## Флаги
|
||||||
|
Используемая оболочка: zsh
|
||||||
|
`./zfs_send.zsh`
|
||||||
|
- `-h | --help` Кратская справка по ключам
|
||||||
|
- `-rs | --remote-server <>` IP адрес или доменное имя сервера для резервирования (по умолчанию 192.168.0.162)
|
||||||
|
- `-ru | --remote-user <>` Пользователь SSH на удалённом сервер (по умолчанию root)
|
||||||
|
- `-s | --snapshot <>` Задать имя снимка для резервирования (по умолчанию manual-YYYYMMDD, где YMD - текущая дата, месяц и день)
|
||||||
|
- `-ld | --local-dataset <>` Резервируемый датасет (по умолчанию интерактивный режим)
|
||||||
|
- `-rd | --remote-dataset <>` Нахождение датасета на удалённой машине (по умолчанию интерактивный режим)
|
||||||
|
- `-i | --incremental` Режим резервирования (по умолчанию полная копия)
|
||||||
|
Данные по умолчанию можно отредактировать в самом скрипте в рамом начале.
|
||||||
|
- ` --no-progress` Не выводить прогресс
|
||||||
|
- ` --progress-only` Запустить только вывод прогресса, если уже запущено резервирование в флне
|
||||||
|
- ` --no-check` Не запрашивать подтверждения. Если нужно запускать скрипт по расписанию
|
||||||
|
- ` --stop` Остановить текщие фоновые процессы резервирования, запучщенные скриптом
|
||||||
|
|
||||||
|
### Настройка доступа по ключу SSH
|
||||||
|
|
||||||
|
Ожидается, что доступ по SSH на удалённую машину уже настроен. Доступ нужен с использованием ssh-ключа.
|
||||||
|
Если доступ по паролю, то скприт попытается создать ключ и закинуть его на сервер (по паролю)
|
||||||
|
|
||||||
|
Если после удачной процедуры всё же не получается подсоединиться, то нужно вручную добавлять выведеный на экран публичный ключ на бэкап-сервер,
|
||||||
|
возможно через веб-гуй.
|
||||||
|
|
||||||
|
### Режим резервирования датасета целиком
|
||||||
|
Когда не использован флаг `-i`
|
||||||
|
Если не указаны `--local-dataset` и `--remote-dataset`, то скрипт запросит список датасетов на локальной машине и на удалённом сервере. Будет предлоден выбор какой датасет на какой резервировать.
|
||||||
|
Примеры:
|
||||||
|
- `./zfs_send.zsh` Полная резервная копия дата сета на сервер по умолчанию. Указать датасаты для резервирования можно в интерактивном режиме.
|
||||||
|
- `./zfs_send.zsh -rs backup.local -ld /main_pool/Documents -rd /backup` В данном случае на удалённой машине будет создан датасет `/backup/Documents`. Если такой датасет уже существует, то будет выдана ошибка и резервирование остановится.
|
||||||
|
|
||||||
|
#### Пример вывода в процессе работы
|
||||||
|
|
||||||
|
### Режим инкрементный
|
||||||
|
После полного резервирования возможно сделать резерв только изменённой части файловой системы указав флаг `-i`
|
||||||
|
Если не указаны `--local-dataset` и `--remote-dataset`, то скрипт сопоставит имеющиеся датасеты на локальной и удалённых машинах и предложит возможные варианты резервирования. Сопоставление производится по совпадения названий датасетов нижнего уровня.
|
||||||
|
- `./zfs_send.zsh -rs 192.168.0.162 -i` Инкрементная резервная копия дата сета. Указать датасаты для резервирования в интерактивном режиме.
|
||||||
|
- `./zfs_send.zsh -rs 192.168.0.162 -i -ld /main_pool/Documents -rd /backup/Documents` В данном случае на удалённой машине должен существовать датасет `/backup/Documents` и у него должны сохранятся прошлые снимки файловой системы для определения новых файлов для резервирования. Если такой датасет отсутвует, то будет выдана ошибка и резервирование остановится.
|
||||||
|
|
||||||
|
#### Пример вывода в процессе работы
|
||||||
|
|
||||||
|
## Примеры запуска
|
||||||
|
- `./zfs_send.zsh` Полная резервная копия выбранного в интерактивном режиме дата сета на сервер по умолчанию. Будет создан снимок файловой системы с названием `manual_20241104`, где цифры - текщая дата.
|
||||||
|
- `./zfs_send.zsh -rs 192.168.0.21 --snapshot man_2405` Полная резервная копия дата сета на сервер `192.168.0.21`. Будет создан снимок файловой системы с названием `man_2405`. Датасет будет
|
||||||
|
- `./zfs_send.zsh -i` Инерментная копия на сервер по умолчанию. Датасеты будут сопоставлены автоматически. Будет предложен выбор резервируемых датасетов.
|
||||||
|
- `./zfs_send.zsh --stop` Прервать запущенные ранее операции резервирования
|
||||||
|
|
||||||
|
## После запуска
|
||||||
|
Во время резервирования данных нельзя закрывать сессию пользователя. Иначе резервирование прервётся.
|
||||||
|
|
||||||
|
Само резервирование происходит в фоновом процессе. Если необходимо прервать процесс, то воспользуйтесь коммандой `./zfs_send.zsh --stop`
|
||||||
280
backup_data.zsh
280
backup_data.zsh
@ -1,280 +0,0 @@
|
|||||||
#!/bin/zsh
|
|
||||||
|
|
||||||
backup_server="192.168.0.162" # Сервер для резервирования
|
|
||||||
backup_user="root" # Пользователь на сервере
|
|
||||||
inc_snapshot="manual-$(date +%Y%m%d)" # Новый создаваемый снисмок
|
|
||||||
incremental=false
|
|
||||||
|
|
||||||
echo -ne "Обработка аргументов...\033[2K\r"
|
|
||||||
|
|
||||||
while [[ "$#" -gt 0 ]]; do
|
|
||||||
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
|
|
||||||
echo "Снимок файловой системы $inc_snapshot"
|
|
||||||
|
|
||||||
case $incremental in
|
|
||||||
true) echo "Режим инкрементной копии";;
|
|
||||||
false) echo "Режим полной копии";;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# --------- Собираем данные ----------
|
|
||||||
|
|
||||||
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 # Словарь ключ = резервируемый локальный датасет, значение = имя датасета на удалённом сервере
|
|
||||||
|
|
||||||
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 # Если не инкрементная копия, то запросить меcто для резервирования
|
|
||||||
# Вывести на экран запрос
|
|
||||||
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 # Конец блока запроса места резервирования
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Словарь со списком последких снапшотов для датасетов
|
|
||||||
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
|
|
||||||
|
|
||||||
# --------- Выбор датасетов ----------
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# --------- Резервирование ----------
|
|
||||||
|
|
||||||
if [[ $work = "y" || $work = "Y" ]]; then
|
|
||||||
TS0=$(date +%s)
|
|
||||||
echo $TS0 > /dev/shm/backup_time_stamp # Сохраняем отметку о времени начала
|
|
||||||
LOGFILE="log_bak_${inc_snapshot}.log"
|
|
||||||
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 # Выполненных зачач
|
|
||||||
echo $i_task > /dev/shm/backup_i_task # Сохраняем количество готовых задач в память
|
|
||||||
for loc_ds bak_ds in ${(kv)work_datasets}; do
|
|
||||||
TS1=$(date +%s)
|
|
||||||
echo $TS1 > /dev/shm/backup_time_stamp # Сохраняем отметку о времени начала
|
|
||||||
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++ ))
|
|
||||||
echo $i_task > /dev/shm/backup_i_task
|
|
||||||
echo_log "--- готово за: $(date -d@$(($TS2-$TS1)) -u '+%H:%M.%S') ---"
|
|
||||||
done & # Запустить резервирование в фоне
|
|
||||||
|
|
||||||
# Выводить прогресс запрашивая список запущенныйх процессов
|
|
||||||
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
|
|
||||||
|
|
||||||
echo_log "\n--- Все завершено за $(date -d@$(( $(date +%s) - $TS0 )) -u '+%H:%M.%S') ---\n"
|
|
||||||
|
|
||||||
fi
|
|
||||||
584
zfs_send.zsh
Executable file
584
zfs_send.zsh
Executable file
@ -0,0 +1,584 @@
|
|||||||
|
#!/bin/zsh
|
||||||
|
|
||||||
|
#TODO list
|
||||||
|
# 1. Скрипт не определяет, что нужно остановиться
|
||||||
|
# 2. Нужно автоматически определить, если копирование уже запущено то предложить остановить и вывести процесс резервирования
|
||||||
|
# Нет проверки на верность введённых параметров
|
||||||
|
# смотреть нужно сразу на всей системе. Не будем запускать несколько параллельных резервирований.
|
||||||
|
|
||||||
|
|
||||||
|
backup_server="192.168.0.120" # Сервер для резервирования
|
||||||
|
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" ;
|
||||||
|
exit 0;;
|
||||||
|
*) 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
|
||||||
|
|
||||||
|
# Если ключей нет, создаем новый
|
||||||
|
if [ ! -f ~/.ssh/id_ed25519 ]; then
|
||||||
|
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519
|
||||||
|
# echo "Создаём новый ключ..."
|
||||||
|
ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519 >&2
|
||||||
|
echo "~/.ssh/id_ed25519"
|
||||||
|
else
|
||||||
|
echo "Ключей для этого сервера не найдено. Ключ по умолчанию ~/.ssh/id_ed25519 уже существует. Создание нового ключа прервано"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Определение ключа
|
||||||
|
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 # Если не инкрементная копия, то запросить меcто для резервирования
|
||||||
|
# Вывести на экран запрос
|
||||||
|
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"
|
||||||
|
column_widths="%-45s * %-45s\n"
|
||||||
|
for loc_ds bak_ds in ${(kv)work_datasets}; do
|
||||||
|
# Запросить последние снимки
|
||||||
|
loc_snap="$(zfs list -t snapshot -o name ${loc_ds} 2>/dev/null | 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} 2>/dev/null | 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
|
||||||
|
# Если последние снимки на обеих машинах совпадают в инкрементном режиме
|
||||||
|
printf $column_widths "${loc_ds}@${loc_snap}" "существует на обеих машинах. Исключено из списка резервируемых"
|
||||||
|
unset "work_datasets[$loc_ds]"
|
||||||
|
else
|
||||||
|
# Если на удалённой машине нет такого снимка, то предложить удалить.
|
||||||
|
printf $column_widths "${loc_ds}@${loc_snap}" "существует на исходной машине, но отсутвует на $backup_server"
|
||||||
|
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
|
||||||
|
printf $column_widths "${loc_ds}" "Не имеет снимков. Не возможно сделать инкрементный резерв."
|
||||||
|
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 $n_tasks > /dev/shm/backup_n_tasks
|
||||||
|
|
||||||
|
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" | grep -v -- 'grep' | grep -v -- 'progress-only' ) # Список PIDs с командой запуска
|
||||||
|
|
||||||
|
if [[ $pids = "" ]]; then
|
||||||
|
echo "Нет запущенного процесса резервирования в этом терминале."
|
||||||
|
read "srch?Икать во всех процессах системы? (y/N) "
|
||||||
|
if [[ $srch = y ]]; then
|
||||||
|
pids=$(ps a -o pid,args | grep -e zfs_send.zsh -e "zfs send" | grep -v -- 'grep' | grep -v -- 'progress-only' )
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo $pids
|
||||||
|
# Выводить прогресс запрашивая список запущенных процессов
|
||||||
|
if [[ $pids = "" ]]; then
|
||||||
|
echo "Нет запущенного процесса резервирования"
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
# инициализируем переменнные
|
||||||
|
TS0=$(< /dev/shm/backup_time_start)
|
||||||
|
TS1=$(< /dev/shm/backup_time_circle)
|
||||||
|
|
||||||
|
last_percent=0
|
||||||
|
last_volume=0
|
||||||
|
last_volume_time=$TS1
|
||||||
|
speed=0
|
||||||
|
reset_interval=10 # интревал сбрасывания скорости до нуля, в случае если нет именений
|
||||||
|
last_update_time=$TS1 # Время последнего обновления
|
||||||
|
|
||||||
|
time_changed_percent_value=$TS1
|
||||||
|
|
||||||
|
n_tasks=$(< /dev/shm/backup_n_tasks)
|
||||||
|
while [[ $i_task -lt $n_tasks ]]; do # выводим статус пока не завершены все задачи
|
||||||
|
i_task=$(< /dev/shm/backup_i_task)
|
||||||
|
progress=$(ps -u | grep "sending" | grep -v "grep" | sed -r "s/(.*) zfs: (.*)/\2/")
|
||||||
|
|
||||||
|
# Извлекаем объем и процент
|
||||||
|
progress_part=$(echo "$progress" | grep -oP '\(\K.*(?=\))') # Извлекаем только часть в скобках
|
||||||
|
|
||||||
|
if [[ -n $progress_part ]]; then
|
||||||
|
# Разбиваем часть на компоненты
|
||||||
|
# Процент определим по объёму
|
||||||
|
# percent=$(echo "$progress_part" | awk -F': |/' '{print $1}' | tr -d '%')
|
||||||
|
current_volume_str=$(echo "$progress_part" | awk -F': |/' '{print $2}')
|
||||||
|
total_volume_str=$(echo "$progress_part" | awk -F': |/' '{print $3}')
|
||||||
|
|
||||||
|
# Определяем единицу для интервала сброса
|
||||||
|
unit=${current_volume_str: -1}
|
||||||
|
case $unit in
|
||||||
|
G) reset_interval=100;;
|
||||||
|
M) reset_interval=10;;
|
||||||
|
*) reset_interval=5;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Преобразуем в байты
|
||||||
|
current=$(echo "$current_volume_str" | awk '{sub(/[^0-9.]/,""); print $1}')
|
||||||
|
case $unit in
|
||||||
|
G) current_volume=$(( current * 1073741824 )) ;;
|
||||||
|
M) current_volume=$(( current * 1048576 )) ;;
|
||||||
|
K) current_volume=$(( current * 1024 )) ;;
|
||||||
|
*) current_volume=0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
total=$(echo "$total_volume_str" | awk '{sub(/[^0-9.]/,""); print $1}')
|
||||||
|
case $unit in
|
||||||
|
G) total_volume=$(( total * 1073741824 )) ;;
|
||||||
|
M) total_volume=$(( total * 1048576 )) ;;
|
||||||
|
K) total_volume=$(( total * 1024 )) ;;
|
||||||
|
*) total_volume=0 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
percent=$(( current_volume * 100 / total_volume ))
|
||||||
|
else
|
||||||
|
percent=0
|
||||||
|
current_volume=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Вычисляем скорость
|
||||||
|
now=$(date +%s)
|
||||||
|
|
||||||
|
# Если объем изменился
|
||||||
|
elapsed_from_vol_change=$((now - last_volume_time))
|
||||||
|
if [[ $current_volume -ne $last_volume && elapsed_from_vol_change -ne 0 ]]; then
|
||||||
|
speed=$(awk "BEGIN { printf \"%.1f\", ($current_volume - $last_volume) / $elapsed_from_vol_change }")
|
||||||
|
last_volume=$current_volume
|
||||||
|
last_volume_time=$now
|
||||||
|
last_update_time=$now # Обновляем время последнего изменения
|
||||||
|
else
|
||||||
|
# Если объем не изменился, проверяем время с последнего обновления
|
||||||
|
time_since_update=$((now - last_update_time))
|
||||||
|
if (( time_since_update >= reset_interval )); then
|
||||||
|
speed=0 # Обнуляем скорость, если нет изменений в течение интервала
|
||||||
|
last_update_time=$now # Обновляем время
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Конвертируем скорость в человекочитаемый формат
|
||||||
|
if (( speed > 1073741824 )); then
|
||||||
|
speed_human=$(awk "BEGIN { printf \"%.1fG\", $speed / 1073741824 }")
|
||||||
|
elif (( speed > 1048576 )); then
|
||||||
|
speed_human=$(awk "BEGIN { printf \"%.1fM\", $speed / 1048576 }")
|
||||||
|
elif (( speed > 1024 )); then
|
||||||
|
speed_human=$(awk "BEGIN { printf \"%.1fK\", $speed / 1024 }")
|
||||||
|
else
|
||||||
|
speed_human="${speed}B"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Начинаем вывод
|
||||||
|
echo -n "\033[2K\r$progress $speed_human/s "
|
||||||
|
|
||||||
|
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_total=$(( $(date +%s) - TS1 ))
|
||||||
|
fi
|
||||||
|
time_part_of_percent=$(( now - time_changed_percent_value )) # Время с последнего изменения процента
|
||||||
|
# Используем awk для более точных расчётов
|
||||||
|
# estimated_total - Всего времени на задачу
|
||||||
|
# estimated_remain - Осталось времени на задачу
|
||||||
|
read estimated_total estimated_remain <<< $(awk -v p="$percent" -v t="$elapsed_total" -v tp="$time_part_of_percent" '
|
||||||
|
BEGIN {
|
||||||
|
total = (p > 0 ? (100 / p * t) : 0);
|
||||||
|
remain = (total > 0 ? (total - t - tp) : 0);
|
||||||
|
printf("%d %d", total, remain)
|
||||||
|
}')
|
||||||
|
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 " $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 " $(date -d@$estimated_remain -u '+%H:%M.%S') ост. / $(date -d@$estimated_total -u '+%H:%M.%S') "
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
for ((i = 1; i < 4 ; i++)); do echo -n "."; sleep 1; done
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
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
|
||||||
|
# Обнуляем переменные после завершения задачи, чтобы избежать ошибок при повторном запуске
|
||||||
|
unset last_volume last_volume_time last_update_time last_percent time_changed_percent_value
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
Loading…
x
Reference in New Issue
Block a user