Python-企业自动化实用指南-全-

Python 企业自动化实用指南(全)

原文:zh.annas-archive.org/md5/0bfb2f4dbc80a06d99550674abb53d0d

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

本书首先介绍了建立 Python 环境以执行自动化任务的设置,以及您将使用的模块、库和工具。

我们将使用简单的 Python 程序和 Ansible 探索网络自动化任务的示例。接下来,我们将带您了解如何使用 Python Fabric 自动化管理任务,您将学习执行服务器配置和管理以及系统管理任务,如用户管理、数据库管理和进程管理。随着您在本书中的进展,您将使用 Python 脚本自动化多个测试服务,并使用 Python 在虚拟机和云基础架构上执行自动化任务。在最后几章中,您将涵盖基于 Python 的攻击性安全工具,并学习如何自动化您的安全任务。

通过本书,您将掌握使用 Python 自动化多个系统管理任务的技能。

您可以访问作者的博客,链接如下:basimaly.wordpress.com/.

这本书是为谁准备的

使用 Python 进行企业自动化适用于寻找 Puppet 和 Chef 等主要自动化框架替代方案的系统管理员和 DevOps 工程师。需要具备 Python 和 Linux shell 脚本的基本编程知识。

这本书涵盖了什么

第一章,设置 Python 环境,探讨了如何下载和安装 Python 解释器以及名为JetBrains PyCharm的 Python 集成开发环境。该 IDE 提供智能自动完成、智能代码分析、强大的重构,并与 Git、virtualenv、Vagrant 和 Docker 集成。这将帮助您编写专业和健壮的 Python 代码。

第二章,自动化中使用的常见库,介绍了今天可用的用于自动化的 Python 库,并根据它们的用途(系统、网络和云)进行分类,并提供简单的介绍。随着您在本书中的进展,您将发现自己深入研究每一个库,并了解它们的用途。

第三章,设置您的网络实验室环境,讨论了网络自动化的优点以及网络运营商如何使用它来自动化当前的设备。我们将探讨今天用于自动化来自思科、Juniper 和 Arista 的网络节点的流行库。本章介绍了如何构建一个网络实验室来应用 Python 脚本。我们将使用一个名为 EVE-NG 的开源网络仿真工具。

第四章,使用 Python 管理网络设备,深入介绍了通过 telnet 和 SSH 连接使用 netmiko、paramiko 和 telnetlib 管理网络设备。我们将学习如何编写 Python 代码来访问交换机和路由器,并在终端上执行命令,然后返回输出。我们还将学习如何利用不同的 Python 技术来备份和推送配置。本章以当今现代网络环境中使用的一些用例结束。

第五章,从网络设备中提取有用数据,解释了如何使用 Python 内部的不同工具和技术从返回的输出中提取有用数据并对其进行操作。此外,我们将使用一个名为CiscoConfParse的特殊库来审计配置。然后,我们将学习如何可视化数据,使用 matplotlib 生成吸引人的图表和报告。

第六章《使用 Python 和 Jinja2 生成配置文件》解释了如何为拥有数百个网络节点的站点生成通用配置。我们将学习如何编写模板,并使用 Jinja2 模板语言生成黄金配置。

第七章《Python 脚本的并行执行》涵盖了如何并行实例化和执行 Python 代码。只要不相互依赖,这将使我们能够更快地完成自动化工作流程。

第八章《准备实验室环境》涵盖了实验室环境的安装过程和准备工作。我们将在不同的虚拟化器上安装我们的自动化服务器,无论是在 CentOS 还是 Ubuntu 上。然后我们将学习如何使用 Cobbler 自动安装操作系统。

第九章《使用 Subprocess 模块》解释了如何从 Python 脚本直接发送命令到操作系统 shell 并调查返回的输出。

第十章《使用 Fabric 运行系统管理任务》介绍了 Fabric,这是一个用于通过 SSH 执行系统管理任务的 Python 库。它也用于大规模应用部署。我们将学习如何利用和发挥这个库来在远程服务器上执行任务。

第十一章《生成系统报告》、《管理用户和系统监控》解释了从系统收集数据并生成定期报告对于任何系统管理员来说都是一项重要任务,自动化这项任务将帮助您及早发现问题并为其提供解决方案。在本章中,我们将看到一些经过验证的自动化从服务器收集数据并生成正式报告的方法。我们将学习如何使用 Python 和 Ansible 管理新用户和现有用户。此外,我们还将深入研究系统 KPI 的监控和日志分析。您还可以安排监控脚本定期运行,并将结果发送到您的邮箱。

第十二章《与数据库交互》指出,如果你是数据库管理员或数据库开发人员,那么 Python 提供了许多库和模块,涵盖了管理和操作流行的 DBMS(如 MySQL、Postgress 和 Oracle)。在本章中,我们将学习如何使用 Python 连接器与 DBMS 进行交互。

第十三章《系统管理的 Ansible》探讨了配置管理软件中最强大的工具之一。当涉及系统管理时,Ansible 非常强大,可以确保配置在数百甚至数千台服务器上同时精确复制。

第十四章《创建和管理 VMWare 虚拟机》解释了如何在 VMWare 虚拟化器上自动创建 VM。我们将探索使用 VMWare 官方绑定库在 ESXi 上创建和管理虚拟机的不同方法。

第十五章《与 Openstack API 交互》解释了在创建私有云时,OpenStack 在私有 IaaS 方面非常受欢迎。我们将使用 Python 模块,如 requests,创建 REST 调用并与 OpenStack 服务(如 nova、cinder 和 neutron)进行交互,并在 OpenStack 上创建所需的资源。在本章后期,我们将使用 Ansible playbooks 执行相同的工作流程。

第十六章,使用 Python 和 Boto3 自动化 AWS,介绍了如何使用官方的 Amazon 绑定(BOTO3)自动化常见的 AWS 服务,如 EC2 和 S3,它提供了一个易于使用的 API 来访问服务。

第十七章,使用 SCAPY 框架,介绍了 SCAPY,这是一个强大的 Python 工具,用于构建和制作数据包,然后将其发送到网络上。您可以构建任何类型的网络流并将其发送到网络上。它还可以帮助您捕获网络数据包并将其重放到网络上。

第十八章,使用 Python 构建网络扫描器,提供了使用 Python 构建网络扫描器的完整示例。您可以扫描完整的子网以查找不同的协议和端口,并为每个扫描的主机生成报告。然后,我们将学习如何通过 Git 与开源社区(GitHub)共享代码。

为了充分利用本书

读者应该熟悉 Python 编程语言的基本编程范式,并且应该具有 Linux 和 Linux shell 脚本的基本知识。

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便直接将文件发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packtpub.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩软件解压缩文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Enterprise-Automation-with-Python。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自我们丰富的图书和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/HandsOnEnterpriseAutomationwithPython_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码字词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。这是一个例子:"一些大型软件包,如matplotlibdjango,其中包含数百个模块,开发人员通常将相关模块分类到子目录中。"

代码块设置如下:

from netmiko import ConnectHandler
from devices import R1,SW1,SW2,SW3,SW4

nodes = [R1,SW1,SW2,SW3,SW4]   for device in nodes:
  net_connect = ConnectHandler(**device)
  output = net_connect.send_command("show run")
  print output

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目将以粗体显示:

hostname {{hostname}}

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

pip install jinja2 

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:

"从下载页面选择您的平台,然后选择 x86 或 x64 版本。"

警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。

第一章:设置我们的 Python 环境

在这一章中,我们将简要介绍 Python 编程语言以及当前版本之间的区别。Python 有两个活跃版本,并且在开发过程中选择使用哪一个是很重要的。在这一章中,我们将下载并安装 Python 二进制文件到操作系统中。

在本章结束时,我们将安装全球专业开发人员使用的最先进的集成开发环境IDE)之一:PyCharm。PyCharm 提供智能代码完成、代码检查、实时错误突出显示和快速修复、自动代码重构以及丰富的导航功能,我们将在本书中编写和开发 Python 代码时进行介绍。

本章将涵盖以下主题:

  • Python 简介

  • 安装 PyCharm IDE

  • 探索一些巧妙的 PyCharm 功能

Python 简介

Python 是一种提供友好语法的高级编程语言;它易于学习和使用,无论是初学者还是专家程序员。

Python 最初是由 Guido van Rossum 于 1991 年开发的;它依赖于 C、C++和其他 Unix shell 工具的混合。Python 被称为通用编程语言,并且今天它被用于许多领域,如软件开发、Web 开发、网络自动化、系统管理和科学领域。由于其大量可供下载的模块,涵盖了许多领域,Python 可以将开发时间缩短到最低。

Python 语法设计得易读;它与英语有些相似,而代码本身的构造也很美。Python 核心开发人员提供了 20 条信息规则,称为 Python 之禅,这些规则影响了 Python 语言的设计;其中大部分涉及构建清洁、有组织和易读的代码。以下是其中一些规则:

美好优于丑陋。

显式优于隐式。

简单优于复杂。

复杂优于复杂。

您可以在www.python.org/dev/peps/pep-0020/上阅读更多关于 Python 之禅的内容。

Python 版本

Python 有两个主要版本:Python 2.x 和 Python 3.x。这两个版本之间有细微的差异;最明显的是它们的print函数对多个字符串的处理方式。此外,所有新功能只会添加到 3.x,而 2.x 将在完全退役之前接收安全更新。这不会是一个简单的迁移,因为许多应用程序都是基于 Python 2.x 构建的。

为什么有两个活跃版本?

我将引用官方 Python 网站上的原因:

"Guido van Rossum(Python 语言的原始创造者)决定彻底清理 Python 2.x,不像对 2.x 范围内的新版本那样考虑向后兼容性。最重大的改进是更好的 Unicode 支持(默认情况下所有文本字符串都是 Unicode),以及更合理的字节/Unicode 分离。 "此外,核心语言的几个方面(如 print 和 exec 作为语句,整数使用地板除法)已经调整为更容易让新手学习,并且更一致于语言的其他部分,并且已经删除了旧的不必要的东西(例如,所有类现在都是新式的,“range()”返回一个内存高效的可迭代对象,而不是像 2.x 中的列表那样)。

您可以在wiki.python.org/moin/Python2orPython3上阅读更多关于这个主题的内容。

您是否应该只学习 Python 3?

这取决于.学习 Python 3 将使您的代码具有未来性,并且您将使用开发人员提供的最新功能。但是,请注意,一些第三方模块和框架不支持 Python 3,并且在不久的将来将继续如此,直到他们完全将他们的库移植到 Python 3。

另外,请注意,一些网络供应商(如思科)对 Python 3.x 提供有限支持,因为大多数所需功能已经在 Python 2.x 版本中涵盖。例如,以下是思科设备支持的 Python 版本;您将看到所有设备都支持 2.x,而不支持 3.x:

来源:developer.cisco.com/site/python/

这是否意味着我不能编写在 Python 2 和 Python 3 上运行的代码?

不,当然可以在 Python 2.x 中编写代码并使其与两个版本兼容,但您需要首先导入一些库,例如__future__模块,使其向后兼容。此模块包含一些函数,可以调整 Python 2.x 的行为,并使其与 Python 3.x 完全相同。看一下以下示例,以了解两个版本之间的区别:

#python 2 only print "Welcome to Enterprise Automation" 

以下代码适用于 Python 2 和 3:

# python 2 and 3  print("Welcome to Enterprise Automation")

现在,如果您需要打印多个字符串,Python 2 的语法将如下所示:

# python 2, multiple strings print "welcome", "to", "Enterprise", "Automation"   # python 3, multiple strings print ("welcome", "to", "Enterprise", "Automation")

如果您尝试在 Python 2 中使用括号打印多个字符串,它将将其解释为元组,这是错误的。因此,我们将在代码开头导入__future__模块,以防止该行为并指示 Python 打印多个字符串。

输出将如下所示:

Python 安装

无论您选择使用流行的 Python 版本(2.x)还是使用 Python 3.x 构建未来的代码,您都需要从官方网站下载 Python 二进制文件并在操作系统中安装它们。 Python 支持不同的平台(Windows,Mac,Linux,Raspberry PI 等):

  1. 转到www.python.org/downloads/并选择最新的 2.x 或 3.x 版本:

  1. 从下载页面选择您的平台,以及 x86 或 x64 版本:

  1. 像往常一样安装软件包。在安装过程中选择将 python 添加到路径选项很重要,以便从命令行访问 Python(在 Windows 的情况下)。否则,Windows 将无法识别 Python 命令并将抛出错误:

  1. 通过在操作系统中打开命令行或终端并键入python来验证安装是否完成。这应该访问 Python 控制台并提供 Python 已成功安装在您的系统上的验证:

安装 PyCharm IDE

PyCharm 是一个完整的 IDE,被世界各地的许多开发人员用来编写和开发 Python 代码。这个 IDE 是由 Jetbrains 公司开发的,提供丰富的代码分析和完成,语法高亮,单元测试,代码覆盖率,错误发现和其他 Python 代码操作。

此外,PyCharm 专业版支持 Python Web 框架,如 Django,web2py 和 Flask,以及与 Docker 和 vagrant 的集成。它与多个版本控制系统(如 Git(和 GitHub),CVS 和 subversion)提供了惊人的集成。

在接下来的几个步骤中,我们将安装 PyCharm 社区版:

  1. 转到 PyCharm 下载页面(www.jetbrains.com/pycharm/download/)并选择您的平台。此外,选择下载 Community Edition(永久免费)或 Professional Edition(Community 版本完全适用于运行本书中的代码):

  1. 像往常一样安装软件,但确保选择以下选项:
  • 32 位或 64 位的启动器(取决于您的操作系统)。

  • 创建关联(这将使 PyCharm 成为 Python 文件的默认应用程序)。

  • 下载并安装 JetBrains 的 JRE x86:

  1. 等待 PyCharm 从互联网下载附加包并安装它,然后选择运行 PyCharm 社区版:

  1. 由于这是一个新的安装,我们不会从中导入任何设置

  1. 选择所需的 UI 主题(默认或darcula,用于暗模式)。您可以安装一些附加插件,例如 Markdown 和 BashSupport,这将使 PyCharm 识别和支持这些语言。完成后,单击开始使用 PyCharm:

在 PyCharm 中设置 Python 项目

在 PyCharm 中,一个 Python 项目是你开发的 Python 文件的集合,以及内置的或从第三方安装的 Python 模块。在开始开发代码之前,您需要创建一个新项目并将其保存到计算机内的特定位置。此外,您需要为该项目选择默认解释器。默认情况下,PyCharm 将扫描系统上的默认位置并搜索 Python 解释器。另一个选项是使用 Python virtualenv 创建一个完全隔离的环境。virtualenv的基本问题是其包依赖性。假设您正在处理多个不同的 Python 项目,其中一个项目需要特定版本的x包。另一方面,另一个项目需要完全不同版本的相同包。请注意,所有安装的 Python 包都放在/usr/lib/python2.7/site-packages中,您无法存储相同包的不同版本。virtualenv将通过创建一个具有自己的安装目录和自己的包的环境来解决此问题;每次您在这两个项目中的任何一个上工作时,PyCharm(借助virtualenv的帮助)将激活相应的环境,以避免包之间的任何冲突。

按照以下步骤设置项目:

  1. 选择创建新项目:

  1. 选择项目设置:

    1. 选择项目类型;在我们的情况下,它将是纯 Python.
  1. 在本地硬盘上选择项目的位置。

  2. 选择项目解释器。要么使用默认目录中现有的 Python 安装,要么创建一个专门与该项目绑定的新虚拟环境。

  3. 单击 Create.

  4. 在项目内创建一个新的 Python 文件:

    1. 右键单击项目名称,然后选择 New。
  1. 从菜单中选择 Python 文件,然后选择文件名。

打开一个新的空白文件,您可以直接在其中编写 Python 代码。例如,尝试导入__future__模块,PyCharm 将自动打开一个弹出窗口,显示所有可能的补全,如下面的屏幕截图所示:

  1. 运行您的代码:

    1. 输入您希望运行的代码。
  1. 选择编辑配置以配置 Python 文件的运行时设置。

  2. 配置运行文件的新 Python 设置:

    1. 单击+号添加新配置,然后选择 Python。
  1. 选择配置名称。

  2. 选择项目内的脚本路径。

  3. 单击确定。

  4. 运行代码:

    1. 单击配置名称旁边的播放按钮。
  1. PyCharm 将执行配置中指定的文件中的代码,并将输出返回到终端。

探索一些巧妙的 PyCharm 功能

在本节中,我们将探讨 PyCharm 的一些特性。PyCharm 拥有大量的内置工具,包括集成调试器和测试运行器、Python 分析器、内置终端、与主要版本控制系统的集成和内置数据库工具、远程开发能力与远程解释器、集成 SSH 终端,以及与 Docker 和 Vagrant 的集成。有关其他功能的列表,请查看官方网站(www.jetbrains.com/pycharm/features/)。

代码调试

代码调试是一个过程,可以帮助您了解错误的原因,通过为代码提供输入并逐行查看代码的执行情况,以及最终的评估结果。Python 语言包含一些调试工具,从简单的print函数、assert 命令到代码的完整单元测试。PyCharm 提供了一种简单的调试代码和查看评估值的方法。

要在 PyCharm 中调试代码(比如,一个带有if子句的嵌套for循环),您需要在希望 PyCharm 停止程序执行的行上设置断点。当 PyCharm 到达这一行时,它将暂停程序并转储内存以查看每个变量的内容:

请注意,在第一次迭代时,每个变量的值都会被打印在其旁边:

此外,您还可以右键单击断点,并为任何变量添加特定条件。如果变量评估为特定值,那么将打印日志消息:

代码重构

重构代码是更改代码中特定变量名称结构的过程。例如,您可能为变量选择一个名称,并在由多个源文件组成的项目中使用它,然后决定将变量重命名为更具描述性的名称。PyCharm 提供了许多重构技术,以确保代码可以更新而不会破坏操作。

PyCharm 执行以下操作:

  • 重构本身

  • 扫描项目中的每个文件,并确保变量的引用已更新

  • 如果某些内容无法自动更新,它将给出警告并打开一个菜单,让您决定如何处理

  • 在重构代码之前保存代码,以便以后可以恢复

让我们来看一个例子。假设我们的项目中有三个 Python 文件,分别为refactor_1.pyrefactor_2.pyrefactor_3.py。第一个文件包含important_funtion(x),它也在refactor_2.pyrefactor_3.py中使用。

将以下代码复制到refactor_1.py文件中:

def important_function(x):
  print(x)

将以下代码复制到refactor_2.py文件中:

from refactor_1 import important_function
important_function(2)

将以下代码复制到refactor_3.py文件中:

from refactor_1 import important_function
important_function(10)

要进行重构,您需要右键单击方法本身,选择重构 | 重命名,并输入方法的新名称:

请注意,IDE 底部会打开一个窗口,列出此函数的所有引用,每个引用的当前值,以及重构后将受影响的文件:

如果选择执行重构,所有引用将使用新名称进行更新,您的代码将不会被破坏。

从 GUI 安装包

PyCharm 可以用来使用 GUI 为现有的解释器(或virtualenv)安装包。此外,您可以查看所有已安装包的列表,以及它们是否有可用的升级版本。

首先,您需要转到文件 | 设置 | 项目 | 项目解释器:

如前面的截图所示,PyCharm 提供了已安装包及其当前版本的列表。您可以点击+号将新包添加到项目解释器中,然后在搜索框中输入包的缩写:

您应该看到一个可用软件包的列表,其中包含每个软件包的名称和描述。此外,您可以指定要安装在您的解释器上的特定版本。一旦您点击安装软件包,PyCharm 将在您的系统上执行一个pip命令(可能会要求您权限);然后,它将下载软件包到安装目录并执行setup.py文件。

总结

在本章中,您学习了 Python 2 和 Python 3 之间的区别,以及如何根据您的需求决定使用哪种版本。此外,您还学习了如何安装 Python 解释器,以及如何使用 PyCharm 作为高级编辑器来编写和管理代码的生命周期。

在下一章中,我们将讨论 Python 软件包结构和自动化中常用的 Python 软件包。

第二章:自动化中常用的库

本章将带您了解 Python 包的结构以及今天用于自动化系统和网络基础设施的常见库。有一个不断增长的 Python 包列表,涵盖了网络自动化、系统管理以及管理公共和私有云的功能。

此外,重要的是要理解如何访问模块源代码,以及 Python 包中的小部分如何相互关联,这样我们就可以修改代码,添加或删除功能,并再次与社区分享代码。

本章将涵盖以下主题:

  • 理解 Python 包

  • 常见的 Python 库

  • 访问模块源代码

理解 Python 包

Python 核心代码实际上是小而简单的。大部分功能都是通过添加第三方包和模块来实现的。

模块是一个包含函数、语句和类的 Python 文件,将在您的代码中使用。首先要做的是import模块,然后开始使用它的函数。

另一方面,一个会收集相关的模块并将它们放在一个层次结构中。一些大型包,如matplotlibdjango,其中包含数百个模块,开发人员通常会将相关的模块分类到子目录中。例如,netmiko包包含多个子目录,每个子目录包含连接到不同供应商的网络设备的模块:

这样做可以让包的维护者灵活地向每个模块添加或删除功能,而不会破坏全局包的操作。

包搜索路径

通常情况下,Python 会在一些特定的系统路径中搜索模块。您可以通过导入sys模块并打印sys.path来打印这些路径。这实际上会返回PYTHONPATH环境变量和操作系统中的字符串;请注意结果只是一个普通的 Python 列表。您可以使用列表函数(如insert())添加更多路径到搜索范围。

然而,最好是将包安装在默认搜索路径中,这样当与其他开发人员共享代码时,代码不会出错:

一个简单的包结构,只有一个模块,会是这样的:

每个包中的__init__文件(在全局目录或子目录中)会告诉 Python 解释器这个目录是一个 Python 包,每个以.py结尾的文件都是一个模块文件,可以在你的代码中导入。init文件的第二个功能是一旦导入包,就执行其中的任何代码。然而,大多数开发人员会将其留空,只是用它来标记目录为 Python 包。

常见的 Python 库

在接下来的章节中,我们将探讨用于网络、系统和云自动化的常见 Python 库。

网络 Python 库

如今的网络环境中包含来自许多供应商的多个设备,每个设备扮演不同的角色。设计和自动化网络设备的框架对于网络工程师来说至关重要,可以自动执行重复的任务,提高他们通常完成工作的方式,同时减少人为错误。大型企业和服务提供商通常倾向于设计一个能够自动执行不同网络任务并提高网络弹性和灵活性的工作流程。这个工作流程包含一系列相关的任务,共同形成一个流程或工作流程,当网络需要变更时将被执行。

网络自动化框架可以在无需人工干预的情况下执行一些任务:

  • 问题的根本原因分析

  • 检查和更新设备操作系统

  • 发现节点之间的拓扑和关系

  • 安全审计和合规性报告

  • 根据应用程序需求从网络设备安装和撤销路由

  • 管理设备配置和回滚

以下是用于自动化网络设备的一些 Python 库:

网络库 描述 链接
Netmiko 一个支持 SSH 和 Telnet 的多供应商库,用于在网络设备上执行命令。支持的供应商包括 Cisco、Arista、Juniper、HP、Ciena 和许多其他供应商。 github.com/ktbyers/netmiko
NAPALM 一个 Python 库,作为官方供应商 API 的包装器工作。它提供了连接到多个供应商设备并从中提取信息的抽象方法,同时以结构化格式返回输出。这可以很容易地被软件处理。 github.com/napalm-automation/napalm
PyEZ 用于管理和自动化 Juniper 设备的 Python 库。它可以从 Python 客户端对设备执行 CRUD 操作。此外,它可以检索有关设备的信息,如管理 IP、序列号和版本。返回的输出将以 JSON 或 XML 格式呈现。 github.com/Juniper/py-junos-eznc
infoblox-client 用于基于 REST 称为 WAPI 与 infoblox NIOS 进行交互的 Python 客户端。 github.com/infobloxopen/infoblox-client
NX-API 一个 Cisco Nexus(仅限某些平台)系列 API,通过 HTTP 和 HTTPS 公开 CLI。您可以在提供的沙箱门户中输入 show 命令,它将被转换为对设备的 API 调用,并以 JSON 和 XML 格式返回输出。 developer.cisco.com/docs/nx-os/#!working-with-nx-api-cli
pyeapi 一个 Python 库,作为 Arista EOS eAPI 的包装器,用于配置 Arista EOS 设备。该库支持通过 HTTP 和 HTTPs 进行 eAPI 调用。 github.com/arista-eosplus/pyeapi
netaddr 用于处理 IPv4、IPv6 和第 2 层地址(MAC 地址)的 Python 库。它可以迭代、切片、排序和总结 IP 块。 github.com/drkjam/netaddr
ciscoconfparse 一个能够解析 Cisco IOS 风格配置并以结构化格式返回输出的 Python 库。该库还支持基于大括号分隔的配置的设备配置,如 Juniper 和 F5。 github.com/mpenning/ciscoconfparse
NSoT 用于跟踪网络设备库存和元数据的数据库。它提供了一个基于 Python Django 的前端 GUI。后端基于 SQLite 数据库存储数据。此外,它提供了使用 pynsot 绑定的库存的 API 接口。 github.com/dropbox/nsot
Nornir 一个基于 Python 的新的自动化框架,可以直接从 Python 代码中使用,无需自定义DSL领域特定语言)。Python 代码称为 runbook,包含一组可以针对存储在库存中的设备运行的任务(还支持 Ansible 库存格式)。任务可以利用其他库(如 NAPALM)来获取信息或配置设备。 github.com/nornir-automation/nornir

系统和云 Python 库

以下是一些可用于系统和云管理的 Python 软件包。像Amazon Web ServicesAWS)和 Google 这样的公共云提供商倾向于以开放和标准的方式访问其资源,以便与组织的 DevOps 模型轻松集成。像持续集成、测试和部署这样的阶段需要对基础设施(虚拟化或裸金属服务器)进行持续访问,以完成代码生命周期。这无法手动完成,需要自动化:

描述 链接
ConfigParser 用于解析和处理 INI 文件的 Python 标准库。 github.com/python/cpython/blob/master/Lib/configparser.py
Paramiko Paramiko 是 SSHv2 协议的 Python(2.7、3.4+)实现,提供客户端和服务器功能。 github.com/paramiko/paramiko
Pandas 提供高性能、易于使用的数据结构和数据分析工具的库。 github.com/pandas-dev/pandas
boto3 官方 Python 接口,用于管理不同的 AWS 操作,例如创建 EC2 实例和 S3 存储。 github.com/boto/boto3
google-api-python-client Google Cloud Platform 的官方 API 客户端库。 github.com/google/google-api-python-client
pyVmomi 来自 VMWare 的官方 Python SDK,用于管理 ESXi 和 vCenter。 github.com/vmware/pyvmomi
PyMYSQL 用于与 MySQL DBMS 一起工作的纯 Python MySQL 驱动程序。 github.com/PyMySQL/PyMySQL
Psycopg 适用于 Python 的 PostgresSQL 适配器,符合 DP-API 2.0 标准。 initd.org/psycopg/
Django 基于 Python 的高级开源 Web 框架。该框架遵循MVTModel, View, and Template)架构设计,用于构建 Web 应用程序,无需进行 Web 开发和常见安全错误。 www.djangoproject.com/
Fabric 用于在基于 SSH 的远程设备上执行命令和软件部署的简单 Python 工具。 github.com/fabric/fabric
SCAPY 一个出色的基于 Python 的数据包操作工具,能够处理各种协议,并可以使用任意组合的网络层构建数据包;它还可以将它们发送到网络上。 github.com/secdev/scapy
Selenium 用于自动化 Web 浏览器任务和 Web 验收测试的 Python 库。该库与 Firefox、Chrome 和 Internet Explorer 的 Selenium Webdriver 一起工作,以在 Web 浏览器上运行测试。 pypi.org/project/selenium/

您可以在以下链接找到更多按不同领域分类的 Python 软件包:github.com/vinta/awesome-python

访问模块源代码

您可以以两种方式访问您使用的任何模块的源代码。首先,转到github.com上的module页面,查看所有文件、发布、提交和问题,就像下面的截图一样。我通过netmiko模块的维护者具有对所有共享代码的读取权限,并可以查看完整的提交列表和文件内容:

第二种方法是使用pip或 PyCharm GUI 将包本身安装到 Python 站点包目录中。pip实际上的操作是到 GitHub 下载模块内容并运行setup.py来安装和注册模块。你可以看到模块文件,但这次你对所有文件都有完全的读写权限,可以更改原始代码。例如,以下代码利用netmiko库连接到思科设备并在其上执行show arp命令:

from netmiko import ConnectHandler

device = {"device_type": "cisco_ios",
  "ip": "10.10.88.110",
  "username": "admin",
  "password": "access123"}   net_connect = ConnectHandler(**device) output = net_connect.send_command("show arp")

如果我想看 netmiko 源代码,我可以去安装 netmiko 库的 site-packages 并列出所有文件我可以在 PyCharm 中使用Ctrl和左键单击模块名称。这将在新标签中打开源代码:

可视化 Python 代码

你是否想知道 Python 自定义模块或类是如何制造的?开发人员如何编写 Python 代码并将其粘合在一起以创建这个漂亮而惊人的x模块?底层发生了什么?

文档是一个很好的开始,当然,我们都知道它通常不会随着每一步或开发人员添加的每一个细节而更新。

例如,我们都知道由 Kirk Byers 创建和维护的强大的 netmiko 库(github.com/ktbyers/netmiko),它利用了另一个名为 Paramiko 的流行 SSH 库(www.paramiko.org/)。但我们不了解细节以及这些类之间的关系。如果你需要了解 netmiko(或任何其他库)背后的魔力,以便处理请求并返回结果,请按照以下步骤(需要 PyCharm 专业版)。

PyCharm 中的代码可视化和检查不受 PyCharm 社区版支持,只有专业版支持。

以下是你需要遵循的步骤:

  1. 转到 Python 库位置文件夹中的 netmiko 模块源代码(通常在 Windows 上为C:\Python27\Lib\site-packages或在 Linux 上为/usr/local/lib/pyhon2.7/dist-packages)并从 PyCharm 中打开文件。

  2. 右键单击地址栏中出现的模块名称,选择 Diagram | Show Diagram。从弹出窗口中选择 Python 类图:

  1. PyCharm 将开始在netmiko模块中的所有类和文件之间构建依赖树,然后在同一个窗口中显示它。请注意,这个过程可能需要一些时间,具体取决于你的计算机内存。此外,最好将图表保存为外部图像以查看:

根据生成的图表,你可以看到 Netmiko 支持许多供应商,如 HP Comware,entrasys,Cisco ASA,Force10,Arista,Avaya 等,所有这些类都继承自netmiko.cisco_base_connection.CicsoSSHConnection父类(我认为这是因为它们使用与思科相同的 SSH 风格)。这又继承自另一个名为netmiko.cisco_base_connection.BaseConnection的大父类。

此外,你可以看到 Juniper 有自己的类(netmiko.juniper.juniper_ssh.JuniperSSH),它直接连接到大父类。最后,我们连接到 Python 中所有父类的父类:Object类(记住最终在 Python 中的一切都是对象)。

你可以找到很多有趣的东西,比如SCP 传输类和SNMP类,每个类都会列出用于初始化该类的方法和参数。

因此,ConnectHandler方法主要用于检查供应商类中的device_type可用性,并根据返回的数据使用相应的 SSH 类:

可视化代码的另一种方法是查看代码执行期间实际命中的模块和函数。这称为分析,它允许你在运行时检查函数。

首先,您需要像往常一样编写您的代码,然后右键单击空白处,选择“profile”而不是像平常一样运行代码:

等待代码执行。这次,PyCharm 将检查从您的代码调用的每个文件,并为执行生成调用图,这样您就可以轻松地知道使用了哪些文件和函数,并计算每个文件的执行时间:

正如您在上一个图表中所看到的,我们在profile_code.py中的代码(图表底部)将调用ConnectHandler()函数,而后者将执行__init__.py,并且执行将继续。在图表的左侧,您可以看到在代码执行期间触及的所有文件。

摘要

在本章中,我们探讨了 Python 提供的一些最受欢迎的网络、系统和云包。此外,我们学习了如何访问模块源代码,并将其可视化,以更好地理解内部代码。我们查看了代码运行时的调用流程。在下一章中,我们将开始构建实验环境,并将我们的代码应用于其中。

第三章:设置网络实验室环境

我们现在已经有了如何编写和开发 Python 脚本的基本概念,这是创建程序的基本组成部分。我们现在将继续了解为什么自动化是当今网络中一个重要的话题,然后我们将使用一种流行的软件之一,名为 EVE-NG,来构建我们的网络自动化实验室,这有助于我们虚拟化网络设备。

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

  • 何时以及为什么自动化网络

  • 屏幕抓取与 API 自动化

  • 为什么要使用 Python 进行网络自动化

  • 网络自动化的未来

  • 实验室设置

  • 准备工作:安装 EVE-NG

  • 构建企业网络拓扑

技术要求

在本章中,我们将介绍 EVE-NG 的安装步骤以及如何创建我们的实验室环境。安装将在 VMware Workstation、VMware ESXi 和最后的 Red Hat KVM 上进行,因此您应该熟悉虚拟化概念,并在实验室设置之前已经运行其中一个 hypervisor。

何时以及为什么自动化网络

网络自动化正在全球范围内增加。然而,了解何时以及为什么自动化您的网络是非常重要的。例如,如果您是几个网络设备(三四个交换机)的管理员,并且不经常在它们上执行许多任务,那么您可能不需要对它们进行全面自动化。实际上,编写和开发脚本以及测试和故障排除所需的时间可能大于手动执行简单任务所需的时间。另一方面,如果您负责一个包含多供应商平台的大型企业网络,并且经常执行重复任务,那么强烈建议您编写脚本来自动化。

我们为什么需要自动化?

今天网络自动化的重要性有几个原因:

  • 降低成本:使用自动化解决方案(无论是内部开发还是从供应商购买)将减少网络操作复杂性以及配置、操作网络设备所需的时间

  • 业务连续性:自动化将减少当前基础设施上服务创建过程中的人为错误,从而使企业能够缩短服务的上市时间TTM

  • 业务敏捷性:大多数网络任务都是重复的,通过自动化,您将提高生产力并推动业务创新

  • 相关性:建立稳固的自动化工作流程使网络和系统管理员能够更快地进行根本原因分析,并增加通过将多个事件相关联来解决问题的可能性

屏幕抓取与 API 自动化

长期以来,CLI 是管理和操作网络设备的唯一访问方法。运营商和管理员过去通常使用 SSH 和 Telnet 来访问网络终端进行配置和故障排除。Python 或任何编程语言在与设备通信时有两种方法。第一种是像以前一样使用 SSH 或 Telnet 获取信息,然后处理它。这种方法称为屏幕抓取,需要能够与设备建立连接并直接在终端上执行命令的库,以及其他库来处理返回的信息,以提取有用的数据。这种方法通常需要了解其他解析语言,如正则表达式,以匹配输出的数据模式并从中提取有用的数据。

第二种方法称为应用程序可编程接口API),这种方法完全依赖于使用 REST 或 SOAP 协议向设备发送结构化请求,并返回输出,也以结构化格式编码为 JSON 或 XML。与第一种方法相比,这种方法中处理返回数据所需的时间相当短;但是,API 需要在网络设备上进行额外的配置以支持它。

为什么要使用 Python 进行网络自动化?

Python 是当今一个非常结构良好且易于使用的编程语言,面向技术、Web 和互联网开发、数据挖掘和可视化、桌面 GUI、分析、游戏开发和自动化测试的许多领域;这就是为什么它被称为通用目的语言

因此,选择 Python 有三个原因:

  • 易读性和易用性:当你使用 Python 进行开发时,实际上是在用英语写作。Python 内部的许多关键字和程序流都被设计成可读的语句。此外,Python 不需要;或花括号来开始和结束代码块,这使得 Python 具有较低的学习曲线。最后,Python 有一些可选的规则,称为 PEP 8,告诉 Python 开发人员如何格式化他们的程序以获得可读的代码。

你可以配置 PyCharm 来遵循这些规则,并通过转到设置|检查|PEP 8 编码风格违规来检查你的代码是否违反了这些规则:

  • :这是 Python 的真正力量:库和包。Python 在许多领域拥有广泛的库。任何 Python 开发人员都可以轻松开发一个 Python 库并将其上传到网上,以便其他开发人员使用。库被上传到一个名为 PyPI 的网站(pypi.Python.org/pypi),并链接到一个 GitHub 存储库。当你想要将库下载到你的电脑上时,你可以使用一个叫做pip的工具连接到 PyPI 并将其下载到本地。诸如思科、Juniper 和 Arista 等网络供应商开发了库来方便访问他们的平台。大多数供应商都在努力使他们的库易于使用,并且需要最少的安装和配置步骤来从设备中检索有用的信息。

  • 强大:Python 试图最小化达到最终结果所需的步骤数量。例如,要使用 Java 打印 hello world,你将需要这个代码块:

然而,在 Python 中,整个代码块都写在一行中以打印它,如下面的屏幕截图所示:

将所有这些原因结合在一起,使 Python 成为自动化的事实标准,也是供应商在自动化网络设备时的首选。

网络自动化的未来

在很长一段时间里,网络自动化只意味着使用编程语言(如 Perl、TcL 或 Python)开发脚本,以便在不同的网络平台上执行任务。这种方法被称为脚本驱动的网络自动化。但随着网络变得更加复杂和服务导向,需要并开始出现新类型的自动化,例如以下内容:

  • 软件定义的网络自动化:网络设备只有一个转发平面,而控制平面是使用外部软件(称为SDN 控制器)实现和创建的。这种方法的好处是任何网络变化都将有一个单一的联系点,并且 SDN 控制器可以接受来自其他软件(如外部门户)的变更请求,通过良好实现的北向接口。

  • 高级编排:这种方法需要一个称为编排器的软件,它与 SDN 控制器集成,并使用抽象服务的语言(如 YANG)创建网络服务模型,该模型将在其上运行的底层设备中运行。此外,编排器可以与虚拟基础设施管理器VIM)(如 OpenStack 和 vCenter)集成,以便管理虚拟机作为网络服务建模的一部分。

  • 基于策略的网络:在这种类型的自动化中,您描述了网络中所需的内容,系统具有所有详细信息,以便找出如何在底层设备中实现它。这使软件工程师和开发人员能够在网络中实施更改,并在声明性策略中描述其应用程序的需求。

网络实验室设置

现在,我们将开始在一个名为 EVE-NG 的流行平台上构建我们的网络实验室。当然,您可以使用物理节点来实现拓扑,但虚拟化环境为我们提供了一个隔离和沙箱环境,可以测试许多不同的配置,还可以灵活地添加/删除节点到/从拓扑中。此外,我们可以对我们的配置进行多个快照,因此可以随时恢复到任何场景。

EVE-NG(以前称为 UNetLab)是网络仿真中最受欢迎的选择之一。它支持来自不同供应商的各种虚拟化节点。还有另一个选择,即 GNS3,但正如我们将在本章和下一章中看到的那样,EVE-NG 提供了许多功能,使其成为网络建模的一个坚实选择。

EVE-NG 有三个版本:社区版、专业版和学习中心。我们将使用社区版,因为它包含了我们在本书中需要的所有功能。

准备就绪-安装 EVE-NG

EVE-NG 社区版有两个选项,OVA 和 ISO。第一个选项是使用 OVA,它为您提供了所需的最少安装步骤,前提是您已经拥有 VMware Player/Workstation/Fusion,或 VMware ESXi,或 Red Hat KVM。第二个选项是在没有虚拟化程序的裸机服务器上直接安装它,这次使用 Ubuntu 16.06 LTS 操作系统:

然而,ISO 选项需要一些 Linux 高级技能来准备机器本身,并将安装存储库导入操作系统。

Oracle VirtualBox 不支持 EVE-NG 所需的硬件加速,因此最好在 VMware 或 KVM 中安装它。

首先,前往www.eve-ng.net/index.php/downloads/eve-ng下载最新版本的 EVE-NG,然后将其导入到您的虚拟化程序中。我为创建的机器分配了 8 GB 内存和四个 vCPU,但您可以为其添加更多资源。在下一节中,我们将看到如何将下载的镜像导入到虚拟化程序并配置每个镜像。

在 VMware Workstation 上安装

在接下来的步骤中,我们将把下载的 EVE-NG OVA 镜像导入到 VMware Workstation 中。基于 OVA 的镜像包含描述虚拟机的文件,如硬盘、CPU 和 RAM 值。导入后,您可以修改这些数字:

  1. 打开 VMware Workstation,从“文件”中选择“打开”以导入 OVA。

  2. 完成导入过程后,右键单击新创建的机器,选择“编辑设置”。

  3. 将处理器数量增加到 4 个,内存分配为 8 GB(同样,如果您有资源,可以添加更多,但此设置对于我们的实验足够了)。

  4. 确保启用虚拟化 Intel VT-x/EPT 或 AMD-V/RVI 复选框。此选项指示 VMware Workstation 将虚拟化标志传递给客户操作系统(嵌套虚拟化):

此外,建议通过向现有硬盘添加额外空间来扩展硬盘,以便有足够的空间来托管来自供应商的多个镜像:

扩展磁盘后,将出现一条消息,指示操作已成功完成,并且您需要按照一些程序在客户操作系统中将新空间与旧空间合并。幸运的是,我们不需要这样做,因为 EVE-NG 将在系统启动期间将硬盘中发现的任何新空间与旧空间合并:

在 VMware ESXi 上安装

VMware ESXi 是直接在系统上运行的一种类型 1 虚拟化程序的良好示例。有时它们被称为裸机虚拟化程序,并且与类型 2 虚拟化程序(如 VMware Workstation/Fusion 或 VirtualBox)相比,它们提供了许多功能:

  1. 打开 vSphere 客户端并连接到您的 ESXi 服务器

  2. 从“文件”菜单中,选择“部署 OVF 模板”

  3. 输入下载的 OVA 镜像的路径并单击“下一步”:

  1. 接受虚拟化程序建议的所有默认设置,直到您到达最终页面“准备完成”,然后单击“完成”:

ESXi 将开始在虚拟化程序上部署镜像,稍后您可以更改其设置并为其添加更多资源,就像我们之前在 VMware Workstation 中所做的那样。

在 Red Hat KVM 上安装

您需要将下载的 OVA 镜像转换为 KVM 支持的 QCOW2 格式。按照以下步骤将一种格式转换为另一种格式。我们将需要一个名为qemu-img的特殊实用程序,该实用程序可在qemu-utils软件包内找到:

  1. 解压下载的 OVA 文件以提取 VMDK 文件(镜像的硬盘):
tar -xvf EVE\ Community\ Edition.ova
EVE Community Edition.ovf
EVE Community Edition.vmdk
  1. 安装qemu-utils工具:
sudo apt-get install qemu-utils
  1. 现在,将 VMDK 转换为 QCOW2。转换可能需要几分钟才能完成:
qemu-img convert -O qcow2 EVE\ Community\ Edition.vmdk eve-ng.qcow

最后,我们有自己的准备好在 Red Hat KVM 中托管的qcow2文件。打开 KVM 控制台,并从菜单中选择“导入现有磁盘映像”选项:

然后,选择转换图像的路径并单击“前进”:

访问 EVE-NG

在将镜像导入虚拟化程序并启动它后,您将被要求提供一些信息以完成安装。首先,您将看到 EVE 标志,表示机器已成功导入虚拟化程序,并准备启动引导阶段:

  1. 提供将用于 SSH 连接到 EVE 机器的 root 密码。默认情况下,它将是eve

  1. 提供将在 Linux 内用作名称的主机名:

  1. 为机器提供一个域名:

  1. 选择使用静态方法配置网络。这将确保即使在机器重启后,给定的 IP 地址也将是持久的:

  1. 最后,提供一个从您的网络可达的范围内的静态 IP 地址。此 IP 将用于 SSH 到 EVE 并将供应商镜像上传到存储库:

要访问 EVE-NG GUI,您需要打开浏览器并转到http://<server_ip>。请注意,server_IP是我们在安装步骤中使用的:

GUI 的默认用户名是admin,密码是eve,而 SSH 的默认用户名是root,密码是在安装步骤中提供的。

安装 EVE-NG 客户端包

EVE-NG 附带的客户端包允许我们选择在 telnet 或 SSH 到设备时使用哪个应用程序(PuTTY 或 SecureCRT),并设置 Wireshark 以在链接之间进行远程数据包捕获。此外,它还便于在基于 RDP 和 VNC 的镜像上工作。首先,您需要从eve-ng.net/index.php/downloads/windows-client-side-pack下载客户端包到您的 PC,然后将文件提取到C:\Program Files\EVE-NG

提取的文件包含许多用 Windows 批处理脚本(.bat)编写的脚本,用于配置将用于访问 EVE-NG 的机器。您将找到配置默认 Telnet/SSH 客户端的脚本和另一个用于 Wireshark 和 VNC 的脚本。软件源也可在文件夹内找到:

如果您使用 Ubuntu 或 Fedora 等 Linux 桌面,则可以使用 GitHub 上的这个优秀项目来获取客户端包:github.com/SmartFinn/eve-ng-integration

将网络图像加载到 EVE-NG 中

从供应商获得的所有网络图像都应上传到/opt/unetlab/addons/qemu。EVE-NG 支持基于 QEMU 的图像和动态图像,还支持 iOL(iOS On Linux)。

当您从供应商那里获得图像时,您应该在/opt/unetlab/addons/qemu内创建一个目录,并将图像上传到该目录;然后,您应该执行此脚本来修复上传图像的权限:

/opt/unetlab/wrappers/unl_wrapper -a fixpermission

构建企业网络拓扑

在我们的基本实验室设置中,我们将模拟一个具有四个交换机和一个充当外部网络网关的路由器的企业网络。以下是将用于每个节点的 IP 模式:

节点名称 IP
GW 10.10.88.110
Switch1 10.10.88.111
Switch2 10.10.88.112
Switch3 10.10.88.113
Switch4 10.10.88.114

我们的 Python 脚本(或 Ansible 剧本)将托管在连接到每个设备的管理的外部 Windows PC 上。

添加新节点

我们将首先选择已经上传到 EVE 的 IOSv 图像,并将四个交换机添加到拓扑。右键单击拓扑中的任何空白处,并从下拉菜单中选择添加新对象,选择添加节点:

您应该看到两个蓝色的 Cisco 图像,表示它们已成功添加到 EVE-NG 库中的可用图像,并映射到相应的模板。选择 Cisco vIOS L2 以添加 Cisco 交换机:

增加要添加的节点数到 4,然后点击确定:

现在,您将看到四个交换机添加到拓扑;再次重复此操作并添加路由器,但这次选择 Cisco vIOS:

连接节点

现在,开始连接节点,当节点离线时,重复每个节点,直到您完成连接拓扑内的所有节点;然后,开始实验:

在将 IP 地址和一些自定义形状添加到拓扑后的最终视图如下:

现在,我们的拓扑已经准备就绪,并且应该加载基本配置。我使用以下片段作为启用 SSH 和 telnet 的任何 Cisco-IOS 设备的配置基础,并配置了用于访问的用户名。请注意,有一些参数被{{ }}包围。我们将在下一章讨论它们,当我们使用 Jinja2 模板生成一个黄金配置时,但现在,分别用hostname和每个设备的管理 IP 地址替换它们:

hostname {{hostname}}
int gig0/0
  no shutdown
  ip address {{mgmt_ip}} 255.255.255.0

aaa new-model
aaa session-id unique
aaa authentication login default local
aaa authorization exec default local none 

enable password access123
username admin password access123
no ip domain-lookup

lldp run

ip domain-name EnterpriseAutomation.net
ip ssh version 2
ip scp server enable
crypto key generate rsa general-keys modulus 1024

总结

在本章中,我们了解了当今可用的不同类型的网络自动化,以及为什么我们选择 Python 作为网络自动化中的主要工具。此外,我们学习了如何在不同的 hypervisor 和平台上安装 EVE-NG,如何提供初始配置,以及如何将我们的网络图像添加到图像目录中。然后,我们添加了不同的节点并将它们连接在一起,以创建我们的网络企业实验室。

在下一章中,我们将开始构建我们的 Python 脚本,使用不同的 Python 库(如 telnetlib、Netmiko、Paramiko 和 Pexpect)自动化拓扑中的不同任务。

第四章:使用 Python 管理网络设备

现在我们对如何在不同操作系统中使用和安装 Python 以及如何使用 EVE-NG 构建网络拓扑有了相当的了解。在本章中,我们将发现如何利用今天用于自动化各种网络任务的许多网络自动化库。Python 可以在许多层面与网络设备进行交互。

首先,它可以通过套接字编程和 socket 模块处理低级层,这些模块作为运行 Python 的操作系统和网络设备之间的低级网络接口。此外,Python 模块通过 telnet、SSH 和 API 提供更高级的交互。在本章中,我们将深入探讨如何使用 Python 建立远程连接并使用 telnet 和 SSH 模块在远程设备上执行命令。

以下主题将被涵盖:

  • 使用 Python 进行 telnet 到设备

  • Python 和 SSH

  • 使用 netaddr 处理 IP 地址和网络

  • 网络自动化示例用例

技术要求

以下工具应安装并在您的环境中可用:

  • Python 2.7.1x

  • PyCharm 社区版或专业版

  • EVE-NG 拓扑;请参阅第三章,设置网络实验室环境,了解如何安装和配置模拟器

您可以在以下 GitHub URL 找到本章中开发的完整脚本:github.com/TheNetworker/EnterpriseAutomation.git

Python 和 SSH

与 telnet 不同,SSH 提供了一个安全的通道,用于在客户端和服务器之间交换数据。在客户端和设备之间创建的隧道使用不同的安全机制进行加密,使得任何人都很难解密通信。对于需要安全管理网络节点的网络工程师来说,SSH 协议是首选。

Python 可以使用 SSH 协议与网络设备通信,利用一个名为 Paramiko 的流行库,该库支持认证、密钥处理(DSA、RSA、ECDSA 和 ED25519)以及其他 SSH 功能,如 proxy 命令和 SFTP。

Paramiko 模块

Python 中最广泛使用的 SSH 模块称为 Paramiko,正如 GitHub 官方页面所说,Paramiko 的名称是 "paranoid" 和 "friend" 这两个世界的组合。该模块本身是使用 Python 编写和开发的,尽管一些核心功能如加密依赖于 C 语言。您可以在官方 GitHub 链接中了解更多有关贡献者和模块历史的信息:github.com/paramiko/paramiko

模块安装

打开 Windows cmd 或 Linux shell 并执行以下命令,从 PyPI 下载最新的 paramiko 模块。它将下载附加的依赖包,如 cyrptographyipaddresssix,并在您的计算机上安装它们:

pip install paramiko

您可以通过进入 Python shell 并导入 paramiko 模块来验证安装是否成功,如下面的屏幕截图所示。Python 应该成功导入它而不打印任何错误:

SSH 到网络设备

与每个 Python 模块一样,我们首先需要将其导入到我们的 Python 脚本中,然后我们将通过继承 SSHClient() 来创建一个 SSH 客户端。之后,我们将配置 Paramiko 自动添加任何未知的主机密钥并信任您和服务器之间的连接。然后,我们将使用 connect 函数并提供远程主机凭据:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import paramiko
import time
Channel = paramiko.SSHClient()  Channel.set_missing_host_key_policy(paramiko.AutoAddPolicy()) Channel.connect(hostname="10.10.88.112", username='admin', password='access123', look_for_keys=False,allow_agent=False)   shell = Channel.invoke_shell()  

AutoAddPolicy() 是可以在 set_missing_host_key_policy() 函数内使用的策略之一。在实验室环境中,它是首选且可接受的。然而,在生产环境中,我们应该使用更严格的策略,比如 WarningPolicy()RejectPolicy()

最后,invoke_shell()将启动与我们的 SSH 服务器的交互式 shell 会话。您可以向其提供附加参数,如终端类型、宽度和高度。

Paramiko 连接参数:

  • Look_For_Keys:默认为True,它将强制 Paramiko 使用密钥对身份验证,用户使用私钥和公钥对来对网络设备进行身份验证。在我们的情况下,我们将其设置为False,因为我们将使用密码身份验证。

  • allow_agent paramiko:它可以连接到本地 SSH 代理操作系统。这在使用密钥时是必要的;在这种情况下,由于使用登录/密码进行身份验证,我们将禁用它。

最后一步是向设备终端发送一系列命令,如show ip int bshow arp,并将输出返回到我们的 Python shell:

shell.send("enable\n") shell.send("access123\n") shell.send("terminal length 0\n") shell.send("show ip int b\n") shell.send("show arp \n") time.sleep(2) print shell.recv(5000)  Channel.close()

脚本输出为:

当您需要在远程设备上执行需要很长时间的命令时,最好使用time.sleep()来强制 Python 等待一段时间,直到设备生成输出并将其发送回 Python。否则,Python 可能会向用户返回空白输出。

Netmiko 模块

netmiko模块是 paramiko 的增强版本,专门针对网络设备。虽然 paramiko 旨在处理对设备的 SSH 连接,并检查设备是服务器、打印机还是网络设备,但 Netmiko 是专为网络设备设计的,并更有效地处理 SSH 连接。此外,Netmiko 支持广泛的供应商和平台。

Netmiko 被认为是 paramiko 的包装器,并通过许多附加增强功能扩展了其功能,例如直接访问供应商启用模式,读取配置文件并将其推送到设备,登录期间禁用分页,并在每个命令后默认发送回车符"\n"

供应商支持

Netmiko 支持许多供应商,并定期向受支持的列表中添加新供应商。以下是受支持的供应商列表,分为三组:定期测试,有限测试和实验性。您可以在模块 GitHub 页面上找到列表github.com/ktbyers/netmiko#supports

以下截图显示了定期测试类别下受支持供应商的数量:

以下截图显示了有限测试类别下受支持供应商的数量:

以下截图显示了实验类别下受支持供应商的数量:

安装和验证

要安装netmiko,打开 Windows cmd 或 Linux shell,并执行以下命令从 PyPI 获取最新包:

pip install netmiko

然后从 Python shell 导入 netmiko,以确保模块已正确安装到 Python site-packages 中:

$python
>>>import netmiko

使用 netmiko 进行 SSH

现在是时候利用 netmiko 并看到它在 SSH 到网络设备并执行命令时的强大功能。默认情况下,netmiko 在会话建立期间在后台处理许多操作,例如添加未知的 SSH 密钥主机,设置终端类型、宽度和高度,并在需要时访问启用模式,然后通过运行特定于供应商的命令来禁用分页。您需要首先以字典格式定义设备,并提供五个强制键:

R1 = {
  'device_type': 'cisco_ios',
  'ip': '10.10.88.110',
  'username': 'admin',
  'password': 'access123',
  'secret': 'access123',  }

第一个参数是device_type,用于定义平台供应商,以便执行正确的命令。然后,我们需要 SSH 的ip地址。如果已经通过 DNS 解析了设备主机名,这个参数可以是设备主机名,或者只是 IP 地址。然后我们提供usernamepassword,和secret中的启用模式密码。请注意,您可以使用getpass()模块隐藏密码,并且只在脚本执行期间提示密码。

虽然变量内的键的顺序并不重要,但键的名称应与前面的示例中提供的完全相同,以便 netmiko 正确解析字典并开始建立与设备的连接。

接下来,我们将从 netmiko 模块中导入ConnectHandler函数,并给它定义的字典以开始连接。由于我们所有的设备都配置了启用模式密码,我们需要通过提供.enable()来访问启用模式到创建的连接。我们将使用.send_command()在路由器终端上执行命令,该命令将执行命令并将设备输出返回到变量中:

from netmiko import ConnectHandler
connection = ConnectHandler(**R1)  connection.enable()  output = connection.send_command("show ip int b") print output

脚本输出为:

请注意输出已经从设备提示和我们在设备上执行的命令中清除。默认情况下,Netmiko 会替换它们并生成一个经过清理的输出,可以通过正则表达式进行处理,我们将在下一章中看到。

如果您需要禁用此行为,并希望在返回的输出中看到设备提示和执行的命令,则需要为.send_command()函数提供额外的标志:

output = connection.send_command("show ip int b",strip_command=False,strip_prompt=False) 

strip_command=Falsestrip_prompt=False标志告诉 netmiko 保留提示和命令,不要替换它们。它们默认为True,如果需要,可以切换它们:

使用 netmiko 配置设备

Netmiko 可以用于通过 SSH 配置远程设备。它通过使用.config方法访问配置模式,然后应用以list格式给出的配置来实现这一点。列表本身可以在 Python 脚本中提供,也可以从文件中读取,然后使用readlines()方法转换为列表:

from netmiko import ConnectHandler

SW2 = {
  'device_type': 'cisco_ios',
  'ip': '10.10.88.112',
  'username': 'admin',
  'password': 'access123',
  'secret': 'access123', }   core_sw_config = ["int range gig0/1 - 2","switchport trunk encapsulation dot1q",
  "switchport mode trunk","switchport trunk allowed vlan 1,2"]     print "########## Connecting to Device {0} ############".format(SW2['ip']) net_connect = ConnectHandler(**SW2) net_connect.enable()    print "***** Sending Configuration to Device *****" net_connect.send_config_set(core_sw_config) 

在上一个脚本中,我们做了与之前连接到 SW2 并进入启用模式相同的事情,但这次我们利用了另一个 netmiko 方法,称为send_config_set(),它以列表格式接收配置并访问设备配置模式并开始应用。我们有一个简单的配置,修改了gig0/1gig0/2,并对它们应用了干线配置。您可以通过在设备上运行show run命令来检查命令是否成功执行;您应该会得到类似以下的输出:

netmiko 中的异常处理

当设计 Python 脚本时,我们假设设备正在运行,并且用户已经提供了正确的凭据,这并不总是情况。有时 Python 和远程设备之间存在网络连接问题,或者用户输入了错误的凭据。通常,如果发生这种情况,Python 会抛出异常并退出,这并不是最佳解决方案。

netmiko 中的异常处理模块netmiko.ssh_exception提供了一些可以处理这种情况的异常类。第一个是AuthenticationException,将捕获远程设备中的身份验证错误。第二个类是NetMikoTimeoutException,将捕获 netmiko 和设备之间的超时或任何连接问题。我们需要做的是用 try-except 子句包装我们的 ConnectHandler()方法,并捕获超时和身份验证异常:


from netmiko import ConnectHandler
from netmiko.ssh_exception import AuthenticationException, NetMikoTimeoutException

device = {
  'device_type': 'cisco_ios',
  'ip': '10.10.88.112',
  'username': 'admin',
  'password': 'access123',
  'secret': 'access123', }     print "########## Connecting to Device {0} ############".format(device['ip']) try:
  net_connect = ConnectHandler(**device)
  net_connect.enable()    print "***** show ip configuration of Device *****"
  output = net_connect.send_command("show ip int b")
  print output

    net_connect.disconnect()   except NetMikoTimeoutException:
  print "=========== SOMETHING WRONG HAPPEN WITH {0} ============".format(device['ip'])   except AuthenticationException:
  print "========= Authentication Failed with {0} ============".format(device['ip'])   except Exception as unknown_error:
  print "============ SOMETHING UNKNOWN HAPPEN WITH {0} ============"

设备自动检测

Netmiko 提供了一种可以猜测设备类型并检测它的机制。它使用 SNMP 发现 OID 和在远程控制台上执行几个 show 命令的组合来检测路由器操作系统和类型,基于输出字符串。然后 netmiko 将加载适当的驱动程序到ConnectHandler()类中:

#!/usr/local/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"     from netmiko import SSHDetect, Netmiko

device = {
  'device_type': 'autodetect',
  'host': '10.10.88.110',
  'username': 'admin',
  'password': "access123", }   detect_device = SSHDetect(**device) device_type = detect_device.autodetect() print(device_type)  print(detect_device.potential_matches)    device['device_type'] = device_type
connection = Netmiko(**device)

在上一个脚本中:

  • 设备字典中的device_type将是autodetect,这将告诉netmiko等待并不加载驱动程序,直到 netmiko 猜测到它。

  • 然后我们指示 netmiko 使用SSHDetect()类执行设备检测。该类将使用 SSH 连接到设备,并执行一些发现命令来定义操作系统类型。返回的结果将是一个字典,并且最佳匹配将使用autodetect()函数分配给device_type变量。

  • 您可以通过打印potential_matches来查看所有匹配的结果。

  • 现在我们可以更新设备字典并为其分配新的device_type

使用 Python 中的 telnet 协议

Telnet 是 TCP/IP 协议栈中可用的最古老的协议之一。它主要用于在服务器和客户端之间建立的连接上交换数据。它在服务器上使用 TCP 端口23来监听来自客户端的传入连接。

在我们的情况下,我们将创建一个充当 telnet 客户端的 Python 脚本,拓扑中的其他路由器和交换机将充当 telnet 服务器。Python 自带了一个名为telnetlib的库,因此我们不需要安装它。

通过从telnetlib模块中可用的Telnet()类实例化客户端对象后,我们可以使用telnetlib中可用的两个重要函数,即read_until()(用于读取输出)和write()(用于在远程设备上写入)。这两个函数用于与创建的通道进行交互,无论是写入还是读取返回的输出。

另外,重要的是要注意,使用read_until()读取通道将清除缓冲区,并且数据将不可用于进一步的读取。因此,如果您读取重要数据并且稍后将对其进行处理和处理,那么您需要在继续脚本之前将其保存为变量。

Telnet 数据以明文格式发送,因此您的凭据和密码可能会被执行中间人攻击的任何人捕获和查看。一些服务提供商和企业仍然使用它,并将其与 VPN 和 radius/tacacs 协议集成,以提供轻量级和安全的访问。

按照以下步骤来理解整个脚本:

  1. 我们将在 Python 脚本中导入telnetlib模块,并将用户名和密码定义为变量,如下面的代码片段所示:
import telnetlib
username = "admin" password = "access123" enable_password = "access123"
  1. 我们将定义一个变量,用于与远程主机建立连接。请注意,在建立连接期间我们不会提供用户名或密码;我们只会提供远程主机的 IP 地址:
cnx = telnetlib.Telnet(host="10.10.88.110") #here we're telnet to Gateway 
  1. 现在我们将通过从通道返回的输出中读取Username:关键字并搜索来为 telnet 连接提供用户名。然后我们写入我们的管理员用户名。当我们需要输入 telnet 密码和启用密码时,也是使用相同的过程:
cnx.read_until("Username:") cnx.write(username + "\n") cnx.read_until("Password:") cnx.write(password + "\n") cnx.read_until(">") cnx.write("en" + "\n") cnx.read_until("Password:") cnx.write(enable_password + "\n")  

重要的是要提供在建立 telnet 连接或连接时出现在控制台中的确切关键字,否则连接将进入无限循环。然后 Python 脚本将因错误而超时。

  1. 最后,我们将在通道上写入show ip interface brief命令,并读取直到路由器提示#以获取输出。这应该可以让我们获取路由器中的接口配置:
cnx.read_until("#") cnx.write("show ip int b" + "\n") output = cnx.read_until("#") print output

完整的脚本如下:

脚本输出如下:

请注意,输出包含执行的命令show ip int b,并且路由器提示“R1#”被返回并打印在stdout中。我们可以使用内置的字符串函数如replace()来清除它们的输出:

cleaned_output = output.replace("show ip int b","").replace("R1#","")
print cleaned_output 

正如您注意到的,我们在脚本中以明文形式提供了密码和启用密码,这被认为是一个安全问题。在 Python 脚本中硬编码这些值也不是一个好的做法。稍后在下一节中,我们将隐藏密码并设计一个机制,仅在脚本运行时提供凭据。

此外,如果您想要执行跨多个页面的命令输出,比如show running config,那么您需要先发送terminal length 0来禁用分页,然后再连接到设备并发送命令。

使用 telnetlib 推送配置

在前一节中,我们通过执行show ip int brief来简化telnetlib的操作。现在我们需要利用它来将 VLAN 配置推送到拓扑中的四个交换机。我们可以使用 python 的range()函数创建一个 VLAN 列表,并迭代它以将 VLAN ID 推送到当前交换机。请注意,我们将交换机 IP 地址定义为列表中的一个项目,这个列表将是我们外部的for循环。此外,我将使用另一个内置模块称为getpass来隐藏控制台中的密码,并且只在脚本运行时提供它:

#!/usr/bin/python import telnetlib
import getpass
import time

switch_ips = ["10.10.88.111", "10.10.88.112", "10.10.88.113", "10.10.88.114"] username = raw_input("Please Enter your username:") password = getpass.getpass("Please Enter your Password:") enable_password = getpass.getpass("Please Enter your Enable Password:")   for sw_ip in switch_ips:
  print "\n#################### Working on Device " + sw_ip + " ####################"
  connection = telnetlib.Telnet(host=sw_ip.strip())
  connection.read_until("Username:")
  connection.write(username + "\n")
  connection.read_until("Password:")
  connection.write(password + "\n")
  connection.read_until(">")
  connection.write("enable" + "\n")
  connection.read_until("Password:")
  connection.write(enable_password + "\n")
  connection.read_until("#")
  connection.write("config terminal" + "\n") # now i'm in config mode
  vlans = range(300,400)
  for vlan_id in vlans:
  print "\n********* Adding VLAN " + str(vlan_id) + "**********"
  connection.read_until("#")
  connection.write("vlan " + str(vlan_id) + "\n")
  time.sleep(1)
  connection.write("exit" + "\n")
  connection.read_until("#")
  connection.close()

在我们最外层的for循环中,我们正在迭代设备,然后在每次迭代(每个设备)中,我们从 300 到 400 生成一个 vlan 范围,并将它们推送到当前设备。

脚本输出为:

此外,您还可以检查交换机控制台本身的输出(输出已省略):

使用 netaddr 处理 IP 地址和网络

处理和操作 IP 地址是网络工程师最重要的任务之一。Python 开发人员提供了一个了不起的库,可以理解 IP 地址并对其进行操作,称为netaddr。例如,假设您开发了一个应用程序,其中的一部分是获取129.183.1.55/21的网络和广播地址。您可以通过模块内部的两个内置方法networkbroadcast轻松实现:

net.network
129.183.0.
net.broadcast
129.183.0.0

总的来说,netaddr 提供以下功能的支持:

第 3 层地址:

  • IPv4 和 IPv6 地址、子网、掩码、前缀

  • 迭代、切片、排序、总结和分类 IP 网络

  • 处理各种范围格式(CIDR、任意范围和通配符、nmap)

  • 基于集合的操作(并集、交集等)在 IP 地址和子网上

  • 解析各种不同格式和符号

  • 查找 IANA IP 块信息

  • 生成 DNS 反向查找

  • 超网和子网

第 2 层地址:

  • 表示和操作 MAC 地址和 EUI-64 标识符

  • 查找 IEEE 组织信息(OUI、IAB)

  • 生成派生的 IPv6 地址

Netaddr 安装

netaddr 模块可以使用 pip 安装,如下所示:

pip install netaddr

作为成功安装模块的验证,您可以在安装后打开 PyCharm 或 Python 控制台,并导入模块。如果没有产生错误,则模块安装成功:

python
>>>import netaddr

探索 netaddr 方法

netaddr模块提供了两种重要的方法来定义 IP 地址并对其进行操作。第一个称为IPAddress(),用于定义具有默认子网掩码的单个类 IP 地址。第二种方法是IPNetwork(),用于定义具有 CIDR 的无类别 IP 地址。

这两种方法都将 IP 地址作为字符串,并返回该字符串的 IP 地址或 IP 网络对象。可以对返回的对象执行许多操作。例如,我们可以检查 IP 地址是单播、多播、环回、私有、公共,甚至是有效还是无效。上述操作的输出要么是True,要么是False,可以在 Python 的if条件中使用。

此外,该模块支持比较操作,如==<>来比较两个 IP 地址,生成子网,还可以检索给定 IP 地址或子网所属的超网列表。最后,netaddr模块可以生成一个完整的有效主机列表(不包括网络 IP 和网络广播):

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"  from netaddr import IPNetwork,IPAddress
def check_ip_address(ipaddr):
  ip_attributes = []
  ipaddress = IPAddress(ipaddr)    if ipaddress.is_private():
  ip_attributes.append("IP Address is Private")   else:
  ip_attributes.append("IP Address is public")   if ipaddress.is_unicast():
  ip_attributes.append("IP Address is unicast")
  elif ipaddress.is_multicast():
  ip_attributes.append("IP Address is multicast")   if ipaddress.is_loopback():
  ip_attributes.append("IP Address is loopback")    return "\n".join(ip_attributes)   def operate_on_ip_network(ipnet):    net_attributes = []
  net = IPNetwork(ipnet)   net_attributes.append("Network IP Address is " + str(net.network) + " and Netowrk Mask is " + str(net.netmask))    net_attributes.append("The Broadcast is " + str(net.broadcast) )   net_attributes.append("IP Version is " + str(net.version) )
  net_attributes.append("Information known about this network is " + str(net.info) )   net_attributes.append("The IPv6 representation is " + str(net.ipv6()))   net_attributes.append("The Network size is " + str(net.size))   net_attributes.append("Generating a list of ip addresses inside the subnet")   for ip in net:
  net_attributes.append("\t" + str(ip))   return "\n".join(net_attributes)  
ipaddr = raw_input("Please Enter the IP Address: ") print check_ip_address(ipaddr)    ipnet = raw_input("Please Enter the IP Network: ") print operate_on_ip_network(ipnet) 

前面的脚本首先使用raw_input()函数从用户那里请求 IP 地址和 IP 网络,然后将调用两个用户方法check_ip_address()operate_on_ip_network(),并将输入的值传递给它们。第一个函数check_ip_address()将检查输入的 IP 地址并尝试生成有关 IP 地址属性的报告,例如它是单播 IP、多播、私有还是环回,并将输出返回给用户。

第二个函数operate_on_ip_network()接受 IP 网络并生成网络 ID、子网掩码、广播、版本、已知有关此网络的信息、IPv6 表示,最后生成此子网中的所有 IP 地址。

重要的是要注意,net.info仅对公共 IP 地址有效,而不适用于私有 IP 地址。

请注意,在使用它们之前,我们需要从netaddr模块导入IP NetworkIP Address

脚本输出如下:

示例用例

随着我们的网络变得越来越大,并开始包含来自不同供应商的许多设备,我们需要创建模块化的 Python 脚本来自动化其中的各种任务。在接下来的几节中,我们将探讨三种用例,这些用例可以用来从我们的网络中收集不同的信息,并降低故障排除所需的时间,或者至少将网络配置恢复到其上次已知的良好状态。这将使网络工程师更专注于完成他们的工作,并为企业提供处理网络故障和恢复的自动化工作流程。

备份设备配置

备份设备配置是任何网络工程师的最重要任务之一。在这种用例中,我们将设计一个示例 Python 脚本,可用于不同的供应商和平台,以备份设备配置。我们将利用netmiko库来执行此任务。

结果文件应该以设备 IP 地址格式化,以便以后轻松访问或引用。例如,SW1 备份操作的结果文件应为dev_10.10.88.111_.cfg

构建 Python 脚本

我们将首先定义我们的交换机。我们希望将它们的配置备份为文本文件,并提供由逗号分隔的凭据和访问详细信息。这将使我们能够在 Python 脚本中使用split()函数获取数据,并在ConnectHandler函数中使用它。此外,该文件可以轻松地从 Microsoft Excel 表或任何数据库中导出和导入。

文件结构如下:

<device_ipaddress>,<username>,<password>,<enable_password>,<vendor>

现在我们将通过导入文件并使用with open子句来开始构建我们的 Python 脚本。我们使用文件上的readlines()将每一行作为列表中的一个项目。我们将创建一个for循环来遍历每一行,并使用split()函数来通过逗号分隔访问详细信息并将它们分配给变量:

from netmiko import ConnectHandler
from datetime import datetime

with open("/media/bassim/DATA/GoogleDrive/Packt/EnterpriseAutomationProject/Chapter5_Using_Python_to_manage_network_devices/UC1_devices.txt") as devices_file:    devices = devices_file.readlines()   for line in devices:
    line = line.strip("\n")
  ipaddr = line.split(",")[0]
  username = line.split(",")[1]
  password = line.split(",")[2]
  enable_password = line.split(",")[3]    vendor = line.split(",")[4]    if vendor.lower() == "cisco":
  device_type = "cisco_ios"
  backup_command = "show running-config"    elif vendor.lower() == "juniper":
  device_type = "juniper"
  backup_command = "show configuration | display set"   

由于我们需要创建一个模块化和多供应商的脚本,我们需要在每一行中使用if子句检查供应商,并为当前设备分配正确的device_typebackup_command

接下来,我们现在准备使用netmiko模块中可用的.send_command()方法建立与设备的 SSH 连接并在其上执行备份命令:


print str(datetime.now()) + " Connecting to device {}" .format(ipaddr)   net_connect = ConnectHandler(device_type=device_type,
  ip=ipaddr,
  username=username,
  password=password,
  secret=enable_password) net_connect.enable() running_config = net_connect.send_command(backup_command)   print str(datetime.now()) + " Saving config from device {}" .format(ipaddr)   f = open( "dev_" + ipaddr + "_.cfg", "w") f.write(running_config) f.close() print "=============================================="

在最后几个语句中,我们打开了一个文件进行写入,并使其名称包含从我们的文本文件中收集的ipaddr变量。

脚本输出如下:

另外,请注意备份配置文件是在项目主目录中创建的,其名称包含每个设备的 IP 地址:

您可以在 Linux 服务器上设计一个简单的 cron 作业,或在 Windows 服务器上安排一个作业,在特定时间运行先前的 Python 脚本。例如,脚本可以每天午夜运行一次,并将配置存储在latest目录中,以便团队以后可以参考。

创建您自己的访问终端

在 Python 中,以及一般的编程中,您就是供应商!您可以创建任何代码组合和程序,以满足您的需求。在第二个用例中,我们将创建我们自己的终端,通过telnetlib访问路由器。通过在终端中写入几个单词,它将被翻译成在网络设备中执行的多个命令并返回输出,这些输出可以只打印在标准输出中,也可以保存在文件中:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import telnetlib

connection = telnetlib.Telnet(host="10.10.88.110") connection.read_until("Username:") connection.write("admin" + "\n") connection.read_until("Password:") connection.write("access123" + "\n") connection.read_until(">") connection.write("en" + "\n") connection.read_until("Password:") connection.write("access123" + "\n") connection.read_until("#") connection.write("terminal length 0" + "\n") connection.read_until("#") while True:
  command = raw_input("#:")
  if "health" in command.lower():
  commands = ["show ip int b",
  "show ip route",
  "show clock",
  "show banner motd"
  ]    elif "discover" in command.lower():
  commands = ["show arp",
  "show version | i uptime",
  "show inventory",   ]
  else:
  commands = [command]
  for cmd in commands:
  connection.write(cmd + "\n")
  output = connection.read_until("#")
  print output
        print "==================="  

首先,我们建立一个 telnet 连接到路由器,并输入用户访问详细信息,直到达到启用模式。然后,我们创建一个始终为true的无限while循环,并使用内置函数raw_input()来期望用户输入命令。当用户输入任何命令时,脚本将捕获它并直接执行到网络设备。

然而,如果用户输入了healthdiscover关键字,那么我们的终端将足够智能,以执行一系列命令来反映所需的操作。这在网络故障排除的情况下应该非常有用,您可以扩展它以进行任何日常操作。想象一下,您需要在两台路由器之间排除 OSPF 邻居关系问题。您只需要打开您自己的终端 Python 脚本,您已经教给它了一些用于故障排除所需的命令,并写入类似于tshoot_ospf的内容。一旦您的脚本看到这个魔术关键字,它将启动一系列多个命令,打印 OSPF 邻居关系状态、MTU 接口、在 OSPF 下广告的网络等,直到找到问题为止。

脚本输出:

通过在提示符中写入health来尝试我们脚本中的第一个命令:

正如您所看到的,脚本返回了在设备中执行的多个命令的输出。

现在尝试第二个支持的命令,discover

这次脚本返回了发现命令的输出。在后面的章节中,我们可以解析返回的输出并从中提取有用的信息。

从 Excel 表中读取数据

网络和 IT 工程师总是使用 Excel 表来存储有关基础设施的信息,如 IP 地址、设备供应商和凭据。Python 支持从 Excel 表中读取信息并处理它,以便您以后在脚本中使用。

在这个用例中,我们将使用Excel Readxlrd)模块来读取包含我们基础设施的主机名、IP、用户名、密码、启用密码和供应商信息的UC3_devices.xlsx文件,并使用这些信息来提供netmiko模块。

Excel 表将如下截图所示:

首先,我们需要安装xlrd模块,使用pip,因为我们将使用它来读取 Microsoft Excel 表:

pip install xlrd

XLRD 模块读取 Excel 工作簿并将行和列转换为矩阵。例如,如果您需要获取左侧的第一项,那么您将需要访问 row[0][0]。右侧的下一个项将是 row[0][1],依此类推。

此外,当 xlrd 读取工作表时,它将通过每次读取一行来增加一个名为nrows(行数)的特殊计数器。类似地,它将通过每次读取列来增加ncols(列数),因此您可以通过这两个参数知道矩阵的大小:

您可以使用open_workbook()函数提供xlrd的文件路径。然后,您可以通过使用sheet_by_index()sheet_by_name()函数访问包含数据的工作表。对于我们的用例,我们的数据存储在第一个工作表(索引=0)中,并且文件路径存储在章节名称下。然后,我们将遍历工作表中的行,并使用row()函数访问特定行。返回的输出是一个列表,我们可以使用索引访问其中的任何项。

Python 脚本:

__author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   from netmiko import ConnectHandler
from netmiko.ssh_exception import AuthenticationException, NetMikoTimeoutException
import xlrd
from pprint import pprint

workbook = xlrd.open_workbook(r"/media/bassim/DATA/GoogleDrive/Packt/EnterpriseAutomationProject/Chapter4_Using_Python_to_manage_network_devices/UC3_devices.xlsx")   sheet = workbook.sheet_by_index(0)   for index in range(1, sheet.nrows):
  hostname = sheet.row(index)[0].value
    ipaddr = sheet.row(index)[1].value
    username = sheet.row(index)[2].value
    password = sheet.row(index)[3].value
    enable_password = sheet.row(index)[4].value
    vendor = sheet.row(index)[5].value

    device = {
  'device_type': vendor,
  'ip': ipaddr,
  'username': username,
  'password': password,
  'secret': enable_password,    }
  # pprint(device)    print "########## Connecting to Device {0} ############".format(device['ip'])
  try:
  net_connect = ConnectHandler(**device)
  net_connect.enable()    print "***** show ip configuration of Device *****"
  output = net_connect.send_command("show ip int b")
  print output

        net_connect.disconnect()    except NetMikoTimeoutException:
  print "=======SOMETHING WRONG HAPPEN WITH {0}=======".format(device['ip'])    except AuthenticationException:
  print "=======Authentication Failed with {0}=======".format(device['ip'])   
  except Exception as unknown_error:
  print "=======SOMETHING UNKNOWN HAPPEN WITH {0}======="   

更多用例

Netmiko 可以用于实现许多网络自动化用例。它可以用于在升级期间从远程设备上传、下载文件,从 Jinja2 模板加载配置,访问终端服务器,访问终端设备等等。您可以在github.com/ktbyers/pynet/tree/master/presentations/dfwcug/examples找到一些有用的用例列表:

摘要

在本章中,我们开始了我们的 Python 网络自动化实践之旅。我们探索了 Python 中可用的不同工具,以建立与 telnet 和 SSH 远程节点的连接,并在其上执行命令。此外,我们学习了如何使用netaddr模块处理 IP 地址和网络子网。最后,我们通过两个实际用例加强了我们的知识。

在下一章中,我们将处理返回的输出并开始从中提取有用的信息。

第五章:从网络设备中提取有用数据

在上一章中,我们已经看到了如何使用不同的方法和协议访问网络设备,然后在远程设备上执行命令,将输出返回到 Python。现在,是时候从这个输出中提取一些有用的数据了。

在本章中,您将学习如何使用 Python 中的不同工具和库从返回的输出中提取有用的数据,并使用正则表达式对其进行操作。此外,我们将使用一个名为CiscoConfParse的特殊库来审计配置,然后学习如何使用matplotlib库可视化数据,生成视觉上吸引人的图形和报告。

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

  • 理解解析器

  • 正则表达式简介

  • 使用Ciscoconfparse进行配置审计

  • 使用matplotlib可视化返回的数据

技术要求

您的环境中应安装并可用以下工具:

  • Python 2.7.1x

  • PyCharm 社区版或专业版

  • EVE-NG 实验室

您可以在以下 GitHub URL 找到本章开发的完整脚本:

github.com/TheNetworker/EnterpriseAutomation.git

理解解析器

在上一章中,我们探讨了访问网络设备、执行命令并将输出返回到终端的不同方式。现在我们需要处理返回的输出,并从中提取一些有用的信息。请注意,从 Python 的角度来看,输出只是一个多行字符串,Python 不区分 IP 地址、接口名称或节点主机名,因为它们都是字符串。因此,第一步是设计和开发我们自己的解析器,使用 Python 根据返回的输出中的重要信息对项目进行分类和区分。

之后,您可以处理解析后的数据,并生成有助于可视化的图形,甚至将它们存储到持久的外部存储或数据库中。

正则表达式简介

正则表达式是一种语言,用于通过跟随整个字符串的模式来匹配特定的字符串出现。当找到匹配时,将返回匹配的字符串,并将其保存在 Python 格式的结构中,如tuplelistdictionary。以下表总结了正则表达式中最常见的模式:

此外,正则表达式中的一个重要规则是您可以编写自己的正则表达式,并用括号()括起来,这称为捕获组,它可以帮助您保存重要数据,以便稍后使用捕获组编号引用它:

line = '30 acd3.b2c6.aac9 FastEthernet0/1' 
match = re.search('(\d+) +([0-9a-f.]+) +(\S+)', line)
print match.group(1)
print match.group(2)

PyCharm 将自动对写成正则表达式的字符串进行着色,并可以帮助您在将其应用于数据之前检查正则表达式的有效性。请确保在设置中启用了 Check RegExp 意图,如下所示:

在 Python 中创建正则表达式

您可以使用 Python 中的re模块构建正则表达式,该模块已经与 Python 安装一起原生地提供。该模块内部有几种方法,如search()sub()split()compile()findall(),它们将以正则表达式对象的形式返回结果。以下是每个函数的用法总结:

函数名称 用法
search() 搜索和匹配模式的第一个出现。
findall() 搜索和匹配模式的所有出现,并将结果作为列表返回。
Finditer() 搜索和匹配模式的所有出现,并将结果作为迭代器返回。
compile() 将正则表达式编译为具有各种操作方法的模式对象,例如搜索模式匹配或执行字符串替换。如果您在脚本中多次使用相同的正则表达式模式,这将非常有用。
sub() 用于用另一个字符串替换匹配的模式。
split() 用于在匹配模式上拆分并创建列表。

正则表达式很难阅读;因此,让我们从简单的开始,看一些最基本级别的简单正则表达式。

使用re模块的第一步是在 Python 代码中导入它

import re

我们将开始探索re模块中最常见的函数,即search(),然后我们将探索findall()。当您需要在字符串中找到一个匹配项,或者当您编写正则表达式模式来匹配整个输出并需要使用groups()方法来获取结果时,search()函数是合适的,正如我们将在接下来的例子中看到的。

re.search()函数的语法如下:

match = re.search('regex pattern', 'string')

第一个参数'regex pattern'是为了匹配'string'中的特定出现而开发的正则表达式。当找到匹配项时,search()函数将返回一个特殊的匹配对象,否则将返回None。请注意,search()将仅返回模式的第一个匹配项,并将忽略其余的匹配项。让我们看一些在 Python 中使用re模块的例子:

示例 1:搜索特定 IP 地址

import re
intf_ip = 'Gi0/0/0.911            10.200.101.242   YES NVRAM  up                    up' match = re.search('10.200.101.242', intf_ip)    if match:
  print match.group()

在这个例子中,我们可以看到以下内容:

  • re模块被导入到我们的 Python 脚本中。

  • 我们有一个字符串,对应于接口详细信息,并包含名称、IP 地址和状态。这个字符串可以在脚本中硬编码,也可以使用 Netmiko 库从网络设备中生成。

  • 我们将这个字符串传递给search()函数,以及我们的正则表达式,即 IP 地址。

  • 然后,脚本检查前一个操作是否返回了match对象;如果是,则会打印出来。

测试匹配的最基本方法是通过re.match函数,就像我们在前面的例子中所做的那样。match函数接受一个正则表达式模式和一个字符串值。

请注意,我们只在intf_ip参数内搜索特定的字符串,而不是每个 IP 地址模式。

示例 1 输出

示例 2:匹配 IP 地址模式

import re
intf_ip = '''Gi0/0/0.705            10.103.17.5      YES NVRAM  up                    up Gi0/0/0.900            86.121.75.31  YES NVRAM  up                    up Gi0/0/0.911            10.200.101.242   YES NVRAM  up                    up Gi0/0/0.7000           unassigned      YES unset  up                    up ''' match = re.search("\d+\.\d+\.\d+\.\d+", intf_ip)   if match:
  print match.group()

在这个例子中,我们可以看到以下内容:

  • re模块被导入到我们的 Python 脚本中。

  • 我们有一个多行字符串,对应于接口详细信息,并包含名称、IP 地址和状态。

  • 我们将这个字符串传递给search()函数,以及我们的正则表达式,即使用\d+匹配一个或多个数字,以及\.匹配点的出现。

  • 然后,脚本检查前一个操作是否返回了match对象;如果是,则会打印出来。否则,将返回None对象。

示例 2 输出

请注意,search()函数只返回模式的第一个匹配项,而不是所有匹配项。

示例 3:使用 groups()正则表达式

如果您有一个长输出,并且需要从中提取多个字符串,那么您可以用()括起提取的值,并在其中编写您的正则表达式。这称为捕获组,用于捕获长字符串中的特定模式,如下面的代码片段所示:

import re
log_msg = 'Dec 20 12:11:47.417: %LINK-3-UPDOWN: Interface GigabitEthernet0/0/4, changed state to down' match = re.search("(\w+\s\d+\s\S+):\s(\S+): Interface (\S+), changed state to (\S+)", log_msg) if match:
  print match.groups() 

在这个例子中,我们可以看到以下内容:

  • re模块被导入到我们的 Python 脚本中。

  • 我们有一个字符串,对应于路由器中发生的事件,并存储在日志中。

  • 我们将这个字符串传递给search()函数,以及我们的正则表达式。请注意,我们将时间戳、事件类型、接口名称和捕获组的新状态都括起来,并在其中编写我们的正则表达式。

  • 然后,脚本检查前一个操作是否返回了匹配对象;如果是,则会打印出来,但这次我们使用了groups()而不是group(),因为我们正在捕获多个字符串。

示例 3 输出

请注意,返回的数据是一个名为tuple的结构化格式。我们可以稍后使用此输出来触发事件,并且例如在冗余接口上启动恢复过程。

我们可以增强我们之前的代码,并使用Named组来为每个捕获组命名,以便稍后引用或用于创建字典。在这种情况下,我们在正则表达式前面加上了?P<"NAME">,就像下一个示例(GitHub 存储库中的示例 4)中一样:示例 4:命名组

示例 5-1:使用 re.search()搜索多行

假设我们的输出中有多行,并且我们需要针对正则表达式模式检查所有这些行。请记住,search()函数在找到第一个模式匹配时退出。在这种情况下,我们有两种解决方案。第一种是通过在"\n"上拆分整个字符串将每行输入到搜索函数中,第二种解决方案是使用findall()函数。让我们探讨这两种解决方案:


import re

show_ip_int_br_full = """ GigabitEthernet0/0/0        110.110.110.1   YES NVRAM  up                    up GigabitEthernet0/0/1        107.107.107.1   YES NVRAM  up                    up GigabitEthernet0/0/2        108.108.108.1   YES NVRAM  up                    up GigabitEthernet0/0/3        109.109.109.1   YES NVRAM  up                    up GigabitEthernet0/0/4   unassigned      YES NVRAM  up                    up GigabitEthernet0/0/5             10.131.71.1     YES NVRAM  up                    up GigabitEthernet0/0/6          10.37.102.225   YES NVRAM  up                    up GigabitEthernet0/1/0            unassigned      YES unset  up                    up GigabitEthernet0/1/1           57.234.66.28   YES manual up                    up GigabitEthernet0/1/2           10.10.99.70   YES manual up                    up GigabitEthernet0/1/3           unassigned      YES manual deleted               down GigabitEthernet0/1/4           192.168.200.1   YES manual up                    up GigabitEthernet0/1/5   unassigned      YES manual down                  down GigabitEthernet0/1/6         10.20.20.1      YES manual down                  down GigabitEthernet0/2/0         10.30.40.1      YES manual down                  down GigabitEthernet0/2/1         57.20.20.1      YES manual down                  down  """ for line in show_ip_int_br_full.split("\n"):
  match = re.search(r"(?P<interface>\w+\d\/\d\/\d)\s+(?P<ip>\d+.\d+.\d+.\d+)", line)
  if match:
  intf_ip = match.groupdict()
  if intf_ip["ip"].startswith("57"):
  print "Subnet is configured on " + intf_ip["interface"] + " and ip is " + intf_ip["ip"]

上面的脚本将拆分show ip interface brief输出并搜索特定模式,即接口名称和配置在其上的 IP 地址。根据匹配的数据,脚本将继续检查每个 IP 地址并使用start with 57进行验证,然后脚本将打印相应的接口和完整的 IP 地址。

示例 5-1 输出

如果您只搜索第一次出现,可以优化脚本,并且只需在找到第一个匹配项时中断外部for循环,但请注意,第二个匹配项将无法找到或打印。

示例 5-2:使用 re.findall()搜索多行

findall()函数在提供的字符串中搜索所有不重叠的匹配项,并返回与正则表达式模式匹配的字符串列表(与search函数不同,后者返回match对象),如果没有捕获组,则返回。如果您用捕获组括起您的正则表达式,那么findall()将返回一个元组列表。在下面的脚本中,我们有相同的多行输出,并且我们将使用findall()方法来获取所有配置了以 57 开头的 IP 地址的接口:

import re
from pprint import pprint
show_ip_int_br_full = """ GigabitEthernet0/0/0        110.110.110.1   YES NVRAM  up                    up GigabitEthernet0/0/1        107.107.107.1   YES NVRAM  up                    up GigabitEthernet0/0/2        108.108.108.1   YES NVRAM  up                    up GigabitEthernet0/0/3        109.109.109.1   YES NVRAM  up                    up GigabitEthernet0/0/4   unassigned      YES NVRAM  up                    up GigabitEthernet0/0/5             10.131.71.1     YES NVRAM  up                    up GigabitEthernet0/0/6          10.37.102.225   YES NVRAM  up                    up GigabitEthernet0/1/0            unassigned      YES unset  up                    up GigabitEthernet0/1/1           57.234.66.28   YES manual up                    up GigabitEthernet0/1/2           10.10.99.70   YES manual up                    up GigabitEthernet0/1/3           unassigned      YES manual deleted               down GigabitEthernet0/1/4           192.168.200.1   YES manual up                    up GigabitEthernet0/1/5   unassigned      YES manual down                  down GigabitEthernet0/1/6         10.20.20.1      YES manual down                  down GigabitEthernet0/2/0         10.30.40.1      YES manual down                  down GigabitEthernet0/2/1         57.20.20.1      YES manual down                  down """    intf_ip = re.findall(r"(?P<interface>\w+\d\/\d\/\d)\s+(?P<ip>57.\d+.\d+.\d+)", show_ip_int_br_full) pprint(intf_ip) 

示例 5-2 输出

请注意,这一次我们不必编写for循环来检查每行是否符合正则表达式模式。这将在findall()方法中自动完成。

使用 CiscoConfParse 进行配置审计

在网络配置上应用正则表达式以从输出中获取特定信息需要我们编写一些复杂的表达式来解决一些复杂的用例。在某些情况下,您只需要检索一些配置或修改现有配置而不深入编写正则表达式,这就是CiscoConfParse库诞生的原因(github.com/mpenning/ciscoconfparse)。

CiscoConfParse 库

正如官方 GitHub 页面所说,该库检查了一个类似 iOS 风格的配置,并将其分解成一组链接的父/子关系。您可以对这些关系执行复杂的查询:

来源:github.com/mpenning/ciscoconfparse

因此,配置的第一行被视为父级,而后续行被视为父级的子级。CiscoConfparse库将父级和子级之间的关系构建成一个对象,因此最终用户可以轻松地检索特定父级的配置,而无需编写复杂的表达式。

非常重要的是,您的配置文件格式良好,以便在父级和子级之间建立正确的关系。

如果需要向文件中注入配置,也适用相同的概念。该库将搜索给定的父级,并将配置插入其下方,并保存到新文件中。这在您需要对多个文件运行配置审计作业并确保它们都具有一致的配置时非常有用。

支持的供应商

作为一个经验法则,任何具有制表符分隔配置的文件都可以被CiscoConfParse解析,并且它将构建父子关系。

以下是支持的供应商列表:

  • Cisco IOS,Cisco Nexus,Cisco IOS-XR,Cisco IOS-XE,Aironet OS,Cisco ASA,Cisco CatOS

  • Arista EOS

  • Brocade

  • HP 交换机

  • Force10 交换机

  • Dell PowerConnect 交换机

  • Extreme Networks

  • Enterasys

  • ScreenOS

另外,从 1.2.4 版本开始,CiscoConfParse可以处理花括号分隔的配置,这意味着它可以处理以下供应商:

  • Juniper Network 的 Junos OS

  • Palo Alto Networks 防火墙配置

  • F5 Networks 配置

CiscoConfParse 安装

CiscoConfParse可以通过在 Windows 命令行或 Linux shell 上使用pip来安装:

pip install ciscoconfparse

请注意,还安装了一些其他依赖项,例如ipaddrdnsPythoncolorama,这些依赖项被CiscoConfParse使用。

使用 CiscoConfParse

我们将要处理的第一个示例是从名为Cisco_Config.txt的文件中提取关闭接口的示例 Cisco 配置。

在这个例子中,我们可以看到以下内容:

  • CiscoConfParse模块中,我们导入了CiscoConfParse类。同时,我们导入了pprint模块,以便以可读格式打印输出以适应 Python 控制台输出。

  • 然后,我们将config文件的完整路径提供给CiscoConfParse类。

  • 最后一步是使用内置函数之一,例如find_parents_w_child(),并提供两个参数。第一个是父级规范,它搜索以interface关键字开头的任何内容,而子规范具有shutdown关键字。

正如您所看到的,在三个简单的步骤中,我们能够获取所有具有关闭关键字的接口,并以结构化列表输出。

示例 1 输出

示例 2:检查特定功能的存在

第二个示例将检查配置文件中是否存在路由器关键字,以指示路由协议(例如ospfbgp)是否已启用。如果模块找到它,则结果将为True。否则,将为False。这可以通过模块内的内置函数has_line_with()来实现:

这种方法可以用于设计if语句内的条件,我们将在下一个和最后一个示例中看到。

示例 2 输出

示例 3:从父级打印特定子项

在这个例子中,我们可以看到以下内容:

  • CiscoConfParse模块中,我们导入了CiscoConfParse类。同时,我们导入了pprint模块,以便以可读格式打印输出以适应 Python 控制台输出。

  • 然后,我们将config文件的完整路径提供给CiscoConfParse类。

  • 我们使用了一个内置函数,例如find_all_children(),并且只提供了父级。这将指示CiscoConfParse类列出此父级下的所有配置行。

  • 最后,我们遍历返回的输出(记住,它是一个列表),并检查字符串中是否存在网络关键字。如果是,则将其附加到网络列表中,并在最后打印出来。

示例 3 输出:

CiscoConfParse模块中还有许多其他可用的函数,可用于轻松从配置文件中提取数据并以结构化格式返回输出。以下是其他函数的列表:

  • find_lineage

  • 查找行()

  • 查找所有子级()

  • 查找块()

  • 查找有子级的父级()

  • 查找有父级的子级()

  • 查找没有子级的父级()

  • 查找没有父级的子级()

使用 matplotLib 可视化返回的数据

俗话说,“一图胜千言”。可以从网络中提取大量信息,如接口状态、接口计数器、路由器更新、丢包、流量量等。将这些数据可视化并放入图表中将帮助您看到网络的整体情况。Python 有一个名为matplotlib的优秀库(matplotlib.org/),用于生成图表并对其进行自定义。

Matplotlib 能够创建大多数类型的图表,如折线图、散点图、条形图、饼图、堆叠图、3D 图和地理地图图表。

Matplotlib 安装

我们将首先使用pip从 PYpI 安装库。请注意,除了 matplotlib 之外,还将安装一些其他包,如numpysix

pip install matplotlib

现在,尝试导入matplotlib,如果没有打印错误,则成功导入模块:

Matplotlib 实践

我们将从简单的示例开始,以探索 matplotlib 的功能。我们通常做的第一件事是将matplotlib导入到我们的 Python 脚本中:

import matplotlib.pyplot as plt

请注意,我们将pyplot导入为一个简短的名称plt,以便在我们的脚本中使用。现在,我们将在其中使用plot()方法来绘制我们的数据,其中包括两个列表。第一个列表表示x轴的值,而第二个列表表示y轴的值:

plt.plot([0, 1, 2, 3, 4], [0, 10, 20, 30, 40])

现在,这些值被放入了图表中。

最后一步是使用show()方法将该图表显示为窗口:

plt.show()

在 Ubuntu 中,您可能需要安装Python-tk才能查看图表。使用apt install Python-tk

生成的图表将显示代表 x 轴和 y 轴输入值的线。在窗口中,您可以执行以下操作:

  • 使用十字图标移动图表

  • 调整图表大小

  • 使用缩放图标放大特定区域

  • 使用主页图标重置到原始视图

  • 使用保存图标保存图表

您可以通过为图表添加标题和两个轴的标签来自定义生成的图表。此外,如果图表上有多条线,还可以添加解释每条线含义的图例:

import matplotlib.pyplot as plt
plt.plot([0, 1, 2, 3, 4], [0, 10, 20, 30, 40]) plt.xlabel("numbers") plt.ylabel("numbers multiplied by ten") plt.title("Generated Graph\nCheck it out") plt.show()

请注意,我们通常不会在 Python 脚本中硬编码绘制的值,而是会从网络外部获取这些值,这将在下一个示例中看到。

此外,您可以在同一图表上绘制多个数据集。您可以添加另一个代表先前图表数据的列表,matplotlib将绘制它。此外,您可以添加标签以区分图表上的数据集。这些标签的图例将使用legend()函数打印在图表上:

import matplotlib.pyplot as plt
plt.plot([0, 1, 2, 3, 4], [0, 10, 20, 30, 40], label="First Line")
plt.plot([5, 6, 7, 8, 9], [50, 60, 70, 80, 90], label="Second Line") plt.xlabel("numbers") plt.ylabel("numbers multiplied by ten") plt.title("Generated Graph\nCheck it out") plt.legend() plt.show()

使用 matplotlib 可视化 SNMP

在这个用例中,我们将利用pysnmp模块向路由器发送 SNMP GET请求,检索特定接口的输入和输出流量速率,并使用matplotlib库对输出进行可视化。使用的 OID 是.1.3.6.1.4.1.9.2.2.1.1.6.1.3.6.1.4.1.9.2.2.1.1.8,分别表示输入和输出速率:

from pysnmp.entity.rfc3413.oneliner import cmdgen
import time
import matplotlib.pyplot as plt    cmdGen = cmdgen.CommandGenerator()   snmp_community = cmdgen.CommunityData('public') snmp_ip = cmdgen.UdpTransportTarget(('10.10.88.110', 161)) snmp_oids = [".1.3.6.1.4.1.9.2.2.1.1.6.3",".1.3.6.1.4.1.9.2.2.1.1.8.3"]   slots = 0 input_rates = [] output_rates = [] while slots <= 50:
  errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(snmp_community, snmp_ip, *snmp_oids)    input_rate = str(varBinds[0]).split("=")[1].strip()
  output_rate = str(varBinds[1]).split("=")[1].strip()    input_rates.append(input_rate)
  output_rates.append(output_rate)    time.sleep(6)
  slots = slots + 1
  print slots

time_range = range(0, slots)   print input_rates
print output_rates
# plt.figure() plt.plot(time_range, input_rates, label="input rate") plt.plot(time_range, output_rates, label="output rate") plt.xlabel("time slot") plt.ylabel("Traffic Measured in bps") plt.title("Interface gig0/0/2 Traffic") 
plt.legend() plt.show()

在这个例子中,我们可以看到以下内容:

  • 我们从pysnmp模块导入了cmdgen,用于为路由器创建 SNMP GET命令。我们还导入了matplotlib模块。

  • 然后,我们使用cmdgen来定义 Python 和路由器之间的传输通道属性,并提供 SNMP 社区。

  • pysnmp将开始使用提供的 OID 发送 SNMP GET 请求,并将输出和错误(如果有)返回到errorIndicationerrorStatuserrorIndexvarBinds。我们对varBinds感兴趣,因为它包含输入和输出流量速率的实际值。

  • 注意,varBinds 的形式将是 <oid> = <value>,因此我们只提取了值,并将其添加到之前创建的相应列表中。

  • 这个操作将在 6 秒的间隔内重复 100 次,以收集有用的数据。

  • 最后,我们将收集到的数据提供给从 matplotlib 导入的 plt,并通过提供 xlabelylabel、标题和 legends 来自定义图表:

脚本输出

总结

在本章中,我们学习了如何在 Python 中使用不同的工具和技术从返回的输出中提取有用的数据并对其进行操作。此外,我们使用了一个名为 CiscoConfParse 的特殊库来审计配置,并学习了如何可视化数据以生成吸引人的图表和报告。

在下一章中,我们将学习如何编写模板并使用它来使用 Jinja2 模板语言生成配置。

第六章:使用 Python 和 Jinja2 生成配置

本章介绍了 YAML 格式,用于表示数据并从 Jinja2 语言创建的黄金模板生成配置。我们将在 Ansible 和 Python 中使用这两个概念来创建我们配置的数据模型存储。

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

  • 什么是 YAML?

  • 使用 Jinja2 构建黄金配置模板

什么是 YAML?

YAML Ain’t Markup LanguageYAML)通常被称为数据序列化语言。它旨在是人类可读的,并将数据组织成结构化格式。编程语言可以理解 YAML 文件的内容(通常具有.yml.yaml扩展名),并将其映射到内置数据类型。例如,当您在 Python 脚本中使用.yaml文件时,它将自动将内容转换为字典{}或列表[],因此您可以对其进行处理和迭代。

YAML 规则有助于构建可读文件,因此了解它们以编写有效和格式良好的 YAML 文件非常重要。

YAML 文件格式

在开发 YAML 文件时需要遵循一些规则。YAML 使用缩进(类似于 Python),它建立了项目之间的关系:

  1. 因此,编写 YAML 文件的第一个规则是使缩进保持一致,使用空格或制表符,并且不要混合使用它们。

  2. 第二条规则是在创建具有键和值的字典时使用冒号:(有时称为yaml中的关联数组)。冒号左侧的项目是键,而冒号右侧的项目是值。

  3. 第三条规则是在列表中使用破折号"-"来分组项目。您可以在 YAML 文件中混合使用字典和列表,以有效地描述您的数据。左侧作为字典键,右侧作为字典值。您可以创建任意数量的级别以获得结构化数据:

让我们举个例子并应用这些规则:

有很多事情要看。首先,文件有一个顶级,my_datacenter,它作为顶级键,其值由它之后的所有缩进行组成,即GWswitch1switch2。这些项目也作为键,并在其中有值,即eve_portdevice_templatehostnamemgmt_intmgmt_ipmgmt_subnet,它们同时作为第 3 级键和第 2 级值。

另一件事要注意的是enabled_ports,它是一个键,但具有作为列表的值。我们知道这一点,因为下一级缩进是一个破折号。

请注意,所有接口都是同级元素,因为它们具有相同级别的缩进。

最后,不需要在字符串周围使用单引号或双引号。当我们将文件加载到 Python 中时,Python 会自动执行这些操作,并且还将根据缩进确定每个项目的数据类型和位置。

现在,让我们开发一个 Python 脚本,读取这个 YAML 文件,并使用yaml模块将其转换为字典和列表:

在这个例子中,我们可以看到以下内容:

  • 我们在 Python 脚本中导入了yaml模块,以处理 YAML 文件。此外,我们导入了pprint函数,以显示嵌套字典和列表的层次结构。

  • 然后,我们使用with子句和open()函数打开了yaml_example.yml文件作为yaml_file

  • 最后,我们使用load()函数将文件加载到yaml_data变量中。在这个阶段,Python 解释器将分析yaml文件的内容并建立项目之间的关系,然后将它们转换为标准数据类型。输出可以使用pprint()函数在控制台上显示。

脚本输出

现在,使用标准 Python 方法访问任何信息都相当容易。例如,您可以通过使用my_datacenter后跟switch1键来访问switch1配置,如以下代码片段所示:

pprint(yaml_data['my_datacenter']['switch1'])

{'device_template': 'vIOSL2_Template',
 'eve_port': 32769,
 'hostname': 'SW1',
 'mgmt_intf': 'gig0/0',
 'mgmt_ip': '10.10.88.111',
 'mgmt_subnet': '255.255.255.0'}    

此外,您可以使用简单的for循环迭代键,并打印任何级别的值:

for device in yaml_data['my_datacenter']:
    print device

GW
switch2
switch1

作为最佳实践,建议您保持键名一致,仅在描述数据时更改值。例如,hostnamemgmt_intfmgmt_ip项目在所有具有相同名称的设备上都存在,而它们在.yaml文件中的值不同。

文本编辑器提示

正确的缩进对于 YAML 数据非常重要。建议使用高级文本编辑器,如 Sublime Text 或 Notepad++,因为它们具有将制表符转换为特定数量的空格的选项。同时,您可以选择特定的制表符缩进大小为 2 或 4。因此,每当您点击Tab按钮时,您的编辑器将将制表符转换为静态数量的空格。最后,您可以选择在每个缩进处显示垂直线,以确保行缩进相同。

请注意,Microsoft Windows Notepad 没有此选项,这可能会导致 YAML 文件的格式错误。

以下是一个名为 Sublime Text 的高级编辑器的示例,可以配置为使用上述选项:

屏幕截图显示了垂直线指南,确保当您点击 Tab 时,兄弟项目处于相同的缩进级别和空格数。

使用 Jinja2 构建黄金配置

大多数网络工程师都有一个文本文件,用作特定设备配置的模板。该文件包含许多值的网络配置部分。当网络工程师想要配置新设备或更改其配置时,他们基本上会用另一个文件中的特定值替换此文件中的特定值,以生成新的配置。

在本书的后面,我们将使用 Python 和 Ansible,使用 Jinja2 模板语言(jinja.pocoo.org)高效地自动化此过程。 Jinja2 开发的核心概念和驱动程序是在特定网络/系统配置的所有模板文件中具有统一的语法,并将数据与实际配置分离。这使我们能够多次使用相同的模板,但使用不同的数据集。此外,正如 Jinja2 网页所示,它具有一些独特的功能,使其脱颖而出,与其他模板语言不同。

以下是官方网站上提到的一些功能:

  • 强大的自动 HTML 转义系统,用于跨站点脚本预防。

  • 高性能,使用即时编译到 Python 字节码。Jinja2 将在首次加载时将您的模板源代码转换为 Python 字节码,以获得最佳的运行时性能。

  • 可选的提前编译。

  • 易于调试,具有将模板编译和运行时错误集成到标准 Python 回溯系统的调试系统。

  • 可配置的语法:例如,您可以重新配置 Jinja2 以更好地适应输出格式,例如 LaTeX 或 JavaScript。

  • 模板设计帮助程序:Jinja2 附带了一系列有用的小助手,可帮助解决模板中的常见任务,例如将项目序列分成多列。

另一个重要的 Jinja 功能是模板继承,我们可以创建一个基础/父模板,为我们的系统或所有设备的 Day 0 初始配置定义基本结构。此初始配置将是基本配置,并包含通用部分,例如用户名、管理子网、默认路由和 SNMP 社区。其他子模板扩展基础模板并继承它。

在本章中,术语 Jinja 和 Jinja2 可以互换使用。

在我们深入研究 Jinja2 语言提供的更多功能之前,让我们先来看几个构建模板的例子:

  1. 首先,我们需要确保 Jinja2 已经安装在您的系统中,使用以下命令:
pip install jinja2 

该软件包将从 PyPi 下载,然后将安装在站点软件包中。

  1. 现在,打开你喜欢的文本编辑器,并编写以下模板,它代表了一个简单的 Day 0(初始)配置,用于配置设备主机名、一些aaa参数、每个交换机上应存在的默认 VLAN 以及 IP 地址的管理:
hostname {{ hostname }}

aaa new-model aaa session-id unique aaa authentication login default local aaa authorization exec default local none vtp mode transparent vlan 10,20,30,40,50,60,70,80,90,100,200   int {{ mgmt_intf }}
no switchport no shut ip address {{ mgmt_ip }} {{ mgmt_subnet }}

一些文本编辑器(如 Sublime Text 和 Notepad++)支持 Jinja2,并可以为您提供语法高亮和自动补全,无论是通过本地支持还是通过扩展。

请注意,在上一个模板中,变量是用双大括号{{  }}写的。因此,当 Python 脚本加载模板时,它将用所需的值替换这些变量:

#!/usr/bin/python   from jinja2 import Template
template = Template(''' hostname {{hostname}}   aaa new-model aaa session-id unique aaa authentication login default local aaa authorization exec default local none vtp mode transparent vlan 10,20,30,40,50,60,70,80,90,100,200   int {{mgmt_intf}}
 no switchport no shut ip address {{mgmt_ip}} {{mgmt_subnet}} ''')   sw1 = {'hostname': 'switch1', 'mgmt_intf': 'gig0/0', 'mgmt_ip': '10.10.88.111', 'mgmt_subnet': '255.255.255.0'} print(template.render(sw1))

在这个例子中,我们可以看到以下内容:

  • 首先,我们导入了jinja2模块中的Template类。这个类将验证和解析 Jinja2 文件。

  • 然后,我们定义了一个变量sw1,它是一个带有与模板内变量名称相等的键的字典。字典值将是渲染模板的数据。

  • 最后,我们在模板中使用了render()方法,该方法以sw1作为输入,将 Jinja2 模板与渲染值连接起来,并打印配置。

脚本输出

现在,让我们改进我们的脚本,使用 YAML 来渲染模板,而不是在字典中硬编码值。这个概念很简单:我们将在 YAML 文件中建模我们实验室的day0配置,然后使用yaml.load()将该文件加载到我们的 Python 脚本中,并使用输出来填充 Jinja2 模板,从而生成每个设备的day0配置文件:

首先,我们将扩展上次开发的 YAML 文件,并在保持每个节点层次结构不变的情况下,向其中添加其他设备:

--- dc1:
 GW: eve_port: 32773
  device_template: vIOSL3_Template
  hostname: R1
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.110
  mgmt_subnet: 255.255.255.0      switch1:
 eve_port: 32769
  device_template: vIOSL2_Template
  hostname: SW1
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.111
  mgmt_subnet: 255.255.255.0    switch2:
 eve_port: 32770
  device_template: vIOSL2_Template
  hostname: SW2
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.112
  mgmt_subnet: 255.255.255.0    switch3:
 eve_port: 32769
  device_template: vIOSL2_Template
  hostname: SW3
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.113
  mgmt_subnet: 255.255.255.0    switch4:
 eve_port: 32770
  device_template: vIOSL2_Template
  hostname: SW4
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.114
  mgmt_subnet: 255.255.255.0 

以下是 Python 脚本:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import yaml
from jinja2 import Template

with open('/media/bassim/DATA/GoogleDrive/Packt/EnterpriseAutomationProject/Chapter6_Configuration_generator_with_python_and_jinja2/network_dc.yml', 'r') as yaml_file:
  yaml_data = yaml.load(yaml_file)   router_day0_template = Template(""" hostname {{hostname}} int {{mgmt_intf}}
 no shutdown ip add {{mgmt_ip}} {{mgmt_subnet}}   lldp run   ip domain-name EnterpriseAutomation.net ip ssh version 2 ip scp server enable crypto key generate rsa general-keys modulus 1024   snmp-server community public RW snmp-server trap link ietf snmp-server enable traps snmp linkdown linkup snmp-server enable traps syslog snmp-server manager   logging history debugging logging snmp-trap emergencies logging snmp-trap alerts logging snmp-trap critical logging snmp-trap errors logging snmp-trap warnings logging snmp-trap notifications logging snmp-trap informational logging snmp-trap debugging   """)     switch_day0_template = Template(""" hostname {{hostname}}   aaa new-model aaa session-id unique aaa authentication login default local aaa authorization exec default local none vtp mode transparent vlan 10,20,30,40,50,60,70,80,90,100,200   int {{mgmt_intf}}
 no switchport no shut ip address {{mgmt_ip}} {{mgmt_subnet}}   snmp-server community public RW snmp-server trap link ietf snmp-server enable traps snmp linkdown linkup snmp-server enable traps syslog snmp-server manager   logging history debugging logging snmp-trap emergencies logging snmp-trap alerts logging snmp-trap critical logging snmp-trap errors logging snmp-trap warnings logging snmp-trap notifications logging snmp-trap informational logging snmp-trap debugging   """)   for device,config in yaml_data['dc1'].iteritems():
  if config['device_template'] == "vIOSL2_Template":
  device_template = switch_day0_template
    elif config['device_template'] == "vIOSL3_Template":
  device_template = router_day0_template

    print("rendering now device {0}" .format(device))
  Day0_device_config = device_template.render(config)    print Day0_device_config
    print "=" * 30 

在这个例子中,我们可以看到以下内容:

  • 我们像往常一样导入了yamlJinja2模块

  • 然后,我们指示脚本将yaml文件加载到yaml_data变量中,这将把它转换为一系列字典和列表

  • 分别定义了路由器和交换机配置的两个模板,分别为router_day0_templateswitch_day0_template

  • for循环将遍历dc1的设备,并检查device_template,然后为每个设备渲染配置

脚本输出

以下是路由器配置(输出已省略):

以下是交换机 1 的配置(输出已省略):

从文件系统中读取模板

Python 开发人员的一种常见方法是将静态的、硬编码的值和模板移出 Python 脚本,只保留脚本内的逻辑。这种方法可以使您的程序更加清晰和可扩展,同时允许其他团队成员通过更改输入来获得期望的输出,而对 Python 了解不多的人也可以使用这种方法。Jinja2 也不例外。您可以使用 Jinja2 模块中的FileSystemLoader()类从操作系统目录中加载模板。我们将修改我们的代码,将router_day0_templateswitch_day0_template的内容从脚本中移到文本文件中,然后将它们加载到我们的脚本中。

Python 代码

import yaml
from jinja2 import FileSystemLoader, Environment

with open('/media/bassim/DATA/GoogleDrive/Packt/EnterpriseAutomationProject/Chapter6_Configuration_generator_with_python_and_jinja2/network_dc.yml', 'r') as yaml_file:
  yaml_data = yaml.load(yaml_file)     template_dir = "/media/bassim/DATA/GoogleDrive/Packt/EnterpriseAutomationProject/Chapter6_Configuration_generator_with_python_and_jinja2"   template_env = Environment(loader=FileSystemLoader(template_dir),
  trim_blocks=True,
  lstrip_blocks= True
  )     for device,config in yaml_data['dc1'].iteritems():
  if config['device_template'] == "vIOSL2_Template":
  device_template = template_env.get_template("switch_day1_template.j2")
  elif config['device_template'] == "vIOSL3_Template":
  device_template = template_env.get_template("router_day1_template.j2")    print("rendering now device {0}" .format(device))
  Day0_device_config = device_template.render(config)    print Day0_device_config
    print "=" * 30 

在这个例子中,我们不再像之前那样从 Jinja2 模块中加载Template()类,而是导入Environment()FileSystemLoader(),它们用于通过提供template_dir从特定操作系统目录中读取 Jinja2 文件,其中存储了我们的模板。然后,我们将使用创建的template_env对象,以及get_template()方法,获取模板名称并使用配置渲染它。

确保您的模板文件以.j2扩展名结尾。这将使 PyCharm 将文件中的文本识别为 Jinja2 模板,从而提供语法高亮和更好的代码完成。

使用 Jinja2 循环和条件

Jinja2 中的循环和条件用于增强我们的模板并为其添加更多功能。我们将首先了解如何在模板中添加for循环,以便迭代从 YAML 传递的值。例如,我们可能需要在每个接口下添加交换机配置,比如使用交换机端口模式并配置 VLAN ID,这将在访问端口下配置,或者在干线端口的情况下配置允许的 VLAN 范围。

另一方面,我们可能需要在路由器上启用一些接口并为其添加自定义配置,比如 MTU、速度和双工。因此,我们将使用for循环。

请注意,我们的脚本逻辑的一部分现在将从 Python 移动到 Jinja2 模板中。Python 脚本将只是从操作系统外部或通过脚本内部的Template()类读取模板,然后使用来自 YAML 文件的解析值渲染模板。

Jinja2 中for循环的基本结构如下:

{% for key, value in var1.iteritems() %}
configuration snippets
{% endfor %}

请注意使用{% %}来定义 Jinja2 文件中的逻辑。

此外,iteritems()具有与迭代 Python 字典相同的功能,即迭代键和值对。循环将为var1字典中的每个元素返回键和值。

此外,我们可以有一个if条件来验证特定条件,如果条件为真,则配置片段将被添加到渲染文件中。基本的if结构如下所示:

{% if enabled_ports %}
configuration snippet goes here and added to template if the condition is true
{% endif %}

现在,我们将修改描述数据中心设备的.yaml文件,并为每个设备添加接口配置和已启用的端口:

--- dc1:
 GW: eve_port: 32773
  device_template: vIOSL3_Template
  hostname: R1
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.110
  mgmt_subnet: 255.255.255.0
  enabled_ports:
  - gig0/0
  - gig0/1
  - gig0/2    switch1:
 eve_port: 32769
  device_template: vIOSL2_Template
  hostname: SW1
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.111
  mgmt_subnet: 255.255.255.0
  interfaces:
 gig0/1: vlan: [1,10,20,200]
  description: TO_DSW2_1
  mode: trunk   gig0/2:
 vlan: [1,10,20,200]
  description: TO_DSW2_2
  mode: trunk   gig0/3:
 vlan: [1,10,20,200]
  description: TO_ASW3
  mode: trunk   gig1/0:
 vlan: [1,10,20,200]
  description: TO_ASW4
  mode: trunk
  enabled_ports:
  - gig0/0
  - gig1/1    switch2:
 eve_port: 32770
  device_template: vIOSL2_Template
  hostname: SW2
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.112
  mgmt_subnet: 255.255.255.0
  interfaces:
 gig0/1: vlan: [1,10,20,200]
  description: TO_DSW1_1
  mode: trunk   gig0/2:
 vlan: [1,10,20,200]
  description: TO_DSW1_2
  mode: trunk
  gig0/3:
 vlan: [1,10,20,200]
  description: TO_ASW3
  mode: trunk   gig1/0:
 vlan: [1,10,20,200]
  description: TO_ASW4
  mode: trunk
  enabled_ports:
  - gig0/0
  - gig1/1    switch3:
 eve_port: 32769
  device_template: vIOSL2_Template
  hostname: SW3
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.113
  mgmt_subnet: 255.255.255.0
  interfaces:
 gig0/1: vlan: [1,10,20,200]
  description: TO_DSW1
  mode: trunk   gig0/2:
 vlan: [1,10,20,200]
  description: TO_DSW2
  mode: trunk   gig1/0:
 vlan: 10
  description: TO_Client1
  mode: access   gig1/1:
 vlan: 20
  description: TO_Client2
  mode: access
  enabled_ports:
  - gig0/0    switch4:
 eve_port: 32770
  device_template: vIOSL2_Template
  hostname: SW4
  mgmt_intf: gig0/0
  mgmt_ip: 10.10.88.114
  mgmt_subnet: 255.255.255.0
  interfaces:
 gig0/1: vlan: [1,10,20,200]
  description: TO_DSW2
  mode: trunk   gig0/2:
 vlan: [1,10,20,200]
  description: TO_DSW1
  mode: trunk   gig1/0:
 vlan: 10
  description: TO_Client1
  mode: access   gig1/1:
 vlan: 20
  description: TO_Client2
  mode: access
  enabled_ports:
  - gig0/0

请注意,我们将交换机端口分类为干线端口或访问端口,并为每个端口添加 VLAN。

根据yaml文件,以交换机端口访问模式进入的数据包将被标记为 VLAN。在干线端口模式下,只有数据包的 VLAN ID 属于配置列表,才允许数据包进入。

现在,我们将为设备 Day 1(运行)配置创建两个额外的模板。第一个模板将是router_day1_template,第二个将是switch_day1_template,它们都将继承之前开发的相应 day0 模板:

router_day1_template:

{% include 'router_day0_template.j2' %}   {% if enabled_ports %}
 {% for port in enabled_ports %} interface {{ port }}
    no switchport
 no shutdown mtu 1520 duplex auto speed auto  {% endfor %}   {% endif %}

switch_day1_template:


{% include 'switch_day0_template.j2' %}   {% if enabled_ports %}
 {% for port in enabled_ports %} interface {{ port }}
    no switchport
 no shutdown mtu 1520 duplex auto speed auto    {% endfor %} {% endif %}   {% if interfaces %}
 {% for intf,intf_config in interfaces.items() %} interface {{ intf }}
 description "{{intf_config['description']}}"
 no shutdown duplex full  {% if intf_config['mode'] %}   {% if intf_config['mode'] == "access" %}
  switchport mode {{intf_config['mode']}}
 switchport access vlan {{intf_config['vlan']}}
   {% elif intf_config['mode'] == "trunk" %}
  switchport {{intf_config['mode']}} encapsulation dot1q
 switchport mode trunk switchport trunk allowed vlan {{intf_config['vlan']|join(',')}}
   {% endif %}
 {% endif %}
 {% endfor %} {% endif %} 

请注意使用{% include <template_name.j2> %}标签,它指的是设备的 day0 模板。

此模板将首先被渲染并填充来自 YAML 的传递值,然后填充下一个部分。

Jinja2 语言继承了许多写作风格和特性,来自 Python 语言。虽然在开发模板和插入标签时不是强制遵循缩进规则,但作者更喜欢在可读的 Jinja2 模板中使用缩进。

脚本输出:

rendering now device GW
hostname R1
int gig0/0
  no shutdown
  ip add 10.10.88.110 255.255.255.0
lldp run
ip domain-name EnterpriseAutomation.net
ip ssh version 2
ip scp server enable
crypto key generate rsa general-keys modulus 1024
snmp-server community public RW
snmp-server trap link ietf
snmp-server enable traps snmp linkdown linkup
snmp-server enable traps syslog
snmp-server manager
logging history debugging
logging snmp-trap emergencies
logging snmp-trap alerts
logging snmp-trap critical
logging snmp-trap errors
logging snmp-trap warnings
logging snmp-trap notifications
logging snmp-trap informational
logging snmp-trap debugging
interface gig0/0
    no switchport
    no shutdown
    mtu 1520
    duplex auto
    speed auto
interface gig0/1
    no switchport
    no shutdown
    mtu 1520
    duplex auto
    speed auto
interface gig0/2
    no switchport
    no shutdown
    mtu 1520
    duplex auto
    speed auto
==============================
rendering now device switch1
hostname SW1
aaa new-model
aaa session-id unique
aaa authentication login default local
aaa authorization exec default local none
vtp mode transparent
vlan 10,20,30,40,50,60,70,80,90,100,200
int gig0/0
 no switchport
 no shut
 ip address 10.10.88.111 255.255.255.0
snmp-server community public RW
snmp-server trap link ietf
snmp-server enable traps snmp linkdown linkup
snmp-server enable traps syslog
snmp-server manager
logging history debugging
logging snmp-trap emergencies
logging snmp-trap alerts
logging snmp-trap critical
logging snmp-trap errors
logging snmp-trap warnings
logging snmp-trap notifications
logging snmp-trap informational
logging snmp-trap debugging
interface gig0/0
    no switchport
    no shutdown
    mtu 1520
    duplex auto
    speed auto
interface gig1/1
    no switchport
    no shutdown
    mtu 1520
    duplex auto
    speed auto
interface gig0/2
 description "TO_DSW2_2"
 no shutdown
 duplex full
 switchport trunk encapsulation dot1q
 switchport mode trunk
 switchport trunk allowed vlan 1,10,20,200
interface gig0/3
 description "TO_ASW3"
 no shutdown
 duplex full
 switchport trunk encapsulation dot1q
 switchport mode trunk
 switchport trunk allowed vlan 1,10,20,200
interface gig0/1
 description "TO_DSW2_1"
 no shutdown
 duplex full
 switchport trunk encapsulation dot1q
 switchport mode trunk
 switchport trunk allowed vlan 1,10,20,200
interface gig1/0
 description "TO_ASW4"
 no shutdown
 duplex full
 switchport trunk encapsulation dot1q
 switchport mode trunk
 switchport trunk allowed vlan 1,10,20,200
==============================

<switch2 output omitted>

==============================
rendering now device switch3
hostname SW3
aaa new-model
aaa session-id unique
aaa authentication login default local
aaa authorization exec default local none
vtp mode transparent
vlan 10,20,30,40,50,60,70,80,90,100,200
int gig0/0
 no switchport
 no shut
 ip address 10.10.88.113 255.255.255.0
snmp-server community public RW
snmp-server trap link ietf
snmp-server enable traps snmp linkdown linkup
snmp-server enable traps syslog
snmp-server manager
logging history debugging
logging snmp-trap emergencies
logging snmp-trap alerts
logging snmp-trap critical
logging snmp-trap errors
logging snmp-trap warnings
logging snmp-trap notifications
logging snmp-trap informational
logging snmp-trap debugging
interface gig0/0
    no switchport
    no shutdown
    mtu 1520
    duplex auto
    speed auto
interface gig0/2
 description "TO_DSW2"
 no shutdown
 duplex full
 switchport trunk encapsulation dot1q
 switchport mode trunk
 switchport trunk allowed vlan 1,10,20,200
interface gig1/1
 description "TO_Client2"
 no shutdown
 duplex full
 switchport mode access
 switchport access vlan 20
interface gig1/0
 description "TO_Client1"
 no shutdown
 duplex full
 switchport mode access
 switchport access vlan 10
interface gig0/1
 description "TO_DSW1"
 no shutdown
 duplex full
 switchport trunk encapsulation dot1q
 switchport mode trunk
 switchport trunk allowed vlan 1,10,20,200
==============================
<switch4 output omitted>

总结

在本章中,我们学习了 YAML 及其格式以及如何使用文本编辑器。我们还了解了 Jinja2 及其配置。然后,我们探讨了在 Jinja2 中使用循环和条件的方法。

在下一章中,我们将学习如何使用多进程同时实例化和执行 Python 代码。

第七章:Python 脚本的并行执行

Python 已成为网络自动化的事实标准。许多网络工程师已经每天使用它来自动化网络任务,从配置到操作,再到解决网络问题。在本章中,我们将讨论 Python 中的一个高级主题:挖掘 Python 的多进程特性,并学习如何使用它来加速脚本执行时间。

本章将涵盖以下主题:

  • Python 代码在操作系统中的执行方式

  • Python 多进程库

计算机如何执行您的 Python 脚本

这是您计算机的操作系统执行 Python 脚本的方式:

  1. 当您在 shell 中键入python <your_awesome_automation_script>.py时,Python(作为一个进程运行)指示您的计算机处理器安排一个线程(这是处理的最小单位):

  1. 分配的线程将开始逐行执行您的脚本。线程可以做任何事情,包括与 I/O 设备交互,连接到路由器,打印输出,执行数学方程等等。

  2. 一旦脚本达到文件结束EOF),线程将被终止并返回到空闲池中,供其他进程使用。然后,脚本被终止。

在 Linux 中,您可以使用#strace –p <pid>来跟踪特定线程的执行。

您为脚本分配的线程越多(并且得到处理器或操作系统允许的线程越多),脚本运行得越快。实际上,有时线程被称为工作者从属

我有一种感觉,你脑海中有这样一个小想法:为什么我们不从所有核心中为 Python 脚本分配大量线程,以便快速完成工作呢?

如果没有特殊处理,将大量线程分配给一个进程的问题是竞争条件。操作系统将为您的进程(在本例中是 Python 进程)分配内存,以供运行时所有线程访问 - 同时。现在,想象一下其中一个线程在另一个线程实际写入数据之前读取了一些数据!您不知道线程尝试访问共享数据的顺序;这就是竞争条件:

一种可用的解决方案是使线程获取锁。事实上,默认情况下,Python 被优化为作为单线程进程运行,并且有一个叫做全局解释器锁GIL)的东西。为了防止线程之间的冲突,GIL 不允许多个线程同时执行 Python 代码。

但是,为什么不使用多个进程,而不是多个线程呢?

多进程的美妙之处,与多线程相比,就在于你不必担心由于共享数据而导致数据损坏。每个生成的进程都将拥有自己分配的内存,其他 Python 进程无法访问。这使我们能够同时执行并行任务:

此外,从 Python 的角度来看,每个进程都有自己的 GIL。因此,在这里没有资源冲突或竞争条件。

Python 多进程库

multiprocessing模块是 Python 的标准库,随 Python 二进制文件一起提供,并且从 Python 2.6 版本开始可用。还有threading模块,它允许您生成多个线程,但它们都共享相同的内存空间。多进程比线程具有更多的优势。其中之一是每个进程都有独立的内存空间,并且可以利用多个 CPU 和核心。

开始使用多进程

首先,您需要为 Python 脚本导入模块:

import multiprocessing as mp

然后,用 Python 函数包装您的代码;这将允许进程针对此函数并将其标记为并行执行。

假设我们有连接到路由器并使用netmiko库在其上执行命令的代码,并且我们希望并行连接到所有设备。这是一个样本串行代码,将连接到每个设备并执行传递的命令,然后继续第二个设备,依此类推:

from netmiko import ConnectHandler
from devices import R1, SW1, SW2, SW3, SW4

nodes = [R1, SW1, SW2, SW3, SW4]   for device in nodes:
  net_connect = ConnectHandler(**device)
  output = net_connect.send_command("show run")
  print output

Python 文件devices.py创建在与我们的脚本相同的目录中,并以dictionary格式包含每个设备的登录详细信息和凭据:

  R1 = {"device_type": "cisco_ios_ssh",
      "ip": "10.10.88.110",
      "port": 22,
      "username": "admin",
      "password": "access123",
      }

SW1 = {"device_type": "cisco_ios_ssh",
       "ip": "10.10.88.111",
       "port": 22,
       "username": "admin",
       "password": "access123",
       }

SW2 = {"device_type": "cisco_ios_ssh",
       "ip": "10.10.88.112",
       "port": 22,
       "username": "admin",
       "password": "access123",
       }

SW3 = {"device_type": "cisco_ios_ssh",
       "ip": "10.10.88.113",
       "port": 22,
       "username": "admin",
       "password": "access123",
       }

SW4 = {"device_type": "cisco_ios_ssh",
       "ip": "10.10.88.114",
       "port": 22,
       "username": "admin",
       "password": "access123",
       } 

现在,如果我们想要改用多进程模块,我们需要重新设计脚本并将代码移动到一个函数下;然后,我们将分配与设备数量相等的进程数(一个进程将连接到一个设备并执行命令),并将进程的目标设置为执行此函数:

  from netmiko import ConnectHandler
from devices import R1, SW1, SW2, SW3, SW4
import multiprocessing as mp
from datetime import datetime

nodes = [R1, SW1, SW2, SW3, SW4]    def connect_to_dev(device):    net_connect = ConnectHandler(**device)
  output = net_connect.send_command("show run")
  print output

processes = []   start_time = datetime.now() for device in nodes:
  print("Adding Process to the list")
  processes.append(mp.Process(target=connect_to_dev, args=[device]))   print("Spawning the Process") for p in processes:
  p.start()   print("Joining the finished process to the main truck") for p in processes:
  p.join()   end_time = datetime.now() print("Script Execution tooks {}".format(end_time - start_time))   

在前面的例子中,适用以下内容:

  • 我们将multiprocess模块导入为mp。模块中最重要的类之一是Process,它将我们的netmiko connect函数作为目标参数。此外,它接受将参数传递给目标函数。

  • 然后,我们遍历我们的节点,并为每个设备创建一个进程,并将该进程附加到进程列表中。

  • 模块中可用的start()方法用于生成并启动进程执行。

  • 最后,脚本执行时间通过从脚本结束时间减去脚本开始时间来计算。

在幕后,执行主脚本的主线程将开始分叉与设备数量相等的进程。每个进程都针对一个函数,同时在所有设备上执行show run,并将输出存储在一个变量中,互不影响。

这是 Python 中进程的一个示例视图:

现在,当您执行完整的代码时,还需要做一件事。您需要将分叉的进程连接到主线程/主线程,以便顺利完成程序的执行:

for p in processes:
  p.join()

在前面的例子中使用的join()方法与原始的字符串方法join()无关;它只是用来将进程连接到主线程。

进程之间的通信

有时,您将有一个需要在运行时与其他进程传递或交换信息的进程。多进程模块有一个Queue类,它实现了一个特殊的列表,其中一个进程可以插入和消耗数据。在这个类中有两个可用的方法:get()put()put()方法用于向Queue添加数据,而从队列获取数据则通过get()方法完成。在下一个示例中,我们将使用Queue来将数据从子进程传递到父进程:

import multiprocessing
from netmiko import ConnectHandler
from devices import R1, SW1, SW2, SW3, SW4
from pprint import pprint

nodes = [R1, SW1, SW2, SW3, SW4]   def connect_to_dev(device, mp_queue):
  dev_id = device['ip']
  return_data = {}   net_connect = ConnectHandler(**device)   output = net_connect.send_command("show run")   return_data[dev_id] = output
    print("Adding the result to the multiprocess queue")
  mp_queue.put(return_data)   mp_queue = multiprocessing.Queue() processes = []   for device in nodes:
  p = multiprocessing.Process(target=connect_to_dev, args=[device, mp_queue])
  print("Adding Process to the list")
  processes.append(p)
  p.start()   for p in processes:
  print("Joining the finished process to the main truck")
  p.join()   results = [] for p in processes:
  print("Moving the result from the queue to the results list")
  results.append(mp_queue.get())   pprint(results)

在前面的例子中,适用以下内容:

  • 我们从multiprocess模块中导入了另一个名为Queue()的类,并将其实例化为mp_queue变量。

  • 然后,在进程创建过程中,我们将此队列作为参数与设备一起附加,因此每个进程都可以访问相同的队列并能够向其写入数据。

  • connect_to_dev()函数连接到每个设备并在终端上执行show run命令,然后将输出写入共享队列。

请注意,在将其添加到共享队列之前,我们将输出格式化为字典项{ip:<command_output>},并使用mp_queue.put()将其添加到共享队列中。

  • 在进程完成执行并加入主(父)进程之后,我们使用mp_queue.get()来检索结果列表中的队列项,然后使用pprint来漂亮地打印输出。

概要

在本章中,我们学习了 Python 多进程库以及如何实例化和并行执行 Python 代码。

在下一章中,我们将学习如何准备实验室环境并探索自动化选项以加快服务器部署速度。

第八章:准备实验室环境

在本章中,我们将使用两个流行的 Linux 发行版 CentOS 和 Ubuntu 来设置实验室。CentOS 是一个以社区驱动的 Linux 操作系统,面向企业服务器,并以其与Red Hat Enterprise LinuxRHEL)的兼容性而闻名。Ubuntu 是另一个基于著名的 Debian 操作系统的 Linux 发行版;目前由 Canonical Ltd.开发,并为其提供商业支持。

我们还将学习如何使用名为Cobbler的免费开源软件安装这两个 Linux 发行版,它将使用kickstart为 CentOS 自动引导服务器并使用 Anaconda 为基于 Debian 的系统进行自定义。

本章将涵盖以下主题:

  • 获取 Linux 操作系统

  • 在 hypervisor 上创建一个自动化机器

  • 开始使用 Cobbler

获取 Linux 操作系统

在接下来的章节中,我们将在不同的 hypervisors 上创建两台 Linux 机器,CentOS 和 Ubuntu。这些机器将作为我们环境中的自动化服务器。

下载 CentOS

CentOS 二进制文件可以通过多种方法下载。您可以直接从世界各地的多个 FTP 服务器下载它们,也可以从种子人员那里以种子方式下载它们。此外,CentOS 有两种版本:

  • Minimal ISO:提供基本服务器和必要软件包

  • Everything ISO:提供服务器和主要存储库中的所有可用软件包

首先,前往 CentOS 项目链接(www.centos.org/)并单击获取 CentOS 现在按钮,如下截图所示:

然后,选择最小的 ISO 镜像,并从任何可用的下载站点下载它。

CentOS 可用于多个云提供商,如 Google、Amazon、Azure 和 Oracle Cloud。您可以在cloud.centos.org/centos/7/images/找到所有云镜像。

下载 Ubuntu

Ubuntu 以为为向最终用户提供良好的桌面体验而广为人知。Canonical(Ubuntu 开发者)与许多服务器供应商合作,以在不同的硬件上认证 Ubuntu。Canonical 还为 Ubuntu 提供了一个服务器版本,其中包括 16.04 中的许多功能,例如:

  • Canonical 将在 2021 年之前提供支持

  • 能够在所有主要架构上运行-x86、x86-64、ARM v7、ARM64、POWER8 和 IBM s390x(LinuxONE)

  • ZFS 支持,这是一种适用于服务器和容器的下一代卷管理文件系统

  • LXD Linux 容器 hypervisor 增强,包括 QoS 和资源控制(CPU、内存、块 I/O 和存储配额)

  • 安装 snaps,用于简单的应用程序安装和发布管理。

  • DPDK 的首个生产版本-线速内核网络

  • Linux 4.4 内核和systemd服务管理器

  • 作为 AWS、Microsoft Azure、Joyent、IBM、Google Cloud Platform 和 Rackspace 上的客户进行认证

  • Tomcat(v8)、PostgreSQL(v9.5)、Docker v(1.10)、Puppet(v3.8.5)、QEMU(v2.5)、Libvirt(v1.3.1)、LXC(v2.0)、MySQL(v5.6)等的更新

您可以通过浏览至www.ubuntu.com/download/server并选择 Ubuntu 16.04 LTS 来下载 Ubuntu LTS:

在 hypervisor 上创建一个自动化机器

下载 ISO 文件后,我们将在 VMware ESXi 和 KVM hypervisors 上创建一个 Linux 机器。

在 VMware ESXi 上创建一个 Linux 机器

我们将使用 VMware vSphere 客户端创建一个虚拟机。使用 root 凭据登录到可用的 ESXi 服务器之一。首先,您需要将 Ubuntu 或 CentOS ISO 上传到 VMware 数据存储中。然后,按照以下步骤创建机器:

  1. 右键单击服务器名称,然后选择新的虚拟机:

  1. 选择自定义安装,这样您在安装过程中将有更多选项:

  1. 为 VM 提供一个名称:AutomationServer。

  2. 选择机器版本:8。

  3. 选择要创建机器的数据存储。

  4. 选择客户操作系统:Ubuntu Linux(64 位)或 Red Hat 版本 6/7:

  1. VM 规格不应少于 2 个 vCPU 和 4GB RAM,以便获得高效的性能。分别在 CPU 和内存选项卡中选择它们。

  2. 在“网络”选项卡中,选择两个带有 E1000 适配器的接口。其中一个接口将连接到互联网,第二个接口将管理客户端:

  1. 选择系统的默认 SCSI 控制器。在我的情况下,它将是 LSI 逻辑并行。

  2. 选择创建一个新的虚拟磁盘,并为 VM 提供 20GB 的磁盘大小。

  3. 现在虚拟机已准备就绪,您可以开始 Linux 操作系统的安装。将上传的镜像关联到 CD/DVD 驱动器,并确保选择“开机时连接”选项:

一旦它开始运行,您将被要求选择一种语言:

按照通常的步骤完成 CentOS/Ubuntu 安装。

在 KVM 上创建 Linux 机器

我们将使用 KVM 中提供的virt-manager实用程序启动 KVM 的桌面管理。然后我们将创建一个新的 VM:

  1. 在这里,我们将选择本地安装媒体(ISO 镜像或 CDROM)作为安装方法:

  1. 然后,我们将点击浏览并选择先前下载的镜像(CentOS 或 Ubuntu)。您将注意到 KVM 成功检测到操作系统类型和版本:

  1. 然后,我们将根据 CPU、内存和存储选择机器规格:

  1. 为您的机器选择适当的存储空间:

  1. 最后一步是选择一个名称,然后点击“在安装前自定义配置”选项,以添加一个额外的网络接口到自动化服务器。然后,点击“完成”:

打开另一个窗口,其中包含机器的所有规格。点击“添加硬件”,然后选择“网络”:

我们将添加另一个网络接口以与客户端通信。第一个网络接口使用 NAT 通过物理无线网卡连接到互联网:

最后,在主窗口上点击“开始安装”,KVM 将开始分配硬盘并将 ISO 镜像附加到虚拟机上:

一旦完成,您将看到以下屏幕:

按照通常的步骤完成 CentOS/Ubuntu 安装。

开始使用 Cobbler

Cobbler 是一款用于无人值守网络安装的开源软件。它利用多个工具,如 DHCP、FTP、PXE 和其他开源工具(稍后我们将解释它们),以便您可以一站式自动安装操作系统。目标机器(裸机或虚拟机)必须支持从其网络接口卡(NIC)引导。此功能使机器能够发送一个 DHCP 请求,该请求会命中 Cobbler 服务器,后者将处理其余事宜。

您可以在其 GitHub 页面上阅读有关该项目的更多信息(github.com/cobbler/cobbler)。

了解 Cobbler 的工作原理

Cobbler 依赖于多个工具来为客户端提供预引导执行环境(PXE)功能。首先,它依赖于接收客户端开机时的 DHCP 广播消息的 DHCP 服务;然后,它会回复一个 IP 地址、子网掩码、下一个服务器(TFTP),最后是pxeLinux.0,这是客户端最初向服务器发送 DHCP 消息时请求的加载程序文件名。

第二个工具是 TFTP 服务器,它托管pxeLinux.0和不同的发行版镜像。

第三个工具是模板渲染实用程序。Cobbler 使用cheetah,这是一个由 Python 开发的开源模板引擎,并且有自己的 DSL(特定领域语言)格式。我们将使用它来生成kickstart文件。

Kickstart 文件用于自动安装基于 Red Hat 的发行版,如 CentOS、Red Hat 和 Fedora。它还有限的支持用于安装基于 Debian 系统的Anaconda文件的渲染。

还有其他附加工具。reposync用于将在线存储库从互联网镜像到 Cobbler 内的本地目录,使其对客户端可用。ipmitools用于远程管理不同服务器硬件的开关机:

在以下拓扑中,Cobbler 托管在先前安装的自动化服务器上,并将连接到一对服务器。我们将通过 Cobbler 在它们上安装 Ubuntu 和 Red Hat。自动化服务器还有另一个接口直接连接到互联网,以便下载 Cobbler 所需的一些附加软件包,我们将在下一节中看到:

服务器 IP 地址
自动化服务器(已安装 cobbler) 10.10.10.130
服务器 1(CentOS 机器) IP 范围为10.10.10.5-10.10.10.10
服务器 2(Ubuntu 机器) IP 范围为10.10.10.5-10.10.10.10

在自动化服务器上安装 Cobbler

我们将首先在我们的自动化服务器(无论是 CentOS 还是 Ubuntu)上安装一些基本软件包,如vimtcpudumpwgetnet-tools。然后,我们将从epel存储库安装cobbler软件包。请注意,这些软件包对于 Cobbler 并不是必需的,但我们将使用它们来了解 Cobbler 的真正工作原理。

对于 CentOS,请使用以下命令:

yum install vim vim-enhanced tcpdump net-tools wget git -y

对于 Ubuntu,请使用以下命令:

sudo apt install vim tcpdump net-tools wget git -y

然后,我们需要禁用防火墙。Cobbler 与 SELinux 策略不兼容,建议禁用它,特别是如果您对它们不熟悉。此外,我们将禁用iptablesfirewalld,因为我们在实验室中,而不是在生产环境中。

对于 CentOS,请使用以下命令:

# Disable firewalld service
systemctl disable firewalld
systemctl stop firewalld

# Disable IPTables service
systemctl disable iptables.service
systemctl stop iptables.service

# Set SELinux to permissive instead of enforcing
sed -i s/^SELinux=.*$/SELinux=permissive/ /etc/seLinux/config
setenforce 0

对于 Ubuntu,请使用以下命令:

# Disable ufw service
sudo ufw disable

# Disable IPTables service 
sudo iptables-save > $HOME/BeforeCobbler.txt 
sudo iptables -X 
sudo iptables -t nat -F 
sudo iptables -t nat -X 
sudo iptables -t mangle -F 
sudo iptables -t mangle -X 
sudo iptables -P INPUT ACCEPT 
sudo iptables -P FORWARD ACCEPT 
sudo iptables -P OUTPUT ACCEPT

# Set SELinux to permissive instead of enforcing
sed -i s/^SELinux=.*$/SELinux=permissive/ /etc/seLinux/config
setenforce 0

最后,重新启动自动化服务器机器以使更改生效:

reboot

现在,我们将安装cobbler软件包。该软件在epel存储库中可用(但我们需要先安装它)在 CentOS 的情况下。Ubuntu 在上游存储库中没有该软件可用,因此我们将在该平台上下载源代码并进行编译。

对于 CentOS,请使用以下命令:

# Download and Install EPEL Repo
yum install epel-release -y

# Install Cobbler
yum install cobbler -y

#Install cobbler Web UI and other dependencies
yum install cobbler-web dnsmasq fence-agents bind xinetd pykickstart -y

撰写本书时的 Cobbler 当前版本为 2.8.2,发布于 2017 年 9 月 16 日。对于 Ubuntu,我们将从 GIT 存储库克隆最新的软件包,并从源代码构建它:

#install the dependencies as stated in (http://cobbler.github.io/manuals/2.8.0/2/1_-_Prerequisites.html)

sudo apt-get install createrepo apache2 mkisofs libapache2-mod-wsgi mod_ssl python-cheetah python-netaddr python-simplejson python-urlgrabber python-yaml rsync sysLinux atftpd yum-utils make python-dev python-setuptools python-django -y

#Clone the cobbler 2.8 from the github to your server (require internet)
git clone https://github.com/cobbler/cobbler.git
cd cobbler

#Checkout the release28 (latest as the developing of this book)
git checkout release28

#Build the cobbler core package
make install

#Build cobbler web
make webtest

成功在我们的机器上安装 Cobbler 后,我们需要自定义它以更改默认设置以适应我们的网络环境。我们需要更改以下内容:

  • 选择binddnsmasq模块来管理 DNS 查询

  • 选择iscdnsmaasq模块来为客户端提供传入的 DHCP 请求

  • 配置 TFTP Cobbler IP 地址(在 Linux 中通常是静态地址)。

  • 提供为客户端提供 DHCP 范围

  • 重新启动服务以应用配置

让我们逐步查看配置:

  1. 选择dnsmasq作为 DNS 服务器:
vim /etc/cobbler/modules.conf
[dns]
module = manage_dnsmasq
vim /etc/cobbler/settings
manage_dns: 1
restart_dns: 1
  1. 选择dnsmasq来管理 DHCP 服务:
vim /etc/cobbler/modules.conf

[dhcp]
module = manage_dnsmasq
vim /etc/cobbler/settings
manage_dhcp: 1
restart_dhcp: 1
  1. 将 Cobbler IP 地址配置为 TFTP 服务器:
vim /etc/cobbler/settings
server: 10.10.10.130
next_server: 10.10.10.130
vim /etc/xinetd.d/tftp
 disable                 = no

还要通过将pxe_just_once设置为0来启用 PXE 引导循环预防:

pxe_just_once: 0
  1. dnsmasq服务模板中添加客户端dhcp-range
vim /etc/cobbler/dnsmasq.template
dhcp-range=10.10.10.5,10.10.10.10,255.255.255.0

注意其中一行写着dhcp-option=66,$next_server。这意味着 Cobbler 将把之前在设置中配置为 TFTP 引导服务器的next_server传递给通过dnsmasq提供的 DHCP 服务请求 IP 地址的任何客户端。

  1. 启用并重新启动服务:
systemctl enable cobblerd
systemctl enable httpd
systemctl enable dnsmasq

systemctl start cobblerd
systemctl start httpd
systemctl start dnsmasq

通过 Cobbler 提供服务器

现在我们离通过 Cobbler 使我们的第一台服务器运行起来只有几步之遥。基本上,我们需要告诉 Cobbler 我们客户端的 MAC 地址以及它们使用的操作系统:

  1. 导入 Linux ISO。Cobbler 将自动分析映像并为其创建一个配置文件:

cobbler import --arch=x86_64 --path=/mnt/cobbler_images --name=CentOS-7-x86_64-Minimal-1708

task started: 2018-03-28_132623_import
task started (id=Media import, time=Wed Mar 28 13:26:23 2018)
Found a candidate signature: breed=redhat, version=rhel6
Found a candidate signature: breed=redhat, version=rhel7
Found a matching signature: breed=redhat, version=rhel7
Adding distros from path /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64:
creating new distro: CentOS-7-Minimal-1708-x86_64
trying symlink: /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64 -> /var/www/cobbler/links/CentOS-7-Minimal-1708-x86_64
creating new profile: CentOS-7-Minimal-1708-x86_64
associating repos
checking for rsync repo(s)
checking for rhn repo(s)
checking for yum repo(s)
starting descent into /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64 for CentOS-7-Minimal-1708-x86_64
processing repo at : /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64
need to process repo/comps: /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64
looking for /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64/repodata/*comps*.xml
Keeping repodata as-is :/var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64/repodata
*** TASK COMPLETE ***

在将其导入到挂载点之前,您可能需要挂载 Linux ISO 映像,使用mount -O loop /root/<image_iso>  /mnt/cobbler_images/

您可以运行cobbler profile report命令来检查创建的配置文件:

cobbler profile report

Name                           : CentOS-7-Minimal-1708-x86_64
TFTP Boot Files                : {}
Comment                        : 
DHCP Tag                       : default
Distribution                   : CentOS-7-Minimal-1708-x86_64
Enable gPXE?                   : 0
Enable PXE Menu?               : 1
Fetchable Files                : {}
Kernel Options                 : {}
Kernel Options (Post Install)  : {}
Kickstart                      : /var/lib/cobbler/kickstarts/sample_end.ks
Kickstart Metadata             : {}
Management Classes             : []
Management Parameters          : <<inherit>>
Name Servers                   : []
Name Servers Search Path       : []
Owners                         : ['admin']
Parent Profile                 : 
Internal proxy                 : 
Red Hat Management Key         : <<inherit>>
Red Hat Management Server      : <<inherit>>
Repos                          : []
Server Override                : <<inherit>>
Template Files                 : {}
Virt Auto Boot                 : 1
Virt Bridge                    : xenbr0
Virt CPUs                      : 1
Virt Disk Driver Type          : raw
Virt File Size(GB)             : 5
Virt Path                      : 
Virt RAM (MB)                  : 512
Virt Type                      : kvm

您可以看到import命令自动填充了许多字段,如KickstartRAM操作系统initrd/kernel文件位置。

  1. 向配置文件添加任何额外的存储库(可选):
cobbler repo add --mirror=https://dl.fedoraproject.org/pub/epel/7/x86_64/ --name=epel-local --priority=50 --arch=x86_64 --breed=yum

cobbler reposync 

现在,编辑配置文件,并将创建的存储库添加到可用存储库列表中:

cobbler profile edit --name=CentOS-7-Minimal-1708-x86_64 --repos="epel-local"
  1. 添加客户端 MAC 地址并将其链接到创建的配置文件:
cobbler system add --name=centos_client --profile=CentOS-7-Minimal-1708-x86_64  --mac=00:0c:29:4c:71:7c --ip-address=10.10.10.5 --subnet=255.255.255.0 --static=1 --hostname=centos-client  --gateway=10.10.10.1 --name-servers=8.8.8.8 --interface=eth0

--hostname字段对应于本地系统名称,并使用--ip-address--subnet--gateway选项配置客户端网络。这将使 Cobbler 生成一个带有这些选项的kickstart文件。

如果您需要自定义服务器并添加额外的软件包、配置防火墙、ntp 以及配置分区和硬盘布局,那么您可以将这些设置添加到kickstart文件中。Cobbler 在/var/lib/cobbler/kickstarts/sample.ks下提供了示例文件,您可以将其复制到另一个文件夹,并在上一个命令中提供--kickstart参数。

您可以通过在kickstart文件中运行 Ansible 来将 Ansible 集成到其中,使用拉模式(而不是默认的推送模式)。Ansible 将从在线 GIT 存储库(如 GitHub 或 GitLab)下载 playbook,并在此之后执行它。

  1. 通过以下命令指示 Cobbler 生成为我们的客户端提供服务所需的配置文件,并使用新信息更新内部数据库:
#cobbler sync  task started: 2018-03-28_141922_sync
task started (id=Sync, time=Wed Mar 28 14:19:22 2018)
running pre-sync triggers
cleaning trees
removing: /var/www/cobbler/images/CentOS-7-Minimal-1708-x86_64
removing: /var/www/cobbler/images/Ubuntu_Server-x86_64
removing: /var/www/cobbler/images/Ubuntu_Server-hwe-x86_64
removing: /var/lib/tftpboot/pxeLinux.cfg/default
removing: /var/lib/tftpboot/pxeLinux.cfg/01-00-0c-29-4c-71-7c
removing: /var/lib/tftpboot/grub/01-00-0C-29-4C-71-7C
removing: /var/lib/tftpboot/grub/efidefault
removing: /var/lib/tftpboot/grub/grub-x86_64.efi
removing: /var/lib/tftpboot/grub/images
removing: /var/lib/tftpboot/grub/grub-x86.efi
removing: /var/lib/tftpboot/images/CentOS-7-Minimal-1708-x86_64
removing: /var/lib/tftpboot/images/Ubuntu_Server-x86_64
removing: /var/lib/tftpboot/images/Ubuntu_Server-hwe-x86_64
removing: /var/lib/tftpboot/s390x/profile_list
copying bootloaders
trying hardlink /var/lib/cobbler/loaders/grub-x86_64.efi -> /var/lib/tftpboot/grub/grub-x86_64.efi
trying hardlink /var/lib/cobbler/loaders/grub-x86.efi -> /var/lib/tftpboot/grub/grub-x86.efi
copying distros to tftpboot
copying files for distro: Ubuntu_Server-x86_64
trying hardlink /var/www/cobbler/ks_mirror/Ubuntu_Server-x86_64/install/netboot/ubuntu-installer/amd64/Linux -> /var/lib/tftpboot/images/Ubuntu_Server-x86_64/Linux
trying hardlink /var/www/cobbler/ks_mirror/Ubuntu_Server-x86_64/install/netboot/ubuntu-installer/amd64/initrd.gz -> /var/lib/tftpboot/images/Ubuntu_Server-x86_64/initrd.gz
copying files for distro: Ubuntu_Server-hwe-x86_64
trying hardlink /var/www/cobbler/ks_mirror/Ubuntu_Server-x86_64/install/hwe-netboot/ubuntu-installer/amd64/Linux -> /var/lib/tftpboot/images/Ubuntu_Server-hwe-x86_64/Linux
trying hardlink /var/www/cobbler/ks_mirror/Ubuntu_Server-x86_64/install/hwe-netboot/ubuntu-installer/amd64/initrd.gz -> /var/lib/tftpboot/images/Ubuntu_Server-hwe-x86_64/initrd.gz
copying files for distro: CentOS-7-Minimal-1708-x86_64
trying hardlink /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64/images/pxeboot/vmlinuz -> /var/lib/tftpboot/images/CentOS-7-Minimal-1708-x86_64/vmlinuz
trying hardlink /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64/images/pxeboot/initrd.img -> /var/lib/tftpboot/images/CentOS-7-Minimal-1708-x86_64/initrd.img
copying images
generating PXE configuration files
generating: /var/lib/tftpboot/pxeLinux.cfg/01-00-0c-29-4c-71-7c
generating: /var/lib/tftpboot/grub/01-00-0C-29-4C-71-7C
generating PXE menu structure
copying files for distro: Ubuntu_Server-x86_64
trying hardlink /var/www/cobbler/ks_mirror/Ubuntu_Server-x86_64/install/netboot/ubuntu-installer/amd64/Linux -> /var/www/cobbler/images/Ubuntu_Server-x86_64/Linux
trying hardlink /var/www/cobbler/ks_mirror/Ubuntu_Server-x86_64/install/netboot/ubuntu-installer/amd64/initrd.gz -> /var/www/cobbler/images/Ubuntu_Server-x86_64/initrd.gz
Writing template files for Ubuntu_Server-x86_64
copying files for distro: Ubuntu_Server-hwe-x86_64
trying hardlink /var/www/cobbler/ks_mirror/Ubuntu_Server-x86_64/install/hwe-netboot/ubuntu-installer/amd64/Linux -> /var/www/cobbler/images/Ubuntu_Server-hwe-x86_64/Linux
trying hardlink /var/www/cobbler/ks_mirror/Ubuntu_Server-x86_64/install/hwe-netboot/ubuntu-installer/amd64/initrd.gz -> /var/www/cobbler/images/Ubuntu_Server-hwe-x86_64/initrd.gz
Writing template files for Ubuntu_Server-hwe-x86_64
copying files for distro: CentOS-7-Minimal-1708-x86_64
trying hardlink /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64/images/pxeboot/vmlinuz -> /var/www/cobbler/images/CentOS-7-Minimal-1708-x86_64/vmlinuz
trying hardlink /var/www/cobbler/ks_mirror/CentOS-7-x86_64-Minimal-1708-x86_64/images/pxeboot/initrd.img -> /var/www/cobbler/images/CentOS-7-Minimal-1708-x86_64/initrd.img
Writing template files for CentOS-7-Minimal-1708-x86_64
rendering DHCP files
rendering DNS files
rendering TFTPD files
generating /etc/xinetd.d/tftp
processing boot_files for distro: Ubuntu_Server-x86_64
processing boot_files for distro: Ubuntu_Server-hwe-x86_64
processing boot_files for distro: CentOS-7-Minimal-1708-x86_64
cleaning link caches
running post-sync triggers
running python triggers from /var/lib/cobbler/triggers/sync/post/*
running python trigger cobbler.modules.sync_post_restart_services
running: service dnsmasq restart
received on stdout: 
received on stderr: Redirecting to /bin/systemctl restart dnsmasq.service

running shell triggers from /var/lib/cobbler/triggers/sync/post/*
running python triggers from /var/lib/cobbler/triggers/change/*
running python trigger cobbler.modules.scm_track
running shell triggers from /var/lib/cobbler/triggers/change/*
*** TASK COMPLETE ***

一旦您启动了 CentOS 客户端,您将注意到它进入 PXE 过程并通过PXE_Network发送 DHCP 消息。Cobbler 将以 MAC 地址分配一个 IP 地址、一个PXELinux0文件和所需的镜像来响应:

在 Cobbler 完成 CentOS 安装后,您将看到主机名在机器中正确配置:

您可以为 Ubuntu 机器执行相同的步骤。

摘要

在本章中,您学习了如何通过在虚拟化程序上安装两台 Linux 机器(CentOS 和 Ubuntu)来准备实验室环境。然后,我们探讨了自动化选项,并通过安装 Cobbler 加快了服务器部署速度。

在下一章中,您将学习如何从 Python 脚本直接向操作系统 shell 发送命令并调查返回的输出。

第九章:使用 Subprocess 模块

运行和生成新的系统进程对于想要自动化特定操作系统任务或在脚本中执行一些命令的系统管理员非常有用。Python 提供了许多库来调用外部系统实用程序,并与生成的数据进行交互。最早创建的库是OS模块,它提供了一些有用的工具来调用外部进程,比如os.systemos.spwanos.popen*。然而,它缺少一些基本功能,因此 Python 开发人员引入了一个新的库,subprocess,它可以生成新的进程,与进程发送和接收,并处理错误和返回代码。目前,官方 Python 文档建议使用subprocess模块来访问系统命令,Python 实际上打算用它来替换旧的模块。

本章将涵盖以下主题:

  • Popen()子进程

  • 读取stdinstdoutstderr

  • 子进程调用套件

popen()子进程

subprocess模块只实现了一个类:popen()。这个类的主要用途是在系统上生成一个新的进程。这个类可以接受运行进程的额外参数,以及popen()本身的额外参数:

参数 含义
args 一个字符串,或者程序参数的序列。
bufsize 它作为open()函数的缓冲参数提供,用于创建stdin/stdout/stderr管道文件对象。
executable 要执行的替换程序。
stdinstdoutstderr 这些分别指定了执行程序的标准输入、标准输出和标准错误文件句柄。
shell 如果为True,则命令将通过 shell 执行(默认为False)。在 Linux 中,这意味着在运行子进程之前调用/bin/sh
cwd 在执行子进程之前设置当前目录。
env 定义新进程的环境变量。

现在,让我们专注于argspopen()命令可以接受 Python 列表作为输入,其中第一个元素被视为命令,后续元素被视为命令args,如下面的代码片段所示:

import subprocess
print(subprocess.Popen("ifconfig"))

脚本输出

从命令返回的输出直接打印到您的 Python 终端。

ifconfig是一个用于返回网络接口信息的 Linux 实用程序。对于 Windows 用户,您可以通过在 cmd 上使用ipconfig命令来获得类似的输出。

我们可以重写上面的代码,使用列表而不是字符串,如下面的代码片段所示:

print(subprocess.Popen(["ifconfig"]))

使用这种方法允许您将额外的参数添加到主命令作为列表项:

print(subprocess.Popen(["sudo", "ifconfig", "enp60s0:0", "10.10.10.2", "netmask", "255.255.255.0", "up"])) enp60s0:0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 10.10.10.2  netmask 255.255.255.0  broadcast 10.10.10.255
        ether d4:81:d7:cb:b7:1e  txqueuelen 1000  (Ethernet)
        device interrupt 16  

请注意,如果您将上一个命令提供为字符串而不是列表,就像我们在第一个示例中所做的那样,命令将失败,如下面的屏幕截图所示。子进程Popen()期望在每个列表元素中有一个可执行名称,而不是其他任何参数。

另一方面,如果您想使用字符串方法而不是列表,您可以将shell参数设置为True。这将指示Popen()在命令之前附加/bin/sh,因此命令将在其后执行所有参数:

print(subprocess.Popen("sudo ifconfig enp60s0:0 10.10.10.2 netmask 255.255.255.0 up", shell=True)) 

您可以将shell=True视为生成一个 shell 进程并将命令与参数传递给它。这可以通过使用split()节省您几行代码,以便直接从外部系统接收命令并运行它。

subprocess使用的默认 shell 是/bin/sh。如果您使用其他 shell,比如tchcsh,您可以在executable参数中定义它们。还要注意,作为 shell 运行命令可能会带来安全问题,并允许安全注入。指示您的代码运行脚本的用户可以添加"; rm -rf /",导致可怕的事情发生。

此外,您可以使用cwd参数在运行命令之前将目录更改为特定目录。当您需要在对其进行操作之前列出目录的内容时,这将非常有用:

import subprocess
print(subprocess.Popen(["cat", "interfaces"], cwd="/etc/network"))  

Ansible 有一个类似的标志叫做chdir:。此参数将用于 playbook 任务中,在执行之前更改目录。

读取标准输入(stdin)、标准输出(stdout)和标准错误(stderr)

生成的进程可以通过三个通道与操作系统通信:

  1. 标准输入(stdin)

  2. 标准输出(stdout)

  3. 标准错误(stderr)

在子进程中,Popen()可以与三个通道交互,并将每个流重定向到外部文件,或者重定向到一个称为PIPE的特殊值。另一个方法叫做communicate(),用于从stdout读取和写入stdincommunicate()方法可以从用户那里获取输入,并返回标准输出和标准错误,如下面的代码片段所示:

import subprocess
p = subprocess.Popen(["ping", "8.8.8.8", "-c", "3"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) stdout, stderr = p.communicate() print("""==========The Standard Output is========== {}""".format(stdout))   print("""==========The Standard Error is========== {}""".format(stderr))

同样,您可以使用communicate()中的输入参数发送数据并写入进程:

import subprocess
p = subprocess.Popen(["grep", "subprocess"], stdout=subprocess.PIPE, stdin=subprocess.PIPE) stdout,stderr = p.communicate(input=b"welcome to subprocess module\nthis line is a new line and doesnot contain the require string")   print("""==========The Standard Output is========== {}""".format(stdout))   print("""==========The Standard Error is========== {}""".format(stderr))

在脚本中,我们在communicate()中使用了input参数,它将数据发送到另一个子进程,该子进程将使用grep命令搜索子进程关键字。返回的输出将存储在stdout变量中:

验证进程成功执行的另一种方法是使用返回代码。当命令成功执行且没有错误时,返回代码将为0;否则,它将是大于0的整数值:

import subprocess

def ping_destination(ip):   p = subprocess.Popen(['ping', '-c', '3'],
  stdout=subprocess.PIPE,
  stderr=subprocess.PIPE)
  stdout, stderr = p.communicate(input=ip)
  if p.returncode == 0:
  print("Host is alive")
  return True, stdout
    else:
  print("Host is down")
  return False, stderr
 while True:
    print(ping_destination(raw_input("Please enter the host:"))) 

脚本将要求用户输入一个 IP 地址,然后调用ping_destination()函数,该函数将针对 IP 地址执行ping命令。ping命令的结果(成功或失败)将返回到标准输出,并且communicate()函数将使用结果填充返回代码:

首先,我们测试了 Google DNS IP 地址。主机是活动的,并且命令将成功执行,返回代码=0。函数将返回True并打印主机是活动的。其次,我们使用了HostNotExist字符串进行测试。函数将返回False到主程序并打印主机已关闭。此外,它将打印返回给子进程的命令标准输出(Name or service not known)。

您可以使用echo $?来检查先前执行的命令的返回代码(有时称为退出代码)。

子进程调用套件

子进程模块提供了另一个函数,使进程生成比使用Popen()更安全。子进程call()函数等待被调用的命令/程序完成读取输出。它支持与Popen()构造函数相同的参数,如shellexecutablecwd,但这次,您的脚本将等待程序完成并填充返回代码,而无需communicate()

如果您检查call()函数,您会发现它实际上是Popen()类的一个包装器,但具有一个wait()函数,它会在返回输出之前等待命令结束:

import subprocess
subprocess.call(["ifconfig", "docker0"], stdout=subprocess.PIPE, stderr=None, shell=False) 

如果您希望为您的代码提供更多保护,可以使用check_call()函数。它与call()相同,但会对返回代码进行另一个检查。如果它等于0(表示命令已成功执行),则将返回输出。否则,它将引发一个带有返回退出代码的异常。这将允许您在程序流中处理异常:

import subprocess

try:
  result = subprocess.check_call(["ping", "HostNotExist", "-c", "3"]) except subprocess.CalledProcessError:
  print("Host is not found") 

使用call()函数的一个缺点是,您无法像使用Popen()那样使用communicate()将数据发送到进程。

总结

在本章中,我们学习了如何在系统中运行和生成新进程,以及我们了解了这些生成的进程如何与操作系统通信。我们还讨论了子进程模块和subprocess调用。

在下一章中,我们将看到如何在远程主机上运行和执行命令。

第十章:使用 Fabric 运行系统管理任务

在上一章中,我们使用了subprocess模块在托管我们的 Python 脚本的机器内运行和生成系统进程,并将输出返回到终端。然而,许多自动化任务需要访问远程服务器以执行命令,这不容易使用子进程来实现。使用另一个 Python 模块Fabric就变得轻而易举。该库连接到远程主机并执行不同的任务,例如上传和下载文件,使用特定用户 ID 运行命令,并提示用户输入。Fabric Python 模块是从一个中心点管理数十台 Linux 机器的强大工具。

本章将涵盖以下主题:

  • 什么是 Fabric?

  • 执行您的第一个 Fabric 文件

  • 其他有用的 Fabric 功能

技术要求

以下工具应安装并在您的环境中可用:

  • Python 2.7.1x。

  • PyCharm 社区版或专业版。

  • EVE-NG 拓扑。有关如何安装和配置系统服务器,请参阅第八章“准备实验环境”。

您可以在以下 GitHub URL 找到本章中开发的完整脚本:github.com/TheNetworker/EnterpriseAutomation.git

什么是 Fabric?

Fabric (www.fabfile.org/)是一个高级 Python 库,用于连接到远程服务器(通过 paramiko 库)并在其上执行预定义的任务。它在托管 fabric 模块的机器上运行一个名为fab的工具。此工具将查找位于您运行工具的相同目录中的fabfile.py文件。fabfile.py文件包含您的任务,定义为从命令行调用的 Python 函数,以在服务器上启动执行。Fabric 任务本身只是普通的 Python 函数,但它们包含用于在远程服务器上执行命令的特殊方法。此外,在fabfile.py的开头,您需要定义一些环境变量,例如远程主机、用户名、密码以及执行期间所需的任何其他变量:

安装

Fabric 需要 Python 2.5 到 2.7。您可以使用pip安装 Fabric 及其所有依赖项,也可以使用系统包管理器,如yumapt。在这两种情况下,您都将在操作系统中准备好并可执行fab实用程序。

要使用pip安装fabric,请在自动化服务器上运行以下命令:

pip install fabric

请注意,Fabric 需要paramiko,这是一个常用的 Python 库,用于建立 SSH 连接。

您可以通过两个步骤验证 Fabric 安装。首先,确保您的系统中有fab命令可用:

[root@AutomationServer ~]# which fab
/usr/bin/fab

验证的第二步是打开 Python 并尝试导入fabric库。如果没有抛出错误,则 Fabric 已成功安装:

[root@AutomationServer ~]# python
Python 2.7.5 (default, Aug  4 2017, 00:39:18) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from fabric.api import *
>>>

Fabric 操作

fabric工具中有许多可用的操作。这些操作在 fabfile 中作为任务内的函数,但以下是fabric库中最重要操作的摘要。

使用运行操作

Fabric 中run操作的语法如下:

run(command, shell=True, pty=True, combine_stderr=True, quiet=False, warn_only=False, stdout=None, stderr=None)

这将在远程主机上执行命令,而shell参数控制是否在执行之前创建一个 shell(例如/bin/sh)(相同的参数也存在于子进程中)。

命令执行后,Fabric 将填充.succeeded.failed,取决于命令输出。您可以通过调用以下内容来检查命令是否成功或失败:

def run_ops():
  output = run("hostname")  

使用获取操作

Fabric get操作的语法如下:

get(remote_path, local_path)

这将从远程主机下载文件到运行 fabfile 的机器,使用 rsyncscp。例如,当您需要将日志文件收集到服务器时,通常会使用此功能。

def get_ops():
  try:
  get("/var/log/messages","/root/")
  except:
  pass

使用 put 操作

Fabric put 操作的语法如下:

put(local_path, remote_path, use_sudo=False, mirror_local_mode=False, mode=None)

此操作将从运行 fabfile(本地)的机器上传文件到远程主机。使用 use_sudo 将解决上传到根目录时的权限问题。此外,您可以保持本地和远程服务器上的当前文件权限,或者您可以设置新的权限:

def put_ops():
  try:
  put("/root/VeryImportantFile.txt","/root/")
  except:
  pass

使用 sudo 操作

Fabric sudo 操作的语法如下:

sudo(command, shell=True, pty=True, combine_stderr=True, user=None, quiet=False, warn_only=False, stdout=None, stderr=None, group=None)

此操作可以被视为 run() 命令的另一个包装器。但是,sudo 操作将默认使用 root 用户名运行命令,而不管用于执行 fabfile 的用户名如何。它还包含一个用户参数,该参数可用于使用不同的用户名运行命令。此外,user 参数使用特定的 UID 执行命令,而 group 参数定义 GID:

def sudo_ops():
  sudo("whoami") #it should print the root even if you use another account

使用提示操作

Fabric prompt 操作的语法如下:

prompt(text, key=None, default='', validate=None)

用户可以使用 prompt 操作为任务提供特定值,并且输入将存储在变量中并被任务使用。请注意,您将为 fabfile 中的每个主机提示:

def prompt_ops():
  prompt("please supply release name", default="7.4.1708")

使用重新启动操作

Fabric reboot 操作的语法如下:

reboot(wait=120)

这是一个简单的操作,默认情况下重新启动主机。Fabric 将等待 120 秒然后尝试重新连接,但是您可以使用 wait 参数将此值更改为其他值:

def reboot_ops():
  reboot(wait=60, use_sudo=True) 

有关其他支持的操作的完整列表,请查看 docs.fabfile.org/en/1.14/api/core/operations.html。您还可以直接从 PyCharm 查看它们,方法是查看在键入 Ctrl + 空格 时弹出的所有自动完成函数。从 fabric.operations 导入 <ctrl+space> 在 fabric.operations 下:

执行您的第一个 Fabric 文件

现在我们知道操作的工作原理,所以我们将把它放在 fabfile 中,并创建一个可以与远程机器一起工作的完整自动化脚本。fabfile 的第一步是导入所需的类。其中大部分位于 fabric.api 中,因此我们将全局导入所有这些类到我们的 Python 脚本中:

from fabric.api import *

下一步是定义远程机器的 IP 地址、用户名和密码。在我们的环境中,除了自动化服务器之外,我们还有两台机器分别运行 Ubuntu 16.04 和 CentOS 7.4,并具有以下详细信息:

机器类型 IP 地址 用户名 密码
Ubuntu 16.04 10.10.10.140 root access123
CentOS 7.4 10.10.10.193 root access123

我们将把它们包含在 Python 脚本中,如下面的片段所示:

env.hosts = [
  '10.10.10.140', # ubuntu machine
  '10.10.10.193', # CentOS machine ]   env.user = "root"  env.password = "access123" 

请注意,我们使用名为 env 的变量,该变量继承自 _AttributeDict 类。在此变量内部,我们可以设置来自 SSH 连接的用户名和密码。您还可以通过设置 env.use_ssh_config=True 使用存储在 .ssh 目录中的 SSH 密钥;Fabric 将使用这些密钥进行身份验证。

最后一步是将任务定义为 Python 函数。任务可以使用前面的操作来执行命令。

以下是完整的脚本:

from fabric.api import *    env.hosts = [
  '10.10.10.140', # ubuntu machine
  '10.10.10.193', # CentOS machine ]   env.user = "root" env.password = "access123"   def detect_host_type():
  output = run("uname -s")   if output.failed:
  print("something wrong happen, please check the logs")   elif output.succeeded:
  print("command executed successfully")   def list_all_files_in_directory():
  directory = prompt("please enter full path to the directory to list", default="/root")
  sudo("cd {0} ; ls -htlr".format(directory))     def main_tasks():
  detect_host_type()
  list_all_files_in_directory()

在上面的示例中,适用以下内容:

  • 我们定义了两个任务。第一个任务将执行 uname -s 命令并返回输出,然后验证命令是否成功执行。该任务使用 run() 操作来完成。

  • 第二个任务将使用两个操作:prompt()sudo()。第一个操作将要求用户输入目录的完整路径,而第二个操作将列出目录中的所有内容。

  • 最终任务main_tasks()将实际上将前面的两种方法组合成一个任务,以便我们可以从命令行调用它。

为了运行脚本,我们将上传文件到自动化服务器,并使用fab实用程序来运行它:

fab -f </full/path/to/fabfile>.py <task_name>

在上一个命令中,如果您的文件名不是fabfile.py,则-f开关是不强制的。如果不是,您将需要向fab实用程序提供名称。此外,fabfile应该在当前目录中;否则,您将需要提供完整的路径。现在我们将通过执行以下命令来运行fabfile

fab -f fabfile_first.py main_tasks

第一个任务将被执行,并将输出返回到终端:

[10.10.10.140] Executing task 'main_tasks'
[10.10.10.140] run: uname -s
[10.10.10.140] out: Linux
[10.10.10.140] out: 

command executed successfully 

现在,我们将进入/var/log/来列出内容:


please enter full path to the directory to list [/root] /var/log/
[10.10.10.140] sudo: cd /var/log/ ; ls -htlr
[10.10.10.140] out: total 1.7M
[10.10.10.140] out: drwxr-xr-x 2 root   root 4.0K Dec  7 23:54 lxd
[10.10.10.140] out: drwxr-xr-x 2 root   root 4.0K Dec 11 15:47 sysstat
[10.10.10.140] out: drwxr-xr-x 2 root   root 4.0K Feb 22 18:24 dist-upgrade
[10.10.10.140] out: -rw------- 1 root   utmp    0 Feb 28 20:23 btmp
[10.10.10.140] out: -rw-r----- 1 root   adm    31 Feb 28 20:24 dmesg
[10.10.10.140] out: -rw-r--r-- 1 root   root  57K Feb 28 20:24 bootstrap.log
[10.10.10.140] out: drwxr-xr-x 2 root   root 4.0K Apr  4 08:00 fsck
[10.10.10.140] out: drwxr-xr-x 2 root   root 4.0K Apr  4 08:01 apt
[10.10.10.140] out: -rw-r--r-- 1 root   root  32K Apr  4 08:09 faillog
[10.10.10.140] out: drwxr-xr-x 3 root   root 4.0K Apr  4 08:09 installer

command executed successfully

如果您需要列出 CentOS 机器上network-scripts目录下的配置文件,也是一样的:

 please enter full path to the directory to list [/root] /etc/sysconfig/network-scripts/ 
[10.10.10.193] sudo: cd /etc/sysconfig/network-scripts/ ; ls -htlr
[10.10.10.193] out: total 232K
[10.10.10.193] out: -rwxr-xr-x. 1 root root 1.9K Apr 15  2016 ifup-TeamPort
[10.10.10.193] out: -rwxr-xr-x. 1 root root 1.8K Apr 15  2016 ifup-Team
[10.10.10.193] out: -rwxr-xr-x. 1 root root 1.6K Apr 15  2016 ifdown-TeamPort
[10.10.10.193] out: -rw-r--r--. 1 root root  31K May  3  2017 network-functions-ipv6
[10.10.10.193] out: -rw-r--r--. 1 root root  19K May  3  2017 network-functions
[10.10.10.193] out: -rwxr-xr-x. 1 root root 5.3K May  3  2017 init.ipv6-global
[10.10.10.193] out: -rwxr-xr-x. 1 root root 1.8K May  3  2017 ifup-wireless
[10.10.10.193] out: -rwxr-xr-x. 1 root root 2.7K May  3  2017 ifup-tunnel
[10.10.10.193] out: -rwxr-xr-x. 1 root root 3.3K May  3  2017 ifup-sit
[10.10.10.193] out: -rwxr-xr-x. 1 root root 2.0K May  3  2017 ifup-routes
[10.10.10.193] out: -rwxr-xr-x. 1 root root 4.1K May  3  2017 ifup-ppp
[10.10.10.193] out: -rwxr-xr-x. 1 root root 3.4K May  3  2017 ifup-post
[10.10.10.193] out: -rwxr-xr-x. 1 root root 1.1K May  3  2017 ifup-plusb

<***output omitted for brevity>***

最后,Fabric 将断开与两台机器的连接:

[10.10.10.193] out: 

Done.
Disconnecting from 10.10.10.140... done.
Disconnecting from 10.10.10.193... done.

有关 fab 工具的更多信息

fab工具本身支持许多操作。它可以用来列出fabfile中的不同任务。它还可以在执行期间设置fab环境。例如,您可以使用-H--hosts开关定义将在其上运行命令的主机,而无需在fabfile中指定。这实际上是在执行期间在fabfile中设置env.hosts变量:

fab -H srv1,srv2

另一方面,您可以使用fab工具定义要运行的命令。这有点像 Ansible 的临时模式(我们将在第十三章中详细讨论这个问题,系统管理的 Ansible):

fab -H srv1,srv2 -- ifconfig -a

如果您不想在fabfile脚本中以明文存储密码,那么您有两个选项。第一个是使用-i选项使用 SSH 身份文件(私钥),它在连接期间加载文件。

另一个选项是使用-I选项强制 Fabric 在连接到远程机器之前提示您输入会话密码。

请注意,如果在fabfile中指定了env.password参数,此选项将覆盖该参数。

-D开关将禁用已知主机,并强制 Fabric 不从.ssh目录加载known_hosts文件。您可以使用-r--reject-unknown-hosts选项使 Fabric 拒绝连接到known_hosts文件中未定义的主机。

此外,您还可以使用-l--listfabfile中列出所有支持的任务,向fab工具提供fabfile名称。例如,将其应用到前面的脚本将生成以下输出:

# fab -f fabfile_first.py -l
Available commands:

    detect_host_type
    list_all_files_in_directory
    main_tasks

您可以使用-h开关在命令行中查看fab命令的所有可用选项和参数,或者在docs.fabfile.org/en/1.14/usage/fab.html上查看。

使用 Fabric 发现系统健康

在这种用例中,我们将利用 Fabric 开发一个脚本,在远程机器上执行多个命令。脚本的目标是收集两种类型的输出:discovery命令和health命令。discovery命令收集正常运行时间、主机名、内核版本以及私有和公共 IP 地址,而health命令收集已使用的内存、CPU 利用率、生成的进程数量和磁盘使用情况。我们将设计fabfile,以便我们可以扩展我们的脚本并向其中添加更多命令:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   from fabric.api import * from fabric.context_managers import * from pprint import pprint

env.hosts = [
  '10.10.10.140', # Ubuntu Machine
  '10.10.10.193', # CentOS Machine ]   env.user = "root" env.password = "access123"     def get_system_health():    discovery_commands = {
  "uptime": "uptime | awk '{print $3,$4}'",
  "hostname": "hostname",
  "kernel_release": "uname -r",
  "architecture": "uname -m",
  "internal_ip": "hostname -I",
  "external_ip": "curl -s ipecho.net/plain;echo",      }
  health_commands = {
  "used_memory": "free  | awk '{print $3}' | grep -v free | head -n1",
  "free_memory": "free  | awk '{print $4}' | grep -v shared | head -n1",
  "cpu_usr_percentage": "mpstat | grep -A 1 '%usr' | tail -n1 | awk '{print $4}'",
  "number_of_process": "ps -A --no-headers | wc -l",
  "logged_users": "who",
  "top_load_average": "top -n 1 -b | grep 'load average:' | awk '{print $10 $11 $12}'",
  "disk_usage": "df -h| egrep 'Filesystem|/dev/sda*|nvme*'"    }    tasks = [discovery_commands,health_commands]    for task in tasks:
  for operation,command in task.iteritems():
  print("============================={0}=============================".format(operation))
  output = run(command)

请注意,我们创建了两个字典:discover_commandshealth_commands。每个字典都包含 Linux 命令作为键值对。键表示操作,而值表示实际的 Linux 命令。然后,我们创建了一个tasks列表来组合这两个字典。

最后,我们创建了一个嵌套的for循环。外部循环用于遍历列表项。内部for循环用于遍历键值对。使用 Fabric 的run()操作将命令发送到远程主机:

# fab -f fabfile_discoveryAndHealth.py get_system_health
[10.10.10.140] Executing task 'get_system_health'
=============================uptime=============================
[10.10.10.140] run: uptime | awk '{print $3,$4}'
[10.10.10.140] out: 3:26, 2
[10.10.10.140] out: 

=============================kernel_release=============================
[10.10.10.140] run: uname -r
[10.10.10.140] out: 4.4.0-116-generic
[10.10.10.140] out: 

=============================external_ip=============================
[10.10.10.140] run: curl -s ipecho.net/plain;echo
[10.10.10.140] out: <Author_Masked_The_Output_For_Privacy>
[10.10.10.140] out: 

=============================hostname=============================
[10.10.10.140] run: hostname
[10.10.10.140] out: ubuntu-machine
[10.10.10.140] out: 

=============================internal_ip=============================
[10.10.10.140] run: hostname -I
[10.10.10.140] out: 10.10.10.140 
[10.10.10.140] out: 

=============================architecture=============================
[10.10.10.140] run: uname -m
[10.10.10.140] out: x86_64
[10.10.10.140] out: 

=============================disk_usage=============================
[10.10.10.140] run: df -h| egrep 'Filesystem|/dev/sda*|nvme*'
[10.10.10.140] out: Filesystem                            Size  Used Avail Use% Mounted on
[10.10.10.140] out: /dev/sda1                             472M   58M  390M  13% /boot
[10.10.10.140] out: 

=============================used_memory=============================
[10.10.10.140] run: free  | awk '{print $3}' | grep -v free | head -n1
[10.10.10.140] out: 75416
[10.10.10.140] out: 

=============================logged_users=============================
[10.10.10.140] run: who
[10.10.10.140] out: root     pts/0        2018-04-08 23:36 (10.10.10.130)
[10.10.10.140] out: root     pts/1        2018-04-08 21:23 (10.10.10.1)
[10.10.10.140] out: 

=============================top_load_average=============================
[10.10.10.140] run: top -n 1 -b | grep 'load average:' | awk '{print $10 $11 $12}'
[10.10.10.140] out: 0.16,0.03,0.01
[10.10.10.140] out: 

=============================cpu_usr_percentage=============================
[10.10.10.140] run: mpstat | grep -A 1 '%usr' | tail -n1 | awk '{print $4}'
[10.10.10.140] out: 0.02
[10.10.10.140] out: 

=============================number_of_process=============================
[10.10.10.140] run: ps -A --no-headers | wc -l
[10.10.10.140] out: 131
[10.10.10.140] out: 

=============================free_memory=============================
[10.10.10.140] run: free  | awk '{print $4}' | grep -v shared | head -n1
[10.10.10.140] out: 5869268
[10.10.10.140] out: 

get_system_health相同的任务也将在第二台服务器上执行,并将输出返回到终端:

[10.10.10.193] Executing task 'get_system_health'
=============================uptime=============================
[10.10.10.193] run: uptime | awk '{print $3,$4}'
[10.10.10.193] out: 3:26, 2
[10.10.10.193] out: 

=============================kernel_release=============================
[10.10.10.193] run: uname -r
[10.10.10.193] out: 3.10.0-693.el7.x86_64
[10.10.10.193] out: 

=============================external_ip=============================
[10.10.10.193] run: curl -s ipecho.net/plain;echo
[10.10.10.193] out: <Author_Masked_The_Output_For_Privacy>
[10.10.10.193] out: 

=============================hostname=============================
[10.10.10.193] run: hostname
[10.10.10.193] out: controller329
[10.10.10.193] out: 

=============================internal_ip=============================
[10.10.10.193] run: hostname -I
[10.10.10.193] out: 10.10.10.193 
[10.10.10.193] out: 

=============================architecture=============================
[10.10.10.193] run: uname -m
[10.10.10.193] out: x86_64
[10.10.10.193] out: 

=============================disk_usage=============================
[10.10.10.193] run: df -h| egrep 'Filesystem|/dev/sda*|nvme*'
[10.10.10.193] out: Filesystem               Size  Used Avail Use% Mounted on
[10.10.10.193] out: /dev/sda1                488M   93M  360M  21% /boot
[10.10.10.193] out: 

=============================used_memory=============================
[10.10.10.193] run: free  | awk '{print $3}' | grep -v free | head -n1
[10.10.10.193] out: 287048
[10.10.10.193] out: 

=============================logged_users=============================
[10.10.10.193] run: who
[10.10.10.193] out: root     pts/0        2018-04-08 23:36 (10.10.10.130)
[10.10.10.193] out: root     pts/1        2018-04-08 21:23 (10.10.10.1)
[10.10.10.193] out: 

=============================top_load_average=============================
[10.10.10.193] run: top -n 1 -b | grep 'load average:' | awk '{print $10 $11 $12}'
[10.10.10.193] out: 0.00,0.01,0.02
[10.10.10.193] out: 

=============================cpu_usr_percentage=============================
[10.10.10.193] run: mpstat | grep -A 1 '%usr' | tail -n1 | awk '{print $4}'
[10.10.10.193] out: 0.00
[10.10.10.193] out: 

=============================number_of_process=============================
[10.10.10.193] run: ps -A --no-headers | wc -l
[10.10.10.193] out: 190
[10.10.10.193] out: 

=============================free_memory=============================
[10.10.10.193] run: free  | awk '{print $4}' | grep -v shared | head -n1
[10.10.10.193] out: 32524912
[10.10.10.193] out: 

最后,fabric模块将在执行所有任务后终止已建立的 SSH 会话并断开与两台机器的连接:

Disconnecting from 10.10.10.140... done.
Disconnecting from 10.10.10.193... done.

请注意,我们可以重新设计之前的脚本,并将discovery_commandshealth_commands作为 Fabric 任务,然后将它们包含在get_system_health()中。当我们执行fab命令时,我们将调用get_system_health(),它将执行另外两个函数;我们将得到与之前相同的输出。以下是修改后的示例脚本:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   from fabric.api import * from fabric.context_managers import * from pprint import pprint

env.hosts = [
  '10.10.10.140', # Ubuntu Machine
  '10.10.10.193', # CentOS Machine ]   env.user = "root" env.password = "access123"     def discovery_commands():
  discovery_commands = {
  "uptime": "uptime | awk '{print $3,$4}'",
  "hostname": "hostname",
  "kernel_release": "uname -r",
  "architecture": "uname -m",
  "internal_ip": "hostname -I",
  "external_ip": "curl -s ipecho.net/plain;echo",      }
  for operation, command in discovery_commands.iteritems():
  print("============================={0}=============================".format(operation))
  output = run(command)   def health_commands():
  health_commands = {
  "used_memory": "free  | awk '{print $3}' | grep -v free | head -n1",
  "free_memory": "free  | awk '{print $4}' | grep -v shared | head -n1",
  "cpu_usr_percentage": "mpstat | grep -A 1 '%usr' | tail -n1 | awk '{print $4}'",
  "number_of_process": "ps -A --no-headers | wc -l",
  "logged_users": "who",
  "top_load_average": "top -n 1 -b | grep 'load average:' | awk '{print $10 $11 $12}'",
  "disk_usage": "df -h| egrep 'Filesystem|/dev/sda*|nvme*'"    }
  for operation, command in health_commands.iteritems():
  print("============================={0}=============================".format(operation))
  output = run(command)   def get_system_health():
  discovery_commands()
  health_commands()

Fabric 的其他有用功能

Fabric 还有其他有用的功能,如角色和上下文管理器。

Fabric 角色

Fabric 可以为主机定义角色,并仅对角色成员运行任务。例如,我们可能有一堆数据库服务器,需要验证 MySql 服务是否正常运行,以及其他需要验证 Apache 服务是否正常运行的 Web 服务器。我们可以将这些主机分组到角色中,并根据这些角色执行函数:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   from fabric.api import *   env.hosts = [
  '10.10.10.140', # ubuntu machine
  '10.10.10.193', # CentOS machine
  '10.10.10.130', ]   env.roledefs = {
  'webapps': ['10.10.10.140','10.10.10.193'],
  'databases': ['10.10.10.130'], }   env.user = "root" env.password = "access123"   @roles('databases') def validate_mysql():
  output = run("systemctl status mariadb")     @roles('webapps') def validate_apache():
  output = run("systemctl status httpd") 

在前面的示例中,我们在设置env.roledef时使用了 Fabric 装饰器roles(从fabric.api导入)。然后,我们将 webapp 或数据库角色分配给每个服务器(将角色分配视为对服务器进行标记)。这将使我们能够仅在具有数据库角色的服务器上执行validate_mysql函数:

# fab -f fabfile_roles.py validate_mysql:roles=databases
[10.10.10.130] Executing task 'validate_mysql'
[10.10.10.130] run: systemctl status mariadb
[10.10.10.130] out: ● mariadb.service - MariaDB database server
[10.10.10.130] out:    Loaded: loaded (/usr/lib/systemd/system/mariadb.service; enabled; vendor preset: disabled)
[10.10.10.130] out:    Active: active (running) since Sat 2018-04-07 19:47:35 EET; 1 day 2h ago
<output omitted>

Fabric 上下文管理器

在我们的第一个 Fabric 脚本fabfile_first.py中,我们有一个任务提示用户输入目录,然后切换到该目录并打印其内容。这是通过使用;来实现的,它将两个 Linux 命令连接在一起。但是,在其他操作系统上运行相同的命令并不总是有效。这就是 Fabric 上下文管理器发挥作用的地方。

上下文管理器在执行命令时维护目录状态。它通常通过with语句在 Python 中运行,并且在块内,您可以编写任何以前的 Fabric 操作。让我们通过一个示例来解释这个想法:

from fabric.api import *
from fabric.context_managers import *   env.hosts = [
  '10.10.10.140', # ubuntu machine
  '10.10.10.193', # CentOS machine ]   env.user = "root" env.password = "access123"   def list_directory():
  with cd("/var/log"):
  run("ls")

在前面的示例中,首先我们在fabric.context_managers中全局导入了所有内容;然后,我们使用cd上下文管理器切换到特定目录。我们使用 Fabric 的run()操作在该目录上执行ls。这与在 SSH 会话中编写cd /var/log ; ls相同,但它提供了一种更 Pythonic 的方式来开发您的代码。

with语句可以嵌套。例如,我们可以用以下方式重写前面的代码:

def list_directory_nested():
  with cd("/var/"):
  with cd("log"):
  run("ls")

另一个有用的上下文管理器是本地更改目录LCD)。这与前面示例中的cd上下文管理器相同,但它在运行fabfile的本地机器上工作。我们可以使用它来将上下文更改为特定目录(例如,上传或下载文件到/从远程机器,然后自动切换回执行目录):

def uploading_file():
  with lcd("/root/"):
  put("VeryImportantFile.txt")

prefix上下文管理器将接受一个命令作为输入,并在with块内的任何其他命令之前执行它。例如,您可以在运行每个命令之前执行源文件或 Python 虚拟env包装器脚本来设置您的虚拟环境:

def prefixing_commands():
  with prefix("source ~/env/bin/activate"):
  sudo('pip install wheel')
  sudo("pip install -r requirements.txt")
  sudo("python manage.py migrate")

实际上,这相当于在 Linux shell 中编写以下命令:

source ~/env/bin/activate && pip install wheel
source ~/env/bin/activate && pip install -r requirements.txt
source ~/env/bin/activate && python manage.py migrate

最后一个上下文管理器是shell_env(new_path, behavior='append'),它可以修改包装命令的 shell 环境变量;因此,在该块内的任何调用都将考虑到修改后的路径:

def change_shell_env():
  with shell_env(test1='val1', test2='val2', test3='val3'):
  run("echo $test1") #This command run on remote host
  run("echo $test2")
  run("echo $test3")
        local("echo $test1") #This command run on local host

请注意,在操作完成后,Fabric 将将旧的环境恢复到原始状态。

摘要

Fabric 是一个出色且强大的工具,可以自动化任务,通常在远程机器上执行。它与 Python 脚本很好地集成,可以轻松访问 SSH 套件。您可以为不同的任务开发许多 fab 文件,并将它们集成在一起,以创建包括部署、重启和停止服务器或进程在内的自动化工作流程。

在下一章中,我们将学习收集数据并为系统监控生成定期报告。

第十一章:生成系统报告和系统监控

收集数据并生成定期系统报告是任何系统管理员的重要任务,并自动化这些任务可以帮助我们及早发现问题,以便为其提供解决方案。在本章中,我们将看到一些经过验证的方法,用于从服务器自动收集数据并将这些数据生成为正式报告。我们将学习如何使用 Python 和 Ansible 管理新用户和现有用户。此外,我们还将深入研究日志分析和监控系统关键绩效指标KPI)。您可以安排监视脚本定期运行。

本章将涵盖以下主题:

  • 从 Linux 收集数据

  • 在 Ansible 中管理用户

从 Linux 收集数据

本机 Linux 命令提供有关当前系统状态和健康状况的有用数据。然而,这些 Linux 命令和实用程序都专注于从系统的一个方面获取数据。我们需要利用 Python 模块将这些详细信息返回给管理员并生成有用的系统报告。

我们将报告分为两部分。第一部分是使用platform模块获取系统的一般信息,而第二部分是探索 CPU 和内存等硬件资源。

我们将首先利用 Python 内置库中的platform模块。platform模块包含许多方法,可用于获取 Python 所在系统的详细信息:

import platform
system = platform.system() print(system)

在 Windows 机器上运行相同的脚本将产生不同的输出,反映当前的系统。因此,当我们在 Windows PC 上运行它时,我们将从脚本中获得Windows作为输出:

另一个有用的函数是uname(),它与 Linux 命令(uname -a)执行相同的工作:检索机器的主机名、架构和内核,但以结构化格式呈现,因此您可以通过引用其索引来匹配任何值:

import platform
from pprint import pprint
uname = platform.uname() pprint(uname)

第一个值是系统类型,我们使用system()方法获取,第二个值是当前机器的主机名。

您可以使用 PyCharm 中的自动完成功能来探索并列出platform模块中的所有可用函数;您可以通过按下CTRL + Q来检查每个函数的文档:

设计脚本的第二部分是使用 Linux 文件提供的信息来探索 Linux 机器中的硬件配置。请记住,CPU、内存和网络信息可以从/proc/下访问;我们将读取此信息并使用 Python 中的标准open()函数进行访问。您可以通过阅读和探索/proc/来获取有关可用资源的更多信息。

脚本:

这是导入platform模块的第一步。这仅适用于此任务:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import platform

此片段包含了此练习中使用的函数;我们将设计两个函数 - check_feature()get_value_from_string()

def check_feature(feature,string):
  if feature in string.lower():
  return True
  else:
  return False   def get_value_from_string(key,string):
  value = "NONE"
  for line in string.split("\n"):
  if key in line:
  value = line.split(":")[1].strip()
  return value

最后,以下是 Python 脚本的主体,其中包含获取所需信息的 Python 逻辑:

cpu_features = [] with open('/proc/cpuinfo') as cpus:
  cpu_data = cpus.read()
  num_of_cpus = cpu_data.count("processor")
  cpu_features.append("Number of Processors: {0}".format(num_of_cpus))
  one_processor_data = cpu_data.split("processor")[1]
 print one_processor_data
    if check_feature("vmx",one_processor_data):
  cpu_features.append("CPU Virtualization: enabled")
  if check_feature("cpu_meltdown",one_processor_data):
  cpu_features.append("Known Bugs: CPU Metldown ")
  model_name = get_value_from_string("model name ",one_processor_data)
  cpu_features.append("Model Name: {0}".format(model_name))    cpu_mhz = get_value_from_string("cpu MHz",one_processor_data)
  cpu_features.append("CPU MHz: {0}".format((cpu_mhz)))   memory_features = [] with open('/proc/meminfo') as memory:
  memory_data = memory.read()
  total_memory = get_value_from_string("MemTotal",memory_data).replace(" kB","")
  free_memory = get_value_from_string("MemFree",memory_data).replace(" kB","")
  swap_memory = get_value_from_string("SwapTotal",memory_data).replace(" kB","")
  total_memory_in_gb = "Total Memory in GB: {0}".format(int(total_memory)/1024)
  free_memory_in_gb = "Free Memory in GB: {0}".format(int(free_memory)/1024)
  swap_memory_in_gb = "SWAP Memory in GB: {0}".format(int(swap_memory)/1024)
  memory_features = [total_memory_in_gb,free_memory_in_gb,swap_memory_in_gb]  

此部分用于打印从上一部分获得的信息:

print("============System Information============")   print(""" System Type: {0} Hostname: {1} Kernel Version: {2} System Version: {3} Machine Architecture: {4} Python version: {5} """.format(platform.system(),
  platform.uname()[1],
  platform.uname()[2],
  platform.version(),
  platform.machine(),
  platform.python_version()))     print("============CPU Information============") print("\n".join(cpu_features))     print("============Memory Information============") print("\n".join(memory_features))

在上述示例中,执行了以下步骤:

  1. 首先,我们打开了/proc/cpuinfo并读取了它的内容,然后将结果存储在cpu_data中。

  2. 通过使用count()字符串函数计算文件中处理器的数量可以找到。

  3. 然后,我们需要获取每个处理器可用的选项和特性。为此,我们只获取了一个处理器条目(因为它们通常是相同的),并将其传递给check_feature()函数。该方法接受我们想要在一个参数中搜索的特性,另一个是处理器数据,如果处理器数据中存在该特性,则返回True

  4. 处理器数据以键值对的形式可用。因此,我们设计了get_value_from_string()方法,它接受键名,并通过迭代处理器数据来搜索其对应的值;然后,我们将在每个返回的键值对上使用:分隔符进行拆分,以获取值。

  5. 所有这些值都使用append()方法添加到cpu_feature列表中。

  6. 然后,我们重复了相同的操作,使用内存信息获取总内存、空闲内存和交换内存。

  7. 接下来,我们使用平台的内置方法,如system()uname()python_version(),来获取有关系统的信息。

  8. 最后,我们打印了包含上述信息的报告。

脚本输出如下截图所示:

表示生成的数据的另一种方法是利用我们在第五章中使用的matplotlib库,以便随时间可视化数据。

通过电子邮件发送生成的数据

在前一节生成的报告中,提供了系统当前资源的良好概述。但是,我们可以调整脚本并扩展其功能,以便通过电子邮件发送所有细节给我们。这对于网络运营中心NoC)团队非常有用,他们可以根据特定事件(硬盘故障、高 CPU 或丢包)从受监控系统接收电子邮件。Python 有一个名为smtplib的内置库,它利用简单邮件传输协议SMTP)负责与邮件服务器发送和接收电子邮件。

这要求您的计算机上有本地电子邮件服务器,或者您使用其中一个免费的在线电子邮件服务,如 Gmail 或 Outlook。在本例中,我们将使用 SMTP 登录到www.gmail.com,并使用我们的数据发送电子邮件。

话不多说,我们将修改我们的脚本,并为其添加 SMTP 支持。

我们将所需的模块导入 Python。同样,smtplibplatform对于这个任务是必需的:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import smtplib
imp        ort platform

这是函数的一部分,包含check_feature()get_value_from_string()函数:

def check_feature(feature,string):
  if feature in string.lower():
  return True
  else:
  return False   def get_value_from_string(key,string):
  value = "NONE"
  for line in string.split("\n"):
  if key in line:
  value = line.split(":")[1].strip()
  return value

最后,Python 脚本的主体如下,包含了获取所需信息的 Python 逻辑:

cpu_features = [] with open('/proc/cpuinfo') as cpus:
  cpu_data = cpus.read()
  num_of_cpus = cpu_data.count("processor")
  cpu_features.append("Number of Processors: {0}".format(num_of_cpus))
  one_processor_data = cpu_data.split("processor")[1]
 if check_feature("vmx",one_processor_data):
  cpu_features.append("CPU Virtualization: enabled")
  if check_feature("cpu_meltdown",one_processor_data):
  cpu_features.append("Known Bugs: CPU Metldown ")
  model_name = get_value_from_string("model name ",one_processor_data)
  cpu_features.append("Model Name: {0}".format(model_name))    cpu_mhz = get_value_from_string("cpu MHz",one_processor_data)
  cpu_features.append("CPU MHz: {0}".format((cpu_mhz)))   memory_features = [] with open('/proc/meminfo') as memory:
  memory_data = memory.read()
  total_memory = get_value_from_string("MemTotal",memory_data).replace(" kB","")
  free_memory = get_value_from_string("MemFree",memory_data).replace(" kB","")
  swap_memory = get_value_from_string("SwapTotal",memory_data).replace(" kB","")
  total_memory_in_gb = "Total Memory in GB: {0}".format(int(total_memory)/1024)
  free_memory_in_gb = "Free Memory in GB: {0}".format(int(free_memory)/1024)
  swap_memory_in_gb = "SWAP Memory in GB: {0}".format(int(swap_memory)/1024)
  memory_features = [total_memory_in_gb,free_memory_in_gb,swap_memory_in_gb]   Data_Sent_in_Email = "" Header = """From: PythonEnterpriseAutomationBot <basim.alyy@gmail.com> To: To Administrator <basim.alyy@gmail.com> Subject: Monitoring System Report   """ Data_Sent_in_Email += Header
Data_Sent_in_Email +="============System Information============"   Data_Sent_in_Email +=""" System Type: {0} Hostname: {1} Kernel Version: {2} System Version: {3} Machine Architecture: {4} Python version: {5} """.format(platform.system(),
  platform.uname()[1],
  platform.uname()[2],
  platform.version(),
  platform.machine(),
  platform.python_version())     Data_Sent_in_Email +="============CPU Information============\n" Data_Sent_in_Email +="\n".join(cpu_features)     Data_Sent_in_Email +="\n============Memory Information============\n" Data_Sent_in_Email +="\n".join(memory_features)  

最后,我们需要为变量赋一些值,以便正确连接到gmail服务器:

fromaddr = 'yyyyyyyyyyy@gmail.com' toaddrs  = 'basim.alyy@gmail.com' username = 'yyyyyyyyyyy@gmail.com' password = 'xxxxxxxxxx' server = smtplib.SMTP('smtp.gmail.com:587') server.ehlo() server.starttls() server.login(username,password)   server.sendmail(fromaddr, toaddrs, Data_Sent_in_Email) server.quit()

在前面的示例中,适用以下内容:

  1. 第一部分与原始示例相同,但是不是将数据打印到终端,而是将其添加到Data_Sent_in_Email变量中。

  2. Header变量代表包含发件人地址、收件人地址和电子邮件主题的电子邮件标题。

  3. 我们使用smtplib模块内的SMTP()类来连接到公共 Gmail SMTP 服务器并协商 TTLS 连接。这是连接到 Gmail 服务器时的默认方法。我们将 SMTP 连接保存在server变量中。

  4. 现在,我们使用login()方法登录到服务器,最后,我们使用sendmail()函数发送电子邮件。sendmail()接受三个参数:发件人、收件人和电子邮件正文。

  5. 最后,我们关闭与服务器的连接:

脚本输出

使用时间和日期模块

很好;到目前为止,我们已经能够通过电子邮件发送从我们的服务器生成的自定义数据。然而,由于网络拥塞或邮件系统故障等原因,生成的数据和电子邮件的传递时间之间可能存在时间差异。因此,我们不能依赖电子邮件将传递时间与实际事件时间相关联。

因此,我们将使用 Python 的datetime模块来跟踪监视系统上的当前时间。该模块可以以许多属性格式化时间,如年、月、日、小时和分钟。

除此之外,datetime模块中的datetime实例实际上是 Python 中的一个独立对象(如 int、string、boolean 等);因此,它在 Python 内部有自己的属性。

要将datetime对象转换为字符串,可以使用strftime()方法,该方法作为创建的对象内的属性可用。此外,它提供了一种通过以下指令格式化时间的方法:

指令 含义
%Y 返回年份,从 0001 到 9999
%m 返回月份
%d 返回月份的日期
%H 返回小时数,0-23
%M 返回分钟数,0-59
%S 返回秒数,0-59

因此,我们将调整我们的脚本,并将以下片段添加到代码中:

from datetime import datetime
time_now = datetime.now() time_now_string = time_now.strftime("%Y-%m-%d %H:%M:%S")
Data_Sent_in_Email += "====Time Now is {0}====\n".format(time_now_string) 

首先,我们从datetime模块中导入了datetime类。然后,我们使用datetime类和now()函数创建了time_now对象,该函数返回正在运行系统上的当前时间。最后,我们使用strftime(),带有一个指令,以特定格式格式化时间并将其转换为字符串以进行打印(记住,该对象有一个datetime对象)。

脚本的输出如下:

定期运行脚本

脚本的最后一步是安排脚本在一定时间间隔内运行。这可以是每天、每周、每小时或在特定时间。这可以通过 Linux 系统上的cron作业来完成。cron用于安排重复事件,如清理目录、备份数据库、旋转日志,或者你能想到的其他任何事情。

要查看当前计划的作业,使用以下命令:

crontab -l

编辑crontab,使用-e开关。如果这是你第一次运行cron,系统会提示你使用你喜欢的编辑器(nanovi)。

典型的crontab由五个星号组成,每个星号代表一个时间条目:

字段
分钟 0-59
小时 0-23
月份的日期 1-31
月份 1-12
星期几 0-6(星期日-星期六)

例如,如果你需要安排一个工作在每周五晚上 9 点运行,你将使用以下条目:

0 21 * * 5 /path/to/command

如果你需要每天凌晨 12 点执行一个命令(例如备份),使用以下cron作业:

0 0 * * * /path/to/command

此外,你可以安排cron每个特定的间隔运行。例如,如果你需要每5分钟运行一次作业,使用这个cron作业:

*/5 * * * * /path/to/command

回到我们的脚本;我们可以安排它在每天上午 7:30 运行:

30 7 * * * /usr/bin/python /root/Send_Email.py

最后,记得在退出之前保存cron作业。

最好提供 Linux 的完整命令路径,而不是相对路径,以避免任何潜在问题。

在 Ansible 中管理用户

现在,我们将讨论如何在不同系统中管理用户。

Linux 系统

Ansible 提供了强大的用户管理模块,用于管理系统上的不同任务。我们有一个专门讨论 Ansible 的章节(第十三章,系统管理的 Ansible),但在本章中,我们将探讨其在管理公司基础设施上管理用户帐户的能力。

有时,公司允许所有用户访问 root 权限,以摆脱用户管理的麻烦;这在安全和审计方面不是一个好的解决方案。最佳实践是给予正确的用户正确的权限,并在用户离开公司时撤销这些权限。

Ansible 提供了一种无与伦比的方式来管理多台服务器上的用户,可以通过密码或无密码(SSH 密钥)访问。

在创建 Linux 系统中的用户时,还有一些其他需要考虑的事项。用户必须有一个 shell(如 Bash、CSH、ZSH 等)才能登录到服务器。此外,用户应该有一个主目录(通常在/home下)。最后,用户必须属于一个确定其特权和权限的组。

我们的第一个示例将是在远程服务器上使用临时命令创建一个带有 SSH 密钥的用户。密钥源位于ansible tower,而我们在all服务器上执行命令:

ansible all -m copy -a "src=~/id_rsa dest=~/.ssh/id_rsa mode=0600"

第二个示例是使用 Playbook 创建用户:

--- - hosts: localhost
  tasks:
    - name: create a username
      user:
        name: bassem
        password: "$crypted_value$"
        groups:
          - root
        state: present
        shell: /bin/bash
        createhome: yes
  home: /home/bassem

让我们来看一下任务的参数:

  • 在我们的任务中,我们使用了一个包含多个参数的用户模块,比如name,用于设置用户的用户名。

  • 第二个参数是password,用于设置用户的密码,但是以加密格式。您需要使用mkpasswd命令,该命令会提示您输入密码并生成哈希值。

  • groups是用户所属的组列表;因此,用户将继承权限。您可以在此字段中使用逗号分隔的值。

  • state用于告诉 Ansible 用户是要创建还是删除。

  • 您可以在shell参数中定义用于远程访问的用户 shell。

  • createhomehome是用于指定用户主目录位置的参数。

另一个参数是ssh_key_file,用于指定 SSH 文件名。此外,ssh_key_passphrase将指定 SSH 密钥的密码。

微软 Windows

Ansible 提供了win_user模块来管理本地 Windows 用户帐户。在创建活动目录域或 Microsoft SQL 数据库(mssql)上的用户或在普通 PC 上创建默认帐户时,这非常有用。以下示例将创建一个名为bassem的用户,并为其设置密码access123。不同之处在于密码是以明文而不是加密值给出的,就像在基于 Unix 的系统中一样:

- hosts: localhost
  tasks:
    - name: create user on windows machine
      win_user:
        name: bassem
        password: 'access123'
  password_never_expires: true
  account_disabled: no
  account_locked: no
  password_expired: no
  state: present
        groups:
          - Administrators
          - Users

password_never_expires参数将防止 Windows 在特定时间后使密码过期;这在创建管理员和默认帐户时非常有用。另一方面,如果将password_expired设置为yes,将要求用户在首次登录时输入新密码并更改密码。

groups参数将用户添加到列出的值或逗号分隔的组列表中。这将取决于groups_action参数,可以是addreplaceremove

最后,状态将告诉 Ansible 应该对用户执行什么操作。此参数可以是presentabsentquery

总结

在本章中,我们学习了如何从 Linux 机器收集数据和报告,并使用时间和日期模块通过电子邮件进行警报。我们还学习了如何在 Ansible 中管理用户。

在下一章中,我们将学习如何使用 Python 连接器与 DBMS 进行交互。

第十二章:与数据库交互

在之前的章节中,我们使用了许多 Python 工具和实用程序生成了多种不同的报告。在本章中,我们将利用 Python 库连接到外部数据库,并提交我们生成的数据。然后,外部应用程序可以访问这些数据以获取信息。

Python 提供了广泛的库和模块,涵盖了管理和处理流行的数据库管理系统DBMSes),如 MySQL、PostgreSQL 和 Oracle。在本章中,我们将学习如何与 DBMS 交互,并填充我们自己的数据。

本章将涵盖以下主题:

  • 在自动化服务器上安装 MySQL

  • 从 Python 访问 MySQL 数据库

在自动化服务器上安装 MySQL

我们需要做的第一件事是设置一个数据库。在接下来的步骤中,我们将介绍如何在我们在第八章中创建的自动化服务器上安装 MySQL 数据库。基本上,您需要一个具有互联网连接的基于 Linux 的机器(CentOS 或 Ubuntu)来下载 SQL 软件包。MySQL 是一个使用关系数据库和 SQL 语言与数据交互的开源 DBMS。在 CentOS 7 中,MySQL 被另一个分支版本 MariaDB 取代;两者具有相同的源代码,但 MariaDB 中有一些增强功能。

按照以下步骤安装 MariaDB:

  1. 使用yum软件包管理器(或apt,在基于 Debian 的系统中)下载mariadb-server软件包,如下摘录所示:
yum install mariadb-server -y
  1. 安装完成后,启动mariadb守护程序。此外,我们需要使用systemd命令在操作系统启动时启用它:
systemctl enable mariadb ; systemctl start mariadb

Created symlink from /etc/systemd/system/multi-user.target.wants/mariadb.service to /usr/lib/systemd/system/mariadb.service.
  1. 通过运行以下命令验证数据库状态,并确保输出包含Active:active (running)
systemctl status mariadb

● mariadb.service - MariaDB database server
 Loaded: loaded (/usr/lib/systemd/system/mariadb.service; enabled; vendor preset: disabled)
 Active: active (running) since Sat 2018-04-07 19:47:35 EET; 1min 34s ago

保护安装

安装完成后的下一个逻辑步骤是保护它。MariaDB 包括一个安全脚本,可以更改 MySQL 配置文件中的选项,比如创建用于访问数据库的 root 密码和允许远程访问。运行以下命令启动脚本:

mysql_secure_installation

第一个提示要求您提供 root 密码。这个 root 密码不是 Linux 的 root 用户名,而是 MySQL 数据库的 root 密码;由于这是一个全新的安装,我们还没有设置它,所以我们将简单地按Enter进入下一步:

Enter current password for root (enter for none): <PRESS_ENTER>

脚本将建议为 root 设置密码。我们将通过按Y并输入新密码来接受建议:

Set root password? [Y/n] Y
New password:EnterpriseAutomation
Re-enter new password:EnterpriseAutomation
Password updated successfully!
Reloading privilege tables..
 ... Success! 

以下提示将建议删除匿名用户对数据库的管理和访问权限,这是强烈建议的:

Remove anonymous users? [Y/n] y
 ... Success!

您可以从远程机器向托管在自动化服务器上的数据库运行 SQL 命令;这需要您为 root 用户授予特殊权限,以便他们可以远程访问数据库:

Disallow root login remotely? [Y/n] n
 ... skipping.

最后,我们将删除任何人都可以访问的测试数据库,并重新加载权限表,以确保所有更改立即生效:

Remove test database and access to it? [Y/n] y
 - Dropping test database...
 ... Success!
 - Removing privileges on test database...
 ... Success!

Reload privilege tables now? [Y/n] y
 ... Success!

Cleaning up...

All done!  If you've completed all of the above steps, your MariaDB
installation should now be secure.

Thanks for using MariaDB!

我们已经完成了安装的保护;现在,让我们验证它。

验证数据库安装

在 MySQL 安装后的第一步是验证它。我们需要验证mysqld守护程序是否已启动并正在侦听端口3306。我们将通过运行netstat命令和在侦听端口上使用grep来做到这一点:

netstat -antup | grep -i 3306
tcp   0   0 0.0.0.0:3306      0.0.0.0:*         LISTEN      3094/mysqld

这意味着mysqld服务可以接受来自端口3306上的任何 IP 的传入连接。

如果您的机器上运行着iptables,您需要向INPUT链添加一个规则,以允许远程主机连接到 MySQL 数据库。还要验证SELINUX是否具有适当的策略。

第二次验证是通过使用mysqladmin实用程序连接到数据库。这个工具包含在 MySQL 客户端中,允许您在 MySQL 数据库上远程(或本地)执行命令:

mysqladmin -u root -p ping
Enter password:EnterpriseAutomation 
mysqld is alive
切换名称 含义
-u 指定用户名。
-p 使 MySQL 提示您输入用户名的密码。
ping 用于验证 MySQL 数据库是否存活的操作名称。

输出表明 MySQL 安装已成功完成,我们准备进行下一步。

从 Python 访问 MySQL 数据库

Python 开发人员创建了MySQLdb模块,该模块提供了一个工具,可以从 Python 脚本中与数据库进行交互和管理。可以使用 Python 的pip或操作系统包管理器(如yumapt)安装此模块。

要安装该软件包,请使用以下命令:

yum install MySQL-python

按以下方式验证安装:

[root@AutomationServer ~]# python
Python 2.7.5 (default, Aug  4 2017, 00:39:18) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import MySQLdb
>>> 

由于模块已经成功导入,我们知道 Python 模块已成功安装。

现在,通过控制台访问数据库,并创建一个名为TestingPython的简单数据库,其中包含一个表。然后我们将从 Python 连接到它:

[root@AutomationServer ~]# mysql -u root -p
Enter password: EnterpriseAutomation
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 12
Server version: 5.5.56-MariaDB MariaDB Server

Copyright (c) 2000, 2017, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE TestingPython;
Query OK, 1 row affected (0.00 sec)

在前述声明中,我们使用 MySQL 实用程序连接到数据库,然后使用 SQL 的CREATE命令创建一个空的新数据库。

您可以使用以下命令验证新创建的数据库:

MariaDB [(none)]> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| TestingPython      |
| mysql              |
| performance_schema |
+--------------------+
4 rows in set (0.00 sec)

在 SQL 命令中不一定要使用大写字母;但是,这是最佳实践,以便将它们与变量和其他操作区分开来。

我们需要切换到新的数据库:

MariaDB [(none)]> use TestingPython;
Database changed

现在,执行以下命令在数据库中创建一个新表:

MariaDB [TestingPython]> CREATE TABLE TestTable (id INT PRIMARY KEY, fName VARCHAR(30), lname VARCHAR(20), Title VARCHAR(10));
Query OK, 0 rows affected (0.00 sec)

在创建表时,应指定列类型。例如,fname是一个最大长度为 30 个字符的字符串,而id是一个整数。

验证表的创建如下:

MariaDB [TestingPython]> SHOW TABLES;
+-------------------------+
| Tables_in_TestingPython |
+-------------------------+
| TestTable               |
+-------------------------+
1 row in set (0.00 sec)

MariaDB [TestingPython]> describe TestTable;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | NO   | PRI | NULL    |       |
| fName | varchar(30) | YES  |     | NULL    |       |
| lname | varchar(20) | YES  |     | NULL    |       |
| Title | varchar(10) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
4 rows in set (0.00 sec)

查询数据库

此时,我们的数据库已准备好接受一些 Python 脚本。让我们创建一个新的 Python 文件,并提供数据库参数:

import MySQLdb
SQL_IP ="10.10.10.130" SQL_USERNAME="root" SQL_PASSWORD="EnterpriseAutomation" SQL_DB="TestingPython"   sql_connection = MySQLdb.connect(SQL_IP,SQL_USERNAME,SQL_PASSWORD,SQL_DB) print sql_connection

提供的参数(SQL_IPSQL_USERNAMESQL_PASSWORDSQL_DB)是建立连接并对端口3306上的数据库进行身份验证所需的。

以下表格列出了参数及其含义:

参数 含义
host 具有mysql安装的服务器 IP 地址。
user 具有对连接数据库的管理权限的用户名。
passwd 使用mysql_secure_installation脚本创建的密码。
db 数据库名称。

输出将如下所示:

<_mysql.connection open to '10.10.10.130' at 1cfd430>

返回的对象表明已成功打开到数据库的连接。让我们使用此对象创建用于执行实际命令的 SQL 游标:

cursor = sql_connection.cursor() cursor.execute("show tables")

您可以有许多与单个连接关联的游标,对一个游标的任何更改都会立即报告给其他游标,因为您已经打开了相同的连接。

游标有两个主要方法:execute()fetch*()

execute()方法用于向数据库发送命令并返回查询结果,而fetch*()方法有三种不同的用法:

方法名称 描述
fetchone() 从输出中获取一个记录,而不管返回的行数。
fetchmany(num) 返回方法内指定的记录数。
fetchall() 返回所有记录。

由于fetchall()是一个通用方法,可以获取一个记录或所有记录,我们将使用它:

output = cursor.fetchall()
print(output) # python mysql_simple.py
(('TestTable',),)

向数据库中插入记录

MySQLdb模块允许我们使用相同的游标操作将记录插入到数据库中。请记住,execute()方法可用于插入和查询。毫不犹豫,我们将稍微修改我们的脚本,并提供以下insert命令:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import MySQLdb

SQL_IP ="10.10.10.130" SQL_USERNAME="root" SQL_PASSWORD="EnterpriseAutomation" SQL_DB="TestingPython"   sql_connection = MySQLdb.connect(SQL_IP,SQL_USERNAME,SQL_PASSWORD,SQL_DB)   employee1 = {
  "id": 1,
  "fname": "Bassim",
  "lname": "Aly",
  "Title": "NW_ENG" }   employee2 = {
  "id": 2,
  "fname": "Ahmed",
  "lname": "Hany",
  "Title": "DEVELOPER" }   employee3 = {
  "id": 3,
  "fname": "Sara",
  "lname": "Mosaad",
  "Title": "QA_ENG" }   employee4 = {
  "id": 4,
  "fname": "Aly",
  "lname": "Mohamed",
  "Title": "PILOT" }   employees = [employee1,employee2,employee3,employee4]   cursor = sql_connection.cursor()   for record in employees:
  SQL_COMMAND = """INSERT INTO TestTable(id,fname,lname,Title) VALUES ({0},'{1}','{2}','{3}')""".format(record['id'],record['fname'],record['lname'],record['Title'])    print SQL_COMMAND
    try:
  cursor.execute(SQL_COMMAND)
  sql_connection.commit()
  except:
  sql_connection.rollback()   sql_connection.close()

在前面的例子中,以下内容适用:

  • 我们将四个员工记录定义为字典。每个记录都有idfnamelnametitle,并具有不同的值。

  • 然后,我们使用employees对它们进行分组,这是一个list类型的变量。

  • 创建一个for循环来迭代employees列表,在循环内部,我们格式化了insert SQL 命令,并使用execute()方法将数据推送到 SQL 数据库。请注意,在execute函数内部不需要在命令后添加分号(;),因为它会自动添加。

  • 在每次成功执行 SQL 命令后,将使用commit()操作来强制数据库引擎提交数据;否则,连接将被回滚。

  • 最后,使用close()函数来终止已建立的 SQL 连接。

关闭数据库连接意味着所有游标都被发送到 Python 垃圾收集器,并且将无法使用。还要注意,当关闭连接而不提交更改时,它会立即使数据库引擎回滚所有事务。

脚本的输出如下:

# python mysql_insert.py
INSERT INTO TestTable(id,fname,lname,Title) VALUES (1,'Bassim','Aly','NW_ENG')
INSERT INTO TestTable(id,fname,lname,Title) VALUES (2,'Ahmed','Hany','DEVELOPER')
INSERT INTO TestTable(id,fname,lname,Title) VALUES (3,'Sara','Mosad','QA_ENG')
INSERT INTO TestTable(id,fname,lname,Title) VALUES (4,'Aly','Mohamed','PILOT')

您可以通过 MySQL 控制台查询数据库,以验证数据是否已提交到数据库:

MariaDB [TestingPython]> select * from TestTable;
+----+--------+---------+-----------+
| id | fName  | lname   | Title     |
+----+--------+---------+-----------+
|  1 | Bassim | Aly     | NW_ENG    |
|  2 | Ahmed  | Hany    | DEVELOPER |
|  3 | Sara   | Mosaad  | QA_ENG    |
|  4 | Aly    | Mohamed | PILOT     |
+----+--------+---------+-----------+

现在,回到我们的 Python 代码,我们可以再次使用execute()函数;这次,我们使用它来选择在TestTable中插入的所有数据:

import MySQLdb

SQL_IP ="10.10.10.130" SQL_USERNAME="root" SQL_PASSWORD="EnterpriseAutomation" SQL_DB="TestingPython"   sql_connection = MySQLdb.connect(SQL_IP,SQL_USERNAME,SQL_PASSWORD,SQL_DB) # print sql_connection   cursor = sql_connection.cursor() cursor.execute("select * from TestTable")   output = cursor.fetchall() print(output)

脚本的输出如下:

python mysql_show_all.py 
((1L, 'Bassim', 'Aly', 'NW_ENG'), (2L, 'Ahmed', 'Hany', 'DEVELOPER'), (3L, 'Sara', 'Mosaa    d', 'QA_ENG'), (4L, 'Aly', 'Mohamed', 'PILOT'))

在上一个示例中,id值后的L字符可以通过再次将数据转换为整数(在 Python 中)来解决,使用int()函数。

游标内另一个有用的属性是.rowcount。这个属性将指示上一个.execute()方法返回了多少行。

总结

在本章中,我们学习了如何使用 Python 连接器与 DBMS 交互。我们在自动化服务器上安装了一个 MySQL 数据库,然后进行了验证。然后,我们使用 Python 脚本访问了 MySQL 数据库,并对其进行了操作。

在下一章中,我们将学习如何使用 Ansible 进行系统管理。

第十三章:系统管理的 Ansible

在本章中,我们将探索一种被成千上万的网络和系统工程师使用的流行自动化框架Ansible,Ansible 用于管理服务器和网络设备,通过多种传输协议如 SSH、Netconf 和 API 来提供可靠的基础设施。

我们首先将学习 ansible 中使用的术语,如何构建包含基础设施访问详细信息的清单文件,使用条件、循环和模板渲染等功能构建强大的 Ansible playbook。

Ansible 属于软件配置管理类别;它用于管理多个不同设备和服务器上的配置生命周期,确保所有设备上都应用相同的步骤,并帮助创建基础设施即代码(IaaC)环境。

本章将涵盖以下主题:

  • Ansible 及其术语

  • 在 Linux 上安装 Ansible

  • 在临时模式下使用 Ansible

  • 创建您的第一个 playbook

  • 理解 Ansible 的条件、处理程序和循环

  • 使用 Ansible 事实

  • 使用 Ansible 模板

Ansible 术语

Ansible 是一个自动化工具和完整的框架,它提供了基于 Python 工具的抽象层。最初,它是设计用来处理任务自动化的。这个任务可以在单个服务器上执行,也可以在成千上万的服务器上执行,ansible 都可以毫无问题地处理;后来,Ansible 的范围扩展到了网络设备和云提供商。Ansible 遵循“幂等性”的概念,其中 Ansible 指令可以多次运行相同的任务,并始终在所有设备上给出相同的配置,最终达到期望的状态,变化最小。例如,如果我们运行 Ansible 将文件上传到特定组的服务器,然后再次运行它,Ansible 将首先验证文件是否已经存在于远程目的地,如果存在,那么 ansible 就不会再次上传它。

再次。这个功能叫做“幂等性”。

Ansible 的另一个方面是它是无代理的。在运行任务之前,Ansible 不需要在服务器上安装任何代理。它利用 SSH 连接和 Python 标准库在远程服务器上执行任务,并将输出返回给 Ansible 服务器。此外,它不会创建数据库来存储远程机器信息,而是依赖于一个名为inventory的平面文本文件来存储所有所需的服务器信息,如 IP 地址、凭据和基础设施分类。以下是一个简单清单文件的示例:

[all:children] web-servers db-servers   [web-servers] web01 Ansible_ssh_host=192.168.10.10     [db-servers] db01 Ansible_ssh_host=192.168.10.11 db02 Ansible_ssh_host=192.168.10.12   [all:vars] Ansible_ssh_user=root Ansible_ssh_pass=access123   [db-servers:vars] Ansible_ssh_user=root Ansible_ssh_pass=access123   
[local] 127.0.0.1 Ansible_connection=local Ansible_python_interpreter="/usr/bin/python"

请注意,我们将在我们的基础设施中执行相同功能的服务器分组在一起(比如数据库服务器,在一个名为[db-servers]的组中;同样的,对于[web-servers]也是如此)。然后,我们定义一个特殊的组,称为[all],它结合了这两个组,以防我们有一个针对所有服务器的任务。

children关键字在[all:children]中的意思是组内的条目也是包含主机的组。

Ansible 的“临时”模式允许用户直接从终端向远程服务器执行任务。假设您想要在特定类型的服务器上更新特定的软件包,比如数据库或 Web 后端服务器,以解决一个新的 bug。与此同时,您不想要开发一个复杂的 playbook 来执行一个简单的任务。通过利用 Ansible 的临时模式,您可以在 Ansible 主机终端上输入命令来在远程服务器上执行任何命令。甚至一些模块也可以在终端上执行;我们将在“在临时模式下使用 Ansible”部分中看到这一点。

在 Linux 上安装 Ansible

Ansible 软件包在所有主要的 Linux 发行版上都可用。在本节中,我们将在 Ubuntu 和 CentOS 机器上安装它。在编写本书时使用的是 Ansible 2.5 版本,并且它支持 Python 2.6 和 Python 2.7。此外,从 2.2 版本开始,Ansible 为 Python 3.5+提供了技术预览。

在 RHEL 和 CentOS

在安装 Ansible 之前,您需要安装和启用 EPEL 存储库。要这样做,请使用以下命令:

sudo yum install epel-release

然后,按照以下命令安装 Ansible 软件包:

sudo yum install Ansible

Ubuntu

首先确保您的系统是最新的,并添加 Ansible 通道。最后,安装 Ansible 软件包本身,如下面的代码片段所示:

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

有关更多安装选项,请查看官方 Ansible 网站(docs.Ansible.com/Ansible/latest/installation_guide/intro_installation.html?#installing-the-control-machine)。

您可以通过运行Ansible --version来验证您的安装,以检查已安装的版本:

Ansible 配置文件通常存储在/etc/Ansible中,文件名为Ansible.cfg

在临时模式下使用 Ansible

当您需要在远程机器上执行简单操作而不创建复杂和持久的任务时,可以使用 Ansible 临时模式。这通常是用户在开始使用 Ansible 时首先使用的地方,然后再执行 playbook 中的高级任务。

执行临时命令需要两件事。首先,您需要清单文件中的主机或组;其次,您需要要执行的针对目标机器的 Ansible 模块:

  1. 首先,让我们定义我们的主机,并将 CentOS 和 Ubuntu 机器添加到一个单独的组中:
[all:children] centos-servers ubuntu-servers   [centos-servers] centos-machine01 Ansible_ssh_host=10.10.10.193   [ubuntu-servers] ubuntu-machine01 Ansible_ssh_host=10.10.10.140   [all:vars] Ansible_ssh_user=root Ansible_ssh_pass=access123   [centos-servers:vars] Ansible_ssh_user=root Ansible_ssh_pass=access123   [ubuntu-servers:vars] Ansible_ssh_user=root Ansible_ssh_pass=access123

[routers]
gateway ansible_ssh_host = 10.10.88.110 ansible_ssh_user=cisco ansible_ssh_pass=cisco   [local] 127.0.0.1 Ansible_connection=local Ansible_python_interpreter="/usr/bin/python"
  1. 将此文件保存为hosts,放在/root/或您的主目录中的AutomationServer下。

  2. 然后,使用ping模块运行Ansible命令:

# Ansible -i hosts all -m ping

-i参数将接受我们添加的清单文件,而-m参数将指定 Ansible 模块的名称。

运行命令后,您将得到以下输出,指示连接到远程机器失败:

ubuntu-machine01 | FAILED! => {
 "msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this.  Please add this host's fingerprint to your known_hosts file to manage this host."
}
centos-machine01 | FAILED! => {
 "msg": "Using a SSH password instead of a key is not possible because Host Key checking is enabled and sshpass does not support this.  Please add this host's fingerprint to your known_hosts file to manage this host."
}

这是因为远程机器不在 Ansible 服务器的known_hosts中;可以通过两种方法解决。

第一种方法是手动 SSH 到它们,这将将主机指纹添加到服务器。或者,您可以在 Ansible 配置中完全禁用主机密钥检查,如下面的代码片段所示:

sed -i -e 's/#host_key_checking = False/host_key_checking = False/g' /etc/Ansible/Ansible.cfg

sed -i -e 's/#   StrictHostKeyChecking ask/   StrictHostKeyChecking no/g' /etc/ssh/ssh_config

重新运行Ansible命令,您应该从三台机器中获得成功的输出:

127.0.0.1 | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}
ubuntu-machine01 | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}
centos-machine01 | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}

Ansible 中的ping模块不执行针对设备的 ICMP 操作。它实际上尝试使用提供的凭据通过 SSH 登录到设备;如果登录成功,它将返回pong关键字给 Ansible 主机。

另一个有用的模块是aptyum,用于管理 Ubuntu 或 CentOS 服务器上的软件包。以下示例将在 Ubuntu 机器上安装apache2软件包:

# Ansible -i hosts ubuntu-servers -m apt -a "name=apache2 state=present" 

apt模块中的状态可以有以下值:

状态 操作
absent 从系统中删除软件包。
present 确保软件包已安装在系统上。
latest 确保软件包是最新版本。

您可以通过运行Ansible-doc <module_name>来访问 Ansible 模块文档;您将看到模块的完整选项和示例。

service模块用于管理服务的操作和当前状态。您可以在state选项中将服务状态更改为startedrestartedstopped,ansible 将运行适当的命令来更改状态。同时,您可以通过配置enabled来配置服务是否在启动时启用或禁用。

#Ansible -i hosts centos-servers -m service -a "name=httpd state=stopped, enabled=no"

此外,您可以通过提供服务名称并将state设置为restarted来重新启动服务:

#Ansible -i hosts centos-servers -m service -a "name=mariadb state=restarted"

以 adhoc 模式运行 Ansible 的另一种方法是直接将命令传递给 Ansible,而不是使用内置模块,而是使用-a参数:

#Ansible -i hosts all -a "ifconfig"

您甚至可以通过运行reboot命令重新启动服务器;但这次,我们只会针对 CentOS 服务器运行它:

#Ansible -i hosts centos-servers -a "reboot"

有时,您需要使用不同的用户运行命令(或模块)。当您在具有分配给不同于 SSH 用户的特定权限的远程服务器上运行脚本时,这将非常有用。在这种情况下,我们将添加-u--become--ask-become-pass-K)开关。这将使 Ansible 使用提供的用户名运行命令,并提示您输入用户的密码:

#Ansible -i hosts ubuntu-servers --become-user bassim  --ask-become-pass -a "cat /etc/sudoers"

Ansible 的实际工作方式

Ansible 基本上是用 Python 编写的,但它使用自己的 DSL(领域特定语言)。您可以使用此 DSL 编写,ansible 将在远程机器上将其转换为 Python 以执行任务。因此,它首先验证任务语法并从 Ansible 主机复制模块到远程服务器,然后在远程服务器上执行它。

执行的结果以json格式返回到 Ansible 主机,因此您可以通过了解其键来匹配任何返回的值:

在安装了 Python 的网络设备的情况下,Ansible 使用 API 或netconf(如果网络设备支持,例如 Juniper 和 Cisco Nexus);或者,它只是使用 paramiko 的exec_command()函数执行命令,并将输出返回到 Ansible 主机。这可以通过使用raw模块来完成,如下面的代码片段所示:

# Ansible -i hosts routers -m raw -a "show arp" 
gateway | SUCCESS | rc=0 >>

Sat Apr 21 01:33:58.391 CAIRO

Address         Age        Hardware Addr   State      Type  Interface
85.54.41.9         -          45ea.2258.d0a9  Interface  ARPA  TenGigE0/2/0/0
10.88.18.1      -          d0b7.428b.2814  Satellite  ARPA  TenGigE0/2/0/0
192.168.100.1   -          00a7.5a3b.4193  Interface  ARPA  GigabitEthernet100/0/0/9
192.168.100.2   02:08:03   fc5b.3937.0b00  Dynamic    ARPA  \

创建您的第一个剧本

现在魔术派对可以开始了。Ansible 剧本是一组需要按顺序执行的命令(称为任务),它描述了执行完成后主机的期望状态。将剧本视为包含一组指令的手册,用于更改基础设施的状态;每个指令都依赖于许多内置的 Ansible 模块来执行任务。例如,您可能有一个用于构建 Web 应用程序的剧本,其中包括 SQL 服务器,用作后端数据库和 nginx Web 服务器。剧本将有一系列任务针对每组服务器执行,以将它们的状态从不存在更改为存在,或者更改为重新启动不存在,如果要删除 Web 应用程序。

剧本的强大之处在于您可以使用它在任何地方配置和设置基础设施。用于创建开发环境的相同过程将用于生产环境。剧本用于创建在您的基础设施上运行的自动化工作流程:

剧本是用 YAML 编写的,我们在第六章中讨论过,使用 Python 和 Jinja2 生成配置。剧本由多个 play 组成,针对清单文件中定义的一组主机执行。主机将被转换为 Python list,列表中的每个项目将被称为play。在前面的示例中,db-servers任务是一些 play,并且仅针对db-servers执行。在剧本执行期间,您可以决定运行文件中的所有 play,仅特定 play 或具有特定标记的任务,而不管它们属于哪个 play。

现在,让我们看看我们的第一个剧本,以了解其外观和感觉:

- hosts: centos-servers
  remote_user: root

  tasks:
    - name: Install openssh
      yum: pkg=openssh-server state=installed

    - name: Start the openssh
      service: name=sshd state=started enabled=yes

这是一个简单的剧本,有一个包含两个任务的play

  1. 安装openssh-server

  2. 安装后启动sshd服务,并确保它在启动时可用。

现在,我们需要将其应用于特定主机(或一组主机)。因此,我们将hosts设置为之前在 inventory 文件中定义的CentOS-servers,并且我们还将remote_user设置为 root,以确保之后的任务将以 root 权限执行。

任务将包括名称和 Ansible 模块。名称用于描述任务。为任务提供名称并不是强制性的,但建议这样做,以防需要从特定任务开始执行。

第二部分是 Ansible 模块,这是必需的。在我们的示例中,我们使用了核心模块yum来在目标服务器上安装openssh-server软件包。第二个任务具有相同的结构,但这次我们将使用另一个核心模块,称为service,来启动和启用sshd守护程序。

最后要注意 Ansible 中不同组件的缩进。例如,任务的名称应该在同一级别,而tasks应该与同一行上的hosts对齐。

让我们在我们的自动化服务器上运行 playbook 并检查输出:

#Ansible-playbook -i hosts first_playbook.yaml 

PLAY [centos-servers] **********************************************************************

TASK [Gathering Facts] *********************************************************************
ok: [centos-machine01]

TASK [Install openssh] *********************************************************************
ok: [centos-machine01]

TASK [Start the openssh] *******************************************************************
ok: [centos-machine01]

PLAY RECAP *********************************************************************************
centos-machine01           : ok=3    changed=0    unreachable=0    failed=0   

您可以看到 playbook 在centos-machine01上执行,并且任务按照 playbook 中定义的顺序依次执行。

YAML 要求保留缩进级别,并且不要混合制表符和空格;否则,将会出现错误。许多文本编辑器和 IDE 将制表符转换为一组空格。以下截图显示了该选项的示例,在 notepad++编辑器首选项中:

理解 Ansible 条件、处理程序和循环

在本章的这一部分,我们将看一些 Ansible playbook 中的高级功能。

设计条件

Ansible playbook 可以根据任务内部特定条件的结果执行任务(或跳过任务)——例如,当您想要在特定操作系统家族(Debian 或 CentOS)上安装软件包时,或者当操作系统是特定版本时,甚至当远程主机是虚拟机而不是裸机时。这可以通过在任务内部使用when子句来实现。

让我们增强先前的 playbook,并将openssh-server安装限制为仅适用于基于 CentOS 的系统,这样当它遇到使用apt模块而不是yum的 Ubuntu 服务器时,就不会出错。

首先,我们将在我们的inventory文件中添加以下两个部分,将 CentOS 和 Ubuntu 机器分组到infra部分中:

[infra:children] centos-servers ubuntu-servers     [infra:vars] Ansible_ssh_user=root Ansible_ssh_pass=access123 

然后,我们将重新设计 playbook 中的任务,添加when子句,将任务执行限制为仅适用于基于 CentOS 的机器。这应该读作如果远程机器是基于 CentOS 的,那么我将执行任务;否则,跳过

- hosts: infra
  remote_user: root

  tasks:
    - name: Install openssh
      yum: pkg=openssh-server state=installed
      when: Ansible_distribution == "CentOS"

    - name: Start the openssh
      service: name=sshd state=started enabled=yes
  when: Ansible_distribution == "CentOS"

让我们运行 playbook:

# Ansible-playbook -i hosts using_when.yaml 

PLAY [infra] *******************************************************************************

TASK [Gathering Facts] *********************************************************************
ok: [centos-machine01]
ok: [ubuntu-machine01]

TASK [Install openssh] *********************************************************************
skipping: [ubuntu-machine01]
ok: [centos-machine01]

TASK [Start the openssh] *******************************************************************
skipping: [ubuntu-machine01]
ok: [centos-machine01]

PLAY RECAP *********************************************************************************
centos-machine01           : ok=3    changed=0    unreachable=0    failed=0 
ubuntu-machine01           : ok=1    changed=0    unreachable=0    failed=0  

请注意,playbook 首先收集有关远程机器的信息(我们将在本章后面讨论),然后检查操作系统。当它遇到ubuntu-machine01时,任务将被跳过,并且在 CentOS 上将正常运行。

您还可以有多个条件需要满足才能运行任务。例如,您可以有以下 playbook,验证两件事情——首先,机器基于 Debian,其次,它是一个虚拟机,而不是裸机:

- hosts: infra
  remote_user: root

  tasks:
    - name: Install openssh
      apt: pkg=open-vm-tools state=installed
      when:
        - Ansible_distribution == "Debian"
        - Ansible_system_vendor == "VMware, Inc."

运行此 playbook 将产生以下输出:

# Ansible-playbook -i hosts using_when_1.yaml 

PLAY [infra] *******************************************************************************

TASK [Gathering Facts] *********************************************************************
ok: [centos-machine01]
ok: [ubuntu-machine01]

TASK [Install openssh] *********************************************************************
skipping: [centos-machine01]
ok: [ubuntu-machine01]

PLAY RECAP *********************************************************************************
centos-machine01           : ok=1    changed=0    unreachable=0    failed=0
ubuntu-machine01           : ok=2    changed=0    unreachable=0    failed=0 

Ansible 的when子句还接受表达式。例如,您可以检查返回的输出中是否存在特定关键字(使用注册标志保存),并根据此执行任务。

以下 playbook 将验证 OSPF 邻居状态。第一个任务将在路由器上执行show ip ospf neighbor并将输出注册到名为neighbors的变量中。接下来的任务将检查返回的输出中是否有EXSTARTEXCHANGE,如果找到,将在控制台上打印一条消息:

hosts: routers

tasks:
  - name: "show the ospf neighbor status"
    raw: show ip ospf neighbor
    register: neighbors

  - name: "Validate the Neighbors"
    debug:
      msg: "OSPF neighbors stuck"
    when: ('EXSTART' in neighbors.stdout) or ('EXCHANGE' in neigbnors.stdout)

您可以在docs.Ansible.com/Ansible/latest/user_guide/playbooks_conditionals.html#commonly-used-facts中检查在when子句中常用的事实。

在 ansible 中创建循环

Ansible 提供了许多重复在 play 中执行相同任务的方法,但每次都有不同的值。例如,当您想在服务器上安装多个软件包时,您不需要为每个软件包创建一个任务。相反,您可以创建一个任务,安装一个软件包并向任务提供软件包名称的列表,Ansible 将对它们进行迭代,直到完成安装。为此,我们需要在包含列表的任务内使用with_items标志,并使用变量{{ item }},它作为列表中项目的占位符。playbook 将利用with_items标志对一组软件包进行迭代,并将它们提供给yum模块,该模块需要软件包的名称和状态:

- hosts: infra
  remote_user: root

  tasks:
    - name: "Modifying Packages"
  yum: name={{ item.name }} state={{ item.state }}
  with_items:
        - { name: python-keyring-5.0-1.el7.noarch, state: absent }
  - { name: python-django, state: absent }
  - { name: python-django-bash-completion, state: absent }
  - { name: httpd, state: present }
  - { name: httpd-tools, state: present }
  - { name: python-qpid, state: present }
  when: Ansible_distribution == "CentOS"

您可以将状态的值硬编码为present;在这种情况下,所有的软件包都将被安装。然而,在前一种情况下,with_items将向yum模块提供两个元素。

playbook 的输出如下:

使用处理程序触发任务

好的;您已经在系统中安装和删除了一系列软件包。您已经将文件复制到/从服务器。并且您已经通过使用 Ansible playbook 在服务器上做了很多改变。现在,您需要重新启动一些其他服务,或者向文件中添加一些行,以完成服务的配置。所以,您应该添加一个新的任务,对吗?是的,这是正确的。然而,Ansible 提供了另一个很棒的选项,称为handlers,它不会在触发时自动执行(不像任务),而是只有在被调用时才会执行。这为您提供了灵活性,可以在 play 中的任务执行时调用它们。

处理程序与主机和任务具有相同的对齐方式,并位于每个 play 的底部。当您需要调用处理程序时,您可以在原始任务内使用notify标志,以确定将执行哪个处理程序;Ansible 将它们链接在一起。

让我们看一个例子。我们将编写一个 playbook,在 CentOS 服务器上安装和配置 KVM。KVM 在安装后需要进行一些更改,比如加载sysctl,启用kvm802.1q模块,并在boot时加载kvm

- hosts: centos-servers
  remote_user: root

  tasks:
    - name: "Install KVM"
  yum: name={{ item.name }} state={{ item.state }}
  with_items:
        - { name: qemu-kvm, state: installed }
  - { name: libvirt, state: installed }
  - { name: virt-install, state: installed }
  - { name: bridge-utils, state: installed }    notify:
        - load sysctl
        - load kvm at boot
        - enable kvm

  handlers:
    - name: load sysctl
      command: sysctl -p

    - name: enable kvm
      command: "{{ item.name }}"
      with_items:
        - {name: modprobe -a kvm}
  - {name: modprobe 8021q}
  - {name: udevadm trigger}    - name: load kvm at boot
      lineinfile: dest=/etc/modules state=present create=True line={{ item.name }}
  with_items:
        - {name: kvm}   

注意安装任务后使用notify。当任务运行时,它将按顺序通知三个处理程序,以便它们将被执行。处理程序将在任务成功执行后运行。这意味着如果任务未能运行(例如,找不到kvm软件包,或者没有互联网连接来下载它),则系统不会发生任何更改,kvm也不会被启用。

处理程序的另一个很棒的特性是,它只在任务中有更改时才运行。例如,如果您重新运行任务,Ansible 不会安装kvm软件包,因为它已经安装;它不会调用任何处理程序,因为它在系统中没有检测到任何更改。

我们将在最后关于两个模块添加一个注释:lineinfilecommand。第一个模块实际上是通过使用正则表达式向配置文件中插入或删除行;我们使用它来将kvm插入/etc/modules,以便在机器启动时自动启动 KVM。第二个模块command用于在设备上直接执行 shell 命令并将输出返回给 Ansible 主机。

使用 Ansible 事实

Ansible 不仅用于部署和配置远程主机。它可以用于收集有关它们的各种信息和事实。事实收集可能需要大量时间来从繁忙的系统中收集所有内容,但将为目标机器提供全面的视图。

收集到的事实可以在后续的 playbook 中使用,设计任务条件。例如,我们使用when子句将openssh安装限制为仅适用于基于 CentOS 的系统:

when: Ansible_distribution == "CentOS"

您可以通过在与主机和任务相同级别上配置gather_facts来在 Ansible plays 中启用/禁用事实收集。

- hosts: centos-servers
  gather_facts: yes
  tasks:
    <your tasks go here>

在 Ansible 中收集事实并打印它们的另一种方法是在 adhoc 模式中使用setup模块。返回的结果以嵌套的字典和列表的形式描述远程目标的事实,例如服务器架构、内存、网络设置、操作系统版本等:

#Ansible -i hosts ubuntu-servers -m setup | less 

您可以使用点表示法或方括号从事实中获取特定值。例如,要获取eth0的 IPv4 地址,可以使用Ansible_eth0["ipv4"]["address"]Ansible_eth0.ipv4.address

使用 Ansible 模板

与 Ansible 一起工作的最后一部分是了解它如何处理模板。Ansible 使用我们在第六章中讨论过的 Jinja2 模板,使用 Python 和 Jinja2 生成配置。它使用 Ansible 事实或在vars部分提供的静态值填充参数,甚至使用使用register标志存储的任务的结果。

在以下示例中,我们将构建一个 Ansible playbook,其中包含前面三个案例。首先,在vars部分中定义一个名为Header的变量,其中包含一个欢迎消息作为静态值。然后,我们启用gather_facts标志,以从目标机器获取所有可能的信息。最后,我们执行date命令,以获取服务器的当前日期并将输出存储在date_now变量中:

- hosts: centos-servers
  vars:
    - Header: "Welcome to Server facts page generated from Ansible playbook"
 gather_facts: yes  tasks:
    - name: Getting the current date
      command: date
      register: date_now
    - name: Setup webserver
      yum: pkg=nginx state=installed
      when: Ansible_distribution == "CentOS"

      notify:
        - enable the service
        - start the service

    - name: Copying the index page
      template: src=index.j2 dest=/usr/share/nginx/html/index.html

  handlers:
    - name: enable the service
      service: name=nginx enabled=yes    - name: start the service
      service: name=nginx state=started

在前面的 playbook 中使用的模板模块将接受一个名为index.j2的 Jinja2 文件,该文件位于 playbook 的同一目录中;然后,它将从我们之前讨论过的三个来源中提供所有 jinj2 变量的值。然后,渲染后的文件将存储在模板模块提供的dest选项中的路径中。

index.j2的内容如下。它将是一个简单的 HTML 页面,利用 jinja2 语言生成最终的 HTML 页面:

<html> <head><title>Hello world</title></head> <body>   <font size="6" color="green">{{ Header }}</font>   <br> <font size="5" color="#ff7f50">Facts about the server</font> <br> <b>Date Now is:</b> {{ date_now.stdout }}

<font size="4" color="#00008b"> <ul>
 <li>IPv4 Address: {{ Ansible_default_ipv4['address'] }}</li>
 <li>IPv4 gateway: {{ Ansible_default_ipv4['gateway'] }}</li>
 <li>Hostname: {{ Ansible_hostname }}</li>
 <li>Total Memory: {{ Ansible_memtotal_mb }}</li>
 <li>Operating System Family: {{ Ansible_os_family }}</li>
 <li>System Vendor: {{ Ansible_system_vendor }}</li> </ul> </font> </body> </html>

运行此 playbook 将在 CentOS 机器上安装 nginx web 服务器,并向其添加一个index.html页面。您可以通过浏览器访问该页面:

您还可以利用模板模块生成网络设备配置。在第六章中使用的 jinja2 模板,使用 Python 和 Jinja2 生成配置,为路由器生成了day0day1配置,可以在 Ansible playbook 中重复使用。

总结

Ansible 是一个非常强大的工具,用于自动化 IT 基础设施。它包含许多模块和库,几乎涵盖了系统和网络自动化中的所有内容,使软件部署、软件包管理和配置管理变得非常容易。虽然 Ansible 可以在 adhoc 模式下执行单个模块,但 Ansible 的真正力量在于编写和开发 playbook。

第十四章:创建和管理 VMware 虚拟机

很长一段时间以来,虚拟化一直是 IT 行业中的重要技术,因为它为硬件资源提供了高效的方式,并允许我们轻松地管理虚拟机VM)内的应用程序生命周期。2001 年,VMware 发布了 ESXi 的第一个版本,可以直接在现成的商用服务器COTS)上运行,并将其转换为可以被多个独立虚拟机使用的资源。在本章中,我们将探索许多可用于通过 Python 和 Ansible 自动构建虚拟机的选项。

本章将涵盖以下主题:

  • 设置实验室环境

  • 使用 Jinja2 生成 VMX 文件

  • VMware Python 客户端

  • 使用 Ansible Playbooks 管理实例

设置环境

在本章中,我们将在 Cisco UCS 服务器上安装 VMware ESXi 5.5,并托管一些虚拟机。我们需要在 ESXi 服务器中启用一些功能,以便将一些外部端口暴露给外部世界:

  1. 首先要启用 ESXi 控制台的 Shell 和 SSH 访问。基本上,ESXi 允许您使用 vSphere 客户端来管理它(基于 C#的 5.5.x 版本之前和基于 HTML 的 6 及更高版本)。一旦我们启用了 Shell 和 SSH 访问,这将使我们能够使用 CLI 来管理虚拟基础架构,并执行诸如创建、删除和自定义虚拟机等任务。

  2. 访问 ESXi vSphere 客户端,转到“配置”,然后从左侧选项卡中选择“安全配置文件”,最后点击“属性”:

将打开一个弹出窗口,其中包含服务、状态和各种可以应用的选项:

  1. 选择 SSH 服务,然后点击“选项”。将打开另一个弹出窗口。

  2. 在启动策略下选择第一个选项,即如果有任何端口打开则自动启动,并在所有端口关闭时停止。

  3. 此外,点击“服务命令”下的“启动”,然后点击“确定”:

再次为 ESXi Shell 服务重复相同的步骤。这将确保一旦 ESXi 服务器启动,两个服务都将启动,并且将打开并准备好接受连接。您可以测试两个服务,通过 SSH 连接到 ESXi IP 地址,并提供 root 凭据:

使用 Jinja2 生成 VMX 文件

虚拟机(有时称为客户机)的基本单元是 VMX 文件。该文件包含构建虚拟机所需的所有设置,包括计算资源、分配的内存、硬盘和网络。此外,它定义了在机器上运行的操作系统,因此 VMware 可以安装一些工具来管理 VM 的电源。

还需要一个额外的文件:VMDK。该文件存储 VM 的实际内容,并充当 VM 分区的硬盘:

这些文件(VMX 和 VMDK)应存储在 ESXi Shell 中的/vmfs/volumes/datastore1目录下,并且应该位于以虚拟机名称命名的目录中。

构建 VMX 模板

现在我们将创建模板文件,用于在 Python 中构建虚拟机。以下是我们需要使用 Python 和 Jinja2 生成的最终运行的 VMX 文件的示例:

.encoding = "UTF-8" vhv.enable = "TRUE" config.version = "8" virtualHW.version = "8"   vmci0.present = "TRUE" hpet0.present = "TRUE" displayName = "test_jinja2"   # Specs memSize = "4096" numvcpus = "1" cpuid.coresPerSocket = "1"     # HDD scsi0.present = "TRUE" scsi0.virtualDev = "lsilogic" scsi0:0.deviceType = "scsi-hardDisk" scsi0:0.fileName = "test_jinja2.vmdk" scsi0:0.present = "TRUE"   # Floppy floppy0.present = "false"   #  CDRom ide1:0.present = "TRUE" ide1:0.deviceType = "cdrom-image" ide1:0.fileName = "/vmfs/volumes/datastore1/ISO Room/CentOS-7-x86_64-Minimal-1708.iso"   #  Networking ethernet0.virtualDev = "e1000" ethernet0.networkName = "network1" ethernet0.addressType = "generated" ethernet0.present = "TRUE"   # VM Type guestOS = "ubuntu-64"   # VMware Tools toolScripts.afterPowerOn = "TRUE" toolScripts.afterResume = "TRUE" toolScripts.beforeSuspend = "TRUE" toolScripts.beforePowerOff = "TRUE" tools.remindInstall = "TRUE" tools.syncTime = "FALSE"

我在文件中添加了一些注释,以说明每个块的功能。但是,在实际文件中,您看不到这些注释。

让我们分析文件并理解一些字段的含义:

  • vhv.enable:当设置为True时,ESXi 服务器将向客户机 CPU 公开 CPU 主机标志,从而允许在客户机内运行 VM(称为嵌套虚拟化)。

  • displayName:在 ESXi 中注册的名称,并在 vSphere 客户端中显示的名称。

  • memsize:定义分配给 VM 的 RAM,应以兆字节为单位提供。

  • numvcpus:这定义了分配给 VM 的物理 CPU 数量。此标志与cpuid.coresPerSocket一起使用,因此可以定义分配的 vCPU 总数。

  • scsi0.virtualDev:虚拟硬盘的 SCSI 控制器类型。它可以是四个值之一:BusLogic,LSI Logic parallel,LSI Logic SAS 或 VMware paravirtual。

  • scsi0:0.fileName:这定义了将存储实际虚拟机设置的vmdk(在同一目录中)的名称。

  • ide1:0.fileName:包含以 ISO 格式打包的安装二进制文件的镜像路径。这将使 ESXi 连接到镜像 CD-ROM(IDE 设备)中的 ISO 镜像。

  • ethernet0.networkName:这是 ESXi 中应连接到 VM NIC 的虚拟交换机的名称。您可以添加此参数的其他实例,以反映其他网络接口。

现在我们将构建 Jinja2 模板;您可以查看第六章,使用 Python 和 Jinja2 进行配置生成,了解使用 Jinja2 语言进行模板化的基础知识:

.encoding = "UTF-8" vhv.enable = "TRUE" config.version = "8" virtualHW.version = "8"   vmci0.present = "TRUE" hpet0.present = "TRUE" displayName = "{{vm_name}}"   # Specs memSize = "{{ vm_memory_size }}" numvcpus = "{{ vm_cpu }}" cpuid.coresPerSocket = "{{cpu_per_socket}}"     # HDD scsi0.present = "TRUE" scsi0.virtualDev = "lsilogic" scsi0:0.deviceType = "scsi-hardDisk" scsi0:0.fileName = "{{vm_name}}.vmdk" scsi0:0.present = "TRUE"   # Floppy floppy0.present = "false"     # CDRom ide1:0.present = "TRUE" ide1:0.deviceType = "cdrom-image" ide1:0.fileName = "/vmfs/volumes/datastore1/ISO Room/{{vm_image}}"     # Networking ethernet0.virtualDev = "e1000" ethernet0.networkName = "{{vm_network1}}" ethernet0.addressType = "generated" ethernet0.present = "TRUE"   # VM Type guestOS = "{{vm_guest_os}}" #centos-64 or ubuntu-64   # VMware Tools toolScripts.afterPowerOn = "TRUE" toolScripts.afterResume = "TRUE" toolScripts.beforeSuspend = "TRUE" toolScripts.beforePowerOff = "TRUE" tools.remindInstall = "TRUE" tools.syncTime = "FALSE"

请注意,我们已经删除了相关字段的静态值,比如diplayNamememsize等,并用双大括号替换为变量名。在 Python 模板渲染期间,这些字段将被实际值替换,以构建有效的 VMX 文件。

现在,让我们构建渲染文件的 Python 脚本。通常,我们会将 YAML 数据序列化与 Jinja2 结合使用,以填充模板的数据。但是,由于我们已经在第六章中解释了 YAML 概念,使用 Python 和 Jinja2 进行配置生成,我们将从另一个数据源,Microsoft Excel 中获取数据:

处理 Microsoft Excel 数据

Python 有一些出色的库,可以处理 Excel 表中的数据。在第四章中,我们已经使用了 Excel 表,使用 Python 管理网络设备,当我们需要自动化netmiko配置并读取描述 Excel 文件基础设施的数据时。现在,我们将开始在自动化服务器中安装 Python xlrd库。

使用以下命令安装xlrd

pip install xlrd

按照以下步骤进行:

  1. XLRD 模块可以打开 Microsoft 工作簿,并使用open_workbook()方法解析内容。

  2. 然后,您可以通过将工作表索引或工作表名称提供给sheet_by_index()sheet_by_name()方法来选择包含数据的工作表。

  3. 最后,您可以通过将行号提供给row()函数来访问行数据,该函数将行数据转换为 Python 列表:

请注意,nrowsncols是特殊变量,一旦打开计算工作表中的行数和列数的工作表,它们将被填充。您可以使用for循环进行迭代。编号始终从开始。

回到虚拟机示例。在 Excel 表中,我们将有以下数据,反映虚拟机设置:

为了将数据读入 Python,我们将使用以下脚本:

import xlrd
workbook = xlrd.open_workbook(r"/media/bassim/DATA/GoogleDrive/Packt/EnterpriseAutomationProject/Chapter14_Creating_and_managing_VMware_virtual_machines/vm_inventory.xlsx") sheet = workbook.sheet_by_index(0) print(sheet.nrows) print(sheet.ncols)   print(int(sheet.row(1)[1].value))   for row in range(1,sheet.nrows):
  vm_name = sheet.row(row)[0].value
    vm_memory_size = int(sheet.row(row)[1].value)
  vm_cpu = int(sheet.row(row)[2].value)
  cpu_per_socket = int(sheet.row(row)[3].value)
  vm_hdd_size = int(sheet.row(row)[4].value)
  vm_guest_os = sheet.row(row)[5].value
    vm_network1 = sheet.row(row)[6].value

在前面的脚本中,我们做了以下事情:

  1. 我们导入了xlrd模块,并将 Excel 文件提供给open_workbook()方法以读取 Excel 工作表,并将其保存到workbook变量中。

  2. 然后,我们使用sheet_by_index()方法访问了第一个工作表,并将引用保存到sheet变量中。

  3. 现在,我们将遍历打开的表格,并使用row()方法获取每个字段。这将允许我们将行转换为 Python 列表。由于我们只需要行内的一个值,我们将使用列表切片来访问索引。请记住,列表索引始终从零开始。我们将把该值存储到变量中,并在下一部分中使用该变量来填充 Jinja2 模板。

生成 VMX 文件

最后一部分是从 Jinja2 模板生成 VMX 文件。我们将从 Excel 表中读取数据,并将其添加到空字典vmx_data中。稍后将该字典传递给 Jinja2 模板中的render()函数。Python 字典键将是模板变量名,而值将是应该在文件中替换的值。脚本的最后一部分是在vmx_files目录中以写入模式打开文件,并为每个 VMX 文件写入数据:

  from jinja2 import FileSystemLoader, Environment
import os
import xlrd

print("The script working directory is {}" .format(os.path.dirname(__file__))) script_dir = os.path.dirname(__file__)   vmx_env = Environment(
  loader=FileSystemLoader(script_dir),
  trim_blocks=True,
  lstrip_blocks= True )     workbook = xlrd.open_workbook(os.path.join(script_dir,"vm_inventory.xlsx")) sheet = workbook.sheet_by_index(0) print("The number of rows inside the Excel sheet is {}" .format(sheet.nrows)) print("The number of columns inside the Excel sheet is {}" .format(sheet.ncols))     vmx_data = {}   for row in range(1,sheet.nrows):
  vm_name = sheet.row(row)[0].value
    vm_memory_size = int(sheet.row(row)[1].value)
  vm_cpu = int(sheet.row(row)[2].value)
  cpu_per_socket = int(sheet.row(row)[3].value)
  vm_hdd_size = int(sheet.row(row)[4].value)
  vm_guest_os = sheet.row(row)[5].value
    vm_network1 = sheet.row(row)[6].value

    vmx_data["vm_name"] = vm_name
    vmx_data["vm_memory_size"] = vm_memory_size
    vmx_data["vm_cpu"] = vm_cpu
    vmx_data["cpu_per_socket"] = cpu_per_socket
    vmx_data["vm_hdd_size"] = vm_hdd_size
    vmx_data["vm_guest_os"] = vm_guest_os
    if vm_guest_os == "ubuntu-64":
  vmx_data["vm_image"] = "ubuntu-16.04.4-server-amd64.iso"    elif vm_guest_os == "centos-64":
  vmx_data["vm_image"] = "CentOS-7-x86_64-Minimal-1708.iso"    elif vm_guest_os == "windows7-64":
  vmx_data["vm_image"] = "windows_7_ultimate_sp1_ x86-x64_bg-en_IE10_ April_2013.iso"    vmx_data["vm_network1"] = vm_network1

    vmx_data = vmx_env.get_template("vmx_template.j2").render(vmx_data)
  with open(os.path.join(script_dir,"vmx_files/{}.vmx".format(vm_name)), "w") as f:
  print("Writing Data of {} into directory".format(vm_name))
  f.write(vmx_data)
  vmx_data = {} 

脚本输出如下:

文件存储在vmx_files下,每个文件都包含在 Excel 表中配置的虚拟机的特定信息:

现在,我们将使用paramikoscp库连接到 ESXi Shell,并将这些文件上传到/vmfs/volumes/datastore1下。为了实现这一点,我们将首先创建一个名为upload_and_create_directory()的函数,该函数接受vm namehard disk size和 VMXsource fileparamiko将连接到 ESXi 服务器并执行所需的命令,这将在/vmfs/volumes/datastore1下创建目录和 VMDK。最后,我们将使用scp模块中的SCPClient将源文件上传到先前创建的目录,并运行注册命令将机器添加到 vSphere 客户端:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import paramiko
from scp import SCPClient
import time

def upload_and_create_directory(vm_name, hdd_size, source_file):    commands = ["mkdir /vmfs/volumes/datastore1/{0}".format(vm_name),
  "vmkfstools -c {0}g -a lsilogic -d zeroedthick /vmfs/volumes/datastore1/{1}/{1}.vmdk".format(hdd_size,
  vm_name),]
  register_command = "vim-cmd solo/registervm /vmfs/volumes/datastore1/{0}/{0}.vmx".format(vm_name)
  ipaddr = "10.10.10.115"
  username = "root"
  password = "access123"    ssh = paramiko.SSHClient()
  ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())    ssh.connect(ipaddr, username=username, password=password, look_for_keys=False, allow_agent=False)    for cmd in commands:
  try:
  stdin, stdout, stderr = ssh.exec_command(cmd)
  print " DEBUG: ... Executing the command on ESXi server".format(str(stdout.readlines()))    except Exception as e:
  print e
            pass
 print " DEBUG: **ERR....unable to execute command"
  time.sleep(2)
  with SCPClient(ssh.get_transport()) as scp:
  scp.put(source_file, remote_path='/vmfs/volumes/datastore1/{0}'.format(vm_name))
  ssh.exec_command(register_command)
  ssh.close()

我们需要在运行 Jinja2 模板并生成 VMX 之前定义这个函数之前,并在将文件保存到vmx_files目录并传递所需的参数后调用该函数。

最终代码应该如下所示:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import paramiko
from scp import SCPClient
import time
from jinja2 import FileSystemLoader, Environment
import os
import xlrd

def upload_and_create_directory(vm_name, hdd_size, source_file):    commands = ["mkdir /vmfs/volumes/datastore1/{0}".format(vm_name),
  "vmkfstools -c {0}g -a lsilogic -d zeroedthick /vmfs/volumes/datastore1/{1}/{1}.vmdk".format(hdd_size,
  vm_name),]
  register_command = "vim-cmd solo/registervm /vmfs/volumes/datastore1/{0}/{0}.vmx".format(vm_name)    ipaddr = "10.10.10.115"
  username = "root"
  password = "access123"    ssh = paramiko.SSHClient()
  ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())    ssh.connect(ipaddr, username=username, password=password, look_for_keys=False, allow_agent=False)    for cmd in commands:
  try:
  stdin, stdout, stderr = ssh.exec_command(cmd)
  print " DEBUG: ... Executing the command on ESXi server".format(str(stdout.readlines()))    except Exception as e:
  print e
            pass
 print " DEBUG: **ERR....unable to execute command"
  time.sleep(2)
  with SCPClient(ssh.get_transport()) as scp:
  print(" DEBUG: ... Uploading file to the datastore")
  scp.put(source_file, remote_path='/vmfs/volumes/datastore1/{0}'.format(vm_name))
  print(" DEBUG: ... Register the virtual machine {}".format(vm_name))
  ssh.exec_command(register_command)    ssh.close()   print("The script working directory is {}" .format(os.path.dirname(__file__))) script_dir = os.path.dirname(__file__)   vmx_env = Environment(
  loader=FileSystemLoader(script_dir),
  trim_blocks=True,
  lstrip_blocks= True )   workbook = xlrd.open_workbook(os.path.join(script_dir,"vm_inventory.xlsx")) sheet = workbook.sheet_by_index(0) print("The number of rows inside the Excel sheet is {}" .format(sheet.nrows)) print("The number of columns inside the Excel sheet is {}" .format(sheet.ncols))     vmx_data = {}   for row in range(1,sheet.nrows):
  vm_name = sheet.row(row)[0].value
    vm_memory_size = int(sheet.row(row)[1].value)
  vm_cpu = int(sheet.row(row)[2].value)
  cpu_per_socket = int(sheet.row(row)[3].value)
  vm_hdd_size = int(sheet.row(row)[4].value)
  vm_guest_os = sheet.row(row)[5].value
    vm_network1 = sheet.row(row)[6].value

    vmx_data["vm_name"] = vm_name
    vmx_data["vm_memory_size"] = vm_memory_size
    vmx_data["vm_cpu"] = vm_cpu
    vmx_data["cpu_per_socket"] = cpu_per_socket
    vmx_data["vm_hdd_size"] = vm_hdd_size
    vmx_data["vm_guest_os"] = vm_guest_os
    if vm_guest_os == "ubuntu-64":
  vmx_data["vm_image"] = "ubuntu-16.04.4-server-amd64.iso"    elif vm_guest_os == "centos-64":
  vmx_data["vm_image"] = "CentOS-7-x86_64-Minimal-1708.iso"    elif vm_guest_os == "windows7-64":
  vmx_data["vm_image"] = "windows_7_ultimate_sp1_ x86-x64_bg-en_IE10_ April_2013.iso"    vmx_data["vm_network1"] = vm_network1

    vmx_data = vmx_env.get_template("vmx_template.j2").render(vmx_data)
  with open(os.path.join(script_dir,"vmx_files/{}.vmx".format(vm_name)), "w") as f:
  print("Writing Data of {} into directory".format(vm_name))
  f.write(vmx_data)
  print(" DEBUG:Communicating with ESXi server to upload and register the VM")
  upload_and_create_directory(vm_name,
  vm_hdd_size,
  os.path.join(script_dir,"vmx_files","{}.vmx".format(vm_name)))
  vmx_data = {}

脚本输出如下:

如果在运行脚本后检查 vSphere 客户端,您会发现已经创建了四台机器,这些机器的名称是在 Excel 表中提供的:

此外,您会发现虚拟机已经定制了诸如 CPU、内存和连接的 ISO 室等设置:

您可以通过将创建的虚拟机连接到 Cobbler 来在 VMware 中完成自动化工作流程。我们在第八章中介绍了这一点,准备系统实验环境。Cobbler 将自动化操作系统安装和定制,无论是 Windows、CentOS 还是 Ubuntu。之后,您可以使用我们在第十三章中介绍的 Ansible,系统管理的 Ansible,来准备系统的安全性、配置和已安装的软件包,然后部署您的应用程序。这是一个全栈自动化,涵盖了虚拟机创建和应用程序的运行。

VMware Python 客户端

VMware 产品(用于管理 ESXi 的 ESXi 和 vCenter)支持通过 Web 服务接收外部 API 请求。您可以执行与 vSphere 客户端上相同的管理任务,例如创建新的虚拟机、创建新的 vSwitch,甚至控制vm状态,但这次是通过支持的 API 进行的,该 API 具有许多语言的绑定,例如 Python、Ruby 和 Go。

vSphere 具有库存的特殊模型,其中的每个对象都具有特定的值。您可以访问此模型,并通过托管对象浏览器MoB)查看基础设施的实际值。我们将使用 VMware 的官方 Python 绑定(pyvmomi)与此模型进行交互,并更改库存中的值(或创建它们)。

值得注意的是,可以通过浏览器访问 MoB,方法是转到http://<ESXi_server_ip_or_domain>/mob,这将要求您提供根用户名和密码:

您可以单击任何超链接以查看更多详细信息并访问每个树或上下文中的每个。例如,单击 Content.about 以查看有关服务器的完整详细信息,例如确切的版本、构建和完整名称:

注意表的结构。第一列包含属性名称,第二列是该属性的数据类型,最后,第三列是实际运行值。

安装 PyVmomi

PyVmomi 可以通过 Python pip或不同仓库的系统包进行下载。

对于 Python 安装,请使用以下命令:

pip install -U pyvmomi

请注意,从pip下载的版本是6.5.2017.5-1,与 vSphere 发布的 VMware vSphere 6.5 相对应,但这并不意味着它不能与旧版本的 ESXi 一起使用。例如,我有 VMware vSphere 5.5,它可以与最新的pyvmomi版本完美配合使用。

系统安装:

yum install pyvmomi -y

Pyvmomi 库使用动态类型,这意味着 IDE 中的智能感知和自动完成功能等功能无法与其一起使用。您必须依赖文档和 MoB 来发现需要哪些类或方法来完成工作,但是一旦您发现它的工作原理,就会很容易使用。

使用 pyvmomi 的第一步

首先,您需要通过提供用户名、密码和主机 IP 连接到 ESXi MoB,并开始导航到 MoB 以获取所需的数据。这可以通过使用SmartConnectNoSSL()方法来完成:

from pyVim.connect import SmartConnect, Disconnect,SmartConnectNoSSL  ESXi_connection = SmartConnectNoSSL(host="10.10.10.115", user="root", pwd='access123')

请注意,还有另一种称为SmartConnect()的方法,当建立连接时必须向其提供 SSL 上下文,否则连接将失败。但是,您可以使用以下代码片段请求 SSL 不验证证书,并将此上下文传递给SmartConnect()sslCContext参数:

import ssl import requests certificate = ssl.SSLContext(ssl.PROTOCOL_TLSv1) certificate.verify_mode = ssl.CERT_NONE
requests.packages.urllib3.disable_warnings()    

为了严谨性和保持代码简洁,我们将使用内置的SmartConnectNoSSL()

接下来,我们将开始探索 MoB 并在about对象中获取服务器的完整名称和版本。请记住,它位于content对象下,所以我们也需要访问它:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   from pyVim.connect import SmartConnect, Disconnect,SmartConnectNoSSL
ESXi_connection = SmartConnectNoSSL(host="10.10.10.115", user="root", pwd='access123')   full_name = ESXi_connection.content.about.fullName
version = ESXi_connection.content.about.version
print("Server Full name is {}".format(full_name)) print("ESXi version is {}".format(version))
Disconnect(ESXi_connection) 

输出如下:

现在我们了解了 API 的工作原理。让我们进入一些严肃的脚本,并检索有关我们 ESXi 中部署的虚拟机的一些详细信息。

脚本如下:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   from pyVim.connect import SmartConnect, Disconnect,SmartConnectNoSSL

ESXi_connection = SmartConnectNoSSL(host="10.10.10.115", user="root", pwd='access123')   datacenter = ESXi_connection.content.rootFolder.childEntity[0] #First Datacenter in the ESXi\   virtual_machines = datacenter.vmFolder.childEntity #Access the child inside the vmFolder   print virtual_machines

for machine in virtual_machines:
  print(machine.name)
  try:
  guest_vcpu = machine.summary.config.numCpu
        print("  The Guest vCPU is {}" .format(guest_vcpu))    guest_os = machine.summary.config.guestFullName
        print("  The Guest Operating System is {}" .format(guest_os))    guest_mem = machine.summary.config.memorySizeMB
        print("  The Guest Memory is {}" .format(guest_mem))    ipadd = machine.summary.guest.ipAddress
        print("  The Guest IP Address is {}" .format(ipadd))
  print "================================="
  except:
  print("  Can't get the summary")

在前面的示例中,我们做了以下事情:

  1. 我们再次通过向SmartConnectNoSSL方法提供 ESXi/vCenter 凭据来建立 API 连接到 MoB。

  2. 然后,我们通过访问content然后rootFolder对象和最后childEntity来访问数据中心对象。返回的对象是可迭代的,因此我们访问了第一个元素(实验室中只有一个 ESXi 的第一个数据中心)。您可以遍历所有数据中心以获取所有注册数据中心中所有虚拟机的列表。

  3. 虚拟机可以通过vmFolderchildEntity访问。同样,请记住返回的输出是可迭代的,并表示存储在virtual_machines变量中的虚拟机列表:

  1. 我们遍历了virtual_machines对象,并查询了每个元素(每个虚拟机)的 CPU、内存、全名和 IP 地址。这些元素位于summaryconfig叶子下的每个虚拟机树中。以下是我们的AutomationServer设置的示例:

脚本输出如下:

请注意,在本章的开头我们创建的python-vm虚拟机在最后一个截图中显示出来。您可以使用 PyVmomi 作为验证工具,与您的自动化工作流集成,验证虚拟机是否正在运行,并根据返回的输出做出决策。

更改虚拟机状态

这次我们将使用pyvmomi绑定来更改虚拟机状态。这将通过检查虚拟机名称来完成;然后,我们将导航到 MoB 中的另一个树,并获取运行时状态。最后,我们将根据其当前状态在机器上应用PowerOn()PowerOff()函数。这将把机器状态从On切换到Off,反之亦然。

脚本如下:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   from pyVim.connect import SmartConnect, Disconnect,SmartConnectNoSSL

ESXi_connection = SmartConnectNoSSL(host="10.10.10.115", user="root", pwd='access123')   datacenter = ESXi_connection.content.rootFolder.childEntity[0] #First Datacenter in the ESXi\   virtual_machines = datacenter.vmFolder.childEntity #Access the child inside the vmFolder   for machine in virtual_machines:
  try:
  powerstate = machine.summary.runtime.powerState
        if "python-vm" in machine.name and powerstate == "poweredOff":
  print(machine.name)
  print("     The Guest Power state is {}".format(powerstate))
  machine.PowerOn()
  print("**Powered On the virtual machine**")    elif "python-vm" in machine.name and powerstate == "poweredOn":
  print(machine.name)
  print("     The Guest Power state is {}".format(powerstate))
  machine.PowerOff()
  print("**Powered Off the virtual machine**")
  except:
  print("  Can't execute the task")   Disconnect(ESXi_connection)

脚本输出如下:

此外,您还可以从 vSphere 客户端验证虚拟机状态,并检查以python-vm*开头的主机,将它们的电源状态从poweredOff更改为poweredOn

还有更多

您可以在 GitHub 的官方 VMware 存储库中找到基于pyvmomi绑定的许多有用的脚本(使用不同的语言)(github.com/vmware/pyvmomi-community-samples/tree/master/samples)。这些脚本由许多使用工具并在日常基础上测试它们的贡献者提供。大多数脚本提供了输入您的配置(如 ESXi IP 地址和凭据)的空间,而无需通过提供它作为参数修改脚本源代码。

使用 Ansible Playbook 管理实例

在 VMware 自动化的最后部分,我们将利用 Ansible 工具来管理 VMware 基础架构。Ansible 附带了 20 多个 VMware 模块(docs.ansible.com/ansible/latest/modules/list_of_cloud_modules.html#vmware),可以执行许多任务,如管理数据中心、集群和虚拟机。在较旧的 Ansible 版本中,Ansible 使用pysphere模块(这不是官方的;模块的作者自 2013 年以来就没有维护过它)来自动化任务。然而,新版本现在支持pyvmomi绑定。

Ansible 还支持 VMware SDN 产品(NSX)。Ansible Tower 可以从VMware vRealize AutomationvRA)访问,允许在不同工具之间实现完整的工作流集成。

以下是 Ansible Playbook:

- name: Provision New VM
  hosts: localhost
  connection: local
  vars:
  - VM_NAME: DevOps
  - ESXi_HOST: 10.10.10.115
  - USERNAME: root
  - PASSWORD: access123
  tasks:   - name: current time
  command: date +%D
  register: current_time
  - name: Check for vSphere access parameters
  fail: msg="Must set vsphere_login and vsphere_password in a Vault"
  when: (USERNAME is not defined) or (PASSWORD is not defined)
  - name: debug vCenter hostname
  debug: msg="vcenter_hostname = '{{ ESXi_HOST }}'"
  - name: debug the time
  debug: msg="Time is = '{{ current_time }}'"    - name: "Provision the VM"
  vmware_guest:
 hostname: "{{ ESXi_HOST }}"
  username: "{{ USERNAME }}"
  password: "{{ PASSWORD }}"
  datacenter: ha-datacenter
  validate_certs: False
  name: "{{ VM_NAME }}"
  folder: /
  guest_id: centos64Guest
  state: poweredon
  force: yes
  disk:
  - size_gb: 100
  type: thin
  datastore: datastore1    networks:
  - name: network1
  device_type: e1000 #            mac: ba:ba:ba:ba:01:02 #            wake_on_lan: True    - name: network2
  device_type: e1000    hardware:
 memory_mb: 4096
  num_cpus: 4
  num_cpu_cores_per_socket: 2
  hotadd_cpu: True
  hotremove_cpu: True
  hotadd_memory: True
  scsi: lsilogic
  cdrom:
 type: "iso"
  iso_path: "[datastore1] ISO Room/CentOS-7-x86_64-Minimal-1708.iso"
  register: result 

在前面的 playbook 中,我们可以看到以下内容:

  • playbook 的第一部分是在vars部分中定义 ESXi 主机 IP 和凭据,并在后续任务中使用它们。

  • 然后我们编写了一个简单的验证,如果未提供用户名或密码,就会使 playbook 失败。

  • 然后,我们使用 ansible 提供的vmware_guest模块(docs.ansible.com/ansible/2.4/vmware_guest_module.html)来提供虚拟机。在这个任务中,我们提供了所需的信息,比如磁盘大小和 CPU 和内存方面的硬件。请注意,我们将虚拟机的状态定义为poweredon,因此 ansible 将在创建后启动虚拟机。

  • 磁盘、网络、硬件和 CD-ROM 都是vmware_guest模块中的关键,用于描述在 VMware ESXi 上生成新 VM 所需的虚拟化硬件规格。

使用以下命令运行 playbook:

# ansible-playbook esxi_create_vm.yml -vv

以下是 Playbook 输出的截图:

您可以在 vSphere 客户端中验证虚拟机的创建和与 CentOS ISO 文件的绑定:

您还可以通过更改剧本中“状态”中的值来更改现有虚拟机的状态,并从poweredonpoweredoffrestartedabsentsuspendedshutdownguestrebootguest中进行选择。

摘要

VMware 产品广泛用于 IT 基础设施中,为运行应用程序和工作负载提供虚拟化环境。与此同时,VMware 还提供了许多语言的 API 绑定,可用于自动化管理任务。在下一章中,我们将探索另一个名为 OpenStack 的虚拟化框架,该框架依赖于红帽公司的 KVM hypervisor。

第十五章:与 OpenStack API 交互

长期以来,IT 基础设施依赖于商业软件(来自 VMWare、Microsoft 和 Citrix 等供应商)提供运行工作负载和管理资源(如计算、存储和网络)的虚拟环境。然而,IT 行业正在迈向云时代,工程师正在将工作负载和应用程序迁移到云(无论是公共还是私有),这需要一个能够管理所有应用程序资源的新框架,并提供一个开放和强大的 API 接口,以与其他应用程序的外部调用进行交互。

OpenStack 提供了开放访问和集成,以管理所有计算、存储和网络资源,避免在构建云时出现供应商锁定。它可以控制大量的计算节点、存储阵列和网络设备,无论每个资源的供应商如何,并在所有资源之间提供无缝集成。OpenStack 的核心思想是将应用于底层基础设施的所有配置抽象为一个负责管理资源的项目。因此,您将找到一个管理计算资源的项目(称为 Nova),另一个提供实例网络的项目(neutron),以及与不同存储类型交互的项目(Swift 和 Cinder)。

您可以在此链接中找到当前 OpenStack 项目的完整列表

www.OpenStack.org/software/project-navigator/

此外,OpenStack 为应用程序开发人员和系统管理员提供统一的 API 访问,以编排资源创建。

在本章中,我们将探索 OpenStack 的新开放世界,并学习如何利用 Python 和 Ansible 与其交互。

本章将涵盖以下主题:

  • 了解 RESTful web 服务

  • 设置环境

  • 向 OpenStack 发送请求

  • 从 Python 创建工作负载

  • 使用 Ansible 管理 OpenStack 实例

了解 RESTful web 服务

表述状态转移REST)依赖于 HTTP 协议在客户端和服务器之间传输消息。HTTP 最初设计用于在请求时从 Web 服务器(服务器)向浏览器(客户端)传递 HTML 页面。页面代表用户想要访问的一组资源,并由统一资源标识符URI)请求。

HTTP 请求通常包含一个方法,该方法指示需要在资源上执行的操作类型。例如,当从浏览器访问网站时,您可以看到(在下面的屏幕截图中)方法是GET

以下是最常见的 HTTP 方法及其用法:

HTTP 方法 操作
GET 客户端将要求服务器检索资源。
POST 客户端将指示服务器创建新资源。
PUT 客户端将要求服务器修改/更新资源。
DELETE 客户端将要求服务器删除资源。

应用程序开发人员可以公开其应用程序的某些资源,以供外部世界的客户端使用。携带请求从客户端到服务器并返回响应的传输协议是 HTTP。它负责保护通信并使用服务器接受的适当数据编码机制对数据包进行编码,并且在两者之间进行无状态通信。

另一方面,数据包有效载荷通常以 XML 或 JSON 编码,以表示服务器处理的请求结构以及客户端偏好的响应方式。

世界各地有许多公司为开发人员提供其数据的公共访问权限,实时提供。例如,Twitter API(developer.twitter.com/)提供实时数据获取,允许其他开发人员在第三方应用程序中使用数据,如广告、搜索和营销。谷歌(developers.google.com/apis-explorer/#p/discovery/v1/)、LinkedIn(developer.linkedin.com/)和 Facebook(developers.facebook.com/)等大公司也是如此。

对 API 的公共访问通常限制为特定数量的请求,无论是每小时还是每天,对于单个应用程序,以免过度使用公共资源。

Python 提供了大量的工具和库来消耗 API、编码消息和解析响应。例如,Python 有一个requests包,可以格式化并发送 HTTP 请求到外部资源。它还有工具来解析 JSON 格式的响应并将其转换为 Python 中的标准字典。

Python 还有许多框架可以将您的资源暴露给外部世界。DjangoFlask是最好的之一,可以作为全栈框架。

设置环境

OpenStack 是一个免费的开源项目,用于基础设施即服务IaaS),可以控制 CPU、内存和存储等硬件资源,并为许多供应商构建和集成插件提供一个开放的框架。

为了设置我们的实验室,我将使用最新的OpenStack-rdo版本(在撰写时),即 Queens,并将其安装到 CentOS 7.4.1708 上。安装步骤非常简单,可以在www.rdoproject.org/install/packstack/找到。

我们的环境包括一台具有 100GB 存储、12 个 vCPU 和 32GB RAM 的机器。该服务器将包含 OpenStack 控制器、计算和 neutron 角色在同一台服务器上。OpenStack 服务器连接到具有我们自动化服务器的相同交换机和相同子网。请注意,这在生产环境中并不总是这样,但您需要确保运行 Python 代码的服务器可以访问 OpenStack。

实验室拓扑如下:

安装 rdo-OpenStack 软件包

在 RHEL 7.4 和 CentOS 上安装 rdo-OpenStack 的步骤如下:

在 RHEL 7.4 上

首先确保您的系统是最新的,然后从网站安装rdo-release.rpm以获取最新版本。最后,安装OpenStack-packstack软件包,该软件包将自动化 OpenStack 安装,如下段所示:

$ sudo yum install -y https://www.rdoproject.org/repos/rdo-release.rpm
$ sudo yum update -y
$ sudo yum install -y OpenStack-packstack

在 CentOS 7.4 上

首先确保您的系统是最新的,然后安装 rdoproject 以获取最新版本。最后,安装centos-release-OpenStack-queens软件包,该软件包将自动化 OpenStack 安装,如下段所示:

$ sudo yum install -y centos-release-OpenStack-queens
$ sudo yum update -y
$ sudo yum install -y OpenStack-packstack

生成答案文件

现在,您需要生成包含部署参数的答案文件。这些参数中的大多数都是默认值,但我们将更改一些内容:

# packstack --gen-answer-file=/root/EnterpriseAutomation

编辑答案文件

使用您喜欢的编辑器编辑EnterpriseAutomtion文件,并更改以下内容:

CONFIG_DEFAULT_PASSWORD=access123 CONFIG_CEILOMETER_INSTALL=n CONFIG_AODH_INSTALL=n CONFIG_KEYSTONE_ADMIN_PW=access123 CONFIG_PROVISION_DEMO=n 

CELIOMETERAODH是 OpenStack 生态系统中的可选项目,可以在实验室环境中忽略。

我们还设置了一个用于生成临时令牌以访问 API 资源并访问 OpenStack GUI 的KEYSTONE密码

运行 packstack

保存文件并通过packstack运行安装:

# packstack answer-file=EnterpriseAutomation

此命令将从 Queens 存储库下载软件包并安装 OpenStack 服务,然后启动它们。安装成功完成后,将在控制台上打印以下消息:

 **** Installation completed successfully ******

Additional information:
 * Time synchronization installation was skipped. Please note that unsynchronized time on server instances might be problem for some OpenStack components.
 * File /root/keystonerc_admin has been created on OpenStack client host 10.10.10.150\. To use the command line tools you need to source the file.
 * To access the OpenStack Dashboard browse to http://10.10.10.150/dashboard .
Please, find your login credentials stored in the keystonerc_admin in your home directory.
 * The installation log file is available at: /var/tmp/packstack/20180410-155124-CMpsKR/OpenStack-setup.log
 * The generated manifests are available at: /var/tmp/packstack/20180410-155124-CMpsKR/manifests

访问 OpenStack GUI

现在您可以使用http://<server_ip_address>/dashboard访问 OpenStack GUI。凭证将是 admin 和 access123(取决于您在之前步骤中在CONFIG_KEYSTONE_ADMIN_PW中写入了什么):

我们的云现在已经启动运行,准备接收请求。

向 OpenStack keystone 发送请求

OpenStack 包含一系列服务,这些服务共同工作以管理虚拟机的创建、读取、更新和删除(CRUD)操作。每个服务都可以将其资源暴露给外部请求进行消费。例如,nova服务负责生成虚拟机并充当一个 hypervisor 层(虽然它本身不是一个 hypervisor,但可以控制其他 hypervisors,如 KVM 和 vSphere)。另一个服务是glance,负责以 ISO 或 qcow2 格式托管实例镜像。neutron服务负责为生成的实例提供网络服务,并确保位于不同租户(项目)上的实例相互隔离,而位于相同租户上的实例可以通过覆盖网络(VxLAN 或 GRE)相互访问。

为了访问上述每个服务的 API,您需要具有用于特定时间段的经过身份验证的令牌。这就是keystone的作用,它提供身份服务并管理每个用户的角色和权限。

首先,我们需要在自动化服务器上安装 Python 绑定。这些绑定包含用于访问每个服务并使用从 KEYSTONE 生成的令牌进行身份验证的 Python 代码。此外,绑定包含每个项目的支持操作(如创建/删除/更新/列出):

yum install -y gcc openssl-devel python-pip python-wheel
pip install python-novaclient
pip install python-neutronclient
pip install python-keystoneclient
pip install python-glanceclient
pip install python-cinderclient
pip install python-heatclient
pip install python-OpenStackclient

请注意,Python 客户端名称为python-<service_name>client

您可以将其下载到站点的全局包或 Python virtualenv环境中。然后,您将需要 OpenStack 管理员权限,这些权限可以在 OpenStack 服务器内的以下路径中找到:

cat /root/keystonerc_admin
unset OS_SERVICE_TOKEN
export OS_USERNAME=admin
export OS_PASSWORD='access123'
export OS_AUTH_URL=http://10.10.10.150:5000/v3
export PS1='[\u@\h \W(keystone_admin)]\$ '

export OS_PROJECT_NAME=admin
export OS_USER_DOMAIN_NAME=Default
export OS_PROJECT_DOMAIN_NAME=Default
export OS_IDENTITY_API_VERSION=3

请注意,当我们与 OpenStack keystone 服务通信时,我们将在OS_AUTH_URLOS_IDENTITY_API_VERSION参数中使用 keystone 版本 3。大多数 Python 客户端与旧版本兼容,但需要您稍微更改脚本。在令牌生成期间还需要其他参数,因此请确保您可以访问keystonerc_admin文件。还可以在同一文件中的OS_USERNAMEOS_PASSWORD中找到访问凭证。

我们的 Python 脚本将如下所示:

from keystoneauth1.identity import v3
from keystoneauth1 import session

auth = v3.Password(auth_url="http://10.10.10.150:5000/v3",
  username="admin",
  password="access123",
  project_name="admin",
  user_domain_name="Default",
  project_domain_name="Default")
sess = session.Session(auth=auth, verify=False)
print(sess) 

在上述示例中,以下内容适用:

  • python-keystoneclient使用v3类(反映了 keystone API 版本)向 keystone API 发出请求。此类可在keystoneayth1.identity内使用。

  • 然后,我们将从keystonerc_admin文件中获取的完整凭证提供给auth变量。

  • 最后,我们建立了会话,使用 keystone 客户端内的会话管理器。请注意,我们将verify设置为False,因为我们不使用证书来生成令牌。否则,您可以提供证书路径。

  • 生成的令牌可以用于任何服务,并将持续一个小时,然后过期。此外,如果更改用户角色,令牌将立即过期,而不必等待一个小时。

OpenStack 管理员可以在/etc/keystone/keystone.conf文件中配置admin_token字段,该字段永不过期。但出于安全原因,这在生产环境中不被推荐。

如果您不想将凭证存储在 Python 脚本中,可以将它们存储在ini文件中,并使用configparser模块加载它们。首先,在自动化服务器上创建一个creds.ini文件,并赋予适当的 Linux 权限,以便只能使用您自己的帐户打开它。

#vim /root/creds.ini [os_creds]  auth_url="http://10.10.10.150:5000/v3" username="admin" password="access123" project_name="admin" user_domain_name="Default" project_domain_name="Default"

修改后的脚本如下:

from keystoneauth1.identity import v3
from keystoneauth1 import session
import ConfigParser
config = ConfigParser.ConfigParser() config.read("/root/creds.ini") auth = v3.Password(auth_url=config.get("os_creds","auth_url"),
  username=config.get("os_creds","username"),
  password=config.get("os_creds","password"),
  project_name=config.get("os_creds","project_name"),
  user_domain_name=config.get("os_creds","user_domain_name"),
  project_domain_name=config.get("os_creds","project_domain_name")) sess = session.Session(auth=auth, verify=False) print(sess)   

configparser模块将解析creds.ini文件并查看文件内部的os_creds部分。然后,它将使用get()方法获取每个参数前面的值。

config.get()方法将接受两个参数。第一个参数是.ini文件内的部分名称,第二个是参数名称。该方法将返回与参数关联的值。

此方法应该为您的云凭据提供额外的安全性。保护文件的另一种有效方法是使用 Linux 的source命令将keystonerc_admin文件加载到环境变量中,并使用os模块内的environ()方法读取凭据。

从 Python 创建实例

要使实例运行起来,OpenStack 实例需要三个组件。由glance提供的引导镜像,由neutron提供的网络端口,最后是由nova项目提供的定义分配给实例的 CPU 数量、RAM 数量和磁盘大小的计算 flavor。

创建图像

我们将首先下载一个cirros图像到自动化服务器。cirros是一个轻量级的基于 Linux 的图像,被许多 OpenStack 开发人员和测试人员用来验证 OpenStack 服务的功能:

#cd /root/ ; wget http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img

然后,我们将使用glanceclient将图像上传到 OpenStack 图像存储库。请注意,我们需要首先具有 keystone 令牌和会话参数,以便与glance通信,否则,glance将不接受我们的任何 API 请求。

脚本将如下所示:

from keystoneauth1.identity import v3
from keystoneauth1 import session
from glanceclient import client as gclient
from pprint import pprint

auth = v3.Password(auth_url="http://10.10.10.150:5000/v3",
  username="admin",
  password="access123",
  project_name="admin",
  user_domain_name="Default",
  project_domain_name="Default")     sess = session.Session(auth=auth, verify=False)    #Upload the image to the Glance  glance = gclient.Client('2', session=sess)   image = glance.images.create(name="CirrosImage",
  container_format='bare',
  disk_format='qcow2',
  )   glance.images.upload(image.id, open('/root/cirros-0.4.0-x86_64-disk.img', 'rb'))   

在上面的示例中,适用以下内容:

  • 由于我们正在与glance(图像托管项目)通信,因此我们将从安装的glanceclient模块导入client

  • 使用相同的 keystone 脚本生成包含 keystone 令牌的sess

  • 我们创建了 glance 参数,该参数使用glance初始化客户端管理器,并提供版本(版本 2)和生成的令牌。

  • 您可以通过访问 OpenStack GUI | API Access 选项卡来查看所有支持的 API 版本,如下面的屏幕截图所示。还要注意每个项目的支持版本。

  • glance 客户端管理器旨在在 glance OpenStack 服务上运行。指示管理器使用名称CirrosImage创建一个磁盘类型为qcow2格式的图像。

  • 最后,我们将以二进制形式打开下载的图像,使用'rb'标志,并将其上传到创建的图像中。现在,glance将图像导入到图像存储库中新创建的文件中。

您可以通过两种方式验证操作是否成功:

  1. 执行glance.images.upload()后如果没有打印出错误,这意味着请求格式正确,并已被 OpenStack glance API 接受。

  2. 运行glance.images.list()。返回的输出将是一个生成器,您可以遍历它以查看有关上传图像的更多详细信息:

print("==========================Image Details==========================") for image in glance.images.list(name="CirrosImage"):
  pprint(image) 
{u'checksum': u'443b7623e27ecf03dc9e01ee93f67afe',
 u'container_format': u'bare',
 u'created_at': u'2018-04-11T03:11:58Z',
 u'disk_format': u'qcow2',
 u'file': u'/v2/images/3c2614b0-e53c-4be1-b99d-bbd9ce14b287/file',
 u'id': u'3c2614b0-e53c-4be1-b99d-bbd9ce14b287',
 u'min_disk': 0,
 u'min_ram': 0,
 u'name': u'CirrosImage',
 u'owner': u'8922dc52984041af8fe22061aaedcd13',
 u'protected': False,
 u'schema': u'/v2/schemas/image',
 u'size': 12716032,
 u'status': u'active',
 u'tags': [],
 u'updated_at': u'2018-04-11T03:11:58Z',
 u'virtual_size': None,
 u'visibility': u'shared'}

分配 flavor

Flavors 用于确定实例的 CPU、内存和存储大小。OpenStack 带有一组预定义的 flavors,具有从微小到超大的不同大小。对于cirros图像,我们将使用小型 flavor,它具有 2GB RAM,1 个 vCPU 和 20GB 存储。访问 flavors 没有独立的 API 客户端;而是作为nova客户端的一部分。

您可以在 OpenStack GUI | Admin | Flavors 中查看所有可用的内置 flavors:

脚本将如下所示:

from keystoneauth1.identity import v3
from keystoneauth1 import session
from novaclient import client as nclient
from pprint import pprint

auth = v3.Password(auth_url="http://10.10.10.150:5000/v3",
  username="admin",
  password="access123",
  project_name="admin",
  user_domain_name="Default",
  project_domain_name="Default")   sess = session.Session(auth=auth, verify=False)   nova = nclient.Client(2.1, session=sess) instance_flavor = nova.flavors.find(name="m1.small") print("==========================Flavor Details==========================") pprint(instance_flavor)

在上述脚本中,适用以下内容:

  • 由于我们将与nova(计算服务)通信以检索 flavor,因此我们将导入novaclient模块作为nclient

  • 使用相同的 keystone 脚本生成包含 keystone 令牌的sess

  • 我们创建了nova参数,用它来初始化具有nova的客户端管理器,并为客户端提供版本(版本 2.1)和生成的令牌。

  • 最后,我们使用nova.flavors.find()方法来定位所需的规格,即m1.small。名称必须与 OpenStack 中的名称完全匹配,否则将抛出错误。

创建网络和子网

为实例创建网络需要两件事:网络本身和将子网与之关联。首先,我们需要提供网络属性,例如 ML2 驱动程序(Flat、VLAN、VxLAN 等),区分在同一接口上运行的网络之间的分段 ID,MTU 和物理接口,如果实例流量需要穿越外部网络。其次,我们需要提供子网属性,例如网络 CIDR、网关 IP、IPAM 参数(如果定义了 DHCP/DNS 服务器)以及与子网关联的网络 ID,如下面的屏幕截图所示:

现在我们将开发一个 Python 脚本来与 neutron 项目进行交互,并创建一个带有子网的网络

from keystoneauth1.identity import v3
from keystoneauth1 import session
import neutronclient.neutron.client as neuclient

auth = v3.Password(auth_url="http://10.10.10.150:5000/v3",
  username="admin",
  password="access123",
  project_name="admin",
  user_domain_name="Default",
  project_domain_name="Default")   sess = session.Session(auth=auth, verify=False)   neutron = neuclient.Client(2, session=sess)   # Create Network   body_network = {'name': 'python_network',
  'admin_state_up': True,
 #'port_security_enabled': False,
  'shared': True,
  # 'provider:network_type': 'vlan|vxlan',
 # 'provider:segmentation_id': 29 # 'provider:physical_network': None, # 'mtu': 1450,  } neutron.create_network({'network':body_network}) network_id = neutron.list_networks(name="python_network")["networks"][0]["id"]     # Create Subnet   body_subnet = {
  "subnets":[
  {
  "name":"python_network_subnet",
  "network_id":network_id,
  "enable_dhcp":True,
  "cidr": "172.16.128.0/24",
  "gateway_ip": "172.16.128.1",
  "allocation_pools":[
  {
  "start": "172.16.128.10",
  "end": "172.16.128.100"
  }
  ],
  "ip_version": 4,
  }
  ]
  } neutron.create_subnet(body=body_subnet) 

在上述脚本中,以下内容适用:

  • 由于我们将与neutron(网络服务)通信来创建网络和关联子网,我们将导入neutronclient模块作为neuclient

  • 相同的 keystone 脚本用于生成sess,该sess保存后来用于访问 neutron 资源的 keystone 令牌。

  • 我们将创建neutron参数,用它来初始化具有 neutron 的客户端管理器,并为其提供版本(版本 2)和生成的令牌。

  • 然后,我们创建了两个 Python 字典,body_networkbody_subnet,它们分别保存了网络和子网的消息主体。请注意,字典键是静态的,不能更改,而值可以更改,并且通常来自外部门户系统或 Excel 表格,具体取决于您的部署。此外,我对在网络创建过程中不必要的部分进行了评论,例如provider:physical_networkprovider:network_type,因为我们的cirros镜像不会与提供者网络(在 OpenStack 域之外定义的网络)通信,但这里提供了参考。

  • 最后,通过list_networks()方法获取network_id,并将其作为值提供给body_subnet变量中的network_id键,将子网和网络关联在一起。

启动实例

最后一部分是将所有内容粘合在一起。我们有引导镜像、实例规格和连接机器与其他实例的网络。我们准备使用nova客户端启动实例(记住nova负责虚拟机的生命周期和 VM 上的 CRUD 操作):


print("=================Launch The Instance=================")   image_name = glance.images.get(image.id)   network1 = neutron.list_networks(name="python_network") instance_nics = [{'net-id': network1["networks"][0]["id"]}]   server = nova.servers.create(name = "python-instance",
  image = image_name.id,
  flavor = instance_flavor.id,
  nics = instance_nics,) status = server.status
while status == 'BUILD':
  print("Sleeping 5 seconds till the server status is changed")
  time.sleep(5)
  instance = nova.servers.get(server.id)
  status = instance.status
    print(status) print("Current Status is: {0}".format(status))

在上述脚本中,我们使用了nova.servers.create()方法,并传递了生成实例所需的所有信息(实例名称、操作系统、规格和网络)。此外,我们实现了一个轮询机制,用于轮询 nova 服务的服务器当前状态。如果服务器仍处于BUILD阶段,则脚本将休眠五秒,然后再次轮询。当服务器状态更改为ACTIVEFAILURE时,循环将退出,并在最后打印服务器状态。

脚本的输出如下:

Sleeping 5 seconds till the server status is changed
Sleeping 5 seconds till the server status is changed
Sleeping 5 seconds till the server status is changed
Current Status is: ACTIVE

此外,您可以从 OpenStack GUI | 计算 | 实例中检查实例:

从 Ansible 管理 OpenStack 实例

Ansible 提供了可以管理 OpenStack 实例生命周期的模块,就像我们使用 API 一样。您可以在docs.ansible.com/ansible/latest/modules/list_of_cloud_modules.html#OpenStack找到支持的模块的完整列表。

所有 OpenStack 模块都依赖于名为shade的 Python 库(pypi.python.org/pypi/shade),该库提供了对 OpenStack 客户端的包装。

一旦您在自动化服务器上安装了shade,您将可以访问os-*模块,这些模块可以操作 OpenStack 配置,比如os_image(处理 OpenStack 镜像),os_network(创建网络),os_subnet(创建并关联子网到创建的网络),os_nova_flavor(根据 RAM、CPU 和磁盘创建 flavors),最后是os_server模块(启动 OpenStack 实例)。

安装 Shade 和 Ansible

在自动化服务器上,使用 Python 的pip来下载和安装shade,以及所有依赖项:

pip install shade

安装完成后,您将在 Python 的正常site-packages下拥有shade,但我们将使用 Ansible。

此外,如果您之前没有在自动化服务器上安装 Ansible,您将需要安装 Ansible:

# yum install ansible -y

通过从命令行查询 Ansible 版本来验证 Ansible 是否已成功安装:

[root@AutomationServer ~]# ansible --version
ansible 2.5.0
 config file = /etc/ansible/ansible.cfg
 configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
 ansible python module location = /usr/lib/python2.7/site-packages/ansible
 executable location = /usr/bin/ansible
 python version = 2.7.5 (default, Aug  4 2017, 00:39:18) [GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]

构建 Ansible playbook

正如我们在第十三章中所看到的,用于管理的 Ansible,依赖于一个 YAML 文件,其中包含了您需要针对清单中的主机执行的一切。在这种情况下,我们将指示 playbook 在自动化服务器上建立与shade库的本地连接,并提供keystonerc_admin凭据,以帮助shade向我们的 OpenStack 服务器发送请求。

playbook 脚本如下:

--- - hosts: localhost
  vars:
 os_server: '10.10.10.150'
  gather_facts: yes
  connection: local
  environment:
 OS_USERNAME: admin
  OS_PASSWORD: access123
  OS_AUTH_URL: http://{{ os_server }}:5000/v3
  OS_TENANT_NAME: admin
  OS_REGION_NAME: RegionOne
  OS_USER_DOMAIN_NAME: Default
  OS_PROJECT_DOMAIN_NAME: Default    tasks:
  - name: "Upload the Cirros Image"
  os_image:
 name: Cirros_Image
  container_format: bare
  disk_format: qcow2
  state: present
  filename: /root/cirros-0.4.0-x86_64-disk.img
  ignore_errors: yes    - name: "CREATE CIRROS_FLAVOR"
  os_nova_flavor:
 state: present
  name: CIRROS_FLAVOR
  ram: 2048
  vcpus: 4
  disk: 35
  ignore_errors: yes    - name: "Create the Cirros Network"
  os_network:
 state: present
  name: Cirros_network
  external: True
  shared: True
  register: Cirros_network
  ignore_errors: yes      - name: "Create Subnet for The network Cirros_network"
  os_subnet:
 state: present
  network_name: "{{ Cirros_network.id }}"
  name: Cirros_network_subnet
  ip_version: 4
  cidr: 10.10.128.0/18
  gateway_ip: 10.10.128.1
  enable_dhcp: yes
  dns_nameservers:
  - 8.8.8.8
  register: Cirros_network_subnet
  ignore_errors: yes      - name: "Create Cirros Machine on Compute"
  os_server:
 state: present
  name: ansible_instance
  image: Cirros_Image
  flavor: CIRROS_FLAVOR
  security_groups: default
  nics:
  - net-name: Cirros_network
  ignore_errors: yes 

在 playbook 中,我们使用os_*模块将镜像上传到 OpenStack 的glance服务器,创建一个新的 flavor(而不是使用内置的 flavor),并创建与子网关联的网络;然后,我们在os_server中将所有内容粘合在一起,该模块与nova服务器通信以生成机器。

请注意,主机将是本地主机(或托管shade库的机器名称),同时我们在环境变量中添加了 OpenStack keystone 凭据。

运行 playbook

将 playbook 上传到自动化服务器并执行以下命令来运行它:

ansible-playbook os_playbook.yml

playbook 的输出将如下所示:

 [WARNING]: No inventory was parsed, only implicit localhost is available

 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] ****************************************************************************

TASK [Gathering Facts] **********************************************************************
ok: [localhost]

TASK [Upload the Cirros Image] **************************************************************
changed: [localhost]

TASK [CREATE CIRROS_FLAVOR] *****************************************************************
ok: [localhost]

TASK [Create the Cirros Network] ************************************************************
changed: [localhost]

TASK [Create Subnet for The network Cirros_network] *****************************************
changed: [localhost]

TASK [Create Cirros Machine on Compute] *****************************************************
changed: [localhost]

PLAY RECAP **********************************************************************************
localhost                  : ok=6    changed=4    unreachable=0    failed=0   

您可以访问 OpenStack GUI 来验证实例是否是从 Ansible playbook 创建的:

摘要

如今,IT 行业正在尽可能地避免供应商锁定,转向开源世界。OpenStack 为我们提供了窥视这个世界的窗口;许多大型组织和电信运营商正在考虑将其工作负载迁移到 OpenStack,以在其数据中心构建私有云。然后,他们可以构建自己的工具来与 OpenStack 提供的开源 API 进行交互。

在下一章中,我们将探索另一个(付费的)公共亚马逊云,并学习如何利用 Python 来自动化实例创建。

第十六章:使用 Boto3 自动化 AWS

在之前的章节中,我们探讨了如何使用 Python 自动化 OpenStack 和 VMware 私有云。我们将继续通过自动化最受欢迎的公共云之一——亚马逊网络服务(AWS)来继续我们的云自动化之旅。在本章中,我们将探讨如何使用 Python 脚本创建 Amazon Elastic Compute Cloud(EC2)和 Amazon Simple Storage Systems(S3)。

本章将涵盖以下主题:

  • AWS Python 模块

  • 管理 AWS 实例

  • 自动化 AWS S3 服务

AWS Python 模块

Amazon EC2 是一个可扩展的计算系统,用于为托管不同虚拟机(例如 OpenStack 生态系统中的 nova-compute 项目)提供虚拟化层。它可以与其他服务(如 S3、Route 53 和 AMI)通信,以实例化实例。基本上,您可以将 EC2 视为其他在虚拟基础设施管理器上设置的虚拟化程序(如 KVM 和 VMware)之上的抽象层。EC2 将接收传入的 API 调用,然后将其转换为适合每个虚拟化程序的调用。

Amazon Machine Image(AMI)是一个打包的镜像系统,其中包含了启动虚拟机所需的操作系统和软件包(类似于 OpenStack 中的 Glance)。您可以从现有的虚拟机创建自己的 AMI,并在需要在其他基础设施上复制这些机器时使用它,或者您可以简单地从互联网或亚马逊市场上选择公开可用的 AMI。我们需要从亚马逊网络控制台获取 AMI ID,并将其添加到我们的 Python 脚本中。

AWS 设计了一个名为 Boto3 的 SDK(github.com/boto/boto3),允许 Python 开发人员编写与不同服务的 API 进行交互和消费的脚本和软件,如 Amazon EC2 和 Amazon S3。该库是为提供对 Python 2.6.5、2.7+和 3.3 的本地支持而编写的。

Boto3 的主要功能在官方文档中有描述,网址为boto3.readthedocs.io/en/latest/guide/new.html,以下是一些重要功能:

  • 资源:高级、面向对象的接口。

  • 集合:用于迭代和操作资源组的工具。

  • 客户端:低级服务连接。

  • 分页器:自动分页响应。

  • 等待者:一种暂停执行直到达到某种状态或发生故障的方式。每个 AWS 资源都有一个等待者名称,可以使用<resource_name>.waiter_names访问。

Boto3 安装

在连接到 AWS 之前需要一些东西:

  1. 首先,您需要一个具有创建、修改和删除基础设施权限的亚马逊管理员帐户。

  2. 其次,安装用于与 AWS 交互的boto3 Python 模块。您可以通过转到 AWS 身份和访问管理(IAM)控制台并添加新用户来创建一个专用于发送 API 请求的用户。您应该在“访问类型”部分下看到“编程访问”选项。

  3. 现在,您需要分配一个允许在亚马逊服务中具有完全访问权限的策略,例如 EC2 和 S3。通过单击“附加现有策略到用户”并将 AmazonEC2FullAccess 和 AmazonS3FullAccess 策略附加到用户名来实现。

  4. 最后,点击“创建用户”以添加具有配置选项和策略的用户。

您可以在 AWS 上注册免费的基础套餐帐户,这将使您在 12 个月内获得亚马逊提供的许多服务。免费访问可以在aws.amazon.com/free/上获得。

在使用 Python 脚本管理 AWS 时,访问密钥 ID 用于发送 API 请求并从 API 服务器获取响应。我们不会使用用户名或密码发送请求,因为它们很容易被他人捕获。此信息是通过下载创建用户名后出现的文本文件获得的。重要的是将此文件放在安全的位置并为其提供适当的 Linux 权限,以打开和读取文件内容。

另一种方法是在您的家目录下创建一个.aws目录,并在其中放置两个文件:credentialsconfig。第一个文件将同时包含访问密钥 ID 和秘密访问 ID。

~/.aws/credentials如下所示:

[default]
aws_access_key_id=AKIAIOSFODNN7EXAMPLE
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

第二个文件将保存用户特定的配置,例如首选数据中心(区域),用于托管创建的虚拟机。在下面的示例中,我们指定要在us-west-2数据中心托管我们的机器。

配置文件~/.aws/config如下所示:

[default]
region=us-west-2

现在,安装boto3需要使用通常的pip命令来获取最新的boto3版本:

pip install boto3

要验证模块是否成功安装,请在 Python 控制台中导入boto3,您不应该看到任何导入错误报告:

管理 AWS 实例

现在,我们准备使用boto3创建我们的第一个虚拟机。正如我们所讨论的,我们需要 AMI,我们将从中实例化一个实例。将 AMI 视为 Python 类;创建一个实例将从中创建一个对象。我们将使用 Amazon Linux AMI,这是由 Amazon 维护的特殊 Linux 操作系统,用于部署 Linux 机器而不收取任何额外费用。您可以在每个区域找到完整的 AMI ID,网址为aws.amazon.com/amazon-linux-ami/

import boto3
ec2 = boto3.resource('ec2') instance = ec2.create_instances(ImageId='ami-824c4ee2', MinCount=1, MaxCount=1, InstanceType='m5.xlarge',
  Placement={'AvailabilityZone': 'us-west-2'},
  ) print(instance[0])   

在上面的示例中,以下内容适用:

  1. 我们导入了之前安装的boto3模块。

  2. 然后,我们指定了要与之交互的资源类型,即 EC2,并将其分配给ec2对象。

  3. 现在,我们有资格使用create_instance()方法,并为其提供实例参数,例如ImageIDInstanceType(类似于 OpenStack 中的 flavor,它确定了计算和内存方面的实例规格),以及我们应该在AvailabilityZone中创建此实例。

  4. MinCountMaxCount确定 EC2 在扩展我们的实例时可以走多远。例如,当一个实例发生高 CPU 时,EC2 将自动部署另一个实例,以分享负载并保持服务处于健康状态。

  5. 最后,我们打印了要在下一个脚本中使用的实例 ID。

输出如下:

您可以在以下链接中检查所有有效的 Amazon EC2 实例类型;请仔细阅读,以免因选择错误的类型而被过度收费:aws.amazon.com/ec2/instance-types/

实例终止

打印的 ID 用于 CRUD 操作,以便稍后管理或终止实例。例如,我们可以使用之前创建的ec2资源提供的terminate()方法来终止实例:

import boto3
ec2 = boto3.resource('ec2') instance_id = "i-0a81k3ndl29175220" instance = ec2.Instance(instance_id) instance.terminate() 

请注意,在前面的代码中我们硬编码了instance_id(当您需要创建一个可以在不同环境中使用的动态 Python 脚本时,这并不总是适用)。我们可以使用 Python 中可用的其他输入方法,例如raw_input(),从用户那里获取输入或查询我们帐户中可用的实例,并让 Python 提示我们需要终止哪些实例。另一个用例是创建一个 Python 脚本,检查我们实例的最后登录时间或资源消耗;如果它们超过特定值,我们将终止该实例。这在实验室环境中非常有用,您不希望因为恶意或设计不良的软件而被收取额外资源的费用。

自动化 AWS S3 服务

AWS 简单存储系统S3)提供了安全和高度可扩展的对象存储服务。您可以使用此服务存储任意数量的数据,并从任何地方恢复它。系统为您提供了版本控制选项,因此您可以回滚到文件的任何先前版本。此外,它提供了 REST Web 服务 API,因此您可以从外部应用程序访问它。

当数据传入 S3 时,S3 将为其创建一个对象,并将这些对象存储在存储桶中(将它们视为文件夹)。您可以为每个创建的存储桶提供复杂的用户权限,并且还可以控制其可见性(公共、共享或私有)。存储桶访问可以是策略或访问控制列表ACL)。

存储桶还存储有描述键值对中对象的元数据,您可以通过 HTTP POST方法创建和设置。元数据可以包括对象的名称、大小和日期,或者您想要的任何其他自定义键值对。用户帐户最多可以拥有 100 个存储桶,但每个存储桶内托管的对象大小没有限制。

创建存储桶

与 AWS S3 服务交互时,首先要做的事情是创建一个用于存储文件的存储桶。在这种情况下,我们将S3提供给boto3.resource()。这将告诉boto3开始初始化过程,并加载与 S3 API 系统交互所需的命令:

import boto3
s3_resource = boto3.resource("s3")   bucket = s3_resource.create_bucket(Bucket="my_first_bucket", CreateBucketConfiguration={
  'LocationConstraint': 'us-west-2'}) print(bucket)

在前面的例子中,以下内容适用:

  1. 我们导入了之前安装的boto3模块。

  2. 然后,我们指定了我们想要与之交互的资源类型,即s3,并将其分配给s3_resource对象。

  3. 现在,我们可以在资源内部使用create_bucket()方法,并为其提供所需的参数来创建存储桶,例如Bucket,我们可以指定其名称。请记住,存储桶名称必须是唯一的,且之前不能已经使用过。第二个参数是CreateBucketConfiguration字典,我们在其中设置了创建存储桶的数据中心位置。

将文件上传到存储桶

现在,我们需要利用创建的存储桶并将文件上传到其中。请记住,存储桶中的文件表示为对象。因此,boto3提供了一些包含对象作为其一部分的方法。我们将从使用put_object()开始。此方法将文件上传到创建的存储桶并将其存储为对象:

import boto3
s3_resource = boto3.resource("s3") bucket = s3_resource.Bucket("my_first_bucket")   with open('~/test_file.txt', 'rb') as uploaded_data:
  bucket.put_object(Body=uploaded_data) 

在前面的例子中,以下内容适用:

  1. 我们导入了之前安装的boto3模块。

  2. 然后,我们指定了我们想要与之交互的资源类型,即s3,并将其分配给s3_resource对象。

  3. 我们通过Bucket()方法访问了my_first_bucket并将返回的值分配给了存储桶变量。

  4. 然后,我们使用with子句打开了一个文件,并将其命名为uploaded_data。请注意,我们以二进制数据的形式打开了文件,使用了rb标志。

  5. 最后,我们使用存储桶空间中提供的put_object()方法将二进制数据上传到我们的存储桶。

删除存储桶

要完成对存储桶的 CRUD 操作,我们需要做的最后一件事是删除存储桶。这是通过在我们的存储桶变量上调用delete()方法来实现的,前提是它已经存在,并且我们通过名称引用它,就像我们创建它并向其中上传数据一样。然而,当存储桶不为空时,delete()可能会失败。因此,我们将使用bucket_objects.all().delete()方法获取存储桶内的所有对象,然后对它们应用delete()操作,最后删除存储桶:

import boto3
s3_resource = boto3.resource("s3") bucket = s3_resource.Bucket("my_first_bucket") bucket.objects.all().delete() bucket.delete()

总结

在本章中,我们学习了如何安装亚马逊弹性计算云(EC2),以及学习了 Boto3 及其安装。我们还学习了如何自动化 AWS S3 服务。

在下一章中,我们将学习 SCAPY 框架,这是一个强大的 Python 工具,用于构建和制作数据包并将其发送到网络上。

第十七章:使用 Scapy 框架

Scapy 是一个强大的 Python 工具,用于构建和制作数据包,然后将其发送到网络。您可以构建任何类型的网络流并将其发送到网络。它可以帮助您使用不同的数据包流测试您的网络,并操纵从源返回的响应。

本章将涵盖以下主题:

  • 了解 Scapy 框架

  • 安装 Scapy

  • 使用 Scapy 生成数据包和网络流

  • 捕获和重放数据包

了解 Scapy

Scapy (scapy.net)是强大的 Python 工具之一,用于捕获、嗅探、分析和操纵网络数据包。它还可以构建分层协议的数据包结构,并将 wiuthib 流注入到网络中。您可以使用它在许多协议之上构建广泛的协议,并设置协议内每个字段的细节,或者更好地让 Scapy 发挥其魔力并选择适当的值,以便每个值都可以有一个有效的帧。如果用户没有覆盖,Scapy 将尝试使用数据包的默认值。以下值将自动设置为每个流:

  • IP 源根据目的地和路由表选择

  • 校验和会自动计算

  • 源 Mac 根据输出接口选择

  • 以太网类型和 IP 协议由上层确定

Scapy 可以编程地将帧注入到流中并重新发送。例如,您可以将 802.1q VLAN ID 注入到流中并重新发送,以执行对网络的攻击或分析。此外,您可以使用GraphvizImageMagick模块可视化两个端点之间的对话并绘制图形。

Scapy 有自己的领域特定语言DSL),使用户能够描述他想要构建或操纵的数据包,并以相同的结构接收答案。这与 Python 内置的数据类型(如列表和字典)非常好地配合和集成。我们将在示例中看到,从网络接收的数据包实际上是一个 Python 列表,我们可以对它们进行常规列表函数的迭代。

安装 Scapy

Scapy 支持 Python 2.7.x 和 3.4+,从 Scapy 版本 2.x 开始。但是,对于低于 2.3.3 的版本,Scapy 需要 Python 2.5 和 2.7,或者 3.4+用于之后的版本。由于我们已经安装了最新的 Python 版本,应该可以毫无问题地运行最新版本的 Scapy。

此外,Scapy 还有一个较旧的版本(1.x),已经不再支持 Python 3,仅在 Python 2.4 上运行。

基于 Unix 的系统

要获取最新版本,您需要使用 python pip:

pip install scapy 

输出应该类似于以下屏幕截图:

要验证 Scapy 是否成功安装,请访问 Python 控制台并尝试将scapy模块导入其中。如果控制台没有报告任何导入错误,则安装已成功完成:

需要一些附加软件包来可视化对话和捕获数据包。根据您的平台使用以下命令安装附加软件包:

在 Debian 和 Ubuntu 上安装

运行以下命令安装附加软件包:

sudo apt-get install tcpdump graphviz imagemagick python-gnuplot python-cryptography python-pyx

在 Red Hat/CentOS 上安装

运行以下命令安装附加软件包:

yum install tcpdump graphviz imagemagick python-gnuplot python-crypto python-pyx -y

如果在基于 CentOS 的系统上找不到上述软件包中的任何一个,请安装epel存储库并更新系统。

Windows 和 macOS X 支持

Scapy 是专为基于 Linux 的系统构建和设计的。但它也可以在其他操作系统上运行。您可以在 Windows 和 macOS 上安装和移植它,每个平台都有一些限制。对于基于 Windows 的系统,您基本上需要删除 WinPcap 驱动程序,并改用 Npcap 驱动程序(不要同时安装两个版本,以避免任何冲突问题)。您可以在scapy.readthedocs.io/en/latest/installation.html#windows上阅读有关 Windows 安装的更多信息。

对于 macOS X,您需要安装一些 Python 绑定并使用 libdnet 和 libpcap 库。完整的安装步骤可在scapy.readthedocs.io/en/latest/installation.html#mac-os-x上找到。

使用 Scapy 生成数据包和网络流

正如我们之前提到的,Scapy 有自己的 DSL 语言,与 Python 集成。此外,您可以直接访问 Scapy 控制台,并开始直接从 Linux shell 发送和接收数据包:

sudo scapy 

前面命令的输出如下:

请注意,有一些关于一些缺少的可选软件包的警告消息,例如matplotlibPyX,但这应该没问题,不会影响 Scapy 的核心功能。

我们可以首先通过运行ls()函数来检查 Scapy 中支持的协议。列出所有支持的协议:

>>> ls()

输出非常冗长,如果在此处发布,将跨越多个页面,因此您可以快速查看终端,以检查它。

现在让我们开发一个 hello world 应用程序,并使用 SCAPY 运行它。该程序将向服务器的网关发送一个简单的 ICMP 数据包。我安装了 Wireshark 并配置它以监听将从自动化服务器(托管 Scapy)接收流的网络接口。

现在,在 Scapy 终端上,执行以下代码:

>>> send(IP(dst="10.10.10.1")/ICMP()/"Welcome to Enterprise Automation Course") 

返回到 Wireshark,你应该看到通信:

让我们分析 Scapy 执行的命令:

  • Send:这是 Scapy Domain Specific Language (DSL)中的内置函数,指示 Scapy 发送单个数据包(并不监听任何响应;它只发送一个数据包并退出)。

  • IP:现在,在这个类中,我们将开始构建数据包层。从 IP 层开始,我们需要指定将接收数据包的目标主机(在这种情况下,我们使用dst参数来指定目的地)。还要注意,我们可以在src参数中指定源 IP;但是,Scapy 将查询主机路由表并找到合适的源 IP,并将其放入数据包中。您可以提供其他参数,例如生存时间TTL),Scapy 将覆盖默认值。

  • /:虽然它看起来像是 Python 中常用的普通除法运算符,但在 Scapy DSL 中,它用于区分数据包层,并将它们堆叠在一起。

  • ICMP():用于创建具有默认值的 ICMP 数据包的内置类。可以向函数提供的值之一是 ICMP 类型,它确定消息类型:echoecho replyunreachable等。

  • 欢迎来到企业自动化课程:如果将字符串注入 ICMP 有效载荷中,Scapy 将自动将其转换为适当的格式。

请注意,我们没有在堆栈中指定以太网层,并且没有提供任何 mac 地址(源或目的地)。这在 Scapy 中默认填充,以创建一个有效的帧。它将自动检查主机 ARP 表,并找到源接口的 mac 地址(如果存在,也是目的地),然后将它们格式化为以太网帧。

在继续下一个示例之前,需要注意的最后一件事是,您可以使用与我们之前用于列出所有支持的协议的ls()函数相同的函数,以获取每个协议的默认值,然后在调用协议时将其设置为任何其他值。

现在让我们做一些更复杂(和邪恶的)事情!假设我们有两台路由器之间形成 VRRP 关系,并且我们需要打破这种关系以成为新的主机,或者至少在网络中创建一个抖动问题,如下拓扑图所示:

请注意,配置为运行 VRRP 的路由器加入多播地址(255.0.0.18)以接收其他路由器的广告。VRRP 数据包的目标 MAC 地址应该包含最后两个数字的 VRRP 组号。它还包含在路由器之间选举过程中使用的路由器优先级。我们将构建一个 Scapy 脚本,该脚本将发送一个具有比网络中配置的更高优先级的 VRRP 通告。这将导致我们的 Scapy 服务器被选为新的主机:

from scapy.layers.inet import * from scapy.layers.vrrp import VRRP

vrrp_packet = Ether(src="00:00:5e:00:01:01",dst="01:00:5e:00:00:30")/IP(src="10.10.10.130", dst="224.0.0.18")/VRRP(priority=254, addrlist=["10.10.10.1"]) sendp(vrrp_packet, inter=2, loop=1) 

在这个例子中:

  • 首先,我们从scapy.layers模块中导入了一些需要的层,我们将这些层叠加在一起。例如,inet模块包含了IP()Ether()ARP()ICMP()等层。

  • 此外,我们还需要 VRRP 层,可以从scapy.layers.vrrp中导入。

  • 其次,我们将构建一个 VRRP 数据包并将其存储在vrrp_packet变量中。该数据包包含以太网帧内的 mac 地址中的 VRRP 组号。多播地址将位于 IP 层内。此外,我们将在 VRRP 层内配置一个更高的优先级号码。这样我们将拥有一个有效的 VRRP 通告,路由器将接受它。我们为每个层提供了信息,例如目标 mac 地址(VRRP MAC +组号)和多播 IP(225.0.0.18)。

  • 最后,我们使用了sendp()函数,并向其提供了一个精心制作的vrrp_packetsendp()函数将在第 2 层发送数据包,与我们在上一个示例中使用的send()函数发送数据包的方式不同,后者是在第 3 层发送数据包。sendp()函数不会像send()函数那样尝试解析主机名,并且只会在第 2 层操作。此外,由于我们需要连续发送此通告,因此我们配置了loopinter参数,以便每 2 秒发送一次通告。

脚本输出为:

您可以将此攻击与 ARP 欺骗和 VLAN 跳跃攻击相结合,以便在第 2 层更改 mac 地址,切换到 Scapy 服务器的 MAC 地址,并执行中间人MITM)攻击。

Scapy 还包含一些执行扫描的类。例如,您可以使用arping()在网络范围内执行 ARP 扫描,并在其中指定 IP 地址的正则表达式格式。Scapy 将向这些子网上的所有主机发送 ARP 请求并检查回复:

from scapy.layers.inet import *  arping("10.10.10.*")

脚本输出为:

根据接收到的数据包,只有一个主机回复 SCAPY,这意味着它是扫描子网上唯一的主机。回复中列出了主机的 mac 地址和 IP 地址。

捕获和重放数据包

Scapy 具有监听网络接口并捕获其所有传入数据包的能力。它可以以与tcpdump相同的方式将其写入pcap文件,但是 Scapy 提供了额外的函数,可以再次读取和重放pcap文件。

从简单的数据包重放开始,我们将指示 Scapy 读取从网络中捕获的正常pcap文件(使用tcpdump或 Scapy 本身)并将其再次发送到网络。如果我们需要测试网络的行为是否通过特定的流量模式,这将非常有用。例如,我们可能已经配置了网络防火墙以阻止 FTP 通信。我们可以通过使用 Scapy 重放的 FTP 数据来测试防火墙的功能。

在这个例子中,我们有捕获的 FTP pcap文件,我们需要将其重新发送到网络:

from scapy.layers.inet import * from pprint import pprint
pkts = PcapReader("/root/ftp_data.pcap") #should be in wireshark-tcpdump format   for pkt in pkts:
  pprint(pkt.show()) 

PcapReader()pcap文件作为输入,并对其进行分析,以单独获取每个数据包,并将其作为pkts列表中的一个项目添加。现在我们可以遍历列表并显示每个数据包的内容。

脚本输出为:

此外,您可以通过get_layer()函数获取特定层的信息,该函数访问数据包层。例如,如果我们有兴趣获取没有标头的原始数据,以便构建传输文件,我们可以使用以下脚本获取十六进制中所需的数据,然后稍后将其转换为 ASCII:

from scapy.layers.inet import * from pprint import pprint
pkts = PcapReader("/root/ftp_data.pcap") #should be in wireshark-tcpdump format   ftp_data = b"" for pkt in pkts:
  try:
  ftp_data += pkt.get_layer(Raw).load
    except:
  pass

请注意,我们必须用 try-except 子句包围get_layer()方法,因为某些层不包含原始数据(例如 FTP 控制消息)。Scapy 会抛出错误,脚本将退出。此外,我们可以将脚本重写为一个if子句,只有在数据包中包含原始层时才会向ftp_data添加内容。

为了避免在读取pcap文件时出现任何错误,请确保将您的pcap文件保存(或导出)为 Wireshark/tcpdump 格式,如下所示,而不是默认格式:

向数据包中注入数据

在将数据包重新发送到网络之前,我们可以操纵数据包并更改其内容。由于我们的数据包实际上存储为列表中的项目,我们可以遍历这些项目并替换特定信息。例如,我们可以更改 MAC 地址、IP 地址,或者为每个数据包或符合特定条件的特定数据包添加附加层。但是,我们应该注意,在特定层(如 IP 和 TCP)中操纵数据包并更改内容将导致整个层的校验和无效,接收方可能因此丢弃数据包。

Scapy 有一个令人惊奇的功能(是的,我知道,我多次说了令人惊奇,但 Scapy 确实是一个很棒的工具)。如果我们在pcap文件中删除原始内容,它将基于新内容自动为我们计算校验和。

因此,我们将修改上一个脚本并更改一些数据包参数,然后在发送数据包到网络之前重新构建校验和:


from scapy.layers.inet import * from pprint import pprint
pkts = PcapReader("/root/ftp_data.pcap") #should be in wireshark-tcpdump format     p_out = []   for pkt in pkts:
  new_pkt = pkt.payload

    try:
  new_pkt[IP].src = "10.10.88.100"
  new_pkt[IP].dst = "10.10.88.1"
  del (new_pkt[IP].chksum)
  del (new_pkt[TCP].chksum)
  except:
  pass    pprint(new_pkt.show())
  p_out.append(new_pkt) send(PacketList(p_out), iface="eth0")

在上一个脚本中:

  • 我们使用PcapReader()类来读取 FTP pcap文件的内容,并将数据包存储在pkts变量中。

  • 然后,我们遍历数据包并将有效载荷分配给new_pkt,以便我们可以操纵内容。

  • 请记住,数据包本身被视为来自该类的对象。我们可以访问srcdst成员,并将它们设置为任何所需的值。在这里,我们将目的地设置为网关,将源设置为与原始数据包不同的值。

  • 设置新的 IP 值将使校验和无效,因此我们使用del关键字删除了 IP 和 TCP 校验和。Scapy 将根据新数据包内容重新计算它们。

  • 最后,我们将new_pkt附加到空的p_out列表中,并使用send()函数发送它。请注意,我们可以在发送函数中指定退出接口,或者只需离开它,Scapy 将查询主机路由表;它将为每个数据包获取正确的退出接口。

脚本输出为:

此外,如果我们仍然在网关上运行 Wireshark,我们会注意到 Wireshark 捕获了在重新计算后设置校验和值的ftp数据包流:

数据包嗅探

Scapy 有一个名为sniff()的内置数据包捕获函数。默认情况下,如果您不指定任何过滤器或特定接口,它将监视所有接口并捕获所有数据包:

from scapy.all import * from pprint import pprint

print("Begin capturing all packets from all interfaces. send ctrl+c to terminate and print summary") pkts = sniff()   pprint(pkts.summary())

脚本输出为:

当然,您可以提供过滤器和特定接口来监视是否满足条件。例如,在前面的输出中,我们可以看到混合了 ICMP、TCP、SSH 和 DHCP 流量命中了所有接口。如果我们只对在 eth0 上获取 ICMP 流量感兴趣,那么我们可以提供过滤器和iface参数来嗅探函数,并且它将只过滤所有流量并记录只有 ICMP 的数据:

from scapy.all import * from pprint import pprint

print("Begin capturing all packets from all interfaces. send ctrl+c to terminate and print summary") pkts = sniff(iface="eth0", filter="icmp")   pprint(pkts.summary())

脚本输出如下:

请注意,我们只捕获 eth0 接口上的 ICMP 通信,所有其他数据包都由于应用在它们上的过滤器而被丢弃。iface值接受我们在脚本中使用的单个接口或要监视它们的接口列表。

sniff的高级功能之一是stop_filter,它是应用于每个数据包的 Python 函数,用于确定我们是否必须在该数据包之后停止捕获。例如,如果我们设置stop_filter = lambda x: x.haslayer(TCP),那么一旦我们命中具有 TCP 层的数据包,我们将停止捕获。此外,store选项允许我们将数据包存储在内存中(默认情况下已启用)或在对每个数据包应用特定函数后丢弃它们。如果您正在从 SCAPY 中获取来自线缆的实时流量,并且不希望将其写入内存,那么将sniff函数中的 store 参数设置为 false,然后 SCAPY 将在丢弃原始数据包之前应用您开发的任何自定义函数(例如获取数据包的一些信息或将其重新发送到不同的目的地等)。这将在嗅探期间节省一些内存资源。

将数据包写入 pcap

最后,我们可以将嗅探到的数据包写入标准的pcap文件,并像往常一样使用 Wireshark 打开它。这是通过一个简单的wrpcap()函数实现的,它将数据包列表写入pcap文件。wrpcap()函数接受两个参数——第一个是文件位置的完整路径,第二个是在使用sniff()函数之前捕获的数据包列表:

from scapy.all import *   print("Begin capturing all packets from all interfaces. send ctrl+c to terminate and print summary") pkts = sniff(iface="eth0", filter="icmp")   wrpcap("/root/icmp_packets_eth0.pcap",pkts)

摘要

在本章中,我们学习了如何利用 Scapy 框架构建任何类型的数据包,包含任何网络层,并用我们的值填充它。此外,我们还看到了如何在接口上捕获数据包并重放它们。

第十八章:使用 Python 构建网络扫描器

在本章中,我们将构建一个网络扫描器,它可以识别网络上的活动主机,并且我们还将扩展它以包括猜测每个主机上正在运行的操作系统以及打开/关闭的端口。通常,收集这些信息需要多个工具和一些 Linux ninja 技能来获取所需的信息,但是使用 Python,我们可以构建自己的网络扫描器代码,其中包括任何工具,并且我们可以获得定制的输出。

本章将涵盖以下主题:

  • 理解网络扫描器

  • 使用 Python 构建网络扫描器

  • 在 GitHub 上分享您的代码

理解网络扫描器

网络扫描器用于扫描提供的网络 ID 范围,包括第 2 层和第 3 层。它可以发送请求并分析数十万台计算机的响应。此外,您可以扩展其功能以显示一些共享资源,通过 Samba 和 NetBIOS 协议,以及运行共享协议的服务器上的未受保护数据的内容。渗透测试中网络扫描器的另一个用途是,当白帽黑客尝试模拟对网络资源的攻击以查找漏洞并评估公司的安全性时。渗透测试的最终目标是生成一份报告,其中包含目标系统中所有弱点,以便原始点可以加强和增强安全策略,以抵御潜在的真实攻击。

使用 Python 构建网络扫描器

Python 工具提供了许多本地模块,并支持与套接字和 TCP/IP 一般的工作。此外,Python 可以使用系统上可用的现有第三方命令来启动所需的扫描并返回结果。这可以使用我们之前讨论过的subprocess模块来完成,在第九章中,使用 Subprocess 模块。一个简单的例子是使用 Nmap 来扫描子网,就像下面的代码中所示:

import subprocess
from netaddr import IPNetwork
network = "192.168.1.0/24" p = subprocess.Popen(["sudo", "nmap", "-sP", network], stdout=subprocess.PIPE)   for line in p.stdout:
  print(line)

在这个例子中,我们可以看到以下内容:

  • 首先,我们导入了subprocess模块以在我们的脚本中使用。

  • 然后,我们使用network参数定义了要扫描的网络。请注意,我们使用了 CIDR 表示法,但我们也可以使用子网掩码,然后使用 Python 的netaddr模块将其转换为 CIDR 表示法。

  • subprocess中的Popen()类用于创建一个对象,该对象将发送常规的 Nmap 命令并扫描网络。请注意,我们添加了一些标志-sP来调整 Nmap 的操作,并将输出重定向到subprocess.PIPE创建的特殊管道。

  • 最后,我们迭代创建的管道并打印每一行。

脚本输出如下:

在 Linux 上访问网络端口需要 root 访问权限,或者您的帐户必须属于 sudoers 组,以避免脚本中的任何问题。此外,在运行 Python 代码之前,系统上应该安装nmap软件包。

这是一个简单的 Python 脚本,我们可以直接使用 Nmap 工具,而不是在 Python 中使用它。然而,使用 Python 代码包装 Nmap(或任何其他系统命令)给了我们灵活性,可以定制输出并以任何方式自定义它。在下一节中,我们将增强我们的脚本并添加更多功能。

增强代码

尽管 Nmap 的输出给了我们对扫描网络上的活动主机的概述,但我们可以增强它并获得更好的输出视图。例如,我需要在输出的开头知道主机的总数,然后是每个主机的 IP 地址、MAC 地址和 MAC 供应商,但以表格形式,这样我就可以轻松地找到任何主机以及与之相关的所有信息。

因此,我将设计一个函数并命名为nmap_report()。这个函数将获取从subprocess管道生成的标准输出,并提取所需的信息,并以表格格式进行格式化:

def nmap_report(data):
  mac_flag = ""
  ip_flag = ""
  Host_Table = PrettyTable(["IP", "MAC", "Vendor"])
  number_of_hosts = data.count("Host is up ")    for line in data.split("\n"):
  if "MAC Address:" in line:
  mac = line.split("(")[0].replace("MAC Address: ", "")
  vendor = line.split("(")[1].replace(")", "")
  mac_flag = "ready"
  elif "Nmap scan report for" in line:
  ip = re.search(r"Nmap scan report for (.*)", line).groups()[0]
  ip_flag = "ready"      if mac_flag == "ready" and ip_flag == "ready":
  Host_Table.add_row([ip, mac, vendor])
  mac_flag = ""
  ip_flag = ""    print("Number of Live Hosts is {}".format(number_of_hosts))
  print Host_Table

首先,我们可以通过计算传递输出中“主机已启动”的出现次数来获取活动主机的数量,并将其分配给number_of_hosts参数。

其次,Python 有一个很好的模块叫做PrettyTable,它可以创建一个文本表,并根据其中的数据处理单元大小。该模块接受表头作为列表,并使用add_row()函数向创建的表中添加行。因此,第一件事是导入这个模块(如果尚未安装,则安装它)。在我们的例子中,我们将传递一个包含三个项目(IPMACVendor)的列表给PrettyTable类(从PrettyTable模块导入),以创建表头。

现在,为了填充这个表,我们将在\n(回车)上进行分割。分割结果将是一个列表,我们可以迭代以获取特定信息,如 MAC 地址和 IP 地址。我们使用了一些分割和替换技巧来提取 MAC 地址。此外,我们使用了正则表达式search函数从输出中获取 IP 地址部分(如果启用了 DNS,则获取主机名)。

最后,我们将这些信息添加到创建的Host_Table中,并继续迭代下一行。

以下是完整的脚本:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   import subprocess
from netaddr import IPNetwork, AddrFormatError
from prettytable import PrettyTable
import re

def nmap_report(data):
  mac_flag = ""
  ip_flag = ""
  Host_Table = PrettyTable(["IP", "MAC", "Vendor"])
  number_of_hosts = data.count("Host is up ")    for line in data.split("\n"):
  if "MAC Address:" in line:
  mac = line.split("(")[0].replace("MAC Address: ", "")
  vendor = line.split("(")[1].replace(")", "")
  mac_flag = "ready"
  elif "Nmap scan report for" in line:
  ip = re.search(r"Nmap scan report for (.*)", line).groups()[0]
  ip_flag = "ready"      if mac_flag == "ready" and ip_flag == "ready":
  Host_Table.add_row([ip, mac, vendor])
  mac_flag = ""
  ip_flag = ""    print("Number of Live Hosts is {}".format(number_of_hosts))
  print Host_Table

network = "192.168.1.0/24"   try:
  IPNetwork(network) 
  p = subprocess.Popen(["sudo", "nmap", "-sP", network], stdout=subprocess.PIPE)
  nmap_report(p.stdout.read()) except AddrFormatError:
  print("Please Enter a valid network IP address in x.x.x.x/y format")

请注意,我们还使用了netaddr.IPNetwork()类对subprocess命令进行了预检查。这个类将在执行subprocess命令之前验证网络是否格式正确,否则该类将引发一个异常,应该由AddrFormatError异常类处理,并向用户打印一个定制的错误消息。

脚本输出如下:

现在,如果我们将网络更改为不正确的值(子网掩码错误或网络 ID 无效),IPNetwork()类将抛出一个异常,并打印出这个错误消息:

network = "192.168.300.0/24"  

扫描服务

主机机器上运行的服务通常会在操作系统中打开一个端口,并开始监听它,以接受传入的 TCP 通信并开始三次握手。在 Nmap 中,您可以在特定端口上发送一个 SYN 数据包,如果主机以 SYN-ACK 响应,则服务正在运行并监听该端口。

让我们测试 HTTP 端口,例如在google.com上,使用nmap

nmap -p 80 www.google.com

我们可以使用相同的概念来发现路由器上运行的服务。例如,运行 BGP 守护程序的路由器将监听端口179以接收/更新/保持活动/通知消息。如果要监视路由器,则应启用 SNMP 服务,并应监听传入的 SNMP get/set 消息。MPLS LDP 通常会监听646以与其他邻居建立关系。以下是路由器上运行的常见服务及其监听端口的列表:

服务 监听端口
FTP 21
SSH 22
TELNET 23
SMTP 25
HTTP 80
HTTPS 443
SNMP 161
BGP 179
LDP 646
RPCBIND 111
NETCONF 830
XNM-CLEAR-TEXT 3221

我们可以创建一个包含所有这些端口的字典,并使用subprocess和 Nmap 对它们进行扫描。然后我们使用返回的输出来创建我们的表,列出每次扫描的开放和关闭的端口。另外,通过一些额外的逻辑,我们可以尝试相关信息来猜测设备功能的操作系统类型。例如,如果设备正在监听端口179(BGP 端口),那么设备很可能是网络网关,如果它监听389636,那么设备正在运行 LDAP 应用程序,可能是公司的活动目录。这将帮助我们在渗透测试期间针对设备创建适当的攻击。

废话不多说,让我们快速把我们的想法和笔记放在下面的脚本中:

#!/usr/bin/python __author__ = "Bassim Aly" __EMAIL__ = "basim.alyy@gmail.com"   from prettytable import PrettyTable
import subprocess
import re

def get_port_status(port, data):
  port_status = re.findall(r"{0}/tcp (\S+) .*".format(port), data)[0]
 return port_status

Router_Table = PrettyTable(["IP Address", "Opened Services"]) router_ports = {"FTP": 21,
  "SSH": 22,
  "TELNET": 23,
  "SMTP": 25,
  "HTTP": 80,
  "HTTPS": 443,
  "SNMP": 161,
  "BGP": 179,
  "LDP": 646,
  "RPCBIND": 111,
  "NETCONF": 830,
  "XNM-CLEAR-TEXT": 3221}     live_hosts = ["10.10.10.1", "10.10.10.2", "10.10.10.65"]     services_status = {} for ip in live_hosts:
  for service, port in router_ports.iteritems():
  p = subprocess.Popen(["sudo", "nmap", "-p", str(port), ip], stdout=subprocess.PIPE)
  port_status = get_port_status(port, p.stdout.read())
  services_status[service] = port_status

    services_status_joined = "\n".join("{} : {}".format(key, value) for key, value in services_status.iteritems())    Router_Table.add_row([ip, services_status_joined])     print Router_Table

在这个例子中,我们可以看到以下内容:

  • 我们开发了一个名为get_port_status()的函数,用于获取 Nmap 端口扫描结果,并使用findall()函数内的正则表达式来搜索端口状态(打开、关闭、过滤等)。它返回端口状态结果。

  • 然后,我们在router_ports字典中添加了映射到服务名称的服务端口,这样我们就可以使用相应的服务名称(字典键)访问任何端口值。此外,我们在live_hosts列表中定义了路由器主机的 IP 地址。请注意,我们可以使用nmap-sP标志来获取活动主机,就像我们之前在之前的脚本中所做的那样。

  • 现在,我们可以遍历live_hosts列表中的每个 IP 地址,并执行 Nmap 来扫描router_ports字典中的每个端口。这需要一个嵌套的for循环,因此对于每个设备,我们都要遍历一个端口列表,依此类推。结果将被添加到services_status字典中——服务名称是字典键,端口状态是字典值。

  • 最后,我们将结果添加到使用prettytable模块创建的Router_Table中,以获得一个漂亮的表格。

脚本输出如下:

在 GitHub 上分享您的代码

GitHub 是一个可以使用 Git 分享代码并与他人合作的地方。Git 是由 Linus Trovalds 发明和创建的源代码版本控制平台,他开始了 Linux,但在许多开发人员为其做出贡献的情况下,维护 Linux 开发成为了一个问题。他创建了一个分散式版本控制,任何人都可以获取整个代码(称为克隆或分叉),进行更改,然后将其推送回中央仓库,以与其他开发者的代码合并。Git 成为许多开发人员共同合作的首选方法。您可以通过 GitHub 提供的这个 15 分钟课程交互式地学习如何在 Git 中编码:try.github.io

GitHub 是托管这些项目的网站,使用 Git 进行版本控制。它就像一个开发者社交媒体平台,您可以跟踪代码开发、编写维基百科,或提出问题/错误报告,并获得开发者的反馈。同一项目上的人可以讨论项目进展,并共同分享代码,以构建更好、更快的软件。此外,一些公司将您在 GitHub 账户中共享的代码和仓库视为在线简历,衡量您在感兴趣的语言中的技能和编码能力。

在 GitHub 上创建账户

在分享您的代码或下载其他代码之前,首先要做的是创建您的账户。

前往github.com/join?source=header-home,选择用户名、密码和电子邮件地址,然后点击绿色的“创建账户”按钮。

第二件事是选择您的计划。默认情况下,免费计划是可以的,因为它为您提供无限的公共仓库,并且您可以推送任何您喜欢的语言开发的代码。但是,免费计划不会使您的仓库私有,并允许其他人搜索和下载它。如果您不在公司中从事秘密或商业项目,这并不是一个不可接受的问题,但是您需要确保不在代码中分享任何敏感信息,如密码、令牌或公共 IP 地址。

创建和推送您的代码

现在我们准备与他人分享代码。在创建 GitHub 账户后的第一件事是创建一个仓库来托管您的文件。通常,您为每个项目创建一个仓库(而不是每个文件),它包含与彼此相关的项目资产和文件。

点击右上角的+图标,就在您的个人资料图片旁边,创建一个新的仓库:

你将被重定向到一个新页面,在这里你可以输入你的仓库名称。请注意,你可以选择任何你喜欢的名称,但它不应与你的个人资料中的其他仓库冲突。此外,你将获得一个唯一的 URL,以便任何人都可以访问它。你可以设置仓库的设置,比如它是公开的还是私有的(只适用于付费计划),以及是否要用 README 文件初始化它。这个文件使用markdown文本格式编写,包括关于你的项目的信息,以及其他开发人员使用你的项目时需要遵循的步骤。

最后,你将有一个选项来添加一个.gitignore文件,告诉 Git 在你的目录中忽略跟踪某种类型的文件,比如日志、pyc编译文件、视频等:

最后,你将创建一个仓库,并获得一个唯一的 URL。记下这个 URL,因为我们稍后将用它来推送文件:

现在是分享你的代码的时候了。我将使用 PyCharm 内置的 Git 功能来完成这项工作,尽管你也可以在 CLI 中执行相同的步骤。此外,还有许多其他可用的 GUI 工具(包括 GitHub 本身提供的工具)可以管理你的 GIT 仓库。我强烈建议你在按照这些步骤之前先接受 GitHub 提供的 Git 培训(try.github.io):

  1. 转到 VCS | Import into Version Control | Create Git Repository:

  1. 选择存储项目文件的本地文件夹:

这将在文件夹中创建一个本地的 Git 仓库。

  1. 在侧边栏中突出显示所有需要跟踪的文件,右键单击它们,然后选择 Git | Add:

PyCharm 使用文件颜色代码来指示 Git 中跟踪的文件类型。当文件没有被跟踪时,它们将被标记为红色,当文件被添加到 Git 中时,它们将被标记为绿色。这样可以让你在不运行命令的情况下轻松了解文件的状态。

  1. 通过转到 VCS | Git | Remotes 来定义在 GitHub 中将映射到本地仓库的远程仓库:

  1. 输入仓库名称和我们创建仓库时记下的 URL;点击两次“确定”退出窗口:

  1. 最后一步是提交你的代码。转到 VCS | Git | Commit,从打开的弹出窗口中选择你要跟踪的文件,在提交消息部分输入描述性消息,而不是点击提交,点击旁边的小箭头并选择提交和推送。可能会打开一个对话框,告诉你 Git 用户名未定义。只需输入你的名字和电子邮件,并确保选中“全局设置属性”框,然后点击设置和提交:

PyCharm 为你提供了将代码推送到 Gerrit 进行代码审查的选项。如果你有一个,你也可以在其中分享你的文件。否则,点击推送。

将出现一个通知消息,告诉你推送成功完成:

你可以从浏览器刷新你的 GitHub 仓库 URL,你将看到所有存储在其中的文件:

现在,每当你在跟踪文件中的代码中进行任何更改并提交时,这些更改将被跟踪并添加到版本控制系统中,并可在 GitHub 上供其他用户下载和评论。

总结

在本章中,我们构建了我们的网络扫描器,它可以在授权的渗透测试期间使用,并学习了如何扫描设备上运行的不同服务和应用程序以检测它们的类型。此外,我们将我们的代码分享到 GitHub,以便我们可以保留我们代码的不同版本,并允许其他开发人员使用我们分享的代码并增强它,然后再次与其他人分享。

posted @ 2025-09-20 21:34  绝不原创的飞龙  阅读(55)  评论(0)    收藏  举报