##
## This file contains library proc's that are used in many places
## by the tkined scripts. They are auto loaded using tcl's autoload
## facility.
##
## Copyright (c) 1993, 1994
##
## J. Schoenwaelder
## TU Braunschweig, Germany
## Institute for Operating Systems and Computer Networks
##
## Permission to use, copy, modify, and distribute this
## software and its documentation for any purpose and without
## fee is hereby granted, provided that this copyright
## notice appears in all copies.  The University of Braunschweig
## makes no representations about the suitability of this
## software for any purpose.  It is provided "as is" without
## express or implied warranty.
##

##
## Write background error messages to stderr.
##

proc scottyerror { msg } {
    global errorInfo
#    puts stderr "$msg\n$errorInfo"
}


##
## =========================================================================
## =================== T K I N E D   related subroutines ===================
## =========================================================================
##
##  LoadDefaults { class }
##  ShowDefaults
##
##  write   { {txt ""} {cmd {}}}
##  writeln { {txt ""} {cmd {}}}
##  debug   { txt }
##
##  ldelete { list elem }
##
## =========================================================================
##

##
## Search for tkined.defaults files following the auto_path and read
## default definition. Each default definition has the following syntax:
##
##    <class>.<attribute>.<index>: <value>
##
## Every entry matching the class argument will be written to the global
## default array, indexed by <attribute>.<index>. A sample application
## is the definition of snmp related parameters like:
##
##    snmp.port.134.169.34.15: 8132
##    snmp.community.134.169.34.15: private
##
## The resulting tcl variables are:
##
##    default(port.134.169.34.15) -> 8132
##    default(community.134.169.34.15) -> private
##

proc LoadDefaults { args } {

    global auto_path 
    global default

    set filename tkined.defaults
    
    set reverse_path ""
    foreach dir $auto_path {
	set reverse_path "$dir $reverse_path"
    }
    
    foreach dir $reverse_path {
	if [file readable $dir/$filename] {
	    set fh [open $dir/$filename r]
	    while {![eof $fh]} {
		gets $fh line
		set line [string trim $line]
		if {($line == "") || ([regexp "^#|^!" $line])} continue
		foreach class $args {
		    if {[string match $class.* $line]} {
			set line [split $line ":"]
			set list [split [lindex $line 0] .]
			set name  [join [lrange $list 1 end] .]
			set value [string trim [lindex $line 1]]
			set default($name) $value
		    }
		}
	    }
	    close $fh
	}
    }
    return ""
}

##
## Show the defaults as loaded from the tkined.defaults files.
##

proc ShowDefaults {} {

    global default

    if {![info exists default]} {
	ined acknowledge \
	    "No parameters used from the tkined.default files."
	return
    }

    set result ""
    foreach name [array names default] {
	set attname  [lindex [split $name .] 0]
	set attindex [join [lrange [split $name .] 1 end] .]
	lappend result [format "%-16s %-16s %s" \
			$attname $attindex $default($name)]
    }

    set result [lsort $result]
    ined browse "Defined defaults used by this application:" "" \
	"Attribute         Index            Value" $result
}

##
## Write a report to a log object. The name is expected to be in
## the variable tool_name which may be set in one of the init
## procs below.
##

proc writeln {{txt ""} {cmd ""}} {
    write "$txt\n" $cmd
}

proc write {{txt ""} {cmd ""}} {
    static log
    global tool_name

    if {![info exists tool_name]} {
	set tool_name "Unknown"
    }

    if {(![info exists log]) || ([ined -noupdate retrieve $log] == "")} {
	set log [ined -noupdate create LOG]
	ined -noupdate name $log "$tool_name Report"
    }

    if {$cmd == ""} {
	ined append $log "$txt"
    } else {
	ined hyperlink $log $cmd $txt
    }
}

##
## Print messages if running in debug mode.
##

proc debug {args} {

    global debug

    if {[info exists debug] && ($debug == "true")} {
	foreach arg $args {
	    writeln $arg
	}
    }
}

##
## Delete an element from a list.
##

proc ldelete {list element} {
    upvar $list mylist
    set result ""
    foreach e $mylist {
        if {$e != $element} { lappend result $e }
    }
    return $result
}


