From 7815e335aa687576df6719ba995f059d1396486a Mon Sep 17 00:00:00 2001 From: crsuser Date: Fri, 19 Jun 2026 17:26:44 +0200 Subject: [PATCH 01/10] feat: update and add modes for Stormshield SNMP monitoring --- .../stormshield/snmp/mode/auto_update.pm | 237 ++++++++ .../stormshield/snmp/mode/components/disk.pm | 131 +++-- .../stormshield/snmp/mode/components/fan.pm | 118 ++-- .../stormshield/snmp/mode/components/psu.pm | 94 +++- .../snmp/mode/components/temperature.pm | 112 ++-- .../stormshield/snmp/mode/connections.pm | 136 ++++- .../stormshield/snmp/mode/ha_cluster.pm | 334 ++++++++++++ src/network/stormshield/snmp/mode/hardware.pm | 101 +++- src/network/stormshield/snmp/mode/health.pm | 71 ++- .../stormshield/snmp/mode/interfaces_disco.pm | 510 ++++++++++++++++++ .../stormshield/snmp/mode/memorydetailed.pm | 295 +++++++--- src/network/stormshield/snmp/plugin.pm | 33 +- 12 files changed, 1863 insertions(+), 309 deletions(-) create mode 100644 src/network/stormshield/snmp/mode/auto_update.pm create mode 100644 src/network/stormshield/snmp/mode/ha_cluster.pm create mode 100644 src/network/stormshield/snmp/mode/interfaces_disco.pm diff --git a/src/network/stormshield/snmp/mode/auto_update.pm b/src/network/stormshield/snmp/mode/auto_update.pm new file mode 100644 index 0000000000..334739d7de --- /dev/null +++ b/src/network/stormshield/snmp/mode/auto_update.pm @@ -0,0 +1,237 @@ +# 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 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 network::stormshield::snmp::mode::auto_update; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use DateTime; +use centreon::plugins::constants qw(:counters); +use centreon::plugins::misc; + +sub custom_auto_update_output { + my ($self, %options) = @_; + + my $state = $self->{result_values}->{state}; + my $last_date = $self->{result_values}->{last_date} // 'N/A'; + my $display = $self->{result_values}->{display}; + + my $msg = ""; + + if ($state eq 'Uptodate') { + $msg = "$display is up to date"; + } elsif ($state eq 'Disabled') { + $msg = "$display is disabled"; + } elsif ($state eq 'NeverStarted') { + $msg = "$display never started"; + } else { + $msg = "$display $state (Last Update: $last_date)"; + } + + if (!$self->{output}->{long_msg_added}) { + $self->{output}->{long_msg_added} = 1; + $self->{output}->output_add( + long_msg => "-----------------------------------------------------------------------\n" + ); + } + + return $msg; + +} + +sub custom_auto_update_threshold_check { + my ($self, %options) = @_; + + my $state = $self->{result_values}->{state}; + + if ($state eq 'Failed' || $state eq 'Broken') { + return 'CRITICAL'; + } elsif ($state eq 'Partially Failed') { + return 'WARNING'; + } elsif ($state eq 'Disabled' || $state eq 'NeverStarted') { + return 'OK'; + } else { + return 'OK'; + } +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'updates', + type => COUNTER_TYPE_INSTANCE, + message_multiple => 'All Updates and Webservices are up to date', + skipped_code => { -10 => 1 } + } + ]; + + $self->{maps_counters}->{updates} = [ + { + label => 'status', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'display' }, + { name => 'state' }, + { name => 'last_date' } + ], + closure_custom_output => $self->can('custom_auto_update_output'), + closure_custom_threshold_check => $self->can('custom_auto_update_threshold_check'), + closure_custom_perfdata => sub { return 0; } + } + }, + ]; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options(arguments => { + }); + + return $self; +} + +sub manage_selection { + my ($self, %options) = @_; + + $self->{updates} = {}; + + my $oid_snsAutoupdateEntry = '.1.3.6.1.4.1.11256.1.9.1.1'; + my $oid_snsAutoupdateIndex = '.1.3.6.1.4.1.11256.1.9.1.1.1'; + my $oid_snsAutoupdateSubsys = '.1.3.6.1.4.1.11256.1.9.1.1.2'; + my $oid_snsAutoupdateState = '.1.3.6.1.4.1.11256.1.9.1.1.3'; + my $oid_snsAutoupdateLast = '.1.3.6.1.4.1.11256.1.9.1.1.4'; + + my $oid_snsCustomWebServicesEntry = '.1.3.6.1.4.1.11256.1.9.2.1'; + my $oid_snsCustomWebServicesIndex = '.1.3.6.1.4.1.11256.1.9.2.1.1'; + my $oid_snsCustomWebServicesName = '.1.3.6.1.4.1.11256.1.9.2.1.2'; + my $oid_snsCustomWebServicesState = '.1.3.6.1.4.1.11256.1.9.2.1.3'; + my $oid_snsCustomWebServicesLast = '.1.3.6.1.4.1.11256.1.9.2.1.4'; + + + my $oid_snsVersion = '.1.3.6.1.4.1.11256.1.18.2.0'; + my $snmp_result_version = $options{snmp}->get_leef(oids => [ $oid_snsVersion ], nothing_quit => 1); + my $version_clean = ''; + + if (defined $snmp_result_version && defined $snmp_result_version->{$oid_snsVersion}) { + $version_clean = $snmp_result_version->{$oid_snsVersion}; + $version_clean =~ s/([0-9]+(?:\.[0-9]+)*).*/$1/; + } + + my $snmp_result_update = $options{snmp}->get_table( + oid => $oid_snsAutoupdateEntry, + nothing_quit => 0 + ); + + my $snmp_result_webservice = {}; + + # The MIB WebService is available only starting with version 5.1.0 + if (defined $version_clean && centreon::plugins::misc::minimal_version($version_clean, '5.1.0')) { + $snmp_result_webservice = $options{snmp}->get_table( + oid => $oid_snsCustomWebServicesEntry, + nothing_quit => 0 + ); + } + + $self->process_table( + snmp_result => $snmp_result_update, + index_oid => $oid_snsAutoupdateIndex, + name_oid => $oid_snsAutoupdateSubsys, + state_oid => $oid_snsAutoupdateState, + date_oid => $oid_snsAutoupdateLast, + prefix => 'Update' + ); + + if (scalar(keys %{$snmp_result_webservice}) > 0) { + $self->process_table( + snmp_result => $snmp_result_webservice, + index_oid => $oid_snsCustomWebServicesIndex, + name_oid => $oid_snsCustomWebServicesName, + state_oid => $oid_snsCustomWebServicesState, + date_oid => $oid_snsCustomWebServicesLast, + prefix => 'WebService' + ); + } + + if (scalar(keys %{$self->{updates}}) == 0) { + $self->{output}->output_add( + severity => 'UNKNOWN', + short_msg => 'No updates or web services found.' + ); + $self->{updates} = {}; + return; + } +} + +sub process_table { + my ($self, %options) = @_; + + my $snmp_result = $options{snmp_result}; + my $index_oid = $options{index_oid}; + my $name_oid = $options{name_oid}; + my $state_oid = $options{state_oid}; + my $date_oid = $options{date_oid}; + my $prefix = $options{prefix}; + + foreach my $oid (sort keys %{$snmp_result}) { + next unless $oid =~ /^$index_oid\./; + + my $index = $oid; + $index =~ s/^$index_oid\.//; + + my $name = $snmp_result->{"$name_oid.$index"} // 'N/A'; + my $state = $snmp_result->{"$state_oid.$index"} // 'N/A'; + my $lastDate = $snmp_result->{"$date_oid.$index"} // 'N/A'; + + my $normalized_state = $state; + if ($normalized_state eq 'Disabled') { + $normalized_state = 'Disabled'; + } elsif ($normalized_state !~ /^(Uptodate|Failed|Broken|Partially Failed)/) { + $normalized_state = 'NeverStarted'; + } + + $self->{updates}->{$prefix . '_' . $index} = { + display => $prefix . ': ' . $name, + state => $normalized_state, + last_date => $lastDate + }; + } +} + +1; + +__END__ + +=head1 MODE + +This mode allows you to monitor the status of auto-updates and web services on a Stormshield device. +It checks for failed, broken, or partially failed updates and reports their status. + +=over 8 + +=back + +=cut \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/components/disk.pm b/src/network/stormshield/snmp/mode/components/disk.pm index e451770745..8ea6e8b7bd 100644 --- a/src/network/stormshield/snmp/mode/components/disk.pm +++ b/src/network/stormshield/snmp/mode/components/disk.pm @@ -1,5 +1,5 @@ # -# Copyright 2024 Centreon (http://www.centreon.com/) +# 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 @@ -23,17 +23,32 @@ package network::stormshield::snmp::mode::components::disk; use strict; use warnings; -my $mapping = { - name => { oid => '.1.3.6.1.4.1.11256.1.10.5.1.2' }, # snsDiskEntryDiskName - isRaid => { oid => '.1.3.6.1.4.1.11256.1.10.5.1.4' }, # snsDiskEntryIsRaid - raidStatus => { oid => '.1.3.6.1.4.1.11256.1.10.5.1.5' } # snsDiskEntryRaidStatus +# Single-node OIDs — structure: oid. +my $mapping_single = { + name => { oid => '.1.3.6.1.4.1.11256.1.10.5.1.2' }, # snsDiskEntryDiskName + smartResult => { oid => '.1.3.6.1.4.1.11256.1.10.5.1.3' }, # snsDiskEntrySmartResult + isRaid => { oid => '.1.3.6.1.4.1.11256.1.10.5.1.4' }, # snsDiskEntryIsRaid + raidStatus => { oid => '.1.3.6.1.4.1.11256.1.10.5.1.5' }, # snsDiskEntryRaidStatus + position => { oid => '.1.3.6.1.4.1.11256.1.10.5.1.6' } # snsDiskEntryPosition }; -my $oid_diskEntry = '.1.3.6.1.4.1.11256.1.10.5.1'; # snsDiskEntry +my $oid_diskEntry_single = '.1.3.6.1.4.1.11256.1.10.5.1'; # snsDiskEntry + +# HA OIDs — structure: oid.. +my $oid_disk_name_ha = '.1.3.6.1.4.1.11256.1.11.11.1.2'; # snsNodeDiskName +my $oid_disk_smartResult_ha = '.1.3.6.1.4.1.11256.1.11.11.1.3'; # snsNodeDiskSmartResult +my $oid_disk_isRaid_ha = '.1.3.6.1.4.1.11256.1.11.11.1.4'; # snsNodeDiskIsRaid +my $oid_disk_raidStatus_ha = '.1.3.6.1.4.1.11256.1.11.11.1.5'; # snsNodeDiskRaidStatus +my $oid_disk_position_ha = '.1.3.6.1.4.1.11256.1.11.11.1.6'; # snsNodeDiskPosition +my $oid_diskEntry_ha = '.1.3.6.1.4.1.11256.1.11.11.1'; # snsNodeDiskEntry sub load { my ($self) = @_; - - push @{$self->{request}}, { oid => $oid_diskEntry, end => $mapping->{raidStatus}->{oid} }; + + if ($self->{is_ha}) { + push @{$self->{request}}, { oid => $oid_diskEntry_ha }; + } else { + push @{$self->{request}}, { oid => $oid_diskEntry_single, end => $mapping_single->{position}->{oid} }; + } } sub check { @@ -43,44 +58,82 @@ sub check { $self->{components}->{disk} = { name => 'disks', total => 0, skip => 0 }; return if ($self->check_filter(section => 'disk')); - foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{ $oid_diskEntry }})) { - next if ($oid !~ /^$mapping->{name}->{oid}\.(.*)$/); - my $instance = $1; - my $result = $self->{snmp}->map_instance(mapping => $mapping, results => $self->{results}->{ $oid_diskEntry }, instance => $instance); - - next if ($self->check_filter(section => 'disk', instance => $instance)); + my $nodes = $self->{is_ha} ? $self->{ha_nodes} : ['0']; + my $serials = $self->{ha_serials}; - $self->{components}->{disk}->{total}++; - if ($result->{isRaid} == 0) { - $self->{output}->output_add( - long_msg => sprintf( - "disk '%s' is not member of a raid [instance: %s]", - $result->{name}, - $instance - ) - ); - next; + foreach my $node_id (@$nodes) { + my $label_prefix = $self->{is_ha} ? $serials->{$node_id} . '_' : ''; + my %disks_data = (); + + if ($self->{is_ha}) { + foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{$oid_diskEntry_ha}})) { + if ($oid =~ /^$oid_disk_name_ha\.$node_id\.(\d+)$/) { + $disks_data{$1}{name} = $self->{results}->{$oid_diskEntry_ha}->{$oid}; + } elsif ($oid =~ /^$oid_disk_smartResult_ha\.$node_id\.(\d+)$/) { + $disks_data{$1}{smartResult} = $self->{results}->{$oid_diskEntry_ha}->{$oid}; + } elsif ($oid =~ /^$oid_disk_isRaid_ha\.$node_id\.(\d+)$/) { + $disks_data{$1}{isRaid} = $self->{results}->{$oid_diskEntry_ha}->{$oid}; + } elsif ($oid =~ /^$oid_disk_raidStatus_ha\.$node_id\.(\d+)$/) { + $disks_data{$1}{raidStatus} = $self->{results}->{$oid_diskEntry_ha}->{$oid}; + } elsif ($oid =~ /^$oid_disk_position_ha\.$node_id\.(\d+)$/) { + $disks_data{$1}{position} = $self->{results}->{$oid_diskEntry_ha}->{$oid}; + } + } + } else { + foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{$oid_diskEntry_single}})) { + next if ($oid !~ /^$mapping_single->{name}->{oid}\.(\d+)$/); + my $disk_id = $1; + my $result = $self->{snmp}->map_instance( + mapping => $mapping_single, + results => $self->{results}->{$oid_diskEntry_single}, + instance => $disk_id + ); + $disks_data{$disk_id} = $result; + } } - $self->{output}->output_add( - long_msg => sprintf( - "disk '%s' raid status is '%s' [instance: %s]", - $result->{name}, - $result->{raidStatus}, - $instance - ) - ); - - my $exit = $self->get_severity(section => 'raid', value => $result->{raidStatus}); - if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { + foreach my $disk_id (sort { $a <=> $b } keys %disks_data) { + my $disk = $disks_data{$disk_id}; + my $instance = $label_prefix . 'disk' . $disk_id; + + next if ($self->check_filter(section => 'disk', instance => $instance)); + $self->{components}->{disk}->{total}++; + + my $smart = $disk->{smartResult} // 'N/A'; + my $name = $label_prefix . $disk->{name} // $instance; + $self->{output}->output_add( - severity => $exit, - short_msg => sprintf( - "Disk '%s' raid status is '%s'", $result->{name}, $result->{raidStatus} + long_msg => sprintf( + "disk '%s' smart result is '%s' [isRaid: %s, raidStatus: %s, position: %s]", + $name, + $smart, + (defined $disk->{isRaid} && $disk->{isRaid} == 1) ? 'true' : 'false', + (defined $disk->{raidStatus} && $disk->{raidStatus} ne '') ? $disk->{raidStatus} : '/', + $disk->{position} // 'N/A' ) ); + + my $exit = $self->get_severity(section => 'disk', value => $smart); + if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { + $self->{output}->output_add( + severity => $exit, + short_msg => sprintf("Disk '%s' smart result is '%s'", $name, $smart) + ); + } + + if (defined $disk->{isRaid} && $disk->{isRaid} == 1 + && defined $disk->{raidStatus} && $disk->{raidStatus} ne '' + && $disk->{raidStatus} ne 'optimal') + { + $self->{output}->output_add( + severity => 'WARNING', + short_msg => sprintf( + "Disk '%s' RAID status is '%s'", $name, $disk->{raidStatus} + ) + ); + } } } } -1; +1; \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/components/fan.pm b/src/network/stormshield/snmp/mode/components/fan.pm index 9dda6a12cc..8018690b17 100644 --- a/src/network/stormshield/snmp/mode/components/fan.pm +++ b/src/network/stormshield/snmp/mode/components/fan.pm @@ -1,5 +1,5 @@ # -# Copyright 2024 Centreon (http://www.centreon.com/) +# 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 @@ -23,20 +23,28 @@ package network::stormshield::snmp::mode::components::fan; use strict; use warnings; -my $mapping = { +# Single-node OIDs — structure: oid. +my $mapping_single = { name => { oid => '.1.3.6.1.4.1.11256.1.10.9.1.2' }, # snsFanName status => { oid => '.1.3.6.1.4.1.11256.1.10.9.1.3' }, # snsFanStatus - rpm => { oid => '.1.3.6.1.4.1.11256.1.10.9.1.4' } # snsFanRpm + rpm => { oid => '.1.3.6.1.4.1.11256.1.10.9.1.4' } # snsFanRpm }; -my $oid_fanEntry = '.1.3.6.1.4.1.11256.1.10.9.1'; # snsFanEntry +my $oid_fanEntry_single = '.1.3.6.1.4.1.11256.1.10.9.1'; # snsFanEntry + +# HA OIDs — structure: oid.. +my $oid_fan_name_ha = '.1.3.6.1.4.1.11256.1.11.13.1.2'; # snsNodeFanName +my $oid_fan_status_ha = '.1.3.6.1.4.1.11256.1.11.13.1.3'; # snsNodeFanStatus +my $oid_fan_rpm_ha = '.1.3.6.1.4.1.11256.1.11.13.1.4'; # snsNodeFanRpm +my $oid_fanEntry_ha = '.1.3.6.1.4.1.11256.1.11.13.1'; # snsNodeFanEntry sub load { my ($self) = @_; - - push @{$self->{request}}, { - oid => $oid_fanEntry, - start => $mapping->{name}->{oid} - }; + + if ($self->{is_ha}) { + push @{$self->{request}}, { oid => $oid_fanEntry_ha }; + } else { + push @{$self->{request}}, { oid => $oid_fanEntry_single }; + } } sub check { @@ -46,51 +54,71 @@ sub check { $self->{components}->{fan} = { name => 'fans', total => 0, skip => 0 }; return if ($self->check_filter(section => 'fan')); - my ($exit, $warn, $crit, $checked); - foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{ $oid_fanEntry }})) { - next if ($oid !~ /^$mapping->{name}->{oid}\.(.*)$/); - my $instance = $1; + my $nodes = $self->{is_ha} ? $self->{ha_nodes} : ['0']; + my $serials = $self->{ha_serials}; - my $result = $self->{snmp}->map_instance(mapping => $mapping, results => $self->{results}->{ $oid_fanEntry }, instance => $instance); + foreach my $node_id (@$nodes) { + my $label_prefix = $self->{is_ha} ? $serials->{$node_id} . '_' : ''; + my %fans_data = (); - next if ($self->check_filter(section => 'fan', instance => $instance)); + if ($self->{is_ha}) { + foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{$oid_fanEntry_ha}})) { + if ($oid =~ /^$oid_fan_name_ha\.$node_id\.(\d+)$/) { + $fans_data{$1}{name} = $self->{results}->{$oid_fanEntry_ha}->{$oid}; + } elsif ($oid =~ /^$oid_fan_status_ha\.$node_id\.(\d+)$/) { + $fans_data{$1}{status} = $self->{results}->{$oid_fanEntry_ha}->{$oid}; + } elsif ($oid =~ /^$oid_fan_rpm_ha\.$node_id\.(\d+)$/) { + $fans_data{$1}{rpm} = $self->{results}->{$oid_fanEntry_ha}->{$oid}; + } + } + } else { + foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{$oid_fanEntry_single}})) { + next if ($oid !~ /^$mapping_single->{name}->{oid}\.(\d+)$/); + my $fan_id = $1; + my $result = $self->{snmp}->map_instance( + mapping => $mapping_single, + results => $self->{results}->{$oid_fanEntry_single}, + instance => $fan_id + ); + $fans_data{$fan_id} = $result; + } + } - $self->{components}->{fan}->{total}++; - $self->{output}->output_add( - long_msg => sprintf( - "fan '%s' status is '%s' [instance: %s, rpm: %s]", - $result->{name}, $result->{status}, $instance, $result->{rpm} - ) - ); + foreach my $fan_id (sort { $a <=> $b } keys %fans_data) { + my $fan = $fans_data{$fan_id}; + my $instance = $label_prefix . $fan->{name}; - $exit = $self->get_severity(section => 'fan', value => $result->{status}); - if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { - $self->{output}->output_add( - severity => $exit, - short_msg => sprintf( - "Fan '%s' status is '%s'", $result->{name}, $result->{status} - ) - ); - } + next if ($self->check_filter(section => 'fan', instance => $instance)); + $self->{components}->{fan}->{total}++; + + my $status = $fan->{status} // 'unknown'; - ($exit, $warn, $crit, $checked) = $self->get_severity_numeric(section => 'fan', instance => $instance, value => $result->{rpm}); - if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { $self->{output}->output_add( - severity => $exit, - short_msg => sprintf( - "fan '%s' speed is '%s' rpm", $result->{name}, $result->{rpm} + long_msg => sprintf( + "fan '%s' status is '%s'", + $instance, $status ) ); + + if (defined $fan->{rpm}) { + $self->{output}->perfdata_add( + nlabel => 'hardware.fan.speed.rpm', + unit => 'rpm', + instances => $instance, + value => $fan->{rpm}, + min => 0 + ); + } + + my $exit = $self->get_severity(section => 'fan', value => $status); + if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { + $self->{output}->output_add( + severity => $exit, + short_msg => sprintf("Fan '%s' status is '%s'", $instance, $status) + ); + } } - $self->{output}->perfdata_add( - nlabel => 'hardware.fan.speed.rpm', - unit => 'rpm', - instances => $result->{fanName}, - value => $result->{fanValue}, - warning => $warn, - critical => $crit, min => 0 - ); } } -1; +1; \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/components/psu.pm b/src/network/stormshield/snmp/mode/components/psu.pm index feef573266..01b81023e9 100644 --- a/src/network/stormshield/snmp/mode/components/psu.pm +++ b/src/network/stormshield/snmp/mode/components/psu.pm @@ -1,5 +1,5 @@ # -# Copyright 2024 Centreon (http://www.centreon.com/) +# 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 @@ -23,16 +23,26 @@ package network::stormshield::snmp::mode::components::psu; use strict; use warnings; -my $mapping = { +# Single-node OIDs — structure: oid. +my $mapping_single = { powered => { oid => '.1.3.6.1.4.1.11256.1.10.6.1.2' }, # snsPowerSupplyPowered status => { oid => '.1.3.6.1.4.1.11256.1.10.6.1.3' } # snsPowerSupplyStatus }; -my $oid_psuEntry = '.1.3.6.1.4.1.11256.1.10.6.1'; # snsPowerSupplyEntry +my $oid_psuEntry_single = '.1.3.6.1.4.1.11256.1.10.6.1'; # snsPowerSupplyEntry + +# HA OIDs — structure: oid.. +my $oid_psu_powered_ha = '.1.3.6.1.4.1.11256.1.11.10.1.2'; # snsNodePowerSupplyPowered +my $oid_psu_status_ha = '.1.3.6.1.4.1.11256.1.11.10.1.3'; # snsNodePowerSupplyStatus +my $oid_psuEntry_ha = '.1.3.6.1.4.1.11256.1.11.10.1'; # snsNodePowerSupplyEntry sub load { my ($self) = @_; - - push @{$self->{request}}, { oid => $oid_psuEntry }; + + if ($self->{is_ha}) { + push @{$self->{request}}, { oid => $oid_psuEntry_ha }; + } else { + push @{$self->{request}}, { oid => $oid_psuEntry_single }; + } } sub check { @@ -42,33 +52,61 @@ sub check { $self->{components}->{psu} = { name => 'psu', total => 0, skip => 0 }; return if ($self->check_filter(section => 'psu')); - foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{ $oid_psuEntry }})) { - next if ($oid !~ /^$mapping->{status}->{oid}\.(.*)$/); - my $instance = $1; - my $result = $self->{snmp}->map_instance(mapping => $mapping, results => $self->{results}->{ $oid_psuEntry }, instance => $instance); - - next if ($self->check_filter(section => 'psu', instance => $instance)); - - $self->{components}->{psu}->{total}++; - $self->{output}->output_add( - long_msg => sprintf( - "power supply '%s' status is '%s' [instance: %s]", - $instance, - $result->{status}, - $instance - ) - ); - - my $exit = $self->get_severity(section => 'psu', value => $result->{status}); - if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { + my $nodes = $self->{is_ha} ? $self->{ha_nodes} : ['0']; + my $serials = $self->{ha_serials}; + + foreach my $node_id (@$nodes) { + my $label_prefix = $self->{is_ha} ? $serials->{$node_id} . '_' : ''; + my %psus_data = (); + + if ($self->{is_ha}) { + foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{$oid_psuEntry_ha}})) { + if ($oid =~ /^$oid_psu_powered_ha\.$node_id\.(\d+)$/) { + $psus_data{$1}{powered} = $self->{results}->{$oid_psuEntry_ha}->{$oid}; + } elsif ($oid =~ /^$oid_psu_status_ha\.$node_id\.(\d+)$/) { + $psus_data{$1}{status} = $self->{results}->{$oid_psuEntry_ha}->{$oid}; + } + } + } else { + foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{$oid_psuEntry_single}})) { + next if ($oid !~ /^$mapping_single->{status}->{oid}\.(\d+)$/); + my $psu_id = $1; + my $result = $self->{snmp}->map_instance( + mapping => $mapping_single, + results => $self->{results}->{$oid_psuEntry_single}, + instance => $psu_id + ); + $psus_data{$psu_id} = $result; + } + } + + foreach my $psu_id (sort { $a <=> $b } keys %psus_data) { + my $psu = $psus_data{$psu_id}; + my $instance = $label_prefix . 'psu' . $psu_id; + + next if ($self->check_filter(section => 'psu', instance => $instance)); + $self->{components}->{psu}->{total}++; + + my $status = $psu->{status} // 'unknown'; + $self->{output}->output_add( - severity => $exit, - short_msg => sprintf( - "Power supply '%s' status is '%s'", $instance, $result->{status} + long_msg => sprintf( + "power supply '%s' status is '%s' [powered: %s]", + $instance, + $status, + (defined $psu->{powered} && $psu->{powered} == 1) ? 'yes' : 'no' ) ); + + my $exit = $self->get_severity(section => 'psu', value => $status); + if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { + $self->{output}->output_add( + severity => $exit, + short_msg => sprintf("Power supply '%s' status is '%s'", $instance, $status) + ); + } } } } -1; +1; \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/components/temperature.pm b/src/network/stormshield/snmp/mode/components/temperature.pm index 22aa4ad3f9..229820ba33 100644 --- a/src/network/stormshield/snmp/mode/components/temperature.pm +++ b/src/network/stormshield/snmp/mode/components/temperature.pm @@ -1,5 +1,5 @@ # -# Copyright 2024 Centreon (http://www.centreon.com/) +# 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 @@ -23,16 +23,20 @@ package network::stormshield::snmp::mode::components::temperature; use strict; use warnings; -my $mapping = { - cpuTemp => { oid => '.1.3.6.1.4.1.11256.1.10.7.1.2' } # snsCpuTemp -}; +# Single-node OIDs +my $oid_cpu_temp_single = '.1.3.6.1.4.1.11256.1.10.7.1.2'; # snsCpuTemp + +# HA OIDs — structure: oid.. +my $oid_cpu_temp_ha = '.1.3.6.1.4.1.11256.1.11.12.1.2'; # snsNodeCpuTemp sub load { my ($self) = @_; - - push @{$self->{request}}, { - oid => $mapping->{cpuTemp}->{oid} - }; + + if ($self->{is_ha}) { + push @{$self->{request}}, { oid => $oid_cpu_temp_ha }; + } else { + push @{$self->{request}}, { oid => $oid_cpu_temp_single }; + } } sub check { @@ -42,38 +46,84 @@ sub check { $self->{components}->{temperature} = { name => 'temperatures', total => 0, skip => 0 }; return if ($self->check_filter(section => 'temperature')); - foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{ $mapping->{cpuTemp}->{oid} }})) { - next if ($oid !~ /^$mapping->{cpuTemp}->{oid}\.(.*)$/); + my $nodes = $self->{is_ha} ? $self->{ha_nodes} : ['0']; + my $serials = $self->{ha_serials}; - my $result = $self->{snmp}->map_instance(mapping => $mapping, results => $self->{results}->{ $mapping->{cpuTemp}->{oid} }, instance => $1); + foreach my $node_id (@$nodes) { + my $label_prefix = $self->{is_ha} ? $serials->{$node_id} . '_' : ''; - my $instance = 'cpu' . $1; - next if ($self->check_filter(section => 'temperature', instance => $instance)); + my @cpu_ids = (); + my %cpu_temps = (); - $self->{components}->{temperature}->{total}++; - $self->{output}->output_add( - long_msg => sprintf( - "temperature '%s' is %s celsius [instance: %s]", - $instance, $result->{cpuTemp}, $instance - ) - ); + if ($self->{is_ha}) { + foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{$oid_cpu_temp_ha}})) { + next if ($oid !~ /^$oid_cpu_temp_ha\.$node_id\.(\d+)$/); + my $cpu_id = $1; + push @cpu_ids, $cpu_id; + $cpu_temps{$cpu_id} = $self->{results}->{$oid_cpu_temp_ha}->{$oid}; + $self->{components}->{temperature}->{total}++; + } + } else { + foreach my $oid ($self->{snmp}->oid_lex_sort(keys %{$self->{results}->{$oid_cpu_temp_single}})) { + next if ($oid !~ /^$oid_cpu_temp_single\.(\d+)$/); + my $cpu_id = $1; + push @cpu_ids, $cpu_id; + $cpu_temps{$cpu_id} = $self->{results}->{$oid_cpu_temp_single}->{$oid}; + $self->{components}->{temperature}->{total}++; + } + } + + next if (scalar @cpu_ids == 0); + @cpu_ids = sort { $a <=> $b } @cpu_ids; + + my $cpu_sum = 0; + foreach my $cpu_id (@cpu_ids) { + my $temp = $cpu_temps{$cpu_id}; + my $instance = $label_prefix . 'cpu' . $cpu_id; + + next if ($self->check_filter(section => 'temperature', instance => $instance)); + + $cpu_sum += $temp; - my ($exit, $warn, $crit, $checked) = $self->get_severity_numeric(section => 'temperature', instance => $instance, value => $result->{cpuTemp}); - if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { $self->{output}->output_add( - severity => $exit, - short_msg => sprintf( - "temperature '%s' is %s celsius", $instance, $result->{cpuTemp} + long_msg => sprintf( + "temperature '%s' is '%s' celsius", + $instance, $temp ) ); + + $self->{output}->perfdata_add( + nlabel => 'hardware.cpu.temperature.celsius', + unit => 'C', + instances => $instance, + value => $temp, + min => 0 + ); + + my ($exit) = $self->get_severity_numeric( + section => 'temperature', + instance => $instance, + value => $temp + ); + if (!$self->{output}->is_status(value => $exit, compare => 'ok', litteral => 1)) { + $self->{output}->output_add( + severity => $exit, + short_msg => sprintf( + "Temperature '%s' is '%s' celsius", $instance, $temp + ) + ); + } } + + my $cpu_avg = int($cpu_sum / scalar(@cpu_ids)); + my $avg_instance = $label_prefix . 'cpu_average_temp'; + $self->{output}->perfdata_add( - nlabel => 'hardware.temperature.celsius', - unit => 'C', - instances => $instance, - value => $result->{cpuTemp}, - warning => $warn, - critical => $crit, min => 0 + nlabel => 'hardware.cpu.average.temperature.celsius', + unit => 'C', + instances => $avg_instance, + value => $cpu_avg, + min => 0 ); } } diff --git a/src/network/stormshield/snmp/mode/connections.pm b/src/network/stormshield/snmp/mode/connections.pm index 615b97fbbc..761b90265c 100644 --- a/src/network/stormshield/snmp/mode/connections.pm +++ b/src/network/stormshield/snmp/mode/connections.pm @@ -1,5 +1,5 @@ # -# Copyright 2024 Centreon (http://www.centreon.com/) +# 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 @@ -24,38 +24,77 @@ use base qw(centreon::plugins::templates::counter); use strict; use warnings; -use Digest::MD5 qw(md5_hex); +use centreon::plugins::constants qw(:counters); sub set_counters { my ($self, %options) = @_; $self->{maps_counters_type} = [ - { name => 'global', type => 0 }, + { name => 'global', type => COUNTER_TYPE_GLOBAL }, + { name => 'policy', type => COUNTER_TYPE_INSTANCE }, ]; $self->{maps_counters}->{global} = [ { label => 'udp', set => { - key_values => [ { name => 'udp', per_second => 1 } ], - output_template => 'UDP : %d connections/s', + key_values => [ { name => 'udp' } ], + output_template => 'UDP : %d connections', perfdatas => [ { label => 'udp', template => '%d', min => 0, unit => 'con' } ] } }, { label => 'tcp', set => { - key_values => [ { name => 'tcp', per_second => 1 } ], - output_template => 'TCP : %d connections/s', + key_values => [ { name => 'tcp' } ], + output_template => 'TCP : %d connections', perfdatas => [ { label => 'tcp', template => '%d', min => 0, unit => 'con' } ] } + }, + { label => 'major', set => { + key_values => [ { name => 'major' } ], + output_template => 'Major Alarms : %d', + perfdatas => [ + { label => 'major', template => '%d', min => 0, unit => 'alarms' } + ] + } + }, + { label => 'minor', set => { + key_values => [ { name => 'minor' } ], + output_template => 'Minor Alarms : %d', + perfdatas => [ + { label => 'minor', template => '%d', min => 0, unit => 'alarms' } + ] + } } ]; + + $self->{maps_counters}->{policy} = [ + { label => 'policy-dummy', threshold => 0, set => { + key_values => [ { name => 'index' }, { name => 'name' }, { name => 'slot_name' }, + { name => 'active' }, { name => 'sync' } ], + closure_custom_output => $self->can('custom_policy_output'), + perfdatas => [] + } + } + ]; +} + +sub custom_policy_output { + my ($self, %options) = @_; + my $obj = $options{new_datas}; + return sprintf( + "Policy: '%s', slot: '%s', active: '%s', sync: %s", + $self->{result_values}->{name}, + $self->{result_values}->{slot_name}, + $self->{result_values}->{active}, + $self->{result_values}->{sync} ? 'True' : 'False' + ); } sub new { my ($class, %options) = @_; - my $self = $class->SUPER::new(package => __PACKAGE__, %options, statefile => 1); + my $self = $class->SUPER::new(package => __PACKAGE__, %options); bless $self, $class; $options{options}->add_options(arguments => { @@ -67,20 +106,47 @@ sub new { sub manage_selection { my ($self, %options) = @_; - $self->{cache_name} = 'fw_stormshield_' . $options{snmp}->get_hostname() . '_' . $options{snmp}->get_port() . '_' . $self->{mode} . '_' . md5_hex('all'); - - my $oid_ntqASQStatsStatefulUdpConn = '.1.3.6.1.4.1.11256.1.12.1.33.0'; - my $oid_ntqASQStatsStatefulTcpConn = '.1.3.6.1.4.1.11256.1.12.1.23.0'; + my $oid_snsASQStatsStatefulUdpConn = '.1.3.6.1.4.1.11256.1.12.1.33.0'; + my $oid_snsASQStatsStatefulTcpConn = '.1.3.6.1.4.1.11256.1.12.1.23.0'; + my $oid_snsASQStatsStatefulMajorAlarm = '.1.3.6.1.4.1.11256.1.12.1.12.0'; + my $oid_snsASQStatsStatefulMinorAlarm = '.1.3.6.1.4.1.11256.1.12.1.11.0'; - my $result = $options{snmp}->get_leef( - oids => [ $oid_ntqASQStatsStatefulUdpConn, $oid_ntqASQStatsStatefulTcpConn ], + my $result_asq = $options{snmp}->get_leef( + oids => [ $oid_snsASQStatsStatefulUdpConn, $oid_snsASQStatsStatefulTcpConn, $oid_snsASQStatsStatefulMajorAlarm, $oid_snsASQStatsStatefulMinorAlarm ], nothing_quit => 1 ); $self->{global} = { - udp => $result->{$oid_ntqASQStatsStatefulUdpConn}, - tcp => $result->{$oid_ntqASQStatsStatefulTcpConn} + udp => $result_asq->{$oid_snsASQStatsStatefulUdpConn}, + tcp => $result_asq->{$oid_snsASQStatsStatefulTcpConn}, + major => $result_asq->{$oid_snsASQStatsStatefulMajorAlarm}, + minor => $result_asq->{$oid_snsASQStatsStatefulMinorAlarm} }; + + + my $oid_snsPolicyIndex = '.1.3.6.1.4.1.11256.1.8.1.1.1'; + my $oid_snsPolicyName = '.1.3.6.1.4.1.11256.1.8.1.1.2'; + my $oid_snsPolicySlotName = '.1.3.6.1.4.1.11256.1.8.1.1.3'; + my $oid_snsPolicyActive = '.1.3.6.1.4.1.11256.1.8.1.1.4'; + my $oid_snsPolicySync = '.1.3.6.1.4.1.11256.1.8.1.1.5'; + + my $result_policy = $options{snmp}->get_table( + oid => '.1.3.6.1.4.1.11256.1.8.1.1', + nothing_quit => 1 + ); + + $self->{policy} = {}; + foreach my $oid (keys %{$result_policy}) { + next unless $oid =~ /^$oid_snsPolicyIndex\.(\d+)$/; + my $idx = $1; + $self->{policy}->{$idx} = { + index => $idx, + name => $result_policy->{"$oid_snsPolicyName.$idx"} // '', + slot_name => $result_policy->{"$oid_snsPolicySlotName.$idx"} // '', + active => $result_policy->{"$oid_snsPolicyActive.$idx"} // '', + sync => $result_policy->{"$oid_snsPolicySync.$idx"} // 0, + }; + } } 1; @@ -89,20 +155,42 @@ __END__ =head1 MODE -Check connections setup rate on Stormshield Firewall equipments. +Check connections setup rate and policy table on Stormshield Firewall equipments. =over 8 -=item B<--warning-*> +=item B<--warning-tcp> + +Warning threshold for TCP connections. + +=item B<--warning-udp> + +Warning threshold for UDP connections. + +=item B<--warning-major> + +Warning threshold for Major Alarms. + +=item B<--warning-minor> + +Warning threshold for Minor Alarms. + +=item B<--critical-tcp> + +Critical threshold for TCP connections. + +=item B<--critical-udp> + +Critical threshold for UDP connections. + +=item B<--critical-major> -Warning threshold. -Can be: 'tcp', 'udp' +Critical threshold for Major Alarms. -=item B<--critical-*> +=item B<--critical-minor> -Critical threshold. -Can be: 'tcp', 'udp' +Critical threshold for Minor Alarms. =back -=cut +=cut \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/ha_cluster.pm b/src/network/stormshield/snmp/mode/ha_cluster.pm new file mode 100644 index 0000000000..0b4a2fb128 --- /dev/null +++ b/src/network/stormshield/snmp/mode/ha_cluster.pm @@ -0,0 +1,334 @@ +# 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 network::stormshield::snmp::mode::ha_cluster; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); +use centreon::plugins::constants qw(:counters); + + +sub custom_status_output { + my ($self, %options) = @_; + + return sprintf( + "Configuration Synced: %s", + $self->{result_values}->{sync_status} + ); +} + +sub custom_node_output { + my ($self, %options) = @_; + + return sprintf( + "Dead Nodes: %s/%s (%s%%)", + $self->{result_values}->{dead_nodes}, + $self->{result_values}->{nb_nodes}, + $self->{result_values}->{dead_pct}, + ); +} + +sub custom_link_output { + my ($self, %options) = @_; + + return sprintf( + "Faulty Links: %s/%s (%s%%)", + $self->{result_values}->{faulty_links}, + $self->{result_values}->{nb_links}, + $self->{result_values}->{faulty_pct}, + ); +} + +sub custom_active_output { + my ($self, %options) = @_; + + return sprintf( + "Active Firewalls: %s/2", + $self->{result_values}->{nb_active}, + ); +} + + +sub custom_node_perfdata { + my ($self, %options) = @_; + my $nb = $self->{result_values}->{nb_nodes}; + my $warn = defined($nb) ? int($nb * 0.5) : undef; + my $crit = defined($nb) ? $nb : undef; + + $self->{output}->perfdata_add( + label => 'ha.dead_nodes.count', + value => $self->{result_values}->{dead_nodes}, + warning => $warn, + critical => $crit, + min => 0, + max => $nb, + ) +} + +sub custom_link_perfdata { + my ($self, %options) = @_; + my $nb = $self->{result_values}->{nb_links}; + my $warn = defined($nb) ? int($nb * 0.5) : undef; + my $crit = defined($nb) ? $nb : undef; + + $self->{output}->perfdata_add( + label => 'ha.faulty_links.count', + value => $self->{result_values}->{faulty_links}, + warning => $warn, + critical => $crit, + min => 0, + max => $nb, + ) +} + +sub custom_node_threshold { + my ($self, %options) = @_; + my $nb = $self->{result_values}->{nb_nodes}; + my $dead = $self->{result_values}->{dead_nodes}; + return 'OK' if !defined($nb) || $nb == 0; + return 'WARNING' if $dead >= int($nb*0.5); + return 'CRITICAL' if $dead >= $nb; + return 'OK'; +} + + +sub custom_link_threshold { + my ($self, %options) = @_; + my $nb = $self->{result_values}->{nb_links}; + my $dead = $self->{result_values}->{faulty_links}; + return 'OK' if !defined($nb) || $nb == 0; + return 'WARNING' if $dead >= int($nb*0.5); + return 'CRITICAL' if $dead >= $nb; + return 'OK'; +} + +sub custom_active_threshold { + my ($self, %options) = @_; + my $nb = $self->{result_values}->{nb_active}; + return 'OK' if !defined($nb) || $nb == 1; + return 'CRITICAL' if $nb == 2 || $nb == 0; + return 'UNKNOWN'; +} + +sub custom_sync_threshold { + my ($self, %options) = @_; + return 'WARNING' if $self->{result_values}->{sync_status} eq 'False'; + return 'OK'; +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'global', type => COUNTER_TYPE_GLOBAL, skipped_code => { -10 => 1 } }, + ]; + + $self->{maps_counters}->{global} = [ + { + label => 'dead-nodes', + set => { + key_values => [ { name => 'dead_nodes' }, { name => 'nb_nodes' }, { name => 'dead_pct' } ], + closure_custom_output => $self->can('custom_node_output'), + closure_custom_perfdata => $self->can('custom_node_perfdata'), + closure_custom_threshold_check => $self->can('custom_node_threshold'), + } + }, + { + label => 'faulty-links', + set => { + key_values => [ { name => 'faulty_links' }, { name => 'nb_links' }, { name => 'faulty_pct' } ], + closure_custom_output => $self->can('custom_link_output'), + closure_custom_perfdata => $self->can('custom_link_perfdata'), + closure_custom_threshold_check => $self->can('custom_link_threshold'), + } + }, + { + label => 'active-firewall', + set => { + key_values => [ { name => 'nb_active' } ], + closure_custom_output => $self->can('custom_active_output'), + closure_custom_threshold_check => $self->can('custom_active_threshold'), + } + }, + { + label => 'sync-status', + set => { + key_values => [ { name => 'sync_status' } ], + closure_custom_output => $self->can('custom_status_output'), + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => $self->can('custom_sync_threshold'), + } + }, + ]; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $options{options}->add_options(arguments => {}); + + $self->{output}->{option_results}->{verbose} = 1; + + return $self; +} + + +my $oid_snsNode = '.1.3.6.1.4.1.11256.1.11.7.1'; +my $oid_snsNbNode = '.1.3.6.1.4.1.11256.1.11.1.0'; +my $oid_snsNbDeadNode = '.1.3.6.1.4.1.11256.1.11.2.0'; +my $oid_snsNbActiveNode = '.1.3.6.1.4.1.11256.1.11.3.0'; +my $oid_snsNbHALinks = '.1.3.6.1.4.1.11256.1.11.5.0'; +my $oid_snsNbFaultyHALinks = '.1.3.6.1.4.1.11256.1.11.6.0'; +my $oid_snsHASyncStatus = '.1.3.6.1.4.1.11256.1.11.8.0'; + + +my %mapping = ( + snsNodeIndex => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.1' }, + snsFwSerial => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.2' }, + snsOnline => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.3' }, + snsOnline => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.4' }, + snsVersion => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.5' }, + snsHALicence => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.6' }, + snsHAQuality => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.7' }, + snsHAPriority => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.8' }, + snsHAStatusForced => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.9' }, + snsHAActive => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.10' }, +); + +my %map_online = ( 0 => 'False', 1 => 'True' ); +my %map_status = ( 0 => 'False', 1 => 'True' ); +my %map_act_pass = ( 2 => 'Passive', 1 => 'Active' ); +my %map_sync = ( 0 => 'False', 1 => 'True' ); + + +sub manage_selection { + my ($self, %options) = @_; + + my $snmp_result_scalar = $options{snmp}->get_leef( + oids => [ + $oid_snsNbNode, + $oid_snsNbDeadNode, + $oid_snsNbActiveNode, + $oid_snsNbHALinks, + $oid_snsNbFaultyHALinks, + $oid_snsHASyncStatus, + ], + nothing_quit => 0 + ); + + my $nb_nodes = $snmp_result_scalar->{$oid_snsNbNode} // 0; + my $dead_nodes = $snmp_result_scalar->{$oid_snsNbDeadNode} // 0; + my $nb_active = $snmp_result_scalar->{$oid_snsNbActiveNode} // 0; + my $nb_links = $snmp_result_scalar->{$oid_snsNbHALinks} // 0; + my $faulty_links = $snmp_result_scalar->{$oid_snsNbFaultyHALinks} // 0; + my $sync_raw = $snmp_result_scalar->{$oid_snsHASyncStatus} // -1; + + if (!(defined $nb_nodes && $nb_nodes > 0)) { + $self->{output}->output_add( + severity => 'UNKNOWN', + short_msg => 'No HA cluster detected' + ); + return; + } + + my $dead_pct = ($nb_nodes > 0) ? int(($dead_nodes / $nb_nodes) * 100) : 0; + my $faulty_pct = ($nb_links > 0) ? int(($faulty_links / $nb_links) * 100) : 0; + my $sync_str = $map_sync{$sync_raw} // 'UNKNOWN'; + + $self->{global} = { + dead_nodes => $dead_nodes, + nb_nodes => $nb_nodes, + dead_pct => $dead_pct, + faulty_links => $faulty_links, + nb_links => $nb_links, + faulty_pct => $faulty_pct, + nb_active => $nb_active, + sync_status => $sync_str, + }; + + my $snmp_result_table = $options{snmp}->get_table( + oid => $oid_snsNode, + nothing_quit => 1 + ); + + my %nodes; + foreach my $oid (sort keys %{$snmp_result_table}) { + foreach my $field (keys %mapping) { + my $col_oid = $mapping{$field}->{oid}; + next if !defined($col_oid); + if ($oid =~ /^\Q$col_oid\E\.(\d+)$/) { + my $idx = $1; + $nodes{$idx}->{$field} = $snmp_result_table->{$oid}; + } + } + } + + + my $cluster_desc = "---- Cluster Description ----\n"; + + foreach my $idx(sort { $a <=> $b } keys %nodes) { + my $n = $nodes{$idx}; + + my $serial = $n->{snsFwSerial} // 'N/A'; + my $model = $n->{snsOnline} // 'N/A'; + my $version = $n->{snsVersion} // 'N/A'; + my $status_f = $map_status{$n->{snsHAStatusForced} // 0 } // 'UNKNOWN'; + my $act_pass = $map_act_pass{$n->{snsHAActive} // 0 } // 'UNKNOWN'; + my $online = $map_online{$n->{snsOnline} // 0 } // 'UNKNOWN'; + my $licence = $n->{snsHALicence} // 'N/A'; + my $quality = $n->{snsHAQuality} // 'N/A'; + my $priority = $n->{snsHAPriority} // 'N/A'; + + + $cluster_desc .= sprintf( + "Serial: %s\nModel: %s\nVersion: %s\nStatus Forced: %s\nActive/Passive: %s\nOnline: %s\nLicense: %s\nQuality: %s\nPriority: %s\n%s\n", + $serial, + $model, + $version, + $status_f, + $act_pass, + $online, + $licence, + $quality, + $priority, + '-' x 25 + ); + } + + $self->{output}->output_add(long_msg => $cluster_desc); +} + +1; + +__END__ + +=head1 MODE + +Check Stormshield HA cluster global status. + +=over 8 + +=back + +=cut \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/hardware.pm b/src/network/stormshield/snmp/mode/hardware.pm index dc337cfb95..176fe62f77 100644 --- a/src/network/stormshield/snmp/mode/hardware.pm +++ b/src/network/stormshield/snmp/mode/hardware.pm @@ -1,5 +1,5 @@ # -# Copyright 2024 Centreon (http://www.centreon.com/) +# 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 @@ -25,36 +25,78 @@ use base qw(centreon::plugins::templates::hardware); use strict; use warnings; +my $oid_snsNodeIndex = '.1.3.6.1.4.1.11256.1.11.7.1.1'; +my $oid_snsFwSerial = '.1.3.6.1.4.1.11256.1.11.7.1.2'; + sub set_system { my ($self, %options) = @_; $self->{regexp_threshold_numeric_check_section_option} = '^(?:fan|temperature)$'; - + + $self->{cb_hook1} = 'detect_topology'; $self->{cb_hook2} = 'snmp_execute'; - + $self->{thresholds} = { fan => [ ['running', 'OK'], - ['.*', 'CRITICAL'] + ['.*', 'CRITICAL'] ], psu => [ - ['OK', 'OK'], - ['.*', 'CRITICAL'] + ['OK', 'OK'], + ['.*', 'CRITICAL'] ], - raid => [ - ['optimal', 'OK'], - ['.*', 'CRITICAL'] + disk => [ + ['PASSED', 'OK'], + ['NotSupported', 'OK'], + ['.*', 'CRITICAL'] ] }; - - $self->{components_path} = 'network::stormshield::snmp::mode::components'; - $self->{components_module} = ['disk', 'fan', 'psu', 'temperature']; + + $self->{components_path} = 'network::stormshield::snmp::mode::components'; + $self->{components_module} = ['temperature', 'fan', 'psu', 'disk']; } -sub snmp_execute { +sub detect_topology { my ($self, %options) = @_; - + $self->{snmp} = $options{snmp}; + + my $ha_result = $options{snmp}->get_table( + oid => $oid_snsNodeIndex, + nothing_quit => 0 + ); + + $self->{ha_nodes} = []; + $self->{ha_serials} = {}; + + if (!defined $ha_result || scalar(keys %$ha_result) == 0) { + # Single Node + $self->{is_ha} = 0; + push @{$self->{ha_nodes}}, '0'; + $self->{ha_serials}->{'0'} = 'single'; + return; + } + + # HA + $self->{is_ha} = 1; + foreach my $oid (sort keys %$ha_result) { + if ($oid =~ /^$oid_snsNodeIndex\.(\d+)$/) { + push @{$self->{ha_nodes}}, $1; + } + } + + my @serial_oids = map { "$oid_snsFwSerial.$_" } @{$self->{ha_nodes}}; + my $serial_res = $options{snmp}->get_leef(oids => \@serial_oids, nothing_quit => 0); + foreach my $node_id (@{$self->{ha_nodes}}) { + my $s = $serial_res->{"$oid_snsFwSerial.$node_id"}; + $self->{ha_serials}->{$node_id} = defined $s ? $s : "node$node_id"; + } +} + +sub snmp_execute { + my ($self, %options) = @_; + + $self->{snmp} = $options{snmp}; $self->{results} = $self->{snmp}->get_multiple_table(oids => $self->{request}); } @@ -74,7 +116,8 @@ __END__ =head1 MODE -Check hardware. +Check hardware components (temperature, fan, PSU, disk). +Automatically detects single-node and high-availability (HA) cluster configurations. =over 8 @@ -85,13 +128,14 @@ Can be: 'disk', 'fan', 'psu', 'temperature'. =item B<--filter> -Exclude the items given as a comma-separated list (example: --filter=fan). -You can also exclude items from specific instances: --filter=fan,1 +Exclude items given as a comma-separated list (example: --filter=fan). +You can also exclude specific instances: --filter=fan,1 =item B<--absent-problem> -Return an error if a component is not 'present' (default is skipping). -It can be set globally or for a specific instance: --absent-problem='component_name' or --absent-problem='component_name,instance_value'. +Return an error if a component is not present (default: skip). +Can be set globally or per instance: --absent-problem='component_name' or +--absent-problem='component_name,instance_value'. =item B<--no-component> @@ -99,27 +143,30 @@ Define the expected status if no components are found (default: critical). =item B<--threshold-overload> -Use this option to override the status returned by the plugin when the status label matches a regular expression (syntax: section,[instance,]status,regexp). +Override the status returned by the plugin when the status label matches a +regular expression (syntax: section,[instance,]status,regexp). Example: --threshold-overload='disk,WARNING,missing' =item B<--warning> -Set warning threshold for 'temperature', 'fan' (syntax: type,regexp,threshold) -Example: --warning='temperature,.*,40' +Set warning threshold for 'temperature' or 'fan' (syntax: type,regexp,threshold). +Example: --warning='temperature,.*,60' =item B<--critical> -Set critical threshold for 'temperature', 'fan' (syntax: type,regexp,threshold) -Example: --critical='temperature,.*,50' +Set critical threshold for 'temperature' or 'fan' (syntax: type,regexp,threshold). +Example: --critical='temperature,.*,70' =item B<--warning-count-*> -Define the warning threshold for the number of components of one type (replace '*' with the component type). +Define the warning threshold for the number of components of one type +(replace '*' with the component type). =item B<--critical-count-*> -Define the critical threshold for the number of components of one type (replace '*' with the component type). +Define the critical threshold for the number of components of one type +(replace '*' with the component type). =back -=cut +=cut \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/health.pm b/src/network/stormshield/snmp/mode/health.pm index 7fab848355..9364ed6d0d 100644 --- a/src/network/stormshield/snmp/mode/health.pm +++ b/src/network/stormshield/snmp/mode/health.pm @@ -1,5 +1,5 @@ # -# Copyright 2024 Centreon (http://www.centreon.com/) +# 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 @@ -25,26 +25,39 @@ use base qw(centreon::plugins::templates::counter); use strict; use warnings; use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); +use centreon::plugins::constants qw/:counters :values/; + +our $GLOBAL_PROBLEM; sub custom_service_status_output { my ($self, %options) = @_; - return sprintf( + my $msg = sprintf( "health: %s", $self->{result_values}->{health} ); -} -sub firewall_long_output { - my ($self, %options) = @_; + my $health = $self->{result_values}->{health}; + + if ($health !~ /minor/i && $health !~ /major/i) { + if (!$GLOBAL_PROBLEM) { + $self->{output}->output_add( + long_msg => "service '" . $self->{result_values}->{service} . "' " . $msg + ); + } + } - return "checking firewall '" . $options{instance_value}->{display} . "'"; + return $msg; } -sub prefix_firewall_output { +sub firewall_long_output { my ($self, %options) = @_; + my $display = $options{instance_value}->{display}; - return "firewall '" . $options{instance_value}->{display} . "' "; + if (defined $display && $display ne '') { + return "--------------Firewall $display--------------"; + } + return "------------------------------------------------------------"; } sub prefix_service_output { @@ -57,10 +70,16 @@ sub set_counters { my ($self, %options) = @_; $self->{maps_counters_type} = [ - { name => 'firewalls', type => 3, cb_prefix_output => 'prefix_firewall_output', cb_long_output => 'firewall_long_output', - indent_long_output => ' ', message_multiple => 'All firewalls are ok', + { name => 'firewalls', type => COUNTER_TYPE_GROUP, cb_long_output => 'firewall_long_output', + message_multiple => 'All firewalls are ok', group => [ - { name => 'services', display_long => 1, cb_prefix_output => 'prefix_service_output', message_multiple => 'All services are ok', type => 1, skipped_code => { -10 => 1 } } + { + name => 'services', + display_long => 1, + cb_prefix_output => 'prefix_service_output', + message_multiple => 'All services are ok', + type => COUNTER_TYPE_INSTANCE, + skipped_code => { -10 => 1 } } ] } ]; @@ -68,7 +87,7 @@ sub set_counters { $self->{maps_counters}->{services} = [ { label => 'service-status', - type => 2, + type => COUNTER_KIND_TEXT, warning_default => '%{health} =~ /minor/i', critical_default => '%{health} =~ /major/i', set => { @@ -94,18 +113,21 @@ sub new { } my $mapping = { + hamode => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.3' }, # snsHaModeHealth link => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.4' }, # snsHaLinkHealth powersupply => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.5' }, # snsPowerSupplyHealth fan => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.6' }, # snsFanHealth - cpu => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.7' }, # snsFanHealth + cpu => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.7' }, # snsCpuHealth memory => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.8' }, # snsMemHealth disk => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.9' }, # snsDiskHealth raid => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.10' }, # snsRaidHealth certificate => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.11' }, # snsCertHealth CRL => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.12' }, # snsCRLHealth - #password => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.13' }, # snsPasswdHealth - #cpu_temperature => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.14' }, # snsCpuTempHealth - #TPM => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.15' } # snsTPMHealth + TPM => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.13' }, # snsTPMHealth + password => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.14' }, # snsPasswdHealth + cpu_temperature => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.15' }, # snsCpuTempHealth + router => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.16' }, # snsRouterHealth + NTP => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.17' }, # snsNTPHealth }; my $oid_snsSerialHealth = '.1.3.6.1.4.1.11256.1.16.2.1.2'; @@ -130,13 +152,20 @@ sub manage_selection { } $self->{firewalls}->{$instance} = { - display => $serial, + display => $serial, services => {} }; } return if (scalar(keys %{$self->{firewalls}}) <= 0); + $GLOBAL_PROBLEM = 0; + + my @instances = keys %{$self->{firewalls}}; + if (scalar(@instances) == 1) { + $self->{firewalls}->{ $instances[0] }->{display} = ''; + } + $options{snmp}->load( oids => [ map($_->{oid}, values(%$mapping)) ], instances => [ keys %{$self->{firewalls}} ], @@ -147,9 +176,13 @@ sub manage_selection { my $result = $options{snmp}->map_instance(mapping => $mapping, results => $snmp_result, instance => $_); foreach my $service (keys %$result) { + my $health = $result->{$service}; + if (defined $health && ($health =~ /minor/i || $health =~ /major/i)) { + $GLOBAL_PROBLEM = 1; + } $self->{firewalls}->{$_}->{services}->{$service} = { service => $service, - health => $result->{$service} + health => $health }; } } @@ -186,4 +219,4 @@ You can use the following variables: %{health}, %{service} =back -=cut +=cut \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/interfaces_disco.pm b/src/network/stormshield/snmp/mode/interfaces_disco.pm new file mode 100644 index 0000000000..44ca98bcc4 --- /dev/null +++ b/src/network/stormshield/snmp/mode/interfaces_disco.pm @@ -0,0 +1,510 @@ +# +# 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 network::stormshield::snmp::mode::interfaces_disco; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use centreon::plugins::constants qw/:counters :values/; + +sub generic_interface_output { + my ($self, %options) = @_; + return sprintf("Interface %s, Address: %s, Mask: %s, Type: %s, Is Protected: %s", + $self->{result_values}->{if_name}, + $self->{result_values}->{if_address}, + $self->{result_values}->{if_mask}, + $self->{result_values}->{if_type}, + $self->{result_values}->{if_protected}, + ); +} + + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'interfaces', + type => COUNTER_TYPE_INSTANCE, + message_multiple => 'All interfaces are OK', + display_long => 1, + skipped_code => { -10 => 1 } + } + ]; + + $self->{maps_counters}->{interfaces} = [ + { + label => 'textual-info', + type => COUNTER_KIND_TEXT, + set => { + key_values => [ + { name => 'if_name' }, + { name => 'if_address' }, + { name => 'if_mask' }, + { name => 'if_type' }, + { name => 'if_protected' } + ], + closure_custom_output => $self->can('generic_interface_output'), + } + }, + { + label => 'throughput-in', + nlabel => 'interface.throughput.in.bitspersecond', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_throughput_in' }, + { name => 'if_name' } + ], + output_template => 'Throughput In: %s b/s', + perfdatas => [ + { + label => 'throughput_in', + value => 'if_throughput_in', + template => '%s', + unit => 'b/s', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-throughput-in'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-throughput-in'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'throughput-out', + nlabel => 'interface.throughput.out.bitspersecond', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_throughput_out' }, + { name => 'if_name' } + ], + output_template => 'Throughput Out: %s b/s', + perfdatas => [ + { + label => 'throughput_out', + value => 'if_throughput_out', + template => '%s', + unit => 'b/s', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-throughput-out'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-throughput-out'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'current-tcp', + nlabel => 'interface.connections.tcp.count', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_current_tcp' }, + { name => 'if_name' } + ], + output_template => 'Current TCP Connections: %s', + perfdatas => [ + { + label => 'current_tcp_con', + value => 'if_current_tcp', + template => '%s', + unit => 'con', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-current-tcp'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-current-tcp'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'current-udp', + nlabel => 'interface.connections.udp.count', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_current_udp' }, + { name => 'if_name' } + ], + output_template => 'Current UDP Connections: %s', + perfdatas => [ + { + label => 'current_udp_con', + value => 'if_current_udp', + template => '%s', + unit => 'con', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-current-udp'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-current-udp'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'tcp-in', + nlabel => 'interface.traffic.tcp.in.bytes', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_tcp_in' }, + { name => 'if_name' } + ], + output_template => 'TCP In: %s bytes', + perfdatas => [ + { + label => 'tcp_in', + value => 'if_tcp_in', + template => '%s', + unit => 'bytes', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-tcp-in'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-tcp-in'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'tcp-out', + nlabel => 'interface.traffic.tcp.out.bytes', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_tcp_out' }, + { name => 'if_name' } + ], + output_template => 'TCP Out: %s bytes', + perfdatas => [ + { + label => 'tcp_out', + value => 'if_tcp_out', + template => '%s', + unit => 'bytes', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-tcp-out'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-tcp-out'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'udp-in', + nlabel => 'interface.traffic.udp.in.bytes', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_udp_in' }, + { name => 'if_name' } + ], + output_template => 'UDP In: %s bytes', + perfdatas => [ + { + label => 'udp_in', + value => 'if_udp_in', + template => '%s', + unit => 'bytes', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-udp-in'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-udp-in'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'udp-out', + nlabel => 'interface.traffic.udp.out.bytes', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_udp_out' }, + { name => 'if_name' } + ], + output_template => 'UDP Out: %s bytes', + perfdatas => [ + { + label => 'udp_out', + value => 'if_udp_out', + template => '%s', + unit => 'bytes', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-udp-out'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-udp-out'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'packets-accepted', + nlabel => 'interface.packets.accepted.count', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_pack_accepted' }, + { name => 'if_name' } + ], + output_template => 'Packets Accepted: %s', + perfdatas => [ + { + label => 'pack_accepted', + value => 'if_pack_accepted', + template => '%s', + unit => 'packets', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-packets-accepted'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-packets-accepted'), + instance_use => 'if_name' + } + ] + } + }, + { + label => 'packets-blocked', + nlabel => 'interface.packets.blocked.count', + type => COUNTER_KIND_METRIC, + set => { + key_values => [ + { name => 'if_pack_blocked' }, + { name => 'if_name' } + ], + output_template => 'Packets Blocked: %s', + perfdatas => [ + { + label => 'pack_blocked', + value => 'if_pack_blocked', + template => '%s', + unit => 'packets', + min => 0, + label_extra_instance => 1, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-packets-blocked'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-packets-blocked'), + instance_use => 'if_name' + } + ] + } + }, + ]; +} + +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; +} + +my $mapping = { + snsifName => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.2' }, + snsifAddr => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.4' }, + snsifMask => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.5' }, + snsifType => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.6' }, + snsifProtected => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.37' }, + snsifPktAccepted => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.11' }, + snsifPktBlocked => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.12' }, + snsifTcpConnCount => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.23' }, + snsifUdpConnCount => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.24' }, + snsifInCurThroughput => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.25' }, + snsifOutCurThroughput => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.26' }, + snsifInTcpBytes => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.31' }, + snsifOutTcpBytes => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.32' }, + snsifInUdpBytes => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.33' }, + snsifOutUdpBytes => { oid => '.1.3.6.1.4.1.11256.1.4.1.1.34' } +}; +my $oid_snsifEntry = '.1.3.6.1.4.1.11256.1.4.1.1'; + + +sub manage_selection { + my ($self, %options) = @_; + + my $snmp_result = $options{snmp}->get_table( + oid => $oid_snsifEntry, + start => $mapping->{snsifName}->{oid}, + end => $mapping->{snsifProtected}->{oid}, + ); + + + $self->{interfaces} = {}; + foreach my $oid (keys %{$snmp_result}) { + next if ($oid !~ /^$mapping->{snsifName}->{oid}\.(.*)$/); + my $instance = $1; + my $result = $options{snmp}->map_instance(mapping => $mapping, results => $snmp_result, instance => $instance); + if ((defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '' && $result->{snsifName} ne $self->{option_results}->{filter_name}) || ($result->{snsifName} =~ /^mgmt/)) { + next; + } + + $self->{interfaces}->{$instance} = { + if_name => $result->{snsifName} // '-', + if_address => $result->{snsifAddr} // '-', + if_mask => $result->{snsifMask} // '-', + if_type => $result->{snsifType} // '-', + if_protected => ($result->{snsifProtected} ? 'true' : 'false'), + if_throughput_in => $result->{snsifInCurThroughput} // 0, + if_throughput_out => $result->{snsifOutCurThroughput} // 0, + if_current_tcp => $result->{snsifTcpConnCount} // 0, + if_current_udp => $result->{snsifUdpConnCount} // 0, + if_tcp_in => $result->{snsifInTcpBytes} // 0, + if_tcp_out => $result->{snsifOutTcpBytes} // 0, + if_udp_in => $result->{snsifInUdpBytes} // 0, + if_udp_out => $result->{snsifOutUdpBytes} // 0, + if_pack_accepted => $result->{snsifPktAccepted} // 0, + if_pack_blocked => $result->{snsifPktBlocked} // 0, + }; + } + + if (scalar(keys %{$self->{interfaces}}) <= 0) { + $self->{output}->add_option_msg( + short_msg => 'No interface found matching: ' . ($self->{option_results}->{filter_name} // '') + ); + $self->{output}->option_exit(); + } +} + +sub disco_format { + my ($self, %options) = @_; + + $self->{output}->add_disco_format(elements => ['if_name', 'if_address', 'if_mask', 'if_type', 'if_protected', 'if_throughput_in', 'if_throughput_out', 'if_current_tcp', 'if_current_udp', 'if_tcp_in', 'if_tcp_out', 'if_udp_in', 'if_udp_out', 'if_pack_accepted', 'if_pack_blocked']); +} + +sub disco_show { + my ($self, %options) = @_; + $self->manage_selection(%options); + foreach my $instance (sort keys %{$self->{interfaces}}) { + $self->{output}->add_disco_entry(%{$self->{interfaces}->{$instance}}); + } +} + +1; + +__END__ + +=head1 MODE + +This mode retrieves and displays the status of interfaces on the Stormshield device, including their name, global in/out traffic, TCP/UDP in/out traffic, and packet acceptance/blocking statistics. + +=over 8 + +=item B<--filter-name> + +Filter by name to only display this interface + +=item B<--warning-throughput-in> + +Set the warning threshold for throughput in (bits per second). + +=item B<--critical-throughput-in> + +Set the critical threshold for throughput in (bits per second). + +=item B<--warning-throughput-out> + +Set the warning threshold for throughput out (bits per second). + +=item B<--critical-throughput-out> + +Set the critical threshold for throughput out (bits per second). + +=item B<--warning-current-tcp> + +Set the warning threshold for current TCP connections. + +=item B<--critical-current-tcp> + +Set the critical threshold for current TCP connections. + +=item B<--warning-current-udp> + +Set the warning threshold for current UDP connections. + +=item B<--critical-current-udp> + +Set the critical threshold for current UDP connections. + +=item B<--warning-tcp-in> + +Set the warning threshold for TCP in traffic (bytes). + +=item B<--critical-tcp-in> + +Set the critical threshold for TCP in traffic (bytes). + +=item B<--warning-tcp-out> + +Set the warning threshold for TCP out traffic (bytes). + +=item B<--critical-tcp-out> + +Set the critical threshold for TCP out traffic (bytes). + +=item B<--warning-udp-in> + +Set the warning threshold for UDP in traffic (bytes). + +=item B<--critical-udp-in> + +Set the critical threshold for UDP in traffic (bytes). + +=item B<--warning-udp-out> + +Set the warning threshold for UDP out traffic (bytes). + +=item B<--critical-udp-out> + +Set the critical threshold for UDP out traffic (bytes). + +=item B<--warning-packets-accepted> + +Set the warning threshold for packets accepted. + +=item B<--critical-packets-accepted> + +Set the critical threshold for packets accepted. + +=item B<--warning-packets-blocked> + +Set the warning threshold for packets blocked. + +=item B<--critical-packets-blocked> + +Set the critical threshold for packets blocked. + +=back + +=cut \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/memorydetailed.pm b/src/network/stormshield/snmp/mode/memorydetailed.pm index 36fc7b46b3..bd82835187 100644 --- a/src/network/stormshield/snmp/mode/memorydetailed.pm +++ b/src/network/stormshield/snmp/mode/memorydetailed.pm @@ -1,5 +1,5 @@ # -# Copyright 2024 Centreon (http://www.centreon.com/) +# 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 @@ -24,152 +24,285 @@ use base qw(centreon::plugins::templates::counter); use strict; use warnings; +use centreon::plugins::constants qw/:counters :values/; +use centreon::plugins::misc; -sub prefix_memory_output { - my ($self, %options) = @_; - - return 'Memory usage '; -} +my @mem_labels = qw(asq icmp frag host system dtrack socket etherstate user); sub set_counters { my ($self, %options) = @_; - + $self->{maps_counters_type} = [ - { name => 'global', type => 0, cb_prefix_output => 'prefix_memory_output', skipped_code => { -10 => 1 } } + { name => 'global', type => 0, skipped_code => { NO_VALUE() => 1 } } ]; - + $self->{maps_counters}->{global} = [ - { label => 'total', nlabel => 'memory.usage.percentage', set => { - key_values => [ { name => 'total' } ], - output_template => 'total: %.2f %%', + { label => 'asq', set => { + key_values => [ { name => 'asq' } ], + output_template => "ASQ: %s%%", perfdatas => [ - { template => '%.2f', min => 0, max => 100, unit => '%' } + { + label => 'mem_asq', + value => 'asq', + template => '%s', + unit => '%', + min => 0, + max => 100, + } ] } }, - { label => 'host', nlabel => 'memory.protected_host.percentage', set => { - key_values => [ { name => 'host' } ], - output_template => 'protected host: %.2f %%', + { label => 'icmp', set => { + key_values => [ { name => 'icmp' } ], + output_template => "ICMP: %s%%", perfdatas => [ - { template => '%.2f', min => 0, max => 100, unit => '%' } + { + label => 'mem_icmp', + value => 'icmp', + template => '%s', + unit => '%', + min => 0, + max => 100, + } ] } }, - { label => 'frag', nlabel => 'memory.fragmented.percentage', set => { + { label => 'frag', set => { key_values => [ { name => 'frag' } ], - output_template => 'fragmented: %.2f %%', + output_template => "Frag: %s%%", perfdatas => [ - { template => '%.2f', min => 0, max => 100, unit => '%' } + { + label => 'mem_frag', + value => 'frag', + template => '%s', + unit => '%', + min => 0, + max => 100, + } ] } }, - { label => 'conn', nlabel => 'memory.connections.percentage', set => { - key_values => [ { name => 'conn' } ], - output_template => 'connections: %.2f %%', + { label => 'host', set => { + key_values => [ { name => 'host' } ], + output_template => "Host: %s%%", perfdatas => [ - { template => '%.2f', min => 0, max => 100, unit => '%' } + { + label => 'mem_host', + value => 'host', + template => '%s', + unit => '%', + min => 0, + max => 100, + } ] } }, - { label => 'icmp', nlabel => 'memory.icmp.percentage', set => { - key_values => [ { name => 'icmp' } ], - output_template => 'icmp: %.2f %%', + { label => 'system', set => { + key_values => [ { name => 'system' } ], + output_template => "System: %s%%", perfdatas => [ - { template => '%.2f', min => 0, max => 100, unit => '%' } + { + label => 'mem_system', + value => 'system', + template => '%s', + unit => '%', + min => 0, + max => 100, + } ] } }, - { label => 'dtrack', nlabel => 'memory.data_tracking.percentage', set => { + { label => 'dtrack', set => { key_values => [ { name => 'dtrack' } ], - output_template => 'data tracking: %.2f %%', + output_template => "Data Tracking: %s%%", perfdatas => [ - { template => '%.2f', min => 0, max => 100, unit => '%' } + { + label => 'mem_dtrack', + value => 'dtrack', + template => '%s', + unit => '%', + min => 0, + max => 100, + } ] } }, - { label => 'dyn', nlabel => 'memory.dynamic.percentage', set => { - key_values => [ { name => 'dyn' } ], - output_template => 'dynamic: %.2f %%', + { label => 'socket', set => { + key_values => [ { name => 'socket' } ], + output_template => "Socket: %s%%", perfdatas => [ - { template => '%.2f', min => 0, max => 100, unit => '%' } + { + label => 'mem_socket', + value => 'socket', + template => '%s', + unit => '%', + min => 0, + max => 100, + } ] } }, - { label => 'etherstate', nlabel => 'memory.ether_state.percentage', set => { - key_values => [ { name => 'ether_state' } ], - output_template => 'ether state: %.2f %%', + { label => 'etherstate', set => { + key_values => [ { name => 'etherstate' } ], + output_template => "EtherState: %s%%", perfdatas => [ - { template => '%.2f', min => 0, max => 100, unit => '%' } + { + label => 'mem_etherstate', + value => 'etherstate', + template => '%s', + unit => '%', + min => 0, + max => 100, + } ] } }, + # only for version >= 4.8.9 + { label => 'user', set => { + key_values => [ { name => 'user' } ], + output_template => "User: %s%%", + perfdatas => [ + { + label => 'mem_user', + value => 'user', + template => '%s', + unit => '%', + min => 0, + max => 100, + } + ] + } + } ]; } sub new { my ($class, %options) = @_; - my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + my $self = $class->SUPER::new(package => __PACKAGE__, %options); bless $self, $class; - - $options{options}->add_options(arguments => {}); - + + $options{options}->add_options(arguments => { + "warning:s" => { name => 'warning_memory' }, + "critical:s" => { name => 'critical_memory' }, + }); + return $self; } +sub check_options { + my ($self, %options) = @_; + $self->SUPER::init(%options); + + foreach my $label (@mem_labels) { + if (($self->{perfdata}->threshold_validate(label => 'warning-' . $label,value => $self->{option_results}->{warning_memory})) == 0) { + $self->{output}->add_option_msg(short_msg => "Wrong warning threshold '" . $self->{option_results}->{warning_memory} . "'."); + $self->{output}->option_exit(); + } + if (($self->{perfdata}->threshold_validate(label => 'critical-' . $label,value => $self->{option_results}->{critical_memory})) == 0) { + $self->{output}->add_option_msg(short_msg => "Wrong critical threshold '" . $self->{option_results}->{critical_memory} . "'."); + $self->{output}->option_exit(); + } + } +} + sub manage_selection { my ($self, %options) = @_; - my $oid_snsMem = '.1.3.6.1.4.1.11256.1.10.3.0'; - my $snmp_result = $options{snmp}->get_leef( - oids => [ $oid_snsMem ], - nothing_quit => 1 - ); - - # host,frag,icmp,conn,dtrack,dyn - # host,frag,icmp,conn,ether_state,dtrack,dyn - - my @values = split(/,/, $snmp_result->{$oid_snsMem}); - my $fields = scalar(@values); - if ($fields == 7) { - $self->{global} = { - total => $values[0] + $values[1] + $values[2] + $values[3] + $values[4] + $values[5] + $values[6], - host => $values[0], - frag => $values[1], - icmp => $values[2], - conn => $values[3], - ether_state => $values[4], - dtrack => $values[5], - dyn => $values[6] - }; - } elsif ($fields == 6) { - $self->{global} = { - total => $values[0] + $values[1] + $values[2] + $values[3] + $values[4] + $values[5], - host => $values[0], - frag => $values[1], - icmp => $values[2], - conn => $values[3], - dtrack => $values[4], - dyn => $values[5] - }; + my $oid_os_version = '.1.3.6.1.4.1.11256.1.0.2.0'; + + # Récupération de la version pour déterminer quel OID utiliser + my $snmp_result = $options{snmp}->get_leef(oids => [ $oid_os_version ], nothing_quit => 1); + my $version_raw = $snmp_result->{$oid_os_version}; + my $version_clean = $version_raw; + $version_clean =~ s/([0-9]+(?:\.[0-9]+)*).*/$1/; + + my %mem_values; + my $is_new_version = centreon::plugins::misc::minimal_version($version_clean, '4.8.9'); + + if ($is_new_version) { + # version >= 4.8.9 + my $oids = [ + '.1.3.6.1.4.1.11256.1.10.10.1.2.1', # snsMemHost + '.1.3.6.1.4.1.11256.1.10.10.1.3.1', # snsMemFrag + '.1.3.6.1.4.1.11256.1.10.10.1.4.1', # snsMemIcmp + '.1.3.6.1.4.1.11256.1.10.10.1.5.1', # snsMemConn + '.1.3.6.1.4.1.11256.1.10.10.1.6.1', # snsMemEther + '.1.3.6.1.4.1.11256.1.10.10.1.7.1', # snsMemDataTrack + '.1.3.6.1.4.1.11256.1.10.10.1.8.1', # snsMemSystem + '.1.3.6.1.4.1.11256.1.10.10.1.9.1', # snsMemUser + '.1.3.6.1.4.1.11256.1.10.10.1.10.1' # snsMemMbuf + ]; + + $snmp_result = $options{snmp}->get_leef(oids => $oids, nothing_quit => 1); + + $mem_values{'host'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.2.1'}; + $mem_values{'frag'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.3.1'}; + $mem_values{'icmp'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.4.1'}; + $mem_values{'asq'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.5.1'}; + $mem_values{'etherstate'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.6.1'}; + $mem_values{'dtrack'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.7.1'}; + $mem_values{'system'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.8.1'}; + $mem_values{'user'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.9.1'}; + $mem_values{'socket'} = $snmp_result->{'.1.3.6.1.4.1.11256.1.10.10.1.10.1'}; + + } else { + # oid for version < 4.8.9 + my $oid_snsMem = '.1.3.6.1.4.1.11256.1.10.3.0'; + $snmp_result = $options{snmp}->get_leef(oids => [ $oid_snsMem ], nothing_quit => 1); + + my @values = split(/,/, $snmp_result->{$oid_snsMem}); + my $fields = scalar(@values); + if ($fields >= 7) { + $mem_values{'host'} = $values[0]; + $mem_values{'frag'} = $values[1]; + $mem_values{'icmp'} = $values[2]; + $mem_values{'asq'} = $values[3]; + $mem_values{'etherstate'} = $values[4]; + $mem_values{'dtrack'} = $values[5]; + $mem_values{'system'} = $values[6]; + + if ($fields == 8) { + $mem_values{'socket'} = $values[7]; + } + } + } + + # Cleaning and Converting Values + foreach my $key (keys %mem_values) { + if (defined $mem_values{$key}) { + $mem_values{$key} =~ s/%//g; + $mem_values{$key} =~ s/^\s+|\s+$//g; + # If the value is not a number, it is removed + if ($mem_values{$key} !~ /^\d+\.?\d*$/) { + delete $mem_values{$key}; + } + } else { + delete $mem_values{$key}; + } } + + $self->{global} = \%mem_values; } + 1; __END__ =head1 MODE -Check memory detailed. +Check memory utilization on Stormshield firewalls. =over 8 -=item B<--warning-*> B<--critical-*> +=item B<--warning> + +Set warning threshold for all memory types (asq, icmp, frag, host, system, dtrack, socket, etherstate, user). + +=item B<--critical> -Thresholds. -Can be: 'total', 'host', 'frag', 'conn', 'icmp', -'dtrack', 'dyn', 'etherstate'. +Set critical threshold for all memory types (asq, icmp, frag, host, system, dtrack, socket, etherstate, user). =back -=cut +=cut \ No newline at end of file diff --git a/src/network/stormshield/snmp/plugin.pm b/src/network/stormshield/snmp/plugin.pm index 068b62a77d..2d81fe59ae 100644 --- a/src/network/stormshield/snmp/plugin.pm +++ b/src/network/stormshield/snmp/plugin.pm @@ -31,21 +31,24 @@ sub new { $self->{version} = '1.0'; $self->{modes} = { - 'cpu' => 'snmp_standard::mode::cpu', - 'cpu-detailed' => 'snmp_standard::mode::cpudetailed', - 'connections' => 'network::stormshield::snmp::mode::connections', - 'interfaces' => 'snmp_standard::mode::interfaces', - 'list-interfaces' => 'snmp_standard::mode::listinterfaces', - 'load' => 'snmp_standard::mode::loadaverage', - 'ha-nodes' => 'network::stormshield::snmp::mode::hanodes', - 'hardware' => 'network::stormshield::snmp::mode::hardware', - 'health' => 'network::stormshield::snmp::mode::health', - 'memory' => 'os::freebsd::snmp::mode::memory', - 'memory-detailed' => 'network::stormshield::snmp::mode::memorydetailed', - 'qos' => 'network::stormshield::snmp::mode::qos', - 'storage' => 'snmp_standard::mode::storage', - 'swap' => 'snmp_standard::mode::swap', - 'vpn-status' => 'network::stormshield::snmp::mode::vpnstatus' + 'auto-update' => 'network::stormshield::snmp::mode::auto_update', + 'cpu' => 'snmp_standard::mode::cpu', + 'cpu-detailed' => 'snmp_standard::mode::cpudetailed', + 'connections' => 'network::stormshield::snmp::mode::connections', + 'interfaces-disco' => 'network::stormshield::snmp::mode::interfaces_disco', + 'interfaces' => 'snmp_standard::mode::interfaces', + 'list-interfaces' => 'snmp_standard::mode::listinterfaces', + 'load' => 'snmp_standard::mode::loadaverage', + 'ha-nodes' => 'network::stormshield::snmp::mode::hanodes', + 'ha-cluster' => 'network::stormshield::snmp::mode::ha_cluster', + 'hardware' => 'network::stormshield::snmp::mode::hardware', + 'health' => 'network::stormshield::snmp::mode::health', + 'memory' => 'snmp_standard::mode::memory', + 'memory-detailed' => 'network::stormshield::snmp::mode::memorydetailed', + 'qos' => 'network::stormshield::snmp::mode::qos', + 'storage' => 'snmp_standard::mode::storage', + 'swap' => 'snmp_standard::mode::swap', + 'vpn-status' => 'network::stormshield::snmp::mode::vpnstatus' }; return $self; From 447c36638463aca086cdeb5b8849b23eb7dad481 Mon Sep 17 00:00:00 2001 From: crsuser Date: Tue, 23 Jun 2026 10:24:47 +0200 Subject: [PATCH 02/10] Fix: changed type in set_counters from 0 to "COUNTER_TYPE_GLOBAL" convention --- src/network/stormshield/snmp/mode/memorydetailed.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network/stormshield/snmp/mode/memorydetailed.pm b/src/network/stormshield/snmp/mode/memorydetailed.pm index bd82835187..7878c93fd3 100644 --- a/src/network/stormshield/snmp/mode/memorydetailed.pm +++ b/src/network/stormshield/snmp/mode/memorydetailed.pm @@ -33,7 +33,7 @@ sub set_counters { my ($self, %options) = @_; $self->{maps_counters_type} = [ - { name => 'global', type => 0, skipped_code => { NO_VALUE() => 1 } } + { name => 'global', type => COUNTER_TYPE_GLOBAL, skipped_code => { NO_VALUE() => 1 } } ]; $self->{maps_counters}->{global} = [ @@ -305,4 +305,4 @@ Set critical threshold for all memory types (asq, icmp, frag, host, system, dtra =back -=cut \ No newline at end of file +=cut From 8bf4d36e7f4f9dc603f2177d7d32da330dda7f33 Mon Sep 17 00:00:00 2001 From: crsuser Date: Tue, 30 Jun 2026 14:06:35 +0200 Subject: [PATCH 03/10] Fix: wrong oid name in ha_cluster.pm --- src/network/stormshield/snmp/mode/ha_cluster.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/network/stormshield/snmp/mode/ha_cluster.pm b/src/network/stormshield/snmp/mode/ha_cluster.pm index 0b4a2fb128..bd05424ecf 100644 --- a/src/network/stormshield/snmp/mode/ha_cluster.pm +++ b/src/network/stormshield/snmp/mode/ha_cluster.pm @@ -207,7 +207,7 @@ my %mapping = ( snsNodeIndex => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.1' }, snsFwSerial => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.2' }, snsOnline => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.3' }, - snsOnline => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.4' }, + snsModel => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.4' }, snsVersion => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.5' }, snsHALicence => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.6' }, snsHAQuality => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.7' }, @@ -216,7 +216,7 @@ my %mapping = ( snsHAActive => { oid => '.1.3.6.1.4.1.11256.1.11.7.1.10' }, ); -my %map_online = ( 0 => 'False', 1 => 'True' ); +my %map_online = ( 2 => 'False', 1 => 'True' ); my %map_status = ( 0 => 'False', 1 => 'True' ); my %map_act_pass = ( 2 => 'Passive', 1 => 'Active' ); my %map_sync = ( 0 => 'False', 1 => 'True' ); @@ -291,7 +291,7 @@ sub manage_selection { my $n = $nodes{$idx}; my $serial = $n->{snsFwSerial} // 'N/A'; - my $model = $n->{snsOnline} // 'N/A'; + my $model = $n->{snsModel} // 'N/A'; my $version = $n->{snsVersion} // 'N/A'; my $status_f = $map_status{$n->{snsHAStatusForced} // 0 } // 'UNKNOWN'; my $act_pass = $map_act_pass{$n->{snsHAActive} // 0 } // 'UNKNOWN'; @@ -331,4 +331,4 @@ Check Stormshield HA cluster global status. =back -=cut \ No newline at end of file +=cut From 93046b331ad44ed2f1a22d220eafe4015a70b68a Mon Sep 17 00:00:00 2001 From: crsuser Date: Mon, 6 Jul 2026 11:28:48 +0200 Subject: [PATCH 04/10] Add discovery mode router_disco.pm --- .../stormshield/snmp/mode/router_disco.pm | 653 ++++++++++++++++++ 1 file changed, 653 insertions(+) create mode 100644 src/network/stormshield/snmp/mode/router_disco.pm diff --git a/src/network/stormshield/snmp/mode/router_disco.pm b/src/network/stormshield/snmp/mode/router_disco.pm new file mode 100644 index 0000000000..abac3b3c60 --- /dev/null +++ b/src/network/stormshield/snmp/mode/router_disco.pm @@ -0,0 +1,653 @@ +# +# 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 network::stormshield::snmp::mode::router_disco; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold_ng); +use centreon::plugins::constants qw/:counters :values/; +use centreon::plugins::misc; + + +sub build_gateways_detail { + my ($self, %options) = @_; + + my @lines = "----------------------------List of the Gateways----------------------------"; + + foreach my $gateway (@{ $options{gateways} }) { + my $packet_loss_str = defined($gateway->{packet_loss_prct}) + ? sprintf('%s %%', $gateway->{packet_loss_prct}) + : 'not monitored'; + my $active_str = $gateway->{active} == 1 ? 'yes' : 'no'; + + if ($gateway->{latency} == 0) { + push @lines, sprintf( + "Gateway '%s' (%s - %s) state: %s, active: %s, unreachable (latency: 0 ms), packet loss: %s.", + $gateway->{display}, $gateway->{gateway_type}, $gateway->{address}, + $gateway->{state}, $active_str, $packet_loss_str + ); + } else { + push @lines, sprintf( + "Gateway '%s' (%s - %s) state: %s, active: %s, latency: %s ms, jitter: %s ms, packet loss: %s.", + $gateway->{display}, $gateway->{gateway_type}, $gateway->{address}, + $gateway->{state}, $active_str, $gateway->{latency}, $gateway->{jitter}, $packet_loss_str + ); + } + } + + return join("\n", @lines); +} + +sub custom_router_status_output { + my ($self, %options) = @_; + + my $principal_total = $self->{result_values}->{principal_total}; + my $principal_down = $self->{result_values}->{principal_down}; + my $backup_total = $self->{result_values}->{backup_total}; + my $backup_down = $self->{result_values}->{backup_down}; + + if ($principal_down == 0 && $backup_down == 0) { + return sprintf( + 'All gateways are working (%d/%d principal, %d/%d backup).', + $principal_total, $principal_total, $backup_total, $backup_total + ); + } + + my $principal_seg = ($principal_total > 0) + ? ($principal_down > 0 + ? sprintf('%d/%d principal down', $principal_down, $principal_total) + : sprintf('%d/%d principal up', $principal_total, $principal_total)) + : ''; + my $backup_seg = ($backup_total > 0) + ? ($backup_down > 0 + ? sprintf('%d/%d backup down', $backup_down, $backup_total) + : sprintf('%d/%d backup up', $backup_total, $backup_total)) + : ''; + + if ($principal_down > 0 && $backup_down > 0) { + return sprintf(join(', ', grep { $_ ne '' } ($principal_seg, $backup_seg)) . '.'); + } + + if ($principal_seg ne '' && $backup_seg ne '') { + return sprintf('%s (%s).', $principal_seg, $backup_seg); + } + return $principal_seg ne '' ? $principal_seg . '.' : $backup_seg . '.'; +} + +sub custom_gateways_calc { + my ($self, %options) = @_; + + $self->{result_values}->{display} = $options{new_datas}->{$self->{instance} . '_display'}; + $self->{result_values}->{gateways} = $options{new_datas}->{$self->{instance} . '_gateways'}; + + return 0; +} + +sub custom_packetloss_threshold { + my ($self, %options) = @_; + + my $severity_rank = { ok => 0, warning => 1, unknown => 2, critical => 3 }; + my $worst_status = 'ok'; + + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + next if (!defined($gateway->{packet_loss_prct})); + + my $gw_status = $self->{perfdata}->threshold_check( + value => $gateway->{packet_loss_prct}, + threshold => [ + { label => 'critical-packet-loss', exit_litteral => 'critical' }, + { label => 'warning-packet-loss', exit_litteral => 'warning' } + ] + ); + + if ($severity_rank->{$gw_status} > $severity_rank->{$worst_status}) { + $worst_status = $gw_status; + } + } + + return $worst_status; +} + +sub custom_packetloss_output { + my ($self, %options) = @_; + + my @bad; + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + next if (!defined($gateway->{packet_loss_prct})); + + my $gw_status = $self->{perfdata}->threshold_check( + value => $gateway->{packet_loss_prct}, + threshold => [ + { label => 'critical-packet-loss', exit_litteral => 'critical' }, + { label => 'warning-packet-loss', exit_litteral => 'warning' } + ] + ); + next if ($gw_status eq 'ok'); + + push @bad, sprintf( + "packet loss: gateway '%s' %s%% (%s)", + $gateway->{display}, $gateway->{packet_loss_prct}, $gw_status + ); + } + + return join(', ', @bad); +} + +sub custom_packetloss_perfdata { + my ($self, %options) = @_; + + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + next if (!defined($gateway->{packet_loss_prct})); + + $self->{output}->perfdata_add( + nlabel => 'router.gateway.packetloss.percentage', + instances => [$self->{result_values}->{display}, $gateway->{display}], + value => $gateway->{packet_loss_prct}, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-packet-loss'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-packet-loss'), + unit => '%', + min => 0, + max => 100 + ); + } +} + +sub custom_latency_threshold { + my ($self, %options) = @_; + + my $severity_rank = { ok => 0, warning => 1, unknown => 2, critical => 3 }; + my $worst_status = 'ok'; + + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + # latency 0 means unreachable: already covered by router-status, not a latency problem + next if ($gateway->{latency} == 0); + + my $gw_status = $self->{perfdata}->threshold_check( + value => $gateway->{latency}, + threshold => [ + { label => 'critical-latency', exit_litteral => 'critical' }, + { label => 'warning-latency', exit_litteral => 'warning' } + ] + ); + + if ($severity_rank->{$gw_status} > $severity_rank->{$worst_status}) { + $worst_status = $gw_status; + } + } + + return $worst_status; +} + +sub custom_latency_output { + my ($self, %options) = @_; + + my @bad; + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + next if ($gateway->{latency} == 0); + + my $gw_status = $self->{perfdata}->threshold_check( + value => $gateway->{latency}, + threshold => [ + { label => 'critical-latency', exit_litteral => 'critical' }, + { label => 'warning-latency', exit_litteral => 'warning' } + ] + ); + next if ($gw_status eq 'ok'); + + push @bad, sprintf( + "latency: gateway '%s' %s ms (%s)", + $gateway->{display}, $gateway->{latency}, $gw_status + ); + } + + return join(', ', @bad); +} + +sub custom_latency_perfdata { + my ($self, %options) = @_; + + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + next if ($gateway->{latency} == 0); + + $self->{output}->perfdata_add( + nlabel => 'router.gateway.latency.milliseconds', + instances => [$self->{result_values}->{display}, $gateway->{display}], + value => $gateway->{latency}, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-latency'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-latency'), + unit => 'ms', + min => 0 + ); + } +} + + +sub custom_principal_count_calc { + my ($self, %options) = @_; + + $self->{result_values}->{display} = $options{new_datas}->{$self->{instance} . '_display'}; + $self->{result_values}->{down} = $options{new_datas}->{$self->{instance} . '_principal_down'}; + $self->{result_values}->{total} = $options{new_datas}->{$self->{instance} . '_principal_total'}; + + return 0; +} + +sub custom_principal_count_threshold { + my ($self, %options) = @_; + + return $self->{perfdata}->threshold_check( + value => $self->{result_values}->{down}, + threshold => [ + { label => 'critical-principal-count', exit_litteral => 'critical' }, + { label => 'warning-principal-count', exit_litteral => 'warning' } + ] + ); +} + +# silent: the down count is already shown in the router-status line, this counter +# only exists to drive warning/critical off a user-defined numeric threshold +sub custom_principal_count_output { + my ($self, %options) = @_; + + return ''; +} + +sub custom_principal_count_perfdata { + my ($self, %options) = @_; + + $self->{output}->perfdata_add( + nlabel => 'router.gateway.principal.down.count', + instances => [$self->{result_values}->{display}], + value => $self->{result_values}->{down}, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-principal-count'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-principal-count'), + min => 0, + max => $self->{result_values}->{total} + ); +} + +sub custom_backup_count_calc { + my ($self, %options) = @_; + + $self->{result_values}->{display} = $options{new_datas}->{$self->{instance} . '_display'}; + $self->{result_values}->{down} = $options{new_datas}->{$self->{instance} . '_backup_down'}; + $self->{result_values}->{total} = $options{new_datas}->{$self->{instance} . '_backup_total'}; + + return 0; +} + +sub custom_backup_count_threshold { + my ($self, %options) = @_; + + return $self->{perfdata}->threshold_check( + value => $self->{result_values}->{down}, + threshold => [ + { label => 'critical-backup-count', exit_litteral => 'critical' }, + { label => 'warning-backup-count', exit_litteral => 'warning' } + ] + ); +} + +sub custom_backup_count_output { + my ($self, %options) = @_; + + return ''; +} + +sub custom_backup_count_perfdata { + my ($self, %options) = @_; + + $self->{output}->perfdata_add( + nlabel => 'router.gateway.backup.down.count', + instances => [$self->{result_values}->{display}], + value => $self->{result_values}->{down}, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-backup-count'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-backup-count'), + min => 0, + max => $self->{result_values}->{total} + ); +} + + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { + name => 'router', + type => COUNTER_TYPE_INSTANCE, + message_multiple => 'All gateways are ok', + display_long => 0, + message_separator => ' ', + skipped_code => { -10 => 1 } + } + ]; + + $self->{maps_counters}->{router} = [ + { + label => 'router-status', + type => 2, + warning_default => '%{one_principal_down} == 1', + critical_default => '%{all_down} == 1', + set => { + key_values => [ + { name => 'one_principal_down' }, { name => 'all_down' }, { name => 'display' }, + { name => 'principal_down' }, { name => 'principal_total' }, + { name => 'backup_down' }, { name => 'backup_total' } + ], + closure_custom_output => $self->can('custom_router_status_output'), + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => \&catalog_status_threshold_ng + } + }, + { + label => 'packet-loss', + nlabel => 'router.gateway.packetloss.percentage', + type => COUNTER_KIND_METRIC, + threshold => 0, + set => { + key_values => [ { name => 'display' }, { name => 'gateways' } ], + closure_custom_calc => $self->can('custom_gateways_calc'), + closure_custom_output => $self->can('custom_packetloss_output'), + closure_custom_perfdata => $self->can('custom_packetloss_perfdata'), + closure_custom_threshold_check => $self->can('custom_packetloss_threshold') + } + }, + { + label => 'latency', + nlabel => 'router.gateway.latency.milliseconds', + type => COUNTER_KIND_METRIC, + threshold => 0, + set => { + key_values => [ { name => 'display' }, { name => 'gateways' } ], + closure_custom_calc => $self->can('custom_gateways_calc'), + closure_custom_output => $self->can('custom_latency_output'), + closure_custom_perfdata => $self->can('custom_latency_perfdata'), + closure_custom_threshold_check => $self->can('custom_latency_threshold') + } + }, + { + label => 'principal-down-count', + nlabel => 'router.gateway.principal.down.count', + type => COUNTER_KIND_METRIC, + threshold => 0, + set => { + key_values => [ { name => 'display' }, { name => 'principal_down' }, { name => 'principal_total' } ], + closure_custom_calc => $self->can('custom_principal_count_calc'), + closure_custom_output => $self->can('custom_principal_count_output'), + closure_custom_perfdata => $self->can('custom_principal_count_perfdata'), + closure_custom_threshold_check => $self->can('custom_principal_count_threshold') + } + }, + { + label => 'backup-down-count', + nlabel => 'router.gateway.backup.down.count', + type => COUNTER_KIND_METRIC, + threshold => 0, + set => { + key_values => [ { name => 'display' }, { name => 'backup_down' }, { name => 'backup_total' } ], + closure_custom_calc => $self->can('custom_backup_count_calc'), + closure_custom_output => $self->can('custom_backup_count_output'), + closure_custom_perfdata => $self->can('custom_backup_count_perfdata'), + closure_custom_threshold_check => $self->can('custom_backup_count_threshold') + } + } + ]; +} + +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 => { + 'warning-packet-loss:s' => { name => 'warning_packet_loss' }, + 'critical-packet-loss:s' => { name => 'critical_packet_loss' }, + 'warning-latency:s' => { name => 'warning_latency' }, + 'critical-latency:s' => { name => 'critical_latency' }, + 'warning-principal-count:s' => { name => 'warning_principal_count' }, + 'critical-principal-count:s' => { name => 'critical_principal_count' }, + 'warning-backup-count:s' => { name => 'warning_backup_count' }, + 'critical-backup-count:s' => { name => 'critical_backup_count' }, + 'filter-name:s' => { name => 'filter_name' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); + + my @thresholds = ( + ['warning-packet-loss', 'warning_packet_loss'], + ['critical-packet-loss', 'critical_packet_loss'], + ['warning-latency', 'warning_latency'], + ['critical-latency', 'critical_latency'], + ['warning-principal-count', 'warning_principal_count'], + ['critical-principal-count', 'critical_principal_count'], + ['warning-backup-count', 'warning_backup_count'], + ['critical-backup-count', 'critical_backup_count'] + ); + + foreach my $threshold (@thresholds) { + my ($label, $option_name) = @$threshold; + + if (($self->{perfdata}->threshold_validate(label => $label, value => $self->{option_results}->{$option_name})) == 0) { + $self->{output}->add_option_msg(short_msg => "Wrong threshold '" . $label . "' value '" . $self->{option_results}->{$option_name} . "'."); + $self->{output}->option_exit(); + } + } +} + +my $mapping = { + snsRouteRouterName => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.4' }, + snsRouteGatewayName => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.5' }, + snsRouteGatewayAddr => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.6' }, + snsRouteGatewayType => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.7' }, + snsRouteState => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.9' }, + snsRouteActive => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.11' }, + snsRouteLatency => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.18' }, + snsRouteJitter => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.19' }, + snsRoutePacketLossPrctRaw => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.23' } +}; + +sub manage_selection { + my ($self, %options) = @_; + + my $snmp_result = $options{snmp}->get_multiple_table( + oids => [ + { oid => $mapping->{snsRouteRouterName}->{oid} }, + { oid => $mapping->{snsRouteGatewayName}->{oid} }, + { oid => $mapping->{snsRouteGatewayAddr}->{oid} }, + { oid => $mapping->{snsRouteGatewayType}->{oid} }, + { oid => $mapping->{snsRouteState}->{oid} }, + { oid => $mapping->{snsRouteActive}->{oid} }, + { oid => $mapping->{snsRouteLatency}->{oid} }, + { oid => $mapping->{snsRouteJitter}->{oid} }, + { oid => $mapping->{snsRoutePacketLossPrctRaw}->{oid} } + ], + nothing_quit => 1 + ); + + $self->{router} = {}; + foreach my $oid (keys %{$snmp_result->{ $mapping->{snsRouteRouterName}->{oid} }}) { + next if ($oid !~ /^$mapping->{snsRouteRouterName}->{oid}\.(.*)$/); + my $instance = $1; + my $result = $options{snmp}->map_instance(mapping => $mapping, results => $snmp_result, instance => $instance); + + next if ( + defined($self->{option_results}->{filter_name}) && $self->{option_results}->{filter_name} ne '' + && lc($result->{snsRouteRouterName}) ne lc($self->{option_results}->{filter_name}) + ); + + my $router_name = $result->{snsRouteRouterName}; + if (!defined($self->{router}->{$router_name})) { + $self->{router}->{$router_name} = { + display => $router_name, + gateways => [] + }; + } + + my $is_principal = ($result->{snsRouteGatewayType} =~ /^principal$/i) ? 1 : 0; + my $is_down; + if ($is_principal) { + # for a principal gateway, active=no while state=up is itself a problem + # it's ready/up but not actually carrying traffic -> counted as down + $is_down = ( + $result->{snsRouteState} =~ /^(down|undef)$/i + || $result->{snsRouteActive} == 0 + || $result->{snsRouteLatency} == 0 + ) ? 1 : 0; + } else { + # for a backup gateway, active=no is the normal/expected state when it's + # not currently taking over -> it does not make the gateway "down" + $is_down = ( + $result->{snsRouteState} =~ /^(down|undef)$/i + || $result->{snsRouteLatency} == 0 + ) ? 1 : 0; + } + + my $raw = $result->{snsRoutePacketLossPrctRaw}; + + push @{ $self->{router}->{$router_name}->{gateways} }, { + display => $result->{snsRouteGatewayName}, + address => $result->{snsRouteGatewayAddr}, + gateway_type => $result->{snsRouteGatewayType}, + state => $result->{snsRouteState}, + active => $result->{snsRouteActive}, + latency => $result->{snsRouteLatency}, + jitter => $result->{snsRouteJitter}, + packet_loss_prct => ($raw < 0 ? undef : $raw / 10), + is_down => $is_down + }; + } + + if (scalar(keys %{$self->{router}}) <= 0) { + $self->{output}->add_option_msg(short_msg => 'No routes found matching filters.'); + $self->{output}->option_exit(); + } + + foreach my $router_name (keys %{$self->{router}}) { + my @gateways = @{ $self->{router}->{$router_name}->{gateways} }; + my @principal_gateways = grep { $_->{gateway_type} =~ /^principal$/i } @gateways; + my @backup_gateways = grep { $_->{gateway_type} =~ /^backup$/i } @gateways; + + my $principal_total = scalar(@principal_gateways); + my $principal_down = scalar(grep { $_->{is_down} == 1 } @principal_gateways); + my $backup_total = scalar(@backup_gateways); + my $backup_down = scalar(grep { $_->{is_down} == 1 } @backup_gateways); + + $self->{router}->{$router_name}->{principal_total} = $principal_total; + $self->{router}->{$router_name}->{principal_down} = $principal_down; + $self->{router}->{$router_name}->{backup_total} = $backup_total; + $self->{router}->{$router_name}->{backup_down} = $backup_down; + + $self->{router}->{$router_name}->{one_principal_down} = ($principal_down > 0) ? 1 : 0; + $self->{router}->{$router_name}->{all_down} = ( + scalar(@gateways) > 0 && $principal_down == $principal_total && $backup_down == $backup_total + ) ? 1 : 0; + + $self->{output}->output_add( + long_msg => $self->build_gateways_detail(gateways => $self->{router}->{$router_name}->{gateways}) + ); + } +} + +sub disco_format { + my ($self, %options) = @_; + + $self->{output}->add_disco_format(elements => ['display', 'gateway_count', 'principal_count', 'backup_count', 'gateways_summary']); +} + +sub disco_show { + my ($self, %options) = @_; + + $self->manage_selection(%options); + foreach my $router_name (sort keys %{$self->{router}}) { + my $router = $self->{router}->{$router_name}; + my @gateways = @{ $router->{gateways} }; + + $self->{output}->add_disco_entry( + display => $router->{display}, + gateway_count => scalar(@gateways), + principal_count => scalar(grep { $_->{gateway_type} =~ /^principal$/i } @gateways), + backup_count => scalar(grep { $_->{gateway_type} =~ /^backup$/i } @gateways), + gateways_summary => join(', ', map { $_->{display} . '(' . $_->{gateway_type} . ')' } @gateways) + ); + } +} + +1; + +__END__ + +=head1 MODE + +Discovery mode for Stormshield SD-WAN/VPN routes, grouped by router. +This mode produces a single Centreon service per router, with every gateway of that router (latency, jitter, packet loss, reachability state) +folded into that one service's output and perfdata, plus an aggregated router-level status. + +=over 8 + +=item B<--warning-principal-count> + +Warning threshold on the number of Principal gateways currently DOWN for the router. + +=item B<--critical-principal-count> + +Critical threshold on the number of Principal gateways currently DOWN for the router. + +=item B<--warning-backup-count> + +Warning threshold on the number of Backup gateways currently DOWN for the router. + +=item B<--critical-backup-count> + +Critical threshold on the number of Backup gateways currently DOWN for the router. + +=item B<--warning-packet-loss> + +Warning threshold on packet loss (in percent), applied to C for each gateway of the router. + +=item B<--critical-packet-loss> + +Critical threshold on packet loss (in percent), applied to C for each gateway of the router. + +=item B<--warning-latency> + +Warning threshold on latency (in milliseconds), applied to C for each gateway of the router. +Gateways with a latency of 0 (unreachable) are excluded from this check, since they are already covered by the down logic above. + +=item B<--critical-latency> + +Critical threshold on latency (in milliseconds), applied to C for each gateway of the router. +Gateways with a latency of 0 (unreachable) are excluded from this check, since they are already covered by the down logic above. + +=item B<--filter-name> + +Filter routers on the exact router name (for discovery). + +=back + +=cut From b365bf8154897b7489f4ec385900908cf17c816e Mon Sep 17 00:00:00 2001 From: crsuser Date: Mon, 6 Jul 2026 11:30:39 +0200 Subject: [PATCH 05/10] Declared router_disco.pm in plugin.pm --- src/network/stormshield/snmp/plugin.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/network/stormshield/snmp/plugin.pm b/src/network/stormshield/snmp/plugin.pm index 2d81fe59ae..75c1518f67 100644 --- a/src/network/stormshield/snmp/plugin.pm +++ b/src/network/stormshield/snmp/plugin.pm @@ -48,7 +48,8 @@ sub new { 'qos' => 'network::stormshield::snmp::mode::qos', 'storage' => 'snmp_standard::mode::storage', 'swap' => 'snmp_standard::mode::swap', - 'vpn-status' => 'network::stormshield::snmp::mode::vpnstatus' + 'vpn-status' => 'network::stormshield::snmp::mode::vpnstatus', + 'router-disco' => 'network::stormshield::snmp::mode::router_disco' }; return $self; From cd7eceba096ae6e61b0e631884f08ab5fccf0235 Mon Sep 17 00:00:00 2001 From: crsuser Date: Mon, 6 Jul 2026 13:36:02 +0200 Subject: [PATCH 06/10] Fix Latency and PacketLoss output --- src/network/stormshield/snmp/mode/router_disco.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network/stormshield/snmp/mode/router_disco.pm b/src/network/stormshield/snmp/mode/router_disco.pm index abac3b3c60..a815c8fbcc 100644 --- a/src/network/stormshield/snmp/mode/router_disco.pm +++ b/src/network/stormshield/snmp/mode/router_disco.pm @@ -150,7 +150,7 @@ sub custom_packetloss_output { ); } - return join(', ', @bad); + return join(', ', @bad) . '.'; } sub custom_packetloss_perfdata { @@ -220,7 +220,7 @@ sub custom_latency_output { ); } - return join(', ', @bad); + return join(', ', @bad) . '.'; } sub custom_latency_perfdata { From a8583616ccd98dc48a224a3556b52f4b91f88e0b Mon Sep 17 00:00:00 2001 From: crsuser Date: Wed, 8 Jul 2026 17:30:52 +0200 Subject: [PATCH 07/10] Fix: add version-based filtering for OIDs in router_disco.pm --- .../stormshield/snmp/mode/router_disco.pm | 52 +++++++++++++++++-- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/network/stormshield/snmp/mode/router_disco.pm b/src/network/stormshield/snmp/mode/router_disco.pm index a815c8fbcc..fca26babb9 100644 --- a/src/network/stormshield/snmp/mode/router_disco.pm +++ b/src/network/stormshield/snmp/mode/router_disco.pm @@ -469,12 +469,41 @@ my $mapping = { snsRouteActive => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.11' }, snsRouteLatency => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.18' }, snsRouteJitter => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.19' }, + snsRoutePacketLossPrctOld => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.20' }, snsRoutePacketLossPrctRaw => { oid => '.1.3.6.1.4.1.11256.1.14.1.1.23' } }; sub manage_selection { my ($self, %options) = @_; + my $oid_snsVersion = '.1.3.6.1.4.1.11256.1.18.2.0'; + + my $snmp_result_version = $options{snmp}->get_leef( + oids => [ $oid_snsVersion ], + nothing_quit => 0, + ); + + my $use_old_packet_loss_oid = 0; + my $version_clean = ''; + + if (defined $snmp_result_version && defined $snmp_result_version->{$oid_snsVersion}) { + $version_clean = $snmp_result_version->{$oid_snsVersion}; + $version_clean =~ s/([0-9]+(?:\.[0-9]+)*).*/$1/; + + if (!centreon::plugins::misc::minimal_version($version_clean, '5.1.0')) { + $use_old_packet_loss_oid = 1; + } + } else { + $use_old_packet_loss_oid = 1; + } + + my %packet_loss_oid_map = ( + use_old => $mapping->{snsRoutePacketLossPrctOld}->{oid}, + use_new => $mapping->{snsRoutePacketLossPrctRaw}->{oid} + ); + + my $packet_loss_oid = $use_old_packet_loss_oid ? $packet_loss_oid_map{use_old} : $packet_loss_oid_map{use_new}; + my $snmp_result = $options{snmp}->get_multiple_table( oids => [ { oid => $mapping->{snsRouteRouterName}->{oid} }, @@ -485,7 +514,7 @@ sub manage_selection { { oid => $mapping->{snsRouteActive}->{oid} }, { oid => $mapping->{snsRouteLatency}->{oid} }, { oid => $mapping->{snsRouteJitter}->{oid} }, - { oid => $mapping->{snsRoutePacketLossPrctRaw}->{oid} } + { oid => $packet_loss_oid } ], nothing_quit => 1 ); @@ -527,8 +556,23 @@ sub manage_selection { || $result->{snsRouteLatency} == 0 ) ? 1 : 0; } - - my $raw = $result->{snsRoutePacketLossPrctRaw}; + + my $packet_loss_value; + my $raw = $result->{ $use_old_packet_loss_oid ? 'snsRoutePacketLossPrctOld' : 'snsRoutePacketLossPrctRaw' }; + + if (defined $raw) { + if ($use_old_packet_loss_oid) { + if ($raw =~ /^-?[0-9]+(\.[0-9]+)?$/) { + $packet_loss_value = $raw; + } else { + $packet_loss_value = undef; + } + } else { + $packet_loss_value = ($raw < 0 ? undef : $raw / 10); + } + } else { + $packet_loss_value = undef; + } push @{ $self->{router}->{$router_name}->{gateways} }, { display => $result->{snsRouteGatewayName}, @@ -538,7 +582,7 @@ sub manage_selection { active => $result->{snsRouteActive}, latency => $result->{snsRouteLatency}, jitter => $result->{snsRouteJitter}, - packet_loss_prct => ($raw < 0 ? undef : $raw / 10), + packet_loss_prct => $packet_loss_value, is_down => $is_down }; } From a6e74438679c4ef1e98375729ed33b15852540ae Mon Sep 17 00:00:00 2001 From: crsuser Date: Fri, 10 Jul 2026 15:20:12 +0200 Subject: [PATCH 08/10] Added jitter in counters, file : router_disco.pm --- .../stormshield/snmp/mode/router_disco.pm | 99 ++++++++++++++++++- 1 file changed, 97 insertions(+), 2 deletions(-) diff --git a/src/network/stormshield/snmp/mode/router_disco.pm b/src/network/stormshield/snmp/mode/router_disco.pm index fca26babb9..aee939a05d 100644 --- a/src/network/stormshield/snmp/mode/router_disco.pm +++ b/src/network/stormshield/snmp/mode/router_disco.pm @@ -242,6 +242,76 @@ sub custom_latency_perfdata { } +sub custom_jitter_threshold { + my ($self, %options) = @_; + + my $severity_rank = { ok => 0, warning => 1, unknown => 2, critical => 3 }; + my $worst_status = 'ok'; + + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + # if latency 0 jitter is not valid + next if ($gateway->{latency} == 0); + + my $gw_status = $self->{perfdata}->threshold_check( + value => $gateway->{jitter}, + threshold => [ + { label => 'critical-jitter', exit_litteral => 'critical' }, + { label => 'warning-jitter', exit_litteral => 'warning' } + ] + ); + + if ($severity_rank->{$gw_status} > $severity_rank->{$worst_status}) { + $worst_status = $gw_status; + } + } + + return $worst_status; +} + +sub custom_jitter_output { + my ($self, %options) = @_; + + my @bad; + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + next if ($gateway->{latency} == 0); + + my $gw_status = $self->{perfdata}->threshold_check( + value => $gateway->{jitter}, + threshold => [ + { label => 'critical-jitter', exit_litteral => 'critical' }, + { label => 'warning-jitter', exit_litteral => 'warning' } + ] + ); + next if ($gw_status eq 'ok'); + + push @bad, sprintf( + "jitter: gateway '%s' %s ms (%s)", + $gateway->{display}, $gateway->{jitter}, $gw_status + ); + } + + return join(', ', @bad) . '.'; +} + +sub custom_jitter_perfdata { + my ($self, %options) = @_; + + foreach my $gateway (@{ $self->{result_values}->{gateways} }) { + next if ($gateway->{latency} == 0); + + $self->{output}->perfdata_add( + nlabel => 'router.gateway.jitter.milliseconds', + instances => [$self->{result_values}->{display}, $gateway->{display}], + value => $gateway->{jitter}, + warning => $self->{perfdata}->get_perfdata_for_output(label => 'warning-jitter'), + critical => $self->{perfdata}->get_perfdata_for_output(label => 'critical-jitter'), + unit => 'ms', + min => 0 + ); + } +} + + sub custom_principal_count_calc { my ($self, %options) = @_; @@ -386,6 +456,19 @@ sub set_counters { closure_custom_threshold_check => $self->can('custom_latency_threshold') } }, + { + label => 'jitter', + nlabel => 'router.gateway.jitter.milliseconds', + type => COUNTER_KIND_METRIC, + threshold => 0, + set => { + key_values => [ { name => 'display' }, { name => 'gateways' } ], + closure_custom_calc => $self->can('custom_gateways_calc'), + closure_custom_output => $self->can('custom_jitter_output'), + closure_custom_perfdata => $self->can('custom_jitter_perfdata'), + closure_custom_threshold_check => $self->can('custom_jitter_threshold') + } + }, { label => 'principal-down-count', nlabel => 'router.gateway.principal.down.count', @@ -425,6 +508,8 @@ sub new { 'critical-packet-loss:s' => { name => 'critical_packet_loss' }, 'warning-latency:s' => { name => 'warning_latency' }, 'critical-latency:s' => { name => 'critical_latency' }, + 'warning-jitter:s' => { name => 'warning_jitter' }, + 'critical-jitter:s' => { name => 'critical_jitter' }, 'warning-principal-count:s' => { name => 'warning_principal_count' }, 'critical-principal-count:s' => { name => 'critical_principal_count' }, 'warning-backup-count:s' => { name => 'warning_backup_count' }, @@ -444,6 +529,8 @@ sub check_options { ['critical-packet-loss', 'critical_packet_loss'], ['warning-latency', 'warning_latency'], ['critical-latency', 'critical_latency'], + ['warning-jitter', 'warning_jitter'], + ['critical-jitter', 'critical_jitter'], ['warning-principal-count', 'warning_principal_count'], ['critical-principal-count', 'critical_principal_count'], ['warning-backup-count', 'warning_backup_count'], @@ -672,11 +759,11 @@ Critical threshold on the number of Backup gateways currently DOWN for the route =item B<--warning-packet-loss> -Warning threshold on packet loss (in percent), applied to C for each gateway of the router. +Warning threshold on packet loss (in percent), applied to C / C for each gateway of the router. =item B<--critical-packet-loss> -Critical threshold on packet loss (in percent), applied to C for each gateway of the router. +Critical threshold on packet loss (in percent), applied to C / C for each gateway of the router. =item B<--warning-latency> @@ -688,6 +775,14 @@ Gateways with a latency of 0 (unreachable) are excluded from this check, since t Critical threshold on latency (in milliseconds), applied to C for each gateway of the router. Gateways with a latency of 0 (unreachable) are excluded from this check, since they are already covered by the down logic above. +=item B<--warning-jitter> + +Warning threshold on jitter (in milliseconds), applied to C for each gateway of the router. + +=item B<--critical-jitter> + +Critical threshold on jitter (in milliseconds), applied to C for each gateway of the router. + =item B<--filter-name> Filter routers on the exact router name (for discovery). From 75f626f1307abe3f04663d5a5f0dbbe97a02d99d Mon Sep 17 00:00:00 2001 From: crsuser Date: Wed, 15 Jul 2026 15:44:11 +0200 Subject: [PATCH 09/10] Fix: OID name link -> halink in health.pm --- src/network/stormshield/snmp/mode/health.pm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/network/stormshield/snmp/mode/health.pm b/src/network/stormshield/snmp/mode/health.pm index 9364ed6d0d..0274498988 100644 --- a/src/network/stormshield/snmp/mode/health.pm +++ b/src/network/stormshield/snmp/mode/health.pm @@ -114,7 +114,7 @@ sub new { my $mapping = { hamode => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.3' }, # snsHaModeHealth - link => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.4' }, # snsHaLinkHealth + halink => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.4' }, # snsHaLinkHealth powersupply => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.5' }, # snsPowerSupplyHealth fan => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.6' }, # snsFanHealth cpu => { oid => '.1.3.6.1.4.1.11256.1.16.2.1.7' }, # snsCpuHealth @@ -219,4 +219,4 @@ You can use the following variables: %{health}, %{service} =back -=cut \ No newline at end of file +=cut From 5c7ea08a227f2f16eab783b9c9456cecab8cf5e2 Mon Sep 17 00:00:00 2001 From: crsuser Date: Thu, 16 Jul 2026 12:08:20 +0200 Subject: [PATCH 10/10] fix: right thresholds for PSU and Disk --- .../stormshield/snmp/mode/components/disk.pm | 27 ++++++++++++------- src/network/stormshield/snmp/mode/hardware.pm | 5 ++-- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/network/stormshield/snmp/mode/components/disk.pm b/src/network/stormshield/snmp/mode/components/disk.pm index 8ea6e8b7bd..f5f3d9c3d6 100644 --- a/src/network/stormshield/snmp/mode/components/disk.pm +++ b/src/network/stormshield/snmp/mode/components/disk.pm @@ -101,7 +101,6 @@ sub check { my $smart = $disk->{smartResult} // 'N/A'; my $name = $label_prefix . $disk->{name} // $instance; - $self->{output}->output_add( long_msg => sprintf( "disk '%s' smart result is '%s' [isRaid: %s, raidStatus: %s, position: %s]", @@ -122,18 +121,26 @@ sub check { } if (defined $disk->{isRaid} && $disk->{isRaid} == 1 - && defined $disk->{raidStatus} && $disk->{raidStatus} ne '' - && $disk->{raidStatus} ne 'optimal') + && defined $disk->{raidStatus} && $disk->{raidStatus} ne '') { - $self->{output}->output_add( - severity => 'WARNING', - short_msg => sprintf( - "Disk '%s' RAID status is '%s'", $name, $disk->{raidStatus} - ) - ); + if ($disk->{raidStatus} eq 'missing'){ + $self->{output}->output_add( + severity => 'WARNING', + short_msg => sprintf( + "Disk '%s' RAID status is '%s'", $name, $disk->{raidStatus} + ) + ); + } elsif ($disk->{raidStatus} eq 'degraded') { + $self->{output}->output_add( + severity => 'CRITICAL', + short_msg => sprintf( + "Disk '%s' RAID status is '%s'", $name, $disk->{raidStatus} + ) + ); + } } } } } -1; \ No newline at end of file +1; \ No newline at end of file diff --git a/src/network/stormshield/snmp/mode/hardware.pm b/src/network/stormshield/snmp/mode/hardware.pm index 176fe62f77..226da7db57 100644 --- a/src/network/stormshield/snmp/mode/hardware.pm +++ b/src/network/stormshield/snmp/mode/hardware.pm @@ -42,8 +42,9 @@ sub set_system { ['.*', 'CRITICAL'] ], psu => [ - ['OK', 'OK'], - ['.*', 'CRITICAL'] + ['FAILED', 'CRITICAL'], + ['NOTFOUND', 'WARNING'], + ['.*', 'OK'] ], disk => [ ['PASSED', 'OK'],