#! /usr/local/bin/scotty -inf
##
## Simple SNMP monitoring utilities for tkined.
##
## 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.
##

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


LoadDefaults snmp monitor

SnmpInit SNMP-Monitor

if {[info exists default(interval)]} {
    set interval $default(interval)
} else {
    set interval 60
}
if {![info exists default(graph)]} {
    set default(graph) false
}

##
## Create a chart. Honours the monitor.graph default definition.
##

proc CreateChart {id x y} {
    global default
    if {$default(graph) != "true"} {
	set id [CloneNode $id [ined create STRIPCHART] $x $y]
    } else {
	set id [CloneNode $id [ined create GRAPH]]
    }
    return $id
}

##
## Tell tkined to restart a command when a saved map gets loaded.
##

proc save { cmd } {
    set rlist [ined restart]
    lappend rlist $cmd
    ined restart $rlist
}

##
## Restart a monitor job by calling cmd. Build the list of objects
## from tkined using ined retrieve commands.
##

proc restart { cmd args } {

    global interval

    if {[regexp {^[0-9]*$} [lindex $args 0]]} {
	set itv  [lindex $args 0]
	set jid  [lindex $args 1]
	set args [lrange $args 2 end]
	set old_interval $interval
	set interval $itv
    }

    foreach id $args {
	catch {ined -noupdate clear $id}
	catch {ined -noupdate clear $id}
    }
    catch {eval $cmd $args}

    if {[info exists old_interval]} {
	set interval $old_interval
    }
}

##
## Return a short readable name for a given oid.
##

proc shortname { oid } {
    set oid [split $oid .]
    set len [llength $oid]
    for {set i $len} {$i > 0} {incr i -1} {
	set test [join [lrange $oid 0 $i] .]
	if {! [catch {mib name $test} name]} {
	    if {$i < $len} {
		set inst [join [lrange $oid [expr $i + 1] $len] .]
		return "$name.$inst"
	    } else {
		return $name
	    }
	}
    }
    return [join $oid .]
}

##
## Monitor a SNMP variable in a stripchart. Not complete yet.
##

proc show_variable { ids } {

    global sv_snmp sv_value sv_time sv_name sv_descr

    set ids [MoJoCheckIds show_variable $ids]
    if {$ids == ""} return

    foreach id $ids {
	set snmpvar [lindex [$sv_snmp($id) get $sv_name($id)] 0]
	set now [getclock]
	set type [lindex $snmpvar 1]
	switch [string toupper $type] {
	    GAUGE {
		set val [lindex $snmpvar 2]
	    }
	    INTEGER {
		set val [lindex $snmpvar 2]
	    }
	    COUNTER {
		set val [expr {([lindex $snmpvar 2] - $sv_value($id))}]
	    }
	}
	if {$now > $sv_time($id)} {
	    set val [expr {$val / ( $now-$sv_time($id) )}]
	} else {
	    set val 0
	}
	ined values $id $val
	ined -noupdate attribute $id $sv_descr($id) "$sv_descr($id) $val"
	set sv_value($id) [lindex $snmpvar 2]
	set sv_time($id) $now
	set txt "[ined name $id] $sv_descr($id)"
	MoJoCheckThreshold $id $txt $val
    }
}

##
## Start a new monitoring job for the device given by ip. The args
## list contains node ids with the snmp variable oid to display.
##

proc start_snmp_monitor { ip args } {

    global interval
    global sv_snmp sv_value sv_time sv_name sv_descr

    set sh [SnmpOpen $ip]

    set ids ""
    set re_args ""

    while {$args != ""} {
	set id [lindex $args 0]
	set var [lindex $args 1]

	if {$id != ""} {
	    set value [SnmpGet $sh $var]
	    set sv_value($id) $value
	    set sv_time($id) [getclock]
	    set sv_snmp($id) $sh
	    set sv_name($id) $var
	    set sv_descr($id) [shortname $var]
	    lappend ids $id

	    ined -noupdate attribute $id $sv_descr($id) $sv_descr($id)
	    ined -noupdate label $id $sv_descr($id)
	}
	
	set args [lrange $args 2 end]
	append re_args " \$$id $var"
    }

    if {$ids != ""} {
	set jid [job create [list show_variable $ids] [expr {$interval * 1000}]]
	save "restart start_snmp_monitor $interval $jid $ip $re_args"
    }
}

