欢迎来到icp许可证证代办理!
西安
切换分站
免费发布信息
信息分类
当前位置:西安icp许可证证代办理 > 西安热点资讯 > 西安增值电信业务 >  SNMP有哪些优点?

SNMP有哪些优点?

发表时间:2023-05-05 11:28:46  来源:网络投稿  浏览:次   【】【】【
[db:摘要]

摘要

在之前的章节中,我们给大家讲解了如何使用 SSH 方式连接设备,并采集相应的信息,除此之外还设计了一个完整的巡检框架,支持巡检项的灵活扩展,支持对设备输出的解析和导出。

虽然和设备交互的过程中 SSH 是使用最为广泛的,但也存在一定的弊端,而 SNMP 正好可以填补这部分能力。这个章节开始我会讲解 SNMP 相关的细节,以及如何用 SNMP 来采集数据。

SNMP vs SSH

SSH

优点

  • 连接加密,数据传输也加密
  • CLI 的理解成本低
  • 容易做权限控制
  • 可交互式

缺点

SSH 可以让程序与设备进行 CLI 交互,也就是说既可以做配置也可以做查询,这里我们主要指的是查询用途。

  • 回显内容是非结构化的
  • 基于 TCP 的建立连接较慢
  • 设备存在 vty 数量限制

SNMP

SNMP 应该几乎做运维的朋友都有听过,它也是既可以做配置也可以做查询,但实际上一般都只用来做查询。

优点

  • 基于 UDP 请求响应较快
  • 结构化的回显 (回显仍然是文本,但内容更为结构化,几乎不需要解析)
  • 可以对设备做较高频率的查询 (频率越高,对设备 CPU 占用越高,所以需要提前压测)

缺点

  • 基于 UDP 连接,内容不可靠
  • 鉴权能力比较弱
  • 查询项与 OID 的对应关系较繁琐

一项技术肯定不可能全是优点没有缺点,所以 SNMP 也有一些非常显著的缺点,但它的优点是可以完美填补 SSH 查询信息的缺陷的,所以我们作为数据采集来说,SNMP 是必不可少的;

但它的缺点我们也不可忽视,也需要做一定的设计来尽量规避其缺点带来的问题。

SNMP 使用流程

确定目标设备的配置

目标设备的 SNMP 配置包含一些基本的信息,如版本号、网络地址、认证信息;

版本号目前广泛使用的是 v2 或 v3,v3 相比 v2 有更高的安全性,且传输的信息都是密文,但考虑到不是所有的设备都支持 v3,所以在只做查询的时候,通常会使用 v2 更为普遍。

v2 版本的认证是基于 Community 的,可以把其理解为一种用户名密码。

选择客户端发起的方法

  • Get,可以从设备中提取一个或多个结果;
  • GetNext,获取被管理设备MIB节点的下一个节点的结果;
  • GetBulk,相当于对 GetNext 的封装,例如一个 GetNext 只能返回当前 OID 的下一个 OID 的值,GetBulk 可以直接返回下 N 个,这样就可以减少发 N 个 GetNext 带来的网络开销;但会对超出响应所能承载的数据做截断;
  • Set/Trap/Inform,忽略

选择 OID

通常 OID 分为公有和私有,公有 OID 就是所有厂商都会遵守的,比如查询设备版本的 OID 就是所有厂商通用的,私有 OID 就是不同厂商针对自家设备的数据提供的特有标识,只能从厂商提供的 MIB 文档中查找。

OID 是和 MIB(Management Information Base) 有密不可分的关系的,设备的数据和各种会被维护的信息都会作为一个对象,在 MIB 数据库中被定义,每个对象会包含一些属性,如:对象名称、对象状态、访问权限、数据类型等。

由于设备上有非常多需要被维护的数据,且它们之间会有一定的关联关系,所以这么多数据对象在 MIB 库中会以树的形式存在,每个 OID 就对应树上的一个节点,树的结构如下所示:



