From c4123298c54bfb5bec97058a7c6b4fd5d18865d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 09:37:31 +0000 Subject: [PATCH 01/26] feat: add Nutanix Prism REST API plugin Adds a new plugin to monitor Nutanix infrastructure via the Prism v2.0 REST API. Modes: - cluster-status: cluster state and node count - hosts-usage: CPU/memory usage and VM count per AHV host - storage-usage: storage pool capacity/free/usage% - vms-count: total/on/off VM counts - list-hosts: discovery of physical hosts - list-vms: discovery of virtual machines https://claude.ai/code/session_01PXMdAKHbDqnPokz5BHWAQp --- src/apps/nutanix/prism/custom/api.pm | 226 ++++++++++++++++++ src/apps/nutanix/prism/mode/clusterstatus.pm | 177 ++++++++++++++ src/apps/nutanix/prism/mode/hostsusage.pm | 229 +++++++++++++++++++ src/apps/nutanix/prism/mode/listhosts.pm | 102 +++++++++ src/apps/nutanix/prism/mode/listvms.pm | 102 +++++++++ src/apps/nutanix/prism/mode/storageusage.pm | 195 ++++++++++++++++ src/apps/nutanix/prism/mode/vmscount.pm | 132 +++++++++++ src/apps/nutanix/prism/plugin.pm | 54 +++++ 8 files changed, 1217 insertions(+) create mode 100644 src/apps/nutanix/prism/custom/api.pm create mode 100644 src/apps/nutanix/prism/mode/clusterstatus.pm create mode 100644 src/apps/nutanix/prism/mode/hostsusage.pm create mode 100644 src/apps/nutanix/prism/mode/listhosts.pm create mode 100644 src/apps/nutanix/prism/mode/listvms.pm create mode 100644 src/apps/nutanix/prism/mode/storageusage.pm create mode 100644 src/apps/nutanix/prism/mode/vmscount.pm create mode 100644 src/apps/nutanix/prism/plugin.pm diff --git a/src/apps/nutanix/prism/custom/api.pm b/src/apps/nutanix/prism/custom/api.pm new file mode 100644 index 0000000000..b86e25a871 --- /dev/null +++ b/src/apps/nutanix/prism/custom/api.pm @@ -0,0 +1,226 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::custom::api; + +use strict; +use warnings; +use centreon::plugins::http; +use JSON::XS; +use MIME::Base64; + +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}->option_exit(short_msg => "Class Custom: Need to specify 'options' argument."); + } + + if (!defined($options{noptions})) { + $options{options}->add_options( + arguments => { + 'hostname:s' => { name => 'hostname' }, + 'port:s' => { name => 'port', default => '9440' }, + 'proto:s' => { name => 'proto', default => 'https' }, + 'username:s' => { name => 'username' }, + 'password:s' => { name => 'password' }, + 'timeout:s' => { name => 'timeout', default => 30 }, + } + ); + } + $options{options}->add_help(package => __PACKAGE__, sections => 'REST API OPTIONS', once => 1); + + $self->{output} = $options{output}; + $self->{http} = centreon::plugins::http->new(%options, default_backend => 'curl'); + + return $self; +} + +sub set_options { + my ($self, %options) = @_; + $self->{option_results} = $options{option_results}; +} + +sub set_defaults {} + +sub check_options { + my ($self, %options) = @_; + + $self->{hostname} = (defined($self->{option_results}->{hostname})) ? $self->{option_results}->{hostname} : ''; + $self->{port} = $self->{option_results}->{port}; + $self->{proto} = $self->{option_results}->{proto}; + $self->{username} = (defined($self->{option_results}->{username})) ? $self->{option_results}->{username} : ''; + $self->{password} = (defined($self->{option_results}->{password})) ? $self->{option_results}->{password} : ''; + $self->{timeout} = $self->{option_results}->{timeout}; + + if ($self->{hostname} eq '') { + $self->{output}->option_exit(short_msg => "Need to specify --hostname option."); + } + if ($self->{username} eq '') { + $self->{output}->option_exit(short_msg => "Need to specify --username option."); + } + if ($self->{password} eq '') { + $self->{output}->option_exit(short_msg => "Need to specify --password option."); + } + + return 0; +} + +sub _get_auth_header { + my ($self) = @_; + + my $auth = MIME::Base64::encode_base64($self->{username} . ':' . $self->{password}); + chomp $auth; + return 'Authorization: Basic ' . $auth; +} + +sub request_api { + my ($self, %options) = @_; + + # Prism utilise le port 9440 par défaut et un préfixe /api/nutanix/v2.0 + my $url = $self->{proto} . '://' . $self->{hostname} . ':' . $self->{port}; + + $self->{option_results}->{hostname} = $self->{hostname}; + $self->{option_results}->{port} = $self->{port}; + $self->{option_results}->{proto} = $self->{proto}; + $self->{option_results}->{timeout} = $self->{timeout}; + $self->{option_results}->{warning_status} = ''; + $self->{option_results}->{critical_status} = ''; + $self->{option_results}->{unknown_status} = '%{http_code} < 200 or %{http_code} >= 300'; + + $self->{http}->set_options(%{$self->{option_results}}); + + my $method = (defined($options{method})) ? $options{method} : 'GET'; + my @headers = ( + $self->_get_auth_header(), + 'Content-Type: application/json', + 'Accept: application/json', + ); + + my ($content) = $self->{http}->request( + method => $method, + url_path => $options{endpoint}, + header => \@headers, + get_param => $options{get_param}, + query_form_post => $options{query_form_post}, + insecure => 1, # Les déploiements Nutanix utilisent souvent des certs auto-signés + ); + + if (!defined($content) || $content eq '') { + $self->{output}->add_option_msg( + short_msg => "API returned empty content [code: '" + . $self->{http}->get_code() . "'] [message: '" + . $self->{http}->get_message() . "']" + ); + $self->{output}->option_exit(); + } + + my $decoded; + eval { $decoded = decode_json($content) }; + if ($@) { + $self->{output}->add_option_msg( + short_msg => "Cannot decode JSON response: $@ [content: $content]" + ); + $self->{output}->option_exit(); + } + + return $decoded; +} + +# Retourne les infos du/des clusters Nutanix +sub get_clusters { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/clusters'); +} + +# Retourne la liste des hôtes physiques +sub get_hosts { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/hosts'); +} + +# Retourne la liste des VMs +sub get_vms { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/vms'); +} + +# Retourne les pools de stockage +sub get_storage_pools { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/storage_pools'); +} + +1; + +__END__ + +=head1 NAME + +apps::nutanix::prism::custom::api - Custom module for Nutanix Prism REST API. + +=head1 SYNOPSIS + + use apps::nutanix::prism::custom::api; + +=head1 DESCRIPTION + +This module handles authentication and HTTP requests to the Nutanix Prism REST API. +It uses HTTP Basic Auth (username:password in Base64) on every request. +The default port is 9440 and the protocol is HTTPS. +Self-signed certificates are accepted (insecure => 1). + +=head1 REST API OPTIONS + +=over 4 + +=item B<--hostname> + +Nutanix Prism hostname or IP address. + +=item B<--port> + +API port (default: 9440). + +=item B<--proto> + +Protocol (default: https). + +=item B<--username> + +API username. + +=item B<--password> + +API password. + +=item B<--timeout> + +HTTP request timeout in seconds (default: 30). + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/clusterstatus.pm b/src/apps/nutanix/prism/mode/clusterstatus.pm new file mode 100644 index 0000000000..2758e64a3d --- /dev/null +++ b/src/apps/nutanix/prism/mode/clusterstatus.pm @@ -0,0 +1,177 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::clusterstatus; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub custom_status_output { + my ($self, %options) = @_; + return sprintf( + "cluster '%s' state is '%s' [version: %s]", + $self->{result_values}->{name}, + $self->{result_values}->{state}, + $self->{result_values}->{version} + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'clusters', + type => 1, + cb_prefix_output => 'prefix_cluster_output', + message_multiple => 'All clusters are OK', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{clusters} = [ + # Compteur de type "status" (type => 2) : vérifie un état via une expression + { + label => 'status', + type => 2, + # Seuil warning par défaut : état différent de "COMPLETE" + warning_default => '%{state} ne "COMPLETE"', + set => { + key_values => [ + { name => 'name' }, + { name => 'state' }, + { name => 'version' }, + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_threshold_check => \&catalog_status_threshold_ng, + } + }, + # Compteur numérique : nombre de nœuds + { + label => 'nodes-count', + nlabel => 'cluster.nodes.count', + set => { + key_values => [ { name => 'num_nodes' }, { name => 'name' } ], + output_template => 'nodes: %d', + perfdatas => [ + { + template => '%d', + label_extra_instance => 1, + instance_use => 'name', + min => 0, + } + ] + } + }, + ]; +} + +sub prefix_cluster_output { + my ($self, %options) = @_; + return "Cluster '" . $options{instance_value}->{name} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + # Appel au module custom (api.pm) via $options{custom} + my $result = $options{custom}->get_clusters(); + + # L'API v2.0 retourne { entities => [...], metadata => {...} } + my $entities = $result->{entities} // []; + + $self->{clusters} = {}; + for my $cluster (@{$entities}) { + my $name = $cluster->{name} // 'unknown'; + + # Filtrage optionnel par nom (regex) + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/; + } + + # On stocke les données dans le hash $self->{clusters} + # La clé est unique par instance (ici le nom du cluster) + $self->{clusters}->{$name} = { + name => $name, + # cluster_state est dans les stats internes de Prism v2 + state => $cluster->{cluster_state} // 'UNKNOWN', + version => $cluster->{version} // 'N/A', + num_nodes => $cluster->{num_nodes} // 0, + }; + } + + if (scalar(keys %{$self->{clusters}}) == 0) { + $self->{output}->add_option_msg(short_msg => 'No cluster found.'); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix cluster status through Prism REST API. + +=over 8 + +=item B<--filter-name> + +Filter clusters by name (regexp). Example: C<--filter-name='^Prod'> + +=item B<--warning-status> + +Warning threshold for cluster state. +Default: C<%{state} ne "COMPLETE"> + +Variables: C<%{name}>, C<%{state}>, C<%{version}> + +=item B<--critical-status> + +Critical threshold for cluster state. + +=item B<--warning-nodes-count> + +Warning threshold for node count. + +=item B<--critical-nodes-count> + +Critical threshold for node count. + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/hostsusage.pm b/src/apps/nutanix/prism/mode/hostsusage.pm new file mode 100644 index 0000000000..2eba708d42 --- /dev/null +++ b/src/apps/nutanix/prism/mode/hostsusage.pm @@ -0,0 +1,229 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::hostsusage; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub custom_status_output { + my ($self, %options) = @_; + return sprintf( + "host '%s' state is '%s'", + $self->{result_values}->{name}, + $self->{result_values}->{state} + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'hosts', + type => 1, + cb_prefix_output => 'prefix_host_output', + message_multiple => 'All hosts are OK', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{hosts} = [ + # Statut de l'hôte + { + label => 'status', + type => 2, + warning_default => '%{state} ne "NORMAL"', + set => { + key_values => [ { name => 'name' }, { name => 'state' } ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_threshold_check => \&catalog_status_threshold_ng, + } + }, + # Utilisation CPU en pourcentage + { + label => 'cpu-usage', + nlabel => 'host.cpu.usage.percentage', + set => { + key_values => [ { name => 'cpu_usage_pct' }, { name => 'name' } ], + output_template => 'CPU usage: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + max => 100, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Utilisation RAM en pourcentage + { + label => 'memory-usage', + nlabel => 'host.memory.usage.percentage', + set => { + key_values => [ { name => 'memory_usage_pct' }, { name => 'name' } ], + output_template => 'memory usage: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + max => 100, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Nombre de VMs sur cet hôte + { + label => 'vms-count', + nlabel => 'host.vms.count', + set => { + key_values => [ { name => 'num_vms' }, { name => 'name' } ], + output_template => 'VMs: %d', + perfdatas => [ + { + template => '%d', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + ]; +} + +sub prefix_host_output { + my ($self, %options) = @_; + return "Host '" . $options{instance_value}->{name} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_hosts(); + my $entities = $result->{entities} // []; + + $self->{hosts} = {}; + for my $host (@{$entities}) { + my $name = $host->{name} // $host->{uuid} // 'unknown'; + + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/; + } + + # L'API v2.0 retourne des stats dans host->{stats} + # cpu_usage_ppm = parties par million (diviser par 10000 pour avoir %) + my $stats = $host->{stats} // {}; + my $cpu_ppm = $stats->{hypervisor_cpu_usage_ppm} // 0; + my $cpu_pct = $cpu_ppm / 10000; + + # memory_usage_ppm également en ppm + my $mem_ppm = $stats->{hypervisor_memory_usage_ppm} // 0; + my $mem_pct = $mem_ppm / 10000; + + $self->{hosts}->{$name} = { + name => $name, + state => $host->{host_in_maintenance_mode} ? 'MAINTENANCE' : 'NORMAL', + cpu_usage_pct => $cpu_pct, + memory_usage_pct => $mem_pct, + num_vms => $host->{num_vms} // 0, + }; + } + + if (scalar(keys %{$self->{hosts}}) == 0) { + $self->{output}->add_option_msg(short_msg => 'No host found.'); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix host CPU, memory usage and VM count through Prism REST API. + +=over 8 + +=item B<--filter-name> + +Filter hosts by name (regexp). Example: C<--filter-name='^AHV'> + +=item B<--warning-status> + +Warning threshold for host state. +Default: C<%{state} ne "NORMAL"> + +Variables: C<%{name}>, C<%{state}> + +=item B<--critical-status> + +Critical threshold for host state. + +=item B<--warning-cpu-usage> + +Warning threshold for CPU usage (%). + +=item B<--critical-cpu-usage> + +Critical threshold for CPU usage (%). + +=item B<--warning-memory-usage> + +Warning threshold for memory usage (%). + +=item B<--critical-memory-usage> + +Critical threshold for memory usage (%). + +=item B<--warning-vms-count> + +Warning threshold for VM count per host. + +=item B<--critical-vms-count> + +Critical threshold for VM count per host. + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/listhosts.pm b/src/apps/nutanix/prism/mode/listhosts.pm new file mode 100644 index 0000000000..6741c51c25 --- /dev/null +++ b/src/apps/nutanix/prism/mode/listhosts.pm @@ -0,0 +1,102 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::listhosts; + +use strict; +use warnings; +use base qw(centreon::plugins::mode); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options(arguments => {}); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); +} + +sub run { + my ($self, %options) = @_; + + my $result = $options{custom}->get_hosts(); + my $entities = $result->{entities} // []; + + for my $host (@{$entities}) { + my $name = $host->{name} // $host->{uuid} // 'unknown'; + my $uuid = $host->{uuid} // 'N/A'; + my $ip = $host->{hypervisor_address} // 'N/A'; + my $model = $host->{block_model_name} // 'N/A'; + + $self->{output}->output_add( + long_msg => sprintf( + " name: %-30s uuid: %-40s ip: %-16s model: %s", + $name, $uuid, $ip, $model + ) + ); + } + + $self->{output}->output_add(severity => 'OK', short_msg => 'List of Nutanix hosts:'); + $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1, force_long_output => 1); + $self->{output}->exit(); +} + +# Appelé par le framework Centreon pour la découverte automatique +sub disco_format { + my ($self, %options) = @_; + $self->{output}->add_disco_format(elements => ['name', 'uuid', 'ip', 'model', 'num_vms']); +} + +sub disco_show { + my ($self, %options) = @_; + + my $result = $options{custom}->get_hosts(); + my $entities = $result->{entities} // []; + + for my $host (@{$entities}) { + $self->{output}->add_disco_entry( + name => $host->{name} // 'unknown', + uuid => $host->{uuid} // 'N/A', + ip => $host->{hypervisor_address} // 'N/A', + model => $host->{block_model_name} // 'N/A', + num_vms => $host->{num_vms} // 0, + ); + } +} + +1; + +__END__ + +=head1 MODE + +List Nutanix hosts for service discovery. + +=over 8 + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/listvms.pm b/src/apps/nutanix/prism/mode/listvms.pm new file mode 100644 index 0000000000..760e0a1202 --- /dev/null +++ b/src/apps/nutanix/prism/mode/listvms.pm @@ -0,0 +1,102 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::listvms; + +use strict; +use warnings; +use base qw(centreon::plugins::mode); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options(arguments => {}); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); +} + +sub run { + my ($self, %options) = @_; + + my $result = $options{custom}->get_vms(); + my $entities = $result->{entities} // []; + + for my $vm (@{$entities}) { + my $name = $vm->{name} // $vm->{uuid} // 'unknown'; + my $uuid = $vm->{uuid} // 'N/A'; + my $power_state = $vm->{power_state} // 'unknown'; + my $host_uuid = $vm->{host_uuid} // 'N/A'; + + $self->{output}->output_add( + long_msg => sprintf( + " name: %-40s uuid: %-40s power_state: %-8s host: %s", + $name, $uuid, $power_state, $host_uuid + ) + ); + } + + $self->{output}->output_add(severity => 'OK', short_msg => 'List of Nutanix VMs:'); + $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1, force_long_output => 1); + $self->{output}->exit(); +} + +sub disco_format { + my ($self, %options) = @_; + $self->{output}->add_disco_format(elements => ['name', 'uuid', 'power_state', 'host_uuid', 'num_vcpus', 'memory_mb']); +} + +sub disco_show { + my ($self, %options) = @_; + + my $result = $options{custom}->get_vms(); + my $entities = $result->{entities} // []; + + for my $vm (@{$entities}) { + $self->{output}->add_disco_entry( + name => $vm->{name} // 'unknown', + uuid => $vm->{uuid} // 'N/A', + power_state => $vm->{power_state} // 'unknown', + host_uuid => $vm->{host_uuid} // 'N/A', + num_vcpus => $vm->{num_vcpus} // 0, + memory_mb => int(($vm->{memory_mb} // 0)), + ); + } +} + +1; + +__END__ + +=head1 MODE + +List Nutanix VMs for service discovery. + +=over 8 + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/storageusage.pm b/src/apps/nutanix/prism/mode/storageusage.pm new file mode 100644 index 0000000000..6e30a857ba --- /dev/null +++ b/src/apps/nutanix/prism/mode/storageusage.pm @@ -0,0 +1,195 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::storageusage; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'storage_pools', + type => 1, + cb_prefix_output => 'prefix_pool_output', + message_multiple => 'All storage pools are OK', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{storage_pools} = [ + # Capacité totale en octets + { + label => 'usage', + nlabel => 'storage.pool.usage.bytes', + set => { + key_values => [ { name => 'usage_bytes' }, { name => 'name' } ], + output_template => 'used: %s', + # Conversion automatique d'octets vers l'unité lisible (KB, MB, GB...) + output_change_bytes => 1, + perfdatas => [ + { + template => '%d', + unit => 'B', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Capacité libre en octets + { + label => 'free', + nlabel => 'storage.pool.free.bytes', + set => { + key_values => [ { name => 'free_bytes' }, { name => 'name' } ], + output_template => 'free: %s', + output_change_bytes => 1, + perfdatas => [ + { + template => '%d', + unit => 'B', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Utilisation en pourcentage (calculée) + { + label => 'usage-prct', + nlabel => 'storage.pool.usage.percentage', + set => { + key_values => [ { name => 'usage_pct' }, { name => 'name' } ], + output_template => 'usage: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + max => 100, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + ]; +} + +sub prefix_pool_output { + my ($self, %options) = @_; + return "Storage pool '" . $options{instance_value}->{name} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_storage_pools(); + my $entities = $result->{entities} // []; + + $self->{storage_pools} = {}; + for my $pool (@{$entities}) { + my $name = $pool->{name} // $pool->{storage_pool_uuid} // 'unknown'; + + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/; + } + + # capacity_bytes et usage_bytes sont fournis directement par l'API v2.0 + my $capacity = $pool->{capacity_bytes} // 0; + my $used = $pool->{usage_bytes} // 0; + my $free = $capacity - $used; + my $pct = ($capacity > 0) ? ($used / $capacity * 100) : 0; + + $self->{storage_pools}->{$name} = { + name => $name, + usage_bytes => $used, + free_bytes => $free, + usage_pct => $pct, + }; + } + + if (scalar(keys %{$self->{storage_pools}}) == 0) { + $self->{output}->add_option_msg(short_msg => 'No storage pool found.'); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix storage pool usage through Prism REST API. + +=over 8 + +=item B<--filter-name> + +Filter storage pools by name (regexp). + +=item B<--warning-usage> + +Warning threshold for used space (bytes). + +=item B<--critical-usage> + +Critical threshold for used space (bytes). + +=item B<--warning-usage-prct> + +Warning threshold for usage percentage (%). + +=item B<--critical-usage-prct> + +Critical threshold for usage percentage (%). Example: C<--critical-usage-prct=90> + +=item B<--warning-free> + +Warning threshold for free space (bytes). + +=item B<--critical-free> + +Critical threshold for free space (bytes). + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/vmscount.pm b/src/apps/nutanix/prism/mode/vmscount.pm new file mode 100644 index 0000000000..339bb8b43b --- /dev/null +++ b/src/apps/nutanix/prism/mode/vmscount.pm @@ -0,0 +1,132 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::vmscount; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); + +sub set_counters { + my ($self, %options) = @_; + + # type => 0 : compteur global (pas d'instance multiple) + $self->{maps_counters_type} = [ + { name => 'global', type => 0 } + ]; + + $self->{maps_counters}->{global} = [ + { + label => 'total', + nlabel => 'vms.total.count', + set => { + key_values => [ { name => 'total' } ], + output_template => 'total VMs: %d', + perfdatas => [ + { template => '%d', min => 0 } + ] + } + }, + { + label => 'on', + nlabel => 'vms.on.count', + set => { + key_values => [ { name => 'on' } ], + output_template => 'powered on: %d', + perfdatas => [ + { template => '%d', min => 0 } + ] + } + }, + { + label => 'off', + nlabel => 'vms.off.count', + set => { + key_values => [ { name => 'off' } ], + output_template => 'powered off: %d', + perfdatas => [ + { template => '%d', min => 0 } + ] + } + }, + ]; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_vms(); + my $entities = $result->{entities} // []; + + my $total = scalar(@{$entities}); + my $on = scalar(grep { ($_->{power_state} // '') eq 'on' } @{$entities}); + my $off = $total - $on; + + $self->{global} = { + total => $total, + on => $on, + off => $off, + }; +} + +1; + +__END__ + +=head1 MODE + +Count Nutanix VMs by power state through Prism REST API. + +=over 8 + +=item B<--warning-total> + +Warning threshold for total VM count. + +=item B<--critical-total> + +Critical threshold for total VM count. + +=item B<--warning-on> + +Warning threshold for powered-on VM count. + +=item B<--critical-on> + +Critical threshold for powered-on VM count. + +=item B<--warning-off> + +Warning threshold for powered-off VM count. + +=item B<--critical-off> + +Critical threshold for powered-off VM count. + +=back + +=cut diff --git a/src/apps/nutanix/prism/plugin.pm b/src/apps/nutanix/prism/plugin.pm new file mode 100644 index 0000000000..ff81e51bc7 --- /dev/null +++ b/src/apps/nutanix/prism/plugin.pm @@ -0,0 +1,54 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::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->{version} = '0.1'; + $self->{modes} = { + 'cluster-status' => 'apps::nutanix::prism::mode::clusterstatus', + 'hosts-usage' => 'apps::nutanix::prism::mode::hostsusage', + 'storage-usage' => 'apps::nutanix::prism::mode::storageusage', + 'vms-count' => 'apps::nutanix::prism::mode::vmscount', + 'list-hosts' => 'apps::nutanix::prism::mode::listhosts', + 'list-vms' => 'apps::nutanix::prism::mode::listvms', + }; + + $self->{custom_modes}->{api} = 'apps::nutanix::prism::custom::api'; + return $self; +} + +1; + +__END__ + +=head1 PLUGIN DESCRIPTION + +Monitor Nutanix infrastructure through Prism REST API. + +=cut From ec33f5ce8eb9fcb7401ae34710d05f55a2d52007 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 14:18:56 +0000 Subject: [PATCH 02/26] feat(nutanix): add disks-status, snapshots and vms-nics modes - disksstatus: monitor physical disk state (NORMAL/FAILED), online status, capacity, free space and usage% per disk with node/id filters - snapshots: monitor snapshot count (global + per VM) and oldest snapshot age per VM with human-readable output (days/hours/minutes) - vmsnics: monitor VM NIC connectivity status and network traffic (in/out) with filters by VM name, MAC address and network name - plugin.pm: register the 3 new modes - custom/api.pm: add get_disks(), get_snapshots() and get_vm_nics() methods https://claude.ai/code/session_01PXMdAKHbDqnPokz5BHWAQp --- src/apps/nutanix/prism/custom/api.pm | 26 +++ src/apps/nutanix/prism/mode/disksstatus.pm | 252 ++++++++++++++++++++ src/apps/nutanix/prism/mode/snapshots.pm | 237 +++++++++++++++++++ src/apps/nutanix/prism/mode/vmsnics.pm | 259 +++++++++++++++++++++ src/apps/nutanix/prism/plugin.pm | 3 + 5 files changed, 777 insertions(+) create mode 100644 src/apps/nutanix/prism/mode/disksstatus.pm create mode 100644 src/apps/nutanix/prism/mode/snapshots.pm create mode 100644 src/apps/nutanix/prism/mode/vmsnics.pm diff --git a/src/apps/nutanix/prism/custom/api.pm b/src/apps/nutanix/prism/custom/api.pm index b86e25a871..326b5f0b12 100644 --- a/src/apps/nutanix/prism/custom/api.pm +++ b/src/apps/nutanix/prism/custom/api.pm @@ -174,6 +174,32 @@ sub get_storage_pools { return $self->request_api(endpoint => '/api/nutanix/v2.0/storage_pools'); } +# Retourne tous les disques physiques du cluster +sub get_disks { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/disks'); +} + +# Retourne tous les snapshots (ou ceux d'une VM spécifique via vm_uuid) +sub get_snapshots { + my ($self, %options) = @_; + if (defined($options{vm_uuid}) && $options{vm_uuid} ne '') { + return $self->request_api( + endpoint => '/api/nutanix/v2.0/snapshots', + get_param => [ 'vm_uuid=' . $options{vm_uuid} ], + ); + } + return $self->request_api(endpoint => '/api/nutanix/v2.0/snapshots'); +} + +# Retourne les NICs d'une VM spécifique +sub get_vm_nics { + my ($self, %options) = @_; + return $self->request_api( + endpoint => '/api/nutanix/v2.0/vms/' . $options{vm_uuid} . '/nics' + ); +} + 1; __END__ diff --git a/src/apps/nutanix/prism/mode/disksstatus.pm b/src/apps/nutanix/prism/mode/disksstatus.pm new file mode 100644 index 0000000000..e57b404d10 --- /dev/null +++ b/src/apps/nutanix/prism/mode/disksstatus.pm @@ -0,0 +1,252 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::disksstatus; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +# ─── output personnalisé pour le statut du disque ─────────────────────────── +sub custom_status_output { + my ($self, %options) = @_; + return sprintf( + "disk '%s' (node: %s, serial: %s) state is '%s', online: %s", + $self->{result_values}->{id}, + $self->{result_values}->{node}, + $self->{result_values}->{serial}, + $self->{result_values}->{state}, + $self->{result_values}->{online}, + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'disks', + type => 1, + cb_prefix_output => 'prefix_disk_output', + message_multiple => 'All disks are OK', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{disks} = [ + # ── Statut opérationnel ────────────────────────────────────────────── + { + label => 'status', + type => 2, + # Un disque sain a disk_status "NORMAL" et online true + critical_default => '%{state} ne "NORMAL" or %{online} ne "true"', + set => { + key_values => [ + { name => 'id' }, + { name => 'node' }, + { name => 'serial' }, + { name => 'state' }, + { name => 'online' }, + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_threshold_check => \&catalog_status_threshold_ng, + } + }, + # ── Capacité totale (octets) ───────────────────────────────────────── + { + label => 'capacity', + nlabel => 'disk.capacity.bytes', + set => { + key_values => [ { name => 'capacity_bytes' }, { name => 'id' } ], + output_template => 'capacity: %s', + output_change_bytes => 1, + perfdatas => [ + { + template => '%d', + unit => 'B', + min => 0, + label_extra_instance => 1, + instance_use => 'id', + } + ] + } + }, + # ── Espace libre (octets) ──────────────────────────────────────────── + { + label => 'free', + nlabel => 'disk.free.bytes', + set => { + key_values => [ { name => 'free_bytes' }, { name => 'id' } ], + output_template => 'free: %s', + output_change_bytes => 1, + perfdatas => [ + { + template => '%d', + unit => 'B', + min => 0, + label_extra_instance => 1, + instance_use => 'id', + } + ] + } + }, + # ── Utilisation en pourcentage ─────────────────────────────────────── + { + label => 'usage-prct', + nlabel => 'disk.usage.percentage', + set => { + key_values => [ { name => 'usage_pct' }, { name => 'id' } ], + output_template => 'usage: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + max => 100, + label_extra_instance => 1, + instance_use => 'id', + } + ] + } + }, + ]; +} + +sub prefix_disk_output { + my ($self, %options) = @_; + return "Disk '" . $options{instance_value}->{id} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-node:s' => { name => 'filter_node' }, + 'filter-id:s' => { name => 'filter_id' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_disks(); + my $entities = $result->{entities} // []; + + $self->{disks} = {}; + for my $disk (@{$entities}) { + my $id = $disk->{id} // $disk->{disk_uuid} // 'unknown'; + my $node = $disk->{node_name} // $disk->{host_name} // 'N/A'; + + if (defined($self->{option_results}->{filter_id}) && $self->{option_results}->{filter_id} ne '') { + next if $id !~ /$self->{option_results}->{filter_id}/; + } + if (defined($self->{option_results}->{filter_node}) && $self->{option_results}->{filter_node} ne '') { + next if $node !~ /$self->{option_results}->{filter_node}/; + } + + my $capacity = $disk->{disk_size} // 0; + my $free = $disk->{free_space} // 0; + # free_space peut être absent selon la version ; on calcule depuis usage si dispo + if ($free == 0 && defined($disk->{usage_stats})) { + my $used = $disk->{usage_stats}->{'storage.usage_bytes'} // 0; + $free = $capacity - $used; + } + my $pct = ($capacity > 0) ? (($capacity - $free) / $capacity * 100) : 0; + + $self->{disks}->{$id} = { + id => $id, + node => $node, + serial => $disk->{disk_hardware_config}->{serial_number} // 'N/A', + state => $disk->{disk_status} // 'UNKNOWN', + online => defined($disk->{online}) ? ($disk->{online} ? 'true' : 'false') : 'true', + capacity_bytes => $capacity, + free_bytes => ($free >= 0) ? $free : 0, + usage_pct => $pct, + }; + } + + if (scalar(keys %{$self->{disks}}) == 0) { + $self->{output}->add_option_msg(short_msg => 'No disk found (check filters).'); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix cluster physical disk status and usage through Prism REST API. + +=over 8 + +=item B<--filter-node> + +Filter disks by node/host name (regexp). Example: C<--filter-node='^NTNX-A'> + +=item B<--filter-id> + +Filter disks by disk id (regexp). Example: C<--filter-id='0c2a'> + +=item B<--warning-status> + +Warning threshold for disk state. +Variables: C<%{id}>, C<%{node}>, C<%{serial}>, C<%{state}>, C<%{online}> + +=item B<--critical-status> + +Critical threshold for disk state. +Default: C<%{state} ne "NORMAL" or %{online} ne "true"> + +=item B<--warning-usage-prct> + +Warning threshold for disk usage (%). + +=item B<--critical-usage-prct> + +Critical threshold for disk usage (%). Example: C<--critical-usage-prct=85> + +=item B<--warning-capacity> + +Warning threshold for disk capacity (bytes). + +=item B<--critical-capacity> + +Critical threshold for disk capacity (bytes). + +=item B<--warning-free> + +Warning threshold for disk free space (bytes). + +=item B<--critical-free> + +Critical threshold for disk free space (bytes). + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/snapshots.pm b/src/apps/nutanix/prism/mode/snapshots.pm new file mode 100644 index 0000000000..4719d97add --- /dev/null +++ b/src/apps/nutanix/prism/mode/snapshots.pm @@ -0,0 +1,237 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::snapshots; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use POSIX qw(floor); + +# ─── Regroupe les snapshots par VM et calcule l'âge du plus vieux ──────────── + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + # Compteurs globaux (toutes VMs confondues) + { + name => 'global', + type => 0, + skipped_code => { -10 => 1 }, + }, + # Compteurs par VM (une ligne de résultat par VM) + { + name => 'vms', + type => 1, + cb_prefix_output => 'prefix_vm_output', + message_multiple => 'All VM snapshot counts are OK', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{global} = [ + # Nombre total de snapshots sur le cluster + { + label => 'total-count', + nlabel => 'snapshots.total.count', + set => { + key_values => [ { name => 'total' } ], + output_template => 'total snapshots: %d', + perfdatas => [ + { template => '%d', min => 0 } + ] + } + }, + ]; + + $self->{maps_counters}->{vms} = [ + # Nombre de snapshots par VM + { + label => 'vm-count', + nlabel => 'vm.snapshots.count', + set => { + key_values => [ { name => 'count' }, { name => 'vm_name' } ], + output_template => 'snapshots: %d', + perfdatas => [ + { + template => '%d', + min => 0, + label_extra_instance => 1, + instance_use => 'vm_name', + } + ] + } + }, + # Âge du snapshot le plus vieux pour cette VM (en secondes) + { + label => 'oldest-age', + nlabel => 'vm.snapshot.oldest.age.seconds', + set => { + key_values => [ { name => 'oldest_age_seconds' }, { name => 'vm_name' } ], + # output_template utilise une closure pour un affichage lisible + closure_custom_output => \&custom_oldest_age_output, + perfdatas => [ + { + template => '%d', + unit => 's', + min => 0, + label_extra_instance => 1, + instance_use => 'vm_name', + } + ] + } + }, + ]; +} + +# Affiche l'âge en jours/heures plutôt qu'en secondes brutes +sub custom_oldest_age_output { + my ($self, %options) = @_; + my $age_s = $self->{result_values}->{oldest_age_seconds}; + return 'no snapshot' if !defined($age_s) || $age_s < 0; + + my $days = floor($age_s / 86400); + my $hours = floor(($age_s % 86400) / 3600); + my $mins = floor(($age_s % 3600) / 60); + + my $human = ''; + $human .= "${days}d " if $days > 0; + $human .= "${hours}h " if $hours > 0; + $human .= "${mins}m" if $mins > 0 || $days == 0 && $hours == 0; + $human = '< 1m' if $human eq ''; + + return "oldest snapshot age: $human"; +} + +sub prefix_vm_output { + my ($self, %options) = @_; + return "VM '" . $options{instance_value}->{vm_name} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-vm-name:s' => { name => 'filter_vm_name' }, + # Seuil d'âge max en heures (pratique pour les alertes métier) + 'warning-oldest-age:s' => { name => 'warning_oldest_age' }, + 'critical-oldest-age:s' => { name => 'critical_oldest_age' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + # Récupère tous les snapshots d'un coup + my $result = $options{custom}->get_snapshots(); + my $entities = $result->{entities} // []; + + # On regroupe par VM + my %by_vm; + for my $snap (@{$entities}) { + my $vm_name = $snap->{vm_name} // $snap->{vm_uuid} // 'unknown'; + + if (defined($self->{option_results}->{filter_vm_name}) && $self->{option_results}->{filter_vm_name} ne '') { + next if $vm_name !~ /$self->{option_results}->{filter_vm_name}/; + } + + push @{ $by_vm{$vm_name} }, $snap; + } + + my $total = 0; + $self->{vms} = {}; + + for my $vm_name (sort keys %by_vm) { + my @snaps = @{ $by_vm{$vm_name} }; + my $count = scalar(@snaps); + $total += $count; + + # Cherche le snapshot le plus vieux. + # created_time est en microsecondes depuis l'epoch. + my $oldest_epoch = undef; + for my $snap (@snaps) { + my $ts = $snap->{created_time}; # µs + next unless defined($ts) && $ts > 0; + $ts = int($ts / 1000000); # → secondes + $oldest_epoch = $ts if !defined($oldest_epoch) || $ts < $oldest_epoch; + } + + my $oldest_age = defined($oldest_epoch) ? (time() - $oldest_epoch) : -1; + + $self->{vms}->{$vm_name} = { + vm_name => $vm_name, + count => $count, + oldest_age_seconds => $oldest_age, + }; + } + + $self->{global} = { total => $total }; +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix VM snapshots (count and age) through Prism REST API. + +=over 8 + +=item B<--filter-vm-name> + +Filter by VM name (regexp). Example: C<--filter-vm-name='^Prod'> + +=item B<--warning-total-count> + +Warning threshold for total snapshot count across all VMs. + +=item B<--critical-total-count> + +Critical threshold for total snapshot count. + +=item B<--warning-vm-count> + +Warning threshold for snapshot count per VM. + +=item B<--critical-vm-count> + +Critical threshold for snapshot count per VM. Example: C<--critical-vm-count=10> + +=item B<--warning-oldest-age> + +Warning threshold for oldest snapshot age per VM (seconds). +Example (7 days): C<--warning-oldest-age=604800> + +=item B<--critical-oldest-age> + +Critical threshold for oldest snapshot age per VM (seconds). +Example (30 days): C<--critical-oldest-age=2592000> + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/vmsnics.pm b/src/apps/nutanix/prism/mode/vmsnics.pm new file mode 100644 index 0000000000..883c2820ed --- /dev/null +++ b/src/apps/nutanix/prism/mode/vmsnics.pm @@ -0,0 +1,259 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::vmsnics; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +# ─── Output du statut NIC ──────────────────────────────────────────────────── +sub custom_nic_status_output { + my ($self, %options) = @_; + return sprintf( + "VM '%s' NIC '%s' (MAC: %s, network: %s) is %s", + $self->{result_values}->{vm_name}, + $self->{result_values}->{nic_id}, + $self->{result_values}->{mac}, + $self->{result_values}->{network}, + $self->{result_values}->{connected}, + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'nics', + type => 1, + cb_prefix_output => 'prefix_nic_output', + message_multiple => 'All VM NICs are connected', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{nics} = [ + # ── Statut de connexion du NIC ─────────────────────────────────────── + { + label => 'status', + type => 2, + # Un NIC non connecté est en warning par défaut + warning_default => '%{connected} ne "connected"', + set => { + key_values => [ + { name => 'vm_name' }, + { name => 'nic_id' }, + { name => 'mac' }, + { name => 'network' }, + { name => 'connected' }, + ], + closure_custom_output => $self->can('custom_nic_status_output'), + closure_custom_threshold_check => \&catalog_status_threshold_ng, + } + }, + # ── Trafic entrant (octets/s) — disponible via stats de la VM ──────── + { + label => 'traffic-in', + nlabel => 'vm.nic.traffic.in.bytespersecond', + set => { + key_values => [ { name => 'rx_bytes_rate' }, { name => 'nic_id' }, { name => 'vm_name' } ], + output_template => 'traffic in: %s/s', + output_change_bytes => 1, + perfdatas => [ + { + template => '%.2f', + unit => 'B/s', + min => 0, + label_extra_instance => 1, + instance_use => 'nic_id', + } + ] + } + }, + # ── Trafic sortant (octets/s) ──────────────────────────────────────── + { + label => 'traffic-out', + nlabel => 'vm.nic.traffic.out.bytespersecond', + set => { + key_values => [ { name => 'tx_bytes_rate' }, { name => 'nic_id' }, { name => 'vm_name' } ], + output_template => 'traffic out: %s/s', + output_change_bytes => 1, + perfdatas => [ + { + template => '%.2f', + unit => 'B/s', + min => 0, + label_extra_instance => 1, + instance_use => 'nic_id', + } + ] + } + }, + ]; +} + +sub prefix_nic_output { + my ($self, %options) = @_; + return "NIC '" . $options{instance_value}->{nic_id} . "' (VM: " . $options{instance_value}->{vm_name} . ") "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-vm-name:s' => { name => 'filter_vm_name' }, + 'filter-mac:s' => { name => 'filter_mac' }, + 'filter-network:s' => { name => 'filter_network' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + # On itère sur toutes les VMs pour récupérer leurs NICs. + # L'API v2.0 expose les NICs dans la réponse de la liste des VMs + # via le champ vm_nics[] — pas besoin d'un appel par VM. + my $vms_result = $options{custom}->get_vms(); + my $vms = $vms_result->{entities} // []; + + $self->{nics} = {}; + + for my $vm (@{$vms}) { + my $vm_name = $vm->{name} // $vm->{uuid} // 'unknown'; + my $vm_uuid = $vm->{uuid} // ''; + + if (defined($self->{option_results}->{filter_vm_name}) && $self->{option_results}->{filter_vm_name} ne '') { + next if $vm_name !~ /$self->{option_results}->{filter_vm_name}/; + } + + my $nics = $vm->{vm_nics} // []; + my $stats = $vm->{stats} // {}; + + # Les stats réseau sont agrégées au niveau VM dans v2.0. + # network_received_bytes et network_transmitted_bytes sont en octets cumulés ; + # Centreon n'a pas d'état persistant ici, on utilise les valeurs "rate" si dispo. + # Si absent, on met 0 (non disponible). + my $rx_rate = $stats->{'nic.received_bytes_rate'} // 0; + my $tx_rate = $stats->{'nic.transmitted_bytes_rate'} // 0; + + my $nic_index = 0; + for my $nic (@{$nics}) { + my $mac = $nic->{mac_address} // 'unknown'; + my $network = $nic->{network_name} // $nic->{vlan_id} // 'N/A'; + my $nic_id = $vm_name . '_nic' . $nic_index; + + if (defined($self->{option_results}->{filter_mac}) && $self->{option_results}->{filter_mac} ne '') { + $nic_index++; + next if $mac !~ /$self->{option_results}->{filter_mac}/i; + } + if (defined($self->{option_results}->{filter_network}) && $self->{option_results}->{filter_network} ne '') { + $nic_index++; + next if $network !~ /$self->{option_results}->{filter_network}/; + } + + # is_connected est un booléen dans l'API Nutanix v2.0 + my $connected = (defined($nic->{is_connected}) && $nic->{is_connected}) ? 'connected' : 'disconnected'; + + $self->{nics}->{$nic_id} = { + vm_name => $vm_name, + nic_id => $nic_id, + mac => $mac, + network => $network, + connected => $connected, + # Les rates réseau ne sont pas par NIC dans v2.0 — on les attribue + # au premier NIC de la VM (index 0). Les autres NIC ont 0. + rx_bytes_rate => ($nic_index == 0) ? $rx_rate : 0, + tx_bytes_rate => ($nic_index == 0) ? $tx_rate : 0, + }; + + $nic_index++; + } + } + + if (scalar(keys %{$self->{nics}}) == 0) { + $self->{output}->add_option_msg(short_msg => 'No NIC found (check filters).'); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix VM NIC connectivity and traffic through Prism REST API. + +Note: In Prism API v2.0, network traffic stats are aggregated at VM level, +not per NIC. Traffic counters are attributed to the first NIC (index 0) of +each VM. For per-NIC traffic, use Prism Central API v3 with metric queries. + +=over 8 + +=item B<--filter-vm-name> + +Filter by VM name (regexp). Example: C<--filter-vm-name='^Prod'> + +=item B<--filter-mac> + +Filter NICs by MAC address (regexp, case-insensitive). Example: C<--filter-mac='^50:6b'> + +=item B<--filter-network> + +Filter NICs by network/VLAN name (regexp). Example: C<--filter-network='Production'> + +=item B<--warning-status> + +Warning threshold for NIC connection status. +Default: C<%{connected} ne "connected"> + +Variables: C<%{vm_name}>, C<%{nic_id}>, C<%{mac}>, C<%{network}>, C<%{connected}> + +=item B<--critical-status> + +Critical threshold for NIC connection status. + +=item B<--warning-traffic-in> + +Warning threshold for inbound traffic (B/s). + +=item B<--critical-traffic-in> + +Critical threshold for inbound traffic (B/s). + +=item B<--warning-traffic-out> + +Warning threshold for outbound traffic (B/s). + +=item B<--critical-traffic-out> + +Critical threshold for outbound traffic (B/s). + +=back + +=cut diff --git a/src/apps/nutanix/prism/plugin.pm b/src/apps/nutanix/prism/plugin.pm index ff81e51bc7..f59ac9fc95 100644 --- a/src/apps/nutanix/prism/plugin.pm +++ b/src/apps/nutanix/prism/plugin.pm @@ -32,9 +32,12 @@ sub new { $self->{version} = '0.1'; $self->{modes} = { 'cluster-status' => 'apps::nutanix::prism::mode::clusterstatus', + 'disks-status' => 'apps::nutanix::prism::mode::disksstatus', 'hosts-usage' => 'apps::nutanix::prism::mode::hostsusage', + 'snapshots' => 'apps::nutanix::prism::mode::snapshots', 'storage-usage' => 'apps::nutanix::prism::mode::storageusage', 'vms-count' => 'apps::nutanix::prism::mode::vmscount', + 'vms-nics' => 'apps::nutanix::prism::mode::vmsnics', 'list-hosts' => 'apps::nutanix::prism::mode::listhosts', 'list-vms' => 'apps::nutanix::prism::mode::listvms', }; From e3e464ceb4572a8de4747bad92aac0d39938be72 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 14:33:14 +0000 Subject: [PATCH 03/26] feat(nutanix): add alerts, capacity, health-checks modes and list-nics discovery New monitoring modes: - alerts: active alerts count by severity (critical/warning/info) with per-alert status check, age display and filters on severity/title/entity - capacity: cluster-wide CPU (cores, usage%), memory (used/total/%) and storage (used/free/total/%) aggregated from hosts and storage pools - health-checks: NCC health check results (pass/fail/warning/error counts) with per-check status and --only-failing option to reduce noise New discovery mode: - list-nics: VM NIC discovery with nic_id, mac, network, ip, connected fields; data extracted from vm_nics[] in the VM list (no extra API call) Also adds get_alerts() and get_health_checks() methods to custom/api.pm. https://claude.ai/code/session_01PXMdAKHbDqnPokz5BHWAQp --- src/apps/nutanix/prism/custom/api.pm | 17 ++ src/apps/nutanix/prism/mode/alerts.pm | 289 ++++++++++++++++++++ src/apps/nutanix/prism/mode/capacity.pm | 280 +++++++++++++++++++ src/apps/nutanix/prism/mode/healthchecks.pm | 270 ++++++++++++++++++ src/apps/nutanix/prism/mode/listnics.pm | 162 +++++++++++ src/apps/nutanix/prism/plugin.pm | 4 + 6 files changed, 1022 insertions(+) create mode 100644 src/apps/nutanix/prism/mode/alerts.pm create mode 100644 src/apps/nutanix/prism/mode/capacity.pm create mode 100644 src/apps/nutanix/prism/mode/healthchecks.pm create mode 100644 src/apps/nutanix/prism/mode/listnics.pm diff --git a/src/apps/nutanix/prism/custom/api.pm b/src/apps/nutanix/prism/custom/api.pm index 326b5f0b12..f60c930703 100644 --- a/src/apps/nutanix/prism/custom/api.pm +++ b/src/apps/nutanix/prism/custom/api.pm @@ -200,6 +200,23 @@ sub get_vm_nics { ); } +# Retourne les alertes actives (non résolues par défaut) +sub get_alerts { + my ($self, %options) = @_; + my @params = ('resolved=false'); + push @params, 'severity=' . $options{severity} if defined($options{severity}); + return $self->request_api( + endpoint => '/api/nutanix/v2.0/alerts', + get_param => \@params, + ); +} + +# Retourne les résultats des health checks NCC +sub get_health_checks { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/health_checks'); +} + 1; __END__ diff --git a/src/apps/nutanix/prism/mode/alerts.pm b/src/apps/nutanix/prism/mode/alerts.pm new file mode 100644 index 0000000000..89a2f7ac06 --- /dev/null +++ b/src/apps/nutanix/prism/mode/alerts.pm @@ -0,0 +1,289 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::alerts; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); +use POSIX qw(floor); + +# ─── Correspondance des sévérités Nutanix → niveau Centreon ────────────────── +# L'API Prism v2.0 retourne kCritical, kWarning, kInfo +my %SEVERITY_MAP = ( + kCritical => 'critical', + kWarning => 'warning', + kInfo => 'info', +); + +sub custom_alert_output { + my ($self, %options) = @_; + + my $age_s = $self->{result_values}->{age_seconds}; + my $days = floor($age_s / 86400); + my $hours = floor(($age_s % 86400) / 3600); + my $mins = floor(($age_s % 3600) / 60); + my $age_str = ''; + $age_str .= "${days}d " if $days > 0; + $age_str .= "${hours}h " if $hours > 0; + $age_str .= "${mins}m" if $mins > 0 || ($days == 0 && $hours == 0); + $age_str = '< 1m' if $age_str eq ''; + + return sprintf( + "alert [severity: %s] [title: %s] [entity: %s] raised %s ago", + $self->{result_values}->{severity}, + $self->{result_values}->{title}, + $self->{result_values}->{entity}, + $age_str, + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + # Compteurs globaux : nombre d'alertes par sévérité + { name => 'global', type => 0 }, + # Compteur par alerte : statut individuel (pour long output) + { + name => 'alerts', + type => 1, + cb_prefix_output => 'prefix_alert_output', + message_multiple => 'No active alerts', + skipped_code => { -10 => 1 }, + }, + ]; + + $self->{maps_counters}->{global} = [ + { + label => 'alerts-critical', + nlabel => 'alerts.severity.critical.count', + set => { + key_values => [ { name => 'critical' } ], + output_template => 'critical alerts: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + { + label => 'alerts-warning', + nlabel => 'alerts.severity.warning.count', + set => { + key_values => [ { name => 'warning' } ], + output_template => 'warning alerts: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + { + label => 'alerts-info', + nlabel => 'alerts.severity.info.count', + set => { + key_values => [ { name => 'info' } ], + output_template => 'info alerts: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + { + label => 'alerts-total', + nlabel => 'alerts.total.count', + set => { + key_values => [ { name => 'total' } ], + output_template => 'total alerts: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + ]; + + $self->{maps_counters}->{alerts} = [ + { + label => 'alert-status', + type => 2, + # Par défaut : chaque alerte critique déclenche CRITICAL, warning → WARNING + warning_default => '%{severity} eq "warning"', + critical_default => '%{severity} eq "critical"', + set => { + key_values => [ + { name => 'id' }, + { name => 'severity' }, + { name => 'title' }, + { name => 'entity' }, + { name => 'age_seconds' }, + ], + closure_custom_output => $self->can('custom_alert_output'), + closure_custom_threshold_check => \&catalog_status_threshold_ng, + } + }, + ]; +} + +sub prefix_alert_output { + my ($self, %options) = @_; + return "Alert '" . $options{instance_value}->{id} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-severity:s' => { name => 'filter_severity' }, + 'filter-title:s' => { name => 'filter_title' }, + 'filter-entity:s' => { name => 'filter_entity' }, + # Âge minimum en secondes pour ignorer les alertes trop récentes + 'min-age:s' => { name => 'min_age', default => 0 }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_alerts(); + my $entities = $result->{entities} // []; + + $self->{global} = { critical => 0, warning => 0, info => 0, total => 0 }; + $self->{alerts} = {}; + + my $now = time(); + + for my $alert (@{$entities}) { + # Sévérité normalisée (kCritical → critical) + my $raw_sev = $alert->{severity} // 'kInfo'; + my $severity = $SEVERITY_MAP{$raw_sev} // 'info'; + + # Titre : reconstruit depuis alert_title ou message + my $title = $alert->{alert_title} // $alert->{message} // $alert->{check_id} // 'N/A'; + + # Entité affectée : context_types[i] = "vm_name" → context_values[i] + my $entity = 'cluster'; + my $types = $alert->{context_types} // []; + my $values = $alert->{context_values} // []; + for my $i (0 .. $#{$types}) { + if ($types->[$i] =~ /^(vm_name|host_name|storage_pool_name|disk_id)$/i) { + $entity = $values->[$i] // $entity; + last; + } + } + + # Âge en secondes (created_time_stamp_in_usecs est en microsecondes) + my $created_usec = $alert->{created_time_stamp_in_usecs} // 0; + my $age_s = ($created_usec > 0) ? ($now - int($created_usec / 1_000_000)) : 0; + $age_s = 0 if $age_s < 0; + + my $id = $alert->{id} // $alert->{alert_type_uuid} // "$severity-$title"; + + # Filtrage + next if defined($self->{option_results}->{filter_severity}) + && $self->{option_results}->{filter_severity} ne '' + && $severity !~ /$self->{option_results}->{filter_severity}/i; + next if defined($self->{option_results}->{filter_title}) + && $self->{option_results}->{filter_title} ne '' + && $title !~ /$self->{option_results}->{filter_title}/i; + next if defined($self->{option_results}->{filter_entity}) + && $self->{option_results}->{filter_entity} ne '' + && $entity !~ /$self->{option_results}->{filter_entity}/i; + next if $age_s < $self->{option_results}->{min_age}; + + $self->{global}->{$severity}++; + $self->{global}->{total}++; + + $self->{alerts}->{$id} = { + id => $id, + severity => $severity, + title => $title, + entity => $entity, + age_seconds => $age_s, + }; + } +} + +1; + +__END__ + +=head1 MODE + +Monitor active Nutanix alerts through Prism REST API. + +Only unresolved alerts are fetched (C). + +=over 8 + +=item B<--filter-severity> + +Filter alerts by severity (regexp, case-insensitive). +Values: C, C, C. +Example: C<--filter-severity='critical|warning'> + +=item B<--filter-title> + +Filter alerts by title (regexp, case-insensitive). + +=item B<--filter-entity> + +Filter alerts by affected entity name (regexp). + +=item B<--min-age> + +Ignore alerts younger than this value in seconds (default: 0). +Example: C<--min-age=300> to skip alerts raised less than 5 minutes ago. + +=item B<--warning-alerts-critical> + +Warning threshold for count of critical-severity alerts. + +=item B<--critical-alerts-critical> + +Critical threshold. Example: C<--critical-alerts-critical=1> + +=item B<--warning-alerts-warning> + +Warning threshold for count of warning-severity alerts. + +=item B<--critical-alerts-warning> + +Critical threshold for warning-severity alert count. + +=item B<--warning-alerts-total> + +Warning threshold for total active alert count. + +=item B<--critical-alerts-total> + +Critical threshold for total active alert count. + +=item B<--warning-alert-status> + +Warning condition per alert (Perl expression). +Default: C<%{severity} eq "warning"> +Variables: C<%{id}>, C<%{severity}>, C<%{title}>, C<%{entity}>, C<%{age_seconds}> + +=item B<--critical-alert-status> + +Critical condition per alert. +Default: C<%{severity} eq "critical"> + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/capacity.pm b/src/apps/nutanix/prism/mode/capacity.pm new file mode 100644 index 0000000000..92aa78cbf1 --- /dev/null +++ b/src/apps/nutanix/prism/mode/capacity.pm @@ -0,0 +1,280 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::capacity; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); + +# Ce mode agrège la capacité CPU, RAM et stockage à l'échelle du cluster +# en consolidant les données des hôtes et des storage pools. + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'cpu', type => 0, message_separator => ' - ' }, + { name => 'memory', type => 0, message_separator => ' - ' }, + { name => 'storage', type => 0, message_separator => ' - ' }, + ]; + + # ── CPU (vCPU alloués vs capacité physique) ─────────────────────────────── + $self->{maps_counters}->{cpu} = [ + { + label => 'cpu-capacity', + nlabel => 'cluster.cpu.capacity.count', + set => { + key_values => [ { name => 'total_cores' } ], + output_template => 'CPU capacity: %d physical cores', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + { + label => 'cpu-allocated', + nlabel => 'cluster.cpu.allocated.count', + set => { + key_values => [ { name => 'allocated_vcpus' } ], + output_template => 'vCPUs allocated: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + { + label => 'cpu-usage-prct', + nlabel => 'cluster.cpu.usage.percentage', + set => { + key_values => [ { name => 'cpu_usage_pct' } ], + output_template => 'CPU usage: %.2f%%', + perfdatas => [ + { template => '%.2f', unit => '%', min => 0, max => 100 } + ], + } + }, + ]; + + # ── Mémoire (octets) ────────────────────────────────────────────────────── + $self->{maps_counters}->{memory} = [ + { + label => 'memory-capacity', + nlabel => 'cluster.memory.capacity.bytes', + set => { + key_values => [ { name => 'memory_total_bytes' } ], + output_template => 'memory capacity: %s', + output_change_bytes => 1, + perfdatas => [ { template => '%d', unit => 'B', min => 0 } ], + } + }, + { + label => 'memory-used', + nlabel => 'cluster.memory.used.bytes', + set => { + key_values => [ { name => 'memory_used_bytes' } ], + output_template => 'memory used: %s', + output_change_bytes => 1, + perfdatas => [ { template => '%d', unit => 'B', min => 0 } ], + } + }, + { + label => 'memory-usage-prct', + nlabel => 'cluster.memory.usage.percentage', + set => { + key_values => [ { name => 'memory_usage_pct' } ], + output_template => 'memory usage: %.2f%%', + perfdatas => [ + { template => '%.2f', unit => '%', min => 0, max => 100 } + ], + } + }, + ]; + + # ── Stockage (octets) ───────────────────────────────────────────────────── + $self->{maps_counters}->{storage} = [ + { + label => 'storage-capacity', + nlabel => 'cluster.storage.capacity.bytes', + set => { + key_values => [ { name => 'storage_total_bytes' } ], + output_template => 'storage capacity: %s', + output_change_bytes => 1, + perfdatas => [ { template => '%d', unit => 'B', min => 0 } ], + } + }, + { + label => 'storage-used', + nlabel => 'cluster.storage.used.bytes', + set => { + key_values => [ { name => 'storage_used_bytes' } ], + output_template => 'storage used: %s', + output_change_bytes => 1, + perfdatas => [ { template => '%d', unit => 'B', min => 0 } ], + } + }, + { + label => 'storage-usage-prct', + nlabel => 'cluster.storage.usage.percentage', + set => { + key_values => [ { name => 'storage_usage_pct' } ], + output_template => 'storage usage: %.2f%%', + perfdatas => [ + { template => '%.2f', unit => '%', min => 0, max => 100 } + ], + } + }, + { + label => 'storage-free', + nlabel => 'cluster.storage.free.bytes', + set => { + key_values => [ { name => 'storage_free_bytes' } ], + output_template => 'storage free: %s', + output_change_bytes => 1, + perfdatas => [ { template => '%d', unit => 'B', min => 0 } ], + } + }, + ]; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + # ── Données CPU et RAM : agrégation depuis les hôtes ───────────────────── + my $hosts_result = $options{custom}->get_hosts(); + my $hosts = $hosts_result->{entities} // []; + + my ($total_cores, $allocated_vcpus) = (0, 0); + my ($mem_total, $mem_used_sum, $cpu_usage_sum) = (0, 0, 0); + my $host_count = scalar(@{$hosts}); + + for my $host (@{$hosts}) { + # num_cpu_cores = cœurs physiques par hôte + $total_cores += $host->{num_cpu_cores} // 0; + # num_vms est un proxy de l'allocation vCPU (on utilisera num_cpu_threads si dispo) + $allocated_vcpus += $host->{num_cpu_threads} // 0; + + # Mémoire : memory_capacity_in_bytes + $mem_total += $host->{memory_capacity_in_bytes} // 0; + + # CPU usage en PPM → % + my $stats = $host->{stats} // {}; + my $cpu_ppm = $stats->{hypervisor_cpu_usage_ppm} // 0; + $cpu_usage_sum += $cpu_ppm / 10000; + + # RAM used : memory_size_bytes (capacité) - memory_usage_ppm + my $mem_ppm = $stats->{hypervisor_memory_usage_ppm} // 0; + $mem_used_sum += ($host->{memory_capacity_in_bytes} // 0) * ($mem_ppm / 1_000_000); + } + + my $cpu_usage_avg = ($host_count > 0) ? ($cpu_usage_sum / $host_count) : 0; + my $mem_usage_pct = ($mem_total > 0) ? ($mem_used_sum / $mem_total * 100) : 0; + + $self->{cpu} = { + total_cores => $total_cores, + allocated_vcpus => $allocated_vcpus, + cpu_usage_pct => $cpu_usage_avg, + }; + $self->{memory} = { + memory_total_bytes => $mem_total, + memory_used_bytes => $mem_used_sum, + memory_usage_pct => $mem_usage_pct, + }; + + # ── Données stockage : agrégation depuis les storage pools ─────────────── + my $pools_result = $options{custom}->get_storage_pools(); + my $pools = $pools_result->{entities} // []; + + my ($storage_total, $storage_used) = (0, 0); + for my $pool (@{$pools}) { + $storage_total += $pool->{capacity_bytes} // 0; + $storage_used += $pool->{usage_bytes} // 0; + } + my $storage_free = $storage_total - $storage_used; + $storage_free = 0 if $storage_free < 0; + my $storage_pct = ($storage_total > 0) ? ($storage_used / $storage_total * 100) : 0; + + $self->{storage} = { + storage_total_bytes => $storage_total, + storage_used_bytes => $storage_used, + storage_free_bytes => $storage_free, + storage_usage_pct => $storage_pct, + }; +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix cluster capacity (CPU, memory, storage) through Prism REST API. + +Data is aggregated from all AHV hosts (CPU/RAM) and all storage pools (storage). +Two API calls are made: C and C. + +=over 8 + +=item B<--warning-cpu-usage-prct> + +Warning threshold for cluster-wide CPU usage (%). + +=item B<--critical-cpu-usage-prct> + +Critical threshold for cluster-wide CPU usage (%). Example: C<--critical-cpu-usage-prct=85> + +=item B<--warning-memory-usage-prct> + +Warning threshold for memory usage (%). + +=item B<--critical-memory-usage-prct> + +Critical threshold for memory usage (%). Example: C<--critical-memory-usage-prct=90> + +=item B<--warning-storage-usage-prct> + +Warning threshold for storage usage (%). + +=item B<--critical-storage-usage-prct> + +Critical threshold for storage usage (%). Example: C<--critical-storage-usage-prct=85> + +=item B<--warning-storage-free> + +Warning threshold for free storage space (bytes). + +=item B<--critical-storage-free> + +Critical threshold for free storage space (bytes). + +=item B<--warning-cpu-capacity> + +Warning threshold for total physical core count. + +=item B<--critical-cpu-capacity> + +Critical threshold for total physical core count. + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/healthchecks.pm b/src/apps/nutanix/prism/mode/healthchecks.pm new file mode 100644 index 0000000000..52c91d246d --- /dev/null +++ b/src/apps/nutanix/prism/mode/healthchecks.pm @@ -0,0 +1,270 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::healthchecks; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +# Les health checks NCC de Nutanix ont un champ execution_results[] qui contient +# les résultats par nœud. Le statut global se lit dans health_check_series.state : +# PASS, FAIL, WARNING, INFO, ERROR, SCHEDULED, RUNNING, ABORTED + +my %STATE_SEVERITY = ( + FAIL => 'critical', + ERROR => 'critical', + WARNING => 'warning', + PASS => 'ok', + INFO => 'info', +); + +sub custom_check_output { + my ($self, %options) = @_; + return sprintf( + "health check '%s' [category: %s] state is '%s' — %s", + $self->{result_values}->{name}, + $self->{result_values}->{category}, + $self->{result_values}->{state}, + $self->{result_values}->{message}, + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + # Résumé global (comptages par résultat) + { name => 'global', type => 0 }, + # Résultat individuel par health check + { + name => 'checks', + type => 1, + cb_prefix_output => 'prefix_check_output', + message_multiple => 'All health checks passed', + skipped_code => { -10 => 1 }, + }, + ]; + + $self->{maps_counters}->{global} = [ + { + label => 'checks-pass', + nlabel => 'healthchecks.pass.count', + set => { + key_values => [ { name => 'pass' } ], + output_template => 'pass: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + { + label => 'checks-fail', + nlabel => 'healthchecks.fail.count', + set => { + key_values => [ { name => 'fail' } ], + output_template => 'fail: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + { + label => 'checks-warning', + nlabel => 'healthchecks.warning.count', + set => { + key_values => [ { name => 'warning' } ], + output_template => 'warning: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + { + label => 'checks-error', + nlabel => 'healthchecks.error.count', + set => { + key_values => [ { name => 'error' } ], + output_template => 'error: %d', + perfdatas => [ { template => '%d', min => 0 } ], + } + }, + ]; + + $self->{maps_counters}->{checks} = [ + { + label => 'check-status', + type => 2, + warning_default => '%{state} eq "WARNING"', + critical_default => '%{state} =~ /^(FAIL|ERROR)$/', + set => { + key_values => [ + { name => 'name' }, + { name => 'category' }, + { name => 'state' }, + { name => 'message' }, + ], + closure_custom_output => $self->can('custom_check_output'), + closure_custom_threshold_check => \&catalog_status_threshold_ng, + } + }, + ]; +} + +sub prefix_check_output { + my ($self, %options) = @_; + return "Check '" . $options{instance_value}->{name} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + 'filter-category:s' => { name => 'filter_category' }, + # N'affiche que les checks non-PASS (utile pour réduire le bruit) + 'only-failing' => { name => 'only_failing' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_health_checks(); + my $entities = $result->{entities} // []; + + $self->{global} = { pass => 0, fail => 0, warning => 0, error => 0 }; + $self->{checks} = {}; + + for my $check (@{$entities}) { + my $name = $check->{name} // $check->{check_id} // 'unknown'; + my $category = $check->{category} // 'N/A'; + + # Filtrage + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/i; + } + if (defined($self->{option_results}->{filter_category}) && $self->{option_results}->{filter_category} ne '') { + next if $category !~ /$self->{option_results}->{filter_category}/i; + } + + # Le statut global du check est dans health_check_series[].state + # ou directement dans state (selon la version de Prism) + my $state = 'UNKNOWN'; + if (defined($check->{health_check_series}) && ref($check->{health_check_series}) eq 'ARRAY' + && @{$check->{health_check_series}}) { + $state = uc($check->{health_check_series}->[-1]->{state} // 'UNKNOWN'); + } elsif (defined($check->{state})) { + $state = uc($check->{state}); + } + + next if defined($self->{option_results}->{only_failing}) && $state eq 'PASS'; + + # Message de détail (causes + resolutions concaténés) + my $message = $check->{message} // ''; + if (!$message && defined($check->{health_check_series}) && @{$check->{health_check_series}}) { + my $last = $check->{health_check_series}->[-1]; + my $causes = join('; ', map { $_->{message} // '' } @{ $last->{execution_results} // [] }); + $message = $causes if $causes ne ''; + } + $message = 'no detail' if $message eq ''; + + # Comptage global (FAIL, ERROR, WARNING, PASS) + my $bucket = lc($state); + $bucket = 'fail' if $bucket eq 'error'; # ERROR → même bucket que FAIL pour les seuils globaux + $self->{global}->{$bucket}++ if exists $self->{global}->{$bucket}; + + $self->{checks}->{$name} = { + name => $name, + category => $category, + state => $state, + message => $message, + }; + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix NCC (Nutanix Cluster Check) health check results through Prism REST API. + +Each health check reports a state: C, C, C, C, +C, C, C or C. + +=over 8 + +=item B<--filter-name> + +Filter health checks by name (regexp, case-insensitive). +Example: C<--filter-name='disk|cvm'> + +=item B<--filter-category> + +Filter health checks by category (regexp, case-insensitive). +Example: C<--filter-category='Hardware'> + +=item B<--only-failing> + +Only report health checks that are not in PASS state. +Useful to reduce noise on large clusters. + +=item B<--warning-check-status> + +Warning condition per check (Perl expression). +Default: C<%{state} eq "WARNING"> + +Variables: C<%{name}>, C<%{category}>, C<%{state}>, C<%{message}> + +=item B<--critical-check-status> + +Critical condition per check. +Default: C<%{state} =~ /^(FAIL|ERROR)$/> + +=item B<--warning-checks-fail> + +Warning threshold for count of FAIL checks. + +=item B<--critical-checks-fail> + +Critical threshold for count of FAIL checks. Example: C<--critical-checks-fail=1> + +=item B<--warning-checks-warning> + +Warning threshold for count of WARNING checks. + +=item B<--critical-checks-warning> + +Critical threshold for count of WARNING checks. + +=item B<--warning-checks-error> + +Warning threshold for count of ERROR checks. + +=item B<--critical-checks-error> + +Critical threshold for count of ERROR checks. + +=back + +=cut diff --git a/src/apps/nutanix/prism/mode/listnics.pm b/src/apps/nutanix/prism/mode/listnics.pm new file mode 100644 index 0000000000..fe708b02b4 --- /dev/null +++ b/src/apps/nutanix/prism/mode/listnics.pm @@ -0,0 +1,162 @@ +# +# Copyright 2025 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::listnics; + +use strict; +use warnings; +use base qw(centreon::plugins::mode); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-vm-name:s' => { name => 'filter_vm_name' }, + 'filter-network:s' => { name => 'filter_network' }, + } + ); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); +} + +# Collecte tous les NICs depuis la liste des VMs (vm_nics[] est inclus dans v2.0) +sub _collect_nics { + my ($self, %options) = @_; + + my $vms = $options{custom}->get_vms(); + my $entities = $vms->{entities} // []; + my @nics; + + for my $vm (@{$entities}) { + my $vm_name = $vm->{name} // $vm->{uuid} // 'unknown'; + my $vm_uuid = $vm->{uuid} // ''; + + if (defined($self->{option_results}->{filter_vm_name}) && $self->{option_results}->{filter_vm_name} ne '') { + next if $vm_name !~ /$self->{option_results}->{filter_vm_name}/; + } + + my $nic_index = 0; + for my $nic (@{ $vm->{vm_nics} // [] }) { + my $network = $nic->{network_name} // $nic->{vlan_id} // 'N/A'; + + if (defined($self->{option_results}->{filter_network}) && $self->{option_results}->{filter_network} ne '') { + $nic_index++; + next if $network !~ /$self->{option_results}->{filter_network}/; + } + + push @nics, { + vm_name => $vm_name, + vm_uuid => $vm_uuid, + nic_index => $nic_index, + nic_id => $vm_name . '_nic' . $nic_index, + mac => $nic->{mac_address} // 'N/A', + network => $network, + connected => (defined($nic->{is_connected}) && $nic->{is_connected}) ? 'true' : 'false', + ip => (defined($nic->{ip_address}) && $nic->{ip_address} ne '') ? $nic->{ip_address} : 'N/A', + }; + $nic_index++; + } + } + + return \@nics; +} + +sub run { + my ($self, %options) = @_; + + my $nics = $self->_collect_nics(%options); + + for my $nic (@{$nics}) { + $self->{output}->output_add( + long_msg => sprintf( + " vm: %-30s nic_id: %-20s mac: %-20s network: %-20s ip: %-16s connected: %s", + $nic->{vm_name}, + $nic->{nic_id}, + $nic->{mac}, + $nic->{network}, + $nic->{ip}, + $nic->{connected}, + ) + ); + } + + $self->{output}->output_add(severity => 'OK', short_msg => 'List of Nutanix VM NICs:'); + $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 => ['nic_id', 'vm_name', 'vm_uuid', 'nic_index', 'mac', 'network', 'ip', 'connected'] + ); +} + +sub disco_show { + my ($self, %options) = @_; + + my $nics = $self->_collect_nics(%options); + + for my $nic (@{$nics}) { + $self->{output}->add_disco_entry( + nic_id => $nic->{nic_id}, + vm_name => $nic->{vm_name}, + vm_uuid => $nic->{vm_uuid}, + nic_index => $nic->{nic_index}, + mac => $nic->{mac}, + network => $nic->{network}, + ip => $nic->{ip}, + connected => $nic->{connected}, + ); + } +} + +1; + +__END__ + +=head1 MODE + +List Nutanix VM NICs for service discovery. + +NIC data is extracted from the VM list endpoint (vm_nics[] field) — no extra +API call per VM is needed. + +=over 8 + +=item B<--filter-vm-name> + +Filter by VM name (regexp). Example: C<--filter-vm-name='^Prod'> + +=item B<--filter-network> + +Filter by network/VLAN name (regexp). Example: C<--filter-network='Production'> + +=back + +=cut diff --git a/src/apps/nutanix/prism/plugin.pm b/src/apps/nutanix/prism/plugin.pm index f59ac9fc95..e423689f24 100644 --- a/src/apps/nutanix/prism/plugin.pm +++ b/src/apps/nutanix/prism/plugin.pm @@ -31,14 +31,18 @@ sub new { $self->{version} = '0.1'; $self->{modes} = { + 'alerts' => 'apps::nutanix::prism::mode::alerts', + 'capacity' => 'apps::nutanix::prism::mode::capacity', 'cluster-status' => 'apps::nutanix::prism::mode::clusterstatus', 'disks-status' => 'apps::nutanix::prism::mode::disksstatus', + 'health-checks' => 'apps::nutanix::prism::mode::healthchecks', 'hosts-usage' => 'apps::nutanix::prism::mode::hostsusage', 'snapshots' => 'apps::nutanix::prism::mode::snapshots', 'storage-usage' => 'apps::nutanix::prism::mode::storageusage', 'vms-count' => 'apps::nutanix::prism::mode::vmscount', 'vms-nics' => 'apps::nutanix::prism::mode::vmsnics', 'list-hosts' => 'apps::nutanix::prism::mode::listhosts', + 'list-nics' => 'apps::nutanix::prism::mode::listnics', 'list-vms' => 'apps::nutanix::prism::mode::listvms', }; From d410ba46378b7cc9721abc2ba8a902a469d87984 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:57:30 +0200 Subject: [PATCH 04/26] Update plugin.pm --- src/apps/nutanix/prism/plugin.pm | 34 +++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/apps/nutanix/prism/plugin.pm b/src/apps/nutanix/prism/plugin.pm index e423689f24..aff71421b5 100644 --- a/src/apps/nutanix/prism/plugin.pm +++ b/src/apps/nutanix/prism/plugin.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -31,19 +31,25 @@ sub new { $self->{version} = '0.1'; $self->{modes} = { - 'alerts' => 'apps::nutanix::prism::mode::alerts', - 'capacity' => 'apps::nutanix::prism::mode::capacity', - 'cluster-status' => 'apps::nutanix::prism::mode::clusterstatus', - 'disks-status' => 'apps::nutanix::prism::mode::disksstatus', - 'health-checks' => 'apps::nutanix::prism::mode::healthchecks', - 'hosts-usage' => 'apps::nutanix::prism::mode::hostsusage', - 'snapshots' => 'apps::nutanix::prism::mode::snapshots', - 'storage-usage' => 'apps::nutanix::prism::mode::storageusage', - 'vms-count' => 'apps::nutanix::prism::mode::vmscount', - 'vms-nics' => 'apps::nutanix::prism::mode::vmsnics', - 'list-hosts' => 'apps::nutanix::prism::mode::listhosts', - 'list-nics' => 'apps::nutanix::prism::mode::listnics', - 'list-vms' => 'apps::nutanix::prism::mode::listvms', + 'alerts' => 'apps::nutanix::prism::mode::alerts', + 'capacity' => 'apps::nutanix::prism::mode::capacity', + 'cluster-status' => 'apps::nutanix::prism::mode::clusterstatus', + 'disks-status' => 'apps::nutanix::prism::mode::disksstatus', + 'health-checks' => 'apps::nutanix::prism::mode::healthchecks', + 'hosts-usage' => 'apps::nutanix::prism::mode::hostsusage', + 'snapshots' => 'apps::nutanix::prism::mode::snapshots', + 'storage-usage' => 'apps::nutanix::prism::mode::storageusage', + 'vms-count' => 'apps::nutanix::prism::mode::vmscount', + 'vms-nics' => 'apps::nutanix::prism::mode::vmsnics', + 'list-hosts' => 'apps::nutanix::prism::mode::listhosts', + 'list-nics' => 'apps::nutanix::prism::mode::listnics', + 'list-vms' => 'apps::nutanix::prism::mode::listvms', + 'vms-performance' => 'apps::nutanix::prism::mode::vmsperformance', + 'protection-domains' => 'apps::nutanix::prism::mode::protectiondomains', + 'storage-containers' => 'apps::nutanix::prism::mode::storagecontainers', + 'tasks' => 'apps::nutanix::prism::mode::tasks', + 'list-protection-domains' => 'apps::nutanix::prism::mode::listprotectiondomains', + 'list-storage-containers' => 'apps::nutanix::prism::mode::liststoragecontainers', }; $self->{custom_modes}->{api} = 'apps::nutanix::prism::custom::api'; From c89c0524d014719d63e565b9c3e868120c353233 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:59:59 +0200 Subject: [PATCH 05/26] Update api.pm --- src/apps/nutanix/prism/custom/api.pm | 45 ++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/apps/nutanix/prism/custom/api.pm b/src/apps/nutanix/prism/custom/api.pm index f60c930703..e615c62d47 100644 --- a/src/apps/nutanix/prism/custom/api.pm +++ b/src/apps/nutanix/prism/custom/api.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -100,7 +100,7 @@ sub _get_auth_header { sub request_api { my ($self, %options) = @_; - # Prism utilise le port 9440 par défaut et un préfixe /api/nutanix/v2.0 + # Prism REST API base: port 9440, prefix /api/nutanix/v2.0 my $url = $self->{proto} . '://' . $self->{hostname} . ':' . $self->{port}; $self->{option_results}->{hostname} = $self->{hostname}; @@ -126,7 +126,7 @@ sub request_api { header => \@headers, get_param => $options{get_param}, query_form_post => $options{query_form_post}, - insecure => 1, # Les déploiements Nutanix utilisent souvent des certs auto-signés + insecure => 1, # Nutanix deployments commonly use self-signed certificates ); if (!defined($content) || $content eq '') { @@ -150,37 +150,37 @@ sub request_api { return $decoded; } -# Retourne les infos du/des clusters Nutanix +# Returns cluster information sub get_clusters { my ($self, %options) = @_; return $self->request_api(endpoint => '/api/nutanix/v2.0/clusters'); } -# Retourne la liste des hôtes physiques +# Returns the list of physical hosts sub get_hosts { my ($self, %options) = @_; return $self->request_api(endpoint => '/api/nutanix/v2.0/hosts'); } -# Retourne la liste des VMs +# Returns the list of virtual machines (includes stats fields) sub get_vms { my ($self, %options) = @_; return $self->request_api(endpoint => '/api/nutanix/v2.0/vms'); } -# Retourne les pools de stockage +# Returns storage pools sub get_storage_pools { my ($self, %options) = @_; return $self->request_api(endpoint => '/api/nutanix/v2.0/storage_pools'); } -# Retourne tous les disques physiques du cluster +# Returns all physical disks in the cluster sub get_disks { my ($self, %options) = @_; return $self->request_api(endpoint => '/api/nutanix/v2.0/disks'); } -# Retourne tous les snapshots (ou ceux d'une VM spécifique via vm_uuid) +# Returns snapshots — optionally filtered by vm_uuid sub get_snapshots { my ($self, %options) = @_; if (defined($options{vm_uuid}) && $options{vm_uuid} ne '') { @@ -192,7 +192,7 @@ sub get_snapshots { return $self->request_api(endpoint => '/api/nutanix/v2.0/snapshots'); } -# Retourne les NICs d'une VM spécifique +# Returns NICs for a specific VM sub get_vm_nics { my ($self, %options) = @_; return $self->request_api( @@ -200,7 +200,7 @@ sub get_vm_nics { ); } -# Retourne les alertes actives (non résolues par défaut) +# Returns active (unresolved) alerts; optional severity filter sub get_alerts { my ($self, %options) = @_; my @params = ('resolved=false'); @@ -211,12 +211,33 @@ sub get_alerts { ); } -# Retourne les résultats des health checks NCC +# Returns NCC health check results sub get_health_checks { my ($self, %options) = @_; return $self->request_api(endpoint => '/api/nutanix/v2.0/health_checks'); } +# Returns protection domains and their replication status +sub get_protection_domains { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/protection_domains/'); +} + +# Returns storage containers with capacity and savings stats +sub get_storage_containers { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/storage_containers/'); +} + +# Returns recent top-level tasks (subtasks excluded, limited to 100) +sub get_tasks { + my ($self, %options) = @_; + return $self->request_api( + endpoint => '/api/nutanix/v2.0/tasks/', + get_param => [ 'includeSubtasks=false', 'count=100' ], + ); +} + 1; __END__ From 9cb444562ec3027eb23c3f25a1585b66ae92f5d4 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:11:15 +0200 Subject: [PATCH 06/26] Update alerts.pm --- src/apps/nutanix/prism/mode/alerts.pm | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/apps/nutanix/prism/mode/alerts.pm b/src/apps/nutanix/prism/mode/alerts.pm index 89a2f7ac06..0035ef5602 100644 --- a/src/apps/nutanix/prism/mode/alerts.pm +++ b/src/apps/nutanix/prism/mode/alerts.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -26,8 +26,7 @@ use base qw(centreon::plugins::templates::counter); use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); use POSIX qw(floor); -# ─── Correspondance des sévérités Nutanix → niveau Centreon ────────────────── -# L'API Prism v2.0 retourne kCritical, kWarning, kInfo +# Nutanix Prism v2.0 API severity values and their Centreon equivalents my %SEVERITY_MAP = ( kCritical => 'critical', kWarning => 'warning', @@ -60,9 +59,9 @@ sub set_counters { my ($self, %options) = @_; $self->{maps_counters_type} = [ - # Compteurs globaux : nombre d'alertes par sévérité + # Global counters: alert count per severity { name => 'global', type => 0 }, - # Compteur par alerte : statut individuel (pour long output) + # Per-alert counters: individual status for long output { name => 'alerts', type => 1, @@ -115,7 +114,7 @@ sub set_counters { { label => 'alert-status', type => 2, - # Par défaut : chaque alerte critique déclenche CRITICAL, warning → WARNING + # Default: each critical alert triggers CRITICAL, warning triggers WARNING warning_default => '%{severity} eq "warning"', critical_default => '%{severity} eq "critical"', set => { @@ -148,7 +147,7 @@ sub new { 'filter-severity:s' => { name => 'filter_severity' }, 'filter-title:s' => { name => 'filter_title' }, 'filter-entity:s' => { name => 'filter_entity' }, - # Âge minimum en secondes pour ignorer les alertes trop récentes + # Minimum alert age in seconds; younger alerts are ignored 'min-age:s' => { name => 'min_age', default => 0 }, } ); @@ -168,14 +167,14 @@ sub manage_selection { my $now = time(); for my $alert (@{$entities}) { - # Sévérité normalisée (kCritical → critical) + # Normalize severity: kCritical → critical my $raw_sev = $alert->{severity} // 'kInfo'; my $severity = $SEVERITY_MAP{$raw_sev} // 'info'; - # Titre : reconstruit depuis alert_title ou message + # Title: reconstructed from alert_title, message, or check_id my $title = $alert->{alert_title} // $alert->{message} // $alert->{check_id} // 'N/A'; - # Entité affectée : context_types[i] = "vm_name" → context_values[i] + # Affected entity: scan context_types for a recognizable entity name key my $entity = 'cluster'; my $types = $alert->{context_types} // []; my $values = $alert->{context_values} // []; @@ -186,14 +185,13 @@ sub manage_selection { } } - # Âge en secondes (created_time_stamp_in_usecs est en microsecondes) + # Age in seconds (created_time_stamp_in_usecs is in microseconds) my $created_usec = $alert->{created_time_stamp_in_usecs} // 0; my $age_s = ($created_usec > 0) ? ($now - int($created_usec / 1_000_000)) : 0; $age_s = 0 if $age_s < 0; my $id = $alert->{id} // $alert->{alert_type_uuid} // "$severity-$title"; - # Filtrage next if defined($self->{option_results}->{filter_severity}) && $self->{option_results}->{filter_severity} ne '' && $severity !~ /$self->{option_results}->{filter_severity}/i; From ccec7a99fb2527028e11253c86458b7b20d0c886 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:12:14 +0200 Subject: [PATCH 07/26] Update capacity.pm --- src/apps/nutanix/prism/mode/capacity.pm | 33 +++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/apps/nutanix/prism/mode/capacity.pm b/src/apps/nutanix/prism/mode/capacity.pm index 92aa78cbf1..65d5f8318e 100644 --- a/src/apps/nutanix/prism/mode/capacity.pm +++ b/src/apps/nutanix/prism/mode/capacity.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -24,8 +24,8 @@ use strict; use warnings; use base qw(centreon::plugins::templates::counter); -# Ce mode agrège la capacité CPU, RAM et stockage à l'échelle du cluster -# en consolidant les données des hôtes et des storage pools. +# Aggregates cluster-wide CPU, RAM, and storage capacity +# by consolidating data from all hosts and storage pools. sub set_counters { my ($self, %options) = @_; @@ -36,7 +36,7 @@ sub set_counters { { name => 'storage', type => 0, message_separator => ' - ' }, ]; - # ── CPU (vCPU alloués vs capacité physique) ─────────────────────────────── + # CPU (allocated vCPUs vs physical core capacity) $self->{maps_counters}->{cpu} = [ { label => 'cpu-capacity', @@ -69,7 +69,7 @@ sub set_counters { }, ]; - # ── Mémoire (octets) ────────────────────────────────────────────────────── + # Memory (bytes) $self->{maps_counters}->{memory} = [ { label => 'memory-capacity', @@ -104,7 +104,7 @@ sub set_counters { }, ]; - # ── Stockage (octets) ───────────────────────────────────────────────────── + # Storage (bytes) $self->{maps_counters}->{storage} = [ { label => 'storage-capacity', @@ -160,7 +160,7 @@ sub new { sub manage_selection { my ($self, %options) = @_; - # ── Données CPU et RAM : agrégation depuis les hôtes ───────────────────── + # Aggregate CPU and RAM data from all hosts my $hosts_result = $options{custom}->get_hosts(); my $hosts = $hosts_result->{entities} // []; @@ -169,20 +169,17 @@ sub manage_selection { my $host_count = scalar(@{$hosts}); for my $host (@{$hosts}) { - # num_cpu_cores = cœurs physiques par hôte - $total_cores += $host->{num_cpu_cores} // 0; - # num_vms est un proxy de l'allocation vCPU (on utilisera num_cpu_threads si dispo) - $allocated_vcpus += $host->{num_cpu_threads} // 0; - - # Mémoire : memory_capacity_in_bytes + $total_cores += $host->{num_cpu_cores} // 0; + # num_cpu_threads is the best available proxy for allocated vCPU count in v2.0 + $allocated_vcpus += $host->{num_cpu_threads} // 0; $mem_total += $host->{memory_capacity_in_bytes} // 0; - # CPU usage en PPM → % - my $stats = $host->{stats} // {}; - my $cpu_ppm = $stats->{hypervisor_cpu_usage_ppm} // 0; + my $stats = $host->{stats} // {}; + # CPU: PPM (parts per million) → percentage + my $cpu_ppm = $stats->{hypervisor_cpu_usage_ppm} // 0; $cpu_usage_sum += $cpu_ppm / 10000; - # RAM used : memory_size_bytes (capacité) - memory_usage_ppm + # Memory used = capacity × (usage_ppm / 1_000_000) my $mem_ppm = $stats->{hypervisor_memory_usage_ppm} // 0; $mem_used_sum += ($host->{memory_capacity_in_bytes} // 0) * ($mem_ppm / 1_000_000); } @@ -201,7 +198,7 @@ sub manage_selection { memory_usage_pct => $mem_usage_pct, }; - # ── Données stockage : agrégation depuis les storage pools ─────────────── + # Aggregate storage data from all storage pools my $pools_result = $options{custom}->get_storage_pools(); my $pools = $pools_result->{entities} // []; From f88efbf652f91988458368f1bc308551e650b6e4 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:13:21 +0200 Subject: [PATCH 08/26] Update clusterstatus.pm --- src/apps/nutanix/prism/mode/clusterstatus.pm | 46 +++++++++----------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/apps/nutanix/prism/mode/clusterstatus.pm b/src/apps/nutanix/prism/mode/clusterstatus.pm index 2758e64a3d..d0b1d87523 100644 --- a/src/apps/nutanix/prism/mode/clusterstatus.pm +++ b/src/apps/nutanix/prism/mode/clusterstatus.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -49,23 +49,23 @@ sub set_counters { ]; $self->{maps_counters}->{clusters} = [ - # Compteur de type "status" (type => 2) : vérifie un état via une expression + # Status counter (type 2 evaluates a threshold expression) + # Prism v2.0 returns cluster_state="NORMAL" for a healthy cluster. { - label => 'status', - type => 2, - # Seuil warning par défaut : état différent de "COMPLETE" - warning_default => '%{state} ne "COMPLETE"', - set => { + label => 'status', + type => 2, + warning_default => '%{state} ne "NORMAL"', + set => { key_values => [ - { name => 'name' }, - { name => 'state' }, + { name => 'name' }, + { name => 'state' }, { name => 'version' }, ], closure_custom_output => $self->can('custom_status_output'), closure_custom_threshold_check => \&catalog_status_threshold_ng, } }, - # Compteur numérique : nombre de nœuds + # Node count { label => 'nodes-count', nlabel => 'cluster.nodes.count', @@ -74,10 +74,10 @@ sub set_counters { output_template => 'nodes: %d', perfdatas => [ { - template => '%d', + template => '%d', label_extra_instance => 1, - instance_use => 'name', - min => 0, + instance_use => 'name', + min => 0, } ] } @@ -107,29 +107,25 @@ sub new { sub manage_selection { my ($self, %options) = @_; - # Appel au module custom (api.pm) via $options{custom} - my $result = $options{custom}->get_clusters(); - - # L'API v2.0 retourne { entities => [...], metadata => {...} } + my $result = $options{custom}->get_clusters(); my $entities = $result->{entities} // []; $self->{clusters} = {}; for my $cluster (@{$entities}) { my $name = $cluster->{name} // 'unknown'; - # Filtrage optionnel par nom (regex) if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { next if $name !~ /$self->{option_results}->{filter_name}/; } - # On stocke les données dans le hash $self->{clusters} - # La clé est unique par instance (ici le nom du cluster) - $self->{clusters}->{$name} = { + # Key on cluster_uuid to avoid silent overwrites when two clusters share the same name. + my $key = $cluster->{cluster_uuid} // $name; + + $self->{clusters}->{$key} = { name => $name, - # cluster_state est dans les stats internes de Prism v2 state => $cluster->{cluster_state} // 'UNKNOWN', - version => $cluster->{version} // 'N/A', - num_nodes => $cluster->{num_nodes} // 0, + version => $cluster->{version} // 'N/A', + num_nodes => $cluster->{num_nodes} // 0, }; } @@ -156,7 +152,7 @@ Filter clusters by name (regexp). Example: C<--filter-name='^Prod'> =item B<--warning-status> Warning threshold for cluster state. -Default: C<%{state} ne "COMPLETE"> +Default: C<%{state} ne "NORMAL"> Variables: C<%{name}>, C<%{state}>, C<%{version}> From 11e14e8aef054a2231068db1235ae35f09cae42e Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:14:59 +0200 Subject: [PATCH 09/26] Update disksstatus.pm --- src/apps/nutanix/prism/mode/disksstatus.pm | 73 ++++++++++++---------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/src/apps/nutanix/prism/mode/disksstatus.pm b/src/apps/nutanix/prism/mode/disksstatus.pm index e57b404d10..26a9e5601e 100644 --- a/src/apps/nutanix/prism/mode/disksstatus.pm +++ b/src/apps/nutanix/prism/mode/disksstatus.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -25,7 +25,6 @@ use warnings; use base qw(centreon::plugins::templates::counter); use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); -# ─── output personnalisé pour le statut du disque ─────────────────────────── sub custom_status_output { my ($self, %options) = @_; return sprintf( @@ -52,13 +51,12 @@ sub set_counters { ]; $self->{maps_counters}->{disks} = [ - # ── Statut opérationnel ────────────────────────────────────────────── + # Operational status (type 2 evaluates a threshold expression against key_values) { - label => 'status', - type => 2, - # Un disque sain a disk_status "NORMAL" et online true + label => 'status', + type => 2, critical_default => '%{state} ne "NORMAL" or %{online} ne "true"', - set => { + set => { key_values => [ { name => 'id' }, { name => 'node' }, @@ -70,7 +68,7 @@ sub set_counters { closure_custom_threshold_check => \&catalog_status_threshold_ng, } }, - # ── Capacité totale (octets) ───────────────────────────────────────── + # Total disk capacity in bytes { label => 'capacity', nlabel => 'disk.capacity.bytes', @@ -80,16 +78,16 @@ sub set_counters { output_change_bytes => 1, perfdatas => [ { - template => '%d', - unit => 'B', - min => 0, + template => '%d', + unit => 'B', + min => 0, label_extra_instance => 1, - instance_use => 'id', + instance_use => 'id', } ] } }, - # ── Espace libre (octets) ──────────────────────────────────────────── + # Free space in bytes { label => 'free', nlabel => 'disk.free.bytes', @@ -99,16 +97,16 @@ sub set_counters { output_change_bytes => 1, perfdatas => [ { - template => '%d', - unit => 'B', - min => 0, + template => '%d', + unit => 'B', + min => 0, label_extra_instance => 1, - instance_use => 'id', + instance_use => 'id', } ] } }, - # ── Utilisation en pourcentage ─────────────────────────────────────── + # Usage percentage { label => 'usage-prct', nlabel => 'disk.usage.percentage', @@ -117,12 +115,12 @@ sub set_counters { output_template => 'usage: %.2f%%', perfdatas => [ { - template => '%.2f', - unit => '%', - min => 0, - max => 100, + template => '%.2f', + unit => '%', + min => 0, + max => 100, label_extra_instance => 1, - instance_use => 'id', + instance_use => 'id', } ] } @@ -142,8 +140,8 @@ sub new { $options{options}->add_options( arguments => { - 'filter-node:s' => { name => 'filter_node' }, - 'filter-id:s' => { name => 'filter_id' }, + 'filter-node:s' => { name => 'filter_node' }, + 'filter-id:s' => { name => 'filter_id' }, } ); @@ -158,8 +156,8 @@ sub manage_selection { $self->{disks} = {}; for my $disk (@{$entities}) { - my $id = $disk->{id} // $disk->{disk_uuid} // 'unknown'; - my $node = $disk->{node_name} // $disk->{host_name} // 'N/A'; + my $id = $disk->{id} // $disk->{disk_uuid} // 'unknown'; + my $node = $disk->{node_name} // $disk->{host_name} // 'N/A'; if (defined($self->{option_results}->{filter_id}) && $self->{option_results}->{filter_id} ne '') { next if $id !~ /$self->{option_results}->{filter_id}/; @@ -168,23 +166,30 @@ sub manage_selection { next if $node !~ /$self->{option_results}->{filter_node}/; } - my $capacity = $disk->{disk_size} // 0; - my $free = $disk->{free_space} // 0; - # free_space peut être absent selon la version ; on calcule depuis usage si dispo - if ($free == 0 && defined($disk->{usage_stats})) { + my $capacity = $disk->{disk_size} // 0; + my $free = $disk->{free_space}; + + # Fall back to usage_stats only when free_space is absent from the API response. + # Do NOT trigger on free_space == 0: that is a legitimately full disk. + if (!defined($free) && defined($disk->{usage_stats})) { my $used = $disk->{usage_stats}->{'storage.usage_bytes'} // 0; $free = $capacity - $used; } + $free //= 0; + + # Clamp free to 0 before computing percentage so pct stays in [0, 100]. + $free = 0 if $free < 0; my $pct = ($capacity > 0) ? (($capacity - $free) / $capacity * 100) : 0; $self->{disks}->{$id} = { id => $id, node => $node, - serial => $disk->{disk_hardware_config}->{serial_number} // 'N/A', - state => $disk->{disk_status} // 'UNKNOWN', + # Guard against absent disk_hardware_config (logical/virtual disks, older API). + serial => ($disk->{disk_hardware_config} // {})->{serial_number} // 'N/A', + state => $disk->{disk_status} // 'UNKNOWN', online => defined($disk->{online}) ? ($disk->{online} ? 'true' : 'false') : 'true', capacity_bytes => $capacity, - free_bytes => ($free >= 0) ? $free : 0, + free_bytes => $free, usage_pct => $pct, }; } From 42ba4b2f5233cd6fd1b4e801d91f65023b191f9e Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:15:39 +0200 Subject: [PATCH 10/26] Update healthchecks.pm --- src/apps/nutanix/prism/mode/healthchecks.pm | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/apps/nutanix/prism/mode/healthchecks.pm b/src/apps/nutanix/prism/mode/healthchecks.pm index 52c91d246d..bb7342f1e5 100644 --- a/src/apps/nutanix/prism/mode/healthchecks.pm +++ b/src/apps/nutanix/prism/mode/healthchecks.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -25,8 +25,8 @@ use warnings; use base qw(centreon::plugins::templates::counter); use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); -# Les health checks NCC de Nutanix ont un champ execution_results[] qui contient -# les résultats par nœud. Le statut global se lit dans health_check_series.state : +# Nutanix NCC health checks expose per-node results in execution_results[]. +# The overall state is read from health_check_series[].state (last entry) or state: # PASS, FAIL, WARNING, INFO, ERROR, SCHEDULED, RUNNING, ABORTED my %STATE_SEVERITY = ( @@ -52,9 +52,9 @@ sub set_counters { my ($self, %options) = @_; $self->{maps_counters_type} = [ - # Résumé global (comptages par résultat) + # Global summary: check counts by result state { name => 'global', type => 0 }, - # Résultat individuel par health check + # Individual result per health check { name => 'checks', type => 1, @@ -137,7 +137,7 @@ sub new { arguments => { 'filter-name:s' => { name => 'filter_name' }, 'filter-category:s' => { name => 'filter_category' }, - # N'affiche que les checks non-PASS (utile pour réduire le bruit) + # Only report non-PASS checks to reduce noise on large clusters 'only-failing' => { name => 'only_failing' }, } ); @@ -158,7 +158,6 @@ sub manage_selection { my $name = $check->{name} // $check->{check_id} // 'unknown'; my $category = $check->{category} // 'N/A'; - # Filtrage if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { next if $name !~ /$self->{option_results}->{filter_name}/i; } @@ -166,8 +165,7 @@ sub manage_selection { next if $category !~ /$self->{option_results}->{filter_category}/i; } - # Le statut global du check est dans health_check_series[].state - # ou directement dans state (selon la version de Prism) + # Overall state from health_check_series[].state (last entry) or state field my $state = 'UNKNOWN'; if (defined($check->{health_check_series}) && ref($check->{health_check_series}) eq 'ARRAY' && @{$check->{health_check_series}}) { @@ -178,7 +176,7 @@ sub manage_selection { next if defined($self->{option_results}->{only_failing}) && $state eq 'PASS'; - # Message de détail (causes + resolutions concaténés) + # Detail message: concatenate execution result messages when available my $message = $check->{message} // ''; if (!$message && defined($check->{health_check_series}) && @{$check->{health_check_series}}) { my $last = $check->{health_check_series}->[-1]; @@ -187,9 +185,9 @@ sub manage_selection { } $message = 'no detail' if $message eq ''; - # Comptage global (FAIL, ERROR, WARNING, PASS) + # Increment the matching global bucket (error stays in its own bucket, + # not merged into fail — otherwise healthchecks.error.count would always be 0). my $bucket = lc($state); - $bucket = 'fail' if $bucket eq 'error'; # ERROR → même bucket que FAIL pour les seuils globaux $self->{global}->{$bucket}++ if exists $self->{global}->{$bucket}; $self->{checks}->{$name} = { From cc4cb9f9f45cd88a67975df657c6e2d9fbefb611 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:16:59 +0200 Subject: [PATCH 11/26] Update hostsusage.pm --- src/apps/nutanix/prism/mode/hostsusage.pm | 69 +++++++++++++---------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/apps/nutanix/prism/mode/hostsusage.pm b/src/apps/nutanix/prism/mode/hostsusage.pm index 2eba708d42..532dc6edd1 100644 --- a/src/apps/nutanix/prism/mode/hostsusage.pm +++ b/src/apps/nutanix/prism/mode/hostsusage.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -48,18 +48,21 @@ sub set_counters { ]; $self->{maps_counters}->{hosts} = [ - # Statut de l'hôte + # Host operational state { label => 'status', type => 2, warning_default => '%{state} ne "NORMAL"', set => { - key_values => [ { name => 'name' }, { name => 'state' } ], + key_values => [ + { name => 'name' }, + { name => 'state' }, + ], closure_custom_output => $self->can('custom_status_output'), closure_custom_threshold_check => \&catalog_status_threshold_ng, } }, - # Utilisation CPU en pourcentage + # CPU usage percentage (stats field is in parts-per-million; divide by 10000) { label => 'cpu-usage', nlabel => 'host.cpu.usage.percentage', @@ -68,17 +71,17 @@ sub set_counters { output_template => 'CPU usage: %.2f%%', perfdatas => [ { - template => '%.2f', - unit => '%', - min => 0, - max => 100, + template => '%.2f', + unit => '%', + min => 0, + max => 100, label_extra_instance => 1, - instance_use => 'name', + instance_use => 'name', } ] } }, - # Utilisation RAM en pourcentage + # Memory usage percentage { label => 'memory-usage', nlabel => 'host.memory.usage.percentage', @@ -87,17 +90,17 @@ sub set_counters { output_template => 'memory usage: %.2f%%', perfdatas => [ { - template => '%.2f', - unit => '%', - min => 0, - max => 100, + template => '%.2f', + unit => '%', + min => 0, + max => 100, label_extra_instance => 1, - instance_use => 'name', + instance_use => 'name', } ] } }, - # Nombre de VMs sur cet hôte + # Number of VMs running on this host { label => 'vms-count', nlabel => 'host.vms.count', @@ -106,10 +109,10 @@ sub set_counters { output_template => 'VMs: %d', perfdatas => [ { - template => '%d', - min => 0, + template => '%d', + min => 0, label_extra_instance => 1, - instance_use => 'name', + instance_use => 'name', } ] } @@ -139,7 +142,7 @@ sub new { sub manage_selection { my ($self, %options) = @_; - my $result = $options{custom}->get_hosts(); + my $result = $options{custom}->get_hosts(); my $entities = $result->{entities} // []; $self->{hosts} = {}; @@ -150,19 +153,25 @@ sub manage_selection { next if $name !~ /$self->{option_results}->{filter_name}/; } - # L'API v2.0 retourne des stats dans host->{stats} - # cpu_usage_ppm = parties par million (diviser par 10000 pour avoir %) - my $stats = $host->{stats} // {}; - my $cpu_ppm = $stats->{hypervisor_cpu_usage_ppm} // 0; - my $cpu_pct = $cpu_ppm / 10000; - - # memory_usage_ppm également en ppm - my $mem_ppm = $stats->{hypervisor_memory_usage_ppm} // 0; - my $mem_pct = $mem_ppm / 10000; + my $stats = $host->{stats} // {}; + # PPM (parts per million): divide by 10000 to get a percentage. + my $cpu_pct = ($stats->{hypervisor_cpu_usage_ppm} // 0) / 10000; + my $mem_pct = ($stats->{hypervisor_memory_usage_ppm} // 0) / 10000; + + # Derive host state from both the maintenance flag and the hypervisor state + # so that crashed or degraded hosts are not silently reported as NORMAL. + my $state; + if ($host->{host_in_maintenance_mode}) { + $state = 'MAINTENANCE'; + } elsif (defined($host->{hypervisor_state}) && $host->{hypervisor_state} ne 'NORMAL') { + $state = $host->{hypervisor_state}; + } else { + $state = 'NORMAL'; + } $self->{hosts}->{$name} = { name => $name, - state => $host->{host_in_maintenance_mode} ? 'MAINTENANCE' : 'NORMAL', + state => $state, cpu_usage_pct => $cpu_pct, memory_usage_pct => $mem_pct, num_vms => $host->{num_vms} // 0, From ad4de68e34d29f2d3170a8844ed129c12f1bd9ed Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:17:49 +0200 Subject: [PATCH 12/26] Update listhosts.pm --- src/apps/nutanix/prism/mode/listhosts.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/nutanix/prism/mode/listhosts.pm b/src/apps/nutanix/prism/mode/listhosts.pm index 6741c51c25..2034c6318f 100644 --- a/src/apps/nutanix/prism/mode/listhosts.pm +++ b/src/apps/nutanix/prism/mode/listhosts.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -64,7 +64,7 @@ sub run { $self->{output}->exit(); } -# Appelé par le framework Centreon pour la découverte automatique +# Called by the Centreon framework for automatic service discovery sub disco_format { my ($self, %options) = @_; $self->{output}->add_disco_format(elements => ['name', 'uuid', 'ip', 'model', 'num_vms']); From 8a50be1cd7135d265e6a2729973c4152e70bc23a Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:18:35 +0200 Subject: [PATCH 13/26] Update listnics.pm --- src/apps/nutanix/prism/mode/listnics.pm | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/apps/nutanix/prism/mode/listnics.pm b/src/apps/nutanix/prism/mode/listnics.pm index fe708b02b4..02936d2b09 100644 --- a/src/apps/nutanix/prism/mode/listnics.pm +++ b/src/apps/nutanix/prism/mode/listnics.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -31,8 +31,8 @@ sub new { $options{options}->add_options( arguments => { - 'filter-vm-name:s' => { name => 'filter_vm_name' }, - 'filter-network:s' => { name => 'filter_network' }, + 'filter-vm-name:s' => { name => 'filter_vm_name' }, + 'filter-network:s' => { name => 'filter_network' }, } ); @@ -44,7 +44,7 @@ sub check_options { $self->SUPER::init(%options); } -# Collecte tous les NICs depuis la liste des VMs (vm_nics[] est inclus dans v2.0) +# Collect all NICs from the VM list (vm_nics[] is included in v2.0 responses). sub _collect_nics { my ($self, %options) = @_; @@ -64,9 +64,14 @@ sub _collect_nics { for my $nic (@{ $vm->{vm_nics} // [] }) { my $network = $nic->{network_name} // $nic->{vlan_id} // 'N/A'; + # nic_index tracks the physical position in the VM's NIC array. + # Skipped NICs still consume an index so nic_id stays stable + # whether or not --filter-network is active. if (defined($self->{option_results}->{filter_network}) && $self->{option_results}->{filter_network} ne '') { - $nic_index++; - next if $network !~ /$self->{option_results}->{filter_network}/; + if ($network !~ /$self->{option_results}->{filter_network}/) { + $nic_index++; + next; + } } push @nics, { @@ -74,7 +79,7 @@ sub _collect_nics { vm_uuid => $vm_uuid, nic_index => $nic_index, nic_id => $vm_name . '_nic' . $nic_index, - mac => $nic->{mac_address} // 'N/A', + mac => $nic->{mac_address} // 'N/A', network => $network, connected => (defined($nic->{is_connected}) && $nic->{is_connected}) ? 'true' : 'false', ip => (defined($nic->{ip_address}) && $nic->{ip_address} ne '') ? $nic->{ip_address} : 'N/A', From 9d26d94d3f7b0a0ec07a1d9ecd854f3011ecc27f Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:19:37 +0200 Subject: [PATCH 14/26] Create listprotectiondomains.pm --- .../prism/mode/listprotectiondomains.pm | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/apps/nutanix/prism/mode/listprotectiondomains.pm diff --git a/src/apps/nutanix/prism/mode/listprotectiondomains.pm b/src/apps/nutanix/prism/mode/listprotectiondomains.pm new file mode 100644 index 0000000000..763fc171ff --- /dev/null +++ b/src/apps/nutanix/prism/mode/listprotectiondomains.pm @@ -0,0 +1,147 @@ +# +# Copyright 2026 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::listprotectiondomains; + +use strict; +use warnings; +use base qw(centreon::plugins::mode); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + } + ); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_protection_domains(); + my $entities = $result->{entities} // []; + + my @pds; + for my $pd (@{$entities}) { + my $name = $pd->{name} // 'unknown'; + + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/; + } + + # Derive replication health from replication_links array. + my $replication_status = 'N/A'; + my @links = @{ $pd->{replication_links} // [] }; + if (@links) { + my @degraded = grep { ($_->{replication_status} // 'Healthy') ne 'Healthy' } @links; + $replication_status = @degraded ? 'Degraded' : 'Healthy'; + } + + my $vstore_count = $pd->{vstore_count} + // scalar(@{ $pd->{vstore_names} // [] }); + + push @pds, { + name => $name, + active => ($pd->{active} // 0) ? 'true' : 'false', + replication_status => $replication_status, + vstore_count => $vstore_count, + pending_replication_count => $pd->{pending_replication_count} // 0, + }; + } + + return @pds; +} + +sub run { + my ($self, %options) = @_; + + my @pds = $self->manage_selection(%options); + for my $pd (sort { $a->{name} cmp $b->{name} } @pds) { + $self->{output}->output_add( + long_msg => sprintf( + '[name: %s] [active: %s] [replication: %s] [vstores: %d] [pending_replications: %d]', + $pd->{name}, + $pd->{active}, + $pd->{replication_status}, + $pd->{vstore_count}, + $pd->{pending_replication_count}, + ) + ); + } + + $self->{output}->output_add( + severity => 'OK', + short_msg => sprintf('%d protection domain(s) found', scalar @pds) + ); + $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1); + $self->{output}->exit(); +} + +sub disco_format { + my ($self, %options) = @_; + + $self->{output}->add_disco_format( + elements => [ 'name', 'active', 'replication_status', 'vstore_count', 'pending_replication_count' ] + ); +} + +sub disco_show { + my ($self, %options) = @_; + + my @pds = $self->manage_selection(%options); + for my $pd (@pds) { + $self->{output}->add_disco_entry( + name => $pd->{name}, + active => $pd->{active}, + replication_status => $pd->{replication_status}, + vstore_count => $pd->{vstore_count}, + pending_replication_count => $pd->{pending_replication_count}, + ); + } +} + +1; + +__END__ + +=head1 MODE + +List Nutanix protection domains (Centreon service discovery). + +=over 8 + +=item B<--filter-name> + +Filter protection domains by name (regexp). + +=back + +=cut From 74a1fbb1ec328bdeda3c531f0654c15ba6238ea8 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:20:34 +0200 Subject: [PATCH 15/26] Create liststoragecontainers.pm --- .../prism/mode/liststoragecontainers.pm | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 src/apps/nutanix/prism/mode/liststoragecontainers.pm diff --git a/src/apps/nutanix/prism/mode/liststoragecontainers.pm b/src/apps/nutanix/prism/mode/liststoragecontainers.pm new file mode 100644 index 0000000000..2c004f14d7 --- /dev/null +++ b/src/apps/nutanix/prism/mode/liststoragecontainers.pm @@ -0,0 +1,144 @@ +# +# Copyright 2026 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::liststoragecontainers; + +use strict; +use warnings; +use base qw(centreon::plugins::mode); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + } + ); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_storage_containers(); + my $entities = $result->{entities} // []; + + my @containers; + for my $container (@{$entities}) { + my $name = $container->{name} // $container->{storage_container_uuid} // 'unknown'; + my $id = $container->{storage_container_uuid} // ''; + + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/; + } + + my $ustats = $container->{usage_stats} // {}; + my $capacity = $container->{max_capacity} + // $ustats->{'storage.capacity_bytes'} + // 0; + my $used = $ustats->{'storage.usage_bytes'} // 0; + my $pct = ($capacity > 0) ? ($used / $capacity * 100) : 0; + + push @containers, { + name => $name, + id => $id, + usage_pct => sprintf('%.2f', $pct), + compression_enabled => ($container->{compression_enabled} // 0) ? 'true' : 'false', + dedup_enabled => ($container->{on_disk_dedup} // 0) ? 'true' : 'false', + }; + } + + return @containers; +} + +sub run { + my ($self, %options) = @_; + + my @containers = $self->manage_selection(%options); + for my $c (sort { $a->{name} cmp $b->{name} } @containers) { + $self->{output}->output_add( + long_msg => sprintf( + '[name: %s] [id: %s] [usage_pct: %s%%] [compression: %s] [dedup: %s]', + $c->{name}, + $c->{id}, + $c->{usage_pct}, + $c->{compression_enabled}, + $c->{dedup_enabled}, + ) + ); + } + + $self->{output}->output_add( + severity => 'OK', + short_msg => sprintf('%d storage container(s) found', scalar @containers) + ); + $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1); + $self->{output}->exit(); +} + +sub disco_format { + my ($self, %options) = @_; + + $self->{output}->add_disco_format( + elements => [ 'name', 'id', 'usage_pct', 'compression_enabled', 'dedup_enabled' ] + ); +} + +sub disco_show { + my ($self, %options) = @_; + + my @containers = $self->manage_selection(%options); + for my $c (@containers) { + $self->{output}->add_disco_entry( + name => $c->{name}, + id => $c->{id}, + usage_pct => $c->{usage_pct}, + compression_enabled => $c->{compression_enabled}, + dedup_enabled => $c->{dedup_enabled}, + ); + } +} + +1; + +__END__ + +=head1 MODE + +List Nutanix storage containers (Centreon service discovery). + +=over 8 + +=item B<--filter-name> + +Filter storage containers by name (regexp). + +=back + +=cut From ef21b8e85c326deba902ff53748f7507db0063ff Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:23:49 +0200 Subject: [PATCH 16/26] Update listvms.pm --- src/apps/nutanix/prism/mode/listvms.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/nutanix/prism/mode/listvms.pm b/src/apps/nutanix/prism/mode/listvms.pm index 760e0a1202..2f287c4c6b 100644 --- a/src/apps/nutanix/prism/mode/listvms.pm +++ b/src/apps/nutanix/prism/mode/listvms.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for From e3204699d21ff60bf4bc19883a86123b1a915a23 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:25:39 +0200 Subject: [PATCH 17/26] Create protectiondomains.pm --- .../nutanix/prism/mode/protectiondomains.pm | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 src/apps/nutanix/prism/mode/protectiondomains.pm diff --git a/src/apps/nutanix/prism/mode/protectiondomains.pm b/src/apps/nutanix/prism/mode/protectiondomains.pm new file mode 100644 index 0000000000..2f4bc796c2 --- /dev/null +++ b/src/apps/nutanix/prism/mode/protectiondomains.pm @@ -0,0 +1,210 @@ +# +# Copyright 2026 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::protectiondomains; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub custom_status_output { + my ($self, %options) = @_; + return sprintf( + "protection domain '%s' role is '%s' [replication: %s]", + $self->{result_values}->{name}, + $self->{result_values}->{role}, + $self->{result_values}->{replication_status} + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'pds', + type => 1, + cb_prefix_output => 'prefix_pd_output', + message_multiple => 'All protection domains are OK', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{pds} = [ + # Replication health status + { + label => 'status', + type => 2, + critical_default => '%{replication_status} ne "Healthy" and %{replication_status} ne "N/A"', + set => { + key_values => [ + { name => 'name' }, + { name => 'role' }, + { name => 'replication_status' }, + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_threshold_check => \&catalog_status_threshold_ng, + } + }, + # Pending replication snapshots + { + label => 'pending-replications', + nlabel => 'protection_domain.replications.pending.count', + set => { + key_values => [ { name => 'pending_replication_count' }, { name => 'name' } ], + output_template => 'pending replications: %d', + perfdatas => [ + { + template => '%d', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Number of vStores protected + { + label => 'vstore-count', + nlabel => 'protection_domain.vstores.count', + set => { + key_values => [ { name => 'vstore_count' }, { name => 'name' } ], + output_template => 'vStores: %d', + perfdatas => [ + { + template => '%d', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + ]; +} + +sub prefix_pd_output { + my ($self, %options) = @_; + return "Protection domain '" . $options{instance_value}->{name} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_protection_domains(); + my $entities = $result->{entities} // []; + + $self->{pds} = {}; + for my $pd (@{$entities}) { + my $name = $pd->{name} // 'unknown'; + + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/; + } + + # Derive replication health from replication_links array. + # Any non-Healthy link marks the whole PD as Degraded. + my $replication_status = 'N/A'; + my @links = @{ $pd->{replication_links} // [] }; + if (@links) { + my @degraded = grep { ($_->{replication_status} // 'Healthy') ne 'Healthy' } @links; + $replication_status = @degraded ? 'Degraded' : 'Healthy'; + } + + # active=true means this is the active (primary) site; false means standby. + my $role = ($pd->{active} // 0) ? 'Active' : 'Standby'; + + # vstore_count may be an integer or we derive it from the vstore_names array. + my $vstore_count = $pd->{vstore_count} + // scalar(@{ $pd->{vstore_names} // [] }); + + $self->{pds}->{$name} = { + name => $name, + role => $role, + replication_status => $replication_status, + pending_replication_count => $pd->{pending_replication_count} // 0, + vstore_count => $vstore_count, + }; + } + + if (scalar(keys %{$self->{pds}}) == 0) { + $self->{output}->add_option_msg(short_msg => 'No protection domain found.'); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix protection domain replication status through Prism REST API. + +=over 8 + +=item B<--filter-name> + +Filter protection domains by name (regexp). Example: C<--filter-name='^PD-Prod'> + +=item B<--warning-status> + +Warning threshold for replication status. +Variables: C<%{name}>, C<%{role}>, C<%{replication_status}> + +=item B<--critical-status> + +Critical threshold for replication status. +Default: C<%{replication_status} ne "Healthy" and %{replication_status} ne "N/A"> + +=item B<--warning-pending-replications> + +Warning threshold for pending replication count. + +=item B<--critical-pending-replications> + +Critical threshold for pending replication count. + +=item B<--warning-vstore-count> + +Warning threshold for vStore count. + +=item B<--critical-vstore-count> + +Critical threshold for vStore count. + +=back + +=cut From 406c2ab2151714dd8b669b077fe02456665e3d30 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:26:16 +0200 Subject: [PATCH 18/26] Update snapshots.pm --- src/apps/nutanix/prism/mode/snapshots.pm | 51 +++++++++++------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/apps/nutanix/prism/mode/snapshots.pm b/src/apps/nutanix/prism/mode/snapshots.pm index 4719d97add..164d4e4f44 100644 --- a/src/apps/nutanix/prism/mode/snapshots.pm +++ b/src/apps/nutanix/prism/mode/snapshots.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -25,19 +25,17 @@ use warnings; use base qw(centreon::plugins::templates::counter); use POSIX qw(floor); -# ─── Regroupe les snapshots par VM et calcule l'âge du plus vieux ──────────── - sub set_counters { my ($self, %options) = @_; $self->{maps_counters_type} = [ - # Compteurs globaux (toutes VMs confondues) + # Global counters (all VMs combined) { name => 'global', type => 0, skipped_code => { -10 => 1 }, }, - # Compteurs par VM (une ligne de résultat par VM) + # Per-VM counters { name => 'vms', type => 1, @@ -48,7 +46,7 @@ sub set_counters { ]; $self->{maps_counters}->{global} = [ - # Nombre total de snapshots sur le cluster + # Total snapshot count across the cluster { label => 'total-count', nlabel => 'snapshots.total.count', @@ -63,7 +61,7 @@ sub set_counters { ]; $self->{maps_counters}->{vms} = [ - # Nombre de snapshots par VM + # Snapshot count per VM { label => 'vm-count', nlabel => 'vm.snapshots.count', @@ -72,29 +70,28 @@ sub set_counters { output_template => 'snapshots: %d', perfdatas => [ { - template => '%d', - min => 0, + template => '%d', + min => 0, label_extra_instance => 1, - instance_use => 'vm_name', + instance_use => 'vm_name', } ] } }, - # Âge du snapshot le plus vieux pour cette VM (en secondes) + # Age of the oldest snapshot for this VM (in seconds) { label => 'oldest-age', nlabel => 'vm.snapshot.oldest.age.seconds', set => { - key_values => [ { name => 'oldest_age_seconds' }, { name => 'vm_name' } ], - # output_template utilise une closure pour un affichage lisible + key_values => [ { name => 'oldest_age_seconds' }, { name => 'vm_name' } ], closure_custom_output => \&custom_oldest_age_output, perfdatas => [ { - template => '%d', - unit => 's', - min => 0, + template => '%d', + unit => 's', + min => 0, label_extra_instance => 1, - instance_use => 'vm_name', + instance_use => 'vm_name', } ] } @@ -102,7 +99,7 @@ sub set_counters { ]; } -# Affiche l'âge en jours/heures plutôt qu'en secondes brutes +# Display age in human-readable days/hours/minutes instead of raw seconds sub custom_oldest_age_output { my ($self, %options) = @_; my $age_s = $self->{result_values}->{oldest_age_seconds}; @@ -133,10 +130,9 @@ sub new { $options{options}->add_options( arguments => { - 'filter-vm-name:s' => { name => 'filter_vm_name' }, - # Seuil d'âge max en heures (pratique pour les alertes métier) - 'warning-oldest-age:s' => { name => 'warning_oldest_age' }, - 'critical-oldest-age:s' => { name => 'critical_oldest_age' }, + 'filter-vm-name:s' => { name => 'filter_vm_name' }, + # Note: --warning-oldest-age and --critical-oldest-age are auto-generated + # by the counter framework from label => 'oldest-age'. Do not declare them here. } ); @@ -146,11 +142,10 @@ sub new { sub manage_selection { my ($self, %options) = @_; - # Récupère tous les snapshots d'un coup my $result = $options{custom}->get_snapshots(); my $entities = $result->{entities} // []; - # On regroupe par VM + # Group snapshots by VM name my %by_vm; for my $snap (@{$entities}) { my $vm_name = $snap->{vm_name} // $snap->{vm_uuid} // 'unknown'; @@ -170,13 +165,13 @@ sub manage_selection { my $count = scalar(@snaps); $total += $count; - # Cherche le snapshot le plus vieux. - # created_time est en microsecondes depuis l'epoch. + # Find the oldest snapshot. + # created_time_in_usecs is microseconds since epoch (Prism v2.0 field name). my $oldest_epoch = undef; for my $snap (@snaps) { - my $ts = $snap->{created_time}; # µs + my $ts = $snap->{created_time_in_usecs}; next unless defined($ts) && $ts > 0; - $ts = int($ts / 1000000); # → secondes + $ts = int($ts / 1_000_000); # microseconds → seconds $oldest_epoch = $ts if !defined($oldest_epoch) || $ts < $oldest_epoch; } From 573f3fa069662d402a4614cdf0a921950b93a775 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:27:14 +0200 Subject: [PATCH 19/26] Create storagecontainers.pm --- .../nutanix/prism/mode/storagecontainers.pm | 258 ++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 src/apps/nutanix/prism/mode/storagecontainers.pm diff --git a/src/apps/nutanix/prism/mode/storagecontainers.pm b/src/apps/nutanix/prism/mode/storagecontainers.pm new file mode 100644 index 0000000000..9e5d48eb00 --- /dev/null +++ b/src/apps/nutanix/prism/mode/storagecontainers.pm @@ -0,0 +1,258 @@ +# +# Copyright 2026 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::storagecontainers; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'containers', + type => 1, + cb_prefix_output => 'prefix_container_output', + message_multiple => 'All storage containers are OK', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{containers} = [ + # Used bytes + { + label => 'usage', + nlabel => 'storage.container.usage.bytes', + set => { + key_values => [ { name => 'usage_bytes' }, { name => 'name' } ], + output_template => 'used: %s', + output_change_bytes => 1, + perfdatas => [ + { + template => '%d', + unit => 'B', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Free bytes + { + label => 'free', + nlabel => 'storage.container.free.bytes', + set => { + key_values => [ { name => 'free_bytes' }, { name => 'name' } ], + output_template => 'free: %s', + output_change_bytes => 1, + perfdatas => [ + { + template => '%d', + unit => 'B', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Usage percentage + { + label => 'usage-prct', + nlabel => 'storage.container.usage.percentage', + set => { + key_values => [ { name => 'usage_pct' }, { name => 'name' } ], + output_template => 'usage: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + max => 100, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Compression saving ratio as a percentage (0 when disabled) + { + label => 'compression-savings', + nlabel => 'storage.container.compression.savings.percentage', + set => { + key_values => [ { name => 'compression_savings_pct' }, { name => 'name' } ], + output_template => 'compression savings: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Deduplication saving ratio as a percentage (0 when disabled) + { + label => 'dedup-savings', + nlabel => 'storage.container.dedup.savings.percentage', + set => { + key_values => [ { name => 'dedup_savings_pct' }, { name => 'name' } ], + output_template => 'dedup savings: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + ]; +} + +sub prefix_container_output { + my ($self, %options) = @_; + return "Storage container '" . $options{instance_value}->{name} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_storage_containers(); + my $entities = $result->{entities} // []; + + $self->{containers} = {}; + for my $container (@{$entities}) { + my $name = $container->{name} // $container->{storage_container_uuid} // 'unknown'; + + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/; + } + + my $ustats = $container->{usage_stats} // {}; + my $capacity = $container->{max_capacity} + // $ustats->{'storage.capacity_bytes'} + // 0; + my $used = $ustats->{'storage.usage_bytes'} // 0; + + # Clamp free to avoid negative perfdata on overcommitted containers. + my $free = $capacity - $used; + $free = 0 if $free < 0; + my $pct = ($capacity > 0) ? ($used / $capacity * 100) : 0; + + # Savings ratios are stored as PPM; divide by 10000 for percentage. + my $compression_pct = ($container->{compression_saving_ratio_ppm} // 0) / 10000; + my $dedup_pct = ($container->{dedup_saving_ratio_ppm} // 0) / 10000; + + my $key = $container->{storage_container_uuid} // $name; + $self->{containers}->{$key} = { + name => $name, + usage_bytes => $used, + free_bytes => $free, + usage_pct => $pct, + compression_savings_pct => $compression_pct, + dedup_savings_pct => $dedup_pct, + }; + } + + if (scalar(keys %{$self->{containers}}) == 0) { + $self->{output}->add_option_msg(short_msg => 'No storage container found.'); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix storage container usage and savings through Prism REST API. + +=over 8 + +=item B<--filter-name> + +Filter storage containers by name (regexp). + +=item B<--warning-usage> + +Warning threshold for used space (bytes). + +=item B<--critical-usage> + +Critical threshold for used space (bytes). + +=item B<--warning-usage-prct> + +Warning threshold for usage percentage (%). + +=item B<--critical-usage-prct> + +Critical threshold for usage percentage (%). Example: C<--critical-usage-prct=90> + +=item B<--warning-free> + +Warning threshold for free space (bytes). + +=item B<--critical-free> + +Critical threshold for free space (bytes). + +=item B<--warning-compression-savings> + +Warning threshold for compression saving ratio (%). + +=item B<--critical-compression-savings> + +Critical threshold for compression saving ratio (%). + +=item B<--warning-dedup-savings> + +Warning threshold for deduplication saving ratio (%). + +=item B<--critical-dedup-savings> + +Critical threshold for deduplication saving ratio (%). + +=back + +=cut From fdceb823d1ae4a420beaf8eab97f8808c0ae7386 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:28:29 +0200 Subject: [PATCH 20/26] Update storageusage.pm --- src/apps/nutanix/prism/mode/storageusage.pm | 47 +++++++++++---------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/src/apps/nutanix/prism/mode/storageusage.pm b/src/apps/nutanix/prism/mode/storageusage.pm index 6e30a857ba..aa413f1b93 100644 --- a/src/apps/nutanix/prism/mode/storageusage.pm +++ b/src/apps/nutanix/prism/mode/storageusage.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -38,27 +38,26 @@ sub set_counters { ]; $self->{maps_counters}->{storage_pools} = [ - # Capacité totale en octets + # Used bytes { label => 'usage', nlabel => 'storage.pool.usage.bytes', set => { - key_values => [ { name => 'usage_bytes' }, { name => 'name' } ], - output_template => 'used: %s', - # Conversion automatique d'octets vers l'unité lisible (KB, MB, GB...) + key_values => [ { name => 'usage_bytes' }, { name => 'name' } ], + output_template => 'used: %s', output_change_bytes => 1, perfdatas => [ { - template => '%d', - unit => 'B', - min => 0, + template => '%d', + unit => 'B', + min => 0, label_extra_instance => 1, - instance_use => 'name', + instance_use => 'name', } ] } }, - # Capacité libre en octets + # Free bytes { label => 'free', nlabel => 'storage.pool.free.bytes', @@ -68,16 +67,16 @@ sub set_counters { output_change_bytes => 1, perfdatas => [ { - template => '%d', - unit => 'B', - min => 0, + template => '%d', + unit => 'B', + min => 0, label_extra_instance => 1, - instance_use => 'name', + instance_use => 'name', } ] } }, - # Utilisation en pourcentage (calculée) + # Computed usage percentage { label => 'usage-prct', nlabel => 'storage.pool.usage.percentage', @@ -86,12 +85,12 @@ sub set_counters { output_template => 'usage: %.2f%%', perfdatas => [ { - template => '%.2f', - unit => '%', - min => 0, - max => 100, + template => '%.2f', + unit => '%', + min => 0, + max => 100, label_extra_instance => 1, - instance_use => 'name', + instance_use => 'name', } ] } @@ -132,11 +131,13 @@ sub manage_selection { next if $name !~ /$self->{option_results}->{filter_name}/; } - # capacity_bytes et usage_bytes sont fournis directement par l'API v2.0 my $capacity = $pool->{capacity_bytes} // 0; my $used = $pool->{usage_bytes} // 0; - my $free = $capacity - $used; - my $pct = ($capacity > 0) ? ($used / $capacity * 100) : 0; + + # Clamp free to 0 to avoid negative perfdata under thin-provisioning overcommit. + my $free = $capacity - $used; + $free = 0 if $free < 0; + my $pct = ($capacity > 0) ? ($used / $capacity * 100) : 0; $self->{storage_pools}->{$name} = { name => $name, From d852eb9a20162a1265e2ab31d361283dc4c71ebd Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:29:33 +0200 Subject: [PATCH 21/26] Create tasks.pm --- src/apps/nutanix/prism/mode/tasks.pm | 176 +++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/apps/nutanix/prism/mode/tasks.pm diff --git a/src/apps/nutanix/prism/mode/tasks.pm b/src/apps/nutanix/prism/mode/tasks.pm new file mode 100644 index 0000000000..93a7f2c3c8 --- /dev/null +++ b/src/apps/nutanix/prism/mode/tasks.pm @@ -0,0 +1,176 @@ +# +# Copyright 2026 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::tasks; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'global', + type => 0, + message_separator => ' ', + } + ]; + + $self->{maps_counters}->{global} = [ + { + label => 'running', + nlabel => 'tasks.running.count', + set => { + key_values => [ { name => 'running' } ], + output_template => 'running: %d', + perfdatas => [ + { + template => '%d', + min => 0, + } + ] + } + }, + { + label => 'succeeded', + nlabel => 'tasks.succeeded.count', + set => { + key_values => [ { name => 'succeeded' } ], + output_template => 'succeeded: %d', + perfdatas => [ + { + template => '%d', + min => 0, + } + ] + } + }, + { + label => 'failed', + nlabel => 'tasks.failed.count', + set => { + key_values => [ { name => 'failed' } ], + output_template => 'failed: %d', + perfdatas => [ + { + template => '%d', + min => 0, + } + ] + } + }, + { + label => 'aborted', + nlabel => 'tasks.aborted.count', + set => { + key_values => [ { name => 'aborted' } ], + output_template => 'aborted: %d', + perfdatas => [ + { + template => '%d', + min => 0, + } + ] + } + }, + ]; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_tasks(); + my $entities = $result->{entities} // []; + + my %counts = ( running => 0, succeeded => 0, failed => 0, aborted => 0 ); + + for my $task (@{$entities}) { + # Prism v2.0 task statuses start with a 'k' prefix (e.g. kRunning, kSucceeded). + # Normalize to lowercase without prefix for consistent matching. + my $status = $task->{progress_status} // ''; + $status =~ s/^k//i; + $status = lc($status); + + if ($status eq 'running') { $counts{running}++ } + elsif ($status eq 'succeeded') { $counts{succeeded}++ } + elsif ($status eq 'failed') { $counts{failed}++ } + elsif ($status eq 'aborted') { $counts{aborted}++ } + } + + $self->{global} = \%counts; +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix background task counts through Prism REST API. + +Returns global counts for running, succeeded, failed and aborted tasks +from the last 100 tasks (top-level tasks only; subtasks excluded). + +=over 8 + +=item B<--warning-running> + +Warning threshold for running task count. + +=item B<--critical-running> + +Critical threshold for running task count. + +=item B<--warning-succeeded> + +Warning threshold for succeeded task count. + +=item B<--critical-succeeded> + +Critical threshold for succeeded task count. + +=item B<--warning-failed> + +Warning threshold for failed task count. Example: C<--critical-failed=1> + +=item B<--critical-failed> + +Critical threshold for failed task count. + +=item B<--warning-aborted> + +Warning threshold for aborted task count. + +=item B<--critical-aborted> + +Critical threshold for aborted task count. + +=back + +=cut From 0f81a652d286c888ad9b23cdb2b641debd55b678 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:30:10 +0200 Subject: [PATCH 22/26] Update vmscount.pm --- src/apps/nutanix/prism/mode/vmscount.pm | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/apps/nutanix/prism/mode/vmscount.pm b/src/apps/nutanix/prism/mode/vmscount.pm index 339bb8b43b..d10ff1c9da 100644 --- a/src/apps/nutanix/prism/mode/vmscount.pm +++ b/src/apps/nutanix/prism/mode/vmscount.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -27,7 +27,7 @@ use base qw(centreon::plugins::templates::counter); sub set_counters { my ($self, %options) = @_; - # type => 0 : compteur global (pas d'instance multiple) + # type => 0: single global counter (no per-instance loop) $self->{maps_counters_type} = [ { name => 'global', type => 0 } ]; @@ -83,8 +83,10 @@ sub manage_selection { my $entities = $result->{entities} // []; my $total = scalar(@{$entities}); - my $on = scalar(grep { ($_->{power_state} // '') eq 'on' } @{$entities}); - my $off = $total - $on; + # Prism v2.0 returns power_state as uppercase "ON"/"OFF" — use lc() for a + # case-insensitive comparison so the mode works across API versions. + my $on = scalar(grep { lc($_->{power_state} // '') eq 'on' } @{$entities}); + my $off = $total - $on; $self->{global} = { total => $total, From 7263916dafbca42d530fc2e7cf73b4c122bc7cd5 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:30:41 +0200 Subject: [PATCH 23/26] Update vmsnics.pm --- src/apps/nutanix/prism/mode/vmsnics.pm | 93 ++++++++++++-------------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/src/apps/nutanix/prism/mode/vmsnics.pm b/src/apps/nutanix/prism/mode/vmsnics.pm index 883c2820ed..57f7741e16 100644 --- a/src/apps/nutanix/prism/mode/vmsnics.pm +++ b/src/apps/nutanix/prism/mode/vmsnics.pm @@ -1,5 +1,5 @@ # -# Copyright 2025 Centreon (http://www.centreon.com/) +# Copyright 2026 Centreon (http://www.centreon.com/) # # Centreon is a full-fledged industry-strength solution that meets # the needs in IT infrastructure and application monitoring for @@ -25,7 +25,6 @@ use warnings; use base qw(centreon::plugins::templates::counter); use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); -# ─── Output du statut NIC ──────────────────────────────────────────────────── sub custom_nic_status_output { my ($self, %options) = @_; return sprintf( @@ -52,13 +51,12 @@ sub set_counters { ]; $self->{maps_counters}->{nics} = [ - # ── Statut de connexion du NIC ─────────────────────────────────────── + # NIC connection status { - label => 'status', - type => 2, - # Un NIC non connecté est en warning par défaut - warning_default => '%{connected} ne "connected"', - set => { + label => 'status', + type => 2, + warning_default => '%{connected} ne "connected"', + set => { key_values => [ { name => 'vm_name' }, { name => 'nic_id' }, @@ -70,40 +68,40 @@ sub set_counters { closure_custom_threshold_check => \&catalog_status_threshold_ng, } }, - # ── Trafic entrant (octets/s) — disponible via stats de la VM ──────── + # Inbound traffic (B/s) from VM-level stats { label => 'traffic-in', nlabel => 'vm.nic.traffic.in.bytespersecond', set => { - key_values => [ { name => 'rx_bytes_rate' }, { name => 'nic_id' }, { name => 'vm_name' } ], - output_template => 'traffic in: %s/s', + key_values => [ { name => 'rx_bytes_rate' }, { name => 'nic_id' }, { name => 'vm_name' } ], + output_template => 'traffic in: %s/s', output_change_bytes => 1, perfdatas => [ { - template => '%.2f', - unit => 'B/s', - min => 0, + template => '%.2f', + unit => 'B/s', + min => 0, label_extra_instance => 1, - instance_use => 'nic_id', + instance_use => 'nic_id', } ] } }, - # ── Trafic sortant (octets/s) ──────────────────────────────────────── + # Outbound traffic (B/s) { label => 'traffic-out', nlabel => 'vm.nic.traffic.out.bytespersecond', set => { - key_values => [ { name => 'tx_bytes_rate' }, { name => 'nic_id' }, { name => 'vm_name' } ], - output_template => 'traffic out: %s/s', + key_values => [ { name => 'tx_bytes_rate' }, { name => 'nic_id' }, { name => 'vm_name' } ], + output_template => 'traffic out: %s/s', output_change_bytes => 1, perfdatas => [ { - template => '%.2f', - unit => 'B/s', - min => 0, + template => '%.2f', + unit => 'B/s', + min => 0, label_extra_instance => 1, - instance_use => 'nic_id', + instance_use => 'nic_id', } ] } @@ -135,9 +133,7 @@ sub new { sub manage_selection { my ($self, %options) = @_; - # On itère sur toutes les VMs pour récupérer leurs NICs. - # L'API v2.0 expose les NICs dans la réponse de la liste des VMs - # via le champ vm_nics[] — pas besoin d'un appel par VM. + # NIC data is embedded in the VM list response under vm_nics[] — no per-VM call needed. my $vms_result = $options{custom}->get_vms(); my $vms = $vms_result->{entities} // []; @@ -145,48 +141,47 @@ sub manage_selection { for my $vm (@{$vms}) { my $vm_name = $vm->{name} // $vm->{uuid} // 'unknown'; - my $vm_uuid = $vm->{uuid} // ''; if (defined($self->{option_results}->{filter_vm_name}) && $self->{option_results}->{filter_vm_name} ne '') { next if $vm_name !~ /$self->{option_results}->{filter_vm_name}/; } - my $nics = $vm->{vm_nics} // []; - my $stats = $vm->{stats} // {}; + my $nics = $vm->{vm_nics} // []; + my $stats = $vm->{stats} // {}; - # Les stats réseau sont agrégées au niveau VM dans v2.0. - # network_received_bytes et network_transmitted_bytes sont en octets cumulés ; - # Centreon n'a pas d'état persistant ici, on utilise les valeurs "rate" si dispo. - # Si absent, on met 0 (non disponible). - my $rx_rate = $stats->{'nic.received_bytes_rate'} // 0; - my $tx_rate = $stats->{'nic.transmitted_bytes_rate'} // 0; + # In API v2.0, network traffic stats are VM-level aggregates, not per-NIC. + # Attribute the rate to the first physical NIC (index 0); others get 0. + my $rx_rate = $stats->{'nic.received_bytes_rate'} // 0; + my $tx_rate = $stats->{'nic.transmitted_bytes_rate'} // 0; my $nic_index = 0; for my $nic (@{$nics}) { - my $mac = $nic->{mac_address} // 'unknown'; - my $network = $nic->{network_name} // $nic->{vlan_id} // 'N/A'; - my $nic_id = $vm_name . '_nic' . $nic_index; + my $mac = $nic->{mac_address} // 'unknown'; + my $network = $nic->{network_name} // $nic->{vlan_id} // 'N/A'; + # Filters: skipped NICs still advance nic_index to preserve physical position. if (defined($self->{option_results}->{filter_mac}) && $self->{option_results}->{filter_mac} ne '') { - $nic_index++; - next if $mac !~ /$self->{option_results}->{filter_mac}/i; + if ($mac !~ /$self->{option_results}->{filter_mac}/i) { + $nic_index++; + next; + } } if (defined($self->{option_results}->{filter_network}) && $self->{option_results}->{filter_network} ne '') { - $nic_index++; - next if $network !~ /$self->{option_results}->{filter_network}/; + if ($network !~ /$self->{option_results}->{filter_network}/) { + $nic_index++; + next; + } } - # is_connected est un booléen dans l'API Nutanix v2.0 + my $nic_id = $vm_name . '_nic' . $nic_index; my $connected = (defined($nic->{is_connected}) && $nic->{is_connected}) ? 'connected' : 'disconnected'; $self->{nics}->{$nic_id} = { - vm_name => $vm_name, - nic_id => $nic_id, - mac => $mac, - network => $network, - connected => $connected, - # Les rates réseau ne sont pas par NIC dans v2.0 — on les attribue - # au premier NIC de la VM (index 0). Les autres NIC ont 0. + vm_name => $vm_name, + nic_id => $nic_id, + mac => $mac, + network => $network, + connected => $connected, rx_bytes_rate => ($nic_index == 0) ? $rx_rate : 0, tx_bytes_rate => ($nic_index == 0) ? $tx_rate : 0, }; From 4f8130a079b3fd43b7df8a754fc1ce7ed9c254e3 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:31:27 +0200 Subject: [PATCH 24/26] Create vmsperformance.pm --- src/apps/nutanix/prism/mode/vmsperformance.pm | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/apps/nutanix/prism/mode/vmsperformance.pm diff --git a/src/apps/nutanix/prism/mode/vmsperformance.pm b/src/apps/nutanix/prism/mode/vmsperformance.pm new file mode 100644 index 0000000000..ecfa0a4e39 --- /dev/null +++ b/src/apps/nutanix/prism/mode/vmsperformance.pm @@ -0,0 +1,215 @@ +# +# Copyright 2026 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package apps::nutanix::prism::mode::vmsperformance; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +sub custom_status_output { + my ($self, %options) = @_; + return sprintf( + "VM '%s' power state is '%s'", + $self->{result_values}->{name}, + $self->{result_values}->{power_state} + ); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'vms', + type => 1, + cb_prefix_output => 'prefix_vm_output', + message_multiple => 'All VMs are OK', + skipped_code => { -10 => 1 }, + } + ]; + + $self->{maps_counters}->{vms} = [ + # Power state status + { + label => 'status', + type => 2, + warning_default => '%{power_state} ne "ON"', + set => { + key_values => [ + { name => 'name' }, + { name => 'power_state' }, + ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_threshold_check => \&catalog_status_threshold_ng, + } + }, + # CPU usage percentage (hypervisor_cpu_usage_ppm / 10000) + { + label => 'cpu-usage', + nlabel => 'vm.cpu.usage.percentage', + set => { + key_values => [ { name => 'cpu_usage_pct' }, { name => 'name' } ], + output_template => 'CPU usage: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + max => 100, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + # Memory usage percentage + { + label => 'memory-usage', + nlabel => 'vm.memory.usage.percentage', + set => { + key_values => [ { name => 'memory_usage_pct' }, { name => 'name' } ], + output_template => 'memory usage: %.2f%%', + perfdatas => [ + { + template => '%.2f', + unit => '%', + min => 0, + max => 100, + label_extra_instance => 1, + instance_use => 'name', + } + ] + } + }, + ]; +} + +sub prefix_vm_output { + my ($self, %options) = @_; + return "VM '" . $options{instance_value}->{name} . "' "; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options( + arguments => { + 'filter-name:s' => { name => 'filter_name' }, + 'filter-state:s' => { name => 'filter_state' }, + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_vms(); + my $entities = $result->{entities} // []; + + $self->{vms} = {}; + for my $vm (@{$entities}) { + my $name = $vm->{name} // $vm->{uuid} // 'unknown'; + my $power_state = $vm->{power_state} // 'UNKNOWN'; + + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_name}/; + } + if (defined($self->{option_results}->{filter_state}) && $self->{option_results}->{filter_state} ne '') { + next if $power_state !~ /$self->{option_results}->{filter_state}/i; + } + + my $stats = $vm->{stats} // {}; + # CPU: hypervisor_cpu_usage_ppm in parts-per-million → divide by 10000 for %. + my $cpu_pct = ($stats->{hypervisor_cpu_usage_ppm} // 0) / 10000; + # Memory: try guest_memory_usage_ppm (guest OS view) then memory_usage_ppm (hypervisor view). + my $mem_pct = ($stats->{guest_memory_usage_ppm} // $stats->{memory_usage_ppm} // 0) / 10000; + + # Key on UUID for uniqueness; fall back to name if uuid is absent. + my $key = $vm->{uuid} // $name; + $self->{vms}->{$key} = { + name => $name, + power_state => $power_state, + cpu_usage_pct => $cpu_pct, + memory_usage_pct => $mem_pct, + }; + } + + if (scalar(keys %{$self->{vms}}) == 0) { + $self->{output}->add_option_msg(short_msg => 'No VM found (check filters).'); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Monitor Nutanix VM CPU and memory usage through Prism REST API. + +Stats are retrieved from the VM list endpoint — no extra per-VM API call. + +=over 8 + +=item B<--filter-name> + +Filter VMs by name (regexp). Example: C<--filter-name='^prod-'> + +=item B<--filter-state> + +Filter VMs by power state (case-insensitive regexp). Example: C<--filter-state='^ON$'> + +=item B<--warning-status> + +Warning threshold for VM power state. +Default: C<%{power_state} ne "ON"> + +Variables: C<%{name}>, C<%{power_state}> + +=item B<--critical-status> + +Critical threshold for VM power state. + +=item B<--warning-cpu-usage> + +Warning threshold for CPU usage (%). Example: C<--warning-cpu-usage=80> + +=item B<--critical-cpu-usage> + +Critical threshold for CPU usage (%). Example: C<--critical-cpu-usage=90> + +=item B<--warning-memory-usage> + +Warning threshold for memory usage (%). + +=item B<--critical-memory-usage> + +Critical threshold for memory usage (%). + +=back + +=cut From 66d1fe8a1eac21a2b5152d4ed217cb5947289873 Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:33:13 +0200 Subject: [PATCH 25/26] Update api.pm --- src/apps/nutanix/prism/custom/api.pm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/apps/nutanix/prism/custom/api.pm b/src/apps/nutanix/prism/custom/api.pm index e615c62d47..46d4921561 100644 --- a/src/apps/nutanix/prism/custom/api.pm +++ b/src/apps/nutanix/prism/custom/api.pm @@ -165,7 +165,10 @@ sub get_hosts { # Returns the list of virtual machines (includes stats fields) sub get_vms { my ($self, %options) = @_; - return $self->request_api(endpoint => '/api/nutanix/v2.0/vms'); + return $self->request_api( + endpoint => '/api/nutanix/v2.0/vms', + get_param => [ 'count=2147483647' ], + ); } # Returns storage pools From 96a5dfe4c3fcbc261fb04f5f0d7650d436cb84ab Mon Sep 17 00:00:00 2001 From: psame <44295022+psamecentreon@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:35:23 +0200 Subject: [PATCH 26/26] Update vmsperformance.pm --- src/apps/nutanix/prism/mode/vmsperformance.pm | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/apps/nutanix/prism/mode/vmsperformance.pm b/src/apps/nutanix/prism/mode/vmsperformance.pm index ecfa0a4e39..33a17fc88e 100644 --- a/src/apps/nutanix/prism/mode/vmsperformance.pm +++ b/src/apps/nutanix/prism/mode/vmsperformance.pm @@ -27,11 +27,7 @@ use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ sub custom_status_output { my ($self, %options) = @_; - return sprintf( - "VM '%s' power state is '%s'", - $self->{result_values}->{name}, - $self->{result_values}->{power_state} - ); + return sprintf("power state is '%s'", $self->{result_values}->{power_state}); } sub set_counters { @@ -144,8 +140,7 @@ sub manage_selection { my $stats = $vm->{stats} // {}; # CPU: hypervisor_cpu_usage_ppm in parts-per-million → divide by 10000 for %. my $cpu_pct = ($stats->{hypervisor_cpu_usage_ppm} // 0) / 10000; - # Memory: try guest_memory_usage_ppm (guest OS view) then memory_usage_ppm (hypervisor view). - my $mem_pct = ($stats->{guest_memory_usage_ppm} // $stats->{memory_usage_ppm} // 0) / 10000; + my $mem_pct = ($stats->{hypervisor_memory_usage_ppm} // 0) / 10000; # Key on UUID for uniqueness; fall back to name if uuid is absent. my $key = $vm->{uuid} // $name;