proc monitor_variable {list varname} {
    global strip
    ForeachIpNode id ip host $list {
	set sh [SnmpOpen $ip]
	# expand the varname to all instances of this var
	set vars ""
	set var [mib oid $varname]
	set pfx $var
	while {1} {
	    set var [$sh getnext $var]
	    if {![string match "$pfx*" [lindex [lindex $var 0] 0]]} break
	    set type [string toupper [lindex [lindex $var 0] 1]]
	    if {[lsearch "GAUGE COUNTER INTEGER" $type] >= 0} {
		lappend vars [lindex [lindex $var 0] 0]
	    }
	}
	
	if {$vars == ""} continue
	
	set args $ip
	set i 0
	foreach var $vars {
	    set nid [CreateChart $id [expr {30+$i}] [expr {30+$i}]]
	    append args " $nid $var"
	    incr i
	}
	
	eval start_snmp_monitor $args
	
	SnmpClose $sh
    }
}

##
## This is the command interface to the generic SNMP variable
## monitor.
##

proc "Monitor Variable" { list } {

    static varname

    if {![info exists varname]} {
	set varname ip.ipForwDatagrams
    }

    set res [ined request "Tell me which variable I should show you:" \
	    [list [list "SNMP Variable:" $varname] ] \
	    [list start cancel] ]
    if {[lindex $res 0] == "cancel"} return
    
    set varname ""
    foreach var [lindex $res 1] {
	if {[catch {mib oid $var}]} {
	    ined acknowledge "Unknown SNMP variable \"$var\"."
	    continue
	}
	lappend varname $var
    }

    foreach var $varname {
	monitor_variable $list $var
    }
}

##
## Monitor the interface load as reported by the interface groups 
## of MIB II.
##

##
## Calculate the interface utilisation. This is done using the formula
##
## util = ( 8 * ( delta (ifInOctets, t1, t0) 
##              + delta (ifOutOctets, t1, t0) ) / (t1 - t0) ) / ifSpeed
##
## This formula returns incorrect results for full-duplex point to point
## links. In this case, the following formula should be used:
##
## util = ( 8 * max ( delta (ifInOctets, t1, t0) 
##                  + delta (ifOutOctets, t1, t0) ) / (t1 - t0) ) / ifSpeed
##
## See Simple Times, 1(5), November/December, 1992 for more details.
##

proc show_ifload { ids } {

    global sv_snmp sv_idx sv_time sv_duplex
    global sv_ifInOctets sv_ifOutOctets sv_ifSpeed sv_ifDescr

    set ids [MoJoCheckIds show_ifload $ids]
    if {$ids == ""} return

    foreach id $ids {

	set idx $sv_idx($id)

	set value [SnmpGet $sv_snmp($id) "ifInOctets.$idx ifOutOctets.$idx"]

	set ifIn  [lindex $value 0]
	set ifOut [lindex $value 1]
	set now   [getclock]

	set deltaIn  [expr {$ifIn - $sv_ifInOctets($id)}]
	set deltaOut [expr {$ifOut - $sv_ifOutOctets($id)}]

	if {$sv_duplex($id)} {
	    set delta [expr {$deltaIn > $deltaOut ? $deltaIn : $deltaOut}]
	} else {
	    set delta [expr $deltaIn + $deltaOut]
	}

	if {$now > $sv_time($id)} {
	    set val [expr {( 8.0 * $delta / ($now - $sv_time($id)) )
				          / $sv_ifSpeed($id) * 100}]
	} else {
	    set val 0
	}
	ined values $id $val
	ined -noupdate attribute $id "interface load" \
	    [format "$sv_ifDescr($id) %.2f %%" $val]

	set txt "[ined name $id] $sv_ifDescr($id) interface load"
	MoJoCheckThreshold $id $txt $val "%%"

	set sv_time($id) $now
	set sv_ifInOctets($id)  $ifIn
	set sv_ifOutOctets($id) $ifOut
    }
}

##
## Start a new interface load monitoring job for the device given by ip. 
## The args list contains node ids with the interface index to display.
##

