#!/usr/local/bin/perl -w # vim:aw: # nsc.pl - curses-based console monitor for Netsaint and Nagios # 000206shj # $Id: nsc.pl,v 1.24 2000/03/06 18:07:41 shj Exp $ # --- Bugs ---------------------------------------------------------------- # o Chg-time sometimes shows '*undef*' (from tdif) # o Background colours sometimes don't, depends on terminaltype + emulator # --- TODO ---------------------------------------------------------------- # o configurable beep-interval? # o configurable colours # o different sort types? # o complain if status.log doesn't get updated (changed?) # o record/history feature that is browseable? # o mc-like line graphics? # o search for string.. (highlight when found) # o collapse hosts, use cursor-bar to uncollapse -- or auto-collapse, when # the screen is full (first hosts with all 'ok' svcs, etc.) # o autosizing column headings and fields (based on longest node lenght, etc.) # o regexps to always filter out uninteresting services (zombies, # postgres, ..) as long as they have 'ok' status # o details display for full (uncut) service info msg + *all* # status.log fields # --- Stuff --------------------------------------------------------------- use Curses; #use strict; # --- Options and their default values ------------------------------------ my $VERSION = "v0.52"; my $myname = 'nsc'; my $fnConfig = "$ENV{'HOME'}/.nsc.conf"; #TODO: defaults should use 'nagios' if that's what were using my %NSC_KEYWORDS = ( 'nslog', '/usr/local/var/netsaint/status.log', 'hostcfg', '/usr/local/etc/netsaint/hosts.cfg', 'reloadcmd','/usr/local/etc/rc.d/netsaint.sh reload', 'showall', '1', 'details', '1', 'reverse', '0', 'bell', '1', 'debug', '0', 'upd_freq', '2', 'version', $VERSION ); # --- Global variables ---------------------------------------------------- my $ns_app = '???'; #ns=netsaint, nag=nagios my $ns_app_long = '?unknown?'; #long version of ns_app from status.log my $num_msglines; my $first_msgline; my $lastBeep = 0; my $listoffs = 0; my %AT = (); my $myself; my @montab = ( "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ); my %sVals = ( 'CRITICAL', 0, 'HOST DOWN', 1, 'UNREACHABLE', 2, 'WARNING', 3, 'UNKNOWN', 4, 'RECOVERY', 5, 'OK', 6, 'PENDING', 7 ); $ENV{'VISUAL'} = $ENV{'EDITOR'} if !defined($ENV{'VISUAL'}); $ENV{'VISUAL'} = 'vi' if !defined($ENV{'VISUAL'}); $ENV{'PAGER'} = 'more' if !defined($ENV{'PAGER'}); # --- Curses replacement functions ---------------------------------------- sub mvaddch { local($y,$x,$ch) = @_; move($y,$x); addch($ch); } sub mvaddstr { local($y,$x,$str) = @_; move($y,$x); addstr($str); } # --- Configuration ------------------------------------------------------- sub SaveConfig { open(CFG, ">$fnConfig") || die("Can't open $fnConfig for writing: $!"); foreach $ky (sort keys %NSC_KEYWORDS) { print CFG "$ky=$CFG{$ky}\n"; } close(CFG); } #SaveConfig sub LoadConfig { %CFG = (); #TODO: Version-check on loaded configfile? if (open(CFG, "$fnConfig")) { while () { chomp; ($kw,$val) = split(/=/); if (defined($NSC_KEYWORDS{$kw})) { $CFG{$kw} = $val; } else { print STDERR "Invalid keyword $kw\n"; } } #eof CFG close(CFG); } #apply default values foreach $kw (keys %NSC_KEYWORDS) { $CFG{$kw} = $NSC_KEYWORDS{$kw} if (!defined($CFG{$kw})); } #override, cause this version to written when exiting $CFG{'version'} = $VERSION; } #LoadConfig # --- Miscellany ---------------------------------------------------------- sub numeric { return 0 if (!defined($_[0]) || ($_[0] eq '')); return ($_[0] =~ /^\d+$/); } sub xcmd { erase(); refresh(); endwin(); system $_[0]; screen_init(); drawScreen(); } #xcmd # This is some of my weirder code # Return difference between two time_t's as a string # (ex. '1h23m40s'..) sub tdif { my $dif; return '*undef*' if (!$_[0] || !$_[1]); # #oh well.. # $dif = ($_[0]>$_[1])?$_[0]-$_[1]:$_[1]-$_[0]; # same shit, different code: $dif = $_[0]-$_[1]; $dif = -$dif if ($dif<0); return "now" if ($dif == 0); my $res = ''; my %sv = ( 'Y', 365*24*60*60, 'M', 30*24*60*60, 'W', 7*24*60*60, 'd', 24*60*60, 'h', 60*60, 'm', 60); my $details = 0; foreach $key ('Y','M','W','d','h','m') { if ($dif > $sv{$key}) { $i = int($dif / $sv{$key}); $dif %= $sv{$key}; $res .= "${i}$key"; $details++; } last if ($details >= 2); } $res .= "${dif}s" if (($dif > 0) && ($details < 2)); if (defined($_[2])) { #wants just first element? #retain only first component $res = $1 if ($res =~ /^(\d+[a-z])\d/); } return $res; } #tdif # --- Low level "graphics" ------------------------------------------------ sub AT { die "$_[0] is not a defined colour" if !defined($AT{$_[0]}); return $AT{$_[0]}; } sub rdKey { move($BOTLINE,$COLS-1); refresh; timeout(1000*$_[0]) if defined($_[0]); getch(); } #rdKey # at(int x, int y, const char *str) sub at { my($x,$y,$str) = @_; if (substr($str,0,1) eq '!') { #blank rest of line? $str = substr($str, 1); $str .= (' ' x ($COLS - length($str) - $x)); } elsif (substr($str,0,1) eq '>') { #relative to right side of display? $str = substr($str, 1); $x = $COLS - length($str) - $x; } #please control yourself or I will (we never wrap) $str = substr($str, 0, ($COLS-$x)) if (length($str) > ($COLS-$x)); mvaddstr($y,$x,$str); return length($str); } #at # aat(int x, int y, attrib, const char *str) sub aat { attron($_[2]); at($_[0], $_[1], $_[3]); attroff($_[2]); return length($_[3]); } sub botline { aat(0, $BOTLINE, AT('botline'), "!$_[0]"); } sub mLine { my ($line,$attrib,$msg) = @_; $attrib = AT('normal') if (!defined($attrib)); aat(0, $line+$first_msgline, $attrib, "!$msg"); } #mLine # --- Static screen stuff ------------------------------------------------- sub drawScreen { if ($opt_colour) { start_color(); #colorpair 0 resets to zero? init_pair(0, COLOR_WHITE, COLOR_BLUE); $AT{'xxskod'} = COLOR_PAIR(0); init_pair(1, COLOR_WHITE, COLOR_BLUE); $AT{'normal'} = COLOR_PAIR(1); $AT{'notok'} = $AT{'normal'} | A_BOLD; init_pair(3, COLOR_WHITE, COLOR_RED); $AT{'critical'} = COLOR_PAIR(3); init_pair(4, COLOR_WHITE, COLOR_RED); $AT{'noisy'} = COLOR_PAIR(4); $AT{'dim'} = $AT{'normal'}; $AT{'heading'} = $AT{'normal'} | A_BOLD; $AT{'total'} = $AT{'normal'}; $AT{'botline'} = $AT{'normal'}; init_pair(9, COLOR_WHITE, COLOR_YELLOW); $AT{'warning'} = COLOR_PAIR(9) | A_BOLD; $AT{'isok'} = $AT{'normal'}; } else { $AT{'botline'} = A_BOLD; $AT{'normal'} = A_NORMAL; $AT{'notok'} = A_BOLD; $AT{'critical'} = A_REVERSE; $AT{'noisy'} = (A_REVERSE|A_BOLD|A_BLINK); $AT{'dim'} = A_DIM; $AT{'heading'} = A_UNDERLINE; $AT{'total'} = A_BOLD; $AT{'warning'} = A_BOLD; $AT{'isok'} = A_NORMAL; } #force update with background colour attron(AT('normal')); erase(); for ($i=0; $i<$LINES; $i++) { aat(0, $i, AT('normal'), ' ' x $COLS); } refresh(); aat(0, 0, AT('dim'), "$myname $VERSION - $ns_app_long"); } #drawScreen sub screen_init { initscr(); cbreak(); noecho(); nonl(); #&stdscr=warns, $stdscr=errs # intrflush(&stdscr, $FALSE); # keypad(&stdscr, $TRUE); $opt_colour = has_colors(); $num_msglines = $LINES - 5; $first_msgline = 2; $BOTLINE = $LINES-1; } #screen_init # --- Help ---------------------------------------------------------------- sub help { my @HLP = ( ' h This help', ' Space Next page', ' a Toggle between showing all services and troubled ones', ' d Toggle service details on/off', ' +/- Increase/decrease display update frequency', ' q Quit', ' r Reverse sort order', ' g Toggle bell on/off', ' ^L Redraw screen', ' C Toggle colour on/off', ' T Run top(1)', ' V View status.log', ' E Edit hosts.cfg and reload', '', 'nsc was written by Stig H. Jacobsen, ', '', 'Check http://pobox.com/~goth/nsc/ for new versions' ); drawScreen(); my $i; for ($i=0; defined($HLP[$i]) && ($i<$BOTLINE); $i++) { mLine($i, AT('normal'), "$HLP[$i]"); } botline("Press any key to continue.."); #should be enough for the most slow user.. rdKey(60); } #help # --- Main loop ----------------------------------------------------------- sub by_prior { my @a = split(/;/, $a); my @b = split(/;/, $b); print "undef $a[3]\n" if !defined($sVals{$a[3]}); print "undef $b[3]\n" if !defined($sVals{$b[3]}); if (($i = ($sVals{$a[3]} - $sVals{$b[3]})) == 0) { if (($i = ($a[1] cmp $b[1])) == 0) { my $t; $a[0] =~ /\[(\d+)\]/; $t = $1; $b[0] =~ /\[(\d+)\]/; if (numeric($1) && numeric($t)) { $i = ($1 - $t); } else { $i = 0; } } } $i = -$i if ($CFG{'reverse'}); return $i; } #by_prior sub sort_state { my @state = @_; @state = sort by_prior @state; return @state; } # --- Display single service line sub dispService { my ($line,$state) = @_; my @s = split(/;/, $state); #my $tm = shift(@s); my ($f1,$node,$service,$status,$attempts,$x1,$x2,$x3,$change) = @s; my $serv_info = $s[$#s]; return 0 if (!$CFG{'showall'} && ($status eq 'OK')); #get rid of html tags my $i1; my $i2; while ((($i1 = index($serv_info, '<')) >= 0) && (($i2 = index($serv_info, '>')) >= 0) && ($i2 > $i1) ) { $serv_info = substr($serv_info, 0, $i1) . substr($serv_info, $i2+1); } if ($f1 =~ /\[(\d+)\]/) { $date = tdif(time, $1, 'y'); } else { $date = ''; } if ($status eq 'OK') { $attrib = AT('isok'); } else { if ($status eq 'CRITICAL') { $attrib = AT('critical'); } elsif ($status eq 'WARNING') { $attrib = AT('warning'); } else { $attrib = AT('notok'); } if ( ($status !~ /^(RECOVERY|OK|PENDING)$/) && ($CFG{'bell'} && ((time - $lastBeep) > 180)) ) { beep(); $lastBeep = time; } } if ($CFG{'details'}) { my $s = ''; if (numeric($change)) { $s = &tdif(time,$change,'y'); } my $chg = "$date/$s"; my $dispnode = $node; $dispnode = ' .' if (($node eq $lastNode) && ($status eq $lastState)); my $dispstate = $status; $dispstate = ' .' if (($node eq $lastNode) && ($status eq $lastState)); #show coloured status $i = aat(0, $line+$first_msgline, AT('normal'), sprintf("%-8.8s %-12.12s ", $dispnode,$service )); $i += aat($i, $line+$first_msgline, $attrib, sprintf("%-8.8s", $dispstate)); $i += aat($i, $line+$first_msgline, AT('normal'), sprintf("! %7.7s %5.5s %s ", $chg,$attempts,$serv_info)); # aat($i, $line+$first_msgline, AT('normal'), # ' ' x ($COLS-$i)); } else { mLine($line, $attrib, sprintf("%-14.14s %s", "$node:$service",$serv_info)); } ($lastNode,$lastState) = ($node,$status); return 1; } #dispService # --- Counts, sorts & weeds servicelist sub getServiceList { my @ol = @_; my @res = (); my $s; foreach $s (@ol) { my @s = split(/;/, $s); if ($s[0] =~ / SERVICE$/) { push(@res, $s); # $longest_node = length($s[1]) # if (length($s[1]) > $longest_node); # $longest_service = length($s[2]) # if (length($s[2]) > $longest_service); $sState{$s[3]}++; } elsif ($s[0] =~ / HOST$/) { $hState{$s[2]}++; } elsif ($s[0] =~ / PROGRAM$/) { if ($ns_app eq 'ns') { # # NetSaint 0.0.7b6 Status File # [1002743117] PROGRAM;1002058577;2180;1;ACTIVE;1002058577;0;1002405600;1;1;1;0;0 $pgmUp = tdif(time, $s[1],'y'); $pgmState = $s[4]; } elsif ($ns_app eq 'nag') { # # Nagios 1.0b6 Status File # [1041516783] PROGRAM;1037463573;27646;1;1041516768;1041116400;1;1;1;1;0;0;1;0 #from nagions 1.06b6 src: snprintf(new_program_string,sizeof(program_string)-1,"[%lu] PROGRAM;%lu;%d;%d;%lu;%lu;%d;%d;%d;%d;%d;%d;%d;%d\n",(unsigned long)current_time,(unsigned long)_program_start,_nagios_pid,_daemon_mode,(unsigned long)_last_command_check,(unsigned long)_last_log_rotation,_enable_notifications,_execute_service_checks,_accept_passive_service_checks,_enable_event_handlers,_obsess_over_services,_enable_flap_detection,_enable_failure_prediction,_process_performance_data); $pgmUp = tdif(time, $s[1],'y'); #Nagios is running if status.log exists - when down, status.log #is removed and data saved to status.sav #(TODO: better support for this scheme) $pgmState = 'ACTIVE'; } else { die "huh!? ($ns_app)"; } } } #foreach @ol return sort_state(@res); } #getServiceList # --- sub showServiceList { my ($NSLOG) = @_; if (!open(STATUS, $NSLOG)) { botline("*** Can't open statuslog $NSLOG: $!"); return; } my @sysState = ; close(STATUS); # my $longest_node = 0, # $longest_service = 0; $hState{'UP'} = 0; $hState{'DOWN'} = 0; $hState{'UNREACHABLE'} = 0; $sState{'OK'} = 0; $sState{'CRITICAL'} = 0; $sState{'WARNING'} = 0; $sState{'UNKNOWN'} = 0; $sState{'PENDING'} = 0; $sState{'HOST DOWN'} = 0; #count states, lengths of hosts, services my @svcList = getServiceList(@sysState); # --- Show netsaint update/state my $s = ''; if (defined($pgmState)) { if ($pgmState eq 'ACTIVE') { $s .= ' run '; } else { $s .= ' STOPPED '."($pgmState)"; } } $s .= "$pgmUp " if (defined($pgmUp)); $s =~ s/\s+$//g; aat(0, 1, ($pgmState eq 'ACTIVE')?AT('dim'):AT('noisy'), ">$s $myself"); # --- Show if ($CFG{'details'}) { mLine(0, AT('heading'), sprintf("%-8.8s %-12.12s %-8.8s %-7.7s %5.5s %s", 'Node','Service','Status','Upd/Chg','Tries','Service information')); } else { mLine(0, AT('heading'), sprintf("%-14.14s %s", 'Node:Service', 'Service information')); } ($lastNode,$lastState) = ('',''); my $i = $listoffs; my $num_shown; if (!defined($svcList[$i])) { $i = $listoffs = 0; #reset as needed } for ($num_shown=0; ($num_shown<($num_msglines-1)) && defined($svcList[$i]); $i++) { $num_shown++ if (dispService($num_shown + 1, $svcList[$i])); } my $msg = ''; if (($num_shown == ($num_msglines-1)) && defined($svcList[$i+1])) { $msg = 'more'; } if ($listoffs > 0) { $msg .= ',' if ($msg ne ''); $msg .= "+$listoffs"; } mLine($num_msglines+$first_msgline-2, AT('normal'), "($msg)") if ($msg ne ''); for ($i=$num_shown+1; $i<$num_msglines; $i++) { mLine($i, undef, '~'); } # --- Show total scores my $s1 = sprintf(">Nodes up/dn/unreach: %d/%d/%d", $hState{'UP'}, $hState{'DOWN'}, $hState{'UNREACHABLE'}); aat(1, $BOTLINE-1, AT('total'), $s1); $s1 = sprintf(">Svcs ok/warn/crit/other: %d/%d/%d/%d", $sState{'OK'}, $sState{'WARNING'}, $sState{'CRITICAL'}, $sState{'UNKNOWN'} + $sState{'PENDING'} + $sState{'HOST DOWN'}); aat(1, $BOTLINE, AT('total'), $s1); } #showServiceList # ------------------------------------------------------------------------- # get_appname() - retrieve application name, only needed at startup # sub get_appname { my($lf) = @_; open(STATUS, $lf) || die('Try again please'); #"never" happens! my ($ln) = ; close(STATUS); if ($ln =~ /(NetSaint) (.*) Status File/) { $ns_app = 'ns'; } elsif ($ln =~ /(Nagios) (.*) Status File/) { $ns_app = 'nag'; } else { botline("*** Invalid or unrecognized statuslog: $lf"); return; } $ns_app_long = "$1 $2"; } # --- Main code here ------------------------------------------------------ $| = 1; #unbuffered tty i/o &LoadConfig(); chomp($myself = `hostname`); $myself =~ s/\..*$//g; { my @s = getpwuid($>); $myself = $s[0] . '@' . $myself; } #override .config w/cmdline arg if given my $opt_logfile = defined($ARGV[0])?$ARGV[0]:$CFG{'nslog'}; while (! -f $opt_logfile) { print "\nCan't open status.log ($opt_logfile)!\n" . "Enter location of status.log: "; chomp($opt_logfile = ); #only set in .config if not commandline supplied and exists $CFG{'nslog'} = $opt_logfile if (-f $opt_logfile && !defined($ARGV[0])); } #can't open &get_appname($opt_logfile); screen_init(); &drawScreen(); do { #show curr config before status draw (clreol) $s = sprintf("!%s/%s/%s/%s/%ds ", ($CFG{'showall'}?'All':'all'), ($CFG{'details'}?'Det':'det'), ($CFG{'bell'}?'Bell':'bell'), ($CFG{'reverse'}?'Rev':'rev'), $CFG{'upd_freq'}); aat(0, $BOTLINE-1, AT('botline'), $s); #main, live, moving, squirming display! showServiceList($opt_logfile); my @l = localtime(time); my $s = sprintf(">Last updated: %s %02d %02d:%02d:%02d", $montab[$l[4]], $l[3], $l[2], $l[1], $l[0]); aat(0, 0, AT('dim'), $s); if ($key = rdKey($CFG{'upd_freq'})) { botline(''); #clear message on input (?) } # --- Keys handling if ($key eq 'a') { $CFG{'showall'} = !$CFG{'showall'}; } elsif ($key eq 'd') { $CFG{'details'} = !$CFG{'details'}; } elsif ($key eq ' ') { $listoffs += ($num_msglines-1); } elsif ($key eq 'D') { $CFG{'debug'} = !$CFG{'debug'}; } elsif ($key eq 'g') { $CFG{'bell'} = !$CFG{'bell'}; } elsif ($key eq 'r') { $CFG{'reverse'} = !$CFG{'reverse'}; } elsif ($key eq '+') { $CFG{'upd_freq'} *= 2; botline(sprintf("Update frequency set to %.f seconds", $CFG{'upd_freq'})); } elsif ($key eq '-') { $CFG{'upd_freq'} /= 2; $CFG{'upd_freq'} = 0.5 if ($CFG{'upd_freq'} < 0.5); botline(sprintf("Update frequency set to %.f seconds", $CFG{'upd_freq'})); } elsif ($key eq sprintf("%c", ord('L')-64)) { drawScreen(); } elsif ($key eq 'C') { if (!$opt_colour && !has_colors()) { botline("Your terminal can't show colours"); } else { $opt_colour = !$opt_colour; drawScreen(); } } elsif ($key eq 'T') { xcmd("top"); #debug } elsif ($key eq 'V') { xcmd("$ENV{'PAGER'} $opt_logfile"); #debug } elsif ($key eq 'E') { xcmd("$ENV{'VISUAL'} $CFG{'hostcfg'}; echo Enter to reload ..; read; $CFG{'reloadcmd'}"); #debug } elsif ($key =~ /[h?]/) { help(); } elsif ($key eq 'q') { botline("Please come back soon"); } else { if ($CFG{'debug'}) { botline(sprintf("($num_msglines,$first_msgline,$LINES) (%d/%d/%d)", ord($key), AT('normal'),AT('botline'))); } else { botline(sprintf("Press 'h' for help")); } } } while ($key ne 'q'); endwin; &SaveConfig(); print "\n"; exit 0; # --- End of file --------------------------------------------------------- __END__ [949803251] SERVICE;dax;processes;WARNING;3/3;HARD;949803551;1;949802658;WARNING;0;0;0;0;949802658;1;Absent processes: dhcpd state? last chg enabled next check hard/soft tries state servname node status type last updated [949803458] PROGRAM;949802365;0;ACTIVE;949802365;949803267;0;949803458;0 [949803297] HOST;core;UP;949802386;0;0;0;0;1;(Host assumed to be up) [949803291] SERVICE;core;ping;OK;1/3;HARD;949803591;1;949802386;OK;0;0;0;0;0;1;PING ok - Packet loss = 0%, RTA = 4.30 ms [949803327] SERVICE;dax;smtp;OK;1/3;HARD;949803627;1;949802425;OK;0;0;0;0;0;1;SMTP ok - 0 second response time [949803367] SERVICE;dax;dns;OK;1/3;HARD;949803667;1;949802467;OK;0;0;0;0;0;1;DNS ok - 0 seconds response time, Address(es) is/are 204.71.200.75, 204.71.200.67, 204.71.200.74, 204.71.202.160 [949803451] SERVICE;dax;Zombie Processes;OK;1/3;HARD;949803751;1;949802547;OK;0;0;0;0;0;1;OK - 0 processes with Z status [949803191] SERVICE;dax;lavd;OK;1/3;HARD;949803491;1;949802587;OK;0;0;0;0;0;1;load average: 1.60, 1.33, 1.29 [949803230] SERVICE;dax;root free space;OK;1/3;HARD;949803530;1;949802627;OK;0;0;0;0;0;1;Disk ok - 513500 kB (26%) free on /dev/hda8 [949803270] SERVICE;dax;vol0 free space;OK;1/3;HARD;949803570;1;949802667;OK;0;0;0;0;0;1;Disk ok - 3901908 kB (26%) free on /dev/hda5 [949803309] SERVICE;dax;smb service;OK;1/3;HARD;949803609;1;949802407;OK;0;0;0;0;0;1;Disk ok - 3.72G (24%) free on \\localhost\lyde [949803348] SERVICE;dax;swap usage;OK;1/3;HARD;949803648;1;949802446;OK;0;0;0;0;0;1;Swap ok - Swap used: 2% (47923200 bytes out of 1620295680) [949803251] SERVICE;dax;processes;WARNING;3/3;HARD;949803551;1;949802658;WARNING;0;0;0;0;949802658;1;Absent processes: dhcpd [949802806] SERVICE;dax;postgresql;UNKNOWN;3/3;HARD;949804006;1;949802817;UNKNOWN;0;0;0;0;949802817;1;/usr/local/netsaint/libexec/check_pgsql: Database Name (pdb_homes) is not valid! [949803454] SERVICE;dax;inet link;WARNING;1/60;SOFT;949803514;1;949802606;OK;0;0;0;0;0;1;Link is up (1 channel) [949803433] SERVICE;dax;TEST;OK;1/1;HARD;949803493;1;949802645;OK;0;0;0;0;0;1;OK (imapd=1247)