#!/usr/bin/perl
#!/usr/bin/perl

#
# Description: 
#
# This program, given an OID reference as an argument, creates some
# template mib module files to be used with the net-snmp agent.  It is
# far from perfect and will not generate working modules, but it
# significantly shortens development time by outlining the basic
# structure.
#
# Its up to you to verify what it does and change the default values
# it returns.
#

# SNMP
my $havesnmp = eval {require SNMP;};

if (!$havesnmp) {
    print "
ERROR: You don't have the SNMP perl module installed.  Please obtain
this by getting the latest source release of the net-snmp toolkit from
http://www.net-snmp.org/download/ .  Once you download the source and
unpack it, the perl module is contained in the perl/SNMP directory.
See the INSTALL file there for instructions.

";
    exit;
}

if ($havesnmp) {
    eval { import SNMP; }
}
use FileHandle;

#use strict 'vars';
$SNMP::save_descriptions=1;
$SNMP::use_long_names=1;
$SNMP::use_enums=1;
SNMP::initMib();

$configfile="mib2c.conf";
$debug=0;
$quiet=0;
$nostats = 0;
$noindent = 0;

sub usage {
    print "$0 [-h] [-c configfile] [-f prefix] mibNode\n\n";
    print "  -h\t\tThis message.\n\n";
    print "  -c configfile\tSpecifies the configuration file to use\n\t\tthat dictates what the output of mib2c will look like.\n\n";
    print "  -f prefix\tSpecifies the output prefix to use.  All code\n\t\twill be put into prefix.c and prefix.h\n\n";
    print "  mibNode\tThe name of the top level mib node you want to\n\t\tgenerate code for.  By default, the code will be stored in\n\t\tmibNode.c and mibNode.h (use the -f flag to change this)\n\n";
    print "  -d\t\tdebugging output (dont do it.  trust me.)\n\n";
    print "  -s\t\tDon't display statistics at the end\n\n";
    print "  -S VAR=VAL\tSet $VAR variable to $VAL\n";
    print "  -i Don't run indent on the resulting code\n";
    1;
}	

while($#ARGV >= 0) {
    $_ = shift;
    $configfile = shift if (/^-c/);
    $debug = 1 if (/^-d/);
    if (/-S/) {
	my $expr = shift;
	my ($var, $val) = ($expr =~ /([^=]*)=(.*)/);
	die "no variable specified for -S flag." if (!$var);
	$vars{$var} = $val;
    }
    $quiet = 1 if (/^-q/);
    $nostats = 1 if (/^-s/);
    $noindent = 1 if (/^-i/);
    usage && exit(1) if (/^-h/);
    $outputName = shift if (/^-f/);
    $oid = $_ if (/^[^-]/);
}
 
#
# internal conversion tables
#

%accessToIsWritable = qw(ReadOnly 0 ReadWrite 1 
			 WriteOnly 1 Create 1);
%perltoctypes = qw(OCTETSTR   ASN_OCTET_STR
		   INTEGER    ASN_INTEGER
		   INTEGER32  ASN_INTEGER
		   UNSIGNED32 ASN_UNSIGNED
		   OBJECTID   ASN_OBJECT_ID
		   COUNTER64  ASN_COUNTER64
		   COUNTER    ASN_COUNTER
		   NETADDR    ASN_COUNTER
		   UINTEGER   ASN_UINTEGER
		   IPADDR     ASN_IPADDRESS
		   BITS       ASN_OCTET_STR
		   TICKS      ASN_TIMETICKS
		   GAUGE      ASN_GAUGE
		   OPAQUE     ASN_OPAQUE);
%perltodecl = ("OCTETSTR",  "char",
	       "INTEGER",  "long",
	       "INTEGER32",  "long",
	       "UNSIGNED32", "u_long",
	       "UINTEGER", "u_long",
	       "OBJECTID", "oid",
	       "COUNTER64", "counter64",
	       "COUNTER", "u_long",
	       "IPADDR", "u_long",
	       "BITS", "char",
	       "TICKS", "u_long",
	       "GAUGE", "u_long",
	       "OPAQUE", "char");

