Subversion Repositories zfs_utils

Rev

Rev 2 | Rev 5 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 rodolico 1
#! /usr/bin/env perl
2
 
4 rodolico 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
 
2 rodolico 7
use strict;
8
use warnings;
9
 
10
use Data::Dumper;
4 rodolico 11
use Getopt::Long;
12
Getopt::Long::Configure ("bundling");
2 rodolico 13
 
4 rodolico 14
# create our configuration, with some defaults
15
# these are overridden by command line stuff
2 rodolico 16
my $config = {
4 rodolico 17
   # the source, where we're coming from
18
   'source' => '',
19
   # the target, where we want to replicate to
20
   'target' => '',
2 rodolico 21
   # compile the regex
4 rodolico 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
2 rodolico 29
   };
30
 
31
sub parseDataSet {
32
   my $data = shift;
33
   my %return;
34
   my ( $server, $dataset ) = split( ':', $data );
35
   if ( $dataset ) { # they passed a server:dataset
36
      $return{'server'} = $server;
37
      $return{'dataset'} = $dataset;
38
   } else { # only passing in dataset, so assume localhost
39
      $return{'server'} = '';
40
      $return{'dataset'} = $server;
41
   }
42
   return \%return;
43
}
44
 
45
# runs a command, redirecting stderr to stdout (which it ignores)
46
# then returns 0 and $output on success.
47
# if error, returns error code and string describing error
48
sub run {
49
   my $command = shift;
50
   my $output = qx/$command 2>&1/;
51
   if ($? == -1) {
52
      return (-1,"failed to execute: $!");
53
   } elsif ($? & 127) {
54
      return ($?, sprintf "child died with signal %d, %s coredump",
55
        ($? & 127),  ($? & 128) ? 'with' : 'without' );
56
   } else {
57
      return ($? >> 8, sprintf "child exited with value %d", $? >> 8 ) if $? >> 8;
58
   }
59
   return (0,$output);
60
}
61
 
62
 
63
sub getSnaps {
64
   my ($config,$pattern) = @_;
65
   my %return;
66
   # actual command to run to get all snapshots, recursively, of the dataset
67
   my $command = 'zfs list -r -t snap ' . $config->{'dataset'};
68
   $command = "ssh $config->{server} '$command'" if $config->{'server'};
69
   #die "$command\n";
70
   my ($error, $output ) = &run( $command );
71
   #die "Error running $command with output\n$output" if $error;
72
   my @snaps = split( "\n", $output );
73
   chomp @snaps;
74
   for (my $i = 0; $i < @snaps; $i++ ) {
75
      # parse out the space delmited fields
76
      my ($fullname, $used, $avail, $refer, $mount) = split( /\s+/, $snaps[$i] );
77
      # break the name into dataset and snapname
78
      my ($dataset, $snap) = split( '@', $fullname );
79
      # remove the root dataset name
80
      $dataset =~ s/^$config->{'dataset'}//;
81
      # skip anything not matching our regex
82
      next unless $pattern && $snap && $snap =~ m/$pattern/;
83
      # grab the matched key
84
      $return{$dataset}{'snaps'}{$snap}{'key'} = $1;
85
      # and remove all non-numerics
86
      $return{$dataset}{'snaps'}{$snap}{'key'} =~ s/[^0-9]//g;
87
      # get the transfer size
88
      $return{$dataset}{'snaps'}{$snap}{'refer'} = $refer;
89
      # get the actual disk space used
90
      $return{$dataset}{'snaps'}{$snap}{'used'} = $used;
91
   }
92
   return \%return;
93
}
94
 
4 rodolico 95
#sub diffSnaps {
96
#   my ( $config->{'source'}, $config->{'target'} ) = @_;
97
#   my @source = sort keys %$config->{'source'};
98
#   my @target = sort keys %$config->{'target'};
2 rodolico 99
#   print "===Source\n" . join( "\n", @source ) . "\n===Target\n" . join( "\n", @target ) . "\n";
100
 
