File Coverage

blib/lib/Daemon/Shutdown.pm
Criterion Covered Total %
statement 30 133 22.5
branch 0 38 0.0
condition 0 14 0.0
subroutine 10 21 47.6
pod 4 4 100.0
total 44 210 20.9


line stmt bran cond sub pod time code
1             package Daemon::Shutdown;
2              
3 1     1   24049 use warnings;
  1         3  
  1         32  
4 1     1   6 use strict;
  1         3  
  1         39  
5 1     1   793 use YAML::Any qw/Dump LoadFile/;
  1         1075  
  1         6  
6 1     1   20815 use Log::Log4perl;
  1         61534  
  1         8  
7 1     1   1152 use Params::Validate qw/:all/;
  1         13529  
  1         270  
8 1     1   10 use File::Basename;
  1         4  
  1         65  
9 1     1   1679 use IPC::Run;
  1         55169  
  1         54  
10 1     1   864 use User;
  1         200  
  1         13  
11 1     1   1746 use AnyEvent;
  1         6343  
  1         16  
12 1     1   39 use Try::Tiny;
  1         3  
  1         1989  
13              
14             =head1 NAME
15              
16             Daemon::Shutdown - A Shutdown Daemon
17              
18             =head1 VERSION
19              
20             Version 0.11
21              
22             =cut
23              
24             our $VERSION = '0.11';
25              
26             =head1 SYNOPSIS
27              
28             This is the core of the shutdown daemon script.
29            
30             use Daemon::Shutdown;
31             my $sdd = Daemon::Shutdown->new( %args );
32             $sdd->start();
33              
34             =head1 METHODS
35              
36             =head2 new
37              
38             Create new instance of Daemon::Shutdown
39              
40             =head3 PARAMS
41              
42             =over 2
43              
44             =item * log_file
45              
46             Path to log file
47              
48             Default: /var/log/sdd.log'
49              
50             =item * log_level
51              
52             Logging level (from Log::Log4perl). Valid are: DEBUG, INFO, WARN, ERROR
53              
54             Default: INFO
55              
56             =item * verbose 1|0
57              
58             If enabled, logging info will be printed to screen as well
59              
60             Default: 0
61              
62             =item * test 1|0
63              
64             If enabled shutdown will not actually be executed.
65              
66             Default: 0
67              
68             =item * sleep_before_run
69              
70             Time in seconds to sleep before running the monitors.
71             e.g. to give the system time to boot, and not to shut down before users
72             have started using the freshly started system.
73              
74             Default: 3600
75              
76             =item * exit_after_trigger 1|0
77              
78             If enabled will exit the application after a monitor has triggered.
79             Normally it is a moot point, because if a monitor has triggered, then a shutdown
80             is initialised, so the script will stop running anyway.
81              
82             Default: 0
83              
84             =item * monitor HASHREF
85              
86             A hash of monitor definitions. Each hash key must map to a Monitor module, and
87             contain a hash with the parameters for the module.
88              
89             =item * use_sudo 1|0
90              
91             Use sudo for shutdown
92            
93             sudo shutdown -h now
94              
95             Default: 0
96              
97             =item * shutdown_binary
98              
99             The full path to the shutdown binary
100              
101             Default: /sbin/poweroff
102              
103             =item * shutdown_args
104              
105             Any args to pass to your shutdown_binary
106              
107             Default: none
108              
109             =item * shutdown_after_triggered_monitors
110              
111             The number of monitors which need to be triggered at the same time to cause a
112             shutdown. Can be a number or the word 'all'.
113              
114             Default: 1
115              
116             =item * timeout_for_shutdown
117              
118             Seconds which the system call for shutdown should wait before timing out.
119              
120             Default: 10
121              
122             =back
123              
124             =head3 Example (YAML formatted) configuration file
125            
126             ---
127             log_level: INFO
128             log_file: /var/log/sdd.log
129             shutdown_binary: /sbin/shutdown
130             shutdown_args:
131             - -h
132             - now
133             exit_after_trigger: 0
134             sleep_before_run: 30
135             verbose: 0
136             use_sudo: 0
137             monitor:
138             hdparm:
139             loop_sleep: 60
140             disks:
141             - /dev/sdb
142             - /dev/sdc
143             - /dev/sdd
144              
145             =cut
146              
147             sub new {
148 0     0 1   my $class = shift;
149              
150 0           my %params = @_;
151              
152             # Remove any undefined parameters from the params hash
153 0 0         map { delete( $params{$_} ) if not $params{$_} } keys %params;
  0            
154              
155             # Validate the config file
156             %params = validate_with(
157             params => \%params,
158             spec => {
159             config => {
160             callbacks => {
161 0     0     'File exists' => sub { -f shift }
162             },
163 0           default => '/etc/sdd.conf',
164             },
165             },
166             allow_extra => 1,
167             );
168 0           my $self = {};
169              
170             # Read the config file
171 0 0         if ( not $params{config} ) {
172 0           $params{config} = '/etc/sdd.conf';
173             }
174 0 0         if ( not -f $params{config} ) {
175 0           die( "Config file $params{config} not found\n" );
176             }
177 0           my $file_config = LoadFile( $params{config} );
178 0           delete( $params{config} );
179              
180             # Merge the default, config file, and passed parameters
181 0           %params = ( %$file_config, %params );
182              
183 0           my @validate = map { $_, $params{$_} } keys( %params );
  0            
184             %params = validate_with(
185             params => \%params,
186             spec => {
187             log_file => {
188             default => '/var/log/sdd.log',
189             callbacks => {
190             'Log file is writable' => sub {
191 0     0     my $filepath = shift;
192 0 0         if ( -f $filepath ) {
193 0           return -w $filepath;
194             } else {
195              
196             # Is directory writable
197 0           return -w dirname( $filepath );
198             }
199             },
200             },
201             },
202             log_level => {
203             default => 'INFO',
204             regex => qr/^(DEBUG|INFO|WARN|ERROR)$/,
205             },
206             verbose => {
207             default => 0,
208             regex => qr/^[1|0]$/,
209             },
210             test => {
211             default => 0,
212             regex => qr/^[1|0]$/,
213             },
214             sleep_before_run => {
215             default => 3600,
216             regex => qr/^\d*$/,
217             },
218             exit_after_trigger => {
219             default => 0,
220             regex => qr/^[1|0]$/,
221             },
222             use_sudo => {
223             default => 0,
224             regex => qr/^[1|0]$/,
225             },
226             shutdown_binary => {
227             default => '/sbin/poweroff',
228             type => SCALAR,
229             callbacks => {
230             'Shutdown binary exists' => sub {
231 0     0     -x shift();
232             },
233             },
234             },
235             shutdown_args => {
236             type => ARRAYREF,
237             optional => 1
238             },
239             monitor => { type => HASHREF, },
240             shutdown_after_triggered_monitors => {
241             default => 1,
242             type => SCALAR,
243             regex => qr/^(all|\d+)$/,
244             },
245             timeout_for_shutdown => {
246             default => 10,
247             regex => qr/^\d+$/,
248             }
249             },
250              
251             # A little less verbose than Carp...
252 0     0     on_fail => sub { die( shift() ) },
253 0           );
254              
255 0           $self->{params} = \%params;
256              
257 0           bless $self, $class;
258              
259             # Set up the logging
260 0   0       my $log4perl_conf = sprintf 'log4perl.rootLogger = %s, Logfile', $params{log_level} || 'WARN';
261 0 0         if ( $params{verbose} > 0 ) {
262 0           $log4perl_conf .= q(, Screen
263             log4perl.appender.Screen = Log::Log4perl::Appender::Screen
264             log4perl.appender.Screen.stderr = 0
265             log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
266             log4perl.appender.Screen.layout.ConversionPattern = [%d] %p %m%n
267             );
268              
269             }
270              
271 0           $log4perl_conf .= q(
272             log4perl.appender.Logfile = Log::Log4perl::Appender::File
273             log4perl.appender.Logfile.layout = Log::Log4perl::Layout::PatternLayout
274             log4perl.appender.Logfile.layout.ConversionPattern = [%d] %p %m%n
275             );
276 0           $log4perl_conf .= sprintf "log4perl.appender.Logfile.filename = %s\n", $params{log_file};
277              
278             # ... passed as a reference to init()
279 0           Log::Log4perl::init( \$log4perl_conf );
280 0           my $logger = Log::Log4perl->get_logger();
281 0           $self->{logger} = $logger;
282              
283 0 0         $self->{is_root} = ( User->Login eq 'root' ? 1 : 0 );
284 0           $self->{logger}->info( "You are " . User->Login );
285              
286 0 0         if ( not $self->{is_root} ) {
287 0           $self->{logger}->warn( "You are not root. SDD will probably not work..." );
288             }
289              
290             # Load the monitors
291 0           my %monitors;
292 0           foreach my $monitor_name ( keys( %{ $params{monitor} } ) ) {
  0            
293 0           eval {
294 0           my $monitor_package = 'Daemon::Shutdown::Monitor::' . $monitor_name;
295 0           my $monitor_path = 'Daemon/Shutdown/Monitor/' . $monitor_name . '.pm';
296 0           require $monitor_path;
297              
298 0           $monitors{$monitor_name} = $monitor_package->new( %{ $params{monitor}->{$monitor_name} } );
  0            
299             };
300 0 0         if ( $@ ) {
301 0           die( "Could not initialise monitor: $monitor_name\n$@\n" );
302             }
303             }
304 0           $self->{monitors} = \%monitors;
305 0           $self->{triggered_monitors} = {};
306              
307 0           my $num_monitors = keys %monitors;
308 0 0 0       if ( $self->{params}->{shutdown_after_triggered_monitors} eq 'all'
309             || $self->{params}->{shutdown_after_triggered_monitors} > $num_monitors )
310             {
311 0           $self->{params}->{shutdown_after_triggered_monitors} = $num_monitors;
312             }
313              
314             $logger->debug(
315 0           sprintf "Will shutdown if %d of %d monitors agree",
316             $self->{params}->{shutdown_after_triggered_monitors},
317             $num_monitors
318             );
319 0           $self->{num_triggered_monitors} = 0;
320 0           return $self;
321             }
322              
323             =head2 toggle_trigger
324              
325             Toggle whether a monitor wants to shutdown and, if enough agree, call shutdown
326              
327             =cut
328              
329             sub toggle_trigger {
330 0     0 1   my ( $self, $monitor_name, $toggle ) = @_;
331 0           my $logger = $self->{logger};
332              
333 0 0 0       if ( !defined $toggle || $toggle !~ /^0|1$/ ) {
334 0           $logger->logdie( "Called with invalid value for toggle" );
335             }
336              
337             # set/unset the toggle
338 0 0 0       if ( $toggle and not $self->{triggered_monitors}->{$monitor_name} ) {
    0 0        
339 0           $self->{triggered_monitors}->{$monitor_name} = 1;
340             } elsif ( $self->{triggered_monitors}->{$monitor_name} and not $toggle ) {
341 0           delete( $self->{triggered_monitors}->{$monitor_name} );
342             } else {
343             # seen it before, do care, because maybe last attempt to shutdown failed?
344             }
345              
346             # Store how many are triggered, and shutdown if limit reached
347 0           $self->{num_triggered_monitors} = scalar keys %{ $self->{triggered_monitors} };
  0            
348 0           $logger->debug( $self->{num_triggered_monitors} . " monitors are ready to shutdown" );
349 0 0         if ( $self->{num_triggered_monitors} >= $self->{params}->{shutdown_after_triggered_monitors} ) {
350 0           $self->shutdown();
351             }
352             }
353              
354             =head2 shutdown
355              
356             Shutdown the system, if not in test mode
357              
358             =cut
359              
360             sub shutdown {
361 0     0 1   my $self = shift;
362 0           my $logger = $self->{logger};
363              
364 0           $logger->info( "Shutting down" );
365              
366 0 0         if ( $self->{params}->{test} ) {
367 0           $logger->info( "Not really shutting down because running in test mode" );
368             } else {
369              
370             # Do the actual shutdown
371 0           my @cmd = ( $self->{params}->{shutdown_binary} );
372              
373             # have any args?
374 0 0         if ( $self->{params}->{shutdown_args} ) {
375 0           push @cmd, @{ $self->{params}->{shutdown_args} };
  0            
376             }
377              
378 0 0         if ( $self->{params}->{use_sudo} ) {
379 0           unshift( @cmd, 'sudo' );
380             }
381 0           $logger->debug( "Shutting down with cmd: " . join( ' ', @cmd ) );
382              
383             # Sometimes the shutdown call can timeout (system unresponsive?). In this case, don't
384             # die, and also don't exit_after_trigger - allow the trigger to hit again, and try again.
385             try {
386 0     0     my ( $in, $out, $err );
387 0           IPC::Run::run( \@cmd, \$in, \$out, \$err, IPC::Run::timeout( $self->{params}->{timeout_for_shutdown} ) );
388 0 0         if ( $err ) {
389 0           $logger->error( "Could not shutdown: $err" );
390             }
391 0 0         if ( $self->{params}->{exit_after_trigger} ) {
392 0           exit;
393             }
394             }
395             catch {
396 0     0     $logger->error( "Shutdown command failed '" . join( ' ', @cmd ) . "': $_" );
397 0           };
398             }
399             }
400              
401             =head2 start
402              
403             Start the shutdown daemon
404              
405             =cut
406              
407             sub start {
408 0     0 1   my $self = shift;
409 0           my $logger = $self->{logger};
410              
411 0           $logger->info( "Started" );
412              
413 0           $logger->info( "Sleeping $self->{params}->{sleep_before_run} seconds before starting monitoring" );
414              
415 0           sleep( $self->{params}->{sleep_before_run} );
416              
417             # set up timers then wait forever
418 0           foreach my $monitor_name ( keys %{ $self->{monitors} } ) {
  0            
419 0           my $monitor = $self->{monitors}->{$monitor_name};
420              
421 0           $logger->debug( "Setting timer for monitor $monitor_name: $monitor->{params}->{loop_sleep} seconds" );
422             $monitor->{timer} = AnyEvent->timer(
423             after => 0,
424             interval => $monitor->{params}->{loop_sleep},
425             cb => sub {
426 0 0   0     if ( $monitor->run() ) {
427 0           $self->toggle_trigger( $monitor_name, 1 );
428             } else {
429 0           $self->toggle_trigger( $monitor_name, 0 );
430             }
431             }
432 0           );
433             }
434 0           $logger->debug( 'Entering main listen loop using ' . $AnyEvent::MODEL );
435 0           AnyEvent::CondVar->recv;
436              
437             }
438              
439             =head1 AUTHOR
440              
441             Robin Clarke, C<< >>
442              
443             =head1 BUGS
444              
445             Please report any bugs or feature requests to L
446              
447             =head1 SUPPORT
448              
449             You can find documentation for this module with the perldoc command.
450              
451             perldoc Daemon::Shutdown
452              
453              
454             You can also look for information at:
455              
456             =over 4
457              
458             =item * Github
459              
460             L
461              
462             =item * Search CPAN
463              
464             L
465              
466             =back
467              
468              
469             =head1 ACKNOWLEDGEMENTS
470              
471              
472             =head1 LICENSE AND COPYRIGHT
473              
474             Copyright 2011 Robin Clarke.
475              
476             This program is free software; you can redistribute it and/or modify it
477             under the terms of either: the GNU General Public License as published
478             by the Free Software Foundation; or the Artistic License.
479              
480             See http://dev.perl.org/licenses/ for more information.
481              
482              
483             =cut
484              
485             1; # End of Daemon::Shutdown