my $mibnode = $SNMP::MIB{$oid};
die "you didn't give me a valid OID to start with" if (!$mibnode);

# setup
$outputName = $mibnode->{'label'} if (!defined($outputName));
$vars{'name'} = $outputName;
$vars{'oid'} = $oid;

# loop through mib nodes, remembering stuff.
setup_data($mibnode);

# process .conf file
$fh = new IO::File;
if (-f "$configfile") {
    $fh->open("$configfile");
} elsif(-f "/etc/snmp/$configfile") {
    $fh->open("/etc/snmp/$configfile");
} elsif(-f "/usr/share/snmp/$configfile") {
    $fh->open("/usr/share/snmp/$configfile");
} else {
    print STDERR "Can't find a configuration file called $configfile\n";
    print STDERR "I looked in ., /etc/snmp and /usr/share/snmp\n";
exit;
}
process();
$fh->close;

if (!$noindent) {
  foreach $i (keys(%written)) {
    next if ($i eq "-");
    print STDERR "running indent on $i\n" if (!$quiet);
    system("indent -orig -nbc -bap -nut -nfca -T netsnmp_mib_handler -T netsnmp_handler_registration -T netsnmp_delegated_cache -T netsnmp_mib_handler_methods -T netsnmp_old_api_info -T netsnmp_old_api_cache -T netsnmp_set_info -T netsnmp_request_info -T netsnmp_set_info -T netsnmp_tree_cache -T netsnmp_agent_request_info -T netsnmp_cachemap -T netsnmp_agent_session -T netsnmp_array_group_item -T netsnmp_array_group -T netsnmp_table_array_callbacks -T netsnmp_table_row -T netsnmp_table_data -T netsnmp_table_data_set_storage -T netsnmp_table_data_set -T netsnmp_column_info -T netsnmp_table_registration_info -T netsnmp_table_request_info -T netsnmp_iterator_info -T netsnmp_data_list -T netsnmp_oid_array_header -T netsnmp_oid_array_header_wrapper -T netsnmp_oid_stash_node -T netsnmp_pdu -T netsnmp_request_list -T netsnmp_callback_pass -T netsnmp_callback_info -T netsnmp_transport -T netsnmp_transport_list -T netsnmp_tdomain $i");
  }
}

sub tocommas {
    my $oid = $_[0];
    $oid =~ s/\./,/g;
    $oid =~ s/^\s*,//;
    return $oid;
}

sub oidlength {
    return scalar ($_[0] =~ /\./);
}

# replaces $VAR type expressions and $VAR.subcomponent expressions
# with data from the mib tree and loop variables.
# possible uses:
#
#   $var               -- as defined by loops, etc.
#   ${var}otherstuff   -- appending text to variable contents
#   $var.uc            -- all upper case version of $var
#
# Mib components, $var must first expand to a mib node name:
#
#   $var.objectID      -- dotted full OID
#   $var.commaoid      -- comma separated OID for array init
#   $var.subid         -- last number component of oid
#   $var.oidlength     -- length of the oid
#   $var.type          -- node's ASN_XXX type
#   $var.settable      -- 1 if it's writable, 0 if not
#   $var.noaccess      -- 1 if not-accessible, 0 if not
#   $var.access        -- node's access type
#   $var.status        -- node's status
#   $var.syntax        -- node's syntax
#   $var.decl          -- C data type
sub process_vars {
    my $it = shift;

    # mib substitutions ($var.type -> $mibnode->{'type'})
    $it =~ s/\$(\w+)\.(uc)/uc($vars{$1})/eg; # make something uppercase
    $it =~ s/\$(\w+)\.(commaoid)/tocommas($SNMP::MIB{$vars{$1}}{objectID})/eg;
    $it =~ s/\$(\w+)\.(oidlength)/oidlength($SNMP::MIB{$vars{$1}}{objectID})/eg;
    $it =~ s/\$(\w+)\.(perltype)/$SNMP::MIB{$vars{$1}}{type}/g;
    $it =~ s/\$(\w+)\.(type)/$perltoctypes{$SNMP::MIB{$vars{$1}}{$2}}/g;
    $it =~ s/\$(\w+)\.(subid)/$SNMP::MIB{$vars{$1}}{subID}/g;
    $it =~ s/\$(\w+)\.(settable)/(($SNMP::MIB{$vars{$1}}{access} =~ \/(ReadWrite|Create|Writeonly)\/)?1:0)/eg;
    $it =~ s/\$(\w+)\.(noaccess)/(($SNMP::MIB{$vars{$1}}{access} =~ \/(NoAccess)\/)?1:0)/eg;
    $it =~ s/\$(\w+)\.(objectID|label|subID|access|status|syntax)/$SNMP::MIB{$vars{$1}}{$2}/g;
    $it =~ s/\$(\w+)\.(decl)/$perltodecl{$SNMP::MIB{$vars{$1}}{type}}/g;
    # normal variable substitions
    $it =~ s/\$\{(\w+)\}/$vars{$1}/g;
    $it =~ s/\$(\w+)/$vars{$1}/g;
    return $it;
}

