#!/usr/local/bin/perl
#
# usage: 
#    check_printer_status [-v][-h ipaddr][-m model][-f config ]
#      [-d debug]
#	-v          - verbose mode
#	-h ipaddr   - printer ip address or DNS name
#	-m model    - model information
#	-f config - configuration file
#	-d debug - debug level
#
#
# Do SNMP query of printer; return PJL-style information for ifhp 
# (must have ifhp-3.5.8a3 or newer)
#
# The basic idea here is to extract all of the important OIDs
# and query them, rather than the brute-force approach of using
# snmpwalk.
#
# The configuration file has the entries and documentation
# on the OIDs used.
#
# The name of hte OID is lowercased and used as the output
# legent.
# To make things a bit simpler for pagecounting,  a list of OIDs 
# is flagged as the ones to return a pagecount.  These will
# 
# The program sits in a loop, checking for status.  If the
# alert_oid is defined, it assumes that the prtAlert (see
# Printer-MIB for details) is defined, and gets the various
# alerts and status messages.  If there is a critical alert
# it skips the status getting step.  Then the other OIDs
# are checked.  If any OID is not present on the device, it
# is removed from the list.  The device needs to have a minimum
# set of OIDs (printerstatus_oid, devicestatus_oid) for status
# to work.
#


use strict;
use Net::SNMP;
use Data::Dumper;
use Getopt::Std;
use IO::Select;
use FileHandle;

$| = 1;
# config file containing printer-specific information
my $config="/usr/libexec/filters/snmp_printer_status.conf";

# other variables
my $host;
my $model = 'default';
my $error;
my $response;
my $status;
my $verbose=0;
my %parms;
my $interval = 1;
my $start_time = time();
my $debug = 0;

sub parse_config( $ );
sub getstatus( $ $ );
$Data::Dumper::Indent = 1;

sub usage()
{
	print STDERR <<EOF;
use: $0 [-v][-t interval][-h ipaddr][-m model][-f config]
	-v          - verbose mode
	-t interval  - interval in secs between queries (default $interval)
	-h ipaddr   - printer ip address or DNS name
	-m model    - model information (default $model)
	-f config   - configuration file (default $config)
	-d debug    - debug level
EOF
	exit(1);
}

getopts("vpsd:f:h:m:t:", \%parms) or usage();
usage() if @ARGV ;
$verbose = 1 if $parms{v};

$host = $parms{h} if defined $parms{h};
$model = $parms{m} if defined $parms{m};
$config = $parms{f} if defined $parms{f};
$interval = $parms{t} if defined $parms{t};
$debug = $parms{d} if defined $parms{d};

if( not $host ){ print STDERR "$0: no host found\n"; exit(1); }
$host =~ s/\%.*//;
if( not $host ){ print STDERR "$0: no host found\n"; exit(1); }

print STDERR "Host: $host\tModel: $model\n" if $verbose;

# slurp in all printer-specific OID info, 

my $ref = parse_config($config);
delete $ref->{comment};
delete $ref->{comments};


# SNMP parameters
# Not sure why the following line doesn't work...I suspect something 
# strange in XML::Simple...Nate thinks it's in XML::Parser called from
# XML::Simple.

my $community=(substr($ref->{snmp_params}{community}, 0) || "public");
my $timeout=($ref->{snmp_params}{timeout} || 5);
my $retries=($ref->{snmp_params}{retries} || 3);
my $version=($ref->{snmp_params}{version} || 1);
my $session;
my $lastmessage = [];
my $label = "";
my $start = 0;
my $old = {};

foreach my $name ( keys %{$ref->{status}} ){
	print "$name:\n" if $verbose;
	my $hash = $ref->{status}{$name};
	foreach my $key (keys %$hash ){
		my $value = $hash->{$key};
		print STDERR "$key $hash->{$key}\n" if $verbose;
		$hash->{$value} = $key if $value;
	}
}

foreach my $name ( grep m/_oid$/, keys %{$ref->{printerdata}{default}} ){
	my $d;
	$ref->{printerdata}{default}{$name} = $d
		if ($d = $ref->{printerdata}{$model}{$name});
}

print STDERR "CONFIG " . Dumper($ref) if $verbose;


print STDERR "SNMP params: host $host, community $community, timeout $timeout, retries $retries\n" if $verbose;
($session, $error) = Net::SNMP->session(-hostname => $host,
					-community => $community,
					-timeout => $timeout,
					-version => $version,
					-retries => $retries);
if( $error ){
	print STDERR "Cannot create SNMP session '$error'\n";
	exit(1);
}


