File HariSekhonUtils.pm of Package monitoring-plugins-yum

#
#  Author: Hari Sekhon
#  Date: 2011-09-15 11:30:24 +0100 (Thu, 15 Sep 2011)
#
#  https://github.com/harisekhon/lib
#
#  License: see accompanying LICENSE file
#

#  HARI SEKHON:
#
#  Library of personal stuff I use a lot, cobbled together from bits of my own
#  scripts over the last few years and a Nagios library I started in Python years ago
#
#  I welcome feedback on this. Currently this lib isn't designed for purity but rather convenience
#  and ease of maintenance.  If you have a better way of doing anything in this library that will
#  not significantly inconvenience me then let me know!
#
#  PLEASE DO NOT CHANGE ANYTHING IN HERE!
#
#  You may use this library at your own risk. You may not change it.
#
# ============================================================================ #
#  Unit Tests
#
#  make test
#
#  This will call a bunch of Test::More unit tests from t/
#
# ============================================================================ #
#  Functional Tests
#
#  If you import this library then at the very minimum I recommend that you add
#  one or more functional tests to cover all usage scenarios for your code to
#  validate when this library is updated.
#
#  ./testcmd.exp path_to_tests/*.exptest
#
#  One of the original purposes of this library was to be able to rapidly develop Nagios plugins.
#  If you use this to ease your development of Nagios plugins I strongly recommend that you add
#  functional tests and run them whenever either this library or your plugin changes
#
#  Running make test under nagios-plugins will run all unit and functional tests
#  to make sure everything still works as expected before releasing to production. It will
#  also check for plugins that are importing this library but don't have any test files
#
#  You don't want your Nagios screen to suddenly go all red because you haven't done your QA!
#
#  If you've added some code and don't have a corresponding suite of test files
#  in the ./tests directory then they may well break when I update this library.

package HariSekhonUtils;
use warnings;
use strict;
# fixes 'Can't locate object method "tid" via package "threads" at /usr/lib64/perl5/XSLoader.pm line 94.' caused by http_proxy/https_proxy environment variables (LWP module)
# eval'ing it for perls built without thread support (like Travis CI)
use Config;
if($Config{usethreads}){
    require threads;
    import threads;
}
use 5.006_001;
use Carp;
use Cwd 'abs_path';
use Fcntl ':flock';
use File::Basename;
use Getopt::Long qw(:config bundling);
# fixes 'Can't locate object method "flush" via package "IO::Handle" at /usr/local/share/perl5/LWP/UserAgent.pm line 536.' in -D/--debug mode
use IO::Handle;
use POSIX;
use JSON 'decode_json';
use Scalar::Util 'blessed';
#use Sys::Hostname;
use Term::ReadKey;
use Time::Local;
# Workaround for IO::Socket::SSL bug not respecting disabling verifying self-signed certs
if( -f dirname(__FILE__) . "/.use_net_ssl" ){
    require Net::SSL;
    import Net::SSL;
}

our $VERSION = "1.18.6";

#BEGIN {
# May want to refactor this so reserving ISA, update: 5.8.3 onwards
#use Exporter "import";
#require Exporter;
use Exporter;
our @ISA = qw(Exporter);
# consider replacing the above with these two lines for compatibility with Perl 5.6 and then removing our from @EXPORT* below
#use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
#@ISA = qw(Exporter);
our %EXPORT_TAGS = (
    'array' =>  [   qw(
                        assert_array
                        assert_hash
                        assert_int
                        assert_float
                        compact_array
                        flattenStats
                        get_field
                        get_field_array
                        get_field_float
                        get_field_hash
                        get_field_int
                        get_field2
                        get_field2_array
                        get_field2_float
                        get_field2_hash
                        get_field2_int
                        inArray
                        sort_insensitive
                        uniq_array
                        uniq_array2
                        uniq_array_ordered
                    ) ],
    'cmd'   =>  [   qw(
                        cmd
                        pkill
                        prompt
                        isYes
                        set_sudo
                        which
                    ) ],
    'file'  => [    qw(
                        open_file
                        get_path_owner
                    ) ],
    'io'    => [    qw(
                        autoflush
                    ) ],
    'is'    => [    qw(
                        isArray
                        isAlNum
                        isAwsAccessKey
                        isAwsHostname
                        isAwsFqdn
                        isAwsSecretKey
                        isChars
                        isCollection
                        isDatabaseName
                        isDatabaseColumnName
                        isDatabaseFieldName
                        isDatabaseTableName
                        isDatabaseViewName
                        isDigit
                        isDomain
                        isDomain2
                        isDomainStrict
                        isDnsShortname
                        isEmail
                        isFilename
                        isDirname
                        isFloat
                        isFqdn
                        isHash
                        isHex
                        isHost
                        isHostname
                        isIP
                        isInt
                        isInterface
                        isKrb5Princ
                        isJavaBean
                        isJavaException
                        isJson
                        isLabel
                        isLdapDn
                        isLinux
                        isLinuxOrMac
                        isMac
                        isMinVersion
                        isNagiosUnit
                        isNoSqlKey
                        isObject
                        isOS
                        isPathQualified
                        isPort
                        isProcessName
                        isPythonTraceback
                        isRef
                        isRegex
                        isScalar
                        isScientific
                        isThreshold
                        isUrl
                        isUrlPathSuffix
                        isUser
                        isVersion
                        isVersionLax
                        isXml
                        user_exists
                    ) ],
    'lock'  =>  [   qw(
                        go_flock_yourself
                        flock_off
                    ) ],
    'log'   =>  [   qw(
                        log
                        loginit
                        logdie
                    ) ],
    'net'   =>  [   qw(
                        resolve_ip
                    ) ],
    'options' => [  qw(
                        add_options
                        add_host_options
                        add_user_options
                        get_options
                        check_regex
                        check_string
                        check_threshold
                        check_thresholds
                        env_cred
                        env_creds
                        env_var
                        env_vars
                        expand_units
                        human_units
                        isYes
                        msg_perf_thresholds
                        minimum_value
                        month2int
                        parse_file_option
                        prompt
                        plural
                        remove_timeout
                        set_port_default
                        set_threshold_defaults
                        timecomponents2days
                        usage
                        validate_ssl
                        validate_tls
                        validate_thresholds
                        version
                    ) ],
    'os'    =>  [   qw(
                        isLinux
                        isMac
                        isOS
                        linux_mac_only
                        linux_only
                        mac_only
                    ) ],
    'regex' =>  [   qw(
                        escape_regex
                        $aws_access_key_regex
                        $aws_host_component
                        $aws_hostname_regex
                        $aws_fqdn_regex
                        $aws_secret_key_regex
                        $column_regex
                        $dirname_regex
                        $domain_regex
                        $domain_regex2
                        $domain_regex_strict
                        $email_regex
                        $filename_regex
                        $fqdn_regex
                        $host_regex
                        $hostname_regex
                        $ip_prefix_regex
                        $ip_regex
                        $krb5_principal_regex
                        $label_regex
                        $ldap_dn_regex
                        $mac_regex
                        $process_name_regex
                        $rwxt_regex
                        $subnet_mask_regex
                        $tld_regex
                        $url_path_suffix_regex
                        $url_regex
                        $user_regex
                        $version_regex
                        $version_regex_lax
                    ) ],
    'status' =>  [  qw(
                        $status
                        status
                        status2
                        status3
                        critical
                        warning
                        unknown
                        is_critical
                        is_warning
                        is_unknown
                        is_ok
                        isYes
                        get_status_code
                        get_upper_threshold
                        get_upper_thresholds
                        msg_thresholds
                        try
                        catch
                        catch_quit
                        quit
                    ) ],
    'string' => [   qw(
                        lstrip
                        ltrim
                        perf_suffix
                        random_alnum
                        rstrip
                        rtrim
                        strBool
                        strip
                        trim
                        trim_float
                    ) ],
    'time'    => [  qw(
                        sec2min
                        sec2human
                        tprint
                        tstamp
                    ) ],
    'timeout' => [  qw(
                        $timeout_current_action
                        set_http_timeout
                        set_timeout
                        set_timeout_default
                        set_timeout_max
                        set_timeout_range
                    ) ],
    'validate' => [ qw(
                        skip_java_output
                        validate_alnum
                        validate_aws_access_key
                        validate_aws_bucket
                        validate_aws_secret_key
                        validate_chars
                        validate_collection
                        validate_database
                        validate_database_columnname
                        validate_database_fieldname
                        validate_database_query_select_show
                        validate_database_tablename
                        validate_database_viewname
                        validate_dir
                        validate_directory
                        validate_dirname
                        validate_domain
                        validate_domainname
                        validate_email
                        validate_file
                        validate_filename
                        validate_float
                        validate_fqdn
                        validate_host_port_user_password
                        validate_host
                        validate_hosts
                        validate_hostname
                        validate_hostport
                        validate_int
                        validate_integer
                        validate_interface
                        validate_ip
                        validate_java_bean
                        validate_krb5_princ
                        validate_krb5_realm
                        validate_label
                        validate_ldap_dn
                        validate_metrics
                        validate_node_list
                        validate_nodeport_list
                        validate_nosql_key
                        validate_password
                        validate_port
                        validate_process_name
                        validate_program_path
                        validate_regex
                        validate_resolvable
                        validate_ssl
                        validate_tls
                        validate_thresholds
                        validate_units
                        validate_url
                        validate_url_path_suffix
                        validate_user
                        validate_user_exists
                        validate_username
                    ) ],
    'vars' =>   [   qw(
                        $critical
                        $debug
                        $default_warning
                        $default_critical
                        $email
                        $expected_version
                        $host
                        $github_repo
                        $json
                        $msg
                        $msg_err
                        $msg_threshold
                        $multiline
                        $nagios_plugins_support_msg
                        $nagios_plugins_support_msg_api
                        $nodes
                        $password
                        $plural
                        $port
                        $progname
                        $status
                        $status_prefix
                        $sudo
                        $ssl
                        $ssl_ca_path
                        $tls
                        $ssl_noverify
                        $timeout
                        $timeout_current_action
                        $timeout_default
                        $timeout_max
                        $timeout_min
                        $usage_line
                        $user
                        $verbose
                        $version
                        $warning
                        %ERRORS
                        %emailoptions
                        %expected_version_option
                        %hostoptions
                        %multilineoption
                        %nodeoptions
                        %options
                        %ssloptions
                        %thresholdoptions
                        %thresholds
                        %tlsoptions
                        %useroption
                        %useroptions
                        @usage_order
                    ) ],
    'verbose' => [  qw(
                        code_error
                        debug
                        hr
                        tprint
                        tstamp
                        verbose_mode
                        vlog
                        vlog2
                        vlog3
                        vlogt
                        vlog2t
                        vlog3t
                        vlog_option
                        vlog_option_bool
                    ) ],
    'web'   =>  [   qw(
                        curl
                        curl_json
                        wget
                    ) ],
);
# same as below
#Exporter::export_tags('foo');
#Exporter::export_ok_tags('bar');
# TODO: move all of this from EXPORT to EXPORT_OK while validating all dependent code still works
our @EXPORT =   (
                    @{$EXPORT_TAGS{'array'}},
                    @{$EXPORT_TAGS{'cmd'}},
                    @{$EXPORT_TAGS{'io'}},
                    @{$EXPORT_TAGS{'is'}},
                    @{$EXPORT_TAGS{'file'}},
                    @{$EXPORT_TAGS{'lock'}},
                    @{$EXPORT_TAGS{'net'}},
                    @{$EXPORT_TAGS{'options'}},
                    @{$EXPORT_TAGS{'os'}},
                    @{$EXPORT_TAGS{'status'}},
                    @{$EXPORT_TAGS{'string'}},
                    @{$EXPORT_TAGS{'timeout'}},
                    @{$EXPORT_TAGS{'validate'}},
                    @{$EXPORT_TAGS{'vars'}},
                    @{$EXPORT_TAGS{'verbose'}},
                    @{$EXPORT_TAGS{'web'}},
                );
our @EXPORT_OK = (  @EXPORT,
                    @{$EXPORT_TAGS{'log'}},
                    @{$EXPORT_TAGS{'regex'}},
                    @{$EXPORT_TAGS{'time'}},
                 );
# could also do this:
#{ my %seen; push @{$EXPORT_TAGS{'all'}}, grep {!$seen{$_}++} @{$EXPORT_TAGS{$_}} foreach keys %EXPORT_TAGS; }
$EXPORT_TAGS{'all'}         = [ @EXPORT_OK  ];
$EXPORT_TAGS{'most'}        = [ @EXPORT     ];
$EXPORT_TAGS{'EXPORT_OK'}   = [ @EXPORT_OK  ];
$EXPORT_TAGS{'EXPORT'}      = [ @EXPORT     ];

our $status_prefix = "";

our %ERRORS;

BEGIN {
    # needs to be before die_sub(), otherwise could get 'Use of uninitialized value $HariSekhonUtils::ERRORS{"CRITICAL"} in exit' and exit with blank / 0 incorrect error code on early stage failures such as 'This Perl not built to support threads'
    #
    # Std Nagios Exit Codes. Not using weak nagios utils.pm. Also improves portability to not rely on it being present
    %ERRORS = (
        "OK"        => 0,
        "WARNING"   => 1,
        "CRITICAL"  => 2,
        "UNKNOWN"   => 3,
        "DEPENDENT" => 4
    );

    delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
    $ENV{'PATH'} = '/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin';

    # If we're a Nagios plugin check_* then make stderr go to stdout
    if(substr(basename($0), 0, 6) eq "check_"){
        open STDERR, ">&STDOUT";
        select(STDERR);
        $| = 1; 
        select(STDOUT);
        $| = 1; 
    }

    sub die_sub {
        # this is auto-translated in to equivalent system error string, we're not interested in system interpretation
        # so explicitly cast back to int so we can compare with std error codes
        # XXX: $? can't be trusted because die calls leave this as zero, especially bad from Perl modules, which then prefixes "OK:" and returns zero exit code!!! Therefore no longer unifying quit() to use die, since this dual behaviour cannot be determined inside this sub. Now only call die for real errors, if UNKNOWN is set for code_error then leave UNKNOWN, otherwise force CRITICAL
        my $exit_code = ( defined($?) and $? == $ERRORS{"UNKNOWN"} ? $ERRORS{"UNKNOWN"} : $ERRORS{"CRITICAL"} );
        #$exit_code = (defined($exit_code) and $exit_code ne "" ? int($exit_code) : $ERRORS{"CRITICAL"});
        my $str   = "@_" || "Died";
        # better to add the status prefix in here instead of in quit calls
        #my $status_prefixes = join("|", keys %ERRORS);
        #$str =~ s/:\s+(?:$status_prefixes):/:/g;
        if(substr(basename($0), 0, 6) eq "check_"){
            my $prefix = "";
            foreach(keys %ERRORS){
                if($exit_code == $ERRORS{$_}){
                    $prefix = $_;
                    last;
                }
            }
            $prefix = "CRITICAL" unless $prefix;
            $status_prefix = "" unless $status_prefix;
            $str = "${status_prefix}${prefix}: $str";
        }
        # mimic original die behaviour by only showing code line when there is no newline at end of string
        if(substr($str, -1, 1) eq "\n"){
            print STDERR $str;
        } else {
            carp $str;
        }
        if(grep(/^$exit_code$/, values %ERRORS)){
            exit $exit_code;
        }
        exit $ERRORS{"CRITICAL"};
    };
    if(substr(basename($0), 0, 6) eq "check_"){
        $SIG{__DIE__} = \&die_sub;
    }

    # This is because the die handler causes program exit instead of return from eval {} block required for exception handling
    sub try(&) {
        my $old_die = $SIG{__DIE__};
        if(defined($SIG{__DIE__})){
            undef $SIG{__DIE__};
        }
        eval {$_[0]->()};
        #$SIG{__DIE__} = \&die_sub;
        $SIG{__DIE__} = $old_die;
    }

    sub catch(&) {
        $_[0]->($@) if $@;
    }
}

# quick prototype to allow me to use this just below
sub quit(@);

our $progname = basename $0;
$progname =~ /^([\w\.\/_-]+)$/ or quit("UNKNOWN", "Invalid program name - does not adhere to strict regex validation, you should name the program simply and sanely");
$progname = $1;

our $nagios_plugins_support_msg = "Please try latest version from https://github.com/harisekhon/nagios-plugins, re-run on command line with -vvv and if problem persists paste full output from -vvv mode in to a ticket requesting a fix/update at https://github.com/harisekhon/nagios-plugins/issues/new";
our $nagios_plugins_support_msg_api = "API may have changed. $nagios_plugins_support_msg";

# ============================================================================ #

our $critical;
our $debug = 0;
our $email;
our $expected_version;
our $help;
our $host;
our $github_repo;
our $json;
our $msg = "";
our $msg_err = "";
our $msg_threshold = "";
our $multiline;
our $nodes;
my  @options;
our %options;
our $password;
our $port;
my  $selflock;
our $status = "UNKNOWN";
our $sudo = "";
our $syslog_initialized = 0;
our $ssl;
our $ssl_ca_path;
our $ssl_noverify;
our $tls;
our $timeout_current_action = "";
our $timeout_default = 10;
our $timeout_max     = 60;
our $timeout_min     = 1;
our $timeout         = $timeout_default;
our $usage_line      = "usage: $progname [ options ]";
our $user;
our %thresholds;
# Standard ordering of usage options for help. Exported and overridable inside plugin to customize usage()
our @usage_order  = qw/host port user users groups password database table query field regex warning critical ssl tls ssl-CA-path ssl-noverify tls-noverify multiline/;
# Not sure if I can relax the case sensitivity on these according to the Nagios Developer guidelines
my  @valid_units = qw/% s ms us B KB MB GB TB c/;
our $verbose = 0;
our $version;
our $warning;