# process various types of statements
#
# which include:
#   @open FILE@
#     writes generated output to FILE
#   @foreach $VAR scalar@
#     repeat iterate over code until @end@ setting $VAR to all known scalars
#   @foreach $VAR table@
#     repeat iterate over code until @end@ setting $VAR to all known tables
#   @foreach $VAR column@
#     repeat iterate over code until @end@ setting $VAR to all known
#     columns within a given table.  Obviously this must be called
#     within a foreach-table clause.
#   @foreach $VAR index@
#     repeat iterate over code until @end@ setting $VAR to all known
#     indexes within a given table.  Obviously this must be called
#     within a foreach-table clause.
#   @eval $VAR = expression@
#     evaluates expression and assigns the results to $VAR
#   @perleval STUFF@
#     evaluates STUFF directly in perl.  Note that all mib2c variables
#     interpereted within .conf files are in $vars{NAME}.
#   @skip@
#     skips everything till the appropriately matched @end@
#   @if expression@
#      evaluates expression, and if expression is true processes
#      contained part until appropriate @end@ is reached.
sub skippart {
    my $endcount = 1;
    while(<$fh>) {
	if (/\@end\@/) {
	    return if ($endcount == 1);
	    $endcount--;
	}
	if (/\@else\@/) {
	    return if ($endcount == 1);
	}
	if (/\@(foreach|if)/) {
	    $endcount++;
	}
    }
}


