#!/usr/bin/perl -w
##############################
# Nagios Ajax API CGI Script #
# Ryan Armstrong 2013        #
##############################
#
# This script will parse the Nagios status cache files and return
# the requested data as Nagios Config, XML or JSON encoded data via HTTP
# or STDOUT.
#
# This script will reparse the source files with each request so please be
# mindful of impact to disk IO and CPU load in larger environments.
#
# For best performance, move your Nagios data to a shared memory location
# such as /dev/shm.
#
# Copy or link this script to <nagios_path>/sbin and access it via:
# http://<nagios_url>/nagios/cgi-bin/ajax.cgi
#
# For web access, pass arguments as follows:
# http://<nagios_url>/nagios/cgi-bin/ajax.cgi?<arg1>=<val1>&<arg2>=<arg2>...
#
# For shell/STDOUT, pass arguments as follows:
# ./ajax.cgi <arg1>=<val1> <arg2>=<val2>...
#
# If no arguments are passed, the script will parse and return all status data.
#
# The location of the Nagios status file can be set below in STATUS_FILE or
# as a command argument with:
#
# status_file=<status_file>
#
# Multiple formats are available including plain text (in Nagios config format),
# XML and JSON. The default for all web requests is JSON while the default for STDOUT
# is plain text. Specify the desired format with:
#
# format=<xml|json|text>
#
# To filter the return data, simply provide the Nagios field name and value for the
# desired data. Pseudo field 'type' is provided for filtering object types.
#
# Example:
# - The following example will return all 'servicestatus' objects for 'host1.example.com':
#   http://<nagios_url>/nagios/cgi-bin/ajax.cgi?type=servicestatus&host_name=host1.example.com
#
# You can also filter which fields are returned for each object by providing a
# comma separated list in the 'fields' argument.
#
# Example:
# - The following example will only return the 'host_name', 'service_desciption',
#   'current_state' and 'type' fields for all services ('type' is always returned):
#   http://.../ajax.cgi?type=servicestatus&fields=host_name,service_description,current_state
#
# For additional performance benefit, you can limit the number of results returned with:
# result_limit=n
#
use strict;
use warnings;
use CGI;
use Data::Dumper;
use JSON;
use String::Util 'trim';
use Switch;
use Time::HiRes 'time';
use Util::Any -all;
use XML::Simple;

use constant STATUS_FILE => '/usr/local/nagios/var/status.dat';
use constant BUILD_TREE => 0;

# Are we in web or shell?
my $web_context = defined($ENV{ GATEWAY_INTERFACE });