# ============================================================================ #
# Validation Regex - maybe should qr// here but it makes the vlog option output messy
# ============================================================================ #
# tried reversing these to be in $regex_blah format and not auto exporting but this turned out to be less intuitive from the perspective of a module caller and it was convenient to just use the regex in pieces of code without having to import them specially. This also breaks some code such as check_hadoop_jobtracker.pl which uses $domain_regex
my  $domain_component   = '\b[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\b';
# validated against http://data.iana.org/TLD/tlds-alpha-by-domain.txt which lists all possible TLDs assigned by IANA
# this matches everything except the XN--\w{6,10} TLDs as of 8/10/2012
#our $tld_regex          = '\b(?:[A-Za-z]{2,4}|london|museum|travel|local|localdomain|intra)\b';
# Using the official list now to be tighter and avoid matching things like node.role in elasticsearch
# to allow the prototype to be checked
sub open_file ($;$);
sub code_error (@);

our $tld_regex = "\\b(?i:";
my $total_tld_count = 0;

sub load_tlds($){
    my $file = shift;
    my $fh = open_file($file);
    my $tld_count;
    while(<$fh>){
        chomp;
        s/#.*//;
        next if /^\s*$/;
        if(/^([A-Za-z0-9-]+)$/){
            $tld_regex .= "$1|";
            $tld_count += 1;
        } else {
            warn "TLD: '$_' from tld file '$file' not validated, skipping that TLD";
        }
    }
    # debug isn't set by this point
    #warn "$tld_count tlds loaded from tld file '$file'\n";
    $total_tld_count += $tld_count;
}
# downloaded from IANA, run 'make tld' to update
my $tld_file = dirname(__FILE__) . "/resources/tlds-alpha-by-domain.txt";
load_tlds($tld_file);
$total_tld_count > 1000 or code_error("$total_tld_count tlds loaded, expected > 1000");
my $custom_tlds = dirname(__FILE__) . "/resources/custom_tlds.txt";
if(-f $custom_tlds){
    load_tlds($custom_tlds);
}
$tld_regex =~ s/\|$//;
$tld_regex .= ")\\b";
#print "tld_regex = $tld_regex\n";
# debug isn't set by this point
#warn "$total_tld_count tlds loaded\n";
$total_tld_count < 2000 or code_error("$total_tld_count tlds loaded, expected < 2000");

# AWS regex from http://blogs.aws.amazon.com/security/blog/tag/key+rotation
our $aws_access_key_regex = '(?<![A-Z0-9])[A-Z0-9]{20}(?![A-Z0-9])';
our $aws_secret_key_regex = '(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])';
our $domain_regex       = '(?:' . $domain_component . '\.)*' . $tld_regex;
our $domain_regex2      = '(?:' . $domain_component . '\.)+' . $tld_regex;
our $domain_regex_strict = $domain_regex2;
# must permit numbers as valid host identifiers that are being used in the wild in FQDNs
our $hostname_component = '\b[A-Za-z0-9](?:[A-Za-z0-9_\-]{0,61}[a-zA-Z0-9])?\b';
our $aws_host_component = 'ip-(?:10-\d+-\d+-\d+|172-1[6-9]-\d+-\d+|172-2[0-9]-\d+-\d+|172-3[0-1]-\d+-\d+|192-168-\d+-\d+)';
our $hostname_regex     = "$hostname_component(?:\.$domain_regex)?";
our $aws_hostname_regex = "$aws_host_component(?:\.$domain_regex)?";
our $dirname_regex      = '[\/\w\s\\.,:*()=%?+-]+';
our $filename_regex     = $dirname_regex . '[^\/]';
our $rwxt_regex         = '[r-][w-][x-][r-][w-][x-][r-][w-][xt-]';
our $fqdn_regex         = $hostname_component . '\.' . $domain_regex;
our $aws_fqdn_regex     = $aws_host_component . '\.' . $domain_regex;
# SECURITY NOTE: I'm allowing single quote through as it's found in Irish email addresses. This makes the $email_regex non-safe without further validation. This regex only tests whether it's a valid email address, nothing more. DO NOT UNTAINT EMAIL or pass to cmd to SQL without further validation!!!
our $email_regex        = '\b[A-Za-z0-9](?:[A-Za-z0-9\._\%\'\+-]{0,62}[A-Za-z0-9\._\%\+-])?@' . $domain_regex . '\b';
# TODO: review this IP regex again
our $ip_prefix_regex    = '\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}';
our $ip_regex           = $ip_prefix_regex . '(?:25[0-5]|2[0-4][0-9]|[01]?[1-9][0-9]|[01]?0[1-9]|[12]00|[0-9])\b'; # now allowing 0 or 255 as the final octet due to CIDR
our $subnet_mask_regex  = '\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[1-9][0-9]|[01]?0[1-9]|[12]00|[0-9])\b';
our $mac_regex          = '\b[0-9A-F-af]{1,2}[:-](?:[0-9A-Fa-f]{1,2}[:-]){4}[0-9A-Fa-f]{1,2}\b';
our $host_regex         = "\\b(?:$hostname_regex|$ip_regex)\\b";
# I did a scan of registered running process names across several hundred linux servers of a diverse group of enterprise applications with 500 unique process names (58k individual processes) to determine that there are cases with spaces, slashes, dashes, underscores, chevrons (<defunct>), dots (script.p[ly], in.tftpd etc) to determine what this regex should be. Incidentally it appears that Linux truncates registered process names to 15 chars.
# This is not from ps -ef etc it is the actual process registered name, hence init not [init] as it appears in ps output
our $process_name_regex = '\s*[\w_\.\/\<\>-][\w\s_\.\/\<\>-]*';
our $url_path_suffix_regex = '/(?:[\w.,:\/%&?!#=*|\[\]~+-]+)?';
our $url_regex          = '\b(?i:https?://' . $host_regex . '(?::\d{1,5})?(?:' . $url_path_suffix_regex . ')?)';
our $user_regex         = '\b[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]\b';
our $column_regex       = '\b[\w\:]+\b';
our $ldap_dn_regex      = '\b\w+=[\w\s]+(?:,\w+=[\w\s]+)*\b';
our $krb5_principal_regex = "$user_regex(?:\/$hostname_regex)?(?:\@$domain_regex)?";
our $threshold_range_regex  = qr/^(\@)?(-?\d+(?:\.\d+)?)(:)(-?\d+(?:\.\d+)?)?$/;
our $threshold_simple_regex = qr/^(-?\d+(?:\.\d+)?)$/;
our $label_regex        = '\s*[\%\(\)\/\*\w-][\%\(\)\/\*\w\s-]+';
our $version_regex      = '\d(\.\d+)*';
our $version_regex_lax  = $version_regex . '-?.*';

# ============================================================================ #
#                                   Options
# ============================================================================ #
# universal options added automatically when using get_options()
our %default_options = (
    "D|debug+"     => [ \$debug,    "Debug code" ],
    "t|timeout=i"  => [ \$timeout,  "Timeout in secs (default: $timeout_default)" ],
    "v|verbose+"   => [ \$verbose,  "Verbose mode (-v, -vv, -vvv ...)" ],
    "V|version"    => [ \$version,  "Print version and exit" ],
    "h|help"       => [ \$help,     "Print description and usage options" ],
);

# These two subroutines are primarily for my other programs such as my spotify programs which have necessarily longer run times and need a good way to set this and have the %default_options auto updated for usage() to automatically stay in sync with the live options
sub set_timeout_max ($) {
    $timeout_max = shift;
    isInt($timeout_max) or code_error("must pass an integer to set_timeout_max()");
}


sub set_timeout_default ($) {
    $timeout_default = shift;
    isInt($timeout_default) or code_error("must pass an integer to set_timeout_default()");
    ($timeout_default > $timeout_max) and code_error("\$timeout_default ($timeout_default) may not be higher than \$timeout_max ($timeout_max)");
    ($timeout_default < $timeout_min) and code_error("\$timeout_default ($timeout_default) may not be lower than \$timeout_min ($timeout_min)");
    $timeout = $timeout_default;
    $default_options{"t|timeout=i"} = [ \$timeout, "Timeout in secs (default: $timeout_default)" ];
}

sub set_timeout_range($$){
    my $min = shift;
    my $max = shift;
    isInt($min) or code_error("non-integer passed to set_timeout_range for min (first arg)");
    isInt($max) or code_error("non-integer passed to set_timeout_range for max (second arg)");
    $timeout_min = $min;
    $timeout_max = $max;
}

# ============================================================================ #
# Optional options
our %hostoptions = (
    "H|host=s"      => [ \$host, "Host to connect to" ],
    "P|port=s"      => [ \$port, "Port to connect to" ],
);
our %nodeoptions = (
    "N|nodes=s"     => [ \$nodes, "Nodes to connect to" ],
    "P|port=s"      => [ \$port,  "Port to connect to if not appended to each node in the node list in the form 'host:port'"  ],
);
our %useroptions = (
    "u|user=s"      => [ \$user,     "User to connect with" ],
    "p|password=s"  => [ \$password, "Password to connect with" ],
);
our %multilineoption = (
    "m|multiline"   => [ \$multiline,  "Multiline output for easier viewing" ],
);
our %thresholdoptions = (
    "w|warning=s"   => [ \$warning,  "Warning  threshold or ran:ge (inclusive)" ],
    "c|critical=s"  => [ \$critical, "Critical threshold or ran:ge (inclusive)" ],
);
our %emailoptions = (
    "E|email=s"     => [ \$email,   "Email address" ],
);
our %expected_version_option = (
    "e|expected=s"     => [ \$expected_version,     "Expected version regex, raises CRITICAL if not matching, optional" ]
);
our %ssloptions = (
    "S|ssl"            => [ \$ssl,          "Use SSL connection" ],
    "ssl-CA-path=s"    => [ \$ssl_ca_path,  "Path to CA certificate directory for validating SSL certificate (automatically enables --ssl)" ],
    "ssl-noverify"     => [ \$ssl_noverify, "Do not verify SSL certificate (automatically enables --ssl)" ],
);
our %tlsoptions = (
    "T|tls"            => [ \$tls,          "Use TLS connection" ],
    "ssl-CA-path=s"    => [ \$ssl_ca_path,  "Path to CA certificate directory for validating SSL certificate (automatically enables --tls)" ],
    "tls-noverify"     => [ \$ssl_noverify, "Do not verify SSL certificate (automatically enables --tls)" ],
);
my $short_options_len = 0;
my $long_options_len  = 0;


#sub add_host_options($){
#    my $name = shift;
#    defined($name) or code_error("no name arg passed to add_host_options()");
#    if(length($name) >= 4){
#        $name = join " ", map {ucfirst} split " ", lc $name;
#    }
#    foreach(keys %hostoptions){
#        $hostoptions{$_}[1] =~ s/^(.)/$name \L$1/;
#    }
#    %options = ( %options, %hostoptions );
#}

#sub add_user_options($){
#    my $name = shift;
#    defined($name) or code_error("no name arg passed to add_user_options()");
#    if(length($name) >= 4){
#        $name = join " ", map {ucfirst} split " ", lc $name;
#    }
#    foreach(keys %useroptions){
#        $useroptions{$_}[1] =~ s/^(.)/$name \L$1/;
#    }
#    %options = ( %options, %useroptions );
#}

my $default_port;
sub set_port_default($;$){
    #defined($default_port) and code_error("default port cannot be set twice");
    # already defined, first one wins
    defined($default_port) and not defined($_[1]) and return;
    $default_port = shift;
    isPort($default_port) or code_error("invalid port passed as first arg to set_port_default");
    $port = $default_port;
    $hostoptions{"P|port=s"}[1] =~ s/\)$/, default: $default_port\)/;
    return $port;
}

sub set_threshold_defaults($$){
    our $default_warning  = shift;
    our $default_critical = shift;
    isThreshold($default_warning)  or code_error("invalid warning threshold passed as first arg to set_threshold_defaults()");
    isThreshold($default_critical) or code_error("invalid critical threshold passed as second arg to set_threshold_defaults()");
    $warning  = $default_warning;
    $critical = $default_critical;
    $thresholdoptions{"w|warning=s"}[1]  =~ s/\)$/, default: $default_warning\)/;
    $thresholdoptions{"c|critical=s"}[1] =~ s/\)$/, default: $default_critical\)/;
}

# ============================================================================ #
# Environment Host/Port and User/Password Credentials

my @host_envs;
my @port_envs;
my @user_envs;
my @password_envs;

my $port_env_found = 0;

sub env_cred($){
    my $name = shift;
    $name = uc $name;
    $name =~ s/[^A-Za-z0-9]/_/g;
    $name .= "_" if $name;
    push(@host_envs,     "\$${name}HOST");
    push(@port_envs,     "\$${name}PORT");
    push(@user_envs,     "\$${name}USERNAME");
    push(@user_envs,     "\$${name}USER");
    push(@password_envs, "\$${name}PASSWORD");
    # Can't vlog here since verbose mode and debug mode aren't set until after option processing
    if($ENV{"${name}HOST"} and not $host){
        #vlog2("reading host from \$${name}HOST environment variable");
        $host = $ENV{"${name}HOST"};
    }
    if($ENV{"${name}PORT"} and not $port_env_found){
        #vlog2("reading port from \$${name}PORT environment variable");
        $port = $ENV{"${name}PORT"};
        $port_env_found++;
    }
    if($ENV{"${name}USERNAME"} and not $user){
        #vlog2("reading user from \$${name}USERNAME environment variable");
        $user = $ENV{"${name}USERNAME"};
    } elsif($ENV{"${name}USER"} and not $user){
        #vlog2("reading user from \$${name}USER environment variable");
        $user = $ENV{"${name}USER"};
    }
    if($ENV{"${name}PASSWORD"} and not $password){
        #vlog2("reading password from \$${name}PASSWORD environment variable");
        $password = $ENV{"${name}PASSWORD"};
    }
    return 1;
}

sub env_creds($;$){
    my $name     = shift;
    my $longname = shift;
    ( defined($name) and $name ) or code_error("no name arg passed to env_creds()");
    unless($longname){
        unless(isScalar(\$name)){
            code_error("must supply longname second arg to env_creds() if first arg for ENV is not a scalar");
        }
        if($name ne uc $name){
            $longname = $name;
        } elsif(length($name) < 5){
            $longname = $name;
        } else {
            $longname = join " ", map {ucfirst} split " ", lc $name;
        }
    }

    if(isScalar(\$name)){
        env_cred($name);
    } elsif(isArray($name)){
        foreach (@{$name}){
            env_cred($_);
        }
    } else {
        code_error("non-scalar/non-array ref passed as first arg to env_creds()");
    }

    env_cred("");
#    if($ENV{"HOST"}){
#        $host = $ENV{"HOST"} unless $host;
#    }
#    if($ENV{"PORT"}){
#        $port = $ENV{"PORT"} unless $port;
#    }
#    if($ENV{"USERNAME"}){
#        $user = $ENV{"USERNAME"} unless $user;
#    } elsif($ENV{"USER"}){
#        $user = $ENV{"USER"} unless $user;
#    }
#    if($ENV{"PASSWORD"}){
#        $password = $ENV{"PASSWORD"} unless $password;
#    }

    $hostoptions{"H|host=s"}[1]     = "$longname host (" . join(", ", @host_envs) . ")";
    $hostoptions{"P|port=s"}[1]     = "$longname port (" . join(", ", @port_envs) . ( defined($port) ? ", default: $port)" : ")");
    #$nodeoptions{"N|node=s"}[1]     = "$longname node (" . join(", ", @host_envs) . ")";
    #$nodeoptions{"P|port=s"}[1]     = "$longname port (" . join(", ", @port_envs) . ( defined($port) ? ", default: $port)" : ")");
    $useroptions{"u|user=s"}[1]     = "$longname user (" . join(", ", @user_envs) . ")";
    $useroptions{"p|password=s"}[1] = "$longname password (" . join(", ", @password_envs) . ")";
    return 1;
}

sub env_var($$){
    my $name    = shift;
    my $var_ref = shift;
    $name = uc $name;
    $name =~ s/[^A-Za-z0-9]/_/g;
    if($ENV{$name} and not defined($$var_ref)){
        $$var_ref = $ENV{$name};
    }
    return 1;
}

sub env_vars($$){
    my $name    = shift;
    my $var_ref = shift;
    if(isScalar(\$name)){
        env_var($name, $var_ref);
    } elsif(isArray($name)){
        foreach (@{$name}){
            env_var($_, $var_ref);
        }
    } else {
        code_error("non-scalar/non-array ref passed as first arg to env_vars()");
    }
    return 1;
}

# ============================================================================ #
#                           Nagios Exit Code Functions
# ============================================================================ #

# Set status safely - escalate only

# there is no ok() since that behaviour needs to be determined by scenario

sub unknown () {
    if($status eq "OK"){
        $status = "UNKNOWN";
    }
}

sub warning () {
    if($status ne "CRITICAL"){
        $status = "WARNING";
    }
}

sub critical () {
    $status = "CRITICAL";
}

############################
sub is_ok () {
    ($status eq "OK");
}

sub is_warning () {
    ($status eq "WARNING");
}

sub is_critical () {
    ($status eq "CRITICAL");
}

sub is_unknown () {
    ($status eq "UNKNOWN");
}

sub get_status_code (;$) {
    if($_[0]){
        defined($ERRORS{$_[0]}) || code_error("invalid status '$_[0]' passed to get_status_code()");
        return $ERRORS{$_[0]};
    } else {
        defined($ERRORS{$status}) || code_error("invalid status '$status' found in \$status variable used by get_status_code()");
        return $ERRORS{$status};
    }
}

