| Line 44... |
Line 44... |
| 44 |
our $VERSION = '0.1';
|
44 |
our $VERSION = '0.1';
|
| 45 |
|
45 |
|
| 46 |
use FindBin;
|
46 |
use FindBin;
|
| 47 |
use lib "$FindBin::Bin/..";
|
47 |
use lib "$FindBin::Bin/..";
|
| 48 |
use Data::Dumper;
|
48 |
use Data::Dumper;
|
| 49 |
use ZFS_Utils qw(loadConfig shredFile logMsg makeReplicateCommands mountDriveByLabel mountGeli runCmd sendReport $logFileName $displayLogsOnConsole);
|
49 |
use ZFS_Utils qw(loadConfig shredFile logMsg makeReplicateCommands mountDriveByLabel unmountDriveByLabel mountGeli runCmd sendReport fatalError cleanDirectory $logFileName $displayLogsOnConsole);
|
| 50 |
|
50 |
|
| 51 |
my $scriptDirectory = $FindBin::RealBin;
|
51 |
my $scriptDirectory = $FindBin::RealBin;
|
| 52 |
my $scriptFullPath = "$scriptDirectory/" . $FindBin::Script;
|
52 |
my $scriptFullPath = "$scriptDirectory/" . $FindBin::Script;
|
| 53 |
|
53 |
|
| 54 |
|
54 |
|
| Line 83... |
Line 83... |
| 83 |
},
|
83 |
},
|
| 84 |
#information about target server
|
84 |
#information about target server
|
| 85 |
'target' => {
|
85 |
'target' => {
|
| 86 |
'hostname' => '', # used to see if we are on target
|
86 |
'hostname' => '', # used to see if we are on target
|
| 87 |
'poolname' => 'backup', # name of the ZFS pool to import
|
87 |
'poolname' => 'backup', # name of the ZFS pool to import
|
| - |
|
88 |
'shutdown_after_replication' => 0, # if set to 1, will shutdown the server after replication
|
| 88 |
# if this is set, the dataset uses GELI, so we must decrypt and
|
89 |
# if this is set, the dataset uses GELI, so we must decrypt and
|
| 89 |
# mount it first
|
90 |
# mount it first
|
| 90 |
'geli' => {
|
91 |
'geli' => {
|
| 91 |
'secureKey ' => {
|
92 |
'secureKey ' => {
|
| 92 |
'label' => 'replica', # the GPT label of the key disk
|
93 |
'label' => 'replica', # the GPT label of the key disk
|
| Line 130... |
Line 131... |
| 130 |
'IV' => '00000000000000000000000000000000',
|
131 |
'IV' => '00000000000000000000000000000000',
|
| 131 |
},
|
132 |
},
|
| 132 |
},
|
133 |
},
|
| 133 |
'datasets' => {
|
134 |
'datasets' => {
|
| 134 |
'dataset1' => {
|
135 |
'dataset1' => {
|
| 135 |
'source' => 'pool/dataset1',
|
136 |
'source' => 'pool', # the parent of the dataset on the source
|
| 136 |
'target' => 'backup/dataset1',
|
137 |
'target' => 'backup', # the parent of the dataset on the target
|
| 137 |
'filename' => 'dataset1'
|
138 |
'dataset' => 'dataset1', # the dataset name
|
| 138 |
},
|
139 |
},
|
| 139 |
'files_share' => {
|
140 |
'files_share' => {
|
| 140 |
'source' => 'pool/files_share',
|
141 |
'source' => 'pool',
|
| 141 |
'target' => 'backup/files_share',
|
142 |
'target' => 'backup',
|
| 142 |
'filename' => 'files_share'
|
143 |
'dataset' => 'files_share'.
|
| 143 |
},
|
144 |
},
|
| 144 |
}
|
145 |
}
|
| 145 |
};
|
146 |
};
|
| 146 |
|
147 |
|
| 147 |
# read the status file and return as list. If the file doesn't exits, returns an empty list
|
148 |
# read the status file and return as list. If the file doesn't exits, returns an empty list
|
| Line 200... |
Line 201... |
| 200 |
my ($config, $statusList) = @_;
|
201 |
my ($config, $statusList) = @_;
|
| 201 |
my $newStatus = [];
|
202 |
my $newStatus = [];
|
| 202 |
foreach my $dataset ( sort keys %{$config->{datasets}} ) {
|
203 |
foreach my $dataset ( sort keys %{$config->{datasets}} ) {
|
| 203 |
logMsg("Processing dataset '$dataset'");
|
204 |
logMsg("Processing dataset '$dataset'");
|
| 204 |
# get list of all snapshots on dataset
|
205 |
# get list of all snapshots on dataset
|
| - |
|
206 |
my $root = $config->{datasets}->{$dataset}->{source} . '/' . $config->{datasets}->{$dataset}->{dataset};
|
| 205 |
my $sourceList = [ runCmd( "zfs list -rt snap -H -o name $config->{datasets}->{$dataset}->{source} " ) ];
|
207 |
my $sourceList = [ runCmd( "zfs list -rt snap -H -o name $root" ) ];
|
| - |
|
208 |
# remove the parent part, leave the dataset itself
|
| - |
|
209 |
$sourceList =~ s|$config->{datasets}->{$dataset}->{source}/||;
|
| 206 |
# process dataset here
|
210 |
# process dataset here
|
| 207 |
my $commands = makeReplicateCommands($sourceList, $statusList, $newStatus );
|
211 |
my $commands = makeReplicateCommands( $config->{datasets}->{$dataset}->{source}, $sourceList, $statusList, $newStatus );
|
| 208 |
if ( %$commands ) {
|
212 |
if ( %$commands ) {
|
| 209 |
foreach my $cmd ( keys %$commands ) {
|
213 |
foreach my $cmd ( keys %$commands ) {
|
| 210 |
my $command = $commands->{$cmd};
|
214 |
my $command = $commands->{$cmd};
|
| 211 |
$command .= " | openssl enc -aes-256-cbc -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
|
215 |
$command .= " | openssl enc -aes-256-cbc -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
|
| 212 |
$command .= " > $config->{transport}->{mount_point}/" . replaceSlashWithDot($cmd);
|
216 |
$command .= " > $config->{transport}->{mount_point}/" . replaceSlashWithDot($cmd);
|
| Line 218... |
Line 222... |
| 218 |
}
|
222 |
}
|
| 219 |
}
|
223 |
}
|
| 220 |
return $newStatus;
|
224 |
return $newStatus;
|
| 221 |
}
|
225 |
}
|
| 222 |
|
226 |
|
| - |
|
227 |
# perform cleanup actions
|
| - |
|
228 |
# $config - configuration hashref
|
| 223 |
# clean all files from a directory, but not any subdirectories
|
229 |
# $message - optional message to include in the report
|
| - |
|
230 |
#
|
| 224 |
sub cleanDirectory {
|
231 |
sub cleanup{
|
| 225 |
my $dirname = shift;
|
232 |
my ( $config, $message ) = @_;
|
| - |
|
233 |
# add disk space utilization information on transport to the log
|
| - |
|
234 |
logMsg( "Disk space utilization on transport disk:\n" . runCmd( "df -h $config->{transport}->{mount_point}" ) . "\n" );
|
| 226 |
logMsg( "Cleaning up $dirname of all files" );
|
235 |
# add information about the server (zpools) to the log
|
| - |
|
236 |
my $servername = `hostname -s`;
|
| 227 |
# clean up a directory
|
237 |
chomp $servername;
|
| 228 |
opendir( my $dh, $dirname ) || fatalError( "Can not open $dirname: #!" );
|
238 |
logMsg( "Zpools on server $servername:\n" . join( "\n", runCmd( "zpool list" ) ) . "\n" );
|
| 229 |
# get all file names, but leave directories alone
|
239 |
$config->{$config->{runningAs}}->{report}->{subject} //= "Replication Report for $config->{runningAs} server $servername";
|
| 230 |
my @files = map{ $dirname . "/$_" } grep { -f "$dirname/$_" } readdir($dh);
|
240 |
$message //= "Replication completed on $config->{runningAs} server $servername.";
|
| 231 |
closedir $dh;
|
241 |
# unmount the sneakernet drive
|
| 232 |
foreach my $file (@files) {
|
242 |
unmountDriveByLabel( $config->{transport} );
|
| - |
|
243 |
sendReport( $config->{$config->{runningAs}}->{report}, $message, $config->{log_file} );
|
| - |
|
244 |
# If they have requested shutdown, do it now
|
| - |
|
245 |
if ( $config->{$config->{runningAs}}->{shutdown_after_replication} ) {
|
| - |
|
246 |
logMsg( "Shutting down target server as per configuration" );
|
| 233 |
unlink $file or warn "Could not unlink $file: #!\n";
|
247 |
runCmd( "shutdown -p now" ) unless $DEBUG;
|
| 234 |
}
|
248 |
}
|
| 235 |
}
|
- |
|
| 236 |
|
- |
|
| 237 |
|
- |
|
| 238 |
|
- |
|
| 239 |
# how to handle a fatal error
|
- |
|
| 240 |
sub fatalError {
|
- |
|
| 241 |
my $message = shift;
|
- |
|
| 242 |
logMsg( $message );
|
- |
|
| 243 |
die;
|
- |
|
| 244 |
}
|
249 |
}
|
| 245 |
|
250 |
|
| - |
|
251 |
# update the target datasets from the files on the transport drive
|
| - |
|
252 |
sub updateTarget {
|
| - |
|
253 |
my $config = shift;
|
| - |
|
254 |
my $files = getDirectoryList( $config->{transport}->{mount_point});
|
| - |
|
255 |
foreach my $filename ( @$files ) {
|
| - |
|
256 |
my $command = "cat $config->{output} | openssl enc -aes-256-cbc -d -K $config->{key} -iv $config->{IV}";
|
| - |
|
257 |
}
|
| - |
|
258 |
}
|
| 246 |
|
259 |
|
| 247 |
##################### main program starts here #####################
|
260 |
##################### main program starts here #####################
|
| 248 |
# Example to create a random key for encryption/decryption:
|
261 |
# Example to create a random key for encryption/decryption:
|
| 249 |
# generate a random key with
|
262 |
# generate a random key with
|
| 250 |
# openssl rand 32 | xxd -p | tr -d '\n' > test.key
|
263 |
# openssl rand 32 | xxd -p | tr -d '\n' > test.key
|
| 251 |
|
264 |
|
| 252 |
# If a YAML config file exists next to the script, load and merge it
|
265 |
# If a YAML config file exists next to the script, load and merge it
|
| 253 |
$config = loadConfig($configFileName, $config );
|
266 |
$config = loadConfig($configFileName, $config );
|
| - |
|
267 |
exit 1 unless keys %$config;
|
| 254 |
|
268 |
|
| 255 |
# set some defaults
|
269 |
# set some defaults
|
| 256 |
$config->{'status_file'} //= "$scriptFullPath.status";
|
270 |
$config->{'status_file'} //= "$scriptFullPath.status";
|
| 257 |
# set log file name for sub logMsg in ZFS_Utils, and remove the old log if it exists
|
271 |
# set log file name for sub logMsg in ZFS_Utils, and remove the old log if it exists
|
| 258 |
# Log file is only valid for one run
|
272 |
# Log file is only valid for one run
|
| 259 |
$logFileName = $config->{'log_file'} //= "$scriptFullPath.log";
|
273 |
$logFileName = $config->{'log_file'} //= "$scriptFullPath.log";
|
| 260 |
# log only for one run
|
274 |
# log only for one run
|
| 261 |
unlink ( $logFileName ) if -f $logFileName;
|
275 |
unlink ( $logFileName ) if -f $logFileName;
|
| 262 |
|
276 |
|
| 263 |
fatalError( "Invalid config file: missing source and/or target server" )
|
277 |
fatalError( "Invalid config file: missing source and/or target server", $config, \&cleanup )
|
| 264 |
unless (defined $config->{source} && defined $config->{target});
|
278 |
unless (defined $config->{source} && defined $config->{target});
|
| 265 |
|
279 |
|
| 266 |
# mount the transport drive, fatal error if we can not find it
|
- |
|
| 267 |
fatalError( "Unable to mount tranport drive with label $config->{transport}->{disk_label}" )
|
- |
|
| 268 |
unless $config->{transport}->{mount_point} = mountDriveByLabel( $config->{transport} );
|
- |
|
| 269 |
|
- |
|
| 270 |
my $servername = `hostname -s`;
|
280 |
my $servername = `hostname -s`;
|
| 271 |
chomp $servername;
|
281 |
chomp $servername;
|
| 272 |
my $runningAs = $servername eq $config->{source}->{hostname} ? 'source' :
|
282 |
$config->{runningAs} = $servername eq $config->{source}->{hostname} ? 'source' :
|
| 273 |
$servername eq $config->{target}->{hostname} ? 'target' : 'unknown';
|
283 |
$servername eq $config->{target}->{hostname} ? 'target' : 'unknown';
|
| 274 |
|
284 |
|
| - |
|
285 |
#cleanup( $config, "Testing" );
|
| - |
|
286 |
|
| - |
|
287 |
# mount the transport drive, fatal error if we can not find it
|
| - |
|
288 |
fatalError( "Unable to mount tranport drive with label $config->{transport}->{disk_label}", $config, \&cleanup )
|
| - |
|
289 |
unless $config->{transport}->{mount_point} = mountDriveByLabel( $config->{transport} );
|
| - |
|
290 |
|
| - |
|
291 |
# mail program logic
|
| 275 |
if ( $runningAs eq 'source' ) {
|
292 |
if ( $config->{runningAs} eq 'source' ) {
|
| 276 |
logMsg "Running as source server";
|
293 |
logMsg "Running as source server";
|
| 277 |
# remove all files from transport disk, but leave all subdirectories alone
|
294 |
# remove all files from transport disk, but leave all subdirectories alone
|
| - |
|
295 |
fatalError( "Failed to clean transport directory $config->{transport}->{mount_point}", $config, \&cleanup )
|
| 278 |
cleanDirectory( $config->{transport}->{mount_point} );
|
296 |
unless cleanDirectory( $config->{transport}->{mount_point} );
|
| 279 |
my $statusList = getStatusFile($config->{status_file});
|
297 |
my $statusList = getStatusFile($config->{status_file});
|
| 280 |
$statusList = doSourceReplication($config, $statusList);
|
298 |
$statusList = doSourceReplication($config, $statusList);
|
| 281 |
writeStatusFile($config->{status_file}, $statusList);
|
299 |
writeStatusFile($config->{status_file}, $statusList);
|
| 282 |
} elsif ( $runningAs eq 'target' ) {
|
300 |
} elsif ( $config->{runningAs} eq 'target' ) {
|
| 283 |
logMsg "Running as target server";
|
301 |
logMsg "Running as target server";
|
| 284 |
mountGeli( $config->{target}->{geli} ) if ( defined $config->{target}->{geli} );
|
302 |
mountGeli( $config->{target}->{geli} ) if ( defined $config->{target}->{geli} );
|
| - |
|
303 |
updateTarget( $config );
|
| 285 |
} else {
|
304 |
} else {
|
| 286 |
fatalError( "This server ($servername) is neither source nor target server as per config\n" );
|
305 |
fatalError( "This server ($servername) is neither source nor target server as per config\n" );
|
| 287 |
}
|
306 |
}
|
| 288 |
|
307 |
|
| 289 |
# add disk space utilization information on transport to the log
|
- |
|
| 290 |
logMsg( "Disk space utilization on transport disk:\n" . runCmd( "df -h $config->{transport}->{mount_point}" ) . "\n" );
|
- |
|
| 291 |
# add information about the server (zpools) to the log
|
- |
|
| 292 |
logMsg( "Zpools on server $servername:\n" . join( "\n", runCmd( "zpool list" ) ) . "\n" );
|
- |
|
| 293 |
|
- |
|
| 294 |
# unmount the sneakernet drive
|
- |
|
| 295 |
`umount $config->{transport}->{mount_point}`;
|
- |
|
| 296 |
# and remove the directory
|
308 |
cleanup( $config );
|
| 297 |
rmdir $config->{transport}->{mount_point};
|
- |
|
| 298 |
|
- |
|
| 299 |
|
309 |
|
| 300 |
1;
|
310 |
1;
|
| 301 |
|
- |
|
| 302 |
|
- |
|
| 303 |
#`cat $config->{input} | openssl enc -aes-256-cbc -K $config->{key} -iv $config->{IV} > $config->{output}`;
|
- |
|
| 304 |
|
- |
|
| 305 |
# this will decrypt $config->{output} to stdout
|
- |
|
| 306 |
#`cat $config->{output} | openssl enc -aes-256-cbc -d -K $config->{key} -iv $config->{IV} > test.out`;
|
- |
|