#!/usr/bin/perl

#Copyright (c) 2024, Zane C. Bowers-Hadley
#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.
#
#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 OWNER 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 warnings;
use strict;

=pod

=head1 NAME

opensearch - LibreNMS JSON SNMP extend for gathering backups for borg

=head1 VERSION

0.1.0

=cut

our $VERSION = '0.1.0';

=head1 SYNOPSIS

opensearch [B<-a> <auth tocken file>] [B<-c> <CA file>] [B<-h> <host>] [B<-p> <port>] [B<-S>]
[B<-I>] [B<-P>] [B<-S>] [B<-w>] [B<-o> <output file base>]

opensearch [B<--help>]

opensearch [B<--version>]

=head1 DESCRIPTION

Needs enabled in snmpd.conf like below.

    extend opensearch /usr/lib64/librenms/snmp/opensearch

If you have issues with it timing taking to long to poll and
occasionally timing out, you can set it up in cron like this.

    */5 * * * * /usr/lib64/librenms/snmp/opensearch -q -w

And then in snmpd.conf like below.

    extend opensearch /bin/cat /var/cache/opensearch_extend.json.snmp

Installing the depends can be done like below.

    # FreeBSD
    pkg install p5-JSON p5-File-Slurp p5-MIME-Base64 p5-libwww p5-LWP-Protocol-https

    # Debian
    apt-get install libjson-perl libfile-slurp-perl liblwp-protocol-https-perl

=head1 FLAGS

=head2 -a <path>

Auth token path.

=head2 -c <path>

CA file path.

Default: empty

=head2 -h <host>

The host to connect to.

Default: 127.0.0.1

=head2 -I

Do not verify hostname (when used with -S).

=head2 -o <output base path>

The base name for the output.

Default: /var/cache/opensearch_extend.json

=head2 -p <port>

The port to use.

Default: 9200

=head2 -P

Pretty print.

=head2 -q

Do not print the output.

Useful for with -w.

=head2 -S

Use HTTPS.

The last is only really relevant to the usage with SNMP.

=head2 -w

Write the results out to two files based on what is specified
via -o .

Default Raw JSON: /var/cache/opensearch_extend.json

Default SNMP Return: /var/cache/opensearch_extend.json.snmp

=cut

use Getopt::Std;
use JSON;
use LWP::UserAgent ();
use File::Slurp;
use Pod::Usage;
use MIME::Base64;
use IO::Compress::Gzip qw(gzip $GzipError);

$Getopt::Std::STANDARD_HELP_VERSION = 1;

sub main::VERSION_MESSAGE {
	print 'opensearch LibreNMS extend version '.$VERSION."\n";
}

sub main::HELP_MESSAGE {
	pod2usage( -exitval => 255, -verbose => 2, -output => \*STDOUT, );
}

my $protocol    = 'http';
my $host        = '127.0.0.1';
my $port        = 9200;
my $schema      = 'http';
my $output_base = '/var/cache/opensearch_extend.json';

#gets the options
my %opts;
getopts( 'a:c:h:p:PISqo:w', \%opts );
if ( defined( $opts{h} ) ) {
	$host = $opts{h};
}
if ( defined( $opts{p} ) ) {
	$port = $opts{p};
}
if ( $opts{S} ) {
	$schema = 'https';
}
if ( defined( $opts{o} ) ) {
	$output_base = $opts{o};
}

my $auth_token;
if ( defined( $opts{a} ) ) {
	open my $auth_file, '<', $opts{a};
	$auth_token = <$auth_file>;
	close $auth_file;
	chop $auth_token;
}

#
my $to_return = {
	error       => 0,
	errorString => '',
	version     => 1,
	date        => {},
};

my $stats_url  = $schema . '://' . $host . ':' . $port . '/_stats';
my $health_url = $schema . '://' . $host . ':' . $port . '/_cluster/health';

my $json = JSON->new->allow_nonref->canonical(1);
if ( $opts{P} ) {
	$json->pretty();
}

my $ua = LWP::UserAgent->new( timeout => 10 );

if ( $opts{I} ) {
	$ua->ssl_opts( verify_hostname => 0, SSL_verify_mode => 0x00 );
}