sub status () {
    my $status = get_status_code();
    vlog("status: $status");
    return $status;
}

# status2/3 not exported/used at this time
sub status2 () {
    my $status = get_status_code();
    vlog2("status: $status");
    return $status;
}

sub status3 () {
    my $status = get_status_code();
    vlog3("status: $status");
    return $status;
}

# requires that you 'use Data::Dumper' in calling program, since not all programs will need this
sub catch_quit ($) {
    my $my_errmsg = $_[0];
    catch {
        if(isObject($@) and defined($@->{"message"})){
            $my_errmsg .= ": " . ref($@) . ": " . $@->{"message"};
        } elsif($!) {
            $my_errmsg .= ": $!";
        } elsif($@ and not isObject($@)) {
            $my_errmsg .= ": $@";
        }
        chomp $my_errmsg;
        #$my_errmsg =~ s/ $filename_regex line \d+\.$//;
        quit "CRITICAL", $my_errmsg;
    };
    return 1;
}

# ============================================================================ #


#sub option_present ($) {
#    my $new_option = shift;
#    grep {
#        my @option_switches = split("|", $_);
#        my @new_option_switches = split("|", $new_option);
#    } (keys %options);
#}


# TODO: consider calling this from get_options and passing hashes we want options for straight to that sub

# TODO: fix this to use option_present
#sub add_options ($) {
#    my $options_hash = shift;
#    isHash($options_hash, 1);
#    #@default_options{keys %options} = values %options;
#    #@default_options{keys %{$_[0]}} = values %{$options_hash};
#    foreach my $option (keys %{$options_hash}){
#        unless(option_present($option)){
#            print "want to add $option\n";
#            #$default_options{$option} = ${$options_hash{$option}};
#        }
#    }
#
##    #my (%optionshash) = @_;
##    # by ref is faster
##    my $hashref = shift;
##    unless(isHash($hashref)){
##        #my ($package, $file, $line) = caller;
##        code_error("non hash ref passed to add_options subroutine"); # at " . $file . " line " . $line);
##    }
##    # TODO: consider replacing this with first position insertion in array in get_options for efficiency
##    foreach my $option (keys %options){
##        unless grep { grep($options keys %{$_} } @options){
##            push(@options, { $_ => $options{$_} }) 
##        };
##    }
##    #foreach(keys %hashref){
##    #    push(@options, { $_ => $hashref{$_} });
##    #}
#}


#sub update_option_description {
#    my $option = 
#}


#sub add_thresholds {
#    our $range_inversion = 0;
#    foreach(keys %thresholds){
#        $options{$_} = $thresholds{$_};
#    }
#}


# For reference only, faster to just put this directly in to code
#sub isUserNameEUID(){
#    #my $user = shift;
#    #defined($user) or code_error "no user passed to amIuser()";
#    # checking EUID against arg
#    getpwuid($>) eq shift;
#}


sub autoflush () {
    select(STDERR);
    $| = 1;
    select(STDOUT);
    $| = 1;
    return 1;
}


sub assert_array($$) {
    my $array = shift;
    my $name  = shift;
    isArray($array) or quit "UNKNOWN", "$name is not an array! $nagios_plugins_support_msg_api";
}

sub assert_float($$) {
    my $float = shift;
    my $name  = shift;
    isFloat($float) or quit "UNKNOWN", "$name is not a float! $nagios_plugins_support_msg_api";
}

sub assert_hash($$) {
    my $hash = shift;
    my $name = shift;
    isHash($hash) or quit "UNKNOWN", "$name is not a hash! $nagios_plugins_support_msg_api";
}

sub assert_int($$) {
    my $int  = shift;
    my $name = shift;
    isInt($int) or quit "UNKNOWN", "$name is not an integer! $nagios_plugins_support_msg_api";
}


sub check_regex ($$;$) {
    my $string = shift;
    my $regex  = shift;
    my $no_msg = shift;
    defined($string) or code_error("undefined string passed to check_regex()");
    defined($regex)  or code_error("undefined regex passed to check_regex()");
    if($string !~ /$regex/){
        critical;
        $msg .= " (expected regex: '$regex')" unless $no_msg;
        return;
    }
    return 1;
}


sub check_string ($$;$) {
    my $string           = shift;
    defined($string) or code_error("undefined string passed to check_string()");
    my $expected_string  = shift;
    my $no_msg           = shift;
    if(defined($expected_string) and $string ne $expected_string){
        critical;
        $msg .= " (expected: '$expected_string')" unless $no_msg;
        return;
    }
    return 1;
}


sub check_threshold ($$) {
    #subtrace(@_);
    my $threshold = shift;
    my $result    = shift;

    $threshold =~ /(?:warning|critical)$/ or code_error("invalid threshold name passed to check_threshold subroutine");
    isFloat($result, 1) or isScientific($result, 1) or code_error("Non-float passed to check_threshold subroutine");

    my $upper = defined($thresholds{$threshold}{"upper"}) ? $thresholds{$threshold}{"upper"} : undef;
    my $lower = defined($thresholds{$threshold}{"lower"}) ? $thresholds{$threshold}{"lower"} : undef;
    my $invert_range = $thresholds{$threshold}{"invert_range"} || undef;
    my $error = 0;

    if(!$invert_range){
        debug("doing straight non range-inverted $threshold threshold checks");
        debug("if result $result > $threshold upper ($upper)") if defined($upper);
        debug("if result $result < $threshold lower ($lower)") if defined($lower);
        if(defined($upper) and $result > $upper){
            $error = "$result>$upper";
        }
        elsif(defined($lower) and $result < $lower){
            $error = "$result<$lower";
        }
    } else {
        debug("doing range-inverted $threshold threshold checks");
        debug("if result $result <= $threshold upper ($upper)") if defined($upper);
        debug("if result $result >= $threshold lower ($lower)") if defined($lower);
        if(defined($upper) and defined($lower)){
            if($lower <= $result and $result <= $upper ){
            #$error = " $result not within range $lower-$upper";
            $error = "not within range $lower-$upper";
            }
        } else {
            if(defined($upper) and $result <= $upper){
                $error = "$result<=$upper";
            }
            elsif(defined($lower) and $result >= $lower){
                $error = "$result>=$lower";
            }
        }
    }
    if($error){
        $thresholds{$threshold}{"error"} = $error;
        vlog2("result outside of $threshold thresholds: $error\n");
        if($threshold =~ /warning/){
            warning;
        } else {
            critical;
        }
        # $threshold_ok false
        return 0;
    } else {
        undef $thresholds{$threshold}{"error"};
    }
    # $threshold_ok true
    return 1;
}


sub check_thresholds ($;$$) {
    #subtrace(@_);
    my $result            = shift;
    my $no_msg_thresholds = shift || 0;
    my $name              = shift() || "";
    $name .= " " if $name;
    vlog2("checking ${name}thresholds");
    defined($result) or code_error("no result passed to check_thresholds()");
    my $status_ok = check_threshold("${name}critical", $result) and
                    check_threshold("${name}warning",  $result);
    # this is switched off because it's done via msg_thresholds chaining in to return below, do not re-enable this or you'll get double printing
    #msg_thresholds() unless $no_msg_thresholds;
    return ($status_ok, msg_thresholds($no_msg_thresholds, $name));
}


#sub checksum ($;$) {
#    my $file = shift;
#    my $algo = shift;
#    $algo or $algo = "md5";
#    my $fh;
#    unless(open($fh, $file)){
#        vlog "Failed to read file '$file': $!\n";
#        return;
#    }
#    binmode($fh);
#    my $checksum;
#    if($algo eq "md5"){
#        $checksum = Digest::MD5->new;
#    } elsif ($algo eq "sha1"){
#        $checksum = Digest::Sha->new("sha1");
#    } else {
#        croak "checksum passed unsupported algorithm type '$algo'";
#    }
#    $checksum->addfile($fh);
#    return $checksum->hexdigest;
#}
#sub sha1sum {
#    return checksum($_[0], "sha1");
#}
#
#sub md5sum {
#    return checksum($_[0], "md5");
#}


sub cmd ($;$$$) {
    my $cmd     = shift;
    my $errchk  = shift;
    my $inbuilt = shift;
    my $return_exitcode = shift;
    $cmd =~ s/^\s+//;
    my $prog      = (split(/\s+/, $cmd))[0];
    if($prog eq "exec"){
        $prog = (split(/\s+/, $cmd))[1];
    }
    if($inbuilt){
        # TODO: consider adding inbuilt check, however this means two shell calls per command, very inefficient, think it's better to just catch failure
        #type($prog, 1);
    } else {
        which($prog, 1);
    }
    # this would be if we were gonna support shell built-ins
    #unless(which($prog)){
    #    type($prog) or quit("UNKNOWN", "'$prog' command was not found in \$PATH and is not a shell built-in");
    #    $prog = (split(/\s+/, $cmd))[1];
    #    which($prog, 1);
    #}
    if($cmd =~ s/\|\s*$//){
        # return reference to filehandle for more efficient processing
        vlog2("opening cmd pipe");
        vlog3("cmd: $cmd");
        open my $fh, "$cmd |";
        return $fh;
    }
    vlog3("cmd: $cmd");
    my $return_output = `$cmd 2>&1`;
    my $exitcode      = $?;
    my @output        = split("\n", $return_output);
    $exitcode         = $exitcode >> 8;
    if ($verbose >= 3) {
        #foreach(@output){ print "output: $_\n"; }
        print "output:\n\n$return_output\n";
        print "exitcode: $exitcode\n\n";
    }
    if ($errchk and $exitcode != 0) {
        my $err = "";
        if(substr($progname, 0, 6) eq "check_"){
            foreach (@output) {
                $err .= " " . trim($_);
            }
        } else {
            $err = join("\n", @output);
        }
        quit("CRITICAL", "'$cmd' returned $exitcode - $err");
    }
    if($return_exitcode){
        return ($exitcode, @output);
    } else {
        return @output;
    }
}


sub code_error (@) {
    use Carp;
    #quit("UNKNOWN", "Code Error - @_");
    $? = $! = $ERRORS{"UNKNOWN"};
    if($debug){
        confess "Code Error - @_";
    } else {
        croak "Code Error - @_";
    }
}


# Remove blanks from array
sub compact_array (@) {
    return grep { $_ !~ /^\s*$/ } @_;
}


sub curl ($;$$$$$$) {
    my $url      = shift;
    my $name     = shift;
    my $user     = shift;
    my $password = shift;
    my $err_sub  = shift;
    my $type     = shift() || 'GET';
    my $body     = shift;
    grep { $type eq $_ } qw/GET POST PUT DELETE HEAD/ or code_error "unsupported type '$type' passed to curl() as sixth argument";
    #debug("url passed to curl: $url");
    defined($url) or code_error "no URL passed to curl()";
    my $url2 = isUrl($url) or code_error "invalid URL '$url' supplied to curl()";
    $url = $url2;
    my $host = $url;
    $host =~ s/^https?:\/\///;
    $host =~ s/(?::\d+)?(?:\/.*)?$//;
    isHost($host) or die "invalid host determined from URL '$url' in curl()";
    my $auth = (defined($user) and defined($password));
    # Don't replace $host with resolved host as this changes the vlog output and also affects proxy exceptions
    validate_resolvable($host);
    if($name){
        if($type eq "POST"){
            vlog2("POSTing to $name");
        } elsif($type eq "PUT"){
            vlog2("PUTing to $name");
        } else {
            vlog2("querying $name");
        }
        vlog3("HTTP $type $url" . ( $auth ? " (basic authentication)" : "") );
    } else {
        vlog2("HTTP $type $url" . ( $auth ? " (basic authentication)" : "") );
    }
    if($type eq "POST" or $type eq "PUT"){
        vlog3($body);
    }
    #unless(defined(&main::get)){
        # inefficient, it'll import for each curl call, instead force top level author to 
        # use LWP::Simple 'get'
        #debug("importing LWP::Simple 'get'\n");
        #require LWP::Simple;
        #import LWP::Simple "get";
        #code_error "called curl() without declaring \"use LWP::Simple 'get'\"";
    #}
    #$content = main::get $url;
    #my ($result, $err) = ($?, $!);
    #vlog2("result: $result");
    #vlog2("error:  " . ( $err ? $err : "<none>" ) . "\n");
    #if($result ne 0 or $err){
    #    quit("CRITICAL", "failed to get '$url': $err");
    #}
    defined_main_ua();
    $main::ua->show_progress(1) if $debug;
    $main::ua->env_proxy;
    my $req = HTTP::Request->new($type, $url);
    # Doesn't work
    #$ua->credentials($host, '', $user, $password);
    $req->authorization_basic($user, $password) if (defined($user) and defined($password));
    $req->content($body) if $body;
    my $response = $main::ua->request($req);
    my $content  = $response->content;
    vlog3("returned HTML:\n\n" . ( $content ? $content : "<blank>" ) . "\n");
    vlog2("http status code:     " . $response->code);
    vlog2("http status message:  " . $response->message . "\n");
    if($err_sub){
        isCode($err_sub) or code_error "invalid subroutine passed to curl() as error handler";
        &$err_sub($response);
    } else {
        unless($response->code eq "200"){
            my $additional_information = "";
            my $json;
            if($json = isJson($content)){
                foreach(qw/status error message reason/){
                    if(defined($json->{$_})){
                        $_ eq "status" and $json->{$_} eq $response->code and next;
                        $additional_information .= ". " . ucfirst($_) . ": " . $json->{$_};
                    }
                }
            }
            quit("CRITICAL", $response->code . " " . $response->message . $additional_information);
        }
        unless($content){
            quit("CRITICAL", "blank content returned from '$url'");
        }
    }
    return $content;
}


sub curl_json ($;$$$$$$) {
    my $url         = shift;
    my $name        = shift;
    my $user        = shift;
    my $password    = shift;
    my $err_handler = shift;
    my $type        = shift() || 'GET';
    my $body        = shift;
    my $content     = curl $url, $name, $user, $password, $err_handler, $type, $body;
    vlog2("parsing output from " . ( $name ? $name : $url ) . "\n");
    $json = isJson($content) or quit "CRITICAL", "invalid json returned " . ( $name ? "by $name at $url" : "from $url");
}


sub debug (@) {
    return unless $debug;
    my ( $package, $filename, $line ) = caller;
    my $debug_msg = "@_";
    $debug_msg =~ s/^(\n+)//;
    #my $prefix_newline = $1 || "";
    my $sub = (caller(1))[3];
    if($sub){
        $sub .= "()";
    } else {
        $filename = basename $filename;
        $sub = "global $filename line $line";
    }
    #printf "${prefix_newline}debug: %s => %s\n", $sub, $debug_msg;
    printf "debug: %s => %s\n", $sub, $debug_msg;
}


sub defined_main_ua(){
    unless(defined($main::ua)){
        code_error "LWP useragent \$ua not defined (or inaccessibly defined with my instead of our), must import to main before calling curl(), do either \"use LWP::Simple '\$ua'\" or \"use LWP::UserAgent; our \$ua = LWP::UserAgent->new\"";
    }
}


sub escape_regex ($) {
    my $regex = shift;
    defined($regex) or code_error "no regex arg passed to escape_regex() subroutine";
    #$regex =~ s/([^\w\s\r\n])/\\$1/g;
    # backslashes everything that isn't /[A-Za-z_0-9]/
    $regex = quotemeta($regex); # $regex = \Q$regex\E;
    return $regex;
}


sub expand_units ($;$$) {
    my $num   = shift;
    my $units = shift;
    my $name  = shift;
    my $power;
    defined($num)   || code_error "no num arg 1 passed to expand_units()";
    if((!defined($units)) and $num =~ /^(\d+(?:\.\d+)?)([A-Za-z]{1,2})$/){
        $num   = $1;
        $units = $2;
    }
    defined($units) || code_error "no units arg 2 passed to expand_units()";
    isFloat($num)   || code_error "non-float num arg 1 passed to expand_units()";
    if   ($units =~ /^B?$/i) { return $num; }
    elsif($units =~ /^KB?$/i){ $power = 1; }
    elsif($units =~ /^MB?$/i){ $power = 2; }
    elsif($units =~ /^GB?$/i){ $power = 3; }
    elsif($units =~ /^TB?$/i){ $power = 4; }
    elsif($units =~ /^PB?$/i){ $power = 5; }
    else { code_error "unrecognized units '$units' " . ($name ? "for $name " : "") . "passed to expand_units(). $nagios_plugins_support_msg"; }
    return $num * (1024**$power);
}

my %stats;

# To check prototype before calling recursively
sub processStat($$);
sub processStat($$){
    my $name = shift;
    my $var  = shift;
    vlog3("processing $name");
    if(isArray($var)){
        if(scalar @{$var} > 0){
            foreach(my $i=0; $i < scalar @{$var}; $i++){
                processStat("$name.$i", $$var[$i]);
            }
        } else {
            processStat("$name.0", "");
        }
    } elsif(isHash($var)){
        if(scalar keys %{$var} and defined($$var{"value"})){
            processStat($name, $$var{"value"});
            #vlog2 "$name='$$var{value}'";
            #$stats{$name} = $$var{"value"};
        } else {
            foreach my $key (keys %{$var}){
                processStat("$name.$key", $$var{$key});
            }
        }
    } else {
        return if $name =~ /\.version$/;
        isFloat($var) or return;
        vlog2("$name='$var'");
        $stats{$name} = $var;
    }
}

sub flattenStats($){
    my $hashref = shift;
    isHash($hashref) or code_error "invalid arg passed to flattenStats, not a hashref!";
    foreach my $stat (sort keys %{$hashref}){
        processStat($stat, $hashref->{$stat});
    }
    return %stats;
}


