File System Update Daemon for synchronizing files between 2 RHAS 4 Apache Servers
File locations:
/var/log/fsupd.webmaster
/etc/fsupd.conf
/etc/rc.d/init.d/fsupd
/usr/local/bin/fsupd.pl
/usr/local/bin/fsupd.pl contains:
#!/usr/bin/perl
use strict;
use POSIX qw(setsid);
use Getopt::Std qw(getopts);
########## BOOTSTRAP ##########
#
# Read the config file
# Check logging directory
# Check user and demote?
# Start logging
# Daemonize
# Author: Thaddeus Hogan
# Get options
my $opts = {};
getopts('d', $opts);
my $debug = $opts->{'d'};
print "Debug output on.n" if $debug;
# Find the config file using the following list in order
my @cfgPaths = (
'fsupd.conf', # Check working directory
'/etc/fsupd.conf' # Check /etc
);
print "Scanning for config location.n" if $debug;
my $cfgPath = undef;
foreach my $c (@cfgPaths) {
print "Checking for config at: $cn" if $debug;
if (-r $c) {
print "Found config at: $cn" if $debug;
$cfgPath = $c;
last;
}
}
if (! $cfgPath) {
print "fsupd Unable to locate configuration file. Terminating.n";
exit 1;
}
# Config file found, parse
my $scanPaths = {};
my $logdir = '/var/log/fsupd';
my $runas = undef;
my $logfile = 'fsupd.log';
my $pidfile = 'fsupd.pid';
open CONF, $cfgPath;
my @cfg = <CONF>;
close CONF;
my $cfgParseMode = 1; # Parse mode 1 = pre-scandefs, 2 = scandefs
my $cscanPath = {}; # Current scan path
foreach my $line (@cfg) {
if ($cfgParseMode == 1) {
$logdir = $1 if $line =~ /logdirs*=s*(.*)/;
$runas = $1 if $line =~ /runass*=s*(.*)/;
$logfile = $1 if $line =~ /logfiles*=s*(.*)/;
$pidfile = $1 if $line =~ /pidfiles*=s*(.*)/;
$cfgParseMode = 2 if $line =~ /^[/;
}
if ($cfgParseMode == 2) {
if ($line =~ /[([^]]*)]/) {
$scanPaths->{$1} = {};
$cscanPath = $scanPaths->{$1};
}
if ($line =~ /(w+)s*=s*(.+)/) {
$cscanPath->{$1} = $2;
}
}
}
my $logpath = "$logdir/$logfile";
# Print out configuration if debug is set
if ($debug) {
print "Config:n";
print "logdir = $logdirn";
print "logfile = $logfilen";
print "runas = $runasn";
foreach my $scanPath (keys %{ $scanPaths }) {
print "Scan Path: $scanPathn";
foreach my $param (keys %{ $scanPaths->{$scanPath} }) {
print "t$param = $scanPaths->{$scanPath}->{$param}n";
}
}
print "n";
}
# Create logging directory if it does not exist
if (! -d $logdir) {
print "Log directory $logdir does not exist, will attempt to create.n" if
$debug;
if (! mkdir($logdir)) {
print "Could not create logging directory: $logdirn";
print "$!n";
print "Terminating.n";
exit 1;
}
}
# Demote daemon if requested
if ($runas) {
print "Preparing to demote process to user: $runasn" if $debug;
# Make sure user can demote
if ($> != 0) {
print "fsupd not run as root, cannot demote to $runas.n";
print "Terminating.n";
exit 1;
}
# Get UID for named user, make sure it exists
my $runasUID = getpwnam($runas);
print "runasUID = $runasUIDn" if $debug;
if (! $runasUID) {
print "fsupd requested to run as $runas but user does not
exist.n";
exit 1;
}
# Make sure the log directory is owned by the requested user
chown $runasUID, -1, $logdir;
if (-e $logpath) {
chown $runasUID, -1, $logpath;
}
# Demote the process
$> = $runasUID;
print "Process demoted to $runas.n" if $debug;
}
# Start logging
my $logfh = undef;
open $logfh, ">>$logpath";
select((select($logfh), $| = 1)[0]); # Make it hot
# Daemonize
&log("fsupd startup successful, daemonizing");
my $pid = fork;
if ($pid) {
&log("daemon process is $pid");
print "daemon process is $pidn" if $debug;
open PIDF, ">$logdir/$pidfile";
print PIDF $pid;
close PIDF;
exit;
}
open STDOUT, "/dev/null";
open STDERR, "/dev/null";
setsid();
######### BUILD MONITOR CHAIN ##########
# Run through the scan paths and construct the monitor chain
# This will be used in a state pump (shift, process, push)
my @monitors = ();
foreach my $scanPath (keys %{ $scanPaths }) {
# Sanity check
if (! -d $scanPath) {
&log("scan path not directory, excluding: $scanPath");
next;
}
if (! -r $scanPath) {
my $uname = getpwnam($>);
&log("scan path not readable by $uname($>), excluding: $scanPath");
next;
}
# Create monitor
my $monitor = {
path => $scanPath,
nextscan => 0,
on_update => $scanPaths->{$scanPath}->{'on_update'},
scanfactor => $scanPaths->{$scanPath}->{'scanfactor'},
scanmin => $scanPaths->{$scanPath}->{'scanmin'},
lastscantime => 0,
evtUpdatePending => 0,
items => {},
excludes => []
};
&log("added scan path: $scanPath");
my @excludes = split /|/, $scanPaths->{$scanPath}->{'excludes'};
foreach my $e (@excludes) {
push @{ $monitor->{'excludes'} }, $e;
&log("excluding on pattern: $e");
}
push @monitors, $monitor;
}
# Always have string 'sleep' in monitor array, the pump below will snooze
# when it encounters this
push @monitors, 'sleep';
########## MAIN MONITOR LOOP ##########
while (1) {
my $monitor = shift @monitors;
# Check for sleep monitor
if ($monitor eq 'sleep') {
sleep 1;
push @monitors, 'sleep';
next;
}
# See if it is time to process this monitor
if (time >= $monitor->{'nextscan'}) {
# Scan the path, start time measure
&log("scanning $monitor->{'path'}") if $debug;
my $st = time;
my $lastScan = $monitor->{'items'};
$monitor->{'items'} = &scanpath($monitor->{'path'},
$monitor->{'excludes'});
# Variables for each event
my $evt_adorm = 0; # added or removed
my $evt_chmod = 0;
my $evt_chown = 0;
my $evt_chgrp = 0;
my $evt_chsiz = 0;
my $evt_chatm = 0;
my $evt_chmtm = 0;
my $evt_chctm = 0;
# Only do the compare if this isn't the first scan
if (scalar(keys(%{ $lastScan }))) {
# Check for updates against the last scan
my @oldFiles = keys %{ $lastScan };
my @newFiles = keys %{ $monitor->{'items'} };
# See if any files were added or removed
if ( (scalar(@oldFiles) != scalar(@newFiles)) || (!
@oldFiles and @newFiles) ) {
&log("$monitor->{'path'} detected files added or
removed");
$evt_adorm = 1;
}
# Test for attribute changes
foreach my $oldFileName (@oldFiles) {
my $oldFile = $lastScan->{$oldFileName};
my $newFile = $monitor->{'items'}->{$oldFileName};
if (! $newFile) {
&log("File $oldFileName not in new set") if
$debug;
next;
}
if ($oldFile->{'mode'} != $newFile->{'mode'}) {
&log("$oldFileName detected mode change")
if $debug;
$evt_chmod = 1;
}
if ($oldFile->{'uid'} != $newFile->{'uid'}) {
&log("$oldFileName detected uid change") if
$debug;
$evt_chown = 1;
}
if ($oldFile->{'gid'} != $newFile->{'gid'}) {
&log("$oldFileName detected gid change") if
$debug;
$evt_chgrp = 1;
}
if ($oldFile->{'size'} != $newFile->{'size'}) {
&log("$oldFileName detected size change")
if $debug;
$evt_chsiz = 1;
}
if ($oldFile->{'atime'} != $newFile->{'atime'}) {
&log("$oldFileName detected access time
change") if $debug;
$evt_chatm = 1;
}
if ($oldFile->{'mtime'} != $newFile->{'mtime'}) {
&log("$oldFileName detected modification
time change") if $debug;
$evt_chmtm = 1;
}
if ($oldFile->{'ctime'} != $newFile->{'ctime'}) {
&log("$oldFileName detected i-node change")
if $debug;
$evt_chctm = 1;
}
}
}
# Composite events
my $evt_update = $evt_chmod || $evt_chown || $evt_chgrp ||
$evt_chsiz || $evt_chmtm || $evt_adorm;
# Capture the elapsed time as soon as we finish
my $et = time - $st;
# Calculate the next scan time
# elapsed time * scan factor OR scanmin, whichever is smaller
my $nextscan = $et * $monitor->{'scanfactor'};
$nextscan = $monitor->{'scanmin'} if $nextscan <
$monitor->{'scanmin'};
$monitor->{'nextscan'} = time + $nextscan;
$monitor->{'lastscantime'} = $et;
&log("scan time for $monitor->{'path'} was $et sec") if $debug;
&log("next scan in T+$nextscan") if $debug;
# Finally, execute requested operations for any events that occured
if ($evt_update) {
$monitor->{'evtUpdatePending'} = 1;
&log("$monitor->{'path'} update detected, pending
stablization for event") if $debug;
}
if ($monitor->{'evtUpdatePending'} > 0 and
$monitor->{'evtUpdatePending'} < 3) {
$monitor->{'evtUpdatePending'}++;
} elsif ($monitor->{'evtUpdatePending'} >= 3) {
&log("$monitor->{'path'} updates appear stable");
&log("exec $monitor->{'on_update'}");
system($monitor->{'on_update'});
$monitor->{'evtUpdatePending'} = 0;
}
}
# Return the monitor to the end of the queue
push @monitors, $monitor;
}
########## FILESYSTEM SCAN FUNCTIONS ##########
# scanpath(pathToScan)
# scans specified path and returns hash where keys are path items
# and contents are file stats
sub scanpath {
my ($path, $excludes) = @_;
opendir DIR, $path;
my @files = grep(!/^..?$/, readdir(DIR));
closedir DIR;
my $stats = {};
foreach my $f (@files) {
my $fp = "$path/$f";
if ($excludes) {
my $excluded = 0;
foreach my $ex (@{ $excludes }) {
if ($fp =~ /$ex/) {
&log("excluded $fp on /$ex/") if $debug;
$excluded++;
last;
}
}
next if $excluded;
}
my @stat = stat($fp);
if (! @stat) {
&log("stat failed, invalidating scan: $path");
return 0;
}
$stats->{$fp} = {};
$stats->{$fp}->{'mode'} = $stat[2];
$stats->{$fp}->{'uid'} = $stat[4];
$stats->{$fp}->{'gid'} = $stat[5];
$stats->{$fp}->{'size'} = $stat[7];
$stats->{$fp}->{'atime'} = $stat[8];
$stats->{$fp}->{'mtime'} = $stat[9];
$stats->{$fp}->{'ctime'} = $stat[10];
if (-d $fp) {
$stats = {%{ $stats }, %{ &scanpath($fp) }};
}
}
return $stats;
}
########## UTILITY FUNCTIONS ##########
# Log sub logs a message and makes sure that the filehandle is still valid
sub log {
my ($msg) = @_;
my ($sec, $min, $hour, $day, $mon, $year) = localtime(time);
$year += 1900;
$mon += 1;
$mon = "0$mon" if $mon < 10;
$day = "0$day" if $day < 10;
$hour = "0$hour" if $hour < 10;
$min = "0$min" if $min < 10;
$sec = "0$sec" if $sec < 10;
if (! -e $logpath) {
print "Log file disappeared! Attempting to re-open.n" if $debug;
close $logfh;
open $logfh, ">>$logpath" or print "Log re-open failed: $!";
}
seek $logfh, 0, 1;
print $logfh "[$year-$mon-$day $hour:$min:$sec] $msgn";
}
/etc/fsupd.conf contains:
logdir = /var/log/fsupd.webmaster
[/var/www/html]
on_update = rsync -a --delete --exclude=/logs --exclude=.* /var/www/html/
mnsvlwwwt001:/apps/apache2/htdocs/
scanfactor = 3
scanmin = 4
excludes = /var/www/html/logs|/.
/etc/rc.d/init.d/fsupd contains:
#!/bin/bash
#
# fsupd System init script for the fsupd daemon
#
# chkconfig: - 99 10
# description: File System Update Daemon
FSUPD=/usr/local/bin/fsupd.pl
PIDFILE=/var/log/fsupd.webmaster/fsupd.pid
start() {
su - webmaster -c "$FSUPD -d"
}
stop() {
kill `cat $PIDFILE`
rm $PIDFILE
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
sleep 1
start
;;
*)
echo "Usage: fsupd {start | stop | restart}"
exit 1
esac