Rev 21 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
#! /usr/bin/env perl
use strict;
use warnings;
BEGIN {
use FindBin;
use File::Spec;
# use libraries from the directory this script is in
use Cwd 'abs_path';
use File::Basename;
use lib dirname( abs_path( __FILE__ ) );
}
use YAML::Tiny; # pkg install p5-YAML-Tiny-1.74
use Data::Dumper;
my $cwd = $FindBin::RealBin;
my $configFileName = $cwd . '/sync.yaml';
my $replicateScript = $cwd . '/replicate';
my $configuration;
# load Configuration File
# read the config file and return it
sub readConfig {
my $filename = shift;
die "Config file $filename not found: $!" unless -f $filename;
my $yaml = YAML::Tiny->new( {} );
if ( -f $filename ) {
$yaml = YAML::Tiny->read( $filename );
}
return $yaml->[0];
}
sub logit {
open LOG, ">>/tmp/replicate.log" or die "Could not open replicate.log: $!\n";
print LOG join( "\n", @_ ) . "\n";
close LOG;
}
# this calls gshred which will overwrite the file 3 times, then
# remove it.
# NOTE: this will not work on ZFS, since ZFS is CopyOnWrite (COW)
# so assuming file is on a ramdisk
sub shredFile {
my $filename = shift;
if ( `which gshred` ) {
`/usr/local/bin/gshred -u -f $filename`;
} else {
warn "gshred not installed, simply deleting $filename\n";
unlink $filename;
}
}
# runs a command, redirecting stderr to stdout (which it ignores)
# then returns 0 on success.
# if error, returns string describing error
sub runCommand {
my $command = shift;
#logit( $command );
my $output = qx/$command 2>&1/;
if ($? == -1) {
return (-1, "failed to execute: $!" );
} elsif ($? & 127) {
return (-1,sprintf "child died with signal %d, %s coredump",
($? & 127), ($? & 128) ? 'with' : 'without');
} else {
return ( $?>>8, sprintf "child exited with value %d", $? >> 8 ) if $? >> 8;
}
return (0, $output);
}
# Checks if a zpool is available. If not, retrieves the geli key from a remote server,
# decrypts the drives, and mounts the zpool.
# Params:
# $zpool - Name of the zpool
# $local_key - Local path to store the geli key
# $remote_server - Remote server to fetch the key from (user@host)
# $remote_key - Path to the geli key on the remote server
# $drives_ref - Arrayref of drives to decrypt (e.g., ['/dev/ada0p3', '/dev/ada1p3'])
sub mountGeliZpool {
my ($zpool, $local_key, $remote_server, $remote_key, $drives_ref) = @_;
# Check if the zpool is available
my ($error, $output) = runCommand("zpool list $zpool");
return 0 unless $error; # zpool is available
#die "zpool=$zpool\nlocalKey=$local_key\nserver=$remote_server\nremotekey=$remote_key\n" . Dumper( $drives_ref ) . "\n";
# Retrieve geli key from remote server
($error, $output) = runCommand("scp $remote_server:$remote_key $local_key");
return ($error, "Failed to retrieve geli key from $remote_server:$remote_key" ) if $error;
# Attach geli key to each drive
foreach my $drive (@$drives_ref) {
($error, $output) = runCommand("geli attach -k $local_key -p $drive");
return ($error, "Failed to attach geli key to $drive: $output" ) if $error;
}
# Import the zpool
($error, $output) = runCommand("zpool import $zpool");
return ( $error, "Failed to import zpool $zpool: $output" ) if $error;
# Optionally, mount all datasets in the zpool
($error, $output) = runCommand("zfs mount -a");
return( $error,"Failed to mount datasets in zpool $zpool: $output" ) if $error;
# Shred the key file after use
shredFile($local_key) if -f $local_key;
return 1;
}
# a very simple mailer, just send information to sendmail
sub sendMail {
my ($message, $configuration, $subject ) = @_;
if ( $message ) {
open MAIL,"|sendmail -t" or die "Could not open sendmail: $!\n";
print MAIL "To: $configuration->{email}->{notify}\n";
print MAIL "From: $configuration->{email}->{from}\n";
print MAIL "Subject: " .
($configuration->{'email'}->{'subject'} . ( $subject ? " - $subject" : '' ) ) .
"\n\n";
print MAIL $message;
close MAIL;
} else {
warn "no message in outgoing email\n";
}
}
# checks to see if we should be in maintenance mode
# if $remoteMachine->{'maintenanceMode'} exists, set mode
# otherwise, wait localMachine->{'waittime'} minutes, then check
# $localMachine->{'maintenanceMode'}.
# if neither exists, begin sync
sub checkMaintenance {
my $configuration = shift;
return 0 unless # exit if maintenanceFlag has not been set at all
( defined( $configuration->{'target'}->{'maintenanceFlag'} ) && $configuration->{'target'}->{'maintenanceFlag'} ) ||
( defined( $configuration->{'source'}->{'maintenanceFlag'} ) && $configuration->{'source'}->{'maintenanceFlag'} );
# see if maintenance is set on remote. If so, simply return the message
if ( $configuration->{'source'}->{'up'} ) {
my ($error, $output) = &runCommand( "ssh $configuration->{remoteMachine}->{ip} 'ls $configuration->{remoteMachine}->{maintenanceFlag}'" );
if ( ! $error ) {
# remove the file from the remote server
&runCommand( "ssh $configuration->{remoteMachine}->{ip} 'rm $configuration->{remoteMachine}->{maintenanceFlag}'" );
# create a valid return, which will exit the program
return "Maintenance Flag found on remote machine";
}
}
# not on remote machine, so give them waitTime seconds to put it here
# we'll loop, checking every $sleepTime seconds until our wait time
# ($configuration->{'target'}->{'waitTime'}) has expired
my $sleepTime = 60; # time between checks
# default one minute if waitTime not set
$configuration->{'target'}->{'waitTime'} = 60 unless $configuration->{'target'}->{'waitTime'};
for ( my $i = $configuration->{'target'}->{'waitTime'}; $i > 0; $i -= $sleepTime ) {
sleep $sleepTime;
# then look for the maintenance flag file on the local machine
return "Maintenance Flag found on local machine" if -f $configuration->{'target'}->{'maintenanceFlag'};
}
# no maintenance flags found, so return false
return 0;
}
sub shutdownMachine {
my $configuration = shift;
exit unless $configuration->{'shutdown'};
# do not actually shut down the server unless we are told to
&runCommand( "poweroff" ) unless $configuration->{'testing'};
}
# returns the current time as a string
sub currentTime {
my $format = shift;
# default to YY-MM-DD HH-MM-SS
$format = '%Y-%m-%d %H:%M:%S' unless $format;
use POSIX;
return POSIX::strftime( $format, localtime() );
}
# verify a remote machine is up and running
sub checkRemoteUp {
my $configuration = shift;
my $ip;
if ( defined( $configuration->{'target'}->{'server'} ) && $configuration->{'target'}->{'server'} ) {
$ip = $configuration->{'target'}->{'server'};
} else {
$ip = $configuration->{'source'}->{'server'};
}
my ($error, $message ) = $ip ? &runCommand( "ping -c 1 -t 5 $ip" ) : (0,'No address defined for either target or server' );
# $message = "Checking IP $ip\n" . $message;
#die "error is $error, message is $message for $ip\n";
return ($error, $message);
}
sub updateStats {
my ( $label, $filename, $output ) = @_;
if ( $output =~ m/bytes\t(\d+).*seconds\t(\d+)/gms ) { # global, multiline, . matches newlines
my $seconds = $2;
my $bytes = $1;
open STATS,">>$filename" or warn "Could not create file $filename: $!\n";
print STATS ¤tTime('') . "\t$label\t$seconds\t$bytes\n";
close STATS
} else {
warn "updateStats called with invalid report\n" if $configuration->{'verbose'}>1;
}
}
my @status;
my $error = 0;
my $output = '';
$configuration = &readConfig($configFileName);
# die Dumper( $configuration ) . "\n";
my $servername = `hostname`;
chomp $servername;
if ( $configuration->{'verbose'} > 1 ) {
push @status, "Replication on $servername has been started at " . ¤tTime();
&sendMail( "Replication on $servername has been started, " . ¤tTime(), $configuration, "Replication on $servername started" );
}
# see if remote machine is up by sending one ping. Expect response in 5 seconds
( $error,$output) = &checkRemoteUp( $configuration );
$configuration->{'up'} = ! $error;
push @status, "remote machine is " . ( $configuration->{'up'} ? 'Up' : 'Down' ) . "\n";
if ( ! $configuration->{'up'} ) {
# we can not connect to the remote server, so just shut down
sendMail( join( "\n", @status ), $configuration, "No connection to remote machine" );
&shutdownMachine( $configuration );
}
# check for maintenance flags, exit if we should go into mainteance mode
if ( my $result = &checkMaintenance( $configuration ) ) {
push @status,$result;
&sendMail( join( "\n", @status), $configuration, "Maintenance Mode" );
exit 1;
}
# if the zpool is encrypted with geli, make sure it is available
($error, $output) = &mountGeliZpool (
$configuration->{'geli'}->{'zpool'},
$configuration->{'geli'}->{'keyPath'},
$configuration->{'geli'}->{'server'},
$configuration->{'geli'}->{'remoteKeyPath'},
[ split( /\s+/, $configuration->{'geli'}->{'drives'} ) ]
)
if exists ( $configuration->{'geli'} );
if ( $error) { # could not mount datasets
push @status, $output;
&sendMail( join( "\n", @status ), $configuration, "Mount Drive Error: [$output]" );
&shutdownMachine( $configuration );
}
#&sendMail( "Backup has been started at " . ¤tTime(), $configuration, "Backup Starting" );
push @status, ¤tTime() . ' Backup started' if $configuration->{'verbose'};
$configuration->{'source'}->{'server'} = $configuration->{'source'}->{'server'} ? $configuration->{'source'}->{'server'} . ':' : '';
$configuration->{'target'}->{'server'} = $configuration->{'target'}->{'server'} ? $configuration->{'target'}->{'server'} . ':' : '';
my @flags;
push @flags, '--dryrun' if $configuration->{'dryrun'};
push @flags, '--recurse' if $configuration->{'recurse'};
push @flags, '-' . 'v'x$configuration->{verbose} if $configuration->{'verbose'};
push @flags, "--bwlimit=$configuration->{bandwidth}" if $configuration->{'bandwidth'};
push @flags, "--filter='$configuration->{filter}'" if $configuration->{'filter'};
# die join( ' ', @flags ) . "\n";
# prepend the current working directory to stats if it does not have a path
$configuration->{'stats'} = $cwd . "/" . $configuration->{'stats'}
if $configuration->{'stats'} && $configuration->{'stats'} !~ m/\//;
# For each dataset, let's find the snapshots we need
foreach my $sourceDir ( keys %{$configuration->{'source'}->{'dataset'}} ) {
print "Working on $sourceDir\n" if $configuration->{'testing'};
print "Looking for $sourceDir\n" if $configuration->{'testing'} > 2;
print "syncing to $configuration->{target}->{dataset}\n" if $configuration->{'testing'} > 2;
my $command = $replicateScript . ' ' . join( ' ', @flags ) . ' ' .
'--source=' .
$configuration->{'source'}->{'server'} .
$configuration->{'source'}->{'dataset'}->{$sourceDir} . '/' . $sourceDir . ' ' .
'--target=' .
$configuration->{'target'}->{'server'} .
$configuration->{'target'}->{'dataset'} . '/' . $sourceDir;
print "Command is $command\n" if $configuration->{'testing'};
push @status, ¤tTime() . " Running $command" if $configuration->{'verbose'} > 1;
if ( ! $configuration->{'testing'} ) {
($error, $output) = &runCommand( $command );
push @status, "Dataset\t$sourceDir\n$output";
# update stats file if they have requested it
&updateStats( $sourceDir, $configuration->{'stats'}, $output ) if $configuration->{'stats'};
}
push @status, ¤tTime() . " Completed command, with status $error" if $configuration->{'verbose'} > 1;;
}
#print "Finished processing\n";
#print "testing is " . $configuration->{'testing'} . "\n";
push @status, ¤tTime() . ' Backup finished';
if ($configuration->{'testing'}) {
print join( "\n", @status ) . "\n";
} else {
#print "Sending final email\n";
&sendMail( join( "\n", @status ), $configuration, "Backup Complete" );
#print "Running shutdown\n";
&shutdownMachine( $configuration ) if $configuration->{'shutdown'};
}
1;