网络自动化实践指南-全-

网络自动化实践指南(全)

原文:zh.annas-archive.org/md5/b7a5db6c0b793e5e9dad5e00a5c7cf54

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

网络自动化是使用 IT 控制来监督和执行日常网络管理功能的应用。它在网络虚拟化技术和网络功能中发挥着关键作用。

本书首先提供网络自动化、SDN 以及网络自动化各种应用的介绍,包括将 DevOps 工具集成到网络中以实现高效自动化。然后,它指导你完成不同的网络自动化任务,并涵盖各种数据挖掘和报告方法,如 IPv6 迁移、数据中心搬迁和接口解析,同时保持安全性并提高数据中心鲁棒性。接着,本书转向 Python 的使用以及 SSH 密钥的管理,用于机器到机器(M2M)通信,所有这些都伴随着实际用例。它还涵盖了 Ansible 在网络自动化中的重要性,包括自动化最佳实践、使用不同工具测试自动化网络的方法以及其他重要技术。

到本书结束时,你将熟悉网络自动化的各个方面。

本书涵盖的内容

第一章,基本概念,介绍了如何开始自动化。

第二章,网络工程师的 Python,介绍了 Python 作为脚本语言,并提供示例来解释 Python 在访问网络设备和从设备输出中解析数据的使用。

第三章,从网络访问和挖掘数据,介绍了在保持安全性和提高数据中心鲁棒性的同时,提供按需自助容量和资源。

第四章,自动化触发器的 Web 框架,讨论了如何对自动化框架进行可扩展调用并生成自定义和动态的 HTML 页面。

第五章,网络自动化中的 Ansible,解释了如何虚拟化 Oracle 数据库并动态扩展以确保达到服务水平。

第六章,网络工程师的持续集成,概述了网络工程师的集成原则,以管理快速增长并实现高可用性和快速灾难恢复。

第七章,网络自动化中的 SDN 概念,讨论了将企业 Java 应用程序迁移到虚拟化的 x86 平台,以更好地利用资源,并简化生命周期和可伸缩性管理。

你需要为本书准备的内容

本书的硬件和软件要求包括 Python(3.5 及以上版本)、IIS、Windows、Linux、Ansible 安装和 GNS3(用于测试)或真实路由器。

您需要互联网连接来下载 Python 库。此外,还需要具备 Python 的基本知识、网络知识以及像 IIS 这样的 Web 服务器的基本熟悉度。

本书面向对象

如果您是一位寻找广泛指南以帮助您高效自动化和管理网络的网络工程师,那么这本书就是为您准备的。

术语约定

在本书中,您将找到许多不同的文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名显示如下:“从安装目录中,我们只需要调用python.exe,这将调用 Python 解释器。”

代码块设置为以下格式:

#PowerShell sample code
$myvalue=$args[0]
write-host ("Argument passed to Powershell is "+$myvalue)

任何命令行输入或输出都按照以下方式编写:

python checkargs.py 5 6

新术语重要词汇以粗体显示。

警告或重要注意事项看起来像这样。

小贴士和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说非常重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。要发送给我们一般性的反馈,请简单地发送电子邮件至 feedback@packtpub.com,并在邮件的主题中提及书的标题。如果您在某个领域有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南,网址为 www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 图书的骄傲拥有者,我们有一些东西可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您的账户中下载本书的示例代码文件,网址为 www.packtpub.com。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。您可以通过以下步骤下载代码文件:

  1. 使用您的电子邮件地址和密码登录或注册我们的网站。

  2. 将鼠标指针悬停在顶部的“支持”标签上。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书的名称。

  5. 选择您想要下载代码文件的书籍。

  6. 从下拉菜单中选择您购买这本书的地方。

  7. 点击“代码下载”。

文件下载完成后,请确保您使用最新版本解压或提取文件夹:

  • WinRAR / 7-Zip for Windows

  • Zipeg / iZip / UnRarX for Mac

  • 7-Zip / PeaZip for Linux

本书的相关代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Practical-Network-Automation。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。请查看它们!

下载本书的颜色图像

我们还为您提供了一个包含本书中使用的截图/图表的颜色图像的 PDF 文件。这些颜色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/PracticalNetworkAutomation_ColorImages.pdf下载此文件。

勘误

尽管我们已经尽最大努力确保内容的准确性,错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以避免其他读者感到沮丧,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

侵权

互联网上版权材料的侵权是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。请通过提供涉嫌侵权材料的链接,通过copyright@packtpub.com联系我们。我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

询问

如果您对本书的任何方面有问题,您可以通过questions@packtpub.com联系我们,我们将尽力解决问题。

第一章:基本概念

本章介绍了网络自动化的概念,并使您熟悉自动化框架中的关键词。在我们深入探讨网络自动化的细节之前,了解为什么我们需要网络自动化以及如果我们接受自动化概念和框架,我们可以实现什么,这一点非常重要。本章还提供了对传统工程师和支持运营模式的洞察,并展示了网络自动化如何帮助缩小这一差距,以实现更高的效率和可靠性。

本章涵盖的一些主题包括以下内容:

  • 什么是网络自动化?

  • DevOps

  • 软件定义网络

  • OpenFlow 基础知识

  • 基本编程概念

  • 自动化编程语言选择

  • REST 框架简介

网络自动化

自动化,正如其词义所示,是通过理解、解释和创建逻辑来自动化特定任务的框架。这包括增强手动执行的任务的当前能力,同时降低这些任务的错误率,并专注于以减少的努力来扩展任务。

例如,假设我们需要升级一台思科路由器的 IOS 映像。这可能涉及多个任务,例如在路由器上加载映像、验证映像的校验和、(如果它是生产路由器)卸载流量、修改引导变量,最后,使用新映像重新加载路由器。

如果我们只需要升级一台路由器,所有这些都是可行的。现在,将类似的场景应用于大约 1,000 台路由器。

假设我们花费 30 分钟让每个路由器执行上述任务。这是一个简单的计算,1000*30=30,000 分钟的手动工作。

此外,如果我们手动在每个路由器上执行任务,想想可能出现的错误。

在这种情况下,网络自动化将非常有帮助,因为它可以处理所有上述方面,并并行执行任务。因此,如果手动对一个路由器进行 30 分钟的努力,在最坏的情况下自动化执行相同任务也需要 30 分钟,那么并行执行将导致所有 1,000 台路由器在相同的 30 分钟内升级完成。

因此,最终所需的时间将仅为 30 分钟,无论您向自动化框架投入多少台路由器。这也极大地减少了手动工作的需求,工程师可以专注于 1,000 台网络设备中的任何故障。

在接下来的章节中,我将向您介绍一些概念、工具和示例,这些将帮助您开始构建自动化框架,并在网络场景中有效地使用它们来执行网络相关活动。

这也假设您对网络概念和在网络中使用的常用术语有所了解。

我将提供的某些示例假设您熟悉 syslog、TACACS、基本路由器配置(如主机名)、iOS 镜像加载、基本路由和交换概念,以及简单网络管理协议SNMP)。

DevOps

从历史上看,每个网络部门都有两个特定的团队。其中一个团队是工程团队,负责构思新想法以改进网络,并设计、部署和优化当前的基础设施。这个团队主要负责从头开始执行配置和布线等任务。

另一个团队是支持团队。这个团队也被称为运维团队,确保当前部署的基础设施正常运行,并专注于执行日常活动,如升级、快速修复和对该网络基础设施的任何消费者的支持。在传统模型中,工程团队到运维团队之间会有交接和知识转移,以支持当前部署的基础设施。由于两个团队的分离,工程师团队成员可能不会专注于编写清晰的文档,有时甚至不会向运维团队成员提供足够的信息,导致故障排除和问题修复延迟。这甚至可能导致一个简单的解决方案因为与运维团队成员相比,工程团队成员采取的不同方法而扩大成更大的问题。

现在,为了解决这个问题,提出了 DevOps 模型,它结合了两个团队的最佳之处。DevOps 模型不仅仅是一个花哨的称号,它是一种需要在现有团队中培养的文化。在 DevOps 模型中,任何团队的工程师都对特定项目的完整生命周期负责。这包括创建部分基础设施并自行支持它。这个模型的一个大优点是,由于一个人创建了一部分系统并支持它,他们了解该部分的各个方面,并可以通过理解客户或用户体验中出现的挑战来再次工作,使其变得更好。DevOps 工程师应该了解他们创建的基础设施部分的工程和运维。通过将自动化技能集添加到 DevOps 经验中,工程师可以轻松管理复杂任务,并以比传统模型中分散在不同领域的工程师更好的方式关注可靠性和可扩展性。

软件定义网络

如你所知,存在多种专有网络设备,如防火墙、交换机和路由器,由不同的网络供应商制造。然而,由于每个不同供应商的专有信息,多个网络设备可能不会存在于单一的网络基础设施环境中。即使它们共同存在,网络工程师也必须专注于确保每个供应商设备可以在网络路径中无缝存在。有时,在多供应商环境中,一个路由协议可能不与所有网络设备兼容,这会导致大量时间被浪费在确保移除该协议或移除不支持该协议的供应商上。这可能会浪费努力和时间,而这些时间和努力本可以更好地用于改善基础设施。

为了解决这类问题,软件定义网络SDN)已被引入。在 SDN 场景中,数据包流是从一个中央控制器定义的,该控制器随后与多供应商设备交互,根据所需的流量定义/创建规则。这完全将网络工程师的焦点转移到如何流量流动,数据包采取哪条路径,甚至通过在控制器上配置一些规则或策略来自动路由数据包以响应链路故障情况。SDN 的另一个优点是,多供应商设备现在不再是基础设施的核心。焦点转移到如何最优地执行路由和流量整形(识别流量流最优路径的过程)。作为软件驱动任务的一部分,有一些代码专门编写来控制特定任务或目标(类似于编程中的函数或方法)。这段代码由控制器决策或规则触发,进而向多供应商设备添加、修改或删除配置,以确保控制器上的规则集得到遵守。SDN 甚至有能力通过识别物理链路故障或设备完全故障来完全隔离故障域,而不会影响实时流量。例如,如果交换机收到一个目的地为它所不知道的网络的数据包,它会向控制器发出请求。在传统网络模型中,这将是数据包丢失或找不到路由,但通过 SDN,这是控制器的任务,为交换机提供目的地或路径信息,以正确路由数据包。

这确保了故障排除变得更加容易,因为网络工程师现在可以完全控制每条路径/数据包流,无论特定的供应商协议或技术支持如何。此外,由于我们现在遵循一套标准协议,我们甚至可以通过移除更昂贵的专有网络设备并用开放标准网络设备替换它们来降低成本。

OpenFlow

OpenFlow 是一种通信协议,用于在不同厂商的设备之间进行数据包流的通信。该标准由一个名为 开放网络基金会ONF) 的团体维护。正如其名所示,OpenFlow 通过结合 访问控制列表ACLs) 和路由协议来控制网络层中数据包的流动。

OpenFlow 主要有两个组件——控制器和交换机。控制器用于在创建数据包在不同连接设备间流动的路径方面做出决策,而交换机(或网络设备)则根据数据包需要采取的路径从控制器动态配置。

深入一点,OpenFlow 控制器通过修改、添加或删除控制器决定的数据包匹配规则来控制 OpenFlow 交换机转发表中的数据包路由。

由于 OpenFlow 是另一种协议,它运行在 TCP 上,并在控制器上工作于端口 6653。在撰写本文时,OpenFlow 标准版本 1.4 正在活跃并广泛用于 SDN 框架中。OpenFlow 是专有网络厂商在其定制软件旁边运行的一项附加服务。通常,这确保了数据转发或数据包处理仍然是专有交换机的一部分,但数据流或控制平面任务现在由 OpenFlow 控制器接管。作为 SDN 框架的一部分,如果一个参与交换机收到一个数据包但不知道该将其发送到哪里,它会与 OpenFlow 控制器通信以获得答案。控制器根据其预先配置的逻辑决定对该未知数据包采取什么行动,并可以指示其控制的交换机为该数据包创建一个单独的或特定的路径以在网络中流动。正因为这种行为,这是目前在所有引入 SDN 的部署中正在部署的协议。

程序概念

现在,当我们开始着手于我们的自动化实践方法时,我们需要了解程序的基本概念以及如何编写程序。

简单来说,程序是一组传递给系统以执行特定任务的指令。这组指令基于现实生活中的挑战和需要以自动化方式完成的任务。小批次的程序可以组合起来创建一个应用程序,该应用程序可以安装、部署和配置以满足个人或组织的需求。从现在开始,我们将讨论的一些关键概念和编程技术包括 PowerShell 和 Python。这些是最受欢迎的脚本语言,用于创建快速、有效且结果导向的自动化。

这些是在创建程序时我想介绍的一些关键概念:

  • 变量

  • 数据类型

  • 决策者

  • 循环

  • 数组

  • 函数

  • 最佳实践

变量

这些是预定义的、可读的、可理解的单词或字母,用于存储一些值。在编写程序的基础中,我们需要一个变量来存储数据或信息,并且基于变量,我们可以进一步增强编程逻辑。正如我们可以在第一行看到的那样,创建变量的一个重要部分是它应该是可读的和可理解的。

让我们举一个例子:假设我想将数字 2 存储在一个变量中。我们可以为变量选择任何名称并定义它:

Option 1: x=2
Option 2: number=2

正确的答案将是 选项 2,因为我们通过变量名(number)知道这个变量包含一个特定的数字。正如先前的例子所示,如果我们继续以创建大型程序时的方式定义变量,由于变量的含义不明确,复杂性将会显著增加。

不同的编程语言有不同的定义变量的方式,但确保变量可读性的基本概念应该是程序员或程序作者的优先考虑。

数据类型

正如其名所示,这些是我们传递给变量的值的分类。一个变量可以被定义为存储特定类型的值,该值可以根据数据类型进行声明。

有多种数据类型,但就我们最初的讨论而言,有四种主要的数据类型需要理解:

  • 字符串:这是一个通用的数据类型。任何定义为字符串的值就像说这个值是纯英文,包含字符、字母、特殊字符等。我将其称为通用数据类型,因为几乎所有其他数据类型都可以转换为字符串格式,在转换为字符串的过程中保持相同的值。

考虑以下示例:

number=2

这定义了一个名为 number 的变量具有 2 的值。

同样地,如果我们声明:

string_value="2"

这等同于说,值 2 现在已定义为字符串,并存储在名为 string_value 的变量中。

  • 整数:这指定了任何数值都需要用这种数据类型来定义。这里要注意的关键点是整数值将包含一个整数,而不是小数值:

考虑以下示例:

integernumber=2

这定义了一个名为 integernumber 的变量具有数字 2 的值。

这里的一个错误赋值可能如下所示:

integernumber=2.4

在某些编程语言中,这会导致错误,因为整数需要被解释为整数,而不是小数值。

  • 浮点数:这种数据类型消除了我们之前在整数中看到的限制。这意味着我们可以有一个小数,并且可以执行数学计算和在浮点数中存储小数值。

  • 日期时间:这是在许多现代脚本语言中找到的一种扩展数据类型。这种数据类型确保存储或检索的值是以日期格式。如果我们需要创建一个使用一些时间或日期计算的程序,这通常非常有用。例如,我们可能需要找出在过去七天中由路由器生成的系统日志数量。最后七天将通过这种数据类型进行存储。

决策者

这些是程序中非常关键的部分,它们可以定义程序的流程。正如其名所示,决策者根据某个条件决定采取某种行动。

简而言之,如果你想买冰淇淋,你会去冰淇淋店,但如果你想喝咖啡,你会去咖啡店。在这种情况下,条件是你想买冰淇淋还是咖啡。行动是基于条件的结果的:你去了那个特定的商店。

这些决策者,也称为条件,在不同的脚本语言中以不同的方式定义,但每个条件的最终结果决定了程序的后续流程。

通常,在条件中,两个或多个值被比较,并返回真或假。根据返回的值,执行一组特定的指令。

考虑以下例子:

Condition:
if (2 is greater than 3), then
Proceed to perform Option 1
else
Proceed to perform Option 2

正如前一个例子所示,一个条件被评估,如果2 大于 3,则程序流程将根据选项 1执行,如果为假(这意味着 2 不大于 3),则选择选项 2

如果我们想要更多的复杂性,我们可以添加多个决策语句或条件,以细化程序的流程。

让我们举一个例子:

if (Car is of red color), then
  if (Car is Automatic), then
    if (Car is a sedan), then
      Option 1 (Purchase the car)
    else (Option 2, ask for a sedan car from dealer)
  else (Option 3, ask for an Automatic car from dealer)
else (Option 4, ask for a red car from dealer)

正如我们可以看到这个复杂条件,我们可以很容易地根据额外的检查来决定程序的流程。在这种情况下,我只想要一辆红色自动轿车汽车。如果这些条件中的任何一个不满足,那么我会要求经销商满足那个具体条件。

在前一个例子中,还有一个需要注意的事情是,条件彼此嵌套,因此它们被显示为通过空格嵌套,以决定子条件与其父条件。这通常在括号内或根据使用的脚本语言进行简单缩进来表示。

有时候,有必要将一个值与多个条件进行比较,并在它匹配任何条件时执行一个动作。这在编程中称为switch case

考虑以下例子:

Carcolor="Red" (Here we define a variable if the value of string as Red)
switch (Carcolor)
Case (Red) (Perform Option 1)
Case (Blue) (Perform Option 2)
Case (Green) (Perform Option 3)

在这里,我们可以看到,根据变量的值,可以执行某种类型的动作。在这种情况下,将执行选项 1。如果我们更改Carcolor变量的值为蓝色,则将执行选项 2。

条件的一个重要组成部分是比较运算符,我们使用它们来比较两个值以获得结果。一些示例运算符包括等于、大于、小于和不等。根据我们使用的比较运算符,结果可能会有所不同。

让我们举一个例子:

greaternumber=5
lessernumber=6

if (greaternumber 'greater than' lessernumber)
Perform Option 1
else
Perform Option 2

我们声明了两个名为greaternumberlessernumber的变量,并在条件中比较它们。我们使用的条件运算符是大于,如果条件为真(greaternumber大于lessernumber),则结果为选项 1,如果条件为假(greaternumber不大于lessernumber),则结果为选项 2。

此外,我们还有被称为逻辑运算符的运算符,如ANDORNOT。我们可以通过使用这些逻辑运算符来组合多个条件。它们在英语中有相似的意义,这意味着如果我们使用AND运算符,我们希望在执行动作之前,条件 1 和条件 2 都必须为真。

考虑一个例子:我想在carredautomaticsedan的情况下才购买一辆车:

if (car is 'red') AND (car is 'automatic') AND (car is 'sedan')
Perform action 'buy car'
else
Perform action 'do not buy'

这仅仅意味着我会评估所有三个条件,并且只有当所有这些条件都为真时,我才会执行buy car动作。在这种情况下,如果任何条件不符合值,比如车是蓝色的,那么就会执行do not buy动作。

循环

如我们通常在日常生活中所知,循环就是一遍又一遍地重复相同的路径。换句话说,如果我被要求从冰淇淋店买五份冰淇淋,而我一次只能拿一份,我会重复去冰淇淋店购买冰淇淋五次的过程。将这一点与编程联系起来,如果需要多次执行相同的指令集,那么我们就将这些指令放在循环中。

一个非常基础的循环通常被描述为根据我们希望执行指令的次数来迭代一个变量的过程。

让我们举一个例子:

Start the loop from one, until the loop has been repeated sixty times, adding a value of 1 to the loop:
Perform action

如果你看到指令被传递,在循环中有三个独立的段落被描绘出来:

  1. 从 1 开始循环:这意味着循环应该从 1 的值开始。

  2. 直到循环重复六十次:这意味着执行相同的任务集,直到循环完成六十次执行。

  3. 在循环中添加 1 的值:这意味着我们规定在每次循环完成之后,循环计数增加 1。

结果将是执行相同动作六十次,直到循环计数达到六十。此外,循环可以用来遍历存储在变量中的多个值,无论它是整数、字符串还是任何其他数据类型。

数组

数组(或在某些脚本语言中称为列表)用于在单个变量中存储相似的多组值。这有助于确保所有具有相似意义的类型数据都存储在单个变量中,并且我们可以轻松地遍历这些数组对象以获取存储在数组中的值。

考虑以下示例:

countries=["India","China","USA","UK"]
for specific country in countries 
 Perform action

正如我们在变量声明中可以看到的,现在我们通过将它们分组并分配给单个变量,以相似的数据类型和上下文或意义来声明。在我们的例子中,是将所有国家名称分配给一个名为countries的数组变量。在下一行,我们现在使用循环方法进行迭代,对于countries列表或数组中的每个特定国家,我们将执行操作。在这种情况下,循环将执行以对每个国家执行操作,从国家名称India到国家名称UK的末尾。

存储在数组中的每个值都被称为数组的元素。此外,数组可以很容易地进行排序,这意味着无论数组中元素的顺序如何,我们都可以通过调用一些额外的编程任务来获取排序后的列表或数组。

让我们考虑一个示例:

countries=["India", "China", "USA","UK"]
Sort (countries)

结果将如下所示:

