# -*- mode: Perl -*- package MRTG_lib; ################################################################### # MRTG 2.14.5 Support library MRTG_lib.pm ################################################################### # Created by Tobias Oetiker # and Dave Rand # # For individual Contributers check the CHANGES file # ################################################################### # # Distributed under the GNU General Public License # ################################################################### require 5.005; use strict; use vars qw($OS $SL $PS @EXPORT @ISA $VERSION %timestrpospattern); if (eval {local $SIG{__DIE__}; require Net_SNMP_util} ) { import Net_SNMP_util; } else { require SNMP_util; import SNMP_util; } my %mrtgrules; BEGIN { # Automatic OS detection ... do NOT touch if ( $^O =~ /^(?:(ms)?(dos|win(32|nt)?))/i ) { $OS = 'NT'; $SL = '\\'; $PS = ';'; } elsif ( $^O =~ /^NetWare$/i ) { $OS = 'NW'; $SL = '/'; $PS = ';'; } elsif ( $^O =~ /^VMS$/i ) { $OS = 'VMS'; $SL = '.'; $PS = ':'; } elsif ( $^O =~ /^os2$/i ) { $OS = 'OS2'; $SL = '/'; $PS = ';'; } else { $OS = 'UNIX'; $SL = '/'; $PS = ':'; } } require Exporter; @ISA = qw(Exporter); @EXPORT = qw(readcfg cfgcheck setup_loghandlers datestr expistr ensureSL timestamp create_pid demonize_me debug log2rrd storeincache readfromcache clearfromcache cleanhostkey populateconfcache readconfcache writeconfcache v4onlyifnecessary); $VERSION = 2.100016; %timestrpospattern = ( 'NO' => 0, 'LU' => 1, 'RU' => 2, 'LL' => 3, 'RL' => 4 ); %mrtgrules = ( # General CFG 'workdir' => [sub{$_[0] && (-d $_[0])}, sub{"Working directory $_[0] does not exist"}], 'htmldir' => [sub{$_[0] && (-d $_[0])}, sub{"Html directory $_[0] does not exist"}], 'imagedir' => [sub{$_[0] && (-d $_[0])}, sub{"Image directory $_[0] does not exist"}], 'logdir' => [sub{$_[0] && (-d $_[0] )}, sub{"Log directory $_[0] does not exist"}], 'forks' => [sub{$_[0] && (int($_[0]) > 0 and $MRTG_lib::OS eq 'UNIX')}, sub{"Less than 1 fork or not running on Unix/Linux"}], 'refresh' => [sub{int($_[0]) >= 300}, sub{"$_[0] should be 300 seconds or more"}], 'enablesnmpv3' => [sub{((lc($_[0])) eq 'yes' or (lc($_[0])) eq 'no')}, sub{"$_[0] must be yes or no"}], 'enableipv6' => [sub{((lc($_[0])) eq 'yes' or (lc($_[0])) eq 'no')}, sub{"$_[0] must be yes or no"}], 'interval' => [sub{int($_[0]) >= 1 and int($_[0]) <= 60}, sub{"$_[0] should be at least 1 Minute and no more than 60 Minutes"}], 'writeexpires' => [sub{1}, sub{"Internal Error"}], 'nomib2' => [sub{1}, sub{"Internal Error"}], 'singlerequest' => [sub{1}, sub{"Internal Error"}], 'icondir' => [sub{$_[0]}, sub{"Directory argument missing"}], 'language' => [sub{1}, sub{"Mrtg not localized for $_[0] - defaulting to english"}], 'loadmibs' => [sub{$_[0]}, sub{"No MIB Files specified"}], 'userrdtool' => [sub{0}, sub{"UseRRDtool is not valid any more. Use LogFormat, PathAdd and LibAdd instead"}], 'userrdtool[]' => [sub{0}, sub{"UseRRDtool[] is not valid any more. Check the new xyz*bla[] syntax for passing parameters to tool xyz who reads the mrtg.cfg"}], 'logformat' => [sub{$_[0] =~ /^(rateup|rrdtool)$/}, sub{"Invalid Logformat '$_[0]'"}], 'pathadd' => [sub{-d $_[0]}, sub{"$_[0] is not the name of a directory"}], 'libadd' => [sub{-d $_[0]}, sub{"$_[0] is not the name of a directory"}], 'runasdaemon' => [sub{1}, sub{"Internal Error"}], 'nodetach' => [sub{1}, sub{"Internal Error"}], 'maxage' => [sub{(($_[0] =~ /^[0-9]+$/) and ($_[0] > 0)) }, sub{"$_[0] must be a Number bigger than 0"}], 'nospacechar' => [sub{length($_[0]) == 1}, sub{"$_[0] must be one character long"}], 'snmpoptions' => [sub{ eval( 'local $SIG{__DIE__};{'.$_[0].'}' ); return not $@}, sub{"Must have the format \"OptA => Number, OptB => 'String', ... \""}], 'conversioncode' => [sub{-r $_[0]}, sub{"Cannot read conversion code file $_[0]"}], # Per Router CFG 'target[]' => [sub{1}, sub{"Internal Error"}], #will test this later 'snmpoptions[]' => [sub{ eval( 'local $SIG{__DIE__};{'.$_[0].'}' ); return not $@}, sub{"Must have the format \"OptA => Number, OptB => 'String', ... \""}], 'routeruptime[]' => [sub{1}, sub{"Internal Error"}], #will test this later 'routername[]' => [sub{1}, sub{"Internal Error"}], #will test this later 'maxbytes[]' => [sub{(($_[0] =~ /^[0-9]+$/) && ($_[0] > 0)) }, sub{"$_[0] must be a Number bigger than 0"}], 'maxbytes1[]' => [sub{(($_[0] =~ /^[0-9]+$/) && ($_[0] > 0))}, sub{"$_[0] must be numerical and larger than 0"}], 'maxbytes2[]' => [sub{(($_[0] =~ /^[0-9]+$/) && ($_[0] > 0))}, sub{"$_[0] must a number bigger than 0"}], 'ipv4only[]' => [sub{((lc($_[0])) eq 'yes' or (lc($_[0])) eq 'no')}, sub{"$_[0] must be yes or no"}], 'absmax[]' => [sub{($_[0] =~ /^[0-9]+$/)}, sub{"$_[0] must be a Number"}], 'title[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'directory[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'clonedirectory[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'pagetop[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'bodytag[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'pagefoot[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'addhead[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'rrdrowcount[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'extension[]' => [sub{1}, sub{"Internal Error"}], #what ever the user chooses. 'unscaled[]' => [sub{$_[0] =~ /[ndwmy]+/i}, sub{"Must be a string of [n]one, [d]ay, [w]eek, [m]onth, [y]ear"}], 'weekformat[]' => [sub{$_[0] =~ /[UVW]/}, sub{"Must be either W, V, or U"}], 'withpeak[]' => [sub{$_[0] =~ /[ndwmy]+/i}, sub{"Must be a string of [n]one, [d]ay, [w]eek, [m]onth, [y]ear"}], 'suppress[]' => [sub{$_[0] =~ /[ndwmy]+/i}, sub{"Must be a string of [n]one, [d]ay, [w]eek, [m]onth, [y]ear"}], 'xsize[]' => [sub{((int($_[0]) >= 30) && (int($_[0]) <= 600))}, sub{"$_[0] must be between 30 and 600 pixels"}], 'ysize[]' => [sub{(int($_[0]) >= 30)}, sub{"Must be >= 30 pixels"}], 'ytics[]' => [sub{(int($_[0]) >= 1) }, sub{"Must be >= 1"}], 'yticsfactor[]' => [sub{$_[0] =~ /[-+0-9.efg]+/}, sub{"Should be a numerical value"}], 'factor[]' => [sub{$_[0] =~ /[-+0-9.efg]+/}, sub{"Should be a numerical value"}], 'step[]' => [sub{(int($_[0]) >= 0)}, sub{"$_[0] must be > 0"}], 'timezone[]' => [sub{1}, sub{"Internal Error"}], 'options[]' => [sub{1}, sub{"Internal Error"}], 'colours[]' => [sub{1}, sub{"Internal Error"}], 'background[]' => [sub{1}, sub{"Internal Error"}], 'kilo[]' => [sub{($_[0] =~ /^[0-9]+$/)}, sub{"$_[0] must be a Integer Number"}], #define whatever k should be (1000, 1024, ???) 'kmg[]' => [sub{1}, sub{"Internal Error"}], 'pngtitle[]' => [sub{1}, sub{"Internal Error"}], 'ylegend[]' => [sub{1}, sub{"Internal Error"}], 'shortlegend[]' => [sub{1}, sub{"Internal Error"}], 'legend1[]' => [sub{1}, sub{"Internal Error"}], 'legend2[]' => [sub{1}, sub{"Internal Error"}], 'legend3[]' => [sub{1}, sub{"Internal Error"}], 'legend4[]' => [sub{1}, sub{"Internal Error"}], 'legend5[]' => [sub{1}, sub{"Internal Error"}], 'legendi[]' => [sub{1}, sub{"Internal Error"}], 'legendo[]' => [sub{1}, sub{"Internal Error"}], 'setenv[]' => [sub{$_[0] =~ /^(?:[-\w]+=\"[^"]*"(?:\s+|$))+$/}, sub{"$_[0] must be XY=\"dddd\" AASD=\"kjlkj\" ... "}], 'xzoom[]' => [sub{($_[0] =~ /^[0-9]+(?:\.[0-9]+)?$/)}, sub{"$_[0] must be a Number xxx.xxx"}], 'yzoom[]' => [sub{($_[0] =~ /^[0-9]+(?:\.[0-9]+)?$/)}, sub{"$_[0] must be a Number xxx.xxx"}], 'xscale[]' => [sub{($_[0] =~ /^[0-9]+(?:\.[0-9]+)?$/)}, sub{"$_[0] must be a Number xxx.xxx"}], 'yscale[]' => [sub{($_[0] =~ /^[0-9]+(?:\.[0-9]+)?$/)}, sub{"$_[0] must be a Number xxx.xxx"}], 'threshdir' => [sub{$_[0] && (-d $_[0])}, sub{"Threshold directory $_[0] does not exist"}], 'threshmini[]' => [sub{1}, sub{"Internal Threshold Config Error"}], 'threshmino[]' => [sub{1}, sub{"Internal Threshold Config Error"}], 'threshmaxi[]' => [sub{1}, sub{"Internal Threshold Config Error"}], 'threshmaxo[]' => [sub{1}, sub{"Internal Threshold Config Error"}], 'threshdesc[]' => [sub{1}, sub{"Internal Threshold Config Error"}], 'threshprogi[]' => [sub{$_[0] && (-e $_[0])}, sub{"Threshold program $_[0] cannot be executed"}], 'threshprogo[]' => [sub{$_[0] && (-e $_[0])}, sub{"Threshold program $_[0] cannot be executed"}], 'threshprogoki[]' => [sub{$_[0] && (-e $_[0])}, sub{"Threshold program $_[0] cannot be executed"}], 'threshprogoko[]' => [sub{$_[0] && (-e $_[0])}, sub{"Threshold program $_[0] cannot be executed"}], 'timestrpos[]' => [sub{$_[0] =~ /^(no|[lr][ul])$/i}, sub{"Must be a string of NO, LU, RU, LL, RL"}], 'timestrfmt[]' => [sub{1}, sub{"Internal Error"}] #what ever the user chooses. ); # config file reading sub readcfg ($$$$;$$) { my $cfgfile = shift; my $routers = shift; my $cfg = shift; my $rcfg = shift; my $extprefix = shift || ''; my $extrules = shift; my ($first,$second,$key,$userules); my (%seen); my (%pre,%post,%deflt,%defaulted); unless ($cfgfile) { die "ERROR: readfg: no configfile specified\n"; } unless (ref($routers) eq 'ARRAY' and ref($cfg) eq 'HASH' and ref($rcfg) eq 'HASH') { die "ERROR: readcfg called with wrong arguments\n"; } if ($extprefix and ref($extrules) ne 'HASH') { die "ERROR: readcfg called with wrong args for mrtg extension\n"; } my $hand; my $file; my @filestack; local *CFG; if ($cfgfile eq '-'){$cfgfile = '<&STDIN'}; open (CFG, $cfgfile) || die "ERROR: unable to open config file: $cfgfile\n"; $hand = *CFG; my @handstack; my $nextfile = $cfgfile; my %routerhash; while (1) { if (eof $hand || not defined ($_ = <$hand>) ) { close $hand; if (scalar @handstack){ $hand = pop @handstack; $nextfile = pop @filestack; next; } else { last; } } $file=$nextfile; chomp; my $line = $.; if (/^include:\s*(.*?\S)\s*$/i){ push @filestack, $file; push @handstack, $hand; $nextfile = $1; local *FH; open (FH, $nextfile) || open (FH, ($cfgfile =~ m#(.+)${MRTG_lib::SL}[^${MRTG_lib::SL}]+$#)[0] . ${MRTG_lib::SL} . $nextfile) || do { die "ERROR: unable to open include file: $nextfile\n"}; $hand = *FH; next; } debug('cfg',"$file\[$.\]: $_"); s/\t/ /g; #replace tab by space s/\r$//; # kill dos newlines ... s/ +$//g; #remove space at the end of the line next if /^ *\#/; #ignore comment lines next if /^ *$/; #ignore empty lines # oops spelling error s/^supress/suppress/gi; # the line we got starts with white space so it is to be appended to what ever # was on the previous line. if (defined $first && /^\s+(.*\S)\s*$/) { if (defined $second) { $second eq '^' && do { $pre{$first} .= "\n".$1; next}; $second eq '$' && do { $post{$first} .= "\n".$1; next}; $second eq '_' && do { $deflt{$first} .= "\n".$1; next}; $$rcfg{$first}{$second} .= " ".$1; } else { $$cfg{$first} .= "\n".$1; } next; } if (defined $first && defined $second && defined $post{$first} && ($second !~ /^[\$^_]$/)) { if (defined $defaulted{$first}{$second}) { $$rcfg{$first}{$second} = $post{$first}; delete $defaulted{$first}{$second}; } else { $$rcfg{$first}{$second} .= ( defined $$cfg{nospacechar} and $post{$first} =~ /(.*)\Q$$cfg{nospacechar}\E$/) ? $1 : " ".$post{$first} ; } } if (defined $first and $first =~ m/^([^*]+)\*(.+)$/) { $userules = ($1 eq $extprefix ? $extrules : ''); } else { $userules = \%mrtgrules; } if ($first && defined $deflt{$first} && ($second eq '_')) { quickcheck($first,$second,$deflt{$first},$file,$line,$userules) } elsif ($first && $second && ($second !~ /^[\$^_]$/)) { quickcheck($first,$second,$$rcfg{$first}{$second},$file,$line,$userules) } elsif ($first && not $second) { quickcheck($first,0,$$cfg{$first},$file, $line,$userules) } if (/^([A-Za-z0-9*]+)\[(\S+)\]\s*:\s*(.*\S?)\s*$/) { $first = lc($1); $second = lc($2); # For us spelling-handicapped Americans. ;) # James Overbeck, grendel@gmo.jp, 2003/01/19 if ($first eq 'colors') { $first = 'colours' }; if ($second eq '^') { if ($3 ne '') { $pre{$first}=$3; } else { delete $pre{$first}; } next; } if ($second eq '$') { if ($3 ne '') { $post{$first}=$3; } else { delete $post{$first}; } next; } if ($second eq '_') { if ($3 ne '') { $deflt{$first}=$3; } else { delete $deflt{$first}; } next; } if (not defined $routerhash{$second}) { push (@{$routers}, $second); $routerhash{$second} = 1; } # make sure that default tags spring into existance upon first # call of a router foreach $key (keys %deflt) { if (! defined $$rcfg{$key}{$second}) { $$rcfg{$key}{$second} = $deflt{$key}; $defaulted{$key}{$second} = 1; } } # make sure that prefix-only tags spring into existance upon first # call of a router foreach $key (keys %pre) { if (! defined $$rcfg{$key}{$second}) { delete $defaulted{$key}{$second} if $defaulted{$key}{$second}; $$rcfg{$key}{$second} = ( defined $$cfg{nospacechar} && $pre{$key} =~ m/(.*)\Q$$cfg{nospacechar}\E$/ ) ? $1 : $pre{$key}." "; } } if ($seen{$first}{$second}) { die ("ERROR: Line $line ($_) in CFG file ($file)\n". "contains a duplicate definition for $first\[$second].\n". "First definition is on line $seen{$first}{$second}\n") } else { $seen{$first}{$second} = $line; } if ($defaulted{$first}{$second}) { $$rcfg{$first}{$second} = ''; delete $defaulted{$first}{$second}; } $$rcfg{$first}{$second} .= $3; next; } if (/^(\S+):\s*(.*\S)\s*$/) { $first = lc($1); $$cfg{$first} = $2; $second = ''; next; } die "ERROR: Line $line ($_) in CFG file ($file) does not make sense\n"; } # append $ stuff to the very last tag in cfg file if necessary if (defined $first && defined $second && defined $post{$first} && ($second !~ /^[\$^_]$/)) { if ($defaulted{$first}{$second}) { $$rcfg{$first}{$second} = $post{$first}; delete $defaulted{$first}{$second}; } else { $$rcfg{$first}{$second} .= ( defined $$cfg{'nospacechar'} && $post{$first} =~ /(.*)\Q$$cfg{nospacechar}\E$/ ) ? $1 : " ".$post{$first} ; } } #check the last input line if ($first =~ m/^([^*]+)\*(.+)$/) { $userules = ($1 eq $extprefix ? $extrules : ''); } else { $userules = \%mrtgrules; } if ($first && defined $deflt{$first} && ($second eq '_')) { quickcheck($first,$second,$deflt{$first},$file,$.,$userules) } elsif ($first && $second && ($second !~ /^[\$^_]$/)) { quickcheck($first,$second,$$rcfg{$first}{$second},$file,$.,$userules) } elsif ($first && not $second) { quickcheck($first,0,$$cfg{$first},$file,$.,$userules) } close (CFG); } # quick checks sub quickcheck ($$$$$$) { my ($first,$second,$arg,$file,$line,$rules) = @_; return unless ref($rules) eq 'HASH'; my $braces = $second ? '[]':''; if (exists $rules->{$first.$braces}) { if (&{$rules->{$first.$braces}[0]}($arg)) { return 1; } else { if ($second) { die "ERROR: CFG Error in \"$first\[$second\]\", line $line: ". &{$rules->{$first.$braces}[1]}($arg)."\n\n"; } else { die "ERROR: CFG Error in \"$first\", line $line: ". &{$rules->{$first.$braces}[1]}($arg)."\n\n"; } } } die "ERROR: CFG Error Unknown Option \"$first\" on line $line or above.\n". " Check doc/reference.txt for Help\n\n"; } # complex config checks sub mkdirhier ($){ my @dirs = split /\Q${MRTG_lib::SL}\E+/, shift; my $path = ""; while (@dirs){ $path .= shift @dirs; $path .= ${MRTG_lib::SL}; if (! -d $path){ warn ("WARNING: $path did not exist I will create it now\n"); mkdir $path, 0777 or die ("ERROR: mkdir $path: $!\n"); } } } sub cfgcheck ($$$$;$) { my $routers = shift; my $cfg = shift; my $rcfg = shift; my $target = shift; my $opts = shift || {}; my ($rou, $confname, $one_option); # Target index hash. Keys are "int:community@router" target definition # strings and values are indices of the @$target array. Used to avoid # duplicate entries in @$target. my $targIndex = { }; my $error="no"; my(@known_options) = qw(growright bits noinfo absolute gauge nopercent avgpeak derive integer perhour perminute transparent dorelpercent unknaszero withzeroes noborder noarrow noi noo nobanner nolegend logscale secondmean pngdate printrouter); snmpmapOID('hrSystemUptime' => '1.3.6.1.2.1.25.1.1'); if (defined $$cfg{workdir}) { die ("ERROR: WorkDir must not contain spaces when running on Windows. (Yeat another reason to get Linux)\n") if ($OS eq 'NT' or $OS eq 'OS2') and $$cfg{workdir} =~ /\s/; ensureSL(\$$cfg{workdir}); $$cfg{logdir}=$$cfg{htmldir}=$$cfg{imagedir}=$$cfg{workdir}; mkdirhier "$$cfg{workdir}" unless $opts->{check}; } elsif ( not (defined $$cfg{logdir} or defined $$cfg{htmldir} or defined $$cfg{imagedir})) { die ("ERROR: \"WorkDir\" not specified in mrtg config file\n"); $error = "yes"; } else { if (! defined $$cfg{logdir}) { warn ("WARNING: \"LogDir\" not specified\n"); $error = "yes"; } else { ensureSL(\$$cfg{logdir}); mkdirhier $$cfg{logdir} unless $opts->{check}; } if (! defined $$cfg{htmldir}) { warn ("WARNING: \"HtmlDir\" not specified\n"); $error = "yes"; } else { ensureSL(\$$cfg{htmldir}); mkdirhier $$cfg{htmldir} unless $opts->{check}; } if (! defined $$cfg{imagedir}) { warn ("WARNING: \"ImageDir\" not specified\n"); $error = "yes"; } else { ensureSL(\$$cfg{imagedir}); mkdirhier $$cfg{imagedir} unless $opts->{check}; } } # build relativ path from htmldir to image dir. my @htmldir = split /\Q${MRTG_lib::SL}\E+/, $$cfg{htmldir}; my @imagedir = split /\Q${MRTG_lib::SL}\E+/, $$cfg{imagedir}; while (scalar @htmldir > 0 and $htmldir[0] eq $imagedir[0]) { shift @htmldir; shift @imagedir; } # this is for the webpages so we use / path separator always $$cfg{imagehtml} = ""; foreach my $dir ( @htmldir ) { $$cfg{imagehtml} .= "../" if $dir; } map {$$cfg{imagehtml} .= "$_/" } @imagedir; # relative path is built debug('dir', "imagehtml = $$cfg{imagehtml}"); $SNMP_util::CacheFile = "$$cfg{'logdir'}oid-mib-cache.txt"; if (defined $$cfg{loadmibs}) { my($mibFile); foreach $mibFile (split /[,\s]+/, $$cfg{loadmibs}) { snmpQueue_MIB_File($mibFile); } } if(defined $$cfg{pathadd}){ ensureSL(\$$cfg{pathadd}); $ENV{PATH} = "$$cfg{pathadd}${MRTG_lib::PS}$ENV{PATH}"; } if(defined $$cfg{libadd}){ ensureSL(\$$cfg{libadd}); eval 'local $SIG{__DIE__};'."use lib qw($$cfg{libadd})"; my @match; foreach my $dir (@INC){ push @match, $dir if -f "$dir/RRDs.pm"; } warn "WARN: found several copies of RRDs.pm in your path: ". (join ", ", @match)." I will be using $match[0]. This could ". "be a problem if this is an old copy and you think I would be using a newer one!\n" if $#match > 0; } $$cfg{logformat} = 'rateup' unless defined $$cfg{logformat}; if($$cfg{logformat} eq 'rrdtool') { my ($name); if ($MRTG_lib::OS eq 'NT' or $MRTG_lib::OS eq 'OS2'){ $name = "rrdtool.exe"; } elsif ($MRTG_lib::OS eq 'NW'){ $name = "rrdtool.nlm"; } else { $name = "rrdtool"; } foreach my $path (split /\Q${MRTG_lib::PS}\E/, $ENV{PATH}) { ensureSL(\$path); -f "$path$name" && do { $$cfg{'rrdtool'} = "$path$name"; last;} }; die "ERROR: could not find $name. Use PathAdd: in mrtg.cfg to help mrtg find rrdtool\n" unless defined $$cfg{rrdtool}; debug ('rrd',"found rrdtool in $$cfg{rrdtool}"); my $found; foreach my $path (@INC) { ensureSL(\$path); -f "${path}RRDs.pm" && do { $found=1; last;} }; die "ERROR: could not find RRDs.pm. Use LibAdd: in mrtg.cfg to help mrtg find RRDs.pm\n" unless defined $found; } if (defined $$cfg{snmpoptions}) { $cfg->{snmpoptions} = eval('local $SIG{__DIE__};{'.$cfg->{snmpoptions}.'}'); } # default interval is 5 minutes $$cfg{interval} = 5 unless defined $$cfg{interval}; unless ($$cfg{logformat} eq 'rrdtool') { # interval has to be 5 minutes at least without userrdtool if ($$cfg{interval} < 5) { die "ERROR: CFG Error in \"Interval\": should be at least 5 Minutes (unless you use rrdtool)"; } } # Check for a Conversion Code file and evaluate its contents, which # should consist of one or more subroutine definitions. The code goes # into the MRTGConversion name space. if( exists $cfg->{ conversioncode } ) { open CONV, $cfg->{ conversioncode } or die "ERROR: Can't open file $cfg->{ conversioncode }\n"; my $code = "local $SIG{__DIE__};package MRTGConversion;\n". join( '', ) . "1;\n"; close CONV; die "ERROR: File $cfg->{ conversioncode } conversion code evaluation failed\n$@\n" unless eval $code; } foreach $rou (@$routers) { # and now for the testing if (! defined $rcfg->{snmpoptions}{$rou}) { $rcfg->{snmpoptions}{$rou} = {%{$cfg->{snmpoptions}}} if defined $cfg->{snmpoptions}; } else { $rcfg->{snmpoptions}{$rou} = eval('local $SIG{__DIE__};{'.$rcfg->{snmpoptions}{$rou}.'}'); } $rcfg->{snmpoptions}{$rou}{avoid_negative_request_ids} = 1; # $rcfg->{snmpoptions}{$rou}{domain} = 'udp'; if (! defined $$rcfg{"title"}{$rou}) { warn ("WARNING: \"Title[$rou]\" not specified\n"); $error = "yes"; } if (defined $$rcfg{'directory'}{$rou} and $$rcfg{'directory'}{$rou} ne "") { # They specified a directory for this router. Append the # pathname seperator to it (so that it can either be present or # absent, and the rules for including it are the same). ensureSL(\$$rcfg{'directory'}{$rou}); for my $x (qw(imagedir logdir htmldir)) { mkdirhier $$cfg{$x}.$$rcfg{directory}{$rou} unless $opts->{check}; } $$rcfg{'directory_web'}{$rou} = $$rcfg{'directory'}{$rou}; $$rcfg{'directory_web'}{$rou} =~ s/\Q${MRTG_lib::SL}\E+/\//g; debug('dir', "directory for $rou '$$rcfg{'directory_web'}{$rou}'"); } else { $$rcfg{'directory'}{$rou}=""; $$rcfg{'directory_web'}{$rou}=""; } if (defined $$rcfg{"pagetop"}{$rou}) { $$rcfg{"pagetop"}{$rou} =~ s/\\n/\n/g; } if (defined $$rcfg{"pagefoot"}{$rou}) { # allow for linebreaks $$rcfg{"pagefoot"}{$rou} =~ s/\\n/\n/g; } $$rcfg{"maxbytes1"}{$rou} = $$rcfg{"maxbytes"}{$rou} unless defined $$rcfg{"maxbytes1"}{$rou}; $$rcfg{"maxbytes2"}{$rou} = $$rcfg{"maxbytes"}{$rou} unless defined $$rcfg{"maxbytes2"}{$rou}; if ( not defined $$rcfg{"maxbytes"}{$rou} and not defined $$rcfg{"maxbytes1"}{$rou} and not defined $$rcfg{"maxbytes2"}{$rou}) { warn ("WARNING: \"MaxBytes[$rou]\" not specified\n"); $error = "yes"; } else { if (not defined $$rcfg{"maxbytes1"}{$rou}) { warn ("WARNING: \"MaxBytes1[$rou]\" not specified\n"); $error = "yes"; } if (not defined $$rcfg{"maxbytes2"}{$rou}) { warn ("WARNING: \"MaxBytes2[$rou]\" not specified\n"); $error = "yes"; } } # set default extension if (! defined $$rcfg{"extension"}{$rou}) { $$rcfg{"extension"}{$rou}="html"; } # set default size if (! defined $$rcfg{"xsize"}{$rou}) { $$rcfg{"xsize"}{$rou}=400; } if (! defined $$rcfg{"ysize"}{$rou}) { $$rcfg{"ysize"}{$rou}=100; } if (! defined $$rcfg{"ytics"}{$rou}) { $$rcfg{"ytics"}{$rou}=4; } if (! defined $$rcfg{"yticsfactor"}{$rou}) { $$rcfg{"yticsfactor"}{$rou}=1; } if (! defined $$rcfg{"factor"}{$rou}) { $$rcfg{"factor"}{$rou}=1; } if (defined $$rcfg{"options"}{$rou}) { my $opttemp = lc($$rcfg{"options"}{$rou}); delete $$rcfg{"options"}{$rou}; foreach $one_option (split /[,\s]+/, $opttemp) { if (grep {$one_option eq $_} @known_options) { $$rcfg{'options'}{$one_option}{$rou} = 1; } else { warn ("WARNING: Option[$rou]: \"$one_option\" is unknown\n"); $error="yes"; } } if ($rcfg->{'options'}{derive}{$rou} and not $cfg->{logformat} eq 'rrdtool'){ warn ("WARNING: Option[$rou]: \"derive\" works only with rrdtool logformat\n"); $error="yes"; } } # # Check out routeruptime definition # if (defined $$rcfg{"routeruptime"}{$rou}) { ($$rcfg{"community"}{$rou},$$rcfg{"router"}{$rou}) = split(/@/,$$rcfg{"routeruptime"}{$rou}); } # # Check out target definition # if (defined $$rcfg{"target"}{$rou}) { $$rcfg{targorig}{$rou} = $$rcfg{target}{$rou}; debug ('tarp',"Starting $rou -> $$rcfg{target}{$rou}"); # Decide whether to turn on IPv6 support for this target. # IPv6 support is turned on only if the EnableIPv6 global # setting is yes and the IPv4Only per-target setting is no. # If IPv6 is disabled, we set IPv4Only to true for all # targets, thus disabling all IPv6-related code. my $ipv4only = 1; if ($$cfg{enableipv6} and $$cfg{enableipv6} eq 'yes') { # IPv4Only is off by default $ipv4only = 0 unless (defined $$rcfg{ipv4only}{$rou}) && (lc($$rcfg{ipv4only}{$rou}) eq 'yes'); } ( $$rcfg{target}{$rou}, $$rcfg{uniqueTarget}{$rou} ) = targparser( $$rcfg{target}{$rou}, $target, $targIndex, $ipv4only, $rcfg->{snmpoptions}{$rou} ); } else { warn ("WARNING: I can't find a \"target[$rou]\" definition\n"); $error = "yes"; } # colors format: name#hexcol, if (defined $$rcfg{"colours"}{$rou}) { if ($$rcfg{'options'}{'dorelpercent'}{$rou}) { if ($$rcfg{"colours"}{$rou} =~ /^([^\#]+)(\#[0-9a-f]{6})\s*,\s* ([^\#]+)(\#[0-9a-f]{6})\s*,\s* ([^\#]+)(\#[0-9a-f]{6})\s*,\s* ([^\#]+)(\#[0-9a-f]{6})\s*,\s* ([^\#]+)(\#[0-9a-f]{6})/ix) { ($$rcfg{'col1'}{$rou}, $$rcfg{'rgb1'}{$rou}, $$rcfg{'col2'}{$rou}, $$rcfg{'rgb2'}{$rou}, $$rcfg{'col3'}{$rou}, $$rcfg{'rgb3'}{$rou}, $$rcfg{'col4'}{$rou}, $$rcfg{'rgb4'}{$rou}, $$rcfg{'col5'}{$rou}, $$rcfg{'rgb5'}{$rou}) = ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10); } else { warn ("WARNING: \"colours[$rou]\" for colour definition\n". " use the format: Name#hexcolour, Name#Hexcolour,...\n", " note, that dorelpercent requires 5 colours"); $error="yes"; } } else { if ($$rcfg{"colours"}{$rou} =~ /^([^\#]+)(\#[0-9a-f]{6})\s*,\s* ([^\#]+)(\#[0-9a-f]{6})\s*,\s* ([^\#]+)(\#[0-9a-f]{6})\s*,\s* ([^\#]+)(\#[0-9a-f]{6})/ix) { ($$rcfg{'col1'}{$rou}, $$rcfg{'rgb1'}{$rou}, $$rcfg{'col2'}{$rou}, $$rcfg{'rgb2'}{$rou}, $$rcfg{'col3'}{$rou}, $$rcfg{'rgb3'}{$rou}, $$rcfg{'col4'}{$rou}, $$rcfg{'rgb4'}{$rou}) = ($1, $2, $3, $4, $5, $6, $7, $8); } else { warn "WARNING: \"colours[$rou]\" for colour definition\n". " use the format: Name#hexcolour, Name#Hexcolour,...\n"; $error="yes"; } } } else { if (defined $$rcfg{'options'}{'dorelpercent'}{$rou}) { ($$rcfg{'col1'}{$rou}, $$rcfg{'rgb1'}{$rou}, $$rcfg{'col2'}{$rou}, $$rcfg{'rgb2'}{$rou}, $$rcfg{'col3'}{$rou}, $$rcfg{'rgb3'}{$rou}, $$rcfg{'col4'}{$rou}, $$rcfg{'rgb4'}{$rou}, $$rcfg{'col5'}{$rou}, $$rcfg{'rgb5'}{$rou}) = ("GREEN","#00cc00", "BLUE","#0000ff", "DARK GREEN","#006600", "MAGENTA","#ff00ff", "AMBER","#ef9f4f"); } else { ($$rcfg{'col1'}{$rou}, $$rcfg{'rgb1'}{$rou}, $$rcfg{'col2'}{$rou}, $$rcfg{'rgb2'}{$rou}, $$rcfg{'col3'}{$rou}, $$rcfg{'rgb3'}{$rou}, $$rcfg{'col4'}{$rou}, $$rcfg{'rgb4'}{$rou}) = ("GREEN","#00cc00", "BLUE","#0000ff", "DARK GREEN","#006600", "MAGENTA","#ff00ff"); } } # Background color, format: #rrggbb if (! defined $$rcfg{'background'}{$rou}) { $$rcfg{'background'}{$rou} = "#ffffff"; } if ($$rcfg{'background'}{$rou} =~ /^(\#[0-9a-f]{6})/i) { $$rcfg{'backgc'}{$rou} = "$1"; } else { warn "WARNING: \"background[$rou]: ". "$$rcfg{'background'}{$rou}\" for colour definition\n". " use the format: #rrggbb\n"; $error="yes"; } if (! defined $$rcfg{'kilo'}{$rou}) { $$rcfg{'kilo'}{$rou} = 1000; } if (defined $$rcfg{'kmg'}{$rou}) { $$rcfg{'kmg'}{$rou} =~ s/\s+//g; } if (! defined $$rcfg{'xzoom'}{$rou}) { $$rcfg{'xzoom'}{$rou} = 1.0; } if (! defined $$rcfg{'yzoom'}{$rou}) { $$rcfg{'yzoom'}{$rou} = 1.0; } if (! defined $$rcfg{'xscale'}{$rou}) { $$rcfg{'xscale'}{$rou} = 1.0; } if (! defined $$rcfg{'yscale'}{$rou}) { $$rcfg{'yscale'}{$rou} = 1.0; } if (! defined $$rcfg{'timestrpos'}{$rou}) { $$rcfg{'timestrpos'}{$rou} = 'NO'; } if (! defined $$rcfg{'timestrfmt'}{$rou}) { $$rcfg{'timestrfmt'}{$rou} = "%Y-%m-%d %H:%M"; } if ($error eq "yes") { die "ERROR: Please fix the error(s) in your config file\n"; } } } # make sure string ends with a slash. sub ensureSL($) { # return; my $ref = shift; return if $$ref eq ""; debug('dir',"ensure path IN: '$$ref'"); if (${MRTG_lib::SL} eq '\\'){ # two slashes at the start of the string are OK $$ref =~ s/(.)\Q${MRTG_lib::SL}\E+/$1${MRTG_lib::SL}/g; } else { $$ref =~ s/\Q${MRTG_lib::SL}\E+/${MRTG_lib::SL}/g; } $$ref =~ s/\Q${MRTG_lib::SL}\E*$/${MRTG_lib::SL}/; debug('dir',"ensure path OUT: '$$ref'"); } # convert current supplied time into a nice date string sub datestr ($) { my ($time) = shift || return 0; my ($wday) = ('Sunday','Monday','Tuesday','Wednesday', 'Thursday','Friday','Saturday')[(localtime($time))[6]]; my ($month) = ('January','February' ,'March' ,'April' , 'May' , 'June' , 'July' , 'August' , 'September' , 'October' , 'November' , 'December' )[(localtime($time))[4]]; my ($mday,$year,$hour,$min) = (localtime($time))[3,5,2,1]; if ($min<10) { $min = "0$min"; } return "$wday, $mday $month ".($year+1900)." at $hour:$min"; } # create expire date for expiery in ARG Minutes sub expistr ($) { my ($time) = time+$_[0]*60+5; my ($wday) = ('Sun','Mon','Tue','Wed','Thu','Fri','Sat')[(gmtime($time))[6]]; my ($month) = ('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep', 'Oct','Nov','Dec')[(gmtime($time))[4]]; my ($mday,$year,$hour,$min,$sec) = (gmtime($time))[3,5,2,1,0]; if ($mday<10) { $mday = "0$mday"; } ; if ($hour<10) { $hour = "0$hour"; } ; if ($min<10) { $min = "0$min"; } if ($sec<10) { $sec = "0$sec"; } return "$wday, $mday $month ".($year+1900)." $hour:$min:$sec GMT"; } sub create_pid ($) { my $pidfile = shift; return if ($OS eq 'NT' ); return if -e $pidfile; if ( open(PIDFILE,">$pidfile")) { close PIDFILE; } else { warn "cannot write to $pidfile: $!\n"; } } sub demonize_me ($) { my $pidfile = shift; my $cfgfile = shift; print "Daemonizing MRTG ...\n"; if ( $OS eq 'NT' ) { print "Do Not close this window. Or MRTG will die\n"; # require Win32::Console; # my $CONSOLE = new Win32::Console; # detach process from Console # $CONSOLE->Flush(); # $CONSOLE->Free(); # $CONSOLE->Alloc(); # $CONSOLE->Mode() } elsif( $OS eq 'OS2') { require OS2::Process; if (my_type() eq 'VIO'){ $main::Cleanfile3 = $pidfile; print "MRTG detached. PID=".system(P_DETACH,$^X." ".$0." ".$cfgfile); exit; } } else { # Check out if there is another mrtg running before forking if (defined $pidfile && open(READPID, "<$pidfile")){ if (not eof READPID) { chomp(my $input = ); # read process id in pidfile if ($input && kill 0 => $input) {# oops - the pid actually exists die "ERROR: I Quit! Another copy of mrtg seems to be running. Check $pidfile\n"; } } close READPID; } defined (my $pid = fork) or die "Can't fork: $!"; if ($pid) { exit; } else { if (defined $pidfile){ $main::Cleanfile3 = $pidfile; if (open(PIDFILE,">$pidfile")) { print PIDFILE "$$\n"; close PIDFILE; } else { warn "cannot write to $pidfile: $!\n"; } } require 'POSIX.pm'; POSIX::setsid() or die "Can't start a new session: $!"; open STDOUT,'>/dev/null' or die "ERROR: Redirecting STDOUT to /dev/null: $!"; open STDIN, '{ Methode } = 'SNMP'; $targ->{ Community } = $if->{ComStr}; $targ->{ Host } = ( defined $if->{HostIPv6} ) ? $if->{HostIPv6} : $if->{HostName}; $targ->{ SnmpOpt } = $if->{SnmpInfo}; $targ->{ snmpoptions} = $if->{snmpoptions}; $targ->{ Conversion } = ( defined $if->{ConvSub} ) ? $if->{ConvSub} : ''; for my $i( 0..1 ) { die 'ERROR: Malformed ', $i ? 'output ' : 'input ', "ifSpec in '$t'\n" if not defined $if->{OID}[$i] and not defined $if->{Alt}[$i]; $targ->{OID}[$i] = $if->{OID}[$i]; if( defined $if->{Alt}[$i] ) { if( defined $if->{Num}[$i] ) { $targ->{IfSel}[$i] = 'If'; $targ->{Key}[$i] = $if->{Num}[$i]; } elsif( defined $if->{IP}[$i] ) { $targ->{IfSel}[$i] = 'Ip'; $targ->{Key}[$i] = $if->{IP}[$i]; } elsif( defined $if->{Desc}[$i] ) { $targ->{IfSel}[$i] = 'Descr'; $targ->{Key}[$i] = $if->{Desc}[$i]; } elsif( defined $if->{Name}[$i] ) { $targ->{IfSel}[$i] = 'Name'; $targ->{Key}[$i] = $if->{Name}[$i]; } elsif( defined $if->{Eth}[$i] ) { $targ->{IfSel}[$i] = 'Eth'; $targ->{Key}[$i] = join( '-', map( { sprintf '%02x', hex $_ } split( /-/, $if->{Eth}[$i] ) ) ); } elsif( defined $if->{Type}[$i] ) { $targ->{IfSel}[$i] = 'Type'; $targ->{Key}[$i] = $if->{Type}[$i]; } else { die "ERROR: Internal error parsing ifSpec in '$t'\n"; } } else { $targ->{IfSel}[$i] = 'None'; $targ->{Key}[$i] = ''; } # Remove escaped characters and trailing space from Descr or Name Key $targ->{Key}[$i] =~ s/\\([\s:&@])/$1/g if $targ->{IfSel}[$i] eq 'Descr' or $targ->{IfSel}[$i] eq 'Name'; $targ->{Key}[$i] =~ s/[\0- ]+$//; } # Remove escaped characters from community $targ->{ Community } =~ s/\\([ @])/$1/g; return $targ; # Return new target closure } # ( $string, $unique ) = targparser( $string, $target, $targIndex, $ipv4only ) # Walk amd analyze the target string $string. $target is a reference to the # array of targets being built. $targIndex is a reference to a hash of targets # previously encountered indexed by target string. When $ipv4only is nonzero, # only IPv4 is in use. Returns the modifed target string and the index of the # @$target array to which the target refers if that index is unique. If the # index is not unique, i.e. the target definition is a calculation involving # two or more different targets, then the value -1 is returned for $unique. # Targparser updates the target array avoiding duplicate targets. The goal is # to substitute all target definitions with strings of the form # "$t1$thisTarg$t2", where $thisTarg is the target index, and $t1 and $t2 are # as defined below. The intended result is a target string that can be eval'ed # in its entirety later on when monitoring data has been collected. This # evaluation occurs in sub getcurrent in the main mrtg script. # Note: In the regular expressions in &targparser, we have avoided m/.../i # and the variables &`, $&, and $'. Use of these makes regex processing less # efficient. See Friedl, J.E.F. Mastering Regular Expressions. O'Reilly. # p. 273 sub targparser( $$$$$ ) { # Target string (int:community@router, etc.) my $string = shift; # Reference to target array my $target = shift; # Reference to target index hash my $targIndex = shift; # Nonzero if only IPv4 is in use my $ipv4only = shift; # options passed per target. my $snmpoptions = shift; # Next available index in the @$target array my $idx = @$target; # Common match strings: pre-target, target, post-target my( $pre, $t, $post ); # Portion of string already parsed my $parsed = ''; # Initialize $unique to undefined. It will take on the $targIndex value # of the first target encountered. $otherTargCount will count the # number of other targets (targets with different values of $targIndex) # encountered during the parse. $unique will be returned as undef # unless $otherTargCount remains 0. my $unique = -1; my $otherTargCount = 0; # Components of the target expression that are substituted into the # target string each time a target is identified. The substitution # string is the interpolated value of "$t1$targIndex$t2". At present # $t1 and $t2 are set to create a new BigFloat object. # my $t1 = ' Math::BigFloat->new($target->['; # my $t2 = ']{$mode}) '; # this gives problems with perl 5.005 so bigfloat is introduces in mrtg itself my $t1 = ' $target->['; my $t2 = ']{$mode} '; # Find and substitute all external program targets while( ( $pre, $t, $post ) = $string =~ m< ^(.*?) # capture pre-target string ` # beginning of program target ((?:\\`|[^`])+) # capture target contents (\` allowed) ` # end of program target (.*)$ # capture post-target string >x ) { # Total of 3 captures my $thisTarg; if( exists $targIndex->{ $t } ) { # This program target has been encountered previously $thisTarg = $targIndex->{ $t }; debug( 'tarp', "Existing program target [$thisTarg]" ); } else { # A new program target is needed my $targ = { }; $targ->{ Methode } = 'EXEC'; $targ->{ Command } = $t; # Remove escaped backticks $targ->{ Command } =~ s/\\\`/\`/g; $target->[ $idx ] = $targ; $thisTarg = $idx++; $targIndex->{ $t } = $thisTarg; debug( 'tarp', "New program target [$thisTarg] '$t'" ); } $parsed .= "$pre$t1$thisTarg$t2"; $string = $post; if( $unique < 0 ) { $unique = $thisTarg; } else { $otherTargCount++ unless $thisTarg == $unique; } }; # Reset $string for new target type search $string = $parsed . $string; $parsed = ''; debug( 'tarp', "&targparser external done: '$string'" ); # Common interface specification regex components # Simple interface specification regex component. Matches interface # specification by IPv4 address, description, name, Ethernet address, or # type. my $ifSimple = ' (\d+)|' . # by number ($if->{Num}) ' / (\d+(?:\.\d+)+)|' . # by IPv4 address ($if->{IP}) ' \\\\ ((?:\\\\[\s:&@]|[^\s:&@])+)|' . # by description (allow \ \: \& \@) ($if->{Desc}) ' \# ((?:\\\\[\s:&@]|[^\s:&@])+)|' . # by name (allow \ \: \& \@) ($if->{Name}) ' ! ([a-fA-F0-9]+(?:-[a-fA-F0-9]+)+)|' . # by Ethernet address ($if->{Eth}) ' % (\d+)'; # by type ($if->{Type}) # Complex interface specification regex component. Note that a null string # will match. Therefore the match must be postprocessed to check that # $ifOID and $ifAlt are not both null. my $ifComplex = '([-a-zA-Z0-9]*(?:\.\d+)*?)' . # OID possibly starting with a MIB name ($if->{OID}) '(' . # Interface specification alternatives: ($if->{Alt}) '\.' . # separator $ifSimple . # simple alternatives (6 variables) ')?'; # maybe none of the above # Community-host interface specification regex component. my $ifComHost = '((?:\\\\[@ ]|[^\s@])+)' . # community string ('\@' and '\ ' allowed) ($if->{ComStr}) '@' . # separator '(?:(\[[a-fA-F0-9:]*\])|' . # hostname as IPv6 address ($if->{HostIPv6}) '([-\w]+(?:\.[-\w]+)*))' . # or DNS name ($if->{HostName}) '((?::[\d.!]*)*)' . # SNMP session configuration ($if->{SnmpInfo}) '(?:\|([a-zA-Z_][\w]*))?'; # numeric conversion subroutine ($if->{ConvSub}) # Match strings for simple and complex interface specifications. Entries # are of the form $if->{k1}[i], where k1 is OID, Alt, Num, IP, Desc, # Name, Eth, or Type, and i is 0 or 1 (input or output). Entries may also # have the form $if->{k1}, where k1 is Rev, ComStr, HostIPv6, HostName, # SnmpInfo, or ConvSub, with no [i] in these cases. my $if; # Find and substitute all complex OID targets while( ( $pre, $t, $if->{OID}[0], $if->{Alt}[0], $if->{Num}[0], $if->{IP}[0], $if->{Desc}[0], $if->{Name}[0], $if->{Eth}[0], $if->{Type}[0], $if->{OID}[1], $if->{Alt}[1], $if->{Num}[1], $if->{IP}[1], $if->{Desc}[1], $if->{Name}[1], $if->{Eth}[1], $if->{Type}[1], $if->{ComStr}, $if->{HostIPv6}, $if->{HostName}, $if->{SnmpInfo}, $if->{ConvSub}, $post ) = $string =~ m< ^(.*?) # capture pre-target string ( # capture entire target ${ifComplex} # input interface specification (8 captures) & # separator ${ifComplex} # output interface specification (8 captures) : # separator ${ifComHost} # community-host specification (5 captures) ) # end of entire target capture (.*)$ # capture post-target string >x ) { # Total of 24 captures my $thisTarg; # Exception: skip and try to parse later as a simple target if # $if->{Desc}[0], $if->{Name}[0], $if->{Desc}[1], or $if->{Name}[1] # ends with a backslash character if( ( defined $if->{Desc}[0] and $if->{Desc}[0] =~ m<\\$> ) or ( defined $if->{Name}[0] and $if->{Name}[0] =~ m<\\$> ) or ( defined $if->{Desc}[1] and $if->{Desc}[1] =~ m<\\$> ) or ( defined $if->{Name}[1] and $if->{Name}[1] =~ m<\\$> ) ) { $parsed .= "$pre$t"; $string = $post; next; } if( exists $targIndex->{ $t } ) { # This complex target has been encountered previously $thisTarg = $targIndex->{ $t }; debug( 'tarp', "Existing complex target [$thisTarg]" ); } else { # A new complex target is needed my $targ = newSnmpTarg( $t, $if ); $targ->{ ipv4only } = $ipv4only; $targ->{ snmpoptions } = $snmpoptions; $target->[ $idx ] = $targ; $thisTarg = $idx++; $targIndex->{ $t } = $thisTarg; debug( 'tarp', "New complex target [$thisTarg] '$t':\n" . " Comu: $targ->{Community}, Host: $targ->{Host}\n" . " Opt: $targ->{SnmpOpt}, IPv4: $targ->{ipv4only}\n" . " Conv: $targ->{Conversion}\n" . " OID: $targ->{OID}[0], $targ->{OID}[1]\n" . " IfSel: $targ->{IfSel}[0], $targ->{IfSel}[1]\n" . " Key: $targ->{Key}[0], $targ->{Key}[1]" ); } $parsed .= "$pre$t1$thisTarg$t2"; $string = $post; if( $unique < 0 ) { $unique = $thisTarg; } else { $otherTargCount++ unless $thisTarg == $unique; } } # Reset $string and $parsedfor new target type search $string = $parsed . $string; $parsed = ''; debug( 'tarp', "&targparser complex done: '$string'" ); # Find and substitute all simple targets while( ( $pre, $t, $if->{Rev}, $if->{Num}[0], $if->{IP}[0], $if->{Desc}[0], $if->{Name}[0], $if->{Eth}[0], $if->{Type}[0], $if->{ComStr}, $if->{HostIPv6}, $if->{HostName}, $if->{SnmpInfo}, $if->{ConvSub}, $post ) = $string =~ m< ^(.*?) # capture pre-target string ( # capture entire target (-)? # capture direction reversal (?: ${ifSimple} ) # simple interface specification (6 captures) : # separator ${ifComHost} # community-host specification (5 captures) ) # end of entire target capture (.*)$ # capture post-target string >x ) { # Total of 15 captures my $thisTarg; if( exists $targIndex->{ $t } ) { # This simple target has been encountered previously $thisTarg = $targIndex->{ $t }; debug( 'tarp', "Existing simple target [$thisTarg]" ); } else { # A new simple target is needed # Reverse interface directions if indicated by $if->{Rev}. # The sense of $d1 and $d2 is 0 for input and 1 for output my $d1 = ( defined $if->{Rev} and $if->{Rev} eq '-' ) ? 1 : 0; my $d2 = 1 - $d1; # Set the OIDs depending on whether SNMPv2 has been specified # and on the direction if( $if->{SnmpInfo} =~ m/(?::[^:]*){4}:[32][Cc]?/ ) { $if->{OID}[$d1] = 'ifHCInOctets'; $if->{OID}[$d2] = 'ifHCOutOctets'; } else { $if->{OID}[$d1] = 'ifInOctets'; $if->{OID}[$d2] = 'ifOutOctets'; } # Give $if->{Alt}[i] an arbitrary defined value so that # &newSnmpTarg works correctly $if->{Alt}[0] = 1; $if->{Alt}[1] = 1; # Copy input specification to output $if->{Num}[1] = $if->{Num}[0]; $if->{IP}[1] = $if->{IP}[0]; $if->{Desc}[1] = $if->{Desc}[0]; $if->{Name}[1] = $if->{Name}[0]; $if->{Eth}[1] = $if->{Eth}[0]; $if->{Type}[1] = $if->{Type}[0]; my $targ = newSnmpTarg( $t, $if ); $targ->{ snmpoptions} = $snmpoptions; $targ->{ ipv4only } = $ipv4only; $target->[ $idx ] = $targ; $thisTarg = $idx++; $targIndex->{ $t } = $thisTarg; debug( 'tarp', "New simple target [$thisTarg] '$t':\n" . " Comu: $targ->{Community}, Host: $targ->{Host}\n" . " Opt: $targ->{SnmpOpt}, IPv4: $targ->{ipv4only}\n" . " Conv: $targ->{Conversion}\n" . " OID: $targ->{OID}[0], $targ->{OID}[1]\n" . " IfSel: $targ->{IfSel}[0], $targ->{IfSel}[1]\n" . " Key: $targ->{Key}[0], $targ->{Key}[1]" ); } $parsed .= "$pre$t1$thisTarg$t2"; $string = $post; if( $unique < 0 ) { $unique = $thisTarg; } else { $otherTargCount++ unless $thisTarg == $unique; } } # Assemble string to be returned $string = $parsed . $string; # Set $unique undefined if more than one target is referred to in the # target string $unique = -1 if $otherTargCount; debug( 'tarp', "&targparser simple done: '$string'" ); debug( 'tarp', "&targparser returning: unique = $unique" ); return ( $string, $unique ); } # Display of &targparser intermediate values for debugging purposes. Call as # showMatch( $string, $pre, $t, $post, $if ) from within &targparser. sub showMatch( $$$$$ ) { my( $string, $pre, $t, $post, $if ) = @_; warn "# Matching on string '$string'\n"; warn "# Prematch: '$pre'\n"; warn "# Target: '$t'\n"; warn "# Postmatch: '$post'\n"; warn "# Captured:\n"; foreach my $k( keys %$if ) { if( ref( $if->{$k} ) eq 'ARRAY' ) { warn "# \$if->{$k}[0,1]: '", ( defined $if->{$k}[0] ) ? $if->{$k}[0] : 'undef', "', '", ( defined $if->{$k}[1] ) ? $if->{$k}[1] : 'undef', "'\n"; } else { warn "# \$if->{$k}: '", ( defined $if->{$k} ) ? $if->{$k} : 'undef', "'\n"; } } } sub readconfcache ($) { my $cfgfile = shift; my %confcache; if (open (CFGOK,"<$cfgfile")) { while () { chomp; next unless /\t/; #ignore odd lines next if /^\S+:/; #ignore legacy lines my ($host,$method,$key,$if) = split (/\t/, $_); $key =~ s/[\0- ]+$//; # no trailing whitespace in keys realy ! $key =~ s/[\0- ]/ /g; # all else becomes a normal space ... get a life $confcache{$host}{$method}{$key} = $if; } close CFGOK; } return \%confcache; } sub writeconfcache ($$) { my $confcache = shift; my $cfgfile = shift; if ($cfgfile ne '&STDOUT'){ open (CFGOK,">$cfgfile") or die "ERROR: writing $cfgfile.ok: $!"; } my @hosts; if (defined $$confcache{___updated}) { @hosts = @{$$confcache{___updated}} ; delete $$confcache{___updated}; } else { @hosts = grep !/^___/, keys %{$confcache} } foreach my $host (sort @hosts) { foreach my $method (sort keys %{$$confcache{$host}}) { foreach my $key (sort keys %{$$confcache{$host}{$method}}) { if ($cfgfile ne '&STDOUT'){ print CFGOK "$host\t$method\t$key\t". $$confcache{$host}{$method}{$key},"\n"; } else { print "$host\t$method\t$key\t". $$confcache{$host}{$method}{$key},"\n"; } } } } close CFGOK; } sub cleanhostkey ($){ my $host = shift; return undef unless defined $host; $host =~ s/(:\d*)(?:(:\d*)(?:(:\d*)(?:(:\d*)(?:(:\d*)))))$/$1$5/ or $host =~ s/(:\d*)(?:(:\d*)(?:(:\d*)(?:(:\d*)?)?)?)$/$1/; $host =~ s/:/_/g; # make sure that double invocations do not kill us return $host; } sub storeincache ($$$$$){ my($confcache,$host,$method,$key,$value) = @_; $host = cleanhostkey $host; if (not defined $value ){ $$confcache{$host}{$method}{$key} = undef; return; } $value =~ s/[\0- ]/ /g; # all else becomes a normal space ... get a life $value =~ s/ +$//; # no trailing spaces if (defined $$confcache{$host}{$method}{$key} and $$confcache{$host}{$method}{$key} ne $value) { $$confcache{$host}{$method}{$key} = "Dup"; debug('coca',"store in confcache $host $method $key --> $value (duplicate)"); } else { $$confcache{$host}{$method}{$key} = $value; debug('coca',"store in confcache $host $method $key --> $value"); } } sub readfromcache ($$$$){ my($confcache,$host,$method,$key) = @_; $host = cleanhostkey $host; return $$confcache{$host}{$method}{$key}; } sub clearfromcache ($$){ my($confcache,$host) = @_; $host = cleanhostkey $host; delete $$confcache{$host}; debug('coca',"clear confcache $host"); } sub populateconfcache ($$$$$) { my $confcache = shift; my $host = shift; my $ipv4only = shift; my $reread = shift; my $snmpoptions = shift || {}; my $hostkey = cleanhostkey $host; return if defined $$confcache{$hostkey} and not $reread; my $snmp_errlevel = $SNMP_Session::suppress_warnings; $SNMP_Session::suppress_warnings = 3; debug('coca',"populate confcache $host"); # clear confcache for host; delete $$confcache{$hostkey}; my @ret; my %tables = ( ifDescr => 'Descr', ifName => 'Name', ifType => 'Type', ipAdEntIfIndex => 'Ip' ); my @nodes = qw (ifName ifDescr ifType ipAdEntIfIndex); # it seems that some devices only give back sensible data if their tables # are walked in the right ordere .... foreach my $node (@nodes) { next if $confcache->{___deadhosts}{$hostkey} and time - $confcache->{___deadhosts}{$hostkey} < 300; $SNMP_Session::errmsg = undef; @ret = snmpwalk(v4onlyifnecessary($host, $ipv4only), $snmpoptions, $node); unless ( $SNMP_Session::errmsg){ foreach my $ret (@ret) { my ($oid, $desc) = split(':', $ret, 2); if ($tables{$node} eq 'Ip') { storeincache($confcache,$host,$tables{$node},$oid,$desc); } else { $desc =~ s/[\0- ]+$//; #trailing whitespace is too sick for us $desc =~ s/[\0- ]/ /g; #whitespace is just whitespace storeincache($confcache,$host,$tables{$node},$desc,$oid); } }; } else { $confcache->{___deadhosts}{$hostkey} = time if $SNMP_Session::errmsg =~ /no response received/; debug('coca',"Skipping $node scanning because $host does not seem to support it"); } } if ($confcache->{___deadhosts}{$hostkey} and time - $confcache->{___deadhosts}{$hostkey} < 300){ $SNMP_Session::suppress_warnings = $snmp_errlevel; return; } $SNMP_Session::errmsg = undef; @ret = snmpwalk(v4onlyifnecessary($host, $ipv4only), $snmpoptions, "ifPhysAddress"); unless ( $SNMP_Session::errmsg){ foreach my $ret (@ret) { my ($oid, $bin) = split(':', $ret, 2); my $eth = unpack 'H*', $bin; my @eth; while ($eth =~ s/^..//){ push @eth, $&; } my $phys=join '-', @eth; storeincache($confcache,$host,"Eth",$phys,$oid); } } else { debug('coca',"Skipping ifPhysAddress scanning because $host does not seem to support it"); } if (ref $$confcache{___updated} ne 'ARRAY') { $$confcache{___updated} = []; #init to empty array } push @{$$confcache{___updated}}, $hostkey; $SNMP_Session::suppress_warnings = $snmp_errlevel; } sub log2rrd ($$$) { my $router = shift; my $cfg = shift; my $rcfg = shift; my %mark; my %incomp; my %elapsed_time; my %rate; my %store; my %first_step; my %cur; my %next; my $rrd; my @steps = qw(300 1800 7200 86400); my %sizes = ( 300 => 600, 1800 => 700, 7200 => 775, 86400 => 797); open R, "<$$cfg{logdir}$$rcfg{'directory'}{$router}$router.log" or die "ERROR: opening $$cfg{logdir}$$rcfg{'directory'}{$router}$router.log: $!"; debug('rrd',"converting $$cfg{logdir}$$rcfg{'directory'}{$router}$router.log"); my $latest_timestamp; my %latest_counter; chomp($_ = ); my $time; my $next_time; ($latest_timestamp,$latest_counter{in},$latest_counter{out}) = split /\s+/; chomp($_ = ); ($time,$cur{in},$cur{out},$cur{maxin},$cur{maxout}) = split /\s+/; foreach my $s (@steps) { $mark{$s} = $latest_timestamp - ($latest_timestamp % $s) + $s; $first_step{$s} = $latest_timestamp - ($mark{$s} - $s); $elapsed_time{$s} = $s - $first_step{$s}; $rate{in}{$s}=$cur{in}; $rate{out}{$s}=$cur{out}; $rate{maxin}{$s}=$cur{maxin}; $rate{maxout}{$s}=$cur{maxout}; } while(){ chomp; ($next_time,$next{in},$next{out},$next{maxin},$next{maxout}) = split /\s+/; foreach my $s (@steps) { # bail if we have enough entries next if ref $store{in}{$s} and scalar @{$store{in}{$s}} > $sizes{$s}; # ok we are still here. If next mark is before the next time # we take a short step, else we gobble up my $next_stop; do { if ($elapsed_time{$s} + $time - $next_time > $s) { $next_stop = $mark{$s}-$s; } else { $next_stop = $next_time; } my $time_diff = $time-$next_stop; foreach my $d (qw(in out)) { $rate{$d}{$s} = ($rate{$d}{$s} * $elapsed_time{$s} + $cur{$d} * $time_diff) / ($elapsed_time{$s} + $time_diff); } foreach my $d (qw(maxin maxout)){ $rate{$d}{$s} = $cur{$d} if $rate{$d}{$s} < $cur{$d}; } $elapsed_time{$s} += $time_diff; # print "$time $next_stop\n" if $s == 300; if ($next_stop == $mark{$s}-$s) { foreach my $t (qw(in out maxin maxout)){ $rate{$t}{$s}/=3600 if (defined $$rcfg{'options'}{'perhour'}{$router}); $rate{$t}{$s}/=60 if (defined $$rcfg{'options'}{'perminute'}{$router}); push @{$store{$t}{$s}}, int($rate{$t}{$s}); } $mark{$s} -= $s; $rate{maxin}{$s} = 0; $rate{maxout}{$s} = 0; $elapsed_time{$s} = 0; } } while ($next_stop > $next_time ); } ($time,$cur{in},$cur{out},$cur{maxin},$cur{maxout}) = ($next_time,$next{in},$next{out},$next{maxin},$next{maxout}); } close R; # lets see if we have rrdtool 1.2 at our hands my $VERSION = '0001'; if ($RRDs::VERSION >= 1.2){ $VERSION = '0003'; } my $DST; my $pdprepin = (shift @{$store{in}{300}})*($first_step{300}); my $pdprepout = (shift @{$store{out}{300}})*($first_step{300}); if (defined $$rcfg{'options'}{'absolute'}{$router}) { $DST = 'ABSOLUTE' } elsif (defined $$rcfg{'options'}{'gauge'}{$router}) { $DST = 'GAUGE' } else { $DST = 'COUNTER' } my $MHB = $$cfg{interval} * 60 * 2; my $MAX1 = $$rcfg{'absmax'}{$router} || $$rcfg{'maxbytes1'}{$router} || 'U'; my $MAX2 = $$rcfg{'absmax'}{$router} || $$rcfg{'maxbytes2'}{$router} || 'U'; $rrd = < $VERSION 300 $latest_timestamp ds0 $DST $MHB 0 $MAX1 $latest_counter{in} $pdprepin 0 ds1 $DST $MHB 0 $MAX2 $latest_counter{out} $pdprepout 0 RRD $first_step{300} = 0; # invalidate addarch(1,'AVERAGE','in','out',\%store,\%first_step,\$rrd); addarch(6,'AVERAGE','in','out',\%store,\%first_step,\$rrd); addarch(24,'AVERAGE','in','out',\%store,\%first_step,\$rrd); addarch(288,'AVERAGE','in','out',\%store,\%first_step,\$rrd); addarch(1,'MAX','maxin','maxout',\%store,\%first_step,\$rrd); addarch(6,'MAX','maxin','maxout',\%store,\%first_step,\$rrd); addarch(24,'MAX','maxin','maxout',\%store,\%first_step,\$rrd); addarch(288,'MAX','maxin','maxout',\%store,\%first_step,\$rrd); $rrd .= < RRD if ( $OS eq 'NT' or $OS eq 'OS2') { open (R, "|$$cfg{rrdtool} restore - $$cfg{logdir}$$rcfg{'directory'}{$router}$router.rrd"); } else { open (R, "|-") or exec "$$cfg{rrdtool}","restore","-","$$cfg{logdir}$$rcfg{'directory'}{$router}$router.rrd"; } print R $rrd; close R; } sub addarch($$$$$$$){ my $steps = shift; my $cons = shift; my $in = shift; my $out = shift; my $store = shift; my $first_step = shift; my $rrd = shift; my $cdpin = 'NaN'; my $cdpout = 'NaN'; my $param_start = ''; my $param_end = ''; my $extra_ds = ''; if ($RRDs::VERSION >= 1.2){ $param_start = ''; $param_end = ''; $extra_ds = ' 0.0000000000e+00 0.0000000000e+00 '; } if ($steps != 300) { $cdpin = shift @{$$store{$in}{300*$steps}}; $cdpout = shift @{$$store{$out}{300*$steps}}; }; $$rrd .= < $cons $steps $param_start 0.5 $param_end $extra_ds $cdpin 0 $extra_ds $cdpout 0 RRD while (@{$$store{$in}{$steps*300}}){ # we take zero as UNKNOWN my $inr = pop @{$$store{$in}{$steps*300}} || 'NaN'; my $outr = pop @{$$store{$out}{$steps*300}} || 'NaN'; $$rrd .= < $inr $outr RRD } $$rrd .= < RRD } # debug if the relevant debug tag is active print the debug message sub debug ($$) { return unless scalar @main::DEBUG; my $tag = shift; my $msg = shift; return unless grep {$_ eq $tag} @main::DEBUG; warn "--",$tag,": ",$msg,"\n"; return; } # timestamp sub timestamp () { my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time); $year += 1900; $mon += 1; return sprintf "%4d-%02d-%02d %02d:%02d:%02d", $year,$mon,$mday,$hour,$min,$sec; } # configure __DIE__ and __WARN__ sub setup_loghandlers ($){ $::global_logfile = $_[0]; for($_[0]){ /^eventlog$/i && do { require Win32::EventLog; $SIG{__WARN__} = sub { my $EventLog = Win32::EventLog->new('MRTG'); my $Type = ($_[0] =~ /warning/) ? &Win32::EventLog::EVENTLOG_WARNING_TYPE : &Win32::EventLog::EVENTLOG_INFORMATION_TYPE; my $Msg = $_[0]; $Msg =~ s/\n/\r\n/g; $Msg =~ s/[\n\r]$//g; $EventLog->Report({ EventID => 1000, Category => "WARN", EventType => $Type, Data => '', Strings => $Msg }); $EventLog->Close; }; $SIG{__DIE__} = sub { my $EventLog = Win32::EventLog->new('MRTG'); my $Msg = $_[0]; $Msg =~ s/\n/\r\n/g; $Msg =~ s/[\n\r]$//g; $EventLog->Report({ EventID => 1000, Category => "ERROR", EventType => &Win32::EventLog::EVENTLOG_ERROR_TYPE, Data => '', Strings => $Msg }); $EventLog->Close; exit 1; }; last; }; $SIG{__WARN__} = sub { if (open DEB, ">>$::global_logfile") { print DEB timestamp." -- $_[0]"; close DEB; } else { print STDERR timestamp." -- $_[0]" } }; $SIG{__DIE__} = sub { if ( open DEB, ">>$::global_logfile") { print DEB timestamp." -- $_[0]"; close DEB; } else { print STDERR timestamp." -- $_[0]" } exit 1 }; } } # Adds the v4only attribute to a target if the caller requests it. # (this includes targets specified using numeric IPv6 addresses...) sub v4onlyifnecessary ($$) { my $target = shift; my $add = shift; my ($v6addr, $temptarget); if($add) { # Catch numeric IPv6 addresses if ( $target =~ /(\[[\w:]*\])(.*)/) { ($v6addr, $temptarget) = ($1,$2); } else { $temptarget = $target; } return $target.(":" x (5 - ($temptarget =~ tr/://))).":v4only"; } else { return $target; } } __END__ =pod =head1 NAME MRTG_lib.pm - Library for MRTG and support scripts =head1 SYNOPSIS use MRTG_lib; my ($configfile, @target_names, %globalcfg, %targetcfg); readcfg($configfile, \@target_names, \%globalcfg, \%targetcfg); my (@parsed_targets); cfgcheck(\@target_names, \%globalcfg, \%targetcfg, \@parsed_targets); =head1 DESCRIPTION MRTG_lib is part of MRTG, the Multi Router Traffic Grapher. It was separated from MRTG to allow other programs to easily use the same config files. The main part of MRTG_lib is the config file parser but some other funcions are there too. =over 4 =item C<$MRTG_lib::OS> Type of OS: WIN, UNIX, VMS =item C<$MRTG_lib::SL> I in the current OS. =item C<$MRTG_lib::PS> Path separator in PATH variable =item C C Reads a config file, parses it and fills some arrays and hashes. The mandatory arguments are: the name of the config file, a ref to an array which will be filled with a list of the target names, a hashref for the global configuration, a hashref for the target configuration. The configuration file syntax is: globaloption: value targetoption[targetname]: value aprefix*extglobal: value aprefix*exttarget[target2]: value E.g. workdir: /var/stat/mrtg target[router1]: 2:public@router1.local.net 14all*columns: 2 The global config hash has the structure $globalcfg{configoption} = 'value' The target config hash has the structure $targetcfg{configoption}{targetname} = 'value' See L for more information about the MRTG configuration syntax. C can take two additional arguments to extend the config file syntax. This allows programs to put their configuration into the mrtg config file. The fifth argument is the prefix of the extension, the sixth argument is a hash with the checkrules for these extension settings. E.g. if the prefix is "14all" C will check config lines that begin with "14all*", i.e. all lines like 14all*columns: 2 14all*graphsize[target3]: 500 200 against the rules in %extrules. The format of this hash is: $extrules{option} = [sub{$_[0] =~ m/^\d+$/}, sub{"Error message for $_[0]"}] i.e. $extrules{option}[0] -> a test expression $extrules{option}[1] -> error message if test fails The first part of the array is a perl expression to test the value of the option. The test can access this value in the variable "$arg". The second part of the array is an error message to display when the test fails. The failed value can be integrated by using the variable "$arg". Config settings with an different prefix than the one given in the C call are not checked but inserted into I<%globalcfg> and I<%targetcfg>. Prefixed settings keep their prefix in the config hashes: $targetcfg{'14all*graphsize'}{'target3'} = '500 200' =item C C Checks the configuration read by C. Checks the values in the config for syntactical and/or semantical errors. Sets defaults for some options. Parses the "target[...]" options and filles the array @parsed_targets ready for mrtg functions. The first three arguments are the same as for C. The fourth argument is an arrayref which will be filled with the parsed target defs. C converts the values of target settings I, e.g. options[router1]: bits, growright to a hash: $targetcfg{'option'}{'bits'}{'router1'} = 1 $targetcfg{'option'}{'growright'}{'router1'} = 1 This is not done by C so if you don't use C you have to check the scalar variable I<$targetcfg{'option'}{'router1'}> (MRTG allows options to be separated by space or ','). =item C C Checks that the I does not contain double path separators and ends with a path separator. It uses $MRTG_lib::SL as path separator which will be / or \ depending on the OS. =item C C Convert log file to rrd format. Needs rrdtool. =item C C Returns the time given in the argument as a nicely formated date string. The argument has to be in UNIX time format (seconds since 1970-1-1). =item C C Return a string representing the current time. =item C C Install signalhandlers for __DIE__ and __WARN__ making the errors go the the specified destination. If filename is 'eventlog' mrtg will log to the windows event logger. =item C C Returns the time given in the argument formatted suitable for HTTP Expire-Headers. =item C C Creates a pid file for the mrtg daemon =item C C Puts the running program into background, detaching it from the terminal. =item C C Reads the SNMP variables I, I, I, I from the I and stores the values in I<%confcache> as follows: $confcache{$host}{'Descr'}{ifDescr}{oid} = (ifDescr or 'Dup') $confcache{$host}{'IP'}{ipAdEntIfIndex}{oid} = (ipAdEntIfIndex or 'Dup') $confcache{$host}{'Eth'}{ifPhysAddress}{oid} = (ifPhysAddress or 'Dup') $confcache{$host}{'Name'}{ifName}{oid} = (ifName or 'Dup') $confcache{$host}{'Type'}{ifType}{oid} = (ifType or 'Dup') The value (at the right side of =) is 'Dup' if a value was retrieved muliple times, the retrieved value else. =item C C Preload the confcache from a file. =item C C Store the current confcache into a file. =item C C Store the current confcache into a file. =item C C =item C C =item C C =item C C Prints the I on STDERR if debugging is enabled for type I. A debug type is enabled if I is in array @main::DEBUG. =back =head1 AUTHORS Rainer Bawidamann ERainer.Bawidamann@rz.uni-ulm.deE (This Manpage) =cut