从上图中可以看出,系统描述信息数据对象名称叫 sysDescr,OID 是 1.3.6.1.2.1.1.1;

这里需要大家注意的是,每个设备都有自己的一个数据库,数据库中的内容会对应到 MIB 中的某个节点。所以MIB文件不包含数据,而是类似于数据库中的表结构的概念。

私有 OID

其实大部分需要高频采集的都是公有 OID,比如端口名称,端口状态,端口流量,设备描述信息等,只有需要采集特定厂商的部分数据时才需要用到私有 OID。

私有 OID 可以直接到厂商的官方文档中寻找,原理其实也是 MIB 树,只不过是有一些自定义的节点而已。

MIB 可视化客户端

官方下载地址: http://www.ireasoning.com/download.shtml 提供个人免费版本下载: MIB Browser Free Personal Edition





确定请求的参数

上面也提到 SNMP 是基于 UDP 的,所以双方的交互是不可靠的,那必然就要考虑到各种异常情况导致的请求失败,所以在发起请求的时候,可以设置请求的最大超时时间,或者失败重试的最大次数。

处理响应内容

SNMP 的响应内容会包含几种不同的数据类型,常用的有整型、字符串、计数、时间戳;其结构也都非常清晰明了,所以对于响应内容的处理也非常简单。

Python 中使用 SNMP

第三方库的选择

PySNMP

有一个用纯 Python 实现 SNMP 协议的第三方库,叫 PySNMP,这个库对于协议的封装做的比较完善,但考虑到纯 Python 实现的性能问题,以及它创造出了不少新的对象和概念,会让刚接触的朋友有些难以理解,所以我们不计划采用 PySNMP,但这个库有一个优点就是支持异步。

NetSNMP

除此之外较为常用的还有 Net-SNMP,这也是一个 Python 第三方库,但它依赖操作系统中的 net-snmp 应用,所以本质上它其实是调用了操作系统上安装的 net-snmp 应用的接口;所以并发情况下,需要开多线程来进行请求,而不支持异步(如果大家对异步感兴趣,可以自行研究,或者找我交流)。

EasySNMP

Net-SNMP 可以看作是 net-snmp 应用的接口调用,那么就必然存在版本兼容问题,比如不同版本的 net-snmp 应用之间的差别可能会导致 Net-SNMP 出现不可预料的异常。

另外由于 net-snmp 是用 C/C++ 实现的,所以 Net-SNMP 基于此提供的封装也不够易用。

为了解决上述的问题,eaysnmp 出现了,据官方文档描述,easysnmp 0.2.5 版本支持 net-snmp 5.7 之后的所有版本,以及支持大多数的 Python3 和 Python 2.7 发行版;并且还提供了非常详细的使用文档。

EasySNMP 使用

安装 easysnmp

Linux 和 MacOS 安装 net-snmp 都相对简单,只需要一行命令即可,如下所示:

yum install net-snmp-devel # centos
brew install net-snmp # macos
pip install easysnmp==0.2.5 # 安装 Python 包,(提示,M1芯片的 Mac 需要 Python3.9 以上才能安装成功)

至于如何在 Windows 上安装 net-snmp,大家可以自行查找教程,我个人建议是既然要学习 SNMP,那么最终程序是一定要运行在服务器上的,所以开发阶段最好就用和服务器相似的环境来开发,避免环境切换导致的各种问题。

初步使用 easysnmp

from easysnmp import snmp_get, snmp_walk, snmp_bulkwalk, Session
resp = snmp_get("sysDescr.0", hostname="localhost", community="public", version=2)
resp = snmp_walk("ifDescr", hostname="localhost", community="public", version=2)
resp = snmp_bulkwalk("ifDescr", hostname="localhost", community="public", version=2)
​
session = Session(hostname="localhost", community="public", version=2)
resp = session.bulkwalk("ifDescr")

上述代码从 easysnmp 中直接引入了三种请求类型,分别是:

  • snmp_get:发起单个查询请求
  • snmp_walk:是对 GETNext 请求的封装
  • snmp_bulkwalk:SNMP 版本 2 以上可以使用,是对 GETBULK 的封装
  • snmp_set:用来对设备做配置,忽略不提

