# Copyright (C) 2002 Stanislav Sinyagin # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. # $Id: Validator.pm,v 1.1 2010-12-27 00:03:45 ivan Exp $ # Stanislav Sinyagin package Torrus::ConfigTree::Validator; use Torrus::ConfigTree; use Torrus::Log; use Torrus::RPN; use Torrus::SiteConfig; use strict; Torrus::SiteConfig::loadStyling(); %Torrus::ConfigTree::Validator::reportedErrors = (); my %rrd_params = ( 'leaf-type' => {'rrd-def' => {'rrd-ds' => undef, 'rrd-cf' => {'AVERAGE' => undef, 'MIN' => undef, 'MAX' => undef, 'LAST' => undef}, 'data-file' => undef, 'data-dir' => undef}, 'rrd-cdef' => {'rpn-expr' => undef}}, ); my %rrdmulti_params = ( 'ds-names' => undef ); # Plugins might need to add a new storage type our %collector_params = ( 'collector-type' => undef, '@storage-type' => { 'rrd' => { 'data-file' => undef, 'data-dir' => undef, 'leaf-type' => { 'rrd-def' => {'rrd-ds' => undef, 'rrd-cf' => {'AVERAGE' => undef, 'MIN' => undef, 'MAX' => undef, 'LAST' => undef}, 'rrd-create-dstype' => {'GAUGE' => undef, 'COUNTER' => undef, 'DERIVE' => undef, 'ABSOLUTE' => undef }, 'rrd-create-rra' => undef, 'rrd-create-heartbeat' => undef, '+rrd-hwpredict' => { 'enabled' => { 'rrd-create-hw-rralen' => undef}, 'disabled' => undef, }}}}, 'ext' => { 'ext-dstype' => { 'GAUGE' => undef, 'COUNTER32' => undef, 'COUNTER64' => undef }, 'ext-service-id' => undef, '+ext-service-units' => { 'bytes' => undef }}}, 'collector-period' => undef, 'collector-timeoffset' => undef, '+collector-scale' => undef, '+collector-dispersed-timeoffset' => { 'no' => undef, 'yes' => undef } # collector-timeoffset-min, max, step, and hashstring are validated # during post-processing ); # Plugins might in theory create new datasource types our %leaf_params = ('ds-type' => {'rrd-file' => \%rrd_params, 'rrd-multigraph' => \%rrdmulti_params, 'collector' => \%collector_params}, 'rrgraph-views' => undef, '+rrd-scaling-base' => {'1000' => undef, '1024' => undef}, '+graph-logarithmic' => {'yes' => undef, 'no' => undef}, '+graph-rigid-boundaries' => {'yes' => undef, 'no' => undef}, '+graph-ignore-decorations' => {'yes' => undef, 'no' => undef}); my %monitor_params = ('monitor-type' => {'expression' => {'rpn-expr' => undef}, 'failures' => undef}, 'action' => undef, 'expires' => undef ); my %action_params = ('action-type' => {'tset' => {'tset-name' => undef}, 'exec' => {'command' => undef} } ); my %view_params = ('expires' => undef, 'view-type' => {'rrgraph' => {'width' => undef, 'height' => undef, 'start' => undef, 'line-style' => undef, 'line-color' => undef, '+ignore-limits' => { 'yes'=>undef, 'no'=>undef }, '+ignore-lower-limit' => { 'yes'=>undef, 'no'=>undef }, '+ignore-upper-limit' => { 'yes'=>undef, 'no'=>undef }}, 'rrprint' => {'start' => undef, 'print-cf' => undef}, 'html' => {'html-template' => undef}, 'adminfo' => undef} ); # Load additional validation, configurable from # torrus-config.pl and torrus-siteconfig.pl foreach my $mod ( @Torrus::Validator::loadLeafValidators ) { eval( 'require ' . $mod ); die( $@ ) if $@; eval( '&' . $mod . '::initValidatorLeafParams( \%leaf_params )' ); die( $@ ) if $@; } sub validateNodes { my $config_tree = shift; my $token = $config_tree->token('/'); if( defined($token) ) { return validateNode($config_tree, $token); } else { Error("The datasource tree is empty"); return 0; } } sub validateNode { my $config_tree = shift; my $token = shift; &Torrus::DB::checkInterrupted(); my $ok = 1; if( $config_tree->isLeaf($token) ) { # Verify the default view my $view = $config_tree->getNodeParam( $token, 'default-leaf-view' ); if( not defined( $view ) ) { my $path = $config_tree->path( $token ); Error("Default view is not defined for leaf $path"); $ok = 0; } elsif( not $config_tree->{'validator'}{'viewExists'}{$view} and not $config_tree->viewExists( $view ) ) { my $path = $config_tree->path( $token ); Error("Non-existent view is defined as default for leaf $path"); $ok = 0; } else { # Cache the view name $config_tree->{'validator'}{'viewExists'}{$view} = 1; } # Verify parameters $ok = validateInstanceParams($config_tree, $token, 'node', \%leaf_params); if( $ok ) { my $rrviewslist = $config_tree->getNodeParam( $token, 'rrgraph-views' ); # Check the cache first if( not $config_tree->{'validator'}{'graphviews'}{$rrviewslist} ) { my @rrviews = split( ',', $rrviewslist ); if( scalar(@rrviews) != 5 ) { my $path = $config_tree->path( $token ); Error('rrgraph-views sould refer 5 views in' . $path); $ok = 0; } else { foreach my $view ( @rrviews ) { if( not $config_tree->viewExists( $view ) ) { my $path = $config_tree->path( $token ); Error("Non-existent view ($view) is defined in " . "rrgraph-views for $path"); $ok = 0; } elsif( $config_tree->getParam($view, 'view-type') ne 'rrgraph' ) { my $path = $config_tree->path( $token ); Error("View $view is not of type rrgraph in " . "rrgraph-views for $path"); $ok = 0; } } } if( $ok ) { # Store the cache $config_tree->{'validator'}{'graphviews'}{$rrviewslist}=1; } } } # Verify monitor references my $mlist = $config_tree->getNodeParam( $token, 'monitor' ); if( defined $mlist ) { foreach my $param ( 'monitor-period', 'monitor-timeoffset' ) { if( not defined( $config_tree->getNodeParam( $token, $param ) ) ) { my $path = $config_tree->path( $token ); Error('Mandatory parameter ' . $param . ' is not defined in ' . $path); $ok = 0; } } foreach my $monitor ( split(',', $mlist) ) { if( not $config_tree->{'validator'}{'monitorExists'}{$monitor} and not $config_tree->monitorExists( $monitor ) ) { my $path = $config_tree->path( $token ); Error("Non-existent monitor: $monitor in $path"); $ok = 0; } else { $config_tree->{'validator'}{'monitorExists'}{$monitor} = 1; } } my $varstring = $config_tree->getNodeParam( $token, 'monitor-vars' ); if( defined $varstring ) { foreach my $pair ( split( '\s*;\s*', $varstring ) ) { if( $pair !~ /^\w+\s*\=\s*[0-9\-+.eU]+$/o ) { Error("Syntax error in monitor variables: $pair"); $ok = 0; } } } my $action_target = $config_tree->getNodeParam($token, 'monitor-action-target'); if( defined( $action_target ) ) { my $target = $config_tree->getRelative($token, $action_target); if( not defined( $target ) ) { my $path = $config_tree->path( $token ); Error('monitor-action-target points to an invalid path: ' . $action_target . ' in ' . $path); $ok = 0; } elsif( not $config_tree->isLeaf( $target ) ) { my $path = $config_tree->path( $token ); Error('monitor-action-target must point to a leaf: ' . $action_target . ' in ' . $path); $ok = 0; } } } # Verify if the data-dir exists my $datadir = $config_tree->getNodeParam( $token, 'data-dir' ); if( defined $datadir ) { if( not $config_tree->{'validator'}{'dirExists'}{$datadir} and not ( -d $datadir ) and not $Torrus::ConfigTree::Validator::reportedErrors{$datadir} ) { my $path = $config_tree->path( $token ); Error("Directory does not exist: $datadir in $path"); $ok = 0; $Torrus::ConfigTree::Validator::reportedErrors{$datadir} = 1; } else { # Store the cache $config_tree->{'validator'}{'dirExists'}{$datadir} = 1; } } # Verify type-specific parameters my $dsType = $config_tree->getNodeParam( $token, 'ds-type' ); if( not defined( $dsType ) ) { # Writer has already complained return 0; } if( $dsType eq 'rrd-multigraph' ) { my @dsNames = split(',', $config_tree->getNodeParam( $token, 'ds-names' ) ); if( scalar(@dsNames) == 0 ) { my $path = $config_tree->path( $token ); Error("ds-names list is empty in $path"); $ok = 0; } foreach my $dname ( @dsNames ) { my $param = 'ds-expr-' . $dname; my $expr = $config_tree->getNodeParam( $token, $param ); if( not defined( $expr ) ) { my $path = $config_tree->path( $token ); Error("Parameter $param is not defined in $path"); $ok = 0; } else { $ok = validateRPN( $token, $expr, $config_tree ) ? $ok : 0; } foreach my $paramprefix ( 'graph-legend-', 'line-style-', 'line-color-', 'line-order-' ) { my $param = $paramprefix.$dname; my $value = $config_tree->getNodeParam($token, $param); if( not defined( $value ) ) { my $path = $config_tree->path( $token ); Error('Parameter ' . $param . ' is not defined in ' . $path); $ok = 0; } elsif( $param eq 'line-style-' and not validateLine( $value ) ) { my $path = $config_tree->path( $token ); Error('Parameter ' . $param . ' is defined incorrectly in ' . $path); $ok = 0; } elsif( $param eq 'line-color-' and not validateColor( $value ) ) { my $path = $config_tree->path( $token ); Error('Parameter ' . $param . ' is defined incorrectly in ' . $path); $ok = 0; } } } } elsif( $dsType eq 'rrd-file' and $config_tree->getNodeParam( $token, 'leaf-type' ) eq 'rrd-cdef') { my $expr = $config_tree->getNodeParam( $token, 'rpn-expr' ); if( defined( $expr ) ) { $ok = validateRPN( $token, $expr, $config_tree ) ? $ok : 0; } # Otherwise already reported by validateInstanceParams() } elsif($dsType eq 'collector' and $config_tree->getNodeParam( $token, 'collector-type' ) eq 'snmp') { # Check the OID syntax my $oid = $config_tree->getNodeParam( $token, 'snmp-object' ); if( defined($oid) and $oid =~ /^\./o ) { my $path = $config_tree->path( $token ); Error("Invalid syntax for snmp-object in " . $path . ": OID must not start with dot"); $ok = 0; } } } else { # This is subtree my $view = $config_tree->getNodeParam( $token, 'default-subtree-view' ); if( not defined( $view ) ) { my $path = $config_tree->path( $token ); Error("Default view is not defined for subtree $path"); $ok = 0; } elsif( not $config_tree->{'validator'}{'viewExists'}{$view} and not $config_tree->viewExists( $view ) ) { my $path = $config_tree->path( $token ); Error("Non-existent view is defined as default for subtree $path"); $ok = 0; } else { # Store the cache $config_tree->{'validator'}{'viewExists'}{$view} = 1; } foreach my $ctoken ( $config_tree->getChildren($token) ) { if( not $config_tree->isAlias($ctoken) ) { $ok = validateNode($config_tree, $ctoken) ? $ok:0; } } } return $ok; } my %validFuntcionNames = ( 'AVERAGE' => 1, 'MIN' => 1, 'MAX' => 1, 'LAST' => 1, 'T' => 1 ); sub validateRPN { my $token = shift; my $expr = shift; my $config_tree = shift; my $timeoffset_supported = shift; &Torrus::DB::checkInterrupted(); my $ok = 1; # There must be at least one DS reference my $ds_couter = 0; my $rpn = new Torrus::RPN; # The callback for RPN translation my $callback = sub { my ($noderef, $timeoffset) = @_; my $function; if( $noderef =~ s/^(.+)\@//o ) { $function = $1; } if( defined( $function ) and not $validFuntcionNames{$function} ) { my $path = $config_tree->path($token); Error('Invalid function name ' . $function . ' in node reference at ' . $path); $ok = 0; return undef; } my $leaf = length($noderef) > 0 ? $config_tree->getRelative($token, $noderef) : $token; if( not defined $leaf ) { my $path = $config_tree->path($token); Error("Cannot find relative reference $noderef at $path"); $ok = 0; return undef; } if( not $config_tree->isLeaf( $leaf ) ) { my $path = $config_tree->path($token); Error("Relative reference $noderef at $path is not a leaf"); $ok = 0; return undef; } if( $config_tree->getNodeParam($leaf, 'leaf-type') ne 'rrd-def' ) { my $path = $config_tree->path($token); Error("Relative reference $noderef at $path must point to a ". "leaf of type rrd-def"); $ok = 0; return undef; } if( defined( $timeoffset ) and not $timeoffset_supported ) { my $path = $config_tree->path($token); Error("Time offsets are not supported at $path"); $ok = 0; return undef; } $ds_couter++; return 'TESTED'; }; $rpn->translate( $expr, $callback ); if( $ok and $ds_couter == 0 ) { my $path = $config_tree->path($token); Error("RPN must contain at least one DS reference at $path"); $ok = 0; } return $ok; } sub validateViews { my $config_tree = shift; my $ok = 1; foreach my $view ($config_tree->getViewNames()) { &Torrus::DB::checkInterrupted(); $ok = validateInstanceParams($config_tree, $view, 'view', \%view_params) ? $ok:0; if( $ok and $config_tree->getParam($view, 'view-type') eq 'rrgraph' ) { my $hrulesList = $config_tree->getParam($view, 'hrules'); if( defined( $hrulesList ) ) { foreach my $hrule ( split(',', $hrulesList ) ) { my $valueParam = $config_tree->getParam($view, 'hrule-value-' . $hrule); if( not defined( $valueParam ) or $valueParam !~ /^\S+$/o ) { Error('Mandatory parameter hrule-value-' . $hrule . ' is not defined or incorrect for view ' . $view); $ok = 0; } my $color = $config_tree->getParam($view, 'hrule-color-'.$hrule); if( not defined( $color ) ) { Error('Mandatory parameter hrule-color-' . $hrule . ' is not defined for view ' . $view); $ok = 0; } else { $ok = validateColor( $color ) ? $ok:0; } } } my $decorList = $config_tree->getParam($view, 'decorations'); if( defined( $decorList ) ) { foreach my $decorName ( split(',', $decorList ) ) { foreach my $paramName ( qw(order style color expr) ) { my $param = 'dec-' . $paramName . '-' . $decorName; if( not defined( $config_tree-> getParam($view, $param) ) ) { Error('Missing parameter: ' . $param . ' in view ' . $view); $ok = 0; } } $ok = validateLine( $config_tree-> getParam($view, 'dec-style-' . $decorName) ) ? $ok:0; $ok = validateColor( $config_tree-> getParam($view, 'dec-color-' . $decorName) ) ? $ok:0; } } $ok = validateColor( $config_tree->getParam($view, 'line-color') ) ? $ok:0; $ok = validateLine( $config_tree->getParam($view, 'line-style') ) ? $ok:0; my $gprintValues = $config_tree->getParam($view, 'gprint-values'); if( defined( $gprintValues ) and length( $gprintValues ) > 0 ) { foreach my $gprintVal ( split(',', $gprintValues ) ) { my $format = $config_tree->getParam($view, 'gprint-format-' . $gprintVal); if( not defined( $format ) or length( $format ) == 0 ) { Error('GPRINT format for ' . $gprintVal . ' is not defined for view ' . $view); $ok = 0; } } } } } return $ok; } sub validateColor { my $color = shift; my $ok = 1; if( $color !~ /^\#[0-9a-fA-F]{6}$/o ) { if( $color =~ /^\#\#(\S+)$/o ) { if( not $Torrus::Renderer::graphStyles{$1}{'color'} ) { Error('Incorrect color reference: ' . $color); $ok = 0; } } else { Error('Incorrect color syntax: ' . $color); $ok = 0; } } return $ok; } sub validateLine { my $line = shift; my $ok = 1; if( $line =~ /^\#\#(\S+)$/o ) { if( not $Torrus::Renderer::graphStyles{$1}{'line'} ) { Error('Incorrect line style reference: ' . $line); $ok = 0; } } elsif( not $Torrus::SiteConfig::validLineStyles{$line} ) { Error('Incorrect line syntax: ' . $line); $ok = 0; } return $ok; } sub validateMonitors { my $config_tree = shift; my $ok = 1; foreach my $action ($config_tree->getActionNames()) { $ok = validateInstanceParams($config_tree, $action, 'action', \%action_params) ? $ok:0; my $atype = $config_tree->getParam($action, 'action-type'); if( $atype eq 'tset' ) { my $tset = $config_tree->getParam($action, 'tset-name'); if( defined $tset ) { $tset = 'S'.$tset; if( not $config_tree->tsetExists( $tset ) ) { Error("Token-set does not exist: $tset in action $action"); $ok = 0; } } # Otherwise the error is already reported by validateInstanceParams } elsif( $atype eq 'exec' ) { my $launch_when = $config_tree->getParam($action, 'launch-when'); if( defined $launch_when ) { foreach my $when ( split(',', $launch_when) ) { my $matched = 0; foreach my $event ('set', 'repeat', 'clear', 'forget') { if( $when eq $event ) { $matched = 1; } } if( not $matched ) { if( $when eq 'throw' ) { Error('Event type "throw" is no longer ' . 'supported. Replace with "set".'); } else { Error("Invalid value in parameter launch-when " . "in action $action: $when"); } $ok = 0; } } } my $setenv_dataexpr = $config_tree->getParam( $action, 'setenv-dataexpr' ); if( defined( $setenv_dataexpr ) ) { # foreach my $pair ( split( ',', $setenv_dataexpr ) ) { my ($env, $param) = split( '=', $pair ); if( not $param ) { Error("Syntax error in setenv-dataexpr in action " . $action . ": \"" . $pair . "\""); $ok = 0; } elsif( $env =~ /\W/o ) { Error("Illegal characters in environment variable ". "name in setenv-dataexpr in action " . $action . ": \"" . $env . "\""); $ok = 0; } elsif( not defined ($config_tree->getParam( $action, $param ) ) ) { Error("Parameter referenced in setenv-dataexpr is " . "not defined in action " . $action . ": " . $param); $ok = 0; } } } } } foreach my $monitor ($config_tree->getMonitorNames()) { $ok = validateInstanceParams($config_tree, $monitor, 'monitor', \%monitor_params) ? $ok:0; my $alist = $config_tree->getParam( $monitor, 'action' ); foreach my $action ( split(',', $alist ) ) { if( not $config_tree->actionExists( $action ) ) { Error("Non-existent action: $action in monitor $monitor"); $ok = 0; } } } return $ok; } sub validateTokensets { my $config_tree = shift; my $ok = 1; my $view = $config_tree->getParam( 'SS', 'default-tsetlist-view' ); if( not defined( $view ) ) { Error("View is not defined for tokensets list"); $ok = 0; } elsif( not $config_tree->viewExists( $view ) ) { Error("Non-existent view is defined for tokensets list"); $ok = 0; } foreach my $tset ($config_tree->getTsets()) { &Torrus::DB::checkInterrupted(); $view = $config_tree->getParam($tset, 'default-tset-view'); if( not defined( $view ) ) { $view = $config_tree->getParam('SS', 'default-tset-view'); } if( not defined( $view ) ) { Error("Default view is not defined for tokenset $tset"); $ok = 0; } elsif( not $config_tree->viewExists( $view ) ) { Error("Non-existent view is defined for tokenset $tset"); $ok = 0; } } return $ok; } sub validateInstanceParams { my $config_tree = shift; my $inst_name = shift; my $inst_type = shift; my $mapref = shift; &Torrus::DB::checkInterrupted(); # Debug("Validating $inst_type $inst_name"); my $ok = 1; my @namemaps = ($mapref); while( $ok and scalar(@namemaps) > 0 ) { my @next_namemaps = (); foreach my $namemap (@namemaps) { foreach my $paramkey (keys %{$namemap}) { # Debug("Checking param: $pname"); my $pname = $paramkey; my $mandatory = 1; if( $pname =~ s/^\+//o ) { $mandatory = 0; } my $listval = 0; if( $pname =~ s/^\@//o ) { $listval = 1; } my $pvalue = $config_tree->getInstanceParam($inst_type, $inst_name, $pname); my @pvalues; if( $listval ) { @pvalues = split(',', $pvalue); } else { @pvalues = ( $pvalue ); } if( not defined( $pvalue ) ) { if( $mandatory ) { my $msg; if( $inst_type eq 'node' ) { $msg = $config_tree->path( $inst_name ); } else { $msg = "$inst_type $inst_name"; } Error("Mandatory parameter $pname is not ". "defined for $msg"); $ok = 0; } } else { if( ref( $namemap->{$paramkey} ) ) { foreach my $pval ( @pvalues ) { if( exists $namemap->{$paramkey}->{$pval} ) { if( defined $namemap->{$paramkey}->{$pval} ) { push( @next_namemaps, $namemap->{$paramkey}->{$pval} ); } } else { my $msg; if( $inst_type eq 'node' ) { $msg = $config_tree->path( $inst_name ); } else { $msg = "$inst_type $inst_name"; } Error("Parameter $pname has ". "unknown value: $pval for $msg"); $ok = 0; } } } } } } @namemaps = @next_namemaps; } return $ok; } 1; # Local Variables: # mode: perl # indent-tabs-mode: nil # perl-indent-level: 4 # End: