diff --git a/src/apps/nutanix/prism/custom/api.pm b/src/apps/nutanix/prism/custom/api.pm new file mode 100644 index 0000000000..46d4921561 --- /dev/null +++ b/src/apps/nutanix/prism/custom/api.pm @@ -0,0 +1,293 @@ +# +# 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::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 REST API base: port 9440, prefix /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, # Nutanix deployments commonly use self-signed certificates + ); + + 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; +} + +# Returns cluster information +sub get_clusters { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/clusters'); +} + +# Returns the list of physical hosts +sub get_hosts { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/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', + get_param => [ 'count=2147483647' ], + ); +} + +# Returns storage pools +sub get_storage_pools { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/storage_pools'); +} + +# Returns all physical disks in the cluster +sub get_disks { + my ($self, %options) = @_; + return $self->request_api(endpoint => '/api/nutanix/v2.0/disks'); +} + +# Returns snapshots — optionally filtered by 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'); +} + +# Returns NICs for a specific VM +sub get_vm_nics { + my ($self, %options) = @_; + return $self->request_api( + endpoint => '/api/nutanix/v2.0/vms/' . $options{vm_uuid} . '/nics' + ); +} + +# Returns active (unresolved) alerts; optional severity filter +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, + ); +} + +# 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__ + +=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/alerts.pm b/src/apps/nutanix/prism/mode/alerts.pm new file mode 100644 index 0000000000..0035ef5602 --- /dev/null +++ b/src/apps/nutanix/prism/mode/alerts.pm @@ -0,0 +1,287 @@ +# +# 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::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); + +# Nutanix Prism v2.0 API severity values and their Centreon equivalents +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} = [ + # Global counters: alert count per severity + { name => 'global', type => 0 }, + # Per-alert counters: individual status for 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, + # Default: each critical alert triggers CRITICAL, warning triggers 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' }, + # Minimum alert age in seconds; younger alerts are ignored + '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}) { + # Normalize severity: kCritical → critical + my $raw_sev = $alert->{severity} // 'kInfo'; + my $severity = $SEVERITY_MAP{$raw_sev} // 'info'; + + # Title: reconstructed from alert_title, message, or check_id + my $title = $alert->{alert_title} // $alert->{message} // $alert->{check_id} // 'N/A'; + + # Affected entity: scan context_types for a recognizable entity name key + 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; + } + } + + # 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"; + + 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..65d5f8318e --- /dev/null +++ b/src/apps/nutanix/prism/mode/capacity.pm @@ -0,0 +1,277 @@ +# +# 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::capacity; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); + +# Aggregates cluster-wide CPU, RAM, and storage capacity +# by consolidating data from all hosts and 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 (allocated vCPUs vs physical core capacity) + $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 } + ], + } + }, + ]; + + # Memory (bytes) + $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 } + ], + } + }, + ]; + + # Storage (bytes) + $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) = @_; + + # Aggregate CPU and RAM data from all hosts + 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}) { + $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; + + 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; + + # 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); + } + + 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, + }; + + # Aggregate storage data from all 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/clusterstatus.pm b/src/apps/nutanix/prism/mode/clusterstatus.pm new file mode 100644 index 0000000000..d0b1d87523 --- /dev/null +++ b/src/apps/nutanix/prism/mode/clusterstatus.pm @@ -0,0 +1,173 @@ +# +# 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::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} = [ + # Status counter (type 2 evaluates a threshold expression) + # Prism v2.0 returns cluster_state="NORMAL" for a healthy cluster. + { + label => 'status', + type => 2, + warning_default => '%{state} ne "NORMAL"', + 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, + } + }, + # Node count + { + 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) = @_; + + my $result = $options{custom}->get_clusters(); + my $entities = $result->{entities} // []; + + $self->{clusters} = {}; + for my $cluster (@{$entities}) { + my $name = $cluster->{name} // 'unknown'; + + if (defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '') { + next if $name !~ /$self->{option_results}->{filter_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, + 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 "NORMAL"> + +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/disksstatus.pm b/src/apps/nutanix/prism/mode/disksstatus.pm new file mode 100644 index 0000000000..26a9e5601e --- /dev/null +++ b/src/apps/nutanix/prism/mode/disksstatus.pm @@ -0,0 +1,257 @@ +# +# 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::disksstatus; + +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( + "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} = [ + # Operational status (type 2 evaluates a threshold expression against key_values) + { + label => 'status', + type => 2, + 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, + } + }, + # Total disk capacity in bytes + { + 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', + } + ] + } + }, + # Free space in bytes + { + 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', + } + ] + } + }, + # Usage percentage + { + 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}; + + # 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, + # 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, + 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/healthchecks.pm b/src/apps/nutanix/prism/mode/healthchecks.pm new file mode 100644 index 0000000000..bb7342f1e5 --- /dev/null +++ b/src/apps/nutanix/prism/mode/healthchecks.pm @@ -0,0 +1,268 @@ +# +# 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::healthchecks; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +# 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 = ( + 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} = [ + # Global summary: check counts by result state + { name => 'global', type => 0 }, + # Individual result per 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' }, + # Only report non-PASS checks to reduce noise on large clusters + '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'; + + 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; + } + + # 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}}) { + $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'; + + # 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]; + my $causes = join('; ', map { $_->{message} // '' } @{ $last->{execution_results} // [] }); + $message = $causes if $causes ne ''; + } + $message = 'no detail' if $message eq ''; + + # 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); + $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/hostsusage.pm b/src/apps/nutanix/prism/mode/hostsusage.pm new file mode 100644 index 0000000000..532dc6edd1 --- /dev/null +++ b/src/apps/nutanix/prism/mode/hostsusage.pm @@ -0,0 +1,238 @@ +# +# 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::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} = [ + # Host operational state + { + 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, + } + }, + # CPU usage percentage (stats field is in parts-per-million; divide by 10000) + { + 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', + } + ] + } + }, + # Memory usage percentage + { + 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', + } + ] + } + }, + # Number of VMs running on this host + { + 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}/; + } + + 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 => $state, + 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..2034c6318f --- /dev/null +++ b/src/apps/nutanix/prism/mode/listhosts.pm @@ -0,0 +1,102 @@ +# +# 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::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(); +} + +# 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']); +} + +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/listnics.pm b/src/apps/nutanix/prism/mode/listnics.pm new file mode 100644 index 0000000000..02936d2b09 --- /dev/null +++ b/src/apps/nutanix/prism/mode/listnics.pm @@ -0,0 +1,167 @@ +# +# 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::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); +} + +# Collect all NICs from the VM list (vm_nics[] is included in v2.0 responses). +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'; + + # 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 '') { + if ($network !~ /$self->{option_results}->{filter_network}/) { + $nic_index++; + next; + } + } + + 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/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 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 diff --git a/src/apps/nutanix/prism/mode/listvms.pm b/src/apps/nutanix/prism/mode/listvms.pm new file mode 100644 index 0000000000..2f287c4c6b --- /dev/null +++ b/src/apps/nutanix/prism/mode/listvms.pm @@ -0,0 +1,102 @@ +# +# 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::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/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 diff --git a/src/apps/nutanix/prism/mode/snapshots.pm b/src/apps/nutanix/prism/mode/snapshots.pm new file mode 100644 index 0000000000..164d4e4f44 --- /dev/null +++ b/src/apps/nutanix/prism/mode/snapshots.pm @@ -0,0 +1,232 @@ +# +# 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::snapshots; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use POSIX qw(floor); + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + # Global counters (all VMs combined) + { + name => 'global', + type => 0, + skipped_code => { -10 => 1 }, + }, + # Per-VM counters + { + 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} = [ + # Total snapshot count across the 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} = [ + # Snapshot count per 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', + } + ] + } + }, + # 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' } ], + closure_custom_output => \&custom_oldest_age_output, + perfdatas => [ + { + template => '%d', + unit => 's', + min => 0, + label_extra_instance => 1, + instance_use => 'vm_name', + } + ] + } + }, + ]; +} + +# 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}; + 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' }, + # Note: --warning-oldest-age and --critical-oldest-age are auto-generated + # by the counter framework from label => 'oldest-age'. Do not declare them here. + } + ); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $result = $options{custom}->get_snapshots(); + my $entities = $result->{entities} // []; + + # Group snapshots by VM name + 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; + + # 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_in_usecs}; + next unless defined($ts) && $ts > 0; + $ts = int($ts / 1_000_000); # microseconds → seconds + $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/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 diff --git a/src/apps/nutanix/prism/mode/storageusage.pm b/src/apps/nutanix/prism/mode/storageusage.pm new file mode 100644 index 0000000000..aa413f1b93 --- /dev/null +++ b/src/apps/nutanix/prism/mode/storageusage.pm @@ -0,0 +1,196 @@ +# +# 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::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} = [ + # Used bytes + { + label => 'usage', + nlabel => 'storage.pool.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.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', + } + ] + } + }, + # Computed usage percentage + { + 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}/; + } + + my $capacity = $pool->{capacity_bytes} // 0; + my $used = $pool->{usage_bytes} // 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, + 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/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 diff --git a/src/apps/nutanix/prism/mode/vmscount.pm b/src/apps/nutanix/prism/mode/vmscount.pm new file mode 100644 index 0000000000..d10ff1c9da --- /dev/null +++ b/src/apps/nutanix/prism/mode/vmscount.pm @@ -0,0 +1,134 @@ +# +# 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::vmscount; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); + +sub set_counters { + my ($self, %options) = @_; + + # type => 0: single global counter (no per-instance loop) + $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}); + # 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, + 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/mode/vmsnics.pm b/src/apps/nutanix/prism/mode/vmsnics.pm new file mode 100644 index 0000000000..57f7741e16 --- /dev/null +++ b/src/apps/nutanix/prism/mode/vmsnics.pm @@ -0,0 +1,254 @@ +# +# 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::vmsnics; + +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +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} = [ + # NIC connection status + { + label => 'status', + type => 2, + 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, + } + }, + # 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', + output_change_bytes => 1, + perfdatas => [ + { + template => '%.2f', + unit => 'B/s', + min => 0, + label_extra_instance => 1, + instance_use => 'nic_id', + } + ] + } + }, + # 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', + 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) = @_; + + # 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} // []; + + $self->{nics} = {}; + + for my $vm (@{$vms}) { + my $vm_name = $vm->{name} // $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}/; + } + + my $nics = $vm->{vm_nics} // []; + my $stats = $vm->{stats} // {}; + + # 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'; + + # Filters: skipped NICs still advance nic_index to preserve physical position. + if (defined($self->{option_results}->{filter_mac}) && $self->{option_results}->{filter_mac} ne '') { + if ($mac !~ /$self->{option_results}->{filter_mac}/i) { + $nic_index++; + next; + } + } + if (defined($self->{option_results}->{filter_network}) && $self->{option_results}->{filter_network} ne '') { + if ($network !~ /$self->{option_results}->{filter_network}/) { + $nic_index++; + next; + } + } + + 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, + 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/mode/vmsperformance.pm b/src/apps/nutanix/prism/mode/vmsperformance.pm new file mode 100644 index 0000000000..33a17fc88e --- /dev/null +++ b/src/apps/nutanix/prism/mode/vmsperformance.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::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("power state is '%s'", $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; + 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; + $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 diff --git a/src/apps/nutanix/prism/plugin.pm b/src/apps/nutanix/prism/plugin.pm new file mode 100644 index 0000000000..aff71421b5 --- /dev/null +++ b/src/apps/nutanix/prism/plugin.pm @@ -0,0 +1,67 @@ +# +# 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::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} = { + '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'; + return $self; +} + +1; + +__END__ + +=head1 PLUGIN DESCRIPTION + +Monitor Nutanix infrastructure through Prism REST API. + +=cut