countries=["China","India","UK",USA"]

排序功能确保数组内部的所有元素都按字母顺序排序并按排序顺序存储。

函数

函数或方法是一组预先编写的指令,当它们被调用时,会执行特定的任务。函数也可以定义为完成共同任务的一组编程指令的单一名称。

以驾驶作为一个函数的例子来考虑。在驾驶中,有许多事情需要关注,比如理解交通信号、驾驶汽车以及在交通和道路上驾驶汽车。

所有这些任务都被分组在一个名为driving的函数中。现在,假设我们有两个想要学习驾驶的人,示例 1 和示例 2。从编程的角度来看,一旦我们定义了一个函数,我们每次想要执行相同的一组任务时都需要调用它。因此,我们会调用driving(example 1)然后调用driving (example 2),这将确保这两个人在经过driving函数中的指令集后都会成为驾驶员。

让我们再来看一个示例:

countries=["India","China","USA","UK"]

function hellocountry(countryname)
 Return "hello " countryname

for each country in countries:
     hellocountry(each country)

在第一行,我们声明了一个包含国家名称作为元素的数组。接下来,我们定义了一个名为hellocountry的函数,它接受countryname作为输入。在函数本身中,我们简单地返回传递给函数作为输入的countryname的值,并在其前面加上hello这个词。

现在剩下的就是遍历所有国家的元素,并将每个countryname作为输入传递给hellocountry函数。正如我们所看到的,我们对每个元素调用了相同的函数,根据函数内部声明的指令,现在为数组中的每个元素执行了特定任务。

最佳实践

既然我们已经了解了程序一些关键组件的基础知识,那么我们还将考虑编写良好程序的一个重要方面。

从机器的角度来看,只要程序中给出的指令格式或语法正确,并且机器能够正确解释每条指令,那么程序是如何编写的就没有理解。对于最终用户来说,只要最终用户得到期望的结果,程序是如何编写的可能并不重要。关注程序编写方式的人是编写自己程序的程序员,或者需要解释其他程序员程序的程序员或开发者。

可能存在多个原因,使得程序员需要解释他们没有编写的程序。可能是因为在编写程序的程序员不可用时支持该程序,或者通过添加自己的代码或编程指令来增强程序。代码可读性的另一个原因是修复错误。任何程序或指令集都可能由于输入错误或不正确的逻辑而出现故障,这可能导致意外的行为或结果。这被称为错误,需要修复以确保程序按照最初编写的方式运行。

每个程序员都有自己的最佳实践,但程序的一些关键方面是可读性、支持信息和缩进。

程序的可读性

这是编写良好程序最重要的方面之一。程序需要以这种方式编写,即使是非专业人士或程序的第一位读者也应该能够理解正在发生的基本情况。

变量需要正确声明,以便每个变量都清楚地表明它存储的内容:

x="India"
y="France"

可以写成这样:

asiancountry="India"
europecountry="France"

这里还有一个例子:

x=5
y=2

它可以写成这样:

biggernumber=5
smallernumber=2

如前例所示,如果我们编写一个两行或三行的程序,我们可以很容易地以随机方式声明变量,但当这些随机变量在更长的程序中使用时,即使是编写自己程序的程序员,事情也会变得复杂得多。想象一下,如果你已经将变量声明为abc等,然后在使用了 10 个或 15 个更多变量之后,你需要回到程序的每一行来理解abc中声明了什么值。

编写良好程序的一个方面是注释。不同的脚本语言提供了不同的注释程序的方式。注释是必要的,以确保我们将每个程序分解为部分,每个部分都有一个注释解释该部分的使用。一个非常好的例子是如果你声明一个函数。例如,一个名为Cooking的函数和另一个名为CookingPractice的函数可能因为它们的名称而令人困惑。现在,如果我们对Cooking方法添加注释说“这个函数是在你学会了如何烹饪后掌握烹饪艺术”,并对CookingPractice添加注释说“这个方法是为了学习烹饪”,这可以使阅读程序的人更容易理解。

现在程序员可以轻松理解,当他想学习烹饪时,他必须调用CookingPractice而不是Cooking方法。注释在任何编程语言中都没有特殊含义,当机器试图将编程语言转换为机器指令时,它们会被忽略。因此,注释仅针对程序员,并让读者了解程序中的情况。每个复杂的条件、循环等也应该放置注释,以阐明该特定条件或循环的使用。

支持信息

如其名所示,这是附加信息,最好以注释的形式添加,包含有关程序和作者的信息。建议至少一个程序应包含作者信息(即创建程序的人)、联系电话和电子邮件地址、程序的基本使用或程序的目的以及程序的版本。

版本号是特定的,例如从 1.0 开始,随着我们增强程序或添加新功能,我们可以将其更改为 1.1 版本(用于小改动)或更新的版本,如 2.0 版本(用于重大更改)。

考虑一个例子:

Program start
Comment: Author: Myself
Comment: Contact: myemail@emailaddress.com
Comment: Phone: 12345
Comment: Version: 1.0
Comment: Purpose: This program is to demo the comments for support info
Comment: Execution method: Open the Command Prompt and run this program by calling this program.
Comment: Any extra additional info (if needed)

Program end

这种方法确保每个人都知道脚本的最新版本以及如何执行程序或脚本。此外,这里还包含作者的联系方式,如果生产过程中出现任何问题,作者可以轻松地被联系以纠正或修复生产中的脚本。

缩进

这与我们用普通英语写作时所做的类似。在某些脚本语言中,缩进是强制性的,但作为最佳实践,我们应该遵循任何编程语言中编写的任何程序。缩进可以提高程序的可读性,因为它有助于程序员或阅读程序的其他人快速理解程序的流程。

让我们看看一个例子,其中我们有一个嵌套条件,检查一辆Car是否是Red,是否是Sedan,以及是否是Automatic

写得不好的方式可能是如下所示:

if (Car is 'Red')
if (Car is 'Sedan')
if (Car is 'Automatic')
do something

现在,想象一下将多行代码添加到长程序中,当你阅读时,程序流程会让你容易感到困惑。

更好且推荐的做法如下:

if (Car is 'Red')
    if (Car is 'Sedan')
        if (Car is 'Automatic')
           do something

这提供了程序清晰的流程。只有当CarRed时,才检查其他条件;否则,不要检查其他条件。这就是我们所说的条件嵌套,也称为嵌套条件

这在调试复杂程序时也消除了很多困惑。我们可以通过快速解析程序并理解每个程序段的流程,轻松地识别出有问题的代码或指令。

样本最佳实践示例

此示例通过创建一个基本程序,总结了迄今为止我们所学到的所有最佳实践。

问题陈述:解析数组中声明的所有国家,并只打印那些名称中包含字母I或字母U的国家:


Program begin:

Comment: This is a sample program to explain best practice
Comment: Author name: Programmer
Comment: Email: Programmer@programming.com
Version: 1.0

Comment: The following section declares the list of countries in array countrylist
countrylist=['India','US','UK','France','China','Japan']

function validatecountryname(countryname)
   Comment: This function takes the input of countryname, checks if it contains I or U and returns value based upon the result.
   if ((countryname contains 'I') OR (countryname contains 'U')
         return "Countryname contains I or U"
   else
       return "Countryname does not contain I our U"

Comment: This is a loop that parses each countryname from the countrylist one by one and sends the variable 'countryname' as input to function validatecoutryname

foreach countryname in countrylist
     validatecountryname (countryname)

Comment: Program ends here

程序是自我解释的,但值得注意的是支持注释,如作者、电子邮件等。缩进确保任何读者都能清楚地了解程序的流程。

此外,还有一个需要注意的事情是使用清楚地描述变量或名称用途的名称。每个变量和函数名称都清楚地说明了它的用途。中间的附加注释行增加了对每个段做什么以及语句或函数目的的清晰度。

语言选择(Python/PowerShell)

前进,掌握了编写程序和理解最佳实践的知识,我们现在将探讨一些适合我们自动化脚本的脚本语言。脚本语言和编程语言(如 C 和 C++)之间的基本区别在于,脚本语言不是编译的,而是通过它执行的底层环境进行解释(换句话说,需要一个转换器将人类可读格式的命令转换为机器格式,一次解析一行),而编程语言主要是编译的,因此可以在多个环境中执行,而无需使用任何特定的底层环境或要求。

这意味着如果我用 Python、PowerShell 或甚至 Perl 编写脚本,我需要安装那种特定的语言才能运行我编写的程序。C 或 C++代码可以编译成可执行文件(.exe),可以独立运行,无需安装任何语言。此外,脚本语言代码密集度较低,这意味着它可以自动解释程序中的一些代码,具体取决于如何调用。

让我们考虑一个例子。以下是我们在脚本语言中声明变量的方式:

x=5

OR

x="author"

OR

x=3.5

而在编程语言中,声明的方式可能是这样的:

integer x=5
String x="author"
Float x=3.5

这表示,根据我们分配给变量的值,脚本语言中会自动识别变量类型,而在编程语言中,声明是严格控制的。在这种情况下,如果我们声明一个变量为 String 类型,这清楚地意味着除非我们明确更改该变量的数据类型,否则我们不能在该变量中声明任何其他类型的值。

我们主要使用三种类型的脚本语言来创建程序,这些语言主要用于自动化脚本或编程。这些是 Perl、Python 和 PowerShell。

随着 Perl 这种最古老的语言的支持减少,现在的焦点转向了 Python,因为它具有开源支持,以及 PowerShell,因为它具有微软或 .NET 环境。比较这两种语言并不理想,因为读者可以根据自己的喜好选择使用哪种编程语言来编写程序。由于我们超过 70% 的计算机运行 Windows,并且随着微软 Azure 作为微软的云操作系统市场的增长,PowerShell 由于其底层的 .NET 环境而成为首选语言。在我们用 PowerShell 创建程序时,很容易将该程序移植到运行 Windows 的另一台机器上并执行,而无需任何特殊设置。

另一方面,Python 由于其开源方法而越来越受欢迎。有成千上万的开发者通过添加特定任务的特殊功能来增强 Python。例如,有一个名为 Paramiko 的函数或子程序,用于登录网络路由器。另一个是 Netmiko,它是 Paramiko 的增强版本,用于根据网络硬件供应商和操作系统(如 Cisco iOS 或 Cisco NXOS)登录网络设备。在编写并成功执行 Python 程序之前,需要安装 Python。

今后,我们的重点将是 Python,同时还会提供一些额外的技巧和窍门,介绍如何使用 PowerShell 而不是 Python 来执行相同的任务。

编写您的第一个程序

现在,因为我们是从零开始,我们需要了解如何编写我们的第一个程序并执行它。PowerShell 预装在 Windows 机器上。但我们需要从网上(www.python.org)下载 Python 并选择适合您操作系统的正确版本。下载后,它可以像在 Windows 机器上安装任何其他应用程序一样安装。

在 Linux 机器上,情况也是如此,但由于 .NET 的要求,PowerShell 不会在 Linux 或 Unix 环境中得到支持。因此,如果我们使用 Unix 或 Linux 环境,Python 或 Perl 仍然是我们的首选脚本语言。

对于 Python 和 PowerShell 都有多种 集成开发环境IDEs),但这些语言自带的一些默认 IDE 也相当有帮助。

正在使用多个版本的 PowerShell 和 Python。当在更高版本中编写程序时,通常向后兼容性不是很好,所以在选择版本之前请确保注意用户和环境。

在我们的情况下,我们将使用 PowerShell 4 和 Python 3 及以上版本来编写程序。一些命令可能在 PowerShell 和 Python 的旧版本中无法运行,并且一些语法或命令在旧版本中是不同的。

PowerShell IDE

这可以通过点击开始按钮并搜索 Windows PowerShell ISE 来调用。一旦调用,初始屏幕将看起来像这样:

图片

如前述截图所示,PowerShell 脚本以.ps1扩展名保存。一旦我们在 IDE(或称为 PowerShell 的 ISE)中编写了一些内容,它需要保存为somefilename.ps1然后执行以查看结果。

让我们编写一个名为Hello World的程序:

图片

  • 如我们可以在我们的第一个程序中看到,我们写入两行来打印Hello World。在 ISE 中,我们通过传递命令来声明一个变量(在 PowerShell 中,变量通过在变量前加美元符号$来表示),将值Hello World赋给它。下一行是简单地通过调用一个名为Write-host的方法或函数来打印这个变量,这个方法或函数用于在 PowerShell 中屏幕上打印值。

  • 一旦我们编写了程序并保存,下一步就是执行以查看我们的结果。

  • ISE 顶部的绿色按钮用于执行脚本,脚本的结果显示在屏幕底部。在这种情况下,它是一个简单的Hello World输出。

PowerShell 脚本也可以通过命令行直接调用。由于 PowerShell 是一种脚本语言并且需要在机器上安装,我们可以直接从 Windows 命令提示符调用 PowerShell 并从 PowerShell 控制台本身执行脚本和单个脚本命令。

这就是我们如何找出 PowerShell 的版本:

图片

如前述截图所示,PowerShell 是通过在 Windows 的命令提示符中直接调用powershell来调用的。当 PowerShell 被调用时,我们在命令行前看到PS,这确认我们现在在 PowerShell 控制台中。要查看版本,我们调用系统变量$psversiontable,它显示了 PowerShell 的版本。

我们可以看到这是 2.x 版本(如CLRVersion所示)。系统变量是具有基于安装类型的预定义值的特殊变量。这些特殊变量可以在我们的脚本中的任何时候调用以获取它们的值并基于返回值执行操作。

以下示例显示我们正在使用 PowerShell 的更高版本:

图片

如我们所见,相同的变量现在为PSVersion返回值为4.0,这证实了这是 PowerShell 的 4 版本。

PowerShell 4.0 是从 Windows 8.1 开始作为客户端操作系统的默认安装,在服务器环境中是 Windows Server 2012 R2。

Python IDE

与 PowerShell 类似,一旦安装了 Python,它就有自己的 IDE。可以通过在开始菜单中键入或调用 IDLE(Python)来调用它:

图片

当打开时,Python IDE,称为 IDLE,看起来与前面的截图相似。标题栏显示了 Python 的版本(在这个例子中是 3.6.1)和三个大于号(>>>)显示了命令行,它已准备好接受 Python 命令并执行它们。要编写程序,我们点击文件 | 新建文件,这将在记事本中打开,我们可以在这里编写程序。

让我们看看 Python 中的类似hello world程序:

图片

当我们编写一个新的程序时,所使用的变量是newvalue,分配给它的值是hello world。下一行仅仅是调用 Python 的print函数来在脚本执行期间打印变量内的值。

一旦我们编写了程序,我们就在编写程序的窗口中点击文件 | 另存为,并将脚本保存。脚本以filename.py保存,其中.py扩展名表示 Python 脚本。一旦保存,我们可以在键盘上按F5按钮或在脚本窗口中选择运行 | 运行模块来运行该特定脚本。以下窗口是我们第一次从开始菜单调用 IDLE 应用程序时调用的同一窗口,但现在它有我们编写的脚本的输出。

hello world的输出现在在 IDLE 窗口中可见。一旦我们完成了编写脚本或 Python 命令,我们只需简单地关闭打开的命令窗口来关闭应用程序或 Python 解释器。

与 PowerShell 类似,我们也可以从命令行调用python,如下所示:

图片

这里还有一个需要注意的额外事项是,要退出 Python 解释器,我们调用exit()函数。这告诉 Python 停止执行并退出到 Windows 的命令提示符。

表征状态转移(REST)框架

网络自动化最重要的方面之一是理解和利用目前可用于特定任务的工具。例如,这可能是用于数据挖掘的 Splunk,用于网络监控的 SolarWinds,syslog 服务器,或者任何用于执行各种任务的定制应用程序。

编写应用程序的另一个重要方面是如何在不更改应用程序本身的情况下利用同一应用程序执行额外任务。换句话说,假设我们为了个人使用买了一辆汽车,但这个增强功能是使用同一辆车作为出租车或扮演其他角色。

这是我们介绍 应用程序编程接口 (API)。APIs 用于暴露已编写应用程序的一些方面,以便与我们所编写的程序合并,这样我们就可以通过特定的 API 轻松调用特定的任务。例如,由于 SolarWinds 是一种用于网络监控和其他目的的专业应用程序,我们可以通过 SolarWinds 的 API 在我们的脚本中获取网络设备列表。因此,我们将发现网络设备的专门任务留给 SolarWinds,但通过该应用程序的 API 在我们的脚本中利用其专业知识。

深入一点,API 仅仅是一个函数(类似于我们在脚本中编写的函数);唯一的区别是这些函数返回的值。API 函数通常以 扩展标记语言 (XML) 或 JavaScript 对象表示法 (JSON) 格式返回值,这些是跨环境和跨机器信息交换的行业标准。想象一下,这就像我们使用英语作为通用语言进行交流一样。尽管我们可能出生在不同的文化、不同的国家,但我们可以使用英语有效地进行交流,因为英语是人际互动的行业标准。同样,无论程序是用什么语言编写的(例如 C、C++、Java、VB、C# 等),每个程序都可以通过调用其 API 与另一个程序进行通信,结果以 XML 或 JSON 格式返回。

XML 是一种标准的编码方式,用于将结果发送给请求者,并且使用相同的标准,请求者可以解码这些结果。JSON 是另一种数据交互可以在应用程序之间发生的方式。

下面是一个示例 XML:

<?xml version="1.0" encoding="UTF-8"?> 
<note>
  <to>Readers</to>
  <from>JAuthor</from>
  <heading>Reminder</heading>
  <body>Read this for more knowledge</body>
 </note>

前面的内容的第一行表明,该行之后的内容是以 XML 格式。XML 文件以 .xml 扩展名保存。

现在我们可以看到,如果我们计算从 XML 结果返回的字符数,如果我们加上像 <heading>Reminder</heading> 这样的字符,它返回的结果在 <heading> 的起始标签和结束标签之间。这意味着由于额外的关闭标签的字符计数开销,XML 文件的大小大大增加。

下面是相同的示例以 JSON 格式呈现:

{
 "note": {
 "to": "Tove",
 "from": "Jani",
 "heading": "Reminder",
 "body": "Don't forget me this weekend!"
 }
 }

正如我们所看到的,我们已经摆脱了之前在 XML 中看到的那些额外的庞大开标签和闭标签。这意味着,如果我们调用 API 以 XML 格式返回大量数据,那么从应用程序获取这些数据肯定需要更长的时间,并且需要更多资源(如内存和存储)来临时或永久存储这些数据。为了克服这种情况,现在更倾向于使用 JSON 格式而不是 XML 通过 API 交换数据。由于数据返回方式的不同,JSON 相比 XML 轻量级且资源消耗较少。JSON 文件以 .json 扩展名保存。

这种与 API、后端方法和特定编程语言编写的函数(称为 API)以及返回 XML 或 JSON 格式值的函数一起工作的功能,所有这些都在 HTTP 或 HTTPS 等网络协议上运行,统称为 REST 框架。REST 框架是之前提到的使用 XML 或 JSON 进行交互的行业标准,增加了通过网络协议(如 GET、POST 等)发生的交互。API 的 HTTP 请求可以是 REST 框架识别的 GET 或 POST 请求,类似于 HTTP GET 和 POST 请求,与底层应用程序交互以执行请求的操作。

脚本语言严重依赖于 API 调用,需要提供 API 功能的应用程序必须遵守 REST 框架的要求,以确保它们能够扩展其功能,以便调用脚本以获取或保存数据。这种做法的一个重大好处是,现在跨平台通信正在发生,双方(API 的调用者或提供 API 功能的应用程序)都不知道对方正在运行哪种语言或环境。因此,Windows 应用程序可以轻松地与 Unix 环境协同工作,反之亦然,使用这种方法,HTTP 是调用 API 的标准通信语言,并使用行业标准 XML 或 JSON 格式解析结果。

PowerShell 中的示例 API REST 调用如下:

图片

如前一张截图所示,我们在 PowerShell 中调用 Invoke-RestMethod 函数,该函数用于使用默认的通信和交互方式通过 JSON 调用应用程序的 API 方法。

被调用的应用程序位于 REST 框架中,可以通过 URL blogs.msdn.microsoft.com/powershell/feed/ 访问 API。这使用 HTTPS 协议与应用程序进行通信。

format-table 是 PowerShell 中的一个函数,它指定无论结果如何,都显示从 API 调用返回的每个记录/结果的 title 属性。如果我们没有使用该命令,显示将显示每个记录返回的所有属性。

下面是一个 Python 中的示例 REST 调用:

图片

在这个例子中,我们调用一个名为 requests 的标准函数。第一行 import requests 表示我们在 Python 脚本中引用了 requests 函数或库。在下一行,我们使用 requests.get 方法通过 JSON 调用 Google Map API。换句话说,我们确保对 Google API URL 进行 HTTP GET 调用。一旦我们得到结果,我们调用 json() 方法将值存储在变量 r 中。

有时候,当我们使用import调用 Python 的自定义函数或库时,可能会出现一个错误,表明找不到该模块。这意味着它不包括在标准的 Python 安装中,需要单独安装。为了解决这个问题,我们可以使用pipeasy_install命令手动安装模块,这些内容我们将在接下来的章节中详细讨论。

摘要

本章涵盖了我们在执行网络自动化时将使用的各种术语的基础知识。本章还向读者介绍了编程的一些基本方面,以帮助构建程序的逻辑。

本章还解释了为什么要编写一个好的程序以及如何编写一个程序,并附带了一些关于脚本语言的参考点。还简要讨论了当前脚本语言的基本用法,以及在两种最受欢迎的脚本语言(Python 和 PowerShell)中编写一个非常基础的程序。

最后,我们通过介绍 REST 框架来总结所有内容,其中包括了关于 API 的讨论、如何调用它们,以及 XML 和 JSON 作为跨平台数据交换语言的解释。

下一章将更深入地探讨如何使用 Python 编写脚本,并附带 PowerShell 的相关示例,以确保读者熟悉 Python 和 PowerShell。还将提供一些技巧和最佳实践。

第二章:网络工程师的 Python

既然我们已经熟悉了如何使用编程语言中的概念以及最佳实践来编写程序,现在让我们深入探讨编写实际的 Python 程序或脚本。主要关注如何在 Python 中编写程序,我们还将看到如何用 PowerShell 编写相同的程序,因为在某些时候我们可能需要使用 PowerShell 来实现我们想要的结果。我们将涵盖创建程序的各种方面,并对每个语句进行一些解释,并提供一些技巧和窍门来应对那些棘手的情况。

在本章中,我们将涵盖以下主题:

  • Python 解释器和数据类型

  • 使用条件循环编写 Python 脚本

  • 函数

  • 安装新的模块/库

  • 为脚本从命令行传递参数

  • 使用 Netmiko 与网络设备交互

  • 多线程

Python 解释器和数据类型

如同其名,解释器用于解释指令,以便其他人可以理解。在我们的情况下,它用于将我们的 Python 语言转换为机器可理解的格式,该格式控制了我们给机器的指令流。

它也被用来将机器给出的值和消息转换成人类可读的格式,以便让我们了解我们的程序是如何执行的。

如 第一章 中所述,基本概念,我们关注的解释器是 Python 3.6。我将在 Windows 平台上使用它,但网站上提供了如何在 Unix 或 Linux 机器上下载和安装的明确说明。一旦我们从 Python 社区下载并安装它,可以在 URL www.python.org/downloads 上找到,我们只需单击安装文件即可安装。从安装目录中,我们只需调用 python.exe,这将调用 Python 解释器。

为了在您的命令提示符的任何位置调用 Python,只需将 Python 安装文件夹添加到您的 PATH 变量中。

以下是一个示例:set path=%path%;C:\python36。这将向当前路径添加 Python36 路径。一旦完成,就可以在任何命令提示符下调用 python.exe

一旦我们调用解释器,首先要做的步骤是创建一个变量并给它赋值。

Python,就像任何其他编程语言一样,支持各种数据类型用于变量。数据类型通常定义了可以存储在变量中的值的类型,但 Python 和 PowerShell 有能力根据值自动评估变量的类型。Python 支持大量数据类型,但在我们日常使用中,我们通常多次引用原生数据类型。

Python 数据类型支持:

  • 数字:这些是整数类型,例如 1、2、100 和 1,000。

  • 字符串:这些是单个或多个字符,可能是 ASCII 中的每个字母,例如 Python、network、age123 和 India。此外,字符串需要存储在双引号(")或单引号(')内,以指定值是一个字符串。因此,1'1' 在 Python 中会被解释为不同的值。

  • 布尔值:这可以是真或假值。

  • 字节:这些通常是二进制值。

  • 列表:这些是有序值序列。

  • 元组:这些与列表类似,但值或长度不能更改。

  • 集合:这些与列表类似,但不排序。

  • 字典哈希值:这些是键值对,类似于电话簿,其中一个主要值(名称)与电话号码和地址相关联。

数据类型的示例如下:

图片

如前例所示,我们声明了具有各种值的变量,Python 根据值自动解释特定的数据类型。如果我们再次输入变量名,它将根据其数据类型打印出存储在变量中的值。

类似地,以下示例指定了其他原生数据类型:

图片

此外,要查看数据类型,我们可以使用 type() 函数,该函数根据我们给出的值返回变量的类型。变量作为参数传递给 type() 函数以获取数据类型值:

图片

PowerShell 中的相同 Python 代码示例如下:

#PowerShell code
$value=5
$value="hello"
write-host $value
write-host $value.gettype()
#This is remark
#A variable in powershell is declared with '$' sign in front.
# The gettype() function in powershell is used to get the type of variable.

对于特定数据类型的变量,存在一些操作,例如加法(+)。我们必须确保我们正在添加的变量类型。如果我们有一个不兼容的数据类型变量被添加到另一个变量中,Python 会抛出一个错误,说明原因。

在以下代码中,我们看到两个字符串变量相加的结果:

图片

类似地,观察如果我们对整型变量使用相同的加法操作时的差异:

图片

如前所述,让我们看看当我们尝试将字符串和整型变量相加时会发生什么:

图片

错误清楚地指出,我们不能添加两个不同的数据类型,因为解释器无法识别需要分配给混合值变量的数据类型。

有时,如果需要,我们可以通过调用将数据类型转换为另一个数据类型的特定函数来将值从一个数据类型转换为另一个数据类型。例如,int("1") 将字符串值 1 转换为整数值 1,或者 str(1) 将整数值 1 转换为字符串值 1。

我们将根据脚本的逻辑和需求广泛使用各种数据类型,并在必要时将一种数据类型转换为另一种数据类型以实现某些结果。

条件和循环

条件是通过左右值比较来检查的。评估返回 true 或 false,并根据结果执行特定操作。

有一些条件运算符用于评估左右值的比较:

运算符 含义
== 如果两个值相等
!= 如果两个值不相等
> 如果左值大于右值
< 如果左值小于右值
>= 如果左值大于或等于右值
<= 如果左值小于或等于右值
in 如果左值是右值的一部分

条件评估的一个例子如下:

如我们所见,我们正在检查 2>3(2 大于 3)。当然,这将导致错误,因此 else 部分的操作将被执行。如果我们反转检查,3>2,那么输出将是 left value is greater

在前面的示例中,我们使用了 if 条件块,它由以下内容组成:

if <condition>:     
perform action
else:   
perform action2

注意缩进,这在 Python 中是强制性的。如果我们没有打算这样做,Python 就不会解释在哪个条件下执行哪个操作,因此会抛出缩进错误。

嵌套和多个条件

有时我们需要在单个 if 条件中检查多个条件。

让我们看看一个例子:

在这里,我们正在检查分数的范围。程序的流程如下:

85 的值分配给 marks 变量。如果 marks 小于或等于 45,则打印 Grade C,否则如果 marks 大于 45 且小于等于 75,则打印 Grade B,否则如果 marks 大于 75,则打印 Grade A,否则如果前面的所有条件都不匹配,则打印 Unable to determine

PowerShell 之前 Python 任务的示例代码如下:

#PowerShell sample code:
$marks=85
if ($marks -le 45)
{
    write-host "Grade C"
}
elseif (($marks -gt 45) -and ($marks -le 75))
{
    write-host "Grade B"
}
elseif ($marks -gt 75)
{
    write-host "Grade A"
}
else
{
    write-host "Unable to determine"
}

同样,这里是一个嵌套条件的示例(注意缩进,它将此与多个条件的早期示例区分开来):

如我们所见,在条件中,只有当父条件评估为 true 时,内部条件才会执行。如果有 false,则执行相应的 else 操作。在示例中,如果 car_details 变量包含 Car,包含 blue,并且包含 sedan,那么才会执行操作 I will buy this car。如果这些条件中的任何一个不满足,则执行相关的 else 操作。

循环

循环用于重复一组指令,直到满足特定条件。Python 中创建循环有两种常见方式,如下所述:

用于下一个循环

这种循环检查条件并在条件满足之前重复循环内的指令:

for <incremental variable> in final value:
  statements

下面是一个在for循环中打印 1 到 10 的数字的例子:

图片

如我们所见,我们使用了一个内置的range(starting value, max value)函数,该函数指定循环从起始值开始重复,直到增量值达到最大值。在这种情况下,变量x增加 1,并在每个循环中打印出值。这会一直重复,直到x的值达到10,此时for循环结束。

以类似的方式,我们也可以遍历给定列表中的项:

图片

以下是对应 Python 代码的 PowerShell 示例:

#PowerShell sample code:
$countries="India","UK","USA","France"
foreach ($country in $countries)
{
    write-host ($country+" is good")
}

在这里,我们可以看到值被分配给countries变量作为一个列表。for循环现在遍历列表中的每个项,打印语句将字符串值添加到另一个字符串值中并打印结果。这个循环会一直重复,直到列表中的所有项都被打印出来。

有时候我们可能不想遍历整个for循环。为了在循环迭代时跳出循环,我们使用break语句。以下是一个例子,我们希望在country列表中的UK之后停止打印:

 for country in countries:
     if 'UK' in country:
         break
     else:
         print (country)

While循环

While循环与for循环不同,因为这个循环中不需要新的变量,任何当前变量都可以用来执行while循环内的任务。以下是一个例子:

while True:
     perform instructions
     if condition():
       break

图片

这与for循环类似,但在这个情况下,操作首先执行,然后检查条件。在上面的例子中,首先打印x的值,然后重复相同的指令集,直到x的值达到10或更大。一旦满足if条件,我们就跳出循环。如果我们没有指定break条件,我们将进入一个无限循环,每次x的值增加 1。

编写 Python 脚本

现在,我们已经熟悉了 Python 的基本概念。接下来,我们将编写一个实际的 Python 程序或脚本。

请求输入一个国家名称,并检查该国家的最后一个字符是否为元音:

countryname=input("Enter country name:")
countryname=countryname.lower()
lastcharacter=countryname.strip()[-1]
if 'a' in lastcharacter:
    print ("Vowel found")
elif 'e' in lastcharacter:
    print ("Vowel found")
elif 'i' in lastcharacter:
    print ("Vowel found")
elif 'o' in lastcharacter:
    print ("Vowel found")
elif 'u' in lastcharacter:
    print ("Vowel found")
else:
    print ("No vowel found")

上述代码的输出如下:

图片

  1. 我们请求输入一个国家名称。input()方法用于从用户获取输入。输入的值是字符串格式,在我们的例子中,countryname变量已被分配了输入值。

  2. 在下一行,countryname.lower()指定我们接收到的输入需要转换为全小写并存储在相同的countryname变量中。这实际上将具有与我们之前输入相同的值,但为小写。

  3. 在下一行,countryname.strip()[-1]在一个语句中指定了两个操作:

    • countryname.strip()确保变量移除了所有前导和尾随的额外值,例如换行符或制表符。

    • 一旦我们得到干净的变量,就移除字符串的最后一个字符,在我们的例子中是国名最后一个字符。-1表示从右到左或从结束到开始的位置,而+1则表示从左到右。

  1. 一旦我们将最后一个字符存储在lastcharacter变量中,所需的就是嵌套条件检查,并根据结果打印值。

要保存此程序,我们需要将此文件保存为somename.py,这将指定此程序需要用 Python 执行:

以下是为前面 Python 任务提供的 PowerShell 示例代码:

#PowerShell sample code
$countryname=read-host "Enter country name" 
$countryname=$countryname.tolower()
$lastcharacter=$countryname[-1]
if ($lastcharacter -contains 'a')
{
    write-host "Vowel found"
}
elseif ($lastcharacter -contains 'e')
{
    write-host "Vowel found"
}
elseif ($lastcharacter -contains 'i')
{
    write-host "Vowel found"
}
elseif ($lastcharacter -contains '0')
{
    write-host "Vowel found"
}
elseif ($lastcharacter -contains 'u')
{
    write-host "Vowel found"
}
else
{
write-host "No vowel found"
}

Python 在缩进方面非常严格。正如我们可以在示例中看到的那样,如果我们改变缩进或制表符,即使是一个空格,Python 也会抛出一个错误,指出缩进不正确,编译将失败。这将导致错误,除非缩进被修复,否则执行将不会进行。

函数

对于任何重复的指令集,我们可以定义一个函数。换句话说,一个函数是一组封闭的指令,用于执行特定的逻辑或任务。根据提供的输入,函数能够返回结果或使用特定的指令解析输入以获取结果,而不返回任何值。

函数是通过def关键字定义的,它指定我们需要定义一个函数并提供与该函数相关的指令集。

在这个任务中,我们将打印两个输入数字中较大的一个:

def checkgreaternumber(number1,number2):
    if number1 > number2:
      print ("Greater number is ",number1)
    else:
     print ("Greater number is",number2)
checkgreaternumber(2,4)
checkgreaternumber(3,1)

图片

正如我们在前面的输出中可以看到的那样,第一次我们调用checkgreaternumber(2,4)函数时,函数打印出较大的值为4,而第二次我们用不同的数字调用该函数时,函数打印出较大的值为3

以下是为前面任务提供的 PowerShell 示例代码:

#PowerShell sample code
function checkgreaternumber($number1,$number2)
{
    if ($number1 -gt $number2)
    {
        write-host ("Greater number is "+$number1)
    }
    else
    {
        write-host ("Greater number is "+$number2)
    }
}

checkgreaternumber 2 4
checkgreaternumber 3 1

我们可以重写同一个函数,但不是在函数内部打印值,而是应该返回较大的数字:

def checkgreaternumber(number1,number2):
    if number1 > number2:
      return number1
    else:
     return number2

print ("My greater number in 2 and 4 is ",checkgreaternumber(2,4))
print ("My greater number in 3 and 1 is ",checkgreaternumber(3,1))

在这种情况下,正如我们所看到的,函数返回值,结果是在函数被调用的行返回的。在这种情况下,因为它是在print函数内部被调用的,它评估输入并返回值,该值也在同一个print函数内部打印出来。

#PowerShell sample code
function checkgreaternumber($number1,$number2)
{
    if ($number1 -gt $number2)
    {
        return $number1
    }
    else
    {
        return $number2
    }
}

write-host ("My greater number in 2 and 4 is ",(checkgreaternumber 2 4))
write-host ("My greater number in 3 and 1 is ",(checkgreaternumber 3 1))

函数的另一个重要方面是我们可以在函数中提供的默认值。有时我们需要编写可能接受多个值作为输入的函数,比如 4、5 或更多。由于很难知道我们需要什么值以及它们的顺序,我们可以确保在调用该特定函数时,如果未提供任何值,则考虑默认值:

def checkgreaternumber(number1,number2=5):
    if number1 > number2:
      return number1
    else:
     return number2
print ("Greater value is",checkgreaternumber(3))
print ("Greater value is",checkgreaternumber(6))
print ("Greater value is",checkgreaternumber(1,4))

代码执行的输出如下:

图片

  1. 如前所述的输出所示,我们指定了number2的默认值为5。现在,正如我们可以在函数的第一次调用中看到的那样,我们只提供了值3。现在,由于函数需要两个输入或参数,但我们只提供了一个,所以函数的第二个值取自默认值,在这个例子中是5。因此,将35进行比较以获取较大的数字。

  2. 在函数的第二次调用中,使用6进行了类似的调用,并且由于没有提供其他值,比较是在65之间进行的,返回的是较大的值,即6

  3. 在第三次调用中,我们提供了两个值,这覆盖了任何默认值,因此进行了14的比较。结果被评估,并返回了4的输出。

另一个重要的考虑因素是函数中变量的本地化:

globalval=6

def checkglobalvalue():
    return globalval

def localvariablevalue():
    globalval=8
    return globalval

print ("This is global value",checkglobalvalue())
print ("This is global value",globalval)
print ("This is local value",localvariablevalue())
print ("This is global value",globalval)

前述代码的输出如下:

  1. 在前述输出中,我们定义了一个名为globalval的变量,其值为6。在checkglobalvalue函数中,我们只是返回globalvalvariable的值,当我们调用第一个print函数时,它打印了一个值为6

  2. 第二个print函数只是打印相同变量的值,也打印了6

  3. 现在,在第三个print函数localvariablevalue中,我们调用相同的globalval,但给它赋值为8并返回globalval的值。在打印局部值时,它打印的结果是值 8。并不假设globalval变量现在的值是8。但是,正如我们在最后的print函数中看到的那样,当我们调用print函数来打印globalval的值时,它仍然打印了一个值为6的值。

这清楚地表明,函数内的任何变量都是局部有效的,或者说本地化的,但它不会对函数外的任何变量产生影响。我们需要使用global命令来引用全局变量并消除其本地化影响。

在使用global命令之前,这里是相同的示例:

如前所述的输出所示,如果我们更改localvariablevalue函数内部的全局变量globalval的值,我们会看到全局变量以新的值8产生的影响。

从命令行传递参数

有时候需要从命令行向脚本传递参数。这通常在我们需要在脚本中执行一些快速操作时需要,而不是脚本要求我们输入。

考虑以下代码行,我们将两个数字作为参数传递给脚本,并打印它们的和:

import sys
print ("Total output is ")
print (int(sys.argv[1])+int(sys.argv[2]))

当我们运行这个脚本时,比如说它保存为checkargs.py,并按照以下方式执行:

python checkargs.py 5 6

返回的输出如下:

Total output is
11

这里关键是导入sys模块,这是 Python 中预定义的模块,用于处理任何与 Python 系统相关的任务。我们作为参数传递的值存储在sys.argv[1]及之后,因为sys.argv[0]是正在运行的脚本的实际名称。在这种情况下,sys.argv[0]将是checkargs.pysys.argv[1]将是5,而sys.argv[2]将是6

以下任务是 PowerShell 代码:

#PowerShell sample code
$myvalue=$args[0]
write-host ("Argument passed to PowerShell is "+$myvalue)

在 Python 脚本中传递的参数是以字符串格式存在的,因此我们需要明确地将它们转换为预期的输出类型。在先前的脚本中,如果我们没有使用int()函数将其转换为整数类型,那么输出将是56而不是int(5) + int(6) = 11

Python 模块和包

因为 Python 是最受欢迎的开源编程语言,有许多开发者通过创建特定的模块并与其他人共享来贡献他们的专业知识。这些模块是一组特定的函数或指令,用于执行特定任务,并且可以在我们的程序中轻松调用。我们可以通过在脚本中使用import命令轻松调用这些模块。Python 有许多内置模块,可以直接使用import调用,但对于特定模块,需要外部安装。幸运的是,Python 提供了一个非常简单的方式来下载和安装这些模块。

例如,让我们安装一个名为Netmiko的模块,它可以帮助我们更有效地登录网络设备。Python 为每个模块提供了详细的参考文档,对于我们的模块,文档可以在pypi.python.org/pypi/netmiko找到。对于安装,我们只需进入命令行中python.exe安装或存在的文件夹。在该位置有一个名为scripts的子文件夹。

在那个文件夹中,我们有两种选项可以用来安装模块,easy_install.exepip.exe

安装 Python 库可以通过两种方式完成:

  • easy_install的语法如下:
easy_install <name of module>

例如:

easy_install netmiko
  • pip install的语法如下:
pip install <name of module>

例如:

pip install netmiko

一旦安装了所需的模块,我们需要通过关闭所有打开的会话并再次调用 IDLE 来重启 Python,以便模块可以被加载。有关模块的更多信息,可以从docs.python.org/2/tutorial/modules.html获取。

并行处理的多线程

由于我们现在专注于高效编写脚本,这个过程中的一个重要方面是如何高效、快速和正确地获取信息。当我们使用for循环时,我们会逐个解析每个项目,如果我们能快速得到结果,这是可以的。

现在,如果我们在一个 for 循环中的每个项目都是一个需要获取 show 版本输出的路由器,并且如果每个路由器需要大约 10 秒来登录、收集输出和登出,而我们大约有 30 个路由器需要获取这些信息,那么程序完成执行将需要 10*30 = 300 秒。如果我们对每个输出进行更高级或复杂的计算,这可能需要长达一分钟的时间,那么仅对 30 个路由器进行计算就需要 30 分钟。

当我们的复杂性和可扩展性增长时,这开始变得非常低效。为了帮助解决这个问题,我们需要在我们的程序中添加并行性。这简单意味着,我们同时登录到所有 30 个路由器,并执行相同的任务以同时获取输出。实际上,这意味着我们现在可以在 10 秒内获取所有 30 个路由器的输出,因为我们有 30 个并行线程被调用。

线程不过是同一函数的另一个实例,调用它 30 次意味着我们同时调用 30 个线程来执行相同的任务。

这里有一个例子:

import datetime
from threading import Thread

def checksequential():
    for x in range(1,10):
        print (datetime.datetime.now().time())

def checkparallel():
    print (str(datetime.datetime.now().time())+"\n")

checksequential()
print ("\nNow printing parallel threads\n")
threads = []
for x in range(1,10):
    t = Thread(target=checkparallel)
    t.start()
    threads.append(t)

for t in threads:
    t.join()

多线程代码的输出如下:

图片

  1. 如前例所示,我们创建了两个函数,分别命名为 checksequentialcheckparallel,用于打印系统的日期时间。在这种情况下,使用 datetime 模块获取系统的日期时间。在 for 循环中,执行了顺序运行,当函数被调用时,输出显示了增量时间。

  2. 对于线程,我们使用一个名为 threads 的空数组。每次 checkparallel 方法被创建时,每个实例都会有一个唯一的线程编号或值,这个编号或引用存储在这个空线程数组中。这个唯一的编号或引用标识每个线程,无论何时执行。

  3. 线程中的最后一个循环很重要。它表示程序将在所有线程完成之前等待。join() 方法指定,直到所有线程都完成,程序将不会进行到下一步。

现在,正如我们在线程输出中看到的那样,一些时间戳是相同的,这意味着所有这些实例都是并行而不是顺序地在同一时间被调用和执行的。

程序中的输出对于并行线程来说不是有序的,因为一旦任何线程完成,就会打印输出,而不考虑顺序。这与顺序执行不同,因为并行线程在执行另一个线程之前不会等待任何之前的线程完成。因此,任何完成的线程都会打印其值并结束。

以下是为前一个任务编写的 PowerShell 示例代码:

#PowerShell sample code
Get-Job  #This get the current running threads or Jobs in PowerShell
Remove-Job -Force * # This commands closes forcible all the previous threads

$Scriptblock = {
      Param (
         [string]$ipaddress
      )
    if (Test-Connection $ipaddress -quiet)
    { 
        return ("Ping for "+$ipaddress+" is successful")
     }
    else
    {
       return ("Ping for "+$ipaddress+" FAILED") 
    }
   }

$iplist="4.4.4.4","8.8.8.8","10.10.10.10","20.20.20.20","4.2.2.2"

foreach ($ip in $iplist)
{
    Start-Job -ScriptBlock $Scriptblock -ArgumentList $ip | Out-Null
    #The above command is used to invoke the $scriptblock in a multithread
}

#Following logic waits for all the threads or Jobs to get completed
While (@(Get-Job | Where { $_.State -eq "Running" }).Count -ne 0)
  { # Write-Host "Waiting for background jobs..."
     Start-Sleep -Seconds 1
  }

#Following logic is used to print all the values that are returned by each thread and then remove the thread # #or job from memory
ForEach ($Job in (Get-Job)) {
  Receive-Job $Job
  Remove-Job $Job
  }

使用 Netmiko 进行 SSH 和网络设备交互

Netmiko (github.com/ktbyers/netmiko) 是一个 Python 库,广泛用于与网络设备交互。这是一个多厂商库,支持 Cisco IOS、NXOS、防火墙和其他许多设备。其底层库是 Paramiko,它再次被广泛用于各种设备的 SSH 连接。

Netmiko 扩展了 Paramiko 的 SSH 能力,增加了增强功能,例如进入网络路由器的配置模式,发送命令,根据命令接收输出,增加等待特定命令执行完成的增强功能,并在命令执行期间处理是/否提示。

下面是一个简单的脚本示例,用于登录到路由器并显示版本:

from netmiko import ConnectHandler

device = ConnectHandler(device_type='cisco_ios', ip='192.168.255.249', username='cisco', password='cisco')
output = device.send_command("show version")
print (output)
device.disconnect()

对路由器执行代码的输出如下:

如我们在示例代码中所见,我们调用 Netmiko 库中的ConnectHandler函数,它接受四个输入(平台类型设备的 IP 地址用户名密码):

Netmiko 支持多种厂商。一些受支持的平台类型及其缩写,在 Netmiko 中调用时使用如下:

'a10': A10SSH,

'accedian': AccedianSSH,

'alcatel_aos': AlcatelAosSSH,

'alcatel_sros': AlcatelSrosSSH,

'arista_eos': AristaSSH,

'aruba_os': ArubaSSH,

'avaya_ers': AvayaErsSSH,

'avaya_vsp': AvayaVspSSH,

'brocade_fastiron': BrocadeFastironSSH,

'brocade_netiron': BrocadeNetironSSH,

'brocade_nos': BrocadeNosSSH,

'brocade_vdx': BrocadeNosSSH,

'brocade_vyos': VyOSSSH,

'checkpoint_gaia': CheckPointGaiaSSH,

'ciena_saos': CienaSaosSSH,

'cisco_asa': CiscoAsaSSH,

'cisco_ios': CiscoIosBase

'cisco_nxos': CiscoNxosSSH,

'cisco_s300': CiscoS300SSH,

'cisco_tp': CiscoTpTcCeSSH,

'cisco_wlc': CiscoWlcSSH,

'cisco_xe': CiscoIosBase,

'cisco_xr': CiscoXrSSH,

'dell_force10': DellForce10SSH,

'dell_powerconnect': DellPowerConnectSSH,

'eltex': EltexSSH,

'enterasys': EnterasysSSH,

'extreme': ExtremeSSH,

'extreme_wing': ExtremeWingSSH,

'f5_ltm': F5LtmSSH,

'fortinet': FortinetSSH,

'generic_termserver': TerminalServerSSH,

'hp_comware': HPComwareSSH,

'hp_procurve': HPProcurveSSH,

'huawei': HuaweiSSH,

'juniper': JuniperSSH,

'juniper_junos': JuniperSSH,

'linux': LinuxSSH,

'mellanox_ssh': MellanoxSSH,

'mrv_optiswitch': MrvOptiswitchSSH,

'ovs_linux': OvsLinuxSSH,

'paloalto_panos': PaloAltoPanosSSH,

'pluribus': PluribusSSH,

'quanta_mesh': QuantaMeshSSH,

'ubiquiti_edge': UbiquitiEdgeSSH,

'vyatta_vyos': VyOSSSH,

'vyos': VyOSSSH,

根据平台类型的选取,Netmiko 可以理解返回的提示信息以及正确的方式 SSH 到特定设备。一旦建立连接,我们可以使用send方法向设备发送命令。

一旦我们获取到返回值,存储在output变量中的值就会被显示出来,这是我们发送给设备的命令的字符串输出。最后一行,使用disconnect函数,确保我们在完成任务后干净地终止连接。

对于配置(例如:我们需要为路由器接口FastEthernet 0/0提供描述),我们可以像以下示例中那样使用 Netmiko:

from netmiko import ConnectHandler

print ("Before config push")
device = ConnectHandler(device_type='cisco_ios', ip='192.168.255.249', username='cisco', password='cisco')
output = device.send_command("show running-config interface fastEthernet 0/0")
print (output)

configcmds=["interface fastEthernet 0/0", "description my test"]
device.send_config_set(configcmds)

print ("After config push")
output = device.send_command("show running-config interface fastEthernet 0/0")
print (output)

device.disconnect()

以下代码执行的结果如下:

图片

  • 如我们所见,对于config push,我们不需要执行任何额外的配置,只需按照我们将手动发送给路由器的顺序指定命令,并将该列表作为send_config_set函数的参数传递。

  • Before config push中的输出是FastEthernet0/0接口的简单输出,但在After config push下的输出现在包含了我们使用命令列表配置的描述。

以类似的方式,我们可以将多个命令传递给路由器,Netmiko 将进入配置模式,将这些命令写入路由器,然后退出配置模式。

如果我们想要保存配置,请在send_config_set命令之后使用以下命令:

device.send_command("write memory")

这确保了路由器将新推送的配置写入内存。

网络自动化用例

由于我们现在已经与 Python 和设备交互的多个部分进行了交互,让我们创建一个用例来整合我们迄今为止所学到的内容。该用例如下**:

登录到路由器并获取一些信息:

  1. task1(): 显示版本,简要显示 IP,显示时钟,并显示路由器上配置的用户名。

  2. task2(): 在test路由器上创建另一个用户名为test的账户,并检查我们是否可以使用新创建的用户名成功登录。

  3. task3(): 使用新创建的用户名test登录,并从running-config中删除所有其他用户名。一旦完成,返回路由器上配置的所有当前用户名,以确认是否只有test用户名被配置在路由器上。

让我们编写一个脚本来逐一处理这些任务:

from netmiko import ConnectHandler

device = ConnectHandler(device_type='cisco_ios', ip='192.168.255.249', username='cisco', password='cisco')

def task1():
    output = device.send_command("show version")
    print (output)
    output= device.send_command("show ip int brief")
    print (output)
    output= device.send_command("show clock")
    print (output)
    output= device.send_command("show running-config | in username")
    output=output.splitlines()
    for item in output:
        if ("username" in item):
            item=item.split(" ")
            print ("username configured: ",item[1])

def task2():
    global device
    configcmds=["username test privilege 15 secret test"]
    device.send_config_set(configcmds)
    output= device.send_command("show running-config | in username")
    output=output.splitlines()
    for item in output:
        if ("username" in item):
            item=item.split(" ")
            print ("username configured: ",item[1])
    device.disconnect()
    try:
        device = ConnectHandler(device_type='cisco_ios', ip='192.168.255.249', username='test', password='test')
        print ("Authenticated successfully with username test")
        device.disconnect()
    except:
        print ("Unable to authenticate with username test")

def task3():
    device = ConnectHandler(device_type='cisco_ios', ip='192.168.255.249', username='test', password='test')
    output= device.send_command("show running-config | in username")
    output=output.splitlines()
    for item in output:
        if ("username" in item):
            if ("test" not in item):
                item=item.split(" ")
                cmd="no username "+item[1]
                outputnew=device.send_config_set(cmd)
    output= device.send_command("show running-config | in username")
    output=output.splitlines()
    for item in output:
        if ("username" in item):
            item=item.split(" ")
            print ("username configured: ",item[1])

    device.disconnect()

#Call task1 by writing task1()
#task1()
#Call task2 by writing task2()
#task2()
#Call task3 by writing task3()
#task3()

如我们所见,给出的三个任务被定义为三个不同的函数:

  1. 第一行表示我们已经导入了 Netmiko 库,而在第二行中,我们正在使用 Cisco 凭据连接到我们的test路由器。

  2. task1()函数中,我们正在获取所有显示命令的输出。此外,由于我们不希望暴露当前用户名的密码,我们添加了额外的逻辑,即对于show running-config | in username的返回输出,将按每行和每个用户名进行解析,并且每行将按空格字符" "分割。另外,由于思科设备在输出中的第二个位置返回实际用户名(例如,用户名test权限 15 密码 5 *),我们在分割输出字符串后打印第二个项目的值,这就是我们的实际用户名。

这是task1()方法的输出:

图片

  1. task2()方法中,我们将创建一个用户名test并设置密码为test,然后使用新用户名进行认证。我们在该方法中添加了一个try:异常块,该块检查try:部分中所有语句的错误/异常,如果有任何异常,而不是中断脚本,它将运行在except:关键字下的异常部分中的代码。如果没有错误,它将继续执行try:部分中的语句。

这是task2()函数的输出:

图片

我们可以看到现在已经配置了两个用户名,并且路由器现在也成功响应了使用test用户名的认证。

  1. task3()函数中,这首先将获取running-config中所有的用户名,如果有任何用户名不是test,它将创建一个没有用户名<username>的动态命令并发送到路由器。一旦处理完所有用户名,它将继续重新检查并列出不在路由器上的所有用户名。成功标准是只有配置的用户名test应该存在于路由器上。

这是task3()函数的输出:

图片

task3()的结果是所有配置的用户名的结果,在这种情况下现在只有test

摘要

在本章中,我们通过使用函数、条件和循环等高级技术学习了编写脚本的方法;我们介绍了多线程脚本以实现更快和并行执行,熟悉了使用 Netmiko 与网络设备交互,并查看了一个使用单个脚本完成一组实际任务的示例。

下一章将重点介绍使用 Web 进行自动化任务。我们还将讨论如何从 Web 调用 Python 脚本并使用 Web 框架执行任务。

此外,还将简要介绍如何创建自己的 API,以便他人可以使用它执行特定任务。

第三章:从网络中访问和挖掘数据

回顾一下,我们现在对编写 Python 脚本和如何从信息中获取有意义的数据有了相当的了解。我们已经介绍了如何编写 Python 脚本,与网络设备交互,并且还研究了 PowerShell 的基础,以便我们可以使用 PowerShell 和 Python 脚本。现在,我们将通过查看各种示例来深入了解 Python 的使用。在本章中,我们将专注于与各种网络设备一起工作,以挖掘或从设备中获取相关信息,处理这些信息以创建新的配置,并将其推回设备以增加或增强功能。

我们将研究一些我们可能遇到的一些常见场景,并尝试用 Python 来解决它们。这些示例或场景可以根据程序员的实际需求进行扩展,并可作为参考以实现复杂任务的自动化。

我们将介绍的一些关键概念如下:

  • 设备配置

  • 多供应商环境

  • IPv4 到 IPv6 的转换

  • 办公室/数据中心搬迁

  • 站点推广

  • 交换机 BYOD 配置

  • 设备操作系统升级

  • IP 配置/接口解析

设备配置

我们需要部署三个具有标准基本配置的路由器。基本配置在每个路由器上保持不变,但由于每个路由器都不同,我们需要自动化生成每个路由器的三个配置文件。假设所有路由器都具有标准硬件配置,具有相同类型的端口:

图片

如我们在图中所见,路由器 R1、R2 和 R3 的布线如下:

  • R1 f1/0(快速以太网 1/0)连接到 R2 f1/0

  • R1 f0/0 连接到 R3 f0/0

  • R2 f0/1 连接到 R3 f0/1

标准配置或模板如下:

 hostname <hname>
 ip domain-lookup
 ip name-server <nameserver>
 logging host <loghost>
 username cisco privilege 15 password cisco
 enable password cisco
 ip domain-name checkmetest.router
line vty 0 4
 exec-timeout 5

增加一些复杂性,我们需要确保每个路由器的名称服务器不同。如果每个路由器将被部署在不同的网络中,这是我们想要的映射:

  • R1 -> 主机名 testindia

  • R2 -> 主机名 testusa

  • R3 -> 主机名 testUK

日志主机和名称服务器将取决于区域,因此映射如下:

  • 印度路由器:日志服务器(1.1.1.1)和名称服务器(1.1.1.2)

  • 美国路由器:日志服务器(2.1.1.1)和名称服务器(2.1.1.2)

  • 英国路由器:日志服务器(3.1.1.1)和名称服务器(3.1.1.2)

执行请求任务的代码如下:

ipdict={'india': '1.1.1.1,1.1.1.2', 'uk': '3.1.1.1,3.1.1.2', 'usa': '2.1.1.1,2.1.1.2'}

standardtemplate="""
hostname <hname>
ip domain-lookup
ip name-server <nameserver>
logging host <loghost>
username cisco privilege 15 password cisco
enable password cisco
ip domain-name checkmetest.router

line vty 0 4
 exec-timeout 5
"""

routerlist="R1,R2,R3"
routers=routerlist.split(",")
for router in routers:
print ("Now printing config for",router)
    if "R1" in router:
        hostname="testindia"
        getips=ipdict["india"]
        getips=getips.split(",")
        logserver=getips[0]
        nameserver=getips[1]
    if "R2" in router:
        hostname="testusa"
        getips=ipdict["usa"]
        getips=getips.split(",")
        logserver=getips[0]
        nameserver=getips[1]
    if "R3" in router:
        hostname="testUK"
        getips=ipdict["uk"]
        getips=getips.split(",")
        logserver=getips[0]
        nameserver=getips[1]
    generatedconfig=standardtemplate
    generatedconfig=generatedconfig.replace("<hname>",hostname)
    generatedconfig=generatedconfig.replace("<nameserver>",nameserver)
    generatedconfig=generatedconfig.replace("<loghost>",logserver)
    print (generatedconfig)

第一个列表是一个字典,它根据区域定义了日志主机和名称服务器配置。standardtemplate变量用于存储模板。如果我们需要将多行值存储在变量中,我们可以使用前面示例中看到的三引号格式。

现在,因为我们目前知道通用的或默认的主机名,我们可以逐个解析当前的主机名,并根据主机名值生成配置。此输出可以保存到文件中,也可以直接从脚本生成并粘贴到路由器上进行基本配置。同样,我们可以通过添加下一个示例中显示的 IP 地址来增强此脚本,格式为<ipaddress> <subnet mask>

  • testindia f1/0: 10.0.0.1 255.0.0.0

  • testusa f1/0: 10.0.0.2 255.0.0.0

  • testindia f0/0: 11.0.0.1 255.0.0.0

  • testUK f0/0: 11.0.0.2 255.0.0.0

  • testusa f0/1: 12.0.0.1 255.0.0.0

  • testUK f0/1: 12.0.0.2 255.0.0.0

执行此任务的代码如下:

def getipaddressconfig(routername):
    intconfig=""
    sampletemplate="""
    interface f0/0
     ip address ipinfof0/0
    interface f1/0
     ip address ipinfof1/0
    interface f0/1
     ip address ipinfof0/1
    """
    if (routername == "testindia"):
        f0_0="11.0.0.1 255.0.0.0"
        f1_0="10.0.0.1 255.0.0.0"
        sampletemplate=sampletemplate.replace("ipinfof0/0",f0_0)
        sampletemplate=sampletemplate.replace("ipinfof1/0",f1_0)
        sampletemplate=sampletemplate.replace("interface f0/1\n","")
        sampletemplate=sampletemplate.replace("ip address ipinfof0/1\n","")
    if (routername == "testusa"):
        f0_0="11.0.0.1 255.0.0.0"
        f0_1="12.0.0.1 255.0.0.0"
        sampletemplate=sampletemplate.replace("ipinfof0/0",f0_0)
        sampletemplate=sampletemplate.replace("ipinfof0/1",f0_1)
        sampletemplate=sampletemplate.replace("interface f1/0\n","")
        sampletemplate=sampletemplate.replace("ip address ipinfof1/0\n","")
    if (routername == "testUK"):
        f0_0="11.0.0.2 255.0.0.0"
        f0_1="12.0.0.2 255.0.0.0"
        sampletemplate=sampletemplate.replace("ipinfof0/0",f0_0)
        sampletemplate=sampletemplate.replace("ipinfof0/1",f0_1)
        sampletemplate=sampletemplate.replace("interface f1/0\n","")
        sampletemplate=sampletemplate.replace("ip address ipinfof1/0\n","")
    return sampletemplate

#calling this function
myfinaloutput=getipaddressconfig("testUK") #for UK router
myfinaloutput=getipaddressconfig("testindia") #for USA router
myfinaloutput=getipaddressconfig("testusa") #for India router

在这种情况下,我们定义了一个具有标准接口模板的函数。现在,该模板已根据函数中的调用值(即路由器名称)进行了修改,并进行了更新。同时,我们通过用两个双引号""(之间没有空格)表示的无用行替换它们来删除未使用的行。

一旦我们有了生成的配置,我们可以使用简单的文件处理操作来保存它:

#Suppose our final value is in myfinaloutput and file name is myrouterconfig.txt
fopen=open("C:\check\myrouterconfig.txt","w")
fopen.write(myfinaloutput)
fopen.close()

如我们所见,通用模板和接口配置的输出可以连接或添加到名为myfinaloutput的变量中,现在它被保存在C:\check文件夹中的名为myrouterconfig.txt的文件中。

同样,我们可以通过添加更多特定任务的功能来增强脚本,例如开放式最短路径优先OSPF)配置和边界网关协议BGP)配置,根据特定的路由器名称创建增强和复杂的配置,并将它们存储在单独的.txt文件中,以便最终推送到网络设备。

多供应商环境

有时我们有许多供应商参与配置更改,甚至从头开始创建各种模板。我们有一些供应商,如 Arista、Cisco(IOS、NXOS)和 Juniper,在不同层次上参与网络设计。在处理此类情况时,我们需要清楚每个供应商正在工作的层次,并为每种类型的供应商创建动态模板。

在一个场景中,如果我们知道硬件平台和设备的作用(例如接入层、核心层或机架顶部TOR)层),我们可以使用非常基本的参数快速生成配置。

