Subversion Repositories sysadmin_scripts

Rev

Rev 102 | Rev 107 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 102 Rev 103
Line 19... Line 19...
19
#    GNU General Public License for more details.
19
#    GNU General Public License for more details.
20
#
20
#
21
#    You should have received a copy of the GNU General Public License
21
#    You should have received a copy of the GNU General Public License
22
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
#
23
#
24
# Warning, this script requires YAML::Tiny and Hash::Merge Perl Modules to be installed
24
# Warning, this script requires non-standard Perl modules YAML::Tiny and Hash::Merge
25
# Under Debian:  apt install libyaml-tiny-perl libhash-merge-simple-perl
25
# Under Debian:  apt install libyaml-tiny-perl libhash-merge-simple-perl
26
# Under FreeBSD: cpan -i Hash::Merge::Simple YAML::Tiny
26
# Under FreeBSD: cpan -i Hash::Merge::Simple YAML::Tiny
27
 
27
 
28
 
28
 
29
use strict;
29
use strict;
Line 95... Line 95...
95
      $snapShots->{$snapShot}->{'date'} = \%temp;
95
      $snapShots->{$snapShot}->{'date'} = \%temp;
96
   }
96
   }
97
}
97
}
98
      
98
      
99
# run $command, then parse its output and return the results as a hashref
99
# run $command, then parse its output and return the results as a hashref
-
 
100
# $command is one of zfs list or zfs list -t snapshot
-
 
101
# In other words, get all datasets/volumes or get all snapshots
100
sub getListing {
102
sub getListing {
101
   my ($configuration, $regex, $command )  = @_;
103
   my ($configuration, $regex, $command )  = @_;
102
   my %dataSets;
104
   my %dataSets;
103
 
105
 
104
   # get all datasets
106
   # get all datasets/volumes or snapshots
105
   my @zfsList = `$command`;
107
   my @zfsList = `$command`;
106
   foreach my $thisSet ( @zfsList ) {
108
   foreach my $thisSet ( @zfsList ) {
-
 
109
      # parse the line into its portions. The only one we use right now is name
107
      my $temp = &parseListing( $thisSet, $configuration->{'listingKeys'} );
110
      my $temp = &parseListing( $thisSet, $configuration->{'listingKeys'} );
108
      if (  $temp->{'name'} =~ m/^($regex)$/ ) {
111
      if (  $temp->{'name'} =~ m/^($regex)$/ ) { # it matches the regex we're using, so save it
109
         $dataSets{$temp->{'name'}} = $temp;
112
         $dataSets{$temp->{'name'}} = $temp;
110
      }
113
      }
111
   }
114
   }
112
   return \%dataSets;
115
   return \%dataSets; # return all entries we are looking for
113
}
116
}
114
 
117
 
115
# will convert something like 1 day to the number of seconds (86400) for math.
118
# will convert something like 1 day to the number of seconds (86400) for math.
116
# month and year are approximations (30.5 day = a month, 365.2425 days is a year)
119
# month and year are approximations (30.5 day = a month, 365.2425 days is a year)
117
# For month and year, use the int function to convert back to integer
120
# For month and year, use the int function to convert back to integer
Line 133... Line 136...
133
   }
136
   }
134
   return $count;
137
   return $count;
135
}
138
}
136
 
139
 
137
# Merges datasets, snapshots and some stuff from the configuration into the datasets
140
# Merges datasets, snapshots and some stuff from the configuration into the datasets
138
# hash
-
 
-
 
