#
# Copyright 2024 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 centreon::plugins::snmp;

use strict;
use warnings;
use centreon::plugins::misc;
use SNMP;
use Socket;
use POSIX;

sub new {
    my ($class, %options) = @_;
    my $self  = {};
    bless $self, $class;
    # $options{options} = options object
    # $options{output} = output object
    # $options{exit_value} = integer

    if (!defined($options{output})) {
        print "Class SNMP: Need to specify 'output' argument.\n";
        exit 3;
    }
    if (!defined($options{options})) {
        $options{output}->add_option_msg(short_msg => "Class SNMP: Need to specify 'options' argument.");
        $options{output}->option_exit();
    }

    if (!defined($options{noptions})) {
        $options{options}->add_options(arguments => {
            'hostname|host:s'    => { name => 'host' },
            'snmp-community:s'   => { name => 'snmp_community', default => 'public' },
            'snmp-version:s'     => { name => 'snmp_version', default => 1 },
            'snmp-port:s'        => { name => 'snmp_port', default => 161 },
            'snmp-timeout:s'     => { name => 'snmp_timeout', default => 1 },
            'snmp-retries:s'     => { name => 'snmp_retries', default => 5 },
            'maxrepetitions:s'   => { name => 'maxrepetitions', default => 50 },
            'subsetleef:s'       => { name => 'subsetleef', default => 50 },
            'subsettable:s'      => { name => 'subsettable', default => 100 },
            'snmp-cache-file:s'  => { name => 'snmp_cache_file' },
            'snmp-autoreduce:s'  => { name => 'snmp_autoreduce' },
            'snmp-force-getnext' => { name => 'snmp_force_getnext' },
            'snmp-username:s'    => { name => 'snmp_security_name' },
            'authpassphrase:s'   => { name => 'snmp_auth_passphrase' },
            'authprotocol:s'     => { name => 'snmp_auth_protocol' },
            'privpassphrase:s'   => { name => 'snmp_priv_passphrase' },
            'privprotocol:s'     => { name => 'snmp_priv_protocol' },
            'contextname:s'      => { name => 'snmp_context_name' },
            'contextengineid:s'  => { name => 'snmp_context_engine_id' },
            'securityengineid:s' => { name => 'snmp_security_engine_id' },
            'snmp-tls-transport:s'      => { name => 'snmp_tls_transport' },
            'snmp-tls-our-identity:s'   => { name => 'snmp_tls_our_identity' },
            'snmp-tls-their-identity:s' => { name => 'snmp_tls_their_identity' },
            'snmp-tls-their-hostname:s' => { name => 'snmp_tls_their_hostname' },
            'snmp-tls-trust-cert:s    ' => { name => 'snmp_tls_trust_cert' },
            'snmp-errors-exit:s'        => { name => 'snmp_errors_exit', default => 'unknown' },
        });
        $options{options}->add_help(package => __PACKAGE__, sections => 'SNMP OPTIONS');
    }

    #####
    $self->{session} = undef;
    $self->{output} = $options{output};
    $self->{snmp_params} = {};

    $self->{use_snmp_cache} = 0;

    # Dont load MIB
    $SNMP::auto_init_mib = 0;
    $ENV{MIBS} = '';
    # For snmpv v1 - get request retries when you have "NoSuchName"
    $self->{RetryNoSuch} = 1;
    # Dont try to translate OID (we keep value)
    $self->{UseNumeric} = 1;

    $self->{error_msg} = undef;
    $self->{error_status} = 0;

    return $self;
}

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

    $self->{snmp_params}->{RetryNoSuch} = $self->{RetryNoSuch};
    $self->{snmp_params}->{UseNumeric} = $self->{UseNumeric};

    if (!$self->{output}->is_litteral_status(status => $self->{snmp_errors_exit})) {
        $self->{output}->add_option_msg(short_msg => "Unknown value '" . $self->{snmp_errors_exit}  . "' for --snmp-errors-exit.");
        $self->{output}->option_exit(exit_litteral => 'unknown');
    }

    $self->{session} = new SNMP::Session(%{$self->{snmp_params}});
    if (!defined($self->{session})) {
        if (defined($options{dont_quit}) && $options{dont_quit} == 1) {
            $self->set_error(error_status => -1, error_msg => 'SNMP Session: unable to create');
            return 1;
        }
        $self->{output}->add_option_msg(short_msg => 'SNMP Session: unable to create');
        $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
    }
    if ($self->{session}->{ErrorNum}) {
        if (defined($options{dont_quit}) && $options{dont_quit} == 1) {
            $self->set_error(error_status => -1, error_msg => 'SNMP Session: ' . $self->{session}->{ErrorStr});
            return 1;
        }
        $self->{output}->add_option_msg(short_msg => 'SNMP Session: ' . $self->{session}->{ErrorStr});
        $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
    }

    return 0;
}

sub load {
    my ($self, %options) = @_;
    # $options{oids} = ref to array of oids (example: ['.1.2', '.1.2'])
    # $options{instances} = ref to array of oids instances
    # $options{begin}, $args->{end} = integer instance end
    # $options{instance_regexp} = str
    # 3 way to use: with instances, with end, none
    
    if (defined($options{end})) {
        for (my $i = $options{begin}; $i <= $options{end}; $i++) {
            foreach (@{$options{oids}}) {
                push @{$self->{oids_loaded}}, $_ . "." . $i;
            }
        }
        return ;
    }
    
    if (defined($options{instances})) {
        $options{instance_regexp} = defined($options{instance_regexp}) ? $options{instance_regexp} : '(\d+)$';
        foreach my $instance (@{$options{instances}}) {
            $instance =~ /$options{instance_regexp}/;
            foreach (@{$options{oids}}) {
                push @{$self->{oids_loaded}}, $_ . "." . $1;
            }
        }
        return ;
    }
    
    push @{$self->{oids_loaded}}, @{$options{oids}};
}

