From 689713245d1c5cba1560fa9b44b4e82bce417e73 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 14:37:38 +0100 Subject: [PATCH 1/5] Add type annotations to Snmp.__init__ Just a slight cleanup before starting on SNMPv3 support --- python/nav/Snmp/pynetsnmp.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/nav/Snmp/pynetsnmp.py b/python/nav/Snmp/pynetsnmp.py index ccb247ed51..65c3481ffc 100644 --- a/python/nav/Snmp/pynetsnmp.py +++ b/python/nav/Snmp/pynetsnmp.py @@ -31,6 +31,7 @@ c_ulong, c_uint64, ) +from typing import Union from IPy import IP from pynetsnmp import netsnmp @@ -85,7 +86,13 @@ class Snmp(object): """ def __init__( - self, host, community="public", version="1", port=161, retries=3, timeout=1 + self, + host: str, + community: str = "public", + version: Union[str, int] = "1", + port: Union[str, int] = 161, + retries: int = 3, + timeout: int = 1, ): """Makes a new Snmp-object. From ebe881377085134900b3e0fced17c634db74831f Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 16:12:24 +0100 Subject: [PATCH 2/5] Define SNMPv3 protocol config enums These could be re-used elsewhere, like in the `SNMPv3Form`. --- python/nav/Snmp/defines.py | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 python/nav/Snmp/defines.py diff --git a/python/nav/Snmp/defines.py b/python/nav/Snmp/defines.py new file mode 100644 index 0000000000..303454e128 --- /dev/null +++ b/python/nav/Snmp/defines.py @@ -0,0 +1,43 @@ +# +# Copyright (C) 2023 Sikt +# +# This file is part of Network Administration Visualized (NAV). +# +# NAV is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License version 3 as published by +# the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. You should have received a copy of the GNU General Public +# License along with NAV. If not, see . +# +"""Defines types and enumerations for SNMP communication parameters""" +from enum import Enum + + +class SecurityLevel(Enum): + """SNMPv3 security levels""" + + NO_AUTH_NO_PRIV = "noAuthNoPriv" + AUTH_NO_PRIV = "authNoPriv" + AUTH_PRIV = "authPriv" + + +class AuthenticationProtocol(Enum): + """SNMPv3 authentication protocols supported by NET-SNMP""" + + MD5 = "MD5" + SHA = "SHA" + SHA512 = "SHA-512" + SHA384 = "SHA-384" + SHA256 = "SHA-256" + SHA224 = "SHA-224" + + +class PrivacyProtocol(Enum): + """SNMPv3 privacy protocols supported by NET-SNMP""" + + DES = "DES" + AES = "AES" From d63e1c33da42f06d95b74379fd813d6cd49f80dd Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 16:19:37 +0100 Subject: [PATCH 3/5] Add SNMPv3 session parameters to Snmp init This adds the optional SNMPv3 parameters to `nav.Snmp.pynetsnmp.Snmp`, along with parameter verification based on the selected SNMP version. --- python/nav/Snmp/errors.py | 4 +++ python/nav/Snmp/pynetsnmp.py | 56 ++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/python/nav/Snmp/errors.py b/python/nav/Snmp/errors.py index 215097b686..d8e5a14e12 100644 --- a/python/nav/Snmp/errors.py +++ b/python/nav/Snmp/errors.py @@ -31,3 +31,7 @@ class UnsupportedSnmpVersionError(SnmpError): class NoSuchObjectError(SnmpError): """SNMP agent did not know of this object""" + + +class SNMPv3ConfigurationError(SnmpError): + """Error in SNMPv3 configuration parameters""" diff --git a/python/nav/Snmp/pynetsnmp.py b/python/nav/Snmp/pynetsnmp.py index 65c3481ffc..869b389c17 100644 --- a/python/nav/Snmp/pynetsnmp.py +++ b/python/nav/Snmp/pynetsnmp.py @@ -31,7 +31,7 @@ c_ulong, c_uint64, ) -from typing import Union +from typing import Union, Optional from IPy import IP from pynetsnmp import netsnmp @@ -49,12 +49,14 @@ ) from nav.oids import OID +from .defines import SecurityLevel, AuthenticationProtocol, PrivacyProtocol from .errors import ( EndOfMibViewError, NoSuchObjectError, SnmpError, TimeOutException, UnsupportedSnmpVersionError, + SNMPv3ConfigurationError, ) PDUVarbind = namedtuple("PDUVarbind", ['oid', 'type', 'value']) @@ -93,12 +95,25 @@ def __init__( port: Union[str, int] = 161, retries: int = 3, timeout: int = 1, + # SNMPv3-only options + sec_level: Optional[SecurityLevel] = None, + auth_protocol: Optional[AuthenticationProtocol] = None, + sec_name: Optional[str] = None, + auth_password: Optional[str] = None, + priv_protocol: Optional[PrivacyProtocol] = None, + priv_password: Optional[str] = None, ): """Makes a new Snmp-object. :param host: hostname or IP address :param community: community (password), defaults to "public" :param port: udp port number, defaults to "161" + :param sec_level: SNMPv3 security level + :param auth_protocol: SNMPv3 authentication protocol + :param sec_name: SNMPv3 securityName + :param auth_password: SNMPv3 authentication password + :param priv_protocol: SNMPv3 privacy protocol + :param priv_password: SNMPv3 privacy password """ @@ -107,15 +122,52 @@ def __init__( self.version = str(version) if self.version == '2': self.version = '2c' - if self.version not in ('1', '2c'): + if self.version not in ('1', '2c', '3'): raise UnsupportedSnmpVersionError(self.version) self.port = int(port) self.retries = retries self.timeout = timeout + self.sec_level = SecurityLevel(sec_level) if sec_level else None + self.auth_protocol = ( + AuthenticationProtocol(auth_protocol) if auth_protocol else None + ) + self.sec_name = sec_name + self.auth_password = auth_password + self.priv_protocol = PrivacyProtocol(priv_protocol) if priv_protocol else None + self.priv_password = priv_password + if self.version == "3": + self._verify_snmpv3_params() + self.handle = _MySnmpSession(self._build_cmdline()) self.handle.open() + def _verify_snmpv3_params(self): + if not self.sec_level: + raise SNMPv3ConfigurationError("sec_level is required to be set") + if not self.sec_name: + raise SNMPv3ConfigurationError( + "sec_name is required regardless of security level" + ) + if self.sec_level in (SecurityLevel.AUTH_NO_PRIV, SecurityLevel.AUTH_PRIV): + if not self.auth_protocol: + raise SNMPv3ConfigurationError( + f"{self.sec_level.value} requires auth_protocol to be set" + ) + if not self.auth_password: + raise SNMPv3ConfigurationError( + f"{self.sec_level.value} requires auth_password to be set" + ) + if self.sec_level == SecurityLevel.AUTH_PRIV: + if not self.priv_protocol: + raise SNMPv3ConfigurationError( + f"{self.sec_level.value} requires priv_protocol to be set" + ) + if not self.priv_password: + raise SNMPv3ConfigurationError( + f"{self.sec_level.value} requires priv_password to be set" + ) + def _build_cmdline(self): try: address = IP(self.host) From 03eba245ab90aef6608316a42bf26dbc361ed28e Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Wed, 1 Nov 2023 16:20:38 +0100 Subject: [PATCH 4/5] Build SNMP command line args for SNMPv3 This reworks the SNMP command line builder to include SNMPv3 parameters as needed. --- python/nav/Snmp/pynetsnmp.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/python/nav/Snmp/pynetsnmp.py b/python/nav/Snmp/pynetsnmp.py index 869b389c17..da619a7cbc 100644 --- a/python/nav/Snmp/pynetsnmp.py +++ b/python/nav/Snmp/pynetsnmp.py @@ -176,16 +176,25 @@ def _build_cmdline(self): else: host = 'udp6:[%s]' % self.host if address.version() == 6 else self.host - return ( - '-v' + self.version, - '-c', - self.community, - '-r', - str(self.retries), - '-t', - str(self.timeout), - '%s:%s' % (host, self.port), + params = [f"-v{self.version}"] + + if self.version in ("1", "2c"): + params.extend(["-c", self.community]) + elif self.version == "3": + params.extend(["-l", self.sec_level.value, "-u", self.sec_name]) + if self.auth_protocol: + params.extend(["-a", self.auth_protocol.value]) + if self.auth_password: + params.extend(["-A", self.auth_password]) + if self.priv_protocol: + params.extend(["-x", self.priv_protocol.value]) + if self.priv_password: + params.extend(["-X", self.priv_password]) + + params.extend( + ["-r", str(self.retries), "-t", str(self.timeout), f"{host}:{self.port}"] ) + return tuple(params) def __del__(self): self.handle.close() From 2131cdafa051147958956ab4e77723689fd9d997 Mon Sep 17 00:00:00 2001 From: Morten Brekkevold Date: Thu, 2 Nov 2023 15:16:42 +0100 Subject: [PATCH 5/5] Protect against closing non-existant SNMP handles Snmp.__init__() can now raise exceptions before `self.handle` is ever created, thereby causing errors to be logged from Snmp.__del_ everytime the garbage collector deletes such defunct object. This protects against that silly situation. --- python/nav/Snmp/pynetsnmp.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/nav/Snmp/pynetsnmp.py b/python/nav/Snmp/pynetsnmp.py index da619a7cbc..43b25e086b 100644 --- a/python/nav/Snmp/pynetsnmp.py +++ b/python/nav/Snmp/pynetsnmp.py @@ -116,7 +116,7 @@ def __init__( :param priv_password: SNMPv3 privacy password """ - + self.handle = None self.host = host self.community = str(community) self.version = str(version) @@ -197,7 +197,8 @@ def _build_cmdline(self): return tuple(params) def __del__(self): - self.handle.close() + if self.handle: + self.handle.close() def get(self, query="1.3.6.1.2.1.1.1.0"): """Performs an SNMP GET query.