Subversion Repositories sysadmin_scripts

Rev

Rev 169 | Go to most recent revision | 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
 
170 rodolico 13
my $dryRun = 0; # if set, will only display the command to be executed
14
 
166 rodolico 15
my $config = {
16
   # compile the regex
17
   'pattern' => qr/auto-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}/
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'};
58
   my ($error, $output ) = &run( $command );
59
   die $output if $error;
60
   my @snaps = split( "\n", $output );
61
   chomp @snaps;
62
   for (my $i = 0; $i < @snaps; $i++ ) {
63
      # parse out the space delmited fields
64
      my ($fullname, $used, $avail, $refer, $mount) = split( /\s+/, $snaps[$i] );
65
      # break the name into dataset and snapname
66
      my ($dataset, $snap) = split( '@', $fullname );
67
      # remove the root dataset name
68
      $dataset =~ s/^$config->{'dataset'}//;
69
      # skip anything not matching our regex
70
      next unless $pattern && $snap && $snap =~ m/$pattern/;
71
      $return{$dataset}{'snaps'}{$snap}{'refer'} = $refer;
72
      $return{$dataset}{'snaps'}{$snap}{'used'} = $used;
73
   }
74
   return \%return;
75
}
76
 
77
sub diffSnaps {
78
   my ( $source, $target ) = @_;
79
   my @source = sort keys %$source;
80
   my @target = sort keys %$target;
81
#   print "===Source\n" . join( "\n", @source ) . "\n===Target\n" . join( "\n", @target ) . "\n";
82
 
83
   my $s = 0;
84
   my $t = 0;
85
   my %return;
86
   $return{'deleteTarget'} = [];
87
   $return{'addTarget'} = [];
88
   $return{'lastMatch'} = 0;
89
   $return{'finalSync'} = 0;
90
   while ( $s < @source && $t < @target ) {
91
      if ( $source[$s] eq $target[$t] ) { # matchies, just keep going
92
#         print "Source $s [$source[$s]] matches target $t [$target[$t]]\n";
93
         $return{'lastMatch'} = $source[$s]; # keep track of the largest match
94
         $s++; $t++;
95
      } elsif ( $target[$t] ne $source[$s] ) { # we are processing stuff that needs to be deleted on target
96
         push @{$return{'deleteTarget'}}, $target[$t];
97
#         print "Adding delete target $t [$target[$t]]\n";
98
         $t++;
99
      }
100
   }
101
   die "Could not reconcile snapshots, ran out of source too soon\n" if $s > @source;
102
   # put a value into finalSync to make sure there is one. If we do not have any sync
103
   # to do, final and lastMatch will be the same
104
   $return{'finalSync'} = $return{'lastMatch'};
105
   while ( $s < @source ) {
106
      push @{$return{'addTarget'}}, $source[$s];
107
      $return{'finalSync'} = $source[$s];
108
      $s++;
109
   }
110
#   die Dumper( \%return );
111
   return \%return;
112
}
113
 
114
sub arrayEquals {
115
   my ($a, $b ) = @_;
116
   return 0 unless @{$a} == @{$b}; # they are different sizes
117
   for ( my $i = 0; $i < @$a; $i++ ) {
118
      if ( $$a[$i] ne $$b[$i] ) {
119
         print STDERR "No Match!\n" . join( "\t", @$a ) . "\n" . join( "\t", @$b ) . "\n";
120
         return 0;
121
      }
122
   }
123
   return 1;
124
}
125
 
