Subversion Repositories zfs_utils

Rev

Rev 20 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 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
 
16
use YAML::Tiny; # pkg install p5-YAML-Tiny-1.74
17
use Data::Dumper;
18
 
19
my $cwd = $FindBin::RealBin;
20
my $configFileName = $cwd . '/sync.yaml';
21
my $replicateScript = $cwd . '/replicate';
22
 
23
my $configuration;
24
 
25
 
26
# load Configuration File
27
# read the config file and return it
28
sub readConfig {
29
   my $filename = shift;
30
   die "Config file $filename not found: $!" unless -f $filename;
31
   my $yaml = YAML::Tiny->new( {} );
32
   if ( -f $filename ) {
33
      $yaml = YAML::Tiny->read( $filename );
34
   }
35
   return $yaml->[0];
36
}
37
 
10 rodolico 38
sub logit {
39
   open LOG, ">>/tmp/replicate.log" or die "Could not open replicate.log: $!\n";
40
   print LOG join( "\n", @_ ) . "\n";
41
   close LOG;
42
}
2 rodolico 43
 
44
 
45
# this calls gshred which will overwrite the file 3 times, then
46
# remove it.
47
# NOTE: this will not work on ZFS, since ZFS is CopyOnWrite (COW)
20 rodolico 48
# so assuming file is on a ramdisk
2 rodolico 49
sub shredFile {
50
   my $filename = shift;
51
   `/usr/local/bin/gshred -u -f -s 32 $filename`;
52
}
53
 
54
 
55
# runs a command, redirecting stderr to stdout (which it ignores)
56
# then returns 0 on success.
57
# if error, returns string describing error
58
sub runCommand {
59
   my $command = shift;
10 rodolico 60
   #logit( $command );
2 rodolico 61
   my $output = qx/$command 2>&1/;
62
   if ($? == -1) {
63
      return (-1, "failed to execute: $!" );
64
   } elsif ($? & 127) {
65
      return (-1,sprintf "child died with signal %d, %s coredump",
66
        ($? & 127),  ($? & 128) ? 'with' : 'without');
67
   } else {
68
      return ( $?>>8, sprintf "child exited with value %d", $? >> 8 ) if $? >> 8;
69
   }
70
   return (0, $output);
71
}
20 rodolico 72
 
73
# Checks if a zpool is available. If not, retrieves the geli key from a remote server,
74
# decrypts the drives, and mounts the zpool.
75
# Params:
76
#   $zpool         - Name of the zpool
77
#   $local_key     - Local path to store the geli key
78
#   $remote_server - Remote server to fetch the key from (user@host)
79
#   $remote_key    - Path to the geli key on the remote server
80
#   $drives_ref    - Arrayref of drives to decrypt (e.g., ['/dev/ada0p3', '/dev/ada1p3'])
81
sub mountGeliZpool {
82
    my ($zpool, $local_key, $remote_server, $remote_key, $drives_ref) = @_;
83
 
84
    # Check if the zpool is available
85
    my ($error, $output) = runCommand("zpool list $zpool");
86
    return 0 unless $error; # zpool is available
87
 
88
    # Retrieve geli key from remote server
89
    ($error, $output) = runCommand("scp $remote_server:$remote_key $local_key");
90
    return ($error, "Failed to retrieve geli key from $remote_server:$remote_key" ) if $error;
91
 
92
    # Attach geli key to each drive
93
    foreach my $drive (@$drives_ref) {
94
        ($error, $output) = runCommand("geli attach -k $local_key $drive");
95
         return ($error, "Failed to attach geli key to $drive: $output" ) if $error;
96
    }
97
 
98
    # Import the zpool
99
    ($error, $output) = runCommand("zpool import $zpool");
100
    return ( $error, "Failed to import zpool $zpool: $output" ) if $error;
101
 
102
    # Optionally, mount all datasets in the zpool
103
    ($error, $output) = runCommand("zfs mount -a");
104
     return( $error,"Failed to mount datasets in zpool $zpool: $output" ) if $error;
105
 
106
    # Shred the key file after use
107
    shredFile($local_key) if -f $local_key;
108
 
109
    return 1;
2 rodolico 110
}
111
 