##
## =========================================================================
## ================== M O N I T O R   related subroutines ==================
## =========================================================================
##
##  CloneNode { id clone {offset_x 20} {offset_y 20} }
##  DeleteClones {}
##  MoJoCheckIds { cmd ids } 
##  MoJoInfo {}
##  MoJoSelect { action } 
##  MoJoModify {}
##
## =========================================================================
##

##
## Clone a node object given by id and move the new object to a
## position given by relative coordinates offset_x and offset_y.
## The clone operation is only done when the global variable
## clone is set to true. All new object ids are collected in the
## global list clone_ids.
##

proc CloneNode { id clone {offset_x 20} {offset_y 20} } {

    global clone_ids

    if {![info exists clone_ids]} { set clone_ids "" }

    # not all objects understand every command, so we catch it

    catch {ined -noupdate name    $clone [ined -noupdate name $id]}
    catch {ined -noupdate address $clone [ined -noupdate address $id]}
    catch {ined -noupdate color   $clone [ined -noupdate color $id]}
    catch {ined -noupdate font    $clone [ined -noupdate font $id]}
    catch {ined -noupdate label   $clone [ined -noupdate label $id]}
    catch {ined -noupdate icon    $clone [ined -noupdate icon $id]}
    
    if {! [catch {ined -noupdate move $id} xy]} {
	set x [expr {[lindex $xy 0]+$offset_x}]
	set y [expr {[lindex $xy 1]+$offset_y}]
	catch {ined -noupdate move $clone $x $y}
    }

    lappend clone_ids $clone

    return $clone
}

##
## Delete all previously created clones.
##

proc DeleteClones {} {

    global clone_ids

    if {[info exists clone_ids]} {
	foreach id $clone_ids {
	    catch {ined delete $id}
	}
    }
}

##
## Check if all ined objects given by ids are still there. Modify the 
## current job if any objects have been removed and kill the job, if no 
## ids have been left. This proc assumes that a command is simply a 
## command name and a list of ids as arguments.
##

proc MoJoCheckIds { cmd ids } {

    set new_ids ""
    foreach id $ids {
        if {[ined retrieve $id] != ""} {
            lappend new_ids $id
        }
    }

    if {$new_ids != $ids} {
        if {$new_ids == ""} {
            job kill [job current]
	    return 
        } else {
	    job command [job current] [list $cmd $new_ids]
        }
    }

    return $new_ids
}

##
## Display the jobs currently running. The ids of the command are
## converted to the name attributes to increase readability.
##

proc MoJoInfo { } {

    set jobs [job list]

    if {$jobs == ""} {
	ined acknowledge "Sorry, no jobs available."
	return
    }
    
    set result ""
    set len 0
    foreach j $jobs {

	set jobid   [lindex $j 0]
	set jobcmd  [lindex $j 1]
	set jobitv  [expr {[lindex $j 2] / 1000.0}]
	set jobrem  [expr {[lindex $j 3] / 1000.0}]
	set jobcnt  [lindex $j 4]
	set jobstat [lindex $j 5]

	set line \
	     [format "%s %6.1f %6.1f %3d %8s %s" \
	      $jobid $jobitv $jobrem $jobcnt $jobstat \
	      [lindex $jobcmd 0] ]

	# Convert the id's to hostnames for readability.
	
	foreach id [lindex $jobcmd 1] {
	    if {[catch {lindex [ined name $id] 0} host]} {
		set host $id
	    }
	    if {[string length $line] < 65} {
		append line " $host"
	    } else {
		lappend result $line
		set line [format "%35s %s" "" $host]
	    }
	}

	lappend result $line
    }

    set header " ID    INTV    REM  CNT STATUS  COMMAND"

    foreach line $result {
	if {[string length $line] > $len} {
	    set len [string length $line]
	}
    }

    for {set i [string length $header]} {$i < $len} {incr i} {
	append header " "
    }

    ined browse $header $result
}

##
## Select one of the existing jobs for doing action with it.
## This is at least used by the MoJoModify proc below.
##

