# copyright (C) 1997-2006 Jean-Luc Fontaine (mailto:jfontain@free.fr)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

# $Id: nagios.tcl,v 1.12 2006/01/28 19:16:59 jfontain Exp $


package provide nagios [lindex {$Revision: 1.12 $} 1]
package require network 1
package require stooop 4.1
namespace import stooop::*
package require switched
if {[catch {package require Thread 2.5}]} {
    namespace eval nagios {variable threads 0}
} else {                                                                                  ;# load thread worker class implementation
    package require threads 1
    namespace eval nagios {variable threads 1}
}
package require linetask 1


namespace eval nagios {

    # use an empty hidden column as index so that cell labels do not include status value
    array set data {
        updates 0
        0,label {} 0,type ascii 0,message {} 0,0 {}
        1,label status 1,type integer 1,message {plugin status (0: OK, 1: Warning, 2: Critical, 3: Unknown)}
        2,label output 2,type ascii 2,message {text output of the plugin} 2,anchor left
        pollTimes {60 10 20 30 120 300 600}
        views {{visibleColumns {1 2}}}
        persistent 1
        switches {--command 1 -C 0 --daemon 0 -i 1 --identifier 1 -p 1 -r 1 --remote 1}
    }
    set file [open nagios.htm]
    set data(helpText) [::read $file]                                                         ;# initialize HTML help data from file
    close $file
    unset file