sub get_field($;$){
    get_field2($json, $_[0], $_[1]);
}

sub get_field_array($;$){
    get_field2_array($json, $_[0], $_[1]);
}

sub get_field_float($;$){
    get_field2_float($json, $_[0], $_[1]);
}

sub get_field_hash($;$){
    get_field2_hash($json, $_[0], $_[1]);
}

sub get_field_int($;$){
    get_field2_int($json, $_[0], $_[1]);
}

sub get_field2($$;$){
    my $hash_ref  = shift;
    my $field     = shift || code_error "field not passed to get_field2()";
    my $noquit    = shift;
    isHash($hash_ref) or code_error "non-hash ref passed to get_field2()";
    # negative lookbehind allows for escaping dot in the field name
    my @parts     = split(/(?<!\\)\./, $field);
    $field =~ s/\\\././g;
    if(scalar(@parts) > 1){
        my $ref = $hash_ref;
        foreach(@parts){
            s/\\\././g;
            # XXX: this returns field not found where field exists but value is 'undef'
            if(isHash($ref) and defined($ref->{$_})){
                $ref = $ref->{$_};
            } elsif(isArray($ref) and $_ =~ /^(\d+)$/){
                if(defined(${$ref}[$1])){
                    $ref = ${$ref}[$1];
                } else {
                    quit "UNKNOWN", "array has no $1 item for field '$field'. $nagios_plugins_support_msg_api" unless $noquit;
                    $ref = undef;
                    last;
                }
            } else {
                quit "UNKNOWN", "'$field' '$_' field not found. $nagios_plugins_support_msg_api" unless $noquit;
                $ref = undef;
                last;
            }
        }
        return $ref;
    } else {
        # XXX: this returns field not found where field exists but value is 'undef'
        if(defined($hash_ref->{$field})){
            return $hash_ref->{$field};
        } else {
            quit "UNKNOWN", "'$field' field not found. $nagios_plugins_support_msg_api" unless $noquit;
            return;
        }
    }
    code_error "hit end of get_field2 sub";
}

sub get_field2_array($$;$){
    my $hash_ref = shift;
    my $field    = shift;
    my $noquit   = shift;
    my $value = get_field2($hash_ref, $field, $noquit);
    if($noquit){
        return unless $value;
        return unless isArray($value);
    }
    assert_array($value, $field);
    return @{$value};
}

sub get_field2_float($$;$){
    my $hash_ref = shift;
    my $field    = shift;
    my $noquit   = shift;
    my $value = get_field2($hash_ref, $field, $noquit);
    if($noquit){
        return unless defined($value);
        return unless isFloat($value);
    }
    assert_float($value, $field);
    return $value;
}

sub get_field2_hash($$;$){
    my $hash_ref = shift;
    my $field    = shift;
    my $noquit   = shift;
    my $value = get_field2($hash_ref, $field, $noquit);
    if($noquit){
        return unless $value;
        return unless isHash($value);
    }
    assert_hash($value, $field);
    if($value){
        return %{$value};
    } else {
        return {};
    }
}

sub get_field2_int($$;$){
    my $hash_ref = shift;
    my $field    = shift;
    my $noquit   = shift;
    my $value = get_field2($hash_ref, $field, $noquit);
    if($noquit){
        return unless defined($value);
        return unless isInt($value);
    }
    assert_int($value, $field);
    return $value;
}

# get a field from a flattened hash
#sub get_field3($$;$){
#    my $hash_ref  = shift;
#    my $field     = shift || code_error "field not passed to get_field3()";
#    my $noquit    = shift;
#    isHash($hash_ref) or code_error "non-hash ref passed to get_field3()";
#    # XXX: this returns field not found where field exists but value is 'undef'
#    if(defined($hash_ref->{$field})){
#        return $hash_ref->{$field};
#    } else {
#        quit "UNKNOWN", "'$field' field not found. $nagios_plugins_support_msg_api" unless $noquit;
#        return;
#    }
#    code_error "hit end of get_field3 sub";
#}


sub get_options {
    my %options3;
    #@default_options{ keys %options } = values %options;
    foreach my $default_option (keys %default_options){
        # Check that the %options given don't clash with any existing or in-built options
        foreach my $option (keys %options){
            foreach my $switch (split(/\s*\|\s*/, $option)){
                if(grep({$_ eq $switch} split(/\s*\|\s*/, $default_option))){
                    code_error("Key clash on switch '$switch' with in-built option '$default_option' vs provided option '$option'");
                }
            }
        }
        $options{$default_option} = $default_options{$default_option}; #unless exists $options{$default_option}; # check above is stronger
    }
    foreach(keys %options){
        unless (isArray($options{$_})){
            code_error("invalid value for %options key '$_', should be an array not " . lc ref($options{$_}) );
        }
        $options3{$_} = $options{$_}[0];
    }
    my %option_count;
    foreach my $option (keys %options3){
        foreach my $switch (split(/\s*\|\s*/, $option)){
            $option_count{$switch}++;
        }
    }
    foreach(keys %option_count){
        $option_count{$_} > 1 and code_error("Duplicate option key detected '$_'");
    }
    GetOptions(%options3) or usage();
    # TODO: finish this debug code
#    if($debug){
#        foreach(sort keys %options3){
#            if(defined($options3{$_}[0])){
#                debug("var $options3{$_}[0] = $options3{$_}[0]");
#            }
#        }
#    }

    defined($help) and usage();
    defined($version) and version();

    if(defined($ENV{"DEBUG"}) and $ENV{"DEBUG"}){
        $debug = 1;
    }
    if($debug){
        $verbose = 3;
    }

    if(defined($ENV{"VERBOSE"}) and isInt($ENV{"VERBOSE"})){
        my $env_verbose = $1;
        if($env_verbose > $verbose){
            $verbose = $env_verbose;
            vlog3("environment variable \$VERBOSE = $env_verbose, increasing verbosity");
        } else {
            warn "environment variable \$VERBOSE is not an integer ('$env_verbose')";
        }
    }

    verbose_mode();
    #vlog2("options:\n");
    # validation is done on an option by option basis
    1;
}


sub get_path_owner ($) {
    # defined($_[0]) || code_error "no path passed to get_path_owner()";
    my $path = shift;
    open my $fh, $path || return;
    my @stats = stat($fh);
    close $fh;
    defined($stats[4]) || return;
    return getpwuid($stats[4]) || 0;
}

sub get_upper_threshold ($) {
    my $type = shift;
    if($type eq "warning" or $type eq "critical"){
        if(defined($thresholds{$type}{"upper"})){
            return $thresholds{$type}{"upper"};
        } else {
            return "";
        }
    }
    code_error "invalid threshold type '$type' passed to get_upper_threshold(), must be one of: warning critical";
}

sub get_upper_thresholds () {
    return get_upper_threshold("warning") . ";" . get_upper_threshold("critical");
}

# go flock ur $self ;)
sub go_flock_yourself (;$$) {
    my $there_can_be_only_one = shift;
    my $wait = shift;
    my $locking_options;
    if($wait){
        vlog2("waiting to go flock myself");
        $locking_options = LOCK_EX;
    } else {
        $locking_options = LOCK_EX|LOCK_NB;
    }
    if($there_can_be_only_one){
        open  *{0} or die "Failed to open *{0} for lock: $!\n";
        flock *{0}, $locking_options or die "Failed to acquire global lock, related code is already running somewhere!\n";
    } else {
        open $selflock, $0 or die "Failed to open $0 for lock: $!\n";
        flock $selflock, $locking_options or die "Another instance of " . abs_path($0) . " is already running!\n";
    }
    vlog2("truly flocked now");
    1;
}

sub flock_off (;$) {
    my $there_can_be_only_one = shift;
    if($there_can_be_only_one){
        open  *{0} or die "Failed to open *{0} for lock: $!\n";
        flock *{0}, LOCK_UN;
    } else {
        open $selflock, $0 or die "Failed to open $0 for lock: $!\n";
        flock $selflock, LOCK_UN;
    }
}


sub hr() {
    print "# " . "="x76 . " #\n";
}


sub human_units ($;$$) {
    my $num   = shift;
    my $units = shift;
    my $terse = shift;
    if($units){
        $num = expand_units($num, $units);
    }
    defined($num) or code_error "no arg passed to human_units()";
    isFloat($num) or isScientific($num) or code_error "non-float passed to human_units()";
    if(     $num >= (1024**7)){
        code_error "determine suspicious units for number $num, larger than Exabytes??!!";
    } elsif($num >= (1024**6)){
        $num = sprintf("%.2f", $num / (1024**6));
        $units = "EB";
    } elsif($num >= (1024**5)){
        $num = sprintf("%.2f", $num / (1024**5));
        $units = "PB";
    } elsif($num >= (1024**4)){
        $num = sprintf("%.2f", $num / (1024**4));
        $units = "TB";
    } elsif($num >= (1024**3)){
        $num = sprintf("%.2f", $num / (1024**3));
        $units = "GB";
    } elsif($num >= (1024**2)){
        $num = sprintf("%.2f", $num / (1024**2));
        $units = "MB";
    } elsif($num >= (1024**1)){
        $num = sprintf("%.2f", $num / (1024**1));
        $units = "KB";
    } elsif($num < 1024){
        if($terse){
            return "${num}B";
        } else {
            return "$num bytes";
        }
    } else {
        code_error "unable to determine units for number $num";
    }
    return trim_float($num) . $units;
}


sub inArray ($@) {
    my $item  = shift;
    my @array = @_;
    my $found = 0;
    foreach(@array){
        #vlog("checking $item against $_");
        if($item eq $_){
            $found++;
        }
    }
    return $found;
}


sub isAlNum ($) {
    my $arg = shift;
    defined($arg) or return; #code_error("no arg passed to isAlNum()");
    $arg =~ /^([A-Za-z0-9]+)$/ or return;
    return $1;
}


sub isArray ($) {
    my $isArray;
    if(defined($_[0])){
        $isArray = ref $_[0] eq "ARRAY";
    }
    if($_[1]){
        unless($isArray){
            code_error "non array reference passed to isArray()";
        }
    }
    return $isArray;
}


sub isAwsAccessKey($){
    my $aws_access_key = shift;
    defined($aws_access_key) or return;
    $aws_access_key =~ /^($aws_access_key_regex)$/ or return;
    return $1;
}

sub isAwsHostname($){
    my $aws_hostname = shift;
    defined($aws_hostname) or return;
    $aws_hostname =~ /^($aws_hostname_regex)$/ or return;
    return $1;
}

sub isAwsFqdn($){
    my $aws_fqdn = shift;
    defined($aws_fqdn) or return;
    $aws_fqdn =~ /^($aws_fqdn_regex)$/ or return;
    return $1;
}

sub isAwsSecretKey($){
    my $aws_secret_key = shift;
    defined($aws_secret_key) or return;
    $aws_secret_key =~ /^($aws_secret_key_regex)$/ or return;
    return $1;
}


sub isChars($$){
    my $string = shift;
    my $chars  = shift;
    defined($string) or return;
    defined($chars) or code_error "no chars passed to isChars";
    $chars = isRegex("[$chars]") or code_error "invalid regex char range passed to isChars()";
    $string =~ /^($chars+)$/ or return;
    return $1;
}

# isSub/isCode is used by set_timeout() to determine if we were passed a valid function for the ALRM sub
sub isCode ($) {
    my $isCode = ref $_[0] eq "CODE";
    return $isCode;
}

sub isCollection($){
    my $collection = shift;
    defined($collection) or return;
    $collection =~ /^(\w(?:[\w\.]*\w)?)$/  or return;
    $collection = $1;
    return $collection;
}

#sub isDigit {
#    isInt(@_);
#}
*isDigit = \&isInt;


sub isDatabaseName ($) {
    my $database = shift;
    defined($database) || return;
    $database =~ /^(\w+)$/ or return;
    $database = $1;
    return $database;
}


sub isDatabaseColumnName ($) {
    my $column = shift;
    defined($column) || return;
    $column =~ /^($column_regex)$/ or return;
    $column = $1;
    return $column;
}


sub isDatabaseFieldName ($) {
    my $field = shift;
    defined($field) || return;
    ( $field  =~ /^(\d+)$/ or $field =~/^([A-Za-z][\w()*,._-]+[A-Za-z0-9)])$/ ) or return;
    return $1;
}


sub isDatabaseTableName ($;$) {
    my $table           = shift;
    my $allow_qualified = shift;
    defined($table) || return;
    if($allow_qualified){
        $table =~ /^([A-Za-z0-9][\w\.]*[A-Za-z0-9])$/i or return;
        return $1;
    } else {
        $table =~ /^([A-Za-z0-9]\w*[A-Za-z0-9])$/i or return;
        return $1;
    }
    return;
}
*isDatabaseViewName = \&isDatabaseTableName;


sub isDomain ($) {
    my $domain = shift;
    defined($domain) or return;
    return if(length($domain) > 255);
    $domain =~ /^($domain_regex)$/ or return;
    return $1;
}

sub isDomainStrict ($) {
    my $domain = shift;
    defined($domain) or return;
    return if(length($domain) > 255);
    $domain =~ /^($domain_regex2)$/ or return;
    return $1;
}
*isDomain2 = \&isDomainStrict;

sub isDnsShortname($){
    my $name = shift;
    defined($name) or return;
    return if(length($name) < 3 or length($name) > 63);
    $name =~ /^($hostname_component)$/ or return;
    return $1;
}


# SECURITY NOTE: this only checks if the email address is valid, it's doesn't make it safe to arbitrarily pass to commands or SQL etc!
sub isEmail ($) {
    my $email = shift;
    defined($email) or return;
    return if(length($email) > 256);
    $email =~ /^$email_regex$/ || return;
    # Intentionally not untainting this as it's not safe given the addition of ' to the $email_regex to support Irish email addresses
    return $email;
}


sub isFilename($){
    my $filename = shift;
    return unless defined($filename);
    return if $filename =~ /^\s*$/;
    return if $filename =~ /\/$/;
    return unless($filename =~ /^($filename_regex)$/);
    return $1;
}

sub isDirname($){
    my $dirname = shift;
    return unless defined($dirname);
    return if $dirname =~ /^\s*$/;
    return unless($dirname =~ /^($dirname_regex)$/);
    return $1;
}


sub isFloat ($;$) {
    my $number = shift;
    my $negative = shift() ? "-?" : "";
    defined($number) or return;
    $number =~ /^$negative\d+(?:\.\d+)?$/;
}


sub isFqdn ($) {
    my $fqdn = shift;
    defined($fqdn) or return;
    return if(length($fqdn) > 255);
    $fqdn =~ /^($fqdn_regex)$/ or return;
    return $1;
}


sub isHash ($) {
    my $isHash;
    if(defined($_[0])){
        $isHash = ref $_[0] eq "HASH";
    }
    if($_[1]){
        unless($isHash){
            code_error "non hash reference passed";
        }
    }
    return $isHash;
}


sub isHex ($) {
    my $hex = shift;
    defined($hex) or return;
    $hex =~ /^((?:0x)?[A-Fa-f\d]+)$/ or return;
    return 1;
}


sub isHost ($) {
    my $host = shift;
    defined($host) or return;
    # special case to short-circuit failure when chaining find_active_server.py
    if($host eq "NO_SERVER_AVAILABLE" or $host eq "NO_HOST_AVAILABLE"){
        return;
    }
    # at casual glance this looks like it's duplicating isHostname but it's using a different unified regex of isHostname + isIP
    if(length($host) > 255){ # Can't be a hostname
        return;
    } elsif($host =~ /^($host_regex)$/){
        $host = $1;
        return $host;
    }
    return;
}


sub isHostname ($) {
    my $hostname = shift;
    defined($hostname) or return;
    # special case to short-circuit failure when chaining find_active_server.py
    if($hostname eq "NO_SERVER_AVAILABLE" or $hostname eq "NO_HOST_AVAILABLE"){
        return;
    }
    return if(length($hostname) > 255);
    $hostname =~ /^($hostname_regex)$/ or return;
    return $1;
}


sub isInt ($;$) {
    my $number = shift;
    my $signed = shift() ? "-?" : "";
    defined($number) or return; # code_error("no number passed to isInt()");
    $number =~ /^($signed\d+)$/;
}


sub isInterface ($) {
    my $interface = shift;
    defined($interface) || return;
    # TODO: consider checking if the interface actually exists on the system
    $interface =~ /^((?:em|eth|bond|lo|docker)\d+|lo|veth[A-Fa-f0-9]+)$/ or return;
    return $1;
}


sub isIP ($) {
    my $ip = shift;
    defined($ip) or return;
    $ip =~ /^($ip_regex)$/ or return;
    $ip = $1;
    my @octets = split(/\./, $ip);
    (@octets == 4) or return;
    foreach(@octets){
        $_ < 0   and return;
        $_ > 255 and return;
    }
    # not disallowing 0 or 255 in final octet any more due to CIDR
    #$octets[3] eq 0  and return;
    #$octets[3] > 254 and return;
    return $ip;
}


sub isJavaBean ($) {
    my $string = shift;
    $string =~ /^([A-Za-z][A-Za-z0-9.,:=_-]+[A-Za-z0-9])$/ or return undef;
    return $1;
}


