#!/usr/bin/env perl ## Copyright © 2010 by Daniel Friesel ## License: WTFPL ## 0. You just DO WHAT THE FUCK YOU WANT TO. use autodie; use strict; use warnings; use 5.010; use Date::Format; use Getopt::Long qw/:config bundling/; use Term::ANSIColor; use Term::Size; my $VERSION = '0.4'; my ($cache, $config, $data, $extra); my $config_file = '/var/cache/icinga/objects.cache'; my $status_file = '/var/lib/icinga/status.dat'; my $rw_file = '/var/lib/icinga/rw/icinga.cmd'; my $context; my $colours = 1; my $list_type = 's'; my $verbosity = 1; my $recheck = 0; my $acknowledge = undef; my $term_width = Term::Size::chars(); my $cut_mode = 'b'; my (@for_hosts, @for_groups, @for_services, @list_hosts, @list_services); my @filters; sub have_host { my ($host) = @_; if ($list_type eq 's') { return exists $data->{services}->{$host}; } else { return exists $data->{hosts}->{$host}; } } sub have_service { my ($host, $service) = @_; foreach my $s (@{$data->{services}->{$host}}) { if ($s->{service_description} eq $service) { return 1; } } return 0; } sub have_service_multi { my ($host, @services) = @_; foreach my $s (@services) { if (have_service($host, $s)) { return 1; } } return 0; } sub with_colour { my ($text, $colour) = @_; if ($colours) { return colored($text, $colour); } else { return $text; } } sub pretty_date { my ($unix) = @_; if ($unix == 0) { return 'never'; } return time2str('%Y-%m-%d %H:%M:%S', $unix); } sub pretty_duration { my ($since) = @_; my $now = time(); my $dif = $now - $since; return sprintf( "%dd %dh %dm %ds", int($dif / (24 * 3600)), int(($dif / 3600) % 24), int(($dif / 60) % 60), $dif % 60, ); } sub pretty_noyes { my ($bool) = @_; return ($bool ? with_colour('YES', 'white on_red') : with_colour('NO', 'black on_green') ); } sub pretty_yesno { my ($bool) = @_; return ($bool ? with_colour('YES', 'black on_green') : with_colour('NO', 'white on_red') ); } sub split_by_words { my ($str, $padding, $max_w) = @_; my @words = split(/ /, $str); my @ret; while (grep { length($_) > $max_w } @words) { for my $i ( 0 .. $#words ) { my $word = $words[$i]; if (length($word) > $max_w) { splice(@words, $i, 1, substr($word, 0, $max_w), substr($word, $max_w)); last; } } } while (@words) { my $cur_str = q{}; my $tr_space = 0; while (@words and ((length($cur_str) + length($words[0]) + $tr_space) <= $max_w)) { if ($tr_space) { $cur_str .= ' '; } else { $tr_space = 1; } $cur_str .= shift(@words); } if (@ret) { $cur_str = (' ' x $padding) . $cur_str; } push(@ret, $cur_str); } return @ret; } sub break_str { my ($text, $waste) = @_; my $cut = $term_width - $waste; if ( (not defined $term_width) or ($term_width == 0) or ($cut < 12)) { return $text; } if ($cut_mode eq 'c') { return substr($text, 0, $cut); } elsif ($cut_mode eq 'b') { return join("\n", split_by_words($text, $waste, $cut)); } else { return $text; } } sub check_is_soft { my ($x) = @_; return ($x->{'last_hard_state'} != $x->{'current_state'}); } sub check_is_host_down { my ($s) = @_; return ( $data->{'hosts'}->{ $s->{'host_name'} }->{'current_state'} != 0 ); } sub filter_generic { my ($x) = @_; my $filters_unfulfilled = @filters; foreach my $f (@filters) { if ( ($f eq 'A' and $x->{'problem_has_been_acknowledged'}) or ($f eq '!A' and not $x->{'problem_has_been_acknowledged'}) or ($f eq 'D' and check_is_host_down($x)) or ($f eq '!D' and not check_is_host_down($x)) or ($f eq 'F' and $x->{'is_flapping'}) or ($f eq '!F' and not $x->{'is_flapping'}) or ($f eq 'P' and $x->{'passive_checks_enabled'} and not $x->{'active_checks_enabled'}) or ($f eq '!P' and $x->{'active_checks_enabled'}) or ($f eq 'S' and check_is_soft($x)) or ($f eq '!S' and not check_is_soft($x)) or ($f eq 'o' and $x->{'current_state'} == 0) or ($f eq '!o' and $x->{'current_state'} != 0) or ($f eq 'w' and $x->{'current_state'} == 1) or ($f eq '!w' and $x->{'current_state'} != 1) or ($f eq 'c' and $x->{'current_state'} == 2) or ($f eq '!c' and $x->{'current_state'} != 2) or ($f eq 'u' and $x->{'current_state'} == 3) or ($f eq '!u' and $x->{'current_state'} != 3) or ($f eq 'd' and $x->{'current_state'} == 1) or ($f eq '!d' and $x->{'current_state'} != 1) or ($f eq 'x' and $x->{'current_state'} == 2) or ($f eq '!x' and $x->{'current_state'} != 2) or 0 # Terminator to ease adding new lines ) { $filters_unfulfilled--; } } if ($filters_unfulfilled) { return 0; } return 1; } sub filter_host { my ($h) = @_; if (not filter_generic($h)) { return 0; } return 1; } sub filter_service { my ($s) = @_; if (not filter_generic($s)) { return 0; } if (@list_services and not ($s->{'service_description'} ~~ [@list_services])) { return 0; } return 1; } sub read_objects_line { my ($line, $ref) = @_; if ($line =~ / ^ (?:define \s )? (? \w+) \s+ { /x) { $context = $+{context}; } elsif ($line =~ / ^ \t (? [^=\t]+ ) [=\t] (? .*) $ /x) { $cache->{$+{key}} = $+{value}; } elsif ($line =~ / ^ \t } $ /x) { given ($context) { when (['info', 'programstatus']) { ${$ref}->{$context} = $cache; } when ('hoststatus') { ${$ref}->{hosts}->{$cache->{host_name}} = $cache; } when ('servicestatus') { push(@{${$ref}->{services}->{$cache->{host_name}}}, $cache); } when ('contactstatus') { push(@{${$ref}->{contacts}}, $cache); } when ('hostdowntime') { push(@{${$ref}->{hostdowntimes}}, $cache); } when ('servicedowntime') { push(@{${$ref}->{servicedowntimes}}, $cache); } when ('hostgroup') { ${$ref}->{hostgroups}->{$cache->{hostgroup_name}} = $cache; } when('servicegroup') { ${$ref}->{servicegroups}->{$cache->{servicegroup_name}} = $cache; } when ('hostcomment') { # TODO } when ('servicecomment') { # TODO } when ([qw[ timeperiod command contactgroup contact host service servicedependency ]]) { # skipped for now } default { warn("Unknown field in $status_file: $context\n"); } } $cache = undef; } } sub read_objects { my ($file, $ref) = @_; open(my $fh, '<', $file); while (my $line = <$fh>) { chomp($line); read_objects_line($line, $ref); } close($fh); } sub enhance_status { HOST: for my $h (keys %{$data->{services}}) { for my $s (@{$data->{services}->{$h}}) { if ($s->{current_state} != 0) { $extra->{$h}->{service_problem} = 1; next HOST; } } } } sub service_state { my ($checked, $digit) = @_; if (not $checked) { return 'PENDING '; } given ($digit) { when(0) { return with_colour(' OK ', 'black on_green' ) } when(1) { return with_colour(' WARNING', 'black on_yellow') } when(2) { return with_colour('CRITICAL', 'white on_red' ) } when(3) { return with_colour(' UNKNOWN', 'white on_blue' ) } default { die("Unknown service state: $digit\n") } } } sub host_state { my ($checked, $digit) = @_; if (not $checked) { return ' PENDING '; } given($digit) { when(0) { return with_colour(' OK ', 'black on_green') } when(1) { return with_colour(' DOWN ', 'white on_red' ) } when(2) { return with_colour('UNREACHABLE', 'white on_blue' ) } default { die("Unknown host state: $digit\n") } } } sub display_queue { my @queue = map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, $_->{next_check}] } (values %{$data->{hosts}}, map { @{$_} } values %{$data->{services}}); @queue = grep { $_->{host_name} ~~ \@list_hosts } @queue; if (@list_services) { @queue = grep { $_->{service_description} ~~ \@list_services } @queue; } printf( "%-25.25s %-20.20s %-19s %-19s\n", 'Host', 'Service', 'Last Check', 'Next Check', ); for my $e (@queue) { if ($e->{next_check} == 0) { next; } printf( "%-25.25s %-20.20s %-19s %-19s\n", $e->{host_name}, $e->{service_description} // q{}, pretty_date($e->{last_check}), pretty_date($e->{next_check}), ); } } sub display_downtime { my ($d) = @_; my $v = $verbosity; printf('%-27.27s', $d->{'host_name'}); if ($v >= 3) { printf( ' %s %-10.10s', pretty_date($d->{'entry_time'}), $d->{'author'}, ); } if ($v >= 2) { printf(' %-30.30s', $d->{'comment'}); } printf( ' %s %s', pretty_date($d->{'start_time'}), pretty_date($d->{'end_time'}), ); if ($v >= 2) { print ($d->{'fixed'} ? ' Fixed' : ' Flexi'); } print "\n"; } sub display_x_verbose { my ($x, $format) = @_; my $v = $verbosity; if ($v > 2) { printf( $format, 'Host', $x->{'host_name'}, ); if ($x->{'service_description'}) { printf( $format, 'Service', $x->{'service_description'}, ); printf( "%-16s : %s (for %s)%s\n", 'Status', service_state($x->{'has_been_checked'}, $x->{'current_state'}), pretty_duration($x->{'last_state_change'}), ($x->{'problem_has_been_acknowledged'} ? ' (Acknowledged)' : q{}), ); } else { printf( "%-16s : %s (for %s)%s\n", 'Status', host_state($x->{'has_been_checked'}, $x->{'current_state'}), pretty_duration($x->{'last_state_change'}), ($x->{'problem_has_been_acknowledged'} ? ' (Acknowledged)' : q{}), ); } printf( $format, 'Plugin Output', break_str($x->{'plugin_output'}, 19), ); for my $line (split(qr{\\n}, $x->{'long_plugin_output'})) { printf( $format, q{}, break_str($line, 19), ); } printf( $format, 'Performance Data', $x->{'performance_data'}, ); printf( "%-16s : %d/%d\n", 'Current Attempt', $x->{'current_attempt'}, $x->{'max_attempts'}, ); printf( $format, 'Last Check Time', pretty_date($x->{'last_check'}), ); printf( $format, 'Next Check', pretty_date($x->{'next_check'}), ); printf( "%-16s : %s (%.1f%% state change)\n", 'Flapping', pretty_noyes($x->{'is_flapping'}), $x->{'percent_state_change'}, ); } if ($v > 3) { printf( $format, 'Check Type', ($x->{'check_type'} ? 'PASSIVE' : 'ACTIVE'), ); printf( "%-16s : %5.3fs\n%-16s : %5.3fs\n", 'Check Latency', $x->{'check_latency'}, 'Check Duration', $x->{'check_execution_time'}, ); if ($x->{'service_description'}) { printf( "%-16s : o %s w %s c %s u %s\n", 'Last State Times', pretty_date($x->{'last_time_ok'}), pretty_date($x->{'last_time_warning'}), pretty_date($x->{'last_time_critical'}), pretty_date($x->{'last_time_unknown'}), ); } else { printf( "%-16s : o %s d %s u %s\n", 'Last State Times', pretty_date($x->{'last_time_up'}), pretty_date($x->{'last_time_down'}), pretty_date($x->{'last_time_unreachable'}), ); } printf( $format, 'In Downtime', 'FIXME' ); printf( $format, 'Active Checks', pretty_yesno($x->{'active_checks_enabled'}), ); printf( $format, 'Passive Checks', pretty_yesno($x->{'passive_checks_enabled'}), ); printf( $format, 'Obsessing', pretty_yesno( $x->{'service_description'} ? $x->{'obsess_over_service'} : $x->{'obsess_over_host'} ), ); printf( $format, 'Notifications', pretty_yesno($x->{'notifications_enabled'}), ); printf( $format, 'Event Handler', pretty_yesno($x->{'event_handler_enabled'}), ); printf( $format, 'Flap Detection', pretty_yesno($x->{'flap_detection_enabled'}), ); } } sub display_service { my ($s, $tab) = @_; my $v = $verbosity; my $flags = q{}; my $format = "%-16s : %s\n"; my $n_width; if ($v < 3) { $n_width = 20 + 8 + 2; if ($tab) { $n_width += 8; } printf("%-20.20s", $s->{service_description}); if ($v >= 2) { $n_width += 5; if ($s->{'problem_has_been_acknowledged'}) { $flags .= 'A'; } if ($s->{'is_flapping'}) { $flags .= 'F'; } if ($s->{'notifications_enabled'} == 0) { $flags .= 'N'; } if ($s->{'active_checks_enabled'} == 0 and $s->{'passive_checks_enabled'} == 1) { $flags .= 'P'; } if (not ($s->{'active_checks_enabled'} or $s->{'passive_checks_enabled'})) { $flags .= '!'; } $flags = sprintf(" %-3s", $flags); print with_colour($flags, 'bold'); } printf(" %s" , service_state($s->{'has_been_checked'}, $s->{'current_state'})); if ($v >= 2) { printf(' %d/%d', $s->{'current_attempt'}, $s->{'max_attempts'}); $n_width += 4; } print ' '; print break_str($s->{'plugin_output'}, $n_width); } else { display_x_verbose($s, $format); } print "\n"; } sub display_host_services { my ($host, $all) = @_; my @services; my $h = $data->{'hosts'}->{$host}; @services = grep { filter_service($_) } @{$data->{'services'}->{$host}}; if ($all and @services and $verbosity < 3) { print "\n$host"; if ($h->{'current_state'}) { print q{ }; } if ($h->{'current_state'} == 1) { print with_colour('DOWN', 'white on_red'); } elsif ($h->{'current_state'} == 2) { print with_colour('UNREACHABLE', 'white on_blue'); } print "\n"; } foreach my $service (@services) { if ($all and $verbosity < 3) { print "\t"; } elsif ($all) { print "\n"; } display_service($service, $all); } } sub display_host_single { my ($host) = @_; my $format = "%-16s : %s\n"; my $h = $data->{hosts}->{$host}; my $v = $verbosity; if ($v < 3) { printf("%-32.32s", $h->{'host_name'}); printf(" %s", host_state($h->{'has_been_checked'}, $h->{'current_state'})); if ($v >= 2) { printf(" %d/%d", $h->{'current_attempt'}, $h->{'max_attempts'}); } printf(" %s", $h->{'plugin_output'}); } else { display_x_verbose($h, $format); } print "\n"; } sub display_host { my ($host, $all) = @_; if ($list_type eq 'h') { display_host_single($host); } else { display_host_services($host, $all); } } sub dispatch_command { my $str = join(';', @_); open(my $cmd_fh, '>', $rw_file); printf $cmd_fh ( "[%d] %s", time(), $str, ); close($cmd_fh); } sub recheck_host_all { my ($host) = @_; dispatch_command('SCHEDULE_HOST_SVC_CHECKS', $host, time()); say "Scheduled check of * on '$host'"; } sub recheck_service { my ($host, $service) = @_; dispatch_command('SCHEDULE_SVC_CHECK', $host, $service, time()); say "Scheduled check of '$service' on '$host'"; } sub acknowledge_service { my ($host, $service) = @_; dispatch_command('ACKNOWLEDGE_SVC_PROBLEM', $host, $service, 2, 1, 1, 'cli', $acknowledge); say "Acknowledged $host/$service: $acknowledge"; } sub action_on_host { my ($h) = @_; if ($recheck) { recheck_host_all($h); } } sub action_on_service { my ($h, $s) = @_; if (not have_service($h, $s)) { return; } if ($recheck) { recheck_service($h, $s); } if ($acknowledge) { acknowledge_service($h, $s); } } GetOptions( 'a|acknowledge=s' => sub { $acknowledge = $_[1]; $list_type = q{} }, 'c|config=s' => \$config_file, 'C|no-colours' => sub { $colours = 0 }, 'f|status-file=s' => \$status_file, 'F|rw-file=s' => \$rw_file, 'g|hostgroup=s' => sub { push(@for_groups, split(/,/, $_[1])) }, 'h|host=s' => sub { push(@for_hosts, split(/,/, $_[1])) }, 'l|list=s' => sub { $list_type = substr($_[1], 0, 1) }, 'r|recheck' => sub { $recheck = 1; $list_type = q{} }, 's|service=s' => sub { push(@for_services, split(/,/, $_[1])) }, 'v|verbose+' => \$verbosity, 'V|version' => sub { say "icli version $VERSION"; exit 0 }, 'x|cut-mode=s' => sub { $cut_mode = substr($_[1], 0, 1) }, 'z|filter=s' => sub { push(@filters, split(/,/, $_[1])) }, ) or die("Please see perldoc -F $0 for help\n"); read_objects($status_file, \$data); read_objects($config_file, \$config); enhance_status(); for my $arg (@ARGV) { my ($host, $service) = split(qr{/}, $arg); if (not grep { $host } @for_hosts) { push(@for_hosts, $host); } if ($service) { push(@for_services, $service); } } foreach my $host (@for_hosts) { if (not exists $data->{services}->{$host}) { die("Unknown host: ${host}\n"); } } @list_hosts = @for_hosts; @list_services = @for_services; foreach my $group (@for_groups) { if (not exists $config->{'hostgroups'}->{$group}) { die("Unknown hostgroup: ${group}\n"); } foreach my $host (split/,/, $config->{'hostgroups'}->{$group}->{'members'}) { if (not grep { $_ eq $host } @list_hosts) { push(@list_hosts, $host); } } } if (@list_hosts == 0) { @list_hosts = sort keys %{$data->{services}}; } if (@list_services) { @list_hosts = grep { have_service_multi($_, @list_services) } @list_hosts; } if ($list_type eq 'h') { @list_hosts = grep { filter_host($data->{'hosts'}->{$_}) } @list_hosts; } if ($list_type ~~ [qw[s h]]) { foreach my $host (@list_hosts) { display_host($host, (@list_hosts > 1)); } } elsif ($list_type eq 'q') { display_queue(); } elsif ($list_type eq 'd') { foreach my $downtime (@{$data->{hostdowntimes}}) { display_downtime($downtime); } } elsif ($recheck or $acknowledge) { foreach my $host (@list_hosts) { if (not @list_services and not @filters) { action_on_host($host); } elsif (not @list_services and @filters) { foreach my $service ( grep { filter_service($_) } @{$data->{'services'}->{$host}} ) { action_on_service($host, $service->{'service_description'}); } } else { foreach my $service (@list_services) { action_on_service($host, $service); } } } } else { die("See perldoc -F $0\n"); } __END__ =head1 NAME B - Icinga Command Line Interface =head1 SYNOPSIS B [B<-v>|B<-vv>] [B<-z> I] [B<-h> I] [B<-g> I] [B<-s> I] [B<-c> I] [B<-C>] [B<-f> I] [B<-F> I] [B<-r>|B<-lh>|B<-ls>|B<-lq>|B<-ld>] [I/I I<...>] =head1 DESCRIPTION B is a command line interface to B. By default it lists all services and their states. Note that when supplying custom config and status file paths, B also works with B. 100% compatibility is not guranteed, however. B only works when executed on the host running the B daemon. To use it on another host, shell aliases (like C<< alias icli='ssh $icingahost icli' >>) or similar are recommended. You can narrow down the list of services you want displayed either using B (like C<< icli -z!o >>), the B<-h>/B<-s> arguments (C<< icli -h aneurysm -s Libraries,Websites >>) or commandline args (C<< icli aneurysm/{Libraries,Websites} >> with shell expansion). =head1 OPTIONS =over =item B<-c>|B<--config> I Read config from I instead of the default F =item B<-C>|B<--no-colours> Disable colours in output =item B<-f>|B<--status-file> I Read the status from I instead of the default F =item B<-F>|B<--rw-file> I Use I as external commands file. Default: F =item B<-g>|B<--hostgroup> I Limit selection to hosts in I (comma separated list) =item B<-h>|B<--host> I Limit selection to I (comma separated list) =item B<-l>|B<--list> B|B|B|B List either services (the default) or hosts. Note that only the first character of the argument is checked, so C<< icli -lh >>, C<< icli -ls >> etc. are also fine. =item B<-r>|B<--recheck> Schedule an immediate recheck of all selected services =item B<-s>|B<--service> I Limit selection to I (comma separated lists). Can be combined with B<-h>/B<-g> to further narrow down the selection, but may also be used stand-alone. =item B<-v>|B<--verbose> Increase output verbosity. Can be combined up to B<-vvv> =item B<-V>|B<--version> Show version information =item B<-x>|B<--cut-mode> I What to do with lines which are too long for the terminal: Bothing, But off, line Break (with proper indentation). The default is line Breaks =item B<-z>|B<--filter> I Limit selection to hosts/services passing the filter. I is a comma separated list of filters, only hosts/services to which all filters apply are selected. See also L =back =head1 OUTPUT =head2 SERVICE LISTING This is the standard output method. It contains the following: =over =item * Service description =item * -v: Service Flags (Bcknowledged, Blapping, B

