#! /usr/bin/perl -w
# nagios: -epn

package Monitoring::GLPlugin::Commandline::Extraopts;
use strict;
use File::Basename;
use strict;

sub new {
  my $class = shift;
  my %params = @_;
  my $self = {
    file => $params{file},
    commandline => $params{commandline},
    config => {},
    section => 'default_no_section',
  };
  bless $self, $class;
  $self->prepare_file_and_section();
  $self->init();
  return $self;
}

sub prepare_file_and_section {
  my $self = shift;
  if (! defined $self->{file}) {
    # ./check_stuff --extra-opts
    $self->{section} = basename($0);
    $self->{file} = $self->get_default_file();
  } elsif ($self->{file} =~ /^[^@]+$/) {
    # ./check_stuff --extra-opts=special_opts
    $self->{section} = $self->{file};
    $self->{file} = $self->get_default_file();
  } elsif ($self->{file} =~ /^@(.*)/) {
    # ./check_stuff --extra-opts=@/etc/myconfig.ini
    $self->{section} = basename($0);
    $self->{file} = $1;
  } elsif ($self->{file} =~ /^(.*?)@(.*)/) {
    # ./check_stuff --extra-opts=special_opts@/etc/myconfig.ini
    $self->{section} = $1;
    $self->{file} = $2;
  }
}

sub get_default_file {
  my $self = shift;
  foreach my $default (qw(/etc/nagios/plugins.ini
      /usr/local/nagios/etc/plugins.ini
      /usr/local/etc/nagios/plugins.ini
      /etc/opt/nagios/plugins.ini
      /etc/nagios-plugins.ini
      /usr/local/etc/nagios-plugins.ini
      /etc/opt/nagios-plugins.ini)) {
    if (-f $default) {
      return $default;
    }
  }
  return undef;
}

sub init {
  my $self = shift;
  if (! defined $self->{file}) {
    $self->{errors} = sprintf 'no extra-opts file specified and no default file found';
  } elsif (! -f $self->{file}) {
    $self->{errors} = sprintf 'could not open %s', $self->{file};
  } else {
    my $data = do { local (@ARGV, $/) = $self->{file}; <> };
    my $in_section = 'default_no_section';
    foreach my $line (split(/\n/, $data)) {
      if ($line =~ /\[(.*)\]/) {
        $in_section = $1;
      } elsif ($line =~ /(.*?)\s*=\s*(.*)/) {
        $self->{config}->{$in_section}->{$1} = $2;
      }
    }
  }
}

sub is_valid {
  my $self = shift;
  return ! exists $self->{errors};
}

sub overwrite {
  my $self = shift;
  if (scalar(keys %{$self->{config}->{default_no_section}}) > 0) {
    foreach (keys %{$self->{config}->{default_no_section}}) {
      $self->{commandline}->{$_} = $self->{config}->{default_no_section}->{$_};
    }
  }
  if (exists $self->{config}->{$self->{section}}) {
    foreach (keys %{$self->{config}->{$self->{section}}}) {
      $self->{commandline}->{$_} = $self->{config}->{$self->{section}}->{$_};
    }
  }
}

sub errors {
  my $self = shift;
  return $self->{errors} || "";
}



package Monitoring::GLPlugin::Commandline::Getopt;
use strict;
use File::Basename;
use Getopt::Long qw(:config no_ignore_case bundling);