sub process {
    while(<$fh>) {
	if (/^\#\#/) {
	    # noop, it's a comment
	} elsif (/\@open\s+([^\@]+)\@/) {
	    my $spec = process_vars($1);
	    $out->close() if ($out);
	    $out = new IO::File;
	    $out->open(">$spec") || die "failed to open $spec";
	    print STDERR "writing to $spec\n" if (!$quiet);
	    $written{$spec} = '1';
	} elsif (/\@end\@/) {
	    return;
	} elsif (/\@if\s+([^@]*)\@/) {
	    if (eval(process_vars($1))) {
		process();
	    } else {
		skippart();
	    }
	} elsif (/\@eval\s+\$(\w+)\s*=\s*([^\@]*)/) {
	    my ($v, $e) = ($1, $2);
	    my $e = process_vars($e);
	    $vars{$v} = eval($e);
	} elsif (/\@perleval\s*(.*)\@/) {
	    eval($1);
	} elsif (/\@skip\@/) {
	    skippart();
	} elsif (/\@\s*foreach\s+\$([^\@]+)\s+scalars*\s*\@/) {
	    my $var = $1;
	    my $startpos = $fh->tell();
	    my $scalar;
	    my @thekeys = keys(%scalars);
	    if ($#thekeys == -1) {
	      skippart();
	    } else {
	      foreach $scalar (@thekeys) {
		$fh->seek($startpos, 0); # go to top of section.
		my $oldvar = $vars{$var};
		$vars{$var} = $scalar;
		my $oldscalar = $currentscalar;
		$currentscalar = $scalar;
		process();
		$vars{$var} = $oldvar;
		$currentscalar = $oldscalar;
	      }
	    }
	  } elsif (/\@\s*foreach\s+\$([^\@]+)\s+tables*\s*\@/) {
	    my $var = $1;
	    my $startpos = $fh->tell();
	    my $table;
	    my @thekeys = keys(%tables);
	    if ($#thekeys == -1) {
	      skippart();
	    } else {
	      foreach $table (@thekeys) {
		$fh->seek($startpos, 0); # go to top of section.
		my $oldvar = $vars{$var};
		$vars{$var} = $table;
		my $oldtable = $currenttable;
		$currenttable = $table;
		process();
		$vars{$var} = $oldvar;
		$currenttable = $oldtable;
	      }
	    }
	  } elsif (/\@\s*foreach\s+\$([^\@]+)\s+(column|index)\s*\@/) {
	    my ($var, $type) = ($1, $2);
	    my $startpos = $fh->tell();
	    my $column;
	    if ($#{$tables{$currenttable}{$type}} == -1) {
	      skippart();
	    } else {
	      foreach $column (@{$tables{$currenttable}{$type}}) {
		#		print "looping on $var for $type -> $column\n";
		$fh->seek($startpos, 0); # go to top of section.
		my $oldvar = $vars{$var};
		$vars{$var} = $column;
		my $oldcolumn = $currentcolumn;
		$currentcolumn = $column;
		process();
		$vars{$var} = $oldvar;
		$currentcolumn = $oldcolumn;
	      }
	    }
	  } elsif (/\@\s*foreach\s+\$([^\@]+)\s+\$([^\@]+)\s+(enums*)\s*\@/) {
	    my ($varvar, $varval, $type) = ($1, $2, $3);
	    my $startpos = $fh->tell();
	    my $enum;
	    my @keys = sort { $SNMP::MIB{$currentcolumn}{'enums'}{$a} <=>
				  $SNMP::MIB{$currentcolumn}{'enums'}{$b} } (keys(%{$SNMP::MIB{$currentcolumn}{'enums'}}));
	    if ($#keys > -1) {
		foreach $enum (@keys) {
		    $fh->seek($startpos, 0); # go to top of section.
		    my $oldvarvar = $vars{$varvar};
		    my $oldvarval = $vars{$varval};
		    $vars{$varvar} = $enum;
		    $vars{$varval} = $SNMP::MIB{$currentcolumn}{'enums'}{$enum};
		    process();
		    $vars{$varvar} = $oldvarvar;
		    $vars{$varval} = $oldvarval;
		}
	    } else {
		skippart();
	    }
	} else {
	    die "no output file specified" if (!$out);
	    print $out process_vars($_);
	}
    }
}

sub setup_data {
    my $mib = shift;
    if ($mib->{label} =~ /Table$/) {
	my $tablename = $mib->{label};
	my $entry = $mib->{children};
	my $columns = $entry->[0]{children};
	foreach my $col (sort { $a->{'subID'} <=> $b->{'subID'} } @$columns) {
	    # store by numeric key so we can sort them later
	    push @{$tables{$tablename}{'column'}}, $col->{'label'};
	}
	foreach my $index (@{$entry->[0]{'indexes'}}) {
	    my $node = $SNMP::MIB{$index} || 
		die "can't find info about index $index in table $tablename\n";
	    push @{$tables{$tablename}{'index'}}, $index;
	}
    } else {
	my $children = $mib->{children};
	if ($#children == -1 && $mib->{type}) {
	    # scalar
	    $scalars{$mib->{label}} = 1;
	} else {
	    my $i;
	    for($i = 0; $i <= $#$children; $i++) {
		setup_data($children->[$i]);
	    }
	}
    }
}

sub min {
    return $_[0] if ($_[0] < $_[1]);
    return $_[1];
}

sub max {
    return $_[0] if ($_[0] > $_[1]);
    return $_[1];
}
