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