#  Altoids.pm - SNMP get/walk with built-in name to OID translation
#  Copyright (C) 1997,1999  Dave Plonka
#
#  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., 675 Mass Ave, Cambridge, MA 02139, USA.

# $Id: Altoids.pm.in,v 1.17 1999/01/28 23:02:47 dplonka Exp $
# Dave Plonka <plonka@doit.wisc.edu>

use strict;

package Altoids;

# convert RCS Revision to a reasonable Exporter.pm VERSION
'$Revision: 1.17 $' =~ m/(\d+)\.(\d+)/;
$Altoids::VERSION = sprintf("%d.%03d", $1, $2);

require Exporter;

use SNMP_Session;
use BER;

@Altoids::ISA = qw(Exporter SNMPv1_Session);
@Altoids::EXPORT = qw(oid);

=head1 NAME

Altoids - SNMP get/walk functionality with built-in name to OID translation

=head1 SYNOPSIS

   use Altoids;

   $snmp = Altoids->new(\%hints, @oid_files);
   $snmp->open($gateway, $community, $port);
   %retblock = $snmp->get($name, $index);
   %retblock = $snmp->walk($name);
   $snmp->close();

=head1 DESCRIPTION

B<Altoids> is based upon F<SNMP_Session.pm> and F<BER.pm>, a self-contained,
low-level perl SNMP implementation.
This module mainly provides two simple calling interfaces:
C<get> and C<walk>, which are the roughly equivalent to the correspondingly
named B<snmpget> and B<snmpwalk> commands of the CMU SNMP package.
Additionally, it is able to assimilate information from SunNet Manager
F<file.oid> files so that short alpha names (eg. "sysName") are used rather
than having to use the cumbersome dotted-decimal OIDs (eg. "1.3.6.1.2.1.1.5").
(It does this as an I<alt>ernative to a full-blown MIB compiler to translate
I<OIDs> - hence the name I<Altoids>.)

=head1 METHODS

Documention on the class and object methods that make up the API follows.
If you find some of the details confusing, please see the B<EXAMPLES> below.

=over 4

=cut

$Altoids::verbose = 0; # verbose mode
$Altoids::quiet = 0; # quiet mode

###############################################################################