snmp_walk 和 snmp_bulkwalk 都可以起到一次性获取多个节点的效果,但 snmp_bulkwalk 与设备进行的 IO 交互更少。

snmp_get

Get 请求有一个值得注意的地方,大家应该可以发现上述代码中调用 snmp_get 函数的第一个参数传入的是 “sysDescr.0”,实际上查询设备描述的 OID 是“sysDescr”,这里为什么多了一个“.0”?

这是因为 SNMP 请求发送到指定设备后,会根据请求的 OID 去查询设备上的 MIB 树,根据 OID 对应找到 MIB 节点后进行实例化,实例化的节点对象中会包含:节点的 OID,索引,类型,以及结果。

MIB树的节点可以分为以下两种。

  • 叶子节点:即在完整的MIB树中不存在子节点的节点。叶子节点又分为标量节点和表节点。
  • 非叶子节点:用来表明该节点的子节点的相关性,不能通过SNMP协议直接访问。

我们通常进行查询的就是叶子节点;类似于 sysDescr 或者 sysUptime 这种节点就是标量对象,它只有一个实例化的值所以需要添加“.0”作为索引来进行请求;如果是 ifDescr 节点就是表对象,它会有多个实例值,可以使用 snmp_walk 请求将其所有实例一次性获取到,也可以通过指定索引来获取某个实例,比如“ifDescr.3”。

snmp_walk

Walk 请求会将该 OID 节点下的所有叶子节点全部请求回来,例如一次性获取所有的端口描述,实际工作中经常会用 snmp_walk 去请求端口的相关信息。

请求结果

Easysnmp 中对 SNMP 请求的结果进行了封装,将结果封装成 SNMPVariable,该对象中包含了:'oid', 'oid_index', 'snmp_type', 'value'四个属性。

snmp_get 函数返回的是一个 SNMPVariable 对象,snmp_walk 函数返回的是一个 SNMPVariable 的对象列表

SNMP 执行器

之前的章节中我们定义了一个 Action 对象如下:

class Action(db.Model):
    __tablename__ = "action"
    __table_args__ = {"extend_existing": True}
​
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(64), nullable=False, comment="动作名称")
    description = db.Column(db.String(256), comment="动作描述")
    vendor = db.Column(db.String(64), comment="厂商")
    model = db.Column(db.String(64), comment="型号")
    cmd = db.Column(db.String(256), nullable=False, comment="命令行")
    type = db.Column(db.String(8), comment="命令类型[show|config]")
    parse_type = db.Column(db.String(8), comment="解析类型[regexp|textfsm]")
    parse_content = db.Column(db.Text, comment="解析内容")

Action 对象之前表示的是巡检项,但从它的字段中可以发现,在不考虑 type/parse_type/parse_content 的同时,将 cmd 字段的内容用来存储某个 OID,那么这个对象就可以被用于表示 snmp 项了。

那么结合抽象的执行器以及复用 Action 对象就可以实现一个 SNMP 执行器,代码如下:

# junior/flaskProject/application/services/snmp_executor.py
import logging
from datetime import datetime
from typing import Optional, Dict, List
​
from easysnmp import Session, SNMPVariable
​
from junior.flaskProject.application.services.executor import Executor
from .action import Action
from .device import Device
​
​
class SNMPExecutor(Executor):
    SNMP_Version = 2
    SNMP_Port = 161
​
    def __init__(
            self,
            community: str,
            device: Device,
            timeout: int = 10,
            logger: Optional[logging.Logger] = None,
            log_file: str = "snmp.log",
            log_level: str = logging.INFO,
            log_format: str = "%(asctime)s %(levelname)s %(name)s %(message)s",
            retry_times: int = 3):
        self.device = device
        self.host = self.device.ip
        self.session = Session(
            hostname=self.device.ip, version=self.SNMP_Version, community=community,
            timeout=timeout, retries=retry_times, remote_port=self.SNMP_Port)