sub autoreduce_table {
    my ($self, %options) = @_;
    
    return 1 if (defined($self->{snmp_force_getnext}) || $self->is_snmpv1());
    if ($self->{snmp_params}->{Retries} > 1) {
        $self->{snmp_params}->{Retries} = 1;
        $self->connect();
    }
    
    return 1 if (${$options{repeat_count}} == 1);
    ${$options{repeat_count}} = int(${$options{repeat_count}} / $self->{snmp_autoreduce_divisor});
    ${$options{repeat_count}} = 1 if (${$options{repeat_count}} < 1);
    return 0;
}

sub autoreduce_multiple_table {
    my ($self, %options) = @_;
    
    if ($self->{snmp_params}->{Retries} > 1) {
        $self->{snmp_params}->{Retries} = 1;
        $self->connect();
    }
    return 1 if (${$options{repeat_count}} == 1);
    
    ${$options{repeat_count}} = int(${$options{repeat_count}} / $self->{snmp_autoreduce_divisor});
    $self->{subsettable} = int($self->{subsettable} / $self->{snmp_autoreduce_divisor});
    ${$options{repeat_count}} = 1 if (${$options{repeat_count}} < 1);
    return 0;
}

sub autoreduce_leef {
    my ($self, %options) = @_;
    
    if ($self->{snmp_params}->{Retries} > 1) {
        $self->{snmp_params}->{Retries} = 1;
        $self->connect();
    }
    
    return 1 if ($self->{subsetleef} == 1);
    $self->{subsetleef} = int($self->{subsetleef} / $self->{snmp_autoreduce_divisor});
    $self->{subsetleef} = 1 if ($self->{subsetleef} < 1);
    
    my $array_ref = [];
    my $subset_current = 0;
    my $subset_construct = [];
    foreach ([@{$options{current}}], @{$self->{array_ref_ar}}) {
        foreach my $entry (@$_) {;
            push @$subset_construct, [$entry->[0], $entry->[1]];
            $subset_current++;
            if ($subset_current == $self->{subsetleef}) {
                push @$array_ref, \@$subset_construct;
                $subset_construct = [];
                $subset_current = 0;
            }
        }
    }
    
    if ($subset_current) {
        push @$array_ref, \@$subset_construct;
    }

    $self->{array_ref_ar} = \@$array_ref;
    return 0;
}

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

    my $results = {};
    foreach my $oid (@{$options{oids}}) {
        if (defined($self->{snmp_cache}->{$oid})) {
            $results->{$oid} = $self->{snmp_cache}->{$oid};
        }
    }

    if ($options{nothing_quit} == 1 && scalar(keys %$results) <= 0) {
        $self->{output}->add_option_msg(short_msg => 'SNMP GET Request: Cant get a single value.');
        $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
    }

    return $results;
}

