#!/usr/bin/perl -w

use warnings;
use strict;
use English;
use 5.006;

# Constants
my $DEBUG      = 0;
my $EMPTY      = q{};
my $BLANK      = q{ };
my $PERF_SEP   = q{ |};

# Nagios Exit Codes
my $OK      = 0;
my $WARN    = 1;
my $CRIT    = 2;
my $UNKNOWN = 3;


=pod

=head1 NAME 

Multi label wrapper (wrap_multi.pl) - standard perfoutput to multi-label-format

=cut

use vars qw( $VERSION );
$VERSION = '1.0.1';

=pod 

=head1 VERSION

Version 1.0.1 (stable)

=cut


sub prt_usage {
    my $err = shift;
    if (defined $err) { print {*STDERR} "$err" . "\n" };
    print {*STDERR} 'usage: ';
    print {*STDERR} 'check_multi.pl <nagios_check_script> <args> <label> [<prefix>]' 
                    . "\n";
    print {*STDERR} q{Example: check_multi.pl check_disk '-H server1 -t 30' free} 
                    . "\n";
    print {*STDERR} q{See the pod in this script for more details. }
                    . q{(or 'man wrap_multi.pl')} . "\n";
    exit $UNKNOWN;
}


my $plugin = $ARGV[0];
my $args   = $ARGV[1];
my $label  = $ARGV[2];
my $pfx    = $ARGV[3] || $EMPTY;
# debug(2, '$plugin: ' . $plugin);
# debug(2, '$args  : ' . $args);

prt_usage() if (not defined $plugin);
prt_usage() if ($plugin eq $EMPTY or $plugin eq '--help');
prt_usage('Needs at least three arguments') if (not defined $plugin 
                                                or not defined $label
                                                or not defined $args);
prt_usage('Too many arguments') if @ARGV > 4;

# Check if plugin exists and is executable
if (not -x $plugin) {
    die 'Can not find or execute this file: ' . "$plugin" . "\n";
}

=pod

=head1 FILENAME- and PATH of the WRAPPED CHECK-SCRIPT

In order to keep the script simple and independent, only usual file- and 
directory-names are supported. 

This script probably will not work with delimiters other than forward-slashes.

=cut

my ($check_path, $check_name);
# Getting the check_name
if ($plugin =~ m{
                    \/  # slash ==> plugin-path has directories in front
                }x) {
    my $check_file_name;
    if ($plugin =~ /^(.*)\/(.+)$/) {
        $check_path  = $1;
        $check_file_name  = $2; # with extension (check_stuff.pl)
    } else {
        die 'Can not get the check_path and check_file_name';
    }
    if ( $check_file_name =~ /([^.]+)\.+(.*)$/ ) {
        $check_name = $1;   # without extension (check_stuff)
        #$check_ext = $2;   # extension         (pl)
    } elsif ( $check_file_name =~ /(.+)$/ ) {
        $check_name = $1;   # check_disk
    } else {
        die 'Can not get the check_name';
    }
} elsif ($plugin =~ m{
                        ^(.+)   # name
                        \.      # literal dot
                        (.*)$   # extension
                      }x
        ){
    
    $check_path  = $EMPTY;
    $check_name  = $1;
    # $check_ext   = $2;
} else {
    die 'Invalid check_name';
}
#debug(2, 'check_path: ' . $check_path);
#debug(2, 'check_name: ' . $check_name);

# Execute the plugin
my (@out, $result); # storage for output and exit-code of plugin

@out = qx("$plugin" $args 2>/dev/null); 
$result =  ${CHILD_ERROR} >> 8; # same as $? >> 8
debug(1, '@out   : ' . "@out" );
debug(1, '$result: ' . "$result" );

if (not @out) {
    die 'wrap-multi ERROR: No output to stdout from ' . $check_name . "\n";
}

# Regex to remove surrounding white-space
my $black = qr/\s*(.*?)\s*\Z/;

# split and parse line 1
my ($msg1, $perf1) = split /\|/, shift @out;
if (not defined $msg1) {$msg1 = $EMPTY};
if (not defined $perf1) {$perf1 = $EMPTY};
$msg1  =~ s/$black/$1/m;
$perf1 =~ s/$black/$1/m;
# debug(2,  '$msg1   >' . $msg1    . q{<} . "\n"
#         . '$perf1  >' . $perf1   . q{<} . "\n"
#      );

