精通-Python-网络编程第四版-全-
精通 Python 网络编程第四版(全)
原文:
zh.annas-archive.org/md5/f88f06bb979fcbf490f1ef5b9be8c705译者:飞龙
前言
如查尔斯·狄更斯在《双城记》中所写,“这是最好的时代,这是最坏的时代,这是智慧的时代,这是愚蠢的时代。”这些看似矛盾的话语完美地描述了变革和过渡时期的混乱和情绪。我们无疑正在经历一个与网络工程领域快速变化相似的时期。随着软件开发越来越多地融入所有工程堆栈,传统的命令行界面和垂直集成的网络控制方法不再是管理今天网络的最佳方式。
对于网络工程师来说,我们所看到的变革充满了激动人心的机遇,同时也充满挑战,尤其是对于那些需要快速适应并跟上步伐的人来说。这本书的编写旨在通过提供一份实用的指南来帮助网络专业人士缓解过渡,该指南涵盖了如何从传统平台过渡到基于软件驱动和开发实践的平台。
在这本书中,我们选择 Python 作为编程语言来掌握网络工程任务。Python 是一种易于学习的高级编程语言,可以有效补充网络工程师的创造力和解决问题的能力,从而简化日常操作。Python 正成为许多大型网络不可或缺的一部分,通过这本书,我希望与您分享我所学到的经验。
自从这本书的前三版出版以来,我有幸与许多读者进行了有趣而有意义的对话。我对前三版的成功感到谦卑,并认真对待了收到的反馈。在第四版中,我尝试融入许多新的库,用最新的软件和新的硬件平台更新了现有示例,增加了两个新章节,并对几个章节进行了重大修改。我相信这些变化更能反映当今的网络工程环境。
变革时期为技术进步提供了巨大的机遇。这本书中的概念和工具在我的职业生涯中帮助极大,我希望它们也能对您有所帮助。
这本书面向谁
这本书非常适合那些已经管理网络设备组并希望扩展他们使用 Python 和其他工具克服网络挑战知识的 IT 专业人士和运维工程师。建议具备基本的网络和 Python 知识。
这本书涵盖的内容
第一章 TCP/IP 协议套件和 Python 回顾,回顾了今天互联网通信的基本技术,从 OSI 和客户端-服务器模型到 TCP、UDP 和 IP 套件。本章将回顾 Python 语言的基础知识,如类型、运算符、循环、函数和包。
第二章 2,低级网络设备交互,使用实际示例来说明如何使用 Python 在网络上执行命令。它还将讨论在自动化中仅具有 CLI 接口的挑战。本章将在示例中使用 Pexpect、Paramiko、Netmiko 和 Nornir 库。
第三章 3,APIs 和意图驱动型网络,讨论了支持应用程序 编程 接口(APIs)和其他高级交互方法的网络设备。它还说明了允许抽象低级任务同时关注网络工程师意图的工具。本章将包含有关 Cisco NX-API、Meraki、Juniper PyEZ、Arista Pyeapi 和 Vyatta VyOS 的讨论和示例。
第四章 4,Python 自动化框架 - Ansible,讨论了 Ansible 的基础知识。Ansible 是一个基于 Python 的开源自动化框架。该框架在 API 之上再迈出一步,专注于声明性任务意图。在本章中,我们将介绍使用 Ansible 的优点及其高级架构,并查看一些 Ansible 与网络设备的实际示例。
第五章 5,网络工程师的 Docker 容器,探讨了容器并解释了 Docker 是如何成为应用开发的新标准的。在本章中,我们将通过介绍整体概念和使用它构建示例应用程序来介绍 Docker 作为工具。
第六章 6,使用 Python 进行网络安全,介绍了几个 Python 工具来帮助您保护您的网络。它将讨论使用 Scapy 进行安全测试、使用 Ansible 快速实施访问列表以及使用 Python 进行网络取证分析。
第七章 7,使用 Python 进行网络监控 - 第一部分,涵盖了使用各种工具进行网络监控。本章包含了一些使用 SNMP 和 PySNMP 进行查询以获取设备信息的示例。将展示 Matplotlib 和 Pygal 示例来绘图结果。本章将以一个使用 Python 脚本作为输入源的 Cacti 示例结束。
第八章 8,使用 Python 进行网络监控 - 第二部分,涵盖了更多的网络监控工具。本章将从使用 Graphviz 从 LLDP 信息中绘制网络开始。我们将转向使用基于推式的网络监控示例,使用 NetFlow 和其他技术。我们将使用 Python 解码流量包,并使用 ntop 来可视化结果。
第九章 9,使用 Python 构建网络 Web 服务,展示了如何使用 Python Flask Web 框架来创建网络自动化的 API 端点。网络 API 提供了诸如将请求者从网络细节中抽象出来、合并和定制操作以及通过限制可用操作的暴露来提供更好的安全性等好处。
第十章,异步 IO 简介,涵盖了异步 IO,这是 Python 3 的新包,允许我们同时执行任务。我们将涵盖多进程、并行性、线程以及其他概念等主题。我们还将涵盖来自 Scrapli 项目的示例。
第十一章,AWS 云网络,展示了我们如何使用 AWS 构建一个功能强大且具有弹性的虚拟网络。我们将涵盖云服务、VPC 路由表、访问列表、弹性 IP、NAT 网关、Direct Connect 以及其他相关主题。
第十二章,Azure 云网络,涵盖了 Azure 提供的网络服务以及如何使用该服务构建网络服务。我们将讨论 Azure VNet、Express Route 和 VPN、Azure 网络负载均衡器以及其他相关网络服务。
第十三章,使用 Elastic Stack 进行网络数据分析,展示了我们如何使用 Elastic Stack 作为一套紧密集成的工具来帮助我们分析和监控我们的网络。我们将涵盖从安装、配置,到使用 Logstash 和 Beats 导入数据,以及使用 Elasticsearch 搜索数据,再到使用 Kibana 进行可视化的各个方面。
第十四章,与 Git 协作,说明了我们如何利用 Git 进行协作和代码版本控制。本章将使用 Git 进行网络操作的实际示例。
第十五章,使用 GitLab 进行持续集成,使用 GitLab 自动创建操作管道,这可以节省我们的时间并提高可靠性。
第十六章,网络测试驱动开发,解释了如何使用 Python 的unittest和pytest创建简单的测试来验证我们的代码。我们还将看到为我们的网络编写测试的示例,以验证可达性、网络延迟、安全性和网络事务。
要充分利用本书
要充分利用本书,建议您具备一些基本的动手网络操作知识和 Python 知识。大多数章节可以按任何顺序阅读,除了第四章和第五章,它们介绍了本书后面将使用的基础技术。除了本书开头介绍的基本软件和硬件工具外,各章还将介绍与各章相关的工具。
强烈建议您在您的网络实验室中遵循并实践所展示的示例。
下载示例代码文件
该书的代码包托管在 GitHub 上,地址为github.com/PacktPublishing/Mastering-Python-Networking-Fourth-Edition。我们还有其他来自我们丰富图书和视频目录的代码包,可在github.com/PacktPublishing/找到。去看看吧!
下载彩色图片
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:packt.link/D2Ttl。
使用的约定
本书使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。例如:“自动配置还生成了 Telnet 和 SSH 的vty访问权限。”
代码块按照以下方式设置:
# This is a comment
print("hello world")
任何命令行输入或输出都按照以下方式编写:
$ python3
Python 3.10.6 (main, Nov 2 2022, 18:53:38) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。例如:“在下一节中,我们将继续讨论网络监控的 SNMP 主题,但使用一个功能齐全的网络监控系统,称为Cacti。”
警告或重要提示看起来像这样。
小贴士和技巧看起来像这样。
联系我们
我们读者的反馈总是受欢迎的。
如果您想联系作者,请访问members.networkautomation.community/
一般反馈:请通过feedback@packtpub.com发送电子邮件,并在邮件主题中提及本书的标题。如果您对本书的任何方面有疑问,请通过questions@packtpub.com发送电子邮件给我们。
勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您向我们报告。请访问www.packtpub.com/submit-errata,点击提交勘误,并填写表格。
盗版:如果您在互联网上遇到任何形式的我们作品的非法副本,我们将非常感激您提供位置地址或网站名称。请通过copyright@packtpub.com与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为本书做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《精通 Python 网络,第四版》,我们非常乐意听到您的想法!请点击此处直接访问该书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都非常重要,它将帮助我们确保我们提供高质量的内容。
下载本书的免费 PDF 副本
感谢您购买本书!
您喜欢在路上阅读,但无法携带您的印刷书籍到处走吗?
您的电子书购买是否与您选择的设备不兼容?
不要担心,现在,随着每本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。
在任何地方、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。
优惠远不止这些,您还可以获得独家折扣、时事通讯和丰富的免费内容,每天直接发送到您的收件箱
按照以下简单步骤获取这些福利:
- 扫描下面的二维码或访问以下链接

packt.link/free-ebook/9781803234618
-
提交您的购买证明
-
就这样!我们将直接将您的免费 PDF 和其他福利发送到您的邮箱
第一章:TCP/IP 协议集和 Python 的回顾
欢迎来到网络工程的新兴和激动人心的时代!当我 20 年前在世纪之交开始作为网络工程师工作时,这个角色的性质与今天的网络工程师角色截然不同。当时,网络工程师主要拥有特定领域的知识,使用命令行界面来管理和操作本地和广域网络。虽然他们偶尔会跨越学科界限来处理通常与系统管理和发展相关联的任务,但并没有明确期望网络工程师编写代码或理解编程概念。这种情况现在已经不再存在。
在过去的几年里,DevOps 和软件定义网络(SDN)运动等因素,以及其他因素,已经极大地模糊了网络工程师、系统工程师和开发者之间的界限。
你选择这本书的事实表明,你可能已经是一名网络 DevOps 的采用者,或者你可能在考虑探索网络可编程性的道路。也许你像我一样,已经作为网络工程师工作了多年,并想知道围绕 Python 编程语言的炒作是什么。你可能甚至已经精通 Python 编程语言,但想知道它在网络工程领域的应用。
如果你属于以下任何一类,或者只是对网络工程领域的 Python 感兴趣,我相信这本书适合你:

图 1.1:Python 和网络工程的交集
已经有许多优秀的书籍分别深入探讨了网络工程和 Python 这两个主题。我并不打算用这本书重复他们的努力。相反,这本书假设你已经有一些实际操作网络的经验,以及对于网络协议的基本理解。如果你已经熟悉 Python 作为编程语言,这将很有帮助,但你不需要成为专家。我们将在本章后面覆盖一些 Python 基础知识,作为 Python 知识的起点。再次强调,你不需要在 Python 或网络工程方面成为专家就能阅读这本书。这本书旨在建立在网络工程和 Python 的基本基础上,帮助读者学习和实践各种可以使他们的生活更轻松的应用。
在本章中,我们将对一些网络和 Python 概念进行一般性回顾。本章的其余部分应该设定了为了充分利用本书所需的前置知识水平。如果您想复习本章的内容,有许多免费或低成本的资源可以帮助您跟上进度。我推荐免费的 Khan Academy(www.khanacademy.org/)和 Python 软件基金会提供的官方 Python 教程,网址为www.python.org/。
本章将以非常快速的方式对相关网络主题进行高层次回顾,而不会过多涉及细节。在一章中深入探讨任何主题都实在没有足够的空间。说实话,我们在日常工作中很少能达到深入的水平。需要多少网络知识水平?根据我在该领域的经验,典型的网络工程师或开发者可能不会记住用于完成日常任务的精确的传输控制协议(TCP)状态机(我知道我不会),但他们会对开放系统互联(OSI)模型的基本知识、TCP 和用户数据报协议(UDP)操作、不同的 IP 头部字段和其他基本概念熟悉。这正是我们将在本章中涵盖的内容。
我们还将首先对 Python 语言进行高层次回顾;对于那些不每天使用 Python 编码的读者来说,这将为他们提供全书的基础。
具体来说,我们将涵盖以下主题:
-
互联网概述
-
OSI 和客户端-服务器模型
-
TCP、UDP 和 IP 协议族
-
Python 语法、类型、运算符和循环
-
使用函数、类和包扩展 Python
当然,本章中提供的信息并不全面;如果需要更多信息,请查阅参考文献。
作为网络工程师,我们常常面临着需要管理的网络的规模和复杂性的挑战。这些网络从小型的家庭网络,到支持小型企业运营的中型网络,再到覆盖全球的大型跨国企业网络,种类繁多。其中最广泛的网络当然是互联网。没有互联网,就没有我们现在所知道的电子邮件、网站、API、流媒体或云计算。因此,在我们深入探讨协议和 Python 的具体细节之前,让我们先对互联网有一个概述。
互联网概述
互联网是什么?这个看似简单的问题可能会因每个人的背景不同而得到不同的答案。互联网对不同的人意味着不同的事情;年轻人、老年人、学生、教师、商人和诗人可能都会对同一个问题给出不同的答案。
对于网络工程师来说,互联网是一个全球计算机网络,由一个连接大型和小型网络的互联网网络组成。换句话说,它是一个没有集中所有者的网络网络。以你的家庭网络为例。它可能由一个集成了路由、以太网交换和无线接入点功能的设备组成,将你的智能手机、平板电脑、电脑和互联网电视连接在一起,以便这些设备相互通信。这就是你的局域网(LAN)。
当你的家庭网络需要与外界通信时,它将信息从你的 LAN 传递到一个更大的网络,通常被称为互联网服务提供商(ISP)。ISP 通常被认为是一家你付费上网的业务。他们可以通过将小型网络聚合到他们维护的更大网络中来做到这一点。
你的 ISP 网络通常由许多边缘节点组成,这些节点将流量聚合到其核心网络。核心网络的功能是通过高速网络将这些边缘网络相互连接。
在一些称为互联网交换点的更专业化的边缘节点中,你的 ISP 连接到其他 ISP,以便将你的流量适当地传递到目的地。从你的目的地到你的家用电脑、平板电脑或智能手机的返回路径可能或可能不会通过所有这些中间网络返回到你的原始设备,而源和目的地保持不变。这种不对称行为旨在具有容错性,以便没有任何一个节点可以关闭整个连接。
让我们来看看构成这个网络网络的组件。
服务器、主机和网络组件
主机是网络上的终端节点,与其他节点进行通信。在当今世界,一个主机可以是一台传统计算机,也可以是你的智能手机、平板电脑或电视。随着物联网(IoT)的兴起,主机的广泛定义可以扩展到包括互联网协议(IP)摄像头、机顶盒以及我们在农业、农业、汽车等领域使用的不断增多的传感器。随着连接到互联网的主机数量的激增,它们都需要被寻址、路由和管理。对适当网络的需求从未如此之大。
大多数时候,我们在互联网上都是进行服务请求。这可能包括浏览网页、发送或接收电子邮件、传输文件以及其他在线活动。这些服务都是由服务器提供的。正如其名所示,服务器为多个节点提供服务,通常具有更高的硬件规格。从某种意义上说,服务器是网络上的特定“超级节点”,为其他节点提供额外的功能。我们将在客户端-服务器模型部分再次探讨服务器。
如果你把服务器和主机看作城市和城镇,那么网络组件就是连接它们的道路和高速公路。实际上,当我们描述传输全球不断增长的比特和字节的网络组件时,信息高速公路这个词就会浮现在脑海中。在我们将要探讨的OSI 模型的七层 OSI 模型中,这些网络组件是一层到三层设备,有时甚至涉及到四层。它们是二层和三层的路由器和交换机,负责引导流量,以及一层传输设备,如光纤电缆、同轴电缆、绞合铜对,以及一些密集波分复用(DWDM)设备等。
总的来说,主机、服务器、存储和网络组件共同构成了我们今天所知道的互联网。
数据中心的兴起
在上一节中,我们探讨了服务器、主机和网络组件在网络中的作用。由于服务器对硬件容量的要求更高,它们通常被放置在中央位置以更有效地管理。我们通常将这些地点称为数据中心。它们通常可以分为三大类:
-
企业数据中心
-
云数据中心
-
边缘数据中心
让我们先看看企业数据中心。
企业数据中心
在典型的企业中,公司通常有对内部工具的需求,如电子邮件、文档存储、销售跟踪、订购、人力资源工具和知识共享内联网。这些服务成为文件和邮件服务器、数据库服务器和 Web 服务器。与用户计算机不同,这些通常是高端计算机,需要更高的电力、冷却和高带宽网络连接。硬件的副产品还包括它产生的噪音量,这在正常的工作空间中是不合适的。服务器通常放置在企业建筑中的中央位置,称为主配线架(MDF),以提供必要的电源供应、电源冗余、冷却和网络连接。
要连接到 MDF,用户的流量通常在更靠近用户的位置进行聚合,这些位置有时被称为中间配线架(IDF),然后它们被捆绑并连接到 MDF。IDF-MDF 的扩展通常遵循企业建筑或校园的物理布局,这是很常见的。例如,每层楼可以由一个 IDF 组成,它将流量聚合到同一建筑另一层的集中式 MDF。如果企业由几座建筑组成,可以在将它们连接到企业数据中心之前,通过组合建筑流量进行进一步的聚合。
许多企业数据中心(有时被称为校园网络)遵循三层网络设计。这些层级包括接入层、分发层和核心层。当然,与任何设计一样,没有硬性规则或一刀切的模式;三层设计只是一个一般性的指导。以我们之前提到的 User-IDF-MDF 为例,接入层相当于每个用户连接的端口,IDF 可以被视为分发层,而核心层则由连接到 MDF 和企业数据中心的连接组成。这当然是对企业网络的一种概括,因为其中一些不会遵循相同的模式。
云数据中心
随着云计算和软件,或基础设施即服务(IaaS)的兴起,云服务提供商建立的数据中心规模庞大,有时被称为超大规模数据中心。我们所说的云计算是指由亚马逊 AWS、微软 Azure 和谷歌云等提供的按需计算资源可用性,而用户无需直接管理这些资源。许多网络规模的服务提供商,如 Facebook,也可以归入这一类别。
由于他们需要容纳的服务器数量,云数据中心通常需要比任何企业数据中心都要高得多的电力、冷却和网络容量。即使在我为云服务提供商的数据中心工作了多年之后,每次我访问云服务提供商的数据中心,我仍然对其规模感到惊讶。仅举几个例子来说明其规模之大,云数据中心如此之大,能耗如此之高,通常建在靠近发电厂的地方,以便在电力运输过程中损失效率最小化。它们的冷却需求如此之高,以至于一些数据中心不得不在建设地点上发挥创意。例如,Facebook 在瑞典北部的吕勒奥(北极圈以南 70 英里)建立了其数据中心,部分原因是为了利用低温进行冷却。任何搜索引擎都可以提供一些令人惊叹的数字,当涉及到像亚马逊、微软、谷歌和 Facebook 这样的公司建造和管理云数据中心时。例如,位于爱荷华州西得梅因的微软数据中心占地 200 英亩,设施面积为 120 万平方英尺,需要该市花费估计 6500 万美元的公共基础设施升级费用。
在云服务提供商的规模下,他们需要提供的服务通常不划算,也不可能在单个服务器中实现。这些服务分布在多台服务器之间,有时跨越多个机架,为服务所有者提供冗余和灵活性。
延迟和冗余需求,以及服务器的物理分布,给网络带来了巨大的压力。连接服务器编队的互连相当于网络设备(如电缆、交换机和路由器)的爆炸性增长。这些需求转化为需要上架、配置和管理设备的数量。典型的网络设计将是多阶段 Clos 网络:

图 1.2:Clos 网络
从某种意义上说,云数据中心和其他网络中不断增加的适应性使得网络自动化成为必要,因为自动化可以提供速度、灵活性和可靠性。如果我们遵循通过终端和命令行界面管理网络设备的传统方式,所需的工程小时数将不允许在合理的时间内提供服务。更不用说人为的重复是易出错的、低效的,并且是对工程人才的巨大浪费。再加上复杂性,通常需要快速更改一些网络配置以适应快速变化的业务需求,例如将三层校园网络重新设计为基于 CLOS 的拓扑结构。
个人而言,云计算数据中心网络是我几年前开始使用 Python 进行网络自动化的起点,从那时起我就再也没有回头了。
边缘数据中心
如果我们在数据中心级别拥有足够的计算能力,为什么还要在其他地方保留任何东西呢?全球客户的所有连接都可以路由回数据中心服务器,然后我们可以结束一天的工作,对吧?当然,答案取决于用例。将请求和会话从客户端完全路由回大型数据中心的最大限制是传输中引入的延迟。换句话说,显著的延迟就是网络成为瓶颈的地方。
当然,任何一本基础物理教科书都可以告诉你,网络延迟的数字永远不会是零:即使光在真空中传播得再快,物理传输也需要时间。在现实世界中,延迟会比真空中的光还要高。为什么?因为网络数据包必须穿越多个网络,有时是通过海底电缆、缓慢的卫星链路、4G 或 5G 蜂窝链路,或者 Wi-Fi 连接。
我们如何减少网络延迟?一个解决方案是减少终端用户请求穿越的网络数量。我们可以尽量靠近终端用户,也许在请求进入我们网络的边缘与用户会合。我们可以在这些边缘位置放置足够的资源来处理请求。这在服务媒体内容,如音乐和视频时尤其常见。
让我们花一分钟时间想象一下,你正在构建下一代视频流媒体服务。为了提高流畅播放的客户满意度,你希望将视频服务器尽可能放置在客户附近,无论是内部还是客户互联网服务提供商(ISP)附近。此外,为了冗余和连接速度,视频服务器农场的上行链路不仅连接到一两个 ISP,而是连接到我们可以连接的所有 ISP,以减少跳数,从而减少我们需要通过设备的数量。所有连接都将拥有所需的带宽,以降低高峰时段的延迟。这种需求催生了大型 ISP 和内容提供商的互连交换边缘数据中心。即使网络设备的数量不如云数据中心高,它们也能从网络自动化中受益,包括提高可靠性、灵活性、安全性和可见性。
如果我们扩展边缘节点的概念并发挥创意,我们可以看到一些最新的技术,如自动驾驶汽车和软件定义广域网(SD-WANs)也是边缘节点的应用。自动驾驶汽车需要根据其传感器做出瞬间决策。SD-WAN 路由器需要本地路由数据包,而不需要咨询中央“大脑”。这些都是智能边缘节点的概念。
就像许多复杂主题一样,我们可以通过将主题分解成更小的、易于消化的部分来应对复杂性。网络通过使用层来模拟其元素的功能来分解复杂性。多年来,已经出现了不同的网络模型。在本书中,我们将探讨两个最重要的模型,从 OSI 模型开始。
OSI 模型
没有一本网络书籍会不先介绍 OSI 模型就完整。该模型是一个概念模型,将电信功能分解为不同的层。该模型定义了七层,每一层都独立地位于另一层之上,具有定义的结构和特征。
例如,在网络层,IP 位于不同的数据链路层之上,如以太网或帧中继。OSI 参考模型是将不同的和多样化的技术规范化为一套人们可以达成共识的通用语言的好方法。这大大减少了在单个层上工作的各方的工作范围,并允许他们深入查看特定任务,而不必过多担心兼容性:

图 1.3:OSI 模型
OSI 模型最初在 20 世纪 70 年代末开始研究,后来由国际标准化组织(ISO),现在被称为国际电信联盟电信标准化部门(ITU-T)联合发布。它在介绍电信新主题时被广泛接受和引用。
在 OSI 模型开发的同时,互联网也在形成。最初为互联网设计的参考模型通常被称为 TCP/IP 模型。TCP 和 IP 是设计中所包含的原始协议套件。从某种程度上说,这与 OSI 模型相似,因为它们将端到端的数据通信划分为抽象层。
TCP/IP 模型的不同之处在于,它将 OSI 模型中的第 5 层到第 7 层合并到应用层,而物理层和数据链路层则合并到链路层:

图 1.4:互联网协议套件
OSI 和 TCP/IP 模型都适用于提供端到端数据通信的标准。在需要时,我们将参考 OSI 或 TCP/IP 模型,例如在接下来章节讨论网络框架时。就像传输层的模型一样,在应用层也有指导通信的参考模型。在现代网络中,客户端-服务器模型是大多数应用程序的基础。我们将在下一节中探讨客户端-服务器模型。
客户端-服务器模型
客户端-服务器参考模型演示了数据在两个节点之间通信的标准方式。当然,到现在我们都知道,并不是所有的节点都是平等的。即使在最早的高级研究计划署网络(ARPANET)时期,也存在工作站节点和提供内容给其他工作站节点的服务器节点。
这些服务器节点通常具有更高的硬件规格,并由工程师更紧密地管理。由于这些节点为其他节点提供资源和服务,因此它们被适当地称为服务器。服务器通常处于空闲状态,等待客户端发起对其资源的请求。这种由客户端请求请求资源的分布式资源模型被称为客户端-服务器模型。
这为什么很重要?如果你稍微思考一下,这种客户端-服务器模型极大地突出了网络的重要性。如果没有在客户端和服务器之间传输服务的需要,那么对网络互连的需求就不大。正是从客户端向服务器传输比特和字节的需要,凸显了网络工程的重要性。当然,我们所有人都清楚,最大的网络——互联网——是如何改变我们所有人的生活的,并且仍在继续这样做。
你可能会问,每个节点如何在每次需要互相交流时确定时间、速度、源和目的地?这引出了网络协议的概念。
网络协议集
在计算机网络发展的早期,协议是专有的,并且由设计连接方法的公司严格控制。如果你在你的主机上使用 Novell 的 IPX/SPX 协议,那么相同的宿主机将无法与 Apple 的 AppleTalk 宿主机通信,反之亦然。这些专有协议集通常具有与 OSI 参考模型类似的层,并遵循客户端-服务器通信方法,但它们之间不兼容。专有协议通常只在封闭的局域网中工作,无需与外界通信。当流量需要超出本地局域网时,通常使用协议转换设备,如路由器,将一个协议转换为另一个协议。例如,要将基于 AppleTalk 的网络连接到互联网,就需要使用路由器将 AppleTalk 协议转换为基于 IP 的网络。这种额外的转换通常并不完美,但由于在早期,大部分通信都在局域网内进行,因此网络管理员接受了这一点。
然而,随着网络间通信需求超出局域网,对标准化网络协议集的需求变得更加迫切。专有协议最终让位于 TCP、UDP 和 IP 的标准化协议集,这大大增强了网络之间通信的能力。互联网,最大的网络,依赖于这些协议的正常运行。在接下来的几节中,我们将查看每个协议集。
传输控制协议
TCP 是今天互联网上使用的主要协议之一。如果你打开了一个网页或者发送了一封电子邮件,你就已经遇到了 TCP 协议。该协议位于 OSI 模型的第 4 层,负责以可靠和错误检查的方式在两个节点之间传递数据段。TCP 包含一个 160 位的头部,其中包含源和目的端口、序列号、确认号、控制标志和校验和:

图 1.5:TCP 头部
TCP 的功能和特性
TCP 使用数据报套接字或端口来建立主机间的通信。被称为互联网分配数字权威机构(IANA)的标准机构指定了知名端口来指示某些服务,例如端口 80用于 HTTP(网页)和端口 25用于 SMTP(邮件)。在客户端-服务器模型中,服务器通常监听这些知名端口之一,以便接收来自客户端的通信请求。TCP 连接由操作系统管理,套接字代表连接的本地端点。
协议操作由一个状态机组成,该机器在通信会话期间监听传入连接时需要跟踪,以及在连接关闭后释放资源。每个 TCP 连接都会经过一系列状态,如Listen、SYN-SENT、SYN-RECEIVED、ESTABLISHED、FIN-WAIT、CLOSE-WAIT、CLOSING、LAST-ACK、TIME-WAIT和CLOSED。不同的状态有助于管理 TCP 消息。
TCP 消息和数据传输
TCP 和 UDP 之间最大的区别,即它们在同一层上的近亲,是它以有序和可靠的方式传输数据。TCP 操作保证交付的事实通常被称为 TCP 是一个面向连接的协议。它是通过首先建立一个三次握手来同步发送者和接收者之间的序列号,即SYN、SYN-ACK和ACK来实现的。
承认(acknowledgment)用于跟踪对话中的后续部分。最后,在对话结束时,一方将发送一个FIN消息,而另一方将确认接收到的FIN消息,并也发送一个自己的FIN消息。然后,FIN发起者将确认它接收到的FIN消息。
正如许多调试过 TCP 连接的人可以告诉你的,操作可能会变得相当复杂。人们当然可以欣赏到,大多数时候,操作只是在后台默默地发生。
关于 TCP 协议,可以写一本书;事实上,已经有许多关于该协议的优秀书籍被撰写。
由于本节是一个快速概述,如果感兴趣,TCP/IP 指南 (www.tcpipguide.com) 是一个优秀的免费资源,你可以用它来深入了解该主题。
用户数据报协议
UDP 也是所使用的协议套件的核心成员之一。与 TCP 一样,它运行在 OSI 模型的第 4 层,负责在应用层和 IP 层之间传递数据段。与 TCP 不同,其头部只有 64 位,仅包含源端口、目的端口、长度和校验和。轻量级的头部使其非常适合那些偏好快速数据传输而不需要在两个主机之间建立会话或需要可靠数据传输的应用程序。或许在今天快速互联网连接的背景下难以想象,但轻量级的头部在X.21和帧中继链路的早期传输速度上起到了重要作用。
除了速度差异外,不需要维护各种状态,如 TCP,这也节省了两个端点上的计算机资源:

图 1.6:UDP 头部
你现在可能想知道为什么在现代社会 UDP 还被使用;鉴于缺乏可靠传输,我们难道不希望所有连接都是可靠且无错误的吗?如果你考虑多媒体视频流或 Skype 通话,这些应用程序在应用程序只想尽可能快地传递数据报时,会从轻量级头部中受益。你也可以考虑基于 UDP 协议的快速域名系统(DNS)查找过程。在准确性和延迟之间的权衡通常倾向于低延迟。
当你在浏览器中输入的地址被转换为计算机可理解的地址时,用户将受益于一个轻量级的过程,因为这在将信息的第一位传递给你之前必须发生。
再次强调,本节并没有公正地处理 UDP 的主题,如果你对学习更多关于 UDP 感兴趣,鼓励你通过各种资源来探索这个主题。
关于 UDP 的维基百科文章en.wikipedia.org/wiki/User_Datagram_Protocol是了解 UDP 的一个好起点。
互联网协议
正如网络工程师们会告诉你的,我们生活在 IP 层,这是 OSI 模型的第 3 层。IP 负责在端节点之间进行寻址和路由,以及其他任务。IP 的寻址可能是其最重要的任务。地址空间被分为两部分:网络部分和主机部分。子网掩码用于通过将网络部分与 1 匹配,将主机部分与 0 匹配,来指示网络地址中的哪一部分由网络组成,哪一部分由主机组成。IPv4 使用点分表示法来表示地址,例如,192.168.0.1。
子网掩码可以是点分表示法(255.255.255.0)或使用正斜杠来表示应考虑的网络位数(255.255.255.0或/24):

图 1.7:IPv4 头部
IPv6 报头,IPv4 IP 报头的下一代,具有固定部分和各种扩展报头:

图 1.8:IPv6 报头
在固定报头部分,IPv6 的下一报头字段可以指示一个后续的扩展报头,该扩展报头携带附加信息。它还可以标识上层协议,如 TCP 和 UDP。扩展报头可以包括路由和分片信息。例如,扩展报头可以包括原始数据包是如何分片的,以便目标节点可以相应地重新组装数据包。尽管协议设计者希望从 IPv4 迁移到 IPv6,但今天的互联网仍然主要使用 IPv4 进行寻址,其中一些服务提供商网络使用 IPv6 进行原生寻址。
IP 网络地址转换(NAT)和网络安全
NAT 通常用于将一系列私有 IPv4 地址转换为公网可路由的 IPv4 地址。但它也可以指 IPv4 和 IPv6 之间的转换,例如在运营商边缘,当他们在需要转换为 IPv4 的网络内部使用 IPv6 时。有时,出于安全原因,也会使用 NAT6 到 IPv6。
安全是一个持续的过程,它整合了网络的所有方面,包括自动化和 Python。本书旨在使用 Python 帮助你管理网络;安全将在本书的后续章节中解决,例如使用 Python 实现访问列表、在日志中搜索漏洞等。我们还将探讨如何使用 Python 和其他工具在网络中获得可见性,例如基于网络设备信息的动态图形网络拓扑。
IP 路由概念
IP 路由是关于两个端点之间的中间设备根据 IP 报头传输数据包的过程。对于在互联网上发生的所有通信,数据包将穿越各种中间设备。如前所述,中间设备包括路由器、交换机、光设备以及可能不检查网络和传输层以外的各种设备。在公路旅行类比中,你可能会从加利福尼亚州的圣地亚哥市到华盛顿州的西雅图市。IP 源地址相当于圣地亚哥,目标 IP 地址可以被认为是西雅图。在你的公路旅行中,你会在许多不同的中间地点停留,例如洛杉矶、旧金山和波特兰;这些可以被认为是源和目标之间的中间路由器和交换机。
为什么这很重要?从某种意义上说,这本书是关于管理和优化这些中间设备。在拥有多个美式足球场大小的超大型数据中心时代,需要高效、敏捷、可靠且成本效益高的网络管理方式,这对公司来说成为了一个主要的竞争优势。在未来的章节中,我们将深入探讨如何使用 Python 编程有效地管理网络。
现在我们已经了解了网络参考模型和协议套件,我们准备深入探讨 Python 语言本身。在本章中,我们将从 Python 的广泛概述开始。
Python 语言概述
简而言之,这本书是关于如何使用 Python 使我们的网络工程生活变得更轻松。但 Python 是什么,为什么它是许多 DevOps 工程师的首选语言呢?用 Python 基金会执行摘要中的话来说(www.python.org/doc/essays/blurb/):
“Python 是一种解释型、面向对象、高级编程语言,具有动态语义。它的高级内置数据结构,结合动态类型和动态绑定,使其非常适合快速应用开发,同时也适用于作为脚本或粘合语言将现有组件连接起来。Python 简单、易于学习的语法强调可读性,因此降低了程序维护的成本。”
如果你对编程有些陌生,总结中提到的“面向对象”和“动态语义”可能对你来说意义不大。但我想我们都可以同意“快速应用开发”和“简单、易于学习的语法”听起来不错。作为解释型语言,Python 意味着在执行前几乎不需要编译过程,因此编写、测试和编辑 Python 程序所需的时间显著减少。对于简单的脚本,如果你的脚本失败,一个打印语句可能就是你需要来调试问题的所有。
使用解释器还意味着 Python 可以轻松地移植到不同的操作系统,如 Windows 和 Linux。在一个操作系统上编写的 Python 程序可以在另一个操作系统上使用,几乎不需要任何修改。
函数、模块和包通过将大型程序分解成简单的可重用部分来鼓励代码重用。Python 的面向对象特性使得将组件分组为对象更进一步。实际上,所有 Python 文件都是模块,可以被重用或导入到另一个 Python 程序中。这使得工程师之间共享程序变得容易,并鼓励代码重用。Python 还有一个“内置电池”的口号,这意味着对于日常任务,你不需要在 Python 语言本身之外下载任何额外的包。为了在不使代码过于臃肿的情况下实现这一目标,当安装 Python 解释器时,会安装一组 Python 模块,即标准库。对于诸如正则表达式、数学函数和 JSON 解码等常见任务,你所需要的就是导入语句,解释器会将这些函数移动到你的程序中。这个“内置电池”的口号是我认为 Python 语言的一个杀手级特性。
最后,Python 代码可以从一个相对较小的脚本开始,只有几行代码,并逐渐发展成为一个完整的生产系统,这对网络工程师来说非常方便。正如我们许多人所知,网络通常是自然生长的,没有明确的总体规划。一种能够随着你的网络一起发展的语言是无价的。你可能会惊讶地发现,许多被认为是脚本语言的编程语言,却被许多尖端公司的完整生产系统所使用(使用 Python 的组织:wiki.python.org/moin/OrganizationsUsingPython)。
如果你曾经在一个必须在不同供应商平台之间切换的环境中工作过,比如在 Cisco IOS 和 Juniper Junos 之间切换,你就知道在尝试完成相同任务时,在语法和用法之间切换是多么痛苦。由于 Python 足够灵活,既可以用于小型程序也可以用于大型程序,因此不存在这样的剧烈上下文切换。它只是从小到大的相同 Python 代码!
在本章的剩余部分,我们将对 Python 语言进行一次高级的巡礼。如果你已经熟悉基础知识,可以快速浏览或跳到第二章。
Python 版本
如许多读者可能已经知道的那样,Python 在过去几年中一直在经历从 Python 2 到 Python 3 的过渡。Python 3 于 2008 年发布,距今已有 10 多年,持续开发,最新的版本是 3.10。不幸的是,Python 3 与 Python 2 不向后兼容。
在撰写本书第四版的中期,即 2022 年中,Python 社区几乎全部转向了 Python 3。事实上,Python 2 正式于 2020 年 1 月 1 日进入生命周期的终结。最新的 Python 2.x 版本,2.7,是在 2010 年中旬发布的,已有六年多。由于 Python 2 已进入生命周期的终结,并且不再由 Python 软件基金会维护,因此我们都应该使用 Python 3。在本书中,我们将使用最新的稳定版 Python 3,Python 3.10。Python 3.10 有许多令人兴奋的功能,例如稳定的异步 I/O,这对于网络自动化非常有帮助。除非另有说明,本书将使用 Python 3 进行代码示例。当适用时,我们将指出 Python 2 和 Python 3 的差异。
操作系统
如前所述,Python 是跨平台的。Python 程序可以在 Windows、Mac 和 Linux 上运行。实际上,当你需要确保跨平台兼容性时,需要特别注意,例如处理 Windows 文件名中反斜杠的细微差别以及在不同操作系统上激活 Python 虚拟环境的步骤。由于本书是为 DevOps、系统和网络工程师编写的,Linux 是目标受众的首选平台,尤其是在生产环境中。
本书中的代码将在 Linux Ubuntu 22.04 LTS 机器上测试。截至本书编写时,Python 3.10.4 是 22.04 默认版本,因此我们不需要单独安装 Python 3。我还会尽力确保代码在 Windows 和 macOS 平台上运行一致。
如果你感兴趣操作系统细节,如下所示:
$ uname -a
Linux network-dev-4 5.15.0-39-generic #42-Ubuntu SMP Thu Jun 9 23:42:32 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04 LTS
Release: 22.04
Codename: jammy
运行 Python 程序
Python 程序由解释器执行,这意味着代码将通过这个解释器传递给底层操作系统执行。Python 开发社区实现了几种不同的解释器,例如 IronPython 和 Jython。在本书中,我们将使用今天最常用的 Python 解释器,CPython。除非另有说明,否则本书中提到的 Python 都是指 CPython。
你可以使用 Python 的一个方法是利用交互式提示符。当你想要快速测试一段 Python 代码或概念而不需要编写整个程序时,这非常有用。
这通常是通过简单地输入 Python3 关键字来完成的:
$ python3
Python 3.10.4 (main, Apr 2 2022, 09:04:19) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print("hello world")
hello world
交互式模式是 Python 最有用的功能之一。在交互式外壳中,你可以输入任何有效的语句或语句序列,并立即得到结果。我通常使用交互式外壳来探索我不熟悉的特性或库。
交互式模式也可以用于更复杂的任务,例如实验数据结构的行为,例如可变与不可变数据类型。谈谈即时的满足感!
在 Windows 上,如果你没有收到 Python 壳提示,可能是因为你的系统搜索路径中没有该程序。最新的 Windows Python 安装程序提供了一个复选框,用于将 Python 添加到系统路径;确保在安装过程中选中它。或者,你可以通过转到 环境设置 来手动将程序添加到路径中。
然而,运行 Python 程序的一个更常见的方式是保存你的 Python 文件,然后在解释器之后运行它。这将避免你重复输入相同的语句。Python 文件只是普通的文本文件,通常以 .py 扩展名保存。在 *Nix 世界中,你还可以在顶部添加 shebang(#!)行来指定将用于运行文件的解释器。# 字符可以用来指定不会被解释器执行的注释。helloworld.py 文件有以下语句:
# This is a comment
print("hello world")
这可以按以下方式执行:
$ python helloworld.py
hello world
让我们看看基本的 Python 构建结构,内置数据类型。
Python 内置类型
在计算机编程中,数据类型通常指的是计算机程序知道变量可以有什么种类的值的方式,例如单词或数字。Python 实现了动态类型或鸭子类型,并试图在您自动声明对象时确定其类型。Python 有几个标准类型是内置到解释器中的:
-
数值:
int、float、complex和bool(具有True或False值的int子类) -
序列:
str、list、tuple和range -
映射:
dict -
集合:
set和frozenset -
None: 空对象
我们将简要检查 Python 中的不同类型。如果它们此刻对你来说没有意义,那么在接下来的示例部分应用它们时可能会更有意义。
None 类型
None 类型表示没有值的对象。None 类型在那些没有明确返回任何内容(如函数)但只是进行一些数学计算并退出的函数中返回。None 类型也用于函数参数中,如果调用者没有传入实际值,则会出错。例如,我们可以在函数中指定“如果 a==None,则引发错误”。
数值
Python 的数值对象基本上是数字。除了布尔值外,int、long、float 和 complex 的数值类型都是带符号的,这意味着它们可以是正数或负数。布尔值是整数的子类,可以是两个值之一:1 表示 True,0 表示 False。在实践中,我们几乎总是用 True 或 False 而不是数值 1 和 0 来测试布尔值。其余的数值类型通过它们可以精确表示数字的程度来区分;在 Python 3 中,int 没有最大大小,而在 Python 2 中,int 表示有限范围的整数。浮点数是使用机器上的双精度表示(64 位)的数字。
序列
序列是有序的对象集合,具有非负整数的索引。由于 Python 中有许多不同的序列类型(str、列表、元组等),让我们使用交互式解释器来展示不同的序列类型。
请随意在自己的电脑上输入。
有时,人们可能会惊讶地发现字符串(想想单词)是一种序列类型。但如果你仔细观察,字符串是由一系列字符组成的。字符串由单引号、双引号或三引号包围。
注意在以下示例中,引号必须匹配。一个开始的双引号需要与一个结束的双引号相匹配。三引号允许字符串跨越不同的行:
>>> a = "networking is fun"
>>> b = 'DevOps is fun too'
>>> c = """what about coding?
... super fun!"""
>>>
另外两种常用的序列类型是列表和元组。列表是由括号包围的对象序列。就像字符串一样,列表通过非零整数索引,从 0 开始。
列表的值通过引用索引号来检索:
>>> vendors = ["Cisco", "Arista", "Juniper"]
>>> vendors[0]
'Cisco'
>>> vendors[1]
'Arista'
>>> vendors[2]
'Juniper'
元组就像列表,通过括号包围值创建。就像列表一样,元组中的值通过引用其索引号来检索。与列表不同,创建后值不能修改:
>>> datacenters = ("SJC1", "LAX1", "SFO1")
>>> datacenters[0]
'SJC1'
>>> datacenters[1]
'LAX1'
>>> datacenters[2]
'SFO1'
一些操作对所有序列类型都是通用的,例如通过索引返回一个元素。序列还可以通过其元素的一部分进行切片:
>>> a
'networking is fun'
>>> a[1]
'e'
>>> vendors
['Cisco', 'Arista', 'Juniper']
>>> vendors[1]
'Arista'
>>> datacenters
('SJC1', 'LAX1', 'SFO1')
>>> datacenters[1]
'LAX1'
>>>
>>> a[0:2]
'ne'
>>> vendors[0:2]
['Cisco', 'Arista']
>>> datacenters[0:2]
('SJC1', 'LAX1')
>>>
记住,索引从 0 开始。因此,索引 1 是序列中的第二个元素。
也有一些可以应用于序列类型的通用函数,例如检查元素数量和查找所有元素中的最小值和最大值:
>>> len(a)
17
>>> len(vendors)
3
>>> len(datacenters)
3
>>>
>>> b = [1, 2, 3, 4, 5]
>>> min(b)
1
>>> max(b)
5
有一些方法只适用于字符串。值得注意的是,这些方法不会修改底层字符串数据本身,总是返回一个新的字符串。简而言之,可变对象(如列表和字典)在创建后可以更改,而不可变对象(例如字符串)则不能。如果你想将新返回的值用于其他操作,你需要捕获返回值并将其分配给不同的变量:
>>> a
'networking is fun'
>>> a.capitalize()
'Networking is fun'
>>> a.upper()
'NETWORKING IS FUN'
>>> a
'networking is fun'
>>> b = a.upper()
>>> b
'NETWORKING IS FUN'
>>> a.split()
['networking', 'is', 'fun']
>>> a
'networking is fun'
>>> b = a.split()
>>> b
['networking', 'is', 'fun']
>>>
这里有一些列表的常用方法。Python 列表数据类型在将多个项目组合在一起并逐个迭代它们方面是一个非常有用的结构。例如,我们可以创建一个数据中心机架交换机的列表,并通过逐个迭代它们将相同的访问列表应用于所有这些交换机。由于列表的值在创建后可以修改(与元组不同),我们还可以在程序执行过程中扩展和收缩现有的列表:
>>> routers = ['r1', 'r2', 'r3', 'r4', 'r5']
>>> routers.append('r6')
>>> routers
['r1', 'r2', 'r3', 'r4', 'r5', 'r6']
>>> routers.insert(2, 'r100')
>>> routers
['r1', 'r2', 'r100', 'r3', 'r4', 'r5', 'r6']
>>> routers.pop(1)
'r2'
>>> routers
['r1', 'r100', 'r3', 'r4', 'r5', 'r6']
Python 列表非常适合存储数据,但如果我们需要按位置引用数据时,有时跟踪数据会有些棘手。如果这是一个问题,我们可以使用不同的 Python 数据类型。让我们看看 Python 的映射类型。
映射
Python 提供了一种映射类型,称为字典。字典数据类型在我看来就像是一个穷人的数据库,因为它包含可以通过键索引的对象。在其他编程语言中,这通常被称为关联数组或散列表。如果你在其他语言中使用过任何类似字典的对象,你就会知道这种类型有多么强大,因为你可以用人类可读的键来引用对象。
这个键,不仅仅是一个数字,对于试图维护和调试代码的可怜家伙来说,会更有意义。那个人可能就是你,在写完代码几个月后,凌晨 2 点试图调试代码的时候。
字典值中的对象也可以是其他数据类型,例如列表。因为我们用方括号表示列表,用圆括号表示元组,所以我们用花括号来创建字典。以下是一个示例,说明我们如何使用字典来表示我们的数据中心设备:
>>> datacenter1 = {'spines': ['r1', 'r2', 'r3', 'r4']}
>>> datacenter1['leafs'] = ['l1', 'l2', 'l3', 'l4']
>>> datacenter1
{'leafs': ['l1', 'l2', 'l3', 'l4'], 'spines': ['r1',
'r2', 'r3', 'r4']}
>>> datacenter1['spines']
['r1', 'r2', 'r3', 'r4']
>>> datacenter1['leafs']
['l1', 'l2', 'l3', 'l4']
Python 字典是我最喜欢的网络脚本中的数据容器之一,我一直在使用它。然而,在其他不同的用例中,还有其他数据容器可能会派上用场——集合就是其中之一。
集合
集合用于包含无序的对象集合。与列表和元组不同,集合是无序的,不能按数字索引。但有一个特性使集合显得非常有用:集合的元素永远不会重复。想象一下,你有一个需要放入访问列表的 IP 列表。这个 IP 列表的唯一问题是它们充满了重复项。
现在,想想你需要多少行代码来遍历 IP 列表,逐个排序出唯一的项目。现在考虑这一点:内置的集合类型只需一行代码就可以消除重复项。就我个人而言,Python 的集合数据类型在我的代码中并不常用,但当我需要它时,我总是非常感激它的存在。一旦创建了集合或集合,可以使用union、intersection和differences来相互比较:
>>> a = "hello"
# Use the built-in function set() to convert the string to a set
>>> set(a)
{'h', 'l', 'o', 'e'}
>>> b = set([1, 1, 2, 2, 3, 3, 4, 4])
>>> b
{1, 2, 3, 4}
>>> b.add(5)
>>> b
{1, 2, 3, 4, 5}
>>> b.update(['a', 'a', 'b', 'b'])
>>> b
{1, 2, 3, 4, 5, 'b', 'a'}
>>> a = set([1, 2, 3, 4, 5])
>>> b = set([4, 5, 6, 7, 8])
>>> a.intersection(b)
{4, 5}
>>> a.union(b)
{1, 2, 3, 4, 5, 6, 7, 8}
>>> 1 *
{1, 2, 3}
现在我们已经了解了不同的数据类型,接下来我们将游览 Python 运算符。
Python 运算符
Python 有一些你从任何编程语言中都会期望的数值运算符,例如+、-等等;注意,截断除法(//,也称为地板除法)将结果截断为整数和浮点数,但只返回整数值。取模运算符(%)返回除法中的余数值:
>>> 1 + 2
3
>>> 2 - 1
1
>>> 1 * 5
5
>>> 5 / 1 #returns float
5.0
>>> 5 // 2 # // floor division
2
>>> 5 % 2 # modulo operator
1
还有比较运算符。注意,双等号用于比较,而单等号用于变量赋值:
>>> a = 1
>>> b = 2
>>> a == b
False
>>> a > b
False
>>> a < b
True
>>> a <= b
True
我们还可以使用两个常见的成员运算符来测试一个对象是否在序列类型中:
>>> a = 'hello world'
>>> 'h' in a
True
>>> 'z' in a
False
>>> 'h' not in a
False
>>> 'z' not in a
True
Python 操作符允许我们高效地执行简单操作。在下一节中,我们将探讨如何使用控制流来重复这些操作。
Python 控制流工具
if、else和elif语句控制条件代码的执行。与一些其他编程语言不同,Python 使用缩进来结构化代码块。只要它们对齐,缩进空间可以有任意数量。常见的做法通常是使用 2 或 4 个空格。条件语句的格式如下:
if expression:
do something
elif expression:
do something if the expression meets
elif expression:
do something if the expression meets
...
else:
statement
这里有一个简单的示例:
>>> a = 10
>>> if a > 1:
... print("a is larger than 1")
... elif a < 1:
... print("a is smaller than 1")
... else:
... print("a is equal to 1")
...
a is larger than 1
>>>
while循环将继续执行,直到条件为False,所以如果你不希望继续执行(并导致进程崩溃),请小心使用:
while expression:
do something
>>> a = 10
>>> b = 1
>>> while b < a:
... print(b)
... b += 1
...
1
2
3
4
5
6
7
8
9
for循环可以与任何支持迭代的对象一起使用;这意味着所有内置的序列类型,如lists、tuples和strings,都可以在for循环中使用。以下for循环中的字母i是一个迭代变量,所以你通常可以选择在代码上下文中有意义的东西:
for i in sequence:
do something
>>> a = [100, 200, 300, 400]
>>> for number in a:
... print(number)
...
100
200
300
400
现在我们已经了解了 Python 数据类型、操作符和控制流,我们准备将它们组合成可重用的代码块,称为函数。
Python 函数
有时当你发现自己正在复制粘贴一些代码片段时,这可能是一个很好的迹象,表明你应该将其拆分成自包含的函数块。这种做法有助于提高模块化程度,更容易维护,并允许代码重用。Python 函数通过使用def关键字和函数名来定义,后跟函数参数。函数体由要执行的 Python 语句组成。在函数的末尾,你可以选择返回一个值给函数调用者。默认情况下,如果没有指定返回值,它将返回None对象:
def name(parameter1, parameter2):
statements
return value
在接下来的章节中,我们将看到更多关于函数的示例,但这里有一个快速示例。在以下示例中,我们使用位置参数,因此第一个元素通过函数中的第一个变量来引用。另一种引用参数的方式是使用具有默认值的键控参数,例如def subtract(a=10, b=5):
>>> def subtract(a, b):
... c = a - b
... return c
...
>>> result = subtract(10, 5)
>>> result
5
Python 函数非常适合将任务分组在一起。我们能否将不同的函数组合成更大的可重用代码块?是的,我们可以通过 Python 类来实现这一点。
Python 类
Python 是一种面向对象编程(OOP)语言。Python 创建对象的方式是使用class关键字。Python 对象最常见的是函数(方法)、变量和属性(属性)的集合。一旦定义了一个类,就可以创建该类的实例。类作为后续实例的蓝图。
面向对象编程(OOP)的主题超出了本章的范围,但这里有一个简单的router对象定义示例,用以说明这一点:
>>> class router(object):
... def __init__(self, name, interface_number, vendor):
... self.name = name
... self.interface_number = interface_number
... self.vendor = vendor
...
>>>
一旦定义,我们可以创建任意数量的该类实例:
>>> r1 = router("SFO1-R1", 64, "Cisco")
>>> r1.name
'SFO1-R1'
>>> r1.interface_number
64
>>> r1.vendor
'Cisco'
>>>
>>> r2 = router("LAX-R2", 32, "Juniper")
>>> r2.name
'LAX-R2'
>>> r2.interface_number
32
>>> r2.vendor
'Juniper'
>>>
当然,Python 对象和面向对象编程(OOP)还有很多内容。我们将在未来的章节中查看更多示例。
Python 模块和包
任何 Python 源文件都可以用作模块,并且你在该源文件中定义的任何函数和类都可以被其他 Python 脚本重用。要加载代码,引用模块的文件需要使用 import 关键字。当文件被导入时,会发生三件事情:
-
该文件为源文件中定义的对象创建了一个新的命名空间。
-
调用者执行模块中包含的所有代码。
-
该文件在调用者中创建了一个名称,该名称引用了被导入的模块。该名称与模块的名称匹配。
记得我们使用交互式 shell 定义的 subtract() 函数吗?为了重用该函数,我们可以将其放入一个名为 subtract.py 的文件中:
def subtract(a, b):
c = a - b
return c
在与 subtract.py 文件相同的目录中的文件内,你可以启动 Python 解释器并导入此函数:
>>> import subtract
>>> result = subtract.subtract(10, 5)
>>> result
5
这之所以有效,是因为默认情况下,Python 会首先在当前目录中搜索可用的模块。记得我们之前提到的标准库吗?没错,那些只是被用作模块的 Python 文件。
如果你在一个不同的目录中,你可以使用 sys 模块中的 sys.path 手动添加一个搜索路径位置。
我们能否在 Python 中将多个模块组合在一起?是的,Python 包允许将多个模块组合在一起。这进一步组织了 Python 模块,以提供更多的命名空间保护和更好的重用性。包是通过创建一个具有你想要用作命名空间的名称的目录来定义的,然后将模块源文件放置在该目录下。
为了让 Python 识别目录为 Python 包,只需在该目录中创建一个 __init__.py 文件。__init__.py 文件可以是一个空文件。在 subtract.py 文件的相同示例中,假设你要创建一个名为 math_stuff 的目录,我们可以在该目录中创建一个 __init__.py 文件:
$ mkdir math_stuff
$ touch math_stuff/__init__.py
$ tree
.
├── helloworld.py
└── math_stuff
├── __init__.py
└── subtract.py
1 directory, 3 files
$
引用模块的方式是使用点符号包含包名,例如,math_stuff.subtract:
>>> from math_stuff.subtract import subtract
>>> result = subtract(10, 5)
>>> result
5
>>>
如你所见,模块和包是组织大型代码文件和使 Python 代码共享变得更容易的绝佳方式。
摘要
在本章中,我们介绍了 OSI 模型并回顾了网络协议套件,如 TCP、UDP 和 IP。它们作为处理任何两个主机之间寻址和通信协商的层工作。这些协议的设计考虑了可扩展性,并且与它们最初的设计相比几乎没有变化。考虑到互联网的爆炸式增长,这真是一项了不起的成就。
我们还快速回顾了 Python 语言,包括内置类型、运算符、控制流、函数、类、模块和包。Python 是一种强大、生产就绪且易于阅读的语言,这使得它成为网络自动化的理想选择。网络工程师可以利用 Python 从简单的脚本开始,逐步过渡到其他高级功能。
在第二章,低级网络设备交互中,我们将开始探讨如何使用 Python 编程方式与网络设备进行交互。
加入我们的书籍社区
要加入这本书的社区——在这里您可以分享反馈、向作者提问,并了解新书发布——请扫描下面的二维码:
packt.link/networkautomationcommunity

第二章:低级网络设备交互
在第一章中,我们回顾了 TCP/IP 协议套件和 Python,我们探讨了网络通信协议背后的理论和规范。我们还快速浏览了 Python 语言。在本章中,我们将开始深入探讨使用 Python 管理网络设备。特别是,我们将检查我们可以使用 Python 以编程方式与传统的网络路由器和交换机通信的不同方式。
我所说的传统网络路由器和交换机是什么意思?虽然现在很难想象一个没有用于程序化通信的应用程序编程接口(API)的网络设备,但众所周知,许多在几年前部署的网络设备并没有包含 API 接口。这些设备的预期管理方式是通过使用终端程序(这些程序最初是为人类工程师开发的)的命令行界面(CLIs)。管理依赖于工程师对设备返回数据的解释来采取适当的行动。正如可以想象的那样,随着网络设备和网络复杂性的增加,手动逐个管理它们变得越来越困难。
Python 有几个非常棒的库和框架可以帮助完成这些任务,例如 Pexpect、Paramiko、Netmiko、NAPALM 和 Nornir 等。值得注意的是,这些库在代码、依赖项和项目维护者方面存在一些重叠。例如,Netmiko 库是由 Kirk Byers 在 2014 年基于 Paramiko SSH 库创建的。Carl Montanari 创建了 Scrapli 库,以利用最新的 Python 3 asyncio 并发功能。近年来,Kirk、Carl、NAPALM 项目的 David Barroso 以及其他一些人合作创建了令人惊叹的 Nornir 框架,以提供一个纯 Python 网络自动化框架。
大多数情况下,这些库足够灵活,可以一起使用或单独使用。例如,Ansible(在第四章中介绍,Python 自动化框架 – Ansible)使用 Paramiko 和 Ansible-NAPALM 作为其网络模块的底层库。
由于现在存在如此多的库,不可能在合理的页数内涵盖所有这些库。在本章中,我们将首先介绍 Pexpect,然后通过 Paramiko 的示例继续。一旦我们了解了 Paramiko 的基本和操作,就很容易扩展到其他库,如 Netmiko 和 NAPALM。在本章中,我们将探讨以下主题:
-
命令行界面的挑战
-
构建虚拟实验室
-
Python Pexpect 库
-
Python Paramiko 库
-
其他库的示例
-
Pexpect 和 Paramiko 的缺点
我们简要讨论了通过命令行界面管理网络设备的不足。这在管理中等规模的网络中已被证明是无效的。本章将介绍可以与这种限制一起工作的 Python 库。首先,让我们更详细地讨论一些 CLI 的挑战。
命令行的挑战
我在 2000 年代初在 ISP 帮助台开始了我的 IT 职业生涯。我记得看着网络工程师在文本终端中输入看似神秘的命令。就像魔法一样,网络设备就会按照他们的意愿弯曲,并以他们期望的方式表现。随着时间的推移,我学会了接受并拥抱这些我可以输入到终端中的魔法命令。作为网络工程师,这些基于 CLI 的命令就像我们在我们称之为网络工程的世界中相互分享的秘密代码。手动输入命令只是我们为了完成任务而必须做的事情,无伤大雅。
然而,正是在 2014 年左右,我们开始看到业界就明确需要从手动、以人为驱动的 CLIs 转向自动、以计算机为中心的自动化 API 达成共识。不要误解,我们在进行网络设计、提出初步的概念验证和首次部署拓扑结构时,仍然需要直接与设备进行通信。然而,一旦网络部署完成,网络管理的需求现在是一致地、可靠地在所有网络设备上做出相同的更改。
这些更改需要无错误,工程师需要重复步骤而不会被分心或感到疲倦。这种需求听起来像是计算机和我们所喜爱的编程语言 Python 的理想工作。
当然,如果网络设备只能通过命令行进行管理,那么主要挑战就变成了我们如何能够通过计算机程序自动复制路由器和管理员之间的先前手动交互。在命令行中,路由器会输出一系列信息,并期望管理员根据工程师对输出的解释输入一系列手动命令。例如,在 Cisco Internetwork Operating System (IOS) 设备中,你必须输入 enable 进入特权模式,在接收到带有 # 符号的返回提示后,然后输入 configure terminal 以进入配置模式。同样的过程可以进一步扩展到接口配置模式和路由协议配置模式。这与计算机驱动的、程序化的思维方式形成鲜明对比。当计算机想要完成单个任务时,比如,在接口上放置 IP 地址,它希望一次性结构化地给出所有信息给路由器,并期望从路由器那里得到一个单一的 yes 或 no 答案来指示任务的成败。
解决方案,如 Pexpect 和 Paramiko 所实施的,是将交互过程视为子进程,并监控子进程与目标设备之间的交互。根据返回值,父进程将决定后续操作,如果有必要的话。
我相信我们所有人都迫不及待地想要开始使用 Python 库,但首先,我们需要构建我们的网络实验室,以便有一个网络来测试我们的代码。我们将从探讨我们可以构建网络实验室的不同方式开始。
构建虚拟实验室
在我们深入研究 Python 库和框架之前,让我们先探讨一下构建实验室以供学习之用的选项。正如古老的谚语所说,“熟能生巧”——我们需要一个隔离的沙盒来安全地犯错,尝试新的做事方式,并重复一些步骤以强化第一次尝试中不清楚的概念。
要构建一个网络实验室,我们基本上有两种选择:物理设备或虚拟设备。让我们看看各自选项的优缺点。
物理设备
此选项包括使用您可以看到和触摸的物理网络设备来构建实验室。如果您足够幸运,甚至可能构建一个与您的生产环境完全相同的实验室。物理实验室的优缺点如下:
-
优点:从实验室到生产的过渡很容易。拓扑结构对于经理和需要查看和操作设备的工程师来说更容易理解。由于熟悉,对物理设备的舒适度极高。
-
缺点:为仅用于实验室的设备付费相对较贵。此外,物理设备需要工程时间进行安装和堆叠,一旦构建完成,其灵活性就不高了。
虚拟设备
虚拟设备是实际网络设备的仿真或模拟。它们可能由供应商或开源社区提供。虚拟设备的优缺点如下:
-
优点:虚拟设备更容易设置,相对便宜,并且可以快速更改拓扑结构。
-
缺点:它们通常是物理设备的缩小版。有时虚拟设备和物理设备之间存在功能差距。
当然,决定使用虚拟实验室还是物理实验室是一个个人决定,这是在成本、实施难度和实验室与生产环境之间差距的风险之间权衡的结果。在我工作的一些地方,虚拟实验室用于进行初步的概念验证,而物理实验室则用于我们接近最终设计时使用。
在我看来,随着越来越多的厂商决定生产虚拟设备,虚拟实验室是在学习环境中前进的正确方式。虚拟设备的功能差距相对较小,并且有具体的文档记录,尤其是当虚拟实例由厂商提供时。与购买物理设备相比,虚拟设备的花费相对较小。使用虚拟设备构建所需的时间要短得多,因为它们只是软件程序。
对于这本书,我将使用物理设备和虚拟设备的组合来展示概念,更倾向于使用虚拟设备。对于我们将看到的示例,差异应该是透明的。如果虚拟设备和物理设备之间存在任何与我们的目标相关的已知差异,我将确保列出它们。
对于书中的代码示例,我将尝试使网络拓扑尽可能简单,同时仍然能够展示手头的概念。每个虚拟网络通常由不超过几个节点组成,如果可能的话,我们将重复使用相同的虚拟网络进行多个实验室。
对于本书中的示例,我将使用思科建模实验室,www.cisco.com/c/en/us/products/cloud-systems-management/modeling-labs/index.html,以及其他虚拟平台,例如 Arista vEOS。正如我们将在下一节中看到的,思科根据可用性提供付费版本的 CML 和免费托管版本的 CML 在 Cisco DevNet (developer.cisco.com/site/devnet/)。使用 CML 是可选的。您可以使用您拥有的任何实验室设备,但这可能有助于您更好地跟随书中的示例。还值得注意的是,思科对设备镜像有严格的软件许可要求,因此通过购买或使用免费托管的 CML,您不太可能违反他们的软件许可要求。
思科建模实验室
我记得当我第一次开始为我的思科认证网络专家(CCIE)实验室考试做准备时,我从 eBay 上购买了一些二手的思科设备来学习。即使有二手设备的折扣,每个路由器和交换机仍然要花费数百美元。为了省钱,我购买了一些 20 世纪 80 年代的过时思科路由器(在您最喜欢的搜索引擎中搜索 Cisco AGS 路由器,会有很多好笑的内容),它们在实验室标准下功能严重不足。尽管当我打开它们时与家人聊天很有趣(它们真的很响),但组装物理设备并不好玩。它们又重又笨拙,连接所有电缆都很痛苦,为了引入链路故障,我实际上必须拔掉一根电缆。
快进几年。Dynamips 被创建出来,我爱上了创建不同网络场景的简便性。这在尝试学习新概念时尤为重要。我所需要的只是思科的 IOS 镜像和一些精心构建的拓扑文件,我就能轻松构建一个虚拟网络来测试我的知识。我有一个包含网络拓扑、预先保存的配置和不同场景所需的不同版本镜像的整个文件夹。添加 GNS3 前端为整个设置提供了一个漂亮的 GUI 界面升级。使用 GNS3,你可以直接点击并拖动你的链接和设备;你甚至可以直接从 GNS3 设计面板打印出网络拓扑图给你的经理或客户。GNS3 的唯一缺点是工具没有得到思科的官方认可,因此它被认为缺乏可信度。
2015 年,思科社区决定通过发布思科的虚拟互联网路由实验室(VIRL),learningnetwork.cisco.com/s/virl来满足这一需求。这很快成为我在开发、学习和实践网络自动化代码时的首选工具。
在 VIRL 推出几年后,思科发布了Cisco Modeling Labs(CML),developer.cisco.com/modeling-labs/。这是一个功能强大的网络仿真平台,拥有易于使用的 HTML 用户界面和全面的 API。
在撰写本文时,CML 的单用户许可证价格为 199 美元(请注意,在思科 DevNet 上有一个免费的托管版本)。在我看来,CML 平台与其他替代方案相比提供了一些优势,而且价格非常划算:
-
易用性:如前所述,所有 IOSv、IOS-XRv、NX-OSv、ASAv 和其他镜像的图像都包含在一个单独的下载中。
-
官方:CML 是思科内部和网络工程社区广泛使用的工具。事实上,CML 被广泛用于新的思科 DevNet 专家实验室考试。由于其受欢迎程度,错误得到快速修复,新功能得到仔细记录,有用知识在用户之间广泛共享。
-
第三方 KVM 镜像集成:CML 允许用户上传默认未捆绑的第三方虚拟机镜像,例如 Windows 虚拟机。
-
其他功能:CML 工具还提供了许多其他功能,例如仪表板列表视图、多用户分组、Ansible 集成和 pyATS 集成。
我们在这本书中不会使用所有 CML 功能,但知道这个工具功能丰富且不断更新是很令人高兴的。再次强调,对于本书中的示例,拥有一个实验室来跟随是很重要的,但它不需要是思科 CML。本书中提供的代码示例应该适用于任何实验室设备,只要它运行相同的软件类型和版本。
CML 技巧
CML 网站([developer.cisco.com/modeling-labs/](https://developer.cisco.com/modeling-labs/))和文档([https://developer.cisco.com/docs/modeling-labs/](https://developer.cisco.com/docs/modeling-labs/))提供了从安装到使用的许多指导和信息。实验室拓扑将包含在书籍 GitHub 仓库的相关章节中(https://github.com/PacktPublishing/Mastering-Python-Networking-Fourth-Edition)。实验室镜像可以直接通过导入按钮导入到实验室中:

图 2.1:CML 控制台镜像实验室镜像
对于实验室,每个设备的管理接口都将连接到一个无管理交换机,该交换机再连接到外部连接以供访问:

图 2.2:用于管理接口访问的无管理交换机
你需要更改管理接口的 IP 地址以适应你自己的实验室架构。例如,在第二章的2_DC_Topology.yaml文件中,lax-edg-r1 GigabitEthernet0/0 0的 IP 地址是192.168.2.51。你需要根据你自己的实验室更改这个 IP 地址。
如果你使用的是除 CML 之外的虚拟实验室软件,你可以用任何文本编辑器(例如下面的 Sublime Text)打开拓扑文件,并查看每个设备的配置。然后你可以将配置复制并粘贴到自己的实验室设备中:

图 2.3:使用文本编辑器查看的拓扑文件
我们在本节前面简要介绍了思科 DevNet。让我们在下一节中更深入地探讨 DevNet。
思科 DevNet
思科 DevNet([developer.cisco.com/site/devnet/](https://developer.cisco.com/site/devnet/))是思科网络自动化资源的首选一站式网站。注册免费,并提供免费远程实验室、免费视频课程、指导学习路径、文档等。
如果你还没有自己的实验室或者想要尝试新技术,思科 DevNet 沙盒([https://developer.cisco.com/site/sandbox/](https://developer.cisco.com/site/sandbox/))是一个很好的替代方案。一些实验室总是可用,而其他则需要预订。实验室的可用性将取决于使用情况。
图 2.4:思科 DevNet 沙盒
自从成立以来,思科 DevNet 已经成为思科网络可编程性和自动化相关事宜的事实上目的地。如果你对追求思科自动化认证感兴趣,DevNet 提供了从助理到专家级别的不同验证路径;更多信息可以在developer.cisco.com/certification/找到。
GNS3 和其他
我还使用过一些其他的虚拟实验室,并且会推荐它们。GNS3 就是其中之一:

图 2.5:GNS3 网站
如前所述,GNS3 是我们许多人用来准备认证考试和练习实验室的工具。这个工具已经从最初作为 Dynamips 的简单前端发展到成为一个可行的商业产品。GNS3 是供应商中立的,如果我们想建立一个多厂商实验室,这会很有帮助。这通常是通过克隆镜像(例如 Arista vEOS)或通过其他虚拟机管理程序(例如 KVM)直接启动网络设备镜像来完成。
另一个获得许多好评的多厂商网络仿真环境是仿真虚拟环境下一代(Eve-NG):www.eve-ng.net/。我个人对这个工具的经验不足,但我的许多同行和行业内的朋友都使用它来建立他们的网络实验室。如果你熟悉容器,containerlab (containerlab.dev/)也可以作为你的一个替代选择。
还有其他一些独立的虚拟化平台,例如 Arista vEOS (www.arista.com/en/cg-veos-router/veos-router-overview)、Juniper vMX (www.juniper.net/us/en/products/routers/mx-series/vmx-virtual-router-software.html)和 Nokia SR-Linux (www.nokia.com/networks/data-center/service-router-linux-NOS/),你可以在测试期间将它们用作独立的虚拟设备。
它们是测试特定平台功能的优秀补充工具。其中许多在公共云提供商市场上作为付费产品提供,以便更容易访问。
现在我们已经建立了我们的网络实验室,我们可以开始尝试使用可以帮助管理和自动化的 Python 库。我们将从启用 Python 虚拟环境开始。然后我们将安装并使用 Pexpect 库进行一些示例。
Python 虚拟环境
让我们首先使用 Python 虚拟环境。Python 虚拟环境允许我们通过创建一个“虚拟”隔离的 Python 安装并在其中安装包来管理不同项目的独立包安装。通过使用虚拟环境,我们不需要担心破坏全局或来自其他虚拟环境的已安装包。我们将首先安装python3.10-venv包,然后创建虚拟环境本身:
$ sudo apt update
$ sudo apt install python3.10-venv
$ python3 -m venv venv
$ source venv/bin/activate
(venv) $
(venv) $ deactivate
从输出中,我们看到我们使用了安装中的 venv 模块,创建了一个名为venv的虚拟环境,然后激活了它。当虚拟环境激活时,你将在主机名前看到(venv)标签,这表明你处于该虚拟环境中。完成工作后,你可以使用 deactivate 命令退出虚拟环境。如果你感兴趣,可以在这里了解更多关于 Python 虚拟环境的信息:packaging.python.org/guides/installing-using-pip-and-virtual-environments/#installing-virtualenv。
在你开始编写代码之前,始终要激活虚拟环境以隔离环境。
一旦我们激活了虚拟环境,我们就可以继续安装 Pexpect 库。
Python pexpect 库
Pexpect 是一个用于派生子应用程序、控制它们并对它们输出中的预期模式做出响应的纯 Python 模块。Pexpect 的工作方式类似于 Don Libes 的 Expect。Pexpect 允许我们的脚本派生一个子应用程序,并像人类输入命令一样控制它;更多详细信息可以在 Pexpect 的文档页面上找到:pexpect.readthedocs.io/en/stable/index.html。
现在,我们通常使用像 Nornir 这样的库来抽象逐行、低级别的交互。然而,至少在高级别上理解这种交互仍然是有用的。如果你是那种不耐烦的人,只需快速浏览以下 Pexpect 和 Paramiko 部分。
与 Don Libes 的原始 工具命令语言(TCL)Expect 模块类似,Pexpect 启动或派生另一个进程,并监视它以控制交互。Expect 工具最初是为了自动化交互式过程,如 FTP、Telnet 和 rlogin 而开发的,后来扩展到包括网络自动化。与原始 Expect 不同,Pexpect 完全用 Python 编写,不需要编译 TCL 或 C 扩展。这使我们能够在代码中使用熟悉的 Python 语法及其丰富的标准库。
Pexpect 安装
Pexpect 的安装过程很简单:
(venv) $ pip install pexpect
让我们快速测试一下以确保包可用;确保我们从虚拟环境启动 Python 交互式外壳:
(venv) $ python
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pexpect
>>> dir(pexpect)
['EOF', 'ExceptionPexpect', 'Expecter', 'PY3', 'TIMEOUT', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__revision__', '__spec__', '__version__', 'exceptions', 'expect', 'is_executable_file', 'pty_spawn', 'run', 'runu', 'searcher_re', 'searcher_string', 'spawn', 'spawnbase',
'spawnu', 'split_command_line', 'sys', 'utils', 'which']
>>> exit()
Pexpect 概述
对于本章,我们将使用 2_DC_Topology 并在两个 IOSv 设备 lax-edg-r1 和 lax-edg-r2 上进行工作:

图 2.6:lax-edg-r1 和 lax-edg-r2
每个设备都将有一个位于 192.16.2.x/24 范围内的管理地址。在示例中,lax-edg-r1 将有 192.168.2.51,而 lax-edg-r2 将有 192.168.2.52 作为管理 IP。如果这是设备第一次开机,它将需要生成一个 RSA 密钥用于 SSH:
lax-edg-r2(config)#crypto key generate rsa
对于较旧的 IOSv 软件镜像,我们可能还需要根据您的平台在ssh配置(~/.ssh/config)中添加以下行:
Host 192.168.2.51
HostKeyAlgorithms +ssh-rsa
KexAlgorithms +diffie-hellman-group-exchange-sha1
Host 192.168.2.52
HostKeyAlgorithms +ssh-rsa
KexAlgorithms +diffie-hellman-group-exchange-sha1
设备准备就绪后,让我们看看如果你通过 telnet 连接到设备,你会如何与之交互:
(venv) $ $ telnet 192.168.2.51
Trying 192.168.2.51...
Connected to 192.168.2.51.
Escape character is '^]'.
<skip>
User Access Verification
Username: cisco
Password:
设备配置使用用户名cisco,密码也是cisco。请注意,由于配置中分配的权限,用户已经处于特权模式:
lax-edg-r1#sh run | i cisco
enable password cisco
username cisco privilege 15 secret 5 $1$SXY7$Hk6z8OmtloIzFpyw6as2G.
password cisco
password cisco
自动配置还生成了 telnet 和 SSH 的vty访问权限:
line con 0
password cisco
line aux 0
line vty 0 4
exec-timeout 720 0
password cisco
login local
transport input telnet ssh
让我们看看一个使用 Python 交互式外壳的 Pexpect 示例:
>>> import pexpect
>>> child = pexpect.spawn('telnet 192.168.2.51')
>>> child.expect('Username')
0
>>> child.sendline('cisco')
6
>>> child.expect('Password')
0
>>> child.sendline('cisco')
6
>>> child.expect('lax-edg-r1#')
0
>>> child.sendline('show version | i V')
19
>>> child.before
b": \r\n************************************************************************\r\n* IOSv is strictly limited to use for evaluation, demonstration and IOS *\r\n* education. IOSv is provided as-is and is not supported by Cisco's *\r\n* Technical Advisory Center. Any use or disclosure, in whole or in part, *\r\n* of the IOSv Software or Documentation to any third party for any *\r\n* purposes is expressly prohibited except as otherwise authorized by *\r\n* Cisco in writing. *\r\n***********************************************************************\r\n"
>>> child.sendline('exit')
5
>>> exit()
从 Pexpect 版本 4.0 开始,你可以在 Windows 平台上运行 Pexpect。但是,正如 Pexpect 文档中提到的,目前将 Pexpect 运行在 Windows 上应被视为实验性的。
在之前的交互示例中,Pexpect 启动了一个子进程,并以交互方式监视它。示例中展示了两个重要的方法,expect()和sendline()。expect()行表示 Pexpect 进程在返回字符串被认为是完成时寻找的字符串。这是预期模式。在我们的例子中,我们知道当返回主机名提示符(lax-edg-r1#)时,路由器已经向我们发送了所有信息。sendline()方法表示应该将哪些单词作为命令发送到远程设备。还有一个名为send()的方法,但sendline()包括一个换行符,这类似于在之前的 telnet 会话中发送的单词末尾按下Enter键。从路由器的角度来看,这就像有人从终端输入文本一样。换句话说,我们正在欺骗路由器,让它们认为它们在与人类交互,而实际上它们正在与计算机通信。
before和after属性将被设置为子应用程序打印的文本。before属性将被设置为子应用程序打印的文本,直到预期的模式。after字符串将包含由预期模式匹配的文本。在我们的例子中,before文本将被设置为两个预期匹配(lax-edg-r1#)之间的输出,包括显示版本命令。after文本是路由器的主机名提示符:
>>> child.sendline('show version | i V')
19
>>> child.expect('lax-edg-r1#')
0
>>> child.before
b'show version | i V\r\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(3)M2, RELEASE SOFTWARE (fc2)\r\nProcessor board ID 9Y0KJ2ZL98EQQVUED5T2Q\r\n'
>>> child.after
b'iosv-1#'
如果你对返回值前面的b'感到好奇,它是一个 Python 字节字符串(docs.python.org/3.10/library/stdtypes.html)。
如果你期望错误的术语会发生什么?例如,如果我们输入了username而不是Username(在启动子应用程序后),Pexpect 进程会在子进程中寻找username字符串。在这种情况下,Pexpect 进程会直接挂起,因为路由器永远不会返回username这个词。会话最终会超时,或者我们可以通过Ctrl + C手动退出。
expect()方法等待子应用返回一个给定的字符串,所以在前面的例子中,如果你想要适应小写和大写的u,你可以使用以下术语:
>>> child.expect('[Uu]sername')
方括号充当or操作符,告诉子应用期望一个小写或大写的“u”后跟sername作为字符串。我们告诉进程的是,我们将接受Username或username作为期望的字符串。有关使用正则表达式进行这些不同类型匹配的更多信息,请参阅:docs.python.org/3.10/library/re.html。
expect()方法也可以包含一个选项列表,而不仅仅是单个字符串;这些选项本身也可以是正则表达式。回到之前的例子,我们可以使用以下选项列表来适应两种可能的字符串:
>>> child.expect(['Username', 'username'])
通常来说,当我们能够将不同的字母组合成一个正则表达式时,使用单个expect字符串的正则表达式;而当我们需要捕获来自设备的完全不同的响应,例如密码拒绝时,则使用可能的选择。例如,如果我们为登录使用几个不同的密码,我们希望捕获% Login invalid以及设备的提示信息。
Pexpect 正则表达式与 Python 正则表达式之间的重要区别在于,Pexpect 匹配是非贪婪的,这意味着在使用特殊字符时,它们会尽可能少地匹配。因为 Pexpect 在流上执行正则表达式,它不能向前查看,因为生成流的子进程可能尚未完成。这意味着通常匹配行尾的特殊美元符号$是无用的,因为.+总是会返回没有字符,而.*模式会尽可能少地匹配。一般来说,只需记住这一点,并在expect匹配字符串上尽可能具体即可。
让我们考虑以下场景:
>>> child.sendline('show run | i hostname')
22
>>> child.expect('lax-edg-r1')
0
>>> child.before
b'show version | i V\r\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.6(3)M2, RELEASE SOFTWARE (fc2)\r\nProcessor board ID 9Y0KJ2ZL98EQQVUED5T2Q\r\n'
>>>
嗯...这里似乎有点不对劲。与之前的终端输出比较;你期望的输出应该是hostname lax-edg-r1:
iosv-1#sh run | i hostname
hostname lax-edg-r1
仔细查看期望的字符串将揭示错误。在这种情况下,我们在lax-edg-r1主机名后面遗漏了井号#。因此,子应用将返回字符串的第二部分作为期望的字符串:
>>> child.sendline('show run | i hostname')
22
>>> child.expect('lax-edg-r1#')
0
>>> child.before
b'#show run | i hostname\r\nhostname lax-edg-r1\r\n'
在几个示例之后,你可以从 Pexpect 的使用中看到一个模式的出现。用户绘制了 Pexpect 进程和子应用之间交互的序列。通过一些 Python 变量和循环,我们可以开始构建一个有用的程序,这将帮助我们收集信息并对网络设备进行更改。
我们的第一个 Pexpect 程序
我们的第一个程序chapter2_1.py,在上一节的基础上添加了一些额外的代码:
#!/usr/bin/env python
import pexpect
devices = {'iosv-1': {'prompt': 'lax-edg-r1#', 'ip': '192.168.2.51'},
'iosv-2': {'prompt': 'lax-edg-r2#', 'ip': '192.168.2.52'}}
username = 'cisco'
password = 'cisco'
for device in devices.keys():
device_prompt = devices[device]['prompt']
child = pexpect.spawn('telnet ' + devices[device]['ip'])
child.expect('Username:')
child.sendline(username)
child.expect('Password:')
child.sendline(password)
child.expect(device_prompt)
child.sendline('show version | i V')
child.expect(device_prompt)
print(child.before)
child.sendline('exit')
我们在第 5 行使用了一个嵌套字典:
devices = {'iosv-1': {'prompt': 'lax-edg-r1#', 'ip': '192.168.2.51'},
'iosv-2': {'prompt': 'lax-edg-r2#', 'ip': '192.168.2.52'}}
嵌套字典允许我们使用适当的 IP 地址和提示符号来引用相同的设备(例如lax-edg-r1)。然后我们可以在循环的后续部分使用这些值进行expect()方法。
输出会在屏幕上打印出每个设备的show version | i V输出:
$ python chapter2_1.py
b'show version | i V\r\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\r\nProcessor board ID 98U40DKV403INHIULHYHB\r\n'
b'show version | i V\r\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\r\n'
现在我们已经看到了 Pexpect 的基本示例,让我们更深入地了解库的更多功能。
更多 Pexpect 功能
在本节中,我们将探讨更多 Pexpect 功能,这些功能在某些情况下可能会很有用。
如果你与远程设备之间的连接速度较慢或较快,默认的expect()方法超时时间为 30 秒,可以通过timeout参数增加或减少:
>>> child.expect('Username', timeout=5)
你可以选择使用interact()方法将命令返回给用户。这在只想自动化初始任务的一部分时很有用:
>>> child.sendline('show version | i V')
19
>>> child.expect('lax-edg-r1#')
0
>>> child.before
b'show version | i V\r\nCisco IOS Software, IOSv Software (VIOS-
-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\r\nProcessor board ID 98U40DKV403INHIULHYHB\r\n'
>>> child.interact()
show version | i V
Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)
Processor board ID 98U40DKV403INHIULHYHB
lax-edg-r1#exit
Connection closed by foreign host.
>>>
你可以通过以字符串格式打印child.spawn对象来获取有关它的很多信息:
>>> str(child)
"<pexpect.pty_spawn.spawn object at 0x7f068a9bf370>\ncommand: /usr/bin/telnet\nargs: ['/usr/bin/telnet', '192.168.2.51']\nbuffer (last 100 chars): b''\nbefore (last 100 chars): b'TERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\\r\\nProcessor board ID 98U40DKV403INHIULHYHB\\r\\n'\nafter: b'lax-edg-r1#'\nmatch: <re.Match object; span=(165, 176), match=b'lax-edg-r1#'>\nmatch_index: 0\nexitstatus: 1\nflag_eof: False\npid: 25510\nchild_fd: 5\nclosed: False\ntimeout: 30\ndelimiter: <class 'pexpect.exceptions.EOF'>\nlogfile: None\nlogfile_read: None\nlogfile_send: None\nmaxread: 2000\nignorecase: False\nsearchwindowsize: None\ndelaybeforesend: 0.05\ndelayafterclose: 0.1\ndelayafterterminate: 0.1"
>>>
对于 Pexpect 来说,最有用的调试工具是将输出记录到文件中:
>>> child = pexpect.spawn('telnet 192.168.2.51')
>>> child.logfile = open('debug', 'wb')
想了解更多关于 Pexpect 功能的信息,请查看:pexpect.readthedocs.io/en/stable/api/index.html
到目前为止,在我们的示例中我们一直在使用 Telnet,这使得在会话期间我们的通信是明文的。在现代网络中,我们通常使用安全外壳(SSH)进行管理。在下一节中,我们将探讨带有 SSH 的 Pexpect。
Pexpect 和 SSH
Pexpect 有一个名为pxssh的子类,它专门用于设置 SSH 连接。该类为登录、登出以及处理ssh登录过程中的不同情况添加了各种方法。程序流程基本上是相同的,除了login()和logout():
>>> from pexpect import pxssh
>>> child = pxssh.pxssh()
>>> child.login('192.168.2.51', 'cisco', 'cisco', auto_prompt_reset=False)
True
>>> child.sendline('show version | i V')
19
>>> child.expect('lax-edg-r1#')
0
>>> child.before
b'show version | i V\r\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\r\nProcessor board ID 98U40DKV403INHIULHYHB\r\n'
>>> child.logout()
>>>
注意login()方法中的auto_prompt_reset=False参数。默认情况下,pxssh使用 shell 提示来同步输出。但由于它主要用于 bash-shell 或 c-shell 的 PS1 选项,它们在 Cisco 或其他网络设备上将会出错。
Pexpect 完整示例
作为最后一步,让我们将到目前为止所学的所有关于 Pexpect 的知识放入一个脚本中。将代码放入脚本中使得在生产环境中使用它更加容易,同时也更容易与同事分享。我们将编写第二个脚本,chapter2_2.py:
#!/usr/bin/env python
import getpass
from pexpect import pxssh
devices = {'lax-edg-r1': {'prompt': 'lax-edg-r1#', 'ip': '192.168.2.51'},
'lax-edg-r2': {'prompt': 'lax-edg-r2#', 'ip': '192.168.2.52'}}
commands = ['term length 0', 'show version', 'show run']
username = input('Username: ')
password = getpass.getpass('Password: ')
# Starts the loop for devices
for device in devices.keys():
outputFileName = device + '_output.txt'
device_prompt = devices[device]['prompt']
child = pxssh.pxssh()
child.login(devices[device]['ip'], username.strip(), password.strip(), auto_prompt_reset=False)
# Starts the loop for commands and write to output
with open(outputFileName, 'wb') as f:
for command in commands:
child.sendline(command)
child.expect(device_prompt)
f.write(child.before)
child.logout()
脚本进一步扩展了我们的第一个 Pexpect 程序,并添加了以下附加功能:
-
它使用 SSH 而不是 Telnet。
-
它通过将命令变成列表(第 8 行)并循环遍历命令(从第 20 行开始)来支持多个命令而不是一个。
-
它提示用户输入用户名和密码,而不是在脚本中硬编码它们,以提高安全性。
-
它将输出写入两个文件,
lax-edg-r1_output.txt和lax-edg-r2_output.txt。
代码执行后,我们应该在同一目录下看到两个输出文件。除了 Pexpect,Paramiko 还是一个流行的 Python 库,用于处理交互式会话。
Python Paramiko 库
Paramiko 是 SSHv2 协议的 Python 实现。就像 Pexpect 的 pxssh 子类一样,Paramiko 简化了主机和远程设备之间的 SSHv2 交互。与 pxssh 不同,Paramiko 仅关注 SSHv2,没有 Telnet 支持。它还提供客户端和服务器操作。
Paramiko 是 Ansible 高级自动化框架网络模块背后的低级 SSH 客户端。我们将在 第四章,Python 自动化框架 – Ansible 中介绍 Ansible。让我们看看 Paramiko 库。
Paramiko 安装
安装 Paramiko 非常简单,使用 Python pip。然而,它对加密库有硬依赖。该库为 SSH 协议提供基于 C 的低级加密算法。
Windows、macOS 和其他 Linux 版本的安装说明可以在:cryptography.io/en/latest/installation/ 找到。
我们将展示为我们的 Ubuntu 22.04 虚拟机安装 Paramiko 的步骤:
sudo apt-get install build-essential libssl-dev libffi-dev python3-dev
pip install cryptography
pip install paramiko
让我们通过使用 Python 解释器导入它来测试库的使用:
$ python
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import paramiko
>>> exit()
现在,我们准备在下一节中查看 Paramiko。
Paramiko 概述
让我们看看一个使用 Python 3 交互式 shell 的快速 Paramiko 示例:
>>> import paramiko, time
>>> connection = paramiko.SSHClient()
>>> connection.set_missing_host_key_policy(paramiko.AutoAddPolicy())
>>> connection.connect('192.168.2.51', username='cisco', password='cisco', look_for_keys=False, allow_agent=False)
>>> new_connection = connection.invoke_shell()
>>> output = new_connection.recv(5000)
>>> print(output) b"\r\n*************************************************************************\
r\n* IOSv is strictly limited to use for evaluation, demonstration and IOS *\r\n* education. IOSv is provided as-is and is not supported by Cisco's *\r\n* Technical Advisory Center. Any use or disclosure, in whole or in part, *\r\n* of the IOSv Software or Documentation to any third party for any *\r\n* purposes is expressly prohibited except as otherwise authorized by *\r\n* Cisco in writing. *\r\n***********************************************************************\r\nlax-edg-r1#"
>>> new_connection.send("show version | i V\n")
19
>>> time.sleep(3)
>>> output = new_connection.recv(5000)
>>> print(output)
b'show version | i V\r\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\r\nProcessor board ID 98U40DKV403INHIULHYHB\r\nlax-edg-r1#'
>>> new_connection.close()
>>>
time.sleep() 函数插入时间延迟以确保所有输出都被捕获。这在较慢的网络连接或繁忙的设备上特别有用。这个命令不是必需的,但根据你的情况推荐使用。
即使我们是第一次看到 Paramiko 的操作,Python 的美感和其清晰的语法意味着我们可以对程序试图做什么有一个相当好的推测:
>>> import paramiko
>>> connection = paramiko.SSHClient()
>>> connection.set_missing_host_key_policy(paramiko.AutoAddPolicy())
>>> connection.connect('192.168.2.51', username='cisco', password='cisco',
look_for_keys=False, allow_agent=False)
前四行创建了一个 SSHClient 类的实例。下一行设置了客户端应使用的策略,关于密钥;在这种情况下,lax-edg-r1 可能既不在系统主机密钥中,也不在应用程序的密钥中。在我们的场景中,我们将自动将密钥添加到应用程序的 HostKeys 对象中。此时,如果你登录到路由器,你会看到所有来自 Paramiko 的登录会话。
接下来的几行从连接中调用一个新的交互式 shell,并重复发送命令和检索输出的模式。最后,我们关闭连接。
一些之前使用过 Paramiko 的读者可能熟悉 exec_command() 方法而不是调用 shell。为什么我们需要调用交互式 shell 而不是直接使用 exec_command()?不幸的是,Cisco IOS 上的 exec_command() 只允许执行单个命令。考虑以下使用 exec_command() 的连接示例:
>>> connection.connect('192.168.2.51', username='cisco', password='cisco', look_for_keys=False, allow_agent=False)
>>> stdin, stdout, stderr = connection.exec_command('show version | i V\n')
>>> stdout.read()
b'Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)rnProcessor board ID 98U40DKV403INHIULHYHBrn'
>>>
一切工作得很好;然而,如果你查看 Cisco 设备上的会话数量,你会注意到连接在没有你关闭连接的情况下被 Cisco 设备断开。因为 SSH 会话不再活跃,如果你想要向远程设备发送更多命令,exec_command()将返回错误:
>>> stdin, stdout, stderr = connection.exec_command('show version | i V\n')
Traceback (most recent call last):
<skip>
raise SSHException('SSH session not active') paramiko.ssh_exception.SSHException: SSH session not active
>>>
在前面的例子中,new_connection.recv()命令显示了缓冲区中的内容,并隐式地为我们清除了它。如果你没有清除接收到的缓冲区会发生什么?输出将不断填充缓冲区并覆盖它:
>>> new_connection.send("show version | i V\n")
19
>>> new_connection.send("show version | i V\n")
19
>>> new_connection.send("show version | i V\n")
19
>>> new_connection.recv(5000)
b'show version | i VrnCisco IOS Software, IOSv Software (VIOS- ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)rnProcessor
board ID 98U40DKV403INHIULHYHBrnlax-edg-r1#show version | i VrnCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)rnProcessor board ID 98U40DKV403INHIULHYHBrnlax-edg-r1#show version | i VrnCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)rnProcessor board ID 98U40DKV403INHIULHYHBrnlax-edg-r1#'
>>>
为了确保确定性输出的连贯性,我们将在每次执行命令时从缓冲区中检索输出。
第一个 Paramiko 程序
我们的第一程序将使用与我们在 Pexpect 程序中使用的相同的一般结构。我们将遍历设备列表和命令,同时使用 Paramiko 而不是 Pexpect。这将让我们很好地比较和对比 Paramiko 和 Pexpect 之间的差异。
如果你还没有这样做,你可以从书的 GitHub 仓库github.com/PacktPublishing/Mastering-Python-Networking-Fourth-Edition下载代码,chapter2_3.py。我将在下面列出显著的不同点:
devices = {'lax-edg-r1': {'ip': '192.168.2.51'},
'lax-edg-r2': {'ip': '192.168.2.52'}}
我们不再需要使用 Paramiko 匹配设备提示符;因此,设备字典可以简化:
commands = ['show version', 'show run']
Paramiko 中没有 sendline 的等效命令;相反,我们手动在每个命令中包含换行符:
def clear_buffer(connection):
if connection.recv_ready():
return connection.recv(max_buffer)
我们增加了一种新的方法来清除发送命令的缓冲区,例如terminal length 0或enable,因为我们不需要那些命令的输出。我们只想清除缓冲区并到达执行提示符。这个功能将在后续的循环中使用,例如在脚本的第 25 行:
output = clear_buffer(new_connection)
程序的其余部分应该相当直观,类似于我们在本章中看到的。我想指出的是,由于这是一个交互式程序,我们在检索输出之前会在远程设备上放置一个缓冲区并等待命令完成:
time.sleep(5)
在我们清除缓冲区后,我们将在命令执行之间等待五秒钟。这将给设备足够的时间来响应,如果它正忙的话。
更多 Paramiko 功能
我们将在稍后的第四章,Python 自动化框架 – Ansible中简要介绍 Paramiko,当我们讨论 Ansible 时,因为 Paramiko 是许多网络模块的底层传输。在本节中,我们将探讨 Paramiko 的一些其他功能。
Paramiko 用于服务器
Paramiko 也可以用来通过 SSHv2 管理服务器。让我们看看如何使用 Paramiko 管理服务器的例子。我们将使用基于密钥的认证来建立 SSHv2 会话。
在这个例子中,我使用了与目标服务器相同的虚拟机管理程序上的另一个 Ubuntu 虚拟机。您也可以使用 CML 模拟器上的服务器或公共云提供商(如 Amazon AWS EC2)中的一个实例。
我们将为我们的 Paramiko 主机生成一个公私钥对:
ssh-keygen -t rsa
默认情况下,此命令将生成一个名为 id_rsa.pub 的公钥,作为用户家目录下 ~/.ssh 中的公钥,以及一个名为 id_rsa 的私钥。对待私钥的重视程度应与您对待不希望与他人共享的私人密码相同。
您可以将公钥视为一张名片,用于识别您的身份。使用私钥和公钥,消息将在本地由您的私钥加密,并由远程主机使用公钥解密。我们应该将公钥复制到远程主机上。在生产环境中,我们可以通过非带外方式使用 USB 驱动器来完成;在我们的实验室中,我们可以简单地将其复制到远程主机的 ~/.ssh/authorized_keys 文件中。为远程服务器打开一个终端窗口,以便您可以粘贴公钥。
使用 Paramiko 将您的管理主机上的 ~/.ssh/id_rsa.pub 内容复制:
$ cat ~/.ssh/id_rsa.pub
ssh-rsa <your public key>
然后,将其粘贴到远程主机下的 user 目录中;在这种情况下,我正在使用 echou 作为双方:
<Remote Host>$ vim ~/.ssh/authorized_keys
ssh-rsa <your public key>
您现在可以使用 Paramiko 来管理远程主机。注意在这个例子中,我们将使用私钥进行认证,以及使用 exec_command() 方法发送命令:
>>> import paramiko
>>> key = paramiko.RSAKey.from_private_key_file('/home/echou/.ssh/id_rsa')
>>> client = paramiko.SSHClient()
>>> client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
>>> client.connect('192.168.199.182', username='echou', pkey=key)
>>> stdin, stdout, stderr = client.exec_command('ls -l')
>>> stdout.read()
b'total 44ndrwxr-xr-x 2 echou echou 4096 Jan 7 10:14 Desktopndrwxr-xr-x 2
echou echou 4096 Jan 7 10:14 Documentsndrwxr-xr-x 2 echou echou 4096 Jan 7
10:14 Downloadsn-rw-r--r-- 1 echou echou 8980 Jan 7 10:03
examples.desktopndrwxr-xr-x 2 echou echou 4096 Jan 7 10:14 Musicndrwxr-xr-x
echou echou 4096 Jan 7 10:14 Picturesndrwxr-xr-x 2 echou echou 4096 Jan 7 10:14 Publicndrwxr-xr-x 2 echou echou 4096 Jan 7 10:14 Templatesndrwxr-xr-x
2 echou echou 4096 Jan 7 10:14 Videosn'
>>> stdin, stdout, stderr = client.exec_command('pwd')
>>> stdout.read()
b'/home/echou'
>>> client.close()
>>>
注意,在服务器示例中,我们不需要创建交互式会话来执行多个命令。您现在可以关闭远程主机的 SSHv2 配置中的基于密码的认证,以启用更安全的基于密钥的认证并启用自动化。
我们为什么要了解使用私钥作为认证方法?越来越多的网络设备,如 Cumulus 和 Vyatta 交换机,正转向使用 Linux shell 和公私钥认证作为安全机制。对于某些操作,我们将使用 SSH 会话和基于密钥的认证的组合进行认证。
更多 Paramiko 示例
在本节中,让我们使 Paramiko 程序更具可重用性。我们现有脚本的一个缺点是:每次我们想要添加或删除主机,或者需要更改在远程主机上要执行的命令时,都需要打开脚本。
这是因为主机和命令信息都是静态地输入到脚本内部的。在更改时,硬编码主机和命令更容易出错。通过将主机和命令文件作为脚本的参数读取,我们可以使脚本更加灵活。用户(以及未来的我们)只需在需要更改主机或命令时修改这些文本文件。
我们已经在名为 chapter2_4.py 的脚本中实现了这一更改。
我们没有硬编码命令,而是将命令拆分到一个单独的commands.txt文件中。到目前为止,我们一直在使用show命令;在这个例子中,我们将进行配置更改。特别是,我们将日志缓冲区大小更改为30000字节:
$ cat commands.txt
config t
logging buffered 30000
end
copy run start
设备信息被写入到devices.json文件中。我们选择 JSON 格式来存储设备信息,因为 JSON 数据类型可以轻松转换为 Python 字典数据类型:
$ cat devices.json
{
"lax-edg-r1": {
"ip": "192.168.2.51"
},
"lax-edg-r2": {
"ip": "192.168.2.52"
}
}
在脚本中,我们做了以下更改:
with open('devices.json', 'r') as f:
devices = json.load(f)
with open('commands.txt', 'r') as f:
commands = f.readlines()
下面是脚本执行的一个简略输出:
$ python chapter2_4.py
Username: cisco
Password:
b'terminal length 0\r\nlax-edg-r1#config t\r\nEnter configuration commands, one per line. End with CNTL/Z.\r\nlax-edg-r1(config)#'
b'logging buffered 30000\r\nlax-edg-r1(config)#'
b'end\r\nlax-edg-r1#'
b'copy run start'
<skip>
快速检查以确保更改已应用于running-config和startup-config:
lax-edg-r1#sh run | i logging
logging buffered 30000
Paramiko 库是一个通用库,旨在与交互式命令行程序一起使用。对于网络管理,还有一个名为 Netmiko 的库,它是从 Paramiko 分叉出来的,专门用于网络设备管理。我们将在下一节中查看它。
Netmiko 库
Paramiko 是一个用于与 Cisco IOS 和其他厂商设备进行低级交互的出色库。但正如您从前面的示例中注意到的,我们在lax-edg-r1和lax-edg-r2设备登录和执行之间重复了许多相同的步骤。一旦我们开始开发更多的自动化命令,我们也开始在捕获终端输出并将它们格式化为可用格式时重复自己。如果有人能编写一个简化这些低级步骤并与其他网络工程师共享的 Python 库,那岂不是很好?
自 2014 年以来,Kirk Byers (github.com/ktbyers) 一直在从事开源项目,以简化网络设备的管理工作。在本节中,我们将查看他创建的 Netmiko (github.com/ktbyers/netmiko) 库的一个示例。
首先,我们将使用pip安装netmiko库:
(venv) $ pip install netmiko
我们可以使用 Kirk 网站上发布的示例,pynet.twb-tech.com/blog/automation/netmiko.html,并将其应用于我们的实验室。我们将首先导入库及其ConnectHandler类。然后我们将定义我们的device参数为一个 Python 字典,并将其传递给ConnectHandler。请注意,我们在device参数中定义了一个device_type为cisco_ios:
>>> from netmiko import ConnectHandler
>>> net_connect = ConnectHandler(
... device_type="cisco_ios",
... host="192.168.2.51",
... username="cisco",
... password="cisco",
... )
这就是简化的开始。请注意,库会自动确定设备提示,并格式化show命令返回的输出:
>>> net_connect.find_prompt()
'lax-edg-r1#'
>>> output = net_connect.send_command('show ip int brief')
>>> print(output)
Interface IP-Address OK? Method Status Protocol
GigabitEthernet0/0 192.168.2.51 YES NVRAM up up
GigabitEthernet0/1 10.0.0.1 YES NVRAM up up
Loopback0 192.168.0.10 YES NVRAM up up
让我们看看实验室中第二个 Cisco IOS 设备的另一个示例,并发送一个configuration命令而不是show命令。请注意,command属性是一个列表,可以包含多个命令:
>>> net_connect_2 = ConnectHandler(
... device_type="cisco_ios",
... host="192.168.2.52",
... username="cisco",
... password="cisco",
... )
>>> output = net_connect_2.send_config_set(['logging buffered 19999'])
>>> print(output)
configure terminal
Enter configuration commands, one per line. End with CNTL/Z.
lax-edg-r2(config)#logging buffered 19999
lax-edg-r2(config)#end
lax-edg-r2#
>>> exit()
这有多酷?Netmiko 自动为我们处理了琐碎的事情,让我们可以专注于命令本身。netmiko库是一个节省时间的库,被许多网络工程师使用。在下一节中,我们将探讨 Nornir(github.com/nornir-automation/nornir)框架,该框架也旨在简化低级交互。
Nornir 框架
Nornir(nornir.readthedocs.io/en/latest/)是一个纯 Python 自动化框架,旨在直接从 Python 使用。我们将从在我们的环境中安装nornir开始:
(venv)$ pip install nornir nornir_utils nornir_netmiko
Nornir 期望我们定义一个名为hosts.yaml的清单文件,其中包含设备的 YAML 格式信息。该文件中指定的信息与我们之前在 Netmiko 示例中使用的 Python 字典定义的信息没有区别:
---
lax-edg-r1:
hostname: '192.168.2.51'
port: 22
username: 'cisco'
password: 'cisco'
platform: 'cisco_ios'
lax-edg-r2:
hostname: '192.168.2.52'
port: 22
username: 'cisco'
password: 'cisco'
platform: 'cisco_ios'
我们可以使用来自nornir库的netmiko插件来与我们的设备交互,如chapter2_5.py文件所示:
#!/usr/bin/env python
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from nornir_netmiko import netmiko_send_command
nr = InitNornir()
result = nr.run(
task=netmiko_send_command,
command_string="show arp"
)
print_result(result)
执行输出如下所示:
(venv) $ python chapter2_5.py
netmiko_send_command************************************************************
* lax-edg-r1 ** changed : False ************************************************
vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Protocol Address Age (min) Hardware Addr Type Interface
Internet 10.0.0.1 - 5254.001e.e911 ARPA GigabitEthernet0/1
Internet 10.0.0.2 17 fa16.3e00.0001 ARPA GigabitEthernet0/1
^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* lax-edg-r2 ** changed : False ************************************************
vvvv netmiko_send_command ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO
Protocol Address Age (min) Hardware Addr Type Interface
Internet 10.0.128.1 17 fa16.3e00.0002 ARPA GigabitEthernet0/1
Internet 10.0.128.2 - 5254.0014.e052 ARPA GigabitEthernet0/1
^^^^ END netmiko_send_command ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Nornir 除了 Netmiko 之外,还有其他插件,例如流行的 NAPALM 库(github.com/napalm-automation/napalm)。请随时查看 Nornir 的项目页面以获取最新的插件:nornir.readthedocs.io/en/latest/plugins/index.html。
在本章中,我们使用 Python 自动化网络方面取得了很大的进步。然而,我们使用的一些方法感觉像是自动化的一种权宜之计。我们试图欺骗远程设备,让它们认为它们正在与另一端的人类交互。即使我们使用 Netmiko 或 Nornir 框架等库,其底层方法仍然相同。尽管有人已经做了工作来帮助我们抽象低级交互的繁琐工作,但我们仍然容易受到仅处理 CLI 设备所带来的缺点的影响。
展望未来,让我们讨论一下与 Pexpect 和 Paramiko 相比,其他工具的一些缺点,为下一章讨论基于 API 的方法做准备。
与其他工具相比,Pexpect 和 Paramiko 的缺点
我们目前用于自动化仅 CLI 设备的当前方法的最大缺点是远程设备不返回结构化数据。它们返回的数据非常适合在终端上显示,以便由人类解释,而不是由计算机程序解释。人眼可以轻松地解释空格,而计算机只能看到回车符。
我们将在下一章中探讨一种更好的方法。作为对第三章,APIs 和 Intent-Driven Networking的序言,让我们讨论一下幂等性的概念。
幂等网络设备交互
术语“幂等性”在不同的上下文中有不同的含义。但在本书的上下文中,这个术语意味着当客户端对远程设备进行相同的调用时,结果应该始终相同。我相信我们都可以同意这是必要的。想象一下,每次你执行相同的脚本时,你都会得到不同的结果。我发现这种情况非常可怕。如果那样的话,你怎么能信任你的脚本呢?这将使我们的自动化工作变得毫无意义,因为我们需要准备好处理不同的返回结果。
由于 Pexpect 和 Paramiko 正在交互式地发出一系列命令,出现非幂等交互的可能性更高。回到需要从屏幕抓取有用元素的事实,差异的风险要高得多。在编写脚本和脚本执行第 100 次之间,远程端可能发生了变化。例如,如果供应商在发布之间更改了屏幕输出,而我们没有更新脚本,脚本可能会破坏我们的网络。
如果我们需要在生产中依赖脚本,我们需要尽可能使脚本具有幂等性。
糟糕的自动化会加速糟糕的事情
糟糕的自动化会让你更快地戳到自己的眼睛,就是这样简单。计算机在执行任务方面比人类工程师要快得多。如果我们让一套操作程序由人类和脚本执行,脚本会比人类更快地完成,有时甚至没有在步骤之间建立稳固的反馈循环的好处。互联网上充满了当有人按下Enter键后立即后悔的故事。
我们需要最小化糟糕的自动化脚本搞砸事情的可能性。我们都会犯错误;在生产工作之前仔细测试你的脚本,并保持一个小的破坏半径是确保你能在错误回来并咬你之前捕捉到错误的关键。没有工具或人类可以完全消除错误,但我们可以努力最小化错误。正如我们所看到的,尽管我们在这章中使用的一些库很棒,但基于 CLI 的方法本质上是有缺陷和容易出错的。我们将在下一章介绍 API 驱动的方法,该方法解决了 CLI 驱动管理的一些不足。
摘要
在本章中,我们介绍了直接与网络设备通信的低级方法。如果没有一种方法可以程序化地与网络设备通信并对其进行更改,就没有自动化。我们查看了一些 Python 库,这些库允许我们管理那些旨在通过 CLI 进行管理的设备。尽管很有用,但很容易看出这个过程可能会有些脆弱。这主要是因为相关的网络设备原本是打算由人类而不是计算机管理的。
在第三章,APIs 和意图驱动的网络中,我们将探讨支持 API 和意图驱动的网络的网络设备。
加入我们的书籍社区
要加入这本书的社区——在那里您可以分享反馈、向作者提问,并了解新书发布——请扫描下面的二维码:
packt.link/networkautomationcommunity

第三章:API 和意图驱动的网络
在第二章,低级网络设备交互中,我们探讨了使用 Python 库(如 Pexpect、Paramiko、Netmiko 和 Nornir)与网络设备交互的方法。Paramiko 和类似的库使用持久会话,模拟用户在终端前输入命令。这种方法在某种程度上是可行的。向设备发送命令并捕获输出很容易。然而,当输出超过几行字符时,计算机程序解释输出就变得困难。例如,Paramiko 返回的输出是一系列字符,旨在由人类阅读。输出的结构由行和空格组成,对人类友好,但对计算机程序来说难以理解。
关键点在于:为了使我们的计算机程序自动化执行我们想要执行的大多数任务,我们需要解释返回的结果并根据返回的结果采取后续行动。当我们无法准确和可预测地解释返回的结果时,我们就无法有信心地执行下一个命令。
这是一个互联网社区普遍面临的问题,而不仅仅是网络自动化,每当计算机需要相互通信时。想象一下当计算机和人类阅读网页时的区别。人类看到的是浏览器解释的文字、图片和空间;而计算机看到的是原始 HTML 代码、Unicode 字符和二进制文件。当网站需要成为另一台计算机的 Web 服务时会发生什么?相同的 Web 资源需要同时容纳人类客户端和其他计算机程序。本质上,Web 服务器需要以最优化的方式将信息传输到另一台计算机。我们如何做到这一点?
答案是应用程序编程接口(API)。需要注意的是,API 是一个概念,而不是特定的技术或框架。根据维基百科的定义:
在计算机编程中,应用程序编程接口(API)是一组子程序定义、协议和工具,用于构建应用程序软件。一般而言,它是一组明确定义的软件组件之间通信的方法。一个好的 API 通过提供所有构建块,使得开发计算机程序变得更加容易,然后由程序员将这些构建块组合起来。
在我们的用例中,明确定义的通信方法将存在于我们的 Python 程序和目标设备之间。我们的网络设备提供的 API 为计算机程序(如我们的 Python 脚本)提供了一个独立的接口。确切的 API 实现是供应商和有时是产品特定的。一个供应商可能会偏好 XML,而另一个可能会使用 JSON;一个产品可能会使用 HTTPS 作为底层传输协议,而其他产品可能会提供用于设备的 Python 库,称为 SDK。在本章中,我们将看到许多不同的供应商和产品示例。
尽管存在差异,API 的概念仍然是相同的:它是一种针对其他计算机程序优化的通信方法。
在本章中,我们将探讨以下主题:
-
将基础设施视为代码(IaC)、意图驱动的网络和数据建模
-
Cisco NX-API、应用中心基础设施(ACI)和 Meraki 示例
-
Juniper 网络配置协议(NETCONF)和 PyEZ
-
Arista eAPI 和 pyeapi
我们将从探讨为什么我们要将基础设施视为代码开始。
基础设施即代码 (IaC)
在一个完美的世界里,网络工程师和架构师在设计和管理网络时应该专注于他们希望网络实现的目标,而不是设备级别的交互。但我们都知道这个世界远非完美。许多年前,当我作为一家二线 ISP 的实习生工作时,我的第一个任务之一就是在客户现场安装一个路由器,以开通他们的分数帧中继链路(还记得那些吗?)。我该如何做呢? 我问。我被 handed down 一份开通帧中继链路的标准操作程序。
我去了客户现场,盲目地输入命令,看着绿色的指示灯闪烁,高兴地收拾好背包,为自己的工作感到自豪。尽管这项任务很令人兴奋,但我并没有完全理解我在做什么。我只是盲目地遵循指示,没有考虑我输入的命令的含义。如果指示灯是红色而不是绿色,我将如何进行故障排除?毫无疑问,我必须打电话给办公室,请求一位经验更丰富的工程师的帮助。
网络工程不仅仅是将命令输入到设备中;它是在尽可能减少摩擦的情况下,建立一个从一点到另一点提供服务的途径。我们必须使用的命令和必须解释的输出只是达到目的的手段。换句话说,我们应该专注于我们对网络的意图。我们希望网络实现的目标比我们用来让设备做我们想要它做的事情的命令语法更重要。如果我们进一步将描述我们意图的想法抽象为代码行,我们就可以潜在地描述我们的整个基础设施为特定的状态。基础设施将通过代码行来描述,并带有必要的软件或框架来强制执行该状态。
意图驱动的网络
自从本书第一版出版以来,随着主要网络供应商选择使用基于意图的网络(IBN)和意图驱动的网络(IDN)来描述他们的下一代设备,这两个术语的使用频率有所上升。这两个术语通常意味着相同的事情。在我看来,IDN 是定义网络应处于的状态并拥有软件代码来强制执行该状态的想法。例如,如果我的目标是阻止端口80从外部访问,这就是我应该将其声明为网络意图的方式。底层软件将负责了解配置和应用于边界路由器上的必要访问列表的语法以实现该目标。当然,IDN 是一个没有明确答案的关于确切实现的想法。我们用来强制执行我们声明的意图的软件可以是一个库、一个框架,或者我们从供应商那里购买的完整包。
当使用 API 时,我认为它使我们更接近 IDN 的状态。简而言之,因为我们抽象了在目标设备上执行特定命令的层,我们关注的是意图而不是特定命令。例如,回到我们的block port 80访问列表示例,我们可能在 Cisco 路由器上使用access-list和access-group,在 Juniper 路由器上使用filter-list。然而,通过使用 API,我们的程序可以开始询问执行者他们的意图,同时隐藏软件正在与哪种物理设备通信。我们甚至可以使用更高层次的声明性框架,例如我们在第四章,Python 自动化框架中将要介绍的 Ansible。但现在,让我们专注于网络 API。
屏幕抓取与 API 结构化输出
想象一个常见的场景,我们需要登录网络设备并确保设备上的所有接口都处于 up/up 状态(状态和协议都显示为up)。对于进入 Cisco NX-OS 设备的网络工程师来说,在终端中发出show ip interface brief命令就足够简单,可以很容易地从输出中看出哪个接口是 up 的:
lax-edg-r1#sh ip int brief
Interface IP-Address OK? Method Status Protocol
GigabitEthernet0/0 192.168.2.51 YES NVRAM up up
GigabitEthernet0/1 10.0.0.1 YES NVRAM up up
Loopback0 192.168.0.10 YES NVRAM up
行中断、空白和列标题的第一行很容易被肉眼区分。它们的存在是为了帮助我们对齐,比如,从第一行到第二行和第三行的每个接口的 IP 地址。如果我们把自己放在计算机的位置来捕获信息,所有这些空白和行中断只会让我们远离重要的输出,即:哪些接口处于 up/up 状态?为了说明这一点,我们可以看看show interface brief命令的 Paramiko 输出:
>>> new_connection.send('show ip int brief/n')
16
>>> output = new_connection.recv(5000)
>>> print(output)
b'show ip interface brief\r\nInterface IP-Address OK? Method Status Protocol\r\nGigabitEthernet0/0 192.168.2.51 YES NVRAM up up \r\nGigabitEthernet0/1 10.0.0.1 YES NVRAM up up \r\nLoopback0 192.168.0.10 YES NVRAM up up \r\nlax-edg-r1#'
>>>
如果我们要解析输出变量中包含的数据,我将以伪代码的形式(伪代码意味着实际代码的简化表示)这样做,以将文本减到所需的信息中:
-
通过行中断分割每一行。
-
我不需要包含
show ip interface brief执行的命令的第一行,我会将其丢弃。 -
从第二行取出直到主机名提示符的所有内容,并将其保存在一个变量中。
-
对于其余的行,因为我们不知道有多少接口,我们将使用正则表达式语句来搜索行是否以接口名称开头,例如
lo代表回环接口和GigabitEthernet代表以太网接口。 -
我们需要将这一行分成三个部分,由空格分隔,每个部分分别包含接口名称、IP 地址以及接口状态。
-
接口状态将通过空格进一步分割,以给我们协议、链路和管理状态。
呼吁,这只是一项大量工作,仅仅是为了人类一眼就能看出的事情!这些步骤是我们需要执行屏幕抓取非结构化文本时的步骤。这种方法有很多缺点,但我能看到的更大问题如下:
-
可扩展性:我们花费了大量时间在繁琐的细节上,以解析每个命令的输出。很难想象我们如何为通常运行的数百个命令做这件事。
-
可预测性:无法保证不同软件版本之间的输出保持一致。如果输出有细微的变化,它可能会使我们的信息收集斗争变得毫无意义。
-
供应商和软件锁定:一旦我们投入所有努力解析特定供应商和软件版本的输出,在这个例子中是 Cisco IOS,我们就需要为下一个我们选择的供应商重复这个过程。我不知道你怎么样,但如果我要评估一个新的供应商,如果我要再次重写所有的屏幕抓取代码,新的供应商将处于严重的入门劣势。
让我们比较一下 NX-API 调用相同show ip interface brief命令的输出。我们将在本章后面详细说明如何从设备获取此输出,但这里重要的是将以下输出与之前的屏幕抓取输出(完整输出在课程代码库中)进行比较:
{
"ins_api":{
"outputs":{
"output":{
"body":{ "TABLE_intf":[
{
"ROW_intf":{
"admin-state":"up",
"intf-name":"Lo0",
"iod":84,
"ip-disabled":"FALSE",
"link-state":"up",
"prefix":"192.168.2.50",
"proto-state":"up"
}
},
{
"ROW_intf":{
"admin-state":"up",
"intf-name":"Eth2/1",
"iod":36,
"ip-disabled":"FALSE",
"link-state":"up",
"prefix":"10.0.0.6",
"proto-state":"up"
}
}
],
"TABLE_vrf":[
{
"ROW_vrf":{
"vrf-name-out":"default"
}
},
{
"ROW_vrf":{
"vrf-name-out":"default"
}
}
]
},
"code":"200",
"input":"show ip int brief",
"msg":"Success"
}
},
"sid":"eoc",
"type":"cli_show",
"version":"1.2"
}
}
NX-API 可以返回 XML 或 JSON 格式的输出,这是 JSON 输出。我们可以立即看到输出是有结构的,可以直接映射到 Python 字典数据结构。一旦转换为 Python 字典,就不需要大量的解析——我们只需简单地选择键并检索与键关联的值。我们还可以从输出中看到,输出中有各种元数据,例如命令的成功或失败。如果命令失败,将有一条消息告诉发送者失败的原因。我们不再需要跟踪发出的命令,因为它已经通过input字段返回给你了。输出中还有其他有用的元数据,例如 NX-API 版本。
这种交换使得供应商和运营商的生活都变得更加容易。在供应商方面,他们可以轻松地传输配置和状态信息。当需要使用相同的数据结构暴露更多数据时,他们可以轻松地添加额外字段。在运营商方面,我们可以轻松地获取信息,并围绕它构建我们的基础设施自动化。所有人都同意,网络自动化和可编程性对网络供应商和运营商都有益。问题通常涉及自动化消息的传输、格式和结构。正如我们将在本章后面看到的那样,在 API 的伞形之下,有许多竞争技术。仅就传输语言而言,我们就有 REST API、NETCONF 和 RESTCONF 等。
IaC 的数据建模
根据维基百科(en.wikipedia.org/wiki/Data_model),数据模型的定义如下:
数据模型是一个抽象模型,它组织数据元素并标准化它们相互之间以及与现实世界实体属性的关系。例如,数据模型可能指定代表汽车的元素由多个其他元素组成,这些元素反过来又代表汽车的颜色和大小,并定义其所有者。
数据建模过程在以下图中展示:

图 3.1:数据示例过程
当将数据模型概念应用于网络时,我们可以称网络数据模型是一个抽象模型,它描述了我们的网络。如果我们仔细观察一个物理数据中心,一个二层以太网交换机可以被认为是一个包含映射到每个端口的 MAC 地址表的设备。我们的交换机数据模型描述了 MAC 地址应该如何保存在表中,包括键、附加特性(想想 VLAN 和私有 VLAN)以及更多。
同样,我们可以超越设备,在数据模型中映射整个数据中心。我们可以从接入层、分发层和核心层中的设备数量开始,它们是如何连接的,以及它们在生产环境中应该如何表现。
例如,如果我们有一个胖树网络,我们可以在模型中声明每个脊路由器有多少链路,它们应该包含多少路由,以及每个前缀会有多少下一跳。
记得我们讨论过 IaC 吗?这些特性可以以软件程序可以参考的格式映射出来,然后作为我们可以检查的理想状态。
YANG 和 NETCONF
网络数据建模语言之一是 YANG,这是一个有趣的缩写,代表“另一个下一代”(尽管普遍认为,一些 IETF 工作组确实有幽默感)。它首次于 2010 年在 RFC 6020 中发布,并自此在供应商和运营商中获得了认可。
作为一种数据建模语言,YANG 用于建模设备的配置。它还可以表示由 NETCONF 协议、NETCONF 远程过程调用和 NETCONF 通知操作的状态数据。它的目标是提供协议(如 NETCONF)和底层供应商特定配置和操作语法之间的通用抽象层。我们将在本章后面查看一些 YANG 的示例。
现在我们已经讨论了基于 API 的设备管理和数据建模的高级概念,让我们来看看思科在其 API 结构中的几个示例。
思科 API 示例
在网络领域占据 800 磅巨猿地位的思科系统,没有错过网络自动化的趋势。在推动网络自动化的过程中,他们进行了各种内部开发、产品增强、合作以及许多外部收购。然而,由于产品线涵盖了路由器、交换机、防火墙、服务器(统一计算)、无线、协作软件和硬件以及分析软件,很难知道从哪里开始。
由于本书侧重于 Python 和网络,我们将本节中的思科示例范围限定在主要网络产品上。特别是,我们将涵盖以下内容:
-
带有 NX-API 的 Nexus
-
思科 NETCONF 和 YANG 示例
-
思科应用中心基础设施(ACI)
-
思科 Meraki 示例
对于本章中的 NX-API 和 NETCONF 示例,我们可以使用第二章,低级网络设备交互中提到的思科 DevNet 始终在线实验室设备,或者使用本地运行的思科 CML 虚拟实验室。
我们将使用与第二章,低级网络设备交互中相同的实验室拓扑,并专注于运行NX-OSv、lax-cor-r1和nyc-cor-r1的设备:

图 3.2:实验室 NX-OSv 设备
让我们先看看思科 NX-API 的示例。
思科 NX-API
Nexus 是思科数据中心交换机的主要产品线。NX-API(www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus9000/sw/6-x/programmability/guide/b_Cisco_Nexus_9000_Series_NX-OS_Programmability_Guide/b_Cisco_Nexus_9000_Series_NX-OS_Programmability_Guide_chapter_011.html)允许工程师通过 SSH、HTTP 和 HTTPS 等多种传输方式与交换机进行交互。
实验室准备
记得激活我们的 Python 虚拟环境。这应该是我们从现在开始进行实验室操作的一个基本要求:
$ source venv/bin/activate
ncclient (github.com/ncclient/ncclient) 库是一个用于 NETCONF 客户端的 Python 库。我们还将安装一个流行的 Python HTTP 客户端库,称为 Requests (pypi.org/project/requests/)。我们可以通过 pip 安装这两个库:
$ pip install ncclient==0.6.13
$ pip install requests==2.28.1
Nexus 设备上的 NX-API 默认是关闭的,因此我们需要将其打开。我们还需要一个用户。在这种情况下,我们将仅使用现有的 cisco 用户:
feature nxapi
username cisco password 5 $1$Nk7ZkwH0$fyiRmMMfIheqE3BqvcL0C1 role network-operator
username cisco role network-admin
username cisco passphrase lifetime 99999 warntime 14 gracetime 3
对于我们的实验室,我们将打开 nxapi http 和 nxapi sandbox 配置;请注意,在生产环境中这两个都应该关闭:
lax-cor-r1(config)# nxapi http port 80
lax-cor-r1(config)# nxapi sandbox
我们现在准备好查看我们的第一个 NX-API 示例。
NX-API 示例
NX-API 沙盒是玩转各种命令、数据格式,甚至直接从网页复制 Python 脚本的好方法。在上一步中,我们为了学习目的将其打开。再次强调,沙盒在生产环境中应该关闭。
使用 Nexus 设备的管理 IP 启动一个网页浏览器,查看基于我们已熟悉的 CLI 命令的各种消息格式、请求和响应:

图 3.3:NX-API 开发者沙盒
在下面的示例中,我选择了 JSON-RPC 和 CLI 命令类型来执行 show version 命令。点击 POST,我们将看到 请求 和 响应:

图 3.4:NX-API 沙盒示例输出
如果你对消息格式的可支持性不确定,或者如果你对你的代码中想要检索的值对应的响应数据字段键有疑问,沙盒会很有用。
在我们的第一个示例 cisco_nxapi_1.py 中,我们只是连接到 Nexus 设备并打印出首次连接时交换的能力:
#!/usr/bin/env python3
from ncclient import manager
conn = manager.connect(
host='192.168.2.50',
port=22,
username='cisco',
password='cisco',
hostkey_verify=False,
device_params={'name': 'nexus'},
look_for_keys=False
)
for value in conn.server_capabilities:
print(value)
conn.close_session()
在我们的示例中,我们使用 ncclient 库连接到设备。主机、端口、用户名和密码的连接参数是显而易见的。设备参数指定客户端连接到的设备类型。hostkey_verify 跳过了 SSH 的 known_host 要求;如果它未设置为 false,则主机指纹需要列在 ~/.ssh/known_hosts 文件中。look_for_keys 选项禁用了公私钥认证,并使用用户名和密码组合进行认证。
输出将显示本版 NX-OS 支持的 XML 和 NETCONF 功能:
(venv) $ python cisco_nxapi_1.py
urn:ietf:params:xml:ns:netconf:base:1.0
urn:ietf:params:netconf:base:1.0
urn:ietf:params:netconf:capability:validate:1.0
urn:ietf:params:netconf:capability:writable-running:1.0
urn:ietf:params:netconf:capability:url:1.0?scheme=file
urn:ietf:params:netconf:capability:rollback-on-error:1.0
urn:ietf:params:netconf:capability:candidate:1.0
urn:ietf:params:netconf:capability:confirmed-commit:1.0
使用 ncclient 和通过 SSH 的 NETCONF 非常好,因为它使我们更接近原生实现和语法。我们将在本书的后面部分使用相同的库来比较其他供应商。对于 NX-API,我们还可以使用 HTTPS 和 JSON-RPC。在 NX-API 开发者沙盒 的早期截图,如果您注意到了,在 请求 框中有一个标记为 Python 的框。如果您点击它,您将能够根据 Requests 库自动转换成 Python 脚本。
对于来自 NX-API 沙盒的 show version 示例,以下 Python 脚本为我们自动生成。我在这里粘贴输出,没有进行任何修改:
"""
NX-API-BOT
"""
import requests
import json
"""
Modify these please
"""
url='http://YOURIP/ins'
switchuser='USERID'
switchpassword='PASSWORD'
myheaders={'content-type':'application/json-rpc'}
payload=[
{
"jsonrpc": "2.0",
"method": "cli",
"params": {
"cmd": "show version",
"version": 1.2
},
"id": 1
}
]
response = requests.post(url,data=json.dumps(payload), headers=myheaders,auth=(switchuser,switchpassword)).json()
在 cisco_nxapi_2.py 脚本中,您将看到我已经修改了 NX-API 沙盒生成的脚本中的 URL、用户名和密码。输出被解析,只包含软件版本。以下是输出:
(venv) $ python cisco_nxapi_2.py
7.3(0)D1(1)
使用这种方法最好的部分是,相同的总体语法结构适用于配置和 show 命令。这可以在 cisco_nxapi_3.py 文件中看到,该文件使用新的主机名配置设备。在命令执行后,您将看到设备主机名从 lax-cor-r1 更改为 lax-cor-r1-new:
lax-cor-r1-new# sh run | i hostname
hostname lax-cor-r1-new
对于多行配置,您可以使用 ID 字段来指定操作的顺序。这可以在 cisco_nxapi_4.py 中看到。以下有效载荷被列出,用于在接口配置模式下更改以太网 2/12 接口的描述:
{
"jsonrpc": "2.0",
"method": "cli",
"params": {
"cmd": "interface ethernet 2/12",
"version": 1.2
},
"id": 1
},
{
"jsonrpc": "2.0",
"method": "cli",
"params": {
"cmd": "description foo-bar",
"version": 1.2
},
"id": 2
},
{
"jsonrpc": "2.0",
"method": "cli",
"params": {
"cmd": "end",
"version": 1.2
},
"id": 3
},
{
"jsonrpc": "2.0",
"method": "cli",
"params": {
"cmd": "copy run start",
"version": 1.2
},
"id": 4
}
]
我们可以通过查看 Nexus 设备的运行配置来验证先前配置脚本的结果:
interface Ethernet2/12
description foo-bar
shutdown
no switchport
mac-address 0000.0000.002f
在下一个示例中,我们将看到如何使用 YANG 与 NETCONF。
Cisco YANG 模型
让我们通过一个示例来看看 Cisco 的 YANG 模型支持。首先,我们应该知道 YANG 模型只定义了通过 NETCONF 协议发送的架构类型,而没有规定数据应该是什么。其次,值得注意的是,NETCONF 作为独立协议存在,正如我们在 NX-API 部分所看到的。第三,YANG 在不同供应商和产品线上的支持性不同。例如,如果我们为运行 IOS-XE 的 Cisco CSR 1000v 运行能力交换脚本,我们可以在平台上看到支持的 YANG 模型:
urn:cisco:params:xml:ns:yang:cisco-virtual-service?module=cisco- virtual-service&revision=2015-04-09
http://tail-f.com/ns/mibs/SNMP-NOTIFICATION-MIB/200210140000Z? module=SNMP-NOTIFICATION-MIB&revision=2002-10-14
urn:ietf:params:xml:ns:yang:iana-crypt-hash?module=iana-crypt-hash&revision=2014-04-04&features=crypt-hash-sha-512,crypt-hash-sha-256,crypt-hash-md5
urn:ietf:params:xml:ns:yang:smiv2:TUNNEL-MIB?module=TUNNEL-MIB&revision=2005-05-16
urn:ietf:params:xml:ns:yang:smiv2:CISCO-IP-URPF-MIB?module=CISCO-IP-URPF-MIB&revision=2011-12-29
urn:ietf:params:xml:ns:yang:smiv2:ENTITY-STATE-MIB?module=ENTITY-STATE-MIB&revision=2005-11-22
urn:ietf:params:xml:ns:yang:smiv2:IANAifType-MIB?module=IANAifType-MIB&revision=2006-03-31
<omitted>
YANG 在不同供应商和产品线上的支持性有些不均匀。我在本书的代码库中包含了一个 cisco_yang_1.py 脚本,展示了如何使用 Cisco Devnet 提供的 Cisco IOS-XE 总是开启沙盒,通过 YANG 过滤器 urn:ietf:params:xml:ns:yang:ietf-interfaces 解析出 NETCONF XML 输出。
我们可以在 YANG GitHub 项目页面上看到最新的供应商支持(github.com/YangModels/yang/tree/master/vendor)。
Cisco ACI 示例
Cisco 应用中心基础设施,或称 ACI,旨在为其管理范围内定义的所有网络组件提供一个集中式控制器方法。在数据中心环境中,集中式控制器了解并管理脊线、叶子和机架顶部交换机,以及所有网络服务功能。这可以通过图形用户界面(GUI)、命令行界面(CLI)或 API 来实现。有些人可能会认为 ACI 是思科对基于控制器、软件定义网络更广泛解决方案的回应。
ACI API 遵循 REST 模型,使用 HTTP 动词(GET、POST 和 DELETE)来指定预期操作。在我们的示例中,我们可以使用 Cisco DevNet 永久在线实验室 ACI 设备(devnetsandbox.cisco.com/RM/Topology):

图 3.5:Cisco DevNet 沙盒
始终检查最新的 Cisco DevNet 页面以获取最新的设备信息、用户名和密码,因为自本书编写以来它们可能已更改。
控制器是网络的“大脑”,它对所有网络设备保持可见性:

图 3.6:Cisco ACI 控制器
我们可以使用网页浏览器登录到控制器并查看不同的租户:

图 3.7:Cisco ACI 租户
让我们使用 Python 交互式提示符来看看我们如何与 ACI 控制器交互。我们将首先导入正确的库,并定义目标 URL 以及登录凭证:
>>> import requests, json
>>> URL = 'https://sandboxapicdc.cisco.com'
>>> PASSWORD = "<password>"
>>> LOGIN = "admin"
>>> AUTH_URL = URL + '/api/aaaLogin.json'
然后,我们可以发出请求并将响应转换为 JSON 格式:
>>> r = requests.post(AUTH_URL, json={"aaaUser":{"attributes":{"name":LOGIN,"pwd":PASSWORD}}}, verify=False)
>>> r_json = r.json()
>>> r_json
{'totalCount': '1', 'imdata': [{'aaaLogin': {'attributes': {'token': _<skip>}
我们可以从响应中获取令牌并将其用作未来请求控制器的身份验证 cookie。在下面的示例中,我们查询控制器租户部分中看到的 cisco 租户:
>>> token = r_json["imdata"][0]["aaaLogin"]["attributes"]["token"]
>>> cookie = {'APIC-cookie':token}
>>> QUERY_URL = URL + '/api/node/class/fvTenant.json?query-target-filter=eq(fvTenant.name,"Cisco")'
>>> r_cisco = requests.get(QUERY_URL, cookies=cookie, verify=False)
>>> r_cisco.json()
{'totalCount': '1', 'imdata': [{'fvTenant': {'attributes': {'annotation': '', 'childAction': '', 'descr': '', 'dn': 'uni/tn-Cisco', 'extMngdBy': '', 'lcOwn': 'local', 'modTs': '2022-08-06T14:05:15.893+00:00', 'monPolDn': 'uni/tn-common/monepg-default', 'name': 'Cisco', 'nameAlias': '', 'ownerKey': '', 'ownerTag': '', 'status': '', 'uid': '15374', 'userdom': ':all:'}}}]}
>>> print(r_cisco.json()['imdata'][0]['fvTenant']['attributes']['dn'])
uni/tn-Cisco
如您所见,我们只查询单个控制器设备,但我们可以获取控制器所了解的所有网络设备的高级视图。这相当不错!当然,缺点是 ACI 控制器目前只支持思科设备。
Cisco IOS-XE
在很大程度上,Cisco IOS-XE 脚本与为 NX-OS 编写的脚本在功能上相似。IOS-XE 具有额外的功能,可以提升 Python 网络编程能力,例如机箱内 Python 和虚拟机壳,developer.cisco.com/docs/ios-xe/#!on-box-python-and-guestshell-quick-start-guide/onbox-python。
与 ACI 类似,Cisco Meraki 是一个集中式管理的控制器,它对多个有线和无线网络具有可见性。与 ACI 控制器不同,Meraki 是基于云的,因此它托管在本地场所之外。让我们在下一节中查看一些 Cisco Meraki 功能和示例。
Cisco Meraki 控制器
Cisco Meraki 是一个基于云的集中式控制器,简化了设备的 IT 管理。其方法与 ACI 非常相似,唯一的区别是控制器有一个基于云的公共 URL。用户通常通过 GUI 接收 API 密钥,然后可以在 Python 脚本中使用它来检索组织 ID:
#!/usr/bin/env python3
import requests
import pprint
myheaders={'X-Cisco-Meraki-API-Key': <skip>}
url ='https://dashboard.meraki.com/api/v0/organizations'
response = requests.get(url, headers=myheaders, verify=False)
pprint.pprint(response.json())
让我们执行脚本 cisco_meraki_1.py,这是一个简单的请求,针对的是由 Cisco DevNet 提供的始终开启的 Meraki 控制器:
(venv) $ python cisco_meraki_1.py
[{'id': '681155',
'name': 'DeLab',
'url': 'https://n6.meraki.com/o/49Gm_c/manage/organization/overview'},
{'id': '865776',
'name': 'Cisco Live US 2019',
'url': 'https://n22.meraki.com/o/CVQqTb/manage/organization/overview'},
{'id': '549236',
'name': 'DevNet Sandbox',
'url': 'https://n149.meraki.com/o/t35Mb/manage/organization/overview'},
{'id': '52636',
'name': 'Forest City - Other',
'url': 'https://n42.meraki.com/o/E_utnd/manage/organization/overview'}]
从那里,可以使用组织 ID 进一步检索信息,例如库存、网络信息等:
#!/usr/bin/env python3
import requests
import pprint
myheaders={'X-Cisco-Meraki-API-Key': <skip>}
orgId = '549236'
url = 'https://dashboard.meraki.com/api/v0/organizations/' + orgId + '/networks'
response = requests.get(url, headers=myheaders, verify=False)
pprint.pprint(response.json())
让我们看看 cisco_meraki_2.py 脚本的输出:
(venv) $ python cisco_meraki_2.py
<skip>
[{'disableMyMerakiCom': False,
'disableRemoteStatusPage': True,
'id': 'L_646829496481099586',
'name': 'DevNet Always On Read Only',
'organizationId': '549236',
'productTypes': ['appliance', 'switch'],
'tags': ' Sandbox ',
'timeZone': 'America/Los_Angeles',
'type': 'combined'},
{'disableMyMerakiCom': False,
'disableRemoteStatusPage': True,
'id': 'N_646829496481152899',
'name': 'test - mx65',
'organizationId': '549236',
'productTypes': ['appliance'],
'tags': None,
'timeZone': 'America/Los_Angeles',
'type': 'appliance'},
<skip>
我们已经看到了使用 NX-API、ACI 和 Meraki 控制器的 Cisco 设备的示例。在下一节中,让我们看看一些与 Juniper Networks 设备一起工作的 Python 示例。
Juniper Networks 的 Python API
Juniper Networks 一直受到服务提供商群体的喜爱。如果我们退后一步,看看服务提供商行业,那么自动化网络设备成为他们需求列表中的首要任务是有道理的。在云规模数据中心出现之前,服务提供商需要管理的网络设备最多。例如,一个典型的企业网络可能在总部有少量冗余互联网连接,以及一些通过使用私有 多协议标签交换(MPLS)网络连接回总部的星型远程站点。但对于服务提供商来说,他们需要构建、配置、管理和排除 MPLS 连接及其底层网络的故障。他们通过销售带宽以及增值管理服务来赚钱。对于服务提供商来说,投资自动化以使用最少的工程小时数来保持网络正常运行是有意义的。在他们的情况下,网络自动化是他们竞争优势的关键。
在我看来,服务提供商的网络需求与云数据中心之间的区别在于,传统上,服务提供商将更多服务聚合到单个设备中。一个很好的例子是 MPLS,几乎所有主要服务提供商都提供但很少在企业或数据中心网络中使用。Juniper Networks 已经识别了网络可编程性的需求,并在满足服务提供商自动化需求方面表现出色。让我们看看 Juniper 的自动化 API 的一些示例。
Juniper 和 NETCONF
NETCONF 是一个由 IETF 发布的标准,首次于 2006 年以 RFC 4741 的形式发布,后来在 RFC 6241 中进行了修订。Juniper Networks 对这两个 RFC 标准都做出了重大贡献。实际上,Juniper 是 RFC 4741 的唯一作者。Juniper 设备完全支持 NETCONF 是合情合理的,它还作为其大多数自动化工具和框架的底层层。NETCONF 的一些主要特性包括以下内容:
-
它使用 可扩展标记语言(XML)进行数据编码。
-
它使用 远程过程调用(RPC)。因此,如果 HTTP(s) 是传输协议,URL 端点是相同的,而操作意图则在请求体中指定。
-
它从上到下基于层结构。层包括内容、操作、消息和传输:

图 3.8:NETCONF 模型
Juniper Networks 在其技术库中提供了一个广泛的 NETCONF XML 管理协议开发者指南 (www.juniper.net/techpubs/en_US/junos13.2/information-products/pathway-pages/netconf-guide/netconf.html#overview)。让我们看看它的用法。
设备准备
要开始使用 NETCONF,让我们创建一个单独的用户以及开启所需的服务:
set system login user juniper uid 2001
set system login user juniper class super-user
set system login user juniper authentication encrypted-password "$1$0EkA.XVf$cm80A0GC2dgSWJIYWv7Pt1"
set system services ssh
set system services telnet
set system services netconf ssh port 830
对于 Juniper 设备实验室,我正在使用一个较旧的、不受支持的平台,称为 JunOS Olive。它仅用于实验室目的。你可以使用你喜欢的搜索引擎查找一些关于 Juniper Olive 的有趣事实和历史。
在 Juniper 设备上,你总是可以在平面文件或 XML 格式下查看配置。当需要指定一行命令来更改配置时,flat 文件会很有用:
netconf@foo> show configuration | display set
set version 12.1R1.9
set system host-name foo set system domain-name bar
<omitted>
当你需要查看配置的 XML 结构时,XML 格式会很有用:
netconf@foo> show configuration | display xml
<rpc-reply >
<configuration junos:commit-seconds="1485561328" junos:commit- localtime="2017-01-27 23:55:28 UTC" junos:commit-user="netconf">
<version>12.1R1.9</version>
<system>
<host-name>foo</host-name>
<domain-name>bar</domain-name>
我们在 Cisco NX-API 中的实验室软件安装和设备准备 部分安装了必要的 Linux 库和 ncclient Python 库。如果你还没有这样做,请回到该部分并安装必要的包。
我们现在可以查看我们的第一个 Juniper NETCONF 示例。
Juniper NETCONF 示例
我们将使用一个非常直接的例子来执行 show version。我们将把这个文件命名为 junos_netconf_1.py:
#!/usr/bin/env python3
from ncclient import manager
conn = manager.connect(
host='192.168.2.70',
port='830',
username='juniper',
password='juniper!',
timeout=10,
device_params={'name':'junos'},
hostkey_verify=False)
result = conn.command('show version', format='text')
print(result.xpath('output')[0].text)
conn.close_session()
脚本中的所有字段都应该相当直观,除了 device_params。从 ncclient 0.4.1 版本开始,设备处理器被添加来指定不同的供应商或平台。例如,名称可以是 Juniper、CSR、Nexus 或华为。我们还添加了 hostkey_verify=False,因为我们正在使用 Juniper 设备的自签名证书。
返回的输出是使用 XML 编码的 rpc-reply,其中包含输出元素:
<rpc-reply message-id="urn:uuid:7d9280eb-1384-45fe-be48- b7cd14ccf2b7">
<output>
Hostname: foo
Model: olive
JUNOS Base OS boot [12.1R1.9]
JUNOS Base OS Software Suite [12.1R1.9]
<omitted>
JUNOS Runtime Software Suite [12.1R1.9] JUNOS Routing Software Suite [12.1R1.9]
</output>
</rpc-reply>
我们可以解析 XML 输出,只包括输出文本:
print(result.xpath('output')[0].text)
在 junos_netconf_2.py 中,我们将对设备进行配置更改。我们将从一些新的导入开始,用于构建新的 XML 元素和连接管理器对象:
#!/usr/bin/env python3
from ncclient import manager
from ncclient.xml_ import new_ele, sub_ele
conn = manager.connect(host='192.168.2.70', port='830', username='juniper', password='juniper!', timeout=10, device_params={'name':'junos'}, hostkey_verify=False)
我们将锁定配置并做出配置更改:
# lock configuration and make configuration changes conn.lock()
# build configuration
config = new_ele('system')
sub_ele(config, 'host-name').text = 'master'
sub_ele(config, 'domain-name').text = 'python'
在构建配置部分,我们创建了一个新的 system 元素,并包含 host-name 和 domain-name 子元素。如果您想知道层次结构,您可以从 XML 显示中看到 system 节点是 host-name 和 domain-name 的父节点:
<system>
<host-name>foo</host-name>
<domain-name>bar</domain-name>
...
</system>
配置构建完成后,脚本将推送配置并提交配置更改。这些是 Juniper 配置更改的正常最佳实践步骤(lock、configure、unlock、commit)。
# send, validate, and commit config conn.load_configuration(config=config)
conn.validate()
commit_config = conn.commit()
print(commit_config.tostring)
# unlock config
conn.unlock()
# close session
conn.close_session()
总体来说,NETCONF 步骤与我们在 CLI 步骤中会执行的操作非常匹配。请查看 junos_netconf_3.py 脚本以获取更可重用的代码。以下示例结合了逐步示例和一些 Python 函数:
# make a connection object
def connect(host, port, user, password):
connection = manager.connect(host=host, port=port,
username=user, password=password, timeout=10,
device_params={'name':'junos'}, hostkey_verify=False)
return connection
# execute show commands
def show_cmds(conn, cmd):
result = conn.command(cmd, format='text')
return result
# push out configuration
def config_cmds(conn, config):
conn.lock()
conn.load_configuration(config=config)
commit_config = conn.commit()
return commit_config.tostring
此文件可以独立执行,也可以导入到其他 Python 脚本中使用。
Juniper 还提供了一个名为 PyEZ 的 Python 库,用于与其设备一起使用。我们将在下一节中查看一些使用该库的示例。
Juniper PyEZ 开发者指南
PyEZ 是一个高级 Python 库实现,与现有的 Python 代码集成得更好。通过利用围绕底层配置的 Python API,您可以在不深入了解 Junos CLI 的情况下执行常见操作和配置任务。
Juniper 在其技术库中维护了一个全面的 Junos PyEZ 开发者指南,网址为 www.juniper.net/techpubs/en_US/junos-pyez1.0/information-products/pathway-pages/junos-pyez-developer-guide.html#configuration。如果您对使用 PyEZ 感兴趣,我强烈建议至少浏览一下指南中的各个主题。
安装和准备
每个操作系统的安装说明可以在 安装 Junos PyEZ (www.juniper.net/techpubs/en_US/junos-pyez1.0/topics/task/installation/junos-pyez-server-installing.html) 页面上找到。
PyEZ 软件包可以通过 pip 进行安装:
(venv) $ pip install junos-eznc
在 Juniper 设备上,需要将 NETCONF 配置为 PyEZ 的底层 XML API:
set system services netconf ssh port 830
对于用户认证,我们可以使用密码认证或 SSH 密钥对。我们可以创建新用户或使用现有用户。对于 ssh 密钥认证,首先,如果你的管理主机尚未为 第二章,低级网络设备交互 生成密钥对,请先生成密钥对:
$ ssh-keygen -t rsa
默认情况下,公钥将在 ~/.ssh/ 下被命名为 id_rsa.pub,而私钥将在同一目录下命名为 id_rsa。将私钥视为你永远不会分享的密码。公钥可以自由分发。在我们的用例中,我们将公钥复制到 /tmp 目录,并启用 Python 3 HTTP 服务器模块以创建一个可访问的 URL:
(venv) $ cp ~/.ssh/id_rsa.pub /tmp
(venv) $ cd /tmp
(venv) $ python3 -m http.server
(venv) Serving HTTP on 0.0.0.0 port 8000 ...
从 Juniper 设备,我们可以创建用户并关联公钥,通过从 Python 3 网络服务器下载公钥:
netconf@foo# set system login user echou class super-user authentication load-key-file http://<management host ip>:8000/id_rsa.pub
/var/home/netconf/...transferring.file........100% of 394 B 2482 kBps
现在,如果我们尝试从管理站使用私钥 ssh,用户将被自动认证:
(venv) $ ssh -i ~/.ssh/id_rsa <Juniper device ip>
--- JUNOS 12.1R1.9 built 2012-03-24 12:52:33 UTC
echou@foo>
让我们确保两种认证方法都能与 PyEZ 一起工作。让我们尝试用户名和密码组合:
>>> from jnpr.junos import Device
>>> dev = Device(host='<Juniper device ip, in our case 192.168.2.70>', user='juniper', password='juniper!')
>>> dev.open()
Device(192.168.2.70)
>>> dev.facts
{'serialnumber': '', 'personality': 'UNKNOWN', 'model': 'olive', 'ifd_style': 'CLASSIC', '2RE': False, 'HOME': '/var/home/juniper', 'version_info': junos.version_info(major=(12, 1), type=R, minor=1, build=9), 'switch_style': 'NONE', 'fqdn': 'foo.bar', 'hostname': 'foo', 'version': '12.1R1.9', 'domain': 'bar', 'vc_capable': False}
>>> dev.close()
我们还可以尝试使用 SSH 密钥认证:
>>> from jnpr.junos import Device
>>> dev1 = Device(host='192.168.2.70', user='echou', ssh_private_key_file='/home/echou/.ssh/id_rsa')
>>> dev1.open()
Device(192.168.2.70)
>>> dev1.facts
{'HOME': '/var/home/echou', 'model': 'olive', 'hostname': 'foo', 'switch_style': 'NONE', 'personality': 'UNKNOWN', '2RE': False, 'domain': 'bar', 'vc_capable': False, 'version': '12.1R1.9', 'serialnumber': '', 'fqdn': 'foo.bar', 'ifd_style': 'CLASSIC', 'version_info': junos.version_info(major=(12, 1), type=R, minor=1, build=9)}
>>> dev1.close()
太好了!我们现在可以看看 PyEZ 的几个示例。
PyEZ 示例
在之前的交互式提示中,我们看到当设备连接时,对象会自动检索一些关于设备的事实。在我们的第一个例子 junos_pyez_1.py 中,我们正在连接到设备并执行 show interface em1 的 RPC 调用:
#!/usr/bin/env python3
from jnpr.junos import Device
import xml.etree.ElementTree as ET
import pprint
dev = Device(host='192.168.2.70', user='juniper', passwd='juniper!')
try:
dev.open()
except Exception as err:
print(err)
sys.exit(1)
result = dev.rpc.get_interface_information(interface_name='em1', terse=True)
pprint.pprint(ET.tostring(result))
dev.close()
Device 类有一个包含所有操作命令的 rpc 属性。这很棒,因为我们可以在 CLI 和 API 之间没有滑动。但是,我们需要找到 CLI 命令对应的 xml rpc 元素标签。在我们的第一个例子中,我们如何知道 show interface em1 等同于 get_interface_information?我们有三种方法来找出这些信息:
-
我们可以参考 Junos XML API 操作开发者参考。
-
我们可以使用 CLI 并显示 XML RPC 等价物,并将单词之间的破折号(-)替换为下划线(_)。
-
我们也可以通过使用 PyEZ 库来编程实现。
我通常使用第二种方法直接获取输出:
netconf@foo> show interfaces em1 | display xml rpc
<rpc-reply >
<rpc>
<get-interface-information>
<interface-name>em1</interface-name>
</get-interface-information>
</rpc>
<cli>
<banner></banner>
</cli>
</rpc-reply>
这里是使用 PyEZ 编程的示例(第三种方法):
>>> dev1.display_xml_rpc('show interfaces em1', format='text')
'<get-interface-information>/n <interface-name>em1</interface- name>/n</get-interface-information>/n'
当然,我们也可以进行配置更改。在 junos_pyez_2.py 配置示例中,我们将从 PyEZ 导入一个额外的 Config() 方法:
#!/usr/bin/env python3
from jnpr.junos import Device
from jnpr.junos.utils.config import Config
我们将使用相同的块来连接到设备:
dev = Device(host='192.168.2.70', user='juniper',
passwd='juniper!')
try:
dev.open()
except Exception as err:
print(err)
sys.exit(1)
new Config() 方法将加载 XML 数据并执行配置更改:
config_change = ""
<system>
<host-name>master</host-name>
<domain-name>python</domain-name>
</system>
""
cu = Config(dev)
cu.lock()
cu.load(config_change)
cu.commit()
cu.unlock()
dev.close()
PyEZ 示例设计得很简单。希望它们能展示你如何利用 PyEZ 满足你的 Junos 自动化需求。在下面的例子中,让我们看看我们如何使用 Python 库与 Arista 网络设备一起工作。
Arista Python API
Arista Networks 一直专注于大规模数据中心网络。在其公司简介页面上(www.arista.com/en/company/company-overview),它声明如下:
“Arista Networks 是大型数据中心、校园和路由环境的数据驱动、客户端到云网络行业的领导者。”
注意,该声明特别提到了大型数据中心,我们知道那里服务器、数据库和网络设备都在爆炸式增长。自动化一直是 Arista 的领先特性之一是有道理的。事实上,它的操作系统背后有 Linux 的支持,这带来了许多附加的好处,例如 Linux 命令和平台上的内置 Python 解释器。从第一天起,Arista 就公开了将 Linux 和 Python 功能暴露给网络操作员。
与其他供应商一样,您可以直接通过 eAPI 与 Arista 设备交互,或者您可以选择利用他们的 Python 库。在本章中,我们将看到这两个示例。
Arista eAPI 管理
几年前,Arista 的 eAPI 首次在 EOS 4.12 中引入。它通过 HTTP 或 HTTPS 传输一组显示或配置命令,并以 JSON 格式响应。一个重要的区别是,它是一个 RPC 和JSON-RPC,而不是在 HTTP 或 HTTPS 上提供的纯 RESTful API。区别在于我们使用相同的 HTTP 方法(POST)向相同的 URL 端点发出请求。但与使用 HTTP 动词(GET、POST、PUT、DELETE)来表示我们的操作不同,我们只需在请求体中声明我们的预期操作。在 eAPI 的情况下,我们将指定一个具有runCmds值的method键。
在以下示例中,我使用的是运行 EOS 4.16 的物理 Arista 交换机。
eAPI 准备
Arista 设备上的 eAPI 代理默认是禁用的,因此在使用它之前,我们需要在设备上启用它:
arista1(config)#management api http-commands
arista1(config-mgmt-api-http-cmds)#no shut
arista1(config-mgmt-api-http-cmds)#protocol https port 443
arista1(config-mgmt-api-http-cmds)#no protocol http
arista1(config-mgmt-api-http-cmds)#vrf management
如您所见,我们已经关闭了 HTTP 服务器,并使用 HTTPS 作为唯一的传输方式。默认情况下,管理接口位于名为management的 VRF 中。在我的拓扑中,我通过管理接口访问设备;因此,我为 eAPI 管理指定了 VRF。
您可以通过show management api http-commands命令检查 API 管理状态:
arista1#sh management
api http-commands Enabled: Yes
HTTPS server: running, set to use port 443 HTTP server: shutdown, set to use port 80
Local HTTP server: shutdown, no authentication, set to use port 8080
Unix Socket server: shutdown, no authentication
VRF: management
Hits: 64
Last hit: 33 seconds ago Bytes in: 8250
Bytes out: 29862
Requests: 23
Commands: 42
Duration: 7.086
seconds SSL Profile: none
QoS DSCP: 0
User Requests Bytes in Bytes out Last hit
----------- -------------- -------------- --------------- -----------
admin 23 8250 29862 33 seconds ago
URLs
-----------------------------------------
Management1 : https://192.168.199.158:443
arista1#
启用代理后,我们可以在网页浏览器中通过访问设备的 IP 地址来访问 eAPI 的探索页面。如果您已更改了默认的访问端口,只需将其附加即可。认证与交换机上设置的认证方式相关联。我们将使用设备上配置的本地用户名和密码。默认情况下,将使用自签名证书:

图 3.9:Arista EOS 探索
我们将被带到探索页面,在那里我们可以输入 CLI 命令,并获得我们请求主体的良好输出。例如,如果我想查看如何为show version创建请求主体,这就是我在探索器中看到的输出:

图 3.10:Arista EOS 探索查看器
概览链接将带我们到示例使用和背景信息,而命令文档将作为 show 命令的参考点。每个命令参考将包含返回值字段名、类型和简要描述。Arista 的在线参考脚本使用jsonrpclib(github.com/joshmarshall/jsonrpclib/),我们将使用它。
本节中的示例主要使用 Python 2.7,因为 jsonrpclib 长时间未移植到 Python 3。然而,根据 GitHub 拉取请求github.com/joshmarshall/jsonrpclib/issues/38,Python 3 支持应该被包含。
使用pip进行安装非常直接:
(venv) $ pip install jsonrpclib
eAPI 示例
然后,我们可以编写一个简单的程序,名为eapi_1.py,来查看响应文本:
#!/usr/bin/python2
from __future__ import print_function
from jsonrpclib import Server
import ssl
ssl._create_default_https_context = ssl._create_unverified_context
switch = Server("https://admin:arista@192.168.199.158/command-api")
response = switch.runCmds( 1, [ "show version" ] )
print('Serial Number: ' + response[0]['serialNumber'])
这是之前runCmds()方法返回的响应:
[{u'memTotal': 3978148, u'internalVersion': u'4.16.6M- 3205780.4166M', u'serialNumber': u'<omitted>', u'systemMacAddress': u'<omitted>', u'bootupTimestamp': 1465964219.71, u'memFree': 277832, u'version': u'4.16.6M', u'modelName': u'DCS-7050QX-32-F', u'isIntlVersion': False, u'internalBuildId': u'373dbd3c-60a7-4736-8d9e-bf5e7d207689', u'hardwareRevision': u'00.00', u'architecture': u'i386'}]
如您所见,结果是包含一个字典项的列表。如果我们需要获取序列号,我们可以简单地引用项号和键:
print('Serial Number: ' + response[0]['serialNumber'])
输出将只包含序列号:
$ python eapi_1.py
Serial Number: <omitted>
要更熟悉命令参考,我建议点击 eAPI 页面上的命令文档链接,并将我们的输出与文档中show version的输出进行比较。
如前所述,与 REST 不同,JSON-RPC 客户端使用相同的 URL 端点来调用服务器资源。我们可以从之前的示例中看到runCmds()方法包含一个命令列表。对于配置命令的执行,你可以遵循相同的步骤,并通过命令列表配置设备。
这里是一个名为eapi_2.py的文件中的配置命令示例。在我们的例子中,我们编写了一个函数,该函数将switch对象和命令列表作为属性:
#!/usr/bin/python2
from __future__ import print_function
from jsonrpclib import Server
import ssl, pprint
ssl._create_default_https_context = ssl._create_unverified_context
# Run Arista commands thru eAPI
def runAristaCommands(switch_object, list_of_commands):
response = switch_object.runCmds(1, list_of_commands)
return response
switch = Server("https://admin:arista@192.168.199.158/command-api")
commands = ["enable", "configure", "interface ethernet 1/3", "switchport access vlan 100", "end", "write memory"]
response = runAristaCommands(switch, commands)
pprint.pprint(response)
这是命令执行输出的结果:
$ python2 eapi_2.py
[{}, {}, {}, {}, {}, {u'messages': [u'Copy completed successfully.']}]
现在,快速检查switch以验证命令的执行情况:
arista1#sh run int eth 1/3
interface Ethernet1/3
switchport access vlan 100
arista1#
总体来说,eAPI 相当直观且易于使用。大多数编程语言都有类似于jsonrpclib的库,这些库抽象了 JSON-RPC 的内部机制。只需几个命令,你就可以开始将 Arista EOS 自动化集成到你的网络中。
Arista Pyeapi 库
Python 客户端库 Pyeapi(pyeapi.readthedocs.io/en/master/index.html)是围绕 eAPI 的本地 Python 库包装器。它提供了一套绑定来配置 Arista EOS 节点。为什么我们需要 Pyeapi,因为我们已经有了 eAPI?答案是“这取决于。”在 Pyeapi 和 eAPI 之间进行选择主要是一个判断决定。
如果你在一个非 Python 环境中,eAPI 可能是更好的选择。从我们的示例中,你可以看到 eAPI 的唯一要求是一个支持 JSON-RPC 的客户端。因此,它与大多数编程语言兼容。当我最初进入这个领域时,Perl 是脚本和网络自动化的主导语言。仍然有许多企业依赖于 Perl 脚本作为它们的主要自动化工具。如果你处于公司已经投入了大量资源且代码库不是 Python 语言的情况,使用带有 JSON-RPC 的 eAPI 将是一个不错的选择。
然而,对于我们这些喜欢用 Python 编程的人来说,像 Pyeapi 这样的本地 Python 库意味着在编写代码时感觉更自然。它确实使得将 Python 程序扩展以支持 EOS 节点变得更加容易。它也使得跟上 Python 的最新变化变得更加容易。例如,我们可以使用 Python 3 与 Pyeapi 一起使用(pyeapi.readthedocs.io/en/master/requirements.html)!
Pyeapi 安装
使用pip安装非常简单:
(venv) $ pip install pyeapi
注意,pip 也会安装netaddr库,因为它是 Pyeapi 的指定要求之一(pyeapi.readthedocs.io/en/master/requirements.html)。
默认情况下,Pyeapi 客户端将在你的主目录中查找一个名为eapi.conf的 INI 风格的隐藏(前面带有点)文件。你可以通过指定eapi.conf文件路径来覆盖此行为。通常,将连接凭证与脚本本身分离并锁定是一个好主意。你可以查看 Arista Pyeapi 文档(pyeapi.readthedocs.io/en/master/configfile.html#configfile)以了解文件中包含的字段。
这里是我在实验室中使用的文件:
cat ~/.eapi.conf
[connection:Arista1]
host: 192.168.199.158
username: admin
password: arista
transport: https
第一行[connection:Arista1]包含我们将用于 Pyeapi 连接的名称;其余字段应该相当直观。你可以锁定文件,使其对使用此文件的用户为只读:
$ chmod 400 ~/.eapi.conf
$ ls -l ~/.eapi.conf
-r-------- 1 echou echou 94 Jan 27 18:15 /home/echou/.eapi.conf
现在 Pyeapi 已经安装好了,让我们来看一些示例。
Pyeapi 示例
让我们从通过在交互式 Python shell 中创建一个对象来连接到 EOS 节点开始:
>>> import pyeapi
>>> arista1 = pyeapi.connect_to('Arista1')
我们可以向节点执行show命令并接收输出:
>>> import pprint
>>> pprint.pprint(arista1.enable('show hostname'))
[{'command': 'show hostname',
'encoding': 'json',
'result': {'fqdn': 'arista1', 'hostname': 'arista1'}}]
配置字段可以是单个命令或使用config()方法的命令列表:
>>> arista1.config('hostname arista1-new')
[{}]
>>> pprint.pprint(arista1.enable('show hostname'))
[{'command': 'show hostname',
'encoding': 'json',
'result': {'fqdn': 'arista1-new', 'hostname': 'arista1-new'}}]
>>> arista1.config(['interface ethernet 1/3', 'description my_link']) [{}, {}]
注意,命令缩写(show run与show running-config)和一些扩展可能不会工作:
>>> pprint.pprint(arista1.enable('show run'))
Traceback (most recent call last):
...
File "/usr/local/lib/python3.5/dist-packages/pyeapi/eapilib.py", line 396, in send
raise CommandError(code, msg, command_error=err, output=out) pyeapi.eapilib.CommandError: Error [1002]: CLI command 2 of 2 'show run' failed: invalid command [incomplete token (at token 1: 'run')]
>>>
>>> pprint.pprint(arista1.enable('show running-config interface ethernet 1/3'))
Traceback (most recent call last):
...
pyeapi.eapilib.CommandError: Error [1002]: CLI command 2 of 2 'show running-config interface ethernet 1/3' failed: invalid command [incomplete token (at token 2: 'interface')]
我们可以始终捕获结果并获取所需值:
>>> result = arista1.enable('show running-config')
>>> pprint.pprint(result[0]['result']['cmds']['interface Ethernet1/3'])
{'cmds': {'description my_link': None, 'switchport access vlan 100': None}, 'comments': []}
到目前为止,我们一直在使用 eAPI 对show和configuration命令进行操作。Pyeapi 提供了各种 API 来简化生活。在下面的示例中,我们将连接到节点,调用 VLAN API,并开始对设备的 VLAN 参数进行操作。让我们看看:
>>> import pyeapi
>>> node = pyeapi.connect_to('Arista1')
>>> vlans = node.api('vlans')
>>> type(vlans)
<class 'pyeapi.api.vlans.Vlans'>
>>> dir(vlans)
[...'command_builder', 'config', 'configure', 'configure_interface', 'configure_vlan', 'create', 'default', 'delete', 'error', 'get', 'get_block', 'getall', 'items', 'keys', 'node', 'remove_trunk_group', 'set_name', 'set_state', 'set_trunk_groups', 'values']
>>> vlans.getall()
{'1': {'vlan_id': '1', 'trunk_groups': [], 'state': 'active', 'name': 'default'}}
>>> vlans.get(1)
{'vlan_id': 1, 'trunk_groups': [], 'state': 'active', 'name': 'default'}
>>> vlans.create(10) True
>>> vlans.getall()
{'1': {'vlan_id': '1', 'trunk_groups': [], 'state': 'active', 'name':
'default'}, '10': {'vlan_id': '10', 'trunk_groups': [], 'state': 'active', 'name': 'VLAN0010'}}
>>> vlans.set_name(10, 'my_vlan_10') True
让我们验证设备上是否已创建了 VLAN 10:
arista1#sh vlan
VLAN Name Status Ports
----- -------------------------------- --------- --------------------
-----
1 default active
10 my_vlan_10 active
如我们所见,EOS 对象上的 Python 原生 API 是 Pyeapi 在 eAPI 之上的优势所在。它将底层属性抽象到设备对象中,使代码更简洁、更容易阅读。
要查看 Pyeapi API 的完整列表,请参阅官方文档(pyeapi.readthedocs.io/en/master/api_modules/_list_of_modules.html)。
为了总结本节,让我们假设我们重复之前的步骤足够多次,以至于我们希望编写另一个 Python 类来节省我们的工作。
pyeapi_1.py 脚本如下所示:
#!/usr/bin/env python3
import pyeapi
class my_switch():
def __init__(self, config_file_location, device):
# loads the config file
pyeapi.client.load_config(config_file_location)
self.node = pyeapi.connect_to(device)
self.hostname = self.node.enable('show hostname')[0]['result']['hostname']
self.running_config = self.node.enable('show running-config')
def create_vlan(self, vlan_number, vlan_name):
vlans = self.node.api('vlans')
vlans.create(vlan_number)
vlans.set_name(vlan_number, vlan_name)
从脚本中我们可以看到,我们自动连接到节点,设置主机名,并在连接时加载 running_config。我们还创建了一个使用 VLAN API 创建 VLAN 的类方法。让我们在交互式外壳中尝试运行这个脚本:
>>> import pyeapi_1
>>> s1 = pyeapi_1.my_switch('/tmp/.eapi.conf', 'Arista1')
>>> s1.hostname
'arista1'
>>> s1.running_config
[{'encoding': 'json', 'result': {'cmds': {'interface Ethernet27': {'cmds':
{}, 'comments': []}, 'ip routing': None, 'interface face Ethernet29':
{'cmds': {}, 'comments': []}, 'interface Ethernet26': {'cmds': {}, 'comments': []}, 'interface Ethernet24/4': h.':
<omitted>
'interface Ethernet3/1': {'cmds': {}, 'comments': []}}, 'comments': [],
'header': ['! device: arista1 (DCS-7050QX-32, EOS-4.16.6M)n!n']},
'command': 'show running-config'}]
>>> s1.create_vlan(11, 'my_vlan_11')
>>> s1.node.api('vlans').getall()
{'11': {'name': 'my_vlan_11', 'vlan_id': '11', 'trunk_groups': [], 'state':
'active'}, '10': {'name': 'my_vlan_10', 'vlan_id': '10', 'trunk_groups': [], 'state': 'active'}, '1': {'name': 'default', 'vlan_id': '1', 'trunk_groups': [], 'state': 'active'}}
>>>
我们现在已经研究了网络领域三大供应商的 Python 脚本:思科系统、Juniper 网络和 Arista 网络。在下一节中,我们将探讨一个在相同领域获得一些动力的开源网络操作系统。
VyOS 示例
VyOS 是一个完全开源的网络操作系统,它可以在广泛的硬件、虚拟机和云服务提供商上运行([vyos.io/](https://vyos.io/))。由于其开源特性,它在开源社区中获得了广泛的支持。许多开源项目都将 VyOS 作为默认的测试平台。在章节的最后部分,我们将查看一个简短的 VyOS 示例。
VyOS 映像可以以各种格式下载:https://wiki.vyos.net/wiki/Installation。一旦下载并初始化,我们就可以在我们的管理主机上安装 Python 库:
(venv) $ pip install vymgmt
示例脚本 vyos_1.py 非常简单:
#!/usr/bin/env python3
import vymgmt
vyos = vymgmt.Router('192.168.2.116', 'vyos', password='vyos')
vyos.login()
vyos.configure()
vyos.set("system domain-name networkautomationnerds.net")
vyos.commit()
vyos.save()
vyos.exit()
vyos.logout()
我们可以执行脚本以更改系统域名:
(venv) $ python vyos_1.py
我们可以登录到设备以验证域名更改:
vyos@vyos:~$ show configuration | match domain
domain-name networkautomationnerds.net
如示例所示,我们使用的 VyOS 方法与其他来自专有供应商的示例非常相似。这主要是出于设计考虑,因为它们提供了一种从使用其他供应商设备到开源 VyOS 的便捷过渡。我们接近章节的结尾。还有一些其他库值得提及,并在开发中保持关注,我们将在下一节中介绍。
其他库
我们将通过提及几个关于供应商中立库的杰出努力来结束这一章,例如 Nornir (nornir.readthedocs.io/en/stable/index.html)、Netmiko (github.com/ktbyers/netmiko)、NAPALM (github.com/napalm-automation/napalm) 和 Scrapli (carlmontanari.github.io/scrapli/)。我们在上一章中看到了它们的一些示例。这些供应商中立库中大多数可能需要更长时间来支持最新的平台或功能。然而,由于这些库是供应商中立的,如果你不喜欢工具的供应商锁定,这些库是不错的选择。使用供应商中立库的另一个好处是它们通常是开源的,因此你可以为新的功能和错误修复做出贡献。
摘要
在本章中,我们探讨了与 Cisco、Juniper、Arista 和 Vyatta 网络设备通信和管理的方法。我们探讨了与 NETCONF 和 REST 的直接通信,以及使用供应商提供的库,如 PyEZ 和 Pyeapi。这些都是不同层次的抽象,旨在提供一种无需人工干预即可通过编程管理网络设备的方法。
在 第四章,Python 自动化框架 中,我们将探讨一个更高层次的供应商中立抽象框架,称为 Ansible。Ansible 是一个用 Python 编写的开源通用自动化工具。它可以用于自动化服务器、网络设备、负载均衡器等等。当然,对于我们的目的,我们将专注于使用这个自动化框架来管理网络设备。
加入我们的图书社区
要加入这本书的社区——在那里你可以分享反馈、向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第四章:Python 自动化框架 – Ansible
前两章逐步介绍了与网络设备交互的不同方式。在 第二章,低级网络设备交互 中,我们讨论了 Pexpect 和 Paramiko 库,它们管理交互会话以控制交互。在 第三章,APIs 和意图驱动型网络 中,我们开始从 API 和意图的角度思考我们的网络。我们研究了包含良好定义的命令结构并提供从设备获取反馈的结构的各种 API。当我们从 第二章,低级网络设备交互 转到 第三章,APIs 和意图驱动型网络 时,我们开始思考我们的网络意图。我们逐渐开始将我们的网络表达为代码。
在本章中,让我们进一步探讨将我们的意图转化为网络需求的想法。如果你从事过网络设计工作,那么最具有挑战性的部分可能不是不同的网络设备,而是对业务需求的评估和将它们转化为实际网络设计。你的网络设计需要解决业务问题。例如,你可能在需要适应在高峰时段出现网站响应时间缓慢的繁荣在线电子商务网站的大型基础设施团队中工作。你如何确定网络是否是问题所在?如果网站上的缓慢响应确实是由于网络拥塞造成的,你应该升级网络的哪个部分?系统的其他部分能否利用更高的速度和吞吐量?
以下图示展示了将我们的业务需求转化为网络设计时可能经历的一个简单流程步骤:

图 4.1:业务逻辑到网络部署
在我看来,网络自动化不仅仅是关于更快的配置更改。它还应该在准确和可靠地将我们的意图转化为设备行为的同时解决业务问题。这些是我们进行网络自动化之旅时应该牢记的目标。在本章中,我们将探讨一个基于 Python 的框架,称为 Ansible,它允许我们声明我们的网络意图,并从 API 和 CLI 中进一步抽象化。
在本章中,我们将探讨以下主题:
-
Ansible 简介
-
Ansible 的优势
-
Ansible 架构
-
Ansible 高级主题
让我们从 Ansible 框架的概述开始。
Ansible – 一个更声明式的框架
想象一下一个假设的情况:你在一个寒冷的早晨从关于潜在网络安全漏洞的恶梦中惊醒。你意识到你的网络中包含应该受到保护的宝贵数字资产。你一直担任网络管理员的工作,所以网络相当安全,但你只是想确保在你的网络设备周围增加更多的安全措施。
首先,您将目标分解为两个可执行项:
-
将设备升级到软件的最新版本。步骤包括以下内容:
-
将图片上传到设备
-
指示设备从新镜像启动
-
继续重启设备
-
核实设备正在运行新的软件镜像
-
-
在网络设备上配置适当的访问控制列表,包括以下内容:
-
在设备上构建访问列表
-
在接口配置部分配置访问列表
-
作为一位专注于自动化的网络工程师,你想编写脚本来可靠地配置设备并从操作中获取反馈。你开始研究每个步骤所需的必要命令和 API,然后在实验室中验证它们,最后在生产环境中部署它们。在操作系统升级和 ACL 部署方面做了大量工作后,你希望这些脚本可以转移到下一代设备上。
如果有一个工具可以缩短这个设计-开发-部署周期,那岂不是很好?在本章中,我们将使用一个名为 Ansible 的开源自动化框架。这是一个可以将从业务逻辑到完成工作简化过程的框架,而无需陷入特定的网络命令。它可以配置系统、部署软件和编排一系列任务。
Ansible 是用 Python 编写的,已成为 Python 开发者领先的自动化工具之一。它也是网络供应商支持最多的自动化框架之一。在 JetBrains 的‘Python Developers Survey 2020’调查中,Ansible 在配置管理工具中排名#1:

图 4.2:Python Developers Survey 2020 结果(来源:https://www.jetbrains.com/lp/python-developers-survey-2020/)
自从 2.10 版本以来,Ansible 已经将ansible-core和社区包的发布计划分开。这有点令人困惑,所以让我们看看它们之间的区别。
Ansible 版本
在版本 2.9 之前,Ansible 有一个相当直接的版本控制系统,从 2.5、2.6、2.7 等版本开始(docs.ansible.com/ansible/latest/roadmap/old_roadmap_index.html)。从版本 2.10 开始,我们看到 Ansible 项目从 2.10、3.0、4.0 等版本跳跃(docs.ansible.com/ansible/latest/roadmap/ansible_roadmap_index.html#ansible-roadmap)。这是怎么回事?Ansible 团队希望将核心引擎、模块和插件与更广泛的社区维护的模块和插件分开。这允许核心团队在核心功能上更快地移动,同时为社区留出时间来跟上其代码的维护。
当我们谈论“Ansible”时,我们指的是该级别的社区包集合,比如说,版本 3.0。在这个版本中,它将指定所需的ansible-core(最初称为ansible-base)版本。例如,Ansible 3.0 需要 ansible-core 2.10 及以上版本,而 Ansible 4.0 需要 ansible-core 2.11+版本。在这个结构中,我们可以将 ansible-core 升级到最新版本,同时如果需要,可以保持社区包在较旧版本中。
如果想了解更多关于版本分割的信息,Ansible 在其首次采用 Ansible 3.0 时提供了一个有用的问答页面,www.ansible.com/blog/ansible-3.0.0-qa。
让我们继续前进,看看一个 Ansible 示例。
我们的第一篇 Ansible 网络示例
Ansible 是一个 IT 自动化工具。其主要属性是简单易用,组件最少。它以无代理的方式管理机器(关于这一点稍后会有更多介绍),并依赖于现有的操作系统凭证和远程 Python 软件来运行其代码。Ansible 安装在称为控制节点的中央机器上,并在它希望控制的机器上执行,称为受管理节点。

图 4.3:Ansible 架构(来源:https://docs.ansible.com/ansible/latest/getting_started/index.html)
与大多数 IT 基础设施自动化一样,Ansible 最初是通过管理服务器开始的。大多数服务器都安装了 Python 或者能够运行 Python 代码;Ansible 会利用这一特性,通过将代码推送到受管理的节点,并在受管理的节点上本地运行。然而,正如我们所知,大多数网络设备无法运行原生 Python 代码;因此,当涉及到网络自动化时,Ansible 配置首先在本地运行,然后再对远程设备进行更改。
想了解更多关于网络自动化差异的信息,请查看 Ansible 的这篇文档,docs.ansible.com/ansible/latest/network/getting_started/network_differences.html。
让我们在控制节点上安装 Ansible。
控制节点安装
我们将在实验室的 Ubuntu 宿主机上安装 Ansible。控制节点仅有的要求是 Python 3.8 或更高版本以及 Python 的pip包管理系统。
(venv) $ pip install ansible
我们可以通过使用‘—version’开关来检查已安装的 Ansible 版本以及其他包级别信息:
(venv) $ ansible --version
ansible [core 2.13.3]
config file = None
configured module search path = ['/home/echou/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /home/echou/Mastering_Python_Networking_Fourth_Edition/venv/lib/python3.10/site-packages/ansible
ansible collection location = /home/echou/.ansible/collections:/usr/share/ansible/collections
executable location = /home/echou/Mastering_Python_Networking_Fourth_Edition/venv/bin/ansible
python version = 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0]
jinja version = 3.1.2
libyaml = True
如果你对在特定的操作系统上使用各自的包管理系统安装 Ansible 感兴趣,请参阅 Ansible 文档,docs.ansible.com/ansible/latest/installation_guide/installation_distros.html。
输出中显示了一些重要信息。最重要的是 Ansible 核心版本(2.13.3)和配置文件(目前没有)。这就是我们开始使用 Ansible 所需的所有信息,我们可以开始构建我们的第一个自动化任务。
实验室拓扑
已知 Ansible 有多种不同的方式来完成同一任务。例如,我们可以在不同的位置定义 Ansible 配置文件。我们还可以在 inventory、playbooks、roles 和命令行等多种地方指定特定宿主机的变量。这对刚开始使用 Ansible 的人来说可能有些令人困惑。在本章中,我将使用我认为最有意义的一种方式来做事情。一旦我们学会了基础知识,我们总是可以查阅文档来了解其他完成任务的方法。
对于第一个示例,我们将使用我们一直在使用的相同实验室拓扑,并对两个 IOSv 设备lax-edg-r1和lax-edg-r2运行任务。

图 4.4:实验室拓扑
我们首先需要考虑的是如何定义我们想要管理的宿主机。在 Ansible 中,我们使用一个名为 inventory 的文件来定义我们打算管理的宿主机。让我们创建一个名为hosts的文件,并将以下文本放入该文件中:
[ios_devices]
iosv-1
iosv-2
这种类型的文件是 INI 格式(en.wikipedia.org/wiki/INI_file),它声明我有一个名为ios_devices的设备组,其成员包括iosv-1和iosv-2。
我们现在应该指定与每个宿主机相关的特定变量。
变量文件
我们可以在许多地方放置与宿主机相关的变量。让我们创建一个名为host_vars的文件夹,并创建两个文件,其名称与 inventory 文件中指定的宿主机名称相同。目录和文件名很重要,因为 Ansible 就是通过这种方式将变量与宿主机匹配的。以下是目录和该目录内文件的输出:
$ tree host_vars/
host_vars/
├── iosv-1
└── iosv-2
文件是我们将放置属于宿主机必要信息的地点。例如,我们可以指定 IP 地址、用户名、密码和其他信息。以下是我们的实验室中iosv-1文件的输出:
$ cat host_vars/iosv-1
---
ansible_host: 192.168.2.51
ansible_user: cisco
ansible_ssh_pass: cisco
ansible_connection: network_cli
ansible_network_os: ios
ansbile_become: yes
ansible_become_method: enable
ansible_become_pass: cisco
此文件采用 YAML 格式(docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html)。符号‘---’表示文档的开始。在开始符号下方,有许多键值对。所有键都以ansible开头,键与值之间用冒号分隔。ansible_host、ansible_user和ansible_ssh_pass应更改为与您自己的实验室匹配的值。我如何知道这些名称?Ansible 文档在这里是我们的好朋友。Ansible 在其文档中列出了命名这些参数的标准方式,docs.ansible.com/ansible/latest/user_guide/intro_inventory.html。
在 Ansible 2.8 版本之前,网络模块没有标准的方式来命名参数,这非常令人困惑。从 2.8 版本开始,网络模块在标准化参数方面变得更好,与 Ansible 的其他模块保持一致。
一旦我们定义了主机变量的相应文件,我们就可以开始构建 Ansible 剧本了。
我们的第一份剧本
剧本(Playbook)是 Ansible 描述对受管理节点进行操作蓝图的方式,使用模块来实现。这是我们作为操作员使用 Ansible 时将花费大部分时间的地方。什么是模块?从简化的角度来看,模块是我们用来完成特定任务的预构建代码。类似于 Python 模块,代码可以随默认的 Ansible 安装提供,也可以单独安装。
如果我们用 Ansible 构建树屋的类比,剧本将是说明书,模块将是使用的工具,而清单(inventory)则是我们工作的组件。
剧本设计为以 YAML 格式(docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html)可读。我们将编写第一个剧本,命名为ios_config_backup.yml,如下所示:
- name: Back Up IOS Device Configurations
hosts: all
gather_facts: false
tasks:
- name: backup
ios_config:
backup: yes
注意name前的-,它指定了 YAML 中的一个列表项。同一列表项中的所有内容都应该有相同的缩进。我们将gather_facts设置为false,因为大多数网络任务在更改设备之前都是在本地执行的。gather_facts主要用于在执行任何任务之前收集受管理节点(通常是服务器)的信息。
列表项中有两个键值对,hosts 和 tasks。具有 all 值的 hosts 变量指定我们将处理清单文件中的所有主机。tasks 键的值中还有一个列表项,它使用 ios_config 模块 (docs.ansible.com/ansible/latest/collections/cisco/ios/ios_config_module.html#ansible-collections-cisco-ios-ios_config-module)。ios_config 模块是随 Ansible 一起安装的模块集合之一。它也有许多参数。我们使用 backup 参数并将其设置为 yes,以表示我们将备份设备的 running-config。
我们接下来要做的任务是使用新的 LibSSH 连接插件来配置 Ansible。默认情况下,Ansible 网络 SSH 连接使用 Paramiko 库。然而,Paramiko 库并不能保证符合 FIPS 标准,并且在需要连接多个设备时速度较慢。我们将按照以下步骤安装 LibSSH:
(venv) $ pip install ansible-pylibssh
我们将在新的 ansible.cfg 文件中指定用法。我们将在这个与我们的剧本相同的目录中创建文件,内容如下。在同一个配置文件中,我们还将 host_key_checking 设置为 false;这是为了防止在 ssh 设置中主机最初不在 known_hosts 列表中时出现错误:
[defaults]
host_key_checking = False
[persistent_connection]
ssh_type = libssh
最后,我们可以通过带有 -i 选项的 ansible-playbook 命令来执行剧本,以指示清单文件:
$ ansible-playbook -i hosts ios_config_backup.yml
PLAY [Back Up IOS Device Configurations] **************************************************************************************
TASK [backup] *****************************************************************************************************************
changed: [iosv-2]
changed: [iosv-1]
PLAY RECAP ********************************************************************************************************************
iosv-1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
iosv-2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
就像魔法一样,如果我们查看执行剧本的工作目录,我们会看到一个名为 backup 的文件夹,其中包含两个设备的运行配置文件,并带有时间戳!现在,这个命令可以通过 cron 计划任务每晚运行,以备份我们所有设备的配置。
恭喜你执行了你的第一个 Ansible 剧本!即使像我们这样的剧本很简单,这也是一个非常有用的自动化任务,我们能够在短时间内完成。我们将在稍后扩展这个剧本,但首先,让我们看看为什么 Ansible 适合网络管理。记住,Ansible 模块是用 Python 编写的;这对于 Python 网络工程师来说是一个优势,对吧?
Ansible 的优势
除了 Ansible 之外,还有许多基础设施自动化框架,如 Chef、Puppet 和 SaltStack。每个框架都提供其独特的功能;没有一个框架适合所有组织。在本节中,让我们看看 Ansible 的一些优势以及为什么我相信它是网络自动化的好工具。
优点将与其他框架进行有限的比较,以避免引发争论战。其他框架可能会采用一些相同的理念或 Ansible 的某些方面,但很少会包含我将要提到的所有功能。正是所有以下功能和理念的组合使得 Ansible 非常适合网络自动化。
无代理
与其一些同行不同,Ansible 不需要严格的 master-client 模型。不需要在向服务器通信的客户端上安装任何软件或代理。除了许多平台默认拥有的 Python 解释器之外,不需要额外的软件。
对于网络自动化模块,Ansible 不是依赖于远程主机代理,而是使用 SSH 或 API 调用将所需更改推送到远程主机。这进一步减少了 Python 解释器的需求。这对于网络设备管理来说是个巨大的优势,因为网络供应商通常不愿意在他们的平台上安装第三方软件。另一方面,SSH 已经存在于网络设备上。正如我们在第三章,APIs 和 Intent-Driven Networking中看到的那样,较新的网络设备也提供了 API 层,这也可以被 Ansible 利用。
由于远程主机上没有代理,Ansible 使用push模型将更改推送到设备,而不是pull模型,其中代理从主服务器拉取信息。push模型更确定,因为所有内容都源自控制机器。在pull模型中,pull的时间可能会因客户端而异,因此导致时间差异。
再次强调,当与现有的网络设备一起工作时,无代理的重要性不容忽视。这通常是网络运营商和供应商接受 Ansible 的主要原因之一。
幂等性
根据维基百科,幂等性是数学和计算机科学中某些操作的性质,可以在不改变初始应用结果的情况下多次应用(zh.wikipedia.org/wiki/幂等性)。用更通俗的话来说,这意味着重复执行相同的程序不会在第一次之后改变系统。Ansible 旨在实现幂等性,这对于需要特定操作顺序的网络操作来说是个优点。在我们的第一个 playbook 示例中,当 playbook 运行时会有一个‘changed’值;如果远程设备上没有进行任何更改,这个值将是‘false’。
幂等性的优势最好与我们所编写的 Pexpect 和 Paramiko 脚本进行比较。请记住,这些脚本是为了像工程师坐在终端一样推送命令而编写的。如果你执行脚本 10 次,脚本会进行 10 次相同的变化。如果我们通过 Ansible 剧本编写相同的任务,首先会检查现有设备配置,并且只有在没有变化的情况下剧本才会执行。如果我们执行剧本 10 次,变化只会在第一次运行时应用,接下来的 9 次运行将抑制配置更改。
由于幂等性,我们可以反复执行剧本而不用担心会有不必要的更改。这对于我们需要自动检查状态一致性而不增加额外开销来说非常重要。
简单和可扩展
Ansible 是用 Python 编写的,并使用 YAML 作为剧本语言,这两者都相对容易学习。还记得 Cisco IOS 语法吗?这是一种仅在管理 Cisco IOS 设备或其他类似结构的设备时适用的领域特定语言;它不是一种超出其有限范围的通用语言。幸运的是,与一些其他自动化工具不同,Ansible 没有额外的领域特定语言(DSL)需要学习,因为 YAML 和 Python 都是广泛使用的通用语言。
Ansible 是可扩展的。正如前面的例子所示,Ansible 最初是为了自动化服务器(主要是 Linux)工作负载而设计的。然后它扩展到使用 PowerShell 管理 Windows 机器。随着越来越多的网络行业人士开始采用 Ansible,网络自动化现在已成为 Ansible 工作组的主要支柱。
简单性和可扩展性为未来的可靠性提供了良好的保障。技术世界正在快速发展,我们一直在努力适应。如果能够学习一种技术并持续使用,而不受最新趋势的影响,那岂不是很好?Ansible 的记录很好地证明了未来技术的适应性。
现在我们已经介绍了一些 Ansible 的优势,让我们在此基础上,利用更多功能继续学习。
Ansible 内容集合
让我们先列出默认 Ansible 安装中我们手头的所有模块。它们被组织到内容集合中(www.ansible.com/products/content-collections),有时简称为集合。我们可以通过ansible-galaxy collection list命令列出集合。以下列出了一些值得注意的网络集合:
(venv) $ ansible-galaxy collection list
# /home/echou/Mastering_Python_Networking_Fourth_Edition/venv/lib/python3.10/site-packages/ansible_collections
Collection Version
----------------------------- -------
…
ansible.netcommon 3.1.0
arista.eos 5.0.1
cisco.aci 2.2.0
cisco.asa 3.1.0
cisco.dnac 6.5.3
cisco.intersight 1.0.19
cisco.ios 3.3.0
cisco.iosxr 3.3.0
cisco.ise 2.5.0
cisco.meraki 2.10.1
cisco.mso 2.0.0
cisco.nso 1.0.3
cisco.nxos 3.1.0
cisco.ucs 1.8.0
community.ciscosmb 1.0.5
community.fortios 1.0.0
community.network 4.0.1
dellemc.enterprise_sonic 1.1.1
f5networks.f5_modules 1.19.0
fortinet.fortimanager 2.1.5
fortinet.fortios 2.1.7
mellanox.onyx 1.0.0
openstack.cloud 1.8.0
openvswitch.openvswitch 2.1.0
vyos.vyos 3.0.1
如列表所示,即使使用默认安装,我们也有大量的网络相关模块可以使用。它们从企业软件到开源项目都有。查看列表并阅读您生产环境中感兴趣的内容是一个好的开始。Ansible 文档还提供了所有可用集合的完整列表,docs.ansible.com/ansible/latest/collections/index.html。集合也可以通过agalaxy install命令进行扩展,docs.ansible.com/ansible/latest/user_guide/collections_using.html。
更多 Ansible 网络示例
我们第一个 Ansible 网络示例让我们从新手变成了运行第一个有用的网络自动化任务。让我们从基础开始,学习更多功能。
我们将首先看看我们如何构建一个包含我们所有网络设备的清单文件。如果您还记得,我们有两个数据中心,每个数据中心都有核心和边缘设备:

图 4.5:完整实验室拓扑
在这个示例中,我们将包含我们清单文件中的所有设备。
清单嵌套
我们可以构建一个包含嵌套的清单文件。例如,我们可以创建一个名为hosts_full的主机文件,它包含来自一个组到另一个组的子组:
[lax_cor_devices]
lax-cor-r1
[lax_edg_devices]
lax-edg-r1
lax-edg-r2
[nyc_cor_devices]
nyc-cor-r1
[nyc_edg_devices]
nyc-edg-r1
nyc-edg-r2
[lax_dc:children]
lax_cor_devices
lax_edg_devices
[nyc_dc:children]
nyc_cor_devices
nyc_edg_devices
[ios_devices:children]
lax_edg_devices
nyc_edg_devices
[nxos_devices:children]
nyc_cor_devices
lax_cor_devices
在文件中,我们使用[<name>:children]格式通过角色和功能对设备进行分组。为了使用这个新的清单文件,我们需要更新host_vars目录以包含相应设备的名称:
(venv) $ tree host_vars/
host_vars/
…
├── lax-cor-r1
├── lax-edg-r1
├── lax-edg-r2
├── nyc-cor-r1
├── nyc-edg-r1
└── nyc-edg-r2
我们还需要相应地更改ansible_host和ansible_network_os,以lax-cor-r1为例:
(venv) $ cat host_vars/lax-cor-r1
---
ansible_host: 192.168.2.50
…
ansible_network_os: nxos
…
现在我们可以使用父组的名称来包含其子组。例如,在nxos_config_backup.yml剧本中,我们只指定了nxos_devices的父组而不是all:
- name: Back Up NX-OS Device Configurations
hosts: nxos_devices
gather_facts: false
tasks:
- name: backup
nxos_config:
backup: yes
当我们执行这个剧本时,它将自动包含其子组,lax_cor_devices和nyc_cor_devices。此外,请注意,我们使用单独的nxos_config模块(docs.ansible.com/ansible/latest/collections/cisco/nxos/nxos_config_module.html#ansible-collections-cisco-nxos-nxos-config-module)来适应新的设备类型。
Ansible 条件语句
Ansible 条件语句类似于编程语言中的条件语句。Ansible 使用条件关键字仅在给定条件满足时运行任务。在许多情况下,剧本或任务的执行可能取决于事实、变量或先前任务的结果。例如,如果您有一个升级路由器映像的剧本,您希望在继续到下一个重启路由器的剧本之前,确保新的路由器映像已经在设备上。
在这个例子中,我们将查看when子句,它适用于所有模块。when子句在您需要检查变量或剧本执行结果的输出并根据结果采取行动时非常有用。以下是一些条件:
-
等于(
eq) -
不等于(
neq) -
大于(
gt) -
大于或等于(
ge) -
小于(
lt) -
小于或等于(
le) -
包含
让我们看看以下名为ios_conditional.yml的剧本:
---
- name: IOS command output for when clause
hosts: ios_devices
gather_facts: false
tasks:
- name: show hostname
ios_command:
commands:
- show run | i hostname
register: output
- name: show output with when conditions
when: output.stdout == ["hostname nyc-edg-r2"]
debug:
msg: '{{ output }}'
在剧本中,有两个任务。在第一个任务中,我们使用register模块将show run | i hostname命令的输出保存到名为output的变量中。output变量包含一个包含输出的stdout列表。我们使用when子句仅在主机名为nyc-edg-r2时显示输出。让我们执行剧本:
(venv) $ ansible-playbook -i hosts_full ios_conditional.yml
PLAY [IOS command output for when clause] *************************************************************************************
TASK [show hostname] **********************************************************************************************************
ok: [lax-edg-r1]
ok: [nyc-edg-r2]
ok: [lax-edg-r2]
ok: [nyc-edg-r1]
TASK [show output with when conditions] ***************************************************************************************
skipping: [lax-edg-r1]
skipping: [lax-edg-r2]
skipping: [nyc-edg-r1]
ok: [nyc-edg-r2] => {
"msg": {
"changed": false,
"failed": false,
"stdout": [
"hostname nyc-edg-r2"
],
"stdout_lines": [
[
"hostname nyc-edg-r2"
]
]
}
}
PLAY RECAP ********************************************************************************************************************
lax-edg-r1 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
lax-edg-r2 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
nyc-edg-r1 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
nyc-edg-r2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
我们可以看到lax-edg-r1、lax-edg-r2和nyc-edg-r1的输出被跳过了,因为它们没有满足条件。此外,我们还可以看到所有设备的changed=0输出。这与 Ansible 的幂等性功能一致。
配置更改
我们可以将条件与配置更改结合起来——例如,在以下剧本中,ios_conditional_config.yml:
---
- name: IOS command output for when clause
hosts: ios_devices
gather_facts: false
tasks:
- name: show hostname
ios_command:
commands:
- show run | i hostname
register: output
- name: show output with when conditions
when: output.stdout == ["hostname nyc-edg-r2"]
ios_config:
lines:
- logging buffered 30000
我们只有在条件满足时才会更改日志缓冲区。以下是第一次执行剧本时的输出:
(venv) $ ansible-playbook -i hosts_full ios_conditional_config.yml
<skip>
TASK [show output with when conditions] ***************************************************************************************
skipping: [lax-edg-r1]
skipping: [lax-edg-r2]
skipping: [nyc-edg-r1]
[WARNING]: To ensure idempotency and correct diff the input configuration lines should be similar to how they appear if
present in the running configuration on device
changed: [nyc-edg-r2]
PLAY RECAP ********************************************************************************************************************
lax-edg-r1 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
lax-edg-r2 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
nyc-edg-r1 : ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
nyc-edg-r2 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
nyc-edg-r2设备控制台将显示配置已更改:
*Sep 10 01:53:43.132: %SYS-5-LOG_CONFIG_CHANGE: Buffer logging: level debugging, xml disabled, filtering disabled, size (30000)
然而,当我们第二次运行剧本时,相同的更改不再应用,因为它已经更改:
<skip>
TASK [show output with when conditions] ***************************************************************************************
skipping: [lax-edg-r1]
skipping: [lax-edg-r2]
skipping: [nyc-edg-r1]
ok: [nyc-edg-r2]
这有多酷?只需一个简单的剧本,我们就可以安全地将配置更改应用到我们想要更改的设备上,同时检查幂等性。
Ansible 网络事实
在 2.5 版本之前,Ansible 网络附带了一些厂商特定的事实模块。因此,不同厂商的事实命名和用法不同。从 2.5 版本开始,Ansible 开始标准化其网络事实模块。Ansible 网络事实模块从系统收集信息,并将结果存储在以ansible_net_为前缀的事实中。这些模块收集的数据在模块文档的返回值中进行了记录。这很有用,因为我们可以收集网络事实,并根据它们执行任务。
作为ios_facts模块的示例,以下为ios_facts_playbook的内容:
---
- name: IOS network facts
connection: network_cli
gather_facts: false
hosts: ios_devices
tasks:
- name: Gathering facts via ios_facts module
ios_facts:
when: ansible_network_os == 'ios'
- name: Display certain facts
debug:
msg: "The hostname is {{ ansible_net_hostname }} running {{ ansible_net_version }}"
- name: Display all facts for hosts
debug:
var: hostvars
在这个剧本中,我们引入了一个变量的概念。双大括号{{ }}表示它是一个变量,变量的值应该用于输出。
执行剧本后,这里是部分输出:
(venv) $ ansible-playbook -i hosts_full ios_facts_playbook.yml
…
TASK [Display certain facts] ***************************************************
ok: [lax-edg-r1] => {
"msg": "The hostname is lax-edg-r1 running 15.8(3)M2"
}
ok: [lax-edg-r2] => {
"msg": "The hostname is lax-edg-r2 running 15.8(3)M2"
}
ok: [nyc-edg-r1] => {
"msg": "The hostname is nyc-edg-r1 running 15.8(3)M2"
}
ok: [nyc-edg-r2] => {
"msg": "The hostname is nyc-edg-r2 running 15.8(3)M2"
}
…
TASK [Display all facts for hosts] *********************************************
ok: [lax-edg-r1] => {
"hostvars": {
"lax-cor-r1": {
…
"ansible_facts": {
"net_api": "cliconf",
"net_gather_network_resources": [],
"net_gather_subset": [
"default"
],
"net_hostname": "lax-edg-r1",
"net_image": "flash0:/vios-adventerprisek9-m",
"net_iostype": "IOS",
"net_model": "IOSv",
"net_python_version": "3.10.4",
"net_serialnum": "98U40DKV403INHIULHYHB",
"net_system": "ios",
"net_version": "15.8(3)M2",
"network_resources": {}
},
…
我们现在可以利用这些事实与我们的条件语句结合,以自定义我们的操作。
Ansible 循环
Ansible 在 playbook 中提供了一系列循环函数:标准循环、遍历文件、子元素、do-until 以及更多。在本节中,我们将探讨两种最常用的循环形式:标准循环和遍历哈希值。
标准循环
在 playbook 中,标准循环通常用于轻松地多次执行类似任务。标准循环的语法非常简单:{{ item }} 变量是循环遍历 loop 列表的占位符。在我们的下一个示例 standard_loop.yml 中,我们将使用 echo 命令遍历 loop 列表中的项目,并显示来自我们的 localhost 的输出。
- name: Echo Loop Items
hosts: "localhost"
gather_facts: false
tasks:
- name: echo loop items
command: echo "{{ item }}"
loop:
- 'r1'
- 'r2'
- 'r3'
- 'r4'
- 'r5'
让我们继续执行 playbook:
(venv) $ ansible-playbook -i hosts_full standard_loop.yml
PLAY [Echo Loop Items] ********************************************************************************************************
TASK [echo loop items] ********************************************************************************************************
changed: [localhost] => (item=r1)
changed: [localhost] => (item=r2)
changed: [localhost] => (item=r3)
changed: [localhost] => (item=r4)
changed: [localhost] => (item=r5)
PLAY RECAP ********************************************************************************************************************
localhost : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
使用相同的概念,我们可以系统地使用 playbook 向我们的设备添加 VLAN。以下是一个使用名为 standard_loop_vlan_example.yml 的 playbook 向主机添加三个 VLAN 的示例:
- name: Add Multiple Vlans
hosts: "nyc-cor-r1"
gather_facts: false
connection: network_cli
vars:
vlan_numbers: [100, 200, 300]
tasks:
- name: add vlans
nxos_config:
lines:
- vlan {{ item }}
loop: "{{ vlan_numbers }}"
register: output
playbook 输出如下:
(venv) $ ansible-playbook -i hosts_full standard_loop
_vlan_example.yml
PLAY [Add Multiple Vlans] *****************************************************************************************************
TASK [add vlans] **************************************************************************************************************
changed: [nyc-cor-r1] => (item=100)
changed: [nyc-cor-r1] => (item=200)
changed: [nyc-cor-r1] => (item=300)
[WARNING]: To ensure idempotency and correct diff the input configuration lines should be similar to how they appear if
present in the running configuration on device
PLAY RECAP ********************************************************************************************************************
nyc-cor-r1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
从 playbook 中我们可以看到,循环列表可以读取自一个变量,这为你的 playbook 结构提供了更大的灵活性:
…
vars:
vlan_numbers: [100, 200, 300]
tasks:
…
loop: "{{ vlan_numbers }}"
标准循环在 playbook 中执行重复性任务时是一个节省时间的好方法。让我们看看我们如何在下一节中遍历字典。
遍历字典
当我们需要生成配置时,我们通常会有一个与多个属性相关联的实体。如果你考虑上一节中的 VLAN 示例,每个 VLAN 都会有几个独特的属性,例如描述、网关 IP 地址,可能还有其他属性。通常,我们可以使用字典来表示实体,以将其多个属性合并到其中。
让我们扩展之前的示例,在 standard_loop_vlan_example_2.yml 中包含一个字典变量。我们为三个 vlan 定义了字典值,每个 vlan 都有一个嵌套字典用于描述和 IP 地址:
---
- name: Add Multiple Vlans
hosts: "nyc-cor-r1"
gather_facts: false
connection: network_cli
vars:
vlans: {
"100": {"description": "floor_1", "ip": "192.168.10.1"},
"200": {"description": "floor_2", "ip": "192.168.20.1"},
"300": {"description": "floor_3", "ip": "192.168.30.1"}
}
tasks:
- name: add vlans
nxos_config:
lines:
- vlan {{ item.key }}
with_dict: "{{ vlans }}"
- name: configure vlans
nxos_config:
lines:
- description {{ item.value.description }}
- ip address {{ item.value.ip }}/24
parents: interface vlan {{ item.key }}
with_dict: "{{ vlans }}"
在 playbook 中,我们通过使用项目的键来配置第一个任务以添加 VLAN。在第二个任务中,我们使用每个项目中的值来配置 VLAN 接口。请注意,我们使用 parents 参数来唯一标识应该检查命令的章节。这是因为描述和 IP 地址都是在配置中的 interface vlan <number> 子章节下配置的。
在我们执行命令之前,我们需要确保在 nyc-cor-r1 设备上启用了三层接口功能:
nyc-cor-r1(config)# feature interface-vlan
我们可以像之前一样运行 playbook。我们可以看到正在循环的字典:
(venv) $ ansible-playbook -i hosts_full standard_loop_vlan_example_2.yml
PLAY [Add Multiple Vlans] ************************************************************************************************
TASK [add vlans] *********************************************************************************************************
changed: [nyc-cor-r1] => (item={'key': '100', 'value': {'description': 'floor_1', 'ip': '192.168.10.1'}})
changed: [nyc-cor-r1] => (item={'key': '200', 'value': {'description': 'floor_2', 'ip': '192.168.20.1'}})
changed: [nyc-cor-r1] => (item={'key': '300', 'value': {'description': 'floor_3', 'ip': '192.168.30.1'}})
[WARNING]: To ensure idempotency and correct diff the input configuration lines should be similar to how they appear if
present in the running configuration on device
TASK [configure vlans] ***************************************************************************************************
changed: [nyc-cor-r1] => (item={'key': '100', 'value': {'description': 'floor_1', 'ip': '192.168.10.1'}})
changed: [nyc-cor-r1] => (item={'key': '200', 'value': {'description': 'floor_2', 'ip': '192.168.20.1'}})
changed: [nyc-cor-r1] => (item={'key': '300', 'value': {'description': 'floor_3', 'ip': '192.168.30.1'}})
PLAY RECAP ***************************************************************************************************************
nyc-cor-r1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
我们可以在设备上验证最终结果:
nyc-cor-r1# sh run
interface Vlan100
description floor_1
ip address 192.168.10.1/24
interface Vlan200
description floor_2
ip address 192.168.20.1/24
interface Vlan300
description floor_3
ip address 192.168.30.1/24
对于 Ansible 的更多循环类型,请随时查看相应的文档(docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html)。
第一次使用字典进行循环时需要一些练习。但就像标准循环一样,使用字典循环将成为我们工具箱中的无价之宝。Ansible 循环是一个可以节省我们时间并使 playbook 更易读的工具。在下一节中,我们将探讨 Ansible 模板,这些模板允许我们对网络设备配置中常用的文本文件进行系统性的更改。
模板
自从我开始担任网络工程师以来,我始终使用某种网络模板系统。根据我的经验,许多网络设备的网络配置部分是相同的,尤其是如果这些设备在网络中扮演相同的角色。
大多数时候,当我们需要部署新设备时,我们使用模板形式的相同配置,替换必要的字段,然后将文件复制到新设备上。使用 Ansible,您可以通过使用模板功能(docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html)来自动化所有工作。
Ansible 使用 Jinja (jinja.palletsprojects.com/en/3.1.x/) 模板来启用动态表达式和访问变量和事实。Jinja 有其自己的语法和循环、条件的方法;幸运的是,为了我们的目的,我们只需要了解它的基础知识。Ansible 模板模块是我们日常任务中将要使用的重要工具,我们将在本节中更多地探讨它。我们将通过逐步构建我们的 playbook,从一些简单的任务到更复杂的任务,来学习语法。
模板使用的语法非常简单;我们只需指定源文件和我们要复制到的目标位置。
让我们创建一个名为Templates的新目录,并开始创建我们的 playbook。我们现在将创建一个空文件:
(venv) $ mkdir Templates
(venv) $ cd Templates/
(venv) $ touch file1
然后,我们将使用以下 playbook,template_1.yml,将file1复制到file2。请注意,playbook 仅在控制机上执行:
---
- name: Template Basic
hosts: localhost
tasks:
- name: copy one file to another
template:
src=/home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter04/Templates/file1
dest=/home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter04/Templates/file2
执行 playbook 将创建一个新文件:
(venv) $ ansible-playbook -i hosts template_1.yml
PLAY [Template Basic] ****************************************************************************************************
TASK [Gathering Facts] ***************************************************************************************************
ok: [localhost]
TASK [copy one file to another] ******************************************************************************************
changed: [localhost]
PLAY RECAP ***************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
(venv) $ ls file*
file1 file2
在我们的模板中,源文件可以有任意扩展名,但由于它们将通过 Jinja2 模板引擎进行处理,让我们创建一个名为nxos.j2的文本文件作为模板源。模板将遵循 Jinja 约定,使用双大括号来指定变量,以及使用大括号加百分号来指定命令:
hostname {{ item.value.hostname }}
feature telnet
feature ospf
feature bgp
feature interface-vlan
{% if item.value.netflow_enable %}
feature netflow
{% endif %}
username {{ item.value.username }} password {{ item.value.password }} role network-operator
{% for vlan_num in item.value.vlans %}
vlan {{ vlan_num }}
{% endfor %}
{% if item.value.l3_vlan_interfaces %}
{% for vlan_interface in item.value.vlan_interfaces %}
interface {{ vlan_interface.int_num }}
ip address {{ vlan_interface.ip }}/24
{% endfor %}
{% endif %}
现在,我们可以组合一个 playbook,根据nxos.j2文件创建网络配置模板。
Jinja 模板变量
template_2.yml playbook 在先前的模板示例的基础上增加了以下内容:
-
源文件是
nxos.j2。 -
目标文件名现在是从 playbook 中定义的
nexus_devices变量中获取的变量。 -
nexus_devices中的每个设备都包含在模板中将被替换或循环遍历的变量。
这个 playbook 可能看起来比上一个复杂,但如果去掉变量定义部分,它与之前简单的模板 playbook 非常相似:
---
- name: Template Looping
hosts: localhost
vars:
nexus_devices: {
"nx-osv-1": {
"hostname": "nx-osv-1",
"username": "cisco",
"password": "cisco",
"vlans": [100, 200, 300],
"l3_vlan_interfaces": True,
"vlan_interfaces": [
{"int_num": "100", "ip": "192.168.10.1"},
{"int_num": "200", "ip": "192.168.20.1"},
{"int_num": "300", "ip": "192.168.30.1"}
],
"netflow_enable": True
},
"nx-osv-2": {
"hostname": "nx-osv-2",
"username": "cisco",
"password": "cisco",
"vlans": [100, 200, 300],
"l3_vlan_interfaces": False,
"netflow_enable": False
}
}
tasks:
- name: create router configuration files
template:
src=/home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter04/Templates/nxos.j2
dest=/home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter04/Templates/{{ item.key }}.conf
with_dict: "{{ nexus_devices }}"
让我们不要立即执行 playbook;我们还需要查看Jinja2模板中的{% %}符号内的if条件语句和for循环。
Jinja 模板循环
在我们的nxos.j2模板中,有两个for循环;一个循环遍历 VLAN,另一个循环遍历 VLAN 接口:
{% for vlan_num in item.value.vlans %}
vlan {{ vlan_num }}
{% endfor %}
{% if item.value.l3_vlan_interfaces %}
{% for vlan_interface in item.value.vlan_interfaces %}
interface {{ vlan_interface.int_num }}
ip address {{ vlan_interface.ip }}/24
{% endfor %}
{% endif %}
如果你记得,我们也可以在 Jinja 中循环遍历列表以及字典。在我们的例子中,vlans变量是一个列表,而vlan_interfaces变量是一个字典列表。
vlan_interfaces循环嵌套在一个条件语句中。在我们执行 playbook 之前,这是我们将要整合到 playbook 中的最后一件事。
Jinja 模板条件语句
Jinja 支持if条件检查。我们在nxos.j2模板中的两个位置添加了这个条件语句;一个是netflow变量,另一个是l3_vlan_interfaces变量。只有当条件为True时,我们才会执行块内的语句:
<skip>
{% if item.value.netflow_enable %}
feature netflow
{% endif %}
<skip>
{% if item.value.l3_vlan_interfaces %}
<skip>
{% endif %}
在 playbook 中,我们已将netflow_enable声明为nx-os-v1的True和nx-osv-2的False:
vars:
nexus_devices: {
"nx-osv-1": {
<skip>
"netflow_enable": True
},
"nx-osv-2": {
<skip>
"netflow_enable": False
}
}
最后,我们准备运行我们的 playbook:
(venv) $ ansible-playbook -i hosts template_2.yml
PLAY [Template Looping] **************************************************************************************************
TASK [Gathering Facts] ***************************************************************************************************
ok: [localhost]
TASK [create router configuration files] *********************************************************************************
changed: [localhost] => (item={'key': 'nx-osv-1', 'value': {'hostname': 'nx-osv-1', 'username': 'cisco', 'password': 'cisco', 'vlans': [100, 200, 300], 'l3_vlan_interfaces': True, 'vlan_interfaces': [{'int_num': '100', 'ip': '192.168.10.1'}, {'int_num': '200', 'ip': '192.168.20.1'}, {'int_num': '300', 'ip': '192.168.30.1'}], 'netflow_enable': True}})
changed: [localhost] => (item={'key': 'nx-osv-2', 'value': {'hostname': 'nx-osv-2', 'username': 'cisco', 'password': 'cisco', 'vlans': [100, 200, 300], 'l3_vlan_interfaces': False, 'netflow_enable': False}})
PLAY RECAP ***************************************************************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
你还记得目标文件是以{{ item.key }}.conf命名的吗?已经创建了两个以设备名称命名的文件:
$ ls nx-os*
nx-osv-1.conf
nx-osv-2.conf
让我们检查这两个配置文件的相似之处和不同之处,以确保所有预期的更改都已到位。两个文件都应该包含静态项,例如feature ospf,主机名和其他变量应相应替换,并且只有nx-osv-1.conf应该启用netflow以及三层vlan接口配置:
$ cat nx-osv-1.conf
hostname nx-osv-1
feature telnet
feature ospf
feature bgp
feature interface-vlan
feature netflow
username cisco password cisco role network-operator
vlan 100
vlan 200
vlan 300
interface 100
ip address 192.168.10.1/24
interface 200
ip address 192.168.20.1/24
interface 300
ip address 192.168.30.1/24
让我们看一下nx-osv-2.conf文件:
$ cat nx-osv-2.conf
hostname nx-osv-2
feature telnet
feature ospf
feature bgp
feature interface-vlan
username cisco password cisco role network-operator
vlan 100
vlan 200
vlan 300
真的吗?这确实可以为我们节省大量时间,因为之前需要重复复制粘贴。就我个人而言,模板模块对我来说是一个巨大的变革。仅此模块就足以激励我在几年前学习并使用 Ansible。
摘要
在本章中,我们全面了解了开源自动化框架 Ansible。与基于 Pexpect 和 API 驱动的网络自动化脚本不同,Ansible 提供了一个更高层次的抽象,称为 playbook,用于自动化我们的网络设备。
Ansible 是一个功能齐全的自动化框架,能够管理大型基础设施。我们的重点是管理网络设备,但 Ansible 还能够管理服务器、数据库、云基础设施等等。我们只是触及了其功能的一角。如果你觉得 Ansible 是一个你想要了解更多信息的工具,Ansible 文档是一个极好的参考资料来源。如果你想要参与,Ansible 社区友好且欢迎。
在第五章,网络工程师的 Docker 容器,我们将开始学习 Docker 和容器世界。
加入我们的书籍社区
要加入我们这本书的社区——在那里你可以分享反馈、向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第五章:网络工程师的 Docker 容器
计算机硬件虚拟化已经彻底改变了我们处理基础设施的方式。那些必须为单个主机和操作系统分配硬件的日子已经过去了。我们现在可以选择将宝贵的硬件资源,如 CPU、内存和磁盘空间,与多个虚拟机共享,每个虚拟机都有自己的操作系统和应用程序。由于在这些虚拟机上运行的软件与底层硬件资源分离,我们可以根据虚拟机的特定需求分配不同的硬件资源组合。如今,很难想象一个没有虚拟机的世界。
尽管虚拟机在构建应用程序方面非常出色,但它们需要花费一些时间来构建、启动,最终还需要拆除。原因是与虚拟机相关的虚拟化技术完全模拟了实际硬件,使得硬件与客户虚拟机无法区分。
现在可能有人会问:有没有一种方法可以通过更多的虚拟化来加速应用程序的生命周期?答案是肯定的,借助容器。
容器和虚拟机在允许不同隔离的应用程序之间共享计算资源方面相似。区别在于虚拟机在虚拟机管理程序级别进行抽象,而容器是由容器引擎在操作系统内部进行抽象。容器通常被称为操作系统级别的虚拟化。

图 5.1:虚拟机和容器比较(来源:https://www.atlassian.com/microservices/cloud-computing/containers-vs-vms)
在全虚拟机中,我们可以安装不同的操作系统,如 Windows 和 Linux。因为容器虚拟化是由操作系统处理的,所以每个容器都将拥有相同的操作系统。然而,应用程序及其相关资源将被隔离,并独立于彼此运行。容器引擎将配置、软件包和库从每个容器中分离出来。
容器虚拟化并不新鲜;Linux 容器(LXC)、Solaris 容器、Docker 和 Podman 都是此类实现的例子。在本章中,我们将探讨当今最流行的容器技术 Docker。我们将讨论与 Docker 容器相关的以下主题:
-
Docker 概述
-
使用 Docker 构建 Python 应用程序
-
容器网络
-
网络工程领域的容器
-
Docker 和 Kubernetes
我们将在本书中学习的一些技术中用到容器;这是一个开始熟悉容器的良好起点。
让我们从 Docker 的高级概述开始。
Docker 概述
Docker 是一套支持容器交付的产品和工具。它始于 2008 年的公司 dotCloud(2013 年更名为 Docker, Inc.)。这套工具包括 Docker 的容器技术、名为 Docker Engine 的容器引擎、名为 Docker Hub 的基于云的容器仓库以及名为 Docker Desktop 的桌面图形用户界面软件。
Docker 有两个版本,Docker 社区版(Docker-CE)和Docker 企业版(Docker-EE)。Docker-CE 是一个基于 Apache 2.0 许可证的免费开源平台,而 Docker-EE 是一个面向企业的付费版本。当本书中提到“Docker”一词时,我们指的是社区版。
Docker 容器环境中有三个主要组件:
-
构建和开发:这包括用于构建容器的工具,包括 CLI 命令、镜像以及我们获取各种基础镜像的仓库。在 Docker 中,我们使用 Dockerfile 来指定容器构建的大多数步骤。
-
Docker 引擎:这是在后台运行的守护进程。我们可以使用 Docker 命令来管理守护进程。
-
容器编排:在开发过程中,我们通常会使用 Docker 的
Docker-compose来管理多容器环境。在生产环境中,一个常见的工具是由谷歌发起的工具,称为 Kubernetes (kubernetes.io/)。
在下一节中,我们将讨论 Docker 的优势。
Docker 的优势
Docker 有许多优势。我们将在此总结其中一些:
-
Docker 容器部署和销毁都非常快速。
-
容器优雅地重置。容器是短暂的,当重启时不会留下任何残留的文件。这确保了每次生成新容器时都有一个干净的状态。
-
它是自包含且确定的。容器通常附带配置文件,其中包含有关如何重建容器的说明。我们可以确信每个容器镜像都是按照相同的方式进行构建的。
-
它允许应用程序开发和 DevOps 之间的无缝集成。由于上述优点,许多公司已将 Docker 镜像直接部署到生产环境中。容器可以精确地按照开发者的意图进行复制并测试到生产环境中。
现在我们已经对 Docker 有了基本的了解,是时候在我们的 Docker 容器中构建第一个 Python 应用程序了。
在 Docker 中构建 Python 应用程序
Docker 容器是构建 Python 应用程序的一种非常流行的方式。
安装 Docker
当然,我们需要安装 Docker 才能开始使用它。我们将遵循 DigitalOcean 为 Ubuntu 22.04 提供的优秀安装指南www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04。如果您使用的是其他版本的 Linux 发行版,您可以直接从文档中的下拉菜单选择不同的版本。对于 Mac 或 Windows 的安装,我的建议是安装 Docker Desktopdocs.docker.com/desktop/。它将包括 Docker Engine、CLI 客户端和 GUI 应用程序。
$ sudo apt-get update
$ sudo apt-get -y upgrade
$ sudo apt install apt-transport-https ca-certificates curl software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt update
$ apt-cache policy docker-ce
$ sudo apt install docker-ce
对于 Linux,有一些可选但有用的安装后步骤,请参阅docs.docker.com/engine/install/linux-postinstall/。
我们可以检查我们的 Docker 安装状态:
$ sudo systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Sun 2022-09-11 15:02:27 PDT; 5s ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
在下一节中,我们将了解如何使用 Docker 容器构建 Python 应用程序。
有用的 Docker 命令
我们将需要使用一些命令来构建、运行和测试我们的容器。
对于更多的 Docker CLI 参考,请查看文档:docs.docker.com/engine/reference/run/。
在本章中,我们将使用以下一些命令:
-
docker run:docker run用于指定从哪个镜像(默认为 Docker Hub)派生容器,网络设置、名称和其他设置。 -
docker container ls:列出容器;默认情况下,它只列出当前正在运行的容器。 -
docker exec:在运行的容器上运行命令。 -
docker network:在需要管理 Docker 网络时使用,例如创建、列出和删除 Docker 网络。 -
docker image:管理 Docker 镜像。
还有许多其他的 CLI 命令,但这些已经足够我们开始使用了。对于完整的参考,请查看信息框中提供的链接。
构建“hello world”
第一步是确保我们能够访问 Docker Hub 以检索镜像。为此,Docker 提供了一个非常简单的hello-world应用程序:
$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1\. The Docker client contacted the Docker daemon.
2\. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3\. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4\. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
<skip>
我们可以看到 Docker 客户端需要执行的各种步骤以显示消息。我们可以显示运行的 Docker 进程:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3cb4f91b6388 hello-world "/hello" About a minute ago Exited (0) About a minute ago fervent_torvalds
我们可以看到hello-world镜像信息:
$ docker images hello-world
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest feb5d9fea6a5 11 months ago 13.3kB
现在,我们可以构建我们的第一个 Python 应用程序。
构建我们的应用程序
让我们先思考一下我们将要构建的内容。由于我们在上一章中构建了一些 Ansible playbooks,我们为什么不将ios_config_backup.yml playbooks 容器化,这样我们就可以与其他团队成员共享呢?
我们将创建一个新的文件夹来保存所有文件。如果您还记得,为了构建 Docker 镜像,我们需要一个特殊的文件,称为 Dockerfile。我们也将在这个目录中创建这样一个文件:
$ mkdir ansible_container && cd ansible_container
$ touch Dockerfile
我们还将把host_vars文件夹、ansible.cfg、hosts和ios_config_backup.yml文件复制到这个文件夹中。我们还需要确保在从它构建 Docker 容器之前,playbook 能够按预期运行。
Docker 以分层的方式构建自身,从基础镜像开始。在 Dockerfile 中,我们将指定以下行:
# Getting base image
FROM ubuntu:22.04
# No need for interactive prompt
ENV DEBIAN_FRONTEND=noninteractive
以“#”开头的行是注释,就像在 Python 中一样。FROM关键字指定了我们将从默认 Docker Hub 检索的基础镜像。所有官方 Ubuntu 镜像都可以在网站上找到,hub.docker.com/_/ubuntu。在ENV语句中,我们指定不需要交互式提示。
Dockerfile 参考可以在docs.docker.com/engine/reference/builder/查看。
让我们构建这个镜像:
$ docker build --tag ansible-docker:v0.1 .
build命令从本地目录中的 Dockerfile 构建,并将最终标记为ansible-docker的镜像版本为 0.1。一旦完成,我们可以查看镜像:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ansible-docker v0.1 e99f103e2d36 3 seconds ago 864MB
如果在重建之前需要删除镜像,我们可以使用“docker rmi <image id>”删除镜像。
我们可以根据镜像启动容器:
$ docker run -it --name ansible-host1 ansible-docker:v0.1
root@96108c94e1d2:/# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.1 LTS
Release: 22.04
Codename: jammy
root@96108c94e1d2:/#
它将带我们进入 bash shell 提示符,容器一旦我们退出就会自己停止。为了使其以分离模式运行,我们需要用"-d"标志启动它。让我们先删除容器,然后带标志重新创建它:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
<container id> ansible-docker:v0.1 "bash" 2 minutes ago Exited (0) 52 seconds ago ansible-host1
$ docker rm <container id>
$ docker run -it -d --name ansible-host1 ansible-docker:v0.1
记得替换你的容器 ID。一次性删除所有容器的快捷方式是docker rm -f $(docker ps -a -q)。
容器现在以分离模式运行,我们可以在容器上执行交互式提示符:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d3b6a6ec90e5 ansible-docker:v0.1 "bash" About a minute ago Up 58 seconds ansible-host1
$ docker exec -it ansible-host1 bash
root@d3b6a6ec90e5:/# ls
我们可以停止容器,然后删除它:
$ docker stop ansible-host1
$ docker rm ansible-host1
我们将介绍一些更多的 Dockerfile 命令:
# Getting base image
FROM ubuntu:22.04
# No need for interactive prompt
ENV DEBIAN_FRONTEND=noninteractive
# Run any command, i.e. install packages
RUN apt update && apt install -y python3.10 python3-pip ansible vim
RUN pip install ansible-pylibssh
# specify a working directory
WORKDIR /app
COPY . /app
RUN命令执行 shell 命令,就像我们在 shell 中输入它们一样。我们可以在容器上指定工作目录为/app,然后将当前工作目录中的所有内容(host_vars、hosts、playbook 等)复制到远程容器的/app目录。
$ docker images
<find the image id>
$ docker rmi <image id>
$ docker build --tag ansible-docker:v0.1 .
我们将保留相同的标签,但如果我们想将其作为新版本发布,我们总是可以将其标记为v0.2。
我们将再次启动容器并执行ansible-playbook:
$ docker run -it -d --name ansible-host1 ansible-docker:v0.1
docker exec -it ansible-host1 bash
root@5ef5e9c85065:/app# pwd
/app
root@5ef5e9c85065:/app# ls
ansible.cfg dockerfile host_vars hosts ios_config_backup.yml
root@5ef5e9c85065:/app# ansible-playbook -i hosts ios_config_backup.yml
PLAY [Back Up IOS Device Configurations] ********************************************************************
TASK [backup] ***********************************************************************************************
changed: [iosv-2]
changed: [iosv-1]
PLAY RECAP **************************************************************************************************
iosv-1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
iosv-2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
root@5ef5e9c85065:/app# ls backup/
iosv-1_config.2022-09-12@23:01:07 iosv-2_config.2022-09-12@23:01:07
一旦容器启动,我们可以通过主机名来启动和停止:
$ docker stop ansible-host1
$ docker start ansible-host1
恭喜你完成了完整的容器工作流程!现在这看起来可能不多,但这是一个很大的进步。现在这些步骤可能看起来有点陌生,但不用担心,随着我们积累更多的实践经验,它们会变得更加熟悉。
分享 Docker 镜像
最后一步将是分享容器镜像。一种方法是将目录 tar zip 并分享文件。另一种方法是推送镜像到任何需要访问的人都可以访问的仓库。Docker Hub 是最受欢迎的仓库之一,但还有许多其他仓库。它们通常提供几个不同的订阅价格层。

图 5.2:Docker Hub 定价(来源:www.docker.com/pricing/)
除了共享容器镜像外,在 DevOps CI/CD(持续集成/持续交付)过程中,拥有可访问的仓库至关重要。例如,我们可能会通过自动构建和测试过程提交代码。一旦所有验证测试通过,我们就可以自动将镜像推送到仓库并部署到生产环境。我们将在 Docker Hub 上创建一个私有仓库:

图 5.3:Docker Hub 仓库
然后我们将通过 Docker CLI 进行登录:
$ docker login
然后,我们可以根据远程仓库标记现有镜像,然后将其推送到它。注意以下输出中目标标签名称与 Docker Hub 上的仓库名称匹配。这允许在本地命名时具有灵活性,同时遵守远程团队命名约定。
$ docker tag ansible-docker:v0.1 ericchou1/mastering-python-networking-example:ch05-ansible-dockerv-v0.1
$ docker push ericchou1/mastering-python-networking-example:ch05-ansible-dockerv-v0.1
一旦镜像上传完成,我们就可以访问该镜像,可以直接使用它,或者将其用作另一个 Dockerfile 中的基础镜像。

图 5.4:新上传的镜像
在下一节中,我们将看到如何在开发过程中本地协调多容器设置。
使用 Docker-compose 进行容器编排
现代应用程序通常相互依赖。例如,对于 Web 应用程序,我们通常有一个“堆栈”的应用程序。流行的 LAMP 堆栈是一个缩写,代表 Linux、Apache、MySQL 和 PHP/Python,用于指定交付 Web 应用程序所需的组件。在 Docker 的世界里,我们可以使用 docker-compose (docs.docker.com/compose/)来指定如何同时构建和运行多个容器。
如果你已经安装了适用于 Mac 或 Windows 的 Docker Desktop,docker-compose 已经包含在内。在 Linux 环境中,需要单独安装 docker-compose。我们将遵循 DigitalOcean 的指南来安装 docker-compose (www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-22-04):
$ mkdir -p ~/.docker/cli-plugins/
$ curl -SL https://github.com/docker/compose/releases/download/v2.3.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
$ chmod +x ~/.docker/cli-plugins/docker-compose
$ docker compose version
Docker Compose version v2.3.3
Docker-compose 使用名为docker-compose.yml的 YAML 文件来构建环境。有许多旋钮可以指定不同的服务依赖关系、持久卷和打开公共端口。让我们来构建一个简单的示例:
version: '3.9'
services:
ansible:
build:
dockerfile: dockerfile
db:
image: postgres:14.1-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- '5432:5432'
volumes:
- db:/var/lib/postgresql/data
volumes:
db:
driver: local
文件中指定了以下内容:
-
文件指定了两个服务,
ansible和db。每个服务都与docker run命令类似。 -
ansible服务使用当前工作目录中名为dockerfile的当前 Dockerfile 进行构建。 -
我们将主机端口 5434 映射到容器端口 5434。
-
我们为 Postgres 数据库指定了两个环境变量。
-
我们使用名为
db的卷,以便将写入数据库的信息持久化在卷中。
想了解更多关于 Docker-compose 的信息,请访问docs.docker.com/compose/。
我们可以使用docker-compose命令运行组合服务:
$ docker compose up
…
Container ansible_container-db-1 Created 0.0s
Container ansible_container-ansible-1 Created 0.0s
ansible_container-db-1 |
ansible_container-db-1 | PostgreSQL Database directory appears to contain a database; Skipping initialization
ansible_container-db-1 |
ansible_container-db-1 | 2022-09-13 00:18:45.195 UTC [1] LOG: starting PostgreSQL 14.1 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.3.1_git20211027) 10.3.1 20211027, 64-bit
ansible_container-db-1 | 2022-09-13 00:18:45.196 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
ansible_container-db-1 | 2022-09-13 00:18:45.196 UTC [1] LOG: listening on IPv6 address "::", port 5432
ansible_container-db-1 | 2022-09-13 00:18:45.198 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
ansible_container-db-1 | 2022-09-13 00:18:45.201 UTC [21] LOG: database system was shut down at 2022-09-13 00:18:36 UTC
ansible_container-db-1 | 2022-09-13 00:18:45.204 UTC [1] LOG: database system is ready to accept connections
…
服务是并发启动的。然后我们可以同时关闭这两个服务:
$ docker compose down
[+] Running 3/3
Container ansible_container-db-1 Removed 0.2s
Container ansible_container-ansible-1 Removed 0.0s
Network ansible_container_default Removed 0.1s
到目前为止,本书中我们只构建了简单的应用程序。当我们学习本书后面的构建 Web API 时,这可能会更有意义。现在,考虑一下我们如何通过 docker-compose 启动多个容器是很好的。
作为网络工程师,了解 Docker 环境中的网络实现方式会很有趣。这就是下一节的主题。
容器网络
容器网络不是一个容易涵盖的主题,因为它涉及的范围和触及的技术数量。这个空间从 Linux 网络,特定类型的 Linux(Ubuntu、Red Hat 等)如何实现网络,到 Docker 对网络的实现。增加复杂性的是,Docker 是一个快速发展的项目,许多第三方插件都可用。
在本节中,我们将坚持介绍 Docker 默认提供的网络选项的基本知识。然后我们将简要解释 overlay、Macvlan 和网络插件选项。
当我们启动一个容器时,它默认可以访问互联网。让我们通过启动一个 Ubuntu 容器并附加到它来进行快速测试:
$ docker run -it ubuntu:22.04
<container launches and attached>
root@dcaa61a548be:/# apt update && apt install -y net-tools iputils-ping
root@dcaa61a548be:/# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.2 netmask 255.255.0.0 broadcast 172.17.255.255
<skip>
root@dcaa61a548be:/# ping -c 1 www.cisco.com
PING e2867.dsca.akamaiedge.net (104.71.231.76) 56(84) bytes of data.
64 bytes from a104-71-231-76.deploy.static.akamaitechnologies.com (104.71.231.76): icmp_seq=1 ttl=53 time=11.1 ms
--- e2867.dsca.akamaiedge.net ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 11.147/11.147/11.147/0.000 ms
我们可以看到主机有一个与我们的主机 IP 不同的私有 IP。它还可以访问 Ubuntu 仓库来安装软件以及 ping 外部网络。它是如何做到这一点的呢?默认情况下,Docker 创建了三种类型的网络:bridge、host和none。让我们在保持第一个终端窗口中主机运行的同时,打开第二个终端窗口:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
78e7ab7ea276 bridge bridge local
93c142329fc9 host host local
da9fe0ed2308 none null local
none网络选项很简单。它禁用了所有网络,使容器独自坐在一个网络孤岛上。这让我们只剩下bridge和host选项。默认情况下,Docker 将主机放入桥接网络docker0,并使用虚拟以太网(veth)接口(man7.org/linux/man-pages/man4/veth.4.html)来允许它与互联网通信:
$ ip link show
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:86:7f:f2:40 brd ff:ff:ff:ff:ff:ff
21: veth3fda84e@if20: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT group default
link/ether 9a:f8:83:ae:cb:ea brd ff:ff:ff:ff:ff:ff link-netnsid 0
如果我们启动另一个容器,我们会看到创建了一个额外的 veth 接口,并将其放入相同的桥接组。默认情况下,它们可以相互通信。
容器主机网络
我们也可以将主机网络与容器共享。让我们在主机网络中启动一个 Ubuntu 容器。我们还将安装 Python 3.10 和其他软件包:
$ docker run -it --network host ubuntu:22.04
root@network-dev-4:/# apt update && apt install -y net-tools iputils-ping python3.10 vim
root@network-dev-4:/# ifconfig ens160
ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.2.126 netmask 255.255.255.0 broadcast 192.168.2.255
如果我们现在检查,我们可以看到容器现在与主机网络共享相同的 IP。我们可以创建一个简单的 HTML 页面,并在容器上启动 Python3 内置的 Web 服务器:
root@network-dev-4:/# cat index.html
<html>
<head></head>
<body><h1>Hello Networkers!</h1></body>
</html>
root@network-dev-4:/# python3.10 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
如果我们在浏览器中打开端口 8000 的 IP 地址,我们可以看到我们创建的页面!

图 5.5:容器主机索引页
如果你主机上(如 iptables 或 ufw)有开启的防火墙,请确保打开端口 8000,以便你可以看到页面。
当我们需要公开服务暴露容器时,主机网络选项很有用。
自定义桥接网络
我们还可以创建自定义桥接网络并将容器分组在一起。我们首先创建网络:
$ docker network create network1
我们现在可以将容器分配到自定义桥接网络:
$ docker run -it --network network1 ubuntu:22.04
root@41a977cd9c5b:/# apt update && apt install -y net-tools iputils-ping
root@41a977cd9c5b:/# ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.18.0.2 netmask 255.255.0.0 broadcast 172.18.255.255
<skip>
root@41a977cd9c5b:/# ping -c 1 www.cisco.com
PING e2867.dsca.akamaiedge.net (23.206.3.148) 56(84) bytes of data.
64 bytes from a23-206-3-148.deploy.static.akamaitechnologies.com (23.206.3.148): icmp_seq=1 ttl=53 time=13.2 ms
主机现在在其自定义桥接网络中。它有权访问公共互联网和同一桥接网络中的其他容器。如果我们想将特定端口公开到自定义桥接网络中的容器,我们可以使用–publish选项将端口映射到本地主机:
$ docker run -it --network network1 --publish 8000:8000 ubuntu:22.04
我们可以通过docker network rm来删除网络:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
30aa5d7887bc network1 bridge local
$ docker network rm network1
自定义网络选项非常适合开发需要彼此隔离的多容器项目。到目前为止,我们一直在查看单个主机中的网络选项。在下一节中,我们将看到容器之间主机间通信的选项。
其他容器网络选项
如果我们仔细查看docker network ls输出,我们可以看到driver和scope列。Docker 的网络子系统是可插拔的,使用驱动程序。核心网络功能由默认的bridge、host和none驱动程序提供。
其他值得注意的驱动程序如下所示:
-
叠加网络:叠加网络在多个 Docker 守护进程主机之间创建一个分布式网络。
-
Macvlan:macvlan 网络选项旨在为需要直接连接到物理网络的应用程序使用。
-
第三方网络插件:我们可以安装第三方网络插件(
hub.docker.com/search?q=&type=plugin)以获得更多功能。例如,vSphere-storage 插件(hub.docker.com/r/vmware/vsphere-storage-for-docker)使客户能够满足 vSphere 环境中容器的持久存储需求。
叠加网络驱动程序可能是我们将在开发阶段之后需要使用的选项。它的目的是处理数据包在 Docker 守护进程主机之间以及到正确目标容器的路由。例如,一个叠加入口网络将处理传入流量并将负载均衡到正确的容器。由于其复杂性,这通常由选择的编排工具处理,例如 Swarm 或 Kubernetes。如果我们使用公共云提供商,如 Google Kubernetes Engine,他们甚至可能为我们处理这个叠加网络。
网络工程领域的容器
容器技术正在改变现代基础设施的建设方式。我们现在有一个额外的抽象层,可以用来克服物理空间、电力、冷却和其他因素的限制。这对于需要向更环保的数据中心过渡的需求尤其如此。
与基于新容器的世界相关联有许多新的挑战和机遇:
-
容器世界的网络。正如我们在上一节中看到的,在容器中进行网络有许多选项。
-
DevOps。在尝试在网络工程中实施 DevOps 实践时,一个挑战是缺乏灵活的、虚拟化的网络设备选项。如果我们能够将网络与主机一起虚拟化,容器有潜力解决这个问题。
-
实验室和测试。如果我们可以通过容器镜像虚拟化网络,这将使实验室和测试变得更加容易。
我们将在第十二章中讨论 DevOps,与 GitLab 的持续集成;在下一节中,我们将探讨测试和运行容器化网络操作系统的新方法。
Containerlab
Containerlab (containerlab.dev/) 是运行容器化网络操作系统的一种方式。这是一个由诺基亚团队发起的项目,由 Roman Dodin 领导(twitter.com/ntdvps)。该团队还负责开发SR Linux(服务路由器 Linux),一个开源的网络操作系统(NOS)。尽管起源于诺基亚,Containerlab 支持多个厂商,包括 Arista cEOS、Azure SONiC、Juniper cRPD 等。让我们通过一个快速示例来说明 Containerlab 的工作流程。对于基于 Debian 的系统,我们可以遵循安装步骤(containerlab.dev/install/)进行安装。为了隔离安装,我们可以创建一个新的目录:
$ mkdir container_lab && cd container_lab
$ echo "deb [trusted=yes] https://apt.fury.io/netdevops/ /" | sudo tee -a /etc/apt/sources.list.d/netdevops.list
$ sudo apt update && sudo apt install containerlab
我们将定义一个clab文件来定义拓扑、镜像和起始配置。在/etc/containerlab/lab-examples/目录下有多个示例实验室。我们将使用包含两个 SR Linux 设备通过以太网接口连接的两个节点实验室示例(github.com/srl-labs/containerlab/blob/main/lab-examples/srl02/srl2.cfg)。由于 SR Linux 容器镜像可以通过公共仓库下载,这将节省我们单独下载容器镜像的步骤。我们将把这个实验室拓扑命名为srl02.clab.yml:
# topology documentation: http://containerlab.dev/lab-examples/two-srls/
# https://github.com/srl-labs/containerlab/blob/main/lab-examples/srl02/srl02.clab.yml
name: srl02
topology:
nodes:
srl1:
kind: srl
image: ghcr.io/nokia/srlinux
startup-config: srl1.cfg
srl2:
kind: srl
image: ghcr.io/nokia/srlinux
startup-config: srl2.cfg
links:
- endpoints: ["srl1:e1-1", "srl2:e1-1"]
如文件所示,拓扑由节点和链路组成。节点是 NOS 系统,而链路定义了它们的连接方式。这两个设备配置文件是厂商特定的,在这种情况下,是 SR Linux 配置:
$ cat srl1.cfg
set / interface ethernet-1/1
set / interface ethernet-1/1 subinterface 0
set / interface ethernet-1/1 subinterface 0 ipv4
set / interface ethernet-1/1 subinterface 0 ipv4 address 192.168.0.0/31
set / interface ethernet-1/1 subinterface 0 ipv6
set / interface ethernet-1/1 subinterface 0 ipv6 address 2002::192.168.0.0/127
set / network-instance default
set / network-instance default interface ethernet-1/1.0
$ cat srl2.cfg
set / interface ethernet-1/1
set / interface ethernet-1/1 subinterface 0
set / interface ethernet-1/1 subinterface 0 ipv4
set / interface ethernet-1/1 subinterface 0 ipv4 address 192.168.0.1/31
set / interface ethernet-1/1 subinterface 0 ipv6
set / interface ethernet-1/1 subinterface 0 ipv6 address 2002::192.168.0.1/127
我们现在可以使用containerlab的deploy命令启动实验室:
$ sudo containerlab deploy --topo srl02.clab.yml
[sudo] password for echou:
INFO[0000] Containerlab v0.31.1 started
INFO[0000] Parsing & checking topology file: srl02.clab.yml
…
从技术上讲,我们不需要使用 —topo 选项来指定拓扑。Containerlab 默认会查找 *.clab.yml 拓扑文件。然而,我发现指定一个拓扑文件是一个好的实践,以防我们在同一个目录中有多个拓扑文件。
如果成功,我们将看到设备信息。设备名称的格式为 clab-{ 实验室名称 }-{ 设备名称 }:
+---+-----------------+--------------+-----------------------+------+---------+----------------+----------------------+
| # | Name | Container ID | Image | Kind | State | IPv4 Address | IPv6 Address |
+---+-----------------+--------------+-----------------------+------+---------+----------------+----------------------+
| 1 | clab-srl02-srl1 | 7cae81c710d8 | ghcr.io/nokia/srlinux | srl | running | 172.20.20.2/24 | 2001:172:20:20::2/64 |
| 2 | clab-srl02-srl2 | c75f274284ef | ghcr.io/nokia/srlinux | srl | running | 172.20.20.3/24 | 2001:172:20:20::3/64 |
+---+-----------------+--------------+-----------------------+------+---------+----------------+----------------------+
我们可以通过 ssh 访问设备;默认用户名和密码都是 admin:
$ ssh admin@172.20.20.3
admin@172.20.20.3's password:
Using configuration file(s): []
Welcome to the srlinux CLI.
Type 'help' (and press <ENTER>) if you need any help using this.
--{ running }--[ ]--
A:srl1# show version
-------------------------------------------------------------------------------------------------------------------------------
Hostname : srl1
Chassis Type : 7220 IXR-D2
Part Number : Sim Part No.
Serial Number : Sim Serial No.
System HW MAC Address: 1A:85:00:FF:00:00
Software Version : v22.6.3
Build Number : 302-g51cb1254dd
Architecture : x86_64
Last Booted : 2022-09-12T03:12:15.195Z
Total Memory : 1975738 kB
Free Memory : 219406 kB
-------------------------------------------------------------------------------------------------------------------------------
--{ running }--[ ]--
A:srl1#
A:srl1# quit
将创建一个目录,其中包含与实验室相关的文件:
$ ls clab-srl02/*
clab-srl02/ansible-inventory.yml clab-srl02/topology-data.json
clab-srl02/ca:
root srl1 srl2
clab-srl02/srl1:
config topology.yml
clab-srl02/srl2:
config topology.yml
我们还可以看到,使用连接到桥接网络的两个 veth 接口创建了一个额外的桥接网络:
(venv) $ ip link show
11: br-4807fa9091c5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
link/ether 02:42:72:7a:9d:af brd ff:ff:ff:ff:ff:ff
13: veth3392afa@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-4807fa9091c5 state UP mode DEFAULT group default
link/ether be:f0:1a:f2:12:23 brd ff:ff:ff:ff:ff:ff link-netnsid 1
15: veth7417e97@if14: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master br-4807fa9091c5 state UP mode DEFAULT group default
link/ether 92:53:d3:ac:20:93 brd ff:ff:ff:ff:ff:ff link-netnsid 0
我们可以使用 containerlab destroy 命令来拆除实验室:
$ sudo containerlab destroy --topo srl02.clab.yml
[sudo] password for echou:
INFO[0000] Parsing & checking topology file: srl02.clab.yml
INFO[0000] Destroying lab: srl02
INFO[0001] Removed container: clab-srl02-srl2
INFO[0001] Removed container: clab-srl02-srl1
INFO[0001] Removing containerlab host entries from /etc/hosts file
我不知道你们的情况如何,但 Containerlab 是我见过的最容易启动网络实验室的方法。随着更多厂商的支持,它可能有一天会成为我们进行网络测试所需的唯一实验室和测试软件。
在下一节中,我们将简要讨论 Docker 和 Kubernetes 之间的关系,并对 Kubernetes 进行简要概述。
Docker 和 Kubernetes
正如我们所见,可以使用 Docker 社区提供的工具进行 Docker 镜像和编排。然而,不提及 Kubernetes 就几乎无法想象 Docker 容器。这是因为当涉及到容器编排时,Kubernetes 正在成为这一领域的实际标准。在这一章中,没有足够的空间来涵盖 Kubernetes,但由于它与容器编排的紧密联系,我们至少应该了解 Kubernetes 的基础知识。
Kubernetes (kubernetes.io/) 最初由 Google 开发,但现在由云原生计算基金会管理。它是一个开源的容器编排系统,可以自动部署、扩展和管理容器。该项目从一开始就得到了社区的广泛欢迎,因为它在 Google 内部使用中证明了其可扩展性。
Kubernetes 使用主节点作为控制单元来管理工作节点以部署容器。每个工作节点可以有一个或多个 pod,pod 是 Kubernetes 中的最小单元。pod 是容器部署的地方。当容器部署时,它们通常被分组到分布在 pod 中的不同类型的集合中。
大多数公共云提供商(AWS、Azure、Google 和 DigitalOcean)都提供用户可以尝试的托管 Kubernetes 集群。Kubernetes 文档 (kubernetes.io/docs/home/) 也提供了许多教程,用于逐步学习更多关于这项技术的内容。
摘要
在本章中,我们学习了容器虚拟化。容器在隔离计算资源方面与虚拟机类似,但在轻量级和快速部署方面有所不同。
我们看到了如何使用 Docker 容器来构建 Python 应用程序,以及如何使用 docker-compose 在单个主机上构建多容器应用程序。
在本章的后面部分,我们学习了如何通过使用默认桥接、自定义桥接和主机选项来使用 Docker 容器构建网络。容器还可以通过 Containerlab 项目帮助进行网络操作系统测试。
在下一章中,我们将探讨如何使用 Python 进行网络安全。
加入我们的书籍社区
要加入本书的社区——在这里您可以分享反馈、向作者提问,并了解新书发布——请扫描下面的二维码:
packt.link/networkautomationcommunity

第六章:使用 Python 进行网络安全
在我看来,网络安全是一个难以写作的话题。原因不是技术性的,而是与设定正确的范围有关。网络安全的边界非常广泛,触及 OSI 模型的七个层次。从第 1 层的窃听到第 4 层的传输协议漏洞,再到第 7 层的中间人欺骗,网络安全无处不在。新发现的所有漏洞都加剧了这个问题,有时似乎每天都有发生。这还不包括网络安全中的人为社会工程方面。
因此,在本章中,我想设定我们将讨论的范围。正如我们到目前为止所做的那样,我们将主要关注使用 Python 在 OSI 第 3 层和第 4 层进行网络设备安全。我们将查看我们可以用于管理单个网络设备以进行安全目的的 Python 工具,以及使用 Python 作为连接不同组件的粘合剂。希望我们能够使用 Python 在不同 OSI 层上全面处理网络安全。
在本章中,我们将探讨以下主题:
-
实验室设置
-
Python Scapy 用于安全测试
-
访问列表
-
使用 Python 的 Syslog 和简单防火墙(UFW)进行取证分析
-
其他工具,如 MAC 地址过滤器列表、私有 VLAN 和 Python IP 表绑定
让我们从本章的实验室设置开始看起。
实验室设置
本章中使用的设备与前面章节略有不同。在前面章节中,我们隔离了一组特定的设备。对于本章,我们将在实验室中使用更多的 Linux 主机来展示我们将使用的工具的功能。连接性和操作系统信息很重要,因为它们与本章后面我们将展示的安全工具有关。例如,如果我们想应用访问列表来保护服务器,我们需要知道拓扑结构是什么样的,以及客户端连接的方向。Ubuntu 主机的连接方式与我们所见的不同,所以如果需要,请参考此实验室部分,稍后查看示例。
我们将使用与 NYC 节点相同的 Cisco CML 工具,并添加两个额外的 Ubuntu 主机。实验室拓扑结构包含在课程文件中。
在 CML 中添加 Linux 主机的步骤与添加网络节点相同,只需点击添加节点并选择 Ubuntu 即可。我们将连接到nyc-cor-r1的外部主机命名为客户端,连接到nyc-cor-edg-r1的主机命名为服务器:

图 6.1:添加 Ubuntu 主机
这是一个回顾和学习 Ubuntu Linux 网络的好时机。我们将花一些时间列出设置中的 Ubuntu Linux 网络选项。以下是实验室拓扑的概述:

图 6.2:实验室拓扑
列出的 IP 地址可能因您的实验室而异。这里列出它们是为了在章节剩余部分的代码示例中方便引用。
我们将在主机上添加两个双栈链接,一个用于默认网关,连接到未管理的交换机以进行管理和互联网访问。另一个链接用于路由互联网流量。如图所示,我们将使用 hostname <name> 命令将顶部的宿主命名为客户端,将底部的宿主命名为服务器。这类似于一个互联网客户端试图访问我们网络内的企业服务器。CML 软件中使用的 Ubuntu Linux 版本是 18.04 LTS:
ubuntu@client:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.3 LTS
Release: 18.04
Codename: bionic
要列出和启用链接,我们可以使用 ip link 和 ifconfig 命令:
ubuntu@client:~$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:1e:bc:51 brd ff:ff:ff:ff:ff:ff
3: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:19:54:b5 brd ff:ff:ff:ff:ff:ff
ubuntu@ubuntu:~$ sudo ifconfig ens3 up
对于主机,当我们首次启动它时,它将在 /etc/netplan/50-cloud-init.yaml 下有一个初始网络配置。我们将备份它并创建自己的配置:
ubuntu@ubuntu:/etc/netplan$ cd /etc/netplan/
ubuntu@ubuntu:/etc/netplan$ cp 50-cloud-init.yaml 50-cloud-init.yaml.bak
ubuntu@ubuntu:/etc/netplan$ sudo rm 50-cloud-init.yaml
ubuntu@ubuntu:/etc/netplan$ sudo touch 50-cloud-init.yaml
对于两个网络链接,我们将使用以下配置来配置 ens3 的默认网关(管理和互联网)以及内部链接:
ubuntu@client:~$ cat /etc/netplan/50-cloud-init.yaml
network:
version: 2
renderer: networkd
ethernets:
ens3:
dhcp4: no
dhcp6: no
addresses: [192.168.2.152/24]
gateway4: 192.168.2.1
nameservers:
addresses: [192.168.2.1,8.8.8.8]
ens2:
dhcp4: no
dhcp6: no
addresses: [10.0.0.5/30]
要使网络更改生效,我们可以使用 netplan apply 命令:
ubuntu@ubuntu:/etc/netplan$ sudo netplan apply
这里是服务器端的一个快速输出:
ubuntu@server:~$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:12:9c:5f brd ff:ff:ff:ff:ff:ff
3: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:0e:f7:ab brd ff:ff:ff:ff:ff:ff
ubuntu@server:~$ cat /etc/netplan/50-cloud-init.yaml
network:
version: 2
renderer: networkd
ethernets:
ens3:
dhcp4: no
dhcp6: no
addresses: [192.168.2.153/24]
gateway4: 192.168.2.1
nameservers:
addresses: [192.168.2.1,8.8.8.8]
ens2:
dhcp4: no
dhcp6: no
addresses: [10.0.0.9/30]
我们将把连接的网络放入现有的 OSPF 网络中。以下是 nyc-cor-r1 的配置:
nyc-cor-r1# config t
Enter configuration commands, one per line. End with CNTL/Z.
nyc-cor-r1(config)# int ethernet 2/4
nyc-cor-r1(config-if)# ip add 10.0.0.6/24
nyc-cor-r1(config-if)# ip router ospf 200 area 0.0.0.200
nyc-cor-r1(config-if)# no shut
nyc-cor-r1(config-if)# end
nyc-cor-r1# ping 10.0.0.5
PING 10.0.0.5 (10.0.0.5): 56 data bytes
36 bytes from 10.0.0.6: Destination Host Unreachable
Request 0 timed out
64 bytes from 10.0.0.5: icmp_seq=1 ttl=63 time=4.888 ms
64 bytes from 10.0.0.5: icmp_seq=2 ttl=63 time=2.11 ms
64 bytes from 10.0.0.5: icmp_seq=3 ttl=63 time=2.078 ms
64 bytes from 10.0.0.5: icmp_seq=4 ttl=63 time=0.965 ms
^C
--- 10.0.0.5 ping statistics ---
5 packets transmitted, 4 packets received, 20.00% packet loss
round-trip min/avg/max = 0.965/2.51/4.888 ms
nyc-cor-r1#
nyc-cor-edg-r1 的配置如下:
nyc-edg-r1#confi t
Enter configuration commands, one per line. End with CNTL/Z.
nyc-edg-r1(config)#int gig 0/2
nyc-edg-r1(config-if)#ip add 10.0.0.10 255.255.255.252
nyc-edg-r1(config-if)#no shut
nyc-edg-r1(config-if)#end
nyc-edg-r1#ping 10.0.0.9
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.0.0.9, timeout is 2 seconds:
.!!!!
Success rate is 80 percent (4/5), round-trip min/avg/max = 1/3/7 ms
nyc-edg-r1#
nyc-edg-r1#confi t
Enter configuration commands, one per line. End with CNTL/Z.
nyc-edg-r1(config)#router ospf 200
nyc-edg-r1(config-router)#net
nyc-edg-r1(config-router)#network 10.0.0.8 0.0.0.3 area 200
nyc-edg-r1(config-router)#end
nyc-edg-r1#
下面是可能对那些对基于主机的网络不太熟悉的工程师来说有点棘手的部分。默认情况下,主机也有一个路由优先级。我们为 ens3 添加的默认网关将允许我们使用实验室网关作为“最后的手段”的目标。我们可以通过 route 命令在主机上查看路由表:
ubuntu@client:~$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.2.1 0.0.0.0 UG 0 0 0 ens3
10.0.0.4 0.0.0.0 255.255.255.252 U 0 0 0 ens2
192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 ens3
我们将使用以下命令通过 route 命令将客户端到服务器的流量路由:
ubuntu@client:~$ sudo route add -net 10.0.0.8/30 gw 10.0.0.6
ubuntu@client:~$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.2.1 0.0.0.0 UG 0 0 0 ens3
10.0.0.4 0.0.0.0 255.255.255.252 U 0 0 0 ens2
10.0.0.8 10.0.0.6 255.255.255.252 UG 0 0 0 ens2
192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 ens3
我们将在服务器端做同样的事情:
ubuntu@server:~$ sudo route add -net 10.0.0.4/30 gw 10.0.0.10
ubuntu@server:~$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.2.1 0.0.0.0 UG 0 0 0 ens3
10.0.0.4 10.0.0.10 255.255.255.252 UG 0 0 0 ens2
10.0.0.8 0.0.0.0 255.255.255.252 U 0 0 0 ens2
192.168.2.0 0.0.0.0 255.255.255.0 U 0 0 0 ens3
为了验证客户端到服务器的路径,让我们使用 ping 和路由跟踪来确保主机之间的流量是通过网络设备而不是默认路由来传输的:
# Install on both client and server
ubuntu@ubuntu:~$ sudo apt install traceroute
# From Server to Client
ubuntu@server:~$ ping -c 1 10.0.0.5
PING 10.0.0.5 (10.0.0.5) 56(84) bytes of data.
64 bytes from 10.0.0.5: icmp_seq=1 ttl=62 time=3.38 ms
--- 10.0.0.5 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 3.388/3.388/3.388/0.000 ms
ubuntu@server:~$ traceroute 10.0.0.5
traceroute to 10.0.0.5 (10.0.0.5), 30 hops max, 60 byte packets
1 10.0.0.10 (10.0.0.10) 2.829 ms 5.832 ms 7.396 ms
2 * * *
3 10.0.0.5 (10.0.0.5) 11.458 ms 11.459 ms 11.744 ms
# From Client to Server
ubuntu@client:~$ ping -c 1 10.0.0.9
PING 10.0.0.9 (10.0.0.9) 56(84) bytes of data.
64 bytes from 10.0.0.9: icmp_seq=1 ttl=62 time=3.32 ms
--- 10.0.0.9 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 3.329/3.329/3.329/0.000 ms
ubuntu@client:~$ traceroute 10.0.0.9
traceroute to 10.0.0.9 (10.0.0.9), 30 hops max, 60 byte packets
1 10.0.0.6 (10.0.0.6) 3.187 ms 3.318 ms 3.804 ms
2 * * *
3 10.0.0.9 (10.0.0.9) 11.845 ms 12.030 ms 12.035 ms
最后的任务是为本章剩余部分准备主机,更新仓库:
$ sudo apt update && sudo apt upgrade -y
$ sudo apt install software-properties-common -y
$ sudo add-apt-repository ppa:deadsnakes/ppa
$ sudo apt install -y python3.10 python3.10-venv
$ python3.10 -m venv venv
$ source venv/bin/activate
太好了!我们的实验室已经准备好了;我们现在可以查看一些使用 Python 的安全工具和措施。
Python Scapy
Scapy (scapy.net) 是一个基于 Python 的强大交互式数据包构建程序。据我所知,除了少数昂贵的商业程序之外,几乎没有其他工具能像 Scapy 那样做到。它是我的 Python 最喜欢的工具之一。
Scapy 的主要优势在于它允许我们从非常基础的级别构建数据包。用 Scapy 创始人的话来说:
“Scapy 是一个强大的交互式数据包操纵程序。它能够伪造或解码大量协议的数据包,将它们发送到线路上,捕获它们,匹配请求和回复,等等……与其他大多数工具相比,你不会构建作者没有想象到的东西。这些工具是为特定目标构建的,不能偏离太多。”
现在,让我们看看这个工具。
安装 Scapy
在 Python 3 支持方面,Scapy 经历了一段有趣的历程。在 2015 年,从版本 2.2.0 开始,Scapy 有一个独立的分支,名为Scapy3k,旨在支持 Python 3。在这本书中,我们使用原始 Scapy 项目的代码库。如果您已经阅读了本书的前一版并使用了仅兼容 Python 2 的 Scapy 版本,请查看 Scapy 每个版本的 Python 3 支持:


图 6.3:Python 版本支持(来源:https://scapy.net/download/)
我们将从源安装官方版本:
(venv) ubuntu@[server|client]:~$ git clone https://github.com/secdev/scapy.git
(venv) ubuntu@[server|client]:~$ cd scapy/
(venv) ubuntu@[server|client]:~/scapy$ sudo python3 setup.py install
(venv) ubuntu@[server|client]:~/scapy$ pip install scapy
安装后,我们可以通过在命令提示符中输入scapy来启动 Scapy 交互式 shell:
(venv) ubuntu@client:~$ sudo scapy
…
aSPY//YASa
apyyyyCY//////////YCa |
sY//////YSpcs scpCY//Pp | Welcome to Scapy
ayp ayyyyyyySCP//Pp syY//C | Version 2.5.0rc1.dev16
AYAsAYYYYYYYY///Ps cY//S |
pCCCCY//p cSSps y//Y | https://github.com/secdev/scapy
SPPPP///a pP///AC//Y |
A//A cyP////C | Have fun!
p///Ac sC///a |
P////YCpc A//A | What is dead may never die!
scccccp///pSP///p p//Y | -- Python 2
sY/////////y caa S//P |
cayCyayP//Ya pY/Ya
sY/PsY////YCc aC//Yp
sc sccaCY//PCypaapyCP//YSs
spCPY//////YPSps
ccaacs
这里是一个快速测试,以确保我们可以从 Python 3 访问Scapy库:
(venv) ubuntu@client:~$ python3.10
Python 3.10.7 (main, Sep 7 2022, 15:23:21) [GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from scapy.all import *
>>> exit()
太棒了!Scapy 现在已安装,可以从我们的 Python 解释器中执行。让我们在下一节通过交互式 shell 查看其用法。
交互式示例
在我们的第一个例子中,我们将在客户端构建一个互联网控制消息协议(ICMP)包并发送到服务器。在服务器端,我们将使用带有主机过滤器的tcpdump来查看进入的包:
## Client Side
ubuntu@client:~/scapy$ sudo scapy
>>> send(IP(dst="10.0.0.9")/ICMP())
.
Sent 1 packets.
# Server side
ubuntu@server:~/scapy$ sudo tcpdump -i ens2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens2, link-type EN10MB (Ethernet), capture size 262144 bytes
02:02:24.402707 Loopback, skipCount 0, Reply, receipt number 0, data (40 octets)
02:02:24.658511 IP 10.0.0.5 > server: ICMP echo request, id 0, seq 0, length 8
02:02:24.658532 IP server > 10.0.0.5: ICMP echo reply, id 0, seq 0, length 8
如您所见,使用 Scapy 构建包非常简单。Scapy 允许您通过斜杠(/)作为分隔符逐层构建包。send函数在第三层操作,负责路由,并为您处理第二层。还有一个sendp()替代方案,它在第二层操作,这意味着您需要指定接口和链路层协议。
让我们看看如何使用send-request(sr)函数来捕获返回的包。我们使用sr的一个特殊变体,称为sr1,它只返回一个响应于发送的包的包:
>>> p = sr1(IP(dst="10.0.0.9")/ICMP())
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
>>> p
<IP version=4 ihl=5 tos=0x0 len=28 id=5717 flags= frag=0 ttl=62 proto=icmp chksum=0x527f src=10.0.0.9 dst=10.0.0.5 |<ICMP type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |<Padding load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>
>>>
有一个需要注意的事项是,sr()函数返回一个包含已响应和未响应列表的元组:
>>> p = sr(IP(dst="10.0.0.9")/ICMP())
.Begin emission:
.....Finished sending 1 packets.
*
Received 7 packets, got 1 answers, remaining 0 packets
>>> type(p)
<class 'tuple'>
现在,让我们看看元组中包含的内容:
>>> ans, unans = sr(IP(dst="10.0.0.9")/ICMP())
.Begin emission:
...Finished sending 1 packets.
..*
Received 7 packets, got 1 answers, remaining 0 packets
>>> type(ans)
<class 'scapy.plist.SndRcvList'>
>>> type(unans)
<class 'scapy.plist.PacketList'>
如果我们只查看响应包列表,我们可以看到它是一个包含我们发送的包以及返回的包的NamedTuple:
>>> for i in ans:
... print(type(i))
...
<class 'scapy.compat.NamedTuple.<locals>._NT'>
>>>
>>>
>>> for i in ans:
... print(i)
...
QueryAnswer(query=<IP frag=0 proto=icmp dst=10.0.0.9 |<ICMP |>>, answer=<IP version=4 ihl=5 tos=0x0 len=28 id=10871 flags= frag=0 ttl=62 proto=icmp chksum=0x3e5d src=10.0.0.9 dst=10.0.0.5 |<ICMP type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |<Padding load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>)
Scapy 还提供第七层构造,例如 DNS 查询。在以下示例中,我们正在查询一个公开的 DNS 服务器以解析www.google.com:
>>> p = sr1(IP(dst="8.8.8.8")/UDP()/DNS(rd=1,qd=DNSQR(qname="www.google.com")))
Begin emission:
Finished sending 1 packets.
......*
Received 7 packets, got 1 answers, remaining 0 packets
>>> p
<IP version=4 ihl=5 tos=0x20 len=76 id=20467 flags= frag=0 ttl=58 proto=udp chksum=0x5d3e src=8.8.8.8 dst=192.168.2.152 |<UDP sport=domain dport=domain len=56 chksum=0xf934 |<DNS id=0 qr=1 opcode=QUERY aa=0 tc=0 rd=1 ra=1 z=0 ad=0 cd=0 rcode=ok qdcount=1 ancount=1 nscount=0 arcount=0 qd=<DNSQR qname='www.google.com.' qtype=A qclass=IN |> an=<DNSRR rrname='www.google.com.' type=A rclass=IN ttl=115 rdlen=4 rdata=142.251.211.228 |> ns=None ar=None |>>>
>>>
让我们看看 Scapy 的一些其他功能。我们将从使用 Scapy 进行包捕获开始。
使用 Scapy 进行包捕获
作为网络工程师,我们在故障排除过程中必须不断在电缆上捕获包。我们通常使用 Wireshark 或类似工具,但 Scapy 也可以用来轻松捕获电缆上的包:
>>> a = sniff(filter="icmp", count=5)
>>> a.show()
0000 Ether / IP / ICMP 192.168.2.152 > 8.8.8.8 echo-request 0 / Raw
0001 Ether / IP / ICMP 8.8.8.8 > 192.168.2.152 echo-reply 0 / Raw
0002 Ether / IP / ICMP 192.168.2.152 > 8.8.8.8 echo-request 0 / Raw
0003 Ether / IP / ICMP 8.8.8.8 > 192.168.2.152 echo-reply 0 / Raw
0004 Ether / IP / ICMP 192.168.2.152 > 8.8.8.8 echo-request 0 / Raw
我们可以更详细地查看包,包括原始格式:
>>> for packet in a:
... print(packet.show())
...
###[ Ethernet ]###
dst = 08:b4:b1:18:01:39
src = 52:54:00:19:54:b5
type = IPv4
###[ IP ]###
version = 4
ihl = 5
tos = 0x0
len = 84
id = 38166
flags = DF
frag = 0
ttl = 64
proto = icmp
chksum = 0xd242
src = 192.168.2.152
dst = 8.8.8.8
\options \
###[ ICMP ]###
type = echo-request
code = 0
chksum = 0x6596
id = 0x502f
seq = 0x1
unused = ''
###[ Raw ]###
load = '\\xaa7%c\x00\x00\x00\x00\\xb2\\xcb\x01\x00\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567'
<skip>
我们已经看到了 Scapy 的基本工作原理。现在,让我们继续看看我们如何使用 Scapy 进行某些方面的常见安全测试。
TCP 端口扫描
对于任何潜在的黑客来说,第一步几乎总是尝试了解网络上哪些服务是开放的,以便将他们的攻击重点放在这些服务上。当然,我们需要打开某些端口来服务我们的客户;这是我们必须接受的风险的一部分。然而,我们应该关闭任何其他不必要的开放端口,以减少攻击面。我们可以使用 Scapy 对我们的主机进行简单的 TCP 开放端口扫描。
我们可以发送一个 SYN 数据包,看看服务器是否会为各种端口返回 SYN-ACK。让我们从 Telnet 开始,TCP 端口 23:
>>> p = sr1(IP(dst="10.0.0.9")/TCP(sport=666,dport=23,flags="S"))
Begin emission:
Finished sending 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets
>>> p.show()
###[ IP ]###
version= 4
ihl= 5
tos= 0x0
len= 40
id= 14089
flags= DF
frag= 0
ttl= 62
proto= tcp
chksum= 0xf1b9
src= 10.0.0.9
dst= 10.0.0.5
\options\
###[ TCP ]###
sport= telnet
dport= 666
seq= 0
ack= 1
dataofs= 5
reserved= 0
flags= RA
window= 0
chksum= 0x9911
urgptr= 0
options= []
注意,在这里的输出中,服务器对于 TCP 端口 23 返回了 RESET+ACK。然而,TCP 端口 22(SSH)是开放的;因此,返回了 SYN-ACK:
>>> p = sr1(IP(dst="10.0.0.9")/TCP(sport=666,dport=22,flags="S")).show()
###[ IP ]###
version= 4
<skip>
proto= tcp
chksum= 0x28bf
src= 10.0.0.9
dst= 10.0.0.5
\options\
###[ TCP ]###
sport= ssh
dport= 666
seq= 1671401418
ack= 1
dataofs= 6
reserved= 0
flags= SA
<skip>
我们还可以扫描从 20 到 22 的目标端口范围;注意,我们在这里使用 sr() 发送-接收而不是 sr1() 发送-接收一个数据包的变体:
>>> ans,unans = sr(IP(dst="10.0.0.9")/TCP(sport=666,dport=(20,22),flags="S"))
>>> for i in ans:
... print(i)
...
QueryAnswer(query=<IP frag=0 proto=tcp dst=10.0.0.9 |<TCP sport=666 dport=ftp_data flags=S |>>, answer=<IP version=4 ihl=5 tos=0x0 len=40 id=0 flags=DF frag=0 ttl=62 proto=tcp chksum=0x28c3 src=10.0.0.9 dst=10.0.0.5 |<TCP sport=ftp_data dport=666 seq=0 ack=1 dataofs=5 reserved=0 flags=RA window=0 chksum=0x9914 urgptr=0 |<Padding load='\x00\x00\x00\x00\x00\x00' |>>>)
QueryAnswer(query=<IP frag=0 proto=tcp dst=10.0.0.9 |<TCP sport=666 dport=ftp flags=S |>>, answer=<IP version=4 ihl=5 tos=0x0 len=40 id=0 flags=DF frag=0 ttl=62 proto=tcp chksum=0x28c3 src=10.0.0.9 dst=10.0.0.5 |<TCP sport=ftp dport=666 seq=0 ack=1 dataofs=5 reserved=0 flags=RA window=0 chksum=0x9913 urgptr=0 |<Padding load='\x00\x00\x00\x00\x00\x00' |>>>)
QueryAnswer(query=<IP frag=0 proto=tcp dst=10.0.0.9 |<TCP sport=666 dport=ssh flags=S |>>, answer=<IP version=4 ihl=5 tos=0x0 len=44 id=0 flags=DF frag=0 ttl=62 proto=tcp chksum=0x28bf src=10.0.0.9 dst=10.0.0.5 |<TCP sport=ssh dport=666 seq=4214084277 ack=1 dataofs=6 reserved=0 flags=SA window=29200 chksum=0x4164 urgptr=0 options=[('MSS', 1460)] |<Padding load='\x00\x00' |>>>)
我们也可以指定一个目标网络而不是单个主机。正如您从 10.0.0.8/29 块中可以看到,主机 10.0.0.9、10.0.0.10 和 10.0.0.14 返回了 SA,这对应于两个网络设备和主机:
>>> ans,unans = sr(IP(dst="10.0.0.8/29")/TCP(sport=666,dport=(22),flags="S"))
>>> for i in ans:
... print(i)
...
(<IP frag=0 proto=tcp dst=10.0.0.14 |<TCP sport=666 dport=ssh flags=S |>>, <IP version=4 ihl=5 tos=0x0 len=44 id=7289 flags= frag=0 ttl=64 proto=tcp chksum=0x4a41 src=10.0.0.14 dst=10.0.0.5 |<TCP sport=ssh dport=666 seq=1652640556 ack=1 dataofs=6 reserved=0 flags=SA window=17292 chksum=0x9029 urgptr=0 options=[('MSS', 1444)] |>>)
(<IP frag=0 proto=tcp dst=10.0.0.9 |<TCP sport=666 dport=ssh flags=S |>>, <IP version=4 ihl=5 tos=0x0 len=44 id=0 flags=DF frag=0 ttl=62 proto=tcp chksum=0x28bf src=10.0.0.9 dst=10.0.0.5 |<TCP sport=ssh dport=666 seq=898054835 ack=1 dataofs=6 reserved=0 flags=SA window=29200 chksum=0x9f0d urgptr=0 options=[('MSS', 1460)] |>>)
(<IP frag=0 proto=tcp dst=10.0.0.10 |<TCP sport=666 dport=ssh flags=S |>>, <IP version=4 ihl=5 tos=0x0 len=44 id=38021 flags= frag=0 ttl=254 proto=tcp chksum=0x1438 src=10.0.0.10 dst=10.0.0.5 |<TCP sport=ssh dport=666 seq=371720489 ack=1 dataofs=6 reserved=0 flags=SA window=4128 chksum=0x5d82 urgptr=0 options=[('MSS', 536)] |>>)
>>>
根据我们迄今为止所学的内容,我们可以编写一个简单的脚本以实现可重用性,scapy_tcp_scan_1.py:
#!/usr/bin/env python3
from scapy.all import *
import sys
def tcp_scan(destination, dport):
ans, unans = sr(IP(dst=destination)/TCP(sport=666,dport=dport,flags="S"))
for sending, returned in ans:
if 'SA' in str(returned[TCP].flags):
return destination + " port " + str(sending[TCP].dport) + " is open."
else:
return destination + " port " + str(sending[TCP].dport) + " is not open."
def main():
destination = sys.argv[1]
port = int(sys.argv[2])
scan_result = tcp_scan(destination, port)
print(scan_result)
if __name__ == "__main__":
main()
在脚本中,我们首先按照建议导入 scapy 和 sys 模块以接收参数。tcp_scan() 函数与我们迄今为止所看到的功能类似,唯一的区别是我们将其功能化,以便可以从参数获取输入,然后在 main() 函数中调用 tcp_scan() 函数。
记住,访问低级网络需要 root 权限;因此,我们的脚本需要以 sudo 方式执行。让我们在端口 22(SSH)和端口 80(HTTP)上尝试脚本:
ubunbu@client:~$ sudo python3 scapy_tcp_scan_1.py "10.0.0.14" 22
Begin emission:
......Finished sending 1 packets.
*
Received 7 packets, got 1 answers, remaining 0 packets
10.0.0.14 port 22 is open.
ubuntu@client:~$ sudo python3 scapy_tcp_scan_1.py "10.0.0.14" 80
Begin emission:
...Finished sending 1 packets.
*
Received 4 packets, got 1 answers, remaining 0 packets
10.0.0.14 port 80 is not open.
这是一个相对较长的 TCP 扫描脚本示例,展示了使用 Scapy 构造数据包的强大功能。我们在交互式外壳中测试了这些步骤,并通过一个简单的脚本最终确定了用法。现在,让我们看看 Scapy 在安全测试中的一些更多用法示例。
Ping 收集
假设我们的网络包含 Windows、Unix 和 Linux 机器的混合,网络用户根据 自带设备(BYOD)政策添加他们的机器;他们可能支持或不支持 ICMP ping。现在,我们可以构建一个包含我们网络三种常见 ping 的文件——ICMP、TCP 和 UDP ping——在 scapy_ping_collection.py 中:
#!/usr/bin/env python3
from scapy.all import *
def icmp_ping(destination):
# regular ICMP ping
ans, unans = sr(IP(dst=destination)/ICMP())
return ans
def tcp_ping(destination, dport):
ans, unans = sr(IP(dst=destination)/TCP(dport=dport,flags="S"))
return ans
def udp_ping(destination):
ans, unans = sr(IP(dst=destination)/UDP(dport=0))
return ans
def answer_summary(ans):
for send, recv in ans:
print(recv.sprintf("%IP.src% is alive"))
我们可以在一个脚本中执行网络上的所有三种类型的 ping:
def main():
print("** ICMP Ping **")
ans = icmp_ping("10.0.0.13-14")
answer_summary(ans)
print("** TCP Ping ***")
ans = tcp_ping("10.0.0.13", 22)
answer_summary(ans)
print("** UDP Ping ***")
ans = udp_ping("10.0.0.13-14")
answer_summary(ans)
if __name__ == "__main__":
main()
到目前为止,希望你会同意我的观点,通过拥有构建自定义数据包的能力,你可以掌控你想要运行的操作和测试类型。沿着使用 Scapy 构建自定义数据包的思路,我们也可以构建数据包来对我们网络进行安全测试。
常见攻击
在这个例子中,让我们看看我们如何构建我们的数据包来进行一些经典攻击,比如 死亡之 ping (en.wikipedia.org/wiki/Ping_of_death) 和 Land 攻击 (en.wikipedia.org/wiki/Denial-of-service_attack)。这些是以前你必须用类似商业软件付费的网络渗透测试。使用 Scapy,你可以在保持完全控制的同时进行测试,并在将来添加更多测试。
第一种攻击发送一个带有虚假 IP 头的数据包到目标主机,例如 IP 头长度为两个和 IP 版本为三个:
def malformed_packet_attack(host):
send(IP(dst=host, ihl=2, version=3)/ICMP())
ping_of_death_attack 由一个负载大于 65,535 字节的常规 ICMP 数据包组成:
def ping_of_death_attack(host):
# https://en.wikipedia.org/wiki/Ping_of_death
send(fragment(IP(dst=host)/ICMP()/("X"*60000)))
land_attack 试图将客户端的响应重定向回客户端,并耗尽主机的资源:
def land_attack(host):
# https://en.wikipedia.org/wiki/Denial-of-service_attack
send(IP(src=host, dst=host)/TCP(sport=135,dport=135))
这些是相当古老的漏洞或经典攻击,现代操作系统已经不再容易受到它们的影响。对于我们的 Ubuntu 20.04 主机,上述任何攻击都不会使其崩溃。然而,随着更多安全问题的发现,Scapy 是一个很好的工具,可以在等待受影响的供应商提供验证工具之前,对我们的网络和主机进行测试。这在互联网上越来越常见的零日(未提前通知发布)攻击中尤其如此。Scapy 是一个可以做很多我们在这个章节中无法涵盖的事情的工具,但幸运的是,Scapy 有许多开源资源可供参考。
Scapy 资源
我们在这一章中花费了很多努力与 Scapy 合作。这部分的理由是我对工具的高度评价。我希望你同意 Scapy 是我们网络工程师工具集中的一个伟大工具。Scapy 最好的部分是它始终由一个活跃的用户社区进行开发。
我强烈建议至少浏览一下 Scapy 教程 scapy.readthedocs.io/en/latest/usage.html#interactive-tutorial,以及任何对你感兴趣的文档。
当然,网络安全不仅仅是构建数据包和测试漏洞。在下一节中,我们将探讨自动化通常用于保护敏感内部资源的访问列表。
访问列表
网络访问列表通常是抵御外部入侵和攻击的第一道防线。一般来说,路由器和交换机通过利用高速内存硬件(如 三值内容可寻址存储器(TCAM))来处理数据包的速度比服务器快得多。它们不需要看到应用层信息。相反,它们只需检查第 3 层和第 4 层的头部信息,并决定是否可以转发数据包。因此,我们通常将网络设备访问列表作为保护网络资源的第一步。
根据经验法则,我们希望将访问列表尽可能靠近源(客户端)放置。本质上,我们也信任内部主机,不相信网络边界之外的客户端。因此,访问列表通常放置在外部网络接口的入站方向。在我们的实验室场景中,这意味着我们将在 nyc-cor-r1 的 Ethernet2/2 上放置一个入站访问列表,该接口直接连接到客户端主机。
如果你对访问列表的方向和位置不确定,以下几点可能对你有所帮助:
-
从网络设备的角度思考访问列表。
-
仅从源和目的 IP 地址的角度简化数据包,并使用一个主机作为示例。
-
在我们的实验室中,从我们的服务器到客户端的流量将具有源 IP 地址
10.0.0.9,目的 IP 地址为10.0.0.5。 -
从客户端到服务器的流量将具有源 IP 地址
10.0.0.5,目的 IP 地址为10.0.0.9。
显然,每个网络都是不同的,访问列表应该如何构建取决于你的服务器提供的服务。但是,作为一个入站边界访问列表,你应该做以下事情:
-
拒绝 RFC 3030 特殊用途地址源,例如
127.0.0.0/8。 -
拒绝 RFC 1918 空间,例如
10.0.0.0/8。 -
以源 IP 地址的形式拒绝我们的空间;在这种情况下,
10.0.0.4/30。 -
允许来自主机
10.0.0.9的入站 TCP 端口22(SSH)和80(HTTP)。 -
拒绝其他所有内容。
这里有一个很好的 bogon 网络列表,可以阻止:ipinfo.io/bogon。
知道要添加什么只是步骤的一半。在下一节中,让我们看看如何使用 Ansible 实现预期的访问列表。
使用 Ansible 实现访问列表
实现此访问列表的最简单方法就是使用 Ansible。我们已经在之前的章节中讨论过 Ansible,但在此场景下使用 Ansible 的优势值得重复提及:
-
易于管理:对于长的访问列表,我们可以使用
include语句将访问列表分解成更易于管理的部分。然后,较小的部分可以由其他团队或服务所有者管理。 -
幂等性:我们可以定期安排剧本执行,并且只有必要的更改会被做出。
-
每个任务都是明确的:我们可以分离条目的结构,并将访问列表应用到适当的接口上。
-
可重用性:在未来,如果我们添加额外的面向外部的接口,我们只需将设备添加到访问列表的设备列表中。
-
可扩展性:您会注意到我们可以使用相同的剧本来构建访问列表并将其应用到正确的接口上。我们可以从小处着手,并在需要时将其扩展为单独的剧本。
主机文件相当标准。我们还将遵循我们的标准,将变量放在host_vars文件夹中:
[nxosv-devices]
nyc-cor-r1
[iosv-devices]
nyc-edg-r1
$ cat host_vars/nyc-cor-r1
---
ansible_host: 192.168.2.60
ansible_user: cisco
ansible_ssh_pass: cisco
ansible_connection: network_cli
ansible_network_os: nxos
ansbile_become: yes
ansible_become_method: enable
ansible_become_pass: cisco
我们将在剧本中声明变量:
---
- name: Configure Access List
hosts: "nxosv-devices"
gather_facts: false
connection: local
vars:
cli:
host: "{{ ansible_host }}"
username: "{{ ansible_username }}"
password: "{{ ansible_password }}"
为了节省空间,我们只展示拒绝 RFC 1918 空间。实施 RFC 3030 和我们的空间的拒绝将与 RFC 1918 空间的步骤相同。请注意,在我们的剧本中我们没有拒绝10.0.0.0/8,因为我们的配置目前使用10.0.0.0网络进行寻址。当然,我们可以在第一个单主机允许之后,在稍后的条目中拒绝10.0.0.0/8,但在这个例子中,我们只是选择省略它:
tasks:
- nxos_acl:
name: border_inbound
seq: 20
action: deny
proto: tcp
src: 172.16.0.0/12
dest: any
log: enable
state: present
- nxos_acl:
name: border_inbound
seq: 30
action: deny
proto: tcp
src: 192.168.0.0/16
dest: any
state: present
log: enable
<skip>
注意,我们允许从服务器内部发起的已建立连接返回。我们使用最终的显式deny ip any语句作为高序列号(1000),这样我们就可以在以后插入任何新的条目。
然后,我们可以将访问列表应用到正确的接口上:
- name: apply ingress acl to Ethernet 2/4
nxos_acl_interface:
name: border_inbound
interface: Ethernet2/4
direction: ingress
state: present
这可能看起来为单个访问列表做的工作很多。对于经验丰富的工程师来说,使用 Ansible 来完成这项任务可能比仅仅登录到设备并配置访问列表要花更长的时间。然而,请记住,这个剧本可以在未来多次重用,所以从长远来看,这将为您节省时间。
根据我的经验,对于长访问列表,一些条目可能用于一个服务,一些条目用于另一个服务,依此类推。访问列表往往会随着时间的推移自然增长,并且很难追踪每个条目的来源和目的。我们可以将它们分开的事实使得管理长访问列表变得更加简单。
现在,让我们在nx-osv-1上执行剧本并verify:
$ ansible-playbook -i hosts access_list_nxosv.yml
PLAY [Configure Access List] ******************************************************************************
TASK [nxos_acl] *******************************************************************************************
ok: [nyc-cor-r1]
<skip>
TASK [nxos_acl] *******************************************************************************************
ok: [nyc-cor-r1]
TASK [apply ingress acl to Ethernet 2/4] ******************************************************************
changed: [nyc-cor-r1]
PLAY RECAP ************************************************************************************************
nyc-cor-r1 : ok=7 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
<skip>
我们应该登录到nyc-cor-r1来验证更改:
nyc-cor-r1# sh ip access-lists border_inbound
IP access list border_inbound
20 deny tcp 172.16.0.0/12 any log
30 deny tcp 192.168.0.0/16 any log
40 permit tcp any 10.0.0.9/32 eq 22 log
50 permit tcp any 10.0.0.9/32 eq www log
60 permit tcp any any established log
1000 deny ip any any log
nx-osv-1# sh run int eth 2/4
!
interface Ethernet2/1
description to Client
no switchport
mac-address fa16.3e00.0001
ip access-group border_inbound in
ip address 10.0.0.6/30
ip router ospf 1 area 0.0.0.0
no shutdown
我们已经看到了实现 IP 访问列表的示例,该访问列表在网络中检查第 3 层信息。在下一节中,让我们看看如何在第 2 层环境中限制设备访问。
MAC 访问列表
在您有第 2 层环境,或者在使用以太网接口上的非 IP 协议的情况下,您仍然可以使用基于 MAC 地址的访问列表来允许或拒绝基于 MAC 地址的主机。步骤与 IP 访问列表类似,但匹配将基于 MAC 地址。回想一下,对于 MAC 地址或物理地址,前六个十六进制符号属于一个组织唯一标识符(OUI)。因此,我们可以使用相同的访问列表匹配模式来拒绝一组特定的主机。
我们正在使用ios_config模块在 IOSv 上进行测试。对于较旧的 Ansible 版本,每次执行剧本时都会推送更改。对于较新的 Ansible 版本,控制节点会首先检查更改,只有在需要时才会进行更改。
host文件和剧本的上半部分与 IP 访问列表相似;tasks部分是使用不同模块和参数的地方:
<skip>
tasks:
- name: Deny Hosts with vendor id fa16.3e00.0000
ios_config:
lines:
- access-list 700 deny fa16.3e00.0000 0000.00FF.FFFF
- access-list 700 permit 0000.0000.0000 FFFF.FFFF.FFFF
- name: Apply filter on bridge group 1
ios_config:
lines:
- bridge-group 1
- bridge-group 1 input-address-list 700
parents
- interface GigabitEthernet0/1
我们可以在iosv-1上执行剧本并验证其应用:
$ ansible-playbook -i hosts access_list_mac_iosv.yml
TASK [Deny Hosts with vendor id fa16.3e00.0000] ****************************************************************************
changed: [nyc-edg-r1]
TASK [Apply filter on bridge group 1] ***************************************************************************************
changed: [nyc-edg-r1]
如我们之前所做的那样,让我们登录到设备以验证我们的更改:
nyc-edg-r1#sh run int gig 0/1
!
interface GigabitEthernet0/1
description to nyc-cor-r1
<skip>
bridge-group 1
bridge-group 1 input-address-list 700
end
随着更多虚拟网络的普及,第三层信息有时对底层虚拟链路来说是透明的。在这些情况下,如果需要限制对这些链路的访问,MAC 访问列表是一个不错的选择。在本节中,我们已使用 Ansible 自动化实现第二层和第三层访问列表的实施。现在,让我们稍微改变一下方向,但仍然保持在安全背景下,看看如何使用 Python 从syslogs中提取必要的安全信息。
Syslog 搜索
在一段较长的时间内,发生了许多记录在案的网络安全漏洞。在这些缓慢的漏洞中,我们经常在日志中看到迹象和痕迹,表明存在可疑活动。这些迹象可以在服务器和网络设备日志中找到。活动没有被检测到,并不是因为没有足够的信息,而是因为信息太多。我们正在寻找的关键信息通常深埋在难以整理的信息堆中。
除了 Syslog 之外,UFW 也是服务器日志信息的一个很好的来源。它是 IP 表的前端,而 IP 表是一个服务器防火墙。UFW 使得管理防火墙规则变得非常简单,并记录了大量信息。有关 UFW 的更多信息,请参阅其他工具部分。
在本节中,我们将尝试使用 Python 搜索 Syslog 文本,以检测我们正在寻找的活动。当然,我们将搜索的确切术语将取决于我们使用的设备。例如,Cisco 提供了一份在 Syslog 中查找任何访问列表违规日志的消息列表。它可在www.cisco.com/c/en/us/about/security-center/identify-incidents-via-syslog.html找到。
想要了解更多关于访问控制列表日志的信息,请访问www.cisco.com/c/en/us/about/security-center/access-control-list-logging.html。
在我们的练习中,我们将使用一个包含大约 65,000 行日志消息的 Nexus 交换机匿名 Syslog 文件。此文件已包含在本书的 GitHub 存储库中:
$ wc -l sample_log_anonymized.log
65102 sample_log_anonymized.log
我们已经从思科文档中插入了一些 Syslog 消息(www.cisco.com/c/en/us/support/docs/switches/nexus-7000-series-switches/118907-configure-nx7k-00.html)作为我们应该寻找的日志消息:
2014 Jun 29 19:20:57 Nexus-7000 %VSHD-5-VSHD_SYSLOG_CONFIG_I: Configured
from vty by admin on console0
2014 Jun 29 19:21:18 Nexus-7000 %ACLLOG-5-ACLLOG_FLOW_INTERVAL: Src IP:
10.1 0.10.1,
Dst IP: 172.16.10.10, Src Port: 0, Dst Port: 0, Src Intf: Ethernet4/1, Pro tocol: "ICMP"(1), Hit-count = 2589
2014 Jun 29 19:26:18 Nexus-7000 %ACLLOG-5-ACLLOG_FLOW_INTERVAL: Src IP:
10.1 0.10.1, Dst IP: 172.16.10.10, Src Port: 0, Dst Port: 0, Src Intf: Ethernet4/1, Pro tocol: "ICMP"(1), Hit-count = 4561
我们将使用简单的例子和正则表达式。如果你已经熟悉 Python 中的正则表达式模块,可以自由跳过本节的其余部分。
使用正则表达式模块进行搜索
对于我们的第一次搜索,我们将简单地使用正则表达式模块来搜索我们正在寻找的术语。我们将使用一个简单的循环来完成以下操作:
#!/usr/bin/env python3
import re, datetime
startTime = datetime.datetime.now()
with open('sample_log_anonymized.log', 'r') as f:
for line in f.readlines():
if re.search('ACLLOG-5-ACLLOG_FLOW_INTERVAL', line):
print(line)
endTime = datetime.datetime.now()
elapsedTime = endTime - startTime
print("Time Elapsed: " + str(elapsedTime))
搜索日志文件大约需要四百分之一秒:
$ python3 python_re_search_1.py
2014 Jun 29 19:21:18 Nexus-7000 %ACLLOG-5-ACLLOG_FLOW_INTERVAL: Src IP: 10.1 0.10.1,
2014 Jun 29 19:26:18 Nexus-7000 %ACLLOG-5-ACLLOG_FLOW_INTERVAL: Src IP: 10.1 0.10.1,
Time Elapsed: 0:00:00.047249
建议编译搜索词以实现更高效的搜索。由于脚本已经相当快,这不会对我们影响很大。Python 的解释性可能使其变慢。然而,当我们搜索更大的文本体时,这会有所不同,所以让我们做出改变:
searchTerm = re.compile('ACLLOG-5-ACLLOG_FLOW_INTERVAL')
with open('sample_log_anonymized.log', 'r') as f:
for line in f.readlines():
if re.search(searchTerm, line):
print(line)
实际的计时结果实际上更慢:
Time Elapsed: 0:00:00.081541
让我们稍微扩展一下这个例子。假设我们有几个文件和多个术语要搜索,我们将复制原始文件到一个新文件中:
$ cp sample_log_anonymized.log sample_log_anonymized_1.log
我们还将包括搜索PAM: Authentication failure这个术语。我们将添加另一个循环来搜索两个文件:
term1 = re.compile('ACLLOG-5-ACLLOG_FLOW_INTERVAL')
term2 = re.compile('PAM: Authentication failure')
fileList = ['sample_log_anonymized.log', 'sample_log_anonymized_1.log']
for log in fileList:
with open(log, 'r') as f:
for line in f.readlines():
if re.search(term1, line) or re.search(term2, line):
print(line)
通过扩展我们的搜索词和消息数量,我们现在可以看到性能的差异:
$ python3 python_re_search_2.py
2016 Jun 5 16:49:33 NEXUS-A %DAEMON-3-SYSTEM_MSG: error: PAM:
Authentication failure for illegal user AAA from 172.16.20.170 - sshd[4425]
2016 Sep 14 22:52:26.210 NEXUS-A %DAEMON-3-SYSTEM_MSG: error: PAM:
Authentication failure for illegal user AAA from 172.16.20.170 - sshd[2811]
<skip>
2014 Jun 29 19:21:18 Nexus-7000 %ACLLOG-5-ACLLOG_FLOW_INTERVAL: Src IP:
10.1 0.10.1,
2014 Jun 29 19:26:18 Nexus-7000 %ACLLOG-5-ACLLOG_FLOW_INTERVAL: Src IP:
10.1 0.10.1,
<skip>
Time Elapsed: 0:00:00.330697
当然,当谈到性能调整时,这是一个永无止境、不可能达到零点的竞赛,性能有时取决于你使用的硬件。但重要的是要定期使用 Python 对你的日志文件进行审计,这样你就可以捕捉到任何潜在违规的早期信号。
我们已经探讨了在 Python 中增强我们的网络安全的一些关键方法,但还有一些其他强大的工具可以使这个过程更容易、更有效。在本章的最后部分,我们将探讨一些这些工具。
其他工具
我们可以使用 Python 自动化并使用其他网络安全工具。让我们看看两个最常用的工具。
私有 VLAN
虚拟局域网(VLANs)已经存在很长时间了。它们本质上是一组广播域,其中所有主机都可以连接到单个交换机,但被分割到不同的域中,因此我们可以根据哪些主机可以通过广播看到其他主机来分离主机。让我们考虑一个基于 IP 子网的映射。例如,在一个企业建筑中,我可能会在每个物理楼层看到一个 IP 子网:一楼为192.168.1.0/24,二楼为192.168.2.0/24,依此类推。在这个模式中,我们为每个楼层使用一个/24块。这为我的物理网络以及我的逻辑网络提供了一个清晰的界定。想要在其子网之外通信的主机需要通过其层 3 网关进行穿越,在那里我可以使用访问列表来强制执行安全性。
当不同的部门位于同一楼层时会发生什么?也许财务和销售团队在二楼,我不希望销售团队的主持人与财务团队处于同一个广播域。我可以进一步细分子网,但这可能会变得繁琐,并破坏之前设置的子网标准方案。这就是私有 VLAN 能发挥作用的地方。
私有 VLAN 本质上将现有的 VLAN 分割成子 VLAN。私有 VLAN 中有三个类别:
-
混杂(P)端口:此端口允许从 VLAN 上的任何其他端口发送和接收层 2 帧;这通常属于连接到层 3 路由器的端口。
-
隔离(I)端口:此端口仅允许与 P 端口通信,通常在您不希望它与同一 VLAN 中的其他主机通信时连接到主机。
-
社区(C)端口:此端口允许与同一社区中的其他 C 端口以及 P 端口通信。
我们可以再次使用 Ansible 或之前介绍过的任何其他 Python 脚本来完成此任务。到目前为止,我们应该已经积累了足够的实践和信心,可以通过自动化来实现此功能,因此这里不再重复步骤。当您需要进一步隔离层 2 VLAN 中的端口时,了解私有 VLAN 功能将非常有用。
UFW 与 Python
我们简要提到了 UFW 作为 Ubuntu 主机上 IP 表的前端。以下是一个快速概述:
$ sudo apt-get install ufw
$ sudo ufw status
$ sudo ufw default outgoing
$ sudo ufw allow 22/tcp
$ sudo ufw allow www
$ sudo ufw default deny incoming
We can see the status of UFW:
$ sudo ufw status verbose Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
22/tcp (v6) ALLOW IN Anywhere (v6)
80/tcp (v6) ALLOW IN Anywhere (v6)
如您所见,UFW 的优势在于它提供了一个简单的界面来构建其他情况下复杂的 IP 表规则。我们可以使用与 UFW 一起使用的几个 Python 相关工具来使事情更加简单:
-
我们可以使用 Ansible UFW 模块来简化我们的操作。更多信息请参阅
docs.ansible.com/ansible/latest/collections/community/general/ufw_module.html。 -
在 UFW 旁边有 Python 包装模块作为 API(访问
gitlab.com/dhj/easyufw)。如果你需要根据某些事件动态修改 UFW 规则,这将使集成更容易。 -
UFW 本身是用 Python 编写的。因此,如果我们需要扩展当前的命令集,你可以使用现有的 Python 知识。更多信息请参阅
launchpad.net/ufw。
UFW 证明是一个很好的工具,可以保护你的网络服务器。
进一步阅读
Python 是一个非常常用的语言,在许多与安全相关的领域中使用。以下是我推荐的一些书籍:
-
《暴力 Python》:黑客、法医分析师、渗透测试人员和安全工程师的食谱,作者 T.J. O’Connor(ISBN-10: 1597499579)
-
《黑帽 Python》:黑客和渗透测试人员的 Python 编程,作者 Justin Seitz(ISBN-10: 1593275900)
我在 A10 Networks 的 分布式拒绝服务(DDoS)研究工作中广泛使用了 Python。如果你有兴趣了解更多,指南可以免费下载,请访问 www.a10networks.com/resources/ebooks/distributed-denial-service-ddos/。
摘要
在本章中,我们探讨了使用 Python 进行网络安全。我们使用 Cisco CML 工具设置实验室,包括主机和网络设备,包括 NX-OSv 和 IOSv 类型。我们参观了 Scapy,它允许我们从底层构建数据包。
Scapy 可以用于交互模式进行快速测试。一旦在交互模式下完成测试,我们可以将这些步骤放入文件中进行更可扩展的测试。它可以用于执行针对已知漏洞的各种网络渗透测试。
我们还探讨了如何使用 IP 访问列表和 MAC 访问列表来保护我们的网络。它们通常是我们的网络保护中的第一道防线。使用 Ansible,我们可以快速且一致地将访问列表部署到多个设备。
Syslog 和其他日志文件包含有用的信息,我们应该定期检查以检测任何早期入侵迹象。使用 Python 正则表达式,我们可以系统地搜索指向需要我们注意的安全事件的已知日志条目。除了我们讨论的工具之外,私有 VLAN 和 UFW 是其他一些我们可以用于更多安全保护的实用工具。
在 第七章,使用 Python 进行网络监控(第一部分),我们将探讨如何使用 Python 进行网络监控。监控使我们能够了解网络中正在发生的事情,以及网络的状态。
加入我们的书籍社区
要加入这本书的社区——在那里你可以分享反馈、向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第七章:使用 Python 进行网络监控——第一部分
想象一下,你在凌晨 2 点接到公司网络操作中心的电话。电话那头的人说:“嗨,我们遇到了一个影响生产服务的问题,我们怀疑可能是网络相关的问题。你能帮我们检查一下吗?”对于这种紧急的开放式问题,你首先会做什么?大多数情况下,你可能会想到:网络在正常工作和出现问题之间发生了什么变化?我们会检查我们的监控工具,看看在过去几小时内是否有任何关键指标发生变化。更好的是,我们可能已经收到了任何偏离正常基线数值的监控警报。
在整本书中,我们一直在讨论系统地对我们网络进行可预测的更改的各种方法,目的是让网络尽可能平稳运行。然而,网络并非静态——远非如此。它们可能是整个基础设施中最流动的部分。根据定义,网络连接基础设施的不同部分,不断进行双向流量传输。
有许多因素可能导致我们的网络无法按预期工作:硬件故障、有缺陷的软件、尽管有最好的意图但人为错误,等等。这不是一个会不会出错的问题,而是出错的时机和出错的内容。我们需要监控网络,确保它按预期工作,并在它不工作时通知我们。
在接下来的两章中,我们将探讨执行网络监控任务的各种方法。到目前为止,我们查看的许多工具都可以通过 Python 连接起来或直接管理。就像我们查看过的许多工具一样,网络监控也有两个部分。
首先,我们需要知道设备可以传输哪些监控相关信息。其次,我们需要确定我们可以从数据中解释哪些有用、可操作的信息。
在本章中,我们将首先探讨一些能够有效监控网络的工具:
-
实验室设置
-
简单网络管理协议(SNMP)和相关 Python 库用于处理 SNMP
-
Python 可视化库:
-
Matplotlib 和示例
-
Pygal 和示例
-
-
Python 与 MRTG 和 Cacti 集成进行网络可视化
这个列表并不全面,网络监控领域的商业供应商当然也不少。然而,我们将探讨的网络监控的基本原理,无论是开源工具还是商业工具都适用。
实验室设置
本章的实验室由 IOSv 设备组成,以简化设备配置。我们将使用这个实验室以及下一章的实验室。拓扑结构如下:

图 7.1:实验室拓扑结构
设备配置如下:
| 设备 | 管理 IP | 环回 IP |
|---|---|---|
| r1 | 192.168.2.218 |
192.168.0.1 |
| r2 | 192.168.2.219 |
192.168.0.2 |
| r3 | 192.168.2.220 |
192.168.0.3 |
| r5 | 192.168.2.221 |
192.168.0.4 |
| r6 | 192.168.2.222 |
192.168.0.5 |
Ubuntu 主机的信息如下:
| 设备名称 | 外部链接 Eth0 | 内部 IP Eth1 |
|---|---|---|
| 客户端 | 192.168.2.211 |
10.0.0.9 |
| 服务器 | 192.168.2.212 |
10.0.0.5 |
Linux 主机是 tinycore-linux (tinycorelinux.net/),从之前的 VIRL 版本迁移而来。默认用户名和密码都是 cisco。如果我们需要更改接口 IP 和默认网关,可以通过以下命令完成:
cisco@Client:~$ sudo ifconfig eth0 192.168.2.211 netmask 255.255.255.0
cisco@Client:~$ sudo route add default gw 192.168.2.1
cisco@Server:~$ sudo ifconfig eth0 192.168.2.212 netmask 255.255.255.0
cisco@Server:~$ sudo route add default gw 192.168.2.1
将使用两个 Ubuntu 主机在网络中生成流量,以便我们可以查看一些非零计数器。实验室文件包含在本书的 GitHub 仓库中。
SNMP
SNMP 是一种用于收集和管理设备的标准化协议。尽管标准允许您使用 SNMP 进行设备管理,但根据我的经验,大多数网络管理员更喜欢将 SNMP 仅作为信息收集机制。由于 SNMP 在无连接的 UDP 上运行,并且考虑到版本 1 和 2 中相对较弱的安全机制,通过 SNMP 进行设备更改往往会让网络运营商感到不安。SNMP 版本 3 在协议中增加了加密安全性和新的概念和术语,但 SNMP 版本 3 的适应方式在不同网络设备供应商之间有所不同。
SNMP 在网络监控中被广泛使用,自 1988 年作为 RFC 1065 的一部分以来一直存在。操作简单,网络管理员向设备发送 GET 和 SET 请求,具有 SNMP 代理的设备根据请求返回信息。最广泛采用的标准是 SNMPv2c,定义在 RFC 1901 – RFC 1908 中。它使用基于社区的简单安全方案进行安全保护。它还引入了新的功能,例如获取大量信息的能力。以下图表显示了 SNMP 的高级操作:

图 7.2:SNMP 操作
设备中驻留的信息以 管理信息库(MIB)的结构化形式存在。MIB 使用包含 对象标识符(OID)的分层命名空间,它代表可以读取并反馈给请求者的信息。当我们谈论使用 SNMP 查询设备信息时,我们是在谈论使用管理站查询代表我们所需信息的特定 OID。一个常见的 OID 结构,如系统和接口 OID,在供应商之间是共享的。除了常见的 OID,每个供应商还可以提供针对他们自己的企业级 OID。
作为操作员,我们必须在我们的环境中投入一些努力,将信息整合到 OID 结构中,以便检索有用的信息。这有时可能是一个繁琐的过程,一次只找到一个 OID。例如,你可能会请求一个设备 OID 并收到一个值为 10,000。这个值是什么?是接口流量吗?是字节还是比特?或者它可能代表数据包的数量?我们怎么知道?我们需要查阅标准或供应商文档来找出答案。有一些工具可以帮助这个过程,例如可以提供更多元数据的 MIB 浏览器。但至少在我的经验中,为你的网络构建基于 SNMP 的监控工具有时感觉像是一场猫捉老鼠的游戏,试图找到那个缺失的值。
从操作中可以吸取的一些主要观点如下:
-
实现高度依赖于设备代理可以提供的信息量。这反过来又取决于供应商如何对待 SNMP:作为一个核心功能还是一个附加功能。
-
SNMP 代理通常需要从控制平面获取 CPU 周期来返回一个值。这不仅对具有大型 BGP 表等设备的设备来说效率低下,而且使用 SNMP 在小间隔内查询数据也是不可行的。
-
用户需要知道 OID 来查询数据。
由于 SNMP 已经存在了一段时间,我假设你已经对它有一些经验了。让我们直接进入包安装和我们的第一个 SNMP 示例。
设置
首先,让我们确保我们的 SNMP 管理设备和代理在我们的设置中正常工作。SNMP 包可以安装在我们实验室中的主机(客户端或服务器)或管理网络中的管理设备上。只要 SNMP 管理器对设备有 IP 可达性,并且被管理设备允许入站连接,SNMP 就应该工作。在生产环境中,你应该只在管理主机上安装软件,并且只在控制平面允许 SNMP 流量。
在这个实验室中,我们在管理网络上的 Ubuntu 主机和实验室中的客户端主机上都安装了 SNMP:
$ sudo apt update && sudo apt upgrade
$ sudo apt-get install snmp
下一步是在网络设备上启用和配置 SNMP 选项。你可以在网络设备上配置许多可选参数,例如联系人、位置、机架 ID 和 SNMP 数据包大小。SNMP 配置选项是设备特定的,你应该检查特定设备的文档。对于 IOSv 设备,我们将配置一个访问列表来限制只允许查询设备的所需主机,并将访问列表与 SNMP 社区字符串绑定。在我们的示例中,我们将使用单词secret作为只读社区字符串,并将permit_snmp作为访问列表名称:
!
ip access-list standard permit_snmp
permit <management station> log
deny any log
!
snmp-server community secret RO permit_snmp
!
SNMP 社区字符串充当管理器和代理之间的共享密码;因此,每次你想查询设备时都需要包含它。
如本章前面所述,在处理 SNMP 时,找到正确的 OID 往往是战斗的一半。我们可以使用像 Cisco SNMP Object Navigator (snmp.cloudapps.cisco.com/Support/SNMP/do/BrowseOID.do?local=en) 这样的工具来查找特定的 OID 以进行查询。
或者,我们也可以从 Cisco 企业树的顶部 .1.3.6.1.4.1.9 开始遍历 SNMP 树。我们将执行遍历以确保 SNMP 代理和访问列表正常工作:
$ snmpwalk -v2c -c secret 192.168.2.218 .1.3.6.1.4.1.9
iso.3.6.1.4.1.9.2.1.1.0 = STRING: "
Bootstrap program is IOSv
"
iso.3.6.1.4.1.9.2.1.2.0 = STRING: "reload"
iso.3.6.1.4.1.9.2.1.3.0 = STRING: "iosv-1"
iso.3.6.1.4.1.9.2.1.4.0 = STRING: "virl.info"
<skip>
我们可以对需要查询的 OID 进行更具体的说明:
$ snmpwalk -v2c -c secret 192.168.2.218 .1.3.6.1.4.1.9.2.1.61.0
iso.3.6.1.4.1.9.2.1.61.0 = STRING: "cisco Systems, Inc.
170 West Tasman Dr.
San Jose, CA 95134-1706
U.S.A.
Ph +1-408-526-4000
Customer service 1-800-553-6387 or +1-408-526-7208
24HR Emergency 1-800-553-2447 or +1-408-526-7209
Email Address tac@cisco.com
World Wide Web http://www.cisco.com"
作为演示,如果我们输入错误值,错误值比最后 OID 的 0 到 1 多 1 位,我们会看到什么:
$ snmpwalk -v2c -c secret 192.168.2.218 .1.3.6.1.4.1.9.2.1.61.1
iso.3.6.1.4.1.9.2.1.61.1 = No Such Instance currently exists at this OID
与 API 调用不同,没有有用的错误代码或消息;它只是简单地声明该 OID 不存在。有时这可能会非常令人沮丧。
最后要检查的是,我们配置的访问列表是否会拒绝不想要的 SNMP 查询。因为我们已经在访问列表的 permit 和 deny 条目中使用了日志关键字,所以只有 172.16.1.123 被允许查询设备:
*Sep 17 23:32:10.155: %SEC-6-IPACCESSLOGNP: list permit_snmp permitted 0 192.168.2.126 -> 0.0.0.0, 1 packet
如您所见,设置 SNMP 最大的挑战在于找到正确的 OID。一些 OID 定义在标准化的 MIB-2 中;其他则位于树的企业部分。尽管如此,供应商的文档仍然是最佳选择。一些工具可以帮助,例如 MIB 浏览器;您可以将 MIB(同样由供应商提供)添加到浏览器中,并查看基于企业的 OID 描述。当您需要找到所需对象的正确 OID 时,像 Cisco 的 SNMP Object Navigator (snmp.cloudapps.cisco.com/Support/SNMP/do/BrowseOID.do?local=en) 这样的工具证明非常有价值。
PySNMP
PySNMP 是由 Ilya Etingof (github.com/etingof) 开发的跨平台、纯 Python SNMP 引擎实现。它为您抽象了许多 SNMP 细节,就像优秀的库一样,并支持 Python 2 和 Python 3。
PySNMP 需要 PyASN1 包。以下内容摘自维基百科:
“ASN.1 是一种标准和表示法,它描述了在电信和计算机网络中表示、编码、传输和解析数据时的规则和结构。”
PyASN1 方便地为 ASN.1 提供了 Python 包装器。让我们首先安装这个包。注意,由于我们使用的是虚拟环境,我们将使用虚拟环境的 Python 解释器:
(venv) $ cd /tmp
(venv) $ git clone https ://github.com/etingof/pyasn1.git
(venv) $ cd pyasn1
(venv) $ git checkout 0.2.3
(venv) $ python3 setup.py install # notice the venv path
接下来,安装 PySNMP 包:
(venv) $ cd /tmp
(venv) $ git clone https://github.com/etingof/pysnmp
(venv) $ cd pysnmp/
(venv) $ git checkout v4.3.10
(venv) $ python3 setup.py install # notice the venv path
我们使用 PySNMP 的较旧版本,因为从 5.0.0 版本开始移除了pysnmp.entity.rfc3413.oneliner(github.com/etingof/pysnmp/blob/a93241007b970c458a0233c16ae2ef82dc107290/CHANGES.txt)。如果你使用pip安装包,示例可能会出错。
让我们看看如何使用 PySNMP 查询之前示例中使用的相同思科联系信息。我们首先导入必要的模块并创建一个CommandGenerator对象:
>>> from pysnmp.entity.rfc3413.oneliner import cmdgen
>>> cmdGen = cmdgen.CommandGenerator()
>>> cisco_contact_info_oid = "1.3.6.1.4.1.9.2.1.61.0"
我们可以使用getCmd方法执行 SNMP。结果被解包到各种变量中;在这些变量中,我们最关心的是varBinds,它包含查询结果:
>>> errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
cmdgen.CommunityData('secret'),
cmdgen.UdpTransportTarget(('192.168.2.218', 161)),
cisco_contact_info_oid)
>>> for name, val in varBinds:
print('%s=%s' % (name.prettyPrint(), str(val)))
SNMPv2-SMI::enterprises.9.2.1.61.0=cisco Systems, Inc.
170 West Tasman Dr.
San Jose, CA 95134-1706
U.S.A.
Ph +1-408-526-4000
Customer service 1-800-553-6387 or +1-408-526-7208
24HR Emergency 1-800-553-2447 or +1-408-526-7209
Email Address tac@cisco.com
World Wide Web http://www.cisco.com
>>>
注意,响应值是 PyASN1 对象。prettyPrint()方法将转换一些这些值到可读的格式,但我们的返回变量中的结果没有被转换。我们手动将其转换为字符串。
我们可以基于前面的交互示例编写一个脚本。我们将命名为pysnmp_1.py并包含错误检查。我们还可以在getCmd()方法中包含多个 OID:
#!/usr/bin/env/python3
from pysnmp.entity.rfc3413.oneliner import cmdgen
cmdGen = cmdgen.CommandGenerator()
system_up_time_oid = "1.3.6.1.2.1.1.3.0"
cisco_contact_info_oid = "1.3.6.1.4.1.9.2.1.61.0"
errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
cmdgen.CommunityData('secret'),
cmdgen.UdpTransportTarget(('192.168.2.218', 161)),
system_up_time_oid,
cisco_contact_info_oid
)
# Check for errors and print out results
if errorIndication:
print(errorIndication)
else:
if errorStatus:
print('%s at %s' % (
errorStatus.prettyPrint(),
errorIndex and varBinds[int(errorIndex)-1] or '?'
)
)
else:
for name, val in varBinds:
print('%s = %s' % (name.prettyPrint(), str(val)))
结果将被解包并列出两个 OID 的值:
$ python pysnmp_1.py
SNMPv2-MIB::sysUpTime.0 = 599083
SNMPv2-SMI::enterprises.9.2.1.61.0 = cisco Systems, Inc.
170 West Tasman Dr.
San Jose, CA 95134-1706
U.S.A.
Ph +1-408-526-4000
Customer service 1-800-553-6387 or +1-408-526-7208
24HR Emergency 1-800-553-2447 or +1-408-526-7209
Email Address tac@cisco.com
World Wide Web http://www.cisco.com
在以下示例中,我们将持久化我们从查询中接收到的值,以便使用数据进行其他操作,例如可视化。在我们的示例中,我们将使用 MIB-2 树中的ifEntry来获取与接口相关的值进行绘图。
你可以找到许多映射ifEntry树的资源;以下是我们在之前访问的思科 SNMP 对象导航器网站的截图:

图 7.3:SNMP ifEntry OID 树
快速测试将展示设备上接口的 OID 映射:
$ snmpwalk -v2c -c secret 172.16.1.189 .1.3.6.1.2.1.2.2.1.2
iso.3.6.1.2.1.2.2.1.2.1 = STRING: "GigabitEthernet0/0"
iso.3.6.1.2.1.2.2.1.2.2 = STRING: "GigabitEthernet0/1"
iso.3.6.1.2.1.2.2.1.2.3 = STRING: "GigabitEthernet0/2"
iso.3.6.1.2.1.2.2.1.2.4 = STRING: "Null0"
iso.3.6.1.2.1.2.2.1.2.5 = STRING: "Loopback0"
从文档中,我们可以将ifInOctets(10)、ifInUcastPkts(11)、ifOutOctets(16)和ifOutUcastPkts(17)的值映射到它们各自的 OID 值。从 CLI 和 MIB 文档的快速检查中,我们可以看到GigabitEthernet0/0数据包输出的值映射到 OID 1.3.6.1.2.1.2.2.1.17.1。我们将遵循相同的流程来映射接口统计信息的其余 OID。在 CLI 和 SNMP 之间检查时,请注意,值应该接近但不相同,因为 CLI 输出时间和 SNMP 查询时间之间可能会有一些流量:
r1#sh int gig 0/0 | i packets
5 minute input rate 0 bits/sec, 0 packets/sec
5 minute output rate 0 bits/sec, 0 packets/sec
6872 packets input, 638813 bytes, 0 no buffer
4279 packets output, 393631 bytes, 0 underruns
$ snmpwalk -v2c -c secret 192.168.2.218 .1.3.6.1.2.1.2.2.1.17.1
iso.3.6.1.2.1.2.2.1.17.1 = Counter32: 4292
如果我们处于生产环境,我们可能会将结果写入数据库。但既然这是一个例子,我们将把查询值写入一个平面文件。我们将编写pysnmp_3.py脚本来进行信息查询并将结果写入文件。在脚本中,我们定义了需要查询的各种 OID:
# Hostname OID
system_name = '1.3.6.1.2.1.1.5.0'
# Interface OID
gig0_0_in_oct = '1.3.6.1.2.1.2.2.1.10.1'
gig0_0_in_uPackets = '1.3.6.1.2.1.2.2.1.11.1'
gig0_0_out_oct = '1.3.6.1.2.1.2.2.1.16.1'
gig0_0_out_uPackets = '1.3.6.1.2.1.2.2.1.17.1'
值在snmp_query()函数中被消耗,以host、community和oid作为输入:
def snmp_query(host, community, oid):
errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
cmdgen.CommunityData(community),
cmdgen.UdpTransportTarget((host, 161)),
oid
)
所有值都放入一个带有各种键的字典中,并写入一个名为results.txt的文件:
result = {}
result['Time'] = datetime.datetime.utcnow().isoformat()
result['hostname'] = snmp_query(host, community, system_name)
result['Gig0-0_In_Octet'] = snmp_query(host, community, gig0_0_in_oct)
result['Gig0-0_In_uPackets'] = snmp_query(host, community, gig0_0_in_uPackets)
result['Gig0-0_Out_Octet'] = snmp_query(host, community, gig0_0_out_oct)
result['Gig0-0_Out_uPackets'] = snmp_query(host, community, gig0_0_out_uPackets)
with open('/home/echou/Master_Python_Networking/Chapter7/results.txt', 'a') as f:
f.write(str(result))
f.write('\n')
结果将是一个文件,显示了查询时表示的接口数据包:
$ cat results.txt
{'Gig0-0_In_Octet': '3990616', 'Gig0-0_Out_uPackets': '60077', 'Gig0-0_In_uPackets': '42229', 'Gig0-0_Out_Octet': '5228254', 'Time': '2017-03-06T02:34:02.146245', 'hostname': 'iosv-1.virl.info'}
{'Gig0-0_Out_uPackets': '60095', 'hostname': 'iosv-1.virl.info', 'Gig0-0_Out_Octet': '5229721', 'Time': '2017-03-06T02:35:02.072340', 'Gig0-0_In_Octet': '3991754', 'Gig0-0_In_uPackets': '42242'}
<skip>
我们可以使这个脚本可执行,并安排一个每 5 分钟执行一次的cron作业:
$ chmod +x pysnmp_3.py
# crontab configuration
*/5 * * * * /home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter07/pysnmp_3.py
如前所述,在生产环境中,我们会将信息放入数据库。对于 SQL 数据库,你可以使用唯一 ID 作为主键。在 NoSQL 数据库中,我们可能会使用时间作为主索引(或键),因为它是始终唯一的,后面跟着各种键值对。
我们将等待脚本执行几次,以便填充值。如果你是那种没有耐心的人,你可以将cron作业间隔缩短到 1 分钟。在你看到results.txt文件中有足够的数据来制作有趣的图表后,我们可以继续到下一部分,看看我们如何使用 Python 来可视化数据。
Python 数据可视化
我们收集网络数据以深入了解我们的网络。了解数据含义的最好方法之一是使用图表进行可视化。这几乎适用于所有数据,但在网络监控的上下文中,对于时间序列数据来说尤其如此。上周网络传输了多少数据?在所有流量中 TCP 协议的百分比是多少?这些都是我们可以通过使用如 SNMP 之类的数据收集机制获得的值,我们可以使用一些流行的 Python 库生成可视化图表。
在本节中,我们将使用上一节通过 SNMP 收集的数据,并使用两个流行的 Python 库,Matplotlib 和 Pygal,来绘制它们。
Matplotlib
Matplotlib (matplotlib.org/)是 Python 语言的 2D 绘图库及其 NumPy 数学扩展。它可以用几行代码生成出版物质量的图表,如图表、直方图和条形图。
NumPy 是 Python 编程语言的扩展。它是开源的,在各种数据科学项目中广泛使用。你可以在en.wikipedia.org/wiki/NumPy了解更多关于它的信息。
让我们从安装开始。
安装
安装可以通过 Linux 发行版的包管理系统或 Python pip来完成。在 Matplotlib 的最新版本中,我们还将安装python3-tk以进行显示:
(venv) $ pip install matplotlib
(venv) $ sudo apt install python3-tk
现在,让我们进入我们的第一个例子。
Matplotlib – 第一个例子
对于以下示例,默认情况下输出图形以标准输出形式显示。通常,标准输出是你的显示器屏幕。在开发过程中,通常更容易先尝试代码并首先在标准输出上生成图形,然后再用脚本最终确定代码。如果你通过虚拟机跟随这本书,建议你使用 VM 窗口而不是 SSH,这样你就可以看到图形。如果你无法访问标准输出,你可以保存图形并在下载后查看(如你将看到的)。请注意,你将需要在某些我们在这个部分生成的图形中设置$DISPLAY变量。
本章可视化示例中使用的 Ubuntu 桌面屏幕截图。一旦在终端窗口中发出plt.show()命令,图 1将出现在屏幕上。当你关闭图形时,你将返回到 Python shell:

图 7.4:使用 Ubuntu 桌面的 Matplotlib 可视化
让我们先看看折线图。折线图简单地给出两组数字,分别对应于x轴和y轴的值:
>>> import matplotlib.pyplot as plt
>>> plt.plot([0,1,2,3,4], [0,10,20,30,40])
[<matplotlib.lines.Line2D object at 0x7f932510df98>]
>>> plt.ylabel('Something on Y')
<matplotlib.text.Text object at 0x7f93251546a0>
>>> plt.xlabel('Something on X')
<matplotlib.text.Text object at 0x7f9325fdb9e8>
>>> plt.show()
图形将是一个折线图:

图 7.5:Matplotlib 折线图
或者,如果你无法访问标准输出或已先保存图形,你可以使用savefig()方法:
>>> plt.savefig('figure1.png') or
>>> plt.savefig('figure1.pdf')
在掌握了基本的绘图知识后,我们现在可以绘制从 SNMP 查询中获得的结果。
Matplotlib 用于 SNMP 结果
在我们的第一个 Matplotlib 示例matplotlib_1.py中,我们将导入pyplot模块的同时导入dates模块。我们将使用matplotlib.dates模块而不是 Python 标准库中的dates模块。
与 Python 的dates模块不同,matplotlib.dates库将内部将日期值转换为浮点类型,这是 Matplotlib 所要求的:
import matplotlib.pyplot as plt
import matplotlib.dates as dates
Matplotlib 提供了复杂的日期绘图功能;你可以在matplotlib.org/stable/api/dates_api.html找到更多相关信息。
在脚本中,我们将创建两个空列表,每个列表代表x轴和y轴的值。请注意,在第 12 行,我们使用了内置的eval()Python 函数将输入读取为字典而不是默认字符串:
x_time = []
y_value = []
with open('results.txt', 'r') as f:
for line in f.readlines():
# eval(line) reads in each line as dictionary instead of string
line = eval(line)
# convert to internal float
x_time.append(dates.datestr2num(line['Time']))
y_value.append(line['Gig0-0_Out_uPackets'])
为了以人类可读的日期格式读取x轴值,我们需要使用plot_date()函数而不是plot()函数。我们还将稍微调整图形的大小,以及旋转x轴上的值,以便我们可以完整地读取值:
plt.subplots_adjust(bottom=0.3)
plt.xticks(rotation=80)
plt.plot_date(x_time, y_value)
plt.title('Router1 G0/0')
plt.xlabel('Time in UTC')
plt.ylabel('Output Unicast Packets')
plt.savefig('matplotlib_1_result.png')
plt.show()
最终结果将显示Router1 G0/0和输出单播数据包,如下所示:

图 7.6:Router1 Matplotlib 图表
注意,如果你更喜欢直线而不是点,你可以在plot_date()函数的第三个可选参数中使用:
plt.plot_date(x_time, y_value, "-")
我们可以重复这些步骤来输出输出八位字节、输入单播数据包和输入作为单独的图表的其余值。然而,在我们的下一个例子,即matplotlib_2.py中,我们将向您展示如何将多个值与相同的时间范围进行绘图,以及额外的 Matplotlib 选项。
在这个例子中,我们将创建额外的列表并相应地填充值:
x_time = []
out_octets = []
out_packets = []
in_octets = []
in_packets = []
with open('results.txt', 'r') as f:
for line in f.readlines():
# eval(line) reads in each line as dictionary instead of string
line = eval(line)
# convert to internal float
x_time.append(dates.datestr2num(line['Time']))
out_packets.append(line['Gig0-0_Out_uPackets'])
out_octets.append(line['Gig0-0_Out_Octet'])
in_packets.append(line['Gig0-0_In_uPackets'])
in_octets.append(line['Gig0-0_In_Octet'])
由于我们的x轴值相同,我们只需将不同的y轴值添加到同一个图表中:
# Use plot_date to display x-axis back in date format
plt.plot_date(x_time, out_packets, '-', label='Out Packets')
plt.plot_date(x_time, out_octets, '-', label='Out Octets')
plt.plot_date(x_time, in_packets, '-', label='In Packets')
plt.plot_date(x_time, in_octets, '-', label='In Octets')
还可以将grid和legend添加到图表中:
plt.title('Router1 G0/0')
plt.legend(loc='upper left')
plt.grid(True)
plt.xlabel('Time in UTC')
plt.ylabel('Values')
plt.savefig('matplotlib_2_result.png')
plt.show()
最终结果将结合单个图表中的所有值。请注意,左上角的一些值被图例遮挡。你可以调整图表大小和/或使用平移/缩放选项来移动图表以查看这些值:

图 7.7:Router1 – Matplotlib 多行图
Matplotlib 提供了许多更多的绘图选项;我们当然不仅限于绘制图表。例如,在matplotlib_3.py中,我们可以使用以下模拟数据来绘制我们可以在线缆上看到的不同流量类型的百分比:
#!/usr/bin/env python3
# Example from http://matplotlib.org/2.0.0/examples/pie_and_polar_charts/pie_demo_features.html
import matplotlib.pyplot as plt
# Pie chart, where the slices will be ordered and plotted counter-clockwise:
labels = 'TCP', 'UDP', 'ICMP', 'Others'
sizes = [15, 30, 45, 10]
explode = (0, 0.1, 0, 0) # Make UDP stand out
fig1, ax1 = plt.subplots()
ax1.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%',
shadow=True, startangle=90)
ax1.axis('equal') # Equal aspect ratio ensures that pie is drawn as a circle.
plt.savefig('matplotlib_3_result.png')
plt.show()
上述代码导致plt.show()生成了以下饼图:

图 7.8:Matplotlib 饼图
在本节中,我们使用了 Matplotlib 将我们的网络数据绘制成更具有视觉吸引力的图表,以帮助我们理解网络的状态。这是通过条形图、折线图和饼图来完成的,这些图表非常适合手头的数据。Matplotlib 是一个强大的工具,不仅限于 Python。作为一个开源工具,我们可以利用许多额外的 Matplotlib 资源来了解这个工具。
额外的 Matplotlib 资源
Matplotlib 是最佳的 Python 绘图库之一,能够生成出版物质量的图表。像 Python 一样,它旨在使复杂任务变得简单。在 GitHub 上有超过 10,000 颗星(并且还在增加),它也是最受欢迎的开源项目之一。
它的流行度直接转化为更快的错误修复、友好的用户社区、广泛的文档和通用易用性。使用这个包需要一点学习曲线,但这是值得努力的。
在本节中,我们对 Matplotlib 的探索还只是触及了皮毛。你可以在matplotlib.org/stable/index.html(Matplotlib 项目页面)和github.com/matplotlib/matplotlib(Matplotlib GitHub 仓库)找到更多资源。
在接下来的章节中,我们将探讨另一个流行的 Python 绘图库:Pygal。
Pygal
Pygal (www.pygal.org/en/stable/) 是一个用 Python 编写的动态 可缩放矢量图形 (SVG) 图表库。在我看来,Pygal 的最大优点是它能够轻松地以原生方式生成 SVG 图表。SVG 相比其他图表格式有许多优点。其中两个主要优点是它对网页浏览器友好,并且在不牺牲图像质量的情况下提供可伸缩性。换句话说,您可以在任何现代网页浏览器中显示生成的图像,并放大和缩小图像而不会丢失图表的细节。我提到过我们可以在几行 Python 代码中做到这一点吗?这有多酷?
让我们安装 Pygal,然后继续第一个示例。
安装
安装是通过 pip 完成的:
(venv)$ pip install pygal
Pygal – 第一个示例
让我们看看 Pygal 文档中展示的线形图示例,该文档可在 pygal.org/en/stable/documentation/types/line.html 找到:
>>> import pygal
>>> line_chart = pygal.Line()
>>> line_chart.title = 'Browser usage evolution (in %)'
>>> line_chart.x_labels = map(str, range(2002, 2013))
>>> line_chart.add('Firefox', [None, None, 0, 16.6, 25, 31, 36.4, 45.5, 46.3, 42.8, 37.1])
<pygal.graph.line.Line object at 0x7f4883c52b38>
>>> line_chart.add('Chrome', [None, None, None, None, None, None, 0, 3.9, 10.8, 23.8, 35.3])
<pygal.graph.line.Line object at 0x7f4883c52b38>
>>> line_chart.add('IE', [85.8, 84.6, 84.7, 74.5, 66, 58.6, 54.7, 44.8, 36.2, 26.6, 20.1])
<pygal.graph.line.Line object at 0x7f4883c52b38>
>>> line_chart.add('Others', [14.2, 15.4, 15.3, 8.9, 9, 10.4, 8.9, 5.8, 6.7, 6.8, 7.5])
<pygal.graph.line.Line object at 0x7f4883c52b38>
>>> line_chart.render_to_file('pygal_example_1.svg')
在这个例子中,我们创建了一个线对象,x_labels 自动渲染为 11 个单位的字符串。每个对象都可以以列表格式添加标签和值,例如 Firefox、Chrome 和 IE。
我们需要关注的有意思的一点是,每个线形图项都有与 x 单位数量完全匹配的匹配数字。例如,如果没有值,例如 Chrome 的 2002-2007 年,则输入 None。
这是 Firefox 浏览器中查看的结果图,如下所示:

图 7.9:Pygal 示例图表
现在我们已经看到了 Pygal 的一般用法,我们可以使用相同的方法来绘制我们手头的 SNMP 结果。我们将在下一节中这样做。
Pygal SNMP 结果
对于 Pygal 线形图,我们可以很大程度上遵循我们 Matplotlib 示例中的相同模式,即通过读取文件创建值列表。我们不再需要将 x-轴值转换为内部浮点数,就像我们为 Matplotlib 做的那样;然而,我们需要将每个值中的数字转换为浮点数:
#!/usr/bin/env python3
import pygal
x_time = []
out_octets = []
out_packets = []
in_octets = []
in_packets = []
with open('results.txt', 'r') as f:
for line in f.readlines():
# eval(line) reads in each line as dictionary instead of string
line = eval(line)
x_time.append(line['Time'])
out_packets.append(float(line['Gig0-0_Out_uPackets']))
out_octets.append(float(line['Gig0-0_Out_Octet']))
in_packets.append(float(line['Gig0-0_In_uPackets']))
in_octets.append(float(line['Gig0-0_In_Octet']))
我们可以使用我们之前看到用来构建线图的相同机制:
line_chart = pygal.Line()
line_chart.title = "Router 1 Gig0/0"
line_chart.x_labels = x_time
line_chart.add('out_octets', out_octets)
line_chart.add('out_packets', out_packets)
line_chart.add('in_octets', in_octets)
line_chart.add('in_packets', in_packets)
line_chart.render_to_file('pygal_example_2.svg')
结果与我们已经看到的结果相似,但现在图表是以 SVG 格式呈现的,可以在网页上轻松显示。它可以在现代网页浏览器中查看:

图 7.10:路由器 1— Pygal 多行图
就像 Matplotlib 一样,Pygal 为图表提供了更多选项。例如,为了在 Matplotlib 中绘制我们之前看到的饼图,我们可以使用 pygal.Pie() 对象。这可以在 pygal_2.py 中看到:
#!/usr/bin/env python3
import pygal
line_chart = pygal.Pie()
line_chart.title = "Protocol Breakdown"
line_chart.add('TCP', 15)
line_chart.add('UDP', 30)
line_chart.add('ICMP', 45)
line_chart.add('Others', 10)
line_chart.render_to_file('pygal_example_3.svg')
生成的 SVG 文件如下所示:

图 7.11:Pygal 饼图
当需要生成生产就绪的 SVG 图形时,Pygal 是一个伟大的工具。如果需要这种类型的图形,无需寻找其他库,只需查看 Pygal 库。在本节中,我们检查了使用 Pygal 生成网络数据图形的示例。类似于 Matplotlib,如果你对 Pygal 感兴趣,有许多额外的资源可以帮助你学习。
额外的 Pygal 资源
Pygal 为从基本的网络监控工具(如 SNMP)收集的数据提供了许多更多可定制的功能和绘图能力。在本节中,我们演示了简单的折线图和饼图。你可以在以下链接中找到更多关于这个项目的信息:
-
Pygal 文档:
www.pygal.org/en/stable/index.html -
Pygal GitHub 项目页面:
github.com/Kozea/pygal
在接下来的章节中,我们将继续探讨网络监控的 SNMP 主题,但将使用一个功能齐全的网络监控系统,称为Cacti。
Cacti 的 Python
在我作为地区 ISP 的初级网络工程师的早期日子里,我们使用开源的跨平台多路由流量图器(MRTG)(en.wikipedia.org/wiki/Multi_Router_Traffic_Grapher)工具来检查网络链路上的流量负载。我们几乎完全依赖这个工具进行流量监控。我对一个开源项目能如此出色和有用感到惊讶。它是第一个为网络工程师抽象 SNMP、数据库和 HTML 细节的开源高级网络监控系统之一。随后出现了循环冗余检测数据库工具(RRDtool)(en.wikipedia.org/wiki/RRDtool)。在 1999 年的首次发布中,它被称为“MRTG Done Right”。它极大地提高了后端数据库和轮询器的性能。
2001 年发布的 Cacti (en.wikipedia.org/wiki/Cacti_(software)) 是一个开源的基于 Web 的网络监控和绘图工具,设计为 RRDtool 的改进前端。由于 MRTG 和 RRDtool 的传承,你会注意到熟悉的图形布局、模板和 SNMP 轮询器。作为一个打包的工具,安装和使用需要保持在工具的边界内。然而,Cacti 提供了一个自定义数据查询功能,我们可以使用 Python 来处理。在本节中,我们将看到如何使用 Python 作为 Cacti 的输入方法。
首先,我们将介绍安装过程。
安装
由于 Cacti 是一个包含前端、收集脚本和数据库后端的综合性工具,除非你已经有了 Cacti 的使用经验,否则我建议在我们的实验室中安装一个独立的虚拟机或容器。以下指令将针对虚拟机进行展示,但容器 Dockerfile 将类似。
在使用 Ubuntu 管理虚拟机上的 APT 时,在 Ubuntu 上安装是直接的:
$ sudo apt-get install cacti
它将触发一系列安装和设置步骤,包括 MySQL 数据库、Web 服务器(Apache 或lighttpd)以及各种配置任务。安装完成后,导航到http://<ip>/cacti开始。最后一步是使用默认用户名和密码(admin/admin)登录;您将被提示更改密码。
在安装过程中,如果有疑问,请选择默认选项并保持简单。
登录后,我们可以按照文档添加设备并将其与模板关联。Cacti 有很好的文档docs.cacti.net/,用于添加设备和创建您的第一个图表,所以我们将快速查看您可能会看到的截图:

图 7.12:Cacti 设备编辑页面
当您可以看到设备运行时间时,这是一个表示 SNMP 通信正在工作的标志:

图 7.13:设备编辑结果页面
您可以为设备添加图表以显示接口流量和其他统计数据:

图 7.14:设备的新的图表
经过一段时间,您将开始看到流量,如图所示:

图 7.15:5 分钟平均图
现在我们准备看看如何使用 Python 脚本来扩展 Cacti 的数据收集功能。
Python 脚本作为输入源
在我们尝试将 Python 脚本作为输入源使用之前,我们应该阅读两个文档:
-
数据输入方法:
www.cacti.net/downloads/docs/html/data_input_methods.html -
使脚本与 Cacti 协同工作:
www.cacti.net/downloads/docs/html/making_scripts_work_with_cacti.html
你可能会想知道使用 Python 脚本作为数据输入扩展的使用场景。其中一个使用场景是为没有相应 OID 的资源提供监控,例如,如果我们想知道如何绘制访问列表permit_snmp允许主机172.16.1.173进行 SNMP 查询的次数。
该示例假设 SNMP 站点的 IP 地址为172.16.1.173;请将 IP 地址替换为当前实验室管理站点的 IP 地址。
我们知道我们可以通过 CLI 看到匹配的数量:
iosv-1#sh ip access-lists permit_snmp | I 172.16.1.173 10 permit 172.16.1.173 log (6362 matches)
然而,很可能是没有 OID 与这个值相关联(或者我们假装没有)。这就是我们可以使用外部脚本来生成可以被 Cacti 主机消费的输出的地方。
我们可以重用第二章中讨论的 Pexpect 脚本,chapter1_1.py。我们将将其重命名为cacti_1.py。除了我们将执行 CLI 命令并保存输出之外,一切都应该与原始脚本相同:
<skip>
for device in devices.keys():
…
child.sendline''sh ip access-lists permit_snmp | i 172.16.1.17'')
child.expect(device_prompt)
output = child.before
其原始输出将如下所示:
''sh ip access-lists permit_snmp | i 172.16.1.173rn 10 permit 172.16.1.173 log (6428 matches)r''
我们将在脚本中使用split()函数对字符串进行处理,只留下匹配的数量,并在脚本中将它们打印到标准输出:
print(str(output).split'''')[1].split()[0])
为了测试这一点,我们可以通过多次执行脚本来查看增加的数量:
$ ./cacti_1.py
6428
$ ./cacti_1.py
6560
$ ./cacti_1.py
6758
我们可以使脚本可执行,并将其放入默认的 Cacti 脚本位置:
$ chmod a+x cacti_1.py
$ sudo cp cacti_1.py /usr/share/cacti/site/scripts/
Cacti 文档,可在www.cacti.net/downloads/docs/html/how_to.html找到,提供了如何将脚本结果添加到输出图的详细步骤。
这些步骤包括将脚本作为数据输入方法添加,将输入方法添加到数据源,然后创建一个可查看的图表:

图 7.16:数据输入方法结果页面
SNMP 是向设备提供网络监控服务的一种常见方式。使用 Cacti 作为前端,RRDtool 为所有网络设备提供了一个良好的 SNMP 平台。我们还可以使用 Python 脚本来扩展超出 SNMP 的信息收集。
摘要
在本章中,我们探讨了通过 SNMP 执行网络监控的方法。我们在网络设备上配置了与 SNMP 相关的命令,并使用我们的带有 SNMP 轮询器的网络管理 VM 来查询设备。我们使用了 PySNMP 模块来简化并自动化我们的 SNMP 查询。我们还学习了如何将查询结果保存到平面文件或数据库中,以供未来示例使用。
在本章的后面部分,我们使用了两个不同的 Python 可视化包,Matplotlib 和 Pygal,来绘制 SNMP 结果。每个包都有其独特的优势。Matplotlib 是一个成熟、功能丰富的库,在数据科学项目中广泛使用。Pygal 可以原生生成 SVG 格式的图表,这些图表灵活且适合网页。我们看到了如何生成与网络监控相关的折线图和饼图。
在本章的末尾,我们查看了一个名为 Cacti 的全能网络监控工具。它主要使用 SNMP 进行网络监控,但我们看到了如何使用 Python 脚本作为输入源来扩展平台监控能力,当远程主机上没有 SNMP OID 时。
在第八章,“使用 Python 进行网络监控(第二部分)”中,我们将继续讨论我们可以用来监控我们的网络并了解网络是否按预期运行的工具。我们将查看基于流量的监控,使用 NetFlow、sFlow 和 IPFIX。我们还将使用 Graphviz 等工具来可视化我们的网络拓扑并检测任何拓扑变化。
加入我们的书籍社区
要加入这本书的社区——在这里您可以分享反馈、向作者提问,并了解新版本信息——请扫描下面的二维码:
packt.link/networkautomationcommunity

第八章:使用 Python 进行网络监控 – 第二部分
在第七章,使用 Python 进行网络监控 – 第一部分中,我们使用 SNMP 从网络设备查询信息。我们这样做是通过使用 SNMP 管理器查询网络设备上驻留的 SNMP 代理来完成的。SNMP 信息以层次格式结构化,使用特定的对象 ID 作为表示对象值的方式。大多数时候,我们关心的值是一个数字,比如 CPU 负载、内存使用或接口流量。我们可以将此数据与时间对比,以了解值随时间的变化情况。
我们通常将 SNMP 方法分类为一种拉方法,因为我们不断地要求设备提供特定的答案。这种方法给设备增加了负担,因为它需要在控制平面花费 CPU 周期来从子系统找到答案,将答案打包成 SNMP 数据包,并将答案传输回轮询器。如果你曾经参加过那种有一个家庭成员总是反复问你同样问题的家庭聚会,那么这就像是 SNMP 管理器轮询受管节点。
随着时间的推移,如果我们有多个 SNMP 轮询器每隔 30 秒查询同一设备(你会惊讶这有多么频繁),管理开销将变得很大。在我们给出的同一个家庭聚会的例子中,不是只有一个家庭成员,想象一下每隔 30 秒就有很多人打断你问问题。我不知道你怎么样,但我知道即使是一个简单的问题(或者更糟糕的是,如果他们都问同样的问题),我也会非常烦恼。
我们可以提供更高效的网络监控的另一种方法是,将管理站与设备之间的关系从拉模式改为推模式。换句话说,信息可以按照约定的格式从设备推送到管理站。这个概念就是基于流量的监控所依据的。在基于流量的模型中,网络设备将流量信息,称为流,流式传输到管理站。格式可以是思科的专有 NetFlow(版本 5 或 9),行业标准 IPFIX,或者开源的 sFlow 格式。在本章中,我们将花一些时间探讨使用 Python 来研究 NetFlow、IPFIX 和 sFlow。
并非所有的监控都是以时间序列数据的形式出现。如果你愿意,你可以将诸如网络拓扑和 Syslog 等信息表示为时间序列格式,但这并不理想。我们可以使用 Python 来检查网络拓扑信息,并查看拓扑是否随时间发生变化。我们可以使用工具,如 Graphviz,以及 Python 包装器来展示拓扑。如已在第六章,使用 Python 进行网络安全中看到,Syslog 包含安全信息。在本书的后续部分,我们将探讨使用 Elastic Stack(Elasticsearch、Logstash、Kibana 和 Beat)作为收集和索引网络安全和日志信息的有效方式。
具体来说,在本章中,我们将涵盖以下主题:
-
Graphviz,这是一款开源的图形可视化软件,可以帮助我们快速高效地绘制我们的网络图
-
基于流的监控,例如 NetFlow、IPFIX 和 sFlow
-
使用 ntop 可视化信息流
让我们先看看如何使用 Graphviz 监控网络拓扑变化。
Graphviz
Graphviz 是一款开源的图形可视化软件。想象一下,如果我们需要向同事描述我们的网络拓扑,而没有图片的帮助。我们可能会说,我们的网络由三层组成:核心层、分发层和接入层。
核心层由两个路由器组成,用于冗余,这两个路由器都向四个分发路由器全连接;分发路由器也向接入路由器全连接。内部路由协议是 OSPF,外部我们使用 BGP 与我们的服务提供商对等。虽然这个描述缺少一些细节,但可能足以让您的同事为您描绘一个相当不错的高级网络图。
Graphviz 的工作原理与描述文本格式的图,Graphviz 可以在文本文件中理解的过程相似。然后我们可以将文件输入到 Graphviz 程序中,构建图。在这里,图是以称为 DOT 的文本格式描述的(en.wikipedia.org/wiki/DOT_(graph_description_language)),Graphviz 根据描述渲染图。当然,因为计算机缺乏人类的想象力,语言必须非常精确和详细。
对于 Graphviz 特定的 DOT 语法定义,请参阅 www.graphviz.org/doc/info/lang.html。
在本节中,我们将使用 链路层发现协议(LLDP)来查询设备邻居,并通过 Graphviz 创建网络拓扑图。完成这个广泛的示例后,我们将看到如何将一些新事物,如 Graphviz,与我们已经学过的(网络 LLDP)结合起来,以解决有趣的问题(自动绘制当前网络拓扑)。
让我们先看看我们将要使用的实验室。
实验室设置
我们将使用与上一章相同的实验室拓扑。为了回顾,我们有一个三层拓扑,其中 r6 是面向外部的边缘设备,r5 是连接服务器的机架顶部路由器。

图 8.1:实验室拓扑
设备是 vIOS 设备,以节省实验室资源并简化配置:
-
由 NX-OS 和 IOS-XR 虚拟化的节点比 IOS 节点内存密集得多。
-
如果你希望使用 NX-OS,考虑使用 NX-API 或其他返回结构化数据的 API 调用。
设备有以下信息:
| 设备 | 管理 IP | 环回 IP |
|---|---|---|
| r1 | 192.168.2.218 |
192.168.0.1 |
| r2 | 192.168.2.219 |
192.168.0.2 |
| r3 | 192.168.2.220 |
192.168.0.3 |
| r5 | 192.168.2.221 |
192.168.0.4 |
| r6 | 192.168.2.222 |
192.168.0.5 |
Ubuntu 主机的信息如下:
| 设备名称 | 外部链路 Eth0 | 内部 IP Eth1 |
|---|---|---|
| 客户端 | 192.168.2.211 |
10.0.0.9 |
| 服务器 | 192.168.2.212 |
10.0.0.5 |
对于我们的示例,我们将使用 LLDP(en.wikipedia.org/wiki/Link_Layer_Discovery_Protocol)。它是一个厂商中立的链路层邻居发现协议。让我们继续安装必要的软件包。
安装
可以通过apt获取 Graphviz:
$ sudo apt-get install graphviz
安装完成后,请注意,验证是通过使用dot命令来完成的:
$ dot -V
dot - graphviz version 2.43.0 (0)$ dot -V
我们将使用 Graphviz 的 Python 包装器,所以现在就让我们安装它:
(venv)$ pip install graphviz
>>> import graphviz
>>> graphviz.__version__
'0.20.1'
>>> exit()
让我们看看我们如何使用这个软件。
Graphviz 示例
就像大多数流行的开源项目一样,Graphviz(www.graphviz.org/documentation/)的文档非常全面。对于软件的新手来说,挑战通常是从零到一的起点。对于我们的目的,我们将专注于 dot 图,它以层次结构的形式绘制有向图(不要与 DOT 语言混淆,DOT 是一种图形描述语言)。
让我们从一些基本概念开始:
-
节点代表我们的网络实体,例如路由器、交换机和服务器
-
边代表网络实体之间的链接
-
图、节点和边都有属性(
www.graphviz.org/doc/info/attrs.html),可以进行调整 -
在描述网络后,我们可以将网络图输出为 PNG、JPEG 或 PDF 格式(
www.graphviz.org/doc/info/output.html)
我们的第一个例子,chapter8_gv_1.gv,是一个由四个节点(core、distribution、access1和access2)组成的无向点图。边由破折号(-)表示,将核心节点与分布节点连接起来,以及将分布节点与两个访问节点连接起来:
graph my_network {
core -- distribution;
distribution -- access1;
distribution -- access2;
}
图可以通过dot -T<格式> source -o <输出文件>命令行输出:
$ mkdir output
$ dot -Tpng chapter8_gv_1.gv -o output/chapter8_gv_1.png
结果图可以在以下输出文件夹中查看:

图 8.2:Graphviz 无向点图示例
就像第七章,使用 Python 进行网络监控 – 第一部分,在处理这些图时在 Linux 桌面窗口中工作可能更容易一些,这样你就可以立即看到图。
注意,我们可以通过指定图为一个 digraph 并使用箭头(->)符号来表示边来使用有向图。在节点和边的情况下,我们可以修改几个属性,例如节点形状、边标签等。相同的图可以在chapter8_gv_2.gv中按以下方式修改:
digraph my_network {
node [shape=box];
size = "50 30";
core -> distribution [label="2x10G"];
distribution -> access1 [label="1G"];
distribution -> access2 [label="1G"];
}
我们这次将输出 PDF 文件:
$ dot -Tpdf chapter8_gv_2.gv -o output/chapter8_gv_2.pdf
查看新图中的方向箭头:

图 8.3:带有方向箭头和行描述的网络图
现在,让我们看看围绕 Graphviz 的 Python 封装。
Python 与 Graphviz 示例
我们可以使用 Python Graphviz 包重现之前相同的拓扑图并构建相同的三层网络拓扑:
>>> from graphviz import Digraph
>>> my_graph = Digraph(comment="My Network")
>>> my_graph.node("core")
>>> my_graph.node("distribution")
>>> my_graph.node("access1")
>>> my_graph.node("access2")
>>> my_graph.edge("core", "distribution")
>>> my_graph.edge("distribution", "access1")
>>> my_graph.edge("distribution", "access2")
代码产生的结果与您通常在 DOT 语言中编写的相同,但方式更 Pythonic。您可以在生成图之前查看图的源代码:
>>> print(my_graph.source)
// My Network
digraph {
core
distribution
access1
access2
core -> distribution
distribution -> access1
distribution -> access2
}
图可以通过 render() 方法进行渲染。默认输出格式是 PDF:
>>> my_graph.render("output/chapter8_gv_3.gv")
'output/chapter8_gv_3.gv.pdf'
Python 包封装紧密地模仿了 Graphviz 的所有 API 选项。您可以在 Graphviz Read The Docs 网站上找到有关选项的文档(graphviz.readthedocs.io/en/latest/index.html)。您还可以参考 GitHub 上的源代码以获取更多信息(github.com/xflr6/graphviz)。我们现在可以使用该工具绘制我们的网络。
LLDP 邻居绘图
在本节中,我们将使用绘制 LLDP 邻居的例子来说明一种多年来帮助我的问题解决模式:
-
如果可能,将每个任务模块化成更小的部分。在我们的例子中,我们可以合并几个步骤,但如果我们将它们分解成更小的部分,我们将能够更容易地重用和改进它们。
-
使用自动化工具与网络设备交互,但在管理站保留更复杂的逻辑。例如,路由器提供了一个有点杂乱的 LLDP 邻居输出。在这种情况下,我们将坚持使用有效的命令和输出,并在管理站使用 Python 脚本来解析我们需要的输出。
-
当对同一任务有选择时,选择可重用的一个。在我们的例子中,我们可以使用低级 Pexpect、Paramiko 或 Ansible playbooks 来查询路由器。在我看来,Ansible 是一个更可重用的选项,所以我选择了它。
要开始,由于默认情况下路由器上未启用 LLDP,我们首先需要在设备上配置它们。到目前为止,我们知道我们有多个选项可供选择;在这种情况下,我选择了带有 ios_config 模块的 Ansible playbook 来完成任务。hosts 文件包含五个路由器:
$ cat hosts
[devices]
r1
r2
r3
r5-tor
r6-edge
[edge-devices]
r5-tor
r6-edge
每个主机都包含 host_vars 文件夹中的对应名称。我们以 r1 为例:
---
ansible_host: 192.168.2.218
ansible_user: cisco
ansible_ssh_pass: cisco
ansible_connection: network_cli
ansible_network_os: ios
ansbile_become: yes
ansible_become_method: enable
ansible_become_pass: cisco
cisco_config_lldp.yml playbook 包含一个带有 ios_lldp 模块的 play:
---
- name: Enable LLDP
hosts: "devices"
gather_facts: false
connection: network_cli
tasks:
- name: enable LLDP service
ios_lldp:
state: present
register: output
- name: show output
debug:
var: output
ios_lldp Ansible 模块是从版本 2.5 及以后版本中引入的。如果您使用的是较旧版本的 Ansible,请使用 ios_config 模块。
运行 playbook 以开启 lldp:
$ ansible-playbook -i hosts cisco_config_lldp.yml
<skip>
PLAY RECAP ****************************************************************************
r1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
r2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
r3 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
r5-tor : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
r6-edge : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
由于默认的 lldp 广告定时器为 30 秒,我们应该等待一段时间,以便设备之间交换 lldp 广告。我们可以验证 LLDP 是否确实在路由器和它发现的邻居上激活:
r1#sh lldp
Global LLDP Information:
Status: ACTIVE
LLDP advertisements are sent every 30 seconds
LLDP hold time advertised is 120 seconds
LLDP interface reinitialisation delay is 2 seconds
r1#sh lldp neighbors
Capability codes:
(R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
(W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other
Device ID Local Intf Hold-time Capability Port ID
r6.virl.info Gi0/1 120 R Gi0/1
r5.virl.info Gi0/2 120 R Gi0/1
Total entries displayed: 2
在 CML 的旧版本中,例如 VIRL 或其他实验室软件中,你可能会在G0/0管理接口上看到 LLDP 邻居。我们真正关心的是直接连接到其他对等体的G0/1和G0/2接口。当准备解析输出和构建我们的拓扑图时,这些信息将非常有用。
信息检索
我们现在可以使用另一个 Ansible 剧本,即cisco_discover_lldp.yml,在设备上执行 LLDP 命令,并将每个设备的输出复制到tmp目录。
让我们创建tmp目录:
$ mkdir tmp
该剧本将包含三个任务。第一个任务将在每个设备上执行show lldp邻居命令,第二个任务将显示输出,第三个任务将输出目录中的输出复制到文本文件:
tasks:
- name: Query for LLDP Neighbors
ios_command:
commands: show lldp neighbors
register: output
- name: show output
debug:
var: output
- name: copy output to file
copy: content="{{ output.stdout_lines }}" dest="./tmp/{{ inventory_hostname }}_lldp_output.txt"
执行后,./tmp目录现在包含所有路由器的输出(显示 LLDP 邻居)在其自己的文件中:
$ ls -l tmp
total 20
-rw-rw-r-- 1 echou echou 413 Sep 18 10:44 r1_lldp_output.txt
-rw-rw-r-- 1 echou echou 413 Sep 18 10:44 r2_lldp_output.txt
-rw-rw-r-- 1 echou echou 413 Sep 18 10:44 r3_lldp_output.txt
-rw-rw-r-- 1 echou echou 484 Sep 18 10:44 r5-tor_lldp_output.txt
-rw-rw-r-- 1 echou echou 484 Sep 18 10:44 r6-edge_lldp_output.txt
r1_lldp_output.txt,与其他输出文件一样,包含来自 Ansible 剧本的每个设备的output.stdout_lines变量:
$ cat tmp/r1_lldp_output.txt
[["Capability codes:", " (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device", " (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other", "", "Device ID Local Intf Hold-time Capability Port ID", "r6.virl.info Gi0/1 120 R Gi0/1", "r5.virl.info Gi0/2 120 R Gi0/1", "", "Total entries displayed: 2"]]
到目前为止,我们已经从网络设备中检索信息。现在我们准备使用 Python 脚本来整合所有内容。
Python 解析脚本
我们现在可以使用 Python 脚本来解析每个设备的 LLDP 邻居输出,并从结果中构建网络拓扑图。目的是自动检查设备,看是否有任何 LLDP 邻居因链路故障或其他问题而消失。让我们看看cisco_graph_lldp.py文件,看看它是如何完成的。
我们从必要的包导入开始:一个空列表,我们将用表示节点关系的元组填充它。我们还知道设备上的Gi0/0连接到管理网络;因此,我们在show LLDP neighbors输出中只搜索Gi0/[1234]作为我们的正则表达式模式:
import glob, re
from graphviz import Digraph, Source
pattern = re.compile('Gi0/[1234]')
device_lldp_neighbors = []
我们将使用glob.glob()方法遍历./tmp目录下的所有文件,解析出设备名称,并找到设备连接到的邻居。脚本中包含一些嵌入的打印语句,我们可以在最终版本中将其注释掉;如果取消注释这些语句,我们可以看到解析结果:
$ python cisco_graph_lldp.py
device: r6-edge
neighbors: r2
neighbors: r1
neighbors: r3
device: r2
neighbors: r5
neighbors: r6
device: r3
neighbors: r5
neighbors: r6
device: r5-tor
neighbors: r3
neighbors: r1
neighbors: r2
device: r1
neighbors: r5
neighbors: r6
完整的边缘列表包含由设备和其邻居组成的元组:
Edges: [('r6-edge', 'r2'), ('r6-edge', 'r1'), ('r6-edge', 'r3'), ('r2', 'r5'), ('r2', 'r6'), ('r3', 'r5'), ('r3', 'r6'), ('r5-tor', 'r3'), ('r5-tor', 'r1'), ('r5-tor', 'r2'), ('r1', 'r5'), ('r1', 'r6')]
我们现在可以使用 Graphviz 包构建网络拓扑图。最重要的是解包表示边关系的元组:
my_graph = Digraph("My_Network")
my_graph.edge("Client", "r6-edge")
my_graph.edge("r5-tor", "Server")
# construct the edge relationships
for neighbors in device_lldp_neighbors:
node1, node2 = neighbors
my_graph.edge(node1, node2)
如果我们打印出结果源 dot 文件,它将是我们网络的准确表示:
digraph My_Network {
Client -> "r6-edge"
"r5-tor" -> Server
"r6-edge" -> r2
"r6-edge" -> r1
"r6-edge" -> r3
r2 -> r5
r2 -> r6
r3 -> r5
r3 -> r6
"r5-tor" -> r3
"r5-tor" -> r1
"r5-tor" -> r2
r1 -> r5
r1 -> r6
}
有时,看到相同的链路两次可能会令人困惑;例如,在之前的图中,r2到r5-tor链路在每个链路方向上出现了两次。作为网络工程师,我们知道有时物理链路的故障会导致单向链路,这是我们不想看到的。
如果我们按照原样绘制图表,节点的放置可能会有些奇怪。节点的放置是自动渲染的。以下图表展示了默认布局以及neato布局(即有向图My_Network,engine='neato')的渲染:

图 8.4:拓扑图 1
neato布局代表了一种尝试用更少的层次结构绘制无向图的方法:

图 8.5:拓扑图 2
有时候,工具提供的默认布局就足够好了,特别是如果你的目标是检测故障而不是使其视觉效果吸引人。然而,在这种情况下,让我们看看我们如何可以将原始 DOT 语言控件插入到源文件中。从研究中我们知道,我们可以使用rank命令来指定某些节点可以保持在同一级别的层级。然而,Graphviz Python API 中没有提供这样的选项。幸运的是,DOT 源文件只是一个字符串,我们可以使用replace()方法将其作为原始 DOT 注释插入,如下所示:
source = my_graph.source
original_text = "digraph My_Network {"
new_text = 'digraph My_Network {\n{rank=same Client "r6-edge"}\n{rank=same r1 r2 r3}\n'
new_source = source.replace(original_text, new_text)
print(new_source)
new_graph = Source(new_source)
new_graph.render("output/chapter8_lldp_graph.gv")
最终结果是我们可以从中渲染最终拓扑图的新的源:
digraph My_Network {
{rank=same Client "r6-edge"}
{rank=same r1 r2 r3}
Client -> "r6-edge"
"r5-tor" -> Server
"r6-edge" -> r2
"r6-edge" -> r1
"r6-edge" -> r3
r2 -> r5
r2 -> r6
r3 -> r5
r3 -> r6
"r5-tor" -> r3
"r5-tor" -> r1
"r5-tor" -> r2
r1 -> r5
r1 -> r6
}
现在图形已经准备好,具有正确的层次结构:

图 8.6:拓扑图 3
我们已经使用 Python 脚本来自动从设备中检索网络信息,并自动绘制拓扑图。这是一项相当多的工作,但回报是确保图形始终代表实际网络的最新状态。让我们通过一些验证来确保我们的脚本可以通过必要的图形检测到网络的最新状态变化。
测试剧本
现在我们已经准备好添加一个测试来检查剧本是否能够准确描述当链路发生变化时拓扑结构的变化。
我们可以通过关闭r6-edge上的Gi0/1和Go0/2接口来测试这一点:
r6#confi t
Enter configuration commands, one per line. End with CNTL/Z.
r6(config)#int gig 0/1
r6(config-if)#shut
r6(config-if)#int gig 0/2
r6(config-if)#shut
r6(config-if)#end
r6#
当 LLDP 邻居通过保持计时器后,它们将从r6-edge上的 LLDP 表中消失:
r6#sh lldp neighbors
Capability codes:
(R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
(W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other
Device ID Local Intf Hold-time Capability Port ID
r1.virl.info Gi0/0 120 R Gi0/0
r2.virl.info Gi0/0 120 R Gi0/0
r3.virl.info Gi0/0 120 R Gi0/0
r5.virl.info Gi0/0 120 R Gi0/0
r3.virl.info Gi0/3 120 R Gi0/1
Device ID Local Intf Hold-time Capability Port ID
Total entries displayed: 5
如果我们执行剧本和 Python 脚本,图形将自动显示r6-edge仅连接到r3,我们可以开始排查为什么会出现这种情况:

图 8.7:拓扑图 4
这是一个相对较长的示例,展示了多个工具协同工作以解决问题。我们使用了我们所学到的工具——Ansible 和 Python——来模块化并将任务分解成可重用的部分。
然后,我们使用了一个新的工具,即 Graphviz,来帮助监控网络,例如网络拓扑关系等非时间序列数据。
在下一节中,我们将稍微改变方向,探讨使用我们网络设备收集的网络流来监控我们的网络。
基于流的监控
如章节介绍中提到的,除了轮询技术,如 SNMP,我们还可以使用推送策略,允许设备将网络信息推送到管理站。NetFlow 及其紧密相关的 IPFIX 和 sFlow 就是从网络设备向管理站推送信息的例子。我们可以认为推送方法更可持续,因为网络设备本质上负责分配推送信息所需的资源。例如,如果设备 CPU 忙碌,它可以跳过流导出过程,转而执行更关键的任务,如路由数据包。
流,正如 IETF 定义的(www.ietf.org/proceedings/39/slides/int/ip1394-background/tsld004.htm),是指从发送应用向接收应用移动的数据包序列。如果我们回顾 OSI 模型,流就是构成两个应用之间单一通信单元的东西。每个流包含一些数据包;有些流包含更多的数据包(例如视频流),而有些则只有少数几个(例如 HTTP 请求)。如果你花一分钟思考流,你会注意到路由器和交换机可能更关心数据包和帧,但应用和用户通常更关心网络流。
基于流的监控通常指的是 NetFlow、IPFIX 和 sFlow:
-
NetFlow:NetFlow v5 是一种技术,网络设备缓存流条目并通过匹配一系列元组(源接口、源 IP/端口、目的 IP/端口等)来聚合数据包。一旦流完成,网络设备将流特征导出到管理站,包括总字节数和包计数。
-
IPFIX:IPFIX 是结构化流式传输的提议标准,类似于 NetFlow v9,也称为灵活 NetFlow。本质上,它是一个可定义的流导出,允许用户导出网络设备所知的几乎所有内容。与 NetFlow v5 相比,灵活性通常以简单性为代价。IPFIX 的配置比传统的 NetFlow v5 更复杂。额外的复杂性使其不太适合入门学习。然而,一旦你熟悉了 NetFlow v5,你就可以解析 IPFIX,只要匹配模板定义即可。
-
sFlow:sFlow 本身没有关于流或数据包聚合的概念。它执行两种类型的数据包采样。它随机从“n”个数据包/应用中采样一个,并有一个基于时间的采样计数器。它将信息发送到管理站,该站通过参考接收到的数据包样本类型和计数器来推导网络流信息。由于它不对网络设备执行任何聚合操作,你可以认为 sFlow 比 NetFlow 和 IPFIX 更可扩展。
了解每一个的最佳方式可能是直接通过示例进行学习。让我们在下一节中查看一些基于流的示例。
使用 Python 进行 NetFlow 解析
我们可以使用 Python 解析在网络上传输的 NetFlow 数据报。这允许我们详细查看 NetFlow 数据包,并排除任何不符合预期的 NetFlow 问题。
首先,让我们在实验室网络中生成客户端和服务器之间的流量。我们可以使用 Python 内置的 HTTP 服务器模块在充当服务器的 VIRL 主机上快速启动一个简单的 HTTP 服务器。打开一个新的终端窗口到服务器主机并启动 HTTP 服务器;让我们保持窗口开启:
cisco@Server:~$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 ...
对于 Python 2,该模块名为 SimpleHTTPServer,例如,python2 -m SimpleHTTPServer。
在一个单独的终端窗口中,使用 ssh 连接到客户端。我们可以在 Python 脚本中创建一个短的 while 循环来持续向 Web 服务器发送 HTTP GET:
cisco@Client:~$ cat http_get.py
import requests
import time
while True:
r = requests.get("http://10.0.0.5:8000")
print(r.text)
time.sleep(5)
客户端应该每 5 秒收到一个非常简单的 HTML 页面:
cisco@Client:~$ python3 http_get.py
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<skip>
</body>
</html>
如果我们回顾服务器的终端窗口,我们还应该看到客户端每 5 秒连续发送的请求:
cisco@Server:~$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 ...
10.0.0.9 - - [02/Oct/2019 00:55:57] "GET / HTTP/1.1" 200 -
10.0.0.9 - - [02/Oct/2019 00:56:02] "GET / HTTP/1.1" 200 -
10.0.0.9 - - [02/Oct/2019 00:56:07] "GET / HTTP/1.1" 200 –
客户端到服务器的流量穿过网络设备,我们可以从中间的任何设备导出 NetFlow。由于 r6-edge 是客户端主机的第一个跳点,我们将让这个路由器将 NetFlow 导出到管理主机的 9995 端口。
在这个例子中,我们只使用一个设备进行演示;因此,我们手动使用必要的命令进行配置。在下一节中,当我们启用所有设备的 NetFlow 时,我们将使用 Ansible playbook 一次性配置所有路由器。
在 Cisco IOS 设备上导出 NetFlow 需要以下配置:
!
ip flow-export version 5
ip flow-export destination 192.168.2.126 9995 vrf Mgmt-intf
!
interface GigabitEthernet0/4
description to Client
ip address 10.0.0.10 255.255.255.252
ip flow ingress
ip flow egress
<skip>
接下来,让我们看看 Python 解析脚本,它帮助我们分离从网络设备接收到的不同网络流字段。
Python Socket 和 Struct
脚本 netFlow_v5_parser.py 是从 Brian Rak 的博客文章 blog.devicenull.org/2013/09/04/python-netflow-v5-parser.html 中修改而来的。修改主要是为了 Python 3 兼容性和解析额外的 NetFlow 版本 5 字段。我们选择 NetFlow v5 而不是 NetFlow v9 的原因是 v9 更复杂,并使用模板来映射字段,这使得在入门课程中学习更加困难。然而,由于 NetFlow 版本 9 是原始 NetFlow 版本 5 的扩展格式,本节中介绍的所有概念都适用于它。
由于 NetFlow 数据包在网络上以字节形式表示,我们将使用标准库中包含的 Python struct 模块将字节转换为原生 Python 数据类型。
你可以在docs.python.org/3.10/library/socket.html和docs.python.org/3.10/library/struct.html找到更多关于这两个模块的信息。
在脚本中,我们将首先使用socket模块来绑定并监听 UDP 数据报。使用socket.AF_INET,我们打算监听 IPv4 地址套接字;使用socket.SOCK_DGRAM,我们指定我们将看到 UDP 数据报:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 9995))
我们将启动一个循环,每次从线路上检索 1,500 字节的信息:
while True:
buf, addr = sock.recvfrom(1500)
以下行是我们开始分解或解包数据包的地方。!HH的第一个参数指定了网络的大端字节序(感叹号表示大端)以及 C 类型的格式(H = 2字节无符号短整数):
(version, count) = struct.unpack('!HH',buf[0:4])
前四个字节包括版本和在此数据包中导出的流数量。如果你不记得 NetFlow 版本 5 的头部(顺便说一句,那是一个笑话;我只在想要快速入睡时才阅读头部),这里是一个快速浏览:

图 8.8:NetFlow v5 头部(来源:http://www.cisco.com/c/en/us/td/docs/net_mgmt/netflow_collection_engine/3-6/user/guide/format.html#wp1006108)
根据字节位置和数据类型,可以相应地解析其余的头部。Python 允许我们在一行中解包多个头部项:
(sys_uptime, unix_secs, unix_nsecs, flow_sequence) = struct.unpack('!IIII', buf[4:20])
(engine_type, engine_id, sampling_interval) = struct.unpack('!BBH', buf[20:24])
下面的while循环将填充nfdata字典,其中包含解包源地址和端口、目标地址和端口、数据包计数和字节计数的流记录,并将信息打印到屏幕上:
nfdata = {}
for i in range(0, count):
try:
base = SIZE_OF_HEADER+(i*SIZE_OF_RECORD)
data = struct.unpack('!IIIIHH',buf[base+16:base+36])
input_int, output_int = struct.unpack('!HH', buf[base+12:base+16])
nfdata[i] = {}
nfdata[i]['saddr'] = inet_ntoa(buf[base+0:base+4])
nfdata[i]['daddr'] = inet_ntoa(buf[base+4:base+8])
nfdata[i]['pcount'] = data[0]
nfdata[i]['bcount'] = data[1]
nfdata[i]['stime'] = data[2]
nfdata[i]['etime'] = data[3]
nfdata[i]['sport'] = data[4]
nfdata[i]['dport'] = data[5]
print(i, " {0}:{1} -> {2}:{3} {4} packts {5} bytes".format(
nfdata[i]['saddr'],
nfdata[i]['sport'],
nfdata[i]['daddr'],
nfdata[i]['dport'],
nfdata[i]['pcount'],
nfdata[i]['bcount']),
)
脚本的输出允许您一目了然地可视化头部以及流内容。在以下输出中,我们可以看到r6-edge上的 BGP 控制包(TCP 端口179)以及 HTTP 流量(TCP 端口8000):
$ python3 netFlow_v5_parser.py
Headers:
NetFlow Version: 5
Flow Count: 6
System Uptime: 116262790
Epoch Time in seconds: 1569974960
Epoch Time in nanoseconds: 306899412
Sequence counter of total flow: 24930
0 192.168.0.3:44779 -> 192.168.0.2:179 1 packts 59 bytes
1 192.168.0.3:44779 -> 192.168.0.2:179 1 packts 59 bytes
2 192.168.0.4:179 -> 192.168.0.5:30624 2 packts 99 bytes
3 172.16.1.123:0 -> 172.16.1.222:771 1 packts 176 bytes
4 192.168.0.2:179 -> 192.168.0.5:59660 2 packts 99 bytes
5 192.168.0.1:179 -> 192.168.0.5:29975 2 packts 99 bytes
**********
Headers:
NetFlow Version: 5
Flow Count: 15
System Uptime: 116284791
Epoch Time in seconds: 1569974982
Epoch Time in nanoseconds: 307891182
Sequence counter of total flow: 24936
0 10.0.0.9:35676 -> 10.0.0.5:8000 6 packts 463 bytes
1 10.0.0.9:35676 -> 10.0.0.5:8000 6 packts 463 bytes
<skip>
11 10.0.0.9:35680 -> 10.0.0.5:8000 6 packts 463 bytes
12 10.0.0.9:35680 -> 10.0.0.5:8000 6 packts 463 bytes
13 10.0.0.5:8000 -> 10.0.0.9:35680 5 packts 973 bytes
14 10.0.0.5:8000 -> 10.0.0.9:35680 5 packts 973 bytes
注意,在 NetFlow 版本 5 中,记录大小固定为 48 字节;因此,循环和脚本相对简单。
然而,在 NetFlow 版本 9 或 IPFIX 的情况下,在头部之后,有一个模板 FlowSet(www.cisco.com/en/US/technologies/tk648/tk362/technologies_white_paper09186a00800a3db9.html),它指定了字段计数、字段类型和字段长度。这使得收集器可以在不知道数据格式的情况下解析数据。我们需要在 Python 脚本中为 NetFlow 版本 9 构建额外的逻辑。
通过在脚本中解析 NetFlow 数据,我们深入理解了字段,但这非常繁琐且难以扩展。正如你可能猜到的,其他工具解决了逐个解析 NetFlow 记录的问题。让我们在下一节中看看这样一个工具,称为ntop。
ntop 流量监控
与第七章中的 PySNMP 脚本和本章中的 NetFlow 解析器脚本一样,我们可以使用 Python 脚本来处理线上的低级任务。然而,有一些工具,如 Cacti,这是一个包含数据收集(轮询器)、数据存储(RRDs)和用于可视化的 Web 前端的一站式开源包。这些工具可以通过在一个包中打包常用的功能和软件来节省你大量的工作。
在 NetFlow 的情况下,我们有几个开源和商业 NetFlow 收集器可供选择。如果我们快速搜索前 N 个开源 NetFlow 分析器,我们将看到针对不同工具的几个比较研究。
每个都有自己的优点和缺点;使用哪个取决于个人喜好、平台和对定制的需求。我建议选择一个支持 v5 和 v9,以及可能 sFlow 的工具。次要考虑因素是工具是否是用我们能够理解的语言编写的;我想象拥有 Python 的可扩展性将是一件好事。
我喜欢并之前使用过的两个开源 NetFlow 工具是 NfSen(后端收集器为 NFDUMP)和ntop(或ntopng)。在这两者之间,ntop是更知名的流量分析器;它运行在 Windows 和 Linux 平台上,并且与 Python 集成良好。因此,让我们在本节中使用ntop作为示例。
与 Cacti 类似,ntop 是一个一站式工具。我建议在生产环境中在管理站之外安装 ntop,或者在管理站上安装容器。
我们 Ubuntu 主机的安装过程很简单:
$ sudo apt-get install ntop
安装过程将提示必要的监听接口和设置管理员密码。默认情况下,ntop的 Web 界面监听端口3000,而探针监听 UDP 端口5556。在网络设备上,我们需要指定 NetFlow 导出器的位置:
!
ip flow-export version 5
ip flow-export destination 192.168.2.126 5556 vrf Mgmt-intf
!
默认情况下,IOSv 创建一个名为Mgmt-intf的 VRF,并将Gi0/0置于 VRF 下。
我们还需要在接口配置下指定流量导出的方向,例如ingress或egress:
!
interface GigabitEthernet0/0
...
ip flow ingress
ip flow egress
...
为了供你参考,我包括了 Ansible playbook,cisco_config_netflow.yml,用于配置实验室设备的 NetFlow 导出。
r5-tor和r6-edge比r1、r2和r3多两个接口;因此,有一个额外的 playbook 来为它们启用额外的接口。
执行 playbook 并确保设备上正确应用了更改:
$ ansible-playbook -i hosts cisco_config_netflow.yml
TASK [configure netflow export station] ****************************************************************************
changed: [r2]
changed: [r1]
changed: [r3]
changed: [r5-tor]
changed: [r6-edge]
TASK [configure flow export on Gi0/0] ****************************************************************************
ok: [r1]
ok: [r3]
ok: [r2]
ok: [r5-tor]
ok: [r6-edge]
<skip>
在 playbook 运行后验证设备配置总是一个好主意,所以让我们对r2进行抽查:
r2#sh run
!
interface GigabitEthernet0/0
description OOB Management
vrf forwarding Mgmt-intf
ip address 192.168.2.126 255.255.255.0
ip flow ingress
ip flow egress
<skip>
!
ip flow-export version 5
ip flow-export destination 192.168.2.126 5556 vrf Mgmt-intf
!
一切设置完成后,你可以检查ntop的 Web 界面以查看本地 IP 流量:

图 8.9:ntop 本地 IP 流量
ntop 最常使用的功能之一是使用它查看 Top Talkers 图:

图 8.10:ntop 主要通信者
ntop 报告引擎是用 C 编写的;它快速高效,但需要对 C 有足够的了解才能进行像更改 Web 前端这样简单的操作,这与现代敏捷开发思维不符。
在 2000 年代中期尝试使用 Perl 失败几次后,ntop 的好人们最终决定嵌入 Python 作为可扩展的脚本引擎。让我们看看。
ntop 的 Python 扩展
我们可以使用 Python 通过 ntop Web 服务器扩展 ntop。ntop Web 服务器可以执行 Python 脚本。从高层次来看,脚本将涉及以下内容:
-
访问 ntop 状态的几种方法
-
Python CGI 模块用于处理表单和 URL 参数
-
制作生成动态 HTML 页面的模板
-
每个 Python 脚本都可以从
stdin读取并输出stdout/stderr。stdout脚本即为返回的 HTTP 页面。
Python 集成提供了几个有用的资源。在 Web 界面下,您可以点击关于 | 显示配置来查看 Python 解释器版本以及您的 Python 脚本目录:

图 8.11:Python 版本
您还可以检查 Python 脚本应驻留的各个目录:

图 8.12:插件目录
在关于 | 在线文档 | Python ntop 引擎下,有 Python API 和教程的链接:

图 8.13:Python ntop 文档
如前所述,ntop Web 服务器直接执行放置在指定目录下的 Python 脚本:
$ pwd
/usr/share/ntop/python
我们将把我们的第一个脚本,即chapter8_ntop_1.py,放置在目录中。Python CGI模块处理表单并解析 URL 参数:
# Import modules for CGI handling
import cgi, cgitb
import ntop
# Parse URL cgitb.enable();
ntop实现了三个 Python 模块;每个模块都有特定的用途:
-
ntop: 本模块与
ntop引擎交互。 -
主持人: 本模块深入解析特定主机的信息。
-
接口: 本模块表示本地主机接口的信息。
在我们的脚本中,我们将使用ntop模块检索ntop引擎信息,并使用sendString()方法发送 HTML 正文:
form = cgi.FieldStorage();
name = form.getvalue('Name', default="Eric")
version = ntop.version()
os = ntop.os()
uptime = ntop.uptime()
ntop.printHTMLHeader('Mastering Python Networking', 1, 0) ntop.sendString("Hello, "+ name +"<br>")
ntop.sendString("Ntop Information: %s %s %s" % (version, os, uptime))
ntop.printHTMLFooter()
我们将使用http://<ip>:3000/python/<script name>执行 Python 脚本。以下是我们的chapter8_ntop_1.py脚本的执行结果:

图 8.14:ntop 脚本结果
我们可以查看另一个与接口模块交互的示例,即chapter8_ntop_2.py。我们将使用 API 遍历接口:
import ntop, interface, json
ifnames = []
try:
for i in range(interface.numInterfaces()):
ifnames.append(interface.name(i))
except Exception as inst:
print(type(inst)) # the exception instance
print(inst.args) # arguments stored in .args
print(inst) # str _ allows args to printed directly
<skip>
生成的页面将显示ntop接口:

图 8.15:ntop 界面信息
除了社区版之外,ntop 还提供了一些商业产品,您可以选择。凭借活跃的开源社区、商业支持以及 Python 可扩展性,ntop 是满足您的 NetFlow 监控需求的好选择。
接下来,让我们看看 NetFlow 的表亲:sFlow。
sFlow
sFlow,代表采样流,最初由 InMon(www.inmon.com)开发,后来通过 RFC 标准化。当前版本是 v5。行业中有许多人认为 sFlow 的主要优势是其可扩展性。
sFlow 使用随机的[在n中的一个]数据包流样本以及计数器样本的轮询间隔来估计流量;这比 NetFlow 对网络设备的 CPU 密集度更低。sFlow 的统计采样与硬件集成,并提供实时原始导出。
由于可扩展性和竞争原因,对于像 Arista Networks、Vyatta 和 A10 Networks 这样的新供应商,通常更倾向于使用 sFlow 而不是 NetFlow。虽然思科在其 Nexus 产品线支持 sFlow,但 sFlow 通常“不支持”在思科平台上。
SFlowtool 和 sFlow-RT 与 Python
不幸的是,到目前为止,sFlow 是我们 CML 实验室设备不支持的东西(甚至不包括 NX-OSv 虚拟交换机)。您可以使用支持 sFlow 的思科 Nexus 3000 交换机或其他供应商的交换机,例如 Arista。对于实验室来说,另一个好选择是使用 Arista vEOS 虚拟实例。我有一个运行 7.0(3)的思科 Nexus 3048 交换机的访问权限,我将在此部分中使用它作为 sFlow 导出器。
配置思科 Nexus 3000 的 sFlow 很简单:
Nexus-2# sh run | i sflow feature sflow
sflow max-sampled-size 256
sflow counter-poll-interval 10
sflow collector-ip 192.168.199.185 vrf management sflow agent-ip 192.168.199.148
sflow data-source interface Ethernet1/48
使用sflowtool是摄取 sFlow 的最简单方法。有关安装说明,请参阅blog.sflow.com/2011/12/sflowtool.html上的文档:
$ wget http://www.inmon.com/bin/sflowtool-3.22.tar.gz
$ tar -xvzf sflowtool-3.22.tar.gz
$ cd sflowtool-3.22/
$ ./configure
$ make
$ sudo make install
我在实验室中使用的是较旧的sFlowtool版本。新版本的工作方式相同。
安装完成后,您可以启动sflowtool并查看 Nexus 3048 在标准输出上发送的数据报:
$ sflowtool
startDatagram =================================
datagramSourceIP 192.168.199.148
datagramSize 88
unixSecondsUTC 1489727283
datagramVersion 5
agentSubId 100
agent 192.168.199.148
packetSequenceNo 5250248
sysUpTime 4017060520
samplesInPacket 1
startSample ----------------------
sampleType_tag 0:4 sampleType COUNTERSSAMPLE sampleSequenceNo 2503508
sourceId 2:1
counterBlock_tag 0:1001
5s_cpu 0.00
1m_cpu 21.00
5m_cpu 20.80
total_memory_bytes 3997478912
free_memory_bytes 1083838464 endSample ----------------------
endDatagram =================================
sflowtool GitHub 仓库(github.com/sflow/sflowtool)中有许多良好的使用示例;其中之一是使用脚本接收sflowtool输入并解析输出。我们可以为此目的使用 Python 脚本。在chapter8_sflowtool_1.py示例中,我们将使用sys.stdin.readline接收输入,并使用正则表达式搜索来打印出包含单词 agent 的行,当我们看到 sFlow 数据包时:
#!/usr/bin/env python3
import sys, re
for line in iter(sys.stdin.readline, ''):
if re.search('agent ', line):
print(line.strip())
可以将脚本管道化到sflowtool:
$ sflowtool | python3 chapter8_sflowtool_1.py
agent 192.168.199.148
agent 192.168.199.148
有许多其他有用的输出示例,例如以 NetFlow 版本 5 记录输出的tcpdump,以及紧凑的逐行输出。这使得sflowtool适用于不同的监控环境。
ntop 支持 sFlow,这意味着您可以直接将 sFlow 导出到 ntop 收集器。如果您的收集器仅支持 NetFlow,您可以使用-c选项在 NetFlow 版本 5 格式中为sflowtool输出:
$ sflowtool --help
...
tcpdump output:
-t - (output in binary tcpdump(1) format)
-r file - (read binary tcpdump(1) format)
-x - (remove all IPV4 content)
-z pad - (extend tcpdump pkthdr with this many zeros
e.g. try -z 8 for tcpdump on Red Hat Linux 6.2)
NetFlow output:
-c hostname_or_IP - (netflow collector host)
-d port - (netflow collector UDP port)
-e - (netflow collector peer_as (default = origin_as))
-s - (disable scaling of netflow output by sampling rate)
-S - spoof source of netflow packets to input agent IP
或者,您也可以使用 InMon 的 sFlow-RT(www.sflow-rt.com/index.php)作为您的 sFlow 分析引擎。从操作员的角度来看,sFlow-RT 与众不同的地方是其庞大的 RESTful API,可以根据您的用例进行定制。您也可以轻松地从 API 中检索指标。您可以在www.sflow-rt.com/reference.php查看其广泛的 API 参考。
注意,sFlow-RT 需要 Java 来运行以下操作:
$ sudo apt-get install default-jre
$ java -version
openjdk version "1.8.0_121"
OpenJDK Runtime Environment (build 1.8.0_121-8u121-b13-0ubuntu1.16.04.2- b13)
OpenJDK 64-Bit Server VM (build 25.121-b13, mixed mode)
安装后,下载和运行 sFlow-RT 非常简单(sflow-rt.com/download.php):
$ wget http://www.inmon.com/products/sFlow-RT/sflow-rt.tar.gz
$ tar -xvzf sflow-rt.tar.gz
$ cd sflow-rt/
$ ./start.sh
2017-03-17T09:35:01-0700 INFO: Listening, sFlow port 6343
2017-03-17T09:35:02-0700 INFO: Listening, HTTP port 8008
我们可以将网络浏览器指向 HTTP 端口8008并验证安装:

图 8.16:sFlow-RT 版本
一旦 sFlow-RT 收到任何 sFlow 数据包,代理和其他指标将出现:

图 8.17:sFlow-RT 代理 IP
这里有两个使用 Python requests 从 sFlow-RT 的 REST API 检索信息的示例:
>>> import requests
>>> r = requests.get("http://192.168.199.185:8008/version")
>>> r.text '2.0-r1180'
>>> r = requests.get("http://192.168.199.185:8008/agents/json")
>>> r.text
'{"192.168.199.148": {n "sFlowDatagramsLost": 0,n "sFlowDatagramSource": ["192.168.199.148"],n "firstSeen": 2195541,n "sFlowFlowDuplicateSamples": 0,n "sFlowDatagramsReceived": 441,n "sFlowCounterDatasources": 2,n "sFlowFlowOutOfOrderSamples": 0,n "sFlowFlowSamples": 0,n "sFlowDatagramsOutOfOrder": 0,n "uptime": 4060470520,n "sFlowCounterDuplicateSamples": 0,n "lastSeen": 3631,n "sFlowDatagramsDuplicates": 0,n "sFlowFlowDrops": 0,n "sFlowFlowLostSamples": 0,n "sFlowCounterSamples": 438,n "sFlowCounterLostSamples": 0,n "sFlowFlowDatasources": 0,n "sFlowCounterOutOfOrderSamples": 0n}}'
请查阅参考文档,了解适用于您需求的额外 REST 端点。
在本节中,我们探讨了基于 sFlow 的监控示例,既作为独立工具,也作为与ntop集成的部分。sFlow 是旨在解决传统netflow格式所面临的可扩展性问题的新流格式之一,值得我们花些时间看看它是否是手头网络监控任务的正确工具。我们接近本章的结尾,让我们看看我们涵盖了哪些内容。
摘要
在本章中,我们探讨了我们可以利用 Python 的额外方式来增强我们的网络监控工作。我们开始使用 Python 的 Graphviz 包,利用网络设备报告的实时 LLDP 信息创建网络拓扑图。这使得我们能够轻松地显示当前网络拓扑,以及轻松地注意到任何链路故障。
接下来,我们使用 Python 解析 NetFlow 版本 5 数据包,以增强我们对 NetFlow 的理解和故障排除。我们还探讨了如何使用 ntop 和 Python 扩展 ntop 以进行 NetFlow 监控。sFlow 是一种替代的包采样技术。我们使用sflowtool和 sFlow-RT 来解释 sFlow 结果。
在第九章,使用 Python 构建网络 Web 服务中,我们将探讨如何使用 Python Web 框架 Flask 来构建网络 Web 服务。
加入我们的书籍社区
要加入本书的社区——在那里您可以分享反馈、向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第九章:使用 Python 构建网络 Web 服务
在前面的章节中,我们是他人提供的 API 的消费者。在第三章,APIs 和 Intent-Driven Networking中,我们看到了我们可以使用HTTP POST请求到 NX-API 的http://<your device ip>/ins URL,在HTTP POST体中嵌入CLI命令,以在 Cisco Nexus 设备上远程执行命令;设备随后在其 HTTP 响应中返回命令执行输出。在第八章,Network Monitoring with Python – Part 2中,我们使用HTTP GET方法在我们的 sFlow-RT 上http://<your host ip>:8008/version,使用空体来检索 sFlow-RT 软件的版本。这些请求-响应交换是 RESTful 网络服务的例子。
根据维基百科(en.wikipedia.org/wiki/Representational_state_transfer):
“表示状态转移(REST)或 RESTful 网络服务是提供互联网上计算机系统之间互操作性的方法之一。符合 REST 规范的 Web 服务允许请求系统使用统一和预定义的无状态操作集来访问和操作 Web 资源的文本表示。”
正如所注,使用 HTTP 协议的 RESTful 网络服务只是网络信息交换的许多方法之一;其他形式的网络服务也存在。然而,它是今天最常用的网络服务,相关的GET、POST、PUT和DELETE动词作为信息交换的预定义方式。
如果您对 HTTPS 与 HTTP 进行比较,在我们的讨论中,我们将 HTTPS 视为 HTTP 的安全扩展(en.wikipedia.org/wiki/HTTPS),与 RESTful API 具有相同的底层协议。
在服务提供方,向用户提供 RESTful 服务的一个优点是能够隐藏内部操作。例如,在 sFlow-RT 的情况下,如果我们想登录设备查看安装的软件版本,而不是使用其 RESTful API,我们就需要更深入地了解工具,知道在哪里检查。然而,通过将资源作为 URL 提供,API 提供者将版本检查操作从请求者那里抽象出来,使得操作变得更加简单。这种抽象还提供了一层安全性,因为它可以根据需要仅打开端点。
作为我们网络宇宙的主宰,RESTful 网络服务为我们提供了许多显著的益处,我们可以享受以下好处:
-
您可以抽象请求者,使其不必了解网络操作的内部。例如,我们可以提供一个网络服务来查询交换机版本,而无需请求者知道确切的 CLI 命令或交换机 API。
-
我们可以整合和定制符合我们网络需求独特操作的,例如一个用于升级我们所有机架顶部交换机的资源。
-
我们可以通过仅按需公开操作来提供更好的安全性。例如,我们可以为核心网络设备提供只读 URL(
GET),为访问级别交换机提供读写 URL(GET/POST/PUT/DELETE)。
在本章中,我们将使用最受欢迎的 Python 网络框架之一,Flask,来为我们的网络创建 RESTful 网络服务。在本章中,我们将学习以下内容:
-
比较 Python 网络框架
-
Flask 简介
-
涉及静态网络内容操作
-
涉及动态网络操作的操作
-
认证和授权
-
在容器中运行我们的网络应用
让我们从查看可用的 Python 网络框架以及为什么我们选择了 Flask 开始。
比较 Python 网络框架
Python 以其众多的网络框架而闻名。在 Python 社区中有一个流行的笑话,那就是你是否可以作为一个全职的 Python 开发者而不使用任何 Python 网络框架。有几个 Python 网络开发者会议,包括 DjangoCon US (djangocon.us/)、DjangoCon EU (djangocon.eu/)、FlaskCon (flaskcon.com/)、Python Web Conference (pythonwebconf.com/)以及许多本地聚会。每个会议每年都吸引数百名参与者。我提到过 Python 有一个蓬勃发展的网络开发社区吗?
如果你按hotframeworks.com/languages/python对 Python 网络框架进行排序,你会发现当涉及到 Python 和网络框架时,选择余地非常丰富:

图 9.1:Python 网络框架排名(来源:https://hotframeworks.com/languages/python)
在最近的 2021 年 Python 开发者调查中,Flask 略胜 Django,成为最受欢迎的网络框架:

图 9.2:2021 年 Python 开发者调查(来源:https://lp.jetbrains.com/python-developers-survey-2021/)
在这么多选项中,我们应该选择哪个框架?逐一尝试所有框架将耗费大量时间。哪个网络框架更好的问题也是网络开发者之间一个充满激情的话题。如果你在任何论坛上提出这个问题,比如 Quora,或者在 Reddit 上搜索,准备好接受一些高度主观的回答和激烈的辩论。
说到 Quora 和 Reddit,这里有一个有趣的事实:Quora 和 Reddit 都是用 Python 编写的。Reddit 使用 Pylons(www.reddit.com/wiki/faq#wiki_so_what_python_framework_do_you_use.3F),而 Quora 最初使用 Pylons,但用内部代码替换了框架的一部分(www.quora.com/What-languages-and-frameworks-are-used-to-code-Quora)。
当然,我偏向于编程语言(Python!)和 Web 框架(Flask 和 Django!)。在本节中,我希望传达我选择其中一个而不是另一个进行特定项目的理由。让我们从先前的 HotFrameworks 列表中挑选前两个框架进行比较:
-
Django:自诩为“对完美主义者有截止日期的 Web 框架”,是一个高级 Python Web 框架,鼓励快速开发和清晰、实用的设计(
www.djangoproject.com/)。它是一个大型框架,包含预构建的代码,提供管理面板和内置内容管理。 -
Flask:这是一个基于 Werkzeug、Jinja2 和其他应用的 Python 微框架(
palletsprojects.com/p/flask/)。作为一个微框架,Flask 旨在保持核心小且易于扩展,当需要时。微框架中的“微”并不意味着 Flask 缺乏功能,也不意味着它不能在生产环境中工作。
我使用 Django 处理一些较大的项目,而使用 Flask 进行快速原型设计。Django 框架对如何做事有很强的观点;任何偏离它的做法有时都会让用户感觉像是在“与框架抗争”。例如,如果你查看 Django 数据库文档(docs.djangoproject.com/en/4.0/ref/databases/),你会注意到该框架支持几种不同的 SQL 数据库。然而,它们都是 SQL 数据库的变体,如 MySQL、PostgreSQL、SQLite 等。
如果我们想使用像 MongoDB 或 CouchDB 这样的 NoSQL 数据库呢?这可能可行,但可能会让我们自己动手,因为 Django 没有官方支持。作为一个有观点的框架当然不是坏事。这只是观点的问题(没有打趣的意思)。
当我们需要简单快速的东西时,保持核心代码小并在需要时扩展它是非常吸引人的。文档中的初始示例只有六行代码,即使你没有任何先前的经验也容易理解。由于 Flask 是考虑到扩展而构建的,因此编写我们的扩展,如装饰器,相对容易。尽管它是一个微型框架,但 Flask 核心仍然包括必要的组件,如开发服务器、调试器、与单元测试的集成、RESTful 请求分发等,以帮助您快速入门。
如你所见,Django 和 Flask 几乎在所有衡量标准上都是最受欢迎的 Python 网络框架。我们选择任何一个作为起点都不会出错。这两个框架带来的流行度意味着它们都有广泛的社区贡献和支持,并且可以快速开发现代功能。
为了便于部署,我认为在构建网络 Web 服务时,Flask 是我们理想的选择。
Flask 和实验室设置
在本章中,我们将继续使用虚拟环境来隔离 Python 环境和依赖。我们可以启动一个新的虚拟环境,或者我们可以继续使用我们一直在使用的现有虚拟环境。我的偏好是启动一个新的虚拟环境。我将称它为ch09-venv:
$ python3 -m venv ch09-venv
$ source ch09-venv/bin/activate
在本章中,我们将安装相当多的 Python 包。为了使生活更轻松,我在本书的 GitHub 仓库中包含了一个requirements.txt文件;我们可以使用它来安装所有必要的包(记得激活你的虚拟环境)。你应该在安装过程中看到正在下载和成功安装的包:
(ch09-venv) $ cat requirements.txt
click==8.1.3
Flask==2.2.2
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
Werkzeug==2.2.2
…
(ch09-venv) $ pip install -r requirements.txt
对于我们的网络拓扑,我们将使用我们在前几章中使用的2_DC_Topology,如图所示:

图 9.3:实验室拓扑
让我们接下来看看 Flask。
请注意,从现在开始,我将假设您将始终在虚拟环境中执行,并且您已经在requirements.txt文件中安装了必要的包。
Flask 简介
与大多数流行的开源项目一样,Flask 拥有非常好的文档,可在flask.palletsprojects.com/en/2.0.x/找到。如果你想深入了解 Flask,项目文档是一个很好的起点。
我强烈推荐 Miguel Grinberg 与 Flask 相关的作品(blog.miguelgrinberg.com/)。他的博客、书籍和视频培训让我对 Flask 有了很多了解。Miguel 的《使用 Flask 构建 Web API》课程激发了我构建第一个基于 Flask 的 API,并启发了本章的写作。您可以在 GitHub 上查看他发布的代码:github.com/miguelgrinberg/。
Flask 版本
到本书写作时,Flask 的最新版本是 2.2.2。Flask 2.0.0 版本于 2021 年 5 月从 1.1.4 版本发布。在这次发布中引入了几个重大变化,因此版本号有大幅提升。以下是一些重大变化的列表:
-
Flask 2.0 正式停止了对 Python 2 和 Python 3.5 的支持。
-
支持 Python 3 类型提示。
-
引入了 HTTP 方法装饰器。
这些变化可能在这个阶段意义不大,因为我们只是刚开始使用 Flask。目前,如果我们正在寻找答案和示例,请记住版本的大幅变化。如果可能的话,寻找基于 2 版本及以上的示例。
Flask 示例
我们的第一款 Flask 应用程序包含在一个单独的文件中,chapter9_1.py:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_networkers():
return 'Hello Networkers!'
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
这是一个 Flask 应用程序简单的设计模式。我们使用应用程序模块包的名称作为第一个参数创建 Flask 类的实例。在这种情况下,我们使用了一个可以作为应用程序启动的单个模块;稍后我们将看到如何将其作为包导入。然后我们使用路由装饰器告诉 Flask 哪个 URL 应由 hello_networkers() 函数处理;在这种情况下,我们指明了根路径。我们以通常的名称作用域结束文件,检查脚本是否单独运行(docs.python.org/3.10/library/__main__.html).
我们还添加了 host 和 debug 选项,这允许更详细的输出,并允许我们在所有主机接口上监听。我们可以使用开发服务器运行此应用程序:
(ch09-venv) $ python chapter9_1.py
* Serving Flask app 'chapter9_1'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000
* Running on http://192.168.2.126:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 218-520-639
如果你从开发服务器收到地址已在使用的错误,可以通过port=xxxx选项更改 Flask 开发服务器运行的端口,flask.palletsprojects.com/en/2.2.x/server/.
现在我们有一个服务器正在运行,让我们使用 HTTP 客户端测试服务器响应。
HTTPie 客户端
我们已经将 HTTPie (httpie.org/) 作为 requirements.txt 文件安装过程的一部分安装好了。这本书是黑白印刷的,所以示例中没有显示颜色高亮,但在你的安装中,你可以看到 HTTPie 对 HTTP 事务有更好的语法高亮。它还与 RESTful HTTP 服务器有更直观的命令行交互。
我们可以使用它来测试我们的第一个 Flask 应用程序(后续将提供更多 HTTPie 的示例)。我们将在管理主机上启动第二个终端窗口,激活虚拟环境,并输入以下内容:
(ch09-venv) $ http http://192.168.2.126:5000
HTTP/1.1 200 OK
Connection: close
Content-Length: 17
Content-Type: text/html; charset=utf-8
Date: Wed, 21 Sep 2022 02:54:54 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
Hello Networkers!
作为比较,如果我们使用 curl,我们需要使用 -i 开关来达到相同的效果:curl -i http://192.168.2.126:5000。
我们将使用HTTPie作为本章的客户端;花一两分钟了解其用法是值得的。我们将使用免费网站 HTTPBin(httpbin.org/)来演示HTTPie的使用。HTTPie的使用遵循以下简单模式:
$ http [flags] [METHOD] URL [ITEM]
按照前面的模式,一个GET请求非常直接,正如我们在我们的 Flask 开发服务器中看到的那样:
(ch09-venv) $ http GET https://httpbin.org/user-agent
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 35
Content-Type: application/json
Date: Wed, 21 Sep 2022 02:56:07 GMT
Server: gunicorn/19.9.0
{
"user-agent": "HTTPie/3.2.1"
}
JSON 是HTTPie的默认隐式内容类型。如果你的 HTTP 正文只包含字符串,则不需要其他操作。如果你需要应用非字符串的 JSON 字段,请使用:=或其他文档化的特殊字符。在以下示例中,我们希望"married"变量是一个Boolean而不是一个字符串:
(ch09-venv) $ http POST https://httpbin.org/post name=eric twitter=at_ericchou married:=true
…
Content-Type: application/json
…
{…
"headers": {
"Accept": "application/json, */*;q=0.5",
…
"Host": "httpbin.org",
"User-Agent": "HTTPie/3.2.1",
…
},
"json": {
"married": true,
"name": "eric",
"twitter": "at_ericchou"
},
"url": "https://httpbin.org/post"
}
如您所见,HTTPie比传统的 curl 语法有了很大的改进,使得测试 REST API 变得轻而易举。
更多使用示例可在httpie.io/docs/cli/usage找到。
回到我们的 Flask 程序,API 构建的大部分内容都是基于 URL 路由的流程。让我们更深入地看看app.route()装饰器。
URL 路由
我们在chapter9_2.py中添加了两个额外的函数,并将它们与适当的app.route()路由配对:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'You are at index()'
@app.route('/routers/')
def routers():
return 'You are at routers()'
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
结果是不同的端点被传递到不同的函数中。我们可以通过两个http请求来验证这一点:
# Server side
$ python chapter9_2.py
<skip>
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
# client side
$ http http://192.168.2.126:5000
<skip>
You are at index()
$ http http://192.168.2.126:5000/routers/
<skip>
You are at routers()
由于请求是从客户端发起的,服务器屏幕将看到请求进入:
(ch09-venv) $ python chapter9_2.py
<skip>
192.168.2.126 - - [20/Sep/2022 20:00:27] "GET / HTTP/1.1" 200 -
192.168.2.126 - - [20/Sep/2022 20:01:05] "GET /routers/ HTTP/1.1" 200 –
如我们所见,不同的端点对应不同的函数;函数返回的内容就是服务器返回给请求者的内容。当然,如果我们必须始终保持路由静态,那么路由将会非常有限。有一种方法可以从 URL 传递动态变量到 Flask;我们将在下一节中查看这个示例。
URL 变量
我们可以将动态变量传递到 URL 中,如chapter9_3.py示例所示:
<skip>
@app.route('/routers/<hostname>')
def router(hostname):
return 'You are at %s' % hostname
@app.route('/routers/<hostname>/interface/<int:interface_number>')
def interface(hostname, interface_number):
return 'You are at %s interface %d' % (hostname, interface_number)
<skip>
在这两个函数中,我们在客户端发起请求时传递动态信息,如主机名和接口号。请注意,在/routers/<hostname> URL 中,我们将<hostname>变量作为字符串传递;在/routers/<hostname>/interface/<int:interface_number>中,我们指定int变量应该仅是整数。让我们运行示例并发出一些请求:
# Server Side
(ch09-venv) $ python chapter9_3.py
(ch09-venv) # Client Side
$ http http://192.168.2.126:5000/routers/host1
HTTP/1.0 200 OK
<skip>
You are at host1
(venv) $ http http://192.168.2.126:5000/routers/host1/interface/1
HTTP/1.0 200 OK
<skip>
You are at host1 interface 1
如果int变量不是整数,将会抛出错误:
(venv) $ http http://192.168.2.126:5000/routers/host1/interface/one
HTTP/1.0 404 NOT FOUND
<skip>
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.</p>
转换器包括整数、浮点数和路径(它接受斜杠)。
除了匹配静态路由与动态变量外,我们还可以在应用程序启动时生成 URL。当我们事先不知道端点变量或端点基于其他条件(如从数据库查询的值)时,这非常有用。让我们看看这个示例。
URL 生成
在 chapter9_4.py 中,我们希望在应用程序启动时动态创建一个 URL,形式为 /<hostname>/list_interfaces,其中主机名可以是 r1、r2 或 r3。我们已经知道我们可以静态配置三个路由和三个相应的函数,但让我们看看如何在应用程序启动时实现这一点:
from flask import Flask, url_for
app = Flask(__name__)
@app.route('/<hostname>/list_interfaces')
def device(hostname):
if hostname in routers:
return 'Listing interfaces for %s' % hostname
else:
return 'Invalid hostname'
routers = ['r1', 'r2', 'r3']
for router in routers:
with app.test_request_context():
print(url_for('device', hostname=router))
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
执行后,我们将有一些很好的、逻辑上围绕路由列表循环的 URL,而不需要静态定义每个 URL:
# server side
$ python chapter9_4.py
<skip>
/r1/list_interfaces
/r2/list_interfaces
/r3/list_interfaces
# client side
(venv) $ http http://192.168.2.126:5000/r1/list_interfaces
<skip>
Listing interfaces for r1
(venv) $ http http://192.168.2.126:5000/r2/list_interfaces
<skip>
Listing interfaces for r2
# bad request
(venv) $ http http://192.168.2.126:5000/r1000/list_interfaces
<skip>
Invalid hostname
目前,你可以将 app.text_request_context() 视为一个用于演示目的的虚拟请求对象。如果你对局部上下文感兴趣,可以自由查看 werkzeug.palletsprojects.com/en/2.2.x/local/。动态生成 URL 端点极大地简化了我们的代码,节省了时间,并使代码更容易阅读。
jsonify 返回值
Flask 中的另一个节省时间的功能是 jsonify() 返回值,它封装了 json.dumps(),并将 JSON 输出转换为带有 HTTP 头中 application/json 作为内容类型的响应对象。我们可以稍微调整一下 chapter9_3.py 脚本,如 chapter9_5.py 中所示:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/routers/<hostname>/interface/<int:interface_number>')
def interface(hostname, interface_number):
return jsonify(name=hostname, interface=interface_number)
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=True)
几行代码后,返回结果现在是一个带有适当头部的 JSON 对象:
$ http http://192.168.2.126:5000/routers/r1/interface/1
HTTP/1.0 200 OK
Content-Length: 38
Content-Type: application/json
Date: Tue, 08 Oct 2019 21:48:51 GMT
Server: Werkzeug/0.16.0 Python/3.6.8
{
"interface": 1,
"name": "r1"
}
结合我们迄今为止学到的所有 Flask 特性,我们现在可以准备构建我们网络的 API。
网络资源 API
当我们在生产中有网络设备时,每个设备都将有一定的状态和信息,你希望将其保存在持久的位置,以便你可以稍后轻松检索它们。这通常是通过在数据库中存储数据来完成的。我们在监控章节中看到了许多此类信息存储的例子。
然而,我们通常不会直接给其他可能需要这些信息的非网络管理用户数据库的直接访问权限;他们也不希望学习所有复杂的 SQL 查询语言。对于这些情况,我们可以利用 Flask 和 Flask 的 Flask-SQLAlchemy 扩展来通过网络 API 提供他们所需的信息。
你可以在 flask-sqlalchemy.palletsprojects.com/en/2.x/ 上了解更多关于 Flask-SQLAlchemy 的信息。
Flask-SQLAlchemy
SQLAlchemy 和 Flask-SQLAlchemy 扩展分别是数据库抽象和对象关系映射器。这是一种使用 Python 对象进行数据库的复杂方式。为了简化问题,我们将使用 SQLite 作为数据库,它是一个作为自包含 SQL 数据库的平面文件。我们将以 chapter9_db_1.py 的内容为例,展示如何使用 Flask-SQLAlchemy 创建网络数据库并在数据库中插入一些表条目。这是一个多步骤的过程,我们将在本节中查看这些步骤。
首先,我们将创建一个 Flask 应用程序并加载 SQLAlchemy 的配置,例如数据库路径和名称,然后通过传递应用程序来创建 SQLAlchemy 对象:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
# Create Flask application, load configuration, and create
# the SQLAlchemy object
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///network.db'
db = SQLAlchemy(app)
我们可以创建一个设备database对象及其相关的主键和各种列:
# This is the database model object
class Device(db.Model):
__tablename__ = 'devices'
id = db.Column(db.Integer, primary_key=True)
hostname = db.Column(db.String(120), index=True)
vendor = db.Column(db.String(40))
def __init__(self, hostname, vendor):
self.hostname = hostname
self.vendor = vendor
def __repr__(self):
return '<Device %r>' % self.hostname
我们可以调用database对象,创建条目,并将它们插入到数据库表中。请注意,我们添加到会话中的任何内容都需要提交到数据库才能永久保存:
if __name__ == '__main__':
db.create_all()
r1 = Device('lax-dc1-core1', 'Juniper')
r2 = Device('sfo-dc1-core1', 'Cisco')
db.session.add(r1)
db.session.add(r2)
db.session.commit()
我们将运行 Python 脚本并检查数据库文件的存在:
$ python chapter9_db_1.py
$ ls -l network.db
-rw-r--r-- 1 echou echou 28672 Sep 21 10:43 network.db
我们可以使用交互式提示来检查数据库表条目:
>>> from flask import Flask
>>> from flask_sqlalchemy import SQLAlchemy
>>> app = Flask(__name__)
>>> app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///network.db'
>>> db = SQLAlchemy(app)
>>> from chapter9_db_1 import Device
>>> Device.query.all()
[<Device 'lax-dc1-core1'>, <Device 'sfo-dc1-core1'>]
>>> Device.query.filter_by(hostname='sfo-dc1-core1')
<flask_sqlalchemy.BaseQuery object at 0x7f09544a0e80>
>>> Device.query.filter_by(hostname='sfo-dc1-core1').first()
<Device 'sfo-dc1-core1'>
我们也可以以相同的方式创建新的条目:
>>> r3 = Device('lax-dc1-core2', 'Juniper')
>>> db.session.add(r3)
>>> db.session.commit()
>>> Device.query.filter_by(hostname='lax-dc1-core2').first()
<Device 'lax-dc1-core2'>
让我们继续删除network.db文件,以免与使用相同db名称的其他示例冲突:
$ rm network.db
现在我们已经准备好继续构建我们的网络内容 API。
网络内容 API
在我们深入研究构建 API 的代码之前,让我们花点时间思考我们将创建的 API 结构。为 API 规划通常更多的是艺术而不是科学;它实际上取决于你的情况和偏好。我在本节中建议的,绝对不是唯一的方法,但现在,为了开始,请跟随我。
回想一下,在我们的图中,我们有四个 Cisco IOSv 设备。让我们假设其中两个,lax-edg-r1和lax-edg-r2,在网络中扮演脊的角色。其他两个设备,nyc-edg-r1和nyc-edg-r2,在我们的网络服务中作为叶子。这些是任意的选择,以后可以修改,但重点是我们要提供有关我们的网络设备的数据,并通过 API 公开它们。
为了简化问题,我们将创建两个 API,一个是设备组 API,另一个是单个设备 API:

图 9.4:网络内容 API
第一个 API 将是我们的http://192.168.2.126/devices/端点,它支持两种方法:GET和POST。GET请求将返回当前设备列表,而带有正确 JSON 主体的POST请求将创建设备。当然,你可以选择不同的端点进行创建和查询,但在这个设计中,我们选择通过 HTTP 方法来区分这两个操作。
第二个 API 将以http://192.168.2.126/devices/<device id>的形式针对我们的设备。使用GET请求的 API 将显示我们已输入数据库的设备的详细信息。
PUT请求将修改带有更新的条目。请注意,我们使用PUT而不是POST。这在 HTTP API 使用中很典型;当我们需要修改现有条目时,我们将使用PUT而不是POST。
到目前为止,你应该对我们的 API 有一个很好的了解。为了更好地可视化最终结果,我在我们查看代码之前,将快速展示结果。如果你想跟随示例,请随意启动chapter9_6.py作为 Flask 服务器。
向/devices/ API 发送POST请求将允许您创建一个条目。在这种情况下,我想创建我们的网络设备,具有诸如主机名、环回 IP、管理 IP、角色、厂商和它运行的操作系统等属性:
$ http POST http://192.168.2.126:5000/devices/ 'hostname'='lax-edg-r1' 'loopback'='192.168.0.10' 'mgmt_ip'='192.168.2.51' 'role'='spine' 'vendor'='Cisco' 'os'='15.8'
HTTP/1.1 201 CREATED
Connection: close
Content-Length: 3
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:01:33 GMT
Location: http://192.168.2.126:5000/devices/1
Server: Werkzeug/2.2.2 Python/3.10.4
{}
我可以为另外三个设备重复前面的步骤:
$ http POST http://192.168.2.126:5000/devices/ 'hostname'='lax-edg-r2' 'loopback'='192.168.0.11' 'mgmt_ip'='192.168.2.52' 'role'='spine' 'vendor'='Cisco' 'os'='15.8'
$ http POST http://192.168.2.126:5000/devices/ 'hostname'='nyc-edg-r1' 'loopback'='192.168.0.12' 'mgmt_ip'='192.168.2.61' 'role'='leaf'
'vendor'='Cisco' 'os'='15.8'
$ http POST http://192.168.2.126:5000/devices/ 'hostname'='nyc-edg-r2' 'loopback'='192.168.0.13' 'mgmt_ip'='192.168.2.62' 'role'='leaf' 'vendor'='Cisco' 'os'='15.8'
如果我们使用相同的 API 端点进行GET请求,我们将能够看到我们创建的网络设备列表:
$ http GET http://192.168.2.126:5000/devices/
HTTP/1.1 200 OK
Connection: close
Content-Length: 193
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:07:16 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
{
"device": [
"http://192.168.2.126:5000/devices/1",
"http://192.168.2.126:5000/devices/2",
"http://192.168.2.126:5000/devices/3",
"http://192.168.2.126:5000/devices/4"
]
}
类似地,使用GET请求对/devices/<id>的请求将返回与设备相关的特定信息:
$ http GET http://192.168.2.126:5000/devices/1
HTTP/1.1 200 OK
Connection: close
Content-Length: 199
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:07:50 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
{
"hostname": "lax-edg-r1",
"loopback": "192.168.0.10",
"mgmt_ip": "192.168.2.51",
"os": "15.8",
"role": "spine",
"self_url": "http://192.168.2.126:5000/devices/1",
"vendor": "Cisco"
}
让我们假设我们将lax-edg-r1操作系统从15.6降级到14.6。我们可以使用PUT请求来更新设备记录:
$ http PUT http://192.168.2.126:5000/devices/1 'hostname'='lax-edg-r1' 'loopback'='192.168.0.10' 'mgmt_ip'='192.168.2.51' 'role'='spine' 'vendor'='Cisco' 'os'='14.6'
HTTP/1.1 200 OK
# Verification
$ http GET http://192.168.2.126:5000/devices/1
HTTP/1.1 200 OK
Connection: close
Content-Length: 199
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:10:37 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
{
"hostname": "lax-edg-r1",
"loopback": "192.168.0.10",
"mgmt_ip": "192.168.2.51",
"os": "14.6",
"role": "spine",
"self_url": "http://192.168.2.126:5000/devices/1",
"vendor": "Cisco"
}
现在,让我们看看chapter9_6.py中创建前面 API 的代码。在我看来,酷的地方在于所有这些 API 都是在单个文件中完成的,包括数据库交互。稍后,当我们超出当前 API 的范围时,我们总是可以分离组件,例如为数据库类创建一个单独的文件。
设备 API
chapter9_6.py文件以必要的导入开始。请注意,以下请求导入的是客户端的request对象,而不是我们在前几章中使用的requests包:
from flask import Flask, url_for, jsonify, request
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///network.db'
db = SQLAlchemy(app)
我们声明了一个以id作为主键的database对象,并为hostname、loopback、mgmt_ip、role、vendor和os声明了字符串字段:
class Device(db.Model):
__tablename__ = 'devices'
id = db.Column(db.Integer, primary_key=True)
hostname = db.Column(db.String(64), unique=True)
loopback = db.Column(db.String(120), unique=True)
mgmt_ip = db.Column(db.String(120), unique=True)
role = db.Column(db.String(64))
vendor = db.Column(db.String(64))
os = db.Column(db.String(64))
Device类下的get_url()函数从url_for()函数返回一个 URL。请注意,在/devices/<int:id>路由下尚未定义被调用的get_device()函数:
def get_url(self):
return url_for('get_device', id=self.id, _external=True)
export_data()和import_data()函数是镜像的。一个用于在GET方法中使用时从数据库获取信息到用户(export_data()),另一个用于在POST或PUT方法中使用时从用户获取信息到数据库(import_data()):
def export_data(self):
return {
'self_url': self.get_url(),
'hostname': self.hostname,
'loopback': self.loopback,
'mgmt_ip': self.mgmt_ip,
'role': self.role,
'vendor': self.vendor,
'os': self.os
}
def import_data(self, data):
try:
self.hostname = data['hostname']
self.loopback = data['loopback']
self.mgmt_ip = data['mgmt_ip']
self.role = data['role']
self.vendor = data['vendor']
self.os = data['os']
except KeyError as e:
raise ValidationError('Invalid device: missing ' + e.args[0])
return self
在database对象就位并创建了导入和导出函数后,设备操作的路由派发就非常直接了。GET请求将通过查询devices表中的所有条目并返回每个条目的 URL 来返回设备列表。POST方法将使用import_data()函数和全局request对象作为输入。然后它将添加设备并将信息提交到数据库:
@app.route('/devices/', methods=['GET'])
def get_devices():
return jsonify({'device': [device.get_url()
for device in Device.query.all()]})
@app.route('/devices/', methods=['POST'])
def new_device():
device = Device()
device.import_data(request.json)
db.session.add(device)
db.session.commit()
return jsonify({}), 201, {'Location': device.get_url()}
如果您查看POST方法,返回的正文是一个空的 JSON 正文,状态码为201(已创建),以及额外的头信息:
HTTP/1.0 201 CREATED
Content-Length: 2
Content-Type: application/json Date: ...
Location: http://192.168.2.126:5000/devices/4
Server: Werkzeug/2.2.2 Python/3.10.4
让我们看看查询并返回单个设备信息的 API。
设备 ID API
单个设备的路由指定 ID 应该是一个整数,这可以作为我们防止不良请求的第一道防线。两个端点遵循与我们的/devices/端点相同的设计模式,其中我们使用相同的import和export函数:
@app.route('/devices/<int:id>', methods=['GET'])
def get_device(id):
return jsonify(Device.query.get_or_404(id).export_data())
@app.route('/devices/<int:id>', methods=['PUT'])
def edit_device(id):
device = Device.query.get_or_404(id)
device.import_data(request.json)
db.session.add(device)
db.session.commit()
return jsonify({})
注意,query_or_404()方法提供了一个方便的方法,当数据库查询对传入的 ID 返回负值时,返回404(未找到)。这是一种相当优雅的方式来快速检查数据库查询。
最后,代码的最后部分创建了数据库表并启动了 Flask 开发服务器:
if __name__ == '__main__':
db.create_all()
app.run(host='0.0.0.0', debug=True)
这是本书中较长的 Python 脚本之一,所以我们花了更多的时间来详细解释它。该脚本提供了一种方法,说明我们如何利用后端数据库来跟踪网络设备,并通过 Flask 将它们仅作为 API 暴露给外部世界。
在下一节中,我们将探讨如何使用 API 在单个设备或一组设备上执行异步任务。
网络动态操作
我们的 API 现在可以提供有关网络的静态信息;我们可以存储在数据库中的任何内容都可以返回给请求者。如果能直接与我们的网络交互就太好了,例如查询设备信息或将配置更改推送到设备。
我们将通过利用我们在第二章中已经看到的脚本开始这个过程,该脚本用于通过 Pexpect 与设备交互。我们将稍微修改该脚本,使其成为我们可以在chapter9_pexpect_1.py中重复使用的函数:
import pexpect
def show_version(device, prompt, ip, username, password):
device_prompt = prompt
child = pexpect.spawn('telnet ' + ip)
child.expect('Username:')
child.sendline(username)
child.expect('Password:')
child.sendline(password)
child.expect(device_prompt)
child.sendline('show version | i V')
child.expect(device_prompt)
result = child.before
child.sendline('exit')
return device, result
我们可以通过交互式提示测试新的功能:
>>> from chapter9_pexpect_1 import show_version
>>> print(show_version('lax-edg-r1', 'lax-edg-r1#', '192.168.2.51', 'cisco', 'cisco'))
('lax-edg-r1', b'show version | i V\r\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\r\nProcessor board ID 98U40DKV403INHIULHYHB\r\n')
在继续之前,确保我们的 Pexpect 脚本正常工作。以下代码假设我们已经从上一节中输入了必要的数据库信息。
我们可以在chapter9_7.py中添加一个新的 API 来查询设备版本:
from chapter9_pexpect_1 import show_version
<skip>
@app.route('/devices/<int:id>/version', methods=['GET'])
def get_device_version(id):
device = Device.query.get_or_404(id)
hostname = device.hostname
ip = device.mgmt_ip
prompt = hostname+"#"
result = show_version(hostname, prompt, ip, 'cisco', 'cisco')
return jsonify({"version": str(result)})
结果将返回给请求者:
$ http GET http://192.168.2.126:5000/devices/1/version
HTTP/1.1 200 OK
Connection: close
Content-Length: 216
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:19:52 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
{
"version": "('lax-edg-r1', b'show version | i V\\r\\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\\r\\nProcessor board ID 98U40DKV403INHIULHYHB\\r\\n')"
}
我们还可以添加另一个端点,允许我们根据设备的公共字段对多个设备执行批量操作。在下面的示例中,端点将使用 URL 中的device_role属性并将其与适当的设备(们)匹配:
@app.route('/devices/<device_role>/version', methods=['GET'])
def get_role_version(device_role):
device_id_list = [device.id for device in Device.query.all() if device.role == device_role]
result = {}
for id in device_id_list:
device = Device.query.get_or_404(id)
hostname = device.hostname
ip = device.mgmt_ip
prompt = hostname + "#"
device_result = show_version(hostname, prompt, ip, 'cisco', 'cisco')
result[hostname] = str(device_result)
return jsonify(result)
当然,像前面的代码那样通过Device.query.all()循环遍历所有设备并不高效。在生产中,我们将使用一个专门针对设备角色的 SQL 查询。
当我们使用 RESTful API 时,我们可以看到所有脊和叶设备可以同时查询:
$ http GET http://192.168.2.126:5000/devices/spine/version
HTTP/1.1 200 OK
Connection: close
Content-Length: 389
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:20:57 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
{
"lax-edg-r1": "('lax-edg-r1', b'show version | i V\\r\\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\\r\\nProcessor board ID 98U40DKV403INHIULHYHB\\r\\n')",
"lax-edg-r2": "('lax-edg-r2', b'show version | i V\\r\\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\\r\\n')"
}
如上图所示,新的 API 端点实时查询设备(们)并返回结果给请求者。当你可以保证在事务的超时值(默认为 30 秒)内收到操作响应,或者你愿意在操作完成之前 HTTP 会话超时时,这工作相对较好。处理超时问题的一种方法是将任务异步执行。我们将在下一节中探讨如何这样做。
异步操作
在我的看法中,异步操作,当执行超出正常时间序列的任务时,是 Flask 的一个高级主题。
幸运的是,Miguel Grinberg (blog.miguelgrinberg.com/),我对他的 Flask 工作非常钦佩,在他的博客和 GitHub 仓库中提供了许多文章和示例。对于异步操作,chapter9_8.py中的示例代码引用了 Miguel 在 GitHub 上的Raspberry Pi文件中的代码(github.com/miguelgrinberg/oreilly-flask-apis-video/blob/master/camera/camera.py)用于后台装饰器。我们将从导入几个额外的模块开始:
from flask import Flask, url_for, jsonify, request,\
make_response, copy_current_request_context
from flask_sqlalchemy import SQLAlchemy
from chapter9_pexpect_1 import show_version
import uuid
import functools
from threading import Thread
背景装饰器接收一个函数,并使用线程和 UUID 作为任务 ID 将其作为后台任务运行。它返回状态码202(已接受)和请求者检查的新资源位置。我们将为状态检查创建一个新的 URL:
@app.route('/status/<id>', methods=['GET'])
def get_task_status(id):
global background_tasks
rv = background_tasks.get(id)
if rv is None:
return not_found(None)
if isinstance(rv, Thread):
return jsonify({}), 202, {'Location': url_for('get_task_status', id=id)}
if app.config['AUTO_DELETE_BG_TASKS']:
del background_tasks[id]
return rv
一旦我们检索到资源,它就会被删除。这是通过在应用顶部设置app.config['AUTO_DELETE_BG_TASKS']为true来完成的。我们将添加此装饰器到我们的版本端点,而不会更改代码的其他部分,因为所有复杂性都隐藏在装饰器中(这有多酷?):
@app.route('/devices/<int:id>/version', methods=['GET'])
@background
def get_device_version(id):
device = Device.query.get_or_404(id)
<skip>
@app.route('/devices/<device_role>/version', methods=['GET'])
@background
def get_role_version(device_role):
device_id_list = [device.id for device in Device.query.all() if device.role == device_role]
<skip>
最终结果是两个步骤的过程。我们将对端点执行GET请求并接收位置头:
$ http GET http://192.168.2.126:5000/devices/spine/version
HTTP/1.1 202 ACCEPTED
Connection: close
Content-Length: 3
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:25:25 GMT
Location: /status/bb57f6cac4c64e0aa2e67415eb7cabd0
Server: Werkzeug/2.2.2 Python/3.10.4
{}
然后,我们可以向该位置发出第二个请求以检索结果:
$ http GET http://192.168.2.126:5000/status/bb57f6cac4c64e0aa2e67415eb7cabd0
HTTP/1.1 200 OK
Connection: close
Content-Length: 389
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:28:30 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
{
"lax-edg-r1": "('lax-edg-r1', b'show version | i V\\r\\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\\r\\nProcessor board ID 98U40DKV403INHIULHYHB\\r\\n')",
"lax-edg-r2": "('lax-edg-r2', b'show version | i V\\r\\nCisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)\\r\\n')"
}
为了验证当资源尚未准备好时返回的状态码202,我们将使用以下脚本chapter9_request_1.py立即对新资源发出请求:
import requests, time
server = 'http://192.168.2.126:5000'
endpoint = '/devices/1/version'
# First request to get the new resource
r = requests.get(server+endpoint)
resource = r.headers['location']
print("Status: {} Resource: {}".format(r.status_code, resource))
# Second request to get the resource status
r = requests.get(server+"/"+resource)
print("Immediate Status Query to Resource: " + str(r.status_code))
print("Sleep for 2 seconds")
time.sleep(2)
# Third request to get the resource status
r = requests.get(server+"/"+resource)
print("Status after 2 seconds: " + str(r.status_code))
如您在结果中看到的那样,状态码在资源仍在后台运行时返回为202:
$ python chapter9_request_1.py
Status: 202 Resource: /status/960b3a4a81d04b2cb7206d725464ef71
Immediate Status Query to Resource: 202
Sleep for 2 seconds
Status after 2 seconds: 200
我们的 API 进展顺利!由于我们的网络资源宝贵,我们应该仅允许授权人员访问 API。我们将在下一节中为我们的 API 添加基本的安全措施。
认证和授权
对于基本用户认证,我们将使用由 Miguel Grinberg 编写的 Flask 的httpauth (flask-httpauth.readthedocs.io/en/latest/) 扩展,以及 Werkzeug 中的密码函数。httpauth扩展应该在本书开头requirements.txt安装过程中已经安装。展示安全功能的新的文件名为chapter9_9.py。在脚本中,我们将从几个额外的模块导入开始:
from werkzeug.security import generate_password_hash, check_password_hash
from flask_httpauth import HTTPBasicAuth
我们将创建一个HTTPBasicAuth对象以及用户数据库对象。请注意,在用户创建过程中,我们将传递密码值;然而,我们只存储password_hash而不是明文password本身:
auth = HTTPBasicAuth()
<skip>
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True)
password_hash = db.Column(db.String(128))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
auth对象有一个verify_password装饰器,我们可以使用它,以及当用户请求开始时创建的 Flask 的g全局上下文对象。因为g是全局的,如果我们把用户保存到g变量中,它将贯穿整个事务:
@auth.verify_password
def verify_password(username, password):
g.user = User.query.filter_by(username=username).first()
if g.user is None:
return False
return g.user.verify_password(password)
有一个方便的before_request处理器可以在调用任何 API 端点之前使用。我们将结合auth.login_required装饰器与将应用于所有 API 路由的before_request处理器:
@app.before_request
@auth.login_required
def before_request():
pass
最后,我们将使用unauthorized错误处理器来返回一个response对象以处理401未授权错误:
@auth.error_handler
def unathorized():
response = jsonify({'status': 401, 'error': 'unauthorized',
'message': 'please authenticate'})
response.status_code = 401
return response
在我们能够测试用户认证之前,我们需要在我们的数据库中创建用户:
>>> from chapter9_9 import db, User
>>> db.create_all()
>>> u = User(username='eric')
>>> u.set_password('secret')
>>> db.session.add(u)
>>> db.session.commit()
>>> exit()
一旦你启动了 Flask 开发服务器,尝试发送一个请求,就像我们之前做的那样。你应该看到,这次,服务器将拒绝请求并返回一个401未授权错误:
$ http GET http://192.168.2.126:5000/devices/
HTTP/1.1 401 UNAUTHORIZED
Connection: close
Content-Length: 82
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:39:06 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
WWW-Authenticate: Basic realm="Authentication Required"
{
"error": "unahtorized",
"message": "please authenticate",
"status": 401
}
我们现在需要为我们的请求提供认证头:
$ http --auth eric:secret GET http://192.168.2.126:5000/devices/
HTTP/1.1 200 OK
Connection: close
Content-Length: 193
Content-Type: application/json
Date: Wed, 21 Sep 2022 18:39:42 GMT
Server: Werkzeug/2.2.2 Python/3.10.4
{
"device": [
"http://192.168.2.126:5000/devices/1",
"http://192.168.2.126:5000/devices/2",
"http://192.168.2.126:5000/devices/3",
"http://192.168.2.126:5000/devices/4"
]
}
我们现在为我们的网络设置了一个不错的 RESTful API。当我们的用户想要检索网络设备信息时,他们可以查询网络的静态内容。他们还可以对单个设备或一组设备执行网络操作。我们还添加了基本的安全措施,以确保只有我们创建的用户才能从我们的 API 中检索信息。酷的地方在于,这一切都是在单个文件中完成的,代码行数不到 250 行(如果你减去注释,不到 200 行)!
关于用户会话管理、登录、登出和记住用户会话的更多信息,我强烈推荐使用 Flask-Login (flask-login.readthedocs.io/en/latest/) 扩展。
我们现在已经将底层供应商 API 从我们的网络中抽象出来,并用我们的 RESTful API 替换了它们。通过提供抽象,我们可以自由地使用后端所需的内容,例如 Pexpect,同时为我们的请求者提供一个统一的接口。我们甚至可以更进一步,替换底层的网络设备,而不会影响向我们发出 API 调用的人。Flask 以紧凑且易于使用的方式为我们提供了这种抽象。我们还可以使用容器等较小的脚本来运行 Flask。
在容器中运行 Flask
在过去几年中,容器变得非常流行。它们提供了比基于虚拟机管理程序的虚拟机更多的抽象和虚拟化。对于感兴趣的读者,我们将提供一个简单的例子,说明我们如何在 Docker 容器中运行我们的 Flask 应用。
我们将基于免费的 DigitalOcean Docker 教程构建我们的示例,该教程介绍了如何在 Ubuntu 20.04 机器上构建容器(www.digitalocean.com/community/tutorials/how-to-build-and-deploy-a-flask-application-using-docker-on-ubuntu-20-04)。如果你是容器的新手,我强烈建议你完成那个教程,然后再回到这一部分。
让我们确保 Docker 已安装:
$ sudo docker –version
Docker version 20.10.18, build b40c2f6
我们将创建一个名为TestApp的目录来存放我们的代码:
$ mkdir TestApp
$ cd TestApp/
在目录中,我们将创建另一个名为app的目录并创建__init__.py文件:
$ mkdir app
$ touch app/__init__.py
在app目录下是我们将包含应用程序逻辑的地方。由于我们到目前为止一直在使用单文件应用程序,我们可以简单地将chapter9_6.py文件的 内容复制到app/__init__.py文件中:
$ cat app/__init__.py
from flask import Flask, url_for, jsonify, request
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///network.db'
db = SQLAlchemy(app)
@app.route('/')
def home():
return "Hello Python Networking!"
<skip>
class Device(db.Model):
__tablename__ = 'devices'
id = db.Column(db.Integer, primary_key=True)
hostname = db.Column(db.String(64), unique=True)
loopback = db.Column(db.String(120), unique=True)
mgmt_ip = db.Column(db.String(120), unique=True)
role = db.Column(db.String(64))
vendor = db.Column(db.String(64))
os = db.Column(db.String(64))
<skip>
我们还可以将我们创建的 SQLite 数据库文件复制到这个目录中:
$ tree app/
app/
├── __init__.py
├── network.db
我们将requirements.txt文件放在TestApp目录中:
$ cat requirements.txt
Flask==1.1.1
Flask-HTTPAuth==3.3.0
Flask-SQLAlchemy==2.4.1
Jinja2==2.10.1
MarkupSafe==1.1.1
Pygments==2.4.2
SQLAlchemy==1.3.9
Werkzeug==0.16.0
httpie==1.0.3
itsdangerous==1.1.0
python-dateutil==2.8.0
requests==2.20.1
由于与tiangolo/uwsgi-nginx-flask镜像和一些 Flask 包的后续版本的冲突,此需求文件正在回滚到 Flask 1.1.1。我们工作的代码部分在 1.1.1 版本和最新的 Flask 版本中都能正常工作。
我们将创建main.py文件作为我们的入口点,以及一个ini文件用于uwsgi:
$ cat main.py
from app import app
$ cat uwsgi.ini
[uwsgi]
module = main
callable = app
master = true
我们将使用预制的 Docker 镜像并创建一个Dockerfile来构建 Docker 镜像:
$ cat Dockerfile
FROM tiangolo/uwsgi-nginx-flask:python3.7-alpine3.7
RUN apk --update add bash vim
RUN mkdir /TestApp
ENV STATIC_URL /static
ENV STATIC_PATH /TestApp/static
COPY ./requirements.txt /TestApp/requirements.txt
RUN pip install -r /TestApp/requirements.txt
我们的start.sh外壳脚本将构建镜像,作为守护进程在后台运行,然后将端口8000转发到 Docker 容器:
$ cat start.sh
#!/bin/bash
app="docker.test"
docker build -t ${app} .
docker run -d -p 8000:80 \
--name=${app} \
-v $PWD:/app ${app}
我们现在可以使用start.sh脚本来构建镜像并启动我们的容器:
$ sudo bash start.sh
Sending build context to Docker daemon 48.13kB
Step 1/7 : FROM tiangolo/uwsgi-nginx-flask:python3.8
python3.8: Pulling from tiangolo/uwsgi-nginx-flask
85bed84afb9a: Pulling fs layer
5fdd409f4b2b: Pulling fs layer
<skip>
我们现在的 Flask 运行在容器中,可以从我们的主机机端口8000查看:
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
25c83da6082c docker.test "/entrypoint.sh /sta…" 2 minutes ago Up 2 minutes 443/tcp, 0.0.0.0:8000->80/tcp, :::8000->80/tcp docker.test
我们可以看到管理主机 IP如下显示在地址栏中:

图 9.5:管理主机 IP 转发
我们可以看到Flask API 端点如下所示:

图 9.6:API 端点
一旦完成,我们可以使用以下命令停止和删除容器:
$ sudo docker stop <container id>
$ sudo docker rm <containter id>
我们还可以删除 Docker 镜像:
$ sudo docker images -a -q #find the image id
$ sudo docker rmi <image id>
如我们所见,在容器中运行 Flask 给我们带来了更多的灵活性,以及将我们的 API 抽象部署到生产中的选项。当然,容器提供了它们的复杂性,并增加了更多的管理任务,因此在我们选择部署方法时,我们需要权衡利弊。我们接近本章的结尾,所以让我们看看我们到目前为止所做的工作,然后再展望下一章。
摘要
在本章中,我们开始转向为我们的网络构建 RESTful API 的道路。我们研究了流行的 Python 网络框架,即 Django 和 Flask,并对两者进行了比较和对比。通过选择 Flask,我们可以从小处着手,并通过使用 Flask 扩展来扩展功能。
在我们的实验室中,我们使用虚拟环境来将 Flask 安装基础与我们的全局站点包分开。我们的实验室网络由几个 IOSv 节点组成,其中两个被指定为脊路由器,而另外两个为叶子路由器。我们游览了 Flask 的基础知识,并使用简单的HTTPie客户端来测试我们的 API 设置。
在 Flask 的不同配置中,我们特别强调了 URL 分发以及 URL 变量,因为它们是请求者与我们的 API 系统之间的初始逻辑。我们研究了使用 Flask-SQLAlchemy 和 SQLite 来存储和返回本质上是静态的网络元素。对于操作任务,我们在调用其他程序(如 Pexpect)以完成配置任务的同时,也创建了 API 端点。我们通过添加异步处理和用户认证到我们的 API 来改进了设置。我们还探讨了如何在 Docker 容器中运行我们的 Flask API 应用程序。
在第十章“异步 IO 简介”中,我们将转换节奏,探讨 Python 3 中较新的功能之一——异步 IO,以及它是如何应用于网络工程的。
加入我们的图书社区
要加入这本书的社区——在这里您可以分享反馈、向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第十章:异步 IO 简介
在前面的章节中,我们通过 API 或其他 Python 库直接与网络设备交互,这些库抽象了我们对远程设备的低级交互。当我们需要与多个设备交互时,我们使用循环来允许我们实用地执行命令。我们可能会开始看到的一个问题是,当我们需要与许多设备交互时,端到端过程开始变慢。瓶颈通常是我们在发送命令直到从远程设备收到适当响应之间等待的时间。如果我们每个操作需要 5 秒钟的等待时间,当我们需要操作 30 个设备时,我们可能需要等待几分钟。
这部分是正确的,因为我们的操作是顺序的。我们一次只操作一个设备,按顺序进行。如果我们能够同时处理多个设备呢?这样会加快速度,对吧?是的,你是对的。但是,告诉我们的 Python 脚本“同时”去处理多个设备并不像听起来那么简单。我们必须考虑计算机调度任务的方式、语言限制以及手头的可用工具。
在本章中,我们将讨论 Async IO,这是一个 Python 包,允许我们同时执行多个任务。我们还将讨论相关主题,如多进程、并行性、线程等。Python 中的异步操作是一个我认为中等到高级水平的话题。async IO 模块本身是在 Python 3.4 中引入的。它也在 Python 3.4 到 Python 3.7 之间经历了快速的变化。不管怎样,它对于网络自动化来说是一个非常相关的主题。我相信,对于任何希望熟悉网络自动化的网络工程师来说,这都是一个值得研究的话题。
在本章中,我们将讨论以下与 Async IO 相关的话题:
-
异步操作概述
-
多进程和线程
-
Python asyncio 模块
-
Scrapli项目
关于 Python 相关异步操作的信息,Real Python(realpython.com/search?q=asyncio)和 Python 文档(docs.python.org/3/library/asyncio.html)都提供了良好的学习资源。
让我们从异步操作概述开始看起。
异步操作概述
在《Python 之禅》中;我们知道 Python 的一个指导原则是最好有“做某事的一种最佳方式。”当涉及到异步操作时,这有点复杂。我们知道如果我们能够同时执行多个任务会很有帮助,但确定正确的解决方案可能并不直接。
首先,我们需要确定是什么减慢了我们的程序。通常,瓶颈可以是 CPU 密集型或 I/O 密集型。在 CPU 密集型情况下,程序将 CPU 推到极限。例如,解决数学问题或图像处理等操作是 CPU 密集型程序的例子。例如,当我们为 VPN 选择加密算法时,我们知道算法越复杂,CPU 消耗就越多。对于 CPU 密集型任务,减轻瓶颈的方法是增加 CPU 功率或允许任务同时使用多个 CPU。
在 I/O 密集型操作中,程序的大部分时间都花在等待从已完成的输入中获取一些输出上。当我们向设备发起 API 调用时,我们必须等到收到所需的答案才能进行下一步。如果时间很重要,那么这将是本可以用于做其他事情的时间。减轻 I/O 密集型任务的方法是同时处理多个任务。
如果当前的工作受 CPU 功率或输入输出延迟的限制,我们可以尝试同时运行多个操作。这被称为并行化。当然,并不是所有任务都可以并行化。正如伟大的沃伦·巴菲特所说:“你不能通过让九个女人怀孕一个月来生一个孩子。”然而,如果你的任务可以并行化,我们有一些并行处理选项:multiprocessing、threading 或新的 asyncio 模块。
Python multiprocessing
Python 的 multiprocessing 允许将 CPU 密集型任务分解成子任务,并创建子进程来处理它们。这对于 CPU 密集型任务非常合适,因为它允许多个 CPU 同时工作。如果我们回顾一下计算历史,我们会注意到大约在 2005 年,单个 CPU 的运行速度就不再提高了。由于干扰和散热问题,我们无法在单个 CPU 上放置更多的晶体管。我们获得更多计算能力的方法是使用多核 CPU。这使我们能够将任务分散到多核 CPU 上。
在 Python 的 multiprocessing 模块中,通过创建一个Process对象并调用其start()方法来创建进程。让我们看一个简单的例子,multiprocess_1.py:
#!/usr/bin/env python3
# Modified from
# https://docs.python.org/3/library/multiprocessing.html
from multiprocessing import Process
import os
def process_info():
print('process id:', os.getpid())
def worker(number):
print(f'Worker number {number}')
process_info()
if __name__ == '__main__':
for i in range(5):
p = Process(target=worker, args=(i,))
p.start()
在这个例子中,我们有一个工作函数,它调用另一个process_info()函数来获取进程 ID。然后我们启动 Process 对象五次,每次都针对工作函数。执行输出如下:
(venv) $ python multiprocess_1.py
Worker number 0
process id: 109737
Worker number 2
process id: 109739
Worker number 3
process id: 109740
Worker number 1
process id: 109738
Worker number 4
process id: 109741
如我们所见,每个进程都有自己的进程和进程 ID。Multiprocessing 非常适合 CPU 密集型任务。如果工作是 I/O 密集型,在 asyncio 模块出现之前,我们最好的选择是使用 threading 模块。
Python multithreading
如我们所知,Python 有一个全局解释器锁,或称为GIL。Python 解释器(确切地说是 CPython)使用它来确保一次只有一个线程执行 Python 字节码。这主要是为了防止内存泄漏中的竞态条件而采取的安全措施。但它可能会成为 IO 密集型任务的性能瓶颈。
想要了解更多信息,请查看realpython.com/python-gil/上的文章。
允许多个线程运行的一种方法是通过使用threading模块。它允许程序并发运行多个操作。我们可以在threading_1.py中看到一个简单的例子:
#!/usr/bin/env python3
# Modified from https://pymotw.com/3/threading/index.html
import threading
# Get thread ID
def thread_id():
print('thread id:', threading.get_ident())
# Worker function
def worker(number):
print(f'Worker number {number}')
thread_id()
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
该脚本与我们的多进程示例类似,只是显示的是线程 ID 而不是进程 ID。脚本执行的输出如下:
(venv) $ python threading_1.py
Worker number 0
thread id: 140170712495680
Worker number 1
thread id: 140170704102976
Worker number 2
thread id: 140170695710272
Worker number 3
thread id: 140170704102976
Worker number 4
thread id: 140170695710272
线程模块是缓解 Python GIL 的多个线程的好选择。然而,当 Python 将任务传递给线程时,主进程在线程过程中可见性有限。线程处理起来更困难,尤其是在协调不同线程和处理可能出现的错误时。对于 IO 密集型任务,Python 3 中的 asyncio 是另一个不错的选择。

图 10.1:CPU 密集型与 IO 密集型 Python 模块
让我们更深入地了解 asyncio 模块。
Python asyncio 模块
我们可以将 asyncio 模块视为 Python 允许我们编写并发运行任务的代码的方式。它使用了新引入的async和await关键字。它可以帮助我们提高许多可能受 IO 限制的操作的性能,例如 Web 服务器、数据库,当然还有通过网络与设备通信。asyncio 模块是 FastAPI 等流行新框架的基础(fastapi.tiangolo.com/)。
然而,重要的是要指出,asyncio 既不是多进程也不是多线程。它被设计为单线程单进程。Python asyncio 使用协作多进程来提供并发的感觉。
与线程不同,Python 从始至终控制着整个过程,而不是将线程过程传递给操作系统。这使得 Python 知道任务何时开始和完成,从而在进程之间进行协调。当我们“暂停”部分代码等待结果时,Python 会在返回“暂停”代码之前先继续执行其他代码部分。
在编写我们的 asyncio 代码之前,这是一个需要掌握的重要概念。我们需要决定代码的哪一部分可以被暂停,以便 Python 可以暂时从它那里移开。我们必须告诉 Python,“嘿,我只是在等待某事。去做其他事情,然后回来检查我。”
让我们从asyncio_1.py中 asyncio 模块语法的简单示例开始:
#!/usr/bin/env python3
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
await asyncio.sleep(2)
print('... and again.')
asyncio.run(main())
当我们执行它时,这里是这个输出:
$ python asyncio_1.py
Hello ...
... World!
... and again.
在这个例子中,我们可以注意以下几点:
-
asyncio 模块是 Python 3.10 的标准库。
-
函数前使用了
async关键字。在 asyncio 中,这被称为协程。 -
await关键字正在等待某些操作的返回。 -
我们不是简单地调用函数/协程,而是使用
asyncio.run()来实现。
asyncio 模块的核心是协程,使用 async 关键字定义。协程是 Python 生成器函数的特化版本,可以在等待时暂时将控制权交还给 Python 解释器。
生成器函数是一种可以像列表一样迭代的函数类型,但在加载内容到内存之前这样做。这在例如数据集非常大,可能会压倒计算机内存的情况下很有用。更多信息,请参阅此文档:wiki.python.org/moin/Generators。

图 10.2:带有 async 和 await 的协程
让我们进一步探讨这个例子,看看我们如何在此基础上构建。以下示例来自 RealPython.com 的优秀教程(realpython.com/async-io-python/#the-asyncio-package-and-asyncawait)。我们将从一个同步的计数函数 sync_count.py 开始:
#!/usr/bin/env python3
# Modified from https://realpython.com/async-io-python/#the-asyncio-package-and-asyncawait countsync.py example
import time
def count():
print("One")
time.sleep(1)
print("Two")
def main():
count()
count()
count()
if __name__ == "__main__":
s = time.perf_counter()
main()
elapsed = time.perf_counter() - s
print(f"Completed in {elapsed:0.2f} seconds.")
执行后,我们可以看到脚本通过忠实地三次顺序执行函数,在 3 秒内执行完成:
(venv) $ python sync_count.py
One
Two
One
Two
One
Two
Completed in 3.00 seconds.
现在,让我们看看我们是否可以构建一个异步版本,async_count.py:
#!/usr/bin/env python3
# example from https://realpython.com/async-io-python/#the-asyncio-package-and-asyncawait countasync.py
import asyncio
async def count():
print("One")
await asyncio.sleep(1)
print("Two")
async def main():
await asyncio.gather(count(), count(), count())
if __name__ == "__main__":
import time
s = time.perf_counter()
asyncio.run(main())
elapsed = time.perf_counter() - s
print(f"Completed in {elapsed:0.2f} seconds.")
当我们执行这个文件时,我们会看到类似的任务在 1/3 的时间内就完成了:
(venv) $ python async_count.py
One
One
One
Two
Two
Two
Completed in 1.00 seconds.
为什么会这样?这是因为现在当我们计数并遇到 sleep 暂停时,我们会将控制权交还给解释器,允许它处理其他任务。

图 10.3:事件循环
在这个例子中有几个重要的点需要注意:
-
sleep()函数被改成了asyncio.sleep()函数。它是一个可等待的函数。 -
count()和main()函数现在都是协程。 -
我们使用了
asyncio.gather()来收集所有的协程。 -
asyncio.run()是一个循环,它会一直运行直到所有任务都完成。
从示例中,我们可以看到我们需要对常规函数进行一些修改,以允许 asyncio 提供的性能提升。记得我们讨论过协作式多进程吗?Asyncio 需要 Python 程序中的所有组件共同工作以实现这一目标。

图 10.4:事件循环
在下一节中,我们将探讨 Scrapli 项目,该项目通过利用 Python 3 asyncio 功能来加速网络设备交互过程。
Scrapli 项目
Scrapli 是一个开源网络库 (github.com/carlmontanari/scrapli),它使用 Python 3 的 asyncio 功能来帮助更快地连接到网络设备。它是由 Carl Montanari (github.com/carlmontanari) 在他的网络自动化项目中创建的。安装过程很简单:
(venv) $ pip install scrapli
(venv) $ mkdir scrapli && cd scrapli
让我们开始使用 Scrapli 进行我们的网络设备通信。
Scrapli 示例
我们可以使用以下示例,scrapli_example_1.py,在我们的实验室 NX-OS 设备 lax-cor-r1 上执行 show 命令:
# Modified from https://github.com/carlmontanari/scrapli
from scrapli import Scrapli
device = {
"host": "192.168.2.50",
"auth_username": "cisco",
"auth_password": "cisco",
"auth_strict_key": False,
"ssh_config_file": True,
"platform": "cisco_nxos",
}
conn = Scrapli(**device)
conn.open()
response = conn.send_command("show version")
print(response.result)
执行脚本将给出 show version 输出。注意这是字符串格式:
(venv) $ python scrapli_example_1.py
Cisco Nexus Operating System (NX-OS) Software
TAC support: http://www.cisco.com/tac
…
Software
loader: version N/A
kickstart: version 7.3(0)D1(1)
system: version 7.3(0)D1(1)
Hardware
cisco NX-Osv Chassis ("NX-Osv Supervisor Module")
IntelI CITM) i5-7260U C with 3064740 kB of memory.
Processor Board ID TM000940CCB
Device name: lax-cor-r1
bootflash: 3184776 kB
…
从表面上看,它可能看起来与其他一些我们见过的库没有太大区别。但在底层,核心驱动程序和相关平台正在使用可以转换为 awaitable 协程的 asyncio 模块:

图 10.5:Scrapli 核心驱动程序(来源:https://carlmontanari.github.io/scrapli/user_guide/basic_usage/)
我们可以通过访问项目的 GitHub 页面来验证代码,github.com/carlmontanari/scrapli。NXOS 异步驱动程序,github.com/carlmontanari/scrapli/blob/main/scrapli/driver/core/cisco_nxos/async_driver.py,可以追溯到基础异步驱动程序,github.com/carlmontanari/scrapli/blob/main/scrapli/driver/base/async_driver.py,以及基础驱动程序,github.com/carlmontanari/scrapli/blob/main/scrapli/driver/base/base_driver.py。这是开源项目的美丽之处之一,我们有自由去探索和构建在彼此知识的基础上。感谢,Carl!
核心驱动程序包括 Cisco IOS-XE、Cisco NX-OS、Cisco IOS-XR、Arista EOS 和 Juniper JunOS。通过简单地指定平台,Scrapli 能够将其与特定的驱动程序相关联。还有一个 scrapli_community 项目 (github.com/scrapli/scrapli_community),它扩展了核心驱动程序的功能。
在我们的实验室中,我们指定了额外的 ssh 配置。因此,我们需要将 ssh_config_file 设置为 true:
$ cat ~/.ssh/config
…
Host 192.168.2.50
HostKeyAlgorithms +ssh-rsa
KexAlgorithms +diffie-hellman-group-exchange-sha1
Scrapli 的文档,carlmontanari.github.io/scrapli/,是一个很好的起点。Packet Coders,www.packetcoders.io/,也提供了良好的网络自动化课程,包括 Scrapli。
现在,我们可以将这个 awaitable 任务放入 asyncio 运行循环中。
Scrapli 异步示例
在这个例子中,我们将更精确地讨论驱动器和传输。我们将从 Scrapli 安装asyncssh插件(carlmontanari.github.io/scrapli/api_docs/transport/plugins/asyncssh/)以供使用:
(venv) $ pip install scrapli[asyncssh]
下面列出了脚本scraplie_example_2.py:
#!/usr/bin/env python3
# Modified from
# https://github.com/carlmontanari/scrapli/blob/main/examples/async_usage/async_multiple_connections.py
import asyncio
from scrapli.driver.core import AsyncNXOSDriver
async def gather_cor_device_version(ip, username, password):
device = {
"host": ip,
"auth_username": username,
"auth_password": password,
"auth_strict_key": False,
"ssh_config_file": True,
"transport": "asyncssh",
"driver": AsyncNXOSDriver
}
driver = device.pop("driver")
conn = driver(**device)
await conn.open()
response = await conn.send_command("show version")
await conn.close()
return response
async def main():
results = await asyncio.gather(
gather_cor_device_version('192.168.2.50', 'cisco', 'cisco'),
gather_cor_device_version('192.168.2.60', 'cisco', 'cisco')
)
for result in results:
print(result.result)
if __name__ == "__main__":
import time
s = time.perf_counter()
asyncio.run(main())
elapsed = time.perf_counter() - s
print(f"Completed in {elapsed:0.2f} seconds.")
该脚本创建了两个新的协程,一个用于收集设备信息,另一个用于在main()函数中收集协程任务。我们还创建了一个asyncio.run()循环,当脚本独立执行时运行main()函数。让我们执行这个脚本:
(venv) $ python scrapli_example_2_async.py
Cisco Nexus Operating System (NX-OS) Software
…
loader: version N/A
kickstart: version 7.3(0)D1(1)
system: version 7.3(0)D1(1)
…
Device name: lax-cor-r1
bootflash: 3184776 kB
…
Device name: nyc-cor-r1
bootflash: 3184776 kB
…
Completed in 1.37 seconds.
除了两个设备输出的show version信息外,我们还看到执行完成仅用了 1 秒多一点的时间。
让我们比较同步和异步操作的性能差异。Scrapli 为同步操作提供了一个GenericDriver。在示例脚本scrapli_example_3_sync.py中,我们将使用GenericDriver来反复收集信息。仅为了说明目的,该脚本连接到每个设备三次:
#!/usr/bin/env python3
# Modified from
# https://github.com/carlmontanari/scrapli/blob/main/examples/async_usage/async_multiple_connections.py
import asyncio
# from scrapli.driver.core import Paramiko
from scrapli.driver import GenericDriver
def gather_cor_device_version(ip, username, password):
device = {
"host": ip,
"auth_username": username,
"auth_password": password,
"auth_strict_key": False,
"ssh_config_file": True,
"driver": GenericDriver
}
driver = device.pop("driver")
conn = driver(**device)
conn.open()
response = conn.send_command("show version")
conn.close()
return response
def main():
results = []
for device in [
'192.168.2.50',
'192.168.2.60',
'192.168.2.50',
'192.168.2.60',
'192.168.2.50',
'192.168.2.60',
'192.168.2.50',
'192.168.2.60',
]:
results.append(gather_cor_device_version(device, 'cisco', 'cisco'))
return results
if __name__ == "__main__":
import time
s = time.perf_counter()
main()
elapsed = time.perf_counter() - s
print(f"Completed in {elapsed:0.2f} seconds.")
此外,还有一个可比较的异步版本,scrapli_example_3_async.py。当我们运行这两个脚本时,这里显示了性能差异:
(venv) $ python scrapli_example_3_sync.py
Completed in 5.97 seconds.
(venv) $ python scrapli_example_3_async.py
Completed in 4.67 seconds.
这可能看起来改进不大,但随着我们扩大运营规模,性能提升将变得更加显著。
摘要
在本章中,我们学习了异步处理的概念。我们简要介绍了 CPU 密集型和 I/O 密集型任务背后的概念。我们之前通过多进程和多线程解决了由它们引起的瓶颈。
从 Python 3.4 开始,引入了新的 asyncio 模块来解决 I/O 密集型任务。它与多线程类似,但使用了一种特殊的协作多任务设计。它们使用特殊的关键字——async关键字用于创建特殊类型的 Python 生成器函数,await关键字用于指定可以暂时“暂停”的任务。asyncio 模块可以收集这些任务并在循环中运行,直到完成。
在本章的后半部分,我们学习了使用 Scrapli,这是一个由 Carl Montanari 为网络工程社区创建的项目。它旨在利用 Python 3 中的 asyncio 特性进行网络设备管理。
Asyncio 并不容易。异步、await、loop 和生成器等新术语可能会让人感到压倒性。从 Python 3.4 到 3.7 版本,asyncio 模块也经历了快速的发展,使得一些在线文档已经过时。希望本章提供的信息能帮助我们理解这个有用的特性。
在下一章中,我们将转向云计算及其周围的网络特性。
加入我们的书籍社区
要加入我们这本书的社区——在那里你可以分享反馈,向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第十一章:AWS 云网络
云计算是当今计算领域的主要趋势,并且已经持续了多年。公共云提供商已经改变了创业行业以及从头开始推出服务的含义。我们不再需要构建自己的基础设施;我们可以支付公共云提供商的费用,租用他们资源的一部分来满足我们的基础设施需求。如今,在任何一个技术会议或聚会中,我们很难找到没有了解过、使用过或基于云构建过服务的人。云计算已经到来,我们最好习惯与它一起工作。
云计算有几种服务模型,大致可以分为 软件即服务(SaaS — en.wikipedia.org/wiki/Software_as_a_service)、平台即服务(PaaS — en.wikipedia.org/wiki/Cloud_computing#Platform_as_a_service_(PaaS)) 和 基础设施即服务(IaaS — en.wikipedia.org/wiki/Infrastructure_as_a_service)。每种服务模型从用户的角度提供了不同层次抽象。对我们来说,网络是 IaaS 提供的一部分,也是本章的重点。
亚马逊网络服务(AWS — aws.amazon.com/) 是首家提供 IaaS 公共云服务的公司,并在 2022 年凭借市场份额成为该领域的明显领导者 (www.statista.com/chart/18819/worldwide-market-share-of-leading-cloud-infrastructure-service-providers/)。如果我们把 软件定义网络(SDN)定义为一系列软件服务协同工作以创建网络结构——IP 地址、访问列表、负载均衡器和 网络地址转换(NAT)——那么我们可以论证 AWS 是世界上最大的 SDN 实施者。他们利用其全球网络的巨大规模、数据中心和服务器,提供了一系列令人惊叹的网络服务。
如果你对了解亚马逊的规模和网络感兴趣,我强烈推荐你看看 James Hamilton 在 2014 年 AWS re:Invent 的演讲:www.youtube.com/watch?v=JIQETrFC_SQ。这是对 AWS 规模和创新的一次罕见的内部视角。
在本章中,我们将讨论 AWS 云服务提供的网络服务以及我们如何使用 Python 与它们一起工作:
-
AWS 设置和网络概述
-
虚拟私有云
-
直接连接和 VPN
-
网络扩展服务
-
其他 AWS 网络服务
让我们从如何设置 AWS 开始。
AWS 设置
如果你还没有 AWS 账户并且想跟随这些示例,请登录到aws.amazon.com/并注册。这个过程相当直接;你需要一张信用卡和一些方式来验证你的身份,例如可以接收短信的手机。
当你刚开始使用 AWS 时,一个好处是他们提供许多免费层级的服务(aws.amazon.com/free/),在那里你可以免费使用服务,直到达到一定水平。例如,在本章中我们将使用弹性计算云(EC2)服务;EC2 的免费层是每月前 750 小时,对于前 12 个月的 t2.micro 或 t3.micro 实例。
我建议始终从免费层开始,并在需要时逐步提高你的层级。请检查 AWS 网站以获取最新的服务:

图 11.1:AWS 免费层
一旦你有账户,你就可以通过 AWS 控制台(console.aws.amazon.com/)登录并查看 AWS 提供的服务。
AWS 控制台的布局不断变化。当你阅读这一章时,你的屏幕可能看起来与显示的不同。然而,AWS 网络概念不会改变。我们应该始终关注概念,并且即使有任何布局变化,我们也应该没问题。
控制台是我们配置所有服务和查看每月账单的地方:

图 11.2:AWS 控制台
现在我们已经设置了账户,让我们看看如何使用 AWS CLI 工具以及 Python SDK 来管理我们的 AWS 资源。
AWS CLI 和 Python SDK
除了控制台,我们还可以通过命令行界面(CLI)和各种 SDK 来管理 AWS 服务。AWS CLI 是一个 Python 包,可以通过 PIP 安装(docs.aws.amazon.com/cli/latest/userguide/installing.html)。让我们在我们的 Ubuntu 主机上安装它:
$ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
$ unzip awscliv2.zip
$ sudo ./aws/install
$ which aws
/usr/local/bin/aws
$ aws --version
aws-cli/2.7.34 Python/3.9.11 Linux/5.15.0-47-generic exe/x86_64.ubuntu.22 prompt/off
一旦安装了 AWS CLI,为了更方便和安全地访问,我们将创建一个用户并使用用户凭证配置 AWS CLI。让我们回到 AWS 控制台并选择身份和访问管理(IAM)进行用户和访问管理:

图 11.3:AWS IAM
我们可以在左侧面板上选择用户来创建用户:

图 11.4:AWS IAM 用户
选择程序访问并将用户分配到默认管理员组:

图 11.5:AWS IAM 添加用户
下一步将把用户添加到组中;现在我们可以把用户添加到管理员组。我们不需要为这个用户添加任何标签。最后一步将显示访问密钥 ID和秘密访问密钥。将它们复制到文本文件中,并保存在安全的地方:

图 11.6:AWS IAM 用户安全凭证
我们将通过终端中的aws configure完成 AWS CLI 身份验证凭证设置。我们将在下一节中介绍 AWS 区域。我们现在使用us-east-1,因为这是服务最多的区域。我们总是可以稍后返回设置来更改区域:
$ aws configure
AWS Access Key ID [None]: <key>
AWS Secret Access Key [None]: <secret>
Default region name [None]: us-east-1
Default output format [None]: json
我们还将安装 AWS Python SDK,Boto3([boto3.readthedocs.io/en/latest/](https://boto3.readthedocs.io/en/latest/)):
(venv) $ pip install boto3
(venv) $ python
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import boto3
>>> boto3.__version__
'1.24.78'
>>> exit()
现在我们已经准备好进入后续部分,从 AWS 云网络服务介绍开始。
AWS 网络概述
当我们讨论 AWS 服务时,我们需要从顶部开始,即区域和可用性 区(AZs)。它们对我们所有服务都有重大影响。在撰写本书时,AWS 在全球范围内列出了 27 个地理区域和 87 个AZs。用 AWS 全球云基础设施的话说([aws.amazon.com/about-aws/global-infrastructure/](https://aws.amazon.com/about-aws/global-infrastructure/)):
“AWS 云基础设施是围绕区域和可用区(AZs)构建的。AWS 区域提供多个、物理上分离且独立的可用区,这些可用区通过低延迟、高吞吐量和高度冗余的网络连接。”
要查看可以按 AZ、区域等过滤的 AWS 区域的好可视化,请查看https://aws.amazon.com/about-aws/global-infrastructure/regions_az/。
AWS 提供的一些服务是全球性的(例如我们创建的 IAM 用户),但大多数服务是基于区域的。区域是地理足迹,例如美国东部、美国西部、欧洲伦敦、亚太东京等。对我们来说,这意味着我们应该在我们目标用户最接近的区域构建我们的基础设施。这将减少我们客户服务的延迟。如果我们的用户位于美国东海岸,如果服务是基于区域的,我们应该选择美国东部(北弗吉尼亚)或美国东部(俄亥俄)作为我们的区域:

图 11.7:AWS 区域
除了用户延迟外,AWS 区域还有服务和成本影响。对于刚开始使用 AWS 的新用户来说,可能会感到惊讶,并不是所有服务都在所有区域提供。我们将在本章中查看的服务在大多数区域提供,但一些较新的服务可能只在选定区域提供。
在下面的示例中,我们可以看到Alexa for Business和Amazon Chime仅在美国弗吉尼亚北部区域提供:

图 11.8:按区域划分的 AWS 服务
除了服务可用性外,不同区域之间的服务成本可能略有不同。例如,在本章中我们将要查看的 EC2 服务,a1.medium实例在US East (N. Virginia)的价格为每小时USD 0.0255;相同的实例在EU (Frankfurt)的价格为每小时USD 0.0291,高出 14%:

图 11.9:AWS EC2 美国东部价格

图 11.10:AWS EC2 欧洲价格
如果有疑问,请选择美国东部(弗吉尼亚北部);它是最古老的区域,可能是最便宜的,提供的服务种类最多。
并非所有区域对所有用户都可用。例如,GovCloud和中国区域默认情况下不对美国用户开放。您可以通过aws ec2 describe-regions列出您可用的区域:
$ aws ec2 describe-regions
{
"Regions":
{
"Endpoint": "ec2.eu-north-1.amazonaws.com",
"RegionName": "eu-north-1",
"OptInStatus": "opt-in-not-required"
},
{
"Endpoint": "ec2.ap-south-1.amazonaws.com",
"RegionName": "ap-south-1",
"OptInStatus": "opt-in-not-required"
},
<skip>
如亚马逊所述,所有区域之间完全独立。因此,大多数资源不会在区域之间复制。这意味着如果我们有多个区域提供相同的服务,例如US-East和US-West,并且需要相互备份服务,我们将需要自行复制必要的资源。
我们可以在 AWS 控制台中,右上角通过下拉菜单选择我们想要的区域:

图 11.12:AWS 区域和可用区
与区域不同,我们在 AWS 中构建的许多资源可以自动跨可用区复制。例如,我们可以配置我们的托管关系数据库(Amazon RDS)跨可用区进行复制。可用区的概念在服务冗余方面非常重要,其限制对我们将要构建的网络服务也很重要。
AWS 独立地将 AZ 映射到每个账户的标识符。例如,我的 AZ,us-east-1a,可能不同于另一个账户的 us-east-1a,尽管它们都被标记为 us-east-1a。
我们可以在 AWS CLI 中检查区域中的 AZ:
$ aws ec2 describe-availability-zones --region us-east-1
{
"AvailabilityZones": [
{
"State": "available",
"Messages": [],
"RegionName": "us-east-1",
"ZoneName": "us-east-1a",
"ZoneId": "use1-az2"
},
{
"State": "available",
"Messages": [],
"RegionName": "us-east-1",
"ZoneName": "us-east-1b",
"ZoneId": "use1-az4"
},
<skip>
我们为什么如此关注区域和 AZ?正如我们将在接下来的几节中看到的那样,AWS 网络服务通常受区域和 AZ 的限制。例如,虚拟专用云(VPC)必须完全位于一个区域中,每个子网也必须完全位于一个 AZ 中。另一方面,NAT 网关是 AZ 绑定的,因此如果我们需要冗余,我们需要为每个 AZ 创建一个。
我们将更详细地介绍这两个服务,但它们的使用案例在此作为区域和 AZ 是 AWS 网络服务提供基础的示例:

图 11.13:每个区域的 VPC 和 AZ
AWS 边缘位置是截至 2022 年 5 月在 48 个国家的 90 多个城市中 AWS CloudFront 内容分发网络的一部分 (aws.amazon.com/cloudfront/features/)。这些边缘位置用于向客户以低延迟分发内容。边缘节点比亚马逊为区域和 AZ 构建的全数据中心占地面积小。有时,人们会将边缘位置的实体点误认为是完整的 AWS 区域。如果占地面积被列为边缘位置,则 AWS 服务(如 EC2 或 S3)将不会提供。我们将在 AWS CloudFront CDN 服务部分重新讨论边缘位置。
AWS 转接中心是 AWS 网络中最少被记录的部分之一。它们在 James Hamilton 2014 年的 AWS re:Invent 大会演讲中被提及,作为区域中不同 AZ 的聚合点(www.youtube.com/watch?v=JIQETrFC_SQ)。为了公平起见,我们不知道在这么多年后,转接中心是否仍然存在并以相同的方式运行。然而,我们可以对转接中心的位置及其与 AWS Direct Connect 服务的相关性做出合理的猜测,我们将在本章后面讨论这一点。
James Hamilton,AWS 的副总裁和杰出工程师,是 AWS 最有影响力的技术专家之一。如果说到 AWS 网络方面,我会认为他是最有权威性的。你可以在他的博客 Perspectives([perspectives.mvdirona.com/](https://perspectives.mvdirona.com/))上了解更多关于他的想法。
在一个章节中涵盖所有与 AWS 相关的服务是不可能的。有一些与网络不直接相关的相关服务我们没有空间涵盖,但我们应该熟悉:
-
IAM 服务,https://aws.amazon.com/iam/,是一种使我们能够安全地管理对 AWS 服务和资源的访问的服务。
-
亚马逊资源名称(ARNs),
docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html,在 AWS 的所有资源中唯一标识 AWS 资源。当我们需要识别一个需要访问我们的 VPC 资源的服务,如 DynamoDB 和 API Gateway 时,这些资源名称非常重要。 -
亚马逊 弹性计算云(EC2),
aws.amazon.com/ec2/,是一种服务,使我们能够通过 AWS 接口获取和配置计算能力,例如 Linux 和 Windows 实例。在本章的示例中,我们将使用 EC2 实例。
为了学习的目的,我们将排除 AWS GovCloud (US) 和中国区域,这两个区域都不使用 AWS 全球基础设施,并且每个区域都有其独特的功能和限制。
这是对 AWS 网络服务的一个相对较长的介绍,但非常重要。这些概念和术语将在后续章节中提到。在接下来的部分,我们将探讨 AWS 网络中最重要的概念(在我看来):VPC。
虚拟专用网络
亚马逊 VPC(docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html)使客户能够在为客户的账户专设的虚拟网络中启动 AWS 资源。这是一个真正可定制的网络,允许您定义您的 IP 地址范围,添加和删除子网,创建路由,添加 VPN 网关,关联安全策略,将 EC2 实例连接到您自己的数据中心,等等。
在 VPC 不可用的时候,早期所有位于一个可用区(AZ)的 EC2 实例都在一个单一、扁平的网络中,该网络由所有客户共享。客户将信息放在云中的舒适度会如何?我想不会很高。在 2007 年 EC2 发布和 2009 年 VPC 发布之间,VPC 功能是 AWS 最受请求的功能之一。
在 VPC 中离开您的 EC2 主机的数据包会被虚拟机管理程序拦截。虚拟机管理程序将检查数据包与理解您的 VPC 结构的映射服务。然后,数据包将被封装成真实 AWS 服务器源地址和目标地址。封装和映射服务使得 VPC 具有灵活性,但也带来了一些限制(多播、嗅探)。毕竟,这是一个虚拟网络。
自 2013 年 12 月以来,所有 EC2 实例都是 VPC 独有的;您不能再创建一个非 VPC(EC2-Classic)的 EC2 实例,而且您也不希望这样做。如果我们使用启动向导来创建我们的 EC2 实例,它将自动放入一个默认 VPC,并带有用于公共访问的虚拟互联网网关。在我看来,只有最基本的用例应该使用默认 VPC。在大多数情况下,我们应该定义自己的非默认、定制的 VPC。
让我们在 us-east-1 的 AWS 控制台中创建以下 VPC:

图 11.14:我们在 US-East-1 的第一个 VPC
如果你还记得,VPC 是 AWS 区域绑定的,子网是基于可用区(AZ)的。我们的第一个 VPC 将位于 us-east-1;三个子网将被分配到 us-east-1a 和 us-east-1b 的两个不同的 AZ 中。
使用 AWS 控制台创建 VPC 和子网非常简单,AWS 在线提供了几个很好的教程。我已经在 VPC 仪表板上列出了步骤及其相关位置:

图 11.15:创建 VPC、子网和其他功能的步骤
前两个步骤是点击操作,大多数网络工程师都可以完成,即使没有先前的经验。默认情况下,VPC 只包含本地路由,10.0.0.0/16。现在,我们将创建一个互联网网关并将其与 VPC 关联:

图 11.16:AWS 互联网网关到 VPC 分配
然后,我们可以创建一个自定义路由表,默认路由指向互联网网关,允许互联网访问。我们将此路由表与我们的 us-east-1a 子网 10.0.0.0/24 关联,从而使 VPC 能够访问互联网:

图 11.17:路由表
让我们使用 Boto3 Python SDK 来查看我们创建了什么;我使用了 mastering_python_networking_demo 作为 VPC 的标签,我们可以用它作为过滤器:
#!/usr/bin/env python3
import json, boto3
region = 'us-east-1'
vpc_name = 'mastering_python_networking_demo'
ec2 = boto3.resource('ec2', region_name=region)
client = boto3.client('ec2')
filters = [{'Name':'tag:Name', 'Values':[vpc_name]}]
vpcs = list(ec2.vpcs.filter(Filters=filters))
for vpc in vpcs:
response = client.describe_vpcs(
VpcIds=[vpc.id,]
)
print(json.dumps(response, sort_keys=True, indent=4))
此脚本将允许我们查询我们创建的 VPC 所在的区域:
(venv) $ python Chapter11_1_query_vpc.py
{
" ResponseMetadata " : {
<skip>
" HTTPStatusCode " : 200,
" RequestId " : " 9416b03f-<skip> " ,
" RetryAttempts " : 0
},
" Vpcs " : [
{
" CidrBlock " : " 10.0.0.0/16 ",
" CidrBlockAssociationSet " : [
{
" AssociationId " : " vpc-cidr-assoc-<skip> ",
"CidrBlock": "10.0.0.0/16",
"CidrBlockState": {
"State": "associated"
}
}
],
"DhcpOptionsId": "dopt-<skip>",
"InstanceTenancy": "default",
"IsDefault": false,
"OwnerId": "<skip>",
"State": "available",
"Tags": [
{
"Key": "Name",
"Value": "mastering_python_networking_demo"
}
],
"VpcId": "vpc-<skip>"
}
]
}
Boto3 VPC API 文档可以在 boto3.readthedocs.io/en/latest/reference/services/ec2.html#vpc 找到。
如果我们创建了 EC2 实例并将它们放在不同的子网中,主机将能够跨子网相互访问。你可能想知道,由于我们只在子网 1a 中创建了一个互联网网关,子网之间是如何在 VPC 内相互连接的。在物理网络中,网络需要连接到路由器才能超出其本地网络。
在 VPC 中,这并没有太大的不同,除了它是一个隐式路由器,默认路由表是本地网络,在我们的例子中是 10.0.0.0/16。这个隐式路由器是在我们创建 VPC 时创建的。任何未与自定义路由表关联的子网都关联到主表。
路由表和路由目标
路由是网络工程中最重要的话题之一。值得更仔细地看看 AWS VPC 中是如何实现的。我们已经看到,当我们创建 VPC 时,我们有一个隐式路由器和主要的路由表。在上一个例子中,我们创建了一个互联网网关,一个自定义路由表,默认路由指向互联网网关,并使用路由目标关联了自定义路由表到一个子网。
到目前为止,只有路由目标的概念在 VPC 中与传统网络略有不同。我们可以将路由目标大致等同于传统路由中的下一跳。
总结:
-
每个 VPC 都有一个隐式路由器
-
每个 VPC 都有一个包含本地路由的主要路由表
-
您可以创建自定义路由表
-
每个子网可以遵循自定义路由表或默认的主要路由表
-
路由表路由目标可以是互联网网关、NAT 网关、VPC 对等连接等
我们可以使用 Boto3 在 Chapter11_2_query_route_tables.py 中查看自定义路由表和子网的关联:
#!/usr/bin/env python3
import json, boto3
region = 'us-east-1'
vpc_name = 'mastering_python_networking_demo'
ec2 = boto3.resource('ec2', region_name=region)
client = boto3.client('ec2')
response = client.describe_route_tables()
print(json.dumps(response['RouteTables'][0], sort_keys=True, indent=4))
主要路由表是隐式的,并且 API 不会返回它。由于我们只有一个自定义路由表,这就是我们将看到的内容:
(venv) $ python Chapter11_2_query_route_tables.py
{
" Associations " : [
<skip>
],
" OwnerId " : " <skip> ",
" PropagatingVgws " : [],
" RouteTableId " : " rtb-<skip> ",
" Routes " : [
{
"DestinationCidrBlock": "10.0.0.0/16",
"GatewayId": "local",
"Origin": "CreateRouteTable",
"State": "active"
},
{
"DestinationCidrBlock": "0.0.0.0/0",
"GatewayId": "igw-041f287c",
"Origin": "CreateRoute",
"State": "active"
}
],
"Tags": [
{
"Key": "Name",
"Value": "public_internet_gateway"
}
],
"VpcId": "vpc-<skip>"
}
我们已经创建了第一个公共子网。我们将按照相同的步骤创建另外两个私有子网,us-east-1b 和 us-east-1c。结果将会有三个子网:一个位于 us-east-1a 的 10.0.0.0/24 公共子网,以及分别位于 us-east-1b 和 us-east-1c 的 10.0.1.0/24 和 10.0.2.0/24 私有子网。
现在,我们有一个包含三个子网的工作 VPC:一个公共子网和两个私有子网。到目前为止,我们已经使用 AWS CLI 和 Boto3 库与 AWS VPC 进行交互。让我们看看 AWS 的另一个自动化工具,CloudFormation。
使用 CloudFormation 自动化
AWS CloudFormation (aws.amazon.com/cloudformation/) 是我们可以使用文本文件来描述和启动所需资源的一种方式。我们可以使用 CloudFormation 在 us-west-1 区域中部署另一个 VPC:

图 11.18:us-west-1 的 VPC
CloudFormation 模板可以是 YAML 或 JSON;我们将使用 YAML 作为我们第一个用于部署的模板 Chapter10_3_cloud_formation.yml:
AWSTemplateFormatVersion: '2010-09-09'
Description: Create VPC in us-west-1
Resources:
myVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: '10.1.0.0/16'
EnableDnsSupport: 'false'
EnableDnsHostnames: 'false'
Tags:
- Key: Name
- Value: 'mastering_python_networking_demo_2'
我们可以通过 AWS CLI 执行模板。请注意,我们在执行时指定了 us-west-1 区域:
(venv) $ aws --region us-west-1 cloudformation create-stack --stack-name 'mpn-ch10-demo' --template-body file://Chapter11_3_cloud_formation.yml
{
"StackId": "arn:aws:cloudformation:us-west-1:<skip>:stack/mpn-ch10- demo/<skip>"
}
我们可以通过 AWS CLI 验证状态:
(venv) $ aws --region us-west-1 cloudformation describe-stacks --stack-name mpn-ch10-demo
{
"Stacks": [
{
"StackId": "arn:aws:cloudformation:us-west-1:<skip>:stack/mpn-ch10-demo/bbf5abf0-8aba-11e8-911f-500cadc9fefe",
"StackName": "mpn-ch10-demo",
"Description": "Create VPC in us-west-1",
"CreationTime": "2018-07-18T18:45:25.690Z",
"LastUpdatedTime": "2018-07-18T19:09:59.779Z",
"RollbackConfiguration": {},
"StackStatus": "UPDATE_ROLLBACK_COMPLETE",
"DisableRollback": false,
"NotificationARNs": [],
"Tags": [],
"EnableTerminationProtection": false,
"DriftInformation": {
"StackDriftStatus": "NOT_CHECKED"
}
}
]
}
最后创建的 CloudFormation 模板创建了一个没有子网的网络。让我们删除该 VPC,并使用以下模板 Chapter11_4_cloud_formation_full.yml 来创建 VPC 和子网。请注意,在创建 VPC 之前,我们不会有 VPC-ID,因此我们将使用一个特殊变量在子网创建中引用 VPC-ID。同样的技术也可以用于其他资源,例如路由表和互联网网关:
AWSTemplateFormatVersion: '2010-09-09'
Description: Create subnet in us-west-1
Resources:
myVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: '10.1.0.0/16'
EnableDnsSupport: 'false'
EnableDnsHostnames: 'false'
Tags:
- Key: Name
Value: 'mastering_python_networking_demo_2'
mySubnet:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref myVPC
CidrBlock: '10.1.0.0/24'
AvailabilityZone: 'us-west-1a'
Tags:
- Key: Name
Value: 'mpn_demo_subnet_1'
我们可以按照以下方式执行和验证资源的创建:
(venv) $ aws --region us-west-1 cloudformation create-stack --stack-name mpn-ch10-demo-2 --template-body file://Chapter11_4_cloud_formation_full.yml
{
"StackId": "arn:aws:cloudformation:us-west-1:<skip>:stack/mpn-ch10- demo-2/<skip>"
}
$ aws --region us-west-1 cloudformation describe-stacks --stack-name mpn- ch10-demo-2
{
"Stacks": [
{
"StackStatus": "CREATE_COMPLETE",
...
"StackName": "mpn-ch10-demo-2", "DisableRollback": false
}
]
}
我们可以从 AWS 控制台中验证 VPC 和子网信息。请记住从右上角的下拉菜单中选择正确的区域:

图 11.19:us-west-1 中的 VPC
我们还可以查看子网:

图 11.20:us-west-1 中的子网
我们现在在美国的两个海岸都有两个 VPC。它们目前就像两个孤岛,各自独立。这可能或可能不是您期望的操作状态。如果我们想将两个 VPC 连接起来,我们可以使用 VPC 对等连接(docs.aws.amazon.com/AmazonVPC/latest/PeeringGuide/vpc-peering-basics.html)来允许直接通信。
有一些 VPC 对等连接的限制,例如不允许重叠的 IPv4 或 IPv6 CIDR 块。还有跨区域 VPC 对等连接的额外限制。请确保您查阅了文档。
VPC 对等连接不仅限于同一账户。您可以将不同账户的 VPC 连接起来,只要请求被接受,并且其他方面(安全、路由和 DNS 名称)得到妥善处理。
在接下来的部分中,我们将查看 VPC 安全组和网络 访问控制列表(ACL)。
安全组和网络 ACL
AWS 安全组 和 网络 ACL 可在您的 VPC 的 安全 部分找到:

图 11.21:VPC 安全
安全组是一种有状态的虚拟防火墙,它控制对资源的入站和出站访问。大多数时候,我们使用安全组来限制对我们的 EC2 实例的公共访问。当前每个 VPC 的限制是 500 个安全组。每个安全组可以包含最多 50 条入站规则和 50 条出站规则。
您可以使用以下示例脚本 Chapter11_5_security_group.py 来创建安全组和两个简单的入站规则:
#!/usr/bin/env python3
import boto3
ec2 = boto3.client('ec2')
response = ec2.describe_vpcs()
vpc_id = response.get('Vpcs', [{}])[0].get('VpcId', '')
# Query for security group id
response = ec2.create_security_group(GroupName='mpn_security_group',
Description='mpn_demo_sg',
VpcId=vpc_id)
security_group_id = response['GroupId']
data = ec2.authorize_security_group_ingress(
GroupId=security_group_id,
IpPermissions=[
{'IpProtocol': 'tcp',
'FromPort': 80,
'ToPort': 80,
'IpRanges': [{'CidrIp': '0.0.0.0/0'}]},
{'IpProtocol': 'tcp',
'FromPort': 22,
'ToPort': 22,
'IpRanges': [{'CidrIp': '0.0.0.0/0'}]}
])
print('Ingress Successfully Set %s' % data)
# Describe security group
#response = ec2.describe_security_groups(GroupIds=[security_group_id])
print(security_group_id)
我们可以执行脚本并收到创建安全组的确认,该安全组可以与其他 AWS 资源相关联:
(venv) $ python Chapter11_5_security_group.py
Ingress Successfully Set {'ResponseMetadata': {'RequestId': '<skip>', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'AmazonEC2', 'content- type': 'text/xml;charset=UTF-8', 'date': 'Wed, 18 Jul 2018 20:51:55 GMT',
'content-length': '259'}, 'RetryAttempts': 0}} sg-<skip>
网络 ACL 是一个无状态的额外安全层。VPC 中的每个子网都与一个网络 ACL 相关联。由于 ACL 是无状态的,您需要指定入站和出站规则。
安全组和 ACL 之间的重要区别如下:
-
安全组在网络接口级别运行,而 ACL 在子网级别运行。
-
对于安全组,我们只能指定
允许规则,而不能指定拒绝规则,而 ACL 支持同时使用允许和拒绝规则。 -
安全组是有状态的,因此返回流量会自动允许;ACL 中的返回流量必须被明确允许。
让我们来看看 AWS 网络中最酷的功能之一:弹性 IP。当我最初了解到弹性 IP 时,我对它们动态分配和重新分配 IP 地址的能力感到震惊。
弹性 IP
弹性 IP(EIP)是一种使用可从互联网访问的公共 IPv4 地址的方式。
截至 2022 年底,EIP 不支持 IPv6。
EIP 可以动态分配给 EC2 实例、网络接口或其他资源。EIP 的一些特性如下:
-
弹性 IP 地址(EIP)与账户相关联,并且具有区域特定性。例如,
us-east-1区域的 EIP 只能关联到us-east-1区域的资源。 -
您可以将 EIP 从资源中解关联,并将其重新关联到不同的资源。这种灵活性有时可以用来确保高可用性。例如,您可以通过将相同的 IP 地址从较小的 EC2 实例重新分配到较大的 EC2 实例来迁移。
-
EIP 与之相关联的是一小笔每小时费用。
您可以从门户请求 EIP。分配后,您可以将其关联到所需的资源:

图 11.22:弹性 IP 地址
不幸的是,EIP 在每个区域中限制为五个,以防止浪费(docs.aws.amazon.com/vpc/latest/userguide/amazon-vpc-limits.html)。然而,如果需要,可以通过 AWS 支持的工单来增加这个数量。
在下一节中,我们将探讨如何使用 NAT 网关允许私有子网与互联网进行通信。
NAT 网关
为了允许我们的 EC2 公共子网中的主机从互联网访问,我们可以分配一个 EIP 并将其关联到 EC2 主机的网络接口。然而,在撰写本文时,每个 EC2-VPC 限制为五个弹性 IP(docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Appendix_Limits.html#vpc-limits-eips)。有时,当需要时,允许私有子网中的主机进行出站访问,而不创建 EIP 和 EC2 主机之间永久的一对一映射,会很好。
NAT 网关可以通过执行 NAT 允许私有子网中的主机临时进行出站访问,从而提供帮助。此操作类似于我们通常在企业防火墙中执行的端口地址转换(PAT)。要使用 NAT 网关,我们可以执行以下步骤:
-
在可以通过互联网网关访问的子网中创建 NAT 网关,使用 AWS CLI、Boto3 库或 AWS 控制台。NAT 网关需要分配一个 EIP。
-
将私有子网中的默认路由指向 NAT 网关。
-
NAT 网关将遵循默认路由到互联网网关以进行外部访问。
这个操作可以用以下图表来表示:

图 11.23:NAT 网关操作
关于 NAT 网关最常见的几个问题通常涉及 NAT 网关应该位于哪个子网。一般来说,要记住 NAT 网关需要公共访问。因此,它应该创建在具有公共互联网访问的子网中,并分配给它一个可用的 EIP:

图 11.24:NAT 网关创建
请记住删除您未使用的任何 AWS 服务,以避免产生费用。
在下一节中,我们将探讨如何将我们光鲜的 AWS 虚拟网络连接到我们的物理网络。
直接连接和 VPN
到目前为止,我们的 VPC 一直是一个位于 AWS 网络中的独立网络。它既灵活又实用,但为了访问 VPC 内的资源,我们需要通过其面向互联网的服务(如 SSH 和 HTTPS)来访问它们。
在本节中,我们将探讨 AWS 允许我们从我们的私有网络连接到 VPC 的方式:IPSec VPN 网关和直接连接。
VPN 网关
将我们的本地网络连接到 VPC 的第一种方式是使用传统的 IPSec VPN 连接。我们需要一个公开可访问的设备来建立与 AWS VPN 设备的 VPN 连接。
客户端网关需要支持基于路由的 IPSec VPN,其中 VPN 连接被视为路由协议和正常用户流量可以穿越的连接。目前,AWS 建议使用边界网关协议(BGP)来交换路由。
在 VPC 方面,我们可以遵循类似的路由表,将特定的子网路由到虚拟私有网关(VPG)目标:

图 11.25:VPC VPN 连接
除了 IPSec VPN 之外,我们还可以使用专用电路进行连接,这被称为直接连接。
直接连接
我们之前提到的 IPSec VPN 连接是提供本地设备到 AWS 云资源连接的一种简单方式。然而,它也面临着互联网上 IPSec 始终存在的相同问题:它不可靠,我们对它的可靠性几乎没有控制。在连接达到我们可以控制的互联网部分之前,几乎没有性能监控,也没有服务级别协议(SLA)。
由于所有这些原因,任何生产级、关键任务流量更有可能通过 Amazon 提供的第二个选项进行传输,即 AWS Direct Connect。AWS Direct Connect 允许客户通过专用虚拟电路将他们的数据中心和托管环境连接到他们的 AWS VPC。
这个操作中相对困难的部分通常是把我们自己的网络带到可以与 AWS 物理连接的地方,通常是在一个运营商酒店。
您可以在此处找到 AWS Direct Connect 的位置列表:aws.amazon.com/directconnect/details/。Direct Connect 链路只是一个可以订购的光纤连接,您可以从特定的运营商酒店订购,将网络连接到网络端口,并配置 dot1q 干线的连接性。
通过第三方运营商使用 多协议标签交换(MPLS)电路和聚合链路,Direct Connect 的连接选项也越来越多。我发现并使用的一个最经济实惠的选项是 Equinix Cloud Exchange Fabric (www.equinix.com/services/interconnection-connectivity/cloud-exchange/)。通过使用 Equinix Cloud Exchange Fabric,我们可以利用相同的电路,以专用电路成本的一小部分连接到不同的云服务提供商:

图 11.26:Equinix Cloud Exchange Fabric
在接下来的部分,我们将探讨 AWS 提供的一些网络扩展服务。
网络扩展服务
AWS 提供的许多网络服务没有直接的网络影响,例如 DNS 和内容分发网络。由于它们与网络和应用程序性能的紧密关系,它们在我们的讨论中是相关的。
弹性负载均衡
弹性负载均衡(ELB)允许来自互联网的入站流量自动分配到多个 EC2 实例。就像物理世界中的负载均衡器一样,这使我们能够拥有更好的冗余和容错能力,同时减少每台服务器的负载。ELB 有两种类型:应用负载均衡和网络负载均衡。
网络负载均衡器通过 HTTP 和 HTTPS 处理网络流量;应用负载均衡器在 TCP 层面上运行。如果你的应用程序运行在 HTTP 或 HTTPS 上,通常选择应用负载均衡器是个好主意。否则,使用网络负载均衡器是个不错的选择。
可以在 aws.amazon.com/elasticloadbalancing/details/ 找到应用和网络负载均衡的详细对比:

图 11.27:ELB 对比
ELB 提供了一种在流量进入我们区域内的资源后进行负载均衡的方法。AWS Route 53 DNS 服务允许区域之间的地理负载均衡,有时也称为全局服务器负载均衡。
Route 53 DNS 服务
我们都知道域名服务是什么——Route 53 是 AWS 的 DNS 服务。Route 53 是一家全面服务的域名注册商,您可以直接从 AWS 购买和管理域名。关于网络服务,DNS 允许使用服务域名在区域之间进行轮询负载均衡,以实现地理区域的负载均衡。
在我们可以使用 DNS 进行负载均衡之前,我们需要以下项目:
-
在每个预期的负载均衡区域都有一个负载均衡器
-
一个已注册的域名。我们不需要 Route 53 作为域名注册商
-
Route 53 是域的 DNS 服务
然后,我们可以在两个弹性负载均衡器之间的活动-活动环境中使用基于 Route 53 延迟的路由策略和健康检查。在下一节中,我们将重点关注 AWS 构建的内容分发网络,称为 CloudFront。
CloudFront CDN 服务
CloudFront 是亚马逊的内容分发网络(CDN),通过在物理上更靠近客户的地方提供服务来减少内容交付的延迟。内容可以是静态网页内容、视频、应用程序、API,或者最近,Lambda 函数。CloudFront 边缘位置包括现有的 AWS 区域以及全球许多其他位置。CloudFront 的高级操作如下:
-
用户访问您的网站是为了一个或多个目标。
-
DNS 将请求路由到用户请求最近的亚马逊 CloudFront 边缘位置。
-
CloudFront 边缘位置将直接通过缓存提供服务或从源请求对象。
AWS CloudFront 和 CDN 服务通常由应用开发者或 DevOps 工程师处理。然而,了解它们的操作总是好的。
其他 AWS 网络服务
这里还有许多其他 AWS 网络服务,我们没有足够的空间来介绍。本节中列出了其中一些更流行的服务:
-
AWS Transit VPC (
aws.amazon.com/blogs/aws/aws-solution-transit-vpc/): 这是一种将多个 VPC 连接到充当中转中心的公共 VPC 的方法。这是一个相对较新的服务,但它可以最小化您需要设置和管理的连接数量。这也可以作为在单独的 AWS 账户之间共享资源时的工具。 -
Amazon GuardDuty (
aws.amazon.com/guardduty/): 这是一个托管威胁检测服务,持续监控恶意或未经授权的行为,以帮助保护我们的 AWS 工作负载。它监控 API 调用或可能未经授权的部署。 -
AWS WAF (
aws.amazon.com/waf/): 这是一个帮助保护 Web 应用免受常见攻击的 Web 应用防火墙。我们可以定义自定义的 Web 安全规则来允许或阻止 Web 流量。 -
AWS Shield (
aws.amazon.com/shield/): 这是一个托管分布式拒绝服务(DDoS)保护服务,保护在 AWS 上运行的应用程序。基本级别的保护服务对所有客户都是免费的;AWS Shield 的高级版本是收费服务。
持续不断地有新的令人兴奋的 AWS 网络服务被宣布,例如我们在本节中探讨的那些。并非所有这些服务都是基础服务,例如 VPC 或 NAT 网关;然而,它们在各自的领域中都发挥着有用的作用。
摘要
在本章中,我们探讨了 AWS 云网络服务。我们回顾了 AWS 网络定义中的区域、可用区、边缘位置和转接中心。了解 AWS 整体网络有助于我们了解其他 AWS 网络服务的一些限制和约束。在本章中,我们使用了 AWS CLI、Python Boto3 库和 CloudFormation 来自动化一些任务。
我们深入探讨了 AWS VPC,包括路由表和路由目标的配置。关于安全组和网络 ACL 的示例确保了我们的 VPC 的安全性。我们还探讨了 EIP 和 NAT 网关以允许外部访问。
将 AWS VPC 连接到本地网络有两种方式:Direct Connect 和 IPSec VPN。我们简要地讨论了每种方式及其优势。在本章的末尾,我们探讨了 AWS 提供的网络扩展服务,包括 ELB、Route 53 DNS 和 CloudFront。
在下一章中,我们将探讨另一家公共云提供商 Microsoft Azure 提供的网络服务。
加入我们的书籍社区
要加入我们的书籍社区——在那里您可以分享反馈、向作者提问并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第十二章:Azure 云网络
正如我们在第十一章“AWS 云网络”中看到的,基于云的网络帮助我们连接我们组织的基于云的资源。一个虚拟 网络(VNet)可以用来分割和保障我们的虚拟机。它还可以将我们的本地资源连接到云中。作为这一领域的先驱,AWS 通常被视为市场领导者,拥有最大的市场份额。在本章中,我们将探讨另一个重要的公共云提供商,Microsoft Azure,重点关注他们的基于云的网络产品。
Microsoft Azure 最初于 2008 年以“Project Red Dog”的代号启动,并于 2010 年 2 月 1 日公开发布。当时,它被称为“Windows Azure”,后来在 2014 年更名为“Microsoft Azure”。自从 AWS 在 2006 年发布了其第一个产品 S3 以来,它实际上领先于 Microsoft Azure 6 年。对于拥有 Microsoft 庞大资源的公司来说,试图赶上 AWS 并非易事。同时,Microsoft 凭借其多年的成功产品和与企业客户群的关系,拥有独特的竞争优势。
由于 Azure 专注于利用现有的 Microsoft 产品和服务以及客户关系,因此 Azure 云网络有一些重要的含义。例如,客户建立与 Azure 的 ExpressRoute 连接(他们的 AWS Direct Connect 等效服务)的主要驱动因素之一可能是 Office 365 的更好体验。另一个例子可能是客户已经与 Microsoft 签订了服务级别协议,该协议可以扩展到 Azure。
在本章中,我们将讨论 Azure 提供的网络服务以及如何使用 Python 与之交互。由于我们在上一章中介绍了一些云网络概念,我们将借鉴那些经验,在适用的情况下比较 AWS 和 Azure 的网络。
具体来说,我们将讨论以下内容:
-
Azure 的设置和网络概述。
-
Azure 虚拟网络(形式为 VNets)。Azure VNet 类似于 AWS VPC,它为用户提供了一个在 Azure 云中的私有网络。
-
ExpressRoute 和 VPN。
-
Azure 网络负载均衡器。
-
其他 Azure 网络服务。
我们在上一个章节中已经学习了众多重要的云网络概念。现在,让我们利用这些知识,首先比较一下 Azure 和 AWS 提供的服务。
Azure 和 AWS 网络服务比较
当 Azure 推出时,他们更专注于 软件即服务(SaaS)和 平台即服务(PaaS),对 基础设施即服务(IaaS)的关注较少。对于 SaaS 和 PaaS,底层网络服务通常被抽象化,远离用户。例如,Office 365 的 SaaS 提供通常作为一个可以通过公共互联网访问的远程托管端点提供。使用 Azure App Service 构建网络应用程序的 PaaS 提供通常通过完全管理的流程完成,通过流行的框架如 .NET 或 Node.js。
相反,IaaS 提供的服务需要我们在 Azure 云中构建我们的基础设施。作为该领域的无可争议的领导者,大部分目标受众已经对 AWS 有所了解。为了帮助过渡,Azure 在其网站上提供了一个“AWS 到 Azure 服务比较”(docs.microsoft.com/en-us/azure/architecture/aws-professional/services)。这是一个方便的页面,当我对于与 AWS 相比,Azure 的等效服务感到困惑时,我经常访问这个页面,尤其是当服务名称没有直接说明服务内容时。(我的意思是,你能从 SageMaker 的名称中看出它是什么吗?我不再争辩了。)
我经常使用这个页面进行竞争分析。例如,当我需要比较 AWS 和 Azure 专用连接的成本时,我会从这个页面开始,验证 AWS Direct Connect 的等效服务是 Azure ExpressRoute,然后通过链接获取更多关于该服务的详细信息。
如果我们滚动到页面上的 网络 部分,我们可以看到 Azure 提供了许多与 AWS 相似的产品,例如 VNet、VPN 网关和负载均衡器。一些服务可能有不同的名称,例如 Route 53 和 Azure DNS,但底层服务是相同的。

图 12.1:Azure 网络服务(来源:https://docs.microsoft.com/en-us/azure/architecture/aws-professional/services)
Azure 和 AWS 网络产品之间有一些功能差异。例如,在全局流量负载均衡使用 DNS 方面,AWS 使用相同的 Route 53 产品,而 Azure 则将其拆分为一个名为 Traffic Manager 的独立产品。当我们深入到产品中时,一些差异可能会根据使用情况而有所不同。例如,Azure 负载均衡器默认允许会话亲和性,也就是所谓的粘性会话,而 AWS 负载均衡器则需要显式配置。
但大部分情况下,Azure 的高级网络产品和服务的确与我们在 AWS 中学到的相似。这是好消息。坏消息是,尽管功能相同,但这并不意味着我们可以在两者之间实现 1:1 的映射。
构建工具不同,对于刚接触 Azure 平台的人来说,实现细节有时可能会让人感到困惑。在接下来的章节中讨论产品时,我们将指出一些差异。让我们先从 Azure 的设置过程开始谈。
Azure 设置
设置 Azure 账户非常简单。就像 AWS 一样,Azure 提供了许多服务和激励措施来吸引在高度竞争的公共云市场中用户。请查看azure.microsoft.com/en-us/free/页面了解最新的服务。在撰写本文时,Azure 提供许多流行的服务免费 12 个月,以及 40 多个其他服务始终免费:

图 12.2:Azure 门户(来源:https://azure.microsoft.com/en-us/free/)
账户创建后,我们可以在 Azure 门户portal.azure.com上查看可用的服务:

图 12.3:Azure 服务
当你阅读这一章时,网页可能会有所变化。这些变化通常是直观的导航更改,即使它们看起来有些不同,也很容易操作。
然而,在启动任何服务之前,我们都需要提供一个支付方式。这是通过添加订阅服务来完成的:

图 12.4:Azure 订阅
我建议添加按量付费计划,这种计划没有前期成本,也没有长期承诺,但我们也有选择通过订阅计划购买各种级别支持的权利。
一旦添加了订阅,我们就可以开始查看在 Azure 云中管理和构建的各种方法,具体细节将在下一节中详细说明。
Azure 管理和 API
Azure 门户是顶级公共云提供商(包括 AWS 和 Google Cloud)中最简洁、最现代的门户。我们可以通过顶部管理栏上的设置图标更改门户设置,包括语言和区域:

图 12.5:不同语言的 Azure 门户
管理 Azure 服务有许多方法:门户、Azure CLI、RESTful API 以及各种客户端库。除了点对点的管理界面外,Azure 门户还提供了一个方便的 shell,称为 Azure Cloud Shell。
它可以从门户的右上角启动:

图 12.6:Azure Cloud Shell
当它首次启动时,您将被要求在 Bash 和 PowerShell 之间进行选择。shell 接口可以在以后切换,但它们不能同时运行:

图 12.7:Azure 云 Shell 与 PowerShell
我个人的偏好是 Bash shell,它允许我使用预安装的 Azure CLI 和 Python SDK:

图 12.8:Azure AZ 工具和 Cloud Shell 中的 Python
Cloud Shell 非常方便,因为它基于浏览器,因此可以从几乎任何地方访问。它按唯一用户账户分配,并且每个会话都会自动进行身份验证,所以我们不需要担心为它生成单独的密钥。但是,由于我们将会经常使用 Azure CLI,让我们在管理主机上安装一个本地副本:
(venv) $ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
(venv) $ az --version
azure-cli 2.40.0
core 2.40.0
telemetry 1.0.8
Dependencies:
msal 1.18.0b1
azure-mgmt-resource 21.1.0b1
让我们还在我们的管理主机上安装 Azure Python SDK。从版本 5.0.0 开始,Azure Python SDK 要求我们安装列在 aka.ms/azsdk/python/all 的服务特定包:
(venv) $ pip install azure-identity
(venv) $ pip install azure-mgmt-compute
(venv) $ pip install azure-mgmt-storage
(venv) $ pip install azure-mgmt-resource
(venv) $ pip install azure-mgmt-network
Azure for Python 开发者页面,docs.microsoft.com/en-us/azure/python/,是使用 Python 开始 Azure 的全面资源。Azure SDK for Python 页面,learn.microsoft.com/en-us/azure/developer/python/sdk/azure-sdk-overview,提供了使用 Python 库进行 Azure 资源管理的详细文档。
我们现在可以查看一些 Azure 的服务原则,并启动我们的 Azure 服务。
Azure 服务主体
Azure 使用服务主体对象的概念用于自动化工具。网络安全最佳实践中的最小权限原则授予任何人员或工具仅足够执行其工作而不更多的访问权限。Azure 服务主体根据角色限制资源和访问级别。要开始,我们将使用 Azure CLI 自动为我们创建的角色,并使用 Python SDK 进行身份验证测试。使用 az login 命令接收令牌:
(venv) $ az login --use-device-code
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code <your code> to authenticate.
按照 URL 粘贴你在命令行中看到的代码,并使用我们之前创建的 Azure 账户进行身份验证:

图 12.9:Azure 跨平台命令行界面
我们可以在 json 格式中创建凭证文件,并将其移动到 Azure 目录。Azure 目录是在我们安装 Azure CLI 工具时创建的:
(venv) $ az ad sp create-for-rbac --sdk-auth > credentials.json
(venv) $ cat credentials.json
{
"clientId": "<skip>",
"clientSecret": "<skip>",
"subscriptionId": "<skip>",
"tenantId": "<skip>",
"<skip>"
}
(venv) echou@network-dev-2:~$ mv credentials.json ~/.azure/
让我们保护凭证文件并将其导出为环境变量:
(venv) $ chmod 0600 ~/.azure/credentials.json
(venv) $ export AZURE_AUTH_LOCATION=~/.azure/credentials.json
我们还将各种凭证导出到我们的环境中:
$ cat ~/.azure/credentials.json
$ export AZURE_TENANT_ID="xxx"
$ export AZURE_CLIENT_ID="xxx"
$ export AZURE_CLIENT_SECRET="xxx"
$ export SUBSCRIPTION_ID="xxx"
我们将授予订阅的角色访问权限:
(venv) $ az ad sp create-for-rbac --role 'Owner' --scopes '/subscriptions/<subscription id>'
{
"appId": "<appId>",
"displayName": "azure-cli-2022-09-22-17-24-24",
"password": "<password>",
"tenant": "<tenant>"
}
(venv) $ az login --service-principal --username "<appId>" --password "<password>" --tenant "<tenant>"
有关 Azure RBAC 的更多信息,请访问learn.microsoft.com/en-us/cli/azure/create-an-azure-service-principal-azure-cli。
如果我们在门户中浏览到访问控制部分(主页 -> 订阅 -> 按需付费 -> 访问控制),我们将能够看到新创建的角色:

图 12.10:Azure 按需付费 IAM
在 GitHub 页面上有许多使用 Azure Python SDK 管理网络的示例代码,github.com/Azure-Samples/azure-samples-python-management/tree/main/samples/network。入门指南,learn.microsoft.com/en-us/samples/azure-samples/azure-samples-python-management/network/,也可能很有用。
我们将使用一个简单的 Python 脚本,Chapter12_1_auth.py,来导入客户端身份验证和网络管理的库:
#!/usr/bin/env python3
import os
import azure.mgmt.network
from azure.identity import ClientSecretCredential
credential = ClientSecretCredential(
tenant_id=os.environ.get("AZURE_TENANT_ID"),
client_id=os.environ.get("AZURE_CLIENT_ID"),
client_secret=os.environ.get("AZURE_CLIENT_SECRET")
)
subscription_id = os.environ.get("SUBSCRIPTION_ID")
network_client = azure.mgmt.network.NetworkManagementClient(credential=credential, subscription_id=subscription_id)
print("Network Management Client API Version: " + network_client.DEFAULT_API_VERSION)
如果文件在没有错误的情况下执行,则表示我们已成功使用 Python SDK 客户端进行身份验证:
(venv) $ python Chapter12_1_auth.py
Network Management Client API Version: 2022-01-01
在阅读 Azure 文档时,你可能已经注意到了 PowerShell 和 Python 的结合。在下一节中,我们将简要考虑 Python 和 PowerShell 之间的关系。
Python 与 PowerShell
微软已经从头开发或实现了包括 C#、.NET 和 PowerShell 在内的许多编程语言和框架,因此.NET(与 C#)和 PowerShell 在 Azure 中享有某种程度的一等公民地位并不令人惊讶。在 Azure 的大部分文档中,你都会找到直接引用 PowerShell 示例的内容。在网络上,关于哪个工具(Python 或 PowerShell)更适合管理 Azure 资源的讨论往往带有主观意见。
截至 2019 年 7 月,我们还可以在预览版中在 Linux 和 macOS 操作系统上运行 PowerShell Core,docs.microsoft.com/en-us/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-6。
我们不会就语言优越性进行辩论。当需要时,我不介意使用 PowerShell——我发现它既简单又直观——并且我同意有时 Python SDK 在实现最新的 Azure 功能方面落后于 PowerShell。但既然 Python 至少是您选择这本书的部分原因,我们将坚持使用 Python SDK 和 Azure CLI 作为我们的示例。
最初,Azure CLI 是以 Windows 的 PowerShell 模块和其他平台的基于 Node.js 的 CLI 的形式提供的。但随着该工具的普及,它现在是一个围绕 Azure Python SDK 的包装器,如Python.org上的这篇文章所述:www.python.org/success-stories/building-an-open-source-and-cross-platform-azure-cli-with-python/。
在本章剩余的部分,当我们介绍一个功能或概念时,我们通常会转向 Azure CLI 进行演示。请放心,如果某个功能可以作为 Azure CLI 命令使用,那么如果我们需要直接用 Python 编写代码,它也将在 Python SDK 中可用。
在介绍了 Azure 管理和相关的 API 之后,让我们继续讨论 Azure 全球基础设施。
Azure 全球基础设施
与 AWS 类似,Azure 全球基础设施由地区、可用区域(AZs)和边缘位置组成。在撰写本文时,Azure 拥有 60 多个地区和 200 多个以上的物理数据中心,如产品页面所示(azure.microsoft.com/en-us/global-infrastructure/)):

图 12.11:Azure 全球基础设施(来源:https://azure.microsoft.com/en-us/global-infrastructure/)
与 AWS 一样,Azure 产品通过地区提供,因此我们需要根据地区检查服务可用性和定价。我们还可以通过在多个 AZs 中构建服务来在服务中构建冗余。然而,与 AWS 不同,并非所有 Azure 地区都有 AZs,也并非所有 Azure 产品都支持它们。实际上,Azure 直到 2018 年才宣布 AZs 的通用可用性,并且它们只在选定地区提供。
在选择我们的地区时,这一点需要我们注意。我建议选择带有 AZs 的地区,例如西 US 2、中 US 和东 US 1。
如果我们在没有可用区域(AZs)的地区进行建设,我们就需要在不同的地区复制该服务,通常是在同一地理区域内。我们将在下一节讨论 Azure 地理区域。
在 Azure 全球基础设施页面上,带有可用区域的地区中间有一个星号标记。
与 AWS 不同,Azure 区域也组织成更高层次的地理分类。地理是一个独立的市场,通常包含一个或多个区域。除了低延迟和更好的网络连接外,跨同一地理区域内区域复制服务和数据对于政府合规性也是必要的。跨区域复制的例子是德国的区域。如果我们需要为德国市场推出服务,政府要求在边境内实施严格的数据主权,但德国没有区域有可用区。我们需要在同一地理区域内不同区域之间复制数据,即德国北部、德国东北部、德国西中部等等。
按照惯例,我通常更喜欢有可用区的区域,以保持不同云服务提供商之间的相似性。一旦我们确定了最适合我们用例的区域,我们就可以在 Azure 中构建我们的 VNet。
Azure 虚拟网络
当我们在 Azure 云中戴上网络工程师的帽子时,Azure 虚拟网络(VNets)是我们花费大部分时间的地方。与传统网络类似,我们在数据中心构建的传统网络,它们是我们 Azure 中私有网络的基本构建块。我们将使用 VNet 来允许我们的虚拟机相互通信,与互联网,以及通过 VPN 或 ExpressRoute 与我们的本地网络通信。
让我们从使用门户构建我们的第一个 VNet 开始。我们将首先通过创建资源 -> 网络 -> 虚拟网络浏览虚拟网络页面:

图 12.12:Azure VNet
每个 VNet 都限于单个区域,并且我们可以在每个 VNet 中创建多个子网。正如我们稍后将会看到的,不同区域的多个 VNet 可以通过 VNet 对等连接到彼此。
从 VNet 创建页面,我们将使用以下凭据创建我们的第一个网络:
Name: WEST-US-2_VNet_1
Address space: 192.168.0.0/23
Subscription: <pick your subscription>
Resource group: <click on new> -> 'Mastering-Python-Networking'
Location: West US 2
Subnet name: WEST-US-2_VNet_1_Subnet_1
Address range: 192.168.1.0/24
DDoS protection: Basic
Service endpoints: Disabled
Firewall: Disabled
这里是必要的字段的截图。如果有任何缺失的必填字段,它们将以红色突出显示。完成时点击创建:

图 12.13:Azure VNet 创建
资源创建完成后,我们可以通过主页 -> 资源组 -> Mastering-Python-Networking导航到它:

图 12.14:Azure VNet 概述
恭喜,我们刚刚在 Azure 云中创建了我们的第一个 VNet!我们的网络需要与外界通信才能发挥作用。我们将在下一节中探讨如何做到这一点。
互联网访问
默认情况下,VNet 内的所有资源都可以与互联网进行出站通信;我们不需要像在 AWS 中那样添加 NAT 网关。对于入站通信,我们需要直接将公网 IP 分配给虚拟机或使用带有公网 IP 的负载均衡器。为了看到这个功能的工作情况,我们将在我们的网络中创建虚拟机。
我们可以从主页 -> 资源组 -> Mastering-Python-Networking -> 新建 -> 创建虚拟机创建我们的第一个虚拟机:

图 12.15:Azure 创建虚拟机
我将选择Ubuntu Server 22.04 LTS作为虚拟机,并在提示时使用名称myMPN-VM1。我将选择区域West US 2。我们可以选择密码认证或 SSH 公钥作为认证方法,并允许 SSH 入站连接。由于我们正在对其进行测试,我们可以选择 B 系列中最小的实例以最小化我们的成本:

图 12.16:Azure 计算 B 系列
我们可以将其他选项保留为默认设置,选择较小的磁盘大小,并勾选与虚拟机一起删除。我们将虚拟机放入我们创建的子网中,并分配一个新的公网 IP:

图 12.17:Azure 网络接口
虚拟机配置完成后,我们可以使用公网 IP 和创建的用户名ssh登录到该机器。虚拟机只有一个接口,位于我们的私有子网内;它也被映射到 Azure 自动分配的公网 IP。这种公网到私网 IP 的转换由 Azure 自动完成。
echou@myMPN-VM1:~$ sudo apt install net-tools
echou@myMPN-VM1:~$ ifconfig eth0
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.1.4 netmask 255.255.255.0 broadcast 192.168.1.255
inet6 fe80::20d:3aff:fe06:68a0 prefixlen 64 scopeid 0x20<link>
ether 00:0d:3a:06:68:a0 txqueuelen 1000 (Ethernet)
RX packets 2344 bytes 2201526 (2.2 MB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 1290 bytes 304355 (304.3 KB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
echou@myMPN-VM1:~$ ping -c 1 www.google.com
PING www.google.com (142.251.211.228) 56(84) bytes of data.
64 bytes from sea30s13-in-f4.1e100.net (142.251.211.228): icmp_seq=1 ttl=115 time=47.7 ms
--- www.google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 47.668/47.668/47.668/0.000 ms
我们可以重复相同的步骤创建第二个名为myMPN-VM2的虚拟机。虚拟机可以配置为具有SSH入站访问,但没有公网 IP:

图 12.18:Azure 虚拟机 IP 地址
在创建虚拟机之后,我们可以从myMPN-VM1使用私网 IPssh连接到myMPN-VM2。
echou@myMPN-VM1:~$ ssh echou@192.168.1.5
echou@myMPN-VM2:~$ who
echou pts/0 2022-09-22 16:43 (192.168.1.4)
我们可以通过尝试访问apt软件包更新仓库来测试互联网连接:
echou@myMPN-VM2:~$ sudo apt update
Hit:1 http://azure.archive.ubuntu.com/ubuntu jammy InRelease
Get:2 http://azure.archive.ubuntu.com/ubuntu jammy-updates InRelease [114 kB]
Get:3 http://azure.archive.ubuntu.com/ubuntu jammy-backports InRelease [99.8 kB]
Get:4 http://azure.archive.ubuntu.com/ubuntu jammy-security InRelease [110 kB]
Get:5 http://azure.archive.ubuntu.com/ubuntu jammy/universe amd64 Packages [14.1 MB]
Fetched 23.5 MB in 6s (4159 kB/s)
在 VNet 内部,我们的虚拟机可以访问互联网,我们可以为我们的网络创建额外的网络资源。
网络资源创建
让我们看看使用 Python SDK 创建网络资源的示例。在下面的示例中,Chapter12_2_network_resources.py,我们将使用subnet.create_or_update API 在 VNet 中创建一个新的192.168.0.128/25子网:
#!/usr/bin/env python3
# Reference example: https://github.com/Azure-Samples/azure-samples-python-management/blob/main/samples/network/virtual_network/manage_subnet.py
#
import os
from azure.identity import ClientSecretCredential
import azure.mgmt.network
from azure.identity import DefaultAzureCredential
from azure.mgmt.network import NetworkManagementClient
from azure.mgmt.resource import ResourceManagementClient
credential = ClientSecretCredential(
tenant_id=os.environ.get("AZURE_TENANT_ID"),
client_id=os.environ.get("AZURE_CLIENT_ID"),
client_secret=os.environ.get("AZURE_CLIENT_SECRET")
)
subscription_id = os.environ.get("SUBSCRIPTION_ID")
GROUP_NAME = "Mastering-Python-Networking"
VIRTUAL_NETWORK_NAME = "WEST-US-2_VNet_1"
SUBNET = "WEST-US-2_VNet_1_Subnet_2"
network_client = azure.mgmt.network.NetworkManagementClient(
credential=credential, subscription_id=subscription_id)
# Get subnet
subnet = network_client.subnets.get(
GROUP_NAME,
VIRTUAL_NETWORK_NAME,
SUBNET
)
print("Get subnet:\n{}".format(subnet))
subnet = network_client.subnets.begin_create_or_update(
GROUP_NAME,
VIRTUAL_NETWORK_NAME,
SUBNET,
{
"address_prefix": "192.168.0.128/25"
}
).result()
print("Create subnet:\n{}".format(subnet))
执行脚本时,我们将收到以下创建结果消息:
(venv) $ python3 Chapter12_2_subnet.py
{'additional_properties': {'type': 'Microsoft.Network/virtualNetworks/subnets'}, 'id': '/subscriptions/<skip>/resourceGroups/Mastering-Python-Networking/providers/Microsoft.Network/virtualNetworks/WEST-US-2_VNet_1/subnets/WEST-US-2_VNet_1_Subnet_2', 'address_prefix': '192.168.0.128/25', 'address_prefixes': None, 'network_security_group': None, 'route_table': None, 'service_endpoints': None, 'service_endpoint_policies': None, 'interface_endpoints': None, 'ip_configurations': None, 'ip_configuration_profiles': None, 'resource_navigation_links': None, 'service_association_links': None, 'delegations': [], 'purpose': None, 'provisioning_state': 'Succeeded', 'name': 'WEST-US-2_VNet_1_Subnet_2', 'etag': 'W/"<skip>"'}
新子网也可以在门户中看到:

图 12.19:Azure VNet 子网
更多使用 Python SDK 的示例,请参阅 github.com/Azure-Samples/azure-samples-python-management。
如果我们在新的子网中创建一个虚拟机,即使跨越子网边界,同一 VNet 中的主机也可以通过我们之前在 AWS 中看到的相同隐式路由相互通信。
当我们需要与其他 Azure 服务交互时,我们有额外的 VNet 服务可用。让我们看看。
VNet 服务端点
VNet 服务端点可以将 VNet 扩展到其他 Azure 服务,通过直接连接。这允许来自 VNet 到 Azure 服务的流量保持在 Azure 网络中。服务端点需要在 VNet 所在区域的标识服务中进行配置。
它们可以通过门户进行配置,对服务和子网进行限制:

图 12.20:Azure 服务端点
严格来说,当我们需要 VNet 中的虚拟机与服务通信时,我们不需要创建 VNet 服务端点。每个虚拟机都可以通过映射的公共 IP 访问服务,我们可以使用网络规则仅允许必要的 IP。然而,使用 VNet 服务端点允许我们使用 Azure 内部的私有 IP 访问资源,而无需流量穿越公共互联网。
VNet 对等连接
如本节开头所述,每个 VNet 限制在一个区域。对于区域到区域 VNet 连接,我们可以利用 VNet 对等连接。让我们在 Chapter11_3_vnet.py 中使用以下两个函数来在 US-East 区域创建一个 VNet:
<skip>
def create_vnet(network_client):
vnet_params = {
'location': LOCATION,
'address_space': {
'address_prefixes': ['10.0.0.0/16']
}
}
creation_result = network_client.virtual_networks.create_or_update(
GROUP_NAME,
'EAST-US_VNet_1',
vnet_params
)
return creation_result.result()
<skip>
def create_subnet(network_client):
subnet_params = {
'address_prefix': '10.0.1.0/24'
}
creation_result = network_client.subnets.create_or_update(
GROUP_NAME,
'EAST-US_VNet_1',
'EAST-US_VNet_1_Subnet_1',
subnet_params
)
return creation_result.result()
要允许 VNet 对等连接,我们需要从两个 VNet 之间双向对等。由于我们到目前为止一直在使用 Python SDK,为了学习目的,让我们看看使用 Azure CLI 的一个示例。
我们将从 az network vnet list 命令中获取 VNet 名称和 ID:
(venv) $ az network vnet list
<skip>
"id": "/subscriptions/<skip>/resourceGroups/Mastering-Python-Networking/providers/Microsoft.Network/virtualNetworks/EAST-US_VNet_1",
"location": "eastus",
"name": "EAST-US_VNet_1"
<skip>
"id": "/subscriptions/<skip>/resourceGroups/Mastering-Python-Networking/providers/Microsoft.Network/virtualNetworks/WEST-US-2_VNet_1",
"location": "westus2",
"name": "WEST-US-2_VNet_1"
<skip>
让我们检查我们 West US 2 VNet 的现有 VNet 对等连接:
(venv) $ az network vnet peering list -g "Mastering-Python-Networking" --vnet-name WEST-US-2_VNet_1
[]
我们将从 West US 到 East US 的 VNet 执行对等连接,然后以相反方向重复:
(venv) $ az network vnet peering create -g "Mastering-Python-Networking" -n WestUSToEastUS --vnet-name WEST-US-2_VNet_1 --remote-vnet "/subscriptions/<skip>/resourceGroups/Mastering-Python-Networking/providers/Microsoft.Network/virtualNetworks/EAST-US_VNet_1"
(venv) $ az network vnet peering create -g "Mastering-Python-Networking" -n EastUSToWestUS --vnet-name EAST-US_VNet_1 --remote-vnet "/subscriptions/b7257c5b-97c1-45ea-86a7-872ce8495a2a/resourceGroups/Mastering-Python-Networking/providers/Microsoft.Network/virtualNetworks/WEST-US-2_VNet_1"
现在如果我们再次运行检查,我们将能够看到 VNet 成功对等:
(venv) $ az network vnet peering list -g "Mastering-Python-Networking" --vnet-name "WEST-US-2_VNet_1"
[
{
"allowForwardedTraffic": false,
"allowGatewayTransit": false,
"allowVirtualNetworkAccess": false,
"etag": "W/\"<skip>\"",
"id": "/subscriptions/<skip>/resourceGroups/Mastering-Python-Networking/providers/Microsoft.Network/virtualNetworks/WEST-US-2_VNet_1/virtualNetworkPeerings/WestUSToEastUS",
"name": "WestUSToEastUS",
"peeringState": "Connected",
"provisioningState": "Succeeded",
"remoteAddressSpace": {
"addressPrefixes": [
"10.0.0.0/16"
]
},
<skip>
我们也可以在 Azure 门户中验证对等连接:

图 12.21:Azure VNet 对等连接
现在我们已经在我们的设置中有几个主机、子网、VNet 和 VNet 对等连接,我们应该看看在 Azure 中是如何进行路由的。这就是我们在下一节将要做的。
VNet 路由
作为一名网络工程师,云提供商添加的隐式路由一直让我感到有些不舒服。在传统网络中,我们需要布线网络,分配 IP 地址,配置路由,实施安全措施,并确保一切正常工作。有时可能会很复杂,但每个数据包和路由都有记录。对于云中的虚拟网络,底层网络已经由 Azure 完成,并且覆盖网络上的某些网络配置需要在启动时自动完成,就像我们之前看到的。
Azure VNet 路由与 AWS 略有不同。在 AWS 章节中,我们看到路由表是在 VPC 网络层实现的。但如果我们浏览到门户上的 Azure VNet 设置,我们将找不到分配给 VNet 的路由表。
如果我们进一步查看子网设置,我们将看到一个路由表下拉菜单,但它显示的值是无:

图 12.22:Azure 子网路由表
我们如何有一个空的路由表,而该子网中的主机能够访问互联网?我们可以在哪里看到 Azure VNet 配置的路由?路由已经在主机和 NIC 级别实现。我们可以通过所有服务 -> 虚拟机 -> myNPM-VM1 -> 网络(左侧面板)-> 拓扑(顶部面板)来查看:

图 12.23:Azure 网络拓扑
网络在 NIC 级别上显示,每个 NIC 连接到北边的 VNet 子网,以及南边的其他资源,如 VM、网络安全组(NSG)和 IP。资源是动态的;在屏幕截图时,我只运行了myMPN-VM1,因此它是唯一一个连接了 VM 和 IP 地址的,而其他 VM 只连接了 NSG。
我们将在下一节中介绍 NSG。
如果我们点击拓扑中的网络接口卡(NIC),mympn-vm1655,我们可以看到与 NIC 相关的设置。在支持 + 故障排除部分,我们将找到有效路由链接,在那里我们可以看到与 NIC 关联的当前路由:

图 12.24:Azure VNet 有效路由
如果我们想自动化这个过程,我们可以使用 Azure CLI 来查找 NIC 名称,然后显示路由表:
(venv) $ az vm show --name myMPN-VM1 --resource-group 'Mastering-Python-Networking'
<skip>
"networkProfile": {
"networkInterfaces": [
{
"id": "/subscriptions/<skip>/resourceGroups/Mastering-Python-Networking/providers/Microsoft.Network/networkInterfaces/mympn-vm1655",
"primary": null,
"resourceGroup": "Mastering-Python-Networking"
}
]
}
<skip>
(venv) $ az network nic show-effective-route-table --name mympn-vm1655 --resource-group "Mastering-Python-Networking"
{
"nextLink": null,
"value": [
{
"addressPrefix": [
"192.168.0.0/23"
],
<skip>
好吧!这是一个谜团解决了,但路由表中的那些下一跳是什么?我们可以参考 VNet 流量路由文档:docs.microsoft.com/en-us/azure/virtual-network/virtual-networks-udr-overview。几点重要的注意事项:
-
如果源指示路由为默认,这些是系统路由,无法删除,但可以用自定义路由覆盖。
-
VNet 下一跳是在自定义 VNet 内部的路线。在我们的例子中,这是
192.168.0.0/23网络,而不仅仅是子网。 -
路由到None下一跳类型的流量将被丢弃,类似于Null接口路由。
-
VNetGlobalPeering下一跳类型是在我们与其他 VNet 建立 VNet 对等连接时创建的。
-
VirtualNetworkServiceEndpoint下一跳类型是在我们为 VNet 启用服务端点时创建的。公共 IP 由 Azure 管理,并会不时更改。
如何覆盖默认路由?我们可以创建一个路由表并将其与子网关联。Azure 选择以下优先级的路由:
-
用户定义的路由
-
BGP 路由(来自站点到站点 VPN 或 ExpressRoute)
-
系统路由
我们可以在网络部分创建路由表:

图 12.25:Azure VNet 路由表
我们也可以通过 Azure CLI 创建路由表,在表中创建路由,并将路由表与子网关联:
(venv) $ az network route-table create --name TempRouteTable --resource "Mastering-Python-Networking"
(venv) $ az network route-table route create -g "Mastering-Python-Networking" --route-table-name TempRouteTable -n TempRoute --next-hop-type VirtualAppliance --address-prefix 172.31.0.0/16 --next-hop-ip-address 10.0.100.4
(venv) $ az network vnet subnet update -g "Mastering-Python-Networking" -n WEST-US-2_Vnet_1_Subnet_1 --vnet-name WEST-US-2_VNet_1 --route-table TempRouteTable
让我们看看 VNet 中的主要安全措施:网络安全组(NSGs)。
网络安全组
VNet 安全主要通过 NSGs 实现。就像传统的访问列表或防火墙规则一样,我们需要一次考虑一个方向的网络安全规则。例如,如果我们想让主机A在子网 1上通过端口80与子网 2中的主机B自由通信,我们需要为两个主机的入站和出站方向实施必要的规则。
如前例所示,NSG 可以与 NIC 或子网关联,因此我们还需要从安全层考虑。一般来说,我们应该在主机级别实施更严格的规则,而在子网级别应用更宽松的规则。这类似于传统的网络。
当我们创建我们的虚拟机时,我们为 SSH TCP 端口22设置了入站允许规则。让我们看看为我们的第一个虚拟机创建的安全组myMPN-VM1-nsg:

图 12.26:Azure VNet NSG
有几点值得指出:
-
系统实施规则的优先级较高,为 65,000 及以上。
-
默认情况下,虚拟网络可以在两个方向上自由通信。
-
默认情况下,内部主机允许访问互联网。
让我们在门户中为现有的 NSG 组实施入站规则:

图 12.27:Azure 安全规则
我们也可以通过 Azure CLI 创建新的安全组和规则:
(venv) $ az network nsg create -g "Mastering-Python-Networking" -n TestNSG
(venv) $ az network nsg rule create -g "Mastering-Python-Networking" --nsg-name TestNSG -n Allow_SSH --priority 150 --direction Inbound --source-address-prefixes Internet --destination-port-ranges 22 --access Allow --protocol Tcp --description "Permit SSH Inbound"
(venv) $ az network nsg rule create -g "Mastering-Python-Networking" --nsg-name TestNSG -n Allow_SSL --priority 160 --direction Inbound --source-address-prefixes Internet --destination-port-ranges 443 --access Allow --protocol Tcp --description "Permit SSL Inbound"
我们可以看到创建的新规则以及默认规则:

图 12.28:Azure 安全规则
最后一步是将此 NSG 绑定到子网:
(venv) $ az network vnet subnet update -g "Mastering-Python-Networking" -n WEST-US-2_VNet_1_Subnet_1 --vnet-name WEST-US-2_VNet_1 --network-security-group TestNSG
在接下来的两节中,我们将探讨将 Azure 虚拟网络扩展到本地数据中心的主要两种方式:Azure VPN 和 Azure ExpressRoute。
Azure VPN
随着网络的持续增长,可能会出现我们需要将 Azure VNet 连接到本地位置的情况。VPN 网关是一种 VNet 网关,可以加密 VNet 和我们的本地网络以及远程客户端之间的流量。每个 VNet 只能有一个 VPN 网关,但可以在同一个 VPN 网关上建立多个连接。
更多有关 Azure VPN 网关的信息可以在以下链接中找到:docs.microsoft.com/en-us/azure/vpn-gateway/.
VPN 网关实际上是配置了加密和路由服务的虚拟机,但用户不能直接配置。Azure 提供基于隧道类型、并发连接数和总吞吐量的 SKU 列表(docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpn-gateway-settings#gwsku):

图 12.29:Azure VPN 网关 SKU(来源:https://docs.microsoft.com/en-us/azure/vpn-gateway/point-to-site-about)
如前表所示,Azure VPN 分为两个不同的类别:点对点(P2S)VPN 和 站点到站点(S2S)VPN。P2S VPN 允许从单个客户端计算机建立安全连接,主要用于远程工作者。加密方法可以是 SSTP、IKEv2 或 OpenVPN 连接。在选择 P2S VPN 网关 SKU 时,我们将关注 SKU 图表上的第二和第三列,以确定连接数。
对于基于客户端的 VPN,我们可以使用 SSTP 或 IKEv2 作为隧道协议:

图 12.30:Azure 站点到站点 VPN 网关(来源:https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpngateways)
除了基于客户端的 VPN,还有一种类型的 VPN 连接是站点到站点或多站点 VPN 连接。加密方法将是 IKE 之上的 IPSec,Azure 和本地网络都需要一个公共 IP,如下面的图所示:

图 12.31:Azure 客户端 VPN 网关(来源:https://docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpngateways)
创建 S2S 或 P2S VPN 的完整示例超出了本节所能涵盖的范围。Azure 提供了 S2S VPN (docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal) 和 P2S VPN (docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-howto-site-to-site-resource-manager-portal) 的教程。
对于之前配置过 VPN 服务的工程师来说,步骤相当简单。可能有点令人困惑,但文档中没有明确指出的是,VPN 网关设备应位于 VNet 内的专用网关子网中,并分配了 /27 IP 区块:

图 12.32:Azure VPN 网关子网
可以在 docs.microsoft.com/en-us/azure/vpn-gateway/vpn-gateway-about-vpn-devices 找到不断增长的经过验证的 Azure VPN 设备列表,其中包含它们各自配置指南的链接。
Azure ExpressRoute
当组织需要将 Azure VNet 扩展到本地站点时,从 VPN 连接开始是有意义的。然而,随着连接承担更多任务关键型流量,组织可能需要一个更稳定和可靠的连接。类似于 AWS Direct Connect,Azure 通过连接提供商提供 ExpressRoute 作为私有连接。从图中我们可以看到,我们的网络在过渡到 Azure 边缘网络之前连接到 Azure 的合作伙伴边缘网络:

图 12.33:Azure ExpressRoute 电路(来源:https://docs.microsoft.com/en-us/azure/expressroute/expressroute-introduction)
ExpressRoute 的优点包括:
-
更可靠,因为它不穿越公共互联网。
-
由于私有连接可能在地面上到 Azure 之间有更少的跳数,因此连接速度更快,延迟更低。
-
由于是私有连接,因此安全性更好,特别是如果公司依赖微软的服务,如 Office 365。
ExpressRoute 的缺点可能包括:
-
在业务和技术要求方面都更难设置。
-
由于端口费用和连接费用通常是固定的,因此前期成本较高。如果它取代了 VPN 连接,一些成本可以通过降低互联网费用来抵消。然而,通常 ExpressRoute 的总拥有成本更高。
更多关于 ExpressRoute 的详细信息可以在docs.microsoft.com/en-us/azure/expressroute/expressroute-introduction找到。与 AWS Direct Connect 最大的不同之处在于,ExpressRoute 可以在地理上提供跨区域的连接。还有一个高级附加功能,允许全球连接到 Microsoft 服务,以及 Skype for Business 的 QoS 支持。
与 Direct Connect 类似,ExpressRoute 要求用户通过合作伙伴连接到 Azure,或者在 ExpressRoute Direct(是的,这个术语很令人困惑)的某个指定地点与 Azure 会合。这对于企业来说通常是最大的障碍,因为他们需要在 Azure 的某个位置建立数据中心,与运营商(MPLS VPN)连接,或者与经纪人作为连接的中介。这些选项通常需要商业合同、更长期的承诺以及承诺的月度费用。
首先,我的建议将与第十一章“AWS 云网络”中的建议相似,即使用现有的运营商经纪人连接到运营商酒店。从运营商酒店,可以直接连接到 Azure 或使用 Equinix FABRIC(www.equinix.com/interconnection-services/equinix-fabric)等中介。
在下一节中,我们将探讨当我们的服务增长到仅一个服务器之外时,如何有效地分配传入流量。
Azure 网络负载均衡器
Azure 提供基本和标准 SKU 的负载均衡器。当我们在本节中讨论负载均衡器时,我们指的是第 4 层 TCP 和 UDP 负载分发服务,而不是应用程序网关负载均衡器(azure.microsoft.com/en-us/services/application-gateway/),它是一个第 7 层负载均衡解决方案。
典型的部署模型通常是单层或双层负载分发,用于从互联网传入的连接:

图 12.34:Azure 负载均衡器(来源:https://docs.microsoft.com/en-us/azure/load-balancer/load-balancer-overview)
负载均衡器根据 5 元组散列(源和目标 IP、源和目标端口以及协议)对传入的连接进行散列,并将流量分发到一个或多个目标。标准负载均衡器 SKU 是基本 SKU 的超集,因此新的设计应采用标准负载均衡器。
与 AWS 一样,Azure 也在不断通过新的网络服务进行创新。我们已经在本章中介绍了基础服务;让我们来看看一些其他值得注意的服务。
其他 Azure 网络服务
我们应该注意的其他 Azure 网络服务包括:
-
DNS 服务: Azure 提供了一套 DNS 服务(
docs.microsoft.com/en-us/azure/dns/dns-overview),包括公共和私有服务。这些服务可用于网络服务的地理负载均衡。 -
容器网络: 近年来,Azure 一直在推动容器的发展。有关 Azure 网络容器能力的更多信息,请参阅
docs.microsoft.com/en-us/azure/virtual-network/container-networking-overview。 -
VNet TAP: Azure VNet TAP 允许您持续地将您的虚拟机网络流量流式传输到网络数据包收集器或分析工具(
docs.microsoft.com/en-us/azure/virtual-network/virtual-network-tap-overview)。 -
分布式拒绝服务保护: Azure DDoS 保护提供针对 DDoS 攻击的防御(
docs.microsoft.com/en-us/azure/virtual-network/ddos-protection-overview)。
Azure 网络服务是 Azure 云家族的重要组成部分,并且正在以较快的速度增长。在本章中,我们只覆盖了部分服务,但希望这已经为您提供了一个良好的基础,以便开始探索其他服务。
摘要
在本章中,我们探讨了各种 Azure 云网络服务。我们讨论了 Azure 全球网络和虚拟网络的各个方面。我们使用 Azure CLI 和 Python SDK 来创建、更新和管理这些网络服务。当我们需要将 Azure 服务扩展到本地数据中心时,我们可以使用 VPN 或 ExpressRoute 进行连接。我们还简要介绍了各种 Azure 网络产品和服务。
在下一章中,我们将使用一站式堆栈:Elastic Stack,重新审视数据分析管道。
加入我们的书籍社区
要加入本书的社区——在这里您可以分享反馈、向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第十三章:使用 Elastic Stack 进行网络数据分析
在 第七章,使用 Python 进行网络监控(第一部分)和 第八章,使用 Python 进行网络监控(第二部分)中,我们讨论了监控网络的各种方法。在这两章中,我们探讨了两种不同的网络数据收集方法:我们可以从网络设备(如 SNMP)检索数据,或者我们可以通过基于流的导出监听网络设备发送的数据。收集数据后,我们需要将数据存储在数据库中,然后分析数据以获得洞察力,从而决定数据的意义。大多数情况下,分析结果以图表的形式显示,无论是折线图、柱状图还是饼图。我们可以为每个步骤使用单独的工具,如 PySNMP、Matplotlib 和 Pygal,或者我们可以利用一体化的工具,如 Cacti 或 ntop 进行监控。这两章中介绍的工具为我们提供了基本的监控和网络理解。
然后,我们继续到 第九章,使用 Python 构建网络 Web 服务,以构建 API 服务来抽象我们的网络,使其从高级工具中分离出来。在 第十一章,AWS 云网络 和 第十二章,Azure 云网络 中,我们使用 AWS 和 Azure 将本地网络扩展到云端。在这些章节中,我们覆盖了大量的内容,并拥有了一套坚实的工具来帮助我们使网络可编程。
从本章开始,我们将基于前几章的工具集,并查看我在熟悉了前几章中介绍的工具后,在旅程中找到的其他有用工具和项目。在本章中,我们将探讨一个开源项目,Elastic Stack (www.elastic.co),它可以帮助我们在之前所见的范围之外分析和监控我们的网络。
在本章中,我们将探讨以下主题:
-
什么是 Elastic(或 ELK)Stack?
-
Elastic Stack 安装
-
使用 Logstash 进行数据摄取
-
使用 Beats 进行数据摄取
-
使用 Elasticsearch 进行搜索
-
使用 Kibana 进行数据可视化
让我们先回答一个问题:Elastic Stack 究竟是什么?
什么是 Elastic Stack?
Elastic Stack 也被称为“ELK” Stack。那么,它究竟是什么呢?让我们看看开发者们用自己的话是如何描述的 (www.elastic.co/what-is/elk-stack):
“ELK”是三个开源项目的缩写:Elasticsearch、Logstash 和 Kibana。Elasticsearch 是一个搜索和分析引擎。Logstash 是一个服务器端数据处理管道,可以同时从多个来源摄取数据,对其进行转换,然后将数据发送到类似 Elasticsearch 的“储藏室”。Kibana 允许用户在 Elasticsearch 中使用图表和图形可视化数据。Elastic Stack 是 ELK Stack 的下一进化阶段。

图 13.1:Elastic Stack(来源:https://www.elastic.co/what-is/elk-stack)
从声明中我们可以看出,Elastic Stack 是由不同项目组成的集合,这些项目协同工作,覆盖了数据收集、存储、检索、分析和可视化的整个范围。这个堆栈的优点在于它紧密集成,但每个组件也可以单独使用。如果我们不喜欢 Kibana 用于可视化,我们可以轻松地插入 Grafana 用于图表。如果我们想使用其他数据摄取工具呢?没问题,我们可以使用 RESTful API 将我们的数据发布到 Elasticsearch。堆栈的中心是 Elasticsearch,这是一个开源的分布式搜索引擎。其他项目都是为了增强和支持搜索功能而创建的。一开始这可能听起来有点令人困惑,但当我们更深入地了解项目的组件时,它将变得更加清晰。
他们为什么将 ELK Stack 的名字改为 Elastic Stack?2015 年,Elastic 引入了一系列轻量级、单用途的数据传输工具,称为 Beats。它们立刻受到欢迎,并且继续非常受欢迎,但创造者无法为“B”想出一个好的首字母缩略词,因此决定将整个堆栈重命名为 Elastic Stack。
我们将重点关注 Elastic Stack 的网络监控和数据分析方面。尽管如此,这个堆栈有许多用例,包括风险管理、电子商务个性化、安全分析、欺诈检测等。它被各种组织使用,从思科、Box 和 Adobe 这样的网络公司,到美国宇航局喷气推进实验室、美国人口普查局等政府机构(www.elastic.co/customers/)。
当我们谈论 Elastic 时,我们指的是 Elastic Stack 背后的公司。这些工具是开源的,公司通过销售支持、托管解决方案和围绕开源项目的咨询服务来赚钱。公司的股票在纽约证券交易所上市,股票代码为 ESTC。
现在我们对 ELK Stack 有了一个更好的了解,让我们看看本章的实验室拓扑。
实验室拓扑
对于网络实验室,我们将重用我们在 第八章,Python 网络监控第二部分 中使用的网络拓扑。网络设备将具有位于 192.168.2.0/24 管理网络的管理接口,以及位于 10.0.0.0/8 网络的互连和 /30s 子网。
我们在哪里可以安装 ELK Stack 到实验室?在生产环境中,我们应该在专用集群中运行 ELK Stack。然而,在我们的实验室中,我们可以通过 Docker 容器快速启动一个测试实例。如果需要 Docker 的复习,请参阅 第五章,网络工程师的 Docker 容器。
下面是我们网络实验室拓扑的图形表示:

图 13.2:实验室拓扑
| 设备 | 管理 IP | 环回 IP |
|---|---|---|
| r1 | 192.168.2.218 |
192.168.0.1 |
| r2 | 192.168.2.219 |
192.168.0.2 |
| r3 | 192.168.2.220 |
192.168.0.3 |
| r5 | 192.168.2.221 |
192.168.0.4 |
| r6 | 192.168.2.222 |
192.168.0.5 |
Ubuntu 主机信息如下:
| 设备名称 | 外部链接 Eth0 | 内部 IP Eth1 |
|---|---|---|
| 客户端 | 192.168.2.211 |
10.0.0.9 |
| 服务器 | 192.168.2.212 |
10.0.0.5 |
要运行多个容器,我们应该至少为宿主机分配 4 GB RAM 或更多。如果尚未启动,请先启动 Docker Engine,然后从 Docker Hub 拉取镜像:
$ sudo service docker start
$ docker network create elastic
$ docker pull docker.elastic.co/elasticsearch/elasticsearch:8.4.2
$ docker run --name elasticsearch –-rm -it --network elastic -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -t docker.elastic.co/elasticsearch/elasticsearch:8.4.2
当 Docker 容器运行时,生成的默认 Elastic 用户密码和 Kibana 注册令牌将输出到终端;请记住它们,因为我们稍后会用到。你可能需要稍微向上滚动屏幕以找到它们:
-> Password for the elastic user (reset with 'bin/elasticsearch-reset-password -u elastic'):
<password>
-> Configure Kibana to use this cluster:
* Run Kibana and click the configuration link in the terminal when Kibana starts.
* Copy the following enrollment token and paste it into Kibana in your browser (valid for the next 30 minutes):
<token>
一旦 Elasticsearch 容器启动,我们可以通过浏览到https://<你的 IP>:9200来测试实例:

图 13.3:Elasticsearch 初始结果
然后,我们可以从另一个终端拉取并运行 Kibana 容器镜像:
$ docker pull docker.elastic.co/kibana/kibana:8.4.2
$ docker run --name kibana –-rm -it --network elastic -p 5601:5601 docker.elastic.co/kibana/kibana:8.4.2
一旦 Kibana 启动,我们就可以通过端口 5601 访问它:

图 13.4:Kibana 启动页面
注意它正在请求我们之前记下的注册令牌。我们可以将其粘贴并点击配置 Elastic。它将提示我们输入令牌,该令牌现在显示在 Kibana 终端上。一旦认证通过,Kibana 将开始配置 Elastic:

图 13.5:配置 Elastic
最后,我们应该能够通过http://<ip>:5601访问 Kibana 界面。目前我们不需要任何集成;我们将选择自行探索:

图 13.6:
我们将看到一个选项来加载一些示例数据。这是了解这个工具的好方法,所以让我们导入这些数据:

图 13.7:Kibana 主页
我们将选择尝试示例数据并添加示例电子商务订单、示例航班数据和示例网络日志:

图 13.8:添加示例数据
总结一下,我们现在已经在管理主机上以转发端口的形式运行了 Elasticsearch 和 Kibana 容器:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f7d6d8842060 docker.elastic.co/kibana/kibana:8.4.2 "/bin/tini -- /usr/l…" 42 minutes ago Up 42 minutes 0.0.0.0:5601->5601/tcp, :::5601->5601/tcp kibana
dc2a1fa15e3b docker.elastic.co/elasticsearch/elasticsearch:8.4.2 "/bin/tini -- /usr/l…" 46 minutes ago Up 46 minutes 0.0.0.0:9200->9200/tcp, :::9200->9200/tcp, 0.0.0.0:9300->9300/tcp, :::9300->9300/tcp elasticsearch
太好了!我们几乎完成了。最后一部分是 Logstash。由于我们将使用不同的 Logstash 配置文件、模块和插件,我们将使用软件包而不是 Docker 容器在管理主机上安装它。Logstash 需要 Java 来运行:
$ sudo apt install openjdk-11-jre-headless
$ java --version
openjdk 11.0.16 2022-07-19
OpenJDK Runtime Environment (build 11.0.16+8-post-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 11.0.16+8-post-Ubuntu-0ubuntu122.04, mixed mode, sharing)
我们可以下载 Logstash 捆绑包:
$ wget https://artifacts.elastic.co/downloads/logstash/logstash-8.4.2-linux-x86_64.tar.gz
$ tar -xvzf logstash-8.4.2-linux-x86_64.tar.gz
$ cd logstash-8.4.2/
我们将修改 Logstash 配置文件中的几个字段:
$ vim config/logstash.yml
# change the following fields
node.name: mastering-python-networking
api.http.host: <your host ip>
api.http.port: 9600-9700
我们现在不会启动 Logstash。我们将等待在本章的后面安装与网络相关的插件并创建必要的配置文件后,再启动 Logstash 进程。
让我们在下一节中花点时间看看如何将 ELK Stack 作为托管服务进行部署。
作为服务的 Elastic Stack
Elasticsearch 是一种流行的服务,由 Elastic.co 和其他云提供商提供托管选项。Elastic Cloud (www.elastic.co/cloud/) 没有自己的基础设施,但它提供了在 AWS、Google Cloud Platform 或 Azure 上启动部署的选项。由于 Elastic Cloud 是基于其他公共云 VM 提供的,因此成本将略高于直接从云提供商(如 AWS)获取:

图 13.9:Elastic Cloud 产品
AWS 提供了与现有 AWS 产品紧密集成的托管 OpenSearch 产品 (aws.amazon.com/opensearch-service/)。例如,AWS CloudWatch 日志可以直接流式传输到 AWS OpenSearch 实例 (docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_OpenSearch_Stream.html )。
从我的个人经验来看,尽管 Elastic Stack 因其优势而具有吸引力,但它是一个容易上手但如果没有陡峭的学习曲线则难以扩展的项目。如果我们不每天处理 Elasticsearch,学习曲线将更加陡峭。如果你像我一样,想利用 Elastic Stack 提供的功能,但又不想成为全职的 Elastic 工程师,我强烈建议使用托管选项进行生产。
选择哪个托管提供商取决于你对云提供商锁定和是否想使用最新功能的偏好。由于 Elastic Cloud 是由 Elastic Stack 项目背后的团队开发的,因此他们通常比 AWS 更快地提供最新功能。另一方面,如果你的基础设施完全建立在 AWS 云中,拥有一个紧密集成的 OpenSearch 实例可以节省你维护单独集群所需的时间和精力。
让我们在下一节中查看一个从数据摄取到可视化的端到端示例。
第一个端到端示例
新手对 Elastic Stack 最常见的反馈之一是需要了解多少细节才能开始。要在 Elastic Stack 中获得第一个可用的记录,用户需要构建一个集群,分配主节点和数据节点,摄取数据,创建索引,并通过网页或命令行界面进行管理。多年来,Elastic Stack 简化了安装过程,改进了其文档,并为新用户创建了示例数据集,以便在使用堆栈进行生产之前熟悉工具。
在 Docker 容器中运行组件有助于减轻安装的一些痛苦,但增加了维护的复杂性。在虚拟机与容器之间运行它们是一种权衡。
在我们深入探讨 Elastic Stack 的不同组件之前,查看一个跨越 Logstash、Elasticsearch 和 Kibana 的示例是有帮助的。通过回顾这个端到端示例,我们将熟悉每个组件提供的功能。当我们在本章的后面更详细地查看每个组件时,我们可以将特定组件在整体图景中的位置进行分类。
让我们首先将我们的日志数据放入 Logstash。我们将配置每个路由器将日志数据导出到 Logstash 服务器:
r[1-6]#sh run | i logging
logging host <logstash ip> vrf Mgmt-intf transport udp port 5144
在我们的 Elastic Stack 主机上,所有组件都已安装完毕后,我们将创建一个简单的 Logstash 配置,该配置监听 UDP 端口 5144,并将数据以 JSON 格式输出到控制台以及 Elasticsearch 主机:
echou@elk-stack-mpn:~$ cd logstash-8.4.2/
echou@elk-stack-mpn:~/logstash-8.4.2$ mkdir network_configs
echou@elk-stack-mpn:~/logstash-8.4.2$ touch network_configs/simple_config.cfg
echou@elk-stack-mpn:~/logstash-8.4.2$ cat network_configs/simple_config.conf
input {
udp {
port => 5144
type => "syslog-ios"
}
}
output {
stdout { codec => json }
elasticsearch {
hosts => ["https://<elasticsearch ip>:9200"]
ssl => true
ssl_certificate_verification => false
user => "elastic"
password => "<password>"
index => "cisco-syslog-%{+YYYY.MM.dd}"
}
}
配置文件仅包含一个输入部分和一个输出部分,不修改数据。类型 syslog-ios 是我们选择的用于识别此索引的名称。在 output 部分中,我们使用代表今天日期的变量配置索引名称。我们可以直接从二进制目录在前台运行 Logstash 进程:
$ ./bin/logstash -f network_configs/simple_config.conf
Using bundled JDK: /home/echou/Mastering_Python_Networking_Fourth_Edition/logstash-8.4.2/jdk
[2022-09-23T13:46:25,876][INFO ][logstash.inputs.udp ][main][516c12046954cb8353b87ba93e5238d7964349b0fa7fa80339b72c6baca637bb] UDP listener started {:address=>"0.0.0.0:5144", :receive_buffer_bytes=>"106496", :queue_size=>"2000"}
<skip>
默认情况下,Elasticsearch 允许在向其发送数据时自动生成索引。我们可以在路由器上通过重置接口、重新加载 BGP 或简单地进入配置模式并退出来生成一些日志数据。一旦生成了一些新的日志,我们将看到创建的 cisco-syslog-<date> 索引:
{"@timestamp":"2022-09-23T20:48:31.354Z", "log.level": "INFO", "message":"[cisco-syslog-2022.09.23/B7PH3hxNSHqAegikXyp9kg] create_mapping", "ecs.version": "1.2.0","service.name":"ES_ECS","event.dataset":"elasticsearch.server","process.thread.name":"elasticsearch[24808013b64b][masterService#updateTask][T#1]","log.logger":"org.elasticsearch.cluster.metadata.MetadataMappingService","elasticsearch.cluster.uuid":"c-j9Dg8YTh2PstO3JFP9AA","elasticsearch.node.id":"Pa4x3YJ-TrmFn5Pb2tObVw","elasticsearch.node.name":"24808013b64b","elasticsearch.cluster.name":"docker-cluster"}
在这一点上,我们可以快速执行 curl 命令来查看在 Elasticsearch 上创建的索引。curl 命令使用 insecure 标志来适应自签名证书。URL 格式为“https://<username>:<password>@<ip><port>/<path>。"_cat/indices/cisco*"` 显示索引类别,然后匹配索引名称:
$ curl -X GET --insecure "https://elastic:-Rel0twWMUk8L-ZtZr=I@192.168.2.126:9200/_cat/indices/cisco*"
yellow open cisco-syslog-2022.09.23 B7PH3hxNSHqAegikXyp9kg 1 1 9 0 21kb 21kb
我们现在可以通过转到 菜单 -> 管理 -> 堆栈管理 来使用 Kibana 创建索引:

图 13.10:堆栈管理
在 数据 -> 索引管理 下,我们可以看到新创建的 cisco-syslog 索引:

图 13.11:索引管理
我们现在可以转到 堆栈管理 -> Kibana -> 数据视图 来创建数据视图。

图 13.12:创建新数据视图步骤 1
由于索引已经在 Elasticsearch 中,我们只需匹配索引名称。请记住,我们的索引名称是基于时间的变量;我们可以使用星号通配符 (*) 来匹配所有以 cisco-syslog 开头的当前和未来索引:

图 13.13:创建新数据视图步骤 2
我们的索引是基于时间的,也就是说,我们有一个可以用作时间戳的字段,并且我们可以根据时间进行搜索。我们应该指定我们指定的用作时间戳的字段。在我们的例子中,Elasticsearch 已经足够智能,可以从我们的 syslog 中选择一个字段作为时间戳;我们只需要在第二步中选择它从下拉菜单中。
在索引模式创建后,我们可以使用 菜单 -> 发现(在 分析 下)选项卡来查看条目。确保你选择了正确的索引和时间范围:

图 13.14:Elasticsearch 索引文档发现
在我们收集了一些更多的日志信息之后,我们可以通过在 Logstash 进程上使用 Ctrl + C 来停止 Logstash 进程。这个第一个例子展示了我们如何利用 Elastic Stack 管道从数据摄取、存储和可视化。在 Logstash(或 Beats)中使用的数据摄取是一个连续的数据流,它会自动流入 Elasticsearch。Kibana 可视化工具为我们提供了一种更直观地分析 Elasticsearch 中的数据的方法,一旦我们对结果满意,就可以创建一个永久的可视化。我们可以使用 Kibana 创建更多的可视化图表,我们将在本章后面看到更多示例。
即使只有一个例子,我们也可以看到工作流程中最重要的部分是 Elasticsearch。它简单的 RESTful 接口、存储可伸缩性、自动索引和快速搜索结果赋予了堆栈适应我们网络分析需求的能力。
在下一节中,我们将探讨如何使用 Python 与 Elasticsearch 进行交互。
使用 Python 客户端连接到 Elasticsearch
我们可以通过 Python 库与 Elasticsearch 进行交互。例如,在下面的例子中,我们将使用 requests 库执行 GET 操作以从 Elasticsearch 主机检索信息。例如,我们知道以下 URL 端点的 HTTP GET 可以检索以 kibana 开头的当前索引:
$ curl -X GET --insecure "https://elastic:-Rel0twWMUk8L-ZtZr=I@192.168.2.126:9200/_cat/indices/kibana*"
green open kibana_sample_data_ecommerce QcLgMu7CTEKNjeJeBxaD3w 1 0 4675 0 4.2mb 4.2mb
green open kibana_sample_data_logs KPcJfMoSSaSs-kyqkuspKg 1 0 14074 0 8.1mb 8.1mb
green open kibana_sample_data_flights q8MkYKooT8C5CQzbMMNTpg 1 0 13059 0 5.8mb 5.8mb
我们可以使用 requests 库在 Python 脚本中创建类似的功能,Chapter13_1.py:
#!/usr/bin/env python3
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
# disable https verification check warning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
def current_indices_list(es_host, index_prefix):
current_indices = []
http_header = {'content-type': 'application/json'}
response = requests.get(es_host + "/_cat/indices/" + index_prefix + "*", headers=http_header, verify=False)
for line in response.text.split('\n'):
if line:
current_indices.append(line.split()[2])
return current_indices
if __name__ == "__main__":
username = 'elastic'
password = '-Rel0twWMUk8L-ZtZr=I'
es_host = 'https://'+username+':'+password+'@192.168.2.126:9200'
indices_list = current_indices_list(es_host, 'kibana')
print(indices_list)
执行脚本将给我们一个以 kibana 开头的索引列表:
$ python Chapter13_1.py
['kibana_sample_data_ecommerce', 'kibana_sample_data_logs', 'kibana_sample_data_flights']
我们还可以使用 Python Elasticsearch 客户端,elasticsearch-py.readthedocs.io/en/master/。它被设计为一个围绕 Elasticsearch RESTful API 的薄包装,以允许最大灵活性。让我们安装它并运行一个简单的示例:
(venv) $ pip install elasticsearch
示例 Chapter13_2 简单地连接到 Elasticsearch 集群,并搜索以 kibana 开头的索引:
#!/usr/bin/env python3
from elasticsearch import Elasticsearch
es_host = Elasticsearch(["https://elastic:-Rel0twWMUk8L-ZtZr=I@192.168.2.126:9200/"],
ca_certs=False, verify_certs=False)
res = es_host.search(index="kibana*", body={"query": {"match_all": {}}})
print("Hits Total: " + str(res['hits']['total']['value']))
默认情况下,结果将返回前 10,000 条条目:
$ python Chapter13_2.py
Hits Total: 10000
使用简单的脚本,客户端库的优势并不明显。然而,当我们需要创建更复杂的搜索操作,例如需要使用每次查询返回的令牌来继续执行后续查询直到所有结果返回的滚动操作时,客户端库非常有帮助。客户端还可以帮助处理更复杂的行政任务,例如当我们需要重新索引现有索引时。我们将在本章剩余部分看到更多使用客户端库的示例。
在下一节中,我们将查看更多来自我们的 Cisco 设备 syslog 的数据摄取示例。
使用 Logstash 进行数据摄取
在最后一个示例中,我们使用 Logstash 从网络设备摄取日志数据。让我们在此基础上构建示例,并在network_config/config_2.cfg中添加一些额外的配置更改:
input {
udp {
port => 5144
type => "syslog-core"
}
udp {
port => 5145
type => "syslog-edge"
}
}
filter {
if [type] == "syslog-edge" {
grok {
match => { "message" => ".*" }
add_field => [ "received_at", "%{@timestamp}" ]
}
}
}
output {
stdout { codec => json }
elasticsearch {
hosts => ["https://192.168.2.126:9200"]
<skip>
}
}
在输入部分,我们将监听两个 UDP 端口,5144和5145。当接收到日志时,我们将使用syslog-core或syslog-edge对日志条目进行标记。我们还将向配置中添加一个过滤器部分,以特别匹配syslog-edge类型,并在消息部分应用正则表达式部分Grok。在这种情况下,我们将匹配所有内容,并添加一个额外的字段received_at,其值为时间戳。
关于 Grok 的更多信息,请参阅以下文档:www.elastic.co/guide/en/logstash/current/plugins-filters-grok.html.
我们将把r5和r6改为将 syslog 信息发送到 UDP 端口5145:
r[5-6]#sh run | i logging
logging host 192.168.2.126 vrf Mgmt-intf transport udp port 5145
当我们启动 Logstash 服务器时,我们会看到两个端口现在都在监听:
$ ./bin/logstash -f network_configs/config_2.conf
<skip>
[2022-09-23T14:50:42,097][INFO ][logstash.inputs.udp ][main][212f078853a453d3d8a5d8c1df268fd628577245cd1b66acb06b9e1cb1ff8a10] UDP listener started {:address=>"0.0.0.0:5144", :receive_buffer_bytes=>"106496", :queue_size=>"2000"}
[2022-09-23T14:50:42,106][INFO ][logstash.inputs.udp ][main][6c3825527b168b167846f4ca7dea5ef55e1437753219866bdcc2eb51aee53c84] UDP listener started {:address=>"0.0.0.0:5145", :receive_buffer_bytes=>"106496", :queue_size=>"2000"}
通过使用不同类型分离条目,我们可以在 Kibana Discover仪表板中特别搜索这些类型:

图 13.15:Syslog 索引
如果我们扩展具有syslog-edge类型的条目,我们可以看到我们添加的新字段:

图 13.16:Syslog 时间戳
Logstash 配置文件在输入、过滤和输出部分提供了许多选项。特别是,过滤部分提供了我们通过选择性匹配数据并在输出到 Elasticsearch 之前进一步处理数据来增强数据的方法。Logstash 可以通过模块进行扩展;每个模块都提供了一个快速端到端解决方案,用于摄取数据和可视化,并具有专门构建的仪表板。
关于 Logstash 模块的更多信息,请参阅以下文档:www.elastic.co/guide/en/logstash/8.4/logstash-modules.html .
Elastic Beats 与 Logstash 模块类似。它们是单一用途的数据传输工具,通常作为代理安装,用于收集主机上的数据并将输出数据直接发送到 Elasticsearch 或 Logstash 进行进一步处理。
有数百种不同的可下载 Beats,例如 Filebeat、Metricbeat、Packetbeat、Heartbeat 等。在下一节中,我们将看到如何使用 Filebeat 将 Syslog 数据摄取到 Elasticsearch 中。
使用 Beats 进行数据摄取
尽管 Logstash 功能强大,但数据摄取过程可能会变得复杂且难以扩展。如果我们以网络日志为例进行扩展,我们可以看到,即使只是网络日志,尝试解析来自 IOS 路由器、NXOS 路由器、ASA 防火墙、Meraki 无线控制器等不同日志格式也可能变得复杂。如果我们需要摄取来自 Apache 网络日志、服务器主机健康和安全信息的日志数据呢?至于像 NetFlow、SNMP 和计数器这样的数据格式呢?我们需要聚合的数据越多,情况可能就越复杂。
虽然我们无法完全摆脱聚合和数据摄取的复杂性,但当前的趋势是向更轻量级、单一用途的代理移动,该代理尽可能靠近数据源。例如,我们可以在我们的 Apache 服务器上安装专门收集网络日志数据的数据收集代理;或者我们可以有一个只收集、聚合和组织 Cisco IOS 日志的主机。Elastic Stack 将这些轻量级数据传输工具统称为 Beats:www.elastic.co/products/beats。
Filebeat 是 Elastic Beats 软件的一个版本,旨在转发和集中日志数据。它寻找配置中指定的要收集的日志文件;一旦处理完成,它将新的日志数据发送到聚合事件并输出到 Elasticsearch 的底层进程。在本节中,我们将探讨如何使用 Filebeat 与 Cisco 模块收集网络日志数据。
让我们安装 Filebeat 并设置带有捆绑可视化模板和索引的 Elasticsearch 主机:
$ $ curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-8.4.2-amd64.deb
$ sudo dpkg -i filebeat-8.4.2-amd64.deb
目录布局可能会让人困惑,因为它们被安装在不同的/usr、/etc和/var位置:

图 13.17:Elastic Filebeat 文件位置(来源:https://www.elastic.co/guide/en/beats/filebeat/8.4/directory-layout.html)
我们将对配置文件/etc/filebeat/filebeat.yml进行一些更改,以设置 Elasticsearch 和 Kibana 的位置:
output.elasticsearch:
# Array of hosts to connect to.
hosts: ["192.168.2.126:9200"]
# Protocol - either 'http' (default) or 'https'.
protocol: "https"
# Authentication credentials - either API key or username/password.
username: "elastic"
password: "changeme"
ssl.verification_mode: none
setup.kibana:
host: "192.168.2.126:5601"
可以使用 Filebeat 来设置索引模板和示例 Kibana 仪表板:
$ sudo filebeat setup --index-management -E output.logstash.enabled=false -E 'output.elasticsearch.hots=["https://elastic:-Rel0twWMUk8L-ZtZr=I@192.168.2.126:9200/"]'
$ sudo filebeat setup –dashboards
让我们为 Filebeat 启用cisco模块:
$ sudo filebeat modules enable cisco
Enabled cisco
让我们先为syslog配置cisco模块。该文件位于/etc/filebeat/modules.d/cisco.yml下。在我们的例子中,我还指定了一个自定义的日志文件位置:
- module: cisco
ios:
enabled: true
var.input: syslog
var.syslog_host: 0.0.0.0
var.syslog_port: 514
var.paths: ['/home/echou/syslog/my_log.log']
我们可以使用常见的 Ubuntu Linux 命令service Filebeat来启动、停止和检查 Filebeat 服务的状态start|stop|status:
$ sudo service filebeat start
$ sudo service filebeat status
● filebeat.service - Filebeat sends log files to Logstash or directly to Elasticsearch.
Loaded: loaded (/lib/systemd/system/filebeat.service; disabled; vendor preset: enabled)
Active: active (running) since Fri 2022-09-23 16:06:09 PDT; 3s ago
<skip>
在我们的设备上修改或添加 UDP 端口514用于 syslog。我们应该能够在filebeat-*索引搜索下看到 syslog 信息:

图 13.18:Elastic Filebeat 索引
如果我们将它与之前的 syslog 示例进行比较,我们可以看到与每条记录关联的字段和元信息要多得多,例如agent.version、event.code和event.severity:

图 13.19:Elastic Filebeat Cisco 日志
为什么额外的字段很重要?除了其他优点外,字段使得搜索聚合更加容易,而这反过来又使我们能够更好地绘制结果。我们将在下一节中看到绘制示例,其中我们将讨论 Kibana。
除了cisco模块外,还有针对 Palo Alto Networks、AWS、Google Cloud、MongoDB 等许多模块。最新的模块列表可以在www.elastic.co/guide/en/beats/filebeat/8.4/filebeat-modules.html查看。
如果我们想监控 NetFlow 数据呢?没问题,有一个模块可以做到这一点!我们将通过启用模块并设置仪表板来运行与 Cisco 模块相同的流程:
$ sudo filebeat modules enable netflow
$ sudo filebeat setup -e
然后,配置模块配置文件,/etc/filebeat/modules.d/netflow.yml:
- module: netflow
log:
enabled: true
var:
netflow_host: 0.0.0.0
netflow_port: 2055
我们将配置设备将 NetFlow 数据发送到端口2055。如果您需要复习,请阅读第八章,使用 Python 进行网络监控 – 第二部分中的相关配置。我们应该能够看到新的netflow数据输入类型:

图 13.20:Elastic NetFlow 输入
记住每个模块都预装了可视化模板吗?不要过多地跳入可视化,但如果我们点击左侧面板上的可视化选项卡,然后搜索netflow,我们可以看到为我们创建的一些可视化:

图 13.21:Kibana 可视化
点击会话伙伴 [Filebeat Netflow]选项,这将给我们一个很好的表格,我们可以通过每个字段重新排序顶级对话者:

图 13.22:Kibana 表格
在下一节中,我们将专注于 ELK 堆栈的 Elasticsearch 部分。
使用 Elasticsearch 进行搜索
我们需要在 Elasticsearch 中添加更多数据,以便使搜索和图形更加有趣。我建议重新加载几个实验室设备,以便有接口重置、BGP 和 OSPF 建立以及设备启动消息的日志条目。否则,请随意使用本章开头导入的样本数据来处理本节。
如果我们回顾一下Chapter13_2.py脚本示例,当我们搜索时,有两项信息可能会因每个查询而异:索引和查询体。我通常喜欢将这些信息分解为可以在运行时动态更改的输入变量,以分离搜索逻辑和脚本本身。让我们创建一个名为query_body_1.json的文件:
{
"query": {
"match_all": {}
}
}
我们将创建一个名为Chapter13_3.py的脚本,该脚本使用argparse在命令行接收用户输入:
import argparse
parser = argparse.ArgumentParser(description='Elasticsearch Query Options')
parser.add_argument("-i", "--index", help="index to query")
parser.add_argument("-q", "--query", help="query file")
args = parser.parse_args()
我们可以使用这两个输入值以与我们之前相同的方式构建搜索:
# load elastic index and query body information
query_file = args.query
with open(query_file) as f:
query_body = json.loads(f.read())
# Elasticsearch instance
es_host = Elasticsearch(["https://elastic:<pass> @192.168.2.126:9200/"],
ca_certs=False, verify_certs=False)
# Query both index and put into dictionary
index = args.index
res = es.search(index=index, body=query_body)
print(res['hits']['total']['value'])
我们可以使用help选项查看应该与脚本一起提供的参数。以下是当我们使用相同的查询针对我们创建的两个不同索引时的结果:
$ python Chapter13_3.py --help
usage: Chapter12_3.py [-h] [-i INDEX] [-q QUERY]
Elasticsearch Query Options
optional arguments:
-h, --help show this help message and exit
-i INDEX, --index INDEX
index to query
-q QUERY, --query QUERY
query file
$ python3 Chapter13_3.py -q query_body_1.json -i "cisco*"
50
$ python3 Chapter13_3.py -q query_body_1.json -i "filebeat*"
10000
在开发我们的搜索时,通常需要尝试几次才能得到我们想要的结果。Kibana 提供的一个工具是开发者控制台,它允许我们在同一页面上玩转搜索条件并查看搜索结果。该工具位于菜单部分Management for Dev Tools。
例如,在下面的图中,我们执行了我们现在所做的相同搜索,并能够看到返回的 JSON 结果。这是我在 Kibana 界面上的最爱工具之一:

图 13.23:Kibana Dev Tools
大多数网络数据都是基于时间的,例如我们收集的日志和 NetFlow 数据。这些值是在某个时间点拍摄的快照,我们可能会在时间范围内分组这些值。例如,我们可能想知道,“在过去的 7 天里,谁是 NetFlow 顶级通信者?”或者“在过去 1 小时内,哪个设备有最多的 BGP 重置消息?”这些问题大多与聚合和时间范围有关。让我们看看一个限制时间范围的查询,query_body_2.json:
{
"query": {
"bool": {
"filter": [
{
"range": {
"@timestamp": {
"gte": "now-10m"
}
}
}
]
}
}
}
这是一个布尔查询,www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html,这意味着它可以组合其他查询。在我们的查询中,我们使用过滤器将时间范围限制为过去 10 分钟。我们将Chapter13_3.py脚本复制到Chapter13_4.py,并修改输出以获取命中次数以及遍历实际返回的结果列表:
<skip>
res = es.search(index=index, body=query_body)
print("Total hits: " + str(res['hits']['total']['value']))
for hit in res['hits']['hits']:
pprint(hit)
执行脚本将显示我们在过去 10 分钟内只有23次命中:
$ python Chapter13_4.py -i "filebeat*" -q query_body_2.json
Total hits: 23
我们可以在查询中添加另一个过滤器选项,通过query_body_3.json限制源 IP:
{
"query": {
"bool": {
"must": {
"term": {
"source.ip": "192.168.0.1"
}
},
<skip>
结果将受到 r1 的 loopback IP 在过去 10 分钟内的源 IP 的限制:
$ python Chapter12_4.py -i "filebeat*" -q query_body_3.json
Total hits: 18
让我们再次修改搜索主体,添加一个聚合,www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket.html,它从我们之前的搜索中计算所有网络字节的总和:
{
"aggs": {
"network_bytes_sum": {
"sum": {
"field": "network.bytes"
}
}
},
<skip>
}
每次运行脚本 Chapter13_5.py 时,结果都会不同。当我连续运行脚本时,当前结果大约为 1 MB:
$ python Chapter13_5.py -i "filebeat*" -q query_body_4.json
1089.0
$ python Chapter13_5.py -i "filebeat*" -q query_body_4.json
990.0
如您所见,构建搜索查询是一个迭代的过程;您通常从一个广泛的网络开始,并逐渐缩小标准以微调结果。一开始,您可能会花很多时间阅读文档并搜索确切的语法和过滤器。随着您经验的积累,搜索语法将变得更加容易。回到我们从 NetFlow 模块设置中看到的上一个可视化,我们可以使用检查工具来查看 请求 主体:

图 13.24:Kibana 请求
我们可以将它放入查询 JSON 文件 query_body_5.json 中,并使用 Chapter13_6.py 文件执行它。我们将收到图表基于的原始数据:
$ python Chapter13_6.py -i "filebeat*" -q query_body_5.json
{'1': {'value': 8156040.0}, 'doc_count': 8256, 'key': '10.0.0.5'}
{'1': {'value': 4747596.0}, 'doc_count': 103, 'key': '172.16.1.124'}
{'1': {'value': 3290688.0}, 'doc_count': 8256, 'key': '10.0.0.9'}
{'1': {'value': 576446.0}, 'doc_count': 8302, 'key': '192.168.0.2'}
{'1': {'value': 576213.0}, 'doc_count': 8197, 'key': '192.168.0.1'}
{'1': {'value': 575332.0}, 'doc_count': 8216, 'key': '192.168.0.3'}
{'1': {'value': 433260.0}, 'doc_count': 6547, 'key': '192.168.0.5'}
{'1': {'value': 431820.0}, 'doc_count': 6436, 'key': '192.168.0.4'}
在下一节中,让我们更深入地了解 Elastic Stack 的可视化部分:Kibana。
使用 Kibana 进行数据可视化
到目前为止,我们已经使用 Kibana 来发现数据,管理 Elasticsearch 中的索引,使用开发者工具来开发查询,以及使用一些其他功能。我们还看到了来自 NetFlow 的预填充可视化图表,它为我们提供了数据中的顶级对话对。在本节中,我们将逐步介绍创建我们自己的图表的步骤。我们将从创建一个饼图开始。
饼图非常适合可视化组件的一部分相对于整体的部分。让我们基于 Filebeat 索引创建一个饼图,根据记录计数绘制前 10 个源 IP 地址。我们将选择 仪表板 -> 创建仪表板 -> 创建可视化 -> 饼图:

图 13.25:Kibana 饼图
然后,我们在搜索栏中输入 netflow 以选择我们的 [Filebeat NetFlow] 索引:

图 13.26:Kibana 饼图来源
默认情况下,我们得到了默认时间范围内所有记录的总数。时间范围可以动态更改:

图 13.27:Kibana 时间范围
我们可以为图表分配一个自定义标签:

图 13.28:Kibana 图表标签
让我们点击添加选项来添加更多桶。我们将选择分割切片,选择聚合的术语,并从下拉菜单中选择source.ip字段。我们将保持顺序为降序,但将大小增加到10。
只有当你点击顶部应用按钮时,更改才会生效。在使用现代网站时,期望更改实时发生而不是通过点击应用按钮是一种常见的错误:

图 13.29:Kibana 播放按钮
我们可以点击顶部的选项来关闭环形图并打开显示标签:

图 13.30:Kibana 图表选项
最终的图表是一个展示基于文档计数数量的顶级 IP 来源的饼图:

图 13.31:Kibana 饼图
与 Elasticsearch 一样,Kibana 的图表也是一个迭代过程,通常需要尝试几次才能正确设置。如果我们把结果分成不同的图表而不是同一图表上的切片呢?是的,这并不非常直观:

图 13.32:Kibana 分割图表
让我们坚持在同一饼图上分割切片,并将时间范围更改为过去 1 小时,然后保存图表以便稍后返回:
注意,我们还可以通过嵌入的 URL(如果 Kibana 可以从共享位置访问)或快照来共享图表:

图 13.33:Kibana 保存图表
我们还可以使用度量操作做更多的事情。例如,我们可以选择数据表图表类型,并使用源 IP 重复之前的桶分解。但我们还可以通过添加每个桶的网络字节总数来添加第二个度量:

图 13.34:Kibana 度量
结果是一个表格,显示了文档计数数量以及网络字节的总和。这可以以 CSV 格式下载以供本地存储:

图 13.35:Kibana 表格
Kibana 是 Elastic Stack 中一个非常强大的可视化工具。我们只是触及了其可视化功能的表面。除了许多其他图表选项,以更好地讲述您数据的故事外,我们还可以将多个可视化组合到一个仪表板上进行展示。我们还可以使用 Timelion (www.elastic.co/guide/en/kibana/8.4/timelion.html) 将独立的数据源组合成单个可视化,或者使用 Canvas (www.elastic.co/guide/en/kibana/current/canvas.html) 作为基于 Elasticsearch 数据的演示工具。
Kibana 通常用于工作流程的末端,以有意义的方式展示我们的数据。我们在本章范围内涵盖了从数据摄取到存储、检索和可视化的基本工作流程。令人惊讶的是,借助如 Elastic Stack 这样的集成开源堆栈,我们可以在短时间内完成这么多工作。
摘要
在本章中,我们使用了 Elastic Stack 来摄取、分析和可视化网络数据。我们使用了 Logstash 和 Beats 来摄取网络系统日志和 NetFlow 数据。然后我们使用 Elasticsearch 对数据进行索引和分类,以便于检索。最后,我们使用 Kibana 来可视化数据。我们使用 Python 与该堆栈交互,帮助我们更深入地了解我们的数据。Logstash、Beats、Elasticsearch 和 Kibana 一起构成了一个强大的全能型项目,可以帮助我们更好地理解我们的数据。
在下一章中,我们将探讨如何使用 Python 结合 Git 进行网络开发。
加入我们的书籍社区
要加入这本书的社区——在那里您可以分享反馈、向作者提问,并了解新书发布——请扫描下面的二维码:
packt.link/networkautomationcommunity

第十四章:使用 Git
我们使用 Python、Ansible 和其他许多工具在网络的自动化方面进行了工作。在您跟随本书前 13 章的示例中,我们已经使用了包含超过 5,300 行代码的 150 多个文件。这对于在阅读本书之前主要使用命令行界面的网络工程师来说已经相当不错了!有了我们新的脚本和工具,我们准备出去征服我们的网络任务,对吧?但是,慢着,我的网络忍者们。
在我们深入任务的核心之前,有几件事情我们需要考虑。我们将逐一讨论这些考虑因素,并探讨版本控制(或源代码控制)系统 Git 如何帮助我们处理这些问题。
我们将涵盖以下主题:
-
内容管理考虑因素与 Git
-
Git 简介
-
设置 Git
-
Git 使用示例
-
Git 与 Python
-
自动化配置备份
-
与 Git 协作
首先,让我们谈谈这些考虑因素的具体内容以及 Git 在帮助我们管理这些因素中能扮演的角色。
内容管理考虑因素与 Git
在创建代码文件时,我们必须考虑的第一件事是如何将它们保存在一个我们可以检索和使用的地方,其他人也可以。理想情况下,这个位置应该是文件唯一中央存放的地方,同时如果需要的话,也应该有备份副本。在代码的初始发布之后,我们可能会添加新功能和修复错误,因此我们希望有一种方法来跟踪这些变更,并保持最新的版本可供下载。如果新的变更不起作用,我们希望有方法可以回滚变更,并在文件的历史记录中反映差异。这将给我们一个关于代码文件演变的良好概念。
第二个问题是关于团队成员之间的协作过程。如果我们与其他网络工程师一起工作,我们很可能会需要集体处理文件。这些可以是 Python 脚本、Ansible Playbooks、Jinja2 模板、INI 风格的配置文件等等。重点是任何基于文本的文件都应该被跟踪,以便团队成员都能看到多个输入。
第三个问题是责任归属。一旦我们有一个允许多个输入和变更的系统,我们需要用适当的记录来标记这些变更,以反映变更的所有者。这个记录还应包括变更的简要原因,以便审查历史记录的人能够理解变更的原因。
这些是版本控制系统(如 Git)试图解决的一些主要挑战。公平地说,版本控制的过程可以存在于除专用软件系统之外的形式。例如,如果我打开我的 Microsoft Word 程序,文件会不断自动保存,我可以回到过去查看变更或回滚到之前的版本。这是一种版本控制的形式;然而,Word 文档很难在我的笔记本电脑之外进行扩展。在本章中,我们关注的版本控制系统是一个独立的软件工具,其主要目的是跟踪软件变更。
软件工程中不缺乏不同的源代码管理工具,既有专有的也有开源的。一些流行的开源版本控制系统包括 CVS、SVN、Mercurial 和 Git。在本章中,我们将重点关注源代码管理系统 Git。我们在这本书中使用的许多软件都使用相同的版本控制系统来跟踪变更、协作开发功能和与用户沟通。我们将更深入地探讨这个工具。Git 是许多大型开源项目的默认版本控制系统,包括 Python 和 Linux 内核。
截至 2017 年 2 月,CPython 的开发过程已转移到 GitHub。自 2015 年 1 月以来,它一直在进行中。更多信息请查看 PEP 512:www.python.org/dev/peps/pep-0512。
在我们深入 Git 的工作示例之前,让我们看看 Git 系统的历史和优势。
Git 简介
Git 是由 Linux 内核的创造者林纳斯·托瓦兹(Linus Torvalds)在 2005 年 4 月创建的。他以幽默的口吻,亲昵地称这个工具为“来自地狱的信息管理器”。在 Linux 基金会的采访中,林纳斯提到,他觉得源代码管理在计算机世界中几乎是最无趣的事情([www.linuxfoundation.org/blog/2015/04/10-years-of-git-an-interview-with-git-creator-linus-torvalds/](https://web.archive.org/web/20210419173925/https://www.linuxfoundation.org/blog/2015/04/10-years-of-git-an-interview-with-git-creator-linus-torvalds/))。尽管如此,他在 Linux 内核开发者社区与当时使用的专有系统 BitKeeper 发生争执后创建了这款工具。
Git 这个名字代表什么?在英国英语俚语中,git 是一个侮辱性的词汇,表示一个令人不愉快、讨厌、幼稚的人。林纳斯以其幽默感表示,他是一个自负的家伙,并且他把所有的项目都命名为自己的名字。首先是 Linux,现在是 Git。然而,有人建议这个名字是 Global Information Tracker(GIT)的缩写。你可以判断你更喜欢哪种解释。
项目很快聚集在一起。在其创建后的大约 10 天(是的,你没看错),林纳斯觉得 Git 的基本想法是正确的,并开始使用 Git 提交第一个 Linux 内核代码。其余的,正如人们所说,就是历史。在创建超过十年后,它仍然满足 Linux 内核项目的所有预期。尽管许多开发者对切换源代码控制系统有固有的惰性,但它接管了许多其他开源项目的版本控制系统。对于 Python 代码库,在 Mercurial (https://hg.python.org/) 上托管代码多年后,该项目于 2017 年 2 月在 GitHub 上切换到 Git。
既然我们已经了解了 Git 的历史,让我们来看看它的一些好处。
Git 的好处
诸如 Linux 内核和 Python 这样的大型和分布式开源项目的托管成功,证明了 Git 的优势。我的意思是,如果这个工具足够好,可以用于世界上(在我看来)最受欢迎的操作系统和编程语言(再次,仅代表我个人观点)的软件开发,那么它可能也足够好,可以用于我的个人项目。
考虑到 Git 是一种相对较新的源代码管理工具,并且人们通常不会切换到新工具,除非它提供了相对于旧工具的显著优势,Git 的流行尤其显著。让我们来看看 Git 的一些好处:
-
分布式开发:Git 支持在离线私有存储库中进行并行、独立和同时开发。许多其他版本控制系统需要与中央存储库进行持续同步。Git 的分布式和离线特性为开发者提供了显著更大的灵活性。
-
扩展以处理数千名开发者:在许多开源项目的不同部分工作的开发者数量达到数千人。Git 支持可靠地整合他们的工作。
-
性能:林纳斯决心确保 Git 快速且高效。为了节省空间和传输时间,仅针对 Linux 内核代码的大量更新,就使用了压缩和差异检查来使 Git 快速且高效。
-
责任和不可变性:Git 对每个更改文件的提交强制执行更改日志,因此可以追踪所有更改及其背后的原因。Git 中的数据对象在创建并放入数据库后不能被修改,这使得它们不可变。这进一步强化了责任。
-
原子事务:由于不同的但相关的更改要么全部执行,要么完全不执行,因此确保了存储库的完整性。这将确保存储库不会被留下处于部分更改或损坏的状态。
-
完整存储库:每个存储库都包含每个文件的每个历史版本的完整副本。
-
自由,如同自由一样:Git 工具的起源源于 Linux 和 BitKeeper VCS 在软件是否应该是自由的以及是否应该基于原则拒绝商业软件之间的分歧,因此这个工具具有非常自由的用法许可。
在我们深入了解 Git 之前,让我们看看一些 Git 中使用的术语。
Git 术语
这里有一些我们应该熟悉的 Git 术语:
-
引用:以
refs开头的名称,指向一个对象。 -
仓库:这是一个包含项目所有信息、文件、元数据和历史的数据库。它包含所有对象集合的 refs 集合。
-
分支:这是一条活跃的开发线。最近的提交是该分支的
tip或HEAD。一个仓库可以有多个分支,但你的working tree或working directory只能关联到一个分支。这有时也被称为当前或checked out分支。 -
检出:这是将工作树的所有或部分更新到特定点的行为。
-
提交:这是 Git 历史中的一个时间点,或者它也可以意味着在仓库中存储一个新的快照。
-
合并:这是将另一个分支的内容合并到当前分支中的行为。例如,我正在将
development分支与master分支合并。 -
获取:这是从远程仓库获取内容的行为。
-
拉取:从仓库获取内容并合并。
-
标签:这是在仓库中某个时间点的标记,具有重大意义。
这不是一个完整的列表;请参考 Git 术语表,git-scm.com/docs/gitglossary,以获取更多术语及其定义。
最后,在进入 Git 的实际设置和使用之前,让我们谈谈 Git 和 GitHub 之间的重要区别;这是工程师在不熟悉这两个系统时容易忽视的。
Git 和 GitHub
Git 和 GitHub 并不相同。有时,对于初学者来说,这可能会造成混淆。Git 是一个版本控制系统,而 GitHub,github.com/,是一个集中式托管服务,用于 Git 仓库。GitHub 公司成立于 2008 年,于 2018 年被微软收购,但继续独立运营。
由于 Git 是一个去中心化系统,GitHub 存储了我们项目仓库的副本,就像任何其他分布式离线副本一样。我们通常将 GitHub 仓库指定为项目的中央仓库,所有其他开发者都将他们的更改推送到该仓库或从该仓库拉取。
2018 年,GitHub 被微软收购后blogs.microsoft.com/blog/2018/10/26/microsoft-completes-github-acquisition/,开发者社区中的许多人担心 GitHub 的独立性。正如新闻稿中所描述的,“GitHub 将保持以开发者为先的宗旨,独立运营,并继续作为一个开源平台。”GitHub 通过使用fork和pull请求机制,进一步将作为分布式系统中的集中式仓库这一理念付诸实践。对于托管在 GitHub 上的项目,项目维护者通常会鼓励其他开发者fork仓库,或者复制仓库,并在复制的仓库上工作。
修改完成后,他们可以向主项目发送一个pull请求,项目维护者可以审查这些更改,并在认为合适的情况下提交更改。GitHub 还增加了命令行之外的 Web 界面;这使得 Git 更加用户友好。
既然我们已经区分了 Git 和 GitHub,我们就可以开始正确地使用它们了!首先,让我们谈谈 Git 的设置。
设置 Git
到目前为止,我们一直在使用 Git 从 GitHub 下载文件。在本节中,我们将通过在本地设置 Git 来进一步操作,这样我们就可以开始提交我们的文件。在示例中,我将使用相同的 Ubuntu 22.04 LTS 管理主机。如果你使用的是不同的 Linux 版本或其他操作系统,快速搜索安装过程应该会带你找到正确的指令集。
如果你还没有这样做,请通过apt包管理工具安装 Git:
$ sudo apt update
$ sudo apt install -y git
$ git --version
git version 2.34.1
一旦安装了git,我们需要配置一些设置,以便我们的提交信息可以包含正确的信息:
$ git config --global user.name "Your Name"
$ git config --global user.email "email@domain.com"
$ git config --list
user.name=Your Name
user.email=email@domain.com
或者,你可以修改~/.gitconfig文件中的信息:
$ cat ~/.gitconfig
[user]
name = Your Name
email = email@domain.com
Git 中有许多我们可以更改的选项,但名字和电子邮件是那些允许我们提交更改而不会收到警告的选项。我个人喜欢使用 Vim 文本编辑器,而不是默认的 Emac,来编写提交信息:
(optional)
$ git config --global core.editor "vim"
$ git config --list
user.name=Your Name
user.email=email@domain.com
core.editor=vim
在我们继续使用 Git 之前,让我们回顾一下gitignore文件的概念。
Gitignore
有一些文件你不想让 Git 提交到 GitHub 或其他仓库,例如包含密码、API 密钥或其他敏感信息的文件。防止文件意外提交到仓库的最简单方法是,在仓库的顶级文件夹中创建一个.gitignore文件。Git 将使用gitignore文件来确定在提交之前应该忽略哪些文件和目录。应尽早将gitignore文件提交到仓库,并与其他用户共享。
想象一下,如果您不小心将您的组 API 密钥检查到公共 Git 存储库中,您会感到多么恐慌。通常,在创建新存储库时创建gitignore文件是有帮助的。实际上,当您在 GitHub 平台上创建存储库时,它提供了一个选项来做这件事。此文件可以包含特定语言的文件。例如,让我们排除 Python 的字节编译文件:
# Byte-compiled / optimized / DLL files
pycache /
*.py[cod]
*$py.class
我们还可以包含特定于您操作系统的文件:
# OSX
# =========================
.DS_Store
.AppleDouble
.LSOverride
您可以在 GitHub 的帮助页面了解更多关于.gitignore的信息:help.github.com/articles/ignoring-files/. 这里还有一些其他参考:
-
Gitignore 手册:
git-scm.com/docs/gitignore -
GitHub 的
.gitignore模板集合:github.com/github/gitignore -
Python 语言
.gitignore示例:github.com/github/gitignore/blob/master/Python.gitignore -
本书存储库的
.gitignore文件:github.com/PacktPublishing/Mastering-Python-Networking-Fourth-Edition/blob/main/.gitignore.
我认为.gitignore文件应该与任何新的存储库同时创建。这就是为什么这个概念要尽早引入。我们将在下一节中查看一些 Git 使用示例。
Git 使用示例
根据我的经验,当我们使用 Git 时,我们可能会使用命令行和各种选项。当我们需要回溯更改、查看日志和比较提交差异时,图形工具很有用,但我们很少在常规分支和提交中使用它们。我们可以通过使用help选项来查看 Git 的命令行选项:
$ git --help
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p | --paginate | --no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
<command> [<args>]
我们将创建一个repository并在存储库内部创建一个文件:
$ mkdir TestRepo-1
$ cd TestRepo-1/
$ git init
Initialized empty Git repository in /home/echou/Mastering_Python_Networking_third_edition/Chapter13/TestRepo-1/.git/
$ echo "this is my test file" > myFile.txt
当存储库使用 Git 初始化时,目录中添加了一个新的隐藏文件夹.git。它包含所有与 Git 相关的文件:
$ ls -a
. .. .git myFile.txt
$ ls .git/
branches config description HEAD hooks info objects refs
Git 在分层格式中从几个位置接收其配置。默认情况下,文件从system、global和repository读取。存储库的位置越具体,覆盖优先级越高。例如,存储库配置将覆盖全局配置。您可以使用git config -l命令来查看聚合的配置:
$ ls .git/config
.git/config
$ ls ~/.gitconfig
/home/echou/.gitconfig
$ git config -l
user.name=Eric Chou
user.email=<email>
core.editor=vim
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
当我们在存储库中创建文件时,它不会被跟踪。为了让git知道这个文件,我们需要添加这个文件:
$ git status
On branch master
Initial commit
Untracked files:
(use "git add <file>..." to include in what will be committed)
myFile.txt
nothing added to commit but untracked files present (use "git add" to track)
$ git add myFile.txt
$ git status
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: myFile.txt
当您添加文件时,它处于暂存状态。为了使更改正式化,我们需要提交这个更改:
$ git commit -m "adding myFile.txt"
[master (root-commit) 5f579ab] adding myFile.txt
1 file changed, 1 insertion(+)
create mode 100644 myFile.txt
$ git status
On branch master
nothing to commit, working directory clean
在最后一个例子中,我们在发出commit语句时提供了带有-m选项的commit消息。如果我们没有使用该选项,我们就会被带到页面去提供提交消息。在我们的场景中,我们配置了文本编辑器为 Vim,因此我们可以用它来编辑消息。
让我们对文件进行一些更改并再次commit。注意,在文件被更改后,Git 知道文件已被修改:
$ vim myFile.txt
$ cat myFile.txt
this is the second iteration of my test file
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: myFile.txt
$ git add myFile.txt
$ git commit -m "made modifications to myFile.txt"
[master a3dd3ea] made modifications to myFile.txt
1 file changed, 1 insertion(+), 1 deletion(-)
git commit编号是一个SHA-1 hash,这是一个重要特性。如果我们遵循相同的步骤在另一台计算机上操作,我们的SHA-1 hash值将是相同的。这就是 Git 知道两个仓库即使并行工作也是相同的。
如果你曾经好奇SHA-1 hash值被意外或故意修改以重叠,GitHub 博客上有一篇关于检测这种SHA-1 hash碰撞的有趣文章:github.blog/2017-03-20-sha-1-collision-detection-on-github-com/。
我们可以使用git log显示提交的历史记录。条目按逆时间顺序显示;每个提交显示作者的姓名和电子邮件地址、日期、日志消息以及提交的内部标识号:
$ git log
commit ff7dc1a40e5603fed552a3403be97addefddc4e9 (HEAD -> master)
Author: Eric Chou <echou@yahoo.com>
Date: Fri Nov 8 08:49:02 2019 -0800
made modifications to myFile.txt
commit 5d7c1c8543c8342b689c66f1ac1fa888090ffa34
Author: Eric Chou <echou@yahoo.com>
Date: Fri Nov 8 08:46:32 2019 -0800
adding myFile.txt
我们还可以使用提交 ID 显示关于更改的更多详细信息:
(venv) $ git show ff7dc1a40e5603fed552a3403be97addefddc4e9
commit ff7dc1a40e5603fed552a3403be97addefddc4e9 (HEAD -> master)
Author: Eric Chou <echou@yahoo.com>
Date: Fri Nov 8 08:49:02 2019 -0800
made modifications to myFile.txt
diff --git a/myFile.txt b/myFile.txt
index 6ccb42e..69e7d47 100644
--- a/myFile.txt
+++ b/myFile.txt
@@ -1 +1 @@
-this is my test file
+this is the second iteration of my test file
如果你需要回滚所做的更改,你可以在revert和reset之间选择。前者将特定commit的所有文件更改回提交之前的状态:
$ git revert ff7dc1a40e5603fed552a3403be97addefddc4e9
[master 75921be] Revert "made modifications to myFile.txt"
1 file changed, 1 insertion(+), 1 deletion(-)
$ cat myFile.txt
this is my test file
revert命令将保留你回滚的commit并创建一个新的commit。你将能够看到到那个点为止的所有更改,包括回滚:
$ git log
commit 75921bedc83039ebaf70c90a3e8d97d65a2ee21d (HEAD -> master)
Author: Eric Chou <echou@yahoo.com>
Date: Fri Nov 8 09:00:23 2019 -0800
Revert "made modifications to myFile.txt"
This reverts commit ff7dc1a40e5603fed552a3403be97addefddc4e9.
On branch master
Changes to be committed:
modified: myFile.txt
reset选项会将你的仓库状态重置到较旧版本,并丢弃之间的所有更改:
$ git reset --hard ff7dc1a40e5603fed552a3403be97addefddc4e9
HEAD is now at ff7dc1a made modifications to myFile.txt
$ git log
commit ff7dc1a40e5603fed552a3403be97addefddc4e9 (HEAD -> master)
Author: Eric Chou <echou@yahoo.com>
Date: Fri Nov 8 08:49:02 2019 -0800
made modifications to myFile.txt
commit 5d7c1c8543c8342b689c66f1ac1fa888090ffa34
Author: Eric Chou <echou@yahoo.com>
Date: Fri Nov 8 08:46:32 2019 -0800
adding myFile.txt
我喜欢保留所有历史记录,包括我做的任何回滚操作。因此,当我需要回滚一个更改时,我通常选择revert而不是reset。在本节中,我们看到了如何处理单个文件。在下一节中,让我们看看如何处理被组织成特定bundle的文件集合,这个bundle被称为branch。
Git 分支
Git 中的branch是指一个仓库内的开发分支。Git 允许有多个分支,因此在一个仓库内可以有不同的开发线。默认情况下,我们有一个主分支。
几年前,GitHub 的默认分支被重命名为“main”:github.com/github/renaming。我们将在实际应用中看到这两个名称。
分支的原因有很多;关于何时分支或直接在 master/main 分支上工作没有硬性规定。大多数时候,当有错误修复、客户软件发布或开发阶段时,我们会创建一个分支。在我们的例子中,让我们创建一个代表开发的分支,命名为dev分支:
$ git branch dev
$ git branch
dev
* master
注意,我们需要在创建后特别移动到 dev 分支。我们使用 checkout 来做这件事:
$ git checkout dev
Switched to branch 'dev'
$ git branch
* dev
master
让我们在 dev 分支中添加第二个文件:
$ echo "my second file" > mySecondFile.txt
$ git add mySecondFile.txt
$ git commit -m "added mySecondFile.txt to dev branch"
[dev a537bdc] added mySecondFile.txt to dev branch
1 file changed, 1 insertion(+)
create mode 100644 mySecondFile.txt
我们可以回到 master 分支并验证两条开发线是分开的。注意,当我们切换到 master 分支时,目录中只有一个文件:
$ git branch
* dev
master
$ git checkout master
Switched to branch 'master'
$ ls
myFile.txt
$ git checkout dev
Switched to branch 'dev'
$ ls
myFile.txt mySecondFile.txt
要将 dev 分支的内容写入 master 分支,我们需要 merge 它们:
$ git branch
* dev
master
$ git checkout master
Switched to branch 'master'
$ git merge dev master
Updating ff7dc1a..a537bdc
Fast-forward
mySecondFile.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 mySecondFile.txt
$ git branch
dev
* master
$ ls
myFile.txt mySecondFile.txt
我们可以使用 git rm 来删除一个文件。为了了解它是如何工作的,让我们创建第三个文件并删除它:
$ touch myThirdFile.txt
$ git add myThirdFile.txt
$ git commit -m "adding myThirdFile.txt"
[master 169a203] adding myThirdFile.txt
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 myThirdFile.txt
$ ls
myFile.txt mySecondFile.txt myThirdFile.txt
$ git rm myThirdFile.txt
rm 'myThirdFile.txt'
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
deleted: myThirdFile.txt
$ git commit -m "deleted myThirdFile.txt"
[master 1b24b4e] deleted myThirdFile.txt
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 myThirdFile.txt
我们将能够在日志中看到最后两个更改:
$ git log
commit 1b24b4e95eb0c01cc9a7124dc6ac1ea37d44d51a (HEAD -> master)
Author: Eric Chou <echou@yahoo.com>
Date: Fri Nov 8 10:02:45 2019 -0800
deleted myThirdFile.txt
commit 169a2034fb9844889f5130f0e42bf9c9b7c08b05
Author: Eric Chou <echou@yahoo.com>
Date: Fri Nov 8 10:00:56 2019 -0800
adding myThirdFile.txt
我们已经了解了大多数我们将使用的 Git 基本操作。让我们看看如何使用 GitHub 来共享我们的仓库。
GitHub 示例
在这个例子中,我们将使用 GitHub 作为集中位置来同步我们的本地仓库并与其他用户共享。
我们将在 GitHub 上创建一个仓库。GitHub 一直免费提供创建公共开源仓库。从 2019 年 1 月开始,它还提供无限免费的私有仓库。在这种情况下,我们将创建一个私有仓库并添加许可证和 .gitignore 文件:

图 14.1:在 GitHub 中创建私有仓库
一旦创建了仓库,我们就可以找到它的 URL:

图 14.2:GitHub 仓库 URL
我们将使用这个 URL 来创建一个远程目标,我们将将其用作项目的“真相之源”。我们将远程目标命名为 gitHubRepo:
$ git remote add gitHubRepo https://github.com/ericchou1/TestRepo.git
$ git remote -v
gitHubRepo https://github.com/ericchou1/TestRepo.git (fetch)
gitHubRepo https://github.com/ericchou1/TestRepo.git (push)
由于我们在创建时选择了创建 README.md 和 LICENSE 文件,远程仓库和本地仓库并不相同。
几年前,GitHub 将 个人访问令牌(PAT)作为密码的术语进行了更改:docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token。要生成令牌,请点击个人标志 -> 设置 -> 开发者设置 -> 个人访问令牌。我们需要在命令行提示时使用此令牌作为密码。
如果我们要将本地更改推送到新的 GitHub 仓库,我们会收到以下错误(记得如果默认分支是 main,请将分支名称更改为 main):
$ git push gitHubRepo master
Username for 'https://github.com': <skip>
Password for 'https://echou@yahoo.com@github.com': <remember to use your personal access token>
To https://github.com/ericchou1/TestRepo.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://github.com/ericchou1/TestRepo.git'
我们将使用 git pull 来从 GitHub 获取新文件:
$ git pull gitHubRepo master
Username for 'https://github.com': <skip>
Password for 'https://<username>@github.com': <personal access token>
From https://github.com/ericchou1/TestRepo
* branch master -> FETCH_HEAD
Merge made by the 'recursive' strategy.
.gitignore | 104
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE | 21 +++++++++++++
README.md | 2 ++
3 files changed, 127 insertions(+)
create mode 100644 .gitignore
create mode 100644 LICENSE
create mode 100644 README.md
现在,我们将能够将内容 push 到 GitHub:
$ git push gitHubRepo master
Username for 'https://github.com': <username>
Password for 'https://<username>@github.com': <personal access token>
Counting objects: 15, done.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (15/15), 1.51 KiB | 0 bytes/s, done. Total 15 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To https://github.com/ericchou1/TestRepo.git a001b81..0aa362a master -> master
我们可以在网页上验证 GitHub 仓库的内容:

图 14.3:GitHub 仓库
现在另一个用户可以简单地复制,或者 clone 仓库:
[This is operated from another host]
$ cd /tmp
$ git clone https://github.com/ericchou1/TestRepo.git
Cloning into 'TestRepo'...
remote: Counting objects: 20, done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 20 (delta 2), reused 15 (delta 1), pack-reused 0
Unpacking objects: 100% (20/20), done.
$ cd TestRepo/
$ ls
LICENSE myFile.txt
README.md mySecondFile.txt
此复制的仓库将是我原始仓库的精确副本,包括所有提交历史:
$ git log
commit 0aa362a47782e7714ca946ba852f395083116ce5 (HEAD -> master, origin/master, origin/HEAD)
Merge: bc078a9 a001b81
Author: Eric Chou <skip>
Date: Fri Jul 20 14:18:58 2018 -0700
Merge branch 'master' of https://github.com/ericchou1/TestRepo
commit a001b816bb75c63237cbc93067dffcc573c05aa2
Author: Eric Chou <skip>
Date: Fri Jul 20 14:16:30 2018 -0700
Initial commit
...
我还可以在仓库设置下邀请另一个人作为项目的协作者:

图 14.4:仓库邀请
在下一个示例中,我们将看到如何对一个我们不维护的仓库进行分支并执行拉取请求。
通过拉取请求进行协作
如前所述,Git 支持开发者之间在单个项目上的协作。我们将探讨当代码托管在 GitHub 上时是如何操作的。
在这种情况下,我们将使用 Packt 的 GitHub 公共仓库中的该书的第二版 GitHub 仓库。我将使用不同的 GitHub 用户名,以便我看起来像非管理员用户。我将点击 Fork 按钮以在我的账户中复制仓库:

图 14.5:Git 分支按钮
复制需要几秒钟:

图 14.6:Git 分支进行中
分支后,我们将在我们的账户中拥有仓库的副本:

图 14.7:Git 分支
我们可以遵循我们用来修改文件的相同步骤。在这种情况下,我将修改 README.md 文件。更改完成后,我可以点击 新建拉取请求 按钮来创建一个拉取请求:

图 14.8:拉取请求
在创建拉取请求时,我们应该尽可能填写信息,以提供更改的理由:

图 14.9:拉取请求详情
仓库维护者将收到拉取请求的通知;如果被接受,更改将进入原始仓库:

图 14.10:拉取请求记录
GitHub 为与其他开发者协作提供了一个出色的平台;这正在迅速成为许多大型开源项目的默认开发选择。由于 Git 和 GitHub 在许多项目中得到广泛使用,下一步自然就是自动化本节中我们看到的过程。在下一节中,我们将看看如何使用 Python 与 Git 一起使用。
Git 与 Python
有一些 Python 包我们可以与 Git 和 GitHub 一起使用。在本节中,我们将探讨 GitPython 和 PyGitHub 库。
GitPython
我们可以使用 GitPython 包,gitpython.readthedocs.io/en/stable/index.html,来与我们的 Git 仓库进行交互。我们将安装此包并使用 Python 命令行来构建一个 Repo 对象。从那里,我们可以列出仓库中的所有提交:
$ pip install gitpython
$ python
>>> from git import Repo
>>> repo = Repo('/home/echou/Mastering_Python_Networking_third_edition/Chapter13/TestRepo-1')
>>> for commits in list(repo.iter_commits('master')):
... print(commits)
...
1b24b4e95eb0c01cc9a7124dc6ac1ea37d44d51a
169a2034fb9844889f5130f0e42bf9c9b7c08b05
a537bdcc1648458ce88120ae607b4ddea7fa9637
ff7dc1a40e5603fed552a3403be97addefddc4e9
5d7c1c8543c8342b689c66f1ac1fa888090ffa34
我们还可以查看 repo 对象中的索引条目:
>>> for (path, stage), entry in repo.index.entries.items():
... print(path, stage, entry)
...
myFile.txt 0 100644 69e7d4728965c885180315c0d4c206637b3f6bad 0 myFile.txt
mySecondFile.txt 0 100644 75d6370ae31008f683cf18ed086098d05bf0e4dc 0 mySecondFile.txt
GitPython 与所有 Git 功能具有良好的集成。然而,对于初学者来说,它可能不是最容易使用的库。我们需要了解 Git 的术语和结构,才能充分利用 GitPython,如果我们在其他项目中需要它,这一点总是值得记住。
PyGitHub
让我们看看如何使用 PyGithub 库,pygithub.readthedocs.io/en/latest/,与 GitHub API v3 交互,developer.github.com/v3/:
$ pip install PyGithub
让我们使用 Python 命令行来打印出用户的当前仓库:
$ python
>>> from github import Github
>>> g = Github("<username>", "<password>")
>>> for repo in g.get_user().get_repos():
... print(repo.name)
...
Mastering-Python-Networking-Second-Edition
Mastering-Python-Networking-Third-Edition
为了进行更程序化的访问,我们还可以使用访问令牌创建更细粒度的控制。GitHub 允许令牌与所选权限相关联:

图 14.11:GitHub 令牌生成
如果您使用访问令牌作为认证机制,输出会有所不同:
>>> from github import Github
>>> g = Github("<token>")
>>> for repo in g.get_user().get_repos():
... print(repo)
...
Repository(full_name="oreillymedia/distributed_denial_of_service_ddos")
Repository(full_name="PacktPublishing/-Hands-on-Network- Programming-with- Python")
Repository(full_name="PacktPublishing/Mastering-Python-Networking")
Repository(full_name="PacktPublishing/Mastering-Python-Networking-Second- Edition")
...
现在我们已经熟悉了 Git、GitHub 以及一些 Python 包,我们可以使用它们来处理技术。我们将在下一节中查看一些实际示例。
自动化配置备份
在此示例中,我们将使用 PyGithub 备份包含我们的路由器配置的目录。我们看到了如何使用 Python 或 Ansible 从我们的设备中检索信息;现在我们可以将它们检查到 GitHub 上。
我们有一个名为 config 的子目录,其中包含我们的路由器配置文本格式:
$ ls configs/
iosv-1 iosv-2
$ cat configs/iosv-1
Building configuration...
Current configuration : 4573 bytes
!
! Last configuration change at 02:50:05 UTC Sat Jun 2 2018 by cisco
!
version 15.6
service timestamps debug datetime msec
...
我们可以使用以下脚本,Chapter14_1.py,从我们的 GitHub 仓库检索最新索引,构建需要提交的内容,并自动提交配置:
#!/usr/bin/env python3
# reference: https://stackoverflow.com/questions/38594717/how-do-i-push-new-files-to-github
from github import Github, InputGitTreeElement
import os
github_token = '<token>'
configs_dir = 'configs'
github_repo = 'TestRepo'
# Retrieve the list of files in configs directory
file_list = []
for dirpath, dirname, filenames in os.walk(configs_dir):
for f in filenames:
file_list.append(configs_dir + "/" + f)
g = Github(github_token)
repo = g.get_user().get_repo(github_repo)
commit_message = 'add configs'
master_ref = repo.get_git_ref('heads/master')
master_sha = master_ref.object.sha
base_tree = repo.get_git_tree(master_sha)
element_list = list()
for entry in file_list:
with open(entry, 'r') as input_file:
data = input_file.read()
element = InputGitTreeElement(entry, '100644', 'blob', data)
element_list.append(element)
# Create tree and commit
tree = repo.create_git_tree(element_list, base_tree)
parent = repo.get_git_commit(master_sha)
commit = repo.create_git_commit(commit_message, tree, [parent])
master_ref.edit(commit.sha)
我们可以在 GitHub 仓库中看到 configs 目录:

图 14.12:配置目录
提交历史显示了来自我们脚本的提交:

图 14.13:提交历史
在 GitHub 示例部分,我们看到了如何通过分叉仓库和发起拉取请求与其他开发者协作。让我们看看我们如何进一步与 Git 协作。
与 Git 协作
Git 是一种出色的协作技术,GitHub 是共同开发项目的极其有效的方式。GitHub 为世界上任何有互联网接入的人提供了一个免费分享他们的想法和代码的地方。我们知道如何使用 Git 以及一些基本的 GitHub 协作步骤,但我们如何加入并贡献到一个项目中呢?
当然,我们愿意回馈给我们带来很多帮助的开源项目,但我们应该如何开始呢?
在本节中,我们将探讨一些关于使用 Git 和 GitHub 进行软件开发协作的知识点:
-
从小处着手:理解我们可以在团队中扮演的角色是最重要的事情之一。我们可能在网络工程方面很出色,但在 Python 开发方面可能一般。有许多事情我们可以做,而不需要成为高技能的开发者。不要害怕从小处开始;文档和测试是作为贡献者入门的两个好方法。
-
学习生态系统:无论项目大小,都有一套约定和已经建立的文化。我们都因为 Python 易于阅读的语法和友好的文化而喜欢它;它还有一个围绕这一理念的指导文档(
devguide.python.org/)。另一方面,Ansible 项目也有一个广泛的社区指南(docs.ansible.com/ansible/latest/community/index.html)。它包括行为准则、拉取请求流程、如何报告错误以及发布流程。阅读这些指南,了解感兴趣项目的生态系统。 -
创建分支:我犯了一个错误,即分叉了一个项目并为主分支提交了拉取请求。主分支应该留给核心贡献者进行更改。我们应该为我们的贡献创建一个单独的分支,并允许该分支稍后合并。
-
保持分叉仓库同步:一旦你分叉了一个项目,没有规则强制克隆的仓库与主仓库同步。我们应该定期做
git pull(获取代码并本地合并)或git fetch(获取代码以及任何本地更改)以确保我们有主仓库的最新副本。 -
保持友好:就像在现实世界中一样,虚拟世界没有敌意的地方。在讨论问题时,即使有分歧,也要保持文明和友好。
Git 和 GitHub 为任何有动力的人提供了一个通过轻松协作在项目中做出贡献的方式。我们都有权为任何我们感兴趣的开源或私有项目做出贡献。
摘要
在本章中,我们探讨了名为 Git 的版本控制系统及其紧密的兄弟 GitHub。Git 由林纳斯·托瓦兹(Linus Torvalds)于 2005 年开发,用于帮助开发 Linux 内核,后来被其他开源项目作为它们的源代码控制系统所采用。Git 是一个快速、分布式和可扩展的系统。GitHub 提供了一个集中位置,在互联网上托管 Git 仓库,允许任何有互联网连接的人进行协作。
我们探讨了如何在命令行中使用 Git 及其各种操作,以及它们如何在 GitHub 中应用。我们还研究了两个流行的 Python 库,用于与 Git 一起工作:GitPython 和 PyGithub。我们以一个配置备份示例和关于项目协作的笔记结束本章。
在第十五章,“使用 GitLab 的持续集成”,我们将探讨另一个用于持续集成和部署的流行开源工具:GitLab。
加入我们的图书社区
要加入这本书的社区——在那里您可以分享反馈,向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity

第十五章:使用 GitLab 进行持续集成
网络触及技术栈的每个部分;在我工作过的所有环境中,网络始终是零级服务。它是一个基础服务,其他服务依赖于它来使它们的服务正常工作。在其他工程师、业务经理、操作员和支持人员的心中,网络应该只是正常工作。它应该始终可访问并正确运行——一个好的网络是没有人会注意到的网络。
当然,作为网络工程师,我们知道网络与其他技术栈一样复杂。由于其复杂性,构成运行网络的构造可能很脆弱。有时,我看到一个网络,想知道它怎么能正常工作,更不用说它如何在没有业务影响的情况下运行数月甚至数年。
我们对网络自动化感兴趣的部分原因是为了找到可靠且一致地重复我们的网络变更流程的方法。通过使用 Python 脚本或 Ansible 框架,我们可以确保我们做出的变更将保持一致并可靠地应用。正如我们在上一章中看到的,我们可以使用 Git 和 GitHub 来可靠地存储流程的组件,如模板、脚本、需求和文件。构成基础设施的代码是版本控制的、协作的,并对变更负责。但我们是如何将这些部分联系在一起的呢?在本章中,我们将探讨一个流行的存储库,它可以优化网络管理流程,称为 GitLab。
GitLab 的开源核心在 MIT 开源许可证下发布。其余的是源代码可用的,about.gitlab.com/solutions/open-source/。(https://about.gitlab.com/solutions/open-source/。)
在本章中,我们将涵盖以下主题:
-
传统变更管理流程的挑战
-
持续集成和 GitLab 简介
-
GitLab 安装和示例
-
GitLab 与 Python
-
网络工程中的持续集成
我们将从传统的变更管理流程开始。正如任何经过实战考验的网络工程师可以告诉你的,传统的变更管理流程通常涉及大量的手工劳动和人工判断。正如我们将看到的,它并不一致,难以简化。
传统的变更管理流程
在大型网络环境中工作过的工程师知道,网络变更出错的影响可能很大。我们可以做出数百次变更而没有任何问题,但只需要一次错误的变更就可能导致整个网络对整个业务造成损害。
关于网络故障导致商业痛苦的战争故事并不少见。2011 年最明显和大规模的 AWS EC2 故障之一是由 AWS US-East 区域正常扩展活动的一部分网络变更引起的。该变更发生在 00:47 PDT,导致各种服务停电超过 12 小时,给亚马逊造成了数百万美元的损失。更重要的是,这个相对年轻的服务声誉受到了严重打击。IT 决策者将故障视为不迁移到年轻的 AWS 云的理由。重建声誉花费了多年时间。您可以在aws.amazon.com/message/65648/了解更多关于事件报告的信息。
由于潜在的影响和复杂性,在许多环境中,网络变更咨询委员会(CAB)流程被实施。典型的 CAB 流程如下:
-
网络工程师将设计变更并详细列出实施变更所需的步骤。这可以包括变更的原因、涉及的设备、将要应用或删除的命令、如何验证输出以及每一步的预期结果。
-
网络工程师通常需要首先向同事请求技术审查。根据变更的性质,可能存在不同级别的同行审查。简单的变更可能只需要单次同行技术审查;更复杂的变更可能需要高级指定工程师的批准。
-
CAB 会议通常安排在固定时间,同时提供紧急的临时会议。
-
工程师将向董事会展示变更。董事会将提出必要的问题,评估影响,并批准或拒绝变更请求。
-
变更将在计划变更窗口期间由原始工程师或另一位工程师执行。
这个过程听起来合理且包容,但在实践中却证明存在一些挑战:
-
编写文档耗时:设计工程师编写文档通常需要很长时间,有时写作过程比应用变更的时间还要长。这通常是由于所有网络变更都具有潜在的破坏性,我们需要为技术和非技术 CAB 成员记录过程。
-
工程师的专业知识:高级工程师专业知识是一种有限资源。存在不同级别的工程专业知识;有些经验更丰富,通常是需求最迫切的资源。我们应该保留他们的时间来解决最复杂的网络问题,而不是审查基本的网络变更。
-
会议耗时:组织会议并确保每位成员出席需要大量努力。如果必要的批准人员休假或生病怎么办?如果需要在计划好的 CAB 时间之前进行网络变更怎么办?
这些只是基于人类 CAB 过程的一些更大挑战。我个人非常讨厌 CAB 过程。我并不否认同行评审和优先级排序的需要;然而,我认为我们需要最小化涉及到的潜在开销。在本章的剩余部分,让我们看看一个可能适合 CAB 和一般变更管理的替代管道,该管道已被软件工程界采用。
连续集成简介
持续集成(CI)在软件开发中是一种快速发布代码库小更改的方法,内置代码测试和验证。关键是分类要 CI 兼容的更改,即不过于复杂,足够小以便可以轻松应用,以便可以轻松回滚。测试和验证过程以自动化的方式构建,以获得一个基线信心,即更改将应用而不会破坏整个系统。
在 CI 之前,软件的更改通常是大批量进行的,并且往往需要漫长的验证过程(这听起来熟悉吗?)。开发者可能需要数月才能在生产环境中看到他们的更改,收到反馈循环,并修复错误。简而言之,CI 过程旨在缩短从想法到更改的过程。
通常,通用工作流程包括以下步骤:
-
第一位工程师获取当前代码库的副本并开始进行更改。
-
第一位工程师将更改提交到仓库。
-
仓库可以通知一组工程师有关仓库更改的通知,他们可以审查更改。他们可以批准或拒绝更改。
-
CI 系统可以持续拉取仓库以获取更改,或者当发生更改时,仓库可以向 CI 系统发送通知。无论哪种方式,CI 系统都会拉取最新的代码版本。
-
CI 系统将运行自动化测试以尝试捕获任何破坏。
-
如果没有发现任何故障,CI 系统可以选择将更改合并到主代码中,并可选择将其部署到生产系统。
这是一个通用的步骤列表。对于每个组织,过程可能不同。例如,可以在代码审查之后而不是在代码审查后立即运行自动测试。有时,组织可能会选择在步骤之间让人类工程师参与进行合理性检查。
在下一节中,我们将说明如何在 Ubuntu 22.04 LTS 系统上安装 GitLab 的说明。
安装 GitLab
GitLab 是一个功能强大、一应俱全的工具,用于处理端到端 DevOps 协作工具。正如我们将在下一分钟看到的那样,它托管代码仓库并处理代码测试、部署和验证。它是目前该领域最受欢迎的 DevOps 工具之一。
该技术背后的公司 GitLab Inc. 在 2021 年晚些时候在纳斯达克(股票代码 GTLB)成功进行了首次公开募股,techcrunch.com/2021/09/17/inside-gitlabs-ipo-filing/。该公司的成功展示了该技术的强大和可持续性。
我们只需要使用其一小部分功能来启动测试实验室。目标是熟悉这些步骤的整体流程。我鼓励您查看 GitLab 文档 docs.gitlab.com/,以了解其功能。

图 15.1:GitLab 文档
对于我们的网络实验室,我们将使用我们在过去几章中一直在使用的相同实验室拓扑。

图 15.2:实验室拓扑
虽然运行 GitLab 作为 Docker 镜像很有吸引力,但 GitLab 运行器(执行步骤的组件)本身也是 Docker 镜像,运行 Docker-in-Docker 在我们的实验室中引入了更多的复杂性。因此,在本章中,我们将在一个虚拟机上安装 GitLab,运行器在容器中运行。安装系统要求可以在以下位置找到,docs.gitlab.com/ee/install/requirements.html。
我们将安装 Docker 引擎、docker-compose,然后是 GitLab 软件包。首先让我们准备好 Docker:
# Installing Docker Engine
$ sudo apt-get install ca-certificates curl gnupg lsb-release
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
$ sudo apt-get update
$ sudo apt-get install docker-ce docker-ce-cli containerd.io
# Run Docker as user
$ sudo groupadd docker
$ sudo usermod -aG docker $USER
$ newgrp docker
# Install Docker-Compose
$ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
$ docker-compose --version
docker-compose version 1.29.2, build 5becea4c
对于 GitLab,我们将按照官方步骤安装自托管的 GitLab:docs.gitlab.com/omnibus/index.html#installation-and-configuration-using-omnibus-package。请注意,这些步骤需要将端口转发到外部可访问的 URL 上的主机:
$ sudo apt update
$ sudo apt-get install -y curl openssh-server ca-certificates tzdata perl
$ sudo apt-get install -y postfix
$ curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh | sudo bash
$ sudo EXTERNAL_URL="http://gitlab.networkautomationnerds.com:9090" apt-get install gitlab-ee
安装完成后,我们应该能看到成功消息:

图 15.3:GitLab 安装
我们将使用初始密码登录,然后重置它(docs.gitlab.com/ee/security/reset_user_password.html#reset-your-root-password):
$ sudo cat /etc/gitlab/initial_root_password
…
Password: <random password>
$ sudo gitlab-rake "gitlab:password:reset"
一切配置完成后,我们应该能在“菜单 -> 管理员”下看到仪表板:

图 15.4:GitLab 仪表板
作为可选步骤,我们可以在 /etc/gitlab/gitlab.rb 下启用 SMTP 设置。这将允许我们接收 GitLab 上重要消息的电子邮件(docs.gitlab.com/omnibus/settings/smtp.html):

图 15.5:GitLab SMTP 设置
让我们谈谈 GitLab 运行器。
GitLab 运行器
GitLab 使用运行者的概念。运行者是一个进程,它从 GitLab 中提取并执行持续集成/持续部署(CI/CD)作业。运行者可以在主机上的 Docker 容器中运行,docs.gitlab.com/runner/install/docker.html:
$ docker run --rm -t -i gitlab/gitlab-runner —help
Unable to find image 'gitlab/gitlab-runner:latest' locally
latest: Pulling from gitlab/gitlab-runner
7b1a6ab2e44d: Pull complete
5580ef77ebbe: Pull complete
d7b21acbe607: Pull complete
Digest: sha256:d2db6b687e9cf5baf96009e43cc3eaebf180f634306cdc74e2400315d35f0dab
Status: Downloaded newer image for gitlab/gitlab-runner:latest
…
$ docker run -d --name gitlab-runner --restart always \
> -v /srv/gitlab-runner/config:/etc/gitlab-runner \
> -v /var/run/docker.sock:/var/run/docker.sock \
> gitlab/gitlab-runner:latest
617b94e5e4c5c72d33610b2eef5eb7027f579f4e069558cbf61f884375812306
我们可以继续在管理区域 -> 运行者 -> 注册下注册主机运行者,docs.gitlab.com/runner/register/index.html#docker。我们将注意令牌:

图 15.6:GitLab 运行者注册
然后,我们可以使用令牌来拉取并注册一个基于镜像的运行者:
(venv) echou@gitlab:~$ docker run --rm -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register
Runtime platform arch=amd64 os=linux pid=8 revision=5316d4ac version=14.6.0
Running in system-mode.
Enter the GitLab instance URL (for example, https://gitlab.com/):
http://<ip>:<port>
Enter the registration token:
<token>
Enter a description for the runner:
[fef6fb5a91dd]: local-runner
Enter tags for the runner (comma-separated): << Leave empty unless we want matching tag to run the runners jobs
Registering runner... succeeded runner=64eCJ5yp
Enter an executor: virtualbox, docker-ssh+machine, kubernetes, custom, docker-ssh, parallels, docker+machine, docker, shell, ssh:
docker
Enter the default Docker image (for example, ruby:2.6):
docker pull ubuntu:latest
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
现在我们已经准备好处理我们的第一个作业了!
第一个 GitLab 示例
我们可以从在菜单 -> 管理区域 -> 用户(在概览下)下创建一个单独的用户开始,并通过该用户登录:

图 15.7:GitLab 用户
要从仓库推送或拉取,我们还将添加我们的 SSH 密钥。这可以通过用户配置文件中的设置部分完成:

图 15.8:用户 SSH 密钥
我们现在可以在菜单 -> 项目 -> 创建新项目下创建一个新项目:

图 15.9:创建新项目
我们将把这个项目命名为chapter15_example1:

图 15.10:新项目设置
我们可以保留其他设置不变。作为预防措施,我通常将项目可见性设置为私有,但我们可以稍后更改它。

图 15.11:项目克隆 URL
我们可以获取项目的 URL 并在我们的管理站上克隆该项目:
$ git clone http://gitlab.<url>/echou/chapter15_example1.git
Cloning into 'chapter15_example1'...
Username for 'http://gitlab.<url>': <user>
Password for 'http://<user>@<url>':
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.
$ cd chapter15_example1/
$ ls
README.md
我们将创建一个特殊的文件.gitlab-ci.yml,该文件被 GitLab 识别为 CI/CD 指令:
# define stages
stages:
- build
- test
- deploy
# define the job
deploy our network:
image: "ubuntu:20.04"
stage: build
script:
- mkdir new_network
- cd new_network
- touch bom.txt
- echo "this is our build" >> bom.txt
artifacts:
paths:
- new_network/
test our network:
stage: test
image: "ubuntu:20.04"
script:
- pwd
- ls
- test -f new_network/bom.txt
deploy to prod:
stage: deploy
image: "ubuntu:20.04"
script:
- echo "deploy to production"
when: manual
我们将签入、提交并将文件推送到我们的 GitLab 仓库:
$ git add .gitlab-ci.yml
$ git commit -m "initial commit"
$ git push origin main
Username for 'http://<url>': <username>
Password for 'http://<url>': <password>
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 2 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 512 bytes | 512.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To http://<url> /echou/chapter15_example1.git
c0b232d..5552a10 main -> main
.gitlab-ci.yml 文件包含用于 GitLab CI/CD 管道的 YAML 格式指令。它包含两个主要部分,阶段和作业定义:

图 15.12:GitLab CI 文件
在我们的文件中,我们使用关键字stages定义了三个阶段。在执行部分,我们定义了要拉取的 Docker 基础镜像、要执行的作业名称、对应的阶段以及script下要执行的步骤。在build下可以有可选的指令如artifacts,在deploy下可以有可选的指令如when。
如果我们回到项目,我们可以点击CI/CD -> 管道来显示作业的状态:

图 15.13:CI/CD 管道
有三个圆圈,每个代表一个阶段。

图 15.14:管道输出
我们可以点击圆圈并查看容器输出:

图 15.15:执行输出
记得我们在build和deploy下有可选步骤吗?工件给我们提供了可以下载的内容:

图 15.16:工件
when关键字允许我们手动推送步骤,而不是让 GitLab 自动为我们执行:

图 15.17:手动推送
这是不是很棒?我们现在有一些工人自动为我们执行作业。我们还可以利用 Git 的许多协作功能,例如邀请同事进行代码审查。让我们看看另一个例子。
GitLab 网络示例
我们将继续在 GitLab 服务器上创建另一个名为chapter15_example2的项目。在本地机器上,我们将克隆远程仓库:
$ git clone http://<url>/echou/chapter15_example2.git
$ cd chapter15_example2/
在这个例子中,我们将集成 Nornir 库,看看我们如何在两个 IOSv 设备上执行show version。我们将首先定义hosts.yaml文件:
---
r1:
hostname: '192.168.2.218'
port: 22
username: 'cisco'
password: 'cisco'
platform: 'cisco_ios'
r2:
hostname: '192.168.2.219'
port: 22
username: 'cisco'
password: 'cisco'
platform: 'cisco_ios'
然后,我们可以构建用于执行的 Python 脚本:
#!/usr/bin/env python
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from nornir_netmiko import netmiko_send_command
nr = InitNornir()
result = nr.run(
task=netmiko_send_command,
command_string="show version"
)
print_result(result)
我们将定义一个requirements.txt文件来指定要安装的包:
$ cat requirements.txt
…
flake8==4.0.1
…
netmiko==3.4.0
nornir==3.2.0
nornir-netmiko==0.1.2
nornir-utils==0.1.2
paramiko==2.9.2
…
我们还将定义.gitlab-ci.yml文件来定义阶段和脚本。注意在文件中,我们指定了另一个在所有阶段之前执行的before_script步骤:
stages:
- Test
- QA
before_script:
- python --version
- pip3 install -r requirements.txt
Test-Job:
stage: Test
script:
- python3 show_version.py
flake8:
stage: QA
script:
- flake8 show_version.py
一旦文件被检查并推送到仓库,我们就可以转到 CI/CD 部分查看输出。这次步骤将因为包下载时间而花费更长的时间。我们可以点击步骤并实时检查执行情况。

图 15.18:Nornir CI/CD 步骤执行
我们应该能够看到管道成功执行。

图 15.19:CI/CD 结果
使用 GitLab CI/CD 是自动化我们的网络操作步骤的绝佳方式。设置管道可能需要更长的时间,但一旦完成,它将为我们节省大量时间,并允许我们保留精力专注于更有趣的工作。更多信息,请参阅docs.gitlab.com/ee/ci/。
摘要
在本章中,我们探讨了传统的变革管理流程以及为什么它不适合当今快速变化的环境。网络需要与业务同步发展,以变得更加灵活,并能快速且可靠地适应变化。
我们探讨了持续集成的概念,特别是开源的 GitLab 系统。GitLab 是一个功能全面、可扩展的持续集成系统,在软件开发中被广泛使用。我们可以将相同的系统应用于我们的网络操作。我们看到了两个使用 GitLab Git 仓库和运行器自动执行我们操作的例子。
在第十六章“网络的测试驱动开发”中,我们将探讨使用 Python 进行测试驱动开发。
加入我们的书籍社区
要加入本书的社区——在这里您可以分享反馈、向作者提问,并了解新书发布——请扫描下面的二维码:
packt.link/networkautomationcommunity

第十六章:网络的测试驱动开发
在前面的章节中,我们使用 Python 与网络设备通信,监控和保障网络,自动化流程,并将本地网络扩展到公有云提供商。我们已经从必须仅使用终端窗口并使用 CLI 管理网络的情况中走了很长的路。当我们共同工作时,我们所构建的服务就像一台运转良好的机器,为我们提供了一个美丽、自动化、可编程的网络。然而,网络永远不会静止,它不断变化以满足业务需求。当我们构建的服务没有最佳运行时会发生什么?正如我们在监控和源代码控制系统中所做的那样,我们正在积极尝试检测故障。
在本章中,我们通过测试驱动开发(TDD)扩展了主动检测的概念。我们将涵盖以下主题:
-
TDD 概述
-
拓扑作为代码
-
编写网络测试
-
pyATS 和 Genie
我们将从这个章节开始,先概述 TDD,然后再深入探讨其在网络中的应用。我们将查看使用 Python 进行 TDD 的示例,并逐步从特定的测试过渡到基于网络的更大测试。
测试驱动开发概述
TDD(测试驱动开发)的概念已经存在了一段时间。美国软件工程师肯特·贝克(Kent Beck)等人被公认为领导了 TDD 运动,并与敏捷软件开发一起发展。敏捷软件开发需要非常短的构建-测试-部署开发周期;所有软件需求都被转化为测试用例。这些测试用例通常在编写代码之前就写好了,只有当测试通过时,软件代码才被接受。
同样的想法可以与网络工程平行。例如,当我们面临设计现代网络的挑战时,我们可以将这个过程分解为以下步骤,从高级设计要求到我们可以部署的网络测试:
-
我们从新网络的整体需求开始。为什么我们需要设计一个新的网络或新网络的一部分?可能是因为新的服务器硬件、新的存储网络或新的微服务软件架构。
-
新的要求被分解为更小、更具体的要求。这可能包括评估新的交换平台,测试可能更有效的路由协议,或新的网络拓扑(例如,胖树拓扑)。每个更小的要求都可以分为必需或可选的类别。
-
我们制定测试计划,并对其与潜在解决方案候选者的潜在性进行评估。
-
测试计划将按相反的顺序进行;我们将从测试功能开始,然后将新功能集成到一个更大的拓扑中。最后,我们将尝试在接近生产环境的情况下运行我们的测试。
我试图说明的是,即使我们没有意识到,我们可能已经在正常的网络工程过程中采用了某些 TDD 方法。这是我研究 TDD 心态时的一个启示。我们已经在隐式地遵循这一最佳实践,而没有正式化该方法。
通过逐渐将网络的部分移动到代码中,我们可以将 TDD 应用于网络。如果我们的网络拓扑以 XML 或 JSON 的分层格式描述,每个组件都可以正确映射并表达在期望的状态,这有些人可能称之为“真相之源”。这是我们针对生产偏离此状态的测试用例所期望的状态。例如,如果我们的期望状态要求 iBGP 邻居的全网状,我们可以始终编写一个测试用例来检查我们的生产设备上的 iBGP 邻居数量。
TDD 的顺序大致基于以下六个步骤:
-
编写一个考虑结果的测试
-
运行所有测试并查看新测试是否失败
-
编写代码
-
再次运行测试
-
如果测试失败,进行必要的更改
-
重复
与任何流程一样,我们遵循指南的紧密程度是一个判断。我更喜欢将这些指南视为目标,并相对宽松地遵循它们。例如,TDD 流程要求在编写任何代码之前编写测试用例,或者在我们的例子中,在构建任何网络组件之前。作为一个个人偏好,我总是喜欢在编写测试用例之前看到网络或代码的工作版本。这给了我更高的信心,所以如果有人评判我的 TDD 过程,我可能会得到一个巨大的“F”。我还喜欢在不同的测试级别之间跳转;有时,我测试网络的一小部分,其他时候,我进行系统级的端到端测试,如 ping 或 traceroute 测试。
重点是我不相信存在一种适合所有情况的测试方法。这取决于个人偏好和项目的范围。这对于我合作过的多数工程师来说都是正确的。记住框架是一个好主意,这样我们就有了一个可以遵循的工作蓝图,但你是你解决问题的最佳评判者。
在我们进一步探讨 TDD 之前,让我们在下一节中介绍一些最常见的术语,以便我们在深入了解细节之前有一个良好的概念基础。
测试定义
让我们看看 TDD 中常用的一些术语:
-
单元测试:检查一小段代码。这是一个针对单个函数或类的测试。
-
集成测试:检查代码库的多个组件;多个单元组合并作为一个组进行测试。这可能是一个针对 Python 模块或多个模块的测试。
-
系统测试:从端到端进行检查。这是一个尽可能接近最终用户所看到内容的测试。
-
功能测试:针对单个函数进行检查。
-
测试覆盖率:一个术语,指的是确定我们的测试用例是否覆盖了应用程序代码。这通常是通过检查运行测试用例时执行了多少代码来完成的。
-
测试固定装置:形成一个基准状态,用于运行我们的测试。测试固定装置的目的是确保有一个已知且固定的环境,以便测试可以重复进行。
-
设置和拆除:所有先决步骤都添加到设置中,并在拆除时清理。
这些术语可能看起来非常侧重于软件开发,有些人可能对网络工程不相关。记住,这些术语是我们用来传达一个概念或步骤的方式。我们将在本章的其余部分使用这些术语。随着我们在网络工程环境中更多地使用这些术语,它们可能会变得更加清晰。在这一点上,让我们深入探讨将网络拓扑视为代码。
将拓扑表示为代码
当我们讨论将拓扑表示为代码时,一个工程师可能会跳起来宣称:“网络太复杂了,不可能将其总结成代码!”从个人经验来看,这在我参加的一些会议中发生过。在会议中,我们会有一群希望将基础设施视为代码的软件工程师,但房间里的传统网络工程师会宣称这是不可能的。在你效仿并在这本书的页面上对我大喊大叫之前,让我们保持开放的心态。如果我告诉你,我们已经在本书中使用代码来描述我们的拓扑,这会有帮助吗?
如果你查看本书中我们一直在使用的任何实验室拓扑文件,它们只是包含节点之间关系描述的 YAML 文件。例如,在本章中,我们将使用我们在上一章中使用的相同拓扑:

图 16.1:实验室拓扑
如果我们用文本编辑器打开拓扑文件,chapter16_topology.yaml,我们会看到该文件是一个描述节点及其之间链接的 YAML 文件:
lab:
description: Imported from 2_DC_Topology.virl
notes: |-
## Import Progress
- processing node /lax-edg-r1 (iosv)
- processing node /lax-edg-r2 (iosv)
- processing node /nyc-edg-r1 (iosv)
- processing node /nyc-edg-r2 (iosv)
- processing node /lax-cor-r1 (nxosv)
- processing node /nyc-cor-r1 (nxosv)
- link GigabitEthernet0/1.lax-edg-r1 -> Ethernet2/1.lax-cor-r1
- link GigabitEthernet0/1.lax-edg-r2 -> Ethernet2/2.lax-cor-r1
- link GigabitEthernet0/1.nyc-edg-r1 -> Ethernet2/1.nyc-cor-r1
- link GigabitEthernet0/1.nyc-edg-r2 -> Ethernet2/2.nyc-cor-r1
- link Ethernet2/3.lax-cor-r1 -> Ethernet2/3.nyc-cor-r1
timestamp: 1615749425.6802542
title: 2_DC_Topology.yaml
version: 0.0.4
节点部分包括每个节点的 id、标签、定义和配置:
nodes:
- id: n0
label: lax-edg-r1
node_definition: iosv
x: -100
y: 200
configuration: |-
!
! Last configuration change at 02:26:08 UTC Fri Apr 17 2020 by cisco
!
version 15.6
service timestamps debug datetime msec
service timestamps log datetime msec
no service password-encryption
!
hostname lax-edg-r1
!
boot-start-marker
boot-end-marker
!
!
vrf definition Mgmt-intf
!
address-family ipv4
exit-address-family
!
<skip>
如果我们打开上一章的实验室拓扑文件,其中包含 Linux 节点,我们可以看到 Linux 主机节点可以用与网络节点相同的方式描述:
- id: n5
label: Client
node_definition: server
x: 0
y: 0
configuration: |-
# converted cloud-config
hostname Client
ifconfig eth1 up 10.0.0.9 netmask 255.255.255.252
route add -net 10.0.0.0/8 gw 10.0.0.10 dev eth1
route add -net 192.168.0.0/28 gw 10.0.0.10 dev eth1
# original cloud-config
# #cloud-config
# bootcmd:
# - ln -s -t /etc/rc.d /etc/rc.local
# hostname: Client
# manage_etc_hosts: true
# runcmd:
# - start ttyS0
# - systemctl start getty@ttyS0.service
# - systemctl start rc-local
# - sed -i '/^\s*PasswordAuthentication\s\+no/d' /etc/ssh/sshd_config
# - echo "UseDNS no" >> /etc/ssh/sshd_config
# - service ssh restart
# - service sshd restart
通过将网络表示为代码,我们可以为我们的网络声明一个真相来源。我们可以编写测试代码来比较实际的生产值与这个蓝图。我们将使用这个拓扑文件作为基础,并将生产网络值与之比较。
XML 解析示例
除了 YAML 之外,另一种流行的将拓扑表示为代码的方式是 XML。事实上,这是 CML 2 的前身 Cisco VIRL 使用的格式。在本书的前几版中,我已经提供了一个名为chapter15_topology.virl的两个主机、两个网络节点示例文件,用于我们的解析示例。
要与 XML 文件一起工作,我们可以使用 Python 从该拓扑文件中提取元素,并将其存储为 Python 数据类型,这样我们就可以处理它了。在chapter16_1_xml.py中,我们将使用ElementTree解析virl拓扑文件,并构建一个包含我们设备信息的字典:
#!/usr/env/bin python3
import xml.etree.ElementTree as ET
import pprint
with open('chapter15_topology.virl', 'rt') as f:
tree = ET.parse(f)
devices = {}
for node in tree.findall('./{http://www.cisco.com/VIRL}node'):
name = node.attrib.get('name')
devices[name] = {}
for attr_name, attr_value in sorted(node.attrib.items()):
devices[name][attr_name] = attr_value
# Custom attributes
devices['iosv-1']['os'] = '15.6(3)M2'
devices['nx-osv-1']['os'] = '7.3(0)D1(1)'
devices['host1']['os'] = '16.04'
devices['host2']['os'] = '16.04'
pprint.pprint(devices)
结果是一个 Python 字典,它根据我们的拓扑文件包含设备信息。
我们还可以将习惯性项目添加到字典中:
(venv) $ python chapter16_1_xml.py
{'host1': {'location': '117,58',
'name': 'host1',
'os': '16.04',
'subtype': 'server',
'type': 'SIMPLE'},
'host2': {'location': '347,66',
'name': 'host2',
'os': '16.04',
'subtype': 'server',
'type': 'SIMPLE'},
'iosv-1': {'ipv4': '192.168.0.3',
'location': '182,162',
'name': 'iosv-1',
'os': '15.6(3)M2',
'subtype': 'IOSv',
'type': 'SIMPLE'},
'nx-osv-1': {'ipv4': '192.168.0.1',
'location': '281,161',
'name': 'nx-osv-1',
'os': '7.3(0)D1(1)',
'subtype': 'NX-OSv',
'type': 'SIMPLE'}}
如果我们想将这个“真相之源”与生产设备版本进行比较,我们可以使用第三章,APIs 和 Intent-Driven Networking中的脚本cisco_nxapi_2.py来检索生产 NX-OSv 设备的软件版本。然后我们可以将我们从拓扑文件中收到的值与生产设备的信息进行比较。稍后,我们可以使用 Python 的内置unittest模块来编写测试用例。
我们将在稍后讨论unittest模块。如果您愿意,可以跳过前面的内容,稍后再回到这个例子。
这里是chapter16_2_validation.py中的相关unittest代码:
import unittest
<skip>
# Unittest Test case
class TestNXOSVersion(unittest.TestCase):
def test_version(self):
self.assertEqual(nxos_version, devices['nx-osv-1']['os'])
if __name__ == '__main__':
unittest.main()
当我们运行验证测试时,我们可以看到测试通过,因为生产环境中的软件版本与我们预期的相匹配:
$ python chapter16_2_validation.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
如果我们手动更改预期的 NX-OSv 版本值以引入一个失败案例,我们将看到以下失败输出:
$ python chapter16_3_test_fail.py
F
======================================================================
FAIL: test_version (__main__.TestNXOSVersion)
----------------------------------------------------------------------
Traceback (most recent call last):
File "chapter15_3_test_fail.py", line 50, in test_version
self.assertEqual(nxos_version, devices['nx-osv-1']['os'])
AssertionError: '7.3(0)D1(1)' != '7.4(0)D1(1)'
- 7.3(0)D1(1)
? ^
+ 7.4(0)D1(1)
? ^
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (failures=1)
我们可以看到测试用例结果返回为失败;失败的原因是两个值之间的版本不匹配。正如我们在上一个例子中看到的,Python 的unittest模块是一种根据我们的预期结果测试现有代码的绝佳方式。让我们更深入地了解一下这个模块。
Python 的unittest模块
Python 标准库中包含一个名为unittest的模块,它处理我们可以比较两个值以确定测试是否通过的情况。在先前的例子中,我们看到了如何使用assertEqual()方法比较两个值以返回True或False。以下是一个使用内置unittest模块比较两个值的例子,chapter16_4_unittest.py:
#!/usr/bin/env python3
import unittest
class SimpleTest(unittest.TestCase):
def test(self):
one = 'a'
two = 'a'
self.assertEqual(one, two)
使用python3命令行界面,unittest模块可以自动在脚本中查找测试用例:
$ python -m unittest chapter16_4_unittest.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
除了比较两个值之外,这里还有一些测试预期值是True或False的例子。当发生失败时,我们还可以生成自定义的错误消息:
#!/usr/bin/env python3
# Examples from https://pymotw.com/3/unittest/index.html#module-unittest
import unittest
class Output(unittest.TestCase):
def testPass(self):
return
def testFail(self):
self.assertFalse(True, 'this is a failed message')
def testError(self):
raise RuntimeError('Test error!')
def testAssesrtTrue(self):
self.assertTrue(True)
def testAssertFalse(self):
self.assertFalse(False)
我们可以使用-v选项来显示更详细的信息输出:
$ python -m unittest -v chapter16_5_more_unittest
testAssertFalse (chapter16_5_more_unittest.Output) ... ok
testAssesrtTrue (chapter16_5_more_unittest.Output) ... ok
testError (chapter16_5_more_unittest.Output) ... ERROR
testFail (chapter16_5_more_unittest.Output) ... FAIL
testPass (chapter16_5_more_unittest.Output) ... ok
======================================================================
ERROR: testError (chapter16_5_more_unittest.Output)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter16/chapter16_5_more_unittest.py", line 14, in testError
raise RuntimeError('Test error!')
RuntimeError: Test error!
======================================================================
FAIL: testFail (chapter16_5_more_unittest.Output)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter16/chapter16_5_more_unittest.py", line 11, in testFail
self.assertFalse(True, 'this is a failed message')
AssertionError: True is not false : this is a failed message
----------------------------------------------------------------------
Ran 5 tests in 0.001s
FAILED (failures=1, errors=1)
从 Python 3.3 开始,unittest模块默认包含mock对象库(docs.python.org/3/library/unittest.mock.html)。这是一个非常有用的模块,您可以使用它来模拟对远程资源的 HTTP API 调用,而不实际进行调用。例如,我们看到了使用 NX-API 检索 NX-OS 版本号。如果我们想运行测试,但没有 NX-OS 设备可用怎么办?我们可以使用unittest模拟对象。
在 chapter16_5_more_unittest_mocks.py 中,我们创建了一个具有执行 HTTP API 调用并期望 JSON 响应的方法的类:
# Our class making API Call using requests
class MyClass:
def fetch_json(self, url):
response = requests.get(url)
return response.json()
我们还创建了一个模拟两个 URL 调用的函数:
# This method will be used by the mock to replace requests.get
def mocked_requests_get(*args, **kwargs):
class MockResponse:
def __init__(self, json_data, status_code):
self.json_data = json_data
self.status_code = status_code
def json(self):
return self.json_data
if args[0] == 'http://url-1.com/test.json':
return MockResponse({"key1": "value1"}, 200)
elif args[0] == 'http://url-2.com/test.json':
return MockResponse({"key2": "value2"}, 200)
return MockResponse(None, 404)
最后,我们在测试用例中对两个 URL 进行 API 调用。然而,我们正在使用 mock.patch 装饰器来拦截 API 调用:
# Our test case class
class MyClassTestCase(unittest.TestCase):
# We patch 'requests.get' with our own method. The mock object is passed in to our test case method.
@mock.patch('requests.get', side_effect=mocked_requests_get)
def test_fetch(self, mock_get):
# Assert requests.get calls
my_class = MyClass()
# call to url-1
json_data = my_class.fetch_json('http://url-1.com/test.json')
self.assertEqual(json_data, {"key1": "value1"})
# call to url-2
json_data = my_class.fetch_json('http://url-2.com/test.json')
self.assertEqual(json_data, {"key2": "value2"})
# call to url-3 that we did not mock
json_data = my_class.fetch_json('http://url-3.com/test.json')
self.assertIsNone(json_data)
if __name__ == '__main__':
unittest.main()
当我们运行测试时,我们将看到测试通过,而无需对远程端点进行实际的 API 调用。不错,对吧?
$ python chapter16_5_more_unittest_mocks.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
关于 unittest 模块的更多信息,Doug Hellmann 的 Python 模块每周一讲(pymotw.com/3/unittest/index.html#module-unittest)是关于 unittest 模块的短小精悍的示例的极好来源。像往常一样,Python 文档也是一个很好的信息来源:docs.python.org/3/library/unittest.html。
更多关于 Python 测试的内容
除了内置的 unittest 库之外,还有来自 Python 社区的许多其他测试框架。pytest 是最稳健、最直观的 Python 测试框架之一,值得一看。pytest 可以用于所有类型和级别的软件测试。它可以由开发者、QA 工程师、实践 TDD 的个人和开源项目使用。
许多大型开源项目已经从unittest或nose(另一个 Python 测试框架)切换到pytest,包括 Mozilla 和 Dropbox。pytest的吸引人之处包括第三方插件模型、简单的 fixture 模型和断言重写。
如果你想了解更多关于 pytest 框架的信息,我强烈推荐 Brian Okken 的《Python Testing with pytest》(ISBN 978-1-68050-240-4)。另一个很好的来源是 pytest 文档:docs.pytest.org/en/latest/。
pytest 是命令行驱动的;它可以自动找到我们编写的测试并运行它们,只需在我们的函数中添加 test 前缀即可。在我们能够使用它之前,我们需要安装 pytest:
$ pip install pytest
$ python
Python 3.10.6 (main, Aug 10 2022, 11:40:04) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pytest
>>> pytest.__version__
'7.1.3'
>>>
让我们看看一些使用 pytest 的示例。
pytest 示例
第一个 pytest 示例,chapter16_6_pytest_1.py,将是对两个值的简单断言:
#!/usr/bin/env python3
def test_passing():
assert(1, 2, 3) == (1, 2, 3)
def test_failing():
assert(1, 2, 3) == (3, 2, 1)
当我们使用 -v 选项运行 pytest 时,pytest 将为我们提供一个关于失败原因的相当健壮的答案。详细的输出是人们喜欢 pytest 的原因之一:
$ pytest -v chapter16_6_pytest_1.py
================================ test session starts =================================
platform linux -- Python 3.10.6, pytest-7.1.3, pluggy-1.0.0 -- /home/echou/Mastering_Python_Networking_Fourth_Edition/venv/bin/python3
cachedir: .pytest_cache
rootdir: /home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter16
collected 2 items
chapter16_6_pytest_1.py::test_passing PASSED [ 50%]
chapter16_6_pytest_1.py::test_failing FAILED [100%]
====================================== FAILURES ======================================
____________________________________ test_failing ____________________________________
def test_failing():
> assert(1, 2, 3) == (3, 2, 1)
E assert (1, 2, 3) == (3, 2, 1)
E At index 0 diff: 1 != 3
E Full diff:
E - (3, 2, 1)
E ? ^ ^
E + (1, 2, 3)
E ? ^ ^
chapter16_6_pytest_1.py:7: AssertionError
============================== short test summary info ===============================
FAILED chapter16_6_pytest_1.py::test_failing - assert (1, 2, 3) == (3, 2, 1)
============================ 1 failed, 1 passed in 0.03s =============================
在第二个 pytest 示例中,chapter16_7_pytest_2.py,我们将创建一个 router 对象。router 对象将使用一些 None 值和一些默认值进行初始化。我们将使用 pytest 测试一个使用默认值和一个未使用默认值的实例:
#!/usr/bin/env python3
class router(object):
def __init__(self, hostname=None, os=None, device_type='cisco_ios'):
self.hostname = hostname
self.os = os
self.device_type = device_type
self.interfaces = 24
def test_defaults():
r1 = router()
assert r1.hostname == None
assert r1.os == None
assert r1.device_type == 'cisco_ios'
assert r1.interfaces == 24
def test_non_defaults():
r2 = router(hostname='lax-r2', os='nxos', device_type='cisco_nxos')
assert r2.hostname == 'lax-r2'
assert r2.os == 'nxos'
assert r2.device_type == 'cisco_nxos'
assert r2.interfaces == 24
当我们运行测试时,我们将看到实例是否准确应用了默认值:
$ pytest chapter16_7_pytest_2.py
================================ test session starts =================================
platform linux -- Python 3.10.6, pytest-7.1.3, pluggy-1.0.0
rootdir: /home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter16
collected 2 items
chapter16_7_pytest_2.py .. [100%]
================================= 2 passed in 0.01s ==================================
如果我们要用 pytest 替换之前的 unittest 示例,在 chapter16_8_pytest_3.py 中,我们可以看到 pytest 的语法更简单:
# pytest test case
def test_version():
assert devices['nx-osv-1']['os'] == nxos_version
然后我们使用 pytest 命令行运行测试:
$ pytest chapter16_8_pytest_3.py
================================ test session starts =================================
platform linux -- Python 3.10.6, pytest-7.1.3, pluggy-1.0.0
rootdir: /home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter16
collected 1 item
chapter16_8_pytest_3.py . [100%]
================================= 1 passed in 3.80s ==================================
在 unittest 和 pytest 之间,我发现 pytest 更直观易用。然而,由于 unittest 包含在标准库中,许多团队可能更倾向于使用 unittest 模块进行测试。
除了对代码进行测试外,我们还可以编写测试来测试整个网络。毕竟,用户更关心他们的服务和应用程序能否正常工作,而不是单个组件。我们将在下一节中探讨如何编写针对网络的测试。
编写网络测试
到目前为止,我们主要编写的是针对我们 Python 代码的测试。我们使用了 unittest 和 pytest 库来断言 True/False 和 equal/non-equal 值。我们还能编写模拟来拦截我们的 API 调用,当我们没有实际的 API 兼容设备但仍想运行测试时。
在本节中,让我们看看如何编写与网络世界相关的测试。关于网络监控和测试的商业产品并不缺乏。多年来,我遇到了许多这样的产品。然而,在本节中,我更倾向于使用简单、开源的工具来进行测试。
测试可达性
故障排除的第一步通常是进行一个小范围的可达性测试。对于网络工程师来说,ping 是我们在进行网络可达性测试时的最佳伙伴。通过在网络中发送一个小数据包到目标,这是一种测试 IP 网络上主机可达性的方法。
我们可以通过 OS 模块或 subprocess 模块自动化 ping 测试:
>>> import os
>>> host_list = ['www.cisco.com', 'www.google.com']
>>> for host in host_list:
... os.system('ping -c 1 ' + host)
...
PING www.cisco.com(2001:559:19:289b::b33 (2001:559:19:289b::b33)) 56 data bytes
64 bytes from 2001:559:19:289b::b33 (2001:559:19:289b::b33): icmp_seq=1 ttl=60 time=11.3 ms
--- www.cisco.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 11.399/11.399/11.399/0.000 ms
0
PING www.google.com(sea15s11-in-x04.1e100.net (2607:f8b0:400a:808::2004)) 56 data bytes
64 bytes from sea15s11-in-x04.1e100.net (2607:f8b0:400a:808::2004): icmp_seq=1 ttl=54 time=10.8 ms
--- www.google.com ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 10.858/10.858/10.858/0.000 ms
0
subprocess 模块提供了额外的优势,可以捕获输出:
>>> import subprocess
>>> for host in host_list:
... print('host: ' + host)
... p = subprocess.Popen(['ping', '-c', '1', host], stdout=subprocess.PIPE)
...
host: www.cisco.com
host: www.google.com
>>> print(p.communicate())
(b'PING www.google.com(sea15s11-in-x04.1e100.net (2607:f8b0:400a:808::2004)) 56 data bytes\n64 bytes from sea15s11-in-x04.1e100.net (2607:f8b0:400a:808::2004): icmp_seq=1 ttl=54 time=16.9 ms\n\n--- www.google.com ping statistics ---\n1 packets transmitted, 1 received, 0% packet loss, time 0ms\nrtt min/avg/max/mdev = 16.913/16.913/16.913/0.000 ms\n', None)
>>>
这两个模块在许多情况下都证明是非常有用的。我们可以在 Linux 和 Unix 环境中执行的任何命令都可以通过 OS 或 subprocess 模块来执行。
测试网络延迟
网络延迟的话题有时可能是主观的。作为网络工程师,我们经常面临用户说网络慢的情况。然而,“慢”是一个非常主观的术语。
如果我们能构建将主观术语转化为客观值的测试,这将非常有帮助。我们应该持续这样做,以便在数据的时间序列中比较值。
这有时可能很困难,因为网络设计上是无状态的。仅仅因为一个数据包成功,并不能保证下一个数据包也会成功。多年来,我看到的最有效的方法是频繁地对多个主机使用 ping 并记录数据,进行 ping-mesh 图。我们可以利用之前示例中使用的相同工具,捕获返回结果的时间,并保存记录。我们在 chapter16_10_ping.py 中这样做:
#!/usr/bin/env python3
import subprocess
host_list = ['www.cisco.com', 'www.google.com']
ping_time = []
for host in host_list:
p = subprocess.Popen(['ping', '-c', '1', host], stdout=subprocess.PIPE)
result = p.communicate()[0]
host = result.split()[1]
time = result.split()[13]
ping_time.append((host, time))
print(ping_time)
在这种情况下,结果被保存在一个 tuple 中,并放入一个 list 中:
$ python chapter16_10_ping.py
[(b'e2867.dsca.akamaiedge.net', b'ttl=54'), (b'www.google.com', b'ttl=58')]
这并不完美,这只是监控和故障排除的起点。然而,在没有其他工具的情况下,这提供了一些基准的客观值。
测试安全性
我们在第六章,Python 网络安全中看到了最好的安全测试工具之一,那就是 Scapy。有很多开源的安全工具,但没有一个提供我们构建数据包时的灵活性。
另一个用于网络安全测试的出色工具是hping3(docs.python-cerberus.org/en/stable/)。它提供了一种简单的方法一次性生成大量数据包。例如,你可以使用以下单行命令生成 TCP SYN 洪水:
# DON'T DO THIS IN PRODUCTION #
echou@ubuntu:/var/log$ sudo hping3 -S -p 80 --flood 192.168.1.202
HPING 192.168.1.202 (eth0 192.168.1.202): S set, 40 headers + 0 data bytes hping in flood mode, no replies will be shown
^C
--- 192.168.1.202 hping statistic ---
2281304 packets transmitted, 0 packets received, 100% packet loss round-trip min/avg/max = 0.0/0.0/0.0 ms
echou@ubuntu:/var/log$
再次强调,由于这是一个命令行工具,我们可以使用subprocess模块来自动化我们想要的任何hping3测试。
交易测试
网络是基础设施的重要组成部分,但它只是其中的一部分。用户关心的是运行在网络上方的服务。如果用户试图观看 YouTube 视频或收听播客,但按他们的观点无法做到,那么他们认为服务是出了问题。我们可能知道网络传输没有问题,但这并不能安慰用户。
因此,我们应该实现尽可能接近用户体验的测试。在 YouTube 视频的例子中,我们可能无法 100%地复制 YouTube 体验(除非你在 Google 工作)。
尽管如此,我们可以在尽可能接近网络边缘的地方实现第 7 层服务。然后我们可以定期模拟客户端的交易作为交易测试。
当我们需要快速测试网络服务的第 7 层可达性时,Python 的HTTP标准库模块是我经常使用的一个模块。我们在第四章,Python 自动化框架 – Ansible中已经看到了如何使用它来进行网络监控,但再次看看也是值得的:
$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 ...
127.0.0.1 - - [25/Jul/2018 10:15:23] "GET / HTTP/1.1" 200 -
如果我们可以模拟预期服务的完整交易,那就更好了。但 Python 标准库中的简单HTTP服务器模块始终是运行一些临时网络服务测试的一个很好的选择。
网络配置测试
在我看来,网络配置的最佳测试方法是使用标准化模板生成配置,并经常备份生产配置。我们已经看到了如何使用 Jinja2 模板来根据设备类型或角色标准化我们的配置。这将消除许多由人为错误引起的错误,例如复制粘贴。
一旦生成了配置,我们就可以针对配置编写测试,以验证在将配置推送到生产设备之前我们期望的已知特性。例如,当涉及到回环 IP 时,所有网络中的 IP 地址都不应该有重叠,因此我们可以编写一个测试来查看新配置是否包含一个在我们设备中独一无二的回环 IP。
Ansible 测试
在我使用 Ansible 的时间里,我不记得使用过类似unittest的工具来测试 playbook。大部分情况下,playbooks 使用的是模块开发者已经测试过的模块。
如果您需要一个轻量级的数据验证工具,请查看 Cerberus (docs.python-cerberus.org/en/stable/)。
Ansible 为其模块库提供单元测试。Ansible 中的单元测试目前是唯一一种在 Ansible 的持续集成过程中从 Python 驱动测试的方式。今天运行的单元测试可以在/test/units下找到 (github.com/ansible/ansible/tree/devel/test/units)。
Ansible 测试策略可以在以下文档中找到:
-
测试 Ansible:
docs.ansible.com/ansible/latest/dev_guide/testing.html -
单元测试:
docs.ansible.com/ansible/latest/dev_guide/testing_units.html -
单元测试 Ansible 模块:
docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html
有趣的 Ansible 测试框架之一是 Molecule (pypi.org/project/molecule/)。它旨在帮助开发 Ansible 角色并进行测试。Molecule 支持使用多个实例、操作系统和发行版进行测试。我尚未使用此工具,但如果我想对我的 Ansible 角色进行更多测试,我会从这里开始。
我们现在应该知道如何为我们的网络编写测试,无论是测试可达性、延迟、安全性、事务还是网络配置。
在下一节中,我们将探讨由 Cisco(最近作为开源发布)开发的一个广泛的测试框架,称为pyATS。Cisco 将这样一个广泛的框架作为开源提供给社区,这是一个值得称赞的举动。
pyATS 和 Genie
pyATS (developer.cisco.com/pyats/) 是由 Cisco 最初开发的一个端到端测试生态系统,并于 2017 年底向公众开放。pyATS 库以前被称为 Genie;它们通常在相同的情况下被提及。由于其根源,该框架非常专注于网络测试。
pyATS,以及 pyATS 库(也称为 Genie),是 2018 年 Cisco 先锋奖的获得者。我们应该为 Cisco 将框架开源并公开提供而鼓掌。干得好,Cisco DevNet!
该框架可在 PyPI 上找到:
$ pip install pyats
要开始,我们可以查看 GitHub 仓库中的一些示例脚本,github.com/CiscoDevNet/pyats-sample-scripts。测试从创建 YAML 格式的测试平台文件开始。我们将为我们的lax-edge-r1-edg-r1设备创建一个简单的chapter16_pyats_testbed_1.yml测试平台文件。文件应类似于我们之前见过的 Ansible 清单文件:
testbed:
name: Chapter_16_pyATS
tacacs:
username: cisco
passwords:
tacacs: cisco
enable: cisco
devices:
lax-edg-r1:
alias: lax-edg-r1
type: ios
connections:
defaults:
class: unicon.Unicon
management:
ip: 192.168.2.51
protocol: ssh
topology:
lax-edg-r1:
interfaces:
GigabitEthernet0/1:
ipv4: 10.0.0.1/30
link: link-1
type: ethernet
Loopback0:
ipv4: 192.168.0.10/32
link: iosv-1_Loopback0
type: loopback
在我们的第一个脚本chapter16_11_pyats_1.py中,我们将加载测试平台文件,连接到设备,发出show version命令,然后从设备断开连接:
#!/usr/bin/env python3
#
# derived from https://devnet-pubhub-site.s3.amazonaws.com/media/pyats/docs/getting_started/index.html
#
from pyats.topology import loader
# load testbed
testbed = loader.load('chapter16_pyats_testbed_1.yml')
# access the device
testbed.devices
lax_edg_r1 = testbed.devices['lax-edg-r1']
# establish connectivity
lax_edg_r1.connect()
# issue command
print(lax_edg_r1.execute('show version'))
# disconnect
lax_edg_r1.disconnect()
当我们执行命令时,我们可以看到输出是pyATS设置以及设备实际输出的混合。这与我们之前看到的 Paramiko 脚本类似,但请注意pyATS已经为我们处理了底层连接:
$ python chapter16_11_pyats_1.py
/home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter16/pyATS/chapter16_11_pyats_1.py:8: DeprecationWarning: 'tacacs.username' is deprecated in the testbed YAML. This key has been moved to 'credentials'.
testbed = loader.load('chapter16_pyats_testbed_1.yml')
/home/echou/Mastering_Python_Networking_Fourth_Edition/Chapter16/pyATS/chapter16_11_pyats_1.py:8: DeprecationWarning: 'passwords.tacacs' is deprecated in the testbed YAML. Use 'credentials' instead.
testbed = loader.load('chapter16_pyats_testbed_1.yml')
device's os is not provided, unicon may not use correct plugins
2022-09-25 17:03:08,615: %UNICON-INFO: +++ lax-edg-r1 logfile /tmp/lax-edg-r1-cli-20220925T170308615.log +++
<skip>
2022-09-25 17:03:09,275: %UNICON-INFO: +++ connection to spawn: ssh -l cisco 192.168.2.51, id: 140685765498848 +++
2022-09-25 17:03:09,276: %UNICON-INFO: connection to lax-edg-r1
cisco@192.168.2.51's password:
**************************************************************************
* IOSv is strictly limited to use for evaluation, demonstration and IOS *
* education. IOSv is provided as-is and is not supported by Cisco's *
* Technical Advisory Center. Any use or disclosure, in whole or in part, *
* of the IOSv Software or Documentation to any third party for any *
* purposes is expressly prohibited except as otherwise authorized by *
* Cisco in writing. *
**************************************************************************
lax-edg-r1#
2022-09-25 17:03:09,364: %UNICON-INFO: +++ initializing handle +++
2022-09-25 17:03:09,427: %UNICON-INFO: +++ lax-edg-r1 with via 'management': executing command 'term length 0' +++
term length 0
lax-edg-r1#
2022-09-25 17:03:09,617: %UNICON-INFO: +++ lax-edg-r1 with via 'management': executing command 'term width 0' +++
term width 0
lax-edg-r1#
2022-09-25 17:03:09,821: %UNICON-INFO: +++ lax-edg-r1 with via 'management': executing command 'show version' +++
show version
Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2019 by Cisco Systems, Inc.
Compiled Thu 28-Mar-19 14:06 by prod_rel_team
在第二个示例中,我们将看到一个完整的连接设置、测试用例然后连接拆除的示例。首先,我们将lax-cor-r1设备添加到我们的测试平台chapter16_pyats_testbed_2.yml中。由于需要作为 ping 测试的连接设备,所以需要额外的设备:
testbed:
name: Chapter_16_pyATS
tacacs:
username: cisco
passwords:
tacacs: cisco
enable: cisco
devices:
lax-edg-r1:
alias: iosv-1
type: ios
connections:
defaults:
class: unicon.Unicon
vty:
ip: 192.168.2.50
protocol: ssh
lax-cor-r1:
alias: nxosv-1
type: ios
connections:
defaults:
class: unicon.Unicon
vty:
ip: 192.168.2.51
protocol: ssh
topology:
lax-edg-r1:
interfaces:
GigabitEthernet0/1:
ipv4: 10.0.0.1/30
link: link-1
type: ethernet
Loopback0:
ipv4: 192.168.0.10/32
link: lax-edg-r1_Loopback0
type: loopback
lax-cor-r1:
interfaces:
Eth2/1:
ipv4: 10.0.0.2/30
link: link-1
type: ethernet
Loopback0:
ipv4: 192.168.0.100/32
link: lax-cor-r1_Loopback0
type: loopback
在chapter16_12_pyats_2.py中,我们将使用来自pyATS的aest模块和各种装饰器。除了设置和清理,ping测试位于PingTestCase类中:
@aetest.loop(device = ('ios1',))
class PingTestcase(aetest.Testcase):
@aetest.test.loop(destination = ('10.0.0.1', '10.0.0.2'))
def ping(self, device, destination):
try:
result = self.parameters[device].ping(destination)
except Exception as e:
self.failed('Ping {} from device {} failed with error: {}'.format(
destination,
device,
str(e),
),
goto = ['exit'])
else:
match = re.search(r'Success rate is (?P<rate>\d+) percent', result)
success_rate = match.group('rate')
在运行时,最好在命令行中引用测试平台文件:
$ python chapter16_12_pyats_2.py --testbed chapter16_pyats_testbed_2.yml
输出与我们的第一个示例类似,增加了每个测试用例的STEPS Report和Detailed Results。
2022-09-25T17:14:13: %AETEST-INFO: +------------------------------------------------------------------------------+
2022-09-25T17:14:13: %AETEST-INFO: | Starting common setup |
2022-09-25T17:14:13: %AETEST-INFO: +------------------------------------------------------------------------------+
2022-09-25T17:14:13: %AETEST-INFO: +------------------------------------------------------------------------------+
2022-09-25T17:14:13: %AETEST-INFO: | Starting subsection check_topology |
2022-09-25T17:14:13: %AETEST-INFO: +------------------------------------------------------------------------------+
2022-09-25T17:14:13: %AETEST-INFO: The result of subsection check_topology is => PASSED
2022-09-25T17:14:13: %AETEST-INFO: +------------------------------------------------------------------------------+
2022-09-25T17:14:13: %AETEST-INFO: | Starting subsection establish_connections |
2022-09-25T17:14:13: %AETEST-INFO: +------------------------------------------------------------------------------+
2022-09-25T17:14:13: %AETEST-INFO: +..............................................................................+
2022-09-25T17:14:13: %AETEST-INFO: : Starting STEP 1: Connecting to lax-edg-r1 :
2022-09-25T17:14:13: %AETEST-INFO: +..............................................................................+
2022-09-25T17:14:13: %UNICON-WARNING: device's os is not provided, unicon may not use correct plugins
输出还指示了写入/tmp目录的日志文件名:
$ ls /tmp/lax*
/tmp/lax-edg-r1-cli-20220925T170012042.log
/tmp/lax-edg-r1-cli-20220925T170030754.log
/tmp/lax-edg-r1-cli-20220925T170308615.log
/tmp/lax-edg-r1-cli-20220925T171145090.log
/tmp/lax-edg-r1-cli-20220925T171413444.log
$ head -20 /tmp/lax-edg-r1-cli-20220925T170012042.log
2022-09-25 17:00:12,043: %UNICON-INFO: +++ lax-edg-r1 logfile /tmp/lax-edg-r1-cli-20220925T170012042.log +++
2022-09-25 17:00:12,043: %UNICON-INFO: +++ Unicon plugin generic (unicon.plugins.generic) +++
**************************************************************************
* IOSv is strictly limited to use for evaluation, demonstration and IOS *
* education. IOSv is provided as-is and is not supported by Cisco's *
* Technical Advisory Center. Any use or disclosure, in whole or in part, *
* of the IOSv Software or Documentation to any third party for any *
* purposes is expressly prohibited except as otherwise authorized by *
* Cisco in writing. *
**************************************************************************
2022-09-25 17:00:12,705: %UNICON-INFO: +++ connection to spawn: ssh -l cisco 192.168.2.51, id: 140482828326976 +++
2022-09-25 17:00:12,706: %UNICON-INFO: connection to lax-edg-r1
cisco@192.168.2.51's password:
**************************************************************************
pyATS 框架是一个优秀的自动化测试框架。然而,由于其起源,对思科以外的供应商的支持略显不足。
一个值得注意的开源网络验证工具是来自 Intentionet 团队的 Batfish,github.com/batfish/batfish,其主要用途是在部署前验证配置更改。另一个开源项目是 Suzieq (suzieq.readthedocs.io/en/latest/)。Suzieq 是第一个开源的多供应商网络可观察性平台应用程序。
pyATS的学习曲线有点陡峭;它基本上有自己的测试执行方式,需要一些时间来适应。可以理解的是,它在其当前版本中也高度关注思科平台。pyATS的核心是闭源的,并以二进制形式发布。为与pyATS一起使用而开发的包,如解析库、YANG 连接器和各种插件是开源的。对于开源部分,我们都被鼓励做出贡献,如果我们想添加额外的供应商支持或进行语法或流程更改的话。
我们接近本章的结尾,让我们回顾一下本章我们做了什么。
摘要
在本章中,我们探讨了测试驱动开发(TDD)及其在网络工程中的应用。我们首先对 TDD 进行了概述;然后,我们通过使用unittest和pytestPython 模块的示例进行了说明。Python 和简单的 Linux 命令行工具可以用来构建网络可达性、配置和安全性的测试。
pyATS 是思科发布的一个工具。它是一个以网络为中心的自动化测试框架,我们可以利用它。
简而言之,如果没有经过测试,我们就无法信任它。我们网络中的每一件事都应尽可能地进行程序化测试。与许多软件概念一样,TDD 是一个永无止境的服务轮。我们努力实现尽可能多的测试覆盖率,但即使在 100%的测试覆盖率下,我们总能找到新的方法和测试案例来实施。这在网络领域尤其如此,因为网络通常是互联网,而互联网的 100%测试覆盖率是不可能的。
我们已经接近本书的结尾。我希望您发现这本书的阅读乐趣与我写作时的乐趣一样。我想真诚地说一声“谢谢”,感谢您抽出时间阅读这本书。祝您在 Python 网络之旅中取得成功和幸福!
加入我们的书籍社区
要加入这本书的社区——在那里您可以分享反馈、向作者提问,并了解新版本——请扫描下面的二维码:
packt.link/networkautomationcommunity



浙公网安备 33010602011771号