sub get_leef {
    my ($self, %options) = @_;
    # $options{dont_quit} = integer
    # $options{nothing_quit} = integer
    # $options{oids} = ref to array of oids (example: ['.1.2', '.1.2'])

    # Returns array
    #    'undef' value for an OID means NoSuchValue

    my ($dont_quit) = (defined($options{dont_quit}) && $options{dont_quit} == 1) ? 1 : 0;
    my ($nothing_quit) = (defined($options{nothing_quit}) && $options{nothing_quit} == 1) ? 1 : 0;
    $self->set_error();

    if (!defined($options{oids})) {
        if ($#{$self->{oids_loaded}} < 0) {
            if ($dont_quit == 1) {
                $self->set_error(error_status => -1, error_msg => "Need to specify OIDs");
                return undef;
            }
            $self->{output}->add_option_msg(short_msg => 'Need to specify OIDs');
            $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
        }
        push @{$options{oids}}, @{$self->{oids_loaded}};
        @{$self->{oids_loaded}} = ();
    }

    if ($self->{use_snmp_cache} == 1) {
        return $self->get_leef_cache(oids => $options{oids}, nothing_quit => $nothing_quit);
    }

    my $results = {};
    $self->{array_ref_ar} = [];
    my $subset_current = 0;
    my $subset_construct = [];
    foreach my $oid (@{$options{oids}}) {
        # Get last value
        next if ($oid !~ /(.*)\.(\d+)([\.\s]*)$/);
        
        my ($oid, $instance) = ($1, $2);
        $results->{$oid . "." . $instance} = undef;
        push @$subset_construct, [$oid, $instance];
        $subset_current++;
        if ($subset_current == $self->{subsetleef}) {
            push @{$self->{array_ref_ar}}, \@$subset_construct;
            $subset_construct = [];
            $subset_current = 0;
        }
    }
    if ($subset_current) {
        push @{$self->{array_ref_ar}}, \@$subset_construct;
    }

    ############################
    # If wrong oid with SNMP v1, packet resent (2 packets more). Not the case with SNMP > 1.
    # Can have "NoSuchName", if nothing works...
    # = v1: wrong oid
    #   bless( [
    #       '.1.3.6.1.2.1.1.3',
    #       '0',
    #       '199720062',
    #       'TICKS'
    #       ], 'SNMP::Varbind' ),
    #   bless( [
    #       '.1.3.6.1.2.1.1.999',
    #       '0'
    #       ], 'SNMP::Varbind' ),
    #   bless( [
    #       '.1.3.6.1.2.1.1',
    #       '1000'
    #       ], 'SNMP::Varbind' )
    # > v1: wrong oid
    #   bless( [
    #        '.1.3.6.1.2.1.1.3',
    #        '0',
    #        '199728713',
    #        'TICKS'
    #       ], 'SNMP::Varbind' ),
    #   bless( [
    #         '.1.3.6.1.2.1.1',
    #         '3',
    #         'NOSUCHINSTANCE',
    #        'TICKS'
    #    ], 'SNMP::Varbind' )
    #   bless( [
    #        '.1.3.6.1.2.1.1.999',
    #        '0',
    #        'NOSUCHOBJECT',
    #        'NOSUCHOBJECT'
    #       ], 'SNMP::Varbind' ),
    #   bless( [
    #        '.1.3.6.1.2.1.1',
    #        '1000',
    #        'NOSUCHOBJECT',
    #        'NOSUCHOBJECT'
    #       ], 'SNMP::Varbind' )
    ############################

    my $total = 0;
    while (my $entry = shift(@{$self->{array_ref_ar}})) {
        my $vb = new SNMP::VarList(@{$entry});
        $self->{session}->get($vb);

        if ($self->{session}->{ErrorNum}) {
            # 0    noError       Pas d'erreurs.
            # 1    tooBig        Reponse de taille trop grande.
            # 2    noSuchName    Variable inexistante.
            # -24  Timeout
            if ($self->{session}->{ErrorNum} == 2) {
                # We are at the end with snmpv1. We next.
                next;
            }

            if ($self->{snmp_autoreduce} == 1 && 
                ($self->{session}->{ErrorNum} == 1 || $self->{session}->{ErrorNum} == 5 || $self->{session}->{ErrorNum} == -24)) {
                next if ($self->autoreduce_leef(current => $entry) == 0);
            }
            my $msg = 'SNMP GET Request: ' . $self->{session}->{ErrorStr};    
            if ($dont_quit == 0) {
                $self->{output}->add_option_msg(short_msg => $msg);
                $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
            }

            $self->set_error(error_status => -1, error_msg => $msg);
            return undef;
        }

        # Some equipments gives a partial response and no error.
        # We look the last value if it's empty or not
        # In snmpv1 we have the retryNoSuch
        if (((scalar(@$vb) != scalar(@{$entry})) || (scalar(@{@$vb[-1]}) < 3)) && !$self->is_snmpv1()) {
            next if ($self->{snmp_autoreduce} == 1 && $self->autoreduce_leef(current => $entry) == 0);
            if ($dont_quit == 0) {
                $self->{output}->add_option_msg(short_msg => 'SNMP partial response. Please try --snmp-autoreduce option');
                $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
            }

            $self->set_error(error_status => -1, error_msg => 'SNMP partial response');
            return undef;
        }

        foreach my $entry (@$vb) {
            if ($#$entry < 3) {
                # Can be snmpv1 not find
                next;
            }
            if (${$entry}[2] eq 'NOSUCHOBJECT' || ${$entry}[2] eq 'NOSUCHINSTANCE') {
                # Error in snmp > 1
                next;
            }

            $total++;
            $results->{${$entry}[0] . "." . ${$entry}[1]} = ${$entry}[2];
        }
    }

    if ($nothing_quit == 1 && $total == 0) {
        $self->{output}->add_option_msg(short_msg => 'SNMP GET Request: Cant get a single value.');
        $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
    }

    $self->debug(results => $results) if ($self->{output}->is_debug());

    return $results;
}

sub multiple_find_bigger {
    my ($self, %options) = @_;
    
    my $getting = {};
    my @values = ();
    foreach my $key (keys %{$options{working_oids}}) {
        push @values, $options{working_oids}->{$key}->{start};
        $getting->{ $options{working_oids}->{$key}->{start} } = $key;
    }
    @values = $self->oid_lex_sort(@values);
    
    return $getting->{pop(@values)};
}

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

    my $results = {};
    foreach my $entry (@{$options{oids}}) {
        my $result = $self->get_table_cache(
            oid => $entry->{oid},
            start => $entry->{start},
            end => $entry->{end},
            nothing_quit => 0
        );
        if ($options{return_type} == 0) {
            $results->{ $entry->{oid} } = $result;
        } else {
            $results = { %$results, %$result };
        }
    }

    my $total = 0;
    if ($options{nothing_quit} == 1) {
        if ($options{return_type} == 1) {
            $total = scalar(keys %$results);
        } else {
            foreach (keys %$results) {
                $total += scalar(keys %{$results->{$_}});
            }
        }

        if ($total == 0) {
            $self->{output}->add_option_msg(short_msg => 'SNMP Table Request: Cant get a single value.');
            $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
        }
    }

    return $results;
}

