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