Removed /usr/local from CDPATH
[clearscm.git] / clearadm / viewager.cgi
1 #!/usr/local/bin/perl
2
3 =pod
4
5 =head1 NAME $RCSfile: viewager.cgi,v $
6
7 View Aging
8
9 =head1 VERSION
10
11 =over
12
13 =item Author
14
15 Andrew DeFaria <Andrew@ClearSCM.com>
16
17 =item Revision
18
19 $Revision: 1.11 $
20
21 =item Created:
22
23 Mon Oct 25 11:10:47 PDT 2008
24
25 =item Modified:
26
27 $Date: 2011/01/14 16:50:54 $
28
29 =back
30
31 =head1 SYNOPSIS
32
33 This script serves 4 distinct functions. One function is to find
34 old views and report them to their owners via email so that view cleanup can be
35 done. Another function just does a quick report stdout. Yet another function is
36 to present the list of views in a web page. Finally there is a function
37 (generate) which generates a cache file containing information about views. This
38 function is designed to be run by a scheduler such as cron. Note that the web
39 page function relies on and uses this cache file too.
40
41 =head1 DESCRIPTION
42
43 Most Clearcase administrators wrestle with trying to keep the number of views 
44 under control. Users often create views but seldom think to remove them. Views
45 grow old and forgotten.
46
47 Many approaches have been taken, usally emailing the users telling them to clean
48 up their views. This script, viewager.cgi, attempts to encapsulate the task of
49 gathering information about old views, informing users of which of their views
50 are old and presenting reports in the form of a web page showing all views
51 including old ones.
52
53 =head1 USAGE Email, Report and Generate modes
54
55  Usage viewager.cgi: [-u|sage] [-region <region>] [-e|mail]
56                      [-a|gethreshold <n>] [-n|brThreshold <n>]
57                      [-ac|tion <act>] [-s|ort <field>]
58                      [-v|erbose] [-d|ebug]
59
60  Where:
61    -u|sage:            Displays usage
62    -region <region>:   Region to use when looking for views (Default
63                        for generate action: all)
64    -e|mail:            Send email to owners of old views
65    -ag|eThreshold:     Number of days before a view is considered old
66                        (Default: 180)
67    -n|brThreshold <n>: Number of views to report. Can be used for say a
68                        "top 10" old views. Useful with -action report
69                        (Default: Report all views)
70    -ac|tion <act>      Valid actions include 'generate' or 'report'.
71                        Generate mode merely regenerates the cache file.
72                        Report produces a quick report to stdout.
73    -s|ort <field>:     Where <field> is one of <tag|ownerName|type|age>
74
75    -ve|rbose:          Be verbose
76    -d|ebug:            Output debug messages
77
78 =head1 USAGE Web Page mode
79
80 Parameters for the web page mode are provided by the CPAN module CGI and are
81 normally passed in as part of the URL. These parameters are specified as
82 name/value pairs:
83
84   sortby=<tag|ownerName|type|age>
85     Note: age will sort in a reverse numerical fashion
86
87   user=<username>
88     <username> can be a partial name (e.g. 'defaria')
89
90 =head1 DESCRIPTION
91
92 This script seek to handle the general issue of handling old views. In generate
93 mode this script goes through all views collecting data about all of the views
94 and creates a cache file. The reason for this is that this process is length
95 (At one client's site with ~2500 views takes about 1 hour). As such you'd
96 probably want to schedule the running of this for once a day.
97
98 Once the cache file is created other modes will read that file and report on it.
99 In report mode you can report to stdout. For example, the following will give
100 you a quick "top 10" oldest views:
101
102  $ viewager.cgi -action report -n 10
103
104 You may wish to add the following to your conrtabe to generated the cachefile
105 nightly:
106
107  0 0 * * * cd /<DocumentRoot>/viewager && /<path>/viewager.cgi -action=generate
108
109 =head1 User module
110
111 Since the method for translating a user's userid into other attributes like
112 the users fullname and email, we rely on a User.pm module to implement a User
113 object that takes a string identifying the user and return useful informaiton
114 about the user, specifically the fullname and email address.
115
116 =cut
117
118 use strict;
119 use warnings;
120
121 use FindBin;
122 use Getopt::Long;
123 use CGI qw(:standard :cgi-lib *table start_Tr end_Tr);
124 use CGI::Carp 'fatalsToBrowser';
125 use File::stat;
126 use Time::localtime;
127
128 use lib "$FindBin::Bin/lib", "$FindBin::Bin/../lib";
129
130 use Clearadm;
131 use ClearadmWeb;
132 use Clearcase;
133 use Clearcase::View;
134 use Clearcase::Views;
135 use DateUtils;
136 use Display;
137 use Mail;
138 use Utils;
139 use User;
140
141 my $VERSION  = '$Revision: 1.11 $';
142   ($VERSION) = ($VERSION =~ /\$Revision: (.*) /);
143
144 my %opts;
145 my $clearadm;
146
147 $opts{sortby}       ||= 'age';
148 $opts{ageThreshold}   = 180; # Default number of days a view must be older than
149
150 my $subtitle = 'View Aging Report';
151 my $email;
152
153 my $port       = CGI::server_port;
154    $port       = ($port == 80) ? '' : ":$port";
155 my $scriptName = CGI::script_name;
156    $scriptName =~ s/index.cgi//;
157 my $script     = 'http://'
158                . $Clearadm::CLEAROPTS{CLEARADM_SERVER}
159                . $port
160                . $scriptName;
161
162 my %total;
163 my $nbrThreshold;       # Number of views threshold - think top 10
164
165 sub GenerateRegion($) {
166   my ($region) = @_;
167
168   verbose "Processing region $region";
169   $total{Regions}++;
170
171   my $views = Clearcase::Views->new ($region);
172   my @Views = $views->views;
173   my @views;
174
175   verbose scalar @Views . " views to process";
176
177   my $i = 0;
178
179   for my $name (@Views) {
180     $total{Views}++;
181
182     if (++$i % 100 == 0) {
183       verbose_nolf $i;
184     } elsif ($i % 25 == 0) {
185       verbose_nolf '.';
186     }# if
187
188     my $view = Clearcase::View->new($name, $region);
189     
190     my $gpath;
191
192     if ($view->webview) {
193       # TODO: There doesn't appear to be a good way to get the gpath for a
194       # webview since it's set to <nogpath>! Here we try to compose one using
195       # $view->host and $view->access_path but this is decidedly Windows centric
196       # and thus not portable. This needs to be fixed!
197       $gpath = '\\\\' . $view->host . '\\' . $view->access_path;
198
199       # Change any ":" to "$". This is to change things like D:\path -> D$\path.
200       # This assumes we have permissions to access through the administrative
201       # <drive>$ mounts.
202       $gpath =~ s/:/\$/; 
203     } else {
204       $gpath = $view->gpath;
205     } # if
206
207     # Note if the view server is unreachable (e.g. user puts view on laptop and
208     # the laptop is powered off), then these fields will be undef. Change them
209     # to Unknown. (Should Clearcase::View.pm do this instead?).
210     my $type      = $view->type;
211        $type    ||= 'dynamic';
212     my $ownerid   = $view->owner;
213        $ownerid ||= 'Unknown';
214
215     my $user;
216
217     if ($ownerid =~ /^\w+(\\|\/)(\w+)/) {
218       # TODO: Handle user identification better
219       #$user = User->new ($ownerid);
220
221       $ownerid       = $2;
222       $user->{name}  = $2;
223       $user->{email} = "$2\@gddsi.com";
224     } else {
225       $ownerid       = 'Unknown';
226       $user->{name}  = 'Unknown';
227       $user->{email} = 'unknown@gddsi.com';
228     } # if
229
230     my $age       = 0;
231     my $ageSuffix = '';
232
233     my $modified_date = $view->modified_date;
234     
235     if ($modified_date) {
236       $modified_date = substr $modified_date, 0, 16;
237       $modified_date =~ s/T/\@/;
238
239       # Compute age
240       $age       = Age ($modified_date);
241       $ageSuffix = $age != 1 ? 'days' : 'day';
242     } # if
243
244     my %oldView = $clearadm->GetView($view->tag, $view->region);
245
246     my ($err, $msg);
247
248     my %viewRec = (
249       region    => $view->region,
250       tag       => $view->tag,
251       owner     => $ownerid,
252       ownerName => $user->{name},
253       email     => $user->{email},
254       type      => $type,
255       gpath     => $gpath,
256       age       => $age,
257       ageSuffix => $ageSuffix,
258     );
259
260     # Some views have not yet been modified
261     $viewRec{modified} = $modified_date if $modified_date;
262
263     if (%oldView) {
264       ($err, $msg) = $clearadm->UpdateView(%viewRec);
265
266       error "Unable to update view $name in Clearadm\n$msg", $err if $err;
267     } else {
268       ($err, $msg) = $clearadm->AddView(%viewRec);
269
270       error "Unable to add view $name to Clearadm\n$msg", $err if $err;
271     } # if
272   } # for
273
274   verbose "\nProcessed region $region";
275   
276   return;
277 } # GenerateRegion
278
279 sub Generate ($) {
280   my ($region) = @_;
281
282   if ($region) {
283     GenerateRegion $region;
284   } else {
285     GenerateRegion $_ for $Clearcase::CC->regions;
286   } # if
287   
288   return;
289 } # Generate
290
291 sub Report (@) {
292   my (@views) = @_;
293
294   $total{'Views processed'} = @views;
295
296   my @sortedViews;
297
298   if ($opts{sortby} eq 'age') {
299     # Sort by age numerically decending
300     @sortedViews = sort { $$b{$opts{sortby}} <=> $$a{$opts{sortby}} } @views;
301   } else {
302     @sortedViews = sort { $$a{$opts{sortby}} cmp $$b{$opts{sortby}} } @views;
303   } # if
304
305   $total{Reported} = 0;
306
307   for (@sortedViews) {
308     my %view = %{$_};
309
310     last
311       if ($nbrThreshold and $total{Reported} + 1 > $nbrThreshold) or
312          ($view{age} < $opts{ageThreshold});
313
314     $total{Reported}++;
315
316     if ($view{type}) {
317       if ($view{type} eq 'dynamic') {
318         $total{Dynamic}++;
319       } elsif ($view{type} eq 'snapshot') {
320         $total{Snapshot}++;
321       } elsif ($view{type} eq 'webview') {
322         $total{Webview}++
323       } else {
324         $total{$view{type}}++;
325       } # if
326     } else {
327       $total{Unknown}++;
328     } # if
329
330 format STDOUT_TOP =
331             View Name                         Owner           View Type   Last Modified      Age
332 ------------------------------------- ---------------------- ----------- ---------------- -----------
333 .
334 format STDOUT =
335 @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<<<<< @>>>> @<<<<
336 $view{tag},$view{owner},$view{type},$view{modified},$view{age},$view{ageSuffix}
337 .
338
339     write;
340   } # for
341   
342   return;
343 } # Report
344
345 sub FormatTable($@) {
346   my ($style, @views) = @_;
347   
348   my $table;
349
350   my $nbrViews = @views;
351   
352   my $legend =
353     font ({-class => 'label'}, 'View type: ') .
354     font ({-class => 'dynamic'}, 'Dyanmic') .
355     ' ' .
356     font ({-class => 'snapshot'}, 'Snapshot') .
357     ' ' .
358     font ({-class => 'web'}, 'Web') .
359     ' ' .
360     font ({-class => 'unknown'}, 'Unknown');
361
362   my $caption;
363
364   my $regionDropdown = start_form(
365     -action => $script,
366   );
367
368   $regionDropdown .= font {-class => 'captionLabel'}, 'Region: ';
369   $regionDropdown .= popup_menu(
370     -name     => 'region',
371     -values   => [$Clearcase::CC->regions],
372     -default  => $Clearcase::CC->region,
373     -onchange => 'submit();',
374   );
375
376   $regionDropdown .= end_form;
377
378   $caption .= start_table {
379     class        => 'caption',
380     cellspacing  => 1,
381     width        => '100%',
382   };
383
384   $caption   .= start_Tr;
385     $caption .= td {
386        -align => 'left',
387        -width => '30%',
388     }, font ({-class => 'label'}, 'Registry: '),
389        setField($Clearcase::CC->registry_host), '<br>',
390        font ({-class => 'label'}, 'Views: '),
391        $nbrViews;
392     $caption .= td {
393       -align => 'center',
394       -width => '40%',
395     }, $legend;
396     $caption .= td {
397       -align => 'right',
398       -width => '30%',
399     }, $regionDropdown;
400   $caption .= end_Tr; 
401
402   $caption .= end_table;
403
404   $table .= start_table {
405     cellspacing => 1,
406     width       => '75%',
407   };
408
409   $table   .= caption $caption;
410   $table   .= start_Tr {-class => 'heading'};
411     $table .= th '#';
412
413     # Set defaults if not set already
414     $opts{sortby}  ||= 'age';
415     $opts{reverse} ||= 0;
416     
417     my $parms  = $opts{user}         ? "&user=$opts{user}" : '';
418        $parms .= $opts{reverse} == 1 ? '&reverse=0'        : '&reverse=1'; 
419
420     if ($style eq 'full') {
421       my $tagLabel   = 'Tag ';
422       my $ownerLabel = 'Owner ';
423       my $typeLabel  = 'Type ';
424       my $ageLabel   = 'Age ';
425       
426       if ($opts{sortby} eq 'tag') {
427         $tagLabel .= $opts{reverse} == 1 
428                    ? img {src => 'up.png',   border => 0} 
429                    : img {src => 'down.png', border => 0}; 
430       } elsif ($opts{sortby} eq 'ownerName') {
431         $ownerLabel .= $opts{reverse} == 1 
432                      ? img {src => 'up.png',   border => 0} 
433                      : img {src => 'down.png', border => 0}; 
434       } elsif ($opts{sortby} eq 'type') {
435         $typeLabel .= $opts{reverse} == 1 
436                     ? img {src => 'up.png',   border => 0} 
437                     : img {src => 'down.png', border => 0}; 
438       } elsif ($opts{sortby} eq 'age') {
439         $ageLabel .= $opts{reverse} == 1 
440                    ? img {src => 'down.png', border => 0} 
441                    : img {src => 'up.png',   border => 0}; 
442       } # if
443       
444       $table .= th a {href => "$script?region=$opts{region}&sortby=tag$parms"},
445         $tagLabel;
446       $table .= th a {href => "$script?region=$opts{region}&sortby=ownerName$parms"},
447         $ownerLabel;
448       $table .= th a {href => "$script?region=$opts{region}&sortby=type$parms"},
449         $typeLabel;
450       $table .= th a {href => "$script?region=$opts{region}&sortby=age$parms"},
451         $ageLabel;
452     } else {
453       $table .= th 'Tag';
454       $table .= th 'Owner';
455       $table .= th 'Type';
456       $table .= th 'Age';
457     } # if
458   $table .= end_Tr;
459
460   if ($opts{sortby} eq 'age') {
461     # Sort by age numerically decending
462     @views = $opts{reverse} == 1
463            ? sort { $$a{$opts{sortby}} <=> $$b{$opts{sortby}} } @views
464            : sort { $$b{$opts{sortby}} <=> $$a{$opts{sortby}} } @views;
465   } else {
466     @views = $opts{reverse} == 1
467            ? sort { $$b{$opts{sortby}} cmp $$a{$opts{sortby}} } @views
468            : sort { $$a{$opts{sortby}} cmp $$b{$opts{sortby}} } @views;
469   } # if
470
471   my $i;
472
473   for (@views) {
474     my %view = %{$_};
475
476     next if $view{region} ne $opts{region};
477
478     my $owner = $view{owner};
479
480     if ($view{owner} =~ /\S+(\\|\/)(\S+)/) {
481       $owner = $2;
482     } # if
483
484     $owner = $view{ownerName} ? $view{ownerName} : 'Unknown';
485
486     next if $opts{user} and $owner ne $opts{user};
487
488     my $rowClass= $view{age} > $opts{ageThreshold} ? 'oldview' : 'view';
489
490     $table   .= start_Tr {
491       class => $rowClass
492     };
493       $table .= td {
494         class => 'center',
495       }, ++$i;
496       $table .= td {
497         align => 'left', 
498       }, a {
499         href => "viewdetails.cgi?tag=$view{tag}&region=$opts{region}"
500       }, $view{tag};
501       $table .= td {
502         align => 'left',
503       }, a { 
504         href => "$script?region=$opts{region}&user=$owner"
505       }, $owner;
506       $table .= td {
507         class => 'center'
508       }, font {
509         class => $view{type}
510       }, $view{type};
511       $table .= td {
512         class => 'right'
513       }, font ({
514         class => $view{type}
515       }, $view{age}, ' ', $view{ageSuffix});
516     $table .= end_Tr;
517   } # for
518
519   $table .= end_table;
520
521   return $table
522 } # FormatTable
523
524 # TODO: Add an option to remove views older than a certain date
525
526 sub EmailUser($@) {
527   my ($emailTo, @oldViews) = @_;
528
529   @oldViews = sort { $$b{age} <=> $$a{age} } @oldViews;
530
531   my $msg  = '<style>' . join("\n", ReadFile 'viewager.css') . '</style>';
532      $msg .= <<"END";
533 <h1 align="center">You have old Clearcase Views</h1>
534
535 <p>Won't you take a moment to review this message and clean up any views you no
536 longer need?</p>
537
538 <p>The following views are owned by you and have not been modified in $opts{ageThreshold}
539 days:</p>
540 END
541
542   $msg .= FormatTable 'partial', @oldViews;
543   $msg .= <<"END";
544
545 <h3>How to remove views you no longer need</h3>
546
547 <p>There are several ways to remove Clearcase views, depending on the view
548 type and the tools you are using.</p>
549
550 <blockquote>
551   <p><b>Dynamic Views</b>: If the view is a dynamic view you can use Clearcase
552   Explorer to remove the view. Find the view in your Clearcase Explorer. If
553   it's not there then add it as a standard view shortcut. Then right click on
554   the view shortcut and select <b>Remove View</b> (not <b>Remove View
555   Shortcut</b>).</p>
556
557   <p><b>Snapshot Views</b>: A snapshot view is a view who's source storage can
558   be located locally. You can remove a snapshot view in a similar manner as a
559   dynamic view, by adding it to Clearcase Explorer if not already present. By
560   doing so you need to tell Clearcase Explorer where the snapshot view storage
561   is located.</p>
562
563   <p><b>Webviews</b>: Webviews are like snapshot views but stored on the web
564   server. If you are using CCRC or the CCRC plugin to Eclipse you would select
565   the view and then do <b>Environment: Remove Clearcase View</b>.</p>
566 </blockquote>
567
568 <p>If you have any troubles removing your old views then submit a case and we
569 will be happy to assist you.</p>
570
571 <h3>But I need for my view to stay around even if it hasn't been modified</h3>
572
573 <p>If you have a long lasting view who does not get modified but needs to
574 remain, contact us and we can arrange for it to be removed from consideration
575 which will stop it from being reported as old.</p>
576
577 <p>Thanks.</p>
578 -- <br>
579 Your friendly Clearcase Administrator
580 END
581  
582   mail(
583     to          => $emailTo,
584 #    to          => 'Andrew@DeFaria.com',
585     mode        => 'html',
586     subject     => 'Old views',
587     data        => $msg,
588   );
589   
590   return
591 } # EmailUser
592
593 sub EmailUsers(@) {
594   my (@views) = @_;
595   
596   @views = sort { $$a{ownerName} cmp $$b{ownerName} } @views;
597
598   my @userViews;
599   my $currUser = $views [0]->{ownerName};
600
601   for (@views) {
602     my %view = %{$_};
603
604     next unless $view{email};
605
606     if ($currUser ne $view{ownerName}) {
607       EmailUser $view{email}, @userViews if @userViews;
608
609       $currUser = $view{ownerName};
610
611       @userViews = ();
612     } else {
613       if ($view{age} > $opts{ageThreshold}) {
614         push @userViews, \%view
615           if !-f "$view{gpath}/ageless";
616       } # if
617     } # if
618   } # for
619
620   display"Done";
621   
622   return;
623 } # EmailUsers
624
625 # Main
626 GetOptions(
627   \%opts,
628   'usage'        => sub { Usage },
629   'verbose'      => sub { set_verbose },
630   'debug'        => sub { set_debug },
631   'region=s',
632   'sortby=s',
633   'action=s',
634   'email',
635   'ageThreshold=i',
636   'nbrThreshold=i',
637 ) or Usage "Invalid parameter";
638
639 # Get options from CGI
640 my %CGIOpts = Vars;
641
642 $opts{$_} = $CGIOpts{$_} for keys %CGIOpts;
643
644 local $| = 1;
645
646 # Announce ourselves
647 verbose "$FindBin::Script v$VERSION";
648
649 $clearadm = Clearadm->new;
650
651 if ($opts{action} and $opts{action} eq 'generate') {
652   Generate $opts{region};
653   Stats \%total if $opts{verbose};
654 } else {
655   if ($opts{region} and ($opts{region} eq 'Clearcase not installed')) {
656     heading;
657     displayError $opts{region};
658     footing;
659     exit 1; 
660   } # if
661   
662   $opts{region} ||= $Clearcase::CC->region;
663
664   my @views = $clearadm->FindView(
665     $opts{tag},
666     $opts{region},
667     $opts{user}
668   );
669   
670   if ($opts{action} and $opts{action} eq 'report') {
671     Report @views;
672     Stats \%total;
673   } elsif ($email) {
674     EmailUsers @views;
675   } else {
676     heading $subtitle;
677
678     display h1 {
679       -class => 'center',
680     }, $subtitle;
681
682     display FormatTable 'full', @views;
683
684     footing;
685   } # if
686 } # if
687
688 =pod
689
690 =head1 CONFIGURATION AND ENVIRONMENT
691
692 DEBUG: If set then $debug is set to this level.
693
694 VERBOSE: If set then $verbose is set to this level.
695
696 TRACE: If set then $trace is set to this level.
697
698 =head1 DEPENDENCIES
699
700 =head2 Perl Modules
701
702 L<CGI>
703
704 L<CGI::Carp|CGI::Carp>
705
706 L<Data::Dumper|Data::Dumper>
707
708 L<File::stat|File::stat>
709
710 L<FindBin>
711
712 L<Getopt::Long|Getopt::Long>
713
714 L<Time::localtime|Time::localtime>
715
716 =head2 ClearSCM Perl Modules
717
718 =begin man 
719
720  Clearadm
721  ClearadmWeb
722  Clearcase
723  Clearcase::View
724  Clearcase::Views
725  DateUtils
726  Display
727  Mail
728  Utils
729
730 =end man
731
732 =begin html
733
734 <blockquote>
735 <a href="http://clearscm.com/php/scm_man.php?file=clearadm/lib/Clearadm.pm">Clearadm</a><br>
736 <a href="http://clearscm.com/php/scm_man.php?file=clearadm/lib/ClearadmWeb.pm">ClearadmWeb</a><br>
737 <a href="http://clearscm.com/php/scm_man.php?file=lib/Clearcase.pm">Clearcase</a><br>
738 <a href="http://clearscm.com/php/scm_man.php?file=lib/Clearcase/View.pm">Clearcase::View</a><br>
739 <a href="http://clearscm.com/php/scm_man.php?file=lib/Clearcase/Views.pm">Clearcase::Views</a><br>
740 <a href="http://clearscm.com/php/scm_man.php?file=lib/DateUtils.pm">DateUtils</a><br>
741 <a href="http://clearscm.com/php/scm_man.php?file=lib/Display.pm">Display</a><br>
742 <a href="http://clearscm.com/php/scm_man.php?file=lib/Mail.pm">Mail</a><br>
743 <a href="http://clearscm.com/php/scm_man.php?file=lib/Utils.pm">Utils</a><br>
744 <a href="http://clearscm.com/php/scm_man.php?file=clearadm/lib/User.pm">User</a><br>
745 </blockquote>
746
747 =end html
748
749 =head1 BUGS AND LIMITATIONS
750
751 There are no known bugs in this script
752
753 Please report problems to Andrew DeFaria <Andrew@ClearSCM.com>.
754
755 =head1 LICENSE AND COPYRIGHT
756
757 Copyright (c) 2010, ClearSCM, Inc. All rights reserved.
758
759 =cut