126
sub createCommands {
127
   my $config = shift;
128
   my @return;
129
   # check for new snapshots to sync
130
   if ( $config->{'actions'}->{'lastMatch'} ne $config->{'actions'}->{'finalSync'} ) {
131
      # first create the replicate command. The send command request recursion (-R)
132
      # and the range of snapshots including all intermediate ones (-I)
133
      my $sourceCommand = 'zfs send -RI ';
134
      $sourceCommand .= $config->{'source'}->{'dataset'} . '@' . $config->{'actions'}->{'lastMatch'} . ' ';
135
      $sourceCommand .= $config->{'source'}->{'dataset'} . '@' . $config->{'actions'}->{'finalSync'};
136
      $sourceCommand = "ssh $config->{source}->{server} '$sourceCommand'" if $config->{'source'}->{'server'};
137
 
138
      my $targetCommand = 'zfs receive -v ';
139
      $targetCommand .= $config->{'target'}->{'dataset'};
140
      $targetCommand = "ssh $config->{target}->{server} '$sourceCommand'" if $config->{'target'}->{'server'};
141
      push @return, $sourceCommand . ' | ' . $targetCommand;
142
   } else {
168 rodolico 143
      push @return, '# Nothing new to sync';
166 rodolico 144
   }
145
   # now, check for snapshots to remove
146
   if ( $config->{'actions'}->{'deleteTarget'} ) {
168 rodolico 147
      my $delete = $config->{'actions'}->{'deleteTarget'};
148
      foreach my $ds ( @$delete ) {
149
         push @return, "zfs destroy -r $config->{target}->{'dataset'}\@$ds";
166 rodolico 150
      }
168 rodolico 151
   } else {
152
      push @return, "# No old snapshots to be removed";
166 rodolico 153
   }
154
   return \@return;
155
}
156
 
157
 
158
 
159
sub calculate {
160
   my $config = shift;
161
   my $return;
162
   my $allMatch;
163
   my $lastMatch;
164
   foreach my $dataset ( sort keys %{$config->{'source'}->{'snapshots'}} ) {
168 rodolico 165
      next unless exists $config->{'source'}->{'snapshots'}->{$dataset};
166 rodolico 166
      die "No matching target for $dataset\n" unless $config->{'target'}->{'snapshots'}->{$dataset};
167
      $return->{$dataset} = &diffSnaps( 
168
         $config->{'source'}->{'snapshots'}->{$dataset}->{'snaps'},
169
         $config->{'target'}->{'snapshots'}->{$dataset}->{'snaps'}
170
      );
171
      $allMatch = $return->{$dataset} unless $allMatch;
168 rodolico 172
#      die Dumper( $allMatch ) . "\n";
166 rodolico 173
      next;
174
      unless ( 
175
         &arrayEquals( $return->{'allMatch'}->{'deleteTarget'}, $return->{$dataset}->{'deleteTarget'} ) &&
176
         &arrayEquals( $return->{'allMatch'}->{'addTarget'},    $return->{$dataset}->{'addTarget'} )
177
         ) {
178
         warn "Warning: dataset $dataset does not match\n";
179
         last;
180
      }
181
   }
182
   #print Dumper( $allMatch );
183
   $return->{'lastMatch'} = $allMatch->{'lastMatch'};
184
   $return->{'finalSync'} = $allMatch->{'finalSync'};
168 rodolico 185
#   die Dumper( $allMatch->{'deleteTarget'} ) . "\n" ;
186
   $return->{'deleteTarget'} = $allMatch->{'deleteTarget'};
187
#   print Dumper( $return ) . "\n"; die;
166 rodolico 188
   return $return;
189
   #print Dumper( $return ) . "\n"; die;
190
}
191
 
192
$config->{'source'} = &parseDataSet( $source );
193
$config->{'target'} = &parseDataSet( $target );
194
 
195
# both source and target can not have a server portion; one must be local
196
die "Source and Target can not both be remote\n" if $config->{'source'}->{'server'} && $config->{'target'}->{'server'};
197
 
198
$config->{'source'}->{'snapshots'} = &getSnaps( $config->{'source'}, $config->{'pattern'} );
199
$config->{'target'}->{'snapshots'} = &getSnaps( $config->{'target'}, $config->{'pattern'} );
200
 
201
$config->{'actions'} = &calculate( $config );
202
 
168 rodolico 203
#print Dumper( $config ); die;
204
 
166 rodolico 205
my $commands = &createCommands( $config );
206
for ( my $i = 0; $i < @{$commands}; $i++ ) {
168 rodolico 207
   print "$$commands[$i]\n";
170 rodolico 208
   if ( $dryRun ) {
209
      print "Dry Run\n";
210
   } else {
211
      print qx/$$commands[$i]/ if $$commands[$i] =~ m/^[a-zA-Z]/;
212
   }
166 rodolico 213
}
214
 
215
#print Dumper( $config );
216
 
217
1;