package Monitoring::Livestatus; use 5.006; use strict; use warnings; use Data::Dumper; use Carp; use Digest::MD5 qw(md5_hex); use Monitoring::Livestatus::INET; use Monitoring::Livestatus::UNIX; use Monitoring::Livestatus::MULTI; use Encode; use JSON::XS; our $VERSION = '0.74'; =head1 NAME Monitoring::Livestatus - Perl API for check_mk livestatus to access runtime data from Nagios and Icinga =head1 SYNOPSIS use Monitoring::Livestatus; my $ml = Monitoring::Livestatus->new( socket => '/var/lib/livestatus/livestatus.sock' ); my $hosts = $ml->selectall_arrayref("GET hosts"); =head1 DESCRIPTION This module connects via socket/tcp to the check_mk livestatus addon for Nagios and Icinga. You first have to install and activate the mklivestatus addon in your monitoring installation. =head1 CONSTRUCTOR =head2 new ( [ARGS] ) Creates an C<Monitoring::Livestatus> object. C<new> takes at least the socketpath. Arguments are in key-value pairs. See L<EXAMPLES> for more complex variants. =over 4 =item socket path to the UNIX socket of check_mk livestatus =item server use this server for a TCP connection =item peer alternative way to set socket or server, if value contains ':' server is used, else socket =item name human readable name for this connection, defaults to the the socket/server address =item verbose verbose mode =item line_seperator ascii code of the line seperator, defaults to 10, (newline) =item column_seperator ascii code of the column seperator, defaults to 0 (null byte) =item list_seperator ascii code of the list seperator, defaults to 44 (comma) =item host_service_seperator ascii code of the host/service seperator, defaults to 124 (pipe) =item keepalive enable keepalive. Default is off =item errors_are_fatal errors will die with an error message. Default: on =item warnings show warnings currently only querys without Columns: Header will result in a warning =item timeout set a general timeout. Used for connect and querys, no default =item query_timeout set a query timeout. Used for retrieving querys, Default 60sec =item connect_timeout set a connect timeout. Used for initial connections, default 5sec =item use_threads only used with multiple backend connections. Default is to don't threads where available. As threads in perl are causing problems with tied resultset and using more memory. Querys are usually faster without threads, except for very slow backends connections. =back If the constructor is only passed a single argument, it is assumed to be a the C<peer> specification. Use either socker OR server. =cut sub new { my $class = shift; unshift(@_, "peer") if scalar @_ == 1; my(%options) = @_; my $self = { "verbose" => 0, # enable verbose output "socket" => undef, # use unix sockets "server" => undef, # use tcp connections "peer" => undef, # use for socket / server connections "name" => undef, # human readable name "line_seperator" => 10, # defaults to newline "column_seperator" => 0, # defaults to null byte "list_seperator" => 44, # defaults to comma "host_service_seperator" => 124, # defaults to pipe "keepalive" => 0, # enable keepalive? "errors_are_fatal" => 1, # die on errors "backend" => undef, # should be keept undef, used internally "timeout" => undef, # timeout for tcp connections "query_timeout" => 60, # query timeout for tcp connections "connect_timeout" => 5, # connect timeout for tcp connections "timeout" => undef, # timeout for tcp connections "use_threads" => undef, # use threads, default is to use threads where available "warnings" => 1, # show warnings, for example on querys without Column: Header "logger" => undef, # logger object used for statistical informations and errors / warnings "deepcopy" => undef, # copy result set to avoid errors with tied structures "disabled" => 0, # if disabled, this peer will not receive any query "retries_on_connection_error" => 3, # retry x times to connect "retry_interval" => 1, # retry after x seconds }; for my $opt_key (keys %options) { if(exists $self->{$opt_key}) { $self->{$opt_key} = $options{$opt_key}; } else { croak("unknown option: $opt_key"); } } if($self->{'verbose'} and !defined $self->{'logger'}) { croak('please specify a logger object when using verbose mode'); $self->{'verbose'} = 0; } # setting a general timeout? if(defined $self->{'timeout'}) { $self->{'query_timeout'} = $self->{'timeout'}; $self->{'connect_timeout'} = $self->{'timeout'}; } bless $self, $class; # set our peer(s) from the options my $peers = $self->_get_peers(); if(!defined $peers) { croak('please specify at least one peer, socket or server'); } if(!defined $self->{'backend'}) { if(scalar @{$peers} == 1) { my $peer = $peers->[0]; $options{'name'} = $peer->{'name'}; $options{'peer'} = $peer->{'peer'}; if($peer->{'type'} eq 'UNIX') { $self->{'CONNECTOR'} = new Monitoring::Livestatus::UNIX(%options); } elsif($peer->{'type'} eq 'INET') { $self->{'CONNECTOR'} = new Monitoring::Livestatus::INET(%options); } $self->{'peer'} = $peer->{'peer'}; } else { $options{'peer'} = $peers; return new Monitoring::Livestatus::MULTI(%options); } } # set names and peer for non multi backends if(defined $self->{'CONNECTOR'}->{'name'} and !defined $self->{'name'}) { $self->{'name'} = $self->{'CONNECTOR'}->{'name'}; } if(defined $self->{'CONNECTOR'}->{'peer'} and !defined $self->{'peer'}) { $self->{'peer'} = $self->{'CONNECTOR'}->{'peer'}; } if($self->{'verbose'} and (!defined $self->{'backend'} or $self->{'backend'} ne 'Monitoring::Livestatus::MULTI')) { $self->{'logger'}->debug('initialized Monitoring::Livestatus ('.$self->peer_name.')'); } return $self; } ######################################## =head1 METHODS =head2 do do($statement) do($statement, %opts) Send a single statement without fetching the result. Always returns true. =cut sub do { my $self = shift; my $statement = shift; return if $self->{'disabled'}; $self->_send($statement); return(1); } ######################################## =head2 selectall_arrayref selectall_arrayref($statement) selectall_arrayref($statement, %opts) selectall_arrayref($statement, %opts, $limit ) Sends a query and returns an array reference of arrays my $arr_refs = $ml->selectall_arrayref("GET hosts"); to get an array of hash references do something like my $hash_refs = $ml->selectall_arrayref( "GET hosts", { Slice => {} } ); to get an array of hash references from the first 2 returned rows only my $hash_refs = $ml->selectall_arrayref( "GET hosts", { Slice => {} }, 2 ); use limit to limit the result to this number of rows column aliases can be defined with a rename hash my $hash_refs = $ml->selectall_arrayref( "GET hosts", { Slice => {}, rename => { 'name' => 'host_name' } } ); =cut sub selectall_arrayref { my $self = shift; my $statement = shift; my $opt = shift; my $limit = shift || 0; return if $self->{'disabled'}; my $result; # make opt hash keys lowercase $opt = $self->_lowercase_and_verify_options($opt); $self->_log_statement($statement, $opt, $limit) if $self->{'verbose'}; $result = $self->_send($statement, $opt); if(!defined $result) { return unless $self->{'errors_are_fatal'}; croak("got undef result for: $statement"); } # trim result set down to excepted row count if(defined $limit and $limit >= 1) { if(scalar @{$result->{'result'}} > $limit) { @{$result->{'result'}} = @{$result->{'result'}}[0..$limit-1]; } } if($opt->{'slice'}) { # make an array of hashes my @hash_refs; for my $res (@{$result->{'result'}}) { my $hash_ref; for(my $x=0;$x<scalar @{$res};$x++) { my $key = $result->{'keys'}->[$x]; if(exists $opt->{'rename'} and defined $opt->{'rename'}->{$key}) { $key = $opt->{'rename'}->{$key}; } $hash_ref->{$key} = $res->[$x]; } # add callbacks if(exists $opt->{'callbacks'}) { for my $key (keys %{$opt->{'callbacks'}}) { $hash_ref->{$key} = $opt->{'callbacks'}->{$key}->($hash_ref); } } push @hash_refs, $hash_ref; } return(\@hash_refs); } elsif(exists $opt->{'callbacks'}) { for my $res (@{$result->{'result'}}) { # add callbacks if(exists $opt->{'callbacks'}) { for my $key (keys %{$opt->{'callbacks'}}) { push @{$res}, $opt->{'callbacks'}->{$key}->($res); } } } } if(exists $opt->{'callbacks'}) { for my $key (keys %{$opt->{'callbacks'}}) { push @{$result->{'keys'}}, $key; } } return($result->{'result'}); } ######################################## =head2 selectall_hashref selectall_hashref($statement, $key_field) selectall_hashref($statement, $key_field, %opts) Sends a query and returns a hashref with the given key my $hashrefs = $ml->selectall_hashref("GET hosts", "name"); =cut sub selectall_hashref { my $self = shift; my $statement = shift; my $key_field = shift; my $opt = shift; $opt = $self->_lowercase_and_verify_options($opt); $opt->{'slice'} = 1; croak("key is required for selectall_hashref") if !defined $key_field; my $result = $self->selectall_arrayref($statement, $opt); my %indexed; for my $row (@{$result}) { if($key_field eq '$peername') { $indexed{$self->peer_name} = $row; } elsif(!defined $row->{$key_field}) { my %possible_keys = keys %{$row}; croak("key $key_field not found in result set, possible keys are: ".join(', ', sort keys %possible_keys)); } else { $indexed{$row->{$key_field}} = $row; } } return(\%indexed); } ######################################## =head2 selectcol_arrayref selectcol_arrayref($statement) selectcol_arrayref($statement, %opt ) Sends a query an returns an arrayref for the first columns my $array_ref = $ml->selectcol_arrayref("GET hosts\nColumns: name"); $VAR1 = [ 'localhost', 'gateway', ]; returns an empty array if nothing was found to get a different column use this my $array_ref = $ml->selectcol_arrayref( "GET hosts\nColumns: name contacts", { Columns => [2] } ); you can link 2 columns in a hash result set my %hash = @{ $ml->selectcol_arrayref( "GET hosts\nColumns: name contacts", { Columns => [1,2] } ) }; produces a hash with host the contact assosiation $VAR1 = { 'localhost' => 'user1', 'gateway' => 'user2' }; =cut sub selectcol_arrayref { my $self = shift; my $statement = shift; my $opt = shift; # make opt hash keys lowercase $opt = $self->_lowercase_and_verify_options($opt); # if now colums are set, use just the first one if(!defined $opt->{'columns'} or ref $opt->{'columns'} ne 'ARRAY') { @{$opt->{'columns'}} = qw{1}; } my $result = $self->selectall_arrayref($statement); my @column; for my $row (@{$result}) { for my $nr (@{$opt->{'columns'}}) { push @column, $row->[$nr-1]; } } return(\@column); } ######################################## =head2 selectrow_array selectrow_array($statement) selectrow_array($statement, %opts) Sends a query and returns an array for the first row my @array = $ml->selectrow_array("GET hosts"); returns undef if nothing was found =cut sub selectrow_array { my $self = shift; my $statement = shift; my $opt = shift; # make opt hash keys lowercase $opt = $self->_lowercase_and_verify_options($opt); my @result = @{$self->selectall_arrayref($statement, $opt, 1)}; return @{$result[0]} if scalar @result > 0; return; } ######################################## =head2 selectrow_arrayref selectrow_arrayref($statement) selectrow_arrayref($statement, %opts) Sends a query and returns an array reference for the first row my $arrayref = $ml->selectrow_arrayref("GET hosts"); returns undef if nothing was found =cut sub selectrow_arrayref { my $self = shift; my $statement = shift; my $opt = shift; # make opt hash keys lowercase $opt = $self->_lowercase_and_verify_options($opt); my $result = $self->selectall_arrayref($statement, $opt, 1); return if !defined $result; return $result->[0] if scalar @{$result} > 0; return; } ######################################## =head2 selectrow_hashref selectrow_hashref($statement) selectrow_hashref($statement, %opt) Sends a query and returns a hash reference for the first row my $hashref = $ml->selectrow_hashref("GET hosts"); returns undef if nothing was found =cut sub selectrow_hashref { my $self = shift; my $statement = shift; my $opt = shift; # make opt hash keys lowercase $opt = $self->_lowercase_and_verify_options($opt); $opt->{slice} = 1; my $result = $self->selectall_arrayref($statement, $opt, 1); return if !defined $result; return $result->[0] if scalar @{$result} > 0; return; } ######################################## =head2 selectscalar_value selectscalar_value($statement) selectscalar_value($statement, %opt) Sends a query and returns a single scalar my $count = $ml->selectscalar_value("GET hosts\nStats: state = 0"); returns undef if nothing was found =cut sub selectscalar_value { my $self = shift; my $statement = shift; my $opt = shift; # make opt hash keys lowercase $opt = $self->_lowercase_and_verify_options($opt); my $row = $self->selectrow_arrayref($statement); return if !defined $row; return $row->[0] if scalar @{$row} > 0; return; } ######################################## =head2 errors_are_fatal errors_are_fatal() errors_are_fatal($value) Enable or disable fatal errors. When enabled the module will croak on any error. returns the current setting if called without new value =cut sub errors_are_fatal { my $self = shift; my $value = shift; my $old = $self->{'errors_are_fatal'}; $self->{'errors_are_fatal'} = $value; $self->{'CONNECTOR'}->{'errors_are_fatal'} = $value if defined $self->{'CONNECTOR'}; return $old; } ######################################## =head2 warnings warnings() warnings($value) Enable or disable warnings. When enabled the module will carp on warnings. returns the current setting if called without new value =cut sub warnings { my $self = shift; my $value = shift; my $old = $self->{'warnings'}; $self->{'warnings'} = $value; $self->{'CONNECTOR'}->{'warnings'} = $value if defined $self->{'CONNECTOR'}; return $old; } ######################################## =head2 verbose verbose() verbose($values) Enable or disable verbose output. When enabled the module will dump out debug output returns the current setting if called without new value =cut sub verbose { my $self = shift; my $value = shift; my $old = $self->{'verbose'}; $self->{'verbose'} = $value; $self->{'CONNECTOR'}->{'verbose'} = $value if defined $self->{'CONNECTOR'}; return $old; } ######################################## =head2 peer_addr $ml->peer_addr() returns the current peer address when using multiple backends, a list of all addresses is returned in list context =cut sub peer_addr { my $self = shift; return "".$self->{'peer'}; } ######################################## =head2 peer_name $ml->peer_name() $ml->peer_name($string) if new value is set, name is set to this value always returns the current peer name when using multiple backends, a list of all names is returned in list context =cut sub peer_name { my $self = shift; my $value = shift; if(defined $value and $value ne '') { $self->{'name'} = $value; } return "".$self->{'name'}; } ######################################## =head2 peer_key $ml->peer_key() returns a uniq key for this peer when using multiple backends, a list of all keys is returned in list context =cut sub peer_key { my $self = shift; if(!defined $self->{'key'}) { $self->{'key'} = md5_hex($self->peer_addr." ".$self->peer_name); } return $self->{'key'}; } ######################################## =head2 marked_bad $ml->marked_bad() returns true if the current connection is marked down =cut sub marked_bad { my $self = shift; return 0; } ######################################## =head2 disable $ml->disable() disables this connection, returns the last state. =cut sub disable { my $self = shift; my $prev = $self->{'disabled'}; $self->{'disabled'} = 1; return $prev; } ######################################## =head2 enable $ml->enable() enables this connection, returns the last state. =cut sub enable { my $self = shift; my $prev = $self->{'disabled'}; $self->{'disabled'} = 0; return $prev; } ######################################## # INTERNAL SUBS ######################################## sub _send { my $self = shift; my $statement = shift; my $opt = shift; delete $self->{'meta_data'}; my $header = ""; my $keys; my $with_peers = 0; if(defined $opt->{'addpeer'} and $opt->{'addpeer'}) { $with_peers = 1; } $Monitoring::Livestatus::ErrorCode = 0; undef $Monitoring::Livestatus::ErrorMessage; return(490, $self->_get_error(490), undef) if !defined $statement; chomp($statement); my($status,$msg,$body); if($statement =~ m/^Separators:/mx) { $status = 492; $msg = $self->_get_error($status); } elsif($statement =~ m/^KeepAlive:/mx) { $status = 496; $msg = $self->_get_error($status); } elsif($statement =~ m/^ResponseHeader:/mx) { $status = 495; $msg = $self->_get_error($status); } elsif($statement =~ m/^ColumnHeaders:/mx) { $status = 494; $msg = $self->_get_error($status); } elsif($statement =~ m/^OuputFormat:/mx) { $status = 493; $msg = $self->_get_error($status); } # should be cought in mlivestatus directly elsif($statement =~ m/^Limit:\ (.*)$/mx and $1 !~ m/^\d+$/mx) { $status = 403; $msg = $self->_get_error($status); } elsif($statement =~ m/^GET\ (.*)$/mx and $1 =~ m/^\s*$/mx) { $status = 403; $msg = $self->_get_error($status); } elsif($statement =~ m/^Columns:\ (.*)$/mx and ($1 =~ m/,/mx or $1 =~ /^\s*$/mx)) { $status = 405; $msg = $self->_get_error($status); } elsif($statement !~ m/^GET\ /mx and $statement !~ m/^COMMAND\ /mx) { $status = 401; $msg = $self->_get_error($status); } else { # Add Limits header if(defined $opt->{'limit_start'}) { $statement .= "\nLimit: ".($opt->{'limit_start'} + $opt->{'limit_length'}); } # for querys with column header, no seperate columns will be returned if($statement =~ m/^Columns:\ (.*)$/mx) { ($statement,$keys) = $self->_extract_keys_from_columns_header($statement); } elsif($statement =~ m/^Stats:\ (.*)$/mx or $statement =~ m/^StatsGroupBy:\ (.*)$/mx) { ($statement,$keys) = $self->_extract_keys_from_stats_statement($statement); } # Commands need no additional header if($statement !~ m/^COMMAND/mx) { $header .= "OutputFormat: json\n"; $header .= "ResponseHeader: fixed16\n"; if($self->{'keepalive'}) { $header .= "KeepAlive: on\n"; } # remove empty lines from statement $statement =~ s/\n+/\n/gmx; } # add additional headers if(defined $opt->{'header'} and ref $opt->{'header'} eq 'HASH') { for my $key ( keys %{$opt->{'header'}}) { $header .= $key.": ".$opt->{'header'}->{$key}."\n"; } } chomp($statement); my $send = "$statement\n$header"; $self->{'logger'}->debug("> ".Dumper($send)) if $self->{'verbose'}; ($status,$msg,$body) = $self->_send_socket($send); if($self->{'verbose'}) { #$self->{'logger'}->debug("got:"); #$self->{'logger'}->debug(Dumper(\@erg)); $self->{'logger'}->debug("status: ".Dumper($status)); $self->{'logger'}->debug("msg: ".Dumper($msg)); $self->{'logger'}->debug("< ".Dumper($body)); } } if($status >= 300) { $body = '' if !defined $body; chomp($body); $Monitoring::Livestatus::ErrorCode = $status; if(defined $body and $body ne '') { $Monitoring::Livestatus::ErrorMessage = $body; } else { $Monitoring::Livestatus::ErrorMessage = $msg; } $self->{'logger'}->error($status." - ".$Monitoring::Livestatus::ErrorMessage." in query:\n'".$statement) if $self->{'verbose'}; if($self->{'errors_are_fatal'}) { croak("ERROR ".$status." - ".$Monitoring::Livestatus::ErrorMessage." in query:\n'".$statement."'\n"); } return; } # return a empty result set if nothing found return({ keys => [], result => []}) if !defined $body; my $line_seperator = chr($self->{'line_seperator'}); my $col_seperator = chr($self->{'column_seperator'}); my $peer_name = $self->peer_name; my $peer_addr = $self->peer_addr; my $peer_key = $self->peer_key; my $limit_start = 0; if(defined $opt->{'limit_start'}) { $limit_start = $opt->{'limit_start'}; } my $result; # fix json output $body =~ s/\],\n\]\n$/]]/mx; eval { $result = decode_json($body); }; if($@) { my $message = "ERROR ".$@." in text: '".$body."'\" for statement: '$statement'\n"; $self->{'logger'}->error($message) if $self->{'verbose'}; if($self->{'errors_are_fatal'}) { croak($message); } } # for querys with column header, no separate columns will be returned if(!defined $keys) { $self->{'logger'}->warn("got statement without Columns: header!") if $self->{'verbose'}; if($self->{'warnings'}) { carp("got statement without Columns: header! -> ".$statement); } $keys = shift @{$result}; } # add peer information? if(defined $with_peers and $with_peers == 1) { unshift @{$keys}, 'peer_name'; unshift @{$keys}, 'peer_addr'; unshift @{$keys}, 'peer_key'; for my $row (@{$result}) { unshift @{$row}, $peer_name; unshift @{$row}, $peer_addr; unshift @{$row}, $peer_key; } } # set some metadata $self->{'meta_data'} = { 'result_count' => scalar @${result}, }; return({ keys => $keys, result => $result }); } ######################################## sub _open { my $self = shift; my $statement = shift; # return the current socket in keep alive mode if($self->{'keepalive'} and defined $self->{'sock'} and $self->{'sock'}->connected) { $self->{'logger'}->debug("reusing old connection") if $self->{'verbose'}; return($self->{'sock'}); } my $sock = $self->{'CONNECTOR'}->_open(); # store socket for later retrieval if($self->{'keepalive'}) { $self->{'sock'} = $sock; } $self->{'logger'}->debug("using new connection") if $self->{'verbose'}; return($sock); } ######################################## sub _close { my $self = shift; my $sock = shift; undef $self->{'sock'}; return($self->{'CONNECTOR'}->_close($sock)); } ######################################## =head1 QUERY OPTIONS In addition to the normal query syntax from the livestatus addon, it is possible to set column aliases in various ways. =head2 AddPeer adds the peers name, addr and key to the result set: my $hosts = $ml->selectall_hashref( "GET hosts\nColumns: name alias state", "name", { AddPeer => 1 } ); =head2 Backend send the query only to some specific backends. Only useful when using multiple backends. my $hosts = $ml->selectall_arrayref( "GET hosts\nColumns: name alias state", { Backends => [ 'key1', 'key4' ] } ); =head2 Columns only return the given column indexes my $array_ref = $ml->selectcol_arrayref( "GET hosts\nColumns: name contacts", { Columns => [2] } ); see L<selectcol_arrayref> for more examples =head2 Deepcopy deep copy/clone the result set. Only effective when using multiple backends and threads. This can be safely turned off if you dont change the result set. If you get an error like "Invalid value for shared scalar" error" this should be turned on. my $array_ref = $ml->selectcol_arrayref( "GET hosts\nColumns: name contacts", { Deepcopy => 1 } ); =head2 Limit Just like the Limit: <nr> option from livestatus itself. In addition you can add a start,length limit. my $array_ref = $ml->selectcol_arrayref( "GET hosts\nColumns: name contacts", { Limit => "10,20" } ); This example will return 20 rows starting at row 10. You will get row 10-30. Cannot be combined with a Limit inside the query because a Limit will be added automatically. Adding a limit this way will greatly increase performance and reduce memory usage. This option is multibackend safe contrary to the "Limit: " part of a statement. Sending a statement like "GET...Limit: 10" with 3 backends will result in 30 rows. Using this options, you will receive only the first 10 rows. =head2 Rename see L<COLUMN ALIAS> for detailed explainaton =head2 Slice see L<selectall_arrayref> for detailed explainaton =head2 Sum The Sum option only applies when using multiple backends. The values from all backends with be summed up to a total. my $stats = $ml->selectrow_hashref( "GET hosts\nStats: state = 0\nStats: state = 1", { Sum => 1 } ); =cut ######################################## # wrapper around _send_socket_do sub _send_socket { my $self = shift; my $statement = shift; my $retries = 0; my($status, $msg, $recv); # try to avoid connection errors eval { local $SIG{PIPE} = sub { die("broken pipe"); $self->{'logger'}->debug("broken pipe, closing socket") if $self->{'verbose'}; $self->_close($self->{'sock'}); }; if($self->{'retries_on_connection_error'} <= 0) { ($status, $msg, $recv) = $self->_send_socket_do($statement); return; } while((!defined $status or ($status == 491 or $status == 497 or $status == 500)) and $retries < $self->{'retries_on_connection_error'}) { $retries++; ($status, $msg, $recv) = $self->_send_socket_do($statement); $self->{'logger'}->debug('query status '.$status) if $self->{'verbose'}; if($status == 491 or $status == 497 or $status == 500) { $self->{'logger'}->debug('got status '.$status.' retrying in '.$self->{'retry_interval'}.' seconds') if $self->{'verbose'}; $self->_close(); sleep($self->{'retry_interval'}) if $retries < $self->{'retries_on_connection_error'}; } } }; if($@) { $self->{'logger'}->debug("try 1 failed: $@") if $self->{'verbose'}; if(defined $@ and $@ =~ /broken\ pipe/mx) { return $self->_send_socket_do($statement); } croak($@) if $self->{'errors_are_fatal'}; } croak($msg) if($status >= 400 and $self->{'errors_are_fatal'}); return($status, $msg, $recv); } ######################################## sub _send_socket_do { my $self = shift; my $statement = shift; my($recv,$header); my $sock = $self->_open() or return(491, $self->_get_error(491), $!); utf8::decode($statement); print $sock encode('utf-8' => $statement) or return($self->_socket_error($statement, $sock, 'write to socket failed: '.$!)); print $sock "\n"; # COMMAND statements never return something if($statement =~ m/^COMMAND/mx) { return('201', $self->_get_error(201), undef); } $sock->read($header, 16) or return($self->_socket_error($statement, $sock, 'reading header from socket failed, check your livestatus logfile: '.$!)); $self->{'logger'}->debug("header: $header") if $self->{'verbose'}; my($status, $msg, $content_length) = $self->_parse_header($header, $sock); return($status, $msg, undef) if !defined $content_length; if($content_length > 0) { $sock->read($recv, $content_length) or return($self->_socket_error($statement, $sock, 'reading body from socket failed')); } $self->_close($sock) unless $self->{'keepalive'}; return($status, $msg, $recv); } ######################################## sub _socket_error { my $self = shift; my $statement = shift; my $sock = shift; my $body = shift; my $message = "\n"; $message .= "peer ".Dumper($self->peer_name); $message .= "statement ".Dumper($statement); $message .= "message ".Dumper($body); $self->{'logger'}->error($message) if $self->{'verbose'}; if($self->{'retries_on_connection_error'} <= 0) { if($self->{'errors_are_fatal'}) { croak($message); } else { carp($message); } } $self->_close(); return(500, $self->_get_error(500), $message); } ######################################## sub _parse_header { my $self = shift; my $header = shift; my $sock = shift; if(!defined $header) { return(497, $self->_get_error(497), undef); } my $headerlength = length($header); if($headerlength != 16) { return(498, $self->_get_error(498)."\ngot: ".$header.<$sock>, undef); } chomp($header); my $status = substr($header,0,3); my $content_length = substr($header,5); if($content_length !~ m/^\s*(\d+)$/mx) { return(499, $self->_get_error(499)."\ngot: ".$header.<$sock>, undef); } else { $content_length = $1; } return($status, $self->_get_error($status), $content_length); } ######################################## =head1 COLUMN ALIAS In addition to the normal query syntax from the livestatus addon, it is possible to set column aliases in various ways. A valid Columns: Header could look like this: my $hosts = $ml->selectall_arrayref( "GET hosts\nColumns: state as status" ); Stats queries could be aliased too: my $stats = $ml->selectall_arrayref( "GET hosts\nStats: state = 0 as up" ); This syntax is available for: Stats, StatsAnd, StatsOr and StatsGroupBy An alternative way to set column aliases is to define rename option key/value pairs: my $hosts = $ml->selectall_arrayref( "GET hosts\nColumns: name", { rename => { 'name' => 'hostname' } } ); =cut ######################################## sub _extract_keys_from_stats_statement { my $self = shift; my $statement = shift; my(@header, $new_statement); for my $line (split/\n/mx, $statement) { if($line =~ m/^Stats:\ (.*)\s+as\s+(.*)$/mxi) { push @header, $2; $line = 'Stats: '.$1; } elsif($line =~ m/^Stats:\ (.*)$/mx) { push @header, $1; } if($line =~ m/^StatsAnd:\ (\d+)\s+as\s+(.*)$/mx) { for(my $x = 0; $x < $1; $x++) { pop @header; } $line = 'StatsAnd: '.$1; push @header, $2; } elsif($line =~ m/^StatsAnd:\ (\d+)$/mx) { my @to_join; for(my $x = 0; $x < $1; $x++) { unshift @to_join, pop @header; } push @header, join(' && ', @to_join); } if($line =~ m/^StatsOr:\ (\d+)\s+as\s+(.*)$/mx) { for(my $x = 0; $x < $1; $x++) { pop @header; } $line = 'StatsOr: '.$1; push @header, $2; } elsif($line =~ m/^StatsOr:\ (\d+)$/mx) { my @to_join; for(my $x = 0; $x < $1; $x++) { unshift @to_join, pop @header; } push @header, join(' || ', @to_join); } # StatsGroupBy header are always sent first if($line =~ m/^StatsGroupBy:\ (.*)\s+as\s+(.*)$/mxi) { unshift @header, $2; $line = 'StatsGroupBy: '.$1; } elsif($line =~ m/^StatsGroupBy:\ (.*)$/mx) { unshift @header, $1; } $new_statement .= $line."\n"; } return($new_statement, \@header); } ######################################## sub _extract_keys_from_columns_header { my $self = shift; my $statement = shift; my(@header, $new_statement); for my $line (split/\n/mx, $statement) { if($line =~ m/^Columns:\s+(.*)$/mx) { for my $column (split/\s+/mx, $1) { if($column eq 'as') { pop @header; } else { push @header, $column; } } $line =~ s/\s+as\s+([^\s]+)/\ /gmx; } $new_statement .= $line."\n"; } return($new_statement, \@header); } ######################################## =head1 ERROR HANDLING Errorhandling can be done like this: use Monitoring::Livestatus; my $ml = Monitoring::Livestatus->new( socket => '/var/lib/livestatus/livestatus.sock' ); $ml->errors_are_fatal(0); my $hosts = $ml->selectall_arrayref("GET hosts"); if($Monitoring::Livestatus::ErrorCode) { croak($Monitoring::Livestatus::ErrorMessage); } =cut sub _get_error { my $self = shift; my $code = shift; my $codes = { '200' => 'OK. Reponse contains the queried data.', '201' => 'COMMANDs never return something', '400' => 'The request contains an invalid header.', '401' => 'The request contains an invalid header.', '402' => 'The request is completely invalid.', '403' => 'The request is incomplete.', '404' => 'The target of the GET has not been found (e.g. the table).', '405' => 'A non-existing column was being referred to', '490' => 'no query', '491' => 'failed to connect', '492' => 'Separators not allowed in statement. Please use the seperator options in new()', '493' => 'OuputFormat not allowed in statement. Header will be set automatically', '494' => 'ColumnHeaders not allowed in statement. Header will be set automatically', '495' => 'ResponseHeader not allowed in statement. Header will be set automatically', '496' => 'Keepalive not allowed in statement. Please use the keepalive option in new()', '497' => 'got no header', '498' => 'header is not exactly 16byte long', '499' => 'not a valid header (no content-length)', '500' => 'socket error', }; confess('non existant error code: '.$code) if !defined $codes->{$code}; return($codes->{$code}); } ######################################## sub _get_peers { my $self = shift; # set options for our peer(s) my %options; for my $opt_key (keys %{$self}) { $options{$opt_key} = $self->{$opt_key}; } my $peers = []; # check if the supplied peer is a socket or a server address if(defined $self->{'peer'}) { if(ref $self->{'peer'} eq '') { my $name = $self->{'name'} || "".$self->{'peer'}; if(index($self->{'peer'}, ':') > 0) { push @{$peers}, { 'peer' => "".$self->{'peer'}, type => 'INET', name => $name }; } else { push @{$peers}, { 'peer' => "".$self->{'peer'}, type => 'UNIX', name => $name }; } } elsif(ref $self->{'peer'} eq 'ARRAY') { for my $peer (@{$self->{'peer'}}) { if(ref $peer eq 'HASH') { next if !defined $peer->{'peer'}; $peer->{'name'} = "".$peer->{'peer'} unless defined $peer->{'name'}; if(!defined $peer->{'type'}) { $peer->{'type'} = 'UNIX'; if(index($peer->{'peer'}, ':') >= 0) { $peer->{'type'} = 'INET'; } } push @{$peers}, $peer; } else { my $type = 'UNIX'; if(index($peer, ':') >= 0) { $type = 'INET'; } push @{$peers}, { 'peer' => "".$peer, type => $type, name => "".$peer }; } } } elsif(ref $self->{'peer'} eq 'HASH') { for my $peer (keys %{$self->{'peer'}}) { my $name = $self->{'peer'}->{$peer}; my $type = 'UNIX'; if(index($peer, ':') >= 0) { $type = 'INET'; } push @{$peers}, { 'peer' => "".$peer, type => $type, name => "".$name }; } } else { confess("type ".(ref $self->{'peer'})." is not supported for peer option"); } } if(defined $self->{'socket'}) { my $name = $self->{'name'} || "".$self->{'socket'}; push @{$peers}, { 'peer' => "".$self->{'socket'}, type => 'UNIX', name => $name }; } if(defined $self->{'server'}) { my $name = $self->{'name'} || "".$self->{'server'}; push @{$peers}, { 'peer' => "".$self->{'server'}, type => 'INET', name => $name }; } # check if we got a peer if(scalar @{$peers} == 0) { croak('please specify at least one peer, socket or server'); } # clean up delete $options{'peer'}; delete $options{'socket'}; delete $options{'server'}; return $peers; } ######################################## sub _lowercase_and_verify_options { my $self = shift; my $opts = shift; my $return = {}; # list of allowed options my $allowed_options = { 'addpeer' => 1, 'backend' => 1, 'columns' => 1, 'deepcopy' => 1, 'header' => 1, 'limit' => 1, 'limit_start' => 1, 'limit_length' => 1, 'rename' => 1, 'slice' => 1, 'sum' => 1, 'callbacks' => 1, }; for my $key (keys %{$opts}) { if($self->{'warnings'} and !defined $allowed_options->{lc $key}) { carp("unknown option used: $key - please use only: ".join(", ", keys %{$allowed_options})); } $return->{lc $key} = $opts->{$key}; } # set limits if(defined $return->{'limit'}) { if(index($return->{'limit'}, ',') != -1) { my($limit_start,$limit_length) = split /,/mx, $return->{'limit'}; $return->{'limit_start'} = $limit_start; $return->{'limit_length'} = $limit_length; } else { $return->{'limit_start'} = 0; $return->{'limit_length'} = $return->{'limit'}; } delete $return->{'limit'}; } return($return); } ######################################## sub _log_statement { my $self = shift; my $statement = shift; my $opt = shift; my $limit = shift; my $d = Data::Dumper->new([$opt]); $d->Indent(0); my $optstring = $d->Dump; $optstring =~ s/^\$VAR1\s+=\s+//mx; $optstring =~ s/;$//mx; # remove empty lines from statement $statement =~ s/\n+/\n/gmx; my $cleanstatement = $statement; $cleanstatement =~ s/\n/\\n/gmx; $self->{'logger'}->debug('selectall_arrayref("'.$cleanstatement.'", '.$optstring.', '.$limit.')'); return 1; } ######################################## 1; =head1 EXAMPLES =head2 Multibackend Configuration use Monitoring::Livestatus; my $ml = Monitoring::Livestatus->new( name => 'multiple connector', verbose => 0, keepalive => 1, peer => [ { name => 'DMZ Monitoring', peer => '50.50.50.50:9999', }, { name => 'Local Monitoring', peer => '/tmp/livestatus.socket', }, { name => 'Special Monitoring', peer => '100.100.100.100:9999', } ], ); my $hosts = $ml->selectall_arrayref("GET hosts"); =head1 SEE ALSO For more information about the query syntax and the livestatus plugin installation see the Livestatus page: http://mathias-kettner.de/checkmk_livestatus.html =head1 AUTHOR Sven Nierlein, E<lt>nierlein@cpan.orgE<gt> =head1 COPYRIGHT AND LICENSE Copyright (C) 2009 by Sven Nierlein This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut __END__