drraw-2.2b2/0000755000014400001440000000000011152024101012603 5ustar kaltkalt00000000000000drraw-2.2b2/README.EVENTS0000644000014400001440000000161510103047462014503 0ustar kaltkalt00000000000000Round Robin Databases are a system to store and display time-series data (i.e. network bandwidth, machine-room temperature, server load average). Every now and then, something significant happens that may result in a noticeable discontinuity on such graphs. Displaying such events on the graphs may be very useful to provide additional information to the viewer. For this reason, drraw now supports a very simple interface for events. Since these cannot be stored in RRD files, extra files are needed. These files should contain one event per line, from the oldest to the most recent, in the following format: For Example, here is an excerpt of an event file for a NetApp toaster: 1045060191 Version: NetApp Release 6.3.1: Wed Nov 20 13:03:17 PST 2002 1045274622 Version: NetApp Release 6.4: Fri Feb 7 01:16:52 PST 2003 $Id: README.EVENTS,v 1.2 2004-08-01 02:02:26 kalt Exp $drraw-2.2b2/drraw.cgi0000755000014400001440000060177111152023615014436 0ustar kaltkalt00000000000000#! /usr/bin/perl -T # # Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008 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 1570 2009-02-27 17:49:01Z kalt $ # # 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 = (dirname($0) =~ /(.*)/)[0] . "/drraw.conf"; # Untaint # 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 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 $use_pnp4nagios $saved_dir $clean_cache $tmp_dir $ERRLOG %users %Index $IndexMax $icon_new $icon_closed $icon_open $icon_text $icon_help $icon_bug $icon_link %colors $CSS $CSS2 $bgColor ); # 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'; # Default colors %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', 'Invisible' => '' ); # Style Sheet $bgColor = '"#FFD89D"'; $CSS = < '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 ); ############################################################################### ## 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; } if ( scalar(grep(/\/$/, keys(%datadirs))) > 0 ) { print header(-status=>500), start_html('500 Internal error'), h2("Invalid \%datadirs in $config"), p("Remove any trailing / character in directory paths."), end_html; exit 1; } my $VERSION = '2.2b2'; my $REVISION = '$LastChangedRevision: 1570 $'; $REVISION =~ s/[^0-9]+//g; my $RELEASE = '1570'; $VERSION = 'SVN-SNAPSHOT ['. $REVISION .' post '. $VERSION .']' unless ( $REVISION eq $RELEASE ); my $DEBUG = 0; my %colorsidx = (); # Additions require changes to &DRAW() my @graphtypes = ( 'LINE1', 'LINE2', 'LINE3', 'LINE?', 'AREA', 'STACK', 'TICK', 'GPRINT', 'COMMENT', 'HRULE', 'VRULE', 'SHIFT', '-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 = div({-class=>'tag'}, '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 ShowNew(srcElement, target) { var targetElement = document.getElementById("newrow" + target) if (!targetElement) return targetElement.style.display = ""; srcElement.innerHTML = "Use the following row to " + srcElement.innerHTML.split("to", 2)[1] } 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 } anElement = document.getElementById(target + "_Type") if (anElement) { TypeCB(anElement.value, target) } } function ToggleDB(target) { var DElement = document.forms.Editor.elements.namedItem(target +"_dname") if (!DElement) return var status = 0 if (DElement.value.substring(0, 1) != "t") status = 1 var anElement var i = 0 while (anElement = document.forms.Editor.elements.item(i++)) { if (anElement.name.split("_", 1)[0] == target && anElement.name != DElement && anElement.name.split("_", 2)[1] != "DELETE" && anElement.name.split("_", 2)[1] != "Seq" && anElement.name.split("_", 2)[1] != "dname" ) { anElement.disabled = status } } } function TypeCB(value, target) { var targetElement = document.getElementById(target + "_Colorize"); if (targetElement) { var ColorList = document.getElementById(target + "_Color"); if (value == "PRINT" || value == "GPRINT" || value == "SHIFT" || value == "COMMENT" || value == "-Nothing-") { targetElement.bgColor = $bgColor if (ColorList) { ColorList.value = $bgColor ColorList.disabled = 1 } } else if (ColorList) ColorList.disabled = 0 } targetElement = document.getElementById(target + "_STACK"); if (targetElement) { if (value.substring(0, 4) == "LINE" || value == "AREA") { targetElement.disabled = 0 } else { targetElement.disabled = 1 } } targetElement = document.getElementById(target + "_Width"); if (targetElement) { if (value == "LINE?" || value == "TICK") { if (targetElement.disabled == 1) { if (value == "TICK") { targetElement.value = "0.1" } else { targetElement.value = "1" } } targetElement.disabled = 0 } else { targetElement.value = "N/A" targetElement.disabled = 1 } } return } 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=>"\n\n"}, -title=>'drraw - '. $title), $header, h1({-class=>'title'}, 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 we should make an effort to use pnp4nagios XML data if ( $use_pnp4nagios ) { eval { require XML::Simple; import XML::Simple; }; if ( $@ ) { warn "Unable to load XML::Simple: $@\n"; $use_pnp4nagios = 0; } } else { $use_pnp4nagios = 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({-class=>'title'}, a({-href=>&MakeURL}, $title)); # 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 print a({-target=>'drrawhelp', href=>MakeURL('Browse', 'Help') . '#contact'}, img({-src=>$icon_bug, -border=>0}), ' Report a bug, ...'), br; } 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', -multiple=>'true', -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("". strftime("%Y-%m-%d %H:%M:%S", 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 %Y-%m-%d %H:%M", localtime($startts)) .' - '. strftime("%a %Y-%m-%d %H:%M", 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))}, &GraphHTML(param("${item}_dname"), undef, param('Start'), param('End'), param('dWidth'), param('dHeight'), ( param('dNoLegend') eq 'On' ) ? 1 : undef))); } 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 ( sort 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 ( sort { $TMPL{$a} cmp $TMPL{$b} } 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])}, &GraphHTML(param("${item}_dname"), $list[0], param('Start'), param('End'), param('dWidth'), param('dHeight'), ( param('dNoLegend') eq 'On' ) ? 1 : undef))); 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])}, &GraphHTML('t'. $cols[$col], $Rows{$row}[$col], param('Start'), param('End'), param('dWidth'), param('dHeight'), ( param('dNoLegend') eq 'On' ) ? 1 : undef))); } 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("". strftime("%Y-%m-%d %H:%M:%S", 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'); my $title = ''; $title = ': '. a({-href=>&MakeURL('Mode', 'view', 'Template',param('Template'), 'Base', param('Base'))}, $TMPL{param('Base')}) if ( scalar([param('Base')]) == 1 ); 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'}) . $title)), 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("". strftime("%Y-%m-%d %H:%M:%S", 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; } # Validate trending specification my ( $trendshift, $trendcount ) = ( 0, 1 ); if ( ( defined(param('Shift')) && param('Shift') ne '' ) || ( defined(param('Count')) && param('Count') ne '' ) ) { if ( param('Shift') =~ /^[-+]/ ) { $trendshift = param('Shift'); $trendcount = param('Count'); } else { &Error("Shift must begin with + or -"); } } # Display the view chooser my $view = 0; print '

' if ( defined(param('View')) || $trendshift > 0 || scalar(@{[param('Base')]}) > 1 ); while ( $view < scalar(@dv_def) ) { if ( defined(param('View')) || $trendshift > 0 || scalar(@{[param('Base')]}) > 1 ) { # View Links, single graph will follow if ( defined(param('View')) && $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 { # Graphs Links for the pre-defined views print p({-align=>'center'}, a({-href=>MakeURL('Mode', 'view', ( defined(param('Graph')) ) ? ( 'Graph', param('Graph') ) : ( 'Template', param('Template'), 'Base', param('Base') ), 'View', $view)}, &GraphHTML($id, ( defined(param('Base')) ) ? param('Base') : undef, $dv_def[$view])), 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 (Start/End) entry fields print start_form({-align=>'center', -method=>'GET'}), '', ( defined(param('Graph')) ) ? '' : ''; if ( defined(param('Base')) && scalar(@{[param('Base')]}) > 0 ) { foreach ( param('Base') ) { print ""; } } my $trend = ''; $trend = '
'. 'Shift: '. textfield(-name=>'Shift'). ' Count: '. textfield(-name=>'Count', -size=>2) if ( defined(param('View')) ); print p({-align=>'center'}, 'Start: ', textfield(-name=>'Start'), 'End: ', textfield(-name=>'End'), submit(-name=>'Mode', -value=>'view'), $trend), end_form; # Validate Start/End times my $start = ( defined(param('Start')) && param('Start') ne '' ) ? param('Start') : 'end - 1 day'; my $end = ( defined(param('End')) && param('End') ne '' ) ? param('End') : 'now'; my ( $startts, $endts ); if ( defined(&RRDs::times) ) { ( $startts, $endts ) = RRDs::times($start, $end); if ( defined(RRDs::error) ) { &Error(RRDs::error); } } # Links to graphs if ( defined(param('Base')) && scalar(@{[param('Base')]}) > 1 ) { # Dashboard style display for templates foreach ( param('Base') ) { print p({-align=>'center'}, a({-href=>MakeURL('Mode', 'view', 'Template', param('Template'), 'Base', $_)}, &GraphHTML($id, $_, param('Start'), param('End')))); } } elsif ( defined(param('View')) ) { # Single graph view, or trend page while ( $trendcount-- > 0 ) { print '

