Subversion Repositories zfs_utils

Rev

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

Rev 57 Rev 60
Line 24... Line 24...
24
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 
28
 
29
# sneakernet.pl
29
# sneakernet
30
# Script to perform sneakernet replication of ZFS datasets between two servers
30
# Script to perform sneakernet replication of ZFS datasets between two servers
31
# using an external transport drive.
31
# using an external transport drive.
32
# Uses ZFS send/receive to replicate datasets to/from the transport drive.
32
# Uses ZFS send/receive to replicate datasets to/from the transport drive.
33
# Optionally uses symmetric encryption to encrypt datasets during transport.
33
# Optionally uses symmetric encryption to encrypt datasets during transport.
34
# On the target server, can optionally use GELI to encrypt the datasets on disk.
34
# On the target server, can optionally use GELI to encrypt the datasets on disk.
Line 43... Line 43...
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
#
45
#
46
# Version: 1.0.1 RWR 2025-12-15
46
# Version: 1.0.1 RWR 2025-12-15
47
# Added verbose logging control to logMsg calls, controlled by ZFS_Utils::$verboseLoggingLevel
47
# Added verbose logging control to logMsg calls, controlled by ZFS_Utils::$verboseLoggingLevel
-
 
48
#
-
 
49
# Version: 1.1 RWR 2025-12-17
-
 
50
# Added filtering so only the snapshots for the current dataset are considered when
-
 
51
# generating replication commands.
-
 
52
# added test.source.status (in script directory) file for dry-run testing of source replication.
-
 
53
# changed default name of status file to sneakernet_target.status to avoid confusion.
-
 
54
# added more logging of source/target snapshots in doSourceReplication for debugging.
-
 
55
#
-
 
56
# Version: 1.1.1 RWR 2025-12-18
-
 
57
# Fixed makeReplicateCommands call to pass actual dataset name from config rather than config key.
-
 
58
# Updated documentation to clarify how makeReplicateCommands filters snapshots by parent+dataset path
-
 
59
# to avoid false matches with similarly-named datasets in different locations.
48
 
60
 
49
 
61
 
50
use strict;
62
use strict;
51
use warnings;
63
use warnings;
52
 
64
 
53
our $VERSION = '1.0.1';
65
our $VERSION = '1.1.1';
54
 
66
 
55
use File::Basename;
67
use File::Basename;
56
use FindBin;
68
use FindBin;
57
use lib "$FindBin::Bin/..";
69
use lib "$FindBin::Bin/..";
58
use Data::Dumper;
70
use Data::Dumper;
Line 69... Line 81...
69
my $configFileName = "$scriptFullPath.conf.yaml";
81
my $configFileName = "$scriptFullPath.conf.yaml";
70
 
82
 