141
# hash. After this, $config and $snapshots should no longer be necessary
139
sub mergeData {
142
sub mergeData {
140
   my ($datasets,$snapshots,$config) = @_;
143
   my ($datasets,$snapshots,$config) = @_;
141
   my $confKeys = $config->{'datasets'};
144
   my $confKeys = $config->{'datasets'};
142
   foreach my $thisDataset ( keys %$datasets ) {
145
   foreach my $thisDataset ( keys %$datasets ) {
-
 
146
      # go through each configuration entry and see if we match the current dataset
143
      foreach my $conf (keys %$confKeys ) {
147
      foreach my $conf (keys %$confKeys ) {
144
         if ( $thisDataset =~ m/^$conf$/ ) {
148
         if ( $thisDataset =~ m/^$conf$/ ) { # found it, so store the configuration values into the dataset
145
            $datasets->{$thisDataset}->{'recursive'} = $confKeys->{$conf}->{'recursive'};
149
            $datasets->{$thisDataset}->{'recursive'} = $confKeys->{$conf}->{'recursive'};
146
            $datasets->{$thisDataset}->{'frequency'} = &period2seconds( $confKeys->{$conf}->{'frequency'} );
150
            $datasets->{$thisDataset}->{'frequency'} = &period2seconds( $confKeys->{$conf}->{'frequency'} );
147
            $datasets->{$thisDataset}->{'retention'} = &period2seconds( $confKeys->{$conf}->{'retention'} );
151
            $datasets->{$thisDataset}->{'retention'} = &period2seconds( $confKeys->{$conf}->{'retention'} );
148
            last;
152
            last; # there is only one, so no need to process any more for this configuration key
149
         } # if
153
         } # if
150
      } # foreach
154
      } # foreach
-
 
155
      # do the same for the snapshots we found; bind them to the data set
151
      foreach my $snapshot ( keys %$snapshots ) {
156
      foreach my $snapshot ( keys %$snapshots ) {
152
         if ( $snapshot =~ m/^$thisDataset@/ ) { # this is a match
157
         if ( $snapshot =~ m/^$thisDataset@/ ) { # this is a match
153
            # copy the snapshot into the dataset
158
            # copy the snapshot into the dataset
154
            $datasets->{$thisDataset}->{'snapshots'}->{$snapshot} = $snapshots->{$snapshot};
159
            $datasets->{$thisDataset}->{'snapshots'}->{$snapshot} = $snapshots->{$snapshot};
155
            # track the latest snapshot
160
            # track the latest snapshot (we use this to decide whether it is time to add a new one)
156
            $datasets->{$thisDataset}->{'lastSnap'} = $snapshots->{$snapshot}->{'date'}->{'unix'}
161
            $datasets->{$thisDataset}->{'lastSnap'} = $snapshots->{$snapshot}->{'date'}->{'unix'}
157
               if ! defined( $datasets->{$thisDataset}->{'lastSnap'} ) || $datasets->{$thisDataset}->{'lastSnap'} < $snapshots->{$snapshot}->{'date'}->{'unix'};
162
               if ! defined( $datasets->{$thisDataset}->{'lastSnap'} ) || $datasets->{$thisDataset}->{'lastSnap'} < $snapshots->{$snapshot}->{'date'}->{'unix'};
158
            # delete the snapshot
163
            # delete the snapshot, to free up memory
159
            delete $snapshots->{$snapshot};
164
            delete $snapshots->{$snapshot};
160
         } # if
165
         } # if
161
      } # foreach
166
      } # foreach
162
   } # foreach
167
   } # foreach
163
} # sub mergeData
168
} # sub mergeData
164
   
169
   
-
 
170
 
-
 
171
# check to see if a particular snapshot is ready to be destroyed, ie right now is greater than the retention period
-
 
172
# if $recurive is true, add the '-r' to the command to do a recursive destroy
165
sub checkRetention {
173
sub checkRetention {
166
   my ( $retentionPeriod, $recursive, $snapshots, $now ) = @_;
174
   my ( $retentionPeriod, $recursive, $snapshots, $now ) = @_;
167
   my @toDelete;
175
   my @toDelete; # an array of destroy commands
168
   foreach my $thisSnapshot ( keys %$snapshots ) {
176
   foreach my $thisSnapshot ( keys %$snapshots ) {
169
      # print "checking $thisSnapshot\n\tNow: $now\n\tDate: $snapshots->{$thisSnapshot}->{date}->{unix}\n\tRetention: $retentionPeriod\n\n";
177
      # print "checking $thisSnapshot\n\tNow: $now\n\tDate: $snapshots->{$thisSnapshot}->{date}->{unix}\n\tRetention: $retentionPeriod\n\n";
170
      if ( $now - $snapshots->{$thisSnapshot}->{'date'}->{'unix'} > $retentionPeriod ) {
178
      if ( $now - $snapshots->{$thisSnapshot}->{'date'}->{'unix'} > $retentionPeriod ) { # it is too old
171
         my $command = 'zfs destroy ' . ($recursive ? '-r ' : '') . $thisSnapshot;
179
         push ( @toDelete, ( 'zfs destroy ' . ($recursive ? '-r ' : '') . $thisSnapshot ) ); # list it to be destroyed
172
         push @toDelete, $command;
-
 
173
      }
180
      }
174
   }
181
   }
175
   return @toDelete;
182
   return @toDelete; # just return the list of destroy commands to be executed
176
}   
183
}   
177
 
