大多数连接到网络的设备使用 SNMP(简单网络管理协议)报告其状态。该协议主要用于管理和监控网络连接的硬件设备,但一些应用程序也使用该协议公开其统计数据。在本章中,我们将了解如何从 Python 应用程序访问此信息。我们将使用 RRDTool(一种广为人知且流行的应用程序和库,用于存储和绘制性能数据)将获得的数据存储在 RRD(循环数据库)中。最后,我们将研究 Jinja2 模板系统,我们将使用它为我们的应用程序生成简单的网页。

应用要求和设计

系统监控的主题非常广泛,通常包含许多不同的领域。一个完整的监控系统相当复杂,通常由多个协同工作的组件组成。我们不会在这里开发一个完整的、自给自足的系统,但我们将研究典型监控系统的两个重要领域:信息收集和表示。在本章中,我们将实现一个系统,该系统使用 SNMP 协议查询设备,然后使用 RRDTool 库存储数据,该库还用于生成用于可视化数据表示的图形。所有这些都使用 Jinja2 模板库绑定到简单的网页中。随着本章的进行,我们将更详细地查看这些组件中的每一个。

指定要求

在我们开始设计我们的应用程序之前,我们需要为我们的系统提出一些要求。首先,我们需要了解我们期望系统提供的功能。这将帮助我们创建一个有效的(并且我们希望易于实现)系统设计。在本章中,我们将创建一个使用 SNMP 协议监视网络连接设备(例如网络交换机和路由器)的系统。所以第一个要求是系统能够使用 SNMP 查询任何设备。

需要存储从设备收集的信息以备将来参考和分析。让我们对这些信息的使用做一些假设。首先,我们不需要无限期地存储它。(我将在第 9-11 章中详细讨论永久信息存储。)这意味着信息仅存储一段预定义的时间,一旦过时,它将被删除。这提出了我们的第二个要求:信息需要在“过期”后删除。

其次,需要存储信息以便生成图表。我们不会将它用于其他任何事情,因此应该针对数据表示任务优化数据存储。

最后,我们需要生成图表并在易于访问的网页上表示这些信息。信息只需按设备名称进行结构化。例如,如果我们正在监控多个设备的 CPU 和网络接口利用率,则需要在单个页面上显示此信息。我们不需要在多个时间尺度上呈现这些信息;默认情况下,图表应显示过去 24 小时的性能指标。

高级设计规范

现在我们对系统的功能有了一些想法,让我们创建一个简单的设计,我们将在开发阶段将其用作指南。基本方法是我们之前指定的每一个要求都应该包含在一个或多个设计决策中。

