| Line 1... |
Line 1... |
| 1 |
#! /usr/bin/env perl
|
1 |
#! /usr/bin/env perl
|
| 2 |
|
2 |
|
| - |
|
3 |
# very simple script to replicate a ZFS snapshot to another server.
|
| - |
|
4 |
# no fancy bells and whistles, does not create snapshots, and does
|
| - |
|
5 |
# not prune them. No major error checking either
|
| - |
|
6 |
|
| 3 |
use strict;
|
7 |
use strict;
|
| 4 |
use warnings;
|
8 |
use warnings;
|
| 5 |
|
9 |
|
| 6 |
use Data::Dumper;
|
10 |
use Data::Dumper;
|
| - |
|
11 |
use Getopt::Long;
|
| - |
|
12 |
Getopt::Long::Configure ("bundling");
|
| 7 |
|
13 |
|
| 8 |
my $source = shift;
|
- |
|
| 9 |
my $target = shift;
|
- |
|
| 10 |
|
- |
|
| 11 |
die "Usage: replicate source target\n" unless $source && $target;
|
14 |
# create our configuration, with some defaults
|
| 12 |
|
- |
|
| 13 |
my $dryRun = 0; # if set, will only display the command to be executed
|
15 |
# these are overridden by command line stuff
|
| 14 |
|
- |
|
| 15 |
my $config = {
|
16 |
my $config = {
|
| - |
|
17 |
# the source, where we're coming from
|
| - |
|
18 |
'source' => '',
|
| - |
|
19 |
# the target, where we want to replicate to
|
| - |
|
20 |
'target' => '',
|
| 16 |
# compile the regex
|
21 |
# compile the regex
|
| 17 |
'pattern' => qr/(\d{4}.\d{2}.\d{2}.\d{2}.\d{2})/
|
22 |
'filter' => qr/(\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
|
| - |
|
24 |
'dryrun' => 0,
|
| - |
|
25 |
# whether to do all child datasets also (default)
|
| - |
|
26 |
'recurse' => 1,
|
| - |
|
27 |
# show more information
|
| - |
|
28 |
'verbose' => 0
|
| 18 |
};
|
29 |
};
|
| 19 |
|
30 |
|
| 20 |
sub parseDataSet {
|
31 |
sub parseDataSet {
|
| 21 |
my $data = shift;
|
32 |
my $data = shift;
|
| 22 |
my %return;
|
33 |
my %return;
|
| Line 79... |
Line 90... |
| 79 |
$return{$dataset}{'snaps'}{$snap}{'used'} = $used;
|
90 |
$return{$dataset}{'snaps'}{$snap}{'used'} = $used;
|
| 80 |
}
|
91 |
}
|
| 81 |
return \%return;
|
92 |
return \%return;
|
| 82 |
}
|
93 |
}
|
| 83 |
|
94 |
|
| 84 |
sub diffSnaps {
|
95 |
#sub diffSnaps {
|
| 85 |
my ( $source, $target ) = @_;
|
96 |
# my ( $config->{'source'}, $config->{'target'} ) = @_;
|
| 86 |
my @source = sort keys %$source;
|
97 |
# my @source = sort keys %$config->{'source'};
|
| 87 |
my @target = sort keys %$target;
|
98 |
# my @target = sort keys %$config->{'target'};
|
| 88 |
# print "===Source\n" . join( "\n", @source ) . "\n===Target\n" . join( "\n", @target ) . "\n";
|
99 |
# print "===Source\n" . join( "\n", @source ) . "\n===Target\n" . join( "\n", @target ) . "\n";
|
| 89 |
|
100 |
|
| 90 |
my $s = 0;
|
101 |
# my $s = 0;
|
| 91 |
my $t = 0;
|
102 |
# my $t = 0;
|
| 92 |
my %return;
|
103 |
# my %return;
|
| 93 |
$return{'deleteTarget'} = [];
|
104 |
# $return{'deleteTarget'} = [];
|
| 94 |
$return{'addTarget'} = [];
|
105 |
# $return{'addTarget'} = [];
|
| 95 |
$return{'lastMatch'} = 0;
|
106 |
# $return{'lastMatch'} = 0;
|
| 96 |
$return{'finalSync'} = 0;
|
107 |
# $return{'finalSync'} = 0;
|
| 97 |
while ( $s < @source && $t < @target ) {
|
108 |
# while ( $s < @source && $t < @target ) {
|
| 98 |
if ( $source[$s] eq $target[$t] ) { # matchies, just keep going
|
109 |
# if ( $config->{'source'}[$s] eq $config->{'target'}[$t] ) { # matchies, just keep going
|
| 99 |
# print "Source $s [$source[$s]] matches target $t [$target[$t]]\n";
|
110 |
# print "Source $s [$config->{'source'}[$s]] matches target $t [$config->{'target'}[$t]]\n";
|
| 100 |
$return{'lastMatch'} = $source[$s]; # keep track of the largest match
|
111 |
# $return{'lastMatch'} = $config->{'source'}[$s]; # keep track of the largest match
|
| 101 |
$s++; $t++;
|
112 |
# $s++; $t++;
|
| 102 |
} elsif ( $target[$t] ne $source[$s] ) { # we are processing stuff that needs to be deleted on target
|
113 |
# } elsif ( $config->{'target'}[$t] ne $config->{'source'}[$s] ) { # we are processing stuff that needs to be deleted on target
|
| 103 |
push @{$return{'deleteTarget'}}, $target[$t];
|
114 |
# push @{$return{'deleteTarget'}}, $config->{'target'}[$t];
|
| 104 |
# print "Adding delete target $t [$target[$t]]\n";
|
115 |
# print "Adding delete target $t [$config->{'target'}[$t]]\n";
|
| 105 |
$t++;
|
116 |
# $t++;
|
| 106 |
}
|
117 |
# }
|
| 107 |
}
|
118 |
# }
|
| 108 |
die "Could not reconcile snapshots, ran out of source too soon\n" if $s > @source;
|
119 |
# die "Could not reconcile snapshots, ran out of source too soon\n" if $s > @source;
|
| 109 |
# put a value into finalSync to make sure there is one. If we do not have any sync
|
120 |
# # put a value into finalSync to make sure there is one. If we do not have any sync
|
| 110 |
# to do, final and lastMatch will be the same
|
121 |
# # to do, final and lastMatch will be the same
|
| 111 |
$return{'finalSync'} = $return{'lastMatch'};
|
122 |
# $return{'finalSync'} = $return{'lastMatch'};
|
| 112 |
while ( $s < @source ) {
|
123 |
# while ( $s < @source ) {
|
| 113 |
push @{$return{'addTarget'}}, $source[$s];
|
124 |
# push @{$return{'addTarget'}}, $config->{'source'}[$s];
|
| 114 |
$return{'finalSync'} = $source[$s];
|
125 |
# $return{'finalSync'} = $config->{'source'}[$s];
|
| 115 |
$s++;
|
126 |
# $s++;
|
| 116 |
}
|
127 |
# }
|
| 117 |
# die Dumper( \%return );
|
128 |
# die Dumper( \%return );
|
| 118 |
return \%return;
|
129 |
# return \%return;
|
| 119 |
}
|
130 |
#}
|
| 120 |
|
131 |
|
| 121 |
sub arrayEquals {
|
132 |
#sub arrayEquals {
|
| 122 |
my ($a, $b ) = @_;
|
133 |
# my ($a, $b ) = @_;
|
| 123 |
return 0 unless @{$a} == @{$b}; # they are different sizes
|
134 |
# return 0 unless @{$a} == @{$b}; # they are different sizes
|
| 124 |
for ( my $i = 0; $i < @$a; $i++ ) {
|
135 |
# for ( my $i = 0; $i < @$a; $i++ ) {
|
| 125 |
if ( $$a[$i] ne $$b[$i] ) {
|
136 |
# if ( $$a[$i] ne $$b[$i] ) {
|
| 126 |
print STDERR "No Match!\n" . join( "\t", @$a ) . "\n" . join( "\t", @$b ) . "\n";
|
137 |
# print STDERR "No Match!\n" . join( "\t", @$a ) . "\n" . join( "\t", @$b ) . "\n";
|
| 127 |
return 0;
|
138 |
# return 0;
|
| 128 |
}
|
139 |
# }
|
| 129 |
}
|
140 |
# }
|
| 130 |
return 1;
|
141 |
# return 1;
|
| 131 |
}
|
142 |
#}
|
| 132 |
|
143 |
|
| 133 |
sub createCommands {
|
144 |
sub createCommands {
|
| 134 |
my ( $source, $target, $config ) = @_;
|
145 |
my ( $config->{'source'}, $config->{'target'}, $config ) = @_;
|
| 135 |
my @return;
|
146 |
my @return;
|
| 136 |
# check for new snapshots to sync
|
147 |
# check for new snapshots to sync
|
| 137 |
if ( $source ne $target ) {
|
148 |
if ( $config->{'source'} ne $config->{'target'} ) {
|
| 138 |
# first create the replicate command. The send command request recursion (-R)
|
149 |
# first create the replicate command. The send command request recursion (-R)
|
| 139 |
# and the range of snapshots including all intermediate ones (-I)
|
150 |
# and the range of snapshots including all intermediate ones (-I)
|
| 140 |
my $sourceCommand = 'zfs send -RI ';
|
151 |
my $config->{'source'}Command = 'zfs send -RI ';
|
| 141 |
$sourceCommand .= $config->{'source'}->{'dataset'} . '@' . $target . ' ';
|
152 |
$config->{'source'}Command .= $config->{'source'}->{'dataset'} . '@' . $config->{'target'} . ' ';
|
| 142 |
$sourceCommand .= $config->{'source'}->{'dataset'} . '@' . $source;
|
153 |
$config->{'source'}Command .= $config->{'source'}->{'dataset'} . '@' . $config->{'source'};
|
| 143 |
$sourceCommand = "ssh $config->{source}->{server} '$sourceCommand'" if $config->{'source'}->{'server'};
|
154 |
$config->{'source'}Command = "ssh $config->{source}->{server} '$config->{'source'}Command'" if $config->{'source'}->{'server'};
|
| 144 |
|
155 |
|
| 145 |
my $targetCommand = 'zfs receive -v ';
|
156 |
my $config->{'target'}Command = 'zfs receive -v ';
|
| 146 |
$targetCommand .= $config->{'target'}->{'dataset'};
|
157 |
$config->{'target'}Command .= $config->{'target'}->{'dataset'};
|
| 147 |
$targetCommand = "ssh $config->{target}->{server} '$sourceCommand'" if $config->{'target'}->{'server'};
|
158 |
$config->{'target'}Command = "ssh $config->{target}->{server} '$config->{'source'}Command'" if $config->{'target'}->{'server'};
|
| 148 |
push @return, $sourceCommand . ' | ' . $targetCommand;
|
159 |
push @return, $config->{'source'}Command . ' | ' . $config->{'target'}Command;
|
| 149 |
} else {
|
160 |
} else {
|
| 150 |
push @return, '# Nothing new to sync';
|
161 |
push @return, '# Nothing new to sync';
|
| 151 |
}
|
162 |
}
|
| 152 |
# now, check for snapshots to remove
|
163 |
# now, check for snapshots to remove
|
| 153 |
#if ( $config->{'actions'}->{'deleteTarget'} ) {
|
164 |
#if ( $config->{'actions'}->{'deleteTarget'} ) {
|
| Line 209... |
Line 220... |
| 209 |
if ( $config->{'source'}->{'allOk'} and $config->{'target'}->{'allOk'} ) { # whew, they match
|
220 |
if ( $config->{'source'}->{'allOk'} and $config->{'target'}->{'allOk'} ) { # whew, they match
|
| 210 |
return( $config->{'source'}->{'last'}, $config->{'target'}->{'last'}, \@warnings );
|
221 |
return( $config->{'source'}->{'last'}, $config->{'target'}->{'last'}, \@warnings );
|
| 211 |
} else {
|
222 |
} else {
|
| 212 |
return( '','',\@warnings);
|
223 |
return( '','',\@warnings);
|
| 213 |
}
|
224 |
}
|
| 214 |
}
|
225 |
} # sub calculate
|
| 215 |
|
226 |
|
| - |
|
227 |
GetOptions( $config,
|
| - |
|
228 |
'source|s=s',
|
| - |
|
229 |
'target|t=s',
|
| - |
|
230 |
'filter|f=s',
|
| - |
|
231 |
'dryrun|n',
|
| - |
|
232 |
'recurse|r',
|
| - |
|
233 |
'verbose|v',
|
| - |
|
234 |
'help|h'
|
| - |
|
235 |
);
|
| - |
|
236 |
|
| - |
|
237 |
# allow them to use positional, without flags, such as
|
| - |
|
238 |
# replicate source target --filter='regex' -n
|
| - |
|
239 |
$config->{'source'} = shift unless $config->{'source'};
|
| - |
|
240 |
$config->{'target'} = shift unless $config->{'target'};
|
| - |
|
241 |
die "You must enter a source and a target, at a minimum\n" unless $config->{'source'} && $config->{'target'};
|
| - |
|
242 |
|
| - |
|
243 |
# WARNING: this converts source and targets from a string to a hash
|
| - |
|
244 |
# '10.0.0.1:data/set' becomes ( 'server' => '10.0.0.1', 'dataset' => 'data/set')
|
| - |
|
245 |
# and 'data/set' becomes ( 'server' => '', 'dataset' => 'data/set')
|
| 216 |
$config->{'source'} = &parseDataSet( $source );
|
246 |
$config->{'source'} = &parseDataSet( $config->{'source'} );
|
| 217 |
$config->{'target'} = &parseDataSet( $target );
|
247 |
$config->{'target'} = &parseDataSet( $config->{'target'} );
|
| 218 |
|
248 |
|
| 219 |
# both source and target can not have a server portion; one must be local
|
249 |
# both source and target can not have a server portion; one must be local
|
| 220 |
die "Source and Target can not both be remote\n" if $config->{'source'}->{'server'} && $config->{'target'}->{'server'};
|
250 |
die "Source and Target can not both be remote\n" if $config->{'source'}->{'server'} && $config->{'target'}->{'server'};
|
| 221 |
|
251 |
|
| 222 |
$config->{'source'}->{'snapshots'} = &getSnaps( $config->{'source'}, $config->{'pattern'} );
|
252 |
$config->{'source'}->{'snapshots'} = &getSnaps( $config->{'source'}, $config->{'filter'} );
|
| 223 |
$config->{'target'}->{'snapshots'} = &getSnaps( $config->{'target'}, $config->{'pattern'} );
|
253 |
$config->{'target'}->{'snapshots'} = &getSnaps( $config->{'target'}, $config->{'filter'} );
|
| 224 |
|
254 |
|
| 225 |
# $config->{'actions'} = &calculate( $config );
|
255 |
# we sync from last snap on target machine to last snap on source machine
|
| 226 |
my ( $lastSource, $lastTarget ) = &calculate( $config );
|
256 |
my ( $lastSource, $lastTarget ) = &calculate( $config );
|
| 227 |
|
257 |
|
| 228 |
#print Dumper( $config ) . "\nSource = $lastSource\nTarget = $lastTarget\n"; die;
|
258 |
#print Dumper( $config ) . "\nSource = $lastSource\nTarget = $lastTarget\n"; die;
|
| 229 |
|
259 |
|
| - |
|
260 |
# actually creates the commands to do the replicate
|
| 230 |
my $commands = &createCommands( $lastSource, $lastTarget, $config );
|
261 |
my $commands = &createCommands( $lastSource, $lastTarget, $config );
|
| 231 |
for ( my $i = 0; $i < @{$commands}; $i++ ) {
|
262 |
for ( my $i = 0; $i < @{$commands}; $i++ ) {
|
| 232 |
print "$$commands[$i]\n";
|
263 |
print "$$commands[$i]\n" if $config->{'verbose'} or $config->{'dryrun'};
|
| 233 |
if ( $dryRun ) {
|
264 |
if ( $config->{'dryrun'} ) {
|
| 234 |
print "Dry Run\n";
|
265 |
print "Dry Run\n";
|
| 235 |
} else {
|
266 |
} else {
|
| 236 |
print qx/$$commands[$i]/ if $$commands[$i] =~ m/^[a-zA-Z]/;
|
267 |
print qx/$$commands[$i]/ if $$commands[$i] =~ m/^[a-zA-Z]/;
|
| 237 |
}
|
268 |
}
|
| 238 |
}
|
269 |
}
|