Subversion Repositories zfs_utils

Rev

Rev 48 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 48 Rev 51
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 );