Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": [
"libjson-perl"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"pkg_name": "centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi",
"pkg_summary": "Centreon Plugin to monitor Vates XCP-ng hosts using Xen Orchestra REST API",
"plugin_name": "centreon_vates_xcpng_host_restapi.pl",
"files": [
"centreon/plugins/script_custom.pm",
"apps/virtualization/vates/host/",
"apps/virtualization/vates/custom/"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"dependencies": [
"perl(JSON)"
]
}
53 changes: 53 additions & 0 deletions src/apps/virtualization/vates/custom/api.pm
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ sub request_api_get {
return json_decode($content, booleans_as_strings => 1);
}

# the XO REST API's 'filter=field:value' does a substring match, not an exact one
# (e.g. 'name_label:vates' also matches 'vates2'/'vates3'), so any lookup expected to
# return exactly one object must narrow the candidates down to the exact match itself.
sub _narrow_to_exact_match {
my ($self, %options) = @_;

return $options{response} if (!defined($options{response}) or ref($options{response}) ne "ARRAY" or scalar(@{$options{response}}) <= 1);

my @exact = grep { defined($_->{$options{field}}) and $_->{$options{field}} eq $options{value} } @{$options{response}};
return \@exact if (scalar(@exact) == 1);
return $options{response};
}

# get_name_and_uuid( type => 'pool', 'api_endpoint' => 'pools');
# check --{type}-name and --{type}-uuid and retrieve the other value from the api.
# it caches the mapping on disk for --reload-cache-time window second
Expand Down Expand Up @@ -131,10 +144,12 @@ sub get_name_and_uuid {
or (time() - $last_timestamp) > ($self->{option_results}->{reload_cache_time} * 60)
) {
my $filter = '';
my ($match_field, $match_value) = ('uuid', $self->{option_results}->{$obj_uuid});

if (is_not_empty($self->{option_results}->{$obj_name})) {

$filter = "name_label:" . $self->{option_results}->{$obj_name};
($match_field, $match_value) = ('name_label', $self->{option_results}->{$obj_name});
}
else {
$filter = "uuid:" . $self->{option_results}->{$obj_uuid};
Expand All @@ -143,6 +158,7 @@ sub get_name_and_uuid {
endpoint => $options{api_endpoint} // $options{type},
get_param => [ 'fields=uuid,name_label', 'filter=' . $filter]
);
$response = $self->_narrow_to_exact_match(response => $response, field => $match_field, value => $match_value);
if (!defined($response) or ref($response) ne 'ARRAY' or scalar @$response != 1) {
$self->{output}->option_exit(short_msg => "no $options{type} found, api did not return an array with one element. Please check --$options{type}-uuid and --$options{type}-name parameter or --debug.");
}
Expand Down Expand Up @@ -200,19 +216,48 @@ sub get_vm_info {
my $fields = "name_label,power_state,uuid,os_version";

# default filter use uuid, or name if not present.
my ($match_field, $match_value) = ('uuid', $self->{option_results}->{vm_uuid});
my $filter = "uuid:". $self->{option_results}->{vm_uuid};
if (is_empty($self->{option_results}->{vm_uuid})){
$filter = "name_label:". $self->{option_results}->{vm_name};
($match_field, $match_value) = ('name_label', $self->{option_results}->{vm_name});
}
my $response = $self->request_api_get(
endpoint => "vms",
get_param => [ "fields=" . $fields, "filter=" . $filter ],
);
$response = $self->_narrow_to_exact_match(response => $response, field => $match_field, value => $match_value);
if (!defined($response) or ref($response) ne "ARRAY" or scalar @$response != 1){
$self->{output}->option_exit(short_msg => "no vm found, api did not return an array with one element. Please check --vm-uuid and --vm-name parameter or --debug.");
}
return $response->[0];
}

# used by the host modes to get one host's data, resolving --host-uuid or --host-name.
# %options input :
# fields: comma separated list of fields to request from the API (defaults to the minimal status fields)
sub get_host_info {
my ($self, %options) = @_;

my $fields = $options{fields} // "name_label,enabled,power_state,uuid";

# default filter use uuid, or name if not present.
my ($match_field, $match_value) = ('uuid', $self->{option_results}->{host_uuid});
my $filter = "uuid:" . $self->{option_results}->{host_uuid};
if (is_empty($self->{option_results}->{host_uuid})) {
$filter = "name_label:" . $self->{option_results}->{host_name};
($match_field, $match_value) = ('name_label', $self->{option_results}->{host_name});
}
my $response = $self->request_api_get(
endpoint => "hosts",
get_param => [ "fields=" . $fields, "filter=" . $filter ],
);
$response = $self->_narrow_to_exact_match(response => $response, field => $match_field, value => $match_value);
if (!defined($response) or ref($response) ne "ARRAY" or scalar @$response != 1){
$self->{output}->option_exit(short_msg => "no host found, api did not return an array with one element. Please check --host-uuid and --host-name parameter or --debug.");
}
return $response->[0];
}
1;

__END__
Expand Down Expand Up @@ -260,6 +305,14 @@ Resolves C<--vm-uuid>/C<--vm-name> and returns the matching VM's C<name_label>,
C<uuid> and C<os_version> fields. Always live (not cached), since C<power_state> can change at
any time.

=head2 get_host_info

my $host = $api->get_host_info(fields => 'name_label,enabled,power_state,memory');

Resolves C<--host-uuid>/C<--host-name> and returns the matching host's fields (the caller picks
which fields to request, since the status/cpu/memory host modes each need a different subset).
Always live (not cached).

=head1 REST API OPTIONS

Command-line options for the Vates Xen Orchestra API:
Expand Down
174 changes: 174 additions & 0 deletions src/apps/virtualization/vates/host/mode/cpu.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#
# Copyright 2026-Present 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::virtualization::vates::host::mode::cpu;
use strict;
use warnings;
use base qw(centreon::plugins::templates::counter);
use centreon::plugins::misc qw/is_empty/;
use centreon::plugins::constants qw(:counters :values);

sub new {
my ($class, %options) = @_;

my $self = $class->SUPER::new(package => __PACKAGE__, force_new_perfdata => 1, %options);

$options{options}->add_options(
arguments => {
'host-uuid:s' => { name => 'host_uuid', default => '' },
'host-name:s' => { name => 'host_name', default => '' }
}
);

return $self;
}

sub check_options {
my ($self, %options) = @_;

if (is_empty($options{option_results}->{host_uuid}) and is_empty($options{option_results}->{host_name})) {
$self->{output}->option_exit(short_msg => "you must fill either --host-uuid or --host-name.");
}
$self->SUPER::check_options(%options);
}

sub set_counters {
my ($self, %options) = @_;

$self->{maps_counters_type} = [
{ name => 'cpu', type => COUNTER_TYPE_GLOBAL }
];

$self->{maps_counters}->{cpu} = [
{
label => 'cpu-usage-prct',
type => COUNTER_KIND_METRIC,
nlabel => 'host.cpu.usage.percentage',
warning_default => '80',
critical_default => '95',
set => {
key_values => [ { name => 'prct_used' }, { name => 'display' } ],
output_template => 'CPU usage is %.2f %%',
perfdatas => [
{ value => 'prct_used', template => '%.2f', min => 0, max => 100, unit => '%' }
]
}
},
{
label => 'cpu-count',
type => COUNTER_KIND_METRIC,
nlabel => 'host.cpu.count',
set => {
key_values => [ { name => 'cpu_count' }, { name => 'display' } ],
output_template => '%s pCPU(s)',
perfdatas => [
{ value => 'cpu_count', template => '%d', min => 0 }
]
}
}
];
}

sub manage_selection {
my ($self, %options) = @_;

# get_name_and_uuid only resolves identity (uuid/name_label) and caches it on disk, unlike
# get_host_info which always hits the API live. We don't need any other host field here: the
# "is the host actually reachable" check is deferred to the /stats call itself below.
my $host = $options{custom}->get_name_and_uuid(type => "host", api_endpoint => "hosts");

# silently_fail lets us read the (non-200) error body ourselves instead of the http layer
# auto-exiting with a generic "500 ..." UNKNOWN: a disabled/halted host makes this endpoint
# fail with a XAPI "HOST_OFFLINE" error, which we can report with a clearer message.
my $host_stats = $options{custom}->request_api_get(endpoint => 'hosts/' . $host->{uuid} . '/stats', silently_fail => 1);

if (defined($host_stats->{error})) {
$self->{output}->option_exit(short_msg => "host '" . $host->{name_label} . "' is not enabled/running, can not get CPU usage data (" . $host_stats->{error} . ").");
}

if (
!defined($host_stats->{stats})
or !defined($host_stats->{stats}->{cpus})
or ref($host_stats->{stats}->{cpus}) ne "HASH"
or scalar(keys %{$host_stats->{stats}->{cpus}}) == 0
) {
$self->{output}->option_exit(short_msg => "Field cpus not found in API response for host '" . $host->{name_label} . "'. Please check --debug or the Swagger documentation.");
}

# the API returns one time series (percentage) per physical core, the last value of each
# series is the most recent one. The aggregated usage is the average of all cores, and the
# number of series is itself the live physical CPU count.
my $total = 0;
my $cores = 0;
for my $core (keys %{$host_stats->{stats}->{cpus}}) {
my $serie = $host_stats->{stats}->{cpus}->{$core};
next if (ref($serie) ne "ARRAY" or scalar @$serie == 0);
$total += $serie->[-1];
$cores++;
}
if ($cores == 0) {
$self->{output}->option_exit(short_msg => "Field cpus is empty in API response for host '" . $host->{name_label} . "'. Please check --debug or the Swagger documentation.");
}

$self->{cpu} = {
display => $host->{name_label},
prct_used => $total / $cores,
cpu_count => $cores
};
}

1;

__END__

=head1 MODE

Check the aggregated CPU usage of a Vates XCP-ng host (average of the last real-time value of
every physical core, as reported by C<< GET /hosts/<uuid>/stats >>).

=over 8

=item B<--host-uuid>

Identify the host by its exact uuid.

=item B<--host-name>

Identify the host by its name (only one host is expected).

=item B<--warning-cpu-usage-prct>

Threshold. Default: 80

=item B<--critical-cpu-usage-prct>

Threshold. Default: 95

=item B<--warning-cpu-count>

Threshold.

=item B<--critical-cpu-count>

Threshold.

=back

=cut
Loading
Loading