| Line 17... |
Line 17... |
| 17 |
# the source, where we're coming from
|
17 |
# the source, where we're coming from
|
| 18 |
'source' => '',
|
18 |
'source' => '',
|
| 19 |
# the target, where we want to replicate to
|
19 |
# the target, where we want to replicate to
|
| 20 |
'target' => '',
|
20 |
'target' => '',
|
| 21 |
# compile the regex
|
21 |
# compile the regex
|
| 22 |
'filter' => qr/(\d{4}.\d{2}.\d{2}.\d{2}.\d{2})/,
|
22 |
'filter' => '(\d{4}.\d{2}.\d{2}.\d{2}.\d{2})',
|
| 23 |
# if non-zero, just display the commands we'd use, don't run them
|
23 |
# if non-zero, just display the commands we'd use, don't run them
|
| 24 |
'dryrun' => 0,
|
24 |
'dryrun' => 0,
|
| 25 |
# whether to do all child datasets also (default)
|
25 |
# whether to do all child datasets also (default)
|
| 26 |
'recurse' => 1,
|
26 |
'recurse' => 0,
|
| 27 |
# show more information
|
27 |
# show more information
|
| 28 |
'verbose' => 0
|
28 |
'verbose' => 0
|
| 29 |
};
|
29 |
};
|
| 30 |
|
30 |
|
| 31 |
sub parseDataSet {
|
31 |
sub parseDataSet {
|
| Line 90... |
Line 90... |
| 90 |
$return{$dataset}{'snaps'}{$snap}{'used'} = $used;
|
90 |
$return{$dataset}{'snaps'}{$snap}{'used'} = $used;
|
| 91 |
}
|
91 |
}
|
| 92 |
return \%return;
|
92 |
return \%return;
|
| 93 |
}
|
93 |
}
|
| 94 |
|
94 |
|
| 95 |
#sub diffSnaps {
|
- |
|
| 96 |
# my ( $config->{'source'}, $config->{'target'} ) = @_;
|
- |
|
| 97 |
# my @source = sort keys %$config->{'source'};
|
95 |
# get tne number of bytes we will be syncing.
|
| 98 |
# my @target = sort keys %$config->{'target'};
|
- |
|
| 99 |
# print "===Source\n" . join( "\n", @source ) . "\n===Target\n" . join( "\n", @target ) . "\n";
|
- |
|
| 100 |
|
- |
|
| 101 |
# my $s = 0;
|
96 |
sub findSize {
|
| 102 |
# my $t = 0;
|
97 |
my $config = shift;
|
| 103 |
# my %return;
|
- |
|
| 104 |
# $return{'deleteTarget'} = [];
|
98 |
# check for new snapshots to sync. If they are equal, we are up to date
|
| 105 |
# $return{'addTarget'} = [];
|
99 |
if ( $config->{'source'}->{'lastSnap'} ne $config->{'target'}->{'lastSnap'} ) {
|
| 106 |
# $return{'lastMatch'} = 0;
|
100 |
# Build the source command
|
| 107 |
# $return{'finalSync'} = 0;
|
101 |
my $sourceCommand = sprintf( '%s@%s %s@%s',
|
| 108 |
# while ( $s < @source && $t < @target ) {
|
102 |
$config->{'source'}->{'dataset'},
|
| 109 |
# if ( $config->{'source'}[$s] eq $config->{'target'}[$t] ) { # matchies, just keep going
|
103 |
$config->{'target'}->{'lastSnap'},
|
| 110 |
# print "Source $s [$config->{'source'}[$s]] matches target $t [$config->{'target'}[$t]]\n";
|
104 |
$config->{'source'}->{'dataset'},
|
| 111 |
# $return{'lastMatch'} = $config->{'source'}[$s]; # keep track of the largest match
|
105 |
$config->{'source'}->{'lastSnap'}
|
| 112 |
# $s++; $t++;
|
106 |
);
|
| 113 |
# } elsif ( $config->{'target'}[$t] ne $config->{'source'}[$s] ) { # we are processing stuff that needs to be deleted on target
|
107 |
# prepend 'zfs send' and the flags. Note that verbose is only for the one which is local
|
| - |
|
108 |
$sourceCommand = 'zfs send -' .
|
| 114 |
# push @{$return{'deleteTarget'}}, $config->{'target'}[$t];
|
109 |
( $config->{'recurse'} ? 'R' : '' ) . # recurse if they asked for it
|
| 115 |
# print "Adding delete target $t [$config->{'target'}[$t]]\n";
|
110 |
# turn on verbose if they asked for level 2 AND if source is local
|
| 116 |
# $t++;
|
111 |
'Pwn' .
|
| 117 |
# }
|
- |
|
| 118 |
# }
|
- |
|
| 119 |
# die "Could not reconcile snapshots, ran out of source too soon\n" if $s > @source;
|
112 |
# this is the part that asks for incremental
|
| 120 |
# # put a value into finalSync to make sure there is one. If we do not have any sync
|
113 |
'I ' .
|
| 121 |
# # to do, final and lastMatch will be the same
|
114 |
$sourceCommand;
|
| 122 |
# $return{'finalSync'} = $return{'lastMatch'};
|
115 |
# wrap the ssh call if this is remote
|
| 123 |
# while ( $s < @source ) {
|
116 |
$sourceCommand = "ssh $config->{source}->{server} '$sourceCommand'" if $config->{'source'}->{'server'};
|
| 124 |
# push @{$return{'addTarget'}}, $config->{'source'}[$s];
|
117 |
print "Checking Size with\n$sourceCommand\n" if $config->{'verbose'} > 2;
|
| 125 |
# $return{'finalSync'} = $config->{'source'}[$s];
|
118 |
my ( $error, $output ) = &run( $sourceCommand );
|
| 126 |
# $s++;
|
119 |
return -1 if $error;
|
| 127 |
# }
|
- |
|
| - |
|
120 |
# the size is the second column (tab separated) of the last line (\n separated) in $output
|
| 128 |
# die Dumper( \%return );
|
121 |
return (
|
| 129 |
# return \%return;
|
122 |
split(
|
| 130 |
#}
|
- |
|
| 131 |
|
- |
|
| 132 |
#sub arrayEquals {
|
123 |
"\t",
|
| 133 |
# my ($a, $b ) = @_;
|
124 |
(
|
| 134 |
# return 0 unless @{$a} == @{$b}; # they are different sizes
|
125 |
split( "\n", $output )
|
| 135 |
# for ( my $i = 0; $i < @$a; $i++ ) {
|
126 |
)[-1]
|
| 136 |
# if ( $$a[$i] ne $$b[$i] ) {
|
127 |
)
|
| 137 |
# print STDERR "No Match!\n" . join( "\t", @$a ) . "\n" . join( "\t", @$b ) . "\n";
|
- |
|
| 138 |
# return 0;
|
128 |
)[1];
|
| - |
|
129 |
} else { # nothing to sync
|
| 139 |
# }
|
130 |
return 0;
|
| 140 |
# }
|
131 |
}
|
| 141 |
# return 1;
|
- |
|
| 142 |
#}
|
132 |
}
|
| 143 |
|
133 |
|
| - |
|
134 |
# create the command necessary to do the replication
|
| 144 |
sub createCommands {
|
135 |
sub createCommands {
|
| 145 |
my ( $lastSource, $lastTarget, $config ) = @_;
|
- |
|
| 146 |
my @return;
|
136 |
my $config = shift;
|
| 147 |
# check for new snapshots to sync
|
137 |
# check for new snapshots to sync. If they are equal, we are up to date
|
| - |
|
138 |
if ( $config->{'source'}->{'lastSnap'} ne $config->{'target'}->{'lastSnap'} ) {
|
| 148 |
if ( $lastSource ne $lastTarget ) {
|
139 |
# Build the source command
|
| 149 |
my $snapRange = $config->{'target'}->{'dataset'} . '@' . $config->{'target'};
|
140 |
my $sourceCommand = sprintf( '%s@%s %s@%s',
|
| 150 |
my $flags = ( $config->{'recurse'} ? 'R' : '' ) .
|
141 |
$config->{'source'}->{'dataset'},
|
| 151 |
( $config->{'verbose'} == 2 ? 'v' : '' );
|
142 |
$config->{'target'}->{'lastSnap'},
|
| 152 |
my $source = $config->{'source'}->{'dataset'} . '@' . $lastSource;
|
143 |
$config->{'source'}->{'dataset'},
|
| 153 |
my $target = $config->{'target'}->{'dataset'};
|
144 |
$config->{'source'}->{'lastSnap'}
|
| 154 |
# first create the replicate command. The send command request recursion (-R)
|
145 |
);
|
| 155 |
# and the range of snapshots including all intermediate ones (-I)
|
146 |
# prepend 'zfs send' and the flags. Note that verbose is only for the one which is local
|
| 156 |
my $command = 'zfs send -RI ';
|
147 |
$sourceCommand = 'zfs send -' .
|
| 157 |
$command .= $config->{'target'}->{'dataset'} . '@' . $config->{'target'} . ' ';
|
148 |
( $config->{'recurse'} ? 'R' : '' ) . # recurse if they asked for it
|
| - |
|
149 |
# turn on verbose if they asked for level 2 AND if source is local
|
| 158 |
$command .= $config->{'source'}->{'dataset'} . '@' . $config->{'source'};
|
150 |
( $config->{'verbose'} > 1 && ! $config->{'source'}->{'server'} ? 'v' : '' ) .
|
| - |
|
151 |
# this is the part that asks for incremental
|
| - |
|
152 |
'I ' .
|
| - |
|
153 |
$sourceCommand;
|
| - |
|
154 |
# wrap the ssh call if this is remote
|
| 159 |
$command = "ssh $config->{source}->{server} '$config->{'source'}Command'" if $config->{'source'}->{'server'};
|
155 |
$sourceCommand = "ssh $config->{source}->{server} '$sourceCommand'" if $config->{'source'}->{'server'};
|
| 160 |
|
- |
|
| - |
|
156 |
# Now, build the target command
|
| 161 |
$command = 'zfs receive -v ';
|
157 |
my $targetCommand = 'zfs receive ' .
|
| - |
|
158 |
( ! $config->{'target'}->{'server'} && $config->{'verbose'} > 1 ? '-v ' : '') .
|
| 162 |
$command .= $config->{'target'}->{'dataset'};
|
159 |
$config->{'target'}->{'dataset'};
|
| 163 |
$command = "ssh $config->{target}->{server} '$config->{'source'}Command'" if $config->{'target'}->{'server'};
|
160 |
$targetCommand = "ssh $config->{target}->{server} '$targetCommand'" if $config->{'target'}->{'server'};
|
| - |
|
161 |
# return the command
|
| 164 |
push @return, $command . ' | ' . $command;
|
162 |
return $sourceCommand . ' | ' . $targetCommand;
|
| 165 |
} else {
|
163 |
} else { # source and target are in sync, so do nothing
|
| 166 |
push @return, '# Nothing new to sync';
|
164 |
return '# Nothing new to sync';
|
| 167 |
}
|
165 |
}
|
| 168 |
# now, check for snapshots to remove
|
- |
|
| 169 |
#if ( $config->{'actions'}->{'deleteTarget'} ) {
|
- |
|
| 170 |
# my $delete = $config->{'actions'}->{'deleteTarget'};
|
- |
|
| 171 |
# foreach my $ds ( @$delete ) {
|
- |
|
| 172 |
# push @return, "zfs destroy -r $config->{target}->{'dataset'}\@$ds";
|
- |
|
| 173 |
# }
|
- |
|
| 174 |
#} else {
|
- |
|
| 175 |
# push @return, "# No old snapshots to be removed";
|
- |
|
| 176 |
#}
|
- |
|
| 177 |
return \@return;
|
- |
|
| 178 |
}
|
166 |
}
|
| 179 |
|
167 |
|
| 180 |
# find the last snapshot in a hash. The hash is assumed to have a subkey
|
168 |
# find the last snapshot in a hash. The hash is assumed to have a subkey
|
| 181 |
# 'key'. look for the largest subkey, and return the key for it
|
169 |
# 'key'. look for the largest subkey, and return the key for it
|
| 182 |
sub getLastSnapshot {
|
170 |
sub getLastSnapshot {
|
| Line 227... |
Line 215... |
| 227 |
} else {
|
215 |
} else {
|
| 228 |
return( '','',\@warnings);
|
216 |
return( '','',\@warnings);
|
| 229 |
}
|
217 |
}
|
| 230 |
} # sub calculate
|
218 |
} # sub calculate
|
| 231 |
|
219 |
|
| - |
|
220 |
sub help {
|
| - |
|
221 |
use File::Basename;
|
| - |
|
222 |
my $me = fileparse( $0 );
|
| - |
|
223 |
my $helpMessage = <<" EOF";
|
| - |
|
224 |
$me [flags] [source [target]]
|
| - |
|
225 |
Syncs source dataset to target dataset
|
| - |
|
226 |
|
| - |
|
227 |
Parameters (optional)
|
| - |
|
228 |
source - dataset syncing from
|
| - |
|
229 |
target - dataset syncing to
|
| - |
|
230 |
|
| - |
|
231 |
Flags
|
| - |
|
232 |
--source|s - Alternate way to pass source dataset
|
| - |
|
233 |
--target|t - Alternate way to pass target dataset
|
| - |
|
234 |
--filter|f - Filter (regex) to limit source snapshots to process
|
| - |
|
235 |
--dryrun|n - Only displays command(s) to be run
|
| - |
|
236 |
--recurse|r - Process dataset and all child datasets
|
| - |
|
237 |
--verbose|v - increase verbosity of output
|
| - |
|
238 |
|
| - |
|
239 |
May use short flags with bundling, ie -nrvv is valid for
|
| - |
|
240 |
--dryrun --recurse --verbose --verbose
|
| - |
|
241 |
|
| - |
|
242 |
Either source or target must contain a DNS name or IP address of a remote
|
| - |
|
243 |
machine, separated from the dataset with a colon, ie
|
| - |
|
244 |
--source fbsd:storage/mydata
|
| - |
|
245 |
would use the dataset storage/mydata on the server fbsd. The other dataset
|
| - |
|
246 |
is assumed to be the local machine
|
| - |
|
247 |
|
| - |
|
248 |
filter is a string which is a valid regular expression. Only snapshots matching
|
| - |
|
249 |
that string will be used from the source dataset
|
| - |
|
250 |
|
| - |
|
251 |
By default, only error messages are displayed. verbose will display statistics
|
| - |
|
252 |
on size and transfer time. Invoking twice will display entire output of
|
| - |
|
253 |
send/receive (whichever is the local machine)
|
| - |
|
254 |
EOF
|
| - |
|
255 |
# get rid of indentation
|
| - |
|
256 |
$helpMessage =~ s/^ //;
|
| - |
|
257 |
$helpMessage =~ s/\n /\n/g;
|
| - |
|
258 |
print $helpMessage;
|
| - |
|
259 |
exit 1;
|
| - |
|
260 |
} # help
|
| - |
|
261 |
|
| - |
|
262 |
|
| 232 |
GetOptions( $config,
|
263 |
GetOptions( $config,
|
| 233 |
'source|s=s',
|
264 |
'source|s=s',
|
| 234 |
'target|t=s',
|
265 |
'target|t=s',
|
| 235 |
'filter|f=s',
|
266 |
'filter|f=s',
|
| 236 |
'dryrun|n',
|
267 |
'dryrun|n',
|
| 237 |
'recurse|r',
|
268 |
'recurse|r',
|
| 238 |
'verbose|v',
|
269 |
'verbose|v+',
|
| 239 |
'help|h'
|
270 |
'help|h'
|
| 240 |
);
|
271 |
);
|
| 241 |
|
272 |
|
| - |
|
273 |
&help() if $config->{'help'};
|
| 242 |
# allow them to use positional, without flags, such as
|
274 |
# allow them to use positional, without flags, such as
|
| 243 |
# replicate source target --filter='regex' -n
|
275 |
# replicate source target --filter='regex' -n
|
| 244 |
$config->{'source'} = shift unless $config->{'source'};
|
276 |
$config->{'source'} = shift unless $config->{'source'};
|
| 245 |
$config->{'target'} = shift unless $config->{'target'};
|
277 |
$config->{'target'} = shift unless $config->{'target'};
|
| 246 |
die "You must enter a source and a target, at a minimum\n" unless $config->{'source'} && $config->{'target'};
|
278 |
die "You must enter a source and a target, at a minimum\n" unless $config->{'source'} && $config->{'target'};
|
| 247 |
|
279 |
|
| - |
|
280 |
# keep track of when we started this run
|
| - |
|
281 |
$config->{'report'}->{'Start Time'} = time;
|
| - |
|
282 |
|
| 248 |
# WARNING: this converts source and targets from a string to a hash
|
283 |
# WARNING: this converts source and targets from a string to a hash
|
| 249 |
# '10.0.0.1:data/set' becomes ( 'server' => '10.0.0.1', 'dataset' => 'data/set')
|
284 |
# '10.0.0.1:data/set' becomes ( 'server' => '10.0.0.1', 'dataset' => 'data/set')
|
| 250 |
# and 'data/set' becomes ( 'server' => '', 'dataset' => 'data/set')
|
285 |
# and 'data/set' becomes ( 'server' => '', 'dataset' => 'data/set')
|
| 251 |
$config->{'source'} = &parseDataSet( $config->{'source'} );
|
286 |
$config->{'source'} = &parseDataSet( $config->{'source'} );
|
| 252 |
$config->{'target'} = &parseDataSet( $config->{'target'} );
|
287 |
$config->{'target'} = &parseDataSet( $config->{'target'} );
|
| 253 |
|
288 |
|
| 254 |
# both source and target can not have a server portion; one must be local
|
289 |
# both source and target can not have a server portion; one must be local
|
| 255 |
die "Source and Target can not both be remote\n" if $config->{'source'}->{'server'} && $config->{'target'}->{'server'};
|
290 |
die "Source and Target can not both be remote\n" if $config->{'source'}->{'server'} && $config->{'target'}->{'server'};
|
| 256 |
|
291 |
|
| 257 |
$config->{'source'}->{'snapshots'} = &getSnaps( $config->{'source'}, $config->{'filter'} );
|
292 |
# connect to servers and get all existing snapshots
|
| 258 |
$config->{'target'}->{'snapshots'} = &getSnaps( $config->{'target'}, $config->{'filter'} );
|
293 |
$config->{'target'}->{'snapshots'} = &getSnaps( $config->{'target'}, $config->{'filter'} );
|
| - |
|
294 |
$config->{'source'}->{'snapshots'} = &getSnaps( $config->{'source'}, $config->{'filter'} );
|
| 259 |
|
295 |
|
| 260 |
# we sync from last snap on target machine to last snap on source machine
|
296 |
# we sync from last snap on target machine to last snap on source machine. calculate simply
|
| - |
|
297 |
# finds the last snapshot on source and target
|
| 261 |
my ( $lastSource, $lastTarget ) = &calculate( $config );
|
298 |
( $config->{'source'}->{'lastSnap'}, $config->{'target'}->{'lastSnap'} ) = &calculate( $config );
|
| 262 |
|
299 |
|
| - |
|
300 |
# calculate transfer size if they want any feedback at all. Since this does take a few seconds
|
| - |
|
301 |
# to calculate, we won't run it unless they want a report
|
| 263 |
#print Dumper( $config ) . "\nSource = $lastSource\nTarget = $lastTarget\n"; die;
|
302 |
$config->{'report'}->{'Bytes Transferred'} = &findSize( $config ) if $config->{'verbose'};
|
| 264 |
|
303 |
|
| 265 |
# actually creates the commands to do the replicate
|
304 |
# actually creates the commands to do the replicate
|
| 266 |
my $commands = &createCommands( $lastSource, $lastTarget, $config );
|
305 |
my $commands = &createCommands( $config );
|
| - |
|
306 |
print "$commands\n" if $config->{'verbose'} or $config->{'dryrun'};
|
| 267 |
for ( my $i = 0; $i < @{$commands}; $i++ ) {
|
307 |
if ( $config->{'dryrun'} ) {
|
| - |
|
308 |
print "Dry Run\n";
|
| - |
|
309 |
} else {
|
| 268 |
print "$$commands[$i]\n" if $config->{'verbose'} or $config->{'dryrun'};
|
310 |
print qx/$commands/ if $commands =~ m/^[a-zA-Z]/;
|
| - |
|
311 |
}
|
| - |
|
312 |
|
| - |
|
313 |
$config->{'report'}->{'End Time'} = time;
|
| - |
|
314 |
$config->{'report'}->{'Elapsed Time'} = $config->{'report'}->{'End Time'} - $config->{'report'}->{'Start Time'};
|
| - |
|
315 |
if ( $config->{'verbose'} ) {
|
| 269 |
if ( $config->{'dryrun'} ) {
|
316 |
if ( $config->{'dryrun'} ) {
|
| 270 |
print "Dry Run\n";
|
317 |
print "Would have transferred $config->{'report'}->{'Bytes Transferred'} bytes\n";
|
| 271 |
} else {
|
318 |
} else {
|
| 272 |
print qx/$$commands[$i]/ if $$commands[$i] =~ m/^[a-zA-Z]/;
|
319 |
print "Transferred $config->{'report'}->{'Bytes Transferred'} bytes in $config->{'report'}->{'Elapsed Time'} seconds\n";
|
| 273 |
}
|
320 |
}
|
| 274 |
}
|
321 |
}
|
| 275 |
|
- |
|
| 276 |
#print Dumper( $config );
|
- |
|
| 277 |
|
- |
|
| 278 |
1;
|
322 |
1;
|