如果一个设备处于生产状态,我们可以使用 SNMP 协议获取该设备的信息,并根据设备的返回类型创建动态值。

作为基本思路,我们可以查看wiki.opennms.org/wiki/Hardware_Inventory_Entity_MIB.

这包含了当前开放标准管理信息库MIB)的信息,该信息由 SNMP 用于获取基本设备信息。

再次遵循良好的实践,我们应该确保我们创建一个通用的函数,该函数可以返回设备类型。此外,SNMP 对象标识符(OIDs)可以深入到获取诸如当前接口数量、接口状态以及哪些接口是操作性的等信息,这样我们就可以根据设备的当前健康状况或从设备获取的信息快速做出智能决策。

我们将安装并使用 PySNMP 库来查询设备上的 SNMP。为了安装它,我们将使用之前的 pip install pysnmp 方法。

基本的 PySNMP 文档可以在以下 URL 中查看:

pynet.twb-tech.com/blog/snmp/python-snmp-intro.html

例如,我们将尝试获取网络设备的当前版本:

from pysnmp.hlapi import *

errorIndication, errorStatus, errorIndex, varBinds = next(
    getCmd(SnmpEngine(),
           CommunityData('public', mpModel=0),
           UdpTransportTarget(('192.168.255.249', 161)),
           ContextData(),
           ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)))
)

if errorIndication:
    print(errorIndication)
