From 35cb117af17ef4ac401bb37506d42fc5cf6cb997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Sun, 16 Dec 2018 08:45:16 +0100 Subject: [PATCH 01/31] Update test configuration --- .../alignak/commands/check_mysql_health | 3932 +++++++++++++++++ .../alignak/commands/check_nginx_status.pl | 474 ++ .../alignak/commands/utils.pm | 68 + .../alignak.d/alignak-module-ui-graphite2.ini | 2 +- .../etc/alignak.d/alignak-module-webui.ini | 14 +- .../alignak/etc/alignak.d/daemons.ini | 1 + .../alignak/etc/alignak.d/extra-daemons.ini | 5 +- .../alignak/etc/alignak.d/modules.ini | 6 +- .../alignak/etc/alignak.ini | 2 + .../arbiter/objects/hosts/elasticsearchs.cfg | 6 +- .../etc/arbiter/objects/hosts/glpi.cfg | 9 + .../etc/arbiter/objects/hosts/localhost.cfg | 12 + .../etc/arbiter/objects/hosts/mariadbs.cfg | 51 + .../etc/arbiter/objects/hosts/nginxs.cfg | 33 + .../etc/arbiter/objects/services/services.cfg | 25 +- .../etc/arbiter/packs/glpi/commands.cfg | 15 + .../alignak/etc/arbiter/packs/glpi/groups.cfg | 10 + .../etc/arbiter/packs/glpi/templates.cfg | 60 + .../etc/arbiter/packs/mysql/commands.cfg | 113 + .../etc/arbiter/packs/mysql/groups.cfg | 10 + .../etc/arbiter/packs/mysql/services.cfg | 218 + .../etc/arbiter/packs/mysql/templates.cfg | 80 + .../etc/arbiter/packs/nginx/commands.cfg | 12 + .../etc/arbiter/packs/nginx/groups.cfg | 10 + .../etc/arbiter/packs/nginx/templates.cfg | 60 + .../arbiter/packs/nrpe/services-freebsd.cfg | 2 +- .../alignak/etc/arbiter/resource.d/mysql.cfg | 4 + .../etc/arbiter/templates/generic-host.cfg | 63 +- .../etc/arbiter/templates/generic-service.cfg | 2 + .../objects/contactgroups/notified.cfg | 7 + 30 files changed, 5267 insertions(+), 39 deletions(-) create mode 100755 test/test-configurations/alignak/commands/check_mysql_health create mode 100755 test/test-configurations/alignak/commands/check_nginx_status.pl create mode 100644 test/test-configurations/alignak/commands/utils.pm create mode 100644 test/test-configurations/alignak/etc/arbiter/objects/hosts/glpi.cfg create mode 100644 test/test-configurations/alignak/etc/arbiter/objects/hosts/mariadbs.cfg create mode 100644 test/test-configurations/alignak/etc/arbiter/objects/hosts/nginxs.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/glpi/commands.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/glpi/groups.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/glpi/templates.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/mysql/commands.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/mysql/groups.cfg create mode 100644 test/test-configurations/alignak/etc/arbiter/packs/mysql/services.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/mysql/templates.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/nginx/commands.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/nginx/groups.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/packs/nginx/templates.cfg create mode 100755 test/test-configurations/alignak/etc/arbiter/resource.d/mysql.cfg create mode 100644 test/test-configurations/shinken/etc/arbiter/objects/contactgroups/notified.cfg diff --git a/test/test-configurations/alignak/commands/check_mysql_health b/test/test-configurations/alignak/commands/check_mysql_health new file mode 100755 index 00000000..33f5ed91 --- /dev/null +++ b/test/test-configurations/alignak/commands/check_mysql_health @@ -0,0 +1,3932 @@ +#! /usr/bin/perl -w +# nagios: -epn + +my %ERRORS=( OK => 0, WARNING => 1, CRITICAL => 2, UNKNOWN => 3 ); +my %ERRORCODES=( 0 => 'OK', 1 => 'WARNING', 2 => 'CRITICAL', 3 => 'UNKNOWN' ); +package DBD::MySQL::Server::Instance::Innodb; + +use strict; + +our @ISA = qw(DBD::MySQL::Server::Instance); + + +sub new { + my $class = shift; + my %params = @_; + my $self = { + handle => $params{handle}, + internals => undef, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + }; + bless $self, $class; + $self->init(%params); + return $self; +} + +sub init { + my $self = shift; + my %params = @_; + $self->init_nagios(); + if ($params{mode} =~ /server::instance::innodb/) { + $self->{internals} = + DBD::MySQL::Server::Instance::Innodb::Internals->new(%params); + } +} + +sub nagios { + my $self = shift; + my %params = @_; + if ($params{mode} =~ /server::instance::innodb/) { + $self->{internals}->nagios(%params); + $self->merge_nagios($self->{internals}); + } +} + + +package DBD::MySQL::Server::Instance::Innodb::Internals; + +use strict; + +our @ISA = qw(DBD::MySQL::Server::Instance::Innodb); + +our $internals; # singleton, nur ein einziges mal instantiierbar + +sub new { + my $class = shift; + my %params = @_; + unless ($internals) { + $internals = { + handle => $params{handle}, + bufferpool_hitrate => undef, + wait_free => undef, + log_waits => undef, + have_innodb => undef, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + }; + bless($internals, $class); + $internals->init(%params); + } + return($internals); +} + +sub init { + my $self = shift; + my %params = @_; + my $dummy; + $self->debug("enter init"); + $self->init_nagios(); + if (DBD::MySQL::Server::return_first_server()->version_is_minimum("5.1")) { + ($dummy, $self->{have_innodb}) = $self->{handle}->fetchrow_array(q{ + SELECT ENGINE, SUPPORT FROM INFORMATION_SCHEMA.ENGINES WHERE ENGINE='InnoDB' + }); + } else { + ($dummy, $self->{have_innodb}) = $self->{handle}->fetchrow_array(q{ + SHOW VARIABLES LIKE 'have_innodb' + }); + } + if ($self->{have_innodb} eq "NO") { + $self->add_nagios_critical("the innodb engine has a problem (have_innodb=no)"); + } elsif ($self->{have_innodb} eq "DISABLED") { + # add_nagios_ok later + } elsif ($params{mode} =~ /server::instance::innodb::bufferpool::hitrate/) { + ($dummy, $self->{bufferpool_reads}) + = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Innodb_buffer_pool_reads' + }); + ($dummy, $self->{bufferpool_read_requests}) + = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Innodb_buffer_pool_read_requests' + }); + if (! defined $self->{bufferpool_reads}) { + $self->add_nagios_critical("no innodb buffer pool info available"); + } else { + $self->valdiff(\%params, qw(bufferpool_reads + bufferpool_read_requests)); + $self->{bufferpool_hitrate_now} = + $self->{delta_bufferpool_read_requests} > 0 ? + 100 - (100 * $self->{delta_bufferpool_reads} / + $self->{delta_bufferpool_read_requests}) : 100; + $self->{bufferpool_hitrate} = + $self->{bufferpool_read_requests} > 0 ? + 100 - (100 * $self->{bufferpool_reads} / + $self->{bufferpool_read_requests}) : 100; + } + } elsif ($params{mode} =~ /server::instance::innodb::bufferpool::waitfree/) { + ($dummy, $self->{bufferpool_wait_free}) + = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Innodb_buffer_pool_wait_free' + }); + if (! defined $self->{bufferpool_wait_free}) { + $self->add_nagios_critical("no innodb buffer pool info available"); + } else { + $self->valdiff(\%params, qw(bufferpool_wait_free)); + $self->{bufferpool_wait_free_rate} = + $self->{delta_bufferpool_wait_free} / $self->{delta_timestamp}; + } + } elsif ($params{mode} =~ /server::instance::innodb::logwaits/) { + ($dummy, $self->{log_waits}) + = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Innodb_log_waits' + }); + if (! defined $self->{log_waits}) { + $self->add_nagios_critical("no innodb log info available"); + } else { + $self->valdiff(\%params, qw(log_waits)); + $self->{log_waits_rate} = + $self->{delta_log_waits} / $self->{delta_timestamp}; + } + } elsif ($params{mode} =~ /server::instance::innodb::needoptimize/) { +#fragmentation=$(($datafree * 100 / $datalength)) + +#http://www.electrictoolbox.com/optimize-tables-mysql-php/ + my @result = $self->{handle}->fetchall_array(q{ +SHOW TABLE STATUS WHERE Data_free / Data_length > 0.1 AND Data_free > 102400 +}); +printf "%s\n", Data::Dumper::Dumper(\@result); + + } +} + +sub nagios { + my $self = shift; + my %params = @_; + my $now = $params{lookback} ? '_now' : ''; + if ($self->{have_innodb} eq "DISABLED") { + $self->add_nagios_ok("the innodb engine has been disabled"); + } elsif (! $self->{nagios_level}) { + if ($params{mode} =~ /server::instance::innodb::bufferpool::hitrate/) { + my $refkey = 'bufferpool_hitrate'.($params{lookback} ? '_now' : ''); + $self->add_nagios( + $self->check_thresholds($self->{$refkey}, "99:", "95:"), + sprintf "innodb buffer pool hitrate at %.2f%%", $self->{$refkey}); + $self->add_perfdata(sprintf "bufferpool_hitrate=%.2f%%;%s;%s;0;100", + $self->{bufferpool_hitrate}, + $self->{warningrange}, $self->{criticalrange}); + $self->add_perfdata(sprintf "bufferpool_hitrate_now=%.2f%%", + $self->{bufferpool_hitrate_now}); + } elsif ($params{mode} =~ /server::instance::innodb::bufferpool::waitfree/) { + $self->add_nagios( + $self->check_thresholds($self->{bufferpool_wait_free_rate}, "1", "10"), + sprintf "%ld innodb buffer pool waits in %ld seconds (%.4f/sec)", + $self->{delta_bufferpool_wait_free}, $self->{delta_timestamp}, + $self->{bufferpool_wait_free_rate}); + $self->add_perfdata(sprintf "bufferpool_free_waits_rate=%.4f;%s;%s;0;100", + $self->{bufferpool_wait_free_rate}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::innodb::logwaits/) { + $self->add_nagios( + $self->check_thresholds($self->{log_waits_rate}, "1", "10"), + sprintf "%ld innodb log waits in %ld seconds (%.4f/sec)", + $self->{delta_log_waits}, $self->{delta_timestamp}, + $self->{log_waits_rate}); + $self->add_perfdata(sprintf "innodb_log_waits_rate=%.4f;%s;%s;0;100", + $self->{log_waits_rate}, + $self->{warningrange}, $self->{criticalrange}); + } + } +} + + + + +package DBD::MySQL::Server::Instance::MyISAM; + +use strict; + +our @ISA = qw(DBD::MySQL::Server::Instance); + + +sub new { + my $class = shift; + my %params = @_; + my $self = { + handle => $params{handle}, + internals => undef, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + }; + bless $self, $class; + $self->init(%params); + return $self; +} + +sub init { + my $self = shift; + my %params = @_; + $self->init_nagios(); + if ($params{mode} =~ /server::instance::myisam/) { + $self->{internals} = + DBD::MySQL::Server::Instance::MyISAM::Internals->new(%params); + } +} + +sub nagios { + my $self = shift; + my %params = @_; + if ($params{mode} =~ /server::instance::myisam/) { + $self->{internals}->nagios(%params); + $self->merge_nagios($self->{internals}); + } +} + + +package DBD::MySQL::Server::Instance::MyISAM::Internals; + +use strict; + +our @ISA = qw(DBD::MySQL::Server::Instance::MyISAM); + +our $internals; # singleton, nur ein einziges mal instantiierbar + +sub new { + my $class = shift; + my %params = @_; + unless ($internals) { + $internals = { + handle => $params{handle}, + keycache_hitrate => undef, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + }; + bless($internals, $class); + $internals->init(%params); + } + return($internals); +} + +sub init { + my $self = shift; + my %params = @_; + my $dummy; + $self->debug("enter init"); + $self->init_nagios(); + if ($params{mode} =~ /server::instance::myisam::keycache::hitrate/) { + ($dummy, $self->{key_reads}) + = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Key_reads' + }); + ($dummy, $self->{key_read_requests}) + = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Key_read_requests' + }); + if (! defined $self->{key_read_requests}) { + $self->add_nagios_critical("no myisam keycache info available"); + } else { + $self->valdiff(\%params, qw(key_reads key_read_requests)); + $self->{keycache_hitrate} = + $self->{key_read_requests} > 0 ? + 100 - (100 * $self->{key_reads} / + $self->{key_read_requests}) : 100; + $self->{keycache_hitrate_now} = + $self->{delta_key_read_requests} > 0 ? + 100 - (100 * $self->{delta_key_reads} / + $self->{delta_key_read_requests}) : 100; + } + } elsif ($params{mode} =~ /server::instance::myisam::sonstnochwas/) { + } +} + +sub nagios { + my $self = shift; + my %params = @_; + if (! $self->{nagios_level}) { + if ($params{mode} =~ /server::instance::myisam::keycache::hitrate/) { + my $refkey = 'keycache_hitrate'.($params{lookback} ? '_now' : ''); + $self->add_nagios( + $self->check_thresholds($self->{$refkey}, "99:", "95:"), + sprintf "myisam keycache hitrate at %.2f%%", $self->{$refkey}); + $self->add_perfdata(sprintf "keycache_hitrate=%.2f%%;%s;%s", + $self->{keycache_hitrate}, + $self->{warningrange}, $self->{criticalrange}); + $self->add_perfdata(sprintf "keycache_hitrate_now=%.2f%%;%s;%s", + $self->{keycache_hitrate_now}, + $self->{warningrange}, $self->{criticalrange}); + } + } +} + + +package DBD::MySQL::Server::Instance::Replication; + +use strict; + +our @ISA = qw(DBD::MySQL::Server::Instance); + + +sub new { + my $class = shift; + my %params = @_; + my $self = { + handle => $params{handle}, + internals => undef, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + }; + bless $self, $class; + $self->init(%params); + return $self; +} + +sub init { + my $self = shift; + my %params = @_; + $self->init_nagios(); + if ($params{mode} =~ /server::instance::replication/) { + $self->{internals} = + DBD::MySQL::Server::Instance::Replication::Internals->new(%params); + } +} + +sub nagios { + my $self = shift; + my %params = @_; + if ($params{mode} =~ /server::instance::replication/) { + $self->{internals}->nagios(%params); + $self->merge_nagios($self->{internals}); + } +} + + +package DBD::MySQL::Server::Instance::Replication::Internals; + +use strict; + +our @ISA = qw(DBD::MySQL::Server::Instance::Replication); + +our $internals; # singleton, nur ein einziges mal instantiierbar + +sub new { + my $class = shift; + my %params = @_; + unless ($internals) { + $internals = { + handle => $params{handle}, + seconds_behind_master => undef, + slave_io_running => undef, + slave_sql_running => undef, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + }; + bless($internals, $class); + $internals->init(%params); + } + return($internals); +} + +sub init { + my $self = shift; + my %params = @_; + $self->debug("enter init"); + $self->init_nagios(); + if ($params{mode} =~ /server::instance::replication::slavelag/) { + # "show slave status", "Seconds_Behind_Master" + my $slavehash = $self->{handle}->selectrow_hashref(q{ + SHOW SLAVE STATUS + }); + if ((! defined $slavehash->{Seconds_Behind_Master}) && + (lc $slavehash->{Slave_IO_Running} eq 'no')) { + $self->add_nagios_critical( + "unable to get slave lag, because io thread is not running"); + } elsif (! defined $slavehash->{Seconds_Behind_Master}) { + $self->add_nagios_critical(sprintf "unable to get replication info%s", + $self->{handle}->{errstr} ? $self->{handle}->{errstr} : ""); + } else { + $self->{seconds_behind_master} = $slavehash->{Seconds_Behind_Master}; + } + } elsif ($params{mode} =~ /server::instance::replication::slaveiorunning/) { + # "show slave status", "Slave_IO_Running" + my $slavehash = $self->{handle}->selectrow_hashref(q{ + SHOW SLAVE STATUS + }); + if (! defined $slavehash->{Slave_IO_Running}) { + $self->add_nagios_critical(sprintf "unable to get replication info%s", + $self->{handle}->{errstr} ? $self->{handle}->{errstr} : ""); + } else { + $self->{slave_io_running} = $slavehash->{Slave_IO_Running}; + } + } elsif ($params{mode} =~ /server::instance::replication::slavesqlrunning/) { + # "show slave status", "Slave_SQL_Running" + my $slavehash = $self->{handle}->selectrow_hashref(q{ + SHOW SLAVE STATUS + }); + if (! defined $slavehash->{Slave_SQL_Running}) { + $self->add_nagios_critical(sprintf "unable to get replication info%s", + $self->{handle}->{errstr} ? $self->{handle}->{errstr} : ""); + } else { + $self->{slave_sql_running} = $slavehash->{Slave_SQL_Running}; + } + } +} + +sub nagios { + my $self = shift; + my %params = @_; + if (! $self->{nagios_level}) { + if ($params{mode} =~ /server::instance::replication::slavelag/) { + $self->add_nagios( + $self->check_thresholds($self->{seconds_behind_master}, "10", "20"), + sprintf "Slave is %d seconds behind master", + $self->{seconds_behind_master}); + $self->add_perfdata(sprintf "slave_lag=%d;%s;%s", + $self->{seconds_behind_master}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::replication::slaveiorunning/) { + if (lc $self->{slave_io_running} eq "yes") { + $self->add_nagios_ok("Slave io is running"); + } else { + $self->add_nagios_critical("Slave io is not running"); + } + } elsif ($params{mode} =~ /server::instance::replication::slavesqlrunning/) { + if (lc $self->{slave_sql_running} eq "yes") { + $self->add_nagios_ok("Slave sql is running"); + } else { + $self->add_nagios_critical("Slave sql is not running"); + } + } + } +} + + + +package DBD::MySQL::Server::Instance; + +use strict; + +our @ISA = qw(DBD::MySQL::Server); + + +sub new { + my $class = shift; + my %params = @_; + my $self = { + handle => $params{handle}, + uptime => $params{uptime}, + replication_user => $params{replication_user}, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + threads_connected => undef, + threads_created => undef, + connections => undef, + threadcache_hitrate => undef, + querycache_hitrate => undef, + lowmem_prunes_per_sec => undef, + slow_queries_per_sec => undef, + longrunners => undef, + tablecache_hitrate => undef, + index_usage => undef, + engine_innodb => undef, + engine_myisam => undef, + replication => undef, + }; + bless $self, $class; + $self->init(%params); + return $self; +} + +sub init { + my $self = shift; + my %params = @_; + my $dummy; + $self->init_nagios(); + if ($params{mode} =~ /server::instance::connectedthreads/) { + ($dummy, $self->{threads_connected}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Threads_connected' + }); + } elsif ($params{mode} =~ /server::instance::createdthreads/) { + ($dummy, $self->{threads_created}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Threads_created' + }); + $self->valdiff(\%params, qw(threads_created)); + $self->{threads_created_per_sec} = $self->{delta_threads_created} / + $self->{delta_timestamp}; + } elsif ($params{mode} =~ /server::instance::runningthreads/) { + ($dummy, $self->{threads_running}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Threads_running' + }); + } elsif ($params{mode} =~ /server::instance::cachedthreads/) { + ($dummy, $self->{threads_cached}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Threads_cached' + }); + } elsif ($params{mode} =~ /server::instance::abortedconnects/) { + ($dummy, $self->{connects_aborted}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Aborted_connects' + }); + $self->valdiff(\%params, qw(connects_aborted)); + $self->{connects_aborted_per_sec} = $self->{delta_connects_aborted} / + $self->{delta_timestamp}; + } elsif ($params{mode} =~ /server::instance::abortedclients/) { + ($dummy, $self->{clients_aborted}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Aborted_clients' + }); + $self->valdiff(\%params, qw(clients_aborted)); + $self->{clients_aborted_per_sec} = $self->{delta_clients_aborted} / + $self->{delta_timestamp}; + } elsif ($params{mode} =~ /server::instance::threadcachehitrate/) { + ($dummy, $self->{threads_created}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Threads_created' + }); + ($dummy, $self->{connections}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Connections' + }); + $self->valdiff(\%params, qw(threads_created connections)); + if ($self->{delta_connections} > 0) { + $self->{threadcache_hitrate_now} = + 100 - ($self->{delta_threads_created} * 100.0 / + $self->{delta_connections}); + } else { + $self->{threadcache_hitrate_now} = 100; + } + $self->{threadcache_hitrate} = 100 - + ($self->{threads_created} * 100.0 / $self->{connections}); + $self->{connections_per_sec} = $self->{delta_connections} / + $self->{delta_timestamp}; + } elsif ($params{mode} =~ /server::instance::querycachehitrate/) { + ($dummy, $self->{qcache_inserts}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Qcache_inserts' + }); + ($dummy, $self->{qcache_not_cached}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Qcache_not_cached' + }); + ($dummy, $self->{com_select}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Com_select' + }); + ($dummy, $self->{qcache_hits}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Qcache_hits' + }); + # SHOW VARIABLES WHERE Variable_name = 'have_query_cache' for 5.x, but LIKE is compatible + ($dummy, $self->{have_query_cache}) = $self->{handle}->fetchrow_array(q{ + SHOW VARIABLES LIKE 'have_query_cache' + }); + # SHOW VARIABLES WHERE Variable_name = 'query_cache_size' + ($dummy, $self->{query_cache_size}) = $self->{handle}->fetchrow_array(q{ + SHOW VARIABLES LIKE 'query_cache_size' + }); + $self->valdiff(\%params, qw(com_select qcache_hits)); + $self->{querycache_hitrate_now} = + ($self->{delta_com_select} + $self->{delta_qcache_hits}) > 0 ? + 100 * $self->{delta_qcache_hits} / + ($self->{delta_com_select} + $self->{delta_qcache_hits}) : + 0; + $self->{querycache_hitrate} = + ($self->{qcache_not_cached} + $self->{qcache_inserts} + $self->{qcache_hits}) > 0 ? + 100 * $self->{qcache_hits} / + ($self->{qcache_not_cached} + $self->{qcache_inserts} + $self->{qcache_hits}) : + 0; + $self->{selects_per_sec} = + $self->{delta_com_select} / $self->{delta_timestamp}; + } elsif ($params{mode} =~ /server::instance::querycachelowmemprunes/) { + ($dummy, $self->{lowmem_prunes}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Qcache_lowmem_prunes' + }); + $self->valdiff(\%params, qw(lowmem_prunes)); + $self->{lowmem_prunes_per_sec} = $self->{delta_lowmem_prunes} / + $self->{delta_timestamp}; + } elsif ($params{mode} =~ /server::instance::slowqueries/) { + ($dummy, $self->{slow_queries}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Slow_queries' + }); + $self->valdiff(\%params, qw(slow_queries)); + $self->{slow_queries_per_sec} = $self->{delta_slow_queries} / + $self->{delta_timestamp}; + } elsif ($params{mode} =~ /server::instance::longprocs/) { + if (DBD::MySQL::Server::return_first_server()->version_is_minimum("5.1")) { + ($self->{longrunners}) = $self->{handle}->fetchrow_array(qq( + SELECT + COUNT(*) + FROM + information_schema.processlist + WHERE user <> ? + AND id <> CONNECTION_ID() + AND time > 60 + AND command <> 'Sleep' + ), $self->{replication_user}); + } else { + $self->{longrunners} = 0 if ! defined $self->{longrunners}; + foreach ($self->{handle}->fetchall_array(q{ + SHOW PROCESSLIST + })) { + my($id, $user, $host, $db, $command, $tme, $state, $info) = @{$_}; + if (($user ne $self->{replication_user}) && + ($tme > 60) && + ($command ne 'Sleep')) { + $self->{longrunners}++; + } + } + } + } elsif ($params{mode} =~ /server::instance::tablecachehitrate/) { + ($dummy, $self->{open_tables}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Open_tables' + }); + ($dummy, $self->{opened_tables}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Opened_tables' + }); + if (DBD::MySQL::Server::return_first_server()->version_is_minimum("5.1.3")) { + # SHOW VARIABLES WHERE Variable_name = 'table_open_cache' + ($dummy, $self->{table_cache}) = $self->{handle}->fetchrow_array(q{ + SHOW VARIABLES LIKE 'table_open_cache' + }); + } else { + # SHOW VARIABLES WHERE Variable_name = 'table_cache' + ($dummy, $self->{table_cache}) = $self->{handle}->fetchrow_array(q{ + SHOW VARIABLES LIKE 'table_cache' + }); + } + $self->{table_cache} ||= 0; + #$self->valdiff(\%params, qw(open_tables opened_tables table_cache)); + # _now ist hier sinnlos, da opened_tables waechst, aber open_tables wieder + # schrumpfen kann weil tabellen geschlossen werden. + if ($self->{opened_tables} != 0 && $self->{table_cache} != 0) { + $self->{tablecache_hitrate} = + 100 * $self->{open_tables} / $self->{opened_tables}; + $self->{tablecache_fillrate} = + 100 * $self->{open_tables} / $self->{table_cache}; + } elsif ($self->{opened_tables} == 0 && $self->{table_cache} != 0) { + $self->{tablecache_hitrate} = 100; + $self->{tablecache_fillrate} = + 100 * $self->{open_tables} / $self->{table_cache}; + } else { + $self->{tablecache_hitrate} = 0; + $self->{tablecache_fillrate} = 0; + $self->add_nagios_critical("no table cache"); + } + } elsif ($params{mode} =~ /server::instance::tablelockcontention/) { + ($dummy, $self->{table_locks_waited}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Table_locks_waited' + }); + ($dummy, $self->{table_locks_immediate}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Table_locks_immediate' + }); + $self->valdiff(\%params, qw(table_locks_waited table_locks_immediate)); + $self->{table_lock_contention} = + ($self->{table_locks_waited} + $self->{table_locks_immediate}) > 0 ? + 100 * $self->{table_locks_waited} / + ($self->{table_locks_waited} + $self->{table_locks_immediate}) : + 100; + $self->{table_lock_contention_now} = + ($self->{delta_table_locks_waited} + $self->{delta_table_locks_immediate}) > 0 ? + 100 * $self->{delta_table_locks_waited} / + ($self->{delta_table_locks_waited} + $self->{delta_table_locks_immediate}) : + 100; + } elsif ($params{mode} =~ /server::instance::tableindexusage/) { + # http://johnjacobm.wordpress.com/2007/06/ + # formula for calculating the percentage of full table scans + ($dummy, $self->{handler_read_first}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Handler_read_first' + }); + ($dummy, $self->{handler_read_key}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Handler_read_key' + }); + ($dummy, $self->{handler_read_next}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Handler_read_next' + }); + ($dummy, $self->{handler_read_prev}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Handler_read_prev' + }); + ($dummy, $self->{handler_read_rnd}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Handler_read_rnd' + }); + ($dummy, $self->{handler_read_rnd_next}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Handler_read_rnd_next' + }); + $self->valdiff(\%params, qw(handler_read_first handler_read_key + handler_read_next handler_read_prev handler_read_rnd + handler_read_rnd_next)); + my $delta_reads = $self->{delta_handler_read_first} + + $self->{delta_handler_read_key} + + $self->{delta_handler_read_next} + + $self->{delta_handler_read_prev} + + $self->{delta_handler_read_rnd} + + $self->{delta_handler_read_rnd_next}; + my $reads = $self->{handler_read_first} + + $self->{handler_read_key} + + $self->{handler_read_next} + + $self->{handler_read_prev} + + $self->{handler_read_rnd} + + $self->{handler_read_rnd_next}; + $self->{index_usage_now} = ($delta_reads == 0) ? 0 : + 100 - (100.0 * ($self->{delta_handler_read_rnd} + + $self->{delta_handler_read_rnd_next}) / + $delta_reads); + $self->{index_usage} = ($reads == 0) ? 0 : + 100 - (100.0 * ($self->{handler_read_rnd} + + $self->{handler_read_rnd_next}) / + $reads); + } elsif ($params{mode} =~ /server::instance::tabletmpondisk/) { + ($dummy, $self->{created_tmp_tables}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Created_tmp_tables' + }); + ($dummy, $self->{created_tmp_disk_tables}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Created_tmp_disk_tables' + }); + $self->valdiff(\%params, qw(created_tmp_tables created_tmp_disk_tables)); + $self->{pct_tmp_on_disk} = $self->{created_tmp_tables} > 0 ? + 100 * $self->{created_tmp_disk_tables} / $self->{created_tmp_tables} : + 100; + $self->{pct_tmp_on_disk_now} = $self->{delta_created_tmp_tables} > 0 ? + 100 * $self->{delta_created_tmp_disk_tables} / $self->{delta_created_tmp_tables} : + 100; + } elsif ($params{mode} =~ /server::instance::openfiles/) { + ($dummy, $self->{open_files_limit}) = $self->{handle}->fetchrow_array(q{ + SHOW VARIABLES LIKE 'open_files_limit' + }); + ($dummy, $self->{open_files}) = $self->{handle}->fetchrow_array(q{ + SHOW /*!50000 global */ STATUS LIKE 'Open_files' + }); + $self->{pct_open_files} = 100 * $self->{open_files} / $self->{open_files_limit}; + } elsif ($params{mode} =~ /server::instance::needoptimize/) { + $self->{fragmented} = []; + #http://www.electrictoolbox.com/optimize-tables-mysql-php/ + my @result = $self->{handle}->fetchall_array(q{ + SHOW TABLE STATUS + }); + foreach (@result) { + my ($name, $engine, $data_length, $data_free) = + ($_->[0], $_->[1], $_->[6 ], $_->[9]); + next if ($params{name} && $params{name} ne $name); + my $fragmentation = $data_length ? $data_free * 100 / $data_length : 0; + push(@{$self->{fragmented}}, + [$name, $fragmentation, $data_length, $data_free]); + } + } elsif ($params{mode} =~ /server::instance::myisam/) { + $self->{engine_myisam} = DBD::MySQL::Server::Instance::MyISAM->new( + %params + ); + } elsif ($params{mode} =~ /server::instance::innodb/) { + $self->{engine_innodb} = DBD::MySQL::Server::Instance::Innodb->new( + %params + ); + } elsif ($params{mode} =~ /server::instance::replication/) { + $self->{replication} = DBD::MySQL::Server::Instance::Replication->new( + %params + ); + } +} + +sub nagios { + my $self = shift; + my %params = @_; + if (! $self->{nagios_level}) { + if ($params{mode} =~ /server::instance::connectedthreads/) { + $self->add_nagios( + $self->check_thresholds($self->{threads_connected}, 10, 20), + sprintf "%d client connection threads", $self->{threads_connected}); + $self->add_perfdata(sprintf "threads_connected=%d;%d;%d", + $self->{threads_connected}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::createdthreads/) { + $self->add_nagios( + $self->check_thresholds($self->{threads_created_per_sec}, 10, 20), + sprintf "%.2f threads created/sec", $self->{threads_created_per_sec}); + $self->add_perfdata(sprintf "threads_created_per_sec=%.2f;%.2f;%.2f", + $self->{threads_created_per_sec}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::runningthreads/) { + $self->add_nagios( + $self->check_thresholds($self->{threads_running}, 10, 20), + sprintf "%d running threads", $self->{threads_running}); + $self->add_perfdata(sprintf "threads_running=%d;%d;%d", + $self->{threads_running}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::cachedthreads/) { + $self->add_nagios( + $self->check_thresholds($self->{threads_cached}, 10, 20), + sprintf "%d cached threads", $self->{threads_cached}); + $self->add_perfdata(sprintf "threads_cached=%d;%d;%d", + $self->{threads_cached}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::abortedconnects/) { + $self->add_nagios( + $self->check_thresholds($self->{connects_aborted_per_sec}, 1, 5), + sprintf "%.2f aborted connections/sec", $self->{connects_aborted_per_sec}); + $self->add_perfdata(sprintf "connects_aborted_per_sec=%.2f;%.2f;%.2f", + $self->{connects_aborted_per_sec}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::abortedclients/) { + $self->add_nagios( + $self->check_thresholds($self->{clients_aborted_per_sec}, 1, 5), + sprintf "%.2f aborted (client died) connections/sec", $self->{clients_aborted_per_sec}); + $self->add_perfdata(sprintf "clients_aborted_per_sec=%.2f;%.2f;%.2f", + $self->{clients_aborted_per_sec}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::threadcachehitrate/) { + my $refkey = 'threadcache_hitrate'.($params{lookback} ? '_now' : ''); + $self->add_nagios( + $self->check_thresholds($self->{$refkey}, "90:", "80:"), + sprintf "thread cache hitrate %.2f%%", $self->{$refkey}); + $self->add_perfdata(sprintf "thread_cache_hitrate=%.2f%%;%s;%s", + $self->{threadcache_hitrate}, + $self->{warningrange}, $self->{criticalrange}); + $self->add_perfdata(sprintf "thread_cache_hitrate_now=%.2f%%", + $self->{threadcache_hitrate_now}); + $self->add_perfdata(sprintf "connections_per_sec=%.2f", + $self->{connections_per_sec}); + } elsif ($params{mode} =~ /server::instance::querycachehitrate/) { + my $refkey = 'querycache_hitrate'.($params{lookback} ? '_now' : ''); + if ((lc $self->{have_query_cache} eq 'yes') && ($self->{query_cache_size})) { + $self->add_nagios( + $self->check_thresholds($self->{$refkey}, "90:", "80:"), + sprintf "query cache hitrate %.2f%%", $self->{$refkey}); + } else { + $self->check_thresholds($self->{$refkey}, "90:", "80:"); + $self->add_nagios_ok( + sprintf "query cache hitrate %.2f%% (because it's turned off)", + $self->{querycache_hitrate}); + } + $self->add_perfdata(sprintf "qcache_hitrate=%.2f%%;%s;%s", + $self->{querycache_hitrate}, + $self->{warningrange}, $self->{criticalrange}); + $self->add_perfdata(sprintf "qcache_hitrate_now=%.2f%%", + $self->{querycache_hitrate_now}); + $self->add_perfdata(sprintf "selects_per_sec=%.2f", + $self->{selects_per_sec}); + } elsif ($params{mode} =~ /server::instance::querycachelowmemprunes/) { + $self->add_nagios( + $self->check_thresholds($self->{lowmem_prunes_per_sec}, "1", "10"), + sprintf "%d query cache lowmem prunes in %d seconds (%.2f/sec)", + $self->{delta_lowmem_prunes}, $self->{delta_timestamp}, + $self->{lowmem_prunes_per_sec}); + $self->add_perfdata(sprintf "qcache_lowmem_prunes_rate=%.2f;%s;%s", + $self->{lowmem_prunes_per_sec}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::slowqueries/) { + $self->add_nagios( + $self->check_thresholds($self->{slow_queries_per_sec}, "0.1", "1"), + sprintf "%d slow queries in %d seconds (%.2f/sec)", + $self->{delta_slow_queries}, $self->{delta_timestamp}, + $self->{slow_queries_per_sec}); + $self->add_perfdata(sprintf "slow_queries_rate=%.2f%%;%s;%s", + $self->{slow_queries_per_sec}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::longprocs/) { + $self->add_nagios( + $self->check_thresholds($self->{longrunners}, 10, 20), + sprintf "%d long running processes", $self->{longrunners}); + $self->add_perfdata(sprintf "long_running_procs=%d;%d;%d", + $self->{longrunners}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /server::instance::tablecachehitrate/) { + if ($self->{tablecache_fillrate} < 95) { + $self->add_nagios_ok( + sprintf "table cache hitrate %.2f%%, %.2f%% filled", + $self->{tablecache_hitrate}, + $self->{tablecache_fillrate}); + $self->check_thresholds($self->{tablecache_hitrate}, "99:", "95:"); + } else { + $self->add_nagios( + $self->check_thresholds($self->{tablecache_hitrate}, "99:", "95:"), + sprintf "table cache hitrate %.2f%%", $self->{tablecache_hitrate}); + } + $self->add_perfdata(sprintf "tablecache_hitrate=%.2f%%;%s;%s", + $self->{tablecache_hitrate}, + $self->{warningrange}, $self->{criticalrange}); + $self->add_perfdata(sprintf "tablecache_fillrate=%.2f%%", + $self->{tablecache_fillrate}); + } elsif ($params{mode} =~ /server::instance::tablelockcontention/) { + my $refkey = 'table_lock_contention'.($params{lookback} ? '_now' : ''); + if ($self->{uptime} > 10800) { # MySQL Bug #30599 + $self->add_nagios( + $self->check_thresholds($self->{$refkey}, "1", "2"), + sprintf "table lock contention %.2f%%", $self->{$refkey}); + } else { + $self->check_thresholds($self->{$refkey}, "1", "2"); + $self->add_nagios_ok( + sprintf "table lock contention %.2f%% (uptime < 10800)", + $self->{$refkey}); + } + $self->add_perfdata(sprintf "tablelock_contention=%.2f%%;%s;%s", + $self->{table_lock_contention}, + $self->{warningrange}, $self->{criticalrange}); + $self->add_perfdata(sprintf "tablelock_contention_now=%.2f%%", + $self->{table_lock_contention_now}); + } elsif ($params{mode} =~ /server::instance::tableindexusage/) { + my $refkey = 'index_usage'.($params{lookback} ? '_now' : ''); + $self->add_nagios( + $self->check_thresholds($self->{$refkey}, "90:", "80:"), + sprintf "index usage %.2f%%", $self->{$refkey}); + $self->add_perfdata(sprintf "index_usage=%.2f%%;%s;%s", + $self->{index_usage}, + $self->{warningrange}, $self->{criticalrange}); + $self->add_perfdata(sprintf "index_usage_now=%.2f%%", + $self->{index_usage_now}); + } elsif ($params{mode} =~ /server::instance::tabletmpondisk/) { + my $refkey = 'pct_tmp_on_disk'.($params{lookback} ? '_now' : ''); + $self->add_nagios( + $self->check_thresholds($self->{$refkey}, "25", "50"), + sprintf "%.2f%% of %d tables were created on disk", + $self->{$refkey}, $self->{delta_created_tmp_tables}); + $self->add_perfdata(sprintf "pct_tmp_table_on_disk=%.2f%%;%s;%s", + $self->{pct_tmp_on_disk}, + $self->{warningrange}, $self->{criticalrange}); + $self->add_perfdata(sprintf "pct_tmp_table_on_disk_now=%.2f%%", + $self->{pct_tmp_on_disk_now}); + } elsif ($params{mode} =~ /server::instance::openfiles/) { + $self->add_nagios( + $self->check_thresholds($self->{pct_open_files}, 80, 95), + sprintf "%.2f%% of the open files limit reached (%d of max. %d)", + $self->{pct_open_files}, + $self->{open_files}, $self->{open_files_limit}); + $self->add_perfdata(sprintf "pct_open_files=%.3f%%;%.3f;%.3f", + $self->{pct_open_files}, + $self->{warningrange}, + $self->{criticalrange}); + $self->add_perfdata(sprintf "open_files=%d;%d;%d", + $self->{open_files}, + $self->{open_files_limit} * $self->{warningrange} / 100, + $self->{open_files_limit} * $self->{criticalrange} / 100); + } elsif ($params{mode} =~ /server::instance::needoptimize/) { + foreach (@{$self->{fragmented}}) { + $self->add_nagios( + $self->check_thresholds($_->[1], 10, 25), + sprintf "table %s is %.2f%% fragmented", $_->[0], $_->[1]); + if ($params{name}) { + $self->add_perfdata(sprintf "'%s_frag'=%.2f%%;%d;%d", + $_->[0], $_->[1], $self->{warningrange}, $self->{criticalrange}); + } + } + } elsif ($params{mode} =~ /server::instance::myisam/) { + $self->{engine_myisam}->nagios(%params); + $self->merge_nagios($self->{engine_myisam}); + } elsif ($params{mode} =~ /server::instance::innodb/) { + $self->{engine_innodb}->nagios(%params); + $self->merge_nagios($self->{engine_innodb}); + } elsif ($params{mode} =~ /server::instance::replication/) { + $self->{replication}->nagios(%params); + $self->merge_nagios($self->{replication}); + } + } +} + + + +package DBD::MySQL::Server; + +use strict; +use Time::HiRes; +use IO::File; +use File::Copy 'cp'; +use Data::Dumper; + + +{ + our $verbose = 0; + our $scream = 0; # scream if something is not implemented + our $access = "dbi"; # how do we access the database. + our $my_modules_dyn_dir = ""; # where we look for self-written extensions + + my @servers = (); + my $initerrors = undef; + + sub add_server { + push(@servers, shift); + } + + sub return_servers { + return @servers; + } + + sub return_first_server() { + return $servers[0]; + } + +} + +sub new { + my $class = shift; + my %params = @_; + my $self = { + mode => $params{mode}, + access => $params{method} || 'dbi', + hostname => $params{hostname}, + database => $params{database} || 'information_schema', + port => $params{port}, + socket => $params{socket}, + username => $params{username}, + password => $params{password}, + replication_user => $params{replication_user}, + mycnf => $params{mycnf}, + mycnfgroup => $params{mycnfgroup}, + timeout => $params{timeout}, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + verbose => $params{verbose}, + report => $params{report}, + negate => $params{negate}, + labelformat => $params{labelformat}, + version => 'unknown', + instance => undef, + handle => undef, + }; + bless $self, $class; + $self->init_nagios(); + if ($self->dbconnect(%params)) { + ($self->{dummy}, $self->{version}) = $self->{handle}->fetchrow_array( + #q{ SHOW VARIABLES WHERE Variable_name = 'version' } + q{ SHOW VARIABLES LIKE 'version' } + ); + $self->{version} = (split "-", $self->{version})[0]; + ($self->{dummy}, $self->{uptime}) = $self->{handle}->fetchrow_array( + q{ SHOW STATUS LIKE 'Uptime' } + ); + DBD::MySQL::Server::add_server($self); + $self->init(%params); + } + return $self; +} + +sub init { + my $self = shift; + my %params = @_; + $params{handle} = $self->{handle}; + $params{uptime} = $self->{uptime}; + $self->set_global_db_thresholds(\%params); + if ($params{mode} =~ /^server::instance/) { + $self->{instance} = DBD::MySQL::Server::Instance->new(%params); + } elsif ($params{mode} =~ /^server::sql/) { + $self->set_local_db_thresholds(%params); + if ($params{regexp}) { + # sql output is treated as text + if ($params{name2} eq $params{name}) { + $self->add_nagios_unknown(sprintf "where's the regexp????"); + } else { + $self->{genericsql} = + $self->{handle}->fetchrow_array($params{selectname}); + if (! defined $self->{genericsql}) { + $self->add_nagios_unknown(sprintf "got no valid response for %s", + $params{selectname}); + } + } + } else { + # sql output must be a number (or array of numbers) + @{$self->{genericsql}} = + $self->{handle}->fetchrow_array($params{selectname}); + if ($self->{handle}->{errstr}) { + $self->add_nagios_unknown(sprintf "got no valid response for %s: %s", + $params{selectname}, $self->{handle}->{errstr}); + } elsif (! (defined $self->{genericsql} && + (scalar(grep { + /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/ + } @{$self->{genericsql}})) == + scalar(@{$self->{genericsql}}))) { + $self->add_nagios_unknown(sprintf "got no valid response for %s", + $params{selectname}); + } elsif (! defined $self->{genericsql}) { + $self->add_nagios_unknown(sprintf "got no valid response for %s", + $params{selectname}); + } else { + # name2 in array + # units in array + } + } + } elsif ($params{mode} =~ /^server::uptime/) { + # already set with the connection. but use minutes here + } elsif ($params{mode} =~ /^server::connectiontime/) { + $self->{connection_time} = $self->{tac} - $self->{tic}; + } elsif ($params{mode} =~ /^my::([^:.]+)/) { + my $class = $1; + my $loaderror = undef; + substr($class, 0, 1) = uc substr($class, 0, 1); + foreach my $libpath (split(":", $DBD::MySQL::Server::my_modules_dyn_dir)) { + foreach my $extmod (glob $libpath."/CheckMySQLHealth*.pm") { + eval { + $self->trace(sprintf "loading module %s", $extmod); + require $extmod; + }; + if ($@) { + $loaderror = $extmod; + $self->trace(sprintf "failed loading module %s: %s", $extmod, $@); + } + } + } + my $obj = { + handle => $params{handle}, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + }; + bless $obj, "My$class"; + $self->{my} = $obj; + if ($self->{my}->isa("DBD::MySQL::Server")) { + my $dos_init = $self->can("init"); + my $dos_nagios = $self->can("nagios"); + my $my_init = $self->{my}->can("init"); + my $my_nagios = $self->{my}->can("nagios"); + if ($my_init == $dos_init) { + $self->add_nagios_unknown( + sprintf "Class %s needs an init() method", ref($self->{my})); + } elsif ($my_nagios == $dos_nagios) { + $self->add_nagios_unknown( + sprintf "Class %s needs a nagios() method", ref($self->{my})); + } else { + $self->{my}->init_nagios(%params); + $self->{my}->init(%params); + } + } else { + $self->add_nagios_unknown( + sprintf "Class %s is not a subclass of DBD::MySQL::Server%s", + ref($self->{my}), + $loaderror ? sprintf " (syntax error in %s?)", $loaderror : "" ); + } + } else { + printf "broken mode %s\n", $params{mode}; + } +} + +sub dump { + my $self = shift; + my $message = shift || ""; + printf "%s %s\n", $message, Data::Dumper::Dumper($self); +} + +sub nagios { + my $self = shift; + my %params = @_; + if (! $self->{nagios_level}) { + if ($params{mode} =~ /^server::instance/) { + $self->{instance}->nagios(%params); + $self->merge_nagios($self->{instance}); + } elsif ($params{mode} =~ /^server::database/) { + $self->{database}->nagios(%params); + $self->merge_nagios($self->{database}); + } elsif ($params{mode} =~ /^server::uptime/) { + $self->add_nagios( + $self->check_thresholds($self->{uptime} / 60, "10:", "5:"), + sprintf "database is up since %d minutes", $self->{uptime} / 60); + $self->add_perfdata(sprintf "uptime=%ds", + $self->{uptime}); + } elsif ($params{mode} =~ /^server::connectiontime/) { + $self->add_nagios( + $self->check_thresholds($self->{connection_time}, 1, 5), + sprintf "%.2f seconds to connect as %s", + $self->{connection_time}, ($self->{username} || getpwuid($<))); + $self->add_perfdata(sprintf "connection_time=%.4fs;%d;%d", + $self->{connection_time}, + $self->{warningrange}, $self->{criticalrange}); + } elsif ($params{mode} =~ /^server::sql/) { + if ($params{regexp}) { + if (substr($params{name2}, 0, 1) eq '!') { + $params{name2} =~ s/^!//; + if ($self->{genericsql} !~ /$params{name2}/) { + $self->add_nagios_ok( + sprintf "output %s does not match pattern %s", + $self->{genericsql}, $params{name2}); + } else { + $self->add_nagios_critical( + sprintf "output %s matches pattern %s", + $self->{genericsql}, $params{name2}); + } + } else { + if ($self->{genericsql} =~ /$params{name2}/) { + $self->add_nagios_ok( + sprintf "output %s matches pattern %s", + $self->{genericsql}, $params{name2}); + } else { + $self->add_nagios_critical( + sprintf "output %s does not match pattern %s", + $self->{genericsql}, $params{name2}); + } + } + } else { + $self->add_nagios( + # the first item in the list will trigger the threshold values + $self->check_thresholds($self->{genericsql}[0], 1, 5), + sprintf "%s: %s%s", + $params{name2} ? lc $params{name2} : lc $params{selectname}, + # float as float, integers as integers + join(" ", map { + (sprintf("%d", $_) eq $_) ? $_ : sprintf("%f", $_) + } @{$self->{genericsql}}), + $params{units} ? $params{units} : ""); + my $i = 0; + # workaround... getting the column names from the database would be nicer + my @names2_arr = split(/\s+/, $params{name2}); + foreach my $t (@{$self->{genericsql}}) { + $self->add_perfdata(sprintf "\'%s\'=%s%s;%s;%s", + $names2_arr[$i] ? lc $names2_arr[$i] : lc $params{selectname}, + # float as float, integers as integers + (sprintf("%d", $t) eq $t) ? $t : sprintf("%f", $t), + $params{units} ? $params{units} : "", + ($i == 0) ? $self->{warningrange} : "", + ($i == 0) ? $self->{criticalrange} : "" + ); + $i++; + } + } + } elsif ($params{mode} =~ /^my::([^:.]+)/) { + $self->{my}->nagios(%params); + $self->merge_nagios($self->{my}); + } + } +} + + +sub init_nagios { + my $self = shift; + no strict 'refs'; + if (! ref($self)) { + my $nagiosvar = $self."::nagios"; + my $nagioslevelvar = $self."::nagios_level"; + $$nagiosvar = { + messages => { + 0 => [], + 1 => [], + 2 => [], + 3 => [], + }, + perfdata => [], + }; + $$nagioslevelvar = $ERRORS{OK}, + } else { + $self->{nagios} = { + messages => { + 0 => [], + 1 => [], + 2 => [], + 3 => [], + }, + perfdata => [], + }; + $self->{nagios_level} = $ERRORS{OK}, + } +} + +sub check_thresholds { + my $self = shift; + my $value = shift; + my $defaultwarningrange = shift; + my $defaultcriticalrange = shift; + my $level = $ERRORS{OK}; + $self->{warningrange} = defined $self->{warningrange} ? + $self->{warningrange} : $defaultwarningrange; + $self->{criticalrange} = defined $self->{criticalrange} ? + $self->{criticalrange} : $defaultcriticalrange; + + if ($self->{warningrange} =~ /^([-+]?[0-9]*\.?[0-9]+)$/) { + # warning = 10, warn if > 10 or < 0 + $level = $ERRORS{WARNING} + if ($value > $1 || $value < 0); + } elsif ($self->{warningrange} =~ /^([-+]?[0-9]*\.?[0-9]+):$/) { + # warning = 10:, warn if < 10 + $level = $ERRORS{WARNING} + if ($value < $1); + } elsif ($self->{warningrange} =~ /^~:([-+]?[0-9]*\.?[0-9]+)$/) { + # warning = ~:10, warn if > 10 + $level = $ERRORS{WARNING} + if ($value > $1); + } elsif ($self->{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 ($self->{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 ($self->{criticalrange} =~ /^([-+]?[0-9]*\.?[0-9]+)$/) { + # critical = 10, crit if > 10 or < 0 + $level = $ERRORS{CRITICAL} + if ($value > $1 || $value < 0); + } elsif ($self->{criticalrange} =~ /^([-+]?[0-9]*\.?[0-9]+):$/) { + # critical = 10:, crit if < 10 + $level = $ERRORS{CRITICAL} + if ($value < $1); + } elsif ($self->{criticalrange} =~ /^~:([-+]?[0-9]*\.?[0-9]+)$/) { + # critical = ~:10, crit if > 10 + $level = $ERRORS{CRITICAL} + if ($value > $1); + } elsif ($self->{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 ($self->{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; + # + # syntax error must be reported with returncode -1 + # +} + +sub add_nagios { + my $self = shift; + my $level = shift; + my $message = shift; + push(@{$self->{nagios}->{messages}->{$level}}, $message); + # recalc current level + foreach my $llevel (qw(CRITICAL WARNING UNKNOWN OK)) { + if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$llevel}}})) { + $self->{nagios_level} = $ERRORS{$llevel}; + } + } +} + +sub add_nagios_ok { + my $self = shift; + my $message = shift; + $self->add_nagios($ERRORS{OK}, $message); +} + +sub add_nagios_warning { + my $self = shift; + my $message = shift; + $self->add_nagios($ERRORS{WARNING}, $message); +} + +sub add_nagios_critical { + my $self = shift; + my $message = shift; + $self->add_nagios($ERRORS{CRITICAL}, $message); +} + +sub add_nagios_unknown { + my $self = shift; + my $message = shift; + $self->add_nagios($ERRORS{UNKNOWN}, $message); +} + +sub add_perfdata { + my $self = shift; + my $data = shift; + push(@{$self->{nagios}->{perfdata}}, $data); +} + +sub merge_nagios { + my $self = shift; + my $child = shift; + foreach my $level (0..3) { + foreach (@{$child->{nagios}->{messages}->{$level}}) { + $self->add_nagios($level, $_); + } + #push(@{$self->{nagios}->{messages}->{$level}}, + # @{$child->{nagios}->{messages}->{$level}}); + } + push(@{$self->{nagios}->{perfdata}}, @{$child->{nagios}->{perfdata}}); +} + +sub calculate_result { + my $self = shift; + my $labels = shift || {}; + my $multiline = 0; + map { + $self->{nagios_level} = $ERRORS{$_} if + (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$_}}})); + } ("OK", "UNKNOWN", "WARNING", "CRITICAL"); + if ($ENV{NRPE_MULTILINESUPPORT} && + length join(" ", @{$self->{nagios}->{perfdata}}) > 200) { + $multiline = 1; + } + my $all_messages = join(($multiline ? "\n" : ", "), map { + join(($multiline ? "\n" : ", "), @{$self->{nagios}->{messages}->{$ERRORS{$_}}}) + } grep { + scalar(@{$self->{nagios}->{messages}->{$ERRORS{$_}}}) + } ("CRITICAL", "WARNING", "UNKNOWN", "OK")); + my $bad_messages = join(($multiline ? "\n" : ", "), map { + join(($multiline ? "\n" : ", "), @{$self->{nagios}->{messages}->{$ERRORS{$_}}}) + } grep { + scalar(@{$self->{nagios}->{messages}->{$ERRORS{$_}}}) + } ("CRITICAL", "WARNING", "UNKNOWN")); + my $good_messages = join(($multiline ? "\n" : ", "), map { + join(($multiline ? "\n" : ", "), @{$self->{nagios}->{messages}->{$ERRORS{$_}}}) + } grep { + scalar(@{$self->{nagios}->{messages}->{$ERRORS{$_}}}) + } ("OK")); + my $all_messages_short = $bad_messages ? $bad_messages : 'no problems'; + # if mode = my-.... + # and there are some ok-messages + # output them instead of "no problems" + if ($self->{mode} =~ /^my\:\:/ && $good_messages) { + $all_messages_short = $bad_messages ? $bad_messages : $good_messages; + } + my $all_messages_html = "". + join("", map { + my $level = $_; + join("", map { + sprintf "", + $level, $_; + } @{$self->{nagios}->{messages}->{$ERRORS{$_}}}); + } grep { + scalar(@{$self->{nagios}->{messages}->{$ERRORS{$_}}}) + } ("CRITICAL", "WARNING", "UNKNOWN", "OK")). + "
%s
"; + if (exists $self->{identstring}) { + $self->{nagios_message} .= $self->{identstring}; + } + if ($self->{report} eq "long") { + $self->{nagios_message} .= $all_messages; + } elsif ($self->{report} eq "short") { + $self->{nagios_message} .= $all_messages_short; + } elsif ($self->{report} eq "html") { + $self->{nagios_message} .= $all_messages_short."\n".$all_messages_html; + } + foreach my $from (keys %{$self->{negate}}) { + if ((uc $from) =~ /^(OK|WARNING|CRITICAL|UNKNOWN)$/ && + (uc $self->{negate}->{$from}) =~ /^(OK|WARNING|CRITICAL|UNKNOWN)$/) { + if ($self->{nagios_level} == $ERRORS{uc $from}) { + $self->{nagios_level} = $ERRORS{uc $self->{negate}->{$from}}; + } + } + } + if ($self->{labelformat} eq "pnp4nagios") { + $self->{perfdata} = join(" ", @{$self->{nagios}->{perfdata}}); + } else { + $self->{perfdata} = join(" ", map { + my $perfdata = $_; + if ($perfdata =~ /^(.*?)=(.*)/) { + my $label = $1; + my $data = $2; + if (exists $labels->{$label} && + exists $labels->{$label}->{$self->{labelformat}}) { + $labels->{$label}->{$self->{labelformat}}."=".$data; + } else { + $perfdata; + } + } else { + $perfdata; + } + } @{$self->{nagios}->{perfdata}}); + } +} + +sub set_global_db_thresholds { + my $self = shift; + my $params = shift; + my $warning = undef; + my $critical = undef; + return unless defined $params->{dbthresholds}; + $params->{name0} = $params->{dbthresholds}; + # :pluginmode :name :warning :critical + # mode empty + # + eval { + if ($self->{handle}->fetchrow_array(q{ + SELECT table_name FROM information_schema.tables + WHERE table_schema = ? + AND table_name = 'CHECK_MYSQL_HEALTH_THRESHOLDS'; + }, $self->{database})) { # either --database... or information_schema + my @dbthresholds = $self->{handle}->fetchall_array(q{ + SELECT * FROM check_mysql_health_thresholds + }); + $params->{dbthresholds} = \@dbthresholds; + foreach (@dbthresholds) { + if (($_->[0] eq $params->{cmdlinemode}) && + (! defined $_->[1] || ! $_->[1])) { + ($warning, $critical) = ($_->[2], $_->[3]); + } + } + } + }; + if (! $@) { + if ($warning) { + $params->{warningrange} = $warning; + $self->trace("read warningthreshold %s from database", $warning); + } + if ($critical) { + $params->{criticalrange} = $critical; + $self->trace("read criticalthreshold %s from database", $critical); + } + } +} + +sub set_local_db_thresholds { + my $self = shift; + my %params = @_; + my $warning = undef; + my $critical = undef; + # :pluginmode :name :warning :critical + # mode name0 + # mode name2 + # mode name + # + # first: argument of --dbthresholds, it it exists + # second: --name2 + # third: --name + if (ref($params{dbthresholds}) eq 'ARRAY') { + my $marker; + foreach (@{$params{dbthresholds}}) { + if ($_->[0] eq $params{cmdlinemode}) { + if (defined $_->[1] && $params{name0} && $_->[1] eq $params{name0}) { + ($warning, $critical) = ($_->[2], $_->[3]); + $marker = $params{name0}; + last; + } elsif (defined $_->[1] && $params{name2} && $_->[1] eq $params{name2}) { + ($warning, $critical) = ($_->[2], $_->[3]); + $marker = $params{name2}; + last; + } elsif (defined $_->[1] && $params{name} && $_->[1] eq $params{name}) { + ($warning, $critical) = ($_->[2], $_->[3]); + $marker = $params{name}; + last; + } + } + } + if ($warning) { + $self->{warningrange} = $warning; + $self->trace("read warningthreshold %s for %s from database", + $marker, $warning); + } + if ($critical) { + $self->{criticalrange} = $critical; + $self->trace("read criticalthreshold %s for %s from database", + $marker, $critical); + } + } +} + +sub debug { + my $self = shift; + my $msg = shift; + if ($DBD::MySQL::Server::verbose) { + printf "%s %s\n", $msg, ref($self); + } +} + +sub dbconnect { + my $self = shift; + my %params = @_; + my $retval = undef; + $self->{tic} = Time::HiRes::time(); + $self->{handle} = DBD::MySQL::Server::Connection->new(%params); + if ($self->{handle}->{errstr}) { + if ($params{mode} =~ /^server::tnsping/ && + $self->{handle}->{errstr} =~ /ORA-01017/) { + $self->add_nagios($ERRORS{OK}, + sprintf "connection established to %s.", $self->{connect}); + $retval = undef; + } elsif ($self->{handle}->{errstr} eq "alarm\n") { + $self->add_nagios($ERRORS{CRITICAL}, + sprintf "connection could not be established within %d seconds", + $self->{timeout}); + } else { + $self->add_nagios($ERRORS{CRITICAL}, + sprintf "cannot connect to %s. %s", + $self->{database}, $self->{handle}->{errstr}); + $retval = undef; + } + } else { + $retval = $self->{handle}; + } + $self->{tac} = Time::HiRes::time(); + return $retval; +} + +sub trace { + my $self = shift; + my $format = shift; + $self->{trace} = -f "/tmp/check_mysql_health.trace" ? 1 : 0; + if ($self->{verbose}) { + printf("%s: ", scalar localtime); + printf($format, @_); + } + if ($self->{trace}) { + my $logfh = new IO::File; + $logfh->autoflush(1); + if ($logfh->open("/tmp/check_mysql_health.trace", "a")) { + $logfh->printf("%s: ", scalar localtime); + $logfh->printf($format, @_); + $logfh->printf("\n"); + $logfh->close(); + } + } +} + +sub DESTROY { + my $self = shift; + my $handle1 = "null"; + my $handle2 = "null"; + if (defined $self->{handle}) { + $handle1 = ref($self->{handle}); + if (defined $self->{handle}->{handle}) { + $handle2 = ref($self->{handle}->{handle}); + } + } + $self->trace(sprintf "DESTROY %s with handle %s %s", ref($self), $handle1, $handle2); + if (ref($self) eq "DBD::MySQL::Server") { + } + $self->trace(sprintf "DESTROY %s exit with handle %s %s", ref($self), $handle1, $handle2); + if (ref($self) eq "DBD::MySQL::Server") { + #printf "humpftata\n"; + } +} + +sub save_state { + my $self = shift; + my %params = @_; + my $extension = ""; + my $mode = $params{mode}; + if ($params{connect} && $params{connect} =~ /(\w+)\/(\w+)@(\w+)/) { + $params{connect} = $3; + } elsif ($params{connect}) { + # just to be sure + $params{connect} =~ s/\//_/g; + } + if ($^O =~ /MSWin/) { + $mode =~ s/::/_/g; + $params{statefilesdir} = $self->system_vartmpdir(); + } + if (! -d $params{statefilesdir}) { + eval { + use File::Path; + mkpath $params{statefilesdir}; + }; + } + if ($@ || ! -w $params{statefilesdir}) { + $self->add_nagios($ERRORS{CRITICAL}, + sprintf "statefilesdir %s does not exist or is not writable\n", + $params{statefilesdir}); + return; + } + my $statefile = sprintf "%s_%s", $params{hostname}, $mode; + $extension .= $params{differenciator} ? "_".$params{differenciator} : ""; + $extension .= $params{socket} ? "_".$params{socket} : ""; + $extension .= $params{port} ? "_".$params{port} : ""; + $extension .= $params{database} ? "_".$params{database} : ""; + $extension .= $params{tablespace} ? "_".$params{tablespace} : ""; + $extension .= $params{datafile} ? "_".$params{datafile} : ""; + $extension .= $params{name} ? "_".$params{name} : ""; + $extension =~ s/\//_/g; + $extension =~ s/\(/_/g; + $extension =~ s/\)/_/g; + $extension =~ s/\*/_/g; + $extension =~ s/\s/_/g; + $statefile .= $extension; + $statefile = lc $statefile; + $statefile = sprintf "%s/%s", $params{statefilesdir}, $statefile; + if (open(STATE, ">$statefile")) { + if ((ref($params{save}) eq "HASH") && exists $params{save}->{timestamp}) { + $params{save}->{localtime} = scalar localtime $params{save}->{timestamp}; + } + printf STATE Data::Dumper::Dumper($params{save}); + close STATE; + } else { + $self->add_nagios($ERRORS{CRITICAL}, + sprintf "statefile %s is not writable", $statefile); + } + $self->debug(sprintf "saved %s to %s", + Data::Dumper::Dumper($params{save}), $statefile); +} + +sub load_state { + my $self = shift; + my %params = @_; + my $extension = ""; + my $mode = $params{mode}; + if ($params{connect} && $params{connect} =~ /(\w+)\/(\w+)@(\w+)/) { + $params{connect} = $3; + } elsif ($params{connect}) { + # just to be sure + $params{connect} =~ s/\//_/g; + } + if ($^O =~ /MSWin/) { + $mode =~ s/::/_/g; + $params{statefilesdir} = $self->system_vartmpdir(); + } + my $statefile = sprintf "%s_%s", $params{hostname}, $mode; + $extension .= $params{differenciator} ? "_".$params{differenciator} : ""; + $extension .= $params{socket} ? "_".$params{socket} : ""; + $extension .= $params{port} ? "_".$params{port} : ""; + $extension .= $params{database} ? "_".$params{database} : ""; + $extension .= $params{tablespace} ? "_".$params{tablespace} : ""; + $extension .= $params{datafile} ? "_".$params{datafile} : ""; + $extension .= $params{name} ? "_".$params{name} : ""; + $extension =~ s/\//_/g; + $extension =~ s/\(/_/g; + $extension =~ s/\)/_/g; + $extension =~ s/\*/_/g; + $extension =~ s/\s/_/g; + $statefile .= $extension; + $statefile = lc $statefile; + $statefile = sprintf "%s/%s", $params{statefilesdir}, $statefile; + if ( -f $statefile) { + our $VAR1; + eval { + require $statefile; + }; + if($@) { + $self->add_nagios($ERRORS{CRITICAL}, + sprintf "statefile %s is corrupt", $statefile); + } + $self->debug(sprintf "load %s", Data::Dumper::Dumper($VAR1)); + return $VAR1; + } else { + return undef; + } +} + +sub valdiff { + my $self = shift; + my $pparams = shift; + my %params = %{$pparams}; + my @keys = @_; + my $now = time; + my $last_values = $self->load_state(%params) || eval { + my $empty_events = {}; + foreach (@keys) { + $empty_events->{$_} = 0; + } + $empty_events->{timestamp} = 0; + if ($params{lookback}) { + $empty_events->{lookback_history} = {}; + } + $empty_events; + }; + foreach (@keys) { + if ($params{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}->{$_}}) { + if ($date >= ($now - $params{lookback})) { + $last_values->{$_} = $last_values->{lookback_history}->{$_}->{$date}; + $last_values->{timestamp} = $date; + last; + } else { + delete $last_values->{lookback_history}->{$_}->{$date}; + } + } + } + } + $last_values->{$_} = 0 if ! exists $last_values->{$_}; + if ($self->{$_} >= $last_values->{$_}) { + $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->{'delta_timestamp'} = $now - $last_values->{timestamp}; + $params{save} = eval { + my $empty_events = {}; + foreach (@keys) { + $empty_events->{$_} = $self->{$_}; + } + $empty_events->{timestamp} = $now; + if ($params{lookback}) { + $empty_events->{lookback_history} = $last_values->{lookback_history}; + foreach (@keys) { + $empty_events->{lookback_history}->{$_}->{$now} = $self->{$_}; + } + } + $empty_events; + }; + $self->save_state(%params); +} + +sub requires_version { + my $self = shift; + my $version = shift; + my @instances = DBD::MySQL::Server::return_servers(); + my $instversion = $instances[0]->{version}; + if (! $self->version_is_minimum($version)) { + $self->add_nagios($ERRORS{UNKNOWN}, + sprintf "not implemented/possible for MySQL release %s", $instversion); + } +} + +sub version_is_minimum { + # the current version is newer or equal + my $self = shift; + my $version = shift; + my $newer = 1; + my @instances = DBD::MySQL::Server::return_servers(); + my @v1 = map { $_ eq "x" ? 0 : $_ } split(/\./, $version); + my @v2 = split(/\./, $instances[0]->{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; + } + } + #printf STDERR "check if %s os minimum %s\n", join(".", @v2), join(".", @v1); + return $newer; +} + +sub instance_thread { + my $self = shift; + my @instances = DBD::MySQL::Server::return_servers(); + return $instances[0]->{thread}; +} + +sub windows_server { + my $self = shift; + my @instances = DBD::MySQL::Server::return_servers(); + if ($instances[0]->{os} =~ /Win/i) { + return 1; + } else { + return 0; + } +} + +sub system_vartmpdir { + my $self = shift; + if ($^O =~ /MSWin/) { + return $self->system_tmpdir(); + } else { + return "/var/tmp/check_mysql_health"; + } +} + +sub system_oldvartmpdir { + my $self = shift; + return "/tmp"; +} + +sub system_tmpdir { + my $self = shift; + 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 decode_password { + my $self = shift; + my $password = shift; + if ($password && $password =~ /^rfc3986:\/\/(.*)/) { + $password = $1; + $password =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg; + } + return $password; +} + + +package DBD::MySQL::Server::Connection; + +use strict; + +our @ISA = qw(DBD::MySQL::Server); + + +sub new { + my $class = shift; + my %params = @_; + my $self = { + mode => $params{mode}, + timeout => $params{timeout}, + access => $params{method} || "dbi", + hostname => $params{hostname}, + database => $params{database} || "information_schema", + port => $params{port}, + socket => $params{socket}, + username => $params{username}, + password => $params{password}, + mycnf => $params{mycnf}, + mycnfgroup => $params{mycnfgroup}, + handle => undef, + }; + bless $self, $class; + if ($params{method} eq "dbi") { + bless $self, "DBD::MySQL::Server::Connection::Dbi"; + } elsif ($params{method} eq "mysql") { + bless $self, "DBD::MySQL::Server::Connection::Mysql"; + } elsif ($params{method} eq "sqlrelay") { + bless $self, "DBD::MySQL::Server::Connection::Sqlrelay"; + } + $self->init(%params); + return $self; +} + + +package DBD::MySQL::Server::Connection::Dbi; + +use strict; +use Net::Ping; + +our @ISA = qw(DBD::MySQL::Server::Connection); + + +sub init { + my $self = shift; + my %params = @_; + my $retval = undef; + if ($self->{mode} =~ /^server::tnsping/) { + if (! $self->{connect}) { + $self->{errstr} = "Please specify a database"; + } else { + $self->{sid} = $self->{connect}; + $self->{username} ||= time; # prefer an existing user + $self->{password} = time; + } + } else { + if ( + ($self->{hostname} ne 'localhost' && (! $self->{username} || ! $self->{password})) && + (! $self->{mycnf}) ) { + $self->{errstr} = "Please specify hostname, username and password or a .cnf file"; + return undef; + } + $self->{dsn} = "DBI:mysql:"; + $self->{dsn} .= sprintf "database=%s", $self->{database}; + if ($self->{mycnf}) { + $self->{dsn} .= sprintf ";mysql_read_default_file=%s", $self->{mycnf}; + if ($self->{mycnfgroup}) { + $self->{dsn} .= sprintf ";mysql_read_default_group=%s", $self->{mycnfgroup}; + } + } else { + $self->{dsn} .= sprintf ";host=%s", $self->{hostname}; + $self->{dsn} .= sprintf ";port=%s", $self->{port} + unless $self->{socket} || $self->{hostname} eq 'localhost'; + $self->{dsn} .= sprintf ";mysql_socket=%s", $self->{socket} + if $self->{socket}; + } + } + if (! exists $self->{errstr}) { + eval { + require DBI; + use POSIX ':signal_h'; + if ($^O =~ /MSWin/) { + local $SIG{'ALRM'} = sub { + die "alarm\n"; + }; + } else { + my $mask = POSIX::SigSet->new( SIGALRM ); + my $action = POSIX::SigAction->new( + sub { die "alarm\n" ; }, $mask); + my $oldaction = POSIX::SigAction->new(); + sigaction(SIGALRM ,$action ,$oldaction ); + } + alarm($self->{timeout} - 1); # 1 second before the global unknown timeout + if ($self->{handle} = DBI->connect( + $self->{dsn}, + $self->{username}, + $self->decode_password($self->{password}), + { RaiseError => 0, AutoCommit => 0, PrintError => 1 })) { +# $self->{handle}->do(q{ +# ALTER SESSION SET NLS_NUMERIC_CHARACTERS=".," }); + $retval = $self; + } else { + $self->{errstr} = DBI::errstr(); + } + }; + if ($@) { + $self->{errstr} = $@; + $retval = undef; + } + } + $self->{tac} = Time::HiRes::time(); + return $retval; +} + +sub selectrow_hashref { + my $self = shift; + my $sql = shift; + my @arguments = @_; + my $sth = undef; + my $hashref = undef; + eval { + $self->trace(sprintf "SQL:\n%s\nARGS:\n%s\n", + $sql, Data::Dumper::Dumper(\@arguments)); + # helm auf! jetzt wirds dreckig. + if ($sql =~ /^\s*SHOW/) { + $hashref = $self->{handle}->selectrow_hashref($sql); + } else { + $sth = $self->{handle}->prepare($sql); + if (scalar(@arguments)) { + $sth->execute(@arguments); + } else { + $sth->execute(); + } + $hashref = $sth->selectrow_hashref(); + } + $self->trace(sprintf "RESULT:\n%s\n", + Data::Dumper::Dumper($hashref)); + }; + if ($@) { + $self->debug(sprintf "bumm %s", $@); + } + if (-f "/tmp/check_mysql_health_simulation/".$self->{mode}) { + my $simulation = do { local (@ARGV, $/) = + "/tmp/check_mysql_health_simulation/".$self->{mode}; <> }; + # keine lust auf den scheiss + } + return $hashref; +} + +sub fetchrow_array { + my $self = shift; + my $sql = shift; + my @arguments = @_; + my $sth = undef; + my @row = (); + my $stderrvar; + *SAVEERR = *STDERR; + open ERR ,'>',\$stderrvar; + *STDERR = *ERR; + eval { + $self->trace(sprintf "SQL:\n%s\nARGS:\n%s\n", + $sql, Data::Dumper::Dumper(\@arguments)); + $sth = $self->{handle}->prepare($sql); + if (scalar(@arguments)) { + $sth->execute(@arguments); + } else { + $sth->execute(); + } + @row = $sth->fetchrow_array(); + $self->trace(sprintf "RESULT:\n%s\n", + Data::Dumper::Dumper(\@row)); + }; + *STDERR = *SAVEERR; + if ($@) { + $self->debug(sprintf "bumm %s", $@); + $self->{errstr} = $@; + return (undef); + } elsif ($stderrvar) { + $self->{errstr} = $stderrvar; + return (undef); + } elsif ($sth->errstr()) { + $self->{errstr} = $sth->errstr(); + return (undef); + } + if (-f "/tmp/check_mysql_health_simulation/".$self->{mode}) { + my $simulation = do { local (@ARGV, $/) = + "/tmp/check_mysql_health_simulation/".$self->{mode}; <> }; + @row = split(/\s+/, (split(/\n/, $simulation))[0]); + } + return $row[0] unless wantarray; + return @row; +} + +sub fetchall_array { + my $self = shift; + my $sql = shift; + my @arguments = @_; + my $sth = undef; + my $rows = undef; + eval { + $self->trace(sprintf "SQL:\n%s\nARGS:\n%s\n", + $sql, Data::Dumper::Dumper(\@arguments)); + $sth = $self->{handle}->prepare($sql); + if (scalar(@arguments)) { + $sth->execute(@arguments); + } else { + $sth->execute(); + } + $rows = $sth->fetchall_arrayref(); + $self->trace(sprintf "RESULT:\n%s\n", + Data::Dumper::Dumper($rows)); + }; + if ($@) { + printf STDERR "bumm %s\n", $@; + } + if (-f "/tmp/check_mysql_health_simulation/".$self->{mode}) { + my $simulation = do { local (@ARGV, $/) = + "/tmp/check_mysql_health_simulation/".$self->{mode}; <> }; + @{$rows} = map { [ split(/\s+/, $_) ] } split(/\n/, $simulation); + } + return @{$rows}; +} + +sub func { + my $self = shift; + $self->{handle}->func(@_); +} + + +sub execute { + my $self = shift; + my $sql = shift; + eval { + my $sth = $self->{handle}->prepare($sql); + $sth->execute(); + }; + if ($@) { + printf STDERR "bumm %s\n", $@; + } +} + +sub errstr { + my $self = shift; + return $self->{errstr}; +} + +sub DESTROY { + my $self = shift; + $self->trace(sprintf "disconnecting DBD %s", + $self->{handle} ? "with handle" : "without handle"); + $self->{handle}->disconnect() if $self->{handle}; +} + +package DBD::MySQL::Server::Connection::Mysql; + +use strict; +use File::Temp qw/tempfile/; + +our @ISA = qw(DBD::MySQL::Server::Connection); + + +sub init { + my $self = shift; + my %params = @_; + my $retval = undef; + $self->{loginstring} = "traditional"; + ($self->{sql_commandfile_handle}, $self->{sql_commandfile}) = + tempfile($self->{mode}."XXXXX", SUFFIX => ".sql", + DIR => $self->system_tmpdir() ); + close $self->{sql_commandfile_handle}; + ($self->{sql_resultfile_handle}, $self->{sql_resultfile}) = + tempfile($self->{mode}."XXXXX", SUFFIX => ".out", + DIR => $self->system_tmpdir() ); + close $self->{sql_resultfile_handle}; + if ($self->{mode} =~ /^server::tnsping/) { + if (! $self->{connect}) { + $self->{errstr} = "Please specify a database"; + } else { + $self->{sid} = $self->{connect}; + $self->{username} ||= time; # prefer an existing user + $self->{password} = time; + } + } else { + if (! $self->{username} || ! $self->{password}) { + $self->{errstr} = "Please specify database, username and password"; + return undef; + } elsif (! (($self->{hostname} && $self->{port}) || $self->{socket})) { + $self->{errstr} = "Please specify hostname and port or socket"; + return undef; + } + } + if (! exists $self->{errstr}) { + $self->{password} = $self->decode_password($self->{password}); + eval { + my $mysql = '/'.'usr'.'/'.'bin'.'/'.'mysql'; + if (! -x $mysql) { + die "nomysql\n"; + } + if ($self->{loginstring} eq "traditional") { + $self->{sqlplus} = sprintf "%s ", $mysql; + $self->{sqlplus} .= sprintf "--batch --raw --skip-column-names "; + $self->{sqlplus} .= sprintf "--database=%s ", $self->{database}; + $self->{sqlplus} .= sprintf "--host=%s ", $self->{hostname}; + $self->{sqlplus} .= sprintf "--port=%s ", $self->{port} + unless $self->{socket} || $self->{hostname} eq "localhost"; + $self->{sqlplus} .= sprintf "--socket=%s ", $self->{socket} + if $self->{socket}; + $self->{sqlplus} .= sprintf "--user=%s --password='%s' < %s > %s", + $self->{username}, $self->{password}, + $self->{sql_commandfile}, $self->{sql_resultfile}; + } + + use POSIX ':signal_h'; + if ($^O =~ /MSWin/) { + local $SIG{'ALRM'} = sub { + die "alarm\n"; + }; + } else { + my $mask = POSIX::SigSet->new( SIGALRM ); + my $action = POSIX::SigAction->new( + sub { die "alarm\n" ; }, $mask); + my $oldaction = POSIX::SigAction->new(); + sigaction(SIGALRM ,$action ,$oldaction ); + } + alarm($self->{timeout} - 1); # 1 second before the global unknown timeout + + my $answer = $self->fetchrow_array( + q{ SELECT 42 FROM dual}); + die unless defined $answer and $answer == 42; + $retval = $self; + }; + if ($@) { + $self->{errstr} = $@; + $self->{errstr} =~ s/at $0 .*//g; + chomp $self->{errstr}; + $retval = undef; + } + } + $self->{tac} = Time::HiRes::time(); + return $retval; +} + +sub selectrow_hashref { + my $self = shift; + my $sql = shift; + my @arguments = @_; + my $sth = undef; + my $hashref = undef; + foreach (@arguments) { + # replace the ? by the parameters + if (/^\d+$/) { + $sql =~ s/\?/$_/; + } else { + $sql =~ s/\?/'$_'/; + } + } + if ($sql =~ /^\s*SHOW/) { + $sql .= '\G'; # http://dev.mysql.com/doc/refman/5.1/de/show-slave-status.html + } + $self->trace(sprintf "SQL (? resolved):\n%s\nARGS:\n%s\n", + $sql, Data::Dumper::Dumper(\@arguments)); + $self->create_commandfile($sql); + my $exit_output = `$self->{sqlplus}`; + if ($?) { + printf STDERR "fetchrow_array exit bumm \n"; + my $output = do { local (@ARGV, $/) = $self->{sql_resultfile}; <> }; + my @oerrs = map { + /((ERROR \d+).*)/ ? $1 : (); + } split(/\n/, $output); + $self->{errstr} = join(" ", @oerrs); + } else { + my $output = do { local (@ARGV, $/) = $self->{sql_resultfile}; <> }; + if ($sql =~ /^\s*SHOW/) { + map { + if (/^\s*([\w_]+):\s*(.*)/) { + $hashref->{$1} = $2; + } + } split(/\n/, $output); + } else { + # i dont mess around here and you shouldn't either + } + $self->trace(sprintf "RESULT:\n%s\n", + Data::Dumper::Dumper($hashref)); + } + unlink $self->{sql_commandfile}; + unlink $self->{sql_resultfile}; + return $hashref; +} + +sub fetchrow_array { + my $self = shift; + my $sql = shift; + my @arguments = @_; + my $sth = undef; + my @row = (); + foreach (@arguments) { + # replace the ? by the parameters + if (/^\d+$/) { + $sql =~ s/\?/$_/; + } else { + $sql =~ s/\?/'$_'/; + } + } + $self->trace(sprintf "SQL (? resolved):\n%s\nARGS:\n%s\n", + $sql, Data::Dumper::Dumper(\@arguments)); + $self->create_commandfile($sql); + my $exit_output = `$self->{sqlplus}`; + if ($?) { + printf STDERR "fetchrow_array exit bumm \n"; + my $output = do { local (@ARGV, $/) = $self->{sql_resultfile}; <> }; + my @oerrs = map { + /((ERROR \d+).*)/ ? $1 : (); + } split(/\n/, $output); + $self->{errstr} = join(" ", @oerrs); + } else { + my $output = do { local (@ARGV, $/) = $self->{sql_resultfile}; <> }; + @row = map { convert($_) } + map { s/^\s+([\.\d]+)$/$1/g; $_ } # strip leading space from numbers + map { s/\s+$//g; $_ } # strip trailing space + split(/\t/, (split(/\n/, $output))[0]); + $self->trace(sprintf "RESULT:\n%s\n", + Data::Dumper::Dumper(\@row)); + } + if ($@) { + $self->debug(sprintf "bumm %s", $@); + } + unlink $self->{sql_commandfile}; + unlink $self->{sql_resultfile}; + return $row[0] unless wantarray; + return @row; +} + +sub fetchall_array { + my $self = shift; + my $sql = shift; + my @arguments = @_; + my $sth = undef; + my $rows = undef; + foreach (@arguments) { + # replace the ? by the parameters + if (/^\d+$/) { + $sql =~ s/\?/$_/; + } else { + $sql =~ s/\?/'$_'/; + } + } + $self->trace(sprintf "SQL (? resolved):\n%s\nARGS:\n%s\n", + $sql, Data::Dumper::Dumper(\@arguments)); + $self->create_commandfile($sql); + my $exit_output = `$self->{sqlplus}`; + if ($?) { + printf STDERR "fetchrow_array exit bumm %s\n", $exit_output; + my $output = do { local (@ARGV, $/) = $self->{sql_resultfile}; <> }; + my @oerrs = map { + /((ERROR \d+).*)/ ? $1 : (); + } split(/\n/, $output); + $self->{errstr} = join(" ", @oerrs); + } else { + my $output = do { local (@ARGV, $/) = $self->{sql_resultfile}; <> }; + my @rows = map { [ + map { convert($_) } + map { s/^\s+([\.\d]+)$/$1/g; $_ } + map { s/\s+$//g; $_ } + split /\t/ + ] } grep { ! /^\d+ rows selected/ } + grep { ! /^Elapsed: / } + grep { ! /^\s*$/ } split(/\n/, $output); + $rows = \@rows; + $self->trace(sprintf "RESULT:\n%s\n", + Data::Dumper::Dumper($rows)); + } + if ($@) { + $self->debug(sprintf "bumm %s", $@); + } + unlink $self->{sql_commandfile}; + unlink $self->{sql_resultfile}; + return @{$rows}; +} + +sub func { + my $self = shift; + my $function = shift; + $self->{handle}->func(@_); +} + +sub convert { + my $n = shift; + # 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 execute { + my $self = shift; + my $sql = shift; + eval { + my $sth = $self->{handle}->prepare($sql); + $sth->execute(); + }; + if ($@) { + printf STDERR "bumm %s\n", $@; + } +} + +sub errstr { + my $self = shift; + return $self->{errstr}; +} + +sub DESTROY { + my $self = shift; + $self->trace("try to clean up command and result files"); + unlink $self->{sql_commandfile} if -f $self->{sql_commandfile}; + unlink $self->{sql_resultfile} if -f $self->{sql_resultfile}; +} + +sub create_commandfile { + my $self = shift; + my $sql = shift; + open CMDCMD, "> $self->{sql_commandfile}"; + printf CMDCMD "%s\n", $sql; + close CMDCMD; +} + +sub decode_password { + my $self = shift; + my $password = shift; + $password = $self->SUPER::decode_password($password); + # we call '...%s/%s@...' inside backticks where the second %s is the password + # abc'xcv -> ''abc'\''xcv'' + # abc'`xcv -> ''abc'\''\`xcv'' + if ($password && $password =~ /'/) { + $password = "'".join("\\'", map { "'".$_."'"; } split("'", $password))."'"; + } + return $password; +} + + +package DBD::MySQL::Server::Connection::Sqlrelay; + +use strict; +use Net::Ping; + +our @ISA = qw(DBD::MySQL::Server::Connection); + + +sub init { + my $self = shift; + my %params = @_; + my $retval = undef; + if ($self->{mode} =~ /^server::tnsping/) { + if (! $self->{connect}) { + $self->{errstr} = "Please specify a database"; + } else { + if ($self->{connect} =~ /([\.\w]+):(\d+)/) { + $self->{host} = $1; + $self->{port} = $2; + $self->{socket} = ""; + } elsif ($self->{connect} =~ /([\.\w]+):([\w\/]+)/) { + $self->{host} = $1; + $self->{socket} = $2; + $self->{port} = ""; + } + } + } else { + if (! $self->{hostname} || ! $self->{username} || ! $self->{password}) { + if ($self->{hostname} && $self->{hostname} =~ /(\w+?)\/(.+)@([\.\w]+):(\d+)/) { + $self->{username} = $1; + $self->{password} = $2; + $self->{hostname} = $3; + $self->{port} = $4; + $self->{socket} = ""; + } elsif ($self->{hostname} && $self->{hostname} =~ /(\w+?)\/(.+)@([\.\w]+):([\w\/]+)/) { + $self->{username} = $1; + $self->{password} = $2; + $self->{hostname} = $3; + $self->{socket} = $4; + $self->{port} = ""; + } else { + $self->{errstr} = "Please specify database, username and password"; + return undef; + } + } else { + if ($self->{hostname} =~ /([\.\w]+):(\d+)/) { + $self->{hostname} = $1; + $self->{port} = $2; + $self->{socket} = ""; + } elsif ($self->{hostname} =~ /([\.\w]+):([\w\/]+)/) { + $self->{hostname} = $1; + $self->{socket} = $2; + $self->{port} = ""; + } else { + $self->{errstr} = "Please specify hostname, username, password and port/socket"; + return undef; + } + } + } + if (! exists $self->{errstr}) { + eval { + require DBI; + use POSIX ':signal_h'; + if ($^O =~ /MSWin/) { + local $SIG{'ALRM'} = sub { + die "alarm\n"; + }; + } else { + my $mask = POSIX::SigSet->new( SIGALRM ); + my $action = POSIX::SigAction->new( + sub { die "alarm\n" ; }, $mask); + my $oldaction = POSIX::SigAction->new(); + sigaction(SIGALRM ,$action ,$oldaction ); + } + alarm($self->{timeout} - 1); # 1 second before the global unknown timeout + if ($self->{handle} = DBI->connect( + sprintf("DBI:SQLRelay:host=%s;port=%d;socket=%s", + $self->{hostname}, $self->{port}, $self->{socket}), + $self->{username}, + $self->decode_password($self->{password}), + { RaiseError => 1, AutoCommit => 0, PrintError => 1 })) { + $retval = $self; + if ($self->{mode} =~ /^server::tnsping/ && $self->{handle}->ping()) { + # database connected. fake a "unknown user" + $self->{errstr} = "ORA-01017"; + } + } else { + $self->{errstr} = DBI::errstr(); + } + }; + if ($@) { + $self->{errstr} = $@; + $self->{errstr} =~ s/at [\w\/\.]+ line \d+.*//g; + $retval = undef; + } + } + $self->{tac} = Time::HiRes::time(); + return $retval; +} + +sub fetchrow_array { + my $self = shift; + my $sql = shift; + my @arguments = @_; + my $sth = undef; + my @row = (); + $self->trace(sprintf "fetchrow_array: %s", $sql); + eval { + $sth = $self->{handle}->prepare($sql); + if (scalar(@arguments)) { + $sth->execute(@arguments); + } else { + $sth->execute(); + } + @row = $sth->fetchrow_array(); + }; + if ($@) { + $self->debug(sprintf "bumm %s", $@); + } + if (-f "/tmp/check_mysql_health_simulation/".$self->{mode}) { + my $simulation = do { local (@ARGV, $/) = + "/tmp/check_mysql_health_simulation/".$self->{mode}; <> }; + @row = split(/\s+/, (split(/\n/, $simulation))[0]); + } + return $row[0] unless wantarray; + return @row; +} + +sub fetchall_array { + my $self = shift; + my $sql = shift; + my @arguments = @_; + my $sth = undef; + my $rows = undef; + $self->trace(sprintf "fetchall_array: %s", $sql); + eval { + $sth = $self->{handle}->prepare($sql); + if (scalar(@arguments)) { + $sth->execute(@arguments); + } else { + $sth->execute(); + } + $rows = $sth->fetchall_arrayref(); + }; + if ($@) { + printf STDERR "bumm %s\n", $@; + } + if (-f "/tmp/check_mysql_health_simulation/".$self->{mode}) { + my $simulation = do { local (@ARGV, $/) = + "/tmp/check_mysql_health_simulation/".$self->{mode}; <> }; + @{$rows} = map { [ split(/\s+/, $_) ] } split(/\n/, $simulation); + } + return @{$rows}; +} + +sub func { + my $self = shift; + $self->{handle}->func(@_); +} + +sub execute { + my $self = shift; + my $sql = shift; + eval { + my $sth = $self->{handle}->prepare($sql); + $sth->execute(); + }; + if ($@) { + printf STDERR "bumm %s\n", $@; + } +} + +sub DESTROY { + my $self = shift; + #$self->trace(sprintf "disconnecting DBD %s", + # $self->{handle} ? "with handle" : "without handle"); + #$self->{handle}->disconnect() if $self->{handle}; +} + + + + +package DBD::MySQL::Cluster; + +use strict; +use Time::HiRes; +use IO::File; +use Data::Dumper; + + +{ + our $verbose = 0; + our $scream = 0; # scream if something is not implemented + our $access = "dbi"; # how do we access the database. + our $my_modules_dyn_dir = ""; # where we look for self-written extensions + + my @clusters = (); + my $initerrors = undef; + + sub add_cluster { + push(@clusters, shift); + } + + sub return_clusters { + return @clusters; + } + + sub return_first_cluster() { + return $clusters[0]; + } + +} + +sub new { + my $class = shift; + my %params = @_; + my $self = { + hostname => $params{hostname}, + port => $params{port}, + username => $params{username}, + password => $params{password}, + timeout => $params{timeout}, + warningrange => $params{warningrange}, + criticalrange => $params{criticalrange}, + version => 'unknown', + nodes => [], + ndbd_nodes => 0, + ndb_mgmd_nodes => 0, + mysqld_nodes => 0, + }; + bless $self, $class; + $self->init_nagios(); + if ($self->connect(%params)) { + DBD::MySQL::Cluster::add_cluster($self); + $self->init(%params); + } + return $self; +} + +sub init { + my $self = shift; + my %params = @_; + if ($self->{show}) { + my $type = undef; + foreach (split /\n/, $self->{show}) { + if (/\[(\w+)\((\w+)\)\]\s+(\d+) node/) { + $type = uc $2; + } elsif (/id=(\d+)(.*)/) { + push(@{$self->{nodes}}, DBD::MySQL::Cluster::Node->new( + type => $type, + id => $1, + status => $2, + )); + } + } + } else { + } + if ($params{mode} =~ /^cluster::ndbdrunning/) { + foreach my $node (@{$self->{nodes}}) { + $node->{type} eq "NDB" && $node->{status} eq "running" && $self->{ndbd_nodes}++; + $node->{type} eq "MGM" && $node->{status} eq "running" && $self->{ndb_mgmd_nodes}++; + $node->{type} eq "API" && $node->{status} eq "running" && $self->{mysqld_nodes}++; + } + } else { + printf "broken mode %s\n", $params{mode}; + } +} + +sub dump { + my $self = shift; + my $message = shift || ""; + printf "%s %s\n", $message, Data::Dumper::Dumper($self); +} + +sub nagios { + my $self = shift; + my %params = @_; + my $dead_ndb = 0; + my $dead_api = 0; + if (! $self->{nagios_level}) { + if ($params{mode} =~ /^cluster::ndbdrunning/) { + foreach my $node (grep { $_->{type} eq "NDB"} @{$self->{nodes}}) { + next if $params{selectname} && $params{selectname} ne $_->{id}; + if (! $node->{connected}) { + $self->add_nagios_critical( + sprintf "ndb node %d is not connected", $node->{id}); + $dead_ndb++; + } + } + foreach my $node (grep { $_->{type} eq "API"} @{$self->{nodes}}) { + next if $params{selectname} && $params{selectname} ne $_->{id}; + if (! $node->{connected}) { + $self->add_nagios_critical( + sprintf "api node %d is not connected", $node->{id}); + $dead_api++; + } + } + if (! $dead_ndb) { + $self->add_nagios_ok("all ndb nodes are connected"); + } + if (! $dead_api) { + $self->add_nagios_ok("all api nodes are connected"); + } + } + } + $self->add_perfdata(sprintf "ndbd_nodes=%d ndb_mgmd_nodes=%d mysqld_nodes=%d", + $self->{ndbd_nodes}, $self->{ndb_mgmd_nodes}, $self->{mysqld_nodes}); +} + + +sub init_nagios { + my $self = shift; + no strict 'refs'; + if (! ref($self)) { + my $nagiosvar = $self."::nagios"; + my $nagioslevelvar = $self."::nagios_level"; + $$nagiosvar = { + messages => { + 0 => [], + 1 => [], + 2 => [], + 3 => [], + }, + perfdata => [], + }; + $$nagioslevelvar = $ERRORS{OK}, + } else { + $self->{nagios} = { + messages => { + 0 => [], + 1 => [], + 2 => [], + 3 => [], + }, + perfdata => [], + }; + $self->{nagios_level} = $ERRORS{OK}, + } +} + +sub check_thresholds { + my $self = shift; + my $value = shift; + my $defaultwarningrange = shift; + my $defaultcriticalrange = shift; + my $level = $ERRORS{OK}; + $self->{warningrange} = $self->{warningrange} ? + $self->{warningrange} : $defaultwarningrange; + $self->{criticalrange} = $self->{criticalrange} ? + $self->{criticalrange} : $defaultcriticalrange; + if ($self->{warningrange} !~ /:/ && $self->{criticalrange} !~ /:/) { + # warning = 10, critical = 20, warn if > 10, crit if > 20 + $level = $ERRORS{WARNING} if $value > $self->{warningrange}; + $level = $ERRORS{CRITICAL} if $value > $self->{criticalrange}; + } elsif ($self->{warningrange} =~ /([\d\.]+):/ && + $self->{criticalrange} =~ /([\d\.]+):/) { + # warning = 98:, critical = 95:, warn if < 98, crit if < 95 + $self->{warningrange} =~ /([\d\.]+):/; + $level = $ERRORS{WARNING} if $value < $1; + $self->{criticalrange} =~ /([\d\.]+):/; + $level = $ERRORS{CRITICAL} if $value < $1; + } + return $level; + # + # syntax error must be reported with returncode -1 + # +} + +sub add_nagios { + my $self = shift; + my $level = shift; + my $message = shift; + push(@{$self->{nagios}->{messages}->{$level}}, $message); + # recalc current level + foreach my $llevel (qw(CRITICAL WARNING UNKNOWN OK)) { + if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$llevel}}})) { + $self->{nagios_level} = $ERRORS{$llevel}; + } + } +} + +sub add_nagios_ok { + my $self = shift; + my $message = shift; + $self->add_nagios($ERRORS{OK}, $message); +} + +sub add_nagios_warning { + my $self = shift; + my $message = shift; + $self->add_nagios($ERRORS{WARNING}, $message); +} + +sub add_nagios_critical { + my $self = shift; + my $message = shift; + $self->add_nagios($ERRORS{CRITICAL}, $message); +} + +sub add_nagios_unknown { + my $self = shift; + my $message = shift; + $self->add_nagios($ERRORS{UNKNOWN}, $message); +} + +sub add_perfdata { + my $self = shift; + my $data = shift; + push(@{$self->{nagios}->{perfdata}}, $data); +} + +sub merge_nagios { + my $self = shift; + my $child = shift; + foreach my $level (0..3) { + foreach (@{$child->{nagios}->{messages}->{$level}}) { + $self->add_nagios($level, $_); + } + #push(@{$self->{nagios}->{messages}->{$level}}, + # @{$child->{nagios}->{messages}->{$level}}); + } + push(@{$self->{nagios}->{perfdata}}, @{$child->{nagios}->{perfdata}}); +} + + +sub calculate_result { + my $self = shift; + if ($ENV{NRPE_MULTILINESUPPORT} && + length join(" ", @{$self->{nagios}->{perfdata}}) > 200) { + foreach my $level ("CRITICAL", "WARNING", "UNKNOWN", "OK") { + # first the bad news + if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$level}}})) { + $self->{nagios_message} .= + "\n".join("\n", @{$self->{nagios}->{messages}->{$ERRORS{$level}}}); + } + } + $self->{nagios_message} =~ s/^\n//g; + $self->{perfdata} = join("\n", @{$self->{nagios}->{perfdata}}); + } else { + foreach my $level ("CRITICAL", "WARNING", "UNKNOWN", "OK") { + # first the bad news + if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$level}}})) { + $self->{nagios_message} .= + join(", ", @{$self->{nagios}->{messages}->{$ERRORS{$level}}}).", "; + } + } + $self->{nagios_message} =~ s/, $//g; + $self->{perfdata} = join(" ", @{$self->{nagios}->{perfdata}}); + } + foreach my $level ("OK", "UNKNOWN", "WARNING", "CRITICAL") { + if (scalar(@{$self->{nagios}->{messages}->{$ERRORS{$level}}})) { + $self->{nagios_level} = $ERRORS{$level}; + } + } +} + +sub debug { + my $self = shift; + my $msg = shift; + if ($DBD::MySQL::Cluster::verbose) { + printf "%s %s\n", $msg, ref($self); + } +} + +sub connect { + my $self = shift; + my %params = @_; + my $retval = undef; + $self->{tic} = Time::HiRes::time(); + eval { + use POSIX ':signal_h'; + local $SIG{'ALRM'} = sub { + die "alarm\n"; + }; + my $mask = POSIX::SigSet->new( SIGALRM ); + my $action = POSIX::SigAction->new( + sub { die "connection timeout\n" ; }, $mask); + my $oldaction = POSIX::SigAction->new(); + sigaction(SIGALRM ,$action ,$oldaction ); + alarm($self->{timeout} - 1); # 1 second before the global unknown timeout + my $ndb_mgm = "ndb_mgm"; + $params{hostname} = "127.0.0.1" if ! $params{hostname}; + $ndb_mgm .= sprintf " --ndb-connectstring=%s", $params{hostname} + if $params{hostname}; + $ndb_mgm .= sprintf ":%d", $params{port} + if $params{port}; + $self->{show} = `$ndb_mgm -e show 2>&1`; + if ($? == -1) { + $self->add_nagios_critical("ndb_mgm failed to execute $!"); + } elsif ($? & 127) { + $self->add_nagios_critical("ndb_mgm failed to execute $!"); + } elsif ($? >> 8 != 0) { + $self->add_nagios_critical("ndb_mgm unable to connect"); + } else { + if ($self->{show} !~ /Cluster Configuration/) { + $self->add_nagios_critical("got no cluster configuration"); + } else { + $retval = 1; + } + } + }; + if ($@) { + $self->{errstr} = $@; + $self->{errstr} =~ s/at $0 .*//g; + chomp $self->{errstr}; + $self->add_nagios_critical($self->{errstr}); + $retval = undef; + } + $self->{tac} = Time::HiRes::time(); + return $retval; +} + +sub trace { + my $self = shift; + my $format = shift; + $self->{trace} = -f "/tmp/check_mysql_health.trace" ? 1 : 0; + if ($self->{verbose}) { + printf("%s: ", scalar localtime); + printf($format, @_); + } + if ($self->{trace}) { + my $logfh = new IO::File; + $logfh->autoflush(1); + if ($logfh->open("/tmp/check_mysql_health.trace", "a")) { + $logfh->printf("%s: ", scalar localtime); + $logfh->printf($format, @_); + $logfh->printf("\n"); + $logfh->close(); + } + } +} + +sub DESTROY { + my $self = shift; + my $handle1 = "null"; + my $handle2 = "null"; + if (defined $self->{handle}) { + $handle1 = ref($self->{handle}); + if (defined $self->{handle}->{handle}) { + $handle2 = ref($self->{handle}->{handle}); + } + } + $self->trace(sprintf "DESTROY %s with handle %s %s", ref($self), $handle1, $handle2); + if (ref($self) eq "DBD::MySQL::Cluster") { + } + $self->trace(sprintf "DESTROY %s exit with handle %s %s", ref($self), $handle1, $handle2); + if (ref($self) eq "DBD::MySQL::Cluster") { + #printf "humpftata\n"; + } +} + +sub save_state { + my $self = shift; + my %params = @_; + my $extension = ""; + mkdir $params{statefilesdir} unless -d $params{statefilesdir}; + my $statefile = sprintf "%s/%s_%s", + $params{statefilesdir}, $params{hostname}, $params{mode}; + $extension .= $params{differenciator} ? "_".$params{differenciator} : ""; + $extension .= $params{socket} ? "_".$params{socket} : ""; + $extension .= $params{port} ? "_".$params{port} : ""; + $extension .= $params{database} ? "_".$params{database} : ""; + $extension .= $params{tablespace} ? "_".$params{tablespace} : ""; + $extension .= $params{datafile} ? "_".$params{datafile} : ""; + $extension .= $params{name} ? "_".$params{name} : ""; + $extension =~ s/\//_/g; + $extension =~ s/\(/_/g; + $extension =~ s/\)/_/g; + $extension =~ s/\*/_/g; + $extension =~ s/\s/_/g; + $statefile .= $extension; + $statefile = lc $statefile; + open(STATE, ">$statefile"); + if ((ref($params{save}) eq "HASH") && exists $params{save}->{timestamp}) { + $params{save}->{localtime} = scalar localtime $params{save}->{timestamp}; + } + printf STATE Data::Dumper::Dumper($params{save}); + close STATE; + $self->debug(sprintf "saved %s to %s", + Data::Dumper::Dumper($params{save}), $statefile); +} + +sub load_state { + my $self = shift; + my %params = @_; + my $extension = ""; + my $statefile = sprintf "%s/%s_%s", + $params{statefilesdir}, $params{hostname}, $params{mode}; + $extension .= $params{differenciator} ? "_".$params{differenciator} : ""; + $extension .= $params{socket} ? "_".$params{socket} : ""; + $extension .= $params{port} ? "_".$params{port} : ""; + $extension .= $params{database} ? "_".$params{database} : ""; + $extension .= $params{tablespace} ? "_".$params{tablespace} : ""; + $extension .= $params{datafile} ? "_".$params{datafile} : ""; + $extension .= $params{name} ? "_".$params{name} : ""; + $extension =~ s/\//_/g; + $extension =~ s/\(/_/g; + $extension =~ s/\)/_/g; + $extension =~ s/\*/_/g; + $extension =~ s/\s/_/g; + $statefile .= $extension; + $statefile = lc $statefile; + if ( -f $statefile) { + our $VAR1; + eval { + require $statefile; + }; + if($@) { +printf "rumms\n"; + } + $self->debug(sprintf "load %s", Data::Dumper::Dumper($VAR1)); + return $VAR1; + } else { + return undef; + } +} + +sub valdiff { + my $self = shift; + my $pparams = shift; + my %params = %{$pparams}; + my @keys = @_; + my $last_values = $self->load_state(%params) || eval { + my $empty_events = {}; + foreach (@keys) { + $empty_events->{$_} = 0; + } + $empty_events->{timestamp} = 0; + $empty_events; + }; + foreach (@keys) { + $self->{'delta_'.$_} = $self->{$_} - $last_values->{$_}; + $self->debug(sprintf "delta_%s %f", $_, $self->{'delta_'.$_}); + } + $self->{'delta_timestamp'} = time - $last_values->{timestamp}; + $params{save} = eval { + my $empty_events = {}; + foreach (@keys) { + $empty_events->{$_} = $self->{$_}; + } + $empty_events->{timestamp} = time; + $empty_events; + }; + $self->save_state(%params); +} + +sub requires_version { + my $self = shift; + my $version = shift; + my @instances = DBD::MySQL::Cluster::return_clusters(); + my $instversion = $instances[0]->{version}; + if (! $self->version_is_minimum($version)) { + $self->add_nagios($ERRORS{UNKNOWN}, + sprintf "not implemented/possible for MySQL release %s", $instversion); + } +} + +sub version_is_minimum { + # the current version is newer or equal + my $self = shift; + my $version = shift; + my $newer = 1; + my @instances = DBD::MySQL::Cluster::return_clusters(); + my @v1 = map { $_ eq "x" ? 0 : $_ } split(/\./, $version); + my @v2 = split(/\./, $instances[0]->{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; + } + } + #printf STDERR "check if %s os minimum %s\n", join(".", @v2), join(".", @v1); + return $newer; +} + +sub instance_rac { + my $self = shift; + my @instances = DBD::MySQL::Cluster::return_clusters(); + return (lc $instances[0]->{parallel} eq "yes") ? 1 : 0; +} + +sub instance_thread { + my $self = shift; + my @instances = DBD::MySQL::Cluster::return_clusters(); + return $instances[0]->{thread}; +} + +sub windows_cluster { + my $self = shift; + my @instances = DBD::MySQL::Cluster::return_clusters(); + if ($instances[0]->{os} =~ /Win/i) { + return 1; + } else { + return 0; + } +} + +sub system_vartmpdir { + my $self = shift; + if ($^O =~ /MSWin/) { + return $self->system_tmpdir(); + } else { + return "/var/tmp/check_mysql_health"; + } +} + +sub system_oldvartmpdir { + my $self = shift; + return "/tmp"; +} + +sub system_tmpdir { + my $self = shift; + 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"; + } +} + + +package DBD::MySQL::Cluster::Node; + +use strict; + +our @ISA = qw(DBD::MySQL::Cluster); + + +sub new { + my $class = shift; + my %params = @_; + my $self = { + mode => $params{mode}, + timeout => $params{timeout}, + type => $params{type}, + id => $params{id}, + status => $params{status}, + }; + bless $self, $class; + $self->init(%params); + if ($params{type} eq "NDB") { + bless $self, "DBD::MySQL::Cluster::Node::NDB"; + $self->init(%params); + } + return $self; +} + +sub init { + my $self = shift; + my %params = @_; + if ($self->{status} =~ /@(\d+\.\d+\.\d+\.\d+)\s/) { + $self->{addr} = $1; + $self->{connected} = 1; + } elsif ($self->{status} =~ /accepting connect from (\d+\.\d+\.\d+\.\d+)/) { + $self->{addr} = $1; + $self->{connected} = 0; + } + if ($self->{status} =~ /starting,/) { + $self->{status} = "starting"; + } elsif ($self->{status} =~ /shutting,/) { + $self->{status} = "shutting"; + } else { + $self->{status} = $self->{connected} ? "running" : "dead"; + } +} + + +package DBD::MySQL::Cluster::Node::NDB; + +use strict; + +our @ISA = qw(DBD::MySQL::Cluster::Node); + + +sub init { + my $self = shift; + my %params = @_; + if ($self->{status} =~ /Nodegroup:\s*(\d+)/) { + $self->{nodegroup} = $1; + } + $self->{master} = ($self->{status} =~ /Master\)/) ? 1 : 0; +} + + +package Extraopts; + +use strict; +use File::Basename; +use Data::Dumper; + +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; + my %commandline = (); + if (scalar(keys %{$self->{config}->{default_no_section}}) > 0) { + foreach (keys %{$self->{config}->{default_no_section}}) { + $commandline{$_} = $self->{config}->{default_no_section}->{$_}; + } + } + if (exists $self->{config}->{$self->{section}}) { + foreach (keys %{$self->{config}->{$self->{section}}}) { + $commandline{$_} = $self->{config}->{$self->{section}}->{$_}; + } + } + foreach (keys %commandline) { + if (! exists $self->{commandline}->{$_}) { + $self->{commandline}->{$_} = $commandline{$_}; + } + } +} + + + +package main; + +use strict; +use Getopt::Long qw(:config no_ignore_case); +use File::Basename; +use lib dirname($0); + + + +use vars qw ($PROGNAME $REVISION $CONTACT $TIMEOUT $STATEFILESDIR $needs_restart %commandline); + +$PROGNAME = "check_mysql_health"; +$REVISION = '$Revision: 2.2.2 $'; +$CONTACT = 'gerhard.lausser@consol.de'; +$TIMEOUT = 60; +$STATEFILESDIR = '/var/tmp/check_mysql_health'; +$needs_restart = 0; + +my @modes = ( + ['server::connectiontime', + 'connection-time', undef, + 'Time to connect to the server' ], + ['server::uptime', + 'uptime', undef, + 'Time the server is running' ], + ['server::instance::connectedthreads', + 'threads-connected', undef, + 'Number of currently open connections' ], + ['server::instance::threadcachehitrate', + 'threadcache-hitrate', undef, + 'Hit rate of the thread-cache' ], + ['server::instance::createdthreads', + 'threads-created', undef, + 'Number of threads created per sec' ], + ['server::instance::runningthreads', + 'threads-running', undef, + 'Number of currently running threads' ], + ['server::instance::cachedthreads', + 'threads-cached', undef, + 'Number of currently cached threads' ], + ['server::instance::abortedconnects', + 'connects-aborted', undef, + 'Number of aborted connections per sec' ], + ['server::instance::abortedclients', + 'clients-aborted', undef, + 'Number of aborted connections (because the client died) per sec' ], + ['server::instance::replication::slavelag', + 'slave-lag', ['replication-slave-lag'], + 'Seconds behind master' ], + ['server::instance::replication::slaveiorunning', + 'slave-io-running', ['replication-slave-io-running'], + 'Slave io running: Yes' ], + ['server::instance::replication::slavesqlrunning', + 'slave-sql-running', ['replication-slave-sql-running'], + 'Slave sql running: Yes' ], + ['server::instance::querycachehitrate', + 'qcache-hitrate', ['querycache-hitrate'], + 'Query cache hitrate' ], + ['server::instance::querycachelowmemprunes', + 'qcache-lowmem-prunes', ['querycache-lowmem-prunes'], + 'Query cache entries pruned because of low memory' ], + ['server::instance::myisam::keycache::hitrate', + 'keycache-hitrate', ['myisam-keycache-hitrate'], + 'MyISAM key cache hitrate' ], + ['server::instance::innodb::bufferpool::hitrate', + 'bufferpool-hitrate', ['innodb-bufferpool-hitrate'], + 'InnoDB buffer pool hitrate' ], + ['server::instance::innodb::bufferpool::waitfree', + 'bufferpool-wait-free', ['innodb-bufferpool-wait-free'], + 'InnoDB buffer pool waits for clean page available' ], + ['server::instance::innodb::logwaits', + 'log-waits', ['innodb-log-waits'], + 'InnoDB log waits because of a too small log buffer' ], + ['server::instance::tablecachehitrate', + 'tablecache-hitrate', undef, + 'Table cache hitrate' ], + ['server::instance::tablelockcontention', + 'table-lock-contention', undef, + 'Table lock contention' ], + ['server::instance::tableindexusage', + 'index-usage', undef, + 'Usage of indices' ], + ['server::instance::tabletmpondisk', + 'tmp-disk-tables', undef, + 'Percent of temp tables created on disk' ], + ['server::instance::needoptimize', + 'table-fragmentation', undef, + 'Show tables which should be optimized' ], + ['server::instance::openfiles', + 'open-files', undef, + 'Percent of opened files' ], + ['server::instance::slowqueries', + 'slow-queries', undef, + 'Slow queries' ], + ['server::instance::longprocs', + 'long-running-procs', undef, + 'long running processes' ], + ['cluster::ndbdrunning', + 'cluster-ndbd-running', undef, + 'ndnd nodes are up and running' ], + ['server::sql', + 'sql', undef, + 'any sql command returning a single number' ], +); + +# rrd data store names are limited to 19 characters +my %labels = ( + bufferpool_hitrate => { + groundwork => 'bp_hitrate', + }, + bufferpool_hitrate_now => { + groundwork => 'bp_hitrate_now', + }, + bufferpool_free_waits_rate => { + groundwork => 'bp_freewaits', + }, + innodb_log_waits_rate => { + groundwork => 'inno_log_waits', + }, + keycache_hitrate => { + groundwork => 'kc_hitrate', + }, + keycache_hitrate_now => { + groundwork => 'kc_hitrate_now', + }, + threads_created_per_sec => { + groundwork => 'thrds_creat_per_s', + }, + connects_aborted_per_sec => { + groundwork => 'conn_abrt_per_s', + }, + clients_aborted_per_sec => { + groundwork => 'clnt_abrt_per_s', + }, + thread_cache_hitrate => { + groundwork => 'tc_hitrate', + }, + thread_cache_hitrate_now => { + groundwork => 'tc_hitrate_now', + }, + qcache_lowmem_prunes_rate => { + groundwork => 'qc_lowm_prnsrate', + }, + slow_queries_rate => { + groundwork => 'slow_q_rate', + }, + tablecache_hitrate => { + groundwork => 'tac_hitrate', + }, + tablecache_fillrate => { + groundwork => 'tac_fillrate', + }, + tablelock_contention => { + groundwork => 'tl_contention', + }, + tablelock_contention_now => { + groundwork => 'tl_contention_now', + }, + pct_tmp_table_on_disk => { + groundwork => 'tmptab_on_disk', + }, + pct_tmp_table_on_disk_now => { + groundwork => 'tmptab_on_disk_now', + }, +); + +sub print_usage () { + print <] [[--hostname ] + [--port | --socket ] + --username --password ] --mode + [--method mysql] + $PROGNAME [-h | --help] + $PROGNAME [-V | --version] + + Options: + --hostname + the database server's hostname + --port + the database's port. (default: 3306) + --socket + the database's unix socket. + --username + the mysql db user + --password + the mysql db user's password + --database + the database's name. (default: information_schema) + --replication-user + the database's replication user name (default: replication) + --warning + the warning range + --critical + the critical range + --mode + the mode of the plugin. select one of the following keywords: +EOUS + 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) { + printf $format, $_->[1], $_->[3]; + } + printf "\n"; + print <new(file => $commandline{'extra-opts'}, commandline => + \%commandline); + if (! $extras->is_valid()) { + printf "extra-opts are not valid: %s\n", $extras->{errors}; + exit $ERRORS{UNKNOWN}; + } else { + $extras->overwrite(); + } +} + +if (exists $commandline{version}) { + print_revision($PROGNAME, $REVISION); + exit $ERRORS{OK}; +} + +if (exists $commandline{help}) { + print_help(); + exit $ERRORS{OK}; +} elsif (! exists $commandline{mode}) { + printf "Please select a mode\n"; + print_help(); + exit $ERRORS{OK}; +} + +if ($commandline{mode} eq "encode") { + my $input = <>; + chomp $input; + $input =~ s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; + printf "%s\n", $input; + exit $ERRORS{OK}; +} + +if (exists $commandline{3}) { + $ENV{NRPE_MULTILINESUPPORT} = 1; +} + +if (exists $commandline{timeout}) { + $TIMEOUT = $commandline{timeout}; +} + +if (exists $commandline{verbose}) { + $DBD::MySQL::Server::verbose = exists $commandline{verbose}; +} + +if (exists $commandline{scream}) { +# $DBD::MySQL::Server::hysterical = exists $commandline{scream}; +} + +if (exists $commandline{method}) { + # snmp or mysql cmdline +} else { + $commandline{method} = "dbi"; +} + +if (exists $commandline{report}) { + # short, long, html +} else { + $commandline{report} = "long"; +} + +if (exists $commandline{labelformat}) { + # groundwork +} else { + $commandline{labelformat} = "pnp4nagios"; +} + +if (exists $commandline{'with-mymodules-dyn-dir'}) { + $DBD::MySQL::Server::my_modules_dyn_dir = $commandline{'with-mymodules-dyn-dir'}; +} else { + $DBD::MySQL::Server::my_modules_dyn_dir = '/usr/local/var/libexec/alignak'; +} + +if (exists $commandline{environment}) { + # if the desired environment variable values are different from + # the environment of this running script, then a restart is necessary. + # because setting $ENV does _not_ change the environment of the running script. + foreach (keys %{$commandline{environment}}) { + if ((! $ENV{$_}) || ($ENV{$_} ne $commandline{environment}->{$_})) { + $needs_restart = 1; + $ENV{$_} = $commandline{environment}->{$_}; + printf STDERR "new %s=%s forces restart\n", $_, $ENV{$_} + if $DBD::MySQL::Server::verbose; + } + } + # e.g. called with --runas dbnagio. shlib_path environment variable is stripped + # during the sudo. + # so the perl interpreter starts without a shlib_path. but --runas cares for + # a --environment shlib_path=... + # so setting the environment variable in the code above and restarting the + # perl interpreter will help it find shared libs +} + +if (exists $commandline{runas}) { + # remove the runas parameter + # exec sudo $0 ... the remaining parameters + $needs_restart = 1; + # if the calling script has a path for shared libs and there is no --environment + # parameter then the called script surely needs the variable too. + foreach my $important_env (qw(LD_LIBRARY_PATH SHLIB_PATH + ORACLE_HOME TNS_ADMIN ORA_NLS ORA_NLS33 ORA_NLS10)) { + if ($ENV{$important_env} && ! scalar(grep { /^$important_env=/ } + keys %{$commandline{environment}})) { + $commandline{environment}->{$important_env} = $ENV{$important_env}; + printf STDERR "add important --environment %s=%s\n", + $important_env, $ENV{$important_env} if $DBD::MySQL::Server::verbose; + } + } +} + +if ($needs_restart) { + my @newargv = (); + my $runas = undef; + if (exists $commandline{runas}) { + $runas = $commandline{runas}; + delete $commandline{runas}; + } + foreach my $option (keys %commandline) { + if (grep { /^$option/ && /=/ } @params) { + if (ref ($commandline{$option}) eq "HASH") { + foreach (keys %{$commandline{$option}}) { + push(@newargv, sprintf "--%s", $option); + push(@newargv, sprintf "%s=%s", $_, $commandline{$option}->{$_}); + } + } else { + push(@newargv, sprintf "--%s", $option); + push(@newargv, sprintf "%s", $commandline{$option}); + } + } else { + push(@newargv, sprintf "--%s", $option); + } + } + if ($runas) { + exec "sudo", "-S", "-u", $runas, $0, @newargv; + } else { + exec $0, @newargv; + # this makes sure that even a SHLIB or LD_LIBRARY_PATH are set correctly + # when the perl interpreter starts. Setting them during runtime does not + # help loading e.g. libclntsh.so + } + exit; +} + +if (exists $commandline{shell}) { + # forget what you see here. + system("/bin/sh"); +} + +if (! exists $commandline{statefilesdir}) { + if (exists $ENV{OMD_ROOT}) { + $commandline{statefilesdir} = $ENV{OMD_ROOT}."/var/tmp/check_mysql_health"; + } else { + $commandline{statefilesdir} = $STATEFILESDIR; + } +} + +if (exists $commandline{name}) { + if ($^O =~ /MSWin/ && $commandline{name} =~ /^'(.*)'$/) { + # putting arguments in single ticks under Windows CMD leaves the ' intact + # we remove them + $commandline{name} = $1; + } + # objects can be encoded like an url + # with s/([^A-Za-z0-9])/sprintf("%%%02X", ord($1))/seg; + if (($commandline{mode} ne "sql") || + (($commandline{mode} eq "sql") && + ($commandline{name} =~ /select%20/i))) { # protect ... like '%cac%' ... from decoding + $commandline{name} =~ s/\%([A-Fa-f0-9]{2})/pack('C', hex($1))/seg; + } + if ($commandline{name} =~ /^0$/) { + # without this, $params{selectname} would be treated like undef + $commandline{name} = "00"; + } +} + +$SIG{'ALRM'} = sub { + printf "UNKNOWN - %s timed out after %d seconds\n", $PROGNAME, $TIMEOUT; + exit $ERRORS{UNKNOWN}; +}; +alarm($TIMEOUT); + +my $nagios_level = $ERRORS{UNKNOWN}; +my $nagios_message = ""; +my $perfdata = ""; +if ($commandline{mode} =~ /^my-([^\-.]+)/) { + my $param = $commandline{mode}; + $param =~ s/\-/::/g; + push(@modes, [$param, $commandline{mode}, undef, 'my extension']); +} elsif ((! grep { $commandline{mode} eq $_ } map { $_->[1] } @modes) && + (! grep { $commandline{mode} eq $_ } map { defined $_->[2] ? @{$_->[2]} : () } @modes)) { + printf "UNKNOWN - mode %s\n", $commandline{mode}; + print_usage(); + exit 3; +} +my %params = ( + timeout => $TIMEOUT, + mode => ( + map { $_->[0] } + grep { + ($commandline{mode} eq $_->[1]) || + ( defined $_->[2] && grep { $commandline{mode} eq $_ } @{$_->[2]}) + } @modes + )[0], + cmdlinemode => $commandline{mode}, + method => $commandline{method} || + $ENV{NAGIOS__SERVICEMYSQL_METH} || + $ENV{NAGIOS__HOSTMYSQL_METH} || 'dbi', + hostname => $commandline{hostname} || + $ENV{NAGIOS__SERVICEMYSQL_HOST} || + $ENV{NAGIOS__HOSTMYSQL_HOST} || 'localhost', + database => $commandline{database} || + $ENV{NAGIOS__SERVICEMYSQL_DATABASE} || + $ENV{NAGIOS__HOSTMYSQL_DATABASE} || 'information_schema', + port => $commandline{port} || (($commandline{mode} =~ /^cluster/) ? + ($ENV{NAGIOS__SERVICENDBMGM_PORT} || $ENV{NAGIOS__HOSTNDBMGM_PORT} || 1186) : + ($ENV{NAGIOS__SERVICEMYSQL_PORT} || $ENV{NAGIOS__HOSTMYSQL_PORT} || 3306)), + socket => $commandline{socket} || + $ENV{NAGIOS__SERVICEMYSQL_SOCKET} || + $ENV{NAGIOS__HOSTMYSQL_SOCKET}, + username => $commandline{username} || + $ENV{NAGIOS__SERVICEMYSQL_USER} || + $ENV{NAGIOS__HOSTMYSQL_USER}, + password => $commandline{password} || + $ENV{NAGIOS__SERVICEMYSQL_PASS} || + $ENV{NAGIOS__HOSTMYSQL_PASS}, + replication_user => $commandline{'replication-user'} || 'replication', + mycnf => $commandline{mycnf} || + $ENV{NAGIOS__SERVICEMYSQL_MYCNF} || + $ENV{NAGIOS__HOSTMYSQL_MYCNF}, + mycnfgroup => $commandline{mycnfgroup} || + $ENV{NAGIOS__SERVICEMYSQL_MYCNFGROUP} || + $ENV{NAGIOS__HOSTMYSQL_MYCNFGROUP}, + warningrange => $commandline{warning}, + criticalrange => $commandline{critical}, + dbthresholds => $commandline{dbthresholds}, + absolute => $commandline{absolute}, + lookback => $commandline{lookback}, + selectname => $commandline{name} || $commandline{tablespace} || $commandline{datafile}, + regexp => $commandline{regexp}, + name => $commandline{name}, + name2 => $commandline{name2} || $commandline{name}, + units => $commandline{units}, + lookback => $commandline{lookback} || 0, + eyecandy => $commandline{eyecandy}, + statefilesdir => $commandline{statefilesdir}, + verbose => $commandline{verbose}, + report => $commandline{report}, + labelformat => $commandline{labelformat}, + negate => $commandline{negate}, +); + +my $server = undef; +my $cluster = undef; + +if ($params{mode} =~ /^(server|my)/) { + $server = DBD::MySQL::Server->new(%params); + $server->nagios(%params); + $server->calculate_result(\%labels); + $nagios_message = $server->{nagios_message}; + $nagios_level = $server->{nagios_level}; + $perfdata = $server->{perfdata}; +} elsif ($params{mode} =~ /^cluster/) { + $cluster = DBD::MySQL::Cluster->new(%params); + $cluster->nagios(%params); + $cluster->calculate_result(\%labels); + $nagios_message = $cluster->{nagios_message}; + $nagios_level = $cluster->{nagios_level}; + $perfdata = $cluster->{perfdata}; +} + +printf "%s - %s", $ERRORCODES{$nagios_level}, $nagios_message; +printf " | %s", $perfdata if $perfdata; +printf "\n"; +exit $nagios_level; + + +__END__ + + diff --git a/test/test-configurations/alignak/commands/check_nginx_status.pl b/test/test-configurations/alignak/commands/check_nginx_status.pl new file mode 100755 index 00000000..02b1c342 --- /dev/null +++ b/test/test-configurations/alignak/commands/check_nginx_status.pl @@ -0,0 +1,474 @@ +#!/usr/bin/env perl +# check_nginx_status.pl +# Author : regis.leroy at makina-corpus.com +# Licence : GPL - http://www.fsf.org/licenses/gpl.txt +# +# help : ./check_nginx_status.pl -h +# +# issues & updates: http://github.com/regilero/check_nginx_status +use warnings; +use strict; +use Getopt::Long; +use LWP::UserAgent; +use Time::HiRes qw(gettimeofday tv_interval); +use Digest::MD5 qw(md5 md5_hex); +use FindBin; + +# ensure all outputs are in UTF-8 +binmode(STDOUT, ":utf8"); + +# Nagios specific +use lib $FindBin::Bin; +use utils qw($TIMEOUT); + +# Globals +my $Version='0.20'; +my $Name=$0; + +my $o_host = undef; # hostname +my $o_help= undef; # want some help ? +my $o_port= undef; # port +my $o_url = undef; # url to use, if not the default +my $o_user= undef; # user for auth +my $o_pass= ''; # password for auth +my $o_realm= ''; # password for auth +my $o_version= undef; # print version +my $o_warn_a_level= -1; # Number of active connections that will cause a warning +my $o_crit_a_level= -1; # Number of active connections that will cause an error +my $o_warn_rps_level= -1; # Number of Request per second that will cause a warning +my $o_crit_rps_level= -1; # Number of request Per second that will cause an error +my $o_warn_cps_level= -1; # Number of Connections per second that will cause a warning +my $o_crit_cps_level= -1; # Number of Connections per second that will cause an error +my $o_timeout= 15; # Default 15s Timeout +my $o_warn_thresold= undef; # warning thresolds entry +my $o_crit_thresold= undef; # critical thresolds entry +my $o_debug= undef; # debug mode +my $o_servername= undef; # ServerName (host header in http request) +my $o_https= undef; # SSL (HTTPS) mode +my $o_disable_sslverifyhostname = 0; + +my $TempPath = '/tmp/'; # temp path +my $MaxTimeDif = 60*30; # Maximum uptime difference (seconds), default 30 minutes + +my $nginx = 'NGINX'; # Could be used to store version also + +# functions +sub show_versioninfo { print "$Name version : $Version\n"; } + +sub print_usage { + print "Usage: $Name -H [-p ] [-s servername] [-t ] [-w -c ] [-V] [-d] [-u ] [-U user -P pass -r realm]\n"; +} +sub nagios_exit { + my ( $nickname, $status, $message, $perfdata , $silent) = @_; + my %STATUSCODE = ( + 'OK' => 0 + , 'WARNING' => 1 + , 'CRITICAL' => 2 + , 'UNKNOWN' => 3 + , 'PENDING' => 4 + ); + if(!defined($silent)) { + my $output = undef; + $output .= sprintf('%1$s %2$s - %3$s', $nickname, $status, $message); + if ($perfdata) { + $output .= sprintf('|%1$s', $perfdata); + } + $output .= chr(10); + print $output; + } + exit $STATUSCODE{$status}; +} + +# Get the alarm signal +$SIG{'ALRM'} = sub { + nagios_exit($nginx,"CRITICAL","ERROR: Alarm signal (Nagios timeout)"); +}; + +sub help { + print "Nginx Monitor for Nagios version ",$Version,"\n"; + print "GPL licence, (c)2012 Leroy Regis\n\n"; + print_usage(); + print </nginx_status" +-s, --servername=SERVERNAME + ServerName, (host header of HTTP request) use it if you specified an IP in -H to match the good Virtualhost in your target +-S, --ssl + Wether we should use HTTPS instead of HTTP +--disable-sslverifyhostname + Disable SSL hostname verification +-U, --user=user + Username for basic auth +-P, --pass=PASS + Password for basic auth +-r, --realm=REALM + Realm for basic auth +-d, --debug + Debug mode (show http request response) +-m, --maxreach=MAX + Number of max processes reached (since last check) that should trigger an alert +-t, --timeout=INTEGER + timeout in seconds (Default: $o_timeout) +-w, --warn=ACTIVE_CONN,REQ_PER_SEC,CONN_PER_SEC + number of active connections, ReqPerSec or ConnPerSec that will cause a WARNING + -1 for no warning +-c, --critical=ACTIVE_CONN,REQ_PER_SEC,CONN_PER_SEC + number of active connections, ReqPerSec or ConnPerSec that will cause a CRITICAL + -1 for no CRITICAL +-V, --version + prints version number + +Note : + 3 items can be managed on this check, this is why -w and -c parameters are using 3 values thresolds + - ACTIVE_CONN: Number of all opened connections, including connections to backends + - REQ_PER_SEC: Average number of request per second between this check and the previous one + - CONN_PER_SEC: Average number of connections per second between this check and the previous one + +Examples: + + This one will generate WARNING and CRITICIAL alerts if you reach 10 000 or 20 000 active connection; or + 100 or 200 request per second; or 200 or 300 connections per second +check_nginx_status.pl -H 10.0.0.10 -u /foo/nginx_status -s mydomain.example.com -t 8 -w 10000,100,200 -c 20000,200,300 + + this will generate WARNING and CRITICAL alerts only on the number of active connections (with low numbers for nginx) +check_nginx_status.pl -H 10.0.0.10 -s mydomain.example.com -t 8 -w 10,-1,-1 -c 20,-1,-1 + + theses two equivalents will not generate any alert (if the nginx_status page is reachable) but could be used for graphics +check_nginx_status.pl -H 10.0.0.10 -s mydomain.example.com -w -1,-1,-1 -c -1,-1,-1 +check_nginx_status.pl -H 10.0.0.10 -s mydomain.example.com + +EOT +} + +sub check_options { + Getopt::Long::Configure ("bundling"); + GetOptions( + 'h' => \$o_help, 'help' => \$o_help, + 'd' => \$o_debug, 'debug' => \$o_debug, + 'H:s' => \$o_host, 'hostname:s' => \$o_host, + 's:s' => \$o_servername, 'servername:s' => \$o_servername, + 'S:s' => \$o_https, 'ssl:s' => \$o_https, + 'u:s' => \$o_url, 'url:s' => \$o_url, + 'U:s' => \$o_user, 'user:s' => \$o_user, + 'P:s' => \$o_pass, 'pass:s' => \$o_pass, + 'r:s' => \$o_realm, 'realm:s' => \$o_realm, + 'p:i' => \$o_port, 'port:i' => \$o_port, + 'V' => \$o_version, 'version' => \$o_version, + 'w:s' => \$o_warn_thresold,'warn:s' => \$o_warn_thresold, + 'c:s' => \$o_crit_thresold,'critical:s' => \$o_crit_thresold, + 't:i' => \$o_timeout, 'timeout:i' => \$o_timeout, + 'disable-sslverifyhostname' => \$o_disable_sslverifyhostname, + ); + + if (defined ($o_help)) { + help(); + nagios_exit($nginx,"UNKNOWN","leaving","",1); + } + if (defined($o_version)) { + show_versioninfo(); + nagios_exit($nginx,"UNKNOWN","leaving","",1); + }; + + if (defined($o_warn_thresold)) { + ($o_warn_a_level,$o_warn_rps_level,$o_warn_cps_level) = split(',', $o_warn_thresold); + } + if (defined($o_crit_thresold)) { + ($o_crit_a_level,$o_crit_rps_level,$o_crit_cps_level) = split(',', $o_crit_thresold); + } + if (defined($o_debug)) { + print("\nDebug thresolds: \nWarning: ($o_warn_thresold) => Active: $o_warn_a_level ReqPerSec :$o_warn_rps_level ConnPerSec: $o_warn_cps_level"); + print("\nCritical ($o_crit_thresold) => : Active: $o_crit_a_level ReqPerSec: $o_crit_rps_level ConnPerSec : $o_crit_cps_level\n"); + } + if ((defined($o_warn_a_level) && defined($o_crit_a_level)) && + (($o_warn_a_level != -1) && ($o_crit_a_level != -1) && ($o_warn_a_level >= $o_crit_a_level)) ) { + nagios_exit($nginx,"UNKNOWN","Check warning and critical values for Active Process (1st part of thresold), warning level must be < crit level!"); + } + if ((defined($o_warn_rps_level) && defined($o_crit_rps_level)) && + (($o_warn_rps_level != -1) && ($o_crit_rps_level != -1) && ($o_warn_rps_level >= $o_crit_rps_level)) ) { + nagios_exit($nginx,"UNKNOWN","Check warning and critical values for ReqPerSec (2nd part of thresold), warning level must be < crit level!"); + } + if ((defined($o_warn_cps_level) && defined($o_crit_cps_level)) && + (($o_warn_cps_level != -1) && ($o_crit_cps_level != -1) && ($o_warn_cps_level >= $o_crit_cps_level)) ) { + nagios_exit($nginx,"UNKNOWN","Check warning and critical values for ConnPerSec (3rd part of thresold), warning level must be < crit level!"); + } + # Check compulsory attributes + if (!defined($o_host)) { + print_usage(); + nagios_exit($nginx,"UNKNOWN","-H host argument required"); + } +} + +########## MAIN ########## + +check_options(); + +my $override_ip = $o_host; +my $ua = LWP::UserAgent->new( + protocols_allowed => ['http', 'https'], + timeout => $o_timeout +); + +if ( $o_disable_sslverifyhostname ) { + $ua->ssl_opts( 'verify_hostname' => 0 ); +} + +# we need to enforce the HTTP request is made on the Nagios Host IP and +# not on the DNS related IP for that domain +@LWP::Protocol::http::EXTRA_SOCK_OPTS = ( PeerAddr => $override_ip ); +# this prevent used only once warning in -w mode +my $ua_settings = @LWP::Protocol::http::EXTRA_SOCK_OPTS; + +my $timing0 = [gettimeofday]; +my $response = undef; +my $url = undef; + +if (!defined($o_url)) { + $o_url='/nginx_status'; +} else { + # ensure we have a '/' as first char + $o_url = '/'.$o_url unless $o_url =~ m(^/) +} +my $proto='http://'; +if(defined($o_https)) { + $proto='https://'; + if (defined($o_port) && $o_port!=443) { + if (defined ($o_debug)) { + print "\nDEBUG: Notice: port is defined at $o_port and not 443, check you really want that in SSL mode! \n"; + } + } +} +if (defined($o_servername)) { + if (!defined($o_port)) { + $url = $proto . $o_servername . $o_url; + } else { + $url = $proto . $o_servername . ':' . $o_port . $o_url; + } +} else { + if (!defined($o_port)) { + $url = $proto . $o_host . $o_url; + } else { + $url = $proto . $o_host . ':' . $o_port . $o_url; + } +} +if (defined ($o_debug)) { + print "\nDEBUG: HTTP url: \n"; + print $url; +} + +my $req = HTTP::Request->new( GET => $url ); + +if (defined($o_servername)) { + $req->header('Host' => $o_servername); +} +if (defined($o_user)) { + $req->authorization_basic($o_user, $o_pass); +} + +if (defined ($o_debug)) { + print "\nDEBUG: HTTP request: \n"; + print "IP used (better if it's an IP):" . $override_ip . "\n"; + print $req->as_string; +} +$response = $ua->request($req); +my $timeelapsed = tv_interval ($timing0, [gettimeofday]); + +my $InfoData = ''; +my $PerfData = ''; +#my @Time = (localtime); # list context and not scalar as we want the brutal timestamp +my $Time = time; + +my $webcontent = undef; +if ($response->is_success) { + $webcontent=$response->decoded_content; + if (defined ($o_debug)) { + print "\nDEBUG: HTTP response:"; + print $response->status_line; + print "\n".$response->header('Content-Type'); + print "\n"; + print $webcontent; + } + if ($response->header('Content-Type') =~ m/text\/html/) { + nagios_exit($nginx,"CRITICAL", "We have a response page for our request, but it's an HTML page, quite certainly not the status report of nginx"); + } + # example of response content expected: + #Active connections: 10 + #server accepts handled requests + #38500 38500 50690 + #Reading: 5 Writing: 5 Waiting: 0 + + # number of all open connections including connections to backends + my $ActiveConn = 0; + if($webcontent =~ m/Active connections: (.*?)\n/) { + $ActiveConn = $1; + # triming + $ActiveConn =~ s/^\s+|\s+$//g; + } + + + # 3 counters with a space: accepted conn, handled conn and number of requests + my $counters = ''; + my $AcceptedConn = 0; + my $HandledConn = 0; + my $NbRequests = 0; + if($webcontent =~ m/\nserver accepts handled requests\n(.*?)\n/) { + $counters = $1; + # triming + $counters =~ s/^\s+|\s+$//g; + #splitting + ($AcceptedConn,$HandledConn,$NbRequests) = split(' ', $counters); + # triming + $AcceptedConn =~ s/^\s+|\s+$//g; + $HandledConn =~ s/^\s+|\s+$//g; + $NbRequests =~ s/^\s+|\s+$//g; + } + + # nginx reads request header + my $Reading = 0; + # nginx reads request body, processes request, or writes response to a client + my $Writing = 0; + # keep-alive connections, actually it is active - (reading + writing) + my $Waiting = 0; + if($webcontent =~ m/Reading: (.*?)Writing: (.*?)Waiting: (.*?)$/) { + $Reading = $1; + $Writing = $2; + $Waiting = $3; + # triming + $Reading =~ s/^\s+|\s+$//g; + $Writing =~ s/^\s+|\s+$//g; + $Waiting =~ s/^\s+|\s+$//g; + } + + # Debug + if (defined ($o_debug)) { + print ("\nDEBUG Parse results => Active :" . $ActiveConn . "\nAcceptedConn :" . $AcceptedConn . "\nHandledConn :" . $HandledConn . "\nNbRequests :".$NbRequests . "\nReading :" .$Reading . "\nWriting :" . $Writing . "\nWaiting :" . $Waiting . "\n"); + } + + my $TempFile = $TempPath.$o_host.'_check_nginx_status'.md5_hex($url); + my $FH; + + my $LastTime = 0; + my $LastAcceptedConn = 0; + my $LastHandledConn = 0; + my $LastNbRequests = 0; + if ((-e $TempFile) && (-r $TempFile) && (-w $TempFile)) { + open ($FH, '<',$TempFile) or nagios_exit($nginx,"UNKNOWN","unable to read temporary data from :".$TempFile); + $LastTime = <$FH>; + $LastAcceptedConn = <$FH>; + $LastHandledConn = <$FH>; + $LastNbRequests = <$FH>; + close ($FH); + if (defined ($o_debug)) { + print ("\nDebug: data from temporary file: $TempFile\n"); + print (" LastTime: $LastTime LastAcceptedConn: $LastAcceptedConn LastHandledConn: $LastHandledConn LastNbRequests: $LastNbRequests \n"); + } + } + + open ($FH, '>'.$TempFile) or nagios_exit($nginx,"UNKNOWN","unable to write temporary data in :".$TempFile); + #print $FH (@Time),"\n"; + print $FH "$Time\n"; + print $FH "$AcceptedConn\n"; + print $FH "$HandledConn\n"; + print $FH "$NbRequests\n"; + close ($FH); + + my $ConnPerSec = 0; + my $ReqPerSec = 0; + my $RequestsNew = 0; + # by default the average + my $ReqPerConn = 0; + if ($AcceptedConn > 0) { + $ReqPerConn = $NbRequests/$AcceptedConn; + } + my $elapsed = $Time - $LastTime ; + if (defined ($o_debug)) { + print ("\nDebug: pre-computation\n"); + print ("Average ReqPerconn: $ReqPerConn, Seconds elapsed Since last check: $elapsed\n"); + } + # check only if the counters may have been incremented + # but not if it may have been too much incremented + # if nginx was restarted ($NbRequests is now lower than previous value), just skip + if ( ($elapsed < $MaxTimeDif) && ($elapsed != 0) && ($NbRequests >= $LastNbRequests) ) { + $ConnPerSec = ($AcceptedConn-$LastAcceptedConn)/$elapsed; + $RequestsNew = $NbRequests-$LastNbRequests; + $ReqPerSec = $RequestsNew/$elapsed; + # get finer value + if ( $ConnPerSec!=0 ) { + my $ReqPerConn = $ReqPerSec/$ConnPerSec; + } else { + my $ReqPerConn = 0; + } + } + if (defined ($o_debug)) { + print ("\nDebug: data computed\n"); + print ("ConnPerSec: $ConnPerSec ReqPerSec: $ReqPerSec ReqPerConn: $ReqPerConn\n"); + } + $InfoData = sprintf (" %.3f sec. response time, Active: %d (Writing: %d Reading: %d Waiting: %d)" + . " ReqPerSec: %.3f ConnPerSec: %.3f ReqPerConn: %.3f" + ,$timeelapsed,$ActiveConn,$Writing,$Reading,$Waiting,$ReqPerSec,$ConnPerSec,$ReqPerConn); + + # Manage warn and crit values for the perfdata + my $p_warn_a_level = "$o_warn_a_level"; + my $p_crit_a_level = "$o_crit_a_level"; + my $p_warn_rps_level = "$o_warn_rps_level"; + my $p_crit_rps_level = "$o_crit_rps_level"; + my $p_warn_cps_level = "$o_warn_cps_level"; + my $p_crit_cps_level = "$o_crit_cps_level"; + + if ($p_warn_a_level == "-1") { + $p_warn_a_level = ""; + } + if ($p_crit_a_level == "-1") { + $p_crit_a_level = ""; + } + if ($p_warn_rps_level == "-1") { + $p_warn_rps_level = ""; + } + if ($p_crit_rps_level == "-1") { + $p_crit_rps_level = ""; + } + if ($p_warn_cps_level == "-1") { + $p_warn_cps_level = ""; + } + if ($p_crit_cps_level == "-1") { + $p_crit_cps_level = ""; + } + + $PerfData = sprintf ("Writing=%d;;;; Reading=%d;;;; Waiting=%d;;;; Active=%d;%s;%s;; " + . "ReqPerSec=%f;%s;%s;; ConnPerSec=%f;%s;%s;; ReqPerConn=%f;;;;" + ,($Writing),($Reading),($Waiting),($ActiveConn) + ,($p_warn_a_level),($p_crit_a_level) + ,($ReqPerSec),($p_warn_rps_level),($p_crit_rps_level) + ,($ConnPerSec),($p_warn_cps_level),($p_crit_cps_level) + ,($ReqPerConn)); + # first all critical exists by priority + if (defined($o_crit_a_level) && (-1!=$o_crit_a_level) && ($ActiveConn >= $o_crit_a_level)) { + nagios_exit($nginx,"CRITICAL", "Active Connections are critically high " . $InfoData,$PerfData); + } + if (defined($o_crit_rps_level) && (-1!=$o_crit_rps_level) && ($ReqPerSec >= $o_crit_rps_level)) { + nagios_exit($nginx,"CRITICAL", "Request per second ratios is critically high " . $InfoData,$PerfData); + } + if (defined($o_crit_cps_level) && (-1!=$o_crit_cps_level) && ($ConnPerSec >= $o_crit_cps_level)) { + nagios_exit($nginx,"CRITICAL", "Connection per second ratio is critically high " . $InfoData,$PerfData); + } + # Then WARNING exits by priority + if (defined($o_warn_a_level) && (-1!=$o_warn_a_level) && ($ActiveConn >= $o_warn_a_level)) { + nagios_exit($nginx,"WARNING", "Active Connections are high " . $InfoData,$PerfData); + } + if (defined($o_warn_rps_level) && (-1!=$o_warn_rps_level) && ($ReqPerSec >= $o_warn_rps_level)) { + nagios_exit($nginx,"WARNING", "Requests per second ratio is high " . $InfoData,$PerfData); + } + if (defined($o_warn_cps_level) && (-1!=$o_warn_cps_level) && ($ConnPerSec >= $o_warn_cps_level)) { + nagios_exit($nginx,"WARNING", "Connection per second ratio is high " . $InfoData,$PerfData); + } + + nagios_exit($nginx,"OK",$InfoData,$PerfData); + +} else { + nagios_exit($nginx,"CRITICAL", $response->status_line); +} diff --git a/test/test-configurations/alignak/commands/utils.pm b/test/test-configurations/alignak/commands/utils.pm new file mode 100644 index 00000000..c1c159f5 --- /dev/null +++ b/test/test-configurations/alignak/commands/utils.pm @@ -0,0 +1,68 @@ +# Utility drawer for Nagios plugins. +# +# This will be deprecated soon. Please use Nagios::Plugin from CPAN +# for new plugins + +package utils; + +require Exporter; +@ISA = qw(Exporter); +@EXPORT_OK = qw($TIMEOUT %ERRORS &print_revision &support &usage); + +#use strict; +#use vars($TIMEOUT %ERRORS); +sub print_revision ($$); +sub usage; +sub support(); +sub is_hostname; + +## updated by autoconf +$PATH_TO_SUDO = "/usr/bin/sudo"; +$PATH_TO_RPCINFO = "" ; +$PATH_TO_LMSTAT = "" ; +$PATH_TO_SMBCLIENT = "" ; +$PATH_TO_MAILQ = ""; +$PATH_TO_QMAIL_QSTAT = ""; + +## common variables +$TIMEOUT = 15; +%ERRORS=('OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3,'DEPENDENT'=>4); + +## utility subroutines +sub print_revision ($$) { + my $commandName = shift; + my $pluginRevision = shift; + print "$commandName v$pluginRevision (nagios-plugins 2.1.1)\n"; + print "The nagios plugins come with ABSOLUTELY NO WARRANTY. You may redistribute\ncopies of the plugins under the terms of the GNU General Public License.\nFor more information about these matters, see the file named COPYING.\n"; +} + +sub support () { + my $support='Send email to help@nagios-plugins.org if you have questions regarding use\nof this software. To submit patches or suggest improvements, send email to\ndevel@nagios-plugins.org. Please include version information with all\ncorrespondence (when possible, use output from the --version option of the\nplugin itself).\n'; + $support =~ s/@/\@/g; + $support =~ s/\\n/\n/g; + print $support; +} + +sub usage { + my $format=shift; + printf($format,@_); + exit $ERRORS{'UNKNOWN'}; +} + +sub is_hostname { + my $host1 = shift; + return 0 unless defined $host1; + if ($host1 =~ m/^[\d\.]+$/ && $host1 !~ /\.$/) { + if ($host1 =~ m/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) { + return 1; + } else { + return 0; + } + } elsif ($host1 =~ m/^[a-zA-Z0-9][-a-zA-Z0-9]*(\.[a-zA-Z0-9][-a-zA-Z0-9]*)*\.?$/) { + return 1; + } else { + return 0; + } +} + +1; diff --git a/test/test-configurations/alignak/etc/alignak.d/alignak-module-ui-graphite2.ini b/test/test-configurations/alignak/etc/alignak.d/alignak-module-ui-graphite2.ini index cd7b10e4..73995de5 100644 --- a/test/test-configurations/alignak/etc/alignak.d/alignak-module-ui-graphite2.ini +++ b/test/test-configurations/alignak/etc/alignak.d/alignak-module-ui-graphite2.ini @@ -7,7 +7,7 @@ module_type=graphite-webui python_name=no_matter_what_is_set_here enabled=0 -uri=http://YOURSERVERNAME/ +uri=http://192.168.43.193/ ; Set your Graphite URI. Note : YOURSERVERNAME will be ; changed by your broker hostname diff --git a/test/test-configurations/alignak/etc/alignak.d/alignak-module-webui.ini b/test/test-configurations/alignak/etc/alignak.d/alignak-module-webui.ini index 6eb5e90e..8d3d4808 100644 --- a/test/test-configurations/alignak/etc/alignak.d/alignak-module-webui.ini +++ b/test/test-configurations/alignak/etc/alignak.d/alignak-module-webui.ini @@ -26,6 +26,18 @@ log_file=/tmp/var/log/alignak/shinken-webui.log # Default is not enabled ;statsd_enabled=0 +## Alignak configuration +# Define the Alignak arbiter API endppont +;alignak_endpoint = http://127.0.0.1:7770 + +# The webUI will get the Alignak livestate every alignak_check_period seconds +;alignak_check_period = 10 + +# The WebUI will store internally an events log queue limited to alignak_events_count items +# If ALIGNAK_EVENTS_LOG_COUNT environment variable is set it will take precedence +;alignak_events_count = 1000 + + ## Modules for WebUI ## User authentication: # - auth-cfg-password (internal) : Use the password set in Shinken contact for auth. @@ -101,7 +113,7 @@ logs_collection=alignak_events ## Declare the list of the WebUI modules ;modules=auth-ws-glpi -modules=auth-ws-glpi, ui-graphite2, alignak-logs +modules=ui-graphite2, alignak-logs # Specify the directory where to search for the WebUI modules # Default is /var/lib/shinken/modules because the Shinken installer locates the module in this directory # You would rather copy the Shinken modules in a dedicated place diff --git a/test/test-configurations/alignak/etc/alignak.d/daemons.ini b/test/test-configurations/alignak/etc/alignak.d/daemons.ini index 00211c31..e52f3c36 100644 --- a/test/test-configurations/alignak/etc/alignak.d/daemons.ini +++ b/test/test-configurations/alignak/etc/alignak.d/daemons.ini @@ -20,6 +20,7 @@ name=arbiter-master ; --- ; My listening interface ;host=127.0.0.1 +host=0.0.0.0 ; Set the host to 0.0.0.0 to allow Arbiter WS access from another system port=7770 ; My adress for the other daemons diff --git a/test/test-configurations/alignak/etc/alignak.d/extra-daemons.ini b/test/test-configurations/alignak/etc/alignak.d/extra-daemons.ini index b4873328..f54f0341 100644 --- a/test/test-configurations/alignak/etc/alignak.d/extra-daemons.ini +++ b/test/test-configurations/alignak/etc/alignak.d/extra-daemons.ini @@ -7,8 +7,9 @@ alignak_launched=1 ;statsd_host = 192.168.0.23 statsd_host = 192.168.43.193 statsd_port = 2004 -statsd_prefix = alignak-test -graphite_enabled = 0 +;statsd_prefix = alignak +statsd_prefix = +graphite_enabled = 1 [alignak-configuration] retain_state_information=true diff --git a/test/test-configurations/alignak/etc/alignak.d/modules.ini b/test/test-configurations/alignak/etc/alignak.d/modules.ini index 3fddcf0c..d4b0fc0b 100644 --- a/test/test-configurations/alignak/etc/alignak.d/modules.ini +++ b/test/test-configurations/alignak/etc/alignak.d/modules.ini @@ -101,9 +101,11 @@ enabled = 1 ; ; Module specific parameters -graphite_host=localhost +;graphite_host=192.168.0.23 +graphite_host=192.168.43.193 graphite_port=2004 -graphite_prefix=alignak +;graphite_prefix=alignak +;graphite_prefix= ; Add this suffix to the hosts/services matrics ;graphite_data_source=from_alignak diff --git a/test/test-configurations/alignak/etc/alignak.ini b/test/test-configurations/alignak/etc/alignak.ini index e4568dfd..3108be36 100644 --- a/test/test-configurations/alignak/etc/alignak.ini +++ b/test/test-configurations/alignak/etc/alignak.ini @@ -455,6 +455,7 @@ logger_configuration=./alignak-logger.json ; Whatever the value set in this file or internally computed, the arbiter will pause ; for a minimum of 1 second ;daemons_dispatch_timeout=5 +daemons_dispatch_timeout=2 ; -------------------------------------------------------------------- @@ -571,6 +572,7 @@ logger_configuration=./alignak-logger.json ; to its Graphite interface. ; See the alignak.d/modules.ini configuration file for more details / parameters ;host_perfdata_file= +host_perfdata_file=test ;service_perfdata_file= ; -------------------------------------------------------------------- diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/elasticsearchs.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/elasticsearchs.cfg index 502c3504..028af84f 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/elasticsearchs.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/elasticsearchs.cfg @@ -1,7 +1,7 @@ define host{ use elasticsearch,freebsd-nrpe - host_name my-elasticsearch-es1 + host_name es1 alias ES server display_name ES es1 @@ -14,7 +14,7 @@ define host{ define host{ use elasticsearch-auth,freebsd-nrpe - host_name my-secured-elasticsearch-es2 + host_name es2 alias ES server - HTTPS display_name ES es2 @@ -27,7 +27,7 @@ define host{ define host{ use elasticsearch,freebsd-nrpe - host_name my-elasticsearch-es3 + host_name es3 alias ES server display_name ES es3 diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/glpi.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/glpi.cfg new file mode 100644 index 00000000..f0dc3b5c --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/glpi.cfg @@ -0,0 +1,9 @@ +define host{ + use glpi,freebsd-nrpe + + host_name glpi2 + alias Glpi 2 + display_name Glpi 2 + + address 10.0.4.21 +} diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg index 3c635283..edc16401 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg @@ -12,4 +12,16 @@ define host{ # GPS _LOC_LAT 45.054700 _LOC_LNG 5.080856 + + notes Simple note + notes Label::note with a label + notes KB1023,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas vestibulum eu non sapien. Nulla facilisi. Aliquam non blandit tellus, non luctus tortor. Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.|note simple|Tag::tagged note ... + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ + + action_url http://www.google.fr|url1::http://www.google.fr|My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ + + # Defined in host template + #custom_views +linux_ssh,linux_ssh_memory,linux_ssh_processes + custom_views host } diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/mariadbs.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/mariadbs.cfg new file mode 100644 index 00000000..0fac590f --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/mariadbs.cfg @@ -0,0 +1,51 @@ +define host{ + use mysql,freebsd-nrpe + + host_name mariadb-master + alias MariaDB master + display_name MariaDB master + + address 10.0.4.11 + + _MYSQLUSER alignak + _MYSQLPASSWORD Alignak-2018 +} + +define host{ + use mysql-slave,freebsd-nrpe + + host_name mariadb-slave + alias MariaDB slave + display_name MariaDB slave + + address 10.0.4.12 + + _MYSQLUSER alignak + _MYSQLPASSWORD Alignak-2018 +} + +define host{ + use mysql,freebsd-nrpe + + host_name mysql-master + alias MySQL master + display_name MySQL master + + address 10.0.2.61 + + _MYSQLUSER alignak + _MYSQLPASSWORD Alignak-2018 +} + +define host{ + use mysql-slave,freebsd-nrpe + + host_name mysql-slave + alias MySQL slave + display_name MySQL slave + + address 10.0.2.62 + + _MYSQLUSER alignak + _MYSQLPASSWORD Alignak-2018 +} diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/nginxs.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/nginxs.cfg new file mode 100644 index 00000000..d1bfedef --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/nginxs.cfg @@ -0,0 +1,33 @@ +define host{ + use nginx,freebsd-nrpe + + host_name my-nginx-rp1 + alias Nginx server + display_name Nginx rp1 + + address 10.0.2.2 + + ; Default status URL + _PORT 80 + _STATUS_URL status + _SERVER_NAME localhost +} + +define host{ + use nginx-auth,freebsd-nrpe + + host_name my-secured-nginx-es2 + alias Nginx server - HTTPS + display_name Nginx rp2 + + address 10.0.2.3 + + ; Default status URL + _PORT 80 + _STATUS_URL status + _SERVER_NAME localhost + + ; Default username / password + _USERNAME admin + _PASSWORD admin +} diff --git a/test/test-configurations/alignak/etc/arbiter/objects/services/services.cfg b/test/test-configurations/alignak/etc/arbiter/objects/services/services.cfg index 7aa6433c..91be058e 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/services/services.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/services/services.cfg @@ -1,2 +1,23 @@ -## In this directory you can put all your specific service -# definitions \ No newline at end of file +define host{ + use generic-host + + host_name applications + address 127.0.0.1 +} +define service { + host_name applications + service_description Elasticsearch + + # Elasticsearch service is ok if one of the 3 ES are Up and Ok + check_command bp_rule! es1,check-es-status | es2,check-es-status | es3,check-es-status + use generic-service +} + + +define service{ + host_name applications + service_description Glpi + + check_command bp_rule! (glpi2,check-glpi-status) & (mariadb-master,Mysql-connection | mariadb-slave,Mysql-connection) + use generic-service +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/glpi/commands.cfg b/test/test-configurations/alignak/etc/arbiter/packs/glpi/commands.cfg new file mode 100755 index 00000000..34ac0d0f --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/glpi/commands.cfg @@ -0,0 +1,15 @@ +# TCP check +define command { + command_name check_glpi_alive_tcp + command_line $NAGIOSPLUGINSDIR$/check_tcp -H $HOSTADDRESS$ -p $_HOSTGLPI_PORT$ +} +# HTTP check +define command { + command_name check_glpi_alive_http + command_line $NAGIOSPLUGINSDIR$/check_http -I $HOSTADDRESS$ -u / -p $_HOSTGLPI_PORT$ -s 'GLPI' -s 'http://glpi-project.org' +} + +define command { + command_name check_glpi_status + command_line $NAGIOSPLUGINSDIR$/check_http -I $HOSTADDRESS$ -u /status.php -p $_HOSTGLPI_PORT$ -s 'GLPI_DB_OK' -s 'Crontasks_OK' -s 'GLPI_OK' +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/glpi/groups.cfg b/test/test-configurations/alignak/etc/arbiter/packs/glpi/groups.cfg new file mode 100755 index 00000000..11fe32fd --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/glpi/groups.cfg @@ -0,0 +1,10 @@ +# Host and service groups +define hostgroup { + hostgroup_name glpi + alias Glpi hosts +} + +define servicegroup { + servicegroup_name glpi + alias Glpi services +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/glpi/templates.cfg b/test/test-configurations/alignak/etc/arbiter/packs/glpi/templates.cfg new file mode 100755 index 00000000..032d5162 --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/glpi/templates.cfg @@ -0,0 +1,60 @@ +# Glpi host +# ------------------ +define host { + name glpi + register 0 + + hostgroups glpi + + _GLPI_PORT 80 +} + +define host { + name glpi-auth + register 0 + + hostgroups glpi + + ; Default username / password + _USERNAME admin + _PASSWORD admin +} + +define service { + name glpi-service + use generic-service + register 0 + + aggregation glpi + servicegroups glpi +} + + +# Glpi services +# ------------------------------------ +define service { + service_description check-glpi-tcp-alive + use glpi-service + register 0 + host_name glpi + + check_command check_glpi_alive_tcp +} +define service { + service_description check-glpi-http-alive + use glpi-service + register 0 + host_name glpi + + service_dependencies ,check-glpi-tcp-alive + check_command check_glpi_alive_http +} +define service { + service_description check-glpi-status + use glpi-service + register 0 + host_name glpi + + service_dependencies ,check-glpi-http-alive + check_command check_glpi_status +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/mysql/commands.cfg b/test/test-configurations/alignak/etc/arbiter/packs/mysql/commands.cfg new file mode 100755 index 00000000..efe9aad2 --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/mysql/commands.cfg @@ -0,0 +1,113 @@ +########################################################### +# Commands definition +########################################################### +# Distant mysql check +define command { + command_name check_mysql_connection + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode connection-time --warning $_HOSTCONNECTIONTIME_WARN$ --critical $_HOSTCONNECTIONTIME_CRIT$ +} + +define command { + command_name check_mysql_querycache_hitrate + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode querycache-hitrate --warning $_HOSTQUERYCACHEHITRATE_WARN$ --critical $_HOSTQUERYCACHEHITRATE_CRIT$ +} + +define command { + command_name check_mysql_uptime + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode uptime --warning $_HOSTUPTIME_WARN$ --critical $_HOSTUPTIME_CRIT$ +} + +define command { + command_name check_mysql_threads_connected + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode threads-connected --warning $_HOSTTHREADSCONNECTED_WARN$ --critical $_HOSTTHREADSCONNECTED_CRIT$ +} + +define command { + command_name check_mysql_qcache_hitrate + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode qcache-hitrate --warning $_HOSTQCACHEHITRATE_WARN$ --critical $_HOSTQCACHEHITRATE_CRIT$ +} + +define command { + command_name check_mysql_qcache_lowmem_prunes + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode qcache-lowmem-prunes --warning $_HOSTQCACHELOWMEMPRUNES_WARN$ --critical $_HOSTQCACHELOWMEMPRUNES_CRIT$ +} + +define command { + command_name check_mysql_keycache_hitrate + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode keycache-hitrate --warning $_HOSTKEYCACHEHITRATE_WARN$ --critical $_HOSTKEYCACHEHITRATE_CRIT$ +} + +define command { + command_name check_mysql_bufferpool_hitrate + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode bufferpool-hitrate --warning $_HOSTBUFFERPOOLHITRATE_WARN$ --critical $_HOSTBUFFERPOOLHITRATE_CRIT$ +} + +define command { + command_name check_mysql_bufferpool_wait_free + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode bufferpool-wait-free --warning $_HOSTBUFFERPOOLWAITFREE_WARN$ --critical $_HOSTBUFFERPOOLWAITFREE_CRIT$ +} + +define command { + command_name check_mysql_log_waits + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode log-waits --warning $_HOSTLOGWAITS_WARN$ --critical $_HOSTLOGWAITS_CRIT$ +} + +define command { + command_name check_mysql_tablecache_hitrate + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode tablecache-hitrate --warning $_HOSTTABLECACHEHITRATE_WARN$ --critical $_HOSTTABLECACHEHITRATE_CRIT$ +} + +define command { + command_name check_mysql_table_lock_contention + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode table-lock-contention --warning $_HOSTTABLELOCKCONTENTION_WARN$ --critical $_HOSTTABLELOCKCONTENTION_CRIT$ +} + +define command { + command_name check_mysql_index_usage + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode index-usage --warning $_HOSTINDEXUSAGE_WARN$ --critical $_HOSTINDEXUSAGE_CRIT$ +} + +define command { + command_name check_mysql_tmp_disk_tables + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode tmp-disk-tables --warning $_HOSTTMPDISKTABLES_WARN$ --critical $_HOSTTMPDISKTABLES_CRIT$ +} + +define command { + command_name check_mysql_slow_queries + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode slow-queries --warning $_HOSTSLOWQUERIES_WARN$ --critical $_HOSTSLOWQUERIES_CRIT$ +} + +define command { + command_name check_mysql_long_running_procs + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode long-running-procs --warning $_HOSTLONGRUNNINGPROCS_WARN$ --critical $_HOSTLONGRUNNINGPROCS_CRIT$ +} + +define command { + command_name check_mysql_slave_lag + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode slave-lag --warning $_HOSTSLAVELAG_WARN$ --critical $_HOSTSLAVELAG_CRIT$ +} + +define command { + command_name check_mysql_slave_io_running + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode slave-io-running +} + +define command { + command_name check_mysql_slave_sql_running + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode slave-sql-running +} + +define command { + command_name check_mysql_open_files + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode open-files --warning $_HOSTOPENFILES_WARN$ --critical $_HOSTOPENFILES_CRIT$ +} + +define command { + command_name check_mysql_cluster_ndbd_running + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode cluster-ndbd-running +} + +define command { + command_name check_mysql_threadcache_hitrate + command_line $PLUGINSDIR$/check_mysql_health --hostname $HOSTADDRESS$ --username $_HOSTMYSQLUSER$ --password $_HOSTMYSQLPASSWORD$ --mode threadcache-hitrate --warning $_HOSTTHREADCACHEHITRATE_WARN$ --critical $_HOSTTHREADCACHEHITRATE_CRIT$ +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/mysql/groups.cfg b/test/test-configurations/alignak/etc/arbiter/packs/mysql/groups.cfg new file mode 100755 index 00000000..a6ffcdaa --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/mysql/groups.cfg @@ -0,0 +1,10 @@ +# Host and service groups +define hostgroup { + hostgroup_name mysql + alias Mysql database hosts +} + +define servicegroup { + servicegroup_name mysql + alias Mysql database services +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/mysql/services.cfg b/test/test-configurations/alignak/etc/arbiter/packs/mysql/services.cfg new file mode 100644 index 00000000..cae32bb8 --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/mysql/services.cfg @@ -0,0 +1,218 @@ +define service{ + service_description Mysql-connection + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_connection +} + +define service{ + service_description Mysql-restart + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_uptime + service_dependencies ,Mysql-connection +} + +define service{ + service_description Mysql-bufferpool-wait-free + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_bufferpool_wait_free + + service_dependencies ,Mysql-connection + aggregation /mysql/innodb +} + +define service{ + service_description Mysql-index-usage + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_index_usage + + service_dependencies ,Mysql-connection +} +define service{ + service_description Mysql-log-waits + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_log_waits + service_dependencies ,Mysql-connection + aggregation /mysql/innodb +} +define service{ + service_description Mysql-long-running-procs + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_long_running_procs + service_dependencies ,Mysql-connection +} +define service{ + service_description Mysql-open-files + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_open_files + + service_dependencies ,Mysql-connection + aggregation /mysql/limits +} +define service{ + service_description Mysql-slow-queries + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_slow_queries + + service_dependencies ,Mysql-connection +} +define service{ + service_description Mysql-table-lock-contention + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_table_lock_contention + + service_dependencies ,Mysql-connection +} + +# Cache management +define service{ + service_description Mysql-qcache-hitrate + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_qcache_hitrate + + service_dependencies ,Mysql-connection + aggregation mysql/cachehit +} +define service{ + service_description Mysql-qcache-lowmem-prunes + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_qcache_lowmem_prunes + + service_dependencies ,Mysql-connection + aggregation mysql/cachehit +} +define service{ + service_description Mysql-keycache-hitrate + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_keycache_hitrate + + service_dependencies ,Mysql-connection + aggregation mysql/cachehit +} +define service{ + service_description Mysql-bufferpool-hitrate + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_bufferpool_hitrate + + service_dependencies ,Mysql-connection + aggregation mysql/cachehit +} +define service{ + service_description Mysql-tablecache-hitrate + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_tablecache_hitrate + + service_dependencies ,Mysql-connection + aggregation mysql/cachehit +} +define service{ + service_description Mysql-threadcache-hitrate + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_threadcache_hitrate + + service_dependencies ,Mysql-connection + aggregation mysql/cachehit +} + +define service{ + service_description Mysql-threads-connected + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_threads_connected + + service_dependencies ,Mysql-connection + aggregation /mysql/limits +} + +define service{ + service_description Mysql-tmp-disk-tables + use mysql-service + register 0 + host_name mysql + + check_command check_mysql_tmp_disk_tables + + service_dependencies ,Mysql-connection +} + +# For a slave DB... +define service{ + service_description Mysql-slave-lag + use generic-service + register 0 + host_name mysql-slave + + check_command check_mysql_slave_lag +} +define service{ + service_description Mysql-slave-sql-running + use generic-service + register 0 + host_name mysql-slave + + check_command check_mysql_slave_sql_running +} +define service{ + service_description Mysql-slave-io-running + use generic-service + register 0 + host_name mysql-slave + + check_command check_mysql_slave_io_running +} + +# For a Mysql cluster... +define service{ + service_description Mysql-cluster-ndbd-running + use mysql-service + register 0 + host_name mysql-cluster + + check_command check_mysql_cluster_ndbd_running +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/mysql/templates.cfg b/test/test-configurations/alignak/etc/arbiter/packs/mysql/templates.cfg new file mode 100755 index 00000000..f214ab04 --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/mysql/templates.cfg @@ -0,0 +1,80 @@ +# NSCA passively monitored host +# DATABASES HOSTS +define host{ + name generic-mysql + register 0 + + hostgroups mysql + + _MYSQLUSER $MYSQLUSER$ + _MYSQLPASSWORD $MYSQLPASSWORD$ +} + +define host{ + name mysql + use generic-mysql + register 0 + + # The plugin need a ":" sign when perfdata should be greater than the thresholds. + _UPTIME_WARN 10: + _UPTIME_CRIT 5: + _CONNECTIONTIME_WARN 1 + _CONNECTIONTIME_CRIT 5 + _QUERYCACHEHITRATE_WARN 90: + _QUERYCACHEHITRATE_CRIT 80: + _THREADSCONNECTED_WARN 10 + _THREADSCONNECTED_CRIT 20 + _QCACHEHITRATE_WARN 90: + _QCACHEHITRATE_CRIT 80: + _QCACHELOWMEMPRUNES_WARN 1 + _QCACHELOWMEMPRUNES_CRIT 10 + _KEYCACHEHITRATE_WARN 99: + _KEYCACHEHITRATE_CRIT 95: + _BUFFERPOOLHITRATE_WARN 99: + _BUFFERPOOLHITRATE_CRIT 95: + _BUFFERPOOLWAITFREE_WARN 1 + _BUFFERPOOLWAITFREE_CRIT 10 + _LOGWAITS_WARN 1 + _LOGWAITS_CRIT 10 + _TABLECACHEHITRATE_WARN 99: + _TABLECACHEHITRATE_CRIT 95: + _TABLELOCKCONTENTION_WARN 1 + _TABLELOCKCONTENTION_CRIT 2 + _INDEXUSAGE_WARN 90: + _INDEXUSAGE_CRIT 80: + _TMPDISKTABLES_WARN 25 + _TMPDISKTABLES_CRIT 50 + _SLOWQUERIES_WARN 0.1 + _SLOWQUERIES_CRIT 1 + _LONGRUNNINGPROCS_WARN 10 + _LONGRUNNINGPROCS_CRIT 20 + _OPENFILES_WARN 80 + _OPENFILES_CRIT 95 + _THREADCACHEHITRATE_WARN 99: + _THREADCACHEHITRATE_CRIT 95: +} + +define host{ + name mysql-slave + use mysql + register 0 + + _SLAVELAG_WARN 10 + _SLAVELAG_CRIT 20 +} + +define host{ + name mysql-cluster + use generic-mysql + register 0 +} + +define service{ + name mysql-service + use generic-service + register 0 + + aggregation mysql + + servicegroups mysql +} \ No newline at end of file diff --git a/test/test-configurations/alignak/etc/arbiter/packs/nginx/commands.cfg b/test/test-configurations/alignak/etc/arbiter/packs/nginx/commands.cfg new file mode 100755 index 00000000..a5cfff0d --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/nginx/commands.cfg @@ -0,0 +1,12 @@ +# ----------------------------------------------------------------- +# Default ES commands +# ----------------------------------------------------------------- +define command { + command_name check_nginx_status + command_line $LIBEXEC$/check_nginx_status.pl -H $HOSTADDRESS$ -P $HOSTPORT$ -u $_HOSTSTATUS_URL$ -s $_HOSTSERVER_NAME$ -w $_HOSTACTIVE_CONN_WARN$,$_HOSTREQ_PER_SEC_WARN$,$_HOSTCONN_PER_SEC_WARN$ -c $_HOSTACTIVE_CONN_CRIT$,$_HOSTREQ_PER_SEC_CRIT$,$_HOSTCONN_PER_SEC_CRIT$ +} +# Check with authentication +define command { + command_name check_nginx_status_auth + command_line $LIBEXEC$/check_nginx_status.pl -H $HOSTADDRESS$ -P $HOSTPORT$ -u $_HOSTSTATUS_URL$ $_HOSTSERVER_NAME$ -S -U $_HOSTUSERNAME$ -P $_HOSTPASSWORD$ -w $_HOSTACTIVE_CONN_WARN$,$_HOSTREQ_PER_SEC_WARN$,$_HOSTCONN_PER_SEC_WARN$ -c $_HOSTACTIVE_CONN_CRIT$,$_HOSTREQ_PER_SEC_CRIT$,$_HOSTCONN_PER_SEC_CRIT$ --ssl +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/nginx/groups.cfg b/test/test-configurations/alignak/etc/arbiter/packs/nginx/groups.cfg new file mode 100755 index 00000000..38f9a015 --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/nginx/groups.cfg @@ -0,0 +1,10 @@ +# Host and service groups +define hostgroup { + hostgroup_name nginx + alias Nginx hosts +} + +define servicegroup { + servicegroup_name nginx + alias Nginx services +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/nginx/templates.cfg b/test/test-configurations/alignak/etc/arbiter/packs/nginx/templates.cfg new file mode 100755 index 00000000..8bdbc0c3 --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/packs/nginx/templates.cfg @@ -0,0 +1,60 @@ +# Nginx host +# ---------- +define host { + name nginx + register 0 + + hostgroups nginx + + ; Default status URL + _PORT 80 + _STATUS_URL nginx_status + _SERVER_NAME localhost + + _ACTIVE_CONN_WARN 500 + _ACTIVE_CONN_CRIT 1000 + _REQ_PER_SEC_WARN 50 + _REQ_PER_SEC_CRIT 75 + _CONN_PER_SEC_WARN 10 + _CONN_PER_SEC_CRIT 20 +} + +define host { + name nginx-auth + register 0 + + hostgroups nginx + + ; Default status URL + _STATUS_URL nginx_status + _SERVER_NAME localhost + + ; Default username / password + _USERNAME admin + _PASSWORD admin +} + +define service { + service_description check_status + use generic-service + alias Check status + register 0 + host_name nginx + + check_command check_nginx_status + + aggregation nginx + servicegroups nginx +} +define service { + service_description check_status_auth + use generic-service + alias Check status (authenticated) + register 0 + host_name nginx-auth + + check_command check_nginx_status_auth + + aggregation nginx + servicegroups nginx +} diff --git a/test/test-configurations/alignak/etc/arbiter/packs/nrpe/services-freebsd.cfg b/test/test-configurations/alignak/etc/arbiter/packs/nrpe/services-freebsd.cfg index c6be00a1..bb68f1e0 100755 --- a/test/test-configurations/alignak/etc/arbiter/packs/nrpe/services-freebsd.cfg +++ b/test/test-configurations/alignak/etc/arbiter/packs/nrpe/services-freebsd.cfg @@ -83,7 +83,7 @@ define service { service_description Opened files use freebsd-nrpe-service register 0 - host_name freebsd-nrpe + host_name freebsd-nrpe-disk check_command check_nrpe!check_openedfiles aggregation system/Health diff --git a/test/test-configurations/alignak/etc/arbiter/resource.d/mysql.cfg b/test/test-configurations/alignak/etc/arbiter/resource.d/mysql.cfg new file mode 100755 index 00000000..fb356dca --- /dev/null +++ b/test/test-configurations/alignak/etc/arbiter/resource.d/mysql.cfg @@ -0,0 +1,4 @@ + +#-- MySQL default credentials +$MYSQLUSER$=root +$MYSQLPASSWORD$=root diff --git a/test/test-configurations/alignak/etc/arbiter/templates/generic-host.cfg b/test/test-configurations/alignak/etc/arbiter/templates/generic-host.cfg index 988b015f..3d2526f3 100644 --- a/test/test-configurations/alignak/etc/arbiter/templates/generic-host.cfg +++ b/test/test-configurations/alignak/etc/arbiter/templates/generic-host.cfg @@ -1,36 +1,45 @@ # Generic host definition template - This is NOT a real host, just a template! # Most hosts should inherit from this one define host{ - name generic-host + name generic-host - # Checking part - # Default is always up - check_command _internal_host_up - initial_state UP - initial_output Host assumed to be Up - max_check_attempts 2 - check_interval 5 + # Checking part + # Default is always up + check_command _internal_host_up + initial_state UNKNOWN + initial_output Host sate is unknown + max_check_attempts 2 - # Check every time - active_checks_enabled 1 - check_period 24x7 + # Active and passive checks are enabled + active_checks_enabled 1 + passive_checks_enabled 1 + check_freshness 1 + freshness_threshold 3600 - # Notification part - # One notification each day (1440 = 60min* 24h) - # every time, and for all 'errors' - # notify the admins contactgroups by default - notifications_enabled 1 - notification_interval 1440 - notification_period 24x7 - notification_options d,u,r,f,s - contact_groups admins,users,notified + # Check every time + check_period 24x7 + check_interval 5 + retry_interval 1 - # Advanced option - event_handler_enabled 0 - flap_detection_enabled 1 - process_perf_data 1 - snapshot_enabled 0 + # For Graphite prefix + _GRAPHITE_PRE alignak - # This to say that it's a template - register 0 + # Notification part + # One notification each day (1440 = 60min* 24h) + # every time, and for all 'errors' + # notify the admins contactgroups by default + notifications_enabled 1 + notification_interval 1440 + notification_period 24x7 + notification_options d,u,r,f,s + contact_groups admins,users,notified + + # Advanced option + event_handler_enabled 0 + flap_detection_enabled 1 + process_perf_data 1 + snapshot_enabled 0 + + # This to say that it's a template + register 0 } diff --git a/test/test-configurations/alignak/etc/arbiter/templates/generic-service.cfg b/test/test-configurations/alignak/etc/arbiter/templates/generic-service.cfg index 92b3c667..0e24fd77 100644 --- a/test/test-configurations/alignak/etc/arbiter/templates/generic-service.cfg +++ b/test/test-configurations/alignak/etc/arbiter/templates/generic-service.cfg @@ -5,6 +5,8 @@ define service{ # Active and passive checks are enabled active_checks_enabled 1 passive_checks_enabled 1 + check_freshness 1 + freshness_threshold 3600 # Advanced option event_handler_enabled 0 diff --git a/test/test-configurations/shinken/etc/arbiter/objects/contactgroups/notified.cfg b/test/test-configurations/shinken/etc/arbiter/objects/contactgroups/notified.cfg new file mode 100644 index 00000000..677a704c --- /dev/null +++ b/test/test-configurations/shinken/etc/arbiter/objects/contactgroups/notified.cfg @@ -0,0 +1,7 @@ +define contactgroup{ + contactgroup_name notified + alias Notified users + + members notified +} + From 1eab371c502c75f840908256529d7d259272660f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Sun, 16 Dec 2018 08:40:37 +0100 Subject: [PATCH 02/31] Manage Alignak arbiter in the main module - get status thread --- etc/alignak.d/alignak-module-webui.ini | 17 ++++++- module/module.py | 62 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/etc/alignak.d/alignak-module-webui.ini b/etc/alignak.d/alignak-module-webui.ini index 6576d291..fcc815b0 100644 --- a/etc/alignak.d/alignak-module-webui.ini +++ b/etc/alignak.d/alignak-module-webui.ini @@ -13,8 +13,9 @@ python_name=alignak_webui ;log_level=INFO # Set log_color=1 to have a (colored) log on the console ;log_console=0 -# Declare the log file for the Shinken Web UI log (default is /var/log/alignak/shinken-webui.log) -;log_file=/var/log/alignak/shinken-webui.log +# Declare the log file for the Shinken Web UI log (default is /var/log/alignak/alignak-webui.log) +; If the configured and/or default log file is not allowed, the UI will log to /tmp/alignak-webui.log +;log_file=/var/log/alignak/alignak-webui.log # Export module metrics to a statsd server. # By default at localhost:8125 (UDP) with the alignak prefix @@ -26,6 +27,18 @@ python_name=alignak_webui # Default is not enabled ;statsd_enabled=0 +## Alignak configuration +# Define the Alignak arbiter API endppont +;alignak_endpoint = http://127.0.0.1:7770 + +# The webUI will get the Alignak livestate every alignak_check_period seconds +;alignak_check_period = 10 + +# The WebUI will store internally an events log queue limited to alignak_events_count items +# If ALIGNAK_EVENTS_LOG_COUNT environment variable is set it will take precedence +;alignak_events_count = 1000 + + ## Modules for WebUI ## User authentication: # - auth-cfg-password (internal) : Use the password set in Shinken contact for auth. diff --git a/module/module.py b/module/module.py index 04800494..834b7fe9 100644 --- a/module/module.py +++ b/module/module.py @@ -35,6 +35,7 @@ WEBUI_LICENSE = "License GNU AGPL as published by the FSF, minimum version 3 of the License." import os +import json import string import random import traceback @@ -46,6 +47,8 @@ import logging import requests +from collections import deque + try: from setproctitle import setproctitle except ImportError as err: @@ -173,6 +176,14 @@ def __init__(self, modconf): self.type = 'webui' self.name = 'webui' + # Configure Alignak Arbiter API endpoint + self.alignak_endpoint = getattr(modconf, 'alignak_endpoint', 'http://127.0.0.1:7770') + self.alignak_check_period = int(getattr(modconf, 'alignak_check_period', '10')) + self.alignak_livestate = {} + self.alignak_events_count = int(getattr(modconf, 'alignak_events_count', '1000')) + self.alignak_events = deque(maxlen=int(os.environ.get('ALIGNAK_EVENTS_LOG_COUNT', + self.alignak_events_count))) + # Shinken logger configuration log_console = (getattr(modconf, 'log_console', '0') == '1') if not log_console: @@ -573,6 +584,11 @@ def main(self): self.data_thread.start() # TODO: look for alive and killing + # Launch the Alignak arbiter data thread ... + if self.alignak and self.alignak_endpoint: + self.fmwk_thread = threading.Thread(None, self.fmwk_thread, 'fmwk_hread') + self.fmwk_thread.start() + logger.info("[WebUI] starting Web UI server on %s:%d ...", self.host, self.port) bottle.TEMPLATES.clear() webui_app.run(host=self.host, port=self.port, server=self.http_backend, **self.serveropts) @@ -730,6 +746,52 @@ def manage_brok_thread(self): logger.debug("[WebUI] manage_brok_thread end ...") + def fmwk_thread(self): + """A thread function that periodically gets its state from the Alignak arbiter + + This function gets Alignak status in our alignak_livestate and + then it gets the Alignak events in our alignak_events queue + """ + logger.debug("[WebUI] fmwk_thread start ...") + + req = requests.Session() + alignak_timestamp = 0 + while not self.interrupted: + # Get Alignak status + try: + raw_data = req.get("%s/status" % self.alignak_endpoint) + data = json.loads(raw_data.content) + self.alignak_livestate = data.get('livestate', 'Unknown') + logger.debug("[WebUI-fmwk_thread] Livestate: %s", data) + except Exception as exp: + logger.info("[WebUI-system] alignak_status, exception: %s", exp) + + try: + # Get Alignak most recent events + # count is the maximum number of events we will be able to get + # timestamp is the most recent event we got + raw_data = req.get("%s/events_log?details=1&count=%d×tamp=%d" + % (self.alignak_endpoint, self.alignak_events_count, alignak_timestamp)) + data = json.loads(raw_data.content) + logger.debug("[WebUI-fmwk_thread] got %d event log", len(data)) + for log in data: + # Data contains: { + # u'date': u'2018-11-24 16:28:03', u'timestamp': 1543073283.434844, + # u'message': u'RETENTION LOAD: scheduler-master', u'level': u'info' + # } + alignak_timestamp = max(alignak_timestamp, log['timestamp']) + if log not in self.alignak_events: + logger.debug("[WebUI-fmwk_thread] New event log: %s", log) + self.alignak_events.appendleft(log) + logger.debug("[WebUI-fmwk_thread] %d log events", len(self.alignak_events)) + except Exception as exp: + logger.info("[WebUI-system] alignak_status, exception: %s", exp) + + # Sleep for a while... + time.sleep(self.alignak_check_period) + + logger.debug("[WebUI] fmwk_thread end ...") + # Here we will load all plugins (pages) under the webui/plugins # directory. Each one can have a page, views and htdocs dir that we must # route correctly From b397a21bb1d06555561e49b909b3019a8429211f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Sun, 16 Dec 2018 08:58:44 +0100 Subject: [PATCH 03/31] Display Alignak parameters - schedulers configuration --- module/htdocs/css/shinken-layout.css | 13 + module/plugins/system/htdocs/css/alignak.css | 62 + .../plugins/system/htdocs/css/multiselect.css | 66 + .../system/htdocs/js/jquery.floatThead.min.js | 3 + .../plugins/system/htdocs/js/multiselect.js | 1716 +++++++++++++++++ module/plugins/system/system.py | 37 + .../plugins/system/views/_dump_dict_panel.tpl | 71 + .../system/views/alignak-parameters.tpl | 19 + module/plugins/system/views/system_widget.tpl | 10 +- module/regenerator.py | 4 +- module/views/header_element.tpl | 5 + 11 files changed, 1998 insertions(+), 8 deletions(-) create mode 100644 module/plugins/system/htdocs/css/alignak.css create mode 100644 module/plugins/system/htdocs/css/multiselect.css create mode 100644 module/plugins/system/htdocs/js/jquery.floatThead.min.js create mode 100644 module/plugins/system/htdocs/js/multiselect.js create mode 100644 module/plugins/system/views/_dump_dict_panel.tpl create mode 100644 module/plugins/system/views/alignak-parameters.tpl diff --git a/module/htdocs/css/shinken-layout.css b/module/htdocs/css/shinken-layout.css index 00bf4adf..b46d4abc 100644 --- a/module/htdocs/css/shinken-layout.css +++ b/module/htdocs/css/shinken-layout.css @@ -696,3 +696,16 @@ footer nav { .badge-critical { background-color: #da4f49 !important; } + +@media (min-width: 768px) { + .dl-horizontal dt { + font-size:x-small; + /* Default bootstrap is a fixed width 160px */ + width: 180px; + } + .dl-horizontal dd { + font-size:small; + /* Default bootstrap is a fixed width 180px */ + margin-left: 200px; + } +} \ No newline at end of file diff --git a/module/plugins/system/htdocs/css/alignak.css b/module/plugins/system/htdocs/css/alignak.css new file mode 100644 index 00000000..8545b76f --- /dev/null +++ b/module/plugins/system/htdocs/css/alignak.css @@ -0,0 +1,62 @@ +th.vertical { + height: 120px; + width: 15px; + vertical-align: bottom; + text-align: center; + overflow: visible!important; + white-space: nowrap; + border-top: 0; + padding-left: 2px; + padding-right: 2px; +} + +/** +* Works everywere ( IE7+, FF, Chrome, Safari, Opera ) +* Example: http://jsbin.com/afAQAWA/2/ +*/ +.rotated-text { + display: inline-block; + overflow: hidden; + font-weight: normal; + padding-left: 2px; + padding-right: 2px; + width: 18px; + margin: 0; + margin-bottom: 4px; +} +.rotated-text__inner { + display: inline-block; + white-space: nowrap; + /* this is for shity "non IE" browsers + that doesn't support writing-mode */ + -webkit-transform: translate(1.1em,0) rotate(90deg); + -moz-transform: translate(1.1em,0) rotate(90deg); + -o-transform: translate(1.1em,0) rotate(90deg); + transform: translate(1.1em,0) rotate(90deg); + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; + transform-origin: 0 0; + /* IE9+ */ + -ms-transform: none; + -ms-transform-origin: none; + /* IE8+ */ + -ms-writing-mode: tb-rl; + /* IE7 and below */ + *writing-mode: tb-rl; +} +.rotated-text__inner:before { + content: ""; + float: left; + margin-top: 100%; +} + + +/* + Fixed table header jQuery plugin when used with bootstrap requires this declaration ... + */ +table.floatThead-table { + border-top: none; + border-bottom: none; + background-color: #FFF; +} \ No newline at end of file diff --git a/module/plugins/system/htdocs/css/multiselect.css b/module/plugins/system/htdocs/css/multiselect.css new file mode 100644 index 00000000..44ad9ab1 --- /dev/null +++ b/module/plugins/system/htdocs/css/multiselect.css @@ -0,0 +1,66 @@ +span.multiselect-native-select { + position: relative +} +span.multiselect-native-select select { + border: 0!important; + clip: rect(0 0 0 0)!important; + height: 1px!important; + margin: -1px -1px -1px -3px!important; + overflow: hidden!important; + padding: 0!important; + position: absolute!important; + width: 1px!important; + left: 50%; + top: 30px +} +.multiselect-container { + position: absolute; + list-style-type: none; + margin: 0; + padding: 0 +} +.multiselect-container .input-group { + margin: 5px +} +.multiselect-container>li { + padding: 0 +} +.multiselect-container>li>a.multiselect-all label { + font-weight: 700 +} +.multiselect-container>li.multiselect-group label { + margin: 0; + padding: 3px 20px 3px 20px; + height: 100%; + font-weight: 700 +} +.multiselect-container>li.multiselect-group-clickable label { + cursor: pointer +} +.multiselect-container>li>a { + padding: 0 +} +.multiselect-container>li>a>label { + margin: 0; + height: 100%; + cursor: pointer; + font-weight: 400; + padding: 3px 0 3px 30px +} +.multiselect-container>li>a>label.radio, .multiselect-container>li>a>label.checkbox { + margin: 0 +} +.multiselect-container>li>a>label>input[type=checkbox] { + margin-bottom: 5px +} +.btn-group>.btn-group:nth-child(2)>.multiselect.btn { + border-top-left-radius: 4px; + border-bottom-left-radius: 4px +} +.form-inline .multiselect-container label.checkbox, .form-inline .multiselect-container label.radio { + padding: 3px 20px 3px 40px +} +.form-inline .multiselect-container li a label.checkbox input[type=checkbox], .form-inline .multiselect-container li a label.radio input[type=radio] { + margin-left: -20px; + margin-right: 0 +} \ No newline at end of file diff --git a/module/plugins/system/htdocs/js/jquery.floatThead.min.js b/module/plugins/system/htdocs/js/jquery.floatThead.min.js new file mode 100644 index 00000000..b1e1fe1d --- /dev/null +++ b/module/plugins/system/htdocs/js/jquery.floatThead.min.js @@ -0,0 +1,3 @@ +// @preserve jQuery.floatThead 1.3.2 - http://mkoryak.github.io/floatThead/ - Copyright (c) 2012 - 2015 Misha Koryak +// @license MIT +!function(a){function b(a,b){if(8==j){var c=o.width(),d=h.debounce(function(){var a=o.width();c!=a&&(c=a,b())},1);o.on(a,d)}else o.on(a,h.debounce(b,1))}function c(a){window&&window.console&&window.console.error&&window.console.error("jQuery.floatThead: "+a)}function d(a){var b=a.getBoundingClientRect();return b.width||b.right-b.left}function e(){var b=a('
');a("body").append(b);var c=b.innerWidth(),d=a("div",b).innerWidth();return b.remove(),c-d}function f(a){if(a.dataTableSettings)for(var b=0;b*:visible",zIndex:1001,position:"auto",top:0,bottom:0,scrollContainer:function(){return a([])},getSizingRow:function(a){return a.find("tbody tr:visible:first>*:visible")},floatTableClass:"floatThead-table",floatWrapperClass:"floatThead-wrapper",floatContainerClass:"floatThead-container",copyTableClass:!0,enableAria:!1,autoReflow:!1,debug:!1};var h=window._,i="undefined"!=typeof MutationObserver,j=function(){for(var a=3,b=document.createElement("b"),c=b.all||[];a=1+a,b.innerHTML="",c[0];);return a>4?a:document.documentMode}(),k=/Gecko\//.test(navigator.userAgent),l=/WebKit\//.test(navigator.userAgent),m=function(){if(l){var b=a('
X
');a("body").append(b);var c=0==b.find("table").width();return b.remove(),c}return!1},n=!k&&!j,o=a(window);a.fn.floatThead=function(k){if(k=k||{},!h&&(h=window._||a.floatThead._,!h))throw new Error("jquery.floatThead-slim.js requires underscore. You should use the non-lite version since you do not have underscore.");if(8>j)return this;var p=null;if(h.isFunction(m)&&(m=m()),h.isString(k)){var q=k,r=this;return this.filter("table").each(function(){var b=a(this),c=b.data("floatThead-lazy");c&&b.floatThead(c);var d=b.data("floatThead-attached");if(d&&h.isFunction(d[q])){var e=d[q]();"undefined"!=typeof e&&(r=e)}}),r}var s=a.extend({},a.floatThead.defaults||{},k);if(a.each(k,function(b){b in a.floatThead.defaults||!s.debug||c("Used ["+b+"] key to init plugin, but that param is not an option for the plugin. Valid options are: "+h.keys(a.floatThead.defaults).join(", "))}),s.debug){var t=a.fn.jquery.split(".");1==parseInt(t[0],10)&&parseInt(t[1],10)<=7&&c("jQuery version "+a.fn.jquery+" detected! This plugin supports 1.8 or better, or 1.7.x with jQuery UI 1.8.24 -> http://jqueryui.com/resources/download/jquery-ui-1.8.24.zip")}return this.filter(":not(."+s.floatTableClass+")").each(function(){function k(a){return a+".fth-"+E+".floatTHead"}function m(){var b=0;if(G.children("tr:visible").each(function(){b+=a(this).outerHeight(!0)}),"collapse"==F.css("border-collapse")){var c=parseInt(F.css("border-top-width"),10),d=parseInt(F.find("thead tr:first").find(">*:first").css("border-top-width"),10);c>d&&(b-=c/2)}eb.outerHeight(b),fb.outerHeight(b)}function q(){var a=g(F,ib,!0),b=O.width()||a,c="hidden"!=O.css("overflow-y")?b-L.vertical:b;if(bb.width(c),P){var d=100*a/c;Y.css("width",d+"%")}else Y.outerWidth(a)}function r(){I=(h.isFunction(s.top)?s.top(F):s.top)||0,J=(h.isFunction(s.bottom)?s.bottom(F):s.bottom)||0}function t(){var b,c=G.find(s.headerCellSelector);if(_?b=$.find("col").length:(b=0,c.each(function(){b+=parseInt(a(this).attr("colspan")||1,10)})),b!=N){N=b;for(var d,e=[],f=[],g=[],h=0;b>h;h++)e.push(s.enableAria&&(d=c.eq(h).text())?''+d+"":''),f.push(""),g.push("");f=f.join(""),e=e.join(""),n&&(g=g.join(""),ab.html(g),ib=ab.find("fthtd")),eb.html(e),fb=eb.find("th"),_||$.html(f),gb=$.find("col"),Z.html(f),hb=Z.find("col")}return b}function u(){if(!K){if(K=!0,Q){var a=g(F,ib,!0),b=W.width();a>b&&F.css("minWidth",a)}F.css(lb),Y.css(lb),Y.append(G),H.before(db),m()}}function v(){K&&(K=!1,Q&&F.width(nb),db.detach(),F.prepend(G),F.css(mb),Y.css(mb),F.css("minWidth",ob),F.css("minWidth",g(F,ib)))}function w(a){pb!=a&&(pb=a,F.triggerHandler("floatThead",[a,bb]))}function x(a){Q!=a&&(Q=a,bb.css({position:Q?"absolute":"fixed"}))}function y(a,b,c,d){return n?c:d?s.getSizingRow(a,b,c):b}function z(){var a,b=t();return function(){gb=$.find("col");var c=y(F,gb,ib,j);if(c.length==b&&b>0){if(!_)for(a=0;b>a;a++)gb.eq(a).css("width","");v();var e=[];for(a=0;b>a;a++)e[a]=d(c.get(a));for(a=0;b>a;a++)hb.eq(a).width(e[a]),gb.eq(a).width(e[a]);u()}else Y.append(G),F.css(mb),Y.css(mb),m();F.triggerHandler("reflowed",[bb])}}function A(a){var b=O.css("border-"+a+"-width"),c=0;return b&&~b.indexOf("px")&&(c=parseInt(b,10)),c}function B(){var a,b=O.scrollTop(),c=0,d=S?R.outerHeight(!0):0,e=T?d:-d,f=bb.height(),g=F.offset(),h=0,i=0;if(P){var j=O.offset();c=g.top-j.top+b,S&&T&&(c+=d),h=A("left"),i=A("top"),c-=i}else a=g.top-I-f+J+L.horizontal;var k=o.scrollTop(),m=o.scrollLeft(),n=O.scrollLeft();return function(j){var p=F[0].offsetWidth<=0&&F[0].offsetHeight<=0;if(!p&&cb)return cb=!1,setTimeout(function(){F.triggerHandler("reflow")},1),null;if(p&&(cb=!0,!Q))return null;if("windowScroll"==j?(k=o.scrollTop(),m=o.scrollLeft()):"containerScroll"==j?(b=O.scrollTop(),n=O.scrollLeft()):"init"!=j&&(k=o.scrollTop(),m=o.scrollLeft(),b=O.scrollTop(),n=O.scrollLeft()),!l||!(0>k||0>m)){if(X)x("windowScrollDone"==j?!0:!1);else if("windowScrollDone"==j)return null;g=F.offset(),S&&T&&(g.top+=d);var q,r,s=F.outerHeight();if(P&&Q){if(c>=b){var t=c-b+i;q=t>0?t:0,w(!1)}else q=V?i:b,w(!0);r=h}else!P&&Q?(k>a+s+e?q=s-f+e:g.top>=k+I?(q=0,v(),w(!1)):(q=I+k-g.top+c+(T?d:0),u(),w(!0)),r=0):P&&!Q?(c>b||b-c>s?(q=g.top-k,v(),w(!1)):(q=g.top+b-k-c,u(),w(!0)),r=g.left+n-m):P||Q||(k>a+s+e?q=s+I-k+a+e:g.top>k+I?(q=g.top-k,u(),w(!1)):(q=I,w(!0)),r=g.left-m);return{top:q,left:r}}}}function C(){var a=null,b=null,c=null;return function(d,e,f){null==d||a==d.top&&b==d.left||(bb.css({top:d.top,left:d.left}),a=d.top,b=d.left),e&&q(),f&&m();var g=O.scrollLeft();Q&&c==g||(bb.scrollLeft(g),c=g)}}function D(){if(O.length)if(O.data().perfectScrollbar)L={horizontal:0,vertical:0};else{var a=O.width(),b=O.height(),c=F.height(),d=g(F,ib),e=d>a?M:0,f=c>b?M:0;L.horizontal=d>a-f?M:0,L.vertical=c>b-e?M:0}}var E=h.uniqueId(),F=a(this);if(F.data("floatThead-attached"))return!0;if(!F.is("table"))throw new Error('jQuery.floatThead must be run on a table element. ex: $("table").floatThead();');i=s.autoReflow&&i;var G=F.children("thead:first"),H=F.children("tbody:first");if(0==G.length||0==H.length)return F.data("floatThead-lazy",s),void F.unbind("reflow").one("reflow",function(){F.floatThead(s)});F.data("floatThead-lazy")&&F.unbind("reflow"),F.data("floatThead-lazy",!1);var I,J,K=!0,L={vertical:0,horizontal:0},M=e(),N=0,O=s.scrollContainer(F)||a([]),P=O.length>0,Q=null;"undefined"!=typeof s.useAbsolutePositioning&&(s.position="auto",s.useAbsolutePositioning&&(s.position=s.useAbsolutePositioning?"absolute":"fixed"),c("option 'useAbsolutePositioning' has been removed in v1.3.0, use `position:'"+s.position+"'` instead. See docs for more info: http://mkoryak.github.io/floatThead/#options")),"undefined"!=typeof s.scrollingTop&&(s.top=s.scrollingTop,c("option 'scrollingTop' has been renamed to 'top' in v1.3.0. See docs for more info: http://mkoryak.github.io/floatThead/#options")),"undefined"!=typeof s.scrollingBottom&&(s.bottom=s.scrollingBottom,c("option 'scrollingBottom' has been renamed to 'bottom' in v1.3.0. See docs for more info: http://mkoryak.github.io/floatThead/#options")),"auto"==s.position?Q=null:"fixed"==s.position?Q=!1:"absolute"==s.position?Q=!0:s.debug&&c('Invalid value given to "position" option, valid is "fixed", "absolute" and "auto". You passed: ',s.position),null==Q&&(Q=P);var R=F.find("caption"),S=1==R.length;if(S)var T="top"===(R.css("caption-side")||R.attr("align")||"top");var U=a(''),V=!1,W=a([]),X=9>=j&&!P&&Q,Y=a(""),Z=a(""),$=F.children("colgroup:first"),_=!0;0==$.length&&($=a(""),_=!1);var ab=a(''),bb=a(''),cb=!1,db=a(""),eb=a(''),fb=a([]),gb=a([]),hb=a([]),ib=a([]);db.append(eb),F.prepend($),n&&(U.append(ab),F.append(U)),Y.append(Z),bb.append(Y),s.copyTableClass&&Y.attr("class",F.attr("class")),Y.attr({cellpadding:F.attr("cellpadding"),cellspacing:F.attr("cellspacing"),border:F.attr("border")});var jb=F.css("display");if(Y.css({borderCollapse:F.css("borderCollapse"),border:F.css("border"),display:jb}),"none"==jb&&(cb=!0),Y.addClass(s.floatTableClass).css({margin:0,"border-bottom-width":0}),Q){var kb=function(a,b){var c=a.css("position"),d="relative"==c||"absolute"==c,e=a;if(!d||b){var f={paddingLeft:a.css("paddingLeft"),paddingRight:a.css("paddingRight")};bb.css(f),e=a.data("floatThead-containerWrap")||a.wrap("
").parent(),a.data("floatThead-containerWrap",e),V=!0}return e};P?(W=kb(O,!0),W.prepend(bb)):(W=kb(F),F.before(bb))}else F.before(bb);bb.css({position:Q?"absolute":"fixed",marginTop:0,top:Q?0:"auto",zIndex:s.zIndex}),bb.addClass(s.floatContainerClass),r();var lb={"table-layout":"fixed"},mb={"table-layout":F.css("tableLayout")||"auto"},nb=F[0].style.width||"",ob=F.css("minWidth")||"",pb=!1;D();var qb,rb=function(){(qb=z())()};rb();var sb=B(),tb=C();tb(sb("init"),!0);var ub=h.debounce(function(){tb(sb("windowScrollDone"),!1)},1),vb=function(){tb(sb("windowScroll"),!1),X&&ub()},wb=function(){tb(sb("containerScroll"),!1)},xb=function(){F.is(":hidden")||(r(),D(),rb(),sb=B(),(tb=C())(sb("resize"),!0,!0))},yb=h.debounce(function(){F.is(":hidden")||(D(),r(),rb(),sb=B(),tb(sb("reflow"),!0))},1);if(P?Q?O.on(k("scroll"),wb):(O.on(k("scroll"),wb),o.on(k("scroll"),vb)):o.on(k("scroll"),vb),o.on(k("load"),yb),b(k("resize"),xb),F.on("reflow",yb),f(F)&&F.on("filter",yb).on("sort",yb).on("page",yb),o.on(k("shown.bs.tab"),yb),o.on(k("tabsactivate"),yb),i){var zb=null;h.isFunction(s.autoReflow)&&(zb=s.autoReflow(F,O)),zb||(zb=O.length?O[0]:F[0]),p=new MutationObserver(function(a){for(var b=function(a){return a&&a[0]&&("THEAD"==a[0].nodeName||"TD"==a[0].nodeName||"TH"==a[0].nodeName)},c=0;ctable>thead").add(F.children("tbody,tfoot")):F.children("thead,tbody,tfoot")}})}),this}}(jQuery),function(a){a.floatThead=a.floatThead||{},a.floatThead._=window._||function(){var b={},c=Object.prototype.hasOwnProperty,d=["Arguments","Function","String","Number","Date","RegExp"];b.has=function(a,b){return c.call(a,b)},b.keys=function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[];for(var d in a)b.has(a,d)&&c.push(d);return c};var e=0;return b.uniqueId=function(a){var b=++e+"";return a?a+b:b},a.each(d,function(){var a=this;b["is"+a]=function(b){return Object.prototype.toString.call(b)=="[object "+a+"]"}}),b.debounce=function(a,b,c){var d,e,f,g,h;return function(){f=this,e=arguments,g=new Date;var i=function(){var j=new Date-g;b>j?d=setTimeout(i,b-j):(d=null,c||(h=a.apply(f,e)))},j=c&&!d;return d||(d=setTimeout(i,b)),j&&(h=a.apply(f,e)),h}},b}()}(jQuery); \ No newline at end of file diff --git a/module/plugins/system/htdocs/js/multiselect.js b/module/plugins/system/htdocs/js/multiselect.js new file mode 100644 index 00000000..d7d476bf --- /dev/null +++ b/module/plugins/system/htdocs/js/multiselect.js @@ -0,0 +1,1716 @@ +/** + * Bootstrap Multiselect (https://github.com/davidstutz/bootstrap-multiselect) + * + * Apache License, Version 2.0: + * Copyright (c) 2012 - 2015 David Stutz + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * BSD 3-Clause License: + * Copyright (c) 2012 - 2015 David Stutz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of David Stutz nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +!function ($) { + "use strict";// jshint ;_; + + if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) { + ko.bindingHandlers.multiselect = { + after: ['options', 'value', 'selectedOptions', 'enable', 'disable'], + + init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var $element = $(element); + var config = ko.toJS(valueAccessor()); + + $element.multiselect(config); + + if (allBindings.has('options')) { + var options = allBindings.get('options'); + if (ko.isObservable(options)) { + ko.computed({ + read: function() { + options(); + setTimeout(function() { + var ms = $element.data('multiselect'); + if (ms) + ms.updateOriginalOptions();//Not sure how beneficial this is. + $element.multiselect('rebuild'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }); + } + } + + //value and selectedOptions are two-way, so these will be triggered even by our own actions. + //It needs some way to tell if they are triggered because of us or because of outside change. + //It doesn't loop but it's a waste of processing. + if (allBindings.has('value')) { + var value = allBindings.get('value'); + if (ko.isObservable(value)) { + ko.computed({ + read: function() { + value(); + setTimeout(function() { + $element.multiselect('refresh'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } + } + + //Switched from arrayChange subscription to general subscription using 'refresh'. + //Not sure performance is any better using 'select' and 'deselect'. + if (allBindings.has('selectedOptions')) { + var selectedOptions = allBindings.get('selectedOptions'); + if (ko.isObservable(selectedOptions)) { + ko.computed({ + read: function() { + selectedOptions(); + setTimeout(function() { + $element.multiselect('refresh'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } + } + + var setEnabled = function (enable) { + setTimeout(function () { + if (enable) + $element.multiselect('enable'); + else + $element.multiselect('disable'); + }); + }; + + if (allBindings.has('enable')) { + var enable = allBindings.get('enable'); + if (ko.isObservable(enable)) { + ko.computed({ + read: function () { + setEnabled(enable()); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } else { + setEnabled(enable); + } + } + + if (allBindings.has('disable')) { + var disable = allBindings.get('disable'); + if (ko.isObservable(disable)) { + ko.computed({ + read: function () { + setEnabled(!disable()); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } else { + setEnabled(!disable); + } + } + + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + $element.multiselect('destroy'); + }); + }, + + update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var $element = $(element); + var config = ko.toJS(valueAccessor()); + + $element.multiselect('setOptions', config); + $element.multiselect('rebuild'); + } + }; + } + + function forEach(array, callback) { + for (var index = 0; index < array.length; ++index) { + callback(array[index], index); + } + } + + /** + * Constructor to create a new multiselect using the given select. + * + * @param {jQuery} select + * @param {Object} options + * @returns {Multiselect} + */ + function Multiselect(select, options) { + + this.$select = $(select); + this.options = this.mergeOptions($.extend({}, options, this.$select.data())); + + // Placeholder via data attributes + if (this.$select.attr("data-placeholder")) { + this.options.nonSelectedText = this.$select.data("placeholder"); + } + + // Initialization. + // We have to clone to create a new reference. + this.originalOptions = this.$select.clone()[0].options; + this.query = ''; + this.searchTimeout = null; + this.lastToggledInput = null; + + this.options.multiple = this.$select.attr('multiple') === "multiple"; + this.options.onChange = $.proxy(this.options.onChange, this); + this.options.onSelectAll = $.proxy(this.options.onSelectAll, this); + this.options.onDeselectAll = $.proxy(this.options.onDeselectAll, this); + this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this); + this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this); + this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this); + this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this); + this.options.onInitialized = $.proxy(this.options.onInitialized, this); + this.options.onFiltering = $.proxy(this.options.onFiltering, this); + + // Build select all if enabled. + this.buildContainer(); + this.buildButton(); + this.buildDropdown(); + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.wasDisabled = this.$select.prop('disabled'); + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + + this.$select.wrap('').after(this.$container); + this.options.onInitialized(this.$select, this.$container); + } + + Multiselect.prototype = { + + defaults: { + /** + * Default text function will either print 'None selected' in case no + * option is selected or a list of the selected options up to a length + * of 3 selected options. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {String} + */ + buttonText: function(options, select) { + if (this.disabledText.length > 0 + && (select.prop('disabled') || (options.length == 0 && this.disableIfEmpty))) { + + return this.disabledText; + } + else if (options.length === 0) { + return this.nonSelectedText; + } + else if (this.allSelectedText + && options.length === $('option', $(select)).length + && $('option', $(select)).length !== 1 + && this.multiple) { + + if (this.selectAllNumber) { + return this.allSelectedText + ' (' + options.length + ')'; + } + else { + return this.allSelectedText; + } + } + else if (options.length > this.numberDisplayed) { + return options.length + ' ' + this.nSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function() { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + /** + * Updates the title of the button similar to the buttonText function. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {@exp;selected@call;substr} + */ + buttonTitle: function(options, select) { + if (options.length === 0) { + return this.nonSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function () { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + checkboxName: function(option) { + return false; // no checkbox name + }, + /** + * Create a label. + * + * @param {jQuery} element + * @returns {String} + */ + optionLabel: function(element){ + return $(element).attr('label') || $(element).text(); + }, + /** + * Create a class. + * + * @param {jQuery} element + * @returns {String} + */ + optionClass: function(element) { + return $(element).attr('class') || ''; + }, + /** + * Triggered on change of the multiselect. + * + * Not triggered when selecting/deselecting options manually. + * + * @param {jQuery} option + * @param {Boolean} checked + */ + onChange : function(option, checked) { + + }, + /** + * Triggered when the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShow: function(event) { + + }, + /** + * Triggered when the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHide: function(event) { + + }, + /** + * Triggered after the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShown: function(event) { + + }, + /** + * Triggered after the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHidden: function(event) { + + }, + /** + * Triggered on select all. + */ + onSelectAll: function() { + + }, + /** + * Triggered on deselect all. + */ + onDeselectAll: function() { + + }, + /** + * Triggered after initializing. + * + * @param {jQuery} $select + * @param {jQuery} $container + */ + onInitialized: function($select, $container) { + + }, + /** + * Triggered on filtering. + * + * @param {jQuery} $filter + */ + onFiltering: function($filter) { + + }, + enableHTML: false, + buttonClass: 'btn btn-default', + inheritClass: false, + buttonWidth: 'auto', + buttonContainer: '
', + dropRight: false, + dropUp: false, + selectedClass: 'active', + // Maximum height of the dropdown menu. + // If maximum height is exceeded a scrollbar will be displayed. + maxHeight: false, + includeSelectAllOption: false, + includeSelectAllIfMoreThan: 0, + selectAllText: ' Select all', + selectAllValue: 'multiselect-all', + selectAllName: false, + selectAllNumber: true, + selectAllJustVisible: true, + enableFiltering: false, + enableCaseInsensitiveFiltering: false, + enableFullValueFiltering: false, + enableClickableOptGroups: false, + enableCollapsibleOptGroups: false, + filterPlaceholder: 'Search', + // possible options: 'text', 'value', 'both' + filterBehavior: 'text', + includeFilterClearBtn: true, + preventInputChangeEvent: false, + nonSelectedText: 'None selected', + nSelectedText: 'selected', + allSelectedText: 'All selected', + numberDisplayed: 3, + disableIfEmpty: false, + disabledText: '', + delimiterText: ', ', + templates: { + button: '', + ul: '', + filter: '
  • ', + filterClearBtn: '', + li: '
  • ', + divider: '
  • ', + liGroup: '
  • ' + } + }, + + constructor: Multiselect, + + /** + * Builds the container of the multiselect. + */ + buildContainer: function() { + this.$container = $(this.options.buttonContainer); + this.$container.on('show.bs.dropdown', this.options.onDropdownShow); + this.$container.on('hide.bs.dropdown', this.options.onDropdownHide); + this.$container.on('shown.bs.dropdown', this.options.onDropdownShown); + this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden); + }, + + /** + * Builds the button of the multiselect. + */ + buildButton: function() { + this.$button = $(this.options.templates.button).addClass(this.options.buttonClass); + if (this.$select.attr('class') && this.options.inheritClass) { + this.$button.addClass(this.$select.attr('class')); + } + // Adopt active state. + if (this.$select.prop('disabled')) { + this.disable(); + } + else { + this.enable(); + } + + // Manually add button width if set. + if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') { + this.$button.css({ + 'width' : '100%', //this.options.buttonWidth, + 'overflow' : 'hidden', + 'text-overflow' : 'ellipsis' + }); + this.$container.css({ + 'width': this.options.buttonWidth + }); + } + + // Keep the tab index from the select. + var tabindex = this.$select.attr('tabindex'); + if (tabindex) { + this.$button.attr('tabindex', tabindex); + } + + this.$container.prepend(this.$button); + }, + + /** + * Builds the ul representing the dropdown menu. + */ + buildDropdown: function() { + + // Build ul. + this.$ul = $(this.options.templates.ul); + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + + // Set max height of dropdown menu to activate auto scrollbar. + if (this.options.maxHeight) { + // TODO: Add a class for this option to move the css declarations. + this.$ul.css({ + 'max-height': this.options.maxHeight + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden' + }); + } + + if (this.options.dropUp) { + + var height = Math.min(this.options.maxHeight, $('option[data-role!="divider"]', this.$select).length*26 + $('option[data-role="divider"]', this.$select).length*19 + (this.options.includeSelectAllOption ? 26 : 0) + (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering ? 44 : 0)); + var moveCalc = height + 34; + + this.$ul.css({ + 'max-height': height + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'hidden', + 'margin-top': "-" + moveCalc + 'px' + }); + } + + this.$container.append(this.$ul); + }, + + /** + * Build the dropdown options and binds all necessary events. + * + * Uses createDivider and createOptionValue to create the necessary options. + */ + buildDropdownOptions: function() { + + this.$select.children().each($.proxy(function(index, element) { + + var $element = $(element); + // Support optgroups and options without a group simultaneously. + var tag = $element.prop('tagName') + .toLowerCase(); + + if ($element.prop('value') === this.options.selectAllValue) { + return; + } + + if (tag === 'optgroup') { + this.createOptgroup(element); + } + else if (tag === 'option') { + + if ($element.data('role') === 'divider') { + this.createDivider(); + } + else { + this.createOptionValue(element); + } + + } + + // Other illegal tags will be ignored. + }, this)); + + // Bind the change event on the dropdown elements. + $('li:not(.multiselect-group) input', this.$ul).on('change', $.proxy(function(event) { + var $target = $(event.target); + + var checked = $target.prop('checked') || false; + var isSelectAllOption = $target.val() === this.options.selectAllValue; + + // Apply or unapply the configured selected class. + if (this.options.selectedClass) { + if (checked) { + $target.closest('li') + .addClass(this.options.selectedClass); + } + else { + $target.closest('li') + .removeClass(this.options.selectedClass); + } + } + + // Get the corresponding option. + var value = $target.val(); + var $option = this.getOptionByValue(value); + + var $optionsNotThis = $('option', this.$select).not($option); + var $checkboxesNotThis = $('input', this.$container).not($target); + + if (isSelectAllOption) { + + if (checked) { + this.selectAll(this.options.selectAllJustVisible, true); + } + else { + this.deselectAll(this.options.selectAllJustVisible, true); + } + } + else { + if (checked) { + $option.prop('selected', true); + + if (this.options.multiple) { + // Simply select additional option. + $option.prop('selected', true); + } + else { + // Unselect all other options and corresponding checkboxes. + if (this.options.selectedClass) { + $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass); + } + + $($checkboxesNotThis).prop('checked', false); + $optionsNotThis.prop('selected', false); + + // It's a single selection, so close. + this.$button.click(); + } + + if (this.options.selectedClass === "active") { + $optionsNotThis.closest("a").css("outline", ""); + } + } + else { + // Unselect option. + $option.prop('selected', false); + } + + // To prevent select all from firing onChange: #575 + this.options.onChange($option, checked); + + // Do not update select all or optgroups on select all change! + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + } + + this.$select.change(); + this.updateButtonText(); + + if(this.options.preventInputChangeEvent) { + return false; + } + }, this)); + + $('li a', this.$ul).on('mousedown', function(e) { + if (e.shiftKey) { + // Prevent selecting text by Shift+click + return false; + } + }); + + $('li a', this.$ul).on('touchstart click', $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + + if (event.shiftKey && this.options.multiple) { + if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431) + event.preventDefault(); + $target = $target.find("input"); + $target.prop("checked", !$target.prop("checked")); + } + var checked = $target.prop('checked') || false; + + if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range + var from = $target.closest("li").index(); + var to = this.lastToggledInput.closest("li").index(); + + if (from > to) { // Swap the indices + var tmp = to; + to = from; + from = tmp; + } + + // Make sure we grab all elements since slice excludes the last index + ++to; + + // Change the checkboxes and underlying options + var range = this.$ul.find("li").slice(from, to).find("input"); + + range.prop('checked', checked); + + if (this.options.selectedClass) { + range.closest('li') + .toggleClass(this.options.selectedClass, checked); + } + + for (var i = 0, j = range.length; i < j; i++) { + var $checkbox = $(range[i]); + + var $option = this.getOptionByValue($checkbox.val()); + + $option.prop('selected', checked); + } + } + + // Trigger the select "change" event + $target.trigger("change"); + } + + // Remembers last clicked option + if($target.is("input") && !$target.closest("li").is(".multiselect-item")){ + this.lastToggledInput = $target; + } + + $target.blur(); + }, this)); + + // Keyboard support. + this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) { + if ($('input[type="text"]', this.$container).is(':focus')) { + return; + } + + if (event.keyCode === 9 && this.$container.hasClass('open')) { + this.$button.click(); + } + else { + var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible"); + + if (!$items.length) { + return; + } + + var index = $items.index($items.filter(':focus')); + + // Navigation up. + if (event.keyCode === 38 && index > 0) { + index--; + } + // Navigate down. + else if (event.keyCode === 40 && index < $items.length - 1) { + index++; + } + else if (!~index) { + index = 0; + } + + var $current = $items.eq(index); + $current.focus(); + + if (event.keyCode === 32 || event.keyCode === 13) { + var $checkbox = $current.find('input'); + + $checkbox.prop("checked", !$checkbox.prop("checked")); + $checkbox.change(); + } + + event.stopPropagation(); + event.preventDefault(); + } + }, this)); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $("li.multiselect-group input", this.$ul).on("change", $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + var checked = $target.prop('checked') || false; + + var $li = $(event.target).closest('li'); + var $group = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden') + .not('.disabled'); + + var $inputs = $group.find("input"); + + var values = []; + var $options = []; + + if (this.options.selectedClass) { + if (checked) { + $li.addClass(this.options.selectedClass); + } + else { + $li.removeClass(this.options.selectedClass); + } + } + + $.each($inputs, $.proxy(function(index, input) { + var value = $(input).val(); + var $option = this.getOptionByValue(value); + + if (checked) { + $(input).prop('checked', true); + $(input).closest('li') + .addClass(this.options.selectedClass); + + $option.prop('selected', true); + } + else { + $(input).prop('checked', false); + $(input).closest('li') + .removeClass(this.options.selectedClass); + + $option.prop('selected', false); + } + + $options.push(this.getOptionByValue(value)); + }, this)) + + // Cannot use select or deselect here because it would call updateOptGroups again. + + this.options.onChange($options, checked); + + this.updateButtonText(); + this.updateSelectAll(); + }, this)); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $("li.multiselect-group .caret-container", this.$ul).on("click", $.proxy(function(event) { + var $li = $(event.target).closest('li'); + var $inputs = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden'); + + var visible = true; + $inputs.each(function() { + visible = visible && $(this).is(':visible'); + }); + + if (visible) { + $inputs.hide() + .addClass('multiselect-collapsible-hidden'); + } + else { + $inputs.show() + .removeClass('multiselect-collapsible-hidden'); + } + }, this)); + + $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea'); + $("li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px'); + $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px'); + } + }, + + /** + * Create an option using the given select option. + * + * @param {jQuery} element + */ + createOptionValue: function(element) { + var $element = $(element); + if ($element.is(':selected')) { + $element.prop('selected', true); + } + + // Support the label attribute on options. + var label = this.options.optionLabel(element); + var classes = this.options.optionClass(element); + var value = $element.val(); + var inputType = this.options.multiple ? "checkbox" : "radio"; + + var $li = $(this.options.templates.li); + var $label = $('label', $li); + $label.addClass(inputType); + $li.addClass(classes); + + if (this.options.enableHTML) { + $label.html(" " + label); + } + else { + $label.text(" " + label); + } + + var $checkbox = $('').attr('type', inputType); + + var name = this.options.checkboxName($element); + if (name) { + $checkbox.attr('name', name); + } + + $label.prepend($checkbox); + + var selected = $element.prop('selected') || false; + $checkbox.val(value); + + if (value === this.options.selectAllValue) { + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + } + + $label.attr('title', $element.attr('title')); + + this.$ul.append($li); + + if ($element.is(':disabled')) { + $checkbox.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('a') + .attr("tabindex", "-1") + .closest('li') + .addClass('disabled'); + } + + $checkbox.prop('checked', selected); + + if (selected && this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + }, + + /** + * Creates a divider using the given select option. + * + * @param {jQuery} element + */ + createDivider: function(element) { + var $divider = $(this.options.templates.divider); + this.$ul.append($divider); + }, + + /** + * Creates an optgroup. + * + * @param {jQuery} group + */ + createOptgroup: function(group) { + var label = $(group).attr("label"); + var value = $(group).attr("value"); + var $li = $('
  • '); + + var classes = this.options.optionClass(group); + $li.addClass(classes); + + if (this.options.enableHTML) { + $('label b', $li).html(" " + label); + } + else { + $('label b', $li).text(" " + label); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $('a', $li).append(''); + } + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $('a label', $li).prepend(''); + } + + if ($(group).is(':disabled')) { + $li.addClass('disabled'); + } + + this.$ul.append($li); + + $("option", group).each($.proxy(function($, group) { + this.createOptionValue(group); + }, this)) + }, + + /** + * Build the select all. + * + * Checks if a select all has already been created. + */ + buildSelectAll: function() { + if (typeof this.options.selectAllValue === 'number') { + this.options.selectAllValue = this.options.selectAllValue.toString(); + } + + var alreadyHasSelectAll = this.hasSelectAll(); + + if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple + && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) { + + // Check whether to add a divider after the select all. + if (this.options.includeSelectAllDivider) { + this.$ul.prepend($(this.options.templates.divider)); + } + + var $li = $(this.options.templates.li); + $('label', $li).addClass("checkbox"); + + if (this.options.enableHTML) { + $('label', $li).html(" " + this.options.selectAllText); + } + else { + $('label', $li).text(" " + this.options.selectAllText); + } + + if (this.options.selectAllName) { + $('label', $li).prepend(''); + } + else { + $('label', $li).prepend(''); + } + + var $checkbox = $('input', $li); + $checkbox.val(this.options.selectAllValue); + + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + + this.$ul.prepend($li); + + $checkbox.prop('checked', false); + } + }, + + /** + * Builds the filter. + */ + buildFilter: function() { + + // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength. + if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) { + var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering); + + if (this.$select.find('option').length >= enableFilterLength) { + + this.$filter = $(this.options.templates.filter); + $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder); + + // Adds optional filter clear button + if(this.options.includeFilterClearBtn) { + var clearBtn = $(this.options.templates.filterClearBtn); + clearBtn.on('click', $.proxy(function(event){ + clearTimeout(this.searchTimeout); + + this.$filter.find('.multiselect-search').val(''); + $('li', this.$ul).show().removeClass('multiselect-filter-hidden'); + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + }, this)); + this.$filter.find('.input-group').append(clearBtn); + } + + this.$ul.prepend(this.$filter); + + this.$filter.val(this.query).on('click', function(event) { + event.stopPropagation(); + }).on('input keydown', $.proxy(function(event) { + // Cancel enter key default behaviour + if (event.which === 13) { + event.preventDefault(); + } + + // This is useful to catch "keydown" events after the browser has updated the control. + clearTimeout(this.searchTimeout); + + this.searchTimeout = this.asyncFunction($.proxy(function() { + + if (this.query !== event.target.value) { + this.query = event.target.value; + + var currentGroup, currentGroupVisible; + $.each($('li', this.$ul), $.proxy(function(index, element) { + var value = $('input', element).length > 0 ? $('input', element).val() : ""; + var text = $('label', element).text(); + + var filterCandidate = ''; + if ((this.options.filterBehavior === 'text')) { + filterCandidate = text; + } + else if ((this.options.filterBehavior === 'value')) { + filterCandidate = value; + } + else if (this.options.filterBehavior === 'both') { + filterCandidate = text + '\n' + value; + } + + if (value !== this.options.selectAllValue && text) { + + // By default lets assume that element is not + // interesting for this search. + var showElement = false; + + if (this.options.enableCaseInsensitiveFiltering) { + filterCandidate = filterCandidate.toLowerCase(); + this.query = this.query.toLowerCase(); + } + + if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') { + var valueToMatch = filterCandidate.trim().substring(0, this.query.length); + if (this.query.indexOf(valueToMatch) > -1) { + showElement = true; + } + } + else if (filterCandidate.indexOf(this.query) > -1) { + showElement = true; + } + + // Toggle current element (group or group item) according to showElement boolean. + $(element).toggle(showElement) + .toggleClass('multiselect-filter-hidden', !showElement); + + // Differentiate groups and group items. + if ($(element).hasClass('multiselect-group')) { + // Remember group status. + currentGroup = element; + currentGroupVisible = showElement; + } + else { + // Show group name when at least one of its items is visible. + if (showElement) { + $(currentGroup).show() + .removeClass('multiselect-filter-hidden'); + } + + // Show all group items when group name satisfies filter. + if (!showElement && currentGroupVisible) { + $(element).show() + .removeClass('multiselect-filter-hidden'); + } + } + } + }, this)); + } + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.onFiltering(event.target); + + }, this), 300, this); + }, this)); + } + } + }, + + /** + * Unbinds the whole plugin. + */ + destroy: function() { + this.$container.remove(); + this.$select.show(); + + // reset original state + this.$select.prop('disabled', this.options.wasDisabled); + + this.$select.data('multiselect', null); + }, + + /** + * Refreshs the multiselect based on the selected options of the select. + */ + refresh: function () { + var inputs = $.map($('li input', this.$ul), $); + + $('option', this.$select).each($.proxy(function (index, element) { + var $elem = $(element); + var value = $elem.val(); + var $input; + for (var i = inputs.length; 0 < i--; /**/) { + if (value !== ($input = inputs[i]).val()) + continue; // wrong li + + if ($elem.is(':selected')) { + $input.prop('checked', true); + + if (this.options.selectedClass) { + $input.closest('li') + .addClass(this.options.selectedClass); + } + } + else { + $input.prop('checked', false); + + if (this.options.selectedClass) { + $input.closest('li') + .removeClass(this.options.selectedClass); + } + } + + if ($elem.is(":disabled")) { + $input.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('li') + .addClass('disabled'); + } + else { + $input.prop('disabled', false) + .closest('li') + .removeClass('disabled'); + } + break; // assumes unique values + } + }, this)); + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Select all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered if + * and only if one value is passed. + * + * @param {Array} selectValues + * @param {Boolean} triggerOnChange + */ + select: function(selectValues, triggerOnChange) { + if(!$.isArray(selectValues)) { + selectValues = [selectValues]; + } + + for (var i = 0; i < selectValues.length; i++) { + var value = selectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (!this.options.multiple) { + this.deselectAll(false); + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + + $checkbox.prop('checked', true); + $option.prop('selected', true); + + if (triggerOnChange) { + this.options.onChange($option, true); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Clears all selected items. + */ + clearSelection: function () { + this.deselectAll(false); + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Deselects all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered, if + * and only if one value is passed. + * + * @param {Array} deselectValues + * @param {Boolean} triggerOnChange + */ + deselect: function(deselectValues, triggerOnChange) { + if(!$.isArray(deselectValues)) { + deselectValues = [deselectValues]; + } + + for (var i = 0; i < deselectValues.length; i++) { + var value = deselectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .removeClass(this.options.selectedClass); + } + + $checkbox.prop('checked', false); + $option.prop('selected', false); + + if (triggerOnChange) { + this.options.onChange($option, false); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Selects all enabled & visible options. + * + * If justVisible is true or not specified, only visible options are selected. + * + * @param {Boolean} justVisible + * @param {Boolean} triggerOnSelectAll + */ + selectAll: function (justVisible, triggerOnSelectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input:enabled' , visibleLis).prop('checked', true); + visibleLis.addClass(this.options.selectedClass); + + $('input:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + else { + $('input:enabled' , allLis).prop('checked', true); + allLis.addClass(this.options.selectedClass); + + $('input:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnSelectAll) { + this.options.onSelectAll(); + } + }, + + /** + * Deselects all options. + * + * If justVisible is true or not specified, only visible options are deselected. + * + * @param {Boolean} justVisible + */ + deselectAll: function (justVisible, triggerOnDeselectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input[type="checkbox"]:enabled' , visibleLis).prop('checked', false); + visibleLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + else { + $('input[type="checkbox"]:enabled' , allLis).prop('checked', false); + allLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', false); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnDeselectAll) { + this.options.onDeselectAll(); + } + }, + + /** + * Rebuild the plugin. + * + * Rebuilds the dropdown, the filter and the select all option. + */ + rebuild: function() { + this.$ul.html(''); + + // Important to distinguish between radios and checkboxes. + this.options.multiple = this.$select.attr('multiple') === "multiple"; + + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + else { + this.enable(); + } + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + }, + + /** + * The provided data will be used to build the dropdown. + */ + dataprovider: function(dataprovider) { + + var groupCounter = 0; + var $select = this.$select.empty(); + + $.each(dataprovider, function (index, option) { + var $tag; + + if ($.isArray(option.children)) { // create optiongroup tag + groupCounter++; + + $tag = $('').attr({ + label: option.label || 'Group ' + groupCounter, + disabled: !!option.disabled + }); + + forEach(option.children, function(subOption) { // add children option tags + var attributes = { + value: subOption.value, + label: subOption.label || subOption.value, + title: subOption.title, + selected: !!subOption.selected, + disabled: !!subOption.disabled + }; + + //Loop through attributes object and add key-value for each attribute + for (var key in subOption.attributes) { + attributes['data-' + key] = subOption.attributes[key]; + } + //Append original attributes + new data attributes to option + $tag.append($('
    @@ -45,7 +43,7 @@ %end - + %# End of this satellite %end diff --git a/module/regenerator.py b/module/regenerator.py index 28235b07..c2f01420 100644 --- a/module/regenerator.py +++ b/module/regenerator.py @@ -1481,7 +1481,7 @@ def manage_host_check_result_brok(self, b): h = self.hosts.find_by_name(hname) if not h: - logger.warning("Got a check result brok for an unknown host: %s", hname) + logger.debug("Got a check result brok for an unknown host: %s", hname) return logger.debug("Host check result: %s - %s (%s)", hname, h.state, h.state_type) @@ -1506,7 +1506,7 @@ def manage_service_check_result_brok(self, b): sdesc = data['service_description'] s = self.services.find_srv_by_name_and_hostname(hname, sdesc) if not s: - logger.warning("Got a check result brok for an unknown service: %s/%s", hname, sdesc) + logger.debug("Got a check result brok for an unknown service: %s/%s", hname, sdesc) return logger.debug("Service check result: %s/%s - %s (%s)", hname, sdesc, s.state, s.state_type) diff --git a/module/views/header_element.tpl b/module/views/header_element.tpl index 5d2d6a43..fcf5147a 100644 --- a/module/views/header_element.tpl +++ b/module/views/header_element.tpl @@ -239,8 +239,13 @@
  • {{helper.print_duration(s.last_check, just_duration=True, x_elts=2)}}{{s.realm}}{{s.realm_name}}
    - + - + %for log in records: + %if not app.alignak: + %else: + + %for field in other_fields: + %if '.' in field: + %before = field.split('.')[0] + %after = field.split('.')[1] + %before_value = log.get(before, None) + %value = before_value.get(after, '') if before else '' + %else: + %value = log.get(field, '') + %end + + %end + %end %end @@ -148,7 +159,14 @@ diff --git a/module/submodules/logs.py b/module/submodules/logs.py index 908ea760..8ba7c654 100644 --- a/module/submodules/logs.py +++ b/module/submodules/logs.py @@ -4,6 +4,7 @@ import traceback import time +import datetime import pymongo @@ -33,15 +34,24 @@ def __init__(self, modules, app): len(modules), modules[0]) self.module = modules[0] else: - try: - self.module = MongoDBLogs(app.modconf) - except Exception as exp: - logger.warning('[WebUI] Exception %s', str(exp)) + if self.app.alignak: + try: + self.module = AlignakLogs(app.modconf) + except Exception as exp: + logger.warning('[WebUI] Exception %s', str(exp)) + else: + try: + self.module = MongoDBLogs(app.modconf) + except Exception as exp: + logger.warning('[WebUI] Exception %s', str(exp)) def is_available(self): if isinstance(self.module, MongoDBLogs): return self.module.is_connected + if isinstance(self.module, AlignakLogs): + return self.module.is_connected + return self.module is not None def get_ui_logs(self, *args, **kwargs): @@ -146,7 +156,8 @@ def close(self): self.con.close() # We will get in the mongodb database the logs - def get_ui_logs(self, filters=None, range_start=None, range_end=None, limit=200, offset=0): + def get_ui_logs(self, filters=None, range_start=None, range_end=None, + limit=200, offset=0, time_field="time"): if not self.uri: return None @@ -163,13 +174,14 @@ def get_ui_logs(self, filters=None, range_start=None, range_end=None, limit=200, for k, v in filters.items(): query.append({k: v}) if range_start: - query.append({'time': {'$gte': range_start}}) + query.append({time_field: {'$gte': range_start}}) if range_end: - query.append({'time': {'$lte': range_end}}) + query.append({time_field: {'$lte': range_end}}) - query = {'$and': query} if query else None - logger.debug("[mongo-logs] Fetching %limit records from database with query: " - "'%s' and offset %s", limit, query, offset) + query = {'$and': query} if len(query) > 1 else query[0] + # query = { "_created": {"$gte" : datetime.datetime(2018, 1, 1), "$lt": datetime.datetime(2018, 12, 31)}} + logger.info("[mongo-logs] Fetching %d records from collection %s/%s with " + "query: '%s' and offset %s", limit, self.database, self.logs_collection, query, offset) records = [] try: @@ -257,3 +269,22 @@ def get_ui_availability(self, elt, range_start=None, range_end=None): record['first_check_state'] = log['first_check_state'] return record + + +class AlignakLogs(MongoDBLogs): + """ + This module job is to get Alignak logs from a mongodb database. + """ + + def __init__(self, mod_conf): + super(AlignakLogs, self).__init__(mod_conf=mod_conf) + + # We will get in the mongodb database the logs + def get_ui_logs(self, filters=None, range_start=None, range_end=None, + limit=200, offset=0, time_field="@timestamp"): + return super(AlignakLogs, self).get_ui_logs(filters=filters, range_start=range_start, range_end=range_end, + limit=limit, offset=offset, time_field=time_field) + + # We will get in the mongodb database the host availability + def get_ui_availability(self, elt, range_start=None, range_end=None): + logger.error("[WebUI-alignak-mongo-logs] availability feature is not yet available!") From a594e7c71194a0f2bd417d83dbaf9f16471dff90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Sun, 16 Dec 2018 09:21:59 +0100 Subject: [PATCH 05/31] Alignak system menu with the live state and the events log --- module/helper.py | 155 +++++++ module/logevent.py | 317 ++++++++++++++ module/plugins/system/system.py | 386 +++++++++++++++++- .../plugins/system/views/alignak_events.tpl | 129 ++++++ module/plugins/system/views/alignak_ls.tpl | 14 + .../plugins/system/views/alignak_status.tpl | 78 ++++ module/views/header_element.tpl | 18 + 7 files changed, 1096 insertions(+), 1 deletion(-) create mode 100755 module/logevent.py create mode 100644 module/plugins/system/views/alignak_events.tpl create mode 100644 module/plugins/system/views/alignak_ls.tpl create mode 100644 module/plugins/system/views/alignak_status.tpl diff --git a/module/helper.py b/module/helper.py index 7f5b571f..7583e43e 100644 --- a/module/helper.py +++ b/module/helper.py @@ -749,5 +749,160 @@ def get_contact_avatar(self, contact, size=24, with_name=True, with_link=True): return s + def get_event_icon(self, event, disabled=False, label='', use_title=True): + ''' + Get an Html formatted string to display a monitoring event + + If disabled is True, the font used is greyed + + If label is empty, only an icon is returned + If label is set as 'state', the icon title is used as text + Else, the content of label is used as text near the icon. + + If use_title is False, do not include title attribute. + + Returns a span element containing a Font Awesome icon that depicts + consistently the event and its state + ''' + cls = event.get('type', 'unknown').lower() + state = event.get('state', 'n/a').upper() + state_type = event.get('state_type', 'n/a').upper() + hard = (state_type == 'HARD') + + # Icons depending upon element and real state ... + # ; History + icons = { + "unknown": { + "class": "history_Unknown", + "text": "Unknown event", + "icon": "question" + }, + + "retention_load": { + "class": "history_RetentionLoad", + "text": "Retention load", + "icon": "save" + }, + "retention_save": { + "class": "history_RetentionSave", + "text": "Retention save", + "icon": "save" + }, + + "alert": { + "class": "history_Alert", + "text": "Monitoring alert", + "icon": "bolt" + }, + + "notification": { + "class": "history_Notification", + "text": "Monitoring notification sent", + "icon": "paper-plane" + }, + + "check_result": { + "class": "history_CheckResult", + "text": "Check result", + "icon": "bolt" + }, + + "comment": { + "class": "history_WebuiComment", + "text": "WebUI comment", + "icon": "send" + }, + "timeperiod_transition": { + "class": "history_TimeperiodTransition", + "text": "Timeperiod transition", + "icon": "clock-o" + }, + "external_command": { + "class": "history_ExternalCommand", + "text": "External command", + "icon": "wrench" + }, + + "event_handler": { + "class": "history_EventHandler", + "text": "Monitoring event handler", + "icon": "bolt" + }, + "flapping_start": { + "class": "history_FlappingStart", + "text": "Monitoring flapping start", + "icon": "flag" + }, + "flapping_stop": { + "class": "history_FlappingStop", + "text": "Monitoring flapping stop", + "icon": "flag-o" + }, + "downtime_start": { + "class": "history_DowntimeStart", + "text": "Monitoring downtime start", + "icon": "ambulance" + }, + "downtime_cancelled": { + "class": "history_DowntimeCancelled", + "text": "Monitoring downtime cancelled", + "icon": "ambulance" + }, + "downtime_end": { + "class": "history_DowntimeEnd", + "text": "Monitoring downtime stopped", + "icon": "ambulance" + }, + "acknowledge_start": { + "class": "history_AckStart", + "text": "Monitoring acknowledge start", + "icon": "check" + }, + "acknowledge_cancelled": { + "class": "history_AckCancelled", + "text": "Monitoring acknowledge cancelled", + "icon": "check" + }, + "acknowledge_end": { + "class": "history_AckEnd", + "text": "Monitoring acknowledge expired", + "icon": "check" + }, + } + + back = '''''' \ + % (state.lower() if not disabled else 'greyed') + + icon_color = 'fa-inverse' + icon_style = "" + if not hard: + icon_style = 'style="opacity: 0.5"' + + try: + icon = icons[cls]['icon'] + title = icons[cls]['text'] + except KeyError: + cls = 'unknown' + icon = icons[cls]['icon'] + title = icons[cls]['text'] + + front = '''''' % (icon, icon_color) + + if use_title: + icon_text = '''%s%s''' % (icon_style, title, back, front) + else: + icon_text = '''%s%s''' % (icon_style, back, front) + + if label == '': + return icon_text + + color = state.lower() if not disabled else 'greyed' + if label == 'title': + label = title + return ''' + + %s %s + + ''' % (color, icon_text, label) helper = Helper() diff --git a/module/logevent.py b/module/logevent.py new file mode 100755 index 00000000..0bde6d62 --- /dev/null +++ b/module/logevent.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2015-2016: Alignak team, see AUTHORS.txt file for contributors +# +# This file is part of Alignak. +# +# Alignak is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Alignak is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Alignak. If not, see . +# +# +# This file incorporates work covered by the following copyright and +# permission notice: +# +# Copyright (C) 2009-2014: +# Thibault Cohen, titilambert@gmail.com +# Grégory Starck, g.starck@gmail.com +# aviau, alexandre.viau@savoirfairelinux.com +# Sebastien Coavoux, s.coavoux@free.fr + +# This file is part of Shinken. +# +# Shinken is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Shinken is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Shinken. If not, see . +""" +This module lists provide facilities to parse log type Broks. +The supported event are listed in the event_type variable +""" + +import re + +# pylint: disable=bad-continuation +EVENT_TYPE_PATTERN = re.compile( + r'^(TIMEPERIOD TRANSITION|EXTERNAL COMMAND|RETENTION SAVE|RETENTION LOAD|' + r'CURRENT HOST STATE|CURRENT SERVICE STATE|HOST COMMENT|SERVICE COMMENT|' + r'HOST NOTIFICATION|SERVICE NOTIFICATION|HOST ALERT|SERVICE ALERT|' + r'HOST EVENT HANDLER|SERVICE EVENT HANDLER|ACTIVE HOST CHECK|ACTIVE SERVICE CHECK|' + r'PASSIVE HOST CHECK|PASSIVE SERVICE CHECK|HOST ACKNOWLEDGE ALERT|SERVICE ACKNOWLEDGE ALERT|' + r'HOST DOWNTIME ALERT|SERVICE DOWNTIME ALERT|HOST FLAPPING ALERT|SERVICE FLAPPING ALERT)' + r'($|: .*)' +) +EVENT_TYPES = { + 'TIMEPERIOD': { + # [1490998324] RETENTION SAVE + 'pattern': r'^(TIMEPERIOD) (TRANSITION): (.*)', + 'properties': [ + 'event_type', # 'TIMEPERIOD' + 'state_type', # 'TRANSITION' + 'output', + ] + }, + 'RETENTION': { + # [1490998324] RETENTION SAVE + 'pattern': r'^(RETENTION) (LOAD|SAVE): (.*)', + 'properties': [ + 'event_type', # 'RETENTION' + 'state_type', # 'LOAD' or 'SAVE' + 'output', # 'scheduler name + ] + }, + 'EXTERNAL': { + # [1490997636] EXTERNAL COMMAND: [1490997512] + # PROCESS_HOST_CHECK_RESULT;ek3022sg-0001;0;EK3022SG-0001 is alive, + # uptime is 43639 seconds (0 days 12 hours 7 minutes 19 seconds 229 ms)|'Uptime'=43639 + 'pattern': r'^(EXTERNAL COMMAND): (\[.*\]) (.*)$', + 'properties': [ + 'event_type', # 'EXTERNAL COMMAND' + 'timestamp', # '[1490997512]' + 'command', # 'PROCESS_SERVICE_CHECK_RESULT;ek3022sg-0001;svc_Screensaver;0;Ok|'ScreensaverOff'=61c + ] + }, + 'CURRENT': { + # ex: "[1498108167] CURRENT HOST STATE: localhost;UP;HARD;1;Host assumed to be UP" + # ex: "[1498108167] CURRENT SERVICE STATE: localhost;Maintenance;UNKNOWN;HARD;0;" + 'pattern': r'^CURRENT (HOST|SERVICE) (STATE): ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^\;]*);([^\;]*);([^\;]*)', + 'properties': [ + 'item_type', # 'SERVICE' (or could be 'HOST') + 'event_type', # 'STATE' + 'hostname', # 'localhost' + 'service_desc', # 'Maintenance' (or could be None) + 'state', # 'UP' + 'state_type', # 'HARD' + 'attempts', # '0' + 'output', # 'WARNING - load average: 5.04, 4.67, 5.04' + ] + }, + 'ACTIVE': { + # ex: "[1402515279] ACTIVE SERVICE CHECK: localhost;Nrpe-status;OK;HARD;1;NRPE v2.15" + 'pattern': r'^(ACTIVE) (HOST|SERVICE) (CHECK): ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^\;]*);([^\;]*);([^\;]*)', + 'properties': [ + 'check_type', # 'ACTIVE' + 'item_type', # 'SERVICE' (or could be 'HOST') + 'event_type', # 'CHECK' + 'hostname', # 'localhost' + 'service_desc', # 'cpu load maui' (or could be None) + 'state', # 'WARNING' + 'state_type', # 'HARD' + 'attempts', # '0' + 'output', # 'NRPE v2.15' + ] + }, + 'PASSIVE': { + # ex: "[1402515279] PASSIVE SERVICE CHECK: localhost;nsca_uptime;0;OK: uptime: 02:38h, + # boot: 2017-08-31 06:18:03 (UTC)|'uptime'=9508s;2100;90000" + 'pattern': r'^(PASSIVE) (HOST|SERVICE) (CHECK): ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^$]*)', + 'properties': [ + 'check_type', # 'PASSIVE' + 'item_type', # 'SERVICE' (or could be 'HOST') + 'event_type', # 'CHECK' + 'hostname', # 'localhost' + 'service_desc', # 'cpu load maui' (or could be None) + 'state_id', # '0' + 'output', # 'K: uptime: 02:38h, boot: 2017-08-31 06:18:03 (UTC) + # |'uptime'=9508s;2100;90000' + ] + }, + 'NOTIFICATION': { + # ex: "[1402515279] SERVICE NOTIFICATION: + # admin;localhost;check-ssh;CRITICAL;notify-service-by-email;Connection refused" + 'pattern': r'(HOST|SERVICE) (NOTIFICATION): ' + r'([^\;]*);([^\;]*);(?:([^\;]*);)?([^\;]*);([^\;]*);([^\;]*)', + 'properties': [ + 'item_type', # 'SERVICE' (or could be 'HOST') + 'event_type', # 'NOTIFICATION' + 'contact', # 'admin' + 'hostname', # 'localhost' + 'service_desc', # 'check-ssh' (or could be None) + 'state', # 'CRITICAL' + 'notification_method', # 'notify-service-by-email' + 'output', # 'Connection refused' + ] + }, + 'ALERT': { + # ex: "[1329144231] SERVICE ALERT: + # dfw01-is02-006;cpu load maui;WARNING;HARD;4;WARNING - load average: 5.04, 4.67, 5.04" + 'pattern': r'^(HOST|SERVICE) (ALERT): ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^\;]*);([^\;]*);([^\;]*)', + 'properties': [ + 'item_type', # 'SERVICE' (or could be 'HOST') + 'event_type', # 'ALERT' + 'hostname', # 'localhost' + 'service_desc', # 'cpu load maui' (or could be None) + 'state', # 'WARNING' + 'state_type', # 'HARD' + 'attempts', # '4' + 'output', # 'WARNING - load average: 5.04, 4.67, 5.04' + ] + }, + 'EVENT': { + # ex: "[1329144231] HOST EVENT HANDLER: host-03;DOWN;HARD;0;g_host_event_handler" + 'pattern': r'^(HOST|SERVICE) (EVENT HANDLER): ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^\;]*);([^\;]*);([^\;]*)', + 'properties': [ + 'item_type', # 'SERVICE' (or could be 'HOST') + 'event_type', # 'EVENT HANDLER' + 'hostname', # 'localhost' + 'service_desc', # 'cpu load maui' (or could be None) + 'state', # 'WARNING' + 'state_type', # 'HARD' + 'attempts', # '4' + 'output', # 'g_host_event_handler' + ] + }, + 'COMMENT': { + # ex: "[1329144231] SERVICE COMMENT: + # dfw01-is02-006;cpu load maui;author;Comment text" + 'pattern': r'^(HOST|SERVICE) (COMMENT): ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^$]*)', + 'properties': [ + 'item_type', # 'SERVICE' (or could be 'HOST') + 'event_type', # 'COMMENT' + 'hostname', # 'localhost' + 'service_desc', # 'cpu load maui' (or could be None) + 'author', + 'comment', # 'WARNING - load average: 5.04, 4.67, 5.04' + ] + }, + 'ACKNOWLEDGE': { + # ex: "[1279250211] HOST ACKNOWLEDGE STARTED: + # maast64;Host has been acknowledged" + 'pattern': r'^(HOST|SERVICE) (ACKNOWLEDGE) ALERT: ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^\;]*)', + 'properties': [ + 'item_type', # 'SERVICE' or 'HOST' + 'event_type', # 'ACKNOWLEDGE' + 'hostname', # The hostname + 'service_desc', # The service description or None + 'state', # 'STARTED' or 'EXPIRED' or 'CANCELLED' + 'output', # 'Host has been acknowledged' + ] + }, + 'DOWNTIME': { + # ex: "[1279250211] HOST DOWNTIME ALERT: + # maast64;STARTED; Host has entered a period of scheduled downtime" + 'pattern': r'^(HOST|SERVICE) (DOWNTIME) ALERT: ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^\;]*)', + 'properties': [ + 'item_type', # 'SERVICE' or 'HOST' + 'event_type', # 'DOWNTIME' + 'hostname', # The hostname + 'service_desc', # The service description or None + 'state', # 'STARTED' or 'STOPPED' or 'CANCELLED' + 'output', # 'Service appears to have started flapping (24% change >= 20.0% threshold)' + ] + }, + 'FLAPPING': { + # service flapping ex: "[1375301662] SERVICE FLAPPING ALERT: + # testhost;check_ssh;STARTED; + # Service appears to have started flapping (24.2% change >= 20.0% threshold)" + + # host flapping ex: "[1375301662] HOST FLAPPING ALERT: + # hostbw;STARTED; Host appears to have started flapping (20.1% change > 20.0% threshold)" + 'pattern': r'^(HOST|SERVICE) (FLAPPING) ALERT: ' + r'([^\;]*);(?:([^\;]*);)?([^\;]*);([^\;]*)', + 'properties': [ + 'item_type', # 'SERVICE' or 'HOST' + 'event_type', # 'FLAPPING' + 'hostname', # The hostname + 'service_desc', # The service description or None + 'state', # 'STOPPED' or 'STARTED' + 'output', # 'Service appears to have started flapping (24% change >= 20.0% threshold)' + ] + } +} + +# SERVICE ACKNOWLEDGE ALERT: south_host_004;dummy_critical;STARTED; Service problem has been acknowledged + +class LogEvent(object): # pylint: disable=too-few-public-methods + """Class for parsing event logs + Populates self.data with the log type's properties + """ + + def __init__(self, log): + self.data = {} + self.syntax = False + self.valid = False + self.time = None + self.event_type = 'unknown' + self.pattern = 'unknown' + + # Find the type of event + event_type_match = EVENT_TYPE_PATTERN.match(log) + if not event_type_match: + return + self.syntax = True + + matched = event_type_match.group(1) + # print("Matched: %s" % matched) + matched = matched.split() + self.pattern = matched[0] + if self.pattern in ['HOST', 'SERVICE']: + self.pattern = matched[1] + # print("Pattern: %s" % self.pattern) + + # parse it with it's pattern + if self.pattern not in EVENT_TYPES: + return + + event_type = EVENT_TYPES[self.pattern] + properties_match = re.match(event_type['pattern'], log) + # print("Properties math: %s" % properties_match) + if not properties_match: + return + + self.valid = True + + # Populate self.data with the event's properties + for i, prop in enumerate(event_type['properties']): + self.data[prop] = properties_match.group(i + 1) + + # # Convert the time to int + # self.data['time'] = int(self.data['time']) + + # Convert event_type to int + if 'event_type' in self.data: + self.event_type = self.data['event_type'] + + # Convert attempts to int + if 'attempts' in self.data: + self.data['attempts'] = int(self.data['attempts']) + + def __iter__(self): + return iter(self.data.items()) + + def __len__(self): + return len(self.data) + + def __getitem__(self, key): + return self.data[key] + + def __contains__(self, key): + return key in self.data + + def __str__(self): + return str(self.data) diff --git a/module/plugins/system/system.py b/module/plugins/system/system.py index 91234029..a320abd9 100644 --- a/module/plugins/system/system.py +++ b/module/plugins/system/system.py @@ -30,6 +30,7 @@ import traceback from copy import deepcopy +from logevent import LogEvent from shinken.log import logger @@ -37,6 +38,383 @@ app = None +def _get_alignak_livesynthesis(): + """Get Alignak livesynthesis from the Arbiter API: + { + "alignak": "My Alignak", + "livesynthesis": { + "_overall": { + "_freshness": 1534237749, + "livesynthesis": { + "hosts_acknowledged": 0, + "hosts_down_hard": 0, + "hosts_down_soft": 0, + "hosts_flapping": 0, + "hosts_in_downtime": 0, + "hosts_not_monitored": 0, + "hosts_total": 13, + "hosts_unreachable_hard": 0, + "hosts_unreachable_soft": 0, + "hosts_up_hard": 13, + "hosts_up_soft": 0, + "services_acknowledged": 0, + "services_critical_hard": 6, + "services_critical_soft": 4, + "services_flapping": 0, + "services_in_downtime": 0, + "services_not_monitored": 0, + "services_ok_hard": 70, + "services_ok_soft": 0, + "services_total": 100, + "services_unknown_hard": 4, + "services_unknown_soft": 6, + "services_unreachable_hard": 0, + "services_unreachable_soft": 0, + "services_warning_hard": 5, + "services_warning_soft": 5 + } + }, + "scheduler-master": { + "_freshness": 1534237747, + "livesynthesis": { + "hosts_acknowledged": 0, + "hosts_down_hard": 0, + "hosts_down_soft": 0, + "hosts_flapping": 0, + "hosts_in_downtime": 0, + "hosts_not_monitored": 0, + "hosts_total": 13, + "hosts_unreachable_hard": 0, + "hosts_unreachable_soft": 0, + "hosts_up_hard": 13, + "hosts_up_soft": 0, + "services_acknowledged": 0, + "services_critical_hard": 6, + "services_critical_soft": 4, + "services_flapping": 0, + "services_in_downtime": 0, + "services_not_monitored": 0, + "services_ok_hard": 70, + "services_ok_soft": 0, + "services_total": 100, + "services_unknown_hard": 4, + "services_unknown_soft": 6, + "services_unreachable_hard": 0, + "services_unreachable_soft": 0, + "services_warning_hard": 5, + "services_warning_soft": 5 + } + } + }, + "name": "arbiter-master", + "running_id": "1534237614.73657398", + "start_time": 1534237614, + "type": "arbiter", + "version": "2.0.0rc2" + } + """ + if not getattr(app, 'alignak_endpoint', None): + logger.info("[WebUI-system] Alignak is not configured. Redirecting to the home page.") + app.bottle.redirect(app.get_url("Dashboard")) + + logger.debug("[WebUI-system] Get Alignak livesynthesis, endpoint: %s", app.alignak_endpoint) + try: + req = requests.Session() + raw_data = req.get("%s/livesynthesis" % app.alignak_endpoint) + data = json.loads(raw_data.content) + logger.debug("[WebUI-system] Result: %s", data) + except Exception as exp: + logger.error("[WebUI-system] alignak_livesynthesis, exception: %s", exp) + app.request.environ['MSG'] = "Alignak Error" + app.bottle.redirect(app.get_url("Dashboard")) + + return data + + +def _get_alignak_status(): + """Get Alignak overall status from the Arbiter API: + { + "livestate": { + "long_output": "broker-master - daemon is alive and reachable.\npoller-master - daemon is alive and reachable.\nreactionner-master - daemon is not reachable.\nreceiver-master - daemon is alive and reachable.\nscheduler-master - daemon is alive and reachable.", + "output": "Some of my daemons are not reachable.", + "perf_data": "'modules'=2 'timeperiods'=4 'services'=100 'servicegroups'=1 'commands'=10 'hosts'=13 'hostgroups'=5 'contacts'=2 'contactgroups'=2 'notificationways'=2 'checkmodulations'=0 'macromodulations'=0 'servicedependencies'=40 'hostdependencies'=0 'arbiters'=1 'schedulers'=1 'reactionners'=1 'brokers'=1 'receivers'=1 'pollers'=1 'realms'=1 'resultmodulations'=0 'businessimpactmodulations'=0 'escalations'=0 'hostsextinfo'=0 'servicesextinfo'=0", + "state": "up", + "timestamp": 1542611507 + }, + "name": "My Alignak", + "services": [ + { + "livestate": { + "long_output": "", + "output": "warning because some daemons are not reachable.", + "perf_data": "", + "state": "warning", + "timestamp": 1542611507 + }, + "name": "arbiter-master" + }, + { + "livestate": { + "long_output": "Realm: All (True). Listening on: http://127.0.0.1:7772/", + "name": "broker_broker-master", + "output": "daemon is alive and reachable.", + "perf_data": "last_check=0.00", + "state": "ok", + "timestamp": 1542611507 + }, + "name": "broker-master" + }, + { + "livestate": { + "long_output": "Realm: All (True). Listening on: http://127.0.0.1:7771/", + "name": "poller_poller-master", + "output": "daemon is alive and reachable.", + "perf_data": "last_check=0.00", + "state": "ok", + "timestamp": 1542611507 + }, + "name": "poller-master" + }, + { + "livestate": { + "long_output": "Realm: All (True). Listening on: http://127.0.0.1:7769/", + "name": "reactionner_reactionner-master", + "output": "daemon is not reachable.", + "perf_data": "last_check=0.00", + "state": "warning", + "timestamp": 1542611507 + }, + "name": "reactionner-master" + }, + { + "livestate": { + "long_output": "Realm: All (True). Listening on: http://127.0.0.1:7773/", + "name": "receiver_receiver-master", + "output": "daemon is alive and reachable.", + "perf_data": "last_check=0.00", + "state": "ok", + "timestamp": 1542611507 + }, + "name": "receiver-master" + }, + { + "livestate": { + "long_output": "Realm: All (True). Listening on: http://127.0.0.1:7768/", + "name": "scheduler_scheduler-master", + "output": "daemon is alive and reachable.", + "perf_data": "last_check=0.00", + "state": "ok", + "timestamp": 1542611507 + }, + "name": "scheduler-master" + } + ], + "template": { + "_templates": [ + "alignak", + "important" + ], + "active_checks_enabled": false, + "alias": "My Alignak", + "notes": "", + "passive_checks_enabled": true + }, + "variables": {} + } + """ + if not getattr(app, 'alignak_endpoint', None): + logger.info("[WebUI-system] Alignak is not configured. Redirecting to the home page.") + app.bottle.redirect(app.get_url("Dashboard")) + + logger.debug("[WebUI-system] Get Alignak status, endpoint: %s", app.alignak_endpoint) + try: + req = requests.Session() + raw_data = req.get("%s/status" % app.alignak_endpoint) + data = json.loads(raw_data.content) + logger.debug("[WebUI-system] Result: %s", data) + except Exception as exp: + logger.error("[WebUI-system] alignak_status, exception: %s", exp) + app.request.environ['MSG'] = "Alignak Error" + app.bottle.redirect(app.get_url("Dashboard")) + + return data + + +def alignak_status(): + """Alignak livestate view: + live state and live synthesis information from the arbiter""" + + return { + 'ls': _get_alignak_livesynthesis(), + 'status': _get_alignak_status() + } + + +def alignak_events(): + """Get Alignak Arbiter events: + """ + if not getattr(app, 'alignak_endpoint', None): + logger.info("[WebUI-system] Alignak is not configured. Redirecting to the home page.") + app.bottle.redirect(app.get_url("Dashboard")) + + user = app.request.environ['USER'] + _ = user.is_administrator() or app.redirect403() + + midnight_timestamp = time.mktime(datetime.date.today().timetuple()) + try: + range_start = int(app.request.query.get('range_start', midnight_timestamp)) + except ValueError: + range_start = midnight_timestamp + + try: + range_end = int(app.request.query.get('range_end', midnight_timestamp + 86399)) + except ValueError: + range_end = midnight_timestamp + 86399 + logger.debug("[WebUI-logs] get_global_history, range: %d - %d", range_start, range_end) + + # Apply search filter if exists ... + search = app.request.query.get('search', "type:host") + if "type:host" not in search: + search = "type:host " + search + logger.debug("[WebUI-system] search parameters '%s'", search) + + filters = ','.join(app.request.query.getall('filter')) or "" + logger.debug("[WebUI-system] filters: %s", filters) + + # Fetch elements per page preference for user, default is 25 + elts_per_page = app.prefs_module.get_ui_user_preference(user, 'elts_per_page', 25) + + # We want to limit the number of elements + step = int(app.request.query.get('step', elts_per_page)) + start = int(app.request.query.get('start', '0')) + end = int(app.request.query.get('end', start + step)) + + items = [] + for log in app.alignak_events: + # Try to get a monitoring event + try: + logger.debug("Log: %s", log) + event = LogEvent(log['message']) + logger.debug("-> event: %s", event) + if not event.valid: + logger.warning("No monitoring event detected from: %s", log['message']) + continue + + # ------------------------------------------- + data = deepcopy(log) + if event.event_type == 'RETENTION': + type = "retention_save" + if event.data['state_type'].upper() == 'LOAD': + type = "retention_load" + data.update({ + "type": type + }) + + if event.event_type == 'TIMEPERIOD': + data.update({ + "type": "timeperiod_transition", + }) + + if event.event_type == 'EXTERNAL COMMAND': + data.update({ + "type": "external_command", + "message": event.data['command'] + }) + + if event.event_type == 'ALERT': + data.update({ + "host_name": event.data['hostname'], + "service_name": event.data['service_desc'] or 'n/a', + "state": event.data['state'], + "state_type": event.data['state_type'], + "type": "alert", + }) + + if event.event_type == 'NOTIFICATION': + data.update({ + "host_name": event.data['hostname'], + "service_name": event.data['service_desc'] or 'n/a', + "type": "notification", + }) + + if event.event_type == 'DOWNTIME': + downtime_type = "downtime_start" + if event.data['state'] == 'STOPPED': + downtime_type = "downtime_end" + if event.data['state'] == 'CANCELLED': + downtime_type = "downtime_cancelled" + + data.update({ + "host_name": event.data['hostname'], + "service_name": event.data['service_desc'] or 'n/a', + "user_name": "Alignak", + "type": downtime_type, + }) + + if event.event_type == 'ACKNOWLEDGE': + ack_type = "acknowledge_start" + if event.data['state'] == 'EXPIRED': + ack_type = "acknowledge_end" + if event.data['state'] == 'CANCELLED': + ack_type = "acknowledge_cancelled" + + data.update({ + "host_name": event.data['hostname'], + "service_name": event.data['service_desc'] or 'n/a', + "user_name": "Alignak", + "type": ack_type, + }) + + if event.event_type == 'FLAPPING': + flapping_type = "monitoring.flapping_start" + if event.data['state'] == 'STOPPED': + flapping_type = "monitoring.flapping_stop" + + data.update({ + "host_name": event.data['hostname'], + "service_name": event.data['service_desc'] or 'n/a', + "user_name": "Alignak", + "type": flapping_type, + }) + + if event.event_type == 'COMMENT': + data.update({ + "host_name": event.data['hostname'], + "service_name": event.data['service_desc'] or 'n/a', + "user_name": event.data['author'] or 'Alignak', + "type": "comment", + }) + + if filters and data.get('type', 'unknown') not in filters: + continue + + items.append(data) + except ValueError: + logger.warning("Unable to decode a monitoring event from: %s", log['message']) + logger.warning(traceback.format_exc()) + continue + + # If we overflow, came back as normal + total = len(items) + if start > total: + start = 0 + end = step + + navi = app.helper.get_navi(total, start, step=step) + + logger.info("[WebUI-system] got %d matching items", len(items)) + + return { + 'navi': navi, + 'page': "alignak/events", + 'logs': items[start:end], + 'total': total, + "filters": filters, + 'range_start': range_start, + 'range_end': range_end + } + + def system_parameters(): user = app.request.environ['USER'] _ = user.is_administrator() or app.redirect403() @@ -153,5 +531,11 @@ def system_widget(): alignak_parameters: { 'name': 'AlignakParameters', 'route': '/alignak/parameters', 'view': 'alignak-parameters', 'static': True - } + }, + alignak_status: { + 'name': 'AlignakStatus', 'route': '/alignak/status', 'view': 'alignak_status' + }, + alignak_events: { + 'name': 'AlignakEvents', 'route': '/alignak/events', 'view': 'alignak_events' + }, } diff --git a/module/plugins/system/views/alignak_events.tpl b/module/plugins/system/views/alignak_events.tpl new file mode 100644 index 00000000..318f3e82 --- /dev/null +++ b/module/plugins/system/views/alignak_events.tpl @@ -0,0 +1,129 @@ +%rebase("layout", title='Alignak events log', css=['system/css/multiselect.css'], js=['system/js/multiselect.js'], breadcrumb=[ ['Alignak events log', '/alignak/events'] ]) + +%helper = app.helper + +
    +
    +
    +
    +

    {{"%d total matching items" % total}}

    +
    +
    + +
    + %event_types = { "retention_load", "retention_save", "alert", "notification", "check_result", "webui_comment", "timeperiod_transition", "event_handler", "flapping_start", "flapping_stop", "downtime_start", "downtime_cancelled", "downtime_end", "acknowledge_start", "acknowledge_end" } + +
    +
    +
    + + +
    + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + + %if not logs: + + %else: +
    {{message}}{{message}}
    {{time.strftime(date_format, time.localtime(log['time']))}} {{log['message']}}{{log[time_field]}}{{value}}
    + + + + + + + + + + + + %for log in logs: + + + + + + + + + %end + +
    EventHostServiceMessage
    {{log.get('date', '')}}{{! helper.get_event_icon(log)}} - {{log.get('type', '')}}{{log.get('host_name', '')}}{{log.get('service_name', '')}}{{log.get('message', '')}}
    + %end +
    + + + diff --git a/module/plugins/system/views/alignak_ls.tpl b/module/plugins/system/views/alignak_ls.tpl new file mode 100644 index 00000000..d8ea8d86 --- /dev/null +++ b/module/plugins/system/views/alignak_ls.tpl @@ -0,0 +1,14 @@ +%rebase("layout", title='Alignak livesynthesis', css=['system/css/alignak.css'], js=['system/js/jquery.floatThead.min.js'], breadcrumb=[ ['Alignak livesynthesis', '/alignak/ls'] ]) + +%helper = app.helper + +
    +
    + %if not ls: +
    +

    No live synthesis information is available.

    +
    + %else: + %end +
    +
    diff --git a/module/plugins/system/views/alignak_status.tpl b/module/plugins/system/views/alignak_status.tpl new file mode 100644 index 00000000..3829ca57 --- /dev/null +++ b/module/plugins/system/views/alignak_status.tpl @@ -0,0 +1,78 @@ +%rebase("layout", title='Alignak livesynthesis', css=['system/css/alignak.css'], js=['system/js/jquery.floatThead.min.js'], breadcrumb=[ ['Alignak status', '/alignak/status'] ]) + +%helper = app.helper + +
    +
    + %if not status: + + %else: + %livestate = status['livestate'] + %state = livestate.get('state', 'unknown').lower() +
    +
    + {{!helper.get_fa_icon_state_and_label(cls="host", state=state)}} +
    +
    +

    {{status['name']}}

    +
    +
    + + + + + + + + + + + + %for satellite in status['services']: + %livestate = satellite['livestate'] + %state = livestate.get('state', 'unknown').lower() + %last_check = livestate.get('timestamp', 0) + + + + + + + %end + +
    SatelliteStateLast checkMessage
    {{livestate.get('name', satellite['name'])}}{{!helper.get_fa_icon_state_and_label(cls="service", state=state, label=state)}}{{helper.print_duration(last_check, just_duration=True, x_elts=2)}}{{livestate.get('output', 'n/a')}}
    + + + + %columns = sorted(ls['livesynthesis']['_overall']['livesynthesis'].keys()) + + + + + + %for counter in columns: + + %end + + + + %for scheduler in ls['livesynthesis']: + + + %for counter in columns: + %cpt = ls['livesynthesis'][scheduler]['livesynthesis'][counter] + + %end + + %end + +
    +
    + {{counter}} +
    +
    {{scheduler}}{{ cpt if cpt else '-' }}
    + %end +
    +
    diff --git a/module/views/header_element.tpl b/module/views/header_element.tpl index fcf5147a..a5163bfe 100644 --- a/module/views/header_element.tpl +++ b/module/views/header_element.tpl @@ -224,6 +224,7 @@ %if user.is_administrator():
  • + %else: +  Alignak + + %end
  • From fe47d22ea3f994d5892301f2829e8879d7e27112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Mon, 17 Dec 2018 07:55:41 +0100 Subject: [PATCH 06/31] Alignak statistics - first version --- module/plugins/stats/stats.py | 153 ++++++++++++++++++++-- module/plugins/stats/views/stats.tpl | 9 ++ module/plugins/stats/views/stats_host.tpl | 19 ++- module/views/header_element.tpl | 2 + 4 files changed, 169 insertions(+), 14 deletions(-) diff --git a/module/plugins/stats/stats.py b/module/plugins/stats/stats.py index c67dee38..4fc28cde 100644 --- a/module/plugins/stats/stats.py +++ b/module/plugins/stats/stats.py @@ -26,6 +26,11 @@ from collections import Counter, OrderedDict from itertools import groupby +from shinken.log import logger + +from copy import deepcopy +from logevent import LogEvent + # Will be populated by the UI with it's own value app = None @@ -74,6 +79,79 @@ def _graph(logs): return graph +def get_alignak_stats(): + user = app.bottle.request.environ['USER'] + _ = user.is_administrator() or app.redirect403() + + logger.info("Get Alignak stats") + + days = int(app.request.GET.get('days', 30)) + + range_end = int(app.request.GET.get('range_end', time.time())) + range_start = int(app.request.GET.get('range_start', range_end - (days * 86400))) + + # Restrictive filter on contact name + filters = ['notification'] + + logs = [] + for log in app.alignak_events: + # Try to get a monitoring event + try: + logger.debug("Log: %s", log) + event = LogEvent(log['message']) + logger.debug("-> event: %s", event) + if not event.valid: + logger.warning("No monitoring event detected from: %s", log['message']) + continue + + # ------------------------------------------- + data = deepcopy(log) + + if event.event_type == 'ALERT': + data.update({ + "host_name": event.data['hostname'], + "service_name": event.data['service_desc'] or 'n/a', + "state": event.data['state'], + "state_type": event.data['state_type'], + "type": "alert", + }) + + if event.event_type == 'NOTIFICATION': + data.update({ + "host_name": event.data['hostname'], + "service_name": event.data['service_desc'] or 'n/a', + "type": "notification", + }) + + if filters and data.get('type', 'unknown') not in filters: + continue + + logs.append(data) + logger.info(data) + except ValueError: + logger.warning("Unable to decode a monitoring event from: %s", log['message']) + continue + + hosts = Counter() + services = Counter() + hostsservices = Counter() + new_logs = [] + for l in logs: + hosts[l['host_name']] += 1 + if 'service_description' in l: + services[l['service_description']] += 1 + hostsservices[l['host_name'] + '/' + l['service_description']] += 1 + new_logs.append(l) + + return { + 'hosts': hosts, + 'services': services, + 'hostsservices': hostsservices, + 'days': days, + 'graph': _graph(new_logs) if new_logs else None + } + + def get_global_stats(): user = app.bottle.request.environ['USER'] _ = user.is_administrator() or app.redirect403() @@ -83,25 +161,44 @@ def get_global_stats(): range_end = int(app.request.GET.get('range_end', time.time())) range_start = int(app.request.GET.get('range_start', range_end - (days * 86400))) - logs = list(app.logs_module.get_ui_logs( - range_start=range_start, range_end=range_end, - filters={'type': 'SERVICE NOTIFICATION', - 'command_name': {'$regex': 'notify-service-by-slack'}}, - limit=None)) + filters = {'type': 'SERVICE NOTIFICATION', + 'command_name': {'$regex': 'notify-service-by-slack'}} + if app.alignak: + # Restrictive filter on contact name + filters = { + 'alignak.event': {'$in': ['HOST NOTIFICATION', 'SERVICE NOTIFICATION']}, + 'alignak.contact': 'notified' + } + + logs = list(app.logs_module.get_ui_logs(range_start=range_start, range_end=range_end, + filters=filters, limit=None)) hosts = Counter() services = Counter() hostsservices = Counter() + new_logs = [] for l in logs: + # Alignak logstash parser.... + if 'alignak' in l: + l = l['alignak'] + if 'time' not in l: + l['time'] = int(time.mktime(l.pop('timestamp').timetuple())) + + if 'service' in l: + l['service_description'] = l.pop('service') + hosts[l['host_name']] += 1 - services[l['service_description']] += 1 - hostsservices[l['host_name'] + '/' + l['service_description']] += 1 + if 'service_description' in l: + services[l['service_description']] += 1 + hostsservices[l['host_name'] + '/' + l['service_description']] += 1 + new_logs.append(l) + return { 'hosts': hosts, 'services': services, 'hostsservices': hostsservices, 'days': days, - 'graph': _graph(logs) if logs else None + 'graph': _graph(new_logs) if new_logs else None } @@ -136,20 +233,50 @@ def get_host_stats(name): range_end = int(app.request.GET.get('range_end', time.time())) range_start = int(app.request.GET.get('range_start', range_end - (days * 86400))) + filters = {'type': 'SERVICE NOTIFICATION', + 'command_name': {'$regex': 'notify-service-by-slack'}, + 'host_name': name} + if app.alignak: + # Restrictive filter on contact name + filters = { + 'alignak.event': {'$in': ['HOST NOTIFICATION', 'SERVICE NOTIFICATION']}, + 'alignak.contact': 'notified', + 'alignak.host_name': name + } + logs = list(app.logs_module.get_ui_logs( range_start=range_start, range_end=range_end, - filters={'type': 'SERVICE NOTIFICATION', - 'command_name': {'$regex': 'notify-service-by-slack'}, - 'host_name': name}, + filters=filters, limit=None)) + hosts = Counter() services = Counter() for l in logs: - services[l['service_description']] += 1 - return {'host': name, 'services': services, 'days': days} + # Alignak logstash parser.... + if 'alignak' in l: + l = l['alignak'] + if 'time' not in l: + l['time'] = int(time.mktime(l.pop('timestamp').timetuple())) + + if 'service' in l: + l['service_description'] = l.pop('service') + + hosts[l['host_name']] += 1 + if 'service_description' in l: + services[l['service_description']] += 1 + return { + 'host': name, + 'hosts': hosts, + 'services': services, + 'days': days + } pages = { + get_alignak_stats: { + 'name': 'AlignakStats', 'route': '/alignak/stats', 'view': 'stats' + }, + get_global_stats: { 'name': 'GlobalStats', 'route': '/stats', 'view': 'stats' }, diff --git a/module/plugins/stats/views/stats.tpl b/module/plugins/stats/views/stats.tpl index e404cf64..79469a26 100644 --- a/module/plugins/stats/views/stats.tpl +++ b/module/plugins/stats/views/stats.tpl @@ -4,7 +4,9 @@
    + %total = sum(hosts.values())

    {{ total }} host alerts

    + %if total: %for l in hosts.most_common(15): @@ -12,12 +14,15 @@ %other = sum((h[1] for h in hosts.most_common()[15:]))
    {{ l[1] }} ({{ round((l[1] / float(total)) * 100, 1) }}%){{ l[0] }}
    {{ other }} ({{ round((other / float(total)) * 100, 1) }}%)Others
    + %end
    + %total = sum(services.values())

    {{ total }} services alerts

    + %if total: %for l in services.most_common(15): @@ -25,12 +30,15 @@ %other = sum((s[1] for s in services.most_common()[15:]))
    {{ l[1] }} ({{ round((l[1] / float(total)) * 100, 1) }}%){{ l[0] }}
    {{ other }} ({{ round((other / float(total)) * 100, 1) }}%)Others
    + %end
    + %total = sum(hostsservices.values())

    {{ total }} hosts/services alerts

    + %if total: %for l in hostsservices.most_common(15): @@ -38,6 +46,7 @@ %other = sum((h[1] for h in hostsservices.most_common()[15:]))
    {{ l[1] }} ({{ round((l[1] / float(total)) * 100, 1) }}%){{ l[0] }}
    {{ other }} ({{ round((other / float(total)) * 100, 1) }}%)Others
    + %end
    diff --git a/module/plugins/stats/views/stats_host.tpl b/module/plugins/stats/views/stats_host.tpl index 089b1e6c..65deb35c 100644 --- a/module/plugins/stats/views/stats_host.tpl +++ b/module/plugins/stats/views/stats_host.tpl @@ -1,10 +1,26 @@ %rebase("layout", css=['logs/css/logs.css'], js=['logs/js/history.js'], title='Alert Statistics on the last 30 days for ' + host) -%total = sum(services.values()) +
    +
    + %total = sum(hosts.values()) +

    {{ total }} host alerts

    + %if total: + + %for l in hosts.most_common(15): + + %end + %other = sum((h[1] for h in hosts.most_common()[15:])) + +
    {{ l[1] }} ({{ round((l[1] / float(total)) * 100, 1) }}%){{ l[0] }}
    {{ other }} ({{ round((other / float(total)) * 100, 1) }}%)Others
    + %end +
    +
    + %total = sum(services.values())

    {{ total }} {{ host }} alerts

    + %if total: %for l in services.most_common(15): @@ -12,6 +28,7 @@ %other = sum((h[1] for h in services.most_common()[15:]))
    {{ l[1] }} ({{ round((l[1] / float(total)) * 100, 1) }}%){{ l[0] }}
    {{ other }} ({{ round((other / float(total)) * 100, 1) }}%)Others
    + %end
    diff --git a/module/views/header_element.tpl b/module/views/header_element.tpl index a5163bfe..cac3da21 100644 --- a/module/views/header_element.tpl +++ b/module/views/header_element.tpl @@ -245,6 +245,8 @@  Live state
  •  Events log
  • +
  • +  Events stats
  • %if app.logs_module.is_available():
  •  Mongo Logs
  • From ddcfa28b470109254060a8eea3e98c45081df0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Thu, 13 Dec 2018 13:04:34 +0100 Subject: [PATCH 07/31] Closes #676 - Contacts list with icon --- module/plugins/contacts/views/contacts.tpl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/module/plugins/contacts/views/contacts.tpl b/module/plugins/contacts/views/contacts.tpl index 155ae59b..954dd0b4 100644 --- a/module/plugins/contacts/views/contacts.tpl +++ b/module/plugins/contacts/views/contacts.tpl @@ -29,12 +29,14 @@ %for contact in contacts: - {{ !helper.get_contact_avatar(contact) }} %if contact.is_admin: - + %elif app.can_action(contact.contact_name): - + + %else: + %end + {{ !helper.get_contact_avatar(contact) }} {{ contact.alias if contact.alias != "none" else "" }} {{ contact.min_business_impact }} From eb8ca94ceeeb06cc26e15377bb223591d63e050a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Tue, 18 Dec 2018 08:10:36 +0100 Subject: [PATCH 08/31] Fix hostgroups filtering behaviour Clean groups / tags filtering Clean service groups relations --- module/datamanager.py | 46 +++++++++++++++++++++---------------------- module/regenerator.py | 8 ++++---- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/module/datamanager.py b/module/datamanager.py index b251f2cb..9c9858fd 100644 --- a/module/datamanager.py +++ b/module/datamanager.py @@ -478,34 +478,34 @@ def search_hosts_and_services(self, search, user, get_impacts=True, sorter=None, if (t in ['hg', 'hgroup', 'hostgroup']) and s.lower() != 'all': logger.debug("[WebUI - datamanager] searching for items in the hostgroup %s", s) group = self.get_hostgroup(s) - if not group: - return [] - logger.debug("[WebUI - datamanager] found the group: %s", group.get_name()) - - items = [i for i in items if i in group.members] + if group: + logger.debug("[WebUI - datamanager] found the group: %s", group.get_name()) + # This filters items that are related with the hostgroup only + # if the item has an hostgroups property + items = [i for i in items if getattr(i, 'get_hostgroups') and + group.get_name() in [g.get_name() for g in i.get_hostgroups()]] if (t in ['sg', 'sgroup', 'servicegroup']) and s.lower() != 'all': - logger.info("[WebUI - datamanager] searching for items in the servicegroup %s", s) + logger.debug("[WebUI - datamanager] searching for items in the servicegroup %s", s) group = self.get_servicegroup(s) - if not group: - return [] - logger.debug("[WebUI - datamanager] found the group: %s", group.get_name()) - - items = [i for i in items if i in group.members] - # items = [i for i in items if i.__class__.my_type == 'service' + if group: + logger.debug("[WebUI - datamanager] found the group: %s", group.get_name()) + # Only the items that have a servicegroups property + items = [i for i in items if getattr(i, 'servicegroups') and + group.get_name() in [g.get_name() for g in i.servicegroups]] - # @mohierf: to be refactored! if (t in ['cg', 'cgroup', 'contactgroup']) and s.lower() != 'all': - logger.info("[WebUI - datamanager] searching for items related with the contactgroup %s", s) + logger.debug("[WebUI - datamanager] searching for items related with the contactgroup %s", s) group = self.get_contactgroup(s, user) - if not group: - return [] - logger.debug("[WebUI - datamanager] found the group: %s", group.get_name()) + if group: + logger.debug("[WebUI - datamanager] found the group: %s", group.get_name()) + + contacts = [c for c in self.get_contacts(user=user) if c in group.members] + logger.info("[WebUI - datamanager] contacts: %s", contacts) - contacts = [c for c in self.get_contacts(user=user) if c in group.members] - items = list(set(itertools.chain(*[self._only_related_to(items, - self.rg.contacts.find_by_name(c)) - for c in contacts]))) + items = list(set(itertools.chain(*[self._only_related_to(items, + self.rg.contacts.find_by_name(c)) + for c in contacts]))) if t == 'realm': r = self.get_realm(s) @@ -514,10 +514,10 @@ def search_hosts_and_services(self, search, user, get_impacts=True, sorter=None, items = [i for i in items if i.get_realm() == r] if t == 'htag' and s.lower() != 'all': - items = [i for i in items if s in i.get_host_tags()] + items = [i for i in items if getattr(i, 'get_host_tags') and s in i.get_host_tags()] if t == 'stag' and s.lower() != 'all': - items = [i for i in items if i.__class__.my_type == 'service' and s in i.get_service_tags()] + items = [i for i in items if getattr(i, 'get_service_tags') and s in i.get_service_tags()] if t == 'ctag' and s.lower() != 'all': contacts = [c for c in self.get_contacts(user=user) if s in c.tags] diff --git a/module/regenerator.py b/module/regenerator.py index c2f01420..51df1f20 100644 --- a/module/regenerator.py +++ b/module/regenerator.py @@ -489,7 +489,7 @@ def all_done_linking(self, inst_id): if not isinstance(sgs, list): sgs = s.servicegroups.split(',') new_groups = [] - logger.debug("Searching servicegroup for the service %s, servicegroups: %s", s.get_name(), sgs) + logger.debug("Searching servicegroup for the service %s, servicegroups: %s", s.get_full_name(), sgs) for sgname in sgs: for group in self.servicegroups: if sgname == group.get_name() or sgname == group.uuid: @@ -497,9 +497,9 @@ def all_done_linking(self, inst_id): logger.debug("Found servicegroup %s", group.get_name()) break else: - logger.warning("No servicegroup %s for service: %s", sgname, h.get_name()) - h.servicegroups = new_groups - logger.debug("Linked %s servicegroups %s", h.get_name(), h.servicegroups) + logger.warning("No servicegroup %s for service: %s", sgname, s.get_full_name()) + s.servicegroups = new_groups + logger.debug("Linked %s servicegroups %s", s.get_full_name(), s.servicegroups) # Now link with host hname = s.host_name From 9b0dc47b70ac41210eaa8e3160a8d319472ea30d Mon Sep 17 00:00:00 2001 From: Guillaume Subiron Date: Tue, 18 Dec 2018 16:45:51 +0100 Subject: [PATCH 09/31] Revert some of 1435bdb about comment tab --- module/plugins/eltdetail/views/_eltdetail_comments.tpl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/module/plugins/eltdetail/views/_eltdetail_comments.tpl b/module/plugins/eltdetail/views/_eltdetail_comments.tpl index 3ef83f74..802b342e 100644 --- a/module/plugins/eltdetail/views/_eltdetail_comments.tpl +++ b/module/plugins/eltdetail/views/_eltdetail_comments.tpl @@ -3,11 +3,13 @@
    %if not elt.comments: - - %else: - %include("_eltdetail_comment_table.tpl", comments=elt.comments) +
    +

    No comment available on this {{ elt_type }}

    +
    %end + %include("_eltdetail_comment_table.tpl", comments=elt.comments) + %if elt_type=='host' and elt.services: %servicecomments = [c for sublist in [s.comments for s in elt.services] for c in sublist] %if servicecomments: From 445c31d6fd3c902c61d066466ca7ed4bf719a9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Fri, 21 Dec 2018 09:33:34 +0100 Subject: [PATCH 10/31] Closes #696 - fix and improve broken parameters view Sort and clean parameters list (hide unuseful information) --- module/plugins/system/system.py | 10 +++++----- module/plugins/system/views/parameters.tpl | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/module/plugins/system/system.py b/module/plugins/system/system.py index a320abd9..a5997bfb 100644 --- a/module/plugins/system/system.py +++ b/module/plugins/system/system.py @@ -419,12 +419,12 @@ def system_parameters(): user = app.request.environ['USER'] _ = user.is_administrator() or app.redirect403() - configs = app.datamgr.get_configs() - if configs: - configs = sorted(vars(configs[0]).iteritems()) - return {'configs': configs} + # configs = app.datamgr.get_configs() + # if configs: + # configs = sorted(vars(configs[0]).iteritems()) + # return {'configs': configs} - return {'configs': None} + return {'configs': app.datamgr.get_configs()} def alignak_parameters(): diff --git a/module/plugins/system/views/parameters.tpl b/module/plugins/system/views/parameters.tpl index 04217794..13ed430e 100644 --- a/module/plugins/system/views/parameters.tpl +++ b/module/plugins/system/views/parameters.tpl @@ -6,13 +6,25 @@

    No system information is available.

    %else: +%for config in configs: + %if config.get('instance_name', None): + + + + + + %end - %for key, value in configs: + %for key in sorted(config.keys()): + %if key in ['id', 'uuid', 'instance_id', 'instance_name']: + %continue + %end + %value=config[key]
    Scheduler {{config.get('instance_name')}}
    {{key}} @@ -27,4 +39,5 @@
    %end +%end
    From 7aeabc950adb9bec58a9c020697c036c7663e5a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Wed, 19 Dec 2018 05:54:52 +0100 Subject: [PATCH 11/31] Helper functions to render notes and actions --- module/helper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/module/helper.py b/module/helper.py index 7583e43e..15decfd0 100644 --- a/module/helper.py +++ b/module/helper.py @@ -45,6 +45,7 @@ from shinken.misc.sorter import hst_srv_sort from shinken.misc.perfdata import PerfDatas +from shinken.macroresolver import MacroResolver # pylint: disable=no-self-use From cfcd48da67cbe8fe1e6e1edcf35b8d26c1a6d439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Thu, 20 Dec 2018 07:45:59 +0100 Subject: [PATCH 12/31] Closes #677 - render notes and actions in the element view --- module/htdocs/css/shinken-layout.css | 2 +- .../views/_eltdetail_information.tpl | 50 +++++++++++++++---- module/plugins/eltdetail/views/eltdetail.tpl | 2 +- .../etc/arbiter/objects/hosts/localhost.cfg | 17 +++---- .../etc/arbiter/objects/hosts/localhost2.cfg | 28 +++++++++-- .../etc/arbiter/objects/hosts/localhost.cfg | 13 ++++- .../etc/arbiter/objects/hosts/localhost2.cfg | 28 +++++++++-- 7 files changed, 109 insertions(+), 31 deletions(-) diff --git a/module/htdocs/css/shinken-layout.css b/module/htdocs/css/shinken-layout.css index b46d4abc..6b35c982 100644 --- a/module/htdocs/css/shinken-layout.css +++ b/module/htdocs/css/shinken-layout.css @@ -41,7 +41,7 @@ body { } .page-header { - margin-top:5px; + margin-top:15px; } .breadcrumb { diff --git a/module/plugins/eltdetail/views/_eltdetail_information.tpl b/module/plugins/eltdetail/views/_eltdetail_information.tpl index 2e7963eb..70dc680b 100644 --- a/module/plugins/eltdetail/views/_eltdetail_information.tpl +++ b/module/plugins/eltdetail/views/_eltdetail_information.tpl @@ -223,17 +223,49 @@
    - %if elt.notes_url or elt.action_url or elt.notes: + %if elt.notes or elt.notes_url: - %if elt.notes != '': -

    {{ elt.notes }}

    + + %if elt.notes: +
      + %for note in helper.get_element_notes(elt, popover=False, css='class="list-group-item"'): + {{! note}} %end -
      - %if elt.notes_url != '': - More notes - %end - %if elt.action_url != '': - Launch custom action +
    + %end + + %if elt.notes_url: +
      + %for note in helper.get_element_notes_url(elt, default_title="More notes", default_icon="external-link-square", popover=True, css='class="btn btn-info"'): +
    • {{! note}}
    • + %end +
    + %end + %end + + %if elt.action_url: + +
      + %for action in helper.get_element_actions_url(elt, default_title="Launch custom action", default_icon="cogs", popover=True, css='class="btn btn-warning"'): +
    • {{! action}}
    • + %end +
    + %end + + %elt_type = elt.__class__.my_type + %tags = elt.get_service_tags() if elt_type=='service' else elt.get_host_tags() + %if tags: + %tag='stag' if elt_type=='service' else 'htag' + + %end diff --git a/module/plugins/eltdetail/views/eltdetail.tpl b/module/plugins/eltdetail/views/eltdetail.tpl index b28d0e05..f38aa149 100644 --- a/module/plugins/eltdetail/views/eltdetail.tpl +++ b/module/plugins/eltdetail/views/eltdetail.tpl @@ -23,7 +23,7 @@ Invalid element name %breadcrumb += [[elt.display_name, '/service/'+helper.get_uri_name(elt)]] %end -%js=['js/jquery.sparkline.min.js', 'js/shinken-charts.js', 'availability/js/justgage.js', 'availability/js/raphael-2.1.4.min.js', 'cv_host/js/flot/jquery.flot.min.js', 'cv_host/js/flot/jquery.flot.tickrotor.js', 'cv_host/js/flot/jquery.flot.resize.min.js', 'cv_host/js/flot/jquery.flot.pie.min.js', 'cv_host/js/flot/jquery.flot.categories.min.js', 'cv_host/js/flot/jquery.flot.time.min.js', 'cv_host/js/flot/jquery.flot.stack.min.js', 'cv_host/js/flot/jquery.flot.valuelabels.js', 'eltdetail/js/custom_views.js', 'eltdetail/js/eltdetail.js', 'logs/js/history.js'] +%js=['js/jquery.sparkline.min.js', 'js/shinken-charts.js', 'cv_host/js/flot/jquery.flot.min.js', 'cv_host/js/flot/jquery.flot.tickrotor.js', 'cv_host/js/flot/jquery.flot.resize.min.js', 'cv_host/js/flot/jquery.flot.pie.min.js', 'cv_host/js/flot/jquery.flot.categories.min.js', 'cv_host/js/flot/jquery.flot.time.min.js', 'cv_host/js/flot/jquery.flot.stack.min.js', 'cv_host/js/flot/jquery.flot.valuelabels.js', 'eltdetail/js/custom_views.js', 'eltdetail/js/eltdetail.js', 'logs/js/history.js'] %if app.logs_module.is_available(): %js=js + ['availability/js/justgage.js', 'availability/js/raphael-2.1.4.min.js'] %end diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg index edc16401..6faf7f90 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg @@ -10,18 +10,15 @@ define host{ contact_groups admins # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 - notes Simple note - notes Label::note with a label - notes KB1023,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas vestibulum eu non sapien. Nulla facilisi. Aliquam non blandit tellus, non luctus tortor. Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.|note simple|Tag::tagged note ... + tags +fred - notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ + notes simple note... only text but may be formated - action_url http://www.google.fr|url1::http://www.google.fr|My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$ + + action_url http://www.my-KB.fr?host=$HOSTNAME$ - # Defined in host template - #custom_views +linux_ssh,linux_ssh_memory,linux_ssh_processes - custom_views host } diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg index 890ff6e6..bf2d973c 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg @@ -9,8 +9,6 @@ define host{ contact_groups admins - tags +fred - _satellites arbiter-master$(arbiter)$$(arbiter-master)$$(7770)$,\ scheduler-master$(scheduler)$$(scheduler-master)$$(7768)$,\ scheduler-second$(scheduler)$$(scheduler-second)$$(17768)$,\ @@ -21,6 +19,28 @@ define host{ receiver-master$(receiver)$$(receiver-master)$$(7773)$ # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 + + tags +fred + + notes simple note... only text but may be formated\ + |Title::note with only title...\ + |Title,,file::note with a title and an icon...\ + |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ + |KB5126,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ + vestibulum eu non sapien. Nulla facilisi. \ + Aliquam non blandit tellus, non luctus tortor. \ + Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.,,http://my-url.fr\ + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ + + action_url http://www.google.fr|url1::http://www.google.fr|\ + My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|\ + Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ + } diff --git a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg index 3c635283..6faf7f90 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg @@ -10,6 +10,15 @@ define host{ contact_groups admins # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 + + tags +fred + + notes simple note... only text but may be formated + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$ + + action_url http://www.my-KB.fr?host=$HOSTNAME$ + } diff --git a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg index 890ff6e6..bf2d973c 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg @@ -9,8 +9,6 @@ define host{ contact_groups admins - tags +fred - _satellites arbiter-master$(arbiter)$$(arbiter-master)$$(7770)$,\ scheduler-master$(scheduler)$$(scheduler-master)$$(7768)$,\ scheduler-second$(scheduler)$$(scheduler-second)$$(17768)$,\ @@ -21,6 +19,28 @@ define host{ receiver-master$(receiver)$$(receiver-master)$$(7773)$ # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 + + tags +fred + + notes simple note... only text but may be formated\ + |Title::note with only title...\ + |Title,,file::note with a title and an icon...\ + |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ + |KB5126,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ + vestibulum eu non sapien. Nulla facilisi. \ + Aliquam non blandit tellus, non luctus tortor. \ + Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.,,http://my-url.fr\ + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ + + action_url http://www.google.fr|url1::http://www.google.fr|\ + My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|\ + Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ + } From 0ebc86059a4900fbe8053b4f1433eccba6975de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Thu, 20 Dec 2018 07:51:53 +0100 Subject: [PATCH 13/31] Closes #677 - render notes and actions in the problems view --- .../problems/views/_problems_eltdetail.tpl | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/module/plugins/problems/views/_problems_eltdetail.tpl b/module/plugins/problems/views/_problems_eltdetail.tpl index e776a5e5..f1b9f9dd 100644 --- a/module/plugins/problems/views/_problems_eltdetail.tpl +++ b/module/plugins/problems/views/_problems_eltdetail.tpl @@ -71,11 +71,28 @@ %end
    - %if pb.notes or pb.notes_url: -
    - {{ pb.notes }}
    - {{ pb.notes_url }} -
    + %if pb.notes: +
      + %for note in helper.get_element_notes(pb, popover=False, css='class="list-group-item"'): + {{! note}} + %end +
    + %end + + %if pb.notes_url: +
      + %for note in helper.get_element_notes_url(pb, default_title="More notes", default_icon="external-link-square", popover=True, css='class="btn btn-info"'): +
    • {{! note}}
    • + %end +
    + %end + + %if pb.action_url: +
      + %for action in helper.get_element_actions_url(pb, default_title="Launch custom action", default_icon="cogs", popover=True, css='class="btn btn-warning"'): +
    • {{! action}}
    • + %end +
    %end %if pb.perf_data: From c8f7c77c0000d3f7c46f8df85f8a1e3ee7fbfbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Wed, 2 Jan 2019 13:20:30 +0100 Subject: [PATCH 14/31] Fix a CSS media query for width around 992px --- module/htdocs/css/shinken-layout.css | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/module/htdocs/css/shinken-layout.css b/module/htdocs/css/shinken-layout.css index 6b35c982..0aa67e59 100644 --- a/module/htdocs/css/shinken-layout.css +++ b/module/htdocs/css/shinken-layout.css @@ -389,12 +389,7 @@ td .ellipsis > .long-output { } @media (min-width: 992px) { #search { - width: 370px; - } -} -@media (min-width: 992px) { - #search { - width: 370px; + width: 360px; } } @media (min-width: 1280px) { From c20d9240560bbe6b665761e00f31b87ef485e2bf Mon Sep 17 00:00:00 2001 From: Guillaume Subiron Date: Wed, 2 Jan 2019 15:11:53 +0100 Subject: [PATCH 15/31] Revert "Closes #677 - render notes and actions in the problems view" This reverts commit feed37ceeda05ae95eabc13556a47b517ac30c7b. --- .../problems/views/_problems_eltdetail.tpl | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/module/plugins/problems/views/_problems_eltdetail.tpl b/module/plugins/problems/views/_problems_eltdetail.tpl index f1b9f9dd..e776a5e5 100644 --- a/module/plugins/problems/views/_problems_eltdetail.tpl +++ b/module/plugins/problems/views/_problems_eltdetail.tpl @@ -71,28 +71,11 @@ %end - %if pb.notes: -
      - %for note in helper.get_element_notes(pb, popover=False, css='class="list-group-item"'): - {{! note}} - %end -
    - %end - - %if pb.notes_url: -
      - %for note in helper.get_element_notes_url(pb, default_title="More notes", default_icon="external-link-square", popover=True, css='class="btn btn-info"'): -
    • {{! note}}
    • - %end -
    - %end - - %if pb.action_url: -
      - %for action in helper.get_element_actions_url(pb, default_title="Launch custom action", default_icon="cogs", popover=True, css='class="btn btn-warning"'): -
    • {{! action}}
    • - %end -
    + %if pb.notes or pb.notes_url: +
    + {{ pb.notes }}
    + {{ pb.notes_url }} +
    %end %if pb.perf_data: From a1bf880bb8e66adf0bf6f58eccbca697b2eea91b Mon Sep 17 00:00:00 2001 From: Guillaume Subiron Date: Wed, 2 Jan 2019 15:12:03 +0100 Subject: [PATCH 16/31] Revert "Closes #677 - render notes and actions in the element view" This reverts commit f26f8db844abfc79f8fe1a87d520ec90acacfe66. --- module/htdocs/css/shinken-layout.css | 2 +- .../views/_eltdetail_information.tpl | 50 ++++--------------- module/plugins/eltdetail/views/eltdetail.tpl | 2 +- .../etc/arbiter/objects/hosts/localhost.cfg | 13 +---- .../etc/arbiter/objects/hosts/localhost2.cfg | 28 ++--------- .../etc/arbiter/objects/hosts/localhost.cfg | 13 +---- .../etc/arbiter/objects/hosts/localhost2.cfg | 28 ++--------- 7 files changed, 23 insertions(+), 113 deletions(-) diff --git a/module/htdocs/css/shinken-layout.css b/module/htdocs/css/shinken-layout.css index 0aa67e59..40890413 100644 --- a/module/htdocs/css/shinken-layout.css +++ b/module/htdocs/css/shinken-layout.css @@ -41,7 +41,7 @@ body { } .page-header { - margin-top:15px; + margin-top:5px; } .breadcrumb { diff --git a/module/plugins/eltdetail/views/_eltdetail_information.tpl b/module/plugins/eltdetail/views/_eltdetail_information.tpl index 70dc680b..2e7963eb 100644 --- a/module/plugins/eltdetail/views/_eltdetail_information.tpl +++ b/module/plugins/eltdetail/views/_eltdetail_information.tpl @@ -223,49 +223,17 @@
    - %if elt.notes or elt.notes_url: + %if elt.notes_url or elt.action_url or elt.notes: - - %if elt.notes: -
      - %for note in helper.get_element_notes(elt, popover=False, css='class="list-group-item"'): - {{! note}} - %end -
    - %end - - %if elt.notes_url: -
      - %for note in helper.get_element_notes_url(elt, default_title="More notes", default_icon="external-link-square", popover=True, css='class="btn btn-info"'): -
    • {{! note}}
    • + %if elt.notes != '': +

      {{ elt.notes }}

      %end -
    - %end - %end - - %if elt.action_url: - -
      - %for action in helper.get_element_actions_url(elt, default_title="Launch custom action", default_icon="cogs", popover=True, css='class="btn btn-warning"'): -
    • {{! action}}
    • - %end -
    - %end - - %elt_type = elt.__class__.my_type - %tags = elt.get_service_tags() if elt_type=='service' else elt.get_host_tags() - %if tags: - %tag='stag' if elt_type=='service' else 'htag' - -
    - %for t in sorted(tags): - - %if app.tag_as_image: - {{t.lower()}} - %else: - - %end - +
    + %if elt.notes_url != '': + More notes + %end + %if elt.action_url != '': + Launch custom action %end
    %end diff --git a/module/plugins/eltdetail/views/eltdetail.tpl b/module/plugins/eltdetail/views/eltdetail.tpl index f38aa149..b28d0e05 100644 --- a/module/plugins/eltdetail/views/eltdetail.tpl +++ b/module/plugins/eltdetail/views/eltdetail.tpl @@ -23,7 +23,7 @@ Invalid element name %breadcrumb += [[elt.display_name, '/service/'+helper.get_uri_name(elt)]] %end -%js=['js/jquery.sparkline.min.js', 'js/shinken-charts.js', 'cv_host/js/flot/jquery.flot.min.js', 'cv_host/js/flot/jquery.flot.tickrotor.js', 'cv_host/js/flot/jquery.flot.resize.min.js', 'cv_host/js/flot/jquery.flot.pie.min.js', 'cv_host/js/flot/jquery.flot.categories.min.js', 'cv_host/js/flot/jquery.flot.time.min.js', 'cv_host/js/flot/jquery.flot.stack.min.js', 'cv_host/js/flot/jquery.flot.valuelabels.js', 'eltdetail/js/custom_views.js', 'eltdetail/js/eltdetail.js', 'logs/js/history.js'] +%js=['js/jquery.sparkline.min.js', 'js/shinken-charts.js', 'availability/js/justgage.js', 'availability/js/raphael-2.1.4.min.js', 'cv_host/js/flot/jquery.flot.min.js', 'cv_host/js/flot/jquery.flot.tickrotor.js', 'cv_host/js/flot/jquery.flot.resize.min.js', 'cv_host/js/flot/jquery.flot.pie.min.js', 'cv_host/js/flot/jquery.flot.categories.min.js', 'cv_host/js/flot/jquery.flot.time.min.js', 'cv_host/js/flot/jquery.flot.stack.min.js', 'cv_host/js/flot/jquery.flot.valuelabels.js', 'eltdetail/js/custom_views.js', 'eltdetail/js/eltdetail.js', 'logs/js/history.js'] %if app.logs_module.is_available(): %js=js + ['availability/js/justgage.js', 'availability/js/raphael-2.1.4.min.js'] %end diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg index 6faf7f90..3c635283 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg @@ -10,15 +10,6 @@ define host{ contact_groups admins # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 - - tags +fred - - notes simple note... only text but may be formated - - notes_url http://www.my-KB.fr?host=$HOSTADDRESS$ - - action_url http://www.my-KB.fr?host=$HOSTNAME$ - + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 } diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg index bf2d973c..890ff6e6 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg @@ -9,6 +9,8 @@ define host{ contact_groups admins + tags +fred + _satellites arbiter-master$(arbiter)$$(arbiter-master)$$(7770)$,\ scheduler-master$(scheduler)$$(scheduler-master)$$(7768)$,\ scheduler-second$(scheduler)$$(scheduler-second)$$(17768)$,\ @@ -19,28 +21,6 @@ define host{ receiver-master$(receiver)$$(receiver-master)$$(7773)$ # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 - - tags +fred - - notes simple note... only text but may be formated\ - |Title::note with only title...\ - |Title,,file::note with a title and an icon...\ - |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ - |KB5126,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ - Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ - nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ - vestibulum eu non sapien. Nulla facilisi. \ - Aliquam non blandit tellus, non luctus tortor. \ - Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.,,http://my-url.fr\ - - notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ - - action_url http://www.google.fr|url1::http://www.google.fr|\ - My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|\ - Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ - Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ - nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ - + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 } diff --git a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg index 6faf7f90..3c635283 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg @@ -10,15 +10,6 @@ define host{ contact_groups admins # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 - - tags +fred - - notes simple note... only text but may be formated - - notes_url http://www.my-KB.fr?host=$HOSTADDRESS$ - - action_url http://www.my-KB.fr?host=$HOSTNAME$ - + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 } diff --git a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg index bf2d973c..890ff6e6 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg @@ -9,6 +9,8 @@ define host{ contact_groups admins + tags +fred + _satellites arbiter-master$(arbiter)$$(arbiter-master)$$(7770)$,\ scheduler-master$(scheduler)$$(scheduler-master)$$(7768)$,\ scheduler-second$(scheduler)$$(scheduler-second)$$(17768)$,\ @@ -19,28 +21,6 @@ define host{ receiver-master$(receiver)$$(receiver-master)$$(7773)$ # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 - - tags +fred - - notes simple note... only text but may be formated\ - |Title::note with only title...\ - |Title,,file::note with a title and an icon...\ - |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ - |KB5126,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ - Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ - nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ - vestibulum eu non sapien. Nulla facilisi. \ - Aliquam non blandit tellus, non luctus tortor. \ - Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.,,http://my-url.fr\ - - notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ - - action_url http://www.google.fr|url1::http://www.google.fr|\ - My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|\ - Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ - Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ - nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ - + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 } From 80e58c5daae4fc46a36d9ce28203e58344e05238 Mon Sep 17 00:00:00 2001 From: Guillaume Subiron Date: Wed, 2 Jan 2019 15:12:08 +0100 Subject: [PATCH 17/31] Revert "Helper functions to render notes and actions" This reverts commit 2020b528e170f3056155e529baa6e8deec6fdd4e. --- module/helper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/module/helper.py b/module/helper.py index 15decfd0..7583e43e 100644 --- a/module/helper.py +++ b/module/helper.py @@ -45,7 +45,6 @@ from shinken.misc.sorter import hst_srv_sort from shinken.misc.perfdata import PerfDatas -from shinken.macroresolver import MacroResolver # pylint: disable=no-self-use From 205edf73b23dbac46619b70514c3e0bfab06f7f3 Mon Sep 17 00:00:00 2001 From: Guillaume Subiron Date: Wed, 2 Jan 2019 15:33:39 +0100 Subject: [PATCH 18/31] Debug notes_url display in cv_hosts --- module/plugins/cv_host/views/cv_host.tpl | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/module/plugins/cv_host/views/cv_host.tpl b/module/plugins/cv_host/views/cv_host.tpl index 428e4a8b..0cb4fe60 100644 --- a/module/plugins/cv_host/views/cv_host.tpl +++ b/module/plugins/cv_host/views/cv_host.tpl @@ -138,13 +138,11 @@
    (none)
    %end - %if elt.notes_url: -
    Notes:
    -
    - %for note_url in elt.notes_url: - - %end -
    + %if elt.notes or elt.notes_url: +
    + {{ elt.notes }}
    + {{ elt.notes_url }} +
    %end
    From 0a614a6fb9f5a3f71743d4ddadc771e1b086d1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Thu, 20 Dec 2018 07:45:59 +0100 Subject: [PATCH 19/31] Closes #677 - render notes and actions in the element view --- module/htdocs/css/shinken-layout.css | 2 +- .../views/_eltdetail_information.tpl | 50 +++++++++++++++---- module/plugins/eltdetail/views/eltdetail.tpl | 2 +- .../etc/arbiter/objects/hosts/localhost.cfg | 13 ++++- .../etc/arbiter/objects/hosts/localhost2.cfg | 28 +++++++++-- .../etc/arbiter/objects/hosts/localhost.cfg | 13 ++++- .../etc/arbiter/objects/hosts/localhost2.cfg | 28 +++++++++-- 7 files changed, 113 insertions(+), 23 deletions(-) diff --git a/module/htdocs/css/shinken-layout.css b/module/htdocs/css/shinken-layout.css index 40890413..0aa67e59 100644 --- a/module/htdocs/css/shinken-layout.css +++ b/module/htdocs/css/shinken-layout.css @@ -41,7 +41,7 @@ body { } .page-header { - margin-top:5px; + margin-top:15px; } .breadcrumb { diff --git a/module/plugins/eltdetail/views/_eltdetail_information.tpl b/module/plugins/eltdetail/views/_eltdetail_information.tpl index 2e7963eb..70dc680b 100644 --- a/module/plugins/eltdetail/views/_eltdetail_information.tpl +++ b/module/plugins/eltdetail/views/_eltdetail_information.tpl @@ -223,17 +223,49 @@
    - %if elt.notes_url or elt.action_url or elt.notes: + %if elt.notes or elt.notes_url: - %if elt.notes != '': -

    {{ elt.notes }}

    + + %if elt.notes: +
      + %for note in helper.get_element_notes(elt, popover=False, css='class="list-group-item"'): + {{! note}} %end -
      - %if elt.notes_url != '': - More notes - %end - %if elt.action_url != '': - Launch custom action +
    + %end + + %if elt.notes_url: +
      + %for note in helper.get_element_notes_url(elt, default_title="More notes", default_icon="external-link-square", popover=True, css='class="btn btn-info"'): +
    • {{! note}}
    • + %end +
    + %end + %end + + %if elt.action_url: + +
      + %for action in helper.get_element_actions_url(elt, default_title="Launch custom action", default_icon="cogs", popover=True, css='class="btn btn-warning"'): +
    • {{! action}}
    • + %end +
    + %end + + %elt_type = elt.__class__.my_type + %tags = elt.get_service_tags() if elt_type=='service' else elt.get_host_tags() + %if tags: + %tag='stag' if elt_type=='service' else 'htag' + + %end diff --git a/module/plugins/eltdetail/views/eltdetail.tpl b/module/plugins/eltdetail/views/eltdetail.tpl index b28d0e05..f38aa149 100644 --- a/module/plugins/eltdetail/views/eltdetail.tpl +++ b/module/plugins/eltdetail/views/eltdetail.tpl @@ -23,7 +23,7 @@ Invalid element name %breadcrumb += [[elt.display_name, '/service/'+helper.get_uri_name(elt)]] %end -%js=['js/jquery.sparkline.min.js', 'js/shinken-charts.js', 'availability/js/justgage.js', 'availability/js/raphael-2.1.4.min.js', 'cv_host/js/flot/jquery.flot.min.js', 'cv_host/js/flot/jquery.flot.tickrotor.js', 'cv_host/js/flot/jquery.flot.resize.min.js', 'cv_host/js/flot/jquery.flot.pie.min.js', 'cv_host/js/flot/jquery.flot.categories.min.js', 'cv_host/js/flot/jquery.flot.time.min.js', 'cv_host/js/flot/jquery.flot.stack.min.js', 'cv_host/js/flot/jquery.flot.valuelabels.js', 'eltdetail/js/custom_views.js', 'eltdetail/js/eltdetail.js', 'logs/js/history.js'] +%js=['js/jquery.sparkline.min.js', 'js/shinken-charts.js', 'cv_host/js/flot/jquery.flot.min.js', 'cv_host/js/flot/jquery.flot.tickrotor.js', 'cv_host/js/flot/jquery.flot.resize.min.js', 'cv_host/js/flot/jquery.flot.pie.min.js', 'cv_host/js/flot/jquery.flot.categories.min.js', 'cv_host/js/flot/jquery.flot.time.min.js', 'cv_host/js/flot/jquery.flot.stack.min.js', 'cv_host/js/flot/jquery.flot.valuelabels.js', 'eltdetail/js/custom_views.js', 'eltdetail/js/eltdetail.js', 'logs/js/history.js'] %if app.logs_module.is_available(): %js=js + ['availability/js/justgage.js', 'availability/js/raphael-2.1.4.min.js'] %end diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg index 3c635283..6faf7f90 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg @@ -10,6 +10,15 @@ define host{ contact_groups admins # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 + + tags +fred + + notes simple note... only text but may be formated + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$ + + action_url http://www.my-KB.fr?host=$HOSTNAME$ + } diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg index 890ff6e6..bf2d973c 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg @@ -9,8 +9,6 @@ define host{ contact_groups admins - tags +fred - _satellites arbiter-master$(arbiter)$$(arbiter-master)$$(7770)$,\ scheduler-master$(scheduler)$$(scheduler-master)$$(7768)$,\ scheduler-second$(scheduler)$$(scheduler-second)$$(17768)$,\ @@ -21,6 +19,28 @@ define host{ receiver-master$(receiver)$$(receiver-master)$$(7773)$ # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 + + tags +fred + + notes simple note... only text but may be formated\ + |Title::note with only title...\ + |Title,,file::note with a title and an icon...\ + |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ + |KB5126,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ + vestibulum eu non sapien. Nulla facilisi. \ + Aliquam non blandit tellus, non luctus tortor. \ + Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.,,http://my-url.fr\ + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ + + action_url http://www.google.fr|url1::http://www.google.fr|\ + My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|\ + Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ + } diff --git a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg index 3c635283..6faf7f90 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg @@ -10,6 +10,15 @@ define host{ contact_groups admins # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 + + tags +fred + + notes simple note... only text but may be formated + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$ + + action_url http://www.my-KB.fr?host=$HOSTNAME$ + } diff --git a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg index 890ff6e6..bf2d973c 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg @@ -9,8 +9,6 @@ define host{ contact_groups admins - tags +fred - _satellites arbiter-master$(arbiter)$$(arbiter-master)$$(7770)$,\ scheduler-master$(scheduler)$$(scheduler-master)$$(7768)$,\ scheduler-second$(scheduler)$$(scheduler-second)$$(17768)$,\ @@ -21,6 +19,28 @@ define host{ receiver-master$(receiver)$$(receiver-master)$$(7773)$ # GPS - _LOC_LAT 45.054700 - _LOC_LNG 5.080856 + _LOC_LAT 45.054700 + _LOC_LNG 5.080856 + + tags +fred + + notes simple note... only text but may be formated\ + |Title::note with only title...\ + |Title,,file::note with a title and an icon...\ + |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ + |KB5126,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ + vestibulum eu non sapien. Nulla facilisi. \ + Aliquam non blandit tellus, non luctus tortor. \ + Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.,,http://my-url.fr\ + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ + + action_url http://www.google.fr|url1::http://www.google.fr|\ + My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|\ + Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ + } From a6081cad1427f3fdbd6914930259d5fe66373253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Wed, 19 Dec 2018 05:54:52 +0100 Subject: [PATCH 20/31] Helper functions to render notes and actions --- module/helper.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/module/helper.py b/module/helper.py index 7583e43e..8355148c 100644 --- a/module/helper.py +++ b/module/helper.py @@ -45,6 +45,7 @@ from shinken.misc.sorter import hst_srv_sort from shinken.misc.perfdata import PerfDatas +from shinken.macroresolver import MacroResolver # pylint: disable=no-self-use @@ -905,4 +906,144 @@ def get_event_icon(self, event, disabled=False, label='', use_title=True): ''' % (color, icon_text, label) + def render_url(self, obj, items, popover=False, css=''): + """Returns formatted HTML for an element URL + + If popover is true, a bootstrap popover is built, else a standard link ... + """ + result = [] + for (icon, title, description, url) in items: + if not url and not description: + # Nothing to do here! + continue + + # Replace MACROS in url, title and description + if hasattr(obj, 'get_data_for_checks'): + if url: + url = MacroResolver().resolve_simple_macros_in_string(url, + obj.get_data_for_checks()) + if title: + title = MacroResolver().resolve_simple_macros_in_string(title, + obj.get_data_for_checks()) + if description: + description = MacroResolver().resolve_simple_macros_in_string(description, + obj.get_data_for_checks()) + + if not description: + description = url + + if not title: + popover = False + + if icon: + icon = '' % icon + else: + icon = '' + + content = ' %s' % description + if popover: + content = ' %s' % title + + if popover: + for code in [['&', '&'], ['<', '<'], ['>', '>'], ['"', '"']]: + description = description.replace(code[0], code[1]) + + popover = 'data-toggle="popover medium" data-html="true" data-content="%s" ' \ + 'data-trigger="hover focus" data-placement="bottom"' % description + else: + popover = '' + + if not url: + url = 'href="#" ' + else: + url = 'href="%s" target="_blank" ' % url + + result.append('%s%s' % (url, css, popover, icon, content)) + + return result + + def get_element_urls(self, obj, property, default_title=None, default_icon=None, popover=False, css=''): + """"Return list of element notes urls + + The notes field is a string in which individual notes are separated with a | character. + + Each note is composed of a left part and a right part: description::url where url is + never present in the Nagios legacy syntax :) + + notes_url contain a list of | separated URLs that would be rendered + as navigable links. If a left part exist left::url, the left part is composed as an + individual note + + Indeed notes, notes_url and actions_url are the same encoding: description::url where + description and url are optional! If url is present, it will be rendered as a navigable + link in the UI. If description is present, it may be title,,icon + + As a sump-up, a notes declaration with all kind of notes: + notes simple note... only text but may be formated\ + |Title::note with only title...\ + |Title,,file::note with a title and an icon...\ + |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ + |KB1023,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ + vestibulum eu non sapien. Nulla facilisi. \ + Aliquam non blandit tellus, non luctus tortor. \ + Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.\ + + notesèurl and actionsèurl may be declared with the same information! + + """ + if not obj or not hasattr(obj, property): + return [] + + # We build a list of: title, icon, description, url + notes = [] + + # Several notes are defined in the notes attribute with | character + for item in getattr(obj, property).split('|'): + # A note is: [[title][,,icon]::]description[,,url] + try: + (decoration, description) = item.split('::') + except: + decoration = "%s,,%s" % (default_title, default_icon) + description = item + + try: + (title, icon) = decoration.split(',,') + except: + icon = default_icon + title = decoration + + if popover and not title: + title = description[5:] + + try: + (description, url) = description.split(',,') + except: + # description = 'No description provided' + url = None + + notes.append((icon, title, description, url)) + + return self.render_url(obj, notes, popover=popover, css=css) + + def get_element_notes(self, obj, default_title=None, default_icon=None, popover=False, css=''): + """"See the comment of get_element_urls""" + return self.get_element_urls(obj, 'notes', + default_title=default_title, default_icon=default_icon, + popover=popover, css=css) + + def get_element_notes_url(self, obj, default_title=None, default_icon=None, popover=False, css=''): + """"See the comment of get_element_urls""" + return self.get_element_urls(obj, 'notes_url', + default_title=default_title, default_icon=default_icon, + popover=popover, css=css) + + def get_element_actions_url(self, obj, default_title=None, default_icon=None, popover=False, css=''): + """"See the comment of get_element_urls""" + return self.get_element_urls(obj, 'action_url', + default_title=default_title, default_icon=default_icon, + popover=popover, css=css) + + helper = Helper() From 2d197ffadf91c2522b87ee5b8bfdfe5aee47a323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Fri, 21 Dec 2018 17:12:12 +0100 Subject: [PATCH 21/31] Closes #699 - enhance plain text search engine --- module/datamanager.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/module/datamanager.py b/module/datamanager.py index 9c9858fd..f52a6661 100644 --- a/module/datamanager.py +++ b/module/datamanager.py @@ -409,21 +409,23 @@ def search_hosts_and_services(self, search, user, get_impacts=True, sorter=None, # Case insensitive pat = re.compile(s, re.IGNORECASE) new_items = [] + # Ordered search in all the items for: displa_name, alias, name and notes for i in items: - if (pat.search(i.get_full_name()) - or (i.__class__.my_type == 'host' and i.alias and pat.search(i.alias))): + if (pat.search(getattr(i, 'display_name', '')) or pat.search(getattr(i, 'alias', '')) + or pat.search(i.get_full_name()) or pat.search(getattr(i, 'notes', ''))): new_items.append(i) - else: - for j in i.impacts + i.source_problems: - if (pat.search(j.get_full_name()) - or (j.__class__.my_type == 'host' and j.alias and pat.search(j.alias))): - new_items.append(i) + # Nothing found in all the items if not new_items: for i in items: + # Search in the last check output if pat.search(i.output): new_items.append(i) - else: + + if not new_items: + # Nothing found in the checks output + for i in items: + # Search in the impacts and source problems last check output for j in i.impacts + i.source_problems: if pat.search(j.output): new_items.append(i) From 8aadced7fd8dcdee18a01e907ca3804c823d56d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Fri, 28 Dec 2018 11:21:34 +0100 Subject: [PATCH 22/31] Improve hovering title (Aka) for host name if host has a display_name --- module/helper.py | 3 + module/plugins/problems/views/problems.tpl | 4 + .../alignak/commands/check_kibana | 207 ++++++++++++++++++ 3 files changed, 214 insertions(+) create mode 100755 test/test-configurations/alignak/commands/check_kibana diff --git a/module/helper.py b/module/helper.py index 8355148c..2954b058 100644 --- a/module/helper.py +++ b/module/helper.py @@ -327,6 +327,9 @@ def get_fa_icon_state(self, obj=None, cls='host', state='UP', disabled=False, la else: icon = icons[cls].get(state, 'UNKNOWN') + if obj and not (obj.active_checks_enabled or obj.passive_checks_enabled): + icon_color = 'bg-lightgrey' + front = '''''' % (icon, icon_color) if use_title: diff --git a/module/plugins/problems/views/problems.tpl b/module/plugins/problems/views/problems.tpl index b3ab9aa1..b232d764 100644 --- a/module/plugins/problems/views/problems.tpl +++ b/module/plugins/problems/views/problems.tpl @@ -123,7 +123,11 @@ Next check {{helper.print_duration(pb.next_chk)}} %aka = '' %if pb_host.alias and not pb_host.alias.startswith(pb_host.get_name()): + %if pb_host.display_name: + %aka = 'Aka %s (%s)' % (pb_host.alias.replace(' ', '
    '), pb_host.get_name()) + %else: %aka = 'Aka %s' % pb_host.alias.replace(' ', '
    ') + %end %end %if pb.host_name != previous_pb_host_name: diff --git a/test/test-configurations/alignak/commands/check_kibana b/test/test-configurations/alignak/commands/check_kibana new file mode 100755 index 00000000..8e1b81b6 --- /dev/null +++ b/test/test-configurations/alignak/commands/check_kibana @@ -0,0 +1,207 @@ +#!/usr/bin/perl +############################################################################## +# +# Nagios check_kibana plugin +# +# License: GPL +# Copyright (c) 2013 Joerg Linge +# +# Description: +# +# This plugin is used to query kibana ( http://kibana.org ) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## + +use strict; +use warnings; +use utf8; +use Encode qw( encode_utf8 ); +use URI::Escape; +use Data::Dumper; +use Nagios::Plugin; + +unless ( eval 'use JSON::XS;1' ){ + die "Perl Module JSON::XS not found"; +} +unless ( eval 'use LWP::Simple;1' ){ + die "Perl Module LWP::Simple not found"; +} +unless ( eval "use Nagios::Plugin;1" ){ + die "Perl Module Nagios::Plugin not found"; +} +unless ( eval "use MIME::Base64;1" ){ + die "Perl Module MIME::Base84 not found"; +} + +my $np = Nagios::Plugin->new( + usage => "Usage: %s [-t ] " . "[-U ]", + version => '0.1', + plugin => 'check_kibana', + timeout => 15, +); + +$np->add_arg( + spec => 'url|U=s', + help => + '-U, --url=. URL to Kibana Webinterface' +); + +$np->add_arg( + spec => 'query|q=s', + help => + '-q, --query=. Base64 encoded Query as shown in the Kibana UI' +); + +$np->add_arg( + spec => 'label|l=s', + help => + '-l, --label=
    diff --git a/module/plugins/eltdetail/views/eltdetail.tpl b/module/plugins/eltdetail/views/eltdetail.tpl index f38aa149..a55beddb 100644 --- a/module/plugins/eltdetail/views/eltdetail.tpl +++ b/module/plugins/eltdetail/views/eltdetail.tpl @@ -4,23 +4,27 @@ %if not elt: %rebase("layout", title='Invalid element name') - Invalid element name +%end -%else: %user = app.get_user() %helper = app.helper -%from shinken.macroresolver import MacroResolver - +%# Element information +%# - type %elt_type = elt.__class__.my_type +%# - is monitored +%elt_monitored = elt.active_checks_enabled or elt.passive_checks_enabled +%# - defined check command +%elt_checked = True if elt.check_command else False -%breadcrumb = [['All '+elt_type.title()+'s', '/'+elt_type+'s-groups']] +%breadcrumb = [[elt_type.title(), '/'+elt_type+'s-groups']] %if elt_type == 'host': -%breadcrumb += [[elt.display_name if elt.display_name else elt.get_name(), '/host/'+elt.host_name]] +%#breadcrumb += [[elt.display_name if elt.display_name else elt.get_name(), '/host/'+elt.host_name]] +%breadcrumb += [[elt.host_name, '/host/'+elt.host_name]] %elif elt_type == 'service': -%breadcrumb += [[elt.host.display_name if elt.host.display_name else elt.host.get_name(), '/host/'+elt.host_name]] -%breadcrumb += [[elt.display_name, '/service/'+helper.get_uri_name(elt)]] +%breadcrumb += [[elt.host_name, '/host/'+elt.host_name]] +%breadcrumb += [[elt.service_description, '/service/'+helper.get_uri_name(elt)]] %end %js=['js/jquery.sparkline.min.js', 'js/shinken-charts.js', 'cv_host/js/flot/jquery.flot.min.js', 'cv_host/js/flot/jquery.flot.tickrotor.js', 'cv_host/js/flot/jquery.flot.resize.min.js', 'cv_host/js/flot/jquery.flot.pie.min.js', 'cv_host/js/flot/jquery.flot.categories.min.js', 'cv_host/js/flot/jquery.flot.time.min.js', 'cv_host/js/flot/jquery.flot.stack.min.js', 'cv_host/js/flot/jquery.flot.valuelabels.js', 'eltdetail/js/custom_views.js', 'eltdetail/js/eltdetail.js', 'logs/js/history.js'] @@ -39,7 +43,7 @@ Invalid element name
    @@ -112,7 +116,7 @@ Invalid element name
    %end - %if elt.check_command and elt.get_check_command().startswith('bp_rule'): + %if elt_checked and elt.get_check_command().startswith('bp_rule'):
    This element is a business rule.
    %end @@ -234,5 +238,3 @@ Invalid element name
    %include("_eltdetail_action-menu.tpl") - -%end diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg index 6faf7f90..9b4da89d 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost.cfg @@ -13,12 +13,22 @@ define host{ _LOC_LAT 45.054700 _LOC_LNG 5.080856 - tags +fred - - notes simple note... only text but may be formated - - notes_url http://www.my-KB.fr?host=$HOSTADDRESS$ - - action_url http://www.my-KB.fr?host=$HOSTNAME$ - + notes simple note... only text but may be formated\ + |Title::note with only title...\ + |Title,,file::note with a title and an icon...\ + |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ + |KB5126,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ + vestibulum eu non sapien. Nulla facilisi. \ + Aliquam non blandit tellus, non luctus tortor. \ + Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.,,http://my-url.fr\ + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ + + action_url http://www.google.fr|url1::http://www.google.fr|\ + My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|\ + Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ } diff --git a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg index bf2d973c..e5a07a68 100644 --- a/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg +++ b/test/test-configurations/alignak/etc/arbiter/objects/hosts/localhost2.cfg @@ -22,8 +22,6 @@ define host{ _LOC_LAT 45.054700 _LOC_LNG 5.080856 - tags +fred - notes simple note... only text but may be formated\ |Title::note with only title...\ |Title,,file::note with a title and an icon...\ @@ -42,5 +40,4 @@ define host{ Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ - } diff --git a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg index 6faf7f90..9b4da89d 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost.cfg @@ -13,12 +13,22 @@ define host{ _LOC_LAT 45.054700 _LOC_LNG 5.080856 - tags +fred - - notes simple note... only text but may be formated - - notes_url http://www.my-KB.fr?host=$HOSTADDRESS$ - - action_url http://www.my-KB.fr?host=$HOSTNAME$ - + notes simple note... only text but may be formated\ + |Title::note with only title...\ + |Title,,file::note with a title and an icon...\ + |Title,,file::note with a title and an icon and an url...,,http://my-url.fr\ + |KB5126,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa. Vestibulum id tincidunt lacus. Ut in arcu at ex egestas \ + vestibulum eu non sapien. Nulla facilisi. \ + Aliquam non blandit tellus, non luctus tortor. \ + Mauris tortor libero, egestas quis rhoncus in, sollicitudin et tortor.,,http://my-url.fr\ + + notes_url http://www.my-KB.fr?host=$HOSTADDRESS$|http://www.my-KB.fr?host=$HOSTNAME$ + + action_url http://www.google.fr|url1::http://www.google.fr|\ + My KB,,tag::http://www.my-KB.fr?host=$HOSTNAME$|\ + Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ + nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ } diff --git a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg index bf2d973c..e5a07a68 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/hosts/localhost2.cfg @@ -22,8 +22,6 @@ define host{ _LOC_LAT 45.054700 _LOC_LNG 5.080856 - tags +fred - notes simple note... only text but may be formated\ |Title::note with only title...\ |Title,,file::note with a title and an icon...\ @@ -42,5 +40,4 @@ define host{ Last URL,,tag::Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ Proin et leo gravida, lobortis nunc nec, imperdiet odio. Vivamus quam velit, scelerisque \ nec egestas et, semper ut massa.,,http://www.my-KB.fr?host=$HOSTADDRESS$ - } From 292e22b8046d0e26b1b0527edea9b61ba4b00527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Tue, 18 Dec 2018 15:29:12 +0100 Subject: [PATCH 27/31] Alert if user and BIs are not consistent (log + user preference message) Disable the unhandled BIs in the filter menu --- module/module.py | 8 ++++++ module/plugins/user/views/user_pref.tpl | 33 +++++++++++++++++++------ module/views/_filters.tpl | 13 +++++----- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/module/module.py b/module/module.py index 834b7fe9..af908a97 100644 --- a/module/module.py +++ b/module/module.py @@ -1018,12 +1018,20 @@ def check_authentication(self, username, password): return False user = User.from_contact(c) + logger.info("[WebUI] User minimum business impact: %s", user.min_business_impact) self.user_session = self.auth_module.get_session() logger.info("[WebUI] User session: %s", self.user_session) self.user_info = self.auth_module.get_user_info() logger.info("[WebUI] User information: %s", self.user_info) + # Raise an lert if the user has a BI defined and this BI is lower than the UI's one + if user.min_business_impact and user.min_business_impact < self.problems_business_impact: + logger.warning("[WebUI] the user minimum business impact (%d) is lower than " + "the UI configured business impact (%d). You should update the UI " + "configuration to suit your users configuration. Some items will be " + "filtered wheres the user may expect to view them!", + user.min_business_impact, self.problems_business_impact) return True logger.warning("[WebUI] The user '%s' has not been authenticated.", username) diff --git a/module/plugins/user/views/user_pref.tpl b/module/plugins/user/views/user_pref.tpl index a92dae35..305c8f86 100644 --- a/module/plugins/user/views/user_pref.tpl +++ b/module/plugins/user/views/user_pref.tpl @@ -15,13 +15,8 @@

    - {{ !helper.get_contact_avatar(user, with_name=False, with_link=False) }} + {{ ! helper.get_contact_avatar(user, with_name=False, with_link=False) }} {{ user.get_name() }} - %if user.is_admin: - - %elif app.can_action(username): - - %end

    @@ -35,14 +30,34 @@ Identification: {{"%s (%s)" % (user.alias, user.contact_name) if user.alias != 'none' else user.contact_name}} + + Administrator: + {{! app.helper.get_on_off(user.is_admin, "Is this contact an administrator?")}} + %if user.is_admin: + + %end + + Commands authorized: - {{! app.helper.get_on_off(app.can_action(), "Is this contact allowed to launch commands from Web UI?")}} + {{! app.helper.get_on_off(app.can_action(), "Is this contact allowed to launch commands from Web UI?")}} + %if app.can_action(username): + + %end + Minimum business impact: {{!helper.get_business_impact_text(user.min_business_impact, text=True)}} + %# Raise an alert if the user has a BI defined and this BI is lower than the UI's one + %if user.min_business_impact and user.min_business_impact < app.problems_business_impact: + + + The UI is configured for a minimum business impact ({{! helper.get_business_impact_text(app.problems_business_impact, text=True) }}). You may not view some hosts/services that you are searching for... + + + %end

    User attributes:

    @@ -53,7 +68,11 @@ %end {{attr}}: + %if value or (isinstance(value, list) and value[0]): {{value}} + %else: + - + %end %end diff --git a/module/views/_filters.tpl b/module/views/_filters.tpl index e92c6f76..b41497d3 100644 --- a/module/views/_filters.tpl +++ b/module/views/_filters.tpl @@ -26,12 +26,13 @@
  • Acknowledged problems
  • Scheduled downtimes
  • -
  • Impact : {{!helper.get_business_impact_text(5, text=True)}}
  • -
  • Impact : {{!helper.get_business_impact_text(4, text=True)}}
  • -
  • Impact : {{!helper.get_business_impact_text(3, text=True)}}
  • -
  • Impact : {{!helper.get_business_impact_text(2, text=True)}}
  • -
  • Impact : {{!helper.get_business_impact_text(1, text=True)}}
  • -
  • Impact : {{!helper.get_business_impact_text(0, text=True)}}
  • + %business_impact = max(user.min_business_impact, app.problems_business_impact) + %for idx in range(5, business_impact - 1, -1): +
  • Impact : {{! helper.get_business_impact_text(idx, text=True)}}
  • + %end + %for idx in range(business_impact - 1, -1, -1): + + %end
  • Search syntax
  • From aedbfda4da5606f3c09326372387a30e78b1ad2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Wed, 19 Dec 2018 03:30:41 +0100 Subject: [PATCH 28/31] Manage BI filter in the search query parameter --- module/datamanager.py | 39 +++++++++------------ module/htdocs/js/shinken-actions.js | 2 ++ module/module.py | 54 +++++++++++++++++++++++++++-- module/plugins/problems/problems.py | 2 +- 4 files changed, 72 insertions(+), 25 deletions(-) diff --git a/module/datamanager.py b/module/datamanager.py index f52a6661..286bcfce 100644 --- a/module/datamanager.py +++ b/module/datamanager.py @@ -44,6 +44,22 @@ from shinken.objects.command import Command, Commands from shinken.misc.sorter import worse_first, last_state_change_earlier +# Search patterns like: isnot:0 isnot:ack isnot:"downtime fred" name "vm fred" +SEARCH_QUERY_PATTERNS = r''' + # 1/ Search a key:value pattern. + (?P\w+): # Key consists of only a word followed by a colon + (?P["']?) # Optional quote character. + (?P.*?) # Value is a non greedy match + (?P=quote2) # Closing quote equals the first. + ($|\s) # Entry ends with whitespace or end of string + | # OR + # 2/ Search a single string quoted or not + (?P["']?) # Optional quote character. + (?P.*?) # Name is a non greedy match + (?P=quote) # Closing quote equals the opening one. + ($|\s) # Entry ends with whitespace or end of string + ''' + class WebUIDataManager(DataManager): @@ -363,31 +379,10 @@ def search_hosts_and_services(self, search, user, get_impacts=True, sorter=None, items.extend(self._only_related_to(super(WebUIDataManager, self).get_hosts(), user)) items.extend(self._only_related_to(super(WebUIDataManager, self).get_services(), user)) - # Filter items according to the logged-in user minimum business impact or the minimum business impact - business_impact = max(user.min_business_impact, self.problems_business_impact) - logger.debug("[WebUI - datamanager] business impact, filtering %d items >= %d (user: %d, ui: %d)", - len(items), business_impact, user.min_business_impact, self.problems_business_impact) - items = [i for i in items if i.business_impact >= business_impact] - logger.debug("[WebUI - datamanager] search_hosts_and_services, search for %s in %d items", search, len(items)) # Search patterns like: isnot:0 isnot:ack isnot:"downtime fred" name "vm fred" - regex = re.compile( - r''' - # 1/ Search a key:value pattern. - (?P\w+): # Key consists of only a word followed by a colon - (?P["']?) # Optional quote character. - (?P.*?) # Value is a non greedy match - (?P=quote2) # Closing quote equals the first. - ($|\s) # Entry ends with whitespace or end of string - | # OR - # 2/ Search a single string quoted or not - (?P["']?) # Optional quote character. - (?P.*?) # Name is a non greedy match - (?P=quote) # Closing quote equals the opening one. - ($|\s) # Entry ends with whitespace or end of string - ''', - re.VERBOSE) + regex = re.compile(SEARCH_QUERY_PATTERNS, re.VERBOSE) # Replace "NOT foo" by "^((?!foo).)*$" to ignore foo search = re.sub(r'NOT ([^\ ]*)', r'^((?!\1).)*$', search) diff --git a/module/htdocs/js/shinken-actions.js b/module/htdocs/js/shinken-actions.js index 61ee7c5a..3131271e 100644 --- a/module/htdocs/js/shinken-actions.js +++ b/module/htdocs/js/shinken-actions.js @@ -609,9 +609,11 @@ $("body").on("click", ".js-recheck", function () { var elt = get_action_element($(this)); if (elt) { + console.log(elt); recheck_now(elt); } else { $.each(selected_elements, function(idx, name){ + console.log("Recheck selected", elt); recheck_now(name); }); } diff --git a/module/module.py b/module/module.py index af908a97..22a27a60 100644 --- a/module/module.py +++ b/module/module.py @@ -36,6 +36,7 @@ import os import json +import re import string import random import traceback @@ -85,7 +86,7 @@ import bottle # Local import -from datamanager import WebUIDataManager +from datamanager import WebUIDataManager, SEARCH_QUERY_PATTERNS from ui_user import User from helper import helper @@ -1114,7 +1115,56 @@ def get_ui_external_links(self): return lst def get_search_string(self, default=""): - return self.request.query.get('search', default) + """Manage the query parameter `search` + + The business impact logic behind this: + - the UI defined BI (problems_business_impact) is considered first + - the user minimum BI is then considered + + Thus, the maximum value of both BIs is added as a filter in the search query if no + BI is specified in the search query. If more than one bi: exist in the search query + the last one is the only one that is kept in the query parameter. + """ + if not self.request.route.config.get('search_engine', None): + # Requested URL do not include the search engine + logger.debug("No search engine on this page!") + return '' + + search_string = self.request.query.get('search', default) + + # Force include the UI BI in the search query if it is not yet present + bi = self.problems_business_impact + if "bi:" not in search_string: + # Include BI as the first search criterion + return ("bi:>=%d " % bi) + search_string + + # Search patterns like: isnot:0 isnot:ack isnot:"downtime fred" name "vm fred" + regex = re.compile(SEARCH_QUERY_PATTERNS, re.VERBOSE) + logger.debug("Search string: %s", search_string) + new_ss = '' + for match in regex.finditer(search_string): + if not match.group('key'): + continue + if match.group('key') not in ['bp', 'bi']: + logger.debug("- match: %s / %s", match.group('key'), match.group('value')) + new_ss += match.group('key') + new_ss += match.group('value') + continue + logger.debug("Found BI filter: %s", match.group('value')) + s = match.group('value') + bi = self.problems_business_impact + try: + if s.startswith('>=') or s.startswith('<='): + bi = int(s[2:]) + elif s.startswith('>') or s.startswith('<') or s.startswith('='): + bi = int(s[1:]) + 1 + except ValueError: + pass + logger.debug("- search BI is >=%d", bi) + new_ss = ("bi:>=%d " % bi) + new_ss + logger.debug("- updated search string: %s", new_ss) + + return new_ss def redirect404(self, msg="Not found"): raise self.bottle.HTTPError(404, msg) diff --git a/module/plugins/problems/problems.py b/module/plugins/problems/problems.py index 62c4525e..a46aa1b5 100644 --- a/module/plugins/problems/problems.py +++ b/module/plugins/problems/problems.py @@ -35,7 +35,7 @@ def get_page(): - app.bottle.redirect("/all?search=%s" % DEFAULT_FILTER) + app.bottle.redirect("/all?search=bi:>=%d %s" % (app.problems_business_impact, DEFAULT_FILTER)) def get_all(): From 9e1c187d5f8af9c0f9e9f091b5eafacc3e15e1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Mon, 31 Dec 2018 17:42:45 +0100 Subject: [PATCH 29/31] BI cleaning - review comments --- module/htdocs/js/shinken-actions.js | 1 - module/module.py | 13 ++++----- module/plugins/problems/problems.py | 8 ++++-- module/views/_filters.tpl | 10 ++++++- .../etc/arbiter/objects/contacts/admin.cfg | 17 ++++++++++++ .../etc/arbiter/objects/contacts/guest.cfg | 18 ++++++++++++- .../shinken/etc/modules/webui2.cfg | 27 ++++++++++++------- 7 files changed, 74 insertions(+), 20 deletions(-) diff --git a/module/htdocs/js/shinken-actions.js b/module/htdocs/js/shinken-actions.js index 3131271e..9fdd23e3 100644 --- a/module/htdocs/js/shinken-actions.js +++ b/module/htdocs/js/shinken-actions.js @@ -613,7 +613,6 @@ $("body").on("click", ".js-recheck", function () { recheck_now(elt); } else { $.each(selected_elements, function(idx, name){ - console.log("Recheck selected", elt); recheck_now(name); }); } diff --git a/module/module.py b/module/module.py index 22a27a60..0bc7113b 100644 --- a/module/module.py +++ b/module/module.py @@ -1133,10 +1133,11 @@ def get_search_string(self, default=""): search_string = self.request.query.get('search', default) # Force include the UI BI in the search query if it is not yet present - bi = self.problems_business_impact + user = self.request.environ['USER'] + bi = user.min_business_impact or self.problems_business_impact if "bi:" not in search_string: - # Include BI as the first search criterion - return ("bi:>=%d " % bi) + search_string + # Include BI as the last search criterion + return search_string + (" bi:>=%d" % bi) # Search patterns like: isnot:0 isnot:ack isnot:"downtime fred" name "vm fred" regex = re.compile(SEARCH_QUERY_PATTERNS, re.VERBOSE) @@ -1147,8 +1148,8 @@ def get_search_string(self, default=""): continue if match.group('key') not in ['bp', 'bi']: logger.debug("- match: %s / %s", match.group('key'), match.group('value')) - new_ss += match.group('key') - new_ss += match.group('value') + new_ss += match.group('key') + ':' + new_ss += match.group('value') + ' ' continue logger.debug("Found BI filter: %s", match.group('value')) s = match.group('value') @@ -1161,7 +1162,7 @@ def get_search_string(self, default=""): except ValueError: pass logger.debug("- search BI is >=%d", bi) - new_ss = ("bi:>=%d " % bi) + new_ss + new_ss = new_ss + ("bi:>=%d" % bi) logger.debug("- updated search string: %s", new_ss) return new_ss diff --git a/module/plugins/problems/problems.py b/module/plugins/problems/problems.py index a46aa1b5..2ee817eb 100644 --- a/module/plugins/problems/problems.py +++ b/module/plugins/problems/problems.py @@ -35,14 +35,18 @@ def get_page(): - app.bottle.redirect("/all?search=bi:>=%d %s" % (app.problems_business_impact, DEFAULT_FILTER)) + user = app.bottle.request.environ['USER'] + app.bottle.redirect("/all?search=%s bi:>=%d" + % (DEFAULT_FILTER, + user.min_business_impact or app.problems_business_impact)) def get_all(): user = app.bottle.request.environ['USER'] # Update the default filter according to the logged-in user minimum business impact - default_filtering = DEFAULT_FILTER + " bi:>=%d" % (user.min_business_impact) + default_filtering = DEFAULT_FILTER + " bi:>=%d" \ + % (user.min_business_impact or app.problems_business_impact) # Fetch elements per page preference for user, default is 25 elts_per_page = app.prefs_module.get_ui_user_preference(user, 'elts_per_page', 25) diff --git a/module/views/_filters.tpl b/module/views/_filters.tpl index b41497d3..c15f1b6d 100644 --- a/module/views/_filters.tpl +++ b/module/views/_filters.tpl @@ -26,13 +26,21 @@
  • Acknowledged problems
  • Scheduled downtimes
  • - %business_impact = max(user.min_business_impact, app.problems_business_impact) + %if user.is_administrator(): + %# Only administrators are allowed to view any BI + %for idx in range(5, -1, -1): +
  • Impact : {{! helper.get_business_impact_text(idx, text=True)}}
  • + %end + %else: + %# Other users are restricted to their own BI or the UI's configured BI + %business_impact = user.min_business_impact or app.problems_business_impact %for idx in range(5, business_impact - 1, -1):
  • Impact : {{! helper.get_business_impact_text(idx, text=True)}}
  • %end %for idx in range(business_impact - 1, -1, -1): %end + %end
  • Search syntax
  • diff --git a/test/test-configurations/shinken/etc/arbiter/objects/contacts/admin.cfg b/test/test-configurations/shinken/etc/arbiter/objects/contacts/admin.cfg index 4b1e94e5..d01168ca 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/contacts/admin.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/contacts/admin.cfg @@ -18,3 +18,20 @@ define contact{ can_submit_commands 1 } +define contact{ + use generic-contact + contact_name admin2 + alias Administrator 2 + +# email alignak@localhost +# pager 0600000000 + + # Minium business impact - Normal and more important only + min_business_impact 3 + + # Only useful for the UI... + password admin2 + is_admin 1 + can_submit_commands 1 +} + diff --git a/test/test-configurations/shinken/etc/arbiter/objects/contacts/guest.cfg b/test/test-configurations/shinken/etc/arbiter/objects/contacts/guest.cfg index 9d9cc87d..43b4f916 100644 --- a/test/test-configurations/shinken/etc/arbiter/objects/contacts/guest.cfg +++ b/test/test-configurations/shinken/etc/arbiter/objects/contacts/guest.cfg @@ -8,11 +8,27 @@ define contact{ email guest@localhost + # Minium business impact - All elements + min_business_impact 0 + + # Only useful for the UI... + password guest + is_admin 0 + can_submit_commands 0 +} + +define contact{ + use generic-contact + contact_name guest2 + alias Guest + + email guest@localhost + # Minium business impact - Only important elements min_business_impact 3 # Only useful for the UI... - password guest + password guest2 is_admin 0 can_submit_commands 0 } diff --git a/test/test-configurations/shinken/etc/modules/webui2.cfg b/test/test-configurations/shinken/etc/modules/webui2.cfg index c471dddb..d6db831b 100644 --- a/test/test-configurations/shinken/etc/modules/webui2.cfg +++ b/test/test-configurations/shinken/etc/modules/webui2.cfg @@ -166,23 +166,32 @@ define module { # Visual alerting thresholds # -------------------------- + # problems_business_impact is the main UI filter # All the hosts and services that are in a HARD non OK/UP state are considered as problems if their # business_impact is greater than or equal this value - # problems_business_impact is the main UI filter - # the UI views are filtered according to this value which defaults to 0 (no filtering) + # The UI views are filtered according to this value which defaults to 1 (ignore only no importance) + # Shinken/Alignak consider 0 as 'no importance' hosts or services + # 0 - no-importance + # 1 - qualification + # 2 - normal + # 3 - production + # 4 - important + # 5 - top-for-business #problems_business_impact 1 - # important_problems_business_impact is used to filter the alerting badges in the header bar (default is 2) - #important_problems_business_impact 2 + + # The important_problems_business_impact value is used currently to display alert badges in the + # top navigation bar. This allows to visually alert only for the most important problems + #important_problems_business_impact 3 # Note that the connected user may also have his/her own min_business_impact. This filter is taking - # precedence over the UI min_business_impact which takes precedence over the important_problems_business_impact! + # precedence over the UI problems_business_impact which takes precedence over the important_problems_business_impact! - # Set to 0 to deactivate Web UI own problems computation rules - # This will make the WebUI build its own 'problem' state for the hosts and services + # Set to 1 to deactivate Web UI own problems computation rules + # As per default the WebUI builds its own 'problem' state for the hosts and services # This makes the UI more consistent for the end-users - # If 0, the is_problem state of Shinken/Alignak is used to count the problems. - #inner_problems_count 0 + # If 1, the is_problem state of Shinken/Alignak is used to count and report the problems. + #disable_inner_problems_computation 0 # Used in the dashboard view to select background color for percentages #hosts_states_warning 95 From 48c3e0f45b67d85205306b2e5317f5270257a5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Tue, 1 Jan 2019 13:02:52 +0100 Subject: [PATCH 30/31] Add a duration check criteria: last_check Allows to filter elements on the last check time --- module/datamanager.py | 7 +++++-- module/views/modal_helpsearch.tpl | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/module/datamanager.py b/module/datamanager.py index 286bcfce..9e58caf0 100644 --- a/module/datamanager.py +++ b/module/datamanager.py @@ -543,9 +543,12 @@ def search_hosts_and_services(self, search, user, get_impacts=True, sorter=None, except ValueError: items = [] - if t == 'duration': + if t in ['duration', 'last_check']: seconds_per_unit = {"s": 1, "m": 60, "h": 3600, "d": 86400, "w": 604800} - times = [(i, time.time() - int(i.last_state_change)) for i in items] + if t == 'duration': + times = [(i, time.time() - int(i.last_state_change)) for i in items] + else: + times = [(i, time.time() - int(i.last_chk)) for i in items] try: if s.startswith('>='): s = int(s[2:-1]) * seconds_per_unit[s[-1].lower()] diff --git a/module/views/modal_helpsearch.tpl b/module/views/modal_helpsearch.tpl index 77aadf94..2084043d 100644 --- a/module/views/modal_helpsearch.tpl +++ b/module/views/modal_helpsearch.tpl @@ -62,10 +62,11 @@

    Note that the WebUI has a default configured filter. Hosts and services that do not have a BI greater than or equal this filter will not be dislayed in the UI. Currently, this filter is bi>={{app.problems_business_impact}}.

    Search by duration

    -

    You can also search by the duration of the last state change. This is very useful to find elements that are warning or critical for a long time. For example:

    +

    You can also search by the duration of the last state change or by the elapsed time since the last check. This is very useful to find elements that are warning or critical for a long time or that did not get checked since a while. For example:

    • isnot:OK duration:>1w Matches hosts and services not OK for at least one week.
    • isnot:OK duration:<1h Matches hosts and services not OK for less than one hour.
    • +
    • last_check:>1d Matches hosts and services not checked in the last day.

    You can use the following time units: s(econds), m(inutes), h(ours), d(ays), w(eeks).

    From 6c4a73e1db8dae6a9958f6914765532570149546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20MOHIER?= Date: Wed, 2 Jan 2019 18:20:13 +0100 Subject: [PATCH 31/31] Clean the anonymous user mode and the currently view --- module/module.py | 95 +++----- module/plugins/dashboard/dashboard.py | 4 +- module/plugins/dashboard/views/currently.tpl | 232 +++++++++---------- module/plugins/login/login.py | 20 +- module/plugins/user/user.py | 12 +- module/ui_user.py | 14 ++ 6 files changed, 178 insertions(+), 199 deletions(-) diff --git a/module/module.py b/module/module.py index 0bc7113b..581fc7d3 100644 --- a/module/module.py +++ b/module/module.py @@ -257,9 +257,9 @@ def __init__(self, modconf): logger.info("[WebUI] server: %s:%d", self.host, self.port) # Build session cookie - self.session_cookie = getattr(modconf, 'cookie_name', 'user') + self.session_cookie = getattr(modconf, 'cookie_name', 'user_session') self.auth_secret = resolve_auth_secret(modconf) - logger.info("[WebUI] cookie: %s", self.session_cookie) + logger.info("[WebUI] user session cookie name: %s", self.session_cookie) # TODO : common preferences self.play_sound = to_bool(getattr(modconf, 'play_sound', '0')) logger.info("[WebUI] sound: %s", self.play_sound) @@ -1026,7 +1026,10 @@ def check_authentication(self, username, password): self.user_info = self.auth_module.get_user_info() logger.info("[WebUI] User information: %s", self.user_info) - # Raise an lert if the user has a BI defined and this BI is lower than the UI's one + if self.user_session and self.user_info: + user.set_information(self.user_session, self.user_info) + + # Raise an alert if the user has a BI defined and this BI is lower than the UI's one if user.min_business_impact and user.min_business_impact < self.problems_business_impact: logger.warning("[WebUI] the user minimum business impact (%d) is lower than " "the UI configured business impact (%d). You should update the UI " @@ -1038,36 +1041,6 @@ def check_authentication(self, username, password): logger.warning("[WebUI] The user '%s' has not been authenticated.", username) return False - ## - # For compatibility with previous defined views ... - ## - # pylint: disable=undefined-variable - def get_user_auth(self): - logger.warning("[WebUI] Deprecated - Getting authenticated user ...") - self.user_session = None - self.user_info = None - - cookie_value = webui_app.request.get_cookie(str(app.session_cookie), secret=app.auth_secret) - if cookie_value: - if 'session' in cookie_value: - self.user_session = cookie_value['session'] - logger.debug("[WebUI] user session: %s", cookie_value['session']) - if 'info' in cookie_value: - self.user_info = cookie_value['info'] - logger.debug("[WebUI] user info: %s", cookie_value['info']) - if 'login' in cookie_value: - logger.debug("[WebUI] user login: %s", cookie_value['login']) - contact = app.datamgr.get_contact(name=cookie_value['login']) - else: - contact = app.datamgr.get_contact(name=cookie_value) - else: - contact = app.datamgr.get_contact(name='anonymous') - if not contact: - return None - - user = User.from_contact(contact) - return user - ## # Current user can launch commands ? # If username is provided, check for the specified user ... @@ -1133,7 +1106,7 @@ def get_search_string(self, default=""): search_string = self.request.query.get('search', default) # Force include the UI BI in the search query if it is not yet present - user = self.request.environ['USER'] + user = self.request.environ.get('USER', None) bi = user.min_business_impact or self.problems_business_impact if "bi:" not in search_string: # Include BI as the last search criterion @@ -1175,7 +1148,7 @@ def redirect403(self, msg="Forbidden"): # pylint: disable=no-self-use def get_user(self): - return request.environ['USER'] + return request.environ.get('USER', None) @webui_app.hook('before_request') @@ -1184,40 +1157,40 @@ def login_required(): app = bottle.BaseTemplate.defaults['app'] logger.debug("[WebUI] login_required, requested URL: %s", request.urlparts.path) - if request.urlparts.path == "/user/logout" or request.urlparts.path == app.get_url("Logout"): - return - if request.urlparts.path == "/user/login" or request.urlparts.path == app.get_url("GetLogin"): - return - if request.urlparts.path == "/user/auth" or request.urlparts.path == app.get_url("SetLogin"): - return + # No static route need user authentication... if request.urlparts.path.startswith('/static'): return + # Nor some specific routes + if request.urlparts.path in ['/favicon.ico', '/gotfirstdata', '/user/get_pref', '/user/set_pref', + app.get_url("Logout"), + app.get_url("GetLogin"), + app.get_url("SetLogin")]: + return - logger.debug("[WebUI] login_required, getting user cookie ...") + logger.debug("[WebUI] login_required for %s, getting user cookie ...", request.urlparts.path) cookie_value = bottle.request.get_cookie(str(app.session_cookie), secret=app.auth_secret) - if not cookie_value and not app.allow_anonymous: - bottle.redirect('/user/login') if cookie_value: - if 'session' in cookie_value: - app.user_session = cookie_value['session'] - logger.debug("[WebUI] user session: %s", cookie_value['session']) - if 'info' in cookie_value: - app.user_info = cookie_value['info'] - logger.debug("[WebUI] user info: %s", cookie_value['info']) - if 'login' in cookie_value: - logger.debug("[WebUI] user login: %s", cookie_value['login']) - contact = app.datamgr.get_contact(name=cookie_value['login']) - else: - contact = app.datamgr.get_contact(name=cookie_value) + app.user_session = cookie_value.get('session', '') + logger.debug("[WebUI] user session: %s", app.user_session) + app.user_info = cookie_value.get('info', '') + logger.debug("[WebUI] user info: %s", app.user_info) + contact_name = cookie_value.get('login', cookie_value) + logger.debug("[WebUI] user login: %s", contact_name) else: # Only the /dashboard/currently should be accessible to anonymous users - if request.urlparts.path != "/dashboard/currently": - bottle.redirect("/user/login") - contact = app.datamgr.get_contact(name='anonymous') - + contact_name = 'anonymous' + if not app.allow_anonymous: + logger.info("[WebUI] anonymous access is forbidden. Redirecting to %s", app.get_url("GetLogin")) + bottle.redirect(app.get_url("GetLogin")) + if request.urlparts.path not in [app.get_url("Currently")]: + logger.info("[WebUI] anonymous access is allowed only for the dashboard, not for %s", + request.urlparts.path) + bottle.redirect(app.get_url("GetLogin")) + + contact = app.datamgr.get_contact(name=contact_name) if not contact: - app.redirect403() - bottle.redirect('/user/login') + logger.info("[WebUI] contact does not exist: %s", contact_name) + bottle.redirect(app.get_url("GetLogin")) user = User.from_contact(contact) if app.user_session and app.user_info: diff --git a/module/plugins/dashboard/dashboard.py b/module/plugins/dashboard/dashboard.py index 383ec443..998b9c06 100644 --- a/module/plugins/dashboard/dashboard.py +++ b/module/plugins/dashboard/dashboard.py @@ -32,7 +32,7 @@ # Our page def get_page(): - user = app.request.environ['USER'] + user = app.request.environ.get('USER', None) # Look for the widgets as the json entry s = app.prefs_module.get_ui_user_preference(user, 'widgets') @@ -69,7 +69,7 @@ def get_page(): def get_currently(): - user = app.request.environ['USER'] + user = app.request.environ.get('USER', None) # Search panels preferences s = app.prefs_module.get_ui_user_preference(user, 'panels') diff --git a/module/plugins/dashboard/views/currently.tpl b/module/plugins/dashboard/views/currently.tpl index d9675a4f..1ff9dcff 100644 --- a/module/plugins/dashboard/views/currently.tpl +++ b/module/plugins/dashboard/views/currently.tpl @@ -395,7 +395,7 @@ %setdefault('user', None) %username = 'anonymous' -%if user is not None: +%if user is not None and not user.is_anonymous(): %if hasattr(user, 'alias'): % username = user.alias %else: @@ -446,7 +446,7 @@ %s = app.datamgr.get_services_synthesis(None, user) %h = app.datamgr.get_hosts_synthesis(None, user) -%if username != 'anonymous': +%if not user.is_anonymous():
    @@ -454,36 +454,36 @@ @@ -520,13 +520,13 @@ %for state in 'up', 'unreachable', 'down', 'pending', 'unknown': %end
    @@ -549,13 +549,13 @@ %for state in 'ok', 'warning', 'critical': %end
    @@ -580,7 +580,7 @@
    - %if username != 'anonymous': + %if not user.is_anonymous(): %end - %for state in 'up', 'unreachable', 'down': - - %end %known_problems=h['nb_problems'] %pct_known_problems=round(100.0 * known_problems / h['nb_elts'], 2) if h['nb_elts'] else 0 %known_problems=h['nb_ack']+h['nb_downtime'] %pct_known_problems=round(100.0 * known_problems / h['nb_elts'], 2) if h['nb_elts'] else 0
    @@ -650,7 +646,7 @@
    - %if username != 'anonymous': + %if not user.is_anonymous(): %end - %for state in 'ok', 'warning', 'critical': - - %end %known_problems=s['nb_problems'] %pct_known_problems=round(100.0 * known_problems / s['nb_elts'], 2) if s['nb_elts'] else 0 %known_problems=s['nb_ack']+s['nb_downtime'] %pct_known_problems=round(100.0 * known_problems / s['nb_elts'], 2) if s['nb_elts'] else 0
    @@ -749,18 +741,18 @@
    - - -
    -
    - -
    -
    + + +
    +
    + +
    +
    @@ -807,18 +799,18 @@
    - - - + + +
    @@ -865,18 +857,18 @@
    - - - + + +
    @@ -923,18 +915,18 @@
    - - - + + +
    diff --git a/module/plugins/login/login.py b/module/plugins/login/login.py index 88f14b42..3a833e5d 100644 --- a/module/plugins/login/login.py +++ b/module/plugins/login/login.py @@ -77,20 +77,22 @@ def user_login(): secret=app.auth_secret, path='/') bottle.redirect(app.get_url("Dashboard")) - logger.info("[WebUI] session user message - get: %s", app.request.environ.get('MSG', 'None...')) + logger.debug("[WebUI] session user message - get: %s", + app.request.environ.get('MSG', 'None...')) return {'msg_text': err, 'login_text': app.login_text, 'company_logo': app.company_logo} def user_logout(): - # To delete it, send the same, with different date - user_name = app.request.get_cookie(app.session_cookie, secret=app.auth_secret) - if user_name: - app.response.set_cookie(str(app.session_cookie), False, secret=app.auth_secret, path='/') + # To delete the cookie, send the same, with different date + cookie_value = app.request.get_cookie(app.session_cookie, secret=app.auth_secret) + if cookie_value: + app.response.set_cookie(app.session_cookie, False, secret=app.auth_secret, path='/') else: - app.response.set_cookie(str(app.session_cookie), '', secret=app.auth_secret, path='/') + app.response.set_cookie(app.session_cookie, '', secret=app.auth_secret, path='/') - logger.info("[WebUI] user '%s' signed out", user_name) + contact_name = cookie_value.get('login', cookie_value) + logger.info("[WebUI] user '%s' signed out", contact_name) bottle.redirect(app.get_url("GetLogin")) return {} @@ -120,7 +122,6 @@ def user_auth(): bottle.redirect(app.get_url("Dashboard")) else: logger.debug("[WebUI] user '%s' access denied, redirection to: %s", login, app.get_url("GetLogin")) - # bottle.redirect(app.get_url("GetLogin") + "?error=%s" % app.request.environ.get('MSG', 'No message')) logger.info("[WebUI] refused login message: %s", app.request.environ.get('MSG', '')) bottle.redirect(app.get_url("GetLogin") + "?error=%s" % app.request.environ.get('MSG', '')) @@ -128,9 +129,8 @@ def user_auth(): return {'is_auth': is_authenticated} -# manage the /. If the user is known, go to home page. -# Should be /dashboard in the future. If not, go login :) def get_root(): + """Navigating to the root is redirecting to the Dashboard view""" bottle.redirect(app.get_url("Dashboard")) diff --git a/module/plugins/user/user.py b/module/plugins/user/user.py index dc79b292..0b0bd436 100644 --- a/module/plugins/user/user.py +++ b/module/plugins/user/user.py @@ -38,27 +38,27 @@ def show_pref(): def get_pref(): - user = app.request.environ['USER'] + user = app.request.environ.get('USER', None) key = app.request.query.get('key', None) - if not key: + if not key or not user: return '' return app.prefs_module.get_ui_user_preference(user, key) def get_common_pref(): - user = app.request.environ['USER'] + user = app.request.environ.get('USER', None) key = app.request.query.get('key', None) - if not key: + if not key or not user: return '' return app.prefs_module.get_ui_common_preference(user, key) def save_pref(): - user = app.request.environ['USER'] + user = app.request.environ.get('USER', None) key = app.request.query.get('key', None) value = app.request.query.get('value', None) @@ -72,7 +72,7 @@ def save_pref(): def save_common_pref(): - user = app.request.environ['USER'] + user = app.request.environ.get('USER', None) key = app.request.query.get('key', None) value = app.request.query.get('value', None) diff --git a/module/ui_user.py b/module/ui_user.py index fa656592..88cf896c 100644 --- a/module/ui_user.py +++ b/module/ui_user.py @@ -62,10 +62,20 @@ def get_name(self): name = getattr(self, 'alias', name) return name + def is_anonymous(self): + """ + Is contact an anonymous user? + """ + return self.get_username() == 'anonymous' + def is_administrator(self): """ Is contact an administrator? """ + # Protecting for anonymous access + if self.is_anonymous(): + return False + if isinstance(getattr(self, 'is_admin', '0'), bool): return self.is_admin @@ -75,6 +85,10 @@ def is_commands_allowed(self): """ Is contact allowed to use commands? """ + # Protecting for anonymous access + if self.is_anonymous(): + return False + if self.is_administrator(): return True