From 1a318f144d70fee8d3918f3358acc52f4f8c4864 Mon Sep 17 00:00:00 2001 From: EvanAdam Date: Tue, 8 Sep 2026 13:29:52 +0200 Subject: [PATCH 1/7] feat(vates): add host plugin skeleton (status, cpu, memory, discovery) Refs:CTOR-2517 Adds a new apps::virtualization::vates::host plugin with the 3 host service modes from the spec (status, cpu, memory) plus a discovery mode to auto-create Centreon hosts from XO's /hosts list. Extends the shared custom/api.pm with a generic get_host_info() (parameterized fields, mirroring get_vm_info but reused across the 3 modes instead of duplicated). --- .../deb.json | 5 + .../pkg.json | 10 + .../rpm.json | 5 + src/apps/virtualization/vates/custom/api.pm | 31 +++ .../virtualization/vates/host/mode/cpu.pm | 173 ++++++++++++++++ .../vates/host/mode/discovery.pm | 131 ++++++++++++ .../virtualization/vates/host/mode/memory.pm | 189 ++++++++++++++++++ .../virtualization/vates/host/mode/status.pm | 135 +++++++++++++ src/apps/virtualization/vates/host/plugin.pm | 50 +++++ tests/resources/spellcheck/stopwords.txt | 2 + 10 files changed, 731 insertions(+) create mode 100644 packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/deb.json create mode 100644 packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/pkg.json create mode 100644 packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/rpm.json create mode 100644 src/apps/virtualization/vates/host/mode/cpu.pm create mode 100644 src/apps/virtualization/vates/host/mode/discovery.pm create mode 100644 src/apps/virtualization/vates/host/mode/memory.pm create mode 100644 src/apps/virtualization/vates/host/mode/status.pm create mode 100644 src/apps/virtualization/vates/host/plugin.pm diff --git a/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/deb.json b/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/deb.json new file mode 100644 index 0000000000..540c269f3b --- /dev/null +++ b/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/deb.json @@ -0,0 +1,5 @@ +{ + "dependencies": [ + "libjson-perl" + ] +} diff --git a/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/pkg.json b/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/pkg.json new file mode 100644 index 0000000000..10ca4f0a4c --- /dev/null +++ b/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/pkg.json @@ -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/" + ] +} diff --git a/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/rpm.json b/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/rpm.json new file mode 100644 index 0000000000..1869614605 --- /dev/null +++ b/packaging/centreon-plugin-Virtualization-Vates-XCPng-Host-Restapi/rpm.json @@ -0,0 +1,5 @@ +{ + "dependencies": [ + "perl(JSON)" + ] +} diff --git a/src/apps/virtualization/vates/custom/api.pm b/src/apps/virtualization/vates/custom/api.pm index 4daef74ee6..1c008ad661 100644 --- a/src/apps/virtualization/vates/custom/api.pm +++ b/src/apps/virtualization/vates/custom/api.pm @@ -213,6 +213,29 @@ sub get_vm_info { } 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 $filter = "uuid:" . $self->{option_results}->{host_uuid}; + if (is_empty($self->{option_results}->{host_uuid})) { + $filter = "name_label:" . $self->{option_results}->{host_name}; + } + my $response = $self->request_api_get( + endpoint => "hosts", + get_param => [ "fields=" . $fields, "filter=" . $filter ], + ); + 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__ @@ -260,6 +283,14 @@ Resolves C<--vm-uuid>/C<--vm-name> and returns the matching VM's C, C and C fields. Always live (not cached), since C 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: diff --git a/src/apps/virtualization/vates/host/mode/cpu.pm b/src/apps/virtualization/vates/host/mode/cpu.pm new file mode 100644 index 0000000000..4ec9d9ad7b --- /dev/null +++ b/src/apps/virtualization/vates/host/mode/cpu.pm @@ -0,0 +1,173 @@ +# +# 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_TYPE_INSTANCE, + 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_TYPE_INSTANCE, + 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) = @_; + + my $host = $options{custom}->get_host_info(fields => "name_label,enabled,power_state,uuid,CPUs"); + + if ($host->{enabled} ne 'true' or $host->{power_state} ne 'Running') { + $self->{output}->option_exit(short_msg => "host '" . $host->{name_label} . "' is not enabled/running, can not get CPU usage data."); + } + + my $cpu_count = 0; + if (defined($host->{CPUs}) and ref($host->{CPUs}) eq "HASH" and defined($host->{CPUs}->{cpu_count})) { + $cpu_count = $host->{CPUs}->{cpu_count} + 0; + } + if ($cpu_count == 0) { + $self->{output}->option_exit(short_msg => "host '" . $host->{name_label} . "' reports a CPU count of 0, inconsistent data."); + } + + my $host_stats = $options{custom}->request_api_get(endpoint => 'hosts/' . $host->{uuid} . '/stats'); + + 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. + 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 => $cpu_count + }; +} + +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//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 warning for the CPU usage percentage. +Default: 80 + +=item B<--critical-cpu-usage-prct> + +Threshold critical for the CPU usage percentage. +Default: 95 + +=item B<--warning-cpu-count> / B<--critical-cpu-count> + +Threshold on the number of physical CPUs reported by the host. Informative counter, no default. + +=back + +=cut diff --git a/src/apps/virtualization/vates/host/mode/discovery.pm b/src/apps/virtualization/vates/host/mode/discovery.pm new file mode 100644 index 0000000000..4168d687fc --- /dev/null +++ b/src/apps/virtualization/vates/host/mode/discovery.pm @@ -0,0 +1,131 @@ +# +# 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::discovery; + +use base qw(centreon::plugins::mode); + +use strict; +use warnings; +use centreon::plugins::misc qw(json_encode is_excluded is_not_empty is_empty); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options(arguments => { + 'prettify' => { name => 'prettify' }, + 'filter-power-states:s' => { name => 'filter_power_states' }, + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); +} + +sub run { + my ($self, %options) = @_; + + my $disco_stats; + $disco_stats->{start_time} = time(); + my $hosts = $options{custom}->request_api_get(endpoint => "hosts", get_param => ["fields=*"]); + + $disco_stats->{results} = []; + foreach my $host (@{$hosts}) { + my $host_disco = {}; + next if is_excluded($host->{power_state}, $self->{option_results}->{filter_power_states}); + + # change the keys to match the host discovery provider's attributes + $host_disco->{host_name} = delete $host->{name_label}; + $host_disco->{host_uuid} = delete $host->{uuid}; + $host_disco->{name_description} = delete $host->{name_description}; + $host_disco->{enabled} = $host->{enabled} // ''; + $host_disco->{power_state} = $host->{power_state} // ''; + $host_disco->{address} = $host->{address} // ''; + $host_disco->{pool_uuid} = $host->{'$pool'} // ''; + + $host_disco->{manufacturer} = ''; + $host_disco->{product_name} = ''; + if (is_not_empty($host->{bios_strings}) and ref($host->{bios_strings}) eq "HASH") { + $host_disco->{manufacturer} = $host->{bios_strings}->{'system-manufacturer'} // ''; + $host_disco->{product_name} = $host->{bios_strings}->{'system-product-name'} // ''; + } + + $host_disco->{cpu_count} = ''; + if (is_not_empty($host->{CPUs}) and ref($host->{CPUs}) eq "HASH") { + $host_disco->{cpu_count} = $host->{CPUs}->{cpu_count} // ''; + } + + $host_disco->{memory_size} = ''; + if (is_not_empty($host->{memory}) and ref($host->{memory}) eq "HASH") { + $host_disco->{memory_size} = $host->{memory}->{size} // ''; + } + + # there can be empty tag in the api answer, this allows to trim empty tags. + $host_disco->{tags} = []; + for my $tag (@{$host->{tags}}) { + if (is_not_empty($tag)) { + push(@{$host_disco->{tags}}, $tag); + } + } + + push(@{$disco_stats->{results}}, $host_disco); + } + # Record the metadata + $disco_stats->{end_time} = time(); + $disco_stats->{duration} = $disco_stats->{end_time} - $disco_stats->{start_time}; + $disco_stats->{discovered_items} = scalar(@{$disco_stats->{results}}); + + my $encoded_data = json_encode( + $disco_stats, + prettify => $self->{option_results}->{prettify}, + errstr => '{"code":"encode_error","message":"Cannot encode discovered data into JSON format"}', + output => $self->{output} + ); + + $self->{output}->output_add(short_msg => $encoded_data); + $self->{output}->display(nolabel => 1, force_ignore_perfdata => 1); +} + +1; + +__END__ + +=head1 MODE + +Discover Vates XCP-ng hosts. + +=over 8 + +=item B<--filter-power-states> + +Filter hosts by power state (can be a regexp). Only matching hosts are included. + +=item B<--prettify> + +Prettify JSON output. + +=back + +=cut diff --git a/src/apps/virtualization/vates/host/mode/memory.pm b/src/apps/virtualization/vates/host/mode/memory.pm new file mode 100644 index 0000000000..d4d508568b --- /dev/null +++ b/src/apps/virtualization/vates/host/mode/memory.pm @@ -0,0 +1,189 @@ +# +# 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::memory; +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 custom_memory_output { + my ($self, %options) = @_; + + return sprintf( + '%s%s out of %s%s total', + $self->{perfdata}->change_bytes(value => $self->{result_values}->{used_bytes}), + $self->{perfdata}->change_bytes(value => $self->{result_values}->{total_bytes}) + ); +} + +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 => 'memory', type => COUNTER_TYPE_GLOBAL, message_separator => ' - ' } + ]; + + $self->{maps_counters}->{memory} = [ + { + label => 'memory-usage-prct', + type => COUNTER_TYPE_INSTANCE, + nlabel => 'host.memory.usage.percentage', + warning_default => '80', + critical_default => '95', + set => { + key_values => [ { name => 'used_prct' } ], + output_template => '%.2f %% of the memory is used', + perfdatas => [ + { value => 'used_prct', template => '%.2f', min => 0, max => 100, unit => '%' } + ] + } + }, + { + label => 'memory-usage-bytes', + type => COUNTER_TYPE_INSTANCE, + nlabel => 'host.memory.usage.bytes', + set => { + key_values => [ { name => 'used_bytes' }, { name => 'total_bytes' } ], + closure_custom_output => $self->can('custom_memory_output'), + threshold_use => 'used_bytes', + perfdatas => [ + { value => 'used_bytes', template => '%d', min => 0, max => 'total_bytes', unit => 'B' } + ] + } + }, + { + label => 'memory-free-bytes', + type => COUNTER_TYPE_INSTANCE, + nlabel => 'host.memory.free.bytes', + set => { + key_values => [ { name => 'free_bytes' } ], + output_template => 'free memory is %s B', + perfdatas => [ + { value => 'free_bytes', template => '%d', min => 0, unit => 'B' } + ] + } + } + ]; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $host = $options{custom}->get_host_info(fields => "name_label,memory,uuid"); + + if ( + !defined($host->{memory}) + or ref($host->{memory}) ne "HASH" + or !defined($host->{memory}->{size}) + or !defined($host->{memory}->{usage}) + ) { + $self->{output}->option_exit(short_msg => "Field memory not found in API response for host '" . $host->{name_label} . "'. Please check --debug or the Swagger documentation."); + } + + my $total = $host->{memory}->{size}; + my $used = $host->{memory}->{usage}; + + if ($total == 0) { + $self->{output}->option_exit(short_msg => "'" . $host->{uuid} . "' host reports a total memory of 0, inconsistent data."); + } + + $self->{memory} = { + used_bytes => $used, + free_bytes => $total - $used, + total_bytes => $total, + used_prct => 100 * $used / $total + }; +} + +1; + +__END__ + +=head1 MODE + +Check the memory usage of a Vates XCP-ng host. C is the real memory in use +(total minus free), not an allocation; a disabled/halted host naturally reports a low or null +usage, which does not need special-casing since it simply will not cross the usage thresholds. + +=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-memory-usage-prct> + +Threshold warning for the memory usage percentage. +Default: 80 + +=item B<--critical-memory-usage-prct> + +Threshold critical for the memory usage percentage. +Default: 95 + +=item B<--warning-memory-usage-bytes> + +Threshold warning for the memory used, in bytes. + +=item B<--critical-memory-usage-bytes> + +Threshold critical for the memory used, in bytes. + +=item B<--warning-memory-free-bytes> + +Threshold warning for the free memory, in bytes. + +=item B<--critical-memory-free-bytes> + +Threshold critical for the free memory, in bytes. + +=back + +=cut diff --git a/src/apps/virtualization/vates/host/mode/status.pm b/src/apps/virtualization/vates/host/mode/status.pm new file mode 100644 index 0000000000..86e81361ce --- /dev/null +++ b/src/apps/virtualization/vates/host/mode/status.pm @@ -0,0 +1,135 @@ +# +# 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::status; +use strict; +use warnings; +use base qw(centreon::plugins::templates::counter); +use centreon::plugins::misc qw/is_empty is_not_empty/; +use centreon::plugins::constants qw(:counters :values); +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); + +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 => 'host', type => COUNTER_TYPE_GLOBAL } + ]; + + $self->{maps_counters}->{host} = [ + { + label => 'status', + type => COUNTER_TYPE_GROUP, + critical_default => '%{enabled} !~ /^true/i || %{power_state} !~ /^Running/i', + set => { + key_values => [ + { name => 'display' }, { name => 'enabled' }, { name => 'power_state' }, + { name => 'address' }, { name => 'manufacturer' }, { name => 'product_name' }, + { name => 'uuid' } + ], + output_template => "host '%{display}' is %{power_state} (enabled: %{enabled}), address: %{address} [%{manufacturer} %{product_name}]", + closure_custom_threshold_check => \&catalog_status_threshold_ng + } + } + ]; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $host = $options{custom}->get_host_info(fields => "name_label,enabled,power_state,address,uuid,bios_strings"); + + my $manufacturer = "unknown"; + my $product_name = "unknown"; + if (is_not_empty($host->{bios_strings}) and ref($host->{bios_strings}) eq "HASH") { + $manufacturer = $host->{bios_strings}->{'system-manufacturer'} if is_not_empty($host->{bios_strings}->{'system-manufacturer'}); + $product_name = $host->{bios_strings}->{'system-product-name'} if is_not_empty($host->{bios_strings}->{'system-product-name'}); + } + + $self->{host} = { + display => $host->{name_label}, + enabled => $host->{enabled}, + power_state => $host->{power_state}, + address => $host->{address} // '', + manufacturer => $manufacturer, + product_name => $product_name, + uuid => $host->{uuid} + }; +} + +1; + +__END__ + +=head1 MODE + +Check the status of a Vates XCP-ng host: enabled state and power state. + +Since a XCP-ng host is not meaningfully reachable via a plain network ping, this mode is meant to +be used as the Centreon host check command for the host template (no dedicated service needed). + +=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-status> + +Define the conditions to match for the status to be WARNING. You can use the following variables: +C<%{display}>, C<%{enabled}>, C<%{power_state}>, C<%{address}>, C<%{manufacturer}>, C<%{product_name}>, C<%{uuid}>. + +=item B<--critical-status> + +Define the conditions to match for the status to be CRITICAL. You can use the following variables: +C<%{display}>, C<%{enabled}>, C<%{power_state}>, C<%{address}>, C<%{manufacturer}>, C<%{product_name}>, C<%{uuid}>. +Default: C<%{enabled} !~ /^true/i || %{power_state} !~ /^Running/i> + +=back + +=cut diff --git a/src/apps/virtualization/vates/host/plugin.pm b/src/apps/virtualization/vates/host/plugin.pm new file mode 100644 index 0000000000..e980509865 --- /dev/null +++ b/src/apps/virtualization/vates/host/plugin.pm @@ -0,0 +1,50 @@ +# +# 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::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} = { + 'status' => 'apps::virtualization::vates::host::mode::status', + 'cpu' => 'apps::virtualization::vates::host::mode::cpu', + 'memory' => 'apps::virtualization::vates::host::mode::memory', + 'discovery' => 'apps::virtualization::vates::host::mode::discovery', + }; + + $self->{custom_modes}->{api} = 'apps::virtualization::vates::custom::api'; + return $self; +} +1; + +__END__ + +=head1 PLUGIN DESCRIPTION + +Monitor Vates XCP-ng hosts through the Xen Orchestra REST API. + +=cut diff --git a/tests/resources/spellcheck/stopwords.txt b/tests/resources/spellcheck/stopwords.txt index 7b79032f7a..f3e25c6eb7 100644 --- a/tests/resources/spellcheck/stopwords.txt +++ b/tests/resources/spellcheck/stopwords.txt @@ -243,6 +243,7 @@ Netasq Netconf Netscaler net-snmp +ng NGINX NLCapacity --noeventlog @@ -439,6 +440,7 @@ WildFire WLAN WLC WSMAN +XCP Xen XPath ZDX From 2cb381fa37a4b3735652f5b82824c4e53962b928 Mon Sep 17 00:00:00 2001 From: EvanAdam Date: Tue, 8 Sep 2026 13:37:51 +0200 Subject: [PATCH 2/7] test(vates): add Robot tests for the host plugin Refs:CTOR-2517 Covers status/cpu/memory/discovery/help against a Mockoon fixture extended with 2 hosts (one enabled+running, one disabled+halted) and a new hosts/:UUID/stats route (per-core CPU percentage series) shared with the rest of the vates suites. Full vates regression (vm, pool, xenorchestra, host) passes: 74/74. --- .../apps/virtualization/vates/host/cpu.robot | 54 ++++++ .../virtualization/vates/host/discovery.robot | 32 ++++ .../apps/virtualization/vates/host/help.robot | 38 ++++ .../virtualization/vates/host/memory.robot | 48 +++++ .../virtualization/vates/host/status.robot | 54 ++++++ tests/apps/virtualization/vates/mockoon.json | 168 +++++++++++++++++- 6 files changed, 393 insertions(+), 1 deletion(-) create mode 100644 tests/apps/virtualization/vates/host/cpu.robot create mode 100644 tests/apps/virtualization/vates/host/discovery.robot create mode 100644 tests/apps/virtualization/vates/host/help.robot create mode 100644 tests/apps/virtualization/vates/host/memory.robot create mode 100644 tests/apps/virtualization/vates/host/status.robot diff --git a/tests/apps/virtualization/vates/host/cpu.robot b/tests/apps/virtualization/vates/host/cpu.robot new file mode 100644 index 0000000000..968aa52ff0 --- /dev/null +++ b/tests/apps/virtualization/vates/host/cpu.robot @@ -0,0 +1,54 @@ +*** Settings *** +Documentation apps::virtualization::vates::host::plugin + +Resource ${CURDIR}${/}..${/}..${/}..${/}..${/}resources/import.resource + +Suite Setup Start Mockoon ${MOCKOON_JSON} +Suite Teardown Stop Mockoon +Test Timeout 120s + + +*** Variables *** +${MOCKOON_JSON} ${CURDIR}${/}..${/}mockoon.json +${CMD} ${CENTREON_PLUGINS} +... --plugin=apps::virtualization::vates::host::plugin +... --mode=cpu +... --password=C3POR2P2 +... --username=obi-wan +... --hostname=127.0.0.1 +... --proto=http +... --port=3000 + + +*** Test Cases *** +Host Cpu ${tc} + [Tags] apps virtualization xenorchestra host + ${command} Catenate + ... ${CMD} + ... ${extra_options} + + Ctn Run Command And Check Result As Strings ${command} ${expected_result} + + Examples: + ... tc + ... extra_options + ... expected_result + ... -- + ... 1 + ... ${EMPTY} + ... UNKNOWN: you must fill either --host-uuid or --host-name. + ... 2 + ... --host-uuid=806ca3d4-a50f-43e7-b19a-9228b999028c + ... OK: CPU usage is 60.00 %, 4 pCPU(s) | 'host.cpu.usage.percentage'=60.00%;0:80;0:95;0;100 'host.cpu.count'=4;;;0; + ... 3 + ... --host-name=vates + ... OK: CPU usage is 60.00 %, 4 pCPU(s) | 'host.cpu.usage.percentage'=60.00%;0:80;0:95;0;100 'host.cpu.count'=4;;;0; + ... 4 + ... --host-name=vates --warning-cpu-usage-prct=1 + ... WARNING: CPU usage is 60.00 % | 'host.cpu.usage.percentage'=60.00%;0:1;0:95;0;100 'host.cpu.count'=4;;;0; + ... 5 + ... --host-name=vates --critical-cpu-usage-prct=1 + ... CRITICAL: CPU usage is 60.00 % | 'host.cpu.usage.percentage'=60.00%;0:80;0:1;0;100 'host.cpu.count'=4;;;0; + ... 6 + ... --host-name=vates3 + ... UNKNOWN: host 'vates3' is not enabled/running, can not get CPU usage data. diff --git a/tests/apps/virtualization/vates/host/discovery.robot b/tests/apps/virtualization/vates/host/discovery.robot new file mode 100644 index 0000000000..4629e31db5 --- /dev/null +++ b/tests/apps/virtualization/vates/host/discovery.robot @@ -0,0 +1,32 @@ +*** Settings *** +Resource ${CURDIR}${/}..${/}..${/}..${/}..${/}resources/import.resource + +Suite Setup Start Mockoon ${MOCKOON_JSON} +Suite Teardown Stop Mockoon +Test Timeout 120s + + +*** Variables *** +${MOCKOON_JSON} ${CURDIR}${/}..${/}mockoon.json + +${CMD} ${CENTREON_PLUGINS} --plugin=apps::virtualization::vates::host::plugin +... --password=C3POR2P2 +... --username=obi-wan +... --mode=discovery +... --hostname=127.0.0.1 +... --proto=http +... --port=3000 + + +*** Test Cases *** +Discovery ${tc} + [Tags] apps virtualization xenorchestra host discovery + ${command} Catenate ${CMD} ${extra_options} + + Ctn Run Command Without Connector And Check Result As Regexp ${command} ${expected_result} + + Examples: tc extra_options expected_result -- + ... 1 ${EMPTY} + ... "discovered_items":3 + ... 2 --filter-power-states=Running + ... "discovered_items":2 diff --git a/tests/apps/virtualization/vates/host/help.robot b/tests/apps/virtualization/vates/host/help.robot new file mode 100644 index 0000000000..c76463e529 --- /dev/null +++ b/tests/apps/virtualization/vates/host/help.robot @@ -0,0 +1,38 @@ +*** Settings *** +Documentation apps::virtualization::vates::host::plugin + +Resource ${CURDIR}${/}..${/}..${/}..${/}..${/}resources/import.resource + +Suite Setup Ctn Generic Suite Setup +Suite Teardown Ctn Generic Suite Teardown +Test Timeout 120s + + +*** Variables *** +${CMD} ${CENTREON_PLUGINS} --plugin=apps::virtualization::vates::host::plugin + + +*** Test Cases *** +Standard ${tc} - ${mode} + [Tags] apps virtualization host + ${command} Catenate + ... ${CMD} + ... --mode=${mode} + ... --help + + Ctn Run Command And Check Result As Regexp ${command} ${expected_result} flags=IGNORECASE + + Examples: + ... tc + ... mode + ... expected_result + ... -- + ... 1 + ... status + ... Mode:\n.*status + ... 2 + ... cpu + ... Mode:\n.*CPU + ... 3 + ... memory + ... Mode:\n.*[Mm]emory diff --git a/tests/apps/virtualization/vates/host/memory.robot b/tests/apps/virtualization/vates/host/memory.robot new file mode 100644 index 0000000000..a71d989a85 --- /dev/null +++ b/tests/apps/virtualization/vates/host/memory.robot @@ -0,0 +1,48 @@ +*** Settings *** +Documentation apps::virtualization::vates::host::plugin + +Resource ${CURDIR}${/}..${/}..${/}..${/}..${/}resources/import.resource + +Suite Setup Start Mockoon ${MOCKOON_JSON} +Suite Teardown Stop Mockoon +Test Timeout 120s + + +*** Variables *** +${MOCKOON_JSON} ${CURDIR}${/}..${/}mockoon.json +${CMD} ${CENTREON_PLUGINS} +... --plugin=apps::virtualization::vates::host::plugin +... --mode=memory +... --password=C3POR2P2 +... --username=obi-wan +... --hostname=127.0.0.1 +... --proto=http +... --port=3000 + + +*** Test Cases *** +Host Memory ${tc} + [Tags] apps virtualization xenorchestra host + ${command} Catenate + ... ${CMD} + ... ${extra_options} + + Ctn Run Command And Check Result As Strings ${command} ${expected_result} + + Examples: + ... tc + ... extra_options + ... expected_result + ... -- + ... 1 + ... ${EMPTY} + ... UNKNOWN: you must fill either --host-uuid or --host-name. + ... 2 + ... --host-uuid=806ca3d4-a50f-43e7-b19a-9228b999028c + ... OK: 72.22 % of the memory is used - 5.78GB out of 8.00GB total - free memory is 2386386944 B | 'host.memory.usage.percentage'=72.22%;0:80;0:95;0;100 'host.memory.usage.bytes'=6203547648B;;;0;8589934592 'host.memory.free.bytes'=2386386944B;;;0; + ... 3 + ... --host-name=vates + ... OK: 72.22 % of the memory is used - 5.78GB out of 8.00GB total - free memory is 2386386944 B | 'host.memory.usage.percentage'=72.22%;0:80;0:95;0;100 'host.memory.usage.bytes'=6203547648B;;;0;8589934592 'host.memory.free.bytes'=2386386944B;;;0; + ... 4 + ... --host-name=vates3 + ... OK: 0.00 % of the memory is used - 0.00B out of 4.00GB total - free memory is 4294967296 B | 'host.memory.usage.percentage'=0.00%;0:80;0:95;0;100 'host.memory.usage.bytes'=0B;;;0;4294967296 'host.memory.free.bytes'=4294967296B;;;0; diff --git a/tests/apps/virtualization/vates/host/status.robot b/tests/apps/virtualization/vates/host/status.robot new file mode 100644 index 0000000000..ae262eaf6a --- /dev/null +++ b/tests/apps/virtualization/vates/host/status.robot @@ -0,0 +1,54 @@ +*** Settings *** +Documentation apps::virtualization::vates::host::plugin + +Resource ${CURDIR}${/}..${/}..${/}..${/}..${/}resources/import.resource + +Suite Setup Start Mockoon ${MOCKOON_JSON} +Suite Teardown Stop Mockoon +Test Timeout 120s + + +*** Variables *** +${MOCKOON_JSON} ${CURDIR}${/}..${/}mockoon.json +${CMD} ${CENTREON_PLUGINS} +... --plugin=apps::virtualization::vates::host::plugin +... --mode=status +... --password=C3POR2P2 +... --username=obi-wan +... --hostname=127.0.0.1 +... --proto=http +... --port=3000 + + +*** Test Cases *** +Host Status ${tc} + [Tags] apps virtualization xenorchestra host + ${command} Catenate + ... ${CMD} + ... ${extra_options} + + Ctn Run Command And Check Result As Strings ${command} ${expected_result} + + Examples: + ... tc + ... extra_options + ... expected_result + ... -- + ... 1 + ... ${EMPTY} + ... UNKNOWN: you must fill either --host-uuid or --host-name. + ... 2 + ... --host-uuid=806ca3d4-a50f-43e7-b19a-9228b999028c + ... OK: host 'vates' is Running (enabled: true), address: 192.168.122.8 [QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996)] + ... 3 + ... --host-name=vates + ... OK: host 'vates' is Running (enabled: true), address: 192.168.122.8 [QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996)] + ... 4 + ... --host-uuid=f71b4cd5-c0e5-4a94-bc96-eaefe4919104 + ... CRITICAL: host 'vates3' is Halted (enabled: false), address: 192.168.122.81 [QEMU Ubuntu 24.04 PC (Q35 + ICH9, 2009)] + ... 5 + ... --host-name=vates3 + ... CRITICAL: host 'vates3' is Halted (enabled: false), address: 192.168.122.81 [QEMU Ubuntu 24.04 PC (Q35 + ICH9, 2009)] + ... 6 + ... --host-name=vates3 --critical-status=${EMPTY} + ... OK: host 'vates3' is Halted (enabled: false), address: 192.168.122.81 [QEMU Ubuntu 24.04 PC (Q35 + ICH9, 2009)] diff --git a/tests/apps/virtualization/vates/mockoon.json b/tests/apps/virtualization/vates/mockoon.json index 7e5cc8f051..ae7407575c 100644 --- a/tests/apps/virtualization/vates/mockoon.json +++ b/tests/apps/virtualization/vates/mockoon.json @@ -588,6 +588,141 @@ "crudKey": "id", "callbacks": [] }, + { + "uuid": "04670896-ab2b-498c-9cb2-ee94f3ca8637", + "body": "[\n {\n \"name_label\": \"vates\",\n \"uuid\": \"806ca3d4-a50f-43e7-b19a-9228b999028c\",\n \"enabled\": true,\n \"power_state\": \"Running\",\n \"address\": \"192.168.122.8\",\n \"bios_strings\": {\n \"system-manufacturer\": \"QEMU\",\n \"system-product-name\": \"Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996)\"\n },\n \"memory\": {\n \"usage\": 6203547648,\n \"size\": 8589934592\n },\n \"CPUs\": {\n \"cpu_count\": \"4\"\n },\n \"$pool\": \"00969214-df4d-83cb-78d5-bec9181903d4\",\n \"href\": \"/rest/v0/hosts/806ca3d4-a50f-43e7-b19a-9228b999028c\"\n }\n]\n", + "latency": 0, + "statusCode": 200, + "label": "host mode: vates found by name", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "query", + "modifier": "filter", + "value": "name_label:vates", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "AND", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "507ac4bf-52aa-4a19-b6c2-4be613bde0a1", + "body": "[\n {\n \"name_label\": \"vates\",\n \"uuid\": \"806ca3d4-a50f-43e7-b19a-9228b999028c\",\n \"enabled\": true,\n \"power_state\": \"Running\",\n \"address\": \"192.168.122.8\",\n \"bios_strings\": {\n \"system-manufacturer\": \"QEMU\",\n \"system-product-name\": \"Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996)\"\n },\n \"memory\": {\n \"usage\": 6203547648,\n \"size\": 8589934592\n },\n \"CPUs\": {\n \"cpu_count\": \"4\"\n },\n \"$pool\": \"00969214-df4d-83cb-78d5-bec9181903d4\",\n \"href\": \"/rest/v0/hosts/806ca3d4-a50f-43e7-b19a-9228b999028c\"\n }\n]\n", + "latency": 0, + "statusCode": 200, + "label": "host mode: vates found by uuid", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "query", + "modifier": "filter", + "value": "uuid:806ca3d4-a50f-43e7-b19a-9228b999028c", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "AND", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "fe46da81-f5f6-418f-b5df-1c0105f9c031", + "body": "[\n {\n \"name_label\": \"vates3\",\n \"uuid\": \"f71b4cd5-c0e5-4a94-bc96-eaefe4919104\",\n \"enabled\": false,\n \"power_state\": \"Halted\",\n \"address\": \"192.168.122.81\",\n \"bios_strings\": {\n \"system-manufacturer\": \"QEMU\",\n \"system-product-name\": \"Ubuntu 24.04 PC (Q35 + ICH9, 2009)\"\n },\n \"memory\": {\n \"usage\": 0,\n \"size\": 4294967296\n },\n \"CPUs\": {\n \"cpu_count\": \"2\"\n },\n \"$pool\": \"00969214-df4d-83cb-78d5-bec9181903d4\",\n \"href\": \"/rest/v0/hosts/f71b4cd5-c0e5-4a94-bc96-eaefe4919104\"\n }\n]\n", + "latency": 0, + "statusCode": 200, + "label": "host mode: vates3 (disabled/halted) found by name", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "query", + "modifier": "filter", + "value": "name_label:vates3", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "AND", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "65b0ebcf-7013-432f-91b8-15abc7b11faf", + "body": "[\n {\n \"name_label\": \"vates3\",\n \"uuid\": \"f71b4cd5-c0e5-4a94-bc96-eaefe4919104\",\n \"enabled\": false,\n \"power_state\": \"Halted\",\n \"address\": \"192.168.122.81\",\n \"bios_strings\": {\n \"system-manufacturer\": \"QEMU\",\n \"system-product-name\": \"Ubuntu 24.04 PC (Q35 + ICH9, 2009)\"\n },\n \"memory\": {\n \"usage\": 0,\n \"size\": 4294967296\n },\n \"CPUs\": {\n \"cpu_count\": \"2\"\n },\n \"$pool\": \"00969214-df4d-83cb-78d5-bec9181903d4\",\n \"href\": \"/rest/v0/hosts/f71b4cd5-c0e5-4a94-bc96-eaefe4919104\"\n }\n]\n", + "latency": 0, + "statusCode": 200, + "label": "host mode: vates3 (disabled/halted) found by uuid", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "query", + "modifier": "filter", + "value": "uuid:f71b4cd5-c0e5-4a94-bc96-eaefe4919104", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "AND", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, + { + "uuid": "5ae0569e-ba81-47b2-8cb2-c7869de16088", + "body": "[\n {\n \"name_label\": \"vates\",\n \"uuid\": \"806ca3d4-a50f-43e7-b19a-9228b999028c\",\n \"name_description\": \"\",\n \"enabled\": true,\n \"power_state\": \"Running\",\n \"address\": \"192.168.122.8\",\n \"bios_strings\": {\n \"system-manufacturer\": \"QEMU\",\n \"system-product-name\": \"Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996)\"\n },\n \"memory\": {\n \"usage\": 6203547648,\n \"size\": 8589934592\n },\n \"CPUs\": {\n \"cpu_count\": \"4\"\n },\n \"$pool\": \"00969214-df4d-83cb-78d5-bec9181903d4\",\n \"tags\": [],\n \"href\": \"/rest/v0/hosts/806ca3d4-a50f-43e7-b19a-9228b999028c\"\n },\n {\n \"name_label\": \"vates2\",\n \"uuid\": \"4dbfc1e1-8256-4e9a-bf4c-20d42fe9f0d3\",\n \"name_description\": \"\",\n \"enabled\": true,\n \"power_state\": \"Running\",\n \"address\": \"192.168.122.68\",\n \"bios_strings\": {\n \"system-manufacturer\": \"QEMU\",\n \"system-product-name\": \"Ubuntu 24.04 PC (Q35 + ICH9, 2009)\"\n },\n \"memory\": {\n \"usage\": 1612414976,\n \"size\": 4294967296\n },\n \"CPUs\": {\n \"cpu_count\": \"2\"\n },\n \"$pool\": \"00969214-df4d-83cb-78d5-bec9181903d4\",\n \"tags\": [],\n \"href\": \"/rest/v0/hosts/4dbfc1e1-8256-4e9a-bf4c-20d42fe9f0d3\"\n },\n {\n \"name_label\": \"vates3\",\n \"uuid\": \"f71b4cd5-c0e5-4a94-bc96-eaefe4919104\",\n \"name_description\": \"\",\n \"enabled\": false,\n \"power_state\": \"Halted\",\n \"address\": \"192.168.122.81\",\n \"bios_strings\": {\n \"system-manufacturer\": \"QEMU\",\n \"system-product-name\": \"Ubuntu 24.04 PC (Q35 + ICH9, 2009)\"\n },\n \"memory\": {\n \"usage\": 0,\n \"size\": 4294967296\n },\n \"CPUs\": {\n \"cpu_count\": \"2\"\n },\n \"$pool\": \"00969214-df4d-83cb-78d5-bec9181903d4\",\n \"tags\": [],\n \"href\": \"/rest/v0/hosts/f71b4cd5-c0e5-4a94-bc96-eaefe4919104\"\n }\n]\n", + "latency": 0, + "statusCode": 200, + "label": "no filter for discovery", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "query", + "modifier": "fields", + "value": "*", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, { "uuid": "0e05de96-8372-44ea-9126-ad78f07c63d0", "body": "[]", @@ -740,6 +875,37 @@ "streamingMode": null, "streamingInterval": 0 }, + { + "uuid": "e83bcdcd-b4b2-4d52-8237-ffb59227a095", + "type": "http", + "documentation": "", + "method": "get", + "endpoint": "rest/v0/hosts/:UUID/stats", + "responses": [ + { + "uuid": "904ddcb7-6a23-4f24-bd9e-59c17afdde87", + "body": "{\n \"endTimestamp\": 1788863115,\n \"interval\": 5,\n \"stats\": {\n \"cpus\": {\n \"0\": [\n 35.5,\n 42.0,\n 50.0\n ],\n \"1\": [\n 55.0,\n 62.0,\n 70.0\n ],\n \"2\": [\n 28.0,\n 33.0,\n 40.0\n ],\n \"3\": [\n 65.0,\n 72.0,\n 80.0\n ]\n }\n }\n}\n", + "latency": 0, + "statusCode": 200, + "label": "host cpu stats (4 cores)", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + } + ], + "responseMode": null, + "streamingMode": null, + "streamingInterval": 0 + }, { "uuid": "ffa0a9c9-8019-431a-8a2f-6f50c1dc56a0", "type": "http", @@ -948,4 +1114,4 @@ ], "data": [], "callbacks": [] -} \ No newline at end of file +} From f246e2542320654794335a3158f619957def0001 Mon Sep 17 00:00:00 2001 From: EvanAdam Date: Tue, 8 Sep 2026 13:40:43 +0200 Subject: [PATCH 3/7] fix(vates): narrow name-based API lookups to an exact match Refs:CTOR-2517 Discovered while smoke-testing the host plugin against the real XO instance: the REST API's filter=field:value does a substring match, not an exact one (filter=name_label:vates also matches 'vates2' and 'vates3'). Every single-object lookup in this file (get_name_and_uuid, get_vm_info, get_host_info) asserts the API returns exactly one match, so any name that is a prefix of another object's name silently produced a false "no host/vm/pool found" instead of resolving the intended object. Narrows multi-match responses down to the exact match before that assertion. --- src/apps/virtualization/vates/custom/api.pm | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/apps/virtualization/vates/custom/api.pm b/src/apps/virtualization/vates/custom/api.pm index 1c008ad661..0be5720bda 100644 --- a/src/apps/virtualization/vates/custom/api.pm +++ b/src/apps/virtualization/vates/custom/api.pm @@ -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 @@ -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}; @@ -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."); } @@ -200,14 +216,17 @@ 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."); } @@ -223,14 +242,17 @@ sub get_host_info { 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."); } From a1f5258d8846d13d80145ebaaa6bef480ea93cb0 Mon Sep 17 00:00:00 2001 From: EvanAdam Date: Tue, 8 Sep 2026 14:33:12 +0200 Subject: [PATCH 4/7] refactor(vates): resolve host cpu mode via cached lookup + let /stats report offline Refs:CTOR-2517 manage_selection() used get_host_info() (a live, uncached call fetching enabled/power_state/CPUs) purely to pre-check the host was running before hitting /stats. Switched to get_name_and_uuid() (cached on disk per --reload-cache-time, like pool/cpuovercommit.pm already does) plus silently_fail on the /stats call itself: a disabled/halted host makes that endpoint fail with a XAPI "HOST_OFFLINE" error, which is now surfaced directly instead of being pre-empted by an extra live API call. Also drops the separate (deprecated, string-typed) CPUs.cpu_count field: the number of per-core series already counted while averaging /stats is the same live CPU count. Mockoon fixture: added a per-UUID error response on hosts/:UUID/stats, and marked the generic one as the route's default response - it turns out mockoon-server only considers rule-bearing responses in its match loop and otherwise falls back to responses[0], so an un-marked generic response happened to work by accident until a rule-bearing sibling was added. --- .../virtualization/vates/host/mode/cpu.pm | 32 +++++++++---------- .../apps/virtualization/vates/host/cpu.robot | 2 +- tests/apps/virtualization/vates/mockoon.json | 29 ++++++++++++++++- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/apps/virtualization/vates/host/mode/cpu.pm b/src/apps/virtualization/vates/host/mode/cpu.pm index 4ec9d9ad7b..77d57f060a 100644 --- a/src/apps/virtualization/vates/host/mode/cpu.pm +++ b/src/apps/virtualization/vates/host/mode/cpu.pm @@ -89,21 +89,19 @@ sub set_counters { sub manage_selection { my ($self, %options) = @_; - my $host = $options{custom}->get_host_info(fields => "name_label,enabled,power_state,uuid,CPUs"); - - if ($host->{enabled} ne 'true' or $host->{power_state} ne 'Running') { - $self->{output}->option_exit(short_msg => "host '" . $host->{name_label} . "' is not enabled/running, can not get CPU usage data."); - } - - my $cpu_count = 0; - if (defined($host->{CPUs}) and ref($host->{CPUs}) eq "HASH" and defined($host->{CPUs}->{cpu_count})) { - $cpu_count = $host->{CPUs}->{cpu_count} + 0; + # 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 ($cpu_count == 0) { - $self->{output}->option_exit(short_msg => "host '" . $host->{name_label} . "' reports a CPU count of 0, inconsistent data."); - } - - my $host_stats = $options{custom}->request_api_get(endpoint => 'hosts/' . $host->{uuid} . '/stats'); if ( !defined($host_stats->{stats}) @@ -115,7 +113,9 @@ sub manage_selection { } # 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. + # 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 (no need for the separate, + # deprecated CPUs.cpu_count field on the host object). my $total = 0; my $cores = 0; for my $core (keys %{$host_stats->{stats}->{cpus}}) { @@ -131,7 +131,7 @@ sub manage_selection { $self->{cpu} = { display => $host->{name_label}, prct_used => $total / $cores, - cpu_count => $cpu_count + cpu_count => $cores }; } diff --git a/tests/apps/virtualization/vates/host/cpu.robot b/tests/apps/virtualization/vates/host/cpu.robot index 968aa52ff0..3876a081cb 100644 --- a/tests/apps/virtualization/vates/host/cpu.robot +++ b/tests/apps/virtualization/vates/host/cpu.robot @@ -51,4 +51,4 @@ Host Cpu ${tc} ... CRITICAL: CPU usage is 60.00 % | 'host.cpu.usage.percentage'=60.00%;0:80;0:1;0;100 'host.cpu.count'=4;;;0; ... 6 ... --host-name=vates3 - ... UNKNOWN: host 'vates3' is not enabled/running, can not get CPU usage data. + ... UNKNOWN: host 'vates3' is not enabled/running, can not get CPU usage data (HOST_OFFLINE(OpaqueRef:2f22ba81-242d-b7e2-9e6b-95b98d931af3)). diff --git a/tests/apps/virtualization/vates/mockoon.json b/tests/apps/virtualization/vates/mockoon.json index ae7407575c..67768159bb 100644 --- a/tests/apps/virtualization/vates/mockoon.json +++ b/tests/apps/virtualization/vates/mockoon.json @@ -882,6 +882,33 @@ "method": "get", "endpoint": "rest/v0/hosts/:UUID/stats", "responses": [ + { + "uuid": "6b2002ff-f4ec-40e2-a588-642060789d98", + "body": "{\n \"error\": \"HOST_OFFLINE(OpaqueRef:2f22ba81-242d-b7e2-9e6b-95b98d931af3)\",\n \"info\": \"This is a XenServer/XCP-ng error, not an XO error\"\n}\n", + "latency": 0, + "statusCode": 500, + "label": "vates3 (disabled/halted) is offline", + "headers": [], + "bodyType": "INLINE", + "filePath": "", + "databucketID": "", + "sendFileAsBody": false, + "rules": [ + { + "target": "params", + "modifier": "UUID", + "value": "f71b4cd5-c0e5-4a94-bc96-eaefe4919104", + "invert": false, + "operator": "equals" + } + ], + "rulesOperator": "OR", + "disableTemplating": false, + "fallbackTo404": false, + "default": false, + "crudKey": "id", + "callbacks": [] + }, { "uuid": "904ddcb7-6a23-4f24-bd9e-59c17afdde87", "body": "{\n \"endTimestamp\": 1788863115,\n \"interval\": 5,\n \"stats\": {\n \"cpus\": {\n \"0\": [\n 35.5,\n 42.0,\n 50.0\n ],\n \"1\": [\n 55.0,\n 62.0,\n 70.0\n ],\n \"2\": [\n 28.0,\n 33.0,\n 40.0\n ],\n \"3\": [\n 65.0,\n 72.0,\n 80.0\n ]\n }\n }\n}\n", @@ -897,7 +924,7 @@ "rulesOperator": "OR", "disableTemplating": false, "fallbackTo404": false, - "default": false, + "default": true, "crudKey": "id", "callbacks": [] } From 6884bec8ffb3049787ab10b22e35699321cc36c2 Mon Sep 17 00:00:00 2001 From: EvanAdam Date: Tue, 8 Sep 2026 14:42:50 +0200 Subject: [PATCH 5/7] fix(vates): return UNKNOWN from host memory mode on a disabled/halted host Refs:CTOR-2517 A disabled host still reports a valid memory.size with usage:0, so the mode silently returned "OK: 0.00 % of the memory is used" instead of signaling the host isn't actually up. Now requests enabled/power_state alongside memory (still a single API call) and exits UNKNOWN before computing usage, matching the status and cpu modes' behavior. --- src/apps/virtualization/vates/host/mode/memory.pm | 10 +++++++--- tests/apps/virtualization/vates/host/memory.robot | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/apps/virtualization/vates/host/mode/memory.pm b/src/apps/virtualization/vates/host/mode/memory.pm index d4d508568b..5fbb6ec1e4 100644 --- a/src/apps/virtualization/vates/host/mode/memory.pm +++ b/src/apps/virtualization/vates/host/mode/memory.pm @@ -112,7 +112,11 @@ sub set_counters { sub manage_selection { my ($self, %options) = @_; - my $host = $options{custom}->get_host_info(fields => "name_label,memory,uuid"); + my $host = $options{custom}->get_host_info(fields => "name_label,enabled,power_state,memory,uuid"); + + if ($host->{enabled} ne 'true' or $host->{power_state} ne 'Running') { + $self->{output}->option_exit(short_msg => "host '" . $host->{name_label} . "' is not enabled/running, can not get memory usage data."); + } if ( !defined($host->{memory}) @@ -145,8 +149,8 @@ __END__ =head1 MODE Check the memory usage of a Vates XCP-ng host. C is the real memory in use -(total minus free), not an allocation; a disabled/halted host naturally reports a low or null -usage, which does not need special-casing since it simply will not cross the usage thresholds. +(total minus free), not an allocation. The host must be enabled and running: a disabled/halted +host returns UNKNOWN rather than a misleading near-zero usage. =over 8 diff --git a/tests/apps/virtualization/vates/host/memory.robot b/tests/apps/virtualization/vates/host/memory.robot index a71d989a85..1291fb4750 100644 --- a/tests/apps/virtualization/vates/host/memory.robot +++ b/tests/apps/virtualization/vates/host/memory.robot @@ -45,4 +45,4 @@ Host Memory ${tc} ... OK: 72.22 % of the memory is used - 5.78GB out of 8.00GB total - free memory is 2386386944 B | 'host.memory.usage.percentage'=72.22%;0:80;0:95;0;100 'host.memory.usage.bytes'=6203547648B;;;0;8589934592 'host.memory.free.bytes'=2386386944B;;;0; ... 4 ... --host-name=vates3 - ... OK: 0.00 % of the memory is used - 0.00B out of 4.00GB total - free memory is 4294967296 B | 'host.memory.usage.percentage'=0.00%;0:80;0:95;0;100 'host.memory.usage.bytes'=0B;;;0;4294967296 'host.memory.free.bytes'=4294967296B;;;0; + ... UNKNOWN: host 'vates3' is not enabled/running, can not get memory usage data. From 489ff7e59168d09bbe63fc1178ce92dd53eacf77 Mon Sep 17 00:00:00 2001 From: EvanAdam Date: Tue, 8 Sep 2026 14:46:02 +0200 Subject: [PATCH 6/7] doc(vates): fix comment Refs:CTOR-2517 --- src/apps/virtualization/vates/host/mode/cpu.pm | 3 +-- src/apps/virtualization/vates/host/mode/memory.pm | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/apps/virtualization/vates/host/mode/cpu.pm b/src/apps/virtualization/vates/host/mode/cpu.pm index 77d57f060a..99693535c2 100644 --- a/src/apps/virtualization/vates/host/mode/cpu.pm +++ b/src/apps/virtualization/vates/host/mode/cpu.pm @@ -114,8 +114,7 @@ sub manage_selection { # 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 (no need for the separate, - # deprecated CPUs.cpu_count field on the host object). + # number of series is itself the live physical CPU count. my $total = 0; my $cores = 0; for my $core (keys %{$host_stats->{stats}->{cpus}}) { diff --git a/src/apps/virtualization/vates/host/mode/memory.pm b/src/apps/virtualization/vates/host/mode/memory.pm index 5fbb6ec1e4..d9024d3481 100644 --- a/src/apps/virtualization/vates/host/mode/memory.pm +++ b/src/apps/virtualization/vates/host/mode/memory.pm @@ -149,8 +149,8 @@ __END__ =head1 MODE Check the memory usage of a Vates XCP-ng host. C is the real memory in use -(total minus free), not an allocation. The host must be enabled and running: a disabled/halted -host returns UNKNOWN rather than a misleading near-zero usage. +(total minus free), not the allocation sum of every virtual machine. +The host must be enabled and running: a disabled/halted host returns UNKNOWN =over 8 From 7b117c747ccc819540dd6511a9a38724b547e689 Mon Sep 17 00:00:00 2001 From: EvanAdam Date: Thu, 24 Sep 2026 16:32:34 +0200 Subject: [PATCH 7/7] fix(vates): changes after review Refs:CTOR-2517 --- .../virtualization/vates/host/mode/cpu.pm | 18 +++++++++-------- .../virtualization/vates/host/mode/memory.pm | 20 +++++++++---------- .../virtualization/vates/host/mode/status.pm | 2 +- tests/resources/spellcheck/stopwords.txt | 5 ++--- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/apps/virtualization/vates/host/mode/cpu.pm b/src/apps/virtualization/vates/host/mode/cpu.pm index 99693535c2..55a9cf127d 100644 --- a/src/apps/virtualization/vates/host/mode/cpu.pm +++ b/src/apps/virtualization/vates/host/mode/cpu.pm @@ -59,7 +59,7 @@ sub set_counters { $self->{maps_counters}->{cpu} = [ { label => 'cpu-usage-prct', - type => COUNTER_TYPE_INSTANCE, + type => COUNTER_KIND_METRIC, nlabel => 'host.cpu.usage.percentage', warning_default => '80', critical_default => '95', @@ -73,7 +73,7 @@ sub set_counters { }, { label => 'cpu-count', - type => COUNTER_TYPE_INSTANCE, + type => COUNTER_KIND_METRIC, nlabel => 'host.cpu.count', set => { key_values => [ { name => 'cpu_count' }, { name => 'display' } ], @@ -155,17 +155,19 @@ Identify the host by its name (only one host is expected). =item B<--warning-cpu-usage-prct> -Threshold warning for the CPU usage percentage. -Default: 80 +Threshold. Default: 80 =item B<--critical-cpu-usage-prct> -Threshold critical for the CPU usage percentage. -Default: 95 +Threshold. Default: 95 -=item B<--warning-cpu-count> / B<--critical-cpu-count> +=item B<--warning-cpu-count> -Threshold on the number of physical CPUs reported by the host. Informative counter, no default. +Threshold. + +=item B<--critical-cpu-count> + +Threshold. =back diff --git a/src/apps/virtualization/vates/host/mode/memory.pm b/src/apps/virtualization/vates/host/mode/memory.pm index d9024d3481..a2492d94f8 100644 --- a/src/apps/virtualization/vates/host/mode/memory.pm +++ b/src/apps/virtualization/vates/host/mode/memory.pm @@ -69,7 +69,7 @@ sub set_counters { $self->{maps_counters}->{memory} = [ { label => 'memory-usage-prct', - type => COUNTER_TYPE_INSTANCE, + type => COUNTER_KIND_METRIC, nlabel => 'host.memory.usage.percentage', warning_default => '80', critical_default => '95', @@ -83,7 +83,7 @@ sub set_counters { }, { label => 'memory-usage-bytes', - type => COUNTER_TYPE_INSTANCE, + type => COUNTER_KIND_METRIC, nlabel => 'host.memory.usage.bytes', set => { key_values => [ { name => 'used_bytes' }, { name => 'total_bytes' } ], @@ -96,7 +96,7 @@ sub set_counters { }, { label => 'memory-free-bytes', - type => COUNTER_TYPE_INSTANCE, + type => COUNTER_KIND_METRIC, nlabel => 'host.memory.free.bytes', set => { key_values => [ { name => 'free_bytes' } ], @@ -164,29 +164,27 @@ Identify the host by its name (only one host is expected). =item B<--warning-memory-usage-prct> -Threshold warning for the memory usage percentage. -Default: 80 +Threshold in percentage. Default: 80 =item B<--critical-memory-usage-prct> -Threshold critical for the memory usage percentage. -Default: 95 +Threshold in percentage. Default: 95 =item B<--warning-memory-usage-bytes> -Threshold warning for the memory used, in bytes. +Threshold in bytes. =item B<--critical-memory-usage-bytes> -Threshold critical for the memory used, in bytes. +Threshold in bytes. =item B<--warning-memory-free-bytes> -Threshold warning for the free memory, in bytes. +Threshold in bytes. =item B<--critical-memory-free-bytes> -Threshold critical for the free memory, in bytes. +Threshold in bytes. =back diff --git a/src/apps/virtualization/vates/host/mode/status.pm b/src/apps/virtualization/vates/host/mode/status.pm index 86e81361ce..add8081d48 100644 --- a/src/apps/virtualization/vates/host/mode/status.pm +++ b/src/apps/virtualization/vates/host/mode/status.pm @@ -60,7 +60,7 @@ sub set_counters { $self->{maps_counters}->{host} = [ { label => 'status', - type => COUNTER_TYPE_GROUP, + type => COUNTER_KIND_TEXT, critical_default => '%{enabled} !~ /^true/i || %{power_state} !~ /^Running/i', set => { key_values => [ diff --git a/tests/resources/spellcheck/stopwords.txt b/tests/resources/spellcheck/stopwords.txt index f3e25c6eb7..78b0748e46 100644 --- a/tests/resources/spellcheck/stopwords.txt +++ b/tests/resources/spellcheck/stopwords.txt @@ -243,7 +243,6 @@ Netasq Netconf Netscaler net-snmp -ng NGINX NLCapacity --noeventlog @@ -308,9 +307,9 @@ psu PVX QoS Qtree -qwinsta queue-messages-inflighted quts +qwinsta raidvolume redis-cli ReplicaJobSession @@ -440,7 +439,7 @@ WildFire WLAN WLC WSMAN -XCP +XCP-ng Xen XPath ZDX