sub get_multiple_table {
    my ($self, %options) = @_;
    # $options{dont_quit} = integer
    # $options{oids} = refs array
    #     [ { oid => 'x.x.x.x', start => '', end => ''}, { oid => 'y.y.y.y', start => '', end => ''} ]
    # $options{return_type} = integer

    my ($return_type) = (defined($options{return_type}) && $options{return_type} == 1) ? 1 : 0;
    my ($dont_quit) = (defined($options{dont_quit}) && $options{dont_quit} == 1) ? 1 : 0;
    my ($nothing_quit) = (defined($options{nothing_quit}) && $options{nothing_quit} == 1) ? 1 : 0;
    $self->set_error();

    if ($self->{use_snmp_cache} == 1) {
        return $self->get_multiple_table_cache(
            oids => $options{oids},
            return_type => $return_type,
            nothing_quit => $nothing_quit
        );
    }

    my $working_oids = {};
    my $results = {};
    # Check overlap
    foreach my $entry (@{$options{oids}}) {
        # Transform asking
        if ($entry->{oid} !~ /(.*)\.(\d+)([\.\s]*)$/) {
            if ($dont_quit == 1) {
                $self->set_error(error_status => -1, error_msg => "Method 'get_multiple_table': Wrong OID '" . $entry->{oid} . "'.");
                return undef;
            }
            $self->{output}->add_option_msg(short_msg => "Method 'get_multiple_table': Wrong OID '" . $entry->{oid} . "'.");
            $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
        }

        if (defined($entry->{start})) {
            $working_oids->{$entry->{oid}} = { start => $entry->{start}, end => $entry->{end} }; # last in it
        } else {
            $working_oids->{$entry->{oid}} = { start => $entry->{oid}, end => $entry->{end} };
        }

        if ($return_type == 0) {
            $results->{ $entry->{oid} } = {};
        }
    }

    # we use a medium (UDP have a PDU limit. SNMP protcol cant send multiples for one request)
    # So we need to manage
    # It's for "bulk". We ask 50 next values. If you set 1, it's like a getnext (snmp v1)
    my $repeat_count = 50;
    if (defined($self->{maxrepetitions}) && 
        $self->{maxrepetitions} =~ /^\d+$/) {
        $repeat_count = $self->{maxrepetitions};
    }

    # Quit if base not the same or 'ENDOFMIBVIEW' value. Need all oid finish otherwise we continue :)
    while (1) {
        my $current_oids = 0;
        my @bindings = ();
        my @bases = ();
        foreach my $key (keys %{$working_oids}) {
            $working_oids->{$key}->{start} =~ /(.*)\.(\d+)([\.\s]*)$/;
            push @bindings, [$1, $2];
            push @bases, $key;

            $current_oids++;
            last if ($current_oids > $self->{subsettable});
        }

        # Nothing more to check. We quit
        last if ($current_oids == 0);

        my $vb = new SNMP::VarList(@bindings);

        if ($self->is_snmpv1() || defined($self->{snmp_force_getnext})) {
            $self->{session}->getnext($vb);
        } else {
            my $current_repeat_count = floor($repeat_count / $current_oids);
            $current_repeat_count = 1 if ($current_repeat_count == 0);
            $self->{session}->getbulk(0, $current_repeat_count, $vb);
        }

        # Error
        if ($self->{session}->{ErrorNum}) {
            # 0    noError       Pas d'erreurs.
            # 1    tooBig        Reponse de taille trop grande.
            # 2    noSuchName    Variable inexistante.
            if ($self->{session}->{ErrorNum} == 2) {
                # We are at the end with snmpv1. Need to find the most up oid ;)
                my $oid_base = $self->multiple_find_bigger(working_oids => $working_oids);
                delete $working_oids->{$oid_base};
                next;
            }

            if ($self->{snmp_autoreduce} == 1 && 
                ($self->{session}->{ErrorNum} == 1 || $self->{session}->{ErrorNum} == 5 || $self->{session}->{ErrorNum} == -24)) {
                next if ($self->autoreduce_multiple_table(repeat_count => \$repeat_count) == 0);
            }

            my $msg = 'SNMP Table Request: ' . $self->{session}->{ErrorStr};
            if ($dont_quit == 0) {
                $self->{output}->add_option_msg(short_msg => $msg);
                $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
            }

            $self->set_error(error_status => -1, error_msg => $msg);
            return undef;
        }

        # Manage
        # step by step: [ 1 => 1, 2 => 1, 3 => 1 ], [ 1 => 2, 2 => 2, 3 => 2 ],...

        my $pos = -1;
        foreach my $entry (@$vb) {
            $pos++;

            # Already destruct. we continue
            next if (!defined($working_oids->{ $bases[$pos % $current_oids] }));

            # ENDOFMIBVIEW is on each iteration. So we need to delete and skip after that
            if (${$entry}[2] eq 'ENDOFMIBVIEW') {
                delete $working_oids->{ $bases[$pos % $current_oids] };
                # END mib
                next;
            }

            # Not in same table
            my $complete_oid = ${$entry}[0] . "." . ${$entry}[1];
            my $base = $bases[$pos % $current_oids];
            if ($complete_oid !~ /^$base\./ ||
                (defined($working_oids->{ $bases[$pos % $current_oids] }->{end}) && 
                 $self->check_oid_up(current => $complete_oid, end => $working_oids->{ $bases[$pos % $current_oids] }->{end}))) {
                delete $working_oids->{ $bases[$pos % $current_oids] };
                next;
            }

            if ($return_type == 0) {
                $results->{$bases[$pos % $current_oids]}->{$complete_oid} = ${$entry}[2];
            } else {
                $results->{$complete_oid} = ${$entry}[2];
            }

            $working_oids->{ $bases[$pos % $current_oids] }->{start} = $complete_oid;
        }

        # infinite loop. Some equipments it returns nothing!!??
        if ($pos == -1) {
            $self->{output}->add_option_msg(short_msg => 'SNMP Table Request: problem to get values (try --snmp-force-getnext option)');
            $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
        }
    }

    my $total = 0;
    if ($nothing_quit == 1) {
        if ($return_type == 1) {
            $total = scalar(keys %{$results});
        } else {
            foreach (keys %{$results}) {
                $total += scalar(keys %{$results->{$_}});
            }
        }

        if ($total == 0) {
            $self->{output}->add_option_msg(short_msg => 'SNMP Table Request: Cant get a single value.');
            $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
        }
    }

    $self->debug(results => $results) if ($self->{output}->is_debug());

    return $results;
}

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

    my $branch = defined($options{start}) ? $options{start} : $options{oid};

    my $results = {};
    foreach my $oid ($self->oid_lex_sort(keys %{$self->{snmp_cache}})) {
        if ($oid =~ /^$branch\./) {
            $results->{$oid} = $self->{snmp_cache}->{$oid};
            if (defined($options{end}) && $self->check_oid_up(current => $oid, end => $options{end})) {
                last;
            } 
        }
    }

    if ($options{nothing_quit} == 1 && scalar(keys %$results) <= 0) {
        $self->{output}->add_option_msg(short_msg => 'SNMP Table Request: Cant get a single value.');
        $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
    }

    return $results;
}