my $stats_response = $ua->get($stats_url);

if ( defined( $opts{c} ) ) {
	# set ca file
	$ua->ssl_opts( SSL_ca_file => $opts{c} );
}

if ( defined( $opts{a} ) ) {
	$stats_response = $ua->get( $stats_url, "Authorization" => $auth_token, );
} else {
	$stats_response = $ua->get($stats_url);
}

my $stats_json;
if ( $stats_response->is_success ) {
	eval { $stats_json = decode_json( $stats_response->decoded_content ); };
	if ($@) {
		$to_return->{errorString} = 'Failed to decode the JSON from "' . $stats_url . '"... ' . $@;
		$to_return->{error}       = 2;
		print $json->encode($to_return);
		if ( !$opts{P} ) {
			print "\n";
		}
		exit;
	}
} else {
	$to_return->{errorString} = 'Failed to get "' . $stats_url . '"... ' . $stats_response->status_line;
	$to_return->{error}       = 1;
	print $json->encode($to_return);
	if ( !$opts{P} ) {
		print "\n";
	}
	exit;
}

my $health_response;
if ( defined( $opts{a} ) ) {
	$health_response = $ua->get( $health_url, "Authorization" => $auth_token, );
} else {
	$health_response = $ua->get($health_url);
}

my $health_json;
if ( $health_response->is_success ) {
	eval { $health_json = decode_json( $health_response->decoded_content ); };
	if ($@) {
		$to_return->{errorString} = 'Failed to decode the JSON from "' . $health_url . '"... ' . $@;
		$to_return->{error}       = 2;
		print $json->encode($to_return);
		if ( !$opts{P} ) {
			print "\n";
		}
		exit;
	}
} else {
	$to_return->{errorString} = 'Failed to get "' . $health_url . '"... ' . $health_response->status_line;
	$to_return->{error}       = 1;
	print $json->encode($to_return);
	if ( !$opts{P} ) {
		print "\n";
	}
	exit;
}

#
# process the health json
#
#
$to_return->{data}{cluster_name}       = $health_json->{cluster_name};
$to_return->{data}{c_nodes}            = $health_json->{number_of_nodes};
$to_return->{data}{c_data_nodes}       = $health_json->{number_of_data_nodes};
$to_return->{data}{c_act_pri_shards}   = $health_json->{active_primary_shards};
$to_return->{data}{c_act_shards}       = $health_json->{active_shards};
$to_return->{data}{c_rel_shards}       = $health_json->{relocating_shards};
$to_return->{data}{c_init_shards}      = $health_json->{initializing_shards};
$to_return->{data}{c_delayed_shards}   = $health_json->{delayed_unassigned_shards};
$to_return->{data}{c_unass_shards}     = $health_json->{unassigned_shards};
$to_return->{data}{c_pending_tasks}    = $health_json->{number_of_pending_tasks};
$to_return->{data}{c_in_fl_fetch}      = $health_json->{number_of_in_flight_fetch};
$to_return->{data}{c_task_max_in_time} = $health_json->{task_max_waiting_in_queue_millis};
$to_return->{data}{c_act_shards_perc}  = $health_json->{active_shards_percent_as_number};

# status color to int, nagios style
# green / ok = 0
# yellow / warning = 1
# red / critical = 2
# unknown = 3
if ( $health_json->{status} =~ /[Gg][Rr][Ee][Ee][Nn]/ ) {
	$to_return->{data}{status} = 0;
} elsif ( $health_json->{status} =~ /[Yy][Ee][Ll][Ll][Oo][Ww]/ ) {
	$to_return->{data}{status} = 1;
} elsif ( $health_json->{status} =~ /[Rr][Ee][Dd]/ ) {
	$to_return->{data}{status} = 2;
} else {
	$to_return->{data}{status} = 3;
}

#
# process the stats json, sucking stuff in from under _all.total
#
$to_return->{data}{ttl_ops}          = $stats_json->{_all}{total}{translog}{operations};
$to_return->{data}{ttl_size}         = $stats_json->{_all}{total}{translog}{size_in_bytes};
$to_return->{data}{ttl_uncom_ops}    = $stats_json->{_all}{total}{translog}{uncommitted_operations};
$to_return->{data}{ttl_uncom_size}   = $stats_json->{_all}{total}{translog}{uncommitted_size_in_bytes};
$to_return->{data}{ttl_last_mod_age} = $stats_json->{_all}{total}{translog}{earliest_last_modified_age};

