#!/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; my $VERSION = '0.2'; 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 $short = 0; my $list_type = 's'; my $verbosity = 1; my $recheck = 0; my (@for_hosts, @for_groups, @for_services, @list_hosts, @list_services); 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) = @_; return time2str('%Y-%m-%d %H:%M:%S', $unix); } 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}->{downtimes}}, $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_service { my ($s) = @_; my $v = $verbosity; printf("%-20.20s", $s->{service_description}); printf(" %s" , service_state($s->{'has_been_checked'}, $s->{'current_state'})); if ($v >= 2) { printf(' %d/%d', $s->{'current_attempt'}, $s->{'max_attempts'}); } printf(" %s\n", $s->{'plugin_output'}); } sub display_host_services { my ($host, $all) = @_; if ($all and (not $short or $extra->{$host}->{service_problem})) { say "\n$host"; } foreach my $service (@{$data->{services}->{$host}}) { if ( ($short and not $service->{current_state}) or (@for_services and not ($service->{service_description} ~~ [@for_services]) )) { next; } if ($all) { print "\t"; } display_service($service); } } sub display_host_single { my ($host) = @_; my $h = $data->{hosts}->{$host}; my $v = $verbosity; if ($short and not $h->{current_state}) { return; } 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\n", $h->{'plugin_output'}); } 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) = @_; if (have_service($host, $service)) { dispatch_command('SCHEDULE_SVC_CHECK', $host, $service, time()); say "Scheduled check of '$service' on '$host'"; } } GetOptions( '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|critical-only' => \$short, ); read_objects($status_file, \$data); read_objects($config_file, \$config); enhance_status(); 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 ~~ [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->{downtimes}}) { display_downtime($downtime); } } elsif ($recheck) { foreach my $host (@list_hosts) { if (not @list_services) { recheck_host_all($host); } else { foreach my $service (@for_services) { recheck_service($host, $service); } } } } else { die("See perldoc -F $0\n"); } __END__ =head1 NAME B [B<-x>] [B<-h> I] [B<-g> I] [B<-s> I] [B<-c> I] [B<-C>] [B<-f> I] [B<-r>|B<-lh>|B<-ls>|B<-lq>|B<-ld>] B - Icinga Command Line Interface =head1 SYNOPSIS B [B<-h> I] =head1 DESCRIPTION B is a command line interface to Icinga. By default it lists all services and their states. =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 =item B<-h>|B<--host> I1,I,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<-x>|B<--short> Only show services which are not OK =item B<-V>|B<--version> Show version information =back The argument to B<-g> and B<-h> may also be comma-separated list of hosts/hostgroups, e.g. C<< icli -h aneurysm,kraftwerk >>. =head1 EXIT STATUS Zero, unless errors occured. =head1 CONFIGURATION None. =head1 DEPENDENCIES None, so far. =head1 BUGS AND LIMITATIONS This software is in early development stages. So there will probably be quite a lot. =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 =back =head1 AUTHOR Copyright (C) 2010 by Daniel Friesel Ederf@finalrewind.orgE =head1 LICENSE 0. You just DO WHAT THE FUCK YOU WANT TO.