sub get_table {
    my ($self, %options) = @_;
    # $options{dont_quit} = integer
    # $options{oid} = string (example: '.1.2')
    # $options{start} = string (example: '.1.2')
    # $options{end} = string (example: '.1.2')

    my ($dont_quit) = (defined($options{dont_quit}) && $options{dont_quit} == 1) ? 1 : 0;
    my ($nothing_quit) = (defined($options{nothing_quit}) && $options{nothing_quit} == 1) ? 1 : 0;
    $self->set_error();

    if (defined($options{start})) {
        $options{start} = $self->clean_oid($options{start});
    }
    if (defined($options{end})) {
        $options{end} = $self->clean_oid($options{end});
    }

    if ($self->{use_snmp_cache} == 1) {
        return $self->get_table_cache(
            oid => $options{oid},
            start => $options{start},
            end => $options{end},
            nothing_quit => $nothing_quit
        );
    }

    # we use a medium (UDP have a PDU limit. SNMP protcol cant send multiples for one request)
    # So we need to manage
    # It's for "bulk". We ask 50 next values. If you set 1, it's like a getnext (snmp v1)
    my $repeat_count = 50;
    if (defined($self->{maxrepetitions}) && 
        $self->{maxrepetitions} =~ /^\d+$/) {
        $repeat_count = $self->{maxrepetitions};
    }

    # Transform asking
    if ($options{oid} !~ /(.*)\.(\d+)([\.\s]*)$/) {
        if ($dont_quit == 1) {
            $self->set_error(error_status => -1, error_msg => "Method 'get_table': Wrong OID '" . $options{oid} . "'.");
            return undef;
        }
        $self->{output}->add_option_msg(short_msg => "Method 'get_table': Wrong OID '" . $options{oid} . "'.");
        $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
    }

    my $main_indice = $1 . '.' . $2;
    my $results = {};

    # Quit if base not the same or 'ENDOFMIBVIEW' value
    my $leave = 1;
    my $last_oid;

    if (defined($options{start})) {
        $last_oid = $options{start};
    } else {
        $last_oid = $options{oid};
    }
    while ($leave) {
        $last_oid =~ /(.*)\.(\d+)([\.\s]*)$/;
        my $vb = new SNMP::VarList([$1, $2]);

        if ($self->is_snmpv1() || defined($self->{snmp_force_getnext})) {
            $self->{session}->getnext($vb);
        } else {
            $self->{session}->getbulk(0, $repeat_count, $vb);
        }

        # Error
        if ($self->{session}->{ErrorNum}) {
            # 0    noError       Pas d'erreurs.
            # 1    tooBig        Reponse de taille trop grande.
            # 2    noSuchName    Variable inexistante.
            # -24  Timeout
            if ($self->{session}->{ErrorNum} == 2) {
                # We are at the end with snmpv1. We quit.
                last;
            }
            if ($self->{snmp_autoreduce} == 1 && 
                ($self->{session}->{ErrorNum} == 1 || $self->{session}->{ErrorNum} == 5 || $self->{session}->{ErrorNum} == -24)) {
                next if ($self->autoreduce_table(repeat_count => \$repeat_count) == 0);
            }

            my $msg = 'SNMP Table Request: ' . $self->{session}->{ErrorStr};

            if ($dont_quit == 0) {
                $self->{output}->add_option_msg(short_msg => $msg);
                $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
            }

            $self->set_error(error_status => -1, error_msg => $msg);
            return undef;
        }

        # Manage
        foreach my $entry (@$vb) {
            if (${$entry}[2] eq 'ENDOFMIBVIEW') {
                # END mib
                $leave = 0;
                last;
            }

            # Not in same table
            my $complete_oid = ${$entry}[0] . "." . ${$entry}[1];
            if ($complete_oid !~ /^$main_indice\./ ||
                (defined($options{end}) && $self->check_oid_up(current => $complete_oid, end => $options{end}))) {
                $leave = 0;
                last;
            }

            $results->{$complete_oid} = ${$entry}[2];
            $last_oid = $complete_oid;
        }
    }

    if ($nothing_quit == 1 && scalar(keys %$results) == 0) {
        $self->{output}->add_option_msg(short_msg => 'SNMP Table Request: Cant get a single value.');
        $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
    }

    $self->debug(results => $results) if ($self->{output}->is_debug());

    return $results;
}

