#!/usr/bin/perl -w
#
# Copyright 2003, 2004 Massachusetts Institute of Technology
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that both the above copyright notice and this
# permission notice appear in all copies, that both the above
# copyright notice and this permission notice appear in all
# supporting documentation, and that the name of M.I.T. not be used
# in advertising or publicity pertaining to distribution of the
# software without specific, written prior permission.  M.I.T. makes
# no representations about the suitability of this software for any
# purpose.  It is provided "as is" without express or implied
# warranty.
#
# THIS SOFTWARE IS PROVIDED BY M.I.T. ``AS IS''.  M.I.T. DISCLAIMS
# ALL EXPRESS OR IMPLIED WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT
# SHALL M.I.T. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
use constant OTHER => 1;
use constant CONTAINER => 5;
use constant SENSOR => 8;
use constant MODULE => 9;
use strict;
use vars qw(%cisco_scale @cisco_units %cisco_relation %cisco_severity %opt $s);

use Carp;
use Getopt::Std;
use SNMP;
use Tree::DAG_Node;

getopts('c:l:Su:v:', \%opt);

%cisco_scale
    = (1 => 1e-24, 2 => 1e-21, 3 => 1e-18, 4 => 1e-15, 5 => 1e-12, 6 => 1e-9,
       7 => 1e-9, 8 => 1e-3, 9 => 1, 10 => 1e3, 11 => 1e6, 12 => 1e9,
       13 => 1e12, 14 => 1e15, 15 => 1e18, 16 => 1e21, 17 => 1e21);

@cisco_units = (undef, 'other', 'unknown', 'VAC', 'VDC', 'A', 'W', 'Hz',
		'degC', '%RH', 'RPM', 'm^3/min', 'truthvalue', 'enumeration');

# Used to begin a sentence so capitalize.
%cisco_severity
    = (1 => 'Other', 10 => 'Minor', 20 => 'Major');

%cisco_relation
    = (1 => '<', 2 => '<=', 3 => '>', 4 => '>=', 5 => '==', 6 => '!=');

sub iid ($) { return $_[0]->[1]; }
sub val ($) { return $_[0]->[2]; }

sub maketree () {
    #
    # Make sure we have the right MIBs loaded even if the user doesn't
    # load everything by default.  It does not appear that net-snmp will
    # complain if you try to load a module that doesn't exist, but things
    # will probably get unhappy below if we can't translate the MIB objects
    # we need.
    #
    &SNMP::loadModules('ENTITY-MIB', 'CISCO-ENTITY-SENSOR-MIB',
		       'CISCO-ASSET-ENTITY-MIB', 'CISCO-ENTITY-EXT-MIB');
    $s = new SNMP::Session(DestHost => $ARGV[0],
			   AuthProto => 'SHA',
			   (exists $opt{c} ? (Community => $opt{c}) : ()),
			   (exists $opt{v} ? (Version => $opt{v}) : ()),
			   (exists $opt{l} ? (SecLevel => $opt{l}) : ()),
			   (exists $opt{u} ? (SecName => $opt{u}) : ()))
	or die "SNMP::Session->new: $!\n";

    my (%ents);

    my ($cl) = $s->bulkwalk(0, 250, [['entPhysicalClass']]);
    if (!defined $cl) {
	die "bulkwalk: $s->{ErrorStr}\n";
    }

    # entPhysicalClass is mandatory for all entities, so we can use it
    # to figure out all of the instances.  Create each node as a trivial
    # tree to be glued together later.
    foreach my $class (@$cl) {
	my ($node) = new Tree::DAG_Node;
	$ents{iid $class} = $node;
	$node->attributes->{Class} = val $class;
    }

    # Now turn all of the other columns into attributes on each node.
    my (@cols) = qw(Descr Name VendorType ContainedIn ParentRelPos HardwareRev
		    FirmwareRev SoftwareRev SerialNum MfgName ModelName
		    Alias AssetID IsFRU);
    my (%nullable) = (Alias => 1, Name => 1, Descr => 1, SerialNum => 1,
		      MfgName => 1, ModelName => 1, HardwareRev => 1,
		      FirmwareRev => 1, SoftwareRev => 1, AssetID => 1);

col:
    foreach my $column (@cols) {
	my ($rows) = $s->bulkwalk(0, 250, [["entPhysical$column"]]);
row:
	foreach my $row (@$rows) {
	    next row if (exists($nullable{$column}) and val($row) eq '');
	    if (!exists($ents{iid $row})) {
		warn "Got entPhysical$column." . iid($row) . " but not entPhysicalClass." . iid($row) . "\n";
		next row;
	    }
	    $ents{iid $row}->attributes->{$column} = val $row;
	}
    }

    foreach my $index (keys %ents) {
	my ($node) = $ents{$index};
	$node->attributes->{Index} = $index;

	# Set up node names...
	if (exists $node->attributes->{Descr}) {
	    $node->name($node->attributes->{Descr});
	} elsif (exists $node->attributes->{Name}) {
	    $node->name($node->attributes->{Name});
	} else {
	    $node->name("[$index]");
	}

	# Build tree
	if ($node->attributes->{ContainedIn} != 0) {
	    my ($parent) = $ents{$node->attributes->{ContainedIn}};
	    if (defined $parent) {
		my (@siblings) = $parent->daughters;
		my ($relpos) = $node->attributes->{ParentRelPos};

		# Preserve ParentRelPos ordering without making gaps.
		push(@siblings, $node);
		@siblings = (sort childsort @siblings);
		$parent->set_daughters(@siblings);
	    } else {
		warn "Entity $index: invalid entPhysicalContainedIn\n";
	    }
	}
    }

    # Doesn't matter which one we use to find the root.  We could have
    # figured it out above but this is easier.
    my ($root) = $ents{(keys %ents)[0]}->root;

    viztree($ARGV[0], $root);
}