proc MoJoSelect { action } {
    
    set jobs [job list]

    if {$jobs == ""} {
	ined acknowledge "Sorry, no job to $action."
        return
    }

    set res ""
    foreach j $jobs {
	# Convert the id's to hostnames for readability.
	
	set hosts [lindex [lindex $j 1] 0]
	foreach id [lindex [lindex $j 1] 1] {
	    if {[catch {lindex [ined -noupdate name $id] 0} host]} {
		set host $id
	    }      
	    lappend hosts $host
	}
	if {[llength $hosts] > 4} {
	    set hosts "[lrange $hosts 0 3] ..."
	}
    
	set status [lindex $j 5]
	if {   ($action == "resume"  && $status == "suspend")
            || ($action == "suspend" && $status == "waiting")
            || ( ($action != "resume") && ($action != "suspend"))} {
		lappend res [format "%s %s" [lindex $j 0] $hosts]
	    }
    }

    if {$res == ""} {
	ined acknowledge "Sorry, no job to $action."
	return ""
    } 

    set res [ined list "Choose a job to $action:" $res [list $action cancel]]
    if {[lindex $res 0] == "cancel"} {
	return ""
    } else {
	return [lindex [lindex $res 1] 0]
    }
}

proc MoJoModify {} {

    static threshold

    # Ask for the job to modify.

    set jobid [MoJoSelect modify]

    if {$jobid == ""} return

    # Set up default values.

    if {![info exists threshold($jobid)]} {
	set threshold($jobid) ""
    }

    # Get the details about the selected job.

    set jobcmd ""
    set jobitv ""
    foreach job [job list] {
	if {$jobid == [lindex $job 0]} {
	    set jobcmd  [lindex $job 1]
	    set jobitv  [expr {[lindex $job 2] / 1000.0}]
	    set jobstat [lindex $job 5]
	}
    }
    
    # Convert the status waiting to active
    
    if {$jobstat == "waiting"} { set jobstat active }
	
    # Convert the id's to hostnames for readability.
    
    set hosts ""
    foreach id [lindex $jobcmd 1] {
	if {[catch {lindex [ined -noupdate name $id] 0} host]} {
	    set host $id
	}      
	lappend hosts $host
    }
    if {[llength $hosts] > 3} {
	set hosts "[lrange $hosts 0 2] ..."
    }
    
    # Request for changes.
    
    set value $threshold($jobid)

    set res [ined request "Modify $jobid ([lindex $jobcmd 0] $hosts)" \
	      [list [list "Intervaltime \[s\]:" $jobitv entry 10] \
	            [list "Job Status:" $jobstat radio active suspend] \
		    [list "Threshold:" $value entry 10] ] \
	      [list modify "kill job" cancel] ]
    
    if {[lindex $res 0] == "cancel"} return

    if {[lindex $res 0] == "kill job"} {
	job kill $jobid
	return
    }
    
    set jobitv  [lindex $res 1]
    set jobstat [lindex $res 2]
    set value   [lindex $res 3]

    set threshold($jobid) $value
    
    if {$jobstat == "active"}  { job status $jobid waiting }
    if {$jobstat == "suspend"} { job status $jobid suspend }

    if {[catch {expr round($jobitv * 1000)} ms] || $jobitv <= 0} {
	ined acknowledge "Illegal interval time ignored."
	return
    }

    if {$ms != [job interval $jobid]} {

	job interval $jobid $ms

	set newlist ""
	foreach cmd [ined restart] {
	    if {[lindex $cmd 3] == $jobid} {
		set cmd [lreplace $cmd 2 2 $jobitv]
	    }
	    lappend newlist $cmd
	}
	ined restart $newlist
    }

    # Now add, update or remove the threshold attribute.

    foreach id [lindex $jobcmd 1] {
	catch {ined attribute $id threshold $value}
    }
}

proc MoJoCheckThreshold { id txt value {unit ""} } {

    set action [MoJoThresholdAction]
    if {$action == ""} return

    set threshold [ined attribute $id threshold]
    if {$threshold == ""} return

    set len [llength $value]
    for {set i 0} {$i < $len} {incr i} {
	catch {
	    set v [lindex $value $i]
	    set t [lindex $threshold $i]
	    if {$t == ""} continue
	    if {$v > $t} {
		set msg "$txt $v $unit exceeds threshold $t $unit"
		if {[lsearch $action syslog] >= 0} {
		    syslog warning $msg
		}
		if {[lsearch $action flash] >= 0} {
		    set jobid [job current]
		    if {$jobid != ""} {
			set secs [expr {[job interval $jobid] / 1000}]
		    } else {
			set secs 2
		    }
		    ined flash $id $secs
		}
		if {[lsearch $action write] >= 0} {
		    writeln "[getdate]:"
		    writeln $msg
		    writeln
                }
	    }
	}
    }
}

