Subversion Repositories sysadmin_scripts

Rev

Rev 97 | Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
96 rodolico 1
#! /usr/bin/env perl
2
 
3
use strict;
4
use warnings;
5
 
6
use Data::Dumper;
7
use Time::Local;
8
use POSIX qw(strftime);
9
use YAML::Tiny; # apt-get libyaml-tiny-perl under debian, BSD Systems: cpan -i YAML::Tiny
10
 
11
 
12
# globals
13
my $CONFIG_FILE_NAME = 'snapShot.yaml';
14
 
15
 
16
# This will be read in from snapShot.yaml
17
my $config;
18
 
19
#
20
# find where the script is actually located as cfg should be there
21
#
22
sub getScriptLocation {
23
   use strict;
24
   use File::Spec::Functions qw(rel2abs);
25
   use File::Basename;
26
   return dirname(rel2abs($0));
27
}
28
 
29
#
30
# Read the configuration file from current location 
31
# and return it as a string
32
#
33
sub readConfig {
34
   my $scriptLocation = &getScriptLocation();
35
   if ( -e "$scriptLocation/$CONFIG_FILE_NAME" ) {
36
      my $yaml = YAML::Tiny->read( "$scriptLocation/$CONFIG_FILE_NAME" );
37
      # use clone_merge to merge conf file into $config
38
      # overwrites anything in $config if it exists in the config file
39
      $config = clone_merge( $config, $yaml->[0] );
40
      return 1;
41
   }
42
   return 0;
43
}
44
 
45
 
46
# parse one single line from the output of `zfs list [-t snapshot]`
47
sub parseListing {
48
   my ($line,$keys) = @_;
49
   chomp $line;
50
   my %values;
51
   @values{@$keys} = split( /\s+/, $line );
52
   return \%values;
53
}      
54
 
55
 
56
# this will parse the date out of the snapshots and put the values into
57
# the hash {'date'}
58
sub parseSnapshots {
59
   my ( $snapShots, $config) = @_;
60
   my $keys = $config->{'snapshot'}->{'parseFields'};
61
   foreach my $snapShot ( keys %$snapShots ) {
62
      my %temp;
63
      # run the regex, capture the output to an array, then populate the hash %temp
64
      # using the regex results as the values, and $keys as the keys
65
      @temp{@$keys} = ( $snapShot =~ m/$config->{'snapshot'}->{'parse'}/ );
66
      # while we're here, calculate the unix time (epoch). NOTE: month is 0 based
67
      $temp{'unix'} = timelocal( 0,$temp{'minute'},$temp{'hour'},$temp{'day'},$temp{'month'}-1,$temp{'year'} );
68
      # put this into our record
69
      $snapShots->{$snapShot}->{'date'} = \%temp;
70
   }
71
}
72
 
73
# run $command, then parse its output and return the results as a hashref
74
sub getListing {
75
   my ($configuration, $regex, $command )  = @_;
76
   my %dataSets;
77
 
78
   # get all datasets
79
   my @zfsList = `$command`;
80
   foreach my $thisSet ( @zfsList ) {
81
      my $temp = &parseListing( $thisSet, $configuration->{'listingKeys'} );
82
      if (  $temp->{'name'} =~ m/^($regex)$/ ) {
83
         $dataSets{$temp->{'name'}} = $temp;
84
      }
85
   }
86
   return \%dataSets;
87
}
88
 
89
# will convert something like 1 day to the number of seconds (86400) for math.
90
# month and year are approximations (30.5 day = a month, 365.2425 days is a year)
91
sub period2seconds {
92
   my ($count, $unit) = ( shift =~ m/\s*(\d+)\s*([a-z]+)\s*/i );
93
   $unit = lc $unit;
94
   if ( unit eq 'hour' ) {
95
      $count *= 3600;
96
   } elsif ( $unit eq 'day' ) {
97
      $count *= 86400;
98
   } elsif ( $unit eq 'week' ) {
99
      $count *= 864000 * 7;
100
   } elsif ( $unit eq 'month' ) {
101
      $count *= 864000 * 30.5;
102
   } elsif ( $unit eq 'year' ) {
103
      $count *= 86400 * 365.2425;
104
   } else {
105
      die "Unknown units [$unit] in period2seconds\n";
106
   }
107
   return $count;
108
}
109
 
110
# Merges datasets, snapshots and some stuff from the configuration into the datasets
111
# hash
112
sub merge {
113
   my ($datasets,$snapshots,$config) = @_;
114
   my $confKeys = $config->{'datasets'};
115
   foreach my $thisDataset ( keys %$datasets ) {
116
      foreach my $conf (keys %$confKeys ) {
117
         if ( $thisDataset =~ m/^$conf$/ ) {
118
            $datasets->{$thisDataset}->{'recursive'} = $confKeys->{$conf}->{'recursive'};
119
            $datasets->{$thisDataset}->{'frequency'} = &period2seconds( $confKeys->{$conf}->{'frequency'} );
120
            $datasets->{$thisDataset}->{'retention'} = &period2seconds( $confKeys->{$conf}->{'retention'} );
121
            last;
122
         } # if
123
      } # foreach
124
      foreach my $snapshot ( keys %$snapshots ) {
125
         if ( $snapshot =~ m/^$thisDataset@/ ) { # this is a match
126
            # copy the snapshot into the dataset
127
            $datasets->{$thisDataset}->{'snapshots'}->{$snapshot} = $snapshots->{$snapshot};
128
            # track the latest snapshot
129
            $datasets->{$thisDataset}->{'lastSnap'} = $snapshots->{$snapshot}->{'date'}->{'unix'}
130
               if ! defined( $datasets->{$thisDataset}->{'lastSnap'} ) || $datasets->{$thisDataset}->{'lastSnap'} < $snapshots->{$snapshot}->{'date'}->{'unix'};
131
            # delete the snapshot
132
            delete $snapshots->{$snapshot};
133
         } # if
134
      } # foreach
135
   } # foreach
136
} # sub merge
137
 
