Subversion Repositories sysadmin_scripts

Rev

Rev 170 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
166 rodolico 1
#! /usr/bin/env perl
2
 
3
use strict;
4
use warnings;
5
 
6
 
7
BEGIN {
8
   use FindBin;
9
   use File::Spec;
10
   # use libraries from the directory this script is in
11
   use Cwd 'abs_path';
12
   use File::Basename;
13
   use lib dirname( abs_path( __FILE__ ) );
14
}
15
 
171 rodolico 16
use YAML::Tiny; # pkg install p5-YAML-Tiny-1.74
17
use Data::Dumper;
18
use Email::Simple; # cpan install Email::Simple
19
 
166 rodolico 20
my $cwd = $FindBin::RealBin;
21
my $configFileName = $cwd . '/sync.yaml';
22
my $replicateScript = $cwd . '/replicate';
23
 
24
my $configuration;
25
 
26
 
27
# load Configuration File
28
# read the config file and return it
29
sub readConfig {
30
   my $filename = shift;
168 rodolico 31
   die "Config file $filename not found: $!" unless -f $filename;
166 rodolico 32
   my $yaml = YAML::Tiny->new( {} );
33
   if ( -f $filename ) {
34
      $yaml = YAML::Tiny->read( $filename );
35
   }
36
   return $yaml->[0];
37
}
38
 
39
 
40
 
41
# this calls gshred which will overwrite the file 3 times, then
42
# remove it.
43
# NOTE: this will not work on ZFS, since ZFS is CopyOnWrite (COW)
44
# so assuming /tmp is a ramdisk
45
sub shredFile {
46
   my $filename = shift;
47
   `/usr/local/bin/gshred -u -f -s 32 $filename`;
48
}
49
 
50
 
51
# runs a command, redirecting stderr to stdout (which it ignores)
52
# then returns 0 on success.
53
# if error, returns string describing error
54
sub runCommand {
55
   my $command = shift;
168 rodolico 56
   my $output = qx/$command 2>&1/;
166 rodolico 57
   if ($? == -1) {
168 rodolico 58
      return (-1, "failed to execute: $!" );
166 rodolico 59
   } elsif ($? & 127) {
168 rodolico 60
      return (-1,sprintf "child died with signal %d, %s coredump",
61
        ($? & 127),  ($? & 128) ? 'with' : 'without');
166 rodolico 62
   } else {
168 rodolico 63
      return ( $?>>8, sprintf "child exited with value %d", $? >> 8 ) if $? >> 8;
166 rodolico 64
   }
168 rodolico 65
   return (0, $output);
166 rodolico 66
}
67
 
68
# grabs the encryption key from the remote server, and uses it to unlock the 
69
# datasets, then mount the drives.
70
# a return of '' is success, anything else is an error
71
sub mountDrives {
171 rodolico 72
   my $configuration = shift;
73
   return (0, 'No encrypted target found' ) unless defined( $configuration->{'target'}->{'encryptionKeyPath'} ) && $configuration->{'target'}->{'encryptionKeyPath'};
166 rodolico 74
   # try to grab the file from the remote machine
171 rodolico 75
   &runCommand( "scp $configuration->{remoteMachine}->{ip}:$configuration->{remoteMachine}->{encryptionKeyPath} $configuration->{localMachine}->{encryptionKeyPath}" );
166 rodolico 76
   # If we do not have the encryption key, we need to abort
171 rodolico 77
   return "Could not copy file $configuration->{remoteMachine}->{ip}:$configuration->{remoteMachine}->{encryptionKeyPath}, aborting" 
78
      unless -f $configuration->{'target'}->{'encryptionKeyPath'};
166 rodolico 79
   my $error = '';
168 rodolico 80
   my $output = '';
166 rodolico 81
   # load the key into zfs and unlock all volumes
168 rodolico 82
   ($error,$output) = &runCommand( "zfs load-key -a" );
166 rodolico 83
   # finally, remount all of the zfs shares which need the key
168 rodolico 84
   ($error,$output) = &runCommand( "zfs mount -a" ) unless $error;
166 rodolico 85
   # if we succeeded, we want to shred the keyfile
171 rodolico 86
   &shredFile( $configuration->{localMachine}->{encryptionKeyPath} ) if -f $configuration->{localMachine}->{encryptionKeyPath};
166 rodolico 87
   return $error;
88
}
89
 
90
# a very simple mailer, using Email::Simple to just get status messages out
91
sub sendMail {
171 rodolico 92
   my ($message, $configuration, $subject ) = @_;
93
   $configuration->{'email'}->{'notify'} = 'root' unless $configuration->{'email'}->{'notify'};
166 rodolico 94
   die "No message in outgoing message\n" unless $message;
95
   my $email = Email::Simple->create(
96
      header => [
171 rodolico 97
         To     => $configuration->{'email'}->{'notify'},
98
         Subject=> $configuration->{'email'}->{'subject'} . ( $subject ? " - $subject" : '' ),
99
         From   => $configuration->{'email'}->{'from'}
166 rodolico 100
      ],
101
      body => $message
102
   );
103
   $message = $email->as_string;
171 rodolico 104
   if ( $configuration->{'testing'} > 1 ) {
105
      print "$message\n";
106
   } else {
107
      `echo '$message' | sendmail $configuration->{'email'}->{'notify'}`;
108
   }
166 rodolico 109
}
110
 
111
# checks to see if we should be in maintenance mode
112
# if $remoteMachine->{'maintenanceMode'} exists, set mode
113
# otherwise, wait localMachine->{'waittime'} minutes, then check
114
# $localMachine->{'maintenanceMode'}.
115
# if neither exists, begin sync
116
sub checkMaintenance {
171 rodolico 117
   my $configuration = shift;
118
   return 0 unless # exit if maintenanceFlag has not been set at all
119
     ( defined( $configuration->{'target'}->{'maintenanceFlag'} ) && $configuration->{'target'}->{'maintenanceFlag'} ) ||
120
     ( defined( $configuration->{'source'}->{'maintenanceFlag'} ) && $configuration->{'source'}->{'maintenanceFlag'} );
166 rodolico 121
   # see if maintenance is set on remote. If so, simply return the message
171 rodolico 122
   if ( $configuration->{'source'}->{'up'} ) {
168 rodolico 123
      my ($error, $output) = &runCommand( "ssh $configuration->{remoteMachine}->{ip} 'ls $configuration->{remoteMachine}->{maintenanceFlag}'" );
124
      if ( ! $error ) {
166 rodolico 125
         # remove the file from the remote server
126
         &runCommand( "ssh $configuration->{remoteMachine}->{ip} 'rm $configuration->{remoteMachine}->{maintenanceFlag}'" );
127
         # create a valid return, which will exit the program
128
         return "Maintenance Flag found on remote machine";
129
      }
130
   }
131
   # not on remote machine, so give them waitTime seconds to put it here
132
   # we'll loop, checking every $sleepTime seconds until our wait time
171 rodolico 133
   # ($configuration->{'target'}->{'waitTime'}) has expired
166 rodolico 134
   my $sleepTime = 60;
171 rodolico 135
   for ( my $i = $configuration->{'target'}->{'waitTime'}; $i > 0; $i -= $sleepTime ) {
166 rodolico 136
      sleep $sleepTime;
137
      # then look for the maintenance flag file on the local machine
171 rodolico 138
      return "Maintenance Flag found on local machine" if -f $configuration->{'target'}->{'maintenanceFlag'};
166 rodolico 139
   }
140
   # no maintenance flags found, so return false
141
   return 0;
142
}
143
 
144
sub shutdownMachine {
171 rodolico 145
   my $configuration = shift;
166 rodolico 146
   my $subject = shift;
171 rodolico 147
   push @_, "Shutting down" if $configuration->{'shutdown'};
148
   &sendMail( join( "\n", @_), $configuration, $subject );
149
   # do not actually shut down the server unless we are told to
150
   exit unless $configuration->{'shutdown'};
151
   &runCommand( "poweroff" ) unless $configuration->{'testing'};
166 rodolico 152
   die "Shutting down machine now\n";
153
}
154
 
155
# returns the current time as a string
156
sub currentTime {
157
   my $format = shift;
158
   # default to YY-MM-DD HH-MM-SS
159
   $format = '%Y-%m-%d %H-%M-%S' unless $format;
160
   use POSIX;
161
   return POSIX::strftime( $format, localtime() );
162
}
163
 
171 rodolico 164
sub checkRemoteUp {
165
   my $configuration = shift;
166
   my $ip;
167
   if ( defined( $configuration->{'target'}->{'server'} ) && $configuration->{'target'}->{'server'} ) {
168
      $ip = $configuration->{'target'}->{'server'};
169
   } else {
170
      $ip = $configuration->{'source'}->{'server'};
171
   }
172
   my ($error, $message ) =  $ip ? &runCommand( "ping -c 1 -t 5 $ip" ) : (0,'No address defined for either target or server' );
173
   $message = "Checking IP $ip\n"  . $message;
174
   #die "error is $error, message is $message for $ip\n";
175
   return ($error, $message);
176
}
177
 
166 rodolico 178
my @status;   
171 rodolico 179
my $error = 0;
180
my $output = '';
166 rodolico 181
 
182
$configuration = &readConfig($configFileName);
183
 
171 rodolico 184
# die Dumper( $configuration ) . "\n";
185
 
186
my $servername = `hostname`;
187
chomp $servername;
188
 
189
&sendMail( "Replication on $servername has been started, " . &currentTime(), $configuration, "Replication on $servername started" );
190
 
166 rodolico 191
# see if remote machine is up by sending one ping. Expect response in 5 seconds
171 rodolico 192
( $error,$output) = &checkRemoteUp( $configuration );
193
$configuration->{'up'} = ! $error;
194
push @status, "remote machine is " . ( $configuration->{'up'} ? 'Up' : 'Down' ) . "\n";
195
# we can not connect to the remote server, so just shut down
196
&shutdownMachine( $configuration, "No connection to remote machine", @status ) unless $configuration->{'up'};
166 rodolico 197
 
171 rodolico 198
 
199
# check for maintenance flags, exit if we should go into mainteance mode
169 rodolico 200
if ( my $result = &checkMaintenance( $configuration ) ) {
201
   push @status,$result;
202
   &sendMail( join( "\n", @status), $configuration, "Maintenance Mode" );
170 rodolico 203
   die;
169 rodolico 204
}
166 rodolico 205
 
171 rodolico 206
# try to mount the datasets if they are encrypted
168 rodolico 207
($error,$output) = &mountDrives( $configuration );
166 rodolico 208
if ( $error ) { # could not mount datasets
209
   push @status, $error;
168 rodolico 210
   &shutdownMachine( $configuration, "Mount Drive Error: [$output]", @status );
166 rodolico 211
}
212
 
213
&sendMail( "Backup has been started at " . &currentTime(), $configuration, "Backup Starting" );
214
push @status, "Backup started at: " . &currentTime();
215
 
171 rodolico 216
$configuration->{'source'}->{'server'} = $configuration->{'source'}->{'server'} ? $configuration->{'source'}->{'server'} . ':' : '';
217
$configuration->{'target'}->{'server'} = $configuration->{'target'}->{'server'} ? $configuration->{'target'}->{'server'} . ':' : '';
166 rodolico 218
 
219
# For each dataset, let's find the snapshots we need
171 rodolico 220
foreach my $sourceDir ( keys %{$configuration->{'source'}->{'dataset'}} ) {
221
   print "Looking for $sourceDir\n";
222
   print "syncing to $configuration->{target}->{targetDataset}\n";
166 rodolico 223
   my $command = $replicateScript . ' ' .
171 rodolico 224
                 $configuration->{'source'}->{'server'} . 
225
                 $configuration->{'source'}->{'dataset'}->{$sourceDir} . '/' . $sourceDir . ' ' .
226
                 $configuration->{'target'}->{'server'} . 
227
                 $configuration->{'target'}->{'targetDataset'} . '/' . $sourceDir;
168 rodolico 228
   push @status, "=== Running $command at " . &currentTime();
171 rodolico 229
   ($error, $output) = &runCommand( $command ) unless $configuration->{'testing'};
168 rodolico 230
   push @status, $output;
231
   push @status, "=== Completed $command with status $error at " . &currentTime();
166 rodolico 232
}
233
 
168 rodolico 234
push @status, "==== Backup finished at: " . &currentTime();
166 rodolico 235
 
171 rodolico 236
if ($configuration->{'testing'}) {
237
   print join( "\n", @status ) . "\n";
238
} else {
239
   &shutdownMachine( $configuration, "Backup Complete", @status );
240
}
166 rodolico 241
 
242
1;