sub set {
    my ($self, %options) = @_;
    # $options{dont_quit} = integer
    # $options{oids} = ref to hash table
    my ($dont_quit) = (defined($options{dont_quit}) && $options{dont_quit} == 1) ? 1 : 0;
    $self->set_error();

    my $vars = [];
    foreach my $oid (keys %{$options{oids}}) {
        # Get last value
        next if ($oid !~ /(.*)\.(\d+)([\.\s]*)$/);

        my $value = $options{oids}->{$oid}->{value};
        my $type = $options{oids}->{$oid}->{type};
        my ($oid, $instance) = ($1, $2);

        push @$vars, [$oid, $instance, $value, $type];
    }

    $self->{session}->set($vars);
    if ($self->{session}->{ErrorNum}) {
        # 0    noError       Pas d'erreurs.
        # 1    tooBig        Reponse de taille trop grande.
        # 2    noSuchName    Variable inexistante.

        my $msg = 'SNMP SET Request: ' . $self->{session}->{ErrorStr};
        if ($dont_quit == 0) {
            $self->{output}->add_option_msg(short_msg => $msg);
            $self->{output}->option_exit(exit_litteral => $self->{snmp_errors_exit});
        }

        $self->set_error(error_status => -1, error_msg => $msg);
        return undef;
    }

    return 0;
}

sub is_snmpv1 {
    my ($self) = @_;

    if ($self->{snmp_params}->{Version} eq '1') {
        return 1;
    }
    return 0;
}