138
sub checkRetention {
139
   my ( $retentionPeriod, $recursive, $snapshots, $now ) = @_;
140
   my @toDelete;
141
   foreach my $thisSnapshot ( keys %$snapshots ) {
142
      # print "checking $thisSnapshot\n\tNow: $now\n\tDate: $snapshots->{$thisSnapshot}->{date}->{unix}\n\tRetention: $retentionPeriod\n\n";
143
      if ( $now - $snapshots->{$thisSnapshot}->{'date'}->{'unix'} > $retentionPeriod ) {
144
         my $command = 'zfs destroy ' . ($recursive ? '-r ' : '') . $thisSnapshot;
145
         push @toDelete, $command;
146
      }
147
   }
148
   return @toDelete;
149
}   
150
 
151
sub makeSnapshot {
152
   my ( $datasetName, $recursive, $snapshotName ) = @_;
153
   return 
154
      'zfs snapshot ' . 
155
      ($recursive ? '-r ' : '') . 
156
      $datasetName . $snapshotName;
157
}
158
 
159
 
160
sub process {
161
   my ( $datasets, $now, $snapshotName, $slop ) = @_;
162
   my @actions;
163
   my @toDelete;
164
   my @toAdd;
165
 
166
   foreach my $thisDataset ( keys %$datasets ) {
167
      push( @toDelete, 
168
         &checkRetention( 
169
         $datasets->{$thisDataset}->{'retention'}, 
170
         $datasets->{$thisDataset}->{'recursive'}, 
171
         $datasets->{$thisDataset}->{'snapshots'}, 
172
         $now )
173
         );
174
      if ( $datasets->{$thisDataset}->{'lastSnap'} + $datasets->{$thisDataset}->{'frequency'} < $now + $slop ) {
175
         push @toAdd, &makeSnapshot( $thisDataset, $datasets->{$thisDataset}->{'recursive'}, $snapshotName )
176
      }
177
   }
178
   return ( @toDelete, @toAdd );
179
}   
180
 
181
sub run {
182
   my $testing = shift;
183
   # bail if there are no commands to run
184
   return 1 unless @_;
185
   if ( $testing ) { # don't do it, just dump the commands
186
      open LOG, ">/tmp/snapShot" or die "could not write to /tmp/snapShot: $!\n";
187
      print LOG join( "\n", @_ ) . "\n";
188
      close LOG;
189
   } else {
190
      my $out;
191
      return 'Not running right now';
192
      while ( my $command = shift ) {
193
         $out .= `$command` . "\n";
194
         if ( $? ) { # we had an error
195
            $out .= "Error executing command\n\t$command\n\t";
196
            if ($? == -1) {
197
                $out .= "failed to execute $command: $!";
198
            } elsif ($? & 127) {
199
                $out .= sprintf( "child died with signal %d, %s coredump", ($? & 127),  ($? & 128) ? 'with' : 'without' );
200
            } else {
201
                $out .= sprintf( "child exited with value %d", $? >> 8 );
202
            }
203
            $out .= "\n";
204
            return $out;
205
         }
206
      }
207
   }
208
   return 0;
209
}
210
 
211
# grab the time once
212
my $now = time;
213
# create the string to be used for all snapshots, using $now and the template provided
214
my $snapshotName = '@' . strftime($config->{'snapshot'}->{'template'},localtime $now);
215
# Create the dataset regex for later use 
216
$config->{'dataset_regex'} = '(' . join( ')|(', keys %{ $config->{'datasets'} }  ) . ')' unless $config->{'dataset_regex'};
217
#print $config{'dataset_regex'} . "\n";
218
$config->{'snapshot_regex'} = '(' . $config->{'dataset_regex'} . ')@' . $config->{'snapshot'}->{'parse'};
219
#print $config->{'snapshot_regex'} . "\n\n";
220
 
221
#die Dumper( $config ) . "\n";   
222
# first, find all datasets which match our keys
223
my $dataSets = &getListing( $config, $config->{'dataset_regex'}, 'zfs list'  );
224
# and, find all snapshots that match
225
my $snapshots = &getListing( $config, $config->{'snapshot_regex'}, 'zfs list -t snapshot'  );
226
# get the date/time of the snapshots and store them in the hash
227
&parseSnapshots($snapshots, $config );
228
# merge the snapshots into the datasets for convenience
229
&merge( $dataSets, $snapshots, $config );
230
# Now, let's do the actual processing
231
my @commands  = &process( $dataSets, $now, $snapshotName, &period2seconds( $config->{'slop'} ) );
232
my $errors;
233
print $errors if $errors = &run( $config->{'TESTING'}, @commands );
234
 
235
 
236
#print Dumper( $dataSets );
237
#print Dumper( $snapshots );
238
 
239
#print join ("\n", sort keys( %$dataSets ) ) . "\n\n";
240
#print join( "\n", sort keys( %$snapshots ) ) . "\n";
241
 
242
1;