#! /usr/local/bin/perl -T # # Copyright (C) 2002, 2003, 2004, 2005 Christophe Kalt # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # 3. All advertising materials mentioning features or use of this software # must display the following acknowledgement: # This product includes software developed by Christophe Kalt. # 4. The name of the author may not be used to endorse or promote products # derived from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # $Id: drraw.cgi,v 1.185.2.18 2005/06/19 18:20:50 kalt Exp $ # # Home Page -- http://web.taranis.org/drraw/ # use warnings; use strict; use CGI qw(:standard :html3 *table *ul -no_xhtml -nosticky); use CGI::Carp qw(fatalsToBrowser); use Config; use Fcntl; use File::Basename; use File::Find; use POSIX qw(strftime); # The following line is needed if "RRDs.pm" was not installed in a # directory mentioned in your perl's @INC. #use lib '/usr/local/rrdtool/lib/perl'; use RRDs; # The configuration file is expected to be found in the same directory # as drraw itself. You may customize this to be elsewhere. my $config = '%%CONFDIR%%/drraw.conf'; # Untaint ############################################################################### ## STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP ## ## ## ## There should be no need for you to change any of what is below! ## ## ## ## STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP ## ############################################################################### # Configuration variable declarations, and default settings use vars qw ( $title $header $footer %datadirs $dsfilter_def @rranames %rranames $vrefresh $drefresh @dv_def @dv_name @dv_secs $gformat $maxtime $crefresh $use_rcs $saved_dir $clean_cache $tmp_dir $ERRLOG %users %Index $IndexMax $icon_new $icon_closed $icon_open $icon_text $icon_help $icon_bug $icon_link $CSS ); # Default title $title = 'Draw Round Robin Archives on the Web'; $header = ''; $footer = ''; # *.rrd and *.evt file list sorting function sub datafnsort { if ( defined(&mydatafnsort) ) { &mydatafnsort; } else { return $_[0] cmp $_[1]; } } # Default DS filter when adding graph data sources from RRD files $dsfilter_def = ''; # Names of defined RRAs and the names to use on the graphs @rranames = ( 'MIN', 'AVERAGE', 'MAX', 'LAST' ); %rranames = ( 'MIN' => 'Min', 'AVERAGE' => 'Avg', 'MAX' => 'Max', 'LAST' => 'Last' ); # Viewer automatic refresh timer (seconds) $vrefresh = '900'; # Minimum dashboard automatic refresh timer (seconds) $drefresh = 1800; # Default Views and their names @dv_def = ( 'end - 28 hours', 'end - 1 week', 'end - 1 month', 'end - 1 year' ); @dv_name = ( 'Past 28 Hours', 'Past Week', 'Past Month', 'Past Year' ); @dv_secs = ( 100800, 604800, 2419200, 31536000 ); # Default "Additonal GPRINTs" format $gformat = "%8.2lf"; # Maximum time (seconds) CGI processes may be running (0 to disable) $maxtime = 60; # Cache refresh time (seconds) $crefresh = 3600; # Where to save data and store temporary files $saved_dir = '/somewhere/drraw/saved'; $tmp_dir = '/somewhere/drraw/tmp'; # Whether or not RCS will be used $use_rcs = 0; # How often should the image cache be purged? $clean_cache = 21600; # Users %users = ( 'guest' => 2 ); my $IndexMax = 0; # Icons used in the browser $icon_new = '/icons/generic.gif'; $icon_closed = '/icons/folder.gif'; $icon_open = '/icons/folder.open.gif'; $icon_text = '/icons/text.gif'; $icon_help = '/icons/unknown.gif'; $icon_bug = '/icons/bomb.gif'; $icon_link = '/icons/link.gif'; # Style Sheet $CSS = < END # Mime Types my %Mime = ( 'PNG' => 'image/png', 'SVG' => 'image/svg+xml', 'PDF' => 'application/pdf', 'EPS' => 'application/postscript', 'GIF' => 'image/gif' ); my @ImgFormat = ( 'PNG', 'SVG', 'PDF', 'EPS' ); @ImgFormat = ( 'PNG', 'GIF' ) if ( $RRDs::VERSION < 1.2 ); # This needs to be manually set for stupid stupid File::Find to work # in tainted mode. $ENV{'PATH'} = '/bin:/usr/bin'; ############################################################################### ## STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP ## ## ## ## There should be no need for you to be subjected to the ## ## sight of the atrocious code found below these lines! ## ## ## ## STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP STOP ## ############################################################################### # Now load the user configuration unless ( do $config ) { my $err = ( $@ ne '' ) ? "$@" : "$!"; print header(-status=>500), start_html('500 Internal error'), h2("Error loading configuration file: $config"), p($err), end_html; exit 1; } # Here is the earliest we can do this if ( defined($ERRLOG) ) { # Abort violently so the user has no doubt about what's going on. open(STDERR, ">> $ERRLOG") or die "Failed to replace STDERR with \$ERRLOG($ERRLOG): $!\n"; } # Make sure that %datadirs is defined if ( !defined(%datadirs) ) { print header(-status=>500), start_html('500 Internal error'), h2("Invalid configuration: $config"), end_html; exit 1; } my $VERSION = '2.1.3'; my $REVISION = '$Revision: 1.185.2.18 $'; $REVISION =~ s/[^.0-9]+//g; my $RELEASE = '1.185.2.18'; $VERSION = 'CVS-SNAPSHOT ['. $REVISION .' post '. $VERSION .']' unless ( $REVISION eq $RELEASE ); my ( $drraw_ID, $drraw_reported ) = ( time . $$, undef ); my $DEBUG = 0; # This one should be configurable my %colors = ( 'Black' => '#000000', 'Silver' => '#C0C0C0', 'Gray' => '#808080', 'White' => '#FFFFFF', 'Maroon' => '#800000', 'Red' => '#FF0000', 'Purple' => '#800080', 'Fuchsia'=> '#FF00FF', 'Green' => '#008000', 'Lime' => '#00FF00', 'Olive' => '#808000', 'Yellow' => '#FFFF00', 'Orange' => '#FFA500', 'Pink' => '#FFC0CB', 'Navy' => '#000080', 'Blue' => '#0000FF', 'Teal' => '#008080', 'Aqua' => '#00FFFF' ); my %colorsidx = (); # Additions require changes to &DRAW() my @graphtypes = ( 'LINE1', 'LINE2', 'LINE3', 'AREA', 'STACK', 'GPRINT', 'COMMENT', 'HRULE', 'VRULE', '-Nothing-' ); my %goptions = ( 'gTitle' => '--title', 'gVLabel' => '--vertical-label', 'gBase' => '--base', 'gUExp' => '--units-exponent', 'gWidth' => '--width', 'gHeight' => '--height', 'gYUp' => '--upper-limit', 'gYLow' => '--lower-limit', 'gFormat' => '--imgformat'); my $drrawhome = hr . p({-align=>'right'}, 'Brought to you by ', a({-href=>'http://web.taranis.org/drraw/'}, em('drraw')), '(version ' . $VERSION . ')'); # Little bit of JavaScript my $IndexJS = < function IndexClick(srcElement) { var iconElement, targetElement; iconElement = document.getElementById(srcElement.id + "-icon"); targetElement = document.getElementById(srcElement.id + "-child"); if (targetElement.style.display == "none") { targetElement.style.display = ""; iconElement.src = "$icon_open"; } else { targetElement.style.display = "none"; iconElement.src = "$icon_closed"; } } END my $ViewerJS = < var ViewerStart; var ViewerRefresh; function ViewerCountdown(target) { if (!ViewerStart || !ViewerRefresh) return; var targetElement; targetElement = document.getElementById(target); if (!targetElement) return; now = new Date(); elapsed = ( now - ViewerStart ) / 1000; if (elapsed > ViewerRefresh + 30) { if ((elapsed - ViewerRefresh) % 2 < 1) { targetElement.innerHTML = "Refresh failed, hit Reload"; } else { targetElement.innerHTML = "Refresh failed, hit Reload"; } setTimeout("ViewerCountdown(thetarget)", 1000); } else if (elapsed > ViewerRefresh-1) { targetElement.innerHTML = "Refreshing..."; setTimeout("ViewerCountdown(thetarget)", 10000); } else { with (Math) { min = floor((ViewerRefresh - elapsed) / 60); sec = floor((ViewerRefresh - elapsed) % 60); if (min < 1) { if (sec < 30 && sec % 2 == 0) targetElement.innerHTML = "Refreshing in "+ sec +"s"; else targetElement.innerHTML = "Refreshing in "+ sec +"s"; } else targetElement.innerHTML = "Refreshing in "+ min +"m "+ sec +"s"; } thetarget=target; setTimeout("ViewerCountdown(thetarget)", 1000); } } END my $EditorJS = < // This should be callable onchange rather than onclick // BUT Opera 7.23 does not implement this at all, and IE 6.0 handles it poorly. function ShowHideChange(srcElement, target) { var targetElement = document.getElementById(target); if (!targetElement) return; if (srcElement.value == "Show") { targetElement.style.display = ""; } else if (srcElement.value == "Hide") { targetElement.style.display = "none"; } } function DisableToggle(target) { var DelElement = document.forms.Editor.elements.namedItem(target +"_DELETE"); if (!DelElement) return; var anElement; var i = 0; while (anElement = document.forms.Editor.elements.item(i++)) { if (anElement.name.split("_", 1)[0] == target && anElement != DelElement) anElement.disabled = DelElement.checked; } } function CheckBgColor(src, target) { var targetElement = document.getElementById(target); if (!targetElement) return; if ( src.value == "PRINT" || src.value == "GPRINT" || src.value == "COMMENT" || src.value == "-Nothing-" ) targetElement.bgColor = "white"; } function SetBgColor(src, target) { var targetElement = document.getElementById(target); if (!targetElement) return; if (!colors) return; var i = 0; while (i < colors.length) { if (colors[i++] == src.value) targetElement.bgColor = colors[i]; i++; } } function ToggleXTRA(target) { var UserChoice; var i = 0; while (UserChoice = document.forms.Editor.XTRAS.item(i++)) { if (UserChoice.checked) break; } if (!UserChoice) return; var anElement; i = 0; while (anElement = document.forms.Editor.elements.item(i++)) { if ((anElement.name.split("_", 2)[1] == "XTRAS" && anElement.value == target) || (anElement.name.split("_", 2)[1] == "BR" && target == "BR")) { if (UserChoice.value == "On") anElement.checked = true; else if (UserChoice.value == "Off") anElement.checked = false; else if (UserChoice.value == "Not") anElement.checked = !anElement.checked; } } } END my ( @rrdfiles, @evtfiles ) = ( (), () ); my ( %GraphsById, %TemplatesById, %BoardsById ) = ( (), (), () ); my %DS = (); my ( %TMPL, %DBTMPL ) = ( (), () ); # # Here's where the fun really starts. # However, most of the code is in subroutines below. # $| = 1; # Make sure CGI succeeded.. my $error = cgi_error; if ($error) { print header(-status=>$error), start_html('Problems'), $header, h2('Request not processed'), strong($error), $footer, end_html; exit 1; } # We really can't do anything useful without these directories if ( ! -d "${tmp_dir}" || ! -d "${saved_dir}" || ! -w "${tmp_dir}" || ! -w "${saved_dir}" ) { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), h3('Configuration problem!'), p("The directories ${tmp_dir} and ${saved_dir} MUST exist and be writable by the CGI user!"), $footer, end_html; exit 1; } # Check whether RCS is available if ( $use_rcs && -d "${saved_dir}/RCS" && -w "${saved_dir}/RCS" ) { if ( $use_rcs == 1 ) { eval { require Rcs; import Rcs qw(nonFatal); }; } else { eval { require Rcs; import Rcs qw(nonFatal Verbose); }; } if ( $@ ) { warn "Unable to load Rcs.pm: $@\n"; $use_rcs = 0; } } else { $use_rcs = 0; } # Check whether MD5 is available for caching if ( $clean_cache ) { eval { require Digest::MD5; import Digest::MD5; }; $clean_cache = 0 if ( $@ ); } # Is this for an authenticated user? my $user; $user = &mygetuser if ( defined(&mygetuser) ); $user = $ENV{'REMOTE_USER'} if ( !defined($user) && defined($ENV{'REMOTE_USER'}) ); $user = 'guest' if ( !defined($user) ); # Nope, let's default to "guest" # Ok, we now have a username, what access should they get? my $level; if ( defined($users{$user}) ) { # User with custom level, including guest. $level = $users{$user}; } else { # Authenticated user defaults level 2 access $level = 2; # But let's bomb out if stupidity seems to be involved! die "SECURITY ALERT: 'guest' user entry is missing from %users configuration!\n" if ( $user eq 'guest' ); } # Setup alarm timer to catch endless loops and other such problems $SIG{'ALRM'} = sub { croak "Ran out of time, aborting! (Consider raising \$maxtime [$maxtime])"; }; alarm($maxtime); my @pnames = param(); if ( scalar(@pnames) == 0 || defined(param('Browse')) ) { # # The graph/template browser pages, by default. # print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr; # Browse=Help -> Display help page if ( defined(param('Browse')) && param('Browse') eq 'Help' ) { &HELP; exit 0; } # Browse=Log -> Display log page if ( defined(param('Browse')) && param('Browse') eq 'Log' ) { &ShowLog; exit 0; } # Browse=Rcs -> Display RCS log page if ( defined(param('Browse')) && param('Browse') eq 'Rcs' ) { &ShowRcsLog; exit 0; } if ( $level > 0 ) { print a({-href=>MakeURL('USERSAID', 'New')}, img({-src=>$icon_new, -border=>0}), ' Create a new graph'), br; } &Indexes_Load; if ( $level > 0 ) { print a({-href=>MakeURL('USERSAID', 'New', 'Type', 'DB')}, img({-src=>$icon_new, -border=>0}), ' Define a new dashboard'),br if ( scalar(keys(%GraphsById))+scalar(keys(%TemplatesById)) >0 ); } print $IndexJS; &CustomIndex(\%Index, '') if ( defined(%Index) ); if ( defined(param('Browse')) && param('Browse') eq 'AllGraphs' ) { print '
', img({-src=>$icon_open, -border=>0, -id=>'AllGraphs-icon'}), ' All Graphs (' . scalar(keys(%GraphsById)) . ')', '
', '
', '
', start_table{-width=>'100%'}; &GraphIndex('.'); print end_table, '
'; } else { print a({-href=>MakeURL('Browse', 'AllGraphs')}, img({-src=>$icon_closed, -border=>0}), ' All Graphs (' . scalar(keys(%GraphsById)) . ')'), br; } if ( defined(param('Browse')) && param('Browse') eq 'AllTemplates' ) { # Templates list print '
', img({-src=>$icon_open, -border=>0, -id=>'AllTemplates-icon'}), ' All Templates (' . scalar(keys(%TemplatesById)) . ')', '
', '
', '
', start_table{-width=>'100%'}; &Cache_Load; &TemplateIndex('.'); print end_table, '
'; } else { print a({-href=>MakeURL('Browse', 'AllTemplates')}, img({-src=>$icon_closed, -border=>0}), ' All Templates (' . scalar(keys(%TemplatesById)) . ')'), br; } if ( defined(param('Browse')) && param('Browse') eq 'AllDashboards' ) { # Dashboards list print '
', img({-src=>$icon_open, -border=>0, -id=>'AllDashboards-icon'}), ' All Dashboards (' . scalar(keys(%BoardsById)) . ')', '
', '
', '
', start_table{-width=>'100%'}; &Cache_Load; &DashBoardIndex('.'); print end_table, '
'; } else { print a({-href=>MakeURL('Browse', 'AllDashboards')}, img({-src=>$icon_closed, -border=>0}), ' All Dashboards (' . scalar(keys(%BoardsById)) . ')'), br; } if ( $level > 0 ) { # Link to log page print a({-href=>MakeURL('Browse', 'Log')}, img({-src=>$icon_text, -border=>0}), ' ChangeLog'), br; # Link to help page print a({-target=>'drrawhelp', -href=>MakeURL('Browse', 'Help')}, img({-src=>$icon_help, -border=>0}), ' Help'), br; } if ( $level > 1 ) { # For level 2 users, # "Report a bug" link # "Add this installation to the compatibility report" link print a({-target=>'drrawhelp', href=>MakeURL('Browse', 'Help') . '#contact'}, img({-src=>$icon_bug, -border=>0}), ' Report a bug, ...'), br; $drraw_ID = param('Reported') if ( defined(param('Reported')) ); my $query = new CGI; $query->delete_all; $query->param('drrawver', "$VERSION"); $query->param('perlver', "$]"); $query->param('cgiver', "$CGI::VERSION"); $query->param('rrdver', "$RRDs::VERSION"); $query->param('osname', "$^O"); $query->param('websvr', $ENV{'SERVER_SOFTWARE'}); $query->param('id', $drraw_ID); my $status; if ( defined(param('Reported')) ) { $drraw_reported = $query->query_string(); &Indexes_Save('drraw', $drraw_ID, $drraw_reported); } else { if ( defined($drraw_reported) ) { $status = b("out of date") if ( $drraw_reported ne $query->query_string() ); } else { $status = b("unreported"); } $query->param('url', url()); print a({-href=>"http://web.taranis.org/cgi-bin/drraw-report.pl?" . $query->query_string()}, img({-src=>$icon_link, -border=>0}), ' Add this installation to the compatibility report.'), ' Status: '. $status if ( defined($status) && scalar(keys(%GraphsById)) + scalar(keys(%TemplatesById)) > 5 && $REVISION eq $RELEASE ); } } print $drrawhome, $footer, end_html; } elsif ( (defined(param('Graph')) || defined(param('Template')) || defined(param('Dashboard')) ) && defined(param('Mode')) ) { # # Displaying, without any editing.. # if ( param('Mode') eq 'view' ) { # Mode=view -> HTML page &Indexes_Load; # Make sure that whatever was requested exists.. if ( defined(param('Graph')) && !defined($GraphsById{param('Graph')}) ) { print header, start_html({-style=>{-code=>$CSS}, -title=>'drraw - '. $title}), $header, h1('Invalid Request.'), p('Sorry, the graph you requested (' . param('Graph') . ') does not appear in the index.'); } elsif ( defined(param('Template')) && !defined($TemplatesById{param('Template')}) ) { print header, start_html({-style=>{-code=>$CSS}, -title=>'drraw - '. $title}), $header, h1('Invalid Request.'), p('Sorry, the template you requested (' . param('Template') . ') does not appear in the index.'); } elsif ( defined(param('Template')) && !defined(param('Base')) ) { # Template, but no base specified, display choices on a single page &Cache_Load; &TMPLFind($TemplatesById{param('Template')}{'Filter'}, $TemplatesById{param('Template')}{'Display'}); print header, start_html({-style=>{-code=>$CSS}, -title=>'drraw - ' . $TemplatesById{param('Template')}{'Name'}}), $header, h1($TemplatesById{param('Template')}{'Name'}), start_form({-align=>'center', -method=>'GET'}), '\n", p({-align=>'center'}, scrolling_list(-name=>'Base', -values=>[sort { $TMPL{$a} cmp $TMPL{$b} } keys(%TMPL)], -labels=>\%TMPL, -size=>(scalar(keys(%TMPL)) < 25) ? scalar(keys(%TMPL)) : 25), submit(-name=>'Mode', -value=>'view', -valign=>'center')), end_form; } elsif ( defined(param('Dashboard')) && !defined($BoardsById{param('Dashboard')}) ) { print header, start_html({-style=>{-code=>$CSS}, -title=>'drraw - '. $title}), $header, h1('Invalid Request.'), p('Sorry, the dashboard you requested (' . param('Dashboard') . ') does not appear in the index.'); } elsif ( defined(param('Dashboard')) ) { # Dashboard template, but no base specified, display choices # on a single page &Cache_Load; &BoardFind(param('Dashboard')); if ( scalar(keys(%DBTMPL)) > 0 && !defined(param('Base')) ) { print header, start_html({-style=>{-code=>$CSS}, -title=>'drraw - ' . $BoardsById{param('Dashboard')}{'Name'}}), $header, h1($BoardsById{param('Dashboard')}{'Name'}), p({-align=>'center'}, start_form({-align=>'center'}, -method=>'GET'), '\n", scrolling_list(-name=>'Base', -values=>[sort keys(%DBTMPL)], -size=>(scalar(keys(%DBTMPL)) < 25) ? scalar(keys(%DBTMPL)) : 25), submit(-name=>'Mode', -value=>'view', -valign=>'center'), end_form); } else { # # Dashboard display page # # Save what we need later on my $board = param('Dashboard'); my $base = param('Base'); my $filter = ( defined(param('Filter')) ) ? param('Filter'):''; my $view = param('View'); my $start = param('Start'); my $end = param('End'); # Load dashboard definition &Definition_Load('d' . param('Dashboard')); # Find out which view to use if ( !defined($view) ) { $view = 0; while ( $view < scalar(@dv_name) ) { last if ( $dv_name[$view] eq param('dView') ); $view += 1; } $view = 0 if ( $view >= scalar(@dv_name) ); } param(-name=>'View', -value=>$view); if ( !defined($start) || !defined($end) ) { param(-name=>'Start', -value=>$dv_def[$view]); param(-name=>'End', -value=>'now'); } else { param(-name=>'Start', -value=>$start); param(-name=>'End', -value=>$end); $view = -1; } # Dashboards are somewhat special in the sense that they will # potentially cause _many_ graphs to be drawn. As this is # typically done for relatively long period of times and # small(er) images, there really is little point in having a # short page refresh to get many cached images, so let's spare # the server. my ( $startts, $endts ) = ( 0, time ); my $ttl = 1800; # 30m Default for dashboards if ( defined(&RRDs::times) ) { ( $startts, $endts ) = RRDs::times(param('Start'), param('End')); die "Invalid result ($startts) from RRDs::times()\n" unless ( $startts =~ /(\d+)/ ); $startts = $1; # Untaint die "Invalid result ($endts) from RRDs::times()\n" unless ( $endts =~ /(\d+)/ ); $endts = $1; # Untaint if ( defined(param('dWidth')) && param('dWidth') =~ /(\d+)/ ) { $ttl = int(($endts - $startts + 1) / $1); # Untaint } else { $ttl = int(($endts - $startts + 1) / 400 ); } } else { if ( $view < scalar(@dv_secs) ) { if ( defined(param('dWidth')) && param('dWidth') =~ /(\d+)/ ) { $ttl = int($dv_secs[$view] / $1); # Untaint } else { $ttl = int($dv_secs[$view] / 400); } } } # The above calculations can result in short refresh times # depends on how sensible the configuration and user settings # are. $ttl = $drefresh unless ( $ttl > $drefresh ); # Header print header, start_html({-style=>{-code=>$CSS}, -title=>'drraw - ' . $BoardsById{$board}{'Name'}, -head=>meta({-http_equiv=>'Refresh', -content=>$ttl})}), $header, $ViewerJS, start_table({-width=>'100%', -border=>0}), Tr(td(h1($BoardsById{$board}{'Name'})), td({-align=>'right', -valign=>'top'}, a({-href=>&MakeURL()}, b('[Home]')), ( $level > 0 ) ? a({-href=>&MakeURL('USERSAID', 'Edit', 'Type', 'DB', 'Dashboard', $board)}, '[Edit]') : '', ( -e $saved_dir .'/RCS/d'. $board .',v' ) ? a({-href=>MakeURL('Browse', 'Rcs', 'Id', 'd'. $board)}, '[RCS Log]') : '', br, small(b("". localtime)), br, small({-id=>'counter'}))), end_table, '', '

