summaryrefslogtreecommitdiff
path: root/lib/PINGDOMFETCH
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2015-01-02 13:27:02 +0100
committerPaul Buetow <paul@buetow.org>2015-01-02 13:27:02 +0100
commit336c6c8a415603c772f97ccd63912d05209f3795 (patch)
tree1a0febb81031d77fa8bec857333cce0a6d87436e /lib/PINGDOMFETCH
initial1.0.0
Diffstat (limited to 'lib/PINGDOMFETCH')
-rw-r--r--lib/PINGDOMFETCH/Config.pm301
-rw-r--r--lib/PINGDOMFETCH/DateHelper.pm179
-rw-r--r--lib/PINGDOMFETCH/Display.pm157
-rw-r--r--lib/PINGDOMFETCH/Notify.pm163
-rw-r--r--lib/PINGDOMFETCH/Pingdom.pm191
-rw-r--r--lib/PINGDOMFETCH/Pingdomfetch.pm245
-rw-r--r--lib/PINGDOMFETCH/Result.pm120
-rw-r--r--lib/PINGDOMFETCH/Service.pm105
-rw-r--r--lib/PINGDOMFETCH/TLS.pm166
-rw-r--r--lib/PINGDOMFETCH/Utils.pm72
10 files changed, 1699 insertions, 0 deletions
diff --git a/lib/PINGDOMFETCH/Config.pm b/lib/PINGDOMFETCH/Config.pm
new file mode 100644
index 0000000..1d1e4eb
--- /dev/null
+++ b/lib/PINGDOMFETCH/Config.pm
@@ -0,0 +1,301 @@
+package PINGDOMFETCH::Config;
+
+use strict;
+use warnings;
+
+use IO::File;
+
+use PINGDOMFETCH::Display;
+use PINGDOMFETCH::Notify;
+use PINGDOMFETCH::Service;
+use PINGDOMFETCH::TLS;
+use PINGDOMFETCH::Utils;
+
+our @ISA = ('PINGDOMFETCH::Display');
+
+sub new {
+ my ( $class, $opts ) = @_;
+
+ my %vals = map {
+ my $k = $_;
+ $k =~ s/_/\./g;
+ "arg.$k" => $opts->{$_}{val};
+
+ } keys %$opts;
+
+ my $self = bless \%vals, $class;
+
+ $self->SUPER::init();
+
+ $self->read_config('/etc/pingdomfetch.conf');
+ $self->read_config('pingdomfetch.conf');
+ $self->read_config($_) for sort glob("/etc/pingdomfetch.d/*.conf");
+
+ $self->read_config("$ENV{HOME}/.pingdomfetch.conf");
+ $self->read_config($_) for sort glob("$ENV{HOME}/.pingdomfetch.d/*.conf");
+
+ $self->read_config( $self->{'arg.config'} );
+
+ unless ( exists $self->{config_was_read} ) {
+ $self->warning("No config file found. Use --verbose or --help");
+ }
+
+ $self->{notify} = PINGDOMFETCH::Notify->new( config => $self );
+ $self->{has_warnings} = 0;
+
+ return $self;
+}
+
+sub read_config {
+ my ( $self, $config_file ) = @_;
+
+ return undef unless -f $config_file;
+
+ my $fh = new IO::File( $config_file, 'r' );
+ $self->error("Could not open file $config_file") unless defined $fh;
+
+ $self->verbose("Reading config $config_file");
+
+ my $section = undef;
+ my $tls = exists $self->{tls} && ref $self->{tls} ? $self->{tls} : {};
+
+ while ( my $line = $fh->getline() ) {
+
+ # Ignore comments
+ $line =~ s/(.*);.*/$1/;
+
+ if ( $line =~ /^\[(.*)\]/ ) {
+ $section = $1;
+ next;
+ }
+
+ next unless defined $section;
+
+ if ( $section eq 'pingdom'
+ or $section eq 'misc'
+ or $section eq 'notify' )
+ {
+
+ # Parse only matching lines
+ if ( $line =~ /^(.*)=(.*)/ ) {
+ my ( $key, $val ) = ( lc trim $1, trim $2);
+ $self->verbose("Reading conf value $key");
+ $self->set( $key, $val );
+ }
+
+ }
+ elsif ( $section =~ /^tls\.(.*)/ ) {
+ my ($tlsname) = ($1);
+
+ next if $line !~ /\w/;
+
+ my ( $servicename, $opts ) = split '=', trim($line);
+
+ $servicename = lc trim($servicename);
+ $opts = trim($opts) if defined $opts;
+
+ $tls->{$tlsname} = PINGDOMFETCH::TLS->new(
+ name => $tlsname,
+ config => $self,
+ services => {},
+
+ ) unless exists $tls->{$tlsname};
+
+ my %opts;
+
+ if ( defined $opts ) {
+ for ( split ',', $opts ) {
+ my ( $k, $v ) = split ':';
+ $opts{$k} = $v;
+ }
+ }
+
+ $self->verbose("TLS $tlsname includes service $servicename");
+ $tls->{$tlsname}{services}{$servicename} = { opts => \%opts };
+ }
+ }
+
+ $fh->close();
+ $self->{tls} = $tls;
+
+ $self->{config_was_read} = 1;
+
+ return undef;
+}
+
+sub read_services {
+ my ( $self, $pingdom ) = @_;
+
+ $self->verbose('Reading all the services');
+
+ my $j = $pingdom->fetch_all_checks_json();
+ my $checks = $pingdom->safe_get( $j, 'checks' );
+
+ my %services = map {
+ my $name = lc $pingdom->safe_get( $_, 'name' );
+ my $checkid = $pingdom->safe_get( $_, 'id' );
+
+ $self->verbose("$name has check id $checkid");
+
+ $name => PINGDOMFETCH::Service->new(
+ config => $self,
+ name => $name,
+ checkid => $checkid,
+ resolution => $pingdom->safe_get( $_, 'resolution' ),
+ );
+
+ } @$checks;
+
+ $self->{services} = \%services;
+
+ return undef;
+}
+
+sub read_tls {
+ my ($self) = @_;
+
+ my $services = $self->{services};
+ my $tls = $self->{tls};
+
+ for my $tlsname ( keys %$tls ) {
+ my $tlsservices = $tls->{$tlsname}{services};
+ my @tlsservicenames = keys %$tlsservices;
+
+ $self->verbose("Validating services for TLS $tlsname");
+
+ my @delete;
+
+ for ( sort @tlsservicenames ) {
+ if ( exists $services->{$_} ) {
+ $services->{$_}{opts} = $tlsservices->{$_}{opts};
+ $tlsservices->{$_} = $services->{$_};
+
+ }
+ else {
+ $self->warning(
+ "Service $_ not configured in Pingdom, ignoring it");
+ push @delete, $_;
+ }
+ }
+
+ delete $tlsservices->{$_} for @delete;
+ }
+
+ return undef;
+}
+
+sub get {
+ my ( $self, $key ) = @_;
+ $key = lc $key;
+
+ $self->{$key} //= do {
+ my $key = uc $key;
+ $key =~ s/\./_/g;
+
+ exists $ENV{$key} ? $ENV{$key} : undef;
+ };
+
+ if ( not exists $self->{$key}
+ or not defined $self->{$key}
+ or $self->{$key} eq '' )
+ {
+ $self->error("$key not configured");
+ }
+
+ $self->verbose("Getting config value $key=$self->{$key}");
+ return $self->{$key};
+}
+
+sub has {
+ my ( $self, $key ) = @_;
+ $key = lc $key;
+
+ $self->{$key} //= do {
+ my $key = uc $key;
+ $key =~ s/\./_/g;
+
+ exists $ENV{$key} ? $ENV{$key} : undef;
+ };
+
+ if ( not exists $self->{$key}
+ or not defined $self->{$key}
+ or $self->{$key} eq '' )
+ {
+ return 0;
+ }
+
+ return 1;
+}
+
+sub bool {
+ my ( $self, $key ) = @_;
+
+ my $val = $self->get($key);
+
+ return $val != 0;
+}
+
+sub array {
+ my ( $self, $key ) = @_;
+
+ my $val = $self->get($key);
+
+ return map { trim $_ } split ',', $val;
+}
+
+sub set {
+ my ( $self, $key, $val ) = @_;
+ $key = lc $key;
+
+ $self->warning("$key already configured, overwriting it with its new value")
+ if exists $self->{$key};
+
+ return $self->{$key} = $val;
+}
+
+sub get_opts_str {
+ my ( $self, $opts ) = @_;
+
+ return '' unless defined $opts;
+
+ my $opts_str = '';
+
+ if (%$opts) {
+ $opts_str = ' [';
+ $opts_str .= join ',', map { "$_:$opts->{$_}" }
+ sort keys %$opts;
+ $opts_str .= ']';
+ }
+
+ return $opts_str;
+}
+
+sub print_services {
+ my ($self) = @_;
+
+ for ( sort keys %{ $self->{services} } ) {
+ my $opts_str = $self->get_opts_str( $self->{services}{$_}{opts} );
+ $self->info( $_ . $opts_str );
+ }
+
+ return 0;
+}
+
+sub print_tls {
+ my ($self) = @_;
+
+ for my $k ( sort keys %{ $self->{tls} } ) {
+ my $v = $self->{tls}{$k};
+ $self->info($k);
+ $self->inc();
+ for ( sort keys %{ $v->{services} } ) {
+ my $opts_str = $self->get_opts_str( $v->{services}{$_}{opts} );
+ $self->info( $_ . $opts_str );
+ }
+ $self->dec();
+ }
+
+ return 0;
+}
+
+1;
diff --git a/lib/PINGDOMFETCH/DateHelper.pm b/lib/PINGDOMFETCH/DateHelper.pm
new file mode 100644
index 0000000..43c2499
--- /dev/null
+++ b/lib/PINGDOMFETCH/DateHelper.pm
@@ -0,0 +1,179 @@
+package PINGDOMFETCH::DateHelper;
+
+use strict;
+use warnings;
+
+use Date::Format;
+use Time::ParseDate;
+
+use PINGDOMFETCH::Config;
+use PINGDOMFETCH::Display;
+use PINGDOMFETCH::Utils;
+
+our @ISA = ('PINGDOMFETCH::Display');
+
+use overload '""' => sub {
+ my ($self) = @_;
+ $self->full_str();
+};
+
+sub new ($;$) {
+ my ( $class, $config, $time ) = @_;
+
+ my $self = bless { config => $config }, $class;
+
+ $self->time($time);
+
+ return $self;
+}
+
+sub time {
+ my ( $self, $time ) = @_;
+
+ my $config = $self->{config};
+
+ $time = $self->{time} if not defined $time or $time eq '';
+
+ if ( not defined $time or $time eq '' ) {
+ $time //= time();
+ return $self->{time} = $time;
+
+ }
+ elsif ( $time !~ /^\d+$/ ) {
+ my $parsed = parsedate($time);
+ $self->error("Can't parse time '$time'") unless defined $parsed;
+ $time = $parsed;
+ }
+
+ return $self->{time} = $time;
+}
+
+sub flatten {
+ my ( $self, $flatten ) = @_;
+
+ if ( $flatten eq 'bod' ) {
+ return $self->begin_of_day();
+ }
+ elsif ( $flatten eq 'eod' ) {
+ return $self->end_of_day();
+ }
+ elsif ( $flatten eq 'boh' ) {
+ return $self->begin_of_hour();
+ }
+ elsif ( $flatten eq 'eoh' ) {
+ return $self->end_of_hour();
+ }
+ else {
+ $self->error("Can't parse flatten method '$flatten'");
+ }
+}
+
+sub localtime {
+ my ( $self, $time ) = @_;
+
+ return localtime( $self->time($time) );
+}
+
+sub prev_day {
+ my ( $self, $time ) = @_;
+
+ return $self->time( $self->time($time) - 86400 );
+}
+
+sub next_day {
+ my ( $self, $time ) = @_;
+
+ return $self->time( $self->time($time) + 86400 );
+}
+
+sub begin_of_day {
+ my ( $self, $time ) = @_;
+
+ my @localtime = $self->localtime($time);
+ my ( $sec, $min, $hour, @rest ) = @localtime;
+
+ return $self->time( $self->time() - $sec - 60 * ( $min + 60 * $hour ) );
+}
+
+sub end_of_day {
+ my ( $self, $time ) = @_;
+
+ my @localtime = $self->localtime($time);
+ my ( $sec, $min, $hour, @rest ) = @localtime;
+
+ return $self->time( $self->begin_of_day() + 86399 );
+}
+
+sub begin_of_hour {
+ my ( $self, $time ) = @_;
+
+ my @localtime = $self->localtime($time);
+ my ( $sec, $min, $hour, @rest ) = @localtime;
+
+ return $self->time( $self->time() - $sec - 60 * $min );
+}
+
+sub end_of_hour {
+ my ( $self, $time ) = @_;
+
+ return $self->time( $self->begin_of_hour() + 59 * ( 1 + 60 ) );
+}
+
+sub is_a_day {
+ my ($self) = @_;
+
+ return $self->time() == 86400;
+}
+
+sub is_begin_of_a_day {
+ my ($self) = @_;
+
+ my @localtime = $self->localtime( $self->time() );
+ my ( $sec, $min, $hour, @rest ) = @localtime;
+
+ return $sec == 0 and $min == 0 and $hour == 0;
+}
+
+sub is_in_future {
+ my ($self) = @_;
+
+ my $dh = PINGDOMFETCH::DateHelper->new( $self->{config} );
+
+ return $self->time() > $dh->time() ? 1 : 0;
+}
+
+sub days_until {
+ my ( $self, $dh ) = @_;
+
+ return ( $dh->time() - $self->time() ) / 86400;
+}
+
+sub day_str {
+ my ( $self, $time ) = @_;
+
+ my @localtime = $self->localtime($time);
+
+ #return strftime( "%D", @localtime );
+ return strftime( "%d.%m.%Y", @localtime );
+}
+
+sub full_str {
+ my ( $self, $time ) = @_;
+
+ my @localtime = $self->localtime($time);
+
+ #return strftime( "%c", @localtime );
+ return strftime( "%d.%m.%Y %H:%M:%S", @localtime );
+}
+
+sub print {
+ my ( $self, $time ) = @_;
+
+ $self->time($time);
+
+ say $self->full_str();
+
+ return undef;
+}
+
+1;
diff --git a/lib/PINGDOMFETCH/Display.pm b/lib/PINGDOMFETCH/Display.pm
new file mode 100644
index 0000000..3838a82
--- /dev/null
+++ b/lib/PINGDOMFETCH/Display.pm
@@ -0,0 +1,157 @@
+package PINGDOMFETCH::Display;
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+
+use PINGDOMFETCH::Config;
+use PINGDOMFETCH::Utils;
+
+our $INDENT = 0;
+our $VERBOSE = 0;
+
+use overload
+ '""' => sub { shift->indents(); },
+ '++' => sub { shift->inc(); },
+ '--' => sub { shift->dec(); };
+
+sub init {
+ my ($self) = @_;
+
+ $VERBOSE = $self->{'arg.verbose'} == 1;
+
+ return undef;
+}
+
+sub inc {
+ my ($self) = @_;
+
+ return ++$INDENT;
+}
+
+sub dec {
+ my ($self) = @_;
+
+ return --$INDENT;
+}
+
+sub indents {
+ my ($self) = @_;
+
+ return ' ' x $INDENT;
+}
+
+sub display {
+ my ( $self, $msging ) = @_;
+
+ print $msging;
+
+ return undef;
+}
+
+sub is_verbose {
+ my ($self) = @_;
+
+ return $VERBOSE == 1;
+}
+
+sub info_no_nl {
+ my ( $self, $msg ) = @_;
+
+ $self->display("$msg");
+
+ return undef;
+}
+
+sub info {
+ my ( $self, $msg, $notify ) = @_;
+
+ my $str = " $self $msg\n";
+
+ $self->display($str);
+ $notify->message_push($str) if defined $notify;
+
+ return undef;
+}
+
+sub nl {
+ my ( $self, $notify ) = @_;
+
+ $self->display("\n");
+ $notify->message_push("\n") if defined $notify;
+
+ return undef;
+}
+
+sub error {
+ my ( $self, $msg ) = @_;
+
+ $self->display("! ERROR: $self $msg\n");
+
+ exit 666;
+
+ return undef;
+}
+
+sub warning {
+ my ( $self, $msg, $notify ) = @_;
+
+ my $str = "! $self $msg\n";
+
+ $self->display($str);
+
+ if ( defined $notify ) {
+ $notify->message_push($str);
+ $notify->{warnings}++;
+ }
+
+ return undef;
+}
+
+sub critical {
+ my ( $self, $msg, $notify ) = @_;
+
+ my $str = "!! $self $msg\n";
+
+ $self->display($str);
+
+ if ( defined $notify ) {
+ $notify->message_push($str);
+ $notify->{criticals}++;
+ }
+
+ return undef;
+}
+
+sub dump {
+ my ( $self, $msg ) = @_;
+
+ $self->display( Dumper $msg );
+
+ return undef;
+}
+
+sub diedump {
+ my ( $self, $msg ) = @_;
+
+ die Dumper $msg;
+
+ return undef;
+}
+
+sub verbose {
+ my ( $self, $msg, $notify ) = @_;
+
+ if ( $self->is_verbose() ) {
+ my $str = " $self $msg\n";
+
+ $self->display($str);
+ $notify->message_push($str) if defined $notify;
+ }
+
+ return undef;
+}
+
+1;
+
diff --git a/lib/PINGDOMFETCH/Notify.pm b/lib/PINGDOMFETCH/Notify.pm
new file mode 100644
index 0000000..07301fd
--- /dev/null
+++ b/lib/PINGDOMFETCH/Notify.pm
@@ -0,0 +1,163 @@
+package PINGDOMFETCH::Notify;
+
+use strict;
+use warnings;
+
+use PINGDOMFETCH::Config;
+use PINGDOMFETCH::DateHelper;
+use PINGDOMFETCH::Display;
+use PINGDOMFETCH::Utils;
+
+use MIME::Lite;
+
+our @ISA = ('PINGDOMFETCH::Display');
+
+sub new {
+ my ( $class, %vals ) = @_;
+
+ my $self = bless \%vals, $class;
+
+ my $config = $self->{config};
+
+ $self->{message} = [];
+ $self->{warnings} = 0;
+ $self->{criticals} = 0;
+
+ return $self;
+}
+
+sub message_push {
+ my ( $self, $message ) = @_;
+
+ push @{ $self->{message} }, $message;
+
+ return undef;
+}
+
+sub message_unshift {
+ my ( $self, $message ) = @_;
+
+ my $config = $self->{config};
+ unshift @{ $self->{message} }, $message;
+
+ return undef;
+}
+
+sub has_messages {
+ my ($self) = @_;
+
+ return @{ $self->{message} } > 0 ? 1 : 0;
+}
+
+sub has_warnings {
+ my ($self) = @_;
+
+ return $self->{warnings} > 0 ? 1 : 0;
+}
+
+sub has_criticals {
+ my ($self) = @_;
+
+ return $self->{criticals} > 0 ? 1 : 0;
+}
+
+sub info_notification_send {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+ $self->notification_send_to( $config->array('notify.info.email.to') );
+
+ return undef;
+}
+
+sub notification_send {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+ $self->notification_send_to( $config->array('notify.email.to') );
+
+ return undef;
+}
+
+sub notification_send_to {
+ my ( $self, @email_to ) = @_;
+
+ return if !$self->has_messages();
+
+ my $config = $self->{config};
+ my $from = $config->get('notify.email.sender');
+ my $warning_less = $config->get('warning.if.avail.is.less');
+ my $critical_less = $config->get('critical.if.avail.is.less');
+
+ my ( $dh_from, $dh_to ) = ( $config->{'dh_from'}, $config->{'dh_to'} );
+ my $message = join '', @{ $self->{message} };
+
+ my $subject = do {
+ if ( $self->has_criticals() ) {
+ '!! ';
+ }
+ elsif ( $self->has_warnings() ) {
+ '! ';
+ }
+ else {
+ ' ';
+ }
+ };
+
+ $subject .= 'Availability stats for ';
+
+ if ( $dh_from->is_begin_of_a_day()
+ and $dh_to->is_begin_of_a_day()
+ and 1 == $dh_from->days_until($dh_to) )
+ {
+ $subject .= $dh_from->day_str();
+ }
+ else {
+ $subject .= "'$dh_from' - '$dh_to'";
+ }
+
+ $message .= "Legend:\n";
+ $message .=
+"'!' means: TLS or Service Availability is less than $warning_less% (Exception: Threshold is non-standard)\n";
+ $message .= "'!!' means: TLS Availability is less than $critical_less%\n\n";
+ $message .=
+"Response times are not reasonable (collected from all over the world)!\n";
+
+ $message .= "\n" . get_version_full();
+
+ unless ( $config->bool('arg.notify-dummy') ) {
+ $self->send_mail( $from, $_, $subject, $message ) for @email_to;
+
+ }
+ else {
+ $self->info("Dummy-Email to stdout");
+
+ say $subject;
+ say "";
+ say $message;
+ }
+
+ $self->{messages} = [];
+
+ return undef;
+}
+
+sub send_mail {
+ my ( $self, $from, $to, $subject, $message ) = @_;
+
+ my $email = MIME::Lite->new(
+ From => $from,
+ To => $to,
+ Subject => $subject,
+ Type => 'TEXT',
+ Data => $message,
+ );
+
+ $self->info("Sending email '$subject' to '$to'");
+ $email->send();
+
+ return undef;
+}
+
+1;
+
diff --git a/lib/PINGDOMFETCH/Pingdom.pm b/lib/PINGDOMFETCH/Pingdom.pm
new file mode 100644
index 0000000..00f78ff
--- /dev/null
+++ b/lib/PINGDOMFETCH/Pingdom.pm
@@ -0,0 +1,191 @@
+package PINGDOMFETCH::Pingdom;
+
+use strict;
+use warnings;
+
+use JSON;
+use Data::Dumper;
+use IO::CaptureOutput qw(capture_exec);
+
+use PINGDOMFETCH::Display;
+use PINGDOMFETCH::Config;
+use PINGDOMFETCH::Result;
+use PINGDOMFETCH::Service;
+use PINGDOMFETCH::DateHelper;
+use PINGDOMFETCH::Utils;
+
+our @ISA = ('PINGDOMFETCH::Display');
+
+sub new {
+ my ( $class, $config ) = @_;
+
+ my $app_key = $config->get('pingdom.api.app.key');
+ my $host = $config->get('pingdom.api.host');
+ my $port = $config->get('pingdom.api.port');
+ my $protocol = $config->get('pingdom.api.protocol');
+
+ my $json = JSON->new()->allow_nonref();
+
+ #$ua->credentials( "$host:$port", $realm, $username, $password );
+
+ my $headers = {
+ 'App-key' => $app_key,
+ 'User-Agent' => 'pingdomfetch',
+ };
+
+ my $url_base = "$protocol://$host:$port";
+
+ my $self = bless {
+ config => $config,
+ json => $json,
+ url_base => $url_base,
+ headers => $headers,
+ }, $class;
+
+ return $self;
+}
+
+sub safe_get {
+ my ( $self, $j, @keys ) = @_;
+
+ my $pos = $j;
+
+ for (@keys) {
+ if ( exists $pos->{$_} ) {
+ $pos = $pos->{$_};
+
+ }
+ else {
+ local $" = '.';
+ $self->error(
+ "Could not get key '@keys' from JSON result: " . Dumper($j) );
+ }
+ }
+
+ return $pos;
+}
+
+sub fetch {
+ my ( $self, $url ) = @_;
+
+ my $config = $self->{config};
+ my $json = $self->{json};
+ my $headers = $self->{headers};
+
+ my $curl = $config->get('curl.path');
+ my $retry = $config->get('pingdom.api.failed.retry.after');
+ my $giveup = $config->get('pingdom.api.failed.giveup.after');
+
+ my $password = $config->get('pingdom.auth.password');
+ my $username = $config->get('pingdom.auth.username');
+
+ my $proxy = '';
+ $proxy = ' -p -x ' . $config->get('pingdom.proxy.address')
+ if $config->bool('pingdom.proxy.use');
+
+ my $cmd = "$curl '$url'$proxy --user '$username:$password'";
+ $cmd .= " --header '$_: $headers->{$_}'" for keys %$headers;
+
+ my ( $stdout, $stderr, $success, $exit_code );
+
+ for ( my $i = 0 ; $i < $giveup ; ++$i ) {
+ $self->verbose("Using URL $url");
+ $self->verbose("$cmd");
+ ( $stdout, $stderr, $success, $exit_code ) = capture_exec($cmd);
+
+ if ( $exit_code == 0 ) {
+ last;
+
+ }
+ else {
+ $self->warning( "Pingdom: stdout=" . $stdout );
+ $self->warning( "Pingdom: stderr=" . $stderr );
+ $self->warning( "Pingdom: success=" . $success );
+ $self->warning( "Pingdom: exit_code=" . $exit_code );
+ $self->warning("Retrying $url after $retry seconds");
+ sleep $retry;
+ }
+ }
+
+ return $json->decode($stdout);
+}
+
+sub fetch_avail_json {
+ my ( $self, $service, $from, $to ) = @_;
+
+ my $config = $self->{config};
+ my $checkid = $service->{checkid};
+ my $url_base = $self->{url_base};
+ my $action = $config->get('pingdom.api.average.action');
+ my $url = "$url_base/$action/$checkid?includeuptime=true&from=$from&to=$to";
+
+ $self->verbose(
+"Fetching availability for service $service->{name} (checkid $checkid) from Pingdom"
+ );
+
+ return $self->fetch($url);
+}
+
+sub fetch_avail_result {
+ my ( $self, $service ) = @_;
+
+ my $config = $self->{config};
+ my $dh_from = $config->{dh_from};
+ my $dh_to = $config->{dh_to};
+
+ if ( $dh_from->is_in_future() ) {
+ $self->verbose("'from' is in future");
+ $dh_from = PINGDOMFETCH::DateHelper->new( $self->{config} );
+ }
+
+ if ( $dh_to->is_in_future() ) {
+ $self->verbose("'to' is in future");
+ $dh_to = PINGDOMFETCH::DateHelper->new( $self->{config} );
+ }
+
+ my $j =
+ $self->fetch_avail_json( $service, $dh_from->time(), $dh_to->time() );
+
+ return PINGDOMFETCH::Result->new(
+ config => $config,
+ service => $service,
+ totalup => $self->safe_get( $j, qw(summary status totalup) ),
+ totalup => $self->safe_get( $j, qw(summary status totalup) ),
+ totaldown => $self->safe_get( $j, qw(summary status totaldown) ),
+ totalunknown => $self->safe_get( $j, qw(summary status totalunknown) ),
+ avgresponse =>
+ $self->safe_get( $j, qw(summary responsetime avgresponse) ),
+ );
+}
+
+sub fetch_all_checks_json {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ my $url_base = $self->{url_base};
+ my $action = $config->get('pingdom.api.all.checks.action');
+
+ my $url = "$url_base/$action";
+
+ $self->verbose("Fetching all checks from Pingdom");
+
+ return $self->fetch($url);
+}
+
+sub fetch_all_subscriptions_json {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ my $url_base = $self->{url_base};
+ my $action = $config->get('pingdom.api.all.report.subscriptions');
+
+ my $url = "$url_base/$action";
+
+ $self->verbose("Fetching all report subscriptions from Pingdom");
+
+ return $self->fetch($url);
+}
+
+1;
diff --git a/lib/PINGDOMFETCH/Pingdomfetch.pm b/lib/PINGDOMFETCH/Pingdomfetch.pm
new file mode 100644
index 0000000..3aa0046
--- /dev/null
+++ b/lib/PINGDOMFETCH/Pingdomfetch.pm
@@ -0,0 +1,245 @@
+package PINGDOMFETCH::Pingdomfetch;
+
+use strict;
+use warnings;
+
+use PINGDOMFETCH::Config;
+use PINGDOMFETCH::DateHelper;
+use PINGDOMFETCH::Display;
+use PINGDOMFETCH::Notify;
+use PINGDOMFETCH::Pingdom;
+use PINGDOMFETCH::Utils;
+
+our @ISA = ('PINGDOMFETCH::Display');
+
+sub new {
+ my ( $class, $opts ) = @_;
+
+ my $config = PINGDOMFETCH::Config->new($opts);
+ my $pingdom = PINGDOMFETCH::Pingdom->new($config);
+
+ my $self = bless {
+ config => $config,
+ pingdom => $pingdom,
+ dots_counter => 0,
+ }, $class;
+
+ $self->init_from_to_interval();
+
+ return $self;
+}
+
+sub init_from_to_interval {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ # Yeah, Hash Slices are hellworks!
+ my ( $from, $to ) = @{$config}{qw(arg.from arg.to)};
+
+ my $dh_from = $config->{dh_from} =
+ PINGDOMFETCH::DateHelper->new( $config, $from );
+ my $dh_to = $config->{dh_to} =
+ PINGDOMFETCH::DateHelper->new( $config, $to );
+
+ $dh_from->begin_of_day() if $from eq '';
+
+ # Handle the --flatten switcht
+ my $flatten = $config->{'arg.flatten'};
+ my ( $flatten_from, $flatten_to ) = split ':', $flatten;
+
+ if ( defined $flatten_from ) {
+ $dh_from->flatten($flatten_from)
+ if $flatten_from ne '';
+
+ $dh_to->flatten($flatten_to)
+ if defined $flatten_to and $flatten_to ne '';
+ }
+
+ $self->error(
+"Interval '$dh_from' - '$dh_to' is negative or zero. 'from' must be < 'to'."
+ ) if $dh_from->time() >= $dh_to->time();
+
+ $config->{interval_is_in_future} = $dh_to->is_in_future();
+
+ $self->{dh_from} = $dh_from;
+ $self->{dh_to} = $dh_to;
+
+ return undef;
+}
+
+sub get_checkid_avail {
+ my ( $self, $checkid ) = @_;
+
+ my $config = $self->{config};
+ my $services = $config->{services};
+
+ while ( my ( $k, $v ) = each %$services ) {
+ if ( $v->{checkid} eq $checkid ) {
+ $self->verbose("Checkid $checkid belongs to service $k");
+ $self->get_all_services_avail( { $k => $v } );
+
+ return ($v);
+ }
+ }
+
+ $self->error("No such service with checkid '$checkid'");
+
+ return ();
+}
+
+sub get_service_avail {
+ my ( $self, $servicename ) = @_;
+
+ my $config = $self->{config};
+ my $services = $config->{services};
+
+ if ( exists $services->{$servicename} ) {
+ my $service = $services->{$servicename};
+ $self->get_all_services_avail( { $servicename => $service } );
+
+ return $service;
+ }
+
+ $self->error("No such service '$servicename'");
+
+ return ();
+}
+
+sub get_tls_avail {
+ my ( $self, $tlsname ) = @_;
+
+ my $config = $self->{config};
+
+ my @results;
+
+ if ( ref $config->{tls}{$tlsname} ) {
+ my $tls = $config->{tls}{$tlsname};
+ my $services = $tls->{services};
+
+ $self->get_all_services_avail($services);
+ $tls->acc();
+
+ return ($tls);
+
+ }
+ else {
+ $self->error("No such TLS '$tlsname'");
+ }
+
+ return ();
+}
+
+sub get_all_services_avail {
+ my ( $self, $services ) = @_;
+
+ my $pingdom = $self->{pingdom};
+ my $config = $self->{config};
+
+ my @return;
+
+ while ( my ( $k, $v ) = each %$services ) {
+ unless ( $config->is_verbose() ) {
+ $self->{dots_counter}++;
+
+ if ( $self->{dots_counter} == 3 ) {
+ print '...';
+
+ }
+ elsif ( $self->{dots_counter} > 3 ) {
+ print '.';
+ }
+ }
+ $v->{result} = $pingdom->fetch_avail_result($v);
+ push @return, $v;
+ }
+
+ return @return;
+}
+
+sub run {
+ my ($self) = @_;
+ my $retval = 0;
+
+ my $config = $self->{config};
+ my $pingdom = $self->{pingdom};
+
+ $config->read_services($pingdom);
+ $config->read_tls();
+
+ return $config->print_services() if $config->{'arg.list-services'};
+
+ return $config->print_tls() if $config->{'arg.list-tls'};
+
+ $self->info(
+ "Fetching stats of interval '$self->{dh_from}' - '$self->{dh_to}'");
+
+ my @data;
+
+ push @data, $self->get_checkid_avail( $config->{'arg.checkid'} )
+ if $config->{'arg.checkid'} ne '';
+
+ push @data, $self->get_service_avail( $config->{'arg.service'} )
+ if $config->{'arg.service'} ne '';
+
+ if ( $config->{'arg.tls'} ne '' ) {
+ if ( $config->{'arg.tls'} =~ /,/ ) {
+ push @data, $self->get_tls_avail($_)
+ for split ',', $config->{'arg.tls'};
+ }
+ else {
+ push @data, $self->get_tls_avail( $config->{'arg.tls'} );
+ }
+ }
+
+ push @data, $self->get_all_services_avail( $config->{services} )
+ if $config->{'arg.all-services'};
+
+ if ( $config->{'arg.all-tls'} ) {
+ push @data, $self->get_tls_avail($_) for sort keys %{ $config->{tls} };
+ }
+
+ if (@data) {
+ my @sorted_data =
+ sort { $b->{result}{avail_perc} <=> $a->{result}{avail_perc} } @data;
+ @sorted_data = reverse @sorted_data
+ if $config->bool('arg.sort-reverse');
+
+ if ( $self->is_verbose() ) {
+ $self->error("--notify* can not be used together with --verbose")
+ if $config->bool('arg.notify-info')
+ or $config->bool('arg.notify');
+
+ for (@sorted_data) {
+ $_->print_full();
+ $self->nl();
+ }
+
+ }
+ else {
+ print "\n" if $self->{dots_counter} > 2;
+
+ my $notify = $config->{notify};
+
+ for (@sorted_data) {
+ $_->print();
+ $self->nl($notify);
+ }
+
+ $notify->info_notification_send()
+ if $config->bool('arg.notify-info');
+
+ $notify->notification_send()
+ if $config->bool('arg.notify')
+ and ( $notify->has_warnings(), or $notify->has_criticals() );
+ }
+ }
+ else {
+ $self->warning(
+ "No results found. Use --all-tls for all TLS or --help for help!");
+ }
+
+ return 0;
+}
+
+1;
diff --git a/lib/PINGDOMFETCH/Result.pm b/lib/PINGDOMFETCH/Result.pm
new file mode 100644
index 0000000..582c354
--- /dev/null
+++ b/lib/PINGDOMFETCH/Result.pm
@@ -0,0 +1,120 @@
+package PINGDOMFETCH::Result;
+
+use strict;
+use warnings;
+
+use PINGDOMFETCH::Config;
+use PINGDOMFETCH::DateHelper;
+use PINGDOMFETCH::Display;
+use PINGDOMFETCH::Utils;
+
+our @ISA = ('PINGDOMFETCH::Display');
+
+sub new {
+ my ( $class, %vals ) = @_;
+
+ my $self = bless \%vals, $class;
+
+ $self->compute();
+
+ return $self;
+}
+
+sub acc {
+ my ( $self, $service, $acc ) = @_;
+
+ $acc->( $service, $self );
+
+ return undef;
+}
+
+sub compute {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ my ( $up, $down, $total ) = $self->compute_up_down();
+ $self->{avail_perc} = $self->compute_avail_perc( $up, $down, $total );
+
+ if ( $config->bool('interval_is_in_future') ) {
+ my $remaining = do {
+
+ # It's a Service result and not a TLS
+ if ( exists $self->{service} ) {
+
+ # Total seconds in the current interval
+ my $seconds =
+ $config->{dh_to}->time() - $config->{dh_from}->time();
+ $self->{remaining} = $seconds - $total;
+
+ }
+ else {
+
+ # It's a TLS result
+ $self->{remaining};
+ }
+ };
+
+ $self->{possible_avail_perc_best} =
+ $self->compute_avail_perc( $up + $remaining, $down );
+
+ $self->{possible_avail_perc_worst} =
+ $self->compute_avail_perc( $up, $down + $remaining );
+ }
+
+ return undef;
+}
+
+sub compute_up_down {
+ my ( $self, $totalup, $totalunknown, $totaldown ) = @_;
+
+ my $config = $self->{config};
+ my $unknown = $config->get('interpret.unknown.status.as.up');
+
+ $totalup = $self->{totalup} unless defined $totalup;
+ $totaldown = $self->{totaldown} unless defined $totaldown;
+ $totalunknown = $self->{totalunknown} unless defined $totalunknown;
+
+ my $total = $totalup + $totaldown + $totalunknown;
+
+ return $unknown =~ /true/i
+ ? ( $totalup + $totalunknown, $totaldown, $total )
+ : ( $totalup, $totaldown + $totalunknown, $total );
+}
+
+sub compute_avail_perc {
+ my ( $self, $up, $down ) = @_;
+
+ my $config = $self->{config};
+ my $zero = $config->get('interpret.zero.results.as.up');
+
+ my $total = $up + $down;
+
+ if ( $total > 0 ) {
+ return 100 * $up / $total;
+ }
+ else {
+ return $zero =~ /true/i ? 100 : 0;
+ }
+}
+
+sub print {
+ my ($self) = @_;
+
+ $self->print_full();
+
+ return undef;
+}
+
+sub print_full {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ $self->info("$_: $self->{$_}")
+ for sort grep { not ref $self->{$_} } keys %$self;
+
+ return undef;
+}
+
+1;
diff --git a/lib/PINGDOMFETCH/Service.pm b/lib/PINGDOMFETCH/Service.pm
new file mode 100644
index 0000000..98633f2
--- /dev/null
+++ b/lib/PINGDOMFETCH/Service.pm
@@ -0,0 +1,105 @@
+package PINGDOMFETCH::Service;
+
+use strict;
+use warnings;
+
+use PINGDOMFETCH::Config;
+use PINGDOMFETCH::Display;
+use PINGDOMFETCH::Result;
+use PINGDOMFETCH::Utils;
+
+our @ISA = ('PINGDOMFETCH::Display');
+
+sub new {
+ my ( $class, %vals ) = @_;
+
+ my $self = bless \%vals, $class;
+
+ return $self;
+}
+
+sub acc {
+ my ( $self, $acc ) = @_;
+
+ $self->{result}->acc( $self, $acc ) if exists $self->{result};
+
+ return undef;
+}
+
+sub print {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+ my $is_in_future = $config->bool('interval_is_in_future');
+ my $notify = $config->{notify};
+
+ my $avail_perc = do {
+ if ( exists $self->{result} ) {
+ }
+ else {
+ '';
+ }
+ };
+ my $str = do {
+ if ($is_in_future) {
+ sprintf(
+"Service: %03.3f%%; %s (Best: %03.3f%%; Worst: %03.3f%%; Avgresponse: %dms)",
+ $self->{result}{avail_perc},
+ $self->{name},
+ $self->{result}{possible_avail_perc_best},
+ $self->{result}{possible_avail_perc_worst},
+ $self->{result}{avgresponse}
+ );
+ }
+ else {
+ sprintf(
+ "Service: %03.3f%%; %s (Avgresponse: %dms)",
+ $self->{result}{avail_perc},
+ $self->{name}, $self->{result}{avgresponse}
+ );
+ }
+ };
+
+ my @opts;
+ my $opts = $self->{opts};
+ my $opts_str = $config->get_opts_str($opts);
+
+ my $warning_less =
+ exists $self->{opts}{warning}
+ ? $self->{opts}{warning}
+ : $config->get('warning.if.avail.is.less');
+
+ my $critical_less =
+ exists $self->{opts}{critical}
+ ? $self->{opts}{critical}
+ : $config->get('critical.if.avail.is.less');
+
+ if ( $self->{result}{avail_perc} < $critical_less ) {
+ $self->critical( $str . $opts_str, $notify );
+ }
+ elsif ( $self->{result}{avail_perc} < $warning_less ) {
+ $self->warning( $str . $opts_str, $notify );
+ }
+ else {
+ $self->info( $str . $opts_str, $notify );
+ }
+
+ return undef;
+}
+
+sub print_full {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ $self->info("Service: $self->{name}");
+
+ $self->inc();
+ $self->{result}->print_full() if exists $self->{result};
+ $self->dec();
+
+ return undef;
+}
+
+1;
+
diff --git a/lib/PINGDOMFETCH/TLS.pm b/lib/PINGDOMFETCH/TLS.pm
new file mode 100644
index 0000000..e5f1325
--- /dev/null
+++ b/lib/PINGDOMFETCH/TLS.pm
@@ -0,0 +1,166 @@
+package PINGDOMFETCH::TLS;
+
+use strict;
+use warnings;
+
+use PINGDOMFETCH::Config;
+use PINGDOMFETCH::Display;
+use PINGDOMFETCH::Result;
+use PINGDOMFETCH::Utils;
+
+our @ISA = ('PINGDOMFETCH::Display');
+
+sub new {
+ my ( $class, %vals ) = @_;
+
+ my $self = bless \%vals, $class;
+ $self->{is_critical} = 0;
+
+ return $self;
+}
+
+sub acc {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+ my $is_in_future = $config->bool('interval_is_in_future');
+
+ my $count = 0;
+ my $tls_result = PINGDOMFETCH::Result->new(
+ config => $config,
+ totaldown => 0,
+ totalup => 0,
+ totalunknown => 0,
+ avgresponse => 0,
+ remaining => 0,
+ );
+ $tls_result->{remaining} = 0 if $is_in_future;
+
+ my $acc = sub {
+ my ( $service, $result ) = @_;
+
+ $count++;
+ my $weight =
+ exists $service->{opts}{weight}
+ ? $service->{opts}{weight}
+ : 1;
+
+ $tls_result->{$_} += $result->{$_} * $weight
+ for qw(totaldown totalup totalunknown);
+
+ $tls_result->{$_} += $result->{$_} for qw(avgresponse);
+
+ $tls_result->{remaining} += $result->{remaining} * $weight
+ if $is_in_future;
+ };
+
+ if ( exists $self->{services} ) {
+ $self->{services}{$_}->acc($acc) for keys %{ $self->{services} };
+ }
+
+ if ( $count > 0 ) {
+ $tls_result->{avgresponse} /= $count;
+ $tls_result->compute();
+ $self->{result} = $tls_result;
+ }
+
+ $self->{is_critical} = 1
+ if $self->{result}{avail_perc} <
+ $config->get('critical.if.avail.is.less');
+
+ return undef;
+}
+
+sub print {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+ my $is_in_future = $config->bool('interval_is_in_future');
+ my $notify = $config->{notify};
+
+ my $str = do {
+ if ($is_in_future) {
+ sprintf(
+"TLS: %03.3f%%; %s (Best: %03.3f%%; Worst: %03.3f%%; Avgresponse: %dms)",
+ $self->{result}{avail_perc},
+ $self->{name},
+ $self->{result}{possible_avail_perc_best},
+ $self->{result}{possible_avail_perc_worst},
+ $self->{result}{avgresponse}
+ );
+ }
+ else {
+ sprintf(
+ "TLS: %03.3f%%; %s (Avgresponse: %dms)",
+ $self->{result}{avail_perc},
+ $self->{name}, $self->{result}{avgresponse}
+ );
+ }
+ };
+
+ if ( $self->{result}{avail_perc} <
+ $config->get('critical.if.avail.is.less') )
+ {
+ $self->critical( $str, $notify );
+
+ }
+ elsif (
+ $self->{result}{avail_perc} < $config->get('warning.if.avail.is.less') )
+ {
+ $self->warning( $str, $notify );
+
+ }
+ else {
+ $self->info( $str, $notify );
+ }
+
+ if ( exists $self->{services} ) {
+ $self->inc();
+
+ my @sorted_data =
+ sort { $b->{result}{avail_perc} <=> $a->{result}{avail_perc} }
+ values %{ $self->{services} };
+ @sorted_data = reverse @sorted_data
+ if $config->bool('arg.sort-reverse');
+
+ $_->print() for @sorted_data;
+ $self->dec();
+ }
+
+ return undef;
+}
+
+sub print_full {
+ my ($self) = @_;
+
+ my $config = $self->{config};
+
+ $self->info("TLS $self->{name}");
+ $self->inc();
+
+ if ( exists $self->{result} ) {
+ $self->{result}->print_full();
+ }
+
+ $self->info("$_: $self->{$_}")
+ for sort grep { not ref $self->{$_} and $_ ne 'name' } keys %$self;
+
+ if ( exists $self->{services} ) {
+ $self->inc();
+ while ( my ( $k, $v ) = each %{ $self->{services} } ) {
+ $v->print_full();
+ }
+ $self->dec();
+
+ }
+ else {
+ $self->warning("No services for this TLS");
+ }
+
+ $self->dec();
+
+ return undef;
+}
+
+1;
+
diff --git a/lib/PINGDOMFETCH/Utils.pm b/lib/PINGDOMFETCH/Utils.pm
new file mode 100644
index 0000000..f66792f
--- /dev/null
+++ b/lib/PINGDOMFETCH/Utils.pm
@@ -0,0 +1,72 @@
+package PINGDOMFETCH::Utils;
+
+use strict;
+use warnings;
+
+use Data::Dumper;
+use Exporter;
+
+use base 'Exporter';
+
+our @EXPORT = qw (
+ d
+ dumper
+ get_version
+ get_version_full
+ newline
+ notnull
+ null
+ remove_spaces
+ say
+ sum
+ trim
+);
+
+sub say (@) { print "$_\n" for @_; return undef }
+sub newline () { say ''; return undef }
+sub sum (@) { my $sum = 0; $sum += $_ for @_; return $sum }
+sub null ($) { defined $_[0] ? $_[0] : 0 }
+sub notnull ($) { $_[0] != 0 ? $_[0] : 1 }
+sub dumper (@) { die Dumper @_ }
+sub d (@) { dumper @_ }
+
+sub trim ($) {
+ my $trimit = shift;
+
+ $trimit =~ s/^[\s\t]+//;
+ $trimit =~ s/[\s\t]+$//;
+
+ return $trimit;
+}
+
+sub remove_spaces ($) {
+ my $str = shift;
+
+ $str =~ s/[\s\t]//g;
+
+ return $str;
+}
+
+sub get_version () {
+ my $versionfile = do {
+ if ( -f '.version' ) {
+ '.version';
+ }
+ else {
+ '/usr/share/pingdomfetch/version';
+ }
+ };
+
+ open my $fh, $versionfile or error("$!: $versionfile");
+ my $version = <$fh>;
+ close $fh;
+
+ chomp $version;
+ return $version;
+}
+
+sub get_version_full () {
+ return "This is Pingdomfetch Version " . get_version() . "\n";
+}
+
+1;