# Check args for usage request
if (!$web_context && defined($ARGV[0]) && any(sub { $ARGV[0] eq $_}, ('-h', '--help', '--usage'))) {
    my $usage = <<EOL;
Usage $0: [ -h ][ status_file=PATH ] [ format=text|xml|json ]
    [ result_limit=n ] [ fields=FIELD1,FIELD2,...] [ hierarchy=tree|flat ]
    [[ FILTER_FIELD=VALUE [ FILTER_FIELD2=VALUE ] ...]

  -h, --help                display this help and exit
  
Search options:

  status_file               path of Nagios status file
  format=json|xml|text      output encoding
  result_limit=LIMIT        limit the number of results returned
  hierarchy=flat|tree       output results as a hierarchical tree or flat array
                            valid only for XML and JSON
  fields=FIELD1,FIELD2,...  filter the fields which are returned for each object
  FIELD=VALUE               filter search results by object field values
  
Examples:
 - Show service state for host as XML:
   $0 format=xml fields=service_description,current_state type=servicestatus host_name=<hostname>
   
EOL
    print $usage;
    exit 0;
}

# Read filters from URL query
my %filters = CGI->new()->Vars();

# Get user defined status file path
my $status_file = STATUS_FILE;
if (defined($filters{ status_file })) {
    $status_file = $filters{ status_file };
    delete $filters{ status_file };
}

# Get user defined output format
my $format = $web_context ? 'json' : 'text';
if (defined($filters{ format })) {
    $format = $filters{ format };
    delete $filters{ format };
}

# Get user defined field list
my @fields = ();
if (defined($filters{ fields })) {
    @fields = split(',', $filters{ fields });
    push(@fields, 'type');
    delete $filters{ fields };
}

# Get user defined result limit
my $result_limit = 0;
if (defined($filters{ result_limit })) {
    $result_limit = int $filters{ result_limit };
    delete $filters{ result_limit };
}

# Get user defined hierarchy
my $build_tree = BUILD_TREE;;
if (defined($filters{ hierarchy })) {
    switch($filters{ hierarchy }) {
        case 'tree' {
            $build_tree = 1;
        }
        
        case 'flat' {
            $build_tree = 0;
        }
        
        else {
            my $msg = "ERROR: Unknown hierarchy: '$filters{ hierarchy }'";
            if($web_context) {
                print "Content-type: text/plain\n\n$msg";
            }
            die $msg;           
        }
    }
    
    delete $filters{ hierarchy };
}

# HTTP Headers
if ($web_context) {
    switch ($format) {
        case 'json' {
            print "Content-type: application/json\n\n";
        }
        
        case 'xml' {
            print "Content-type: application/xml\n\n";
        }
        
        case 'text' {
            print "Content-type: text/plain\n\n";
        }
        
        else {
            my $msg = "ERROR: Unknown format: '$format'";
            print "Content-type: text/plain\n\n$msg";
            die $msg;
        }
    }
}

# Create results hash
my %tree;
my @results = ();
my %output = (
    'filters'           => \%filters,
    'fields'            => \@fields,
    'results'           => $build_tree ? \%tree : \@results,
    'result_count'      => 0,
    'line_count'        => 0,
    'status'            => 'OK'
);

my %object_def;
my $object_type;
my $in_def = 0;
my $line_count = 0;
my $result_count = 0;

# Start stopwatch
my $time_start = time();

# Open and parse status file
if(open my $fh, $status_file) {
    while( my $line = <$fh>)  {   
        $line_count++;
        
        unless ($in_def) {
            # Check for new object definition
            ($object_type) = ($line =~ m/(\w+)\s\{/);        
            if ($object_type) {
                # New object definition
                $in_def = 1;
                %object_def = ('type' => $object_type);
            }
        } else {
            # Grab key=val pair
            my ($key, $val) = ($line =~ m/\s+([\w_]+)=(.*)/);
            if ($key) {
                $object_def{ $key } = $val;
            } else {
                # No match. Might be end of definition
                if ($line =~ /\s+\}/) {
                    $in_def = 0;
                    
                    # Check filters
                    my $include = 1;
                    foreach my $filter_key (keys %filters) {
                        $include &= (defined($object_def{ $filter_key }) && $object_def{ $filter_key } eq $filters{ $filter_key });
                    }
                    
                    # Process search result
                    if($include) {
                        $result_count++;
                        
                        # Copy hash
                        my %result = %object_def;
                        
                        # Build hierarchical tree
                        if ($build_tree) {
                            
                            switch($result{ type }) {
                                case 'info' {
                                    $tree{ info } = \%result;
                                }                                
                                case 'programstatus' {
                                    $tree{ programstatus } = \%result;
                                }                                
                                case 'contactstatus' {
                                    $tree{ contact }->{ $result{ contact_name } }->{ status } = \%result;
                                }                                
                                case 'hoststatus' {
                                    $tree{ hosts }{ host }->{ $result{ host_name } }->{ status } = \%result;
                                }                                
                                case 'hostcomment' {
                                    $tree{ hosts }{ host }->{ $result{ host_name } }->{ comment }->{ $result{ comment_id } } = \%result;
                                }
                                case 'servicestatus' {
                                    $tree{ hosts }{ host }->{ $result{ host_name } }->{ services }{ service }->{ $result{ service_description } }->{ status } = \%result;
                                }
                                case 'servicecomment' {
                                    $tree{ hosts }{ host }->{ $result{ host_name } }->{ services }{ service }->{ $result{ service_description } }->{ comments }{ comment }->{ $result{ comment_id } } = \%result;
                                }
                            }
                        } else {
                            push @results, \%result;
                        }
                        
                        # Apply field list
                        if (@fields) {
                            foreach my $field (keys %result) {
                                unless (any(sub { $_ eq $field }, @fields)) {
                                    delete $result{ $field };
                                }
                            }
                        }
                        
                        # Check result limit
                        if ($result_limit && $result_count >= $result_limit) {
                            last;
                        }
                    }
                }
            }
        }
    }
    
    # Close status file
    close $fh;
} else {
    $output{ status } = 'ERROR';
    $output{ error } = "Unable to open status file.";
}

# Search stats
my $time_end = time();
$output{ time } = $time_end - $time_start;
$output{ line_count } = $line_count;
$output{ result_count } = $result_count;
$output{ result_limit } = $result_limit;

# Output data
switch ($format) {
    case 'json' {
        print encode_json(\%output);
    }
    
    case 'xml' {
        print XMLout(\%output, (
            RootName    => 'nagios_status',
            GroupTags   => {
                fields      => 'field',
                results     => 'result',
                hosts       => 'host',
                services    => 'service',
                comments    => 'comment'
            },
            ContentKey  => 'comment_data',
            NoIndent    => $web_context,
            NoSort      => 1
        ));
    }
    
    case 'text' {
        foreach my $result (@results) {
            print "$result->{ type } {\n";
            foreach my $field (sort keys %{ $result }) {
                print "\t$field=$result->{ $field }\n";
            }
            print "\t}\n\n";
        }
        
        if (defined($output{ error })) {
            die "Error: $output{ error }\n";
        }
    }
}

# Tidy line break for shell users
if (!$web_context && $format ne 'text') {
    print "\n";
}