#!/usr/bin/perl -w 
#
# Help : ./check_snmp_attributes.pl -h

use strict;
use Net::SNMP;
use Getopt::Long;

# Nagios Perl Attributes Library (Napal) is likely name for future library based on this plugin
# package Napal;

#use lib "/usr/local/nagios/libexec";
#use utils qw(%ERRORS $TIMEOUT);
my $TIMEOUT = 45;
my %ERRORS=('OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3,'DEPENDENT'=>4);

# Globals

my $Version='0.3';

my $o_host = 	undef; 		# hostname
my $o_community = undef; 	# community
my $o_port = 	161; 		# port
my $o_help=	undef; 		# wan't some help ?
my $o_verb=	undef;		# verbose mode (base level1 verbose)
my $o_vdebug=   -1;             # same as above but specifies actual level as integer
my $o_version=	undef;		# print version
my $o_timeout=  5;             	# Default 5s Timeout
my $o_version2= undef;          # use snmp v2c
# SNMPv3 specific
my $o_login=	undef;		# Login for snmpv3
my $o_passwd=	undef;		# Pass for snmpv3

my $o_perf=     undef;          # Performance data option
my $o_attr=	undef;  	# What attribute(s) to check (specify more then one separated by '.')
my @o_attrL=    ();             # array for above list
my $o_perfattr= undef;		# List of attributes to only provide values in perfomance data but no checking
my @o_perfattrL=();		# array for above list
my $o_warn=     undef;          # warning level option
my @o_warnL=    ();              # array of warning values (array of hashes really)
my $o_crit=     undef;          # Critical level option
my @o_critL=    ();             # array of critical values
my $o_dispattr= undef;          # List of attributes that would be displayed but not checked
my @o_dispattrL=();             # array of display-only attributes
my $o_unkdef=	undef;		# Default value to report for unknown attributes
my $o_label=    '';             # Label used to show what is in plugin output
my $o_confexpr= undef;          # Config expression
my $o_enfwcnum= undef;          # Special check to make sure number of warning and critical parameters is same as what is at -a

# non-config global arrays
my %data_vars=  ();             # Main hash array holding variable & attribute data and expressions ready for processing; 
                                # This also serves as symbols table and contains function names, operators, etc.
my @expr_order=  ();            # This array holds expressions in the order of how they are to be processed, it is filled by process_config()
my %func_table= ();             # This is an array of registered functions containing references to perl functions that deal with them
my @nets_oid_array=();          # Array that holds all SNMP data OIDs to be retrieved
my %nets_oid_hash=();           # Pointers from SNMP OIDs to data locations within data_vars array
my @debug_buffer= ();           # Used to store debugging data for verb calls done prior to check_options()

# Functions

sub p_version { print "Called as $0. Base is check_snmp_attributes version : $Version\n"; }

sub print_usage {
    print "Usage: $0 [-v] -H <host> -C <snmp_community> [-2] | (-l login -x passwd)  [-p <port>] -e <config expressions> [-a <attributes to check> -w <warn level> -c <crit level> [-f]] [-A <attributes to show>] [-W <warn expressions>] [-C <crit expressions>] [-F <attributes for perfdata>] [-t <timeout>] [-V] [-u <default value>]\n";
}

# Return true if arg is a number (not true number really - it checks if arguments starts with a proper number actually)
sub isnum {
    my $num = shift;
    if ( $num =~ /^([-|+]?(\d+\.?\d*)|[-|+]?(^\.\d+))/ ) { return 1; }
    return 0;
}

sub tonum {
    my $num = shift;
    if ( $num =~ /^([-|+]?(\d+\.?\d*)|[-|+]?(^\.\d+))/ ) { return $1; }
    return undef;
}

# For verbose/debug output
sub verb { 
    my ($lv,$dbg)=@_;
    $debug_buffer[$lv].= "($lv) ".$dbg."\n" if defined($dbg) && ($o_vdebug==-1 || $o_vdebug >= $lv);
    for (my $i=1;$i<=$o_vdebug;$i++) {
	    print $debug_buffer[$i] if defined($debug_buffer[$i]);
	    $debug_buffer[$i]="";
    }
}

sub help {
   print "\nAdvanced SNMP (Attributes) Plugin for Nagios version ",$Version,"\n";
   print "  by William Leibzon - william(at)leibzon.org\n\n";
   print_usage();
   print <<EOT;
-v, --verbose
	print extra debugging information
-h, --help
	print this help message
-V, --version
	prints version number
-L, --label
        Plugin output label
-H, --hostname=HOST
	name or IP address of host to check
-C, --community=COMMUNITY NAME
	community name for the host s SNMP agent (implies v 1 protocol)
-P, --port=PORT
	SNMPD port (Default 161)
-t, --timeout=INTEGER
	timeout for SNMP in seconds (Default : 5)
-2, --v2c 
        use SNMP v2 (instead of SNMP v1)
-l, --login=LOGIN
   Login for snmpv3 authentication (implies v3 protocol with MD5)
-x, --passwd=PASSWD
   Password for snmpv3 authentication
-e, --expression=STR[;STR[;STR[..]]]
   List of Configuration expression(s). They are entered as
     attrib1,attrib2,..=oper,oper,oper,...
   Where operands order is in RPN (Reverse Polish Notation) and can contain function names, 
   operators (+/-*%), attribute variable names and data numbers (or data strings enclosed in '..').
   Multiple expressions can be specified separated from each other with ';'
-w, --warn=STR[,STR[,STR[..]]]
	Warning level(s) - usually numbers (same number of values specified as number of attributes)
	Warning values can have the following prefix modifiers:
	   > : warn if data is above this value (default for numeric values)
	   < : warn if data is below this value (must be followed by number)
	   = : warn if data is equal to this value (default for non-numeric values)
	   ! : warn if data is not equal to this value
	   ~ : do not check this data (must not be followed by number)
	   ^ : for numeric values this disables checks that warning is less then critical
        Note that level is considered as one-operand expression, so in fact you can specify
	name of attribute you previously specified with --expression=..
-c, --critical=STR[,STR[,STR[..]]]
	critical level(s) (if more then one name is checked, must have multiple values)
	Critical values can have the same prefix modifiers as warning (see above) except '^'
--check-warncritlist
        This causes enforced checking to make sure that number of critical and warning levels specified is exactly
	the same as number of attributes specified with -A and enforce checking that warning values are smaller 
        (or larger for '>') then critical. This does not allow "attribute<level" syntax in -w or -c either.
-a, --attributes=STR[,STR[,STR[..]]]
	Which attribute(s) to check. This is used as regex to check if attribute is found in attribnames table
-D, --display_attributes=STR[,STR[,STR[..]]]
        List of attributes that would be displayed (in main screen) but not checked with 
-A, --perf_attributes=STR[,STR[,STR[..]]]
	Which attribute(s) to add to as part of performance data output. These names can be different then the
	ones listed in '-a' to only output attributes in perf data but not check. Special value of '*' gets them all.
-f, --perfparse
        Used only with '-a'. Causes to output data not only in main status line but also as perfparse output
-u, --unknown_default=INT
        If attribute is not found then report the output as this number (i.e. -u 0)
EOT
}

sub check_options {
    Getopt::Long::Configure ("bundling");
    Getopt::Long::GetOptions(
   	'v+'	=> \$o_verb,		'verbose+'	=> \$o_verb,   'debug:i'       => \$o_vdebug,
        'h'     => \$o_help,    	'help'        	=> \$o_help,
        'H:s'   => \$o_host,		'hostname:s'	=> \$o_host,
        'p:i'   => \$o_port,   		'port:i'	=> \$o_port,
        'C:s'   => \$o_community,	'community:s'	=> \$o_community,
	'l:s'	=> \$o_login,		'login:s'	=> \$o_login,
	'x:s'	=> \$o_passwd,		'passwd:s'	=> \$o_passwd,
        't:i'   => \$o_timeout,       	'timeout:i'     => \$o_timeout,
	'V'	=> \$o_version,		'version'	=> \$o_version,
        '2'     => \$o_version2,        'v2c'           => \$o_version2,
	'L:s'   => \$o_label,           'label:s'       => \$o_label,
        'c:s'   => \$o_crit,            'critical:s'    => \$o_crit,
        'w:s'   => \$o_warn,            'warn:s'        => \$o_warn,
        'f'     => \$o_perf,            'perfparse'     => \$o_perf,
        'a:s'   => \$o_attr,         	'attributes:s' 	=> \$o_attr,
	'A:s'	=> \$o_perfattr,	'perf_attributes:s' => \$o_perfattr,
	'u:i'	=> \$o_unkdef,		'unknown_default:i' => \$o_unkdef,
	'e:s'   => \$o_confexpr,        'expression:s'  => \$o_confexpr,
	'D:s'   => \$o_dispattr,        'display_attributes:s' => \$o_dispattr,
	'check-warncritlist' => \$o_enfwcnum,
    );
    if (defined($o_help) ) { help(); plugin_exit("UNKNOWN"); };
    if (defined($o_version)) { p_version(); plugin_exit("UNKNOWN"); }
    if (defined($o_verb) && $o_vdebug==-1) { $o_vdebug=$o_verb; }
    if ($o_vdebug==-1) { $o_vdebug=0; }
    verb($o_vdebug,undef);
    if ( ! defined($o_host) ) # check host and filter 
	{ print "No host defined!\n"; print_usage(); plugin_exit("UNKNOWN"); }
    # check snmp information
    if ( !defined($o_community) && (!defined($o_login) || !defined($o_passwd)) )
	{ print "Put snmp login info!\n"; print_usage(); plugin_exit("UNKNOWN"); }

    if (!defined($o_confexpr) && scalar(@expr_order)==0) {
	print "Config expression not defined!\n";
	print_usage(); 
	plugin_exit("UNKNOWN"); 
    }
    elsif (defined($o_confexpr)) {
	my @config_expressions = split /;/, $o_confexpr;
        process_config_expressions(\@config_expressions);
    }

    if (defined($o_perfattr)) {
        @o_perfattrL=split(/,/ ,$o_perfattr) if defined($o_perfattr);
    }
    if (defined($o_dispattr)) {
        @o_dispattrL=split(/,/ ,$o_dispattr) if defined($o_dispattr);
    }
    if (defined($o_warn) || defined($o_crit) || defined($o_attr)) {
        if (defined($o_attr)) {
          @o_attrL=split(/,/, $o_attr);
        }
        else {
          print "Specifying warning and critical levels requires '-a' parameter with attribute names\n";
          print_usage();
          plugin_exit("UNKNOWN");
        }
	if (defined($o_enfwcnum)) {
	    set_warncrit_enforced();
	}
	else {
	    set_warncrit_unrestricted();
	}
    }
    if (scalar(@o_attrL)==0 && scalar(@o_perfattrL)==0 && scalar(@o_dispattrL)==0) {
        print "You must specify list of attributes with either '-a' or '-A'\n";
        print_usage();
	plugin_exit("UNKNOWN");
    }
}

sub set_alarm {
    my $timeout_in = shift;

    # Get the alarm signal (just in case snmp timout screws up)
    $SIG{'ALRM'} = sub {
       print ("ERROR: Alarm signal (Nagios time-out)\n");
       exit $ERRORS{"UNKNOWN"};
    };

    # Check gobal timeout if snmp screws up
    if (defined($TIMEOUT) || defined($timeout_in)) {
       $timeout_in = $TIMEOUT if !defined($timeout_in);
       verb(1,"Alarm at $timeout_in");
       alarm($timeout_in);
    } else {
       verb(1,"no timeout defined : $o_timeout + 10");
       alarm ($o_timeout+10);
    }
}

sub get_perfdata {
    my $i;
    my $perfdata;
    my $dt;

    # Decide which performance attributes are going to be reported back and then actually do it
    if (defined($o_perf)) {
         for ($i=0;$i<scalar(@o_attrL);$i++) {
	     $data_vars{$o_attrL[$i]}{'perf'}=1;
	 }
     }

    if (defined($o_perfattr) && $o_perfattr ne '*') {
	for ($i=0;$i<scalar(@o_perfattrL);$i++) {
	    $data_vars{$o_perfattrL[$i]}{'perf'}=1;
	}
    }
    foreach (keys %data_vars) {
	if (((defined($o_perfattr) && $o_perfattr eq '*') || defined($data_vars{$_}{'perf'}))
	     && defined($data_vars{$_}{'data'}) && $data_vars{$_}{'from'} eq 'config') {
	    $dt=$data_vars{$_}{'data'};
	    $dt=tonum($dt) if isnum($dt);
	    $perfdata .= " " . $_ . "=" . $dt;
	}
    }

    return $perfdata;
}

sub plugin_output {
    my ($label, $errorcode, $errorinfo, $statusdata, $perfdata) = @_;

    # Output everything and exit
    $label .= " " if $o_label ne '';
    print $label . $errorcode;
    print " -".$errorinfo if $errorinfo;
    print " -".$statusdata if $statusdata;
    print " |".$perfdata if $perfdata;
    print "\n";
}

# main function
sub run_plugin {
    set_alarm(); # this resets alarm even if it was set before (yes, that's on purpose)
    # This processes config and then processes functions (which does SNMP connection) and expressions
    preproc_allfunctions();
    check_options();
    globalproc_allfunctions();
    process_allexpressions();
    # Sets default for things not otherwise done - this might need to be revised
    foreach (@expr_order) {
	$data_vars{$_}{'data'}=$o_unkdef if !defined($data_vars{$_}{'data'}) && defined($o_unkdef);
    }
    postproc_allfunctions();

    # Check data with specified thresholds and output results
    my ($statuscode, $statusinfo, $statusdata) = check_warncrit_thresholds();
    my $perfdata = get_perfdata();
    plugin_output($o_label, $statuscode, $statusinfo, $statusdata, $perfdata);
    plugin_exit($statuscode);
}

############## MAIN ###########
set_alarm();
registerbasefunctions();
if (!caller(0)) {
	run_plugin();
}

1;

################### SUBROUTINES DEALING WITH PROCESSING OF EXPRESSION HANDLER FUNCTIONS  ###################

# For reference here is list of acceptable handler definition variables - 'id' is reguired, others are optional and depend on how this is used
#   'id'    - id of the function - functions are grouped based on this name
#   'uid'   - unique self-id for this function handler (actual real unique registration id '_id' is generated based on this and random number)
#   'oper'  - this is set of symbols that must match exactly
#   'name'  - function name in expression when called directly as in 'name()'
#   'regex' - this is a regex that if matched also causes use of this handler; can lead to errors, avoid this unless necessary
#  [all below should contain pointers to functions]
#   'sub_init'       - called before all processing has started, can be used as constructor-like way for delayed initialization
#   'sub_config'     - called when function is being configured for each mention in expression; full function call is a paramter
#                      should return true (1) on success and false (0) if its invaliud call for this function
#   'sub_globalproc' - called after configuration to do general processing before variables are actually evaluated; no parameters
#   'sub_eval'       - called for each case where expressions are evaluated; full function call is first parameter, reference to stack is second
#                      should return true (1) on success and false (0) if there was some error
#   'sub_postproc'   - called after evaluation of expressions
#   'sub_exit'       - called after everything has been processed right before plugin is ready to exit, can be used as destructor; no parameters
sub register_function {
    my $funcdef = shift;
    my $fn;

    if (!defined($funcdef->{'id'})) {
	print "Invalid call to register_function()\n";
	plugin_exit("UNKNOWN");
    }
    $fn=$funcdef->{'id'};
    $funcdef->{'_id'}=$funcdef->{'id'}."_";
    $funcdef->{'_id'}.=$funcdef->{'uid'}."_" if defined($funcdef->{'uid'});
    $funcdef->{'_id'}.=rand();
    $func_table{$fn}=[] if !exists($func_table{$fn});
    unshift @{$func_table{$fn}}, $funcdef;
    if (defined($funcdef->{'oper'}) && !defined($data_vars{$funcdef->{'oper'}})) {
	verb(4,"Function $fn will be called when operator '".$funcdef->{'oper'}."' is used");
	$data_vars{$funcdef->{'oper'}}={ 'type' =>'func', 'handler'=> $fn };
    }
    if (defined($funcdef->{'name'}) && !defined($data_vars{$funcdef->{'name'}})) {
	verb(4,"Function $fn will be called when expression contains '".$funcdef->{'name'}."(..)'");
	$data_vars{$funcdef->{'name'}}={ 'type' =>'func', 'handler'=> $fn }
    }
    verb(2,"Registered function $fn");
}

# Functional general processing function - iterates through all of them doing same type of call
sub process_allfunctions {
    my $call_type = shift;
    my $fname;
    my $fcall;
    my $ft;

    foreach $fname (keys %func_table) {
	foreach (@{$func_table{$fname}}) {
	    if (defined($_->{$call_type})) {
		$fcall=$_->{$call_type};
		$ft=&$fcall();
	    }
	}
    }
}

sub preproc_allfunctions {
    process_allfunctions('sub_init');
}

sub globalproc_allfunctions {
    process_allfunctions('sub_globalproc');
}

sub postproc_allfunctions {
    process_allfunctions('sub_postproc');
}

# Interates through destructors at the end right before exit
sub plugin_exit {
    my $exitcode=shift;
    process_allfunctions('sub_exit');
    exit $ERRORS{$exitcode};
}

# Function processing during processing of config string
sub config_functioncall {
    my $func_str = shift;
    my $fcall;
    my $fn=undef;
    my $fnp;
    my $reg;
    my $ret="";

    if ($func_str eq "") {
	verb(2,"Empty operand in expression, giving up");
	return "";
    }
    elsif ($func_str =~ /^(.+)\(.*\)?$/ && exists($data_vars{$1}) && $data_vars{$1}{'type'} eq 'func') {
	$fn=$data_vars{$1}{'handler'};
	verb(6,"Match found: function $fn for symbol '".$1."'");
    }
    elsif (exists($data_vars{$func_str}) && $data_vars{$func_str}{'type'} eq 'func') {
	$fn=$data_vars{$func_str}{'handler'};
	verb(6,"Match found: function $fn for symbol '$func_str'");
    }
    if (defined($fn)) {
	L1: foreach(@{$func_table{$fn}}) {
	    $ret=$fn;
	    if (defined($_->{'sub_config'})) {
		$fcall=$_->{'sub_config'};
		$ret=&$fcall($func_str);
		$ret=$fn if $ret;
	    }
	    last L1 if $ret;
	}
    }
    if (!$ret) {
    L2: foreach $fn (sort keys %func_table) {
            foreach $fnp (@{$func_table{$fn}}) {
		if (defined($fnp->{'regex'})) {
		    $reg=$fnp->{'regex'};
		    if ($func_str =~ /^$reg/) {
			verb(6,"Match found: function $fn for symbol '$func_str'");
			$ret=$fn;
			if (defined($fnp->{'sub_config'})) {
			    $fcall=$fnp->{'sub_config'};
			    $ret=&$fcall($func_str);
			    $ret=$fn if $ret;
			}
			last L2 if $ret;
		    }
		}
	    }
        }
    }
    if ($ret) {
	verb(6,"Configured function $ret for '$func_str'");
	return $ret;
    }
    return ""; # no suitable function found, error will be given out by process_config due to this
}

# Evaluate function - a lot simpler since function name is an argument here
sub eval_functioncall {
    my ($fn, $func_str, $stack) = @_;
    my $fcall;
    my $is_ok;

    if (exists($func_table{$fn})) {
	foreach(@{$func_table{$fn}}) {
	    $fcall=$_->{'sub_eval'};
	    $is_ok=&$fcall($func_str,$stack);
	    if ($is_ok) {
		verb(3,"Function '$fn' with id ".$_->{'_id'}." was used to handle $func_str");
		verb(3,"Stack data: ".join(" ",@{$stack}));
		return;
	    }
	}
    }
    if (!$func_str) {
	print "Error evaluating expression - no function/value specified (likely empty expression)\n";
    }
    else {
        print "Error evaluating function $func_str\n";
    }
    plugin_exit("UNKNOWN");
}

sub process_expressions {
    return process_config_expressions(\@_);
}

# This function processes list of configuration expressions
sub process_config_expressions {
    my $expressions_config = shift;
    my $expr;
    my $vars;
    my @var_list;
    my $tstr;
    my $elt;
    
    foreach (@{$expressions_config}) {
	($vars, $expr) = split /=/;
	if ($expr eq "") {
	    print "Configuration error - empty expression at $_";
	    plugin_exit("UNKNOWN");
	}
	if ($vars eq "") {
	    $vars = "_var_".rand() until !exists($data_vars{$vars});
	    $data_vars{$vars}={'type' => 'var', 'from' => '_auto_expr', 'dependencies' => [], 'data' => undef };
	    @var_list = ($vars);
	    verb(2,"Empty variable list at $_ - will create variable with random name: $vars");
	}
	else {
	    @var_list = split(/,/,$vars);
	    verb(2,"Variables '$vars' are defined by expression '$expr'");

	    foreach(@var_list) {
		if ($data_vars{$_}) {
		    print "Configuration error - trying to define variable '$_' second time at: $vars=$expr\n";
		    plugin_exit("UNKNOWN");
		}
		$data_vars{$_}={'type' => 'var', 'from' => 'config', 'dependencies' => [], 'data' => undef };
	    }
	}

	# First variable carries actual expression and list of other variables (including self) to which values are to be assigned after processing
	$data_vars{$var_list[0]}{'vars'}=[@var_list];
	$data_vars{$var_list[0]}{'expr'}=[];

	# look at the expression, this also deals with functions that take multiple arguments (for future)
	$elt="";
	foreach (split /,/,$expr) {
	    $elt.=',' if $elt;
	    $elt.=$_;
	    next if $elt =~ /\(/ && $elt !~ /\)/; # incomplete function without closing ')'
	    # main check to see what it is
	    $tstr=config_functioncall($elt);
	    if ($tstr) {
		push @{ $data_vars{$var_list[0]}{'expr'} }, [$tstr, $elt];
		verb(3,"Function '$tstr' will be used when handling $elt");
		if ($tstr eq "__var") {
		    verb(2,"Adding variable $elt to dependency list for variable(s) $vars");
		    push @{ $data_vars{$_}{'dependencies'} }, $elt foreach(@var_list);
		}
	    }
	    else {
		print("Unable to find suitable function for symbol '$elt' found in expression $expr\n");
		plugin_exit("UNKNOWN");
	    }
	    # clear it up for next loop iteration
	    $elt="";
        }
	if ($elt ne "") {
	    print "Configuration error - incomplete function $elt in configuration\n";
	    plugin_exit("UNKNOWN");
	}
	# TODO: for future may need to add algorithm that resolves dependencies and creates proper
	#       order for processing of expressions. For right now just do it in the order given in config
	push @expr_order, $_ foreach(@var_list);
    }
}


# This function processes expression [ordered list of function calls] that is in RPN (Reverse Polish Notation) form
sub evaluate_expression {
    # expressions come as a pointer to list of 2-dimensional arrays ([0] is type of function and [1] actual function call)
    my $expressions = shift;
    my $expression_string = "";
    my @stack=();

    $expression_string .= ($expression_string?',':'').$_->[1] foreach(@{$expressions});
    verb(2, "Processing expression '$expression_string'");
    foreach (@{ $expressions }) {
	eval_functioncall($_->[0], $_->[1], \@stack);
    }
    verb(2, "Results of evaluation of '$expression_string' is: ".join(",",@stack));
    return @stack;
}

sub process_allexpressions {
    my @expression_results;
    my $vname;

    foreach $vname (@expr_order) {
	if (!defined($data_vars{$vname}{'data'}) && exists($data_vars{$vname}{'expr'})) {
	    @expression_results=evaluate_expression($data_vars{$vname}{'expr'});
	    if (defined($data_vars{$vname}{'vars'}) && scalar(@expression_results)!=scalar(@{ $data_vars{$vname}{'vars'} })) {
		verb(2,"Warning - number of results from expression (".scalar(@expression_results).") is not equal to number of variables (".scalar(@{$data_vars{$vname}{'vars'}}).") in assignment");
	    }
	    if (defined($data_vars{$vname}{'vars'})) {
	        foreach (@{ $data_vars{$vname}{'vars'} }) {
		    if (scalar(@expression_results)>0) {
			$data_vars{$_}{'data'}=shift @expression_results;
			verb(1,"Var '$_' evaluated with result: ".$data_vars{$_}{'data'});
		    }
		    else {
			verb(1,"Warning - not enough results in expression to set variable $_");
		    }
		}
	    }
	}
    }
}

################## SUBROUTINES DEALING WITH CHECKING OF CRITICAL & WARNING VALUES ############

# This code for parsing warn/crit parameters imported from in check_snmp_table
# In this system for every attributed listed in '-a' there should be corresponding data in -w and -c
# This would get used only if '--check-warncritlist' option is used
sub set_warncrit_enforced {
        my ($isnw,$isnc,$nw,$nc,$opw,$opc); # these are just for optimization and to shorten the code
        my @o_warnLv=();
	my @o_critLv=();
        @o_warnLv=split(/,/ ,$o_warn) if defined($o_warn);
        @o_critLv=split(/,/ ,$o_crit) if defined($o_crit);

        if ((scalar(@o_warnLv)!=scalar(@o_attrL) && (scalar(@o_warnLv)-scalar(@o_attrL)>1 || $o_warn !~ /.*,$/)) ||
	    (scalar(@o_critLv)!=scalar(@o_attrL) && (scalar(@o_critLv)-scalar(@o_attrL)>1 || $o_crit !~ /.*,$/))) {
	        printf "Number of spefied warning levels (%d) and critical levels (%d) must be equal to the number of attributes specified at '-a' (%d). If you need to ignore some attribute specify it as '~'\n", scalar(@o_warnLv), scalar(@o_critLv), scalar(@o_attrL);
	        print_usage();
	        plugin_exit("UNKNOWN");
	}
        for (my $i=0; $i<scalar(@o_attrL); $i++) {
		if ($o_warnLv[$i]) {
			$o_warnLv[$i] =~ s/^(\^?[>|<|=|!|~]?)//;
			$opw=$1;
		}
		else {
			$opw="";
			$o_warnLv[$i]="";
		}
		if ($o_critLv[$i]) {
			$o_critLv[$i] =~ s/^([>|<|=|!|~]?)//;
			$opc=$1;
		}
		else {
			$opc="";
			$o_critLv[$i]="";
		}
		$isnw=isnum($o_warnLv[$i]);
		$isnc=isnum($o_critLv[$i]);
		$nw=tonum($o_warnLv[$i]) if $isnw;
		$nc=tonum($o_critLv[$i]) if $isnc;
		if (($opw =~ /^[>|<]/ && !$isnw) ||
		    ($opc =~ /^[>|<]/ && !$isnc)) {
			print "Numeric value required when '>' or '<' are used !\n";
			print_usage();
			plugin_exit("UNKNOWN");
          	}
		if ($isnw && $isnc && $opw eq $opc && (($nw>=$nc && $opw !~ /</) || ($nc<=$nc && $opc =~ /</))) {
			print "Problem with warning value $o_warnLv[$i] and critical value $o_critLv[$i] :\n";
                        print "All numeric warning values must be less then critical (or greater then when '<' is used)\n";
			print "Note: to override this check prefix warning value with ^\n";
                        print_usage();
			plugin_exit("UNKNOWN");
		}
		$opw =~ s/\^//;
		$opw = '=' if !$opw && !$isnw;
		$opw = '>' if !$opw && $isnw;
		$opc = '=' if !$opc && !$isnc;
		$opc = '>' if !$opc && $isnc;

		# Finally set the main warning & critical arrays - as part of that process passed value as one-operand in expression
		#   note: in the future will need to call modified version of process_config_expressions
		#         when support for regular (rather then just RPN) expressions would become available
		$o_warnL[$i] = { 'var' => $o_attrL[$i], 'comp' => $opw, 'configstr'=> $o_warnLv[$i] };
		if ($o_warnLv[$i]) {
		    $nw=config_functioncall($o_warnLv[$i]);
		    if ($nw) {
			$o_warnL[$i]{'expr'} = [ [$nw, $o_warnLv[$i]] ];
		    }
		    else {
			verb(2, "Problem finding function to process warning expression '".$o_warnLv[$i]."', will treat it as data");
			$o_warnL[$i]{'data'} = $o_warnLv[$i];
		    }

		}
	        $o_critL[$i] = { 'var' => $o_attrL[$i], 'comp' => $opc, 'configstr'=> $o_critLv[$i] };
		if ($o_critLv[$i]) {
		    $nc=config_functioncall($o_critLv[$i]);
		    if ($nc) {
			$o_critL[$i]{'expr'} = [ [$nc, $o_critLv[$i]] ];
		    }
		    else {
			verb(2, "Problem finding function to process crtical expression '".$o_critLv[$i]."', will treat it as data") if !$nc;
			$o_critL[$i]{'data'} = $o_critLv[$i];
		    }
		}
        }
}

# This is code for parsing warn/crit parameters imported from in check_snmp_table with extended syntax for it
sub set_warncrit_unrestricted {
    set_warncrit_enforced();
}

# help function when checking data against critical and warn values
sub check_value {
    my ($attrib, $data, $level, $modifier) = @_;

    my ($d,$l)=($data,$level);
    $d=tonum($d) if isnum($d);
    $l=tonum($l) if isnum($l);
    return "" if $modifier eq '~';
    return " " . $attrib . " is " . $data . " = " . $level if $modifier eq '=' && $d eq $l;
    return " " . $attrib . " is " . $data . " != " . $level if $modifier eq '!' && $d ne $l;
    return " " . $attrib . " is " . $data . " > " . $level if $modifier eq '>' && $d > $l;
    return " " . $attrib . " is " . $data . " < " . $level if $modifier eq '<' && $d < $l;
    return "";
} 

sub check_warncrit_thresholds {
    # loop to check if warning & critical attributes are ok
    my $statuscode = "OK";
    my $statusinfo = "";
    my $statusdata = "";
    my $chk;
    my $i;
    my @arv;
    my $vname;

    for ($i=0;$i<scalar(@o_critL);$i++) {
	$vname=$o_critL[$i]{'var'};
        if ($data_vars{$vname}{'type'} eq 'var' && $data_vars{$vname}{'from'} eq 'config') {
	    if (defined($data_vars{$o_attrL[$i]}{'data'})) {
		verb(2, "Processing critical expression '".$o_critL[$i]{'configstr'}."' to compare with $vname");
		if (!defined($o_critL[$i]{'data'}) && defined($o_critL[$i]{'expr'})) {
		    @arv=evaluate_expression($o_critL[$i]{'expr'});
		    if (scalar(@arv)==0) {
			print "Problem evaluating critical level expression '".$o_critL[$i]{'configstr'}."' - no data values returned\n";
			plugin_exit("UNKNOWN");
		    }
		    $o_critL[$i]{'data'}=$arv[0];
		}
	        if (defined($o_critL[$i]{'data'})) { # data can still be missing if ~ was used, in which case skip
		    if ($chk = check_value($vname,$data_vars{$vname}{'data'},$o_critL[$i]{'data'},$o_critL[$i]{'comp'})) {
		        $statuscode = "CRITICAL";
		        $statusinfo .= $chk;
		        $data_vars{$vname}{'out'}=1;
		    }
	        }
	    }
	    else {
	        $statusdata .= "," if ($statusdata);
	        $statusdata .= " $o_attrL[$i] data is missing";
		$data_vars{$vname}{'out'}=1
	    }
	}
    }

    for ($i=0;$i<scalar(@o_warnL);$i++) {
	$vname=$o_warnL[$i]{'var'};
        if ($data_vars{$vname}{'type'} eq 'var' && $data_vars{$vname}{'from'} eq 'config' && !defined($data_vars{$vname}{'out'})) {
	    if (defined($data_vars{$o_attrL[$i]}{'data'})) {
		verb(2, "Processing warning expression '".$o_warnL[$i]{'configstr'}."' to compare with $vname");
		if (!defined($o_warnL[$i]{'data'}) && defined($o_warnL[$i]{'expr'})) {
		    @arv=evaluate_expression($o_warnL[$i]{'expr'});
		    if (scalar(@arv)==0) {
			print "Problem evaluating warning level expression '".$o_warnL[$i]{'configstr'}."' - no data values returned\n";
			plugin_exit("UNKNOWN");
		    }
		    $o_warnL[$i]{'data'}=$arv[0];
		}
	        if (defined($o_warnL[$i]{'data'})) { # data can still be missing if ~ was used, in which case skip
		    if ($chk = check_value($vname,$data_vars{$vname}{'data'},$o_warnL[$i]{'data'},$o_warnL[$i]{'comp'})) {
		        $statuscode = "WARNING" if $statuscode ne "CRITICAL";
		        $statusinfo .= $chk;
		        $data_vars{$vname}{'out'}=1;
		    }
	        }
	    }
	    else {
	        $statusdata .= "," if ($statusdata);
	        $statusdata .= " $o_attrL[$i] data is missing";
		$data_vars{$vname}{'out'}=1
	    }
	}
    }

    for ($i=0;$i<scalar(@o_attrL);$i++) {
	if (defined($data_vars{$o_attrL[$i]}) && $data_vars{$o_attrL[$i]}{'type'} eq 'var' && $data_vars{$o_attrL[$i]}{'from'} eq 'config' && !defined($data_vars{$o_attrL[$i]}{'out'})) {
	    if (defined($data_vars{$o_attrL[$i]}{'data'})) {
		$statusdata .= "," if ($statusdata);
		$statusdata .= " " . $o_attrL[$i] . " is " . $data_vars{$o_attrL[$i]}{'data'} ; 
	    }
	    else {
	        $statusdata .= "," if ($statusdata);
	        $statusdata .= " $o_attrL[$i] data is missing";
	    }
	    $data_vars{$o_attrL[$i]}{'out'}=1
	}
    }
    for ($i=0;$i<scalar(@o_dispattrL);$i++) {
	if (defined($data_vars{$o_dispattrL[$i]}) && $data_vars{$o_dispattrL[$i]}{'type'} eq 'var' && $data_vars{$o_dispattrL[$i]}{'from'} eq 'config' && !defined($data_vars{$o_dispattrL[$i]}{'out'})) {
	    if (defined($data_vars{$o_dispattrL[$i]}{'data'})) {
		$statusdata .= "," if ($statusdata);
		$statusdata .= " " . $o_dispattrL[$i] . " is " . $data_vars{$o_dispattrL[$i]}{'data'} ; 
	    }
	    else {
	        $statusdata .= "," if ($statusdata);
	        $statusdata .= " $o_dispattrL[$i] data is missing";
	    }
	    $data_vars{$o_dispattrL[$i]}{'out'}=1
	}
    }

    return ($statuscode, $statusinfo, $statusdata);
}


################## DEFAULT SET OF BASE EXPRESSION OPERATORS ##################################

sub registerbasefunctions {
    basefunc_data_register();  # deals with numbers & other base data in expression, like '100' from expression 'a,100,*'
    basefunc_var_register();   # deals with retrieving value of another variable like. 'a' from expression 'a,100,*'
    basefunc_operatorplus_register();  # deals with '+' operator
    basefunc_operatorminus_register(); # deals with '-' operator
    basefunc_operatormult_register();  # deals with '*' operator
    basefunc_operatordiv_register();   # deals with '/' operator
    basefunc_operatorplus_stringadd_register(); # this overloads '+' operator but only works if one of the stack elements is not a number
    basefunc_operatordot_stringadd_register();  # this defined '.' operator only for strings
    basefunc_percent_register();	# deals with '%' operator
    function_round_register(); # deals with function 'round' that rounds float numbers
    function_snmp_register();  # deals with function that retrieves specified OID by SNMP
}

# Base 'data' function/operator - it copies argument into stack as is if its a number
sub basefunc_data_eval {
    my ($func_str, $stack) = @_;
    if ($func_str =~ /^'(.*)'$/ || $func_str =~ /^_data\((.*)\)$/) { unshift @{$stack}, $1; }
    else { unshift @{$stack}, $func_str; }
    return 1;
}
sub basefunc_data_register {
    my $def = {
	'id'   => '_data',
	'name' => '_data',
	'uid' => '_base_data0',
	'regex' => '[-|+]?(\d+\.?\d*)|[-|+]?(^\.\d+)|'."('.*')",
	'sub_eval' => \&basefunc_data_eval,
    };
    register_function($def);
}

# Base 'var' function/operator - it assumes argument is variable name and copies its 'data' into stack
sub basefunc_var_config {
    my $func_str = shift;
    return 1 if exists($data_vars{$func_str}) && $data_vars{$func_str}{'type'} eq 'var';
    return 1 if $func_str =~ /^__var\((.*)\)/;
    return 0;
}

sub basefunc_var_eval {
    my ($func_str, $stack) = @_;
    my $vname="";

    $vname=$1 if $func_str =~ /^__var\((.*)\)/;
    $vname=$func_str if !$vname;	    
    if (exists($data_vars{$vname}) && defined($data_vars{$vname}{'data'})) {
	unshift @{$stack},$data_vars{$vname}{'data'};
	return 1;
    }
    return 0;
}
sub basefunc_var_register {
    my $def = {
	'id'   => '__var',
	'name' => '_var',
	'uid'  => '_var_data0',
	'regex' => '.*', # anything matches
	'sub_config' => \&basefunc_var_config,
	'sub_eval' => \&basefunc_var_eval,
    };
    register_function($def);
}

# Base '+' operator
sub basefunc_operatorplus_eval {
    my ($func_str, $stack) = @_;
    if ($func_str eq '+' && isnum($stack->[0]) && isnum($stack->[1])) {
	$stack->[1] = tonum($stack->[1]) + tonum($stack->[0]);
	shift @{$stack};
	return 1;
    }
    return 0;
}
sub basefunc_operatorplus_register {
    my $def = {
	'id' => '_operator_plus',
	'name' => '_plus',
	'uid' => '_base_operator_plus0',
	'oper' => '+',
	'sub_eval' => \&basefunc_operatorplus_eval,
    };
    register_function($def);
}

# Base '-' operator
sub basefunc_operatorminus_eval {
    my ($func_str, $stack) = @_;
    if ($func_str eq '-' && isnum($stack->[0]) && isnum($stack->[1])) {
	$stack->[1] = tonum($stack->[1]) - tonum($stack->[0]);
	shift @{$stack};
	return 1;
    }
    return 0;
}
sub basefunc_operatorminus_register {
    my $def = {
	'id' => '_operator_minus',
	'name' => '_minus',
	'uid' => '_base_operator_minus0',
	'oper' => '-',
	'sub_eval' => \&basefunc_operatorminus_eval,
    };
    register_function($def);
}

# Base '*' operator
sub basefunc_operatormult_eval {
    my ($func_str, $stack) = @_;
    if ($func_str eq '*' && isnum($stack->[0]) && isnum($stack->[1])) {
	$stack->[1] = tonum($stack->[1]) * tonum($stack->[0]);
	shift @{$stack};
	return 1;
    }
    return 0;
}
sub basefunc_operatormult_register {
    my $def = {
	'id' => '_operator_mult',
	'name' => '_mult',
	'uid' => '_base_operator_mult0',
	'oper' => '*',
	'sub_eval' => \&basefunc_operatormult_eval,
    };
    register_function($def);
}

# Base '/' operator
sub basefunc_operatordiv_eval {
    my ($func_str, $stack) = @_;
    if ($func_str eq '/' && isnum($stack->[0]) && isnum($stack->[1])) {
	my $n0 = tonum($stack->[0]);
	my $n1 = tonum($stack->[1]);
	$stack->[1] = $n1 / $n0 if $n0 != 0;
	$stack->[1] = 0 if $n0 == 0; # strictly speaking this is wrong
	shift @{$stack};
	return 1;
    }
    return 0;
}
sub basefunc_operatordiv_register {
    my $def = {
	'id' => '_operator_div',
	'name' => '_div',
	'uid' => '_base_operator_div',
	'oper' => '/',
	'sub_eval' => \&basefunc_operatordiv_eval,
    };
    register_function($def);
}

# Operator to add strings - this overloads '+' but only works if one of the arguments is not a number
sub basefunc_operatorplus_stringadd_eval {
    my ($func_str, $stack) = @_;
    if ($func_str eq '+' && (!isnum($stack->[0]) || !isnum($stack->[1]))) {
	$stack->[1] = "$stack->[1]"."$stack->[0]";
	shift @{$stack};
	return 1;
    }
    return 0;
}
sub basefunc_operatorplus_stringadd_register {
    my $def = {
	'id' => '_operator_plus',
	'uid' => '_base_string_add0',
	'oper' => '+',
	'sub_eval' => \&basefunc_operatorplus_stringadd_eval,
    };
    register_function($def);
}

# Operator to add strings - this is more common "." with alias 'stradd' which will work even if argumets are a number
sub basefunc_operatordot_stringadd_eval {
    my ($func_str, $stack) = @_;
    if ($func_str eq '.') {
	$stack->[1] = "$stack->[1]"."$stack->[0]";
	shift @{$stack};
	return 1;
    }
    return 0;
}
sub basefunc_operatordot_stringadd_register {
    my $def = {
	'id'   => '_operator_dot',
	'name' => 'stradd',
	'uid' => '_base_string_add2',
	'oper' => '.',
	'sub_eval' => \&basefunc_operatordot_stringadd_eval,
    };
    register_function($def);
}

# SNMP Function/Operator - retrieves data from specified OID
sub func_snmp_config {
    my $in = shift;    
    if ($in =~ /snmp\((.*)\)/) {
	push @nets_oid_array, $1;
	$nets_oid_hash{$1}=undef;
	verb(2,"snmp oid $1 added to list of OIDs to be retrieved");
	return 1;
    }
    return 0;
}

sub func_snmp_eval {
    my ($func_str, $stack) = @_;
    if ($func_str =~ /snmp\((.*)\)/) {
	if (defined($nets_oid_hash{$1})) {
	    unshift @{ $stack }, ${$nets_oid_hash{$1}};
	    return 1;
        }
        else {
	    print "No data argument found for function $func_str\n";
        }
    }
    return 0;
}

sub func_snmp_proc {
    my ($session,$error,$resultat);

    # Connect to host
    if ( defined($o_login) && defined($o_passwd)) {
	# SNMPv3 login
	verb("SNMPv3 login");
	($session, $error) = Net::SNMP->session(
	      -hostname   	=> $o_host,
	      -version		=> '3',
	      -username		=> $o_login,
	      -authpassword	=> $o_passwd,
	      -authprotocol	=> 'md5',
	      -privpassword	=> $o_passwd,
	      -timeout          => $o_timeout
        );
    } 
    elsif (defined ($o_version2)) {
	# SNMPv2 Login
	($session, $error) = Net::SNMP->session(
	      -hostname  => $o_host,
	      -version   => 2,
	      -community => $o_community,
	      -port      => $o_port,
	      -timeout   => $o_timeout
        );
    } else {
        # SNMPV1 login
        ($session, $error) = Net::SNMP->session(
	      -hostname  => $o_host,
	      -community => $o_community,
              -port      => $o_port,
              -timeout   => $o_timeout
        );
    }

    if (!defined($session)) {
	printf("ERROR opening session: %s.\n", $error);
	plugin_exit("UNKNOWN");
    }

     # Get NetSNMP memory values 
     $resultat = (Net::SNMP->VERSION < 4) ?
		$session->get_request(@nets_oid_array)
		:$session->get_request(-varbindlist => \@nets_oid_array);

     # Exit with error if SNMP get_request failed  
     if (!defined($resultat)) {
	 printf("ERROR: snmp get_request failed - %s.\n", $session->error);
	 $session->close;
	 plugin_exit("UNKNOWN");
     }
	
     # Put SNMP data into %data_vars{'varname'}{'data'} - all done one line :)
     verb(1,"SNMP $_ = ".( ${$nets_oid_hash{$_}} = $$resultat{$_} )) foreach (@nets_oid_array);

     $session->close;
}

sub function_snmp_register {
    my $def = {
	'id'  => 'func_snmp',
	'name'=> 'snmp',
	'uid' => 'snmp0',
	'sub_config' => \&func_snmp_config,
	'sub_globalproc' => \&func_snmp_proc,
	'sub_eval' => \&func_snmp_eval,
    };
    register_function($def);
}

# Round function - assumes data is float number and rounds it to set number of digits before and after the '.'
sub func_round_eval {
    my ($func_str, $stack) = @_;
    if ($func_str =~ /round\((.*)\)/ && isnum($stack->[0])) {
	$stack->[0] = sprintf("%.$1f", tonum($stack->[0]));
	return 1;
    }
    return 0;
}

sub function_round_register {
    my $def = {
	'id'  => "func_round",
	'name' => 'round',
	'uid' => 'round0',
	'sub_eval' => \&func_round_eval,
    };
    register_function($def);
}

# Percent calculation function - this is optimization and is equivalent to doing "arg1,arg2,/,100,*,round(2),'%',+"
sub basefunc_percent_eval {
    my ($func_str, $stack) = @_;
    if ($func_str eq '%') {
	my $n0 = tonum($stack->[0]);
	my $n1 = tonum($stack->[1]);
	$stack->[1] = sprintf("%.2f%%", $n1*100/$n0) if $n0 != 0;
	$stack->[1] = '0%' if $n0 == 0;
	shift @{$stack};
	return 1;
    }
    return 0;
}

sub basefunc_percent_register {
    my $def = {
	'id' => '_operator_percent',
	'name' => 'percent',
	'uid' => 'percent0',
	'oper' => '%',
	'sub_eval' => \&basefunc_percent_eval,
    };
    register_function($def);
}
