diff --git a/backup_data.zsh b/backup_data.zsh index 2601919..ffb8f31 100755 --- a/backup_data.zsh +++ b/backup_data.zsh @@ -1,99 +1,177 @@ #!/bin/zsh backup_server="192.168.0.162" # Сервер для резервирования +backup_user="root" # Пользователь на сервере inc_snapshot="manual-$(date +%Y%m%d)" # Новый создаваемый снисмок +incremental=false -echo -ne "Обработка аргументов...\033[0K\r" +echo -ne "Обработка аргументов...\033[2K\r" -if [[ $# == 1 ]]; then - backup_server=$1 +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 -if [[ $# == 2 ]]; then - backup_server=$1 - inc_snapshot=$2 -fi -if [[ $# > 2 ]]; then - echo "Ожидается 0 или 2 аргумента: backup_server или ни одного для сервера по умолчанию: $backup_server" - exit -fi -echo "Резервирование на $backup_server" echo "Снимок файловой системы $inc_snapshot" + +case $incremental in + true) echo "Режим инкрементной копии";; + false) echo "Режим полной копии";; +esac + # --------- Собираем данные ---------- -echo -ne "Собираю данные...\033[0K\r" +echo -ne "Собираю данные...\033[2K\r" # Получить список датасетов на локальной машине -local_datasets=$(zfs list -o name | grep -v -e ix-app -e boot-pool -e system-data -e jails | grep /) # Отрезаем всё лишнее, отделяем нужные данные по 4 пробелам в выводе. +local_datasets=$(zfs list -o name | grep -v -e ix-app -e boot-pool -e system-data -e jails -e NAME) local_datasets=(${=local_datasets}) # Конвертирование в массив # Получить список датасетов на удалённой машине -backup_datasets=$(ssh root@$backup_server zfs list -o name | grep -v -e ix-app -e boot-pool -e system-data -e jails | grep /) # Отрезаем всё лишнее, отделяем нужные данные по 4 пробелам в выводе. +backup_datasets=$(ssh $backup_user@$backup_server zfs list -o name | grep -v -e ix-app -e boot-pool -e system-data -e jails -e NAME) backup_datasets=(${=backup_datasets}) # Конвертирование в массив -# Высчитать что на что резервировать -declare -A work_datasets -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]} +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 -done + + echo "Введите номер датасета в котором создать кописю исходного (- исключить, 0 выйти)" + for i in $loc_ds_indx_list; do + read "bak_ds_indx?$i - ${local_datasets[i]} -> " + case $bak_ds_indx in + 0) exit;; + -) ;; + *) if (( $bak_ds_indx >= 1 )) && (( $bak_ds_indx <= ${#backup_datasets} )); then + work_datasets[$local_datasets[$i]]=$backup_datasets[bak_ds_indx] + else; exit 1; fi ;; + esac + done +fi # Конец блока запроса места резервирования + + # Словарь со списком последких снапшотов для датасетов declare -A last_loc_snaps -declare -A last_bak_snaps -echo -ne "Проверяю на наличие $inc_snapshot...\033[0K\r" +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="$(zfs list -t snapshot -o name ${loc_ds} | grep 'manual' | tail -n1 | egrep -o '@.+' )" loc_snap=${loc_snap:1} # удалить @ в начале строки - bak_snap="$(ssh root@$backup_server zfs list -t snapshot -o name ${bak_ds} | grep "manual" | tail -n1 | egrep -o '@.+' )" - bak_snap=${bak_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 [[ "$loc_snap" = "$bak_snap" ]]; then + if [[ $incremental = true ]] && [[ "$loc_snap" = "$bak_snap" ]]; then + # Если последние снимки на обеих машинах совпадают в инкрементном режиме echo "${loc_ds}@${loc_snap} * существует на обеих машинах. Исключено из списка резервируемых" unset "work_datasets[$loc_ds]" else # Если на удалённой машине нет такого снимка, то предложить удалить. - read "del_snapshot?Датасет $loc_ds уже имеет последний снимок $inc_snapshot. Удалить? (y/N)" - if [ "$del_snapshot" = "y" ]; then - 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 - # last_bak_snaps[$bak_ds]=$bak_snap # не нужно, нигде не используется - else - # Если не удалять, то просто исключить из резервирования - echo "$ds Исключено из списка" - unset "work_datasets[$ds]" - fi + 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 # добавить в список снапшотов - last_loc_snaps[$loc_ds]=$loc_snap - # last_bak_snaps[$bak_ds]=$bak_snap # не нужно, нигде не используется + 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 "" echo "Список резервируемых датасетов:" -i=0; for loc_ds bak_ds in ${(kv)work_datasets}; do - i=$((i+1)) - echo "$i - $loc_ds \t ${last_loc_snaps[$loc_ds]} \t -> \t${bak_ds}" -done -echo -echo "Инкрементный снимок:\t$inc_snapshot" +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), или перечислить через пробел номера, НЕТ по умолчанию? " +read 'work?Утвердите данные. "y" в работу | "1 2.." выбрать через пробел | НЕТ по умолчанию : ' if [[ $work = *[[:digit:]]* ]]; then indx_list=(${=work}) # Если нет индекса в списке, то удалить из словаря для резервирования @@ -118,7 +196,8 @@ fi if [[ $work = "y" || $work = "Y" ]]; then TS0=$(date +%s) - LOGFILE="backup_${inc_snapshot}.log" + echo $TS0 > /dev/shm/backup_time_stamp # Сохраняем отметку о времени начала + LOGFILE="log_bak_${inc_snapshot}.log" echo "Результат работы записываю в файл $LOGFILE" # Записать в файл список резервируемых датасетов @@ -126,7 +205,7 @@ if [[ $work = "y" || $work = "Y" ]]; then echo "Список резервируемых датасетов:" >> "${LOGFILE}" n_tasks=0 for loc_ds in ${(k)work_datasets}; do - n_tasks=$((n_tasks+1)) + (( n_tasks++ )) echo "$key \t ${last_loc_snaps[$loc_ds]} \t -> \t${work_datasets[$loc_ds]}" >> "${LOGFILE}" done @@ -134,30 +213,65 @@ if [[ $work = "y" || $work = "Y" ]]; then exec 6>&1 # Saves stdout exec >&1 >>$LOGFILE 2>&1 # Вывод на экран и в файл - echo "\n--- Резервирую данные ---" + echo "--- Резервирую данные ---" 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 " * snapshot ${loc_ds}@${inc_snapshot}" - zfs snapshot ${loc_ds}@${inc_snapshot}; + zfs snapshot ${loc_ds}@${inc_snapshot}; #FIXME выдаёт ошибку если мы оставили существующий снимок echo " * Start sending ${loc_ds} at $(date +'%Y.%m.%d %H:%M.%S')" - zfs send -V -i ${loc_ds}@${last_loc_snaps[$loc_ds]} ${loc_ds}@${inc_snapshot} | ssh "root@${backup_server}" zfs receive ${bak_ds}@${inc_snapshot} + case $incremental in + true) zfs send -V -i ${loc_ds}@${last_loc_snaps[$loc_ds]} ${loc_ds}@${inc_snapshot} | ssh "$backup_user@${backup_server}" zfs receive ${bak_ds}@${inc_snapshot} ;; + false) loc_name_ds=(${(@s:/:)loc_ds}); loc_name_ds=$loc_name_ds[-1] # Получить имя датасета без пути + bak_ds="$bak_ds/$loc_name_ds" + zfs send -V ${loc_ds}@${inc_snapshot} | ssh "$backup_user@${backup_server}" zfs receive ${bak_ds} ;; + esac TS2=$(date +%s) - i_task=$((i_task+1)) - echo "------ готово за: $(date -d@$(($TS2-$TS1)) -u '+%H:%M:%S') " + (( i_task++ )) + echo $i_task > /dev/shm/backup_i_task + echo "------ готово за: $(date -d@$(($TS2-$TS1)) -u '+%H:%M.%S') ------" done & # Запустить резервирование в фоне exec 1>&6 # Restore stdout. Не писать в файл лога отследивание процента # Выводить прогресс запрашивая список запущенныйх процессов - - while [[ $i_task != $n_tasks ]]; do - PROGR=$(ps -u | grep "send" | grep -v "grep" | sed -r "s/(.*) zfs: (.*)/\2/") - echo -ne "$PROGR" - dots_per_min=10 - for ((i = 0; i < dots_per_min; i++)); do echo -n "."; sleep ((60 / dots_per_min)); done - echo -e "\r"; + 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 exec >&1 >>$LOGFILE 2>&1 # Вывод на экран и в файл echo "\n--- Все завершено за $(date -d@$(($TS2-$TS0)) -u '+%H:%M.%S') ---"