Subversion Repositories zfs_utils

Rev

Rev 5 | Rev 7 | 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
6 rodolico 22
   'filter' => '(\d{4}.\d{2}.\d{2}.\d{2}.\d{2})',
4 rodolico 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)
6 rodolico 26
   'recurse' => 0,
4 rodolico 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
 
6 rodolico 95
# get tne number of bytes we will be syncing.
96
sub findSize {
97
   my $config = shift;
98
   # check for new snapshots to sync. If they are equal, we are up to date
99
   if ( $config->{'source'}->{'lastSnap'} ne $config->{'target'}->{'lastSnap'} ) {
100
      # Build the source command
101
      my $sourceCommand = sprintf( '%s@%s %s@%s', 
102
                               $config->{'source'}->{'dataset'},
103
                               $config->{'target'}->{'lastSnap'},
104
                               $config->{'source'}->{'dataset'},
105
                               $config->{'source'}->{'lastSnap'}
106
                           );
107
      # prepend 'zfs send' and the flags. Note that verbose is only for the one which is local
108
      $sourceCommand = 'zfs send -' . 
109
                  ( $config->{'recurse'} ? 'R' : '' ) . # recurse if they asked for it
110
                  # turn on verbose if they asked for level 2 AND if source is local
111
                  'Pwn' .
112
                  # this is the part that asks for incremental
113
                  'I ' .
114
                  $sourceCommand;
115
      # wrap the ssh call if this is remote
116
      $sourceCommand = "ssh $config->{source}->{server} '$sourceCommand'" if  $config->{'source'}->{'server'};
117
      print "Checking Size with\n$sourceCommand\n" if $config->{'verbose'} > 2;
118
      my ( $error, $output ) = &run( $sourceCommand );
119
      return -1 if $error;
120
      # the size is the second column (tab separated) of the last line (\n separated) in $output
121
      return ( 
122
               split( 
123
                  "\t",
124
                  (
125
                     split( "\n", $output )
126
                  )[-1]
127
               )
128
            )[1];
129
   } else { # nothing to sync
130
      return 0;
131
   }
132
}
2 rodolico 133
 
6 rodolico 134
# create the command necessary to do the replication
2 rodolico 135
sub createCommands {
6 rodolico 136
   my $config = shift;
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'} ) {
139
      # Build the source command
140
      my $sourceCommand = sprintf( '%s@%s %s@%s', 
141
                               $config->{'source'}->{'dataset'},
142
                               $config->{'target'}->{'lastSnap'},
143
                               $config->{'source'}->{'dataset'},
144
                               $config->{'source'}->{'lastSnap'}
145
                           );
146
      # prepend 'zfs send' and the flags. Note that verbose is only for the one which is local
147
      $sourceCommand = 'zfs send -' . 
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
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
155
      $sourceCommand = "ssh $config->{source}->{server} '$sourceCommand'" if  $config->{'source'}->{'server'};
156
      # Now, build the target command
157
      my $targetCommand = 'zfs receive ' . 
158
                          ( ! $config->{'target'}->{'server'} && $config->{'verbose'} > 1 ? '-v ' : '') .
159
                          $config->{'target'}->{'dataset'};
160
      $targetCommand = "ssh $config->{target}->{server} '$targetCommand'" if  $config->{'target'}->{'server'};
161
      # return the command
162
      return $sourceCommand . ' | ' . $targetCommand;
163
   } else { # source and target are in sync, so do nothing
164
      return '# Nothing new to sync';
2 rodolico 165
   }
166
}
167
 
168
# find the last snapshot in a hash. The hash is assumed to have a subkey
169
# 'key'. look for the largest subkey, and return the key for it
170
sub getLastSnapshot {
171
   my $snapList = shift;
172
   my $lastKey = 0;
173
   my $lastSnap = '';
174
   foreach my $snap ( keys %$snapList ) {
175
      if ( $snapList->{$snap}->{'key'} > $lastKey ) {
176
         $lastKey = $snapList->{$snap}->{'key'};
177
         $lastSnap = $snap;
178
      }
179
   }
180
   return $lastSnap;
181
}
182
 
183
 