'; # Display the view chooser $view = 0; while ( $view < scalar(@dv_name) ) { if ( $view == param('View') ) { print b(' [' . $dv_name[$view] . '] '); } else { print a({-href=>&MakeURL('Mode', 'view', 'Dashboard', $board, 'Base', $base, 'View', $view, 'Filter', $filter)}, ' [' . $dv_name[$view] . '] '); } $view += 1; } # Display custom view entry fields print '

', start_form({-align=>'center', -method=>'GET'}), '

', '', '', 'Start: ', textfield(-name=>'Start'), 'End: ', textfield(-name=>'End'); if ( defined($base) ) { print ''; } else { print '
', 'Filter: ', textfield(-name=>'Filter', -value=>$filter), '
'; } print submit(-name=>'Mode', -value=>'view'), '

', end_form; # Validate Start/End times if ( defined(&RRDs::times) ) { RRDs::times(( defined(param('Start')) && param('Start') ne '' ) ? param('Start') : 'end - 1 day', ( defined(param('End')) && param('End') ne '' ) ? param('End') : 'now' ); if ( defined(RRDs::error) ) { &Error(RRDs::error); } } # Start dashboard table print '

'; print b(strftime("%a %b %e %H:%M %Y", localtime($startts)) .' - '. strftime("%a %b %e %H:%M %Y", localtime($endts))) if ( param('View') == -1 && $startts > 0 ); print start_table({-border=>0,-align=>'center'}); if ( !defined(param('dGrouped')) ) { # Standard dashboard layout algorithm print ''; my $col = 1; my $item; foreach $item ( sort { param($a) <=> param($b) } grep(/^[a-z]+_Seq$/, param()) ) { $item =~ s/_Seq//; next if ( defined(param("${item}_DELETE")) ); if ( param("${item}_dname") =~ /^g/ ) { print td(a({-href=>MakeURL('Mode', 'view', 'Graph', substr(param("${item}_dname"), 1))}, img({-src=>&GraphURL(param("${item}_dname"), undef, param('Start'), param('End'), param('dWidth'), param('dHeight'), ( param('dNoLegend') eq 'On' ) ? 1 : undef), -align=>'center', -border=>0}))); } elsif ( param("${item}_dname") =~ /^t/ ) { my $titem = param("${item}_dname"); $titem =~ s/^t//; my @list = (); if ( param("${item}_type") eq 'List' ) { &TMPLFind($TemplatesById{$titem}{'Filter'}, $TemplatesById{$titem}{'Display'}); foreach ( param("${item}_list") ) { push @list, $_ if ( $filter eq '' || $TMPL{$_} =~ /$filter/ ); } } elsif ( param("${item}_type") eq 'Regex' ) { &TMPLFind($TemplatesById{$titem}{'Filter'}, $TemplatesById{$titem}{'Display'}); my $rx = param("${item}_regex"); foreach ( keys(%TMPL) ) { push @list, $_ if ( $TMPL{$_} =~ /$rx/ && ( $filter eq '' || $TMPL{$_} =~ /$filter/ )); } } elsif ( param("${item}_type") eq 'All' ) { &TMPLFind($TemplatesById{$titem}{'Filter'}, $TemplatesById{$titem}{'Display'}); foreach ( sort { $TMPL{$a} cmp $TMPL{$b} } keys(%TMPL) ) { push @list, $_ if ( $filter eq '' || $TMPL{$_} =~ /$filter/ ); } } elsif ( param("${item}_type") eq 'Base' ) { @list = ( $DBTMPL{$base}{$item} ) if ( defined($DBTMPL{$base}{$item}) ); } else { print td({-align=>'center'}, em('Error in dashboard definition')); $col += 1; } while ( scalar(@list) > 0 ) { print td(a({-href=>MakeURL('Mode', 'view', 'Template', $titem, 'Base', $list[0])}, img({-src=>&GraphURL(param("${item}_dname"), $list[0], param('Start'), param('End'), param('dWidth'), param('dHeight'), ( param('dNoLegend') eq 'On' ) ? 1 : undef), -align=>'center', -border=>0}))); shift @list; $col += 1; if ( $col > param('dCols') ) { print ''; $col = 1; } } $col -= 1; } else { print td({-align=>'center'}, em('Error in dashboard definition')); } $col += 1; if ( $col > param('dCols') ) { print ''; $col = 1; } } print ''; } else { # Dashboard 'Grouped' layout algorithm my ( %Rows, @cols ); my $item; my $titem; foreach $item ( sort { param($a) <=> param($b) } grep(/^[a-z]+_Seq$/, param()) ) { $item =~ s/_Seq//; next if ( defined(param("${item}_DELETE")) ); my @list = (); if ( param("${item}_dname") =~ /^t/ ) { $titem = param("${item}_dname"); $titem =~ s/^t//; &TMPLFind($TemplatesById{$titem}{'Filter'}, $TemplatesById{$titem}{'Display'}); if ( param("${item}_type") eq 'List' ) { foreach ( param("${item}_list") ) { push @list, $_ if ( $filter eq '' || $TMPL{$_} =~ /$filter/ ); } } elsif ( param("${item}_type") eq 'Regex' ) { my $rx = param("${item}_regex"); foreach ( keys(%TMPL) ) { push @list, $_ if ( $TMPL{$_} =~ /$rx/ && ( $filter eq '' || $TMPL{$_} =~ /$filter/ )); } } elsif ( param("${item}_type") eq 'All' ) { foreach ( keys(%TMPL) ) { push @list, $_ if ( $filter eq '' || $TMPL{$_} =~ /$filter/ ); } } else { &Error('Error in dashboard definition'); } } else { &Error('Error in dashboard definition'); next; } push @cols, $titem; foreach ( @list ) { my $nex = param("${item}_row"); my $row; if ( $nex eq '' ) { $row = $TMPL{$_}; } else { # Why ' - ' ? Is it okay? $row = join(' - ', ($TMPL{$_} =~ /$nex/)); } $Rows{$row}[scalar(@cols)-1] = $_; } } my $row; foreach $row ( sort(keys(%Rows)) ) { print ''; my $col = 0; while ( $col < scalar(@cols) ) { print ''; if ( defined($Rows{$row}[$col]) ) { print td(a({-href=>MakeURL('Mode', 'view', 'Template', $cols[$col], 'Base', $Rows{$row}[$col])}, img({-src=>&GraphURL('t'. $cols[$col], $Rows{$row}[$col], param('Start'), param('End'), param('dWidth'), param('dHeight'), ( param('dNoLegend') eq 'On' ) ? 1 : undef), -align=>'center', -border=>0}))); } else { print td('N/A'); } $col += 1; } print ''; } } print end_table; } } else { # # Graph/template display page # print header; my $id; if ( defined(param('Graph')) ) { # Graph header $id = 'g' . param('Graph'); print start_html({-style=>{-code=>$CSS}, -title=>'drraw - ' . $GraphsById{param('Graph')}{'Name'}, -head=>meta({-http_equiv=>'Refresh', -content=>$vrefresh})}), $header, $ViewerJS, start_table({-width=>'100%', -border=>0}), Tr(td(h1(a({-href=>&MakeURL('Mode', 'view', 'Graph', param('Graph'))}, $GraphsById{param('Graph')}{'Name'}))), td({-align=>'right', -valign=>'top'}, a({-href=>&MakeURL()}, b('[Home]')), ( $level > 0 ) ? a({-href=>&MakeURL('USERSAID', 'Edit', 'Graph', param('Graph'))}, '[Edit]') : '', ( -e $saved_dir .'/RCS/g'. param('Graph') .',v' ) ? a({-href=>MakeURL('Browse', 'Rcs', 'Id', 'g'. param('Graph'))}, '[RCS Log]') : '', br, small(b("". localtime)), br, small({-id=>'counter'}))), end_table, ''; } else { # Template header &Cache_Load; &TMPLFind($TemplatesById{param('Template')}{'Filter'}, $TemplatesById{param('Template')}{'Display'}); $id = 't' . param('Template'); print start_html({-style=>{-code=>$CSS}, -title=>'drraw - ' . $TemplatesById{param('Template')}{'Name'}, -head=>meta({-http_equiv=>'Refresh', -content=>$vrefresh})}), $header, $ViewerJS, start_table({-width=>'100%', -border=>0}), Tr(td(h1(a({-href=>&MakeURL('Mode', 'view', 'Template',param('Template'))}, $TemplatesById{param('Template')}{'Name'}) . ': ', a({-href=>&MakeURL('Mode', 'view', 'Template', param('Template'), 'Base', param('Base'))}, $TMPL{param('Base')}))), td({-align=>'right', -valign=>'top'}, a({-href=>&MakeURL()}, b('[Home]')), ( $level > 0 ) ? a({-href=>&MakeURL('USERSAID', 'Edit', 'Template', param('Template'))}, '[Edit]') : '', ( -e $saved_dir .'/RCS/t'. param('Template') .',v' ) ? a({-href=>MakeURL('Browse', 'Rcs', 'Id', 't'. param('Template'))}, '[RCS Log]') : '', br, small(b("". localtime)), br, small({-id=>'counter'}))), end_table, '', start_form({-align=>'center'}, -method=>'GET'), '\n", p({-align=>'center'}, popup_menu(-name=>'Base', -values=>[sort { $TMPL{$a} cmp $TMPL{$b} } keys(%TMPL)], -labels=>\%TMPL), submit(-name=>'Mode', -value=>'view')), end_form; } # Display the view chooser my $view = 0; print '

' if ( defined(param('View')) ); while ( $view < scalar(@dv_def) ) { if ( defined(param('View')) ) { if ( $view == param('View') ) { print b(' [' . $dv_name[$view] . '] '); } elsif ( defined(param('Graph')) ) { print a({-href=>&MakeURL('Mode', 'view', 'Graph', param('Graph'), 'View', $view)}, ' [' . $dv_name[$view] . '] '); } else { print a({-href=>&MakeURL('Mode', 'view', 'Template', param('Template'), 'Base', param('Base'), 'View', $view)}, ' [' . $dv_name[$view] . '] '); } } else { print p({-align=>'center'}, a({-href=>MakeURL('Mode', 'view', ( defined(param('Graph')) ) ? ( 'Graph', param('Graph') ) : ( 'Template', param('Template'), 'Base', param('Base') ), 'View', $view)}, img({-src=>&GraphURL($id, ( defined(param('Base')) ) ? param('Base') : undef, $dv_def[$view]), -align=>'center', -border=>0})), br, b($dv_name[$view])); } $view += 1; } # If this is a pre-defined view, define Start/End if ( defined(param('View')) ) { print '

'; if ( param('View') >= 0 ) { param('Start', $dv_def[param('View')]); param('End', 'now'); } } # Display the custom view entry fields print start_form({-align=>'center', -method=>'GET'}), '', ( defined(param('Graph')) ) ? '' : '' . '', p({-align=>'center'}, 'Start: ', textfield(-name=>'Start'), 'End: ', textfield(-name=>'End'), submit(-name=>'Mode', -value=>'view')), end_form; # Validate Start/End times my ( $startts, $endts ); if ( defined(&RRDs::times) ) { ( $startts, $endts ) = RRDs::times(( defined(param('Start')) && param('Start') ne '' ) ? param('Start') : 'end - 1 day', ( defined(param('End')) && param('End') ne '' ) ? param('End') : 'now' ); if ( defined(RRDs::error) ) { &Error(RRDs::error); } } # Links to graphs if ( defined(param('View')) ) { print '

', img({-src=>&GraphURL($id, ( defined(param('Base')) ) ? param('Base') : undef, param('Start'), param('End')), -align=>'center', -border=>0}), br; if ( param('View') >= 0 ) { print b($dv_name[param('View')]); } else { if ( defined(&RRDs::times) ) { print b(strftime("%a %b %e %H:%M %Y", localtime($startts)) .' - '. strftime("%a %b %e %H:%M %Y", localtime($endts))); } else { print b('Custom View'); } } print '