sub clean_oid {
    my ($self, $oid) = @_;

    $oid =~ s/\.$//;
    $oid =~ s/^(\d)/\.$1/;
    return $oid;
}

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

    my $current_oid = $options{current};
    my $end_oid = $options{end};

    my @current_oid_splitted = split /\./, $current_oid;
    my @end_oid_splitted = split /\./, $end_oid;
    # Skip first value (before first '.' empty)
    for (my $i = 1; $i <= $#current_oid_splitted && $i <= $#end_oid_splitted; $i++) {
        if (int($current_oid_splitted[$i]) > int($end_oid_splitted[$i])) {
            return 1;
        }
    }

    return 0;
}

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

    $self->{snmp_errors_exit} = $options{option_results}->{snmp_errors_exit};

    if (defined($options{option_results}->{snmp_cache_file}) && $options{option_results}->{snmp_cache_file} ne '') {
        centreon::plugins::misc::mymodule_load(
            output => $self->{output},
            module => 'JSON::XS',
            error_msg => "Cannot load module 'JSON::XS'."
        );
        my $content = centreon::plugins::misc::slurp_file(output => $self->{output}, file => $options{option_results}->{snmp_cache_file});
        eval {
            $self->{snmp_cache} = JSON::XS->new->decode($content);
        };
        if ($@) {
            $self->{output}->add_option_msg(short_msg => "Cannot decode json cache file: $@");
            $self->{output}->option_exit();
        }

        $self->{use_snmp_cache} = 1;
        return ;
    }

    if (!defined($options{option_results}->{host})) {
        $self->{output}->add_option_msg(short_msg => 'Missing parameter --hostname.');
        $self->{output}->option_exit();
    }

    $options{option_results}->{snmp_version} =~ s/^v//;
    if ($options{option_results}->{snmp_version} !~ /1|2c|2|3/) {
        $self->{output}->add_option_msg(short_msg => 'Unknown snmp version.');
        $self->{output}->option_exit();
    }

    $self->{snmp_force_getnext} = $options{option_results}->{snmp_force_getnext};
    $self->{maxrepetitions} = $options{option_results}->{maxrepetitions};
    $self->{subsetleef} = (defined($options{option_results}->{subsetleef}) && $options{option_results}->{subsetleef} =~ /^[0-9]+$/) ? $options{option_results}->{subsetleef} : 50;
    $self->{subsettable} = (defined($options{option_results}->{subsettable}) && $options{option_results}->{subsettable} =~ /^[0-9]+$/) ? $options{option_results}->{subsettable} : 100;
    $self->{snmp_autoreduce} = 0;
    $self->{snmp_autoreduce_divisor} = 2;
    if (defined($options{option_results}->{snmp_autoreduce})) {
        $self->{snmp_autoreduce} = 1;
        $self->{snmp_autoreduce_divisor} = $1 if ($options{option_results}->{snmp_autoreduce} =~ /(\d+(\.\d+)?)/ && $1 > 1);
    }

    %{$self->{snmp_params}} = (
        DestHost => $options{option_results}->{host},
        Community => $options{option_results}->{snmp_community},
        Version => $options{option_results}->{snmp_version},
        RemotePort => $options{option_results}->{snmp_port},
        Retries => 5
    );

    if (defined($options{option_results}->{snmp_timeout}) && $options{option_results}->{snmp_timeout} =~ /^[0-9]+$/) {
        $self->{snmp_params}->{Timeout} = $options{option_results}->{snmp_timeout} * (10**6);
    }

    if (defined($options{option_results}->{snmp_retries}) && $options{option_results}->{snmp_retries} =~ /^[0-9]+$/) {
        $self->{snmp_params}->{Retries} = $options{option_results}->{snmp_retries};
    }

    if ($options{option_results}->{snmp_version} eq '3') {
        delete $self->{snmp_params}->{Community};

        $self->{snmp_params}->{Context} = $options{option_results}->{snmp_context_name} if (defined($options{option_results}->{snmp_context_name}));
        $self->{snmp_params}->{ContextEngineId} = $options{option_results}->{snmp_context_engine_id} if (defined($options{option_results}->{snmp_context_engine_id}));
        $self->{snmp_params}->{SecEngineId} = $options{option_results}->{snmp_security_engine_id} if (defined($options{option_results}->{snmp_security_engine_id}));
        $self->{snmp_params}->{SecName} = $options{option_results}->{snmp_security_name} if (defined($options{option_results}->{snmp_security_name}));

        # Certificate SNMPv3. Need net-snmp > 5.6
        if (defined($options{option_results}->{snmp_tls_transport}) && $options{option_results}->{snmp_tls_transport} =~ /^dtlsudp|tlstcp$/) {
            $self->{snmp_params}->{DestHost} = $options{option_results}->{snmp_tls_transport} . ':' . $options{option_results}->{host};
            $self->{snmp_params}->{OurIdentity} = $options{option_results}->{snmp_tls_our_identity} if (defined($options{option_results}->{snmp_tls_our_identity}));
            $self->{snmp_params}->{TheirIdentity} = $options{option_results}->{snmp_tls_their_identity} if (defined($options{option_results}->{snmp_tls_their_identity}));
            $self->{snmp_params}->{TheirHostname} = $options{option_results}->{snmp_tls_their_hostname} if (defined($options{option_results}->{snmp_tls_their_hostname}));
            $self->{snmp_params}->{TrustCert} = $options{option_results}->{snmp_tls_trust_cert} if (defined($options{option_results}->{snmp_tls_trust_cert}));
            $self->{snmp_params}->{SecLevel} = 'authPriv';
            return ;
        }

        if (!defined($options{option_results}->{snmp_security_name}) || $options{option_results}->{snmp_security_name} eq '') {
            $self->{output}->add_option_msg(short_msg => 'Missing parameter Security Name.');
            $self->{output}->option_exit();
        }

        # unauthenticated and unencrypted
        $self->{snmp_params}->{SecLevel} = 'noAuthNoPriv';

        my $user_activate = 0;
        if (defined($options{option_results}->{snmp_auth_passphrase}) && $options{option_results}->{snmp_auth_passphrase} ne '') {
            if (!defined($options{option_results}->{snmp_auth_protocol})) {
                $self->{output}->add_option_msg(short_msg => 'Missing parameter authenticate protocol.');
                $self->{output}->option_exit();
            }
            $options{option_results}->{snmp_auth_protocol} = uc($options{option_results}->{snmp_auth_protocol});
            if ($options{option_results}->{snmp_auth_protocol} !~ /^(?:MD5|SHA|SHA224|SHA256|SHA384|SHA512)$/) {
                $self->{output}->add_option_msg(short_msg => 'Wrong authentication protocol.');
                $self->{output}->option_exit();
            }

            $self->{snmp_params}->{SecLevel} = 'authNoPriv';
            $self->{snmp_params}->{AuthProto} = $options{option_results}->{snmp_auth_protocol};
            $self->{snmp_params}->{AuthPass} = $options{option_results}->{snmp_auth_passphrase};
            $user_activate = 1;
        }

        if (defined($options{option_results}->{snmp_priv_passphrase}) && $options{option_results}->{snmp_priv_passphrase} ne '') {
            if (!defined($options{option_results}->{snmp_priv_protocol})) {
                $self->{output}->add_option_msg(short_msg => 'Missing parameter privacy protocol.');
                $self->{output}->option_exit();
            }

            $options{option_results}->{snmp_priv_protocol} = uc($options{option_results}->{snmp_priv_protocol});
            if ($options{option_results}->{snmp_priv_protocol} !~ /^(?:DES|AES|AES192|AES192C|AES256|AES256C)$/) {
                $self->{output}->add_option_msg(short_msg => 'Wrong privacy protocol.');
                $self->{output}->option_exit();
            }
            if ($user_activate == 0) {
                $self->{output}->add_option_msg(short_msg => 'Cannot use snmp v3 privacy option without snmp v3 authentification options.');
                $self->{output}->option_exit();
            }
            $self->{snmp_params}->{SecLevel} = 'authPriv';
            $self->{snmp_params}->{PrivPass} = $options{option_results}->{snmp_priv_passphrase};
            $self->{snmp_params}->{PrivProto} = $options{option_results}->{snmp_priv_protocol};
        }
    }
}

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

    foreach (keys %options) {
        $self->{snmp_params}->{$_} = $options{$_};
    }
}

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

    foreach (keys %options) {
        $self->{$_} = $options{$_};
    }
}

sub set_error {
    my ($self, %options) = @_;
    # $options{error_msg} = string error
    # $options{error_status} = integer status

    $self->{error_status} = defined($options{error_status}) ? $options{error_status} : 0;
    $self->{error_msg} = defined($options{error_msg}) ? $options{error_msg} : undef;
}

sub error_status {
    my ($self) = @_;

    return $self->{error_status};
}

sub error {
    my ($self) = @_;

    return $self->{error_msg};
}

sub get_hostname {
    my ($self) = @_;

    my $host = $self->{snmp_params}->{DestHost};
    $host =~ s/.*://;
    return $host;
}