# concat the following lines up to the perfdata 
my ($msg2, $perf2) = ($EMPTY, $EMPTY);
LINE: while (my $line = shift @out) {
    if ($line =~ /\|/) {
        my ($msg, $perf) = split /\|/, $line ;
        # remove surounding whitespace
        $msg  =~ s/$black/$1/m; 
        $perf =~ s/$black/$1/m;
        $msg2  .= $msg;
        $perf2 .= $perf;
        last LINE;
    }
    $msg2 .= $line;
    
}


# debug(2, '$msg2   >' . $msg2    . q{<} . "\n"
#        . '$perf2  >' . $perf2   . q{<} . "\n"
#     );
   
# perfdata of multi-line output
my $perf3 = $EMPTY;
while (my $line = shift @out) {
    $perf3 .= $line;
} 
# debug(2, '$perf3  >' . $perf3   . q{<} . "\n"
#     );

my $perf_elem_cnt = 0;  # counter for number of instances
my $perf_pos = 0;       # position of perf-data in output
my @perf_out;
foreach ($perf1, $perf2, $perf3) {
    $perf_pos++;
    #debug(2, '$perf_pos: ' . $perf_pos);
    foreach my $perf_elem (split /\s+/) {
        #debug(2, 'perf-element in : ' . $perf_elem);
        $perf_elem_cnt++;
        $perf_elem =~ s{
            (.+)    # instance is anything before = (copy into $1)
            =       # /var/log=819MB;80;95;0;5958
            (.*)    # data is after =               (copy into $2)
        }{${pfx}$1::${check_name}::${label}=$2}x;
        #debug(2, 'perf-element out: ' . $perf_elem);
        $perf_out[$perf_pos] .= $BLANK . "$perf_elem";   
    }
}

# debug(3, '$perf_out[0]: >' . $perf_out[0] . q{<});
# debug(3, '$perf_out[1]: >' . $perf_out[1] . q{<});
# debug(3, '$perf_out[2]: >' . $perf_out[2] . q{<});
# debug(3, '$perf_out[3]: >' . $perf_out[3] . q{<});

if (defined $msg1) {print "$msg1"};
print $PERF_SEP . $BLANK . $check_name . q{::} . q{multi_label} . q{::} 
    . q{instances=} . $perf_elem_cnt;
if (defined $perf_out[1]) { print "$perf_out[1]" }; 

if (defined $msg2 and $msg2 ne $EMPTY) {print "\n" . "$msg2"};
if (defined $perf_out[2] and $perf_out[2] ne $EMPTY) {
    print $PERF_SEP . $perf_out[2];
}
if (defined $perf_out[3] and $perf_out[3] ne $EMPTY) {
    print "\n" . $perf_out[3];
}
print "\n";
exit $result;


# ===============
# = Helper subs =
# ===============

sub debug {
    my $d  = shift;
    my $text  = shift;
    print {*STDERR} "$text" . "\n" if ($DEBUG) >= $d;
    return;
}

sub clean {
    # removes leading and trailing whitespace
    my $in = shift;
    $in =~ /\s*(.*)\s*/;
    return $1;  ## no critic (ProhibitCaptureWithoutTest)
}

__END__

=pod

=head1 SYNOPSIS

This script runs the specified Nagios-plugin, captures stdout and reformats the
performance-data to multi-label-format as specified by check_multi.

=head1 EXAMPLE

    $ wrap_multi.pl 'check_disk.pl' '-H host' free_space

This prints 

    DISK OK - free space: / 3326 MB (56%); | check_disk::multi_label::instances=4
    / 15272 MB (77%);
    /boot 68 MB (69%);
    /home 69357 MB (27%);
    /var/log 819 MB (84%); | /::check_disk::free_space=2643MB;5948;5958;0;5968
    /boot::check_disk::free_space=68MB;88;93;0;98
    /home::check_disk::free_space=69357MB;253404;253409;0;253414 
    /var/log::check_disk::free_space=818MB;970;975;0;980