184
sub calculate {
185
   my $config = shift;
186
 
187
   my @warnings;
188
 
189
   # find the last snapshot date in each dataset, on each target
190
   foreach my $machine ( 'source', 'target' ) {
191
      $config->{$machine}->{'last'} = 0; # track the last entry in all children in dataset
192
      $config->{$machine}->{'allOk'} = 1; # assumed to be true, becomes false if some children do not have snapshots
193
      foreach my $child ( keys %{ $config->{$machine}->{'snapshots'} } ) {
194
         $config->{$machine}->{'snapshots'}->{$child}->{'last'} = 
195
            &getLastSnapshot( $config->{$machine}->{'snapshots'}->{$child}->{'snaps'} );
196
         # set the machine last if we haven't done so yet
197
         $config->{$machine}->{'last'} = $config->{$machine}->{'snapshots'}->{$child}->{'last'} unless $config->{$machine}->{'last'};
198
         # keep track of the last snapshot for each set
199
         if ( $config->{$machine}->{'last'} ne $config->{$machine}->{'snapshots'}->{$child}->{'last'} ) {
200
            $config->{$machine}->{'allOk'} = 0;
201
            push @warnings, "Warning: $machine does not have consistent snapshots at $child";;
202
         }
203
      }
204
   }
205
   # make sure the source has a corresponding snap for target->last
206
   foreach my $child ( keys %{ $config->{'target'}->{'snapshots'} } ) {
207
      if (! exists ($config->{'source'}->{'snapshots'}->{$child}->{'snaps'}->{$config->{'target'}->{'snapshots'}->{$child}->{'last'}} ) ) {
208
         $config->{'source'}->{'allOk'} = 0;
209
         push @warnings, "Warning: We  do not have consistent snapshots";
210
      }
211
   }
212
   my $return;
213
   if ( $config->{'source'}->{'allOk'} and $config->{'target'}->{'allOk'} ) { # whew, they match
214
      return( $config->{'source'}->{'last'}, $config->{'target'}->{'last'}, \@warnings );
215
   } else {
216
      return( '','',\@warnings);
217
   }
4 rodolico 218
} # sub calculate
2 rodolico 219
 
6 rodolico 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
 
4 rodolico 263
GetOptions( $config,
264
   'source|s=s',
265
   'target|t=s',
266
   'filter|f=s',
267
   'dryrun|n',
268
   'recurse|r',
6 rodolico 269
   'verbose|v+',
4 rodolico 270
   'help|h'
271
);
2 rodolico 272
 
6 rodolico 273
&help() if $config->{'help'};
4 rodolico 274
# allow them to use positional, without flags, such as
275
# replicate source target --filter='regex' -n
276
$config->{'source'} = shift unless $config->{'source'};
277
$config->{'target'} = shift unless $config->{'target'};
278
die "You must enter a source and a target, at a minimum\n" unless $config->{'source'} && $config->{'target'};
279
 
6 rodolico 280
# keep track of when we started this run
281
$config->{'report'}->{'Start Time'} = time;
282
 
4 rodolico 283
# WARNING: this converts source and targets from a string to a hash
284
# '10.0.0.1:data/set' becomes ( 'server' => '10.0.0.1', 'dataset' => 'data/set')
285
# and 'data/set' becomes ( 'server' => '', 'dataset' => 'data/set')
286
$config->{'source'} = &parseDataSet( $config->{'source'} );
287
$config->{'target'} = &parseDataSet( $config->{'target'} );
288
 
2 rodolico 289
# both source and target can not have a server portion; one must be local
290
die "Source and Target can not both be remote\n" if $config->{'source'}->{'server'} && $config->{'target'}->{'server'};
291
 
6 rodolico 292
# connect to servers and get all existing snapshots
293
$config->{'target'}->{'snapshots'} = &getSnaps( $config->{'target'}, $config->{'filter'} );
4 rodolico 294
$config->{'source'}->{'snapshots'} = &getSnaps( $config->{'source'}, $config->{'filter'} );
2 rodolico 295
 
6 rodolico 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
298
( $config->{'source'}->{'lastSnap'}, $config->{'target'}->{'lastSnap'} ) = &calculate( $config );
2 rodolico 299
 
6 rodolico 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
302
$config->{'report'}->{'Bytes Transferred'} = &findSize( $config ) if $config->{'verbose'};
2 rodolico 303
 
4 rodolico 304
# actually creates the commands to do the replicate
6 rodolico 305
my $commands = &createCommands( $config );
306
print "$commands\n" if $config->{'verbose'} or $config->{'dryrun'};
307
if ( $config->{'dryrun'} ) {
308
   print "Dry Run\n";
309
} else {
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'} ) {
4 rodolico 316
   if ( $config->{'dryrun'} ) {
6 rodolico 317
      print "Would have transferred $config->{'report'}->{'Bytes Transferred'} bytes\n";
2 rodolico 318
   } else {
6 rodolico 319
      print "Transferred $config->{'report'}->{'Bytes Transferred'} bytes in $config->{'report'}->{'Elapsed Time'} seconds\n";
2 rodolico 320
   }
321
}
322
1;