diff --git a/Library/org.zfs.snapshot.hourly.plist b/Library/org.zfs.snapshot.hourly.plist new file mode 100644 index 0000000..96df579 --- /dev/null +++ b/Library/org.zfs.snapshot.hourly.plist @@ -0,0 +1,20 @@ + + + + + Label + org.zfs.snapshot.hourly + ProgramArguments + + /usr/sbin/zfs-auto-snapshot.sh + --syslog + --keep=24 + --label=hourly + // + + RunAtLoad + + StartInterval + 3600 + + diff --git a/Makefile b/Makefile index 8ad02b3..b558b41 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,16 @@ +SUPPORTED_PLATFORMS=linux-gnu darwin12 darwin11 + +ifeq (,$(findstring $(OSTYPE),$(SUPPORTED_PLATFORMS))) + +all %: + @echo The OS environment variable is set to [$(OSTYPE)]. + @echo Please set the OS environment variable to one of the following: + @echo $(SUPPORTED_PLATFORMS) + +else + all: -install: - install -d $(DESTDIR)$(PREFIX)/etc/cron.d - install -d $(DESTDIR)$(PREFIX)/etc/cron.daily - install -d $(DESTDIR)$(PREFIX)/etc/cron.hourly - install -d $(DESTDIR)$(PREFIX)/etc/cron.weekly - install -d $(DESTDIR)$(PREFIX)/etc/cron.monthly - install etc/zfs-auto-snapshot.cron.frequent $(DESTDIR)$(PREFIX)/etc/cron.d/zfs-auto-snapshot - install etc/zfs-auto-snapshot.cron.hourly $(DESTDIR)$(PREFIX)/etc/cron.hourly/zfs-auto-snapshot - install etc/zfs-auto-snapshot.cron.daily $(DESTDIR)$(PREFIX)/etc/cron.daily/zfs-auto-snapshot - install etc/zfs-auto-snapshot.cron.weekly $(DESTDIR)$(PREFIX)/etc/cron.weekly/zfs-auto-snapshot - install etc/zfs-auto-snapshot.cron.monthly $(DESTDIR)$(PREFIX)/etc/cron.monthly/zfs-auto-snapshot - install -d $(DESTDIR)$(PREFIX)/sbin - install src/zfs-auto-snapshot.sh $(DESTDIR)$(PREFIX)/sbin/zfs-auto-snapshot +include makefile.$(OSTYPE) + +endif diff --git a/README b/README index 745cf7d..4a6c959 100644 --- a/README +++ b/README @@ -1,12 +1,28 @@ zfs-auto-snapshot: An alternative implementation of the zfs-auto-snapshot service for Linux -that is compatible with zfs-linux and zfs-fuse. +and Macosx (currently tested and compatible with ZEVO Community Edition). -Automatically create, rotate, and destroy periodic ZFS snapshots. This is -the utility that creates the @zfs-auto-snap_frequent, @zfs-auto-snap_hourly, +It can automatically create, rotate, and destroy periodic ZFS snapshots. This +is utility that creates the @zfs-auto-snap_frequent, @zfs-auto-snap_hourly, @zfs-auto-snap_daily, @zfs-auto-snap_weekly, and @zfs-auto-snap_monthly snapshots if it is installed. -This program is a posixly correct bourne shell script. It depends only on -the zfs utilities and cron, and can run in the dash shell. +It can backup (send) the snapshots to remote systems or external disks, +utilizing zfs send command. On darwin this can replace TimeMachine backups, +currently not running on top of ZFS filesystems. + +This program is a posixly correct bourne shell script. It depends on zfs +utilities only (Linux). Unfortunatelly on Darwin it needs 'getopt' from +macports or homebrew. + +For using --send option, adapt opt_sendtocmd variable accordingly by editing the +script zfs-auto-snapshot.sh. + + sudo make OSTYPE=linux|darwin install + + installs cron / launchd startup scripts and copies script to /usr/sbin +directory. + + On darwin for daily, weekly and monthly stuff, anacron install is highly +recommended. diff --git a/makefile.darwin11 b/makefile.darwin11 new file mode 100644 index 0000000..be3f874 --- /dev/null +++ b/makefile.darwin11 @@ -0,0 +1,7 @@ + +install: + + install Library/org.zfs.snapshot.hourly.plist /Library/LaunchDaemons/org.zfs.snapshot.hourly.plist + install src/zfs-auto-snapshot.sh /usr/sbin/zfs-auto-snapshot.sh + launchctl load -w /Library/LaunchDaemons/org.zfs.snapshot.hourly.plist + diff --git a/makefile.darwin12 b/makefile.darwin12 new file mode 100644 index 0000000..be3f874 --- /dev/null +++ b/makefile.darwin12 @@ -0,0 +1,7 @@ + +install: + + install Library/org.zfs.snapshot.hourly.plist /Library/LaunchDaemons/org.zfs.snapshot.hourly.plist + install src/zfs-auto-snapshot.sh /usr/sbin/zfs-auto-snapshot.sh + launchctl load -w /Library/LaunchDaemons/org.zfs.snapshot.hourly.plist + diff --git a/makefile.linux-gnu b/makefile.linux-gnu new file mode 100644 index 0000000..d5f0b5d --- /dev/null +++ b/makefile.linux-gnu @@ -0,0 +1,15 @@ + +install: + + install -d $(DESTDIR)$(PREFIX)/etc/cron.d + install -d $(DESTDIR)$(PREFIX)/etc/cron.daily + install -d $(DESTDIR)$(PREFIX)/etc/cron.hourly + install -d $(DESTDIR)$(PREFIX)/etc/cron.weekly + install -d $(DESTDIR)$(PREFIX)/etc/cron.monthly + install etc/zfs-auto-snapshot.cron.frequent $(DESTDIR)$(PREFIX)/etc/cron.d/zfs-auto-snapshot + install etc/zfs-auto-snapshot.cron.hourly $(DESTDIR)$(PREFIX)/etc/cron.hourly/zfs-auto-snapshot + install etc/zfs-auto-snapshot.cron.daily $(DESTDIR)$(PREFIX)/etc/cron.daily/zfs-auto-snapshot + install etc/zfs-auto-snapshot.cron.weekly $(DESTDIR)$(PREFIX)/etc/cron.weekly/zfs-auto-snapshot + install etc/zfs-auto-snapshot.cron.monthly $(DESTDIR)$(PREFIX)/etc/cron.monthly/zfs-auto-snapshot + install -d $(DESTDIR)$(PREFIX)/sbin + install src/zfs-auto-snapshot.sh $(DESTDIR)$(PREFIX)/sbin/zfs-auto-snapshot diff --git a/src/zfs-auto-snapshot.sh b/src/zfs-auto-snapshot.sh index fb4c8d1..fad8b9a 100755 --- a/src/zfs-auto-snapshot.sh +++ b/src/zfs-auto-snapshot.sh @@ -1,9 +1,12 @@ #!/bin/sh -# zfs-auto-snapshot for Linux +# zfs-auto-snapshot for Linux and Macosx # Automatically create, rotate, and destroy periodic ZFS snapshots. # Copyright 2011 Darik Horn # +# zfs send, hanoi rotation, macosx/linux multiplatform changes - +# Matus Kral +# # This program 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 @@ -20,7 +23,7 @@ # # Set the field separator to a literal tab and newline. -IFS=" +IFS=" " # Set default program options. @@ -30,7 +33,7 @@ opt_default_exclude='' opt_dry_run='' opt_event='-' opt_keep='' -opt_label='' +opt_label='regular' opt_prefix='zfs-auto-snap' opt_recursive='' opt_sep='_' @@ -38,35 +41,106 @@ opt_setauto='' opt_syslog='' opt_skip_scrub='' opt_verbose='' +opt_remove='' +opt_fallback='0' +opt_force='' +opt_sendprefix='' +opt_send='no' +opt_atonce='-I' +opt_create='0' +opt_destroy='0' +opt_rotation='rr' +opt_base='day' +opt_namechange='0' +opt_factor='1' +opt_limit='3' +opt_includeall='0' + +# if pipe needs to be used, uncomment opt_pipe="|". arcfour or blowfish will reduce cpu load caused by ssh and mbuffer will +# boost network bandwidth and mitigate low and high peaks during transfer +opt_sendtocmd='ssh -2 root@media -c arcfour,blowfish-cbc -i /var/root/.ssh/media.rsa' +opt_buffer='' +#opt_buffer='mbuffer -q -m 250MB |' +opt_pipe='|' # Global summary statistics. DESTRUCTION_COUNT='0' SNAPSHOT_COUNT='0' WARNING_COUNT='0' +CREATION_COUNT='0' +SENT_COUNT='0' +KEEP='' + +PLATFORM_LOC='' +PLATFORM_REM='' # Other global variables. -SNAPSHOTS_OLD='' +SNAPSHOTS_OLD_LOC='' +SNAPSHOTS_OLD_REM='' +CREATED_TARGETS='' +ZFS_REMOTE_LIST='' +ZFS_LOCAL_LIST='' +TARGETS_DRECURSIVE='' +TARGETS_DREGULAR='' +MOUNTED_LIST_LOC='' +MOUNTED_LIST_REM='' +RC='99' +tmp_dir="/tmp/zfs-auto-snapshot.lock" print_usage () { - echo "Usage: $0 [options] [-l label] <'//' | name [name...]> - --default-exclude Exclude datasets if com.sun:auto-snapshot is unset. - -d, --debug Print debugging messages. - -e, --event=EVENT Set the com.sun:auto-snapshot-desc property to EVENT. - -n, --dry-run Print actions without actually doing anything. - -s, --skip-scrub Do not snapshot filesystems in scrubbing pools. - -h, --help Print this usage message. - -k, --keep=NUM Keep NUM recent snapshots and destroy older snapshots. - -l, --label=LAB LAB is usually 'hourly', 'daily', or 'monthly'. - -p, --prefix=PRE PRE is 'zfs-auto-snap' by default. - -q, --quiet Suppress warnings and notices at the console. - --send-full=F Send zfs full backup. Unimplemented. - --send-incr=F Send zfs incremental backup. Unimplemented. - --sep=CHAR Use CHAR to separate date stamps in snapshot names. - -g, --syslog Write messages into the system log. - -r, --recursive Snapshot named filesystem and all descendants. - -v, --verbose Print info messages. + echo "Usage: $0 [options] [-l label] <'//' | name [name...]> + + --default-exclude Exclude datasets if com.sun:auto-snapshot is unset + (not explicitly set to true). + --include-all Include datasets even if com.sun:auto-snapshot is set + to false. + --remove-local=n Remove local snapshots after successfully sent via + --send-incr or --send-full but still keeps n newest + snapshots (this will destroy snapshots named according + to --prefix, but regardless of --label). Only valid for + round-robin rotation. + -d, --debug Print debugging messages. + -e, --event=EVENT Set the com.sun:auto-snapshot-desc property to EVENT. + -n, --dry-run Print actions without actually doing anything. + -s, --skip-scrub Do not snapshot filesystems in scrubbing pools. + -h, --help Print this usage message. + -k, --keep=NUM Keep NUM recent snapshots and destroy older snapshots. + -l, --label=LAB LAB is usually 'hourly', 'daily', or 'monthly' (default + is 'regular'). + -p, --prefix=PRE PRE is 'zfs-auto-snap' by default. + -q, --quiet Suppress warnings and notices at the console. + -c, --create Create missing filesystems at destination. + -i, --send-at-once Send more incremental snapshots at once in one package + (-i argument is passed to zfs send instead of -I). + --send-full=F Send zfs full backup. F is target filesystem. + --send-incr=F Send zfs incremental backup. F is target filesystem. + --sep=CHAR Use CHAR to separate date stamps in snapshot names. + -X, --destroy Destroy remote snapshots to allow --send-full if + destination has snapshots (needed for -F in case + incremental snapshots on local and remote do not match). + -f is used automatically. + -F, --fallback Allow fallback from --send-incr to --send-full, + if incremental sending is not possible (filesystem + on remote just created or snapshots do not match - + see -X). + -g, --syslog Write messages into the system log. + -r, --recursive Snapshot named filesystem and all descendants. + -R, --replication Use zfs's replication (zfs send -R) instead of simple + send over newly created snapshots (check man zfs for + details). -f is used automatically. + -v, --verbose Print info messages. + -f, --force Passes -F argument to zfs receive (e.g. makes possible + to overwrite remote filesystem during --send-full). + -o, --rotation Round-robin (rr) or hanoi (hanoi) rotation (if -l nor -p + is specified, default label will change from 'regular' + to 'hanoi_regular'). + -a, --base Base unit for hanoi cycle. Can be minute, hour, day, + week, month or year (should follow your cron schedule + frequency). Default base is day. + --local-only Parameters opt_sendtocmd and opt_buffer are not used, + target for --send will be local machine. name Filesystem and volume names, or '//' for all ZFS datasets. " } @@ -79,40 +153,41 @@ print_log () # level, message, ... case $LEVEL in (eme*) - test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.emerge $* - echo Emergency: $* 1>&2 + test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.emerge "$*" + echo Emergency: "$*" 1>&2 ;; (ale*) - test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.alert $* - echo Alert: $* 1>&2 + test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.alert "$*" + echo Alert: "$*" 1>&2 ;; (cri*) - test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.crit $* - echo Critical: $* 1>&2 + test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.crit "$*" + echo Critical: "$*" 1>&2 ;; (err*) - test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.err $* - echo Error: $* 1>&2 + test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.err "$*" + echo Error: "$*" 1>&2 ;; (war*) - test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.warning $* - test -z "$opt_quiet" && echo Warning: $* 1>&2 + test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.warning "$*" + test -z "$opt_quiet" && echo Warning: "$*" 1>&2 + WARNING_COUNT=$(( $WARNING_COUNT + 1 )) ;; (not*) - test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.notice $* - test -z "$opt_quiet" && echo $* + test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.notice "$*" + test -z "$opt_quiet" && echo "$*" ;; (inf*) - # test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.info $* - test -n "$opt_verbose" && echo $* + # test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.info "$*" + test -n "$opt_verbose" && echo "$*" ;; (deb*) - # test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.debug $* - test -n "$opt_debug" && echo Debug: $* + # test -n "$opt_syslog" && logger -t "$opt_prefix" -p daemon.debug "$*" + test -n "$opt_debug" && echo Debug: "$*" ;; (*) - test -n "$opt_syslog" && logger -t "$opt_prefix" $* - echo $* 1>&2 + test -n "$opt_syslog" && logger -t "$opt_prefix" "$*" + echo "$*" 1>&2 ;; esac } @@ -122,7 +197,7 @@ do_run () # [argv] { if [ -n "$opt_dry_run" ] then - echo $* + echo "... Running $*" RC="$?" else eval $* @@ -137,68 +212,462 @@ do_run () # [argv] return "$RC" } +do_unmount () +{ + local TYPE="$1" + local FLAGS="$3" + local FSNAME="$4" + local SNAPNAME="$5" + local rsort_cmd='0' + local remote_cmd='' + + case "$TYPE" in + (remote) + umount_list="$MOUNTED_LIST_REM" + remote_cmd="$opt_sendtocmd" + ;; + (local) + umount_list="$MOUNTED_LIST_LOC" + ;; + esac + + if [ -n "$SNAPNAME" ]; then + SNAPNAME="@$SNAPNAME" + else + rsort_cmd='1' + fi + umount_list=$(printf "%s\n" "$umount_list" | grep ^"$FSNAME$SNAPNAME" ) + + test -z "$umount_list" && return 0 + + # reverse sort the list if unmounting filesystem and not only snapshot + umount_list=$(printf "%s\n" "$umount_list" | awk -F'\t' '{print $2}') + test $rsort_cmd -eq '1' && umount_list=$(printf "%s\n" "$umount_list" | sort -r) + + for kk in $umount_list; do + print_log debug "Trying to unmount '$kk'." + umount_cmd="umount '$kk'" + if ! do_run "$remote_cmd" "$umount_cmd"; then return "$RC"; fi + test "$FLAGS" != "-r" && break + done + + return 0 +} + +do_delete () +{ + + local DEL_TYPE="$1" + local FSSNAPNAME="$2" + local FLAGS="$3" + KEEP="$4" + local FSNAME=$(echo $FSSNAPNAME | awk -F'@' '{print $1}') + local SNAPNAME=$(echo $FSSNAPNAME | awk -F'@' '{print $2}') + local remote_cmd='' + + if [ "$FSSNAPNAME" = "$FSNAME" -a "$FLAGS" = "-r" ]; then + if [ "$opt_destroy" -ne '1' ]; then + print_log warning "Filesystem $FSNAME destroy requested, but option -X not specified. Aborting." + return 1 + else + KEEP='0' + fi + fi + + KEEP=$(( $KEEP - 1 )) + if [ "$KEEP" -le '0' ] + then + if do_unmount "$DEL_TYPE" "" "$FLAGS" "$FSNAME" "$SNAPNAME"; then + if [ "$DEL_TYPE" = "remote" ]; then + remote_cmd="$opt_sendtocmd" + fi + if do_run "$remote_cmd" "zfs destroy $FLAGS '$FSSNAPNAME'"; then + DESTRUCTION_COUNT=$(( $DESTRUCTION_COUNT + 1 )) + fi + fi + fi + + return "$RC" +} + +is_member () +{ + local ARRAY="$1" + local MEMBER="$2" + local ISMEMBER='1' + local mm='' + + for mm in $ARRAY; do + if test "$mm" = "$MEMBER"; then + ISMEMBER='0' + break + fi + done + + return "$ISMEMBER" +} + +do_send () +{ + local SENDTYPE="$1" + local SNAPFROM="$2" + local SNAPTO="$3" + local SENDFLAGS="$4" + local REMOTEFS="$5" + local list_child='' + local lq='' + + if [ "$SENDFLAGS" = "-R" -a "$SENDTYPE" = "full" ]; then + # for full send with -R, target filesystem must be with no snapshots (including snapshots on child filesystems) + list_child=$(printf "%s\n" "$SNAPSHOTS_OLD_REM" | grep ^"$REMOTEFS/" ) + fi + if [ "$SENDTYPE" = "full" ]; then + list_child=$(printf "%s\n%s\n" "$list_child" $(printf "%s\n" "$SNAPSHOTS_OLD_REM" | grep ^"$REMOTEFS@" ) ) + fi + + for ll in $list_child; do + if [ "$opt_destroy" -eq '1' ]; then + if do_delete "remote" "$ll" ""; then + continue + fi + fi + print_log debug "Can't destroy remote objects $REMOTEFS ($ll). Can't continue with send-full. -X allowed?" + return 1 + done + + test -n "$opt_buffer" && lq="'" + + test $SENDTYPE = "incr" && do_run "zfs send " "$SENDFLAGS" "$opt_atonce $SNAPFROM $SNAPTO" "$opt_pipe" "$opt_sendtocmd" "$lq$opt_buffer zfs recv $opt_force -u $REMOTEFS$lq" + test $SENDTYPE = "full" && do_run "zfs send " "$SENDFLAGS" "$SNAPTO" "$opt_pipe" "$opt_sendtocmd" "$lq$opt_buffer zfs recv $opt_force -u $REMOTEFS$lq" + + return "$RC" +} + +delete_rotation_hanoi () +{ + + local SND_RC="$1" + local FALLBACK="$2" + local FSNAME="$3" + local GLOB="$4" + local FLAGS="$5" + local SNAPNAME="$6" + + local base_minute=$((60 * $opt_factor )) + local base_hour=$(($base_minute * 60)) + local base_day=$(($base_hour * 24)) + local base_week=$(($base_day * 7)) + local base_month=$(($base_day * 31)) + local base="base_$opt_base" + + local opt_hbase=$(eval echo \$$base) + + classify () + { + rec () + { + local class='0' + local nr="$1" + + while test $(( 1 << $(($class)) )) -le $(($nr>>1)); do + class=$(($class+1)) + done + bla=$(($nr - $(( 1 << $(($class)) )) )) + test "$bla" -eq '0' && echo $(($class+1)) || rec "$bla" + } + + local creation="$1" + local creation_std='' + local snapdate='' + + case $PLATFORM_LOC in + (Linux) + snapdate=$(echo "$creation" | awk -F'-' '{print $1"-"$2"-"$3" "$4}') + creation_std=$(($(env LC_ALL=C date -d "$snapdate" +%s ) / $opt_hbase )) + ;; + (Darwin) + creation_std=$(($(env LC_ALL=C date -j -f "%F-%H%M" "$creation" +%s ) / $opt_hbase )) + ;; + esac + + echo $(rec $creation_std) + } + + destroy () + { + local dlist="$1" + local dprefix="$2" + local dtype="$3" + local dFSNAME="$4" + local dFLAGS="$5" + local class='' + local previous_class='0' + + tmp_table=$(printf "%s\n" "$dlist" |\ + grep -e ^"$dprefix$FSNAME@$opt_prefix.$opt_label" | + while read name; do + echo "$(classify ${name#$dprefix$FSNAME@$opt_prefix${opt_label:+?$opt_label}?}) $name" + done | sort -k 1rn -k 2r | awk '{print $1"\t"$2}') + + for mm in $tmp_table + do + class=$(echo "$mm" | awk -F'\t' '{print $1}') + if [ "$class" -eq "$previous_class" ]; then + do_delete "$dtype" $(echo "$mm" | awk -F'\t' '{print $2}') "$FLAGS" + fi + previous_class="$class" + done + + } + + if [ "$SND_RC" -eq '0' -a "$opt_send" != "no" -o "$opt_send" = "no" ]; then + destroy "$(printf "%s\n%s\n" "$SNAPSHOTS_OLD_LOC" "$FSNAME@$SNAPNAME" )" "" "local" "$FSNAME" "$FLAGS" + if [ "$opt_send" != "no" ]; then + destroy "$(printf "%s\n%s\n" "$SNAPSHOTS_OLD_REM" "$opt_sendprefix/$FSNAME@$SNAPNAME" )" "$opt_sendprefix/" "remote" "$FSNAME" "$FLAGS" + fi + fi + +} + +delete_rotation_rr () +{ + + local SND_RC="$1" + local FALLBACK="$2" + local FSNAME="$3" + local GLOB="$4" + local FLAGS="$5" + + # Retain at most $opt_keep number of old snapshots of this filesystem, + # including the one that was just recently created. + if [ -z "$opt_keep" ] + then + print_log debug "Number of snapshots not specified. Keeping all." + continue + elif [ "$opt_send" != "no" ] && [ "$SND_RC" -ne '0' ] + then + print_log debug "Sending of filesystem was requested, but send failed. Ommiting destroy procedures." + continue + elif [ "$opt_send" != "no" -a -n "$opt_remove" ] + then + KEEP="$opt_remove" + else + KEEP="$opt_keep" + fi + print_log debug "Destroying local snapshots, keeping $KEEP." + + # ASSERT: The old snapshot list is sorted by increasing age. + for jj in $SNAPSHOTS_OLD_LOC + do + # Check whether this is an old snapshot of the filesystem. + test -z "${jj#$FSNAME@$GLOB}" -o -z "${jj##$FSNAME@$opt_prefix*}" -a -n "$opt_remove" -a "$opt_send" != "no" && do_delete "local" "$jj" "$FLAGS" "$KEEP" + done + + if [ "$opt_send" = "no" ] + then + print_log debug "No sending option specified, skipping remote snapshot removal." + continue + elif [ "$sFLAGS" = "-R" ] + then + print_log debug "Replication specified, remote snapshots were removed while sending." + continue + elif [ "$opt_destroy" -eq '1' -a "$FALLBACK" -ne '0' -o "$opt_send" = "full" ] + then + print_log debug "Sent full copy, all remote snapshots were already destroyed." + continue + else + KEEP="$opt_keep" + print_log debug "Destroying remote snapshots, keeping $KEEP." + fi + + # ASSERT: The old snapshot list is sorted by increasing age. + for jj in $SNAPSHOTS_OLD_REM + do + # Check whether this is an old snapshot of the filesystem. + test -z "${jj#$opt_sendprefix/$FSNAME@$GLOB}" && do_delete "remote" "$jj" "$FLAGS" "$KEEP" + done + +} do_snapshots () # properties, flags, snapname, oldglob, [targets...] { local PROPS="$1" - local FLAGS="$2" + local sFLAGS="$2" local NAME="$3" local GLOB="$4" local TARGETS="$5" - local KEEP='' + local LAST_REMOTE='' - # global DESTRUCTION_COUNT - # global SNAPSHOT_COUNT - # global WARNING_COUNT - # global SNAPSHOTS_OLD + local FALLBACK='' + + if test "$sFLAGS" = '-R'; then + FLAGS='-r' + SNexp='.*' + else + FLAGS='' + fi for ii in $TARGETS do - if do_run "zfs snapshot $PROPS $FLAGS '$ii@$NAME'" + FALLBACK='0' + SND_RC='1' + + print_log debug "--> Snapshooting $ii" + + if ! do_run "zfs snapshot $PROPS $FLAGS '$ii@$NAME'" then - SNAPSHOT_COUNT=$(( $SNAPSHOT_COUNT + 1 )) - else - WARNING_COUNT=$(( $WARNING_COUNT + 1 )) continue - fi + fi + SNAPSHOT_COUNT=$(( $SNAPSHOT_COUNT + 1 )) - # Retain at most $opt_keep number of old snapshots of this filesystem, - # including the one that was just recently created. - test -z "$opt_keep" && continue - KEEP="$opt_keep" + if [ "$opt_send" = "incr" ] + then - # ASSERT: The old snapshot list is sorted by increasing age. - for jj in $SNAPSHOTS_OLD - do - # Check whether this is an old snapshot of the filesystem. - if [ -z "${jj#$ii@$GLOB}" ] + LAST_REMOTE=$(printf "%s\n" "$SNAPSHOTS_OLD_REM" | grep ^"$opt_sendprefix/$ii@" | grep -m1 . | awk -F'@' '{print $2}') + + # in case of -R and incremental send, receiving side needs to have $LAST_REMOTE snapshot for each replicated filesystem + if [ "$FLAGS" = "-r" ]; then + snaps_needed=$(( $(printf "%s\n" "$ZFS_LOCAL_LIST" | grep -c ^"$ii/") + 1 )) + else + snaps_needed='1' + fi + + # remote filesystem just created. if -R run + if is_member "$CREATED_TARGETS" "$opt_sendprefix/$ii" + then + FALLBACK='2' + elif [ -z "$LAST_REMOTE" ] then - KEEP=$(( $KEEP - 1 )) - if [ "$KEEP" -le '0' ] - then - if do_run "zfs destroy $FLAGS '$jj'" - then - DESTRUCTION_COUNT=$(( $DESTRUCTION_COUNT + 1 )) + # no snapshot on remote + FALLBACK='1' + elif [ "$snaps_needed" -ne $(printf "%s" "$SNAPSHOTS_OLD_REM" | grep -c -e ^"$opt_sendprefix/$ii$SNexp@$LAST_REMOTE" ) -o \ + "$snaps_needed" -ne $(printf "%s" "$SNAPSHOTS_OLD_LOC" | grep -c -e ^"$ii$SNexp@$LAST_REMOTE" ) ] + then + FALLBACK='3' + else + FALLBACK='0' + fi + + case "$FALLBACK" in + (1) + print_log info "Going back to full send, no snapshot exists at destination: $ii" + ;; + (2) + print_log info "Going back to full send, remote filesystem was just created: $ii" + ;; + (3) + if [ "$FLAGS" = "-r" ]; then + print_log info "Going back to full send, last snapshot on remote is not the last one for whole recursion: $opt_sendprefix/$ii@$LAST_REMOTE" else - WARNING_COUNT=$(( $WARNING_COUNT + 1 )) + print_log info "Going back to full send, last snapshot on remote is not available on local: $opt_sendprefix/$ii@$LAST_REMOTE" fi - fi - fi - done + ;; + (0) + do_send "incr" "$ii@$LAST_REMOTE" "$ii@$NAME" "$sFLAGS" "$opt_sendprefix/$ii" + SND_RC="$?" + ;; + esac + fi + + if [ "$opt_send" = "full" -o "$FALLBACK" -ne '0' -a "$opt_fallback" -eq '1' ]; then + do_send "full" "" "$ii@$NAME" "$sFLAGS" "$opt_sendprefix/$ii" + SND_RC="$?" + fi + test "$SND_RC" -eq '0' && SENT_COUNT=$(( $SENT_COUNT + 1 )) + + case $opt_rotation in + (rr) + delete_rotation_rr "$SND_RC" "$FALLBACK" "$ii" "$GLOB" "$FLAGS" + ;; + (hanoi) + delete_rotation_hanoi "$SND_RC" "$FALLBACK" "$ii" "$GLOB" "$FLAGS" "$NAME" + ;; + esac + done } +do_getmountedfs () +{ + + local MOUNTED_TYPE="$1" + local MOUNTED_LIST='' + local remote_cmd='' + + case "$MOUNTED_TYPE" in + (remote) + remote_cmd="$opt_sendtocmd" + PLATFORM="$PLATFORM_REM" + ;; + (local) + PLATFORM="$PLATFORM_LOC" + ;; + esac + + case "$PLATFORM" in + (Linux) + MOUNTED_LIST=$(eval $remote_cmd cat /proc/mounts | grep zfs | awk -F' ' '{OFS="\t"}{ORS="\n"}{print $1,$2}' ) + ;; + (Darwin) + MOUNTED_LIST=$(printf "%s\n%s\n" $(eval $remote_cmd zfs mount | awk -F' ' '{OFS="\t"}{ORS="\n"}{print $1,$2}') \ + $(eval $remote_cmd mount -t zfs | grep @ | awk -F' ' '{OFS="\t"}{ORS="\n"}{print $1,$3}') ) + ;; + esac + + printf "%s\n" "$MOUNTED_LIST" | sort +} + +do_createfs () +{ + + local FS="$1" + + for ii in $FS; do + + print_log debug "checking: $opt_sendprefix/$ii" + + if ! is_member "$ZFS_REMOTE_LIST" "$opt_sendprefix/$ii" -eq 0 + then + print_log debug "creating: $opt_sendprefix/$ii" + + if do_run "$opt_sendtocmd" "zfs create -p -o canmount=off -o snapdir=hidden $opt_sendprefix/$ii" + then + CREATION_COUNT=$(( $CREATION_COUNT + 1 )) + CREATED_TARGETS=$(printf "%s\n%s\n" "$CREATED_TARGETS" "$opt_sendprefix/$ii" ) + fi + fi + done + +} # main () # { -GETOPT=$(getopt \ - --longoptions=default-exclude,dry-run,skip-scrub,recursive \ - --longoptions=event:,keep:,label:,prefix:,sep: \ - --longoptions=debug,help,quiet,syslog,verbose \ - --options=dnshe:l:k:p:rs:qgv \ - -- "$@" ) \ - || exit 128 +PLATFORM_LOC=`uname` +case "$PLATFORM_LOC" in + (Linux) + getopt_cmd='getopt' + ;; + (Darwin) + # macports path as default. Homebrew as fallback + getopt_cmd='/opt/local/bin/getopt' + if [ ! -f $getopt_cmd ]; then + getopt_cmd='/usr/local/opt/gnu-getopt/bin/getopt' + fi + ;; + (*) + print_log error "Local system not known ($PLATFORM_LOC) - needs one of Darwin, Linux. Exiting." + exit 300 + ;; +esac + +GETOPT=$("$getopt_cmd" \ + --longoptions=default-exclude,dry-run,skip-scrub,recursive,send-atonce,rotation:,local-only \ + --longoptions=event:,keep:,label:,prefix:,sep:,create,fallback,rollback,base:,factor: \ + --longoptions=debug,help,quiet,syslog,verbose,send-full:,send-incr:,remove-local:,destroy,include-all \ + --options=dnshe:l:k:p:rs:qgvfixcXFRba:o: \ + -- "$@" ) \ + || exit 128 eval set -- "$GETOPT" @@ -211,7 +680,11 @@ do opt_verbose='1' shift 1 ;; - (--default-exclude) + (-c|--create) + opt_create='1' + shift 1 + ;; + (-x|--default-exclude) opt_default_exclude='1' shift 1 ;; @@ -219,7 +692,7 @@ do if [ "${#2}" -gt '1024' ] then print_log error "The $1 parameter must be less than 1025 characters." - exit 139 + exit 239 elif [ "${#2}" -gt '0' ] then opt_event="$2" @@ -238,17 +711,53 @@ do print_usage exit 0 ;; + (--local-only) + opt_sendtocmd='' + opt_buffer='' + shift 1 + ;; + (--include-all) + opt_includeall='1' + print_log debug "Not considering com.sun:auto-snapshot." + shift 1 + ;; (-k|--keep) if ! test "$2" -gt '0' 2>/dev/null then print_log error "The $1 parameter must be a positive integer." - exit 129 + exit 229 fi opt_keep="$2" shift 2 ;; + (-a|--base) + case $2 in + (day|week|month|hour|minute) + opt_base="$2" + ;; + (*) + print_log error "The $1 parameter must be one of: minute, hour, day, week, month, year." + exit 244 + ;; + esac + shift 2 + ;; + (-o|--rotation) + case $2 in + (hanoi|rr) + opt_rotation="$2" + ;; + (*) + print_log error "Rotation must be one of hanoi or rr + ." + exit 245 + ;; + esac + shift 2 + ;; (-l|--label) opt_label="$2" + opt_namechange='1' shift 2 ;; (-p|--prefix) @@ -258,22 +767,41 @@ do case $opt_prefix in ([![:alnum:]_.:\ -]*) print_log error "The $1 parameter must be alphanumeric." - exit 130 + exit 230 ;; esac opt_prefix="${opt_prefix#?}" done opt_prefix="$2" + opt_namechange='1' + shift 2 + ;; + (--factor) + opt_factor="$2" shift 2 ;; (-q|--quiet) opt_debug='' - opt_quiet='1' + opt_quiet='1' opt_verbose='' shift 1 ;; (-r|--recursive) - opt_recursive='1' + opt_recursive=' ' + shift 1 + ;; + (-R|--replication) + opt_recursive='-R' + opt_force='-F' + shift 1 + ;; + (-X|--destroy) + opt_destroy='1' + opt_force='-F' + shift 1 + ;; + (-F|--fallback) + opt_fallback='1' shift 1 ;; (--sep) @@ -283,25 +811,69 @@ do ;; ('') print_log error "The $1 parameter must be non-empty." - exit 131 + exit 231 ;; (*) print_log error "The $1 parameter must be one alphanumeric character." - exit 132 + exit 232 ;; esac opt_sep="$2" shift 2 ;; + (--send-full) + if [ -n "$opt_sendprefix" ]; then + print_log error "Only one of --send-incr and --send-full must be specified." + exit 239 + fi + if [ -z "$2" ]; then + print_log error "Target filesystem needs to be specified with --send-full." + exit 243 + fi + opt_sendprefix="$2" + opt_send='full' + shift 2 + ;; + (--send-incr) + opt_sendincr="$2" + if [ -n "$opt_sendprefix" ]; then + print_log error "Only one of --send-incr and --send-full must be specified." + exit 240 + fi + if [ -z "$2" ]; then + print_log error "Target filesystem needs to be specified with --send-incr." + exit 242 + fi + opt_sendprefix="$2" + opt_send='incr' + shift 2 + ;; (-g|--syslog) opt_syslog='1' shift 1 ;; + (-i|--send-atonce) + opt_atonce='-i' + shift 1 + ;; + (--remove-local) + if ! test "$2" -gt '0' 2>/dev/null + then + print_log error "The $1 parameter must be a positive integer." + exit 241 + fi + opt_remove="$2" + shift 2 + ;; (-v|--verbose) opt_quiet='' opt_verbose='1' shift 1 ;; + (-f|--force|-b|--rollback) + opt_force='-F' + shift 1 + ;; (--) shift 1 break @@ -315,6 +887,20 @@ then exit 133 fi +# ISO style date; fifteen characters: YYYY-MM-DD-HHMM +# On Solaris %H%M expands to 12h34. +DATE=$(date +%F-%H%M) + +COUNTER='0' +while true; do + if do_run "mkdir '${tmp_dir}'"; then break; fi + print_log error "another copy is running ... $COUNTER" + test "$COUNTER" -gt '11' && exit 99 + sleep 5 + COUNTER=$(( $COUNTER + 1 )) +done +trap "rm -fr '${tmp_dir}'" INT TERM EXIT + # Count the number of times '//' appears on the command line. SLASHIES='0' for ii in "$@" @@ -332,65 +918,66 @@ fi # this program for Linux has a much better runtime complexity than the similar # Solaris implementation. -ZPOOL_STATUS=$(env LC_ALL=C zpool status 2>&1 ) \ - || { print_log error "zpool status $?: $ZPOOL_STATUS"; exit 135; } - -ZFS_LIST=$(env LC_ALL=C zfs list -H -t filesystem,volume -s name \ - -o name,com.sun:auto-snapshot,com.sun:auto-snapshot:"$opt_label") \ - || { print_log error "zfs list $?: $ZFS_LIST"; exit 136; } +ZPOOL_STATUS=$(env LC_ALL=C zpool status 2>&1 )\ + || { print_log error "zpool status $?: $ZPOOL_STATUS"; exit 135; } -SNAPSHOTS_OLD=$(env LC_ALL=C zfs list -H -t snapshot -S creation -o name) \ - || { print_log error "zfs list $?: $SNAPSHOTS_OLD"; exit 137; } +ZFS_LIST=$(env LC_ALL=C zfs list -H -t filesystem,volume -s name\ + -o name,com.sun:auto-snapshot,com.sun:auto-snapshot:"$opt_label",mountpoint,canmount,snapdir)\ + || { print_log error "zfs list $?: $ZFS_LIST"; exit 136; } +ZFS_LOCAL_LIST=$(echo "$ZFS_LIST" | awk -F'\t' '{print $1}') # Verify that each argument is a filesystem or volume. for ii in "$@" do test "$ii" = '//' && continue 1 - while read NAME PROPERTIES + for jj in $ZFS_LOCAL_LIST do - test "$ii" = "$NAME" && continue 2 - done <<-HERE - $ZFS_LIST - HERE + test "$ii" = "$jj" && continue 2 + done print_log error "$ii is not a ZFS filesystem or volume." exit 138 done # Get a list of pools that are being scrubbed. ZPOOLS_SCRUBBING=$(echo "$ZPOOL_STATUS" | awk -F ': ' \ - '$1 ~ /^ *pool$/ { pool = $2 } ; \ - $1 ~ /^ *scan$/ && $2 ~ /scrub in progress/ { print pool }' \ - | sort ) + '$1 ~ /^ *pool$/ { pool = $2 } ; \ + $1 ~ /^ *scan$/ && $2 ~ /scrub in progress/ { print pool }' \ + | sort ) # Get a list of pools that cannot do a snapshot. ZPOOLS_NOTREADY=$(echo "$ZPOOL_STATUS" | awk -F ': ' \ - '$1 ~ /^ *pool$/ { pool = $2 } ; \ - $1 ~ /^ *state$/ && $2 !~ /ONLINE|DEGRADED/ { print pool } ' \ - | sort) - -# Get a list of datasets for which snapshots are explicitly disabled. -NOAUTO=$(echo "$ZFS_LIST" | awk -F '\t' \ - 'tolower($2) ~ /false/ || tolower($3) ~ /false/ {print $1}') + '$1 ~ /^ *pool$/ { pool = $2 } ; \ + $1 ~ /^ *state$/ && $2 !~ /ONLINE|DEGRADED/ { print pool } ' \ + | sort ) # If the --default-exclude flag is set, then exclude all datasets that lack # an explicit com.sun:auto-snapshot* property. Otherwise, include them. -if [ -n "$opt_default_exclude" ] -then - # Get a list of datasets for which snapshots are explicitly enabled. +if [ "$opt_includeall" -eq '0' ]; then + # Get a list of datasets for which snapshots are not explicitly disabled. CANDIDATES=$(echo "$ZFS_LIST" | awk -F '\t' \ - 'tolower($2) ~ /true/ || tolower($3) ~ /true/ {print $1}') + 'tolower($2) !~ /false/ && tolower($3) !~ /false/ {print $1}' ) + + if [ -n "$opt_default_exclude" ] + then + # Get a list of datasets for which snapshots are not explicitly enabled. + NOAUTO=$(echo "$ZFS_LIST" | awk -F '\t' \ + 'tolower($2) !~ /true/ && tolower($3) !~ /true/ {print $1}') + else + # Get a list of datasets for which snapshots are explicitly disabled. + NOAUTO=$(echo "$ZFS_LIST" | awk -F '\t' \ + 'tolower($2) ~ /false/ || tolower($3) ~ /false/ {print $1}') + fi else - # Invert the NOAUTO list. - CANDIDATES=$(echo "$ZFS_LIST" | awk -F '\t' \ - 'tolower($2) !~ /false/ && tolower($3) !~ /false/ {print $1}') + CANDIDATES=$(echo "$ZFS_LIST" | awk -F '\t' '{print $1}') fi # Initialize the list of datasets that will get a recursive snapshot. -TARGETS_RECURSIVE='' +TARGETS_DRECURSIVE='' +TARGETS_TMP_RECURSIVE='' # Initialize the list of datasets that will get a non-recursive snapshot. -TARGETS_REGULAR='' +TARGETS_DREGULAR='' for ii in $CANDIDATES do @@ -403,7 +990,7 @@ do IN_ARGS='0' for jj in "$@" do - if [ "$jj" = '//' -o "$jj" = "$ii" ] + if [ "$jj" = '//' -o "$jj" = "$ii" -o -n "$opt_recursive" -a -z "${ii##$jj/*}" ] then IN_ARGS=$(( $IN_ARGS + 1 )) fi @@ -441,29 +1028,31 @@ do fi done + noauto_parent='0' for jj in $NOAUTO do # Ibid regarding iii. jjj="$jj/" - # The --recursive switch only matters for non-wild arguments. - if [ -z "$opt_recursive" -a "$1" != '//' ] + if [ "$jjj" = "$iii" ] then - # Snapshot this dataset non-recursively. - print_log debug "Including $ii for regular snapshot." - TARGETS_REGULAR="${TARGETS_REGULAR:+$TARGETS_REGULAR }$ii" # nb: \t continue 2 - # Check whether the candidate name is a prefix of any excluded dataset name. + # Check whether the candidate name is a prefix of any excluded dataset name. elif [ "$jjj" != "${jjj#$iii}" ] then - # Snapshot this dataset non-recursively. - print_log debug "Including $ii for regular snapshot." - TARGETS_REGULAR="${TARGETS_REGULAR:+$TARGETS_REGULAR }$ii" # nb: \t - continue 2 + noauto_parent='1' && break fi done - for jj in $TARGETS_RECURSIVE + # not scrubbing + if [ -z "$opt_recursive" -a "$1" != '//' -o "$noauto_parent" = '1' ] + then + print_log debug "Including $ii for regular snapshot." + TARGETS_DREGULAR=$(printf "%s\n%s\n" "$TARGETS_DREGULAR" "$ii" ) + continue + fi + + for jj in $TARGETS_TMP_RECURSIVE do # Ibid regarding iii. jjj="$jj/" @@ -484,16 +1073,18 @@ do # * Is not the descendant of an already included filesystem. # print_log debug "Including $ii for recursive snapshot." - TARGETS_RECURSIVE="${TARGETS_RECURSIVE:+$TARGETS_RECURSIVE }$ii" # nb: \t + TARGETS_TMP_RECURSIVE=$( printf "%s\n%s\n" $TARGETS_TMP_RECURSIVE "$ii" ) + done # Linux lacks SMF and the notion of an FMRI event, but always set this property # because the SUNW program does. The dash character is the default. SNAPPROP="-o com.sun:auto-snapshot-desc='$opt_event'" -# ISO style date; fifteen characters: YYYY-MM-DD-HHMM -# On Solaris %H%M expands to 12h34. -DATE=$(date +%F-%H%M) +# if hanoi rotation was requested but prefix or label wasn't changed from default, change label to hanoi to avoid mixing of those backup sets. +if [ "$opt_namechange" -eq '0' ] && [ "$opt_rotation" = "hanoi" ]; then + opt_label="hanoi_regular" +fi # The snapshot name after the @ symbol. SNAPNAME="$opt_prefix${opt_label:+$opt_sep$opt_label-$DATE}" @@ -501,22 +1092,95 @@ SNAPNAME="$opt_prefix${opt_label:+$opt_sep$opt_label-$DATE}" # The expression for matching old snapshots. -YYYY-MM-DD-HHMM SNAPGLOB="$opt_prefix${opt_label:+?$opt_label}????????????????" -test -n "$TARGETS_REGULAR" \ - && print_log info "Doing regular snapshots of $TARGETS_REGULAR" +msg_to_log="Using $opt_rotation type rotation, with params keep: $opt_keep" +if test "$opt_rotation" = "hanoi"; then + msg_to_log=$(echo "$msg_to_log," "base: $opt_base") +fi +print_log debug "$msg_to_log." + +test -n "$TARGETS_DREGULAR" && \ + print_log info "Doing regular snapshots of $(echo $TARGETS_DREGULAR)" + +test -n "$TARGETS_TMP_RECURSIVE" && \ + print_log info "Doing recursive snapshots of $(echo $TARGETS_TMP_RECURSIVE)" -test -n "$TARGETS_RECURSIVE" \ - && print_log info "Doing recursive snapshots of $TARGETS_RECURSIVE" +SNAPSHOTS_OLD_LOC=$(env LC_ALL=C zfs list -r -H -t snapshot -S creation -o name $(echo "$TARGETS_DREGULAR") $(echo "$TARGETS_TMP_RECURSIVE") ) \ + || { print_log error "zfs list $?: $SNAPSHOTS_OLD_LOC"; exit 137; } test -n "$opt_dry_run" \ - && print_log info "Doing a dry run. Not running these commands..." + && print_log info "Doing a dry run. Not running these commands..." + +# expand FS list if replication is not used +if [ "$opt_recursive" = ' ' -o "$1" = "//" ] +then + for ii in $TARGETS_TMP_RECURSIVE; do TARGETS_DRECURSIVE=$(printf "%s\n%s\n%s\n" "$TARGETS_DRECURSIVE" $(printf "$ii\n") $(printf "%s\n" "$ZFS_LOCAL_LIST" | grep ^"$ii/") ); done +else + TARGETS_DRECURSIVE="$TARGETS_TMP_RECURSIVE" +fi + +MOUNTED_LIST_LOC=$(eval do_getmountedfs "local") + +# initialize remote system parameters, filesystems, mounts and snapshots +if [ "$opt_send" != "no" ] +then + PLATFORM_REM=$(eval "$opt_sendtocmd" "uname") + + case "$PLATFORM_REM" in + (Linux|Darwin) + ;; + (*) + print_log error "Remote system not known ($PLATFORM_REM) - needs one of Darwin, Linux. Exiting." + exit 301 + ;; + esac + + if [ -n $opt_limit ]; then + runs='1' + condition='1' + while [ $condition -eq '1' ]; do + load=$(eval "$opt_sendtocmd" "uptime") + load=$(echo ${load##*"load average"} | awk '{print $2}' | awk -F'.' '{print $1}') + if [ $load -ge $opt_limit -a $runs -lt '3' ]; then + print_log warning "Over load limit on remote machine. Going for sleep for 5 minutes. (run #$runs, load still $load)" + sleep 300 + else + if [ $load -ge $opt_limit ]; then + opt_send="no" + opt_keep='' + print_log warning "Over load limit on remote machine. Will not send to remote. (run #$runs, load still $load)" + fi + condition='0' + fi + runs=$(( $runs + 1 )) + done + fi +fi + +if [ "$opt_send" != "no" ]; then + MOUNTED_LIST_REM=$(eval do_getmountedfs "remote") + + ZFS_REMOTE_LIST=$(eval "$opt_sendtocmd" zfs list -H -t filesystem,volume -s name -o name) \ + || { print_log error "$opt_sendtocmd zfs list $?: $ZFS_REMOTE_LIST"; exit 139; } + + if [ "$opt_create" -eq '1' ]; then + do_createfs "$TARGETS_DREGULAR" + do_createfs "$TARGETS_DRECURSIVE" + fi + + SNAPSHOTS_OLD_REM=$(eval "$opt_sendtocmd" zfs list -r -H -t snapshot -S creation -o name "$opt_sendprefix") \ + || { print_log error "zfs remote list $?: $SNAPSHOTS_OLD_REM"; exit 140; } +fi + -do_snapshots "$SNAPPROP" "" "$SNAPNAME" "$SNAPGLOB" "$TARGETS_REGULAR" -do_snapshots "$SNAPPROP" "-r" "$SNAPNAME" "$SNAPGLOB" "$TARGETS_RECURSIVE" +do_snapshots "$SNAPPROP" "" "$SNAPNAME" "$SNAPGLOB" "$TARGETS_DREGULAR" +do_snapshots "$SNAPPROP" "$opt_recursive" "$SNAPNAME" "$SNAPGLOB" "$TARGETS_DRECURSIVE" print_log notice "@$SNAPNAME," \ - "$SNAPSHOT_COUNT created," \ - "$DESTRUCTION_COUNT destroyed," \ - "$WARNING_COUNT warnings." + "$SNAPSHOT_COUNT created snapshots," \ + "$SENT_COUNT sent snapshots," \ + "$DESTRUCTION_COUNT destroyed," \ + "$CREATION_COUNT created filesystems," \ + "$WARNING_COUNT warnings." exit 0 # }