proc MoJoThresholdAction { args } {
    static action
    if {![info exists action]} { set action [list flash write] }
    if {$args != ""} { set action [lindex $args 0] }
    return $action
}


##
## =========================================================================
## ======================= I P   related subroutines =======================
## =========================================================================
##
##  IpInit { toolname }
##
##  GetIpAddress { node }
##  ForeachIpNode { id ip host list body }
##  IpFlash { ip }
##  IpService {}
##
## =========================================================================
##

##
## Initialize global variables that are used by these ip procs.
##

proc IpInit { toolname } {

    global default icmp_retries icmp_timeout icmp_delay icmp_routelength
    global tool_name

    set tool_name $toolname

    if {[info exists default(retries)]} {
	set icmp_retries $default(retries)
    } else {
	set icmp_retries 3
    }
    icmp -retries $icmp_retries
    
    if {[info exists default(timeout)]} {
	set icmp_timeout $default(timeout)
    } else {
	set icmp_timeout 3
    }
    icmp -timeout $icmp_timeout
    
    if {[info exists default(delay)]} {
	set icmp_delay $default(delay)
    } else {
	set icmp_delay 5
    }
    icmp -delay $icmp_delay
    
    if {[info exists default(routelength)]} {
	set icmp_routelength $default(routelength)
    } else {
	set icmp_routelength 16
    }
}

##
## Get the IP Address of a node. Query the name server, if the
## address attribute is not set to something that looks like a
## valid IP address.
##

proc GetIpAddress { node } {
    if {[lsearch "NODE STRIPCHART BARCHART GRAPH" [ined type $node]] >= 0} {
        set host [lindex [ined name $node] 0]
        set ip [lindex [ined address $node] 0]
        if {[regexp "^\[0-9\]+\.\[0-9\]+\.\[0-9\]+\.\[0-9\]+$" $ip] > 0} {
            return $ip
        }
	if {[regexp "^\[0-9\]+\.\[0-9\]+\.\[0-9\]+\.\[0-9\]+$" $host] > 0} {
	    return $host
	}
        if {[catch {nslook $host} ip]==0} {
            return [lindex $ip 0]
        }
    }
    return ""
}

##
## Evaluate body for every node where we get an IP address for. The id
## and ip variables are set to the id of the NODE object and it's IP
## address.
##

proc ForeachIpNode { id ip host list body } {
    upvar $id lid
    upvar $ip lip
    upvar $host lhost
    foreach comp $list {
        if {[ined type $comp] == "NODE"} {
	    set lid [ined id $comp]
            set lip [GetIpAddress $comp]
	    set lhost [ined name $comp]
            if {$lip == ""} {
		set host [lindex [ined name $comp] 0]
                ined acknowledge "Can not lookup IP Address for $host."
                continue
            }
	    uplevel 1 $body
	}
    }
}

##
## Flash the icon of the object given by the ip address.
##

proc IpFlash { ip {secs 2} } {
    foreach comp [ined retrieve] {
	if {([ined type $comp] == "NODE") && ([GetIpAddress $comp] == $ip)} {
	    ined flash [ined id $comp] $secs
	}
    }
}

##
## Read the file /etc/services and let the user select a TCP service of
## interest. The proc returns a list containing the name and the port number.
##

proc IpService { protocol } {

    if {[catch {open /etc/services} fh]} {
	ined acknowledge "Can not open /etc/services."
        return
    }
    
    set service(X11) 6000
    while {! [eof $fh]} {
	gets $fh line
	if {[string index [string trim $line] 0] == "#"} continue
	if {[scan $line "%s %d/$protocol" name number] != 2} continue
	set service($name) $number
    }
    close $fh

    set result [ined list "Select the TCP service of interest:" \
		  [lsort [array names service]] \
		  [list select default cancel]]

    switch [lindex $result 0] {
	cancel { return }
	select {
	    set name [lindex [lindex $result 1] 0]
	    if {$name == ""} return
	}
	define {
	    static sname
	    static sport
	    if {![info exists sport]} { set sport "" }
	    if {![info exists sname]} { set sname "noname" }
	    set result [ined request "Set name and port number for service:" \
			 [list [list Name: $sname] [list Port: $sport]] \
			 [list ok cancel] ]
	    if {[lindex $result 0] == "cancel"} return
	    set sname [lindex $result 1]
	    set sport [lindex $result 2]
	    set name $sname
	    set service($name) $sport
	}
	{} {
	    set name [lindex [lindex $result 1] 0]
            if {$name == ""} return
	}
    }

    return [list $name $service($name)]
}

