Subversion Repositories zfs_utils

Rev

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