proc start_ifload_monitor { ip args } {

    global interval
    global sv_snmp sv_idx sv_time sv_duplex
    global sv_ifInOctets sv_ifOutOctets sv_ifSpeed sv_ifDescr

    # Note, IANAifType (RFC 1573) has somewhat different encodings
    # than RFC 1213. We use RFC 1213 style.

    set full_duplex {
	regular1822 hdh1822 ddn-x25 rfc877-x25 lapb sdlc ds1 e1 
	basicISDN primaryISDN propPointToPointSerial ppp slip ds3 sip 
	frame-relay
    }

    set sh [SnmpOpen $ip]

    set ids ""
    set re_args ""

    while {$args != ""} {
	set id [lindex $args 0]
	set idx [lindex $args 1]
	
	if {$id != ""} {
	    
	    if {[catch {$sh get [list ifInOctets.$idx ifOutOctets.$idx \
		    ifSpeed.$idx ifDescr.$idx ifType.$idx ]} value]} {
		writeln "fatal error for $ip ($id) : $value"
		set args [lrange $args 2 end]
		continue
	    }
	    
	    set sv_ifInOctets($id)  [lindex [lindex $value 0] 2]
	    set sv_ifOutOctets($id) [lindex [lindex $value 1] 2]
	    set sv_ifSpeed($id)     [lindex [lindex $value 2] 2]
	    set sv_ifDescr($id)     [lindex [lindex $value 3] 2]
	    set sv_ifType($id)      [lindex [lindex $value 4] 2]
	    set sv_time($id)        [getclock]
	    set sv_snmp($id) $sh
	    set sv_idx($id) $idx
	    if {[lsearch $full_duplex $sv_ifType($id)] < 0} {
		set sv_duplex($id) 0
	    } else {
		set sv_duplex($id) 1
	    }
	    lappend ids $id

	    ined -noupdate attribute $id "interface load" \
		"ifLoad $sv_ifDescr($id)"
	    ined -noupdate label $id "interface load"
	}
	
	set args [lrange $args 2 end]
	append re_args " \$$id $idx"
    }

    if {$ids != ""} {
	set jid [job create [list show_ifload $ids] [expr {$interval * 1000}]]
	save "restart start_ifload_monitor $interval $jid $ip $re_args"
    }
}

proc "Interface Load" { list } {
    global strip
    ForeachIpNode id ip host $list {
	set sh [SnmpOpen $ip]
	
	# test which interfaces might be of interest	
	set iflist ""
	if {[catch {
	    $sh walk x "ifIndex ifAdminStatus ifSpeed ifInOctets ifOutOctets" {
		set idx    [lindex [lindex $x 0] 2]
		set status [lindex [lindex $x 1] 2]
		set speed  [lindex [lindex $x 2] 2]
		
		if {$status != "up" && $status != "1"} {
		    writeln "Interface $idx on $host \[$ip\] ignored (ifAdminStatus = $status)"
		    continue
		}
		
		if {$speed < 300} {
		    writeln "Interface $idx on $host \[$ip\] ignored (ifSpeed = $speed)"
		    continue
		}
		
		lappend iflist $idx
	    }
	}]} {
	    SnmpClose $sh
	    continue
	}

	set args $ip
	set i 0
	foreach if $iflist {
	    set nid [CreateChart $id [expr {30+$i}] [expr {30+$i}]]
	    append args " $nid $if"
	    incr i
	}
	
	eval start_ifload_monitor $args
	
	SnmpClose $sh
    }
}

##
## Count the number of connections to a TCP service and display the
## result in a stripchart.
##

proc tcp_service_user { ids } {
    
    global sv_snmp sv_port
    
    set ids [MoJoCheckIds tcp_service_user $ids]
    if {$ids == ""} return
    
    foreach id $ids {
	
	set count 0
	
	if {[catch {
	    $sv_snmp($id) walk x "tcpConnState tcpConnLocalPort" {
		set tcpConnState        [lindex [lindex $x 0] 2]
		set tcpConnLocalPort    [lindex [lindex $x 1] 2]
		# established == 5
		if { ($tcpConnState == 5 || $tcpConnState == "established")
		     && $tcpConnLocalPort == $sv_port($id)} { incr count }
	    }
	}]} continue

	ined values $id $count
    }
}

