#
# Copyright 2022 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 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-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-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} = {};

    # 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 {
    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}} = ();
    }

    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});
    }

    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 {
    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();

    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});
        }
    }

    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});
    }

    # 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});
    }

    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) = @_;
    # $options{option_results} = ref to options result

    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_errors_exit} = $options{option_results}->{snmp_errors_exit};
    $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 ($options{option_results}->{host} =~ /^(dtls|tls|ssh).*:/) {
            $self->{snmp_params}->{OurIdentity} = $options{option_results}->{snmp_our_identity} if (defined($options{option_results}->{snmp_our_identity}));
            $self->{snmp_params}->{TheirIdentity} = $options{option_results}->{snmp_their_identity} if (defined($options{option_results}->{snmp_their_identity}));
            $self->{snmp_params}->{TheirHostname} = $options{option_results}->{snmp_their_hostname} if (defined($options{option_results}->{snmp_their_hostname}));
            $self->{snmp_params}->{TrustCert} = $options{option_results}->{snmp_trust_cert} if (defined($options{option_results}->{snmp_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 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>

Hostname to query (required).

=item B<--snmp-community>

Read community (defaults to public).

=item B<--snmp-version>

Version: 1 for SNMP v1 (default), 2 for SNMP v2c, 3 for SNMP v3.

=item B<--snmp-port>

Port (default: 161).

=item B<--snmp-timeout>

Timeout in secondes (default: 1) before retries.

=item B<--snmp-retries>

Set the number of retries (default: 5) before failure.

=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 whe you set it. Prefer to let the default value).

=item B<--snmp-autoreduce>
 
Auto reduce SNMP request size in case of SNMP errors (By default, the divisor is 2).

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

Use snmp getnext function (even in snmp v2c and v3).

=item B<--snmp-username>

Security name (only for SNMP v3).

=item B<--authpassphrase>

Authentication protocol pass phrase.

=item B<--authprotocol>

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

=item B<--privpassphrase>

Privacy protocol pass phrase

=item B<--privprotocol>

Privacy protocol: DES|AES. Since net-snmp 5.9.1: AES192|AES192C|AES256|AES256C.

=item B<--contextname>

Context name

=item B<--contextengineid>

Context engine ID

=item B<--securityengineid>

Security engine ID

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

Exit code for SNMP Errors (default: unknown)

=back

=head1 DESCRIPTION

B<snmp>.

=cut