assive, Bno checks) =item * Service state (ok / warning / critical / unknown) =item * -v: Current attempt / Max attempts =item * Plugin output =back =head2 HOST LISTING Enabled with -ld =over =item * Host name =item * Host state (ok / down / unreachable) =item * -v: Current attempt / Max attempts =item * Plugin output =back =head2 QUEUE LISTING Enabled with -lq =over =item * Host name =item * Service name =item * Last check =item * Next check =back =head1 FILTER EXPRESSIONS Each expression can be negated with an exclamation mark, e.g. "!A" for all non-acknowledged services. =over =item B Check state has been acknowledged =item B The host this service belongs to is Down or Unreachable =item B Service is flapping between states =item B Notifications for this service are disabled =item B

Only passive checks are enabled. Note that B simply means that active checks are enabled, no matter the status of passive checks =item B Check state is soft. For instance, it used to be OK and is now critical, but has not reached its maximum number and caused a notification yet. Good to find (or ignore) service problems which might just be temporary, non-critical glitches. =item B Host/Service state is OK =item B Service state is Warning =item B Service state is Critical =item B Service state is Unknown =item B Host state is Down =item B Host state is Unreachable =back =head1 EXIT STATUS Zero, unless errors occured. =head1 CONFIGURATION None. =head1 DEPENDENCIES =over =item * autodie (included with perl >= 5.10.1) =item * Date::Format =item * Term::Size =back =head1 BUGS AND LIMITATIONS This software is in early development stages. So there will probably be quite a lot. =head2 REPORTING BUGS Either via mail to Ederf@finalrewind.orgE or on Ehttp://github.com/derf/icinga-cli/issuesE. =head1 EXAMPLES =over =item C<< icli -r -s 'APT Updates' >> Schedule a check of the "APT Updates" service on all hosts having it =item C<< icli -lq -h aneurysm -g chaosdorf-hosts >> List check queue for all hosts in the hostgroup "chaosdorf-hosts", plus the host aneurysm =item C<< icli -z!o,!A,!S,!D >> Show all service problems which are already hard states and have not yet been acknowledged. Also weed out problem services on hosts which are down anyways =back =head1 AUTHOR Copyright (C) 2010 by Daniel Friesel Ederf@finalrewind.orgE =head1 LICENSE 0. You just DO WHAT THE FUCK YOU WANT TO.