##
## =========================================================================
## ========== S N M P   related subroutines ================================
## =========================================================================
##
##  SnmpInit { toolname }
##  SnmpParameter
##  SnmpOpen  { ip }
##  SnmpGet { s varlist }
##  SnmpGetBulk { s varlist varname body }
##  SnmpClose { s }
##  SnmpShowScalars { ip path }
##  SnmpShowTable {ip table }
##  SnmpWalk { ip path }
##
## =========================================================================
##


##
## Initialize global variables that are used by these snmp procs.
##

proc SnmpInit { toolname } {

    global snmp_community snmp_timeout snmp_retries snmp_port
    global snmp_protocol snmp_context
    global snmp_browser
    global tool_name
    global default

    if {[info exists default(community)]} {
	set snmp_community $default(community)
    } else {
	set snmp_community "public"
    }

    if {[info exists default(timeout)]} {
	set snmp_timeout $default(timeout)
    } else {
	set snmp_timeout 5
    }

    if {[info exists default(retries)]} {
	set snmp_retries $default(retries)
    } else {
	set snmp_retries 3
    }

    if {[info exists default(port)]} {
	set snmp_port $default(port)
    } else {
	set snmp_port 161
    }

    if {[info exists default(protocol)]} {
	set snmp_protocol $default(protocol)
    } else {
	set snmp_protocol SNMPv1
    }

    if {[info exists default(context)]} {
	set snmp_context $default(context)
    } else {
	set snmp_context 1
    }

    set snmp_browser ""

    set tool_name $toolname

    if {[lsearch [info commands] snmp] < 0} {
	ined acknowledge "Sorry, this version of scotty has no snmp support."
	exit
    }
}


##
## Set the parameters (community, timeout, retry) for snmp requests.
##

proc SnmpParameter {} {

    global snmp_community snmp_timeout snmp_retries snmp_port
    global snmp_protocol snmp_context

    set result [ined request "SNMP Parameter" \
		[list [list "Community:" $snmp_community entry 20] \
                      [list "UDP Port:" $snmp_port entry 10] \
                      [list "Timeout \[s\]:" $snmp_timeout entry 10] \
                      [list "Retries:" $snmp_retries scale 1 8] \
		      [list "Protocol:" $snmp_protocol radio SNMPv1 SNMPv2] \
		      [list "Context:" $snmp_context entry 10] ] \
		[list "set values" cancel] ]

    if {[lindex $result 0] == "cancel"} return

    set snmp_community [lindex $result 1]
    set snmp_port      [lindex $result 2]
    set snmp_timeout   [lindex $result 3]
    set snmp_retries   [lindex $result 4]
    set snmp_protocol  [lindex $result 5]
    set snmp_context   [lindex $result 6]
}

##
## Open a snmp session and initialize it to the defaults stored in
## the global variables snmp_community, snmp_timeout, snmp_retries.
##

proc SnmpOpen { ip } {

    global snmp_community snmp_timeout snmp_retries snmp_port 
    global snmp_protocol snmp_context
    global default

    if {[info exists default(protocol.$ip)]} {
	set protocol $default(protocol.$ip)
    } else {
	set protocol $snmp_protocol
    }

    if {$protocol != "SNMPv2"} {
	set s [snmp session -address $ip -community $snmp_community \
		-timeout $snmp_timeout -retries $snmp_retries]
	
	catch {$s configure -port $snmp_port}
	
	if {[info exists default(community.$ip)]} { 
	    $s configure -community $default(community.$ip) 
	}
	if {[info exists default(port.$ip)]} {
	    $s configure -port $default(port.$ip)
	}
    } else {
	if {[info exists default(context.$ip)]} {
	    set context $default(context.$ip)
	} else {
	    set context $snmp_context
	}
	set s [snmp session]
	$s configure -dstparty "1.3.6.1.6.3.3.1.3.$ip.1 UDP $ip 161 484" \
		     -srcparty "1.3.6.1.6.3.3.1.3.$ip.2 UDP 0.0.0.0 161 484" \
		     -context 1.3.6.1.6.3.3.1.4.$ip.$context
    }

    if {[info exists default(timeout.$ip)]} { 
	$s configure -timeout $default(timeout.$ip) 
    }
    if {[info exists default(retries.$ip)]} { 
	$s configure -retries $default(retries.$ip) 
    }

    return $s
}

