#! /usr/bin/env perl use strict; use warnings; use YAML::Tiny; # pkg install p5-YAML-Tiny-1.74 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__ ) ); } my $cwd = $FindBin::RealBin; my $configFileName = $cwd . '/sync.yaml'; my $replicateScript = $cwd . '/replicate'; my $TESTING=0; my $configuration; use Data::Dumper; use Email::Simple; # cpan install Email::Simple # 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]; } # 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 /tmp is a ramdisk sub shredFile { my $filename = shift; `/usr/local/bin/gshred -u -f -s 32 $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; 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); } # grabs the encryption key from the remote server, and uses it to unlock the # datasets, then mount the drives. # a return of '' is success, anything else is an error sub mountDrives { my $config = shift; # try to grab the file from the remote machine &runCommand( "scp $config->{remoteMachine}->{ip}:$config->{remoteMachine}->{encryptionKeyPath} $config->{localMachine}->{encryptionKeyPath}" ); # If we do not have the encryption key, we need to abort return "Could not copy file $config->{remoteMachine}->{ip}:$config->{remoteMachine}->{encryptionKeyPath}, aborting" unless -f $config->{'localMachine'}->{'encryptionKeyPath'}; my $error = ''; my $output = ''; # load the key into zfs and unlock all volumes ($error,$output) = &runCommand( "zfs load-key -a" ); # finally, remount all of the zfs shares which need the key ($error,$output) = &runCommand( "zfs mount -a" ) unless $error; # if we succeeded, we want to shred the keyfile &shredFile( $config->{localMachine}->{encryptionKeyPath} ) if -f $config->{localMachine}->{encryptionKeyPath}; return $error; } # a very simple mailer, using Email::Simple to just get status messages out sub sendMail { my ($message, $config, $subject ) = @_; $config->{'email'}->{'notify'} = 'root' unless $config->{'email'}->{'notify'}; die "No message in outgoing message\n" unless $message; my $email = Email::Simple->create( header => [ To => $config->{'email'}->{'notify'}, Subject=> $config->{'email'}->{'subject'} . ( $subject ? " - $subject" : '' ), From => $config->{'email'}->{'from'} ], body => $message ); $message = $email->as_string; `echo '$message' | sendmail $config->{'email'}->{'notify'}`; } # 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 $config = shift; # see if maintenance is set on remote. If so, simply return the message if ( $config->{'remoteMachine'}->{'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 # ($config->{'localMachine'}->{'waitTime'}) has expired my $sleepTime = 60; for ( my $i = $config->{'localMachine'}->{'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 $config->{'localMachine'}->{'maintenanceFlag'}; } # no maintenance flags found, so return false return 0; } sub shutdownMachine { my $config = shift; my $subject = shift; push @_, "Shutting down"; &sendMail( join( "\n", @_), $configuration, $subject ); &runCommand( "poweroff" ) unless $TESTING; die "Shutting down machine now\n"; } # 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() ); } my @status; $configuration = &readConfig($configFileName); &sendMail( "mc-009 has been started, " . ¤tTime() . " checking for maintenance mode", $configuration ); # see if remote machine is up by sending one ping. Expect response in 5 seconds my ( $error,$output) = &runCommand( "ping -c 1 -t 5 " . $configuration->{'remoteMachine'}->{'ip'} ); $configuration->{'remoteMachine'}->{'up'} = ! $error; push @status, "remote machine ($configuration->{'remoteMachine'}->{'ip'}) is " . ( $configuration->{'remoteMachine'}->{'up'} ? 'Up' : 'Down' ) . "\n"; # check for maintenance flags, exit if we should go into mainteannce mode if ( my $result = &checkMaintenance( $configuration ) ) { push @status,$result; &sendMail( join( "\n", @status), $configuration, "Maintenance Mode" ); die; } # we can not connect to the remote server, so just shut down &shutdownMachine( $configuration, "No connection to remote machine", @status ) unless $configuration->{'remoteMachine'}->{'up'}; # try to mount the datasets ($error,$output) = &mountDrives( $configuration ); if ( $error ) { # could not mount datasets push @status, $error; &shutdownMachine( $configuration, "Mount Drive Error: [$output]", @status ); } &sendMail( "Backup has been started at " . ¤tTime(), $configuration, "Backup Starting" ); push @status, "Backup started at: " . ¤tTime(); # For each dataset, let's find the snapshots we need foreach my $sourceDir ( keys %{$configuration->{'remoteMachine'}->{'dataset'}} ) { my $command = $replicateScript . ' ' . $configuration->{'remoteMachine'}->{'ip'} . ':' . $configuration->{'remoteMachine'}->{'dataset'}->{$sourceDir} . '/' . $sourceDir . ' ' . $configuration->{'localMachine'}->{'targetDataset'} . '/' . $sourceDir; push @status, "=== Running $command at " . ¤tTime(); my ($error, $output) = &runCommand( $command ); push @status, $output; push @status, "=== Completed $command with status $error at " . ¤tTime(); } push @status, "==== Backup finished at: " . ¤tTime(); &shutdownMachine( $configuration, "Backup Complete", @status ); 1;