184
 
-
 
185
 
-
 
186
# just return the command to create a new snapshot. Very simple, but I wanted the code to be isolated in case something needed
-
 
187
# to change. Basically, zfs snapshot [-r] datasetname@template
178
sub makeSnapshot {
188
sub makeSnapshot {
179
   my ( $datasetName, $recursive, $snapshotName ) = @_;
189
   my ( $datasetName, $recursive, $snapshotName ) = @_;
180
   return 
190
   return 
181
      'zfs snapshot ' . 
191
      'zfs snapshot ' . 
182
      ($recursive ? '-r ' : '') . 
192
      ($recursive ? '-r ' : '') . 
183
      $datasetName . $snapshotName;
193
      $datasetName . $snapshotName;
184
}
194
}
185
 
195
 
186
 
-
 
-
 
196
# this is the biggie; everything leads to here. We will take every dataset/volume we found, and decide whether some old snapshots
-
 
197
# need to be destroyed, and whether a new snapshot needs to be created.
187
sub process {
198
sub process {
188
   my ( $datasets, $now, $snapshotName, $slop ) = @_;
199
   my ( $datasets, $now, $snapshotName, $slop ) = @_;
189
   my @actions;
-
 
190
   my @toDelete;
200
   my @toDelete; # will hold all the destroy commands
191
   my @toAdd;
201
   my @toAdd; # will hold all the create commands
192
   
202
   
193
   foreach my $thisDataset ( keys %$datasets ) {
203
   foreach my $thisDataset ( keys %$datasets ) { # Look at each dataset/volume in turn
-
 
204
      # if any snapshots need to be destroyed, add them to @toDelete
194
      push( @toDelete, 
205
      push( @toDelete, 
195
         &checkRetention( 
206
         &checkRetention( 
196
         $datasets->{$thisDataset}->{'retention'}, 
207
         $datasets->{$thisDataset}->{'retention'}, 
197
         $datasets->{$thisDataset}->{'recursive'}, 
208
         $datasets->{$thisDataset}->{'recursive'}, 
198
         $datasets->{$thisDataset}->{'snapshots'}, 
209
         $datasets->{$thisDataset}->{'snapshots'}, 
199
         $now )
210
         $now )
200
         );
211
         );
-
 
212
      # if it is time to add a new snapshot, add it to @toAdd
201
      if ( $datasets->{$thisDataset}->{'lastSnap'} + $datasets->{$thisDataset}->{'frequency'} - $slop < $now ) {
213
      if ( $datasets->{$thisDataset}->{'lastSnap'} + $datasets->{$thisDataset}->{'frequency'} - $slop < $now ) {
202
         push @toAdd, &makeSnapshot( $thisDataset, $datasets->{$thisDataset}->{'recursive'}, $snapshotName )
214
         push @toAdd, &makeSnapshot( $thisDataset, $datasets->{$thisDataset}->{'recursive'}, $snapshotName )
203
      }
215
      }
204
   }
216
   }
-
 
217
   # return the actions, deletions first, adds second (executed in that order)
205
   return ( @toDelete, @toAdd );
218
   return ( @toDelete, @toAdd );
206
}   
219
}   
207
 
220
 
-
 
221
# Run 0 or more commands
-
 
222
# the first thing on the stack is a flag for testing
-
 
223
# everything after that is an ordered list of commands to be executed.
-
 
