Subversion Repositories sysadmin_scripts

Rev

Rev 170 | Details | Compare with Previous | Last modification | View Log | RSS feed

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