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