sub isJavaException ($) {
    my $string = shift;
    if($string =~ /(?:^\s+at|^Caused by:)\s+\w+(?:\.\w+)+/){
        #debug "skipping java exception \\s+at|^Caused by => '$string'";
        return 1;
    } elsif($string =~ /\(.+:[\w-]+\(\d+\)\)/){
        #debug "skipping java exception (regex):\\w(\\d+) => '$string'";
        return 1;
    } elsif($string =~ /(\b|_).+\.\w+Exception:/){
        #debug "skipping java exception regex\\.\\w+Exception: => '$string'";
        return 1;
    } elsif($string =~ /^(?:\w+\.)*\w+Exception:/){
        #debug "skipping java exception (?:\\w+\\.)*\\w+Exception: => '$string'";
        return 1;
    } elsif($string =~ /\$\w+\(\w+:\d+\)/){
        #debug "skipping java exception \$\\w+(regex) => '$string'";
        return 1;
    #} elsif($string =~ /\s\w+\s\[[\w-]+\]\s[A-Z][a-z]+(?:[A-Z][a-z]+)+:\d+\s/){
        #debug "skipping java exception \\w+\\s\\[[\\w-]+\\]\\s[A-Z][a-z]+(?:[A-Z][a-z]+)+:\\d+\\s => '$string'";
        #return 1;
    }
    return;
}

# wish there was a better way of validating the JSON returned but Test::JSON is_valid_json() also errored out badly from underlying JSON::Any module, similar to JSON's decode_json
#sub isJson($){
#    my $data = shift;
#    defined($data) or return;
#    # slightly modified from http://stackoverflow.com/questions/2583472/regex-to-validate-json
#    # XXX: Unfortunately this only work on RHEL6's version of Perl and parse failure breaks all dependent code on RHEL5 now
##    my $json_regex = qr/
##      (?(DEFINE)
##         (?<number>   -? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)? )
##         (?<boolean>   true | false | null )
##         (?<string>    " ([^"\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* " )
##         (?<array>     \[  (?: (?&json)  (?: , (?&json)  )*  )?  \s* \] )
##         (?<pair>      \s* (?&string) \s* : (?&json)  )
##         (?<object>    \{  (?: (?&pair)  (?: , (?&pair)  )*  )?  \s* \} )
##         (?<json>      \s* (?: (?&number) | (?&boolean) | (?&string) | (?&array) | (?&object) ) \s* )
##      )
##      \A (?&json) \Z
##      /six;
#    # TODO: reinvestigate if this can be made to work
##    my $json;
##    my $number  = qr/(-? (?= [1-9]|0(?!\d) ) \d+ (\.\d+)? ([eE] [+-]? \d+)?)/six;
##    my $boolean = qr/(true | false | null)/six;
##    my $string  = qr/(" ([^"\\\\]* | \\\\ ["\\\\bfnrt\/] | \\\\ u [0-9a-f]{4} )* ")/six;
##    my $array   = qr/(\[  (?: (&$json)  (?: , (&$json)  )*  )?  \s* \])/six;
##    my $pair    = qr/\s* ($string) \s* : ($json)/six;
##    my $object  = qr/(\{  (?: ($pair)  (?: , ($pair)  )*  )?  \s* \})/six;
##    $json    = qr/(\s* (?: ($number) | ($boolean) | ($string) | ($array) | ($object) ) \s*)/six;
##    my $json_regex = qr/\A ($json) \Z/six;
#    #if($data =~ $json_regex){
#    #    return 1;
#    #}
#    return 0;
#}

sub isJson($){
    my $string = shift;
    defined($string) or return;
    my $json = undef;
    try {
        $json = decode_json($string);
    };
    return $json;
}


sub isXml($){
    require XML::Simple;
    import XML::Simple;
    my $string = shift;
    defined($string) or return;
    my $xml = undef;
    try {
        $xml = XMLin($string, forcearray => 1, keyattr => []);
    };
    return $xml;
}


sub isKrb5Princ ($) {
    my $principal = shift;
    defined($principal) or return;
    $principal =~ /^($krb5_principal_regex)$/ or return;
    return $1;
}


# Primarily for Nagios perfdata labels
sub isLabel ($) {
    my $label = shift;
    defined($label) or return;
    $label =~ /^$label_regex$/ or return;
    return $label;
}


sub isLdapDn ($) {
    #subtrace(@_);
    my $dn = shift;
    defined($dn) or return;
    $dn =~ /^($ldap_dn_regex)$/ || return;
    return $1;
}


sub isMinVersion ($$) {
    my $version = shift;
    my $min     = shift;
    if(not isVersionLax($version)){
        warn(sprintf("'%s' is not a recognized version format", $version));
        return;
    }
    isFloat($min) or code_error("invalid second arg passed to min_version");
    if($version =~ /(\d+(?:\.\d+)?)/){
        my $detected_version = $1;
        if($detected_version >= $min){
            return $detected_version;
        }
    }
    return;
}


sub isNagiosUnit ($) {
    my $units = shift;
    defined($units) or return;
    foreach(@valid_units){
        if(lc $units eq lc $_){
            return $_;
        }
    }
    return;
}


sub isNoSqlKey ($) {
    my $key = shift;
    defined($key) or return;
    $key =~ /^([\w\_\,\.\:\+\-]+)$/ or return;
    $key = $1;
    return $key;
}


sub isObject ($) {
    my $object = shift;
    return blessed($object);
}


sub isPathQualified($){
    my $path = shift;
    $path =~ /^(?:\.?\/)/;
}


sub isPort ($) {
    my $port = shift;
    defined($port) or return;
    $port  =~ /^(\d+)$/ || return;
    $port = $1;
    ($port >= 1 && $port <= 65535) || return;
    return $port;
}


sub isProcessName ($) {
    my $process = shift;
    defined($process) or return;
    $process =~ /^($process_name_regex)$/ or return;
    return $1;
}


sub isPythonTraceback ($) {
    my $string = shift;
    if($string =~ /\bFile "$filename_regex", line \d+, in (?:<module>|[A-Za-z]+)/){
        #debug "skipping python traceback 'File "...", line \\d+, in ...';
        return 1;
    }
    if($string =~ /\bTraceback \(most recent call last\):/){
        #debug "skipping python traceback 'Traceback \(most recent call last\)'";
        return 1;
    }
    return;
}

# XXX: doesn't catch error before Perl errors out, only using for late loading of regex from files, not in validate_regex()
sub isRegex ($) {
    my $regex = shift;
    defined($regex) || code_error "no regex arg passed to isRegex()";
    #defined($regex) || return;
    #vlog3("testing regex '$regex'");
    if(eval { qr/$regex/ }){
        return $regex;
    } else {
        return;
    }
}


sub isRef ($;$) {
    my $isRef = ref $_[0] eq "REF";
    if($_[1]){
        unless($isRef){
            code_error "non REF reference passed";
        }
    }
    return $isRef;
}


sub isScalar ($;$) {
    my $arg  = shift;
    my $quit = shift;
    my $ref = ref $arg;
    my $isScalar = 0;
    # needs more testing and thought before I can enable this
    #if(not $ref or $ref eq "SCALAR" or $ref eq "JSON::PP::Boolean"){
    if($ref eq "SCALAR"){
        $isScalar = 1;
    }
    if($quit and !$isScalar){
        code_error "non scalar reference passed";
    }
    return $isScalar;
}


sub isScientific($;$){
    my $num      = shift;
    my $negative = shift() ? "-?" : "";
    defined($num) or code_error "no arg passed to isScientific()";
    $num =~ /^$negative\d+(?:\.\d+)?e[+-]?\d+$/i or return;
    return $num;
}


#sub isSub {
#    isCode(@_);
#}
*isSub = \&isCode;


sub isThreshold($){
    my $threshold = shift;
    defined($threshold) or code_error "threshold arg to isThreshold() not defined";
    if($threshold =~ $threshold_range_regex){
        return 1;
    } elsif($threshold =~ $threshold_simple_regex){
        return 1;
    }
    return 0;
}


sub isUrl ($) {
    my $url = shift;
    defined($url) or return;
    #debug("url_regex: $url_regex");
    $url = trim($url);
    $url = "http://$url" unless $url =~ /:\/\//i;
    $url =~ /^($url_regex)$/ or return;
    return $1;
}


sub isUrlPathSuffix ($) {
    my $url = shift;
    defined($url) or return;
    $url =~ /^($url_path_suffix_regex)$/ or return;
    return $1;
}


sub isUser ($) {
    #subtrace(@_);
    my $user = shift;
    defined($user) or return; # code_error "user arg not passed to isUser()";
    $user =~ /^($user_regex)$/ || return;
    return $1;
}


sub isVersion($){
    my $version = shift;
    defined($version) or return;
    $version =~ /^($version_regex)$/ || return;
    return $1;
}

sub isVersionLax($){
    my $version = shift;
    defined($version) or return;
    # would use version_regex_lax but need to capture and don't want to force capture in the regex as that can mess with client code captures
    $version =~ /^($version_regex)-?.*$/ || return;
    return $1;
}

# =============================== OS CHECKS ================================== #
sub isOS ($) {
    $^O eq shift;
}

sub isMac () {
    isOS "darwin";
}

sub isLinux () {
    isOS "linux";
}

sub isLinuxOrMac () {
    isLinux() or isMac();
}

our $supported_os_msg = "this program is only supported on %s at this time";
sub mac_only () {
    isMac or quit("UNKNOWN", sprintf($supported_os_msg, "Mac/Darwin") );
}

sub linux_only () {
    isLinux or quit("UNKNOWN", sprintf($supported_os_msg, "Linux") );
}

sub linux_mac_only () {
    isLinuxOrMac or quit("UNKNOWN", sprintf($supported_os_msg, "Linux or Mac/Darwin") );
}
# ============================================================================ #


sub loginit () {
    # This can cause plugins to fail if there is no connection to syslog available at plugin INIT
    # Let's only use this for something that really needs it
    #INIT {
        #require Sys::Syslog;
        #import Sys::Syslog qw(:standard :macros);
        # Can't actually require/import optimize here because barewards aren't recognized early enough which breaks strict
        use Sys::Syslog qw(:standard :macros);
        # nofatal doesn't appear in earlier 5.x versions
        #openlog $progname, "ndelay,nofatal,nowait,perror,pid", LOG_LOCAL0;
        openlog $progname, "ndelay,nowait,perror,pid", LOG_LOCAL0;
        $syslog_initialized = 1;
    #}
}


sub log (@) {
    loginit() unless $syslog_initialized;
    # For some reason perror doesn't seem to print so do it manually here
    print strftime("%F %T", localtime) . "  $progname\[$$\]: @_\n";
    syslog LOG_INFO, "%s", "@_";
}


sub logdie (@) {
    &log("ERROR: @_");
    exit get_status_code("CRITICAL");
}


sub lstrip ($) {
    my $string = shift;
    #defined($string) or code_error "no arg passed to lstrip()";
    $string =~ s/^\s+//o;
    return $string;
}
#sub ltrim { lstrip(@_) }
*ltrim = \&lstrip;


sub minimum_value ($$) {
    my $value = shift;
    my $min   = shift;
    isFloat($value) or code_error "invalid first arg passed to minimum_value(), must be float";
    isFloat($min)   or code_error "invalid second arg passed to minimum_value(), must be float";
    if($value < $min){
        return $min;
    }
    return $value;
}


sub msg_perf_thresholds (;$$$) {
    my $return = shift;
    my $type   = shift() ? "lower" : "upper";
    my $name   = shift() || "";
    $name .= " " if $name and $name !~ / $/;
    my $tmp = ";";
    $tmp .= $thresholds{"${name}warning"}{$type}  if defined($thresholds{"${name}warning"}{$type});
    $tmp .= ";";
    $tmp .= $thresholds{"${name}critical"}{$type} if defined($thresholds{"${name}critical"}{$type});
    $tmp .= ";";
    if(defined($return) and $return){
        return $tmp;
    } else {
        $msg .= $tmp;
    }
}


sub msg_thresholds (;$$) {
    my $no_msg_thresholds = shift || 0;
    my $name = shift() || "";
    my $msg2 = "";
    if (defined($thresholds{"${name}critical"}{"error"}) or
        defined($thresholds{"${name}warning"}{"error"})  or
            ($verbose and (
                            defined($thresholds{"${name}warning"}{"range"}) or
                            defined($thresholds{"${name}critical"}{"range"})
                          )
            ) 
        ) {
        $msg2 .= " (";
        if(defined($thresholds{"${name}critical"}{"error"})){
            $msg2 .= $thresholds{"${name}critical"}{"error"} . ", ";
        }
        elsif(defined($thresholds{"${name}warning"}{"error"})){
            $msg2 .= $thresholds{"${name}warning"}{"error"} . ", ";
        }
        if(defined($thresholds{"${name}warning"}{"range"})){
            $msg2 .= "w=" . $thresholds{"${name}warning"}{"range"};
        }
        if(defined($thresholds{"${name}warning"}{"range"}) and defined($thresholds{"${name}critical"}{"range"})){
            $msg2 .= "/";
        }
        if(defined($thresholds{"${name}critical"}{"range"})){
            $msg2 .= "c=" . $thresholds{"${name}critical"}{"range"};
        }
        $msg2 .= ")";
    }
    unless($no_msg_thresholds){
        $msg .= $msg2 if $msg2;
    }
    return $msg2;
}


sub month2int($){
    my $month = shift;
    defined($month) or code_error "no arg passed to month2int";
    my %months = (
        "Jan" => 0,
        "Feb" => 1,
        "Mar" => 2,
        "Apr" => 3,
        "May" => 4,
        "Jun" => 5,
        "Jul" => 6,
        "Aug" => 7,
        "Sep" => 8,
        "Oct" => 9,
        "Nov" => 10,
        "Dec" => 11
    );
    grep { $month eq $_ } keys %months or code_error "non-month passed to month2int()";
    return $months{$month};
}


sub open_file ($;$) {
    my $filename = shift;
    my $lock = shift;
    #my $mode = shift;
    my $tmpfh;
    defined($filename) or code_error "no filename given to open_file()";
    ( -e $filename ) or quit("CRITICAL", "file not found: '$filename'");
    ( -f $filename ) or quit("CRITICAL", "not a valid file: '$filename'");
    ( -r $filename ) or quit("CRITICAL", "file not readable: '$filename'");
    vlog2("opening file: '$filename'");
    open $tmpfh, "$filename" or quit("UNKNOWN", "Error: failed to open file '$filename': $!");
    if($lock){
        flock($tmpfh, LOCK_EX | LOCK_NB) or quit("UNKNOWN", "Failed to aquire a lock on file '$filename', another instance of this code may be running?");
    }
    return $tmpfh;
}


sub parse_file_option($;$){
    my $file      = shift;
    my $file_args = shift;
    my @files;
    my @tmp;
    if($file){
        my @tmp = split(/\s*[\s,]\s*/, $file);
        push(@files, @tmp);
    }

    if($file_args){
        # @ARGV should only be used after get_options()
        foreach(@ARGV){
            push(@files, $_);
        }
    }

    foreach my $f (@files){
        if(not -f $f ){
            print STDERR "File not found: '$f'\n";
            @files = grep { $_ ne $f } @files;
        }
    }
    if($file or ($file_args and @ARGV)){
        if(not @files){
            die "Error: no files found\n";
        }
    }

    vlog_option("files", "[ '" . join("', '", @files) . "' ]");

    return @files;
}


sub perf_suffix($){
    my $key = shift;
    my $prefix = '[\b\s\._-]';
    if($key =~ /${prefix}bytes$/){
        return "b";
    } elsif($key =~ /${prefix}millis$/){
        return "ms";
    }
    return "";
}


# parsing ps aux is more portable than pkill -f command. Useful for alarm sub
# Be careful to validate and make sure you use taint mode before calling this sub
sub pkill ($;$) {
    my $search    = $_[0] || code_error "No search arg specified for pkill sub";
    my $kill_args = $_[1] || "";
    $search =~ s/(\/)/\\$1/g;
    $search =~ s/'/./g;
    return `ps aux | awk '/$search/ {print \$2}' | while read pid; do kill $kill_args \$pid >/dev/null 2>&1; done`;
}


our $plural;
sub plural ($) {
    my $var = $_[0];
    #print "var = $var\n";
    #print "var ref = " . ref($var) . "\n";
    if(isArray($var)){
        $var = scalar(@{$var});
    } elsif (isHash($var)){
        $var = scalar keys %{$var};
    # TODO: enable this, currently doesn't work
    #} elsif (not isFloat($var)) {
    #    code_error "non-scalar, non-array ref and non-hash ref passed to plural()";
    }
    isFloat($var) or code_error("arg passed to plural() is not a float");
    ( $var == 1 ) ? ( $plural = "" ) : ( $plural = "s" );
}


my ($wchar, $hchar, $wpixels, $hpixels);
sub print_options (@) {
    check_terminal_size();
    #subtrace(@_);
    my $switch_width = $short_options_len + 2 + $long_options_len + 4 - 1;
    my $desc_width   = $wchar - $switch_width;
    foreach my $option (@_){
        my $option_regex = $option;
        $option_regex  =~ s/^\w\|//;
        $option_regex  =~ s/=.*$//;
        # pointless since this is hardcoded Perl interpreter will always error out first
        #$option_regex  = isRegex($option_regex) || code_error "invalid option regex '$option_regex' passed in \@options array to print_options()";
        #debug "\noption is $option";
        if($option =~ /debug/){
            #debug "skipping debug option";
            next;
        }
        foreach(keys %options){
            #debug $_;
            #debug $options{$_};
            #debug "options long value is $options{$_}{desc}";
            if($options{$_}{"long"} =~ /^.*--(?:$option_regex)\s*$/ or $options{$_}{"short"} =~ /^-(?:$option_regex)\s*$/){
                # This format string must match the length of $switch_width at top of sub
                printf STDERR "%-${short_options_len}s  %-${long_options_len}s    ", $options{$_}{"short"}, $options{$_}{"long"};
                my $option_desc_len = length($options{$_}{"desc"});
                for(my $start=0; $start < $option_desc_len; ){
                    my ($len, $end);
                    if($option_desc_len - $start < $desc_width){
                        $end = $option_desc_len;
                    } else {
                        my $space_index   = rindex($options{$_}{"desc"}, " ",  $start + $desc_width - 1);
                        if($space_index > $start){
                            $end = $space_index;
                        } else{
                            $space_index = index($options{$_}{"desc"}, " ", $start);
                            if($space_index > $start){
                                $end = $space_index;
                            } else {
                                $end = $option_desc_len;
                            }
                        }
                    }
                    $end > $start or $end = $option_desc_len;
                    $len = $end - $start;
                    if($start > 0){ # and $end <= $option_desc_len){
                        printf STDERR "%${switch_width}s", "";
                    }
                    printf STDERR "%s\n", substr($options{$_}{"desc"}, $start, $len);
                    $start = $end;
                }
                delete $options{$_};
                last;
            }
        }
    }
    1;
}

