#!/usr/bin/perl -w
# FreeBSD UFS2 Snapshot Management Tool
#
# Wu Chin-Hao <wchunhao@cs.nctu.edu.tw>
#
# If you are to send feedback,
# plz include 'FreeBSD Port SNAP' in the subject.
use Time::Local;
use Getopt::Std;
%opt = ();
getopts('pmhc:', \%opt) or &usage;
&usage if $opt{h};
$conf_file = (defined $opt{c})? $opt{c} : "/usr/local/etc/snap.conf";
do $conf_file or die "Cannot found configuration file at $conf_file";
&check_config;
if (defined $opt{m}){
&show_manual_files(@ARGV);
exit;
}
## create new snapshot
for $base (keys %fs) {
next unless $fs{$base}{enable};
my $dir = "$base/$fs{$base}{dir}";
my $all = $fs{$base}{all};
my $w = $fs{$base}{weekly};
my $d = $fs{$base}{daily};
my $h = $fs{$base}{hourly};
my $at = $fs{$base}{atHour};
my $snapTime = time;
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($snapTime);
$mon ++;
$year = $year % 100;
my $snapDate = sprintf "%02d%02d%02d_%02d:%02d:%02d", $year, $mon, $mday, $hour, $min, $sec;
if (opendir DIR, $dir){
my @files = grep {! /^(\.|\.\.)$/} readdir DIR;
my $timeDiff = ($snapTime - &getSnapTime($dir, "hour.0"))/60;
my $doNow = 0;
if ($at) {
for (@$at){
if ($_ == $hour) { $doNow = 1; last }
}
} else { $doNow = 1 }
if ($opt{p} and $doNow and ($min < 5 or $min > 55) and ( $timeDiff > 55))
{ # Considered as periodic snapshot and then do hourly rotation
if ($debug) {
print "=> Periodic snapshot for $base\n";
}
my $last = 0;
for (@files){
if (/^hour\.(\d+)$/) { $last = $1 if $1 > $last }
}
if ($last >= $h - 1) { &delete($dir, "hour.$last") }
for (sort {
$a =~ /^hour\.(\d+)$/; my $a1= $1;
$b =~ /^hour\.(\d+)$/; my $b1= $1;
$b1 <=> $a1; # decreasing
}
grep { /^hour\.(\d+)$/ } @files){
/^hour\.(\d+)$/;
system "$config{mv} $dir/hour.$1 $dir/hour.".($1 + 1)
if -f "$dir/hour.$1";
if ($debug) {
print "Moving $dir/hour.$1 to $dir/hour.", ($1 + 1), "\n";
}
}
rewinddir DIR;
@files = grep {! /^(\.|\.\.)$/} readdir DIR;
if ($#files +1 >= $all) { # still no space after rotation?
warn "Cannot create more snapshots, $base skipped..";
next
}
&createSnap("$dir/$snapDate", $base);
link "$dir/$snapDate", "$dir/hour.0";
if ($debug) {
print "$dir/hour.0 created\n";
}
# do daily rotation
rewinddir DIR;
@files = grep {! /^(\.|\.\.)$/} readdir DIR or warn "cannot readdir when doing daily rotation";
if (($hour == 0 and $min < 5) or ($hour == 23 and $min > 55)){
$last = 0;
for (@files){
if (/^daily\.(\d+)$/) { $last = $1 if $1 > $last }
}
if ($last >= $d - 1) { &delete($dir, "daily.$last") }
for (sort {
$a =~ /^daily\.(\d+)$/; my $a1= $1;
$b =~ /^daily\.(\d+)$/; my $b1= $1;
$b1 <=> $a1; # decreasing
}
grep { /^daily\.(\d+)$/ } @files){
/^daily\.(\d+)$/;
system "$config{mv} $dir/daily.$1 $dir/daily.".($1 + 1)
if -f "$dir/daily.$1";
if ($debug) {
print "Moving $dir/daily.$1 to $dir/daily.".($1 + 1)."\n";
}
}
link "$dir/$snapDate", "$dir/daily.0" or warn "link $snapDate to daily.0 failed";
if ($debug) {
print "$dir/daily.0 created\n";
}
}
# do weekly rotation
rewinddir DIR;
@files = grep {! /^(\.|\.\.)$/} readdir DIR or warn "cannot readdir when doing weekly rotation";
if ($wday == 0 and
(($hour == 0 and $min < 5) or ($hour == 23 and $min > 55)) ){
$last = 0;
for (@files){
if (/^week\.(\d+)$/ and $1 > $last) { $last = $1 }
}
if ($last >= $d - 1) { &delete($dir, "week.$last") }
for (sort {
$a =~ /^week\.(\d+)$/; my $a1= $1;
$b =~ /^week\.(\d+)$/; my $b1= $1;
$b1 <=> $a1; # decreasing
}
grep { /^week\.(\d+)$/ } @files){
/^week\.(\d+)$/;
system "$config{mv} $dir/week.$1 $dir/week.".($1 + 1)
if -f "$dir/week.$1";
if ($debug) {
print "Moving $dir/week.$1 to $dir/week.".($1 + 1)."\n";
}
}
link "$dir/$snapDate", "$dir/week.0" or warn "link $snapDate to week.0 failed";
if ($debug) {
print "$dir/week.0 created\n";
}
}
# manually creation
} elsif (not defined $opt{p}) {
my $num = &manual_left($dir, $all, $w, $d, $h, $at);
if ($debug) {
print "=> Manual snapshot for $base\n";
print "We can create $num more manual snapshots\n";
}
if ($num > 0) {
&createSnap("$dir/$snapDate", $base);
} else {
warn "Cannot create more snapshots, $base skipped..";
next
}
}
closedir DIR;
} else {
warn "Cannot open $dir, $base skipped...";
next;
}
}
exit;
sub check_config {
my ($key,@a, @b, @mount, $all, $w, $d, $h, $at);
if ((!$config{enable}) and !$ARGV[0]) { exit }
if ($config{debug}) { $debug = 1 }
@a = `mount -t ufs -p`;
for (@a){
@b = split;
push @mount, $b[1];
}
my $got;
for $key (keys %fs){
if ($#ARGV > -1) {
$fs{$key}{enable} = 0;
for (@ARGV){
if ($_ eq $key) { $fs{$key}{enable} = 1; last }
}
}
next if (!$fs{$key}{enable});
$got = 0;
for (@mount){
if ($key eq $_) { $got = 1; last}
}
unless ($got) {
warn "Not a UFS2 filesystem, $key skipped...";
$fs{$key}{enable} = 0;
next
}
if (! -d "$key/$fs{$key}{dir}") {
mkdir "$key/$fs{$key}{dir}", 0775 or
do {
warn "Cannot mkdir $key/$fs{$key}{dir}, $key skipped...";
$fs{$key}{enable} = 0;
next
};
system "$config{chown} root:operator $key/$fs{$key}{dir}";
chmod 0775,"$key/$fs{$key}{dir}";
}
$all = $fs{$key}{all} || 20;
$w = $fs{$key}{weekly} || 0;
$d = $fs{$key}{daily} || 0;
$h = $fs{$key}{hourly} || 0;
$at = $fs{$key}{atHour};
if ($all > 20 or $all < 0) { # filesystem limit
warn "all > 20 or all < 0, $key skipped...";
$fs{$key}{enable} = 0;
next
}
if ($w < 0 or $d < 0 or $h < 0 or $w > 20 or $d > 20 or $h > 20) {
warn "incorrect rotation time setting, $key skipped...";
$fs{$key}{enable} = 0;
next
}
if (&max_periodic($w,$d,$h,$at) > $all) {
warn "number of periodic snapshot > all, $key skipped...";
$fs{$key}{enable} = 0;
next
}
if ($at){ # for ${fs}{key}{atHour} extension
$got = 0;
for (@$at){
if ($_ < 0 or $_ > 23 or $_ != int($_)){
$got = 1; last
}
}
if ($got) {
warn "incorrect atHour setting, $key skipped...";
$fs{$key}{enable} = 0;
next
}
}
}
}
sub createSnap {
my ($file, $fs) = @_;
if ($debug) {
print "Creating snapshot $file for $fs ...";
}
system "$config{mount} -u -o snapshot $file $fs";
if ($debug){
if ($?) {
print "FAILED\n";
} else {
print "done\n";
}
}
}
sub getSnapTime {
my $dir = shift;
my $file = shift; # eg, hour.0
return 0 unless -f "$dir/$file";
my @a = stat("$dir/$file");
my $inode = $a[1];
my $ctime = $a[10]; # inode change time, in fact not usable.
opendir KK, $dir;
my @b = readdir KK or warn "Cannot readdir in getSnapTime!";
closedir KK;
for (@b){
if (-f "$dir/$_"){
@a = stat("$dir/$_");
if ($a[1] == $inode and /(\d\d)(\d\d)(\d\d)_(\d\d):(\d\d):(\d\d)/){
my $k = timelocal($6,$5,$4,$3,$2-1,$1+100);
return $k;
}
}
}
if ($debug) {
print "getSnapTime uses ctime: $ctime, may be incorrect\n";
}
return $ctime;
}
sub delete {
my $dir = shift;
my $file = shift;
my @a = stat("$dir/$file");
my $inode = $a[1];
return unless -f "$dir/$file";
if ($debug) {
print "Unlinking snapshot $dir/$file\n";
}
unlink "$dir/$file";
opendir KK, $dir;
my @b = readdir KK or warn "Cannot readdir in delete";
closedir KK;
my $tf = undef;
for (@b){
if (-f "$dir/$_"){
@a = stat("$dir/$_");
if ($a[1] == $inode){
if (/(\d\d)(\d\d)(\d\d)_(\d\d):(\d\d):(\d\d)/) {
$tf = $_;
} else { return }
}
}
}
if ($debug) {
print "Also delete unused snapshot $dir/$tf\n";
}
unlink "$dir/$tf";
}
sub max_periodic {
my ($w, $d, $h, $at) = @_;
my ($num_h, $dh, $wh, $wd, $wdh);
if ($at) { $num_h = scalar @$at } else { $num_h = 24; }
if ($num_h == 0) { return 0 }
$wd = ($w > int($d/7))? int($d/7) : $w;
$wh = ($w > int($h/(7*$num_h)))? int($h/(7*$num_h)) : $w ;
$dh = ($d > int($h/$num_h))? int($h/$num_h) : $d;
$wdh = ($wd > $wh)? $wh : $wd;
return $w + $d + $h - $wd - $wh - $dh + $wdh;
}
sub manual_left {
my ($dir, $all, $w, $d, $h, $at) = @_;
return $all - &max_periodic($w,$d,$h,$at) - (scalar &manual_files($dir));
}
sub manual_files {
my $dir = shift;
opendir KK, $dir;
my @b = readdir KK or warn "Cannot readdir in manaul_files";
closedir KK;
my @periodic = ();
my %aperiodic = ();
my $file;
my @a;
for $file (@b){
if (-f "$dir/$file") {
if ($file =~ /^(hour\.(\d+)|week\.(\d+)|daily\.(\d+))$/) {
@a = stat("$dir/$file");
push @periodic, $a[1]; # put inode
}
}
}
for $file (@b){
if (-f "$dir/$file") {
@a = stat("$dir/$file");
my $got = 0;
for (@periodic) {
if ($a[1] == $_) { $got = 1; last }
}
if (!$got) { $aperiodic{$a[1]} = $file; }
}
}
@b = values %aperiodic;
return wantarray ? @b : ($#b + 1);
}
sub show_manual_files {
my @d = @_;
my @dd = ();
print "\n" if $debug;
if ($#d == -1) {
for (keys %fs){
my $a = ($_ eq "/") ? "" : $_;
push @dd, "$a/$fs{$_}{dir}" if $fs{$_}{enable};
}
} else {
my $key;
for $key (keys %fs){
for (@d) {
if ($key eq $_) {
$key = "" if ($key eq "/");
push @dd, "$key/$fs{$_}{dir}"; last
}
}
}
}
for (sort @dd) {
print "$_ :\n";
my @a = &manual_files($_);
my @b = `/bin/ls -alih $_`;
my $k;
for $k (sort @b) {
for (@a) {
if ($k =~ /\s+$_$/) {
print $k
}
}
}
}
print "\n";
}
sub usage {
print <<END;
FreeBSD UFS2 Snapshot Management Tool
Usage: $0 [-p] [-m] [-h] [-c config_file] [filesystem ...]
-p : periodic task, otherwise manual task.
-m : list manually created snapshots and exit
-h : show this message
-c : where the configuration file is
You can specify filesystems as targets or use all
entries in config_file.
Notice:
*) Filesystems specified should have corresponding
entries in config_file.
*) If filesystem is specified, then it's `enable'
flag and `atHour' will be ignored.
Description:
3 types of periodic snapshots (hourly, daily, weekly)
will be maintained and rotated according to settings
in conf_file.
When $0 is run with -p and if it's on the hour,
then it will be treated as periodic task. Otherwise it
will be considered as a manual snapshop creation task.
Example:
./snap
Do snapshot according to config_file.
In /etc/crontab, specify
0 * * * * root /path-to/snap -p
This will run snap and take 'atHour' into consideration.
./snap / /home /var
Do snapshot on filesystem / and /home and /var only,
other setting are set as in conf_file. This command will
ignore the 'enable' and 'atHour' flag in conf_file.
If, say, /var is not in conf_file, nothing would happen.
./snap -m / /home
List manual created snapshots on / and /home
./snap -m -c /tmp/gg/snap.conf / /home
List manually created files of filesystem / and /home
according to configuration /tmp/gg/snap.conf
END
exit;
}
syntax highlighted by Code2HTML, v. 0.9.1