9ac1b1e71b15107317cdbc116edec19f344cbf73
[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 ||= 'Unknown';
212
213     my $user;
214
215     my $ownerid = $view->owner;
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     #} else {
243     #  $modified_date = 'Unknown';
244     } # if
245
246     my %oldView = $clearadm->GetView($view->tag, $view->region);
247
248     my ($err, $msg);
249
250     my %viewRec = (
251       system    => $view->shost,
252       region    => $view->region,
253       tag       => $view->tag,
254       owner     => $ownerid,
255       ownerName => $user->{name},
256       email     => $user->{email},
257       type      => $type,
258       gpath     => $gpath,
259       age       => $age,
260       ageSuffix => $ageSuffix,
261     );
262
263     # Some views have not yet been modified
264     $viewRec{modified} = $modified_date if $modified_date;
265
266     if (%oldView) {
267       ($err, $msg) = $clearadm->UpdateView($view->tag, $view->region, %viewRec);
268
269       error "Unable to update view $name in Clearadm\n$msg", $err if $err;
270     } else {
271       ($err, $msg) = $clearadm->AddView (%viewRec);
272
273       error "Unable to add view $name to Clearadm\n$msg", $err if $err;
274     } # if
275   } # for
276
277   verbose "\nProcessed region $region";
278   
279   return;
280 } # GenerateRegion
281
282 sub Generate ($) {
283   my ($region) = @_;
284
285   if ($region =~ /all/i) {
286     for ($Clearcase::CC->regions) {
287       GenerateRegion $_;
288     } # for
289   } else {
290     GenerateRegion $region;
291   } # if
292   
293   return;
294 } # Generate
295
296 sub Report (@) {
297   my (@views) = @_;
298
299   $total{'Views processed'} = @views;
300
301   my @sortedViews;
302
303   if ($opts{sortby} eq 'age') {
304     # Sort by age numerically decending
305     @sortedViews = sort { $$b{$opts{sortby}} <=> $$a{$opts{sortby}} } @views;
306   } else {
307     @sortedViews = sort { $$a{$opts{sortby}} cmp $$b{$opts{sortby}} } @views;
308   } # if
309
310   $total{Reported} = 0;
311
312   for (@sortedViews) {
313     my %view = %{$_};
314
315     last
316       if ($nbrThreshold and $total{Reported} + 1 > $nbrThreshold) or
317          ($view{age} < $opts{ageThreshold});
318
319     $total{Reported}++;
320
321     if ($view{type}) {
322       if ($view{type} eq 'dynamic') {
323         $total{Dynamic}++;
324       } elsif ($view{type} eq 'snapshot') {
325         $total{Snapshot}++;
326       } elsif ($view{type} eq 'webview') {
327         $total{Webview}++
328       } else {
329         $total{$view{type}}++;
330       } # if
331     } else {
332       $total{Unknown}++;
333     } # if
334
335 format STDOUT_TOP =
336             View Name                         Owner           View Type   Last Modified      Age
337 ------------------------------------- ---------------------- ----------- ---------------- -----------
338 .
339 format STDOUT =
340 @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<<<<< @>>>> @<<<<
341 $view{tag},$view{owner},$view{type},$view{modified},$view{age},$view{ageSuffix}
342 .
343
344     write;
345   } # for
346   
347   return;
348 } # Report
349
350 sub FormatTable ($@) {
351   my ($style, @views) = @_;
352   
353   my $table;
354
355   my $nbrViews = @views;
356   
357   my $legend =
358     font ({-class => 'label'}, 'View type: ') .
359     font ({-class => 'dynamic'}, 'Dyanmic') .
360     ' ' .
361     font ({-class => 'snapshot'}, 'Snapshot') .
362     ' ' .
363     font ({-class => 'web'}, 'Web') .
364     ' ' .
365     font ({-class => 'unknown'}, 'Unknown');
366
367   my $caption;
368
369   my $regionDropdown = start_form (
370     -action => $script,
371   );
372
373   $regionDropdown .= font {-class => 'captionLabel'}, 'Region: ';
374   $regionDropdown .= popup_menu (
375     -name     => 'region',
376     -values   => [$Clearcase::CC->regions],
377     -default  => $Clearcase::CC->region,
378     -onchange => 'submit();',
379   );
380
381   $regionDropdown .= end_form;
382
383   $caption .= start_table {
384     class        => 'caption',
385     cellspacing  => 1,
386     width        => '100%',
387   };
388
389   $caption   .= start_Tr;
390     $caption .= td {
391        -align => 'left',
392        -width => '30%',
393     }, font ({-class => 'label'}, 'Registry: '),
394        setField($Clearcase::CC->registry_host), '<br>',
395        font ({-class => 'label'}, 'Views: '),
396        $nbrViews;
397     $caption .= td {
398       -align => 'center',
399       -width => '40%',
400     }, $legend;
401     $caption .= td {
402       -align => 'right',
403       -width => '30%',
404     }, $regionDropdown;
405   $caption .= end_Tr; 
406
407   $caption .= end_table;
408
409   $table .= start_table {
410     cellspacing => 1,
411     width       => '75%',
412   };
413
414   $table   .= caption $caption;
415   $table   .= start_Tr {-class => 'heading'};
416     $table .= th '#';
417
418     # Set defaults if not set already
419     $opts{sortby}  ||= 'age';
420     $opts{reverse} ||= 0;
421     
422     my $parms  = $opts{user}         ? "&user=$opts{user}" : '';
423        $parms .= $opts{reverse} == 1 ? '&reverse=0'        : '&reverse=1'; 
424
425     if ($style eq 'full') {
426       my $tagLabel   = 'Tag ';
427       my $ownerLabel = 'Owner ';
428       my $typeLabel  = 'Type ';
429       my $ageLabel   = 'Age ';
430       
431       if ($opts{sortby} eq 'tag') {
432         $tagLabel .= $opts{reverse} == 1 
433                    ? img {src => 'up.png',   border => 0} 
434                    : img {src => 'down.png', border => 0}; 
435       } elsif ($opts{sortby} eq 'ownerName') {
436         $ownerLabel .= $opts{reverse} == 1 
437                      ? img {src => 'up.png',   border => 0} 
438                      : img {src => 'down.png', border => 0}; 
439       } elsif ($opts{sortby} eq 'type') {
440         $typeLabel .= $opts{reverse} == 1 
441                     ? img {src => 'up.png',   border => 0} 
442                     : img {src => 'down.png', border => 0}; 
443       } elsif ($opts{sortby} eq 'age') {
444         $ageLabel .= $opts{reverse} == 1 
445                    ? img {src => 'down.png', border => 0} 
446                    : img {src => 'up.png',   border => 0}; 
447       } # if
448       
449       $table .= th a {href => "$script?region=$opts{region}&sortby=tag$parms"},
450         $tagLabel;
451       $table .= th a {href => "$script?region=$opts{region}&sortby=ownerName$parms"},
452         $ownerLabel;
453       $table .= th a {href => "$script?region=$opts{region}&sortby=type$parms"},
454         $typeLabel;
455       $table .= th a {href => "$script?region=$opts{region}&sortby=age$parms"},
456         $ageLabel;
457     } else {
458       $table .= th 'Tag';
459       $table .= th 'Owner';
460       $table .= th 'Type';
461       $table .= th 'Age';
462     } # if
463   $table .= end_Tr;
464
465   if ($opts{sortby} eq 'age') {
466     # Sort by age numerically decending
467     @views = $opts{reverse} == 1
468            ? sort { $$a{$opts{sortby}} <=> $$b{$opts{sortby}} } @views
469            : sort { $$b{$opts{sortby}} <=> $$a{$opts{sortby}} } @views;
470   } else {
471     @views = $opts{reverse} == 1
472            ? sort { $$b{$opts{sortby}} cmp $$a{$opts{sortby}} } @views
473            : sort { $$a{$opts{sortby}} cmp $$b{$opts{sortby}} } @views;
474   } # if
475
476   my $i;
477
478   for (@views) {
479     my %view = %{$_};
480
481     next if $view{region} ne $opts{region};
482
483     my $owner = $view{owner};
484
485     if ($view{owner} =~ /\S+(\\|\/)(\S+)/) {
486       $owner = $2;
487     } # if
488
489     $owner = $view{ownerName} ? $view{ownerName} : 'Unknown';
490
491     next if $opts{user} and $owner ne $opts{user};
492
493     my $rowClass= $view{age} > $opts{ageThreshold} ? 'oldview' : 'view';
494
495     $table   .= start_Tr {
496       class => $rowClass
497     };
498       $table .= td {
499         class => 'center',
500       }, ++$i;
501       $table .= td {
502         align => 'left', 
503       }, a {
504         href => "viewdetails.cgi?tag=$view{tag}&region=$opts{region}"
505       }, $view{tag};
506       $table .= td {
507         align => 'left',
508       }, a { 
509         href => "$script?region=$opts{region}&user=$owner"
510       }, $owner;
511       $table .= td {
512         class => 'center'
513       }, font {
514         class => $view{type}
515       }, $view{type};
516       $table .= td {
517         class => 'right'
518       }, font ({
519         class => $view{type}
520       }, $view{age}, ' ', $view{ageSuffix});
521     $table .= end_Tr;
522   } # for
523
524   $table .= end_table;
525
526   return $table
527 } # FormatTable
528
529 # TODO: Add an option to remove views older than a certain date
530
531 sub EmailUser ($@) {
532   my ($emailTo, @oldViews) = @_;
533
534   @oldViews = sort { $$b{age} <=> $$a{age} } @oldViews;
535
536   my $msg  = '<style>' . join ("\n", ReadFile 'viewager.css') . '</style>';
537      $msg .= <<"END";
538 <h1 align="center">You have old Clearcase Views</h1>
539
540 <p>Won't you take a moment to review this message and clean up any views you no
541 longer need?</p>
542
543 <p>The following views are owned by you and have not been modified in $opts{ageThreshold}
544 days:</p>
545 END
546
547   $msg .= FormatTable 'partial', @oldViews;
548   $msg .= <<"END";
549
550 <h3>How to remove views you no longer need</h3>
551
552 <p>There are several ways to remove Clearcase views, depending on the view
553 type and the tools you are using.</p>
554
555 <blockquote>
556   <p><b>Dynamic Views</b>: If the view is a dynamic view you can use Clearcase
557   Explorer to remove the view. Find the view in your Clearcase Explorer. If
558   it's not there then add it as a standard view shortcut. Then right click on
559   the view shortcut and select <b>Remove View</b> (not <b>Remove View
560   Shortcut</b>).</p>
561
562   <p><b>Snapshot Views</b>: A snapshot view is a view who's source storage can
563   be located locally. You can remove a snapshot view in a similar manner as a
564   dynamic view, by adding it to Clearcase Explorer if not already present. By
565   doing so you need to tell Clearcase Explorer where the snapshot view storage
566   is located.</p>
567
568   <p><b>Webviews</b>: Webviews are like snapshot views but stored on the web
569   server. If you are using CCRC or the CCRC plugin to Eclipse you would select
570   the view and then do <b>Environment: Remove Clearcase View</b>.</p>
571 </blockquote>
572
573 <p>If you have any troubles removing your old views then submit a case and we
574 will be happy to assist you.</p>
575
576 <h3>But I need for my view to stay around even if it hasn't been modified</h3>
577
578 <p>If you have a long lasting view who does not get modified but needs to
579 remain, contact us and we can arrange for it to be removed from consideration
580 which will stop it from being reported as old.</p>
581
582 <p>Thanks.</p>
583 -- <br>
584 Your friendly Clearcase Administrator
585 END
586  
587   mail (
588     to          => $emailTo,
589 #    to          => 'Andrew@DeFaria.com',
590     mode        => 'html',
591     subject     => 'Old views',
592     data        => $msg,
593   );
594   
595   return
596 } # EmailUser
597
598 sub EmailUsers (@) {
599   my (@views) = @_;
600   
601   @views = sort { $$a{ownerName} cmp $$b{ownerName} } @views;
602
603   my @userViews;
604   my $currUser = $views [0]->{ownerName};
605
606   for (@views) {
607     my %view = %{$_};
608
609     next
610       unless $view{email};
611
612     if ($currUser ne $view{ownerName}) {
613       EmailUser $view{email}, @userViews
614         if @userViews;
615
616       $currUser = $view{ownerName};
617
618       @userViews =();
619     } else {
620       if ($view{age} > $opts{ageThreshold}) {
621         push @userViews, \%view
622           if !-f "$view{gpath}/ageless";
623       } # if
624     } # if
625   } # for
626
627   display"Done";
628   
629   return;
630 } # EmailUsers
631
632 # Main
633 GetOptions (
634   \%opts,
635   'usage'        => sub { Usage },
636   'verbose'      => sub { set_verbose },
637   'debug'        => sub { set_debug },
638   'region=s',
639   'sortby=s',
640   'action=s',
641   'email',
642   'ageThreshold=i',
643   'nbrThreshold=i',
644 ) or Usage "Invalid parameter";
645
646 # Get options from CGI
647 my %CGIOpts = Vars;
648
649 $opts{$_} = $CGIOpts{$_} for keys %CGIOpts;
650
651 local $| = 1;
652
653 # Announce ourselves
654 verbose "$FindBin::Script v$VERSION";
655
656 $clearadm = Clearadm->new;
657
658 if ($opts{action} and $opts{action} eq 'generate') {
659   $opts{region} ||= 'all';
660
661   Generate $opts{region};
662   Stats \%total if $opts{verbose};
663 } else {
664   if ($opts{region} and ($opts{region} eq 'Clearcase not installed')) {
665     heading;
666     displayError $opts{region};
667     footing;
668     exit 1; 
669   } # if
670   
671   $opts{region} ||= $Clearcase::CC->region;
672
673   my @views = $clearadm->FindView (
674     'all',
675     $opts{region},
676     $opts{tag},
677     $opts{user}
678   );
679   
680   if ($opts{action} and $opts{action} eq 'report') {
681     Report @views;
682     Stats \%total;
683   } elsif ($email) {
684     EmailUsers @views;
685   } else {
686     heading $subtitle;
687
688     display h1 {
689       -class => 'center',
690     }, $subtitle;
691
692     display FormatTable 'full', @views;
693
694     footing;
695   } # if
696 } # if
697
698 =pod
699
700 =head1 CONFIGURATION AND ENVIRONMENT
701
702 DEBUG: If set then $debug is set to this level.
703
704 VERBOSE: If set then $verbose is set to this level.
705
706 TRACE: If set then $trace is set to this level.
707
708 =head1 DEPENDENCIES
709
710 =head2 Perl Modules
711
712 L<CGI>
713
714 L<CGI::Carp|CGI::Carp>
715
716 L<Data::Dumper|Data::Dumper>
717
718 L<File::stat|File::stat>
719
720 L<FindBin>
721
722 L<Getopt::Long|Getopt::Long>
723
724 L<Time::localtime|Time::localtime>
725
726 =head2 ClearSCM Perl Modules
727
728 =begin man 
729
730  Clearadm
731  ClearadmWeb
732  Clearcase
733  Clearcase::View
734  Clearcase::Views
735  DateUtils
736  Display
737  Mail
738  Utils
739
740 =end man
741
742 =begin html
743
744 <blockquote>
745 <a href="http://clearscm.com/php/scm_man.php?file=clearadm/lib/Clearadm.pm">Clearadm</a><br>
746 <a href="http://clearscm.com/php/scm_man.php?file=clearadm/lib/ClearadmWeb.pm">ClearadmWeb</a><br>
747 <a href="http://clearscm.com/php/scm_man.php?file=lib/Clearcase.pm">Clearcase</a><br>
748 <a href="http://clearscm.com/php/scm_man.php?file=lib/Clearcase/View.pm">Clearcase::View</a><br>
749 <a href="http://clearscm.com/php/scm_man.php?file=lib/Clearcase/Views.pm">Clearcase::Views</a><br>
750 <a href="http://clearscm.com/php/scm_man.php?file=lib/DateUtils.pm">DateUtils</a><br>
751 <a href="http://clearscm.com/php/scm_man.php?file=lib/Display.pm">Display</a><br>
752 <a href="http://clearscm.com/php/scm_man.php?file=lib/Mail.pm">Mail</a><br>
753 <a href="http://clearscm.com/php/scm_man.php?file=lib/Utils.pm">Utils</a><br>
754 <a href="http://clearscm.com/php/scm_man.php?file=clearadm/lib/User.pm">User</a><br>
755 </blockquote>
756
757 =end html
758
759 =head1 BUGS AND LIMITATIONS
760
761 There are no known bugs in this script
762
763 Please report problems to Andrew DeFaria <Andrew@ClearSCM.com>.
764
765 =head1 LICENSE AND COPYRIGHT
766
767 Copyright (c) 2010, ClearSCM, Inc. All rights reserved.
768
769 =cut