10 rodolico 112
# a very simple mailer, just send information to sendmail
2 rodolico 113
sub sendMail {
114
   my ($message, $configuration, $subject ) = @_;
10 rodolico 115
   if ( $message ) {
116
      open MAIL,"|sendmail -t" or die "Could not open sendmail: $!\n";
117
      print MAIL "To: $configuration->{email}->{notify}\n";
118
      print MAIL "From: $configuration->{email}->{from}\n";
119
      print MAIL "Subject: " . 
120
                 ($configuration->{'email'}->{'subject'} . ( $subject ? " - $subject" : '' ) ) .
121
                 "\n\n";
122
      print MAIL $message;
123
      close MAIL;
2 rodolico 124
   } else {
10 rodolico 125
      warn "no message in outgoing email\n";
2 rodolico 126
   }
127
}
128
 
129
# checks to see if we should be in maintenance mode
130
# if $remoteMachine->{'maintenanceMode'} exists, set mode
131
# otherwise, wait localMachine->{'waittime'} minutes, then check
132
# $localMachine->{'maintenanceMode'}.
133
# if neither exists, begin sync
134
sub checkMaintenance {
135
   my $configuration = shift;
136
   return 0 unless # exit if maintenanceFlag has not been set at all
137
     ( defined( $configuration->{'target'}->{'maintenanceFlag'} ) && $configuration->{'target'}->{'maintenanceFlag'} ) ||
138
     ( defined( $configuration->{'source'}->{'maintenanceFlag'} ) && $configuration->{'source'}->{'maintenanceFlag'} );
139
   # see if maintenance is set on remote. If so, simply return the message
140
   if ( $configuration->{'source'}->{'up'} ) {
141
      my ($error, $output) = &runCommand( "ssh $configuration->{remoteMachine}->{ip} 'ls $configuration->{remoteMachine}->{maintenanceFlag}'" );
142
      if ( ! $error ) {
143
         # remove the file from the remote server
144
         &runCommand( "ssh $configuration->{remoteMachine}->{ip} 'rm $configuration->{remoteMachine}->{maintenanceFlag}'" );
145
         # create a valid return, which will exit the program
146
         return "Maintenance Flag found on remote machine";
147
      }
148
   }
149
   # not on remote machine, so give them waitTime seconds to put it here
150
   # we'll loop, checking every $sleepTime seconds until our wait time
151
   # ($configuration->{'target'}->{'waitTime'}) has expired
21 rodolico 152
   # set default of 3 minutes if waitTime not set
153
   $configuration->{'target'}->{'waitTime'} = 300 unless $configuration->{'target'}->{'waitTime'};
154
   # we will check every $sleepTime seconds (ie, every minute)
2 rodolico 155
   my $sleepTime = 60;
156
   for ( my $i = $configuration->{'target'}->{'waitTime'}; $i > 0; $i -= $sleepTime ) {
157
      sleep $sleepTime;
158
      # then look for the maintenance flag file on the local machine
159
      return "Maintenance Flag found on local machine" if -f $configuration->{'target'}->{'maintenanceFlag'};
160
   }
161
   # no maintenance flags found, so return false
162
   return 0;
163
}
164
 
165
sub shutdownMachine {
166
   my $configuration = shift;
10 rodolico 167
   exit unless $configuration->{'shutdown'};
2 rodolico 168
   # do not actually shut down the server unless we are told to
169
   &runCommand( "poweroff" ) unless $configuration->{'testing'};
170
}
171
 
172
# returns the current time as a string
173
sub currentTime {
174
   my $format = shift;
175
   # default to YY-MM-DD HH-MM-SS
16 rodolico 176
   $format = '%Y-%m-%d %H:%M:%S' unless $format;
2 rodolico 177
   use POSIX;
178
   return POSIX::strftime( $format, localtime() );
179
}
180
 