##
## Close an snmp handle. Catch all errors. Why bother about it.
##

proc SnmpClose { s } {
    catch {$s destroy}
}

##
## Send a snmp get request and return the retrieves values.
##

proc SnmpGet { s varlist } {
    set result ""
    foreach rvb [$s get $varlist] {
	lappend result [lindex $rvb 2]
    }
    return $result
}

##
## Send a snmp getnext request and set the variable varname to the
## retrieved values. Evaluate the body foreach getnext that is in
## our subtree.
##

proc SnmpGetBulk { s varlist varname body } {
    upvar $varname result
    $s walk x $varlist {
	set result ""
	foreach rvb $x { lappend result [lindex $rvb 2] }
	uplevel 1 $body
    }
}


##
## Show all scalar variables that are successors of path.
##

proc SnmpShowScalars {ip path} {

    if {[catch {nslook $ip} host]} { set host "" }
    set host [lindex $host 0]

    set s [SnmpOpen $ip]
    set txt "Scalar variables in group $path of $host \[$ip\]:\n"
    set path [mib oid $path]
    set plen [llength [split $path .]]
    set sucs [mib successor $path]
    foreach var $sucs {
	if {[catch {$s getnext $var} result]} {
	    append txt "$result\n"
	    break
	}
	set oid [lindex [lindex $result 0] 0]
	set val [lindex [lindex $result 0] 2]
	set len [llength [split $oid .]]
	if {$len != $plen+2} continue
	if {![string match "$path.*.0" $oid]} break
	if {$val != ""} {
	    if {! [info exists done($oid)]} {
		append txt [format "  %-24s %s\n" "[mib name $oid]:" $val]
	    }
	    set done($oid) 1
	}
    }
    writeln $txt
    SnmpClose $s
}

proc SnmpEditScalars {ip path} {

    if {[catch {nslook $ip} host]} { set host "" }
    set host [lindex $host 0]

    set s [SnmpOpen $ip]
    foreach suc [mib successor $path] {
	set access [mib access $suc]
	if {[string match *write* $access]} {
	    if {[catch {$s get $suc.0} var]} continue
	    lappend varlist [list [mib name $suc] [lindex [lindex $var 0] 2]]
	    lappend oidlist [lindex [lindex $var 0] 0]
	}
    }
    if {![info exists varlist]} {
	SnmpClose $s
	return
    }

    set result [ined request "Edit SNMP scalars ($path) on $host \[$ip\]:" \
	             $varlist [list "set scalars" cancel] ]
    if {[lindex $result 0] == "cancel"} {
	SnmpClose $s
        return
    }

    set idx 0
    foreach oid $oidlist {
	set old [lindex [lindex $varlist $idx] 1]
	incr idx
	set val [lindex $result $idx]
	if {$val == $old} continue
	if {[catch {$s set [list [list $oid [mib syntax $oid] $val]]} err]} {
	    ined acknowledge "Set on [mib name $oid] failed: " "" $err
	}
    }

    SnmpClose $s
}

##
## Show a complete MIB table. First determine which rows are supported
## by an agent and then walk through the table. The formating code is
## ugly and should be replaced by a better version.
##

