Бекап мережевий кулі (samba) у Linux за мотивами Windows Server Backup

Робимо зручний доступ до архівів (і створюємо ці архіви) мережевих куля, для клієнтів працюють під Windows.


Введення
Чим хороша служба Windows Server Backup і тіньові копії? Вони входять в поставку Windows Server і не вимагають доплати (якщо не використовувати хмарну архівацію), а також добре справляються з покладеними на них завданнями. Для простих сценаріїв використання — дуже гідне рішення. А доступ до тіньових копій через діалог властивостей файлу — взагалі дуже зручний. Тепер спробуємо зробити аналогічно для файлового сервера Linux з Samba.

Доступ до попередніх версій файлів
Цю можливість нам дає модуль Samba shadow_copy2.
Його треба прописувати в секції мережевого ресурсу у файлі smb.conf
[share]
vfs objects = shadow_copy2
shadow:snapdir = /mnt/.share
path = /mnt/share

На відміну від модуля першої версії, це дозволяє розмістити папки з копіями в різних місцях і з різними іменами.
Тепер, якщо всередині папки path = /mnt/.share ми створимо папку @GMT-2016.12.25-10.17.52
то у нас нічого не вийде. Додамо такі налаштування в секції [general]
wide links = yes # дозволяємо samba проходити за символьними посиланнями
unix extensions = no # заборонимо *nix клієнтам створення символьних посилань (і ще купу можливостей
# які вам можуть знадобитися)
allow insecure wide links = no # цю опцію включаємо в yes тільки при включених unix extensions
# інакше wide links залишаться недоступними, і дірки в безпеці не буде

Тепер у властивостях мережного кулі, в розділі попередніх версій, ми побачимо нашу «копію». Зверніть увагу, час вказується у UTC і перераховуються в місцевий вільний по часовому поясу.

Створення архівів і snapshot
Мати механізм доступу до копій без механізму їх створення — марно. В цьому нам допоможе наступний скрипт:
thin_lv_backup.sh
#!/bin/bash
#
# LVM-ThinVolume BackUp with rsync script set
#
# © 2016 -
# Andrew Leshkevich (magicgts@gmail.com)
#
#
# This script set is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by the
# Free Software Foundation, either version 2 of the license, or, at your
# option, any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
#
# For a list of supported commands, type 'thin_lv_backup help'
#
# !!! Please forgive me for bad english !!!
#
################################################################################################

################################################################################################
#Mount the snapshot to the specified mount point, if a snapshot is not active, then activate it
# Arguments:
# ${1} - Short path to Volume (in VG/LV format)
# ${2} - Mount point
# ${3} - Optional LMV Volume attribute
# Returns:
# Return 0 if no errors
################################################################################################
mount_snapshot(){
local SRC=${1}
local MNT_TGT=${2}
[ "$#" -lt 2 ] && echo 'Error: expected <VOLUME GROUP>/<LOGICAL VOLUME SNAPSHOT> <MOUNT POINT> [<VOLUME ATTRIBUTES>]' && return 1
if [ "$#" -eq 2 ]; then 
local ATTR=$(lvs --noheadings -o lv_attr ${SRC})
else
local ATTR=${3}
fi
findmnt -nf --source /dev/${SRC} >/dev/null 2 > &1 && echo "Skip: LV ${SRC} is already mounted!" && return 0
findmnt -nf --target ${MNT_TGT} >/dev/null 2 > &1 | grep -v -q ${MNT_TGT} && echo "Skip: the directory ${MNT_TGT} is already a mount point" && return 3
if [ ! -d "${MNT_TGT}" ]; then
mkdir -p "${MNT_TGT}" || echo "Error: Creating directory ${MNT_TGT}" || return 4
echo "Info: directory ${MNT_TGT} has been created"
fi
find ${MNT_TGT} -prune -empty | grep -v -q ${MNT_TGT} && echo "Skip: ${MNT_TGT} directory is not empty" && return 5
[[ ${ATTR} =~ .*a.* ]] || lvchange -ay -K ${SRC} || echo "Error: Volume Activation ${SRC}" || return 6
mount -o ro,nouuid /dev/${SRC} ${MNT_TGT} || echo "Error: Mounting ${MNT_TGT}" || return 7
return 0
}

