#!/usr/bin/perl # Copyright (c) 2014, Evgeni Golov # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, this # list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above copyright notice, this # list of conditions and the following disclaimer in the documentation and/or # other materials provided with the distribution. # # * Neither the name of the {organization} nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use strict; use warnings; use JSON; use LWP; use Monitoring::Plugin; use Date::Parse; my $np = Monitoring::Plugin->new( usage => "Usage: %s [ -H|--hostname=] " . "[ -p|--port= ] [-s] [ -w|--warning= ] " . "[ -c|--critical= ] [ -W|--warnfails= ] " . "[ -C|--critfails= ] [ -n|--node= ]" . "[ -a|--apiversion= ]" . "[ -i|--ignore= ]", shortname => 'Check last node runs from PuppetDB', url => 'https://github.com/evgeni/check_puppetdb_nodes', version => '1.0', license => 'This plugin is free software, and comes with ABSOLUTELY NO WARRANTY. It may be used, redistributed and/or modified under the terms of the BSD 3-clause license.', ); $np->add_arg( spec => 'warning|w=i', help => "Exit with WARNING status if nodes did not update for " . "more than INTEGER minutes (default: %s)", default => 120, ); $np->add_arg( spec => 'critical|c=i', help => "Exit with CRITICAL status if nodes did not update for " . "more than INTEGER minutes (default: %s)", default => 1440, ); $np->add_arg( spec => 'warnfails|W=i', help => "Exit with WARNING status if nodes had at least INTEGER " . "failures in the last run (default: %s)", default => 1, ); $np->add_arg( spec => 'critfails|C=i', help => "Exit with CRITICAL status if nodes had at least INTEGER " . "failures in the last run (default: %s)", default => 1, ); $np->add_arg( spec => 'hostname|H=s', help => 'Hostname of the PuppetDB (default: %s)', default => 'localhost', ); $np->add_arg( spec => 'port|p=i', help => 'Port PuppetDB is running on (default: %s)', default => 8080, ); $np->add_arg( spec => 'node|n=s', help => 'Node name to check, if not given, all nodes will be checked', ); $np->add_arg( spec => 'ssl|s', help => "Use HTTPS instead of HTTP", ); $np->add_arg( spec => 'insecure|k', help => "Allow connections via HTTPS without checking certificates", ); $np->add_arg( spec => 'apiversion|a=n', help => 'Specify PupppetDB API version (default: %s)', default => 3, ); $np->add_arg( spec => 'ignore|i=s', help => 'Node names to ignore (comma-separated list) (default: %s)', default => '', ); $np->getopts; my %apiurls = ( 3 => { 'nodes' => 'v3/nodes', 'event-counts' => 'v3/event-counts' }, 4 => { 'nodes' => 'pdb/query/v4/nodes', 'event-counts' => 'pdb/query/v4/event-counts', 'logs' => 'pdb/query/v4/reports/{hash}/logs' }, ); if ( !exists $apiurls{$np->opts->apiversion} ) { $np->nagios_exit( 'UNKNOWN', 'Unsupported PuppetDB API version ' . $np->opts->apiversion ); } my @ignore_list = split( ',', $np->opts->ignore ); my $url = sprintf( 'http%s://%s:%d/', defined( $np->opts->ssl ) ? 's' : '', $np->opts->hostname, $np->opts->port ); my $ua = new LWP::UserAgent; $ua->default_header( 'Accept' => 'application/json' ); if ( defined( $np->opts->insecure ) ) { $ua->ssl_opts( verify_hostname => 0 ,SSL_verify_mode => 0x00); } my %parameters = (); if ( defined( $np->opts->node ) ) { %parameters = ( 'query' => '["=","certname","' . $np->opts->node . '"]' ); } my $uri = URI->new( $url . $apiurls{$np->opts->apiversion}{'nodes'} ); $uri->query_form(%parameters); my $response = $ua->get($uri); if ( !$response->is_success ) { $np->nagios_exit( 'UNKNOWN', $response->code . ": " . $response->status_line ); } my $data = decode_json( $response->decoded_content ); my $now = time(); if ( defined( $np->opts->node ) and !@$data ) { $np->add_message( CRITICAL, $np->opts->node . " not found in puppetdb\n" ); } foreach my $node (@$data) { my $certname = defined($node->{'certname'}) ? $node->{'certname'} : $node->{'name'} ; my $deactivated = $node->{'deactivated'}; my $catalog_timestamp = $node->{'catalog_timestamp'}; my $report_hash = $node->{'latest_report_hash'}; my $ts = str2time($catalog_timestamp); next if grep { $certname eq $_ } @ignore_list; if ( !defined $deactivated and !length $catalog_timestamp ) { $np->add_message( CRITICAL, "$certname last run UNAVAILABLE\n" ); } if ( !defined $deactivated and length $catalog_timestamp ) { my $delta = ( $now - $ts ); if ( $delta > ( $np->opts->critical * 60 ) ) { $np->add_message( CRITICAL, "$certname did not update since $catalog_timestamp\n" ); } elsif ( $delta > ( $np->opts->warning * 60 ) ) { $np->add_message( WARNING, "$certname did not update since $catalog_timestamp\n" ); } my %apiparameters = ( 3 => { 'query' => '["and",["=","certname","' . $certname . '"],["=","latest-report?",true]]', 'summarize-by' => 'certname', 'count-by' => 'resource', }, 4 => { 'query' => '["and",["=","certname","' . $certname . '"],["=","latest_report?",true]]', 'summarize_by' => 'certname', 'count_by' => 'resource', } ); my $uri = URI->new( $url . $apiurls{$np->opts->apiversion}{'event-counts'} ); $uri->query_form($apiparameters{$np->opts->apiversion}); $response = $ua->get($uri); if ( $response->is_success ) { my $node_data = decode_json( $response->decoded_content ); my $failures = 0; if ( defined( @$node_data[0] ) and defined( @$node_data[0]->{'failures'} ) ) { $failures = @$node_data[0]->{'failures'}; } if ( $failures >= $np->opts->critfails ) { $np->add_message( CRITICAL, "$certname had $failures failures in the last run\n" ); } elsif ( $failures >= $np->opts->warnfails ) { $np->add_message( WARNING, "$certname had $failures failures in the last run\n" ); } elsif ( exists $apiurls{$np->opts->apiversion}{'logs'} and $report_hash) { my $apiurl = $apiurls{$np->opts->apiversion}{'logs'}; $apiurl =~ s/{hash}/$report_hash/; $uri = URI->new( $url . $apiurl ); $response = $ua->get($uri); if ( $response->is_success ) { my $logs = decode_json( $response->decoded_content ); foreach my $log (@$logs) { my $tags = $log->{'tags'}; if ( grep(/^err$/, @$tags) ) { $np->add_message( WARNING, "$certname, $log->{'message'}" ); } } } } } else { $np->nagios_exit( 'UNKNOWN', 'Unsupported query ' . $response->decoded_content); } } } my $code; my $message; ( $code, $message ) = $np->check_messages; $np->nagios_exit( $code, $message );