#!/bin/bash # Includes . $(miniade) || { echo "${0##*/}: ERROR: miniade failed (hint: run 'miniade' to see error)" >&2; exit 1; } # Configurable stuff # Other globals # /sbin is not in root/crontab path PATH=$PATH:/sbin main() { local MY_ARGS # Defaults for options miniade_get_progname PROGNAME CONF_FILE=/etc/$PROGNAME.conf FORCE=false # Process options special_opts_handler() { case $1 in --config-file=*) CONF_FILE=${1#*=} ;; --true) FORCE=true ;; *) return 1 ;; esac } miniade_process_options --help-handler=help --special-opts-handler=special_opts_handler MY_ARGS "$@" && set -- "${MY_ARGS[@]}" # Process arguments [ $# -ge 1 ] || miniade_bad_usage VERB=$1 shift [[ $VERB =~ ^(list|process|inspect|scrub)$ ]] || miniade_bad_usage # Sanity checks and derivations [ -r $CONF_FILE ] || miniade_error "$CONF_FILE: not accessible" # Guts # (delegate) $VERB "$@" } help() { local PROGNAME miniade_get_progname PROGNAME echo "Usage: $PROGNAME [ ] { list [ ] | process [ ] | inspect | scrub }" exit 0 } list() { local DATASET [ "X$2" = X ] || miniade_bad_usage DATASET=$1 zfs list -ro type,name,creation,mountpoint $DATASET } process() { local PROGNAME miniade_get_progname PROGNAME CMDLINE_DATASETS="$@" [ "X$CMDLINE_DATASETS" != X ] || CMDLINE_DATASETS=$(while IFS= read -r XXX; do eval "set -- $XXX"; echo "$2"; done < /etc/snapshot.conf | sort -u) # Because the snapshotting can take a little time, an a applicability test for a later entry in # the config file might fail when, had it been closer to the top of the file, it might have # passed. Therefore, before doing any snapshotting, we save the applicability tests' results. LINENUM=0 ALLAPPLICABILITYTESTSFAILED=true while IFS= read -r LINE; do LINENUM=$((LINENUM+1)) eval "set $LINE" APPLICABILITYTEST=$1 CONFFILE_DATASET=$2 PREFIX=$3 MAX=$4 miniade_internal "process: the next line has not updated to support bash 4.3's eval idiosyncracies; code should be updated" { eval "$APPLICABILITYTEST" && { eval "APPLICABLE_$LINENUM=true"; ALLAPPLICABILITYTESTSFAILED=false; }; } || eval "APPLICABLE_$LINENUM=false" #eval "miniade_debug 10 \"process: LINENUM=$LINENUM, APPLICABLE_$LINENUM=\$APPLICABLE_$LINENUM\"" done < $CONF_FILE # Minor optimisation: if all the applicability tests failed then there is no point in even considering # which datasets need to be snapshotted. ! $ALLAPPLICABILITYTESTSFAILED || { miniade_debug 10 "process: no matches at all found so returning early ..."; return 0; } # Don't do this if a scrub is in progress; scrubbing will restart after snapshotting and # therefore scrubbing never finishes. miniade_lock /tmp/$PROGNAME.scrub-snapshot-contention-lock || { miniade_warning "scrub (or improbably snapshot) in progress; snapshotting skipped" return 0 } # Loop over the config file snapshotting all the stuff for CMDLINE_DATASET in $CMDLINE_DATASETS; do miniade_debug 10 "process: want to do dataset $CMDLINE_DATASET" LINENUM=0 while IFS= read -r LINE; do LINENUM=$((LINENUM+1)) eval "set $LINE" APPLICABILITYTEST=$1 eval "APPLICABLE=\$APPLICABLE_$LINENUM" CONFFILE_DATASET=$2 PREFIX=$3 MAX=$4 WIDTH=`echo -n $MAX | wc -c` #debug 10 "process: read configuration for dataset $CONFFILE_DATASET" # Ignore the conf file entry if it does not match the specified dataset. [ $CMDLINE_DATASET = $CONFFILE_DATASET ] || { miniade_debug 10 "process: $CONF_FILE:$LINENUM: skipping (doesn't match currently considered dataset) ..."; continue; } # Ignore the conf file entry if it's applicability test failed. $FORCE || $APPLICABLE || { miniade_debug 10 "process: $CONF_FILE:$LINENUM: skipping (its applicability test failed) ..."; continue; } # If we get this far then we want to do three things: (1) delete the # oldest snapshot, (2) rotate the others, (3) create a new snapshot. for I in `seq $MAX -1 0`; do FMTIP0=`printf "%0${WIDTH}d" $I` FMTIP1=`printf "%0${WIDTH}d" $((I+1))` FMTIM1=`printf "%0${WIDTH}d" $((I-1))` miniade_debug 10 "process: FMTIP0=$FMTIP0, FMTIP1=$FMTIP1, FMTIM1=$FMTIM1" # Last loop: create the new snapshot. if [ $I = 0 ]; then miniade_debug 10 "process: determining mountpoint to see if possible to touch .$PROGNAME-creation-time within it ..." MNTPNT=`zfs list -Ht filesystem -ro mountpoint $CONFFILE_DATASET` miniade_debug 10 "process: MNTPNT=$MNTPNT" if [ "X$MNTPNT" != X ]; then miniade_debug 10 "process: creating .$PROGNAME-creation-time inside $MNTMNT ..." date > $MNTPNT/.$PROGNAME-creation-time fi miniade_debug 10 "process: running \"zfs snapshot $CONFFILE_DATASET@$PREFIX.$FMTIP0\" ..." zfs snapshot $CONFFILE_DATASET@$PREFIX.$FMTIP0 if [ "X$MNTPNT" != X ]; then miniade_debug 10 "process: deleting .$PROGNAME-creation-time inside $MNTMNT (so it remains only in snapshot) ..." rm -f $MNTPNT/.$PROGNAME-creation-time fi # Don't destroy/rotate non-existent snapshots. elif [ "X`zfs list $CONFFILE_DATASET@$PREFIX.$FMTIM1 2>&1`" = "Xcannot open '$CONFFILE_DATASET@$PREFIX.$FMTIM1': dataset does not exist" ]; then miniade_debug 10 "process: snapshot $CONFFILE_DATASET@$PREFIX.$FMTIM1 does not exist; so skipping renaming/deletion ..." # First loop: destroy oldest snapshot elif [ $I = $MAX ]; then miniade_debug 10 "process: running \"zfs destroy $CONFFILE_DATASET@$PREFIX.$FMTIM1\" ..." zfs destroy $CONFFILE_DATASET@$PREFIX.$FMTIM1 # Middle loops: rotate snapshots else miniade_debug 10 "process: running \"zfs rename $CONFFILE_DATASET@$PREFIX.$FMTIM1 $CONFFILE_DATASET@$PREFIX.$FMTIP0\" ..." zfs rename $CONFFILE_DATASET@$PREFIX.$FMTIM1 $CONFFILE_DATASET@$PREFIX.$FMTIP0 fi done done < $CONF_FILE done miniade_unlock /tmp/$PROGNAME.scrub-snapshot-contention-lock } inspect() { local SNAPSHOT R [ "X$1" != X -a "X$2" = X ] || miniade_bad_usage SNAPSHOT=$1 # Don't do this if a scrub is in progress; scrubbing will restart after snapshotting and # therefore scrubbing never finishes. miniade_lock /tmp/$PROGNAME.scrub-snapshot-contention-lock || error "scrub (or improbably snapshot) in progress; snapshotting skipped" miniade_info "$SNAPSHOT: cloning ..." R=$RANDOM zfs clone $SNAPSHOT ${SNAPSHOT/@/_}.$R zfs set mountpoint=/${SNAPSHOT/@/_}.$R ${SNAPSHOT/@/_}.$R # Start a shell in the clone ( cd /${SNAPSHOT/@/_}.$R && HOME=/${SNAPSHOT/@/_}.$R PS1="${SNAPSHOT/@/_}.$R> " bash --norc; sync; sleep 5; ) zfs destroy ${SNAPSHOT/@/_}.$R rmdir /${SNAPSHOT/@/_}.$R miniade_unlock /tmp/$PROGNAME.scrub-snapshot-contention-lock } scrub() { local POOL COMPLETED [ "X$1" != X -a "X$2" = X ] || miniade_bad_usage POOL=$1 # Don't do this if a snapshot is in progress; scrubbing will restart after snapshotting and # therefore scrubbing never finishes. miniade_lock /tmp/$PROGNAME.scrub-snapshot-contention-lock || error "scrub (or improbably snapshot) in progress; scrubbing skipped" miniade_info "scrub started (use 'zpool status' to monitor) ..." zpool scrub $POOL # 'zpool scrub' returns immediately so here we wait for the scrub to finish while sleep 60; do COMPLETED=$(zpool status $POOL | sed -n 's/.*scrub in progress, \(.*\)% done.*/\1/p') if [ "X$COMPLETED" = X ]; then break else miniade_debug 10 "scrub: $COMPLETED% completed; waiting ..." fi done miniade_unlock /tmp/$PROGNAME.scrub-snapshot-contention-lock } main "$@"