diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c802e8f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +log* diff --git a/README.md b/README.md new file mode 100644 index 0000000..b125923 --- /dev/null +++ b/README.md @@ -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` diff --git a/backup_data.zsh b/backup_data.zsh deleted file mode 100755 index a70a3a2..0000000 --- a/backup_data.zsh +++ /dev/null @@ -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 diff --git a/zfs_send.zsh b/zfs_send.zsh new file mode 100755 index 0000000..b4df0c9 --- /dev/null +++ b/zfs_send.zsh @@ -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 "; + echo "-гu | --remote-user "; + echo "-s | --snapshot "; + echo "-ld | --local-dataset "; + echo "-rd | --remote-dataset "; + echo "-l | --log-file "; + 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