воскресенье, 24 апреля 2016 г.

ZRescue: ZFS backup, часть I

Давным-давно, в далекой-предалекой галактике, в 2009м году, я начал писать о резервировании и восстановлении ZFS: здесь и здесь.

С тех пор утекло немало воды, сам Solaris поменял место жительства. ZFS отныне поддерживается FreeBSD. Появилось множество коммерческих, тяжелых и дорогих, решений по резервированию и восстановлению. И складывается впечатление, что это очень сложно, страшно дорого и ужасно больно.

На самом деле это не так. Дабы доказать это, вытащим для всеобщего обозрения и применения финальную версию того решения, на начало которого я сослался выше и которое началось в далеком 2009м году.

Итак, сперва утилита zfsbackup. Просто и со вкусом написанная на шелле.
 #!/sbin/sh  
   
 #  
 # ZRescue - ZFS Backup/Restore  
 #  
 # ZFS archives uses for bare-metal restore  
 # and systems cloning.  
 #  
 # Archive names will be incremental, like that:  
 # [hostname].[pool|pool%dataset].n.zfs<.gz>, n=0,1,2...  
 # Note: Do not rename archive files! Filenames will  
 #    use to recovery purposes.  
 #  
 # Version 2.0 (C) 2009,2016 Y.Voinov  
 #  
 #pragma ident "@(#)zfsbackup.sh  2.0  10/22/09 YV"  
 #  
   
 #############  
 # Variables #  
 #############  
   
 # Environment variables   
 PATH=/usr/local/bin:/sbin:/usr/bin:$PATH  
   
 # Copyright and version  
 PKG_NAME="ZRescue"  
 PKG_VERSION="2.0"  
 COPY="Copyright (C) 2009, Yuri Voinov"  
   
 # Archive file suffix  
 ARC_SUFFIX=".gz"  
 # GZip default compression level  
 COMP_LEVEL="9"  
 # Default archive extension  
 ext="zfs"  
 # Initial archive sequence  
 initial_arc_seq="0"  
   
 # OS utilities  
 BASENAME=`which basename`  
 CUT=`which cut`  
 DATE=`which date`  
 ECHO=`which echo`  
 EXPR=`which expr`  
 GETOPT=`which getopt`  
 GZIP=`which gzip`  
 HOSTNAME=`which hostname`  
 ID=`which id`  
 PRINTF=`which printf`  
 SED=`which sed`  
 SSH=`which ssh`  
 UNAME=`which uname`  
 WHOAMI=`which whoami`  
 ZFS=`which zfs`  
   
 OS_VER=`$UNAME -r|$CUT -f2 -d"."`  
 OS_NAME=`$UNAME -s|$CUT -f1 -d" "`  
 OS_FULL=`$UNAME -sr`  
   
 # System name  
 system=`$HOSTNAME`  
   
 # Snapshots extension by default   
 # Correct it if these snapshots already exists in the system  
 SNAP_EXT="$system""_""$PKG_NAME""-""$PKG_VERSION""_snapshot"  
   
 ###############  
 # Subroutines #  
 ###############  
   
 check_os ()  
 {  
  # Check OS  
  $PRINTF "Checking OS... "  
  if [ "$OS_NAME" = "SunOS" -a "$OS_VER" -lt "10" ]; then  
  $ECHO "ERROR: Unsupported OS: $OS_FULL"  
  $ECHO "Exiting..."  
  exit 1  
  else  
  $ECHO "$OS_FULL"  
  fi  
 }  
   
 check_root ()  
 {  
  # Check if user root  
  $PRINTF "Checking super-user... "  
  if [ -f /usr/xpg4/bin/id ]; then  
  WHO=`/usr/xpg4/bin/id -n -u`  
  elif [ "`$ID | $CUT -f1 -d" "`" = "uid=0(root)" ]; then  
  WHO="root"  
  else  
  WHO=$WHOAMI  
  fi  
   
  if [ ! "$WHO" = "root" ]; then  
  $ECHO  
  $ECHO "ERROR: You must be super-user to run this script."  
  exit 1  
  fi  
  $ECHO "$WHO"  
 }  
   
 archive_exists ()  
 {  
  # Check archive file exist  
  if [ "$compress" = "1" -a -f "$file.gz" ]; then  
   $ECHO "1"  
  elif [ "$compress" = "0" -a -f "$file" ]; then  
  $ECHO "1"  
  else  
  $ECHO "0"  
  fi  
 }  
   
 set_file ()  
 {  
  # Check archive name exists  
  # and create new name if needful  
  remote_host_arg=$1  
  attempt=0  
   
  while [ "`archive_exists`" = "1" ]; do  
  file=`$ECHO $file|$CUT -f2 -d"."`  
  if [ -z "$remote_host_arg" ]; then  
   # Localhost file name  
   file="$dest/$system.$file.$attempt.$ext"  
  else  
   # Remote file name  
   file="$dest/$remote_host_arg.$file.$attempt.$ext"  
  fi  
  if [ "`archive_exists`" != "1" ]; then  
   break  
  fi  
  attempt=`expr $attempt + 1`  
  done  
 }  
   
 check_fs_exists ()  
 {  
  # Check filesystem exists  
  arg_fs=$1  
   
  ret=`$ZFS list -H -o name $arg_fs > /dev/null 2>&1; $ECHO $?`  
  if [ "$ret" != "0" ]; then  
  $ECHO "ERROR: ZFS pool/dataset $arg_fs does not exist."  
  $ECHO "    Please specify another ZFS."  
  $ECHO "Exiting..."  
  exit 1  
  fi  
 }  
   
 check_dest_dir ()  
 {  
  # Check directory exist and it writable  
  arg_dest=$1  
   
  if [ ! -d "$arg_dest" or ! -w "$arg_dest" ]; then  
  $ECHO "ERROR: Directory $arg_dest does not exist"  
  $ECHO "    or you haven't permissions to write."  
  $ECHO "Exiting..."  
  exit 1  
  fi   
 }  
   
 copyright_and_version ()  
 {  
  # Print package and copyright info  
  $ECHO  
  $ECHO "$PKG_NAME $PKG_VERSION $COPY"  
  $ECHO  
 }  
   
 usage_note ()  
 {  
  copyright_and_version  
  $ECHO "Usage: `$BASENAME $0` [-v] [-n] [-r] [-f] [pool|dataset] [/mntpoint|local fs] [host]"  
  $ECHO  
  $ECHO "Note: Compression will use if GZip installed,"  
  $ECHO "   both for local archives or remote streams transfer."  
  $ECHO "Beware: GZip must be installed in both nodes in case of remote backup."  
  exit 1  
 }  
   
 destroy_fs ()  
 {  
  # Destroy filesystem(s) recursively  
  arg_fs=$1  
   
  $ZFS destroy -r $arg_fs > /dev/null 2>&1  
   
  # Check exit code  
  if [ "`$ECHO $?`" != "0" ]; then  
  $ECHO "WARNING: Filesystem $arg_fs does not exists."  
  fi  
 }  
   
 create_snap ()  
 {  
  # Create snapshot recursively  
  arg_filesys=$1  
   
  $ZFS snapshot -r "$arg_filesys@$SNAP_EXT"  
 }  
   
 zfs_send ()  
 {  
  # Send filesystem to the destination  
  arg_filesys=$1  
  arg_dest=$2  
  arg_host=$3  
  # Verbose output flag set  
  if [ "$verbose" = "1" ]; then  
  verb="v"  
  fi  
  # When use compression, send fs with archiver  
  if [ "$compress" = "1" ]; then  
  if [ "$remote_mode" = "1" -a "$remote_file_archive" != "1" ]; then  
   # Send from remote with compression  
   $SSH $arg_host "/sbin/zfs snapshot -r $arg_filesys@$SNAP_EXT &&\  
   /sbin/zfs send -R$verb $arg_filesys@$SNAP_EXT|/bin/gzip -c -$COMP_LEVEL"|\  
   $GZIP -c -d -|$ZFS receive -dF$verb $arg_dest  
  elif [ "$remote_mode" = "1" -a "$remote_file_archive" = "1" ]; then  
   # Send from remote to file with compression  
   $SSH $arg_host "/sbin/zfs snapshot -r $arg_filesys@$SNAP_EXT &&\  
   /sbin/zfs send -R$verb $arg_filesys@$SNAP_EXT|/bin/gzip -c -$COMP_LEVEL">$file$ARC_SUFFIX  
  else  
   # Send to local with compression  
   $ZFS send -R"$verb" "$arg_filesys@$SNAP_EXT"|$GZIP -c "-$COMP_LEVEL">"$file$ARC_SUFFIX"  
  fi  
  else  
  if [ "$remote_mode" = "1" -a "$remote_file_archive" != "1" ]; then  
   # Send from remote without compression  
   $SSH $arg_host "/sbin/zfs snapshot -r $arg_filesys@$SNAP_EXT &&\  
   /sbin/zfs send -R$verb $arg_filesys@$SNAP_EXT"|\  
   $ZFS receive -dF$verb $arg_dest  
  elif [ "$remote_mode" = "1" -a "$remote_file_archive" = "1" ]; then  
   # Send from remote to file without compression  
   $SSH $arg_host "/sbin/zfs snapshot -r $arg_filesys@$SNAP_EXT &&\  
   /sbin/zfs send -R$verb $arg_filesys@$SNAP_EXT">$file$ARC_SUFFIX  
  else  
   # Send to local without compression  
   $ZFS send -R"$verb" "$arg_filesys@$SNAP_EXT">"$file"  
  fi  
  fi  
  # In remote mode destroy target snapshot on local machine and on remote machine  
  if [ "$remote_mode" = "1" ]; then  
  destroy_fs "`/sbin/zfs list -H -o name -t snapshot|/bin/grep $arg_dest`"  
  $SSH $arg_host "/sbin/zfs destroy -r `/sbin/zfs list -H -o name -t snapshot|\  
  /bin/grep $arg_filesys`> /dev/null 2>&1"  
  fi  
 }  
   
 ##############  
 # Main block #  
 ##############  
   
 # Checking OS  
 check_os  
   
 # Checking root  
 check_root  
   
 # Check command-line arguments  
 if [ "x$*" = "x" ]; then  
  # If arguments list empty, show usage note  
  usage_note  
 else  
  arg_list=$*  
  # Parse command line  
  set -- `$GETOPT fFrRnNvVhH: $arg_list` || {  
  usage_note  
  }  
   
  # Read arguments  
  for i in $arg_list  
  do  
   case $i in  
   -f|-F) remote_file_archive="1";;  
   -r|-R) remote_mode="1";;  
   -n|-N) compress_off="1";;  
   -v|-V) verbose="1";;  
   -h|-H|\?) usage_note;;  
   *) shift  
     filesystem=$1  
     dest=$2;  
     remote_host=$3  
     break;;  
   esac  
   shift  
  done  
   
  # Remove trailing --  
  shift `$EXPR $OPTIND - 1`  
 fi  
   
 # Check filesystem exists  
 check_fs_exists $filesystem  
   
 $ECHO "*** BEGIN: ZFS backup for $filesystem at `$DATE`."  
   
 # Check archiver  
 if [ ! -f "$GZIP" -a ! -x "$GZIP" -o "$compress_off" = "1" ]; then  
  $ECHO "INFO: Compression will NOT be used. GZip not found or compression disabled."  
  compress="0"  
 elif [ -f "$GZIP" -a -x "$GZIP" -a "$compress_off" != "1" ]; then  
  $ECHO "INFO: Data will be compressed with gzip -$COMP_LEVEL."  
  compress="1"  
 fi  
   
 if [ "$remote_mode" != "1" ]; then  
  # Check destination directory when file archive operation  
  check_dest_dir $dest  
  # Set initial archive file name  
  # Replase slashes with % if dataset specified in non-remote mode  
  file="$dest/$system.`$ECHO $filesystem | $SED -e 's/\//%/g'`.$initial_arc_seq.$ext"  
  # Set file name if incremental naming using  
  # (if archive with the same name already exists)  
  set_file  
 else  
  # Replase slashes with % if dataset specified in non-remote mode  
  file="$dest/$remote_host.`$ECHO $filesystem | $SED -e 's/\//%/g'`.$initial_arc_seq.$ext"  
  set_file $remote_host  
 fi  
   
 # First destroy snapshots recursively if it exists  
 destroy_fs "$filesystem@$SNAP_EXT"  
   
 # Second create recursive snapshots  
 if [ "$remote_mode" != "1" ]; then  
  create_snap $filesystem  
 fi  
   
 # Third send all snapshots recursively to archive  
 zfs_send $filesystem $dest $remote_host  
   
 # Finally destroy all snapshots recursively  
 if [ "$remote_mode" != "1" ]; then  
  destroy_fs "$filesystem@$SNAP_EXT"  
 fi  
   
 $ECHO "*** DONE: ZFS backup for $filesystem at `$DATE`."  

Небольшая цитата из readme:

 Пакет предназначен для создания компрессированных резервных  
 копий ZFS-пулов (включая root pool) и отдельных датасетов  
 с целью последующего восстановления bare-metal, для целей  
 ординарного  резервирования  и  восстановления  и  для  
 клонирования систем (в том числе с целью создания физических  
 standby-систем).  
   
 Он  реализован  в  виде  двух программ, zfsbackup  и  
 zfsrestore,  могущих использоваться как интерактивно, так  
 и в режиме командной строки (в том числе из cron), как  
 на  автономных  системах,  так  и в составе комплексных  
 решений.  
   
 По умолчанию, и при наличии в системе архиватора  GZip,  
 архивы  файловых  систем  создаются компрессированными  
 с   максимальной  степенью  компрессии,  для  экономии  
 пространства хранения и уменьшения объема передачи данных.  
   
 Пакет может резервировать данные либо локально или на NFS (в  
 виде файлового архива, компрессированного или нет, целого  
 пула данных либо отдельного датасета), либо c удаленного  
 бэкап-сервера.  Восстановление  файловых  систем  также  
 выполняется либо из файлового архива, взятого с локальной  
 системы либо NFS, либо с удаленного сервера резервных копий.  
   
 Вы также можете использовать пакет для резерврирования  
 принятой файловой системы на сервере резервного копирования  
 далее, в файловый архив.  
   
 Также вы можете создавать бэкап удаленной файловой системы на  
 сервер резервного копирования непосредственно в архивный файл  
 на диске или ленте.  
   

Как этим пользоваться? Снова readme:

 Команда  zfsbackup  создает рекурсивный снапшот ZFS от  
 уровня иерархии файловой системы, заданной в виде аргумента  
 (интерактивно или в командной строке) и создает на его  
 основе компрессированный с максимальной степенью сжатия  
 архив  (стрим)  (если  находит в системе установленный  
 архиватор,  в  данной версии GZip) на локальной точке  
 монтирования или на NFS.  
   
 Архив  может  быть распакован (в случае необходимости,  
 например, использования архива ZFS root pool при выполнении  
 flash-установок) вручную с использованием gzip или gzcat.  
   
 В удаленном режиме (опция -r) снапшот посылается посредством  
 SSH  с заданного удаленного хоста на сервер резервного  
 копирования  в  заданную  файловую  систему  (пул).  
 Стрим,посылаемый   по  сети,  так  же  может  быть  
 компрессирован или нет, как и в случае локальной архивации.  
   
 Команда  zfsrestore  используется  при  восстановлении  
 архивов  или  удаленных  файловых  систем,   созданных  
 программой   zfsbackup  .   Она  может  выполнять  
 восстановление  как  пулов  целиком  (в  первоначальном  
 состоянии), так и отдельных датасетов, в первоначальные или  
 произвольно заданные файловые системы.  
   
 Команда   zfsrestore   распознает    вид   архива  
 (компрессированный  или  без  компрессии) в локальном  
 режиме, и, в соответствие с  видом  архива  использует  
 соответствующий  метод восстановления. В удаленном режиме  
 (опция   -r)   ранее  посланная  файловая  система  
 восстанавливается на удаленной машине.  
   
 Команды могут использоваться как на автономных системах  
 с периодическим сохранением резервных копий на внешних  
 носителяХ, так и в системах масштаба предприятия, при  
 использовании ленточных носителей или NFS.  
   
 Резервное копирование  
 ---------------------  
   
 Для резервного копирования ZFS-пулов и датасетов (из любого  
 уровня иерархии) используется команда zfsbackup.  
   
 Команда может выполняться в двух режимах:  
   
 1. Резервирование в точку монтирования (локальную или NFS).  
 2. Резервирование с удаленной машины на сервер резервного  
 копирования.  
   
 Команда  автоматически  компрессирует  данные (стрим)  
 в случае обнаружения утилиты GZip с мааксимальной степенью  
 компрессии (gzip -9).  
   
 Команда   выполняет  создание  полного  рекурсивного  
 снапшота  ZFS-пула   либо  датасета,  с  последующей  
 автоматической компрессией  и  записью  файла в целевую  
 точку  монтирования  (локальную  либо NFS), в которую  
 суперпользоваль имеет право записи либо посылает сжатый  
 стрим с удаленной машины на резервный сервер в заданную  
 файловую систему или файловый архив через SSH.  
   
 По умолчанию имена архивов образуются по инкрементальному  
 принципу в следующем формате:  
   
 [hostname].[pool|pool%dataset].n.zfs<.gz>, n=0,1,2...  
   
 где  hostname  -  имя  хоста,  на котором выполняется  
 архивирование, pool|dataset - имя ZFS-пула или датасета, при  
 этом для датасетов, с целью корректного формирования имен  
 файлов,  осуществляется  замена  символов  "/" на "%".  
 Некомпрессированные  ZFS-стримы  имеют  расширение .zfs,  
 компрессированные - .gz .  
   
 Первый архив с одинаковым расширением в целевом каталоге  
 будет иметь номер 0, последующие, соответственно, 1,2,3  
 и так далее. Нумерация архивов с разными расширениям (.gz  
 или .zfs) будет выполняться независимо, от текущего номера  
 каждого типа архивов.  
   
 ВНИМАНИЕ! Не переименовывайте архивные файлы, поскольку  
 имена  архивов  используются  командой zfsrestore  для  
 восстановления в случае, если не задано имя целевой файловой  
 системы.  
   
 Выполнение  резервного  копирования производится вызовом  
 команды в локальном режиме:  
   
 # zfsbackup [-v] [-n] local_fs /mntpt  
   
 или в удаленном режиме:  
   
 # zfsbackup [-v] [-n] -r [-f] remote_fs /mntpt|local_fs \  
                     [user@][host]  
   
 В обеих случаях аргументы являются обязательными. Первым  
 аргументом  является локальный или удаленный архивируемый  
 ZFS-пул  либо датасет, вторым - локальная директория или  
 точка монтирования NFS с правами записи либо локальная  
 файловая  система  для  приема стрима, третий аргумент  
 в удаленном режиме представляет  имя  машины с которой  
 выполняется резервное копирование пула/датасета.  
   
 Команда выполняет  протоколирование  начала и окончания  
 резервного  копирования,  а  также  (в verbose режиме)  
 протоколирует  работу  команды  zfs  send. Все ошибки,  
 возникающие  в  процессе  резервного копирования, также  
 направляются  в  STDOUT  и  могут  быть перенаправлены  
 в журнальный файл.  
   
 Возможно также включение вызова команды в cron, как показано  
 на примере:  
   
 # Automated data backup job with zfs  
 # Running weekly at 00:00 Saturday  
 0 0 * * 6 [ -x /usr/local/bin/zfsbackup ] && \  
 rm -f /backup/*.data.* > /dev/null 2>&1; \  
 /usr/local/bin/zfsbackup data /backup >> /var/log/backup.log  
   
 # Automated system backup job with zfs  
 # Running weekly at 01:00 Saturday  
 0 1 * * 6 [ -x /usr/local/bin/zfsbackup ] && \  
 rm -f /backup/*.rpool.* > /dev/null 2>&1; \  
 /usr/local/bin/zfsbackup -r rpool root@backup backup \  
 >> /var/log/backup.log  
   

Как видите, никаких сложностей, никаких гуёв, никаких чудес. Простой и обыкновенный UNIX-way. Только штатные средства и никаких лицензий за тысячи долларов. Просто немного работы системного администратора. Ну и сервер резервного копирования, разумеется, который штатно строится из обычной машины Solaris с достаточным объемом СХД.

О восстановлении из созданных бэкапов мы поговорим в следующей статье.