| Line 40... |
Line 40... |
| 40 |
# Version: 0.1 RWR 2025-12-10
|
40 |
# Version: 0.1 RWR 2025-12-10
|
| 41 |
# Development version
|
41 |
# Development version
|
| 42 |
#
|
42 |
#
|
| 43 |
# Version: 1.0 RWR 2025-12-15
|
43 |
# Version: 1.0 RWR 2025-12-15
|
| 44 |
# Tested and ready for initial release
|
44 |
# Tested and ready for initial release
|
| - |
|
45 |
#
|
| - |
|
46 |
# Version: 1.0.1 RWR 2025-12-15
|
| - |
|
47 |
# Added verbose logging control to logMsg calls, controlled by ZFS_Utils::$verboseLoggingLevel
|
| 45 |
|
48 |
|
| 46 |
|
49 |
|
| 47 |
use strict;
|
50 |
use strict;
|
| 48 |
use warnings;
|
51 |
use warnings;
|
| 49 |
|
52 |
|
| 50 |
our $VERSION = '1.0';
|
53 |
our $VERSION = '1.0.1';
|
| 51 |
|
54 |
|
| 52 |
use File::Basename;
|
55 |
use File::Basename;
|
| 53 |
use FindBin;
|
56 |
use FindBin;
|
| 54 |
use lib "$FindBin::Bin/..";
|
57 |
use lib "$FindBin::Bin/..";
|
| 55 |
use Data::Dumper;
|
58 |
use Data::Dumper;
|
| 56 |
use ZFS_Utils qw(loadConfig shredFile logMsg makeReplicateCommands mountDriveByLabel unmountDriveByLabel mountGeli runCmd sendReport fatalError getDirectoryList cleanDirectory $logFileName $displayLogsOnConsole);
|
59 |
use ZFS_Utils qw(loadConfig shredFile logMsg makeReplicateCommands mountDriveByLabel unmountDriveByLabel mountGeli runCmd sendReport fatalError getDirectoryList cleanDirectory $logFileName $displayLogsOnConsole $verboseLoggingLevel);
|
| 57 |
use Getopt::Long qw(GetOptions);
|
60 |
use Getopt::Long qw(GetOptions);
|
| 58 |
Getopt::Long::Configure ("bundling");
|
61 |
Getopt::Long::Configure ("bundling");
|
| 59 |
|
62 |
|
| 60 |
my $scriptDirectory = $FindBin::RealBin;
|
63 |
my $scriptDirectory = $FindBin::RealBin;
|
| 61 |
my $scriptFullPath = "$scriptDirectory/" . $FindBin::Script;
|
64 |
my $scriptFullPath = "$scriptDirectory/" . $FindBin::Script;
|
| Line 168... |
Line 171... |
| 168 |
# read in history/status file
|
171 |
# read in history/status file
|
| 169 |
my @lines = ();
|
172 |
my @lines = ();
|
| 170 |
if ( -e $filename && open my $fh, '<', $filename ) {
|
173 |
if ( -e $filename && open my $fh, '<', $filename ) {
|
| 171 |
chomp( @lines = <$fh> );
|
174 |
chomp( @lines = <$fh> );
|
| 172 |
close $fh;
|
175 |
close $fh;
|
| 173 |
logMsg("Read status file '$filename' with contents:\n" . join( "\n", @lines ) . "\n");
|
176 |
logMsg("Read status file '$filename' with contents:\n" . join( "\n", @lines ) . "\n") if $verboseLoggingLevel >= 3;
|
| 174 |
} else {
|
177 |
} else {
|
| 175 |
logMsg("Error: could not read status file '$filename', assuming a fresh start: $!");
|
178 |
logMsg("Error: could not read status file '$filename', assuming a fresh start: $!") if $verboseLoggingLevel >= 2;
|
| 176 |
}
|
179 |
}
|
| 177 |
return \@lines;
|
180 |
return \@lines;
|
| 178 |
}
|
181 |
}
|
| 179 |
|
182 |
|
| 180 |
## Write the status list to disk safely.
|
183 |
## Write the status list to disk safely.
|
| Line 189... |
Line 192... |
| 189 |
## - Logs the written contents. Dies on failure to backup or write the file.
|
192 |
## - Logs the written contents. Dies on failure to backup or write the file.
|
| 190 |
sub writeStatusFile {
|
193 |
sub writeStatusFile {
|
| 191 |
my ( $filename, $statusList ) = @_;
|
194 |
my ( $filename, $statusList ) = @_;
|
| 192 |
# backup existing status file
|
195 |
# backup existing status file
|
| 193 |
if ( -e $filename ) {
|
196 |
if ( -e $filename ) {
|
| 194 |
rename( $filename, "$filename.bak" ) or do {
|
197 |
rename( $filename, "$filename.bak" )
|
| 195 |
logMsg("Error: could not backup existing status file '$filename': $!");
|
198 |
or fatalError("Error: could not backup existing status file '$filename': $!");
|
| 196 |
die;
|
- |
|
| 197 |
};
|
- |
|
| 198 |
}
|
199 |
}
|
| 199 |
# write new status file
|
200 |
# write new status file
|
| 200 |
if ( open my $fh, '>', $filename ) {
|
201 |
if ( open my $fh, '>', $filename ) {
|
| 201 |
foreach my $line ( @$statusList ) {
|
202 |
foreach my $line ( @$statusList ) {
|
| 202 |
print $fh "$line\n";
|
203 |
print $fh "$line\n";
|
| 203 |
}
|
204 |
}
|
| 204 |
close $fh;
|
205 |
close $fh;
|
| 205 |
logMsg("Wrote status file '$filename' with contents:\n" . join( "\n", @$statusList ) . "\n");
|
206 |
logMsg("Wrote status file '$filename' with contents:\n" . join( "\n", @$statusList ) . "\n") if $verboseLoggingLevel >= 3;
|
| 206 |
} else {
|
207 |
} else {
|
| 207 |
logMsg("Error: could not write status file '$filename': $!");
|
208 |
fatalError("Error: could not write status file '$filename': $!");
|
| 208 |
die;
|
- |
|
| 209 |
}
|
209 |
}
|
| 210 |
}
|
210 |
}
|
| 211 |
|
211 |
|
| 212 |
## Convert a path-like dataset name into a filename-safe string.
|
212 |
## Convert a path-like dataset name into a filename-safe string.
|
| 213 |
##
|
213 |
##
|
| Line 245... |
Line 245... |
| 245 |
## Returns: ARRAYREF `$newStatus` containing updated status lines (latest snapshots per dataset).
|
245 |
## Returns: ARRAYREF `$newStatus` containing updated status lines (latest snapshots per dataset).
|
| 246 |
sub doSourceReplication {
|
246 |
sub doSourceReplication {
|
| 247 |
my ($config, $statusList) = @_;
|
247 |
my ($config, $statusList) = @_;
|
| 248 |
my $newStatus = [];
|
248 |
my $newStatus = [];
|
| 249 |
foreach my $dataset ( sort keys %{$config->{datasets}} ) {
|
249 |
foreach my $dataset ( sort keys %{$config->{datasets}} ) {
|
| 250 |
logMsg("Processing dataset '$dataset'");
|
250 |
logMsg("Processing dataset '$dataset'") if $verboseLoggingLevel >= 1;
|
| 251 |
# get list of all snapshots on dataset
|
251 |
# get list of all snapshots on dataset
|
| 252 |
my $sourceList;
|
252 |
my $sourceList;
|
| 253 |
if ( -e "$scriptDirectory/test.status") {
|
253 |
if ( -e "$scriptDirectory/test.status") {
|
| 254 |
$sourceList = getStatusFile( "$scriptDirectory/test.status" );
|
254 |
$sourceList = getStatusFile( "$scriptDirectory/test.status" );
|
| 255 |
} else {
|
255 |
} else {
|
| Line 270... |
Line 270... |
| 270 |
my $command = $commands->{$cmd};
|
270 |
my $command = $commands->{$cmd};
|
| 271 |
my $outputFile = $cmd;
|
271 |
my $outputFile = $cmd;
|
| 272 |
my $outfile = dirnameToFileName( $cmd );
|
272 |
my $outfile = dirnameToFileName( $cmd );
|
| 273 |
$command .= " | openssl enc -aes-256-cbc -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
|
273 |
$command .= " | openssl enc -aes-256-cbc -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
|
| 274 |
$command .= " > $config->{transport}->{mount_point}/$outfile";
|
274 |
$command .= " > $config->{transport}->{mount_point}/$outfile";
|
| 275 |
logMsg("Running command: $command");
|
275 |
logMsg("Running command: $command") if $verboseLoggingLevel >= 2;
|
| 276 |
runCmd( $command ) unless $config->{dryrun};
|
276 |
runCmd( $command ) unless $config->{dryrun};
|
| 277 |
}
|
277 |
}
|
| 278 |
} else {
|
278 |
} else {
|
| 279 |
logMsg( "Nothing to do for $dataset" );
|
279 |
logMsg( "Nothing to do for $dataset" ) if $verboseLoggingLevel >= 1;
|
| 280 |
}
|
280 |
}
|
| 281 |
}
|
281 |
}
|
| 282 |
return $newStatus;
|
282 |
return $newStatus;
|
| 283 |
}
|
283 |
}
|
| 284 |
|
284 |
|
| Line 295... |
Line 295... |
| 295 |
## - If `shutdown_after_replication` is set in the running role's config, attempts
|
295 |
## - If `shutdown_after_replication` is set in the running role's config, attempts
|
| 296 |
## to shut down the machine (honors `$config->{dryrun}`).
|
296 |
## to shut down the machine (honors `$config->{dryrun}`).
|
| 297 |
sub cleanup{
|
297 |
sub cleanup{
|
| 298 |
my ( $config, $message ) = @_;
|
298 |
my ( $config, $message ) = @_;
|
| 299 |
# add disk space utilization information on transport to the log
|
299 |
# add disk space utilization information on transport to the log
|
| 300 |
logMsg( "Disk space utilization on transport disk:\n" . runCmd( "df -h $config->{transport}->{mount_point}" ) . "\n" );
|
300 |
logMsg( "Disk space utilization on transport disk:\n" . runCmd( "df -h $config->{transport}->{mount_point}" ) . "\n" )
|
| - |
|
301 |
if $verboseLoggingLevel >= 1;
|
| 301 |
# add information about the server (zpools) to the log
|
302 |
# add information about the server (zpools) to the log
|
| 302 |
my $servername = `hostname -s`;
|
303 |
my $servername = `hostname -s`;
|
| 303 |
chomp $servername;
|
304 |
chomp $servername;
|
| 304 |
logMsg( "Zpools on server $servername:\n" . join( "\n", runCmd( "zpool list" ) ) . "\n" );
|
305 |
logMsg( "Zpools on server $servername:\n" . join( "\n", runCmd( "zpool list" ) ) . "\n" ) if $verboseLoggingLevel >= 1;
|
| 305 |
$config->{$config->{runningAs}}->{report}->{subject} //= "Replication Report for $config->{runningAs} server $servername";
|
306 |
$config->{$config->{runningAs}}->{report}->{subject} //= "Replication Report for $config->{runningAs} server $servername";
|
| 306 |
$message //= "Replication completed on $config->{runningAs} server $servername.";
|
307 |
$message //= "Replication completed on $config->{runningAs} server $servername.";
|
| 307 |
# unmount the sneakernet drive
|
308 |
# unmount the sneakernet drive
|
| 308 |
unmountDriveByLabel( $config->{transport} ) unless $config->{dryrun};
|
309 |
unmountDriveByLabel( $config->{transport} ) unless $config->{dryrun};
|
| 309 |
sendReport( $config->{$config->{runningAs}}->{report}, $message, $config->{log_file} );
|
310 |
sendReport( $config->{$config->{runningAs}}->{report}, $message, $config->{log_file} );
|
| 310 |
# If they have requested shutdown, do it now
|
311 |
# If they have requested shutdown, do it now
|
| 311 |
if ( $config->{$config->{runningAs}}->{shutdown_after_replication} ) {
|
312 |
if ( $config->{$config->{runningAs}}->{shutdown_after_replication} ) {
|
| 312 |
logMsg( "Shutting down target server as per configuration" );
|
313 |
logMsg( "Shutting down target server as per configuration" ) if $verboseLoggingLevel >= 0;
|
| 313 |
runCmd( "shutdown -p now" ) unless $config->{dryrun};
|
314 |
runCmd( "shutdown -p now" ) unless $config->{dryrun};
|
| 314 |
}
|
315 |
}
|
| 315 |
}
|
316 |
}
|
| 316 |
|
317 |
|
| 317 |
## Update the target zfs datasets from files on the transport drive.
|
318 |
## Update the target zfs datasets from files on the transport drive.
|
| Line 333... |
Line 334... |
| 333 |
my ($dataset) = split( /\Q\.\E/, $targetDataset ); # grab only the first element of a string which has internal delimiters
|
334 |
my ($dataset) = split( /\Q\.\E/, $targetDataset ); # grab only the first element of a string which has internal delimiters
|
| 334 |
$targetDataset = $config->{datasets}->{$dataset}->{target} . '/' . dirnameToFileName( $targetDataset, '.', '/' );
|
335 |
$targetDataset = $config->{datasets}->{$dataset}->{target} . '/' . dirnameToFileName( $targetDataset, '.', '/' );
|
| 335 |
my $command = "cat $filename";
|
336 |
my $command = "cat $filename";
|
| 336 |
$command .= " | openssl enc -aes-256-cbc -d -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
|
337 |
$command .= " | openssl enc -aes-256-cbc -d -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
|
| 337 |
$command .= " | zfs receive -F $targetDataset";
|
338 |
$command .= " | zfs receive -F $targetDataset";
|
| 338 |
logMsg( $command );
|
339 |
logMsg( $command ) if $verboseLoggingLevel >= 2;
|
| 339 |
runCmd( $command );
|
340 |
runCmd( $command );
|
| 340 |
}
|
341 |
}
|
| 341 |
}
|
342 |
}
|
| 342 |
|
343 |
|
| 343 |
##################### main program starts here #####################
|
344 |
##################### main program starts here #####################
|
| Line 365... |
Line 366... |
| 365 |
} elsif (defined $config->{version}) {
|
366 |
} elsif (defined $config->{version}) {
|
| 366 |
print "$FindBin::Script v$VERSION\n";
|
367 |
print "$FindBin::Script v$VERSION\n";
|
| 367 |
exit 0;
|
368 |
exit 0;
|
| 368 |
}
|
369 |
}
|
| 369 |
|
370 |
|
| - |
|
371 |
# set some defaults in library from config
|
| - |
|
372 |
$verboseLoggingLevel = $config->{verbose} // 0;
|
| 370 |
# set some defaults
|
373 |
# status file path
|
| 371 |
$config->{'status_file'} //= "$scriptFullPath.status";
|
374 |
$config->{'status_file'} //= "$scriptFullPath.status";
|
| 372 |
# set log file name for sub logMsg in ZFS_Utils, and remove the old log if it exists
|
375 |
# set log file name for sub logMsg in ZFS_Utils, and remove the old log if it exists
|
| 373 |
# Log file is only valid for one run
|
376 |
# Log file is only valid for one run
|
| 374 |
$logFileName = $config->{'log_file'} //= "$scriptFullPath.log";
|
377 |
$logFileName = $config->{'log_file'} //= "$scriptFullPath.log";
|
| 375 |
# log only for one run
|
378 |
# log only for one run
|
| Line 381... |
Line 384... |
| 381 |
my $servername = `hostname -s`;
|
384 |
my $servername = `hostname -s`;
|
| 382 |
chomp $servername;
|
385 |
chomp $servername;
|
| 383 |
$config->{runningAs} = $servername eq $config->{source}->{hostname} ? 'source' :
|
386 |
$config->{runningAs} = $servername eq $config->{source}->{hostname} ? 'source' :
|
| 384 |
$servername eq $config->{target}->{hostname} ? 'target' : 'unknown';
|
387 |
$servername eq $config->{target}->{hostname} ? 'target' : 'unknown';
|
| 385 |
|
388 |
|
| 386 |
#cleanup( $config, "Testing" );
|
- |
|
| 387 |
|
- |
|
| 388 |
# mount the transport drive, fatal error if we can not find it
|
389 |
# mount the transport drive, fatal error if we can not find it
|
| 389 |
fatalError( "Unable to mount tranport drive with label $config->{transport}->{label}", $config, \&cleanup )
|
390 |
fatalError( "Unable to mount tranport drive with label $config->{transport}->{label}", $config, \&cleanup )
|
| 390 |
unless $config->{transport}->{mount_point} = mountDriveByLabel( $config->{transport} );
|
391 |
unless $config->{transport}->{mount_point} = mountDriveByLabel( $config->{transport} );
|
| 391 |
|
392 |
|
| 392 |
# main program logic
|
393 |
# main program logic
|
| 393 |
if ( $config->{runningAs} eq 'source' ) {
|
394 |
if ( $config->{runningAs} eq 'source' ) {
|
| 394 |
logMsg "Running as source server";
|
395 |
logMsg "Running as source server" if $verboseLoggingLevel >= 1;
|
| 395 |
# remove all files from transport disk, but leave all subdirectories alone
|
396 |
# remove all files from transport disk, but leave all subdirectories alone
|
| 396 |
fatalError( "Failed to clean transport directory $config->{transport}->{mount_point}", $config, \&cleanup )
|
397 |
fatalError( "Failed to clean transport directory $config->{transport}->{mount_point}", $config, \&cleanup )
|
| 397 |
unless $config->{dryrun} or cleanDirectory( $config->{transport}->{mount_point} );
|
398 |
unless $config->{dryrun} or cleanDirectory( $config->{transport}->{mount_point} );
|
| 398 |
my $statusList = getStatusFile($config->{status_file});
|
399 |
my $statusList = getStatusFile($config->{status_file});
|
| 399 |
$statusList = doSourceReplication($config, $statusList);
|
400 |
$statusList = doSourceReplication($config, $statusList);
|
| 400 |
writeStatusFile($config->{status_file}, $statusList) unless $config->{dryrun};
|
401 |
writeStatusFile($config->{status_file}, $statusList) unless $config->{dryrun};
|
| 401 |
} elsif ( $config->{runningAs} eq 'target' ) {
|
402 |
} elsif ( $config->{runningAs} eq 'target' ) {
|
| 402 |
logMsg "Running as target server";
|
403 |
logMsg "Running as target server" if $verboseLoggingLevel >= 1;
|
| 403 |
mountGeli( $config->{target}->{geli} ) if ( defined $config->{target}->{geli} );
|
404 |
mountGeli( $config->{target}->{geli} ) if ( defined $config->{target}->{geli} );
|
| 404 |
umountDiskByLabel( $config->{target}->{geli}->{secureKey} )
|
405 |
umountDiskByLabel( $config->{target}->{geli}->{secureKey} )
|
| 405 |
unless $config->{target}->{geli}->{secureKey}->{label} eq $config->{transport}->{label};
|
406 |
unless $config->{target}->{geli}->{secureKey}->{label} eq $config->{transport}->{label};
|
| 406 |
print "Please insert device labeled REPORT\n" if $config->{target}->{report}->{targetDrive}->{label};
|
407 |
print "Please insert device labeled REPORT\n" if $config->{target}->{report}->{targetDrive}->{label};
|
| 407 |
updateTarget( $config );
|
408 |
updateTarget( $config );
|