# Standard defaults
our %DEFAULT = (
  timeout => 15,
  verbose => 0,
  license =>
"This monitoring plugin is free software, and comes with ABSOLUTELY NO WARRANTY.
It may be used, redistributed and/or modified under the terms of the GNU
General Public Licence (see http://www.fsf.org/licensing/licenses/gpl.txt).",
);
# Standard arguments
our @ARGS = ({
    spec => 'usage|?',
    help => "-?, --usage\n   Print usage information",
  }, {
    spec => 'help|h',
    help => "-h, --help\n   Print detailed help screen",
  }, {
    spec => 'version|V',
    help => "-V, --version\n   Print version information",
  }, {
    #spec => 'extra-opts:s@',
    #help => "--extra-opts=[<section>[@<config_file>]]\n   Section and/or config_file from which to load extra options (may repeat)",
  }, {
    spec => 'timeout|t=i',
    help => sprintf("-t, --timeout=INTEGER\n   Seconds before plugin times out (default: %s)", $DEFAULT{timeout}),
    default => $DEFAULT{timeout},
  }, {
    spec => 'verbose|v+',
    help => "-v, --verbose\n   Show details for command-line debugging (can repeat up to 3 times)",
    default => $DEFAULT{verbose},
  },
);
# Standard arguments we traditionally display last in the help output
our %DEFER_ARGS = map { $_ => 1 } qw(timeout verbose);

sub _init {
  my ($self, %params) = @_;
  # Check params
  my %attr = (
    usage => 1,
    version => 0,
    url => 0,
    plugin => undef,
    blurb => 0,
    extra => 0,
    'extra-opts' => 0,
    license => { default => $DEFAULT{license} },
    timeout => { default => $DEFAULT{timeout} },
  );

  # Add attr to private _attr hash (except timeout)
  $self->{timeout} = delete $attr{timeout};
  $self->{_attr} = { %attr };
  foreach (keys %{$self->{_attr}}) {
    if (exists $params{$_}) {
      $self->{_attr}->{$_} = $params{$_};
    } else {
      $self->{_attr}->{$_} = $self->{_attr}->{$_}->{default}
          if ref ($self->{_attr}->{$_}) eq 'HASH' &&
              exists $self->{_attr}->{$_}->{default};
    }
    chomp $self->{_attr}->{$_} if exists $self->{_attr}->{$_};
  }

  # Setup initial args list
  $self->{_args} = [ grep { exists $_->{spec} } @ARGS ];

  $self
}

sub new {
  my ($class, @params) = @_;
  require Monitoring::GLPlugin::Commandline::Extraopts
      if ! grep /BEGIN/, keys %Monitoring::GLPlugin::Commandline::Extraopts::;
  my $self = bless {}, $class;
  $self->_init(@params);
}

sub decode_rfc3986 {
  my ($self, $password) = @_;
  if ($password && $password =~ /^rfc3986:\/\/(.*)/) {
    $password = $1;
    $password =~ s/%([A-Za-z0-9]{2})/chr(hex($1))/seg;
  }
  return $password;
}

sub add_arg {
  my ($self, %arg) = @_;
  push (@{$self->{_args}}, \%arg);
}

sub mod_arg {
  my ($self, $argname, %arg) = @_;
  foreach my $old_arg (@{$self->{_args}}) {
    next unless $old_arg->{spec} =~ /(\w+).*/ && $argname eq $1;
    foreach my $key (keys %arg) {
      $old_arg->{$key} = $arg{$key};
    }
  }
}

sub getopts {
  my ($self) = @_;
  my %commandline = ();
  $self->{opts}->{all_my_opts} = {};
  my @params = map { $_->{spec} } @{$self->{_args}};
  if (! GetOptions(\%commandline, @params)) {
    $self->print_help();
    exit 3;
  } else {
    no strict 'refs';
    no warnings 'redefine';
    if (exists $commandline{'extra-opts'}) {
      # read the extra file and overwrite other parameters
      my $extras = Monitoring::GLPlugin::Commandline::Extraopts->new(
          file => $commandline{'extra-opts'},
          commandline => \%commandline
      );
      if (! $extras->is_valid()) {
        printf "UNKNOWN - extra-opts are not valid: %s\n", $extras->errors();
        exit 3;
      } else {
        $extras->overwrite();
      }
    }
    do { $self->print_help(); exit 0; } if $commandline{help};
    do { $self->print_version(); exit 0 } if $commandline{version};
    do { $self->print_usage(); exit 3 } if $commandline{usage};
    foreach (map { $_->{spec} =~ /^([\w\-]+)/; $1; } @{$self->{_args}}) {
      my $field = $_;
      *{"$field"} = sub {
        return $self->{opts}->{$field};
      };
    }
    *{"all_my_opts"} = sub {
      return $self->{opts}->{all_my_opts};
    };
    foreach (@{$self->{_args}}) {
      $_->{spec} =~ /^([\w\-]+)/;
      my $spec = $1;
      my $envname = uc $spec;
      $envname =~ s/\-/_/g;
      if (! exists $commandline{$spec}) {
        # Kommandozeile hat oberste Prioritaet
        # Also: --option ueberschreibt NAGIOS__HOSTOPTION
        # Aaaaber: extra-opts haben immer noch Vorrang vor allem anderen.
        # https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
        # beschreibt das anders, Posix-Tools verhalten sich auch entsprechend.
        # Irgendwann wird das hier daher umgeschrieben, so dass extra-opts
        # die niedrigste Prioritaet erhalten.
        if (exists $ENV{'NAGIOS__SERVICE'.$envname}) {
          $commandline{$spec} = $ENV{'NAGIOS__SERVICE'.$envname};
        } elsif (exists $ENV{'NAGIOS__HOST'.$envname}) {
          $commandline{$spec} = $ENV{'NAGIOS__HOST'.$envname};
        }
      }
      $self->{opts}->{$spec} = $_->{default};
    }
    foreach (map { $_->{spec} =~ /^([\w\-]+)/; $1; }
        grep { exists $_->{required} && $_->{required} } @{$self->{_args}}) {
      do { $self->print_usage(); exit 3 } if ! exists $commandline{$_};
    }
    foreach (grep { exists $_->{default} } @{$self->{_args}}) {
      $_->{spec} =~ /^([\w\-]+)/;
      my $spec = $1;
      $self->{opts}->{$spec} = $_->{default};
    }
    foreach (keys %commandline) {
      $self->{opts}->{$_} = $commandline{$_};
      $self->{opts}->{all_my_opts}->{$_} = $commandline{$_};
    }
    foreach (grep { exists $_->{env} } @{$self->{_args}}) {
      $_->{spec} =~ /^([\w\-]+)/;
      my $spec = $1;
      if (exists $ENV{'NAGIOS__HOST'.$_->{env}}) {
        $self->{opts}->{$spec} = $ENV{'NAGIOS__HOST'.$_->{env}};
      }
      if (exists $ENV{'NAGIOS__SERVICE'.$_->{env}}) {
        $self->{opts}->{$spec} = $ENV{'NAGIOS__SERVICE'.$_->{env}};
      }
    }
    foreach (grep { exists $_->{aliasfor} } @{$self->{_args}}) {
      my $field = $_->{aliasfor};
      $_->{spec} =~ /^([\w\-]+)/;
      my $aliasfield = $1;
      next if $self->{opts}->{$field};
      $self->{opts}->{$field} = $self->{opts}->{$aliasfield};
      *{"$field"} = sub {
        return $self->{opts}->{$field};
      };
    }
    foreach (grep { exists $_->{decode} } @{$self->{_args}}) {
      my $decoding = $_->{decode};
      $_->{spec} =~ /^([\w\-]+)/;
      my $spec = $1;
      if (exists $self->{opts}->{$spec}) {
        if ($decoding eq "rfc3986") {
	  $self->{opts}->{$spec} =
	      $self->decode_rfc3986($self->{opts}->{$spec});
	}
      }
    }
  }
}

sub create_opt {
  my ($self, $key) = @_;
  no strict 'refs';
  *{"$key"} = sub {
      return $self->{opts}->{$key};
  };
}

sub override_opt {
  my ($self, $key, $value) = @_;
  $self->{opts}->{$key} = $value;
}

sub get {
  my ($self, $opt) = @_;
  return $self->{opts}->{$opt};
}

sub print_help {
  my ($self) = @_;
  $self->print_version();
  printf "\n%s\n", $self->{_attr}->{license};
  printf "\n%s\n\n", $self->{_attr}->{blurb};
  $self->print_usage();
  foreach (grep {
      ! (exists $_->{hidden} && $_->{hidden}) 
  } @{$self->{_args}}) {
    printf " %s\n", $_->{help};
  }
}

sub print_usage {
  my ($self) = @_;
  printf $self->{_attr}->{usage}, $self->{_attr}->{plugin};
  print "\n";
}

sub print_version {
  my ($self) = @_;
  printf "%s %s", $self->{_attr}->{plugin}, $self->{_attr}->{version};
  printf " [%s]", $self->{_attr}->{url} if $self->{_attr}->{url};
  print "\n";
}

sub print_license {
  my ($self) = @_;
  printf "%s\n", $self->{_attr}->{license};
  print "\n";
}



package Monitoring::GLPlugin::Commandline;
use strict;
use IO::File;
use constant { OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3, DEPENDENT => 4 };
our %ERRORS = (
    'OK'        => OK,
    'WARNING'   => WARNING,
    'CRITICAL'  => CRITICAL,
    'UNKNOWN'   => UNKNOWN,
    'DEPENDENT' => DEPENDENT,
);

our %STATUS_TEXT = reverse %ERRORS;
our $AUTOLOAD;


sub new {
  my ($class, %params) = @_;
  require Monitoring::GLPlugin::Commandline::Getopt
      if ! grep /BEGIN/, keys %Monitoring::GLPlugin::Commandline::Getopt::;
  my $self = {
       perfdata => [],
       messages => {
         ok => [],
         warning => [],
         critical => [],
         unknown => [],
       },
       args => [],
       opts => Monitoring::GLPlugin::Commandline::Getopt->new(%params),
       modes => [],
       statefilesdir => undef,
  };
  foreach (qw(shortname usage version url plugin blurb extra
      license timeout)) {
    $self->{$_} = $params{$_};
  }
  bless $self, $class;
  $self->{name} = $self->{plugin};

  $self
}

sub AUTOLOAD {
  my ($self, @params) = @_;
  return if ($AUTOLOAD =~ /DESTROY/);
  $self->debug("AUTOLOAD %s\n", $AUTOLOAD)
        if $self->{opts}->verbose >= 2;
  if ($AUTOLOAD =~ /^.*::(add_arg|override_opt|create_opt)$/) {
    $self->{opts}->$1(@params);
  }
}

sub DESTROY {
  my ($self) = @_;
  # ohne dieses DESTROY rennt nagios_exit in obiges AUTOLOAD rein
  # und fliegt aufs Maul, weil {opts} bereits nicht mehr existiert.
  # Unerklaerliches Verhalten.
}

sub debug {
  my ($self, $format, @message) = @_;
  if ($self->opts->verbose && $self->opts->verbose > 10) {
    printf("%s: ", scalar localtime);
    printf($format, @message);
    printf "\n";
  }
  if ($Monitoring::GLPlugin::tracefile) {
    my $logfh = IO::File->new();
    $logfh->autoflush(1);
    if ($logfh->open($Monitoring::GLPlugin::tracefile, "a")) {
      $logfh->printf("%s: ", scalar localtime);
      $logfh->printf($format, @message);
      $logfh->printf("\n");
      $logfh->close();
    }
  }
}

sub opts {
  my ($self) = @_;
  return $self->{opts};
}

sub getopts {
  my ($self) = @_;
  $self->opts->getopts();
}

sub add_message {
  my ($self, $code, @messages) = @_;
  $code = (qw(ok warning critical unknown))[$code] if $code =~ /^\d+$/;
  $code = lc $code;
  push @{$self->{messages}->{$code}}, @messages;
}

sub selected_perfdata {
  my ($self, $label) = @_;
  if ($self->opts->can("selectedperfdata") && $self->opts->selectedperfdata) {
    my $pattern = $self->opts->selectedperfdata;
    return ($label =~ /$pattern/i) ? 1 : 0;
  } else {
    return 1;
  }
}

sub add_perfdata {
  my ($self, %args) = @_;
#printf "add_perfdata %s\n", Data::Dumper::Dumper(\%args);
#printf "add_perfdata %s\n", Data::Dumper::Dumper($self->{thresholds});
#
# wenn warning, critical, dann wird von oben ein expliziter wert mitgegeben
# wenn thresholds
#  wenn label in 
#    warningx $self->{thresholds}->{$label}->{warning} existiert
#  dann nimm $self->{thresholds}->{$label}->{warning}
#  ansonsten thresholds->default->warning
#

  my $label = $args{label};
  my $value = $args{value};
  my $uom = $args{uom} || "";
  my $format = '%d';

  if ($self->opts->can("morphperfdata") && $self->opts->morphperfdata) {
    # 'Intel [R] Interface (\d+) usage'='nic$1'
    foreach my $key (keys %{$self->opts->morphperfdata}) {
      if ($label =~ /$key/) {
        my $replacement = '"'.$self->opts->morphperfdata->{$key}.'"';
        my $oldlabel = $label;
        $label =~ s/$key/$replacement/ee;
        if (exists $self->{thresholds}->{$oldlabel}) {
          %{$self->{thresholds}->{$label}} = %{$self->{thresholds}->{$oldlabel}};
        }
      }
    }
  }
  if ($value =~ /\./) {
    if (defined $args{places}) {
      $value = sprintf '%.'.$args{places}.'f', $value;
    } else {
      $value = sprintf "%.2f", $value;
    }
  } else {
    $value = sprintf "%d", $value;
  }
  my $warn = "";
  my $crit = "";
  my $min = defined $args{min} ? $args{min} : "";
  my $max = defined $args{max} ? $args{max} : "";
  if ($args{thresholds} || (! exists $args{warning} && ! exists $args{critical})) {
    if (exists $self->{thresholds}->{$label}->{warning} &&
        defined $self->{thresholds}->{$label}->{warning}) {
      $warn = $self->{thresholds}->{$label}->{warning};
    } elsif (exists $self->{thresholds}->{default}->{warning}) {
      $warn = $self->{thresholds}->{default}->{warning};
    }
    if (exists $self->{thresholds}->{$label}->{critical} &&
        defined $self->{thresholds}->{$label}->{critical}) {
      $crit = $self->{thresholds}->{$label}->{critical};
    } elsif (exists $self->{thresholds}->{default}->{critical}) {
      $crit = $self->{thresholds}->{default}->{critical};
    }
  } else {
    if ($args{warning}) {
      $warn = $args{warning};
    }
    if ($args{critical}) {
      $crit = $args{critical};
    }
  }
  if ($uom eq "%") {
    $min = 0 if $min eq "";
    $max = 100 if $max eq "";
  }
  if (defined $args{places}) {
    # cut off excessive decimals which may be the result of a division
    # length = places*2, no trailing zeroes
    if ($warn ne "") {
      $warn = join("", map {
          s/\.0+$//; $_
      } map {
          s/(\.[1-9]+)0+$/$1/; $_
      } map {
          /[\+\-\d\.]+/ ? sprintf '%.'.2*$args{places}.'f', $_ : $_;
      } split(/([\+\-\d\.]+)/, $warn));
    }
    if ($crit ne "") {
      $crit = join("", map {
          s/\.0+$//; $_
      } map {
          s/(\.[1-9]+)0+$/$1/; $_
      } map {
          /[\+\-\d\.]+/ ? sprintf '%.'.2*$args{places}.'f', $_ : $_;
      } split(/([\+\-\d\.]+)/, $crit));
    }
    if ($min ne "") {
      $min = join("", map {
          s/\.0+$//; $_
      } map {
          s/(\.[1-9]+)0+$/$1/; $_
      } map {
          /[\+\-\d\.]+/ ? sprintf '%.'.2*$args{places}.'f', $_ : $_;
      } split(/([\+\-\d\.]+)/, $min));
    }
    if ($max ne "") {
      $max = join("", map {
          s/\.0+$//; $_
      } map {
          s/(\.[1-9]+)0+$/$1/; $_
      } map {
          /[\+\-\d\.]+/ ? sprintf '%.'.2*$args{places}.'f', $_ : $_;
      } split(/([\+\-\d\.]+)/, $max));
    }
  }
  push @{$self->{perfdata}}, sprintf("'%s'=%s%s;%s;%s;%s;%s",
      $label, $value, $uom, $warn, $crit, $min, $max)
      if $self->selected_perfdata($label);
}

sub add_pandora {
  my ($self, %args) = @_;
  my $label = $args{label};
  my $value = $args{value};

  if ($args{help}) {
    push @{$self->{pandora}}, sprintf("# HELP %s %s", $label, $args{help});
  }
  if ($args{type}) {
    push @{$self->{pandora}}, sprintf("# TYPE %s %s", $label, $args{type});
  }
  if ($args{labels}) {
    push @{$self->{pandora}}, sprintf("%s{%s} %s", $label,
        join(",", map {
            sprintf '%s="%s"', $_, $args{labels}->{$_};
        } keys %{$args{labels}}),
        $value);
  } else {
    push @{$self->{pandora}}, sprintf("%s %s", $label, $value);
  }
}

sub add_html {
  my ($self, $line) = @_;
  push @{$self->{html}}, $line;
}

sub suppress_messages {
  my ($self) = @_;
  $self->{suppress_messages} = 1;
}

sub clear_messages {
  my ($self, $code) = @_;
  $code = (qw(ok warning critical unknown))[$code] if $code =~ /^\d+$/;
  $code = lc $code;
  $self->{messages}->{$code} = [];
}

sub reduce_messages_short {
  my ($self, $message) = @_;
  $message ||= "no problems";
  if ($self->opts->report && $self->opts->report eq "short") {
    $self->clear_messages(OK);
    $self->add_message(OK, $message) if ! $self->check_messages();
  }
}

sub reduce_messages {
  my ($self, $message) = @_;
  $message ||= "no problems";
  $self->clear_messages(OK);
  $self->add_message(OK, $message) if ! $self->check_messages();
}

sub check_messages {
  my ($self, %args) = @_;

  # Add object messages to any passed in as args
  for my $code (qw(critical warning unknown ok)) {
    my $messages = $self->{messages}->{$code} || [];
    if ($args{$code}) {
      unless (ref $args{$code} eq 'ARRAY') {
        if ($code eq 'ok') {
          $args{$code} = [ $args{$code} ];
        }
      }
      push @{$args{$code}}, @$messages;
    } else {
      $args{$code} = $messages;
    }
  }
  my %arg = %args;
  $arg{join} = ' ' unless defined $arg{join};

  # Decide $code
  my $code = OK;
  $code ||= CRITICAL  if @{$arg{critical}};
  $code ||= WARNING   if @{$arg{warning}};
  $code ||= UNKNOWN   if @{$arg{unknown}};
  return $code unless wantarray;

  # Compose message
  my $message = '';
  if ($arg{join_all}) {
      $message = join( $arg{join_all},
          map { @$_ ? join( $arg{'join'}, @$_) : () }
              $arg{critical},
              $arg{warning},
              $arg{unknown},
              $arg{ok} ? (ref $arg{ok} ? $arg{ok} : [ $arg{ok} ]) : []
      );
  }

  else {
      $message ||= join( $arg{'join'}, @{$arg{critical}} )
          if $code == CRITICAL;
      $message ||= join( $arg{'join'}, @{$arg{warning}} )
          if $code == WARNING;
      $message ||= join( $arg{'join'}, @{$arg{unknown}} )
          if $code == UNKNOWN;
      $message ||= ref $arg{ok} ? join( $arg{'join'}, @{$arg{ok}} ) : $arg{ok}
          if $arg{ok};
  }

  return ($code, $message);
}

sub status_code {
  my ($self, $code) = @_;
  $code = (qw(ok warning critical unknown))[$code] if $code =~ /^\d+$/;
  $code = uc $code;
  $code = $ERRORS{$code} if defined $code && exists $ERRORS{$code};
  $code = UNKNOWN unless defined $code && exists $STATUS_TEXT{$code};
  return "$STATUS_TEXT{$code}";
}

sub perfdata_string {
  my ($self) = @_;
  if (scalar (@{$self->{perfdata}})) {
    return join(" ", @{$self->{perfdata}});
  } else {
    return "";
  }
}

sub metrics_string {
  my ($self) = @_;
  if (scalar (@{$self->{metrics}})) {
    return join("\n", @{$self->{metrics}});
  } else {
    return "";
  }
}

sub html_string {
  my ($self) = @_;
  if (scalar (@{$self->{html}})) {
    return join(" ", @{$self->{html}});
  } else {
    return "";
  }
}

sub nagios_exit {
  my ($self, $code, $message, $arg) = @_;
  $code = $ERRORS{$code} if defined $code && exists $ERRORS{$code};
  $code = UNKNOWN unless defined $code && exists $STATUS_TEXT{$code};
  $message = '' unless defined $message;
  if (ref $message && ref $message eq 'ARRAY') {
      $message = join(' ', map { chomp; $_ } @$message);
  } else {
      chomp $message;
  }
  if ($self->opts->negate) {
    my $original_code = $code;
    foreach my $from (keys %{$self->opts->negate}) {
      if ((uc $from) =~ /^(OK|WARNING|CRITICAL|UNKNOWN)$/ &&
          (uc $self->opts->negate->{$from}) =~ /^(OK|WARNING|CRITICAL|UNKNOWN)$/) {
        if ($original_code == $ERRORS{uc $from}) {
          $code = $ERRORS{uc $self->opts->negate->{$from}};
        }
      }
    }
  }
  my $output = "$STATUS_TEXT{$code}";
  $output .= " - $message" if defined $message && $message ne '';
  if ($self->opts->can("morphmessage") && $self->opts->morphmessage) {
    # 'Intel [R] Interface (\d+) usage'='nic$1'
    # '^OK.*'="alles klar"   '^CRITICAL.*'="alles hi"
    foreach my $key (keys %{$self->opts->morphmessage}) {
      if ($output =~ /$key/) {
        my $replacement = '"'.$self->opts->morphmessage->{$key}.'"';
        $output =~ s/$key/$replacement/ee;
      }
    }
  }
  if ($self->opts->negate) {
    # negate again: --negate "UNKNOWN - no peers"=ok
    my $original_code = $code;
    foreach my $from (keys %{$self->opts->negate}) {
      if ((uc $from) !~ /^(OK|WARNING|CRITICAL|UNKNOWN)$/ &&
          (uc $self->opts->negate->{$from}) =~ /^(OK|WARNING|CRITICAL|UNKNOWN)$/) {
        if ($output =~ /$from/) {
          $code = $ERRORS{uc $self->opts->negate->{$from}};
          $output =~ s/^.*? -/$STATUS_TEXT{$code} -/;
        }
      }
    }
  }
  $output =~ s/\|/!/g if $output;
  if (scalar (@{$self->{perfdata}})) {
    $output .= " | ".$self->perfdata_string();
  }
  $output .= "\n";
  if ($self->opts->can("isvalidtime") && ! $self->opts->isvalidtime) {
    $code = OK;
    $output = "OK - outside valid timerange. check results are not relevant now. original message was: ".
        $output;
  }
  if (! exists $self->{suppress_messages}) {
    $output =~ s/[^[:ascii:]]//g;
    print $output;
  }
  exit $code;
}

sub set_thresholds {
  my ($self, %params) = @_;
  if (exists $params{metric}) {
    my $metric = $params{metric};
    # erst die hartcodierten defaultschwellwerte
    $self->{thresholds}->{$metric}->{warning} = $params{warning};
    $self->{thresholds}->{$metric}->{critical} = $params{critical};
    # dann die defaultschwellwerte von der kommandozeile
    if (defined $self->opts->warning) {
      $self->{thresholds}->{$metric}->{warning} = $self->opts->warning;
    }
    if (defined $self->opts->critical) {
      $self->{thresholds}->{$metric}->{critical} = $self->opts->critical;
    }
    # dann die ganz spezifischen schwellwerte von der kommandozeile
    if ($self->opts->warningx) { # muss nicht auf defined geprueft werden, weils ein hash ist
      # Erst schauen, ob einer * beinhaltet. Von denen wird vom Laengsten
      # bis zum Kuerzesten probiert, ob die matchen. Der laengste Match
      # gewinnt.
      my @keys = keys %{$self->opts->warningx};
      my @stringkeys = ();
      my @regexkeys = ();
      foreach my $key (sort { length($b) > length($a) } @keys) {
        if ($key =~ /\*/) {
          push(@regexkeys, $key);
        } else {
          push(@stringkeys, $key);
        }
      }
      foreach my $key (@regexkeys) {
        next if $metric !~ /$key/;
        $self->{thresholds}->{$metric}->{warning} = $self->opts->warningx->{$key};
        last;
      }
      # Anschliessend nochmal schauen, ob es einen nicht-Regex-Volltreffer gibt
      foreach my $key (@stringkeys) {
        next if $key ne $metric;
        $self->{thresholds}->{$metric}->{warning} = $self->opts->warningx->{$key};
        last;
      }
    }
    if ($self->opts->criticalx) {
      my @keys = keys %{$self->opts->criticalx};
      my @stringkeys = ();
      my @regexkeys = ();
      foreach my $key (sort { length($b) > length($a) } @keys) {
        if ($key =~ /\*/) {
          push(@regexkeys, $key);
        } else {
          push(@stringkeys, $key);
        }
      }
      foreach my $key (@regexkeys) {
        next if $metric !~ /$key/;
        $self->{thresholds}->{$metric}->{critical} = $self->opts->criticalx->{$key};
        last;
      }
      # Anschliessend nochmal schauen, ob es einen nicht-Regex-Volltreffer gibt
      foreach my $key (@stringkeys) {
        next if $key ne $metric;
        $self->{thresholds}->{$metric}->{critical} = $self->opts->criticalx->{$key};
        last;
      }
    }
  } else {
    $self->{thresholds}->{default}->{warning} =
        defined $self->opts->warning ? $self->opts->warning : defined $params{warning} ? $params{warning} : 0;
    $self->{thresholds}->{default}->{critical} =
        defined $self->opts->critical ? $self->opts->critical : defined $params{critical} ? $params{critical} : 0;
  }
}

sub force_thresholds {
  my ($self, %params) = @_;
  if (exists $params{metric}) {
    my $metric = $params{metric};
    $self->{thresholds}->{$metric}->{warning} = $params{warning} || 0;
    $self->{thresholds}->{$metric}->{critical} = $params{critical} || 0;
  } else {
    $self->{thresholds}->{default}->{warning} = $params{warning} || 0;
    $self->{thresholds}->{default}->{critical} = $params{critical} || 0;
  }
}

sub get_thresholds {
  my ($self, @params) = @_;
  if (scalar(@params) > 1) {
    my %params = @params;
    my $metric = $params{metric};
    return ($self->{thresholds}->{$metric}->{warning},
        $self->{thresholds}->{$metric}->{critical});
  } else {
    return ($self->{thresholds}->{default}->{warning},
        $self->{thresholds}->{default}->{critical});
  }
}

sub check_thresholds {
  my ($self, @params) = @_;
  my $level = $ERRORS{OK};
  my $warningrange;
  my $criticalrange;
  my $value;
  if (scalar(@params) > 1) {
    my %params = @params;
    $value = $params{value};
    my $metric = $params{metric};
    if ($metric ne 'default') {
      $warningrange = defined $params{warning} ? $params{warning} :
          (exists $self->{thresholds}->{$metric}->{warning} ?
              $self->{thresholds}->{$metric}->{warning} :
              $self->{thresholds}->{default}->{warning});
      $criticalrange = defined $params{critical} ? $params{critical} :
          (exists $self->{thresholds}->{$metric}->{critical} ?
              $self->{thresholds}->{$metric}->{critical} :
              $self->{thresholds}->{default}->{critical});
    } else {
      $warningrange = (defined $params{warning}) ?
          $params{warning} : $self->{thresholds}->{default}->{warning};
      $criticalrange = (defined $params{critical}) ?
          $params{critical} : $self->{thresholds}->{default}->{critical};
    }
  } else {
    $value = $params[0];
    $warningrange = $self->{thresholds}->{default}->{warning};
    $criticalrange = $self->{thresholds}->{default}->{critical};
  }
  if (! defined $warningrange) {
    # there was no set_thresholds for defaults, no --warning, no --warningx
  } elsif ($warningrange =~ /^([-+]?[0-9]*\.?[0-9]+)$/) {
    # warning = 10, warn if > 10 or < 0
    $level = $ERRORS{WARNING}
        if ($value > $1 || $value < 0);
  } elsif ($warningrange =~ /^([-+]?[0-9]*\.?[0-9]+):$/) {
    # warning = 10:, warn if < 10
    $level = $ERRORS{WARNING}
        if ($value < $1);
  } elsif ($warningrange =~ /^~:([-+]?[0-9]*\.?[0-9]+)$/) {
    # warning = ~:10, warn if > 10
    $level = $ERRORS{WARNING}
        if ($value > $1);
  } elsif ($warningrange =~ /^([-+]?[0-9]*\.?[0-9]+):([-+]?[0-9]*\.?[0-9]+)$/) {
    # warning = 10:20, warn if < 10 or > 20
    $level = $ERRORS{WARNING}
        if ($value < $1 || $value > $2);
  } elsif ($warningrange =~ /^@([-+]?[0-9]*\.?[0-9]+):([-+]?[0-9]*\.?[0-9]+)$/) {
    # warning = @10:20, warn if >= 10 and <= 20
    $level = $ERRORS{WARNING}
        if ($value >= $1 && $value <= $2);
  }
  if (! defined $criticalrange) {
    # there was no set_thresholds for defaults, no --critical, no --criticalx
  } elsif ($criticalrange =~ /^([-+]?[0-9]*\.?[0-9]+)$/) {
    # critical = 10, crit if > 10 or < 0
    $level = $ERRORS{CRITICAL}
        if ($value > $1 || $value < 0);
  } elsif ($criticalrange =~ /^([-+]?[0-9]*\.?[0-9]+):$/) {
    # critical = 10:, crit if < 10
    $level = $ERRORS{CRITICAL}
        if ($value < $1);
  } elsif ($criticalrange =~ /^~:([-+]?[0-9]*\.?[0-9]+)$/) {
    # critical = ~:10, crit if > 10
    $level = $ERRORS{CRITICAL}
        if ($value > $1);
  } elsif ($criticalrange =~ /^([-+]?[0-9]*\.?[0-9]+):([-+]?[0-9]*\.?[0-9]+)$/) {
    # critical = 10:20, crit if < 10 or > 20
    $level = $ERRORS{CRITICAL}
        if ($value < $1 || $value > $2);
  } elsif ($criticalrange =~ /^@([-+]?[0-9]*\.?[0-9]+):([-+]?[0-9]*\.?[0-9]+)$/) {
    # critical = @10:20, crit if >= 10 and <= 20
    $level = $ERRORS{CRITICAL}
        if ($value >= $1 && $value <= $2);
  }
  return $level;
}

sub mod_threshold {
  # this method can be used to modify/multiply thresholds or upper and lower
  # limit of a threshold range. For example, we have thresholds for an
  # interface usage together with the maximum bandwidth and want to
  # create thresholds for bitrates.
  my ($self, $threshold, $func) = @_;
  if (! $threshold) {
    return "";
  } elsif ($threshold =~ /^([-+]?[0-9]*\.?[0-9]+)$/) {
    # 10
    return &{$func}($1);
  } elsif ($threshold =~ /^([-+]?[0-9]*\.?[0-9]+):$/) {
    # 10:
    return &{$func}($1).":";
  } elsif ($threshold =~ /^~:([-+]?[0-9]*\.?[0-9]+)$/) {
    # ~:10
    return "~:".&{$func}($1);
  } elsif ($threshold =~ /^([-+]?[0-9]*\.?[0-9]+):([-+]?[0-9]*\.?[0-9]+)$/) {
    # 10:20
    return &{$func}($1).":".&{$func}($2);
  } elsif ($threshold =~ /^@([-+]?[0-9]*\.?[0-9]+):([-+]?[0-9]*\.?[0-9]+)$/) {
    # @10:20
    return "@".&{$func}($1).":".&{$func}($2);
  } else {
    return $threshold."scheise";
  }
}

sub strequal {
  my($self, $str1, $str2) = @_;
  return 1 if ! defined $str1 && ! defined $str2;
  return 0 if ! defined $str1 && defined $str2;
  return 0 if defined $str1 && ! defined $str2;
  return 1 if $str1 eq $str2;
  return 0;
}



package Monitoring::GLPlugin;

=head1 Monitoring::GLPlugin

Monitoring::GLPlugin - infrastructure functions to build a monitoring plugin

=cut

use strict;
use IO::File;
use File::Basename;
use Digest::MD5 qw(md5_hex);
use Errno;
use JSON;
use Sys::Hostname;
use File::Slurp qw(read_file);
use Data::Dumper;
$Data::Dumper::Indent = 1;
eval {
  # avoid "used only once" because older Data::Dumper don't have this
  # use OMD please because OMD has everything!
  no warnings 'all';
  $Data::Dumper::Sparseseen = 1;
};
our $AUTOLOAD;
*VERSION = \'5.53';

use constant { OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3 };

{
  our $mode = undef;
  our $plugin = undef;
  our $pluginname = undef;
  our $blacklist = undef;
  our $info = [];
  our $extendedinfo = [];
  our $summary = [];
  our $variables = {};
  our $survive_sudo_env = ["LD_LIBRARY_PATH", "SHLIB_PATH"];
}

sub new {
  my ($class, %params) = @_;
  my $self = {};
  bless $self, $class;
  require Monitoring::GLPlugin::Commandline
      if ! grep /BEGIN/, keys %Monitoring::GLPlugin::Commandline::;
  require Monitoring::GLPlugin::Item
      if ! grep /BEGIN/, keys %Monitoring::GLPlugin::Item::;
  require Monitoring::GLPlugin::TableItem
      if ! grep /BEGIN/, keys %Monitoring::GLPlugin::TableItem::;
  $params{plugin} ||= basename($ENV{'NAGIOS_PLUGIN'} || $0);
  $Monitoring::GLPlugin::pluginname = $params{plugin};
  $Monitoring::GLPlugin::plugin = Monitoring::GLPlugin::Commandline->new(%params);
  return $self;
}

sub rebless {
  my ($self, $class) = @_;
  bless $self, $class;
  $self->debug('using '.$class);
  # gilt nur fuer "echte" Fabrikate mit "Classes::" vorndran
  $self->{classified_as} = ref($self) if $class !~ /^Monitoring::GLPlugin/;
}

sub init {
  my ($self) = @_;
  if ($self->opts->can("blacklist") && $self->opts->blacklist &&
      -f $self->opts->blacklist) {
    $self->opts->blacklist = do {
        local (@ARGV, $/) = $self->opts->blacklist; <> };
  }
}

sub dumper {
  my ($self, $object) = @_;
  my $run = $object->{runtime};
  delete $object->{runtime};
  printf STDERR "%s\n", Data::Dumper::Dumper($object);
  $object->{runtime} = $run;
}

sub no_such_mode {
  my ($self) = @_;
  $self->nagios_exit(3,
      sprintf "Mode %s is not implemented for this type of device",
      $self->opts->mode
  );
  exit 3;
}

#########################################################
# framework-related. setup, options
#
sub add_default_args {
  my ($self) = @_;
  $self->add_arg(
      spec => 'mode=s',
      help => "--mode
   A keyword which tells the plugin what to do",
      required => 1,
  );
  $self->add_arg(
      spec => 'regexp',
      help => "--regexp
   Parameter name/name2/name3 will be interpreted as (perl) regular expression",
      required => 0,);
  $self->add_arg(
      spec => 'warning=s',
      help => "--warning
   The warning threshold",
      required => 0,);
  $self->add_arg(
      spec => 'critical=s',
      help => "--critical
   The critical threshold",
      required => 0,);
  $self->add_arg(
      spec => 'warningx=s%',
      help => '--warningx
   The extended warning thresholds
   e.g. --warningx db_msdb_free_pct=6: to override the threshold for a
   specific item ',
      required => 0,
  );
  $self->add_arg(
      spec => 'criticalx=s%',
      help => '--criticalx
   The extended critical thresholds',
      required => 0,
  );
  $self->add_arg(
      spec => 'units=s',
      help => "--units
   One of %, B, KB, MB, GB, Bit, KBi, MBi, GBi. (used for e.g. mode interface-usage)",
      required => 0,
  );
  $self->add_arg(
      spec => 'name=s',
      help => "--name
   The name of a specific component to check",
      required => 0,
      decode => "rfc3986",
  );
  $self->add_arg(
      spec => 'name2=s',
      help => "--name2
   The secondary name of a component",
      required => 0,
      decode => "rfc3986",
  );
  $self->add_arg(
      spec => 'name3=s',
      help => "--name3
   The tertiary name of a component",
      required => 0,
      decode => "rfc3986",
  );
  $self->add_arg(
      spec => 'extra-opts=s',
      help => "--extra-opts
   read command line arguments from an external file",
      required => 0,
  );
  $self->add_arg(
      spec => 'blacklist|b=s',
      help => '--blacklist
   Blacklist some (missing/failed) components',
      required => 0,
      default => '',
  );
  $self->add_arg(
      spec => 'mitigation=s',
      help => "--mitigation
   The parameter allows you to change a critical error to a warning.
   It works only for specific checks. Which ones? Try it out or look in the code.
   --mitigation warning ranks an error as warning which by default would be critical.",
      required => 0,
  );
  $self->add_arg(
      spec => 'lookback=s',
      help => "--lookback
   The amount of time you want to look back when calculating average rates.
   Use it for mode interface-errors or interface-usage. Without --lookback
   the time between two runs of check_nwc_health is the base for calculations.
   If you want your checkresult to be based for example on the past hour,
   use --lookback 3600. ",
      required => 0,
  );
  $self->add_arg(
      spec => 'environment|e=s%',
      help => "--environment
   Add a variable to the plugin's environment",
      required => 0,
  );
  $self->add_arg(
      spec => 'negate=s%',
      help => "--negate
   Emulate the negate plugin. --negate warning=critical --negate unknown=critical",
      required => 0,
  );
  $self->add_arg(
      spec => 'morphmessage=s%',
      help => '--morphmessage
   Modify the final output message',
      required => 0,
      decode => "rfc3986",
  );
  $self->add_arg(
      spec => 'morphperfdata=s%',
      help => "--morphperfdata
   The parameter allows you to change performance data labels.
   It's a perl regexp and a substitution.
   Example: --morphperfdata '(.*)ISATAP(.*)'='\$1patasi\$2'",
      required => 0,
      decode => "rfc3986",
  );
  $self->add_arg(
      spec => 'selectedperfdata=s',
      help => "--selectedperfdata
   The parameter allows you to limit the list of performance data. It's a perl regexp.
   Only matching perfdata show up in the output",
      required => 0,
  );
  $self->add_arg(
      spec => 'report=s',
      help => "--report
   Can be used to shorten the output",
      required => 0,
      default => 'long',
  );
  $self->add_arg(
      spec => 'multiline',
      help => '--multiline
   Multiline output',
      required => 0,
  );
  $self->add_arg(
      spec => 'with-mymodules-dyn-dir=s',
      help => "--with-mymodules-dyn-dir
   Add-on modules for the my-modes will be searched in this directory",
      required => 0,
  );
  $self->add_arg(
      spec => 'statefilesdir=s',
      help => '--statefilesdir
   An alternate directory where the plugin can save files',
      required => 0,
      env => 'STATEFILESDIR',
  );
  $self->add_arg(
      spec => 'isvalidtime=i',
      help => '--isvalidtime
   Signals the plugin to return OK if now is not a valid check time',
      required => 0,
      default => 1,
  );
  $self->add_arg(
      spec => 'reset',
      help => "--reset
   remove the state file",
      required => 0,
      hidden => 1,
  );
  $self->add_arg(
      spec => 'runas=s',
      help => "--runas
   run as a different user",
      required => 0,
      hidden => 1,
  );
  $self->add_arg(
      spec => 'shell',
      help => "--shell
   forget what you see",
      required => 0,
      hidden => 1,
  );
  $self->add_arg(
      spec => 'drecksptkdb=s',
      help => "--drecksptkdb
   This parameter must be used instead of --name, because Devel::ptkdb is stealing the latter from the command line",
      aliasfor => "name",
      required => 0,
      hidden => 1,
  );
  $self->add_arg(
      spec => 'tracefile=s',
      help => "--tracefile
   Write debugging-info to this file (if it exists)",
      required => 0,
      hidden => 1,
  );
}

sub add_default_modes {
  my ($self) = @_;
  $self->add_mode(
      internal => 'encode',
      spec => 'encode',
      alias => undef,
      help => 'encode stdin',
      hidden => 1,
  );
  $self->add_mode(
      internal => 'decode',
      spec => 'decode',
      alias => undef,
      help => 'decode stdin or --name',
      hidden => 1,
  );
}

sub add_modes {
  my ($self, $modes) = @_;
  my $modestring = "";
  my @modes = @{$modes};
  my $longest = length ((reverse sort {length $a <=> length $b} map { $_->[1] } @modes)[0]);
  my $format = "       %-".
      (length ((reverse sort {length $a <=> length $b} map { $_->[1] } @modes)[0])).
      "s\t(%s)\n";
  foreach (@modes) {
    $modestring .= sprintf $format, $_->[1], $_->[3];
  }
  $modestring .= sprintf "\n";
  $Monitoring::GLPlugin::plugin->{modestring} = $modestring;
}

sub add_arg {
  my ($self, %args) = @_;
  if ($args{help} =~ /^--mode/) {
    $args{help} .= "\n".$Monitoring::GLPlugin::plugin->{modestring};
  }
  $Monitoring::GLPlugin::plugin->{opts}->add_arg(%args);
}

sub mod_arg {
  my ($self, @arg) = @_;
  $Monitoring::GLPlugin::plugin->{opts}->mod_arg(@arg);
}

sub add_mode {
  my ($self, %args) = @_;
  push(@{$Monitoring::GLPlugin::plugin->{modes}}, \%args);
  my $longest = length ((reverse sort {length $a <=> length $b} map { $_->{spec} } @{$Monitoring::GLPlugin::plugin->{modes}})[0]);
  my $format = "       %-".
      (length ((reverse sort {length $a <=> length $b} map { $_->{spec} } @{$Monitoring::GLPlugin::plugin->{modes}})[0])).
      "s\t(%s)\n";
  $Monitoring::GLPlugin::plugin->{modestring} = "";
  foreach (@{$Monitoring::GLPlugin::plugin->{modes}}) {
    $Monitoring::GLPlugin::plugin->{modestring} .= sprintf $format, $_->{spec}, $_->{help};
  }
  $Monitoring::GLPlugin::plugin->{modestring} .= "\n";
}

sub validate_args {
  my ($self) = @_;
  if ($self->opts->mode =~ /^my-([^\-.]+)/) {
    my $param = $self->opts->mode;
    $param =~ s/\-/::/g;
    $self->add_mode(
        internal => $param,
        spec => $self->opts->mode,
        alias => undef,
        help => 'my extension',
    );
  } elsif ($self->opts->mode eq 'encode') {
    my $input = <>;
    chomp $input;
    $input =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg;
    printf "%s\n", $input;
    exit 0;
  } elsif ($self->opts->mode eq 'decode') {
    if (! -t STDIN) {
      my $input = <>;
      chomp $input;
      $input =~ s/%([A-Za-z0-9]{2})/chr(hex($1))/seg;
      printf "%s\n", $input;
      exit OK;
    } else {
      if ($self->opts->name) {
        my $input = $self->opts->name;
        $input =~ s/%([A-Za-z0-9]{2})/chr(hex($1))/seg;
        printf "%s\n", $input;
        exit OK;
      } else {
        printf "i can't find your encoded statement. use --name or pipe it in my stdin\n";
        exit UNKNOWN;
      }
    }
  } elsif ((! grep { $self->opts->mode eq $_ } map { $_->{spec} } @{$Monitoring::GLPlugin::plugin->{modes}}) &&
      (! grep { $self->opts->mode eq $_ } map { defined $_->{alias} ? @{$_->{alias}} : () } @{$Monitoring::GLPlugin::plugin->{modes}})) {
    printf "UNKNOWN - mode %s\n", $self->opts->mode;
    $self->opts->print_help();
    exit 3;
  }
  if ($self->opts->name && $self->opts->name =~ /(%22)|(%27)/) {
    my $name = $self->opts->name;
    $name =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg;
    $self->override_opt('name', $name);
  }
  $Monitoring::GLPlugin::mode = (
      map { $_->{internal} }
      grep {
         ($self->opts->mode eq $_->{spec}) ||
         ( defined $_->{alias} && grep { $self->opts->mode eq $_ } @{$_->{alias}})
      } @{$Monitoring::GLPlugin::plugin->{modes}}
  )[0];
  if ($self->opts->multiline) {
    $ENV{NRPE_MULTILINESUPPORT} = 1;
  } else {
    $ENV{NRPE_MULTILINESUPPORT} = 0;
  }
  if ($self->opts->can("statefilesdir") && ! $self->opts->statefilesdir) {
    if ($^O =~ /MSWin/) {
      if (defined $ENV{TEMP}) {
        $self->override_opt('statefilesdir', $ENV{TEMP}."/".$Monitoring::GLPlugin::plugin->{name});
      } elsif (defined $ENV{TMP}) {
        $self->override_opt('statefilesdir', $ENV{TMP}."/".$Monitoring::GLPlugin::plugin->{name});
      } elsif (defined $ENV{windir}) {
        $self->override_opt('statefilesdir', File::Spec->catfile($ENV{windir}, 'Temp')."/".$Monitoring::GLPlugin::plugin->{name});
      } else {
        $self->override_opt('statefilesdir', "C:/".$Monitoring::GLPlugin::plugin->{name});
      }
    } elsif (exists $ENV{OMD_ROOT}) {
      $self->override_opt('statefilesdir', $ENV{OMD_ROOT}."/var/tmp/".$Monitoring::GLPlugin::plugin->{name});
    } else {
      $self->override_opt('statefilesdir', "/var/tmp/".$Monitoring::GLPlugin::plugin->{name});
    }
  }
  $Monitoring::GLPlugin::plugin->{statefilesdir} = $self->opts->statefilesdir
      if $self->opts->can("statefilesdir");
  if ($self->opts->can("warningx") && $self->opts->warningx) {
    foreach my $key (keys %{$self->opts->warningx}) {
      $self->set_thresholds(metric => $key,
          warning => $self->opts->warningx->{$key});
    }
  }
  if ($self->opts->can("criticalx") && $self->opts->criticalx) {
    foreach my $key (keys %{$self->opts->criticalx}) {
      $self->set_thresholds(metric => $key,
          critical => $self->opts->criticalx->{$key});
    }
  }
  $self->set_timeout_alarm() if ! $SIG{'ALRM'};
}

sub set_timeout_alarm {
  my ($self, $timeout, $handler) = @_;
  $timeout ||= $self->opts->timeout;
  $handler ||= sub {
    $self->nagios_exit(UNKNOWN,
        sprintf("%s timed out after %d seconds\n",
            $Monitoring::GLPlugin::plugin->{name}, $self->opts->timeout)
    );
  };
  use POSIX ':signal_h';
  if ($^O =~ /MSWin/) {
    local $SIG{'ALRM'} = $handler;
  } else {
    my $mask = POSIX::SigSet->new( SIGALRM );
    my $action = POSIX::SigAction->new(
        $handler, $mask
    );   
    my $oldaction = POSIX::SigAction->new();
    sigaction(SIGALRM ,$action ,$oldaction );
  }    
  alarm(int($timeout)); # 1 second before the global unknown timeout
}

#########################################################
# global helpers
#
sub set_variable {
  my ($self, $key, $value) = @_;
  $Monitoring::GLPlugin::variables->{$key} = $value;
}

sub get_variable {
  my ($self, $key, $fallback) = @_;
  return exists $Monitoring::GLPlugin::variables->{$key} ?
      $Monitoring::GLPlugin::variables->{$key} : $fallback;
}

sub debug {
  my ($self, $format, @message) = @_;
  if ($self->get_variable("verbose") &&
      $self->get_variable("verbose") > $self->get_variable("verbosity", 10)) {
    printf("%s: ", scalar localtime);
    printf($format, @message);
    printf "\n";
  }
  if ($Monitoring::GLPlugin::tracefile) {
    my $logfh = IO::File->new();
    $logfh->autoflush(1);
    if ($logfh->open($Monitoring::GLPlugin::tracefile, "a")) {
      $logfh->printf("%s: ", scalar localtime);
      $logfh->printf($format, @message);
      $logfh->printf("\n");
      $logfh->close();
    }
  }
}

sub filter_namex {
  my ($self, $opt, $name) = @_;
  if ($opt) {
    if ($self->opts->regexp) {
      if ($name =~ /$opt/i) {
        return 1;
      }
    } else {
      if (lc $opt eq lc $name) {
        return 1;
      }
    }
  } else {
    return 1;
  }
  return 0;
}

sub filter_name {
  my ($self, $name) = @_;
  return $self->filter_namex($self->opts->name, $name);
}

sub filter_name2 {
  my ($self, $name) = @_;
  return $self->filter_namex($self->opts->name2, $name);
}

sub filter_name3 {
  my ($self, $name) = @_;
  return $self->filter_namex($self->opts->name3, $name);
}

sub version_is_minimum {
  my ($self, $version) = @_;
  my $installed_version;
  my $newer = 1;
  if ($self->get_variable("version")) {
    $installed_version = $self->get_variable("version");
  } elsif (exists $self->{version}) {
    $installed_version = $self->{version};
  } else {
    return 0;
  }
  my @v1 = map { $_ eq "x" ? 0 : $_ } split(/\./, $version);
  my @v2 = split(/\./, $installed_version);
  if (scalar(@v1) > scalar(@v2)) {
    push(@v2, (0) x (scalar(@v1) - scalar(@v2)));
  } elsif (scalar(@v2) > scalar(@v1)) {
    push(@v1, (0) x (scalar(@v2) - scalar(@v1)));
  }
  foreach my $pos (0..$#v1) {
    if ($v2[$pos] > $v1[$pos]) {
      $newer = 1;
      last;
    } elsif ($v2[$pos] < $v1[$pos]) {
      $newer = 0;
      last;
    }
  }
  return $newer;
}

sub accentfree {
  my ($self, $text) = @_;
  # thanks mycoyne who posted this accent-remove-algorithm
  # http://www.experts-exchange.com/Programming/Languages/Scripting/Perl/Q_23275533.html#a21234612
  my @transformed;
  my %replace = (
    '9a' => 's', '9c' => 'oe', '9e' => 'z', '9f' => 'Y', 'c0' => 'A', 'c1' => 'A',
    'c2' => 'A', 'c3' => 'A', 'c4' => 'A', 'c5' => 'A', 'c6' => 'AE', 'c7' => 'C',
    'c8' => 'E', 'c9' => 'E', 'ca' => 'E', 'cb' => 'E', 'cc' => 'I', 'cd' => 'I',
    'ce' => 'I', 'cf' => 'I', 'd0' => 'D', 'd1' => 'N', 'd2' => 'O', 'd3' => 'O',
    'd4' => 'O', 'd5' => 'O', 'd6' => 'O', 'd8' => 'O', 'd9' => 'U', 'da' => 'U',
    'db' => 'U', 'dc' => 'U', 'dd' => 'Y', 'e0' => 'a', 'e1' => 'a', 'e2' => 'a',
    'e3' => 'a', 'e4' => 'a', 'e5' => 'a', 'e6' => 'ae', 'e7' => 'c', 'e8' => 'e',
    'e9' => 'e', 'ea' => 'e', 'eb' => 'e', 'ec' => 'i', 'ed' => 'i', 'ee' => 'i',
    'ef' => 'i', 'f0' => 'o', 'f1' => 'n', 'f2' => 'o', 'f3' => 'o', 'f4' => 'o',
    'f5' => 'o', 'f6' => 'o', 'f8' => 'o', 'f9' => 'u', 'fa' => 'u', 'fb' => 'u',
    'fc' => 'u', 'fd' => 'y', 'ff' => 'y',
    '8a' => 'S', '8c' => 'CE', '9a' => 's', '9c' => 'oe', '9f' => 'Y', 'a2' => 'o', 'aa' => 'a',
    'b2' => '2', 'b3' => '3', 'b9' => '1', 'bc' => '1/4', 'bd' => '1/2', 'be' => '3/4',
    'c0' => 'A', 'c1' => 'A', 'c2' => 'A', 'c3' => 'A', 'c4' => 'A', 'c5' => 'A', 'c6' => 'AE',
    'c7' => 'C', 'c8' => 'E', 'c9' => 'E', 'ca' => 'E', 'cb' => 'E',
    'cc' => 'I', 'cd' => 'I', 'ce' => 'I', 'cf' => 'I', 'd0' => 'D', 'd1' => 'N',
    'd2' => 'O', 'd3' => 'O', 'd4' => 'O', 'd5' => 'O', 'd6' => 'O',
    'd8' => 'O', 'd9' => 'U', 'da' => 'U', 'db' => 'U', 'dc' => 'U', 'dd' => 'Y',
    'df' => 'ss', 'e0' => 'a', 'e1' => 'a', 'e2' => 'a', 'e3' => 'a', 'e4' => 'a', 'e5' => 'a',
    'e6' => 'ae', 'e7' => 'c', 'e8' => 'e', 'e9' => 'e', 'ea' => 'e', 'eb' => 'e',
    'ec' => 'i', 'ed' => 'i', 'ee' => 'i', 'ef' => 'i', 'f1' => 'n',
    'f2' => 'o', 'f3' => 'o', 'f4' => 'o', 'f5' => 'o', 'f6' => 'o', 'f8' => 'o',
    'f9' => 'u', 'fa' => 'u', 'fb' => 'u', 'fc' => 'u', 'fd' => 'y', 'ff' => 'yy',
  );
  my @letters = split //, $text;;
  for (my $i = 0; $i <= $#letters; $i++) {
    my $hex = sprintf "%x", ord($letters[$i]);
    $letters[$i] = $replace{$hex} if (exists $replace{$hex});
  }
  push @transformed, @letters;
  $text = join '', @transformed;
  $text =~ s/[[:^ascii:]]//g;
  return $text;
}

sub dump {
  my ($self, $indent) = @_;
  $indent = $indent ? " " x $indent : "";
  if ($self->can("internal_name")) {
    printf "%s[%s]\n", $indent, $self->internal_name();
  } else {
    my $class = ref($self);
    $class =~ s/^.*:://;
    printf "%s[%s]\n", $indent, uc $class;
  }
  foreach (grep !/^(info|trace|warning|critical|blacklisted|extendedinfo|flat_indices|indices)$/, sort keys %{$self}) {
    printf "%s%s: %s\n", $indent, $_, $self->{$_} if defined $self->{$_} && ref($self->{$_}) ne "ARRAY";
  }
  if ($self->{info}) {
    printf "%sinfo: %s\n", $indent, $self->{info};
  }
  foreach (grep !/^(info|trace|warning|critical|blacklisted|extendedinfo|flat_indices|indices)$/, sort keys %{$self}) {
    if (defined $self->{$_} && ref($self->{$_}) eq "ARRAY") {
      my $have_flat_indices = 1;
      foreach my $obj (@{$self->{$_}}) {
        $have_flat_indices = 0 if (ref($obj) ne "HASH" || ! exists $obj->{flat_indices});
      }
      if ($have_flat_indices) {
        foreach my $obj (sort {
            join('', map { sprintf("%30d",$_) } split( /\./, $a->{flat_indices})) cmp
            join('', map { sprintf("%30d",$_) } split( /\./, $b->{flat_indices}))
        } @{$self->{$_}}) {
          $obj->dump();
        }
      } else {
        foreach my $obj (@{$self->{$_}}) {
          $obj->dump() if UNIVERSAL::can($obj, "isa") && $obj->can("dump");
        }
      }
    } elsif (defined $self->{$_} && ref($self->{$_}) =~ /^Classes::/) {
      $self->{$_}->dump(2) if UNIVERSAL::can($self->{$_}, "isa") && $self->{$_}->can("dump");
    }
  }
  printf "\n";
}

sub table_ascii {
  my ($self, $table, $titles) = @_;
  my $text = "";
  my $column_length = {};
  my $column = 0;
  foreach (@{$titles}) {
    $column_length->{$column++} = length($_);
  }
  foreach my $tr (@{$table}) {
    @{$tr} = map { ref($_) eq "ARRAY" ? $_->[0] : $_; } @{$tr};
    $column = 0;
    foreach my $td (@{$tr}) {
      if (length($td) > $column_length->{$column}) {
        $column_length->{$column} = length($td);
      }
      $column++;
    }
  }
  $column = 0;
  foreach (@{$titles}) {
    $column_length->{$column} = "%".($column_length->{$column} + 3)."s";
    $column++;
  }
  $column = 0;
  foreach (@{$titles}) {
    $text .= sprintf $column_length->{$column++}, $_;
  }
  $text .= "\n";
  foreach my $tr (@{$table}) {
    $column = 0;
    foreach my $td (@{$tr}) {
      $text .= sprintf $column_length->{$column++}, $td;
    }
    $text .= "\n";
  }
  return $text;
}

sub table_html {
  my ($self, $table, $titles) = @_;
  my $text = "";
  $text .= "<table style=\"border-collapse:collapse; border: 1px solid black;\">";
  $text .= "<tr>";
  foreach (@{$titles}) {
    $text .= sprintf "<th style=\"text-align: left; padding-left: 4px; padding-right: 6px;\">%s</th>", $_;
  }
  $text .= "</tr>";
  foreach my $tr (@{$table}) {
    $text .= "<tr>";
    foreach my $td (@{$tr}) {
      my $class = "statusOK";
      if (ref($td) eq "ARRAY") {
        $class = {
          0 => "statusOK",
          1 => "statusWARNING",
          2 => "statusCRITICAL",
          3 => "statusUNKNOWN",
        }->{$td->[1]};
        $td = $td->[0];
      }
      $text .= sprintf "<td style=\"text-align: left; padding-left: 4px; padding-right: 6px;\" class=\"%s\">%s</td>", $class, $td;
    }
    $text .= "</tr>";
  }
  $text .= "</table>";
  return $text;
}

sub load_my_extension {
  my ($self) = @_;
  if ($self->opts->mode =~ /^my-([^-.]+)/) {
    my $class = $1;
    my $loaderror = undef;
    substr($class, 0, 1) = uc substr($class, 0, 1);
    if (! $self->opts->get("with-mymodules-dyn-dir")) {
      $self->override_opt("with-mymodules-dyn-dir", "");
    }
    my $plugin_name = $Monitoring::GLPlugin::pluginname;
    $plugin_name =~ /check_(.*?)_health/;
    my $deprecated_class = "DBD::".(uc $1)."::Server";
    $plugin_name = "Check".uc(substr($1, 0, 1)).substr($1, 1)."Health";
    foreach my $libpath (split(":", $self->opts->get("with-mymodules-dyn-dir"))) {
      foreach my $extmod (glob $libpath."/".$plugin_name."*.pm") {
        my $stderrvar;
        *SAVEERR = *STDERR;
        open OUT ,'>',\$stderrvar;
        *STDERR = *OUT;
        eval {
          $self->debug(sprintf "loading module %s", $extmod);
          require $extmod;
        };
        *STDERR = *SAVEERR;
        if ($@) {
          $loaderror = $extmod;
          $self->debug(sprintf "failed loading module %s: %s", $extmod, $@);
        }
      }
    }
    my $original_class = ref($self);
    my $original_init = $self->can("init");
    $self->compatibility_class() if $self->can('compatibility_class');
    bless $self, "My$class";
    $self->compatibility_methods() if $self->can('compatibility_methods') &&
        $self->isa($deprecated_class);
    if ($self->isa("Monitoring::GLPlugin")) {
      my $new_init = $self->can("init");
      if ($new_init == $original_init) {
          $self->add_unknown(
              sprintf "Class %s needs an init() method", ref($self));
      } else {
        # now go back to check_*_health.pl where init() will be called
      }
    } else {
      bless $self, $original_class;
      $self->add_unknown(
          sprintf "Class %s is not a subclass of Monitoring::GLPlugin%s",
              "My$class",
              $loaderror ? sprintf " (syntax error in %s?)", $loaderror : "" );
      my ($code, $message) = $self->check_messages(join => ', ', join_all => ', ');
      $self->nagios_exit($code, $message);
    }
  }
}

sub number_of_bits {
  my ($self, $unit) = @_;
  # https://en.wikipedia.org/wiki/Data_rate_units
  my $bits = {
    'bit' => 1,			# Bit per second
    'B' => 8,			# Byte per second, 8 bits per second
    'kbit' => 1000,		# Kilobit per second, 1,000 bits per second
    'kb' => 1000,		# Kilobit per second, 1,000 bits per second
    'Kibit' => 1024,		# Kibibit per second, 1,024 bits per second
    'kB' => 8000,		# Kilobyte per second, 8,000 bits per second
    'KiB' => 8192,		# Kibibyte per second, 1,024 bytes per second
    'Mbit' => 1000000,		# Megabit per second, 1,000,000 bits per second
    'Mb' => 1000000,		# Megabit per second, 1,000,000 bits per second
    'Mibit' => 1048576,		# Mebibit per second, 1,024 kibibits per second
    'MB' => 8000000,		# Megabyte per second, 1,000 kilobytes per second
    'MiB' => 8388608,		# Mebibyte per second, 1,024 kibibytes per second
    'Gbit' => 1000000000,	# Gigabit per second, 1,000 megabits per second
    'Gb' => 1000000000,		# Gigabit per second, 1,000 megabits per second
    'Gibit' => 1073741824,	# Gibibit per second, 1,024 mebibits per second
    'GB' => 8000000000,		# Gigabyte per second, 1,000 megabytes per second
    'GiB' => 8589934592,	# Gibibyte per second, 8192 mebibits per second
    'Tbit' => 1000000000000,	# Terabit per second, 1,000 gigabits per second
    'Tb' => 1000000000000,	# Terabit per second, 1,000 gigabits per second
    'Tibit' => 1099511627776,	# Tebibit per second, 1,024 gibibits per second
    'TB' => 8000000000000,	# Terabyte per second, 1,000 gigabytes per second
    # eigene kreationen
    'Bits' => 1,
    'Bit' => 1,			# Bit per second
    'KB' => 1024,		# Kilobyte (like disk kilobyte)
    'KBi' => 1024,		# -"-
    'MBi' => 1024 * 1024,	# Megabyte (like disk megabyte)
    'GBi' => 1024 * 1024 * 1024, # Gigybate (like disk gigybyte)
  };
  if (exists $bits->{$unit}) {
    return $bits->{$unit};
  } else {
    return 0;
  }
}


#########################################################
# runtime methods
#
sub mode : lvalue {
  my ($self) = @_;
  $Monitoring::GLPlugin::mode;
}

sub statefilesdir {
  my ($self) = @_;
  return $Monitoring::GLPlugin::plugin->{statefilesdir};
}

sub opts { # die beiden _nicht_ in AUTOLOAD schieben, das kracht!
  my ($self) = @_;
  return $Monitoring::GLPlugin::plugin->opts();
}

sub getopts {
  my ($self, $envparams) = @_;
  $envparams ||= [];
  my $needs_restart = 0;
  my @restart_opts = ();
  $Monitoring::GLPlugin::plugin->getopts();
  # es kann sein, dass beim aufraeumen zum schluss als erstes objekt
  # das $Monitoring::GLPlugin::plugin geloescht wird. in anderen destruktoren
  # (insb. fuer dbi disconnect) steht dann $self->opts->verbose
  # nicht mehr zur verfuegung bzw. $Monitoring::GLPlugin::plugin->opts ist undef.
  $self->set_variable("verbose", $self->opts->verbose);
  $Monitoring::GLPlugin::tracefile = $self->opts->tracefile ?
      $self->opts->tracefile :
      $self->system_tmpdir()."/".$Monitoring::GLPlugin::pluginname.".trace";
  if (! -f $Monitoring::GLPlugin::tracefile) {
    $Monitoring::GLPlugin::tracefile = undef;
  }
  #
  # die gueltigkeit von modes wird bereits hier geprueft und nicht danach
  # in validate_args. (zwischen getopts und validate_args wird
  # normalerweise classify aufgerufen, welches bereits eine verbindung
  # zum endgeraet herstellt. bei falschem mode waere das eine verschwendung
  # bzw. durch den exit3 ein evt. unsauberes beenden der verbindung.
  if ((! grep { $self->opts->mode eq $_ } map { $_->{spec} } @{$Monitoring::GLPlugin::plugin->{modes}}) &&
      (! grep { $self->opts->mode eq $_ } map { defined $_->{alias} ? @{$_->{alias}} : () } @{$Monitoring::GLPlugin::plugin->{modes}})) {
    if ($self->opts->mode !~ /^my-/) {
      printf "UNKNOWN - mode %s\n", $self->opts->mode;
      $self->opts->print_help();
      exit 3;
    }
  }
  if ($self->opts->environment) {
    # wenn die gewuenschten Environmentvariablen sich von den derzeit
    # gesetzten unterscheiden, dann restart. Denn $ENV aendert
    # _nicht_ das Environment des laufenden Prozesses. 
    # $ENV{ZEUGS} = 1 bedeutet lediglich, dass $ENV{ZEUGS} bei weiterer
    # Verwendung 1 ist, bedeutet aber _nicht_, dass diese Variable 
    # im Environment des laufenden Prozesses existiert.
    foreach (keys %{$self->opts->environment}) {
      if ((! $ENV{$_}) || ($ENV{$_} ne $self->opts->environment->{$_})) {
        $needs_restart = 1;
        $ENV{$_} = $self->opts->environment->{$_};
        $self->debug(sprintf "new %s=%s forces restart\n", $_, $ENV{$_});
      }
    }
  }
  if ($self->opts->runas) {
    # exec sudo $0 ... und dann ohne --runas
    $needs_restart = 1;
    # wenn wir environmentvariablen haben, die laut survive_sudo_env als
    # wichtig erachtet werden, dann muessen wir die ueber einen moeglichen
    # sudo-aufruf rueberretten, also in zusaetzliche --environment umwandenln.
    # sudo putzt das Environment naemlich aus.
    foreach my $survive_env (@{$Monitoring::GLPlugin::survive_sudo_env}) {
      if ($ENV{$survive_env} && ! scalar(grep { /^$survive_env=/ }
          keys %{$self->opts->environment})) {
        $self->opts->environment->{$survive_env} = $ENV{$survive_env};
        printf STDERR "add important --environment %s=%s\n",
            $survive_env, $ENV{$survive_env} if $self->opts->verbose >= 2;
        push(@restart_opts, '--environment');
        push(@restart_opts, sprintf '%s=%s',
            $survive_env, $ENV{$survive_env});
      }
    }
  }
  if ($needs_restart) {
    foreach my $option (keys %{$self->opts->all_my_opts}) {
      # der fliegt raus, sonst gehts gleich wieder in needs_restart rein
      next if $option eq "runas";
      foreach my $spec (map { $_->{spec} } @{$Monitoring::GLPlugin::plugin->opts->{_args}}) {
        if ($spec =~ /^([\-\w]+)[\?\+:\|\w+]*=(.*)/) {
          if ($1 eq $option && $2 =~ /s%/) {
            foreach (keys %{$self->opts->$option()}) {
              push(@restart_opts, sprintf "--%s", $option);
              push(@restart_opts, sprintf "%s=%s", $_, $self->opts->$option()->{$_});
            }
          } elsif ($1 eq $option) {
            push(@restart_opts, sprintf "--%s", $option);
            push(@restart_opts, sprintf "%s", $self->opts->$option());
          }
        } elsif ($spec eq $option) {
          push(@restart_opts, sprintf "--%s", $option);
        }
      }
    }
    if ($self->opts->runas && ($> == 0)) {
      # Ja, es gibt so Narrische, die gehen mit check_by_ssh als root
      # auf Datenbankmaschinen drauf und lassen dann dort check_oracle_health
      # laufen. Damit OPS$-Anmeldung dann funktioniert, wird mit --runas
      # auf eine andere Kennung umgeschwenkt. Diese Kennung gleich fuer
      # ssh zu verwenden geht aus Sicherheitsgruenden nicht. Narrische halt.
      exec "su", "-c", sprintf("%s %s", $0, join(" ", @restart_opts)), "-", $self->opts->runas;
    } elsif ($self->opts->runas) {
      exec "sudo", "-S", "-u", $self->opts->runas, $0, @restart_opts;
    } else {
      exec $0, @restart_opts;
      # dadurch werden SHLIB oder LD_LIBRARY_PATH sauber gesetzt, damit beim
      # erneuten Start libclntsh.so etc. gefunden werden.
    }
    exit;
  }
  if ($self->opts->shell) {
    # So komme ich bei den Narrischen zu einer root-Shell.
    system("/bin/sh");
  }
}


sub add_ok {
  my ($self, $message) = @_;
  $message ||= $self->{info};
  $self->add_message(OK, $message);
}

sub add_warning {
  my ($self, $message) = @_;
  $message ||= $self->{info};
  $self->add_message(WARNING, $message);
}

sub add_critical {
  my ($self, $message) = @_;
  $message ||= $self->{info};
  $self->add_message(CRITICAL, $message);
}

sub add_unknown {
  my ($self, $message) = @_;
  $message ||= $self->{info};
  $self->add_message(UNKNOWN, $message);
}

sub add_ok_mitigation {
  my ($self, $message) = @_;
  if (defined $self->opts->mitigation()) {
    $self->add_message($self->opts->mitigation(), $message);
  } else {
    $self->add_ok($message);
  }
}

sub add_warning_mitigation {
  my ($self, $message) = @_;
  if (defined $self->opts->mitigation()) {
    $self->add_message($self->opts->mitigation(), $message);
  } else {
    $self->add_warning($message);
  }
}

sub add_critical_mitigation {
  my ($self, $message) = @_;
  if (defined $self->opts->mitigation()) {
    $self->add_message($self->opts->mitigation(), $message);
  } else {
    $self->add_critical($message);
  }
}

sub add_unknown_mitigation {
  my ($self, $message) = @_;
  if (defined $self->opts->mitigation()) {
    $self->add_message($self->opts->mitigation(), $message);
  } else {
    $self->add_unknown($message);
  }
}

sub add_message {
  my ($self, $level, $message) = @_;
  $message ||= $self->{info};
  $Monitoring::GLPlugin::plugin->add_message($level, $message)
      unless $self->is_blacklisted();
  if (exists $self->{failed}) {
    if ($level == UNKNOWN && $self->{failed} == OK) {
      $self->{failed} = $level;
    } elsif ($level > $self->{failed}) {
      $self->{failed} = $level;
    }
  }
}

sub clear_ok {
  my ($self) = @_;
  $self->clear_messages(OK);
}

sub clear_warning {
  my ($self) = @_;
  $self->clear_messages(WARNING);
}

sub clear_critical {
  my ($self) = @_;
  $self->clear_messages(CRITICAL);
}

sub clear_unknown {
  my ($self) = @_;
  $self->clear_messages(UNKNOWN);
}

sub clear_all { # deprecated, use clear_messages
  my ($self) = @_;
  $self->clear_ok();
  $self->clear_warning();
  $self->clear_critical();
  $self->clear_unknown();
}

sub set_level {
  my ($self, $code) = @_;
  $code = (qw(ok warning critical unknown))[$code] if $code =~ /^\d+$/;
  $code = lc $code;
  if (! exists $self->{tmp_level}) {
    $self->{tmp_level} = {
      ok => 0,
      warning => 0,
      critical => 0,
      unknown => 0,
    };
  }
  $self->{tmp_level}->{$code}++;
}

sub get_level {
  my ($self) = @_;
  return OK if ! exists $self->{tmp_level};
  my $code = OK;
  return CRITICAL if $self->{tmp_level}->{critical};
  return WARNING  if $self->{tmp_level}->{warning};
  return UNKNOWN  if $self->{tmp_level}->{unknown};
  return $code;
}

sub worst_level {
  my ($self, @levels) = @_;
  my $level = 0;
  foreach (@levels) {
    if ($_ == 2) {
      $level = 2;
    } elsif ($_ == 1) {
      if ($level == 0 || $level == 3) {
        $level = 1;
      }
    } elsif ($_ == 3) {
      if ($level == 0) {
        $level = 3;
      }
    }
  }
  return $level;
}

#########################################################
# blacklisting
#
sub blacklist {
  my ($self) = @_;
  $self->{blacklisted} = 1;
}

sub add_blacklist {
  my ($self, $list) = @_;
  $Monitoring::GLPlugin::blacklist = join('/',
      (split('/', $self->opts->blacklist), $list));
}

sub is_blacklisted {
  my ($self) = @_;
  if (! $self->opts->can("blacklist")) {
    return 0;
  }
  if (! exists $self->{blacklisted}) {
    $self->{blacklisted} = 0;
  }
  if (exists $self->{blacklisted} && $self->{blacklisted}) {
    return $self->{blacklisted};
  }
  # FAN:459,203/TEMP:102229/ENVSUBSYSTEM
  # FAN_459,FAN_203,TEMP_102229,ENVSUBSYSTEM
  # ALERT:(The Storage Center is not able to access Tiebreaker)/TEMP:102229
  if ($self->opts->blacklist =~ /_/) {
    foreach my $bl_item (split(/,/, $self->opts->blacklist)) {
      if ($bl_item eq $self->internal_name()) {
        $self->{blacklisted} = 1;
      }
    }
  } else {
    foreach my $bl_items (split(/\//, $self->opts->blacklist)) {
      if ($bl_items =~ /^(\w+):([\:\d\-\.,]+)$/) {
        my $bl_type = $1;
        my $bl_names = $2;
        foreach my $bl_name (split(/,/, $bl_names)) {
          if ($bl_type."_".$bl_name eq $self->internal_name()) {
            $self->{blacklisted} = 1;
          }
        }
      } elsif ($bl_items =~ /^(\w+):\((.*)\)$/ and $self->can("internal_content")) {
        my $bl_type = $1;
        my $bl_pattern = qr/$2/;
        if ($self->internal_name() =~ /^${bl_type}_/) {
          if ($self->internal_content() =~ /$bl_pattern/) {
            $self->{blacklisted} = 1;
          }
        }
      } elsif ($bl_items =~ /^(\w+)$/) {
        if ($bl_items eq $self->internal_name()) {
          $self->{blacklisted} = 1;
        }
      }
    }
  }
  return $self->{blacklisted};
}

#########################################################
# additional info
#
sub add_info {
  my ($self, $info) = @_;
  $info = $self->is_blacklisted() ? $info.' (blacklisted)' : $info;
  $self->{info} = $info;
  push(@{$Monitoring::GLPlugin::info}, $info);
}

sub annotate_info {
  my ($self, $annotation) = @_;
  my $lastinfo = pop(@{$Monitoring::GLPlugin::info});
  $lastinfo .= sprintf ' (%s)', $annotation;
  $self->{info} = $lastinfo;
  push(@{$Monitoring::GLPlugin::info}, $lastinfo);
}

sub add_extendedinfo {  # deprecated
  my ($self, $info) = @_;
  $self->{extendedinfo} = $info;
  return if ! $self->opts->extendedinfo;
  push(@{$Monitoring::GLPlugin::extendedinfo}, $info);
}

sub get_info {
  my ($self, $separator) = @_;
  $separator ||= ' ';
  return join($separator , @{$Monitoring::GLPlugin::info});
}

sub get_last_info {
  my ($self) = @_;
  return pop(@{$Monitoring::GLPlugin::info});
}

sub get_extendedinfo {
  my ($self, $separator) = @_;
  $separator ||= ' ';
  return join($separator, @{$Monitoring::GLPlugin::extendedinfo});
}

sub add_summary {  # deprecated
  my ($self, $summary) = @_;
  push(@{$Monitoring::GLPlugin::summary}, $summary);
}

sub get_summary {
  my ($self) = @_;
  return join(', ', @{$Monitoring::GLPlugin::summary});
}

#########################################################
# persistency
#
sub valdiff {
  my ($self, $pparams, @keys) = @_;
  my %params = %{$pparams};
  my $now = time;
  my $newest_history_set = {};
  $params{freeze} = 0 if ! $params{freeze};
  my $mode = "normal";
  if ($self->opts->lookback && $self->opts->lookback == 99999 && $params{freeze} == 0) {
    $mode = "lookback_freeze_chill";
  } elsif ($self->opts->lookback && $self->opts->lookback == 99999 && $params{freeze} == 1) {
    $mode = "lookback_freeze_shockfrost";
  } elsif ($self->opts->lookback && $self->opts->lookback == 99999 && $params{freeze} == 2) {
    $mode = "lookback_freeze_defrost";
  } elsif ($self->opts->lookback) {
    $mode = "lookback";
  }
  # lookback=99999, freeze=0(default)
  #  nimm den letzten lauf und schreib ihn nach {cold}
  #  vergleich dann
  #    wenn es frozen gibt, vergleich frozen und den letzten lauf
  #    sonst den letzten lauf und den aktuellen lauf
  # lookback=99999, freeze=1
  #  wird dann aufgerufen,wenn nach dem freeze=0 ein problem festgestellt wurde
  #     (also als 2.valdiff hinterher)
  #  schreib cold nach frozen
  # lookback=99999, freeze=2
  #  wird dann aufgerufen,wenn nach dem freeze=0 wieder alles ok ist
  #     (also als 2.valdiff hinterher)
  #  loescht frozen
  #
  my $last_values = $self->load_state(%params) || eval {
    my $empty_events = {};
    foreach (@keys) {
      if (ref($self->{$_}) eq "ARRAY") {
        $empty_events->{$_} = [];
      } else {
        $empty_events->{$_} = 0;
      }
    }
    $empty_events->{timestamp} = 0;
    if ($mode eq "lookback") {
      $empty_events->{lookback_history} = {};
    } elsif ($mode eq "lookback_freeze_chill") {
      $empty_events->{cold} = {};
      $empty_events->{frozen} = {};
    }
    $empty_events;
  };
  $self->{'delta_timestamp'} = $now - $last_values->{timestamp};
  foreach (@keys) {
    if ($mode eq "lookback_freeze_chill") {
      # die werte vom letzten lauf wegsichern.
      # vielleicht gibts gleich einen freeze=1, dann muessen die eingefroren werden
      if (exists $last_values->{$_}) {
        if (ref($self->{$_}) eq "ARRAY") {
          $last_values->{cold}->{$_} = [];
          foreach my $value (@{$last_values->{$_}}) {
            push(@{$last_values->{cold}->{$_}}, $value);
          }
        } else {
          $last_values->{cold}->{$_} = $last_values->{$_};
        }
      } else {
        if (ref($self->{$_}) eq "ARRAY") {
          $last_values->{cold}->{$_} = [];
        } else {
          $last_values->{cold}->{$_} = 0;
        }
      }
      # es wird so getan, als sei der frozen wert vom letzten lauf
      if (exists $last_values->{frozen}->{$_}) {
        if (ref($self->{$_}) eq "ARRAY") {
          $last_values->{$_} = [];
          foreach my $value (@{$last_values->{frozen}->{$_}}) {
            push(@{$last_values->{$_}}, $value);
          }
        } else {
          $last_values->{$_} = $last_values->{frozen}->{$_};
        }
      }
    } elsif ($mode eq "lookback") {
      # find a last_value in the history which fits lookback best
      # and overwrite $last_values->{$_} with historic data
      if (exists $last_values->{lookback_history}->{$_}) {
        foreach my $date (sort {$a <=> $b} keys %{$last_values->{lookback_history}->{$_}}) {
            $newest_history_set->{$_} = $last_values->{lookback_history}->{$_}->{$date};
            $newest_history_set->{timestamp} = $date;
        }
        foreach my $date (sort {$a <=> $b} keys %{$last_values->{lookback_history}->{$_}}) {
          if ($date >= ($now - $self->opts->lookback)) {
            $last_values->{$_} = $last_values->{lookback_history}->{$_}->{$date};
            $last_values->{timestamp} = $date;
            $self->{'delta_timestamp'} = $now - $last_values->{timestamp};
            if (ref($last_values->{$_}) eq "ARRAY") {
              $self->debug(sprintf "oldest value of %s within lookback is size %s (age %d)",
                  $_, scalar(@{$last_values->{$_}}), $now - $date);
            } else {
              $self->debug(sprintf "oldest value of %s within lookback is %s (age %d)",
                  $_, $last_values->{$_}, $now - $date);
            }
            last;
          } else {
            $self->debug(sprintf "deprecate %s of age %d", $_, time - $date);
            delete $last_values->{lookback_history}->{$_}->{$date};
          }
        }
      }
    }
    if ($mode eq "normal" || $mode eq "lookback" || $mode eq "lookback_freeze_chill") {
      if (exists $self->{$_} && defined $self->{$_} && $self->{$_} =~ /^\d+\.*\d*$/) {
        # $VAR1 = { 'sysStatTmSleepCycles' => '',
        # no idea why this happens, but we can repair it.
        $last_values->{$_} = $self->{$_} if ! (exists $last_values->{$_} && defined $last_values->{$_} && $last_values->{$_} ne "");
        if ($self->{$_} >= $last_values->{$_}) {
          $self->{'delta_'.$_} = $self->{$_} - $last_values->{$_};
        } elsif ($self->{$_} eq $last_values->{$_}) {
          # dawischt! in einem fall wurde 131071.999023438 >= 131071.999023438 da oben nicht erkannt
          # subtrahieren ging auch daneben, weil ein winziger negativer wert rauskam.
          $self->{'delta_'.$_} = 0;
        } else {
          if ($mode =~ /lookback_freeze/) {
            # hier koennen delta-werte auch negativ sein, wenn z.b. peers verschwinden
            $self->{'delta_'.$_} = $self->{$_} - $last_values->{$_};
          } elsif (exists $params{lastarray}) {
            $self->{'delta_'.$_} = $self->{$_} - $last_values->{$_};
          } else {
            # vermutlich db restart und zaehler alle auf null
            $self->{'delta_'.$_} = $self->{$_};
          }
        }
        $self->debug(sprintf "delta_%s %f", $_, $self->{'delta_'.$_});
        $self->{$_.'_per_sec'} = $self->{'delta_timestamp'} ?
            $self->{'delta_'.$_} / $self->{'delta_timestamp'} : 0;
      } elsif (ref($self->{$_}) eq "ARRAY") {
        if ((! exists $last_values->{$_} || ! defined $last_values->{$_}) && exists $params{lastarray}) {
          # innerhalb der lookback-zeit wurde nichts in der lookback_history
          # gefunden. allenfalls irgendwas aelteres. normalerweise
          # wuerde jetzt das array als [] initialisiert.
          # d.h. es wuerde ein delta geben, @found s.u.
          # wenn man das nicht will, sondern einfach aktuelles array mit
          # dem array des letzten laufs vergleichen will, setzt man lastarray
          $last_values->{$_} = %{$newest_history_set} ?
              $newest_history_set->{$_} : []
        } elsif ((! exists $last_values->{$_} || ! defined $last_values->{$_}) && ! exists $params{lastarray}) {
          $last_values->{$_} = [] if ! exists $last_values->{$_};
        } elsif (exists $last_values->{$_} && ! defined $last_values->{$_}) {
          # $_ kann es auch ausserhalb des lookback_history-keys als normalen
          # key geben. der zeigt normalerweise auf den entspr. letzten
          # lookback_history eintrag. wurde der wegen ueberalterung abgeschnitten
          # ist der hier auch undef.
          $last_values->{$_} = %{$newest_history_set} ?
              $newest_history_set->{$_} : []
        }
        my %saved = map { $_ => 1 } @{$last_values->{$_}};
        my %current = map { $_ => 1 } @{$self->{$_}};
        my @found = grep(!defined $saved{$_}, @{$self->{$_}});
        my @lost = grep(!defined $current{$_}, @{$last_values->{$_}});
        $self->{'delta_found_'.$_} = \@found;
        $self->{'delta_lost_'.$_} = \@lost;
      } else {
        # nicht ganz sauber, aber das artet aus, wenn man jedem uninitialized hinterherstochert.
        # wem das nicht passt, der kann gerne ein paar tage debugging beauftragen.
        # das kostet aber mehr als drei kugeln eis.
        $last_values->{$_} = 0 if ! (exists $last_values->{$_} && defined $last_values->{$_} && $last_values->{$_} ne "");
        $self->{$_} = 0 if ! (exists $self->{$_} && defined $self->{$_} && $self->{$_} ne "");
        $self->{'delta_'.$_} = 0;
      }
    }
  }
  $params{save} = eval {
    my $empty_events = {};
    foreach (@keys) {
      $empty_events->{$_} = $self->{$_};
      if ($mode =~ /lookback_freeze/) {
        if (exists $last_values->{frozen}->{$_}) {
          if (ref($last_values->{frozen}->{$_}) eq "ARRAY") {
            @{$empty_events->{cold}->{$_}} = @{$last_values->{frozen}->{$_}};
          } else {
            $empty_events->{cold}->{$_} = $last_values->{frozen}->{$_};
          }
        } else {
          if (ref($last_values->{cold}->{$_}) eq "ARRAY") {
            @{$empty_events->{cold}->{$_}} = @{$last_values->{cold}->{$_}};
          } else {
            $empty_events->{cold}->{$_} = $last_values->{cold}->{$_};
          }
        }
        $empty_events->{cold}->{timestamp} = $last_values->{cold}->{timestamp};
      }
      if ($mode eq "lookback_freeze_shockfrost") {
        if (ref($empty_events->{cold}->{$_}) eq "ARRAY") {
          @{$empty_events->{frozen}->{$_}} = @{$empty_events->{cold}->{$_}};
        } else {
          $empty_events->{frozen}->{$_} = $empty_events->{cold}->{$_};
        }
        $empty_events->{frozen}->{timestamp} = $now;
      }
    }
    $empty_events->{timestamp} = $now;
    if ($mode eq "lookback") {
      $empty_events->{lookback_history} = $last_values->{lookback_history};
      foreach (@keys) {
        if (ref($self->{$_}) eq "ARRAY") {
          @{$empty_events->{lookback_history}->{$_}->{$now}} = @{$self->{$_}};
        } else {
          $empty_events->{lookback_history}->{$_}->{$now} = $self->{$_};
        }
      }
    }
    if ($mode eq "lookback_freeze_defrost") {
      delete $empty_events->{freeze};
    }
    $empty_events;
  };
  $self->save_state(%params);
}

sub create_statefilesdir {
  my ($self) = @_;
  if (! -d $self->statefilesdir()) {
    eval {
      use File::Path;
      mkpath $self->statefilesdir();
    };
    if ($@ || ! -w $self->statefilesdir()) {
      $self->add_message(UNKNOWN,
        sprintf "cannot create status dir %s! check your filesystem (permissions/usage/integrity) and disk devices", $self->statefilesdir());
    }
  } elsif (! -w $self->statefilesdir()) {
    $self->add_message(UNKNOWN,
        sprintf "cannot write status dir %s! check your filesystem (permissions/usage/integrity) and disk devices", $self->statefilesdir());
  }
}

sub create_statefile {
  my ($self, %params) = @_;
  my $extension = "";
  $extension .= $params{name} ? '_'.$params{name} : '';
  $extension =~ s/\//_/g;
  $extension =~ s/\(/_/g;
  $extension =~ s/\)/_/g;
  $extension =~ s/\*/_/g;
  $extension =~ s/\s/_/g;
  return sprintf "%s/%s%s", $self->statefilesdir(),
      $self->clean_path($self->mode), $self->clean_path(lc $extension);
}

sub clean_path {
  my ($self, $path) = @_;
  if ($^O =~ /MSWin/) {
    $path =~ s/:/_/g;
  }
  return $path;
}

sub schimpf {
  my ($self) = @_;
  printf "statefilesdir %s is not writable.\nYou didn't run this plugin as root, didn't you?\n", $self->statefilesdir();
}

# $self->protect_value('1.1-flat_index', 'cpu_busy', 'percent');
sub protect_value {
  my ($self, $ident, $key, $validfunc) = @_;
  if (ref($validfunc) ne "CODE" && $validfunc eq "percent") {
    $validfunc = sub {
      my $value = shift;
      return 0 if ! defined $value;
      return 0 if $value !~ /^[-+]?([0-9]+(\.[0-9]+)?|\.[0-9]+)$/;
      return ($value < 0 || $value > 100) ? 0 : 1;
    };
  } elsif (ref($validfunc) ne "CODE" && $validfunc eq "positive") {
    $validfunc = sub {
      my $value = shift;
      return 0 if ! defined $value;
      return 0 if $value !~ /^[-+]?([0-9]+(\.[0-9]+)?|\.[0-9]+)$/;
      return ($value < 0) ? 0 : 1;
    };
  }
  if (&$validfunc($self->{$key})) {
    $self->save_state(name => 'protect_'.$ident.'_'.$key, save => {
        $key => $self->{$key},
        exception => 0,
    });
  } else {
    # if the device gives us an clearly wrong value, simply use the last value.
    my $laststate = $self->load_state(name => 'protect_'.$ident.'_'.$key) || {
        exception => 0,
    };
    $self->debug(sprintf "self->{%s} is %s and invalid for the %dth time",
        $key, defined $self->{$key} ? $self->{$key} : "<undef>",
         $laststate->{exception} + 1);
    if ($laststate->{exception} <= 5) {
      # but only 5 times.
      # if the error persists, somebody has to check the device.
      $self->{$key} = $laststate->{$key};
    }
    $self->save_state(name => 'protect_'.$ident.'_'.$key, save => {
        $key => $laststate->{$key},
        exception => ++$laststate->{exception},
    });
  }
}

sub save_state {
  my ($self, %params) = @_;
  $self->create_statefilesdir();
  my $statefile = $self->create_statefile(%params);
  my $tmpfile = $statefile.$$.rand();
  if ((ref($params{save}) eq "HASH") && exists $params{save}->{timestamp}) {
    $params{save}->{localtime} = scalar localtime $params{save}->{timestamp};
  }
  my $seekfh = IO::File->new();
  if ($seekfh->open($tmpfile, "w")) {
    my $coder = JSON::XS->new->ascii->pretty->allow_nonref;
    my $jsonscalar = $coder->encode($params{save});
    $seekfh->print($jsonscalar);
    # the very time-consuming old way.
    # $seekfh->printf("%s", Data::Dumper::Dumper($params{save}));
    $seekfh->flush();
    $seekfh->close();
    $self->debug(sprintf "saved %s to %s",
        Data::Dumper::Dumper($params{save}), $statefile);
  }
  if (! rename $tmpfile, $statefile) {
    $self->add_message(UNKNOWN,
        sprintf "cannot write status file %s! check your filesystem (permissions/usage/integrity) and disk devices", $statefile);
  }
}

sub load_state {
  my ($self, %params) = @_;
  my $statefile = $self->create_statefile(%params);
  if ( -f $statefile) {
    our $VAR1;
    eval {
      delete $INC{$statefile} if exists $INC{$statefile}; # else unit tests fail
      my $jsonscalar = read_file($statefile);
      my $coder = JSON::XS->new->ascii->pretty->allow_nonref;
      $VAR1 = $coder->decode($jsonscalar);
    };
    if($@) {
      $self->debug(sprintf "json load from %s failed. fallback", $statefile);
      eval {
        require $statefile;
      };
      if($@) {
        printf "FATAL: Could not load old state in perl format!\n";
      }
    }
    $self->debug(sprintf "load %s from %s", Data::Dumper::Dumper($VAR1), $statefile);
    return $VAR1;
  } else {
    return undef;
  }
}

sub release_lock {
  my ($self, $lock_file) = @_;
  $self->debug(sprintf "release %s", $lock_file);
  unlink $lock_file if -f $lock_file;
}

sub acquire_lock {
  my ($self, $lock_file, $max_depth, $depth) = @_;
  $max_depth ||= 2;
  $depth ||= 0;
  $self->debug(sprintf "try to aquire %s, attempt %d", $lock_file, $depth);
  if ($depth > $max_depth) {
    # wait no longer
    return 0;
  }
  if (-f $lock_file && (time - (stat($lock_file))[9]) > 600) {
    $self->debug(sprintf "lock %s exists, but is quite old", $lock_file);
    # lock_file is older than 10 minutes, check PID
    # maybe the process, which refreshed the cache, crashed or was killed
    # by the Naemon timeout.
    my ($pid, $hostname);
    if (open(my $pid_fh, '<', $lock_file)) {
      ($pid, $hostname) = split /\s+/, <$pid_fh>;
      close $pid_fh;
      my $is_process_running = sub {
        my $pid = shift;
        return kill(0, $pid) ? 1 : 0;
      };
      if (!$pid || ! &$is_process_running($pid)) {
        # orphaned lock, wait a bit then retry
        $self->debug(sprintf "lock %s is orphaned", $lock_file);
        $self->release_lock($lock_file);
        sleep rand(2);
        return $self->acquire_lock($lock_file, $max_depth, $depth + 1);
      } else {
        # the lock is held by a running process
        $self->debug(sprintf "lock %s is justified, refresh in progress", $lock_file);
        return 0;
      }
    } else {
      # cannot read PID, assume lock is orphaned and retry
      $self->debug(sprintf "lock %s is damaged", $lock_file);
      $self->release_lock($lock_file);
      sleep rand(2);
      return $self->acquire_lock($lock_file, $max_depth, $depth + 1);
    }
  } elsif (-f $lock_file) {
    # lock_file is younger than 10 minutes, refreshing is in-progress
    $self->debug(sprintf "lock %s exists, refresh in progress", $lock_file);
    return 0;
  }
  # attempt to create a new lock file
  $self->create_statefilesdir();
  if (open(my $lock_fh, ">", $lock_file)) {
    printf $lock_fh "%d %s\n", $$, hostname();
    close $lock_fh;
    $self->debug(sprintf "lock %s claimed", $lock_file);
    return 1; # lock acquired
  } else {
    # failed to create lock_file, try again
    sleep rand(2);
    return $self->acquire_lock($lock_file, $max_depth, $depth + 1);
  }
}

#########################################################
# daemon mode
#
sub check_pidfile {
  my ($self) = @_;
  my $fh = IO::File->new();
  if ($fh->open($self->{pidfile}, "r")) {
    my $pid = $fh->getline();
    $fh->close();
    if (! $pid) {
      $self->debug("Found pidfile %s with no valid pid. Exiting.",
          $self->{pidfile});
      return 0;
    } else {
      $self->debug("Found pidfile %s with pid %d", $self->{pidfile}, $pid);
      kill 0, $pid;
      if ($! == Errno::ESRCH) {
        $self->debug("This pidfile is stale. Writing a new one");
        $self->write_pidfile();
        return 1;
      } else {
        $self->debug("This pidfile is held by a running process. Exiting");
        return 0;
      }
    }
  } else {
    $self->debug("Found no pidfile. Writing a new one");
    $self->write_pidfile();
    return 1;
  }
}

sub write_pidfile {
  my ($self) = @_;
  if (! -d dirname($self->{pidfile})) {
    eval "require File::Path;";
    if (defined(&File::Path::mkpath)) {
      import File::Path;
      eval { mkpath(dirname($self->{pidfile})); };
    } else {
      my @dirs = ();
      map {
          push @dirs, $_;
          mkdir(join('/', @dirs))
              if join('/', @dirs) && ! -d join('/', @dirs);
      } split(/\//, dirname($self->{pidfile}));
    }
  }
  my $fh = IO::File->new();
  $fh->autoflush(1);
  if ($fh->open($self->{pidfile}, "w")) {
    $fh->printf("%s", $$);
    $fh->close();
  } else {
    $self->debug("Could not write pidfile %s", $self->{pidfile});
    die "pid file could not be written";
  }
}

sub system_vartmpdir {
  my ($self) = @_;
  if ($^O =~ /MSWin/) {
    return $self->system_tmpdir();
  } else {
    return "/var/tmp/".$Monitoring::GLPlugin::pluginname;
  }
}

sub system_tmpdir {
  my ($self) = @_;
  if ($^O =~ /MSWin/) {
    return $ENV{TEMP} if defined $ENV{TEMP};
    return $ENV{TMP} if defined $ENV{TMP};
    return File::Spec->catfile($ENV{windir}, 'Temp')
        if defined $ENV{windir};
    return 'C:\Temp';
  } else {
    return "/tmp";
  }
}

sub convert_scientific_numbers {
  my ($self, $n) = @_;
  # mostly used to convert numbers in scientific notation
  if ($n =~ /^\s*\d+\s*$/) {
    return $n;
  } elsif ($n =~ /^\s*([-+]?)(\d*[\.,]*\d*)[eE]{1}([-+]?)(\d+)\s*$/) {
    my ($vor, $num, $sign, $exp) = ($1, $2, $3, $4);
    $n =~ s/E/e/g;
    $n =~ s/,/\./g;
    $num =~ s/,/\./g;
    my $sig = $sign eq '-' ? "." . ($exp - 1 + length $num) : '';
    my $dec = sprintf "%${sig}f", $n;
    $dec =~ s/\.[0]+$//g;
    return $dec;
  } elsif ($n =~ /^\s*([-+]?)(\d+)[\.,]*(\d*)\s*$/) {
    return $1.$2.".".$3;
  } elsif ($n =~ /^\s*(.*?)\s*$/) {
    return $1;
  } else {
    return $n;
  }
}

sub compatibility_methods {
  my ($self) = @_;
  # add_perfdata
  # add_message
  # nagios_exit
  # ->{warningrange}
  # ->{criticalrange}
  # ...
  $self->{warningrange} = ($self->get_thresholds())[0];
  $self->{criticalrange} = ($self->get_thresholds())[1];
  my $old_init = $self->can('init');
  my %params = (
    'mode' => join('::', split(/-/, $self->opts->mode)),
    'name' => $self->opts->name,
    'name2' => $self->opts->name2,
  );
  {
    no strict 'refs';
    no warnings 'redefine';
    *{ref($self).'::init'} = sub {
      $self->$old_init(%params);
      $self->nagios(%params);
    };
    *{ref($self).'::add_nagios'} = \&{"Monitoring::GLPlugin::add_message"};
    *{ref($self).'::add_nagios_ok'} = \&{"Monitoring::GLPlugin::add_ok"};
    *{ref($self).'::add_nagios_warning'} = \&{"Monitoring::GLPlugin::add_warning"};
    *{ref($self).'::add_nagios_critical'} = \&{"Monitoring::GLPlugin::add_critical"};
    *{ref($self).'::add_nagios_unknown'} = \&{"Monitoring::GLPlugin::add_unknown"};
    *{ref($self).'::add_perfdata'} = sub {
      my $self = shift;
      my $message = shift;
      foreach my $perfdata (split(/\s+/, $message)) {
      my ($label, $perfstr) = split(/=/, $perfdata);
      my ($value, $warn, $crit, $min, $max) = split(/;/, $perfstr);
      $value =~ /^([\d\.\-\+]+)(.*)$/;
      $value = $1;
      my $uom = $2;
      $Monitoring::GLPlugin::plugin->add_perfdata(
        label => $label,
        value => $value,
        uom => $uom,
        warn => $warn,
        crit => $crit,
        min => $min,
        max => $max,
      );
      }
    };
    *{ref($self).'::check_thresholds'} = sub {
      my $self = shift;
      my $value = shift;
      my $defaultwarningrange = shift;
      my $defaultcriticalrange = shift;
      $Monitoring::GLPlugin::plugin->set_thresholds(
          metric => 'default',
          warning => $defaultwarningrange,
          critical => $defaultcriticalrange,
      );
      $self->{warningrange} = ($self->get_thresholds())[0];
      $self->{criticalrange} = ($self->get_thresholds())[1];
      return $Monitoring::GLPlugin::plugin->check_thresholds(
          metric => 'default',
          value => $value,
          warning => $defaultwarningrange,
          critical => $defaultcriticalrange,
      );
    };
  }
}

sub AUTOLOAD {
  my ($self, @params) = @_;
  return if ($AUTOLOAD =~ /DESTROY/);
  $self->debug("AUTOLOAD %s\n", $AUTOLOAD)
        if $self->opts->verbose >= 2;
  if ($AUTOLOAD =~ /^(.*)::analyze_and_check_(.*)_subsystem$/) {
    my $class = $1;
    my $subsystem = $2;
    my $analyze = sprintf "analyze_%s_subsystem", $subsystem;
    my $check = sprintf "check_%s_subsystem", $subsystem;
    if (@params) {
      # analyzer class
      my $subsystem_class = shift @params;
      $self->{components}->{$subsystem.'_subsystem'} = $subsystem_class->new();
      $self->debug(sprintf "\$self->{components}->{%s_subsystem} = %s->new()",
          $subsystem, $subsystem_class);
    } else {
      $self->$analyze();
      $self->debug("call %s()", $analyze);
    }
    $self->$check();
  } elsif ($AUTOLOAD =~ /^(.*)::check_(.*)_subsystem$/) {
    my $class = $1;
    my $subsystem = sprintf "%s_subsystem", $2;
    $self->{components}->{$subsystem}->check();
    $self->{components}->{$subsystem}->dump()
        if $self->opts->verbose >= 2;
  } elsif ($AUTOLOAD =~ /^.*::(status_code|check_messages|nagios_exit|html_string|perfdata_string|selected_perfdata|check_thresholds|get_thresholds|mod_threshold|opts|pandora_string|strequal)$/) {
    return $Monitoring::GLPlugin::plugin->$1(@params);
  } elsif ($AUTOLOAD =~ /^.*::(reduce_messages|reduce_messages_short|clear_messages|suppress_messages|add_html|add_perfdata|override_opt|create_opt|set_thresholds|force_thresholds|add_pandora)$/) {
    $Monitoring::GLPlugin::plugin->$1(@params);
  } elsif ($AUTOLOAD =~ /^.*::mod_arg_(.*)$/) {
    return $Monitoring::GLPlugin::plugin->mod_arg($1, @params);
  } else {
    $self->debug("AUTOLOAD: class %s has no method %s\n",
        ref($self), $AUTOLOAD);
  }
}



package Monitoring::GLPlugin::Item;
our @ISA = qw(Monitoring::GLPlugin);

use strict;

sub new {
  my ($class, %params) = @_;
  my $self = {
    blacklisted => 0,
    info => undef,
    extendedinfo => undef,
  };
  bless $self, $class;
  $self->init(%params);
  return $self;
}

sub check {
  my ($self, $lists) = @_;
  my @lists = $lists ? @{$lists} : grep { ref($self->{$_}) eq "ARRAY" } keys %{$self};
  foreach my $list (@lists) {
    $self->add_info('checking '.$list);
    foreach my $element (@{$self->{$list}}) {
      $element->blacklist() if $self->is_blacklisted();
      $element->check();
    }
  }
}

sub init_subsystems {
  my ($self, $subsysref) = @_;
  foreach (@{$subsysref}) {
    my ($subsys, $class) = @{$_};
    $self->{$subsys} = $class->new()
        if (! $self->opts->subsystem || grep {
            $_ eq $subsys;
        } map {
            s/^\s+|\s+$//g;
            $_;
        } split /,/, $self->opts->subsystem);
  }
}

sub check_subsystems {
  my ($self) = @_;
  my @subsystems = grep { $_ =~ /.*_subsystem$/ } keys %{$self};
  foreach (@subsystems) {
    $self->{$_}->check();
  }
  $self->reduce_messages_short(join(", ",
      map {
          sprintf "%s working fine", $_;
      } map {
          s/^\s+|\s+$//g;
          $_;
      } split /,/, $self->opts->subsystem
  )) if $self->opts->subsystem;
}

sub summarize_subsystems {
  my ($self) = @_;
  my @subsystems = grep { $_ =~ /.*_subsystem$/ } keys %{$self};
  my @subsystem_summary = ();
  foreach (@subsystems) {
    if ($self->{$_}->{subsystem_summary}) {
      push(@subsystem_summary, $self->{$_}->{subsystem_summary});
    }
  }
  return join(", ", @subsystem_summary);
}

sub dump_subsystems {
  my ($self) = @_;
  my @subsystems = grep { $_ =~ /.*_subsystem$/ } keys %{$self};
  foreach (@subsystems) {
    $self->{$_}->dump();
  }
}

sub subsystem_summary {
  my ($self, $summary) = @_;
  $self->{subsystem_summary} = $summary;
}



package Monitoring::GLPlugin::TableItem;
our @ISA = qw(Monitoring::GLPlugin::Item);

use strict;

sub new {
  my ($class, %params) = @_;
  my $self = {};
  bless $self, $class;
  foreach (keys %params) {
    $self->{$_} = $params{$_};
  }
  if ($self->can("finish")) {
    $self->finish(%params);
  }
  return $self;
}

sub check {
  my ($self) = @_;
  # some tableitems are not checkable, they are only used to enhance other
  # items (e.g. sensorthresholds enhance sensors)
  # normal tableitems should have their own check-method
}



package CheckSapHealth::SAP;
our @ISA = qw(CheckSapHealth::Device);
use strict;


sub init {
  my $self = shift;
  if ($self->mode =~ /^netweaver/i) {
    bless $self, 'CheckSapHealth::SAP::Netweaver';
    $self->debug('using CheckSapHealth::SAP::Netweaver');
  }
  if (ref($self) ne "CheckSapHealth::SAP") {
    $self->init();
  } else {
    $self->no_such_mode();
  }
}

package CheckSapHealth::SAP::Netweaver;
our @ISA = qw(CheckSapHealth::SAP);

use strict;
use File::Basename;
use Time::HiRes;
use Time::Local;
use Digest::MD5 qw(md5_hex);
use Data::Dumper;
use AutoLoader;
use File::Spec;
our $AUTOLOAD;

use constant { OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3 };

{
  our $mode = undef;
  our $plugin = undef;
  our $blacklist = undef;
  our $session = undef;
  our $info = [];
  our $extendedinfo = [];
  our $summary = [];
  our $oidtrace = [];
  our $uptime = 0;
}

sub check_rfc_and_model {
  my $self = shift;
  chdir("/tmp");
  if (eval "require sapnwrfc") {
    my %params = (
      'LCHECK' => '1',
    );
    if ($self->opts->ashost) {
      $params{ASHOST} = $self->opts->ashost;
    }
    if (defined $self->opts->sysnr) {
      $params{SYSNR} = sprintf "%02d", $self->opts->sysnr;
    }
    if ($self->opts->mshost) {
      $params{MSHOST} = $self->opts->mshost;
    }
    if ($self->opts->msserv) {
      $params{MSSERV} = $self->opts->msserv;
    }
    if ($self->opts->r3name) {
      $params{R3NAME} = $self->opts->r3name;
    }
    if ($self->opts->saprouter) {
      $params{SAPROUTER} = $self->opts->saprouter;
      if (rindex($params{SAPROUTER}, "/H/", 0) != 0) {
        $params{SAPROUTER} = "/H/".$params{SAPROUTER};
      }
    }
    if ($self->opts->group) {
      # Wenn man nichts angibt, dann ist beim NWRFCSDK "PUBLIC" der Default.
      # Sollte man aber nicht nehmen, da diese Group fuer SAPGUI-Benutzer
      # gedacht ist. Besser ist "INTERFACE".
      $params{GROUP} = $self->opts->group;
    }
    if ($self->opts->gwhost) {
      $params{GWHOST} = $self->opts->gwhost;
    }
    if ($self->opts->gwserv) {
      $params{GWSERV} = $self->opts->gwserv;
    }
    if (defined $self->opts->client) {
      $params{CLIENT} = sprintf "%03d", $self->opts->client;
    }
    if ($self->opts->lang) {
      $params{LANG} = $self->opts->lang;
    }
    if ($self->opts->username) {
      $params{USER} = $self->opts->username;
    }
    if ($self->opts->password) {
      $params{PASSWD} = $self->opts->password;
    }
    if ($self->opts->verbose) {
      $params{DEBUG} = '1';
      $params{TRACE} = '1';
    }
    if ($self->opts->mode =~ /sapinfo/) {
      $params{LCHECK} = '0';
      $params{USER} = "";
      $params{PASSWD} = "";
      $params{ABAP_DEBUG} = 0;
      $params{SAPGUI} = 0;
    }
    if ($self->opts->snc) {
      # The SNC flag to indicate whether the communication should use SNC protection	
      # 0 - Do not apply SNC to connections.
      # 1 - Apply SNC to connections.
      $params{'SNC_MODE'} = '1';
      # SNC_MYNAME
      # Client SNC name (DataStage Server SNC Name).\
      # It is also referred as client Personal Security Environment (PSE) Name.
      # A valid client SNC name, which is equal to
      # Distinguished Name(DN) of client PSE
      # SNC_PARTNERNAME
      # The communication partner's SNC name.
      # Therefore, this is SAP server SNC PSE name.
      # A valid SAP server SNC name, which is equal to
      # Distinguished Name(DN) of SAP server PSE
      # SNC_QOP
      # The quality of protection level.
      # Enter one of the following values:
      # 1 - Apply authentication only.
      # 2 - Apply authentication and integrity protection
      # 3 - Apply authentication, integrity, and privacy protection (encryption)
      # 8 - Apply global default protection (usually 3)
      # 9 - Apply the maximum protection.
      # SNC_LIB
      # The external security product's library
      # The path and file name for the SAP Cryptography library.

      if ($self->opts->get('secudir')) {
        $ENV{'SECUDIR'} = $self->opts->get('secudir');
      }
      if ($self->opts->get('snclib')) {
        $params{'SNC_LIB'} = $self->opts->get('snclib');
      } else {
        my $found_snc_lib = 0;
        if (exists $ENV{LD_LIBRARY_PATH} && $ENV{LD_LIBRARY_PATH} ne '') {
          foreach my $dir (split(/:/, $ENV{LD_LIBRARY_PATH})) {
            my $snc_lib_path = File::Spec->catfile($dir, 'libsapcrypto.so');
            if (-f $snc_lib_path) {
              $params{'SNC_LIB'} = $snc_lib_path;
              $self->debug("Found libsapcrypto.so in LD_LIBRARY_PATH: %s", $snc_lib_path);
              $found_snc_lib = 1;
              last; # Stop searching once found
            }
          }
        }
        if (!$found_snc_lib) {
          $self->debug("libsapcrypto.so not found in LD_LIBRARY_PATH and --snc-lib not provided.");
        }
      }
      if ($self->opts->sncmyname) {
        $params{'SNC_MYNAME'} = $self->opts->sncmyname;
      }
      if ($self->opts->sncpartnername) {
        $params{'SNC_PARTNERNAME'} = $self->opts->sncpartnername;
      }
      $params{'SNC_QOP'} = $self->opts->get('sncqop');
      $params{'SNC_SSO'} = '0';
    }
    $self->{tic} = Time::HiRes::time();
    my $session = undef;
    eval {
      $session = SAPNW::Rfc->rfc_connect(%params);
    };
    if ($@) {
      $self->add_message(CRITICAL,
          sprintf 'cannot create rfc connection: %s', $@);
      $self->debug(Data::Dumper::Dumper(\%params));
    } elsif (! defined $session) {
      $self->add_message(CRITICAL,
          sprintf 'cannot create rfc connection');
      $self->debug(Data::Dumper::Dumper(\%params));
    } else {
      $CheckSapHealth::SAP::Netweaver::session = $session;
    }
    $self->{tac} = Time::HiRes::time();
  } else {
    $self->add_message(CRITICAL,
        'could not load perl module SAPNW');
  }
}

sub session {
  my $self = shift;
  return $CheckSapHealth::SAP::Netweaver::session;
}

sub set_failed_connection_flag {
  my($self) = @_;
  my $save_name = $self->opts->name;
  my $save_name2 = $self->opts->name2;
  my $save_name3 = $self->opts->name3;
  my $save_mode = $self->opts->mode;
  $self->override_opt("name", "");
  $self->override_opt("name2", "");
  $self->override_opt("name3", "");
  $self->override_opt("mode", "");
  $self->save_state(name => 'connection_failed');
  $self->override_opt("name", $save_name);
  $self->override_opt("name2", $save_name2);
  $self->override_opt("name3", $save_name3);
  $self->override_opt("mode", $save_mode);
}

sub init {
  my $self = shift;
  if ($self->mode =~ /^netweaver::connectiontime/) {
    my $fc = undef;
    if ($self->mode =~ /^netweaver::connectiontime::sapinfo/) {
      eval {
        my $fl = $self->session->function_lookup("RFC_SYSTEM_INFO");
        $fc = $fl->create_function_call;
        $fc->invoke();
        printf "rrc %s\n", Data::Dumper::Dumper($fc->RFCSI_EXPORT);
      };
      if ($@) {
        printf "crash %s\n", $@;
      }
      $self->{tac} = Time::HiRes::time();
    }
    $self->{connection_time} = $self->{tac} - $self->{tic};
    $self->set_thresholds(warning => 1, critical => 5);
    $self->add_message($self->check_thresholds($self->{connection_time}),
         sprintf "%.2f seconds to connect as %s@%s",
              $self->{connection_time}, $self->opts->username,
              $self->session->connection_attributes->{sysId});
    $self->add_perfdata(
        label => 'connection_time',
        value => $self->{connection_time},
    );
    if ($self->mode =~ /^netweaver::connectiontime::sapinfo/) {
      # extraoutput
      # $fc
    }
  } elsif ($self->mode =~ /^netweaver::ccms::/) {
    $self->analyze_and_check_ccms_subsystem("CheckSapHealth::SAP::Netweaver::Component::CCMS");
  } elsif ($self->mode =~ /^netweaver::snap::/) {
    $self->analyze_and_check_snap_subsystem("CheckSapHealth::SAP::Netweaver::Component::SNAP");
  } elsif ($self->mode =~ /^netweaver::updates::/) {
    $self->analyze_and_check_snap_subsystem("CheckSapHealth::SAP::Netweaver::Component::UpdateSubsystem");
  } elsif ($self->mode =~ /^netweaver::backgroundjobs::/) {
    $self->analyze_and_check_snap_subsystem("CheckSapHealth::SAP::Netweaver::Component::BackgroundjobSubsystem");
  } elsif ($self->mode =~ /^netweaver::processes::/) {
    $self->analyze_and_check_proc_subsystem("CheckSapHealth::SAP::Netweaver::Component::ProcessSubsystem");
  } elsif ($self->mode =~ /^netweaver::idocs::/) {
    $self->analyze_and_check_idoc_subsystem("CheckSapHealth::SAP::Netweaver::Component::IdocSubsystem");
    $self->reduce_messages_short(sprintf "%d idoc status checked, no problems found", $self->{components}->{idoc_subsystem}->{num_idoc_status});
  } elsif ($self->mode =~ /^netweaver::workload::/) {
    $self->analyze_and_check_proc_subsystem("CheckSapHealth::SAP::Netweaver::Component::WorkloadSubsystem");
    $self->reduce_messages_short('no workload problems');
  }
}

sub validate_args {
  my $self = shift;
  $self->SUPER::validate_args();
  $MTE::separator = $self->opts->separator if $self->opts->separator;
  if ($self->opts->get("with-my-modules-dyn-dir")) {
  }
}

sub create_statefile {
  my $self = shift;
  my %params = @_;
  my $extension = "";
  $extension .= $params{name} ? '_'.$params{name} : '';
  $extension .= $self->opts->name ? '_'.$self->opts->name : '';
  $extension .= $self->opts->name2 ? '_'.$self->opts->name2 : '';
  $extension .= $self->opts->name3 ? '_'.$self->opts->name3 : '';
  $extension =~ s/\//_/g;
  $extension =~ s/\(/_/g;
  $extension =~ s/\)/_/g;
  $extension =~ s/\*/_/g;
  $extension =~ s/\s/_/g;
  $extension =~ s/\//_/g;
  $extension =~ s/\|/_/g;
  my $target = "";
  $target .= $self->opts->ashost.'_'.$self->opts->sysnr if $self->opts->ashost;
  $target .= $self->opts->mshost if $self->opts->mshost;
  $target .= $self->opts->msserv if $self->opts->msserv;
  $target .= $self->opts->r3name if $self->opts->r3name;
  $target .= $self->opts->group if $self->opts->group;
  $target .= $self->opts->gwhost if $self->opts->gwhost;
  $target .= $self->opts->gwserv if $self->opts->gwserv;
  $target =~ s/\//_/g;
  return sprintf "%s/%s_%s%s", $self->statefilesdir(),
      $target, $self->opts->mode, lc $extension;
}

sub epoch_to_abap_date {
  my $self = shift;
  my $timestamp = shift || time;
  my($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
      localtime($timestamp);
  return sprintf "%04d%02d%02d", $year + 1900, $mon + 1, $mday;
}

sub epoch_to_abap_time {
  my $self = shift;
  my $timestamp = shift || time;
  my($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
      localtime($timestamp);
  return sprintf "%02d%02d%02d", $hour, $min, $sec;
}

sub epoch_to_abap_date_and_time {
  my $self = shift;
  my $timestamp = shift || time;
  my($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
      localtime($timestamp);
  my $date = sprintf "%04d%02d%02d", $year + 1900, $mon + 1, $mday;
  my $time = sprintf "%02d%02d%02d", $hour, $min, $sec;
  return ($date, $time);
}

sub abap_date_and_time_to_epoch {
  my ($self, $date, $time) = @_;
  $date =~ /(\d\d\d\d)(\d\d)(\d\d)/;
  my ($year, $mon, $mday) = ($1, $2, $3);
  $time =~ /(\d\d)(\d\d)(\d\d)/;
  my ($hour, $min, $sec) = ($1, $2, $3);
  return timelocal($sec, $min, $hour, $mday, $mon - 1, $year);
}

sub compatibility_methods {
  my ($self) = @_;
  # there are no old-style extensions out there
}

sub rstrip {
  my $self = shift;
  my $message = shift;
  $message =~ s/\s+$//g;
  chomp $message;
  return $message;
}

sub strip {
  my $self = shift;
  my $message = shift;
  if (ref($message) eq "HASH") {
    foreach (keys %{$message}) {
      $self->strip($message->{$_});
    }
  } else {
    $message =~ s/^\s+//g;
    $message = $self->rstrip($message);
  }
  return $message;
}

sub DESTROY {
  my ($self) = @_;
  if (ref($self) ne "CheckSapHealth::SAP") {
    return;
    # Dieses DESTROY wird auch von irgendwelchen schwindligen Erbschleichern
    # aufgerufen, die mir hier die Session womoeglich zumachen.
  }
  my $plugin_exit = $?;
  if ($CheckSapHealth::SAP::Netweaver::session) {
    $CheckSapHealth::SAP::Netweaver::session->disconnect();
  }
  #$self->debug("disconnected");
  my $now = time;
  eval {
    my $ramschdir = $ENV{RFC_TRACE_DIR} ? $ENV{RFC_TRACE_DIR} : "/tmp";
    unlink $ramschdir."/dev_rfc.trc" if -f $ramschdir."/dev_rfc.trc";
    no warnings "all";
    foreach (glob $ramschdir."/rfc*.trc") {
      eval {
        if (($now - (stat $_)[9]) > 300) {
          unlink $_;
        }
      };
    }
  };
  $? = $plugin_exit;
}

package CheckSapHealth::SAP::Netweaver::Item;
#our @ISA = qw(Monitoring::GLPlugin::Item CheckSapHealth::SAP::Netweaver);
our @ISA = qw(CheckSapHealth::SAP::Netweaver Monitoring::GLPlugin::Item);
use strict;

{
  no strict 'refs';
  *{'CheckSapHealth::SAP::Netweaver::Item::new'} = \&{'Monitoring::GLPlugin::Item::new'};
}

sub compatibility_methods {
  my ($self) = @_;
}

package CheckSapHealth::SAP::Netweaver::TableItem;
#our @ISA = qw(Monitoring::GLPlugin::TableItem CheckSapHealth::SAP::Netweaver);
our @ISA = qw(CheckSapHealth::SAP::Netweaver Monitoring::GLPlugin::TableItem);
use strict;

{
  no strict 'refs';
  *{'CheckSapHealth::SAP::Netweaver::TableItem::new'} = \&{'Monitoring::GLPlugin::TableItem::new'};
}

sub compatibility_methods {
  my ($self) = @_;
}


package CheckSapHealth::SAP::Netweaver::Component::CCMS;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::Item);
use strict;


sub init {
  my $self = shift;
  my($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
      localtime(time);
  my $bapi_tic = Time::HiRes::time();
  if ($self->mode =~ /netweaver::ccms::/) {
    eval {
      my $rcb = $self->session->function_lookup("SXMI_VERSIONS_GET");
      my $tsl = $rcb->create_function_call;
      #$tsl->INTERFACE("XMB"); # weglassen fuer alle interfaces
      $tsl->invoke;
      my $xmbversion = "0.1";
      my $xalversion = "0.1";
      foreach my $row (@{$tsl->VERSIONS}) {
        $xmbversion = $row->{'VERSION'} if $row->{'INTERFACE'} eq "XMB";
        $xalversion = $row->{'VERSION'} if $row->{'INTERFACE'} eq "XAL";
      }
      my $fl = $self->session->function_lookup("BAPI_XMI_LOGON");
      my $fc = $fl->create_function_call;
      $fc->EXTCOMPANY('LAUSSER');
      $fc->EXTPRODUCT('CHECK_SAP_HEALTH');
      $fc->INTERFACE('XAL');
      $fc->parameter('VERSION')->value($xalversion);
      $fc->invoke;
      if ($fc->RETURN->{TYPE} !~ /^E/) {
        if ($self->mode =~ /netweaver::ccms::moniset::list/) {
          $fl = $self->session->function_lookup("BAPI_SYSTEM_MS_GETLIST");
          $fc = $fl->create_function_call;
          $fc->EXTERNAL_USER_NAME("Agent");
          $fc->invoke;
          my @sets = @{$fc->MONITOR_SETS};
          foreach (@sets) {
            printf "%s\n", $_->{NAME};
          }
          $self->add_ok("have fun");

        } elsif ($self->mode =~ /netweaver::ccms::monitor::list/) {
          $fl = $self->session->function_lookup("BAPI_SYSTEM_MON_GETLIST");
          # details with BAPI_SYSTEM_MS_GETDETAILS
          $fc = $fl->create_function_call;
          $fc->EXTERNAL_USER_NAME("Agent");
          if ($self->opts->name) {
            $fc->MONI_SET_NAME({
                NAME => $self->opts->name,
            });
          }
          $fc->invoke;
          my @names = @{$fc->MONITOR_NAMES};
          foreach my $ms (sort keys %{{ map {$_ => 1} map { $_->{MS_NAME} } @names }}) {
            printf "%s\n", $ms;
            foreach my $moni (sort keys %{{ map {$_ => 1} map { $_->{MONI_NAME} }
                grep { $_->{MS_NAME} eq $ms } @names }}) {
              printf " %s\n", $moni;
            }
          }
          $self->add_ok("have fun");
        } elsif ($self->mode =~ /netweaver::ccms::mte::/) {
          if (! $self->opts->name || ! $self->opts->name2) {
            die "__no_internals__you need to specify --name moniset --name2 monitor";
          }
          if ($self->mode =~ /netweaver::ccms::mte::list/) {
            $self->update_tree_cache(1);
          }
          my $update = $self->update_tree_cache(0);
          my @tree_nodes = @{$update->{tree_nodes}};
          my %seen;
          my @mtes = sort {
              $a->{MTNAMELONG} cmp $b->{MTNAMELONG}
          } grep {
            $self->filter_name3($_->{MTNAMELONG})
          } grep {
            ! $seen{$_->tid_flat()}++;
          } map { 
              MTE->new(%{$_});
          } @tree_nodes;
          if ($self->mode =~ /netweaver::ccms::mte::list/) {
            foreach my $mte (@mtes) {
              printf "%s %d\n", $mte->{MTNAMELONG}, $mte->{MTCLASS};
            }
          } elsif ($self->mode =~ /netweaver::ccms::mte::check/) {
            if (! @mtes) {
              # one more try
              $self->debug("no suitable mtes found in cache (or live). sleep and retry");
              sleep 10;
              $update = $self->update_tree_cache(1);
              @tree_nodes = @{$update->{tree_nodes}};
              %seen = ();
              @mtes = sort {
                  $a->{MTNAMELONG} cmp $b->{MTNAMELONG}
              } grep {
                $self->filter_name3($_->{MTNAMELONG})
              } grep {
                ! $seen{$_->tid_flat()}++;
              } map {
                  MTE->new(%{$_});
              } @tree_nodes;
            }
            $self->set_thresholds();
            foreach my $mte (@mtes) {
              next if grep { $mte->{MTCLASS} == $_ } (50, 70, 199);
              $self->debug(sprintf "collect_details for %s", $mte->{MTNAMELONG});
              $mte->collect_details($self->session);
              $mte->check();
            }
            if (! @mtes) {
              if (defined $self->opts->mitigation()) {
                $self->add_message($self->opts->mitigation(), 'no mtes');
              } else {
                $self->add_unknown("no mtes");
                my $statefile = $update->{statefile};
                if (-f $statefile) {
                  $self->add_unknown(sprintf "(%d tree nodes, age %ds, size %d, inode %s, %s)", scalar(@tree_nodes),
                      time - (stat($statefile))[9], (stat($statefile))[7],
                      (stat($statefile))[1], $statefile);
                }
              }
              my $successfile = $self->create_statefile(name => 'success');
              if (-f $successfile) {
                my $maxlastsuccess = time - 4 * 3600;
                if (((stat $successfile)[9]) > $maxlastsuccess) {
                  $self->debug(sprintf "last OK was %ds ago",
                      time - ((stat $successfile)[9]));
                  # Letztes OK-Ergebnis liegt nicht laenger als 4h zurueck.
                  # Dann gehen wir davon aus, daß --name3 existiert und
                  # das Cachefile kaputt ist.
                  $self->discard_tree_cache();
                } else {
                  $self->debug("looks like there was never an OK for this mode");
                  # Mittlerweile muesste der Cache x Mal neu erstellt worden
                  # sein, wahrscheinlich gibts die gewuenschten MTE
                  # tatsaechlich nicht.
                  unlink $successfile;
                }
              }
            } else {
              # obiger Fall mit den leeren MTEs kann vorkommen, wenn die
              # Cachedatei nicht sonderlich valide ist, wenn nach einem
              # Reboot o.ae. der BAPI_SYSTEM_MON_GETTREE Elemente liefert.
              # Beispiel:
              # -    'VMSEGNAME' => '                                        ',
              # -    'ACTUALVAL' => 0,
              # -    'DUMMYALIGN' => '  ',
              # -    'MTNAMESHRT' => 'P56 : MTE Class CCMS_UpdateStatusClass :',
              # -    'TDSTATUS' => 0,
              # -    'VALINDEX' => '          ',
              # -    'ACTUALSEV' => 0,
              # -    'ALIDXINTRE' => 89,
              # -    'VALERTDATE' => '00000000',
              # -    'EXTINDEX' => '        1-',
              # -    'CUSGRPNAME' => '                                        ',
              # -    'VALERTTIME' => '000000',
              # -    'MTNUMRANGE' => ' 1-',
              # -    'ALERTTIME' => '000000',
              # +    'VMSEGNAME' => 'SAP_CCMS_bgp56as12_P56_56               ',
              # +    'ACTUALVAL' => 1,
              # +    'DUMMYALIGN' => '  ',
              # +    'MTNAMESHRT' => 'Update Status                           ',
              # +    'TDSTATUS' => 20,
              # +    'VALINDEX' => '000000001-',
              # +    'ACTUALSEV' => 50,
              # +    'ALIDXINTRE' => 89,
              # +    'VALERTDATE' => '20200702',
              # +    'EXTINDEX' => '0000000055',
              # +    'CUSGRPNAME' => 'CCMS_UpdateStatusClass                  ',
              # +    'VALERTTIME' => '122718',
              # +    'MTNUMRANGE' => '004',
              # +    'ALERTTIME' => '122718',
              # Obige Suche mit $self->filter_name3($_->{MTNAMELONG})
              # laeuft ins Leere. Jetzt koennte man eine weitere Datei schreiben
              # mit lastSeen-Zeiten fuer erfolgreich gefundene MTEs, aber
              # --name3 kann auch ein Regexp sein und was da an MTEs eigentlich
              # in @mtes stehen muesste, das weiss man erst nach dem Filtern.
              # Also wird einfach eine Datei geschrieben, die --name3 im Namen
              # hat. Damit und mit ihrem modification date weiss man, wann
              # dieser Plugin-Aufruf zuletzt geklappt hat. Wenn ja und innerhalb
              # der letzten vier Stunden, dann fliegt die Cachedatei raus.
              $self->save_state(name => 'success', save => []);
            }
          }
        }
        $self->debug("logoff");
        $fl = $self->session->function_lookup("BAPI_XMI_LOGOFF");
        $fc = $fl->create_function_call;
        $fc->INTERFACE('XAL');
        $fc->invoke;
      } else {
        $self->add_critical($fc->RETURN->{MESSAGE});
      }
    };
    if ($@) {
      my $message = $@;
      $message =~ s/[^[:ascii:]]//g;
      $message =~ s/\s+$//g;
      chomp($message);
      if ($message =~ /__no_internals__/) {
        my $pluginname = File::Basename::basename($ENV{'NAGIOS_PLUGIN'} || $0);
        $message =~ s/at \Q$pluginname\E line.*//g;
        $message =~ s/__no_internals__//g;
      }
      $self->add_unknown($message);
    }
  }
  my $bapi_tac = Time::HiRes::time();
  $self->set_thresholds(warning => 5, critical => 10);
  #$self->add_message($self->check_thresholds($bapi_tac - $bapi_tic),
  #     sprintf "runtime was %.2fs", $bapi_tac - $bapi_tic);
  #$self->add_perfdata(
  #    label => 'runtime',
  #    value => $bapi_tac - $bapi_tic,
  #    warning => $self->{warning},
  #    critical => $self->{critical},
  #);
}

sub discard_tree_cache {
  my $self = shift;
  my $save_name3 = $self->opts->name3;
  my $save_mode = $self->opts->mode;
  $self->override_opt("name3", "");
  $self->override_opt("mode", "");
  my $statefile = $self->create_statefile(name => 'tree');
  $self->override_opt("name3", $save_name3);
  $self->override_opt("mode", $save_mode);
  eval {
    unlink $statefile;
  };
}

sub update_tree_cache {
  my $self = shift;
  my $force = shift;
  my @tree_nodes = ();
  my $save_name = $self->opts->name;
  my $save_name2 = $self->opts->name2;
  my $save_name3 = $self->opts->name3;
  my $save_mode = $self->opts->mode;
  $self->override_opt("name", "");
  $self->override_opt("name2", "");
  $self->override_opt("name3", "");
  $self->override_opt("mode", "");
  my $delete_statefile = $self->create_statefile(name => 'connection_failed');
  $self->override_opt("name", $save_name);
  $self->override_opt("name2", $save_name2);
  my $statefile = $self->create_statefile(name => 'tree');
  if (-f $delete_statefile or $self->opts->refreshmtes) {
    $force = 1;
    $self->debug("we had a connection problem, rebuild the tree cache");
    # here we delete all the ccms tree caches
    # if every name/name2 would do it on it's own, we would never know
    # when all of them have deleted their cache, so there would be no
    # criteria for deleting the delete_statefile
    my $delete_the_trees = $delete_statefile;
    $delete_the_trees =~ s/__connection_failed$/__tree_\*/;
    eval {
      unlink glob $delete_the_trees;
    };
  }
  my $update = time - 24 * 3600;
  if ($force || ! -f $statefile || ((stat $statefile)[9]) < ($update)) {
    $self->debug(sprintf "updating the tree cache for %s %s",
        $self->opts->name, $self->opts->name2);
    my $fl = $self->session->function_lookup("BAPI_SYSTEM_MON_GETTREE");
    my  $fc = $fl->create_function_call;
    $fc->EXTERNAL_USER_NAME("Agent");
    $fc->MONITOR_NAME({
      MS_NAME => $self->opts->name,
      MONI_NAME => $self->opts->name2,
    });
    $fc->invoke;
    # TREE_NODES
    if ($fc->RETURN->{TYPE} =~ /^E/) {
      $self->add_critical($fc->RETURN->{MESSAGE});
    } else {
      map { push(@tree_nodes, $_) } @{$fc->TREE_NODES};
    }
    $self->debug(sprintf "updated the tree cache for %s %s",
        $self->opts->name, $self->opts->name2);
    $self->save_state(name => 'tree', save => \@tree_nodes);
  }
  my $cache = $self->load_state(name => 'tree');
  @tree_nodes = @{$cache};
  $self->debug(sprintf "return cached tree nodes for %s %s",
      $self->opts->name, $self->opts->name2);
  $self->override_opt("name3", $save_name3);
  $self->override_opt("mode", $save_mode);
  if (-f $delete_statefile && $force) {
    unlink $delete_statefile;
  }
  return {
      tree_nodes => \@tree_nodes,
      statefile => $statefile,
  };
}

sub map_alvalue {
  my $self = shift;
  my $value = shift;
  if ($value && 1 <= $value && $value <= 3) {
    return {
      1 => 0,
      2 => 1,
      3 => 2,
    }->{$value};
  } else {
    return 3;
  }
}



package MTE;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::TableItem);
use strict;

#our @ISA = qw(SAP::CCMS);
# can't inherit, because this undefines the session handle. hurnmatz, greisliche

use constant OK         => 0;
use constant WARNING    => 1;
use constant CRITICAL   => 2;
use constant UNKNOWN    => 3;

use constant { GREEN => 1, YELLOW => 2, RED => 3, GRAY => 4 };

# http://www.benx.de/en/sap/program/RSALBAPI---code.htm
use constant MT_CLASS_NO_CLASS    => 0;
use constant MT_CLASS_SUMMARY     => 50;
use constant MT_CLASS_MONIOBJECT  => 70;
use constant MT_CLASS_FIRST_MA    => 99;
use constant MT_CLASS_PERFORMANCE => 100;
use constant MT_CLASS_MSG_CONT    => 101;
use constant MT_CLASS_SINGLE_MSG  => 102;
use constant MT_CLASS_HEARTBEAT   => 103;
use constant MT_CLASS_LONGTEXT    => 110;
use constant MT_CLASS_SHORTTEXT   => 111;
use constant MT_CLASS_VIRTUAL     => 199;
# skip 50, 70, 199

use constant AL_VAL_INAKTIV => 0;
use constant AL_VAL_GREEN => 1;
use constant AL_VAL_YELLOW => 2;
use constant AL_VAL_RED => 3;

# Attribute Type
#  Description
#  
# This graphic is explained in the accompanying text Performance Attribute
#  Collects reported performance values and calculates the average
#  
# This graphic is explained in the accompanying text Status Attribute
#  Reports error message texts and alert status
#  
# This graphic is explained in the accompanying text Heartbeat Attribute
#  Checks whether components of the SAP system are active; if no values are reported for a monitoring attribute for a long time, it triggers an alert
#  
# This graphic is explained in the accompanying text Log Attribute
#  Checks log and trace files (these attributes can use an existing log mechanism, such as the SAP system log, or they can be used by an application for the implementation of a separate log)
#  
# This graphic is explained in the accompanying text Text Attribute
#  Allows a data supplier to report information that is not evaluated for alerts; the text can be updated as required
#  


our $separator = "\\";

{
  sub sap2nagios {
    my $sap = shift;
    my $nagios = 0;
    if ($sap == 1) {
      return OK;
    } elsif ($sap == 2) {
      return WARNING;
    } elsif ($sap == 3) {
      return CRITICAL;
    } elsif ($sap == 4) {
      return UNKNOWN;
    } else {
      return UNKNOWN;
    }
  }
}


sub new {
  my $class = shift;
  my %params = @_;
  my $self = {};
  bless $self, $class;
  foreach (qw(MTSYSID MTCLASS MTMCNAME MTNUMRANGE MTUID MTINDEX EXTINDEX
      OBJECTNAME MTNAMESHRT PRNTMTNAMESHRT)) {
    $self->{$_} = $self->rstrip($params{$_}) if defined $params{$_};
  }
  foreach (qw(ALTREENUM ALIDXINTRE ALLEVINTRE ALPARINTRE VALINDEX)) {
    $self->{$_} = $self->rstrip($params{$_}) if defined $params{$_};
  }
  # CUSGRPNAME = Eigenschaften: ..der MTE-Klasse
  $self->{MTNAMELONG} = $self->mkMTNAMELONG;
  $self->{MTNAGIOSNAME} = $self->opts->mtelong ?
      $self->{MTNAMELONG} : $self->{MTNAMESHRT};
  $self->{TID} = $self->tid();
  if ($self->{MTCLASS} == MT_CLASS_PERFORMANCE) {
    bless $self, "MTE::Performance";
  } elsif ($self->{MTCLASS} == MT_CLASS_MSG_CONT) {
    bless $self, "MTE::ML";
  } elsif ($self->{MTCLASS} == MT_CLASS_SINGLE_MSG) {
    bless $self, "MTE::SM";
  } elsif ($self->{MTCLASS} == MT_CLASS_SHORTTEXT) {
    bless $self, "MTE::ST";
  } else {
    # es kann vorkommen, dass hier MTCLASS == MT_CLASS_VIRTUAL (=199)
    # und MTNAMESHRT' => 'Q4X : System is not configured', MTINDEX' => '-99'
    # und zwar bei fast allen Knoten. Zwischendurch so "Oberpunkte"
    # 'MTNAMESHRT' => 'Verbucherstatus', 'MTMCNAME' => '',
    # 'MTINDEX' => '1-', 'MTCLASS' => '199',
    # was zu dem ekligen "UNKNOWN - no mtes" fuehrt.
    # In dem Fall ist wahrscheinlich ein Reboot o.ae. schuld und man kann
    # BAPI_SYSTEM_MON_GETTREE aufrufen sooft man will, es kommt nichts
    # Brauchbares raus dabei. Und selbst wenn man noch die alte Cachedatei
    # haette mit TID, der schon mal funktioniert hat, dann kracht's sicher
    # auch, weil der CCMS-Baum andere MTINDEX o.ae. hat.
    # Und das, nachdem ich ein sich ueber Monate hinziehendes Ticket bzgl.
    # "no mtes" triumphierend geschlossen hatte.
  }
  return $self;
                my $bapi = {
                    MT_CLASS_PERFORMANCE() => "BAPI_SYSTEM_MTE_GETPERFCURVAL",
                    MT_CLASS_SINGLE_MSG() => "BAPI_SYSTEM_MTE_GETSMVALUE",
                    #MT_CLASS_MSGCONT() => "BAPI_SYSTEM_MTE_?",
                    MT_CLASS_LONGTEXT() => "BAPI_SYSTEM_MTE_GETMLCURVAL",
                    MT_CLASS_SHORTTEXT() => "BAPI_SYSTEM_MTE_GETTXTPROP",
                }->{$self->{MTCLASS}};

}

sub mkMTNAMELONG {
  my $self = shift;
  my $myname = "";
  if ($self->{MTSYSID}) {
    $myname = $myname.$self->{MTSYSID}.$MTE::separator;
  } else {
    #return undef;
  }
  $myname = $myname.$self->{MTMCNAME}.$MTE::separator;
  if ($self->{PRNTMTNAMESHRT} && $self->{PRNTMTNAMESHRT} ne $self->{MTSYSID}.$MTE::separator.$self->{MTMCNAME}) {
    $myname = $myname.$self->{PRNTMTNAMESHRT}.$MTE::separator;
  }
  if ($self->{OBJECTNAME}) {
    $myname = $myname.$self->{OBJECTNAME}.$MTE::separator;
  } else {
    #return undef;
  }
  if ($self->{OBJECTNAME} ne $self->{MTNAMESHRT}) {
    $myname = $myname.$self->{MTNAMESHRT};
  }
  return $myname;
}

sub collect_details {
  my $self = shift;
  my $session = shift;
  $self->debug(sprintf "collect_details with BAPI_SYSTEM_MTE_GETGENPROP %s",
      Data::Dumper::Dumper($self->tid));
  my $fl = $session->function_lookup("BAPI_SYSTEM_MTE_GETGENPROP");
  my $fc = $fl->create_function_call;
  $fc->TID($self->tid);
  $fc->EXTERNAL_USER_NAME("CHECK_SAP_HEALTH");
  $fc->invoke;
  #next if $fc->ACTUAL_ALERT_DATA->{VALUE} == 0;
  $self->{ACTUAL_ALERT_DATA_VALUE} = $fc->ACTUAL_ALERT_DATA->{VALUE};
  $self->{ACTUAL_ALERT_DATA_LEVEL} = $fc->ACTUAL_ALERT_DATA->{LEVEL};
}

sub check {
  my $self = shift;
  $self->debug(sprintf "mte %s has alert %s",
      $self->mkMTNAMELONG(), MTE::sap2nagios($self->{ACTUAL_ALERT_DATA_VALUE}));
  $self->add_info("");
  $self->add_message(MTE::sap2nagios($self->{ACTUAL_ALERT_DATA_VALUE}));
}

sub tid {
  my $self = shift;
  if (! exists $self->{tid}) {
    $self->{tid} = {
      MTSYSID => $self->{MTSYSID},
      MTMCNAME => $self->{MTMCNAME},
      MTNUMRANGE => $self->{MTNUMRANGE},
      MTUID => $self->{MTUID},
      MTCLASS => $self->{MTCLASS},
      MTINDEX => $self->{MTINDEX},
      EXTINDEX => $self->{EXTINDEX},
    };
  }
  return $self->{tid};
}

sub tid_flat {
  my $self = shift;
  return sprintf "%s_%s_%s_%s_%s_%s_%s_",
      $_->{MTSYSID},
      $_->{MTMCNAME},
      $_->{MTNUMRANGE},
      $_->{MTUID},
      $_->{MTCLASS},
      $_->{MTINDEX},
      $_->{EXTINDEX};
}

sub extra_props {
  my $self = shift;
  my %params = @_;
}

sub rstrip {
  my $self = shift;
  my $message = shift;
  $message =~ s/\s+$//g;
  chomp $message;
  return $message;
}

sub strip {
  my $self = shift;
  my $message = shift;
  if (ref($message) eq "HASH") {
    foreach (keys %{$message}) {
      $self->strip($message->{$_});
    }
  } else {
    $message =~ s/^\s+//g;
    $message = $self->rstrip($message);
  }
  return $message;
}


package MTE::Performance;
use strict;
our @ISA = qw(MTE);
use constant { GREEN => 1, YELLOW => 2, RED => 3, GRAY => 4 };

sub collect_details {
  my $self = shift;
  my $session = shift;
  $self->SUPER::collect_details($session);
  my $fl = $session->function_lookup("BAPI_SYSTEM_MTE_GETPERFCURVAL");
  my $fc = $fl->create_function_call;
  $fc->TID($self->tid);
  $fc->EXTERNAL_USER_NAME("CHECK_SAP_HEALTH");
  $fc->invoke;
  foreach (qw(ALRELEVVAL AVG15PVAL ALRELVALTI AVG05PVAL MAXPFDATE ALRELVALDT MINPFDATE
      LASTPERVAL MAXPFTIME AVG15SVAL AVG15CVAL MAXPFVALUE AVG01PVAL AVG01SVAL MINPFVALUE
      LASTALSTAT AVG01CVAL AVG05CVAL MINPFTIME AVG05SVAL)) {
    $self->{$_} = $self->strip($fc->CURRENT_VALUE->{$_});
  }
  $fl = $session->function_lookup("BAPI_SYSTEM_MTE_GETPERFPROP");
  $fc = $fl->create_function_call;
  $fc->TID($self->tid);
  $fc->EXTERNAL_USER_NAME("CHECK_SAP_HEALTH");
  $fc->invoke;
  foreach (qw(DECIMALS MSGID RELVALTYPE VALUNIT ATTRGROUP
      TRESHR2Y THRESHDIR TRESHG2Y TRESHY2G THRESHSTAT MSGCLASS TRESHY2R)) {
    $self->{$_} = $self->strip($fc->PROPERTIES->{$_});
  }
  if ($self->{DECIMALS} != 0) {
    # aus SAP_BASIS Modul BC-CCM-MON-OS
    my $exp = 10 ** $self->{DECIMALS};
    $self->{ALRELEVVAL} = sprintf("%.*f", $self->{DECIMALS}, $self->{ALRELEVVAL} / $exp);
    foreach (qw(TRESHR2Y TRESHG2Y TRESHY2G TRESHY2R)) {
      $self->{$_} = sprintf("%.*f", $self->{DECIMALS}, $self->{$_} / $exp) if $self->{$_};
    }
  }
}

sub check {
  my $self = shift;
  my $perfdata = {
    label => $self->{OBJECTNAME}."_".$self->{MTNAGIOSNAME},
    value => $self->{ALRELEVVAL},
  };
  if ($self->{VALUNIT}) {
    my $unit = lc $self->{VALUNIT};
    $unit = "ms" if $unit eq "msec";
    if ($unit =~ /^([u,m]{0,1}s|%|[kmgt]{0,1}b)$/) {
      $perfdata->{uom} = $unit;
    }
  }
  if ($self->{THRESHDIR} == 1 || $self->{THRESHDIR} == 2) {
    if ($self->{THRESHDIR} == 1) {
      $perfdata->{warning} = $self->{TRESHG2Y};
      $perfdata->{critical} = $self->{TRESHY2R};
    } else {
      $perfdata->{warning} = $self->{TRESHG2Y}.":";
      $perfdata->{critical} = $self->{TRESHY2R}.":";
    }
  }
  if ($self->{THRESHDIR} == 0) {
    # keine Doku gefunden, aber sowas gibt's tatsaechlich
    # THRESHDIR ist 0, TRESHY2G, TRESHR2Y, THRESHSTAT, TRESHG2Y, TRESHY2R auch alle 0
    # 'value' => 0,
    # 'label' => 'Enqueue Server_PRL\\Enqueue\\Enqueue Server\\Granule Entries Actual Utilisation'
    # In der Hoffnung, dass in solchen Faellen immer alles Null ist, werden Schwellwerte gesetzt.
    # Denn moeglicherweise ist das nur ein voruebergehender Zustand, dann ist eine Nulllinie immer
    # noch besser als eine Luecke.
    $perfdata->{warning} = $self->{TRESHG2Y};
    $perfdata->{critical} = $self->{TRESHY2R};
  }
  $self->set_thresholds(warning => $perfdata->{warning}, critical => $perfdata->{critical}, metric => $perfdata->{label});
  delete $perfdata->{warning};
  delete $perfdata->{critical};
  $self->add_perfdata(%{$perfdata});
  $self->add_message(
      $self->check_thresholds(
          value => $self->{ALRELEVVAL}, metric => $perfdata->{label}),
      sprintf "%s %s = %s%s", $self->{OBJECTNAME}, $self->{MTNAGIOSNAME}, $self->{ALRELEVVAL}, $self->{VALUNIT}
  );
}

sub nagios_level { #deprecated
  my $self = shift;
  if ($self->{ACTUAL_ALERT_DATA_VALUE} == 0) {
    if ($self->{THRESHDIR} == 1 || $self->{THRESHDIR} == 2) {
      # this mte is threshold driven
      if ($self->{THRESHDIR} == 1) {
        if ($self->{ALRELEVVAL} > $self->{TRESHY2R}) {
          $self->{ACTUAL_ALERT_DATA_VALUE} = RED;
        } elsif ($self->{ALRELEVVAL} > $self->{TRESHG2Y}) {
          $self->{ACTUAL_ALERT_DATA_VALUE} = YELLOW;
        } else {
          $self->{ACTUAL_ALERT_DATA_VALUE} = GREEN;
        }
      } else {
        if ($self->{ALRELEVVAL} < $self->{TRESHY2R}) {
          $self->{ACTUAL_ALERT_DATA_VALUE} = RED;
        } elsif ($self->{ALRELEVVAL} < $self->{TRESHG2Y}) {
          $self->{ACTUAL_ALERT_DATA_VALUE} = YELLOW;
        } else {
          $self->{ACTUAL_ALERT_DATA_VALUE} = GREEN;
        }
      }
    }
  }
  return MTE::sap2nagios($self->{ACTUAL_ALERT_DATA_VALUE});
}



package MTE::ML;
our @ISA = qw(MTE);
use strict;

sub collect_details {
  my $self = shift;
  my $session = shift;
  $self->SUPER::collect_details($session);
  my $fl = $session->function_lookup("BAPI_SYSTEM_MTE_GETMLCURVAL");
  my $fc = $fl->create_function_call;
  $fc->TID($self->tid);
  $fc->EXTERNAL_USER_NAME("CHECK_SAP_HEALTH");
  $fc->invoke;
  foreach (qw(MSG)) {
    $self->{$_} = $self->strip($fc->XMI_MSG_EXT->{$_});
  }
}

sub check {
  my $self = shift;
  $self->debug(sprintf "mte %s has alert %s",
      $self->mkMTNAMELONG(), MTE::sap2nagios($self->{ACTUAL_ALERT_DATA_VALUE}));
  $self->add_info($self->{MTNAGIOSNAME}." = ".$self->{MSG});
  $self->add_message(MTE::sap2nagios($self->{ACTUAL_ALERT_DATA_VALUE}));
}


package MTE::SM;
our @ISA = qw(MTE);
use strict;

sub collect_details {
  my $self = shift;
  my $session = shift;
  $self->SUPER::collect_details($session);
  my $fl = $session->function_lookup("BAPI_SYSTEM_MTE_GETSMVALUE");
  my $fc = $fl->create_function_call;
  $fc->TID($self->tid);
  $fc->EXTERNAL_USER_NAME("CHECK_SAP_HEALTH");
  $fc->invoke;
  foreach (qw(MSG SMSGDATE SMSGDATE SMSGVALUE)) {
    $self->{$_} = $self->strip($fc->VALUE->{$_});
  }
}

sub check {
  my $self = shift;
  $self->debug(sprintf "mte %s has alert %s",
      $self->mkMTNAMELONG(), MTE::sap2nagios($self->{ACTUAL_ALERT_DATA_VALUE}));
  $self->{MSG} ||= "<empty>";
  $self->add_info($self->{MTNAGIOSNAME}." = ".$self->{MSG});
  $self->add_message(MTE::sap2nagios($self->{ACTUAL_ALERT_DATA_VALUE}));
}


package MTE::ST;
our @ISA = qw(MTE);
use strict;

sub collect_details {
  my $self = shift;
  my $session = shift;
  $self->SUPER::collect_details($session);
  my $fl = $session->function_lookup("BAPI_SYSTEM_MTE_GETTXTPROP");
  my $fc = $fl->create_function_call;
  $fc->TID($self->tid);
  $fc->EXTERNAL_USER_NAME("CHECK_SAP_HEALTH");
  $fc->invoke;
  foreach (qw(TEXT)) {
    $self->{$_} = $self->strip($fc->PROPERTIES->{$_});
  }
}

sub check {
  my $self = shift;
  $self->debug(sprintf "mte %s has alert 0",
      $self->mkMTNAMELONG());
  $self->{TEXT} ||= "<empty>";
  $self->add_info($self->{MTNAGIOSNAME}." = ".$self->{TEXT});
  $self->add_ok();
  if ($self->{TEXT} =~ /([\d\.]+)\s*(s|%|[kmgt]{0,1}b|ms|msec)($|\s)/) {
    my $value = $1;
    my $unit = $2;
    $self->add_perfdata(
        label => $self->{OBJECTNAME}."_".$self->{MTNAGIOSNAME},
        value => $value,
        uom => $unit eq "msec" ? "ms" : $unit,
    );
  }
}

package CheckSapHealth::SAP::Netweaver::Component::SNAP;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::Item);
use strict;


sub init {
  my $self = shift;
  if ($self->mode =~ /netweaver::snap::shortdumps/) {
    eval {
      my $now = time - 1;
      my($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
          localtime($now);
      my $todate = sprintf "%04d%02d%02d", $year + 1900, $mon + 1, $mday;
      my $totime = sprintf "%02d%02d%02d", $hour, $min, $sec;
      my $from = $self->opts->lookback ? $now - $self->opts->lookback :
          $self->load_state( name => "to" ) ? $self->load_state( name => "to" )->{to} :
          $now - 3600;
      ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
          localtime($from + 1);
      my $fromdate = sprintf "%04d%02d%02d", $year + 1900, $mon + 1, $mday;
      my $fromtime = sprintf "%02d%02d%02d", $hour, $min, $sec;
      my $fl = $self->session->function_lookup("RFC_READ_TABLE");
      my $fc = $fl->create_function_call;
      $fc->QUERY_TABLE("SNAP");
      $fc->DELIMITER(";");
      my $condition = sprintf "SEQNO = '000' AND ( DATUM > '%s' OR ( DATUM = '%s' AND UZEIT > '%s' ) ) AND ( DATUM < '%s' OR ( DATUM = '%s' AND UZEIT <= '%s' ) )",
          $fromdate, $fromdate, $fromtime, $todate, $todate, $totime;
      my @options = ();
      while ($condition ne "") {
        $condition =~ /(.{1,70}(\s|$))/ms;
        push(@options, {'TEXT' => $1});
        $condition = $';
      }
      $fc->OPTIONS(\@options);
      $fc->FIELDS([
          { 'FIELDNAME' => 'DATUM' },
          { 'FIELDNAME' => 'UZEIT' },
          { 'FIELDNAME' => 'AHOST' },
          { 'FIELDNAME' => 'UNAME' },
          { 'FIELDNAME' => 'MANDT' },
          { 'FIELDNAME' => 'FLIST' }
      ]);
      $fc->invoke;
      #printf "%s\n", Data::Dumper::Dumper($fc);
      my @rows = @{$fc->DATA};
      my @shortdumps = ();
      foreach (@rows) {
        my $shortdump = {};
        (my $dump = $_->{WA}) =~ s/\s+$//;
        ($shortdump->{datum}, $shortdump->{uzeit},
            $shortdump->{ahost}, $shortdump->{uname},
            $shortdump->{mandt}, $shortdump->{flist}) = 
            map { s/^\s+//; s/\s+$//; $_ } split(";", $dump);
        $shortdump->{error} = substr($shortdump->{flist}, 5,
            (index($shortdump->{flist}, 'AP0') - 5));
        $shortdump->{program} = substr($shortdump->{flist},
            (index($shortdump->{flist}, 'AP0') + 5),
            (index($shortdump->{flist}, 'AI0') -
            (index($shortdump->{flist}, 'AP0') + 5)));
        $shortdump->{bgcolor} = "#f83838";
        next if ! $self->filter_name($shortdump->{uname});
        next if ! $self->filter_name2($shortdump->{program});
        push(@shortdumps, $shortdump);
      }
      if (scalar(@shortdumps) == 0) {
        $self->add_ok(sprintf "no new shortdumps between %s %s and %s %s",
            $fromdate, $fromtime, $todate, $totime);
      } else {
        if ($self->mode =~ /netweaver::snap::shortdumps::list/) {
          foreach my $row (@rows) {
            (my $dump = $row->{WA}) =~ s/\s+$//;
            $dump = join(";", map { s/^\s+//; s/\s+$//; $_ } split(";", $dump));
            printf "%s\n", $dump;
          }
        } elsif ($self->mode =~ /netweaver::snap::shortdumps::/) {
          my $num_shortdumps = scalar(@shortdumps);
          $self->add_info(sprintf "%d new shortdumps appeared between %s %s and %s %s",
              $num_shortdumps, $fromdate, $fromtime, $todate, $totime);
          $self->set_thresholds(warning => 50, critical => 100, metric => 'shortdumps');
          $self->add_message($self->check_thresholds(value => $num_shortdumps, metric => 'shortdumps'));
          $self->add_perfdata(
              label => 'shortdumps',
              value => $num_shortdumps
          );
          my $table = [];
          my @titles = ();
          my $unique_dumps = {};
          if ($self->mode =~ /netweaver::snap::shortdumps::recurrence/) {
            my $max_unique_shortdumps = 0;
            my $max_unique_overflows = 0;
            foreach my $shortdump (@shortdumps) {
              my $signature = join("_", map { $shortdump->{$_} } qw(ahost uname mandt error program));
              if (! exists $unique_dumps->{$signature}) {
                $unique_dumps->{$signature} = {
                  count => 1,
                  dump => $shortdump,
                };
              } else {
                $unique_dumps->{$signature}->{count}++;
              }
            }
            $self->set_thresholds(warning => 50, critical => 100, metric => 'max_unique_shortdumps');
            foreach my $unique_dump (map { $unique_dumps->{$_} } keys %{$unique_dumps}) {
              $max_unique_shortdumps = $unique_dump->{count} if ($unique_dump->{count} > $max_unique_shortdumps);
              $max_unique_overflows++ if $self->check_thresholds(value => $unique_dump->{count}, metric => 'max_unique_shortdumps');
            }
            $self->add_info(sprintf "the most frequent error appeared %d times", $max_unique_shortdumps);
            $self->add_message($self->check_thresholds(value => $max_unique_shortdumps, metric => 'max_unique_shortdumps'));
            $self->add_perfdata(
                label => 'max_unique_shortdumps',
                value => $max_unique_shortdumps
            );
          }
          if ($self->opts->report eq "html") {
            if ($self->mode =~ /netweaver::snap::shortdumps::count/) {
              @titles = qw(datum uzeit ahost uname mandt error program);
              foreach my $shortdump (@shortdumps) {
                push(@{$table}, [map { [$shortdump->{$_}, 2] } @titles]);
              }
            } elsif ($self->mode =~ /netweaver::snap::shortdumps::recurrence/) {
              @titles = qw(count ahost uname mandt error program);
              foreach my $unique_dump (map { 
                  $unique_dumps->{$_}
              } reverse sort {
                  $unique_dumps->{$a}->{count} <=> $unique_dumps->{$b}->{count} 
              } keys %{$unique_dumps}) {
                my $level = $self->check_thresholds(value => $num_shortdumps, metric => 'shortdumps') ?
                    $self->check_thresholds(value => $num_shortdumps, metric => 'shortdumps') :
                    $self->check_thresholds(value => $unique_dump->{count}, metric => 'max_unique_shortdumps');
                my @line = ([$unique_dump->{count}, $level]);
                push(@line, map {
                    [$unique_dump->{dump}->{$_}, $level]
                } qw(ahost uname mandt error program));
                push(@{$table}, \@line);
              }
            }
            $self->add_html($self->table_html($table, \@titles));
            my ($code, $message) = $self->check_messages();
            printf "%s - %s%s\n", $self->status_code($code), $message, $self->perfdata_string() ? " | ".$self->perfdata_string() : "";
            $self->suppress_messages();
            print $self->html_string();
            printf "\n <!--\nASCII_NOTIFICATION_START\n";
            printf "%s - %s%s\n", $self->status_code($code), $message, $self->perfdata_string() ? " | ".$self->perfdata_string() : "";
            printf "%s", $self->table_ascii($table, \@titles);
            printf "ASCII_NOTIFICATION_END\n-->\n";
          }
        }
      }
      $self->save_state( name => "to", save => {to => $now} );
    };
    if ($@) {
      $self->add_unknown($@);
    }
  }
}

package CheckSapHealth::SAP::Netweaver::Component::UpdateSubsystem;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::Item);
use strict;


sub init {
  my $self = shift;
  if ($self->mode =~ /netweaver::updates::failed/) {
    eval {
      my $now = time - 1;
      my($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
          localtime($now);
      my $todate = sprintf "%04d%02d%02d", $year + 1900, $mon + 1, $mday;
      my $totime = sprintf "%02d%02d%02d", $hour, $min, $sec;
      my $from = $self->opts->lookback ? $now - $self->opts->lookback :
          $self->load_state( name => "to" ) ? $self->load_state( name => "to" )->{to} :
          $now - 3600;
      ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
          localtime($from + 1);
      my $fromdate = sprintf "%04d%02d%02d", $year + 1900, $mon + 1, $mday;
      my $fromtime = sprintf "%02d%02d%02d", $hour, $min, $sec;
      my $fl = $self->session->function_lookup("RFC_READ_TABLE");
      my $fc = $fl->create_function_call;
      $fc->QUERY_TABLE("VBHDR");
      $fc->DELIMITER(";");
      my $condition = sprintf "VBDATE > '%s%s'", $fromdate, $fromtime;
      my @options = ();
      while ($condition ne "") {
        $condition =~ /(.{1,70}(\s|$))/ms;
        push(@options, {'TEXT' => $1});
        $condition = $';
      }
      $fc->OPTIONS(\@options);
      $fc->FIELDS([
          { 'FIELDNAME' => 'VBKEY' },
          { 'FIELDNAME' => 'VBREPORT' },
          { 'FIELDNAME' => 'VBENQKEY' },
          { 'FIELDNAME' => 'VBDATE' }
      ]);
      $fc->invoke;
      my @rows = @{$fc->DATA};
      my $failed_updates = scalar(@rows);
      $self->set_thresholds(warning => 10, critical => 20);
      if ($failed_updates == 0) {
        $self->add_info("no failed updates in system");
      } else {
        $self->add_info(
            sprintf "%d new failed update records appeared between %s %s and %s %s", 
            $failed_updates, $fromdate, $fromtime, $todate, $totime);
      }
      $self->add_message($self->check_thresholds($failed_updates));
      $self->add_perfdata(
          label => 'failed_updates',
          value => $failed_updates,
      );
      $self->save_state( name => "to", save => {to => $now} );
    };
    if ($@) {
      $self->add_unknown($@);
    }
  }
}

package CheckSapHealth::SAP::Netweaver::Component::BackgroundjobSubsystem;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::Item);
use strict;


sub init {
  my $self = shift;
  $self->{jobs} = [];
  eval {
    my $now = time - 1;
    my ($todate, $totime) = $self->epoch_to_abap_date_and_time($now);
    my $from = $self->opts->lookback ? $now - $self->opts->lookback :
        $self->load_state( name => "to" ) ? $self->load_state( name => "to" )->{to} :
        $now - 3600;
    my ($fromdate, $fromtime) = $self->epoch_to_abap_date_and_time($from + 1);
    my $fl = $self->session->function_lookup("RFC_READ_TABLE");
    my $fc = $fl->create_function_call;
    $fc->QUERY_TABLE("TBTCO");
    $fc->DELIMITER(";");
    my $condition = sprintf "ENDDATE > '%s' OR ( ENDDATE = '%s' AND ENDTIME > '%s' )", $fromdate, $fromdate, $fromtime;
    my @options = ();
    while ($condition ne "") {
      $condition =~ /(.{1,70}(\s|$))/ms;
      push(@options, {'TEXT' => $1});
      $condition = $';
    }
    $fc->OPTIONS(\@options);
    # This was the original order
    # my @fields = qw(JOBNAME SDLUNAME STRTDATE STRTTIME ENDDATE ENDTIME STATUS SDLSTRTDT SDLSTRTTM);
    # beginning from 7.50 SP23 the returned values in $fc->DATA...->{WA} no
    # longer corresponded to these fields. Instead (no matter how @fields
    # was arranged), the returned order was always JOBNAME SDLSTRTDT SDLSTRTTM SDLUNAME STRTDATE STRTTIME ENDDATE ENDTIME STATUS.
    # So this is then used as the the new @fields
    my @fields = qw(JOBNAME SDLSTRTDT SDLSTRTTM SDLUNAME STRTDATE STRTTIME ENDDATE ENDTIME STATUS);
    # For older systems this means, SAP returns the DATA in this order as it was
    # told to do so.
    # For newer systems, the @fields is only used to correctly identify the
    # columns in the splitted WA string.
    $fc->FIELDS([map { { 'FIELDNAME' => $_ } } @fields]);
    $fc->invoke;
    # This is just for debugging purposes if things go wrong again in the future
    my @return_fields = $fc->FIELDS;
    @{$self->{jobs}} = sort {
      $a->{stop} <=> $b->{stop}
    } grep {
      $self->filter_name($_->{JOBNAME}) && $self->filter_name2($_->{SDLUNAME});
    } map {
      my %hash = ();
      my @values = split(";", $_->{WA});
      @hash{@fields} = @values;
      Job->new(%hash);
    } @{$fc->DATA};
    $self->save_state( name => "to", save => {to => $now} );
  };
  if ($@) {
    $self->add_unknown($@);
  }
}

sub check {
  my $self = shift;
  if (! $self->check_messages()) {
    if (! @{$self->{jobs}}) {
      $self->add_unknown("no finished jobs were found");
    } else {
      if ($self->mode =~ /netweaver::backgroundjobs::list/) {
        foreach (@{$self->{jobs}}) {
          printf "%-12s %-32s %s %4d %4d %s\n", $_->{SDLUNAME}, $_->{JOBNAME},
              $_->{output_start}, $_->{runtime}, $_->{delay}, $_->{STATUS};
        }
      } else {
        my $jobs = {};
        map { $jobs->{$_->{JOBNAME}.$_->{SDLUNAME}}++ } @{$self->{jobs}};
        if ($self->mode =~ /netweaver::backgroundjobs::(failed|runtime)/) {
          foreach (@{$self->{jobs}}) {
            $_->check() if (! $self->opts->unique || ($self->opts->unique && ! --$jobs->{$_->{JOBNAME}.$_->{SDLUNAME}}));
          }
        }
        if (! $self->check_messages()) {
          $self->add_ok("all jobs finished in time with status ok");
        }
      }
    }
  }
}

package Job;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::TableItem);
use strict;
use Date::Manip::Date;

sub finish {
  my $self = shift;
  # man kann eigentlich davon ausgehen, dass jeder Job STRT* und END* hat,
  # da die Tabelle TBTCO nur beendete (mit welchem Status auch immer) Eintraege
  # enthaelt.
  foreach (keys %{$self}) {
    $self->{$_} =~ s/^\s+//;
    $self->{$_} =~ s/\s+$//;
  }
  my $date = new Date::Manip::Date;
  if ($self->{ENDDATE} && $self->{ENDTIME}) {
    $date->parse_format("%Y%m%d%H%M%S", $self->{ENDDATE}.$self->{ENDTIME});
    $self->{stop} = $date->printf("%s");
    $self->{output_stop} = $date->printf("%d.%m.%Y %H:%M:%S");
  }
  if ($self->{STRTDATE} && $self->{STRTTIME}) {
    $date->parse_format("%Y%m%d%H%M%S", $self->{STRTDATE}.$self->{STRTTIME});
    $self->{start} = $date->printf("%s");
    $self->{output_start} = $date->printf("%d.%m.%Y %H:%M:%S");
  }
  if ($self->{start} && $self->{stop}) {
    $self->{runtime} = $self->{stop} - $self->{start};
  }
  if ($self->{SDLSTRTDT} && $self->{SDLSTRTTM}) {
    $date->parse_format("%Y%m%d%H%M%S", $self->{SDLSTRTDT}.$self->{SDLSTRTTM});
    $self->{planned_start} = $date->printf("%s");
  }
  if ($self->{start} && $self->{planned_start}) {
    $self->{delay} = $self->{start} - $self->{planned_start};
  }
}

sub check {
  my $self = shift;
  if ($self->mode =~ /netweaver::backgroundjobs::(failed|runtime)/) {
    if ($self->{STATUS} eq "A") {
      $self->add_critical(sprintf "job %s failed at %s", $self->{JOBNAME}, $self->{output_stop});
    }
  }
  if ($self->mode =~ /netweaver::backgroundjobs::runtime/) {
    $self->set_thresholds(metric => $self->{SDLUNAME}.'_'.$self->{JOBNAME}.'_runtime',
       warning => 60, critical => 300);
    $self->add_info(sprintf "job %s of user %s ran for %ds",
        $self->{JOBNAME}, $self->{SDLUNAME}, $self->{runtime});
    if ($self->check_thresholds(metric => $self->{SDLUNAME}.'_'.$self->{JOBNAME}.'_runtime',
        value => $self->{runtime},)) {
      my ($warning, $critical) = $self->get_thresholds(
          metric => $self->{SDLUNAME}.'_'.$self->{JOBNAME}.'_runtime');
      $self->annotate_info(sprintf "limit: %ds", $self->{runtime} > $critical ?
          $critical : $warning);
      $self->add_message($self->check_thresholds(
          metric => $self->{SDLUNAME}.'_'.$self->{JOBNAME}.'_runtime',
          value => $self->{runtime},
      ));
    }
    $self->add_perfdata(
        label => $self->{SDLUNAME}.'_'.$self->{JOBNAME}.'_runtime',
        value => $self->{runtime},
        uom => 's',
    );
  }
}

package CheckSapHealth::SAP::Netweaver::Component::ProcessSubsystem;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::Item);
use strict;


sub init {
  my $self = shift;
  my $fl = $self->session->function_lookup("TH_WPINFO");
  my $fc = $fl->create_function_call;
  my @fields = qw(WP_TYP WP_STATUS WP_PID);
  $fc->invoke;
  @{$self->{workprocs}} = map {
    CheckSapHealth::SAP::Netweaver::Component::ProcessSubsystem::WorkProc->new(
        WP_TYP => $_->{WP_TYP},
        WP_PID => $_->{WP_PID},
        WP_STATUS => $_->{WP_STATUS},
    );
  } @{$fc->WPLIST};
  if ($self->mode =~ /netweaver::processes::count/) {
    # Note 39412
    $self->{types} = [qw(DIA UPD UP2 BGD ENQ SPO)];
    $self->{types} = [map {
        s/^\s*//g; $_;
    } map {
        s/\s*$//g; $_;
    } map {
        uc $_;
    } split(/,/, $self->opts->name)] if $self->opts->name;
    $self->{num_types} = {};
    foreach my $type (@{$self->{types}}) {
      $self->{num_types}->{$type} = 0;
      map { $self->{num_types}->{$_->{WP_TYP}}++ if $_->{WP_TYP} eq $type; } @{$self->{workprocs}};
    }
  }
}

sub check {
  my $self = shift;
  if ($self->mode =~ /netweaver::processes::list/) {
    $self->SUPER::check();
    $self->add_ok("have fun");
  } elsif ($self->mode =~ /netweaver::processes::count/) {
    foreach my $type (@{$self->{types}}) {
      $self->{num_types}->{$type} = 0 if ! exists $self->{num_types}->{$type};
      my $metric = lc 'num_'.$type;
      $self->set_thresholds(metric => $metric,
          warning => '1:', critical => '1:',
      );
      $self->add_message(
          $self->check_thresholds(metric => $metric, value => $self->{num_types}->{$type}),
          sprintf "%d %s process%s", $self->{num_types}->{$type}, $type, $self->{num_types}->{$type} == 1 ? "" : "es");
      $self->add_perfdata(
          label => $metric, value => $self->{num_types}->{$type},
      );
    }
  } else {
    if (! @{$self->{workprocs}}) {
      $self->add_unknown("no workprocs were found");
    }
  }
}


package CheckSapHealth::SAP::Netweaver::Component::ProcessSubsystem::WorkProc;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::TableItem);
use strict;

sub rstrip {
  my $self = shift;
  my $message = shift;
  $message =~ s/\s+$//g;
  chomp $message;
  return $message;
}

sub strip {
  my $self = shift;
  my $message = shift;
  if (ref($message) eq "HASH") {
    foreach (keys %{$message}) {
      $self->strip($message->{$_});
    }
  } else {
    $message =~ s/^\s+//g;
    $message = $self->rstrip($message);
  }
  return $message;
}

sub finish {
  my $self = shift;
  foreach (qw(WP_TYP WP_PID WP_STATUS)) {
    $self->{$_} = $self->strip($self->{$_});
  }
  # BTC and BGD are technically the same, it is only a different translation
  # from German (BTC) to English (BGD).
  # Depending on the logon language the WP type is either BTC or BGD,
  # but never both.
  if ($self->{WP_TYP} eq 'BTC') {
    $self->{WP_TYP} = 'BGD';
  }
}

sub check {
  my $self = shift;
  if ($self->mode =~ /netweaver::processes::list/) {
    printf "%s %s %s\n", $self->{WP_TYP}, $self->{WP_PID}, $self->{WP_STATUS};
  }
}


package CheckSapHealth::SAP::Netweaver::Component::IdocSubsystem;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::Item);
use strict;

sub init {
  my ($self) = @_;
  $self->{idocs} = [];
  my %languages = ();
  my %messages = ();
  if ($self->mode =~ /netweaver::idocs/) {
    my $now = time - 1;
    my ($todate, $totime) = $self->epoch_to_abap_date_and_time($now);
    my $from = $self->opts->lookback ? $now - $self->opts->lookback :
        $self->load_state( name => "to" ) ? $self->load_state( name => "to" )->{to} :
        $now - 3600;
    my ($fromdate, $fromtime) = $self->epoch_to_abap_date_and_time($from + 1);

    my $t002fl = $self->session->function_lookup("RFC_READ_TABLE");
    my $t002fc = $t002fl->create_function_call;
    $t002fc->QUERY_TABLE("T002");
    $t002fc->DELIMITER(";");
    my @t002fcfields = qw(SPRAS LAISO);
    $t002fc->FIELDS([map { { 'FIELDNAME' => $_ } } @t002fcfields]);
    $t002fc->invoke;
    map {
        $languages{$_->[0]} = $_->[1];
    } map {
        my($a, $b) = ($_->[0], $_->[1]); $b =~ s/\s*$//; [$a, $b];
    } map {
        [split(';', $_->{WA})];
    } @{$t002fc->DATA};
    %languages = reverse %languages;
    my $shortlang = $languages{uc $self->opts->lang};

    my $teds2fl = $self->session->function_lookup("RFC_READ_TABLE");
    my $teds2fc = $teds2fl->create_function_call;
    $teds2fc->QUERY_TABLE("TEDS2");
    $teds2fc->DELIMITER(";");
    my @teds2fcfields = qw(STATUS LANGUA DESCRP);
    $teds2fc->FIELDS([map { { 'FIELDNAME' => $_ } } @teds2fcfields]);
    $teds2fc->invoke;
    map {
        $messages{$_->[0]}{$_->[1]} = $_->[2];
    } map {
        my($a, $b, $c) = ($_->[0], $_->[1], $_->[2]); $c =~ s/\s*$//; [$a, $b, $c];
    } map {
        [split(';', $_->{WA})];
    } @{$teds2fc->DATA};

    $self->{num_idoc_status} = 0;
    my $fl = $self->session->function_lookup("RFC_READ_TABLE");
    my $fc = $fl->create_function_call;
    $fc->QUERY_TABLE("EDIDS");
    $fc->DELIMITER(";");
    my $condition = sprintf "LOGDAT >= '%s' AND LOGTIM > '%s'", $fromdate, $fromtime;
    my @options = ();
    while ($condition ne "") {
      $condition =~ /(.{1,70}(\s|$))/ms;
      push(@options, {'TEXT' => $1});
      $condition = $';
    }
    $fc->OPTIONS(\@options);
    my @fields = qw(MANDT DOCNUM LOGDAT LOGTIM COUNTR STATUS UNAME REPID STATXT STATYP);
    $fc->FIELDS([map { { 'FIELDNAME' => $_ } } @fields]);
    $fc->invoke;
    @{$self->{all_idocs}} = grep {
      $self->filter_name($_->{MANDT}) && $self->filter_name2($_->{REPID});
    } map {
      my %hash = ();
      my @values = split(";", $_->{WA});
      @hash{@fields} = @values;
      eval {
        $hash{STATUSDESCRP} = $messages{$hash{STATUS}}{$shortlang};
      };
      $hash{STATUSDESCRP} = '-unknown-' if $@;
      IdocStatus->new(%hash);
    } @{$fc->DATA};
    # Sortieren nach DOCNUM, LOGDAT+LOGTIM, COUNTR descending und dann
    # nur den aktuellsten Eintrag pro DOCNUM rausfiltern.
    my %seen = ();
    @{$self->{idocs}} = map {
      $seen{$_->{DOCNUM}} = 1;
      $_;
    } grep {
      $self->{num_idoc_status}++;
      not exists $seen{$_->{DOCNUM}};
    } sort {
      $a->{DOCNUM} <=> $b->{DOCNUM} or
      $b->{TIMESTAMP} <=> $a->{TIMESTAMP} or
      $b->{COUNTR} <=> $a->{COUNTR}
    } @{$self->{all_idocs}};
    delete $self->{all_idocs};
  }
}

sub check {
  my ($self) = @_;
  foreach my $status (@{$self->{idocs}}) {
    $status->check();
  }
  if (! $self->check_messages()) {
    $self->reduce_messages_short(sprintf "%d status checked, no problems found", $self->{num_idoc_status});
  }
}


package IdocStatus;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::TableItem);
use strict;

sub finish {
  my ($self) = @_;
  my @fields = qw(MANDT DOCNUM LOGDAT LOGTIM COUNTR STATUS UNAME REPID STATXT STATYP);
  foreach (@fields) {
    if (! defined $self->{$_}) {
      $self->{$_} = '-undef-';
    }
    $self->{$_} =~ s/^\s+//g;
    $self->{$_} =~ s/\s+$//g;
  }
  $self->{STATYP} ||= 'I';
  $self->{STATYPLONG} = {
    'A' => 'Cancel',
    'W' => 'Warning',
    'E' => 'Error',
    'S' => 'Success',
    'I' => 'Information',
  }->{$self->{STATYP}};
  $self->{TIMESTAMP} = $self->abap_date_and_time_to_epoch(
    $self->{LOGDAT}, $self->{LOGTIM}
  );
}

sub check {
  my ($self) = @_;
  if ($self->mode =~ /netweaver::idocs::list/) {
    printf "%s;%s;%s;%s;%s;%s;%s;%s;%s;%s\n",
        $self->{MANDT}, $self->{DOCNUM}, $self->{LOGDAT},
        $self->{LOGTIM}, $self->{STATUS}, $self->{STATUSDESCRP},
        $self->{UNAME}, $self->{REPID}, $self->{STATXT}, $self->{STATYP};
  } elsif ($self->mode =~ /netweaver::idocs::failed/) {
    $self->add_info(sprintf "idoc %s has status \"%s\" (%s) at %s",
        $self->{DOCNUM}, $self->{STATUSDESCRP}, $self->{STATYPLONG}, 
        scalar localtime $self->{TIMESTAMP});
    if ($self->{STATYP} eq "A") {
      $self->add_ok();
    } elsif ($self->{STATYP} eq "W") {
      $self->add_warning();
    } elsif ($self->{STATYP} eq "E") {
      $self->add_critical();
    } elsif ($self->{STATYP} eq "S") {
      $self->add_ok();
    } elsif ($self->{STATYP} eq "I") {
      $self->add_ok();
    }
  }
}

package CheckSapHealth::SAP::Netweaver::Component::WorkloadSubsystem;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::Item);
use strict;

# SE24 CL_SWNC_CONSTANTS, GET_TASKTYPES
our $tasktypes = {
    '00' => 'NONE',
    '01' => 'DIALOG',
    '02' => 'UPDATE',
    '03' => 'SPOOL',
    '04' => 'BACKGROUND',
    '05' => 'ENQUEUE',
    '06' => 'BUFFER SYNC',
    '07' => 'AUTOABAP',
    '08' => 'UPDATE2',
    '09' => 'NATIVE RFC',
    '0A' => 'EXT.PLUGIN',
    '0B' => 'AUTOTH',
    '0C' => 'RPCTH',
    '0D' => 'RFCVMC',
    '0E' => 'DDLOG CLEANUP',
    '0F' => 'DEL. THCALL',
    '10' => 'AUTOJAVA',
    '11' => 'LICENCESRV',
    '12' => 'AUTOCCMS',
    '13' => 'MSADM',
    '14' => 'SYS. STARTUP',
    '15' => 'BGRFC Scheduler',
    '16' => 'BGRFC Unit',
    '17' => 'APC',
    '18' => 'AMC',
    '21' => 'OTHER',
    '22' => 'DIALOG(-)GUI',
    '23' => 'B.INPUT',
    '65' => 'HTTP',
    '66' => 'HTTPS',
    '67' => 'NNTP',
    '68' => 'SMTP',
    '69' => 'FTP',
    '6C' => 'LCOM',
    '75' => 'HTTP/JSP',
    '76' => 'HTTPS/JSP',
    'F7' => 'LR.RFC',
    'F8' => 'NBR.BUFFER',
    'F9' => 'AUTORFC',
    'FA' => 'WS-RFC',
    'FB' => 'WS-HTTP',
    'FC' => 'ESI',
    'FD' => 'ALE',
    'FE' => 'RFC',
    'FF' => 'CPIC',
};

sub translate_tasktype {
  my ($self, $tasktype) = @_;
  return exists $tasktypes->{uc $tasktype} ?
      $tasktypes->{uc $tasktype} : 'UNKNOWN';
}

sub init {
  my ($self) = @_;
  if ($self->mode =~ /netweaver::workload/) {
    $self->{tasktypes} = [];
    my $now = time - 1;
    my ($todate, $totime) = $self->epoch_to_abap_date_and_time($now);
    my $from = $self->opts->lookback ? $now - $self->opts->lookback :
        $self->load_state( name => "to" ) ?
            $self->load_state( name => "to" )->{to} : $now - 3600;
    my ($fromdate, $fromtime) = $self->epoch_to_abap_date_and_time($from + 1);
    my $fl = $self->session->function_lookup("SWNC_GET_WORKLOAD_SNAPSHOT");
    my $fc = $fl->create_function_call;
    $fc->READ_START_DATE($fromdate);
    $fc->READ_START_TIME($fromtime);
    $fc->READ_END_DATE($todate);
    $fc->READ_END_TIME($totime);
    if ($self->opts->name2) {
      $fc->SELECT_SERVER($self->opts->name2);
    }
    if ($self->opts->name3) {
      $fc->READ_CLIENT($self->opts->name3);
    }
    $fc->invoke;
    my $avg_times = {};
    my @types = @{$fc->TASKTYPE};
    foreach my $row (@types) {
      my $entry_id = uc unpack "H*", $row->{TASKTYPE};
      foreach my $key (keys %{$row}) {
        if ($key eq "COUNT" || $key =~ /.*TI$/) {
          if (exists $avg_times->{$entry_id}->{$key}) {
            $avg_times->{$entry_id}->{$key} += $row->{$key};
          } else {
            $avg_times->{$entry_id}->{$key} = $row->{$key};
          }
        }
      }
    }
    foreach my $tasktype (keys %{$tasktypes}) {
      if (exists $avg_times->{$tasktype}) {
        my @times = ();
        foreach my $key (keys %{$avg_times->{$tasktype}}) {
          if ($key =~ /.*TI$/) {
            $avg_times->{$tasktype}->{$key.'AVG'} =
                $avg_times->{$tasktype}->{$key} /
                $avg_times->{$tasktype}->{COUNT};
          }
        }
      } else {
        $avg_times->{$tasktype}->{RESPTIAVG} = 0;
        $avg_times->{$tasktype}->{COUNT} = 0;
      }
      next if ! $self->filter_name($self->translate_tasktype($tasktype));
      push(@{$self->{tasktypes}},
          CheckSapHealth::SAP::Netweaver::Component::WorkloadSubsystem::Task->new(
              name => $self->translate_tasktype($tasktype),
              count => $avg_times->{$tasktype}->{COUNT},
              avg_response_time => $avg_times->{$tasktype}->{RESPTIAVG},
      ));
      $self->save_state( name => "to", save => {to => $now} );
    };
  }
}


package CheckSapHealth::SAP::Netweaver::Component::WorkloadSubsystem::Task;
our @ISA = qw(CheckSapHealth::SAP::Netweaver::TableItem);
use strict;

sub check {
  my ($self) = @_;
  $self->valdiff({ name => $self->{name} }, qw(count));
  my $label = $self->{name}.'_avg_response_time';
  $self->add_info(sprintf "%s: %d steps (%.2f/s), avg time was %.2fms",
      $self->{name}, $self->{count}, $self->{count_per_sec},
      $self->{avg_response_time});
  $self->set_thresholds(
      metric => $label,
      warning => 500,
      critical => 1000,
  );
  $self->add_message($self->check_thresholds(
      metric => $label,
      value => $self->{avg_response_time})
  );
  $self->add_perfdata(
      label => $label,
      value => $self->{avg_response_time},
      uom => 'ms',
  );
  $self->add_perfdata(
      label => $self->{name}.'_steps_per_sec',
      value => $self->{count_per_sec},
      uom => 'ms',
  );
}

package CheckSapHealth::Device;
our @ISA = qw(Monitoring::GLPlugin);
use strict;


sub classify {
  my $self = shift;
  #if (! (($self->opts->ashost || $self->opts->mshost) && $self->opts->username && $self->opts->password)) {
  if (! ($self->opts->ashost || $self->opts->mshost)) {
    $self->add_unknown('specify at least hostname, username and password');
  } else {
    bless $self, 'CheckSapHealth::SAP::Netweaver';
    $self->debug('using CheckSapHealth::SAP::Netweaver');
    $self->check_rfc_and_model();
  }
  if ($self->opts->mode =~ /^my-/) {
    $self->load_my_extension();
  }
}

package main;
package CheckSapHealth;
use strict;
no warnings qw(once);
      
sub run_plugin {
  my $plugin_class = (caller(0))[0]."::Device";
  if ( ! grep /BEGIN/, keys %Monitoring::GLPlugin::) {
    eval { 
      require Monitoring::GLPlugin;
    };
    if ($@) {
      printf "UNKNOWN - module Monitoring::GLPlugin was not found. Either build a standalone version of this plugin or set PERL5LIB\n";
      printf "%s\n", $@;
      exit 3;
    }
  }   
      
  my $plugin = $plugin_class->new(
      shortname => '',
      usage => 'Usage: %s [ -v|--verbose ] [ -t <timeout> ] '.
          '--mode <what-to-do> '.
          '--ashost <hostname> --sysnr <system number> '.
          '  ...]',
      version => '$Revision: 4.0.0.3 $',
      blurb => 'This plugin checks sap netweaver ',
      url => 'http://labs.consol.de/nagios/check_sap_health',
      timeout => 60,
      plugin => $Monitoring::GLPlugin::pluginname,
  );  
  $plugin->add_mode(
      internal => 'device::uptime',
      spec => 'uptime',
      alias => undef,
      help => 'Check the uptime of the device',
  );
  $plugin->add_mode(
      internal => 'netweaver::connectiontime',
      spec => 'connection-time',
      alias => undef,
      help => 'Time to connect to the server',
  );
  $plugin->add_mode(
      internal => 'netweaver::connectiontime::sapinfo',
      spec => 'sapinfo',
      alias => undef,
      help => 'Time to connect and show system atttributes like sapinfo',
  );
  $plugin->add_mode(
      internal => 'netweaver::ccms::moniset::list',
      spec => 'list-ccms-monitor-sets',
      alias => undef,
      help => 'List all available monitor sets',
  );
  $plugin->add_mode(
      internal => 'netweaver::ccms::monitor::list',
      spec => 'list-ccms-monitors',
      alias => undef,
      help => 'List all monitors (can be restricted to a monitor set with --name)',
  );
  $plugin->add_mode(
      internal => 'netweaver::ccms::mte::list',
      spec => 'list-ccms-mtes',
      alias => undef,
      help => 'List all MTEs (must be restricted to a monitor set / monitor with --name/--name2)',
  );
  $plugin->add_mode(
      internal => 'netweaver::ccms::mte::check',
      spec => 'ccms-mte-check',
      alias => undef,
      help => 'Check all MTEs (must be restricted to a monitor set / monitor with --name/--name2)',
  );
  $plugin->add_mode(
      internal => 'netweaver::snap::shortdumps::list',
      spec => 'shortdumps-list',
      alias => ['list-shortdumps'],
      help => 'Read the SNAP table and list the short dumps',
  );
  $plugin->add_mode(
      internal => 'netweaver::snap::shortdumps::count',
      spec => 'shortdumps-count',
      alias => undef,
      help => 'Read the SNAP table and count the short dumps (can be restricted with --name/--name2 = username/program)',
  );
  $plugin->add_mode(
      internal => 'netweaver::snap::shortdumps::recurrence',
      spec => 'shortdumps-recurrence',
      alias => undef,
      help => 'Like shortdumps-count, but counts the recurrence of the same errors',
  );
  $plugin->add_mode(
      internal => 'netweaver::updates::failed',
      spec => 'failed-updates',
      alias => undef,
      help => 'Counts new entries in the VHDR table (since last run or appeared in the interval specified by --lookback)',
  );
  $plugin->add_mode(
      internal => 'netweaver::backgroundjobs::failed',
      spec => 'failed-jobs',
      alias => undef,
      help => 'Looks for failed jobs in the TBTCO table (since last run or in the interval specified by --lookback)',
  );
  $plugin->add_mode(
      internal => 'netweaver::backgroundjobs::runtime',
      spec => 'exceeded-failed-jobs',
      alias => undef,
      help => 'Looks for jobs in the TBTCO table which failed or exceeded a certain runtime (since last run or in the interval specified by --lookback)',
  );
  $plugin->add_mode(
      internal => 'netweaver::processes::count',
      spec => 'count-processes',
      alias => undef,
      help => 'count the types of work processes',
  );
  $plugin->add_mode(
      internal => 'netweaver::workload::overview',
      spec => 'workload-overview',
      alias => undef,
      help => 'Checks response time of task types (like ST03)',
  );
  $plugin->add_mode(
      internal => 'netweaver::idocs::failed',
      spec => 'failed-idocs',
      alias => undef,
      help => 'Looks for failed IDoc-status-records in the EDIDS table',
  );
  $plugin->add_mode(
      internal => 'netweaver::processes::list',
      spec => 'list-processes',
      alias => undef,
      help => 'List the running work processes',
  );
  $plugin->add_mode(
      internal => 'netweaver::backgroundjobs::list',
      spec => 'list-jobs',
      alias => undef,
      help => 'Read the TBTCO table and list the jobs',
  );
  $plugin->add_mode(
      internal => 'netweaver::idocs::list',
      spec => 'list-idocs',
      alias => undef,
      help => 'Lists IDoc-status-records in the EDIDS table',
  );
  $plugin->add_default_modes();
  $plugin->add_default_args();
  $plugin->add_arg(
      spec => 'ashost|H=s',
      help => '--ashost
     Hostname or IP-address of the application server',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'sysnr=s',
      help => '--sysnr
     The system number',
      required => 0,
      default => '00',
  );
  $plugin->add_arg(
      spec => 'saprouter=s',
      help => '--saprouter
     A SAPRouter, should start with /H/',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'mshost=s',
      help => '--mshost
     Hostname or IP-address of the message server',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'msserv=s',
      help => '--msserv
     The port for mshost connections',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'r3name=s',
      help => '--r3name
     The SID for mshost connections',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'group=s',
      help => '--group
     The logon group for mshost connections',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'gwhost=s',
      help => '--gwhost
     The gateway host',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'gwserv=s',
      help => '--gwserv
     The gateway port',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'username=s',
      help => '--username
     The username',
      required => 0,
      decode => "rfc3986",
  );
  $plugin->add_arg(
      spec => 'password=s',
      help => '--password
     The password',
      required => 0,
      decode => "rfc3986",
  );
  $plugin->add_arg(
      spec => 'client=s',
      help => '--client
     The client',
      default => '001',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'lang=s',
      help => '--lang
     The language',
      default => 'EN',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'snc',
      help => '--snc
     This flag switches on the SNC protocol',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'secudir=s',
      help => '--secudir
     Path to the directory with SAPSNCS.pse',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'snc-partnername=s',
      aliasfor => 'sncpartnername',
      help => '--snc-partnername
     The communication partner\'s SNC name',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'snc-myname=s',
      aliasfor => 'sncmyname',
      help => '--snc-myname
     The client Personal Security Environment (PSE) Name',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'snc-qop=i',
      aliasfor => 'sncqop',
      help => '--snc-qop
     The quality of protection level',
      default => 3,
      required => 0,
  );
  $plugin->add_arg(
      spec => 'snc-lib=s',
      aliasfor => 'snclib',
      help => '--snc-lib
     The path and file name for the SAP Cryptography library',
      required => 0,
  );
  $plugin->add_arg(
      spec => 'separator=s',
      help => "--separator
     A separator for MTE path elements",
      required => 0,
  );
  $plugin->add_arg(
      spec => 'mtelong',
      help => "--mtelong
     Output the full path of MTEs",
      default => 0,
      required => 0,
  );
  $plugin->add_arg(
      spec => 'unique',
      help => "--unique
     The parameter limits the output to unique (or only the last) items.",
      required => 0,
  );
  $plugin->add_arg(
      spec => 'refresh-mtes',
      aliasfor => 'refreshmtes',
      help => "--refresh-mtes
     Rebuild the MTE cache every time. Can put some pressure on SAP, but avoids stale statefiles",
      required => 0,
  );
  
  $plugin->getopts();
  $plugin->classify();
  $plugin->validate_args();
  
  
  if (! $plugin->check_messages()) {
    $plugin->init();
    if (! $plugin->check_messages()) {
      $plugin->add_ok($plugin->get_summary())
          if $plugin->get_summary();
      $plugin->add_ok($plugin->get_extendedinfo(" "))
          if $plugin->get_extendedinfo();
    }
  } else {
    $plugin->set_failed_connection_flag();
    $plugin->add_critical('wrong device');
  }
  my ($code, $message) = $plugin->opts->multiline ?
      $plugin->check_messages(join => "\n", join_all => ', ') :
      $plugin->check_messages(join => ', ', join_all => ', ');
  $message .= sprintf "\n%s\n", $plugin->get_info("\n")
      if $plugin->opts->verbose >= 1;

  $plugin->nagios_exit($code, $message);
}

1;

join('', map { ucfirst } split(/_/, (split(/\//, (split ' ', $0 // '')[0]))[-1]))->run_plugin();