71
my $config = {
83
my $config = {
72
   'dryrun' => 0,
84
   'dryrun' => 0,
73
   'verbosity' => 1,
85
   'verbosity' => 1,
74
   # file created on source server to track last copyed dataset
86
   # file created on source server to track last copied dataset, assuming
-
 
87
   # the import goes well on the target server
75
   'status_file' => "$scriptFullPath.status",
88
   'status_file' => "$scriptDirectory/sneakernet_target.status",
76
   'log_file' => "$scriptFullPath.log",
89
   'log_file' => "$scriptFullPath.log",
77
   #information about source server
90
   #information about source server
78
   'source' => {
91
   'source' => {
79
      'hostname' => '', # used to see if we are on source
92
      'hostname' => '', # used to see if we are on source
80
      'poolname' => 'pool', # name of the ZFS pool to export
93
      'poolname' => 'pool', # name of the ZFS pool to export
Line 235... Line 248...
235
##   $statusList - ARRAYREF of previously replicated snapshot full names (used to compute incremental sends)
248
##   $statusList - ARRAYREF of previously replicated snapshot full names (used to compute incremental sends)
236
##
249
##
237
## Behavior:
250
## Behavior:
238
##   - Iterates over datasets defined in `$config->{datasets}`.
251
##   - Iterates over datasets defined in `$config->{datasets}`.
239
##   - For each dataset, enumerates available snapshots on the source and calls
252
##   - For each dataset, enumerates available snapshots on the source and calls
240
##     `makeReplicateCommands` to produce zfs send commands.
253
##     `makeReplicateCommands` (from ZFS_Utils) to produce zfs send commands.
-
 
254
##   - `makeReplicateCommands` filters snapshots by matching parent+dataset path (e.g., ^pool/dataset(?:/|@))
-
 
255
##     to avoid false matches with similarly-named datasets, and intelligently determines whether to use
-
 
256
##     recursive vs per-filesystem sends and incremental vs full sends based on snapshot availability.
241
##   - Commands are optionally piped through `openssl enc` when `transport.encryption.key` is set.
257
##   - Commands are optionally piped through `openssl enc` when `transport.encryption.key` is set.
242
##   - The output is written to files on the transport mount point (one file per dataset snapshot set).
258
##   - The output is written to files on the transport mount point (one file per dataset snapshot set).
243
##   - Respects `$config->{dryrun}`: no commands are executed when dryrun is enabled.
259
##   - Respects `$config->{dryrun}`: no commands are executed when dryrun is enabled.
244
##
260
##
245
## Returns: ARRAYREF `$newStatus` containing updated status lines (latest snapshots per dataset).
261
## Returns: ARRAYREF `$newStatus` containing updated status lines (latest snapshots per dataset).
246
sub doSourceReplication {
262
sub doSourceReplication {
247
   my ($config, $statusList) = @_;
263
   my ($config, $targetList) = @_;
248
   my $newStatus = [];
264
   my $newStatus = [];
249
   foreach my $dataset ( sort keys %{$config->{datasets}} ) {
265
   foreach my $dataset ( sort keys %{$config->{datasets}} ) {
250
      logMsg("Processing dataset '$dataset'") if $verboseLoggingLevel >= 1;
266
      logMsg("Processing dataset '$dataset'") if $verboseLoggingLevel >= 1;
251
      # get list of all snapshots on dataset
267
      # get list of all snapshots on dataset
252
      my $sourceList;
268
      my $sourceList;
253
      if ( -e "$scriptDirectory/test.status") {
269
      if ( -e "$scriptDirectory/test.source.status" && $config->{dryrun} ) {
-
 
270
         logMsg("Using test.source.status file for source snapshots in dry-run mode") if $verboseLoggingLevel >= 2;
254
         $sourceList = getStatusFile( "$scriptDirectory/test.status" );
271
         $sourceList = getStatusFile( "$scriptDirectory/test.source.status" );
255
      } else {
272
      } else {
256
         $sourceList = [ runCmd( "zfs list -rt snap -H -o name $config->{datasets}->{$dataset}->{source}" ) ];
273
         $sourceList = [ runCmd( "zfs list -rt snap -H -o name $config->{datasets}->{$dataset}->{source}/$config->{datasets}->{$dataset}->{dataset}" ) ];
-
 
274
      }
-
 
275
 
-
 
276
      if ($verboseLoggingLevel >= 5) {
-
 
277
         logMsg("Source snapshots for dataset '$dataset': " . join( ', ', sort @$sourceList ) );
-
 
278
         logMsg("Target snapshots for dataset '$dataset': " . join( ', ', sort @$targetList ) );
257
      }
279
      }
258
      
-
 
259
      # process dataset here
280
      # process dataset here using makeReplicateCommands from ZFS_Utils
-
 
281
      # $dataset here is the config key; the actual dataset name is in $config->{datasets}->{$dataset}->{dataset}
260
      my $commands = makeReplicateCommands( 
282
      my $commands = makeReplicateCommands( 
261
                        $sourceList,
283
                        $sourceList,
262
                        $statusList,
284
                        $targetList,
263
                        $dataset,
285
                        $config->{datasets}->{$dataset}->{dataset},
264
                        $config->{datasets}->{$dataset}->{source},
286
                        $config->{datasets}->{$dataset}->{source},
265
                        $config->{datasets}->{$dataset}->{target},
287
                        $config->{datasets}->{$dataset}->{target},
266
                        $newStatus
288
                        $newStatus
267
                     );
289
                     );
268
      if ( %$commands ) {
290
      if ( %$commands ) {
Line 296... Line 318...
296
##     to shut down the machine (honors `$config->{dryrun}`).
318
##     to shut down the machine (honors `$config->{dryrun}`).
297
sub cleanup{
319
sub cleanup{
298
   my ( $config, $message ) = @_;
320
   my ( $config, $message ) = @_;
299
   # add disk space utilization information on transport to the log
321
   # 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" )
322
   logMsg( "Disk space utilization on transport disk:\n" . runCmd( "df -h $config->{transport}->{mount_point}" ) . "\n" )
301
      if $verboseLoggingLevel >= 1;
323
      if -d $config->{transport}->{mount_point} && $verboseLoggingLevel >= 1;
302
   # add information about the server (zpools) to the log
324
   # add information about the server (zpools) to the log
303
   my $servername = `hostname -s`;
325
   my $servername = `hostname -s`;
304
   chomp $servername;
326
   chomp $servername;
305
   logMsg( "Zpools on server $servername:\n" . join( "\n", runCmd( "zpool list" ) ) . "\n" ) if $verboseLoggingLevel >= 1;
327
   logMsg( "Zpools on server $servername:\n" . join( "\n", runCmd( "zpool list" ) ) . "\n" ) if `which zpool` && $verboseLoggingLevel >= 1;
306
   $config->{$config->{runningAs}}->{report}->{subject} //= "Replication Report for $config->{runningAs} server $servername";
328
   $config->{$config->{runningAs}}->{report}->{subject} //= "Replication Report for $config->{runningAs} server $servername";
307
   $message //= "Replication completed on $config->{runningAs} server $servername.";
329
   $message //= "Replication completed on $config->{runningAs} server $servername.";
308
   # unmount the sneakernet drive
330
   # unmount the sneakernet drive
309
   unmountDriveByLabel( $config->{transport} ) unless $config->{dryrun};
331
   unmountDriveByLabel( $config->{transport} ) unless $config->{dryrun};
310
   sendReport( $config->{$config->{runningAs}}->{report}, $message, $config->{log_file} );
332
   sendReport( $config->{$config->{runningAs}}->{report}, $message, $config->{log_file} );
Line 386... Line 408...
386
$config->{runningAs} = $servername eq $config->{source}->{hostname} ? 'source' :
408
$config->{runningAs} = $servername eq $config->{source}->{hostname} ? 'source' :
387
                $servername eq $config->{target}->{hostname} ? 'target' : 'unknown';
409
                $servername eq $config->{target}->{hostname} ? 'target' : 'unknown';
388
 
410
 
389
# mount the transport drive, fatal error if we can not find it
411
# mount the transport drive, fatal error if we can not find it
390
fatalError( "Unable to mount tranport drive with label $config->{transport}->{label}", $config, \&cleanup )
412
fatalError( "Unable to mount tranport drive with label $config->{transport}->{label}", $config, \&cleanup )
391
   unless $config->{transport}->{mount_point} =  mountDriveByLabel( $config->{transport} );
413
   unless $config->{dryrun} or $config->{transport}->{mount_point} =  mountDriveByLabel( $config->{transport} );
392
 
414
 
393
# main program logic
415
# main program logic
394
if ( $config->{runningAs} eq 'source' ) {
416
if ( $config->{runningAs} eq 'source' ) {
395
    logMsg "Running as source server" if $verboseLoggingLevel >= 1;
417
    logMsg "Running as source server" if $verboseLoggingLevel >= 1;
396
    # remove all files from transport disk, but leave all subdirectories alone
418
    # remove all files from transport disk, but leave all subdirectories alone