#!/usr/bin/env perl use warnings; use strict; # Performs multiple types of rsync backups from local machine to remote backup server # now, see what kind of controls we have. If %CONTROL is empty, only @options will be used. # however, if %OPTIONS has values, the key is the flag to be passed to rsync, and # the value is a reference to a function which returns true or false. Actually, the function # can return any value, but it is evaluated as a perl logical expression. # NOTE: using this schema, you can not have multiple entries for any flags to be passed, ie you # MUST have each flag/group of flags only one time. Also, it is your responsibility to ensure # valid flag setups: use the $TEST_CONDITIONS flag above to show output instead of executing # Examples follow: # always adds --dry-run, using an anonymous hash # $CONTROL{'--dry-run'} = sub { return 1; }; # using a sub, and a reference to it, do checksum processing on first Sunday of month # sub firstOfMonth { # my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); # return $wday == 0 && $mday < 7; # day of week is Sunday, and day of month < 7 # } # $CONTROL{'--cksum'} = \&firstOfMonth; # add reference to above routine # # Only perform chksum on saturdays # $CONTROL{'--cksum'} = sub{my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); return $wday == 6; } ; # Requires: # IO::Zlib (found in libio-zlib-perl under Debian, perl-IO-Zlib.noarch under Fedora) # Version 0.6.0 20110214 - RWR # Moved standard library routines to rsbackup.pm, and modified the installer to install that library. # This library contains one standard location for all shared routines. # Version 0.5.0 20100602 - RWR # Modified to allow the user to define the target path explicitly, and also allow executing a command on the remote # machine to do things like clean up and/or get statistics. # Version 0.4.1 20090317 - RWR # modified to allow $GZIP to be something that creates file other than .gz file. This was done due to requirement by one # client for a zip format. I created a bash script that did a zip compress, but needed to change the extension because # .gz was hard coded into the app (yes, i know, i know) # Version 0.4.0 20081212 - RWR # added ability for $INITIALIZATION_SCRIPT and $CLEANUP_SCRIPT to send output to stdout, which is captured by application # and sent to stdout (ie, the summary of the backup). Also, added some additional spec's to the summary # Version 0.5.1 20110101 - RWR # Modified to NOT require executing to be called from the configuration file. Script will automatically execute a 'prepare' # command prior to running rsync and, on completion of the job, will run a 'cleanup' command. # format of the commands will be a space separated list starting with the 'prepare' or 'cleanup' literal, # followed by the client name and the machine name. On receiving prepare and cleanup, the server will execute the commands # assigned that machine. # Version 1.0.0 20161119 - RWR BEGIN { use File::Basename; use Cwd 'abs_path'; push @INC, abs_path( dirname(__FILE__) ); push @INC, abs_path( dirname(__FILE__) ) . '/../'; push @INC, abs_path( dirname(__FILE__) ) . '/../rsbackup_lib'; } require 'rsbackup.pm'; # standard package library use Getopt::Long; use File::Path qw(make_path); use Data::Dumper; my %configuration; # following variables to be overridden by configuration file # boolean to not run command, just prints command that would be executed. # also, at level 3, will not use standard configurations, but will use configuration file ../../etc/rsbackup/rsbackup.conf # for development my $TEST_CONDITIONS = 0; # who gets backup logs my $MAIL_TO = 'backups@example.com'; # full server name/IP and path to backup server, suitable for pasting into the rsync command # NOTE: the default assumes the user is root, the server is named backup in the same domain as this machine # and the target path on the server is /home/backups directory my $BACKUP_SERVER = 'root@backup.example.com'; # full server name to back up to (or IP) my $PATH_ON_SERVER = '/home/backups'; # root path on the server # the below is used to determine the subdirectory on the backup server that is used, and also used to in the # e-mail report. my $MY_NAME = 'server.example.com'; # used to determine target subdir for storage. # The client name. Used to determine the backup subdirectory on the remote machine and also used in the e-mail # report. Do not use spaces in here. my $CLIENT_NAME = 'ClientName'; # THIS IS THE ACTUAL PATH USED. It is generally used by calculating from variables above. Note that it can be # used for other purposes also, such as a root directory for versions. my $BACKUP_PATH = "$BACKUP_SERVER:$PATH_ON_SERVER/$CLIENT_NAME/$MY_NAME/backup"; # The following list determines the directories on THIS MACHINE that are to be backed up. NOTE: During # upgrades, this entry is assumed to be on one line, upgrade may corrupt this entry if you write it # as a multi-line. my @DIRS_TO_BACKUP = ('/etc', '/root', '/home'); # which directories on source are to be backed up # where to store the logs on this machine. NOTE: log files are compressed and stored, and are not removed # by rsync_backup. The default is to place them in /root/backup_logs, so they are actually backed up # then next day also. my $LOGDIR = '/root/backup_logs'; # log directory, either relative to script or absolute # if the following scripts are set, they are run at the appropriate time # the scripts MUST return a null on stdio to indicate success. Any output on stdio is considered # an error message and the backup is halted immediately with the error being emailed to the # account defined in the cron file. # see additional information with some pre-written scripts in the examples directory of the docs folder my $INITIALIZATION_SCRIPT = ''; # optional script to execute immediately after configuration file read my $CLEANUP_SCRIPT = ''; # optional script to clean up when backup done my $EXCLUDE_FILE = '/etc/rsync.exclude'; # location of file containing file patterns to be excluded my $RSYNC; # full path to rsync executable my $GZIP; my $MPACK; my $COMPRESSED_FILE_EXTENSION = 'gz'; # extension for the compressed file (gz for gzip, zip for zip) my @OPTIONS; # List of command line flags to be passed to rsync my %CONTROL; # options flags and conditions for using them. See notes above my $MAILER_SCRIPT; # option used if a mailer script is used my $MAIL_FROM; # name of sender for e-mail my $MAIL_SERVER; # name of mail server to use my $TEMP_FILE = '/tmp/rsbackup.tmp'; # name of some temp file that can be overwritten at will my $PREPARE_COMMAND = 'prepare'; my $CLEANUP_COMMAND = 'cleanup'; # Global Variables my $MY_PATH; my $VERSION = '1.0.0'; my $START_TIME; my $END_TIME; my $body; my $logFile; my $rsyncReturnCode; my $USAGE; my $remoteOutput = ''; my $testConfigurationFile; sub buildPrepCommands { my $rsyncCommand = shift; my $prepareCommand = qq/'$PREPARE_COMMAND $CLIENT_NAME $MY_NAME'/; my $cleanupCommand = qq/'$CLEANUP_COMMAND $CLIENT_NAME $MY_NAME'/; my $sshCommand = 'ssh -p 22 '; if ( $rsyncCommand =~ m/(ssh -p \d+)/ ) { $sshCommand = $1; } $sshCommand .= " $BACKUP_SERVER "; return ("$sshCommand $prepareCommand", "$sshCommand $cleanupCommand"); } GetOptions( 'testlevel:i' => \$TEST_CONDITIONS, 'configuration:s' => \$testConfigurationFile, 'help!' => \$USAGE ); if ($USAGE) { print "rsbackup [--testlevel={0|1|3}] [--configuration=configurationFileName]\n"; print "--testlevel\n\t0 (normal run)\n\t1 (show commands but do not run)\n"; print "--testconf\n\tconffile (use alternate conf file, bypasses all configuration file settings)\n"; exit 0; } my $configuration = &loadConfigurationFile(); die "No Configuration file found" unless $configuration; # load configuration file into memory and die if it is invalid eval( $configuration ) or die "Error in configuration file. Execute perl -c conffile for all copies"; # if utilities were found with which command, they have trailing \n chomp($RSYNC); chomp($GZIP); chomp($MPACK); # if we can't find rsync, there is no use in continuing die "Could not execute rsync at $RSYNC" unless -x $RSYNC; my $scriptResults; # this will store any output from the initialization / cleanup scripts so they can be returned in summary my $scriptReturnCode; # stores the return code of the scripts called. # If there is an initialization script, run it print "Initialization Script set to [$INITIALIZATION_SCRIPT]\n" if $TEST_CONDITIONS; if ( $INITIALIZATION_SCRIPT ) { ($scriptReturnCode, $scriptResults) = &runScript( $INITIALIZATION_SCRIPT ); if ( $scriptReturnCode == 1 ) { # any return code other than 1 indicates an error print "Initialization Script Failed. Output Follows\n"; print "$scriptResults\n"; exit 1; } else { $scriptResults = "Initialization Script Output\n============================\n$scriptResults" if $scriptResults; } } # looks like we are ok, so start processing. # record the start time $START_TIME = &dateTimeStamp('u'); # find the location where we are supposed to put the logs. $LOGDIR = "$MY_PATH/$LOGDIR" unless $LOGDIR =~ m/^\//; # prepend script name to log directory unless it is absolute mkdir $LOGDIR unless -d $LOGDIR; # create it if it doesn't exist. # now, create a log file name based on the date. Name will be: # YYYYMMDD_computername.backup.log in $LOGDIR $logFile = "$LOGDIR/" . &dateTimeStamp('ymd',$START_TIME) . "_$MY_NAME.backup.log"; # if exclude file exists, add the exclude directory push @OPTIONS, "--exclude-from=$EXCLUDE_FILE" if $EXCLUDE_FILE and -e $EXCLUDE_FILE; # determine if any additional flags need to be used, ie checksum or dryrun while (my ($value, $function ) = each(%CONTROL)){ print "Checking Function for $value " . (&$function ? "Yes" : "No") . "\n" if $TEST_CONDITIONS; push @OPTIONS, $value if &$function; } # we are now ready to begin the actual backup. open OUTPUT,">>$logFile" or die "Could not append to $logFile"; print OUTPUT "Backing up " . join (' ', @DIRS_TO_BACKUP) . " to $BACKUP_SERVER at $START_TIME\n"; print OUTPUT "backup v$VERSION\n"; close OUTPUT; print "Backing up " . join (' ', @DIRS_TO_BACKUP) . " to $BACKUP_SERVER at $START_TIME\n" if $TEST_CONDITIONS; # build the final copy command my $command = join( ' ', ($RSYNC, join( ' ', @OPTIONS) , join( ' ', @DIRS_TO_BACKUP), "'$BACKUP_PATH/' >> $logFile") ); # build the prepare and cleanup commands. Note it gets the ssh parameters from that command my ($prepareCommand, $cleanupCommand) = &buildPrepCommands( $command ); if ( $TEST_CONDITIONS ) { print "$prepareCommand\n"; print "$command\n"; print "$cleanupCommand\n"; } else { $remoteOutput = qx/$prepareCommand/; qx/$command/; $rsyncReturnCode = $?; $remoteOutput .= qx/$cleanupCommand/; } $END_TIME = &dateTimeStamp('u'); print "Cleanup Script set to [$CLEANUP_SCRIPT]\n" if $TEST_CONDITIONS; if ( $CLEANUP_SCRIPT ) { my $temp; ($scriptReturnCode, $temp) = &runScript( $CLEANUP_SCRIPT ); if ( $scriptReturnCode == 1 ) { # any return code other than 1 indicates an error print "Cleanup Script Failed. Output Follows\n"; print "$temp\n"; } else { $scriptResults .= "\nCleanup Script Output\n============================\n$temp\n" if $temp; } } $body = "\n\nBackup Version $VERSION\nComputer: $CLIENT_NAME - $MY_NAME\nCompleted Synchronization\nBegin " . &dateTimeStamp('t',$START_TIME) . " [$START_TIME]\nComplete " . &dateTimeStamp('t',$END_TIME) . " [$END_TIME]\n"; $body .= "Duration " . &time2HHMMSS($END_TIME-$START_TIME) . ' [' . ($END_TIME-$START_TIME) . " seconds]\n"; open my $fh, "<$logFile" or die "Could not open $logFile: $!"; $body .= &summarizeRsyncLog($fh, $rsyncReturnCode ); close $fh; $body .= $remoteOutput ? $remoteOutput : 'No Remote Output'; $body .= "\n\nOutput of Initialization and/or Cleanup Scripts\n\n$scriptResults" if $scriptResults; open OUTPUT,">>$logFile" or die "Could not append to $logFile"; print OUTPUT $body; close OUTPUT; &sendLogs ( &compressFile($logFile), $MAIL_TO, $body, $TEST_CONDITIONS, $MAILER_SCRIPT, ( $MAIL_FROM ? $MAIL_FROM : 'root@' . $MY_NAME ), 'localhost', "Backup for $MY_NAME, " . &dateTimeStamp($START_TIME) ); 1;