#!/usr/bin/perl # FREP_Resync - Resynchronization daemon used as a failsafe. # Walks the defines paths, builds a file list and sends it to the clients. # # This product uses software developed by Spread Concepts LLC for use in the Spread toolkit. # For more information about Spread see http://www.spread.org # # Copyright (C) 2007 Mark Steele http://www.control-alt-del.org/code # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # use File::Find; use Spread qw(:ERROR :MESS); use strict; use File::Path; use Event::Lib; use File::Temp qw(:seekable); use POSIX qw(nice SIGCHLD SIGTERM SIGHUP); use Digest::MD5 qw(md5_hex); use Compress::Zlib; use File::Basename; use Sys::Syslog; use XML::Simple; $| = 1; nice(19); my $CONFIG; eval {$CONFIG = XMLin("/etc/FMon.conf",ForceArray => ['Dir','Pool']);}; die "Couldn't parse config: $!" if ($@); ## Configuration sanity checks for (qw(ResyncTimer)) { die("Missing configuration parameter: $_") if (!$CONFIG->{Resync}->{$_}); } die "No pools defined" if (!scalar(keys %{$CONFIG->{Pool}})); ## Go through each pool and make sure it contains an array for (keys %{$CONFIG->{Pool}}) { die "Configuration error, pools seem misconfigured" if (ref($CONFIG->{Pool}->{$_}->{Dir}) ne 'ARRAY'); } ## End sanity checking ## Build fast regular expression lookup tables to determine if an item is part of ## a specific watch group or ignore list. my %WatchRegex; my $ignore_regex; if (defined($CONFIG->{IgnoreList}->{Dir}) && ref($CONFIG->{IgnoreList}->{Dir}) eq 'ARRAY') { my $expr = join('||', map { "m#^@{$CONFIG->{IgnoreList}->{Dir}}[$_]#o" } 0..$#{$CONFIG->{IgnoreList}->{Dir}}); $ignore_regex = eval "sub { $expr }"; die "couldn't build regexes" if $@; } else { $ignore_regex = eval "sub { return 0 }"; } ########## $WatchRegex{'Admin'}->($itm) to test... foreach my $itm (keys %{$CONFIG->{Pool}}) { my $expr = join('||',map {"\$a =~ m#^@{$CONFIG->{Pool}->{$itm}->{Dir}}[$_]#o" } 0..$#{$CONFIG->{Pool}->{$itm}->{Dir}}); $WatchRegex{$itm} = eval "sub { \$a = shift; $expr }"; die "couldn't build regexes" if $@; } ## Unique folder list my (%DIRS,%FH); foreach my $group (keys %{$CONFIG->{Pool}}) { for (@{$CONFIG->{Pool}->{$group}->{Dir}}) { if (!-d) { eval {mkpath($_,0755);}; if ($@) { die "error creating path: $!"; } } $DIRS{$_} = 1; } } ## Setup signal handler for SIGHUP to start resync my $sig_hup = signal_new(SIGHUP,\&resync); $sig_hup->add(); ## Resync every 300 seconds my $timer_resync = timer_new(\&resync); $timer_resync->add($CONFIG->{Resync}->{ResyncTimer}); ## Update time left in process list my $timer_stats = timer_new(\&display_stats); $timer_stats->add(2); debug_log("Entering main loop"); event_mainloop(); sub find_watchgroup { my @groups; my $itm = shift; for (keys %{$CONFIG->{Pool}}) { push(@groups,$_) if ($WatchRegex{$_}->($itm)); } return @groups; } sub build_list { $_ ||= shift; if (&$ignore_regex) { undef $_; return; } if (-d) { ## Got directory foreach my $group (find_watchgroup($_ . '/')) { print {$FH{$group}} "DIR:$_\n"; } } elsif (-f && !-z) { foreach my $group (find_watchgroup($_)) { print {$FH{$group}} join(":",(scalar(fileparse($_)),(stat($_))[7,9])),"\n"; } } } sub resync { my $e = shift; my $type = shift; $0 = "FREP_Resync => Resync in progress"; debug_log("Starting resync"); if ($type == EV_TIMEOUT) { ## Timer caused us debug_log("Resync caused by timeout"); $e->add($CONFIG->{Resync}->{ResyncTimer}); } else { ## From a sighup, reset the timer debug_log("Resync caused by SIGHUP"); $timer_resync->remove; $timer_resync->add($CONFIG->{Resync}->{ResyncTimer}); } ## Remove old resync files if any `rm /tmp/*.resync`; ## Cleanup file handles undef %FH; File::Temp::cleanup(); ## Open up a filehandle for each watch group. for (keys %{$CONFIG->{Pool}}) { $FH{$_} = new File::Temp(SUFFIX => '.resync'); if (!$FH{$_}) { debug_log("Error creating temp file: $!"); return; } } debug_log("Filehandles open for each group"); ## Loop through unique directory listing and create a list of all files for each group. debug_log("Walking file tree"); for (keys %DIRS) { find({ wanted => \&build_list, follow => 1, no_chdir => 1}, $_); } debug_log("Finished walking tree"); ## Connect to spread my ($mbox, $private_group) = Spread::connect({ spread_name => $CONFIG->{Spread}->{Port}.'@'.$CONFIG->{Sperad}->{Address},private_name => "$$-".int(rand(100)) }); if (!defined($mbox)) { debug_log("Ack no mailbox, bailing"); return; } for (keys %{$CONFIG->{Pool}}) { send_sync_file($mbox,$FH{$_},$_); } Spread::disconnect($mbox); debug_log("Resync finished"); } sub debug_log { return if (!$CONFIG->{Resync}->{Debug}); openlog($0, 'cons,pid', 'user'); syslog('debug', '%s', shift); closelog(); } sub md5_file { my $file = shift; my $digest = Digest::MD5->new->addfile($file)->hexdigest; close(FILE); return $digest; } sub send_sync_file { my ($mbox,$fh,$group) = @_; my $filesize = (stat($fh->filename))[7]; $fh->seek(0,0); my $sig = md5_file($fh); $fh->seek(0,0); my $cookie = time . int(rand(1000)); my ($chunk,$lastchunk); debug_log("Sending to $group ".$fh->filename.": $sig"); while (my $t = read($fh, my $buf,90000)) { if (!defined($t)) { return if $! =~ /^Interrupted/; debug_log("ACK: system read error: $!"); return; } if (eof($fh)) { $lastchunk = "LastChunk:1\n"; } my $sendbuf = compress($buf); if (!$sendbuf) { debug_log("Ack error compressing buffer"); return; } $chunk++; my $r = Spread::multicast($mbox,SAFE_MESS|SELF_DISCARD,"Resync-$group",0,"Command:Sync\nChunk:$chunk\n".$lastchunk."Cookie:$cookie\nFileSig:$sig\n__DATA__\n$sendbuf"."__DATAEND__"); if ($r < 0) { if ($r == ILLEGAL_SESSION) { debug_log("Ack: mailbox no good! (maybe disconnected?)"); return; } elsif ($r == ILLEGAL_MESSAGE) { debug_log("Ack: the message had an illegal structure! (oops)"); return; } elsif ($r == CONNECTION_CLOSED) { debug_log("Ack: spread closed!"); return; } else { debug_log("Ack: dunno whats wrong"); return; } } } } sub display_stats { my $e = shift; $e->add(2); $0 = "FREP_Resync => next resync: " . int($timer_resync->pending) . " seconds"; }