#!/usr/bin/perl -w use strict; use Time::Local; use Getopt::Long; # +---------------------------------------------------------------------------------------------------+ # ZFS Snapshot (point in time copy) Admin for Rolling Snapshots (snapadm.pl) # Create a policy for snap creation in root crontab with retention time to clear down i.e. cycle them. # e.g. # ...minutes: 0,20,40 * * * * /usr/local/bin/snapadm.pl -ret=0:1:0 -filesystem=storage -class=mins # (generate a snapshot postfix beginning @mins_ on zpool storage at 0,20,40 mins past hour and retain for 1 hour) # ...hours: 0 * * * * /usr/local/bin/snapadm.pl -ret=0:3:0 -filesystem=storage -class=hours # (generate a snapshot postfix beginning @hours_ on zpool storage every hour and clear down after 3 hours) # ...days: 0 1 * * * /usr/local/bin/snapadm.pl -ret=3:0:0 -filesystem=storage -class=days # (generate a snapshot postfix beginning @days_ on zpool storage every day and clear down after 3 days) # # Default class is 'daily' and you would retain for 1 week running nightly before backups. # You dont need -cl for classes unless you want a short term regular interval snapshot on data for # the day and a longer term once daily snapshot going back weeks, -class is to distinguish between backup classes/sets. # * -r is recursive # * -e will not generate point in time snapshot is it used to free up space, clears down snapshots to # given retention period e.g. "-e -ret=0:0:0" will delete all, "-e -ret=0:1:0" keeps last hour. # * -cr is critical. stops a possible scrub, rather than skip snapshot. # # File: "nozfsexpire" if exists within pool or zfs filesystem then snapshots will not be expired, # handy if looking for files to restore on frequent snapshots. # # e.g. Checkpoints # Another good use would be for checkpoints or upgrades, especially within scripts. At build points or stages # in db upgrades you can snap under a class name for the workload (project or database etc). # "snapadm.pl -filesystem=storage/project -class=myproject" notice no -ret so live forever. # if at any point there is a failure you have a restore point. Once completed successfully you can clear them down with # "snapadm.pl -filesystem=storage/project -class=myproject -expire -yes". # Remeber data is never overwritten, there is a cost to freeing up space, snapshots could increase performance. # # e.g. Core Hours # Snapshot through core hours with clear down:- # 0 7-18 * * * /usr/local/bin/snapadm.pl -filesystem=tank -class=corehours # 0 19 * * * /usr/local/bin/snapadm.pl -filesystem=tank -class=corehours -e -y # 0 1 * * * /usr/local/bin/snapadm.pl -ret=7:0:0 -filesystem=tank -class=days # # NOTE: please ensure you have a solid system time. # # (Paul.Errington@northtyneside.gov.uk) # (Live since Friday 13th June 2008) # +---------------------------------------------------------------------------------------------------+ my $timestamp = time; my $SnapFilesystem = ""; # ZFS to act on my $ret; # format: "0d:0h:0m" 'hourly TTL in days' my $class_name = "daily"; # when -class name not given my $ret_ts = 0; # zero (clear) -retention period specified my $opt_exp; # do not snap, expire all or expire top -retention period my $opt_rec; # recursive ZFS snapshots if version support. my $opt_ver; my $opt_critical; my %Days = qw { 0 Sun 1 Mon 2 Tue 3 Wed 4 Thu 5 Fri 6 Sat }; my $zfs_version = `zpool upgrade -v | head -1`; $zfs_version =~ /(\d+)/; my $zfsv_d = $1; &setup_config(); print "ZFS Snap Admin for Rolling Snapshots.\n" if($opt_ver); print "$zfs_version" if($opt_ver); if(`uname -sr` !~ /SunOS 5.10/) { print "(Tested on Solaris 10 only, unexpected results may occur on your machine)\n" if($opt_ver); } &main(); sub main() { if(isScrubbing($SnapFilesystem)) { # scrubs restart when you create/delete snaps. if(defined($opt_critical)) { #for critical snaps stop scrub "-critical" my $poolname = extractPoolName($SnapFilesystem); my $result = `/sbin/zpool scrub -s $poolname`; print "Warning: Scrub on pool $poolname stopped\n"; } else { print "Warning: Scrub in progress admin tasks skipped\n"; exit 0; } } if(defined($ret) || ($ret_ts == $timestamp)) { #checks for an expire period. or dont. no ret then snap forever more. my $ZFSpath = getZFSPath($SnapFilesystem); my $ZFSpool = getZFSPath(extractPoolName($SnapFilesystem)); if(-e "$ZFSpath/nozfsexpire") { #we only expire if no hold on current FS or the pool. print "Cant expire zfs snapshots hold on current FS, delete file $ZFSpath/nozfsexpire\n"; } elsif (-e "$ZFSpool/nozfsexpire") { print "Cant expire zfs snapshots hold on pool, delete file $ZFSpool/nozfsexpire\n"; } else { expireSnapshots(); } } if(!$opt_exp) { createSnapshot($SnapFilesystem); } } # # # sub usage { print "\tZFS SnapAdmin for Rolling Snapshots.\n"; print "\tsnapadm.pl -filesystem=tank/live\n\t-ret=day:hour:min\n"; print "\t-cl=name (default class is daily)\n"; print "\t-rec (recursive filesystem snapshot ZFSv3 11/06).\n"; print "\t-e (expire only, dont snapshot, when used with -r will expire recursive).\n"; print "\t-y (-e without a retention period will deleted all, -y approves this\n\t shorthand for -ret=0:0:0).\n"; print "\t-cr critical stops a possible scrub and snapshots.\n"; print "\t-v more detail.\n"; exit 0; } sub setup_config { if(@ARGV == 0) { &usage(); } my $approve; GetOptions("verbose" => \$opt_ver, "expire" => \$opt_exp, "recursive" => \$opt_rec, "retention=s" => \$ret, "filesystem=s" => \$SnapFilesystem, "class=s" => \$class_name, "yes" => \$approve, "critical" => \$opt_critical); if($SnapFilesystem eq "" || (substr($SnapFilesystem, 0, 1) eq "/")) { print "\nPlease specify a valid zfs filesystem name (not a filepath) !\n\n"; &usage(); exit 0; } if(defined($opt_exp) && !defined($ret) && !defined($approve)) { print "\nPlease specify a retention period! or use -y to approve (deletes all old).\n\n"; &usage(); exit 0; } if(defined($opt_rec)&&($zfsv_d<=2)) { print "\nRecursive snapshots not available in this release! (11/06 lowest release)\n\n"; &usage(); exit 0; } if($class_name =~ /\@/ || $class_name =~ /\_/) { print "Not a valid class name for snapshot postfix, remove '\@' and '_'.\n"; } if(defined($ret)) { setRetentionTimestamp(); } elsif(defined($approve)) { $ret_ts = $timestamp; } } sub setRetentionTimestamp { my $retd = 0; my $reth = 0; my $retm = 0; if($ret =~ /^(\d+):(\d+):(\d+)$/) { $retd=$1; $reth=$2; $retm=$3; } else { print "\nPlease specify valid retention period in form DD:HH:MM\ne.g. year=365:0:0, week=7:0:0, half day=0:12:0, half hour=0:0:30\n"; exit 0; } my $TS_OFFSET = 0; if($retd>0) { $TS_OFFSET+=86400*$retd; } if($reth>0) { $TS_OFFSET+=3600*$reth; } if($retm>0) { $TS_OFFSET+=60*$retm; } $ret_ts = $timestamp-$TS_OFFSET; } sub createSnapshot { my $SnapFS = shift; #print " Snapshotting: $SnapFS\n"; my $ztimestamp = GenerateZFSSnapshotTime(); # Current Time if (defined($opt_rec)) { my $result = `/sbin/zfs snapshot -r $SnapFS\@$class_name\_$ztimestamp`; print "$SnapFS\@$class_name\_$ztimestamp\n"; } else { my $result = `/sbin/zfs snapshot $SnapFS\@$class_name\_$ztimestamp`; print "$SnapFS\@$class_name\_$ztimestamp\n"; } } sub expireSnapshots { my @Snapshots = GetSnapshots(); foreach my $snap (@Snapshots) { $snap =~ s/\s+$//; if(isExpired($SnapFilesystem, $snap)) { my $result = ""; if (defined($opt_rec)) { my $result = `/sbin/zfs destroy -r $snap`; } else { my $result = `/sbin/zfs destroy $snap`; } print " DELETE: $snap. $result\n" if($opt_ver); } } } sub GenerateZFSSnapshotTime { #Format: TextDayOfWeek_YYYYMMDD_HHMM my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($timestamp); return sprintf("%s_%d%02d%02d_%02d%02d", $Days{$wday}, ($year+=1900), ++$mon, $mday, $hour, $min); } sub isExpired { #Format: YYYYMMDD my ($SnapFilesystem, $snapshotName) = @_; if($snapshotName =~ /$SnapFilesystem\@$class_name\_/) { $snapshotName =~ /(\d\d\d\d)(\d\d)(\d\d)_(\d\d)(\d\d)$/; my ($year, $mon, $day, $hour, $min) = ($1, $2, $3, $4, $5); #print "isExpired: $snapshotName: $year, $mon, $day, $hour, $min."; my $snapshot_ts = timelocal(0, $min, $hour, $day, $mon-1, $year); my $retention_ts = $ret_ts; if($snapshot_ts < $retention_ts) { print " Expiring: $snapshotName. ($snapshot_ts < $retention_ts)\n" if($opt_ver); return 1; } else { print " Retained: $snapshotName. ($snapshot_ts < $retention_ts)\n" if($opt_ver); return 0; } } } sub GetFilesystems { return GetZFSType("filesystem"); } sub GetSnapshots { return GetZFSType("snapshot"); } sub GetZFSType { my ($type) = shift; return `/sbin/zfs list -H -t $type -o name`; } sub isScrubbing { my ($FS) = shift; my $poolname = extractPoolName($FS); my $result = `zpool status $poolname | grep "^ scrub: scrub in progress"`; if($result) { return 1; } else { return 0; } } sub extractPoolName { my ($FS) = shift; my $poolname = $FS; if($poolname =~ /\//) { $poolname = substr($poolname, 0, index($poolname, "\/")); } return $poolname; } sub getZFSPath { my ($FS) = shift; my $result = `/sbin/zfs list -H -o mountpoint $FS`; chomp($result); return $result; } #END ############################################################################## ### This script is submitted to BigAdmin by a user of the BigAdmin community. ### Sun Microsystems, Inc. is not responsible for the ### contents or the code enclosed. ### ### ### Copyright Sun Microsystems, Inc. ALL RIGHTS RESERVED ### Use of this software is authorized pursuant to the ### terms of the license found at ### http://www.sun.com/bigadmin/common/berkeley_license.jsp ##############################################################################