my $prtalertseveritylevel = $ref->{printerdata}{default}{alertseveritylevel};
my $prtalertdescription = $ref->{printerdata}{default}{alertdescription};
my $prtalertgroup = $ref->{printerdata}{default}{alertgroup};
my $pagecounters = $ref->{printerdata}{default}{pagecounters};
if( not $pagecounters ){
	print STDERR "$0: no pagecounters value in config\n";
	exit 1;
}
$pagecounters = [ split( ' ', $pagecounters) ];
my $required = $ref->{printerdata}{default}{required};
if( not $required ){
	print STDERR "$0: no required value in config\n";
	exit 1;
}
$required = [ split( ' ', $required) ];
 
while(1){
	if ($start){
		# be nice to the network
		my $s = IO::Select->new();
		$s->add(\*STDIN);
		my @ready;
		print STDERR "Starting wait\n" if $verbose;
		@ready = $s->can_read( $interval );
		print STDERR "End wait\n" if $verbose;
		while( @ready ){
			my $fd = shift(@ready);
			if( <$fd> ){
				print STDERR "Refresh\n" if $verbose;
				$lastmessage = [];
				$old = {};
			} else {
				print STDERR "Exit\n" if $verbose;
				exit(0);
			}
		}
	}
	$start = 1;
	my $alert_oid = ($ref->{printerdata}{default}{alert_oid} || "");
	my $critical_error = 0;
	my $change = 0;
	if( $alert_oid ){
		print STDERR "Running alert_oid get_table ($alert_oid)\n" if $verbose;
		$response = $session->get_table( -baseoid =>$alert_oid);
		if( !$response ){
			$status = $session->error();
			if( $status =~ /table is empty/i ){
				print STDERR "No OID alert_oid $alert_oid - $status\n" if $verbose;
				delete $ref->{printerdata}{default}{alert_oid};
			} elsif( not defined $old->{status} or $status ne $old->{status} ){
				my $now = (time - $start_time) . "- ";
				print STDERR "${now}STATUS= $status\n" if $verbose;
				print "STATUS= $status\n";
				$old->{status} = $status;
				$old->{pagecount_value} = "";
			}
			next;
		}
		print STDERR "printerstatus response: " . Dumper($response) if $verbose;
		my $v = {};
		foreach my $k (sort keys %$response ){
			my $r = $response->{$k};
			my($val, $index) = $k =~ m/^(.*)\.(\d+)$/;
			print STDERR "my $k = $r, '$val' $index\n" if $verbose;
			$v->{$val}->[$index] = $r;
		}
		my $n = @{$v->{$prtalertseveritylevel}};
		my $m = @{$lastmessage};
		$n = $m if $n < $m;
		print STDERR "decoded response: " . Dumper($v) if $verbose;
		for( my $i = 1; $i < $n; ++$i ){
			my $clear = 0;
			my $level = ($v->{$prtalertseveritylevel}->[$i] ||"" );
			$level = $label if ($label = $ref->{status}{alertseveritylevel}{$level});
			$critical_error = 1 if( $level eq 'critical' );
			print STDERR "LEVEL $level\n" if $verbose;
			my $alertgroup = ($v->{$prtalertgroup}->[$i] || "");
			$alertgroup = $label if ($label = $ref->{status}{alertgroup}{$alertgroup});
			my $warning = ($v->{$prtalertdescription}->[$i] || "");
			my $out = "";
			if( $critical_error ){
				$out = "STATUS= level: $level, group: $alertgroup, message: $warning" if( $warning );
			} else {
				$out = "WARNING= level: $level, group: $alertgroup($i), message: $warning" if( $warning );
			}
			my $last = ($lastmessage->[$i] || "");
			next if( $out eq $last );
			if( not $warning ){
				if( $last ){
					$out = $last . " CLEARED";
				} else {
					$out = "";
				}
				$clear = 1;
			}
			if( $out ){
				my $now = (time - $start_time) . "- ";
				print STDERR "${now}${out}\n" if $verbose;
				print $out . "\n";
				$change = 1;
			}
			if( $clear ){
				$lastmessage->[$i] = "";
			} else {
				$lastmessage->[$i] = $out;
			}
		}
		print "CRITICAL $critical_error\n" if $verbose;
		if( $critical_error or $change ){
			$old->{pagecount_value} = "";
		}
	}
	# status is irrelevant while error
	next if( $critical_error );

	$error = 0;
	foreach my $key (keys %{$ref->{printerdata}{default}} ){
		next if $key !~ /_oid$/;
		next if $key =~ /alert_oid/;
		$error |= getstatus( $ref, $key );
	}
	next if $error;

	foreach my $id (@{$required}){
		if( not $ref->{printerdata}{default}{$id} ){
			print STDERR "Device does not support $id - may not be a printer\n";
			exit 1;
		}
	}

	my $found = 0;
	my $pc = "" ;
	my $old_pc = $old->{pagecount_value};
	$old_pc = "" if not defined $old_pc;
	foreach my $pr (@$pagecounters){
		print "checking $pr\n" if $debug;
		if( defined $ref->{printerdata}{default}{$pr} ){
			$pc = $old->{$pr};
			$pc = "" if not defined $pc;
			print "value $pr = '$pc'\n" if $debug;
			$found = 1;
			last;
		}
	}
	$old->{pagecount_value} = $pc;

	if( not $found ){
		print STDERR "Device does not have support for any of " . join (", ",@$pagecounters) . "\n";
		exit 1;
	}

	print "PAGECOUNT= $old_pc\n" if( $old_pc ne "" );
}