', &GraphHTML($id, ( defined(param('Base')) ) ? param('Base') : undef, $start, $end), br; if ( param('View') >= 0 ) { print b($dv_name[param('View')]); } else { if ( defined(&RRDs::times) ) { print b(strftime("%a %Y-%m-%d %H:%M", localtime($startts)) .' - '. strftime("%a %Y-%m-%d %H:%M", localtime($endts))); } else { print b('Custom View'); } } print '

'; # Trend adjustments if ( $trendcount > 0 ) { $start .= " $trendshift" unless ( $start =~ /end/ ); $end .= " $trendshift" unless ( $end =~ /start/ ); if ( defined(&RRDs::times) ) { ( $startts, $endts ) = RRDs::times($start, $end); if ( defined(RRDs::error) ) { &Error(RRDs::error); last; } last if ( $startts > time() ); } } } } } 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, ( defined(param('Format')) ) ? param('Format') : 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]) ); param(-name=>'gFormat', -value=>$graph[7]) if ( defined($graph[7]) ); &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({-class=>'title'}, a({-href=>&MakeURL}, $title)), 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({-class=>'title'}, a({-href=>&MakeURL}, $title)), h1('Board Deleted!'); Indexes_Save('d', $1, ''); } } else { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-class=>'title'}, a({-href=>&MakeURL}, $title)), &Error('Deletion failed.'); } } else { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-class=>'title'}, a({-href=>&MakeURL}, $title)); } # 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({-class=>'title'}, a({-href=>&MakeURL}, $title)), 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({-class=>'title'}, a({-href=>&MakeURL}, $title)), 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({-class=>'title'}, a({-href=>&MakeURL}, $title)), 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({-class=>'title'}, a({-href=>&MakeURL}, $title)), h1('Template Deleted!'); Indexes_Save('t', $1, ''); } } else { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-class=>'title'}, a({-href=>&MakeURL}, $title)), &Error('Deletion failed.'); } } else { print header, start_html(-style=>{-code=>$CSS}, -title=>'drraw - '. $title), $header, h1({-class=>'title'}, a({-href=>&MakeURL}, $title)); } # 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'}, &GraphHTML($gname, undef, param('Start'), param('End'))); } 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+)\/(.*)$/ ) { next; } 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' ) { next unless ( param("${item}_dname") =~ /^t/ ); $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'}); } } 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({-class=>($silver++ % 2 == 0) ? 'header' : ''}, 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({-class=>($silver++ % 2 == 0) ? 'header' : ''}, 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({-class=>($silver++ % 2 == 0) ? 'header' : ''}, 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 ) = ( @_ ); die "Please update CGI.pm to version 3.05 or newer to use Custom Indexes\n" if ( $CGI::VERSION > 2.91 && $CGI::VERSION < 3.05 ); 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; } my $gform = param('gFormat'); param('gFormat', 'PNG') if ( defined($gform) && $gform eq 'GIF' && $RRDs::VERSION >= 1.2 ); param('Code', 'Hide'); } sub Definition_Get { croak 'Definition_Get(file, name)' if ( scalar(@_) != 2); my ( $file, $name ) = ( @_ ); 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; } if (!open(FILE, "< ${file}")) { &Error("Could not open definition file \"$file\" to get $name: $!"); return undef; } my $def = new CGI(\*FILE); close FILE; return $def->param($name); } 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}]"; } $rcsuser =~ /(.+)/; $rcsuser = $1; # Untaint 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 && $log =~ /(.+)/ ) { if ( $rcs->ci('-u', '-m'. $1, $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, no_chdir=>1, follow=>1, untaint=>1, # Untaint, lame... untaint_pattern=>qr|^([-+@\w./:]+)$|}, keys(%datadirs)); alarm($maxtime); Cache_Save; } sub DBFinder { if ( -f $_ && ( /.\.rrd$/ || /.\.evt$/ ) ) { my $start; foreach $start ( keys(%datadirs) ) { if ( $_ =~ /^${start}\/(.+)$/ ) { my $end = $1; if ( $_ =~ /\.rrd$/ ) { push @rrdfiles, $start . '//' . $end; } else { push @evtfiles, $start . '//' . $end; } return; } } warn "DBFinder called for $_ which does not match any of \%datadirs: ". join(", ", keys(%datadirs)) ."\n"; 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 ($last) = RRDs::last($file); my ($start, $step, $DSnames, $data) = RRDs::fetch($file, $cf); my $ds; if ( defined($DSnames) && scalar(@{$DSnames}) > 0 ) { foreach $ds ( @{$DSnames} ) { my $filter = param('db_FILTER'); next if ( $filter ne '' && "$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 { my $param_a = param("${a}_Seq"); my $param_b = param("${b}_Seq"); if (defined($param_a)) { if (defined($param_b)) { return ($param_a <=> $param_b); } else { return -1; } } else { if (defined($param_b)) { return 1; } else { return 0; } } } 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}_Width", -value=>'') unless ( defined(param("${ds}_Width")) ); param(-name=>"${ds}_tWidth", -value=>'') if ( defined(param("${ds}_Formula")) && !defined(param("${ds}_tWidth")) ); } param(-name=>'DS', -value=>[keys(%DS)]); } # DS Config: Lists chosen DS for the user to configure sub DSConfig { my $color; my $i =0; print '

', start_table({-border=>1,-align=>'center'}), caption(em(b('Available Colors'))), ''; foreach $color ( sort Sort_Colors (keys(%colors)) ) { next if ( $colors{$color} eq '' ); print "\n" if (($i % 6) == 0); } print '' . end_table, ''; my @new = ( &DSNew('', '', ''), &DSNew('', '', ''), &DSNew('', '', '') ); param("$new[0]_DELETE", 'Y'); param("$new[1]_DELETE", 'Y'); param("$new[2]_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({-class=>'header'}, th(['DEL'. br .'?', 'Name'. br .'Seq', 'Data Source ('. checkbox(-name=>'DSFList', -title=>'Select to enable drop-down filename lists', -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', -title=>'Click above to toggle all additional GPRINTs and BR checkboxes', -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 ( grep(/^$ds$/, @new) ); # Extra rows for "new" DS entries my $n; if ( $ds eq $new[0] ) { $n = 0; print Tr({-class=>'header'}, td({-colspan=>8, -align=>'center', -id=>'new0', -onclick=>'ShowNew(this, '. $n .')'}, 'Use the following row to define a new CDEF or VDEF.')); } elsif ( $ds eq $new[1] ) { $n = 1; print Tr({-class=>'header'}, td({-colspan=>8, -align=>'center', -id=>'new1', -onclick=>'ShowNew(this, '. $n .')'}, 'Use the following row to add a new RULE to the graph.')); } elsif ( $ds eq $new[2] ) { $n = 2; print Tr({-class=>'header'}, td({-colspan=>8, -align=>'center', -id=>'new2', -onclick=>'ShowNew(this, '. $n .')'}, 'Use the following row to define a DS based on a perl regular expression.')); } # Figure out once and for all what we're dealing with my $type; my $rowcount = 1; if ( defined(param("${ds}_Formula")) || $ds eq $new[2] ) { # DS Template $type = 'tmpl'; $rowcount = 2 unless ( $ds eq $new[2] ); } elsif ( param("${ds}_File") eq '' ) { if ( param("${ds}_Type") !~ /RULE$/ && !&IsNum(param("${ds}_CDEF")) && $ds ne $new[1]) { # Pure CDEF $type = 'def'; } else { # RULE $type = 'rule'; } } elsif ( param("${ds}_File") =~ /\.rrd$/ ) { # Regular DS $type = 'ds'; } elsif ( param("${ds}_File") =~ /\.evt$/ ) { # Event file $type = 'evt'; } else { print Tr(td({-rowspan=>8, -class=>'error', -align=>'center'}, "Skipping invalid DS $ds")); next; } print ""; # Delete box print td({-align=>'center', -rowspan=>$rowcount}, checkbox(-name=>"${ds}_DELETE", -title=>'If checked, this row will be deleted on the next update', -label=>'', -value=>'Y', -onclick=>"DisableToggle(\"${ds}\")")); # Name/Seq print td(b($ds) . ':' . textfield(-name=>"${ds}_Seq", -title=>'Change to reorder the table rows', -class=>'small', -default=>param("${ds}_Seq"), -size=>2)); # Data Source.. ouch if ( $type eq 'ds' ) { print td(( defined(param('DSFList')) && param('DSFList') eq 'Y' )? popup_menu(-name=>"${ds}_File", -class=>'small', -values=>[sort { &datafnsort($rrd{$a}, $rrd{$b}) } keys(%rrd)], -labels=>\%rrd, -default=>$DS{$ds}) : (hidden("${ds}_File") . $DS{$ds}), br, b(param("${ds}_DS")), hidden("${ds}_DS", param("${ds}_DS"))); } elsif ( $type eq 'evt' ) { print 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'); } elsif ( $type eq 'tmpl' ) { print td('File RE:', textfield(-name=>"${ds}_File", -class=>'small', -title=>'All files matching the regular expression will automatically be generated Data Sources on the graph', -default=>param("${ds}_File"), -size=>40), br, 'DS:', textfield(-name=>"${ds}_DS", -class=>'small', -title=>'Name of the Data Source to use', -default=>param("${ds}_DS")), br, 'Element:', textfield(-name=>"${ds}_Element", -class=>'small', -title=>'Use this to define a CDEF for each individual Data Source', -default=>param("${ds}_Element")), 'Formula:', textfield(-name=>"${ds}_Formula", -class=>'small', -title=>'Operation to use to combine all Data Sources into one, for example, for the total, use "+"', -default=>param("${ds}_Formula")), hidden("${ds}_NEW", '')); } elsif ( $type eq 'def' ) { my $v = ''; $v = '/VDEF' if ( $RRDs::VERSION >= 1.2 ); print td({-align=>'center'}, "CDEF${v} Definition", hidden("${ds}_File", '')); } elsif ( $type eq 'rule' ) { print td({-align=>'center'}, 'RULE Definition', hidden("${ds}_File", '')); } else { die "Ugh, we should not be here!"; } # RRA/CDEF my @rra = @rranames; if ( $RRDs::VERSION >= 1.2 && $type eq 'def' ) { if ( param("${ds}_Type") ne 'GPRINT' || param("${ds}_RRA") =~ /^.DEF$/ ) { @rra = (); param("${ds}_RRA", 'CDEF') unless ( param("${ds}_RRA") =~ /^.DEF$/ ); } push @rra, 'VDEF', 'CDEF'; } print td(( $type eq 'rule' ) ? 'Value:' : popup_menu(-name=>"${ds}_RRA", -class=>'small', -values=>[@rra], -labels=>\%rranames, -default=>param("${ds}_RRA")), br, textfield(-name=>"${ds}_CDEF", -class=>'small', -title=>'CDEF/VDEF formula', -default=>param("${ds}_CDEF"), -size=>11)) unless ( $type eq 'evt' ); # Type/Color my @gt; if ( $type eq 'evt' ) { @gt = ( 'VRULE' ); } elsif ( $type eq 'rule' ) { # HRULE deprecated in 1.2 if ( $RRDs::VERSION >= 1.2 ) { if ( param("${ds}_Type") eq 'VRULE' ) { @gt = ( 'VRULE' ); } else { @gt = grep(/(RULE$|^LINE)/, @graphtypes); } } else { @gt = grep(/RULE$/, @graphtypes); } } elsif ( $RRDs::VERSION >= 1.2 ) { # HRULE & STACK deprecated in 1.2 if ( param("${ds}_Type") ne 'STACK' ) { @gt = grep(!/^(HRULE|STACK)$/, @graphtypes); } else { @gt = grep(!/^HRULE$/, @graphtypes); } } else { @gt = grep(!/^(LINE\?|SHIFT|TICK)$/, @graphtypes); } print td({-bgcolor=>$colors{param("${ds}_Color")}, -id=>"${ds}_Colorize"}, popup_menu(-name=>"${ds}_Type", -class=>'small', -id=>"${ds}_Type", -values=>[@gt], -default=>param("${ds}_Type"), -onchange=>"TypeCB(this.value, \"${ds}\")"), ( $RRDs::VERSION >= 1.2 && ( $type eq 'ds' || $type eq 'def' || $type eq 'tmpl' )) ? checkbox(-id=>"${ds}_STACK", -name=>"${ds}_STACK", -title=>'Whether to stack this element on top of the previous one', -label=>'', -value=>'Y') : '', popup_menu(-name=>"${ds}_Color", -class=>'small', -id=>"${ds}_Color", -values=>[sort Sort_Colors keys(%colors)], -default=>param("${ds}_Color"), -onchange=>"SetBgColor(this, \"${ds}_Colorize\")")); # Label/Format my $width = textfield(-name=>"${ds}_Width", -class=>'small', -title=>'Width for TICK (0 to 1) and LINE? commands', -id=>"${ds}_Width", -size=>3, -default=>param("${ds}_Width")); $width = '' unless ( $RRDs::VERSION >= 1.2 ); print td(textfield(-name=>"${ds}_Label", -class=>'small', -title=>'Graph label, format for GPRINT commands, offset for SHIFT commands', -default=>param("${ds}_Label"), -size=>15), $width); # GPRINTs if ( $type ne 'evt' && $type ne 'rule' ) { print td({-align=>'center'}, checkbox_group(-name=>"${ds}_XTRAS", -values=>[@rranames], -labels=>\%rra, -defaults=>param("${ds}_XTRAS"))); } else { print td({-align=>'center'}, 'N/A'); } # BR print td({-align=>'center'}, checkbox(-name=>"${ds}_BR", -label=>'', -value=>'Y', -title=>'Whether or not to add a line break (in the legend area) after this element')); print ''; print "\n\n"; next unless ( $rowcount == 2 ); if ( $RRDs::VERSION >= 1.2 ) { # STACK deprecated in 1.2 @gt = grep(!/^(.RULE|SHIFT|STACK)$/, @graphtypes); } else { @gt = grep(!/^(.RULE|LINE\?|SHIFT|TICK)$/, @graphtypes); } $width = textfield(-name=>"${ds}_tWidth", -class=>'small', -title=>'Width for TICK (0 to 1) and LINE? commands', -id=>"${ds}_tWidth", -size=>3, -default=>param("${ds}_tWidth")) if ( $RRDs::VERSION >= 1.2 ); 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=>[@gt], -default=>param("${ds}_tType")), radio_group(-name=>"${ds}_tSTACK", -label=>'', -values=>[ 'N', 'Y', '+' ], -default=>'N', -title=>'Whether to stack these elements on top of the previous one')), td(textfield(-name=>"${ds}_tLabel", -class=>'small', -size=>15, -default=>param("${ds}_tLabel")), $width), 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'))); } print end_table; print < var t; i = 0; while (i < 3) { t = document.getElementById("new" + i) t.innerHTML = "Click here to " + t.innerHTML.split("to", 2)[1] t.style.cursor = "pointer" t = document.getElementById("newrow" + i) t.style.display = "none" i = i + 1 } DisableToggle("$new[0]") DisableToggle("$new[1]") DisableToggle("$new[2]") END print p({-align=>'center'}, submit(-name=>'USERSAID', -value=>'Update')); } # GOptions: Lists general options for the user to set sub GOptions { my $nogridfit = Tr(td('No Grid Fit'), td(checkbox(-name=>'gNoGridFit', -label=>'', -class=>'normal', -default=>param('gNoGridFit')))); $nogridfit = '' unless ( $RRDs::VERSION >= 1.2 ); 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')))), $nogridfit, Tr(td('Image Format'), td(popup_menu(-name=>'gFormat', -class=>'normal', -values=>[@ImgFormat], -default=>param('gFormat')))), Tr(td('Dates'), td(checkbox(-name=>'gDateStart', -label=>'Start', -class=>'normal', -default=>param('gDateStart')), checkbox(-name=>'gDateEnd', -label=>'End', -class=>'normal', -default=>param('gDateEnd')), checkbox(-name=>'gDateNow', -label=>'Now', -class=>'normal', -default=>param('gDateNow')))), 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, $width, $stack, $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$/ ) { my @rra = ( $rra ); @rra = () if ( $rra =~ /^.DEF$/ ); push @ELEM, join(':', $type, (( $cdef eq '' ) ? $ds : uc($ds)), @rra, $label . (( $gp == -1 ) ? $br : '')); } elsif ( $type =~ /RULE$/ ) { # HRULE is deprecated in 1.2 $type = 'LINE1' if ( $type eq 'HRULE' && $RRDs::VERSION >= 1.2 ); push @ELEM, join(':', $type, $cdef . $colors{$color}, $label . (( $gp == -1 ) ? $br : '')); } else { if ( &IsNum($cdef) ) { # It's a value, not a valid CDEF $ds = $cdef; $cdef = ''; } $type = 'LINE'. $width if ( $type eq 'LINE?' ); my @stack = (); @stack = ( 'STACK' ) if ( $stack ); push @ELEM, join(':', $type, (( $cdef eq '' ) ? $ds : uc($ds)) . $colors{$color}, (( $type eq 'TICK' ) ? $width .':' : '' ) . $label . (( $gp == -1 ) ? $br : ''), @stack); } return @ELEM if ( &IsNum($ds) ); my $i = 0; while ( $i <= $gp ) { my $vname = ( $cdef eq '' ) ? $ds : uc($ds); my @rra = ( $xtras[$i] ); if ( $RRDs::VERSION >= 1.2 ) { push @ELEM, 'VDEF:'. $vname .'_'. $xtras[$i] .'='. $vname .','. $xtras[$i] . ( ( length($xtras[$i]) == 3 ) ? 'IMUM' : '' ); $vname .= '_'. $xtras[$i]; @rra = (); } push @ELEM, join(':', 'GPRINT', $vname, @rra, $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 ( $start, $end ); $start = ( defined(param('Start')) && param('Start') ne '' ) ? param('Start') : 'end - 1 day'; $end = ( defined(param('End')) && param('End') ne '' ) ? param('End') : 'now'; my ( $startts, $endts ) = ( 0, time ); if ( defined(&RRDs::times) ) { ( $startts, $endts ) = RRDs::times($start, $end); 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 ( @dscdefs, @dselements ); foreach $file ( sort(@rrdfiles) ) { next unless ( $file =~ /\.rrd$/ ); if ( $file =~ /$re/ ) { # Don't consider stale files which have nothing # interesting to contribute to the graph. next unless ( (stat($file))[9] >= $startts ); next unless ( RRDs::last($file) >= $startts ); $file =~ s/:/\\:/g; # Check for real values my @dsxz = ( join(':', 'DEF', 'xz='. $file, param("${ds}_DS"), param("${ds}_RRA")), join(':', 'PRINT', 'xz', param("${ds}_RRA"), '%lf') ); my ($graphret, $xs, $ys) = RRDs::graph(( $Config{'osname'} eq 'MSWin32' ) ? 'NUL:' : '/dev/null', "--start", $startts, "--end", $endts, @dsxz); next if ( ${$graphret}[0] eq "nan" ); # Looks good, use it push @DEF, join(':', 'DEF', $ds . $count .'='. $file, param("${ds}_DS"), param("${ds}_RRA")); my $elmt = $ds . $count; $elmt .= ','. param("${ds}_Element") if ( param("${ds}_Element") ne '' ); $elmt =~ s/\$/$ds$count/g; push @dselements, $elmt; if ( param("${ds}_tCDEF") ne '' ) { my $cdef = param("${ds}_tCDEF"); $cdef =~ s/\$/$ds$count/g; push @dscdefs, 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, $stack ) = ( param("${ds}_tType"), 0 ); if ( defined(param("${ds}_tSTACK")) ) { if ( ( $count > 0 && param("${ds}_tSTACK") eq 'Y' ) || param("${ds}_tSTACK") eq '+' ) { if ( $RRDs::VERSION >= 1.2 ) { $stack = 1; } else { $type = 'STACK'; } } } push @ELEM, &DRAW_Element($ds . $count, param("${ds}_RRA") .'', param("${ds}_tCDEF"), $type, param("${ds}_tWidth"), $stack, ( 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; } } foreach ( @dscdefs ) { $_ =~ s/\#/$count/g; push @CDEF, $_; } if ( param("${ds}_Formula") ne '' ) { my $dsdef = ''; my $i; my $first = shift @dselements; if ( param("${ds}_Formula") eq 'AVERAGE' ) { $dsdef = "$first,". join(",+,", @dselements); $dsdef .= ",+,$count,/"; } elsif ( param("${ds}_Formula") eq 'STDDEV' ) { my ( $sum, $sumsq ); $sum = "$first,". join(",+,", @dselements) .",+"; $sumsq = "$first,DUP,*,"; $sumsq .= join(",DUP,*,+,", @dselements) .",DUP,*,+"; $dsdef = "$sumsq,$count,/"; $dsdef .= ",$sum,$count,/,DUP,*"; $dsdef .= ",-"; $dsdef .= ",SQRT"; } else { $dsdef = "$first,"; $dsdef .= join(",". param("${ds}_Formula") .",", @dselements); $dsdef .= ",". param("${ds}_Formula") } $dsdef =~ s/\#/$count/g; push @CDEF, join(':', 'CDEF', $ds .'='. $dsdef); } } 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$/ && !&IsNum(param("${ds}_CDEF")) ) { my $cdef = param("${ds}_CDEF"); $cdef =~ s/\$/$ds/g; push @CDEF, join(':', ( param("${ds}_RRA") eq 'VDEF' ) ? 'VDEF' : 'CDEF', uc($ds) .'='. $cdef); } if ( param("${ds}_Type") eq 'SHIFT' ) { if ( param("${ds}_CDEF") eq '' && param("${ds}_Formula") eq '' ) { push @DEF, join(':', 'SHIFT', $ds, param("${ds}_Label")); } else { push @CDEF, join(':', 'SHIFT', ( param("${ds}_Formula") eq '' ) ? uc($ds) : $ds, param("${ds}_Label")); } next; } $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}_Width"), defined(param("${ds}_STACK")), 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 = time; if ( $now - $ts > 30 * 86400 ) { # More than a month ago.. $format = "%Y-%m-%d %H:%M"; } elsif ( $now - $ts > 7 * 86400 ) { # More than a week ago.. $format = "%a %e %H:%M"; } else { $format = "%a %H:%M"; } } $msg = '['. strftime($format, localtime($ts)) .'] '. $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('gDateStart')) && defined(param('gDateEnd')) ) { push @ELEM, "COMMENT:". strftime("%a %Y-%m-%d %H\\:%M", localtime($startts)) . " - ". strftime("%a %Y-%m-%d %H\\:%M", localtime($endts)) ."\\c"; } elsif ( defined(param('gDateStart')) ) { push @ELEM, "COMMENT:". strftime("%a %Y-%m-%d %H\\:%M", localtime($startts)). "\\n"; } elsif ( defined(param('gDateEnd')) ) { push @ELEM, "COMMENT:". strftime("%a %Y-%m-%d %H\\:%M", localtime($endts)) ."\\r"; } if ( defined(param('gDateNow')) ) { push @ELEM, "COMMENT:Created on". strftime("%a %Y-%m-%d %H\\:%M", localtime($endts)) ."\\r"; } push @Options, '--start='. $start; push @Options, '--end='. $end; 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, '--no-gridfit' if ( defined(param('gNoGridFit')) && $RRDs::VERSION >= 1.2 ); 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')) && $end eq 'now' ) { $view = 0; while ( $view < scalar(@dv_name) ) { last if ( $dv_def[$view] eq $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; } # GraphHTML: Returns HTML to include a graph in a page sub GraphHTML { croak 'GraphHTML(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) ); # Image or Other? my $type = Definition_Get($name, 'gFormat'); my $url = $query->url(-path_info=>1, -query=>1, -relative=>1); if ( !defined($type) || $type =~ /^(PNG|GIF)/ ) { return img({-src=>$url,-align=>'center', -border=>0, -onerror=>'this.onerror=null; this.src="/icons/unknown.gif"'}); } elsif ( $type eq 'SVG' ) { return "Your browser does not support embedded $type files."; } else { # Could we do better? Do we care to? return a({-href=>$url}, "$type Document: '". Definition_Get($name, 'gTitle') ."'"); } } 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"), -onchange=>"ToggleDB(\"${item}\")") . $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(strftime("%Y-%m-%d %H:%M", 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 { confess 'MakeURL(name1, value1, ...)' unless ( scalar(@_) % 2 == 0 ); my $query = new CGI; $query->delete_all; while ( scalar(@_) > 0 ) { if ( defined($_[1]) ) { if ( ref($_[1]) eq 'ARRAY' ) { $query->param(-name=>shift @_, -value=>shift @_); } else { $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) ) { if ( $colors{$color} eq '' ) { $colorsidx{$color} = 301; next; } 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); if ( $use_pnp4nagios ) { my ( $xml_file, $xml, $data_xml, $ds_xml ); $xml = new XML::Simple; $xml_file = $rrdfile; if ( $xml_file =~ s/rrd$/xml/g ) { $data_xml = $xml->XMLin($xml_file); foreach $ds_xml ( @{$data_xml->{DATASOURCE}} ) { if ( $DSname == $ds_xml->{DS} ) { $DSname = $ds_xml->{NAME}; } } } } param("${name}_Label", "$DSname $CFname"); param("${name}_Width", ''); param("${name}_CDEF", ''); param("${name}_Type", 'LINE1'); param("${name}_STACK", 0); param("${name}_Color", (sort Sort_Colors keys(%colors)) [(((param("${name}_Seq") -1) * 6) % scalar(keys(%colors))) + ((param("${name}_Seq") -1) / (scalar(keys(%colors)) / 6)) + 1]); 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 IsNum { croak 'IsNum($str)' if ( scalar(@_) != 1 ); return ( $_[0] =~ /^-?\d+\.?\d*$/ ); } 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. The basic format is for each row to allow configuring a single Data Source and/or CDEF, although various elements are displayed slightly differently. It is important to understand and realize that ', em('drraw'), ' does not fully check what you specify to be valid. It does, however, try to steer you away from simple mistakes, but ultimately, only ', a({-href=>'http://www.rrdtool.org/'}, 'RRDtool'), ' fully checks the syntax of your settings. The table columns are as follows:', 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(b('HRULE'), ', ', b('VRULE'), ' graph elements as well as "pure" ', b('CDEF'), ' or ', b('VDEF'), ': 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 ', b('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.', ul(li('The second row allows defining how each created Data Source will be displayed as if it was a Regular Data Sources.'), li('The "Label" entry may contain references ($1, $2, ..) to subexpressions defined by the "File RE".'), li('All these Data Sources may be combined into a composite 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.'), li('You may also use "AVERAGE" or "STDDEV" in the "Formula" field to have ', em('drraw'), ' calculate the average or the standard deviation (respectively).'), li('The character "#" will be replaced by the count of Data Sources defined by the "File RE".')))), 'Checking the "Lists" option allows changing the Data Source file for Regular and Event Data Sources'), li(b('RRA'), ': RRA selection for Data Sources, or ', b('CDEF'), '/', b('VDEF'), ' choice'), li(b('CDEF'), ': If defined, this will be used to create a ', b('CDEF'), ' that will be used in place of the Data Source. The ', b('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. This is also used as the formula for "pure"', b('CDEF'), ' and ', b('VDEF'), ' definitions, as well as the value for ', b('HRULE'), ' and ', b('VRULE'), ' graph elements.'), 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('Width'), ': Width for ', b('TICK'), ' and ', b('LINE?'), ' graph elements. (', a({-href=>'http://www.rrdtool.org/'}, 'RRDtool'), ' 1.2 and above only)'), li(b('Additional GPRINTs'), ': For each defined RRA, additional GPRINTs may be automatically generated for the Data Source (or CDEF if it 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'))). p(b('Note'), ': If you are using ', a({-href=>'http://www.rrdtool.org/'}, 'RRDtool'), ' 1.2 or above, ', em('drraw'), ' automatically enables new features available with it, and silently disables deprecated features. However, deprecated features used in previously saved graphs are ', b('not'), ' automatically updated if you edit the graph and save it again. When possible, deprecated features in use in previously saved graphs are nonetheless converted when running ', a({-href=>'http://www.rrdtool.org/'}, 'RRDtool')); } 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'), li('Format'))); } sub help_contact { return h4('Mailing lists') . 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'), '.') . p('For other discussions or if you have questions about ', em('drraw'), ', subscribe to the drraw-users mailing list, either by sending a mail including the "subscribe" keyword to ', a({-href=>'mailto:drraw-users-request@taranis.org?body=subscribe'}, 'drraw-users-request@taranis.org'), 'or simply visit the following web page: ', a({-href=>'http://web.taranis.org/mailman/listinfo/drraw-users'}, 'http://web.taranis.org/mailman/listinfo/drraw-users'), '. You must be a subscriber to post on this list.') . h4('Wiki') . p('Additional documentation can be found on ', a({-href=>'http://web.taranis.org/drraw/wiki'}, em('drraw'), ' Wiki'), '.') . 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: drraw-2.2b2/LICENSE0000644000014400001440000000307511006730200013617 0ustar kaltkalt00000000000000 Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008 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. drraw-2.2b2/drraw.conf0000644000014400001440000002745611126211727014624 0ustar kaltkalt00000000000000# # drraw configuration template/sample # # $Id: drraw.conf 1557 2008-12-29 18:15:51Z kalt $ # # This file should be installed in the same directory as drraw.cgi and # edited as needed to customize your installation. # # ** QUICK START : You need to edit variables which are not commented (below) # # ** ONCE IT WORKS: Take some time to go over this file again once you are # more familiar with drraw to customize it in order to better # suit your needs and preferences. # # this will help catch configuration mistakes use strict; # The title used as header for the index web pages #$title = 'Draw Round Robin Archives on the Web'; # If you want to add a custom header for all pages, define this.. #$header = # If you want to add a custom footer for all pages, define this.. #$footer = # Directories where Round Robin Databases may be found # They will be searched recursively for files matching *.rrd (Round Robin # Database files) and *.evt (Event files). # NOTE: Specify directory paths without any trailing slash %datadirs = ('/here/are/some/files' => '[Label1] ', '/here/are/other/files' => '[Label2] ', ); # This function is used to sort *.rrd and *.evt filenames before displaying # them for the user to choose from. It is passed the two arguments that # need to be compared.#sub mydatafnsort { 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 # ** IMPORTANT: These MUST match with how your RRD files are setup! @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' ); # How many seconds for each of the above views? # (Ignored when using rrdtool 1.0.47 or later) #@dv_secs = ( 100800, 604800, 2419200, 31536000 ); # Format used for the "Additional GPRINTs" #$gformat = "%8.2lf"; # Graph Overlay (Requires RRDtool 1.0.36 or later) #$overlay = '/somewhere/logo.gd'; # 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 # These directories MUST EXIST and the user running CGI scripts must have # read AND write access to them. It is entirely safe to delete any content # found in the temporary directory. $saved_dir = '/somewhere/drraw/saved'; $tmp_dir = '/somewhere/drraw/tmp'; # By default, critical errors are shown in the produced HTML pages produced # and sent to standard error (which web servers typically write to some # logfile). If the following is defined, such errors will be written to # file directly by drraw. #$ERRLOG = '/somewhere/drraw/tmp/errors.log'; # By default (as long as the Digest::MD5 Perl module is available), drraw # will cache images to avoid recomputing them needlessly. The following # sets how often drraw should purge expired images from the cache. # (Expired images being are automatically purged when refreshed.) # Setting this to 0 will disable caching altogether. $clean_cache = 21600; # 6 hours # Whether or not RCS will be used, this requires: # (1) The Perl "Rcs" module to be installed # (2) The $saved_dir/RCS directory to exist #$use_rcs = 1; # Whether or not to look for data source names in pnp4nagios' XML files. # This requires the Perl "XML::Simple" module to be installed. #$use_pnp4nagios = 1; # Security Levels: # 0 : Read-Only (e.g. graph viewing only) # 1 : Limited Read-Write (such users can define/save new graphs, # and delete saved graphs they saved) # 2 : Read-Write # User authentication/validation handling is left up to the web server. In # other words, for things to work, you will need to change the web server # configuration. If you don't, nothing will happen :-) # # Authenticated users automatically set the level to 2 unless the username # is found in %users (below) in which case the level is set accordingly to # what is defined. # Unauthenticated users get the 'guest' username. # # If you don't care for authentication, set the following to 2. #%users = ( 'guest' => 0 ); # By default, drraw will use the REMOTE_USER environment variable to find # the user's username. If you wish to use something else, simply define # the following subroutine. # NOTES: (1) drraw will fall back to using the REMOTE_USER environment variable # if this subroutine returns 'undef': use 'guest' if you wish to # leave the user unauthenticated. # (2) this subroutine may manipulate the %users array based on external # data allowing to process authentication entirely out of this # configuration file. #sub mygetuser { return $ENV{'SSL_CLIENT_EMAIL'}; } # If you want to override your system's default timezone, you may do so here. #$ENV{'TZ'} = 'UCT'; # If you have many RRD files and templates, indexes can become extremely # big and slow to load. This variable allows you to prevent all templates # choices from being displayed in the indexes based on how many choices # are available per template. #$IndexMax = 100; # Custom Index Definition # # Custom Indexes are defined as a HASH: # - Each key is the name that will be presented to the user # - Keys are sorted when the index is built # - If a key matches ^\d+\s+(\S.*)$ then only $1 will be shown # (This allows for keys to be sorted independently of what is shown.) # - The value for a given key must be one of the following: # - A HASH (for nested indexes) # - A SCALAR (for leaves) which will be matched as a regular expression # against the Graph titles, Template titles and Dashboard titles. # Matching titles will be presented to the user. # - An ARRAY (for leaves) containing 3 elements. Each element will be # matched as a regular expression against the Graph titles, Template # titles and Dashboard titles (respectively). Matching titles will # be presented to the user. # # For example: # %Index = ( '1 Network' => { 'Cisco' => [ undef, 'Cisco', 'Cisco' ], # 'Interfaces Statistics' => 'Interface', # 'Miscellaneous' => [ 'Traffic', '(DMZ Traffic|Firewall)', undef ], # 'VPN' => 'VPN' }, # '2 Storage' => [ undef, '(Disk|SAN)', '(Disk|SAN)' ], # '3 UNIX' => [ '(Solaris|BSD)', '(Solaris|BSD)', 'UNIX' ], # '4 Windows => [ 'Windows', 'Windows', 'Windows' ], # '5 Environmental' => [ 'Datacenter', undef, 'Datacenter' ] ); # # IMPORTANT NOTE #1: Nested indexes are implemented using JavaScript. # # IMPORTANT NOTE #2: The above feature trigges a bug in the CGI.pm module # from version 2.92 to version 3.04 # # Icons used in the browser # These may be copied from the drraw distribution if they are missing from # your web server installation. #$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'; # # Available colors # Note: You should never remove colors from the configured list as it will # break any saved graph/template using the colors you remove. #%colors = ( 'Black' => '#000000', # 'Silver' => '#C0C0C0', # 'Gray' => '#808080', # 'White' => '#FFFFFF', # 'Maroon' => '#800000', # 'Red' => '#FF0000', # 'Purple' => '#800080', # 'Green' => '#008000', # 'Lime' => '#00FF00', # 'Olive' => '#808000', # 'Yellow' => '#FFFF00', # 'Orange' => '#FFA500', # 'Pink' => '#FFC0CB', # 'Navy' => '#000080', # 'Blue' => '#0000FF', # 'Teal' => '#008080', # 'Aqua' => '#00FFFF', # 'Invisible' => '', # 'Aquamarine' => '#7FFFD4', # 'BlueViolet' => '#8A2BE2', # 'Brown' => '#A52A2A', # 'BurlyWood' => '#DEB887', # 'CadetBlue' => '#5F9EA0', # 'Chocolate' => '#D2691E', # 'Coral' => '#FF7F50', # 'CornflowerBlue' => '#6495ED', # 'Crimson' => '#DC143C', # 'DarkGoldenRod' => '#B8860B', # 'DarkGray' => '#A9A9A9', # 'DarkKhaki' => '#BDB76B', # 'DarkMagenta' => '#8B008B', # 'DarkOliveGreen' => '#556B2F', # 'Darkorange' => '#FF8C00', # 'DarkOrchid' => '#9932CC', # 'DarkRed' => '#8B0000', # 'DarkSalmon' => '#E9967A', # 'DarkSeaGreen' => '#8FBC8F', # 'DarkSlateBlue' => '#483D8B', # 'DarkSlateGray' => '#2F4F4F', # 'DarkViolet' => '#9400D3', # 'DeepPink' => '#FF1493', # 'DeepSkyBlue' => '#00BFFF', # 'DimGray' => '#696969', # 'DodgerBlue' => '#1E90FF', # 'Feldspar' => '#D19275', # 'FireBrick' => '#B22222', # 'ForestGreen' => '#228B22', # 'Fuchsia' => '#FF00FF', # 'Gainsboro' => '#DCDCDC', # 'Gold' => '#FFD700', # 'GoldenRod' => '#DAA520', # 'GreenYellow' => '#ADFF2F', # 'HotPink' => '#FF69B4', # 'IndianRed' => '#CD5C5C', # 'Indigo' => '#4B0082', # 'Khaki' => '#F0E68C', # 'LightBlue' => '#ADD8E6', # 'LightCoral' => '#F08080', # 'LightGrey' => '#D3D3D3', # 'LightGreen' => '#90EE90', # 'LightPink' => '#FFB6C1', # 'LightSalmon' => '#FFA07A', # 'LightSeaGreen' => '#20B2AA', # 'LightSlateBlue' => '#8470FF', # 'LimeGreen' => '#32CD32', # 'Magenta' => '#FF00FF', # 'Maroon' => '#800000', # 'MediumAquaMarine' => '#66CDAA', # 'MediumBlue' => '#0000CD', # 'MediumOrchid' => '#BA55D3', # 'MediumPurple' => '#9370D8', # 'MediumSeaGreen' => '#3CB371', # 'MediumSlateBlue' => '#7B68EE', # 'MediumSpringGreen' => '#00FA9A', # 'MediumTurquoise' => '#48D1CC', # 'MediumVioletRed' => '#C71585', # 'MidnightBlue' => '#191970', # 'Moccasin' => '#FFE4B5', # 'OliveDrab' => '#6B8E23', # 'OrangeRed' => '#FF4500', # 'Orchid' => '#DA70D6', # 'PaleGreen' => '#98FB98', # 'PaleVioletRed' => '#D87093', # 'Peru' => '#CD853F', # 'Plum' => '#DDA0DD', # 'RosyBrown' => '#BC8F8F', # 'RoyalBlue' => '#4169E1', # 'SaddleBrown' => '#8B4513', # 'Salmon' => '#FA8072', # 'SeaGreen' => '#2E8B57', # 'Sienna' => '#A0522D', # 'SkyBlue' => '#87CEEB', # 'SlateBlue' => '#6A5ACD', # 'SlateGray' => '#708090', # 'SpringGreen' => '#00FF7F', # 'SteelBlue' => '#4682B4', # 'Tan' => '#D2B48C', # 'Teal' => '#008080', # 'Thistle' => '#D8BFD8', # 'Tomato' => '#FF6347', # 'Turquoise' => '#40E0D0', # 'Violet' => '#EE82EE', # 'VioletRed' => '#D02090', # 'Wheat' => '#F5DEB3', # 'White' => '#FFFFFF', # 'Yellow' => '#FFFF00', # 'YellowGreen' => '#9ACD32', # ); # Custom Style Sheet # $CSS 1; # Local Variables: # mode: perl # indent-tabs-mode: nil # tab-width: 4 # cperl-indent-level: 4 # End: drraw-2.2b2/INSTALL0000644000014400001440000001044110755671614013665 0ustar kaltkalt00000000000000-- drraw NEWS Version 2.2 adds full support for the new features found in RRDtool 1.2 and above. It is important to note that RRDtool 1.2 and above deprecate a number of graph commands previously available. drraw handles this in the following way: * deprecated syntax is not allowed when creating new graphs. * deprecated syntax from old saved graphs is converted (when possible) when drawing these graphs. * deprecated syntax from old saved graphs is never converted when editing these graphs, this has to be done manually. -- drraw dependencies drraw requires: * a working perl with the CGI module. It was originally written and tested using version 5.6.1. It is known to run fine on UNIX (with Apache) as well as Windows (with Apache and IIS). Note that on Windows, ActivePerl 5.8.0 is required but that earlier versions may work as long as you comment out lines with calls to alarm(). * The RRDtool Perl module "RRDs" is also (unsurprisingly) needed. It is strongly recommended to use version 1.0.47 or above if possible, and version 1.0.36 at the very least. -- drraw installation notes drraw is a standalone CGI script with a separate configuration file. The file drraw.cgi should be installed as a CGI script for your web server, and the drraw.conf file should be copied to the same directory. ****************************************************************************** ** To get going, the only configuration required is %datadirs in drraw.conf ** ****************************************************************************** While all the configurable options are located in the drraw.conf file, you will need to edit drraw.cgi if (a) you want to put the drraw.conf file somewhere else, (b) the RRDs.pm perl module was not installed in a directory mentioned in your perl's @INC, (c) UNIX: /usr/bin/perl doesn't exist or isn't the perl you want to use OTHERS: Typically, web servers running on non UNIX platforms will ignore the first line of the script and are instead configured to use Perl (for example). Microsoft IIS is an example of such a server. To get drraw working on such a server you have three choices: (1) Remove the -T option on the first line. !!!NOT RECOMMENDED!!! (2) Configure the server to run Perl scripts with taint mode on. (This will affect _all_ Perl scripts.) (3) Work around the problem by creating a second special extension such as tcgi or tgi which is associated with taint mode Perl. -- drraw security Don't believe that because drraw is open source, it is secure. This is just stupid anti Microsoft propaganda. To my knowledge, noone has ever thoroughly reviewed drraw's code, meaning that while some care was taken to write a safe tool, it comes with NO GUARANTEE at all. Run it at your own risk. This said, drraw uses Perl's "taint mode", and has a simple (and optional) security system built-in to allow restricting read-write access. In other words, you should be fine (but don't take my word on it). -- drraw contact If you want to be informed about important news about drraw, such as new releases, subscribe to the drraw-announce mailing list, either by sending a mail including the "subscribe" keyword to drraw-announce-request@taranis.org, or simply visit the following web page: http://web.taranis.org/mailman/listinfo/drraw-announce For more discussions, subscribe to the drraw-users mailing list, either by sending a mail including the "subscribe" keyword to drraw-users-request@taranis.org, or simply visit the following web page: http://web.taranis.org/mailman/listinfo/drraw-users Send bug reports to `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. 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 `drraw' or have suggestions, share your findings by writing to `drraw@taranis.org'. -- $Id: INSTALL 1389 2008-02-16 23:36:44Z kalt $ drraw-2.2b2/CHANGES0000644000014400001440000002766711126211727013635 0ustar kaltkalt00000000000000$Id: CHANGES 1557 2008-12-29 18:15:51Z kalt $ Changes since 2.2b1 [2008-10-06]: - Added pnp4nagios awareness (courtesy of Jeremy Jacquier-Roux). - Dashboards weren't always sorted. - Updated logic to prevent Data Source Templates from skipping too many files. - It wasn't possible to add a numeric DS (reported by Jeremy Jacquier-roux). - Removed compatibility reporting code. Changes since 2.2a5 [2008-05-03]: - Added trend display pages. - Dashboard style display for templates lacked links. - Suppressed undefined variable warnings (courtesy of Martin Schuster). - Replace "missing" graphs with "unknown" icon (courtesy of R.P. Aditya). - Rcs module now(?) needs more to be untainted (reported by "Matt Perry). Changes since 2.2a4 [2007-05-30]: - Various dates displayed now follow the ISO 8601 format. - Graph start date, end date and creation dates can now be added to the image. - Data Source Templates have been enhanced: - Dynamic elements can now be stacked on top of previous graph element not just on top of themselves (courtesy of François Deppierraz), - Number of elements can be used in dynamic element CDEFs, and Formula field, - Builtin formulas allow calculating the AVERAGE and STDDEV, - Old files and files without values are automatically skipped (courtesy of R.P. Aditya). - Orange look, still ugly though :) - New Data Source rows are now properly disabled on load. - Allow &DBFinder() to follow symlinks. - Switched to /usr/bin/perl (from /usr/local/bin/perl). Changes since 2.2a3 [2007-03-17]: - Fixed support for SHIFT graph command. - Updated RRDs::fetch usage again (courtesy of Andy Mayhew). - Saving dashboards could generate bad index entries (reported by Fabien Wernli). - Added "Invisible" color (suggested by John Rouillard). - Dashboard style display for templates broke custom time display of templates (reported by Mark Noworolski). - Two typos caused forms to use POST instead of GET (courtesy of Fabien Wernli). - The graph editor no longer allowed stacking CDEFs or VDEFs (reported by John Rouillard). Changes since 2.2a2 [2007-01-14]: - Improved default color assignment (courtesy of David Nolan). - Allow configuring a custom set of colors (courtesy of David Nolan). - Updated RRDs::fetch usage for newer RRDtool versions (courtesy of John Milton) Changes since 2.2a1 [2006-07-20]: - Added a dashboard style display for templates (with multiple selections). - New "Format" parameter can be used in graph URL to override saved setting. - PDF and SVG formats are now properly embedded in HTML. - Added warning and check to avoid obscure error caused by misconfiguration. - Minor JavaScript enhancements to the graph editor. - Added notice about CGI.pm incompatibility. Changes since 2.1.3 [2005-06-19]: - Various improvements for the graph editor: + Extensive code rewrite/cleanup. + Added simple HTML tooltips to provide (some) contextual help. + Extended JavaScript automation. + Clarified creation of new Data Sources. - rrdtool 1.2.x support: + Three new image formats: SVG, EPS and PDF. + GIF is no longer supported, automatically switch to PNG. + Removed support for deprecated --overlay parameter. + Added support for --no-gridfit. + Added support for new VDEF, TICK and SHIFT graph commands. + Deprecated use of HRULE, STACK and old style GPRINT graph command. Changes since 2.1 [2004-10-14]: - Fixed misleading typo in drraw.conf (courtesy of Rodney Beede). - Disable fields for new (marked for deletion) DS in editor. - Fixed yet another File::Find (although unlikely) taint problem. - GPRINTs were not handled correctly for CDEFs (courtesy of Jim Pirzyk). - Fixed "None" choice for "Y Grid" graph option (courtesy of Darryl Dunkin). - Fixed CGI parameter collision (courtesy of John Milton). - Disabled generation of hidden .submit and .cgifields. - Added $drefresh configuration setting. - Win32's inability to rename opened files broke caching (reported by Patrick Varney). - The cache was unlikely to ever be thoroughly purged, fixed logic. - Addition of $ERRLOG configuration setting. - Fixed relaying of cached images for portability (reported by Patrick Varney). - More error checking (and logging) about some cache operations. - Fixed regular expression used for finding files in %datadirs (reported by John Milton). Changes since 2.0 [2004-06-24]: - Authentication code improvements (courtesy of David Nolan). NOTE: The $level and %guests configuration setting are now obsolete; NOTE: Use %users instead. - warn() when unable to load Rcs.pm (courtesy of David Nolan). - Non standard views now include start and end dates. - Fixed spurious cloning when subsequently saving the same item in editor. - Use stricter regular expression when finding files in %datadirs. - Fixed handling of DashBoard filtering. - Minor HTML changes for Mozilla. - Added $IndexMax configuration setting. - Added "missing" Update button after Start/End fields in Graph/Template editor. - Added refresh countdown on display pages. - Enhanced Graph/Template/Dashboard editor using JavaScript. - Main page Graph/Template/Dashboard browser is now customizable. - Event timestamp format now depends on how long ago the first event occured. - Added time to pages with graphs. - Dashboard pages now have a dynamic refresh time. Changes since 2.0b3 [2004-04-27]: - Quieted harmless Perl warning related to cache purging. - Fixed typo resulting in occasional deletion of cache files being generated. Changes since 2.0b2 [2004-04-15]: - ':' characters in all labels are now escaped prior to calling RRDs::graph(). NOTE: This will break graph definitions where ':' were manually escaped. - Fixed ':' characters escaping for events prior to calling RRDs::graph(). - BR setting was ignored for events (courtesy of Jesper Dangaard Brouer). - Fixed labeled event filtering. Changes since 2.0b1 [2004-04-09]: - Fixed "Graph Title" validation for templates. - &DBFinder's die message was fairly useless, who knew it'd ever show up. - Successful drraw reporting needn't be logged. Changes since 1.2 [2004-01-05]: - Templates are now required to have a "Graph Title". - Dashboards may now be filtered. - Quieted harmless (but annoying) Perl warning related to template edition. - Handle HTTP 'If-Modified-Since' headers. - Don't bother specifying events outside time boundaries when producing graphs. - Fixed File::Find Win32 taint problem (reported by Ben Swaby). - Produced images are now cached. - HTTP 'Expires' and 'Last-Modified' headers are now generated. - Allow customizing remote username determination (suggested by David Nolan). - Added $header and $footer configuration settings (courtesy of David Nolan). - Fixed minor locking bug. - Improved timeout error message. - Added simple logging of saves and deletes. - Minor style (CSS) changes. - Added "rrdtool info" hook. - Saved definitions may now be kept under revision control (using RCS). - Fixed templates to work with multiple %datadirs directories. - Base choices are now scrolling list in "empty" template and dashboard viewers. - Base parameters may contain spaces and therefore need to be quoted. - Not all generated pages had style (reported by Craig Van Rij). - Dashboard "base" was lost by Start/End entries (reported by Steffen Plotner). Changes since 1.1 [2003-11-10]: - Dashboard template choices should be sorted (reported by Steffen Plotner). - Only do work in &TMPLFind() if a filter is actually given. - Quieted harmless Perl warning (reported by Edgar Whipple). - self_url() URLs aren't usable through proxies (reported by Abfalterer Roland). - Fixed logic checking whether &mydatafnsort() is defined. - RRDs::fetch result check needs to be stricter (reported by Jon A. Tankersley). Changes since 1.0 [2003-10-31]: - Use NUL: instead of /dev/null on Windows (reported by Moore Macauley). - ':' characters need to be escaped from filenames (reported by Moore Macauley). - It is now possible to pass any (even unsupported by drraw) option to rrdgraph. - Added support for rrdgraph's --units-exponent (courtesy of John P. Rouillard). - Added $title and $gformat configuration settings (courtesy of Edgar Whipple). - Labels are now automatically padded. Changes since 0.11b [2003-09-16]: - Added a default overlay. - Pages that are refreshed must be GET'd, fixed couple typos related to this. Changes since 0.10b [2003-07-25]: - Added default icons to the distribution. - Suppressed spurious warnings related to $overlay (reported by Edgar Whipple). - Fixed handling of "Formula" DS Templates graph. - STACKing of DS Templates is now properly configurable. - Cleaned up use of styles. - Help links now have a (non blank) "target". - Improved documentation of dashboard configuration options. - Fixed handling of grouped dashboards when using default "Row Name". - Finally (briefly) documented DS Templates. - DS Configuration drop down lists are now optional to avoid massive forms (reported by Didier Colin). Changes since 0.8b [2003-07-15]: - Minor documentation updates. - Improved handling of stale references when editing dashboards. - Fixed refresh of custom views. - Miscellaneous minor code cleanups. - Added timezone to configuration (suggested by Edgar Whipple). - Moved icon and style definitions to configuration file. - Fixed view links on dashboard template pages (reported by Taylor Steil). Changes since 0.7a [2003-03-12]: - Code allowing old template definitions to work has been removed. NOTE: Yes, this means that backward compatibility is no longer supported! - Added link to drraw compatibility report page. - Moved option configuration to a separate file. - Fixed handling of template based graph cloning. - Fixed dashboard index corruption. - Improved online help (somewhat). Changes since 0.6a [2003-03-10]: - Added RRDtool error reporting. - Enhanced auto-generation of graph DS. - $ can be used as an alias for the DS variable in associated CDEF strings. - Check if overlay files exists before passing to RRDtool. - Fixed CDEFs which were broken. Changes since 0.5a [2003-02-17]: - Graph DS may now be generated on the fly, along with associated CDEF. - Added graph type ("Nothing") to simplify defining a DS without plotting it. - Graph/Template editor now allows changing DS files for events as well. - New more flexible template syntax supports perl-like string substitutions. NOTE: Old definitions should still work (but expect support for these to be > removed before 1.0 release) but will be shown as erroneous (e.g. in > red) in the edition page. - Added $dsfilter_def configuration setting (suggested by Kim Taylor). - RRD and DB file lists may now be filtered. - %rranames is now used in editing pages. - Sorting of RRD/EVT filenames is now configurable (suggested by Kim Taylor). Changes since 0.4a [2002-12-28]: - Graph/Template editor now allows changing a DS file. - Added validation checks for $tmp_dir and $saved_dir configuration settings. - Cache refresh timer is now configurable ($crefresh configuration setting). - Template file generation slightly enhanced (** in addition to *). - Added support for "event files" allowing easy generation of VRULE elements. - Enhanced dashboards to allow for "dashboard templates". - Optionally limit how long a CGI may run ($maxtime configuration setting). - Fixed error reporting to only output HTML code when producing an HTML page. - Various changes to the UI to improve user experience/friendliness. - Added "/bin" to the PATH for File::Find (reported by Bill Bradley). Changes since 0.3a [2002-11-19]: - Updated code to run with taint mode enabled. - Added support for user authentication. - Fixed edition bug for graphs cloned from a template. - Implemented basic dashboards. - STDOUT needs to be unbuffered (reported by Daniel Dulac). - Added a quick way to switch between targets when viewing a template. Changes since 0.2a [2002-11-11]: - Added basic inline help. - Added 'Start' and 'End' textboxes to the graph editor page. - Added 'Start' and 'End' textboxes to the multi-view page. - Minor cosmetic changes. Changes since 0.1a [2002-11-03]: - Fixed "Make Template" which was broken. - Templating now ignores CDEFs. drraw-2.2b2/icons/0000755000014400001440000000000011152024101013716 5ustar kaltkalt00000000000000drraw-2.2b2/icons/folder.gif0000644000014400001440000000034107731712060015676 0ustar kaltkalt00000000000000GIF89aÂÿÿÿÿÌ™Ìÿÿ™f3333!þNThis art is in the public domain. Kevin Hughes, kevinh@eit.com, September 1995!ù,T(ºÜþ0ÊIY¹øÎºïEÄ}d¦)Å@zj‰C,Ç+ŽõWž<œì IEáqg<²‚N"4ÚIRaV˜VÅÝÂx‚Ôl<&€Ãæ´zÍ#,LðŒ;;drraw-2.2b2/icons/folder.open.gif0000644000014400001440000000036207731712060016641 0ustar kaltkalt00000000000000GIF89aÂÿÿÿÿÌ™Ìÿÿ»»»™f3333!þNThis art is in the public domain. Kevin Hughes, kevinh@eit.com, September 1995!ù,e(ºÜþ0ÊI«m&ë|—`Ý'ž™g’뺰#£sÊÝoÀ ¡ uÂa¡ˆJ:Ÿ„¥à|ZQ˜¬zuf=½®W pÅÁ/ãsF«×mñC¨Ûï÷9¦ïûý*:ƒ5 ;drraw-2.2b2/icons/generic.gif0000644000014400001440000000033507731712060016042 0ustar kaltkalt00000000000000GIF89aÂÿÿÿÌÿÿ™™™333!þNThis art is in the public domain. Kevin Hughes, kevinh@eit.com, September 1995!ù,P8º¼ñ0 @«#¾i;Ï×U"¸×‰(1¸ð‰RDmòL ¼€K:KïÇ |9㑨¬ ÍeÒø,«Q)´‰v¹L/qÐ+›y¯ÈmÍþ%;drraw-2.2b2/icons/link.gif0000644000014400001440000000037107731712060015363 0ustar kaltkalt00000000000000GIF89aÂÿÿÿÿ33Ìÿÿ™™™f333!þNThis art is in the public domain. Kevin Hughes, kevinh@eit.com, September 1995!ù,lXº¼òP@«%¾i;Ï×U"¸×‰¨Q¸ð‰RFmòL ü€K:KïDz_®sDV”@TÓ €N¦¬6»#°ÛmÕ ãaÍ^kÑéè¨ +Ǻ}¶NzƒBD=„…