sub prompt($){
    my $question = shift;
    print "\n$question ";
    my $response = <STDIN>;
    chomp $response;
    vlog();
    return $response;
}

sub isYes($;$$){
    my $val  = shift;
    my $name = shift() || "";
    my $noquit = shift;
    $name = " for $name";
    unless($val =~ /^\s*(?:y(?:es)?|n(?:o)?)?\s*$/i){
        die "invalid response$name, must be 'yes' or 'no'\n" unless $noquit;
    }
    if($val =~ /^\s*y(?:es)?\s*$/i){
        return 1;
    } else {
        return 0;
    }
}

# Also prototyped at top to allow me to call it earlier
sub quit (@) {
    if($status_prefix ne ""){
        $status_prefix .= " ";
    }
    if(@_ eq 0){
        chomp $msg;
        # This ends up bit shifting to 255 instead of 0
        grep(/^$status$/, keys %ERRORS) or die "Code error: unrecognized exit code '$status' specified on quit call, not found in %ERRORS hash\n";
        # XXX: do not use die function, some modules call die without setting $? to something other than zero, causing an OK: prefix and zero exit code :-/
        #$? = $ERRORS{$status};
        #die "${status_prefix}$status: $msg\n";
        #die "$msg\n";
        print "${status_prefix}$status: $msg\n";
        exit $ERRORS{$status};
    } elsif(@_ eq 1){
        $msg = $_[0];
        chomp $msg;
        #$? = $ERRORS{"CRITICAL"};
        #die "${status_prefix}CRITICAL: $msg\n";
        #die "$msg\n";
        print "${status_prefix}CRITICAL: $msg\n";
        exit $ERRORS{"CRITICAL"};
    } elsif(@_ eq 2) {
        $status = $_[0];
        $msg    = $_[1];
        $msg or $msg = "msg not defined";
        chomp $msg;
        grep(/^$status$/, keys %ERRORS) or die "Code error: unrecognized exit code '$status' specified on quit call, not found in %ERRORS hash\n";
        #$? = $ERRORS{$status};
        #die "${status_prefix}$status: $msg\n";
        #die "$msg\n";
        print "${status_prefix}$status: $msg\n";
        exit $ERRORS{$status};
    } else {
        #print "UNKNOWN: Code Error - Invalid number of arguments passed to quit function (" . scalar(@_). ", should be 0 - 2)\n";
        #exit $ERRORS{"UNKNOWN"};
        code_error("invalid number of arguments passed to quit function (" . scalar(@_) . ", should be 0 - 2)");
    }
}


sub random_alnum($){
    my $length = shift;
    isInt($length) or code_error "invalid length passed to random_alnum";
    my @chars  = ("A".."Z", "a".."z", 0..9);
    my $string = "";
    $string .= $chars[rand @chars] for 1..$length;
    return $string;
}


sub remove_timeout(){
    delete $HariSekhonUtils::default_options{"t|timeout=i"};
}


sub resolve_ip ($) {
    require Socket;
    import Socket;
    my $ip;
    defined($_[0]) or return;
    # returns packed binary address
    $ip = inet_aton($_[0])  || return;
    # returns human readable x.x.x.x - only supporting IPv4 for now
    $ip = inet_ntoa($ip)    || return;
    # validate what we have is a correct IP address
    $ip = isIP($ip)         || return;
    return $ip;
}


sub rstrip ($) {
    my $string = shift;
    defined($string) or code_error "no arg passed to rstrip()";
    $string =~ s/\s+$//;
    return $string;
}
#sub rtrim { rstrip(@_) }
*rtrim = \&rstrip;


sub sec2min ($){
    my $secs = shift;
    isFloat($secs) or return;
    return sprintf("%d:%.2d", int($secs / 60), $secs % 60);
}


# Time::Seconds and Time::Piece are available from Perl v5.9.5 but CentOS 5 is v5.8
sub sec2human ($){
    my $secs = shift;
    isFloat($secs) or code_error "invalid non-float argument passed to sec2human";
    my $human_time = "";
    if($secs >= 86400){
        my $days = int($secs / 86400);
        plural $days;
        $human_time .= sprintf("%d day$plural ", $days);
        $secs %= 86400;
    }
    if($secs >= 3600){
        my $hours = int($secs / 3600);
        plural $hours;
        $human_time .= sprintf("%d hour$plural ", $hours);
        $secs %= 3600;
    }
    if($secs >= 60){
        my $mins = int($secs / 60);
        plural $mins;
        $human_time .= sprintf("%d min$plural ", $mins);
        $secs %= 60;
    }
    plural $secs;
    $human_time .= sprintf("%d sec$plural", int($secs));
    return $human_time;
}


sub set_http_timeout($){
    my $http_timeout = shift;
    isFloat($http_timeout) or code_error "invalid arg passed to set_http_timeout(), must be float";
    defined_main_ua();
    $http_timeout = sprintf("%.2f", minimum_value($http_timeout, 1) );
    vlog2("setting http per request timeout to $http_timeout secs\n");
    $main::ua->timeout($http_timeout);
}


sub set_sudo (;$) {
    local $user = $_[0] if defined($_[0]);
    defined($user) or code_error "user arg not passed to set_sudo() and \$user not defined in outer scope";
    # Quit if we're not the right user to ensure we don't sudo command and hang or return with a generic timeout error message
    #quit "UNKNOWN", "not running as '$hadoop_user' user";
    # only Mac has -n switch for non-interactive :-/
    #$sudo = "sudo -n -u $hadoop_user ";
    if(getpwuid($>) eq $user){
        $sudo = "";
    } else {
        vlog2("EUID doesn't match user $user, using sudo\n");
        $sudo = "echo | sudo -S -u $user ";
    }
}


sub set_timeout (;$$) {
    $timeout    = $_[0] if $_[0];
    my $sub_ref;
    $sub_ref = $_[1] if $_[1];
    $timeout =~ /^\d+$/ || usage("timeout value must be a positive integer\n");
    ($timeout >= $timeout_min && $timeout <= $timeout_max) || usage("timeout value must be between $timeout_min - $timeout_max secs\n");
    if(defined($sub_ref)){
        isSub($sub_ref) or code_error "invalid sub ref passed to set_timeout()";
    }

    $SIG{ALRM} = sub {
        &$sub_ref if defined($sub_ref);
        quit("UNKNOWN", "self timed out after $timeout seconds" . ($timeout_current_action ? " while $timeout_current_action" : ""));
    };
    #verbose_mode() unless $_[1];
    vlog2("setting timeout to $timeout secs\n");
    # alarm returns the time of the last timer, on first run this is zero so cannot die here
    alarm($timeout) ;#or die "Failed to set time to $timeout";
}


#sub sub_noarg {
#    quit "UNKNOWN", "Code Error: no arg supplied to subroutine " . (caller(1))[3];
#}

sub skip_java_output($){
    @_ or code_error "no input passed to skip_java_output()";
    my $str = join(" ", @_);
    # warning due to Oracle 7 JDK bug fixed in 7u60
    # objc[54213]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/bin/java and /Library/Java/JavaVirtualMachines/jdk1.7.0_45.jdk/Contents/Home/jre/lib/libinstrument.dylib. One of the two will be used. Which one is undefined.
    if($str =~ /Class JavaLaunchHelper is implemented in both|^SLF4J/){
        return 1;
    }
    return 0;
}


sub strBool($){
    my $str = shift;
    # " " returns true otherwise
    $str = strip($str);
    return "false" if $str =~ /false/i;
    ( $str ? "true" : "false" );
}


sub strip ($) {
    my $string = shift;
    defined($string) or code_error "no arg passed to strip()";
    $string =~ s/^\s+//o;
    $string =~ s/\s+$//o;
    return $string;
}
*trim = \&strip;


sub subtrace (@) {
    #@_ || code_error("\@_ not passed to subtrace");
    return unless ($debug >= 2);
    my ( $package, $filename, $line ) = caller;
    my $debug_msg = "entering with args: @_";
    $debug_msg =~ s/^(\n+)//;
    my $prefix_newline = $1 || "";
    # TODO: can improve this if we can go one level up, dedupe with debug, do this later
    printf "${prefix_newline}debug: %s() => $debug_msg\n", (caller(1))[3];
}


sub timecomponents2days($$$$$$){
    my $year  = shift;
    my $month = shift;
    my $day   = shift;
    my $hour  = shift;
    my $min   = shift;
    my $sec   = shift;
    my $month_int;
    if(isInt($month)){
        $month_int = $month;
    } else {
        $month_int = month2int($month);
    }
    my $epoch = timegm($sec, $min, $hour, $day, $month_int, $year-1900) || code_error "failed to convert timestamp $year-$month-$day $hour:$min:$sec";
    my $now   = time || code_error "failed to get epoch timestamp";
    return ($epoch - $now) / (86400);
}


sub tstamp () {
    return strftime("%F %T %z  ", localtime);
}

sub tprint ($) {
    my $msg = shift;
    defined($msg) or code_error "tprint msg arg not defined";
    print tstamp() . "$msg\n";
}


sub trim_float ($) {
    my $num = shift;
    defined($num) or code_error "no arg passed to trim_float()";
    $num =~ s/\.0+$//;
    $num =~ s/\.([1-9]*)0+$/\.$1/;
    return $num;
}


#sub type {
#    my $builtin = $_[0] || code_error "no arg supplied to which() subroutine";
#    my $quit    = $_[1] || 0;
#    $builtin =~ /^([\w-]+)$/ or quit "UNKNOWN", "invalid command/builtin passed to type subroutine";
#    $builtin = $1;
#   `type $builtin`;
#    return 1 if($? == 0);
#    quit "UNKNOWN", "$builtin is not a shell built-in" if $quit;
#    return;
#}


sub sort_insensitive (@) {
    my @array = @_; # or code_error "no arg passed to sort_insensitive()";
    isArray(\@array) or code_error "sort_insensitive() was passed a non-array";
    scalar @array or code_error "sort_insensitive() was passed an empty array";
    return sort { "\L$a" cmp "\L$b" } @array;
}


sub uniq_array (@) {
    my @array = @_; # or code_error "no arg passed to uniq_array";
    isArray(\@array) or code_error "uniq_array was passed a non-array";
    scalar @array or code_error "uniq_array was passed an empty array";
    return sort keys %{{ map { $_ => 1 } @array }};
}


sub uniq_array2(@){
    my @array = @_; # or code_error "no arg passed to uniq_array";
    isArray(\@array) or code_error "uniq_array2 was passed a non-array";
    scalar @array or code_error "uniq_array2 was passed an empty array";
    my @array2;
    my $item;
    foreach $item (@array){
        grep { $item eq $_ } @array2 and next;
        push(@array2, $item);
    }
    return @array2;
}
*uniq_array_ordered = \&uniq_array2;

sub get_terminal_size(){
    eval {
        local $SIG{__WARN__} = sub {};
        ($wchar, $hchar, $wpixels, $hpixels) = GetTerminalSize();
    };
    check_terminal_size();
}

sub check_terminal_size(){
    unless(defined($wchar) and defined($hchar) and defined($wpixels) and defined($hpixels)){
        #warn "\nTerm::ReadKey GetTerminalSize() failed to return values! Ignore this warning if you are teeing to a logfile (otherwise your terminal is messed up...)\n\n\n";
        $wchar   = 99999999;
        $hchar   = 99999999;
        $wpixels = 99999999;
        $hpixels = 99999999;
    }
    # Travis gets suspiciously small width
    if($wchar < 80){
        $wchar = 80;
    }
    if($hchar < 25){
        $hchar = 25;
    }
    1;
}

sub usage (;@) {
    get_terminal_size();
    print STDERR "@_\n\n" if (@_);
    if(not @_ and $main::DESCRIPTION){
        print STDERR "Hari Sekhon - https://github.com/harisekhon";
        if($github_repo){
            print STDERR "/$github_repo";
        } elsif(dirname(abs_path(__FILE__)) =~ /tools/i){
            print STDERR "/tools";
        } elsif(dirname(abs_path(__FILE__)) =~ /nagios-plugins/i or $main::DESCRIPTION =~ /Nagios/i){
            print STDERR "/nagios-plugins";
        }
        print STDERR "\n\n$progname\n\n";
        #print STDERR "$main::DESCRIPTION\n\n";
        my $desc_len = length($main::DESCRIPTION);
        for(my $start=0; $start < $desc_len; ){
            #print "desc len $desc_len\n";
            #print "start $start\n";
            my ($len, $end);
            # reset the start to after newlines
            # the problem is that the start is taken across newlines
            if(($desc_len - $start) < $wchar){
                $end = $desc_len;
            } else {
                my $newline_index = rindex($main::DESCRIPTION, "\n", $start + $wchar - 1);
                my $space_index   = rindex($main::DESCRIPTION, " ",  $start + $wchar - 1);
                if($newline_index > $start){
                    #print "newline index $newline_index\n";
                    $end = $newline_index;
                } elsif($space_index > $start){
                    $end = $space_index;
                } else{
                    $newline_index = index($main::DESCRIPTION, "\n", $start);
                    if($newline_index > $start){
                        $end = $newline_index;
                    } else {
                        $end = $desc_len;
                    }
                }
            }
            #print "end $end\n";
            $end > $start or $end = $desc_len;
            $len = $end - $start;
            #print "len $len\n";
            printf STDERR "%s\n", substr($main::DESCRIPTION, $start, $len);
            $start = $end + 1;
        }
        print STDERR "\n";
    }
    print STDERR "$usage_line\n\n";
    foreach my $key_orig (sort keys %options){
        my $key = $key_orig;
        $key =~ s/=.*$//;
        $key =~ s/\+//;
        code_error("invalid array count in value for key '$key' in options hash") unless(scalar(@{$options{$key_orig}}) == 2);
        $options{$key} = $options{$key_orig}[1];
        #debug "key: $key  key_orig: $key_orig";
        delete $options{$key_orig} if($key ne $key_orig);
    }
    foreach(sort keys %options){
        my $option = "";
        my @short_options = ();
        my @long_options  = ();
        if($_ =~ /\|/){
            @_ = split('\|', $_);
            foreach(@_){
                if(length($_) == 1){
                    push(@short_options, "-$_");
                } else {
                    push(@long_options, "--$_");
                }
            }
        } else {
            if(length($_) == 1){
                push(@short_options, "-$_");
            } else {
                push(@long_options, "--$_");
            }
        }
        #debug "$_ short_options: " . join(",", @short_options) . "  long_options:" . join(",", @long_options) . "  desc: $options{$_}";
        $options{$_} = {
            "short" => join(" ", @short_options),
            "long"  => join(" ", @long_options),
            "desc"  => $options{$_}
        };
    }

    foreach(sort keys %options){
        $short_options_len = length($options{$_}{"short"}) if($short_options_len < length($options{$_}{"short"}));
        $long_options_len  = length($options{$_}{"long"} ) if($long_options_len  < length($options{$_}{"long"} ));
    }
    # First print options in the order specified in @usage_order
    print_options(@usage_order);
    # Now print any unspecified order options in alphabetical order
    foreach my $option (sort keys %options){
        #debug "iterating over general options $option";
        # TODO: improve this matching for more than one long opt
        my $option_regex = escape_regex($option);
        if(grep($_ =~ /\A$option_regex\Z/, keys %default_options)){
            #debug "skipping $option cos it matched \%default_options";
            next;
        }
        print_options($option);
        #printf "%-${short_options_len}s  %-${long_options_len}s \t%s\n", $options{$option}{"short"}, $options{$option}{"long"}, $options{$option}{"desc"};
    }
    # Finally print base common options, verbosity, timeout etc
    print_options(sort { lc($a) cmp lc($b) } keys %default_options);
    exit $ERRORS{"UNKNOWN"};
}


sub user_exists ($) {
    my $user = shift; # if $_[0];
    #defined($user) or code_error("no user passed to user_exists()");
    #$user = isUser($user) || return;

    # using id command since this should exist on most unix systems
    #which("id", 1);
    #`id "$user" >/dev/null 2>&1`;
    #return 1 if ( $? eq 0 );
    #return;

    # More efficient
    return defined(getpwnam($user));
}


sub validate_alnum($$){
    my $arg  = shift;
    my $name = shift || croak "second argument (name) not defined when calling validate_alnum()";
    defined($arg) or usage "$name not defined";
    $arg = isAlNum($arg);
    # isAlNum returns zero as valid and undef when not valid so must check explicitly for undef and avoid 0 which is false in Perl
    defined($arg) || usage "invalid $name defined: must be alphanumeric";
    vlog_option($name, $arg);
    return $arg;
}


sub validate_aws_access_key($){
    my $aws_access_key = shift;
    defined($aws_access_key) or usage "aws access key not defined";
    $aws_access_key = isAwsAccessKey($aws_access_key) || usage "invalid aws access key defined: must be 20 alphanumeric characters";
    vlog_option("aws access key", "X"x18 . substr($aws_access_key, 18, 2));
    return $aws_access_key;
}


sub validate_aws_bucket($){
    my $bucket = shift;
    defined($bucket) or usage "no aws bucket specified";
    $bucket = isDnsShortname($bucket) || usage "invalid aws bucket name defined: must be alphanumeric between 3 and 63 characters long";
    isIP($bucket) and usage "invalid aws bucket name defined: may not be formatted as an IP address";
    vlog_option("aws bucket", $bucket);
    return $bucket;
}


sub validate_aws_secret_key($){
    my $aws_secret_key = shift;
    defined($aws_secret_key) or usage "aws secret key not defined";
    $aws_secret_key = isAwsSecretKey($aws_secret_key) || usage "invalid aws secret key defined: must be 40 alphanumeric characters";
    vlog_option("aws secret key", "X"x38 . substr($aws_secret_key,38, 2));
    return $aws_secret_key;
}


# Takes a 3rd arg as a regex char range
sub validate_chars($$$){
    my $string   = shift;
    my $name  = shift || croak "second argument (name) not defined when calling validate_chars()";
    my $chars = shift;
    defined($string) or usage "$name not defined";
    $string = isChars($string, $chars) || usage "invalid $name defined: must be one of the following chars - $chars";
    vlog_option($name, $string);
    return $string;
}


sub validate_collection ($;$) {
    my $collection = shift;
    my $name       = shift || "";
    $name .= " " if $name;
    defined($collection) or usage "${name}collection not defined";
    $collection = isCollection($collection) || usage "invalid ${name}collection defined: must be alphanumeric, with optional periods in the middle";
    vlog_option("${name}collection", $collection);
    return $collection;
}


sub validate_database ($;$) {
    my $database = shift;
    my $name     = shift || "";
    $name .= " " if $name;
    defined($database)      || usage "${name}database not defined";
    $database = isDatabaseName($database) || usage "invalid ${name}database defined: must be alphanumeric";
    vlog_option("${name}database", $database);
    return $database;
}


sub validate_database_columnname ($) {
    my $column = shift;
    defined($column) || usage "column not defined";
    $column = isDatabaseColumnName($column) || usage "invalid column defined: must be alphanumeric";
    vlog_option("column", $column);
    return $column;
}


sub validate_database_fieldname ($) {
    my $field = shift;
    defined($field) || usage "field not defined";
    $field = isDatabaseFieldName($field) || usage "invalid field defined: must be a positive integer, or a valid field name";
    ($field eq "0") and usage "invalid field defined: cannot be zero";
    vlog_option("field", $field);
    return $field;
}


sub validate_database_tablename ($;$$) {
    my $table           = shift;
    my $name            = shift;
    my $allow_qualified = shift;
    $name .= " " if $name;
    defined($table) || usage "${name}table not defined";
    $table = isDatabaseTableName($table, $allow_qualified) || usage "invalid ${name}table defined: must be alphanumeric";
    vlog_option("${name}table", $table);
    return $table;
}


sub validate_database_viewname ($;$$) {
    my $view           = shift;
    my $name            = shift;
    my $allow_qualified = shift;
    $name .= " " if $name;
    defined($view) || usage "${name}view not defined";
    $view = isDatabaseViewName($view, $allow_qualified) || usage "invalid ${name}view defined: must be alphanumeric";
    vlog_option("${name}view", $view);
    return $view;
}


sub validate_database_query_select_show ($;$) {
    my $query = shift;
    my $name  = shift || "";
    $name .= " " if $name;
    defined($query) || usage "${name}query not defined";
    #$query =~ /^\s*((?i:SHOW|SELECT)\s[\w\s;:,\.\?\(\)*='"-]+)$/ || usage "invalid query supplied";
    #debug("regex validating query: $query");
    $query =~ /^\s*((?:SHOW|SELECT)\s+.+)$/i || usage "invalid ${name}query defined: may only be a SELECT or SHOW statement";
    $query = $1;
    $query =~ /\b(?:insert|update|delete|create|drop|alter|truncate)\b/i and usage "invalid ${name}query defined: found DML statement keywords!";
    # this trips up users who put ; at the end of their query and doesn't offer that much protection anyway since DML is already checked for and it may be convenient to comment out end of query for testing
    #$query =~ /;|--/i and usage "invalid ${name}query defined: suspect chars ';' or '--' detected in query!";
    $query =~ /;/ and usage "invalid ${name}query defined: you may not add semi-colons to your queries, while it works on the command line, Nagios ends up choking by prematurely terminating the check command resulting in a null shell error before this plugin executes so the error handlers in this code do not have any chance to catch it";
    vlog_option("${name}query", $query);
    return $query;
}


#sub validate_dir ($;$) {
#    validate_directory(@_);
#}


sub validate_dirname ($;$$$) {
    my $dirname = shift;
    my $name     = shift || "";
    my $noquit   = shift;
    my $no_vlog  = shift;
    $name .= " " if $name;
    if(not defined($dirname) or $dirname =~ /^\s*$/){
        usage "${name}directory not defined";
        return;
    }
    my $dirname2;
    unless($dirname2 = isDirname($dirname)){
        usage "invalid ${name}directory (does not match regex critera): '$dirname'" unless $noquit;
        return;
    }
    vlog_option("${name}directory", $dirname2) unless $no_vlog;
    return $dirname2;
}


sub validate_directory ($;$$$) {
    my $dir     = shift;
    my $name    = shift || "";
    my $noquit  = shift;
    my $no_vlog = shift;
    $name .= " " if $name;
    if($noquit){
        return validate_dirname($dir, $name, "noquit");
    }
    defined($dir) || usage "${name}directory not defined";
    $dir = validate_dirname($dir, $name, "noquit", $no_vlog) || usage "invalid ${name}directory (does not match regex criteria): '$dir'";
    ( -d $dir) || usage "cannot find ${name}directory: '$dir'";
    return $dir;
}
*validate_dir = \&validate_directory;


sub validate_domain ($;$) {
    my $domain = shift;
    my $name   = shift || "";
    $name .= " " if $name;
    defined($domain) || usage "${name}domain name not defined";
    # don't print the domain as it gets reset to undef and results in "Use of uninitialized value $domain in concatenation (.) or string"
    my $domain2 = $domain;
    $domain = isDomain($domain) or usage "invalid ${name}domain name '$domain2' defined";
    vlog_option("${name}domain", $domain);
    return $domain;
}


# SECURITY NOTE: this only validates the email address is valid, it's doesn't make it safe to arbitrarily pass to commands or SQL etc!
sub validate_email ($) {
    my $email = shift;
    defined($email) || usage "email not defined";
    isEmail($email) || usage "invalid email address defined: failed regex validation";
    # Not passing it through regex as I don't want to untaint it due to the addition of the valid ' char in email addresses
    return $email;
}


sub validate_filename ($;$$$) {
    my $filename = shift;
    my $name     = shift || "filename";
    my $noquit   = shift;
    my $no_vlog  = shift;
    if(not defined($filename) or $filename =~ /^\s*$/){
        usage "$name not defined";
        return;
    }
    my $filename2;
    unless($filename2 = isFilename($filename)){
        usage "invalid $name (does not match regex critera): '$filename'" unless $noquit;
        return;
    }
    vlog_option($name, $filename2) unless $no_vlog;
    return $filename2;
}


sub validate_file ($;$$$) {
    my $filename = shift;
    my $name     = shift || "";
    my $noquit   = shift;
    my $no_vlog  = shift;
    $filename = validate_filename($filename, $name, $noquit, $no_vlog) or return;
    unless( -f $filename ){
        $name .= " " if $name;
        usage "${name}file not found: '$filename' ($!)" unless $noquit;
        return
    }
    return $filename;
}


sub validate_float ($$$$) {
    my ($float, $name, $min, $max) = @_;
    defined($float) || usage "$name not defined";
    isFloat($float,1) or usage "invalid $name defined: must be a real number";
    if(
        not ( isFloat($min, "allow_negative") or isScientific($min, "allow_negative") )
        or
        not ( isFloat($max, "allow_negative") or isScientific($max, "allow_negative") )
    ){
        usage "invalid min/max ($min/$max) passed to validate_float()";
    }
    ($float >= $min && $float <= $max) or usage "invalid $name defined: must be real number between $min and $max";
    $float =~ /^(-?\d+(?:\.\d+)?)$/ or usage "invalid float $name passed to validate_float(), WARNING: caught LATE";
    $float = $1;
    vlog_option($name, $float);
    return $float;
}


sub validate_fqdn ($;$) {
    my $fqdn = shift;
    my $name = shift || "";
    $name .= " " if $name;
    defined($fqdn) || usage "${name}FQDN not defined";
    my $fqdn2 = $fqdn;
    $fqdn = isFqdn($fqdn) || usage "invalid ${name}FQDN '$fqdn' defined";
    vlog_option("${name}fqdn", $fqdn);
    return $fqdn
}


sub validate_host_port_user_password($$$$){
    return (validate_host($_[0]), validate_port($_[1]), validate_user($_[2]), validate_password($_[3]));
}


sub validate_host ($;$) {
    my $host = shift;
    my $name = shift || "";
    $name = "$name " if $name;
    defined($host) || usage "${name}host not defined";
    $host = isHost($host) || usage "invalid ${name}host '$host' defined: not a valid hostname or IP address";
    vlog_option("${name}host", $host);
    return $host;
}


sub validate_hosts($$){
    my $hosts = shift;
    my $port  = shift;
    $port = isPort($port) or usage "invalid port given";
    defined($hosts) or usage "hosts not defined";
    my @hosts = split(/\s*,\s*/, $hosts);
    @hosts or usage "no hosts defined";
    my $node_port;
    foreach(my $i = 0; $i < scalar @hosts; $i++){
        undef $node_port;
        if($hosts[$i] =~ /:(\d+)$/){
            $node_port = isPort($1) or usage "invalid port given for host " . $i+1;
            $hosts[$i] =~ s/:$node_port$//;
        }
        $hosts[$i]  = validate_host($hosts[$i]);
        $hosts[$i]  = validate_resolvable($hosts[$i]);
        $node_port  = $port unless defined($node_port);
        $hosts[$i] .= ":$node_port";
        vlog_option("port", $node_port);
    }
    return @hosts;
}


sub validate_hostport ($;$) {
    my $hostport      = shift;
    my $name          = shift || "";
    my $port_required = shift;
    my $no_vlog       = shift;
    $name .= " " if $name;
    defined($hostport) || usage "${name}host:port option not defined";
    my ($host, $port) = split(":", $hostport, 2);
    $host = isHost($host) || usage "invalid ${name}host '$host' defined for host:port: not a valid hostname or IP address";
    if($port){
        $port = isPort($port) || usage "invalid ${name}port '$port' defined for host:port: must be a positive integer";
    } elsif($port_required){
        usage "':port' is required for ${name}host:port option";
    }
    $hostport = $host;
    $hostport .= ":$port" if $port;
    vlog_option("${name}host:port", $hostport) unless $no_vlog;
    return $hostport;
}


sub validate_hostname ($;$) {
    my $hostname = shift;
    my $name     = shift || "";
    $name = "$name " if $name;
    defined($hostname) || usage "${name}hostname not defined";
    $hostname = isHostname($hostname) || usage "invalid ${name}hostname defined";
    vlog_option("${name}hostname", $hostname);
    return $hostname;
}


sub validate_int ($$;$$) {
    my ($integer, $name, $min, $max) = @_;
    defined($name) || code_error "name not defined when calling validate_int()";
    defined($integer) || usage "$name not defined";
    isInt($integer, 1) or usage "invalid $name defined: must be an integer";
    if(defined($min)){
        isFloat($min, 1) or code_error "invalid min value '$min' passed to validate_int() for 2nd arg (min value): must be float value";
        $integer < $min and usage "invalid $name defined: cannot be lower than $min";
    }
    if(defined($max)){
        isFloat($max, 1) or code_error "invalid max value '$max' passed to validate_int() for 3rd arg (max value): must be float value";
        $integer > $max and usage "invalid $name defined: cannot be greater than $max";
    }
    $integer =~ /^(-?\d+)$/ or usage "invalid integer $name passed to validate_int() - WARNING: caught LATE code may need updating";
    $integer = $1;
    vlog_option($name, $integer);
    return $integer;
}
*validate_integer = \&validate_int;


sub validate_interface ($) {
    my $interface = shift;
    defined($interface) || usage "interface not defined";
    $interface = isInterface($interface) || usage "invalid interface defined: must be either eth<N>, bond<N> or lo<N>";
    vlog_option("interface", $interface);
    return $interface;
}


sub validate_ip ($;$) {
    my $ip   = shift;
    my $name = shift || "";
    $name   .= " " if $name;
    defined($ip) || usage "${name}IP not defined";
    $ip = isIP($ip) || usage "invalid ${name}IP defined";
    vlog_option("${name}IP", $ip);
    return $ip;
}


sub validate_java_bean ($;$) {
    my $bean = shift;
    my $name = shift || "";
    $name .= " " if $name;
    defined($bean) or usage "java bean not defined";
    $bean = isJavaBean($bean) || usage "invalid ${name}java bean defined";
    vlog_option("${name}java bean", $bean);
    return $bean;
}


sub validate_krb5_princ ($;$) {
    my $principal = shift;
    my $name      = shift || "";
    $name .= " " if $name;
    defined($principal) or usage "krb5 principal not defined";
    $principal = isKrb5Princ($principal) || usage "invalid ${name}krb5 principal defined";
    vlog_option("${name}krb5 principal", $principal);
    return $principal;
}


sub validate_krb5_realm ($;$) {
    my $realm = shift;
    my $name   = shift || "";
    $name .= " " if $name;
    defined($realm) || usage "${name}krb5 realm name not defined";
    $realm = isDomain($realm) || usage "invalid ${name}krb5 realm name defined";
    vlog_option("${name}krb5 realm", $realm);
    return $realm;
}


sub validate_label ($) {
    my $label  = shift;
    defined($label) or usage "label not defined";
    $label = isLabel($label) || usage "invalid label defined: must be an alphanumeric identifier";
    vlog_option("label", $label);
    return $label;
}


sub validate_ldap_dn ($;$) {
    #subtrace(@_);
    my $dn   = shift;
    my $name = shift || "";
    $name .= " " if $name;
    defined($dn) or usage "ldap ${name}dn not defined";
    $dn = isLdapDn($dn) || usage "invalid ldap ${name}dn defined";
    vlog_option("ldap ${name}dn", $dn);
    return $dn;
}


sub validate_metrics ($) {
    my $metrics = shift;
    my @metrics;
    if($metrics){
        foreach(split(/\s*,\s*/, $metrics)){
            $_ = trim($_);
            /^\s*([A-Za-z0-9][\w\.]+[A-Za-z0-9])\s*$/ or usage "invalid metric '$_' given, must be alphanumeric, may contain underscores and dots in the middle";
            push(@metrics, $1);
        }
        @metrics or usage "no valid metrics given";
        @metrics = uniq_array @metrics;
        vlog_option("metrics", "[ " . join(" ", @metrics) . " ]");
    }
    return @metrics;
}


# Takes an array and for any items separated by spaces or commas also splits them into array components to be able to conveniently pass a string and/or arrays mixed together and do the right thing
sub validate_node_list (@) {
    my @nodes = @_;
    @nodes or usage "node(s) not defined";
    my @nodes2;
    foreach(@nodes){
        push(@nodes2, split(/[,\s]+/, $_));
    }
    # do this validate_node_list
    #push(@nodes, @ARGV);
    scalar @nodes2 or usage "node list empty";
    @nodes = uniq_array(@nodes2);
    my $node_count = scalar @nodes;
    foreach (my $i = 0; $i < $node_count; $i++){
        $nodes[$i] = isHost($nodes[$i]) || usage "invalid node name '$nodes[$i]': must be hostname/FQDN or IP address";
    }
    vlog_option("node list", "[ '" . join("', '", @nodes) . "' ]");
    return @nodes;
}


# Takes an array and for any items separated by spaces or commas also splits them into array components to be able to conveniently pass a string and/or arrays mixed together and do the right thing
sub validate_nodeport_list (@) {
    my @nodes = @_;
    @nodes or usage "node(s) not defined";
    my @nodes2;
    foreach(@nodes){
        defined($_) or next;
        push(@nodes2, split(/[,\s]+/, $_));
    }
    scalar @nodes2 or usage "node list empty";
    @nodes = uniq_array2(@nodes2);
    my $node_count = scalar @nodes;
    foreach(my $i = 0; $i < $node_count; $i++){
        $nodes[$i] = validate_hostport($nodes[$i]);
    }
    vlog_option("node list", "[ '" . join("', '", @nodes) . "' ]");
    return @nodes;
}


sub validate_nosql_key($;$){
    my $key  = shift;
    my $name = shift || "";
    $name .= " " if $name;
    defined($key) or usage "${name}key not defined";
    $key = isNoSqlKey($key) || usage "invalid ${name}key name defined: may only contain characters: alphanumeric, commas, colons, underscores, pluses, dashes";
    vlog_option("${name}key", $key);
    return $key;
}


sub validate_port ($;$) {
    my $port = shift;
    my $name = shift || "";
    $name    = "$name " if $name;
    defined($port)         || usage "${name}port not defined";
    $port  = isPort($port) || usage "invalid ${name}port number defined: must be a positive integer";
    vlog_option("${name}port", $port);
    return $port;
}


sub validate_process_name ($;$) {
    my $process = shift;
    my $name    = shift || "";
    $name .= " " if $name;
    defined($process) or usage "${name}process name not defined";
    $process = isProcessName($process) || usage "invalid ${name}process name defined";
    vlog_option("${name}process name", $process);
    return $process;
}


sub validate_program_path ($$;$) {
    my $path  = shift;
    my $name  = shift;
    my $regex = shift() || $name;
    defined($path) or usage "$name program path not defined";
    defined($name) or usage "$path program name not defined";
    if($path !~ /^[\.\/]/){
        $path = which($path);
        unless(defined($path)){
            usage "$name program not found in \$PATH ($ENV{PATH})";
        }
    }
    validate_regex($regex, "program path regex", 1) or code_error "invalid regex given to validate_program_path()";
    $path = validate_filename($path, undef, undef, "no vlog") or usage "invalid path given for $name, failed filename regex";
    $path =~ /(?:^|\/)$regex$/ || usage "invalid path given for $name, is not a path to the $name command";
    ( -f $path ) or usage "$path not found";
    ( -x $path ) or usage "$path not executable";
    vlog_option("${name} program path", $path);
    return $path;
}


# TODO: unify with isRegex and do not allow noquit
sub validate_regex ($;$$$) {
    my $regex  = shift;
    my $name   = shift || "";
    my $noquit = shift;
    my $posix  = shift;
    $name = "${name} " if $name;
    my $regex2;
    if($noquit){
        defined($regex) or return;
    } else {
        defined($regex) or usage "${name}regex not defined";
    }
    if($posix){
        if($regex =~ /\$\(|\`/){
            quit "UNKNOWN", "invalid ${name}posix regex supplied: contains sub shell metachars ( \$( / ` ) that would be dangerous to pass to shell" unless $noquit;
            return;
        } else {
            # XXX: this behaviour is broken in busybox (used in Alpine linux on docker) - it doesn't detect the error in the regex - the validation must be too weak - must install proper grep in that case
            my @output = cmd("egrep '$regex' < /dev/null");
            #if(grep({$_ =~ "Unmatched"} @output)){
            if(@output){
                #quit "UNKNOWN", "invalid posix regex supplied: contains unbalanced () or []" unless $noquit;
                quit "UNKNOWN", "invalid ${name}posix regex defined: @output" unless $noquit;
                return;
            }
        }
    } else {
        #$regex2 = isRegex($regex);
        $regex2 = eval { qr/$regex/ };
        if($@){
            my $errstr = $@;
            $errstr =~ s/;.*?$//;
            $errstr =~ s/in regex m\/.*?$/in regex/;
            quit "UNKNOWN", "invalid ${name}regex defined: $errstr" unless $noquit;
            return;
        }
    }
    if($regex2){
        vlog_option("${name}regex", $regex2) unless $noquit;
        return $regex2;
    } else {
        vlog_option("${name}regex", $regex) unless $noquit;
        return $regex;
    }
}


sub validate_password ($;$$) {
    my $password  = shift;
    my $name      = shift || "";
    my $allow_all = shift;
    $name = "$name " if $name;
    defined($password) or usage "${name}password not defined";
    if($allow_all){
        # intentionally not untaining
        $password =~ /^(.+)$/ || usage "invalid ${name}password defined";
    } else {
        $password =~ /^([^'"`]+)$/ or usage "invalid ${name}password defined: may not contain quotes or backticks";
        $password = $1;
        $password =~ /\$\(/ and usage "invalid ${name}password defined: may not contain \$( as this is a subshell escape and could be dangerous to pass through to programs on the command line";
    }
    vlog_option("${name}password", "<omitted>");
    return $password;
}


sub validate_resolvable($;$){
    my $host = shift;
    my $name = shift || "";
    $name .= " " if $name;
    defined($host) or code_error "${name}host not defined";
    return resolve_ip($host) || quit "CRITICAL", "failed to resolve ${name}host '$host'";
}


sub validate_ssl_opts(){
    if(defined($ssl_noverify)){
        $main::ua->ssl_opts( verify_hostname => 0 );
    }
    if(defined($ssl_ca_path)){
        $ssl_ca_path = validate_directory($ssl_ca_path, "SSL CA directory", undef, "no vlog");
        $main::ua->ssl_opts( SSL_ca_path => $ssl_ca_path );
    }
    if($ssl or $tls){
        vlog_option("SSL CA Path",  $ssl_ca_path) if defined($ssl_ca_path);
        vlog_option("SSL noverify", $ssl_noverify ? "true" : "false");
        $main::protocol = "https" if defined($main::protocol);
    }
}

sub validate_ssl(){
    defined_main_ua();
    $ssl = 1 if(defined($ssl_ca_path) or defined($ssl_noverify));
    if($ssl){
        vlog_option("SSL enabled",  "true");
    }
    validate_ssl_opts();
}

sub validate_tls(){
    defined_main_ua();
    $tls = 1 if(defined($ssl_ca_path) or defined($ssl_noverify));
    if($tls){
        vlog_option("TLS enabled",  "true");
    }
    validate_ssl_opts();
}


sub validate_threshold ($$;$) {
    #subtrace(@_);
    my $name        = shift;
    my $threshold   = shift;
    my $options_ref = shift() || {};
    isHash($options_ref) or code_error "3rd arg to validate_threshold() must be a hash ref of options";
    $options_ref->{"positive"} = 1 unless defined($options_ref->{"positive"});
    $options_ref->{"simple"} = "upper" unless $options_ref->{"simple"};
    my @valid_options = qw/simple positive integer min max/;
    foreach my $option (sort keys %$options_ref){
        grep(/^$option$/, @valid_options) or code_error "invalid option '$option' passed to validate_threshold(), must be one of " . join("/", @valid_options);
    }
    unless ($options_ref->{"simple"} eq "upper" or $options_ref->{"simple"} eq "lower") {
        code_error "simple => '$options_ref->{simple}' option to validate_threshold() must be either 'upper' or 'lower', not '$options_ref->{simple}'";
    }
    #debug("validating $name threshold against $threshold");
    my $invert_range = 0;
    defined($threshold) or code_error "no threshold (arg 2) given to validate_threshold subroutine";
    $thresholds{"$name"}{"invert_range"} = 0;
    # Make this more flexible
    if ($threshold =~ $threshold_range_regex) {
        $thresholds{$name}{"invert_range"} = 1 if $1;
        if(defined($3)){
            $thresholds{$name}{"upper"} = $4 if defined($4);
            $thresholds{$name}{"lower"} = $2;
        } else {
            $thresholds{$name}{"upper"} = $2;
        }
        if(defined($thresholds{$name}{"upper"}) and defined($thresholds{$name}{"lower"})){
            $thresholds{$name}{"upper"} < $thresholds{$name}{"lower"} and usage "invalid args: upper $name threshold cannot be lower than lower $name threshold";
        }
    } elsif($threshold =~ $threshold_simple_regex) {
        if($options_ref->{"simple"} eq "upper"){
            $thresholds{$name}{"upper"} = $1;
        } elsif($options_ref->{"simple"} eq "lower"){
            $thresholds{$name}{"lower"} = $1;
        }
    } else {
        usage "invalid $name threshold given, must be in standard nagios threshold format [@][start:]end";
    }
    foreach(qw/upper lower/){
        if($options_ref->{"positive"} and defined($thresholds{$name}{$_}) and $thresholds{$name}{$_} < 0){
            usage "$name threshold may not be less than zero";
        }
        if($options_ref->{"integer"} and defined($thresholds{$name}{$_}) and not isInt($thresholds{$name}{$_}, 1)){
            usage "$name threshold must be an integer";
        }
        if($options_ref->{"min"} and defined($thresholds{$name}{$_}) and $thresholds{$name}{$_} < $options_ref->{"min"}){
            usage "$name threshold cannot be less than $options_ref->{min}";
        }
        if($options_ref->{"max"} and defined($thresholds{$name}{$_}) and $thresholds{$name}{$_} > $options_ref->{"max"}){
            usage "$name threshold cannot be greater than $options_ref->{max}";
        }
    }
    $thresholds{"defined"} = 1 if (defined($thresholds{$name}{"upper"}) or defined($thresholds{$name}{"lower"}));
    $thresholds{$name}{"range"} = "";
    $thresholds{$name}{"range"} .= $thresholds{$name}{"lower"} if defined($thresholds{$name}{"lower"});
    $thresholds{$name}{"range"} .= ":" if (defined($thresholds{$name}{"lower"}) and defined($thresholds{$name}{"upper"}));
    $thresholds{$name}{"range"}.= $thresholds{$name}{"upper"} if defined($thresholds{$name}{"upper"});
    vlog_option(sprintf("%-8s lower", $name), $thresholds{"$name"}{"lower"}) if defined($thresholds{"$name"}{"lower"});
    vlog_option(sprintf("%-8s upper", $name), $thresholds{"$name"}{"upper"}) if defined($thresholds{"$name"}{"upper"});
    vlog_option(sprintf("%-8s range inversion", $name), "on") if $thresholds{$name}{"invert_range"};
    1;
}


sub validate_thresholds (;$$$$$) {
    # TODO: CRITICAL vs WARNING threshold logic is only applied to simple thresholds, not to range ones, figure out if I can reasonably do range ones later
    my $require_warning  = shift;
    my $require_critical = shift;
    my $options          = shift;
    my $name             = shift() || "";
    my $dual_threshold   = shift;
    my $warning          = $warning;
    my $critical         = $critical;
    if($name){
        $name .= " ";
        if(defined($dual_threshold)){
            ($warning, $critical) = split(",", $dual_threshold, 2);
            if(defined($warning) and not defined($critical)){
                $critical = $warning;
                $warning  = undef;
            }
        } else {
            if($require_warning or $require_critical){
                code_error "no threshold given for $name";
            }
        }
    }
    if($require_warning){
        defined($warning)  || usage "${name}warning threshold not defined";
    }
    if($require_critical){
        defined($critical) || usage "${name}critical threshold not defined";
    }
    # replace $warning and $critical with $name options somehow
    validate_threshold("${name}warning",  $warning,  $options) if(defined($warning));
    validate_threshold("${name}critical", $critical, $options) if(defined($critical));
    # sanity checking on thresholds for simple upper or lower thresholds only
    if(isHash($options) and $options->{"simple"} and $options->{"simple"} eq "lower"){
        if (defined($thresholds{"${name}warning"}{"lower"})
        and defined($thresholds{"${name}critical"}{"lower"})
        and $thresholds{"${name}warning"}{"lower"} < $thresholds{"${name}critical"}{"lower"}){
            usage "${name}warning threshold (" . $thresholds{"${name}warning"}{"lower"} . ") cannot be lower than ${name}critical threshold (" . $thresholds{"${name}critical"}{"lower"} . ") for lower limit thresholds";
        }
    } elsif(isHash($options) and $options->{"simple"} and $options->{"simple"} eq "upper"){
        if (defined($thresholds{"${name}warning"}{"upper"})
        and defined($thresholds{"${name}critical"}{"upper"})
        and $thresholds{"${name}warning"}{"upper"} > $thresholds{"${name}critical"}{"upper"}){
            usage "${name}warning threshold (" . $thresholds{"${name}warning"}{"upper"} . ") cannot be higher than ${name}critical threshold (" . $thresholds{"${name}critical"}{"upper"} . ") for upper limit thresholds";
        }
    }
    1;
}


# Not sure if I can relax the case sensitivity on these according to the Nagios Developer guidelines
sub validate_units ($;$) {
    my $units = shift;
    my $name  = shift || "";
    $name .= " " if $name;
    $units or usage("${name}units not defined");
    $units = isNagiosUnit($units) || usage("invalid ${name}units defined, must be one of: " . join(" ", @valid_units));
    vlog_option("${name}units", $units);
    return $units;
}


sub validate_url ($;$) {
    my $url  = $_[0] if $_[0];
    my $name = $_[1] || "";
    $name .= " " if $name;
    defined($url) or usage "${name}url not defined";
    $url = isUrl($url) || usage "invalid ${name}url defined: '$url'";
    vlog_option("${name}url", $url);
    return $url;
}


sub validate_url_path_suffix ($;$) {
    my $url  = $_[0] if $_[0];
    my $name = $_[1] || "";
    $name .= " " if $name;
    defined($url) or usage "${name}url not defined";
    $url = isUrlPathSuffix($url) || usage "invalid ${name}url defined: '$url'";
    vlog_option("${name}url", $url);
    return $url;
}


sub validate_user ($;$) {
    #subtrace(@_);
    my $user = shift;
    my $name = shift || "";
    $name .= " " if $name;
    defined($user) or usage "${name}username not defined";
    $user = isUser($user) || usage "invalid ${name}username defined: must be alphanumeric";
    vlog_option("${name}user", $user);
    return $user;
}
*validate_username = \&validate_user;


sub validate_user_exists ($;$) {
    #subtrace(@_);
    my $user = shift;
    my $name = shift || "";
    $name .= " " if $name;
    $user = validate_user($user);
    user_exists($user) or usage "invalid ${name}user defined, not found on local system";
    return $user;
}


sub verbose_mode () {
    vlog2("verbose mode on\n");
    vlog3(version_string() . "\n");
    return $verbose >= 1;
}

sub version_string () {
    my $version_str = "";
    $version_str .= "$progname version $main::VERSION  =>  " if defined($progname and $main::VERSION);
    $version_str .= "Hari Sekhon Utils version $HariSekhonUtils::VERSION";
    return $version_str;
}

sub version () {
    defined($main::VERSION) or $main::VERSION = "unset";
    usage version_string();
}


sub vlog (@) {
    if($debug){
        print STDERR strftime("%F %T %z  ", localtime);
    }
    print STDERR "@_\n" if $verbose;
}
sub vlog2 (@) {
    vlog @_ if ($verbose >= 2);
}

sub vlog3 (@) {
    vlog @_ if ($verbose >= 3);
}

sub vlogt (@) {
    vlog tstamp() . "@_";
}

sub vlog2t (@) {
    vlog2 tstamp . "@_";
}

sub vlog3t (@) {
    vlog3 tstamp . "@_";
}

# TODO: check this
# $progname: prefixed
sub vlog4 (@){
    if($verbose){
        foreach(@_){
            foreach (split(/\n/, $_)){
                vlog "$progname\[$$\]: $_";
            }
        }
        1;
    }
}


sub vlog_option ($$) {
    #scalar @_ eq 2 or code_error "incorrect number of args passed to vlog_option()";
    vlog2 sprintf("%-25s %s", "$_[0]:", $_[1]);
}

sub vlog_option_bool ($$) {
    vlog_option $_[0], ( $_[1] ? "true" : "false" );
}


#my %download_tries;
#my %lock_tries;
#sub wget ($$) {
#    require LWP::Simple;
#    import LWP::Simple;
#    my $url        = shift;
#    my $local_file = shift;
#
#    $download_tries{$url}++;
#    $lock_tries{$url} = 0;
#    until(go_flock_yourself){
#        $lock_tries{$url}++;
#        if($lock_tries{$url} > $LOCK_TRY_ATTEMPTS){
#            vlog "Hit max lock attempts on url '$url' ($LOCK_TRY_ATTEMPTS attempts, $LOCK_TRY_INTERVAL secs apart) while waiting for download lock, aborting download...\n";
#            return;
#        }
#        vlog "sleeping for $LOCK_TRY_INTERVAL secs before retrying download lock for url '$url'";
#        sleep $LOCK_TRY_INTERVAL;
#    }
#    vlog "download lock acquired, fetcing '$url' (attempt $download_tries{$url}/$DOWNLOAD_TRIES)";
#    my $rc = mirror($url, $local_file);
#    if ($rc == RC_NOT_MODIFIED){
#        vlog "local file '$local_file' is up to date, not redownloaded";
#        return 1;
#    } elsif(is_success($rc)){
#        vlog "download successful";
#        return 1;
#    } else {
#        vlog "error downloading $url: return code is '$rc'";
#        flock_off;
#        if($download_tries{$url} >= $DOWNLOAD_TRIES){
#            vlog "failed to download url '$url' $DOWNLOAD_TRIES times";
#            return;
#        }
#        vlog "sleeping for $DOWNLOAD_RETRY_INTERVAL secs before trying again";
#        sleep $DOWNLOAD_RETRY_INTERVAL;
#        return wget($url, $local_file);
#    }
#    return;
#    #vlog "fetching $url.md5...";
#    #getstore("$url.md5", "$local_file.md5");
##    if(open(my $fh, "$local_file.md5")){
##        #vlog ".md5 file present, checking md5sum against '$local_file'";
##        $md5sum = do { local $/; <$fh> };
##        chomp $md5sum;
##        my $md5 = md5sum($local_file);
##        if($md5 eq $md5sum){
##            vlog "$local_file .md5 file matched '$md5' == '$md5sum', proceeding...";
##            last;
##        } else {
##            vlog "attempt $tries: $local_file did not match yet, file is '$md5' but .md5 file contains '$md5sum'";
##            wget($url, $local_file);
##        }
##    }
#}


sub which ($;$) {
    my $bin  = $_[0] || code_error "no arg supplied to which() subroutine";
    my $quit = $_[1] || 0;
    $bin = isFilename($bin) || quit "UNKNOWN", "invalid filename '$bin' supplied";
    if($bin =~ /^(?:\/|\.\/)/){
        if(-f $bin){
            if(-x $bin){
                return $bin;
            } else {
                quit "UNKNOWN", "'$bin' is not executable" if $quit;
            }
        } else {
            quit "UNKNOWN", "couldn't find executable '$bin': $!" if $quit;
        }
    } else {
        foreach(split(":", $ENV{"PATH"})){
            (-x "$_/$bin") && return "$_/$bin";
        }
        quit "UNKNOWN", "couldn't find '$bin' in \$PATH ($ENV{PATH})" if $quit;
    }
    return;
}


1;
openSUSE Build Service is sponsored by