#!/usr/bin/perl
# History:
#   Revision 1.0.1
#     Enhanced by Minh Van Le.
#
#   Version 1.0
#     Originally created by Tytus Kurek (tkurek7@gmail.com)
#     Web: tkurek.blogspot.com
#
#
# Revision History:
# 2016-08-14    Minh Van Le         1.0.1:
#                                     * Changed a whole bunch of things
# 2012-02-27    Tytus Kurek         1.0: Created

##################################################################################
##################################################################################
#######################  Made by Tytus Kurek on August 2012  #####################
##################################################################################
##################################################################################
###  This script automatically removes Nagios acknowledgements that have been  ###
###   not fixed since last script execution. It is mainly destined to run on   ###
###   some daily bases. Commented out section is capable of sending out the    ###
###           report into external recipients as an e-mail message.            ###
##################################################################################
##################################################################################
# https://exchange.nagios.org/directory/Addons/Comments-and-Acknowledgements/Remove-acknowledgements-older-than-n-hours--2F-days--2F-weeks-and-send-a-report/details

use feature "switch";
use feature say;

use strict;

use Socket;
use Net::SMTP;
use Time::Local;
use Time::HiRes qw(time);
use POSIX qw(strftime);
use Getopt::Long qw(:config no_ignore_case);
use Sys::Hostname;
use Cwd 'abs_path';

# Global variables
use vars qw($report @rep @acksToBeRemoved @hosts @services @definitions @acknowledgements);

use vars qw(
    $NAME $VERSION $AUTHOR $CONTACT $E_OK $E_WARNING $E_CRITICAL
    $E_UNKNOWN $USAGE $HELP $LICENSE $DEBUG $VERBOSE $TEST
    %opt @argv
);

# Exit codes
$E_OK       = 0;
$E_WARNING  = 1;
$E_CRITICAL = 2;
$E_UNKNOWN  = 3;

# Version and similar info
$NAME    = 'nagiosAck';
$VERSION = '1.0.1';
$AUTHOR  = 'Minh Van Le';
$CONTACT = 'mvanle@sourceforge.net';

# Usage text
$USAGE = <<"END_USAGE";
Usage: $NAME <COMMAND>|<-?> [OPTION]...
END_USAGE

# Help text
$HELP = <<'END_HELP';

COMMAND
    -a, --add             Add acknowledgement
    -r, --remove          Remove acknowledgement(s)
    -i, --info            Get acknowledgement details
    -?, --help            Display this help text
    -V, --version         Display version info

OPTIONS
  Searching:
    --older-than          Older than @tag
    --since-last-remove   Since last remove time
    --use-ack-comment     Use attributes from acknowledgement comment
    --no-end-time         Acks with no end time
    --type                Regex of definition type
    --host                Regex of host
    --service             Regex of service
    --author              Regex of author
    --comment             Regex of comment
    --commentid           Regex of comment id

  Other:
    --test                Do nothing except display actions that would be performed
    --email-report        Send an email report
    --logfile             Log to file
    -v, --verbose         Verbose output
    -d, --debug           Debug output

IMPORTANT
    Ensure that "retention.dat" is up to date before running this program (ie. 
    reload Nagios).