$to_return->{data}{ti_total}          = $stats_json->{_all}{total}{indexing}{index_total};
$to_return->{data}{ti_time}           = $stats_json->{_all}{total}{indexing}{index_time_in_millis};
$to_return->{data}{ti_failed}         = $stats_json->{_all}{total}{indexing}{index_failed};
$to_return->{data}{ti_del_total}      = $stats_json->{_all}{total}{indexing}{delete_total};
$to_return->{data}{ti_del_time}       = $stats_json->{_all}{total}{indexing}{delete_time_in_millis};
$to_return->{data}{ti_noop_up_total}  = $stats_json->{_all}{total}{indexing}{noop_update_total};
$to_return->{data}{ti_throttled_time} = $stats_json->{_all}{total}{indexing}{throttle_time_in_millis};

if ( defined( $stats_json->{_all}{total}{indexing}{is_throttled} )
	&& $stats_json->{_all}{total}{indexing}{is_throttled} eq 'true' )
{
	$to_return->{data}{ti_throttled} = 1;
} else {
	$to_return->{data}{ti_throttled} = 0;
}

$to_return->{data}{ts_q_total}  = $stats_json->{_all}{total}{search}{query_total};
$to_return->{data}{ts_q_time}   = $stats_json->{_all}{total}{search}{query_time_in_millis};
$to_return->{data}{ts_f_total}  = $stats_json->{_all}{total}{search}{fetch_total};
$to_return->{data}{ts_f_time}   = $stats_json->{_all}{total}{search}{fetch_time_in_millis};
$to_return->{data}{ts_sc_total} = $stats_json->{_all}{total}{search}{scroll_total};
$to_return->{data}{ts_sc_time}  = $stats_json->{_all}{total}{search}{scroll_time_in_millis};
$to_return->{data}{ts_su_total} = $stats_json->{_all}{total}{search}{suggest_total};
$to_return->{data}{ts_su_time}  = $stats_json->{_all}{total}{search}{suggest_time_in_millis};

$to_return->{data}{tr_total}     = $stats_json->{_all}{total}{refresh}{total};
$to_return->{data}{tr_time}      = $stats_json->{_all}{total}{refresh}{total_time_in_millis};
$to_return->{data}{tr_ext_total} = $stats_json->{_all}{total}{refresh}{external_total};
$to_return->{data}{tr_ext_time}  = $stats_json->{_all}{total}{refresh}{external_total_time_in_millis};

$to_return->{data}{tf_total}    = $stats_json->{_all}{total}{flush}{total};
$to_return->{data}{tf_periodic} = $stats_json->{_all}{total}{flush}{periodic};
$to_return->{data}{tf_time}     = $stats_json->{_all}{total}{flush}{total_time_in_millis};

$to_return->{data}{tqc_size}        = $stats_json->{_all}{total}{query_cache}{memory_size_in_bytes};
$to_return->{data}{tqc_total}       = $stats_json->{_all}{total}{query_cache}{total_count};
$to_return->{data}{tqc_hit}         = $stats_json->{_all}{total}{query_cache}{hit_count};
$to_return->{data}{tqc_miss}        = $stats_json->{_all}{total}{query_cache}{miss_count};
$to_return->{data}{tqc_miss}        = $stats_json->{_all}{total}{query_cache}{miss_count};
$to_return->{data}{tqc_cache_size}  = $stats_json->{_all}{total}{query_cache}{cache_size};
$to_return->{data}{tqc_cache_count} = $stats_json->{_all}{total}{query_cache}{cache_count};
$to_return->{data}{tqc_evictions}   = $stats_json->{_all}{total}{query_cache}{evictions};