instead of the following (output without wrap_multi):

    DISK OK - free space: / 3326 MB (56%); | /=2643MB;5948;5958;0;5968
    / 15272 MB (77%);/boot 68 MB (69%);
    /home 69357 MB (27%);
    /var/log 819 MB (84%); | /boot=68MB;88;93;0;98 
    /home=69357MB;253404;253409;0;253414 
    /var/log=818MB;970;975;0;980

=head1 SYNTAX

=head2 BASIC

This wrapper-script receives three parameter, the name of the check-script
to wrap, its arguments and a label:

    $ wrap_multi.pl <check_script> <arguments> <label>


=head2 ADVANCED

An optional 4th parameter prefixes the instance-names and makes them unique in 
case of having several checks, retrieving different metrics from the same
instance. (f.e. usage- and snapshot-data from the volumes)

    $ wrap_multi.pl check_disk.pl 'H host' free fs-

This prints

       DISK OK - free space: / 3326 MB (56%); | check_disk::multi_label::instances=4
       / 15272 MB (77%);
       /boot 68 MB (69%);
       /home 69357 MB (27%);
       /var/log 819 MB (84%); | fs-/::check_disk::free=2643MB;5948;5958;0;5968
       fs-/boot::check_disk::free=68MB;88;93;0;98
       fs-/home::check_disk::free=69357MB;253404;253409;0;253414 
       fs-/var/log::check_disk::free=818MB;970;975;0;980

=cut


=pod

=head1 IMPLEMENTATION

This script executes whatever is defined by the first argument. 
Stdout is captured, B<stderr is send to /dev/null>.

=head2 MULTI-LINE HEADER (INSTANCE COUNTER)

The number of perf-data elements is counted and used to construct the first
multi-label-perfelement in the following format:

    <check-name>::multi_label::<label>=<number of instances>

So the number of perf-elements is taken as the number of instances.

If there is no perf-data, the counter is zero. 

=head2 NEWLINES VS. BLANKS

The Nagios-documentation allows newlines to separate perf-data-elements. The 
Perl-Module Nagios::Plugin uses blanks. This scripts does the latter and follows
what most plugins will do: Separate the perf-elements by blanks. So some of the
above examples do not reflect 100% correct what this script will output.

Example: This input (second part of perf-data in multi-line output)

    some message | a=1 
    b=2
    c=3

will get changed to:

    some message | a::some_check::lab=1 b::some_check::lab=2 c::some_check::lab=3

=head1 FORMAT DEFINITIONS

=head2 STANDARD NAGIOS PERFDATA (INPUT)

This script can process both the Nagios 2.x service-output and the newer Nagios 3.x 
multiline service-output.

    TEXT OUTPUT | OPTIONAL PERFDATA
    LONG TEXT LINE 1
    LONG TEXT LINE 2
    ...
    LONG TEXT LINE N | PERFDATA LINE 2
    PERFDATA LINE 3
    ...
    PERFDATA LINE N

This definition has been taken from nagios.org on 13th of April 2010.
L<http://nagios.sourceforge.net/docs/3_0/pluginapi.html>

=head2 MULTI-LABEL-FORMAT (OUTPUT)

    <Instance-Name>::<Plugin>::<Label>=<Value[;warn;crit;min;max]> 

=over

=item Instance-Name 

The string left of each = in the original perf-output

=item Plugin

The file-name of the called script w/o extension (anything left of the last '.') 

=item Label

The label is to set to the command-lines second parameter. 

Example - this results in 'size_used' as label:

    $ wrap_multi.pl some_check.pl size_used

=item Value

Value, including thresholds and min/max values without any change.

=back

=head2 INSTANCE-LINE

The wrapper-script counts the number of instances and adds it as perfdata in 
front of the first perfdata. 

    <Plugin>::multi_label::instances=<no of instances>

Example - if four volumes have been checked by check_disk.pl the first line of the 
perfdata would be:

    check_disk::multi_label::instances=4

See also the example above.

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2010 Bacher Systems GmbH. All rights reserved.

Author: Ingo Lantschner, L<http://perl.lantschner.name>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see L<http://www.gnu.org/licenses/>.

=cut
