#!/usr/bin/perl #https://github.com/zorkian/nagios-irc-bot/blob/master/nagiosirc.pl #http://www.update.uu.se/~zrajm/programs/irssi-scripts/fifo_remote.pl-0.5 #https://github.com/mikegrb/irssi-scripts/blob/master/nagios-ack.pl # DONE # 1/ check nagios.log # 2/ setup an array of alerts # 3/ ability to remove-change alerts on new notification for same host/[service] # 4/ dialog with livestatus to replay full alerts on load/setup : irssi command /nagrefresh # 5/ define !recheck to nagios # 6/ ability to define chan/server on irssi configuration instead of bot config. # TODO # 3/ define an ACK command # define !ack irc command # use %ACK_INDEX by alert type HOST / SERVICE # use %ACK_INDEX_LEVEL by alert type CRIT/UP/DOWN/WARN/UNK… use strict; use vars qw($VERSION %IRSSI); use Irssi; use POSIX "sys_wait_h"; use POSIX qw(strftime); use Term::ANSIColor qw/ :constants /; use Fcntl; # provides `O_NONBLOCK' and `O_RDONLY' constants our ( $FIFO, # fifo absolute filename (expanded from Irssi config) $FIFO_HANDLE, # fifo filehandle for `open' et al. $FIFO_TAG ); # fifo signal tag for `input_add' # probably not very useful for other people but who knows? $VERSION = "0.2.0"; %IRSSI = ( authors => 'asr', contact => 'root@lautre.net', name => 'nagios-ack', description => 'ack nagios alerts in irc / follow nagios log', license => 'GPLv2', url => 'http://www.lautre.net/', changed => '20141006', modules => '' ); # Put signals to irssi ################################################### Irssi::settings_add_str($IRSSI{name}, # default fifo_remote_file 'fifo_remote_file', '/home/tc-14/var/nagios-fifo'); # Irssi::settings_add_str("nagios_ack", "nagios_ack_channel", "#naglautre"); #Irssi::settings_add_str("nagios_ack", "nagios_ack_channel", "#root"); Irssi::settings_add_str("nagios_ack", "nagios_ack_nick", ""); Irssi::settings_add_str("nagios_ack", "nagios_command", "/var/lib/nagios3/rw/live"); #Irssi::command_bind( 'ack', \&nagios_ack ); #Irssi::command_bind( 'nagstat', \&nagios_status ); Irssi::command_bind( 'nagrefresh', \&query_nagios_status ); # CONFIG: point this where your Nagios configuration files live #my $nagioslog = "/var/log/nagios3/nagios.log"; # To be used to check immediately / hosts-stats my $nagioscmd = "/var/lib/nagios3/rw/live"; my %renot; # { "host" or "host:service" => time_last_notification } my @cmdqueue = (); my %ignore = (); my @ACKS; my %ACK_Ind; my %C = ( K => "\x0301", # 00. White B => "\x0302", # 01. Black G => "\x0303", # 02. Blue (Navy) R => "\x0304", # 03. Green # 04. Red V => "\x0306", # 05. Brown (Maroon) O => "\x0307", # 06. Purple Y => "\x0308", # 07. Orange Gg => "\x0309", # 08. Yellow # 09. Light Green (Lime) C => "\x0311", # 10. Teal (Green/Blue Cyan) Bb => "\x0312", # 11. Light Cyan (Cyan) (Aqua) Ma => "\x0313", # 12. Light Blue (Royal) Gr => "\x0314", # 13. Pink (Light Purple) (Fuchsia) W => "\x0315", # 14. Grey # 15. Light Grey (Silver) Z => "\x03", ); my @NagStates = qw/UP DOWN WARNING UNKNOWN .. .. .. .. .. .. OK WARNING CRITICAL UNKNOWN/; # Bold: U+0002 ("0x02") — Example: ^Bold Text^ whereas ^ represents the control character. # Italics: U+001D ("0x1D") — Example: ^Italicized Text^ whereas ^ represents the control character. # Underline: U+001F ("0x1F") — Example: ^Underlined Text^ whereas ^ represents the control character. # The control character used for color is U+0003 ("0x03"). my $state_to_color = { OK => $C{G}, UP => $C{G}, WARNING => $C{Y}, CRITICAL => $C{R}, DOWN => $C{V}, UNKNOWN => $C{Gr}, }; my ($status_line); my @match; my ($msg,$num); my ($message,$i); my ($type,$data); my ($d,$type,$host,$service,$state,$output); my ( $host, $svc, $state, $msg, $id ); #my $state_to_color = { OK => '', UP => '', WARNING => '', CRITICAL => '', DOWN => '', UNKNOWN => '' }; # simple subs sub TRUE() { 1 } # some constants [perlsyn(1) sub FALSE() { "" } # "Constant Functions"] sub DEBUG(@) { print "%B", join(":", @_),"%n" }# DEBUG thingy # Acknowledge alerts ##################################################### my $last_alert; my ($server, $msg, $nick, $addr, $target); my ( $param, $server, $window ); my (@issue, $issue); #sub on_public { # ($server, $msg, $nick, $addr, $target) = @_; # # return unless $target eq Irssi::setting_get_str("nagios_ack_channel") && $nick eq Irssi::setting_get_str("nagios_ack_nick"); # $last_alert = $msg if $msg =~ m/^PROBLEM/; # return; #} my $DEBUG=Irssi::settings_get_str("nagios_ack_channel"); sub nagios_ack { ( $param, $server, $window ) = @_; @issue = parse_status(); if (!@issue) { #$window->print("Failed to parse last status: $last_alert"); } my $message = " ACK ".$param . join ' ', reverse @issue; Irssi::active_server->command('MSG ' . $DEBUG . $message); } sub nagios_check { #1412375470;filer2;DISK_all;2;W=10% C=5% ( $param, $server, $window ) = @_; @issue = parse_status(); if (!@issue) { #$window->print("Failed to parse last status: $last_alert"); } my $message = " CHECK ".$param . join ' ', reverse @issue; Irssi::active_server->command('MSG ' . $DEBUG . $message); # validate_alert($$$$$$) { # $d,$type,$host,$state,$output,$service } sub nagios_status { (undef, undef, $window) = @_; $issue = join ',', map { "'" . $_ . "'" } reverse parse_status(); $window->print("Last issue: '$last_alert' ($issue)"); } sub parse_status { return $1 if $last_alert =~ /^PROBLEM - (\S+) is DOWN/; return ($1, $2) if $last_alert =~ /^PROBLEM - (\S+) on (\S+) is/; return; } sub nagios_inject { $last_alert = shift; } sub time2date($) { my ($d)=@_; return strftime("%d/%m/%y %H:%M", localtime($d)); } sub mark($$$) { my ($Flag,$I,$state)=@_; my $M=sprintf('%02d', scalar @ACKS % 100); my $N=sprintf('%02d', $I % 100); if (($I == 0) && (scalar @ACKS == 0)) { return "[".$C{W}.$Flag.$C{Z}." ]/$M "; } else { return "[".$C{W}.$Flag.$state_to_color->{$state}.$N.$C{Z}."]/$M "; } } # Add alert to array / index sub ackable { #Irssi::print(">> On vire $host/$svc"); my $i; ( $host, $svc, $state, $msg, $d ) = @_; # ALERT if ( $state eq 'WARNING' || $state eq 'CRITICAL' || $state eq 'UNKNOWN' || $state eq 'DOWN' ) { while ($i <= $#ACKS) { # Update, or insert ? if (($ACKS[$i]->[0] eq $host) && ($ACKS[$i]->[1] eq $svc)) { $ACKS[$i]->[4]=$d; Irssi::print(">> found $i"); if ($ACKS[$i]->[2] ne $state) { # Same alert, but different level $ACKS[$i]->[2]=$state; return mark ('C', $i, $state); } else { # Same alert return mark ('!', $i, $state); } } $i++; } # New alert, insert. push (@ACKS, [ $host, $svc, $state, $msg, $d ] ); return mark('+',scalar @ACKS,$state); # Clear alert } else { # Irssi::print(">> On vire $host/$svc"); $i=0; # Find alert (need to be used by service/host index) while ($i <= $#ACKS) { if (($ACKS[$i]->[0] eq $host) && ($ACKS[$i]->[1] eq $svc)) { $ACKS[$i] = pop @ACKS; $i=$#ACKS; Irssi::print(">> found $i"); return mark('-',$i+1,$state); } $i++; } return mark('?',0,$state); } } ########################################################################## # Logfile management ##################################################### # disable fifo and erase fifo file sub destroy_fifo($) { # [2004-08-14] my ($fifo) = @_; # get args if (defined $FIFO_TAG) { # if fifo signal is active Irssi::input_remove($FIFO_TAG); # disable fifo signal undef $FIFO_TAG; # and forget its tag } # if (defined $FIFO_HANDLE) { # if fifo is open close $FIFO_HANDLE; # close it undef $FIFO_HANDLE; # and forget handle } # if (-p $fifo) { # if named fifo exists unlink $fifo; # erase fifo file undef $FIFO; # and forget filename } # return 1; # return } # # Open logfile/fifo sub open_fifo($) { # [2004-08-14] my ($fifo) = @_; # get args if (not sysopen $FIFO_HANDLE, $fifo, # open fifo for non-blocking O_NONBLOCK | O_RDONLY) { # reading print CLIENTERROR "could not open nagios logfile for reading"; return ""; # } # Irssi::input_remove($FIFO_TAG) # disable fifo reading signal if defined $FIFO_TAG; # if there is one $FIFO_TAG = Irssi::input_add # set up signal called when fileno($FIFO_HANDLE), INPUT_READ, # there's input in the pipe \&read_fifo, ''; # return 1; # } # read from fifo # (called by fifo input signal) sub read_fifo() { # [2004-08-14] # Read logfile ########################################################### foreach (<$FIFO_HANDLE>) { # for each input line chomp; # strip trailing newline parse_naglog($_); #if (/ALERT/ && /HARD;/); #Irssi::active_win->print( # show incoming commands (debug) # "\u$IRSSI{name} received command: \"$_\"", # # MSGLEVEL_CLIENTNOTICE); # # Irssi::active_win->command($_); # run incoming commands } # open_fifo($FIFO); # re-open fifo # TODO: Is the above re-opening of fifo really necessary? -- If not # invoked here `read_fifo' is called repeatedly, even though no input # is to be found on the fifo. (This seems a waste of resources to me.) } # create new fifo (erase any old) and get command prefix # (called on script loading and on user /set) sub setup() { # [2004-08-13] my $new_fifo = Irssi::settings_get_str # setting from Irssi 'fifo_remote_file'; # (and add path to it) return if $new_fifo eq $FIFO and -p $FIFO; # do nada if already exists destroy_fifo($FIFO) if -p $FIFO; # destroy old fifo create_fifo($new_fifo) # create new fifo and $FIFO = $new_fifo; # and remember that fifo # To ADD : # request to livestatus to fetch stored alerts } # create named fifo and open it for input # (called on script load and fifo name changes) sub create_fifo($) { # [2004-08-14] my ($new_fifo) = @_; # get args if (not -p $new_fifo) { # create fifo if non-existant if (system "mkfifo '$new_fifo' &>/dev/null" and system "chmod 777 '$new_fifo' &>/dev/null" and system "mknod '$new_fifo' &>/dev/null"){ print CLIENTERROR "`mkfifo' failed -- could not create named pipe"; # TODO: capture `mkfifo's stderr and show that here return ""; # } # } # $FIFO = $new_fifo; # remember fifo name open_fifo($new_fifo); # open fifo for reading } # # Query nagios ########################################################### sub query_nagios_status($){ # 1412478149;hyppocampe;apt;2;CRITICAL : unknown: qemu-utils, qemu-kvm, qemu-keymaps my ($server)=@_; my @unixcat; my $unixline; foreach $unixline ( `echo "GET services Columns: last_state_change host_name display_name state plugin_output Filter: state > 1" | unixcat $nagioscmd` ) { ($d,$host,$service,$state,$output)=split /;/,$unixline; validate_alert($d,'SERVICE',$host,$NagStates[$state+10],$output,$service); Irssi::print("%B>>%n $unixline", MSGLEVEL_CLIENTCRAP); } return; foreach $unixline ( `echo "GET hosts Columns: last_state_change host_name state plugin_output Filter: state > 1" | unixcat $nagioscmd` ) { ($d,$host,$state,$output)=split /;/,$unixline; validate_alert($d,'HOST',$host,$NagStates[$state],$output,""); Irssi::print("%B>>%n $unixline", MSGLEVEL_CLIENTCRAP); } # cat << EOF | unixcat /var/lib/nagios3/rw/live # GET services # Columns: last_state_change host_name display_name state plugin_output # Filter: state > 1 # EOF } # Parse alert to find vars ############################################### sub parse_naglog(){ $d=0; $status_line=""; $host="";$output="";$service=""; # Search for services/host informations, and post them to IRC ############ # # Delimiter : ; for logs; @ for direct nagios custom notification command # # [1412330770] SERVICE ALERT: ella;IMAPs_LOGIN;OK;SOFT;2;OK - CO1N OK LOGIN Ok. # /\[\d+\] (\w+) ALERT: (\w+);(\w+);(\w+);HARD;(\d+);(.+)/ # [1410969598] HOST ALERT: filou;DOWN;SOFT;1;PING CRITICAL - Paquets perdus = 100% # PROCESS type ALERT: host;service;STATE1;HARD;num;commentaire if (@match=$status_line =~ /\[?(\d+)\]? HOST ALERT: ([^@;]+)[@;](\w+)[@;]HARD[@;].*[@;](.+)/) { # HOST ######################## ($d,$host,$state,$output)=@match; validate_alert($d,"HOST",$host,$state,$output,$service); # SERVICE ##################### } elsif (@match=$status_line =~ /\[?(\d+)\]? (\w+) ALERT: ([^;@]+)[@;]([^;@]+)[@;](\w+)[@;]HARD[@;].*[@;](.+)/) { ($d,$type,$host,$service,$state,$output)=@match; $service=~s/[^\w\d_-]/_/g; validate_alert($d,$type,$host,$state,$output,$service); # OTHER ####################### } elsif (@match=$status_line =~ /\[\d+\] (\w+) ALERT: (.*)/) { ($type,$data)=@match; Irssi::print( "%B>>%n $IRSSI{name} $type - $data", MSGLEVEL_CLIENTCRAP) unless ($status_line =~ /[;@]SOFT[@;]/); # FALLBACK #################### } else { Irssi::print( # "%B>>%n $IRSSI{name} received command: \"$_\"", MSGLEVEL_CLIENTCRAP); # next } } sub validate_alert($$$$$$) { # $d,$type,$host,$state,$output,$service ($d,$type,$host,$state,$output,$service)=@_; next if exists $renot{"$host:$service"} && $renot{"$host:$service"} >= time() - 5; $renot{"$host:$service"} = time(); ################# # HOST $id = ackable($host,$service,$state,$output,$d); $msg = "$id".$state_to_color->{$state} . "$host:$service".$C{Z}." is $state : $output"; #Irssi::print( "%B>>%n $IRSSI{name} $msg", MSGLEVEL_CLIENTCRAP); $last_alert="NAGIOS - $service/$host is $state"; $d=time2date($d); $message = " $d - $last_alert - $msg"; Irssi::active_server->command('MSG ' . Irssi::settings_get_str("nagios_ack_channel") . " ".$message); # Irssi::print( "%B>>%n $IRSSI{name} $msg", MSGLEVEL_CLIENTCRAP); } sub event_privmsg { # Commamd channel my $K; my ($server, $data, $nick, $mask) =@_; my ($target, $text) = $data =~ /^(\S*)\s:(.*)/; #print ( "C:$target X:$text A:$admin D:$warndate L:$last W:$warn N:$nick D:$data" ); if ( $text =~ /^!nagios ?(.*)/i ) { # !nagios [*] : $server->command ( "msg ".Irssi::settings_get_str("nagios_ack_channel"). " ".scalar @ACKS." alertes"); return unless $#ACKS > 0; my $i=0; # !nagios list [*] : if ($1 =~ /^refresh/i) { query_nagios_status($server); } if ($1 =~ /^list/i) { foreach $K (@ACKS) { $server->command ( "msg ".Irssi::settings_get_str("nagios_ack_channel"). " ". mark(' ',$i++,$K->[2]) ." ".$K->[0]." / $K->[1] / $K->[2] / $K->[3] / ".time2date $K->[4]); } } elsif ($1 =~ /^help/i) { $server->command ( "msg ".Irssi::settings_get_str("nagios_ack_channel"). " !nagios list : liste des alertes nagios reçuesici"); $server->command ( "msg ".Irssi::settings_get_str("nagios_ack_channel"). " !nagios help : l'aide"); $server->command ( "msg ".Irssi::settings_get_str("nagios_ack_channel"). " !nagios ack <#ALERTE> : aquitte l'alerte"); $server->command ( "msg ".Irssi::settings_get_str("nagios_ack_channel"). " !nagios check <#ALERTE> : recheck une alerte donnée (service/host)"); $server->command ( "msg ".Irssi::settings_get_str("nagios_ack_channel"). " !nagios refresh : interroge le nagios pour avoir la liste de toutes les alertes"); } return 1; } elsif ( $text =~ /^check ?(.*)/i ){ nagios_check($1,$server,undef); } elsif ( $text =~ /^ack ?(.*)/i ){ nagios_ack($1,$server,undef); } } ########################################################################## # Main ################################################################### ########################################################################## print "starting...\n"; # clean up fifo on unload # (called on /script unload) Irssi::signal_add_first # 'command script unload', sub { # [2004-08-13] my ($script) = @_; # get args return unless $script =~ # only do cleanup when /(?:^|\s) $IRSSI{name} # unloading *this* script (?:\.[^. ]*)? (?:\s|$) /x; # destroy_fifo($FIFO) if -p $FIFO; # destroy old fifo Irssi::print("%B>>%n $IRSSI{name} $VERSION unloaded", MSGLEVEL_CLIENTCRAP); }; # setup(); # initialize setup values Irssi::signal_add('event privmsg', 'event_privmsg'); Irssi::signal_add("message public", "event_privmsg"); #Irssi::signal_add_first("message public", "on_public"); Irssi::signal_add('setup changed', \&setup); # re-read setup when it changes print CLIENTCRAP "%B>>%n $IRSSI{name} $VERSION (by $IRSSI{authors}) loaded"; 1;