sub slurpoids {
   my $this = shift;
   my $file;

   foreach $file (@_) {
   print STDERR "Slurping \"$file\"...\n" if $Altoids::verbose;

   if (open(OID, "<$file")) {
      while (<OID>) {
         next if (m/^\s*#/);
	 if (!m/\s*\"(.*?)\"\s+\"(.*?)\"\s*$/) {
            print STDERR "line garbled in file \"$file\": \"$_\"\n" unless $Altoids::quiet;
	    next
	 }
	 my $name = $1;
	 my $oid = $2;
         my $num;
         my $avoid = $this->{oidsalt};
	 my @oid = split(m/\./, $oid);
	 while ($num = shift @oid) {
	    if ($[-1 == $#oid) {
               if (!defined($avoid->{$num})) { # create the new node

	          print STDERR "Adding \"$oid\" ($name)...\n" if $Altoids::verbose;
	          $avoid->{$num}->{name} = $name;
	          $this->{altoids}{$name}{oid} = $oid;

		  my @humanoid = $this->translate($oid);
		  ;# discard the final "value" element that translate returns
		  pop(@humanoid);
		  if ($[-1 < $#humanoid && !grep(m/^\d+$/, @humanoid)) {
                     ;# the whole thing translated to non-digits...
		     my $humanoid = join('.', @humanoid);
		     if (defined($this->{altoids}{$name}{humanoid})) {
		        if ($this->{altoids}{$name}{humanoid} ne $humanoid) {
                           print STDERR "conflict in file \"$file\": \"$this->{altoids}{$name}{humanoid}\" ne \"$humanoid\"\n"
		        }
		     } else {
		        $this->{altoids}{$name}{humanoid} = $humanoid
		     }
		  }

	          $avoid->{$num}->{hashref} = \%{$this->{altoids}{$name}}

	       } elsif ($avoid->{$num}->{name} ne $name) {
		  if (!$Altoids::quiet) {
                     print STDERR "conflict in file \"$file\": \"$avoid->{$num}->{name}\" ne \"$name\"\n" unless $name =~ m/(mib|mib-2)/
		  }
	       } else {
	          print STDERR "\"$oid\" ($name) was already there...\n" if $Altoids::verbose
	       }
	    } else {
               if (!defined($avoid->{$num})) { # create the new node
	          $avoid->{$num} = {};
	       }
               $avoid = $avoid->{$num}
	    }
         }
      }
      close(OID)
   } else {
      print STDERR "could not open \"$file\": $!\n";
      exit 1
   }
   }
}

###############################################################################
# build an OID search tree
# This tree "oidsalt" is traversed to traslate numeric oids to human-readable
# dotted-alpha oid format.
sub rehash {
   my $this = shift;
   my $hash2scan;

   foreach $hash2scan (@_, \%{$this->{altoids}}) {
   my($num, $name, $hashref);
   while (($name, $hashref) = each(%{$hash2scan})) {
      my $avoid = $this->{oidsalt};
      my(@humanoid, @oid);
      if (!defined($hashref->{oid}) && !defined($this->{altoids}{$name}{oid})) {
         warn "skipped altoid entry \"$name\": oid not defined\n";
         next
      }
      if ($hash2scan != \%{$this->{altoids}}) {
         my $member;
         foreach $member (qw(humanoid oid lhs rhs rhstrans)) {
            if (!defined($this->{altoids}{$name}{$member}) &&
	         defined($hashref->{$member})) {
               $this->{altoids}{$name}{$member} = $hashref->{$member}
	    }
         }
         $hashref = \%{$this->{altoids}{$name}}
      }
      next unless defined($hashref->{humanoid});
      @humanoid = split(m/\./, $hashref->{humanoid});
      @oid = split(m/\./, $hashref->{oid});
      if ($#humanoid != $#oid) {
         warn "skipped altoid entry \"$name\": length('$hashref->{humanoid}' != length($hashref->{oid}))\n";
         next
      }
      my $lcv;
      for ($lcv = 0; $lcv < 1+$#oid; $lcv++) {
         $num = $oid[$lcv];
         if (!defined($avoid->{$num})) {
            $avoid->{$num} = {
               name => $humanoid[$lcv]
            };
         } elsif (!defined($avoid->{$num}->{name})) {
            $avoid->{$num}->{name} = $humanoid[$lcv]
	 }
         if (!defined($avoid->{$num}->{hashref})) {
	    ;# link up the decoding info
	    $avoid->{$num}->{hashref} = $hashref;
         }
         $avoid = $avoid->{$num}
      }
   }
   }
}

###############################################################################

sub dolhs { # DO Left Hand Side
   my $hashref = shift;
   my @iid = @_;
   @_ = ();
   if (defined($hashref->{lhs}[0])) {
      my $lcv;
      for ($lcv = 0; defined($hashref->{lhs}[$lcv]); $lcv++) {
	 my $type;
         $type = $hashref->{lhs}[$lcv];
         if ('nstring' eq $type) { # an "octet string", prefixed by length
            my $len = shift @iid;
            push(@_, join('', grep($_ = pack("C", $_), splice(@iid, 0, $len))))
         } elsif ('string' eq $type) { # an "octet string" (implied length)
            push(@_, join('', grep($_ = pack("C", $_), splice(@iid, 0, 1+$#iid))));
	    last
         } elsif ('atmaddr' eq $type) { # a 20-byte ATM address
            push(@_, join('', grep($_ = sprintf("%02x", $_), splice(@iid, 0, 20))));
	 } elsif ('macaddr' eq $type) { # a 6-byte hardware (mac) address
            push(@_, join(':', grep($_ = sprintf("%02X", $_), splice(@iid, 0, 6))));
	 } elsif ('ipaddr' eq $type) { # a 4-byte IP address
            push(@_, join('.', splice(@iid, 0, 4)));
	 } elsif ('ushort' eq $type) { # a 2-byte integer (OCTET-STRING in ASN.1)
            ;# push(@_, unpack("s", pack("CC", reverse(splice(@iid, 0, 2)))));
	    my ($high, $low) = splice(@iid, 0, 2);
            push(@_, ($high << 8) + $low);
	 } elsif ('index' eq $type) {
            push(@_, splice(@iid, 0, 1));
	 } else { # FIXME - don't just die!
	    die
	 }
      }
   }
   (@_, @iid)
}

###############################################################################

sub dorhs { # DO Right Hand Side
   my $hashref = shift;
   my $val = shift;
   if (defined($hashref->{rhs})) {
      my $type = $hashref->{rhs};
      if ('atmaddr' eq $type) { # a 20-byte ATM address
         $val = unpack("H*", $val)
      } elsif ('string' eq $type) { # a printable string (OCTET-STRING in ASN.1)
         ;# squash carriage-returns, new-lines, and tabs
         $val =~ s/\r/\\r/g;
         $val =~ s/\n/\\n/g;
         $val =~ s/\t/\\t/g;
      } elsif ('ushort' eq $type) { # an unsigned 2-byte integer (OCTET-STRING in ASN.1)
	 my ($high, $low) = (substr($val, 0, 1), substr($val, 1, 1));
         $val = (unpack("C", $high) << 8) + unpack("C", $low);
      } else { # FIXME - don't just die!
	 die
      }
   }
   if (defined($hashref->{rhstrans}{$val})) {
      $val = $hashref->{rhstrans}{$val}
   }
   $val
}

###############################################################################

sub translate {
   my $this = shift;

   unless (ref($this)) {
      die "bad method call"
   }

   my $oid = shift;
   my $val = shift;
   my $paranoid = $this->{oidsalt};
   my @oid = split(m/\./, $oid);
   my @human = ();
   my $prevlhs; # a hashref saving the previous $hashref that had an lhs member

   while ($[-1 < $#oid) {
      my $num = shift @oid;
      if (defined($paranoid->{$num})) {
	 $paranoid = $paranoid->{$num};
	 if (defined($paranoid->{hashref}->{lhs})) { # remember the "parent" lhs
	    $prevlhs = $paranoid->{hashref}
	 }
	 push(@human, defined($paranoid->{name})? $paranoid->{name} : $num)
      } else { # let's see if it's a "leaf" node with oid decoding info
	 if (defined($paranoid->{hashref}) &&
	    defined($paranoid->{hashref}->{lhs})) {
            push(@human, dolhs($paranoid->{hashref}, ($num, @oid)))
	 } elsif ($prevlhs) { # use the translations from the latest hashref
	    ;# that we've seen (ie. from the parent node)
            push(@human, dolhs($prevlhs, ($num, @oid)))
	 }else { # alas, we can do no more to make things human-readable
	    push(@human, ($num, @oid))
	 }
	 if ($paranoid->{hashref} &&
	     (defined($paranoid->{hashref}->{rhs}) ||
	      defined($paranoid->{hashref}->{rhstrans}))) {
            $val = dorhs($paranoid->{hashref}, $val)
	 }
	 last
      }
   }

   return wantarray? (@human, $val) : join('.', @human);#  . '=' . $val
}

###############################################################################

=item Altoids->new([hints_hashref] [[,] oid_file [...]])

This is the B<Altoids> constructor.  It has the following argument types:

=over 4

=item hints

The B<hints_hashref> argument, if supplied, must be the first argument.
This argument is used to inform your B<Altoids> object about the type
of various items in a MIB.  (This is necessary in some instances because
B<Altoids> contains no MIB compiler so it can't know about a given item
from a MIB definition.)

An example B<hints_hashref> is defined in the file F<altoids.pl> that
will be installed with this package.  This example is used by the
B<altwalk> and B<altget> utilities, also supplied.  If you find the need
to write your own "hints", or to augment that example, the following
discussion is an attempt to explain this feature.

B<hints_hashref> is a I<reference> to a hash containing entries of this form:

      shortname => {
	 attribute => value(s),
      },

where I<shortname> is the short name by which you'd like to refer to a given
OID.  I<attribute> is an attribute of this entry.  The possible attributes
are: B<oid>, B<humanoid>, B<lhs>, B<rhs>, B<rhstrans>.

=over 4

=item oid

The B<oid> attribute simply specifies the OID as a scalar string.  For
example (for B<ifAdminStatus>):

      oid => '1.3.6.1.2.1.2.2.1.7',

Definition of this attribute is not necessary I<if> you have specified an
appropriate oid file, which contains this definition, as an argument
to the constructor.

=item humanoid

The B<humanoid> attribute simply specifies the "humanly-readable" symbolic
version of the OID as a scalar string.  For example:

      humanoid => 'iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry.ifAdminStatus',

Definition of this attribute is not necessary I<if> you have specified an
appropriate oid file, which contains this definition, as an argument
to the constructor.

=item lhs

The B<lhs> attribute, meaning "Left-Hand Side", specifies data type(s)
of information that is encoded as a suffix within the OID.  It is an
array reference.  This SNMP feature is sometimes referred to as
"conceptual rows".  For example for B<lesMacRegEntry>:

      lhs => [ 'nstring', 'index', 'macaddr' ],

Possible values for the elements of this array are
B<atmaddr>, B<ipaddr>, B<macaddr>, B<nstring>, B<string>, and B<ushort>:

=over 4

=item atmaddr

An byte ATM address.

=item ipaddr

A 4 byte IP address.

=item macaddr

A 6 byte MAC address.

=item nstring

An ASCII string, preceeded by its numeric (1-255) length.

=item string

An ASCII string terminated by the end of the OID.  (Therefore this must be
the last element of an B<lhs> attribute array.)

=item ushort

An unsigned 16-bit value (i.e. one that is assembled from two OID octets).

=back

=item rhs

Possibly values for B<rhs> are B<atmaddr>, B<string>, and B<ushort>:

=over 4

=item atmaddr

An byte ATM address.

=item string

An ASCII string.

=item ushort

An unsigned 16-bit value (i.e. one that is assembled from two octets).

=back

=item rhstrans

The B<rhstrans> attribute, meaning "Right-Hand Side", specifies a
translation table for the value.  This is often used to translate
integer values to meaningful strings.  For example, for B<ifAdminStatus>:

         rhstrans => {
            1 => 'up(1)',
            2 => 'down(2)',
            3 => 'testing(3)'
         }

=back

So, a full example of the elements of a B<hints> hash might be:

      ifAdminStatus => {
         humanoid => 'iso.org.dod.internet.mgmt.mib-2.interfaces.ifTable.ifEntry.ifAdminStatus',
         oid => '1.3.6.1.2.1.2.2.1.7',
         rhstrans => {
            1 => 'up(1)',
            2 => 'down(2)',
            3 => 'testing(3)'
         }
      },

      sysDescr => {
	 rhs => 'string' # sysDescr is a printable ASCII string
      },

      # ...

=item files

The B<oid_file> arguments are the names of files containing the symbolic
name (e.g. "sysName") to OID (e.g. "1.3.6.1.2.1.1.5") translation.
The format of the files is that used by SunNet Manager.
Files of this form can be hand-written (examples shipped with this package)
or are available at "http://www.cisco.com/public/mibs/oid".  These were
apparently created from the MIB definitions using a tool called "MIB2SCHEMA".

=back

=cut

sub new {
   my $class = shift;
   my $postload;
   my $self = {
      altoids => {
         iso => {
            humanoid => 'iso',
	    'oid' => '1'
         }
      },
      oidsalt => { # root node in the search tree (It has no name.)
         name => '',
         1 => {
             name => 'iso'
         }
      },
      byname => {
	 number => 0,
	 iso => {
	    number => 1
	 }
      }
   };

   if ('HASH' eq ref($_[0])) { ;# a postload hash reference has been supplied...
      $postload = shift
   } elsif ('Altoid' eq ref($_[0])) { ;# an Altoid has been supplied...
      $postload = shift
   }

   bless $self, $class;

   if ($[-1 < $#_) { # file names were supplied
      $self->slurpoids(@_)
      ;#; $self->rehash()
   }
   $self->rehash($postload);
   return $self
}

###############################################################################

sub open
{
    my($this) = @_;
    my $type = ref($this) || $this;

    if (!ref($this)) {
        return SNMP_Session->open(@_)
    } else {
	;# if SNMP_Session were written as a dual-nature method, we could
	;# simply call $this->SUPER::open and skip the kludge.

        my($this,$remote_hostname,$community,$port,$max_pdu_len) = @_;
	shift(@_); # don't pass the object
        my $that = SNMP_Session->open(@_);

	;# kludge
        my ($key, $value);
	while (($key, $value) = each(%{$that})) {
	   $this->{$key} = $value
	}
	$this->{that} = \$that; # keep a reference to the SNMP_Session object

        return bless $this, $type
    }
    #NOTREACHED
}

###############################################################################

=item $snmp->walk(SHORTNAME)

Takes a short name and returns a hash where the human-readable OID segments
are the keys and either hashes or, ultimately, the OID values as scalar
values.  That is, it is a hash of hashes of values in the simplest case.
If no name can be translated to an OID or the named variables are not
available returns an empty hash.  Returns (-1, -1) on other errors.

=cut

sub walk {
   my $session = shift;
   my %retval;
   my $var;
   foreach $var (@_) {
      my @oid = $session->oid($var);
      die unless @oid; # FIXME
      my $enoid = encode_oid(@oid);
      my $oidstart = $enoid;
      for (;;) {
         if ($session->getnext_request_response($enoid)) {
            my $response = $session->pdu_buffer;
            my ($bindings, $binding, $oid, $value);
            ($bindings) = $session->decode_get_response($response);
   
            ($binding,$bindings) = decode_sequence($bindings);
            ($oid,$value) = decode_by_template($binding, "%O%@");
            last if (!BER::encoded_oid_prefix_p($oidstart, $oid));

	    if (ord(substr($value, 0, 1)) == BER::uptime_tag ||
		ord(substr($value, 0, 1)) == BER::snmp_timeticks_tag) {
	       $value = local_pretty_uptime($value)
	    } else {
	       $value = BER::pretty_print($value)
	    }
	    my @seg = $session->translate(pretty_print($oid),
	                                  $value);
	    my $val = pop(@seg);
	    my $iid = pop(@seg);

	    ;# splice out the part we know by the short name ($var)
	    splice(@seg, 0, 1+$#oid);

            ;# protect ourselves from non-printable characters
            if ($val =~ m/[\x00-\x1D\x7F-\xFF]/ ) {
               $val = unpack("H*", $val)
            }

	    my $retvalp = \%{$retval{$var}};
	    foreach (@seg) {
	       $retvalp = \%{$retvalp->{$_}}
	    }
	    $retvalp->{$iid} = $val;

            $enoid = $oid
         } else {
            return (-1, -1)
         }
      }
   }
   return %retval
}

###############################################################################

=item $snmp->get(SHORTNAME, INDEX [, INDEX [...] ])

Takes a short name and index number(s) and returns a hash where the
human-readable OID segments are the keys and either hashes or, ultimately,
the OID values is the value.  That is, it is a hash of a hash of a value
in the simplest case.
If the name can't be translated to an OID or the named variable is not
available returns an empty hash.  Returns (-1, -1) on other errors.

=cut

sub get {
   my $session = shift;
   my $name = shift;
   my @iid = @_;
   my %retval;
   my $val;

      my @oid = $session->oid($name);
      die unless @oid; # FIXME
      my $enoid = encode_oid(@oid, @iid);
      if ($session->get_request_response($enoid)) {
         my $response = $session->pdu_buffer;
         my ($bindings, $binding, $oid, $value);
         ($bindings) = $session->decode_get_response($response);
         ($binding,$bindings) = decode_sequence($bindings);
         ($oid, $value) = decode_by_template($binding, "%O%@");

	 if (ord(substr($value, 0, 1)) == BER::uptime_tag ||
	    ord(substr($value, 0, 1)) == BER::snmp_timeticks_tag) {
	    $value = local_pretty_uptime($value)
	 } else {
	    $value = BER::pretty_print($value)
	 }

	 my @seg = $session->translate(pretty_print($oid),
	                                  $value);
	 my $val = pop(@seg);
	 # my $iid = pop(@seg);

         ;# protect ourselves from non-printable characters
         if ($val =~ m/[\x00-\x1D\x7F-\xFF]/ ) {
            $val = unpack("H*", $val)
         }

	 my $hr = \%{$retval{$name}};
	 my $iid;
	 my $last = pop(@iid);
	 foreach $iid (@iid) {
	    $hr = \%{$hr->{$iid}}
	 }
         $hr->{$last} = $val
      } else {
         return wantarray? (-1, -1) : ''
      }

   return wantarray? %retval : $val
}

=item $snmp->oid(SHORTNAME)

Takes a short name and returns a dotted-decimal OID in a scalar context, or
array of the OID decimals in an array context.

=cut

sub oid {
   my $this = shift;
   unless (ref($this)) {
      die "bad method call"
   }

   my $name = shift;
   my $oid;
   if ($name =~ m/^([^.]*)(.*)$/ && defined($this->{altoids}{$1}{oid})) {
      $oid = "$this->{altoids}{$1}{oid}$2"
   } else {
      $oid = $name
   }

   return wantarray? split(m/\./, $oid) : $oid
}

sub local_pretty_uptime {
  my($packet,$uptime);
  
  ($uptime,$packet) = &decode_unsignedlike(@_);

  ## We divide the uptime by hundred since we're not interested in
  ## sub-second precision.  However, we'll go to great lengths to
  ## avoid the result being negative.
  $uptime >>= 1;
  $uptime &= 0x7fffffff if $uptime < 0;
  $uptime /= 50;

  return int($uptime);
}

sub decode_unsignedlike ($ )
{
    decode_intlike_s ($_[0], 0);
}

sub decode_intlike_s ($ $ )
{
    my($pdu, $signedp) = @_;
    my($length,$result);
    $length = ord (substr ($pdu, 1, 1));
    my $ptr = 2;
    $result = unpack ($signedp ? "c" : "C", substr ($pdu, $ptr++, 1));
    while (--$length > 0) {
	$result <<= 8;
	$result += unpack ("C", substr ($pdu, $ptr++, 1));
    }
    ($result, substr ($pdu, $ptr));
}

###############################################################################

package Altoid;

@Altoid::ISA = qw(Exporter);
@Altoid::EXPORT = qw(humanoid oid lhs rhs rhstrans);

sub humanoid {
   my $this = shift;
   if ($[-1 < $#_) {
      $this->{humanoid} = shift
   }
   return $this->{humanoid}
}

sub oid {
   my $this = shift;
   if ($[-1 < $#_) {
      $this->{oid} = shift
   }
   return $this->{oid}
}

sub lhs {
   my $this = shift;
   if ($[-1 < $#_) {
      $this->{lhs} = \@_
   }
   return $this->{lhs}
}

sub rhs {
   my $this = shift;
   if ($[-1 < $#_) {
      $this->{rhs} = shift
   }
   return $this->{rhs}
}

sub rhstrans {
   my $this = shift;
   if ($[-1 < $#_) {
      my %hash = @_;
      $this->{rhstrans} = \%hash
   }
   return $this->{rhstrans}
}

sub new {
   my $class = shift;
   my $oid = shift;
   return bless { 'oid' => $oid }, $class
}

###############################################################################

=back

=head1 EXAMPLES

F<example.pl>:

   use Altoids;

   $hints = {
      sysDescr => { # from "RFC1213-MIB"
         humanoid => 'iso.org.dod.internet.mgmt.mib-2.system.sysDescr',
              oid => '1.3.6.1.2.1.1.1',
              rhs => 'string',
      },
   };

   $snmp = Altoids->new($hints);
   $snmp->open('localhost', 'public', 161);
   %retblock = $snmp->get('sysDescr', 0);
   die "get failed.\n" if (-1 == $retblock{-1});
   print $retblock{sysDescr}{0}, "\n";
   $snmp->close();

   exit

F<example2.pl>:

   use Altoids;

   $snmp = Altoids->new('oids/RFC1213-MIB.oid');
   $snmp->open('localhost', 'public', 161);
   %retblock = $snmp->walk('system');
   die "walk failed.\n" if (-1 == $retblock{-1});
   hashprint('', %retblock);
   $snmp->close();

   exit;

   sub hashprint {
      my $prefix  = shift @_;
      my %hash  = @_;
      my ($val, $key);

      while (($key, $val) = each(%hash)) {
         if ('HASH' eq ref($val)) {
            hashprint($prefix? $prefix . '.' . $key : $key, %{$val})
         } else {
            print $prefix, '.', "$key = $val\n"
         }
      }
   }

=head1 NOTES

Enable verbose mode (informational messages to C<STDERR>) by:

   $Altoids::verbose = 1;

=head1 BUGS

When performing a B<get> or a B<walk> you can't specify the index portion
of the OID symbolicly.  That is, you must specify the index as integers,
even when you have defined the given item as a "conceptual row" in your
"hints" by specifying an B<lhs> attribute.  In practice, this isn't usually
a problem for B<walk> because you can simply walk the parent item rather
than performing a single get on just the item in which you're interested.

This package sometimes C<die>s when bad things happen.

=head1 AUTHOR

Dave Plonka <plonka@doit.wisc.edu>

=cut

1;
