Subversion Repositories zfs_utils

Rev

Rev 33 | Rev 35 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 33 Rev 34
Line 1... Line 1...
1
#! /usr/bin/env perl
1
#! /usr/bin/env perl
2
 
2
 
-
 
3
# Simplified BSD License (FreeBSD License)
-
 
4
#
-
 
5
# Copyright (c) 2025, Daily Data Inc.
-
 
6
# All rights reserved.
-
 
7
#
-
 
8
# Redistribution and use in source and binary forms, with or without
-
 
9
# modification, are permitted provided that the following conditions are met:
-
 
10
#
-
 
11
# 1. Redistributions of source code must retain the above copyright notice, this
-
 
12
#    list of conditions and the following disclaimer.
-
 
13
#
-
 
14
# 2. Redistributions in binary form must reproduce the above copyright notice,
-
 
15
#    this list of conditions and the following disclaimer in the documentation
-
 
16
#    and/or other materials provided with the distribution.
-
 
17
#
-
 
18
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-
 
19
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-
 
20
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-
 
21
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-
 
22
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-
 
23
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-
 
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,
-
 
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.
-
 
28
 
-
 
29
# sneakernet.pl
-
 
30
# Script to perform sneakernet replication of ZFS datasets between two servers
-
 
31
# using an external 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.
-
 
34
# On the target server, can optionally use GELI to encrypt the datasets on disk.
-
 
35
# Requires a configuration file in YAML format next to the script.
-
 
36
# Author: R. W. Rodlico <rodo@dailydata.net>
-
 
37
# Created: December 2025
-
 
38
# Revision History:
-
 
39
# Version: 0.1 2025-12-10 Initial version
-
 
40
 
3
use strict;
41
use strict;
4
use warnings;
42
use warnings;
5
 
43
 
-
 
44
our $VERSION = '0.1';
-
 
45
 
6
use FindBin;
46
use FindBin;
7
use lib "$FindBin::Bin/..";
47
use lib "$FindBin::Bin/..";
8
use Data::Dumper;
48
use Data::Dumper;
9
use ZFS_Utils qw(loadConfig shredFile logMsg makeReplicateCommands mountDriveByLabel mountGeli runCmd $logFileName $displayLogsOnConsole);
49
use ZFS_Utils qw(loadConfig shredFile logMsg makeReplicateCommands mountDriveByLabel mountGeli runCmd $logFileName $displayLogsOnConsole);
10
 
50
 
Line 25... Line 65...
25
   # file created on source server to track last copyed dataset
65
   # file created on source server to track last copyed dataset
26
   'status_file' => "$0.status",
66
   'status_file' => "$0.status",
27
   #information about source server
67
   #information about source server
28
   'source_server' => {
68
   'source_server' => {
29
      'hostname' => '', # used to see if we are on source
69
      'hostname' => '', # used to see if we are on source
30
      'poolname' => '', # name of the ZFS pool to export
70
      'poolname' => 'pool', # name of the ZFS pool to export
-
 
71
      # if set, will generate a report via email or by storing on a drive
-
 
72
      'report' => {
-
 
73
         'email' => 'tech@example.org',
-
 
74
         'subject' => 'AG Transport Report',
-
 
75
         'targetDrive' => {
-
 
76
            'label' => '',
-
 
77
            'mount_point' => '',
-
 
78
         }
-
 
79
      }
31
   },
80
   },
32
   #information about target server
81
   #information about target server
33
   'target_server' => {
82
   'target_server' => {
34
      'hostname' => '', # used to see if we are on target
83
      'hostname' => '', # used to see if we are on target
35
      'poolname' => '', # name of the ZFS pool to import
84
      'poolname' => 'backup', # name of the ZFS pool to import
36
      # if this is set, the dataset uses GELI, so we must decrypt and
85
      # if this is set, the dataset uses GELI, so we must decrypt and
37
      # mount it first
86
      # mount it first
38
      'geli' => {
87
      'geli' => {
39
         'keydiskname' => 'replica', # the GPT label of the key disk
88
         'keydiskname' => 'replica', # the GPT label of the key disk
40
         'keyfile' => 'geli.key', # the name of the key file on keydiskname
89
         'keyfile' => 'geli.key', # the name of the key file on keydiskname
41
         'localKey' => 'e98c660cccdae1226550484d62caa2b72f60632ae0c607528aba1ac9e7bfbc9c', # hex representation of the local key part
90
         'localKey' => 'e98c660cccdae1226550484d62caa2b72f60632ae0c607528aba1ac9e7bfbc9c', # hex representation of the local key part
42
         'target' => '/media/geli.key', # location to create the combined keyfile
91
         'target' => '/media/geli.key', # location to create the combined keyfile
43
         'poolname' => '', # name of the ZFS pool to import
92
         'poolname' => 'backup', # name of the ZFS pool to import
44
         'diskList' => [ 
93
         'diskList' => [ 
45
            '/dev/gpt/sneakernet_disk' 
94
            'da0',
-
 
95
            'da1'
46
            ], # list of disks to try to mount the dataset from
96
            ], # list of disks to try to mount the dataset from
-
 
97
      },
-
 
98
      'report' => {
-
 
99
         'email' => '',
-
 
100
         'subject' => '',
-
 
101
         'targetDrive' => {
-
 
102
            'label' => 'sneakernet_report',
-
 
103
            'mount_point' => '/mnt/sneakernet_report',
-
 
104
         }
47
      }
105
      }
48
   },
106
   },