'; } } print $drrawhome, $footer, end_html; } elsif ( param('Mode') eq 'show' ) { # Mode=show -> Image my @graph = ( ( defined(param('Graph')) ) ? param('Graph') : param('Template'), ( defined(param('Base')) ) ? param('Base') : undef, ( defined(param('Start')) ) ? param('Start') : undef, ( defined(param('End')) ) ? param('End') : undef, ( defined(param('Width')) ) ? param('Width') : undef, ( defined(param('Height')) ) ? param('Height') : undef, ( defined(param('NoLegend')) ) ? param('NoLegend') : undef); &Definition_Load($graph[0]); unlink "${tmp_dir}/$1" if ( $graph[0] =~ /^(\d+\.\d+)$/ ); # Untaint # The following were just lost param(-name=>'Mode', -value=>'show'); # For &Error param(-name=>'Graph', -value=>$graph[0]) unless ( $graph[0] =~ /^t/ ); param(-name=>'Template', -value=>$graph[0]) if ( $graph[0] =~ /^t/ ); param(-name=>'Base', -value=>$graph[1]) if ( defined($graph[1]) ); param(-name=>'Start', -value=>$graph[2]); param(-name=>'End', -value=>$graph[3]); param(-name=>'gWidth', -value=>$graph[4]) if ( defined($graph[4]) ); param(-name=>'gHeight', -value=>$graph[5]) if ( defined($graph[5]) ); param(-name=>'gNoLegend', -value=>1) if ( defined($graph[6]) ); &DSLoad; &Cache_Load; &Sort_Colors_Init; if ( $graph[0] =~ /^t/ ) { # Define graph from the template definition and chosen base my %tDB; my ( $ds, @subs ); foreach $ds ( sort keys(%DS) ) { next if ( param("${ds}_File") eq '' ); if ( defined($tDB{param("${ds}_File")}) ) { param("${ds}_File", $tDB{param("${ds}_File")}); } else { die "Bad Template [${ds}]\n" unless ( defined(param("${ds}_Tmpl"))); if ( !defined(param("${ds}_Formula")) ) { $tDB{param("${ds}_File")} = param("${ds}_File"); $tDB{param("${ds}_File")} =~ s/\/\/.*$/\/\//; } else { $tDB{param("${ds}_File")} = ''; } if ( scalar(@subs) == 0 ) { my $dbname = param('Base'); $dbname =~ s/^.*\/\///; my $ex = param('tRegex'); @subs = ( $dbname =~ /$ex/ ); } $tDB{param("${ds}_File")} .= &ExpandMatches(param("${ds}_Tmpl"), \@subs ); if ( !defined(param("${ds}_Formula")) ) { foreach ( sort keys %datadirs ) { $tDB{param("${ds}_File")} =~ s/^.*\/\//$_\/\//; last if ( -r $tDB{param("${ds}_File")} ); } } param(-name=>"${ds}_File", -value=>$tDB{param("${ds}_File")}); } } &TMPLFind(param('tRegex'), param('tNiceRegex')); param(-name=>'gTitle', -value=>param('gTitle') .': '. $TMPL{param('Base')}); } # Finally, produce the image &DRAW(( $graph[0] =~ /^\d/ ) ? 1 : 2); } else { # Catch all for invalid requests print header, start_html({-style=>{-code=>$CSS}, -title=>'drraw - '. $title}), $header, h1('Invalid Request.'), $footer, end_html; } } elsif ( defined(param('Type')) && param('Type') eq 'DB' ) { # # DashBoard editing pages # if ( $level == 0 ) { print header(-status=>'Permission Denied'), start_html('Permission Denied'), $header, h2('Permission Denied'); exit 0; } &Indexes_Load; &Cache_Load; # Avoids having to check whether it's defined below.. param('USERSAID', 'New') unless ( defined(param('USERSAID')) ); # Deleting a saved definition if ( param('USERSAID') eq 'Delete' ) { if ( defined(param('Dashboard')) && param('Dashboard') =~ /^(\d+\.\d+)$/ ) { if ($level == 0 || ( $level == 1 && $user ne $BoardsById{$1}{'Owner'} ) ) { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr, h1('Permission Denied!'); } else { unlink "${saved_dir}/d" . $1; # Untaint print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '.$title, -head=>meta({-http_equiv=>'refresh', -content=>'1;URL=' . &MakeURL('Browse', 'AllDashboards')})), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr, h1('Board Deleted!'); Indexes_Save('d', $1, ''); } } else { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr, &Error('Deletion failed.'); } } else { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr; } # New editing session based on an existing graph? if ( param('USERSAID') eq 'Edit' ) { my ( $user, $board ) = ( param('USERSAID'), param('Dashboard') ); &Definition_Load('d' . $board); param('USERSAID', $user); param('Dashboard', $board); } if ( param('USERSAID') eq 'Save Dashboard' || param('USERSAID') eq 'Clone Dashboard' ) { if ( param('dTitle') !~ /^(\S| )+$/ ) { &Error('Invalid Dashboard Name: ' . param('dTitle')); } elsif ( !defined(param('dGrouped')) && param('dCols') < 1 ) { &Error('Invalid Number of Columns: ' . param('dCols')); } else { # Saving/Cloning a definition my $dname = time . '.' . $$; $dname = param('Dashboard') if ( defined(param('Dashboard')) && param('USERSAID') eq 'Save Dashboard' ); my $msg = 'oink.'; $msg = param('RCS') if ( defined(param('RCS')) ); $msg = 'Cloned from '. param('Dashboard') if ( param('USERSAID') eq 'Clone Dashboard' ); if (Definition_Save('d' . $dname, $msg) && Indexes_Save('d', $dname, param('dTitle'))) { print h1('Dashboard Saved!'), p('The dashboard you just saved may be subsequently edited or cloned, or simply viewed at the following address: ', a({-href=>MakeURL('Mode', 'view', 'Dashboard', $dname)}, MakeURL('Mode', 'view', 'Dashboard', $dname))), p('You may continue editing, or ', a({-href=>MakeURL('Browse', 'AllDashboards')}, 'return to the main page'), '.'); param(-name=>'Dashboard', -value=>$dname); } } print hr; } if ( param('USERSAID') ne 'Delete' ) { # Finally, edition pages # It's all a big form.. print $EditorJS, start_form(-method=>'POST', -name=>'Editor'), ''; print '\n" if ( defined(param('Dashboard')) ); if ( param('USERSAID') eq 'Switch' ) { if ( defined(param('dGrouped')) ) { Delete('dGrouped'); } else { param(-name=>'dGrouped', -value=>'Yes'); } } print '' if ( defined(param('dGrouped')) ); &BoardOptions; print p({-align=>'center'}, ( $level == 2 || ( $level == 1 && ( !defined(param('Dashboard')) || ( defined($BoardsById{param('Dashboard')}{'Owner'}) && $user eq $BoardsById{param('Dashboard')}{'Owner'} ) ) ) ) ? textfield(-name=>'RCS', -default=>'Describe your change(s) here before saving..', -size=>80, -override=>1) . ( ( defined(param('Dashboard')) && -e $saved_dir .'/RCS/d'. param('Dashboard') .',v' ) ? a({-href=>MakeURL('Browse', 'Rcs', 'Id', 'd'. param('Dashboard'))}, img({-src=>$icon_text, -border=>0})) : '' ) .'
'. submit(-name=>'USERSAID', -value=>'Save Dashboard') : '', ( $level > 0 && defined(param('Dashboard')) ) ? submit(-name=>'USERSAID', -value=>'Clone Dashboard') : ''), p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Update')); &BoardConfig; print end_form; print hr, &Dump if ( $DEBUG ); } print $drrawhome, $footer, end_html; } else { # # Editing pages # # XXX -- The Type should really be set and checked here... if ( $level == 0 ) { print header(-status=>'Permission Denied'), start_html('Permission Denied'), $header, h2('Permission Denied'); exit 0; } &Indexes_Load; # Avoids having to check whether it's defined below.. param('USERSAID', 'New') unless ( defined(param('USERSAID')) ); # Deleting a saved definition if ( param('USERSAID') eq 'Delete' ) { if ( defined(param('Graph')) && param('Graph') =~ /^(\d+\.\d+)$/ ) { if ($level == 0 || ( $level == 1 && $user ne $GraphsById{$1}{'Owner'} ) ) { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr, h1('Permission Denied!'); } else { unlink "${saved_dir}/g" . $1; # Untaint print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '.$title, -head=>meta({-http_equiv=>'refresh', -content=>'1;URL=' . &MakeURL('Browse', 'AllGraphs')})), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr, h1('Graph Deleted!'); Indexes_Save('g', $1, ''); } } elsif ( defined(param('Template')) && param('Template') =~ /^(\d+\.\d+)$/ ) { if ($level == 0 || ( $level == 1 && $user ne $TemplatesById{$1}{'Owner'} ) ){ print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr, h1('Permission Denied!'); } else { unlink "${saved_dir}/t" . $1; print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '.$title, -head=>meta({-http_equiv=>'refresh', -content=>'1;URL=' . &MakeURL('Browse', 'AllTemplates')})), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr, h1('Template Deleted!'); Indexes_Save('t', $1, ''); } } else { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr, &Error('Deletion failed.'); } } else { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-align=>'center'}, a({-href=>&MakeURL}, $title)), hr; } # New editing session based on an existing graph? if ( param('USERSAID') eq 'Edit' ) { my ( $user, $graph ) = ( param('USERSAID'), param('Graph') ); my $template = param('Template'); &Definition_Load('g' . $graph) if ( defined($graph) ); &Definition_Load('t' . $template) if ( defined($template) ); param('USERSAID', $user); param('Graph', $graph) if ( defined($graph) ); param('Template', $template) if ( defined($template) ); } if ( param('USERSAID') eq 'Save Graph' || param('USERSAID') eq 'Clone Graph' ) { # Saving a graph if ( param('gTitle') !~ /^(\S| )+$/ ) { &Error('Invalid Graph Name: ' . param('gTitle')); } else { my $gname = time . '.' . $$; $gname = param('Graph') if ( defined(param('Graph')) && param('USERSAID') eq 'Save Graph' ); my $msg = 'oink.'; $msg = param('RCS') if ( defined(param('RCS')) ); $msg = 'Cloned from '. param('Graph') if ( param('USERSAID') eq 'Clone Graph' ); &DSLoad; if (Definition_Save('g' . $gname, $msg) && Indexes_Save('g', $gname, param('gTitle'))) { print h1('Graph Saved!'), p('The graph you just saved may be subsequently edited or cloned, or simply viewed at the following address: ', a({-href=>MakeURL('Mode', 'view', 'Graph', $gname)}, MakeURL('Mode', 'view', 'Graph', $gname))), p('You may continue editing, or ', a({-href=>MakeURL('Browse', 'AllGraphs')}, 'return to the main page'), '.'); param(-name=>'Graph', -value=>$gname); } } Delete('tName'); # May have been cloned from a template print hr; } elsif ( param('USERSAID') eq 'Save Template' || param('USERSAID') eq 'Clone Template' ) { # Saving a template if ( param('tName') !~ /^(\S| )+$/ ) { &Error('Invalid Template Name: ' . param('tName')); } elsif ( param('gTitle') !~ /^(\S| )+$/ ) { &Error('Invalid Graph Title: ' . param('gTitle')); } else { my $tname = time . '.' . $$; $tname = param('Template') if ( defined(param('Template')) && param('USERSAID') eq 'Save Template' ); my $msg = 'oink.'; $msg = param('RCS') if ( defined(param('RCS')) ); $msg = 'Cloned from '. param('Template') if ( param('USERSAID') eq 'Clone Template' ); if (Definition_Save('t' . $tname, $msg) && Indexes_Save('t', $tname, param('tName'), param('tRegex'), param('tNiceRegex'))) { print h1('Template Saved!'), p('The template you just saved may be subsequently edited or cloned, or simply viewed at the following address: ', a({-href=>MakeURL('Mode', 'view', 'Template', $tname)}, MakeURL('Mode', 'view', 'Template', $tname))), p('You may continue editing, or ', a({-href=>MakeURL('Browse', 'AllTemplates')}, 'return to the main page'), '.'); param(-name=>'Template', -value=>$tname); } } print hr; } if ( param('USERSAID') ne 'Delete' ) { # General edition page # It's all a big form.. print $EditorJS, start_form(-method=>'POST', -name=>'Editor'); if ( param('USERSAID') eq 'Refresh' ) { &DBFind; } else { &Cache_Load; } &DSLoad unless ( param('USERSAID') eq 'Save Graph' || param('USERSAID') eq 'Clone Graph' ); &Sort_Colors_Init; my $old = scalar(keys(%DS)); &DBAdd if ( param('USERSAID') eq 'Add DB(s) to data sources' ); &EVTAdd if ( param('USERSAID') eq 'Add to data sources' ); if ( scalar(keys(%DS)) > 0 ) { param('Code', 'Show') if ( !defined(param('Code')) ); if ( param('USERSAID') !~ /^Add .*to data sources$/ ) { my $gname = time . "." . $$; if ( &DRAW(0) ) { Definition_Save($gname, 'Temporary file'); print table({-align=>'center', -cellpadding=>5, -border=>1}, Tr(td({-align=>'center'}, b('rrdtool invocation'), br, radio_group(-name=>'Code', -values=>['Show', 'Hide'], -default=>param('Code'), -onclick=>'ShowHideChange(this, "CODE")')))), p({-align=>'center'}, img({-src=>&GraphURL($gname, undef, param('Start'), param('End')), -align=>'center', -border=>0})); } else { print table({-align=>'center', -cellpadding=>5, -border=>1}, Tr(td({-align=>'center'}, b('rrdtool invocation'), br, radio_group(-name=>'Code', -values=>['Show', 'Hide'], -default=>param('Code'), -onclick=>'ShowHideChange(this, "CODE")')))); } print p({-align=>'center'}, 'Start: ', textfield(-name=>'Start'), 'End: ', textfield(-name=>'End'), submit(-name=>'USERSAID', -value=>'Update')); } elsif ( $old == 0 ) { print p(table({-width=>'70%', -align=>'center'}, Tr(td({-class=>'help'}, 'From here on, you may do the following:', ul(li('Add more data sources to the graph'), li('Define graph options'), li('Configure data sources'), li('Create a template based on this graph definition'), li('Save this graph')), 'Click any "Update" button to update the graph preview after making changes to the graph options and/or data source configuration.')))); } if ( ( defined(param('tName')) && !defined(param('Graph')) ) || param('USERSAID') eq 'Make Template' ) { # 'Template' needs to be remembered print '\n" if ( defined(param('Template')) ); print p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Clone Graph')), p({-align=>'center'}, ( $level == 2 || ( $level == 1 && ( !defined(param('Template')) || ( defined($TemplatesById{param('Template')}{'Owner'}) && $user eq $TemplatesById{param('Template')}{'Owner'} ) ) ) ) ? textfield(-name=>'RCS', -default=>'Describe your change(s) here before saving..', -size=>80, -override=>1) . ( ( defined(param('Template')) && -e $saved_dir .'/RCS/t'. param('Template') .',v' ) ? a({-href=>MakeURL('Browse', 'Rcs', 'Id', 't'. param('Template'))}, img({-src=>$icon_text, -border=>0})) : '' ) .'
'. submit(-name=>'USERSAID', -value=>'Save Template') : '', ( $level > 0 && defined(param('Template')) ) ? submit(-name=>'USERSAID', -value=>'Clone Template') :""), hr; &TMPLFind(param('tRegex'), param('tNiceRegex')); &TMPLConfig; print hr; } else { # 'Graph' needs to be remembered print '\n" if ( defined(param('Graph')) ); print p({-align=>'center'}, ( $level == 2 || ( $level == 1 && ( !defined(param('Graph')) || ( defined($GraphsById{param('Graph')}{'Owner'}) && $user eq $GraphsById{param('Graph')}{'Owner'} ) ) ) ) ? textfield(-name=>'RCS', -default=>'Describe your change(s) here before saving..', -size=>80, -override=>1) . ( ( defined(param('Graph')) && -e $saved_dir .'/RCS/g'. param('Graph') .',v' ) ? a({-href=>MakeURL('Browse', 'Rcs', 'Id', 'g'. param('Graph'))}, img({-src=>$icon_text, -border=>0})) : '' ) .'
'. submit(-name=>'USERSAID', -value=>'Save Graph') : '', ( $level > 0 && defined(param('Graph')) ) ? submit(-name=>'USERSAID', -value=>'Clone Graph') : ""), p({-align=>'center'}, ( $level > 0 ) ? submit(-name=>'USERSAID', -value=>'Make Template') : ''), hr; } if ( $old == 0 ) { print p(table({-width=>'70%', -align=>'center'}, Tr(td({-class=>'help'}, &help_dsconfig)))); } &DSConfig; print hr; &GOptions; print hr; } else { print p(table({-width=>'70%', -align=>'center'}, Tr(td({-class=>'help'}, &help_graphfiles)))); } &DBChooser; &DBInfo if ( param('USERSAID') eq 'RRD Info for selected DB' ); print end_form; print hr, &Dump if ( $DEBUG ); } print $drrawhome, $footer, end_html; } exit 0; # # Loading and saving state # sub Cache_Load { if ( scalar(@rrdfiles) > 0 ) { &Error("Cache may only be loaded once..\n"); return; } if ( -f "${tmp_dir}/rrdfiles" ) { if ( time - (stat("${tmp_dir}/rrdfiles"))[9] < $crefresh ) { open CACHE, "< ${tmp_dir}/rrdfiles" or die "Could not load saved cache (rrdfiles): $!\n"; while () { chomp; push @rrdfiles, $_; } close CACHE; } } if ( scalar(@evtfiles) > 0 ) { &Error("Cache may only be loaded once..\n"); return; } if ( -f "${tmp_dir}/evtfiles" ) { if ( time - (stat("${tmp_dir}/evtfiles"))[9] < 3600 ) { open CACHE, "< ${tmp_dir}/evtfiles" or die "Could not load saved cache (evtfiles): $!\n"; while () { chomp; push @evtfiles, $_; } close CACHE; } } &DBFind if ( scalar(@rrdfiles) == 0 ); } sub Cache_Save { if ( !open(CACHE, "> ${tmp_dir}/rrdfiles.$$") ) { &Error("Could not save cache (evtfiles): $!\n"); return; } my $entry; foreach $entry ( @rrdfiles ) { print CACHE $entry . "\n"; } close CACHE; if ( !rename("${tmp_dir}/rrdfiles.$$", "${tmp_dir}/rrdfiles") ) { unlink "${tmp_dir}/rrdfiles.$$"; &Error("Could not save cache (evtfiles): $!\n"); } if ( !open(CACHE, "> ${tmp_dir}/evtfiles.$$") ) { &Error("Could not save cache (evtfiles): $!\n"); return; } foreach $entry ( @evtfiles ) { print CACHE $entry . "\n"; } close CACHE; if ( !rename("${tmp_dir}/evtfiles.$$", "${tmp_dir}/evtfiles") ) { unlink "${tmp_dir}/evtfiles.$$"; &Error("Could not save cache (evtfiles): $!\n"); } } sub Indexes_Load { my $cnt = 0; while ( $cnt++ < 3 ) { last if (sysopen(LOCK, "${saved_dir}/LCK.index", &O_WRONLY|&O_EXCL|&O_CREAT)); sleep 5; } close LOCK; # We try, but force the lock if necessary.. if ( -f "${saved_dir}/index" ) { open INDEX, "< ${saved_dir}/index" or die "Could not load saved index: $!\n"; while () { chomp; if ( /^g(\d+\.\d+):(.+)$/ ) { $GraphsById{$1}{'Name'} = $2 if ( -f "${saved_dir}/g${1}" ); } elsif ( /^g(\d+\.\d+)=(.*)$/ ) { $GraphsById{$1}{'Owner'} = $2 if ( -f "${saved_dir}/g${1}" ); } elsif ( /^t(\d+\.\d+):(.+)$/ ) { $TemplatesById{$1}{'Name'} = $2 if ( -f "${saved_dir}/t${1}" ); } elsif ( /^tfr(\d+\.\d+):(.*)$/ ) { $TemplatesById{$1}{'Filter'} = $2 if ( -f "${saved_dir}/t${1}" ); } elsif ( /^tdr(\d+\.\d+):(.*)$/ ) { $TemplatesById{$1}{'Display'} = $2 if ( -f "${saved_dir}/t${1}" ); } elsif ( /^drraw=(\d+)\/(.*)$/ ) { ( $drraw_ID, $drraw_reported ) = ( $1, $2 ); } elsif ( /^t(\d+\.\d+)=(.*)$/ ) { $TemplatesById{$1}{'Owner'} = $2 if ( -f "${saved_dir}/t${1}" ); } elsif ( /^d(\d+\.\d+):(.+)$/ ) { $BoardsById{$1}{'Name'} = $2 if ( -f "${saved_dir}/d${1}" ); } elsif ( /^dtn(\d+\.\d+):(\w+):t(\d+\.\d+)$/ ) { $BoardsById{$1}{'Filters'}{$2}{'Template'} = $3 if ( -f "${saved_dir}/d${1}" ); } elsif ( /^dfr(\d+\.\d+):(\w+):(.*)$/ ) { $BoardsById{$1}{'Filters'}{$2}{'Filter'} = $3 if ( -f "${saved_dir}/d${1}" ); } elsif ( /^ddr(\d+\.\d+):(\w+):(.*)$/ ) { $BoardsById{$1}{'Filters'}{$2}{'Display'} = $3 if ( -f "${saved_dir}/d${1}" ); } elsif ( /^d(\d+\.\d+)=(.*)$/ ) { $BoardsById{$1}{'Owner'} = $2 if ( -f "${saved_dir}/d${1}" ); } else { &Error("Bad index entry: $_\n"); } } close INDEX; } unlink "${saved_dir}/LCK.index"; } sub Indexes_Save { croak 'Indexes_Save(type, idx, name)' if ( scalar(@_) < 3 || scalar(@_) > 5 ); my ( $type, $idx, $name, $regex, $niceregex ) = ( @_ ); &Indexes_Load; # XXX Small race condition here.. my $cnt = 0; while ( $cnt++ < 3 ) { last if (sysopen(LOCK, "${saved_dir}/LCK.index", &O_WRONLY|&O_EXCL|&O_CREAT)); sleep 5; } close LOCK; # We try, but force the lock if necessary.. if ( $type ne 'drraw' && !open(LOG, ">> ${saved_dir}/log") ) { &Error("Could not append log entry: $!\n"); unlink "${saved_dir}/LCK.index"; return 0; } if ( $name ne '' ) { # Hard to say why, but if these two ifs are combined, # Perl (5.6.1) bombs out with an "insecure dependency in open" error. if ( !open(INDEX, "> ${saved_dir}/index") ) { &Error("Could not save index: $!\n"); unlink "${saved_dir}/LCK.index"; close LOG unless ( $type eq 'drraw' ); return 0; } } if ( $type ne 'drraw' ) { print LOG time ."|". $type . $idx ."|"; print LOG $user; print LOG " [$ENV{REMOTE_ADDR}]" if ($user eq 'guest'); print LOG "|$name\n"; close LOG; } if ( $name eq '' ) { unlink "${saved_dir}/LCK.index"; # No name means something was deleted return; } if ( $type eq 'g' ) { $GraphsById{$idx}{'Name'} = $name; if ( $level == 1 ) { $GraphsById{$idx}{'Owner'} = $user; } else { delete($GraphsById{$idx}{'Owner'}); } } foreach $idx ( keys(%GraphsById) ) { print INDEX 'g' . $idx . ':' . $GraphsById{$idx}{'Name'} . "\n"; print INDEX 'g' . $idx . '=' . $GraphsById{$idx}{'Owner'} . "\n" if ( defined($GraphsById{$idx}{'Owner'}) ); } if ( $type eq 't' ) { $TemplatesById{$idx}{'Name'} = $name; $TemplatesById{$idx}{'Filter'} = $regex; $TemplatesById{$idx}{'Display'} = $niceregex; if ( $level == 1 ) { $TemplatesById{$idx}{'Owner'} = $user; } else { delete($TemplatesById{$idx}{'Owner'}); } } foreach $idx ( keys(%TemplatesById) ) { print INDEX 't' . $idx . ':' . $TemplatesById{$idx}{'Name'} . "\n"; print INDEX 'tfr' . $idx . ':' . $TemplatesById{$idx}{'Filter'} . "\n"; print INDEX 'tdr' . $idx . ':' . $TemplatesById{$idx}{'Display'} ."\n"; print INDEX 't' . $idx . '=' . $TemplatesById{$idx}{'Owner'} . "\n" if ( defined($TemplatesById{$idx}{'Owner'}) ); } if ( $type eq 'd' ) { $BoardsById{$idx}{'Name'} = $name; delete($BoardsById{$idx}{'Template'}); if ( !defined(param('dGrouped')) ) { my $item; foreach $item ( grep(/^[a-z]+_Seq$/, param()) ) { $item =~ s/_Seq//; if ( param("${item}_type") eq 'Base' ) { $BoardsById{$idx}{'Filters'}{$item}{'Template'} = param("${item}_dname"); $BoardsById{$idx}{'Filters'}{$item}{'Template'} =~ s/^t//; $BoardsById{$idx}{'Filters'}{$item}{'Filter'} = param("${item}_regex"); $BoardsById{$idx}{'Filters'}{$item}{'Display'} = param("${item}_row"); } } } if ( $level == 1 ) { $BoardsById{$idx}{'Owner'} = $user; } else { delete($BoardsById{$idx}{'Owner'}); } } ( $drraw_ID, $drraw_reported ) = ( $idx, $name ) if ( $type eq 'drraw' ); print INDEX 'drraw='. $drraw_ID .'/'; if ( defined($drraw_reported) ) { print INDEX $drraw_reported; } print INDEX "\n"; foreach $idx ( keys(%BoardsById) ) { print INDEX 'd' . $idx . ':' . $BoardsById{$idx}{'Name'} . "\n"; if ( defined($BoardsById{$idx}{'Filters'}) ) { my $item; foreach $item ( sort keys(%{$BoardsById{$idx}{'Filters'}}) ) { print INDEX "dtn${idx}:${item}:t" . $BoardsById{$idx}{'Filters'}{$item}{'Template'} . "\n"; print INDEX "dfr${idx}:${item}:" . $BoardsById{$idx}{'Filters'}{$item}{'Filter'} . "\n"; print INDEX "ddr${idx}:${item}:" . $BoardsById{$idx}{'Filters'}{$item}{'Display'} . "\n"; } } print INDEX 'd' . $idx . '=' . $BoardsById{$idx}{'Owner'} . "\n" if ( defined($BoardsById{$idx}{'Owner'}) ); } close INDEX; unlink "${saved_dir}/LCK.index"; return 1; } my $silver = 0; sub GraphIndex { croak 'GraphIndex(regex)' if ( scalar(@_) != 1 ); my $gid; foreach $gid ( sort { $GraphsById{$a}{'Name'} cmp $GraphsById{$b}{'Name'} } keys(%GraphsById) ) { next unless ( $GraphsById{$gid}{'Name'} =~ /$_[0]/ ); print Tr({-bgcolor=>($silver++ % 2 == 0) ? '#E0E0E0' : ''}, td(a({-href=>MakeURL('Mode', 'view', 'Graph', $gid)}, '[View]'), ' ', ( $level > 0 ) ? ( $level == 2 || ( defined($GraphsById{$gid}{'Owner'}) && $user eq $GraphsById{$gid}{'Owner'} ) ) ? a({-href=>MakeURL('USERSAID', 'Edit', 'Graph', $gid)}, '[Edit/Clone]') .' '. a({-href=>MakeURL('USERSAID', 'Delete', 'Graph',$gid)}, '[Delete]') .' ' : '[Edit/'. a({-href=>MakeURL('USERSAID', 'Edit', 'Graph', $gid)}, 'Clone'). '] [Delete] ' : '', b($GraphsById{$gid}{'Name'})), td(''), td('')); } } sub TemplateIndex { croak 'TemplateIndex(regex)' if ( scalar(@_) != 1 ); my $tid; foreach $tid ( sort { $TemplatesById{$a}{'Name'} cmp $TemplatesById{$b}{'Name'} } keys(%TemplatesById) ) { next unless ( $TemplatesById{$tid}{'Name'} =~ /$_[0]/ ); &TMPLFind($TemplatesById{$tid}{'Filter'}, $TemplatesById{$tid}{'Display'}); print Tr({-bgcolor=>($silver++ % 2 == 0) ? '#E0E0E0' : ''}, td(a({-href=>MakeURL('Mode', 'view', 'Template', $tid)}, '[View]'), ' ', ( $level > 0 ) ? ( $level == 2 || ( defined($TemplatesById{$tid}{'Owner'}) && $user eq $TemplatesById{$tid}{'Owner'} ) ) ? a({-href=>MakeURL('USERSAID', 'Edit', 'Template', $tid)}, '[Edit/Clone]') .' '. a({-href=>MakeURL('USERSAID', 'Delete', 'Template', $tid)}, '[Delete]') : '[Edit/'. a({-href=>MakeURL('USERSAID', 'Edit', 'Template', $tid)}, 'Clone') .'] [Delete] ' : '', ' ', b($TemplatesById{$tid}{'Name'})), td({-align=>'right'}, ' (' . scalar(keys(%TMPL)) . ')'), td(start_form(-method=>'GET'), submit(-name=>'Mode', -value=>'view'), '[sort { $TMPL{$a} cmp $TMPL{$b} } keys(%TMPL)], -labels=>\%TMPL) : '', end_form)); } } sub DashBoardIndex { croak 'DashBoardIndex(regex)' if ( scalar(@_) != 1 ); my $did; foreach $did ( sort { $BoardsById{$a}{'Name'} cmp $BoardsById{$b}{'Name'} } keys(%BoardsById) ) { next unless ( $BoardsById{$did}{'Name'} =~ /$_[0]/ ); &BoardFind($did); my @extra; if ( defined($BoardsById{$did}{'Filters'}) ) { $extra[0] = ' (' . scalar(keys(%DBTMPL)) . ')'; $extra[1] = start_form(-method=>'GET') . submit(-name=>'Mode', -value=>'view') . '[sort keys(%DBTMPL)]) if ($IndexMax <= 0 || scalar(keys(%DBTMPL)) <= $IndexMax); $extra[1] .= end_form; } else { $extra[0] = ''; $extra[1] = start_form(-method=>'GET') . submit(-name=>'Mode', -value=>'view') . ' with filter: ' . ''') . end_form; } print Tr({-bgcolor=>($silver++ % 2 == 0) ? '#E0E0E0' : ''}, td(a({-href=>MakeURL('Mode', 'view', 'Dashboard', $did)}, '[View]'), ' ', ( $level > 0 ) ? ( $level == 2 || ( defined($BoardsById{$did}{'Owner'}) && $user eq $BoardsById{$did}{'Owner'} ) ) ? a({-href=>MakeURL('USERSAID', 'Edit', 'Type', 'DB', 'Dashboard', $did)}, '[Edit/Clone]') .' '. a({-href=>MakeURL('USERSAID', 'Delete', 'Type', 'DB', 'Dashboard', $did)}, '[Delete]') : '[Edit/'. a({-href=>MakeURL('USERSAID', 'Edit', 'Type', 'DB', 'Dashboard', $did)}, 'Clone') .'] [Delete] ' : '', ' ', b($BoardsById{$did}{'Name'}), td({-align=>'right'}, $extra[0]), td($extra[1]))); } } sub CustomIndex { croak 'CustomIndex(ref, id)' if ( scalar(@_) != 2); my ( $ref, $id ) = ( @_ ); if ( ref($ref) eq 'HASH' ) { my $key; foreach $key ( sort keys %{$ref} ) { my $name = $key; $name = $1 if ( $key =~ /^\d+\s+(\S.*)$/ ); my $nid = $id . $name ."\n"; my $uniqid = $$ref{$key}; if ( ref($$ref{$key}) eq 'HASH' ) { my $icon = $icon_closed; my $style = 'style="display:none"'; if ( defined(param('Browse')) && $nid eq substr(param('Browse'), 0, length($nid)) ) { $icon = $icon_open; $style = ''; } print "
", img({-src=>$icon, -border=>0, -id=>"${uniqid}-icon"}), ' '. $name, '', br, '
', "
", '
'; &CustomIndex($$ref{$key}, $nid); print '
'; } else { if ( defined(param('Browse')) && $nid eq param('Browse') ) { # User selected index item, list it print "
", img({-src=>$icon_open, -border=>0, -id=>"${uniqid}-icon"}), ' '. $name, br, '
', "
", '
', start_table{-width=>'100%'}; &Cache_Load; if ( ref($$ref{$key}) eq 'ARRAY' ) { &GraphIndex($$ref{$key}[0]) if ( defined($$ref{$key}[0]) ); &TemplateIndex($$ref{$key}[1]) if ( defined($$ref{$key}[1]) ); &DashBoardIndex($$ref{$key}[2]) if ( defined($$ref{$key}[2]) ); } elsif ( ! ref($$ref{$key}) ) { &GraphIndex($$ref{$key}); &TemplateIndex($$ref{$key}); &DashBoardIndex($$ref{$key}); } else { die 'Bad %Index entry ['. $key .']: '. ref($$ref{$key}) ."\n"; } print end_table, '
'; } else { # Not an open index item, just provide a link print a({-href=>MakeURL('Browse', $nid)}, img({-src=>$icon_closed, -border=>0}), ' '. $name), br; } } } } else { die 'Bad %Index entry: '. ref($ref) ."\n"; } } # Loading and Saving a Graph (or Template) definition sub Definition_Load { croak 'Definition_Load(file)' if ( scalar(@_) != 1); my ( $file ) = ( @_ ); die "Invalid Request: $file\n" unless ( $file =~ /^(g|t|d)*\d+\.\d+$/ ); if ( $file =~ /^(g|t|d)/ ) { $file = $saved_dir . '/' . $file; } else { $file = $tmp_dir . '/' . $file; } open(FILE, "< ${file}") or die "Could not open definition file \"$file\": $!\n";; restore_parameters(\*FILE); close FILE; my @plist = param(); while ( scalar(@plist) > 0) { Delete($plist[0]) if ( $plist[0] ne 'DS' && $plist[0] !~ /^[a-z]+_/ && $plist[0] !~ /^(g|t|d)/ ); # XXX Too vague.. shift @plist; } param('gFormat', 'PNG') if ( param('gFormat') eq 'GIF' && $RRDs::VERSION >= 1.2 ); param('Code', 'Hide'); } sub Definition_Save { croak 'Definition_Save(file, msg)' if ( scalar(@_) != 2); my ( $file, $log ) = ( @_ ); my $rcsuser; if ( $user ne 'guest' ) { $rcsuser = '-w'. $user; } else { $rcsuser = "-wguest[$ENV{REMOTE_ADDR}]"; } my $rcs; if ( $file =~ /^([dgt])(\d+\.\d+)/ ) { $file = $saved_dir . '/' . $1 . $2; # Untaint my $owner; $owner = $GraphsById{$2}{'Owner'} if ( $1 eq 'g' ); $owner = $TemplatesById{$2}{'Owner'} if ( $1 eq 't' ); $owner = $BoardsById{$2}{'Owner'} if ( $1 eq 'd' ); if ( $level == 0 || ( $level == 1 && -f "${file}" && ( !defined($owner) || $owner ne $user ) ) ){ print h1('Permission Denied'); return 0; } if ( $use_rcs ) { $rcs = new Rcs; $rcs->file($1 . $2); $rcs->workdir($saved_dir); $rcs->rcsdir($saved_dir .'/RCS'); if ( -f $saved_dir .'/RCS/'. $1 . $2 .',v' ) { if ( $rcs->co('-l') != 1 ) { &Error("Failed to check out $file"); return 0; } } } } else { $file = $tmp_dir . '/' . $file; } my $ok = 0; if ( open(FILE, "> ${file}") ) { save_parameters(\*FILE); close FILE; $ok = 1; } else { &Error("Could not save definition: $!"); } if ( $use_rcs && defined($rcs) ) { if ( $ok ) { if ( $rcs->ci('-u', '-m'. $log, $rcsuser) != 1 ) { &Error("Failed to check in $file"); $ok = 0; } } else { if ( $rcs->co('-u', '-f') != 1 ) { &Error("Failed to unlock $file"); } } } return $ok; } # Finding RRD files sub DBFind { @rrdfiles = (); @evtfiles = (); alarm(0); # This may take a while, and that'd be okay.. find({wanted=>\&DBFinder, untaint=>1, # Untaint, lame... untaint_pattern=>qr|^([-+@\w./:]+)$|}, keys(%datadirs)); alarm($maxtime); Cache_Save; } sub DBFinder { if ( -f $_ && ( /^.+\.rrd$/ || /^.+\.evt$/ ) ) { my $dir = $File::Find::dir; my $start; foreach $start ( keys(%datadirs) ) { $dir =~ s/^${start}(\/|$)//; if ( $dir ne $File::Find::dir ) { $dir .= '/' unless ( $dir eq '' ); if ( $_ =~ /\.rrd$/ ) { push @rrdfiles, $start . '//' . $dir . $_; } else { push @evtfiles, $start . '//' . $dir . $_; } return; } } die "Something is wrong in DBFinder... (". $File::Find::dir .")\n"; } } # Lists available RRD files for the user to pick sub DBChooser { my ( %rrd, %evt ); my $file; foreach $file ( sort(@rrdfiles) ) { if ( $file =~ /^(.+)\/\/(.+)\.rrd$/ ) { $rrd{$file} = $datadirs{$1} . " " . $2; } else { die "Bad (rrd) filename in DBChooser: $file\n"; } } foreach $file ( sort(@evtfiles) ) { if ( $file =~ /^(.+)\/\/(.+)\.evt$/ ) { $evt{$file} = $datadirs{$1} . " " . $2; } else { die "Bad (evt) filename in DBChooser: $file\n"; } } print p({-align=>'center'}, b('Filename filter regexp: '), textfield(-name=>'fn_FILTER', -default=>'', -size=>40), submit(-name=>'USERSAID', -value=>'Filter')); my @files; @files = sort { &datafnsort($rrd{$a}, $rrd{$b}) } (keys(%rrd)); my $filter = param('fn_FILTER'); @files = grep { $rrd{$_} =~ /$filter/ } @files if ( defined(param('fn_FILTER')) && param('fn_FILTER') ne '' ); print p({-align=>'center'}, b(em('Available Database Files')), br, submit(-name=>'USERSAID', -value=>'Refresh'), br, scrolling_list(-name=>'db_FILES', -multiple=>'true', -size=>10, -values=>\@files, -labels=>\%rrd, -default=>[])), p({-align=>'center'}, b('Data Source filter regexp: '), textfield(-name=>'db_FILTER', -default=>$dsfilter_def, -size=>40), br, b('Data Source RRA(s): '), checkbox_group(-name=>'db_RRAS', -values=>[@rranames], -label=>\%rranames), br, submit(-name=>'USERSAID', -value=>'Add DB(s) to data sources'), br, submit(-name=>'USERSAID', -value=>'RRD Info for selected DB')); @files = sort { &datafnsort($evt{$a}, $evt{$b}); } (keys(%evt)); @files = grep { $evt{$_} =~ /$filter/ } @files if ( defined(param('fn_FILTER')) && param('fn_FILTER') ne '' ); print p({-align=>'center'}, b(em('Available Event Files')), br, submit(-name=>'USERSAID', -value=>'Refresh'), br, scrolling_list(-name=>'evt_FILES', -multiple=>'true', -size=>10, -values=>\@files, -labels=>\%evt, -default=>[])), p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Add to data sources')); } # DS Addition: Validation and Initialization sub DBAdd { my ( $cf, $file ); if ( !defined(param('db_RRAS')) ) { &Error('You must specify at least one RRA to add!'); return; } my @dbs = param('db_FILES'); if ( scalar(@dbs) == 0 ) { &Error('You must select at least one DB file to add data source(s) from!'); return; } foreach $cf ( param('db_RRAS') ) { foreach $file ( param('db_FILES') ) { my ($start, $step, $DSnames, $data) = RRDs::fetch($file, $cf, '-s', 0, '-e', 0); my $ds; if ( defined($DSnames) && scalar(@{$DSnames}) > 0 ) { foreach $ds ( @{$DSnames} ) { my $filter = param('db_FILTER'); next if ( $ds !~ /$filter/ ); &DSNew($file, $ds, $cf); } } else { &Error("No data source for \"${cf}\" RRA found in file \"${file}\"!"); } } } } # DS Info: Validation and Initialization sub DBInfo { my ( $cf, $file ); my @dbs = param('db_FILES'); if ( scalar(@dbs) == 0 ) { &Error('You must select at least one DB file to get info from!'); return; } foreach $file ( param('db_FILES') ) { my $hash = RRDs::info($file); if (RRDs::error) { &Error("Error getting info for \"${file}\": ". RRDs::error); } else { print p({-align=>'center'}, b(em('Info for Database File ' . $file))); my $first = ''; foreach my $key (sort keys %$hash) { print "$key = $$hash{$key}", br unless ( $key =~ /^(ds|rra)\[/ ); } print start_table({-align=>'center', -border=>1}), 'DS'; foreach my $key (sort keys %$hash) { if ( $key =~ /^ds\[.+\]\.(.+)$/ ) { last if ( $first eq $1 ); $first = $1 if ( $first eq '' ); print th($1); } } $first = ''; foreach my $key (sort keys %$hash) { if ( $key =~ /^ds\[(.+)\]\..+$/ ) { if ( $first ne $1 ) { $first = $1; print '', td($first); } print td($$hash{$key}); } } print '', end_table, start_table({-align=>'center', -border=>1}), 'rra'; $first = ''; foreach my $key (sort keys %$hash) { if ( $key =~ /^rra\[[^[]+\]\.([^[]+)$/ ) { last if ( $first eq $1 ); $first = $1 if ( $first eq '' ); print th($1); } } $first = ''; foreach my $key (sort keys %$hash) { if ( $key =~ /^rra\[([^[]+)\]\.[^\[]+$/ ) { if ( $first ne $1 ) { $first = $1; print '', td($first); } print td($$hash{$key}); } } print '', end_table, start_table({-align=>'center', -border=>1}), 'rrads'; $first = ''; foreach my $key (sort keys %$hash) { if ( $key =~ /^rra\[.+\]\.cdp_prep\[.+\]\.(.+)$/ ) { last if ( $first eq $1 ); $first = $1 if ( $first eq '' ); print th($1); } } $first = ''; foreach my $key (sort keys %$hash) { if ( $key =~ /^rra\[(.+)\]\.cdp_prep\[(.+)\]\..+$/ ) { if ( $first ne "$1.$2" ) { $first = "$1.$2"; print '', td($1), td($2); } print td($$hash{$key}); } } print ''; print end_table; } } } sub EVTAdd { my ( $cf, $file ); my @evts = param('evt_FILES'); if ( scalar(@evts) == 0 ) { &Error("You must select at least one event file to add!"); return; } foreach $file ( param('evt_FILES') ) { my $new = &DSNew($file, '', ''); param(-name=>"${new}_Label", -value=>''); } } # DS Load: sub DSLoad { my $seq = 1; my $ds; foreach $ds ( sort { param("${a}_Seq") <=> param("${b}_Seq") } param('DS') ) { if ( defined($DS{$ds}) ) { &Error("Discarded duplicate DS definition: $ds"); next; } if ( defined(param("${ds}_DELETE")) ) { foreach ( grep(/^${ds}_/, param()) ) { Delete($_); } next; } # Automatically deduplicate Sequence numbers to be more friendly param("${ds}_Seq", $seq++); if ( defined(param("${ds}_NEW")) && param("${ds}_File") eq '' && param("${ds}_DS") eq '' && param("${ds}_Element") eq '' && param("${ds}_Formula") eq '' ){ Delete("${ds}_Formula"); } if ( defined(param("${ds}_Formula")) ) { my $fre = param("${ds}_File"); $fre =~ s!/+!/!g; param(-name=>"${ds}_File", -value=>$fre); $DS{$ds} = param("${ds}_File"); param(-name=>"${ds}_Type", -value=>'-Nothing-') if ( param("${ds}_Formula") eq "" ); } elsif ( param("${ds}_File") ne '' ) { if ( param("${ds}_File") =~ /^(.+)\/\/(.+).(rrd|evt)$/ ) { $DS{$ds} = $datadirs{$1} . " " . $2; } else { &Error("Invalid filename for DS \"$ds\": " . param("${ds}_File")); next; } } else { $DS{$ds} = ''; &Error("CDEF ${ds} configured but NOT defined!") if ( param("${ds}_CDEF") eq '' ); } param(-name=>"${ds}_Color", -value=>'White') if ( param("${ds}_Type") =~ /(PRINT|COMMENT|-Nothing-)/ ); } param(-name=>'DS', -value=>[keys(%DS)]); } # DS Config: Lists chosen DS for the user to configure sub DSConfig { my $color; print '

', start_table({-border=>1,-align=>'center'}), caption(em(b('Available Colors'))), ''; foreach $color ( sort Sort_Colors (keys(%colors)) ) { print "var colors = [ '; foreach $color ( keys(%colors) ) { print "\"${color}\", \"$colors{$color}\", "; } print ']'; my $new = &DSNew('', '', ''); param("${new}_DELETE", 'Y'); my ( %rrd, %evt ); my $file; foreach $file ( sort(@rrdfiles) ) { if ( $file =~ /^(.+)\/\/(.+)\.rrd$/ ) { $rrd{$file} = $datadirs{$1} . " " . $2; } } foreach $file ( sort(@evtfiles) ) { if ( $file =~ /^(.+)\/\/(.+)\.evt$/ ) { $evt{$file} = $datadirs{$1} . " " . $2; } } my %rra = map{ $_ => '' } @rranames; my $ds; foreach $ds ( keys(%DS) ) { print "\n"; } print p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Update')), start_table({-border=>1,-width=>'100%'}), caption(em(b('Data Source Configuration'), a({-href=>MakeURL('Browse', 'Help') . '#dsconfig', -target=>'drrawhelp'}, small('Help')))), Tr({-bgcolor=>'silver'}, th(['DEL'. br .'?', 'Name'. br .'Seq', 'Data Source ('. checkbox(-name=>'DSFList', -label=>'Lists', -value=>'Y') .')', 'RRA'. br .'CDEF', 'Type'. br .'Color', 'Label / Format', 'Additional GPRINTs'. br . small({-style=>'cursor:pointer'}, join('/', map { span({-onclick=>"ToggleXTRA(\"$_\")"}, $rranames{$_}) } @rranames)). br . small(radio_group(-name=>'XTRAS', -values=>['On', 'Off'])), small({-style=>'cursor:pointer', -onclick=>'ToggleXTRA("BR")'}, 'BR')])); foreach $ds ( sort { param("${a}_Seq") <=> param("${b}_Seq") } keys(%DS) ) { Delete("${ds}_DELETE") unless ( $ds eq $new ); if ( $ds eq $new ) { print Tr(td({-colspan=>8, -align=>'center', -bgcolor=>'silver'}, 'Use the following row to add a new CDEF to the graph, or to define a DS based on a perl regular expression.')); } if ( param("${ds}_File") =~ /\.rrd$/ || param("${ds}_File") eq '' || defined(param("${ds}_Formula")) ) { my $rowcount = 1; my $filecell; if ( defined(param("${ds}_Formula")) || $ds eq $new ) { # CDEF Template $filecell = br .'File RE:' .textfield(-name=>"${ds}_File", -class=>'small', -default=>param("${ds}_File"), -size=>40) .br .'DS:' .textfield(-name=>"${ds}_DS", -class=>'small', -default=>param("${ds}_DS")) .br .'Element:' .textfield(-name=>"${ds}_Element", -class=>'small', -default=>param("${ds}_Element")) .'Formula:' .textfield(-name=>"${ds}_Formula", -class=>'small', -default=>param("${ds}_Formula")); $filecell .= hidden("${ds}_NEW", ''); $rowcount = 2 unless ( $ds eq $new ); } elsif ( param("${ds}_File") eq '' ) { # Pure CDEF $filecell = 'CDEF Definition'. hidden("${ds}_File", ''); } else { # Regular DS if ( defined(param('DSFList')) && param('DSFList') eq 'Y' ) { $filecell = popup_menu(-name=>"${ds}_File", -class=>'small', -values=>[sort { &datafnsort($rrd{$a}, $rrd{$b}) } keys(%rrd)], -labels=>\%rrd, -default=>$DS{$ds}); } else { $filecell = hidden("${ds}_File") . $DS{$ds}; } $filecell .= br .b(param("${ds}_DS")) .hidden("${ds}_DS", param("${ds}_DS")); } print Tr({-class=>'small'}, td({-align=>'center', -rowspan=>$rowcount}, checkbox(-name=>"${ds}_DELETE", -label=>'', -value=>'Y', -onclick=>"DisableToggle(\"${ds}\")")), td(b($ds) . ':' . textfield(-name=>"${ds}_Seq", -class=>'small', -default=>param("${ds}_Seq"), -size=>2)), td($filecell), td(popup_menu(-name=>"${ds}_RRA", -class=>'small', -values=>[@rranames], -labels=>\%rranames, -default=>param("${ds}_RRA")) . br, textfield(-name=>"${ds}_CDEF", -class=>'small', -default=>param("${ds}_CDEF"), -size=>11)), td({-bgcolor=>$colors{param("${ds}_Color")}, -id=>"${ds}_Colorize"}, popup_menu(-name=>"${ds}_Type", -class=>'small', -values=>[@graphtypes], -default=>param("${ds}_Type"), -onchange=>"CheckBgColor(this, \"${ds}_Colorize\")"), popup_menu(-name=>"${ds}_Color", -class=>'small', -values=>[sort Sort_Colors keys(%colors)], -default=>param("${ds}_Color"), -onchange=>"SetBgColor(this, \"${ds}_Colorize\")")), td(textfield(-name=>"${ds}_Label", -class=>'small', -default=>param("${ds}_Label"), -size=>15)), td({-align=>'center'}, checkbox_group(-name=>"${ds}_XTRAS", -values=>[@rranames], -labels=>\%rra, -defaults=>param("${ds}_XTRAS"))), td({-align=>'center'}, checkbox(-name=>"${ds}_BR", -label=>'', -value=>'Y'))); print Tr({-class=>'small'}, td({-align=>'center'}, 'N/A'), td(checkbox_group(-name=>"${ds}_tColors", -values=>[sort Sort_Colors keys(%colors)], -defaults=>param("${ds}_tColors"))), td($rranames{param("${ds}_RRA")}, br, textfield(-name=>"${ds}_tCDEF", -class=>'small', -size=>11, -default=>param("${ds}_tCDEF"))), td(popup_menu(-name=>"${ds}_tType", -class=>'small', -values=>[grep(!/RULE/, @graphtypes)], -default=>param("${ds}_tType")), checkbox(-name=>"${ds}_tSTACK", -values=>'Y', -label=>'STACK?')), td(textfield(-name=>"${ds}_tLabel", -class=>'small', -size=>15, -default=>param("${ds}_tLabel"))), td({-align=>'center'}, checkbox_group(-name=>"${ds}_tXTRAS", -values=>[@rranames], -labels=>\%rra, -defaults=>param("${ds}_tXTRAS"))), td({-align=>'center'}, checkbox(-name=>"${ds}_tBR", -label=>'', -value=>'Y'))) if ( $rowcount == 2 ); } elsif ( param("${ds}_File") =~ /\.evt$/ ) { print Tr({-class=>'small'}, td({-align=>'center'}, checkbox(-name=>"${ds}_DELETE", -label=>'', -value=>'Y', -onclick=>"DisableToggle(\"${ds}\")")), td(b($ds) . ':' . textfield(-name=>"${ds}_Seq", -class=>'small', -default=>param("${ds}_Seq"), -size=>2)), td(( defined(param('DSFList')) && param('DSFList') eq 'Y' )? popup_menu(-name=>"${ds}_File", -class=>'small', -values=>[sort { &datafnsort($evt{$a}, $evt{$b}) } keys(%evt)], -labels=>\%evt, -default=>$DS{$ds}) : (hidden("${ds}_File") . $DS{$ds}), br, 'Event Filter Regex: ', textfield(-name=>"${ds}_EvtFilter", -class=>'small', -default=>param("${ds}_EvtFilter"))), td({-align=>'center'}, 'N/A'), td({-bgcolor=>$colors{param("${ds}_Color")}, -id=>"${ds}_Colorize"}, popup_menu(-name=>"${ds}_Type", -class=>'small', -values=>['VRULE'], -default=>param("${ds}_Type"), -onchange=>"CheckBgColor(this, \"${ds}_Colorize\")"), popup_menu(-name=>"${ds}_Color", -class=>'small', -values=>[sort Sort_Colors keys(%colors)], -default=>param("${ds}_Color"), -onchange=>"SetBgColor(this, \"${ds}_Colorize\")")), td(textfield(-name=>"${ds}_Label", -class=>'small', -default=>param("${ds}_Label"), -size=>15)), td({-align=>'center'}, 'N/A'), td({-align=>'center'}, checkbox(-name=>"${ds}_BR", -label=>'', -value=>'Y'))); } } print end_table, p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Update')); } # GOptions: Lists general options for the user to set sub GOptions { print table({-border=>1,-align=>'center'}, caption(em(b('Graph Options'))), '', '', Tr(th({-align=>'center'}, 'Options'), th({-align=>'center'}, 'Values')), Tr(td('Graph Title'), td(textfield(-name=>'gTitle', -size=>30, -class=>'normal', -default=>param('gTitle')))), Tr(td('Vertical Label'), td(textfield(-name=>'gVLabel', -size=>30, -class=>'normal', -default=>param('gVLabel')))), Tr(td('Base'), td(popup_menu(-name=>'gBase', -values=>['1000', '1024'], -default=>param('gBase')))), Tr(td('Units Exponent'), td(popup_menu(-name=>'gUExp', -values=>['', '-18', '-15', '-12', '-9', '-6', '-3', '0', '3', '6', '9', '12', '15', '18'], -default=>param('gUExp')))), Tr(td('Graph Width'), td(textfield(-name=>'gWidth', -size=>30, -class=>'normal', -default=>(param('gWidth') ? param('gWidth') : 500)))), Tr(td('Graph Height'), td(textfield(-name=>'gHeight', -size=>30, -class=>'normal', -default=>(param('gHeight') ? param('gHeight') : 120)))), Tr(td('Upper Limit', br, br, 'Lower Limit', br, br, 'Rigid Boundaries'), td(textfield(-name=>'gYUp', -size=>30, -class=>'normal', -default=>param('gYUp')), br, textfield(-name=>'gYLow', -size=>30, -class=>'normal', -default=>param('gYLow')), br, checkbox(-name=>'gRigid', -label=>'', -class=>'normal', -default=>param('gRigid')))), Tr(td('Y Grid'), td(radio_group(-name=>'gYGrid', -class=>'normal', -values=>['Auto', 'None', 'Alternate', 'MRTG'], -default=>param('gYGrid')))), Tr(td('Alternate Autoscale'), td(radio_group(-name=>'gAuto', -class=>'normal', -values=>['No', 'Yes', 'Max Only'], -default=>param('gAuto')))), Tr(td('Logarithmic Auto Scaling'), td(checkbox(-name=>'gLog', -label=>'', -class=>'normal', -default=>param('gLog')))), Tr(td('Image Format'), td(popup_menu(-name=>'gFormat', -class=>'normal', -values=>[@ImgFormat], -default=>param('gFormat')))), Tr(td('Additional rrdgraph Options'), td(textfield(-name=>'gRaw', -size=>30, -class=>'normal', -default=>param('gRaw'))))), p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Update')); } sub DRAW_Element { my ( $ds, $rra, $cdef, $type, $color, $label, $br, @xtras ) = ( @_ ); return () if ( $type eq '-Nothing-' ); my @ELEM = (); my $gp = scalar(@xtras) - 1; $label =~ s/:/\\:/g; if ( $type eq 'COMMENT' ) { push @ELEM, join(':', $type, $label . (( $gp == -1 ) ? $br : '')); } elsif ( $type =~ /PRINT$/ ) { push @ELEM, join(':', $type, (( $cdef eq '' ) ? $ds : uc($ds)), $rra, $label . (( $gp == -1 ) ? $br : '')); } elsif ( $type =~ /RULE$/ ) { push @ELEM, join(':', $type, $cdef . $colors{$color}, $label . (( $gp == -1 ) ? $br : '')); } else { push @ELEM, join(':', $type, (( $cdef eq '' ) ? $ds : uc($ds)) . $colors{$color}, $label . (( $gp == -1 ) ? $br : '')); } my $i = 0; while ( $i <= $gp ) { push @ELEM, join(':', 'GPRINT', (( $cdef eq '' ) ? $ds : uc($ds) ), $xtras[$i], $rranames{$xtras[$i]} . "\\: ". $gformat ."%s" . (( $i == $gp ) ? $br : '')); $i += 1; } return @ELEM; } # DRAW: Build "rrdtool graph" arguments, and either call it or display them # mode: 0 -> Dry run for errors, report any and optionally show the code # 1 -> Produce image, be quiet about errors # 2 -> Produce image, log errors to STDERR sub DRAW { croak 'DRAW(mode)' if ( scalar(@_) != 1 ); my ( $mode ) = ( @_ ); my ( @DEF, @CDEF, @ELEM, @Options ) = ( (), (), (), () ); my ( $startts, $endts ) = ( 0, time ); if ( defined(&RRDs::times) ) { ( $startts, $endts ) = RRDs::times(( defined(param('Start')) && param('Start') ne '' ) ? param('Start') : 'end - 1 day', ( defined(param('End')) && param('End') ne '' ) ? param('End') : 'now' ); if ( defined(RRDs::error) ) { if ( $mode == 0 ) { &Error(RRDs::error); return 0; } else { die 'Failed to produce graph: '. RRDs::error; } } die "Invalid result ($startts) from RRDs::times()\n" unless ( $startts =~ /(\d+)/ ); $startts = $1; # Untaint die "Invalid result ($endts) from RRDs::times()\n" unless ( $endts =~ /(\d+)/ ); $endts = $1; # Untaint } my $pad = "\001"; my $ds; foreach $ds ( sort { param("${a}_Seq") <=> param("${b}_Seq") } keys(%DS) ) { if ( param("${ds}_File") !~ /\.evt$/ ) { if ( defined(param("${ds}_Formula")) ) { my $file; my $re = param("${ds}_File"); my $count = 0; my @colors = param("${ds}_tColors"); my $dsdef = ''; foreach $file ( sort(@rrdfiles) ) { next unless ( $file =~ /\.rrd$/ ); if ( $file =~ /$re/ ) { $file =~ s/:/\\:/g; push @DEF, join(':', 'DEF', $ds . $count .'='. $file, param("${ds}_DS"), param("${ds}_RRA")); if ( $dsdef eq '' ) { $dsdef = $ds . $count; $dsdef .= ','. param("${ds}_Element") if ( param("${ds}_Element") ne '' ); $dsdef =~ s/\$/$ds$count/g; } else { $dsdef .= ','. $ds . $count; $dsdef .= ','. param("${ds}_Element") if ( param("${ds}_Element") ne '' ); $dsdef .= ','. param("${ds}_Formula"); $dsdef =~ s/\$/$ds$count/g; } if ( param("${ds}_tCDEF") ne '' ) { my $cdef = param("${ds}_tCDEF"); $cdef =~ s/\$/$ds$count/g; push @CDEF, join(':', 'CDEF', uc($ds . $count) .'='. $cdef); } my $label = param("${ds}_tLabel"); my @subs = ( $file =~ /$re/ ); $label = &ExpandMatches($label, \@subs); $pad = "" if ( $label =~ /(%|\\)/ || $label =~ /^\s*$/); my $type = param("${ds}_tType"); $type = 'STACK' if ( $count > 0 && defined(param("${ds}_tSTACK")) && param("${ds}_tSTACK") ); push @ELEM, &DRAW_Element($ds . $count, param("${ds}_RRA") .'', param("${ds}_tCDEF"), $type, ( scalar(@colors) == 0 ) ? param("${ds}_Color") : $colors[$count%scalar(@colors)], $pad . $label . $pad, defined(param("${ds}_tBR")) ? '\n' : '', param("${ds}_tXTRAS")); $pad = ( defined(param("${ds}_tBR")) ) ? "\001" : ""; $count += 1; } } push @CDEF, join(':', 'CDEF', $ds .'='. $dsdef) unless ( param("${ds}_Formula") eq '' ); } else { my $file = param("${ds}_File"); $file =~ s/:/\\:/g; push @DEF, join(':', 'DEF', $ds .'='. $file, param("${ds}_DS"), param("${ds}_RRA")) unless ( param("${ds}_File") eq '' ); } if ( param("${ds}_CDEF") ne '' && param("${ds}_Type") !~ /RULE$/ ) { my $cdef = param("${ds}_CDEF"); $cdef =~ s/\$/$ds/g; push @CDEF, join(':', 'CDEF', uc($ds) .'='. $cdef); } $pad = "" if ( param("${ds}_Label") =~ /(%|\\)/ || param("${ds}_Label") =~ /^\s*$/ ); push @ELEM, &DRAW_Element($ds, param("${ds}_RRA") .'', param("${ds}_CDEF"), param("${ds}_Type"), param("${ds}_Color"), $pad . param("${ds}_Label") . $pad, defined(param("${ds}_BR")) ? '\n' : '', param("${ds}_XTRAS")); $pad = ( defined(param("${ds}_BR")) ) ? "\001" : ""; } else { open EVENTS, "< " . param("${ds}_File") or &Error("Failed to open ". param("${ds}_File") . ": $!\n"); my $format; while () { chomp; next if ( /^\#/ ); if ( /^(\d+)\s+(.+)$/ ) { my ( $ts, $msg ) = ( $1, $2 ); next if ( defined(&RRDs::times) && ( $ts < $startts || $ts > $endts ) ); my $re = param("${ds}_EvtFilter"); if ( !defined($format) ) { # Try to be smart about the timestamps format my @now = localtime; my @then = localtime($1); if ( ( $then[5] == $now[5] - 1 && $then[7] < $now[7] ) || $then[5] < $now[5] -1 ) { # More than a year ago.. $format = "%a %b %e %H:%M %Y"; } elsif ( ( $then[5] == $now[5] && $then[7] < $now[7] - 30 ) || ( $then[5] == $now[5] -1 && $then[7] - 365 < $now[7] - 30 ) ) { # More than a month ago.. $format = "%a %b %e %H:%M"; } elsif ( ( $then[5] == $now[5] && $then[7] < $now[7] - 30 ) || ( $then[5] == $now[5] -1 && $then[7] - 365 < $now[7] - 7 ) ) { # More than a week ago.. $format = "%a %e %H:%M"; } else { $format = "%a %H:%M"; } } $msg = '['. strftime($format, localtime($1)) .'] '. $msg; next unless ( $re eq '' || $msg =~ /$re/ ); $msg = param("${ds}_Label") if ( defined(param("${ds}_Label")) && param("${ds}_Label") ne '' ); $msg =~ s/:/\\:/g; $msg .= '\n' if ( defined(param("${ds}_BR")) ); push @ELEM, join(':', 'VRULE', $ts . $colors{param("${ds}_Color")},$msg); } else { &Error("Bad Entry in ". param("${ds}_File") . ": $_\n"); } } close EVENTS; } } my $i = 0; my $maxlen = 0; while ( $i < scalar(@ELEM) ) { if ( $ELEM[$i] =~ /\001(.+)\001/ ) { $maxlen = length($1) if ( $maxlen < length($1) ); } $i += 1; } $i = 0; while ( $i < scalar(@ELEM) ) { if ( $ELEM[$i] =~ /\001(.+)\001/ ) { $pad = ' ' x ( $maxlen - length($1) ); $pad .= ' ' if ( $ELEM[$i] =~ /^COMMENT:/ ); $ELEM[$i] =~ s/\001(.+)\001/$1$pad/; } $i += 1; } if ( defined(param('Start')) && param('Start') ne '' ) { push @Options, '--start=' . param('Start'); } else { push @Options, '--start=end-86400'; } push @Options, '--end=' . param('End') if ( defined(param('End')) && param('End') ne '' && param('End') ne 'now' ); my $option; foreach $option ( keys(%goptions) ) { push @Options, $goptions{$option} . '=' . param("$option") if ( defined(param("$option")) && param("$option") ne '' ); } push @Options, '--rigid' if ( defined(param('gRigid')) ); if ( param('gYGrid') eq 'None' ) { push @Options, '--y-grid', 'none'; } elsif ( param('gYGrid') eq 'Alternate' ) { push @Options, '--alt-y-grid'; } elsif ( param('gYGrid') eq 'MRTG' ) { push @Options, '--alt-y-mrtg'; } if ( param('gAuto') eq 'Yes' ) { push @Options, '--alt-autoscale'; } elsif ( param('gAuto') eq 'MaxOnly' ) { push @Options, '--alt-autoscale-max'; } push @Options, '--logarithmic' if ( defined(param('gLog')) ); push @Options, '--interlaced'; push @Options, '--no-legend' if ( defined(param('gNoLegend')) ); push @Options, split(/ /, param('gRaw')) if ( defined(param('gRaw')) ); my ( $out, $ttl ); if ( $mode > 0 ) { if ( defined(&RRDs::times) ) { if ( defined(param('gWidth')) && param('gWidth') =~ /(\d+)/ ) { $ttl = int(($endts - $startts + 1) / $1); # Untaint } else { $ttl = int(($endts - $startts + 1) / 400 ); } } else { # Find out whether this is a configured view my $view = scalar(@dv_name); if ( defined(param('Start')) && ( !defined(param('End')) || param('End') eq '' || param('End') eq 'now' ) ) { $view = 0; while ( $view < scalar(@dv_name) ) { last if ( $dv_def[$view] eq param('Start') ); $view += 1; } } if ( $view >= scalar(@dv_secs) ) { # TTL defaults to 5m $ttl = 300; } else { if ( defined(param('gWidth')) && param('gWidth') =~ /(\d+)/ ) { $ttl = int($dv_secs[$view] / $1); # Untaint } else { $ttl = int($dv_secs[$view] / 400); } } } $ttl = 60 if ( $ttl < 60 ); # Enforce a sane 1m minimum # Now, let's figure out if there's anything usable cached if ( $clean_cache ) { my $cid = Digest::MD5::md5_hex(@Options, @DEF, @CDEF, @ELEM); my @cached = glob($tmp_dir ."/cached-image-". $cid ."-*"); while ( scalar(@cached) > 0 ) { my $hit = $cached[0]; $hit =~ s/.+cached-image-[^-]+-//; if ( $hit eq '' ) { if ( $cached[0] =~ /(.+)/ && (time - (stat($cached[0]))[9]) > 900 ) { unlink $1; # Untaint } } elsif ( -z $cached[0] || $hit < time ) { # Delete any file expired for over 2 minutes if ( $cached[0] =~ /(.+)/ && time - $hit > 120 ) { unlink $1; # Untaint } } else { # Usable cached file, is it the latest? $out = $cached[0] unless ( defined($out) && (stat($out))[9] > (stat($cached[0]))[9] ); } shift @cached; } if ( !defined($out) || ! -r $out ) { # Nothing usable in cache, will have to generate a new file $cid =~ /(.+)/; $out = $tmp_dir ."/cached-image-". $1 ."-"; # Untaint } } else { $out = '-'; print header(-type=>$Mime{param('gFormat')}, -expires=>'+'. $ttl .'s', -last_modified=>&time2str()); } } elsif ( $Config{'osname'} eq 'MSWin32' ) { $out = 'NUL:'; } else { $out = '/dev/null'; } RRDs::graph($out, @Options, @DEF, @CDEF, @ELEM) unless ( $mode > 0 && $out ne '-' && -r $out ); if ( defined(RRDs::error) ) { if ( $mode == 0 ) { &Error(RRDs::error); } else { if ( $mode == 2 ) { warn 'Failed to produce graph: ' . RRDs::error . "\n"; } unlink $out # Wipe out whatever may have been created on disk unless ( $out eq '-' ); } } elsif ( $mode > 0 && $out ne '-' ) { my $lm = &time2str((stat($out))[9]); if ( $out =~ /-$/ ) { # New cache file, let's rename it so that others can use it. my $new = $out . ( time + $ttl ); if ( !rename($out, $new) ) { warn 'Failed to rename temporary cache file "'. $out .'": '. "$!\n"; open IMG, "< $out" or die "Failed to open temporary cached file $out: $!\n"; } else { open IMG, "< $new" or die "Failed to open renamed cached file $new: $!\n"; } $out = time + $ttl; } else { open IMG, "< $out" or die "Failed to open existing cached file $out: $!\n"; # Extract the expiration date $out =~ s/.+cached-image-[^-]+-//; } binmode(IMG); if ( !defined($ENV{'HTTP_IF_MODIFIED_SINCE'}) || $ENV{'HTTP_IF_MODIFIED_SINCE'} ne $lm ) { print header(-type=>$Mime{param('gFormat')}, -expires=>&time2str($out), -last_modified=>$lm); while () { print; }; } else { # Apache would do this for us, but why bother sending the data? print header(-status=>'304', -type=>$Mime{param('gFormat')}, -expires=>&time2str($out), -last_modified=>$lm); } if ( !close(IMG) ) { warn "Problems while sending cached file: $!\n"; } if ( ( ! -e "${tmp_dir}/purged" || time - (stat($tmp_dir .'/purged'))[9] > $clean_cache ) && open(TS, "> ${tmp_dir}/purged") ) { print TS "oink!\n"; close TS; my @cached = glob($tmp_dir ."/cached-image-*"); while ( scalar(@cached) > 0 ) { my $expires = $cached[0]; $expires =~ s/.+cached-image-[^-]+-//; if ( $expires eq '' ) { # Should only happen for files _being_ generated, but.. if ( $cached[0] =~ /(.+)/ && (time - (stat($cached[0]))[9]) > 900 ) { unlink $1; # Untaint } } elsif ( $expires < time ) { # Delete any file expired for over 2 minutes if ( $cached[0] =~ /(.+)/ && time - $expires > 120 ) { unlink $1; # Untaint } } shift @cached; } } } if ( $mode == 0 ) { if ( defined(param('Code')) && param('Code') eq 'Show' ) { print pre({-class=>'code', -id=>'CODE'}, "rrdtool graph - \\\n " . join(" \\\n ", @Options, @DEF, @CDEF, @ELEM)); } else { print pre({-class=>'code', -id=>'CODE', -style=>'display:none'}, "rrdtool graph - \\\n " . join(" \\\n ", @Options, @DEF, @CDEF, @ELEM)); } } return 0 if ( defined(RRDs::error) ); return 1; } # GraphURL: Return a graph URL sub GraphURL { croak 'GraphURL(name, base, [start [, end, [width, height, nolegend]]])' if ( scalar(@_) < 2 || scalar(@_) > 7 ); my ( $name, $base, $start, $end, $width, $height, $nolegend ) = ( @_ ); my $query = new CGI; $query->delete_all; $query->param('Mode', 'show'); if ( $name !~ /^t/ ) { $query->param('Graph', $name); } else { $query->param('Template', $name); $query->param('Base', $base) if ( defined($base) ); } $query->param('Start', $start) if ( defined($start) ); $query->param('End', $end) if ( defined($end) ); $query->param('Width', $width) if ( defined($width) ); $query->param('Height', $height) if ( defined($height) ); $query->param('NoLegend', 1) if ( defined($nolegend) ); return $query->url(-path_info=>1, -query=>1, -relative=>1); } sub TMPLFind { croak 'TMPLFind(filter, display)' unless ( scalar(@_) == 0 || scalar(@_) == 2 ); my ( $ex, $nex ) = ( @_ ); my %once; %TMPL = (); return unless ( defined($ex) && $ex ne '' ); foreach ( @rrdfiles ) { my $label = $_; my $base = $_; $label =~ s/.*\/\///; my @subs = ( $label =~ $ex ); next unless ( scalar(@subs) > 0 ); if ( defined($nex) && $nex ne '' ) { $label = &ExpandMatches($nex, \@subs); } else { $label = join(' - ', ($label =~ /$ex/)); } next if ( defined($once{$label}) ); $once{$label} = 1; $TMPL{$base} = $label; } } sub TMPLConfig { print p(table({-width=>'70%', -align=>'center'}, Tr(td({-class=>'help'}, &help_templates)))) if ( param('USERSAID') eq 'Make Template' ); print p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Update')), start_table({-border=>1,-align=>'center'}, caption(em(b('Template Settings')), a({-href=>MakeURL('Browse', 'Help') .'#templates', -target=>'drrawhelp'}, small('Help'))), '', ''), Tr(th({-align=>'center'}, 'Setting'), th({-align=>'center'}, 'Values')), Tr(td('Template Name'), td(textfield(-name=>'tName', -size=>40, -class=>'normal', -default=>param('tName')))), Tr(td('Base Regular Expression'), td(textfield(-name=>'tRegex', -size=>40, -class=>'normal', -default=>param('tRegex')))), Tr(td('Selection Regular Expression'), td(textfield(-name=>'tNiceRegex', -size=>40, -class=>'normal', -default=>param('tNiceRegex')))); my ( %once, $ds, @subs ); if ( scalar(keys(%TMPL)) > 0 ) { my ( @tmp, $best, @count ); $best = 0; # It'd work better with two passes, but this should be enough foreach $ds ( sort keys(%DS) ) { next if ( param("${ds}_File") eq '' ); next if ( defined(param("${ds}_Formula")) ); my $ex = param('tRegex'); my $dbname = substr(param("${ds}_File"), 2+index(param("${ds}_File"), '//')); @tmp = ( $dbname =~ /$ex/ ); next if ( scalar(@tmp) == 0 ); if ( !defined(param("${ds}_Tmpl")) || param("${ds}_Tmpl") eq '' ) { param(-name=>"${ds}_Tmpl", -value=>&ReduceMatches($dbname, \@tmp)); } else { next unless ( $dbname eq &ExpandMatches(param("${ds}_Tmpl"), \@tmp) ); } @count = ( param("${ds}_Tmpl") =~ /(\$\d+)/g ); if ( scalar(@count) > $best ) { @subs = @tmp; $best = scalar(@count); } } } foreach $ds ( sort keys(%DS) ) { next if ( param("${ds}_File") eq '' ); next if ( $once{$DS{$ds}} ); $once{$DS{$ds}} = 1; my $class = 'normal'; my $dbname; if ( !defined(param("${ds}_Formula")) ) { $dbname = substr(param("${ds}_File"), 2+index(param("${ds}_File"),'//')); } else { $dbname = param("${ds}_File"); } if ( scalar(@subs) > 0 ) { if ( param("${ds}_Tmpl") eq '' ) { param(-name=>"${ds}_Tmpl", -value=>&ReduceMatches($dbname, \@subs)); } else { $class = 'smallred' if ( $dbname ne &ExpandMatches(param("${ds}_Tmpl"), \@subs) ); } } else { $class = 'red'; } print Tr(td($dbname), td(textfield(-name=>"${ds}_Tmpl", -class=>$class, -size=>40, -default=>param("${ds}_Tmpl")))); } print end_table, p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Update')); if ( defined(param('tRegex')) && param('tRegex') ne '' ) { if ( scalar(keys(%TMPL)) == 0 ) { &Error("Base Regular Expression produced no match!"); } else { print p({-align=>'center'}, 'There were ' . scalar(keys(%TMPL)) . ' match for the Base Regular Expression: ', popup_menu(-name=>'TempPopup', -class=>'small', -values=>[sort(keys(%TMPL))])); } } } sub BoardFind { croak 'BoardFind(board)' unless ( scalar(@_) == 1 ); my ( $board ) = ( @_ ); die "Undefined board: ${board}" unless ( defined($BoardsById{$board}) ); %DBTMPL = (); return unless ( defined($BoardsById{$board}{'Filters'}) ); my $item; foreach $item ( keys(%{$BoardsById{$board}{'Filters'}}) ) { &TMPLFind($TemplatesById{$BoardsById{$board}{'Filters'}{$item}{'Template'}}{'Filter'}, $TemplatesById{$BoardsById{$board}{'Filters'}{$item}{'Template'}}{'Display'}); my $key; foreach $key ( keys(%TMPL) ) { my $filter = $BoardsById{$board}{'Filters'}{$item}{'Filter'}; next if ( defined($filter) && $filter ne '' && $TMPL{$key} !~ /$filter/ ); my $display = $TMPL{$key}; $filter = $BoardsById{$board}{'Filters'}{$item}{'Display'}; $display = join(' - ', ($display =~ /$filter/)) if ( defined($filter) && $filter ne '' ); $DBTMPL{$display}{$item} = $key; } } } sub BoardOptions { print p(table({-width=>'70%', -align=>'center'}, Tr(td({-class=>'help'}, &help_dbintro)))) unless ( scalar(grep(/_Seq/, param())) > 0 ); print table({-border=>1,-align=>'center'}, caption(em(b('Dashboard Options'), a({-href=>MakeURL('Browse', 'Help') . '#dbintro', -target=>'drrawhelp'}, small('Help')))), '', '', Tr(th({-align=>'center'}, 'Options'), th({-align=>'center'}, 'Values')), Tr(td('Dashboard Title'), td(textfield(-name=>'dTitle', -size=>20, -default=>param('dTitle')))), Tr(td('Type'), ( !defined(param('dGrouped')) ) ? td(b('Standard'), submit(-name=>'USERSAID', -value=>'Switch')) : td(b('Grouped'), submit(-name=>'USERSAID', -value=>'Switch'))), ( defined(param('dGrouped')) ) ? '' : Tr(td('Columns'), td(textfield(-name=>'dCols', -size=>20, -default=>(param('dCols') ? param('dCols') : 2)))), Tr(td('Preview Width'), td(textfield(-name=>'dWidth', -size=>20, -default=>(param('dWidth') ? param('dWidth') : 300)))), Tr(td('Preview Height'), td(textfield(-name=>'dHeight', -size=>20, -default=>(param('dHeight') ? param('dHeight') : 100)))), Tr(td('Suppress Legends'), td(radio_group(-name=>'dNoLegend', -values=>['On', 'Off'], -default=>param('dNoLegend')))), Tr(td('Default View'), td(popup_menu(-name=>'dView', -values=>[@dv_name])))); } sub BoardConfig { my $item; # First add 10 more rows my $seq = 1; $item = 'a'; while ( $seq++ < 10 ) { while ( defined(param("${item}_Seq")) ) { if ( $item =~ /(z+)$/ ) { my $zzz = $1; $zzz =~ s/z/a/g; substr($item, length($item)-length($zzz)) = $zzz; if ( length($zzz) < length($item) ) { my $chr = substr($item, length($item)-length($zzz)-1, 1); substr($item, length($item)-length($zzz)-1, 1) = chr(ord($chr) + 1); } else { $item .= 'a'; } } else { my $chr = substr($item, length($item)-1, 1); substr($item, length($item)-1) = chr(ord($chr) + 1); } } param("${item}_DELETE", 'Y'); param("${item}_Seq", 99999); param("${item}_new", 1); } # Second, get the labels ready my %pretty; if ( !defined(param('dGrouped')) ) { foreach ( keys(%GraphsById) ) { $pretty{'g' . $_} = $GraphsById{$_}{'Name'} .' (Graph)'; } } foreach ( keys(%TemplatesById) ) { $pretty{'t' . $_} = $TemplatesById{$_}{'Name'} . ' (Template)'; } print p(table({-width=>'70%', -align=>'center'}, Tr(td({-class=>'help'}, &help_dbconfig)))) unless ( scalar(grep(/_Seq/, param())) > 0 && param('USERSAID') ne 'Switch' ); print start_table({-border=>1, -align=>'center'}), caption(em(b('Dashboard Configuration'), a({-href=>MakeURL('Browse', 'Help') . '#dbconfig', -target=>'drrawhelp'}, small('Help')))), Tr(th(['DEL ?', 'Seq', ( defined(param('dGrouped')) ) ? 'Template' . br . 'Available Template Selections' : 'Graph / Template' . br . 'Available Template Selections', 'Template Style', 'Template Base', ( defined(param('dGrouped')) ) ? 'Row Name' : 'Selection Regular Expression'])); $seq = 1; foreach $item ( sort { param($a) <=> param($b) } grep(/^[a-z]+_Seq$/, param()) ) { $item =~ s/_Seq//; next if ( defined(param("${item}_DELETE")) && !defined(param("${item}_new")) ); next if ( defined(param('dGrouped')) && defined(param("${item}_dname")) && param("${item}_dname") !~ /^t/ ); param("${item}_Seq", $seq); $seq += 1; # Automatic resequencing my @tstyles = ( 'List', 'Regex', 'All' ); push @tstyles, ( 'Base' ) unless ( defined(param('dGrouped')) ); my $tmpl = param("${item}_dname"); if ( defined($tmpl) && $tmpl =~ /^t/ ) { $tmpl =~ s/^t//; &TMPLFind($TemplatesById{$tmpl}{'Filter'}, $TemplatesById{$tmpl}{'Display'}); $tmpl = br . 'Selection: ' . popup_menu(-name=>"${item}_tlist", -class=>'small', -values=>[sort { $TMPL{$a} cmp $TMPL{$b} } keys(%TMPL)], -labels=>\%TMPL); } else { $tmpl = ''; } my $class = 'small'; if ( !defined(param("${item}_new")) && !defined($pretty{param("${item}_dname")}) ) { param(-name=>"${item}_DELETE", -value=>'Y'); $class = 'smallred'; } print '', td({-align=>'center'}, checkbox(-name=>"${item}_DELETE", -label=>'', -value=>'Y', -onclick=>"DisableToggle(\"${item}\")")), td({-align=>'center'}, textfield(-name=>"${item}_Seq", -class=>'small', -default=>param("${item}_Seq"), -size=>3)), td(popup_menu(-name=>"${item}_dname", -class=>$class, -values=>[sort { $pretty{$a} cmp $pretty{$b} } (keys(%pretty))], -labels=>\%pretty, -default=>param("${item}_dname")) . $tmpl), td({-align=>'center'}, popup_menu(-name=>"${item}_type", -class=>'small', -values=>\@tstyles)); if ( ( defined(param("${item}_dname")) && param("${item}_dname") =~ /^g/ ) || ( defined(param("${item}_type")) && param("${item}_type") eq 'All' ) ) { print td({-align=>'center', -class=>'small'}, 'N/A'); } elsif ( defined(param("${item}_type")) && param("${item}_type") eq 'List' ) { my $tmpl = param("${item}_dname"); $tmpl =~ s/^t//; &TMPLFind($TemplatesById{$tmpl}{'Filter'}, $TemplatesById{$tmpl}{'Display'}); print td({-align=>'center'}, scrolling_list(-name=>"${item}_list", -class=>'small', -values=>[sort { $TMPL{$a} cmp $TMPL{$b} } keys(%TMPL)], -labels=>\%TMPL, -size=>5, -multiple=>'true')); } else { print td({-align=>'center'}, textfield(-name=>"${item}_regex", -class=>'small', -default=>param("${item}_regex"))); } if ( defined(param('dGrouped')) || ( defined(param("${item}_dname")) && param("${item}_dname") =~ /^t/ && param("${item}_type") eq 'Base' ) ) { print td({-align=>'center'}, textfield(-name=>"${item}_row", -class=>'small', -default=>param("${item}_row"))); } else { print td({-align=>'center', -class=>'small'}, 'N/A'); } } print end_table, p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Update')); } # # Log related functions # sub ShowLog { print h2('ChangeLog'); if ( !open(LOG, "< ${saved_dir}/log") ) { print "No log available: $!"; } else { my ( %titles, @entry ); my $page = ( defined(param('Page')) ) ? param('Page') : 0; print '

'; print a({-href=>&MakeURL('Browse', 'Log', 'Page', $page - 1)}, 'Next') if ( $page > 0 ); while () { chomp; my ( $ts, $id, $who, $title ) = split(/\|/); unshift @entry, ( $ts, $id, $who, $title, $titles{$id} ); $titles{$id} = $title; } close LOG; print ' '; print a({-href=>&MakeURL('Browse', 'Log', 'Page', $page + 1)}, 'Previous') if ( scalar(@entry) > 250 ); print '

', start_table({-border=>1, -align=>'center'}), Tr(th(['Date', 'Item', 'Title', 'Updated By', 'Old Title'])); $page *= 50; while ( scalar(@entry) > 0 ) { my ( $ts, $id, $who, $title, $old ) = ( shift @entry, shift @entry, shift @entry, shift @entry, shift @entry ); next unless ( $page-- <= 0 ); my $url = &MakeURL('Mode', 'view', ( $id =~ /^g/ ) ? 'Graph' : ( $id =~ /^t/ ) ? 'Template' : 'Dashboard', substr($id, 1)); print Tr(td(localtime($ts) .''), td((-e "$saved_dir/RCS/$id,v") ? a({-href=>MakeURL('Browse', 'Rcs', 'Id', $id)}, img({-src=>$icon_text, -border=>0})) : '', (-e "$saved_dir/$id") ? a({-href=>$url}, $id) : $id), td((-e "$saved_dir/$id") ? a({-href=>$url}, $title):$title), td(( $who ne '' ) ? $who : ' '), td(( defined($old) && $title ne $old ) ? $old : ' ')); last if ( $page == -50 ); } print end_table, p; } print $drrawhome, $footer, end_html; } sub ShowRcsLog { print h2('RCS Log'); if ( ! $use_rcs ) { &Error("Rcs module is missing."); } elsif ( !defined(param('Id')) || param('Id') !~ /^([tgd]\d+\.\d+)$/ ) { &Error("Invalid Request"); } else { my $rcs = new Rcs; $rcs->file($1); $rcs->workdir($saved_dir); $rcs->rcsdir($saved_dir .'/RCS'); my @rlog = $rcs->rlog; my $show = 0; print start_ul; while ( scalar(@rlog) > 0 ) { $_ = shift @rlog; chomp; $show = 1 if ( /^revision / ); next unless ( $show ); next if ( /^[-=]+$/ ); if ( /^revision (\S+)$/ ) { print '
  • revision '. b($1); } elsif ( /^date: ([^;]+); +author: ([^;]+);/ ) { print ' on '. b($1) .' by '. b($2) .'
    '; } else { print; } } print end_ul; } print $drrawhome, $footer, end_html; } # # Utility functions # sub MakeURL { croak 'MakeURL(name1, value1, ...)' unless ( scalar(@_) % 2 == 0 ); my $query = new CGI; $query->delete_all; while ( scalar(@_) > 0 ) { if ( defined($_[1]) ) { $query->param(shift @_, shift @_); } else { shift @_; shift @_; } } return $query->url(-path_info=>1, -query=>1, -relative=>1); } # Courtesy of Cliff Miller sub Sort_Colors_Init { my $color; foreach $color ( keys(%colors) ) { my $x = hex(substr($colors{$color}, 1, 2)) / 256 - 0.5; my $y = hex(substr($colors{$color}, 3, 2)) / 256 - 0.5; my $z = hex(substr($colors{$color}, 5, 2)) / 256 - 0.5; if ( $x == $y && $y == $z ) { $colorsidx{$color} = 300 - hex(substr($colors{$color}, 1, 2)); } else { my $nx = sqrt(2)/2 * ( $x - $y ); my $ny = sqrt(2)/2 * sqrt(3)/3 * ( $x + $y ) - sqrt(6)/3 * $z; $colorsidx{$color} = atan2($ny, $nx); } } } sub Sort_Colors { return $colorsidx{$a} <=> $colorsidx{$b}; } sub DSNew { croak 'DSNew(file, DSname, CFname)' if ( scalar(@_) != 3); my ( $rrdfile, $DSname, $CFname ) = ( @_ ); my $len; my $name = 'a'; while ( defined($DS{$name}) ) { if ( $name =~ /(z+)$/ ) { my $zzz = $1; $zzz =~ s/z/a/g; substr($name, length($name)-length($zzz)) = $zzz; if ( length($zzz) < length($name) ) { my $chr = substr($name, length($name)-length($zzz)-1, 1); substr($name, length($name)-length($zzz)-1, 1) = chr(ord($chr) + 1); } else { $name .= 'a'; } } else { my $chr = substr($name, length($name)-1, 1); substr($name, length($name)-1) = chr(ord($chr) + 1); } } param("${name}_File", $rrdfile); if ( $rrdfile eq '' ) { $DS{$name} = ''; } elsif ( $rrdfile =~ /^(.+)\/\/(.+).(rrd|evt)$/ ) { $DS{$name} = $datadirs{$1} . " " . $2; } else { die "Bad filename index in DSNew: $rrdfile\n"; } param("${name}_DS", $DSname); param("${name}_RRA", $CFname); param("${name}_Seq", scalar(keys(%DS))+1); param("${name}_Label", "$DSname $CFname"); param("${name}_CDEF", ''); param("${name}_Type", 'LINE1'); param("${name}_Color", (sort Sort_Colors keys(%colors)) [(param("${name}_Seq") -1) % scalar(keys(%colors))]); param("${name}_XTRAS", ()); param("${name}_BR", 0); return $name; } sub ExpandMatches { croak 'ExpandMatches($string, @array)' if ( scalar(@_) != 2 ); my ( $str, $arr ) = ( @_ ); my $i = 0; while ( $i < scalar(@$arr) ) { $i += 1; $str =~ s/\$$i(\D|$)/$$arr[$i-1]$1/g; } return $str; } sub ReduceMatches { croak 'ReduceMatches($string, @array)' if ( scalar(@_) != 2 ); my ( $str, $arr ) = ( @_ ); my $i = 0; while ( $i < scalar(@$arr) ) { $i += 1; $str =~ s/$$arr[$i-1]/\$$i/g; } return $str; } sub time2str { croak 'time2str([time])' if ( scalar(@_) > 1 ); my $time = shift; $time = time unless ( defined($time) ); my @weekday = ( 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'); my @month = ( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ); my ( $sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($time); return sprintf("%s, %02d %s %0d %02d:%02d:%02d GMT", $weekday[$wday], $mday, $month[$mon], $year + 1900, $hour, $min, $sec); } sub Error { croak 'Error(text)' if ( scalar(@_) != 1 ); if ( defined(param('Mode')) && param('Mode') eq 'show' ) { carp 'Error while producing image: Mode=show, ' . ( ( defined(param('Graph')) ) ? 'Graph=' . param('Graph') : 'Template=' . param('Template') ) . ': ' . $_[0]; } else { print p({-class=>'error', -align=>'center'}, $_[0]); } } # # Help # sub HELP { my @toc = ( [ 'overview', 'Overview', &help_overview ], [ 'terms', 'Terminology', &help_terms ], [ 'gfiles', 'Graphs', &help_graphfiles ], [ 'dsconfig', 'DS Configuration', &help_dsconfig ], [ 'templates', 'Graph Templates', &help_templates ], [ 'dbintro', 'Dashboard Overview', &help_dbintro ], [ 'dbconfig', 'Dashboard Configuration', &help_dbconfig ], [ 'links', 'drraw URLs', &help_urls ], [ 'contact', 'Contact information', &help_contact ], [ 'version', 'Version information', &help_version ] ); print h2('Table Of Contents'), start_ul; my $i = 0; while ($i < scalar(@toc) ) { print li(a({-href=>url(-path_info=>1, -query=>1, -relative=>1) .'#'. $toc[$i][0]}, $toc[$i][1])); $i += 1; } print end_ul, hr; $i = 0; while ($i < scalar(@toc) ) { print a({-name=>$toc[$i][0]}, h2($toc[$i][1])), $toc[$i][2]; $i += 1; } $footer, print end_html; } sub help_overview { return p(em('drraw'), 'is a simple web based presentation front-end for ', a({-href=>'http://www.rrdtool.org/'}, 'RRDtool'), ' that allows you to interactively build graphs of your own design. The following pages provide help on how to use ', em('drraw'), ' but no help on how to use ', a({-href=>'http://www.rrdtool.org/'}, 'RRDtool'), '. Indeed, such knowledge is a basic requirement to using ', em('drraw'), ' and making the most of out it and the following documentation. It is however interesting to note that it takes very little knowledge to be able to "play" with ', em('drraw'), ' as it provides a convenient interface to experiment and learn how to create graphs based on RRD files.'); } sub help_terms { return p('The following terms are used throughout this document and ', em('drraw'), '\'s edition pages. Understanding what they mean is critical to understanding how to use ', em('drraw'), ul( li(b('RE'), ': Short for ', a({-href=>'http://www.perldoc.com/perl5.6/pod/perlre.html'}, '(perl) Regular Expression')), li(b('Database / DB'), ': RRD database file'), li(b('Event File'), ': Event file'), li(b('CDEF'), ': Virtual Data Source (based on a mathematical expression)'), li(b('DS'), ': Data Source'), li(b('Graph'), ': Graphic representation of Data Sources from DB and event files'), li(b('Template'), ': Graph definition with variable Data Sources'), li(b('Dashboard'), ': Collection of graph previews'), )); } sub help_graphfiles { return p('The first step in creating a graph is to choose which data to use. This is done by selecting Data Sources from available databases and event files.', b('Only one type of Data Source (either database or event file) may be added at the same time.')); } sub help_dsconfig { return p('Data Source configuration is contained into a single table which allows configuring graph elements from these Data Sources. Each line typically allows configuring a single Data Source and/or CDEF:', ul(li(b('DEL?'), ': Check to remove this line'), li(b('Name'), ': Data Source virtual name. This is the name that you can reference in CDEFs.'), li(b('Seq'), ': Sequence'), li(b('Data Source'), ': Configuration depends on the type of Data Source being configured:', ul(li(b('Regular Data Source'), ': this is the RRD File and Data Source defined'), li('"pure" ', b('CDEF'), ': this is blank'), li(b('Event Data Sources'), ': this is the Event File. These files contain one event per line, prefixed by the event timestamp (the number of non-leap seconds since whatever time the system considers to be the epoch) followed by a space. These events are automatically added to the graph as VRULE elements.'), li(b('Data Source Template'), ': this defines how Data Sources will be automatically generated by ', em('drraw'), ':', br, 'For each RRD File matching the "File RE", a Data Source will be created for the specified Data Source name. The second row allows defining how each created Data Source will be displayed as if it was a Regular Data Sources. The "Label" entry may contain references ($1, $2, ..) to subexpressions defined by the "File RE". All these Data Sources may be combined into one Data Source by using the "Element" and "Formula" fields. The former is applied to each Data Source individually, and the latter used to combine them together. For example, to get the sum of all Data Sources, use "+" as "Formula". In addition, if you want to make sure you get a real value for the combined Data Source and not NaN whenever one of the element values is NaN, you may use "UN,0,$,IF" in the "Element" field.')), 'Checking the "Lists" option allows changing the Data Source file for Regular and Event Data Sources'), li(b('RRA'), ': RRA selection for Data Sources'), li(b('CDEF'), ': If defined, this will be used to create a CDEF that will be used in place of the Data Source. The CDEF virtual name is the Data Source virtual name in uppercase (see below). The character "$" will automatically be replaced by the Data Source virtual name.'), li(b('Type'), ': Graph element type'), li(b('Color'), ': Graph element color (if applicable)'), li(b('Label / Format'), ': Legend label, or format. For convenience, the first label of each line (in the legend area) is padded with spaces so that all such labels are of the same length. No padding is done if the label contains a % or \\ character.'), li(b('Additional GPRINTs'), ': For each defined RRA, additional GPRINTs may be automatically generated for the Data Source (or CDEF is defined) and added to the legend.'), li(b('BR'), ': Whether or not to add a line break (in the legend area) after this element'))); } sub help_templates { return p('A template is simply a graph definition for which the database files are variable. In other words, a template is defined by selecting database files: ', ul(li(b('Base Regular Expression'), ': This (perl) regular expression will be applied to the list of database files. This expression must have at least one subexpression. (see below)'), li(b('Selection Expression'), ': If defined, this expression should contain references ($1, $2, ..) to subexpressions defined in the base regular expression. The strings resulting from expanding these will be displayed to users as a list for choosing which graph to view.'), li(b('Database File Names'), ': Each database file used by the graph definition must have a name that can be derived from the result of the base regular expression. As for the selection expression, this should be done by using references ($1, $2, ..) to subexpressions defined by the base regular expression.')), 'Note that ', em('drraw'), ' will make a simple attempt to generate the database filename strings automatically, so you should click "Update" after defining the base expression.', b('Any database filename string for which the result does not match the graph definition filename will appear in red')) . p('For example, if you have a graph using the "host1/Interfaces/int1.rrd", you may want to set the "Base Regular Expression" to be: "(.+)/Interfaces/(.+)\.rrd". The "Database File Name" will be replaced by "$1/Interfaces/$2.rrd". Unless you define a "Selection Expression", one of the choices you will be given as selection for the template is "host1 - int1".'); } sub help_dbintro { return p('A dashboard is a collection of graph previews. Defining a dashboard is a simple matter of selecting which graphs and templates will appear on it. There are two basic types of boards:', ul(li(b('Standard'), ': These have a set (by configuration) number of columns, and may mix graphs with templates.'), li(b('Grouped'), ': This special type may only be used with templates. The number of columns is determined by the number of items defined by the configuration. Rows are made up by the targets for which the templates are previewed.'))); } sub help_dbconfig { return p('Dashboard configuration is done by choosing the previews that will appear on the dashboard. Each line configures a graph or template preview. For templates, there are several ways to configure which available selections are actually displayed in the dashboard:', ul(li(b('List'), ': The list of targets is manually configured/selected.'), li(b('Regex'), ': The list of targets is defined by a (perl) regular expression'), li(b('All'), ': Display all available targets'), li(b('Base'), ': One target only, user selected'))) . p('With ', b('"Standard" dashboards'), ' it is possible to define a "', b('dashboard template'), '" based on existing (graph) templates by using the "Base" display style (see above). A (perl) regular expression may be entered in the "Template Base" column to (optionally) restrict the choices available for the already defined template. The "Selection Regular Expression" column should be a (perl) regular expression to be applied to the (graph) template selections, it must cause subpatterns to be defined. These will be presented to the user for selection.') . p('With ', b('"Grouped" dashboards'), ', the "Row Name" column defines in which row targets will be displayed. By default (if undefined), targets are placed in a row of the same name. If defined, the "Row Name" must be a (perl) regular expression that causes subpatterns to be defined which will be used as row name. These names are never shown to the user.') . p('The following two notes are important to keep in mind when configuring template previews:', ul(li('The "Selection" drop-down list shown for each chosen template is not actually used by ', em('drraw'), 'but is presented to you as a help to write regular expressions.'), li('Unlike template configuration regular expressions, all regular expressions defined here will be applied to the values listed in the "Selection" drop-down list shown for the template.')), 'Finally, it is important to understand that the "Graph / Template" selections are stored using an internal ID rather than the actual title. If the selected graph or template is later deleted, its name will be lost and the next time the dashboard is edited, this row will be automatically marked for deletion and displayed in red.'); } sub help_urls { return h4('drraw URLs') . p('The URLs used by ', em('drraw'), ' to view defined graphs, templates and dashboards are fairly short, and as such, suitable for use in e-mail or other web pages. The internal ID used to reference a graph, template or dashboard is generated upon creation (or cloning) and will remain the same for a given item. It is interesting to note that the following options may be applied to actual graph (e.g. image) URLs to override settings saved:', ul(li('Start'), li('End'), li('Width'), li('Height'), li('NoLegend'))); } sub help_contact { return h4('Mailing list') . p('If you want to be informed about important news about ', em('drraw'), ', such as new releases, subscribe to the drraw-announce mailing list, either by sending a mail including the "subscribe" keyword to ', a({-href=>'mailto:drraw-announce-request@taranis.org?body=subscribe'}, 'drraw-announce-request@taranis.org'), 'or simply visit the following web page: ', a({-href=>'http://web.taranis.org/mailman/listinfo/drraw-announce'}, 'http://web.taranis.org/mailman/listinfo/drraw-announce'), '.') . h4('Bugs') . p('Send bug reports to ', a({-href=>"mailto:drraw-bugs\@taranis.org?subject=drraw bug report&body=drraw version: $VERSION, Perl $], CGI $CGI::VERSION, RRD $RRDs::VERSION on $^O with ". $ENV{'SERVER_SOFTWARE'}}, 'drraw-bugs@taranis.org'), '. A bug report is an adequate description of the environment (versions of this package, the OS, Perl, the CGI module and RRDtool) and of the problem: your input, what you expected, what you got, and why you believe it to be wrong. Diffs are welcome, but they only describe a solution, from which the problem might be difficult to infer.') . h4('Others') . p('Your feedback will help to make a better and more portable package. Consider documentation errors as bugs, and report them as such. If you develop anything pertaining to ', em('drraw'), ' or have suggestions, share them by writing to ', a({-href=>'mailto:drraw@taranis.org'}, 'drraw@taranis.org'), '.'); } sub help_version { return table({-align=>'center', -cellpadding=>'5%', -border=>2}, Tr(td({-align=>'right', -width=>'50%'}, b('drraw.cgi')), td({-align=>'left', -width=>'50%'}, $VERSION .' <'. $REVISION .'>')), Tr(td({-align=>'right', -width=>'50%'}, b('Perl')), td({-align=>'left', -width=>'50%'}, "$]")), Tr(td({-align=>'right', -width=>'50%'}, b('CGI.pm')), td({-align=>'left', -width=>'50%'}, "$CGI::VERSION")), Tr(td({-align=>'right', -width=>'50%'}, b('RRDs.pm')), td({-align=>'left', -width=>'50%'}, "$RRDs::VERSION"))); } # Local Variables: # mode: perl # indent-tabs-mode: nil # tab-width: 4 # cperl-indent-level: 4 # End: