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