################################################################################################
# UnMount snaphot, deactivate volume and remove it mount point directory
# Arguments:
# ${1} - Short path to Volume (in VG/LV format)
# Returns:
# Return 0 if no errors
################################################################################################
umount_snapshot(){
local SRC=${1}
local TGT
[ "$#" -ne 1 ] && echo 'Error: expected <VOLUME GROUP>/<LOGICAL VOLUME SNAPSHOT>' && return 1
local _TGT=("$( findmnt -nf --source /dev/${SRC} | cut -d '' -f 1 )")
if [ ! -z "$_TGT" ]; then
umount -A /dev/${SRC} || echo "Error: Umounting ${SRC}" || return 2
for TGT in "${_TGT[@]}"; do
find ${TGT} -prune -empty | grep -q "${TGT}" && rm --one-file-system -df ${TGT}
[ -d "${TGT}" ] && echo "Info: Fail to remove target directory ${TGT}"
done
fi
lvchange -an -K ${SRC} || echo "Error: Volume Deactivation ${SRC}" || return 3
return 0
}

################################################################################################
# Mount all associated snapshots of the volume to its origin mount points
# All snapshots must be named on the template: <ORIGIN VOLUME NAME>-GMT-%Y%m.%d-%H.%M.%S
# Arguments:
# ${1} - Short path to Origin Volume (in VG/LV format)
# ${2} - Optional archive volume group, that used to mount all archive snapshots
# Returns:
# Return 0 if no errors
################################################################################################
mount_all_snapshot(){
local SRC=${1}
local A_VG=${2}
local ATTR_S
local SNAP
[ "$#" -lt 1 ] && echo 'Error: expected <VOLUME GROUP>/<LOGICAL VOLUME> [<ARCHIVE VOLUME GROUP>]' && exit 1
IFS=$'/' read -r -a ATTR_S <<< "${SRC}"
[ "$#" -eq 2 ] && ATTR_S[0]=${A_VG}
local SRC="$( findmnt -nf --source /dev/${SRC} | cut -d '' -f 1 )/"
local DST_BASE="$( dirname ${SRC} )/.$( basename ${SRC} )/"
while IFS=" read -r SNAP; do
IFS=$' \t' read -r -a ATTR <<< "${SNAP}"
local DST=${ATTR[0]//${ATTR_S[1]}-/}
mount_snapshot ${ATTR_S[0]}/${ATTR[0]} ${DST_BASE}@${DST} ${ATTR[1]} || echo "Error: mounting ${ATTR_S[0]}/${ATTR[0]}"
done < <( lvs --noheadings -o lv_name,lv_attr -S Origin=${ATTR_S[1]} ${ATTR_S[0]} )
}

################################################################################################
# UnMount and Remove snapshot
# Arguments:
# ${1} - Short path to Volume Snapshot (in VG/LV format)
# Returns:
# Return 0 if no errors
################################################################################################
remove_snaphot(){
local TGT=${1}
local ATTR_S
[ "$#" -ne 1 ] && echo 'Error: expected <VOLUME GROUP>/<LOGICAL VOLUME SNAPSHOT>' && return 1
IFS=$'/' read -r -a ATTR_S <<< "${TGT}"
[ -z $(lvs --noheadings -o Origin -S lv_name=${ATTR_S[1]} ${ATTR_S[0]}) ] && echo "Error: not a snapshot ${TGT}" && return 2
umount_snapshot ${TGT} || echo "Error: umounting snapshot ${TGT}" || return 3
lvremove -f /dev/${TGT} || echo "Error: removing snapshot ${TGT}" || return 4
return 0
}

################################################################################################
# Create and Mount it to folder hidden on top level with same name as Original mount point
# Arguments:
# ${1} - Short path to Origin Volume (in VG/LV format)
# ${2} - Optional postfix, that replace default postfix GMT-%Y%m.%d-%H.%M.%S
# Returns:
# Return 0 if no errors
################################################################################################
create_snaphot(){
local TGT=${1}
local ATTR_S
[ "$#" -lt 1 ] && echo 'Error: expected <VOLUME GROUP>/<LOGICAL VOLUME> [<POSTFIX>]' && exit 1
local DATE=$(date -u +GMT-%Y%m.%d-%H.%M.%S)
[ "$#" -eq 2 ] && DATE="${2}"
IFS=$'/' read -r -a ATTR_S <<< "${TGT}"
lvcreate -n ${ATTR_S[1]}-${DATE} -s /dev/${TGT} || echo "Error: Creating snapshot of ${TGT}" || return 2
local SRC="$( findmnt -nf --source /dev/${TGT} | cut -d '' -f 1 )/"
local DST_BASE="$( dirname $SRC )/.$( basename $SRC )/"
mount_snapshot ${TGT}-${DATE} до ${DST_BASE}@${DATE} || echo "Error: Mounting snapshot ${TGT}-${DATE}" || return 3
}

################################################################################################
# Remove old snaphots and keep last N snapshot
# Arguments:
# ${1} - Short path to Origin Volume (in VG/LV format)
# ${2} - Number of keeping snapshot
# Returns:
# Return 0 if no errors
################################################################################################
remove_old_snapshot_copy(){
local TGT=${1}
local NUM=${2}
local SNAP
local ATTR_S
local ATTR
[ "$#" -ne 2 ] && echo 'Error: expected <VOLUME GROUP>/<LOGICAL VOLUME> <NUMBER OF KEEP>' && return 1
IFS=$'/' read -r -a ATTR_S <<< "${TGT}"
while IFS=" read -r SNAP; do
IFS=$' \t' read -r -a ATTR <<< "${SNAP}"
local DST=${ATTR[0]//${ATTR_S[1]}-/}
remove_snaphot ${ATTR_S[0]}/${ATTR[0]} || echo "Error: removing snapshot ${ATTR_S[0]}/${ATTR[0]}"
done < <( (lvs --noheadings -O -lv_name -o lv_name -S Origin=${ATTR_S[1]} ${ATTR_S[0]}) | head -n -${NUM} )
return 0
}

################################################################################################
# Prepare archive operation
# Arguments:
# ${1} - Short path to Origin Volume (in VG/LV format)
# ${2} - Mount point for ${1}
# Returns:
# Return 0 if no errors
################################################################################################
pre_archive(){
[ "$#" -ne 2 ] && echo 'Error: expected <VOLUME GROUP>/<LOGICAL VOLUME> <MOUNT POINT>' && return 1
local VOL_SRC=${1}
local MNT_TGT=${2}
mkdir -p ${MNT_TGT}
mount /dev/${VOL_SRC} ${MNT_TGT} || echo "Error: Mounting ${MNT_TGT}" || return 7
}

################################################################################################
# Post archive operation: unmount target volume, remove its mount point, create its snaphot
# Arguments:
# ${1} - Short path to Origin Volume (in VG/LV format)
# ${2} - Mount point for ${1}
# Returns:
# Return 0 if no errors
################################################################################################
post_archive(){
[ "$#" -ne 3 ] && echo 'Error: expected <VOLUME GROUP>/<LOGICAL VOLUME> <MOUNT POINT> <VOLUME GROUP>/<LOGICAL VOLUME>' && return 1
local VOL_SRC=${1}
local MNT_TGT=${2}
local TGT=${3}
umount /dev/${VOL_SRC} && rm -rd ${MNT_TGT} && lvcreate -n ${TGT} -s /dev/${VOL_SRC} && return 0
return 1
}
################################################################################################
# Create archive rsync
# Arguments:
# ${1} - Short path to Origin Volume (in VG/LV format)
# ${2} - Name of archive Volume Group
# ${3} - Optional connection string in <user name>@<host name> format
# ${4} - Optional path to this script on remote machine
# ${5} - Optional name prefix for volume name on remote machine (<PREFIX><VOLUME NAME>-GMT-%Y%m.%d-%H.%M.%S)
# ${6} - Optional also make local archive
# Returns:
# Return 0 if no errors
################################################################################################
create_archive(){
local SRC=${1}
local TGT=${2}
local CONN=${3}
local CALL=${4}
local PREFIX=${5}
local ATTR_S
local ATTR_D
local RESULTS
local RET
[ "$#" -lt 2 ] && echo 'Error: expected <ORIG VOLUME GROUP>/<ORIG LOGICAL VOLUME> <DST VOLUME GROUP> [<SSH CONNECT> <SCRIPT CALL> <PREFIX> [<LOCAL COPY?>]]' && return 1
IFS=$'/' read -r -a ATTR_S <<< "${SRC}"
IFS=$'/' read -r -a ATTR_D <<< "${TGT}"
local SRC="$( findmnt -nf --source /dev/${ATTR_S[0]}/${ATTR_S[1]} | cut -d '' -f 1 )/"
local DST_BASE="$( dirname $SRC )/.$( basename $SRC )/"
create_snaphot ${ATTR_S[0]}/${ATTR_S[1]} archive_orig
local DATE=$(date -u +GMT-%Y%m.%d-%H.%M.%S) 
if [ "$#" -ge 5 ]; then
RESULTS=$(ssh ${CONN} "${CALL} pre_archive ${TGT}/${PREFIX}-${ATTR_S[1]} ${DST_BASE}@archive_dst")
RET=$?
echo "$RESULTS"
[ "${RET}" -ne 0 ] && return ${RET}
rsync -aAXx --delete ${DST_BASE}@archive_orig/ ${CONN}:${DST_BASE}@archive_dst &&\ 
RESULTS=$(ssh ${CONN} "${CALL} post_archive ${TGT}/${PREFIX}-${ATTR_S[1]} ${DST_BASE}@archive_dst ${PREFIX}-${ATTR_S[1]}-${DATE}")
RET=$?
echo "$RESULTS"
[ "${RET}" -ne 0 ] && return ${RET}
fi
if [ "$#" -eq 2 ] || [ "$#" -eq 6 ]; then
pre_archive ${TGT}/${ATTR_S[1]} ${DST_BASE}@archive_dst
rsync -aAXx --delete ${DST_BASE}@archive_orig/ ${DST_BASE}@archive_dst &&\
post_archive ${TGT}/${ATTR_S[1]} ${DST_BASE}@archive_dst ${ATTR_S[1]}-${DATE}
RET=$?
[ "${RET}" -ne 0 ] && return ${RET}
mount_snapshot ${TGT}/${ATTR_S[1]}-${DATE} до ${DST_BASE}@${DATE}
else
echo 'Error: expected <ORIG VOLUME GROUP>/<ORIG LOGICAL VOLUME> <DST VOLUME GROUP> [<SSH CONNECT> <SCRIPT CALL> <PREFIX> [<LOCAL COPY?>]]>' && return 1
fi
remove_snaphot ${ATTR_S[0]}/${ATTR_S[1]}-archive_orig
}
case ${1} in
'help')
[ -z "${2}" ] && echo -e "create - create snapshot and mount it\nmount - mount snapshot\numount unmount snapshot\nmount_all - mount all snapshot\n\
remove - remove snapshot\nremove_old - keep last n snapshot\ncreate_archive - create archive"
case ${2} in
'create')
echo 'thin_lv_backup.sh create <VOLUME GROUP>/<LOGICAL VOLUME> [<POSTFIX>]'
;;
'mount')
echo 'thin_lv_backup.sh mount <VOLUME GROUP>/<LOGICAL VOLUME SNAPSHOT> <MOUNT POINT> [<VOLUME ATTRIBUTES>]'
;;
'umount')
echo 'thin_lv_backup.sh umount <VOLUME GROUP>/<LOGICAL VOLUME SNAPSHOT>'
;;
'mount_all')
echo 'thin_lv_backup.sh mount_all <VOLUME GROUP>/<LOGICAL VOLUME> [<ARCHIVE VOLUME GROUP>]'
;;
'remove')
echo 'thin_lv_backup.sh remove <VOLUME GROUP>/<LOGICAL VOLUME SNAPSHOT>'
;;
'remove_old')
echo 'thin_lv_backup.sh remove_old <VOLUME GROUP>/<LOGICAL VOLUME> <NUMBER OF KEEP>'
;;
'create_archive')
echo 'thin_lv_backup.sh create_archive <ORIG VOLUME GROUP>/<ORIG LOGICAL VOLUME> <DST VOLUME GROUP> [<SSH CONNECT> <SCRIPT CALL> <PREFIX> [<LOCAL COPY?>]]'
;;
esac
;;
'create')
create_snaphot $2 $3
;;
'mount')
mount_snapshot $2 $3 $4
;;
'umount')
umount_snapshot $2
;;
'mount_all')
mount_all_snapshot $2 $3
;;
'remove')
remove_snaphot $2
;;
'remove_old')
remove_old_snapshot_copy $2 $3
;;
'create_archive')
create_archive $2 $3 $4 $5 $6 $7
;;
'pre_archive')
pre_archive $2 $3
;;
'post_archive')
post_archive $2 $3 $4
;;
esac


