#!/usr/bin/env perl ## Copyright © 2010-2012 by Daniel Friesel ## License: WTFPL ## 0. You just DO WHAT THE FUCK YOU WANT TO. use strict; use warnings; use 5.010; no if $] >= 5.018, warnings => 'experimental::smartmatch'; use App::Icli::ConfigData; use Carp qw(croak); use DateTime; use DateTime::Format::Strptime; use DateTime::TimeZone; use Getopt::Long qw/:config bundling/; use JSON; use List::MoreUtils qw(any firstval); use LWP::UserAgent; use Net::Netrc; use POSIX qw(strftime); use Term::ANSIColor; use Term::Size; our $VERSION = '0.48'; my ( $cache, $config, $data, $extra ); my $config_file = App::Icli::ConfigData->config('object_file'); my $status_file = App::Icli::ConfigData->config('status_file'); my $rw_file = App::Icli::ConfigData->config('command_file'); my ( $api1_root, $ua ); my $realm = 'Icinga Access'; my $context; my $colours = 1; my $list_type = 's'; my $verbosity = 1; my $overview = 0; my $match_output = undef; my $action = undef; my $as_contact = undef; my $term_width = Term::Size::chars(); my $cut_mode = 'b'; my ( @for_hosts, @for_groups, @for_services, @list_hosts, @list_services ); my @action_args; 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 strftime( '%Y-%m-%d %H:%M:%S', localtime($unix) ); } sub pretty_duration { my ($since) = @_; my $now = time(); my $dif = $now - $since; return pretty_duration_abs($dif); } sub pretty_duration_abs { my ($dif) = @_; return sprintf( '%dd %dh %dm %ds', int( $dif / ( 24 * 3600 ) ), int( ( $dif / 3600 ) % 24 ), int( ( $dif / 60 ) % 60 ), $dif % 60, ); } sub parse_duration { my ($raw) = @_; my %factors = ( s => 1, m => 60, h => 3_600, d => 86_400, w => 604_800, ); $raw =~ m{ ^ (? [.0-9]+ ) \s* (? [mhdw] )? $ }x or die( "Cannot parse '$raw' - must be a duration like '50s', '2m', '10h' or '1d'" ); if ( $+{unit} ) { return $+{value} * $factors{ $+{unit} }; } return $+{value}; } 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 pretty_state { my ( $count, $state ) = @_; my $colour; given ($state) { when ('ok') { $colour = 'black on_green' } when ('warning') { $colour = 'black on_yellow' } when ('critical') { $colour = 'white on_red' } when ('unknown') { $colour = 'white on_blue' } } if ( $count == 0 ) { return q{ }; } if ($colour) { return with_colour( sprintf( '%4d', $count ), $colour ); } return sprintf( '%4d', $count ); } sub setup_ua { my ($url) = @_; my $m; $url =~ m{ ^ (?: (? [^:]+ ) :// )? (?: (? [^@]+ ) @ )? (? [^:/@]+ ) (?: : (? \d++ ) )? (? .* ) $ }x or die( "Cannot parse API url '$url'\n" . "This may be a bug. If you think so, please report it\n" ); my $proto = $+{proto} // 'http'; my $host = $+{host}; my $port = $+{port} // ( $proto eq 'http' ? 80 : 443 ); if ( $+{user} ) { $as_contact = $+{user}; } my $netrc_name = "$host:$port"; $ua = LWP::UserAgent->new( timeout => 5 ); $m = Net::Netrc->lookup( $netrc_name, $as_contact ) or warn( "Cannot find an entry for '$netrc_name' " . ( $as_contact ? "with login '$as_contact' " : q{} ) . "in ~/.netrc\n" ); if ($m) { $ua->credentials( $netrc_name, $realm, $m->login, $m->password ); } $ua->env_proxy; } sub split_by_words { my ( $str, $padding, $max_w ) = @_; my @words = split( / /, $str ); my @ret; while ( any { 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 .= q{ }; } else { $tr_space = 1; } $cur_str .= shift(@words); } if (@ret) { $cur_str = ( q{ } 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; if ( $match_output and not $x->{plugin_output} =~ $match_output ) { return 0; } 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 'N' and not $x->{'notifications_enabled'} ) or ( $f eq '!N' and $x->{'notifications_enabled'} ) 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 'p' and $x->{'has_been_checked'} == 0 ) or ( $f eq '!p' and $x->{'has_been_checked'} != 0 ) or ( $f eq 'o' and $x->{'current_state'} == 0 ) or ( $f eq '!o' and ( $x->{'current_state'} != 0 or $x->{'has_been_checked'} == 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; } if ( $as_contact and not host_has_contact( $h, $as_contact ) and not $api1_root ) { 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; } if ( $as_contact and not service_has_contact( $s, $as_contact ) and not $api1_root ) { return 0; } return 1; } sub host_has_contact { my ( $h, $contact ) = @_; my $conf_h = $config->{hosts}{ $h->{host_name} }; return any { $_ eq $contact } @{ $conf_h->{contacts} }; } sub service_has_contact { my ( $s, $contact ) = @_; my $conf_s = firstval { $_->{service_description} eq $s->{service_description} } @{ $config->{services}{ $s->{host_name} } }; return any { $_ eq $contact } @{ $conf_s->{contacts} }; } sub read_json { my ( $res, $ref ) = @_; my %statusmap = ( OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3, UP => 0, DOWN => 1, UNREACHABLE => 2, PENDING => 0, ); my $json = from_json( $res->decoded_content ); if ( $json->{config}->{error} ) { warn( 'While reading ' . $res->request->uri . ":\n" ); warn( 'JSON API Error: ' . $json->{config}->{error}->{title} . "\n" ); warn( ' ' . $json->{config}->{error}->{text} . "\n" ); } if ( $json->{config}->{hosts} ) { for my $host ( @{ $json->{config}->{hosts} } ) { ${$ref}->{hosts}->{ $host->{host_name} } = $host; } } if ( $json->{config}->{hostgroups} ) { for my $group ( @{ $json->{config}->{hostgroups} } ) { ${$ref}->{hostgroups}->{ $group->{group_name} } = $group; } } if ( $json->{config}->{services} ) { for my $service ( @{ $json->{config}->{services} } ) { push( @{ ${$ref}->{services}->{ $service->{host_name} } }, $service ); } } if ( $json->{config}->{servicegroups} ) { for my $group ( @{ $json->{config}->{servicegroups} } ) { ${$ref}->{servicegroups}->{ $group->{group_name} } = $group; } } if ( $json->{status}->{host_status} ) { for my $host ( @{ $json->{status}->{host_status} } ) { $host->{has_been_checked} = ( $host->{status} eq 'PENDING' ? 0 : 1 ); $host->{current_state} = $statusmap{ $host->{status} }; $host->{plugin_output} = $host->{status_information}; $host->{long_plugin_output} = q{}; $host->{performance_data} = q{}; ( $host->{current_attempt}, $host->{max_attempts} ) = split( '/', $host->{attempts} ); ${$ref}->{hosts}->{ $host->{host_name} } = $host; } } if ( $json->{status}->{service_status} ) { for my $service ( @{ $json->{status}->{service_status} } ) { $service->{has_been_checked} = ( $service->{status} eq 'PENDING' ? 0 : 1 ); $service->{current_state} = $statusmap{ $service->{status} }; $service->{plugin_output} = $service->{status_information}; $service->{long_plugin_output} = q{}; $service->{performance_data} = q{}; ( $service->{current_attempt}, $service->{max_attempts} ) = split( '/', $service->{attempts} ); push( @{ ${$ref}->{services}->{ $service->{host_name} } }, $service ); } } } sub read_objects_line { my ( $line, $ref ) = @_; if ( $line =~ / ^ (?:define \s )? (? \w+) \s+ { /x ) { $context = $+{context}; } elsif ( $line =~ / ^ \t (? [^=\t]+ ) [=\t] \s* (? .*) $ /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') { push( @{ ${$ref}->{hostcomments} }, $cache ); } when ('servicecomment') { push( @{ ${$ref}->{servicecomments} }, $cache ); } when ('host') { ${$ref}->{hosts}->{ $cache->{host_name} } = $cache; } when ('service') { push( @{ ${$ref}->{services}->{ $cache->{host_name} } }, $cache ); } when ('contactgroup') { ${$ref}->{contactgroups}->{ $cache->{contactgroup_name} } = [ split( m{, *}, $cache->{members} // q{} ) ]; } when ( [ qw[ timeperiod command contactgroup contact host service servicedependency serviceescalation module hostdependency ] ] ) { # skipped for now } default { warn("Unknown field in $status_file: $context\n"); } } $cache = undef; } } sub read_objects { my ( $file, $ref, $description, $opt ) = @_; open( my $fh, '<', $file ) or die( "Failed to read $description ($file): $!\n" . "Set $opt to change it\n" ); while ( my $line = <$fh> ) { chomp($line); read_objects_line( $line, $ref ); } close($fh) or warn("Failed to close $description ($file): $!\n"); } sub enhance_status { for my $c ( @{ $data->{servicecomments} } ) { my $service = firstval { $_->{service_description} eq $c->{service_description} } @{ $data->{services}->{ $c->{host_name} } }; push( @{ $service->{comments} }, $c ); } for my $c ( @{ $data->{hostcomments} } ) { push( @{ $data->{hosts}->{ $c->{host_name} }->{comments} }, $c ); } 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; } } } for my $h ( values %{ $config->{hosts} } ) { if ( $h->{contacts} ) { $h->{contacts} =~ s{^ *}{}o; $h->{contacts} = [ split( m{, *}, $h->{contacts} ) ]; } } HOST: for my $h ( keys %{ $config->{services} } ) { for my $s ( @{ $config->{services}->{$h} } ) { if ( $s->{contacts} ) { $s->{contacts} =~ s{^ *}{}o; $s->{contacts} = [ split( m{, *}, $s->{contacts} ) ]; } if ( $s->{contact_groups} ) { for my $group ( split( m{, *}, $s->{contact_groups} ) ) { push( @{ $s->{contacts} }, @{ $config->{contactgroups}{$group} } ); } } } } } sub parse_action { if ( not $action ) { return; } my @raw_args; my %actionmap = ( a => 'acknowledge', d => 'downtime', r => 'recheck', R => 'force_recheck', ); ( $action, @raw_args ) = split( /:/, $action ); @action_args = split( /,/, join( ':', @raw_args ) ); $list_type = q{}; if ( exists $actionmap{$action} ) { $action = $actionmap{$action}; } elsif ( length($action) <= 2 ) { say STDERR "Note: Action shortcut '${action}' is unknown"; } } sub compute_hostlist { for my $arg (@ARGV) { my ( $host, $service ) = split( qr{/}, $arg ); if ( not any { $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 any { $_ eq $host } @list_hosts ) { push( @list_hosts, $host ); } } } if ( @list_hosts == 0 ) { @list_hosts = sort keys %{ $data->{hosts} }; } 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; } } sub service_state { my ($s) = @_; my $checked = $s->{has_been_checked}; my $digit = $s->{current_state}; 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 { croak("Unknown service state: $digit\n") } } } sub host_state { my ($h) = @_; my $checked = $h->{has_been_checked}; my $digit = $h->{current_state}; 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 { croak("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 ($as_contact) { @queue = grep { exists $_->{service_description} ? service_has_contact( $_, $as_contact ) : host_has_contact( $_, $as_contact ) } @queue; } if (@list_services) { @queue = grep { $_->{service_description} ~~ \@list_services } @queue; if ($as_contact) { @queue = grep { host_has_contact( $_, $as_contact ) } @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; my $format = "%-10s : %s\n"; if ( $v > 2 ) { printf( $format, 'Host', $d->{host_name} ); if ( $d->{service_description} ) { printf( $format, 'Service', $d->{service_description} ); } printf( $format, 'Start', $d->{is_in_effect} ? with_colour( pretty_date( $d->{start_time} ), 'bold' ) : pretty_date( $d->{start_time} ) ); printf( $format, 'End', $d->{is_in_effect} ? with_colour( pretty_date( $d->{end_time} ), 'bold' ) : pretty_date( $d->{end_time} ) ); printf( $format, 'Duration', $d->{fixed} ? 'Fixed' : pretty_duration_abs( $d->{duration} ) ); if ( $v > 3 ) { printf( $format, 'ID', $d->{downtime_id} ); if ( $d->{trigger_time} ) { printf( $format, 'Trigger', $d->{triggered_by} . ' (active since ' . pretty_date( $d->{trigger_time} ) . ')' ); } else { printf( $format, 'Trigger', $d->{triggered_by} || 'None' ); } } printf( $format, 'Author', $d->{author} ); printf( $format, 'Comment', break_str( $d->{comment}, 19 ) ); } else { if ( $d->{service_description} ) { printf( '%-25.25s %-25.25s', $d->{'host_name'}, $d->{service_description} ); } else { printf( '%-25.25s', $d->{'host_name'} ); } if ( $v >= 3 ) { printf( ' %s %-10.10s', pretty_date( $d->{'entry_time'} ), $d->{'author'}, ); } if ( $d->{is_in_effect} ) { printf( ' %-28s %-28s', with_colour( pretty_date( $d->{'start_time'} ), 'bold' ), with_colour( pretty_date( $d->{'end_time'} ), 'bold' ), ); } else { printf( ' %-20.20s %-20.20s', pretty_date( $d->{'start_time'} ), pretty_date( $d->{'end_time'} ), ); } if ( $v >= 2 ) { printf( '%-17.17s', $d->{'fixed'} ? ' Fixed' : q{ } . pretty_duration_abs( $d->{duration} ) ); } if ( $v >= 2 ) { print( $d->{comment} ); } } 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), 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), 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'} ), ); for my $c ( @{ $x->{comments} // [] } ) { printf( $format, 'Comment', break_str( $c->{comment_data}, 19 ) ); } } } 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) ); if ( $v >= 2 ) { printf( ' %d/%d', $s->{'current_attempt'}, $s->{'max_attempts'} ); $n_width += 4; } print q{ }; 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 %s', $h->{host_name}, host_state($h) ); 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 display_host_overview { my ($host) = @_; my ( $ok, $warn, $crit, $unk, $pend ) = (0) x 5; my $h = $data->{hosts}->{$host}; my @services = grep { filter_service($_) } @{ $data->{services}->{$host} }; for my $s (@services) { if ( $s->{has_been_checked} == 0 ) { $pend++; } else { given ( $s->{current_state} ) { when (0) { $ok++ } when (1) { $warn++ } when (2) { $crit++ } when (3) { $unk++ } } } } printf( '%-32.32s %s', $h->{host_name}, host_state($h) ); printf( ' %s %s %s %s %s', pretty_state( $ok, 'ok' ), pretty_state( $warn, 'warning' ), pretty_state( $crit, 'critical' ), pretty_state( $unk, 'unknown' ), pretty_state( $pend, 'pending' ), ); print "\n"; } sub display_overview { my ( $h_ok, $h_d, $h_u, $h_p, $s_ok, $s_w, $s_c, $s_u, $s_p ) = (0) x 9; for my $h (@list_hosts) { if ( $data->{hosts}{$h}{has_been_checked} == 0 ) { $h_p++; } else { given ( $data->{hosts}{$h}{current_state} ) { when (0) { $h_ok++ } when (1) { $h_d++ } when (2) { $h_u++ } } } for my $s ( grep { filter_service($_) } @{ $data->{services}{$h} } ) { if ( $s->{has_been_checked} == 0 ) { $s_p++; } else { given ( $s->{current_state} ) { when (0) { $s_ok++ } when (1) { $s_w++ } when (2) { $s_c++ } when (3) { $s_u++ } } } } } printf( "%-16.16s %4s\n", 'total hosts', $h_ok + $h_d + $h_u ); printf( "%-16.16s %s\n", 'up', pretty_state( $h_ok, 'ok' ) ); printf( "%-16.16s %s\n", 'down', pretty_state( $h_d, 'critical' ) ); printf( "%-16.16s %s\n", 'unreachable', pretty_state( $h_u, 'unknown' ) ); printf( "%-16.16s %s\n", 'pending', pretty_state( $h_p, 'pending' ) ); print "\n"; printf( "%-16.16s %4s\n", 'total services', $s_ok + $s_w + $s_c + $s_u ); printf( "%-16.16s %s\n", 'ok', pretty_state( $s_ok, 'ok' ) ); printf( "%-16.16s %s\n", 'warning', pretty_state( $s_w, 'warning' ) ); printf( "%-16.16s %s\n", 'critical', pretty_state( $s_c, 'critical' ) ); printf( "%-16.16s %s\n", 'unknown', pretty_state( $s_u, 'unknown' ) ); printf( "%-16.16s %s\n", 'pending', pretty_state( $s_p, 'pending' ) ); } sub dispatch_command { my $str = join( ';', @_ ) . "\n"; open( my $cmd_fh, '>', $rw_file ) or die( "Failed to open icinga command file ($rw_file): $!\n" . "Set --rw-file to change it\n" ); printf $cmd_fh ( '[%d] %s', time(), $str, ); close($cmd_fh) or warn("Failed to close $rw_file: $!\n"); } sub action_on_host { my ($host) = @_; my $tz = DateTime::TimeZone->new( name => 'local' ); given ($action) { when ('downtime') { my ( $start, $end, $duration, $comment, @opts ) = @action_args; my $strp = DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%S', time_zone => $tz->name, ); my $dt_start = $strp->parse_datetime($start); my $dt_end = $strp->parse_datetime($end); my $fixed = $duration ? 0 : 1; my $command = 'SCHEDULE_HOST_DOWNTIME'; my $addendum = q{}; $duration = parse_duration($duration); if ( 'children' ~~ \@opts ) { $command = 'SCHEDULE_AND_PROPAGATE_HOST_DOWNTIME'; $addendum = ' and its children'; } if ( 'trigger_children' ~~ \@opts ) { $command = 'SCHEDULE_AND_PROPAGATE_TRIGGERED_HOST_DOWNTIME'; $addendum = ' and its children (triggered)'; } dispatch_command( $command, $host, $dt_start->epoch, $dt_end->epoch, $fixed, 0, $duration, 'cli', $comment ); say "Scheduled host downtime for '$host'$addendum"; } when ('recheck') { dispatch_command( 'SCHEDULE_HOST_SVC_CHECKS', $host, time() ); say "Scheduled check of * on '$host'"; } when ('force_recheck') { dispatch_command( 'SCHEDULE_FORCED_HOST_SVC_CHECKS', $host, time() ); say "Scheduled forced check of * on '$host'"; } default { say STDERR "Cannot run action '${action}' on a host" } } } sub action_on_service { my ( $host, $service ) = @_; if ( not have_service( $host, $service ) ) { return; } my $tz = DateTime::TimeZone->new( name => 'local' ); given ($action) { when ('downtime') { my ( $start, $end, $duration, $comment, @opts ) = @action_args; my $strp = DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%S', time_zone => $tz->name, ); my $dt_start = $strp->parse_datetime($start); my $dt_end = $strp->parse_datetime($end); my $fixed = $duration ? 0 : 1; $duration = parse_duration($duration); dispatch_command( 'SCHEDULE_SVC_DOWNTIME', $host, $service, $dt_start->epoch, $dt_end->epoch, $fixed, 0, $duration, 'cli', $comment ); say "Scheduled service downtime for '$service' on '$host'"; } when ('recheck') { dispatch_command( 'SCHEDULE_SVC_CHECK', $host, $service, time() ); say "Scheduled check of '$service' on '$host'"; } when ('force_recheck') { dispatch_command( 'SCHEDULE_FORCED_SVC_CHECK', $host, $service, time() ); say "Scheduled forced check of '$service' on '$host'"; } when ('acknowledge') { dispatch_command( 'ACKNOWLEDGE_SVC_PROBLEM', $host, $service, 2, 1, 1, 'cli', $action_args[0] ); say "Acknowledged $host/$service: $action_args[0]"; } default { say STDERR "Cannot run action '${action}' on a service" } } } GetOptions( 'api1=s' => \$api1_root, 'a|action=s' => \$action, '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 ) }, 'm|match=s' => sub { $match_output = qr{$_[1]}i }, 'o|overview' => \$overview, 'realm=s' => \$realm, 's|service=s' => sub { push( @for_services, split( /,/, $_[1] ) ) }, 'U|as-contact=s' => \$as_contact, '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"); if ($api1_root) { setup_ua($api1_root); my $config_url = "$api1_root/config.cgi?jsonoutput&type=all"; my $hdata_url = "$api1_root/status.cgi?jsonoutput&style=hostdetail"; my $sdata_url = "$api1_root/status.cgi?jsonoutput"; my $config_res = $ua->get($config_url); my $hdata_res = $ua->get($hdata_url); my $sdata_res = $ua->get($sdata_url); for my $request ( [ $config_url, $config_res ], [ $hdata_url, $hdata_res ], [ $sdata_url, $sdata_res ] ) { my ( $url, $res ) = @{$request}; if ( $res->is_error ) { die( "Error while requesting $url\nError description:\n\n" . $res->as_string ); } } read_json( $config_res, \$config ); read_json( $hdata_res, \$data ); read_json( $sdata_res, \$data ); } else { read_objects( $status_file, \$data, 'icinga status_file', '--status-file' ); read_objects( $config_file, \$config, 'icinga object_cache_file', '--config' ); } enhance_status(); parse_action(); compute_hostlist(); if ($overview) { if ( $list_type eq 'h' ) { for my $host (@list_hosts) { display_host_overview($host); } } else { display_overview(); } } elsif ( $list_type ~~ [qw[s h]] ) { for my $host (@list_hosts) { display_host( $host, ( @list_hosts > 1 ) ); } } elsif ( $list_type eq 'q' ) { display_queue(); } elsif ( $list_type eq 'd' ) { if ( exists $data->{hostdowntimes} ) { say 'Host downtimes:'; if ( $verbosity == 1 ) { printf( "%-25.25s %-20.20s %-20.20s\n", 'Host', 'start', 'stop' ); } elsif ( $verbosity == 2 ) { printf( "%-25.25s %-20.20s %-20.20s %-17.17s %s\n", 'Host', 'start', 'stop', 'duration', 'comment' ); } foreach my $downtime ( @{ $data->{hostdowntimes} } ) { display_downtime($downtime); } } else { say 'No host downtimes'; } if ( exists $data->{servicedowntimes} ) { say "\nService downtimes:"; if ( $verbosity == 1 ) { printf( "%-25.25s %-25.25s %-20.20s %-20.20s\n", 'Host', 'Service', 'start', 'stop' ); } elsif ( $verbosity == 2 ) { printf( "%-25.25s %-25.25s %-20.20s %-20.20s %-17.17s %s\n", 'Host', 'Service', 'start', 'stop', 'duration', 'comment' ); } foreach my $downtime ( @{ $data->{servicedowntimes} } ) { display_downtime($downtime); } } else { say "\nNo service downtimes"; } } elsif ($action) { 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<-vvv>] [B<-z> I] [B<-h> I] [B<-g> I] [B<-s> I] [B<-c> I] [B<-C>] [B<-f> I] [B<-F> I] [B<-lh>|B<-ls>|B<-lq>|B<-ld>] [B<-a> I[B<:>I]] [I/I I<...>] =head1 VERSION version 0.48 =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 guaranteed, 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<-a>|B<--action> I[:I] Run I on all matching hosts or services. I is a comma-separated list of action arguments and depends on the action in question. I may also be a one or two letter shortcut. The following actions are supported: =over =item a|acknowledge I Acknowledge service problems with string I. This creates a sticky acknwoledgment with notification and no expire time. The comment will not be persistent. Note: Acknowledgement of host problems is not yet supported. =item d|downtime I,I,I,I[,I] Schedule a non-triggered host or service (depending on the filter arguments) downtime. I and I are timestamps and must be formatted as YYYY-MM-DDTHH:MM:SS, where the "T" is literal. The timestamp is assumed to be in the same time zone as the system running icli. If I is 0 (zero), a fixed downtime between I and I is scheduled. Otherwise, a flexible downtime which will start between I and I and last I is scheduled. In this case, I must be a real number appended with an optional unit (s for seconds, m for minutes, h for hours, d for days, w for weeks). If no unit is specified, seconds are used. If a host is selected and I contains "children", a downtime for all of its children will be scheduled with the same parameters as the host's. Likewise, if I contains "trigger_children", a triggered downtime for all of the host's children will be scheduled. I refers to the downtime's comment field and must not contain the "," (comma) character. =item r|recheck Schedule an immediate recheck =item R|force_recheck Schedule a forced, immediate recheck =back =item B<--api1> I Set Icinga 1.x (Classic UI) JSON API root to I. I must be the root of Icinga's CGI directory. For instance, if your tactical overview has the URL C<< https://monitoring.finalrewind.org/cgi-bin/icinga/tac.cgi >>, you need to set I to C<< https://monitoring.finalrewind.org/cgi-bin/icinga >>. Please refer to the L section in this manual for more information about this API. =item B<-c>|B<--config> I Read config from I =item B<-C>|B<--no-colours> Disable colours in output =item B<-f>|B<--status-file> I Read the status from I =item B<-F>|B<--rw-file> I Use I as external commands file. =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<-m>|B<--match> I Limit selection to hosts/services whose plugin output matches I (perl regular expression, case insensitive. see L). =item B<-o>|B<--overview> Display "tactical overview"-style overview. By default (or when used with C<< -ls >>) the number of all hosts and services (both total and divided by their state) is shown. When used with C<< -lh >>, lists all hosts with the number of ok / warning / ... checks on each host. =item B<--realm> I Set HTTP Basic Auth realm to I. Defaults to C<< Icinga Access >>. Only useful when combined with a JSON or XML API. =item B<-U>|B<--as-contact> I Only operate on service visible to I. NOTE: This is meant to help find out which services a user has access to. It is NOT intended as a way to restrict access and should never be used that way. =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 or service state is Pending =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 * DateTime =item * DateTime::Format::Strptime =item * DateTime::TimeZone =item * Term::Size =back =head1 BUGS AND LIMITATIONS It is probably not clear from the documentation when an action will operate on hosts and when on services. Note that this software is not yet stable. Command line options may be changed / removed and thus break backwards compatibility at any time. =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.