Big update of Clearadm
[clearscm.git] / clearadm / cleartasks.pl
1 #!/usr/bin/env perl
2
3 =pod
4
5 =head1 NAME $RCSfile: cleartasks.pl,v $
6
7 Scrub Clearadm records
8
9 =head1 VERSION
10
11 =over
12
13 =item Author
14
15 Andrew DeFaria <Andrew@ClearSCM.com>
16
17 =item Revision
18
19 $Revision: 1.25 $
20
21 =item Created:
22
23 Sun Jan  2 19:40:28 EST 2011
24
25 =item Modified:
26
27 $Date: 2013/06/02 18:47:26 $
28
29 =back
30
31 =head1 SYNOPSIS
32
33  Usage cleartasks.pl: [-u|sage] [-ve|rbose] [-deb|ug]
34
35  Where:
36    -u|sage:     Displays usage
37  
38    -v|erbose:   Be verbose
39    -de|bug:     Output debug messages
40    
41    -da|emon:    Run in daemon mode (Default: yes)
42    -p|idfile:   File to be created with the pid written to it (Default:
43                 cleartasks.pid). Note: pidfile is only written if -daemon is
44                 specified.
45                        
46 =head1 DESCRIPTION
47
48 Examine the Clearadm schedule and perform the tasks required.
49
50 Note that sending the Cleartasks.pl process a sigusr1 will cause it to toggle
51 verbose mode.
52
53 =cut
54
55 use strict;
56 use warnings;
57
58 use FindBin;
59 use Getopt::Long;
60 use Sys::Hostname;
61
62 use lib "$FindBin::Bin/lib", "$FindBin::Bin/../lib";
63
64 use Clearadm;
65 use Clearexec;
66 use DateUtils;
67 use Display;
68 use TimeUtils;
69 use Utils;
70
71 my $VERSION  = '$Revision: 1.25 $';
72   ($VERSION) = ($VERSION =~ /\$Revision: (.*) /);
73
74 my $logfile =  "$Clearadm::CLEAROPTS{CLEARADM_LOGDIR}/$FindBin::Script";
75    $logfile =~ s/\.pl$//;
76    $logfile .= '.' . hostname() . '.log';
77
78 my $pidfile = "$Clearadm::CLEAROPTS{CLEARADM_RUNDIR}/$FindBin::Script.pid";
79 my $daemon  = 1;
80
81 # Augment PATH with $Clearadm::CLEAROPTS{CLEARADM_BASE}
82 $ENV{PATH} .= ":$Clearadm::CLEAROPTS{CLEARADM_BASE}";
83
84 my ($clearadm, $clearexec);
85
86 sub ToggleVerbose() {
87   if (get_verbose) {
88     display 'Turning verbose off';
89     set_verbose 0;
90   } else {
91     display 'Turning verbose on';
92     set_verbose 1;
93   } # if
94 } # ToggleVerbose
95
96 $SIG{USR1} = \&ToggleVerbose;
97
98 sub HandleSystemNotCheckingIn (%) {
99   my (%system) = @_;
100    
101   my $startTime = time;
102   
103   my $message = "Unable to connect to system $system{name}:$system{port}";
104
105   my %runlog = (
106     task     => 'System checkin',
107     started  => Today2SQLDatetime,
108     status   => 1,
109     message  => $message,
110     system   => $system{name},
111   );
112
113   my ($err, $msg, $lastid) = $clearadm->AddRunlog (%runlog);
114   
115   $clearadm->Error ("Unable to add to runlog (Status: $err)\n$msg") if $err;
116    
117   # Check to see if we should notify anybody about this non-responding system
118   my %notification = $clearadm->GetNotification ('System checkin'); 
119           
120   my $when            = Today2SQLDatetime;
121   my $nomorethan      = lc $notification{nomorethan};
122   my $systemLink      = $Clearadm::CLEAROPTS{CLEARADM_WEBBASE};
123      $systemLink     .= "/systemdetails.cgi?system=$system{name}";
124   my $runlogLink      = $Clearadm::CLEAROPTS{CLEARADM_WEBBASE};
125      $runlogLink     .= "/runlog.cgi?id=$lastid";
126    my $subject         = "System is not responding (Is clearagent running?)";
127      $message = <<"END";      
128 <center>
129 <h1><font color="red">Alert</font> System not responding!</h1>
130 </center>
131
132 <p>On $when the system <a href="$systemLink">$system{name}</a> was <a 
133 href="$runlogLink">not responding</a> to clearagent requests. This can happen if
134 clearagent is not setup and running on the system.</p> 
135 END
136      
137   $clearadm->Notify (
138     $notification{name},
139     $subject,
140     $message,
141     'System Checkin',
142     $system{name},
143     undef,
144     $lastid,
145   );
146               
147   verbose "$system{name}: $subject";
148   
149   return;
150 } # HandleSystemNotCheckingIn
151
152 sub SystemsCheckin () {
153   for ($clearadm->FindSystem) {
154     my %system = %$_;
155     
156     next if $system{active} eq 'false';
157     
158     verbose "Contacting system $system{name}:$system{port}";
159     
160     my $startTime = time;
161     
162     my $status = $clearexec->connectToServer (
163       $system{name},
164       $system{port}
165     );
166     
167     unless ($status) {
168       HandleSystemNotCheckingIn %system;
169       next;
170     } # unless
171     
172     $clearexec->disconnectFromServer;
173     
174     verbose 'Successfully checked in with system: '
175           . "$system{name}:$system{port}";
176     
177     display __FILE__ . " DEBUG: System undefined 1" unless $system{name};
178     $clearadm->UpdateSystem (
179       $system{name},
180       (lastheardfrom => Today2SQLDatetime)
181     );
182   
183     $clearadm->ClearNotifications ($system{name})
184       if $system{notification} and $system{notification} eq 'Heartbeat';
185   } # for
186   
187   return;
188 } # SystemsCheckin
189
190 sub UpdateRunlog ($$$$) {
191   my ($status, $startTime, $task, $output) = @_;
192   
193   my %runlog = (
194     task    => $$task{name},
195     system  => $$task{system},
196     started => Today2SQLDatetime,
197   );
198
199   $runlog{status} = $status;
200     
201   if ($status == 0) {
202     if (@$output) {
203       $runlog{message} = join "\n", @$output;
204     } else {
205       $runlog{message}  = 'Successful execution of ';
206       $runlog{message} .= "$$task{name}: $$task{command}";
207     } # if
208   } else {
209     if (@$output) {
210       $runlog{message} = join "\n", @$output;
211     } else {
212       $runlog{message}  = 'Unable to execute ';
213       $runlog{message} .= "$$task{name}: $$task{command} ";
214       $runlog{message} .= join (' ', @$output);
215     } # if
216   } # if
217     
218   my ($err, $msg, $lastid) = $clearadm->AddRunlog (%runlog);
219     
220   $clearadm->Error ($msg, $err) if $err;
221
222   return $lastid;
223 } # UpdateRunlog
224
225 sub MakeSystemLink ($) {
226   my ($system) = @_;
227   
228   return "$Clearadm::CLEAROPTS{CLEARADM_WEBBASE}/systemdetails.cgi?system="
229        . $system;
230 } # MakeSystemLink
231
232 sub MakeLoadavgLink ($) {
233   my ($system) = @_;
234
235   return "$Clearadm::CLEAROPTS{CLEARADM_WEBBASE}/plot.cgi?type=loadavg&system="
236        . "$system&scaling=Hour&points=24";
237 } # MakeLoadavgLink
238
239 sub ProcessLoadavgErrors ($$$$@) {
240   # TODO: Also need to handle the case where the error was something other
241   # than "Load average over threshold". Perhaps by having different return
242   # status. Also, runlog entry #22169 never reported!
243   my ($notification, $task, $system, $lastid, @output) = @_;
244   
245   my $when = Today2SQLDatetime;
246   
247   for (@output) {
248     # We need to log this output. Write it to STDOUT
249     display $_;
250
251     my ($subject, $message, $currLoadavg, $threshold, $systemLink, $loadavgLink);
252
253     if (/System: (\w+) Loadavg (\d+\.\d+) Threshold (\d+\.\d+)/) {
254       $system       = $1;
255       $currLoadavg  = $2;
256       $threshold    = $3;
257       $systemLink   = MakeSystemLink $system;
258       $loadavgLink  = MakeLoadavgLink $system;
259       $subject      = "Load average of $currLoadavg exceeds threshold ";
260       $subject     .= "($threshold)";
261       $message      = <<"END";      
262 <center>
263 <h1><font color="red">Alert</font> Load Average is over the threshold!</h1>
264 </center>
265
266 <p>On $when the system <a href="$systemLink">$system</a>'s load avg
267 (<a href="$loadavgLink">$currLoadavg</a>) had exceeded the threshold set for
268 this system ($threshold).</p> 
269 END
270     } elsif (/ERROR.*system\s+(\S+):/) {
271       $system     = $1;
272       $systemLink = MakeSystemLink $system;
273       $subject    = "Error trying to obtain Loadavg";
274       $message    = <<"END";
275 <center>
276 <h1><font color="red">Alert</font> Unable to obtain Loadavg!</h1>
277 </center>
278
279 <p>On $when we were unable to obtain the Loadavg for
280 system <a href="$systemLink">$system</a>.</p>
281
282 <p>The following was the error message:</p>
283 <pre>$_</pre>
284 END
285     } else {
286       $message = <<"END";
287 <p>On $when on the system $system, we were unable to parse the Loadavg output. This is what we saw:</p>
288
289 <pre>
290 END
291       $message .= join "\n", @output;
292       $message .= "</pre>";
293       $clearadm->Error ($message, -1);
294       
295       last;
296     } # if
297
298     $clearadm->Notify (
299       $notification,
300       $subject,
301       $message,
302       $task,
303       $system,
304       undef,
305       $lastid,
306     );
307   } # for
308   
309   return;
310 } # ProcessLoadAvgErrors
311
312 sub ProcessFilesystemErrors ($$$$@) {
313   # TODO: Also need to handle the case where the error was something other
314   # than "Filesystem over threshold". Perhaps by having different return
315   # status.
316   my ($notification, $task, $system, $lastid, @output) = @_;
317
318   my $when = Today2SQLDatetime;
319
320   my %system;
321   
322   for (@output) {
323     # We need to log this output. Write it to STDOUT
324     display $_;
325     
326     if (/System:\s*(\S+)\s*Filesystem:\s*(\S+)\s*Used:\s*(\d+\.\d+)%\s*Threshold:\s*(\d+)/) {
327       my %fsinfo = (
328         filesystem => $2,
329         usedPct    => $3,
330         threshold  => $4
331       );
332       
333       if ($system{$1}) {
334          $system{$1} = [$system{$1}, \%fsinfo];
335       } else {
336         $system{$1} = \%fsinfo;
337       } # if
338     } # if
339   } # for
340    
341   for my $systemName (keys %system) {
342     my @fsinfo;
343     
344     if (ref $system{$systemName} eq 'HASH') {
345        push @fsinfo, $system{$systemName};
346     } else {
347        push @fsinfo, @{$system{$systemName}};
348     } # if
349
350     my $systemLink = MakeSystemLink ($systemName);
351     my $subject    = 'Filesystem has exceeded threshold';
352     my $message = <<"END";      
353 <center>
354 <h1><font color="red">Alert</font> Filesystem is over the threshold!</h1>
355 </center>
356
357 <p>On $when the following filesystems on <a href="$systemLink">$systemName</a>
358 were over their threshold.</p>
359
360 <ul>
361 END
362     for (@fsinfo) {
363       my %fsinfo = %{$_};
364       my $filesystemLink  = $Clearadm::CLEAROPTS{CLEARADM_WEBBASE};
365          $filesystemLink .= "/plot.cgi?type=filesystem&system=$systemName";
366          $filesystemLink .= "&filesystem=$fsinfo{filesystem}";
367          $filesystemLink .= '&scaling=Day&points=7';
368       $message .= "<li>Filesystem <a href=\"$filesystemLink\">";
369       $message .= "$fsinfo{filesystem}</a> is $fsinfo{usedPct}% full. Threshold is ";
370       $message .= "$fsinfo{threshold}%</li>";
371     } # for
372       
373     $message .= "</ul>";
374     
375     $clearadm->Notify (
376       $notification,
377       $subject,
378       $message,
379       $task,
380       $systemName,
381       undef,
382       $lastid,
383     );
384   } # for
385   
386   return;
387 } # ProcessFilesystemErrors
388
389 sub NonZeroReturn ($$$$$$) {
390   my ($system, $notification, $status, $lastid, $output, $task) = @_;
391
392   my @output = @{$output};
393   my %task   = %{$task};
394   
395   my $when = Today2SQLDatetime;
396     
397   my $subject      = "Non zero return from $task{command} "
398                    . "executing on $system";
399   my $taskLink     = $Clearadm::CLEAROPTS{CLEARADM_WEBBASE};
400      $taskLink    .= "/tasks.cgi?task=$task{name}";
401   my $similarLink  = $Clearadm::CLEAROPTS{CLEARADM_WEBBASE};
402      $similarLink .= "/runlog.cgi?system=$task{system}"
403                   . "&status=$status&"
404                   . "&task=$task{name}";
405   my $runlogLink   = $Clearadm::CLEAROPTS{CLEARADM_WEBBASE};
406      $runlogLink  .= "/runlog.cgi?id=$lastid";
407   my $message      = <<"END";
408 <center>
409 <h1><font color="red">Alert</font> Non zero status from script execution!</h1>
410 </center>
411
412 <p>On $when, while executing <a href="$taskLink">$task{name}</a> on
413 $task{system}, a non zero status of $status was returned. Here is the resulting
414 output:</p><blockquote><pre>
415 END
416
417   $message .= join "\n", @output;
418   $message .= <<"END";
419 </pre></blockquote>
420 <p>You may wish to examine the individual <a href="$runlogLink">runlog entry</a>
421 that caused this alert or a list of <a href="$similarLink">similar 
422 failures</a>.</p>
423 END
424
425   $message .= "</pre></blockquote>";
426   
427   $clearadm->Notify (
428     $notification,
429     $subject,
430     $message,
431     $task,
432     $system,
433     undef,
434     $lastid,
435   );
436   
437   return;   
438 } # NonZeroReturn
439
440 sub ExecuteTask ($%) {
441   my ($sleep, %task) = @_;
442   
443   my ($status, @output, %system, $subject, $message);
444
445   verbose_nolf "Performing task $task{name}";
446   
447   my %notification = $clearadm->GetNotification ($task{notification});
448        
449   my $startTime = time;
450   
451   if ($task{system} =~ /localhost/i) {
452     verbose " on localhost";
453     ($status, @output) = Execute "$task{command} 2>&1";
454   } else {
455     %system = $clearadm->GetSystem ($task{system});
456     
457     verbose " on $system{name}";
458
459     $status = $clearexec->connectToServer (
460       $system{name},
461       $system{port}
462     );
463     
464     unless ($status) {
465       $output[0] = "Unable to connect to system $system{name}:$system{port} to "
466                  . "execute $task{command}";
467       $status = -1;
468     } else {
469       ($status, @output) = $clearexec->execute ($task{command});
470       
471       $output[0] = "Unable to exec $task{command} on $system{name}"
472         if $status == -1;
473     } # unless
474     
475     $clearexec->disconnectFromServer;    
476   } # if
477
478   my $lastid = UpdateRunlog ($status, $startTime, \%task, \@output);
479     
480   if ($status != 0) {
481     if ($notification{cond}
482       and $notification{cond} =~ /non zero return/i) {
483       NonZeroReturn (
484         $system{name},
485         $notification{name},
486         $status,
487         $lastid,
488         \@output,
489         \%task
490       );
491     } elsif ($notification{cond} =~ /loadavg over threshold/i) {
492       ProcessLoadavgErrors ($notification{name}, $task{name}, $system{name}, $lastid, @output);
493     } elsif ($notification{cond} =~ /filesystem over threshold/i) {
494       ProcessFilesystemErrors ($notification{name}, $task{name}, $system{name}, $lastid, @output);
495     } # if
496   } else {
497     $clearadm->ClearNotifications ($task{system});
498   } # if
499         
500   my ($err, $msg) = $clearadm->UpdateSchedule (
501     $task{schedulename},
502     ( 'lastrunid' => $lastid ),
503   );
504     
505   $clearadm->Error ($msg, $err) if $err;  
506   
507   $sleep -= time - $startTime;
508   
509   return $sleep;
510 } # ExecuteTask
511
512 # Main
513 GetOptions (
514   'usage'     => sub { Usage },
515   'verbose'   => sub { set_verbose },
516   'debug'     => sub { set_debug },
517   'daemon!'   => \$daemon,
518   'pidfile=s' => \$pidfile,
519 ) or Usage "Invalid parameter";
520
521 Usage 'Extraneous options: ' . join ' ', @ARGV
522   if @ARGV;
523
524 EnterDaemonMode $logfile, $logfile, $pidfile
525   if $daemon;
526
527 display "$FindBin::Script V$VERSION started at " . localtime;
528
529 $clearadm  = Clearadm->new;
530 $clearexec = Clearexec->new;
531
532 $clearadm->SetNotify;
533
534 while () {
535   # First check in with all systems
536   SystemsCheckin;
537   
538   my ($sleep, @workItems) = $clearadm->GetWork;
539   
540   for (@workItems) {
541     my %scheduledTask = %{$_};
542     
543     $scheduledTask{system} ||= 'All systems';
544     
545     if ($scheduledTask{system} =~ /all systems/i) {
546       for my $system ($clearadm->FindSystem) {
547         next if $$system{active} eq 'false';
548
549         $scheduledTask{system} = $$system{name};
550         $sleep = ExecuteTask $sleep, %scheduledTask;
551       } # for
552     } else {
553       $sleep = ExecuteTask $sleep, %scheduledTask;
554     } # if
555   } # for  
556   
557   if ($sleep > 0) {
558     verbose "Sleeping for $sleep seconds";
559     sleep $sleep;
560   } # if  
561 } # for
562
563 =pod
564
565 =head1 CONFIGURATION AND ENVIRONMENT
566
567 DEBUG: If set then $debug is set to this level.
568
569 VERBOSE: If set then $verbose is set to this level.
570
571 TRACE: If set then $trace is set to this level.
572
573 =head1 DEPENDENCIES
574
575 =head2 Perl Modules
576
577 L<FindBin>
578
579 L<Getopt::Long|Getopt::Long>
580
581 =head2 ClearSCM Perl Modules
582
583 =begin man 
584
585  Clearadm
586  Clearexec
587  DateUtils
588  Display
589  TimeUtils
590  Utils
591
592 =end man
593
594 =begin html
595
596 <blockquote>
597 <a href="http://clearscm.com/php/scm_man.php?file=clearadm/lib/Clearadm.pm">Clearadm</a><br>
598 <a href="http://clearscm.com/php/scm_man.php?file=clearadm/lib/Clearexec.pm">Clearexec</a><br>
599 <a href="http://clearscm.com/php/scm_man.php?file=lib/DateUtils.pm">DateUtils</a><br>
600 <a href="http://clearscm.com/php/scm_man.php?file=lib/Display.pm">Display</a><br>
601 <a href="http://clearscm.com/php/scm_man.php?file=lib/TimeUtils.pm">TimeUtils</a><br>
602 <a href="http://clearscm.com/php/scm_man.php?file=lib/Utils.pm">Utils</a><br>
603 </blockquote>
604
605 =end html
606
607 =head1 BUGS AND LIMITATIONS
608
609 There are no known bugs in this script
610
611 Please report problems to Andrew DeFaria <Andrew@ClearSCM.com>.
612
613 =head1 LICENSE AND COPYRIGHT
614
615 Copyright (c) 2010, ClearSCM, Inc. All rights reserved.
616
617 =cut