#!/cw/bin/perl ## ## OSSP snmpdx -- SNMP Daemon Extension ## Copyright (c) 2003 The OSSP Project ## Copyright (c) 2003 Ralf S. Engelschall ## Copyright (c) 2003 Cable & Wireless ## ## Permission to use, copy, modify, and distribute this software for ## any purpose with or without fee is hereby granted, provided that ## the above copyright notice and this permission notice appear in all ## copies. ## ## THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED ## WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF ## MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. ## IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR ## CONTRIBUTORS 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. ## # requirements require 5; use strict; use warnings; use FindBin; use Getopt::Long; use IO; # program information my $progname = "snmpdx"; my $progvers = "0.1.0"; ## _________________________________________________________________________ ## ## HELPER CLASS: LOGFILE WRITING ## _________________________________________________________________________ ## package My::Log; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(new destroy DESTROY write); sub new ($$) { my $proto = shift @_; my $class = ref $proto || $proto; my $self = {}; bless ($self, $class); my $level = shift @_ || die "missing level argument"; my $filename = shift @_ || die "missing filename argument"; $self->{-level} = $level; $self->{-io} = new IO::File ">>$filename" or die "cannot open logfile \"$filename\" for writing"; $self->{-io}->autoflush(1); return $self; } sub destroy ($) { my ($self) = @_; $self->{-io}->close() if (defined($self->{-io})); return; } sub DESTROY ($) { my ($self) = @_; $self->destroy; return; } sub printf ($;@) { my $self = shift @_; my $level = shift @_ || die "missing level argument"; my $fmt = shift @_ || die "missing format argument"; if ($self->{-level} >= $level) { my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time()); my $id = sprintf("[%04d-%02d-%02d/%02d:%02d:%02d@%05d] ", $year+1900, $mon+1, $mday, $hour, $min, $sec, $$); $self->{-io}->printf($id . $fmt . "\n", @_); } return; } ## _________________________________________________________________________ ## ## HELPER CLASS: VALUE CACHING ## _________________________________________________________________________ ## package My::Cache; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(new destroy DESTROY store fetch); sub new ($) { my ($proto) = @_; my $class = ref $proto || $proto; my $self = {}; bless ($self, $class); $self->{-cache} = {}; return $self; } sub destroy ($) { my ($self) = @_; return; } sub DESTROY ($) { my ($self) = @_; $self->destroy; return; } sub store ($$$$) { my ($self, $id, $expires, $value) = @_; die "invalid id" if (not defined($id) or $id eq ''); if ($expires =~ m|^(\d+)[sS]?$|) { $expires = $1; } elsif ($expires =~ m|^(\d+)[mM]$|) { $expires = $1 * 60; } elsif ($expires =~ m|^(\d+)[hH]$|) { $expires = $1 * 60 * 60; } elsif ($expires =~ m|^(\d+)[dD]$|) { $expires = $1 * 60 * 60 * 24; } elsif ($expires =~ m|^(\d+)[wW]$|) { $expires = $1 * 60 * 60 * 24 * 7; } elsif ($expires =~ m|^forever$|i) { $expires = 99999 * 365 * 24 * 60 * 60; } else { die "invalid expire time"; } $expires = time() + $expires; $self->{-cache}->{$id} = { -expires => $expires, -value => $value }; return; } sub fetch ($$) { my ($self, $id) = @_; my $value = undef; if (defined($self->{-cache}->{$id})) { if ($self->{-cache}->{$id}->{-expires} > time()) { $value = $self->{-cache}->{$id}->{-value}; } else { undef $self->{-cache}->{$id}; } } return $value; } ## _________________________________________________________________________ ## ## HELPER CLASS: RUNNING SYSTEM COMMANDS ## _________________________________________________________________________ ## package My::System; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(new destroy DESTROY run); sub new ($;$) { my ($proto, $log, $cache) = @_; my $class = ref $proto || $proto; my $self = {}; bless ($self, $class); $self->{-log} = $log; $self->{-cache} = (defined($cache) ? $cache : new My::Cache); return $self; } sub destroy ($) { my ($self) = @_; $self->{-cache}->destroy() if (defined($self->{-cache})); return; } sub DESTROY ($) { my ($self) = @_; $self->destroy; return; } sub run ($$;$) { my ($self, $cmd, $expires) = @_; $expires = "1m" if (not defined($expires)); my $result = $self->{-cache}->fetch($cmd); if (defined($result)) { $self->{-log}->printf(4, "system: run: \"$cmd\" ($expires) [CACHE HIT]"); } else { $self->{-log}->printf(3, "system: run: \"$cmd\" ($expires) [CACHE MISS]"); $self->{-log}->printf(5, "system: executing command: \"$cmd\""); $result = { -stdout => '', -rv => 0 }; $result->{-stdout} = `$cmd 2>/dev/null`; $result->{-rv} = ($? >> 8); $self->{-log}->printf(6, "system: return value: ". $result->{-rv}); $self->{-cache}->store($cmd, $expires, $result); } return $result; } ## _________________________________________________________________________ ## ## HELPER CLASS: PLATFORM IDENTIFICATION ## _________________________________________________________________________ ## package My::Platform; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(new destroy DESTROY id); sub new ($) { my ($proto) = @_; my $class = ref $proto || $proto; my $self = {}; bless ($self, $class); $self->{-machine} = `(uname -m) 2>/dev/null`; $self->{-machine} = `(uname -p) 2>/dev/null` if ($self->{-machine} eq ''); $self->{-machine} =~ s|^\s*(.+?)\s*$|$1|s; $self->{-system} = `(uname -s) 2>/dev/null`; $self->{-system} =~ s|^\s*(.+?)\s*$|$1|s; $self->{-release} = `(uname -r) 2>/dev/null`; $self->{-release} = `(uname -v) 2>/dev/null` if ($self->{-release} eq ''); $self->{-release} =~ s|^\s*(.+?)\s*$|$1|s; return $self; } sub destroy ($) { my ($self) = @_; return; } sub DESTROY ($) { my ($self) = @_; $self->destroy; return; } sub id ($) { my ($self) = @_; return sprintf("%s-%s-%s", $self->{-machine}, $self->{-system}, $self->{-release}); } ## _________________________________________________________________________ ## ## HELPER CLASS: MIB OID/NAME MAPPING ## _________________________________________________________________________ ## package My::MIB; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(new destroy DESTROY oid2name name2oid oid2type); sub new ($$$) { my ($proto, $bindir, $mibdir, $mib) = @_; my $class = ref $proto || $proto; my $self = {}; bless ($self, $class); $self->{-oid2name} = {}; $self->{-name2oid} = {}; my @entries = `$bindir/snmptranslate -M $mibdir -m $mib -Tl`; foreach my $entry (@entries) { $entry =~ s|\n$||s; my $name = $entry; my $oid = ''; $name =~ s|\((\d+)\)|$oid .= ".$1", ''|sge; my $out = `$bindir/snmptranslate -M $mibdir -m $mib -Td $oid`; if ($out =~ m|\s+MAX-ACCESS\s+not-accessible\s*|si) { next; } my $type; if ($out =~ m|TEXTUAL\s+CONVENTION\s+(.+?)\n\s+SYNTAX|si) { $type = $1; } elsif ($out =~ m|SYNTAX\s+(.+?)\s*\n|si) { $type = $1; $type =~ s|\s+\([^\)]+\)$||s; } else { next; } if ($name =~ m|[^.]+TABLE\.[^.]+ENTRY\.[^.]+$|) { $name .= ".#"; $oid .= ".#"; } $self->{-oid2type}->{$oid} = $type; $self->{-oid2name}->{$oid} = $name; $self->{-name2oid}->{$name} = $oid; } return $self; } sub destroy ($) { my ($self) = @_; return; } sub DESTROY ($) { my ($self) = @_; $self->destroy; return; } sub oid2name ($$) { my ($self, $oid) = @_; return $self->{-oid2name}->{$oid}; } sub name2oid ($$) { my ($self, $name) = @_; return $self->{-name2oid}->{$name}; } sub oid2type ($$) { my ($self, $oid) = @_; return $self->{-oid2type}->{$oid}; } sub oids ($;@) { my ($self, @patterns) = @_; my $oids = []; foreach my $pattern (@patterns) { $pattern = "*" if (not defined($pattern)); $pattern =~ s/\./\\./sg; $pattern =~ s/\*/.*/sg; foreach my $name (keys(%{$self->{-name2oid}})) { if ($name =~ m|^${pattern}$|si) { push(@{$oids}, $self->{-name2oid}->{$name}); } } } return $oids; } ## _________________________________________________________________________ ## ## HELPER CLASS: PROBE (abstract only) ## _________________________________________________________________________ ## package My::Probe; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(new destroy DESTROY oids probe); sub new ($$) { my ($proto, $ctx) = @_; my $class = ref $proto || $proto; my $self = {}; bless ($self, $class); $self->{-ctx} = $ctx; return $self; } sub destroy ($) { my ($self) = @_; return; } sub DESTROY ($) { my ($self) = @_; $self->destroy; return; } sub oids ($) { my ($self) = @_; return []; } sub probe ($$$) { my ($self, $ctx, $oid) = @_; return {}; } ## _________________________________________________________________________ ## ## HELPER CLASS: Encoding ## _________________________________________________________________________ ## package My::Enc; require Exporter; our @ISA = qw(Exporter); our @EXPORT_OK = qw(new destroy DESTROY dat_encode dat_decode octet_encode octet_decode); sub new ($$) { my ($proto) = @_; my $class = ref $proto || $proto; my $self = {}; bless ($self, $class); return $self; } sub destroy ($) { my ($self) = @_; return; } sub DESTROY ($) { my ($self) = @_; $self->destroy; return; } # ASN.1 DateAndTime encoding/decoding sub dat_encode ($$) { my ($self, $asc) = @_; my ($y, $m, $d, $H, $M, $S) = (0, 0, 0, 0, 0, 0); if ($asc =~ m|^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$|) { ($y, $m, $d, $H, $M, $S) = ($1, $2, $3, $4, $5, $6); } return pack("nCCCCCCCCC", $y, $m, $d, $H, $M, $S, 0, 0, 0, 0); } sub dat_decode ($$) { my ($self, $bin) = @_; my ($y, $m, $d, $H, $M, $S) = unpack("nCCCCCCCCC", $bin); return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $y, $m, $d, $H, $M, $S); } # snmpd(8) pass_persist protocol 'octet' encoding/decoding sub octet_encode ($$) { my ($self, $bin) = @_; my $asc = ''; $bin =~ s|(.)|$asc .= sprintf("%02x ", ord($1)), ''|sge; return $asc; } sub octet_decode ($$) { my ($self, $asc) = @_; my $bin = ''; $asc =~ s|([0-9a-zA-Z]{2})\s*|$bin .= chr(hex($1)), ''|sge; return $bin; } ## _________________________________________________________________________ ## ## MAIN PROCEDURE ## _________________________________________________________________________ ## package main; # find path to ourself my $myroot = "$FindBin::Bin"; # parameters (defaults) my $config = "$myroot/snmpdx.cfg"; my $version = 0; my $help = 0; my $tmpdir = ($ENV{TMPDIR} || $ENV{TEMPDIR} || "/tmp"); my $get = 0; my $next = 0; my $bindir = "/cw/bin"; my $probedir = "$myroot/snmpdx.d"; my $probename = "*"; my $mibdir = "$myroot/snmpdx.mib"; my $mibname = "snmpdx"; my $logfile = "$myroot/snmpdx.log"; my $loglevel = 9; # command line parsing my %options = ( 'c|config=s' => \$config, 'V|version' => \$version, 'h|help' => \$help, 't|tmpdir=s' => \$tmpdir, 'g|get' => \$get, 'n|next' => \$next, 'b|bindir=s' => \$bindir, 'P|probedir=s' => \$probedir, 'p|probename=s' => \$probename, 'M|mibdir=s' => \$mibdir, 'm|mibname=s' => \$mibname, 'l|logfile=s' => \$logfile, 'L|loglevel=i' => \$loglevel, ); Getopt::Long::Configure("bundling"); my $result = GetOptions(%options) || die "option parsing failed"; if ($help) { print "Usage: $progname [options] [SPECFILE ...]\n" . "Available options:\n" . " -c,--config=PATH read command line options from configuration fil\n" . " -V,--version print program version\n" . " -h,--help print out this usage page\n" . " -t,--tmpdir=PATH filesystem path to temporary directory\n" . " -g,--get=MIBOID get value of this MIB OID\n" . " -n,--next=MIBOID get value of next MIB OID\n" . " -b,--bindir=PATH path to the net-snmp binaries\n" . " -P,--probedir=PATH path to probe directory\n" . " -p,--probename=NAME the pattern for probes to load\n" . " -M,--mibdir=PATH path to MIB directory\n" . " -m,--mibname=MIB the name of the MIB to act under\n" . " -l,--logfile=PATH path to daemon logfile\n" . " -L,--loglevel=NUM logging level (0...9)\n"; exit(0); } if ($version) { print "$progname $progvers\n"; exit(0); } # support external configuration file if (-f $config) { my $cfg = new IO::File "<$config"; my $line; while (defined($line = $cfg->getline())) { $line =~ s|\r?\n$||s; next if ($line =~ m/^\s*(#.*)?$/s); my ($option, $value) = ($line =~ m|^\s*(\S+)\s+(.+?)\s*$|s); die "invalid configuration line \"$line\"" if (not defined($option) or not defined($value)); my ($var) = map { $options{$_} } grep { $_ =~ m/^.\|\Q$option\E(=[si])?$/s } keys(%options); die "invalid configuration option \"$option\"" if (not defined($var)); ${$var} = $value; } $cfg->close; } # create daemon run-time context my $ctx = {}; $ctx->{-log} = new My::Log ($loglevel, $logfile); $ctx->{-cache} = new My::Cache; $ctx->{-sys} = new My::System ($ctx->{-log}, $ctx->{-cache}); $ctx->{-platform} = new My::Platform; $ctx->{-log}->printf(1, "startup %s %s (%s)", $progname, $progvers, $ctx->{-platform}->id()); $ctx->{-mib} = new My::MIB ($bindir, $mibdir, $mibname); $ctx->{-enc} = new My::Enc; # exception handling support $SIG{__DIE__} = sub { my ($err) = @_; $err =~ s|\r?\n$||s; $ctx->{-log}->printf(1, "ERROR: $err ". ($! ? "($!)" : "")); undef $ctx; exit(1); }; $SIG{__WARN__} = sub { my ($err) = @_; $err =~ s|\r?\n$||s; $ctx->{-log}->printf(1, "WARNING: $err"); return; }; # PROTOCOL MIB MIB TYPE net-snmp API CONVERT TYPE # ---------- ---------- ------------------- -------------- ------- ------ # string SNMPv2-TC DisplayString ASN_OCTET_STR - char * # integer ASN.1 Integer32 ASN_INTEGER strtol long # unsigned SNMPv2-SMI Unsigned32 ASN_UNSIGNED strtoul long # counter SNMPv2-SMI Counter32 ASN_COUNTER strtoul long # octet ASN.1 OCTET STRING ASN_OCTET_STR asc2bin char * # opaque SNMPv2-SMI Opaque ASN_OPAQUE asc2bin char * # gauge SNMPv2-SMI Gauge32 ASN_GAUGE strtoul long # objectid ASN.1 OBJECT IDENTIFIER ASN_OBJECT_ID - char * # timetick SNMPv2-SMI TimeTicks ASN_TIMETICKS strtoul long # ipaddress SNMPv2-SMI IpAddress ASN_IPADDRESS htonl long # octet SNMPv2-TC DateAndTime ASN_OCTET_STR asc2bin char * # load probes and determine their handled OIDs my $oidtable = []; my $oidhandler = {}; my $oidtype = { 'DisplayString' => [ 'string', 'N.A.' ], 'Integer32' => [ 'integer', '-1' ], 'Unsigned32' => [ 'unsigned', '0' ], 'Counter32' => [ 'counter', '-1' ], 'OCTET STRING' => [ 'octet', 'N.A.' ], 'Opaque' => [ 'opaque', 'N.A.' ], 'Gauge32' => [ 'gauge', '-1' ], 'OBJECT IDENTIFIER' => [ 'objectid', '.0' ], 'TimeTicks' => [ 'timetick', '0' ], 'IpAddress' => [ 'ipaddress', '0.0.0.0' ], 'DateAndTime' => [ 'octet', $ctx->{-enc}->dat_encode("1970-01-01 01:02:03") ], }; $ctx->{-log}->printf(1, "MIB id: %s", $mibname); foreach my $probefile (glob("$probedir/$probename.pm")) { my $probe = $probefile; $probe =~ s|^.*/([^/]+)\.pm$|$1|sg; my $p; eval { local $SIG{__DIE__} = 'DEFAULT'; eval "require \"$probefile\";"; die if ($@); eval "\$p = new My::Probe::$probe (\$ctx);"; die if ($@); }; if ($@) { die "invalid probe \"$probe\""; } $ctx->{-log}->printf(2, "Probe: \"%s\"", $probe); foreach my $oid (sort(@{$p->oids()})) { push(@{$oidtable}, $oid); $oidhandler->{$oid} = $p; $ctx->{-log}->printf(1, "MIB OID: %s [%s] ::= %s (%s)", $oid, $ctx->{-mib}->oid2name($oid), $ctx->{-mib}->oid2type($oid), $oidtype->{$ctx->{-mib}->oid2type($oid)}->[0]); } } @{$oidtable} = sort(@{$oidtable}); # connect I/O channels my $stdin = new IO::Handle;; $stdin->fdopen(fileno(STDIN), "r"); $stdin->blocking(1); my $stdout = new IO::Handle;; $stdout->fdopen(fileno(STDOUT), "w"); $stdout->autoflush(1); open(STDERR, ">/dev/null"); # daemon loop while (1) { # read next command from snmpd my $cmd = $stdin->getline; if (not defined($cmd)) { $ctx->{-log}->printf(4, "IO: EOF"); last; } $cmd =~ s|\n?$||s; $ctx->{-log}->printf(4, "IO: recv: << \"%s\\n\"", $cmd); if ($cmd =~ m/^PING$/i) { # the PING/PONG protocol part $ctx->{-log}->printf(4, "IO: send: >> \"PONG\\n\""); $stdout->printf("PONG\n"); } elsif ($cmd =~ m/^GET$/i or $cmd =~ m/^GETNEXT$/i) { # the GET/GETNEXT protocol part my $error = 1; my $oid = $stdin->getline; if (not defined($oid)) { $ctx->{-log}->printf(1, "ERROR: EOF instead of OID"); goto ERROR; } $oid =~ s|\n?$||s; $ctx->{-log}->printf(4, "IO: recv: << \"%s\\n\"", $oid); if ($oid !~ m/^(\.\d+)+$/) { $ctx->{-log}->printf(1, "ERROR: invalid query OID \"%s\"", $oid); goto ERROR; } my $oid_table = undef; if ($cmd =~ m/^GETNEXT$/i) { my $oid_parent = $oid; $oid_parent =~ s|(\.\d+)$||s; for (my $i = 0; defined($oidtable->[$i]); $i++) { if ($oidtable->[$i] =~ m|^\Q$oid\E(\.\d+)+$|) { # OID start with requested OID prefix, so use this OID $oid = $oidtable->[$i]; last; } elsif ($oidtable->[$i] eq $oid) { # OID is the requested OID exactly, so use next OID if (defined($oidtable->[$i+1])) { $oid = $oidtable->[$i+1]; last; } else { goto ERROR; # end-of-MIB case } } elsif ($oidtable->[$i] =~ m|^\Q$oid\E(\.\d+)*(\.#)$|) { # OID start with requested OID prefix, so use this OID (special case of table) $oid = $oidtable->[$i]; $oid =~ s|\.#$|.1|s; last; } elsif ($oidtable->[$i] =~ m|^\Q$oid_parent\E\.#$|) { # OID is the a requested OID of a table, so use next OID in table $oid = s|\.(\d+)$|".".($1+1)|se; $oid_table = $oidtable->[$i+1]; $oid =~ s|\.#$|.1|s; last; } } } TABLE_NEXT: my $oid_wild = $oid; my $handler = $oidhandler->{$oid}; if (not defined($handler)) { $oid_wild =~ s|(\.\d+)$|.#|s; $handler = $oidhandler->{$oid_wild}; } if (not defined($handler)) { $ctx->{-log}->printf(1, "ERROR: no handler found for OID \"%s\"", $oid); goto ERROR; } my $obj = { -oid => $oid, -name => $ctx->{-mib}->oid2name($oid_wild), -type => $ctx->{-mib}->oid2type($oid_wild), -value => undef }; $handler->probe($obj); if (not defined($obj->{-value})) { if (defined($oid_table)) { # end of table entry $oid = $oid_table; goto TABLE_NEXT; } #$ctx->{-log}->printf(1, "ERROR: handler was unable to provide probe value for OID \"%s\"", $oid); #goto ERROR; $ctx->{-log}->printf(1, "WARNING: handler was unable to provide probe value for OID \"%s\"", $oid); $obj->{-value} = $oidtype->{$obj->{-type}}->[1]; } if ($oidtype->{$obj->{-type}}->[0] =~ m/^(octet|opaque)$/) { $obj->{-value} = $ctx->{-enc}->octet_encode($obj->{-value}); } $ctx->{-log}->printf(1, "QUERY: %s [%s] ::= %s \"%s\"", $obj->{-name}, $obj->{-oid}, $obj->{-type}, $obj->{-value}); $stdout->printf("%s\n%s\n%s\n", $obj->{-oid}, $oidtype->{$obj->{-type}}->[0], $obj->{-value}); $ctx->{-log}->printf(4, "IO: send: >> \"%s\\n%s\\n%s\\n\"", $obj->{-oid}, $oidtype->{$obj->{-type}}->[0], $obj->{-value}); $error = 0; if ($error) { # end-of-MIB or error ERROR: $ctx->{-log}->printf(4, "IO: send: >> \"NONE\\n\""); $stdout->printf("NONE\n"); } } else { # for anything else (not expected) just send # something in case snmpd expects something. $ctx->{-log}->printf(4, "IO: send: >> \"\""); $stdout->printf("\n"); } } # shutdown gracefully $ctx->{-log}->printf(1, "shutdown"); undef $ctx; exit(0);