第一个要求是我们需要监控网络连接的设备,我们需要使用 SNMP 来做到这一点。这意味着我们必须使用适当的 Python 库来处理 SNMP 对象。默认 Python 安装中不包含 SNMP 模块,因此我们必须使用外部模块之一。我建议使用 PySNMP 库(可从http://pysnmp.sourceforge.net/ 获得),它在大多数流行的 Linux 发行版上都可用。

数据存储引擎的完美候选者是 RRDTool(可从http://oss.oetiker.ch/rrdtool/ 获得)。循环数据库意味着数据库的结构是每个“表”的长度都是有限的,一旦达到限制,最旧的条目就会被删除。事实上,它们并没有被丢弃;新的只是简单地写入他们的位置。

RRDTool 库提供两种不同的功能:数据库服务和图形生成工具包。Python 中没有对 RRD 数据库的本机支持,但有一个可用的外部库,它提供了到 RRDTool 库的接口。

最后,为了生成网页,我们将使用 Jinja2 模板库(可在http://jinja.pocoo.org或 GitHub 上获取:https : //github.com/mitsuhiko/jinja2),它让我们可以创建复杂的模板并解耦设计和开发任务。

我们将使用一个简单的 Windows INI 样式的配置文件来存储有关我们将要监控的设备的信息。此信息将包括设备地址、SNMP 对象引用和访问控制详细信息等详细信息。

该应用程序将分为两部分:第一部分是信息收集工具,用于查询所有配置的设备并将数据存储在 RRDTool 数据库中,第二部分是报告生成器,它生成网站结构以及所有所需的图像。这两个组件都将从标准的 UNIX 调度程序应用程序 cron 中实例化。这两个脚本将分别命名为snmp-manager.py和snmp-pages.py 。

SNMP 简介

SNMP(简单网络管理协议)是一种基于 UDP 的协议,主要用于管理网络连接设备,例如路由器、交换机、计算机、打印机、摄像机等。某些应用程序还允许通过 SNMP 协议访问内部计数器。

SNMP 不仅允许您从设备读取性能统计信息,它还可以发送控制消息来指示设备执行某些操作——例如,您可以使用 SNMP 命令远程重启路由器。

由简单网络管理协议 (SNMP) 管理的系统中包含三个主要组件:

·负责管理所有设备的管理系统

·被管理的设备,即管理系统管理的所有设备

·SNMP 代理,它是一个应用程序,运行在每个受管设备上并与管理系统交互

这种关系如图 1-1 所示。

图1-1。SNMP 网络组件

这种方法比较通用。该协议定义了七个基本命令,其中我们最感兴趣的是get 、get bulk和response 。你可能已经猜到了,前两个是管理系统发给代理的命令,后两个是代理软件的响应。

管理系统如何知道要查找什么?该协议没有定义交换此信息的方式,因此管理系统无法询问代理以获取可用变量列表。

该问题可通过使用管理信息库(或 MIB)解决。每个设备通常都有一个关联的 MIB,它描述了该系统上管理数据的结构。此类 MIB 将按层次顺序列出受管设备上可用的所有对象标识符 (OID)。OID 有效地表示对象树中的一个节点。它包含从树顶部的节点开始,通向当前 OID 的所有节点的数字标识符。节点 ID 由 IANA(互联网号码分配机构)分配和管理。一个组织可以申请一个 OID 节点,当它被分配时,它负责管理分配的节点下面的 OID 结构。

图 1-2说明了 OID 树的一部分。

图1-2。SNMP OID 树

让我们看一些示例 OID。分配给 Cisco 组织的 OID 树节点的值为 1.3.6.1.4.1.9,这意味着与 Cisco 制造的设备关联的所有专有 OID 都将以这些数字开头。同样,Novell 设备的 OID 将以 1.3.6.1.4.1.23 开头。

我特意强调了专有 OID,因为某些属性预计会出现在所有设备上(如果可用)。它们位于 RFC1213 定义的 1.3.6.1.2.1.1(系统 SNMP 变量)节点下。有关 OID 树及其元素的更多详细信息,请访问http://www.alvestrand.no/objectid/top.html。该网站允许您浏览 OID 树,它包含相当多的各种 OID 的集合。

系统 SNMP 变量节点

在大多数情况下,有关设备的基本信息将在 System SNMP Variables OID 节点子树下可用。因此,让我们仔细看看你能在那里找到什么。

这个 OID 节点包含几个额外的 OID 节点。表 1-1提供了大多数子节点的说明。

表1-1。系统 SNMP OID

OID 字符串

OID 名称

描述

1.3.6.1.2.1.1.1

系统描述

包含系统或设备简短描述的字符串。通常包含硬件类型和操作系统详细信息。

1.3.6.1.2.1.1.2

系统对象ID

包含供应商特定设备 OID 节点的字符串。例如,如果组织被分配了一个 OID 节点 1.3.6.1.4.1.8888 并且这个特定设备被分配了一个组织空间下的 .1.1 OID 空间,则该字段将包含值 1.3.6.1.4.1.8888.1 .1.

1.3.6.1.2.1.1.3

系统正常运行时间

一个数字,表示从系统初始化开始的数百秒的时间。

1.3.6.1.2.1.1.4

系统联系人

包含有关负责此系统的联系人的信息的任意字符串。

1.3.6.1.2.1.1.5

系统名称

已分配给系统的名称。通常这个字段包含一个完全限定的域名。

1.3.6.1.2.1.1.6

系统位置

描述系统物理位置的字符串。

1.3.6.1.2.1.1.7

系统服务

指示此系统提供哪些服务的数字。该数字是所有 OSI 协议的位图表示,最低位表示第一个 OSI 层。例如,一个交换设备(在第 2 层上运行)会将这个数字设置为 2 2 = 4。这个字段现在很少使用。

1.3.6.1.2.1.1.8

sysLastChange

包含在更改任何系统 SNMP 对象时sysUpTime值的数字。

1.3.6.1.2.1.1.9

系统表

包含多个sysEntry元素的节点。每个元素代表一个不同的能力和相应的 OID 节点值。

接口 SNMP 变量节点

类似地,可以从 Interfaces SNMP Variables OID 节点子树中获取基本接口统计信息。接口变量的 OID 是 1.3.6.1.2.1.2 并且包含两个子节点:

·包含网络接口总数的 OID。此条目的 OID 值为 1.3.6.1.2.1.2.1;它通常被引用为ifNumber 。此 OID 下没有可用的子节点。

·包含所有接口条目的 OID 节点。它的 OID 是 1.3.6.1.2.1.2.2 并且它通常被引用为ifTable 。该节点包含一个或多个入口节点。入口节点(1.3.6.1.2.1.2.2.1,也称为ifEntry )包含有关该特定接口的详细信息。列表中的条目数由ifNumber节点值定义。

您可以在表 1-2 中找到有关所有ifEntry子节点的详细信息。

表1-2。接口条目 SNMP OID

OID 字符串

OID 名称

描述

1.3.6.1.2.1.2.2.1.1

如果索引

分配给接口的唯一序列号。

1.3.6.1.2.1.2.2.1.2

如果描述

包含接口名称和其他可用信息的字符串,例如硬件制造商的名称。

1.3.6.1.2.1.2.2.1.3

如果类型

代表接口类型的数字,取决于接口的物理链路和协议。

1.3.6.1.2.1.2.2.1.4

如果Mtu

此接口可以传输的最大网络数据报。

1.3.6.1.2.1.2.2.1.5

如果速度

接口的估计当前带宽。如果无法计算当前带宽,则此数字应包含接口的最大可能带宽。

1.3.6.1.2.1.2.2.1.6

如果物理地址

接口的物理地址,通常是以太网接口上的 MAC 地址。

1.3.6.1.2.1.2.2.1.7

如果管理员状态

此 OID 允许设置接口的新状态。通常限于以下值:1(向上)、2(向下)、3(测试)。

1.3.6.1.2.1.2.2.1.8

操作状态

接口的当前状态。通常限于以下值:1(向上)、2(向下)、3(测试)。

1.3.6.1.2.1.2.2.1.9

ifLastChange

当此接口进入其当前状态时,包含系统正常运行时间 ( sysUpTime ) 读数的值。如果接口在上次系统重新初始化之前进入此状态,则可以设置为零。

1.3.6.1.2.1.2.2.1.10

ifInOctets

在接口上接收的字节(八位字节)总数。

1.3.6.1.2.1.2.2.1.11

ifInUcastPkts

转发到设备网络堆栈的单播数据包数。

1.3.6.1.2.1.2.2.1.12

ifInNUcastPkts

传送到设备网络堆栈的非单播数据包的数量。非单播数据包通常是广播或多播数据包。

1.3.6.1.2.1.2.2.1.13

ifInDiscards

丢弃的数据包数。这并不表示数据包错误,但可能表示接收缓冲区太小而无法接受数据包。

1.3.6.1.2.1.2.2.1.14

如果错误

收到的无效数据包的数量。

1.3.6.1.2.1.2.2.1.15

ifInUnknownProtos

由于设备接口不支持协议而被丢弃的数据包数。

1.3.6.1.2.1.2.2.1.16

ifOutOctets

从接口传输的字节数(八位字节)。

1.3.6.1.2.1.2.2.1.17

ifOutUcastPkts

从设备的网络堆栈接收的单播数据包的数量。该数字还包括被丢弃或未发送的数据包。

1.3.6.1.2.1.2.2.1.18

ifNUcastPkts

从设备的网络堆栈接收的非单播数据包的数量。该数字还包括被丢弃或未发送的数据包。

1.3.6.1.2.1.2.2.1.19

ifOutDiscards

被丢弃的有效数据包的数量。这不是错误,但可能表示发送缓冲区太小而无法接受所有数据包。

1.3.6.1.2.1.2.2.1.20

ifOutErrors

由于错误而无法传输的传出数据包的数量。

1.3.6.1.2.1.2.2.1.21

ifOutQLen

出站数据包队列的长度。

1.3.6.1.2.1.2.2.1.22

如果特定

通常包含对描述此接口的供应商特定 OID 的引用。如果此类信息不可用,则将值设置为 OID 0.0,这在  语法上是 有效的,但不指向任何内容。

SNMP 中的身份验证

早期 SNMP 实现中的身份验证有些原始并且容易受到攻击。SNMP 代理定义了两个团体字符串:一个用于只读访问,另一个用于读/写访问。当管理系统连接到代理时,它必须使用这两个字符串之一进行身份验证。代理仅接受来自已使用有效社区字符串进行身份验证的管理系统的命令。

从命令行查询 SNMP

在开始编写  应用程序之前,让我们快速了解一下如何从命令行查询 SNMP。如果您想检查 SNMP 代理返回的信息是否被您的应用程序正确接受,这将特别有用。

命令行工具由 Net-SNMP-Utils 包提供,该包可用于大多数 Linux 发行版。该软件包包括用于查询和设置 SNMP 对象的工具。有关安装此软件包的详细信息,请参阅 Linux 发行版文档。例如,在基于 RedHat 的系统上,您可以使用以下命令安装这些工具:

$ sudo yum install net-snmp-utils

在基于 Debian 的系统上,可以像这样安装软件包:

$ sudo apt-get install snmp

这个包中最有用的命令是snmpwalk ,它将一个 OID 节点作为参数并尝试发现所有子节点 OID。此命令使用 SNMP 操作getnext ,它返回树中的下一个节点,并有效地允许您从指定的节点遍历整个子树。如果没有指定 OID,snmpwalk将使用默认的 SNMP 系统 OID (1.3.6.1.2.1) 作为起点。清单 1-1演示了针对运行 Fedora Linux 的笔记本电脑发出的snmpwalk命令。

清单 1-1。snmpwalk命令的示例

$ snmpwalk –v2c -c public -On 192.168.1.68

作为练习,尝试使用表 1-1和1-2识别一些列出的 OID , 并找出它们的含义。

从 Python 查询 SNMP 设备

现在我们对 SNMP 有了足够的了解,可以开始在我们自己的管理系统上工作,它将定期查询配置的系统。首先让我们指定我们将在应用程序中使用的配置。

配置应用程序

我们已经知道,每次检查都需要以下信息:

·运行SNMP代理软件的系统的IP地址或可解析域名

·将用于与代理软件进行身份验证的只读社区字符串

·OID节点的数值表示

我们将使用 Windows INI 风格的配置文件,因为它很简单。Python默认包含了一个配置解析模块,使用起来也很方便。(第 9 章详细讨论了ConfigParser模块;有关该模块的更多信息,请参阅该章节。)

让我们回到我们的应用程序的配置文件。不需要为我们要查询的每个 SNMP 对象重复系统信息,因此我们可以在单独的部分中定义每个系统参数一次,然后在每个检查部分中引用系统 ID。check 部分定义了 OID 节点标识符字符串和简短描述,如清单1-2 所示。使用下面列表中的内容创建一个名为snmp-manage.cfg的配置文件;不要忘记相应地修改 IP 和安全详细信息。

清单 1-2。snmp-manager.cfg

[system_1]
description=My Laptop
address=127.0.0.1
port=161
communityro=public#[system_2]
#description=My Server
#address=192.168.1.68
#port=161
#communityro=public[check_1]
description=WLAN incoming traffic
oid=1.3.6.1.2.1.2.2.1.10.3
system=system_1
sampling_rate=600[check_2]
description=WLAN outgoing traffic
oid=1.3.6.1.2.1.2.2.1.16.3
system=system_1
sampling_rate=600

确保 系统和检查部分 ID 是唯一的,否则您可能会得到不可预测的结果。

我们将 使用两种方法创建一个SnmpManager类,一种用于添加系统,另一种用于添加检查。由于支票包含系统 ID 字符串,它将自动分配给该特定系统。在清单1-3 中,您可以看到类定义以及读取配置并遍历各个部分并相应地更新类对象的初始化部分。创建一个名为snmp-manage.py的文件,其内容如下表所示。我们将继续为脚本添加新功能。

清单 1-3。读取和存储配置

import sys
from configparser import ConfigParserclass SnmpManager:def __init__(self):self.systems = {}def add_system(self, id, descr, addr, port, comm_ro):self.systems[id] = {'description': descr,'address': addr,'port': int(port),'communityro': comm_ro,'checks': {}}def add_check(self, id, oid, descr, system):oid_tuple = tuple([int(i) for i in oid.split('.')])self.systems[system]['checks'][id] = {'description': descr,'oid': oid_tuple,}def main(conf_file=""):if not conf_file:sys.exit(-1)config = ConfigParser()config.read(conf_file)snmp_manager = SnmpManager()for system in [s for s in config.sections() if s.startswith('system')]:snmp_manager.add_system(system,config.get(system, 'description'),config.get(system, 'address'),config.get(system, 'port'),config.get(system, 'communityro'))for check in [c for c in config.sections() if c.startswith('check')]:snmp_manager.add_check(check,config.get(check, 'oid'),config.get(check, 'description'),config.get(check, 'system'))print(snmp_manager.systems)if __name__ == '__main__':main(conf_file='snmp-manager.cfg')

正如您在示例中看到的,在继续检查部分之前,我们首先必须遍历系统部分并更新对象。

注意  此顺序很重要,因为如果我们尝试为尚未插入的系统添加检查,我们将收到字典索引错误。

另请注意,我们将 OID 字符串转换为整数元组。您将在本节后面看到为什么我们必须这样做。配置文件已加载,我们已准备好对配置的设备运行 SNMP 查询。

使用 PySNMP 库

在这个项目中,我们将使用 PySNMP 库,它是用纯 Python 实现的,不依赖于任何预编译库。该pysnmp包可用于大多数Linux发行版,可以使用标准的分发包管理器进行安装。除了pysnmp,您还需要 ASN.1 库,它由pysnmp使用,也可作为 Linux 分发包选择的一部分使用。例如,在 Fedora 系统上,您可以使用以下命令安装pysnmp模块:

$须藤yum 安装 pysnmp
$ 须藤 yum 安装 python-pyasn1

或者,您可以使用 Python 包管理器 (PiP) 为您安装此库:

$ sudo pip install pysnmp
$ sudo pip install pyasn1

如果您没有可用的pip命令,您可以从http://pypi.python.org/pypi/pip下载并安装此工具。我们也会在后面的章节中使用它。

PySNMP 库使用简单的 API 将 SNMP 处理的所有复杂性隐藏在单个类后面。您所要做的就是创建一个CommandGenerator类的实例。此类可从pysnmp.entity.rfc3413.oneliner.cmdgen模块获得,并实现大多数标准 SNMP 协议命令:getCmd( ) 、setCmd()和nextCmd()。让我们更详细地了解其中的每一个。

SNMP GET 命令

我们将要讨论的所有命令都遵循相同的调用模式:导入模块,创建 CommandGenerator 类的实例,创建三个必需的参数(身份验证对象、传输目标对象和参数列表),最后调用适当的方法。该方法返回一个包含错误指示符(如果有错误)和结果对象的元组。

在清单1-4 中,我们使用标准 SNMP OID (1.3.6.1.2.1.1.1.0) 查询远程 Linux 机器。

清单 1-4。SNMP GET 命令示例

from pysnmp.entity.rfc3413.oneliner import cmdgencg = cmdgen.CommandGenerator()
comm_data = cmdgen.CommunityData('my-manager', 'public')
transport = cmdgen.UdpTransportTarget(('127.0.0.1', 161))
variables = (1, 3, 6, 1, 2, 1, 1, 1, 0)
errIndication, errStatus, errIndex, result = cg.getCmd(comm_data, transport, variables)
print(errIndication)print(errStatus)print(errIndex)print(result)

输出:

None
0
0
[ObjectType(ObjectIdentity(<ObjectName value object, tagSet <TagSet object, tags 0:0:6>, payload [1.3.6.1.2.1.1.1.0]>), <DisplayString value object, tagSet <TagSet object, tags 0:0:4>, subtypeSpec <ConstraintsIntersection object, consts <ValueSizeConstraint object, consts 0, 65535>, <ValueSizeConstraint object, consts 0, 255>, <ValueSizeConstraint object, consts 0, 255>>, encoding iso-8859-1, payload [Hardware: Intel6...iprocessor Free)]>)]

让我们更仔细地看一些步骤。当我们启动社区数据对象时,我们提供了两个字符串——社区字符串(第二个参数)和代理或经理安全名称字符串;在大多数情况下,这可以是任何字符串。可选参数指定要使用的 SNMP 版本(默认为 SNMP v2c)。如果您必须查询版本 1 设备,请使用以下命令:

>>> comm_data =  cmdgen.CommunityData( '我的经理','公共',mpModel=0)

传输对象由包含完全限定域名或 IP 地址字符串和整数端口号的元组启动。

最后一个参数是 OID,表示为组成我们正在查询的 OID 的所有节点 ID 的元组。因此,我们之前在读取配置项时,不得不将点分隔的字符串转换为元组。

最后,我们调用 API 命令getCmd( ) ,它实现了 SNMP GET 命令,并将这三个对象作为其参数传递。该命令返回一个元组,其中的每个元素在表 1-3 中描述。

表1-3。命令生成器返回对象

元组元素

描述

错误指示

如果此字符串不为空,则表示 SNMP 引擎错误。

错误状态

如果此元素评估为 True,则表示 SNMP 通信中存在错误;产生错误的对象由errIndex元素指示。

错误索引

如果errStatus指示发生了错误,则该字段可用于查找导致错误的 SNMP 对象。结果数组中的对象位置是errIndex-1 。

结果

此元素包含所有返回的 SNMP 对象元素的列表。每个元素都是一个包含对象名称和对象值的元组。

SNMP SET 命令

SNMP SET 命令在 PySNMP 中映射到setCmd( )方法调用。所有参数都相同;唯一的区别是变量部分现在包含一个元组:OID 和新值。让我们试着用这个命令来改变一个只读对象;清单1-5显示了命令行序列。

清单 1-5。SNMP SET 命令示例

>>> from pysnmp.entity.rfc3413.oneliner import cmdgen
>>> from pysnmp.proto import rfc1902
>>> cg = cmdgen.CommandGenerator()
>>> comm_data = cmdgen.CommunityData('my-manager', 'public' )
>>> 传输 = cmdgen.UdpTransportTarget(('192.168.1.68', 161))
>>> 变量 = ((1, 3, 6, 1, 2, 1, 1, 1, 0), rfc1902.OctetString( '新系统描述'))
>>> errIndication, errStatus, errIndex, result = cg.setCmd(comm_data, transport, variables) >>> print errIndication None >>> print errStatus 6 >>> print errIndex 1 >>> print错误状态。 PrettyPrint () noAccess(6) >>> 打印结果

[(ObjectName('1.3.6.1.2.1.1.1.0'), OctetString('新系统描述'))]
>>>

这里发生的事情是我们试图写入一个只读对象,这导致了错误。这个例子中有趣的是我们如何格式化参数。您必须将字符串转换为 SNMP 对象类型;除此以外; 它们不会作为有效参数传递。因此,字符串必须封装在OctetString类的实例中。如果需要转换为其他SNMP类型,可以使用rfc1902模块的其他方法;方法包括Bits() , Counter32 () , Counter64() , Gauge32() , Integer() , Integer32() , IpAddress() , OctetString() ,Opaque() 、TimeTicks()和Unsigned32() 。如果您需要将字符串转换为特定类型的对象,则可以使用这些所有类名。

SNMP GETNEXT 命令

SNMP GETNEXT 命令是作为nextCmd( )方法实现的。语法和用法与getCmd( )相同;唯一的区别是结果是一个对象列表,这些对象是指定 OID 节点的直接子节点。

让我们用这个命令来查询所有是 SNMP 系统 OID (1.3.6.1.2.1.1) 的直接子节点的对象;清单 1-6显示了正在运行的 nextCmd( )方法。

清单 1-6。SNMP GETNEXT 命令示例

>>> from pysnmp.entity.rfc3413.oneliner import cmdgen
>>> cg = cmdgen.CommandGenerator()
>>> comm_data = cmdgen.CommunityData('my-manager', 'public')
>>> transport = cmdgen.UdpTransportTarget (('192.168.1.68', 161))
>>> variables = (1, 3, 6, 1, 2, 1, 1)
>>> errIndication, errStatus, errIndex, result = cg.nextCmd(comm_data, transport,变量)
>>> 打印 errIndication
requestTimedOut
>>> errIndication, errStatus, errIndex, result = cg.nextCmd(comm_data, transport, variables)
>>> 打印 errIndication
None
>>> 打印 errStatus
0
>>> 打印 errIndex
0
>>> for object in result:
... 打印对象
...
[(ObjectName('1.3.6.1.2.1.1.1.0'), OctetString('Linux fedolin.example.com  2.6.32.11-99.fc12.i686 #1 SMP Mon Apr 5 16:32:08 EDT 2010 i686' ))] [(ObjectName('1.3.6.1.2.1.1.2.0'), ObjectIdentifier('1.3.6.1.4.1.8072.3.2.10'))] [(ObjectName('1.3.6.1.2.1.1.3.0 ') '), TimeTicks('340496'))] [(ObjectName('1.3.6.1.2.1.1.4.0'), OctetString('Administrator (admin@example.com)'))] [(ObjectName('1.3. 6.1.2.1.1.5.0'), OctetString('fedolin.example.com'))] [(ObjectName('1.3.6.1.2.1.1.6.0'), OctetString('MyLocation, MyOrganization, MyStreet, MyCity, MyCountry'))] [(ObjectName('1.3.6.1.2.1.1.8.0'), TimeTicks('3'))] [(ObjectName('1.3.6.1.2.1.1.9.1.2.1'), ObjectIdentifier('1.3.6.1.6.3.10.3.1.1'))]

[(ObjectName('1.3.6.1.2.1.1.9.1.2.2'), ObjectIdentifier('1.3.6.1.6.3.11.3.1.1'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.2.) 3'), ObjectIdentifier('1.3.6.1.6.3.15.2.1.1'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.2.4'), ObjectIdentifier('1.3.6.1.6.3.1') ))]
[(ObjectName('1.3.6.1.2.1.1.9.1.2.5'), ObjectIdentifier('1.3.6.1.2.1.49'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.2 ') .6'), ObjectIdentifier('1.3.6.1.2.1.4'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.2.7'), ObjectIdentifier('1.3.6.1.2.1.50') )]
[(ObjectName('1.3.6.1.2.1.1.9.1.2.8'), ObjectIdentifier('1.3.6.1.6.3.16.2.2.1')​​)]
[(ObjectName('1.3.6.1.2.1.1.9. 1.3.1'), OctetString('SNMP 管理 架构 MIB.'))] [( 对象名称(
'1.3.6.1.2.1.1.9.1.3.2'), OctetString('消息处理 和调度的 MIB .'))] [( ObjectName( '1.3.6.1.2.1.1.9.1.3.3'), OctetString ('  SNMP 基于用户的安全模型的管理信息定义。'))] [( ObjectName( '1.3.6.1.2.1.1.9.1.3.4'), OctetString('SNMPv2 实体的 MIB 模块')) ] [( ObjectName( '1.3.6.1.2.1.1.9.1.3.5'), OctetString('管理 TCP  实现的 MIB 模块'))] [( ObjectName( '1.3.6.1.2.1.1.9.1.3.6 '), OctetString('管理 IP  和 ICMP 实现的 MIB 模块'))] [( ObjectName(

'1.3.6.1.2.1.1.9.1.3.7'), OctetString('管理UDP  实现的MIB模块'))] [( ObjectName( '1.3.6.1.2.1.1.9.1.3.8'), OctetString( '基于视图的 SNMP访问控制模型。'))] [( ObjectName( '1.3.6.1.2.1.1.9.1.4.1'), TimeTicks('3'))] [( ObjectName( '1.3.6.1. 2.1.1.9.1.4.2'), TimeTicks('3'))] [( ObjectName( '1.3.6.1.2.1.1.9.1.4.3'), TimeTicks('3'))] [( ObjectName( ') 1.3.6.1.2.1.1.9.1.4.4'), TimeTicks('3'))] [( ObjectName( '1.3.6.1.2.1.1.9.1.4.5'), TimeTicks('3')))] [( ObjectName( '1.3.6.1.2.1.1.9.1.4.6'), TimeTicks('3'))] [(

ObjectName( '1.3.6.1.2.1.1.9.1.4.7'), TimeTicks('3'))]
[( ObjectName( '1.3.6.1.2.1.1.9.1.4.8'), TimeTicks('3') )]
>>>

如您所见,结果与命令行工具snmpwalk产生的结果相同,后者使用相同的技术来检索 SNMP OID 子树。

实现 SNMP 读取功能

让我们在我们的应用程序中实现读取功能。工作流程如下:我们需要遍历列表中的所有系统,并为每个系统遍历所有定义的检查。对于每次检查,我们将执行 SNMP GET 命令并将结果存储在相同的数据结构中。

出于调试和测试目的,我们将添加一些打印语句来验证应用程序是否按预期工作。稍后我们将用 RRDTool 数据库存储命令替换这些打印语句。我将把这个方法称为 query_all_ systems( ) 。清单 1-7显示了您希望将其添加到您之前创建的snmp-manager.py文件中的代码。

清单 1-7。查询所有定义的 SNMP 对象

def query_all_systems(self):
    cg = cmdgen.CommandGenerator()
    for system in self.systems.values():
        comm_data = cmdgen.CommunityData('my-manager', system['communityro'])
        transport = cmdgen.UdpTransportTarget(( system['address'], system['port']))
        用于检入 system['checks'].values():
            oid = check['oid']
            errInd, errStatus, errIdx, result = cg.getCmd(comm_data) , transport, oid)
            如果不是 errInd 也不是 errStatus:
                打印 "%s/%s -> %s" % (system['description'],
                                       check['description'],
                                       str(结果[0][1]))

如果您运行该工具,您将获得与这些类似的结果(假设您正确地将配置指向响应 SNMP 查询的工作设备):

$ ./snmp-manager.py
我的笔记本电脑/WLAN 传出流量 -> 1060698
我的笔记本电脑/WLAN 传入流量 -> 14305766

现在我们准备将所有这些数据写入 RRDTool 数据库。

使用 RRDTool 存储数据

RRDTool 是 Tobias Oetiker 开发的应用程序,它已成为图形化监控数据的事实标准。RRDTool 生成的图形用于许多不同的监控工具,例如 Nagios、Cacti 等。在本节中,我们将查看 RRTool 数据库的结构和应用程序本身。我们将讨论循环数据库的细节、如何向其中添加新数据以及如何在稍后检索它。我们还将研究数据绘图命令和技术。最后,我们会将 RRDTool 数据库与我们的应用程序集成。

RRDTool 简介

正如我所指出的,RRDTool 提供了三个不同的功能。首先,它作为一个数据库管理系统,允许您从它自己的数据库格式中存储和检索数据。它还执行复杂的数据操作任务,例如数据重采样和速率计算。最后,它允许您创建包含来自各种源数据库的数据的复杂图形。

让我们从循环数据库结构开始,对于您在本节中遇到的首字母缩略词的数量,我深表歉意,但在这里提及它们很重要,因为它们都用于 RRDTool 的配置中,所以它是熟悉它们至关重要。

使 RRD 与传统数据库不同的第一个属性是数据库的大小有限。这意味着数据库大小在初始化时是已知的,并且大小永远不会改变。新记录会覆盖旧数据,这个过程会一遍又一遍地重复。图 1-3显示了 RRD 的简化版本,以帮助您可视化结构。

图1-3。RRD结构

假设我们已经初始化了一个能够保存 12 条记录的数据库,每条记录都在它自己的单元格中。当数据库为空时,我们首先将数据写入单元格 1。我们还使用我们写入数据的最后一个单元格的 ID 更新指针。图 1-3显示已经将 6 条记录写入数据库(如阴影框所示)。指针在单元格 6 上,因此当接收到下一个写指令时,数据库会将其写入下一个单元格(单元格 7)并相应地更新指针。一旦到达最后一个单元格(单元格 12),该过程将再次从单元格编号 1 开始。

RRD 数据存储的唯一目的是存储性能数据,因此不需要维护不同数据表之间的复杂关系。实际上,RRD 中没有表,只有单个数据源 (DS)。

RRD 的最后一个重要属性是数据库引擎旨在存储时间序列数据,因此每条记录都需要标记时间戳。此外,当您创建新数据库时,您需要指定采样率,即条目写入数据库的速率。默认值为 300 秒或 5 分钟,但如果需要,可以覆盖此值。

存储在 RDD 中的数据称为循环归档 (RRA)。RRA 使 RRD 如此有用。它允许您通过应用可用的合并功能 (CF) 来合并从 DS 收集的数据。您可以指定将应用于许多实际数据记录的四个 CF(average 、min 、max和last )之一。结果存储在循环“表”中。您可以在数据库中以不同的粒度存储多个 RRA。例如,一个 RRA 存储最近 10 条记录的平均值,而另一个 RRA 存储最近 100 条记录的平均值。

当我们查看下一节中的使用场景时,所有这些都会汇总在一起。

在 Python 程序中使用 RRDTool

在我们开始创建 RRDTool 数据库之前,让我们看一下为 RRDTool 提供 API 的 Python 模块。我们将在本章中使用的模块称为 Python RRDTool,可从http://sourceforge.net/projects/py-rrdtool/下载。

但是,大多数 Linux 发行版都已预先打包并可以使用标准包管理工具进行安装。例如,在 Fedora 系统上,您将运行以下命令来安装 Python RRDTool 模块:

$ sudo yum install rrdtool-python

在基于 Debian 的系统上,安装命令是:

$ sudo apt-get install python-rrd

安装包后,您可以验证安装是否成功:

$ python
Python 2.6.2 (r262:71600, Jan 25 2010, 18:46:45)
[GCC 4.4.2 20091222 (Red Hat 4.4.2-20)] on linux2
键入“help”、“copyright”、“credits” ”或“许可证”了解更多信息。
>>> 导入 rrdtool
>>> rrdtool.__version__
'$Revision: 1.14 $'
>>>

创建循环数据库

让我们从创建一个简单的数据库开始。我们将要创建的数据库将有一个数据源,它是一个简单的递增计数器:计数器值随时间增加。这种计数器的一个典型例子是通过接口传输的字节数。每 5 分钟进行一次读数。

我们还将定义两个 RRA。一种是对单个读数求平均值,这有效地指示 RRDTool 存储实际值,另一种是对六个测量值求平均值。以下是用于创建此数据库的命令行工具语法示例:

$ rrdtool 创建interface.rrd \
> DS:packets:COUNTER:600:U:U \
> RRA:AVERAGE:0.5:1:288 \
> RRA:AVERAGE:0.5:6:336

同样,您可以使用 Python 模块创建相同的数据库:

>>> 导入 rrdtool
>>> rrdtool.create( 'interface.rrd',
... 'DS:packets:COUNTER:600:U:U',
... 'RRA :AVERAGE:0.5:1:288' ,
... 'RRA :AVERAGE:0.5:6:336' )
>>>

所述的结构DS (数据源)定义行是:

DS: <名称> : <DS 类型> : <心跳> : <下限> : <上限>

名称 字段是什么,你的名字这个特定的数据源。由于 RRD 允许您存储来自多个数据源的数据,您必须为每个数据源提供一个唯一的名称,以便您以后可以访问它。如果您需要定义多个数据源,只需添加另一行DS 即可。

的DS型(或数据源类型)字段指示什么数据的类型将被提供给该数据源。有四种类型可用:COUNTER 、GAUGE 、DERIVE和ABSOLUTE :

·该COUNTER类型意味着测量值随时间增加。要计算速率,RRDTool 会从当前测量值中减去最后一个值,然后除以测量步长(或采样率)以获得速率值。如果结果为负数,则需要补偿计数器翻转。一个典型的用途是监控不断增加的计数器,例如通过接口传输的字节总数。

·该DERIVE类型类似于COUNTER ,但它也允许阴性率。您可以使用此类型来检查传入您站点的 HTTP 请求的速率。如果图表位于零线上方,则意味着您收到越来越多的请求。如果它低于零线,则意味着您的网站变得不那么受欢迎。

·该ABSOLUTE类型指示计数器复位每次读取测量时间。对于 COUNTER和DERIVE类型,RRDTool 在除以时间段之前从当前测量值中减去最后一个测量值,而ABSOLUTE告诉它不要执行减法运算。您可以在以与您进行测量相同的速率重置的计数器上使用它。例如,您可以每 15 分钟测量一次系统平均负载(过去 15 分钟内)读数。这将代表平均系统负载的变化率。

·该GAUGE型装置,该测量值是速率值,并计算没有需要执行。例如,当前的 CPU 使用率和温度传感器读数很适合GAUGE类型。

心跳 值表示多少时间允许读它重置为之前进来未知状态。RRDTool 允许数据丢失,但它不做任何假设,如果未收到数据,它使用未知的特殊值。在我们的示例中,我们将心跳设置为 600,这意味着数据库在宣布下一个测量值未知之前等待两次读数(请记住,步长为 300)。

最后两个字段指示可以从数据源接收的最小值和最大值。如果您指定这些,则超出该范围的任何内容都将自动标记为unknown 。

RRA 定义结构为:

RRA: <合并函数> : <XFiles factor> : <dataset> : <samples>

集合功能定义了数学函数将被应用到数据集 的值。所述数据集的参数是最后数据集从数据源接收的测量。在我们的示例中,我们有两个 RRA,一个在数据集中只有一个读数,另一个在数据集中有六个测量值。可用的合并函数是AVERAGE 、MIN 、MAX和LAST :

·AVERAGE指示RRDTool 计算数据集的平均值并存储。

·MIN和MAX从数据集中选择最小值或最大值并存储它。

·LAST表示使用数据集中的最后一个条目。

XFiles因子值显示多少百分比的数据集可以有未知的价值观和集合功能的计算仍将进行。例如,如果设置为 0.5 (50%),则六分之三的测量值可能未知,并且仍将计算数据集的平均值。如果错过四个读数,则不执行计算,未知值存储在 RRA 中。将此设置为 0(0% 遗漏容限),并且仅当数据集中的所有数据点都可用时才会执行计算。将此设置保持在 0.5 似乎是一种常见做法。

如前所述,数据集参数指示将有多少记录参与合并函数计算。

最后,样本 告诉 RRDTool 应该保留多少 CF 结果。因此,回到我们的示例,数字 288 告诉 RRDTool 保留 288 条记录。因为我们每 5 分钟测量一次,所以这是 24 小时的数据 (288 /( 60/5))。同样,数字 336 表示我们正在以 30 分钟的采样率存储 7 天的数据 (336 /( 60/30)/24)。如您所见,第二个 RRA 中的数据被重新采样;我们通过整合每六个(5 分钟)样本的数据,将采样率从 5 分钟更改为 30 分钟。

从循环数据库写入和读取数据

将数据写入 RRD 数据文件非常简单。您只需调用update命令,并假设您已经定义了多个数据源,按照您在创建数据库文件时指定的相同顺序向其提供数据源读数列表。每个条目之前必须是当前(或所需)时间戳,以自纪元 (1970-01-01) 以来的秒数表示。或者,您可以使用字符N来表示当前时间,而不是使用实际数字来表示时间戳。可以在一个命令中提供多个读数:

$ date +"%s"
1273008486
$ rrdtool 更新界面.rrd 1273008486:10
$ rrdtool 更新界面.rrd 1273008786:15
$ rrdtool 更新界面.rrd 1273009086:25
$ rrdtool1063067306730673067306730607307
$ rrdtool 更新 interface.rrd 1273010286:100 1273010586:160 1273010886:166

Python 替代品看起来非常相似。在以下代码中,我们将插入另外 20 条记录,指定固定间隔(300 秒)并提供生成的测量值:

>>> 导入 rrdtool
>>> for i in range(20):
... rrdtool.update('interface.rrd',
... '%d:%d' % (1273010886 + (1+i)*300 , i*10+200))
...
>>>

现在让我们从 RRDTool 数据库中取回数据:

$ rrdtool的取interface.rrd平均
                        包

1272983100:-NAN
[...]
1273008600:-NAN
1273008900:2.3000000000e-02
1273009200:3.9666666667e-02
1273009500:5.6333333333e-02
1273009800:4.8933333333e-02
1273010100:5.5466666667e- 02
1273010400:1.4626666667e-01
1273010700:1.3160000000e-01
1273011000:5.5466666667e-02
1273011300:8.2933333333e-02
1273011600:3.3333333333e-02
1273011900:3.3333333333e-02
1273012200:3.3333333333e-02
1273012500:3.3333333333e-02
1273012800 : 3.3333333333e-02
1273013100: 3.3333333333e-02
1273013400: 3.3333333333e-02
1273013700:3.3333333333e-02
1273014000:3.3333333333e-02
1273014300:3.3333333333e-02
1273014600:3.3333333333e-02
1273014900:3.3333333333e-02
1273015200:3.3333333333e-02
1273015500:3.3333333333e-02
1273015800:3.3333333333e-02
1273016100: 3.3333333333e-02
1273016400: 3.3333333333e-02
1273016700: 3.3333333333e-02
1273017000: -nan
[...]
9500: 9500

如果您计算条目数,您会发现它与我们对数据库执行的更新数相匹配。这意味着我们以最大分辨率查看结果——在我们的例子中,每条记录一个样本。以最大分辨率显示结果是默认行为,但您可以通过指定分辨率标志来选择其他分辨率(前提是它具有匹配的 RRA)。请记住,分辨率必须以秒数表示,而不是以 RRA 定义中的样本数表示。因此,在我们的示例中,下一个可用分辨率是 6(样本)* 300(秒/样本)= 1800(秒):

$ rrdtool的取interface.rrd平均-r 1800
                        包

[...]
1273010400:6.1611111111e-02
1273012200:6.1666666667e-02
1273014000:3.3333333333e-02
1273015800:3.3333333333e-02
1273017600:3.3333333333e-02
[... ]

现在,您可能已经注意到,我们的 Python 应用程序插入的记录与存储在数据库中的数字相同。这是为什么?计数器肯定在增加吗?请记住,RRDTool 始终存储速率而不是实际值。因此,您在结果数据集中看到的数字显示了值的变化速度。并且因为 Python 应用程序以稳定的速率生成新的测量值(值之间的差异始终相同),所以速率数字始终相同。

这个数字究竟是什么意思?我们知道每次插入新记录时生成的值都会增加 10,但是fetch打印的值命令是 3.3333333333e-02。(对于许多人来说,这可能看起来有点令人困惑,但这只是值 0.0333(3) 的另一种表示法。)它从何而来?在讨论不同的数据源类型时,我提到 RRDTool 将两个数据点值之间的差异除以采样间隔中的秒数。默认的采样间隔是 300 秒,所以已经计算出速率为 10/300 = 0.0333(3),这就是写入 RRDTool 数据库的内容。换句话说,这意味着我们的计数器平均每秒增加 0.0333(3)。请记住,所有速率测量值都存储为每秒变化。我们将在本节后面介绍如何将此值转换为更具可读性的内容。

以下是使用 Python 模块方法调用检索数据的方法:

>>> for i in rrdtool.fetch('interface.rrd', 'AVERAGE'): 打印 i
...
(1272984300, 1273071000, 300)
('packets',)
[(None,), [...] (无,),(0.023,),(0.03966666666666667,),(0.056333333333333339,),  (0.048933333333333336,),(0.055466666666666671,),(0.14626666666666666),  (0.13160000000000002,),(0.055466666666666671,),(0.082933333333333331,) ,  (0.033333333333333333,),(0.033333333333333333,),(0.033333333333333333,),  (0.033333333333333333,),(0.033333333333333333,),(0.033333333333333333,),  (0.033333333333333333,),(0.033333333333333333,),(0.033333333333333333,),  (0.033333333333333333,) , (0.033333333333333333,), (0.0333333333333333333,),

(0.033333333333333333,),(0.033333333333333333,),(0.033333333333333333,),
 (0.033333333333333333,),(0.033333333333333333,),(0.033333333333333333,),
 (无),[...](无,)]
>>>

结果是一个包含三个元素的元组:数据集信息数据源列表结果数组

·数据集信息是另一个具有三个值的元组:开始和结束时间戳以及采样率。

·数据源列表只是列出了所有存储在 RRDTool 数据库中并由您的查询返回的变量。

·结果数组包含存储在 RRD 中的实际值。每个条目都是一个元组,包含被查询的每个变量的值。在我们的示例数据库中,我们只有一个变量;因此元组只包含一个元素。如果无法计算值(未知),则返回Python 的None对象。

如果需要,您还可以更改采样率:

>>> rrdtool.fetch( 'interface.rrd', 'AVERAGE', '-r', '1800')
((1272983400, 1273071600, 1800), ('packets',), [(None,), [. ..] (无,),
 (0.06161111111111111,), (0.061666666666666668,), (0.033333333333333333,),
 (0.0333333333333,) (0.0333333333333,3),3,33333,3,3333,333333333333333330

注意  现在您应该了解命令行工具语法是如何映射到 Python 模块调用的。您总是调用模块方法,该方法总是以 RRDTool 函数名称命名,例如fetch 、update等。该函数的参数是一个任意值列表。在这种情况下,值是命令行上由空格分隔的任何字符串。基本上,您可以使用命令行并将其作为参数列表复制到函数中。显然,您需要用引号将每个单独的字符串括起来,并用逗号分隔它们。为了节省空间并避免混淆,在进一步的示例中,我将只提供命令行语法,您应该能够很容易地将其映射到 Python 语法。

使用 RRDTool 绘制图形

使用 RRDTool 绘制图形非常简单,绘图是该工具如此受欢迎的原因之一。在最简单的形式中,图形生成命令与数据获取命令非常相似:

$ rrdtool graph packet.png --start 1273008600 --end 1273016400 --step 300\
> DEF:packetrate=interface.rrd:packets:AVERAGE \
> LINE2:packetrate#c0c0c0

即使没有任何额外的修改,结果也是一个非常专业的性能图,如图 1-4 所示。

图1-4。RRDTool 生成的简单图形

首先,我们来看看命令参数。所有绘图命令都以结果图像的文件名和可选的时间刻度值开头。您还可以提供分辨率设置,如果未指定,它将默认为最详细的分辨率。这类似于fetch命令中的-r选项。分辨率以秒表示。

下一行(尽管您可以在一行中键入整个图形命令)是选择器行,它从 RRDTool 数据库中选择数据集。选择器语句的格式为:

DEF: <选择器名称>=< rrd文件>:<数据源>:<合并函数>

选择名称参数是一个任意字符串,您可以使用它的名字所产生的数据集。将其视为存储 RRDTool 数据库结果的数组变量。您可以根据需要使用任意数量的选择器语句,但您需要至少有一个选择器语句才能产生任何输出。

该组合RRD文件数据源集合功能 的变量定义究竟要选择什么样的数据需求。如您所见,此语法将数据存储和数据表示功能完全解耦。您可以将来自不同 RRDTool 数据库的结果包含在同一图表上,并以您喜欢的任何方式组合它们。图表的数据可以在不同的监控服务器上收集,然后组合并呈现在单个图像上。

可以使用可选参数扩展此选择器语句,这些参数指定每个数据源的开始、停止和分辨率值。格式如下,该字符串应附加在选择器语句的末尾。每个元素都是可选的,您可以使用它们的任意组合。

:step =<步长值>:start=<开始时间值>:end=<结束时间值>

所以我们可以将之前的绘图命令改写为:

$ rrdtool图包
.png \ > DEF:packetrate=interface.rrd:packets:AVERAGE:step=300: start=1273008600:end=1273016400 \ > LINE2:packetrate#c0c0c0

命令行上的最后一个元素是告诉 RRDTool 如何绘制数据的语句。数据绘图命令的基本语法是:

<情节类型> : <选择器名称><#color> : <图例>

最广泛使用的绘图类型是LINE和AREA 。的LINE关键字之后可以进行一个浮点数以指示线的宽度。的AREA关键字指示的RRDTool绘制线,并且还填充在x轴和图线之间的区域。

这两个命令后面都是选择器名称,它为绘图函数提供数据。该颜色值被写入作为HTML颜色格式字符串。您还可以指定一个可选参数legend,它告诉 RRDTool 需要在图形底部显示一个匹配颜色的小矩形,然后是图例字符串。

就像使用数据选择器语句一样,您可以根据需要使用任意数量的图形语句,但是您需要至少定义一个来生成图形。

让我们再看一下我们生成的图表。RRDTool 方便地在 x 轴上打印时间戳,但在 y 轴上显示的是什么?它可能看起来像以米为单位的测量值,但实际上m代表“毫”,或值的千分之一。因此,打印在那里的值正是存储在 RRDTool 数据库中的值。然而,这并不直观。我们看不到数据包大小,数据传输率可能非常低或非常高,这取决于传输的数据包大小。让我们假设我们正在处理 4KB 数据包。在这种情况下,逻辑解决方案是将信息表示为每秒位数。我们需要做什么来将每秒的数据包转换为每秒的比特数?因为速率间隔不会改变(在这两种情况下我们都测量每秒的数量),所以只需要乘以数据包值,首先乘以 4096(数据包中的字节数),然后乘以 8(比特数)在一个字节)。

RRDTool图形命令允许定义将应用于任何数据选择器变量的数据转换函数。在我们的示例中,我们将使用以下语句将每秒数据包转换为每秒字节数:

$ rrdtool graph kbps.png --step 300 --start 1273105800 --end 1273114200 \
DEF:packetrate=interface.rrd:packets:AVERAGE \
CDEF:kbps=packetrate,4096,\*,8,\* \
LINE2:kbps #c0c0c0

如果您查看此命令生成的图像,您会发现其形状与图 1-4相同,但 y 轴标签已更改。它们不再表示“毫”值——所有数字都标记为k。这更有意义,因为大多数人看到每秒 3kbps 而不是 100 毫包会更舒服。

注意  您可能想知道为什么计算字符串看起来很奇怪。首先,我必须对 * 字符进行转义,以便将它们传递给rrdtool应用程序,而不会被 shell 处理。公式本身必须用逆波兰表示法编写,其中您指定第一个参数,然后是第二个参数,然后是您要执行的函数。然后可以将结果用作第一个参数。在我的示例中,我有效地告诉应用程序“将数据包速率与 4096 相乘,将结果与 8 相乘”。这需要一些时间来适应,但一旦你得到它的句柄,在RPN表达公式  是真的很容易。

最后,我们需要通过在 y 轴上添加一个标签、我们正在绘制的值的图例以及图形本身的标题来使图形更具表现力。此示例还演示了如何更改生成图像的大小:

$ rrdtool graph packet.png --step 300 --start 1273105800 --end 1273114200 \ --width
500
--height 200 \ --title "主接口" --vertical-label "Kbp/s" \
DEF:packetrate= interface.rrd:packets:AVERAGE\
CDEF:kbps=packetrate,4096,\*,8,\*\
AREA:kbps#c0c0c0:"数据传输率"

结果如图1-5所示。

图1-5。格式化 RRDTool 生成的图形

RRDTool 的介绍仅涵盖了其基本用途。然而,该应用程序附带了一个非常广泛的 API,它允许您更改图形的几乎每个方面。我建议阅读 RRDTool 文档,该文档位于http://oss.oetiker.ch/rrdtool/doc/。

将 RRDTool 与监控解决方案集成

我们现在已准备好将 RRDTool 调用集成到我们的监控应用程序中,以便我们从支持 SNMP 的设备收集的信息被记录下来并随时可用于报告。尽管可以在一个 RRDTool 数据库中维护多个数据源,但建议仅对密切相关的测量进行维护。例如,如果您正在监控一个多处理器系统并希望存储每个 CPU 的中断计数,那么将它们全部存储在一个数据文件中是非常有意义的。相比之下,混合使用内存利用率和温度传感器读数可能不是一个好主意,因为您可能决定需要更高的采样率进行一次测量,并且您无法在不影响其他数据源的情况下轻松更改它。

在我们的系统中,SNMP OID 是在配置文件中提供的,应用程序完全不知道它们是否相关。因此,我们将每个读数存储在一个单独的数据文件中。每个数据文件将获得与检查部分名称相同的名称(例如,check_1.rrd ),因此请确保它们保持唯一。

我们还必须扩展配置文件,以便每个检查定义所需的采样率。最后,每次调用应用程序时,它都会检查数据存储文件是否存在并创建任何丢失的文件。这消除了应用程序用户为每次新检查手动创建文件的负担。您可以在清单1-8 中看到更新后的脚本。

清单 1-8。使用 SNMP 数据更新 RRD

#!/usr/bin/env python

import sys, os.path, time
from ConfigParser import SafeConfigParser
from pysnmp.entity.rfc3413.oneliner import cmdgen
import rrdtool

class SnmpManager:
    def __init__(self):
        self.systems = {}
        self. database_initialised = False

def add_system(self, id, descr, addr, port, comm_ro):
        self.systems[id] = {'description' : descr,
                            'address' : addr,
                            'port' : int(port),
                            'communityro ' : comm_ro,
                            '检查' : {}
                           }

def add_check(self, id, oid, descr, system, sampling_rate):
        oid_tuple = tuple([int(i) for i in oid.split('.')])
        self.systems[system]['checks'][ id] = {'description':descr,
                                              'oid':oid_tuple,
                                              'result':None,
                                              'sampling_rate':sampling_rate
                                             }

def query_all_systems(self):
        如果不是 self.databases_initialised:
            self.initialise_databases()
            self.databases_initialised = True
        cg = cmdgen.CommandGenerator()
        对于 self.systems.values() 中的系统:
            comm_data = cmdgen.CommunityData('my-manager', system['communityro'])
            transport = cmdgen.UdpTransportTarget((system['address'],
            system['port'] ))
            对于密钥,检查 system['checks'].iteritems():
                oid = check['oid']
                errInd, errStatus, errIdx, result = cg.getCmd(comm_data, transport,
                oid)

如果不是 errInd 和不是 errStatus :
                    file_name = "%s.rrd" % key
                    rrdtool.update(file_name,
                                   "%d:%d" % (int(time.time(),),
                                              float(result[0][1]),)
                                  )

def initialise_databases(self):
        for system in self.systems.values():
            for check in system['checks']:
                data_file = "%s.rrd" % check
                如果不是 os.path.isfile(data_file):
                    打印 data_file, '不存在'
                    rrdtool.create(data_file,
                                   "DS:%s:COUNTER:%s:U:U" % (check,
                                    system['checks'] [检查]['sampling_rate']),
                                   "RRA:AVERAGE:0.5:1:288",)

def main(conf_file=""):
    如果不是 conf_file:
        sys.exit(-1)
    config = SafeConfigParser()
    config.read(conf_file)
    snmp_manager = SnmpManager()
    for system in [s for s in config.sections() if s.startswith('system')]:
        snmp_manager.add_system (system,
                                config.get(system, 'description'),
                                config.get(system, 'address'),
                                config.get(system, 'port'),
                                config.get(system, 'communityro'))
    用于签入[c for c in config.sections() if c.startswith('check')]:
        snmp_manager.add_check(check,
                               config.get(check, 'oid'),
                               config.get(check, 'description'),
                               config.get(check, 'system'),
                               config.get(check, 'sampling_rate'))
    snmp_manager.query_all_systems()

if __name__ == '__main__':
    main(conf_file= 'snmp-manager.cfg')

该脚本现在已准备好进行监控。您可以将它添加到 Linux cron 调度程序并每 5 分钟执行一次。如果您配置一些采样率大于 5 分钟的检查,请不要担心;RRDTool 足够聪明,可以以在数据库创建时指定的采样率存储测量值。这是我用来生成示例结果的示例 cronjob 条目,我们将在下一节中使用它:

$ crontab -l
*/5 * * * * (cd /home/rytis/snmp-monitor/; ./snmp-manager.py > log.txt)

使用 Jinja2 模板系统创建网页

在本章的最后一节,我们将创建另一个脚本,这个脚本生成一个包含图表的简单网页结构。主条目页面列出了按系统分组的所有可用支票以及指向支票详细信息页面的链接。当用户导航到该页面时,她将看到 RRDTool 生成的图表以及有关支票本身的一些详细信息(例如支票描述和 OID)。现在,这看起来相对容易实现,大多数人会简单地开始编写一个使用print的 Python 脚本语句来生成 HTML 页面。尽管这种方法看似有效,但在大多数情况下,很快就会变得难以管理。功能代码通常与内容生成代码混合在一起,添加新功能通常会破坏一切,从而导致花费数小时调试应用程序。

此问题的解决方案是使用模板框架之一,该框架允许将应用程序逻辑与表示分离。模板系统的基本原理很简单:您编写代码来执行计算和其他非内容特定的任务,例如从数据库或其他来源检索数据。然后将此信息连同使用此信息的模板的名称传递给模板框架。在模板代码中,您将所有 HTML 格式文本与动态数据(之前生成的)放在一起。然后,框架解析模板以获取简单的处理语句(如迭代循环和逻辑测试语句)并生成结果。您可以在图 1-6 中看到此处理的基本流程。

图1-6。模板框架中的数据流

这样,您的应用程序代码就不会包含所有内容生成语句,并且更易于维护。该模板可以访问呈现给它的所有变量,但它看起来更像是一个 HTML 页面,并且将其加载到 Web 浏览器中通常会产生可接受的结果。因此,您甚至可以请专门的 Web 开发人员为您创建模板,因为无需了解任何 Python 即可修改它们。

我将使用一个名为Jinja的模板框架,它的语法与 Django Web 框架使用的语法非常相似。我们还将在本书中讨论 Django 框架,因此使用类似的模板语言是有意义的。Jinja 框架也被广泛使用,大多数 Linux 发行版都包含 Jinja 包。在 Fedora 系统上,您可以使用以下命令安装它:

$ sudo yum install python-jinja2

或者,您可以使用画中画应用程序来安装它:

$ sudo pip install Jinja2

也可以从官网获取Jinja2框架的最新开发版本:http : //jinja.pocoo.org/。

提示  制作一定要安装Jinja2的,而不是早期版本神社。Jinja2 提供了一种扩展的模板语言,并为其积极开发和支持。

使用 Jinja2 加载模板文件

Jinja2 旨在用于 Web 框架,因此具有非常广泛的 API。它的大部分功能都不会在只生成几页的简单应用程序中使用,因此我将跳过这些功能,因为它们可能成为他们自己的书的主题。在本节中,我将向您展示如何加载模板、向其传递一些变量并保存结果。这三个函数是您在应用程序中大部分时间将使用的函数。有关 Jinja2 API 的更多文档,请参阅http://jinja.pocoo.org/docs/api/。

Jinja2 框架使用所谓的加载器类来加载模板文件。这些可以从各种来源加载,但很可能它们存储在文件系统中。负责加载存储在文件系统上的模板的加载器类称为jinja2.FileSystemLoader 。它接受一个字符串或一系列字符串,这些字符串是可以在其中找到模板文件的文件系统上的路径名:

from jinja2 import FileSystemLoader

loader1 = FileSystemLoader('/path/to/your/templates')
loader2 = FileSystemLoader(['/templates1/', '/teamplates2/']

初始化加载器类后,您可以创建jinja2.Environment类的实例。这个类是框架的核心部分,用于存储配置变量、访问模板(通过加载器实例)以及将变量传递给模板对象。初始化环境时,如果要访问外部存储的模板,则必须传递 loader 对象:

from jinja2 import Environment, FileSystemLoader

loader = FileSystemLoader('/path/to/your/templates')
env = Environment(loader=loader)

创建环境后,您可以加载模板并呈现输出。首先调用get_template方法,该方法返回与模板文件关联的模板对象。接下来调用模板对象的方法render ,该方法处理模板内容(由先前初始化的加载器类加载)。结果是处理后的模板代码,可以写入文件。您必须将所有变量作为字典传递给模板。字典键是模板中可用变量的名称。字典值可以是您想要传递给模板的任何 Python 对象。

from jinja2 import Environment, FileSystemLoader

loader = FileSystemLoader('/path/to/your/templates')
env = Environment(loader=loader)
template = env.get_template('template.tpl')
r_file = open('index.html' , 'w')
name = 'John'
age = 30
result = template.render({'name': name, 'age': age})
r_file.write(result)
r_file.close()

Jinja2 模板语言

Jinja2 模板语言非常广泛且功能丰富。但是,基本概念非常简单,并且该语言与 Python 非常相似。有关完整的语言描述,请查看http://jinja.pocoo.org/2/documentation/templates 上的官方 Jinja2 模板语言定义。

模板语句必须被转义;任何未转义的内容都不会被处理,并将在渲染过程后逐字返回。

有两种类型的语言分隔符:

·变量访问分隔符,表示对变量的引用:{ { ... }}

·语句执行定界符,它告诉框架定界符里面的语句是一个功能指令:{% ... %}

访问变量

正如您已经知道的那样,模板通过作为字典键给出的名称来了解变量。假设传递给渲染函数的字典是这样的:

{'姓名':姓名,'年龄':年龄}

模板中的以下语句可以访问这些变量,如下所示:

{ { 姓名}} / { { 年龄}}

传递给模板的对象可以是任何 Python 对象,模板可以使用相同的 Python 语法访问它。例如,您可以访问字典或数组元素。假设以下渲染调用:

person = {'name': 'John', 'age': 30}
r = t.render({'person': person})

然后您可以使用以下语法访问模板中的字典元素:

{ { person.name }} / { { person.age }}

流控制语句

流控制语句允许您对变量执行检查并选择将相应呈现的模板的不同部分。在生成表或列表等结构时,您还可以使用这些语句来重复模板的一部分。

在为...在循环语句中可以通过这些循环迭代Python对象,每次返回一个元素:

可用产品</h1>
<ul >
{% for item in products %}
  <li>{{ item }}</li>
{% endfor %}
</ul>

进入循环后,将定义以下特殊变量。您可以使用它们来准确检查您在循环中的位置。

表 1-4。循环属性变量

多变的

描述

循环索引

循环的当前迭代。索引从 1 开始;将loop.index0用于从 0 开始索引的计数。

loop.revindex

类似于loop.index ,但从循环结束开始计算迭代次数。

循环优先

如果第一次迭代,则设置为 True。

循环.last

如果最后一次迭代,则设置为 True。

循环长度

序列中元素的总数。

逻辑测试函数if用作布尔检查,类似于 Python if语句的使用:

{% if items %}
  <ul>
  {% for item in items %}
    {% if item.for_sale %}
      <li>{{ item.description }}</li>
    {% endif %}
  {% endfor %}
  < /ul>
{% else %}
  没有项目
{% endif %}

Jinja2 框架还允许模板继承。也就是说,您可以定义一个基本模板并从中继承。每个子模板然后用适当的内容重新定义主模板文件中的块。例如,父模板 ( parent.tpl)可能如下所示:

< head >
  <title> MyCompany – {% block title %}默认标题{% endblock %}</title>
</head>
<html>
{% block content %}
没有内容
{% endblock %}
</html >

子模板然后从基本模板继承并使用自己的内容扩展块:

{% extends 'parent.tpl' %}
{% block title %}My Title{ %endblock %}
{% block content %}
My content %}
{% endblock %}

生成网站页面

生成页面和图像的脚本使用与检查脚本相同的配置文件。它遍历所有系统和检查部分并构建字典树。整个树被传递给索引生成函数,后者又将它传递给索引模板。

每张支票的详细信息由单独的函数生成。相同的函数还调用rrdtool方法来绘制图形。所有文件都保存在网站的根目录中,该目录在全局变量中定义,但可以在函数调用中被否决。您可以在清单1-9 中看到整个脚本。

清单 1-9。生成网站页面

#!/usr/bin/env python

from jinja2 import Environment, FileSystemLoader
from ConfigParser import SafeConfigParser
import rrdtool
import sys

WEBSITE_ROOT = '/home/rytis/public_html/snmp-monitor/'

def generate_index(systems, env, website_root):
    template = env.get_template('index.tpl')
    f = open("%s/index.html" % website_root, 'w')
    f.write(template.render({'systems': systems}))
    f.close( )

def generate_details(system, env, website_root):
    template = env.get_template('details.tpl')
    for check_name, check_obj in system['checks'].iteritems():
        rrdtool.graph ("%s/%s. png" % (website_root, check_name),
                      '--title', "%s" % check_obj['description'],
                      "DEF:data=%(name)s.rrd:%(name)s:AVERAGE" % {'name':
                                                                   check_name},
                      'AREA :data#0c0c0c')
        f = open("%s/%s.html" % (website_root, str(check_name)), 'w')
        f.write(template.render({'check': check_obj, 'name ': check_name}))
        f.close()

def generate_website(conf_file="", website_root=WEBSITE_ROOT):
    如果不是 conf_file:
        sys.exit(-1)
    config = SafeConfigParser()
    config.read(conf_file)
    loader = FileSystemLoader('.')
    env = Environment(loader=loader)
    systems = {}
    for system in [s for s in config.sections() if s.startswith('system')]:
        systems[system] = {'description': config.get(system, 'description'),
                           ' address' : config.get(system, 'address'),
                           'port' : config.get(system, 'port'),
                           'checks' : {}
                          }
    用于检入 [c for c in config.sections() if c.startswith('check')]:
        systems[config.get(check, 'system')]['checks'][check] = {
                                        'oid' : config.get(check, 'oid'),
                                        '描述':config.get(检查,
                                                                 'description'),
                                                                }

generate_index(systems, env, website_root)
    for system in systems.values():
        generate_details(system, env, website_root)

if __name__ == '__main__':
    generate_website(conf_file='snmp-manager.cfg' )

大多数表示逻辑,例如检查是否定义了变量和遍历列表项,都是在模板中实现的。在清单1-10 中,我们首先定义了索引模板,它负责生成index.html页面的内容。如您所知,在此页面中,我们将列出所有已定义的系统以及每个系统可用的完整检查列表。

清单 1-10。索引模板

系统检查</h1>
{% if systems %}
  {% for system in systems %}
    <h2>{{ systems[system].description }}</h2>
    <p>{{ systems[system].address }} :{{ systems[system].port }}</p>
    {% if systems[system].checks %}
      以下检查可用:
      <ul>
        {% for check in systems[system].checks %}
          <li ><a href="{{ check }}.html">
              {{ systems[system].checks[check].description }}</a></li>
        {% endfor %}
      </ul>
    {% else %}
      没有为此系统定义检查
    {% endif %}
  {% endfor %}
{% else %}
  没有可用的系统配置
{% 万一 %}

该模板生成的网页呈现如图1-7所示。

图1-7。浏览器窗口中的索引网页

每个列表项的链接指向一个单独的支票详细信息网页。每个这样的网页都有一个检查部分名称,例如check_1.html 。这些页面是从details.tpl模板生成的:

{ { check.description }}</h1>
<p>OID: { { check.oid }}</p>
<img src="{{ name }}.png" />

该模板链接到由 RRDTool图形方法生成的图形图像。图 1-8显示了结果页面。

图1-8。带图形的 SNMP 详细信息

概括

在本章中,我们构建了一个简单的设备监控系统。通过这样做,您了解了 SNMP,以及与 Python 一起使用的数据收集和绘图库——RRDTool 和 Jinja2 模板系统。要记住的要点:

·大多数网络连接设备使用 SNMP 公开其内部计数器。

·每个这样的计数器都有一个分配给它的专用对象 ID。

·对象 ID 以树状结构组织,其中树枝分配给各种组织。

·RRDTool 是一个允许您存储、检索和绘制网络统计数据的库。

·RRD 数据库是一个循环数据库,这意味着它有一个恒定的大小,新记录插入时将旧记录推出。

·如果您生成网页,请使用 Jinja2 模板系统,它允许您将功能代码与表示分离。

使用 SNMP 读取和收集性能数据相关推荐

  1. 在LoadRunner向远程Linux/Unix执行命令行并收集性能数据

    前面介绍过在LoadRunner的Java协议实现"使用SSH连接Linux",当然连接之后的故事由你主导. 今天要讲的,是一个非Java版本.是对"在LoadRunne ...

  2. check_mk自定义监控增加性能数据图形展示

    在nagios中可以实现性能图形展示,利用的是PNP4Nagios,check_mk当然也可以,而且很简单. 这篇文章在前一篇文章<check_mk自定义监控实践之powershell>的 ...

  3. 可测含多进程的app-- python调用adb命令获取Android App应用的性能数据:CPU、GPU、内存、电池、耗电量(含python源码)

    可测含多进程的app–Python–通过adb命令获取Android App应用的性能数据:CPU.GPU.内存.电池.耗电量,并与Perfdog取值对比结果 1.原理 python脚本通过os.po ...

  4. 服务器性能数据收集,使用 Windows 性能监视器收集数据

    若要监视资源使用量和服务器进程,您可以使用 Windows 服务器附带的 Windows 性能监视器 (PerfMon).使用 PerfMon 来收集详细性能信息,包括 CPU 的使用频率.使用的内存 ...

  5. 【java 性能优化实战】3 工具实践:如何获取代码性能数据?

    首先解答一下上一课时的问题.磁盘的速度这么慢,为什么 Kafka 操作磁盘,吞吐量还能那么高? 这是因为,磁盘之所以慢,主要就是慢在寻道的操作上面.Kafka 官方测试表明,这个寻道时间长达 10ms ...

  6. 读取txt里面的数据进行计算

    双在论坛上找到一个问题,有关读取txt里面的数据进行计算的问题. 尝试解决这个问题,获取每一行的X和Y的浮点数据即可. 读取文本文件每一行,判断是否为空行,是否符以分隔符号(,)分隔的两个数值.每个数 ...

  7. windows下磁盘IO性能数据评测

    windows下如何查看磁盘IO性能 http://www.51testing.com/?uid-211722-action-viewspace-itemid-233892 服务器性能瓶颈如何判断.C ...

  8. linux环境安装nagiosgraph将nagios的性能数据绘制成动态图表?

    需求描述: 在安装完成nagios之后,比如有监控磁盘负载信息的,连接数的,进程数的,可以通过安装nagiosgraph软件, 将nagios的性能数据绘制成图表,可以看到一段时间内数据的变化 环境说 ...

  9. python 速度 memmap_浅析Python 读取图像文件的性能对比

    浅析Python 读取图像文件的性能对比 发布时间:2020-08-30 16:31:06 来源:脚本之家 阅读:57 作者:BriFuture''s Blog 使用 Python 读取一个保存在本地 ...

最新文章

  1. 用自定义代码分析来标准开发人员的开发规范
  2. 官方公布94本预警期刊名单,其中5本高风险
  3. java三层架构是不是策略模式,把「策略模式」应用到实际项目中
  4. Ubuntu 查看操作系统的位数
  5. Java static initialization研究
  6. 关于Fiori应用sandbox JavaScript的两个疑问
  7. java web 应用技术与案例教程_《Java Web应用开发技术与案例教程》怎么样_目录_pdf在线阅读 - 课课家教育...
  8. win10 安装docker流程_Windows10下安装Docker的步骤图文教程
  9. Vue指令_常用vue指令_自定义全局指令_自定义局部指令---vue工作笔记0016
  10. 机器学习的参数正则化
  11. 《Skype for Business Server 2015-项目实战》
  12. android控件的touch事件_Android touch 事件分发时序
  13. 真分布式SolrCloud+Zookeeper+tomcat搭建、索引Mysql数据库、IK中文分词器配置以及web项目中solr的应用(1)
  14. JS 里的数据类型及几个操作
  15. MacOS Big Sur 11.2.1 (20D75) 纯净恢复版黑苹果镜像下载
  16. 涉密专用计算机平台,涉密计算机及移动存储介质保密管理系统(三合一)
  17. 为ramda添加类型
  18. python合并相同内容单元格_快速合并单元格相同项的内容
  19. win 10 PHP开发环境配置
  20. [转载]1986年吴图南 马岳梁 吴英华 孙剑云等名家大師

热门文章

  1. 21天学通Python,从入门到上手,学习方式+学习资料+学习视频汇总,零基础转行自学必备
  2. Android Studio重装之后打开之前的项目后报错(Java Runtime (class file version 55.0), this version of the Java Runt)
  3. sqlserver agent无法启动
  4. linux cron 进程查询,Linux下cron服务
  5. CentOS 7系统安装配置图解教程
  6. 计算机毕业设计(32)java毕设作品之二手交易系统
  7. QQ小程序服务器出错是什么意思,QQ小程序为什么打不开_为什么QQ下拉小程序显示请求失败没有权限_3DM手游...
  8. [常规赛] PALM眼底彩照视盘探测与分割 - 9月第1名方案
  9. node.js接入支付宝小程序的实名认证接口
  10. 基于Android平台的疫情小助手APP