From 00fc986ffb0fc4d07cb1c906bd3c9a80a1b8233b Mon Sep 17 00:00:00 2001 From: rmorandell_pgum Date: Thu, 19 Feb 2026 14:46:53 +0100 Subject: [PATCH 1/7] init commit --- src/cloud/cisco/webex/restapi/custom/api.pm | 259 ++++++++++++++++ .../cisco/webex/restapi/mode/devicestatus.pm | 171 +++++++++++ .../cisco/webex/restapi/mode/discovery.pm | 148 +++++++++ .../cisco/webex/restapi/mode/listdevices.pm | 200 +++++++++++++ .../webex/restapi/mode/workspacehealth.pm | 283 ++++++++++++++++++ src/cloud/cisco/webex/restapi/plugin.pm | 51 ++++ 6 files changed, 1112 insertions(+) create mode 100644 src/cloud/cisco/webex/restapi/custom/api.pm create mode 100644 src/cloud/cisco/webex/restapi/mode/devicestatus.pm create mode 100644 src/cloud/cisco/webex/restapi/mode/discovery.pm create mode 100644 src/cloud/cisco/webex/restapi/mode/listdevices.pm create mode 100644 src/cloud/cisco/webex/restapi/mode/workspacehealth.pm create mode 100644 src/cloud/cisco/webex/restapi/plugin.pm 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..b9be35249c --- /dev/null +++ b/src/cloud/cisco/webex/restapi/custom/api.pm @@ -0,0 +1,259 @@ +# +# 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' } + }); + } + $options{options}->add_help(package => __PACKAGE__, sections => 'HPE Primera 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(); + } + $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 => + 'cloud_cisco_webexapi_' . md5_hex($self->get_connection_info() . '_' . $self->{option_results}->{client_id})); + my $access_token = $self->{cache}->get(name => 'access_token'); + my $expires_on = $self->{cache}->get(name => 'expires_on'); + + 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(); + } + + $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} + }; + $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(); + my ($content) = $self->{http}->request( + url_path => $options{endpoint}, + get_param => $get_param, + header => ['Authorization: Bearer ' . $token], + unknown_status => '', + warning_status => '', + critical_status => '' + ); + + # Maybe token is invalid. so we retry + if (!defined($token) || $self->{http}->get_code() >= 400) { + $self->clean_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} + ); + } + + if (!defined($content) || $content eq '') { + $self->{output}->add_option_msg(short_msg => + "API returns empty content [code: '" . $self->{http}->get_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; +} + +1; + +__END__ + +=head1 NAME + +cloud 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/devicestatus.pm b/src/cloud/cisco/webex/restapi/mode/devicestatus.pm new file mode 100644 index 0000000000..70f243fe52 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/devicestatus.pm @@ -0,0 +1,171 @@ +# +# 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 => 'device', type => 1, cb_prefix_output => 'prefix_output', skipped_code => { -10 => 1 } } + ]; + + $self->{maps_counters}->{device} = [ + { + label => 'status', + type => 2, + unknown_default => '', + critical_default => '%{error_codes} =~ /accountmissing|softwareupgradekeepsfailing|wifiradioquality/i', + warning_default => '%{lifecycle} =~ /END_OF_SALE|UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality/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) = @_; + + if (defined($self->{result_values}->{error_codes}) && $self->{result_values}->{error_codes}) { + return "Error codes: $self->{result_values}->{error_codes} - 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' } }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); +} + +sub manage_selection { + my ($self, %options) = @_; + + my $params = { + endpoint => "/v1/devices/$self->{option_results}->{device_id}" + }; + + my $response = $options{custom}->request_api(%$params); + $self->{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 => join(';', @{$response->{errorCodes}}) + }; + + if (scalar(keys %{$self->{device}}) <= 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<--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} =~ /END_OF_SALE|UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality/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} =~ /accountmissing|softwareupgradekeepsfailing|wifiradioquality|temperaturecheck/i'). +Can used special variables like: %{error_codes}, %{connection_status}, %{planned_maintenance}, %{lifecycle} + +=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..5264e42256 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/discovery.pm @@ -0,0 +1,148 @@ +# +# 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 get_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 = $options{custom}->request_api(%$params); + my $disco_data = []; + + for my $item (@{$response->{items}}) { + my $workspace = { + id => $item->{id}, + name => $item->{displayName}, + type => $item->{type} + }; + + push @$disco_data, $workspace; + } + + return $disco_data; +} + +sub discovery_node { + my ($self, %options) = @_; + my $disco_data = []; + + my $start = 0; + my $max = 100; + + # gets the first 100 workspaces + my $paged_items = $self->get_workspaces(custom => $options{custom}, start => $start, max => $max); + push @$disco_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_workspaces(custom => $options{custom}, start => $start, max => $max); + $item_cnt = scalar(@{$paged_items}); + push @$disco_data, @{$paged_items}; + } + + return $disco_data; +} + +sub run { + my ($self, %options) = @_; + + my $disco_stats; + $disco_stats->{start_time} = time(); + + my $results = $self->discovery_node(custom => $options{custom}); + + $disco_stats->{end_time} = time(); + $disco_stats->{duration} = $disco_stats->{end_time} - $disco_stats->{start_time}; + $disco_stats->{discovered_items} = scalar(@$results); + $disco_stats->{results} = $results; + + 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..0acc6975b5 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/listdevices.pm @@ -0,0 +1,200 @@ +# +# 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' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); + + if (!defined($self->{option_results}->{resource_type}) || $self->{option_results}->{resource_type} eq '') { + $self->{option_results}->{resource_type} = 'workspace'; + } + + 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', + 'error_codes' +); + +sub get_devices { + my ($self, %options) = @_; + + my $params = { + endpoint => '/v1/devices', + get_param => [ 'start=' . $options{start}, 'max=' . $options{max} ] + }; + + if ($self->{option_results}->{resource_type} eq 'workspace' && defined $self->{option_results}->{workspace_id}) { + push @{$params->{get_param}}, + 'workspaceId=' . $self->{option_results}->{workspace_id}; + } + + if ($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 = $options{custom}->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 => $item->{serial}, + lifecycle => $item->{lifecycle}, + planned_maintenance => $item->{plannedMaintenance}, + connection_status => $item->{connectionStatus}, + error_codes => join('|', @{$item->{errorCodes}}) + }; + } + + return $results; +} + +sub manage_selection { + my ($self, %options) = @_; + my $disco_data = []; + + my $start = 0; + my $max = 100; + + # gets the first 100 workspaces + my $paged_items = $self->get_devices(custom => $options{custom}, start => $start, max => $max); + push @$disco_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_devices(custom => $options{custom}, start => $start, max => $max); + $item_cnt = scalar(@{$paged_items}); + push @$disco_data, @{$paged_items}; + } + + return $disco_data; +} + +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. + +=item B<--person-id> + +Filter devices by person id. + +=item B<--resource-type> + +Choose the type of resources to discover (can be: C, C). Default: C. + +=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..9e583385c9 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm @@ -0,0 +1,283 @@ +# +# 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 } } + ]; + + $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 . " - "; + + 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' }, + } + ); + + 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) = @_; + + my $params = { + endpoint => "/v1/workspaces/$self->{option_results}->{workspace_id}" + }; + + my $response = $options{custom}->request_api(%$params); + my $id = $response->{id}; + + $self->{workspace}->{$id} = { + display_name => $response->{displayName}, + type => $response->{type}, + planned_maintenance => $response->{plannedMaintenance}->{mode}, + health => $response->{health}->{level}, + }; + + if (scalar(keys %{$self->{workspace}}) <= 0) { + $self->{output}->add_option_msg(short_msg => "No workspace found with this --workspace-id."); + $self->{output}->option_exit(); + } + + $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 workspace by 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. + +=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..3561f07639 --- /dev/null +++ b/src/cloud/cisco/webex/restapi/plugin.pm @@ -0,0 +1,51 @@ +# +# 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} = { + '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 From 88f9ca51950bd3ae26d2850af43405425bc38c72 Mon Sep 17 00:00:00 2001 From: rmorandell_pgum Date: Mon, 23 Feb 2026 10:37:37 +0100 Subject: [PATCH 2/7] refactor help --- src/cloud/cisco/webex/restapi/mode/workspacehealth.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm index 9e583385c9..7e58626500 100644 --- a/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm +++ b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm @@ -276,7 +276,7 @@ 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. +Can be: C (C), C (%), C (dB), C. =back From 4b6bfb1c8f0d914f208e0cd1eba1fdadd236c3f3 Mon Sep 17 00:00:00 2001 From: rmorandell_pgum Date: Tue, 24 Feb 2026 16:26:50 +0100 Subject: [PATCH 3/7] introducing --use-cahce to avoid HTTP 429 --- src/cloud/cisco/webex/restapi/custom/api.pm | 328 ++++++++++++++++-- src/cloud/cisco/webex/restapi/mode/cache.pm | 60 ++++ .../cisco/webex/restapi/mode/devicestatus.pm | 113 +++--- .../cisco/webex/restapi/mode/discovery.pm | 52 +-- .../cisco/webex/restapi/mode/listdevices.pm | 75 +--- .../webex/restapi/mode/workspacehealth.pm | 116 ++++--- src/cloud/cisco/webex/restapi/plugin.pm | 1 + 7 files changed, 510 insertions(+), 235 deletions(-) create mode 100644 src/cloud/cisco/webex/restapi/mode/cache.pm diff --git a/src/cloud/cisco/webex/restapi/custom/api.pm b/src/cloud/cisco/webex/restapi/custom/api.pm index b9be35249c..ef66248ddd 100644 --- a/src/cloud/cisco/webex/restapi/custom/api.pm +++ b/src/cloud/cisco/webex/restapi/custom/api.pm @@ -55,7 +55,8 @@ sub new { default => '%{http_code} < 200 or %{http_code} >= 300' }, 'warning-http-status:s' => { name => 'warning_http_status' }, - 'critical-http-status:s' => { name => 'critical_http_status' } + 'critical-http-status:s' => { name => 'critical_http_status' }, + 'cache-use' => { name => 'cache_use' } }); } $options{options}->add_help(package => __PACKAGE__, sections => 'HPE Primera API OPTIONS', once => 1); @@ -138,10 +139,10 @@ sub get_token { $access_token = $decoded->{access_token}; my $data = { - updated => time(), + updated => time(), access_token => $decoded->{access_token}, - expires_in => $decoded->{expires_in}, - expires_on => time() + $decoded->{expires_in} + expires_in => $decoded->{expires_in}, + expires_on => time() + $decoded->{expires_in} }; $self->{cache}->write(data => $data); } @@ -165,47 +166,304 @@ sub request_api { } my $token = $self->get_token(); - my ($content) = $self->{http}->request( - url_path => $options{endpoint}, - get_param => $get_param, - header => ['Authorization: Bearer ' . $token], - unknown_status => '', - warning_status => '', - critical_status => '' - ); - - # Maybe token is invalid. so we retry - if (!defined($token) || $self->{http}->get_code() >= 400) { - $self->clean_token(); - $token = $self->get_token(); - $content = $self->{http}->request( + while (1) { + my ($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} + 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 =~ /400|401|403/) { + $self->clean_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} + ); + } + + 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) = @_; - if (!defined($content) || $content eq '') { - $self->{output}->add_option_msg(short_msg => - "API returns empty content [code: '" . $self->{http}->get_code() . "'] [message: '" . $self->{http}->get_message() . "']"); + my $token = $self->get_token(); + $self->{cache}->read( + statefile => 'cache_webexapi_' . md5_hex($token) + ); + $self->{cache}->write(data => { + update_time => time(), + response => $self->{data} + }); +} + +sub get_cache_file_response { + my ($self, %options) = @_; + + my $token = $self->get_token(); + my $cache_filename = 'cache_webexapi_' . md5_hex($token); + + $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(); } - my $decoded; - eval { - $decoded = JSON::XS->new->allow_nonref(1)->utf8->decode($content); + 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 ($@) { - $self->{output}->add_option_msg(short_msg => - "Cannot decode response (add --debug option to display returned content)"); - $self->{output}->option_exit(); + + 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}; } - return $decoded; + 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 => 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 => 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} + }; + } + + return $results; } 1; @@ -250,6 +508,10 @@ Define the refresh token associated with the username. Used to renew the access Define the timeout in seconds for HTTP requests (default: 30). +=item B<--cache-use> + +Use the cache file instead of requesting the API (the cache file can be created with the cache mode). + =back =head1 DESCRIPTION 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 index 70f243fe52..504add7a40 100644 --- a/src/cloud/cisco/webex/restapi/mode/devicestatus.pm +++ b/src/cloud/cisco/webex/restapi/mode/devicestatus.pm @@ -30,33 +30,41 @@ sub set_counters { my ($self, %options) = @_; $self->{maps_counters_type} = [ - { name => 'device', type => 1, cb_prefix_output => 'prefix_output', skipped_code => { -10 => 1 } } + { + name => 'devices', + type => 1, + cb_prefix_output => 'prefix_output', + skipped_code => { -10 => 1 }, + message_multiple => 'All devices are ok' + } ]; - $self->{maps_counters}->{device} = [ + $self->{maps_counters}->{devices} = [ { label => 'status', type => 2, unknown_default => '', critical_default => '%{error_codes} =~ /accountmissing|softwareupgradekeepsfailing|wifiradioquality/i', - warning_default => '%{lifecycle} =~ /END_OF_SALE|UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality/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 - } + warning_default => + '%{lifecycle} =~ /END_OF_SALE|UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality/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 + } } ]; } @@ -89,8 +97,10 @@ sub prefix_output { 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: $self->{result_values}->{error_codes} - Connection status: $self->{result_values}->{connection_status} - Planed maintenance: $self->{result_values}->{planned_maintenance} - Lifecycle: $self->{result_values}->{lifecycle}"; + 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}"; @@ -101,7 +111,14 @@ sub new { 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' } }); + $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; } @@ -109,29 +126,41 @@ sub new { 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) = @_; - my $params = { - endpoint => "/v1/devices/$self->{option_results}->{device_id}" - }; - - my $response = $options{custom}->request_api(%$params); - $self->{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 => join(';', @{$response->{errorCodes}}) - }; - - if (scalar(keys %{$self->{device}}) <= 0) { + 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(); } @@ -151,6 +180,10 @@ Check device status. Filter device by device-id. +=item B<--workspace-id> + +Filter devices by workspace id. + =item B<--unknown-status> Set unknown threshold for status. (Default: '') diff --git a/src/cloud/cisco/webex/restapi/mode/discovery.pm b/src/cloud/cisco/webex/restapi/mode/discovery.pm index 5264e42256..ea0ddd1953 100644 --- a/src/cloud/cisco/webex/restapi/mode/discovery.pm +++ b/src/cloud/cisco/webex/restapi/mode/discovery.pm @@ -49,63 +49,13 @@ sub check_options { } } -sub get_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 = $options{custom}->request_api(%$params); - my $disco_data = []; - - for my $item (@{$response->{items}}) { - my $workspace = { - id => $item->{id}, - name => $item->{displayName}, - type => $item->{type} - }; - - push @$disco_data, $workspace; - } - - return $disco_data; -} - -sub discovery_node { - my ($self, %options) = @_; - my $disco_data = []; - - my $start = 0; - my $max = 100; - - # gets the first 100 workspaces - my $paged_items = $self->get_workspaces(custom => $options{custom}, start => $start, max => $max); - push @$disco_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_workspaces(custom => $options{custom}, start => $start, max => $max); - $item_cnt = scalar(@{$paged_items}); - push @$disco_data, @{$paged_items}; - } - - return $disco_data; -} - sub run { my ($self, %options) = @_; my $disco_stats; $disco_stats->{start_time} = time(); - my $results = $self->discovery_node(custom => $options{custom}); + my $results = $options{custom}->get_workspaces_from_api(); $disco_stats->{end_time} = time(); $disco_stats->{duration} = $disco_stats->{end_time} - $disco_stats->{start_time}; diff --git a/src/cloud/cisco/webex/restapi/mode/listdevices.pm b/src/cloud/cisco/webex/restapi/mode/listdevices.pm index 0acc6975b5..4669d9a899 100644 --- a/src/cloud/cisco/webex/restapi/mode/listdevices.pm +++ b/src/cloud/cisco/webex/restapi/mode/listdevices.pm @@ -31,9 +31,10 @@ sub new { 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' } + '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; @@ -43,10 +44,6 @@ sub check_options { my ($self, %options) = @_; $self->SUPER::init(%options); - if (!defined($self->{option_results}->{resource_type}) || $self->{option_results}->{resource_type} eq '') { - $self->{option_results}->{resource_type} = 'workspace'; - } - 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(); @@ -76,69 +73,13 @@ my @labels = ( 'serial', 'lifecycle', 'planned_maintenance', - 'connection_status', - 'error_codes' + 'connection_status' ); -sub get_devices { - my ($self, %options) = @_; - - my $params = { - endpoint => '/v1/devices', - get_param => [ 'start=' . $options{start}, 'max=' . $options{max} ] - }; - - if ($self->{option_results}->{resource_type} eq 'workspace' && defined $self->{option_results}->{workspace_id}) { - push @{$params->{get_param}}, - 'workspaceId=' . $self->{option_results}->{workspace_id}; - } - - if ($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 = $options{custom}->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 => $item->{serial}, - lifecycle => $item->{lifecycle}, - planned_maintenance => $item->{plannedMaintenance}, - connection_status => $item->{connectionStatus}, - error_codes => join('|', @{$item->{errorCodes}}) - }; - } - - return $results; -} - sub manage_selection { my ($self, %options) = @_; - my $disco_data = []; - - my $start = 0; - my $max = 100; - - # gets the first 100 workspaces - my $paged_items = $self->get_devices(custom => $options{custom}, start => $start, max => $max); - push @$disco_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_devices(custom => $options{custom}, start => $start, max => $max); - $item_cnt = scalar(@{$paged_items}); - push @$disco_data, @{$paged_items}; - } - return $disco_data; + return $options{custom}->get_devices_from_api(); } sub run { @@ -193,6 +134,10 @@ Filter devices by person id. 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 diff --git a/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm index 7e58626500..c832fc5a02 100644 --- a/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm +++ b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm @@ -31,15 +31,19 @@ sub set_counters { my ($self, %options) = @_; $self->{maps_counters_type} = [ - { name => 'workspace', type => 1, cb_prefix_output => 'prefix_output', skipped_code => { -10 => 1 } } + { + 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, + label => 'status', + type => 2, unknown_default => '', critical_default => '', warning_default => '', @@ -61,8 +65,12 @@ sub set_counters { key_values => [ { name => 'temperature' } ], output_template => 'Temperature: %d C', perfdatas => [ - { label => 'temperature', value => 'temperature', template => '%d', - unit => 'C' } + { + label => 'temperature', + value => 'temperature', + template => '%d', + unit => 'C' + } ], } }, @@ -70,8 +78,14 @@ sub set_counters { key_values => [ { name => 'humidity' } ], output_template => 'Humidity: %.2f%%', perfdatas => [ - { label => 'humidity', value => 'humidity', template => '%.2f', - min => 0, max => 100, unit => '%' } + { + label => 'humidity', + value => 'humidity', + template => '%.2f', + min => 0, + max => 100, + unit => '%' + } ], } }, @@ -79,7 +93,11 @@ sub set_counters { key_values => [ { name => 'ambient_noise' } ], output_template => 'Ambient noise: %.2f dB', perfdatas => [ - { template => '%.2f', unit => 'dB', min => 0 }, + { + template => '%.2f', + unit => 'dB', + min => 0 + }, ], } }, @@ -87,7 +105,10 @@ sub set_counters { key_values => [ { name => 'tvoc' } ], output_template => 'TVOC: %.2f', perfdatas => [ - { template => '%.2f', min => 0 }, + { + template => '%.2f', + min => 0 + }, ], } } @@ -102,8 +123,7 @@ sub prefix_output { $pref = $pref . " ($options{instance_value}->{type})"; } - $pref = $pref . " - "; - + $pref = $pref . " - " if defined($self->{option_results}->{add_metrics}); return $pref; } @@ -124,6 +144,7 @@ sub new { 'timeframe:s' => { name => 'timeframe', default => 900 }, 'aggregation:s' => { name => 'aggregation', default => 'none' }, 'zeroed' => { name => 'zeroed' }, + 'add-metrics' => { name => 'add_metrics' }, } ); @@ -187,45 +208,44 @@ sub get_metric_value { sub manage_selection { my ($self, %options) = @_; - my $params = { - endpoint => "/v1/workspaces/$self->{option_results}->{workspace_id}" - }; + 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(); - my $response = $options{custom}->request_api(%$params); - my $id = $response->{id}; - - $self->{workspace}->{$id} = { - display_name => $response->{displayName}, - type => $response->{type}, - planned_maintenance => $response->{plannedMaintenance}->{mode}, - health => $response->{health}->{level}, - }; + 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(); } - $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} - ); - + 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; @@ -242,6 +262,10 @@ Check workspace status. Filter workspace by workspace-id. +=item B<--add-metrics> + +Requests the metric values from the API for the single workspace + =item B<--timeframe> Set timeframe in seconds (i.e. 3600 to check last hour, i.e 900 to check last 15 minutes). Default: 900. @@ -276,7 +300,7 @@ 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. +Can be: C (C), C (%), C (dB), C. =back diff --git a/src/cloud/cisco/webex/restapi/plugin.pm b/src/cloud/cisco/webex/restapi/plugin.pm index 3561f07639..692e97bc2a 100644 --- a/src/cloud/cisco/webex/restapi/plugin.pm +++ b/src/cloud/cisco/webex/restapi/plugin.pm @@ -30,6 +30,7 @@ sub new { 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', From fbe4dd4745ae3a46aa2872bf58d6d5dd5a301b79 Mon Sep 17 00:00:00 2001 From: rmorandell_pgum Date: Wed, 25 Feb 2026 16:04:50 +0100 Subject: [PATCH 4/7] refactoring help & stopwords --- src/cloud/cisco/webex/restapi/custom/api.pm | 18 +++++--- .../cisco/webex/restapi/mode/devicestatus.pm | 18 +++++--- .../cisco/webex/restapi/mode/listdevices.pm | 4 +- .../webex/restapi/mode/workspacehealth.pm | 43 ++++++++++--------- src/cloud/cisco/webex/restapi/plugin.pm | 2 +- tests/resources/spellcheck/stopwords.txt | 3 ++ 6 files changed, 53 insertions(+), 35 deletions(-) diff --git a/src/cloud/cisco/webex/restapi/custom/api.pm b/src/cloud/cisco/webex/restapi/custom/api.pm index ef66248ddd..f8b63391bd 100644 --- a/src/cloud/cisco/webex/restapi/custom/api.pm +++ b/src/cloud/cisco/webex/restapi/custom/api.pm @@ -59,7 +59,7 @@ sub new { 'cache-use' => { name => 'cache_use' } }); } - $options{options}->add_help(package => __PACKAGE__, sections => 'HPE Primera API OPTIONS', once => 1); + $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'); @@ -83,14 +83,22 @@ sub check_options { $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'); @@ -109,7 +117,7 @@ sub get_token { my ($self, %options) = @_; my $has_cache_file = $self->{cache}->read(statefile => - 'cloud_cisco_webexapi_' . md5_hex($self->get_connection_info() . '_' . $self->{option_results}->{client_id})); + 'cisco_webexapi_' . md5_hex($self->get_connection_info() . '_' . $self->{option_results}->{client_id})); my $access_token = $self->{cache}->get(name => 'access_token'); my $expires_on = $self->{cache}->get(name => 'expires_on'); @@ -472,7 +480,7 @@ __END__ =head1 NAME -cloud cisco webex REST API +Cisco Webex REST API =head1 Webex API OPTIONS @@ -508,10 +516,6 @@ Define the refresh token associated with the username. Used to renew the access Define the timeout in seconds for HTTP requests (default: 30). -=item B<--cache-use> - -Use the cache file instead of requesting the API (the cache file can be created with the cache mode). - =back =head1 DESCRIPTION diff --git a/src/cloud/cisco/webex/restapi/mode/devicestatus.pm b/src/cloud/cisco/webex/restapi/mode/devicestatus.pm index 504add7a40..77311887a1 100644 --- a/src/cloud/cisco/webex/restapi/mode/devicestatus.pm +++ b/src/cloud/cisco/webex/restapi/mode/devicestatus.pm @@ -44,9 +44,9 @@ sub set_counters { label => 'status', type => 2, unknown_default => '', - critical_default => '%{error_codes} =~ /accountmissing|softwareupgradekeepsfailing|wifiradioquality/i', + critical_default => '%{error_codes} =~ /softwareupgradekeepsfailing/i', warning_default => - '%{lifecycle} =~ /END_OF_SALE|UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality/i', + '%{lifecycle} =~ /UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality|wifiradioquality/i', set => { key_values => [ @@ -182,7 +182,11 @@ Filter device by device-id. =item B<--workspace-id> -Filter devices by 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> @@ -191,14 +195,18 @@ Can used special variables like: %{error_codes}, %{connection_status}, %{planned =item B<--warning--status> -Set warning threshold for status (Default: '%{lifecycle} =~ /END_OF_SALE|UPCOMING_END_OF_SUPPORT/i || %{connection_status} =~ /disconnected/i || %{error_codes} =~ /upcomingendofsupport|networkquality|currentnetworkquality/i') +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} =~ /accountmissing|softwareupgradekeepsfailing|wifiradioquality|temperaturecheck/i'). +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/listdevices.pm b/src/cloud/cisco/webex/restapi/mode/listdevices.pm index 4669d9a899..cb4c35dd77 100644 --- a/src/cloud/cisco/webex/restapi/mode/listdevices.pm +++ b/src/cloud/cisco/webex/restapi/mode/listdevices.pm @@ -124,11 +124,11 @@ List devices. =item B<--workspace-id> -Filter devices by workspace id. +Filter devices by workspace id. Used together with --resource-type "workspace". =item B<--person-id> -Filter devices by person id. +Filter devices by person id. Used together with --resource-type "person". =item B<--resource-type> diff --git a/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm index c832fc5a02..9c629e8611 100644 --- a/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm +++ b/src/cloud/cisco/webex/restapi/mode/workspacehealth.pm @@ -66,10 +66,10 @@ sub set_counters { output_template => 'Temperature: %d C', perfdatas => [ { - label => 'temperature', - value => 'temperature', - template => '%d', - unit => 'C' + label => 'temperature', + value => 'temperature', + template => '%d', + unit => 'C' } ], } @@ -79,12 +79,12 @@ sub set_counters { output_template => 'Humidity: %.2f%%', perfdatas => [ { - label => 'humidity', - value => 'humidity', - template => '%.2f', - min => 0, - max => 100, - unit => '%' + label => 'humidity', + value => 'humidity', + template => '%.2f', + min => 0, + max => 100, + unit => '%' } ], } @@ -94,9 +94,9 @@ sub set_counters { output_template => 'Ambient noise: %.2f dB', perfdatas => [ { - template => '%.2f', - unit => 'dB', - min => 0 + template => '%.2f', + unit => 'dB', + min => 0 }, ], } @@ -106,8 +106,8 @@ sub set_counters { output_template => 'TVOC: %.2f', perfdatas => [ { - template => '%.2f', - min => 0 + template => '%.2f', + min => 0 }, ], } @@ -195,7 +195,6 @@ sub get_metric_value { defined($item->{mean}) ? $item->{mean} : 0; $value_cnt++; - } if ($value_cnt > 0) { @@ -260,11 +259,11 @@ Check workspace status. =item B<--workspace-id> -Filter workspace by workspace-id. +Filter workspaces by workspace-id. =item B<--add-metrics> -Requests the metric values from the API for the single workspace +Requests the metric values from the API for the single workspace. Can be used only with --workspace-id. =item B<--timeframe> @@ -276,8 +275,7 @@ Define how the data must be aggregated. Available aggregations: C, C -Set metrics value to 0 if they are missing. Useful when some metrics are -undefined. +Set metrics value to 0 if they are missing. Useful when some metrics are undefined. =item B<--unknown-status> @@ -302,6 +300,11 @@ C<%(health)> can have one of these values: C, C, C, C 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 index 692e97bc2a..b754fdfc2e 100644 --- a/src/cloud/cisco/webex/restapi/plugin.pm +++ b/src/cloud/cisco/webex/restapi/plugin.pm @@ -47,6 +47,6 @@ __END__ =head1 PLUGIN DESCRIPTION -Monitor cisco webex devices and workspaces. +Monitor Cisco Webex devices and workspaces. =cut diff --git a/tests/resources/spellcheck/stopwords.txt b/tests/resources/spellcheck/stopwords.txt index d5b003cfa8..b7be33faef 100644 --- a/tests/resources/spellcheck/stopwords.txt +++ b/tests/resources/spellcheck/stopwords.txt @@ -398,8 +398,11 @@ WaaS --warning-cpu-utilization --warning-na --warning-total-cpu-utilization +Webex WLAN WLC +workspace +workspaces WSMAN XPath ZDX From 2efe38eef2698ddac6376f62dbf82a51a46c163a Mon Sep 17 00:00:00 2001 From: rmorandell_pgum Date: Fri, 27 Feb 2026 16:35:25 +0100 Subject: [PATCH 5/7] discovery enhancements --- src/cloud/cisco/webex/restapi/custom/api.pm | 41 +++++++++++++++---- .../cisco/webex/restapi/mode/discovery.pm | 19 +++++++-- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/cloud/cisco/webex/restapi/custom/api.pm b/src/cloud/cisco/webex/restapi/custom/api.pm index f8b63391bd..28fa7ab2a1 100644 --- a/src/cloud/cisco/webex/restapi/custom/api.pm +++ b/src/cloud/cisco/webex/restapi/custom/api.pm @@ -117,7 +117,7 @@ sub get_token { my ($self, %options) = @_; my $has_cache_file = $self->{cache}->read(statefile => - 'cisco_webexapi_' . md5_hex($self->get_connection_info() . '_' . $self->{option_results}->{client_id})); + 'cisco_webexapi_' . md5_hex($self->{option_results}->{client_id})); my $access_token = $self->{cache}->get(name => 'access_token'); my $expires_on = $self->{cache}->get(name => 'expires_on'); @@ -195,7 +195,7 @@ sub request_api { } # Maybe token is invalid. so we retry - if (!defined($token) || $code =~ /400|401|403/) { + if (!defined($token) || $code < 200 || $code >= 300) { $self->clean_token(); $token = $self->get_token(); @@ -232,9 +232,8 @@ sub request_api { sub write_cache_file { my ($self, %options) = @_; - my $token = $self->get_token(); $self->{cache}->read( - statefile => 'cache_webexapi_' . md5_hex($token) + statefile => 'cache_webexapi_' . md5_hex($self->{option_results}->{client_id}) ); $self->{cache}->write(data => { update_time => time(), @@ -245,8 +244,7 @@ sub write_cache_file { sub get_cache_file_response { my ($self, %options) = @_; - my $token = $self->get_token(); - my $cache_filename = 'cache_webexapi_' . md5_hex($token); + my $cache_filename = 'cache_webexapi_' . md5_hex($self->{option_results}->{client_id}); $self->{cache}->read( statefile => $cache_filename @@ -338,7 +336,7 @@ sub get_max_devices { lifecycle => $item->{lifecycle}, planned_maintenance => $item->{plannedMaintenance}, connection_status => $item->{connectionStatus}, - error_codes => join('|', @{$item->{errorCodes}}) + error_codes => defined($item->{errorCodes}) ? join(';', @{$item->{errorCodes}}) : '' }; } @@ -382,7 +380,7 @@ sub get_device_from_api { lifecycle => $response->{lifecycle}, connection_status => $response->{connectionStatus}, planned_maintenance => $response->{plannedMaintenance}, - error_codes => join(';', @{$response->{errorCodes}}) + error_codes => defined($response->{errorCodes}) ? join(';', @{$response->{errorCodes}}) : '' }; return $device; @@ -463,11 +461,36 @@ sub get_max_workspaces { 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}, - type => $item->{type} + address => $item->{address}, + city => $item->{cityName}, + latitude => $item->{latitude}, + longitude => $item->{longitude} }; } diff --git a/src/cloud/cisco/webex/restapi/mode/discovery.pm b/src/cloud/cisco/webex/restapi/mode/discovery.pm index ea0ddd1953..65a0900151 100644 --- a/src/cloud/cisco/webex/restapi/mode/discovery.pm +++ b/src/cloud/cisco/webex/restapi/mode/discovery.pm @@ -55,12 +55,25 @@ sub run { my $disco_stats; $disco_stats->{start_time} = time(); - my $results = $options{custom}->get_workspaces_from_api(); + 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(@$results); - $disco_stats->{results} = $results; + $disco_stats->{discovered_items} = scalar(@$workspaces); + $disco_stats->{results} = $workspaces; my $encoded_data; eval { From d214f0adddd440c1dae239a341d5ab785866537f Mon Sep 17 00:00:00 2001 From: rmorandell_pgum Date: Thu, 5 Mar 2026 11:21:29 +0100 Subject: [PATCH 6/7] created token count in statefile --- src/cloud/cisco/webex/restapi/custom/api.pm | 29 ++++++++++++++++----- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/cloud/cisco/webex/restapi/custom/api.pm b/src/cloud/cisco/webex/restapi/custom/api.pm index 28fa7ab2a1..53656e275a 100644 --- a/src/cloud/cisco/webex/restapi/custom/api.pm +++ b/src/cloud/cisco/webex/restapi/custom/api.pm @@ -120,6 +120,11 @@ sub get_token { 'cisco_webexapi_' . md5_hex($self->{option_results}->{client_id})); 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} . @@ -145,12 +150,14 @@ sub get_token { $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} + 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); } @@ -176,6 +183,7 @@ sub request_api { 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, @@ -195,10 +203,19 @@ sub request_api { } # Maybe token is invalid. so we retry - if (!defined($token) || $code < 200 || $code >= 300) { - $self->clean_token(); + 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, From 72924d852a7c4197ab7e9b3c371c3c3a94fc42a6 Mon Sep 17 00:00:00 2001 From: rmorandell_pgum Date: Fri, 6 Mar 2026 11:58:30 +0100 Subject: [PATCH 7/7] add refresh_token & client_secret in hash to avoid token cleanup by wrong settings --- src/cloud/cisco/webex/restapi/custom/api.pm | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cloud/cisco/webex/restapi/custom/api.pm b/src/cloud/cisco/webex/restapi/custom/api.pm index 53656e275a..fee46241f9 100644 --- a/src/cloud/cisco/webex/restapi/custom/api.pm +++ b/src/cloud/cisco/webex/restapi/custom/api.pm @@ -116,8 +116,12 @@ sub get_connection_info { sub get_token { my ($self, %options) = @_; - my $has_cache_file = $self->{cache}->read(statefile => - 'cisco_webexapi_' . md5_hex($self->{option_results}->{client_id})); + 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');