proc start_tcp_service_user { ip id port } {

    global sv_snmp sv_port
    global interval

    set sh [SnmpOpen $ip]
    catch {ined -noupdate scale $id 1}
    ined -noupdate label $id text
    set sv_snmp($id) $sh
    set sv_port($id) $port

    set jid [job create [list tcp_service_user $id] \
	    [expr {$interval * 1000}]]

    save "restart start_tcp_service_user $interval $jid $ip \$$id $port"
}

proc "TCP Service User" { list } {

    set service [IpService tcp]

    if {$service == ""} return

    set name [lindex $service 0]
    set port [lindex $service 1]

    set args ""
    ForeachIpNode id ip host $list {
	set nid [CreateChart $id -20 20]
	ined -noupdate attribute $nid text "$name established"
	start_tcp_service_user $ip $nid $port
    }
}

##
## Display the jobs currently running.
##

proc "Monitor Job Info" { list } {
    MoJoInfo
}

##
## Modify the state or the interval of a running job.
##

proc "Modify Monitor Job" { list } {
    MoJoModify
}

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

proc "Set Parameter" {list} {

    global snmp_community snmp_timeout snmp_retries snmp_port
    global snmp_protocol snmp_context
    global interval
    global default

    set action [MoJoThresholdAction]

    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 "Interval \[s\]:" $interval entry 10] \
          [list "Threshold action:" $action check syslog flash write] \
	  [list "Use Graph Diagram:" $default(graph) radio true false ] ] \
	[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]
    set interval       [lindex $result 7]
    set action         [lindex $result 8]
    set default(graph) [lindex $result 9]

    MoJoThresholdAction $action
}

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

proc "Show Defaults" {list} {
    ShowDefaults
}

##
## Display some help about this tool.
##

proc "Help SNMP-Monitor" {list} {
    ined browse "Help about SNMP-Monitor" {
	"Monitor Variable:" 
	"    Monitor a SNMP variable in a stripchart." 
	"" 
	"Interface Load:" 
	"    Compute the load of an interface and display it in a stripchart." 
	"" 
	"TCP Service User" 
	"    Show the number of users of a given TCP service. This can be" 
	"    useful to monitor NNTP connections." 
	"" 
	"Monitor Job Info:" 
	"    This command display information about all monitoring jobs" 
	"    started by this monitor script." 
	"" 
	"Modify Monitor Job:" 
	"    Select one of the monitoring jobs and modify it. You can change" 
	"    the sampling interval and switch the state from active (which" 
	"    means running) to suspended." 
	"" 
	"Set Parameter:" 
	"    This dialog allows you to set the sampling interval and " 
	"    SNMP related parameter like retries and community names." 
	"" 
	"Show Defaults:" 
	"    Show the defaults that may be defined in the tkined.defaults" 
	"    files. This script makes use of definitions in the form" 
	"" 
	"        snmp.community:      <string>" 
	"        snmp.port:           <number>" 
	"        snmp.retries:        <number>" 
	"        snmp.timeout:        <number>" 
	"" 
	"        snmp.community.<ip>: <string>" 
	"        snmp.port.<ip>:      <number>" 
	"        snmp.retries.<ip>:   <number>" 
	"        snmp.timeout.<ip>:   <number>" 
	"" 
	"    where <ip> is an IP address in dot notation. The default " 
	"    interval can be set using:" 
	"" 
	"        monitor.interval:    <number>" 
    }
}

##
## Delete the menus created by this interpreter.
##

proc "Delete SNMP-Monitor" {list} {

    global menus

    if {[job list] != ""} {
	set res [ined confirm "Kill running monitoring jobs?" \
		 [list "kill & exit" cancel] ]
	if {$res == "cancel"} return
    }

    DeleteClones

    foreach id $menus { ined delete $id }
    exit
}

set menus [ ined create MENU "SNMP-Monitor" \
	   "Monitor Variable" "" \
	   "Interface Load" "TCP Service User" "" \
	   "Monitor Job Info" "Modify Monitor Job" "" \
	   "Set Parameter" "Show Defaults" "" \
	   "Help SNMP-Monitor" "Delete SNMP-Monitor" ]
