| 2 |
rodolico |
1 |
#! /usr/bin/env perl
|
|
|
2 |
|
| 20 |
rodolico |
3 |
# replicate
|
|
|
4 |
# Author: R. W. Rodolico
|
| 4 |
rodolico |
5 |
# very simple script to replicate a ZFS snapshot to another server.
|
|
|
6 |
# no fancy bells and whistles, does not create snapshots, and does
|
|
|
7 |
# not prune them. No major error checking either
|
| 20 |
rodolico |
8 |
#
|
|
|
9 |
# This is free software, and may be redistributed under the same terms
|
|
|
10 |
#
|
|
|
11 |
# Copyright (c) 2025, R. W. Rodolico
|
|
|
12 |
#
|
|
|
13 |
# Redistribution and use in source and binary forms, with or without
|
|
|
14 |
# modification, are permitted provided that the following conditions are met:
|
|
|
15 |
#
|
|
|
16 |
# Redistributions of source code must retain the above copyright notice, this
|
|
|
17 |
# list of conditions and the following disclaimer.
|
|
|
18 |
#
|
|
|
19 |
# Redistributions in binary form must reproduce the above copyright notice,
|
|
|
20 |
# this list of conditions and the following disclaimer in the documentation
|
|
|
21 |
# and/or other materials provided with the distribution.
|
|
|
22 |
#
|
|
|
23 |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
|
24 |
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
|
25 |
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
|
26 |
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
|
27 |
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
|
28 |
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
|
29 |
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
30 |
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
|
31 |
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
|
32 |
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
|
33 |
# POSSIBILITY OF SUCH DAMAGE.[13]
|
|
|
34 |
#
|
|
|
35 |
# version 1.0.0 20250614 RWR
|
|
|
36 |
# Added support for inconsistent child dataset snapshots
|
|
|
37 |
# If all child datasets do not have all the snapshots the parent has,
|
|
|
38 |
# then we break the job into multiple jobs, one for each dataset
|
| 4 |
rodolico |
39 |
|
| 20 |
rodolico |
40 |
|
| 2 |
rodolico |
41 |
use strict;
|
|
|
42 |
use warnings;
|
|
|
43 |
|
|
|
44 |
use Data::Dumper;
|
| 4 |
rodolico |
45 |
use Getopt::Long;
|
|
|
46 |
Getopt::Long::Configure ("bundling");
|
| 2 |
rodolico |
47 |
|
| 20 |
rodolico |
48 |
# define the version number
|
|
|
49 |
# see https://metacpan.org/pod/release/JPEACOCK/version-0.97/lib/version.pod
|
|
|
50 |
use version 0.77; our $VERSION = version->declare("v1.0.0");
|
|
|
51 |
|
|
|
52 |
|
| 4 |
rodolico |
53 |
# create our configuration, with some defaults
|
|
|
54 |
# these are overridden by command line stuff
|
| 2 |
rodolico |
55 |
my $config = {
|
| 4 |
rodolico |
56 |
# the source, where we're coming from
|
|
|
57 |
'source' => '',
|
|
|
58 |
# the target, where we want to replicate to
|
|
|
59 |
'target' => '',
|
| 2 |
rodolico |
60 |
# compile the regex
|
| 6 |
rodolico |
61 |
'filter' => '(\d{4}.\d{2}.\d{2}.\d{2}.\d{2})',
|
| 4 |
rodolico |
62 |
# if non-zero, just display the commands we'd use, don't run them
|
|
|
63 |
'dryrun' => 0,
|
|
|
64 |
# whether to do all child datasets also (default)
|
| 6 |
rodolico |
65 |
'recurse' => 0,
|
| 4 |
rodolico |
66 |
# show more information
|
|
|
67 |
'verbose' => 0
|
| 2 |
rodolico |
68 |
};
|
|
|
69 |
|
| 20 |
rodolico |
70 |
# Parses a dataset string, which may include a server (server:dataset),
|
|
|
71 |
# and returns a hashref with 'server' and 'dataset' keys.
|
| 2 |
rodolico |
72 |
sub parseDataSet {
|
|
|
73 |
my $data = shift;
|
|
|
74 |
my %return;
|
|
|
75 |
my ( $server, $dataset ) = split( ':', $data );
|
|
|
76 |
if ( $dataset ) { # they passed a server:dataset
|
|
|
77 |
$return{'server'} = $server;
|
|
|
78 |
$return{'dataset'} = $dataset;
|
|
|
79 |
} else { # only passing in dataset, so assume localhost
|
|
|
80 |
$return{'server'} = '';
|
|
|
81 |
$return{'dataset'} = $server;
|
|
|
82 |
}
|
|
|
83 |
return \%return;
|
|
|
84 |
}
|
|
|
85 |
|
| 20 |
rodolico |
86 |
# Appends log messages to /tmp/replicate.log.
|
| 9 |
rodolico |
87 |
sub logit {
|
|
|
88 |
open LOG, ">>/tmp/replicate.log" or die "Could not open replicate.log: $!\n";
|
|
|
89 |
print LOG join( "\n", @_ ) . "\n";
|
|
|
90 |
close LOG;
|
|
|
91 |
}
|
|
|
92 |
|
| 20 |
rodolico |
93 |
# Runs a shell command, capturing stdout and stderr.
|
|
|
94 |
# Returns (0, output) on success, or (error_code, error_message) on failure.
|
| 2 |
rodolico |
95 |
sub run {
|
|
|
96 |
my $command = shift;
|
| 9 |
rodolico |
97 |
#&logit( $command );
|
| 2 |
rodolico |
98 |
my $output = qx/$command 2>&1/;
|
|
|
99 |
if ($? == -1) {
|
|
|
100 |
return (-1,"failed to execute: $!");
|
|
|
101 |
} elsif ($? & 127) {
|
|
|
102 |
return ($?, sprintf "child died with signal %d, %s coredump",
|
|
|
103 |
($? & 127), ($? & 128) ? 'with' : 'without' );
|
|
|
104 |
} else {
|
|
|
105 |
return ($? >> 8, sprintf "child exited with value %d", $? >> 8 ) if $? >> 8;
|
|
|
106 |
}
|
|
|
107 |
return (0,$output);
|
|
|
108 |
}
|
|
|
109 |
|
| 20 |
rodolico |
110 |
# Retrieves all ZFS snapshots for a given dataset (and server, if remote).
|
|
|
111 |
# Filters snapshots by a regex pattern, and returns a hashref of snapshots
|
|
|
112 |
# with metadata (key, refer, used).
|
| 2 |
rodolico |
113 |
sub getSnaps {
|
|
|
114 |
my ($config,$pattern) = @_;
|
|
|
115 |
my %return;
|
|
|
116 |
# actual command to run to get all snapshots, recursively, of the dataset
|
|
|
117 |
my $command = 'zfs list -r -t snap ' . $config->{'dataset'};
|
|
|
118 |
$command = "ssh $config->{server} '$command'" if $config->{'server'};
|
|
|
119 |
my ($error, $output ) = &run( $command );
|
| 21 |
rodolico |
120 |
die "Error running $command with output [$output]\nMisconfigured Dataset?\n" if $error;
|
| 2 |
rodolico |
121 |
my @snaps = split( "\n", $output );
|
|
|
122 |
chomp @snaps;
|
|
|
123 |
for (my $i = 0; $i < @snaps; $i++ ) {
|
|
|
124 |
# parse out the space delmited fields
|
|
|
125 |
my ($fullname, $used, $avail, $refer, $mount) = split( /\s+/, $snaps[$i] );
|
|
|
126 |
# break the name into dataset and snapname
|
|
|
127 |
my ($dataset, $snap) = split( '@', $fullname );
|
|
|
128 |
# remove the root dataset name
|
|
|
129 |
$dataset =~ s/^$config->{'dataset'}//;
|
|
|
130 |
# skip anything not matching our regex
|
|
|
131 |
next unless $pattern && $snap && $snap =~ m/$pattern/;
|
|
|
132 |
# grab the matched key
|
|
|
133 |
$return{$dataset}{'snaps'}{$snap}{'key'} = $1;
|
|
|
134 |
# and remove all non-numerics
|
|
|
135 |
$return{$dataset}{'snaps'}{$snap}{'key'} =~ s/[^0-9]//g;
|
|
|
136 |
# get the transfer size
|
|
|
137 |
$return{$dataset}{'snaps'}{$snap}{'refer'} = $refer;
|
|
|
138 |
# get the actual disk space used
|
|
|
139 |
$return{$dataset}{'snaps'}{$snap}{'used'} = $used;
|
|
|
140 |
}
|
|
|
141 |
return \%return;
|
|
|
142 |
}
|
|
|
143 |
|
| 20 |
rodolico |
144 |
# Calculates the number of bytes that would be transferred for the next sync.
|
|
|
145 |
# Returns the size in bytes, or 0 if datasets are up to date.
|
| 6 |
rodolico |
146 |
sub findSize {
|
|
|
147 |
my $config = shift;
|
|
|
148 |
# check for new snapshots to sync. If they are equal, we are up to date
|
|
|
149 |
if ( $config->{'source'}->{'lastSnap'} ne $config->{'target'}->{'lastSnap'} ) {
|
|
|
150 |
# Build the source command
|
|
|
151 |
my $sourceCommand = sprintf( '%s@%s %s@%s',
|
|
|
152 |
$config->{'source'}->{'dataset'},
|
|
|
153 |
$config->{'target'}->{'lastSnap'},
|
|
|
154 |
$config->{'source'}->{'dataset'},
|
|
|
155 |
$config->{'source'}->{'lastSnap'}
|
|
|
156 |
);
|
|
|
157 |
# prepend 'zfs send' and the flags. Note that verbose is only for the one which is local
|
|
|
158 |
$sourceCommand = 'zfs send -' .
|
|
|
159 |
( $config->{'recurse'} ? 'R' : '' ) . # recurse if they asked for it
|
| 15 |
rodolico |
160 |
# Tell it to give us the size in bytes
|
| 8 |
rodolico |
161 |
'Pn' .
|
| 6 |
rodolico |
162 |
# this is the part that asks for incremental
|
|
|
163 |
'I ' .
|
|
|
164 |
$sourceCommand;
|
|
|
165 |
# wrap the ssh call if this is remote
|
|
|
166 |
$sourceCommand = "ssh $config->{source}->{server} '$sourceCommand'" if $config->{'source'}->{'server'};
|
| 15 |
rodolico |
167 |
print "Checking Size with\n$sourceCommand\n" if $config->{'verbose'} > 3;
|
| 6 |
rodolico |
168 |
my ( $error, $output ) = &run( $sourceCommand );
|
|
|
169 |
return -1 if $error;
|
|
|
170 |
# the size is the second column (tab separated) of the last line (\n separated) in $output
|
|
|
171 |
return (
|
|
|
172 |
split(
|
|
|
173 |
"\t",
|
|
|
174 |
(
|
|
|
175 |
split( "\n", $output )
|
|
|
176 |
)[-1]
|
|
|
177 |
)
|
|
|
178 |
)[1];
|
|
|
179 |
} else { # nothing to sync
|
|
|
180 |
return 0;
|
|
|
181 |
}
|
|
|
182 |
}
|
| 2 |
rodolico |
183 |
|
| 20 |
rodolico |
184 |
# Builds the shell command(s) needed to replicate the ZFS snapshot(s)
|
|
|
185 |
# from source to target, using zfs send/receive and optionally pv.
|
| 2 |
rodolico |
186 |
sub createCommands {
|
| 6 |
rodolico |
187 |
my $config = shift;
|
|
|
188 |
# check for new snapshots to sync. If they are equal, we are up to date
|
|
|
189 |
if ( $config->{'source'}->{'lastSnap'} ne $config->{'target'}->{'lastSnap'} ) {
|
|
|
190 |
# Build the source command
|
|
|
191 |
my $sourceCommand = sprintf( '%s@%s %s@%s',
|
|
|
192 |
$config->{'source'}->{'dataset'},
|
|
|
193 |
$config->{'target'}->{'lastSnap'},
|
|
|
194 |
$config->{'source'}->{'dataset'},
|
|
|
195 |
$config->{'source'}->{'lastSnap'}
|
|
|
196 |
);
|
|
|
197 |
# prepend 'zfs send' and the flags. Note that verbose is only for the one which is local
|
|
|
198 |
$sourceCommand = 'zfs send -' .
|
|
|
199 |
( $config->{'recurse'} ? 'R' : '' ) . # recurse if they asked for it
|
|
|
200 |
# turn on verbose if they asked for level 2 AND if source is local
|
| 15 |
rodolico |
201 |
( $config->{'verbose'} > 2 && ! $config->{'source'}->{'server'} ? 'v' : '' ) .
|
| 6 |
rodolico |
202 |
# this is the part that asks for incremental
|
|
|
203 |
'I ' .
|
|
|
204 |
$sourceCommand;
|
|
|
205 |
# wrap the ssh call if this is remote
|
|
|
206 |
$sourceCommand = "ssh $config->{source}->{server} '$sourceCommand'" if $config->{'source'}->{'server'};
|
|
|
207 |
# Now, build the target command
|
|
|
208 |
my $targetCommand = 'zfs receive ' .
|
| 15 |
rodolico |
209 |
( ! $config->{'target'}->{'server'} && $config->{'verbose'} > 2 ? '-v ' : '') .
|
| 6 |
rodolico |
210 |
$config->{'target'}->{'dataset'};
|
|
|
211 |
$targetCommand = "ssh $config->{target}->{server} '$targetCommand'" if $config->{'target'}->{'server'};
|
| 7 |
rodolico |
212 |
# if the command pv is installed
|
|
|
213 |
if ( `which pv` ) {
|
|
|
214 |
my $tags;
|
|
|
215 |
# add bandwdith limits, if requested
|
|
|
216 |
$tags = " --si -L $config->{bwlimit} " if $config->{'bwlimit'};
|
|
|
217 |
# if interactive, or if we are in dry run, add thermometer
|
|
|
218 |
$tags .= '-petrs ' . $config->{'report'}->{'Bytes Transferred'} if -t *STDOUT || $config->{'dryrun'};
|
|
|
219 |
$sourceCommand .= " | pv $tags" if $tags;
|
|
|
220 |
}
|
| 6 |
rodolico |
221 |
# return the command
|
|
|
222 |
return $sourceCommand . ' | ' . $targetCommand;
|
|
|
223 |
} else { # source and target are in sync, so do nothing
|
|
|
224 |
return '# Nothing new to sync';
|
| 2 |
rodolico |
225 |
}
|
|
|
226 |
}
|
|
|
227 |
|
| 20 |
rodolico |
228 |
# Finds the most recent snapshot in a hash of snapshots.
|
|
|
229 |
# Returns the snapshot name with the largest 'key' value.
|
| 2 |
rodolico |
230 |
sub getLastSnapshot {
|
|
|
231 |
my $snapList = shift;
|
|
|
232 |
my $lastKey = 0;
|
|
|
233 |
my $lastSnap = '';
|
|
|
234 |
foreach my $snap ( keys %$snapList ) {
|
|
|
235 |
if ( $snapList->{$snap}->{'key'} > $lastKey ) {
|
|
|
236 |
$lastKey = $snapList->{$snap}->{'key'};
|
|
|
237 |
$lastSnap = $snap;
|
|
|
238 |
}
|
|
|
239 |
}
|
|
|
240 |
return $lastSnap;
|
|
|
241 |
}
|
|
|
242 |
|
| 20 |
rodolico |
243 |
# Checks if all child datasets have all the snapshots the parent has.
|
|
|
244 |
# If not, returns a list of datasets to replicate individually.
|
|
|
245 |
sub check_child_snap_consistency {
|
|
|
246 |
my ($config, $side) = @_;
|
| 21 |
rodolico |
247 |
#print Dumper( $config ) . "\n"; die;
|
|
|
248 |
my $snaps = $config->{$side}->{'snapshots'};
|
|
|
249 |
#print Dumper( $snaps ) . "\n"; die;
|
| 20 |
rodolico |
250 |
my @datasets = keys %$snaps;
|
| 21 |
rodolico |
251 |
#die Dumper( \@datasets ) . "\n";
|
| 20 |
rodolico |
252 |
return @datasets if @datasets == 1; # Only parent, nothing to check
|
| 2 |
rodolico |
253 |
|
| 20 |
rodolico |
254 |
my $parent = (sort @datasets)[0]; # Assume parent is first (no / in name or shortest)
|
| 21 |
rodolico |
255 |
#die Dumper( \@datasets ) . "\n";
|
| 20 |
rodolico |
256 |
my %parent_snaps = %{ $snaps->{$parent}{'snaps'} };
|
|
|
257 |
|
|
|
258 |
my @inconsistent;
|
|
|
259 |
foreach my $child (@datasets) {
|
|
|
260 |
next if $child eq $parent;
|
|
|
261 |
foreach my $snap (keys %parent_snaps) {
|
|
|
262 |
unless (exists $snaps->{$child}{'snaps'}{$snap}) {
|
|
|
263 |
push @inconsistent, $child;
|
|
|
264 |
last;
|
|
|
265 |
}
|
|
|
266 |
}
|
|
|
267 |
}
|
|
|
268 |
if (@inconsistent) {
|
|
|
269 |
# Return all datasets as separate jobs
|
|
|
270 |
return @datasets;
|
|
|
271 |
} else {
|
|
|
272 |
# All children have all parent snaps, treat as one job
|
|
|
273 |
return ($parent);
|
|
|
274 |
}
|
|
|
275 |
}
|
|
|
276 |
|
|
|
277 |
# Calculates the last snapshot for source and target, and checks for consistency.
|
|
|
278 |
# Returns (source_last_snap, target_last_snap, warnings_arrayref).
|
| 2 |
rodolico |
279 |
sub calculate {
|
|
|
280 |
my $config = shift;
|
|
|
281 |
|
|
|
282 |
my @warnings;
|
|
|
283 |
|
|
|
284 |
# find the last snapshot date in each dataset, on each target
|
|
|
285 |
foreach my $machine ( 'source', 'target' ) {
|
|
|
286 |
$config->{$machine}->{'last'} = 0; # track the last entry in all children in dataset
|
|
|
287 |
$config->{$machine}->{'allOk'} = 1; # assumed to be true, becomes false if some children do not have snapshots
|
|
|
288 |
foreach my $child ( keys %{ $config->{$machine}->{'snapshots'} } ) {
|
|
|
289 |
$config->{$machine}->{'snapshots'}->{$child}->{'last'} =
|
|
|
290 |
&getLastSnapshot( $config->{$machine}->{'snapshots'}->{$child}->{'snaps'} );
|
|
|
291 |
# set the machine last if we haven't done so yet
|
|
|
292 |
$config->{$machine}->{'last'} = $config->{$machine}->{'snapshots'}->{$child}->{'last'} unless $config->{$machine}->{'last'};
|
|
|
293 |
# keep track of the last snapshot for each set
|
|
|
294 |
if ( $config->{$machine}->{'last'} ne $config->{$machine}->{'snapshots'}->{$child}->{'last'} ) {
|
|
|
295 |
$config->{$machine}->{'allOk'} = 0;
|
|
|
296 |
push @warnings, "Warning: $machine does not have consistent snapshots at $child";;
|
|
|
297 |
}
|
|
|
298 |
}
|
|
|
299 |
}
|
|
|
300 |
# make sure the source has a corresponding snap for target->last
|
|
|
301 |
foreach my $child ( keys %{ $config->{'target'}->{'snapshots'} } ) {
|
|
|
302 |
if (! exists ($config->{'source'}->{'snapshots'}->{$child}->{'snaps'}->{$config->{'target'}->{'snapshots'}->{$child}->{'last'}} ) ) {
|
|
|
303 |
$config->{'source'}->{'allOk'} = 0;
|
|
|
304 |
push @warnings, "Warning: We do not have consistent snapshots";
|
|
|
305 |
}
|
|
|
306 |
}
|
|
|
307 |
my $return;
|
|
|
308 |
if ( $config->{'source'}->{'allOk'} and $config->{'target'}->{'allOk'} ) { # whew, they match
|
|
|
309 |
return( $config->{'source'}->{'last'}, $config->{'target'}->{'last'}, \@warnings );
|
|
|
310 |
} else {
|
|
|
311 |
return( '','',\@warnings);
|
|
|
312 |
}
|
| 4 |
rodolico |
313 |
} # sub calculate
|
| 2 |
rodolico |
314 |
|
| 20 |
rodolico |
315 |
# Prints usage/help message and exits.
|
| 6 |
rodolico |
316 |
sub help {
|
|
|
317 |
use File::Basename;
|
|
|
318 |
my $me = fileparse( $0 );
|
|
|
319 |
my $helpMessage = <<" EOF";
|
|
|
320 |
$me [flags] [source [target]]
|
| 20 |
rodolico |
321 |
Version $VERSION
|
| 6 |
rodolico |
322 |
Syncs source dataset to target dataset
|
|
|
323 |
|
|
|
324 |
Parameters (optional)
|
|
|
325 |
source - dataset syncing from
|
|
|
326 |
target - dataset syncing to
|
|
|
327 |
|
|
|
328 |
Flags
|
|
|
329 |
--source|s - Alternate way to pass source dataset
|
|
|
330 |
--target|t - Alternate way to pass target dataset
|
|
|
331 |
--filter|f - Filter (regex) to limit source snapshots to process
|
|
|
332 |
--dryrun|n - Only displays command(s) to be run
|
|
|
333 |
--recurse|r - Process dataset and all child datasets
|
|
|
334 |
--verbose|v - increase verbosity of output
|
| 7 |
rodolico |
335 |
--bwlimit - Limit the speed of the connect to # bytes/s. KMGT allowed
|
| 20 |
rodolico |
336 |
--version|V - display the version number and exit
|
| 6 |
rodolico |
337 |
|
|
|
338 |
May use short flags with bundling, ie -nrvv is valid for
|
|
|
339 |
--dryrun --recurse --verbose --verbose
|
|
|
340 |
|
|
|
341 |
Either source or target must contain a DNS name or IP address of a remote
|
|
|
342 |
machine, separated from the dataset with a colon, ie
|
|
|
343 |
--source fbsd:storage/mydata
|
|
|
344 |
would use the dataset storage/mydata on the server fbsd. The other dataset
|
|
|
345 |
is assumed to be the local machine
|
|
|
346 |
|
|
|
347 |
filter is a string which is a valid regular expression. Only snapshots matching
|
|
|
348 |
that string will be used from the source dataset
|
|
|
349 |
|
|
|
350 |
By default, only error messages are displayed. verbose will display statistics
|
| 15 |
rodolico |
351 |
on size and transfer time. Twice will give the commands, and three times will
|
|
|
352 |
display entire output of send/receive (whichever is the local machine)
|
| 7 |
rodolico |
353 |
|
|
|
354 |
Example:
|
|
|
355 |
$me -r prod.example.org:pool/mydata -t pool/backup/mydata \
|
|
|
356 |
--bwlimit=5M --filter='(\\d{4}.\\d{2}.\\d{2}.\\d{2}.\\d{2})'
|
|
|
357 |
|
|
|
358 |
Would sync pool/mydata and all child datasets on prod.example.org to
|
|
|
359 |
pool/backup/mydata on the local server. Only the snapshots which had a
|
|
|
360 |
datetime stamp matching the --filter rule would be used. The transfer
|
|
|
361 |
would not exceed 5MB/s (40Mb/s) if the pv app was installed
|
| 6 |
rodolico |
362 |
EOF
|
|
|
363 |
# get rid of indentation
|
|
|
364 |
$helpMessage =~ s/^ //;
|
|
|
365 |
$helpMessage =~ s/\n /\n/g;
|
|
|
366 |
print $helpMessage;
|
|
|
367 |
exit 1;
|
|
|
368 |
} # help
|
|
|
369 |
|
|
|
370 |
|
| 4 |
rodolico |
371 |
GetOptions( $config,
|
|
|
372 |
'source|s=s',
|
|
|
373 |
'target|t=s',
|
|
|
374 |
'filter|f=s',
|
|
|
375 |
'dryrun|n',
|
|
|
376 |
'recurse|r',
|
| 8 |
rodolico |
377 |
'bwlimit=s',
|
| 6 |
rodolico |
378 |
'verbose|v+',
|
| 20 |
rodolico |
379 |
'version|V',
|
| 4 |
rodolico |
380 |
'help|h'
|
|
|
381 |
);
|
| 2 |
rodolico |
382 |
|
| 6 |
rodolico |
383 |
&help() if $config->{'help'};
|
| 20 |
rodolico |
384 |
if ($config->{'version'}) {
|
|
|
385 |
print "replicate version $VERSION\n" ;
|
|
|
386 |
exit 0;
|
|
|
387 |
}
|
| 4 |
rodolico |
388 |
# allow them to use positional, without flags, such as
|
|
|
389 |
# replicate source target --filter='regex' -n
|
|
|
390 |
$config->{'source'} = shift unless $config->{'source'};
|
|
|
391 |
$config->{'target'} = shift unless $config->{'target'};
|
|
|
392 |
die "You must enter a source and a target, at a minimum\n" unless $config->{'source'} && $config->{'target'};
|
|
|
393 |
|
| 6 |
rodolico |
394 |
# keep track of when we started this run
|
|
|
395 |
$config->{'report'}->{'Start Time'} = time;
|
|
|
396 |
|
| 4 |
rodolico |
397 |
# WARNING: this converts source and targets from a string to a hash
|
|
|
398 |
# '10.0.0.1:data/set' becomes ( 'server' => '10.0.0.1', 'dataset' => 'data/set')
|
|
|
399 |
# and 'data/set' becomes ( 'server' => '', 'dataset' => 'data/set')
|
|
|
400 |
$config->{'source'} = &parseDataSet( $config->{'source'} );
|
|
|
401 |
$config->{'target'} = &parseDataSet( $config->{'target'} );
|
|
|
402 |
|
| 2 |
rodolico |
403 |
# both source and target can not have a server portion; one must be local
|
|
|
404 |
die "Source and Target can not both be remote\n" if $config->{'source'}->{'server'} && $config->{'target'}->{'server'};
|
|
|
405 |
|
| 6 |
rodolico |
406 |
# connect to servers and get all existing snapshots
|
|
|
407 |
$config->{'target'}->{'snapshots'} = &getSnaps( $config->{'target'}, $config->{'filter'} );
|
| 4 |
rodolico |
408 |
$config->{'source'}->{'snapshots'} = &getSnaps( $config->{'source'}, $config->{'filter'} );
|
| 2 |
rodolico |
409 |
|
| 20 |
rodolico |
410 |
# Check for child dataset snapshot consistency on source and target
|
|
|
411 |
my @source_jobs = check_child_snap_consistency($config, 'source');
|
|
|
412 |
my @target_jobs = check_child_snap_consistency($config, 'target');
|
| 2 |
rodolico |
413 |
|
| 20 |
rodolico |
414 |
# If either side has inconsistencies, break into per-dataset jobs
|
|
|
415 |
my %all_jobs;
|
|
|
416 |
$all_jobs{$_}++ for (@source_jobs, @target_jobs);
|
| 2 |
rodolico |
417 |
|
| 20 |
rodolico |
418 |
foreach my $dataset (sort keys %all_jobs) {
|
|
|
419 |
# Prepare a config for this dataset
|
|
|
420 |
my %job_config = %$config;
|
|
|
421 |
$job_config{'source'} = { %{$config->{'source'}}, 'dataset' => $config->{'source'}{'dataset'} . $dataset, 'snapshots' => { $dataset => $config->{'source'}{'snapshots'}{$dataset} } };
|
|
|
422 |
$job_config{'target'} = { %{$config->{'target'}}, 'dataset' => $config->{'target'}{'dataset'} . $dataset, 'snapshots' => { $dataset => $config->{'target'}{'snapshots'}{$dataset} } };
|
|
|
423 |
|
|
|
424 |
( $job_config{'source'}{'lastSnap'}, $job_config{'target'}{'lastSnap'} ) = &calculate( \%job_config );
|
|
|
425 |
$job_config{'report'}{'Bytes Transferred'} = &findSize( \%job_config ) if $config->{'verbose'};
|
|
|
426 |
my $commands = &createCommands( \%job_config );
|
|
|
427 |
print "$commands\n" if $config->{'verbose'} > 1 or $config->{'dryrun'};
|
|
|
428 |
if ( $config->{'dryrun'} ) {
|
|
|
429 |
print "Dry Run for $dataset\n";
|
|
|
430 |
} else {
|
|
|
431 |
print qx/$commands/ if $commands =~ m/^[a-zA-Z]/;
|
|
|
432 |
}
|
| 6 |
rodolico |
433 |
}
|
|
|
434 |
|
|
|
435 |
$config->{'report'}->{'End Time'} = time;
|
|
|
436 |
$config->{'report'}->{'Elapsed Time'} = $config->{'report'}->{'End Time'} - $config->{'report'}->{'Start Time'};
|
| 15 |
rodolico |
437 |
if ( $config->{'verbose'} ) {
|
| 4 |
rodolico |
438 |
if ( $config->{'dryrun'} ) {
|
| 6 |
rodolico |
439 |
print "Would have transferred $config->{'report'}->{'Bytes Transferred'} bytes\n";
|
| 18 |
rodolico |
440 |
} elsif ( $config->{'report'}->{'Bytes Transferred'} ) {
|
|
|
441 |
print "bytes\t$config->{'report'}->{'Bytes Transferred'}\nseconds\t$config->{'report'}->{'Elapsed Time'}\n";
|
| 2 |
rodolico |
442 |
} else {
|
| 18 |
rodolico |
443 |
print "Nothing to do, datasets up to date\n";
|
| 2 |
rodolico |
444 |
}
|
|
|
445 |
}
|
|
|
446 |
1;
|