$to_return->{data}{tg_total}         = $stats_json->{_all}{total}{get}{total};
$to_return->{data}{tg_time}          = $stats_json->{_all}{total}{get}{time_in_millis};
$to_return->{data}{tg_exists_total}  = $stats_json->{_all}{total}{get}{exists_total};
$to_return->{data}{tg_exists_time}   = $stats_json->{_all}{total}{get}{exists_time_in_millis};
$to_return->{data}{tg_missing_total} = $stats_json->{_all}{total}{get}{missing_total};
$to_return->{data}{tg_missing_time}  = $stats_json->{_all}{total}{get}{missing_time_in_millis};

$to_return->{data}{tm_total}          = $stats_json->{_all}{total}{merges}{total};
$to_return->{data}{tm_time}           = $stats_json->{_all}{total}{merges}{total_time_in_millis};
$to_return->{data}{tm_docs}           = $stats_json->{_all}{total}{merges}{total_docs};
$to_return->{data}{tm_size}           = $stats_json->{_all}{total}{merges}{total_size_in_bytes};
$to_return->{data}{tm_throttled_time} = $stats_json->{_all}{total}{merges}{total_throttled_time_in_millis};
$to_return->{data}{tm_throttled_size} = $stats_json->{_all}{total}{merges}{total_auto_throttle_in_bytes};

$to_return->{data}{tw_total} = $stats_json->{_all}{total}{warmer}{total};
$to_return->{data}{tw_time}  = $stats_json->{_all}{total}{warmer}{total_time_in_millis};

$to_return->{data}{tfd_size}      = $stats_json->{_all}{total}{fielddata}{memory_size_in_bytes};
$to_return->{data}{tfd_evictions} = $stats_json->{_all}{total}{fielddata}{evictions};

$to_return->{data}{tseg_count}        = $stats_json->{_all}{total}{segments}{count};
$to_return->{data}{tseg_size}         = $stats_json->{_all}{total}{segments}{memory_in_bytes};
$to_return->{data}{tseg_terms_size}   = $stats_json->{_all}{total}{segments}{terms_memory_in_bytes};
$to_return->{data}{tseg_fields_size}  = $stats_json->{_all}{total}{segments}{stored_fields_memory_in_bytes};
$to_return->{data}{tseg_tvector_size} = $stats_json->{_all}{total}{segments}{term_vectors_memory_in_bytes};
$to_return->{data}{tseg_norms_size}   = $stats_json->{_all}{total}{segments}{norms_memory_in_bytes};
$to_return->{data}{tseg_points_size}  = $stats_json->{_all}{total}{segments}{points_memory_in_bytes};
$to_return->{data}{tseg_docval_size}  = $stats_json->{_all}{total}{segments}{doc_values_memory_in_bytes};
$to_return->{data}{tseg_indwrt_size}  = $stats_json->{_all}{total}{segments}{index_writer_memory_in_bytes};
$to_return->{data}{tseg_vermap_size}  = $stats_json->{_all}{total}{segments}{version_map_memory_in_bytes};
$to_return->{data}{tseg_fbs_size}     = $stats_json->{_all}{total}{segments}{fixed_bit_set_memory_in_bytes};

$to_return->{data}{trc_size}      = $stats_json->{_all}{total}{request_cache}{memory_size_in_bytes};
$to_return->{data}{trc_evictions} = $stats_json->{_all}{total}{request_cache}{evictions};
$to_return->{data}{trc_hits}      = $stats_json->{_all}{total}{request_cache}{hit_count};
$to_return->{data}{trc_misses}    = $stats_json->{_all}{total}{request_cache}{miss_count};

$to_return->{data}{tst_size}     = $stats_json->{_all}{total}{store}{size_in_bytes};
$to_return->{data}{tst_res_size} = $stats_json->{_all}{total}{store}{reserved_in_bytes};

my $raw_json = $json->encode($to_return);
if ( !$opts{P} ) {
	$raw_json = $raw_json . "\n";
}

if ( !$opts{q} ) {
	print $raw_json;
}

if ( !$opts{w} ) {
	exit 0;
}

write_file( $output_base, { atomic => 1 }, $raw_json );

my $compressed_string;
gzip \$raw_json => \$compressed_string;
my $compressed = encode_base64($compressed_string);
$compressed =~ s/\n//g;
$compressed = $compressed . "\n";

write_file( $output_base . '.snmp', { atomic => 1 }, $compressed );

exit 0;