elif errorStatus:
    print('%s at %s' % (errorStatus.prettyPrint(),
                        errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
else:
    for varBind in varBinds:
        print(' = '.join([x.prettyPrint() for x in varBind]))

当查询网络设备时,前面代码的示例输出如下:

在我们的测试路由器上,我们使用 snmp-server community public RO 命令启用了 SNMP,并通过执行前面编写的 Python 代码,获取了 RO 字符串 public 以读取 sysDescr.0 值,这在 Cisco 标准中是截断的显示版本。

使用通过 SNMP 获取信息的方法,我们可以发现有哪些类型的设备,并根据输出,我们可以做出智能决策,例如生成特定设备的配置,而无需请求设备类型输入。

此外,这里有一个使用 PySNMP 获取路由器当前接口的示例:

from pysnmp.entity.rfc3413.oneliner import cmdgen

cmdGen = cmdgen.CommandGenerator()

errorIndication, errorStatus, errorIndex, varBindTable = cmdGen.bulkCmd(
    cmdgen.CommunityData('public'),
    cmdgen.UdpTransportTarget(('192.168.255.249', 161)),
    0,25,
    '1.3.6.1.2.1.2.2.1.2'
)

# Check for errors and print out results
if errorIndication:
    print(errorIndication)
else:
    if errorStatus:
        print('%s at %s' % (
            errorStatus.prettyPrint(),
            errorIndex and varBindTable[-1][int(errorIndex)-1] or '?'
            )
        )
    else:
        for varBindTableRow in varBindTable:
            for name, val in varBindTableRow:
                print('%s = %s' % (name.prettyPrint(), val.prettyPrint()))

当在示例路由器上查询接口信息时的输出如下:

如我们所见,我们使用 bulkCmd 方法,该方法遍历所有 SNMP 值并返回接口的输出。

OID 1.3.6.1.2.1.2.2.1.2 被用作参考,从设备中获取这些值。

以类似的方式,我们可以利用不同厂商提供的可用 SNMP OIDs 从多个设备中获取特定信息,并根据返回的值执行预期的任务。

IP 配置/接口解析

在许多情况下,我们需要解析接口配置以获取有用的信息。例如,从一个设备列表中找到所有 trunk 接口。另一个例子可能是找到所有 admin-shutdown(在路由器上关闭)的接口,或者甚至从接口中获取 IP 地址配置。

可能会有这样的情况,我们需要找出特定的 IP 地址或子网是否已在路由器上配置。

提取任何信息的一个好方法是使用正则表达式。正则表达式是一个用于匹配特定模式并从解析的文本中获取匹配的模式或验证是否存在特定模式的术语。

这里是 Python 中使用的一些最基本和最重要的正则表达式:

. 匹配除换行符之外的任何字符
^ 匹配字符串的开始
` .
--- ---
^ 匹配字符串的开始
匹配字符串的末尾
* 匹配 0 或更多重复
+ 匹配 1 或更多重复
? 匹配 0 或 1 次重复
\A 仅匹配字符串的开始
\b 匹配一个空字符串,仅在单词的开始或结束时
\B 匹配一个空字符串,仅在它不是单词的开始或结束时
\d 匹配数字(例如 [0-9])
\D 匹配任何非数字字符(例如 [⁰-9])
\Z 仅匹配字符串的末尾
\ 转义特殊字符
[] 匹配一组字符
[a-z] 匹配任何小写 ASCII 字母
[^] 匹配不在集合中的字符
A&#124;B 匹配 A 或 B 正则表达式(非贪婪)
\s 匹配空白字符(例如 [ \t\n\r\f\v])
\S 匹配非空白字符(例如 [^ \t\n\r\f\v])
\w 匹配任何 Unicode 单词字符(例如 [a-zA-Z0-9_])
\W 匹配任何不是 Unicode 单词字符的字符(例如 [^a-zA-Z0-9_])

从这个字符串中,我的 IP 地址是 10.10.10.20,子网掩码是 255.255.255.255,我们需要使用正则表达式获取 IP 地址和子网掩码:

import re
mystring='My ip address is 10.10.10.20 and by subnet mask is 255.255.255.255'

if (re.search("ip address",mystring)):
    ipaddregex=re.search("ip address is \d+.\d+.\d+.\d+",mystring)
    ipaddregex=ipaddregex.group(0)
    ipaddress=ipaddregex.replace("ip address is ","")
    print ("IP address is :",ipaddress)

if (re.search("subnet mask",mystring)):
    ipaddregex=re.search("subnet mask is \d+.\d+.\d+.\d+",mystring)
    ipaddregex=ipaddregex.group(0)
    ipaddress=ipaddregex.replace("subnet mask is ","")
    print ("Subnet mask is :",ipaddress)

执行上述代码的输出如下:

图片

如我们所见,我们使用的 IP 地址正则表达式是 \d+.\d+.\d+.\d+。这里的 \d 表示一个数字,而 + 表示多次重复,因为我们正在寻找由三个点分隔的多个数字的值。

然而,在我们的情况下,我们在两个地方有这种重复类型,一个在 IP 地址中,另一个在子网掩码中,因此我们修改正则表达式以搜索 ip address is  \d+.\d+.\d+.\d+ 用于 IP 地址和 subnet mask is \d+.\d+.\d+.\d+ 用于子网掩码。if 循环内部的 re.search 命令在找到匹配项时返回 true,如果没有找到匹配项则返回 false。在示例中,一旦我们在 if 条件中找到模式,我们再次使用 re.search 并使用 .group(0) 提取值,现在它包含匹配的正则表达式模式。

由于我们只关心 IP 地址和子网掩码,我们将其他字符串值替换为空白或无值,以便我们只获取特定的 IP 地址和子网掩码值。

此外,使用内置的 socket 库,可能存在检查 IP 地址(IPv4 或 IPv6)是否有效的理由。以下是一个示例:

import socket

def validateipv4ip(address):
    try:
        socket.inet_aton(address)
        print ("Correct IPv4 IP")
    except socket.error:
        print ("wrong IPv4 IP")

def validateipv6ip(address):
    ### for IPv6 IP address validation
    try:
        socket.inet_pton(socket.AF_INET6,address)
        print ("Correct IPv6 IP")
    except socket.error:
        print ("wrong IPv6 IP")

#correct IPs:
validateipv4ip("2.2.2.1")
validateipv6ip("2001:0db8:85a3:0000:0000:8a2e:0370:7334")

#Wrong IPs:
validateipv4ip("2.2.2.500")
validateipv6ip("2001:0db8:85a3:0000:0000:8a2e")

上述代码的输出如下:

图片

使用 socket 库,我们验证 IPv4 和 IPv6 IP 地址。

另一个任务,如我们之前提到的,是找到已启用 trunk 的接口:

import re
sampletext="""
interface fa0/1
switchport mode trunk
no shut

interface fa0/0
no shut

interface fa1/0
switchport mode trunk
no shut

interface fa2/0
shut

interface fa2/1
switchport mode trunk
no shut

interface te3/1
switchport mode trunk
shut
"""

sampletext=sampletext.split("interface")
#check for interfaces that are in trunk mode
for chunk in sampletext:
    if ("mode trunk" in chunk):
        intname=re.search("(fa|te)\d+/\d+",chunk)
        print ("Trunk enabled on "+intname.group(0))

上述代码的输出如下:

图片

在这里,我们需要找出分隔每个接口块的共同配置。正如我们在每个接口配置中看到的那样,单词interface分隔了每个接口的配置,因此我们使用split命令在接口工作上将配置分割成块。

一旦我们有了每个块,我们使用(fa|te)\d+/\d+re模式来获取包含单词trunk的任何块上的接口名称。该模式表示任何以fate开头,后跟一个或多个数字和一个\,然后再次跟一个或多个数字的值将匹配。

同样,在相同的代码中,我们只想知道哪些配置为trunk的接口处于活动状态(未关闭)。以下是代码:

import re
sampletext="""
interface fa0/1
switchport mode trunk
no shut

interface fa0/0
no shut

interface fa1/0
switchport mode trunk
no shut

interface fa2/0
shut

interface fa2/1
switchport mode trunk
no shut

interface te3/1
switchport mode trunk
shut
"""

sampletext=sampletext.split("interface")
#check for interfaces that are in trunk mode
for chunk in sampletext:
    if ("mode trunk" in chunk):
        if ("no shut" in chunk):
            intname=re.search("(fa|te)\d+/\d+",chunk)
            print ("Trunk enabled on "+intname.group(0))

上述代码的输出如下:

图片

我们添加了一个额外条件,仅对那些除了trunk关键字外还有no shut的块进行操作。在这种情况下,我们只对满足这两个条件的块进行操作,在前面的示例中,te3/1不在列表中,因为它处于shut状态。

当验证任何 IP 配置时,我们可以解析配置,获取 IP 地址,验证每个 IP 地址(IPv4 或 IPv6),如果有任何不正确的值,则指出不正确的值。这有助于确保我们正在验证可能由于任何手动复制或粘贴操作而悄悄进入的 IP 地址。当然,这也意味着我们不会看到任何生产问题,因为配置已经使用此逻辑预先验证了正确性。

验证设备配置中任何给定的 IPv4 或 IPv6 地址的代码如下:

import socket
import re

def validateipv4ip(address):
    try:
        socket.inet_aton(address)
    except socket.error:
        print ("wrong IPv4 IP",address)

def validateipv6ip(address):
    ### for IPv6 IP address validation
    try:
        socket.inet_pton(socket.AF_INET6,address)
    except socket.error:
        print ("wrong IPv6 IP", address)

sampletext="""
ip tacacs server 10.10.10.10
int fa0/1
ip address 25.25.25.298 255.255.255.255
no shut
ip name-server 100.100.100.200
int fa0/0
ipv6 address 2001:0db8:85a3:0000:0000:8a2e:0370:7334
ip logging host 90.90.91.92
int te0/2
ipv6 address 2602:306:78c5:6a40:421e:6813:d55:ce7f
no shut
exit

"""

sampletext=sampletext.split("\n")
for line in sampletext:
    if ("ipv6" in line):
        ipaddress=re.search("(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))",line)
        validateipv6ip(ipaddress.group(0))
    elif(re.search("\d+.\d+.\d+.\d+",line)):
        ipaddress=re.search("\d+.\d+.\d+.\d+",line)
        validateipv4ip(ipaddress.group(0))

上述代码的输出如下:

图片

我们从sampletext的每一行中获取 IPv4 或 IPv6 IP 地址,然后将该信息解析到我们的 IP 验证函数中,如果有不正确的 IP 地址,它将打印出不正确的 IP 地址。

同样,我们可以通过创建特定的函数来验证配置的其他方面,并对任何给定的配置执行完整的健全性和验证检查。

设备操作系统升级

有时我们需要升级设备,如路由器、交换机和防火墙。在一个设备上执行升级很容易,但我们需要自动化来升级多个路由器。不同的设备有不同的升级 IOS 或 OS 映像的方式,自动化或脚本根据设备的不同而采用不同的方法。

以升级思科 IOS 路由器为例;需要执行两个基本步骤或任务:

  1. 将相关的 OS 或 IOS 映像复制到flash:bootflash:

  2. 更改配置以使用新映像重新加载路由器。

任务 1:先决条件(复制相关的 OS 或 IOS 映像):

  • 我们需要一个可以从路由器访问的 FTP 服务器,并且服务器上有我们在路由器上需要的 IOS 映像

  • 我们需要映像、正确的 MD5 校验和以及映像大小以进行验证

任务 1 的示例代码如下:

from netmiko import ConnectHandler
import time

def pushimage(imagename,cmd,myip,imgsize,md5sum=None):
    uname="cisco"
    passwd="cisco"
    print ("Now working on IP address: ",myip)
    device = ConnectHandler(device_type='cisco_ios', ip=myip, username=uname, password=passwd)
    outputx=device.send_command("dir | in Directory")
    outputx=outputx.split(" ")
    outputx=outputx[-1]
    outputx=outputx.replace("/","")
    precmds="file prompt quiet"
    postcmds="file prompt"
    xcheck=device.send_config_set(precmds)
    output = device.send_command_timing(cmd)
    flag=True
    devicex = ConnectHandler(device_type='cisco_ios', ip=myip, username=uname, password=passwd)
    outputx=devicex.send_command("dir")
    print (outputx)
    while (flag):
        time.sleep(30)
        outputx=devicex.send_command("dir | in "+imagename)
        print (outputx)
        if imgsize in outputx:
            print("Image copied with given size. Now validating md5")
            flag=False
        else:
            print (outputx)
        if (flag == False):
            cmd="verify /md5 "+imagename
            outputmd5=devicex.send_command(cmd,delay_factor=50)
        if (md5sum not in outputmd5):
            globalflag=True
            print ("Image copied but Md5 validation failed on ",myip)
        else:
            print ("Image copied and validated on ",myip)
    devicex.send_config_set(postcmds)
    devicex.disconnect()
    device.disconnect()

ipaddress="192.168.255.249"
imgname="c3745-adventerprisek9-mz.124-15.T14.bin"
imgsize="46509636"
md5sum="a696619869a972ec3a27742d38031b6a"
cmd="copy ftp://ftpuser:ftpuser@192.168.255.250/c3745-adventerprisek9-mz.124-15.T14.bin flash:"
pushimage(imgname,cmd,ipaddress,imgsize,md5sum)

这段代码将把 IOS 映像推送到路由器。while循环将继续监控代码复制的进度,直到目录中不满足特定的映像大小。当我们达到指定的映像大小时,脚本将移动到下一个动作,即验证 MD5 校验和。一旦 MD5 校验和被验证,它将打印出一个最终确认,表明 IOS 映像已复制并 MD5 验证。

我们可以在任何路由器上使用此功能,只需对映像名称、大小和不同映像集的 MD5 校验和进行一些调整。

这里需要注意的一个重要事项是file prompt quiet命令。在我们开始复制命令之前,需要执行此命令,因为它会抑制路由器中的任何确认提示。如果我们得到这些确认提示,处理所有提示将变得困难,从而增加了代码的复杂性。

通过添加此命令,我们抑制了确认,一旦代码复制完成,我们就将其启用到默认的文件提示状态。

任务 2:将路由器的 bootvar 更改为新的 OS 映像

这是我们设置 Cisco 中的 bootvar,使其指向要加载的新 IOS 映像的地方:

from netmiko import ConnectHandler
import time

uname="cisco"
passwd="cisco"
device = ConnectHandler(device_type='cisco_ios', ip="192.168.255.249", username=uname, password=passwd)
output=device.send_command("show run | in boot")
print ("Current config:")
print (output)
cmd="boot system flash:c3745-adventerprisek9-mz.124-15.T14.bin"
device.send_config_set(cmd)
print ("New config:")
output=device.send_command("show run | in boot")
print (output)
device.send_command("wr mem")
device.disconnect()

上述代码的输出如下:

图片

如我们所见,在这段代码中,我们使用新映像创建一个命令,并通过send_config_set方法将其发送到路由器。此方法在config t下执行命令。一旦完成,我们再次运行show run | in boot命令以获取新的输出,以验证 bootvar 现在是否指向新的 OS 映像。

如果一切正常,我们就运行wr mem来保存这个新配置。

一旦两个任务都完成,我们需要重新加载路由器以使更改生效。在重新加载之前,需要处理多个场景。可以直接使用reload命令作为任务 3 执行直接重新加载,但作为最佳实践,我们需要确保路由器上没有生产或实时流量,因为重新加载将中断当前的流量流。此外,建议登录到控制台以验证重新加载进度,并在重新加载失败时快速恢复。

IPv4 到 IPv6 转换

将 IPv4 地址转换为 IPv6 地址有多种方法。在 Python 3 中,我们有内置的ipaddress模块:

import ipaddress

def convertusingipaddress(ipv4address):
    print(ipaddress.IPv6Address('2002::' + ipv4address).compressed)

convertusingipaddress("10.10.10.10")
convertusingipaddress("192.168.100.1")

上述代码的输出如下:

图片

ipaddress库中,有许多不同的方法或函数可供我们用于各种目的。文档和详细信息可以在docs.python.org/3/library/ipaddress.html.找到。

站点部署

随着我们继续在多供应商环境中工作,有需求快速部署设备和配置以使特定地点上线运行。可以部署多种技术进行地点部署,这涉及到一组标准设备连接到标准端口,每个设备上都有标准的 IOS 或代码镜像,准备上架和供电。为了确定特定地点的标准库存单位SKU),我们可以将其划分为 T 恤尺寸。在规划阶段,我们可以根据某些参数创建 T 恤尺寸,例如使用情况、负载和冗余。

在最低级别,比如说超小尺寸XS)可以只有一个路由器和一台交换机,路由器终止于互联网链路。交换机连接到路由器的FastEthernet 0/1(100 Mbps)或Gi0/1(1000 Mbps)端口,最终用户直接将设备插入交换机以获取访问权限。基于这个 XS SKU(或 T 恤尺寸),我们可以确定每个路由器和交换机的硬件供应商,例如 Cisco、DLink 或其他网络设备提供商。接下来,当我们确定了硬件供应商后,我们开始生成配置模板。

配置模板通常基于两个标准:

  • 设备的作用

  • 硬件供应商

在相同的 XS 尺寸中,比如说我们有一个作为路由器的 Cisco 3064(运行 Cisco NXOS 的 Cisco Nexus)和一个在交换层中的 Alcatel 交换机。既然我们已经确定了硬件供应商和每个设备的作用,我们就可以轻松地创建模板配置。

如前所述,一旦我们有了标准硬件,我们还需要确保端口是标准的(例如,交换机的上行链路将从端口Gi1/0连接到路由器的Gi1/1)。这将帮助我们确保我们创建了一个几乎完整的模板,同时考虑了接口配置。

模板包含一个基本的配置,某些值将在以后确定。这是一个非常通用的布局,我们可以用各种输入的值来填充,例如识别可用的 IP 地址、序列中的下一个主机名,以及作为标准配置需要放置哪种路由:

图片

如前图所示,中央 Python 脚本正在调用不同的函数(初始输入为供应商和基于角色的标准模板),并获取特定的信息,如可用的 IP 地址、下一个可用的主机名(如rtr01rtr05)以及路由信息(如增强型内部网关路由协议EIGRP)在网络上广播子网10.10.10.0/255)。这些输入以及更多(根据需求而定)都是独立的 Python 函数,模板根据 Python 函数的返回值进行更改。

例如,我们需要使用 Python 从 SQL 表中获取 IP 地址,其中 IP 地址显示为未分配(我们将使用 Python 中的MySQLdb库来完成此操作):

import MySQLdb

def getfreeip():
    # Open database connection
    db = MySQLdb.connect("testserver","user","pwd","networktable" )
    cursor = db.cursor()

    sql = "select top 1 from freeipaddress where isfree='true'"
    try:
       # Execute the SQL command
       cursor.execute(sql)
       # Fetch all the rows in a list of lists.
       results = cursor.fetchall()
       for eachrow in results:
          freeip=eachrow[0]
          return (freeip)
    except:
       print "Error: unable to fetch data"
       return "error in accessing table"
    db.close()

print (getfreeip())

这将从 SQL 表中返回一个空闲 IP 地址,我们可以将其调用到其他函数中以生成我们的配置。当然,一旦给出这个,我们还需要更新表以确保我们将记录中的isfree值设置为false,这样对函数的新调用将确保我们获取 SQL 表中的下一个空闲 IP 地址。

将所有这些结合起来,我们可以从多个表中获取详细信息,甚至调用特定工具的 API 以获取专门的信息,并且,将这些函数或方法的返回值作为输入,模板将使用这些返回值替换模板中指定的变量。一旦模板值填写完毕,输出将是最终生成的配置,该配置已准备好部署到路由器/网络设备上。

通过根据 T 恤尺寸规范创建此基线自动化,脚本可以再次被调用,以包含新的一组设备,例如负载均衡器、多个路由器,以及根据 T 恤尺寸和复杂度,不同角色的每个不同路由器。

在生成最终配置模板之后,下一步是将配置应用到路由器上。始终建议使用控制台执行此功能。一旦我们有了基本的配置以获取对设备的 SSH/Telnet 访问权限,我们就可以在控制台上保持会话打开,同时将剩余的配置推送到各个设备上。Netmiko 可用于此目的,目的是使用新生成的模板推送所有配置。

假设电缆已正确连接,符合标准,下一步是验证流量和配置。为此,我们再次依赖 Netmiko 来获取路由、日志以及诸如接口计数器和 BGP 路由器表等特定信息。

此外,我们还可以对 SNMP 输入进行工作以验证每个设备的当前运行健康状态。设备有时在测试条件下表现良好,但一旦生产或实时流量在其数据平面上,它可能会在硬件资源上激增,导致延迟或数据包丢失。SNMP 统计信息将给我们一个清晰的每个设备的健康状况,例如 CPU 使用率、内存使用率,甚至某些设备的当前温度及其模块,以显示 SKU 或 T 恤尺寸站点的整体健康状况。

办公/数据中心搬迁

有时候,我们需要将站点搬迁、关闭或迁移到不同的位置。这涉及到大量的预检查、预验证,并确保网络 PoD 的相同设置在另一个位置是活跃的。

在一个多厂商环境中,随着 T 恤尺码的增加,SKU 数量的增加,手动跟踪所有活跃会话、流量、当前接口状态和特定路由是困难的。使用 Python,我们可以创建一个自动化的基本清单,并确保在搬迁后,相同的清单可以作为后验证清单。

例如,我们创建一个基本的脚本,询问是否需要执行预检查/后检查,并将结果保存到名为pre-checkpost-check的文件中:

from netmiko import ConnectHandler
import time

def getoutput(cmd):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip="192.168.255.249", username=uname, password=passwd)
    output=device.send_command(cmd)
    return (output)

checkprepost=input("Do you want a pre or post check [pre|post]: ")
checkprepost=checkprepost.lower()
if ("pre" in checkprepost ):
    fname="precheck.txt"
else:
    fname="postcheck.txt"

file=open(fname,"w")
file.write(getoutput("show ip route"))
file.write("\n")
file.write(getoutput("show clock"))
file.write("\n")
file.write(getoutput("show ip int brief"))
file.write("\n")

print ("File write completed",fname)

file.close()

上述代码的输出如下:

假设precheck.txt文件在迁移或搬迁之前在多个设备所在的现场被采集,而postcheck.txt文件是在搬迁之后在现场采集的。现在让我们编写一个简单的脚本,比较这两个文件并打印出差异。

Python 有一个名为difflib的库来执行这个任务:

import difflib

file1 = "precheck.txt"
file2 = "postcheck.txt"

diff = difflib.ndiff(open(file1).readlines(),open(file2).readlines())
print (''.join(diff),)

上述代码的输出如下:

如我们在precheck.txtpostcheck.txt中看到的,文件是逐行比较的。任何没有变化的内容都按原样显示,但任何有差异的内容都会用-+来表示。行首的-符号指定了该特定行来自第一个文件(在我们的例子中是precheck.txt),而+符号表示相同的行已经在新的文件(即postcheck.txt)中输出。使用这种方法,我们可以快速验证precheckpostcheck之间的差异,并在迁移或搬迁后修复相关的问题。

有时候我们希望自动运行脚本以备份当前路由器的配置。在这种情况下,让我们假设搬迁计划在明天进行。在开始任何活动之前,我们想要确保我们有当前设备配置的备份。

一个简单的脚本就能解决问题:

from netmiko import ConnectHandler

def takebackup(cmd,rname):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=rname, username=uname, password=passwd)
    output=device.send_command(cmd)
    fname=rname+".txt"
    file=open(fname,"w")
    file.write(output)
    file.close()

# assuming we have two routers in network 
devices="rtr1,rtr2"
devices=devices.split(",")

for device in devices:
    takebackup("show run",device)

脚本将逐个解析设备列表中的每个设备,执行show run命令,并将其保存到给定的文件名中(文件名与给定的设备名或 IP 相同)。然而,下一个问题是如何确保它在预定的时间运行。在 Linux 中,我们有 cron 作业可以设置用于此目的,Windows 也有任务计划程序。

以下示例显示了在任务计划程序中创建任务的基本过程:

  1. 在 Windows 中打开任务计划程序:

  1. 在任务计划程序的右侧单击“创建基本任务”:

  1. 点击“下一步”并选择任务的频率:

  1. 点击下一步,选择时间,然后再次点击下一步。转到启动程序。在这个时候,您需要添加以下屏幕截图中显示的详细信息。我们必须在程序/脚本窗口中提供python.exe的完整路径,并在添加参数(可选)部分,用双引号括起 Python 脚本(带有.py扩展名)的完整路径:

图片

  1. 在最后一页,点击完成以提交更改并创建任务:

图片

完成此操作后,您可以通过右键单击创建的任务并点击运行选项来手动运行它。如果任务成功,它将在任务计划程序窗口中返回相同的结果。如果一切正常,任务将在任务计划程序中指定的时间自动运行。

这也可以作为一个服务运行,并且可以按照固定的间隔,例如每天和每小时,根据我们希望在环境中运行脚本的频率来决定。

这些计划好的备份有时可以作为基线,也可以作为已知良好配置的场景。

交换机的自带设备(BYOD)配置

随着我们的网络变得更加可扩展,我们需要拓宽我们当前的网络架构设计,以纳入更好的交换机和路由器来满足需求。有时我们可能会有特定的需求,需要将特定的硬件添加到我们的网络中以满足这些需求。

另一个可能的需求可能是降低成本同时提高可扩展性。在这种情况下,我们需要添加不同供应商的交换机来满足需求。也可能会有对某个办公室或地点的非常具体的需求。在这种情况下,我们需要添加不同供应商的硬件来满足一些特定的要求。

我们刚才观察到的所有场景都有一个共同点。为了满足需求或特定要求,我们不能仅仅依赖网络上的单一供应商解决方案。我们需要一个随机集合的设备来确保满足特定的要求。这就是我们引入 BYOD(Bring Your Own Device,自带设备)概念的地方。BYOD 是一个新的标准,它接纳新的设计、硬件和架构,以适应我们当前的 SKU 或设计。它可能简单到只是通过无线方式将一部新手机添加到我们的企业网络,或者稍微复杂一些,比如将特定供应商的硬件添加到网络中。

架构师需要确保他们有良好的预测需求的方法,并了解当前的网络设计或硬件是否能够满足这些需求。在任何情况下,初始设计都需要确保当前技术支持跨厂商平台。在这个设计方法中存在一些冲突。例如,某些厂商,如思科,拥有邻居发现协议,思科特定协议 (CDP),它可以发现当前设备的正确思科设备作为邻居。然而,为了确保 CDP 能够发现并显示正确的信息,每个设备都需要是思科的。另一方面,我们有 链路层发现协议 (LLDP),它与 CDP 几乎相同,但它是开源的,因此许多其他厂商,包括思科,也有选择使用 LLDP 而不是 CDP 进行发现的机会。现在,思科 CDP 是一个思科特定协议;思科确保某些参数只能通过 CDP 交换或发现,因此参与 CDP 的每个设备都必须是思科的设备。

LLDP 作为开源协议,仅限于开放标准或 互联网工程任务组 (IETF) 框架中的参数,并且所有支持 LLDP 的厂商仅遵守这些开放标准以实现跨平台和硬件兼容性。这也导致一些参与厂商不会发送或发现专门为该厂商设计的参数(例如思科)。回到之前提到的观点,在这种情况下,从第一天开始的设计架构需要确保多厂商或开源标准仅用于基本设计或架构。LLDP 的一个类似例子是使用开放标准如 OSPF 或 BGP 而不是 EIGRP,后者仅适用于思科设备。

如前所述,我们需要定义特定的角色,并根据我们在当前设计中引入的设备或硬件创建硬件或厂商模板,这些模板应基于我们作为 BYOD 策略引入的设备。保持开放标准的方法,我们需要确保创建的模板是通用的,并且可以稍后将其特定厂商的配置引入到设备中。

SNMP 是一种强大的协议,它有助于无缝地管理许多跨厂商或 BYOD 策略。通过基本配置启用 SNMP 并使用特定的只读社区字符串,我们可以用 Python 编写快速脚本以从 BYOD 设备获取基本信息。以一个例子来说明,假设我们需要知道以下两个设备的类型和厂商:

from pysnmp.hlapi import *

def finddevices(ip):
    errorIndication, errorStatus, errorIndex, varBinds = next(
        getCmd(SnmpEngine(),
               CommunityData('public', mpModel=0),
               UdpTransportTarget((ip, 161)),
               ContextData(),
               ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)))
    )

    if errorIndication:
        print(errorIndication)
    elif errorStatus:
        print('%s at %s' % (errorStatus.prettyPrint(),
                            errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
    else:
        for varBind in varBinds:
            print(' = '.join([x.prettyPrint() for x in varBind]))

ipaddress="192.168.255.248,192.168.255.249"
ipaddress=ipaddress.split(",")
for ip in ipaddress:
    print (ip)
    finddevices(ip)
    print ("\n")    

之前代码的输出如下:

如前述输出所示,现在我们只需要知道 IP 地址和开放标准 SNMP OID,即 SNMPv2-MIB。sysDescr将为两个设备提供输出。在这种情况下,我们可以看到一个是 Cisco 3600,另一个是 Cisco 3700。根据返回的信息,我们可以进行配置。

根据 BYOD 策略,需要执行各种其他任务。如果您想将一部手机连接到您的网络,所需的就是连接到企业网络,以及可以推送到设备以检查各种合规性检查(如操作系统和防病毒软件)的策略。根据这些结果,查询的设备可以被放置在另一个 VLAN 中,可以称为隔离 VLAN,它具有非常有限的可访问性,或者企业 VLAN,它可以完全访问企业资源。

以类似的方式,作为交换机 BYOD 策略的一部分,我们需要执行某些检查以确保设备适合成为我们网络设计的一部分。是的,我们需要为各种类型的设备保持开放政策,但需要在设备能够符合 BYOD 接受资格的松散耦合框架下。

让我们看看一个确保设备足够兼容以成为 BYOD 框架一部分的例子。核心要求是 Cisco 交换机,并且它应该有一个FastEthernet0/0作为其接口之一:

from pysnmp.hlapi import *
from pysnmp.entity.rfc3413.oneliner import cmdgen

cmdGen = cmdgen.CommandGenerator()

def validateinterface(ip):
    errorIndication, errorStatus, errorIndex, varBindTable = cmdGen.bulkCmd(
        cmdgen.CommunityData('public'),
        cmdgen.UdpTransportTarget((ip, 161)),
        0,25,
        '1.3.6.1.2.1.2.2.1.2',
        '1.3.6.1.2.1.2.2.1.7'
    )
    flag=False
    # Check for errors and print out results
    if errorIndication:
        print(errorIndication)
    else:
        if errorStatus:
            print('%s at %s' % (
                errorStatus.prettyPrint(),
                errorIndex and varBindTable[-1][int(errorIndex)-1] or '?'
                )
            )
        else:
            for varBindTableRow in varBindTable:
                for name, val in varBindTableRow:
                    if ("FastEthernet0/0" in val.prettyPrint()):
                        flag=True
    if (flag):
        return True
    else:
        return False

def finddevice(ip):
    errorIndication, errorStatus, errorIndex, varBinds = next(
        getCmd(SnmpEngine(),
               CommunityData('public', mpModel=0),
               UdpTransportTarget((ip, 161)),
               ContextData(),
               ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)))
    )

    if errorIndication:
        print(errorIndication)
    elif errorStatus:
        print('%s at %s' % (errorStatus.prettyPrint(),
                            errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
    else:
        for varBind in varBinds:
            if ("Cisco" in varBind.prettyPrint()):
                return True
    return False

mybyoddevices="192.168.255.249,192.168.255.248"
mybyoddevices=mybyoddevices.split(",")
for ip in mybyoddevices:
    getvendorvalidation=False
    getipvalidation=False
    print ("Validating IP",ip)
    getipvalidation=validateinterface(ip)
    print ("Interface has fastethernet0/0 :",getipvalidation)
    getvendorvalidation=finddevice(ip)
    print ("Device is of vendor Cisco:",getvendorvalidation)
    if getipvalidation and getvendorvalidation:
        print ("Device "+ip+" has passed all validations and eligible for BYOD")
        print ("\n\n")
    else:
        print ("Device "+ip+" has failed validations and NOT eligible for BYOD")
        print ("\n\n")  

之前代码的输出如下:

图片

我们解析了两个设备,并使用开源 SNMP 获取厂商和接口信息。接下来,我们进行验证,并根据我们的条件返回TrueFalse。所有检查条件都为True将导致设备作为 BYOD 被接受。

让我们稍微改变一下规则。假设任何具有以太网接口的设备都不符合 BYOD 资格:

from pysnmp.hlapi import *
from pysnmp.entity.rfc3413.oneliner import cmdgen

cmdGen = cmdgen.CommandGenerator()

def validateinterface(ip):
    errorIndication, errorStatus, errorIndex, varBindTable = cmdGen.bulkCmd(
        cmdgen.CommunityData('public'),
        cmdgen.UdpTransportTarget((ip, 161)),
        0,25,
        '1.3.6.1.2.1.2.2.1.2',
        '1.3.6.1.2.1.2.2.1.7'
    )
    flag=False
    # Check for errors and print out results
    if errorIndication:
        print(errorIndication)
    else:
        if errorStatus:
            print('%s at %s' % (
                errorStatus.prettyPrint(),
                errorIndex and varBindTable[-1][int(errorIndex)-1] or '?'
                )
            )
        else:
            for varBindTableRow in varBindTable:
                for name, val in varBindTableRow:
                    if ((val.prettyPrint()).startswith("Ethernet")):
                        return False
                    if ("FastEthernet0/0" in val.prettyPrint()):
                        flag=True
    if (flag):
        return True
    else:
        return False

def finddevice(ip):
    errorIndication, errorStatus, errorIndex, varBinds = next(
        getCmd(SnmpEngine(),
               CommunityData('public', mpModel=0),
               UdpTransportTarget((ip, 161)),
               ContextData(),
               ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)))
    )

    if errorIndication:
        print(errorIndication)
    elif errorStatus:
        print('%s at %s' % (errorStatus.prettyPrint(),
                            errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
    else:
        for varBind in varBinds:
            if ("Cisco" in varBind.prettyPrint()):
                return True
    return False

mybyoddevices="192.168.255.249,192.168.255.248"
mybyoddevices=mybyoddevices.split(",")
for ip in mybyoddevices:
    getvendorvalidation=False
    getipvalidation=False
    print ("Validating IP",ip)
    getipvalidation=validateinterface(ip)
    print ("Device has No Ethernet only Interface(s) :",getipvalidation)
    getvendorvalidation=finddevice(ip)
    print ("Device is of vendor Cisco:",getvendorvalidation)
    if getipvalidation and getvendorvalidation:
        print ("Device "+ip+" has passed all validations and eligible for BYOD")
        print ("\n\n")
    else:
        print ("Device "+ip+" has failed validations and NOT eligible for BYOD")
        print ("\n\n")    

之前代码的输出如下:

图片

如本例所示,我们对以Ethernet关键字开头的任何接口进行了验证。使用string.startswith("given string")函数来评估任何给定的字符串是否是其正在比较的字符串的开头。在我们的情况下,具有 IP 地址192.168.255.248的设备有一个仅以太网接口,该接口在验证仅以太网接口时返回了True值。由于这被认为是我们验证的失败,因此返回了False,并且脚本将其称为 BYOD 接受失败,因为这一特定条件失败了。

以类似的方式,我们可以对任何数量的设备进行验证和多项检查,并确保只有通过 BYOD 框架检查的设备被接受到网络中。

摘要

在本章中,我们研究了各种复杂场景,以了解新的站点迁移和验证是如何进行的。我们还探讨了多厂商配置的概念,创建模板,为设备生成配置,以及 IPv4 到 IPv6 迁移技术。

我们专注于特定数据的专项提取,例如 IP 地址,以及该数据的验证,以及数据失败或接受的条件。此外,还讨论了站点推广和 BYOD 策略,以及最佳实践,如 T 恤尺码和 BYOD 条件的验证。

在下一章中,我们将进一步深入,介绍 Web 启用框架。这将帮助我们创建自己的 API,并创建基于浏览器的 Python 脚本,这些脚本可以从任何地方执行。

第四章:用于自动化触发的 Web 框架

随着我们继续前进并提高对编码技术和 Python 的理解,下一步是确保脚本在没有最终用户实际在本地运行代码的情况下执行,并确保代码执行的平台或操作系统独立的方法。本章重点是将我们的脚本放在 Web 平台上。我们将涵盖以下主题:

  • 创建可访问 Web 的脚本示例

  • 从 HTML/动态 HTML 访问脚本

  • 使用 IIS 理解和配置 Web 框架的环境

  • API 的基本原理以及使用 C# 创建示例 API

  • 在 Python 中使用 API

  • 创建任务以理解 Web 框架的完整端到端功能

为什么创建基于 Web 的脚本/框架?

Web 框架 是一组脚本集合,托管在 Web 平台(如 Internet Information Services(IIS)**(在 Windows 上)或 Apache(在 Linux 上))上,并使用前端基于 Web 的语言(如 HTML)调用相同的脚本。

有时候人们会问为什么我们要迁移我们当前的脚本或创建 Web 框架上的脚本。答案非常简单。Web 框架确保我们的脚本仅通过浏览器就能被多个最终用户使用。这给了程序员在他们的首选平台(如 Windows 或 Linux)上编写脚本的独立性,人们可以在他们选择的浏览器上使用这些脚本。他们不需要理解你如何编写代码,或者你在后端调用或使用什么,当然,这也确保了你的代码不会被最终用户直接看到。

假设你已经编写了一个调用四个或五个库以执行特定任务的脚本。虽然有一些通用库,但正如我们在前面的章节中看到的,某些特定库需要安装以执行这些任务。在这种情况下,如果你想确保最终用户能够执行你的脚本,他们需要在他们的机器上安装相同的库。此外,他们需要在他们的机器上运行 Python 环境,没有这个环境脚本将无法运行。因此,为了运行一个五行的简单脚本,用户需要通过安装 Python、安装库等方式来定制他们的环境。

由于机器上的限制(例如不允许安装),这可能对许多用户来说并不可行,因此尽管有运行这些脚本的需求,但用户将无法使用这些脚本,这实际上会降低效率。但是,如果给这些用户提供选择,他们可以轻松地打开他们选择的浏览器并像打开任何其他网页一样使用这些脚本,这将确保我们的脚本为任务带来更高的效率。

理解和配置 IIS 以用于 Web 框架

在这里,我们将关注 IIS 是什么以及如何配置它,以确保我们的 Python 脚本能够通过利用 Web 服务器框架的力量来执行。

理解 IIS

IIS 是 Windows 上可用的一个工具,用于托管网络服务。换句话说,如果我们安装了 IIS,我们确保安装了它的机器现在正在充当网络服务器。IIS 是一个完全功能的程序,可在 Windows 的“添加或删除程序”中找到。它支持机器成为网络服务器、FTP 服务器以及其他一些功能。

以下截图显示了在 Windows 中使用 IIS 图标安装并打开 IIS 后出现的第一个屏幕:

图片

如截图所示,应用程序的左侧表示服务器名称,右侧显示我们可以为不同目的配置的属性。

在安装 IIS 时选择 通用网关接口CGI)支持是很重要的。在 Windows 的“添加或删除程序”中安装 IIS 后,Windows 会给我们选择 IIS 中特定子项的选项,其中 CGI 和 CGI 支持是一个选项。如果在安装过程中没有选择此选项,Python 脚本将无法从网络服务器上运行。

配置 IIS 以支持 Python 脚本

现在,让我们配置 IIS,以确保它支持在 Web 服务器本身上执行 Python 脚本,并允许最终用户通过从 Web 服务器调用 Web URL 来直接运行 Python 脚本。以下是要执行此操作的步骤:

  1. 当我们展开左侧的属性时,我们看到默认网站选项。如果你右键单击它,有一个名为添加应用程序的章节。点击它以查看以下截图:

图片

在这个屏幕上,我们必须输入两个特定的值:

    • 别名:这是我们 Web URL 的一部分值。例如,如果我们的选择别名是 test,则 http://<servername>/test 将是 URL。

    • 物理路径:这是我们脚本实际物理目录映射的位置。例如,我们的脚本 testscript.py 的路径如下。要从 URL 调用它,我们将在浏览器中输入以下内容:

http://<server IP>/test/testscript.py

一旦我们有了这些值,我们点击“确定”,我们的网站引用就创建完成了。

  1. 现在我们需要将我们的 Python 脚本映射到在执行时使用 Python 解释器。一旦我们创建了网站,我们会在右侧面板看到一个名为处理器映射(Handler Mappings)的选项。点击它并打开如下截图所示的章节。要添加 Python 引用,请点击截图右侧的“添加脚本映射...”:

图片

在这个部分,我们需要填写三个值:

    • 请求路径:这始终是 *.py,因为我们调用的任何脚本都将具有 .py 扩展名。

    • 可执行文件:这是一个重要的部分,其中我们引用了python.exe的实际位置。需要python.exe的完整路径。除了路径之外,我们还需要在可执行文件路径后添加两次%s,因为这被解释为从 IIS 传递的参数。例如,如果我们的 Python 路径是C:\Python,那么我们会添加以下内容:

C:\Python:\python.exe %s %s
    • 名称:这是对当前配置的简单引用名称。可以是任何你喜欢的名称。
  1. 在“添加脚本映射”部分中有一个名为“请求限制”的按钮。我们需要点击该按钮,然后在“访问”下选择“执行”选项并点击确定:

图片

  1. 一旦我们点击确定,IIS 会弹出一个提示,允许扩展。我们需要选择是,以便设置生效:

图片

  1. 作为最后一步,我们选择新创建的脚本映射(在我们的例子中是Python),然后在右侧点击“编辑功能权限...”。在对话框中,选择“执行”选项并点击确定:

图片

一旦遵循所有前面的步骤,我们就有一个运行环境,支持从 Web 浏览器执行 Python 脚本。

一旦 Web 服务器启动,你可以通过在 Web 服务器本身调用默认页面来测试以确保其配置正确。这可以通过浏览器中的http://localhost URL 来完成,它应该显示欢迎 IIS 页面。如果它没有显示出来,那么我们需要回到并验证 Web 服务器的安装,因为这意味着 Web 服务器没有启动和运行。

创建特定于 Web 的脚本

现在我们有一个运行环境,可以运行我们的脚本,让我们创建一个非常基本的脚本来看看它是如何工作的:

print('Content-Type: text/plain')
print('')
print('Hello, world!')

在 IDLE 中,我们输入前面的代码并将其保存为 Python 文件(例如testscript.py)。现在,正如我们之前讨论的,对于我们的 Web 引用,我们在 IIS 中映射了一个物理目录或位置。新创建的testscript.py需要在该文件夹中才能从 Web 访问。

基于 Web 的 URL 调用 Python 脚本的输出如下:

图片

  • 如前述截图所示,脚本现在是通过浏览器使用 localhost URL 调用的。输出是一个简单的Hello, world !,这是在脚本代码中调用来打印的。

  • 此外,值Content-Type: text/plain指定 Python 的返回值将是简单的文本,浏览器将其解释为纯文本而不是 HTML。

现在让我们看看如何将其修改为 HTML 的示例:

print('Content-Type: text/html')
print('')
print("<font color='red'>Hello, <b>world!</b></font>")

修改值后的 URL 输出如下:

图片

如我们所见,代码的第一行已被修改为Content-Type: text/html 这确保了现在从脚本返回的文本是 HTML,因此最后的打印语句,带有font colorredworld!在粗体 HTML 标签中的内容,现在在浏览器中被正确解释。在现实场景中,如果我们想从我们的脚本中打印通过、失败或其他任何特定的消息或输出,我们应该以 HTML 颜色编码和粗体格式返回值,以便在浏览器中清晰可读。

让我们看看在 HTML 中以表格格式打印 5 的示例:

print('Content-Type: text/html')
print('')
value=5
xval=0
tval="<table border='1' style='border-collapse: collapse'><tr><th>Table for "+str(value)+"</th></tr>"
for xval in range(1,11):
    mval=value*xval
    tval=tval+"<tr><td>"+str(value)+"</td><td>*</td><td>"+str(xval)+"</td><td>=</td><td><font color='blue'><b>"+str(mval)+"</b></font></td></tr>"

tval=tval+"</table>"

print(tval)

上述代码的输出如下:

图片

  • 如我们所见,第一行指示返回类型为 HTML。在接下来的几行中,我们使用名为value的变量,其值为5。使用for循环,我们在tval变量中创建 HTML 表格及其值(对于每一行和单元格)。

  • 最后一条语句将tval变量的值返回到调用脚本的浏览器。

深入这个示例,现在让我们创建相同的表格,但数字需要由 Web 用户在 URL 中提供。换句话说,而不是在我们的示例中坚持静态值5,表格需要为用户在 URL 中输入的值生成:

import cgi

form = cgi.FieldStorage()
value=int(form.getvalue('number'))

print('Content-Type: text/html')
print('')
xval=0
tval="<table border='1' style='border-collapse: collapse'><tr><th>Table for "+str(value)+"</th></tr>"
for xval in range(1,11):
    mval=value*xval
    tval=tval+"<tr><td>"+str(value)+"</td><td>*</td><td>"+str(xval)+"</td><td>=</td><td><font color='blue'><b>"+str(mval)+"</b></font></td></tr>"

tval=tval+"</table>"

print(tval)

上述代码的输出如下:

图片

  • 如我们在 URL 变化中看到的那样,我们使用增强的 URL http://localhost/test/testscript.py?number=8传递数字。问号后面的指定值,即作为传递给参数number的值,现在被作为脚本中的输入。代码现在导入一个特定的内置库cgi,以读取从浏览器传递给自身的参数。

  • 接下来是这两行:

form = cgi.FieldStorage()
value=int(form.getvalue('number'))

它们用于获取从浏览器返回的表单的引用,并且从表单中获取名为number的特定参数 返回的参数始终以字符串格式,因此我们需要确保将其转换为我们的特定数据类型,具体取决于我们的使用。

  • value变量现在有了我们从浏览器传递的数字,其余的脚本以与前面示例中给出相同的方式进行执行。

如前述示例所示,最终用户现在只根据他们的需求调用脚本,而不关心后端逻辑或程序。对于开发者来说,如果在脚本中发现了错误,一旦最终用户开始获得正确的结果,就可以在主 Web 服务器上立即修复。与用户从他们的机器上的特定位置下载新的修复脚本然后自行运行相比,这也节省了大量精力。有时,甚至从浏览器中调用带有参数的脚本也变得有点困难。在这种情况下,我们使用 HTML 中的表单标签将值传递给脚本以获取输出。

例如,要求用户输入他们的名字,需要生成表格的数字,并以友好的方式输出生成的表格,其中包含呼叫者的名字。以下是 HTML 代码:

<html>
<form action="testscript.py" method="get">
 Enter your name: <br>
  <input type="text" name="name">
  <br>
  Enter your number:<br>
  <input type="text" name="number">
  <br><br>
  <input type="submit" value="Submit">
</form>
</html>    

这是 Python 代码:

import cgi

form = cgi.FieldStorage()
value=int(form.getvalue('number'))
callername=form.getvalue('name')

print('Content-Type: text/html')
print('')
xval=0
tval="<h2>Hello <font color='red'>"+callername+"</font><h2><br><h3>Your requested output is below:</h3>"
tval=tval+"<table border='1' style='border-collapse: collapse'><tr><th>Table for "+str(value)+"</th></tr>"
for xval in range(1,11):
    mval=value*xval
    tval=tval+"<tr><td>"+str(value)+"</td><td>*</td><td>"+str(xval)+"</td><td>=</td><td><font color='blue'><b>"+str(mval)+"</b></font></td></tr>"

tval=tval+"</table>"

print(tval)

上述代码的输出如下:

图片

HTML 页面

使用 HTML 代码,我们创建了一个表单,该表单需要我们脚本所需的输入。在这种情况下,它要求输入一个名字和需要生成表格的数字。一旦用户输入这些信息,就需要点击提交按钮,以便将值传递到脚本中:

图片

脚本输出

当用户点击提交按钮时,值会被传递到脚本中。在代码中,我们使用相同的 form.getvalue() 方法获取每个 HTML 元素的值。一旦脚本从浏览器中获取了值,脚本逻辑就会处理需要返回的内容。在我们的例子中,正如我们所看到的,用户名已经在浏览器中显示,同时还有用户想要看到的输出表格。

让我们以一个例子来说明,我们输入设备的 IP 地址和我们想从设备中看到的命令,使用表单和浏览器输出。以下是 HTML 代码:

<html>
<form action="getweboutput.py" method="get">
 Enter device IP address: <br>
  <input type="text" name="ipaddress">
  <br>
  Enter command:<br>
  <input type="text" name="cmd">
  <br><br>
  <input type="submit" value="Submit">
</form>
</html>    

在此代码中,唯一的区别是我们现在正在调用 getweboutput.py 脚本,我们将设备 IP 地址(我们想要输出信息的设备)和实际命令作为参数发送到该脚本。以下是 Python 代码:

import cgi
from netmiko import ConnectHandler
import time

form = cgi.FieldStorage()
ipvalue=form.getvalue('ipaddress')
cmd=form.getvalue('cmd')

def getoutput(cmd):
    global ipvalue
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=ipvalue, username=uname, password=passwd)
    output=device.send_command(cmd)
    return (output)

print('Content-Type: text/plain')
print('')
print ("Device queried for ",ipvalue)
print ("\nCommand:",cmd)
print ("\nOutput:")
print (getoutput(cmd))

现在的 Python 代码正在获取设备的 IP 地址的输入参数 ipaddress,以及需要发送到路由器的实际命令 cmd。它再次使用 Netmiko,正如在第二章中提到的,《网络工程师的 Python》,来获取信息并使用 getoutput() 函数返回信息:

示例 1我们提供 IP 地址和 show clock 命令

图片

登录页面

点击提交按钮:

图片

示例 2对于提供不同参数的相同脚本

图片

登录页面

这是点击提交按钮时的输出:

图片

正如我们所见,我们可以创建一个基于 Web 的查询工具,用于从具有指定命令的设备中获取信息,这可以作为设备验证的快速参考。此外,使用这种方法,我们还隐藏了用户名和密码(在这个例子中是 cisco:cisco),而没有将设备凭据暴露给最终用户。最终用户只需在网页上提供输入,而不会意识到后端正在执行的代码,该代码包含设备凭据。

我们甚至可以进行额外的检查,以确保用户只能运行show命令,并根据用户尝试在网页上调用的各种命令显示适当的消息。

从动态 HTML 访问脚本

有时我们运行 Python 脚本来创建动态 HTML 页面(这些页面基于我们在脚本中设置的某些触发器)。这些页面可以增强,以添加额外的 URL,当我们点击这些页面时,可以调用其他脚本。

例如,假设我们需要找出我们网络中有多少种设备型号。为此,我们创建一个脚本,使用任务计划程序将其安排为每小时运行一次,并在每次运行后创建一个动态 HTML 页面来显示更新的统计数据或库存。在 BYOD 场景中,这也扮演着重要的角色,因为每小时我们都可以监控我们的网络上有哪些设备,如果我们点击任何发现的设备,我们可以获取额外的信息,例如详细的 show 版本。

这是创建动态 HTML 的 Python 代码:

from pysnmp.hlapi import *

print('Content-Type: text/html')
print('')

def finddevices(ip):
    errorIndication, errorStatus, errorIndex, varBinds = next(
        getCmd(SnmpEngine(),
               CommunityData('public', mpModel=0),
               UdpTransportTarget((ip, 161)),
               ContextData(),
               ObjectType(ObjectIdentity('SNMPv2-MIB', 'sysDescr', 0)))
    )

    if errorIndication:
        print(errorIndication)
    elif errorStatus:
        print('%s at %s' % (errorStatus.prettyPrint(),
                            errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
    else:
        for varBind in varBinds:
            xval=(' = '.join([x.prettyPrint() for x in varBind]))
            xval=xval.replace("SNMPv2-MIB::sysDescr.0 = ","")
            xval=xval.split(",")
            return (xval[1])

ipaddress="192.168.255.248,192.168.255.249"
ipaddress=ipaddress.split(",")
tval="<table border='1'><tr><td>IP address</td><td>Model</td></tr>"
for ip in ipaddress:
    version=finddevices(ip)
    version=version.strip()
    ahref="http://localhost/test/showversion.py?ipaddress="+ip
    tval=tval+"<tr><td><a href='"+ahref+"' target='_blank'>"+ip+"</a></td>"

    tval=tval+"<td>"+version+"</td></tr>"

tval=tval+"</table>"
print (tval)   

上述代码的输出如下:

图片

上述代码创建了之前截图所示的动态 HTML。它从 SNMP 查询给定的 IP 地址的供应商,并根据这些值创建表格。IP 地址中的蓝色表示超链接,点击后会产生如下所示的输出:

图片

这是 show 版本输出的 Python 代码:

import cgi
from netmiko import ConnectHandler
import time

form = cgi.FieldStorage()
ipvalue=form.getvalue('ipaddress')

def getoutput(cmd):
    global ipvalue
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=ipvalue, username=uname, password=passwd)
    output=device.send_command(cmd)
    return (output)

print('Content-Type: text/plain')
print('')
print ("Device queried for ",ipvalue)
print ("\nOutput:")
print (getoutput("show version"))

正如我们在 URL 中看到的,当用户点击主动态 HTML 页面时,它调用了另一个脚本(之前列出的那个),该脚本从 URL 中获取 IP 地址的输入参数,并使用 Netmiko 获取设备的版本。

同样,我们可以使用 Web 框架方法快速收集其他统计数据,例如 CPU、内存、路由摘要以及其他每个设备的任务。

在 C#中创建后端 API

随着我们继续前进,有时作为开发者,我们不仅需要消费 API,还需要为他人创建自己的 API。即使我们发现某些函数有重复的使用,考虑到 Web 框架策略,我们仍需要确保,而不是简单地创建用于该任务的函数,我们需要将它们转换为 API。这样做的重大优势是,我们的函数或任务的使用将不再仅限于 Python,它可以在任何脚本语言或 Web 语言中使用。

在这里,我们将看到创建一个功能性的 C# Hello World API 的非常基本的方法。作为先决条件,我们需要 IIS 来运行 Web 服务,以及 Visual Studio(社区版免费使用)来创建我们自己的 API。稍后,我们将看到如何使用 Python 来消费该 API。

此外,我们还将确保返回值是 JSON 格式,这是 API 通信的行业标准,取代了 XML。

  1. 在 Visual Studio 中调用 C# Web 项目:

图片

  1. 在下一个屏幕上选择 Web API 复选框,如下所示:

图片

  1. 添加控制器(这是确保 API 框架激活的主要组件):

图片

  1. 给控制器一个有意义的名称。注意,名称后面必须跟有单词Controller(例如testController),否则控制器将无法工作,API 框架将损坏:

图片

  1. 控制器添加后,在WebApiConfig.cs文件中添加新的JsonMediaTypeFormatter()配置,如下所示。这确保了 API 返回的每个输出都将以 JSON 格式呈现:

图片

  1. 在主apitestController.cs程序中,一旦调用Get方法,返回值Hello World

图片

  1. 完成后,点击 Visual Studio 应用程序中可用的运行按钮。将打开一个类似于以下屏幕截图的窗口,这确保了本地 IIS 服务器正在被调用,并且应用程序已初始化以进行测试:

图片

  1. 一旦应用程序加载,将出现类似于以下 URL 的链接,以确认我们的 API 运行正常。请注意,此时正在使用本地的 IIS Express,API 尚未对外发布:

图片

  1. 验证完成后,现在我们需要将此发布到我们的 IIS 中。类似于我们之前所做的那样,我们在 IIS 中创建一个新的应用程序(在我们的例子中命名为apitest):

图片

  1. 一旦完成 IIS 映射,我们使用 Visual Studio 将我们的 API 项目发布到这个 Web 文件夹:

图片

  1. 我们创建一个 Web 发布配置文件,并将其发布到我们映射到 IIS 的本地文件夹:

图片

  1. 我们的 API 已准备好使用。我们可以通过在浏览器中访问http://localhost来验证它:

图片

在 Python 中消费 API

现在,既然我们已经创建了 API,让我们看看如何在 Python 中消费 API。

代码如下:

import requests
r = requests.get('http://localhost/apitest/api/apitest/5')
print (r.json())

上述代码的输出如下:

图片

对于 API 交互,我们在 Python 中使用requests库。当我们对 API 进行调用时,API 以 JSON 格式返回字符串。r.json()方法将返回的 JSON 转换为提取文本值,并将输出显示为Hello World

以类似的方式,我们可以使用 requests 库从各种基于 Web 的 API 调用中获取 API 结果。结果通常是 XML 或 JSON 格式,其中 JSON 是 API 调用的首选返回方法。

让我们再举一个例子,从 GitHub 获取更多 JSON 数据:

import requests
r = requests.get('https://github.com/timeline.json')
jsonvalue=r.json()
print (jsonvalue)
print ("\nNow printing value of message"+"\n")
print (jsonvalue['message']+"\n")

上述代码的输出如下:

图片

现在我们调用 GitHub API,我们得到了之前显示的 JSON 值。正如我们所见,返回值类似于 API 调用 JSON 返回数据中的字典,因此我们可以明确地获取message字典键内的文本。在前面的输出中,第一个输出是原始的 JSON 返回值,下一个输出是从message键中提取的文本。

除了调用标准 API 之外,有时我们需要将凭据传递给 API 以获取信息。换句话说,API 需要有一个认证机制来响应请求的数据。在我们的 API 中,现在让我们从 IIS 中启用基本认证:

  1. 在 IIS 中,选择你的网站(在我们的例子中是apitest),然后在认证部分选择基本认证:

图片

这确保了任何调用此 API 的请求都需要基本认证(用户名和密码)来访问内容。此外,我们在 Windows 机器上的用户应用程序中创建了一个简单的用户testuser,密码为testpassword

  1. 由于现在已启用认证,让我们看看如果没有传递凭据我们会得到什么:
import requests

r = requests.get('http://localhost/apitest/api/apitest/5')
print (r)

上述代码的输出如下:

图片

我们得到了一个响应[401],这意味着 HTTP 调用中的未授权访问。换句话说,API 调用未授权,因此不会返回任何输出。

  1. 接下来,我们看到相同的调用,但这次带有认证:
import requests
from requests.auth import HTTPBasicAuth
r = requests.get('http://localhost/apitest/api/apitest/5', auth=('testuser', 'testpassword'))
print (r)
print (r.json())

上述代码的输出如下:

图片

在这种情况下,我们调用认证方法HTTPBasicAuth,并在requests.get调用中传递用户名和密码。因为我们提供了正确的凭据,所以我们得到了一个响应[200],这在 HTTP 中是 OK 的,在最后一行我们打印了返回值的输出,在我们的例子中是Hello World

样本总结任务

既然我们已经熟悉了 Web 框架,让我们执行一个涵盖我们之前看到的所有方面的任务:

  1. 我们编写一个 HTML 页面,要求用户输入用户名和密码。这些值将被传递到一个 Python 脚本中,该脚本将调用我们之前创建的 API 进行认证。如果返回值是授权的,那么我们将显示我们想要在另一个网页上查看额外信息的设备的 IP 地址。

  2. 接下来,用户可以点击任何 IP 地址来查看show ip int brief的输出。如果授权失败,脚本将返回“未授权”的消息,并且不会显示 IP 地址。以下为参考(有效的用户名和密码):

    • 用户名Abhishek

    • 密码password

HTML 代码如下:

<html>
<form action="validatecreds.py" method="post">
 Enter your name: <br>
  <input type="text" name="name">
  <br>
  Enter your password:<br>
  <input type="password" name="password">
  <br><br>
  <input type="submit" value="Submit">
</form>
</html>    

在这种情况下,我们使用了POST方法,因为如果我们使用默认的GET方法,密码将在浏览器 URL 中以明文形式显示。在POST方法中,后端会建立单独的连接,并且 URL 不会显示传递给脚本的值。

Python 代码如下:

import cgi, cgitb
import requests
from requests.auth import HTTPBasicAuth

form = cgi.FieldStorage()
uname=form.getvalue('name')
password=form.getvalue('password')

r = requests.get('http://localhost/apitest/api/apitest/5', auth=(uname, password))

print('Content-Type: text/HTML')
print('')
print ("<h2>Hello "+uname+"</h2>")

htmlform="<form action='showoutput.py' method='post'>"
htmlform=htmlform+"<br><input type='radio' name='ipaddress' value='192.168.255.248' /> 192.168.255.248"
htmlform=htmlform+"<br><input type='radio' name='ipaddress' value='192.168.255.249' /> 192.168.255.249"
htmlform=htmlform+"<br><input type='submit' value='Select IPaddress' /></form>"

if (r.status_code != 200):
    print ("<h3><font color='red'>Not Authorized.</font> Try again!!!!</h3>")
else:
    print ("<h3><font color='lime'>Authorized.</font> Please select from list below:</h3>")
    print (htmlform)
  1. 如果提供的凭证不正确(无效的凭证,如示例凭证):

图片

当我们点击“提交”按钮时,它将显示如下截图所示的消息:

图片

  1. 如果凭证正确(在 Web 服务器上正确且成功认证):

图片

当我们点击“提交”按钮:

图片

  1. 接下来,我们看到 IP 地址,我们现在可以使用这些地址来获取输出。我们选择我们想要使用的 IP 地址,然后点击“选择 IP 地址”:

图片

一旦我们点击“提交”(或“选择 IP 地址”按钮),我们将得到以下输出:

图片

再次,如果我们查看前面的输出 URL,由于我们为这个选择使用了POST方法,所以我们只看到脚本,而没有看到在 URL 中传递的参数。这确保了人们需要从着陆页(在我们的案例中是main.html)开始,不能直接调用任何可能使用GET方法给出的带有参数的 URL。

通过这样做,我们还确保了执行此操作的最终用户遵循逐步选择,而不是在不执行顺序步骤的情况下从一个 URL 跳转到另一个 URL。

以类似的方式,最终用户可以创建他们自己的 API 来获取诸如大量设备名称和 IP 地址等信息,并可以在他们的脚本中使用这些信息来快速创建前端、后端或 Web 启用场景,无需任何最终用户安装。

摘要

我们现在已经了解了 Web 框架,以及相关的示例,API 的使用。这包括如何创建 API、访问 API,甚至在与 API 进行认证。使用这些知识,我们现在将能够为最终用户开发基于 Web 的工具。IIS 功能也已介绍,这有助于开发者自定义各种基于 Web 的设置,如认证、授权和创建网站。

此外,通过一个给定场景的完整功能示例,读者可以快速构建基于 Web 的 Python 脚本,这消除了对 Python 和定制库的任何最终用户安装的需求。由于只需在一台机器上进行修复,这使得支持和错误修复变得更加容易。在服务器上进行的修复将确保所有最终用户现在都将使用修复或增强的脚本功能,而不是在自己的机器上下载本地副本以获取修复或增强的脚本。

在下一章中,我们将介绍 Ansible 的使用,它是一个流行的开源自动化平台。

第五章:Ansible 用于网络自动化

在本章中,我们将看到使用一个流行的网络自动化工具Ansible。本章将指导您了解 Ansible 的基础知识,包括安装和基本配置,并给出如何从 Ansible 执行与网络自动化相关的任务的示例。

这将涵盖 Ansible 中使用的各种术语和概念,例如示例、使用 Ansible 的执行,以及如何使用 Ansible 模板为各种设备创建配置,以及如何从 Ansible 获取有关受管理节点的某些信息。

本章将向读者介绍:

  • Ansible 的概述和安装

  • 理解 Ansible 的编程概念

  • Playbooks

  • Ansible 的使用场景

Ansible 概述和术语

Ansible 是一个自动化工具或平台,它作为开源软件提供,用于配置诸如路由器、交换机和各种类型的服务器等设备。Ansible 的主要目的是配置三种主要类型的任务:

  • 配置管理:这用于从我们称为 Ansible 中的清单的各种设备中获取和推送配置。根据清单的类型,Ansible 能够批量推送特定的或完整的配置。

  • 应用部署:在服务器场景中,很多时候我们需要批量部署一些特定的应用程序或补丁。Ansible 也负责在服务器上批量上传补丁或应用程序,安装它们,甚至配置特定任务的程序。Ansible 还可以根据清单中的设备自定义设置。

  • 任务自动化:这是 Ansible 的一个功能,它在单个设备或一组设备上执行某些书写的任务。这些任务可以编写,并且 Ansible 可以配置为一次性或定期运行这些任务。

Ansible 的另一个强大功能是 IT 或基础设施编排。为了详细解释这一点,让我们假设我们需要升级某些路由器或网络设备。Ansible 可以执行一系列步骤来隔离特定的路由器,推送代码,更新代码,然后根据前一个结果或任务的返回值移动到下一个路由器。

Ansible 的基本要求

Ansible 非常易于安装和设置。它基于控制器和受管理节点模型工作。在这个模型中,Ansible 安装在控制器上,控制器是一个 Linux 服务器,可以访问我们想要管理的所有清单或节点。正如我们所看到的,Ansible 支持 Linux(目前有一个 Windows 控制器的 beta 版本,但尚未完全支持),它依赖于 SSH 协议与节点通信。因此,除了控制器的配置之外,我们还需要确保将要管理的节点具有 SSH 能力。

在托管节点上安装 Python 是一个额外的要求,因为多个 Ansible 模块是用 Python 编写的,并且 Ansible 会将模块本地复制到客户端并在节点本身上执行它。在运行 Linux 的服务器上,这已经满足,然而,在网络设备如 Cisco IOS 上,这可能不是一个可能性,因为 Cisco 节点上没有 Python。

为了克服这种限制,有一种称为原始模块的东西可以执行原始命令,例如 show version 从 Cisco 设备获取输出。这可能帮助不大,但还有一种方法可以使 Ansible 在服务器本身上运行其模块,而不是在客户端(或托管节点)上执行这些模块。这确保了模块使用 Ansible 服务器的资源(包括 Python),并且它们可以调用 Cisco 供应商的 SSH 或 HTTP API 来执行在服务器上本地配置的任务。甚至对于没有良好 API 集的设备(如 Cisco IOS),也可以使用 SNMP 来执行我们的任务。

如我们之前所见,SNMP 可以在读取和写入模式下使用,因此使用 Ansible 并在本地运行模块,我们甚至可以使用 SNMP 协议的帮助来配置旧的 IOS 设备。

安装 Ansible

Ansible 控制器(管理节点的主要组件)支持多种 Linux 版本,但不能安装在 Windows 上。

对于托管节点,核心要求是任何具有 Python 2.6 及以上版本的东西。此外,由于 Ansible 使用 SSH 与托管节点通信,节点必须能够通过 SSH 访问。对于任何文件传输,默认是SSH 文件传输协议SFTP),但始终有使用 scp 作为默认文件传输协议的选项。话虽如此,如前所述,如果无法安装 Python,那么我们将使用从服务器本身运行的 Ansible 的原始模块。

回到控制器机器的安装,需要安装 Python 2(2.6 或更高版本)。在我们的案例中,我们使用 Ubuntu 作为我们的操作系统,因此我们的重点将放在使用 Ubuntu 作为底层操作系统的情况下与 Ansible 一起工作。安装 Ansible 的方法之一是使用 Ubuntu 中的高级打包工具APT)。以下命令将配置个人软件包存档PPA)并安装 Ansible。

这里是基本命令,按照它们在安装 Ansible 时所需的顺序:

$ sudo apt-get update $ sudo apt-get install software-properties-common $ sudo apt-add-repository ppa:ansible/ansible $ sudo apt-get update $ sudo apt-get install ansible

在我们的案例中,Ansible 已经安装。以下是我们再次运行命令 sudo apt-get install ansible 时得到的示例输出。在这种情况下,如果有一个新的更新可用,Ansible 将升级到最新版本,否则它将退出命令,表示我们已经有最新版本,如下面的截图所示):

图片

安装 Ansible 的另一种方式是使用我们熟知的 Python 库安装命令 pip。这个命令将是:

pip install --user ansible

安装完成后,以下是文件夹的信息:

hosts 文件是我们添加要由 Ansible 控制的受管理节点的清单文件。ansible.cfg 是实际用于调整 Ansible 参数的配置文件。一旦安装完成,我们需要在 hosts 文件中添加一些节点。在我们的例子中,作为一个全新的安装,我们需要添加我们的本地主机(127.0.0.1)。此节点可以通过 SSH 使用用户名 abhishek 和密码 abhishek 访问。

下面是我们 /etc/hosts 文件的示例输出:

这行 127.0.0.1 ansible_connection=ssh ansible_user=abhishek ansible_ssh_pass=abhishek 是我们指定访问此系统所需参数的地方。

我们可以使用任何文本编辑器(在我们的例子中我们使用 nano 或 vi 编辑器)来添加或修改这些文件的更改。要修改 hosts 文件,我们使用以下命令:

$ sudo nano /etc/ansible/hosts

下一步是验证我们添加到 hosts 文件中的节点的可访问性/可达性,可以使用 ansible all -m ping 命令来完成,如下截图所示:

如前一个截图所示,命令 ansible all -m pinghosts 文件中配置的所有节点进行 ping 操作,并响应 ping。此外,在相同的输出中,如果我们使用命令 ansible all -m ping --ask-pass,这将要求输入密码以访问该特定节点。在我们的例子中,我们输入密码,然后得到响应。现在,你可能会问:我正在执行一个简单的 ping,那么在这种情况下为什么还需要 SSH 呢?

让我们在 hosts 文件中添加全局 DNS 服务器(4.2.2.2),然后按照以下截图所示进行测试。如前所述,我们使用 sudo nano /etc/ansible/hosts 来调用 nano 编辑器:

完成后,我们再次尝试执行相同的 ping 测试:

我们现在看到了什么?尽管我可以轻松地从我的机器上 ping 4.2.2.2,但 Ansible 返回 false 的值,因为 Ansible 首先尝试使用 SSH 登录到设备,然后尝试 ping IP。在这种情况下,4.2.2.2 的 SSH 没有打开,我们从 Ansible 那里得到一个针对该特定 IP 地址的失败消息。此外,我们可以在 hosts 文件中按特定名称分组受管理的对象,例如 routersswitchesservers 或我们喜欢的任何名称。

考虑以下示例:

我们将当前的 IP(本地主机和 4.2.2.2)分组到一个新的组中,myrouters。我们返回并修改 /etc/ansible/hosts 文件:

注意文件中添加了 myrouters 组。一旦我们保存它,现在让我们使用这个组来执行 ping 任务:

图片

如我们所见,我们不再 ping 所有设备,而是 ping myrouters 组,在我们的例子中是环回 IP 和 4.2.2.2

当然,结果将与之前相同,但现在我们增加了灵活性,确保我们根据单个节点或特定名称下的节点组执行任务。

临时命令简介

临时命令在 Ansible 中用于执行基于临时需求或仅一次需求的任务。换句话说,这些是用户希望即时执行但不想保存以供以后使用的任务。一个 Ansible 临时命令的快速用例可能是快速获取一组受管理节点的版本信息,作为一次性任务用于其他用途。由于这是一个快速的信息需求且不需要重复,我们会使用临时任务来执行此请求。

随着本章的进行,将会有一些额外的开关(我们传递给 Ansible 命令的额外选项),这些开关将根据需求引入。仅调用 ansible 命令将产生所有可以作为选项或参数传递的值:

图片

一些临时命令的示例如下:

  1. 假设我们需要 ping 同一组设备,但现在是在并行方式下(默认是顺序执行,但为了使任务更快,我们会使用并行方法):
ansible myrouters -m ping -f 5
  1. 如果我们想要使用一个单独的 username 而不是默认配置的用户:
ansible myrouters -m ping -f 5 -u <username>
  1. 如果我们想要增强会话(或使用 sudoroot):
ansible myrouters -m ping -f 5 -u username --become -k (-k will ask for password)

对于单独的用户名,我们使用 --become-user 开关。

  1. 对于执行特定的命令,我们使用 -a 选项(假设我们想要以并行方式获取 myrouters 列表中路由器的 show version 信息):
ansible myrouters -a "show version" -f 5 

5 是并行线程数的默认值,但为了再次更改此值,我们可以在 Ansible 配置文件中修改它。

  1. 另一个例子是将文件从源复制到目标。假设我们需要将文件从当前源复制到多个服务器,这些服务器属于,比如说,servers 组:
ansible servers -m copy -a "src=/home/user1/myfile.txt dest=/tmp/myfile.txt"
  1. 我们想要在 Web 服务器上启动 httpd
ansible mywebservers -m service -a "name=httpd state=started"

反过来,如果我们想要停止 httpd

ansible mywebservers -m service -a "name=httpd state=stopped"
  1. 作为另一个重要的例子,让我们假设我们想要运行一个长时间运行的命令,例如 show tech-support,但我们不想在前台等待。我们可以为这个命令指定一个超时时间(在我们的例子中是 600 秒):
ansible servers -B 600 -m -a "show tech-support"

这将返回一个 jobid,稍后可以用来更新。一旦我们有了 jobid,我们可以使用以下命令检查该特定 jobid 的状态:

ansible servers -m async_status -a "jobid"
  1. 有一个额外的命令可以提供 Ansible 可以获取并工作的特定节点的所有信息:
ansible localhost -m setup |more 

查看本地机器(localhost)上事实的输出如下:

图片

  1. 另一个常用的 ad hoc 命令是shell命令。它用于控制整体操作系统、shell 或 root 场景。让我们看一个示例来重启servers组中的受管理节点:
ansible servers -m shell -a "reboot"

如果我们想要关闭同一组服务器而不是重启:

ansible servers -m shell -a "shutdown"

这样,我们可以确保使用 ad hoc 任务,我们可以快速对单个或多个受管理节点执行基本任务,以快速获得结果。

Ansible playbooks

Playbooks 是我们为 Ansible 创建的一组指令,用于配置、部署和管理节点。这些作为指南,使用 Ansible 在个人或组上执行一组特定任务。将 Ansible 视为你的画册,playbooks 视为你的颜色,受管理节点视为图片。以这个例子为例,playbooks 确保需要将哪种颜色添加到图片的哪个部分,而 Ansible 框架执行为受管理节点执行 playbook 的任务。

Playbooks 是用一种称为YAML Ain't Markup LanguageYAML)的基本文本语言编写的。Playbooks 包含执行在受管理节点上特定任务的配置。此外,playbooks 还用于定义一个工作流程,其中根据条件(如不同类型的设备或不同类型的操作系统),可以执行特定任务,并根据从任务执行中检索到的结果进行验证。它还结合了多个任务(以及每个任务中的配置步骤),可以按顺序执行这些任务,或者针对选定的或所有受管理节点并行执行。

关于 YAML 的良好信息可以在此处参考:

learn.getgrav.org/advanced/yaml

在基本层面上,playbook 由列表中的多个plays组成。每个 play 都是为了在特定的受管理节点组上执行某些 Ansible 任务(或要执行的命令集合)而编写的(例如myroutersservers)。

从 Ansible 网站,以下是一个示例 playbook:

- hosts: webservers
  vars:
    http_port: 80
    max_clients: 200
  remote_user: root
  tasks:
  - name: test connection
    ping:

在这个例子中,有一些部分我们需要理解:

  1. hosts:此列表包含组或受管理节点(在本例中为webservers),或由空格分隔的单独节点。

  2. vars:这是声明部分,我们可以在此处定义变量,类似于我们在任何其他编程语言中定义它们的方式。在这种情况下http_port: 80表示将80的值分配给http_port变量。

  3. tasks:这是实际声明部分,用于在- hosts部分定义的组(或受管理节点)上执行的任务。

  4. name:这表示用于识别特定任务的注释行。

使用此示例,让我们创建我们的 playbook 来 ping 早期示例中的受管理节点:

- hosts: myrouters
  vars:
    http_port: 80
    max_clients: 200
  remote_user: root
  tasks:
  - name: test connection
    ping:

要创建配置,我们输入:

nano checkme.yml

在编辑器中,我们复制并粘贴之前的代码,并保存它。对于执行,我们可以使用 --check 参数。这确保在远程系统上,如果要在 playbook 中执行更改,它将在本地模拟而不是在远程系统上实际执行:

ansible-playbook checkme.yml --check 

执行前述给定命令的输出如下:

图片

正如前一个输出所示,我们的 playbook checkme.yml 的模拟执行完成,并在 PLAY RECAP 部分显示了结果。

另一个例子是如果我们想根据我们的初始结果调用特定的任务。在 Ansible 中,我们使用 handlers 来执行该任务。在一个 play 中,我们创建一个可以基于任何从该任务中产生的变化执行 notify 动作的任务。这些动作在所有任务通过处理程序完成后被触发。换句话说,只有在 play 中的所有任务都完成后,handlers 条件才会被触发(例如,如果所有配置任务都已完成,则重新启动服务器)。

处理程序只是另一种类型的任务,但通过全局唯一名称引用,并且仅在由 notify 调用时执行:

- hosts: myrouters
tasks:
 - name: show uptime
   command: echo "this task will show uptime of all hosts"
   notify: "show device uptime"
handlers:
 - name: show variables
   shell: uptime
   listen: "show device uptime"

正如前一个示例中所示,任务是在 myrouters 上执行的,它调用 notify 来执行处理程序任务。handlers 中的 -name 描述了任务将要调用的处理程序名称。

Ansible 是区分大小写的(例如:两个名为 xX 的变量将是不同的)。

一旦调用通知处理程序,shell: uptime 命令将在远程 shell 上运行 uptime 命令并获取要显示的输出。在 handlers 部分下的 listen 命令也是一个调用特定处理程序的替代或更通用的方式。在这种情况下,notify 部分可以调用 handlers 下的特定处理程序,这与 listen 声明与 notify 声明相匹配(例如,在我们的案例中 notify : "show device uptime" 将调用监听 show device uptime 命令的特定处理程序,在这种情况下是由 -name 作为显示变量定义的),而不是调用我们在配置中目前看到的 handlers 下的 -name 声明。

Ansible playbook(在本例中为 showenv.yml)需要使用 -v 开关来调用以查看 verbose 输出,其中 verbose 输出是一种输出,我们可以通过执行看到所有活动,而不仅仅是显示最终结果)。

  • 没有使用 -v 命令(verbose 输出未启用)的输出如下:

图片

  • 使用 -v 命令(verbose 输出已启用)的输出如下:

图片

在前面的屏幕截图中,注意 playbook 执行中 uptime 命令的输出(从前面的屏幕截图中,changed: [127.0.0.1] => {"changed": true, "cmd": "uptime", "delta":")。详细的输出显示了执行命令(在这种情况下为 uptime)以及从管理节点获取的值。

在许多情况下,如果我们创建了多个 playbook 并想在主 playbook 中运行它们,我们可以导入创建的 playbook:

#example
- import_playbook: myroutercheck.yml
- import_playbook: myserver.yml

如果我们明确从 .yml 文件中导入某些任务:

# mytask.yml
---
- name: uptime
  shell: uptime

查看内部 main.yml 的配置:

tasks:
- import_tasks: mytask.yml
# or
- include_tasks: mytask.yml

同样,我们可以从另一个 .yml 文件中调用处理器:

# extrahandler.yml
---
- name: show uptime
  shell: uptime    

main.yml 的配置中:

handlers:
- include_tasks: extrahandler.yml
# or 
- import_tasks: extrahandler.yml

此外,当我们定义 Ansible 变量时,有一些考虑因素我们需要记住。变量不能包含特殊字符(除了下划线)或两个字符或单词之间的空格,并且不应以数字或特殊字符开头。

例如:

  • check_mecheck123 是有效的变量

  • check-mecheck mecheck.me 是无效变量

在 YAML 中,我们可以使用冒号方法创建字典:

myname:
  name: checkme
  age: 30

要引用或检索由名称 myname 创建的字典中的任何值,我们可以指定如下命令:myname['name']myname.name

使用 Ansible 事实

如我们之前所见,我们可以使用以下命令收集关于管理节点的事实:

ansible <hostname> -m setup

例如:

ansible localhost -m setup

现在,当我们获取回值(称为 事实)时,我们可以引用这些系统变量。每个系统变量将根据在 hosts 部分下调用的每个管理节点持有唯一值:

- hosts: myrouters
  vars:
      mypath: "{{ base_path }}/etc"

可以使用 {{variable name}} 调用系统变量,但在 playbook 中需要用双引号引用。

让我们看看一个示例,其中我们在 playbook 中获取 hostname 的值:

- hosts: myrouters
  tasks: 
   - debug:
       msg: "System {{ inventory_hostname }} has hostname as {{ ansible_nodename }}"

获取主机名的 playbook 输出:

如我们所见,在 playbook(在前面代码中),我们引用了 {{inventory_hostname}}{{ansible_nodename}} 变量,这导致在 msg 部分的输出:System 127.0.0.1 has host hostname as ubuntutest。使用相同的 playbook 配置,我们可以使用所有或任何其他通过替换配置中的系统变量检索到的事实。

如果我们想从事实中获取更多信息,我们可以引用方括号内的特定值:

{{ ansible_eth0.ipv4.address }}{{ ansible_eth0["ipv4"]["address"] }}

我们还可以通过命令行将变量传递给 playbook,如下例所示:

- hosts: "{{hosts}}"
  tasks: 
   - debug:
       msg: "Hello {{user}}, System {{ inventory_hostname }} has hostname as {{ ansible_nodename }}"

执行包含变量的 playbook 的命令:

ansible-playbook gethosts.yml --extra-vars "hosts=myrouters user=Abhishek"

在命令行执行期间提供变量时命令的输出:

正如我们所见,myrouters的值被传递给hostsAbhishek的值被传递给user变量。正如我们在 playbook 执行的输出中看到的,msg变量输出包含user变量的值和配置在myrouters组中的主机(在本例中,单个主机是这个组的一部分,IP 地址为127.0.0.1)。

Ansible 条件

有时候,我们想要根据条件执行某些任务。when语句用于确定这些条件,如果条件评估为真,则执行指定的任务。让我们用一个例子来执行uptime命令,如果我们传递参数clock到变量clock

- hosts: myrouters
 tasks: 
 - shell: uptime
 - debug:
 msg: "This is clock condition" 
 when: clock == "clock"

 - debug: 
 msg: "This is NOT a clock condition"
 when: clock != "clock"

从命令行执行:将错误的值clock123传递给clock变量:

ansible-playbook checkif.yml --extra-vars "clock=clock123"

提供错误值后的执行输出:

正如我们在前面的输出中看到的,消息This is NOT a clock condition是根据我们传递的值执行的。同样,如果我们传递如下所示的时钟变量:

ansible-playbook checkif.yml --extra-vars "clock=clock"

从命令行传递正确变量值后的输出:

消息This is clock condition现在是根据传递的参数执行的。看另一个例子,以类似的方式,我们可以确保根据某个事实采取某些行动:

- hosts: myrouters
  tasks: 
   - shell: uptime
   - debug:
       msg: "This is clock condition on Ubuntu" 
     when: 
      - clock == "clock"
      - ansible_distribution == "Ubuntu"

   - debug: 
       msg: "This is clock condition on Red HAT"
     when: 
      - clock = "clock" 
      - ansible_distribution == "Red Hat"

正如我们所见,条件是在ansible_distribution事实上触发的。如果响应是 Ubuntu,则执行第一个条件,否则,根据Red Hat执行其他条件。此外,我们还在从命令行使用clock作为传递给 playbook 的变量调用 playbook 时验证时钟值是否为clock。在前面的代码中,如果我们要得到那个特定的结果,两个条件都需要评估为真。

Ansible 循环

我们可以使用with_items循环进行重复操作。让我们看看一个解析列表并打印值的例子:

---
- hosts : all
 vars:
 - test: Server
tasks:
 - debug: 
 msg: "{{ test }} {{ item }}" 
 with_items: [ 0, 2, 4, 6, 8, 10 ]

使用前面代码执行 playbook 的输出:

正如我们在前面的屏幕截图中所见,迭代打印了Server的值加上列表中每个项目的值。同样,对于整数迭代,我们可以使用with_sequence命令执行循环:

---
- hosts : all
 vars:
 - test: Server
tasks:
 - debug: 
 msg: "{{ test }} {{ item }}" 
 with_sequence: count=10

此外,假设我们想要打印跳过 2(从 0 到 10 的偶数)的值,相同的with_sequence命令将写成:

with_sequence: start=0 end=10 stride=2

有时候,我们也需要为执行特定任务随机选择任何值。以下示例代码从 4 个可用的选项中(在我们的案例中,从Choice Random 1Choice Random 4)随机选择一个值,并使用msg变量显示它:

---
- hosts : all
 vars:
 - test: Server
tasks:
 - debug: 
 msg: "{{ test }} {{ item }}" 
 with_random_choice:
    - "Choice Random 1"
    - "Choice Random 2"
    - "Choice Random 3"
    - "Choice Random 4"

这将从with_random_choice声明下的给定选项列表中随机选择任何值。

Python API 与 Ansible

可以使用 Python 调用 Ansible 代码,使用 Ansible API。Ansible 已发布其 API 的 2.0 版本,以更好地与编程语言集成。需要注意的是,Ansible 已扩展其功能以支持使用 Python 进行开发,但它在网站上也建议,根据其自己的判断,它也可以停止支持 API(创建甚至修复其当前 API 版本的)框架。

让我们看看一个创建 play 的例子,该 play 的任务是从我们之前myrouters的清单中查看用户名:

#call libraries
import json
from collections import namedtuple
from ansible.parsing.dataloader import DataLoader
from ansible.vars.manager import VariableManager
from ansible.inventory.manager import InventoryManager
from ansible.playbook.play import Play
from ansible.executor.task_queue_manager import TaskQueueManager
from ansible.plugins.callback import CallbackBase

Options = namedtuple('Options', ['connection', 'module_path', 'forks', 'become', 'become_method', 'become_user', 'check', 'diff'])

# initialize objects
loader = DataLoader()
options = Options(connection='local', module_path='', forks=100, become=None, become_method=None, become_user=None, check=False,
                  diff=False)
passwords = dict(vault_pass='secret')

# create inventory
inventory = InventoryManager(loader=loader, sources=['/etc/ansible/hosts'])
variable_manager = VariableManager(loader=loader, inventory=inventory)

# create play with task
play_source = dict(
        name = "mypythoncheck",
        hosts = 'myrouters',
        gather_facts = 'no',
        tasks = [
            dict(action=dict(module='shell', args='hostname'), register='shell_out'),
            dict(action=dict(module='debug', args=dict(msg='{{shell_out.stdout}}')))
         ]
    )
play = Play().load(play_source, variable_manager=variable_manager, loader=loader)

# execution
task = None
try:
    task = TaskQueueManager(
              inventory=inventory,
              variable_manager=variable_manager,
              loader=loader,
              options=options,
              passwords=passwords,
              stdout_callback='default'
          )
    result = task.run(play)
finally:
    if task is not None:
        task.cleanup()

在显示从受管理节点用户名的先前代码中:

  1. '#call libraries':这些用于初始化可用的 Ansible API 库。其中一些重要的库有:

    • from ansible.parsing.dataloader import DataLoader:此用于加载或解析 YAML 或 JSON 格式的文件或值,如果被调用

    • from ansible.vars import VariableManager:此用于清单文件位置

    • from ansible.inventory.manager import InventoryManager:此用于清单初始化

    • from ansible.playbook.play import Play:此用于配置 play

    • from ansible.executor.task_queue_manager import TaskQueueManager:此用于配置的 play 的实际执行

  2. # initialize objects:此部分初始化各种组件,如 root 用户、become_user(如果有)和其他运行 play 所需的参数。

  3. # create inventory:这是指定实际清单位置并初始化它的地方。

  4. # create play with task:这是我们在类似创建.yml文件的方式中创建任务的地方。在这种情况下,是为了显示清单中myrouters部分所有节点的主机名。

  5. # execution:这是使用任务的run()方法创建的 play 的执行。

上述代码的输出如下:

图片

如我们所见,在调用 Python 文件后,我们得到了在hosts文件(/etc/ansible/hosts)中定义的myrouters清单部分下 localhost 的主机名。

创建网络配置模板

由于我们现在已经熟悉了 Ansible 的基础知识,让我们看看一个例子,其中我们为一些路由器生成准备部署的配置。首先,我们需要了解 Ansible 中的角色。角色用于为 Ansible playbooks 创建文件结构。基于角色,我们可以分组相似的数据。与他人共享角色意味着我们共享整个定义的文件结构,以共享一组常见内容。一个典型的角色文件结构将包含主文件夹和内容文件夹,在内容文件夹下,我们将有templatesvarstasks文件夹。

在我们的情况下,层次结构如下:

  • 主目录

      • 角色
        • 路由器
          • 模板
          • 变量
          • 任务

在每个模板、变量或任务文件夹下,如果我们调用该特定角色,将自动搜索名为main.yml的文件,并考虑该特定角色的该文件中的任何配置。使用之前提到的层次结构细节,在我们的测试机器(运行 Ubuntu)上,以下是我们的文件结构示例:

图片

如我们所见,在rtrconfig文件夹下,我们根据 Ansible 标准定义了文件夹。一旦我们创建了文件夹层次结构,下一步就是在每个部分下根据我们的需求配置/创建文件。

首先,由于我们将使用路由器模板来生成配置,我们创建一个模板并将其放入roles/routers/templates文件夹中。

路由器配置模板如下(用作通用路由器模板以生成路由器配置):

no service pad
 service tcp-keepalives-in
 service tcp-keepalives-out
 service password-encryption
 username test password test
 !
 hostname {{item.hostname}}
 logging server {{logging_server}}
 !
 logging buffered 32000
 no logging console
 !
 ip domain-lookup enable
 !
 exit

如我们在模板中所见,{{item.hostname}}{{logging_server}}是我们创建实际配置时需要替换的两个值。由于这是一个 Jinja 模板,我们将把这个模板保存为somename.j2(在我们的例子中,routers.j2)。下一步是定义变量值。

如我们之前所见,我们需要确保logging_server变量已经定义了一个值。这将在roles/routers/vars文件夹中:

---
logging_server: 10.10.10.10

我们将此文件保存为vars文件夹中的main.yml,这将默认在执行 playbook 时用于变量值声明。一旦我们有了定义和模板,下一步就是定义实际需要执行的任务。

这将在roles/routers/tasks文件夹中完成,并再次保存为main.yml,以便在执行该特定角色时进行自动发现。

让我们看看这个配置:

---
- name: Generate configuration files
  template: src=routers.j2 dest=/home/abhishek/{{item.hostname}}.txt
  with_items: 
  - { hostname: myrouter1 }
  - { hostname: myrouter2 }

在任务的配置中,我们调用我们创建的模板(在这种情况下,routers.j2),并提供一个配置文件将保存到的目标文件夹(在这种情况下,/home/abhishek/{{item.hostname}}.txt)。

这里需要特别注意的一个点是{{item.hostname}}将解析为我们使用with_items循环提供的每个主机名。因此,将生成的文件名将是with_items循环中定义的每个项(在我们的例子中,myrouter1.txtmyrouter2.txt)。

如前所述,with_items将循环每个值,其中主机变量值在每个迭代中都会改变。一旦我们有了模板、变量和任务,我们将在主 playbook 中调用该角色并执行它。

主 playbook 配置如下:

---
- name: Generate router configuration files
  hosts: localhost

  roles:
    - routers

在这里,我们只是调用主机(在我们的例子中是 localhost,因为我们想在本地上执行),并调用在 playbook 中需要执行的角色(在我们的例子中,routers)。我们将其保存为任何带有.yml扩展名的名称(在我们的例子中,makeconfig.yml)。

确保所有.yml文件都创建在相应文件夹中的最终验证如下:

  1. 回顾一下,以下是详细的文件结构,正如我们现在在rtrconfig文件夹下看到的文件:

图片

  1. 要为路由器生成配置,我们执行makeconfig.yml剧本:

图片

  1. 一旦成功执行,我们应该在/home/abhishek文件夹中有两个文件(myrouter1.txtmyrouter2.txt),其中包含生成的配置:

图片

  1. 这里是从生成的文件中提取的内容:

图片

  1. 如我们所见,现在我们有了生成的配置,使用模板,并替换了主机名和logging_server部分的值。

配置现在已生成并准备好推送到那些特定的路由器(它们位于roles/routers/tasks下的main.yml中),以类似的方式,我们可以为具有各种角色和每个角色中多个设备的配置生成配置,例如交换机、路由器、负载均衡器等,每个角色都包含与该角色相关的特定信息,如变量、模板和任务。

摘要

在本章中,我们学习了 Ansible 是什么,它的安装以及 Ansible 的基本使用。本章还介绍了 Ansible 中使用的概念和术语,包括如何创建剧本、任务和其他基本功能。我们还熟悉了 ad-hoc 命令,并理解了事实的概念及其在 Ansible 中的使用。

最后,通过使用 Jinja 模板,我们了解了如何使用模板创建完整的配置,并参考设备/角色特定信息使用 Ansible 中的角色。

在下一章中,我们将看到如何调用自动化的一些其他方面,例如使用 Splunk 进行 syslog 收集和从 Python 获取信息,在 BGP 上进行基本自动化,UC 集成示例以及其他相关示例,这些示例在创建自动化脚本时可以作为参考资料。

第六章:网络工程师的持续集成

如前几章所述,现在我们已具备使用各种技术创建自动化、使用 Ansible 以及理解最佳实践的知识或相当的了解,我们继续我们的旅程,了解如何进行自动化项目的基本规划。

在本章中,我们将看到一些帮助我们规划自动化项目的工具,以及一些与各种设备或网络技术相关的日益复杂的场景的交互示例。

我们将工作的某些方面包括:

  • 与 Splunk 的交互

  • BGP 和路由表

  • 无线客户端到 AP 到交换端口

  • 电话到交换端口

  • WLAN 和 IPAM

  • 有用的最佳实践和用例

与 Splunk 的交互

Splunk 是最广泛使用的数据挖掘工具之一。凭借其数据挖掘和挖掘能力,工程师可以根据决策采取行动。虽然它在各个方面都很有用,但在这里我们将看到一个 Splunk 作为 Syslog 服务器使用的例子,我们的测试路由器向该服务器发送消息(作为 syslog),以及我们如何从自动化中查询 Splunk 的这些 syslogs 并采取行动。

这是自动化过程中的一个重要部分,因为基于某些事件(警报和 syslogs),工程师需要执行自动化任务,如自我修复,甚至触发电子邮件或使用第三方工具为各个团队创建工单。

在这里,我们将看到 Splunk 作为 Syslog 服务器的基本实现和配置:

  1. 下载并安装 Splunk 后,可以通过以下 URL 访问它:http://localhost:8000/en-US/account/login?return_to=%2Fen-US%2F,如下截图所示:

图片

  1. 登录后,我们创建一个监听 syslogs 的监听器(在我们的案例中我们使用TCP协议并保持默认端口514开放):

图片

在 Splunk 上完成 TCP 端口514的配置(监听 syslog 消息)后,确保服务器上的任何本地防火墙都允许 TCP 端口514的入站数据包,并且我们的机器已准备好从网络设备通过 TCP 端口514访问 syslogs。

  1. 配置路由器发送 syslogs。我们在路由器上应用以下命令以启用日志记录(在我们的案例中,Syslog 服务器的 IP 地址为192.168.255.250):
config t
logging host 192.168.255.250 transport tcp port 514
logging buffered informational
exit

这配置了路由器将 syslogs 发送到指定 IP 地址,通过 TCP 协议在端口514上。此外,我们还在路由器上声明仅记录信息 syslog 消息。

  1. 完成后,为了确认,我们可以尝试执行任何接口(在我们的案例中是Loopback0)的关闭和非关闭操作,并使用路由器上的show logging命令查看日志:
R2#show logging
Syslog logging: enabled (11 messages dropped, 0 messages rate-limited,
 0 flushes, 0 overruns, xml disabled, filtering disabled)
 Console logging: level debugging, 26 messages logged, xml disabled,
 filtering disabled
 Monitor logging: level debugging, 0 messages logged, xml disabled,
 filtering disabled
 Buffer logging: level informational, 7 messages logged, xml disabled,
 filtering disabled
 Logging Exception size (4096 bytes)
 Count and timestamp logging messages: disabled
No active filter modules.
 Trap logging: level informational, 30 message lines logged
 Logging to 192.168.255.250(global) (tcp port 514,audit disabled, link up), 30 message lines logged, xml disabled,
 filtering disabled
Log Buffer (4096 bytes):
*Mar  1 01:02:04.223: %SYS-5-CONFIG_I: Configured from console by console
*Mar  1 01:02:10.275: %SYS-6-LOGGINGHOST_STARTSTOP: Logging to host 192.168.255.250 started - reconnection
*Mar  1 01:02:32.179: %LINK-5-CHANGED: Interface Loopback0, changed state to administratively down
*Mar  1 01:02:33.179: %LINEPROTO-5-UPDOWN: Line protocol on Interface Loopback0, changed state to down
*Mar  1 01:02:39.303: %SYS-5-CONFIG_I: Configured from console by console
*Mar  1 01:02:39.647: %LINK-3-UPDOWN: Interface Loopback0, changed state to up
*Mar  1 01:02:40.647: %LINEPROTO-5-UPDOWN: Line protocol on Interface Loopback0, changed state to up

确认路由器是否发送 syslogs 的一个重要方面是行tcp port 514, audit disabled, link up,这确认了路由器正在向 Syslog 服务器发送 syslog 流量。

  1. 这是 Splunk 上生成的 syslog 的原始输出:

图片

正如我们在“新搜索”部分所看到的那样,我们可以编写查询来获取我们想要的确切数据。在我们的例子中,我们只想看到来自我们的路由器且带有Interface Loopback0下线消息的日志,因此我们编写了以下查询:

host="192.168.255.248" "Interface Loopback0, changed state to down"
  1. 现在让我们看看我们可以用 Python 编写的代码来使用脚本获取相同的信息:
import requests
import json
from xml.dom import minidom

username="admin"
password="admin"

### For generating the session key ####
url = 'https://localhost:8089/services/auth/login'
headers = {'Content-Type': 'application/json'}
data={"username":username,"password":password}
requests.packages.urllib3.disable_warnings()
r = requests.get(url, auth=(username, password), data=data, headers=headers,verify=False)
sessionkey = minidom.parseString(r.text).getElementsByTagName('sessionKey')[0].childNodes[0].nodeValue

#### For executing the query using the generated sessionkey
headers={"Authorization":"Splunk "+sessionkey}
data={"search":'search host="192.168.255.248" "Interface Loopback0, changed state to down"',"output_mode":"json"}
r=requests.post('https://localhost:8089/servicesNS/admin/search/search/jobs/export',data=data , headers=headers,verify=False);
print (r.text)

在第一部分,我们查询 Splunk 的 API 以获取运行查询和获取结果的认证会话密钥(或令牌)。一旦我们有了会话密钥(从 XML 输出中提取),我们创建一个头文件,并使用requests.post执行我们的查询。数据变量包含以下格式的我们的查询:

{"search":'search host="192.168.255.248" "Interface Loopback0, changed state to down"'}

换句话说,如果我们把这个作为一个变量(命名为Search),并将结果作为值提供给这个变量,它看起来会像下面这样:

Search='search host="192.168.255.248" "Interface Loopback0, changed state to down"'

此外,我们还提供了另一个output_mode选项,因为我们希望输出为 JSON 格式(其他值可以是 CSV 或 XML)。

执行相同的操作将得到以下输出:

图片

正如我们在前面的输出中所看到的那样,我们现在正在以 JSON 格式检索和显示值。

我们在这里停止我们的例子,但为了增强这个脚本,这个结果现在可以成为一个触发器,我们可以添加额外的方法或逻辑来决定触发进一步的动作。通过这种逻辑,我们可以有自我修复的脚本,这些脚本可以找出数据(作为触发器),评估触发器(识别其可操作性),并根据进一步的逻辑采取行动。

在各种技术领域上的自动化示例

通过对自动化与设备、API、控制器交互的熟悉和理解,让我们看看一些如何与其他网络域设备交互以及使用自动化框架处理一些复杂场景的例子。

其中一些示例将是一个小项目本身,但将帮助你深入了解执行自动化任务的方式。

BGP 和路由表

让我们举一个例子,我们需要配置 BGP,验证会话是否启动,并报告相同细节。在我们的例子中,我们会选择两个路由器(作为先决条件,两个路由器都能互相 ping 通)如下:

图片

正如我们所见,R2testrouter能够使用彼此的FastEthernet0/0接口的 IP 地址互相 ping 通。

下一步是一个非常基本的 BGP 配置(在我们的案例中,我们使用自治系统AS)号200)。代码如下:

from netmiko import ConnectHandler
import time

def pushbgpconfig(routerip,remoteip,localas,remoteas,newconfig="false"):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds=""
    cmds="router bgp "+localas
    cmds=cmds+"\n neighbor "+remoteip+" remote-as "+remoteas
    xcheck=device.send_config_set(cmds)
    print (xcheck)
    outputx=device.send_command("wr mem")
    print (outputx)
    device.disconnect()

def validatebgp(routerip,remoteip):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="show ip bgp neighbors "+remoteip+" | include BGP state"
    outputx=device.send_command(cmds)
    if ("Established" in outputx):
        print ("Remote IP "+remoteip+" on local router "+routerip+" is in ESTABLISHED state")
    else:
        print ("Remote IP "+remoteip+" on local router "+routerip+" is NOT IN ESTABLISHED state")
    device.disconnect()

pushbgpconfig("192.168.255.249","192.168.255.248","200","200")
### we give some time for bgp to establish
print ("Now sleeping for 5 seconds....")
time.sleep(5) # 5 seconds
validatebgp("192.168.255.249","192.168.255.248")

输出如下:

图片

正如我们所见,我们将邻居配置(BGP 配置)推送到路由器。一旦配置被推送,脚本等待 5 秒钟并验证 BGP 的状态是否处于ESTABLISHED状态。这种验证确认了我们推送的配置中所有新配置的会话都已建立。

让我们推送一个不正确的配置如下:

from netmiko import ConnectHandler
import time
def pushbgpconfig(routerip,remoteip,localas,remoteas,newconfig="false"):
 uname="cisco"
 passwd="cisco"
 device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
 cmds=""
 cmds="router bgp "+localas
 cmds=cmds+"\n neighbor "+remoteip+" remote-as "+remoteas
 xcheck=device.send_config_set(cmds)
 print (xcheck)
 outputx=device.send_command("wr mem")
 print (outputx)
 device.disconnect()
def validatebgp(routerip,remoteip):
 uname="cisco"
 passwd="cisco"
 device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
 cmds="show ip bgp neighbors "+remoteip+" | include BGP state"
 outputx=device.send_command(cmds)
 if ("Established" in outputx):
 print ("Remote IP "+remoteip+" on local router "+routerip+" is in ESTABLISHED state")
 else:
 print ("Remote IP "+remoteip+" on local router "+routerip+" is NOT IN ESTABLISHED state")
 device.disconnect()

pushbgpconfig("192.168.255.249","192.168.255.248","200","400")
### we give some time for bgp to establish
print ("Now sleeping for 5 seconds....")
time.sleep(5) # 5 seconds
validatebgp("192.168.255.249","192.168.255.248")

前面代码的输出如下:

正如我们在前面的输出中看到的,现在我们正在使用一个错误的远程配置(在这种情况下是400)。当然,由于配置不正确,我们得到了一个未建立的消息,这证实了我们推送的配置是不正确的。以类似的方式,我们可以通过为每个要配置的远程邻居调用方法多次来推送大量配置。此外,有时我们需要从运行配置的某些配置部分获取特定信息。

例如,以下代码将为运行配置的每个部分输出一个列表:

from netmiko import ConnectHandler
import itertools

class linecheck:
    def __init__(self):
        self.state = 0
    def __call__(self, line):
        if line and not line[0].isspace():
            self.state += 1
        return self.state

def getbgpipaddress(routerip):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="show running-config"
    outputx=device.send_command(cmds)
    device.disconnect()
    for _, group in itertools.groupby(outputx.splitlines(), key=linecheck()):
        templist=list(group)
        if (len(templist) == 1):
            if "!" in str(templist):
                continue 
        print(templist)

getbgpipaddress("192.168.255.249")

输出如下:

正如我们在前面的输出中看到的,我们得到了运行配置的所有部分,除了在运行配置中看到的感叹号!(在路由器上执行show running-config命令时)。这个输出的重点是,我们现在解析了运行配置中的每个部分,或者换句话说,一个特定的配置集(如接口或 BGP)被组合在一个单独的列表中。

让我们增强这段代码。例如,我们只想看到在路由器中配置了哪些 BGP 远程 IP:

from netmiko import ConnectHandler
import itertools
import re

class linecheck:
    def __init__(self):
        self.state = 0
    def __call__(self, line):
        if line and not line[0].isspace():
            self.state += 1
        return self.state

def getbgpipaddress(routerip):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="show running-config"
    outputx=device.send_command(cmds)
    device.disconnect()
    for _, group in itertools.groupby(outputx.splitlines(), key=linecheck()):
        templist=list(group)
        if (len(templist) == 1):
            if "!" in str(templist):
                continue 
        if "router bgp" in str(templist):
            for line in templist:
                if ("neighbor " in line):
                    remoteip=re.search("\d+.\d+.\d+.\d+",line)
                    print ("Remote ip: "+remoteip.group(0))

getbgpipaddress("192.168.255.249")

输出如下:

在这种情况下,首先我们解析运行配置,并关注包含router bgp配置的部分。一旦到达那个特定的列表,我们就解析列表并使用正则表达式在包含字符串neighbor的特定命令中获取远程 IP。结果值将是 BGP 部分下的远程 IP。

由于我们正在处理 BGP,AS 号码作为 BGP 的一个组成部分,需要被解析或验证。使用前面的策略,我们可以获取 BGP 路由/前缀的 AS 号码,但除此之外,还有一个名为pyasn的 Python 库,可以轻松地查找给定公网 IP 地址的 AS 号码信息。

再次,如前所述,在代码中调用它之前,我们需要安装以下库:

pip install cymruwhois

代码如下:

import socket

def getfromhostname(hostname):
    print ("AS info for hostname :"+hostname)
    ip = socket.gethostbyname(hostname)
    from cymruwhois import Client
    c=Client()
    r=c.lookup(ip)
    print (r.asn)
    print (r.owner)

def getfromip(ip):
    print ("AS info for IP : "+ip)
    from cymruwhois import Client
    c=Client()
    r=c.lookup(ip)
    print (r.asn)
    print (r.owner)

getfromhostname("google.com")
getfromip("107.155.8.0")

输出如下:

正如我们所见,第一个方法getfromhostname用于获取给定主机名的信息。另一个方法getfromip用于通过 IP 地址而不是任何主机名来获取相同的信息。

配置思科交换机端口为接入点

当在多设备环境中工作时,除了路由器和交换机外,我们还需要与其他网络设备(如无线设备)交互。本例将展示如何配置交换机,使其特定端口连接到接入点AP)作为 trunk。

在我们的测试案例中,假设在 AP 上配置的 VLAN 为vlan 100vlan 200用于用户,而原生 VLAN 为vlan 10,代码如下:

from netmiko import ConnectHandler
import time

def apvlanpush(routerip,switchport):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="interface "+switchport
    cmds=cmds+"\nswitchport mode trunk\nswitchport trunk encapsulation dot1q\n"
    cmds=cmds+ "switchport trunk native vlan 10\nswitchport trunk allowed vlan add 10,100,200\nno shut\n"
    xcheck=device.send_config_set(cmds)
    print (xcheck)
    device.disconnect()

def validateswitchport(routerip,switchport):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="show interface "+switchport+" switchport "
    outputx=device.send_command(cmds)
    print (outputx)
    device.disconnect()

apvlanpush("192.168.255.245","FastEthernet2/0")
time.sleep(5) # 5 seconds
validateswitchport("192.168.255.245","FastEthernet2/0")

输出如下:

图片

如我们所见,AP 需要连接到我们的交换机端口,该端口需要是一个 trunk,并允许某些访问 VLAN;因此我们创建了两个方法,第一个方法传递路由器/交换机名称和需要配置的接口。

一旦配置成功推送到交换机,我们执行validateswitchport方法来验证该端口是否现在处于 trunk 模式。validateswitchport方法的输出将输出命令的输出,我们可以进一步介绍正则表达式和分割来从该输出中获取我们想要的任何特定信息(如Administrative ModeOperational Mode)。

作为一种增强,我们还可以使用验证方法的结果来调用其他方法,这些方法将执行一些额外的配置(如果需要),基于我们之前得到的结果。(例如,将Trunking Native Mode VLAN更改为20)。

让我们看看带有将原生 VLAN 更改为20的额外增强功能的新代码。代码如下

from netmiko import ConnectHandler
import time

def apvlanpush(routerip,switchport):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="interface "+switchport
    cmds=cmds+"\nswitchport mode trunk\nswitchport trunk encapsulation dot1q\n"
    cmds=cmds+ "switchport trunk native vlan 10\nswitchport trunk allowed vlan add 10,100,200\nno shut\n"
    xcheck=device.send_config_set(cmds)
    print (xcheck)
    device.disconnect()

def validateswitchport(routerip,switchport):
    print ("\nValidating switchport...."+switchport)
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="show interface "+switchport+" switchport "
    outputx=device.send_command(cmds)
    print (outputx)
    outputx=outputx.split("\n")
    for line in outputx:
        if ("Trunking Native Mode VLAN: 10" in line):
            changenativevlan(routerip,switchport,"20")
    device.disconnect()

def changenativevlan(routerip,switchport,nativevlan):
    print ("\nNow changing native VLAN on switchport",switchport)
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="interface "+switchport
    cmds=cmds+"\nswitchport trunk native vlan "+nativevlan+"\n"
    xcheck=device.send_config_set(cmds)
    print (xcheck)
    validateswitchport(routerip,switchport)
    device.disconnect()

apvlanpush("192.168.255.245","FastEthernet2/0")
time.sleep(5) # 5 seconds
validateswitchport("192.168.255.245","FastEthernet2/0")

输出如下:

  • 验证并更改原生 VLAN 为20

图片

  • 使用新的原生 VLAN 号重新验证:

图片

如我们在最终验证中看到的,现在我们有原生 VLAN 20,而不是之前的10。这也是一种良好的故障排除技术,因为在多种场景中,存在对假设分析(基于对某种条件的评估做出决策)的需求,在这种情况下,我们需要根据收到的动态结果采取一些行动。由于,在这里我们的代码验证了原生 VLAN 需要是20,因此我们执行了另一个操作来纠正之前的配置。

配置思科交换机端口以用于 IP 电话

与之前的场景类似,我们希望交换机端口作为 AP 的 trunk 端口,我们可以配置交换机端口与 IP 电话一起工作。配置端口用作 IP 电话的附加任务之一是,另一端机器或数据机器可以连接到 IP 电话进行数据传输。换句话说,当与 IP 电话一起使用时,思科路由器的一个单独的交换机端口可以同时作为语音和数据端口。

让我们看看配置交换机端口作为 IP 电话端口的示例:

from netmiko import ConnectHandler
import time

def ipphoneconfig(routerip,switchport):
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="interface "+switchport
    cmds=cmds+"\nswitchport mode access\nswitchport access vlan 100\n"
    cmds=cmds+ "switchport voice vlan 200\nspanning-tree portfast\nno shut\n"
    xcheck=device.send_config_set(cmds)
    print (xcheck)
    device.disconnect()

def validateswitchport(routerip,switchport):
    print ("\nValidating switchport...."+switchport)
    uname="cisco"
    passwd="cisco"
    device = ConnectHandler(device_type='cisco_ios', ip=routerip, username=uname, password=passwd)
    cmds="show interface "+switchport+" switchport "
    outputx=device.send_command(cmds)
    print (outputx)
    outputx=outputx.split("\n")
    for line in outputx:
        if ("Trunking Native Mode VLAN: 10" in line):
            changenativevlan(routerip,switchport,"20")
    device.disconnect()

ipphoneconfig("192.168.255.245","FastEthernet2/5")
time.sleep(5) # 5 seconds
validateswitchport("192.168.255.245","FastEthernet2/5")

输出如下:

图片

正如我们现在所看到的,配置的端口(FastEthernet 2/5)已被分配了一个Voice VLAN200和一个数据/访问 VLAN 为100(从前面的输出中,注意Access Mode VLAN: 100 (VLAN0100)这一行。任何连接到此端口的 IP 电话都将能够访问其语音和数据使用的两个 VLAN。再次根据前面的示例,我们可以对端口进行额外的验证和检查,并在配置不正确或缺失的情况下触发一些操作)。

无线局域网(WLAN)

有许多厂商提供了后端 API,可以使用 Python 来控制或调用以执行某些无线任务。在无线领域,Netgear是一个常用的厂商。Python 有一个名为pynetgear的库,可以帮助我们实现一些自动化,以控制我们本地连接的设备。

让我们看看一个示例,如何在我们的网络中获取当前连接到本地无线 Netgear 路由器的网络设备:

>>> from pynetgear import Netgear, Device
>>> netgear = Netgear("myrouterpassword", "192.168.100.1","admin","80")
>>> for i in netgear.get_attached_devices():
  print (i)

Netgear方法接受以下顺序的四个参数(routerpasswordrouteriprouterusernamerouterport)。正如当前示例所示,路由器可以通过http://192.168.100.1使用用户名admin和密码myrouterpassword访问。因此,我们使用这些参数调用该方法。

输出如下:

>>> netgear.get_attached_devices()
[Device(signal=3, ip='192.168.100.4', name='ANDROID-12345', mac='xx:xx:xx:xx:xx:xx', type='wireless', link_rate=72), Device(signal=None, ip='192.168.100.55', name='ANDROID-678910', mac='yy:yy:yy:yy:yy:yy', type='wireless', link_rate=72), Device(signal=None, ip='192.168.100.10', name='mylaptop', mac='zz:zz:zz:zz:zz:zz', type='wireless', link_rate=520)]

正如我们所见,get_attached_devices()方法返回了一个包含所有 IP 地址、它们的 MAC 地址(在本例中隐藏)、信号(或正在使用的无线频段)以及连接的链路速率(以 Mbps 为单位)的列表。

我们可以使用类似的方法来操纵带宽、阻止任何用户或执行特定硬件制造商 API 公开的其他任务。

IP 地址管理(IPAM)访问

网络中的另一个要求是使用 IPAM 数据库进行 IPAM。它由不同的厂商提供,在此例中,我们将参考 SolarWind 的 IPAM。SolarWinds 再次是网络监控和执行各种功能的行业标准工具,它有一套良好的 API,可以使用其 ORION SDK 工具包与之交互。

在 Python 中,我们可以安装orionsdk库以实现与 SolarWinds 的交互。让我们看看一个示例,其中我们从 SolarWinds 的 IPAM 模块中获取下一个可用的 IP 地址:

from orionsdk import SwisClient

npm_server = 'mysolarwindsserver'
username = "test"
password = "test"

verify = False
if not verify:
    from requests.packages.urllib3.exceptions import InsecureRequestWarning
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

swis = SwisClient(npm_server, username, password)

print("My IPAM test:")
results=swis.query("SELECT TOP 1 Status, DisplayName FROM IPAM.IPNode WHERE Status=2")
print (results)

### for a formatted printing
for row in results['results']:
 print("Avaliable: {DisplayName}".format(**row))

输出如下:

正如我们在前面的代码中所看到的,我们使用orionsdk库从mysolarwindsserver服务器调用 SolarWinds 的 API。SolarWinds 所需的用户名和密码在脚本中传递,我们使用一个简单的 SQL 查询(SolarWinds 可以理解),如下所示:

 SELECT TOP 1 Status, DisplayName FROM IPAM.IPNode  WHERE Status=2 

此查询获取下一个可用的 IP 地址(在 SolarWinds 中表示为Status=2)并打印出来。第一个打印是原始打印,在for循环中的打印;它以更好的可理解格式打印出值,如前面的输出所示。

示例和用例

在这里,我们将看到一个对大多数网络工程师都常见的详细示例,以及如何使用 Python 自动化它。我们还将创建一个基于 Web 的工具,使其能够从任何环境或机器上运行,仅使用浏览器。

创建基于 Web 的预和后检查验证工具

在以下示例中,我们将看到我们如何对任何网络维护活动进行预检查和后检查。这通常是每个网络工程师在生产设备上执行活动时所需,以确保一旦维护活动完成,工程师没有遗漏任何可能导致后续问题的内容。这也需要验证我们的更改和维护是否成功完成,或者如果验证失败,我们需要执行额外的修复和回滚。

以下是如何创建和执行工具的步骤:

第 1 步 – 创建主 HTML 文件

我们将设计一个基于 Web 的表单来选择我们将用于执行检查的某些显示命令。这些命令在执行时将作为预检查;一旦我们的维护活动完成,我们将再次作为后检查执行。

在预检查或后检查场景中,相同命令输出的任何差异都将被突出显示,工程师将能够根据输出决定将维护活动称为成功或失败。

HTML 代码(prepostcheck.html)如下:

<!DOCTYPE html>

<html >
<head>
         <script>
             function checkme() {
        var a=document.forms["search"]["cmds"].value;
        var b=document.forms["search"]["searchbox"].value;
        var c=document.forms["search"]["prepost"].value;
        var d=document.forms["search"]["changeid"].value;
        if (a==null || a=="")
        {
          alert("Please Fill All Fields");
          return false;
        }
        if (b==null || b=="")
        {
          alert("Please Fill All Fields");
          return false;
        }
        if (c==null || c=="")
        {
          alert("Please Fill All Fields");
          return false;
        }
        if (d==null || d=="")
        {
          alert("Please Fill All Fields");
          return false;
        }
                 document.getElementById("mypoint").style.display = "inline";
             }
</script>
</head>
<body>
<h2> Pre/Post check selection </h2>
<form name="search" action="checks.py" method="post" onsubmit="return checkme()">
Host IP: (Multiple IPs seperated by comma)<br><input type="text" name="searchbox" size='80' required>
<p></p>
Commands (Select):
<br>
<select name="cmds" multiple style="width:200px;height:200px;" required>
  <option value="show version">show version</option>
  <option value="show ip int brief">show ip int brief</option>
  <option value="show interface description">show interface description</option>
  <option value="show clock">show clock</option>
  <option value="show log">show log (last 100)</option>
  <option value="show run">show run</option>
  <option value="show ip bgp summary">show ip bgp summary</option>
  <option value="show ip route">show ip route</option>
  <option value="show ip route summary">show ip route summary</option>
  <option value="show ip ospf">show ip ospf</option>
  <option value="show interfaces status">show interfaces status</option>

</select>
<p></p>
Mantainence ID: <input type="text" name="changeid" required>
<p></p>
Pre/Post: <br>
<input type="radio" name="prepost" value="pre" checked> Precheck<br>
<input type="radio" name="prepost" value="post"> Postcheck<br>
<p></p>
<input type="submit" value="Submit">
<br><br><br>
</form> 
<p><label id="mypoint" style="display: none;background-color: yellow;"><b>Please be Patient.... Gathering results!!!</b></label></p>
</body>
</html>

这将创建一个主页面,我们在其中选择初始选项(命令集以及是否需要执行预检查或后检查)。输出如下:

图片

主页面

HTML 中的附加 JavaScript 代码确保在所有选择都完成之前,提交按钮不会发送任何数据。发送未完成的数据没有意义;例如,如果我们没有填写完整字段,提交选项将不会进行,显示我们在以下截图中所见到的消息:

图片

除非所有字段都已填写,否则点击“提交”按钮将显示此消息,代码将不会继续执行。此外,正如我们在代码中所见,提交按钮与 Python 脚本相关联,checks.py作为 POST 方法。换句话说,我们将做出的选择将以 POST 方法发送到checks.py

第 2 步 – 创建后端 Python 代码

现在,让我们看看后端 Python 代码(checks.py),它将接受来自 HTML 表单的输入并执行其任务。代码如下:

#!/usr/bin/env python
import cgi
import paramiko
import time
import re
import sys
import os
import requests
import urllib
import datetime
from datetime import datetime
from threading import Thread
from random import randrange

form = cgi.FieldStorage()
searchterm = form.getvalue('searchbox')
cmds = form.getvalue('cmds')
changeid = form.getvalue('changeid')
prepost=form.getvalue('prepost')
searchterm=searchterm.split(",")
xval=""
xval=datetime.now().strftime("%Y-%m-%d_%H_%M_%S")

returns = {}
def getoutput(devip,cmd):
    try:
        output=""
        mpath="C:/iistest/logs/"
        fname=changeid+"_"+devip+"_"+prepost+"_"+xval+".txt"
        fopen=open(mpath+fname,"w")
        remote_conn_pre = paramiko.SSHClient()
        remote_conn_pre.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        remote_conn_pre.connect(devip, username='cisco', password='cisco', look_for_keys=False, allow_agent=False)
        remote_conn = remote_conn_pre.invoke_shell()
        remote_conn.settimeout(60)
        command=cmd
        remote_conn.send(command+"\n")
        time.sleep(15)
        output=(remote_conn.recv(250000)).decode()
        fopen.write(output)
        remote_conn.close()
        fopen.close()
        returns[devip]=("Success: <a href='http://localhost/test/logs/"+fname+"' target='_blank'>"+fname +"</a> Created")
    except:
        returns[devip]="Error. Unable to fetch details"

try:
    xtmp=""
    cmdval="terminal length 0\n"
    if (str(cmds).count("show") > 1):
        for cmdvalue in cmds:
            if ("show" in cmdvalue):
                if ("show log" in cmdvalue):
                    cmdvalue="terminal shell\nshow log | tail 100"
                cmdval=cmdval+cmdvalue+"\n\n"
    else:
        if ("show" in cmds):
            if ("show log" in cmds):
                cmds="terminal shell\nshow log | tail 100"
            cmdval=cmdval+cmds+"\n\n"
    threads_imagex= []
    for devip in searchterm:
        devip=devip.strip()
        t = Thread(target=getoutput, args=(devip,cmdval,))
        t.start()
        time.sleep(randrange(1,2,1)/20)
        threads_imagex.append(t)

    for t in threads_imagex:
        t.join()

    print("Content-type: text/html")
    print()
    xval=""
    for key in returns:
        print ("<b>"+key+"</b>:"+returns[key]+"<br>")

    print ("<br>Next step: <a href='http://localhost/test/selectfiles.aspx'> Click here to compare files </a>")
    print ("<br>Next step: <a href='http://localhost/test/prepostcheck.html'> Click here to perform pre/post check </a>")

except:
    print("Content-type: text/html")
    print()
    print("Error fetching details. Need manual validation")
    print ("<br>Next step: <a href='http://localhost/test/selectfiles.aspx'> Click here to compare files </a>")
    print ("<br>Next step: <a href='http://localhost/test/prepostcheck.html'> Click here to perform pre/post check </a>")

此代码使用 CGI 参数从网页接收输入。网页的各种值使用以下代码片段解析到变量中:

form = cgi.FieldStorage()
searchterm = form.getvalue('searchbox')
cmds = form.getvalue('cmds')
changeid = form.getvalue('changeid')
prepost=form.getvalue('prepost')

一旦我们有了这些值,附加的逻辑就是使用paramiko库登录到指定的设备,获取show命令的输出,并将其保存在logs文件夹下的文件中。这里需要注意的一个重要方面是我们构建文件名的方式:

#xval=datetime.now().strftime("%Y-%m-%d_%H_%M_%S")
#and
#fname=changeid+"_"+devip+"_"+prepost+"_"+xval+".txt"

fname是我们将要写入输出的文件名,但文件名是根据维护 ID、设备 IP、预/后状态以及文件创建的时间动态构建的。这是为了确保我们知道我们正在执行预检查或后检查的设备,以及文件是在什么时间创建的,以确保我们有一个正确的预检查和后检查组合。

getoutput()函数从线程(在多线程函数调用中)调用以获取输出并将其存储在新建的文件中。调用多线程过程是因为如果我们想在多个设备上执行预检查或后检查,我们可以在 Web 中提供一个以逗号分隔的 IP 地址列表,Python 脚本将并行地在所有设备上调用show命令,并根据主机名创建多个预检查或后检查文件。

让我们在示例中为一些命令创建一个precheck文件,其中我们填写一些值并点击提交按钮:

图片

当数据收集正在进行时,黄色消息将显示以确认后端工作正在进行。

任务完成后,我们看到的是(如 Python 代码返回的):

图片

如我们所见,代码返回成功,这意味着它能够获取我们想要验证的命令的输出。文件名是根据我们在主页上的选择动态创建的。

点击生成的可点击 URL(.txt 文件名,可以用来重新确认我们是否得到了之前选择的命令的正确输出),显示以下输出:

图片

现在,让我们执行相同的步骤并创建一个postcheck文件。

我们回到主页,保持其他值不变,只是选择单选按钮从Precheck改为Postcheck。请确保我们选择相同的命令集,因为只有当我们有相同的数据来工作的时候,预检查和后检查才有意义:

图片

以类似的方式,一旦后端执行完成,我们将创建一个如下所示的postcheck文件:

图片

注意文件名、时间戳以及根据我们的选择post字样会发生变化。

第 3 步 – 为工具创建基于 Web 服务器的文件

现在已经创建了前后检查文件,让我们创建一个用于执行基于网络的文件前后检查的 Web 框架。我们需要创建一个网页,其中我们的当前日志文件作为前后文件可见,并且我们可以选择precheck文件及其相关的postcheck文件进行比较。正如我们所知,我们不能使用 HTML 或浏览器语言从服务器获取任何文件的信息,因此我们需要使用某种后端 Web 语言来为我们执行此功能。我们利用 ASP 和 VB.NET 创建网页,以显示已创建的日志文件以供选择和比较。

selectfiles.aspx的后端代码如下(这是在浏览器上显示日志目录中的文件):

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="selectfiles.aspx.vb" Inherits="selectfiles" %>

<!DOCTYPE html>

<html >
<head runat="server">
    <title></title>
</head>
<body>
    <form id="form1" method="post" action="comparefiles.aspx" >
    <div>
    <%response.write(xmystring.tostring())%>
    </div>
         <input type="submit" value="Submit">
    </form>
  <br><br><br>
</body>
</html>

用于在前面提到的.aspx页面selectfiles.aspx.vb上填写值的 VB.NET 后端代码如下:

Imports System.IO
Partial Class selectfiles
    Inherits System.Web.UI.Page
    Public xmystring As New StringBuilder()
  Public tableval As New Hashtable
    Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
        Dim precheck As New List(Of String)
        Dim postcheck As New List(Of String)
        Dim prename = New SortedList
        Dim postname = New SortedList
        Dim infoReader As System.IO.FileInfo
    Dim rval as Integer
    rval=0

        xmystring.Clear()
        Dim xval As String
    Dim di As New DirectoryInfo("C:\iistest\logs\")
    Dim lFiles As FileInfo() = di.GetFiles("*.txt")
    Dim fi As System.IO.FileSystemInfo
    Dim files() As String = IO.Directory.GetFiles("C:\iistest\logs\", "*.txt", SearchOption.TopDirectoryOnly)
    xmystring.Append("<head><style type='text/css'>a:hover{background:blue;color:yellow;}</style></head>")
        xmystring.Append("<fieldset style='float: left;width: 49%;display: inline-block;box-sizing: border-box;'>")
        xmystring.Append("<legend>Pre check files (Sorted by Last Modified Date)</legend>")

         For Each fi In lFiles
      rval=rval+1
      tableval.add(fi.LastWriteTime.ToString()+rval.tostring(),fi.Name)
            'infoReader = My.Computer.FileSystem.GetFileInfo(file)
            If (fi.Name.Contains("pre")) Then
                precheck.Add(fi.LastWriteTime.ToString()+rval.tostring()) 
            Else
                postcheck.Add(fi.LastWriteTime.ToString()+rval.tostring())
            End If
        Next
        precheck.Sort()
        postcheck.Sort()

        xval = ""
        Dim prekey As ICollection = prename.Keys
        Dim postkey As ICollection = postname.Keys
        Dim dev As String
    Dim fnameval as String
        For Each dev In precheck
            infoReader = My.Computer.FileSystem.GetFileInfo(tableval(dev))
      fnameval="http://localhost/test/logs/"+Path.GetFileName(tableval(dev))
            xval = "<input type = 'radio' name='prechecklist' value='C:\iistest\logs\" + tableval(dev) + "' required><a href='" & fnameval & "' target='blank'>" & tableval(dev) & "</a> ( <b>" & dev.Substring(0,dev.LastIndexOf("M")).Trim() + "M</b>)<br>"

      xmystring.Append(xval)
        Next
    xmystring.Append("</fieldset>")
           xmystring.Append("<fieldset style='float: right;width: 49%;display: inline-block;box-sizing: border-box;'>")
        xmystring.Append("<legend>Post check files (Sorted by Last Modified Date)</legend>")
              For Each dev In postcheck
      fnameval="http://localhost/test/logs/"+tableval(dev)
            xval = "<input type = 'radio' name='postchecklist' value='C:\iistest\logs\" + tableval(dev) + "' required><a href='" & fnameval & "' target='blank'>" & tableval(dev) & "</a> ( <b>" & dev.Substring(0,dev.LastIndexOf("M")).Trim() + "M</b>)<br>"
            xmystring.Append(xval)
        Next
        xmystring.Append("</fieldset>")

    End Sub
End Class

此代码用于从日志目录获取文件,并根据它们的文件名,将它们分为precheck文件或postcheck文件。此外,文件按时间顺序排列,以便在比较过程中易于选择。

现在让我们看看这个页面的输出:

图片

第 4 步 – 创建用于前后文件比较的服务器端文件

最后一步是创建一个网页,它检索这些文件中的文本,并提供前端(或基于 Web 的工具)以便于比较。为了我们的目的,我们使用一个名为diffview的 JScript 库要调用此依赖项,我们需要下载diffview.jsdifflib.jsdiffview.css,这些文件可在以下位置找到:github.com/cemerick/jsdifflib,并将文件复制到我们的 Web 服务器文件夹中。一旦完成,我们就会像访问文件一样再次创建一个.aspx页面来获取所选文件的内容,并将其显示以进行比较。

下面的代码是主页面comparefiles.aspx的代码:

<%@ Page Language="VB" AutoEventWireup="false" CodeFile="comparefiles.aspx.vb" Inherits="comparefiles" %>

<!DOCTYPE html>

<html >
<head>
  <meta charset="utf-8"/>
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1"/>
  <link rel="stylesheet" type="text/css" href="diffview.css"/>
  <script type="text/javascript" src="img/diffview.js"></script>
  <script type="text/javascript" src="img/difflib.js"></script>
<style type="text/css">
body {
  font-size: 12px;
  font-family: Sans-Serif;
}
h2 {
  margin: 0.5em 0 0.1em;
  text-align: center;
}
.top {
  text-align: center;
}
.textInput {
  display: block;
  width: 49%;
  float: left;
}
textarea {
  width:100%;
  height:300px;
}
label:hover {
  text-decoration: underline;
  cursor: pointer;
}
.spacer {
  margin-left: 10px;
}
.viewType {
  font-size: 16px;
  clear: both;
  text-align: center;
  padding: 1em;
}
#diffoutput {
  width: 100%;
}
</style>

<script type="text/javascript">

function diffUsingJS(viewType) {
  "use strict";
  var byId = function (id) { return document.getElementById(id); },
    base = difflib.stringAsLines(byId("baseText").value),
    newtxt = difflib.stringAsLines(byId("newText").value),
    sm = new difflib.SequenceMatcher(base, newtxt),
    opcodes = sm.get_opcodes(),
    diffoutputdiv = byId("diffoutput"),
    contextSize = byId("contextSize").value;

  diffoutputdiv.innerHTML = "";
  contextSize = contextSize || null;

  diffoutputdiv.appendChild(diffview.buildView({
    baseTextLines: base,
    newTextLines: newtxt,
    opcodes: opcodes,
    baseTextName: "Base Text",
    newTextName: "New Text",
    contextSize: contextSize,
    viewType: viewType
  }));
}

</script>
</head>
<body>
  <div class="top">
    <strong>Context size (optional):</strong> <input type="text" id="contextSize" value="" />
  </div>
  <div class="textInput">
    <h2>Pre check</h2>
    <textarea id="baseText" runat="server" readonly></textarea>
  </div>
  <div class="textInput spacer">
    <h2>Post check</h2>
    <textarea id="newText" runat="server" readonly></textarea>
  </div>
    <% Response.Write(xmystring.ToString()) %>
  <div class="viewType">
    <input type="radio" name="_viewtype" id="sidebyside" onclick="diffUsingJS(0);" /> <label for="sidebyside">Side by Side Diff</label>
    &nbsp; &amp;amp;amp;nbsp;
    <input type="radio" name="_viewtype" id="inline" onclick="diffUsingJS(1);" /> <label for="inline">Inline Diff</label>
  </div>
  <div id="diffoutput"> </div>

</body>
</html>

用于获取文件内容(comparefiles.aspx.vb)的主页后端代码如下:

Imports System.IO

Partial Class comparefiles
    Inherits System.Web.UI.Page
    Public xmystring As New StringBuilder()

    Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
        Dim fp As StreamReader
        Dim precheck As New List(Of String)
        Dim postcheck As New List(Of String)
        xmystring.Clear()
        Dim prefile As String
        Dim postfile As String
        prefile = Request.Form("prechecklist")
        postfile = Request.Form("postchecklist")
        fp = File.OpenText(prefile)
        baseText.InnerText = fp.ReadToEnd()
        fp = File.OpenText(postfile)
        newText.InnerText = fp.ReadToEnd()
        fp.Close()

    End Sub

End Class

准备就绪后,让我们比较文件并查看结果。我们选择前后检查文件,然后点击提交

图片

下一页带我们到内容和比较:

图片

如前述截图所示,在左侧,我们有precheck文件,在右侧,我们有postcheck文件。两者都可以通过两个窗口上的幻灯片在页面上本身读取。当选择Side by Side DiffInline Diff时,底部窗口会出现。

Side by Side Diff中,任何不同之处都会被突出显示。在我们的案例中,是运行时间不同。对于其他所有共同点,没有颜色突出显示,工程师可以安全地假设非突出显示颜色的相同状态。

让我们用Inline Diff比较选择来查看相同的示例:

图片

结果是一样的;不同的行用不同的颜色突出显示,以确认前后检查的差异。有了这个工具,工程师现在可以快速解析整个日志文件,并根据突出显示的差异(precheck文件内容和postcheck文件内容之间的不匹配),决定将任务标记为成功或失败。

摘要

在本章中,我们看到了与日常网络场景中自动化使用相关的各种概念。我们熟悉了执行与无线 AP 和 IP 电话等附加设备相关的各种任务的例子。此外,我们还介绍了 SolarWinds 的 IPAM 以及如何使用 Python 操作 API。

我们还看到了一个创建前后验证工具的真实世界例子,以帮助工程师快速做出维护验证决策,并且将该工具移植到网络上,这样工具就可以在任何地方使用,而不是在安装了 Python 的单独机器上运行。

最后,在我们的结论章节中,我们将探讨 SDN 的一些附加方面,以更好地理解使用方法以及如何在 SDN 场景中自动化。

第七章:网络自动化中的 SDN 概念

如我们迄今为止所看到的,有许多场景可以自动化网络,从日常或常规任务,到从单一基于控制器的架构管理基础设施。在这些概念的基础上,我们将现在获得一些关于在 软件定义网络SDNs)中工作的额外见解,并查看一些与云平台一起工作的示例。

我们将要介绍的一些关键组件包括:

  • 云平台自动化

  • 网络自动化工具

  • 基于控制器的网络架构

  • 可编程网络设备

管理云平台

我们可以通过 Python 中的网络自动化技术来处理各种云提供商。从处理云实例,到启动新的虚拟机,控制完全访问,如 ACLs,以及创建特定网络层任务,如 VPN 和每个实例的网络配置,我们可以使用 Python 中可用的连接器或 API 自动化几乎所有事情。让我们看看在最受欢迎的云平台 Amazon Web ServicesAWS)上使用 Python 的一些基本配置和连接。

AWS 通过其 SDK Boto 3 提供了一个广泛的 API。Boto 3 提供两种类型的 API 供使用,一种用于与直接 AWS 服务交互的低级 API 集合,以及一种用于与 AWS 快速交互的 Python 友好的高级 API 集合。除了 Boto 3 之外,我们还需要 AWS CLI,它用作 命令行界面CLI)从本地机器与 AWS 交互。从 CLI 角度来看,这就像 DOS 对 Windows 一样是一个基于 CLI 的工具。

AWS CLI 和 Boto 3 的安装都使用 pip 完成:

  • 要从 AWS CLI 安装,请使用以下命令:
pip install awscli
  • 要从 Boto 3 安装,请使用以下命令:
pip install boto3

安装完成后,这些包就准备好使用了。然而,我们需要在 AWS 网络管理控制台中配置一个访问密钥,它将具有某种程度的限制(我们将在创建访问密钥时定义)。

让我们快速设置一个新的访问密钥,以便从我们的本地机器管理 AWS:

  1. 登录 AWS 网络控制台,选择 IAM 选项:

图片

  1. 点击添加用户以创建以下所示的用户名和密码对:

图片

  1. 选择用户名并确保勾选程序访问权限,以获取用于我们 Python 调用的访问 ID 和密钥:

图片

  1. 我们还需要用户成为某个特定组的成员(出于安全限制)。在我们的例子中,我们将其设置为管理员组的成员,该组在 AWS 实例上拥有完全权限:

图片

  1. 如果我们正确地选择了选项,将创建一个用户,用户名为我们选择的 (booktest),并带有访问密钥和秘密访问密钥:

图片

  1. 一旦我们有了这个密钥,我们就回到我们的 Python 安装,在命令提示符下调用 AWS CLI 命令aws configure

图片

  1. 根据提出的问题,我们从 AWS 网络控制台获取值并将其粘贴到 CLI 中。默认输出格式可以是textjson。然而,为了我们的自动化目的和与 Python 一起工作,我们会选择json而不是text

一旦我们完成了这个后端配置,我们就可以通过在 Python 中调用 Boto 3 API 来测试我们的脚本。

让我们看看获取当前 AWS 账户中所有运行实例的示例,其中我们拥有密钥:

import boto3
ec2 = boto3.resource('ec2')
for instance in ec2.instances.all():
    print (instance)
    print (instance.id, instance.state)

由于我们已通过aws configure CLI 命令配置了后端凭据和密钥,因此我们不需要在我们的脚本中指定任何凭据。

上述代码的输出如下:

图片

正如我们在前面的输出中看到的,我们得到了两个实例,它们是具有其实例 ID 的 EC2 实例。此外,我们还得到了一些当前配置实例的其他参数。在某些情况下,如果我们不想使用当前的预配置密钥,我们可以通过以下方式直接将值传递给 Boto 3 函数来调用 Python 程序:

import boto3

aws_access_key_id = 'accesskey'
aws_secret_access_key = 'secretaccesskey'
region_name = 'us-east-2'

ec2 = boto3.client('ec2',aws_access_key_id=aws_access_key_id,aws_secret_access_key=aws_secret_access_key,region_name=region_name)

让我们看看获取每个实例的私有 IP 地址和实例 ID 的另一个示例:

import boto3

ec2 = boto3.client('ec2')
response = ec2.describe_instances()
for item in response['Reservations']:
    for eachinstance in item['Instances']:
        print (eachinstance['InstanceId'],eachinstance['PrivateIpAddress'])

上述代码给出了以下输出:

图片

使用 Boto 3 API,我们也可以在我们的订阅中启动新的实例。让我们看看使用 Boto 3 启动一个新的虚拟机VM)的最终示例。

在我们调用 Python 来启动新的 VM 之前,我们需要选择用于实例的哪个亚马逊机器镜像AMI)图像。要找出 AMI 图像值,我们需要打开如以下所示的 AWS 网络控制台中的 AMI:

图片

一旦我们完成了 AMI 的最终确定,我们就进入了简单的部分,即启动新的 VM:

import boto3
ec2 = boto3.resource('ec2')
ec2.create_instances(ImageId='amid-imageid', MinCount=1, MaxCount=5)

脚本执行需要一些时间,结果值将是基于所选 AMI 图像 ID 配置的所有参数的实例。同样,我们可以使用 Boto 3 启动各种类型的实例,甚至可以启动新的安全过滤器,并确保我们拥有云控制自动化。

可编程网络设备

回顾历史实现,我们有一套固定的硬件或网络,专为向最终用户提供服务而设计。最终用户也有有限的连接选项来访问有限的网络或连接资源。随着用户数量的增加,一个简单的解决方案是添加额外的硬件或网络设备。然而,随着不同终端用户设备(如手机)的激增,以及最终用户对高数据需求和正常运行时间的要求,管理不断增加的硬件和额外连接变得复杂。

简单的设备故障或电缆故障可能会影响整个连接的硬件或网络设备,这将为最终用户造成广泛的中断,导致生产力和信任度下降。想象一下,一个大型互联网服务提供商ISP)频繁出现故障,每次故障都会影响大量企业和家庭用户。如果一个新的 ISP 带着可靠性作为其独特的卖点进入市场,人们将毫不犹豫地跳转到新的提供商。实际上,这可能导致业务损失,最终,由于当前用户群中可靠性和信任度的下降,导致早期提供商关闭。

为了处理这种情况,出现了一种解决方案,即使用同一套设备或网络硬件,在相同的硬件平台上执行不同的功能。这是通过 SDN 和可编程网络PNs)的组合实现的。

SDN 负责控制平面配置,以便数据能够自动重新路由到从特定源到目的地的最佳可用路径。例如,假设我们需要从源 A 到达目的地 D。到达 D 的最佳路径是 A -> C -> D。

现在,在处理传统流量流的情况下,除非 C 出现故障或实际上关闭,否则流量不会从 A -> B -> D 流动(除非在每个网络设备/设备上执行特殊的复杂配置)。在 SDN 环境中,使用 OpenFlow 作为底层协议,控制器将检测 A -> C -> D 路径上的任何问题,并根据某些问题(如路径中的数据包丢失或拥塞),做出智能决策,确保有新的路径让数据从 A -> B -> D 流动。

就像我们在本例中看到的那样,即使 C 没有物理问题,SDN 也已经负责确定数据流动的最佳路径,这有效地实现了对最终用户来说最佳的可实现性能,并且具有可靠性。

PN 是一个补充,它是一组网络层的硬件设备,可以根据需求编程以不同的方式行为。想象一下,一个交换机通过编写一段代码来改变其功能,充当一个路由器。假设我们迎来了大量新的终端用户,并且需要在网络层拥有高交换能力。一些设备现在可以充当交换机而不是路由器。这确保了两方面的好处:

  • 根据需求和需求使用同一套硬件,硬件可以重用于处理新的场景,而不会通过添加额外的硬件集来增加网络的复杂性。

  • 通过增加通过同一组设备安全传输流量的额外能力,更好地控制流量。这是通过为从一组特定设备流向流量的流量添加 ACLs 来实现的,甚至确保设备只处理特定类型的流量,并将剩余流量发送到专门编程以处理该特定流量的其他设备。想象一下,视频和语音流量从不同的设备流向,以确保使用我们目前拥有的相同硬件在特定设备上实现最佳性能和负载。

可编程网络设备(PNs)的一个主要组成部分是使用由各种网络供应商(如 Cisco、Arista 和 Juniper)提供的 API。通过调用这些 API,我们可以确保来自特定供应商的每个设备可以轻松地相互通信(以统一格式交换信息),并且可以根据 API 调用改变特定硬件的行为。在当今市场上一个常见的例子是 Cisco Nexus 9000 系列设备。这些是模块化或固定交换机(具有不同的变体),通过使用 OpenFlow,我们能够根据动态需求以编程方式改变它们的行为。

以这个开关为例,直接访问专用集成电路ASIC)芯片级编程也被暴露出来,这确保了 ASIC 也可以根据需求以及软件级别的变化进行编程。在 SDN 部署的情况下,控制器可以利用 OpenFlow 以及这些交换机上暴露的 API 来控制这些交换机的角色。

Cisco 还为多个设备(主要在 Nexus 平台上)提供了一种电源开启自动配置PoAP)功能,这有助于在新的设备启动时立即实现自动配置和部署。这个过程的基本概述是,如果一个启用了 PoAP 功能的 Nexus 设备启动后无法找到任何启动配置,它将在网络中定位一个动态主机配置协议DHCP)服务器,并使用从该 DHCP 服务器获得的 IP 地址和 DNS 信息进行引导。它还会获取一个在设备上执行的定制脚本,该脚本包含下载和安装相关软件映像文件以及该设备的特定配置的指令。

这种类型功能的一个大优势是,我们只需打开电源并将其连接到具有 DHCP 功能以获取网络中新设备相关信息的网络,就可以在一到两分钟内启动新设备。想象一下,与需要数小时人工干预的传统方式相比,现在启动路由器的方式,路由器无需任何人工干预就能自行处理。

类似地,使用 API(NX-API 是用于 Nexus API 的底层术语),Cisco 也提供了关于数据包流和监控的更好可见性,并且可以使用任何语言(如 Python)编写的简单脚本,根据通过调用这些 API 返回的结果修改路径和流量。

以另一个例子为例,我们有一个网络设备供应商 Arista。Arista 引入了 Arista 可扩展操作系统(EOS),这是一个高度模块化和基于 Linux 的网络操作系统。使用 Arista EOS,管理多个设备变得容易,因为它具有提供广泛 API(基于 Linux 内核和与 Arista 相关的附加 API)的能力,并调用 API 为各种供应商配置和部署多个终端节点。Arista 引入的一个名为 智能系统升级(SSU)的功能确保,当我们对 Arista 设备进行操作系统升级时,它将使用升级的操作系统版本重新启动其服务,但不会重启,以确保在升级期间最小化流量中断。这些功能确保即使在数据中心或多个设备上同时推出新的补丁和操作系统升级时,我们也有弹性和正常运行时间。

Arista EOS 通过提供一套 API 调用 eAPI 来为通过 API 管理的设备提供扩展功能。eAPI 可以通过从任何脚本或可编程语言中调用 eAPI 框架来配置 Arista 设备。让我们看看如何使用 eAPI 管理一个 Arista 交换机的非常基本的例子。

我们需要在 Arista 交换机上配置 eAPI:

Arista> enable
Arista# configure terminal
Arista(config)# management api http-commands
Arista(config-mgmt-api-http-cmds)# no shutdown
Arista(config-mgmt-api-http-cmds)# protocol http
Arista(config-mgmt-api-http-cmds)#end

这确保了 Arista eAPI 功能在路由器上已启用,我们可以使用 HTTP 协议与 API 进行交互。我们还可以通过使用命令 protocol https 在 HTTPS 上可用的 eAPI 选项之间切换。

为了验证我们的配置是否正确,我们使用命令 show management api http-commands,如下所示:

Arista# show management api http-commands 
Enabled: Yes 
HTTPS server: shutdown, set to use port 443 
HTTP server: running, set to use port 80

我们可以使用浏览器命令 http://<路由器 IP> 来检查 eAPI 框架是否现在可以访问。

Arista 的一些例子展示了我们使用 URL(在这种情况下我们启用了 HTTPS 而不是 HTTP)得到的输出:

在这里,我们看到一组传递的命令(show versionshow hostname),API 的响应确认了结果集。此外,命令响应文档选项卡显示了我们可以用于参考的可用 API:

让我们看看如何在 Python 中调用相同的操作:

作为先决条件,我们需要安装 jsonrpclib,可以在 URL pypi.python.org/pypi/jsonrpclib. 找到。这个库用于解析 JSON 格式的 远程过程调用(RPC)。一旦完成,以下代码将产生与使用浏览器得到相同的一组值:

from jsonrpclib import Server 
switch = Server( "https://admin:admin@172.16.130.16/command-api" ) 
response = switch.runCmds( 1, [ "show hostname" ] ) 
print ("Hello, my name is: ", response[0][ "hostname" ] )
response = switch.runCmds( 1, [ "show version" ] ) 
print ("My MAC address is: ", response[0][ "systemMacAddress" ] )
print ("My version is: ", response[0][ "version" ])

上述代码给出了以下输出:

Hello, my name is: Arista 
My MAC address is: 08:00:27:0e:bf:31 
My version is: 4.14.5F

以类似的方式,Arista 也引入了一个 Python 库,可以作为jsonrpclib的替代品。可以在 URL pypi.python.org/pypi/pyeapi找到的pyeapi库,是 Arista EOS eAPI 的 Python 包装器。按照示例,以下是使用pyeapi访问相同设备集的方法。

从开发者页面,这里有一个示例,展示了我们如何使用pyeapi在 Arista 上进行 API 处理:

>>> from pprint import pprint as pp
>>> node = pyeapi.connect(transport='https', host='veos03', username='eapi', password='secret', return_node=True)
>>> pp(node.enable('show version'))
[{'command': 'show version',
  'encoding': 'json',
  'result': {u'architecture': u'i386',
             u'bootupTimestamp': 1421765066.11,
             u'hardwareRevision': u'',
             u'internalBuildId': u'f590eed4-1e66-43c6-8943-cee0390fbafe',
             u'internalVersion': u'4.14.5F-2209869.4145F',
             u'memFree': 115496,
             u'memTotal': 2028008,
             u'modelName': u'vEOS',
             u'serialNumber': u'',
             u'systemMacAddress': u'00:0c:29:f5:d2:7d',
             u'version': u'4.14.5F'}}]

观察 Cisco 和 Arista(它们是云和 SDN 市场的主要参与者),我们可以结合 Arista eAPI 和 Cisco NX-API 来管理我们的整个数据中心库存,并在一些任务上工作,如新设备的提供或现有设备的升级,这些任务对业务流程的影响最小或没有影响,从而确保可扩展性、可靠性和正常运行时间。

基于控制器的网络布线

随着我们走出每个物理路径都是连接和设计用来从一个点到另一个点传输流量的传统硬件时代,以及数据包从一台设备到达另一台设备的可用性有限,SDN 确保我们有网络布线,以便我们的数据在源和目的地之间传输。

网络布线是一组通过一个共同的控制器连接在一起的不同网络设备,确保网络中的每个组件都优化了发送流量到每个节点的性能。底层的交换布线,即带有端口(如以太网、ATM 和 DSL)的物理交换板,也由控制器控制和编程,可以确保(通过创建路径或特定的端口)特定的数据类型可以穿越到达到其目的地。

在典型的网络设计中,我们有第 2 层(或交换域)和第 3 层(或路由域)。如果我们没有基于控制器的方案,每个网络组件都可以从其下一个连接的组件学习流量的行为(如第 2 层的生成树协议STP))或某些路由协议(如第 3 层的 OSPF)。在这种情况下,每个设备都充当自己的控制器,并且只能有限地看到它直接连接的设备(也称为邻居设备)。在任何设备上都没有整个网络的单一视图,并且此外,每个组件(或单个控制器)都充当其邻居设备的单点故障。任何组件的故障都可能导致其邻居设备重新收敛,甚至由于连接组件的故障而隔离。

与基于控制器的环境相比,理论上每个设备拥有的连接数与其连接的端口数相同。因此,如果我们考虑在基于控制器的环境中连接的三个设备,由于它们之间的物理连接,每个设备之间都有多个连接。在组件(或设备)故障的情况下,控制器可以迅速做出智能决策,重新配置新路径,并改变其他两个网络设备的行为,以确保对交通的最小干扰,保持所有其他可用链路上的相同吞吐量和分布式负载。从理论上讲,控制器消除了每个设备的控制平面行为,并确保控制器管理的每个设备上更新了优化的转发表(用于将数据转发到特定目的地)。这是因为控制器开始充当主要组件,它可以看到每个设备,包括每个设备的每个入口和出口点以及从每个受管理的网络设备流过的数据类型的粒度。

根据厂商,像思科(拥有其开放网络环境)、瞻博网络(拥有其 QFabric 交换机)和艾维雅(拥有其 VENA 交换机)这样的主要厂商已经提供了充当控制器或配置为被控制器管理的功能。此外,随着控制器到管理器网络组件的引入,每个网络设备现在可以虚拟地成为客户端,控制器做出所有智能决策,从学习到转发表。

控制器充当多厂商网络设备和网络任务之间的抽象层。作为终端用户,某人可以配置控制器执行的具体任务,并且,使用来自不同厂商的底层 API 模型(使用 JSON 或 XML),控制器可以将这些具体任务转换为各种厂商特定的 API 调用,通过使用这些 API 向每个厂商设备发送这些具体指令来配置设备。应用策略基础设施控制器APIC)组件负责控制和编程每个网络设备组件上的布线。

让我们看看 Cisco APIC 的示例以及一些基本用法。Cisco APIC 用于管理、自动化、监控和编程应用中心基础设施ACI)。ACI 是一组对象,每个对象代表一个租户。租户可以称为基于业务分类的特定客户群、组或业务单元。例如,一个单一的组织可能将其整个基础设施转换为一个单一租户,而一个组织可以根据其功能(如 HR 和财务)将租户分离出来。租户可以进一步细分为上下文,每个上下文作为一个单独的转发平面,因此每个上下文中都可以使用相同的 IP 地址集,因为在每个上下文中,IP 地址集将被不同地处理。

上下文中包含端点EPs)和端点组EPGs)。这些 EPs 是物理组件,如硬件 NICs,而 EPGs 是 DNSs、IP 地址等项目的集合,它们为特定应用程序(如 Web 应用程序)定义了类似的功能。

使用 APIC 进行编程时,所需的主要组件如下:

  • APIC Rest Python AdaptorARYA

这是一个由 Cisco 创建的工具,用于将 APIC 返回的 XML 或 JSON 对象转换为直接 Python 代码。在底层,它利用 COBRA SDK 执行此任务。这可以通过pip install arya在 Python 中安装。

  • ACI SDK

这是一个包含直接调用控制器 API 的 API 的 SDK。我们需要安装acicobra,可以在以下网址找到:www.cisco.com/c/en/us/td/docs/switches/datacenter/aci/apic/sw/1-x/api/python/install/b_Install_Cisco_APIC_Python_SDK_Standalone.html,以便将其调用到 Python 中。

一旦安装了此软件,以下是一些来自 Cisco 的示例,可以在以下网址找到:github.com/CiscoDevNet/python_code_samples_network/blob/master/acitoolkit_show_tenants/aci-show-tenants.py。这可以帮助我们理解创建对象:

#!/usr/bin/env python
"""
Simple application that logs on to the APIC and displays all
of the Tenants. 
Leverages the DevNet Sandbox - APIC Simulator Always On 
    Information at https://developer.cisco.com/site/devnet/sandbox/available-labs/data-center/index.gsp 

Code sample based off the ACI-Toolkit Code sample
https://github.com/datacenter/acitoolkit/blob/master/samples/aci-show-tenants.py 
"""

import sys
import acitoolkit.acitoolkit as ACI

# Credentials and information for the DevNet ACI Simulator Always-On Sandbox
APIC_URL = "https://sandboxapicdc.cisco.com/"
APIC_USER = "admin"
APIC_PASSWORD = "C1sco12345"

def main():
    """
    Main execution routine
    :return: None
    """

    # Login to APIC
    session = ACI.Session(APIC_URL, APIC_USER, APIC_PASSWORD)
    resp = session.login()
    if not resp.ok:
        print('%% Could not login to APIC')
        sys.exit(0)

    # Download all of the tenants
    print("TENANT")
    print("------")
    tenants = ACI.Tenant.get(session)
    for tenant in tenants:
        print(tenant.name)

if __name__ == '__main__':
    main()

在考虑前面的概念时,我们可以增强并确保控制器中的管理节点可以根据应用程序需求进行控制,而不是受硬件限制。这也确保了基础设施现在根据应用程序进行调整,而不是相反,应用程序性能受硬件限制。

网络自动化工具

如前几章所述,我们在自动化网络方面有多种选择。从使用 Netmiko 为任何设备进行基本配置,到使用 Ansible 在网络中的各种设备上部署和创建配置,工程师们有许多选项可以根据不同的需求自动化网络。

Python 在创建自动化场景中被广泛使用,这得益于其对各种厂商和协议的开放社区支持。行业中的几乎每个主要参与者都支持 Python 编程,调整他们自己的工具或任何支持技术。网络自动化的另一个主要方面是为组织需求定制的解决方案。自服务 API 模型是一个很好的起点,确保一些手动完成的任务可以转换为 API,然后可以根据自动化需求利用任何语言。

让我们看看一个可以作为基本指南的示例,以了解自建或自定义自动化工具的优势。在 Cisco 中 show ip bgp summary 的输出与在 Juniper 中 show bgp summary 的输出相同。现在,作为一个需要验证两个厂商 BGP 的工程师,我需要理解这两个命令并解释输出。

想象一下,通过添加更多具有自己独特方式获取 BGP 输出的厂商,这会变得复杂,网络工程师需要接受多厂商环境的培训,以便能够从每个厂商获取相同类型的输出。

现在,假设我们创建一个 API(例如,getbgpstatus),它接受主机名作为输入。后端的 API 足够智能,能够使用 SNMP 获取厂商型号,并根据厂商发送特定的命令(如 Cisco 的 show ip bgp summary 或 Juniper 的 show ip summary),并将该输出解析为人类可读的格式,例如仅包含该 BGP 邻居的 IP 地址和状态。

例如,我们不是打印 show ip bgp summaryshow bgp summary 的原始输出,而是像这样解析输出:

IPaddress1 : Status is UP
IPaddress2 : Status is Down (Active)

此输出可以作为 JSON 值返回到 API 的调用中。

因此,假设我们可以通过 http://localhost/networkdevices/getbgpstatus?device=devicex 调用 API,后端的 API 将识别 devicex 是 Juniper、Cisco 还是其他厂商,并根据这一点,该厂商将获取并解析与该厂商相关的输出。该 API 调用的返回将是类似于前面示例中的 JSON 文本,我们可以在我们的自动化语言中解析它。

让我们看看另一个流行工具 SolarWinds 的基本示例。SolarWinds 有许多方面;它可以自动发现设备(基于 MIBs 和 SNMP),识别厂商,并从设备中获取相关信息。

让我们看看以下截图,了解基本的 SolarWinds 设备管理。SolarWinds 作为试用版免费提供下载。

SolarWinds 设备管理的先决条件如下:

  1. 我们需要在 SolarWinds 中添加一个设备,如下所示:

如我们所见,SolarWinds 有能力发现设备(使用网络发现),或者我们可以添加一个特定的 IP 地址/主机名,并使用正确的 SNMP 字符串,以便 SolarWinds 识别该设备。

  1. 一旦设备被检测到,它将显示为监控节点,如下面的屏幕截图所示:

注意 IP 地址(或主机名)旁边的绿色点。这表示节点是活跃的(可到达的),SolarWinds 可以正确地与节点交互。

在设备发现之后可以执行的其他任务如下:

一旦我们在 SolarWinds 中有了可用的节点或检测到的节点,以下是一些在 SolarWinds 中可以执行的其他任务(如下面的屏幕截图所示):

我们已选择 CONFIGS 菜单,在该菜单下我们可以对设备执行配置管理。此外,如以下屏幕截图所示,我们还有创建小脚本的能力(就像我们在这里对show running config所做的那样),我们可以使用这些脚本在 SolarWinds 本身针对一组特定的设备执行(如下面的屏幕截图所示):

检索到的结果可以存储为文本文件,或者如果配置了,甚至可以发送回任何电子邮件客户端作为报告。同样,有一些任务(在 SolarWinds 中称为作业),可以按计划执行,如下面的屏幕截图所示:

如前一个屏幕截图所示,我们可以从设备下载配置,然后在下一步中选择所有或某些设备并安排作业。这在从以前的日期获取配置或在需要回滚到最后已知良好配置场景时非常有用。此外,有时需要对配置更改进行审计,以了解谁更改了什么以及更改了什么,SolarWinds 可以通过发送报告和警报来扩展这一功能。从编程的角度来看,我们还有额外的能力通过 Python 调用 SolarWinds API 来获取结果。

假设 OrionSDK 已经安装在 Python 中。如果没有,我们可以使用pip install orionsdk来安装它。

考虑以下示例:

from orionsdk import SwisClient
import requests

npm_server = 'myserver'
username = "username"
password = "password"

verify = False
if not verify:
    from requests.packages.urllib3.exceptions import InsecureRequestWarning
    requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

swis = SwisClient(npm_server, username, password)

results = swis.query("SELECT NodeID, DisplayName FROM Orion.Nodes Where Vendor= 'Cisco'")

for row in results['results']:
    print("{NodeID:<5}: {DisplayName}".format(**row))

由于 SolarWinds 支持直接 SQL 查询,我们使用以下查询:

SELECT NodeID, DisplayName FROM Orion.Nodes Where Vendor= 'Cisco'

我们试图获取所有具有思科供应商的设备的NodeIDDisplayName(或设备名称)。一旦我们有了结果,我们将以格式化的方式打印结果。在我们的例子中,输出将是(假设我们的思科设备在 SolarWinds 中已添加为mytestrouter1mytestrouter2):

>>> 
===================== RESTART: C:\a1\checksolarwinds.py =====================
101 : mytestrouter1
102 : mytestrouter2
>>>

使用这些自动化工具和 API 中的某些工具,我们可以确保我们的任务专注于实际工作,一些基本或核心任务(如从设备获取值等)被卸载到工具或 API 中处理。

现在我们从头开始创建一个基本的自动化工具,该工具使用 ping 测试来监控任何作为监控工具一部分的节点的可达性。我们可以将其称为 PingMesh 或 PingMatrix,因为该工具将生成一个基于 Web 的矩阵来显示路由器的可达性。

我们将要使用的拓扑如下:

图片

在这里,我们将使用四个路由器(R1R4),以及Cloud1作为我们的监控源。每个路由器将尝试通过 ping 相互可达,并将结果报告给在Cloud1上运行的脚本,该脚本将解释结果并通过基于 Web 的 URL 显示网络矩阵。

前面拓扑的解释如下:

  1. 我们试图做的是登录到每个路由器(最好是并行登录),从每个源 ping 每个目标,并报告每个目标的可达性状态。

  2. 例如,如果我们想手动执行任务,我们会登录到R1并尝试从源 ping R2R3R4,以检查每个路由器从R1的可达性。Cloud1上的主脚本(充当控制器)将解释结果并相应地更新网络矩阵。

  3. 在我们的案例中,所有路由器(以及控制器)都位于192.168.255.x子网中,因此它们可以通过简单的 ping 相互可达。

我们将创建两个独立的 Python 程序(一个作为调用各种节点上命令、从节点获取结果、解释结果并将解析后的数据发送到主程序的库)。主程序将负责调用库,并使用我们得到的结果创建 HTML 网络矩阵。

让我们先创建主程序中要调用的库或程序(我们称之为getmeshvalues.py):

#!/usr/bin/env python
import re
import sys
import os
import time
from netmiko import ConnectHandler
from threading import Thread
from random import randrange
username="cisco"
password="cisco"

splitlist = lambda lst, sz: [lst[i:i+sz] for i in range(0, len(lst), sz)]

returns = {}
resultoutput={}
devlist=[]
cmdlist=""

def fetchallvalues(sourceip,sourcelist,delay,cmddelay):
    print ("checking for....."+sourceip)
    cmdend=" repeat 10" # this is to ensure that we ping for 10 packets
    splitsublist=splitlist(sourcelist,6) # this is to ensure we open not more than 6 sessions on router at a time
    threads_imagex= []
    for item in splitsublist:
        t = Thread(target=fetchpingvalues, args=(sourceip,item,cmdend,delay,cmddelay,))
        t.start()
        time.sleep(randrange(1,2,1)/20)
        threads_imagex.append(t)

    for t in threads_imagex:
        t.join() 

def fetchpingvalues(devip,destips,cmdend,delay,cmddelay):
    global resultoutput
    ttl="0"
    destip="none"
    command=""
    try:
        output=""
        device = ConnectHandler(device_type='cisco_ios', ip=devip, username=username, password=password, global_delay_factor=cmddelay)
        time.sleep(delay)
        device.clear_buffer()
        for destip in destips:
            command="ping "+destip+" source "+devip+cmdend
            output = device.send_command_timing(command,delay_factor=cmddelay)
            if ("round-trip" in output):
                resultoutput[devip+":"+destip]="True"
            elif ("Success rate is 0 percent" in output):
                resultoutput[devip+":"+destip]="False"
        device.disconnect()
    except:
        print ("Error connecting to ..."+devip)
        for destip in destips:
            resultoutput[devip+":"+destip]="False"

def getallvalues(allips):
    global resultoutput
    threads_imagex= []
    for item in allips:
        #print ("calling "+item)
        t = Thread(target=fetchallvalues, args=(item,allips,2,1,))
        t.start()
        time.sleep(randrange(1,2,1)/30)
        threads_imagex.append(t)
    for t in threads_imagex:
        t.join()
    dnew=sorted(resultoutput.items()) 
    return dnew

#print (getallvalues(["192.168.255.240","192.168.255.245","192.168.255.248","192.168.255.249","4.2.2.2"]))

在前面的代码中,我们创建了三个主要函数,我们在一个线程中调用这些函数(用于并行执行)。getallvalues()包含我们想要获取数据的 IP 地址列表。然后它将此信息传递给fetchallvalues(),带有特定设备信息以并行执行再次获取 ping 值。对于在路由器上执行命令并获取结果,我们调用fetchpingvalues()函数。

让我们看看这个代码的结果(通过删除调用函数的注释)。我们需要传递一个列表,其中包含我们想要验证的设备 IP。在我们的案例中,我们所有的有效路由器都在192.168.255.x范围内,4.2.2.2被用作一个不可达路由器的示例:

print(getallvalues(["192.168.255.240","192.168.255.245","192.168.255.248","192.168.255.249","4.2.2.2"]))

前面的代码给出了以下输出:

图片

如我们在结果中看到的那样,我们从每个节点到其他节点获得了可达性的TrueFalse

例如,列表中的第一个项目('192.168.255.240:192.168.255.240','True')表示从源192.168.255.240到目标192.168.255.240(即相同的自 IP)是可达的。同样,同一列表中的下一个项目('192.168.255.240:192.168.255.245','True')确认从源 IP192.168.255.240到目标192.168.255.245我们通过 ping 有可达性。这些信息是创建基于结果的矩阵所必需的。接下来我们看到主要代码,其中我们获取这些结果并创建一个基于 Web 的矩阵页面。

接下来,我们需要创建主文件(我们将其命名为pingmesh.py):

import getmeshvalue
from getmeshvalue import getallvalues

getdevinformation={}
devicenamemapping={}
arraydeviceglobal=[]
pingmeshvalues={}

arraydeviceglobal=["192.168.255.240","192.168.255.245","192.168.255.248","192.168.255.249","4.2.2.2"]

devicenamemapping['192.168.255.240']="R1"
devicenamemapping['192.168.255.245']="R2"
devicenamemapping['192.168.255.248']="R3"
devicenamemapping['192.168.255.249']="R4"
devicenamemapping['4.2.2.2']="Random"

def getmeshvalues():
        global arraydeviceglobal
        global pingmeshvalues
        arraydeviceglobal=sorted(set(arraydeviceglobal))
        tval=getallvalues(arraydeviceglobal)
        pingmeshvalues = dict(tval)

getmeshvalues()

def createhtml():
    global arraydeviceglobal
    fopen=open("C:\pingmesh\pingmesh.html","w") ### this needs to be changed as web path of the html location

    head="""<html><head><meta http-equiv="refresh" content="60" ></head>"""
    head=head+"""<script type="text/javascript">
function updatetime() {
    var x = new Date(document.lastModified);
    document.getElementById("modified").innerHTML = "Last Modified: "+x+" ";
}
</script>"""+"<body onLoad='updatetime();'>"
    head=head+"<div style='display: inline-block;float: right;font-size: 80%'><h4><h4><p id='modified'></p></div>"
    head=head+"<div style='display: inline-block;float: left;font-size: 90%'></h4><center><h2>Network Health Dashboard<h2></div>"
    head=head+"<br><div><table border='1' align='center'><caption><b>Ping Matrix</b></caption>"
    head=head+"<center><br><br><br><br><br><br><br><br>"
    fopen.write(head)
    dval=""
    fopen.write("<tr><td>Devices</td>")
    for fromdevice in arraydeviceglobal:
        fopen.write("<td><b>"+devicenamemapping[fromdevice]+"</b></td>")
    fopen.write("</tr>")
    for fromdevice in arraydeviceglobal:
        fopen.write("<tr>")
        fopen.write("<td><b>"+devicenamemapping[fromdevice]+"</b></td>")
        for todevice in arraydeviceglobal:
            askvalue=fromdevice+":"+todevice
            if (askvalue in pingmeshvalues):
                getallvalues=pingmeshvalues.get(askvalue)
                bgcolor='lime'
                if (getallvalues == "False"):
                    bgcolor='salmon'
            fopen.write("<td align='center' font size='2' height='2' width='2' bgcolor='"+bgcolor+"'title='"+askvalue+"'>"+"<font color='white'><b>"+getallvalues+"</b></font></td>")
        fopen.write("</tr>\n")
    fopen.write("</table></div>")
    fopen.close()

createhtml()

print("All done!!!!")

在这种情况下,我们已设置以下映射:

devicenamemapping['192.168.255.240']="R1"
devicenamemapping['192.168.255.245']="R2"
devicenamemapping['192.168.255.248']="R3"
devicenamemapping['192.168.255.249']="R4"
devicenamemapping['4.2.2.2']="Random"

最后一个名为Random的设备是一个测试设备,它不在我们的网络中,并且为了测试目的不可达。一旦执行,它将创建一个名为pingmesh.html的文件,该文件使用标准的 HTML 格式和最后刷新时钟(来自 JavaScript),以确认最后刷新发生的时间。如果我们想从任务计划程序(比如说每五分钟)执行脚本,并且任何人打开 HTML 页面都会知道探测发生的时间。HTML 文件需要放置或保存在一个映射到 Web 文件夹的文件夹中,以便可以使用 URLhttp://<服务器>/pingmesh.html访问。

当执行时,这里是 Python 脚本的输出:

图片

当将 HTML 文件放置在 Web 映射的 URL 中并调用时,看起来是这样的:

图片

如我们所见,在 PingMatrix 中存在一整行和一整列红色,这意味着任何路由器到随机路由器以及从随机路由器到任何路由器的连接都不存在。绿色表示所有其他路由器之间的连接都正常。

此外,我们还在每个单元格上配置了工具提示,将鼠标悬停在该特定单元格上也会显示该特定单元格的源和目标 IP 地址映射,如下面的截图所示:

图片

让我们看看另一个截图,其中我们关闭了 R2 以使其不可达:

图片

现在,正如我们所见,R2的整个行和列都是红色,因此 PingMatrix 显示R2现在从其他任何地方都不可达,并且R2也无法在网络中到达其他人。

让我们看看一个最终的例子,在这个例子中,为了测试目的,我们故意使用扩展的 Cisco ACL 阻止R2R4(反之亦然)的 ping 流量,这反过来又报告说R4R2在 PingMatrix 中存在可达性问题:

图片

如前一个屏幕截图所示,随机路由器仍然显示为红色或错误,因为它不在我们的网络中,但现在它在R2R4之间以及R4R2之间显示为红色/错误。这让我们快速了解到,即使有多个路径可以到达每个节点,我们仍然存在两个节点之间的连接问题。

根据前面的示例,我们可以增强这个工具,以便轻松监控和理解任何路由/可达性问题,甚至使用我们对网络中所有连接的整体视图来检测链路断开连接问题。PingMesh/Matrix 可以扩展以检查延迟,甚至检查各个节点之间每个连接中的数据包丢失。此外,使用 syslog 或电子邮件功能(Python 中可用于发送 syslog 消息的特定 Python 库,甚至可以从 Python 代码发送电子邮件),在检测到故障或从 Python 脚本本身观察到高延迟的情况下,还可以生成警报或工单。

这个工具可以轻松地成为任何组织中的中心监控工具,工程师可以根据模式(如绿色或红色,以及如果需要其他颜色代码),对实际问题做出决策,并采取主动行动而不是被动行动,以确保网络的高可靠性和正常运行时间。

摘要

在本章中,我们学习了 SDN 控制器的基本功能、可编程布线以及一些网络自动化工具。我们还了解了如何与云平台协同工作,并通过一个从 Python 管理 AWS 云的实时示例,理解了我们可以如何使用自动化来控制云操作。

我们对控制器的作用有了深入的理解,并通过一些思科控制器的例子,详细介绍了控制器如何在程序/脚本中编程或调用以执行特定任务。我们还了解了某些流行的网络自动化工具的基础知识,例如 SolarWinds,并创建了一个内部基于 Web 的自动化工具,用于监控我们的网络,称为 PingMatrix 或 PingMesh。

posted @ 2025-09-24 13:51  绝不原创的飞龙  阅读(8)  评论(0)    收藏  举报