49
   
-
 
50
   'transport' => {
107
   'transport' => {
51
      # this is the GPT label of the sneakernet disk
108
      # this is the GPT label of the sneakernet disk
52
      'disk_label' => 'sneakernet',
109
      'disk_label' => 'sneakernet',
53
      # where we want to mount it
110
      # where we want to mount it
54
      'mount_point' => '/mnt/sneakernet',
111
      'mount_point' => '/mnt/sneakernet',
Line 59... Line 116...
59
         'key'    => '', # openssl rand 32 | xxd -p | tr -d '\n' > test.key
116
         'key'    => '', # openssl rand 32 | xxd -p | tr -d '\n' > test.key
60
         'IV'     => '00000000000000000000000000000000',
117
         'IV'     => '00000000000000000000000000000000',
61
      },
118
      },
62
   },
119
   },
63
   'datasets' => {
120
   'datasets' => {
64
      'iscsi' => {
121
      'dataset1' => {
65
         'source' => 'storage/backup/iscsi',
122
         'source' => 'pool/dataset1',
66
         'target' => 'storage/backup/iscsi',
123
         'target' => 'backup/dataset1',
67
         'filename' => 'iscsi'
124
         'filename' => 'dataset1'
68
      },
125
      },
69
      'files_share'  => {
126
      'files_share'  => {
70
         'source' => 'storage/backup/files_share',
127
         'source' => 'pool/files_share',
71
         'target' => 'storage/backup/files_share',
128
         'target' => 'backup/files_share',
72
         'filename' => 'files_share'
129
         'filename' => 'files_share'
73
      },
130
      },
74
   }
131
   }
75
};
132
};
76
 
133
 
77
# read the status file and return as list
134
# read the status file and return as list
78
sub getStatusFile {
135
sub getStatusFile {
79
   my $filename = shift;
136
   my $filename = shift;
80
   # read in history/status file
137
   # read in history/status file
81
   my @lines;
138
   my @lines = ();
82
   if ( -e $filename && open my $fh, '<', $filename ) {
139
   if ( -e $filename && open my $fh, '<', $filename ) {
83
      chomp( @lines = <$fh> );
140
      chomp( @lines = <$fh> );
84
      close $fh;
141
      close $fh;
85
      logMsg("Read status file '$filename' with contents:\n" . join( "\n", @lines ) . "\n");
142
      logMsg("Read status file '$filename' with contents:\n" . join( "\n", @lines ) . "\n");
86
   } else {
143
   } else {
87
      logMsg("Error: could not read status file '$filename': $!");
144
      logMsg("Error: could not read status file '$filename', assuming a fresh start: $!");
88
      die;
-
 
89
   }
145
   }
90
   return \@lines;
146
   return \@lines;
91
}
147
}
92
 
148
 
93
# write the status list to file
149
# write the status list to file
Line 161... Line 217...
161
   my @files = map{ $dirname . "/$_" } grep { -f "$dirname/$_" } readdir($dh);
217
   my @files = map{ $dirname . "/$_" } grep { -f "$dirname/$_" } readdir($dh);
162
   closedir $dh;
218
   closedir $dh;
163
   foreach my $file (@files) {
219
   foreach my $file (@files) {
164
      unlink $file or warn "Could not unlink $file: #!\n";
220
      unlink $file or warn "Could not unlink $file: #!\n";
165
   }
221
   }
166
   return;
-
 
167
 }
222
 }
168
    
223
    
169
 
224
 
170
 
225
 
171
# how to handle a fatal error
226
# how to handle a fatal error
Line 183... Line 238...
183
 
238
 
184
# If a YAML config file exists next to the script, load and merge it
239
# If a YAML config file exists next to the script, load and merge it
185
$config = loadConfig($configFileName, $config );
240
$config = loadConfig($configFileName, $config );
186
 
241
 
187
# set some defaults
242
# set some defaults
188
$config->{'status_file'} = "$0.status" unless ( defined $config->{'status_file'} );
243
$config->{'status_file'} //= "$0.status";
189
 
-
 
190
 
244
 
191
fatalError( "Invalid config file: missing source and/or target server" )
245
fatalError( "Invalid config file: missing source and/or target server" )
192
    unless (defined $config->{source_server} && defined $config->{target_server});
246
    unless (defined $config->{source_server} && defined $config->{target_server});
193
 
247
 
194
# mount the transport drive, fatal error if we can not find it
248
# mount the transport drive, fatal error if we can not find it