proc SnmpShowTable {ip table} {

    if {[catch {nslook $ip} host]} { set host "" }
    set host [lindex $host 0]

    if {[mib syntax $table] != "SEQUENCE"} return

    set s [SnmpOpen $ip]
    writeln "SNMP table [mib name $table] of $host \[$ip\]:"

    $s walk x $table {
	set x [join $x]
	set oid [lindex $x 0]
	set name [mib name $oid]
	set pfx [lindex [split $name .] 0]
	set idx [join [lrange [split $name .] 1 end] .]
	set val [lindex $x 2]
	set len [string length $val]
	if {![info exists xindex($pfx)]} {
	    set xindex($pfx) [string length $pfx]
	} 
	if {$len > $xindex($pfx)} {
	    set xindex($pfx) $len
	}
	set yindex($idx) ""
	set value($pfx:$idx) $val
	if {![info exists value(Instanze:$idx)]} {
	    set value(Instance:$idx) $idx
	    set xindex(Instance) [string length $idx]
	    if {$xindex(Instance) < 8} { set xindex(Instance) 8 }
	}
    }
    if {![info exists xindex]} return

    # Try to display the table as a table. This is no good solution... 

    while {[array names xindex] != "Instance"} {
	set foo "Instance"
	set total $xindex(Instance)
	set fmt "%$xindex(Instance)s"
	set txt [format $fmt Instance]
	foreach pfx [array names xindex] {
	    if {$pfx == "Instance"} continue
	    incr total $xindex($pfx)
	    incr total 2
	    if {$total > 160} break
	    set fmt "  %$xindex($pfx)s"
	    append txt [format $fmt $pfx]
	    lappend foo $pfx
	}
	writeln $txt
	if {[catch {lsort -integer [array names yindex]} sorted]} {
	    set sorted [lsort [array names yindex]]
	}
	foreach idx $sorted {
	    set txt ""
	    foreach pfx $foo {
		set fmt "%$xindex($pfx)s  "
		append txt [format $fmt $value($pfx:$idx)]
	    }
	    writeln $txt
	}
	foreach pfx $foo {
	    if {$pfx != "Instance"} { 
		unset xindex($pfx) 
	    }
	}
    }

    writeln
    SnmpClose $s
}

##
## Edit a MIB table.
##

proc SnmpEditTable {ip table args} {

    if {[catch {nslook $ip} host]} { set host "" }
    set host [lindex $host 0]
    set tablename [mib name $table]

    set s [SnmpOpen $ip]
    set list ""
    if {[catch {
	$s walk x [lindex [mib successor [mib successor $table]] 0] {
	    foreach x $x {
		set oid  [lindex $x 0]
		set val  [lindex $x 2]
		set name [mib name $oid]
		set pfx [lindex [split $name .] 0]
		set idx [join [lrange [split $name .] 1 end] .]
		lappend list "$tablename $idx"
	    }
	}
    } error]} {
	ined acknowledge "Failed to retrieve $tablename:" "" $error
	SnmpClose $s
        return
    }
    if {$list == ""} {	
	SnmpClose $s
	return
    }

    set result [ined list "Select a row to edit:" $list [list edit cancel]]
    if {[lindex $result 0] == "cancel"} {
	SnmpClose $s
        return
    }

    set idx [lindex [lindex $result 1] 1]
    
    set list ""
    foreach pfx [mib successor [mib successor $table]] {
	set access [mib access $pfx.$idx]
	if {[string match *write* $access]} {
	    set name [lindex [split [mib name $pfx.$idx] .] 0]
	    if {[catch {lindex [lindex [$s get $pfx.$idx] 0] 2} value]} {
		writeln "$value"
		continue
	    }
	    lappend list [list $name [lindex [lindex [$s get $pfx.$idx] 0] 2]]
	    lappend oidlist "$pfx.$idx"
	}
    }
    
    if {$list != ""} {
	set txt "Edit instance $idx of $tablename:"
	set result [ined request $txt $list [list "set values" cancel]]
	if {[lindex $result 0] != "cancel"} {
	    set i 0
	    foreach value [lrange $result 1 end] {
		lappend varbind [list [lindex $oidlist $i] $value]
		incr i
	    }
	    if {[catch {$s set $varbind} error]} {
		ined acknowledge "Set operation failed:" "" $error
	    }
	}
    }

    SnmpClose $s
}

##
## Dump a hierarchy of the MIB. Sometimes called a walk in SNMP
## terminology.
##

proc SnmpWalk {ip path} {

    if {[catch {nslook $ip} host]} { set host "" }
    set host [lindex $host 0]

    writeln "$host \[$ip\] $path:"
    set s [SnmpOpen $ip]

    if {[catch {
	$s walk x $path {
	    set x [lindex $x 0]
	    writeln "  [mib name [lindex $x 0]] = [lindex $x 2]"
	}
    } err]} {
	writeln $err
    }

    SnmpClose $s
}
