| 24 |
rodolico |
1 |
#! /usr/bin/env perl
|
|
|
2 |
|
| 34 |
rodolico |
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
|
| 48 |
rodolico |
38 |
#
|
| 34 |
rodolico |
39 |
# Revision History:
|
| 48 |
rodolico |
40 |
# Version: 0.1 RWR 2025-12-10
|
|
|
41 |
# Development version
|
|
|
42 |
#
|
|
|
43 |
# Version: 1.0 RWR 2025-12-15
|
|
|
44 |
# Tested and ready for initial release
|
| 34 |
rodolico |
45 |
|
| 48 |
rodolico |
46 |
|
| 24 |
rodolico |
47 |
use strict;
|
|
|
48 |
use warnings;
|
|
|
49 |
|
| 48 |
rodolico |
50 |
our $VERSION = '1.0';
|
| 34 |
rodolico |
51 |
|
| 46 |
rodolico |
52 |
use File::Basename;
|
| 24 |
rodolico |
53 |
use FindBin;
|
|
|
54 |
use lib "$FindBin::Bin/..";
|
| 27 |
rodolico |
55 |
use Data::Dumper;
|
| 46 |
rodolico |
56 |
use ZFS_Utils qw(loadConfig shredFile logMsg makeReplicateCommands mountDriveByLabel unmountDriveByLabel mountGeli runCmd sendReport fatalError getDirectoryList cleanDirectory $logFileName $displayLogsOnConsole);
|
| 44 |
rodolico |
57 |
use Getopt::Long qw(GetOptions);
|
|
|
58 |
Getopt::Long::Configure ("bundling");
|
| 24 |
rodolico |
59 |
|
| 35 |
rodolico |
60 |
my $scriptDirectory = $FindBin::RealBin;
|
|
|
61 |
my $scriptFullPath = "$scriptDirectory/" . $FindBin::Script;
|
|
|
62 |
|
| 24 |
rodolico |
63 |
# display all log messages on console in addition to the log file
|
|
|
64 |
$displayLogsOnConsole = 1;
|
|
|
65 |
|
| 35 |
rodolico |
66 |
my $configFileName = "$scriptFullPath.conf.yaml";
|
| 24 |
rodolico |
67 |
|
|
|
68 |
my $config = {
|
| 44 |
rodolico |
69 |
'dryrun' => 0,
|
|
|
70 |
'verbosity' => 1,
|
| 24 |
rodolico |
71 |
# file created on source server to track last copyed dataset
|
| 35 |
rodolico |
72 |
'status_file' => "$scriptFullPath.status",
|
|
|
73 |
'log_file' => "$scriptFullPath.log",
|
| 24 |
rodolico |
74 |
#information about source server
|
| 37 |
rodolico |
75 |
'source' => {
|
| 24 |
rodolico |
76 |
'hostname' => '', # used to see if we are on source
|
| 34 |
rodolico |
77 |
'poolname' => 'pool', # name of the ZFS pool to export
|
|
|
78 |
# if set, will generate a report via email or by storing on a drive
|
|
|
79 |
'report' => {
|
|
|
80 |
'email' => 'tech@example.org',
|
|
|
81 |
'subject' => 'AG Transport Report',
|
|
|
82 |
'targetDrive' => {
|
| 35 |
rodolico |
83 |
'fstype' => '', # filesystem type of the report drive
|
|
|
84 |
# How often to check for the disk (seconds), message displayed every interval
|
|
|
85 |
'check_interval' => 15,
|
| 34 |
rodolico |
86 |
'label' => '',
|
|
|
87 |
'mount_point' => '',
|
|
|
88 |
}
|
|
|
89 |
}
|
| 24 |
rodolico |
90 |
},
|
|
|
91 |
#information about target server
|
| 37 |
rodolico |
92 |
'target' => {
|
| 24 |
rodolico |
93 |
'hostname' => '', # used to see if we are on target
|
| 34 |
rodolico |
94 |
'poolname' => 'backup', # name of the ZFS pool to import
|
| 42 |
rodolico |
95 |
'shutdown_after_replication' => 0, # if set to 1, will shutdown the server after replication
|
| 24 |
rodolico |
96 |
# if this is set, the dataset uses GELI, so we must decrypt and
|
|
|
97 |
# mount it first
|
|
|
98 |
'geli' => {
|
| 35 |
rodolico |
99 |
'secureKey ' => {
|
|
|
100 |
'label' => 'replica', # the GPT label of the key disk
|
|
|
101 |
'fstype' => 'ufs', # filesystem type of the key disk
|
|
|
102 |
'check_interval' => 15,
|
|
|
103 |
'wait_timeout' => 300,
|
|
|
104 |
'keyfile' => 'geli.key', # the name of the key file on the secureKey disk
|
|
|
105 |
},
|
| 24 |
rodolico |
106 |
'localKey' => 'e98c660cccdae1226550484d62caa2b72f60632ae0c607528aba1ac9e7bfbc9c', # hex representation of the local key part
|
|
|
107 |
'target' => '/media/geli.key', # location to create the combined keyfile
|
| 34 |
rodolico |
108 |
'poolname' => 'backup', # name of the ZFS pool to import
|
| 24 |
rodolico |
109 |
'diskList' => [
|
| 34 |
rodolico |
110 |
'da0',
|
|
|
111 |
'da1'
|
| 24 |
rodolico |
112 |
], # list of disks to try to mount the dataset from
|
| 34 |
rodolico |
113 |
},
|
|
|
114 |
'report' => {
|
|
|
115 |
'email' => '',
|
|
|
116 |
'subject' => '',
|
|
|
117 |
'targetDrive' => {
|
| 35 |
rodolico |
118 |
'fstype' => 'msdos', # filesystem type of the report drive
|
|
|
119 |
'label' => 'sneakernet',
|
|
|
120 |
'mount_point' => '',
|
| 34 |
rodolico |
121 |
}
|
| 24 |
rodolico |
122 |
}
|
|
|
123 |
},
|
|
|
124 |
'transport' => {
|
|
|
125 |
# this is the GPT label of the sneakernet disk
|
| 37 |
rodolico |
126 |
'label' => 'sneakernet',
|
| 35 |
rodolico |
127 |
# this is the file system type. Not needed if ufs
|
|
|
128 |
'fstype' => 'ufs',
|
| 24 |
rodolico |
129 |
# where we want to mount it
|
|
|
130 |
'mount_point' => '/mnt/sneakernet',
|
|
|
131 |
# amount of time to wait for the disk to appear
|
|
|
132 |
'timeout' => 600,
|
| 35 |
rodolico |
133 |
# How often to check for the disk (seconds), message displayed every interval
|
|
|
134 |
'check_interval' => 15,
|
| 24 |
rodolico |
135 |
# if set, all files will be encrypted with this key/IV during transport
|
|
|
136 |
'encryption' => {
|
|
|
137 |
'key' => '', # openssl rand 32 | xxd -p | tr -d '\n' > test.key
|
|
|
138 |
'IV' => '00000000000000000000000000000000',
|
|
|
139 |
},
|
|
|
140 |
},
|
|
|
141 |
'datasets' => {
|
| 34 |
rodolico |
142 |
'dataset1' => {
|
| 42 |
rodolico |
143 |
'source' => 'pool', # the parent of the dataset on the source
|
|
|
144 |
'target' => 'backup', # the parent of the dataset on the target
|
|
|
145 |
'dataset' => 'dataset1', # the dataset name
|
| 24 |
rodolico |
146 |
},
|
|
|
147 |
'files_share' => {
|
| 42 |
rodolico |
148 |
'source' => 'pool',
|
|
|
149 |
'target' => 'backup',
|
| 43 |
rodolico |
150 |
'dataset' => 'files_share',
|
| 24 |
rodolico |
151 |
},
|
|
|
152 |
}
|
|
|
153 |
};
|
|
|
154 |
|
| 48 |
rodolico |
155 |
## Read the status file and return its lines as an ARRAYREF.
|
|
|
156 |
##
|
|
|
157 |
## Arguments:
|
|
|
158 |
## $filename - path to the status file (string)
|
|
|
159 |
##
|
|
|
160 |
## Behavior:
|
|
|
161 |
## - If the file exists and is readable, reads all lines, chomps newlines and returns an ARRAYREF
|
|
|
162 |
## containing the lines (each line generally holds a fully qualified snapshot name).
|
|
|
163 |
## - If the file does not exist or cannot be opened, logs a message and returns an empty ARRAYREF.
|
|
|
164 |
##
|
|
|
165 |
## Returns: ARRAYREF of lines (possibly empty).
|
| 30 |
rodolico |
166 |
sub getStatusFile {
|
|
|
167 |
my $filename = shift;
|
|
|
168 |
# read in history/status file
|
| 34 |
rodolico |
169 |
my @lines = ();
|
| 30 |
rodolico |
170 |
if ( -e $filename && open my $fh, '<', $filename ) {
|
|
|
171 |
chomp( @lines = <$fh> );
|
|
|
172 |
close $fh;
|
|
|
173 |
logMsg("Read status file '$filename' with contents:\n" . join( "\n", @lines ) . "\n");
|
|
|
174 |
} else {
|
| 34 |
rodolico |
175 |
logMsg("Error: could not read status file '$filename', assuming a fresh start: $!");
|
| 30 |
rodolico |
176 |
}
|
|
|
177 |
return \@lines;
|
|
|
178 |
}
|
| 24 |
rodolico |
179 |
|
| 48 |
rodolico |
180 |
## Write the status list to disk safely.
|
|
|
181 |
##
|
|
|
182 |
## Arguments:
|
|
|
183 |
## $filename - path to the status file to write
|
|
|
184 |
## $statusList - ARRAYREF of lines to write into the file
|
|
|
185 |
##
|
|
|
186 |
## Behavior:
|
|
|
187 |
## - If an existing file is present, renames it to `$filename.bak` as a simple backup.
|
|
|
188 |
## - Writes the provided lines to `$filename` (one per line).
|
|
|
189 |
## - Logs the written contents. Dies on failure to backup or write the file.
|
| 30 |
rodolico |
190 |
sub writeStatusFile {
|
|
|
191 |
my ( $filename, $statusList ) = @_;
|
|
|
192 |
# backup existing status file
|
|
|
193 |
if ( -e $filename ) {
|
|
|
194 |
rename( $filename, "$filename.bak" ) or do {
|
|
|
195 |
logMsg("Error: could not backup existing status file '$filename': $!");
|
|
|
196 |
die;
|
|
|
197 |
};
|
|
|
198 |
}
|
|
|
199 |
# write new status file
|
|
|
200 |
if ( open my $fh, '>', $filename ) {
|
|
|
201 |
foreach my $line ( @$statusList ) {
|
|
|
202 |
print $fh "$line\n";
|
|
|
203 |
}
|
|
|
204 |
close $fh;
|
|
|
205 |
logMsg("Wrote status file '$filename' with contents:\n" . join( "\n", @$statusList ) . "\n");
|
|
|
206 |
} else {
|
|
|
207 |
logMsg("Error: could not write status file '$filename': $!");
|
|
|
208 |
die;
|
|
|
209 |
}
|
|
|
210 |
}
|
|
|
211 |
|
| 48 |
rodolico |
212 |
## Convert a path-like dataset name into a filename-safe string.
|
|
|
213 |
##
|
|
|
214 |
## Examples:
|
|
|
215 |
## 'pool/fs/sub' => 'pool.fs.sub' (default)
|
|
|
216 |
##
|
|
|
217 |
## Arguments:
|
|
|
218 |
## $string - input string to convert
|
|
|
219 |
## $delimiter - input delimiter to split on (default: '/')
|
|
|
220 |
## $substitution - output separator to join with (default: '.')
|
|
|
221 |
##
|
|
|
222 |
## Returns: a joined string suitable for use as a filename.
|
| 46 |
rodolico |
223 |
sub dirnameToFileName {
|
|
|
224 |
my ( $string, $delimiter, $substitution ) = @_;
|
|
|
225 |
$delimiter //= '/';
|
|
|
226 |
$substitution //= '.';
|
|
|
227 |
my @parts = split( /\Q$delimiter\E/, $string );
|
|
|
228 |
return join( $substitution, @parts );
|
| 31 |
rodolico |
229 |
}
|
|
|
230 |
|
| 48 |
rodolico |
231 |
## Perform replication for all configured datasets on the source server.
|
|
|
232 |
##
|
|
|
233 |
## Arguments:
|
|
|
234 |
## $config - configuration HASHREF (loaded from YAML). Must contain `datasets` and `transport` entries.
|
|
|
235 |
## $statusList - ARRAYREF of previously replicated snapshot full names (used to compute incremental sends)
|
|
|
236 |
##
|
|
|
237 |
## Behavior:
|
|
|
238 |
## - Iterates over datasets defined in `$config->{datasets}`.
|
|
|
239 |
## - For each dataset, enumerates available snapshots on the source and calls
|
|
|
240 |
## `makeReplicateCommands` to produce zfs send commands.
|
|
|
241 |
## - 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).
|
|
|
243 |
## - Respects `$config->{dryrun}`: no commands are executed when dryrun is enabled.
|
|
|
244 |
##
|
|
|
245 |
## Returns: ARRAYREF `$newStatus` containing updated status lines (latest snapshots per dataset).
|
| 30 |
rodolico |
246 |
sub doSourceReplication {
|
|
|
247 |
my ($config, $statusList) = @_;
|
|
|
248 |
my $newStatus = [];
|
|
|
249 |
foreach my $dataset ( sort keys %{$config->{datasets}} ) {
|
| 31 |
rodolico |
250 |
logMsg("Processing dataset '$dataset'");
|
|
|
251 |
# get list of all snapshots on dataset
|
| 44 |
rodolico |
252 |
my $sourceList;
|
|
|
253 |
if ( -e "$scriptDirectory/test.status") {
|
|
|
254 |
$sourceList = getStatusFile( "$scriptDirectory/test.status" );
|
|
|
255 |
} else {
|
| 46 |
rodolico |
256 |
$sourceList = [ runCmd( "zfs list -rt snap -H -o name $config->{datasets}->{$dataset}->{source}" ) ];
|
| 44 |
rodolico |
257 |
}
|
|
|
258 |
|
| 30 |
rodolico |
259 |
# process dataset here
|
| 44 |
rodolico |
260 |
my $commands = makeReplicateCommands(
|
|
|
261 |
$sourceList,
|
|
|
262 |
$statusList,
|
|
|
263 |
$dataset,
|
|
|
264 |
$config->{datasets}->{$dataset}->{source},
|
|
|
265 |
$config->{datasets}->{$dataset}->{target},
|
|
|
266 |
$newStatus
|
|
|
267 |
);
|
| 31 |
rodolico |
268 |
if ( %$commands ) {
|
|
|
269 |
foreach my $cmd ( keys %$commands ) {
|
|
|
270 |
my $command = $commands->{$cmd};
|
| 43 |
rodolico |
271 |
my $outputFile = $cmd;
|
| 48 |
rodolico |
272 |
my $outfile = dirnameToFileName( $cmd );
|
| 45 |
rodolico |
273 |
$command .= " | openssl enc -aes-256-cbc -K $config->{transport}->{encryption}->{key} -iv $config->{transport}->{encryption}->{IV} " if $config->{transport}->{encryption}->{key};
|
| 48 |
rodolico |
274 |
$command .= " > $config->{transport}->{mount_point}/$outfile";
|
| 31 |
rodolico |
275 |
logMsg("Running command: $command");
|
| 44 |
rodolico |
276 |
runCmd( $command ) unless $config->{dryrun};
|
| 31 |
rodolico |
277 |
}
|
|
|
278 |
} else {
|
|
|
279 |
logMsg( "Nothing to do for $dataset" );
|
| 30 |
rodolico |
280 |
}
|
|
|
281 |
}
|
|
|
282 |
return $newStatus;
|
|
|
283 |
}
|
|
|
284 |
|
| 48 |
rodolico |
285 |
## Perform cleanup and final reporting after replication.
|
|
|
286 |
##
|
|
|
287 |
## Arguments:
|
|
|
288 |
## $config - configuration HASHREF (required)
|
|
|
289 |
## $message - OPTIONAL message to include in the report
|
|
|
290 |
##
|
|
|
291 |
## Behavior:
|
|
|
292 |
## - Logs disk usage for the transport mount and zpool list for diagnostics.
|
|
|
293 |
## - Ensures the report subject and message are populated, then attempts to unmount
|
|
|
294 |
## the transport drive and send the report (via `sendReport`).
|
|
|
295 |
## - If `shutdown_after_replication` is set in the running role's config, attempts
|
|
|
296 |
## to shut down the machine (honors `$config->{dryrun}`).
|
| 42 |
rodolico |
297 |
sub cleanup{
|
|
|
298 |
my ( $config, $message ) = @_;
|
|
|
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" );
|
|
|
301 |
# add information about the server (zpools) to the log
|
|
|
302 |
my $servername = `hostname -s`;
|
|
|
303 |
chomp $servername;
|
|
|
304 |
logMsg( "Zpools on server $servername:\n" . join( "\n", runCmd( "zpool list" ) ) . "\n" );
|
|
|
305 |
$config->{$config->{runningAs}}->{report}->{subject} //= "Replication Report for $config->{runningAs} server $servername";
|
|
|
306 |
$message //= "Replication completed on $config->{runningAs} server $servername.";
|
|
|
307 |
# unmount the sneakernet drive
|
| 44 |
rodolico |
308 |
unmountDriveByLabel( $config->{transport} ) unless $config->{dryrun};
|
| 42 |
rodolico |
309 |
sendReport( $config->{$config->{runningAs}}->{report}, $message, $config->{log_file} );
|
|
|
310 |
# If they have requested shutdown, do it now
|
|
|
311 |
if ( $config->{$config->{runningAs}}->{shutdown_after_replication} ) {
|
|
|
312 |
logMsg( "Shutting down target server as per configuration" );
|
| 44 |
rodolico |
313 |
runCmd( "shutdown -p now" ) unless $config->{dryrun};
|
| 33 |
rodolico |
314 |
}
|
| 42 |
rodolico |
315 |
}
|
| 33 |
rodolico |
316 |
|
| 48 |
rodolico |
317 |
## Update the target zfs datasets from files on the transport drive.
|
|
|
318 |
##
|
|
|
319 |
## Arguments:
|
|
|
320 |
## $config - configuration HASHREF containing `transport` and `datasets` entries.
|
|
|
321 |
##
|
|
|
322 |
## Behavior:
|
|
|
323 |
## - Reads all regular files from the transport mount point (via `getDirectoryList`).
|
|
|
324 |
## - For each file, determines the intended target dataset based on the filename and
|
|
|
325 |
## the `datasets` mapping in the config, optionally decrypts via `openssl enc -d`,
|
|
|
326 |
## and pipes the stream into `zfs receive -F` to update the target dataset.
|
|
|
327 |
## - Uses `runCmd` to execute the receive commands and logs the executed command string.
|
| 42 |
rodolico |
328 |
sub updateTarget {
|
|
|
329 |
my $config = shift;
|
|
|
330 |
my $files = getDirectoryList( $config->{transport}->{mount_point});
|
|
|
331 |
foreach my $filename ( @$files ) {
|
| 46 |
rodolico |
332 |
my $targetDataset = basename( $filename );
|
|
|
333 |
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 |
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 .= " | zfs receive -F $targetDataset";
|
|
|
338 |
logMsg( $command );
|
|
|
339 |
runCmd( $command );
|
| 42 |
rodolico |
340 |
}
|
| 31 |
rodolico |
341 |
}
|
| 30 |
rodolico |
342 |
|
|
|
343 |
##################### main program starts here #####################
|
|
|
344 |
# Example to create a random key for encryption/decryption:
|
| 24 |
rodolico |
345 |
# generate a random key with
|
|
|
346 |
# openssl rand 32 | xxd -p | tr -d '\n' > test.key
|
|
|
347 |
|
|
|
348 |
# If a YAML config file exists next to the script, load and merge it
|
|
|
349 |
$config = loadConfig($configFileName, $config );
|
| 42 |
rodolico |
350 |
exit 1 unless keys %$config;
|
| 27 |
rodolico |
351 |
|
| 44 |
rodolico |
352 |
# parse CLI options
|
|
|
353 |
GetOptions( $config,
|
|
|
354 |
'dryrun|n',
|
|
|
355 |
'verbose|v+',
|
|
|
356 |
'version|V',
|
|
|
357 |
'help|h',
|
|
|
358 |
) or do { print "Invalid options\n"; exit 2 };
|
|
|
359 |
if (defined ($config->{help})) {
|
|
|
360 |
print "Usage: $FindBin::Script [--dryrun] [--verbose] [--help]\n";
|
|
|
361 |
print " --dryrun, -n Run in dry-run mode (no writes)\n";
|
|
|
362 |
print " --verbose, -v Run in verbose mode (more v's mean more verbose)\n";
|
|
|
363 |
print " --version, -V Display version number\n";
|
|
|
364 |
exit 0;
|
|
|
365 |
} elsif (defined $config->{version}) {
|
|
|
366 |
print "$FindBin::Script v$VERSION\n";
|
|
|
367 |
exit 0;
|
|
|
368 |
}
|
|
|
369 |
|
| 25 |
rodolico |
370 |
# set some defaults
|
| 35 |
rodolico |
371 |
$config->{'status_file'} //= "$scriptFullPath.status";
|
|
|
372 |
# 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
|
|
|
374 |
$logFileName = $config->{'log_file'} //= "$scriptFullPath.log";
|
|
|
375 |
# log only for one run
|
|
|
376 |
unlink ( $logFileName ) if -f $logFileName;
|
| 24 |
rodolico |
377 |
|
| 42 |
rodolico |
378 |
fatalError( "Invalid config file: missing source and/or target server", $config, \&cleanup )
|
| 37 |
rodolico |
379 |
unless (defined $config->{source} && defined $config->{target});
|
| 24 |
rodolico |
380 |
|
|
|
381 |
my $servername = `hostname -s`;
|
|
|
382 |
chomp $servername;
|
| 42 |
rodolico |
383 |
$config->{runningAs} = $servername eq $config->{source}->{hostname} ? 'source' :
|
| 37 |
rodolico |
384 |
$servername eq $config->{target}->{hostname} ? 'target' : 'unknown';
|
|
|
385 |
|
| 42 |
rodolico |
386 |
#cleanup( $config, "Testing" );
|
|
|
387 |
|
|
|
388 |
# mount the transport drive, fatal error if we can not find it
|
| 46 |
rodolico |
389 |
fatalError( "Unable to mount tranport drive with label $config->{transport}->{label}", $config, \&cleanup )
|
|
|
390 |
unless $config->{transport}->{mount_point} = mountDriveByLabel( $config->{transport} );
|
| 42 |
rodolico |
391 |
|
| 44 |
rodolico |
392 |
# main program logic
|
| 42 |
rodolico |
393 |
if ( $config->{runningAs} eq 'source' ) {
|
| 33 |
rodolico |
394 |
logMsg "Running as source server";
|
|
|
395 |
# remove all files from transport disk, but leave all subdirectories alone
|
| 42 |
rodolico |
396 |
fatalError( "Failed to clean transport directory $config->{transport}->{mount_point}", $config, \&cleanup )
|
| 44 |
rodolico |
397 |
unless $config->{dryrun} or cleanDirectory( $config->{transport}->{mount_point} );
|
| 30 |
rodolico |
398 |
my $statusList = getStatusFile($config->{status_file});
|
| 37 |
rodolico |
399 |
$statusList = doSourceReplication($config, $statusList);
|
| 44 |
rodolico |
400 |
writeStatusFile($config->{status_file}, $statusList) unless $config->{dryrun};
|
| 42 |
rodolico |
401 |
} elsif ( $config->{runningAs} eq 'target' ) {
|
| 33 |
rodolico |
402 |
logMsg "Running as target server";
|
| 37 |
rodolico |
403 |
mountGeli( $config->{target}->{geli} ) if ( defined $config->{target}->{geli} );
|
| 46 |
rodolico |
404 |
umountDiskByLabel( $config->{target}->{geli}->{secureKey} )
|
|
|
405 |
unless $config->{target}->{geli}->{secureKey}->{label} eq $config->{transport}->{label};
|
|
|
406 |
print "Please insert device labeled REPORT\n" if $config->{target}->{report}->{targetDrive}->{label};
|
| 42 |
rodolico |
407 |
updateTarget( $config );
|
| 24 |
rodolico |
408 |
} else {
|
| 37 |
rodolico |
409 |
fatalError( "This server ($servername) is neither source nor target server as per config\n" );
|
| 24 |
rodolico |
410 |
}
|
|
|
411 |
|
| 42 |
rodolico |
412 |
cleanup( $config );
|
| 35 |
rodolico |
413 |
|
| 25 |
rodolico |
414 |
1;
|