4 rodolico 101
#   my $s = 0;
102
#   my $t = 0;
103
#   my %return;
104
#   $return{'deleteTarget'} = [];
105
#   $return{'addTarget'} = [];
106
#   $return{'lastMatch'} = 0;
107
#   $return{'finalSync'} = 0;
108
#   while ( $s < @source && $t < @target ) {
109
#      if ( $config->{'source'}[$s] eq $config->{'target'}[$t] ) { # matchies, just keep going
110
#         print "Source $s [$config->{'source'}[$s]] matches target $t [$config->{'target'}[$t]]\n";
111
#         $return{'lastMatch'} = $config->{'source'}[$s]; # keep track of the largest match
112
#         $s++; $t++;
113
#      } elsif ( $config->{'target'}[$t] ne $config->{'source'}[$s] ) { # we are processing stuff that needs to be deleted on target
114
#         push @{$return{'deleteTarget'}}, $config->{'target'}[$t];
115
#         print "Adding delete target $t [$config->{'target'}[$t]]\n";
116
#         $t++;
117
#      }
118
#   }
119
#   die "Could not reconcile snapshots, ran out of source too soon\n" if $s > @source;
120
#   # put a value into finalSync to make sure there is one. If we do not have any sync
121
#   # to do, final and lastMatch will be the same
122
#   $return{'finalSync'} = $return{'lastMatch'};
123
#   while ( $s < @source ) {
124
#      push @{$return{'addTarget'}}, $config->{'source'}[$s];
125
#      $return{'finalSync'} = $config->{'source'}[$s];
126
#      $s++;
127
#   }
2 rodolico 128
#   die Dumper( \%return );
4 rodolico 129
#   return \%return;
130
#}
2 rodolico 131
 
4 rodolico 132
#sub arrayEquals {
133
#   my ($a, $b ) = @_;
134
#   return 0 unless @{$a} == @{$b}; # they are different sizes
135
#   for ( my $i = 0; $i < @$a; $i++ ) {
136
#      if ( $$a[$i] ne $$b[$i] ) {
137
#         print STDERR "No Match!\n" . join( "\t", @$a ) . "\n" . join( "\t", @$b ) . "\n";
138
#         return 0;
139
#      }
140
#   }
141
#   return 1;
142
#}
2 rodolico 143
 
144
sub createCommands {
4 rodolico 145
   my ( $config->{'source'}, $config->{'target'}, $config ) = @_;
2 rodolico 146
   my @return;
147
   # check for new snapshots to sync
4 rodolico 148
   if ( $config->{'source'} ne $config->{'target'} ) {
2 rodolico 149
      # first create the replicate command. The send command request recursion (-R)
150
      # and the range of snapshots including all intermediate ones (-I)
4 rodolico 151
      my $config->{'source'}Command = 'zfs send -RI ';
152
      $config->{'source'}Command .= $config->{'source'}->{'dataset'} . '@' . $config->{'target'} . ' ';
153
      $config->{'source'}Command .= $config->{'source'}->{'dataset'} . '@' . $config->{'source'};
154
      $config->{'source'}Command = "ssh $config->{source}->{server} '$config->{'source'}Command'" if $config->{'source'}->{'server'};
2 rodolico 155
 
4 rodolico 156
      my $config->{'target'}Command = 'zfs receive -v ';
157
      $config->{'target'}Command .= $config->{'target'}->{'dataset'};
158
      $config->{'target'}Command = "ssh $config->{target}->{server} '$config->{'source'}Command'" if $config->{'target'}->{'server'};
159
      push @return, $config->{'source'}Command . ' | ' . $config->{'target'}Command;
2 rodolico 160
   } else {
161
      push @return, '# Nothing new to sync';
162
   }
163
   # now, check for snapshots to remove
164
   #if ( $config->{'actions'}->{'deleteTarget'} ) {
165
   #   my $delete = $config->{'actions'}->{'deleteTarget'};
166
   #   foreach my $ds ( @$delete ) {
167
   #      push @return, "zfs destroy -r $config->{target}->{'dataset'}\@$ds";
168
   #   }
169
   #} else {
170
   #   push @return, "# No old snapshots to be removed";
171
   #}
172
   return \@return;
173
}
174
 
