diff --git a/src/cloud/cisco/webex/restapi/custom/api.pm b/src/cloud/cisco/webex/restapi/custom/api.pm new file mode 100644 index 0000000000..fee46241f9 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/custom/api.pm @@ -0,0 +1,569 @@ +# +# Copyright 2024 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::cisco::webex::restapi::custom::api; + +use strict; +use warnings; +use centreon::plugins::http; +use centreon::plugins::statefile; +use JSON::XS; +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 => { + 'client-id:s' => { name => 'client_id' }, + 'client-secret:s' => { name => 'client_secret' }, + 'refresh-token:s' => { name => 'refresh_token' }, + 'hostname:s' => { name => 'hostname' }, + 'port:s' => { name => 'port', default => 443 }, + 'proto:s' => { name => 'proto', default => 'https' }, + 'timeout:s' => { name => 'timeout', default => 30 }, + 'unknown-http-status:s' => { + name => 'unknown_http_status', + default => '%{http_code} < 200 or %{http_code} >= 300' + }, + 'warning-http-status:s' => { name => 'warning_http_status' }, + 'critical-http-status:s' => { name => 'critical_http_status' }, + 'cache-use' => { name => 'cache_use' } + }); + } + $options{options}->add_help(package => __PACKAGE__, sections => 'Webex API OPTIONS', once => 1); + + $self->{output} = $options{output}; + $self->{http} = centreon::plugins::http->new(%options, default_backend => 'curl'); + $self->{cache} = centreon::plugins::statefile->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) = @_; + + if (centreon::plugins::misc::is_empty($self->{option_results}->{hostname})) { + $self->{output}->add_option_msg(short_msg => 'Need to specify --hostname option.'); + $self->{output}->option_exit(); + } + + if (centreon::plugins::misc::is_empty($self->{option_results}->{client_id})) { + $self->{output}->add_option_msg(short_msg => 'Need to specify --client-id option.'); + $self->{output}->option_exit(); + } + + if (centreon::plugins::misc::is_empty($self->{option_results}->{client_secret})) { + $self->{output}->add_option_msg(short_msg => 'Need to specify --client-secret option.'); + $self->{output}->option_exit(); + } + + if (centreon::plugins::misc::is_empty($self->{option_results}->{refresh_token})) { + $self->{output}->add_option_msg(short_msg => 'Need to specify --refresh-token option.'); + $self->{output}->option_exit(); + } + + $self->{http}->set_options(%{$self->{option_results}}); + $self->{http}->add_header(key => 'Content-Type', value => 'application/x-www-form-urlencoded'); + + $self->{cache}->check_options(option_results => $self->{option_results}); + + return 0; +} + +sub get_connection_info { + my ($self, %options) = @_; + + return $self->{option_results}->{hostname} . ':' . $self->{option_results}->{port}; +} + +sub get_token { + my ($self, %options) = @_; + + my $has_cache_file = $self->{cache}->read( + statefile => + 'cisco_webexapi_' . md5_hex($self->{option_results}->{client_id}) + . '_' . md5_hex($self->{option_results}->{refresh_token}) + . '_' . md5_hex($self->{option_results}->{client_secret}) + ); + my $access_token = $self->{cache}->get(name => 'access_token'); + my $expires_on = $self->{cache}->get(name => 'expires_on'); + my $created_token_cnt = $self->{cache}->get(name => 'created_token_cnt'); + + if (!defined($created_token_cnt)) { + $created_token_cnt = 0; + } + + if ($has_cache_file == 0 || !defined($access_token) || $access_token eq '' || (($expires_on - time()) < 60)) { + my $post_data = 'client_id=' . $self->{option_results}->{client_id} . + '&client_secret=' . $self->{option_results}->{client_secret} . + '&refresh_token=' . $self->{option_results}->{refresh_token} . + '&grant_type=refresh_token'; + + my $content = $self->{http}->request( + method => 'POST', + url_path => '/v1/access_token', + query_form_post => $post_data, + unknown_status => $self->{option_results}->{unknown_http_status}, + warning_status => $self->{option_results}->{warning_http_status}, + critical_status => $self->{option_results}->{critical_http_status} + ); + + my $decoded; + eval { + $decoded = JSON::XS->new->utf8->decode($content); + }; + if ($@) { + $self->{output}->add_option_msg(short_msg => "An error occurred while decoding the response ('$content')."); + $self->{output}->option_exit(); + } + + $created_token_cnt += 1; + $access_token = $decoded->{access_token}; + my $data = { + updated => time(), + access_token => $decoded->{access_token}, + expires_in => $decoded->{expires_in}, + expires_on => time() + $decoded->{expires_in}, + created_token_cnt => $created_token_cnt + }; + $self->{cache}->write(data => $data); + } + + return $access_token; +} + +sub clean_token { + my ($self, %options) = @_; + + my $data = { updated => time() }; + $self->{cache}->write(data => $data); +} + +sub request_api { + my ($self, %options) = @_; + + my $get_param = []; + if (defined($options{get_param})) { + $get_param = $options{get_param}; + } + + my $token = $self->get_token(); + + while (1) { + # call the API without status code check to avoid token expiration problems + my ($content) = $self->{http}->request( + url_path => $options{endpoint}, + get_param => $get_param, + header => [ 'Authorization: Bearer ' . $token ], + unknown_status => '', + warning_status => '', + critical_status => '' + ); + + my $code = $self->{http}->get_code(); + + if ($code == 429) { + my ($retry) = $self->{http}->get_header(name => 'Retry-After'); + $retry = defined($retry) && $retry =~ /^\s*(\d+)\s*/ ? $retry : 1; + sleep($retry); + next; + } + + # Maybe token is invalid. so we retry + if (!defined($token) || $code =~ /401|403/) { + $self->clean_token() if (defined($token)); + $token = $self->get_token(); + + $content = $self->{http}->request( + url_path => $options{endpoint}, + get_param => $get_param, + header => [ 'Authorization: Bearer ' . $token ], + unknown_status => $self->{unknown_http_status}, + warning_status => $self->{warning_http_status}, + critical_status => $self->{critical_http_status} + ); + } elsif ($code < 200 || $code >= 300) {# in this case we retry with the same token but with status code check + $content = $self->{http}->request( + url_path => $options{endpoint}, + get_param => $get_param, + header => [ 'Authorization: Bearer ' . $token ], + unknown_status => $self->{unknown_http_status}, + warning_status => $self->{warning_http_status}, + critical_status => $self->{critical_http_status} + ); + } + + if (!defined($content) || $content eq '') { + $self->{output}->add_option_msg(short_msg => + "API returns empty content [code: '" . $code . "'] [message: '" . $self->{http}->get_message() . "']"); + $self->{output}->option_exit(); + } + + my $decoded; + eval { + $decoded = JSON::XS->new->allow_nonref(1)->utf8->decode($content); + }; + if ($@) { + $self->{output}->add_option_msg(short_msg => + "Cannot decode response (add --debug option to display returned content)"); + $self->{output}->option_exit(); + } + + return $decoded; + } +} + +sub write_cache_file { + my ($self, %options) = @_; + + $self->{cache}->read( + statefile => 'cache_webexapi_' . md5_hex($self->{option_results}->{client_id}) + ); + $self->{cache}->write(data => { + update_time => time(), + response => $self->{data} + }); +} + +sub get_cache_file_response { + my ($self, %options) = @_; + + my $cache_filename = 'cache_webexapi_' . md5_hex($self->{option_results}->{client_id}); + + $self->{cache}->read( + statefile => $cache_filename + ); + $self->{data} = $self->{cache}->get(name => 'response'); + if (!defined($self->{data})) { + $self->{output}->add_option_msg(short_msg => 'Cache file missing or could not load ' . $cache_filename); + $self->{output}->option_exit(); + } + + return $self->{data}; +} + +sub cache_data { + my ($self, %options) = @_; + + $self->{data}->{devices} = $self->get_devices(); + $self->{data}->{workspaces} = $self->get_workspaces(); + $self->write_cache_file(); +} + +sub get_devices { + my ($self, %options) = @_; + + return $self->get_cache_file_response()->{devices} if (defined($self->{option_results}->{cache_use})); + return $self->get_devices_from_api(); +} + +sub get_workspaces { + my ($self, %options) = @_; + + return $self->get_cache_file_response()->{workspaces} if (defined($self->{option_results}->{cache_use})); + return $self->get_workspaces_from_api(); +} + +sub get_devices_from_api { + my ($self, %options) = @_; + my $data = []; + + my $start = 0; + my $max = 100; + + # gets the first 100 devices + my $paged_items = $self->get_max_devices(start => $start, max => $max); + push @$data, @{$paged_items}; + my $item_cnt = scalar(@{$paged_items}); + # gets the next 100 devices until there are no more devices left in the response + while ($item_cnt > 0) { + $start += 100; + $paged_items = $self->get_max_devices(start => $start, max => $max); + $item_cnt = scalar(@{$paged_items}); + push @$data, @{$paged_items}; + } + + return $data; +} + +sub get_max_devices { + my ($self, %options) = @_; + + my $params = { + endpoint => '/v1/devices', + get_param => [ 'start=' . $options{start}, 'max=' . $options{max} ] + }; + + if (defined($self->{option_results}->{resource_type}) && $self->{option_results}->{resource_type} eq 'workspace' && defined $self->{option_results}->{workspace_id}) { + push @{$params->{get_param}}, + 'workspaceId=' . $self->{option_results}->{workspace_id}; + } + + if (defined($self->{option_results}->{resource_type}) && $self->{option_results}->{resource_type} eq 'person' && defined $self->{option_results}->{person_id}) { + push @{$params->{get_param}}, + 'personId=' . $self->{option_results}->{person_id}; + } + + my $response = $self->request_api(%$params); + my $results = []; + + for my $item (@{$response->{items}}) { + push @$results, { + id => $item->{id}, + display_name => $item->{displayName}, + product => $item->{product}, + ip => defined($item->{ip}) ? $item->{ip} : '', + type => $item->{type}, + serial => defined($item->{serial}) && $item->{serial} ? + $item->{serial} : + ($self->{option_results}->{use_id_empty_serial} ? 'id:' . substr($item->{id}, -10) : ''), + lifecycle => $item->{lifecycle}, + planned_maintenance => $item->{plannedMaintenance}, + connection_status => $item->{connectionStatus}, + error_codes => defined($item->{errorCodes}) ? join(';', @{$item->{errorCodes}}) : '' + }; + } + + return $results; +} + +sub get_device { + my ($self, %options) = @_; + + if (defined($self->{option_results}->{cache_use})) { + my $cached_devices = $self->get_cache_file_response()->{devices}; + + foreach my $cached_device (@$cached_devices) { + if ($cached_device->{id} eq $self->{option_results}->{device_id}) { + my $device = {}; + $device->{$cached_device->{id}} = $cached_device; + return $device; + } + } + } + + return $self->get_device_from_api(); +} + +sub get_device_from_api { + my ($self, %options) = @_; + + my $params = { + endpoint => "/v1/devices/$self->{option_results}->{device_id}" + }; + + my $response = $self->request_api(%$params); + my $device = {}; + + $device->{$response->{id}} = { + display_name => $response->{displayName}, + product => $response->{product}, + ip => defined($response->{ip}) ? $response->{ip} : '', + type => $response->{type}, + serial => $response->{serial}, + lifecycle => $response->{lifecycle}, + connection_status => $response->{connectionStatus}, + planned_maintenance => $response->{plannedMaintenance}, + error_codes => defined($response->{errorCodes}) ? join(';', @{$response->{errorCodes}}) : '' + }; + + return $device; +} + +sub get_workspace { + my ($self, %options) = @_; + + if (defined($self->{option_results}->{cache_use})) { + my $cached_workspaces = $self->get_cache_file_response()->{workspaces}; + + foreach my $cached_workspace (@$cached_workspaces) { + if ($cached_workspace->{id} eq $self->{option_results}->{workspace_id}) { + my $workspace = {}; + $workspace->{$cached_workspace->{id}} = $cached_workspace; + return $workspace; + } + } + } + + return $self->get_workspace_from_api(); +} + +sub get_workspace_from_api { + my ($self, %options) = @_; + + my $params = { + endpoint => "/v1/workspaces/$self->{option_results}->{workspace_id}" + }; + + my $response = $self->request_api(%$params); + my $workspace = {}; + + $workspace->{$response->{id}} = { + display_name => $response->{displayName}, + type => $response->{type}, + planned_maintenance => $response->{plannedMaintenance}->{mode}, + health => $response->{health}->{level}, + }; + + return $workspace; +} + +sub get_workspaces_from_api { + my ($self, %options) = @_; + my $data = []; + + my $start = 0; + my $max = 100; + + # gets the first 100 workspaces + my $paged_items = $self->get_max_workspaces(start => $start, max => $max); + push @$data, @{$paged_items}; + my $item_cnt = scalar(@{$paged_items}); + # gets the next 100 workspaces until there are no more workspaces left in the response + while ($item_cnt > 0) { + $start += 100; + $paged_items = $self->get_max_workspaces(start => $start, max => $max); + $item_cnt = scalar(@{$paged_items}); + push @$data, @{$paged_items}; + } + + return $data; +} + +sub get_max_workspaces { + my ($self, %options) = @_; + + my $params = { + endpoint => '/v1/workspaces', + get_param => [ 'start=' . $options{start}, 'max=' . $options{max} ] + }; + + if ($self->{option_results}->{type}) { + push @{$params->{get_param}}, 'type=' . $self->{option_results}->{type}; + } + + my $response = $self->request_api(%$params); + my $results = []; + + for my $item (@{$response->{items}}) { + push @$results, { + id => $item->{id}, + display_name => $item->{displayName}, + type => $item->{type}, + workspace_location_id => $item->{workspaceLocationId} + }; + } + + return $results; +} + +sub get_workspace_locations_from_api { + my ($self, %options) = @_; + + my $params = { + endpoint => '/v1/workspaceLocations' + }; + + my $response = $self->request_api(%$params); + my $results = []; + + for my $item (@{$response->{items}}) { + push @$results, { + id => $item->{id}, + display_name => $item->{displayName}, + address => $item->{address}, + city => $item->{cityName}, + latitude => $item->{latitude}, + longitude => $item->{longitude} + }; + } + + return $results; +} + +1; + +__END__ + +=head1 NAME + +Cisco Webex REST API + +=head1 Webex API OPTIONS + +Webex REST API + +=over 8 + +=item B<--hostname> + +Address of the server that hosts the API. + +=item B<--port> + +Define the TCP port to use to reach the API (default: 443). + +=item B<--proto> + +Define the protocol to reach the API (default: 'https'). + +=item B<--client-id> + +Define the client-id for authentication. + +=item B<--client-secret> + +Define the secret associated with the username. + +=item B<--refresh-token> + +Define the refresh token associated with the username. Used to renew the access token + +=item B<--timeout> + +Define the timeout in seconds for HTTP requests (default: 30). + +=back + +=head1 DESCRIPTION + +B. + +=cut diff --git a/src/cloud/cisco/webex/restapi/mode/cache.pm b/src/cloud/cisco/webex/restapi/mode/cache.pm new file mode 100644 index 0000000000..e86ed824cf --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/cache.pm @@ -0,0 +1,60 @@ +# +# Copyright 2024 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::cisco::webex::restapi::mode::cache; + +use base qw(centreon::plugins::templates::counter); + +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 manage_selection { + my ($self, %options) = @_; + + $options{custom}->cache_data(); + $self->{output}->output_add( + severity => 'OK', + short_msg => 'Cache files created successfully' + ); +} + +1; + +__END__ + +=head1 MODE + +Create cache files (other modes could use it with --cache-use option). + +=over 8 + +=back + +=cut diff --git a/src/cloud/cisco/webex/restapi/mode/devicestatus.pm b/src/cloud/cisco/webex/restapi/mode/devicestatus.pm new file mode 100644 index 0000000000..77311887a1 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/devicestatus.pm @@ -0,0 +1,212 @@ +# +# Copyright 2022 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::cisco::webex::restapi::mode::devicestatus; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'devices', + type => 1, + cb_prefix_output => 'prefix_output', + skipped_code => { -10 => 1 }, + message_multiple => 'All devices are ok' + } + ]; + + $self->{maps_counters}->{devices} = [ + { + label => 'status', + type => 2, + unknown_default => '', + critical_default => '%{error_codes} =~ /softwareupgradekeepsfailing/i', + warning_default => + '%{lifecycle} =~ /UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality|wifiradioquality/i', + set => + { + key_values => [ + { name => 'display_name' }, + { name => 'product' }, + { name => 'ip' }, + { name => 'type' }, + { name => 'serial' }, + { name => 'error_codes' }, + { name => 'planned_maintenance' }, + { name => 'lifecycle' }, + { name => 'connection_status' }, + + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_perfdata => sub {return 0;}, + closure_custom_threshold_check => \&catalog_status_threshold_ng + } + } + ]; +} + +sub prefix_output { + my ($self, %options) = @_; + my $pref = "device '" . $options{instance_value}->{display_name} . "'"; + + if (defined($options{instance_value}->{ip}) && $options{instance_value}->{ip}) { + $pref = $pref . " - $options{instance_value}->{ip}"; + } + + if (defined($options{instance_value}->{product}) && $options{instance_value}->{product}) { + $pref = $pref . ", $options{instance_value}->{product}"; + } + + if (defined($options{instance_value}->{type}) && $options{instance_value}->{type}) { + $pref = $pref . " ($options{instance_value}->{type})"; + } + + if (defined($options{instance_value}->{serial}) && $options{instance_value}->{serial}) { + $pref = $pref . " - $options{instance_value}->{serial}"; + } + + $pref = $pref . " - "; + + return $pref; +} + +sub custom_status_output { + my ($self, %options) = @_; + + my $error = defined($self->{result_values}->{error_codes}) ? $self->{result_values}->{error_codes} : 'NA'; + + if (defined($self->{result_values}->{error_codes}) && $self->{result_values}->{error_codes}) { + return "Error codes: $error - Connection status: $self->{result_values}->{connection_status} - Planed maintenance: $self->{result_values}->{planned_maintenance} - Lifecycle: $self->{result_values}->{lifecycle}"; + } + + return "Connection status: $self->{result_values}->{connection_status} - Planed maintenance: $self->{result_values}->{planned_maintenance} - Lifecycle: $self->{result_values}->{lifecycle}"; +} + +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 => + { + 'device-id:s' => { name => 'device_id' }, + 'workspace-id:s' => { name => 'workspace_id' }, + 'person-id:s' => { name => 'person_id' }, + 'resource-type:s' => { name => 'resource_type', default => 'workspace' }, + } + ); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); + + if ($self->{option_results}->{resource_type} !~ /^workspace|person/) { + $self->{output}->add_option_msg(short_msg => 'Unknown resource type. Must be "workspace" or "person"'); + $self->{output}->option_exit(); + } + + if (!defined($self->{option_results}->{device_id}) && $self->{option_results}->{resource_type} eq 'workspace' + && (!defined($self->{option_results}->{workspace_id}) || $self->{option_results}->{workspace_id} eq '')) { + $self->{output}->add_option_msg(short_msg => + 'Need to specify --workspace-id option when using --resource-type "workspace"'); + $self->{output}->option_exit(); + } + + if (!defined($self->{option_results}->{device_id}) && $self->{option_results}->{resource_type} eq 'person' + && (!defined($self->{option_results}->{person_id}) || $self->{option_results}->{person_id} eq '')) { + $self->{output}->add_option_msg(short_msg => + 'Need to specify --person-id option when using --resource-type "person"'); + $self->{output}->option_exit(); + } +} + +sub manage_selection { + my ($self, %options) = @_; + + if (defined($self->{option_results}->{device_id}) && $self->{option_results}->{device_id} ne '') { + $self->{devices} = $options{custom}->get_device(); + } else { + my $devices = $options{custom}->get_devices(); + + foreach my $device (@{$devices}) { + $self->{devices}->{$device->{id}} = $device; + } + } + + if (scalar(keys %{$self->{devices}}) <= 0) { + $self->{output}->add_option_msg(short_msg => "No device found with this --device-id."); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Check device status. + +=over 8 + +=item B<--device-id> + +Filter device by device-id. + +=item B<--workspace-id> + +Filter devices by workspace id. Used together with --resource-type "workspace". + +=item B<--person-id> + +Filter devices by personal id. Used together with --resource-type "person". + +=item B<--unknown-status> + +Set unknown threshold for status. (Default: '') +Can used special variables like: %{error_codes}, %{connection_status}, %{planned_maintenance}, %{lifecycle} + +=item B<--warning--status> + +Set warning threshold for status (Default: '%{lifecycle} =~ /UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality|wifiradioquality/i') +Can used special variables like: %{error_codes}, %{connection_status}, %{planned_maintenance}, %{lifecycle} + +=item B<--critical-status> + +Set critical threshold for status (Default: '%{error_codes} =~ /softwareupgradekeepsfailing/i'). +Can used special variables like: %{error_codes}, %{connection_status}, %{planned_maintenance}, %{lifecycle} + +=item B<--cache-use> + +Use the cache file instead of requesting the API (the cache file can be created with the cache mode). + +=back + +=cut \ No newline at end of file diff --git a/src/cloud/cisco/webex/restapi/mode/discovery.pm b/src/cloud/cisco/webex/restapi/mode/discovery.pm new file mode 100644 index 0000000000..65a0900151 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/discovery.pm @@ -0,0 +1,111 @@ +# +# Copyright 2024 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::cisco::webex::restapi::mode::discovery; + +use base qw(centreon::plugins::mode); + +use strict; +use warnings; +use JSON::XS; + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options(arguments => { + 'type:s' => { name => 'type' }, + 'prettify' => { name => 'prettify' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); + + if (defined($self->{option_results}->{type}) && $self->{option_results}->{type} !~ /^focus|huddle|meetingRoom|open|desk|other|notSet/) { + $self->{output}->add_option_msg(short_msg => 'unknown workspace type'); + $self->{output}->option_exit(); + } +} + +sub run { + my ($self, %options) = @_; + + my $disco_stats; + $disco_stats->{start_time} = time(); + + my $ws_locations = $options{custom}->get_workspace_locations_from_api(); + foreach my $ws_location (@{$ws_locations}) { + $self->{workspace_locations}->{$ws_location->{id}} = $ws_location; + } + + my $workspaces = $options{custom}->get_workspaces_from_api(); + + foreach my $workspace (@{$workspaces}) { + $workspace->{location_name} = $self->{workspace_locations}->{$workspace->{workspace_location_id}}->{display_name}; + $workspace->{longitude} = $self->{workspace_locations}->{$workspace->{workspace_location_id}}->{longitude}; + $workspace->{latitude} = $self->{workspace_locations}->{$workspace->{workspace_location_id}}->{latitude}; + $workspace->{address} = $self->{workspace_locations}->{$workspace->{workspace_location_id}}->{address}; + $workspace->{city} = $self->{workspace_locations}->{$workspace->{workspace_location_id}}->{city}; + } + + $disco_stats->{end_time} = time(); + $disco_stats->{duration} = $disco_stats->{end_time} - $disco_stats->{start_time}; + $disco_stats->{discovered_items} = scalar(@$workspaces); + $disco_stats->{results} = $workspaces; + + my $encoded_data; + eval { + if (defined($self->{option_results}->{prettify})) { + $encoded_data = JSON::XS->new->utf8->pretty->encode($disco_stats); + } else { + $encoded_data = JSON::XS->new->utf8->encode($disco_stats); + } + }; + if ($@) { + $encoded_data = '{"code":"encode_error","message":"Cannot encode discovered data into JSON format"}'; + } + + $self->{output}->output_add(short_msg => $encoded_data); + $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1); + $self->{output}->exit(); +} + +1; + +__END__ + +=head1 MODE + +workspace discovery. + +=over 8 + +=item B<--type> + +Choose the type of workspace to discover (can be: C, C, C, C, C, C, C). + +=back + +=cut \ No newline at end of file diff --git a/src/cloud/cisco/webex/restapi/mode/listdevices.pm b/src/cloud/cisco/webex/restapi/mode/listdevices.pm new file mode 100644 index 0000000000..cb4c35dd77 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/listdevices.pm @@ -0,0 +1,145 @@ +# +# Copyright 2022 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::cisco::webex::restapi::mode::listdevices; + +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 => { + 'workspace-id:s' => { name => 'workspace_id' }, + 'person-id:s' => { name => 'person_id' }, + 'resource-type:s' => { name => 'resource_type', default => 'workspace' }, + 'use-id-empty-serial' => { name => 'use_id_empty_serial' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); + + if ($self->{option_results}->{resource_type} !~ /^workspace|person/) { + $self->{output}->add_option_msg(short_msg => 'Unknown resource type. Must be "workspace" or "person"'); + $self->{output}->option_exit(); + } + + if ($self->{option_results}->{resource_type} eq 'workspace' + && (!defined($self->{option_results}->{workspace_id}) || $self->{option_results}->{workspace_id} eq '')) { + $self->{output}->add_option_msg(short_msg => + 'Need to specify --workspace-id option when using --resource-type "workspace"'); + $self->{output}->option_exit(); + } + + if ($self->{option_results}->{resource_type} eq 'person' + && (!defined($self->{option_results}->{person_id}) || $self->{option_results}->{person_id} eq '')) { + $self->{output}->add_option_msg(short_msg => + 'Need to specify --person-id option when using --resource-type "person"'); + $self->{output}->option_exit(); + } +} + +my @labels = ( + 'id', + 'display_name', + 'product', + 'ip', + 'type', + 'serial', + 'lifecycle', + 'planned_maintenance', + 'connection_status' +); + +sub manage_selection { + my ($self, %options) = @_; + + return $options{custom}->get_devices_from_api(); +} + +sub run { + my ($self, %options) = @_; + + my $results = $self->manage_selection(custom => $options{custom}); + foreach my $entry (@$results) { + $self->{output}->output_add(long_msg => + join('', map("[$_: " . $entry->{$_} . ']', @labels)) + ); + } + + $self->{output}->output_add(severity => 'OK', short_msg => 'List devices:'); + $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 => [ @labels ]); +} + +sub disco_show { + my ($self, %options) = @_; + + my $results = $self->manage_selection(%options); + foreach my $entry (@$results) { + $self->{output}->add_disco_entry(%$entry); + } +} + +1; + +__END__ + +=head1 MODE + +List devices. + +=over 8 + +=item B<--workspace-id> + +Filter devices by workspace id. Used together with --resource-type "workspace". + +=item B<--person-id> + +Filter devices by person id. Used together with --resource-type "person". + +=item B<--resource-type> + +Choose the type of resources to discover (can be: C, C). Default: C. + +=item B<--use-id-empty-serial> + +use the last 10 characters of the id as the serial number if serial number is empty. + +=back + + +=cut + diff --git a/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm new file mode 100644 index 0000000000..9c629e8611 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm @@ -0,0 +1,310 @@ +# +# Copyright 2022 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::cisco::webex::restapi::mode::workspacehealth; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use DateTime; +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'workspace', + type => 1, + cb_prefix_output => 'prefix_output', + skipped_code => { -10 => 1 }, + message_multiple => 'All workspaces are ok' + } + ]; + + $self->{maps_counters}->{workspace} = [ + { + label => 'status', + type => 2, + unknown_default => '', + critical_default => '', + warning_default => '', + set => + { + key_values => [ + { name => 'display_name' }, + { name => 'type' }, + { name => 'planned_maintenance' }, + { name => 'health' } + + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_perfdata => sub {return 0;}, + closure_custom_threshold_check => \&catalog_status_threshold_ng + } + }, + { label => 'temperature', nlabel => 'temperature.celsius', set => { + key_values => [ { name => 'temperature' } ], + output_template => 'Temperature: %d C', + perfdatas => [ + { + label => 'temperature', + value => 'temperature', + template => '%d', + unit => 'C' + } + ], + } + }, + { label => 'humidity', nlabel => 'humidity.percentage', set => { + key_values => [ { name => 'humidity' } ], + output_template => 'Humidity: %.2f%%', + perfdatas => [ + { + label => 'humidity', + value => 'humidity', + template => '%.2f', + min => 0, + max => 100, + unit => '%' + } + ], + } + }, + { label => 'ambient-noise', nlabel => 'ambient.noise.dB', set => { + key_values => [ { name => 'ambient_noise' } ], + output_template => 'Ambient noise: %.2f dB', + perfdatas => [ + { + template => '%.2f', + unit => 'dB', + min => 0 + }, + ], + } + }, + { label => 'tvoc', nlabel => 'tvoc', set => { + key_values => [ { name => 'tvoc' } ], + output_template => 'TVOC: %.2f', + perfdatas => [ + { + template => '%.2f', + min => 0 + }, + ], + } + } + ]; +} + +sub prefix_output { + my ($self, %options) = @_; + my $pref = "workspace '" . $options{instance_value}->{display_name} . "'"; + + if (defined($options{instance_value}->{type}) && $options{instance_value}->{type}) { + $pref = $pref . " ($options{instance_value}->{type})"; + } + + $pref = $pref . " - " if defined($self->{option_results}->{add_metrics}); + return $pref; +} + +sub custom_status_output { + my ($self, %options) = @_; + + return "Workspace health: $self->{result_values}->{health} - Planed maintenance: $self->{result_values}->{planned_maintenance}"; +} + +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 => { + 'workspace-id:s' => { name => 'workspace_id' }, + 'timeframe:s' => { name => 'timeframe', default => 900 }, + 'aggregation:s' => { name => 'aggregation', default => 'none' }, + 'zeroed' => { name => 'zeroed' }, + 'add-metrics' => { name => 'add_metrics' }, + } + ); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); + + $self->{timeframe} = (defined($self->{option_results}->{timeframe})) ? $self->{option_results}->{timeframe} : undef; + + if ($self->{option_results}->{aggregation} !~ /^none|hourly|daily$/i) { + $self->{output}->add_option_msg(short_msg => 'Unknown aggregation. Must be "none", "hourly" or "daily"'); + $self->{output}->option_exit(); + } +} + +sub get_metric_value { + my ($self, %options) = @_; + + my $start_time = DateTime->now->subtract(seconds => $self->{option_results}->{timeframe})->iso8601 . 'Z'; + my $end_time = DateTime->now->iso8601 . 'Z'; + + my $params = { + endpoint => + "/v1/workspaceMetrics", + get_param => [ + 'metricName=' . $options{metric}, + 'workspaceId=' . $self->{option_results}->{workspace_id}, + 'aggregation=' . $self->{option_results}->{aggregation}, + 'from=' . $start_time, + 'to=' . $end_time + ] + }; + + my $response = $options{custom}->request_api(%$params); + my $value_cnt = 0; + my $value = 0; + for my $item (@{$response->{items}}) { + if (!defined($item->{value}) && !defined($item->{mean})) { + next if (!defined($self->{option_results}->{zeroed})); + } + + $value += $self->{option_results}->{aggregation} eq 'none' ? + defined($item->{value}) ? $item->{value} : 0 + : + defined($item->{mean}) ? $item->{mean} : 0; + + $value_cnt++; + } + + if ($value_cnt > 0) { + return $value / $value_cnt; + } + + return undef; +} + +sub manage_selection { + my ($self, %options) = @_; + + if (defined($self->{option_results}->{workspace_id}) && $self->{option_results}->{workspace_id} ne '') { + $self->{workspace} = $options{custom}->get_workspace(); + } else { + my $workspaces = $options{custom}->get_workspaces(); + + foreach my $workspace (@{$workspaces}) { + $self->{workspace}->{$workspace->{id}} = $workspace; + } + } + + if (scalar(keys %{$self->{workspace}}) <= 0) { + $self->{output}->add_option_msg(short_msg => "No workspace found with this --workspace-id."); + $self->{output}->option_exit(); + } + + if (defined($self->{option_results}->{workspace_id}) && defined($self->{option_results}->{add_metrics})) { + foreach my $id (keys %{$self->{workspace}}) { + $self->{workspace}->{$id}->{temperature} = $self->get_metric_value( + metric => 'temperature', + custom => $options{custom} + ); + + $self->{workspace}->{$id}->{humidity} = $self->get_metric_value( + metric => 'humidity', + custom => $options{custom} + ); + + $self->{workspace}->{$id}->{ambient_noise} = $self->get_metric_value( + metric => 'ambientNoise', + custom => $options{custom} + ); + + $self->{workspace}->{$id}->{tvoc} = $self->get_metric_value( + metric => 'tvoc', + custom => $options{custom} + ); + } + } +} + +1; + +__END__ + +=head1 MODE + +Check workspace status. + +=over 8 + +=item B<--workspace-id> + +Filter workspaces by workspace-id. + +=item B<--add-metrics> + +Requests the metric values from the API for the single workspace. Can be used only with --workspace-id. + +=item B<--timeframe> + +Set timeframe in seconds (i.e. 3600 to check last hour, i.e 900 to check last 15 minutes). Default: 900. + +=item B<--aggregation> + +Define how the data must be aggregated. Available aggregations: C, C, C. Default: C. + +=item B<--zeroed> + +Set metrics value to 0 if they are missing. Useful when some metrics are undefined. + +=item B<--unknown-status> + +Set unknown threshold for status. (Default: '') +You can use the following variables: C<%{planned_maintenance}>, C<%{health}>. +C<%(health)> can have one of these values: C, C, C, C + +=item B<--warning--status> + +Set warning threshold for status (Default: '') +You can use the following variables: C<%{planned_maintenance}>, C<%{health}>. +C<%(health)> can have one of these values: C, C, C, C + +=item B<--critical-status> + +Set critical threshold for status (Default: ''). +You can use the following variables: C<%{planned_maintenance}>, C<%{health}>. +C<%(health)> can have one of these values: C, C, C, C + +=item B<--warning-*> B<--critical-*> + +Thresholds. +Can be: C (C), C (%), C (dB), C. + +=item B<--cache-use> + +Use the cache file instead of requesting the API (the cache file can be created with the cache mode). +The metrics are not get from the cache but always directly from the API. + +=back + +=cut \ No newline at end of file diff --git a/src/cloud/cisco/webex/restapi/plugin.pm b/src/cloud/cisco/webex/restapi/plugin.pm new file mode 100644 index 0000000000..b754fdfc2e --- /dev/null +++ b/src/cloud/cisco/webex/restapi/plugin.pm @@ -0,0 +1,52 @@ +# +# Copyright 2024 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::cisco::webex::restapi::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} = { + 'cache' => 'cloud::cisco::webex::restapi::mode::cache', + 'device-status' => 'cloud::cisco::webex::restapi::mode::devicestatus', + 'discovery' => 'cloud::cisco::webex::restapi::mode::discovery', + 'list-devices' => 'cloud::cisco::webex::restapi::mode::listdevices', + 'workspace-health' => 'cloud::cisco::webex::restapi::mode::workspacehealth' + }; + + $self->{custom_modes}->{api} = 'cloud::cisco::webex::restapi::custom::api'; + return $self; +} + +1; + +__END__ + +=head1 PLUGIN DESCRIPTION + +Monitor Cisco Webex devices and workspaces. + +=cut diff --git a/tests/resources/spellcheck/stopwords.txt b/tests/resources/spellcheck/stopwords.txt index 3b584cb49a..4b7f2ca33a 100644 --- a/tests/resources/spellcheck/stopwords.txt +++ b/tests/resources/spellcheck/stopwords.txt @@ -400,8 +400,11 @@ WaaS --warning-cpu-utilization --warning-na --warning-total-cpu-utilization +Webex WLAN WLC +workspace +workspaces WSMAN XPath ZDX