    proc initialize {optionsName} {
        upvar 1 $optionsName options
        variable task
        variable remote
        variable data
        variable threads                                                                     ;# whether threads package is available
        variable thresholds

        catch {set command $options(--command)}
        if {![info exists command]} {
            error {plugin command required: use --command option}
        }
        set command [string trim $command]
        set path /usr/lib/nagios/plugins/                                                                ;# default path for plugins
        switch -glob $command {
            /* {
                set name [file tail $command]                                                   ;# absolute path included in command
                regexp {^check_(\S+)} $name ignore name
            }
            check_* {
                regexp {^check_(\S+)} $command ignore name
                set command $path$command                                                         ;# full plugin name: just add path
            }
            default {
                regexp {^\S+} $command name
                set command ${path}check_$command                                                               ;# short plugin name
            }
        }
        if {![info exists name]} {
            error "could not determine plugin name from: $command"
        }
        catch {set locator $options(-r)}; catch {set locator $options(--remote)}                                ;# favor long option
        if {[info exists locator]} {
            # for remote monitoring, decode protocol, remote user and host
            foreach {remote(protocol) remote(user) remote(host)} [network::parseRemoteLocator $locator] {}
            set remote(rsh) [string equal $remote(protocol) rsh]
            set data(identifier) nagios($remote(host),$name)                     ;# note: user may also force identifier (see below)
            # important: pack data in a single line using special control separator characters
            set remote(command) "echo -n `$command 2>&1`\\\v\$? | tr '\\n' '\\v'"
            if {$::tcl_platform(platform) eq "unix"} {
                if {$remote(rsh)} {
                    set command "rsh -n -l $remote(user) $remote(host) {$remote(command)}"
                } else {
                    set command ssh
                    if {[info exists options(-C)]} {append command { -C}}                                        ;# data compression
                    if {[info exists options(-i)]} {append command " -i \"$options(-i)\""}                          ;# identity file
                    if {[info exists options(-p)]} {append command " -p $options(-p)"}                                       ;# port
                    # note: redirect standard error to pipe output in order to be able to detect remote errors
                    append command " -T -l $remote(user) $remote(host) 2>@ stdout"
                }
            } else {                                                                                                      ;# windows
                if {$remote(rsh)} {
                    error {use -r(--remote) ssh://session syntax (see help)}
                }
                set remote(rsh) 0                                  ;# note: host must be a putty session and pageant must be running
                set command "plink -ssh -batch -T $remote(host) 2>@ stdout"
            }
            if {$remote(rsh)} {
                set access r                                                                        ;# writing to pipe is not needed
            } else {
                set access r+                                                                                 ;# bi-directional pipe
                # terminate remote command output by a newline so that the buffered stream flushes it through the pipe as soon as
                # the remote data becomes available:
                append remote(command) {; echo}
            }
        } else {                                                                                                       ;# local host
            unset -nocomplain host
            regexp {\s+-H\s+(\S+)} $command ignore host                        ;# automatically retrieve host in plugin command line
            if {[info exists host]} {                                            ;# note: user may also force identifier (see below)
                set data(identifier) nagios($name,$host)
            } else {
                set data(identifier) nagios($name)
            }
            regexp {^\S+} $command plugin
            exec $plugin -V                                         ;# detect errors early by attempting immediate version retrieval
            set access r                                                                            ;# writing to pipe is not needed
            # capture plugin output (make sure to also capture all error output) and its status code in a single line
            # do not use simple quotes (see lineTask -command documentation)
            set command "sh -c \"echo -n `$command 2>&1`\\v\$?\" |& tr '\\n' '\\v'"
        }
        catch {set data(identifier) nagios($options(--identifier))}                                         ;# may be forced by user
        set task(object) [new lineTask\
            -command $command -callback nagios::read -begin 0 -access $access -translation lf -threaded $threads\
        ]
        if {[info exists remote] && ![info exists options(--daemon)] && !$remote(rsh)} {
            lineTask::begin $task(object)                                    ;# for ssh, detect errors early when not in daemon mode
        }                                                       ;# note: for rsh, shell and command need be restarted at each update
        set thresholds {}       ;# trigger thresholds creation once module is initialized in order to get complete thresholds labels
        set task(busy) 0
    }

    proc update {} {
        variable task
        variable remote

        if {$task(busy)} return                                                     ;# core invocation while waiting for remote data
        set task(busy) 1
        if {[lineTask::end $task(object)]} {                                                        ;# local, rsh or ssh daemon mode
            lineTask::begin $task(object)                           ;# note: for rsh, shell and command are restarted here each time
        }
        if {[info exists remote] && !$remote(rsh)} {
            lineTask::write $task(object) $remote(command)                 ;# start data retrieval by sending command to remote side
        }
    }

    proc process {status lines} {                                                    ;# process plugin data lines and update display
        variable data
        variable thresholds

        set data(0,1) $status
        regsub {\s*\|.*$} [lindex $lines 0] {} data(0,2)           ;# suppress plugin extra data (white space and after pipe symbol)
        incr data(updates)
        if {[info exists thresholds] && ([llength [currentThresholds]] == 0)} {        ;# do not override existing thresholds if any
            unset thresholds                                                 ;# create thresholds once now that target cell is valid
            createThreshold 0 1 -type equal -value 0 -level info -color green -active 1
            createThreshold 0 1 -type equal -value 1 -level warning -color orange -active 1
            createThreshold 0 1 -type equal -value 2 -level critical -color red -active 1
            createThreshold 0 1 -type equal -value 3 -level notice -color white -active 0
        }
    }

    proc read {line} {                                       ;# read remote data now that it is available and possibly handle errors
        variable task
        variable remote

        switch $lineTask::($task(object),event) {
            end {
                # either valid data availability as rsh connection was closed or local command finished, or connection broken for
                # ssh, in which case remote shell command will be attempted to be restarted at next update
                if {$line eq ""} {
                    set task(busy) 0
                    return    ;# happens after local command error which resulted in an error event just processed in this procedure
                }
            }
            error {
                set message "data error: $lineTask::($task(object),error)"
            }
            timeout {
                if {[info exists remote]} {                                                   ;# remote host did not respond in time
                    set message "timeout on remote host: $remote(host)"
                } else {
                    set message {timeout while waiting for plugin data}
                }
            }
        }
        if {[info exists message]} {
            flashMessage $message
        }
        # note: in case of an unexpected event, task insures that line is empty
        set lines [split [string trimright $line \v] \v]
        set status [lindex $lines end]; if {![string is integer -strict $status]} {set status ?}             ;# plugin return status
        process $status [lrange $lines 0 end-1] 
        set task(busy) 0
    }

}