175
# find the last snapshot in a hash. The hash is assumed to have a subkey
176
# 'key'. look for the largest subkey, and return the key for it
177
sub getLastSnapshot {
178
   my $snapList = shift;
179
   my $lastKey = 0;
180
   my $lastSnap = '';
181
   foreach my $snap ( keys %$snapList ) {
182
      if ( $snapList->{$snap}->{'key'} > $lastKey ) {
183
         $lastKey = $snapList->{$snap}->{'key'};
184
         $lastSnap = $snap;
185
      }
186
   }
187
   return $lastSnap;
188
}
189
 
190
 
191
sub calculate {
192
   my $config = shift;
193
 
194
   my @warnings;
195
 
196
   # find the last snapshot date in each dataset, on each target
197
   foreach my $machine ( 'source', 'target' ) {
198
      $config->{$machine}->{'last'} = 0; # track the last entry in all children in dataset
199
      $config->{$machine}->{'allOk'} = 1; # assumed to be true, becomes false if some children do not have snapshots
200
      foreach my $child ( keys %{ $config->{$machine}->{'snapshots'} } ) {
201
         $config->{$machine}->{'snapshots'}->{$child}->{'last'} = 
202
            &getLastSnapshot( $config->{$machine}->{'snapshots'}->{$child}->{'snaps'} );
203
         # set the machine last if we haven't done so yet
204
         $config->{$machine}->{'last'} = $config->{$machine}->{'snapshots'}->{$child}->{'last'} unless $config->{$machine}->{'last'};
205
         # keep track of the last snapshot for each set
206
         if ( $config->{$machine}->{'last'} ne $config->{$machine}->{'snapshots'}->{$child}->{'last'} ) {
207
            $config->{$machine}->{'allOk'} = 0;
208
            push @warnings, "Warning: $machine does not have consistent snapshots at $child";;
209
         }
210
      }
211
   }
212
   # make sure the source has a corresponding snap for target->last
213
   foreach my $child ( keys %{ $config->{'target'}->{'snapshots'} } ) {
214
      if (! exists ($config->{'source'}->{'snapshots'}->{$child}->{'snaps'}->{$config->{'target'}->{'snapshots'}->{$child}->{'last'}} ) ) {
215
         $config->{'source'}->{'allOk'} = 0;
216
         push @warnings, "Warning: We  do not have consistent snapshots";
217
      }
218
   }
219
   my $return;
220
   if ( $config->{'source'}->{'allOk'} and $config->{'target'}->{'allOk'} ) { # whew, they match
221
      return( $config->{'source'}->{'last'}, $config->{'target'}->{'last'}, \@warnings );
222
   } else {
223
      return( '','',\@warnings);
224
   }
4 rodolico 225
} # sub calculate
2 rodolico 226
 
4 rodolico 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
);
2 rodolico 236
 
4 rodolico 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')
246
$config->{'source'} = &parseDataSet( $config->{'source'} );
247
$config->{'target'} = &parseDataSet( $config->{'target'} );
248
 
2 rodolico 249
# both source and target can not have a server portion; one must be local
250
die "Source and Target can not both be remote\n" if $config->{'source'}->{'server'} && $config->{'target'}->{'server'};
251
 
4 rodolico 252
$config->{'source'}->{'snapshots'} = &getSnaps( $config->{'source'}, $config->{'filter'} );
253
$config->{'target'}->{'snapshots'} = &getSnaps( $config->{'target'}, $config->{'filter'} );
2 rodolico 254
 
4 rodolico 255
# we sync from last snap on target machine to last snap on source machine
2 rodolico 256
my ( $lastSource, $lastTarget ) = &calculate( $config );
257
 
258
#print Dumper( $config ) . "\nSource = $lastSource\nTarget = $lastTarget\n"; die;
259
 
4 rodolico 260
# actually creates the commands to do the replicate
2 rodolico 261
my $commands = &createCommands( $lastSource, $lastTarget, $config );
262
for ( my $i = 0; $i < @{$commands}; $i++ ) {
4 rodolico 263
   print "$$commands[$i]\n" if $config->{'verbose'} or $config->{'dryrun'};
264
   if ( $config->{'dryrun'} ) {
2 rodolico 265
      print "Dry Run\n";
266
   } else {
267
      print qx/$$commands[$i]/ if $$commands[$i] =~ m/^[a-zA-Z]/;
268
   }
269
}
270
 
271
#print Dumper( $config );
272
 
273
1;