#!perl

=head1 NAME

sneck - a boopable LibreNMS JSON style SNMP extend for remotely running nagios style checks akin to NRPE

=head1 SYNOPSIS

sneck -u [B<-C> <cache file] [B<-f> <config file>] [B<-p>] [B<-i>] [B<-d>] [B<-q>] [B<-l>] [B<-P> <pid dir>] [B<-r>]

sneck -c [B<-C> <cache file] [B<-b>]

sneck [B<-f> <config file>] [B<-p>] [B<-i>] [B<-r>]

sneck -t [B<-f> <config file>]

=head1 DESCRIPTION

For a description of the config file format and output,
please see L<Monitoring::Sneck>.

=head1 FLAGS

=head2 -f <config file>

The config file to use.

Files ending in .yaml or .yml are read as YAML, which needs YAML::XS.
See L<Monitoring::Sneck::Config> for both formats.

Default :: /usr/local/etc/sneck.conf

=head2 -p

Pretty it in a nicely formatted format.

=head2 -C <cache file>

The cache file to use.

Default :: /var/cache/sneck.cache

A secondary cache file based on this name
is also created. By default it is
/var/cache/sneck.cache.snmp and always holds
the GZip+BASE64 compressed version.

=head2 -u

Update the cache file. Will also print the was written to it.

=head2 -c

Print the cache file. Please note that B<-p> or B<-i> won't affect
this as this flag only reads/prints the cache file.

=head2 -b

When used with B<-c>, print the LibreNMS style GZip+BASE64
compressed cache instead.

=head2 -i

Includes the config file used.

=head2 -d

Print debugging info if called with -u.

=head2 -q

Don't print the results for -u. Exit queitly.

=head2 -l

Enable locking for with -u so more than one instance can't run at a time.

The PID file is sneck.pid in the directory set by B<-P>.

=head2 -P <pid dir>

The directory for the PID file used by B<-l>. The user running sneck
must be able to write to it.

Default :: /var/run

=head2 -r

Run any restarts whose checks failed. Without this, restarts are only
reported as 'restarts disabled'. Meant to be used with B<-u> from cron.
Never use it from snmpd.

Restart state, used for min_interval and max_retries, is kept in the
cache file name with '.restarts' added, so by default
/var/cache/sneck.cache.restarts.

Use B<-l> with this to make sure two runs can't restart things at the
same time. Without B<-l>, a warning saying so is printed to stderr.

See L<Monitoring::Sneck::Config> for how restarts are configured.

=head2 -t

Test the config file. Prints any errors and warnings, then exits 0 if
there are no errors and 1 if there are or the file can't be read.
Warnings, such as a check using a undefined variable, do not affect the
exit code.

=cut

use strict;
use warnings;
use Getopt::Long       qw( GetOptions );
use File::Slurp        qw(read_file write_file);
use JSON               qw(encode_json);
use Monitoring::Sneck  ();
use Monitoring::Sneck::Config ();
use MIME::Base64       qw(encode_base64);
use IO::Compress::Gzip qw(gzip);
use Pod::Usage         qw(pod2usage);
use Proc::PID::File    ();

sub main::VERSION_MESSAGE {
	print 'sneck v. ' . $Monitoring::Sneck::VERSION . "\n";
	exit 255;
}

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

my $cache_file  = '/var/cache/sneck.cache';
my $config_file = '/usr/local/etc/sneck.conf';
my $update;
my $print_cache;
my $fallback;
my $help;
my $version;
my $pretty;
my $include;
my $compress;
my $debug;
my $quiet;
my $locking;
my $pid_dir = '/var/run';
my $test_config;
my $restart;
Getopt::Long::Configure('no_ignore_case');
Getopt::Long::Configure('bundling');
GetOptions(
	'version' => \$version,
	'v'       => \$version,
	'help'    => \$help,
	'h'       => \$help,
	'c'       => \$print_cache,
	'f=s'     => \$config_file,
	'C=s'     => \$cache_file,
	'p'       => \$pretty,
	'u'       => \$update,
	'i'       => \$include,
	'b'       => \$compress,
	'd'       => \$debug,
	'q'       => \$quiet,
	'l'       => \$locking,
	'P=s'     => \$pid_dir,
	't'       => \$test_config,
	'r'       => \$restart,
);

# print version or help if requested
if ($help) {
	main::HELP_MESSAGE;
	exit 42;
}
if ($version) {
	main::VERSION_MESSAGE;
	exit 42;
}

# test the config and exit if requested
if ($test_config) {
	my $parsed_config;
	eval { $parsed_config = Monitoring::Sneck::Config->new( { file => $config_file } ); };
	if ($@) {
		print 'error: ' . $@;
		exit 1;
	}

	foreach my $error ( $parsed_config->errors ) {
		print 'error: ' . $error->{where} . ': ' . $error->{message} . "\n";
	}
	foreach my $warning ( $parsed_config->warnings ) {
		print 'warning: ' . $warning->{where} . ': ' . $warning->{message} . "\n";
	}

	if ( !$parsed_config->is_valid ) {
		exit 1;
	}
	print 'config OK: ' . $config_file . "\n";
	exit 0;
} ## end if ($test_config)

# prints the cache and exit if requested
if ($print_cache) {
	if ($compress) {
		$cache_file = $cache_file . '.snmp';
	}

	if ( !-f $cache_file || !-r $cache_file ) {
		my $error = 'Cache file does not exist or is not readable "' . $cache_file . '"';
		my $possible_error
			= { error => 1, version => 1, errorString => $error, data => { alert => 1, alertString => $error } };
		print encode_json($possible_error) . "\n";
		exit 3;
	}
	my $cache = read_file($cache_file);

	print $cache;

	exit;
} ## end if ($print_cache)

if ( $restart && !$locking ) {
	warn("-r used without -l, so overlapping runs may restart things more than once\n");
}

my $sneck = Monitoring::Sneck->new(
	{
		config     => $config_file,
		include    => $include,
		debug      => $debug,
		restart    => $restart,
		state_file => $cache_file . '.restarts',
	}
);
# -l if asked for
if ($locking) {
	my $alive;
	eval { $alive = Proc::PID::File->running( dir => $pid_dir, name => 'sneck' ); };
	if ($@) {
		die( '-l specified and PID file check failed... ' . $@ );
		exit 255;
	}
	if ($alive) {
		die( 'Already running as ' . $alive );
		exit 255;
	}
} ## end if ($locking)
my $returned = $sneck->run;

# encode it and print it
my $json = JSON->new->utf8->canonical(1);
if ($pretty) {
	$json->pretty;
}
my $raw_json = $json->encode($returned);

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

# non-pretty does not include a new line, so add it
if ( !$pretty ) {
	$raw_json = $raw_json . "\n";
}

# don't print it if told to be quiet via -q
if ( !$quiet ) {
	print $raw_json;
}

if ($update) {
	write_file( $cache_file,           { atomic => 1 }, $raw_json );
	write_file( $cache_file . '.snmp', { atomic => 1 }, $snmp );
}