224
# If any command fails, all subsequent commands abort
208
sub run {
225
sub run {
209
   my $testing = shift;
226
   my $testing = shift;
210
   # bail if there are no commands to run
227
   return 0 unless @_; # bail if there are no commands to run
211
   return 0 unless @_;
-
 
212
   if ( $testing ) { # don't do it, just dump the commands
228
   if ( $testing ) { # don't do it, just dump the commands to /tmp/snapShot
213
      open LOG, ">/tmp/snapShot" or die "could not write to /tmp/snapShot: $!\n";
229
      open LOG, ">/tmp/snapShot" or die "could not write to /tmp/snapShot: $!\n";
214
      print LOG join( "\n", @_ ) . "\n";
230
      print LOG join( "\n", @_ ) . "\n";
215
      close LOG;
231
      close LOG;
216
   } else {
232
   } else { # actually execute the commands
217
      my $out;
233
      my $out; # capture all output
218
      return 'Not running right now';
234
      return 'Not running right now'; # temp while testing
219
      while ( my $command = shift ) {
235
      while ( my $command = shift ) { # for each command on the stack
220
         $out .= `$command` . "\n";
236
         $out .= `$command` . "\n"; # add it to $out
221
         if ( $? ) { # we had an error
237
         if ( $? ) { # we had an error, add debugging text, the end program
222
            $out .= "Error executing command\n\t$command\n\t";
238
            $out .= "Error executing command\n\t$command\n\t";
223
            if ($? == -1) {
239
            if ($? == -1) {
224
                $out .= "failed to execute $command: $!";
240
                $out .= "failed to execute $command: $!";
225
            } elsif ($? & 127) {
241
            } elsif ($? & 127) {
226
                $out .= sprintf( "child died with signal %d, %s coredump", ($? & 127),  ($? & 128) ? 'with' : 'without' );
242
                $out .= sprintf( "child died with signal %d, %s coredump", ($? & 127),  ($? & 128) ? 'with' : 'without' );
Line 230... Line 246...
230
            $out .= "\n";
246
            $out .= "\n";
231
            return $out;
247
            return $out;
232
         }
248
         }
233
      }
249
      }
234
   }
250
   }
235
   return 0;
251
   return 0; # we succeeded
236
}
252
}
237
 
253
 
238
&readConfig() or die "Could not read config file: $!\n";
254
&readConfig() or die "Could not read config file: $!\n";
239
 
255
 
-
 
256
# we're pre-calculating some things so we don't do it over and over for each entry
240
# grab the time once
257
# grab the time once
241
my $now = time;
258
my $now = time;
242
# create the string to be used for all snapshots, using $now and the template provided
259
# create the string to be used for all snapshots, using $now and the template provided
243
my $snapshotName = '@' . strftime($config->{'snapshot'}->{'template'},localtime $now);
260
my $snapshotName = '@' . strftime($config->{'snapshot'}->{'template'},localtime $now);
244
# Create the dataset regex for later use 
261
# Create the dataset regex by joing all of the regexes defined.
245
$config->{'dataset_regex'} = '(' . join( ')|(', keys %{ $config->{'datasets'} }  ) . ')' unless $config->{'dataset_regex'};
262
$config->{'dataset_regex'} = '(' . join( ')|(', keys %{ $config->{'datasets'} }  ) . ')' unless $config->{'dataset_regex'};
246
#print $config{'dataset_regex'} . "\n";
263
#print $config{'dataset_regex'} . "\n";
247
$config->{'snapshot_regex'} = '(' . $config->{'dataset_regex'} . ')@' . $config->{'snapshot'}->{'parse'};
264
$config->{'snapshot_regex'} = '(' . $config->{'dataset_regex'} . ')@' . $config->{'snapshot'}->{'parse'};
248
#print $config->{'snapshot_regex'} . "\n\n";
265
#print $config->{'snapshot_regex'} . "\n\n";
249
   
266
   
Line 259... Line 276...
259
# Now, let's do the actual processing
276
# Now, let's do the actual processing
260
my @commands  = &process( $dataSets, $now, $snapshotName, &period2seconds( $config->{'slop'} ) );
277
my @commands  = &process( $dataSets, $now, $snapshotName, &period2seconds( $config->{'slop'} ) );
261
#print join ( "\n", @commands ) . "\n";
278
#print join ( "\n", @commands ) . "\n";
262
my $errors;
279
my $errors;
263
print "Error: $errors\n" if $errors = &run( $config->{'TESTING'}, @commands );
280
print "Error: $errors\n" if $errors = &run( $config->{'TESTING'}, @commands );
264
   
-
 
265
 
281
 
266
# print Dumper( $dataSets );
282
# print Dumper( $dataSets );
267
#print Dumper( $snapshots );
283
#print Dumper( $snapshots );
268
 
284
 
269
#print join ("\n", sort keys( %$dataSets ) ) . "\n\n";
285
#print join ("\n", sort keys( %$dataSets ) ) . "\n\n";