From 336c6c8a415603c772f97ccd63912d05209f3795 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Fri, 2 Jan 2015 13:27:02 +0100 Subject: initial --- lib/PINGDOMFETCH/Config.pm | 301 +++++++++++++++++++++++++++++++++++++++ lib/PINGDOMFETCH/DateHelper.pm | 179 +++++++++++++++++++++++ lib/PINGDOMFETCH/Display.pm | 157 ++++++++++++++++++++ lib/PINGDOMFETCH/Notify.pm | 163 +++++++++++++++++++++ lib/PINGDOMFETCH/Pingdom.pm | 191 +++++++++++++++++++++++++ lib/PINGDOMFETCH/Pingdomfetch.pm | 245 +++++++++++++++++++++++++++++++ lib/PINGDOMFETCH/Result.pm | 120 ++++++++++++++++ lib/PINGDOMFETCH/Service.pm | 105 ++++++++++++++ lib/PINGDOMFETCH/TLS.pm | 166 +++++++++++++++++++++ lib/PINGDOMFETCH/Utils.pm | 72 ++++++++++ 10 files changed, 1699 insertions(+) create mode 100644 lib/PINGDOMFETCH/Config.pm create mode 100644 lib/PINGDOMFETCH/DateHelper.pm create mode 100644 lib/PINGDOMFETCH/Display.pm create mode 100644 lib/PINGDOMFETCH/Notify.pm create mode 100644 lib/PINGDOMFETCH/Pingdom.pm create mode 100644 lib/PINGDOMFETCH/Pingdomfetch.pm create mode 100644 lib/PINGDOMFETCH/Result.pm create mode 100644 lib/PINGDOMFETCH/Service.pm create mode 100644 lib/PINGDOMFETCH/TLS.pm create mode 100644 lib/PINGDOMFETCH/Utils.pm (limited to 'lib') 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; -- cgit v1.2.3