From 62eb5088bedc30a7cd28726af56108c5878094e2 Mon Sep 17 00:00:00 2001 From: garnier-quentin Date: Fri, 5 Jun 2020 11:14:56 +0200 Subject: [PATCH] add services status docker --- cloud/docker/restapi/custom/api.pm | 217 ++++++++++++++++----- cloud/docker/restapi/mode/nodestatus.pm | 51 ++--- cloud/docker/restapi/mode/servicestatus.pm | 177 +++++++++++++++++ cloud/docker/restapi/plugin.pm | 7 +- 4 files changed, 363 insertions(+), 89 deletions(-) create mode 100644 cloud/docker/restapi/mode/servicestatus.pm diff --git a/cloud/docker/restapi/custom/api.pm b/cloud/docker/restapi/custom/api.pm index 157ae0738..3262dbffe 100644 --- a/cloud/docker/restapi/custom/api.pm +++ b/cloud/docker/restapi/custom/api.pm @@ -43,23 +43,23 @@ sub new { if (!defined($options{noptions})) { $options{options}->add_options(arguments => { - "hostname:s@" => { name => 'hostname' }, - "port:s" => { name => 'port', default => 8080 }, - "proto:s" => { name => 'proto' }, - "credentials" => { name => 'credentials' }, - "basic" => { name => 'basic' }, - "username:s" => { name => 'username' }, - "password:s" => { name => 'password' }, - "timeout:s" => { name => 'timeout', default => 10 }, - "cert-file:s" => { name => 'cert_file' }, - "key-file:s" => { name => 'key_file' }, - "cacert-file:s" => { name => 'cacert_file' }, - "cert-pwd:s" => { name => 'cert_pwd' }, - "cert-pkcs12" => { name => 'cert_pkcs12' }, - "api-display" => { name => 'api_display' }, - "api-write-file:s" => { name => 'api_write_file' }, - "api-read-file:s" => { name => 'api_read_file' }, - "reload-cache-time:s" => { name => 'reload_cache_time', default => 300 }, + 'hostname:s@' => { name => 'hostname' }, + 'port:s' => { name => 'port', default => 8080 }, + 'proto:s' => { name => 'proto' }, + 'credentials' => { name => 'credentials' }, + 'basic' => { name => 'basic' }, + 'username:s' => { name => 'username' }, + 'password:s' => { name => 'password' }, + 'timeout:s' => { name => 'timeout', default => 10 }, + 'cert-file:s' => { name => 'cert_file' }, + 'key-file:s' => { name => 'key_file' }, + 'cacert-file:s' => { name => 'cacert_file' }, + 'cert-pwd:s' => { name => 'cert_pwd' }, + 'cert-pkcs12' => { name => 'cert_pkcs12' }, + 'api-display' => { name => 'api_display' }, + 'api-write-file:s' => { name => 'api_write_file' }, + 'api-read-file:s' => { name => 'api_read_file' }, + 'reload-cache-time:s' => { name => 'reload_cache_time', default => 300 }, }); } $options{options}->add_help(package => __PACKAGE__, sections => 'REST API OPTIONS', once => 1); @@ -108,14 +108,14 @@ sub check_options { $self->{output}->add_option_msg(short_msg => "Need to specify hostname option."); $self->{output}->option_exit(); } - + $self->{node_names} = []; foreach my $node_name (@{$self->{hostname}}) { if ($node_name ne '') { push @{$self->{node_names}}, $node_name; } } - + $self->{http}->set_options(%{$self->{option_results}}); return 0; @@ -123,33 +123,38 @@ sub check_options { sub api_display { my ($self, %options) = @_; - + if (defined($self->{option_results}->{api_display})) { if (!defined($self->{option_results}->{api_write_file}) || $self->{option_results}->{api_write_file} eq '') { - $self->{output}->output_add(severity => 'OK', - short_msg => $options{content}); + $self->{output}->output_add( + severity => 'OK', + short_msg => $options{content} + ); $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1, force_long_output => 1); $self->{output}->exit(); } if (!open (FH, '>', $self->{option_results}->{api_write_file})) { - $self->output_add(severity => 'UNKNOWN', - short_msg => "cannot open file '" . $self->{option_results}->{api_write_file} . "': $!"); - + $self->output_add( + severity => 'UNKNOWN', + short_msg => "cannot open file '" . $self->{option_results}->{api_write_file} . "': $!" + ); } - + FH->autoflush(1); print FH $options{content}; close FH; - $self->output_add(severity => 'OK', - short_msg => "Data written in file '" . $self->{option_results}->{api_write_file} . "': $!"); + $self->output_add( + severity => 'OK', + short_msg => "Data written in file '" . $self->{option_results}->{api_write_file} . "': $!" + ); $self->{output}->exit(); } } sub api_read_file { my ($self, %options) = @_; - + my $file_content = do { local $/ = undef; if (!open my $fh, "<", $self->{option_results}->{api_read_file}) { @@ -158,7 +163,7 @@ sub api_read_file { } <$fh>; }; - + my $content; eval { $content = JSON::XS->new->utf8->decode($file_content); @@ -173,16 +178,28 @@ sub api_read_file { sub get_hostnames { my ($self, %options) = @_; - + return $self->{hostname}; } sub get_port { my ($self, %options) = @_; - + return $self->{option_results}->{port}; } +sub internal_get_by_id{ + my ($self, %options) = @_; + + foreach my $obj (@{$options{list}}) { + if ($obj->{ID} eq $options{Id}) { + return $obj; + } + } + + return undef; +} + sub cache_containers { my ($self, %options) = @_; @@ -192,7 +209,7 @@ sub cache_containers { if ($has_cache_file == 0 || !defined($timestamp_cache) || ((time() - $timestamp_cache) > (($options{reload_cache_time})))) { $containers = {}; my $datas = { last_timestamp => time(), containers => $containers }; - + foreach my $node_name (@{$self->{node_names}}) { my $list_containers = $self->internal_api_list_containers(node_name => $node_name); foreach my $container (@$list_containers) { @@ -215,15 +232,20 @@ sub internal_api_list_nodes { my $response = $self->{http}->request( hostname => $options{node_name}, url_path => '/nodes', - unknown_status => '', critical_status => '', warning_status => ''); + unknown_status => '', + critical_status => '', + warning_status => '' + ); my $nodes; eval { $nodes = JSON::XS->new->utf8->decode($response); }; if ($@) { $nodes = []; - $self->{output}->output_add(severity => 'UNKNOWN', - short_msg => "Node '$options{node_name}': cannot decode json list nodes response: $@"); + $self->{output}->output_add( + severity => 'UNKNOWN', + short_msg => "Node '$options{node_name}': cannot decode json list nodes response: $@" + ); } else { $nodes = [] if (ref($nodes) eq 'HASH'); # nodes is not in a swarm } @@ -237,17 +259,22 @@ sub internal_api_info { my $response = $self->{http}->request( hostname => $options{node_name}, url_path => '/info', - unknown_status => '', critical_status => '', warning_status => ''); + unknown_status => '', + critical_status => '', + warning_status => '' + ); my $nodes; eval { $nodes = JSON::XS->new->utf8->decode($response); }; if ($@) { $nodes = []; - $self->{output}->output_add(severity => 'UNKNOWN', - short_msg => "Node '$options{node_name}': cannot decode json info response: $@"); + $self->{output}->output_add( + severity => 'UNKNOWN', + short_msg => "Node '$options{node_name}': cannot decode json info response: $@" + ); } - + return $nodes; } @@ -257,15 +284,20 @@ sub internal_api_list_containers { my $response = $self->{http}->request( hostname => $options{node_name}, url_path => '/containers/json?all=true', - unknown_status => '', critical_status => '', warning_status => ''); + unknown_status => '', + critical_status => '', + warning_status => '' + ); my $containers; eval { $containers = JSON::XS->new->utf8->decode($response); }; if ($@) { $containers = []; - $self->{output}->output_add(severity => 'UNKNOWN', - short_msg => "Node '$options{node_name}': cannot decode json get containers response: $@"); + $self->{output}->output_add( + severity => 'UNKNOWN', + short_msg => "Node '$options{node_name}': cannot decode json get containers response: $@" + ); } return $containers; @@ -273,27 +305,104 @@ sub internal_api_list_containers { sub internal_api_get_container_stats { my ($self, %options) = @_; - + my $response = $self->{http}->request( hostname => $options{node_name}, url_path => '/containers/' . $options{container_id} . '/stats?stream=false', - unknown_status => '', critical_status => '', warning_status => ''); + unknown_status => '', + critical_status => '', + warning_status => '' + ); my $container_stats; eval { $container_stats = JSON::XS->new->utf8->decode($response); }; if ($@) { $container_stats = {}; - $self->output_add(severity => 'UNKNOWN', - short_msg => "Node '$options{node_name}': cannot decode json get container stats response: $@"); + $self->output_add( + severity => 'UNKNOWN', + short_msg => "Node '$options{node_name}': cannot decode json get container stats response: $@" + ); } - + return $container_stats; } +sub internal_api_list_services { + my ($self, %options) = @_; + + my $response = $self->{http}->request( + hostname => $options{node_name}, + url_path => '/services', + unknown_status => '', critical_status => '', warning_status => ''); + my $services; + eval { + $services = JSON::XS->new->utf8->decode($response); + }; + if ($@) { + $services = []; + $self->{output}->output_add( + severity => 'UNKNOWN', + short_msg => "Service '$options{node_name}': cannot decode json list services response: $@" + ); + } + + return $services; +} + +sub internal_api_list_tasks { + my ($self, %options) = @_; + + my $response = $self->{http}->request( + hostname => $options{node_name}, + url_path => '/tasks', + unknown_status => '', + critical_status => '', + warning_status => '' + ); + my $tasks; + eval { + $tasks = JSON::XS->new->utf8->decode($response); + }; + if ($@) { + $tasks = []; + $self->{output}->output_add( + severity => 'UNKNOWN', + short_msg => "Task '$options{node_name}': cannot decode json list services response: $@" + ); + } + + return $tasks; +} + +sub api_list_services { + my ($self, %options) = @_; + + my $services = {}; + foreach my $node_name (@{$self->{node_names}}) { + my $list_tasks = $self->internal_api_list_tasks(node_name => $node_name); + my $list_services = $self->internal_api_list_services(node_name => $node_name); + foreach my $task (@$list_tasks) { + $services->{ $task->{ServiceID} } = {} if (!defined($services->{ $task->{ServiceID} })); + my $service = $self->internal_get_by_id(list => $list_services, Id => $task->{ServiceID}); + $services->{ $task->{ServiceID} }->{ $task->{ID} } = { + node_id => $task->{NodeID}, + node_name => $node_name, + service_name => $service->{Spec}->{Name}, + container_id => $task->{Status}->{ContainerStatus}->{ContainerID}, + desired_state => defined($task->{DesiredState}) && $task->{DesiredState} ne '' ? $task->{DesiredState} : '-', + state => defined($task->{Status}->{State}) && $task->{Status}->{State} ne '' ? $task->{Status}->{State} : '-', + state_message => defined($task->{Status}->{Message}) && $task->{Status}->{Message} ne '' ? $task->{Status}->{Message} : '-' + }; + } + } + + return $services; +} + sub api_list_containers { my ($self, %options) = @_; - + my $containers = {}; foreach my $node_name (@{$self->{node_names}}) { my $list_containers = $self->internal_api_list_containers(node_name => $node_name); @@ -305,13 +414,13 @@ sub api_list_containers { }; } } - + return $containers; } sub api_list_nodes { my ($self, %options) = @_; - + my $nodes = {}; foreach my $node_name (@{$self->{node_names}}) { my $info_node = $self->internal_api_info(node_name => $node_name); @@ -325,7 +434,7 @@ sub api_list_nodes { push @{$nodes->{$node_name}->{nodes}}, { Status => $node->{Status}->{State}, ManagerStatus => $node->{ManagerStatus}->{Reachability}, Addr => $node->{Status}->{Addr} }; } } - + return $nodes; } @@ -350,7 +459,7 @@ sub api_get_containers { last; } } - + if (defined($container_id)) { $content_total->{$container_id}->{Stats} = $self->internal_api_get_container_stats(node_name => $content_total->{$container_id}->{NodeName}, container_id => $container_id); } @@ -359,7 +468,7 @@ sub api_get_containers { $content_total->{$container_id}->{Stats} = $self->internal_api_get_container_stats(node_name => $content_total->{$container_id}->{NodeName}, container_id => $container_id); } } - + $self->api_display(); return $content_total; } diff --git a/cloud/docker/restapi/mode/nodestatus.pm b/cloud/docker/restapi/mode/nodestatus.pm index 0320818da..5ceb395ef 100644 --- a/cloud/docker/restapi/mode/nodestatus.pm +++ b/cloud/docker/restapi/mode/nodestatus.pm @@ -28,19 +28,8 @@ use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold) sub custom_status_output { my ($self, %options) = @_; - my $msg = 'status : ' . $self->{result_values}->{status} . ' [manager status: ' . $self->{result_values}->{manager_status} . ']'; - return $msg; -} - -sub custom_status_calc { - my ($self, %options) = @_; - - $self->{result_values}->{status} = $options{new_datas}->{$self->{instance} . '_status'}; - $self->{result_values}->{manager_status} = $options{new_datas}->{$self->{instance} . '_manager_status'}; - $self->{result_values}->{display} = $options{new_datas}->{$self->{instance} . '_display'}; - - return 0; + return 'status : ' . $self->{result_values}->{status} . ' [manager status: ' . $self->{result_values}->{manager_status} . ']'; } sub set_counters { @@ -54,41 +43,40 @@ sub set_counters { $self->{maps_counters}->{nodes} = [ { label => 'node-status', threshold => 0, set => { key_values => [ { name => 'status' }, { name => 'manager_status' }, { name => 'display' } ], - closure_custom_calc => $self->can('custom_status_calc'), closure_custom_output => $self->can('custom_status_output'), closure_custom_perfdata => sub { return 0; }, - closure_custom_threshold_check => \&catalog_status_threshold, + closure_custom_threshold_check => \&catalog_status_threshold } - }, + } ]; $self->{maps_counters}->{node} = [ { label => 'containers-running', set => { key_values => [ { name => 'containers_running' }, { name => 'display' } ], output_template => 'Containers Running : %s', perfdatas => [ - { label => 'containers_running', value => 'containers_running', template => '%s', - min => 0, label_extra_instance => 1, instance_use => 'display' }, - ], + { label => 'containers_running', template => '%s', + min => 0, label_extra_instance => 1, instance_use => 'display' } + ] } }, { label => 'containers-stopped', set => { key_values => [ { name => 'containers_stopped' }, { name => 'display' } ], output_template => 'Containers Stopped : %s', perfdatas => [ - { label => 'containers_stopped', value => 'containers_stopped', template => '%s', - min => 0, label_extra_instance => 1, instance_use => 'display' }, - ], + { label => 'containers_stopped', template => '%s', + min => 0, label_extra_instance => 1, instance_use => 'display' } + ] } }, { label => 'containers-running', set => { key_values => [ { name => 'containers_paused' }, { name => 'display' } ], output_template => 'Containers Paused : %s', perfdatas => [ - { label => 'containers_paused', value => 'containers_paused', template => '%s', - min => 0, label_extra_instance => 1, instance_use => 'display' }, - ], + { label => 'containers_paused', template => '%s', + min => 0, label_extra_instance => 1, instance_use => 'display' } + ] } - }, + } ]; } @@ -96,13 +84,12 @@ sub new { my ($class, %options) = @_; my $self = $class->SUPER::new(package => __PACKAGE__, %options); bless $self, $class; - - $options{options}->add_options(arguments => - { - "warning-node-status:s" => { name => 'warning_node_status', default => '' }, - "critical-node-status:s" => { name => 'critical_node_status', default => '%{status} !~ /ready/ || %{manager_status} !~ /reachable|-/' }, - }); - + + $options{options}->add_options(arguments => { + 'warning-node-status:s' => { name => 'warning_node_status', default => '' }, + 'critical-node-status:s' => { name => 'critical_node_status', default => '%{status} !~ /ready/ || %{manager_status} !~ /reachable|-/' } + }); + return $self; } diff --git a/cloud/docker/restapi/mode/servicestatus.pm b/cloud/docker/restapi/mode/servicestatus.pm new file mode 100644 index 000000000..c3c87383d --- /dev/null +++ b/cloud/docker/restapi/mode/servicestatus.pm @@ -0,0 +1,177 @@ +# +# Copyright 2020 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 cloud::docker::restapi::mode::servicestatus; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold); + +sub custom_status_output { + my ($self, %options) = @_; + + return sprintf( + 'state: %s [node: %s (%s)] [container: %s] [desired state: %s] [message: %s]', + $self->{result_values}->{state}, + $self->{result_values}->{node_name}, + $self->{result_values}->{node_id}, + $self->{result_values}->{container_id}, + $self->{result_values}->{desired_state}, + $self->{result_values}->{state_message} + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'global', type => 0 }, + { + name => 'services', type => 2, message_multiple => 'All services running well', + format_output => '%s services not in desired stated', + display_counter_problem => { nlabel => 'alerts.problems.current.count', min => 0 }, + group => [ { name => 'service', cb_prefix_output => 'prefix_service_output', skipped_code => { -11 => 1 } } ] + } + ]; + + $self->{maps_counters}->{global} = [ + { label => 'tasks-total', nlabel => 'services.tasks.total.count', set => { + key_values => [ { name => 'total' } ], + output_template => 'total tasks of services: %s', + perfdatas => [ + { template => '%s', min => 0 } + ] + } + } + ]; + + $self->{maps_counters}->{service} = [ + { label => 'status', threshold => 0, set => { + key_values => [ + { name => 'service_name' }, { name => 'task_id' }, + { name => 'node_name' }, { name => 'node_id' }, + { name => 'desired_state' }, { name => 'state_message' }, + { name => 'service_id' }, { name => 'container_id' } + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => \&catalog_status_threshold, + } + }, + ]; +} + +sub prefix_service_output { + my ($self, %options) = @_; + + return "service '" . $options{instance_value}->{service_name} . "' task '" . $options{instance_value}->{task_id} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $self->{version} = '1.0'; + $options{options}->add_options(arguments => { + 'filter-service-name:s' => { name => 'filter_service_nname' }, + 'unknown-status:s' => { name => 'unknown_status', default => '' }, + 'warning-status:s' => { name => 'warning_status', default => '' }, + 'critical-status:s' => { name => 'critical_status', default => '%{desired_state} ne %{state} and %{state} !~ /complete|preparing|assigned/' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); + + $self->change_macros(macros => [ + 'warning_status', 'critical_status', 'unknown_status', + ]); +} + +sub manage_selection { + my ($self, %options) = @_; + + my $results = $options{custom}->api_list_services(); + + $self->{global} = { total => 0 }; + $self->{services}->{global} = { service => {} }; + + foreach my $service_id (keys %$results) { + foreach my $task_id (keys %{$results->{$service_id}}) { + if (defined($self->{option_results}->{filter_service_name}) && $self->{option_results}->{filter_service_name} ne '' && + $_->{Name} !~ /$self->{option_results}->{filter_service_name}/) { + $self->{output}->output_add(long_msg => "skipping service '" . $_->{service_name} . "': no matching filter type.", debug => 1); + next; + } + + $self->{services}->{global}->{service}->{ $task_id } = { + service_id => $service_id, + task_id => $task_id, + %{$results->{$service_id}->{$task_id}} + }; + + $self->{global}->{total}++; + } + } +} + +1; + +__END__ + +=head1 MODE + +Check service status. + +=over 8 + +=item B<--filter-service-name> + +Filter service by service name (can be a regexp). + +=item B<--unknown-status> + +Set unknown threshold for status. +Can used special variables like: %{service_id}, %{task_id}, %{service_name}, %{node_name}, %{node_id}, %{desired_state}, %{state_message}, %{container_id}. + +=item B<--warning-status> + +Set warning threshold for status. +Can used special variables like: %{service_id}, %{task_id}, %{service_name}, %{node_name}, %{node_id}, %{desired_state}, %{state_message}, %{container_id}. + +=item B<--critical-status> + +Set critical threshold for status (Default: '%{desired_state} ne %{state} and %{state} !~ /complete|preparing|assigned/'). +Can used special variables like: %{service_id}, %{task_id}, %{service_name}, %{node_name}, %{node_id}, %{desired_state}, %{state_message}, %{container_id}. + +=item B<--warning-*> B<--critical-*> + +Thresholds. +Can be: 'tasks-total'. + +=back + +=cut diff --git a/cloud/docker/restapi/plugin.pm b/cloud/docker/restapi/plugin.pm index d8cc44b31..1e3255238 100644 --- a/cloud/docker/restapi/plugin.pm +++ b/cloud/docker/restapi/plugin.pm @@ -30,13 +30,14 @@ sub new { bless $self, $class; $self->{version} = '0.3'; - %{$self->{modes}} = ( + $self->{modes} = { 'container-usage' => 'cloud::docker::restapi::mode::containerusage', 'list-containers' => 'cloud::docker::restapi::mode::listcontainers', 'node-status' => 'cloud::docker::restapi::mode::nodestatus', - ); + 'service-status' => 'cloud::docker::restapi::mode::servicestatus' + }; - $self->{custom_modes}{api} = 'cloud::docker::restapi::custom::api'; + $self->{custom_modes}->{api} = 'cloud::docker::restapi::custom::api'; return $self; }