| 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 ¤tTime('') . "\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 " . ¤tTime();
|
|
|
227 |
&sendMail( "Replication on $servername has been started, " . ¤tTime(), $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 " . ¤tTime(), $configuration, "Backup Starting" );
|
| 18 |
rodolico |
264 |
push @status, ¤tTime() . ' 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, ¤tTime() . " 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, ¤tTime() . " 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, ¤tTime() . ' 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;
|