From: Carl Baldwin Date: Tue, 16 Feb 2016 19:51:49 +0000 (-0700) Subject: Merge all the work Alan's done for the past 8 years X-Git-Url: http://git.pippins.net/embedvideo/.git/static/git-logo.png?a=commitdiff_plain;h=refs%2Fremotes%2Fgerrit%2Fmaster;hp=6157f3e40dc98af4fbcd7641eefbe7d807cffc99;p=zfs-ubuntu%2F.git Merge all the work Alan's done for the past 8 years Conflicts: zfs-replicate Change-Id: I79a54bccf63ee4383045240c6b72b8bff6313f0d --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60fa576 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +zfs-scripts.conf diff --git a/snap.sh b/snap.sh deleted file mode 100755 index 077e6f7..0000000 --- a/snap.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash - -exec >> /var/log/snap.log 2>&1 - -# This script makes the following assumptions/requirements: -# * this script only handles one zfs filesystem, a wrapper should be created -# to handle more -# * this script handles all snapshots that are named in this format: -# YYYY-MM-DD.hh.mm -# * It ignores other snapshots that don't follow this naming convention - -# This converts the YYYY-MM-DD.hh.mm format to an integer. -datetime_to_minutes() { - perl -n -e '/(\d+)-(\d+)-(\d+)\.(\d+)\.(\d+)/; print $1 * 527040 + $2 * 44640 + $3 * 1440 + $4 * 60 + $5,"\n"' -} - -datetime_to_minutes2() { - perl -n -e '/(\d+)-(\d+)-(\d+)\.(\d+)\.(\d+)/; $monthadj=int(($2-1)/3)-1; $minadj=int($5/15); $houradj=int($4/3); print $1 * 1048576 + ( $2 + $monthadj ) * 65536 + $3 * 2048 + ( $4 + $houradj ) * 64 + $5 + $minadj,"\n"' -} - -# filesystem: This is the zfs filesystem to snapshot -# mountpoint: This is the mountpoint of the zfs filesystem to snapshot -# numsnapshots: This number is the number of equally spaced snapshots that should exist over any given period in the past -# maxagedays: This is the maximum number of days to keep any snapshot around for (0=infinite) (default=0). -filesystem=$1 -mountpoint=$2 -numsnapshots=${3-12} -maxagedays=${4-0} -lockdir="/tmp/${filesystem}.lock" -pool=`echo "$filesystem" | awk -F '/' '{ print $1 }'` - -if [ -z "$filesystem" ] || [ -z "$mountpoint" ] || [ -z "$numsnapshots" ] || [ -z "$maxagedays" ]; then - echo "-E- Usage: $0 " - exit 1 -fi - -if [ ! -d "$mountpoint" ]; then - echo "-E- Unable to find the mountpoint: $mountpoint" - exit 1 -fi - -snapshotdir="${mountpoint}/.zfs/snapshot" -if [ ! -d "$snapshotdir" ]; then - echo "-E- Unable to find the snapshotdir: $snapshotdir" - exit 1 -fi - -# Check to see if this zfs filesystem has a scrub being performed on it now. -# If it does, we cannot perform any snapshot create or destroy operations. -zpool status $pool | grep scrub: | grep "in progress" > /dev/null 2>&1 -if [ $? == 0 ]; then - echo "-W- The zfs pool '$pool' is currently being scrubbed. Skipping all snapshot operations." - exit 0 -fi - -# Get the various components of the date -datetime=${ZFSDATETIME:-$(date +%Y-%m-%d.%H.%M)} - -# Create the snapshot for this minute -echo "-I- Creating ${filesystem}@${datetime}" -zfs snapshot "${filesystem}@${datetime}" - -minutes=$(echo $datetime | datetime_to_minutes) - -lockdir="/tmp/zfs-admin-lock" -if ! mkdir "$lockdir" >/dev/null 2>&1; then - exit 0 -fi -cleanup() { rm -rf "$lockdir"; } -trap cleanup EXIT - -# Trim them down -snapshots=$(ls -d ${snapshotdir}/????-??-??.??.?? 2>/dev/null) -for snapshot in $snapshots; do - snapminutes=$(echo "$snapshot" | sed 's,.*/,,' | datetime_to_minutes) - snapminutes2=$(echo "$snapshot" | sed 's,.*/,,' | datetime_to_minutes2) - age=$((minutes - snapminutes)) - window=1 - while true; do - if [ $age -lt $((window * numsnapshots)) ]; then - case $((snapminutes2 % window)) in - 0) ;; - *) - snapname=$(echo "$snapshot" | - sed 's,/\(.*\)/.zfs/snapshot/\(.*\),\1@\2,') - echo "-I- Destroying $snapname" - zfs destroy "$snapname" - ;; - esac - break - fi - window=$((window*2)) - done - if [ $maxagedays -gt 0 ] && [ $age -gt $((maxagedays * 24 * 60)) ]; then - snapname=$(echo "$snapshot" | - sed 's,/\(.*\)/.zfs/snapshot/\(.*\),\1@\2,') - echo "-I- Destroying old $snapname" - zfs destroy "$snapname" - fi -done diff --git a/update_links b/update_links new file mode 100755 index 0000000..9a64660 --- /dev/null +++ b/update_links @@ -0,0 +1,6 @@ +#!/bin/bash + +LINKTO=/etc/bin + +git ls-files | grep -v .gitignore | grep -v wrapper | xargs -I % ln -s zfs/% $LINKTO/% 2>/dev/null + diff --git a/zfs-autosnap b/zfs-autosnap new file mode 100755 index 0000000..09e1a62 --- /dev/null +++ b/zfs-autosnap @@ -0,0 +1,149 @@ +#!/bin/bash + +# Author: Carl Baldwin & Alan Pippin +# Description: This script takes a snapshot of the given zfs filesystem. +# It also employs an intelligent algorithm to roll off, +# or destroy, old snapshots. + +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} + +if [[ -z "$SNAP_UNDER_TEST" ]]; then + exec >> $logdir/zfs-autosnap.log 2>&1 +fi + +# This script makes the following assumptions/requirements: +# * this script only handles one zfs filesystem, a wrapper should be created +# to handle more +# * this script handles all snapshots that are named in this format: +# YYYY-MM-DD.hh.mm +# * It ignores other snapshots that don't follow this naming convention + +# This converts the YYYY-MM-DD.hh.mm format to an integer. +datetime_to_minutes() { + perl -n -e '/(\d+)-(\d+)-(\d+)\.(\d+)\.(\d+)/; print $1 * 527040 + $2 * 44640 + $3 * 1440 + $4 * 60 + $5,"\n"' +} + +# This converts date/time from YYYY-MM-DD.hh.mm to an integer that aligns +# things to prefer certain times such as the first of the month or midnight, +# etc +datetime_to_minutes2() { + perl -n -e '/(\d+)-(\d+)-(\d+)\.(\d+)\.(\d+)/; + $monthadj=int(($2-1)/3)-1; # Prefer months numbered 1,4,7 and 10 + $dayadj=$3 == 1 ? -1 : 0; # Make sure day 1 is prefered + $minadj=int($5/15); # Prefer multiples of 15 minutes + $houradj=int($4/3); # Prefer midnight,noon,etc + $intvalue=( + 1048576 * $1 + + 65536 * ( $2 + $monthadj ) + + 2048 * ( $3 + $dayadj ) + + 64 * ( $4 + $houradj ) + + $5 + $minadj + ); + print $intvalue,"\n"' +} + +# filesystem: This is the zfs filesystem to snapshot +# mountpoint: This is the mountpoint of the zfs filesystem to snapshot +# numsnapshots: This number is the number of equally spaced snapshots that should exist over any given period in the past +# maxagedays: This is the maximum number of days to keep any snapshot around for (0=infinite) (default=0). +filesystem=$1 +mountpoint=${2-/$1} +numsnapshots=${3-12} +maxagedays=${4-0} +pool=`echo "$filesystem" | awk -F '/' '{ print $1 }'` + +if [ -z "$filesystem" ] || [ -z "$mountpoint" ] || [ -z "$numsnapshots" ] || [ -z "$maxagedays" ]; then + echo "-E- Usage: $0 " + exit 1 +fi + +if [ -z "$SNAP_UNDER_TEST" -a ! -d "$mountpoint" ]; then + echo "-E- Unable to find the mountpoint: $mountpoint" + exit 1 +fi + +if [ -n "$SNAP_UNDER_TEST" ]; then + snapshotdir="./snapshot" +else + snapshotdir="${mountpoint}/.zfs/snapshot" +fi + +if [ ! -d "$snapshotdir" ]; then + echo "-E- Unable to find the snapshotdir: $snapshotdir" + exit 1 +fi + +# Check to see if this zfs filesystem has a scrub being performed on it now. +# If it does, we cannot perform any snapshot create or destroy operations. +if [ -z "$SNAP_UNDER_TEST" ]; then + zpool status $pool | grep scan: | grep "in progress" > /dev/null 2>&1 + if [ $? == 0 ]; then + echo "-W- The zfs pool '$pool' is currently being scrubbed. Skipping all snapshot operations." + exit 0 + fi +fi + +snapshot() { + echo "-I- Creating $1" + if [ -z "$SNAP_UNDER_TEST" ]; then + zfs snapshot "$1" + else + mkdir -p snapshot/$(dirname "$(echo "$1" | sed 's,.*@,,')") + touch snapshot/"$(echo "$1" | sed 's,.*@,,')" + fi +} + +destroy() { + echo "-I- Destroying old $1" + if [ -z "$SNAP_UNDER_TEST" ]; then + zfs destroy "$1" + else + rm -f "$1" + fi +} + +# Get the various components of the date +datetime=${ZFSDATETIME:-$(date +%Y-%m-%d.%H.%M)} + +# Create the snapshot for this minute +snapshot "${filesystem}@${datetime}" + +minutes=$(echo $datetime | datetime_to_minutes) + +if ! mkdir "$lockdir" >/dev/null 2>&1; then + echo "-W- The zfs filesystem has been locked down. Skipping snapshot cleanup." + exit 0 +fi +cleanup() { rm -rf "$lockdir"; } +trap cleanup EXIT + +# Trim them down +snapshots=$(ls -d ${snapshotdir}/????-??-??.??.?? 2>/dev/null) +for snapshot in $snapshots; do + snapminutes=$(echo "$snapshot" | sed 's,.*/,,' | datetime_to_minutes) + snapminutes2=$(echo "$snapshot" | sed 's,.*/,,' | datetime_to_minutes2) + age=$((minutes - snapminutes)) + window=1 + while true; do + if [ $age -lt $((window * numsnapshots)) ]; then + case $((snapminutes2 % window)) in + 0) ;; + *) + snapname=${filesystem}$(echo "$snapshot" | + sed 's,/\(.*\)/.zfs/snapshot/\(.*\),@\2,') + destroy "$snapname" + ;; + esac + break + fi + window=$((window*2)) + done + if [ $maxagedays -gt 0 ] && [ $age -gt $((maxagedays * 24 * 60)) ]; then + snapname=${filesystem}$(echo "$snapshot" | + sed 's,/\(.*\)/.zfs/snapshot/\(.*\),@\2,') + destroy "$snapname" + fi +done diff --git a/zfs-autosnap-wrapper b/zfs-autosnap-wrapper new file mode 100755 index 0000000..fc17c08 --- /dev/null +++ b/zfs-autosnap-wrapper @@ -0,0 +1,51 @@ +#!/bin/bash + +# Author: Alan J. Pippin +# Description: This script is a wrapper script that calls zfs-autosnap +# for each filesystem provided below. + +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} + +# Setup some default values +logfile="$logdir/zfs-autosnap.log" +numsnapshots=20 +maxagedays=365 +date=`date` +mylockdir="/tmp/zfs-autosnap-all" +current_hour=`date +"%H"` +current_minute=`date +"%M"` +current_day=`date +"%u"` + +# Make sure we aren't already running +if ! mkdir "$mylockdir" >/dev/null 2>&1; then + echo "$date Another $0 process is already running" >> $logfile + exit 1 +fi + +# Auto snapshot every zfs filesystem on the system specified below +date >> $logfile + +# Special filesystems +# ex: zfs-autosnap storage /storage $numsnapshots 15 +# ex: zfs-autosnap tank/usr/videos /usr/videos $numsnapshots 15 + +# Normal filesystems +# ex: zfs-autosnap tank / $numsnapshots $maxagedays +# ex: zfs-autosnap tank/home /home $numsnapshots $maxagedays + +# Daily filesystems (only perform these at midnight) +# midnight = true if (midnight < current time < midnight+5 min) +if [[ $current_hour == "00" && $current_minute -lt 5 ]]; then + echo "Performing Daily snapshots" >> $logfile +fi + +# Weekly filesystems (only perform these on Sunday at midnight) +# midnight = true if (midnight < current time < midnight+5 min) +if [[ $current_day == "7" && $current_hour == "00" && $current_minute -lt 5 ]]; then + echo "Performing Weekly snapshots" >> $logfile +fi + +rm -rf "$mylockdir" diff --git a/zfs-diff b/zfs-diff new file mode 100755 index 0000000..fe857be --- /dev/null +++ b/zfs-diff @@ -0,0 +1,167 @@ +#!/usr/bin/perl + +use warnings; +use strict; +use File::Temp qw/ tempfile tempdir /; + +my $SSLPATH = '/usr/bin/openssl'; + +#Usage +sub print_usage { + print "ZFS Snapshot diff\n"; + print "\t$0 [-dhirv] [filename]\n\n"; + print " -d Display the lines that are different (diff output)\n"; + print " -h Display this usage\n"; + print " -m Use md5sum when checking for file differences"; + print " -i Ignore files that don't exist in the snapshot (only necessary for recursing)\n"; + print " -r Recursively diff every file in the snapshot (filename not required)\n"; + print " -v Verbose mode\n\n"; + + print " [filename] is the filename RELATIVE to the ZFS snapshot root. For example, if\n"; + print " I had a filesystem snapshot called pool/data/zone\@initial. The filename '/etc/passwd'\n"; + print " would refer to the filename /pool/data/zone/etc/passwd in the filesystem and filename\n"; + print " /pool/data/zone/.zfs/snapshot/initial/etc/passwd in the snapshot.\n\n"; + + print " A couple of examples:\n"; + print "\t$0 -v -r -i pool/zones/lava2019\@Fri\n"; + print "\t\tChecks the current pool/zones/lava2019 filesystem against the snapshot\n"; + print "\t\treturning the md5sum difference of any files (ignore files that don't\n"; + print "\t\texist in the snapshot). With verbose mode\n\n"; + + print "\t$0 -d pool/zones/lava2019\@Mon /root/etc/passwd\n"; + print "\t\tCheck the md5sum for /pool/zones/lava2019/root/etc/passwd and compare\n"; + print "\t\tit to /pool/zones/lava2019/.zfs/snapshot/Mon/root/etc/passwd. Display\n"; + print "\t\tthe lines that are different also.\n\n"; + + exit(0); +} + +use Getopt::Long; +my %options = (); +my $verbose; + +GetOptions("h" => \$options{help}, + "r!" => \$options{recurse}, + "d!" => \$options{diff}, + "i!" => \$options{ignore}, + "m!" => \$options{md5sum}, + "v!" => \$verbose + ); + +if ($options{help}) { + print_usage(); +} + +if ($options{recurse}) { + recurse_diff(shift || die "Need a ZFS snapshot name\n"); +} else { + my $zfsname = shift || die "Need a ZFS snapshot name\n"; + my $file = shift || die "Need a filename\n"; + diff_single_file($zfsname,$file); +} + +exit(0); + +sub recurse_diff { + my $zfssnap = shift; + print "Recursive diff on $zfssnap\n" if $verbose; + + $zfssnap =~ /(.+)\@(.+)/i; + if(($1 eq "") || ($2 eq "")) { die "-E- Invalid snapshot name\n"; } + my $fsname = "/" . $1; + my $snapname = $2; + if(! -d $fsname && $fsname =~ /\/\S+?(\/.*)/) { $fsname = $1; } + elsif(! -d $fsname && $fsname =~ /\/\S+?/) { $fsname = "/"; } + print "Filesystem: $fsname, Snapshot: $snapname\n" if $verbose; + + my $snappath = $fsname . "/.zfs/snapshot/" . $snapname . "/"; + my $fspath = $fsname . "/"; + $fspath =~ s/\/\//\//gi; + $snappath =~ s/\/\//\//gi; + print "Comparing: $fspath\nto: $snappath\n" if $verbose; + + my $dir = tempdir( CLEANUP => 0 ); + my ($fh, $filename) = tempfile( DIR => $dir ); + + print "-> Finding files in $fspath to compare against files in $snappath\n"; + `find $fspath -name "*" -type f > $filename`; + print "-> Performing a diff operation on each file found\n"; + + my $num_files = `cat $filename | wc | awk '{print \$1}'`; + + foreach my $file (<$fh>) { + chomp($file); + $file =~ /(.*)\/(.*)/; + my $shortname = $2; + $file =~ /$fspath(.*)/; + my $diff = $snappath . $1; + if (!-e $diff) { + print "$file does not exist in snapshot\n" if !$options{ignore}; + next; + } + + if($options{md5sum}) { + # do the md5 sums + my $orig = `$SSLPATH md5 $file`; + my $snap = `$SSLPATH md5 $diff`; + $orig =~ /[\s\S]+= (.+)/; + my $sum1 = $1; + $snap =~ /[\s\S]+= (.+)/; + my $sum2 = $1; + if ($sum1 ne $sum2) { + print "** $file is different\n"; + print "** $orig** $snap" if $verbose; + } + } else { + my $differ = system("diff \"$file\" \"$diff\" > /dev/null 2>&1"); + if($differ) { print "** $file is different\n"; } + } + if ($options{diff}) { + system("diff \"$file\" \"$diff\""); + } + } +} + +sub diff_single_file { + my $zfssnap = shift; + my $filename = shift; + print "Single-file diff on $zfssnap, file: $filename\n" if $verbose; + + $zfssnap =~ /(.+)\@(.+)/i; + my $fsname = "/" . $1 . "/"; + my $snapname = $2; + if(! -d $fsname && $fsname =~ /\/\S+?(\/.*)/) { $fsname = $1; } + print "Filesystem: $fsname, Snapshot: $snapname\n" if $verbose; + + my $fspath; + if($filename !~ /^\//) { $fspath = $ENV{'PWD'} . "/" . $filename; } + else { $fspath = $filename; } + + my $snapfspath = $fspath; + $snapfspath =~ s/$fsname//g; + my $snappath = $fsname . "/.zfs/snapshot/" . $snapname . "/" . $snapfspath; + $fspath =~ s/\/\//\//gi; + $snappath =~ s/\/\//\//gi; + print "Comparing: $fspath\nto: $snappath\n" if $verbose; + if(! -f $fspath) { print "-E- Cannot find source file: $fspath\n"; exit 1; } + if(! -f $snappath) { print "-E- Cannot find source file: $snappath\n"; exit 1; } + + if($options{md5sum}) { + my $orig = `$SSLPATH md5 $fspath`; + my $snap = `$SSLPATH md5 $snappath`; + $orig =~ /[\s\S]+= (.+)/; + my $sum1 = $1; + $snap =~ /[\s\S]+= (.+)/; + my $sum2 = $1; + if ($sum1 ne $sum2) { + print "** Files are different\n"; + print "** $orig** $snap" if $verbose; + } + } else { + my $differ = system("diff \"$fspath\" \"$snappath\" > /dev/null 2>&1"); + if($differ) { print "** Files are different\n"; } + } + if ($options{diff}) { + system("diff \"$fspath\" \"$snappath\""); + } +} diff --git a/zfs-log-parser b/zfs-log-parser new file mode 100755 index 0000000..f13c2a0 --- /dev/null +++ b/zfs-log-parser @@ -0,0 +1,171 @@ +#!/usr/bin/perl + +# Author: Alan J. Pippin +# Description: This script parses logfiles generated by the following zfs scripts: +# zfs-replicate + +$logfile=shift(@ARGV); +$startdate=shift(@ARGV); +$backup_pool="backups"; + +sub usage { + print "Usage: $0 \n"; + exit 0; +} +if(! -f "$logfile") { &usage; } + +$kilo = 1024; +$mega = 1024 * 1024; +$giga = 1024 * 1024 * 1024; + +%month2num = ( "Jan" => "01", "Feb" => "02", "Mar" => "03", "Apr" => "04", "May" => "05", "Jun" => "06", + "Jul" => "07", "Aug" => "08", "Sep" => "09", "Oct" => "10", "Nov" => "11", "Dec" => "12"); + +sub time_to_seconds { + my ($hour,$minute,$sec) = @_; + $seconds = ($hour * 60 * 60) + ($minute * 60) + ($sec); + return($seconds); +} + +sub adjust_duration { + my ($duration) = @_; + if($duration > 3600) { $duration=int($duration/3600); $duration.="h"; } + elsif($duration > 60) { $duration=int($duration/60); $duration.="m"; } + else { $duration.="s"; } + return $duration; +} + +sub adjust_data { + my ($data) = @_; + if(abs($data) > ($giga)) { $data = $data / $giga; $data = sprintf("%2.2fGb",$data); } + elsif(abs($data) > ($mega)) { $data = int($data / $mega); $data = "$data"."Mb"; } + elsif(abs($data) > ($kilo)) { $data = int($data / $kilo); $data = "$data"."Kb"; } + return $data; +} + +sub parse_replicate_logfile { + $in_replicate=0; + $date=""; + %totals=(); + while() { + $line = $_; + if(($in_replicate == 0) && ("$startdate" ne "") && ($line !~ /$startdate/)) { next; } + if($line =~ /(\S+)\s+(\S+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\S+)\s+(\S+)/) { + $dayname=$1; $month=$2; $daynum=$3; $hour=$4; $minute=$5; $sec=$6; $year=$8; + if(($in_replicate == 0) && ($line =~ /replicate started/)) { + $in_replicate = 1; + $date="$dayname $month $daynum $hour:$minute:$sec $year"; + $totals{$date}{data} = 0; + $totals{$date}{transfertime} = 0; + $totals{$date}{duration} = time_to_seconds($hour,$minute,$sec); + } + elsif(($in_replicate == 1) && ($line=~ /replicate complete/)) { + $in_replicate = 0; + $totals{$date}{duration} = time_to_seconds($hour,$minute,$sec) - $totals{$date}{duration}; + } + } + if(($in_replicate == 1) && ($line =~ /received ([\d\.]+)(\w+)/)) { + $data = $1; $size = $2; + if($size =~ /Kb/i) { $data = $data * $kilo; } + if($size =~ /Mb/i) { $data = $data * $mega; } + if($size =~ /Gb/i) { $data = $data * $giga; } + chomp($line); + $totals{$date}{data} += $data; + } + if(($in_replicate == 1) && ($line =~ /in (\d+) seconds/)) { + $transfertime = $1; + $totals{$date}{transfertime} += $transfertime; + } + } + + foreach $date (keys %totals) { + $duration=adjust_duration($totals{$date}{duration}); + $data=adjust_data($totals{$date}{data}); + $transfertime=adjust_duration($totals{$date}{transfertime}); + if($totals{$date}{transfertime} > 0) { + $rate = adjust_data(int($totals{$date}{data}/$totals{$date}{transfertime})); + } else { + $rate = 0; + } + print "$date: data=${data} transfertime=$transfertime rate=${rate}/sec duration=$duration\n"; + } +} + +sub parse_snapshot_totals_logfile { + %totals=(); + $in_totals=0; + $maxlen=0; + $found_startdate=0; + $header=""; + while() { + $line = $_; + if($line =~ /logfile turned over/) { next; } + if(($in_totals == 0) && ("$startdate" ne "") && ($line !~ /$startdate/) && ($found_startdate==0)) { next; } + if(($in_totals == 0) && ("$startdate" ne "") && ($line =~ /$startdate/) && ($found_startdate==0)) { $found_startdate=1; } + if(($in_totals == 0) && ($line =~ /(\S+)\s+(\S+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\S+)\s+(\S+)/)) { + $dayname=$1; $month=$2; $daynum=$3; $hour=$4; $minute=$5; $sec=$6; $year=$8; + $in_totals = 1; + $month = $month2num{$month}; + if($daynum < 10) { $daynum = "0".$daynum; } + $date="$month-$daynum-$year"; + if(!defined $founddates{$date}) { $header .= $date . " "; } + $founddates{$date} = 1; + } + elsif(($in_totals == 1) && ($line =~ /^\s+$/)) { + $in_totals = 0; + } + elsif(($in_totals == 1) && ($line =~ /(\S+)\s+([\d\.]*)(\w+)\s+(\d+)/)) { + $filesystem = $1; $data = $2; $size = $3; $num_snaps = $4; + if($filesystem =~ /Snapshots/ || $filesystem =~ /Total/) { next; } + if($filesystem =~ /^$backup_pool/) { next; } + if(length($filesystem) > $maxlen) { $maxlen = length($filesystem); } + if($size =~ /K/i) { $data = $data * $kilo; } + if($size =~ /M/i) { $data = $data * $mega; } + if($size =~ /G/i) { $data = $data * $giga; } + chomp($line); + $totals{$filesystem}{$date}{data} = $data; + if($totals{$filesystem}{start_data} == 0) { $totals{$filesystem}{start_data} = $data; } + $totals{$filesystem}{delta} = $data - $totals{$filesystem}{start_data}; + } + } + $total_delta=0; + printf("%-${maxlen}s %s %s\n","ZFS Filesystem","$header","Net Change"); + foreach $filesystem (sort keys %totals) { + $hashref = $totals{$filesystem}; + $data=""; + foreach $date (sort keys %$hashref) { + if($date !~ /(\d+)-(\d+)-(\d+)/) { next; } + $date_data=adjust_data($totals{$filesystem}{$date}{data}); + if($date_data eq "") { $date_data = "0"; } + $data .= sprintf("%10s",$date_data). " "; + $date_totals{$date}{data} += $totals{$filesystem}{$date}{data}; + } + $data_total=adjust_data($data_total); + $total_delta+=$totals{$filesystem}{delta}; + $delta=adjust_data($totals{$filesystem}{delta}); + printf("%-${maxlen}s %s %10s\n",$filesystem,$data,$delta); + } + $data=""; + $data_len=length($data); + $total_delta=adjust_data($total_delta); + foreach $date (sort keys %date_totals) { + $date_data=adjust_data($date_totals{$date}{data}); + $data .= sprintf("%10s",$date_data). " "; + } + printf("%-${maxlen}s %-${data_len}s %10s\n","Totals:",$data,$total_delta); +} + +######### +# MAIN +######### +#print "-> Parsing $logfile\n"; +if("$logfile" =~ /\.[bz2|gz]/) { + open(FILE,"zcat $logfile|") || die "-E- Unable to open $logfile\n"; +} else { + open(FILE,"$logfile") || die "-E- Unable to open $logfile\n"; +} + +if($logfile =~ /replicate/) { parse_replicate_logfile(); } +if($logfile =~ /snapshot-totals/) { parse_snapshot_totals_logfile(); } + +close(FILE); diff --git a/zfs-replicate b/zfs-replicate index 20c480f..9f72b06 100755 --- a/zfs-replicate +++ b/zfs-replicate @@ -1,104 +1,265 @@ #!/bin/bash -: Usage: zfs-backup [filesystem] [destination_pool] +# Author: Carl Baldwin & Alan Pippin +# Description: This script replicates a remote zfs filesystem to a local zfs pool. +# This script will keep all snapshots in sync, removing the ones +# that have been deleted since the last replicate was performed. +# This script will only send the new, or missing, snapshots since +# the last replicate was performed. +# Usage: replicate -tmpdir="/export/home/tmp" +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} -lockdir="/tmp/zfs-admin-lock" -if ! mkdir "$lockdir" >/dev/null 2>&1; then - echo >&2 "ZFS admin lock directory is present." - exit 1 -fi +# command line arg parsing +remote=$1 +remote_fs=$2 +remote_pool=${2%%/*} -cleanup() { rm -rf "$lockdir"; } +# Setup our cleanup and exit trap +cleanup() { + if [[ -e "$local_list" ]]; then + rm -f $local_list + fi + if [[ -e "$remote_list" ]]; then + rm -f $remote_list + fi + $ssh $remote ls -d "$lockdir" > /dev/null 2>&1 + if [[ $? == 0 ]]; then + $ssh $remote rm -rf "$lockdir" + fi +} +fatal_and_exit() { + echo -e 2>&1 "$1" + # Destroy the current backup markers from the local backup_pool and remote_pool if they exist + if [[ -n "$current_backup_marker" ]]; then + # Local backup pool current backup marker + $zfs list -t snapshot ${backup_pool}/${current_backup_marker} > /dev/null 2>&1 + if [ $? == 0 ]; then + $zfs destroy ${backup_pool}/${current_backup_marker} + fi + # Remote pool current backup marker + $ssh $remote zfs list -t snapshot ${current_backup_marker} > /dev/null 2>&1 + if [ $? == 0 ]; then + $ssh $remote $zfs destroy ${current_backup_marker} + fi + fi + # send email notification + if [[ -n "$2" ]]; then + echo -e "$1" | $mailx -s "zfs replicate on $hostname failed" "$2" + fi + # exit + exit 1 +} +trap fatal_and_exit INT trap cleanup EXIT -fs=$1 -sourcepool=${1%%/*} -fsname=${1#*/} -destinationpool=$2 +# Declare a function to handle the replicate operation +replicate() { + zfs_send="$1" + zfs_recv="$zfs receive -vF -d ${backup_pool}/${remote_pool}" + glue="$throttle $throttle_opt" + if [[ $throttle_enable == 1 && -e $throttle ]]; then + # handle using the glue in the local and remote host case properly + if [[ -z "$ssh" ]]; then + # local host glue case + $zfs_send | $glue | $zfs_recv + else + # remote host glue case + $ssh $remote "$zfs_send | $glue" | $zfs_recv + fi + else + # no glue case - works for both the local and remote host cases + $ssh $remote $zfs_send | $zfs_recv + fi + # The return code of the zfs_send | zfs_recv operation will be returned to the caller +} -if ! zpool list -H "$sourcepool" >/dev/null 2>&1; then - echo >&2 "The source pool, '$sourcepool' doesn't seem to exist." - exit 1 +# Make sure we have valid arguments +if [[ -z "$remote" ]] || [[ -z "$remote_fs" ]]; then + fatal_and_exit "Usage: $0 " fi -if ! zpool list -H "$destinationpool" >/dev/null 2>&1; then - echo >&2 "The destination pool, '$destinationpool' doesn't seem to exist." - exit 1 +# check for localhost +if [[ $remote = "localhost" ]]; then + remote="" + ssh="" fi -if ! zfs list -H "$fs" >/dev/null 2>&1; then - echo >&2 "The source filesytem, '$fs' doesn't seem to exist." - exit 1 +# Make sure the local backup pool and local receiving filesystem exist, or print some errors +zpool list -H "$backup_pool" >/dev/null 2>&1 +if [ $? != 0 ]; then + fatal_and_exit "-E- The local backup pool on $hostname, '$backup_pool' doesn't seem to exist." $mailto +fi +zfs list "$backup_pool/$remote_pool" >/dev/null 2>&1 +if [ $? != 0 ]; then + echo >&2 "-I- The local filesystem for the remote pool, '$backup_pool/$remote_pool' doesn't seem to exist." + echo >&2 " Creating the local filesystem to receive the remote pool into: $backup_pool/$remote_pool" + $zfs create $backup_pool/$remote_pool + if [ $? != 0 ]; then + fatal_and_exit "-E- remote $zfs on $hostname create command failed" $mailto + fi fi -printsnaps() { -sed 's,.*/,,' | awk '{print $1}' -} +# Obtain the zpool guid for the local backup pool +backup_pool_guid=`zpool get guid $backup_pool 2>&1 | grep $backup_pool | awk '{ print $3 }'` +zpool get guid $backup_pool > /dev/null 2>&1 +if [ $? != 0 ]; then + fatal_and_exit "-E- Unable to extract the guid for the local backup pool on $hostname: $backup_pool" $mailto +fi -zfs list -rH -t snapshot $sourcepool/$fsname | printsnaps > /tmp/source-list -zfs list -rH -t snapshot $destinationpool/$fsname | printsnaps > /tmp/destination-list -diff -u /tmp/source-list /tmp/destination-list | grep -v '^+++' | awk '/^\+/ {print}' | sed "s,^\+,$destinationpool/," > /tmp/obsolete-snapshots -rm -f /tmp/source-list /tmp/destination-list +# Turn on shell verbosity +set -x -echo >&2 "Removing obsolete backups from the destination pool" -for snapshot in $(cat /tmp/obsolete-snapshots); do - echo >&2 "Removing '$snapshot' from destination." - zfs destroy "$snapshot" +# Create the remote lockdir before continuing with the replicate +# Spinlock on creating the lock +maxsleeptime=60 +maxattempts=500 +attempts=0 +while true; do + $ssh $remote mkdir "$lockdir" >/dev/null 2>&1 + if [ $? != 0 ]; then + # Another zfs admin tool is running. + # Wait a random amount of time and try again + ransleep=$(($RANDOM % $maxsleeptime)) + sleep $ransleep + ((attempts=attempts+1)) + else + # No other zfs admin tool is running, we can now. + break + fi + if [[ $attempts -gt $maxattempts ]]; then + # We've exceeded our maximum while loop count + echo "-E- The zfs filesystem has been locked down. Skipping replicate operation." + fail_msg=`$ssh $remote ls -ld $lockdir 2>&1` + fatal_and_exit "zfs-replicate-all on $hostname unable to obtain zfs admin lock:\n$fail_msg" $mailto + fi done -echo >&2 "Rolling back to the most recent snapshot on the destination." -zfs rollback $(zfs list -rH -t snapshot $destinationpool/$fsname | awk '{snap=$1} END {print snap}') +# Setup our backup marker names +current_backup_marker=${remote_fs}@current-backup-${backup_pool_guid} +previous_backup_marker=${remote_fs}@previous-backup-${backup_pool_guid} -echo >&2 "Calculating the most recent common snapshot between the two filesystems." -common="" -if zfs list -H "$destinationpool/$fsname" >/dev/null 2>&1; then - for snap in $(zfs list -rH -t snapshot "$destinationpool/$fsname" | - sed 's,.*@,,' | awk '{print$1}'); do - if zfs list -rH -t snapshot "$fs" | sed 's,.*@,,' | awk '{print$1}' | grep "^${snap}$" >/dev/null 2>&1; then - common=$snap - fi - done +# List the snapshots on the remote machine. +remote_list=$(mktemp /tmp/replicate.XXXXXX) +$ssh $remote \ + $zfs list -H -t snapshot | + grep ^${remote_fs}@ | + awk '{print$1}' > $remote_list +if [ $? != 0 ]; then + fatal_and_exit "-E- remote $zfs list on $hostname command failed" $mailto fi -base=$common -foundcommon=false -if [ -z "$common" ]; then - foundcommon=true +# List the snapshots on the local machine. +# Don't list the current backup marker if it exists on the local side. +# If you do, it can mess up the common finding algorithm below. +local_list=$(mktemp /tmp/replicate.XXXXXX) +$zfs list -H -t snapshot | + grep ^${backup_pool}/${remote_fs}@ | + grep -v ^${backup_pool}/${current_backup_marker} | + awk "{gsub(/^$backup_pool./,\"\",\$1); print\$1}" > $local_list +if [ $? != 0 ]; then + fatal_and_exit "-E- local $zfs list on $hostname command failed" $mailto fi -for snap in $(zfs list -rH -t snapshot "$fs" | - sed 's,.*@,,' | awk '{print$1}'); do - if [ "$snap" = "$common" ]; then - foundcommon=true - continue +# Destroy the current backup marker snapshot on the remote system if it exists +grep -q ${current_backup_marker} $remote_list +if [ $? == 0 ]; then + $ssh $remote $zfs destroy ${current_backup_marker} + if [ $? != 0 ]; then + fatal_and_exit "-E- remote $zfs destroy on $hostname command failed" $mailto fi +fi - if $foundcommon; then - if [ -z "$base" ]; then - echo >&2 "Sending '$1/$snap'" - zfs send "$1@$snap" > $tmpdir/zfs.part - mv $tmpdir/zfs.part $tmpdir/zfs.data - zfs recv "$destinationpool/$fsname" < $tmpdir/zfs.data - rm -f $tmpdir/zfs.data - zfs set readonly=on "$destinationpool" - zfs set atime=off "$destinationpool" - zfs set sharenfs=off "$destinationpool" - zfs set mountpoint=legacy "$destinationpool" - zfs unmount "$destinationpool/$fsname" - zfs rollback "$destinationpool/$fsname@$snap" - else - echo >&2 "Sending '$1@$base' -> '$1/$snap'" - zfs send -i "$1@$base" "$1@$snap" > $tmpdir/zfs.part - mv $tmpdir/zfs.part $tmpdir/zfs.data - zfs recv "$destinationpool/$fsname" < $tmpdir/zfs.data - rm -f $tmpdir/zfs.data - # zfs unmount "$destinationpool/$fsname" - # zfs rollback "$destinationpool/$fsname@$snap" +# Create the current backup marker snapshot on the remote system +$ssh $remote $zfs snapshot ${current_backup_marker} +if [ $? != 0 ]; then + fatal_and_exit "-E- remote $zfs snapshot on $hostname command failed" $mailto +fi + +# Check to see if the previous backup marker exists in the remote snapshot list. +# Check to see if the previous backup marker exists in the local snapshot list. +# If the previous backup markers exists, perform an incremental replicate. Else: +# 1) check to see if a common snapshot exists, and perform an incremental replicate. +# 2) if no common snapshot exists, destroy the local filesystem, and perform a full replicate. +grep -q ${previous_backup_marker} $remote_list +no_markers=$? +grep -q ${previous_backup_marker} $local_list +no_markers=$(($no_markers || $?)) + +if [ $no_markers == 0 ]; then + # We found backup markers, incrementally send the new snaps + + # First, rollback the local backup pool to the previous backup marker in case the previous + # backup was interrupted for some reason. If we don't do this, the zfs send -R command + # below may complain about snaps already existing as it tries to resend from the + # previous backup marker again from a previously interrupted replicate. + $zfs rollback -r ${backup_pool}/${previous_backup_marker} + if [ $? != 0 ]; then + fatal_and_exit "-E- remote incremental $zfs rollback command failed on $hostname" $mailto + fi + # Now it should be safe to send the snaps + replicate "$zfs send -R -I${previous_backup_marker} ${current_backup_marker}" + if [ $? != 0 ]; then + fatal_and_exit "-E- remote incremental $zfs send command failed on $hostname" $mailto + fi +else + # We didn't find any backup markers, next check to see if we have a common snapshot. + + # See what the most recent snapshot on the remote end is. + latest=$(tail -n 1 $remote_list) + + # I did this to make sure that diff would always display the most recent common + # Since we're keying off the context of the diff, we need to ensure we will get context + # by injecting a known difference in case no others exist in the lists. + echo bogus.remote >> $remote_list + echo bogus.local >> $local_list + common=$(diff -u $remote_list $local_list | grep '^ ' | tail -n 1) + + if [[ -n "$common" ]]; then + # We found a common snapshot, incrementally send the new snaps + replicate "$zfs send -R -I${common/*@/@} ${current_backup_marker}" + if [ $? != 0 ]; then + fatal_and_exit "-E- remote incremental $zfs send command failed on $hostname" $mailto + fi + else + # We did not find any markers or a common snapshot + # At this point, we'll have to send the entire filesystem + # Destroy the local filesystem if it exists before receving the full replicate + zfs list ${backup_pool}/${remote_fs} > /dev/null 2>&1 + if [ $? == 0 ]; then + if [[ $destroy_local_filesystem_on_full_replicate == 1 ]]; then + $zfs destroy -r ${backup_pool}/${remote_fs} + if [ $? != 0 ]; then + fatal_and_exit "-E- remote full $zfs destroy command failed on $hostname" $mailto + fi + else + echo "-W- We need to destroy a local filesystem before receiving a full stream." + echo " However, since the option is set to prevent this, skipping replicate operation." + fatal_and_exit "unable to destroy local filesystem:\n$zfs destroy -r ${backup_pool}/${remote_fs} not able to run on $hostname" $mailto + fi + fi + # Send the full filesystem + replicate "$zfs send -R ${current_backup_marker}" + if [ $? != 0 ]; then + fatal_and_exit "-E- remote full $zfs send command failed on $hostname" $mailto fi - base=$snap fi -done +fi + +# destroy the previous backup markers now that we've replicated past them +# don't check the return codes here because these may not exist, and that is ok +$zfs destroy ${backup_pool}/${previous_backup_marker} > /dev/null 2>&1 +$ssh $remote $zfs destroy ${previous_backup_marker} > /dev/null 2>&1 -true +# Rename the current backup marker to be the previous backup marker +$zfs rename ${backup_pool}/${current_backup_marker} ${backup_pool}/${previous_backup_marker} +if [ $? != 0 ]; then + fatal_and_exit "-E- local $zfs rename command failed on $hostname" $mailto +fi +$ssh $remote $zfs rename ${current_backup_marker} ${previous_backup_marker} +if [ $? != 0 ]; then + fatal_and_exit "-E- remote $zfs rename command failed on $hostname" $mailto +fi diff --git a/zfs-replicate-all b/zfs-replicate-all new file mode 100755 index 0000000..62a0ce4 --- /dev/null +++ b/zfs-replicate-all @@ -0,0 +1,150 @@ +#!/bin/bash + +# Author: Alan J. Pippin +# Description: This script calls zfs-replicate for each filesystem needing +# to be backed up, or replicated, to another ZFS pool. + +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} + +# Setup some default values +logfile="$logdir/zfs-replicate.log" +mylogfile="$logdir/zfs-replicate-all.log" +date=`date` +starttime=`date +%s` + +# Make sure we aren't already running +SCRIPT_NAME=${0##*/} +PROCESS_LIST=`tempfile` +ps -ef | grep -e "$SCRIPT_NAME" | grep -v grep | grep -v $$ | grep -v $PPID >> $PROCESS_LIST +if [[ $? == 0 ]]; then + echo "$date Another $SCRIPT_NAME process is already running" >> $mylogfile + cat $PROCESS_LIST >> $mylogfile + exit 1 +fi +[[ -e "$PROCESS_LIST" ]] && rm -f $PROCESS_LIST + +# This function checks to see if our runtime has exceeded our stoptime +timeexceeded() { + if [[ $maxruntime == 0 ]]; then + return 0 + fi + currenttime=`date +%s` + elapsedtime=$(($currenttime - $starttime)) + stoptime=$(($maxruntime*60)) + if [[ $elapsedtime -gt $stoptime ]]; then + #echo "$elapsedtime > $stoptime" + return 1 + fi + #echo "$elapsedtime < $stoptime" + return 0 +} + +# This function cleanup and exit trap +cleanup_and_exit() { + #echo "cleanup and exit" + rm -rf "$lockdir" + exit 0 +} +trap cleanup_and_exit INT + +fatal_and_exit() { + # echo message to terminal + echo -e 2>&1 "$1" + # send email notification + echo -e "$1" | $mailx -s "zfs-replicate-all on $hostname failed" "$mailto" + exit 1 +} + +# This function executes the replicate command and checks the stoptime +replicate() { + zfs-replicate $* >> $logfile 2>&1 + timeexceeded + if [ $? == 1 ]; then + cleanup_and_exit + fi +} + +# This function obtains the date a given snapshot was created in epoch seconds +snapshot_age() { + snapshot=${backup_pool}/${1}${previous_backup_marker} + $zfs list -t snapshot ${snapshot} > /dev/null 2>&1 + if [ $? == 0 ]; then + $zfs get creation ${snapshot} > /dev/null 2>&1 + if [ $? == 0 ]; then + snap_creation=`$zfs get creation ${snapshot} | grep $1 | awk '{ print $3" "$4" "$5" "$6" "$7 }'` + snap_age=`date -d "$snap_creation" +%s` + echo "$snap_age" + else + echo "0" + fi + else + echo "0" + fi +} + +# Import the local backup pool if needed and the option is given to do so, else error out +zpool list -H "$backup_pool" >/dev/null 2>&1 +if [ $? != 0 ]; then + if [[ $import_export_backup_pool == 1 ]]; then + zpool import $backup_pool + if [ $? != 0 ]; then + fatal_and_exit "-E- unable to import the backup pool $backup_pool on $hostname" "$mailto" + fi + else + fatal_and_exit "-E- The local backup pool on $hostname, '$backup_pool' doesn't seem to exist." "$mailto" + fi +fi + +# Obtain the zpool guid for the local backup pool +backup_pool_guid=`zpool get guid $backup_pool 2>&1 | grep $backup_pool | awk '{ print $3 }'` +zpool get guid $backup_pool > /dev/null 2>&1 +if [ $? != 0 ]; then + fatal_and_exit "-E- Unable to extract the guid for the local backup pool on $hostname: $backup_pool" "$mailto" +fi + +# Setup our backup marker names +current_backup_marker=@current-backup-${backup_pool_guid} +previous_backup_marker=@previous-backup-${backup_pool_guid} + +# Auto snapshot every zfs filesystem on the system specified below +echo "$date ZFS replicate started" >> $logfile +echo "$date ZFS replicate started" | tee -a $mylogfile + +# Sort the filesystems to replicate by the oldest backup first +tmpfile=`tempfile` +for filesystem in $filesystems_to_replicate; do + age=`snapshot_age $filesystem` + echo $filesystem $age >> $tmpfile +done +sorted_filesystems=`cat $tmpfile | sort -n -k 2 | awk '{ print $1 }'` +rm -f $tmpfile + +# Replicate the sorted filesystems +for filesystem in $sorted_filesystems; do + echo "-> Replicating $remote:$filesystem to ${backup_pool}/${filesystem}" | tee -a $mylogfile + replicate $remote $filesystem +done + +# Export the local pool if told to do so +if [[ $import_export_backup_pool == 1 ]]; then + # Don't export the pool if there is a currently running zfs-scrub operation + ps -ef | grep -q "zfs-scrub ${backup_pool}" | grep -v grep + if [ $? != 0 ]; then + zpool export $backup_pool + if [ $? != 0 ]; then + fatal_and_exit "-E- unable to export the local backup pool $backup_pool on $hostname" "$mailto" + fi + fi +fi + +# All done +echo `date` ZFS replicate complete >> $logfile +echo `date` ZFS replicate complete | tee -a $mylogfile + +# Parse the log file and extract our backup stats +zfs-log-parser "$logfile" "$date" >> $logfile +zfs-log-parser "$logfile" "$date" | tee -a $mylogfile + diff --git a/zfs-restore b/zfs-restore new file mode 100755 index 0000000..fab2b6e --- /dev/null +++ b/zfs-restore @@ -0,0 +1,102 @@ +#!/bin/bash + +# Author: Alan Pippin +# Description: This script "restores" or sends a local filesystem to a remote zfs pool. +# Usage: zfs-restore + +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} + +# command line arg parsing +src_pool=$1 +src_fs=$2 +dst_pool=$3 +dst_fs=$4 +dst_hostname=$5 + +if [[ -z "$SCRIPT_UNDER_TEST" ]]; then + exec >> $logdir/zfs-restore.log 2>&1 +fi + +fatal_and_exit() { + echo -e 2>&1 "$1" + exit 1 +} +trap fatal_and_exit INT + +# Make sure we have valid arguments +if [[ -z "$src_pool" ]] || [[ -z "$src_fs" ]] || [[ -z "$dst_pool" ]] || [[ -z "$dst_fs" ]] || [[ -z "$dst_hostname" ]]; then + fatal_and_exit "Usage: $0 " +fi + +date=`date` +echo "$date ZFS restore started: $src_pool/$src_fs -> $dst_hostname:$dst_pool/$dst_fs" + +# check for localhost +if [[ $dst_hostname = "localhost" ]]; then + dst_hostname="" + ssh="" +fi + +# Make sure the src pool and src filesystem exist, or print some errors +zpool list -H "$src_pool" >/dev/null 2>&1 +if [ $? != 0 ]; then + fatal_and_exit "-E- The src pool, '$src_pool' doesn't seem to exist." +fi +zfs list "$src_pool/$src_fs" >/dev/null 2>&1 +if [ $? != 0 ]; then + fatal_and_exit "-E- The src filesystem for the src pool, '$src_pool/$src_fs' doesn't seem to exist." +fi + +# Obtain the zpool guid for the src pool +src_pool_guid=`zpool get guid $src_pool 2>&1 | grep $src_pool | awk '{ print $3 }'` +zpool get guid $src_pool > /dev/null 2>&1 +if [ $? != 0 ]; then + fatal_and_exit "-E- Unable to extract the guid for the src pool on $hostname: $src_pool" $mailto +fi + +# Setup our backup marker names +last_backup_marker=${src_fs}@previous-backup-${src_pool_guid} + +# Check to make sure the src fs exists +$zfs list -t snapshot "$src_pool/$last_backup_marker" > /dev/null 2>&1 +if [ $? != 0 ]; then + fatal_and_exit "-E- The src snapshot '$src_pool/$last_backup_marker' does not exist. Unable to continue." +fi + +# Check to make sure the dst pool exists +$ssh $dst_hostname $zfs list ${dst_pool} > /dev/null 2>&1 +if [ $? != 0 ]; then + fatal_and_exit "-E- The destination pool '$dst_pool' does not exist. Create the pool '$dst_pool' and try again." +fi + +# Check to make sure the dst filesystem does not exist +$ssh $dst_hostname $zfs list ${dst_pool}/${dst_fs} > /dev/null 2>&1 +if [ $? == 0 ]; then + fatal_and_exit "-E- The destination pool/filesystem '$dst_pool/$dst_fs' already exists. Destroy the filesystem '$dst_fs' and try again." +fi + +# Now send the src filesystem +if [[ -n "$SCRIPT_UNDER_TEST" ]]; then + echo "$zfs send -R $src_pool/$last_backup_marker | $ssh $dst_hostname $zfs recv -dv $dst_pool" +else + if [[ $throttle_enable == 1 && -e $throttle ]]; then + $zfs send -R "$src_pool/$last_backup_marker" | $throttle $throttle_opt | $ssh $dst_hostname $zfs recv -dv $dst_pool + else + $zfs send -R "$src_pool/$last_backup_marker" | $ssh $dst_hostname $zfs recv -dv $dst_pool + fi +fi + +# Now rename the dst filesystem (move it into place) +if [[ -n "$SCRIPT_UNDER_TEST" ]]; then + echo "$ssh $dst_hostname $zfs rename $dst_pool/$src_fs $dst_pool/$dst_fs" +else + $ssh $dst_hostname $zfs rename $dst_pool/$src_fs $dst_pool/$dst_fs +fi + +# All done! +date=`date` +echo "$date ZFS restore completed: $src_pool/$src_fs -> $dst_hostname:$dst_pool/$dst_fs" + diff --git a/zfs-restore-all b/zfs-restore-all new file mode 100755 index 0000000..3931190 --- /dev/null +++ b/zfs-restore-all @@ -0,0 +1,55 @@ +#!/bin/bash + +# Author: Alan J. Pippin +# Description: This script calls zfs-restore for each filesystem needing +# to be restored (that was formerly replicated here) to another ZFS pool. + +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} + +# Setup some default values +logfile="$logdir/zfs-restore-all.log" + +# Setup our output +if [[ -z "$SCRIPT_UNDER_TEST" ]]; then + exec >> $logfile 2>&1 +fi + +cleanup_and_exit() { + exit 1 +} +trap cleanup_and_exit INT + +# See if the user has a specific pool to restore in mind +restore_pool=$1 + +# Restore every ZFS filesystem we were told to replicate +echo `date` ZFS restore started + +# For each filesystem we are supposed to restore, do it +for filesystem in $filesystems_to_replicate; do + dst_pool=${filesystem%%/*} + dst_fs=${filesystem#*/} + # Check to make sure the dst filesystem does not exist + if [[ $remote = "localhost" ]]; then + $ssh $remote $zfs list ${dst_pool}/${dst_fs} > /dev/null 2>&1 + else + $zfs list ${dst_pool}/${dst_fs} > /dev/null 2>&1 + fi + if [ $? != 0 ]; then + echo "$filesystem" | grep -q "$restore_pool" + if [ $? == 0 ]; then + # This filesystem matches our restore pool pattern + echo `date` Restoring $filesystem to $remote + zfs-restore $backup_pool $filesystem $dst_pool $dst_fs $remote + fi + else + echo "-I- Filesystem already exists on destination. Skipping: $filesystem" + fi +done + +# All done +echo `date` ZFS restore complete + diff --git a/zfs-rm-all-snaps b/zfs-rm-all-snaps new file mode 100755 index 0000000..73c0746 --- /dev/null +++ b/zfs-rm-all-snaps @@ -0,0 +1,35 @@ +#!/bin/bash + +# Author: Alan Pippin +# Description: This script will remove all snaps containing a given snapshot pattern +# across all filesystems on a given pool. +# Usage: zfs-rm-all-snaps + +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} + +# command line arg parsing +zfs_pool=$1 +snap_pattern=$2 + +if [[ -z "$zfs_pool" ]] || [[ -z "$snap_pattern" ]]; then + echo "Usage: $0 " + exit 1 +fi + +echo "-> Deleting all snapshots on pool '$zfs_pool' with pattern '$snap_pattern'" +for snap in `zfs list -t snapshot 2>/dev/null | grep "^$zfs_pool" | grep "$snap_pattern" | awk '{print $1}'`; do + echo " removing snapshot: $snap" +done + +echo -e "\n"; +read + +for snap in `zfs list -t snapshot 2>/dev/null | grep "^$zfs_pool" | grep "$snap_pattern" | awk '{print $1}'`; do + echo " destroying snapshot: $snap" + zfs destroy "$snap" 2>/dev/null +done + +echo "-> Snapshots removed" diff --git a/zfs-scripts.conf.dist b/zfs-scripts.conf.dist new file mode 100644 index 0000000..40a7e4b --- /dev/null +++ b/zfs-scripts.conf.dist @@ -0,0 +1,60 @@ +########################################################################### +# local configuration file for ZFS scripts +########################################################################### + +# setup your path here to find all the binaries the ZFS scripts call +PATH=/usr/sbin:/sbin:/etc/bin:.:$PATH + +# set our hostname +hostname=`hostname` + +# specify the path to the zfs binary +zfs=/sbin/zfs + +# specify the path to your command line mailer +mailx=/usr/bin/mailx + +# specify the path to the logdir the ZFS scripts should dump their logs to +logdir="/var/log/zfs" + +# specify the name of the lockdir used when performing ZFS admin operations +lockdir="/tmp/zfs-admin-lock" + +# specify the user to send email reports to +mailto="user@domain" + +# specifu the ssh command and any options needed for accessing a remote server +ssh="ssh" + +# specify the fqdn of the remote server to pull snapshots from to backup locally +# specify "localhost" if the remote server is the local machine +remote= + +# specify the name of the local pool to store the $remote's snapshots into +backup_pool= + +# import backup pool before replication and export backup pool after replication +import_export_backup_pool=0 + +# when this variable is set, local filesystems will be destroyed +# before receiving a full streams into them from the remote source. +# if it needs to do this, and this option is set to 0, it aborts. +destroy_local_filesystem_on_full_replicate=0 + +# set this to 1 if you want the snapshot script to run in "test" mode (not really take any snaps) +# leave blank if not under test +SNAP_UNDER_TEST= + +# Specify the maximum run time in minutes that the replicate script can run for (0=no limit) +maxruntime=0 + +# Setup throttling related parameters that will rate limit the zfs send | zfs receive pipe +# The maximum limit a local backup can handle to avoid excessive CPU load is 10MB/s +throttle_enable=0 +throttle_opt="-v -M 10" + +# Specify the list of filesystems to replicate from the $remote to the $backup_pool (1 per line) +# The format of each line should be: pool/filesystem +filesystems_to_replicate=" +" + diff --git a/zfs-scrub b/zfs-scrub new file mode 100755 index 0000000..fbeadb7 --- /dev/null +++ b/zfs-scrub @@ -0,0 +1,105 @@ +#!/bin/bash + +# Author: Alan J. Pippin +# Description: This script will attempt to scrub a given pool. +# This script ensures that only 1 scrub operation is +# running at any given time. This serializes the zfs +# scrub process for any pool. + +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} + +exec >> $logdir/zfs-scrub.log 2>&1 + +pools="$*" +maxsleeptime=360 + +if [ -z "$pools" ]; then + echo "-E- Usage: $0 " + exit 1 +fi + +fatal_and_exit() { + # echo message to terminal + echo -e 2>&1 "$1" + # send email notification + echo -e "$1" | $mailx -s "zfs scrub on $hostname failed" "$mailto" + exit 1 +} + +for i in $pools +do + # Import the local pool if needed and option was given to do so, else error out + zpool list -H "$i" >/dev/null 2>&1 + if [ $? != 0 ]; then + if [[ $import_export_backup_pool == 1 ]] && [[ "$i" =~ "$backup_pool" ]]; then + zpool import $i + if [ $? != 0 ]; then + fatal_and_exit "-E- unable to import the pool $i" + fi + else + fatal_and_exit "-E- The local pool, '$i' doesn't seem to exist." + fi + fi + + # Check to see if any zfs filesystem has a scrub being performed on it now. + # If it does, we cannot perform more than one scrub operation at a time. + while true; do + zpool status | grep scan: | grep "in progress" > /dev/null 2>&1 + if [ $? == 0 ]; then + # Another zpool scrub operation is already running + # Wait until it is done before continuing + ransleep=$(($RANDOM % $maxsleeptime)) + sleep $ransleep + else + # Another zpool scrub operation is not running + break + fi + done + + date=`date` + echo "$date: Scrub started for zfs pool $i" + zpool scrub $i + + # Wait until the scrub completes, and check for any errors + while true; do + zpool status $i 2>/dev/null | grep scan: | grep "in progress" > /dev/null 2>&1 + if [ $? == 0 ]; then + # Our zpool scrub operation is still running + # Wait until it is done before continuing + ransleep=$(($RANDOM % $maxsleeptime)) + sleep $ransleep + else + # Our scrub operation has completed + break + fi + done + + date=`date` + echo "$date: Scrub completed for zfs pool $i" + + # Check for any scrub errors + zpool status $i | grep scan: + zpool status $i | grep scan: | grep "with 0 errors" > /dev/null 2>&1 + if [ $? != 0 ]; then + # The scrub found errors + zpool status $i | $mailx -s "zpool scrub on $hostname $i found errors" "$mailto" + fi + + # Export the local pool if told to do so + if [[ $import_export_backup_pool == 1 ]] && [[ "$i" =~ "$backup_pool" ]]; then + # Don't export the pool if there is a currently running zfs-replicate operation + ps -ef | grep -q "zfs-replicate" | grep -v grep + if [ $? != 0 ]; then + zpool export $i + if [ $? != 0 ]; then + echo "-E- unable to export the local pool $i" + zpool status $i | $mailx -s "zpool scrub on $hostname unable to export the local pool $i" "$mailto" + fi + fi + fi + +done + diff --git a/zfs-snapshot-totals b/zfs-snapshot-totals new file mode 100755 index 0000000..be223d9 --- /dev/null +++ b/zfs-snapshot-totals @@ -0,0 +1,49 @@ +#!/usr/bin/perl + +@snapshots=`zfs list -t snapshot`; + +$kilo = 1024; +$mega = 1024 * 1024; +$giga = 1024 * 1024 * 1024; +$maxlen = 0; + +sub adjust_size +{ + my ($size) = @_; + if($size > ($giga)) { $size = $size / $giga; $size = sprintf("%2.2fG",$size); } + elsif($size > ($mega)) { $size = int($size / $mega); $size = "$size"."M"; } + elsif($size > ($kilo)) { $size = int($size / $kilo); $size = "$size"."K"; } + return $size; +} + +foreach $snapshot (@snapshots) +{ + chomp($snapshot); + if($snapshot =~ /(\S+)\@(\S+)\s+(\S+)\s+/) { + $filesystem = $1; + $size = $3; + if(length($filesystem) > $maxlen) { $maxlen = length($filesystem); } + if($size =~ /k/i) { $size = $size * $kilo; } + if($size =~ /m/i) { $size = $size * $mega; } + if($size =~ /g/i) { $size = $size * $giga; } + $totals{$filesystem}{size} += $size; + $totals{$filesystem}{snapshots}++; + } +} + +$maxlen=$maxlen+2; +printf "%-${maxlen}s %-15s %-10s\n","ZFS Filesystem","Snapshots Size","Num Snapshots"; +printf "%-${maxlen}s %-15s %-10s\n","--","--","--"; +foreach $key (sort keys %totals) +{ + $size = $totals{$key}{size}; + $total_size += $size; + $total_snapshots += $totals{$key}{snapshots}; + $size = &adjust_size($size); + printf "%-${maxlen}s %-15s %-10s\n", $key, $size, $totals{$key}{snapshots}; +} + +$total_size = &adjust_size($total_size); +printf "%-${maxlen}s %-15s %-10s\n","--","--","--"; +printf "%-${maxlen}s %-15s %-10s\n","Total Snapshots",$total_size,$total_snapshots; + diff --git a/zfs-snapshot-totals-cron b/zfs-snapshot-totals-cron new file mode 100755 index 0000000..7ac2f55 --- /dev/null +++ b/zfs-snapshot-totals-cron @@ -0,0 +1,18 @@ +#!/bin/bash + +# Place this script in your crontab to run daily. +# It produces a log file that can be parsed by the +# zfs-log-parser script to display a nice table +# showing how much space the snapshots are taking up. + +# source our configuration +config="${0%/*}/zfs-scripts.conf" +[ -e "${config}.dist" ] && . ${config}.dist +[ -e "${config}" ] && . ${config} + +logfile="$logdir/zfs-snapshot-totals.log" + +date >> $logfile +zfs-snapshot-totals >> $logfile +echo >> $logfile +