​
        self.logger = logger
        if self.logger is None:
            logging.basicConfig(filename=log_file, level=log_level, format=log_format)
            self.logger = logging.getLogger(__name__)
​
        self.result: List[Dict] = []
​
    def __enter__(self) -> "SNMPExecutor":
        return self
​
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            self.logger.error(exc_type)
            self.logger.error(exc_val)
            self.logger.error(exc_tb)
​
    def execute(self, action: Action, last_variables: Optional[List[Dict]] = None) -> List[Dict]:
        if ".0" in action.cmd:
            self.logger.info(f"{self.session.hostname} snmp get action: {action.cmd}")
            output = [self.session.get(action.cmd)]
        else:
            self.logger.info(f"{self.session.hostname} snmp bulkwalk action: {action.cmd}")
            output = self.session.bulkwalk(action.cmd)
        return self.parse(output, last_variables)
​
    def parse(self, current_variables: List[SNMPVariable], last_variables: List[Dict]) -> List[Dict]:
        oid_idx_map = {item.oid_index: item for item in current_variables}
        last_oid_idx_map = {}
        if last_variables:
            last_oid_idx_map = {item["oid_index"]: item for item in last_variables}
        for idx in oid_idx_map:
            if oid_idx_map[idx].snmp_type == "COUNTER" and last_oid_idx_map:
                if idx not in last_oid_idx_map:
                    self.logger.error(f"not found oid index {idx} in last_variable")
                    continue
                last_var = last_oid_idx_map[idx]
                oid_idx_map[idx].value = (int(oid_idx_map[idx].value) - int(last_var["value"])) / (datetime.now().timestamp() - last_var["timestamp"])
        return self.save(oid_idx_map)
​
    def save(self, variables: Dict[str, SNMPVariable]) -> List[Dict]:
        res = []
        for item in variables.values():
            value = item.value
            if item.snmp_type in ["COUNTER", "INTEGER", "unsigned INTEGER", "TIMETICKS", "unsigned int64", "signed int64", "float", "double"]:
                value = float(value)
            res.append({
                "oid_index": item.oid_index,
                "oid": item.oid,
                "value": value,
                "snmp_type": item.snmp_type,
                "timestamp": int(datetime.now().timestamp()),
            })
        return res

上述代码中的三个方法简单阐述一下:

  • self.execute 方法:该方法通过判断 OID 中是否包含“.0”来决定使用的 snmp 请求类型
  • self.parse 方法:该方法中接受了一个 last_variables 参数,这是为了处理 COUNTER 类型的信息,因为对于流量之类的采集项,是通过计数器来进行计算的,所以通过传入上一次采集的结果,来对这一段时间的计数器差值做计算,再除以时间差。
  • self.save 方法,该方法是为了将 SNMVariable 类型不透出底层的执行器,在将它转成字典的同时,添加上此刻的时间戳。

测试

import json
​
from junior.flaskProject.application.models import Action
from junior.flaskProject.application.services.snmp_executor import SNMPExecutor
from junior.flaskProject.application.services.device import Device
​
device = Device.to_model(**{"ip": "192.168.31.149"})
action = Action.to_model(**{"cmd": "ifOutOctets"})
​
with SNMPExecutor("public", device) as snmp:
    last = snmp.execute(action)
    resp = snmp.execute(action, last)
print(json.dumps(resp, indent=2))

总结

这一章节主要介绍了在自动化运维中不可或缺的 SNMP 信息采集相关的内容,包括 SNMP 的基本原理,以及在 Python 中如何进行 SNMP 请求,最后实现了一个 SNMPExecutor 来丰富我们底层执行器的功能,这样可以为后续上层的功能提供更好的复用能力。

后面的章节我们会尝试使用 SSHExecutor 和 SNMPExecutor 来实现 CMDB 信息的更新,以此来让 CMDB 不止具有单纯的记录作用,并且还具备实效性。

附录

责任编辑: