From d474dd2ca4bbcf9930ed3d02b3200eb323ddb1e5 Mon Sep 17 00:00:00 2001 From: qgarnier Date: Thu, 30 Dec 2021 14:12:09 +0100 Subject: [PATCH] add(plugin): redis sentinel (#3352) --- apps/redis/sentinel/custom/cli.pm | 251 ++++++++++++++++++ apps/redis/sentinel/mode/listclusters.pm | 96 +++++++ apps/redis/sentinel/mode/redisclusters.pm | 263 +++++++++++++++++++ apps/redis/sentinel/mode/sentinelclusters.pm | 256 ++++++++++++++++++ apps/redis/sentinel/plugin.pm | 50 ++++ database/redis/plugin.pm | 2 + 6 files changed, 918 insertions(+) create mode 100644 apps/redis/sentinel/custom/cli.pm create mode 100644 apps/redis/sentinel/mode/listclusters.pm create mode 100644 apps/redis/sentinel/mode/redisclusters.pm create mode 100644 apps/redis/sentinel/mode/sentinelclusters.pm create mode 100644 apps/redis/sentinel/plugin.pm diff --git a/apps/redis/sentinel/custom/cli.pm b/apps/redis/sentinel/custom/cli.pm new file mode 100644 index 000000000..4ba127d61 --- /dev/null +++ b/apps/redis/sentinel/custom/cli.pm @@ -0,0 +1,251 @@ +# +# Copyright 2021 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# 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. +# + +package apps::redis::sentinel::custom::cli; + +use strict; +use warnings; +use centreon::plugins::ssh; +use centreon::plugins::misc; +use Digest::MD5 qw(md5_hex); + +sub new { + my ($class, %options) = @_; + my $self = {}; + bless $self, $class; + + if (!defined($options{output})) { + print "Class Custom: Need to specify 'output' argument.\n"; + exit 3; + } + if (!defined($options{options})) { + $options{output}->add_option_msg(short_msg => "Class Custom: Need to specify 'options' argument."); + $options{output}->option_exit(); + } + + if (!defined($options{noptions})) { + $options{options}->add_options(arguments => { + 'ssh-hostname:s' => { name => 'ssh_hostname' }, + 'server:s' => { name => 'server' }, + 'port:s' => { name => 'port' }, + 'username:s' => { name => 'username' }, + 'password:s' => { name => 'password' }, + 'tls' => { name => 'tls' }, + 'cacert:s' => { name => 'cacert' }, + 'insecure' => { name => 'insecure' }, + 'timeout:s' => { name => 'timeout' } + }); + } + + $options{options}->add_help(package => __PACKAGE__, sections => 'REDIS SENTINEL OPTIONS', once => 1); + + $self->{output} = $options{output}; + $self->{ssh} = centreon::plugins::ssh->new(%options); + + return $self; +} + +sub set_options { + my ($self, %options) = @_; + + $self->{option_results} = $options{option_results}; +} + +sub set_defaults {} + +sub check_options { + my ($self, %options) = @_; + + $self->{ssh_hostname} = defined($self->{option_results}->{ssh_hostname}) && $self->{option_results}->{ssh_hostname} ne '' ? $self->{option_results}->{ssh_hostname} : ''; + $self->{server} = defined($self->{option_results}->{server}) && $self->{option_results}->{server} ne '' ? $self->{option_results}->{server} : ''; + $self->{port} = defined($self->{option_results}->{port}) && $self->{option_results}->{port} ne '' ? $self->{option_results}->{port} : 26379; + $self->{username} = defined($self->{option_results}->{password}) && $self->{option_results}->{username} ne '' ? $self->{option_results}->{username} : ''; + $self->{password} = defined($self->{option_results}->{password}) && $self->{option_results}->{password} ne '' ? $self->{option_results}->{password} : ''; + $self->{timeout} = defined($self->{option_results}->{timeout}) && $self->{option_results}->{timeout} =~ /(\d+)/ ? $1 : 10; + $self->{tls} = defined($self->{option_results}->{tls}) ? 1 : 0; + $self->{insecure} = defined($self->{option_results}->{insecure}) ? 1 : 0; + $self->{cacert} = defined($self->{option_results}->{cacert}) && $self->{option_results}->{cacert} ne '' ? $self->{option_results}->{cacert} : ''; + + if ($self->{server} eq '') { + $self->{output}->add_option_msg(short_msg => 'Need to specify --server option.'); + $self->{output}->option_exit(); + } + if ($self->{ssh_hostname} ne '') { + $self->{option_results}->{hostname} = $self->{ssh_hostname}; + $self->{ssh}->check_options(option_results => $self->{option_results}); + } + if ($self->{username} ne '' && $self->{option_results}->{password} eq '') { + $self->{output}->add_option_msg(short_msg => 'Need to specify --password option.'); + $self->{output}->option_exit(); + } + + return 0; +} + +sub get_connection_info { + my ($self, %options) = @_; + + my $id = $self->{server} . ':' . $self->{port}; + return md5_hex($id); +} + +sub execute_command { + my ($self, %options) = @_; + + my $command_options = "-h '" . $self->{server} . "' -p " . $self->{port}; + $command_options .= $self->get_extra_options(); + $command_options .= ' --no-raw ' . $options{command}; + + my $timeout = $self->{timeout}; + if (!defined($timeout)) { + $timeout = defined($options{timeout}) ? $options{timeout} : 10; + } + + my ($stdout, $exit_code); + if ($self->{ssh_hostname} ne '') { + ($stdout, $exit_code) = $self->{ssh}->execute( + hostname => $self->{ssh_hostname}, + sudo => $self->{option_results}->{sudo}, + command => 'redis-cli', + command_options => $command_options, + timeout => $timeout, + no_quit => $options{no_quit} + ); + } else { + ($stdout, $exit_code) = centreon::plugins::misc::execute( + output => $self->{output}, + sudo => $self->{option_results}->{sudo}, + options => { timeout => $timeout }, + command => 'redis-cli', + command_options => $command_options, + no_quit => $options{no_quit} + ); + } + + $self->{output}->output_add(long_msg => "command response: $stdout", debug => 1); + + if ($stdout =~ /^NOPERM/m) { + $self->{output}->add_option_msg(short_msg => 'Permissions issue'); + $self->{output}->option_exit(); + } + + return ($stdout, $exit_code); +} + +sub get_extra_options { + my ($self, %options) = @_; + + my $options = ''; + $options .= ' --tls' if ($self->{tls} == 1); + $options .= " --cacert '" . $self->{cacert} . "'" if ($self->{cacert} ne ''); + $options .= ' --insecure' if ($self->{insecure} == 1); + $options .= " --user '" . $self->{username} . "'" if ($self->{username} ne ''); + $options .= " -a '" . $self->{password} . "'" if ($self->{password} ne ''); + return $options; +} + +sub ckquorum { + my ($self, %options) = @_; + + my ($stdout) = $self->execute_command(command => $options{command}); + return $stdout; +} + +sub command { + my ($self, %options) = @_; + + my ($stdout) = $self->execute_command(command => $options{command}); + my $results = []; + while ($stdout =~ /^\d+\)(.*?)(?=\n\d+\)|\Z$)/msg) { + my @lines = split(/\n/, $1); + my $entry = {}; + while (scalar(@lines) > 0) { + my $label = shift(@lines); + my $value = shift(@lines); + if (defined($label) && defined($value) && $label =~ /\d+\) "(.*)"/) { + $label = $1; + $entry->{$label} = $1 if ($value =~ /\d+\) "(.*)"/); + } + } + + push @$results, $entry; + } + return $results; +} + +1; + +__END__ + +=head1 NAME + +redis-cli. + +=head1 SYNOPSIS + +redis-cli. + +=head1 REDIS SENTINEL OPTIONS + +=over 8 + +=item B<--server> + +Sentinel server. + +=item B<--port> + +Sentinel port (Default: 26379). + +=item B<--tls> + +Establish a secure TLS connection (redis-cli >= 6.x mandatory). + +=item B<--cacert> + +CA Certificate file to verify with (redis-cli >= 6.x mandatory). + +=item B<--insecure> + +Allow insecure TLS connection by skipping cert validation (Since redis-cli 6.2.0). + +=item B<--username> + +Sentinel username (redis-cli >= 6.x mandatory). + +=item B<--password> + +Sentinel password. + +=item B<--ssh-hostname> + +Remote ssh redis-cli execution. + +=item B<--timeout> + +Timeout in seconds for the command (Default: 10). + +=back + +=head1 DESCRIPTION + +B. + +=cut diff --git a/apps/redis/sentinel/mode/listclusters.pm b/apps/redis/sentinel/mode/listclusters.pm new file mode 100644 index 000000000..6fa242f0b --- /dev/null +++ b/apps/redis/sentinel/mode/listclusters.pm @@ -0,0 +1,96 @@ +# +# Copyright 2021 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# 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. +# + +package apps::redis::sentinel::mode::listclusters; + +use base qw(centreon::plugins::mode); + +use strict; +use warnings; + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options(arguments => {}); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); +} + +sub manage_selection { + my ($self, %options) = @_; + + return $options{custom}->command(command => 'sentinel masters'); +} + +sub run { + my ($self, %options) = @_; + + my $results = $self->manage_selection(%options); + foreach my $entry (@$results) { + $self->{output}->output_add( + long_msg => '[name: ' . $entry->{name} . ']' + ); + } + + $self->{output}->output_add( + severity => 'OK', + short_msg => 'List clusters:' + ); + $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1, force_long_output => 1); + $self->{output}->exit(); +} + +sub disco_format { + my ($self, %options) = @_; + + $self->{output}->add_disco_format(elements => ['name']); +} + +sub disco_show { + my ($self, %options) = @_; + + my $results = $self->manage_selection(%options); + foreach my $entry (@$results) { + $self->{output}->add_disco_entry( + name => $entry->{name} + ); + } +} + +1; + +__END__ + +=head1 MODE + +List clusters. + +=over 8 + +=back + +=cut diff --git a/apps/redis/sentinel/mode/redisclusters.pm b/apps/redis/sentinel/mode/redisclusters.pm new file mode 100644 index 000000000..bc67ca0a1 --- /dev/null +++ b/apps/redis/sentinel/mode/redisclusters.pm @@ -0,0 +1,263 @@ +# +# Copyright 2021 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# 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. +# + +package apps::redis::sentinel::mode::redisclusters; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub custom_status_output { + my ($self, %options) = @_; + + return sprintf( + "status: %s [role: %s]", + $self->{result_values}->{status}, + $self->{result_values}->{role} + ); +} + +sub prefix_cluster_output { + my ($self, %options) = @_; + + return "cluster '" . $options{instance} . "' "; +} + +sub cluster_long_output { + my ($self, %options) = @_; + + return "checking cluster '" . $options{instance} . "'"; +} + +sub prefix_instance_output { + my ($self, %options) = @_; + + return "instance '" . $options{instance} . "' "; +} + +sub prefix_global_output { + my ($self, %options) = @_; + + return 'number of '; +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'clusters', type => 3, cb_prefix_output => 'prefix_cluster_output', cb_long_output => 'cluster_long_output', indent_long_output => ' ', message_multiple => 'All clusters are ok', + group => [ + { name => 'global', type => 0, cb_prefix_output => 'prefix_global_output', skipped_code => { -10 => 1 } }, + { name => 'stddev', type => 0, skipped_code => { -10 => 1 } }, + { name => 'instances', type => 1, display_long => 1, cb_prefix_output => 'prefix_instance_output', message_multiple => 'All redis instances are ok', skipped_code => { -10 => 1 } } + ] + } + ]; + + $self->{maps_counters}->{global} = [ + { label => 'slaves-detected', nlabel => 'cluster.redis.slaves.detected.count', set => { + key_values => [ { name => 'num_slaves' } ], + output_template => 'detected slaves: %s', + perfdatas => [ + { template => '%s', min => 0, label_extra_instance => 1 } + ] + } + }, + { label => 'redis-sdown', nlabel => 'cluster.redis.subjectively_down.count', set => { + key_values => [ { name => 'sdown' } ], + output_template => 'subjectively down instances: %s', + perfdatas => [ + { template => '%s', min => 0, label_extra_instance => 1 } + ] + } + }, + { label => 'redis-odown', nlabel => 'cluster.redis.objectively_down.count', set => { + key_values => [ { name => 'odown' } ], + output_template => 'objectively down instances: %s', + perfdatas => [ + { template => '%s', min => 0, label_extra_instance => 1 } + ] + } + } + ]; + + $self->{maps_counters}->{stddev} = [ + { label => 'slave-repl-offset-stddev', nlabel => 'cluster.redis.slave_replication_offset.stddev.count', set => { + key_values => [ { name => 'stddev_repl_offset' } ], + output_template => 'slave replication offset standard deviation: %.2f', + perfdatas => [ + { template => '%.2f' } + ] + } + } + ]; + + $self->{maps_counters}->{instances} = [ + { + label => 'status', + type => 2, + critical_default => '%{status} =~ /o_down|s_down|master_down|disconnected/i', + set => { + key_values => [ + { name => 'status' }, { name => 'role' }, + { name => 'address' }, { name => 'port' }, + { name => 'cluster_name' } + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => \&catalog_status_threshold_ng + } + }, + { label => 'redis-ping-ok-latency', nlabel => 'cluster.redis.ping_ok.latency.milliseconds', set => { + key_values => [ { name => 'ping_ok_latency' } ], + output_template => 'last ok ping: %s ms', + perfdatas => [ + { template => '%d', min => 0, unit => 's', label_extra_instance => 1 } + ] + } + } + ]; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options(arguments => { + 'filter-cluster-name:s' => { name => 'filter_cluster_name' } + }); + + return $self; +} + +sub add_instance { + my ($self, %options) = @_; + + my $key = $options{entry}->{ip} . ':' . $options{entry}->{port}; + $self->{clusters}->{ $options{cluster_name} }->{instances}->{$key} = { + cluster_name => $options{cluster_name}, + address => $options{entry}->{ip}, + port => $options{entry}->{port}, + role => $options{entry}->{'role-reported'}, + status => $options{entry}->{flags}, + ping_ok_latency => $options{entry}->{'last-ok-ping-reply'}, + slave_repl_offset => $options{entry}->{'slave-repl-offset'} + }; + $self->{clusters}->{ $options{cluster_name} }->{global}->{sdown}++ + if ($options{entry}->{flags} =~ /s_down/); + $self->{clusters}->{ $options{cluster_name} }->{global}->{odown}++ + if ($options{entry}->{flags} =~ /o_down/); +} + +sub stddev { + my ($self, %options) = @_; + + my $total = 0; + my $num = 0; + foreach my $entry (values %{$self->{clusters}->{ $options{cluster_name} }->{instances}}) { + next if (!defined($entry->{slave_repl_offset})); + $total += $entry->{slave_repl_offset}; + $num++; + } + + return if ($num <= 1); + + my $mean = $total / $num; + $total = 0; + foreach my $entry (values %{$self->{clusters}->{ $options{cluster_name} }->{instances}}) { + next if (!defined($entry->{slave_repl_offset})); + $total += ($mean - $entry->{slave_repl_offset}) ** 2; + } + $self->{clusters}->{ $options{cluster_name} }->{stddev} = { stddev_repl_offset => sqrt($total / $num) }; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $results = $options{custom}->command(command => 'sentinel masters'); + + $self->{clusters} = {}; + foreach my $entry (@$results) { + next if (defined($self->{option_results}->{filter_cluster_name}) && $self->{option_results}->{filter_cluster_name} ne '' + && $entry->{name} !~ /$self->{option_results}->{filter_cluster_name}/); + + $self->{clusters}->{ $entry->{name} } = { + global => { num_slaves => $entry->{'num-slaves'}, odown => 0, sdown => 0 }, + instances => {} + }; + $self->add_instance(cluster_name => $entry->{name}, entry => $entry); + + my $replicas = $options{custom}->command(command => 'sentinel replicas ' . $entry->{name}); + foreach (@$replicas) { + $self->add_instance(cluster_name => $entry->{name}, entry => $_); + } + + $self->stddev(cluster_name => $entry->{name}); + } + + if (scalar(keys %{$self->{clusters}}) <= 0) { + $self->{output}->add_option_msg(short_msg => "No redis cluster found."); + $self->{output}->option_exit(); + } + + +} + +1; + +__END__ + +=head1 MODE + +Check redis clusters informations. + +=over 8 + +=item B<--filter-cluster-name> + +Filter clusters by name (Can be a regexp). + +=item B<--unknown-status> + +Set unknown threshold for status. +Can used special variables like: %{status}, %{role}, %{address}, %{port}, %{cluster_name} + +=item B<--warning-status> + +Set warning threshold for status. +Can used special variables like: %{status}, %{role}, %{address}, %{port}, %{cluster_name} + +=item B<--critical-status> + +Set critical threshold for status (Default: '%{status} =~ /o_down|s_down|master_down|disconnected/i'). +Can used special variables like: %{status}, %{role}, %{address}, %{port}, %{cluster_name} + +=item B<--warning-*> B<--critical-*> + +Thresholds. +Can be: 'redis-ping-ok-latency', 'redis-sdown', 'redis-odown', +'slave-repl-offset-stddev', 'slaves-detected'. + +=back + +=cut diff --git a/apps/redis/sentinel/mode/sentinelclusters.pm b/apps/redis/sentinel/mode/sentinelclusters.pm new file mode 100644 index 000000000..3ba1b51c1 --- /dev/null +++ b/apps/redis/sentinel/mode/sentinelclusters.pm @@ -0,0 +1,256 @@ +# +# Copyright 2021 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# 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. +# + +package apps::redis::sentinel::mode::sentinelclusters; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub custom_status_output { + my ($self, %options) = @_; + + return sprintf( + "status: %s", + $self->{result_values}->{status} + ); +} + +sub prefix_cluster_output { + my ($self, %options) = @_; + + return "cluster '" . $options{instance} . "' "; +} + +sub cluster_long_output { + my ($self, %options) = @_; + + return "checking cluster '" . $options{instance} . "'"; +} + +sub prefix_instance_output { + my ($self, %options) = @_; + + return "instance '" . $options{instance} . "' "; +} + +sub prefix_global_output { + my ($self, %options) = @_; + + return 'number of sentinels '; +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'clusters', type => 3, cb_prefix_output => 'prefix_cluster_output', cb_long_output => 'cluster_long_output', indent_long_output => ' ', message_multiple => 'All clusters are ok', + group => [ + { name => 'global', type => 0, cb_prefix_output => 'prefix_global_output', skipped_code => { -10 => 1 } }, + { name => 'quorum', type => 0, skipped_code => { -10 => 1 } }, + { name => 'instances', type => 1, display_long => 1, cb_prefix_output => 'prefix_instance_output', message_multiple => 'All sentinel instances are ok', skipped_code => { -10 => 1 } } + ] + } + ]; + + $self->{maps_counters}->{global} = [ + { label => 'sentinels-detected', nlabel => 'cluster.sentinels.detected.count', set => { + key_values => [ { name => 'num_sentinels' } ], + output_template => 'detected: %s', + perfdatas => [ + { template => '%s', min => 0, label_extra_instance => 1 } + ] + } + }, + { label => 'sentinels-sdown', nlabel => 'cluster.sentinels.subjectively_down.count', set => { + key_values => [ { name => 'sdown' } ], + output_template => 'subjectively down: %s', + perfdatas => [ + { template => '%s', min => 0, label_extra_instance => 1 } + ] + } + }, + { label => 'sentinels-odown', nlabel => 'cluster.sentinels.objectively_down.count', set => { + key_values => [ { name => 'odown' } ], + output_template => 'objectively down: %s', + perfdatas => [ + { template => '%s', min => 0, label_extra_instance => 1 } + ] + } + } + ]; + + $self->{maps_counters}->{quorum} = [ + { + label => 'quorum-status', + type => 2, + critical_default => '%{status} =~ /noQuorum/', + set => { + key_values => [ { name => 'status' }, { name => 'cluster_name' } ], + output_template => 'quorum status: %s', + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => \&catalog_status_threshold_ng + } + } + ]; + + $self->{maps_counters}->{instances} = [ + { + label => 'status', + type => 2, + critical_default => '%{status} =~ /o_down|s_down|master_down|disconnected/i', + set => { + key_values => [ + { name => 'status' }, { name => 'address' }, { name => 'port' }, { name => 'cluster_name' } + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => \&catalog_status_threshold_ng + } + }, + { label => 'sentinel-ping-ok-latency', nlabel => 'cluster.sentinel.ping_ok.latency.milliseconds', set => { + key_values => [ { name => 'ping_ok_latency' } ], + output_template => 'last ok ping: %s ms', + perfdatas => [ + { template => '%d', min => 0, unit => 's', label_extra_instance => 1 } + ] + } + } + ]; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options(arguments => { + 'filter-cluster-name:s' => { name => 'filter_cluster_name' } + }); + + return $self; +} + +sub add_instance { + my ($self, %options) = @_; + + my $key = $options{entry}->{ip} . ':' . $options{entry}->{port}; + $self->{clusters}->{ $options{cluster_name} }->{instances}->{$key} = { + cluster_name => $options{cluster_name}, + address => $options{entry}->{ip}, + port => $options{entry}->{port}, + status => $options{entry}->{flags}, + ping_ok_latency => $options{entry}->{'last-ok-ping-reply'} + }; + $self->{clusters}->{ $options{cluster_name} }->{global}->{sdown}++ + if ($options{entry}->{flags} =~ /s_down/); + $self->{clusters}->{ $options{cluster_name} }->{global}->{odown}++ + if ($options{entry}->{flags} =~ /o_down/); +} + +sub manage_selection { + my ($self, %options) = @_; + + my $results = $options{custom}->command(command => 'sentinel masters'); + + $self->{clusters} = {}; + foreach my $entry (@$results) { + next if (defined($self->{option_results}->{filter_cluster_name}) && $self->{option_results}->{filter_cluster_name} ne '' + && $entry->{name} !~ /$self->{option_results}->{filter_cluster_name}/); + + $self->{clusters}->{ $entry->{name} } = { + global => { num_sentinels => $entry->{'num-other-sentinels'}, odown => 0, sdown => 0 }, + quorum => { status => 'unknown', cluster_name => $entry->{name} }, + instances => {} + }; + + my $quorum = $options{custom}->ckquorum(command => 'sentinel ckquorum ' . $entry->{name}); + if ($quorum =~ /OK \d+ usable Sentinels/m) { + $self->{clusters}->{ $entry->{name} }->{quorum}->{status} = 'ok'; + } elsif ($quorum =~ /NOQUORUM \d+ usable Sentinels/m) { + $self->{clusters}->{ $entry->{name} }->{quorum}->{status} = 'noQuorum'; + } + + my $sentinels = $options{custom}->command(command => 'sentinel sentinels ' . $entry->{name}); + foreach (@$sentinels) { + $self->add_instance(cluster_name => $entry->{name}, entry => $_); + } + } + + if (scalar(keys %{$self->{clusters}}) <= 0) { + $self->{output}->add_option_msg(short_msg => "No sentinel cluster found."); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Check sentinel clusters informations. + +=over 8 + +=item B<--filter-cluster-name> + +Filter clusters by name (Can be a regexp). + +=item B<--unknown-status> + +Set unknown threshold for status. +Can used special variables like: %{status}, %{address}, %{port}, %{cluster_name} + +=item B<--warning-status> + +Set warning threshold for status. +Can used special variables like: %{status}, %{address}, %{port}, %{cluster_name} + +=item B<--critical-status> + +Set critical threshold for status (Default: '%{status} =~ /o_down|s_down|master_down|disconnected/i'). +Can used special variables like: %{status}, %{address}, %{port}, %{cluster_name} + +=item B<--unknown-status> + +Set unknown threshold for status. +Can used special variables like: %{status}, %{address}, %{port}, %{cluster_name} + +=item B<--warning-quorum-status> + +Set warning threshold for quorum status. +Can used special variables like: %{status}, %{address}, %{port}, %{cluster_name} + +=item B<--critical-quorum-status> + +Set critical threshold for quorum status (Default: '%{status} =~ /noQuorum/'). +Can used special variables like: %{status}, %{cluster_name} + +=item B<--warning-*> B<--critical-*> + +Thresholds. +Can be: 'sentinel-ping-ok-latency', 'sentinels-sdown', 'sentinels-odown', 'sentinels-detected'. + +=back + +=cut diff --git a/apps/redis/sentinel/plugin.pm b/apps/redis/sentinel/plugin.pm new file mode 100644 index 000000000..169208a25 --- /dev/null +++ b/apps/redis/sentinel/plugin.pm @@ -0,0 +1,50 @@ +# +# Copyright 2021 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# 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. +# + +package apps::redis::sentinel::plugin; + +use strict; +use warnings; +use base qw(centreon::plugins::script_custom); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $self->{modes} = { + 'redis-clusters' => 'apps::redis::sentinel::mode::redisclusters', + 'list-clusters' => 'apps::redis::sentinel::mode::listclusters', + 'sentinel-clusters' => 'apps::redis::sentinel::mode::sentinelclusters' + }; + + $self->{custom_modes}->{cli} = 'apps::redis::sentinel::custom::cli'; + return $self; +} + +1; + +__END__ + +=head1 PLUGIN DESCRIPTION + +Check Redis sentinel. + +=cut diff --git a/database/redis/plugin.pm b/database/redis/plugin.pm index fbff996c5..30adb07b0 100644 --- a/database/redis/plugin.pm +++ b/database/redis/plugin.pm @@ -52,3 +52,5 @@ __END__ =head1 PLUGIN DESCRIPTION Check Redis database. + +=cut