# all done
$session->close;


# exit with JSUCC
$status = 0;
print STDERR "Exit status: 0\n" if $verbose;
exit $status;

sub getstatus( $ $ ){
	my ($ref, $name ) = @_;
	my $oid= $ref->{printerdata}{default}{$name};
	my $legend = $name;
	$legend =~ s/_oid$//;
	$legend =~ tr/a-z/A-Z/;

	$old->{$oid} = "";
	return if( not defined $oid or $oid !~ m/^\./ );
	print STDERR "Running get_table '${name}' oid '$oid' $legend\n" if $verbose;
	$response = $session->get_table( -baseoid =>$oid);
	if( !$response ){
		$status = $session->error();
		if( $status =~ /no response from host/i ){
			if( $status ne $old->{$status} ){
				print "STATUS= $status\n";
			}
			$old->{status} = $status;
			return 1;
		}
		print STDERR "Running ${name} get_request ($oid)\n" if $verbose;
		$response = $session->get_request( -varbindlist =>[$oid]);
	}
	if( !$response ){
		$status = $session->error();
		if( $status =~ /noSuchName/i ){
			print STDERR "No OID $name $oid - $status\n" if $verbose;
			delete $ref->{printerdata}{default}{$name};
			return 0;
		}
		print STDERR "OID $name $oid - $status\n" if $verbose;
		if( not defined $old->{status} or $status ne $old->{status} ){
			my $now = (time - $start_time) . "- ";
			print STDERR "${now}STATUS= $status\n" if $verbose;
			print "STATUS= $status\n";
			$old->{status} = $status;
			$old->{pagecount_value} = "";
		}
		return 1;
	}
	print STDERR "${name} response: " . Dumper($response) if $verbose;
	my $value = "";
	foreach (keys %$response){
		$value = $response->{$_};
	}
	$value = $label if ($label = $ref->{status}{$name}{$value});
	if( ${value} ne ($old->{$name} || "") ){
		$old->{pagecount_value} = "";
		print "CHANGE NAME '$name' OLD '$old->{$name}' NEW '$value'\n" if $debug > 1;
	}
	$old->{$name} = ${value};
	if( $name ne "pagecount_oid" ){
		print "${legend}= ${value}\n";
	}
	return 0;
}

sub parse( $ $ $ ){
	my( $ref, $key, $line ) = @_;
	my $top = $ref;
	print "key '$key', line '$line'\n" if $debug> 2;
	my @list = split(' ', $key );
	if( @list == 0 ){
		print STDERR "bad entry '[ $key ]'\n";
		exit(1);
	}
	if( @list == 1 and $list[0] =~ /comment/ ){
		$ref->{$list[0]} = $line;
		return( $ref );
	}
	my @line;
	$line =~ s/\n/ /g;
	$line =~ s/(\w+)\s*=\s*"([^"]*)"/push @line,"$1=$2"; ""/eg;
	$line =~ s/^\s*//;
	$line =~ s/\s*$//;
	#print "LINE '" . join("' '", @line) . "'\n";
	#print "LEFT '$line'\n";
	if( $line ne "" ){
		print STDERR "bad entry '$line'\n";
		exit 1;
	}
	foreach my $id ( @list ){
		if( not defined  $top->{$id} ){
			$top->{$id} = {};
		}
		$top = $top->{$id};
	}
	foreach my $id ( @line ){
		my($key,$value) = split( '=', $id, 2);
		$top->{$key} = $value;
	}
	print "parse: " . Dumper($ref) . "\n" if $debug > 1;
	return( $ref );
}

sub parse_config( $ ){
	my ($config) = @_;

	if( not defined $config ){
		print STDERR "$0: no config file";
		exit 1;
	}
	my $fh = new FileHandle "<$config" or die  "$0: cannot open config file '$config'";
	my $ref = {};
	my ($key, $line);
	while( <$fh> ){
		print "key '$key', line '$line'\n" if $debug > 2;
		next if( /^\s*#/ );
		if( /^\[\s*([^\]]*)\s*]/ ){
			parse( $ref, $key, $line ) if( $key );
			$line = "";
			$key = $1;
		} else {
			$line .= $_;
		}
	}
	parse( $ref, $key, $line ) if( $key );
	print "parse_config: " . Dumper($ref) . "\n" if $debug;
	return( $ref );
}