sub get_port {
    my ($self) = @_;

    return $self->{snmp_params}->{RemotePort};
}

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

    my $results = {};
    my $instance = '';
    $instance = '.' . $options{instance} if (defined($options{instance}));
    foreach my $name (keys %{$options{mapping}}) {
        my $entry = $options{mapping}->{$name}->{oid} . $instance;
        if (defined($options{results}->{$entry})) {
            $results->{$name} = $options{results}->{$entry};
        } elsif (defined($options{results}->{$options{mapping}->{$name}->{oid}}->{$entry})) {
            $results->{$name} = $options{results}->{$options{mapping}->{$name}->{oid}}->{$entry};
        } else {
            $results->{$name} = defined($options{default}) ? $options{default} : undef;
        }

        if (defined($options{mapping}->{$name}->{map})) {
            if (defined($results->{$name})) {
                $results->{$name} = defined($options{mapping}->{$name}->{map}->{$results->{$name}}) ? $options{mapping}->{$name}->{map}->{$results->{$name}} : (defined($options{default}) ? $options{default} : 'unknown');
            }
        }
    }

    return $results;
}

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

    foreach my $oid1 ($self->oid_lex_sort(keys %{$options{results}})) {
        if (ref($options{results}->{$oid1}) eq 'HASH') {
            foreach my $oid2 ($self->oid_lex_sort(keys %{$options{results}->{$oid1}})) {
                $self->{output}->output_add(long_msg => $oid2 . ' = ' . (defined($options{results}->{$oid1}->{$oid2}) ? $options{results}->{$oid1}->{$oid2} : 'undef'), debug => 1);
            }
        } else {
            $self->{output}->output_add(long_msg => $oid1 . ' = ' . (defined($options{results}->{$oid1}) ? $options{results}->{$oid1} : 'undef'), debug => 1);
        }
    }
}

sub oid_lex_sort {
    my $self = shift;

    if (@_ <= 1) {
        return @_;
    }

    return map { $_->[0] }
        sort { $a->[1] cmp $b->[1] }
        map {
           my $oid = $_;
           $oid =~ s/^\.//;
           $oid =~ s/ /\.0/g;
           [$_, pack 'N*', split m/\./, $oid]
        } @_;
}

1;

__END__

=head1 NAME

SNMP global

=head1 SYNOPSIS

snmp class

=head1 SNMP OPTIONS

=over 8

=item B<--hostname>

Name or address of the host to monitor (mandatory).

=item B<--snmp-community>

SNMP community (default value: public). It is recommended to use a read-only
community.

=item B<--snmp-version>

Version of the SNMP protocol. 1 for SNMP v1 (default), 2 for SNMP v2c, 3 for SNMP v3.

=item B<--snmp-port>

UDP port to send the SNMP request to (default: 161).

=item B<--snmp-timeout>

Time to wait before sending the request again if no reply has been received,
in seconds (default: 1). See also --snmp-retries.

=item B<--snmp-retries>

Maximum number of retries (default: 5).

=item B<--maxrepetitions>

Max repetitions value (default: 50) (only for SNMP v2 and v3).

=item B<--subsetleef>

How many OID values per SNMP request (default: 50) (for get_leef method. Be cautious when you set it. Prefer to let the default value).

=item B<--snmp-autoreduce>
 
Progressively reduce the number of requested OIDs in bulk mode. Use it in case of
SNMP errors (by default, the number is divided by 2).

=item B<--snmp-force-getnext>

Use SNMP getnext function in SNMP v2c and v3. This will request one OID at a
time.

=item B<--snmp-cache-file>

Use SNMP cache file.

=item B<--snmp-username>

SNMP v3 only:
User name (securityName). 

=item B<--authpassphrase>

SNMP v3 only:
Pass phrase hashed using the authentication protocol defined in the 
--authprotocol option.

=item B<--authprotocol>

SNMP v3 only:
Authentication protocol: MD5|SHA. Since net-snmp 5.9.1: SHA224|SHA256|SHA384|SHA512.

=item B<--privpassphrase>

SNMP v3 only:
Privacy pass phrase (privPassword) to encrypt messages using the protocol
defined in the --privprotocol option.

=item B<--privprotocol>

SNMP v3 only:
Privacy protocol (privProtocol) used to encrypt messages.
Supported protocols are: DES|AES and since net-snmp 5.9.1: AES192|AES192C|AES256|AES256C.

=item B<--contextname>

SNMP v3 only:
Context name (contextName), if relevant for the monitored host.

=item B<--contextengineid>

SNMP v3 only:
Context engine ID (contextEngineID), if relevant for the monitored host, given 
as a hexadecimal string.

=item B<--securityengineid>

SNMP v3 only:
Security engine ID, given as a hexadecimal string.

=item B<--snmp-errors-exit>

Expected status in case of SNMP error or timeout.
Possible values are warning, critical and unknown (default).

=item B<--snmp-tls-transport>

Transport protocol for TLS communication (can be: 'dtlsudp', 'tlstcp').

=item B<--snmp-tls-our-identity>

X.509 certificate to identify ourselves. Can be the path to the certificate file
or its contents.

=item B<--snmp-tls-their-identity>

X.509 certificate to identify the remote host. Can be the path to the 
certificate file or its contents. This option is unnecessary if the certificate
is already trusted by your system.

=item B<--snmp-tls-their-hostname>

Common Name (CN) expected in the certificate sent by the host if it differs from
the value of the --hostname parameter.

=item B<--snmp-tls-trust-cert>

A trusted CA certificate used to verify a remote host's certificate. 
If you use this option, you must also  define --snmp-tls-their-hostname.

=back

=head1 DESCRIPTION

B<snmp>.

=cut
