Subversion Repositories zfs_utils

Rev

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

Rev 2 Rev 4
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
}