sub childsort {
    my ($rv) = ($a->attributes->{Class} <=> $b->attributes->{Class});
    if ($rv == 0) {
	return ($a->attributes->{ParentRelPos} 
		<=> $b->attributes->{ParentRelPos});
    }
    return $rv;
}	

sub viztree ($$) {
    my ($name, $root) = @_;

    print "Physical entities of \"$name\":\n";
    walktree("", 0, $root);
}

sub walktree ($$$) {
    my ($prefix, $morekids, $node) = @_;
    my ($cont) = $morekids ? " | " : "   ";

    return if ($node->attributes->{Class} == &SENSOR and exists($opt{S}));

    my ($label) = join("\n$prefix   ", split(/\n/, nodelabel($node)));
    printf STDOUT ("%s%-3d%s\n", $prefix, 
		   $node->attributes->{ParentRelPos} || 0.0,
		   $label, "\n");

    my (@children) = $node->daughters;
    for (my ($i) = 0; $i <= $#children; $i++) {
	walktree($prefix . "   ", $i == $#children, $children[$i]);
    }
    print "\n" if ($node->attributes->{Class} == &CONTAINER);
}

sub nodelabel ($) {
    my ($node) = @_;
    
    my ($label) = $node->name;
    my ($attr) = $node->attributes;

    # We used Name in preference to Descr, so if both are set, give
    # description first.
# Not any more....
#    if (exists $attr->{Name} and exists $attr->{Descr}
#	and $attr->{Name} ne $attr->{Descr}) {
#	$label .= "\n$attr->{Descr}";
#    }
    if ($attr->{IsFRU} == 1) {
	$label .= ' (FRU)';
    }
    if ($attr->{VendorType} =~ /^\Q.1.3.6.1.4.1.9.12.3.1.\E/) {
	if ($attr->{Class} == &SENSOR) {
	    $label .= "\n" . ciscosensor($node->attributes->{Index});
	} elsif ($attr->{Class} == &OTHER) {
	    my ($ram) = $s->get(['ceExtProcessorRam', $attr->{Index}]);
	    if (defined($ram) and $ram ne '' and $ram ne 'NOSUCHINSTANCE') {
		$label .= "\nInstalled RAM: $ram bytes";
	    }
	    $ram = $s->get(['ceExtNVRAMSize', $attr->{Index}]);
	    my ($used) = $s->get(['ceExtNVRAMUsed', $attr->{Index}]);
	    if (defined($ram) and defined($used) and $ram ne '' and $used ne ''
		and $ram ne 'NOSUCHINSTANCE' and $used ne 'NOSUCHINSTANCE') {
		$label .= "\nInstalled NVRAM: $ram bytes ($used used)";
	    }
	} elsif ($attr->{Class} == &MODULE) {
	    my ($swid) = $s->get(['ceAssetSoftwareID', $attr->{Index}]);
	    if (defined($swid) and $swid ne '' and $swid ne 'unknown') {
		$label .= "\nRunning software $swid";
	    }
	}
    }
    if (exists $attr->{Alias}) {
	$label .= "\nAlias: $attr->{Alias}";
    }
    if (exists $attr->{MfgName} and exists $attr->{ModelName}) {
	$label .= "\n$attr->{MfgName} $attr->{ModelName}";
    }
    if (exists $attr->{HardwareRev} or exists $attr->{FirmwareRev}
	or exists $attr->{SoftwareRev}) {
	my ($space) = '';

	$label .= "\n";
	if (exists $attr->{HardwareRev}) {
	    $label .= "HW $attr->{HardwareRev}";
	    $space = ', ';
	}
	if (exists $attr->{FirmwareRev}) {
	    $label .= $space . "FW $attr->{FirmwareRev}";
	    $space = ', ';
	}
	if (exists $attr->{SoftwareRev}) {
	    $label .= $space . "SW $attr->{SoftwareRev}";
	}
    }
    if (exists $attr->{SerialNum}) {
	$label .= "\nS/N $attr->{SerialNum}";
    }
    if (exists $attr->{AssetID}) {
	$label .= "\nAsset Tag $attr->{AssetID}";
    }
    return $label;
}

my ($boottime);

sub tt2time ($) {
    my ($ticks) = @_;

    unless (defined $boottime) {
	my ($sut) = $s->get('sysUpTime.0');
	$boottime = time() - $sut / 100.0;
    }
    return $boottime + $ticks / 100.0;
}

sub ciscosensor ($) {
    my ($iid) = @_;

    my ($type, $scale, $precision, $value, $status, $valuets, $valueupd)
	= $s->get([['entSensorType', $iid], ['entSensorScale', $iid],
		   ['entSensorPrecision', $iid], ['entSensorValue', $iid],
		   ['entSensorStatus', $iid], 
		   ['entSensorValueTimeStamp', $iid],
		   ['entSensorValueUpdateRate', $iid]]);
    if (!defined($type) or $type eq 'NOSUCHINSTANCE') {
#	warn "unable to retrieve Cisco sensor information for entity $iid\n";
	return "";
    }

    if ($status == 2) {
	return "- sensor value is not available";
    } elsif ($status == 3) {
	return "- sensor is not operational";
    }

    my ($formatted) = ciscosensorvalue($type, $value, $scale, $precision);
    if ($valueupd != 0) {
	$valuets = localtime(int(tt2time($valuets)));
	$formatted .= " (updated every $valueupd s, last $valuets)";
    }

    # Look for sensor thresholds...
    my ($sev, $rel, $val, $eval)
	= $s->bulkwalk(0, 10, [['entSensorThresholdSeverity', $iid],
			       ['entSensorThresholdRelation', $iid],
			       ['entSensorThresholdValue', $iid],
			       ['entSensorThresholdEvaluation', $iid]]);

    if (!defined($sev) or $#$sev < 0) {
	# No thresholds defined for this entity.
	return $formatted;
    }

    for (my ($i) = 0; $i <= $#$sev; $i++) {
	$formatted 
	    .= ("\n- " . ($cisco_severity{val $sev->[$i]} || val $sev->[$i])
		. " threshold: sensor value "
		. $cisco_relation{val $rel->[$i]} . " "
		. ciscosensorvalue($type, val $val->[$i], $scale, $precision)
		. " (currently " 
		. (val($eval->[$i]) == 1 ? "true)" : "false)"));
    }
    return '- ' . $formatted;
}

sub ciscosensorvalue ($$$$) {
    my ($type, $value, $scale, $precision) = @_;

    my $formatted;
    if ($type == 1) {
	$formatted = "other: $value";
    } elsif ($type == 2) {
	$formatted = "unknown: $value";
    } elsif ($type == 12) {
	$formatted = ($value == 1 ? "true" : "false");
    } elsif ($type == 13) {
	$formatted = "enumeration: $value";
    } else {
	if (exists $cisco_scale{$scale}) {
	    $value *= $cisco_scale{$scale};
	}
	if ($precision > 0) {
	    $value /= 10**$precision;
	} elsif ($precision < 0) {
	    $precision = 0;	# XXX how to deal with this case?
	}
	$formatted = sprintf("%.*f ", $precision, $value);
	$formatted .= $cisco_units[$type];

    }
    return $formatted;
}

maketree();