# limited testing under their belt.
#
# Instructions:
-#
+#
# The first thing you need to do is go down to the end of this header section
# and set all the user defined variables in the user settings section.
# They have a brief description of what they do and how they should be set.
# Once that is done, you can run the program.
#
# mirror supports one command line switches ( -s ).
-# -s = run sync mode only
+# -s = run sync mode only
#
-# Sync mode should be ran the first time you setup mirror. Otherwise, use
+# Sync mode should be ran the first time you setup mirror. Otherwise, use
# no switch.
# Once you have run mirror for the first time, you will find that it has setup
# a directory hierarchy in your backup root directory:
#
-# /<backuproot>/<hostid> ( host root )
-# /<backuproot>/<hostid>/working ( working directory )
-# /<backuproot>/<hostid>/hourly ( hourly saves )
-# /<backuproot>/<hostid>/daily ( daily saves )
-# /<backuproot>/<hostid>/weekly ( weekly saves )
+# /<backuproot>/<hostid> ( host root )
+# /<backuproot>/<hostid>/working ( working directory )
+# /<backuproot>/<hostid>/hourly ( hourly saves )
+# /<backuproot>/<hostid>/daily ( daily saves )
+# /<backuproot>/<hostid>/weekly ( weekly saves )
#
# The directories are named 'image-<year>-<month>-<day>-<hour>'
#
use YAML::Syck;
use POSIX qw/strftime/;
+use LockFile::Simple;
+use Sys::Hostname;
-use GetOptions::Long;
+use Getopt::Long;
my $config_file = "mirror.yaml";
my $sync = undef;
+my $lock_file = "/var/lock/mirror-" . hostname;
GetOptions("config|c=s" => $config_file,
- "sync|s" => $sync);
+ "sync|s" => $sync);
my @errors = ();
my @warnings = ();
my $c = load_config();
+# Try and obtain a lock.
+my $lockmgr = LockFile::Simple->make(-stale => 1,
+ -autoclean => 1,
+ -hold => 60 * 60 * 3);
+if (! $lockmgr->lock($lock_file)) {
+ push @errors, "Failed to obtain lockfile";
+ die_gracefully();
+}
+
+dodir();
+# do work
+
+$lockmgr->unlock($lock_file);
+
+
sub load_config {
- my $c = LoadFile($config_file);
+ my $c = eval { LoadFile($config_file) };
+ die_gracefully("Failed to load $config_file: $@")
+ if $@;
- for my $required ('backuproot', 'hostid') {
- die_gracefully("No $required set")
- unless defined $c->{$required} || $c->{required} ne '';
- }
+ for my $required ('backuproot', 'hostid') {
+ die_gracefully("No $required set")
+ unless defined $c->{$required} && $c->{$required} ne '';
+ }
$c->{'cp'} ||= "cp -alf";
- $c->{'weekdir'} = "$c->{'backuproot'}/$c->{'hostid'}/weekly";
+ $c->{'weekdir'} = "$c->{'backuproot'}/$c->{'hostid'}/weekly";
$c->{'daydir'} = "$c->{'backuproot'}/$c->{'hostid'}/daily";
- $c->{'hourdir'} = "$c->{'backuproot'}/$c->{'hostid'}/hourly";
- $c->{'working'} = "$c->{'backuproot'}/$c->{'hostid'}/working";
- $c->{'lockfile'} = "$c->{'backuproot'}/$c->{'hostid'}/syncing-now";
+ $c->{'hourdir'} = "$c->{'backuproot'}/$c->{'hostid'}/hourly";
+ $c->{'working'} = "$c->{'backuproot'}/$c->{'hostid'}/working";
+ $c->{'lockfile'} = "$c->{'backuproot'}/$c->{'hostid'}/syncing-now";
- $c->{'datedir'} = strftime("image-%Y-%m-%d-%H");
- $c->{'dow'} = strftime("%w");
- $c->{'chour'} = strftime("%H");
- $c->{'lday'} = strftime("image-%Y-%m-%d-%H", localtime() - 3600 * 24);
- $c->{'1week'} = strftime("image-%Y-%m-%d-%H", localtime() - 3600 * 24 * 7);
+ $c->{'datedir'} = strftime("image-%Y-%m-%d-%H");
+ $c->{'dow'} = strftime("%w");
+ $c->{'chour'} = strftime("%H");
+ $c->{'lday'} = strftime("image-%Y-%m-%d-%H", localtime() - 3600 * 24);
+ $c->{'1week'} = strftime("image-%Y-%m-%d-%H", localtime() - 3600 * 24 * 7);
- return \%c;
+ return $c;
};
sub dodir {
- for my dir (qw'weekdir daydir hourdir working') {
- if (test("! -d $c->{$dir}")) {
- run_and_check("mkdir -p $c->{$dir}");
- }
- }
+ for my $dir (qw'weekdir daydir hourdir working') {
+ if (test("! -d $c->{$dir}")) {
+ run_and_check("mkdir -p $c->{$dir}");
+ }
+ }
}
-sub doweek {
- return
- if $c->{'saveweeks'} == 0;
+sub doweek {
+ return
+ if defined $c->{'save'}{'weeks'} && $c->{'save'}{'weeks'} == 0;
- if (test("-d $c->{'daydir'}/$c->{'lweek'}")) {
- run_and_check("mv $c->{'daydir'}/$c->{'lweek'} $c->{'weekdir'}")
- } else {
- push @warnings, "I can't find a daily snapshot that is a week old...";
- }
+ if (test("-d $c->{'daydir'}/$c->{'lweek'}")) {
+ run_and_check("mv $c->{'daydir'}/$c->{'lweek'} $c->{'weekdir'}")
+ } else {
+ push @warnings, "I can't find a daily snapshot that is a week old...";
+ }
}
sub doday {
- return
- if $c->{'savedays'} == 0;
-
- if (test("-d $c->{'hourdir'}/$c->{'lday'}")) {
- run_and_check("mv $c->{'hourdir'}/$c->{'lday'} $c->{'daydir'}");
- } else {
- push @warnings, "I can't find an hourly snapshot that is 24hrs old..."
- }
+ return
+ if defined $c->{'save'}{'days'} && $c->{'save'}{'days'} == 0;
+
+ if (test("-d $c->{'hourdir'}/$c->{'lday'}")) {
+ run_and_check("mv $c->{'hourdir'}/$c->{'lday'} $c->{'daydir'}");
+ } else {
+ push @warnings, "I can't find an hourly snapshot that is 24hrs old..."
+ }
}
sub dohour {
- run_and_check("$c->{'cp'} $c->{'working'} $c->{'hourdir'}/$c->{'datedir'}");
- run_and_check("touch $c->{'hourdir'}/$c->{'datedir'}");
+ run_and_check("$c->{'cp'} $c->{'working'} $c->{'hourdir'}/$c->{'datedir'}");
+ run_and_check("touch $c->{'hourdir'}/$c->{'datedir'}");
}
sub dosync {
- if (defined $c->{'autosrcdir'}) {
- allsrcdir=`ssh ${backedhost} "ls -d /*"`
- }
-
- for srcdir in ${allsrcdir}; do
- if [ "${backedhost}" != "" ]; then
- if [ ${advexcludes} -gt 0 ]; then
- run_and_check "${rsync} ${EXCLUDES} ${backedhost}:${srcdir} ${working}"
- else
- run_and_check "${rsync} ${backedhost}:${srcdir} ${working}"
- fi
- else
- if [ ${advexcludes} -gt 0 ]; then
- run_and_check "${rsync} ${EXCLUDES} ${srcdir} ${working}"
- else
- run_and_check "${rsync} ${srcdir} ${working}"
- fi
- fi
-done
+ # See if we should backup everything...
+ my $srcdirs;
+ if (defined $c->{'autosrcdir'}) {
+ if (defined $c->{'backedhost'}) {
+ push @{ $srcdirs }, split("\n", `ssh $c->{'backedhost'} "ls -d /*"`);
+ } else {
+ push @{ $srcdirs }, split("\n", `ls -d /*`);
+ }
+ } else {
+ $srcdirs = $c->{'allsrcdir'};
+ }
+
+ my $dest = (defined $c->{'rmthost'} ? "$c->{'rmtlogin'}@$c->{'rmthost'}:" : '')
+ . $c->{'working'};
+
+ for my $dir (@{ $srcdirs }) {
+ my $srcdir = (defined $c->{'backedhost'} ? "$c->{backedhost}:" : '')
+ . $dir;
+
+ if (defined $c->{advexcludes} && $c->{advexcludes} == 1) {
+ run_and_check("$c->{'rsync'} $EXCLUDES $srcdir $dest");
+ } else {
+ run_and_check("$c->{'rsync'} $srcdir $dest");
+ }
+ }
}
+sub docleanup {
+ $c->{'save'}{'hours'} ||= 1;
+
+ my @hours = split("\n", run_and_return("ls -t $c->{'hourdir'}"));
+
+ my $count = 0;
+ for my $hour (@hours) {
+ $count++;
+
+ if ($count > $c->{'save'}{'hours'}) {
+ run_and_check("rm -Rf $c->{'hourdir'}/$hour");
+ }
+ }
+
+ if ($c->{'save'}{'weeks'} > 0 && $c->{'save'}{'days'} < 7) {
+ $c->{'save'}{'days'} = 7;
+ }
+
+ my @days = split("\n", run_and_return("ls -t $c->{'daydir'}"));
+
+ $count = 0;
+ for my $day (@days) {
+ $count++;
+
+ if ($count > $c->{'save'}{'days'}) {
+ run_and_check("rm -Rf $c->{'daydir'}/$day");
+ }
+ }
+
+ my @weeks = split("\n", run_and_return("ls -t $c->{'weekdir'}"));
+
+ $count = 0;
+ for my $week (@weeks) {
+ $count++;
+
+ if ($count > $c->{'save'}{'weeks'}) {
+ run_and_check("rm -Rf $c->{'weekdir'}/$week");
+ }
+ }
+}
+
+
# Perform a test, either via ssh or using a local perl check
sub test {
- my ($test) = shift;
-
- if (defined $c->{rmtssh}) {
- system("$c->{rmtssh} [ $test ]");
-
- return ($? >> 8);
- } else {
- return $test;
- }
+ my ($test) = shift;
+
+ if (defined $c->{rmtssh}) {
+ system("$c->{rmtssh} [ $test ]");
+
+ return ($? >> 8);
+ } else {
+ return $test;
+ }
}
-
+
# Run a command and check its return code.
sub run_and_check {
my ($cmd, $die) = shift;
- $cmd = "$c->{$rmtssh} $cmd"
- if defined $c->{rmtssh};
+ $cmd = "$c->{$rmtssh} \"$cmd\""
+ if defined $c->{rmtssh};
system($cmd);
if ($? >> 8 != 0) {
- push @errors "Command \"$cmd\" failed with return code " . ($? >> 8) . "\n";
+ push @errors, "Command \"$cmd\" failed with return code " . ($? >> 8);
+
+ die_gracefully()
+ if defined $die;
+
+ return 0;
+ }
+}
+
+# Run a command and return its output.
+sub run_and_return {
+ my ($cmd, $die) = shift;
+
+ $cmd = "$c->{$rmtssh} \"$cmd\""
+ if defined $c->{rmtssh};
+
+ my $output = `$cmd`;
+ if ($? >> 8 != 0) {
+ push @errors, "Command \"$cmd\" failed with return code " . ($? >> 8);
+
+ die_gracefully()
+ if defined $die;
+ }
+
+ return $output;
+}
+
+
+sub die_gracefully {
+ my $message = shift;
+
+ if ($#errors > 0 || $#warnings > 0 || defined $message) {
+ handle_output($message);
+ }
+
+ print "$message\n"
+ if defined $message;
+
+ exit 0;
+}
- die_gracefully()
- if defined $die;
+# Either print the status out, or email it.
+sub handle_output {
+ my $output = shift;
+ $output .= "\n"
+ if defined $output && $output ne "";
- return 0;
+ my $title = "mirror report from " . hostname;
+ my $suffix = undef;
+
+ if ($#errors > 0) {
+ $output .= "Errors:\n";
+ $output .= join("\n", @errors);
+ $output .= "\n";
+ $suffix = "ERRORS";
+ }
+
+ if ($#warnings > 0) {
+ $output .= "Warnings:\n";
+ $output .= join("\n", @warnings);
+ $suffix ||= "WARNINGS";
+ }
+
+ if (scalar(@{ $c->{'emails'} }) == 0) {
+ print "\n$output";
+ } else {
+ my $mail = MIME::Entity->build(
+ To => \@{ $c->{emails} },
+ Subject => $title . (defined $suffix ? " - $suffix" : ''),
+ Data => $output
+ );
+
+ $mail->send('sendmail');
}
}
+
+