#!perl

use JSON qw(decode_json encode_json);
use strict;
use warnings;
use Getopt::Long       qw(GetOptions);
use OSLV::Monitor      ();
use MIME::Base64       qw(encode_base64);
use IO::Compress::Gzip qw(gzip);
use File::Slurp        qw(read_file write_file);
use Pod::Usage         qw(pod2usage);
use Time::HiRes        qw(gettimeofday);

=head1 NAME

oslv_monitor - LibreNMS style JSON SNMP extend for OS level virtualization monitoring

=head1 VERSION

Same as L<OSLV::Monitor>.

=cut

our $VERSION = $OSLV::Monitor::VERSION;

=head1 SYNOPSIS

oslv_monitor [B<-c>] [B<-l>] [B<-f> <config file>] [B<-b> <base dir>] [B<-q>] [B<-t> <time divider>] [B<--backend> <backend>]

oslv_monitor B<-h>|B<--help>

oslv_monitor B<-v>|B<--version>

=head1 DESCRIPTION

For cron...

    */5 * * * * /usr/local/bin/oslv_monitor -q

For snmpd...

    extend oslv_monitor /bin/cat /var/cache/oslv_monitor/snmp

Debugging info can be printed via OSLV_MONITOR_DEBUG=1.

=head1 FLAGS

=head2 -b <base dir>

The dir to write the cache and output files to.

Default :: /var/cache/oslv_monitor

=head2 --backend <backend>

The backend to use. Overrides the one set in the config file.

Default :: FreeBSD on FreeBSD, cgroups on Linux

=head2 -c

Print the output gzip compressed and base64 encoded so it
can be transmitted via SNMP with out issue.

$base_dir/snmp is always written compressed regardless of this.

=head2 -f <file>

The config file to use.

Default :: /usr/local/etc/oslv_monitor.json

=head2 -h, --help

Print the help and exit.

=head2 -l

Lock $base_dir/lock while running so overlapping runs, such as one taking
longer than the cron interval, do not clobber each other's cache. If another
run holds the lock, it exits 1 right away without writing the output files, so
the output of the previous run is kept. As the rates are computed over the
time since the previous run, a skipped run does not throw them off.

=head2 -q

Do not print the results.

=head2 -t <time divider>

What to use for the time divider. Overrides the one set in the config file.

=head2 -v, --version

Print the version and exit.

=head1 CONFIG FILE

The following keys are used in the JSON config file.

    - include :: A array of regular expressions to include.
        Default :: ["^.+$"]

    - exclude :: A array of regular expressions to exlclude.
        Default :: undef

    - backend :: Override the the backend and automatically choose it.

    - time_divider :: Override the time_divider value. The default value varies
        per backend and if it is needed.

Time divider notes.

    - cgroups :: While the default for usec to sec conversion should be 1000000,
              some settings report the value in nanoseconds, requiring 1000000000.
        Default :: 1000000

    - FreeBSD :: not used

By Defaults the backends are as below.

    FreeBSD: FreeBSD
    Linux: cgroups

Default would be like this.

    {
        "include": ["^.+$"]
    }

=cut

sub main::VERSION_MESSAGE {
	print "oslv_monitor v. $VERSION\n";
}

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

# get the commandline options
my $help     = 0;
my $version  = 0;
my $base_dir = '/var/cache/oslv_monitor';
my $quiet;
my $lock;
my $compress;
my $backend;
my $exclude;
my $include;
my $time_divider;
my $config_file = '/usr/local/etc/oslv_monitor.json';
Getopt::Long::Configure('no_ignore_case');
Getopt::Long::Configure('bundling');
GetOptions(
	't=s'     => \$time_divider,
	'version' => \$version,
	'v'       => \$version,
	'help'    => \$help,
	'h'       => \$help,
	'q'         => \$quiet,
	'c'         => \$compress,
	'l'         => \$lock,
	'f=s'       => \$config_file,
	'b=s'       => \$base_dir,
	'backend=s' => \$backend,
) || exit 1;

if ($version) {
	&main::VERSION_MESSAGE;
	exit 0;
}

if ($help) {
	&main::HELP_MESSAGE;
	exit 0;
}

if ( -f $config_file ) {
	if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
		warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': reading in config file "' . $config_file . '"' );
	}
	eval {
		my $raw_config    = read_file($config_file);
		my $parsed_config = decode_json($raw_config);
		if ( !defined($backend) && defined( $parsed_config->{backend} ) && ref( $parsed_config->{backend} ) eq '' )
		{
			$backend = $parsed_config->{backend};
		}
		if (  !defined($time_divider)
			&& defined( $parsed_config->{time_divider} )
			&& ref( $parsed_config->{time_divider} ) eq '' )
		{
			$time_divider = $parsed_config->{time_divider};
		}
		if ( defined( $parsed_config->{include} ) && ref( $parsed_config->{include} ) eq 'ARRAY' ) {
			$include = $parsed_config->{include};
		}
		if ( defined( $parsed_config->{exclude} ) && ref( $parsed_config->{exclude} ) eq 'ARRAY' ) {
			$exclude = $parsed_config->{exclude};
		}
	};
	if ($@) {
		die( 'Failed to read the config "' . $config_file . '" ... ' . $@ );
	}
} ## end if ( -f $config_file )

if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
	warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': initing module' );
}

my $monitor = OSLV::Monitor->new(
	backend      => $backend,
	base_dir     => $base_dir,
	include      => $include,
	exclude      => $exclude,
	time_divider => $time_divider,
	lock         => $lock,
);

if ( !-d $base_dir ) {
	if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
		warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': creating "' . $base_dir . '"' );
	}
	mkdir($base_dir) || die( 'Unable to mkdir "' . $base_dir . '"' );
}

if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
	warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': calling load' );
}

eval { $monitor->load; };
if ($@) {
	print encode_json( { version => 1, data => {}, error => 1, errorString => 'load failed... ' . $@ } ) . "\n";
	exit 1;
}

if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
	warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': calling run' );
}

my $data = encode_json( $monitor->run );

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

if ( !$quiet ) {
	if ($compress) {
		print $compressed;
	} else {
		print $data. "\n";
	}
}

# another run is going, so leave the output it will write alone
if ( $monitor->lock_failed ) {
	exit 1;
}

# written atomically so snmpd never reads a partially written file... a failure
# is noted and the other file still written
my $exit_code = 0;
foreach my $output ( [ 'json', $data . "\n" ], [ 'snmp', $compressed ] ) {
	my ( $file, $contents ) = @{$output};
	if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
		warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': writing out "' . $base_dir . '/' . $file . '"' );
	}
	eval { write_file( $base_dir . '/' . $file, { atomic => 1 }, $contents ); };
	if ($@) {
		warn( 'Failed to write "' . $base_dir . '/' . $file . '"... ' . $@ );
		$exit_code = 1;
	}
}

if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
	warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': done' );
}

exit $exit_code;