TAGS
    The following tags can be specified in an acknowledgement comment or with 
    the --older-than option (don't forget to persist the comment):

        @period:[nd] [nh] [nm] [ns]
        @until:yyyy-mm-dd hh:mm[:ss]

EXAMPLES
  Removing: 
    Remove all acknowledgements (use with caution, or add --test):

        # nagiosAck.pl -r

    Remove any acknowledgement older than 8 days 7 hours 6 minutes and 5 seconds 
    (ie. 716765 seconds):

        # nagiosAck.pl -r --older-than="@period:8d 7h 6m 5s"

    Remove any acknowledgement older than Thu Jan 12 21:15 2016:

        # nagiosAck.pl -r --older-than="@until:2016-01-21 21:15"

    Remove acknowledgements based on the @tag in its acknowledgement comment:

        # nagiosAck.pl -r --use-ack-comment

    Remove useless acknowledgements from all 'nagios.org' subdomains older than 
    31 days:

        # nagiosAck.pl -r --host=".*\.nagios\.org" --comment="test" \
            --older-than="31d"

  Adding:
    Add a host acknowledgement:

        # nagiosAck.pl -a --host="old.nagios.com" --author="admin" \
            --comment="@until:2016-01-015 09:00"

    Add multiple service acknowledgements for multiple hosts:

        # nagiosAck.pl -a --host="exchange.nagios.com,assets.nagios.com" \
            --service="ping,ssh" --author="admin" --comment="@period:15m"

  Searching:
    Find all host acknowledgements from host 'webserver':

        # nagiosAck.pl -i --type="host" --host="webserver"

    Find all service acknowledgements with 'test' in the comment:

        # nagiosAck.pl -i --type="service" --comment="test"
    
    Find all acknowledgements created by user1:

        # nagiosAck.pl -i --author="user1"

  Reporting:
    Remove host acknowledgements matching "test*123" and send a report to a 
    list of email recipients:

        # nagiosAck.pl -r --host="test.*\d+" \
            --email-report="root@localhost,admin@website.com"

  Logging:
    Log remove-all actions to a file:

        # nagiosAck.pl -r --logfile="/tmp/acks.log"

For more information and advanced options, see the source code. Also refer to 
the "External Commands" section of the Nagios server documentation.
END_HELP

# Version text
$LICENSE = <<"END_LICENSE";
$NAME $VERSION
Copyright (C) 2016 $AUTHOR
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by $AUTHOR <$CONTACT>
END_LICENSE

if (@ARGV < 1) {
    print $USAGE;
    exit -1;
} else {
    #/* Make a copy of ARGV before Getopt::Long::GetOptions() messes with it */
    @argv = @ARGV;
}

# Options with default values
%opt
  = (
     'remove'           => 0,
     'test'             => 0,
     'debug'            => 0
    );

# Get options
GetOptions(
    'a|add'             => \$opt{add},
    'r|remove'          => \$opt{remove},
    'i|info'            => \$opt{info},
    'older-than=s'      => \$opt{olderthan},
    'since-last-remove' => \$opt{sincelastremove},
    'use-ack-comment'   => \$opt{useackcomment},
    'no-end-time'       => \$opt{noendtime},
    'test'              => \$opt{test},
    'type=s'            => \$opt{type},
    'host=s'            => \$opt{host},
    'service=s'         => \$opt{service},
    'author=s'          => \$opt{author},
    'comment=s'         => \$opt{comment},
    'commentid=s'       => \$opt{commentid},
    'email-report:s'    => \$opt{emailreport},
    'help|?'            => \$opt{help},
    'v|verbose'         => \$opt{verbose},
    'V|version'         => \$opt{version},
    'd|debug'           => \$opt{debug},
    'logfile=s'         => \$opt{logfile}
) or do { print $USAGE; exit $E_UNKNOWN; };

# If user requested help
if ($opt{'help'}) {
    print $USAGE, $HELP;
    exit $E_OK;
}

# If user requested version info
if ($opt{'version'}) {
    print $LICENSE;
    exit $E_OK;
}

if ($opt{'debug'}) {
    $DEBUG = 1;
} else {
    $DEBUG = 0;
}

if ($opt{'verbose'}) {
    $VERBOSE = 1
} else {
    $VERBOSE = 0
}

if ($opt{'test'}) {
    $TEST = 1
} else {
    $TEST = 0
}

################################################################################
# Configuration
################################################################################

# Number of lines from the beginning of a 'host' definition to its acknowledgement in the retention file. Leave Undefined to dynamically determine its value.
my $hostAcknowledgementThreshold = undef;                      
#my $hostAcknowledgementThreshold = 39;

# Number of lines from the beginning of a 'service' definition to its acknowledgement in the retention file. Leave Undefined to dynamically determine its value.
my $serviceAcknowledgementThreshold = undef;
#my $serviceAcknowledgementThreshold = 45;

#
# Platform specific settings
#

my $commandFile = '/usr/local/nagios/var/rw/nagios.cmd';    # Path to nagios.cmd pipe
my $nagiosScript = '/etc/init.d/nagios';                    # Path to Nagios initd script
my $reportFile = '/usr/local/nagios/var/ACKs';              # Path to the file where the addon will store its temporary data
my $reportSource = '/usr/local/nagios/var/retention.dat';   # Path to retention.dat file
my $mailFrom = 'nagios@myhost.local';                       # E-mail address of Nagios server 
my @mailTo = ('m.le@myhost.local', 'admin@web.local');      # Default email address recipients
my $nagiosServer = 'nagiosxi.myhost.local';                 # Nagios server hostname
my $smtpServer = 'smtp.myhost.local';                       # SMTP server hostname

################################################################################
# Main
################################################################################

my $scriptLocation = getScriptLocation();
my $commandLineArgs = join(" ", @argv);
($DEBUG) && writeLog("script start");
writeLog("\@ARGV: " . $commandLineArgs);

#/* Build internal dictionary and arrays */
createReport ();

#/*
# * Process selected options
# */

if ($opt{'info'}) {
    my @acks;

    @acks = searchAcks(@acknowledgements);
    push @rep, "searching acks with args: $commandLineArgs", "", "results:", "";
    displayAcks(@acks);
    my $str = (($TEST) ? "[test] " : "") . "found " . scalar @acks . " acknowledgements";
    say $str;
}

if ($opt{'add'}) {
    push @rep, "adding acks with args: $commandLineArgs", "", "results:", "";
    addAcknowledgements();
    my $str = (($TEST) ? "[test] " : "") . "added acknowledgement";
    say $str;
}

if ($opt{'remove'}) {
    push @rep, "removing acks with args: $commandLineArgs", "", "results:", "";
    compareReports ();
    removeAcknowledgements ();
    my $str = (($TEST) ? "[test] " : "") . "removed " . scalar @acknowledgements . " acknowledgements";
    say $str;
}

if (defined $opt{'emailreport'}) {
    my @recipients = split(",", $opt{'emailreport'});
    if (@recipients > 0) {
        @mailTo = @recipients;
    }

    #/* Add some source details to the report */
    unshift @rep, 
        "date: " . strftime("%A %Y-%m-%d %H:%M:%S %Z", localtime(time)), 
        "source: " . $scriptLocation, ""
    ;

    if (not $TEST) {
        sendReport ();
        ($VERBOSE) && say "report sent.";
    }
}

##/* Display test status */
#if ($opt{'test'}) {
#    say "* Test completed.";
#} else {
#    say "Committed.";
#}

($DEBUG) && writeLog("script end");

################################################################################
# Subs
################################################################################

#/* v1.0 (legacy/enhanced) */
sub createReport () # Fills in @hosts and @services arrays based on the content of $nagiosFile file
{
    my $index = 0;
    my @file;

    #/* Open retention.dat */
    open FILE, "<", $reportSource or die $!;
    while (<FILE>)
    {
        $_ =~ s/\n//;
        push @file, $_;
    }
    close FILE;

    #/* Get the definitions */
    @definitions = getDefinitions(@file);
    ($DEBUG) && say "createReport(): \@file: ", scalar @file, ", \@definitions: ", scalar @definitions;

    #/* v1.0 (legacy) - Locate the acknowledgements */
    foreach my $line (@file)
    {
        $index++;
        
        if ($line =~ m/^host\s\{.*$/)
        {
            #/* Dynamically obtain the offset */
            if (not defined $hostAcknowledgementThreshold) { 
                $hostAcknowledgementThreshold = getDefinitionOffset(\@file, $index, "^problem_has_been_acknowledged=.*");
            }

            #/* Get the host acknowledgement */
            my $ack = $file[$index+$hostAcknowledgementThreshold];
            if ($ack =~ m/^problem_has_been_acknowledged=1.*$/)
            {
                my $host = $file[$index];
                $host =~ m/^host_name=(.*)$/;
                push @hosts, $1;
            }
        }

        elsif ($line =~ m/^service\s\{.*$/)
        {
            #/* Dynamically obtain the offset */
            if (not defined $serviceAcknowledgementThreshold) { 
                $serviceAcknowledgementThreshold = getDefinitionOffset(\@file, $index, "^problem_has_been_acknowledged=.*");
            }

            #/* Get the service acknowledgement */
            my $ack = $file[$index+$serviceAcknowledgementThreshold];
            if ($ack =~ m/^problem_has_been_acknowledged=1.*$/)
            {
                my $pair = {};
                my $host = $file[$index];
                my $service = $file[$index+1];
                $host =~ m/^host_name=(.*)$/;
                $host = $1;
                #$service =~ m/^display_name=(.*)$/;
                $service =~ m/^service_description=(.*)$/;
                $service = $1;
                $pair->{$host} = $service;
                push @services, $pair;

                ($DEBUG) && say "createReport(): pair: ($host, $service)";
            }
        }
    }

    #/*
    # * v1.0.1 - Build the @acknowledgements array
    # */

    foreach my $host (@hosts) {
        push @acknowledgements, getAck(type=>"host", host=>$host);
    }

    foreach my $service (@services) {
        foreach my $host (keys $service) {
            push @acknowledgements, getAck(type=>"service", host=>$host, service=>$service->{$host});
        }
    }

    if ($DEBUG) {
        foreach my $ack (@acknowledgements) {
            say "createReport(): pushed into \@acknowledgements: \n", 
                " type:", $ack->{type}    ,"\n", 
                " host:", $ack->{host}    ,"\n", 
                " service:", $ack->{service} ,"\n", 
                " start:", $ack->{start}   ,"\n", 
                " end:", $ack->{end}     
        }
    }

    #/* v1.0.1 - Apply search criteria */
    @acknowledgements = searchAcks(@acknowledgements);

    #/*
    # * v1.0 (legacy) - Create the report
    # * (original code from Tytus Kurek)
    # */

    my $hostsReport;
    my $servicesReport;
    my $counter = 0;

    foreach my $host (@hosts)
    {
        $hostsReport = $hostsReport . "* $host\n";
        $counter++;
    }

    for my $pair (@services)
    {
        for my $host (keys %$pair)
        {
            $servicesReport = $servicesReport . "+ $host \($pair->{$host}\)\n";
            $counter++;
        }
    }

    $report = $hostsReport . $servicesReport;
}

#/* v1.0 (legacy/enhanced) */
sub sendReport ()   # Based on the content of @hosts and @services arrays sends a report to external recipients
{
    my ($hostsReport) = @_;
    my $subject = "nagiosAck Report";
    my $hostsHeader = "Host problems that have been acknowledged:\n\n";
    my $servicesHeader = "Service problems that have been acknowledged:\n\n";

    if (scalar @acknowledgements > 0)
    {
        my $smtp = Net::SMTP->new ($smtpServer, Hello => $smtpServer, Timeout => 30);
        #my $body = $hostsHeader . $hostsReport . $servicesHeader . $servicesReport;
        #my $body = $report;
        my $body;
        my $message = 
            "From: $mailFrom\nTo: " . 
            join(";", @mailTo) . 
            "\nSubject: $subject\n$body\n" . 
            join("\n", @rep);
        $smtp->mail ($mailFrom);
        $smtp->to (@mailTo);
        $smtp->data ();
        $smtp->datasend ($message);
        $smtp->dataend ();
        $smtp->quit ();
    }
}

#/* v1.0 (legacy) */
sub compareReports ()   # Based on the content of $nagiosReport file determines content of @acksToBeRemoved array and saves new report
{
    my @file;
    my %duplicates;

    my @acks = split "\n", $report;

    if (not -f $reportFile) {
        open FILE, ">", $reportFile or die $!;
        close FILE;
    }

    open FILE, "<", $reportFile or die $!;

       while (<FILE>)
        {
               push @file, $_;
        }

    close FILE;
    
    my @acksOld = @acks;
    push @acks, @file;

    foreach my $line (@acks)
    {
        $line =~ s/\n//;
        $duplicates{$line}++;
    }

    foreach my $duplicated (keys %duplicates)
    {
        if ($duplicates{$duplicated} > 1)
        {
            push @acksToBeRemoved, $duplicated;
        }
    }

    open FILE, ">", $reportFile or die $!;
    
    foreach my $line (@acksOld)
    {
        print FILE "$line\n";
    }

    close FILE;
}

#/* v1.0.1 - Add acknowledgements */
sub addAcknowledgements {
    my @hosts    = split(',', $opt{'host'});
    my @services = split(',', $opt{'service'});
    my $author   = $opt{'author'};
    my $comment  = $opt{'comment'};
    my @acknowledgements;

    #/*
    # * Build the @acknowledgements array
    # */

    foreach my $host (@hosts) {
        my $ack;

        $ack = {
            host    => $host, 
            author  => $author,
            comment => $comment,
        };

        if (defined $opt{'service'}) {
            foreach my $service (@services) {
                my %h;
                %h = %$ack;     #// Create a copy
                $h{type} = "service";
                $h{service} = $service;
                push @acknowledgements, \%h;
            }
        } else {
            $ack->{type} = "host";
            push @acknowledgements, $ack;
        }
    }

    #/*
    # * Run command
    # */

    my $now = `date +%s`;
    $now =~ s/\n//;

    my $commandType;
    foreach my $ack (@acknowledgements) {
        given ($ack->{type}) {
            when ("host") { 
                $commandType = "ACKNOWLEDGE_HOST_PROBLEM;$ack->{host}";
            }
            when ("service") { 
                $commandType = "ACKNOWLEDGE_SVC_PROBLEM;$ack->{host};$ack->{service}";
            }
        }

        my $command = "/usr/bin/printf \"[%lu] $commandType;2;0;1;$ack->{author};$ack->{comment}\\n\" $now > $commandFile";
        executeCommand($command);
    }
}

#/* v1.0 (legacy/enhanced) */
sub removeAcknowledgements ()   # Removes acknowledgements for hosts and services in @acksToBeRemoved array
{
    my @hostAcks;
    my @serviceAcks;

    if ($opt{'sincelastremove'}) {
        foreach my $ack (@acksToBeRemoved)
        {
            if ($ack =~ m/^\*\s(.*).*$/)
            {
                push @hostAcks, $1;
            }

            else
            {
                $ack =~ m/^\+\s(.*)\s\((.*)\).*$/;
                my $host = $1;
                my $service = $2;
                my $pair->{$host} = $service;
                push @serviceAcks, $pair;
            }
        }
    }

    #/* v1.0.1 */
    foreach my $ack (@acknowledgements) {
        given ($ack->{type}) {
            when ("host")    { push @hostAcks, $ack->{host}; }
            when ("service") { push @serviceAcks, { $ack->{host}=>$ack->{service} }; }
        }
    }

    removeAck(\@hostAcks, \@serviceAcks);
}

#/* v1.0.1 - Helper method for removeAcknowledgements() */
sub removeAck {
    my ($hostAcks, $serviceAcks) = @_;
    my $command;

    my $now = `date +%s`;
    $now =~ s/\n//;
    
    foreach my $ack (@$hostAcks)
    {
        my $command = "/usr/bin/printf \"[%lu] REMOVE_HOST_ACKNOWLEDGEMENT;$ack\\n\" $now > $commandFile";
        executeCommand($command);
    }

    for my $pair (@$serviceAcks)
    {
        for my $host (keys %$pair)
        {
            my $service = $pair->{$host};
            my $command = "/usr/bin/printf \"[%lu] REMOVE_SVC_ACKNOWLEDGEMENT;$host;$service\\n\" $now > $commandFile";
            executeCommand($command);
        }
    }

    #my $command = "$nagiosScript reload";
    #`$command`;
}

#/* v1.0.1 - Convenience method for executing ack commands */
sub executeCommand {
    my ($command) = @_;
    my $action = (($TEST) ? "[test] " : "") . "executing: $command";
    push @rep, $action;
    ($VERBOSE) && say $action;
    writeLog($action);
    if (not $TEST) {
        `$command`;
    }
}

#/* v1.0.1 - Definitions dictionary. Things need to be in here before they are accessed elsewhere. */
sub getDefinitions {
    my @file = @_;
    my @definitions;
    my ($type, $host_name, $service_description, $problem_has_been_acknowledged, $service_description, $entry_type, $comment_id, $entry_time, $author, $comment_data);

    for (my $line = 0; $line < scalar @file; $line++) {
        given ($file[$line]) {
            when (/^(\w+)\s\{/)                          { $type = $1; }    #// Start of definition
            when (/^host_name=(.*)/)                     { $host_name = $1; }
            when (/^service_description=(.*)/)           { $service_description = $1; }
            when (/^problem_has_been_acknowledged=(.*)/) { $problem_has_been_acknowledged = $1; }
            when (/^service_description=(.*)/)           { $service_description = $1; }
            when (/^entry_type=(.*)/)                    { $entry_type = $1; }
            when (/^comment_id=(.*)/)                    { $comment_id = $1; }
            when (/^entry_time=(.*)/)                    { $entry_time = $1; }
            when (/^author=(.*)/)                        { $author = $1; }
            when (/^comment_data=(.*)/)                  { $comment_data = $1; }
            when (/^\}$/)                                {                  #// End of definition reached
                push @definitions, {
                    type => $type, 
                    host_name => $host_name,
                    service_description => $service_description,
                    problem_has_been_acknowledged => $problem_has_been_acknowledged,
                    service_description => $service_description,
                    entry_type => $entry_type,
                    comment_id => $comment_id,
                    entry_time => $entry_time,
                    author => $author,
                    comment_data => $comment_data
                };
            }
        }
    }

    return @definitions;
}

#/* v1.0.1 - Get an acknowledgement */
sub getAck {
    my %args = @_;
    my ($latestAckComment, 
        $ackAuthor,
        $ackComment,
        $ackCommentId,
        $ackStartTime,
        $ackEndTime, 
        $ack);
   
    given ($args{type}) {
        when (/host/)    { $latestAckComment = getLatestAckComment("hostcomment", $args{host}); }
        when (/service/) { $latestAckComment = getLatestAckComment("servicecomment", $args{host}, $args{service}); }
    }

    $ackAuthor    = $latestAckComment->{author};
    $ackComment   = $latestAckComment->{comment_data};
    $ackCommentId = $latestAckComment->{comment_id};
    $ackStartTime = $latestAckComment->{entry_time};
    $ackEndTime   = getAckEndTime($latestAckComment);

    #/* The ack href */
    $ack = {
        type      => $args{type}, 
        host      => $args{host},
        service   => $args{service},
        author    => $ackAuthor,
        comment   => $ackComment,
        commentid => $ackCommentId,
        start     => $ackStartTime,
        end       => $ackEndTime
    };

    return $ack;
}

#/* v1.0.1 - Display acknowledgements */
sub displayAcks {
    my @acks = @_;
    my $i;

    foreach my $ack (@acks) {
        $i++;
        my $strfStart = "(" . strftime("%A %Y-%m-%d %H:%M:%S %Z", localtime($ack->{start})) . ")";
        my $strfEnd = (defined $ack->{end}) ? "(" . strftime("%A %Y-%m-%d %H:%M:%S %Z", localtime($ack->{end})) . ")" : undef;

        my $line = 
              "[$i] type: "      . $ack->{type}      .  "\n" .  
              "[$i] host: "      . $ack->{host}      .  "\n" .  
              "[$i] service: "   . $ack->{service}   .  "\n" .  
              "[$i] author: "    . $ack->{author}    .  "\n" .  
              "[$i] comment: "   . $ack->{comment}   .  "\n" .  
              "[$i] commentid: " . $ack->{commentid} .  "\n" .  
              #"[$i] start: "     . $ack->{start}     .  "\n" .  
              #"[$i] end: "       . $ack->{end}       .  "\n" ;
              "[$i] start: "     . $ack->{start}     .  " $strfStart\n" .  
              "[$i] end: "       . $ack->{end}       .  " $strfEnd\n" ;
        ($VERBOSE) && say sprintf("%s", $line);
        push @rep, split("\n", $line), "";
    }
}

#/* v1.0.1 - Search acknowledgements based on specified criteria and return matches */
sub searchAcks {
    my @acks = @_;
    my @matches;
    my $pass = 1;

    foreach my $ack (@acks) {
        if (defined $opt{'type'}) {
            ($ack->{type} =~ /$opt{'type'}/) ? $pass : next;
        }

        if (defined $opt{'host'}) {
            ($ack->{host} =~ /$opt{'host'}/) ? $pass : next;
        }

        if (defined $opt{'service'}) {
            ($ack->{service} =~ /$opt{'service'}/) ? $pass : next;
        }

        if (defined $opt{'author'}) {
            ($ack->{author} =~ /$opt{'author'}/) ? $pass : next;
        }

        if (defined $opt{'comment'}) {
            ($ack->{comment} =~ /$opt{'comment'}/) ? $pass : next;
        }

        if (defined $opt{'commentid'}) {
            ($ack->{commentid} =~ /$opt{'commentid'}/) ? $pass : next;
        }

        if (defined $opt{'noendtime'}) {
            (not defined $ack->{end}) ? $pass : next;
        }

        if (defined $opt{'useackcomment'} || defined $opt{'olderthan'}) {
            my $currentTime = time;
            my $endTime;
            if (my $tag = $opt{'olderthan'}) {
                #/* Construct a partial comment */
                my $ackComment = {entry_time=>$ack->{start}, comment_data=>$tag};
                $endTime = getAckEndTime($ackComment);
            } else {
                $endTime = $ack->{end};
            }

            #/* Do the comparison */
            (defined $endTime and $currentTime > $endTime) ? $pass : next;
        }

        #/* If all passed */
        push @matches, $ack;
    }

    return @matches;
}

#/* v1.0.1 - Get the latest acknowledgement comment */
sub getLatestAckComment {
    my ($type, $host_name, $service_description) = @_;
    my (@ackComments,
        $latestAckComment);

    #/* Find matching definitions */
    foreach my $definition (@definitions) {
        if ($definition->{type} eq $type and 
                $definition->{entry_type} eq 4 and      #// type 4 is an Acknowledgement
                $definition->{host_name} eq $host_name) {

            if (($type eq "servicecomment" and $definition->{service_description} eq $service_description) or 
                    $type eq "hostcomment") {

                ($DEBUG) && say "getLatestAckComment(): about to push @ackComments, $definition";
                push @ackComments, $definition;
            }
        }
    }

    $latestAckComment = (sort {$a->{comment_id} <=> $b->{comment_id}} @ackComments)[-1];
    return $latestAckComment;
}

#/* v1.0.1 - Get the acknowledgement end time */
sub getAckEndTime {
    my ($ackComment) = @_;
    my ($endTag, $startTime, $endTime);

    $startTime = $ackComment->{entry_time};
    $endTag = $ackComment->{comment_data};

    #say "X: commentData: $ackComment->{comment_data}";
    ($DEBUG) && say "getAckEndTime(): startTime: $startTime, endTag: $endTag";

    #/*
    # * Tag format: 
    # *
    # *     Option 1: @period:[<int>d] [<int>h] [<int>m] [<int>s]
    # *     Option 2: @until:yyyy-mm-dd hh:mm[:ss]
    # */

    if ($endTag =~ /\@period:\s*(.*)/i) { 
        ($DEBUG) && say "getAckEndTime(): got \@period $1";
        foreach my $i (split('\s', $1)) {
            for ($i) {
                if    (/(\d+)d/) {$endTime += $startTime + ($1 * 86400);}
                elsif (/(\d+)h/) {$endTime += $startTime + ($1 * 3600);}
                elsif (/(\d+)m/) {$endTime += $startTime + ($1 * 60);}
                elsif (/(\d+)s/) {$endTime += $startTime + ($1);}
            }
        }
    } elsif ($endTag =~ /\@until:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?).*/i) {
    #} elsif ($endTag =~ /@until:(.*)/) {
        ($DEBUG) && say "getAckEndTime(): got \@until: $1";
        $endTime = getEpoc($1)
    } else {
        ($DEBUG) && say "getAckEndTime(): did not parse any tags !";
        $endTime = undef;
    }

    ($DEBUG) && print "getAckEndTime(): endtime: " . $endTime, "\n";
    return $endTime;
}

################################################################################
# Utility methods
################################################################################

#/* Utility to find threshold offset */
sub getDefinitionOffset {
    my ($file, $index, $pattern) = @_;
    my $offset;
    my $str;
    my $found;
    for ($offset = 1; $index+$offset < scalar @$file; $offset++) {
        $str = @$file[$index+$offset];
        if ($str =~ m/$pattern/) {
            $found = 1;
            last;
        }
    }

    return $found ? $offset : undef;
}

#/* http://stackoverflow.com/questions/95492/how-do-i-convert-a-date-time-to-epoch-time-aka-unix-time-seconds-since-1970 */
sub getEpoc { 
  my($s) = @_;
  my($year, $month, $day, $hour, $minute, $second);

  if($s =~ m{^\s*(\d{1,4})\W*0*(\d{1,2})\W*0*(\d{1,2})\W*0*
                 (\d{0,2})\W*0*(\d{0,2})\W*0*(\d{0,2})}x) {
    $year = $1;  $month = $2;   $day = $3;
    $hour = $4;  $minute = $5;  $second = $6;
    $hour |= 0;  $minute |= 0;  $second |= 0;  # defaults.
    $year = ($year<100 ? ($year<70 ? 2000+$year : 1900+$year) : $year);

    return timelocal($second,$minute,$hour,$day,$month-1,$year);  
  }
  return -1;
}

#/* Poverty logger */
sub writeLog {
    if (defined $opt{'logfile'}) {
        my @lines = @_;
        my ($logfile, $logtime);

        $logfile = $opt{'logfile'};
        if (not -f $logfile) {
            open FILE, ">", $logfile or die $!;
            close FILE;
        }

        open(fd, ">>", $logfile) or die "cannot >> $logfile: $!";

        my $t = time;
        my $millis = sprintf(".%03d", ($t-int($t))*1000);
        $logtime = strftime("%Y-%m-%d %H:%M:%S", localtime($t)) . $millis;

        foreach my $line (@lines) {
            print fd $logtime ." - ". $line, "\n";
        }
    }
}

#/* Get script location details */
sub getScriptLocation {
    my $name = undef;
    my $host = hostname();
    #my $addr = inet_ntoa(scalar(gethostbyname($name)) || 'localhost');
    my $addr = inet_ntoa(scalar(gethostbyname($name || 'localhost')));
    my $path = abs_path($0);
    my $login = getlogin || getpwuid($<);

    return "$login\@$host:$path";
}
