Python 系统管理高级教程(一)
零、简介
这些年来,系统管理员的角色已经大大增加了。单个工程师支持的系统数量也增加了。因此,手工制作每个安装是不切实际的,需要尽可能多地自动化任务。系统的结构因组织而异,因此系统管理员必须能够创建自己的管理工具。历史上,用于这些任务的最流行的编程语言是 UNIX shell 和 Perl。它们很好地实现了自己的目的,我怀疑它们是否会不复存在。然而,当前系统的复杂性需要新的工具,Python 编程语言就是其中之一。
Python 是一种面向对象的编程语言,适合开发大规模应用。它的语法和结构使它非常容易阅读——以至于这种语言有时被称为“可执行伪代码”Python 解释器允许交互式执行,因此在某些情况下,管理员可以使用它来代替标准的 UNIX shell。尽管 Python 主要是一种面向对象的语言,但它很容易被用于过程式和函数式编程。综上所述,Python 非常适合作为实现系统管理应用的新语言。已经有大量的 Linux 系统工具是用 Python 编写的,比如 Yum 包管理器和 Anaconda,Linux 安装程序。
使用这本书的先决条件
这本书是关于使用 Python 编程语言来解决特定的系统管理任务。我们着眼于四个不同的系统管理领域:网络管理、web 服务器和 web 应用管理、数据库系统管理和系统监控。虽然我详细解释了本书中使用的大部分技术,但请记住,这里的主要目标是展示 Python 库的实际应用,以便解决相当具体的问题。因此,我假设您是一位经验丰富的系统管理员。你应该能够自己找到额外的信息;这本书给了你一个粗略的指导,告诉你如何达到你的目标,但是你必须能够想出如何使它适应你的特定系统和环境。
在我们讨论示例时,会要求您安装额外的包和库。在大多数情况下,我提供了在 Fedora 系统上执行这些任务的命令和指令,但是您应该准备好在您将要使用的 Linux 发行版中采用这些指令。大多数例子也可以在最近的 OS X 版本(10.10.X)上运行,不需要太多修改。
我还假设您有 Python 编程语言的背景。我介绍了系统管理任务中使用的特定库,以及一些鲜为人知或很少讨论的语言功能,如生成器函数或类内部方法,但这里没有解释基本的语言语法。如果你想刷新你的 Python 技能,我推荐以下几本书:Marty al chin 和 J. Burton Browning 的 Pro Python(a press,2012;但请关注将于 2015 年初发布的新版本);Mike Dawson 为绝对初学者编写的 Python 编程(课程技术 PTR,2010);以及 Wesley Chun 的核心 Python 应用编程
本书中的所有例子都假设 Python 版本为 2.7。这主要是由示例中使用的库决定的。部分库已经移植到 Python 3;然而,有些人没有。因此,如果您需要运行 Python 3,请确保检查所需的库是否支持 Python 3。
这本书的结构
这本书包含 14 章,每章解决一个独特的问题。有些例子跨越了多个章节,但即使这样,每章都处理特定问题的一个特定方面。
除了这些章节,这本书还有其他几个组织层次。首先,我根据问题类型对章节进行了分组。第一章到第四章处理网络管理问题;第五章到第七章讲的是 Apache web 服务器和 web 应用管理;第八章至 11 章专门用于监控和统计计算;而第十二章和第十三章关注的是数据库管理问题。
第二,我在所有章节中保持一个共同的模式。我从问题陈述开始,然后继续收集需求,在进入实现部分之前继续设计阶段。
第三,每章关注一种或多种技术以及为特定技术提供语言接口的 Python 库。这种技术的例子可以是 SOAP 协议、应用插件架构或云计算概念。
更具体地说,以下是各章节的分类:
第一章:使用 SNMP 读取和收集性能数据
大多数网络连接设备通过简单网络管理协议(SNMP)公开内部计数器。本章解释了 SNMP 的基本原理和数据结构。然后我们看一下 Python 库,这些库为支持 SNMP 的设备提供了接口。我们还研究了循环数据库,它是存储统计数据的事实上的标准。最后,我们看看 Jinja2 模板框架,它允许我们生成简单的 web 页面。
第二章:使用 SOAP API 管理设备
复杂的任务,如管理设备配置,不能通过使用 SNMP 轻易完成,因为该协议过于简单。因此,高级设备(如 Citrix Netscaler 负载平衡器)为设备管理系统提供 SOAP API 接口。在这一章中,我们研究了 SOAP API 结构和支持 Python 编程语言中基于 SOAP 的通信的库。我们还将使用内置的库来查看基本的日志功能。这本书的第二版包含了如何使用新的 REST API 来管理负载平衡器设备的例子。
第三章:创建一个用于 IP 地址统计的 Web 应用
在这一章中,我们将构建一个 web 应用来维护分配的 IP 地址和地址范围的列表。我们学习如何使用 Django 框架创建 web 应用。我将向您展示 Django 应用的构造方式,告诉您如何创建和配置应用设置,并解释 URL 结构。我们还研究了如何使用 Apache web 服务器部署 Django 应用。
第四章:集成 IP 地址应用和 DHCP
本章是对前一章的扩展,我们实现了 DHCP 地址范围支持。我们还研究了一些高级 Django 编程技术,比如定制响应 MIME 类型和服务 AJAX 调用。第二版增加了使用 OMAPI 协议管理动态 DHCP 租约的新功能。
第五章:在 Apache 配置文件中维护虚拟主机列表
这是我们在本书中开发的另一个 Django 应用,但是这次我们的重点是 Django 管理接口。在构建 Apache 配置管理应用时,您学习了如何用自己的视图和函数定制默认的 Django 管理界面。
第六章:从 Apache 日志文件中收集和显示统计数据
在本章中,我们的目标是构建一个解析和分析 Apache web 服务器日志文件的应用。我们没有采用构建单一应用这种简单但不灵活的方法,而是着眼于构建插件应用所涉及的设计原则。您将学习如何使用对象和类类型发现功能,以及如何执行动态模块加载。本书的第二版向您展示了如何基于收集的数据执行数据可视化。
第七章:执行复杂的搜索并报告应用日志文件
本章还涉及日志文件解析,但这次我将向您展示如何解析复杂的多行日志文件条目。我们研究了名为 Exctractor 的开源日志文件解析工具的功能,您可以从exctractor.sourceforge.net/下载。
第八章:Nagios 的网站可用性检查脚本
Nagios 是最流行的开源监控系统之一,因为它的模块化结构允许用户实现自己的检查脚本,从而定制工具来满足他们的需求。在这一章中,我们创建了两个检查网站功能的脚本。我们研究如何使用美汤 HTML 解析库从 HTML 网页中提取信息。
第九章:管理监控子系统
本章是三章系列的开始,在这一系列中,我们将构建一个完整的监控系统。本章的目标不是取代成熟的监控系统,如 Nagios 或 Zenoss,而是展示分布式应用编程的基本原则。我们着眼于数据库设计原则,如数据规范化。我们还研究了如何使用 RPC 调用实现网络服务之间的通信机制。
第十章:远程监控代理
这是监视系列的第二章,我们将实现远程监视代理组件。在本章中,我还描述了如何使用 ConfigParser 模块将应用从其配置中分离出来。
第十一章:统计数据收集和报告
这是监控系列的最后一部分,我将向您展示如何对收集的性能数据执行基本的统计分析。我们使用科学库:NumPy 执行计算,matplotlib 创建图形。您将学习如何发现哪些性能读数属于舒适区,以及如何计算该区域的边界。我们还进行基本的趋势检测,这为容量规划提供了很好的洞察力。
第十二章:分布式消息处理系统
这是本书第二版的新章节。在这一章中,我将向您展示如何使用远程任务执行框架 Celery 来转换分布式管理系统。
第十三章【MySQL 数据库性能自动调优
在本章中,我将向您展示如何获取 MySQL 数据库的配置变量和内部状态指示器。我们构建了一个应用,该应用根据获得的数据就如何提高数据库引擎性能提出建议。
第十四章:亚马逊 EC2/S3 作为数据仓库解决方案
本章向您展示了如何利用 Amazon 弹性计算云(EC2)并将不常见的计算任务卸载给它。我们构建了一个自动创建数据库服务器的应用,您可以在其中传输数据以供进一步分析。您可以使用这个示例作为构建随需应变数据仓库解决方案的基础。
示例源代码
本书中所有示例的源代码,以及任何适用的样本数据,都可以按照www.apress.com/source-code…的说明从 Apress 网站下载。存储在该位置的源代码包含书中描述的相同代码。
本书中描述的大多数原型也可以作为开源项目获得。你可以在作者的网站找到这些项目,www.sysadminpy.com/。
一、使用 SNMP 读取和收集性能数据
大多数连接到网络的设备使用 SNMP(简单网络管理协议)报告它们的状态。该协议主要是为管理和监控网络连接的硬件设备而设计的,但是一些应用也使用该协议来公开它们的统计数据。在本章中,我们将探讨如何从 Python 应用中访问这些信息。我们将使用 RRDTool 将获得的数据存储在 RRD(循环数据库)中,RRDTool 是一个广为人知的流行应用和库,用于存储和绘制性能数据。最后,我们将研究 Jinja2 模板系统,我们将使用它为我们的应用生成简单的 web 页面。
应用需求和设计
系统监控的主题非常广泛,通常包含许多不同的领域。一个完整的监控系统相当复杂,通常由多个部件协同工作组成。我们不打算在这里开发一个完整的、自给自足的系统,但是我们将研究典型监控系统的两个重要方面:信息收集和表示。在本章中,我们将实现一个系统,该系统使用 SNMP 协议查询设备,然后使用 RRDTool 库存储数据,该库也用于生成可视化数据表示的图形。所有这些都使用 Jinja2 模板库绑定到简单的 web 页面中。在本章中,我们将更详细地了解这些组件。
详细说明要求
在开始设计我们的应用之前,我们需要对我们的系统提出一些要求。首先,我们需要理解我们期望我们的系统提供的功能。这将帮助我们创建一个有效的(我们希望易于实现的)系统设计。在本章中,我们将创建一个使用 SNMP 协议监控网络连接设备(如网络交换机和路由器)的系统。因此,第一个要求是系统能够使用 SNMP 查询任何设备。
需要存储从设备收集的信息,以供将来参考和分析。让我们对这些信息的使用做一些假设。首先,我们不需要无限期地存储它。(我将在第 9-11 章中详细讨论永久信息存储。)这意味着信息只存储预定义的一段时间,一旦过时就会被擦除。这提出了我们的第二个需求:信息在“过期”后需要被删除
第二,需要存储信息,以便生成图表。我们不会将它用于任何其他用途,因此数据存储应该针对数据表示任务进行优化。
最后,我们需要生成图表,并在易于访问的网页上显示这些信息。该信息只需要由设备名称构成。例如,如果我们正在监控几个设备的 CPU 和网络接口利用率,那么这些信息需要显示在一个页面上。我们不需要在多个时间尺度上呈现这些信息;默认情况下,图表应该显示过去 24 小时的性能指标。
高级设计规范
现在我们已经对系统的功能有了一些想法,让我们创建一个简单的设计,我们将使用它作为开发阶段的指南。基本的方法是,我们之前指定的每个需求都应该被一个或多个设计决策所覆盖。
第一个要求是我们需要监控网络连接设备,我们需要使用 SNMP 来实现。这意味着我们必须使用适当的 Python 库来处理 SNMP 对象。SNMP 模块不包含在默认的 Python 安装中,所以我们必须使用一个外部模块。我推荐使用 PySNMP 库(可以在pysnmp.sourceforge.net/获得),它可以在大多数流行的 Linux 发行版上获得。
数据存储引擎的完美候选是 RRDTool (在oss.oetiker.ch/rrdtool/有售)。循环数据库意味着数据库的结构是这样的,每个“表”都有一个有限的长度,一旦达到限制,最旧的条目就会被丢弃。事实上,它们没有被丢弃;新的只是被写入它们的位置。
RRDTool 库提供了两种不同的功能:数据库服务和图形生成工具包。Python 中没有对 RRD 数据库的本地支持,但是有一个外部库提供了到 RRDTool 库的接口。
最后,为了生成网页,我们将使用 Jinja2 模板库(可在jinja.pocoo.org或 GitHub: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 的例子。分配给思科组织的 OID 树节点的值为 1.3.6.1.4.1.9,这意味着所有与思科制造的设备相关联的专有 oid 将以这些数字开头。同样,Novell 设备的 oid 将从 1.3.6.1.4.1.23 开始。
我特意强调了专有 oid,因为有些属性预计会出现在所有设备上(如果有的话)。这些在 RFC1213 定义的 1.3.6.1.2.1.1(系统 SNMP 变量)节点下。欲了解更多关于 OID 树及其元素的细节,请访问 www.alvestrand.no/objectid/to… OID 树,它包含了相当大的各种 oid 的集合。
系统 SNMP 变量节点
在大多数情况下,关于设备的基本信息可以在系统 SNMP 变量 OID 节点下的子树中找到。因此,让我们仔细看看你能在那里找到什么。
这个 OID 节点包含几个额外的 OID 节点。表 1-1 提供了大多数子节点的描述。
表 1-1 。系统 SNMP OIDs
|
OID 字符串
|
目录名称
|
描述
| | --- | --- | --- | | 1.3.6.1.2.1.1.1 | 系统描述 | 包含系统或设备简短描述的字符串。通常包含硬件类型和操作系统的详细信息。 | | 1.3.6.1.2.1.1.2 | sysObjectID | 包含特定于供应商的设备 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 变量节点
类似地,基本的接口统计数据可以从接口 SNMP 变量 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 OIDs
|
OID 字符串
|
目录名称
|
描述
| | --- | --- | --- | | 1.3.6.1.2.1.2.2.1.1 | 如果索引 | 分配给接口的唯一序列号。 | | 1.3.6.1.2.1.2.2.1.2 | ifDescr | 包含接口名称和其他可用信息的字符串,如硬件制造商的名称。 | | 1.3.6.1.2.1.2.2.1.3 | ifType | 代表接口类型的数字,取决于接口的物理链路和协议。 | | 1.3.6.1.2.1.2.2.1.4 | ifMtu | 这个接口可以传输的最大网络数据报。 | | 1.3.6.1.2.1.2.2.1.5 | 芯倍速 | 接口的估计当前带宽。如果无法计算当前带宽,该数字应包含接口的最大可能带宽。 | | 1.3.6.1.2.1.2.2.1.6 | ifPhysAddress | 接口的物理地址,通常是以太网接口上的 MAC 地址。 | | 1.3.6.1.2.1.2.2.1.7 | ifAdminStatus | 该 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 | iflastschange | 当接口进入当前状态时,包含系统运行时间(sysUpTime)读数的值。如果接口在最后一次系统重新初始化之前进入此状态,可以设置为零。 | | 1.3.6.1.2.1.2.2.1.10 | 菲诺特人 | 接口上接收的字节(八位字节)总数。 | | 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 | ifInErrors | 收到的无效数据包的数量。 | | 1.3.6.1.2.1.2.2.1.15 | ifinunnknownprotos | 由于设备接口不支持该协议而丢弃的数据包数量。 | | 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 | 如果丢弃 | 被丢弃的有效数据包的数量。这不是一个错误,但它可能表明发送缓冲区太小,无法接受所有数据包。 | | 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.3.6.1.2.1.1.1.0 = STRING: Linux fedolin.example.com 2.6.32.11-99.fc12.i686 #1 SMP Mon Apr 5 16:32:08 EDT 2010 i686
.1.3.6.1.2.1.1.2.0 = OID: .1.3.6.1.4.1.8072.3.2.10
.1.3.6.1.2.1.1.3.0 = Timeticks: (110723) 0:18:27.23
.1.3.6.1.2.1.1.4.0 = STRING: Administrator (admin@example.com)
.1.3.6.1.2.1.1.5.0 = STRING: fedolin.example.com
.1.3.6.1.2.1.1.6.0 = STRING: MyLocation, MyOrganization, MyStreet, MyCity, MyCountry
.1.3.6.1.2.1.1.8.0 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.1.9.1.2.1 = OID: .1.3.6.1.6.3.10.3.1.1
.1.3.6.1.2.1.1.9.1.2.2 = OID: .1.3.6.1.6.3.11.3.1.1
.1.3.6.1.2.1.1.9.1.2.3 = OID: .1.3.6.1.6.3.15.2.1.1
.1.3.6.1.2.1.1.9.1.2.4 = OID: .1.3.6.1.6.3.1
.1.3.6.1.2.1.1.9.1.2.5 = OID: .1.3.6.1.2.1.49
.1.3.6.1.2.1.1.9.1.2.6 = OID: .1.3.6.1.2.1.4
.1.3.6.1.2.1.1.9.1.2.7 = OID: .1.3.6.1.2.1.50
.1.3.6.1.2.1.1.9.1.2.8 = OID: .1.3.6.1.6.3.16.2.2.1
.1.3.6.1.2.1.1.9.1.3.1 = STRING: The SNMP Management Architecture MIB.
.1.3.6.1.2.1.1.9.1.3.2 = STRING: The MIB for Message Processing and Dispatching.
.1.3.6.1.2.1.1.9.1.3.3 = STRING: The management information definitions for the SNMP User-based Security Model.
.1.3.6.1.2.1.1.9.1.3.4 = STRING: The MIB module for SNMPv2 entities
.1.3.6.1.2.1.1.9.1.3.5 = STRING: The MIB module for managing TCP implementations
.1.3.6.1.2.1.1.9.1.3.6 = STRING: The MIB module for managing IP and ICMP implementations
.1.3.6.1.2.1.1.9.1.3.7 = STRING: The MIB module for managing UDP implementations
.1.3.6.1.2.1.1.9.1.3.8 = STRING: View-based Access Control Model for SNMP.
.1.3.6.1.2.1.1.9.1.4.1 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.1.9.1.4.2 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.1.9.1.4.3 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.1.9.1.4.4 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.1.9.1.4.5 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.1.9.1.4.6 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.1.9.1.4.7 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.1.9.1.4.8 = Timeticks: (3) 0:00:00.03
.1.3.6.1.2.1.2.1.0 = INTEGER: 5
.1.3.6.1.2.1.2.2.1.1.1 = INTEGER: 1
.1.3.6.1.2.1.2.2.1.1.2 = INTEGER: 2
.1.3.6.1.2.1.2.2.1.1.3 = INTEGER: 3
.1.3.6.1.2.1.2.2.1.1.4 = INTEGER: 4
.1.3.6.1.2.1.2.2.1.1.5 = INTEGER: 5
.1.3.6.1.2.1.2.2.1.2.1 = STRING: lo
.1.3.6.1.2.1.2.2.1.2.2 = STRING: eth0
.1.3.6.1.2.1.2.2.1.2.3 = STRING: wlan1
.1.3.6.1.2.1.2.2.1.2.4 = STRING: pan0
.1.3.6.1.2.1.2.2.1.2.5 = STRING: virbr0
.1.3.6.1.2.1.2.2.1.3.1 = INTEGER: softwareLoopback(24)
.1.3.6.1.2.1.2.2.1.3.2 = INTEGER: ethernetCsmacd(6)
.1.3.6.1.2.1.2.2.1.3.3 = INTEGER: ethernetCsmacd(6)
.1.3.6.1.2.1.2.2.1.3.4 = INTEGER: ethernetCsmacd(6)
.1.3.6.1.2.1.2.2.1.3.5 = INTEGER: ethernetCsmacd(6)
.1.3.6.1.2.1.2.2.1.4.1 = INTEGER: 16436
.1.3.6.1.2.1.2.2.1.4.2 = INTEGER: 1500
.1.3.6.1.2.1.2.2.1.4.3 = INTEGER: 1500
.1.3.6.1.2.1.2.2.1.4.4 = INTEGER: 1500
.1.3.6.1.2.1.2.2.1.4.5 = INTEGER: 1500
.1.3.6.1.2.1.2.2.1.5.1 = Gauge32: 10000000
.1.3.6.1.2.1.2.2.1.5.2 = Gauge32: 0
.1.3.6.1.2.1.2.2.1.5.3 = Gauge32: 10000000
.1.3.6.1.2.1.2.2.1.5.4 = Gauge32: 10000000
.1.3.6.1.2.1.2.2.1.5.5 = Gauge32: 10000000
.1.3.6.1.2.1.2.2.1.6.1 = STRING:
.1.3.6.1.2.1.2.2.1.6.2 = STRING: 0:d:56:7d:68:b0
.1.3.6.1.2.1.2.2.1.6.3 = STRING: 0:90:4b:64:7b:4d
.1.3.6.1.2.1.2.2.1.6.4 = STRING: 4e:e:b8:9:81:3b
.1.3.6.1.2.1.2.2.1.6.5 = STRING: d6:f9:7c:2c:17:28
.1.3.6.1.2.1.2.2.1.7.1 = INTEGER: up(1)
.1.3.6.1.2.1.2.2.1.7.2 = INTEGER: up(1)
.1.3.6.1.2.1.2.2.1.7.3 = INTEGER: up(1)
.1.3.6.1.2.1.2.2.1.7.4 = INTEGER: down(2)
.1.3.6.1.2.1.2.2.1.7.5 = INTEGER: up(1)
.1.3.6.1.2.1.2.2.1.8.1 = INTEGER: up(1)
.1.3.6.1.2.1.2.2.1.8.2 = INTEGER: down(2)
.1.3.6.1.2.1.2.2.1.8.3 = INTEGER: up(1)
.1.3.6.1.2.1.2.2.1.8.4 = INTEGER: down(2)
.1.3.6.1.2.1.2.2.1.8.5 = INTEGER: up(1)
.1.3.6.1.2.1.2.2.1.9.1 = Timeticks: (0) 0:00:00.00
.1.3.6.1.2.1.2.2.1.9.2 = Timeticks: (0) 0:00:00.00
.1.3.6.1.2.1.2.2.1.9.3 = Timeticks: (0) 0:00:00.00
.1.3.6.1.2.1.2.2.1.9.4 = Timeticks: (0) 0:00:00.00
.1.3.6.1.2.1.2.2.1.9.5 = Timeticks: (0) 0:00:00.00
.1.3.6.1.2.1.2.2.1.10.1 = Counter32: 89275
.1.3.6.1.2.1.2.2.1.10.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.10.3 = Counter32: 11649462
.1.3.6.1.2.1.2.2.1.10.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.10.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.11.1 = Counter32: 1092
.1.3.6.1.2.1.2.2.1.11.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.11.3 = Counter32: 49636
.1.3.6.1.2.1.2.2.1.11.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.11.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.12.1 = Counter32: 0
.1.3.6.1.2.1.2.2.1.12.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.12.3 = Counter32: 0
.1.3.6.1.2.1.2.2.1.12.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.12.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.13.1 = Counter32: 0
.1.3.6.1.2.1.2.2.1.13.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.13.3 = Counter32: 0
.1.3.6.1.2.1.2.2.1.13.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.13.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.14.1 = Counter32: 0
.1.3.6.1.2.1.2.2.1.14.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.14.3 = Counter32: 0
.1.3.6.1.2.1.2.2.1.14.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.14.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.15.1 = Counter32: 0
.1.3.6.1.2.1.2.2.1.15.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.15.3 = Counter32: 0
.1.3.6.1.2.1.2.2.1.15.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.15.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.16.1 = Counter32: 89275
.1.3.6.1.2.1.2.2.1.16.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.16.3 = Counter32: 922277
.1.3.6.1.2.1.2.2.1.16.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.16.5 = Counter32: 3648
.1.3.6.1.2.1.2.2.1.17.1 = Counter32: 1092
.1.3.6.1.2.1.2.2.1.17.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.17.3 = Counter32: 7540
.1.3.6.1.2.1.2.2.1.17.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.17.5 = Counter32: 17
.1.3.6.1.2.1.2.2.1.18.1 = Counter32: 0
.1.3.6.1.2.1.2.2.1.18.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.18.3 = Counter32: 0
.1.3.6.1.2.1.2.2.1.18.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.18.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.19.1 = Counter32: 0
.1.3.6.1.2.1.2.2.1.19.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.19.3 = Counter32: 0
.1.3.6.1.2.1.2.2.1.19.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.19.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.20.1 = Counter32: 0
.1.3.6.1.2.1.2.2.1.20.2 = Counter32: 0
.1.3.6.1.2.1.2.2.1.20.3 = Counter32: 0
.1.3.6.1.2.1.2.2.1.20.4 = Counter32: 0
.1.3.6.1.2.1.2.2.1.20.5 = Counter32: 0
.1.3.6.1.2.1.2.2.1.21.1 = Gauge32: 0
.1.3.6.1.2.1.2.2.1.21.2 = Gauge32: 0
.1.3.6.1.2.1.2.2.1.21.3 = Gauge32: 0
.1.3.6.1.2.1.2.2.1.21.4 = Gauge32: 0
.1.3.6.1.2.1.2.2.1.21.5 = Gauge32: 0
.1.3.6.1.2.1.2.2.1.22.1 = OID: .0.0
.1.3.6.1.2.1.2.2.1.22.2 = OID: .0.0
.1.3.6.1.2.1.2.2.1.22.3 = OID: .0.0
.1.3.6.1.2.1.2.2.1.22.4 = OID: .0.0
.1.3.6.1.2.1.2.2.1.22.5 = OID: .0.0
.1.3.6.1.2.1.25.1.1.0 = Timeticks: (8232423) 22:52:04.23
.1.3.6.1.2.1.25.1.1.0 = No more variables left in this MIB View (It is past the end of the MIB tree)
作为练习,尝试使用表 1-1 和 1-2 识别一些列出的 oid,并找出它们的含义。
从 Python 查询 SNMP 设备
现在,我们已经对 SNMP 有了足够的了解,可以开始在我们自己的管理系统上工作了,该系统将定期查询已配置的系统。首先,让我们指定我们将在应用中使用的配置。
配置应用
正如我们已经知道的,我们需要每次检查可用的以下信息:
- 运行 SNMP 代理软件的系统的 IP 地址或可解析域名
- 将用于通过代理软件进行身份验证的只读社区字符串
- OID 节点的数值表示
因为简单,我们将使用 Windows INI 风格的配置文件。Python 默认包含一个配置解析模块,所以使用起来也很方便。(第九章非常详细的讨论了 ConfigParser 模块;有关模块的更多信息,请参考该章。)
让我们回到应用的配置文件。没有必要为我们将要查询的每个 SNMP 对象重复系统信息,所以我们可以在单独的部分定义每个系统参数一次,然后在每个检查部分引用系统 ID。check 部分定义了 OID 节点标识符字符串和简短描述,如清单 1-2 所示。使用下面列表中的内容创建一个名为 snmp-manage.cfg 的配置文件;不要忘记相应地修改 IP 和安全细节。
清单 1-2 。M 管理系统配置文件
[system_1]
description=My Laptop
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
[check_2]
description=WLAN incoming traffic
oid=1.3.6.1.2.1.2.2.1.16.3
system=system_1
确保系统和检查部分 id 是唯一的,否则可能会得到不可预知的结果。
我们将用两个方法创建一个 SnmpManager 类,一个用于添加系统,另一个用于添加检查。由于支票包含系统 ID 字符串,它将自动分配给该特定系统。在清单 1-3 中,你可以看到类定义和初始化部分,它读入配置,遍历各个部分,并相应地更新类对象。创建一个名为 snmp-manage.py 的文件,其内容如下所示。我们将继续在脚本中添加新的特性。
清单 1-3 。读取和存储配置
import sys
from ConfigParser import SafeConfigParser
class 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 = 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'))
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'))
if __name__ == '__main__':
main(conf_file='snmp-manager.cfg')
正如您在示例中看到的,在继续检查部分之前,我们首先必须遍历系统部分并更新对象。
注意这个顺序很重要,因为如果我们试图为一个还没有插入的系统添加检查,我们会得到一个字典索引错误。
还要注意,我们将 OID 字符串转换为整数元组。在本节的后面,您将会看到为什么我们必须这样做。配置文件已加载,我们准备好对已配置的设备运行 SNMP 查询。
使用 PySNMP 库
在这个项目中,我们将使用 PySNMP 库,它是用纯 Python 实现的,不依赖于任何预编译的库。pysnmp 包适用于大多数 Linux 发行版,可以使用标准发行版包管理器安装。除了 pysnmp,您还需要 ASN.1 库,它由 pysnmp 使用,也可以作为 Linux 发行包的一部分获得。例如,在 Fedora 系统上,您可以使用以下命令安装 pysnmp 模块:
$ sudo yum install pysnmp
$ sudo yum install python-pyasn1
或者,您可以使用 Python 包管理器(PiP) 来安装这个库:
$ sudo pip install pysnmp
$ sudo pip install pyasn1
如果你没有可用的 pip 命令,你可以从pypi.python.org/pypi/pip下载并安装这个工具。我们也会在后面的章节中用到它。
PySNMP 库将 SNMP 处理的所有复杂性隐藏在具有简单 API 的单个类后面。您所要做的就是创建一个 CommandGenerator 类的实例。该类可从 py snmp . entity . RFC 3413 . oneliner . cmdgen 模块中获得,并实现大多数标准 SNMP 协议命令:getCmd()、setCmd()和 nextCmd()。让我们更详细地看一下每一项。
SNMP GET 命令
我们将要讨论的所有命令都遵循相同的调用模式:导入模块,创建 CommandGenerator 类的实例,创建三个必需的参数(身份验证对象、传输目标对象和参数列表),最后调用适当的方法。该方法返回一个包含错误指示符(如果有错误)和结果对象的元组。
在清单 1-4 中,我们使用标准 SNMP OID(1 . 3 . 6 . 1 . 2 . 1 . 1 . 0)查询一台远程 Linux 机器。
清单 1-4 。SNMP GET 命令的一个例子
>>> 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, 1, 0)
>>> errIndication, errStatus, errIndex, result = cg.getCmd(comm_data, transport, variables)
>>> print errIndication
None
>>> print errStatus
0
>>> print errIndex
0
>>> print 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'))]
>>>
让我们更仔细地看一些步骤。当我们启动社区数据对象时,我们提供了两个字符串——社区字符串(第二个参数)和代理或管理器安全名称字符串;在大多数情况下,这可以是任何字符串。可选参数指定要使用的 SNMP 版本(默认为 SNMP v2c)。如果您必须查询版本 1 设备,请使用以下命令:
>>> comm_data = cmdgen.CommunityData('my-manager', 'public', mpModel=0)
传输对象由包含完全限定的域名或 IP 地址字符串和整数端口号的元组初始化。
最后一个参数是 OID,表示为所有节点 ID 的元组,这些节点 ID 组成了我们正在查询的 OID。因此,当我们读取配置项时,我们必须将点分隔的字符串转换为元组。
最后,我们调用 API 命令 getCmd(),它实现了 SNMP GET 命令,并传递这三个对象作为它的参数。该命令返回一个元组,元组的每个元素在表 1-3 中描述。
表 1-3 。CommandGenerator 返回对象
|
元组元素
|
描述
| | --- | --- | | 根除 | 如果该字符串不为空,则表明 SNMP 引擎出错。 | | 错误状态 | 如果此元素评估为真,则表明 SNMP 通信中有错误;产生错误的对象由 errIndex 元素指示。 | | ehrindex | 如果 errStatus 指示发生了错误,则该字段可用于查找导致错误的 SNMP 对象。结果数组中的对象位置是 errIndex-1。 | | 结果 | 此元素包含所有返回的 SNMP 对象元素的列表。每个元素都是一个包含对象名和对象值的元组。 |
SNMP SET 命令
SNMP SET 命令在 PySNMP 中被映射到 setCmd()方法调用。所有参数都相同;唯一的区别是 variables 部分现在包含一个元组: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')
>>> transport = cmdgen.UdpTransportTarget(('192.168.1.68', 161))
>>> variables = ((1, 3, 6, 1, 2, 1, 1, 1, 0), rfc1902.OctetString('new system description'))
>>> errIndication, errStatus, errIndex, result = cg.setCmd(comm_data, transport,
variables)
>>> print errIndication
None
>>> print errStatus
6
>>> print errIndex
1
>>> print errStatus.prettyPrint()
noAccess(6)
>>> print result
[(ObjectName('1.3.6.1.2.1.1.1.0'), OctetString('new system description'))]
>>>
这里发生的情况是,我们试图写入一个只读对象,这导致了一个错误。这个例子中有趣的是我们如何格式化参数。你必须把字符串转换成 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, variables)
>>> print errIndication
requestTimedOut
>>> errIndication, errStatus, errIndex, result = cg.nextCmd(comm_data, transport, variables)
>>> print errIndication
None
>>> print errStatus
0
>>> print errIndex
0
>>> for object in result:
... print object
...
[(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('The SNMP Management Architecture MIB.'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.3.2'), OctetString('The MIB for Message Processing and Dispatching.'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.3.3'), OctetString('The management information
definitions for the SNMP User-based Security Model.'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.3.4'), OctetString('The MIB module for SNMPv2 entities'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.3.5'), OctetString('The MIB module for managing TCP
implementations'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.3.6'), OctetString('The MIB module for managing IP
and ICMP implementations'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.3.7'), OctetString('The MIB module for managing UDP
implementations'))]
[(ObjectName('1.3.6.1.2.1.1.9.1.3.8'), OctetString('View-based Access Control Model for 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']))
for check in system['checks'].values():
oid = check['oid']
errInd, errStatus, errIdx, result = cg.getCmd(comm_data, transport, oid)
if not errInd and not errStatus:
print "%s/%s -> %s" % (system['description'],
check['description'],
str(result[0][1]))
如果您运行该工具,您将得到类似于以下的结果(假设您正确地将您的配置指向响应 SNMP 查询的工作设备):
$ ./snmp-manager.py
My Laptop/WLAN outgoing traffic -> 1060698
My Laptop/WLAN incoming traffic -> 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 中没有表,只有单独的数据源(DSs) 。
RRD 的最后一个重要属性是数据库引擎被设计用来存储时间序列数据,因此每条记录都需要标记时间戳。此外,当您创建一个新的数据库时,您需要指定采样率,即条目被写入数据库的速率。默认值为 300 秒或 5 分钟,但如果需要,可以覆盖该值。
存储在 RDD 中的数据称为循环档案(RRA) 。RRA 是 RRD 如此有用的原因。它允许您通过应用可用的合并功能(CF) 来合并从 DS 收集的数据。您可以指定四个 CFs(平均值、最小值、最大值和最后值)中的一个,应用于许多实际数据记录。结果存储在循环“表”中您可以在数据库中以不同的粒度存储多个 rra。例如,一个 RRA 存储最后 10 条记录的平均值,另一个存储最后 100 条记录的平均值。
当我们在接下来的部分中查看使用场景时,这些都会一起出现。
在 Python 程序中使用 RRDTool
在我们开始创建 RRDTool 数据库之前,让我们看看为 RRDTool 提供 API 的 Python 模块。我们在这一章将要使用的模块叫做 Python RRDTool,可以在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
Type "help", "copyright", "credits" or "license" for more information.
>>> import rrdtool
>>> rrdtool.__version__
'$Revision: 1.14 $'
>>>
创建循环数据库
让我们从创建一个简单的数据库开始。我们将要创建的数据库将有一个数据源,这是一个简单的递增计数器:计数器值随着时间的推移而增加。这种计数器的典型例子是通过接口传输的字节。读数每 5 分钟进行一次。
我们还将定义两个 rra。一种是对单个读数求平均值,这有效地指示 RRDTool 存储实际值,另一种是对六次测量求平均值。以下是创建此数据库的命令行工具语法示例:
$ rrdtool create interface.rrd \
> DS:packets:COUNTER:600:U:U \
> RRA:AVERAGE:0.5:1:288 \
> RRA:AVERAGE:0.5:6:336
类似地,您可以使用 Python 模块来创建相同的数据库:
>>> import 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:*<name>*:*<DS type>*:*<heartbeat>*:*<lower limit>*:*<upper limit>*
名称字段是您命名这个特定数据源的名称。由于 RRD 允许您存储来自多个数据源的数据,因此您必须为每个数据源提供一个唯一的名称,以便以后访问。如果您需要定义多个数据源,只需添加另一个 DS 行。
DS type(或 data source type)字段指示将向该数据源提供什么类型的数据。有四种类型可用:计数器、仪表、衍生和绝对:
- 计数器类型表示测量值随时间增加。为了计算速率,RRDTool 从当前测量值中减去最后一个值,然后除以测量步长(或采样速率)以获得速率数值。如果结果是负数,它需要补偿计数器翻转。典型用途是监控不断增加的计数器,例如通过接口传输的总字节数。
- 派生类型类似于 COUNTER,但是它也允许负速率。您可以使用此类型来检查对您的站点的传入 HTTP 请求的速率。如果图表在零线以上,这意味着你收到越来越多的请求。如果它低于零线,这意味着你的网站变得不那么受欢迎。
- 绝对类型表示每次读取测量值时计数器都会复位。而对于计数器和派生类型,RRDTool 在除以时间段之前从当前测量值中减去上一次测量值,ABSOLUTE 告诉它不要执行减法运算。您可以在计数器上使用它,计数器的重置速率与您进行测量的速率相同。例如,您可以每 15 分钟测量一次系统平均负载(过去 15 分钟内)读数。这将代表平均系统负载的变化率。
- 仪表类型表示测量的是速率值,不需要进行计算。例如,当前 CPU 使用率和温度传感器读数是仪表类型的良好候选。
心跳值表示在将其重置为未知状态之前,允许读数进入的时间。RRDTool 允许数据丢失,但它不做任何假设,如果没有收到数据,它使用特殊值 unknown。在我们的示例中,我们将心跳设置为 600,这意味着数据库在声明下一个测量未知之前,要等待两次读取(记住,步长是 300)。
最后两个字段指示可以从数据源接收的最小值和最大值。如果您指定了这些,超出该范围的任何内容都将被自动标记为未知。
RRA 定义结构是:
RRA:*<consolidation function>*:*<XFiles factor>*:*<dataset>*:*<samples>*
合并函数定义了将对数据集的值应用什么数学函数。数据集参数是从数据源接收的最后一个数据集测量值。在我们的示例中,我们有两个 rra,一个在数据集中只有一个读数,另一个在数据集中有六个测量值。可用的合并函数有平均值、最小值、最大值和最后值:
- AVERAGE 指示 RRDTool 计算数据集的平均值并存储它。
- 最小值和最大值从数据集中选择最小值或最大值并存储。
- 最后一个指示使用数据集中的最后一个条目。
XFiles 因子值 s 如何确定数据集的多少百分比可以有未知值,合并函数计算仍将执行。例如,如果设置为 0.5 (50%),则六次测量中有三次可能是未知的,仍将计算数据集的平均值。如果错过四个读数,则不执行计算,未知值存储在 RRA 中。将此值设置为 0 (0%的遗漏余量),只有当数据集中的所有数据点都可用时,才会执行计算。将该设置保持在 0.5 似乎是一种常见的做法。
正如已经讨论过的,数据集参数指示有多少记录将参与合并函数计算。
最后, samples 告诉 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 update interface.rrd 1273008486:10
$ rrdtool update interface.rrd 1273008786:15
$ rrdtool update interface.rrd 1273009086:25
$ rrdtool update interface.rrd 1273009386:40 1273009686:60 1273009986:66
$ rrdtool update interface.rrd 1273010286:100 1273010586:160 1273010886:166
Python 替代看起来非常相似。在下面的代码中,我们将插入另外 20 条记录,指定定期间隔(300 秒)并提供生成的测量值:
>>> import rrdtool
>>> for i in range(20):
... rrdtool.update('interface.rrd',
... '%d:%d' % (1273010886 + (1+i)*300, i*10+200))
...
>>>
现在让我们从 RRDTool 数据库取回数据:
$ rrdtool fetch interface.rrd AVERAGE
packets
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
[...]
1273069500: -nan
如果您计算条目的数量,您将看到它与我们在数据库上执行的更新数量相匹配。这意味着我们看到的是最高分辨率的结果——在我们的例子中,是每条记录一个样本。以最大分辨率显示结果是默认行为,但是您可以通过指定分辨率标志来选择另一种分辨率(前提是它具有匹配的 RRA)。请记住,在 RRA 定义中,分辨率必须用秒数来表示,而不是用样本数来表示。因此,在我们的示例中,下一个可用分辨率是 6(样本)* 300(秒/样本)= 1800(秒):
$ rrdtool fetch interface.rrd AVERAGE -r 1800
packets
[...]
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.333333333 e-02。(对于许多人来说,这可能看起来有点混乱,但这只是值 0.0333(3)的另一种表示法。)那是哪里来的?在讨论不同的数据源类型时,我提到过 RRDTool 获取两个数据点值之间的差值,并将其除以采样间隔的秒数。默认的采样间隔是 300 秒,因此计算出的速率是 10/300 = 0.0333(3),这是写入 RRDTool 数据库的值。换句话说,这意味着我们的计数器平均每秒增加 0.0333(3)。请记住,所有速率测量值都存储为每秒的变化。在本节的后面,我们将把这个值转换成可读性更强的形式。
下面是使用 Python 模块方法调用检索数据的方法:
>>> for i in rrdtool.fetch('interface.rrd', 'AVERAGE'): print i
...
(1272984300, 1273071000, 300)
('packets',)
[(None,), [...], (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.033333333333333333,),
(0.033333333333333333,), (0.033333333333333333,), (0.033333333333333333,),
(0.033333333333333333,), (0.033333333333333333,), (0.033333333333333333,),
(None,), [...] (None,)]
>>>
结果是一个三元组:数据集信息、数据源列表和结果数组:
- 数据集信息是另一个元组,它有三个值:开始和结束时间戳以及采样率。
- 数据源列表简单地列出了存储在 RRDTool 数据库中并由您的查询返回的所有变量。
- 结果数组包含存储在 RRD 中的实际值。每个条目都是一个元组,包含被查询的每个变量的值。在我们的示例数据库中,我们只有一个变量;因此,元组只包含一个元素。如果无法计算该值(未知),则返回 Python 的 None 对象。
如果需要,您还可以更改采样率:
>>> rrdtool.fetch('interface.rrd', 'AVERAGE', '-r', '1800')
((1272983400, 1273071600, 1800), ('packets',), [(None,), [...] (None,),
(0.06161111111111111,), (0.061666666666666668,), (0.033333333333333333,),
(0.033333333333333333,), (0.033333333333333333,), (None,), [...] (None,)])
注意到目前为止,您应该知道命令行工具语法是如何映射到 Python 模块调用的,您总是调用模块方法,它总是以 RRDTool 函数名命名,例如 fetch、update 等等。该函数的参数是一个任意的值列表。在这种情况下,值是命令行上用空格分隔的任何字符串。基本上,你可以把命令行作为参数列表复制到函数中。显然,您需要用引号将每个单独的字符串括起来,并用逗号分隔它们。为了节省空间和避免混乱,在接下来的例子中,我将只提供命令行语法,您应该能够很容易地将它映射到 Python 语法。
用 RRDTool 绘制图形
用 RRDTool 绘制图表真的很容易,而且绘图是这个工具如此受欢迎的原因之一。最简单的形式是,图形生成命令非常类似于数据获取命令:
$ rrdtool graph packets.png --start 1273008600 --end 1273016400 --step 300\
> DEF:packetrate=interface.rrd:packets:AVERAGE \
> LINE2:packetrate#c0c0c0
即使没有任何额外的修改,结果也是一个非常专业的性能图,正如你在图 1-4 中看到的。
图 1-4 。RRDTool 生成的简单图形
首先,我们来看看命令参数。所有绘图命令都以结果图像的文件名和可选的时间比例值开始。您还可以提供分辨率设置,如果没有指定,将默认为最详细的分辨率。这类似于 fetch 命令中的-r 选项。分辨率用秒表示。
下一行(尽管您可以在一行中键入整个 graph 命令)是选择器行,它从 RRDTool 数据库中选择数据集。选择器语句的格式为:
DEF:<*selector name*>=<*rrd file*>:<*data source*>:<*consolidation function*>
选择器名称参数是一个任意字符串,用于命名结果数据集。可以把它看作一个数组变量,它存储来自 RRDTool 数据库的结果。您可以根据需要使用任意多的选择器语句,但是您至少需要一个来产生输出。
rrd 文件、数据源和合并函数变量的组合准确定义了需要选择的数据。如您所见,这种语法完全解耦了数据存储和数据表示功能。您可以将来自不同 RRDTool 数据库的结果包含在同一个图表中,并以您喜欢的任何方式组合它们。图形数据可以在不同的监控服务器上收集,也可以在单个图像上组合和呈现。
这个选择器语句可以用可选参数扩展,这些参数指定每个数据源的开始、停止和解析值。格式如下,该字符串应该附加在选择器语句的末尾。每个元素都是可选的,您可以使用它们的任意组合。
:step=<*step value*>:start=<*start time value*>:end=<*end time value*>
因此,我们可以将前面的绘图命令重写为:
$ rrdtool graph packets.png \
> DEF:packetrate=interface.rrd:packets:AVERAGE:step=300:
start=1273008600:end=1273016400 \
> LINE2:packetrate#c0c0c0
命令行的最后一个元素是告诉 RRDTool 如何绘制数据的语句。数据绘图命令的基本语法是:
*<PLOT TYPE>*:*<selector name><#color>*:*<legend>*
最广泛使用的绘图类型是线和面积。LINE 关键字后面可以跟一个浮点数来表示线条的宽度。AREA 关键字指示 RRDTool 绘制直线,并填充 x 轴和图形线之间的区域。
两个命令后面都跟有选择器名称,为绘图功能提供数据。颜色值被写成 HTML 颜色格式字符串。您还可以指定一个可选参数图例,它告诉 RRDTool 需要在图形的底部显示一个匹配颜色的小矩形,后跟图例字符串。
就像使用数据选择器语句一样,您可以根据需要使用任意多的绘图语句,但是您至少需要定义一个来生成图形。
让我们再看一下我们制作的图表。RRDTool 很方便地将时间戳打印在 x 轴上,但是 y 轴上显示的是什么呢?这看起来像是以米为单位的测量值,但事实上米代表“毫”,或者是数值的千分之一。因此,打印出来的值与存储在 RRDTool 数据库中的值完全相同。然而,这并不直观。我们看不到数据包的大小,数据传输速率可能很低,也可能很高,具体取决于传输的数据包大小。让我们假设我们正在处理 4KB 的数据包。在这种情况下,合理的解决方案是将信息表示为每秒位数。要将每秒的数据包转换为每秒的位数,我们需要做什么?因为速率间隔不变(在这两种情况下,我们测量的是每秒的数量),所以只需要乘以数据包值,首先乘以 4096(数据包中的字节数),然后乘以 8(一个字节中的位数)。
RRDTool graph 命令允许定义将应用于任何数据选择器变量的数据转换函数。在我们的示例中,我们将使用以下语句将每秒的数据包转换为每秒的字节数:
$ 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 处理。公式本身必须用逆波兰符号来写,在这里你指定第一个参数,然后第二个参数,然后是你想要执行的函数。然后可以将结果用作第一个参数。在我的例子中,我有效地告诉应用“取 packetrate 和 4096 并相乘,取结果和 8 并相乘。”这需要一些时间来调整,但是一旦你掌握了它,用 RPN 表达公式就真的很容易了。
最后,我们需要为 y 轴添加一个标签,为我们正在绘制的值添加一个图例,并为图表本身添加标题,从而使图表更加直观。此示例还演示了如何更改生成图像的大小:
$ rrdtool graph packets.png --step 300 --start 1273105800 --end 1273114200 \
--width 500 --height 200 \
--title "Primary Interface" --vertical-label "Kbp/s" \
DEF:packetrate=interface.rrd:packets:AVERAGE \
CDEF:kbps=packetrate,4096,\*,8,\* \
AREA:kbps#c0c0c0:"Data transfer rate"
结果如图 1-5 所示。
图 1-5 。格式化 RRDTool 生成的图形
对 RRDTool 的介绍只涵盖了它的基本用途。然而,该应用附带了一个非常广泛的 API,允许您更改图形的几乎每个方面。我推荐阅读 RRDTool 文档,可以在oss.oetiker.ch/rrdtool/doc/获得。
将 RRDTool 与监控解决方案集成
我们现在准备将 RRDTool 调用集成到我们的监控应用中,这样我们从支持 SNMP 的设备中收集的信息就会被记录下来,并可随时用于报告。虽然可以在一个 RRDTool 数据库中维护多个数据源,但建议只对密切相关的测量进行维护。例如,如果您正在监视一个多处理器系统,并且想要存储每个 CPU 的中断计数,那么将它们全部存储在一个数据文件中是非常合理的。相比之下,将内存利用率和温度传感器读数混合在一起可能不是一个好主意,因为您可能决定需要一个更高的采样速率来进行一次测量,并且您不能在不影响其他数据源的情况下轻易改变这一点。
在我们的系统中,SNMP OIDs 是在配置文件中提供的,应用完全不知道它们是否相关。因此,我们将每个读数存储在一个单独的数据文件中。每个数据文件将获得与检查部分名称相同的名称(例如,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.databases_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,
'checks' : {}
}
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):
if not self.databases_initialised:
self.initialise_databases()
self.databases_initialised = True
cg = cmdgen.CommandGenerator()
for system in self.systems.values():
comm_data = cmdgen.CommunityData('my-manager', system['communityro'])
transport = cmdgen.UdpTransportTarget((system['address'],
system['port']))
for key, check in system['checks'].iteritems():
oid = check['oid']
errInd, errStatus, errIdx, result = cg.getCmd(comm_data, transport,
oid)
if not errInd and not 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
if not os.path.isfile(data_file):
print data_file, 'does not exist'
rrdtool.create(data_file,
"DS:%s:COUNTER:%s:U:U" % (check,
system['checks'][check]['sampling_rate']),
"RRA:AVERAGE:0.5:1:288",)
def main(conf_file=""):
if not 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'))
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'),
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)。现在,这看起来相对容易实现,大多数人会简单地开始编写 Python 脚本,使用 print 语句来生成 HTML 页面。尽管这种方法看似可行,但在大多数情况下,它很快就会变得难以控制。功能代码通常与内容生成代码混合在一起,添加新功能通常会破坏一切,这反过来又会导致花费数小时来调试应用。
这个问题的解决方案是使用一个模板框架,它允许将应用逻辑从表示中分离出来。模板系统的基本原理很简单:您编写代码来执行计算和其他与内容无关的任务,例如从数据库或其他来源检索数据。然后将这些信息以及使用这些信息的模板的名称传递给模板框架。在模板代码中,您将所有 HTML 格式文本与动态数据(之前生成的)放在一起。然后,框架解析模板中的简单处理语句(比如迭代循环和逻辑测试语句)并生成结果。你可以在图 1-6 中看到该加工的基本流程。
图 1-6 。模板框架中的数据流
这样,您的应用代码就没有任何内容生成语句,并且更易于维护。该模板可以访问提供给它的所有变量,但它看起来更像一个 HTML 页面,将其加载到 web 浏览器中通常会产生可接受的结果。因此,你甚至可以请一个专门的 web 开发人员为你创建模板,因为没有必要知道任何 Python 来修改它们。
我将使用一个名为 Jinja 的模板框架,它的语法与 Django web 框架使用的语法非常相似。我们还将在本书中讨论 Django 框架,因此使用类似的模板语言是有意义的。Jinja 框架也被广泛使用,大多数 Linux 发行版都包含 Jinja 包。在 Fedora 系统上,您可以使用以下命令安装它:
$ sudo yum install python-jinja2
或者,您可以使用 PiP 应用来安装它:
$ sudo pip install Jinja2
你也可以从官网获得 Jinja2 框架的最新开发版本:jinja.pocoo.org/。
提示确保安装的是 Jinja2 而不是更早的版本——Jinja。Jinja2 提供了一种扩展的模板语言,并被积极开发和支持。
用 Jinja2 加载模板文件
Jinja2 被设计用于 web 框架,因此具有非常广泛的 API。它的大部分功能在只生成几页的简单应用中没有使用,所以我将跳过这些功能,因为它们可以成为自己的一本书的主题。在这一节中,我将向您展示如何加载一个模板,向它传递一些变量,并保存结果。这三个函数是您在应用中大多数时间会用到的。有关 Jinja2 API 的更多文档,请参考jinja.pocoo.org/docs/api/。
Jinja2 框架使用所谓的加载器类来加载模板文件。这些可以从各种来源加载,但最有可能的是它们存储在一个文件系统中。负责加载存储在文件系统上的模板的 loader 类称为 jinja2。FileSystemLoader 。它接受一个字符串或字符串列表,这些字符串是可以找到模板文件的文件系统上的路径名:
from jinja2 import FileSystemLoader
loader1 = FileSystemLoader('/path/to/your/templates')
loader2 = FileSystemLoader(['/templates1/', '/teamplates2/']
一旦初始化了 loader 类,就可以创建 jinja2 的实例了。环境等级。这个类是框架的核心部分,用于存储配置变量,访问模板(通过加载器实例),并将变量传递给模板对象。初始化环境时,如果要访问外部存储的模板,必须传递 loader 对象:
from jinja2 import Environment, FileSystemLoader
loader = FileSystemLoader('/path/to/your/templates')
env = Environment(loader=loader)
创建环境后,您可以加载模板并呈现输出。首先调用 get_template 方法,它返回一个与模板文件相关联的模板对象。接下来调用模板对象的方法 render,该方法处理模板内容(由之前初始化的 loader 类加载)。结果是经过处理的模板代码,可以写入文件。您必须将所有变量作为字典传递给模板。字典键是模板中可用变量的名称。字典值可以是您想要传递给模板的任何 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。要获得完整的语言描述,请在jinja.pocoo.org/2/documentation/templates查看官方的 Jinja2 模板语言定义。
模板语句必须转义;任何未被转义的内容都不会被处理,并将在呈现过程后被逐字返回。
有两种类型的语言分隔符:
- 变量访问分隔符,表示对变量的引用:{{...}}
- 语句执行分隔符,它告诉框架分隔符内的语句是函数指令:{%...%}
访问变量
正如您已经知道的,模板通过作为字典键给出的名称来识别变量。假设传递给呈现函数的字典是这样的:
{'name': name, 'age': age}
模板中的以下语句可以访问这些变量,如下所示:
{{ name }} / {{ age }}
传递给模板的对象可以是任何 Python 对象,模板可以使用相同的 Python 语法访问它。例如,您可以访问字典或数组元素。假设以下呈现调用:
person = {'name': 'John', 'age': 30}
r = t.render({'person': person})
然后,您可以使用以下语法来访问模板中的字典元素:
{{ person.name }} / {{ person.age }}
流量控制语句
流控制语句允许您对变量进行检查,并选择模板的不同部分进行相应的渲染。在生成表格或列表等结构时,也可以使用这些语句来重复模板的一部分。
森林...in loop 语句可以遍历这些 iterable Python 对象,一次返回一个元素:
Available products</h1>
<ul>
{% for item in products %}
<li>{{ item }}</li>
{% endfor %}
</ul>
一旦进入循环,将定义以下特殊变量。您可以使用它们来检查您在循环中的确切位置。
。循环属性变量
|
可变的
|
描述
| | --- | --- | | 循环索引 | 循环的当前迭代。索引从 1 开始;使用 loop.index0 进行从 0 开始的计数。 | | walk.revindex | 类似于 loop.index,但从循环结束时开始计算迭代次数。 | | 循环优先 | 如果第一次迭代。 | | loop.last | 如果最后一次迭代。 | | 循环长度 | 序列中元素的总数。 |
逻辑测试函数 if 用作布尔检查,类似于 Python if 语句的使用:
{% if items %}
<ul>
{% for item in items %}
{% if item.for_sale %}
<li>{{ item.description }}</li>
{% endif %}
{% endfor %}
</ul>
{% else %}
There are no items
{% endif %}
Jinja2 框架还允许模板继承。也就是说,您可以定义一个基本模板并从它继承。然后,每个子模板用适当的内容重新定义主模板文件中的块。例如,父模板(parent.tpl)可能如下所示:
<head>
<title> MyCompany – {% block title %}Default title{% endblock %}</title>
</head>
<html>
{% block content %}
There is no 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):
if not 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' : {}
}
for check in [c for c in config.sections() if c.startswith('check')]:
systems[config.get(check, 'system')]['checks'][check] = {
'oid' : config.get(check, 'oid'),
'description': config.get(check,
'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 。索引模板
System checks</h1>
{% if systems %}
{% for system in systems %}
<h2>{{ systems[system].description }}</h2>
<p>{{ systems[system].address }}:{{ systems[system].port }}</p>
{% if systems[system].checks %}
The following checks are available:
<ul>
{% for check in systems[system].checks %}
<li><a href="{{ check }}.html">
{{ systems[system].checks[check].description }}</a></li>
{% endfor %}
</ul>
{% else %}
There are no checks defined for this system
{% endif %}
{% endfor %}
{% else %}
No system configuration available
{% endif %}
该模板生成的网页呈现如图图 1-7 所示。
图 1-7 。浏览器窗口中的索引网页
每个列表项的链接指向一个单独的支票详细信息网页。每个这样的网页都有一个检查部分名称,如 check_1.html。这些页面是从 details.tpl 模板生成的:
{{ check.description }}</h1>
<p>OID: {{ check.oid }}</p>
<img src="{{ name }}.png" />
该模板链接到由 RRDTool graph 方法生成的图形图像。图 1-8 显示了结果页面。
图 1-8 。带有图形的 SNMP 详细信息
摘要
在这一章中,我们建立了一个简单的设备监控系统。在此过程中,您了解了 SNMP,以及用于 Python 的数据收集和绘图库——RRD tool 和 Jinja2 模板系统。需要记住的要点:
- 大多数网络连接设备使用 SNMP 公开其内部计数器。
- 每个这样的计数器都有一个分配给它的专用对象 ID。
- 对象 id 被组织成树状结构,其中树状分支被分配给不同的组织。
- RRDTool 是一个库,允许您存储、检索和绘制网络统计数据。
- RRD 数据库是一个循环数据库,这意味着它有一个固定的大小,新记录在插入时会将旧记录推出。
- 如果您生成 web 页面,请使用 Jinja2 模板系统,它允许您将功能代码从表示中分离出来。
二、使用 SOAP API 管理设备
在本章中,我们将构建一个命令行工具来查询和管理 Citrix Netscaler 负载平衡器设备。这些设备通过 SOAP API 公开管理服务,这是 web 服务之间通信的标准方式之一。
什么是 SOAP API?
SOAP 代表简单对象访问协议。开发和创建该协议是为了用作在各种 web 服务之间交换结构化信息的机制。许多知名公司通过 SOAP API 接口公开他们的服务;例如,Amazon 允许使用 SOAP API 调用来控制他们的弹性计算云(EC2)和简单存储系统(S3)服务。
使用 SOAP 查询,用户可以创建虚拟机、启动和停止服务、操作远程分布式文件系统上的数据,以及执行产品搜索。支持 SOAP 的应用通过发送 SOAP“消息”来交换信息每条消息都是 XML 格式的文档。SOAP 协议位于其他传输协议之上,如 HTTP、HTTPS、SMTP 等。理论上,您可以发送封装在电子邮件消息(SMTP)中的 SOAP 请求,但是最广泛使用的 SOAP 传输机制不是普通 HTTP 就是 HTTPS (SSL 加密 HTTP)。
由于 XML 的冗长,SOAP 不是最有效的通信方式,因为即使是最小和最简单的消息也会变得非常大和神秘。SOAP 定义了一组用于构造应用层协议消息的规则。最常用的协议之一是 RPC(远程过程调用)。因此,通常所说的 SOAP API 实际上是一个 SOAP 编码的 RPC API。RPC 定义了 web 服务如何相互通信和交互。当与 RPC 一起使用时,SOAP 可以执行请求-响应对话。
SOAP 的最大优势在于它不是特定于语言或平台的,因此用不同语言编写并在不同平台上运行的应用可以很容易地相互通信。它还是一个开放标准协议,这意味着有许多库为开发支持 SOAP 的应用和服务提供支持。
SOAP 消息的结构
每个 SOAP 消息包含以下元素:
- *信封。*该元素将 XML 文档标识为 SOAP 消息。它还定义了在 SOAP 消息中使用的名称空间。
- 消息头。该元素位于 Envelope 元素中,包含特定于应用的信息。例如,认证细节通常存储在 Header 元素中。该元素还可能包含不是发送给消息接收方的数据,而是发送给重新传输 SOAP 通信的中间设备的数据。
- 消息体。该元素位于 SOAP Envelope 元素中,包含请求和响应信息。邮件正文元素是必填字段,不能省略。该元素包含打算发送给消息接收者的实际数据。
- 故障元素。这个可选元素位于消息体中。如果存在,它包含错误代码、可读的错误描述、错误发生的原因以及任何特定于应用的详细信息。
清单 2-1 是一个框架 SOAP 消息的例子。
清单 2-1 。一条简单的 SOAP 消息
<?xml version="1.0"?>
<soap:Envelope
xmlns:soap="http://www.w3.org/2001/12/soap-envelope"
soap:encodingStyle="http://www.w3.org/2001/12/soap-encoding">
<soap:Header>
[...]
</soap:Header>
<soap:Body>
[...]
<soap:Fault>
[...]
</soap:Fault>
</soap:Body>
</soap:Envelope>
用 SOAP 请求服务
假设我们有两个 web 服务:web 服务 A 和 Web 服务 b。每个 Web 服务都是运行在专用服务器上的应用。我们还假设服务 B 实现了一个简单的客户查找服务,它接受一个表示客户标识符的整数,并在一个数组中返回两个字段:客户的姓名和联系电话号码。服务 A 是一个应用,它充当客户端并向服务 b 请求详细信息。
当服务 A(发送者)想要找出关于客户的细节时,它构造清单 2-2 中所示的 SOAP 消息,并将其作为 HTTP POST 请求发送给服务 B。
清单 2-2 。SOAP 请求消息
<?xml version="1.0" encoding="UTF-8" ?>
<SOAP-ENV:Envelope
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/1999/XMLSchema">
<SOAP-ENV:Body>
<ns1:getCustomerDetails
xmlns:ns1="urn:CustomerSoapServices">
<param1 xsi:type="xsd:int">213307</param1>
</ns1:getCustomerDetails>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
接下来,服务 B(服务器)执行查找,将结果封装在 SOAP 消息中,并将其发送回来。响应消息(清单 2-3 )作为对原始 POST 请求的 HTTP 响应。
清单 2-3 。SOAP 响应消息
<?xml version="1.0" encoding="UTF-8" ?>
<SOAP-ENV:Envelope
xmlns:xsi="http://www.w3.org/1999/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/1999/XMLSchema"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Body>
<ns1: getCustomerDetailsResponse
xmlns:ns1="urn:CustomerSoapServices"
SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<return
xmlns:ns2="http://schemas.xmlsoap.org/soap/encoding/"
xsi:type="ns2:Array"
ns2:arrayType="xsd:string[2]">
<item xsi:type="xsd:string">John Palmer</item>
<item xsi:type="xsd:string">+44-(0)306-999-0033</item>
</return>
</ns1:getCustomerDetailsResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
从例子中可以看出,SOAP 对话非常健谈。所有这些额外的信息(包括名称空间定义和字段数据类型)都是必需的,这样客户机和服务器都知道如何解析和验证数据。
查找有关 WSDL 可用服务的信息
如果您仔细查看前面的示例,您会注意到客户端请求了以下方法:getCustomerDetails。我们如何知道哪些方法或服务是可用的?此外,我们如何找出方法需要什么参数,以及什么方法将返回它的响应消息?
找到这些信息最简单的方法是从 web 服务的 WSDL (Web 服务描述语言)文档中找到。这个 XML 格式的文档描述了与 web 服务相关的各种细节,例如:
- 使用的通信协议(部分)
- 接受和发送的消息(部分)
- web 服务公开的方法(部分)
- 使用的数据类型(部分)
这些部分中的每一个都可能包含多个条目,这取决于 web 服务正在做什么。例如,清单 2-4 是翻译服务的简化 WSDL 定义。在这个例子中,我们假想的自动翻译器接受一个文本字符串作为输入参数,并返回一个翻译后的字符串作为结果。我们有两个远程方法,分别叫做 translateEnglishToFrench 和 translateFrenchToEnglish。它们都使用相同的请求和响应数据类型。
清单 2-4 。WSDL 定义的一个例子
<message name="translateRequest">
<part name="term" type="xs:string"/>
</message>
<message name="translateResponse">
<part name="value" type="xs:string"/>
</message>
<portType name="languageTranslations">
<operation name="translateEnglishToFrench">
<input message="translateRequest"/>
<output message="translateRequest"/>
</operation>
<operation name="translateFrenchToEnglish">
<input message="translateRequest"/>
<output message="translateRequest"/>
</operation>
</portType>
<binding type="languageTranslations" name="bn">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<operation>
<soap:operation soapAction="http://example.com/translateEnglishToFrench"
name="trEn2Fr"/>
<input><soap:body use="literal"/></input>
<output><soap:body use="literal"/></output>
</operation>
<operation>
<soap:operation soapAction="http://example.com/translateFrenchToEnglish"
name="trFr2En"/>
<input><soap:body use="literal"/></input>
<output><soap:body use="literal"/></output>
</operation>
</binding>
绑定部分定义了用于访问每个公开的方法的访问 URL。每个操作都有一个用于引用它的名称。
Python 中的 SOAP 支持
在支持 SOAP 协议方面,Python 不如其他语言幸运。过去有一些计划和项目试图在 Python 中实现 SOAP 库,但是大多数都被放弃了。目前最活跃和最成熟的项目是 Zolera SOAP 基础设施(ZSI)。
在大多数 Linux 发行版中,这个包被命名为 python-ZSI,可以从发行版的默认包管理器中安装。如果你选择从源代码安装 ZSI 软件包,它可以在pywebsvcs.sourceforge.net/找到。
使用 ZSI 从 Python 访问 SOAP 服务有两种方式:
- 服务方法可以通过 ServiceProxy 类访问,它是 ZSI 库的一部分。创建该类的对象时,所有远程函数都可以作为该对象实例的方法使用。这是访问所有服务的一种便捷方式,但是它要求您手动生成类型代码和定义名称空间,这是一项繁重的工作。
- 可以使用 wsdl2py 工具访问 SOAP 接口。该工具读取服务的 WSDL 定义并生成两个模块:一个包含类型代码信息,另一个包含服务方法。
我更喜欢使用第二种方法,因为它让我不必定义类型代码和记忆名称空间。使用 ServiceProxy 类时,用户必须显式定义过程的命名空间。此外,请求对象的类型代码必须与 WSDL 中定义的类型兼容,并且这种类型代码必须手工编写,对于使用复杂数据结构的服务来说,这可能是一个真正的难题。
将 WSDL 模式转换为 Python 助手模块
到目前为止,您已经了解了 SOAP 协议(一种基于 XML 的协议,它定义了如何封装消息)、RPC 的通信方式(客户端发送一条消息,告诉服务器它希望服务器执行什么功能,服务器用一条包含远程功能生成的数据的消息来响应),以及 WSDL (这种语言定义了哪些方法可用以及在请求/响应中使用哪些数据类型)。
我们还决定生成两个助手模块:一个包含远程方法,另一个定义数据结构,我们将使用 ZSI 图书馆提供的 wsdl2py 工具。
我将编写一个工具来管理 Citrix Netscaler 负载平衡器设备。这些设备提供了两个 web 服务接口:
- 统计 Web 服务 。该服务提供查询负载平衡器所有功能方面的统计信息的方法,如虚拟服务器、服务、VLAN 配置等。在 Netscaler 操作系统的 8.1 版中,有 44 个对象可用于收集性能信息。 ** 配置 Web 服务 。此服务允许您更改设备配置和执行维护任务,例如启用/禁用服务器和服务。在同一个 8.1 NS 操作系统中,有 2,364 个可通过 SOAP 接口访问的可配置参数。*
**可以通过访问192.168.1.1/ws/download . pl找到 WSDL 位置和其他有用信息的链接,例如 API 文档和 SNMP 对象定义,其中 192 . 168 . 1 . 1 需要替换为您正在使用的 Netscaler 负载平衡器的 IP 地址。在本章中,我将使用 192.168.1.1 作为我的 Netscaler 设备的 IP。也可以从主管理屏幕获得下载页面的链接。
我提供了以下 WSDL 网址,因为它们不太可能会改变:
- 统计 SOAP 接口的 WSDL:
192 . 168 . 1 . 1/API/nsstat . wsdl - 配置 SOAP 接口的 WSDL:
192 . 168 . 1 . 1/API/ns config . wsdl
使用 wsdl2py 脚本非常简单;如果不需要特殊的配置,我们只需要提供 WSDL 文档的位置,它就会自动生成方法和数据类型模块。不需要额外的用户输入。wsdl2py 工具可以从 web 位置获取 wsdl 文档,或者我们可以提供一个文件名,它将解析该文件。
在下面的示例中,我们将把 wsdl2py 脚本直接指向 Netscaler 负载平衡器上的 WSDL URL。
$ wsdl2py --url http://192.168.1.1/api/NSStat.wsdl
如果脚本可以联系目标服务器,并且它接收的 XML 文档不包含错误,那么它将不会产生任何消息,并且会自动创建两个 Python 包。
注意如果您检索了一个 WSDL 文件并将其存储在本地,您可以使用- file 标志并提供 WSDL 文件的文件名。这将指示 wsdl2py 解析本地存储的文件。
此时,我们已经运行了脚本,wsdl2py 已经生成了以下两个模块:
- NSStat_services.py:这个模块包含 Locator 类,用于连接到每个远程可用方法的服务和类。
- NSStat_services_types.py:这个文件很少被直接使用。它是从前面的模块导入的,包含我们的 web 服务使用的每种数据类型的类定义。它确实包含了一些有用的信息,我们稍后在创建请求和检查来自 web 服务的响应时会用到这些信息。
wsdl2py 工具还有其他选项可用于生成服务器助手模块。有了这些模块,我们就可以实现我们自己版本的 web 服务,该服务公开相同的接口,并理解由我们的 WSDL 文件定义的相同协议,但这超出了我们这里项目的范围。
为我们的负载平衡器工具定义需求
到目前为止,我们只研究了 SOAP 协议和提供 SOAP 支持的 Python 库,它们创建了我们将用来访问 Netscaler web 服务的助手模块。我们还没有编写实际的代码来执行 SOAP 调用,并利用它接收到的信息做一些有用的事情,但是在我们深入有趣的事情(即编写代码)之前,让我们后退一步,决定几件重要的事情:
- 我们希望我们的工具做什么?
- 我们将如何构建我们的代码?
因为这些问题听起来简单,看起来显而易见,所以经常被忽视。这通常会导致糟糕的编写和难以管理的代码。
如果我们不确切地知道我们想要我们的代码做什么,我们就有可能使我们的代码过于简单或者过于复杂。换句话说,我们可能会写几行简单的代码,但实际上我们希望它更通用,可以为其他人或其他项目重用。因此,我们不断增加新的功能,创造各种变通方法,代码变成了一个不可维护的庞然大物。这种过度复杂也是危险的,因为我们可能会发现自己花了几天、几周(如果我们真的有创造力,甚至几个月)来编写复杂的数据结构,而几行抛弃式的原型代码会更有效。所以,在开始之前仔细考虑你想做什么,但是也不要在上面花太多时间;在大多数情况下,系统管理员不需要开发全面的应用,因此对他们来说事情就容易多了。
在开始之前,我发现考虑以下几点,并为每一点写几个简单的段落,就足以作为一个粗略的指南和需求规范文档:
- 定义基本要求
- 定义代码结构
- 决定可配置和可更改的项目
- 定义错误处理和日志记录
基本要求
我们列出了我们希望这个工具做什么的列表;简单的陈述,如“我想要...去做……”非常有效,因为我们不追求正式的需求规格。下面的例子说明了这种思维方式:
- 我们希望我们的应用收集以下统计信息:
- CPU 和内存利用率
- 系统概述:请求速率、数据速率、已建立的连接
- 所有虚拟服务器概述:启动/关闭以及每个虚拟服务器中有哪些服务关闭
- 我们希望我们的应用能够:
- 禁用/启用任何可用虚拟服务器的所有服务
- 禁用/启用任何单独的服务
- 禁用/启用任何一组服务(可能跨越多个虚拟服务器)
- 我们希望在其他脚本中重用已定义的函数。
- 代码应该易于修改和添加新功能。
代码结构
现在我们已经定义了对工具的需求,我们可以清楚地看到如何组织我们的脚本:
- 所有进行 SOAP 调用的函数都需要在单独的模块中定义。这个模块可以由不同的脚本导入,这些脚本可以使用相同的功能。
- 最好定义一个包含访问 web 服务的方法的类,这样任何人都可以简单地继承这个类并扩展附加功能。
- 该工具将由两个不同的部分组成,一部分用于读取统计数据,另一部分用于控制服务。
将其映射到源代码,我们将拥有以下文件和模块:
- 我们自己的库 NSLib.py,它将包含以下内容的定义:
- NSLibError 异常类。每当我们遇到任何不可恢复的问题,我们将提出这个例外。
- NSSoapApi 类。这是根类,实现所有 Netscaler SOAP API 对象共有的方法:初始化和登录。
- NSStatApi 类。这继承了 NSSoapApi 类。该类实现所有处理统计信息收集和监视的方法。它只执行 WSDL 统计局定义的呼叫。
- NSConfigApi 类。这继承了 NSSoapApi 类。该类实现处理负载平衡器配置的所有方法,并调用由配置 WSDL 定义的方法。
- 这个文件使用 NSLib 中的 NSStatApi,并且是实现我们的统计信息收集任务的实际脚本。这是我们将从命令行调用的脚本。
- 该文件使用 NSLib 中的 NSConfigApi,并且是实现我们的负载平衡器配置任务的实际脚本。这是我们将从命令行调用的脚本。
- 这是我们的配置脚本,包含了与负载平衡器建立通信所需的所有定义。参见下面的详细描述。
配置
我们可能需要管理和监控不止一个负载平衡器。因此,我们将创建一个简单的配置文件来标识它们中的每一个,并且还包含登录详细信息和服务组。
因为它将被对脚本相当熟悉的人使用,而不是针对简单用户的脚本,所以我们可以创建一个带有静态定义变量的 Python 文件并导入它。清单 2-5 是我将在本章中使用的例子。
清单 2-5 。包含负载平衡器详细信息的配置文件
#!/usr/bin/env python
netscalers = {
'default': 'primary',
'primary': {
'USERNAME': 'nstest',
'PASSWORD': 'nstest',
'NS_ADDR' : '192.168.1.1',
'groups': {},
},
'secondary': {
'USERNAME': 'nstest',
'PASSWORD': 'nstest',
'NS_ADDR' : '192.168.1.2',
'groups': {},
},
}
如您所见,我们这里有两个 netscalers,主要和辅助,具有不同的 IP 地址(您也可以有不同的用户和密码)。尚未定义服务组;我们可以在以后需要时添加它们。
在我们的工具中,如果我们需要访问这些配置数据,我们将如清单 2-6 所示检索它。
清单 2-6 。访问配置数据
import ns_config as config
# to access configuration of the 'primary' loadbalancer
username_pri = config.netscalers['primary']['USERNAME']
# to access configuration of the default loadbalancer
default_lb = config.netscalers['default']
username_def = config.netscalers[default_lb]['USERNAME']
使用 SOAP API 访问 Citrix Netscaler 负载平衡器
我们需要找到服务地点。对于 web 服务,它几乎总是一个 URL。然而,我们并不真的需要知道 URL,因为我们有特殊的 Locator 类,它一旦被初始化就创建一个绑定对象,我们用它来访问 SOAP 服务。
但是,在继续之前,我们需要解决 Netscaler 的 WSDL 的一个小问题。
修复 Citrix Netscaler WSDL 的问题
我们生成的服务访问助手模块(NSStat_services.py)中的定位器类如清单 2-7 中的所示定义。
清单 2-7 。定位器类定义
# Locator
class NSStatServiceLocator:
NSStatPort_address = "http://netscaler_ip/soap/"
def getNSStatPortAddress(self):
return NSStatServiceLocator.NSStatPort_address
def getNSStatPort(self, url=None, **kw):
return NSStatBindingSOAP(url or NSStatServiceLocator.NSStatPort_address, **kw)
这显然是错误的,因为服务主机名 netscaler_ip 不是有效的 ip 地址(应该是 192.168.1.1),也不是有效的域名。Citrix Netscaler 一直以这种方式公开其端点,因此我们只能假设这是设计使然。
以这种方式发生的一个可能的原因是,某人可能想要使用相同的 WSDL 信息连同他的软件来管理多个负载平衡设备,因此从他将要管理的每一个设备中检索和编译 WSDL 是不切实际的。因此,由 API 用户/开发者用正确的地址替换这个地址。Netscaler SOAP API 手册中的所有示例都以相同的方式运行,它们忽略此变量,而不是传递自己的设置。
因此,我们必须修改 NSStatPort_address 变量,将 netscaler_ip 替换为我们设备的 ip 地址。幸运的是,这只能做一次;WSDL 不会经常改变(通常只在主要的操作系统升级期间)。清单 2-8 显示了修改。
清单 2-8 。手动修改定位器类
# Locator
class NSStatServiceLocator:
NSStatPort_address = "http://192.168.1.1/soap/"
def getNSStatPortAddress(self):
return NSStatServiceLocator.NSStatPort_address
def getNSStatPort(self, url=None, **kw):
return NSStatBindingSOAP(url or NSStatServiceLocator.NSStatPort_address, **kw)
注意如果您不希望修改模块,您将在本章后面看到解决这个问题的另一种方法,通过这种方法,您可以在定位器对象的初始化过程中指定服务端点。
创建连接对象
我们已经准备好并修复了助手模块,所以最后我们将通过 SOAP API 与我们的负载平衡器进行通信。在我们继续之前,我们需要导入我们用 wsdl2py 生成的所有方法:
import NSStat_services
初始化定位器和服务访问对象非常简单,只需两行代码就可以完成。首先,我们创建实际的定位器对象,它包含关于 web 服务位置的信息:
locator = NSStat_services.NSStatServiceLocator()
然后,我们调用将返回绑定对象的方法,该对象已经用服务 URL 初始化:
soap = locator.getNSStatPort()
定位器对象只有两个方法:一个从 WSDL 读取 URL,另一个初始化并返回绑定对象。
绑定对象(在我们的示例中,初始化为变量 soap)包含我们的 web 服务上可用的所有方法(在这个实例中,是 Citrix Netscaler Statistics API)。它就像一个代理,将对象方法映射到 API 函数。
在我们继续之前,让我们看看如何修复 Netscaler 无效 URL 问题。正如您已经知道的,您可以询问定位器对象并请求一个端点 URL。还可以强制 getNSStatPort 使用自定义 URL,而不是生成的 URL。因此,我们要做的是获取 URL,用我们的负载平衡器的 IP 替换假字符串,然后用正确的 URL 生成一个绑定对象。清单 2-9 显示了代码。
清单 2-9 。替换负载平衡器地址
MY_NS_IP = '192.168.1.1'
locator = NSStat_services.NSStatServiceLocator()
bad_url = locator.getNSStatPortAddress()
good_url = re.sub('netscaler_ip', MY_NS_IP, bad_url)
soap = locator.getNSStatPort(url=good_url)
正如您所看到的,这里我使用了 getNSStatPortAddress 定位器方法来检索 URL 字符串,然后使用正则表达式对其进行修改,并用负载平衡器的 ip 替换 netscaler_ip 字符串。最后,我请求定位器用我新的(正确的)URL 创建我的 SOAP 绑定对象。
这种方法比更改自动生成的模块更加灵活。无论出于什么原因(例如升级 NS OS),如果您决定生成一个新的模块,您将会丢失所做的更改。另一种方法也要求你记住你必须修改代码。在发出请求的代码中覆盖 IP 更加明显,并且它不会干扰可能重用相同助手模块的其他工具。
因此,这是创建连接对象的一种快速方法,但是我们如何将它放入我们之前定义的所需结构中呢?请记住,我们决定创建一个具有初始化和日志记录功能的泛型类,然后从中派生出两个不同的类:一个用于统计和监控模块,另一个用于管理和配置模块。你可以看到 图 2-1 中的类继承。
图 2-1 。类继承图
这造成了一个直接的问题,因为我们需要为每个服务使用不同的定位器对象;我们不能在 NSSoapApi 类中初始化它们,因为我们不知道需要使用哪种类型的定位器对象,Stat 还是 Config。
泛型类需要能够识别它应该使用哪个模块作为服务定位器,因此我将把 NSStatApi 或 NSConfigApi 中的模块对象作为参数传递给 NSSoapApi,NSSoapApi 将使用该参数初始化适当的定位器,并使用特定的模块调用执行登录调用。这听起来可能很复杂,但实际上并不复杂。清单 2-10 显示了实现这个的代码。
清单 2-10 。定义泛型类
class NSSoapApi(object):
def __init__(self, module=None,
hostname=None,
username=None,
password=None):
[...]
self.username = username
self.password = password
self.hostname = hostname
self.module = module
if self.module.__name__ == 'NSStat_services':
[...]
self.locator = self.module.NSStatServiceLocator()
bad_url = self.locator.getNSStatPortAddress()
good_url = re.sub('netscaler_ip', self.hostname, bad_url)
self.soap = self.locator.getNSStatPort(url=good_url)
elif self.module.__name__ == 'NSConfig_services':
[...]
self.locator = self.module.NSConfigServiceLocator()
bad_url = self.locator.getNSConfigPortAddress()
good_url = re.sub('netscaler_ip', self.hostname, bad_url)
self.soap = self.locator.getNSConfigPort(url=good_url)
else:
[...]
self.login()
def login(self):
[...]
req = self.module.login()
req._username = self.username
req._password = self.password
[...]
res = self.soap.login(req)._return
[...]
这个泛型类需要一个模块对象传递给它,因此它可以执行以下操作:
- 从传递的模块中直接调用通用方法,如 login。
- 根据模块的不同,调用特定的方法或引用特定于模块的类,如 NSStatServiceLocator vs NSConfigServiceLocator
。
我们的子类将把模块对象传递给超类,如清单 2-11 所示。
清单 2-11 。将模块对象传递给泛型类
class NSStatApi(NSSoapApi):
def __init__(self, hostname=None, username=None, password=None):
super(NSStatApi, self).__init__(hostname=hostname,
username=username,
password=password,
module=NSStat_services)
class NSConfigApi(NSSoapApi):
def __init__(self, hostname=None, username=None, password=None):
super(NSConfigApi, self).__init__(hostname=hostname,
username=username,
password=password,
module=NSConfig_services)
登录:我们的第一个 SOAP 调用
此时,还没有进行任何实际的 API 调用;我们所做的只是准备和初始化工作。在开始请求性能数据或进行配置更改之前,我们需要做的第一件事是向负载平衡器进行验证。因此,我们的第一个 API 调用将是登录方法。
使用生成的助手库执行 SOAP 请求总是遵循相同的模式:
- 创建一个请求对象。
- 用参数初始化请求对象;这是 SOAP 函数的参数列表。
- 调用表示适当 SOAP 方法的 binder 方法,并将请求对象传递给它。
- binder 方法返回一个 API 响应(或者在联系 web 服务失败时引发一个异常)。
正如我们已经看到的,定位器返回的绑定对象属于 NSStatBindingSOAP 类。该类的方法表示 web 服务上所有可用的函数。其中之一是登录功能,如清单 2-12 所示,我们将使用它向负载均衡器表明自己的身份。
清单 2-12 。登录方法的定义
# op: login
def login(self, request):
if isinstance(request, login) is False:
raise TypeError, "%s incorrect request type" % (request.__class__)
kw = {}
# no input wsaction
self.binding.Send(None, None, request, soapaction="urn:NSConfigAction",
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/",
**kw)
# no output wsaction
typecode = Struct(pname=None, ofwhat=loginResponse.typecode.ofwhat,
pyclass=loginResponse.typecode.pyclass)
response = self.binding.Receive(typecode)
return response
与 NSStatBindingSOAP 类的其他方法一样,login 方法只接受一个参数,即请求对象。
请求对象必须从 login 类构造,该类可从同一个 helper 模块获得。找出请求对象必须包含什么的最简单方法是查看它的定义;清单 2-13 显示了我们实例中的内容。
清单 2-13 。登录请求类
class login:
def __init__(self):
self._username = None
self._password = None
return
因此,当我们初始化新的请求对象时,我们必须在将它传递给绑定对象之前设置 _username 和 _password。
现在,让我们创建这些对象并进行登录 SOAP 调用。清单 2-14 显示了代码。
清单 2-14 。默认登录方法的包装
class NSSoapApi(object):
[...]
def login(self):
# create request object and assign default values
req = self.module.login()
req._username = self.username
req._password = self.password
[...]
res = self.soap.login(req)._return
if res._rc != 0:
# an error has occurred
与所有其他请求一样,发出 SOAP 登录调用是一个两步过程:
-
我们创建并初始化请求对象;这个对象包含我们将要发送给 web 服务的数据。在下面的例子中,req 是我们的登录请求对象,我们通过为登录调用设置用户名和密码来初始化它:
req = self.module.login() req._username = self.username req._password = self.password -
我们从绑定对象中调用适当的代理函数,并将请求对象传递给它。以下步骤被压缩成一行代码:
- 调用我们的绑定对象的登录方法。
- 传递在上一步中构造的请求对象。
- 阅读回复。
所有步骤完成后,res 将包含返回对象,变量在 NSStat_services_types.py 模块(或 WSDL 数据类型部分)中定义:
res = self.soap.login(req)._return
查找响应中返回的内容
我们已经知道,要想知道我们应该在对 web 服务的请求中发送什么,我们需要查看服务的 helper 模块,它包含所有请求对象的类。但是我们如何知道我们收到的回应是什么呢?
如果我们再次查看绑定类中的 login 方法,我们会发现它返回一个 loginResponse 类型的对象,如清单 2-15 所示。
清单 2-15 。绑定类的返回值
def login(self, request):
[...]
typecode = Struct(pname=None, ofwhat=loginResponse.typecode.ofwhat,
pyclass=loginResponse.typecode.pyclass)
response = self.binding.Receive(typecode)
return response
从 loginResponse 类(清单 2-16 )中,我们发现它只包含一个变量,_return。
清单 2-16 。LoginResponse 类的内容
class loginResponse:
def __init__(self):
self._return = None
returnloginResponse.typecode =
Struct(pname=("urn:NSConfig","loginResponse"),
ofwhat=[ns0.simpleResult_Def(pname="return", aname="_return", typed=False, encoded=None,
minOccurs=1, maxOccurs=1, nillable=True)], pyclass=loginResponse, encoded="urn:NSConfig")
然而这还不够,因为 _return 是包含我们需要的信息的对象,我们需要找到如何引用它。由于 loginResponse 非常简单(只返回两个字段),所以它使用了一个通用的响应对象;我们通过查看 loginResponse 类的 typecode 定义中的 ofwhat 设置,从该类的 typecode 设置中发现了这一点。在以下示例中,它是突出显示的字符串:
class loginResponse:
def __init__(self):
self._return = None
return
loginResponse.typecode = Struct(pname=("urn:NSConfig","loginResponse"),
ofwhat=[ns0.simpleResult_Def(pname="return",
aname="_return",
typed=False,
encoded=None,
minOccurs=1,
maxOccurs=1,
nillable=True)],
pyclass=loginResponse, encoded="urn:NSConfig")
更复杂的结构有以它们命名的结果对象,所以更容易找到它们,但是对于 login,我们需要在类型定义模块(NSStat_services_types.py)中寻找 simpleResult 类。这个类的定义,如清单 2-17 所示,可能看起来有点神秘,但是我们并不真的需要知道它运行的细节;只需查找 Holder 类定义。
清单 2-17 。simpleResult 的类定义
class simpleResult_Def(ZSI.TCcompound.ComplexType, TypeDefinition):
[...]
class Holder:
typecode = self
def __init__(self):
# pyclass
self._rc = None
self._message = None
return
[...]
我将在本章后面更详细地解释如何找到复杂数据类型的对象的引用和定义;请参阅“读取系统健康数据”
我们登录后,如何维护会话?
您可能想知道在我们成功登录到我们的 web 服务后,接下来会发生什么。当其他调用不需要用户名和密码以及其他参数时,负载平衡器如何知道我们被授权进行其他调用?
一些 web 服务发回一个特殊的令牌,这个令牌是在服务器上生成的,并且与使用 API 的帐户相关联。如果是这样的话,我们就必须将这个令牌与我们发送给 web 服务的每个请求结合起来。
不过,使用 Netscaler 负载平衡器,事情就简单多了。在我们发送登录请求之后,如果我们的身份验证信息是正确的,负载均衡器将会用一个简单的“OK”消息来响应。它还会在 HTTP 头中用一个特殊的 cookie 来响应,这个 cookie 充当我们的令牌。我们只需要在向 web 服务发送后续请求时,确保在 HTTP 头中设置了这个 cookie,而不是将令牌细节合并到每个 SOAP 请求中。清单 2-18 显示了 tcpdump 命令的输出,它清楚地展示了这一点。(我省略了其他 TCP 包,去掉了无关的二进制数据,所以只显示 HTTP 和 SOAP 协议。)
清单 2-18 。HTTP 封装的 SOAP 登录请求和登录响应消息
11:11:35.283170 IP 192.168.1.10.40494 > 192.168.1.1.http: P 1:166(165) ack 1 win 5488
[...]
POST /soap/ HTTP/1.1
Host: 192.168.1.1
Accept-Encoding: identity
Content-Length: 540
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:NSConfigAction"
[...]
<SOAP-ENV:Envelope xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:
SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:ZSI="http://www.zolera.com/schemas/ZSI/" xmlns:xsd=
"http://www.w3.org/2001/XMLSchema" xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance" SOAP-ENV:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/"><SOAP-ENV:Header></SOAP-ENV:
Header><SOAP-ENV:Body xmlns:ns1="urn:NSConfig"><ns1:login><username>nstest</username>
<password>nstest</password></ns1:login></SOAP-ENV:Body></SOAP-ENV:Envelope>
11:11:35.567226 IP 192.168.1.1.http > 192.168.1.10.40494: P 1:949(948) ack 706 win 57620
[...]
HTTP/1.1 200 OK
Date: Mon, 29 Jun 2009 11:13:08 GMT
Server: Apache
Last-Modified: Mon, 29 Jun 2009 11:13:08 GMT
Status: 200 OK
Content-Length: 622
Connection: close
Set-Cookie:
NSAPI=##F0F402A6574084DB4956184C6443FEE54DD5FC1E1953E3730A5A307BBEC3;Domain=
192.168.1.1; Path=/soap
Content-Type: text/xml; charset=utf-8
<?xml version="1.0" encoding="UTF-8"?><SOAP-ENV:Envelope xmlns:SOAP-ENV=
"http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC=
"http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instanc" xmlns:xsd=
"http://www.w3.org/2001/XMLSchema" xmlns:ns=
"urn:NSConfig"><SOAP-ENV:Header></SOAP-ENV:Header><SOAP-ENV:Body SOAP-ENV:
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" id=
"_0"><ns:loginResponse><return xsi:type=
"ns:simpleResult"><rc xsi:type="xsd:unsignedInt">0</rc><message xsi:type=
"xsd:string">Done</message></return></ns:loginResponse></SOAP-ENV:Body></SOAP-ENV:Envelope>
我们可以看到,对于登录操作的初始请求,我们发送了一个 SOAP 消息,其中我们的凭据被封装为一个 HTTP POST 请求。
响应也是 SOAP 消息,封装在 HTTP 响应中。SOAP 响应没有携带太多有用的信息;它只包含两段数据:一个数字返回代码(rc)和一个字母数字字符串(message)。当一切正常时,rc 设置为 0,message 设置为 Done。
HTTP 头包含更重要的信息:它设置了一个 cookie,我们需要将它用于其他请求:
Set-Cookie: NSAPI=##F0F402A6574084DB4956184C6443FEE54DD5FC1E1953E3730A5A307BBEC3;↲
Domain=192.168.1.1;Path=/soap
这个 cookie 值与我们在 NS 上的帐户相关联,因此 web 服务知道发送这个 cookie 的人已经通过了身份验证。
收集性能统计数据
我们已经为统计数据收集和监控工具建立了以下要求:
- 我们希望我们的工具能够收集以下统计信息:
- CPU 和内存利用率
- 系统概述:请求速率、数据速率、已建立的连接
- 所有虚拟服务器概述:启动/关闭以及每个虚拟服务器中有哪些服务关闭
- 这些可以分为两组:
- 系统状态(CPU、内存和请求率读数)
- 虚拟服务器状态(虚拟服务器状态)
我们现在可以将我们的实现分成两部分,这更容易编码和测试。
读取统计数据及其返回值的 SOAP 方法
表 2-1 列出了我们的统计收集工具中使用的方法,以及每个方法的返回对象的名称和简要描述。我们将在代码中使用其中一些。(您应该能够非常容易地修改代码,并为工具添加更多的查询项。如果您发现自己需要更多关于更具体项目的详细信息,如 AAA、GSLB 或压缩,请参考 Netscaler API 文档,可从 Netscaler 管理网页下载。)
表 2-1 。统计我们示例中使用的 Web 服务方法及其返回值
|
方法
|
返回变量
|
描述
| | --- | --- | --- | | 统计系统 | _internaltemp | 内部系统温度,单位为摄氏度。 | | | _ rescpuusage | 以百分比表示的组合 CPU 使用率。 | | | _memusagepcnt | 以百分比表示的内存使用量。 | | statprotocolhttp | _httprequestsrate | 总 HTTP(S)请求速率(每秒)。 | | statlbvserver | _ 主 IP 地址 | 虚拟服务器的 IP 地址。 | | | _primaryport | 虚拟服务器的端口号 | | | _state | 虚拟服务器的状态:启动:虚拟服务器正在运行。DOWN:虚拟服务器中的所有服务都失败。停止服务:虚拟服务器被禁用。 | | | _vslbhealth | 虚拟服务器的运行状况,表示为处于运行状态的服务的百分比。 | | | _requestsrate | 虚拟服务器每秒接收请求的速率。 | | 启动服务 | _ 主 IP 地址 | 虚拟服务器的 IP 地址。 | | | _primaryport | 虚拟服务器的端口号。 | | | _state | 虚拟服务器的状态:启动:服务正在运行。关闭:服务没有在物理服务器上运行。停止服务:服务被禁用。 | | | _requestsrate | 该服务每秒接收请求的速率。 |
读取系统健康数据
读取系统状态数据非常简单;我们需要做的就是调用两个方法:一个检索关于硬件和内存状态的读数,另一个检查由负载均衡器服务的 HTTP 和 HTTPS 请求总数。
从 表 2-1 中我们可以看到,我们将调用 statsystem 和 statprotocolhttp 方法。这两种方法都不需要任何输入参数。清单 2-19 显示了我们的 NSStatApi 类中统计信息收集方法的简化版本。
清单 2-19 。获取系统健康数据
def system_health_check(self):
results = {}
[...]
req = self.module.statsystem()
res = self.soap.statsystem(req)._return
results['temp'] = res._List[0]._internaltemp
results['cpu'] = res._List[0]._rescpuusage
results['mem'] = res._List[0]._memusagepcnt
[...]
req = self.module.statprotocolhttp()
res = self.soap.statprotocolhttp(req)._return
results['http_req_rate'] = res._List[0]._httprequestsrate
[...]
return results
这看起来类似于我们之前执行的登录请求;然而,有一个重要的区别需要注意。这一次我们需要使用 _List 变量来访问我们感兴趣的细节。这是因为所有 response _return 对象都包含两个必需变量和一个可选变量:_rc、_message 或 _List。我们已经知道 _rc 和 _message 包含一个请求返回代码和一个提供关于请求状态更多细节的消息。
但是,_List 是可选的,它是一个数组,可以包含返回对象的一个或多个实例。即使该方法总是返回单个实例,它仍然包含在数组中。这是提供标准通信方式的方法之一:每个请求总是返回相同的变量集,所以如果我们需要,我们可以编写一个标准的 SOAP 请求调度程序/响应处理程序。
我们如何找出列表中返回了哪些结构对象?这很简单。首先,您需要在 NSStat_services_types.py 模块中查找包含 SOAP 通信中使用的所有数据类型的 methodname 响应类。在我们的例子中,我们正在搜索 statsystemResult_Def 类。
一旦我们找到了它,我们需要寻找类型定义,如下所示:
TClist = [ZSI.TCnumbers.IunsignedInt(pname="rc", aname="_rc", minOccurs=1,
maxOccurs=1, nillable=False, typed=False, encoded=kw.get("encoded")),
ZSI.TC.String(pname="message", aname="_message", minOccurs=1, maxOccurs=1,
nillable=False, typed=False, encoded=kw.get("encoded")),
GTD("urn:NSConfig","systemstatsList",lazy=False)(pname="List",
aname="_List", minOccurs=0, maxOccurs=1, nillable=False, typed=False,
encoded=kw.get("encoded"))]
现在,我们寻找 systemstatsList 类定义,如清单 2-20 所示。
清单 2-20 。systemstatsList 类定义
class systemstatsList_Def(ZSI.TC.Array, TypeDefinition):
#complexType/complexContent base="SOAP-ENC:Array"
schema = "urn:NSConfig"
type = (schema, "systemstatsList")
def __init__(self, pname, ofwhat=(), extend=False, restrict=False,
attributes=None, **kw):
ofwhat = ns0.systemstats_Def(None, typed=False)
atype = (u'urn:NSConfig', u'systemstats[]')
ZSI.TCcompound.Array.__init__(self, atype, ofwhat, pname=pname,
childnames='item', **kw)
在这个类定义中,我们找到了对实际类的引用,它将包含我们将在 SOAP 响应中收到的所有变量。
最后,在清单 2-21 中,我们搜索 systemstats_Def 类,其中子类 Holder 包含所有可用的变量。
清单 2-21 。systemstats 返回类型的定义
class systemstats_Def(ZSI.TCcompound.ComplexType, TypeDefinition):
[...]
class Holder:
typecode = self
def __init__(self):
# pyclass
self._rescpuusage = None
self._memusagepcnt = None
self._internaltemp = None
[...]
这可能看起来很复杂,但是对于自动化系统来说,访问信息总是相同的模式,这有助于简化过程。
正在读取服务状态数据
检索关于服务的信息非常类似;只是涉及到更多的步骤:
- 我们需要检索 Netscaler 上所有虚拟服务器的列表。这可以通过 statlbvserver 方法实现,该方法接受一个可选的 name 参数。如果指定了该选项,将只返回有关该虚拟服务器的信息。如果没有指定名称或者设置为空,将返回所有虚拟服务器的信息。
- 对于列表中的每个虚拟服务器,我们会创建一个附加到它的服务列表。这实际上需要使用不同的 SOAP 服务 Netscaler 配置 SOAP。Statistics API 不提供查询配置实体之间的依赖关系的功能,因此我们将使用配置 API 中的 getlbvserver 方法。
- 我们检查虚拟服务器健康分数是否为 100 %而不是。如果服务器不在忽略列表中,我们会列出附加到它的不健康服务。我们使用 statservice 方法来检索每个服务的统计信息,如果服务没有处于运行状态,我们会指出这一点。
注意在 Citrix 负载均衡器中,虚拟服务器附带了许多服务来满足用户请求。虚拟服务器的运行状况分数是以虚拟服务器池中活动服务的百分比来计算的。如果虚拟服务器池中包含十项服务,其中两项没有响应运行状况检查,则该虚拟服务器的得分为 80%。
在下面的代码清单中,我展示了实现健康和服务统计信息收集的类和方法。为了保持代码简单,这些例子没有任何错误处理。完整的源代码可以从该书在 www.apress.com 的页面下载,包含了额外的错误处理和报告功能。
首先,在清单 2-22 中,我们定义了一个新的统计 API 包装类,它实现了两个方法:get_vservers_list 和 get_service_details。该类继承了我们前面定义的 NSSoapApi 基类的所有函数。
清单 2-22 。统计 API 包装类
class NSStatApi(NSSoapApi):
[...]
def get_vservers_list(self, name=''):
result = {}
req = self.module.statlbvserver()
req._name = name
res = self.soap.statlbvserver(req)._return
for e in res._List:
result[e._name.strip('"')] = { 'ip': e._primaryipaddress,
'port': e._primaryport,
'status': e._state,
'health': e._vslbhealth,
'requestsrate': e._requestsrate, }
return result
def get_service_details(self, service):
result = {}
req = self.module.statservice()
req._name = service
res = self.soap.statservice(req)._return
result = { 'ip': res._List[0]._primaryipaddress,
'port': res._List[0]._primaryport,
'status': res._List[0]._state,
'requestsrate': res._List[0]._requestsrate, }
return result
get_vservers_list 方法调用 statlbvserver SOAP 方法,并传递一个可选的 name 参数。如果名称字符串为空,将返回所有虚拟服务器的列表。当列表返回时,我们用完整列表中的几个条目创建自己的字典。
get_service_details 方法调用 statservice SOAP 方法,并将服务名作为参数传递。SOAP 响应由关于服务的详细信息组成。我们将只提取我们感兴趣的信息,并将其作为 Python 字典返回。
在清单 2-23 中,我们定义的第二个类是一个配置 API 包装类。这个类应该主要用于处理负载平衡器配置的函数,但是我们需要从这个服务中调用一个函数:getlbvserver。该函数返回(除了关于虚拟服务器的其他细节之外)绑定到特定虚拟服务器的所有服务的列表。我们的方法叫做 get_services_list,它只是以 Python 列表的形式返回结果,以服务名作为元素。
清单 2-23 。配置 API 包装类
class NSConfigApi(NSSoapApi):
def get_services_list(self, vserver):
req = self.module.getlbvserver()
req._name = vserver
res = self.soap.getlbvserver(req)._return
result = [e.strip('"') for e in res._List[0]._servicename]
return result
最后,在清单 2-24 中,我们实现了我们的查询函数,它执行以下步骤:
- 初始化两个类的实例。
- 检索所有虚拟服务器的列表。
- 如果虚拟服务器运行状况不是 100%,则获取绑定到它的服务列表。
- 打印出所有不健康的服务。
清单 2-24 。正在检索服务状态数据
ns = NSStatApi([...])
ns_c = NSConfigApi([...])
for (vs, data) in ns.get_vservers_list(name=OPTS.vserver_query).iteritems():
if (data['status'] != 'UP' or data['health'] != 100) and
vs not in config.netscalers['primary']['vserver_ignore_list'] or
OPTS.verbose:
print " SERVICE: %s (%s:%s)" % (vs, data['ip'], data['port'])
print " LOAD: %s req/s" % data['requestsrate']
print " HEALTH: %s%%" % data['health']
for srv in sorted(ns_c.get_services_list(vs)):
service = ns.get_service_details(srv)
if service['status'] != 'UP' or OPTS.vserver_query or OPTS.verbose:
print ' * %s (%s:%s) - %s (%s req/sec)' % (srv, service['ip'],
service['port'],
service['status'],
service['requestsrate'])
以下是该工具的示例输出。根据您的负载平衡器配置以及虚拟服务器和服务的运行状态,您显然会得到不同的结果。
在本例中,第一部分显示了负载平衡器的基本健康信息:内存使用、CPU 使用、温度和 HTTP 请求总数。第二部分显示关于不完全健康的服务的信息。该服务应该有 30 个服务在运行,但其中两个被标记为关闭:
$ ./ns_stat.py
**************************************************
Health check for loadbalancer: 192.168.1.1
Memory usage: 6.434952%
CPU usage: 15%
Temperature: 47C
Requests: 4926/sec
------------------------------
SERVICE: main_web_server (192.168.0.5:80)
LOAD: 1140 req/s
HEALTH: 92%
* web_farm_service-13 (192.168.2.13:80) - DOWN (0 req/sec)
* web_farm_service-14 (192.168.2.14:80) - DOWN (0 req/sec)
------------------------------
$
自动化一些管理任务
我们练习的第二部分是为我们的负载平衡器创建一个管理工具。回到我们最初的需求,我们知道我们希望配置工具执行以下任务:
- 禁用/启用任何可用虚拟服务器的所有服务。
- 禁用/启用任何单独的服务。
- 禁用/启用任何一组服务(可能跨多个虚拟服务器)。
设备配置 SOAP 方法
配置 API 提供了 2500 多种不同的方法来改变负载平衡器的配置。配置负载平衡器通常是一项复杂的任务,远远超出了本书的范围。不过,在本节中,我将展示如何获取服务列表以及如何启用和禁用它们。其他函数的行为方式类似,所以如果您需要创建一个新的虚拟服务器,您只需调用适当的函数。
表 2-2 列出了我们将在配置工具中使用的方法,以及每个方法的返回变量和描述。
表 2-2 。用于启用和禁用服务器的方法
|
方法
|
返回变量
|
描述
| | --- | --- | --- | | 禁用服务 | _rc | 操作的返回代码(simpleResult 类型);如果成功,则为 0。 | | | _ 消息 | 结果的详细说明(simpleResult 类型)。如果成功,则“完成”;否则会提供有意义的解释。 | | 启用服务 | _rc | 操作的返回代码(simpleResult 类型);如果成功,则为 0。 | | | _ 消息 | 结果的详细说明(simpleResult 类型)。如果成功,则“完成”;否则会提供有意义的解释。 | | getlbvserver | _ 服务名称 | 绑定到特定虚拟服务器的所有服务的列表。 |
如您所见,启用和禁用服务的前两种方法的响应非常简单:要么成功,要么失败。就像 login 方法一样,它们返回一个数据结构 simpleResponse,这个数据结构只包含一个返回代码和一个详细的错误描述,以防失败。
最后一个方法是 getlbvserver,我们在上一节中使用它来检索绑定到虚拟服务器的所有服务的列表。这里将使用相同的方法包装器。
设置服务状态
设置服务的状态非常简单,只需调用 enableservice 或 disableservice,并将服务名作为方法调用的参数。Citrix Netscaler 负载平衡器服务和虚拟服务器名称不区分大小写,因此在调用任一方法时,您都不需要考虑为 name 参数设置正确的大小写。
我们在 NSConfigApi 类中定义了另一个函数,它将实现状态之间的切换,并将两个 SOAP 函数包装到一个方便、易用的类方法中。我们将这个方法称为 set_service_state,它将接受两个必需的参数:一个新的状态和一个 Python 数组,该数组包含我们想要更改其状态的所有服务的名称。清单 2-25 显示了代码。
清单 2-25 。SOAP enableservice 和 disableservice 函数的包装器
def set_service_state(self, state, service_list, verbose=False):
[...]
for service in service_list:
if verbose:
print 'Changing state of %s to %sd... ' % (service, state)
req = getattr(self.module, '%sservice' % state)()
req._name = service
res = getattr(self.soap, '%sservice' % state)(req)._return
[...]
return
如你所见,这是一个简单的函数;然而,它包含了一件值得多加注意的事情:我们没有明确地指定我们所调用的方法的名称;它是在运行时根据我们在状态变量中收到的参数值自动构造的。
为了实现这一点,我们使用 Python getattr 函数,该函数允许我们在运行时获取对对象属性的引用,而无需事先知道属性名。当我们调用 getattr 时,我们提供两个参数:一个对象的引用和我们正在处理的属性的名称。因此,我们对方法的显式调用如下所示:
result = some_object.some_function()
这相当于:
result = getattr(some_object, "some_function")()
请务必注意 getattr 调用后的()。getattr 返回值是对对象的引用,因此不执行函数。如果我们正在访问一个对象变量,它将返回该变量的值,但是如果我们正在访问一个函数,我们将只获得对它的引用:
>>> class C():
... var = 'test'
... def func(self):
... print 'hello'
...
>>> o = C()
>>> getattr(o, 'var')
'test'
>>> getattr(o, 'func')
<bound method C.func of <__main__.C instance at 0xb7fe038c>>
>>> getattr(o, 'func')()
hello
>>>
此方法通常用于实现 dispatcher 功能,我们也在代码中使用它,而不是显式测试 state 参数,如下所示:
if state == 'enable':
req = self.module.enableservice()
req._name = service
res = self.soap.enableservice(req)._return
elif state == 'disable':
req = self.module.disableservice()
req._name = service
res = self.soap.disableservice(req)._return
此时,我们构造函数的名称,并自动调用它:
req = getattr(self.module, '%sservice' % state)()
req._name = service
res = getattr(self.soap, '%sservice' % state)(req)._return
这是一项强大的技术,它使您的代码可读性更强,更易于维护。在前面的例子中,我们将行数从八行减少到只有三行。但是,有一个警告:我们可能会引用一个不存在的属性。在我们的示例中,我们必须确保状态设置为“启用”或“禁用”;否则,getattr 将返回 None 作为结果。
关于日志记录和错误处理
虽然它们不影响我们的工具或 API 访问库的功能,但是实现基本的日志记录、错误报告和错误处理是很重要的。在编写代码的每个阶段,我们需要预测所有可能的结果,特别是如果我们使用外部库和/或外部服务,比如 SOAP API。
使用 Python 日志记录模块
不管我们的项目有多大,尽可能多地报告代码中发生的事情的细节是一个好的做法。Python 附带了一个内置的日志模块,该模块灵活且可配置,并且易于使用。
日志记录级别和范围
Python 日志模块提供了五个级别的详细信息。表 2-3 提供了何时使用每个等级的详细信息。
表 2-3 。日志记录级别以及何时应该使用每个
|
水平
|
何时使用
| | --- | --- | | 调试 | 顾名思义,这个日志记录级别是为了调试目的。使用 DEBUG 记录尽可能多的信息;这个级别的消息应该包含足够的细节,以便您识别代码可能存在的问题。 | | 信息 | 这是一个不太详细的级别,通常用于记录系统生命周期中的关键事件,比如联系外部服务或调用相当复杂的子系统。 | | 警告 | 报告此日志记录级别的所有意外事件。凡是没有危害,但是出格的,都要在这里举报。例如,如果没有找到配置文件,但是我们有默认设置,我们应该发出警告。 | | 错误 | 此级别用于记录任何阻止我们完成给定任务但仍允许我们继续完成剩余任务的事件。例如,如果我们需要检查五台虚拟服务器的状态,但其中一台找不到,我们就报告这是一个错误,并继续检查其他服务器。 | | 批评的 | 如果我们不能继续下去,我们用这个日志级别记录错误并退出。此时不需要提供详细信息;说到故障排除,我们就切换到较低级别的,比如 DEBUG。 |
考虑我们日志记录的范围和目的是很重要的。我们必须区分工具的常规输出和日志记录。常规输出和报告是该工具的主要功能,因此不能与来自应用的日志消息混淆。我们也可以选择使用日志模块来编写应用输出消息,但是它们需要进入不同的流。应用日志纯粹是为了报告应用的状态。
例如,如果我们无法连接到负载平衡器,我们必须将其记录为严重事件并退出。换句话说,我们的工具发生了一些问题,导致它无法完成操作。但是,如果我们获得温度读数,并确定它高于正常值,我们不能在日志流中将其记录为临界,因为高系统温度与我们的应用无关。不管负载平衡器的健康状况如何,我们的工具都可以正常运行。继续这个例子,我们可能决定要么简单地打印警告消息,要么将它记录在其他流中,可能称为 loadbalancers_health.log。
配置和使用记录器
根据我们想要实现的目标,日志配置可以简单也可以复杂。我倾向于不使它过于复杂,尽可能保持简单。在一天结束的时候,在我们的日志配置中,我们只需要几样东西:
- 测井级别。我们希望我们的记录器产生多少输出?如果工具是成熟的、经过良好测试的、稳定的,实际上我们会将日志级别设置为错误,但是如果我们正在开发,我们可能会坚持调试。
- 日志目的地。我们希望在屏幕上还是在文件中记录消息?最好将它写到一个文件中,特别是当我们使用多个记录器时,一个用于应用状态消息,另一个用于我们正在管理或监控的系统。
- 日志消息格式。默认的日志记录程序消息格式信息不多,所以我们可能希望向它添加额外的字段,这很容易实现。
幸运的是,日志模块提供了一个 basicConfig 方法,允许我们通过一个函数调用来设置所有这些:
import logging
logging.basicConfig(level=logging.DEBUG, filename='NSLib.log',
format="%(asctime)s [%(levelname)s] (%(funcName)s() (%(filename)s:%(lineno)d)) %(message)s")
正如您可能已经猜到的,设置日志记录级别是微不足道的;我们只需要使用一个已定义的内部变量,其名称与我们之前使用的日志级别名称相匹配:DEBUG、INFO、WARNING、ERROR 或 CRITICAL。日志输出目标只是一个文件名。如果我们不指定任何文件名,日志模块将使用标准输出(stdout)来写入所有消息。
日志格式有点复杂。必须按照 Python 字符串格式规则定义格式,假设正确的参数是字典。使用散列数组中的参数格式化 Python 中的字符串的标准约定如下:
>>> string = "%(var1)s %(var2)d %(var3)s" % {'var1': 'I bought', 'var2': 3, 'var3':
'sausages'}
>>> print string
I bought 3 sausages
>>>
正如在我们的例子中,日志模块期望在%操作符的左边有一个格式化的字符串,并提供一个标准的预填充字典作为右边的参数。表 2-4 列出了在测井格式字符串中使用的最有用的参数。
表 2-4 。可在日志格式字符串中使用的预定义字典字段
|
水平
|
描述
| | --- | --- | | %(asctime)s | 以可读形式显示日志消息的时间,例如 2009-07-07 14:04:39,462。逗号后面的数字是以毫秒为单位的时间部分。 | | %(levelname)s | 表示日志级别的字符串。可能的默认值:调试、信息、警告、错误或严重。 | | %(funcName)s | 生成日志消息的函数的名称。 | | %(文件名)s | 进行日志记录调用的文件的名称。这不包含文件的完整路径,只包含文件名部分。 | | %(模块)s | 生成日志调用的模块的名称。这与去掉扩展名的文件名相同。 | | %(lineno)d | 发出日志记录调用的文件中的行号。并不总是可用。 | | %(消息)s | 以 msg % args 格式处理的实际日志记录消息:logging.debug(msg,args) |
一旦我们配置了日志模块,使用它就非常简单了—我们所要做的就是初始化日志记录器的一个新实例,并调用它的方法来编写适当的日志消息(所有方法都由适当的日志级别名称调用):
清单 2-26 。初始化新的记录器实例
logging.basicConfig(level=logging.DEBUG, filename='NSLib.log',
format="%(asctime)s [%(levelname)s] (%(funcName)s() (%(filename)s:%(lineno)d)) %(message)s")
logger = logging.getLogger()
logger.critical('Simple message...')
logger.error('Message with one argument: %s', str1)
logger.warning('Message with two arguments. String %s and digit: %d', (msg, val))
try:
not_possible = 1 / 0
except:
logger.critical('An exception has occurred! Stack trace below:', exc_info=True)
如您所见,日志模块非常灵活,而且易于配置。尽可能多地使用它,并尽量避免使用打印语句的旧式日志记录。
处理异常
异常是阻止我们的代码(或者我们的代码正在调用的模块的代码)正确执行并导致执行终止的错误。在我们之前的例子中,在清单 2-26 中,代码失败了,因为我们包含了一个指示 Python 执行被零除的语句,这是不可能的。这引发了一个 ZeroDivisionError 异常,代码的执行在此终止。除非我们使用了 try:...除了:...语句,我们的程序将在这一点上终止。Python 允许我们对异常采取行动,因此我们可以决定如何适当地处理它们。例如,如果我们试图与远程 web 服务建立通信,但是服务没有响应,我们将得到一个“连接超时”异常。如果我们有不止一个服务要查询,我们可能只是报告这是一个错误,并继续其他服务。
捕捉异常很容易:
try:
call_to_some_function()
except:
do_something_about_it()
正如我们在上一节中看到的,我们可以只通过指示我们想要记录日志记录器函数调用的异常细节来记录完整的异常堆栈跟踪。在我的代码示例中,我使用下面的结构来检测异常,记录它,并传递它。如果您正在编写一个模块,并且您不能真正决定如何处理发生的异常,这是处理它们的方法之一:
try:
module.function()
except:
logger.error('An exception has occurred while executing module.function()',
exc_info=True)
raise
还可以捕捉特定的异常,并对每个异常执行不同的操作:
try:
result = divide_two_numbers(arg1, arg2)
except ZeroDivisionError:
# if this happens, we will return 0
logger.error('We attempted to divide by zero, setting result to 0')
result = 0
except:
# something else has happened, so we reraise it
logger.critical('An exception has occurred while executing module.function()',
exc_info=True)
raise
如果您正在编写自己的模块,您可能会决定引入特定于该模块的异常,这样就可以相应地捕捉和处理它们。我在 NSLib.py 模块中使用了这种技术。自定义异常必须从泛型异常类派生。如果您不需要任何特定的功能,您可以将新的异常定义为以下类:
class NSLibError(Exception):
def __init__(self, error_message):
self.error_message = error_message
def __str__(self):
return repr(self.error_message)
一旦定义了异常类,就可以通过调用 raise 运算符并传递该异常类的对象实例来引发它:
class NSSoapApi(object):
def __init__(self, module=None, hostname=None, username=None, password=None):
[...]
if not (hostname and username and password):
self.logger.critical('One or more from the following: hostname, username and password, are undefined')
raise NSLibError('hostname, username and password must be defined')
尽管这不是必需的,但遵循异常类约定是一种好的做法,该约定规定所有异常类名都应以 Error 结尾。除非模块很大,并且实现明显不同的功能,否则您可能只需为每个模块或一组模块定义一个异常。
NetScaler NITRO API
现在我们知道了如何使用 SOAP API 来管理 NetScaler 设备,让我们快速了解一下管理这些负载平衡器的替代方法。从 9.x 版本开始,Citrix 引入了基于 REST 的 API,称为 Nitro API。从 NetScaler 操作系统的 10.5 版本开始,Citrix 还提供了一个用于访问和使用 Nitro API 的 Python 模块。本章的其余部分展示了如何使用这个模块。
[计] 下载
首先,我们需要获得模块和文档文件。我们可以从正在运行的 NetScaler 设备中检索它们:
- 登录 netscaler web 控制台。
- 点击“下载”
- 下载“NITRO API SDK for Python”(NITRO-Python . tgz)。
下载完模块和文档归档文件后,我们需要对它们进行解压缩:
$ tar zxf nitro-python.tgz
$ ls -l
total 27724
-rw-r--r-- 1 rytis rytis 7700769 Aug 21 17:35 nitro-python.tgz
-rwxr-xr-- 1 rytis rytis 20684800 Jul 3 20:52 ns_nitro-python_tagma_50_10.tar
$ rm nitro-python.tgz
$ tar xf ns_nitro-python_tagma_50_10.tar
$ ls -l
total 20204
drwxr-xr-x 7 rytis rytis 4096 Jul 3 20:26 nitro-python-1.0
-rwxr-xr-- 1 rytis rytis 20684800 Jul 3 20:52 ns_nitro-python_tagma_50_10.tar
$ ls -l nitro-python-1.0/
total 52
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 doc
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 lib
-r-x------ 1 rytis rytis 10351 Jul 3 19:44 License.txt
-r-x------ 1 rytis rytis 109 Jul 3 19:44 MANIFEST.in
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 nitro_python.egg-info
drwxr-xr-x 3 rytis rytis 4096 Jul 3 20:26 nssrc
-rw-r--r-- 1 rytis rytis 353 Jul 3 20:26 PKG-INFO
-r-x------ 1 rytis rytis 1054 Jul 3 19:44 readme_start.txt
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 sample
-rw-r--r-- 1 rytis rytis 59 Jul 3 20:26 setup.cfg
-r-x------ 1 rytis rytis 1573 Jul 3 19:44 setup.py
为了安装和检查依赖项,nitro-python 包依赖于 requests 模块,但是所需的文件在同一个包中提供,所以我们不需要担心单独安装它们:
$ cd nitro-python-1.0
$ python setup.py install
$ pip freeze
argparse==1.2.1
distribute==0.6.24
nitro-python==1.0
requests==2.3.0
wsgiref==0.1.2
$
使用 Nitro-Python 模块
模块布局
Citrix Netscaler Python API 包的布局略有不同,它遵循典型的 Java 打包模式,将类和方法分成许多子包。我们不打算讨论这在布局 Python 项目中是否是一个好的实践;不过,我发现这有点不寻常,它导致了大量的、有时不方便的导入语句,我们将在后面看到。
如果我们检查包的目录结构,我们会看到子包结构是多么的细粒度,有许多目录(每个目录都是一个独立的子包)和很少的模块。每个模块通常只包含一个或两个在每个模块中定义的类。我们可以通过在 nitro-python-1.0 目录下运行以下命令来确认这一点:
$ nitro-python-1.0/
$ find nssrc/ -name \*.py -exec grep -c ^class {} \; -print
主包中有许多模块文件,这表明代码很可能是自动生成的:
$ cd nitro-python-1.0/
$ find nssrc/ -name \*.py -not -name __init__.py | wc -l
1132
那么,我们怎样才能找到我们需要的类或方法呢?
API 函数分为两个主要部分:
- 配置。此类别中的功能用于主动管理 Netscaler 设备。
- 统计。此类别中的功能用于从 Netscaler 设备收集统计数据。
每个类别中的项目列表几乎相同,因为可以配置的项目通常也可以被监控。表 2-5 显示每个类别中项目的逻辑组。
表 2-5 。功能的逻辑分组
|
配置
|
统计数字
|
描述
| | --- | --- | --- | | 事件 | - | 用于订阅和发布 Netscaler 事件的事件框架 | | 美国汽车协会 | 美国汽车协会 | 认证、授权和记账服务 | | 应用 | - | 应用资源配置 | | 应用流加速 | 应用流加速 | AppFlow 资源 | | 应用防火墙 | 应用防火墙 | 应用防火墙 | | 云应用平台 | 云应用平台 | 应用级别的体验质量 | | 审计 | 审计 | 审计资源 | | 证明 | 证明 | 认证资源 | | 批准 | 批准 | 授权服务 | | 自动缩放 | 自动缩放 | 自动缩放 | | 基础 | 基础 | 基本系统配置资源 | | 钙 | 钙 | 内容加速器服务 | | 集成缓存 | 集成缓存 | 集成缓存服务 | | 串 | 串 | Netscaler 集群管理 | | 压缩 | 压缩 | HTTP 压缩服务 | | 缓存重定向 | 缓存重定向 | HTTP 缓存管理服务 | | 内容切换 | 内容切换 | 内容感知流量管理服务 | | 分贝 | - | 数据库用户配置 | | 域名服务 | 域名服务 | DNS 管理 | | HTTP DoS 保护 | HTTP DoS 保护 | HTTP 拒绝服务保护服务 | | 前端优化 | 前端优化 | Web 内容优化服务 | | 过滤器 | - | 请求内容过滤配置 | | 全局服务器负载平衡 | 全局服务器负载平衡 | 全球服务器负载平衡服务 | | 高可用性 | 高可用性 | Netscaler 高可用性配置资源 | | 安全协议 | 安全协议 | IPsec 管理 | | 负载平衡 | 负载平衡 | 负载平衡管理资源 | | 线性低密度聚乙烯 | 线性低密度聚乙烯 | 链路层发现协议资源 | | 网络 | 网络 | 网络配置管理 | | 纳秒 | 纳秒 | 全局系统配置资源 | | 标准温度和压力 | - | 系统 NTP 配置 | | 政策 | - | 系统策略配置 | | 优先排队 | 优先排队 | 优先排队服务 | | 草案 | 草案 | 协议管理 | | - | 服务质量 | 服务质量统计数据 | | 回答者 | 回答者 | 响应服务 | | 重写 | 重写 | HTTP 重写服务 | | 升高 | - | 远程集成服务引擎配置 | | 路由器 | - | 路由器配置 | | 当然连接 | 当然连接 | SureConnect 服务 | | 简单网络管理协议(Simple Network Management Protocol) | 简单网络管理协议(Simple Network Management Protocol) | 简单网络管理协议服务 | | 溢出 | 溢出 | 溢出管理资源 | | 加密套接字协议层 | 加密套接字协议层 | 安全套接字层配置资源 | | 溪流 | 溪流 | 连接流管理资源 | | 系统 | 系统 | 系统配置管理资源 | | 交通管理 | 交通管理 | 交通服务/政策管理资源 | | 改变 | 改变 | URL 转换资源 | | 隧道 | - | SSL VPN 隧道管理 | | 效用 | - | 系统技术支持工具 | | 虚拟专用网 | 虚拟专用网 | 虚拟专用网管理资源 | | 网络界面 | - | Netscaler Web 界面配置 |
每个组包含处理系统的相同方面的资源;例如,负载平衡组包含处理负载平衡器资源配置、虚拟服务器资源配置等的所有资源。要了解所有详细信息,您必须从正在运行的设备下载 NetScaler NITRO API 文档档案,并使用 web 浏览器阅读文档。它设计得很好,信息也很容易找到。
所有 NetScaler 资源都在 ns src . com . Citrix . NetScaler . nitro . resource 包的子包中定义。配置资源可以在 ns src . com . Citrix . netscaler . nitro . resource . config 包中的包中找到(例如,LBserver 资源定义在 ns src . com . Citrix . netscaler . nitro . resource . config . lb . lbv server 模块中),统计资源可以在 ns src . com . Citrix . netscaler . nitro . resource . stats 包中的包中找到(例如,LBserver 统计资源可以在 ns src . com . Citrix . netscaler . nitro . resource . stat 中找到
不幸的是,很难找到正确的方法,在大多数情况下,我们必须遵循以下清单:
- 确定我们是想要获取统计数据还是进行配置更改。
- 如果我们正在读取统计数据,那么我们会对 ns src/com/Citrix/netscaler/nitro/resource/stats 目录中包含的模块感兴趣。
- 如果我们正在配置资源,那么我们应该查看位于 ns src/com/Citrix/netscaler/nitro/resource/config 目录中的模块。
- 服务管理资源位于 ns src/com/Citrix/netscaler/nitro/service 目录中。
- 工具类位于 ns src/com/Citrix/netscaler/nitro/util 目录中。这些主要供图书馆内部使用,但我们可能会发现它们也很有用。
- 异常定义位于 ns src/com/Citrix/netscaler/nitro/中。
- 一旦我们确定了包含与我们要做的事情相关的功能的包,我们必须手动定位相关的模块,并找出我们需要使用的资源的名称。
继续 LBservice 配置示例,假设我们想要找到与 LBservice 配置相关的方法。我们知道它们位于/ns src/com/Citrix/netscaler/nitro/resource/config 中,因此我们转到该目录并列出所有子目录:
$ cd nssrc/com/citrix/netscaler/nitro/resource/config
$ ls -l
total 200
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 aaa
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 app
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 appflow
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 appfw
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 appqoe
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 audit
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 authentication
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 authorization
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 autoscale
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 basic
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 ca
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 cache
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 cluster
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 cmp
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 cr
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 cs
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 db
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 dns
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 dos
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 Event
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 feo
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 filter
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 gslb
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 ha
-rw-r--r-- 1 rytis rytis 449 Jul 3 20:04 __init__.py
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 ipsec
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 lb
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 lldp
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 network
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 ns
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 ntp
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 policy
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 pq
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 protocol
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 responder
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 rewrite
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 rise
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 router
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 sc
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 snmp
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 spillover
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 ssl
drwxr-xr-x 2 rytis rytis 4096 Aug 28 11:55 stream
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 system
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 tm
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 transform
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 tunnel
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 utility
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 vpn
drwxr-xr-x 2 rytis rytis 4096 Jul 3 20:26 wi
我们可以看到子目录大致与 表 2-5 中的列表相匹配,虽然有些名字是缩写的。在我们的示例中,负载平衡组缩写为“lb”
现在,如果我们转到“lb”目录,我们会发现许多处理负载平衡器资源配置的模块:
$ cd lb
$ ls -l
total 744
-rw-r--r-- 1 rytis rytis 1369 Jul 3 20:04 __init__.py
-rw-r--r-- 1 rytis rytis 3446 Jul 3 20:04 lbgroup_binding.py
-rw-r--r-- 1 rytis rytis 6986 Jul 3 20:04 lbgroup_lbvserver_binding.py
-rw-r--r-- 1 rytis rytis 16062 Jul 3 20:04 lbgroup.py
-rw-r--r-- 1 rytis rytis 3664 Jul 3 20:04 lbmetrictable_binding.py
-rw-r--r-- 1 rytis rytis 7232 Jul 3 20:04 lbmetrictable_metric_binding.py
-rw-r--r-- 1 rytis rytis 9059 Jul 3 20:04 lbmetrictable.py
-rw-r--r-- 1 rytis rytis 3885 Jul 3 20:04 lbmonbindings_binding.py
-rw-r--r-- 1 rytis rytis 6741 Jul 3 20:04 lbmonbindings.py
-rw-r--r-- 1 rytis rytis 7171 Jul 3 20:04 lbmonbindings_service_binding.py
-rw-r--r-- 1 rytis rytis 6665 Jul 3 20:04
lbmonbindings_servicegroup_binding.py
-rw-r--r-- 1 rytis rytis 3055 Jul 3 20:04 lbmonitor_args.py
-rw-r--r-- 1 rytis rytis 3548 Jul 3 20:04 lbmonitor_binding.py
-rw-r--r-- 1 rytis rytis 8153 Jul 3 20:04 lbmonitor_metric_binding.py
-rw-r--r-- 1 rytis rytis 103635 Jul 3 20:04 lbmonitor.py
-rw-r--r-- 1 rytis rytis 7894 Jul 3 20:04 lbmonitor_service_binding.py
-rw-r--r-- 1 rytis rytis 7954 Jul 3 20:04 lbmonitor_servicegroup_binding.py
-rw-r--r-- 1 rytis rytis 16051 Jul 3 20:04 lbparameter.py
-rw-r--r-- 1 rytis rytis 1102 Jul 3 20:04 lbpersistentsessions_args.py
-rw-r--r-- 1 rytis rytis 8354 Jul 3 20:04 lbpersistentsessions.py
-rw-r--r-- 1 rytis rytis 8144 Jul 3 20:04 lbroute6.py
-rw-r--r-- 1 rytis rytis 8731 Jul 3 20:04 lbroute.py
-rw-r--r-- 1 rytis rytis 7512 Jul 3 20:04 lbsipparameters.py
-rw-r--r-- 1 rytis rytis 10514 Jul 3 20:04 lbvserver_appflowpolicy_binding.py
-rw-r--r-- 1 rytis rytis 10721 Jul 3 20:04 lbvserver_appfwpolicy_binding.py
-rw-r--r-- 1 rytis rytis 11369 Jul 3 20:04 lbvserver_appqoepolicy_binding.py
-rw-r--r-- 1 rytis rytis 14857 Jul 3 20:04
lbvserver_auditnslogpolicy_binding.py
-rw-r--r-- 1 rytis rytis 14877 Jul 3 20:04
lbvserver_auditsyslogpolicy_binding.py
-rw-r--r-- 1 rytis rytis 10881 Jul 3 20:04
lbvserver_authorizationpolicy_binding.py
-rw-r--r-- 1 rytis rytis 9367 Jul 3 20:04 lbvserver_binding.py
-rw-r--r-- 1 rytis rytis 10474 Jul 3 20:04 lbvserver_cachepolicy_binding.py
-rw-r--r-- 1 rytis rytis 11289 Jul 3 20:04 lbvserver_capolicy_binding.py
-rw-r--r-- 1 rytis rytis 10681 Jul 3 20:04 lbvserver_cmppolicy_binding.py
-rw-r--r-- 1 rytis rytis 7236 Jul 3 20:04 lbvserver_csvserver_binding.py
-rw-r--r-- 1 rytis rytis 11386 Jul 3 20:04 lbvserver_dnspolicy64_binding.py
-rw-r--r-- 1 rytis rytis 6143 Jul 3 20:04 lbvserver_dospolicy_binding.py
-rw-r--r-- 1 rytis rytis 11309 Jul 3 20:04 lbvserver_feopolicy_binding.py
-rw-r--r-- 1 rytis rytis 14777 Jul 3 20:04 lbvserver_filterpolicy_binding.py
-rw-r--r-- 1 rytis rytis 14644 Jul 3 20:04 lbvserver_pqpolicy_binding.py
-rw-r--r-- 1 rytis rytis 124769 Jul 3 20:04 lbvserver.py
-rw-r--r-- 1 rytis rytis 10591 Jul 3 20:04
lbvserver_responderpolicy_binding.py
-rw-r--r-- 1 rytis rytis 10514 Jul 3 20:04 lbvserver_rewritepolicy_binding.py
-rw-r--r-- 1 rytis rytis 14450 Jul 3 20:04 lbvserver_scpolicy_binding.py
-rw-r--r-- 1 rytis rytis 11101 Jul 3 20:04 lbvserver_service_binding.py
-rw-r--r-- 1 rytis rytis 8876 Jul 3 20:04 lbvserver_servicegroup_binding.py
-rw-r--r-- 1 rytis rytis 9325 Jul 3 20:04
lbvserver_servicegroupmember_binding.py
-rw-r--r-- 1 rytis rytis 11429 Jul 3 20:04
lbvserver_spilloverpolicy_binding.py
-rw-r--r-- 1 rytis rytis 14590 Jul 3 20:04
lbvserver_tmtrafficpolicy_binding.py
-rw-r--r-- 1 rytis rytis 10554 Jul 3 20:04
lbvserver_transformpolicy_binding.py
-rw-r--r-- 1 rytis rytis 3448 Jul 3 20:04 lbwlm_binding.py
-rw-r--r-- 1 rytis rytis 6260 Jul 3 20:04 lbwlm_lbvserver_binding.py
-rw-r--r-- 1 rytis rytis 10356 Jul 3 20:04 lbwlm.py
模块名称通常与 NetScaler 命令行界面名称非常匹配,因此如果您熟悉 NetScaler 命令行配置,您应该能够识别包含资源定义的正确模块。例如,lbvserver.py 有一个表示 lbvserver 资源的类定义。
注意如果你只使用了命令行或者 web 界面,你可能不会意识到一些中间资源,比如绑定资源。如果您熟悉 NetScaler 的基本概念,您应该知道可以将服务(服务资源表示在专用主机上运行的任何服务)绑定到 LBvserver (LBvserver 表示虚拟负载平衡服务)。当您将多个服务绑定到虚拟负载平衡服务器时,您实际上是指示 NetScaler 开始将到达 LBvserver 的所有流量转发到绑定到 LBvserver 的服务。绑定服务的 CLI 命令是“bind lb vserverservice name>CLI 不公开的是中间对象,称为“绑定”此绑定对象是 LBvserver 和服务之间的一对一映射。可以把它想象成关系数据库中的多对多表关系。当两个表之间存在多对多关系时,您将创建第三个表,用于分离其他两个表。
注册
我们要做的第一件事就是用 NetScaler 验证我们自己。在幕后,我们发送身份验证细节,负载平衡器用身份验证令牌进行回复,我们将在后续请求中使用该令牌。
为了建立连接,我们需要创建 nitro_service 类的一个实例,用正确的凭证细节初始化它,并调用 login()方法:
>>> from nssrc.com.citrix.netscaler.nitro.service.nitro_service import nitro_service
>>> client = nitro_service('192.168.0.100', 'http')
>>> client.set_credential('nsroot', 'nsroot')
>>> client.timeout = 10
>>> client.isLogin()
False
>>> client.login()
<nssrc.com.citrix.netscaler.nitro.resource.base.base_response.base_response
instance at 0x7ffe352ad098>
>>> client.isLogin()
True
>>>
重要的是,我们要为连接设置超时,如示例所示。如果我们不这样做,默认超时将保持设置为零秒,如果我们尝试建立连接,将会得到以下异常:
>>> client.login()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/rytis/.virtualenvs/nitro-test/local/lib/python2.7/site-packages/nitro_python-1.0-py2.7.egg/nssrc/com/citrix/netscaler/nitro/service/nitro_service.py",
line 220, in login
raise e
requests.exceptions.ConnectionError:
HTTPConnectionPool(host='192.168.0.100', port=80): Max retries
exceeded with url: /nitro/v1/config/login (Caused by <class
'socket.error'>: [Errno 115] Operation now in progress)
一旦我们建立了连接,我们就可以在与 NetScaler 负载平衡器的通信中使用客户端对象。
收集统计数据
如前所述,所有处理从设备收集的统计信息的类都可以在 ns src/com/Citrix/netscaler/nitro/resource/stats/子目录中找到。在本节中,我们将了解如何收集特定于系统和特定于资源(虚拟服务器)的统计数据。
处理特定于系统的数据的类位于 ns src/com/Citrix/netscaler/nitro/resource/stat/system/中。在这个包中,我们可以找到以下模块:
- systembw_stats.py
- systemcpu_stats.py
- systemmemory_stats.py
- system_stats.py
为了获得 CPU 使用情况的详细信息,我们必须使用 systemcpu_stats.py 模块中定义的类。首先,我们需要初始化会话对象。不需要显式调用 login()方法,因为库会自动为我们完成这项工作:
>>> from nssrc.com.citrix.netscaler.nitro.service.nitro_service import nitro_service
>>> from nssrc.com.citrix.netscaler.nitro.resource.stat.system.systemcpu_stats import systemcpu_stats
>>> client = nitro_service('192.168.0.100', 'http')
>>> client.set_credential('nsroot', 'nsroot')
>>> client.timeout = 500
然后,我们创建一个 systemcpu_stats 类的实例。我们必须传递客户端对象,以便 systemcpu_stats 对象知道如何连接到负载平衡器:
>>> cpu_stats = systemcpu_stats.get(client)
在我的实例中,我有一个带有六个 CPU 的设备,因此响应包含六个元素:
>>> len(cpu_stats)
6
最后,我们来看看实际的统计数据:
>>> for c in cpu_stats:
... print c.percpuuse
...
0
2
1
0
2
0
可以看到,设备并不是特别忙。
类似地,我们可以检索关于内存使用的数据:
>>> from nssrc.com.citrix.netscaler.nitro.service.nitro_service import nitro_service
>>> from nssrc.com.citrix.netscaler.nitro.resource.stat.system.systemmemory_stats import systemmemory_stats
>>> client = nitro_service('192.168.0.100', 'http')
>>> client.set_credential('nsroot', 'nsroot')
>>> client.timeout = 500
>>> mem_stats = systemmemory_stats.get(client)
>>> mem_stats[0].memtotallocmb
u'1964'
其他可供读取的有趣属性见 表 2-6 。
表 2-6 。Netscaler 设备特定属性
|
属性名称
|
描述
| | --- | --- | | Shmemallocpcnt | 共享内存百分比。 | | Shmemallocinmb | 共享内存,以兆字节为单位。 | | Shmemtotinmb | 允许分配的总共享内存,以兆字节为单位。 | | Memtotfree | 系统中总的空闲 PE 内存。 | | Memusagepcnt | NetScaler 上的内存利用率百分比。 | | Memtotuseinmb | 使用的 NetScaler 内存总量,以兆字节为单位。 | | Memtotallocpcnt | 当前分配的内存百分比。 | | Memtotallocmb | 当前分配的内存,以兆字节为单位。 | | memtotinmb | 可供数据包引擎(PE)使用的总可用(抓取)内存,以兆字节为单位。 | | Memtotavail | 可供 PE 从系统中获取的总系统内存。 |
您可以在 REST API 文档中找到有关可用属性的详细信息,可以从 NetScaler 管理 Web UI 下载该文档。
如果我们想要检索在我们的负载平衡器设备上运行的虚拟服务器的信息,我们需要使用 ns src . com . Citrix . netscaler . nitro . resource . stat . lb . lbv server _ stats 模块。
首先,让我们检查虚拟服务器的状态。在以下示例中,我们检索所有虚拟服务器的统计数据,然后查看每台服务器的名称和状态:
>>> from nssrc.com.citrix.netscaler.nitro.service.nitro_service
import nitro_service
>>> client = nitro_service('192.168.0.100', 'http')
>>> client.set_credential('nsroot', 'nsroot')
>>> client.timeout = 500
>>> from nssrc.com.citrix.netscaler.nitro.resource.stat.lb.lbvserver_stats import lbvserver_stats
>>> lbvs_stats = lbvserver_stats.get(client)
>>> len(lbvs_stats)
4
>>> for lbvs in lbvs_stats:
... print "%s: %s" % (lbvs.name, lbvs.state)
...
test_1: UP
test_2: UP
test_3: UP
test_4: DOWN
如果我们需要检索单个虚拟服务器的详细信息,我们指定服务器的名称。在这种情况下,结果不是一个列表,而只是一个对象:
>>> lbvs_stats = lbvserver_stats.get(client, name="test_4")
>>> len(lbvs_stats)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'lbvserver_stats' has no len()
>>> lbvs_stats.state
u'DOWN'
有关属性的完整列表,请参考 NetScaler REST API 文档。最有用的属性在表 2-7 中列出。
表 2-7 。虚拟服务器的特定属性
|
属性名称
|
描述
| | --- | --- | | Vsvrsurgecount | 在此 vserver 上等待的请求数。 | | 已建立连接 | 处于已建立状态的客户端连接数。 | | Inactsvcs | 绑定到 vserver 的非活动服务的数量。 | | Vslbhealth | vserver 的运行状况。这给出了绑定到此 vserver 的启动服务的百分比。 | | 小学女教师 | SVM 的 IP 地址。 | | 主端口 | 运行服务的端口。 | | 类型 | 与 vserver 关联的协议。 | | 状态 | 服务器的当前状态。可能的值有启动、关闭、未知、OFS(停止服务)、TROFS(停止服务的转换)、TROFS_DOWN(停止服务时关闭)。 | | actsvcs | 绑定到 vserver 的活动服务的数量。 | | 托菲特 | vserver 命中总数。 | | hitstate | tothits 的速率(/s)计数器。 | | 请求总数 | 此服务或虚拟服务器上接收的请求总数。(这适用于 HTTP/SSL 服务和服务器。) | | 请求率 | totalrequests 的速率(/s)计数器。 | | 总计响应 | 在此服务或虚拟服务器上接收的响应数。(这适用于 HTTP/SSL 服务和服务器。) | | 响应状态 | totalresponses 的速率(/s)计数器。 | | totalrequestbytes | 此服务或虚拟服务器上接收的请求字节总数。 | | requestbytesrate | totalrequestbytes 的速率(/s)计数器。 | | totalresponsebytes | 此服务或虚拟服务器接收的响应字节数。 | | responsebytestate | totalresponsebytes 的速率(/s)计数器。 | | totalpktsrecvd | 此服务或虚拟服务器接收的数据包总数。 | | pktsrecvdrate | totalpktsrecvd 的速率(/s)计数器。 | | totalpktssent | 发送的数据包总数。 | | pktssentrate | totalpktssent 的速率(/s)计数器。 | | 电路连接 | 当前客户端连接数。 | | 光标连接 | 虚拟服务器后面的实际服务器的当前连接数。 |
我希望这些信息为您指明了正确的道路,并使您能够找到 NetScaler 设备上可用的大量统计数据。
执行管理任务
寻找管理任务方法同样容易。所有方法都在 ns src/com/Citrix/netscaler/nitro/resource/config/目录中的可用模块中定义。以下示例显示了如何禁用和启用任何特定的服务器:
>>> from nssrc.com.citrix.netscaler.nitro.service.nitro_service import nitro_service
>>> from nssrc.com.citrix.netscaler.nitro.resource.config.basic.server import server
>>> client = nitro_service('192.168.0.100', 'http')
>>> client.set_credential('nsroot', 'nsroot')
>>> srv_obj = server.get(client, name="test_srv_1")
>>> srv_obj.state
u'ENABLED'
>>> srv_obj.state = 'DISABLED'
>>> server.update(client, srv_obj)
>>> >>> srv_obj = server.get(client, name="test_srv_1")
>>> srv_obj.state
u'DISABLED'
摘要
本章演示了如何使用 Python 访问 SOAP API 来监控和管理 Citrix Netscaler 负载平衡器。它还介绍了如何组织您自己的项目,如何组织您的代码,以及如何处理错误和报告模块的功能状态。提出了以下几点:
- SOAP API 是一种调用远程服务器上的过程的方法,也称为 web 服务。
- SOAP 协议为服务提供者和消费者之间的信息交换定义了一个消息结构。
- SOAP 消息使用 XML 语言来结构化数据。
- 底层或载体协议是 HTTP。
- WSDL 用于描述 web 服务上可用的所有服务以及调用/响应消息中使用的数据结构。
- 可以使用 wsdl2py 工具将 WSDL 定义转换为 Python 助手模块。
- 在开始编码之前定义需求是很重要的。
- 错误和异常必须得到适当的处理。
- 日志模块用于记录消息,并按严重性对其进行分组。
- 从版本 10.5 开始,nitro-python 包可用于访问 NetScaler 上的 REST API。**