7 rodolico 181
# verify a remote machine is up and running
2 rodolico 182
sub checkRemoteUp {
183
   my $configuration = shift;
184
   my $ip;
185
   if ( defined( $configuration->{'target'}->{'server'} ) && $configuration->{'target'}->{'server'} ) {
186
      $ip = $configuration->{'target'}->{'server'};
187
   } else {
188
      $ip = $configuration->{'source'}->{'server'};
189
   }
190
   my ($error, $message ) =  $ip ? &runCommand( "ping -c 1 -t 5 $ip" ) : (0,'No address defined for either target or server' );
7 rodolico 191
#   $message = "Checking IP $ip\n"  . $message;
2 rodolico 192
   #die "error is $error, message is $message for $ip\n";
193
   return ($error, $message);
194
}
195
 
12 rodolico 196
sub updateStats {
197
   my ( $label, $filename, $output ) = @_;
198
   if ( $output =~ m/bytes\t(\d+).*seconds\t(\d+)/gms ) { # global, multiline, . matches newlines
199
      my $seconds = $2;
200
      my $bytes = $1;
201
      open STATS,">>$filename" or warn "Could not create file $filename: $!\n";
16 rodolico 202
      print STATS &currentTime('') . "\t$label\t$seconds\t$bytes\n";
12 rodolico 203
      close STATS
204
   } else {
18 rodolico 205
      warn "updateStats called with invalid report\n" if $configuration->{'verbose'}>1;
12 rodolico 206
   }
207
}
208
 
2 rodolico 209
my @status;   
210
my $error = 0;
211
my $output = '';
212
 
213
$configuration = &readConfig($configFileName);
214
 
215
# die Dumper( $configuration ) . "\n";
216
 
217
my $servername = `hostname`;
218
chomp $servername;
219
 
18 rodolico 220
if ( $configuration->{'verbose'} > 1 ) {
221
   push @status, "Replication on $servername has been started at " . &currentTime();
222
   &sendMail( "Replication on $servername has been started, " . &currentTime(), $configuration, "Replication on $servername started" );
223
}
2 rodolico 224
 
225
# see if remote machine is up by sending one ping. Expect response in 5 seconds
226
( $error,$output) = &checkRemoteUp( $configuration );
227
$configuration->{'up'} = ! $error;
228
push @status, "remote machine is " . ( $configuration->{'up'} ? 'Up' : 'Down' ) . "\n";
10 rodolico 229
if ( ! $configuration->{'up'} ) {
230
   # we can not connect to the remote server, so just shut down
231
   sendMail( join( "\n", @status ), $configuration, "No connection to remote machine" );
232
   &shutdownMachine( $configuration );
233
}
2 rodolico 234
 
235
# check for maintenance flags, exit if we should go into mainteance mode
236
if ( my $result = &checkMaintenance( $configuration ) ) {
237
   push @status,$result;
238
   &sendMail( join( "\n", @status), $configuration, "Maintenance Mode" );
20 rodolico 239
   exit 1;
2 rodolico 240
}
241
 
20 rodolico 242
# if the zpool is encrypted with geli, make sure it is available
243
($error, $output) = &mountGeliZpool {(
244
   $configuration->{'geli'}->{'zpool'},
245
   $configuration->{'geli'}->{'localKey'},
246
   $configuration->{'geli'}->{'server'},
247
   $configuration->{'geli'}->{'remoteKey'},
248
   split( ' ', $configuration->{'geli'}->{'drives'} ) )
249
   if exists ( $configuration->{'geli'} );
250
 
251
if ( $error) { # could not mount datasets
10 rodolico 252
   push @status, $output;
253
   &sendMail( join( "\n", @status ), $configuration, "Mount Drive Error: [$output]" );
254
   &shutdownMachine( $configuration );
2 rodolico 255
}
256
 
10 rodolico 257
#&sendMail( "Backup has been started at " . &currentTime(), $configuration, "Backup Starting" );
18 rodolico 258
push @status, &currentTime() . ' Backup started' if $configuration->{'verbose'};
2 rodolico 259
 
260
$configuration->{'source'}->{'server'} = $configuration->{'source'}->{'server'} ? $configuration->{'source'}->{'server'} . ':' : '';
261
$configuration->{'target'}->{'server'} = $configuration->{'target'}->{'server'} ? $configuration->{'target'}->{'server'} . ':' : '';
262
 
7 rodolico 263
my @flags;
264
push @flags, '--dryrun' if $configuration->{'dryrun'};
265
push @flags, '--recurse' if $configuration->{'recurse'};
16 rodolico 266
push @flags, '-' . 'v'x$configuration->{verbose} if $configuration->{'verbose'};
10 rodolico 267
push @flags, "--bwlimit=$configuration->{bandwidth}" if $configuration->{'bandwidth'};
7 rodolico 268
push @flags, "--filter='$configuration->{filter}'" if $configuration->{'filter'};
269
 
16 rodolico 270
# die join( ' ', @flags ) . "\n";
271
 
12 rodolico 272
# prepend the current working directory to stats if it does not have a path
273
$configuration->{'stats'} = $cwd . "/" . $configuration->{'stats'}
274
   if $configuration->{'stats'} && $configuration->{'stats'} !~ m/\//;
275
 
2 rodolico 276
# For each dataset, let's find the snapshots we need
277
foreach my $sourceDir ( keys %{$configuration->{'source'}->{'dataset'}} ) {
21 rodolico 278
   print "Working on $sourceDir\n" if $configuration->{'testing'};
7 rodolico 279
   print "Looking for $sourceDir\n" if $configuration->{'testing'} > 2;
280
   print "syncing to $configuration->{target}->{dataset}\n" if $configuration->{'testing'} > 2;
281
   my $command = $replicateScript . ' ' . join( ' ', @flags ) . ' ' .
282
                 '--source=' .
2 rodolico 283
                 $configuration->{'source'}->{'server'} . 
284
                 $configuration->{'source'}->{'dataset'}->{$sourceDir} . '/' . $sourceDir . ' ' .
7 rodolico 285
                 '--target=' .
2 rodolico 286
                 $configuration->{'target'}->{'server'} . 
7 rodolico 287
                 $configuration->{'target'}->{'dataset'} . '/' . $sourceDir;
21 rodolico 288
   print "Command is $command\n" if $configuration->{'testing'};
18 rodolico 289
   push @status, &currentTime() . " Running $command" if $configuration->{'verbose'} > 1;
7 rodolico 290
   if ( ! $configuration->{'testing'} ) {
291
      ($error, $output) = &runCommand( $command );
16 rodolico 292
      push @status, "Dataset\t$sourceDir\n$output";
12 rodolico 293
      # update stats file if they have requested it
294
      &updateStats( $sourceDir, $configuration->{'stats'}, $output ) if $configuration->{'stats'};
7 rodolico 295
   }
18 rodolico 296
   push @status, &currentTime() . " Completed command, with status $error" if $configuration->{'verbose'} > 1;;
2 rodolico 297
}
298
 
10 rodolico 299
#print "Finished processing\n";
300
#print "testing is " . $configuration->{'testing'} . "\n";
301
 
7 rodolico 302
push @status, &currentTime() . ' Backup finished';
2 rodolico 303
 
304
if ($configuration->{'testing'}) {
305
   print join( "\n", @status ) . "\n";
306
} else {
10 rodolico 307
   #print "Sending final email\n";
308
   &sendMail( join( "\n", @status ), $configuration, "Backup Complete" );
309
   #print "Running shutdown\n";
310
   &shutdownMachine( $configuration ) if $configuration->{'shutdown'};
2 rodolico 311
}
312
 
313
1;