disclaimerСкрипт не ідеальний (поки я набиваю руку) і спирається на вашу сумлінність (немає перевірки на коректність аргументів, тільки на кількість). Однак, я постарався зробити його максимально безпечним для даних (він не видаляє тома, тільки snapshot, видаляє тільки порожні каталоги). Ваші пропозиції, щодо поліпшення і виправлення, тільки вітаються.

Для роботи необхідно використовувати LVM ThinVolumes. Порівняно із звичайними томами, їх продуктивність слабо залежить від числа snapshot (COW знижує продуктивність в 2-3 рази, поки ви модифікуєте «свіжі» блоки, а якщо знімків 2 і більше, то робота просто завмирає).

Принцип створення архівних копій:

  1. Створити snapshot вихідного тома і змонтувати його
  2. Змонтувати тому призначення
  3. Провести копіювання за допомогою rsync
  4. Відмонтувати тому призначення і зробити його snaphot
  5. Відмонтувати snapshot джерела і видалити його
  6. При локальному архівування, подмонтировать останній snapshot тома з архівом
Таким чином ми одержуємо нормальний інкрементний бекап. При бажанні, можна адаптувати для btrfs або ZFS.

Використання
Створити snapshot і примонтувати його в прихований каталог з ім'ям кулі (папки містить кулі):
/usr/local/bin/thin_lv_backup.sh create vg_system/share

Створити архів з допомогою rsync на іншу групу томів:
/usr/local/bin/thin_lv_backup.sh create_archive vg_system/share vg_archive

Створити віддалений архів з допомогою rsync:
/usr/local/bin/thin_lv_backup.sh create_archive vg_system/share vg_archive archive@archive.localdomain 'sudo /usr/local/bin/thin_lv_backup.sh' srv1

Створити віддалений і локальні архів з допомогою rsync:
/usr/local/bin/thin_lv_backup.sh create_archive vg_system/share vg_archive archive@archive.localdomain 'sudo /usr/local/bin/thin_lv_backup.sh' srv1 true

Видалити старі копії (залишити N останніх)
/usr/local/bin/thin_lv_backup.sh remove_old vg_system/share 5

Вбудована довідка:
/usr/local/bin/thin_lv_backup.sh help

/usr/local/bin/thin_lv_backup.sh help mount_all

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.