Added caching to clearadm
[clearscm.git] / clearadm / lib / Clearadm.pm
1 =pod
2
3 =head1 NAME $RCSfile: Clearadm.pm,v $
4
5 Object oriented interface to Clearadm.
6
7 =head1 VERSION
8
9 =over
10
11 =item Author
12
13 Andrew DeFaria <Andrew@ClearSCM.com>
14
15 =item Revision
16
17 $Revision: 1.54 $
18
19 =item Created
20
21 Tue Dec 07 09:13:27 EST 2010
22
23 =item Modified
24
25 $Date: 2012/11/09 06:43:26 $
26
27 =back
28
29 =head1 SYNOPSIS
30
31 Provides the Clearadm object which handles all interaction with the Clearadm
32 database. Similar add/change/delete/update methods for other record types. In
33 general you must orient your record hashs to have the appropriately named
34 keys that correspond to the database. Also see mothod documentation for
35 specifics about the method you are envoking.
36
37  # Create new Clearadm object
38  my $clearadm = new Clearadm;
39
40  # Add a new system
41  my %system = (
42   name          => 'jupiter',
43   alias         => 'defaria.com',
44   admin         => 'Andrew DeFaria',
45   os            => 'Linux defaria.com 2.6.32-25-generic-pae #45-Ubuntu SMP Sat Oct 16 21:01:33 UTC 2010 i686 GNU/Linux',
46   type          => 'Linux',
47   description   => 'Home server',
48  );
49
50  my ($err, $msg) = $clearadm->AddSystem (%system);
51
52  # Find systems matching 'jup'
53  my @systems = $clearadm->FindSystem ('jup');
54
55  # Get a system by name
56  my %system = $clearadm->GetSystem ('jupiter');
57
58  # Update system
59  my %update = (
60   'region' => 'East Coast',
61  );
62
63  my ($err, $msg) = $clearadm->UpdateSystem ('jupiter', %update);
64
65  # Delete system (Warning: will delete all related records regarding this
66  # system).
67  my ($err, $msg) = $clearadm->DeleteSystem ('jupiter');
68
69 =head1 DESCRIPTION
70
71 This package provides and object oriented interface to the Clearadm database.
72 Methods are provided to manipulate records by adding, updating and deleting
73 them. In general you need to specify a hash which contains keys and values
74 corresponding to the database field names and values.
75
76 =head1 ROUTINES
77
78 The following methods are available:
79
80 =cut
81
82 package Clearadm;
83
84 use strict;
85 use warnings;
86
87 use Carp;
88 use DBI;
89 use File::Basename;
90 use Net::Domain qw(hostdomain);
91 use Sys::Hostname;
92
93 use FindBin;
94
95 use lib "$FindBin::Bin", "$FindBin::Bin/../../lib";
96
97 use DateUtils;
98 use Display;
99 use GetConfig;
100 use Mail;
101
102 my $conf = dirname(__FILE__) . '/../etc/clearadm.conf';
103
104 our %CLEAROPTS = GetConfig($conf);
105
106 # Globals
107 our $VERSION  = '$Revision: 1.54 $';
108    ($VERSION) = ($VERSION =~ /\$Revision: (.*) /);
109
110 $CLEAROPTS{CLEARADM_USERNAME} = $ENV{CLEARADM_USERNAME}
111                               ? $ENV{CLEARADM_USERNAME}
112                               : $CLEAROPTS{CLEARADM_USERNAME}
113                               ? $CLEAROPTS{CLEARADM_USERNAME}
114                               : 'clearwriter';
115 $CLEAROPTS{CLEARADM_PASSWORD} = $ENV{CLEARADM_PASSWORD}
116                               ? $ENV{CLEARADM_PASSWORD}
117                               : $CLEAROPTS{CLEARADM_PASSWORD}
118                               ? $CLEAROPTS{CLEARADM_PASSWORD}
119                               : 'clearwriter';
120 $CLEAROPTS{CLEARADM_SERVER}   = $ENV{CLEARADM_SERVER}
121                               ? $ENV{CLEARADM_SERVER}
122                               : $CLEAROPTS{CLEARADM_SERVER}
123                               ? $CLEAROPTS{CLEARADM_SERVER}
124                               : 'localhost';
125
126 my $defaultFilesystemThreshold = 90;
127 my $defaultFilesystemHist      = '6 months';
128 my $defaultLoadavgHist         = '6 months';
129
130 # Internal methods
131 sub _dberror($$) {
132   my ($self, $msg, $statement) = @_;
133
134   my $dberr    = $self->{db}->err;
135   my $dberrmsg = $self->{db}->errstr;
136
137   $dberr    ||= 0;
138   $dberrmsg ||= 'Success';
139
140   my $message = '';
141
142   if ($dberr) {
143     my $function = (caller(1)) [3];
144
145     $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
146              . "SQL Statement: $statement";
147   } # if
148
149   return $dberr, $message;
150 } # _dberror
151
152 sub _formatValues(@) {
153   my ($self, @values) = @_;
154
155   my @returnValues;
156
157   # Quote data values
158   push @returnValues, $_ eq '' ? 'null' : $self->{db}->quote($_)
159     for (@values);
160
161   return @returnValues;
162 } # _formatValues
163
164 sub _formatNameValues(%) {
165   my ($self, %rec) = @_;
166
167   my @nameValueStrs;
168
169   push @nameValueStrs, "$_=" . $self->{db}->quote($rec{$_})
170     for (keys %rec);
171
172   return @nameValueStrs;
173 } # _formatNameValues
174
175 sub _addRecord($%) {
176   my ($self, $table, %rec) = @_;
177
178   my $statement  = "insert into $table (";
179      $statement .= join ',', keys %rec;
180      $statement .= ') values (';
181      $statement .= join ',', $self->_formatValues(values %rec);
182      $statement .= ')';
183
184   my ($err, $msg);
185
186   $self->{db}->do($statement);
187
188   return $self->_dberror("Unable to add record to $table", $statement);
189 } # _addRecord
190
191 sub _deleteRecord($;$) {
192   my ($self, $table, $condition) = @_;
193
194   my $count;
195
196   my $statement  = "select count(*) from $table ";
197      $statement .= "where $condition"
198       if $condition;
199
200   my $sth = $self->{db}->prepare($statement)
201     or return $self->_dberror('Unable to prepare statement', $statement);
202
203   $sth->execute
204     or return $self->_dberror('Unable to execute statement', $statement);
205
206   my @row = $sth->fetchrow_array;
207
208   $sth->finish;
209
210   if ($row[0]) {
211     $count = $row[0];
212   } else {
213     $count = 0;
214   } # if
215
216   return ($count, 'Records deleted')
217     if $count == 0;
218
219   $statement  = "delete from $table ";
220   $statement .= "where $condition"
221     if $condition;
222
223   $self->{db}->do($statement);
224
225   if ($self->{db}->err) {
226     return $self->_dberror("Unable to delete record from $table", $statement);
227   } else {
228     return $count, 'Records deleted';
229   } # if
230 } # _deleteRecord
231
232 sub _updateRecord($$%) {
233   my ($self, $table, $condition, %rec) = @_;
234
235   my $statement  = "update $table set ";
236      $statement .= join ',', $self->_formatNameValues(%rec);
237      $statement .= " where $condition"
238        if $condition;
239
240   $self->{db}->do($statement);
241
242   return $self->_dberror("Unable to update record in $table", $statement);
243 } # _updateRecord
244
245 sub _checkRequiredFields($$) {
246   my ($fields, $rec) = @_;
247
248   for my $fieldname (@$fields) {
249     my $found = 0;
250
251     for (keys %$rec) {
252       if ($fieldname eq $_) {
253          $found = 1;
254          last;
255       } # if
256     } # for
257
258     return "$fieldname is required"
259       unless $found;
260   } # for
261
262   return;
263 } # _checkRequiredFields
264
265 sub _getRecords($$;$) {
266   my ($self, $table, $condition, $additional) = @_;
267
268   my ($err, $msg);
269
270   $additional ||= '';
271
272   my $statement  = "select * from $table";
273      $statement .= " where $condition" if $condition;
274      $statement .= $additional;
275
276   my $sth = $self->{db}->prepare($statement);
277
278   unless ($sth) {
279     ($err, $msg) = $self->_dberror('Unable to prepare statement', $statement);
280
281     croak $msg;
282   } # if
283
284   my $attempts    = 0;
285   my $maxAttempts = 3;
286   my $sleepTime   = 30;
287   my $status;
288
289   # We've been having the server going away. Supposedly it should reconnect so
290   # here we simply retry up to $maxAttempts times to re-execute the statement.
291   # (Are there other places where we need to do this?)
292   $err = 2006;
293
294   while ($err == 2006 and $attempts++ < $maxAttempts) {
295     $status = $sth->execute;
296
297     if ($status) {
298       $err = 0;
299       last;
300     } else {
301       ($err, $msg) = $self->_dberror('Unable to execute statement',
302                                       $statement);
303     } # if
304
305     last if $err == 0;
306
307     croak $msg unless $err == 2006;
308
309     my $timestamp = YMDHMS;
310
311     $self->Error("$timestamp: Unable to talk to DB server.\n\n$msg\n\n"
312                 . "Will try again in $sleepTime seconds", -1);
313
314     # Try to reconnect
315     $self->_connect($self->{dbserver});
316
317     sleep $sleepTime;
318   } # while
319
320   $self->Error("After $maxAttempts attempts I could not connect to the database", $err)
321     if ($err == 2006 and $attempts > $maxAttempts);
322
323   my @records;
324
325   while (my $row = $sth->fetchrow_hashref) {
326     push @records, $row;
327   } # while
328
329   return @records;
330 } # _getRecords
331
332 sub _aliasSystem($) {
333   my ($self, $system) = @_;
334
335   my %system = $self->GetSystem($system);
336
337   if ($system{name}) {
338     return $system{name};
339   } else {
340         return;
341   } # if
342 } # _aliasSystem
343
344 sub _getLastID() {
345   my ($self) = @_;
346
347   my $statement = 'select last_insert_id()';
348
349   my $sth = $self->{db}->prepare($statement);
350
351   my ($err, $msg);
352
353   unless ($sth) {
354     ($err, $msg) = $self->_dberror('Unable to prepare statement', $statement);
355
356     croak $msg;
357   } # if
358
359   my $status = $sth->execute;
360
361   unless ($status) {
362     ($err, $msg) = $self->_dberror('Unable to execute statement', $statement);
363
364     croak $msg;
365   } # if
366
367   my @records;
368
369   my @row = $sth->fetchrow_array;
370
371   return $row[0];
372 } # _getLastID
373
374 sub _connect(;$) {
375   my ($self, $dbserver) = @_;
376
377   $dbserver ||= $CLEAROPTS{CLEARADM_SERVER};
378
379   my $dbname   = 'clearadm';
380   my $dbdriver = 'mysql';
381
382   $self->{db} = DBI->connect(
383     "DBI:$dbdriver:$dbname:$dbserver",
384     $CLEAROPTS{CLEARADM_USERNAME},
385     $CLEAROPTS{CLEARADM_PASSWORD},
386     {PrintError => 0},
387   ) or croak(
388     "Couldn't connect to $dbname database "
389   . "as $CLEAROPTS{CLEARADM_USERNAME}\@$CLEAROPTS{CLEARADM_SERVER}"
390   );
391
392   $self->{dbserver} = $dbserver;
393
394   return;
395 } # _connect
396
397 sub new(;$) {
398   my ($class, $dbserver) = @_;
399
400   my $self = bless {}, $class;
401
402   $self->_connect($dbserver);
403
404   return $self;
405 } # new
406
407 sub SetNotify() {
408   my ($self) = @_;
409
410   $self->{NOTIFY} = $CLEAROPTS{CLEARADM_NOTIFY};
411
412   return;
413 } # SetNotify
414
415 sub Error($;$) {
416   my ($self, $msg, $errno) = @_;
417
418   # If $errno is specified we need to stop. However we need to notify somebody
419   # that cleartasks is no longer running.
420   error $msg;
421
422   if ($errno) {
423     if ($self->{NOTIFY}) {
424       mail(
425         to      => $self->{NOTIFY},
426         subject => 'Internal error occurred in Clearadm',
427         data    => "<p>An unexpected, internal error occurred in Clearadm:</p><p>$msg</p>",
428         mode    => 'html',
429       );
430
431       exit $errno  if $errno > 0;
432     } # if
433   } # if
434
435   return;
436 } # Error
437
438 sub AddSystem(%) {
439   my ($self, %system) = @_;
440
441   my @requiredFields = (
442     'name',
443   );
444
445   my $result = _checkRequiredFields \@requiredFields, \%system;
446
447   return -1, "AddSystem: $result"
448     if $result;
449
450   $system{loadavgHist} ||= $defaultLoadavgHist;
451
452   return $self->_addRecord('system', %system);
453 } # AddSystem
454
455 sub DeleteSystem($) {
456   my ($self, $name) = @_;
457
458   return $self->_deleteRecord('system', "name='$name'");
459 } # DeleteSystem
460
461 sub UpdateSystem ($%) {
462   my ($self, $name, %update) = @_;
463
464   return $self->_updateRecord('system', "name='$name'", %update);
465 } # UpdateSystem
466
467 sub GetSystem($) {
468   my ($self, $system) = @_;
469
470   return
471     unless $system;
472
473   my @records = $self->_getRecords(
474     'system',
475     "name='$system' or alias like '%$system%'"
476   );
477
478   if ($records[0]) {
479     return %{$records[0]};
480   } else {
481         return;
482   } # if
483 } # GetSystem
484
485 sub FindSystem(;$) {
486   my ($self, $system) = @_;
487
488   $system ||= '';
489
490   my $condition = "name like '%$system%' or alias like '%$system%'";
491
492   return $self->_getRecords('system', $condition);
493 } # FindSystem
494
495 sub SearchSystem(;$) {\r
496   my ($self, $condition) = @_;
497
498   $condition = "name like '%'" unless $condition;
499
500   return $self->_getRecords('system', $condition);\r
501 } # SearchSystem
502
503 sub AddPackage(%) {
504   my ($self, %package) = @_;
505
506   my @requiredFields = (
507     'system',
508     'name',
509     'version'
510   );
511
512   my $result = _checkRequiredFields \@requiredFields, \%package;
513
514   return -1, "AddPackage: $result"
515     if $result;
516
517   return $self->_addRecord('package', %package);
518 } # AddPackage
519
520 sub DeletePackage($$) {
521   my ($self, $system, $name) = @_;
522
523   return $self->_deleteRecord(
524     'package',
525     "(system='$system' or alias='$system') and name='$name'");
526 } # DeletePackage
527
528 sub UpdatePackage($$%) {
529   my ($self, $system, $name, %update) = @_;
530
531   $system = $self->_aliasSystem($system);
532
533   return
534     unless $system;
535
536   return $self->_updateRecord('package', "system='$system'", %update);
537 } # UpdatePackage
538
539 sub GetPackage($$) {
540   my ($self, $system, $name) = @_;
541
542   $system = $self->_aliasSystem($system);
543
544   return
545     unless $system;
546
547   return
548     unless $name;
549
550   my @records = $self->_getRecords(
551     'package',
552     "system='$system' and name='$name'"
553   );
554
555   if ($records[0]) {
556     return %{$records[0]};
557   } else {
558         return;
559   } # if
560 } # GetPackage
561
562 sub FindPackage($;$) {
563   my ($self, $system, $name) = @_;
564
565   $name ||= '';
566
567   $system = $self->_aliasSystem($system);
568
569   return
570     unless $system;
571
572   my $condition = "system='$system' and name like '%$name%'";
573
574   return $self->_getRecords('package', $condition);
575 } # FindPackage
576
577 sub AddFilesystem(%) {
578   my ($self, %filesystem) = @_;
579
580   my @requiredFields = (
581     'system',
582     'filesystem',
583     'fstype'
584   );
585
586   my $result = _checkRequiredFields \@requiredFields, \%filesystem;
587
588   return -1, "AddFilesystem: $result"
589     if $result;
590
591   # Default filesystem threshold
592   $filesystem{threshold} ||= $defaultFilesystemThreshold;
593
594   return $self->_addRecord('filesystem', %filesystem);
595 } # AddFilesystem
596
597 sub DeleteFilesystem($$) {
598   my ($self, $system, $filesystem) = @_;
599
600   $system = $self->_aliasSystem($system);
601
602   return
603     unless $system;
604
605   return $self->_deleteRecord(
606     'filesystem',
607     "system='$system' and filesystem='$filesystem'"
608   );
609 } # DeleteFilesystem
610
611 sub UpdateFilesystem($$%) {
612   my ($self, $system, $filesystem, %update) = @_;
613
614   $system = $self->_aliasSystem($system);
615
616   return
617     unless $system;
618
619   return $self->_updateRecord(
620     'filesystem',
621     "system='$system' and filesystem='$filesystem'",
622     %update
623   );
624 } # UpdateFilesystem
625
626 sub GetFilesystem($$) {
627   my ($self, $system, $filesystem) = @_;
628
629   $system = $self->_aliasSystem($system);
630
631   return
632     unless $system;
633
634   return
635     unless $filesystem;
636
637   my @records = $self->_getRecords(
638     'filesystem',
639     "system='$system' and filesystem='$filesystem'"
640   );
641
642   if ($records[0]) {
643     return %{$records[0]};
644   } else {
645     return;
646   } # if
647 } # GetFilesystem
648
649 sub FindFilesystem($;$) {
650   my ($self, $system, $filesystem) = @_;
651
652   $filesystem ||= '';
653
654   $system = $self->_aliasSystem($system);
655
656   return
657     unless $system;
658
659   my $condition = "system='$system' and filesystem like '%$filesystem%'";
660
661   return $self->_getRecords('filesystem', $condition);
662 } # FindFilesystem
663
664 sub AddVob(%) {
665   my ($self, %vob) = @_;
666
667   my @requiredFields = (
668     'tag',
669     'region',
670   );
671
672   my $result = _checkRequiredFields \@requiredFields, \%vob;
673
674   return -1, "AddVob: $result" if $result;
675
676   return $self->_addRecord('vob', %vob);
677 } # AddVob
678
679 sub DeleteVob($$) {
680   my ($self, $tag, $region) = @_;
681
682   return $self->_deleteRecord('vob', "tag='$tag' and region='$region'");
683 } # DeleteVob
684
685 sub GetVob($$) {
686   my ($self, $tag, $region) = @_;
687
688   return unless $tag;
689
690   # Windows vob tags begin with "\", which is problematic. The solution is to
691   # escape the "\"
692   $tag =~ s/^\\/\\\\/;
693
694   my @records = $self->_getRecords('vob', "tag='$tag' and region='$region'");
695
696   if ($records[0]) {
697     return %{$records[0]};
698   } else {
699     return;
700   } # if
701 } # GetVob
702
703 sub FindVob($;$) {
704   my ($self, $tag, $region) = @_;
705
706   # Windows vob tags begin with "\", which is problematic. The solution is to
707   # escape the "\"
708   $tag =~ s/^\\/\\\\/;
709
710   my $condition = "tag like '%$tag%'";
711   
712   $condition .= " and region='$region'" if $region;
713
714   return $self->_getRecords('vob', $condition);
715 } # FindVob
716
717 sub UpdateVob(%) {
718   my ($self, %vob) = @_;
719
720   # Windows vob tags begin with "\", which is problematic. The solution is to
721   # escape the "\"
722   my $vobtag = $vob{tag};
723
724   $vobtag =~ s/^\\/\\\\/;
725
726   return $self->_updateRecord('vob', "tag='$vobtag' and region='$vob{region}'", %vob);
727 } # UpdateVob
728
729 sub AddView(%) {
730   my ($self, %view) = @_;
731
732   my @requiredFields = (
733     'tag',
734     'region'
735   );
736
737   my $result = _checkRequiredFields \@requiredFields, \%view;
738
739   return -1, "AddView: $result"
740     if $result;
741
742   return $self->_addRecord('view', %view);
743 } # AddView
744
745 sub DeleteView($$) {
746   my ($self, $tag, $region) = @_;
747
748   return $self->_deleteRecord('vob', "tag='$tag' and region='$region'");
749 } # DeleteView
750
751 sub UpdateView(%) {
752   my ($self, %view) = @_;
753
754   return $self->_updateRecord('view', "tag='$view{tag}' and region='$view{region}'", %view);
755 } # UpdateView
756
757 sub GetView($$) {
758   my ($self, $tag, $region) = @_;
759
760   return unless $tag;
761
762   my @records = $self->_getRecords('view', "tag='$tag' and region='$region'");
763
764   if ($records[0]) {
765     return %{$records[0]};
766   } else {
767     return;
768   } # if
769 } # GetView
770
771 sub FindView(;$$$$) {
772   my ($self, $system, $region, $tag, $ownerName) = @_;
773
774   $system    ||= '';
775   $region    ||= '';
776   $tag       ||= '';
777   $ownerName ||= '';
778
779   my $condition;
780
781   $condition  = "system like '%$system%'";
782   $condition .= ' and ';
783   $condition  = "region like '%$region%'";
784   $condition .= ' and ';
785   $condition .= "tag like '%$tag'";
786   $condition .= ' and ';
787   $condition .= "ownerName like '%$ownerName'";
788
789   return $self->_getRecords('view', $condition);
790 } # FindView
791
792 sub AddFS(%) {
793   my ($self, %fs) = @_;
794
795   my @requiredFields = (
796     'system',
797     'filesystem',
798   );
799
800   my $result = _checkRequiredFields \@requiredFields, \%fs;
801
802   return -1, "AddFS: $result"
803     if $result;
804
805   # Timestamp record
806   $fs{timestamp} = Today2SQLDatetime;
807
808   return $self->_addRecord('fs', %fs);
809 } # AddFS
810
811 sub TrimFS($$) {
812   my ($self, $system, $filesystem) = @_;
813
814   my %filesystem = $self->GetFilesystem($system, $filesystem);
815
816   return
817     unless %filesystem;
818
819   my %task = $self->GetTask('scrub');
820
821   $self->Error("Unable to find scrub task!", 1) unless %task;
822
823   my $days;
824   my $today = Today2SQLDatetime;
825
826   # TODO: SubtractDays uses just an approximation (i.e. subtracting 30 days when
827   # in February is not right.
828   if ($filesystem{filesystemHist} =~ /(\d+) month/i) {
829     $days = $1 * 30;
830   } elsif ($filesystem{filesystemHist} =~ /(\d+) year/i) {
831     $days = $1 * 365;
832   } # if
833
834   my $oldage = SubtractDays $today, $days;
835
836   my ($dberr, $dbmsg) = $self->_deleteRecord(
837     'fs',
838     "system='$system' and filesystem='$filesystem' and timestamp<='$oldage'"
839   );
840
841   if ($dbmsg eq 'Records deleted') {
842     return (0, $dbmsg)
843       if $dberr == 0;
844
845     my %runlog;
846
847     $runlog{task}    = $task{name};
848     $runlog{started} = $today;
849     $runlog{status}  = 0;
850     $runlog{message} =
851       "Scrubbed $dberr fs records for filesystem $system:$filesystem";
852
853     my ($err, $msg) = $self->AddRunlog(%runlog);
854
855     $self->Error("Unable to add runlog - (Error: $err)\n$msg") if $err;
856   } # if
857
858   return ($dberr, $dbmsg);
859 } # TrimFS
860
861 sub TrimLoadavg($) {
862   my ($self, $system) = @_;
863
864   my %system = $self->GetSystem($system);
865
866   return
867     unless %system;
868
869   my %task = $self->GetTask('loadavg');
870
871   $self->Error("Unable to find loadavg task!", 1) unless %task;
872
873   my $days;
874   my $today = Today2SQLDatetime;
875
876   # TODO: SubtractDays uses just an approximation (i.e. subtracting 30 days when
877   # in February is not right.
878   if ($system{loadavgHist} =~ /(\d+) month/i) {
879     $days = $1 * 30;
880   } elsif ($system{loadavgHist} =~ /(\d+) year/i) {
881     $days = $1 * 365;
882   } # if
883
884   my $oldage = SubtractDays $today, $days;
885
886   my ($dberr, $dbmsg) = $self->_deleteRecord(
887     'loadavg',
888     "system='$system' and timestamp<='$oldage'"
889   );
890
891   if ($dbmsg eq 'Records deleted') {
892     return (0, $dbmsg)
893       if $dberr == 0;
894
895     my %runlog;
896
897     $runlog{task}    = $task{name};
898     $runlog{started} = $today;
899     $runlog{status}  = 0;
900     $runlog{message} =
901       "Scrubbed $dberr loadavg records for system $system";
902
903     my ($err, $msg) = $self->AddRunlog(%runlog);
904
905     $self->Error("Unable to add runload (Error: $err)\n$msg") if $err;
906   } # if
907
908   return ($dberr, $dbmsg);
909 } # TrimLoadavg
910
911 sub GetFS($$;$$$$) {
912   my ($self, $system, $filesystem, $start, $end, $count, $interval) = @_;
913
914   $system = $self->_aliasSystem($system);
915
916   return
917     unless $system;
918
919   return
920     unless $filesystem;
921
922   $interval ||= 'Minute';
923
924   my $size = $interval =~ /month/i
925            ? 7
926            : $interval =~ /day/i
927            ? 10
928            : $interval =~ /hour/i
929            ? 13
930            : 16;
931
932   undef $start if $start and $start =~ /earliest/i;
933   undef $end   if $end   and $end   =~ /latest/i;
934
935   my $condition  = "system='$system' and filesystem='$filesystem'";
936      $condition .= " and timestamp>='$start'" if $start;
937      $condition .= " and timestamp<='$end'"   if $end;
938
939      $condition .= " group by left(timestamp,$size)";
940
941   if ($count) {
942     # We can't simply do a "limit 0, $count" as that just gets the front end of
943     # the records return (i.e. if $count = say 10 and the timestamp range
944     # returns 40 rows we'll see only rows 1-10, not rows 31-40). We need limit
945     # $offset, $count where $offset = the number of qualifying records minus
946     # $count
947     my $nbrRecs = $self->Count('fs', $condition);
948     my $offset  = $nbrRecs - $count;
949
950     # Offsets of < 0 are not allowed.
951     $offset = 0
952       if $offset < 0;
953
954     $condition .= " limit $offset, $count";
955   } # if
956
957   my $statement = <<"END";
958 select
959   system,
960   filesystem,
961   mount,
962   left(timestamp,$size) as timestamp,
963   avg(size) as size,
964   avg(used) as used,
965   avg(free) as free,
966   reserve
967 from
968   fs
969   where $condition
970 END
971
972   my ($err, $msg);
973
974   my $sth = $self->{db}->prepare($statement);
975
976   unless ($sth) {
977     ($err, $msg) = $self->_dberror('Unable to prepare statement', $statement);
978
979     croak $msg;
980   } # if
981
982   my $status = $sth->execute;
983
984   unless ($status) {
985     ($err, $msg) = $self->_dberror('Unable to execute statement', $statement);
986
987     croak $msg;
988   } # if
989
990   my @records;
991
992   while (my $row = $sth->fetchrow_hashref) {
993     push @records, $row;
994   } # while
995
996   return @records;
997 } # GetFS
998
999 sub GetLatestFS($$) {
1000   my ($self, $system, $filesystem) = @_;
1001
1002   $system = $self->_aliasSystem($system);
1003
1004   return
1005     unless $system;
1006
1007   return
1008     unless $filesystem;
1009
1010   my @records = $self->_getRecords(
1011     'fs',
1012     "system='$system' and filesystem='$filesystem'"
1013   . " order by timestamp desc limit 0, 1",
1014   );
1015
1016   if ($records[0]) {
1017         return %{$records[0]};
1018   } else {
1019         return;
1020   } # if
1021 } # GetLatestFS
1022
1023 sub AddLoadavg() {
1024   my ($self, %loadavg) = @_;
1025
1026   my @requiredFields = (
1027     'system',
1028   );
1029
1030   my $result = _checkRequiredFields \@requiredFields, \%loadavg;
1031
1032   return -1, "AddLoadavg: $result"
1033     if $result;
1034
1035   # Timestamp record
1036   $loadavg{timestamp} = Today2SQLDatetime;
1037
1038   return $self->_addRecord('loadavg', %loadavg);
1039 } # AddLoadavg
1040
1041 sub GetLoadavg($;$$$$) {
1042   my ($self, $system, $start, $end, $count, $interval) = @_;
1043
1044   $system = $self->_aliasSystem($system);
1045
1046   return
1047     unless $system;
1048
1049   $interval ||= 'Minute';
1050
1051   my $size = $interval =~ /month/i
1052            ? 7
1053            : $interval =~ /day/i
1054            ? 10
1055            : $interval =~ /hour/i
1056            ? 13
1057            : 16;
1058
1059   my $condition;
1060
1061   undef $start if $start and $start =~ /earliest/i;
1062   undef $end   if $end   and $end   =~ /latest/i;
1063
1064   $condition .= " system='$system'"        if $system;
1065   $condition .= " and timestamp>='$start'" if $start;
1066   $condition .= " and timestamp<='$end'"   if $end;
1067
1068   $condition .= " group by left(timestamp,$size)";
1069
1070   if ($count) {
1071     # We can't simply do a "limit 0, $count" as that just gets the front end of
1072     # the records return (i.e. if $count = say 10 and the timestamp range
1073     # returns 40 rows we'll see only rows 1-10, not rows 31-40). We need limit
1074     # $offset, $count where $offset = the number of qualifying records minus
1075     # $count
1076     my $nbrRecs = $self->Count('loadavg', $condition);
1077     my $offset  = $nbrRecs - $count;
1078
1079     # Offsets of < 0 are not allowed.
1080     $offset = 0
1081       if $offset < 0;
1082
1083     $condition .= " limit $offset, $count";
1084   } # if
1085
1086   my $statement = <<"END";
1087 select
1088   system,
1089   left(timestamp,$size) as timestamp,
1090   uptime,
1091   users,
1092   avg(loadavg) as loadavg
1093 from
1094   loadavg
1095   where $condition
1096 END
1097
1098   my ($err, $msg);
1099
1100   my $sth = $self->{db}->prepare($statement);
1101
1102   unless ($sth) {
1103     ($err, $msg) = $self->_dberror('Unable to prepare statement', $statement);
1104
1105     croak $msg;
1106   } # if
1107
1108   my $status = $sth->execute;
1109
1110   unless ($status) {
1111     ($err, $msg) = $self->_dberror('Unable to execute statement', $statement);
1112
1113     croak $msg;
1114   } # if
1115
1116   my @records;
1117
1118   while (my $row = $sth->fetchrow_hashref) {
1119     push @records, $row;
1120   } # while
1121
1122   return @records;
1123 } # GetLoadvg
1124
1125 sub GetLatestLoadavg($) {
1126   my ($self, $system) = @_;
1127
1128   $system = $self->_aliasSystem($system);
1129
1130   return
1131     unless $system;
1132
1133   my @records = $self->_getRecords(
1134     'loadavg',
1135     "system='$system'"
1136   . " order by timestamp desc limit 0, 1",
1137   );
1138
1139   if ($records[0]) {
1140     return %{$records[0]};
1141   } else {
1142     return;
1143   } # if
1144 } # GetLatestLoadavg
1145
1146 sub GetStorage($$$;$$$$$) {
1147   my ($self, $type, $tag, $storage, $region, $start, $end, $count, $interval) = @_;
1148
1149   $interval ||= 'Day';
1150   $region   ||= $Clearcase::CC->region;
1151
1152   return unless $type =~ /vob/i or $type =~ /view/;
1153
1154   my $size = $interval =~ /month/i
1155            ? 7
1156            : $interval =~ /day/i
1157            ? 10
1158            : $interval =~ /hour/i
1159            ? 13
1160            : 16;
1161
1162   undef $start if $start and $start =~ /earliest/i;
1163   undef $end   if $end   and $end   =~ /latest/i;
1164
1165   # Windows vob tags begin with "\", which is problematic. The solution is to
1166   # escape the "\"
1167   $tag =~ s/^\\/\\\\/;
1168
1169   my $condition;
1170   my $table = $type eq 'vob' ? 'vobstorage' : 'viewstorage';
1171
1172   $condition  = "tag='$tag' and region='$region'";
1173   $condition .= " and timestamp>='$start'" if $start;
1174   $condition .= " and timestamp<='$end'"   if $end;
1175
1176   $condition .= " group by left(timestamp,$size)";
1177
1178   if ($count) {
1179     # We can't simply do a "limit 0, $count" as that just gets the front end of
1180     # the records return (i.e. if $count = say 10 and the timestamp range
1181     # returns 40 rows we'll see only rows 1-10, not rows 31-40). We need limit
1182     # $offset, $count where $offset = the number of qualifying records minus
1183     # $count
1184     my $nbrRecs = $self->Count($table, $condition);
1185     my $offset  = $nbrRecs - $count;
1186
1187     # Offsets of < 0 are not allowed.
1188     $offset = 0 if $offset < 0;
1189
1190     $condition .= " limit $offset, $count";
1191   } # if
1192
1193   my $statement = <<"END";
1194 select
1195   tag,
1196   region,
1197   left(timestamp,$size) as timestamp,
1198   avg($storage) as size
1199 from
1200   $table
1201   where $condition
1202 END
1203
1204   my ($err, $msg);
1205
1206   my $sth = $self->{db}->prepare($statement);
1207
1208   unless ($sth) {
1209     ($err, $msg) = $self->_dberror('Unable to prepare statement', $statement);
1210
1211     croak $msg;
1212   } # if
1213
1214   my $status = $sth->execute;
1215
1216   unless ($status) {
1217     ($err, $msg) = $self->_dberror('Unable to execute statement', $statement);
1218
1219     croak $msg;
1220   } # if
1221
1222   my @records;
1223
1224   while (my $row = $sth->fetchrow_hashref) {
1225     push @records, $row;
1226   } # while
1227
1228   return @records;
1229 } # GetStorage
1230
1231 sub AddTask(%) {
1232   my ($self, %task) = @_;
1233
1234   my @requiredFields = (
1235     'name',
1236     'command'
1237   );
1238
1239   my $result = _checkRequiredFields \@requiredFields, \%task;
1240
1241   return -1, "AddTask: $result"
1242     if $result;
1243
1244   return $self->_addRecord('task', %task);
1245 } # AddTask
1246
1247 sub DeleteTask($) {
1248   my ($self, $name) = @_;
1249
1250   return $self->_deleteRecord('task', "name='$name'");
1251 } # DeleteTask
1252
1253 sub FindTask($) {
1254   my ($self, $name) = @_;
1255
1256   $name ||= '';
1257
1258   my $condition = "name like '%$name%'";
1259
1260   return $self->_getRecords('task', $condition);
1261 } # FindTask
1262
1263 sub GetTask($) {
1264   my ($self, $name) = @_;
1265
1266   return
1267     unless $name;
1268
1269   my @records = $self->_getRecords('task', "name='$name'");
1270
1271   if ($records[0]) {
1272     return %{$records[0]};
1273   } else {
1274     return;
1275   } # if
1276 } # GetTask
1277
1278 sub UpdateTask($%) {
1279   my ($self, $name, %update) = @_;
1280
1281   return $self->_updateRecord('task', "name='$name'", %update);
1282 } # Update
1283
1284 sub AddSchedule(%) {
1285   my ($self, %schedule) = @_;
1286
1287   my @requiredFields = (
1288     'task',
1289   );
1290
1291   my $result = _checkRequiredFields \@requiredFields, \%schedule;
1292
1293   return -1, "AddSchedule: $result"
1294     if $result;
1295
1296   return $self->_addRecord('schedule', %schedule);
1297 } # AddSchedule
1298
1299 sub DeleteSchedule($) {
1300   my ($self, $name) = @_;
1301
1302   return $self->_deleteRecord('schedule', "name='$name'");
1303 } # DeleteSchedule
1304
1305 sub FindSchedule(;$$) {
1306   my ($self, $name, $task) = @_;
1307
1308   $name ||= '';
1309   $task||= '';
1310
1311   my $condition  = "name like '%$name%'";
1312      $condition .= ' and ';
1313      $condition .= "task like '%$task%'";
1314
1315   return $self->_getRecords('schedule', $condition);
1316 } # FindSchedule
1317
1318 sub GetSchedule($) {
1319   my ($self, $name) = @_;
1320
1321   my @records = $self->_getRecords('schedule', "name='$name'");
1322
1323   if ($records[0]) {
1324     return %{$records[0]};
1325   } else {
1326     return;
1327   } # if
1328 } # GetSchedule
1329
1330 sub UpdateSchedule($%) {
1331   my ($self, $name, %update) = @_;
1332
1333   return $self->_updateRecord('schedule', "name='$name'", %update);
1334 } # UpdateSchedule
1335
1336 sub AddRunlog(%) {
1337   my ($self, %runlog) = @_;
1338
1339   my @requiredFields = (
1340     'task',
1341   );
1342
1343   my $result = _checkRequiredFields \@requiredFields, \%runlog;
1344
1345   return -1, "AddRunlog: $result"
1346     if $result;
1347
1348   $runlog{ended} = Today2SQLDatetime;
1349
1350   $runlog{system} = hostname if $runlog{system} =~ /localhost/i;
1351
1352   my ($err, $msg) = $self->_addRecord('runlog', %runlog);
1353
1354   return ($err, $msg, $self->_getLastID);
1355 } # AddRunlog
1356
1357 sub DeleteRunlog($) {
1358   my ($self, $condition) = @_;
1359
1360   return $self->_deleteRecord('runlog', $condition);
1361 } # DeleteRunlog
1362
1363 sub FindRunlog(;$$$$$$) {
1364   my ($self, $task, $system, $status, $id, $start, $page) = @_;
1365
1366   # If ID is specified then that's all that really matters as it uniquely
1367   # identifies a runlog entry;
1368   my ($condition, $conditions);
1369   my $limit = '';
1370
1371   unless ($id) {
1372     if ($task !~ /all/i) {
1373       $conditions++;
1374       $condition = "task like '%$task%'";
1375     } # if
1376
1377     if ($system !~ /all/i) {
1378       $condition .= ' and ' if $conditions;
1379       $condition .= "system like '%$system%'";
1380       $conditions++;
1381     } # if
1382
1383     if ($status) {
1384       $condition .= ' and ' if $conditions;
1385
1386       if ($status =~ /!(-*\d+)/) {
1387         $condition .= "status<>$1";
1388       } else {
1389         $condition .= "status=$status"
1390       } # if
1391     } # if
1392
1393     # Need defined here as $start may be 0!
1394     if (defined $start) {
1395       $page ||= 10;
1396       $limit = "limit $start, $page";
1397     } # unless
1398   } else {
1399     $condition = "id=$id";
1400   } # unless
1401
1402   return $self->_getRecords('runlog', $condition, " order by started desc $limit");
1403 } # FindRunlog
1404
1405 sub GetRunlog($) {
1406   my ($self, $id) = @_;
1407
1408   return
1409     unless $id;
1410
1411   my @records = $self->_getRecords('runlog', "id=$id");
1412
1413   if ($records[0]) {
1414     return %{$records[0]};
1415   } else {
1416     return;
1417   } # if
1418 } # GetRunlog
1419
1420 sub UpdateRunlog($%) {
1421   my ($self, $id, %update) = @_;
1422
1423   return $self->_updateRecord('runlog', "id=$id", %update);
1424 } # UpdateRunlog
1425
1426 sub Count($;$) {
1427   my ($self, $table, $condition) = @_;
1428
1429   $condition = $condition ? 'where ' . $condition : '';
1430
1431   my ($err, $msg);
1432
1433   my $statement = "select count(*) from $table $condition";
1434
1435   my $sth = $self->{db}->prepare($statement);
1436
1437   unless ($sth) {
1438     ($err, $msg) = $self->_dberror('Unable to prepare statement', $statement);
1439
1440     croak $msg;
1441   } # if
1442
1443   my $status = $sth->execute;
1444
1445   unless ($status) {
1446     ($err, $msg) = $self->_dberror('Unable to execute statement', $statement);
1447
1448     croak $msg;
1449   } # if
1450
1451   # Hack! Statements such as the following:
1452   #
1453   # select count(*) from fs where system='jupiter' and filesystem='/dev/sdb5'
1454   # > group by left(timestamp,10);
1455   # +----------+
1456   # | count(*) |
1457   # +----------+
1458   # |       49 |
1459   # |       98 |
1460   # |      140 |
1461   # |        7 |
1462   # |       74 |
1463   # |      124 |
1464   # |      190 |
1465   # +----------+
1466   # 7 rows in set (0.00 sec)
1467   #
1468   # Here we want 7 but what we see in $records[0] is 49. So the hack is that if
1469   # statement contains "group by" then we assume we have the above and return
1470   # scalar @records, otherwise we return $records[0];
1471   if ($statement =~ /group by/i) {
1472     my $allrows = $sth->fetchall_arrayref;
1473
1474     return scalar @{$allrows};
1475   } else {
1476     my @records = $sth->fetchrow_array;
1477
1478     return $records[0];
1479   } # if
1480 } # Count
1481
1482 # GetWork returns two items, the number of seconds to wait before the next task
1483 # and array of hash records of work to be done immediately. The caller should
1484 # execute the work to be done, timing it, and subtracting it from the $sleep
1485 # time returned. If the caller exhausts the $sleep time then they should call
1486 # us again.
1487 sub GetWork() {
1488   my ($self) = @_;
1489
1490   my ($err, $msg);
1491
1492   my $statement = <<"END";
1493 select
1494   schedule.name as schedulename,
1495   task.name,
1496   task.system as system,
1497   task.command,
1498   schedule.notification,
1499   frequency,
1500   runlog.started as lastrun
1501 from
1502   task,
1503   schedule left join runlog on schedule.lastrunid=runlog.id
1504 where
1505       schedule.task=task.name
1506   and schedule.active='true'
1507 order by lastrun
1508 END
1509
1510   my $sth = $self->{db}->prepare($statement);
1511
1512   unless ($sth) {
1513     ($err, $msg) = $self->_dberror('Unable to prepare statement', $statement);
1514
1515     croak $msg;
1516   } # if
1517
1518   my $status = $sth->execute;
1519
1520   unless ($status) {
1521     ($err, $msg) = $self->_dberror('Unable to execute statement', $statement);
1522
1523     croak $msg;
1524   } # if
1525
1526   my $sleep;
1527   my @records;
1528
1529   while (my $row = $sth->fetchrow_hashref) {
1530    if ($$row{system} !~ /localhost/i) {
1531      my %system = $self->GetSystem($$row{system});
1532
1533      # Skip inactive systems
1534      next if $system{active} eq 'false';
1535    } # if
1536
1537     # If started is not defined then this task was never run so run it now.
1538     unless ($$row{lastrun}) {
1539       push @records, $row;
1540       next;
1541     } # unless
1542
1543     # TODO: Handle frequencies better.
1544     my $seconds;
1545
1546     if ($$row{frequency} =~ /(\d+) seconds/i) {
1547       $seconds = $1;
1548     } elsif ($$row{frequency} =~ /(\d+) minute/i) {
1549       $seconds = $1 * 60;
1550     } elsif ($$row{frequency} =~ /(\d+) hour/i) {
1551       $seconds = $1 * 60 * 60;
1552     } elsif ($$row{frequency} =~ /(\d+) day/i) {
1553       $seconds= $1 * 60 * 60 * 24;
1554     } else {
1555       warning "Don't know how to handle frequencies like $$row{frequency}";
1556       next;
1557     } # if
1558
1559     my $today    = Today2SQLDatetime;
1560     my $lastrun  = Add($$row{lastrun}, (seconds => $seconds));
1561     my $waitTime = DateToEpoch($lastrun) - DateToEpoch($today);
1562
1563     if ($waitTime < 0) {
1564       # We're late - push this onto records and move on
1565       push @records, $row;
1566     } # if
1567
1568     $sleep ||= $waitTime;
1569
1570     if ($sleep > $waitTime) {
1571       $sleep = $waitTime;
1572     } # if
1573   } # while
1574
1575   # Even if there is nothing to do the caller should sleep a bit and come back
1576   # to us. So if it ends up there's nothing past due, and nothing upcoming, then
1577   # sleep for a minute and return here. Somebody may have added a new task next
1578   # time we're called.
1579   if (@records == 0 and not $sleep) {
1580     $sleep = 60;
1581   } # if
1582
1583   return ($sleep, @records);
1584 } # GetWork
1585
1586 sub GetUniqueList($$) {
1587   my ($self, $table, $field) = @_;
1588
1589   my ($err, $msg);
1590
1591   my $statement = "select $field from $table group by $field";
1592
1593   my $sth = $self->{db}->prepare($statement);
1594
1595   unless ($sth) {
1596     ($err, $msg) = $self->_dberror('Unable to prepare statement', $statement);
1597
1598     croak $msg;
1599   } # if
1600
1601   my $status = $sth->execute;
1602
1603   unless ($status) {
1604     ($err, $msg) = $self->_dberror('Unable to execute statement', $statement);
1605
1606     croak $msg;
1607   } # if
1608
1609   my @values;
1610
1611   while (my @row = $sth->fetchrow_array) {
1612     if ($row[0]) {
1613       push @values, $row[0];
1614     } else {
1615       push @values, '<NULL>';
1616     } # if
1617   } # for
1618
1619   return @values;
1620 } # GetUniqueList
1621
1622 sub AddAlert(%) {
1623   my ($self, %alert) = @_;
1624
1625   my @requiredFields = (
1626     'name',
1627     'type',
1628   );
1629
1630   my $result = _checkRequiredFields \@requiredFields, \%alert;
1631
1632   return -1, "AddAlert: $result"
1633     if $result;
1634
1635   return $self->_addRecord('alert', %alert);
1636 } # AddAlert
1637
1638 sub DeleteAlert($) {
1639   my ($self, $name) = @_;
1640
1641   return $self->_deleteRecord('alert', "name='$name'");
1642 } # DeleteAlert
1643
1644 sub FindAlert(;$) {
1645   my ($self, $alert) = @_;
1646
1647   $alert ||= '';
1648
1649   my $condition = "name like '%$alert%'";
1650
1651   return $self->_getRecords('alert', $condition);
1652 } # FindAlert
1653
1654 sub GetAlert($) {
1655   my ($self, $name) = @_;
1656
1657   return
1658     unless $name;
1659
1660   my @records = $self->_getRecords('alert', "name='$name'");
1661
1662   if ($records[0]) {
1663     return %{$records[0]};
1664   } else {
1665     return;
1666   } # if
1667 } # GetAlert
1668
1669 sub SendAlert($$$$$$$) {
1670   my (
1671     $self,
1672     $alert,
1673     $system,
1674     $notification,
1675     $subject,
1676     $message,
1677     $to,
1678     $runlogID,
1679   ) = @_;
1680
1681   my $footing  = '<hr><p style="text-align: center;">';
1682      $footing .= '<font color="#bbbbbb">';
1683   my $year     = (localtime)[5] + 1900;
1684      $footing .= "<a href='$CLEAROPTS{CLEARADM_WEBBASE}'>Clearadm</a><br>";
1685      $footing .= "Copyright &copy; $year, ClearSCM, Inc. - All rights reserved";
1686
1687   my %alert = $self->GetAlert($alert);
1688
1689   if ($alert{type} eq 'email') {
1690     my $from = 'Clearadm@' . hostdomain;
1691
1692     mail(
1693       from    => $from,
1694       to      => $to,
1695       subject => "Clearadm Alert: $system: $subject",
1696       mode    => 'html',
1697       data    => $message,
1698       footing => $footing,
1699     );
1700   } else {
1701     $self->Error("Don't know how to send $alert{type} alerts\n"
1702                 . "Subject: $subject\n"
1703                 . "Message: $message", 1);
1704   } # if
1705
1706   # Log alert
1707   my %alertlog = (
1708     alert        => $alert,
1709     system       => $system,
1710     notification => $notification,
1711     runlog       => $runlogID,
1712     timestamp    => Today2SQLDatetime,
1713     message      => $subject,
1714   );
1715
1716   return $self->AddAlertlog(%alertlog);
1717 } # SendAlert
1718
1719 sub GetLastAlert($$) {
1720   my ($self, $notification, $system) = @_;
1721
1722   my $statement = <<"END";
1723 select
1724   runlog,
1725   timestamp
1726 from
1727   alertlog
1728 where
1729       notification='$notification'
1730   and system='$system'
1731 order by
1732   timestamp desc
1733 limit
1734   0, 1
1735 END
1736
1737   my $sth = $self->{db}->prepare($statement)
1738     or return $self->_dberror('Unable to prepare statement', $statement);
1739
1740   $sth->execute
1741     or return $self->_dberror('Unable to execute statement', $statement);
1742
1743   my $alertlog= $sth->fetchrow_hashref;
1744
1745   $sth->finish;
1746
1747   if ($alertlog) {
1748     return %$alertlog;
1749   } else {
1750     return;
1751   } # if
1752 } # GetLastAlert
1753
1754 sub GetLastTaskFailure($$) {
1755   my ($self, $task, $system) = @_;
1756
1757   my $statement = <<"END";
1758 select
1759   id,
1760   ended
1761 from
1762   runlog
1763 where
1764       status <> 0
1765   and task='$task'
1766   and system='$system'
1767   and alerted='true'
1768 order by
1769   ended desc
1770 limit
1771   0, 1
1772 END
1773
1774   my $sth = $self->{db}->prepare($statement)
1775     or return $self->_dberror('Unable to prepare statement', $statement);
1776
1777   $sth->execute
1778     or return $self->_dberror('Unable to execute statement', $statement);
1779
1780   my $runlog= $sth->fetchrow_hashref;
1781
1782   $sth->finish;
1783
1784   if ($$runlog{ended}) {
1785     return %$runlog;
1786   } # if
1787
1788   # If we didn't get any ended in the last call then there's nothing that
1789   # qualified. Still let's return a record (%runlog) that has a valid id so
1790   # that the caller can update that runlog with alerted = 'true'.
1791   $statement = <<"END";
1792 select
1793   id
1794 from
1795   runlog
1796 where
1797       status <> 0
1798   and task='$task'
1799   and system='$system'
1800 order by
1801   ended desc
1802 limit
1803   0, 1
1804 END
1805
1806   $sth = $self->{db}->prepare($statement)
1807     or return $self->_dberror('Unable to prepare statement', $statement);
1808
1809   $sth->execute
1810     or return $self->_dberror('Unable to execute statement', $statement);
1811
1812   $runlog = $sth->fetchrow_hashref;
1813
1814   $sth->finish;
1815
1816   if ($runlog) {
1817     return %$runlog;
1818   } else {
1819     return
1820   } # if
1821 } # GetLastTaskFailure
1822
1823 sub Notify($$$$$$) {
1824   my (
1825     $self,
1826     $notification,
1827     $subject,
1828     $message,
1829     $task,
1830     $system,
1831     $filesystem,
1832     $runlogID,
1833   ) = @_;
1834
1835   $runlogID = $self->_getLastID
1836     unless $runlogID;
1837
1838   my ($err, $msg);
1839
1840   # Update filesystem, if $filesystem was specified
1841   if ($filesystem) {
1842     ($err, $msg) = $self->UpdateFilesystem(
1843       $system,
1844       $filesystem, (
1845         notification => $notification,
1846       ),
1847     );
1848
1849     $self->Error("Unable to set notification for filesystem $system:$filesystem "
1850                . "(Status: $err)\n$msg", $err) if $err;
1851   } # if
1852
1853   # Update system
1854   ($err, $msg) = $self->UpdateSystem(
1855     $system, (
1856       notification => $notification,
1857     ),
1858   );
1859
1860   my %notification = $self->GetNotification($notification);
1861
1862   my %lastnotified = $self->GetLastAlert($notification, $system);
1863
1864   if (%lastnotified and $lastnotified{timestamp}) {
1865     my $today        = Today2SQLDatetime;
1866     my $lastnotified = $lastnotified{timestamp};
1867
1868     if ($notification{nomorethan} =~ /hour/i) {
1869       $lastnotified = Add($lastnotified, (hours => 1));
1870     } elsif ($notification{nomorethan} =~ /day/i) {
1871       $lastnotified = Add($lastnotified, (days => 1));
1872     } elsif ($notification{nomorethan} =~ /week/i) {
1873       $lastnotified = Add($lastnotified, (days => 7));
1874     } elsif ($notification{nomorethan} =~ /month/i) {
1875       $lastnotified = Add($lastnotified, (month => 1));
1876     } # if
1877
1878     # If you want to fake an alert in the debugger just change $diff accordingly
1879     my $diff = Compare($today, $lastnotified);
1880
1881     return
1882       if $diff <= 0;
1883   } # if
1884
1885   my $when       = Today2SQLDatetime;
1886   my $nomorethan = lc $notification{nomorethan};
1887   my %alert      = $self->GetAlert($notification{alert});
1888   my $to         = $alert{who};
1889
1890   # If $to is null then this means to send the alert to the admin for the
1891   # machine.
1892   unless ($to) {
1893     if ($system) {
1894       my %system = $self->GetSystem($system);
1895
1896       $to = $system{email};
1897     } else {
1898       # If we don't know what system this error occurred on we'll have to notify
1899       # the "super user" defined as $self->{NOTIFY} (The receiver of last
1900       # resort)
1901       $to = $self->{NOTIFY};
1902     } # if
1903   } # unless
1904
1905   unless ($to) {
1906     Error "To undefined";
1907   } # unless
1908
1909   $message .= "<p>You will receive this alert no more than $nomorethan.</p>";
1910
1911   ($err, $msg) = $self->SendAlert(
1912     $notification{alert},
1913     $system,
1914     $notification{name},
1915     $subject,
1916     $message,
1917     $to,
1918     $runlogID,
1919   );
1920
1921   $self->Error("Unable to send alert (Status: $err)\n$msg", $err) if $err;
1922
1923   verbose "Sent alert to $to";
1924
1925   # Update runlog to indicate we notified the user for this execution
1926   ($err, $msg) = $self->UpdateRunlog(
1927     $runlogID, (
1928       alerted => 'true',
1929     ),
1930   );
1931
1932   $self->Error("Unable to update runlog (Status: $err)\n$msg", $err) if $err;
1933
1934   return;
1935 } # Notify
1936
1937 sub ClearNotifications($$;$) {
1938   my ($self, $system, $filesystem) = @_;
1939
1940   my ($err, $msg);
1941
1942   if ($filesystem) {
1943     ($err, $msg) = $self->UpdateFilesystem(
1944       $system,
1945       $filesystem, (notification => undef),
1946     );
1947
1948     error "Unable to clear notification for filesystem $system:$filesystem "
1949         . "(Status: $err)\n$msg", $err
1950       if $err;
1951
1952     # Check to see any of this system's filesystems have notifications. If none
1953     # then it's save to say we've turned off the last notification for a
1954     # filesystem involved with this system and if $system{notification} was
1955     # 'Filesystem' then we can toggle off the notification on the system too
1956     my $filesystemsAlerted = 0;
1957
1958     for ($self->FindFilesystem($system)) {
1959       $filesystemsAlerted++
1960         if $$_{notification};
1961     } # for
1962
1963     my %system = $self->GetSystem($system);
1964
1965     return
1966       unless $system;
1967
1968     if ($system{notification}                 and
1969         $system{notification} eq 'Filesystem' and
1970         $filesystemsAlerted == 0) {
1971       ($err, $msg) = $self->UpdateSystem($system, (notification => undef));
1972
1973       $self->Error("Unable to clear notification for system $system "
1974                   . "(Status: $err)\n$msg", $err) if $err;
1975     } # if
1976   } else {
1977     ($err, $msg) = $self->UpdateSystem($system, (notification => undef));
1978
1979     $self->Error("Unable to clear notification for system $system "
1980                 . "(Status: $err)\n$msg", $err) if $err;
1981   } # if
1982
1983   return;
1984 } # ClearNotifications
1985
1986 sub SystemAlive(%) {
1987   my ($self, %system) = @_;
1988
1989   # If we've never heard from this system then we will assume that the system
1990   # has not been set up to run clearagent and has never checked in. In any event
1991   # we cannot say the system died because we've never known it to be alive!
1992   return 1
1993     unless $system{lastheardfrom};
1994
1995   # If a system is not active (may have been temporarily been deactivated) then
1996   # we don't want to turn on the bells and whistles alerting people it's down.
1997   return 1
1998     if $system{active} eq 'false';
1999
2000   my $today         = Today2SQLDatetime;
2001   my $lastheardfrom = $system{lastheardfrom};
2002
2003   my $tenMinutes = 10 * 60;
2004
2005   $lastheardfrom = Add($lastheardfrom, (seconds => $tenMinutes));
2006
2007   if (DateToEpoch($lastheardfrom) < DateToEpoch($today)) {
2008     $self->UpdateSystem(
2009       $system{name}, (
2010         notification => 'Heartbeat'
2011       ),
2012     );
2013
2014     return;
2015   } else {
2016     if ($system{notification}) {
2017       $self->UpdateSystem(
2018         $system{name}, (
2019           notification => undef
2020         ),
2021       );
2022     }
2023     return 1;
2024   } # if
2025 } # SystemAlive
2026
2027 sub UpdateAlert($%) {
2028   my ($self, $name, %update) = @_;
2029
2030   return $self->_updateRecord(
2031     'alert',
2032     "name='$name'",
2033     %update
2034   );
2035 } # UpdateAlert
2036
2037 sub AddAlertlog(%) {
2038   my ($self, %alertlog) = @_;
2039
2040   my @requiredFields = (
2041     'alert',
2042     'notification',
2043   );
2044
2045   my $result = _checkRequiredFields \@requiredFields, \%alertlog;
2046
2047   return -1, "AddAlertlog: $result"
2048     if $result;
2049
2050   # Timestamp record
2051   $alertlog{timestamp} = Today2SQLDatetime;
2052
2053   return $self->_addRecord('alertlog', %alertlog);
2054 } # AddAlertlog
2055
2056 sub DeleteAlertlog($) {
2057   my ($self, $condition) = @_;
2058
2059   return
2060     unless $condition;
2061
2062   if ($condition =~ /all/i) {
2063     return $self->_deleteRecord('alertlog');
2064   } else {
2065     return $self->_deleteRecord('alertlog', $condition);
2066   } # if
2067 } # DeleteAlertlog
2068
2069 sub FindAlertlog(;$$$$$) {
2070   my ($self, $alert, $system, $notification, $start, $page) = @_;
2071
2072   $alert        ||= '';
2073   $system       ||= '';
2074   $notification ||= '';
2075
2076   my $condition  = "alert like '%$alert%'";
2077      $condition .= ' and ';
2078      $condition .= "system like '%$system%'";
2079      $condition .= ' and ';
2080      $condition .= "notification like '%$notification%'";
2081      $condition .= " order by timestamp desc";
2082
2083      if (defined $start) {
2084        $page ||= 10;
2085        $condition .= " limit $start, $page";
2086      } # unless
2087
2088   return $self->_getRecords('alertlog', $condition);
2089 } # FindAlertLog
2090
2091 sub GetAlertlog($) {
2092   my ($self, $alert) = @_;
2093
2094   return
2095     unless $alert;
2096
2097   my @records = $self->_getRecords('alertlog', "alert='$alert'");
2098
2099   if ($records[0]) {
2100     return %{$records[0]};
2101   } else {
2102     return;
2103   } # if
2104 } # GetAlertlog
2105
2106 sub UpdateAlertlog($%) {
2107   my ($self, $alert, %update) = @_;
2108
2109   return $self->_updateRecord(
2110     'alertlog',
2111     "alert='$alert'",
2112     %update
2113   );
2114 } # UpdateAlertlog
2115
2116 sub AddNotification(%) {
2117   my ($self, %notification) = @_;
2118
2119   my @requiredFields = (
2120     'name',
2121     'alert',
2122     'cond'
2123   );
2124
2125   my $result = _checkRequiredFields \@requiredFields, \%notification;
2126
2127   return -1, "AddNotification: $result"
2128     if $result;
2129
2130   return $self->_addRecord('notification', %notification);
2131 } # AddNotification
2132
2133 sub DeleteNotification($) {
2134   my ($self, $name) = @_;
2135
2136   return $self->_deleteRecord('notification', "name='$name'");
2137 } # DeletePackage
2138
2139 sub FindNotification(;$$) {
2140   my ($self, $name, $cond, $ordering) = @_;
2141
2142   $name ||= '';
2143
2144   my $condition  = "name like '%$name%'";
2145      $condition .= " and $cond"
2146        if $cond;
2147
2148   return $self->_getRecords('notification', $condition);
2149 } # FindNotification
2150
2151 sub GetNotification($) {
2152   my ($self, $name) = @_;
2153
2154   return
2155     unless $name;
2156
2157   my @records = $self->_getRecords('notification', "name='$name'");
2158
2159   if ($records[0]) {
2160     return %{$records[0]};
2161   } else {
2162     return;
2163   } # if
2164 } # GetNotification
2165
2166 sub UpdateNotification($%) {
2167   my ($self, $name, %update) = @_;
2168
2169   return $self->_updateRecord(
2170     'notification',
2171     "name='$name'",
2172     %update
2173   );
2174 } # UpdateNotification
2175
2176 sub AddVobStorage(%) {
2177   my ($self, %vobstorage) = @_;
2178
2179   my @requiredFields = (
2180     'tag',
2181   );
2182
2183   my $result = _checkRequiredFields \@requiredFields, \%vobstorage;
2184
2185   return -1, "AddVobStorage: $result" if $result;
2186
2187   # Timestamp record
2188   $vobstorage{timestamp} = Today2SQLDatetime;
2189
2190   return $self->_addRecord('vobstorage', %vobstorage);
2191 } # AddVobStorage
2192
2193 sub AddViewStorage(%) {
2194   my ($self, %viewstorage) = @_;
2195
2196   my @requiredFields = (
2197     'tag',
2198   );
2199
2200   my $result = _checkRequiredFields \@requiredFields, \%viewstorage;
2201
2202   return -1, "AddViewStorage: $result" if $result;
2203
2204   # Timestamp record
2205   $viewstorage{timestamp} = Today2SQLDatetime;
2206
2207   return $self->_addRecord('viewstorage', %viewstorage);
2208 } # AddViewStorage
2209
2210 1;
2211
2212 =pod
2213
2214 =head1 CONFIGURATION AND ENVIRONMENT
2215
2216 DEBUG: If set then $debug is set to this level.
2217
2218 VERBOSE: If set then $verbose is set to this level.
2219
2220 TRACE: If set then $trace is set to this level.
2221
2222 =head1 DEPENDENCIES
2223
2224 =head2 Perl Modules
2225
2226 L<Carp>
2227
2228 L<DBI>
2229
2230 L<FindBin>
2231
2232 L<Net::Domain|Net::Domain>
2233
2234 =head2 ClearSCM Perl Modules
2235
2236 =begin man
2237
2238  DateUtils
2239  Display
2240  GetConfig
2241  Mail
2242
2243 =end man
2244
2245 =begin html
2246
2247 <blockquote>
2248 <a href="http://clearscm.com/php/scm_man.php?file=lib/DateUtils.pm">DateUtils</a><br>
2249 <a href="http://clearscm.com/php/scm_man.php?file=lib/Display.pm">Display</a><br>
2250 <a href="http://clearscm.com/php/scm_man.php?file=lib/GetConfig.pm">GetConfig</a><br>
2251 <a href="http://clearscm.com/php/scm_man.php?file=lib/Mail.pm">Mail</a><br>
2252 </blockquote>
2253
2254 =end html
2255
2256 =head1 BUGS AND LIMITATIONS
2257
2258 There are no known bugs in this module
2259
2260 Please report problems to Andrew DeFaria <Andrew@ClearSCM.com>.
2261
2262 =head1 LICENSE AND COPYRIGHT
2263
2264 Copyright (c) 2010, ClearSCM, Inc. All rights reserved.
2265
2266 =cut