网络自动化秘籍-全-

网络自动化秘籍(全)

原文:zh.annas-archive.org/md5/9FD2C03E57DE97FDB15C42452017B0E9

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《网络自动化食谱》概述了网络自动化的各种主题以及如何使用软件开发实践来设计和操作不同的网络解决方案。我们使用 Ansible 作为我们的框架,介绍网络自动化的主题以及如何使用 Ansible 来管理不同厂商的设备。在第一部分中,我们概述了如何安装和配置 Ansible,专门用于网络自动化的目的。我们将探讨如何使用 Ansible 来管理来自 Cisco、Juniper、Arista 和 F5 等各种厂商的传统网络解决方案。接下来,我们将继续探讨如何利用 Ansible 来构建和扩展来自 AWS、Azure 和 Google Cloud Platform(GCP)等主要云服务提供商的网络解决方案。最后,我们概述了网络自动化中不同的支持开源项目,如 NetBox、Batfish 和 AWX。我们概述了如何将所有这些工具与 Ansible 集成,以构建一个完整的网络自动化框架。

通过本书,您将建立起如何将 Ansible 与不同厂商设备集成以及如何基于 Ansible 构建网络自动化解决方案的坚实基础。此外,您还将了解如何使用各种开源项目,并如何将所有这些解决方案与 Ansible 集成,以构建一个强大而可扩展的网络自动化框架。

本书适合对象

本书适合负责组织内部网络设备设计和操作的 IT 专业人员和网络工程师,他们希望扩展自己在使用 Ansible 自动化网络基础设施方面的知识。建议具备基本的网络和 Linux 知识。

本书内容

第一章《Ansible 的构建模块》着重介绍了如何安装 Ansible,并描述了 Ansible 的主要构建模块以及如何利用它们来构建高级的 Ansible playbook。

第二章《使用 Ansible 管理 Cisco IOS 设备》着重介绍了如何将 Ansible 与 Cisco IOS 设备集成,以及如何使用 Ansible 配置 Cisco IOS 设备。我们将探讨与 Cisco IOS 设备交互的核心 Ansible 模块。最后,我们还将探讨如何使用 Cisco PyATS 库以及如何将其与 Ansible 集成,以验证 Cisco IOS 和 Cisco IOS-XE 设备上的网络状态。

第三章《使用 Ansible 在服务提供商中自动化 Juniper 设备》描述了如何在服务提供商环境中将 Ansible 与 Juniper 设备集成,以及如何使用 Ansible 管理 Juniper 设备的配置。我们将探讨如何使用核心的 Ansible 模块来管理 Juniper 设备。此外,我们还将探讨 PyEZ 库,该库被 Juniper 定制的 Ansible 模块使用,以扩展 Ansible 在管理 Juniper 设备方面的功能。

第四章《使用 Arista 和 Ansible 构建数据中心网络》概述了如何将 Ansible 与 Arista 设备集成,以使用 EVPN/VXLAN 构建数据中心网络。我们将探讨如何使用核心的 Ansible 模块来管理 Arista 设备,以及如何使用这些模块来配置和验证 Arista 交换机上的网络状态。

第五章《使用 F5 LTM 和 Ansible 自动化应用交付》着重介绍了如何将 Ansible 与 F5 BIG-IP LTM 设备集成,以便为应用交付新的 BIG-IP LTM 设备,并将 BIG-IP 系统设置为应用交付的反向代理。

第六章《使用 NAPALM 和 Ansible 管理多厂商网络》介绍了 NAPALM 库,并概述了如何将该库与 Ansible 集成。我们将探讨如何利用 Ansible 和 NAPALM 来简化多厂商环境的管理。

第七章《使用 Ansible 部署和操作 AWS 网络资源》概述了如何将 Ansible 与您的 AWS 环境集成,以及如何使用 Ansible 描述您的 AWS 基础设施。我们探讨了如何利用核心 Ansible AWS 模块来管理 AWS 中的网络资源,以便使用 Ansible 构建 AWS 网络基础设施。

第八章《使用 Ansible 部署和操作 Azure 网络资源》概述了如何将 Ansible 与您的 Azure 环境集成,以及如何使用 Ansible 描述您的 Azure 基础设施。我们将探讨如何利用核心 Ansible Azure 模块来管理 Azure 中的网络资源,以便使用 Ansible 构建 Azure 网络解决方案。

第九章《使用 Ansible 部署和操作 GCP 网络资源》描述了如何将 Ansible 与您的 GCP 环境集成,以及如何使用 Ansible 描述您的 GCP 基础设施。我们探讨了如何利用核心 Ansible GCP 模块来管理 GCP 中的网络资源,以便使用 Ansible 构建 GCP 网络解决方案。

第十章《使用 Batfish 和 Ansible 进行网络验证》介绍了离线网络验证的 Batfish 框架以及如何将该框架与 Ansible 集成,以便使用 Ansible 和 Batfish 进行离线网络验证。

第十一章《使用 Ansible 和 NetBox 构建网络清单》介绍了 NetBox,这是一个完整的清单系统,用于记录和描述任何网络。我们概述了如何将 Ansible 与 NetBox 集成,以及如何使用 NetBox 数据构建 Ansible 动态清单。

第十二章《使用 AWX 和 Ansible 简化自动化》介绍了 AWX 项目,它扩展了 Ansible,并在 Ansible 之上提供了强大的 GUI 和 API,以简化组织内运行自动化任务。我们概述了 AWX 提供的额外功能以及如何使用它来管理组织内的网络自动化。

第十三章《Ansible 的高级技术和最佳实践》描述了可用于更高级 playbook 的各种最佳实践和高级技术。

为了充分利用本书

假定您具有关于不同网络概念的基本知识,例如开放最短路径优先OSPF)和边界网关协议BGP)。

假定您具有 Linux 的基本知识,包括如何在 Linux 机器上创建文件和文件夹以及安装软件的知识。

本书涵盖的软件/硬件 操作系统要求
Ansible 2.9 CentOS 7
Python 3.6.8

如果您使用的是本书的数字版本,我们建议您自己输入代码或通过 GitHub 存储库访问代码(链接在下一节中提供)。这样做将有助于避免与复制和粘贴代码相关的任何潜在错误。

下载示例代码文件

您可以从您在www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support注册,以便直接通过电子邮件接收文件。

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

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

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

  3. 单击“代码下载”。

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

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

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

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

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

下载彩色图片

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

代码演示

代码演示视频基于 Ansible 版本 2.8.5。该代码还经过了 2.9.2 版本的测试,可以正常运行。

访问以下链接,查看代码运行的视频:

bit.ly/34JooNp

惯例使用

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

CodeInText:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄。这是一个例子:“将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。”

一段代码设置如下:

$ cat ansible.cfg

[defaults]
 inventory=hosts
 retry_files_enabled=False
 gathering=explicit
 host_key_checking=False

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

- name: Configure ACL on IOS-XR
  hosts: all
  serial: 1
  tags: deploy
  tasks:
    - name: Backup Config
      iosxr_config:
        backup:
 when: not ansible_check_mode    - name: Deploy ACLs
      iosxr_config:
        src: acl_conf.cfg
        match: line
 when: not ansible_check_mode

任何命令行输入或输出都写成如下形式:

$ python3 -m venv dev
$ source dev/bin/activate

粗体:表示一个新术语,一个重要单词,或者您在屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“从管理面板中选择系统信息。”

警告或重要说明会出现在这样的地方。提示和技巧会出现在这样的地方。

章节

在本书中,您会经常看到几个标题(准备工作如何做...它是如何工作的...还有更多...另请参阅)。

为了清晰地说明如何完成一个配方,使用以下各节:

准备工作

本节告诉您在配方中可以期待什么,并描述如何设置配方所需的任何软件或初步设置。

如何做...

本节包含了遵循配方所需的步骤。

它是如何工作的...

本节通常包括对上一节发生的事情的详细解释。

还有更多...

本节包含有关配方的其他信息,以使您对配方更加了解。

另请参阅

本节提供了有用的链接,指向配方的其他有用信息。

第一章:Ansible 的构建块

Ansible 是一个非常受欢迎的自动化框架,长期以来一直被用于自动化 IT 运营。它简化了不同基础设施节点的管理,并将业务逻辑转化为明确定义的程序,以实现这些业务逻辑。Ansible 是用 Python 编写的,主要依赖 SSH 与基础设施节点通信以执行指令。它从 Ansible 1.9 开始支持网络设备,并且随着 Ansible 2.9 的到来,它对网络设备的支持已经得到了大幅增强。它可以使用 SSH 或者如果网络供应商在其设备上支持 API 的话,还可以通过 API 与网络设备进行交互。它还提供了多个优势,包括以下内容:

  • 易学习曲线:编写 Ansible Playbooks 需要了解 YAML 和 Jinja2 模板,这些都很容易学习,其描述性语言易于理解。

  • 无需代理:无需在远程管理设备上安装代理即可控制该设备。

  • 可扩展:Ansible 配备了多个模块,可在受控节点上执行各种任务。它还支持编写自定义模块和插件,以扩展 Ansible 的核心功能。

  • 幂等性:除非需要改变设置以达到期望的状态,否则 Ansible 不会改变设备的状态。一旦设备处于期望的状态,运行 Ansible Playbooks 不会改变其配置。

在本章中,我们将介绍 Ansible 的主要组件,并概述 Ansible 支持的不同功能和选项。以下是将涵盖的主要内容:

  • 安装 Ansible

  • 构建 Ansible 的清单

  • 使用 Ansible 的变量

  • 构建 Ansible 的 Playbook

  • 使用 Ansible 的条件语句

  • 使用 Ansible 的循环

  • 使用 Ansible Vault 保护秘密

  • 使用 Jinja2 与 Ansible

  • 使用 Ansible 的过滤器

  • 使用 Ansible 标签

  • 定制 Ansible 的设置

  • 使用 Ansible 角色

本章的目的是基本了解我们将在本书中利用的不同 Ansible 组件,以便与网络设备进行交互。因此,本章中的所有示例都不是专注于管理网络设备。相反,我们将专注于理解 Ansible 中的不同组件,以便在下一章中有效地使用它们。

技术要求

以下是安装 Ansible 和运行所有 Ansible Playbooks 的要求:

  • 具有以下发行版之一的 Linux 虚拟机VM):

  • Ubuntu 18.04 或更高版本

  • CentOS 7.0 或更高版本

  • 虚拟机的互联网连接

设置 Linux 机器超出了本教程的范围。然而,使用Vagrant创建和设置 Ansible 虚拟机是设置具有任何操作系统版本的 Linux 虚拟机的最简单方法。

安装 Ansible

安装 Ansible 的机器(称为 Ansible 控制机)应该运行在任何 Linux 发行版上。在本教程中,我们将概述如何在 Ubuntu Linux 机器或 CentOS 机器上安装 Ansible。

准备工作

要安装 Ansible,我们需要使用 Ubuntu 18.04+或 CentOS 7+操作系统的 Linux 虚拟机。此外,这台机器需要有互联网访问权限才能安装 Ansible。

如何做...

Ansible 是用 Python 编写的,所有模块都需要在 Ansible 控制机上安装 Python。我们的第一个任务是确保 Python 已安装在 Ansible 控制机上,如下所述。

  1. 大多数 Linux 发行版默认安装了 Python。但是,如果 Python 未安装,以下是在 Linux 上安装它的步骤:
    • 在 Ubuntu 操作系统上,执行以下命令:
# Install python3
$sudo apt-get install python3

# validate python is installed 
$python3 --version
Python 3.6.9
    • 在 CentOS 操作系统上,执行以下命令:
# Install python
$sudo yum install pytho3

# validate python is installed 
$python3 --version
Python 3.6.8
  1. 在验证了 Python 已安装后,我们可以开始安装 Ansible:
    • 在 Ubuntu 操作系统上,执行以下命令:
# We need to use ansible repository to install the latest version of Ansible
$ sudo apt-add-repository ppa:ansible/ansible

# Update the repo cache to use the new repo added
$ sudo apt-get update

# We install Ansible
$ sudo apt-get install ansible
    • 在 CentOS 操作系统上,执行以下命令:
# We need to use latest epel repository to get the latest ansible 
$ sudo yum install epel-release

# We install Ansible
$ sudo yum install ansible

它是如何工作的...

安装 Ansible 的最简单方法是使用特定于我们的 Linux 发行版的软件包管理器。我们只需要确保已启用所需的存储库以安装最新版本的 Ansible。在 Ubuntu 和 CentOS 中,我们需要启用提供最新版本 Ansible 的额外存储库。在 CentOS 中,我们需要安装和启用企业 Linux 额外软件包EPEL)存储库,该存储库提供额外的软件包,并为 CentOS 提供最新的 Ansible 软件包。

使用此方法,我们将安装 Ansible 和运行 Ansible 模块所需的所有必需系统软件包。在 Ubuntu 和 CentOS 中,此方法还将安装 Python 2 并使用 Python 2 运行 Ansible。我们可以通过运行以下命令验证 Ansible 已安装并使用的版本:

$ ansible --version
ansible 2.9
 config file = /etc/ansible/ansible.cfg
 configured module search path = [u'/home/vagrant/.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 7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

此外,我们可以通过尝试使用ping模块连接到本地机器来检查 Ansible 是否按预期工作,如下所示:

$ ansible -m ping localhost

localhost | SUCCESS => {
 "changed": false,
 "ping": "pong"
}

使用此方法,我们可以看到它存在以下问题:

  • 它使用 Python 2 作为执行环境,但我们想使用 Python 3。

  • 它更新了系统上安装的 Python 软件包,这可能是不可取的。

  • 它不提供我们所需的细粒度,以便选择要使用的 Ansible 版本。使用此方法,我们将始终安装最新版本的 Ansible,这可能不是我们所需要的。

它是如何工作的...

为了在 Python 3 环境中安装 Ansible 并对部署的 Ansible 版本进行更多控制,我们将使用 pip Python 程序来安装 Ansible,如下所示:

  • 如果尚未安装 Python 3,请按以下方式安装:
# Ubuntu
$ sudo apt-get install python3

# CentOS
sudo yum install python3
  • 安装python3-pip软件包:
# Ubuntu
$ sudo apt-get install python3-pip

# CentOS
$ sudo yum install python3-pip
  • 安装 Ansible:
# Ubuntu and CentOS
# This will install ansible for the current user ONLY
$ pip3 install ansible==2.9 --user

# We Can install ansible on the System Level
$ sudo pip3 install ansible==2.9
  • 我们可以验证 Ansible 已成功安装,如下所示:
$$ ansible --version
ansible 2.9
 config file = None
 configured module search path = ['/home/vagrant/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
 ansible python module location = /home/vagrant/.local/lib/python3.6/site-packages/ansible
 executable location = /home/vagrant/.local/bin/ansible
 python version = 3.6.8 (default, Aug 7 2019, 17:28:10) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

使用此方法安装 Ansible 可确保我们使用 Python 3 作为执行环境,并允许我们控制要安装的 Ansible 版本,如所示的示例。

我们将使用此方法作为我们的 Ansible 安装方法,并且所有后续章节将基于此安装过程。

在第十三章中,Ansible 的高级技术和最佳实践,我们将概述使用 Python 虚拟环境安装 Ansible 的另一种方法。

另请参阅...

有关安装 Ansible 的更多信息,请查看以下网址:

docs.ansible.com/ansible/latest/installation_guide/intro_installation.html

构建 Ansible 的清单

安装完 Ansible 后,我们需要定义 Ansible 的清单,这是一个定义 Ansible 将管理的节点的文本文件。在本教程中,我们将概述如何创建和构造 Ansible 的清单文件。

准备工作

我们需要创建一个包含本章中将概述的所有代码的文件夹。我们创建一个名为ch1_ansible的文件夹,如下所示:

$ mkdir ch1_ansible
$ cd ch1_ansible

如何做...

执行以下步骤创建清单文件:

  1. 创建名为hosts的文件:
$ touch hosts
  1. 使用任何文本编辑器打开文件并添加以下内容:
$ cat hosts

[cisco]
csr1 ansible_host=172.10.1.2
csr2 ansible_host=172.10.1.3

[juniper]
mx1 ansible_host=172.20.1.2
mx2 ansible_host=172.20.1.3

[core]
mx1
mx2

[edge]
csr[1:2]

[network:children]
core
edge

Ansible 清单文件可以有任何名称。但是,作为最佳实践,我们将使用名称hosts来描述清单中的设备。

它是如何工作的...

Ansible 清单文件定义了将由 Ansible 管理的主机(在上面的示例中,这是csr1-2mx1-2),以及如何根据不同标准将这些设备分组到自定义定义的组中。组用[]定义。这种分组有助于我们定义变量并简化设备之间的分隔以及 Ansible 与它们的交互。我们如何分组设备是基于我们的用例的,因此我们可以根据供应商(Juniper 和 IOS)或功能(核心和边缘)对它们进行分组。

我们还可以使用在清单文件中概述的 children 为组构建层次结构。以下图显示了主机是如何分组的以及组层次结构是如何构建的:

使用 Ansible 的变量

Ansible 使用 Ansible 变量存储其管理的节点的信息。Ansible 变量可以在多个位置声明。然而,为了遵循 Ansible 的最佳实践,我们将概述 Ansible 寻找用于在清单文件中声明的节点的变量的两个主要部分。

准备工作

为了按照这个步骤进行,必须已经按照之前的步骤定义了 Ansible 清单文件。

操作步骤

在清单文件中,我们定义主机并将主机分组。现在我们定义了 Ansible 搜索组变量和主机变量的两个目录:

  1. 创建两个文件夹,group_varshost_vars
$ cd ch1_ansible $ mkdir group_vars host_vars
  1. group_vars内创建ios.ymljunos.yml文件:
$ touch group_vars/cisco.yml group_vars/juniper.yml
  1. host_vars内创建mx1.ymlcsr1.yml
$ touch host_vars/csr1.yml host_vars/mx1.yml
  1. 填充所有文件中的变量,如下所示:
$echo 'hostname: core-mx1' >> host_vars/mx1.yml
$echo 'hostname: core-mx2' >> host_vars/mx2.yml
$echo 'hostname: edge-csr1' >> host_vars/csr1.yml
$echo 'hostname: edge-csr2' >> host_vars/csr2.yml
$echo 'os: ios' >> group_vars/cisco.yml
$echo 'os: junos' >> group_vars/juniper.yml

工作原理

我们创建了以下目录和文件结构来存储我们的变量,如下图所示:

group_vars目录内的所有文件包含我们在清单中定义的组的组变量,并且适用于该组内的所有主机。至于host_vars内的文件,它们包含每个主机的变量。使用这种结构,我们可以将多个主机的变量分组到一个特定的组文件中,并且特定于主机的变量将被放在一个特定于该主机的单独文件中。

还有更多...

除了host_varsgroup_vars,Ansible 还支持使用其他技术定义变量,包括以下内容:

  • 在 play 中使用vars关键字指定多个变量

  • 使用vars_files在文件中定义变量,并在运行 playbook 时让 Ansible 从该文件中读取这些变量

  • 使用--e选项在命令行指定变量

除了我们可以指定的用户定义变量之外,Ansible 还有一些默认变量,它会动态构建用于清单的变量。以下表格捕获了一些最常用的变量:

inventory_hostname 主机在清单中定义的名称(例如,csr1mx1
play_hosts play 中包含的所有主机的列表
group_names 特定主机所属的所有组的列表(例如,对于csr1,这将是[edge,Cisco,network])

构建 Ansible 的 playbook

Ansible playbook 是 Ansible 中声明我们想要在我们管理的主机上执行的操作的基本元素。Ansible playbook 是一个以 YAML 格式编写的文件,定义了将在我们管理的设备上执行的任务列表。在这个步骤中,我们将概述如何编写一个 Ansible playbook 以及如何定义将被此 playbook 定位的主机。

准备工作

为了按照这个步骤进行,必须已经定义了 Ansible 清单文件,并根据之前的步骤创建了所有组和主机特定的变量文件。

操作步骤

  1. ch1_ansible文件夹内创建一个名为playbook.yml的新文件,并在此文件中加入以下行:
$  cat playbook.yml

---
 - name: Initial Playbook
 hosts: all
 gather_facts: no
 tasks:
 - name: Display Hostname
 debug:
 msg: "Router name is {{ hostname }}"
 - name: Display OS
 debug:
 msg: "{{ hostname }} is running {{ os }}"
  1. 按照以下步骤运行 playbook:
$ ansible-playbook -i hosts playbook.yml

工作原理

Ansible playbook 是一系列 play 的列表,每个 play 针对清单文件中定义的特定主机组。每个 play 可以有一个或多个任务针对此 play 中的主机执行。每个任务运行一个特定的 Ansible 模块,该模块有一些参数。playbook 的一般结构如下截图所示:

在上述 playbook 中,我们在{{ }}括号中引用了我们在前面步骤中定义的变量。Ansible 从group_varshost_vars中读取这些变量,我们在这个 playbook 中使用的模块是debug模块,它将在终端输出中显示为msg参数中指定的自定义消息。playbook 运行如下所示:

我们在ansible-playbook命令中使用-i选项来指向 Ansible 清单文件,这将作为我们构建清单的源。

在这个 playbook 中,我使用了all关键字来指定清单中的所有主机。这是 Ansible 为清单中的所有主机动态构建的一个众所周知的组名。

使用 Ansible 的条件语句

Ansible 的核心功能之一是条件任务执行。这为我们提供了根据我们指定的条件/测试来控制在给定主机上运行哪些任务的能力。在这个步骤中,我们将概述如何配置条件任务执行。

准备工作

为了按照这个步骤进行操作,必须存在一个已配置的 Ansible 清单文件,如前面的步骤所述。此外,所有主机的 Ansible 变量应该按照前面的步骤所述进行定义。

如何做...

  1. ch1_ansible文件夹中创建一个名为ansible_cond.yml的新 playbook。

  2. 按照这里所示在新的 playbook 中加入以下内容:

---
 - name: Using conditionals
 hosts: all
 gather_facts: no
 tasks:
 - name: Run for Edge nodes Only
 debug:
 msg: "Router name is {{ hostname }}"
 when: "'edge' in group_names"

 - name: Run for Only MX1 node
 debug:
 msg: "{{ hostname }} is running {{ os }}"
 when:
 - inventory_hostname == 'mx1'
  1. 按照这里所示运行 playbook:
$ ansible-playbook -i hosts ansible_cond.yml

它是如何工作的...

Ansible 使用when语句为任务提供条件执行。when语句应用于任务级别,如果when语句中的条件评估为true,则任务将针对给定的主机执行。如果为false,则该任务将跳过该主机。运行上述 playbook 的输出如下所示:

when语句可以像第一个任务中那样接受一个条件,也可以像第二个任务中那样接受一个条件列表。如果when是一个条件列表,所有条件都需要为真才能执行任务。

在第一个任务中,when语句用""括起来,因为语句以字符串开头**。然而,在第二个语句中,我们使用了一个普通的when语句,没有"",因为when语句以变量名开头。

另请参阅...

有关 Ansible 条件语句的更多信息,请查看以下网址:

docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html

使用 Ansible 的循环

在某些情况下,我们需要在 Ansible playbook 中运行一个任务来循环处理一些数据。Ansible 的循环允许我们多次循环遍历一个变量(字典或列表)以实现这种行为。在这个步骤中,我们将概述如何使用 Ansible 的循环。

准备工作

为了按照这个步骤进行操作,必须存在一个已配置的 Ansible 清单文件,如前面的步骤所述。

如何做...

  1. ch1_ansible文件夹中创建一个名为ansible_loops.yml的新 playbook。

  2. group_vars/cisco.yml文件中,加入以下内容:

snmp_servers:
 - 10.1.1.1
 - 10.2.1.1
  1. group_vars/juniper.yml文件中,加入以下内容:
users:
 admin: admin123
 oper: oper123
  1. ansible_loops.yml文件中,加入以下内容:
---
 - name: Ansible Loop over a List
 hosts: cisco
 gather_facts: no
 tasks:
 - name: Loop over SNMP Servers
 debug:
 msg: "Router {{ hostname }} with snmp server {{ item }}"
 loop: "{{ snmp_servers}}"

 - name: Ansible Loop over a Dictionary
 hosts: juniper
 gather_facts: no
 tasks:
 - name: Loop over Username and Passowrds
 debug:
 msg: "Router {{ hostname }} with user {{ item.key }} password {{ item.value }}"
 with_dict: "{{ users}}"
  1. 按照这里所示运行 playbook:
$ ansible-playbook ansible_loops.yml -i hosts

它是如何工作的..

Ansible 支持对两种主要的可迭代数据结构进行循环:列表和字典。当我们需要对列表进行迭代时(snmp_servers是一个列表数据结构),我们使用loops关键字,当我们循环遍历字典时(users是一个字典数据结构,其中用户名是键,密码是值),我们使用with_dicts。在这两种情况下,我们使用item关键字来指定当前迭代中的值。在with_dicts的情况下,我们使用item.key来获取键,使用item.value来获取值。

以下是前面剧本运行的输出:

另请参阅...

有关不同的 Ansible循环结构的更多信息,请参考以下网址:

docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html

使用 Ansible Vault 保护秘密

当我们处理需要在 Ansible 剧本中引用的敏感材料时,比如密码,我们不应该将这些数据保存为纯文本。Ansible Vault 提供了一种加密这些数据并在运行剧本时安全解密和访问的方法。在这个示例中,我们将概述如何使用 Ansible Vault 来保护 Ansible 中的敏感信息。

如何做...

  1. 创建一个名为decrypt_passwd的新文件,如下所示:
$ echo 'strong_password' > decrypt_passwd
  1. 使用ansible-vault创建一个名为secrets的新文件,如下所示:
$ ansible-vault create --vault-id=decrypt_passwd secrets
  1. 将以下变量添加到这个新的secrets文件中:
ospf_password: ospf_P@ssw0rD
bgp_password: BGP_p@ssw0rd
  1. 创建一个名为ansible_vault.yml的新剧本,如下所示:
---
 - name: Using Ansible vault
 hosts: all
 gather_facts: no
 vars_files:
 - secrets
 tasks:
 - name: Output OSPF passowrd
 debug:
 msg: "Router {{ hostname }} ospf Password {{ ospf_password }}"
 when: inventory_hostname == 'csr1'

 - name: Output BGP passowrd
 debug:
 msg: "Router {{ hostname }} BGP Password {{ bgp_password }}"
 when: inventory_hostname == 'mx1'
  1. 按照这里所示的方式运行剧本:
$ ansible-playbook --vault-id=decrypt_passwd ansible_vault.yml -i hosts

它是如何工作的..

我们使用ansible-vault命令创建一个使用--vault-id指定的密钥加密的新文件。我们将这个密钥/密码放在另一个文件中(在我们的示例中称为decrypt_passwd),并将此文件作为vault-id的参数传递。在这个文件中,我们可以放置尽可能多的变量。最后,我们使用vars_files将这个文件作为变量文件包含在剧本中。如果我们尝试在没有解密的情况下读取秘密文件,其内容如下:

$ cat secrets
$ANSIBLE_VAULT;1.1;AES256
61383264326363373336383839643834386661343630393965656135666336383763343938313963
3538376230613534323833356237663532666363626462640a663839396230646634353839626461
31336461386361616261336534663137326265363261626536663564623764663861623735633865
3033356536393631320a643561623635363830653236633833383531366166326566623139633838
32633335616663623761313630613134636635663865363563366564313365376431333461623232
34633838333836363865313238363966303466373065356561353638363731616135386164373263
666530653334643133383239633237653034

为了让 Ansible 解密这个文件,我们必须通过--vault-id选项提供解密密码(在这个示例中存储在decrypt_passwd文件中)。当我们运行ansible-playbook时,必须提供这个解密密码,否则ansible-playbook会失败,如下所示:

### Running the Ansible playbook without --vault-id 
$ansible-playbook ansible_vault.yml -i hosts
ERROR! Attempting to decrypt but no vault secrets found

还有更多...

如果我们不想在文本文件中指定加密/解密密码,我们可以在运行剧本时使用--ask-vault-passansible-playbook命令一起输入密码,如下所示:

### Running the Ansible playbook with --ask-vault-pass
$ansible-playbook ansible_vault.yml -i hosts --ask-vault-pass
Vault password:

使用 Jinja2 与 Ansible

Jinja2 是 Python 的一个强大的模板引擎,受到 Ansible 的支持。它还用于生成任何基于文本的文件,如 HTML、CSV 或 YAML。我们可以利用 Ansible 变量来使用 Jinja2 生成网络设备的自定义配置文件。在这个示例中,我们将概述如何在 Ansible 中使用 Jinja2 模板。

准备工作

为了按照这个示例进行操作,必须存在并按照前面的示例配置好 Ansible 清单文件。

如何做...

  1. group_vars目录中创建一个名为network.yml的新文件:
$ cat group_vars/network.yml

---
ntp_servers:
 - 172.20.1.1
 - 172.20.2.1
  1. 创建一个新的templates目录,并创建一个名为ios_basic.j2的新文件,内容如下:
$ cat templates/ios_basic.j2
hostname {{ hostname }}
!
{% for server in ntp_servers %}
ntp {{ server }}
{% endfor %}
!
  1. templates目录中创建一个名为junos_basic.j2的新文件,内容如下:
$ cat templates/junos_basic.j2
set system host-name {{ hostname }}
{% for server in ntp_servers %}
set system ntp server {{ server }}
{% endfor %}
  1. 创建一个名为ansible_jinja2.yml的新剧本,内容如下:
---
 - name: Generate Cisco config from Jinja2
 hosts: localhost
 gather_facts: no
 tasks:
 - name: Create Configs Directory
 file: path=configs state=directory

 - name: Generate Cisco config from Jinja2
 hosts: cisco
 gather_facts: no
 tasks:
 - name: Generate Cisco Basic Config
 template:
 src: "templates/ios_basic.j2"
 dest: "configs/{{inventory_hostname}}.cfg"
 delegate_to: localhost

 - name: Generate Juniper config from Jinja2
 hosts: juniper
 gather_facts: no
 tasks:
 - name: Generate Juniper Basic Config
 template:
 src: "templates/junos_basic.j2"
 dest: "configs/{{inventory_hostname}}.cfg"
 delegate_to: localhost
  1. 按照这里所示的方式运行 Ansible 剧本:
$ ansible-playbook -i hosts ansible_jinja2.yml

它是如何工作的...

我们创建了network.yml文件,以便将适用于该组下所有设备的所有变量进行分组。之后,我们创建了两个 Jinja2 文件,一个用于 Cisco IOS 设备,另一个用于 Juniper 设备。在每个 Jinja2 模板中,我们使用{{}}引用 Ansible 变量。我们还使用 Jinja2 模板引擎支持的for循环构造{% for server in ntp_servers %},以循环遍历ntp_servers变量(这是一个列表),以访问此列表中的每个项目。

Ansible 提供了template模块,它有两个参数:

  • src:这引用了 Jinja2 模板文件。

  • dest:这指定将生成的输出文件。

在我们的情况下,我们使用{{inventory_hostname}}变量,以使输出配置文件对我们清单中的每个路由器都是唯一的。

默认情况下,template模块在远程管理节点上创建输出文件。但是,在我们的情况下,由于受管设备是网络节点,这是不可能的。因此,我们使用delegate_to: localhost选项,以便在 Ansible 控制机上本地运行此任务。

playbook 中的第一个 play 创建configs目录,用于存储网络设备的配置文件。第二个 play 在 Cisco 设备上运行模板模块,第三个 play 在 Juniper 设备上运行template任务。

以下是 Cisco 设备的配置文件:

$ cat configs/csr1.cfg
hostname edge-csr1
!
ntp 172.20.1.1
ntp 172.20.2.1
!

这是 Juniper 设备的配置文件:

$ cat configs/mx1.cfg
set system host-name core-mx1
set system ntp server 172.20.1.1
set system ntp server 172.20.2.1

另请参阅...

有关 Ansible 模板模块的更多信息,请参考以下网址:

docs.ansible.com/ansible/latest/modules/template_module.html

使用 Ansible 的过滤器

Ansible 的过滤器主要源自 Jinja2 过滤器,所有 Ansible 过滤器都用于转换和操作数据(Ansible 的变量)。除了 Jinja2 过滤器外,Ansible 还实现了自己的过滤器来增强 Jinja2 过滤器,同时还允许用户定义自己的自定义过滤器。在本教程中,我们将概述如何配置和使用 Ansible 过滤器来操作我们的输入数据。

如何做...

  1. 安装python3-pip和 Python 的netaddr库,因为我们将使用需要 Python 的netaddr库的 Ansible IP 过滤器:
# On ubuntu
$ sudo apt-get install python3-pip

# On CentOS
$ sudo yum install python3-pip

$ pip3 install netaddr
  1. 创建一个名为ansible_filters.yml的新 Ansible playbook,如下所示:
---
 - name: Ansible Filters
 hosts: csr1
 vars:
 interfaces:
 - { port: FastEthernet0/0, prefix: 10.1.1.0/24 }
 - { port: FastEthernet1/0, prefix: 10.1.2.0/24 }
 tasks:
 - name: Generate Interface Config
 blockinfile:
 block: |
 hostname {{ hostname | upper }}
 {% for intf in interfaces %}
 !
 interface {{ intf.port }}
 ip address {{intf.prefix | ipv4(1) | ipv4('address') }} {{intf.prefix | ipv4('netmask') }}
 !
 {% endfor %}
 dest: "configs/csr1_interfaces.cfg"
 create: yes
 delegate_to: localhost

工作原理...

首先,我们使用blockinfile模块在 Ansible 控制机上创建一个新的配置文件。此模块与template模块非常相似。但是,我们可以直接在block选项中编写 Jinja2 表达式。我们在 playbook 中使用vars参数定义了一个名为interfaces的新变量。此变量是一个列表数据结构,其中列表中的每个项目都是一个字典数据结构。此嵌套数据结构指定了每个接口上使用的 IP 前缀。

在 Jinja2 表达式中,我们可以看到我们使用了一些过滤器,如下所示:

  • {{ hostname | upper}}upper是一个 Jinja2 过滤器,将输入字符串的所有字母转换为大写。通过这种方式,我们将主机名变量的值传递给此过滤器,输出将是此值的大写版本。

  • {{intf.prefix | ipv4(1) | ipv4('address') }}:在这里,我们两次使用了 Ansible IP 地址过滤器。ipv4(1)接受输入 IP 前缀并输出此前缀中的第一个 IP 地址。然后,我们使用另一个 IP 地址过滤器ipv4('address'),以便仅获取 IP 前缀的 IP 地址部分。因此,在我们的情况下,我们取10.1.1.0/24,我们输出第一个接口的10.1.1.1

  • {{intf.prefix | ipv4('netmask') }}:在这里,我们使用 Ansible IP 地址过滤器来获取 IP 地址前缀的子网掩码,因此在我们的情况下,我们得到/24子网并将其转换为255.255.255.0

此 playbook 运行后,csr1路由器的输出文件如下所示:

$ cat configs/csr1_interfaces.cfg
# BEGIN ANSIBLE MANAGED BLOCK
hostname EDGE-CSR1
!
interface FastEthernet0/0
 ip address 10.1.1.1 255.255.255.0
!
!
interface FastEthernet1/0
 ip address 10.1.2.1 255.255.255.0
!
# END ANSIBLE MANAGED BLOCK

使用 Ansible 标签

Ansible 标签是一个强大的工具,允许我们在大型 Ansible playbook 中为特定任务打标签,并灵活选择基于我们指定的标签在给定 playbook 中运行哪些任务。在这个示例中,我们将概述如何配置和使用 Ansible 标签。

操作步骤...

  1. 创建一个名为ansible_tags.yml的新的 Ansible playbook,如下所示:
---
 - name: Using Ansible Tags
 hosts: cisco
 gather_facts: no
 tasks:
 - name: Print OSPF
 debug:
 msg: "Router {{ hostname }} will Run OSPF"
 tags: [ospf, routing]

 - name: Print BGP
 debug:
 msg: "Router {{ hostname }} will Run BGP"
 tags:
 - bgp
 - routing

 - name: Print NTP
 debug:
 msg: "Router {{ hostname }} will run NTP"
 tags: ntp
  1. 按照以下示例运行 playbook:
$ ansible-playbook ansible_tags.yml -i hosts --tags routing
  1. 再次运行 playbook,这次使用标签,如下所示:
$ ansible-playbook ansible_tags.yml -i hosts --tags ospf

$ ansible-playbook ansible_tags.yml -i hosts --tags routing

工作原理...

我们可以使用标签来标记具有特定标签的任务和 play,以控制执行哪些任务或 play。这在开发 playbook 时给予我们更多的控制,允许我们运行相同的 playbook,但每次运行时都可以控制我们部署的内容。在这个示例中的 playbook 中,我们已经将任务标记为 OSPF、BGP 或 NTP,并将routing标签应用到了 OSPF 和 BGP 任务。这允许我们有选择性地运行 playbook 中的任务,如下所示:

  • 如果没有指定标签,这将运行 playbook 中的所有任务,行为不会改变,如下截图所示:

  • 使用ospf标签,我们将只运行标记有这个标签的任何任务,如下所示:

  • 使用routing标签,我们将运行所有标记有这个标签的任务,如下所示:

另请参阅...

有关 Ansible 标签的更多信息,请参考以下 URL:

docs.ansible.com/ansible/latest/user_guide/playbooks_tags.html

自定义 Ansible 的设置

Ansible 有许多设置可以通过一个名为ansible.cfg的配置文件进行调整和控制。这个文件有多个选项,控制着 Ansible 的许多方面,包括 Ansible 的外观和它如何连接到受控设备。在这个示例中,我们将概述如何调整一些默认设置。

操作步骤...

  1. 创建一个名为ansible.cfg的新文件,如下所示:
[defaults]
inventory=hosts
vault_password_file=decryption_password
gathering=explicit

工作原理...

默认情况下,Ansible 的设置由位于/etc/ansible目录中的ansible.cfg文件控制。这是 Ansible 的默认配置文件,控制着 Ansible 与受控节点的交互方式。我们可以直接编辑这个文件。但是,这将影响到我们在 Ansible 控制机上使用的任何 playbook,以及这台机器上的任何用户。一个更灵活和定制的选项是在项目目录中包含一个名为ansible.cfg的文件,其中包含了需要从默认参数中修改的所有选项。在上面的示例中,我们只概述了其中的一小部分选项。

  • inventory: 这个选项修改了 Ansible 搜索的默认清单文件,以便找到它的清单(默认情况下,这是/etc/ansible/hosts)。我们调整这个选项,让 Ansible 使用我们的清单文件,并在每次运行 playbook 时停止使用-i操作符来指定我们的清单。

  • vault_password_file:这个选项设置了用于加密和解密ansible-vault密码的秘密密码文件。这个选项消除了在使用ansible-vault加密变量时需要使用--vault-id操作符运行 Ansible playbook 的需要。

  • gathering = explicit:默认情况下,Ansible 在运行 playbook 时运行一个设置模块来收集有关受控节点的事实。由于这个设置模块需要受控节点上的 Python 解释器,因此这个设置模块与网络节点不兼容。通过将事实收集设置为explicit,我们禁用了这个默认行为。

另请参阅...

有关 Ansible 配置设置的更多信息,请参考以下 URL:

docs.ansible.com/ansible/latest/reference_appendices/config.html#ansible-configuration-settings

使用 Ansible 角色

Ansible 角色促进了代码的可重用性,并提供了一种简单的方法来打包 Ansible 代码,以便可以共享和使用。Ansible 角色是所有所需的 Ansible 任务、处理程序和 Jinja2 模板的集合,它们以特定的结构打包在一起。角色应设计为提供特定的功能/任务。在这个示例中,我们将概述如何创建一个 Ansible 角色以及如何在我们的 playbooks 中使用它。

操作步骤...

  1. ch1_ansible文件夹中,创建一个名为roles的新文件夹,并创建一个名为basic_config的新角色,如下所示:
$ mkdir roles
$ cd roles
$ ansible-galaxy init basic_config
  1. 使用以下变量更新basic_config/vars/main.yml文件:
$ cat roles/basic_config/vars/main.yml

---
config_dir: basic_config
  1. 使用以下任务更新basic_config/tasks/main.yml文件:
$ cat roles/basic_config/tasks/main.yml

---
 - name: Create Configs Directory
 file:
 path: "{{ config_dir }}"
 state: directory
 run_once: yes

 - name: Generate Cisco Basic Config
 template:
 src: "{{os}}.j2"
 dest: "{{config_dir}}/{{inventory_hostname}}.cfg"
  1. basic_config/templates文件夹内,创建以下结构:
$ tree roles/basic_config/templates/

roles/basic_config/templates/
├── ios.j2
└── junos.j2

$ cat roles/basic_config/templates/ios.j2
hostname {{ hostname }}
!
{% for server in ntp_servers %}
ntp {{ server }}
{% endfor %}
  1. 创建一个新的 playbook,pb_ansible_role.yml,内容如下以使用我们的角色:
$ cat pb_ansible_role.yml
---
 - name: Build Basic Config Using Roles
 hosts: all
 connection: local
 roles:
 - basic_config

工作原理...

在这个示例中,我们首先在主文件夹中创建roles目录。默认情况下,使用角色时,Ansible 会按照以下顺序在以下位置查找角色:

  • 当前工作目录中的roles文件夹

  • /etc/ansible/roles

因此,我们在当前工作目录(ch1_ansible)中创建roles文件夹,以承载我们将在此文件夹中创建的所有角色。我们使用ansible-galaxy命令和init选项以及角色名称(basic_config)创建角色,这将在roles文件夹内创建以下角色结构:

$ tree roles/
roles/
└── basic_config
 ├── defaults
 │   └── main.yml
 ├── files
 ├── handlers
 │   └── main.yml
 ├── meta
 │   └── main.yml
 ├── README.md
 ├── tasks
 │   └── main.yml
 ├── templates
 ├── tests
 │   ├── inventory
 │   └── test.yml
 └── vars
 └── main.yml

从前面的输出可以看出,使用ansible-galaxy命令创建了这个文件夹结构,这个命令根据最佳实践角色布局构建了角色。并非所有这些文件夹都需要具有我们可以使用的功能角色,以下列表概述了通常使用的主要文件夹:

  • tasks文件夹:这包含了main.yml文件,列出了在使用此角色时应执行的所有任务。

  • templates文件夹:这包含我们将作为此角色一部分使用的所有 Jinja2 模板。

  • vars文件夹:这包含了我们想要定义并在角色中使用的所有变量。vars文件夹中的变量在运行 playbook 时评估变量时具有非常高的优先级。

  • handlers文件夹:这包含了main.yml文件,其中包含了作为此角色一部分应运行的所有处理程序。

我们创建的角色只有一个目的,那就是为我们的设备构建基本配置。为了完成这个任务,我们需要定义一些 Ansible 任务,并使用一些 Jinja2 模板来生成设备的基本配置。我们在tasks/main.yml文件中列出了所有需要运行的任务,并在templates文件夹中包含了所有必要的 Jinja2 模板。我们在vars文件夹中定义了我们将在角色中使用的任何必需变量。

我们创建一个新的 playbook,将使用我们的新角色来生成设备的配置。我们在roles参数中调用我们想要运行的所有角色作为 playbook 的一部分。在我们的情况下,我们只想要运行一个角色,即basic_config角色。

运行我们的 playbook 后,我们可以看到一个名为basic_config的新目录被创建,其中包含以下内容:

$ tree basic_config/
basic_config/
├── csr1.cfg
├── csr2.cfg
├── mx1.cfg
└── mx2.cfg    

另请参阅

有关 Ansible 角色的更多信息,请参阅以下网址:

docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html

第二章:使用 Ansible 管理 Cisco IOS 设备

在本章中,我们将概述如何使用 Ansible 自动化 Cisco 基于 IOS 的设备。我们将探索 Ansible 中可用的不同模块,以自动化配置并从 Cisco IOS 设备收集网络信息。本章将基于以下示例网络图,并将介绍如何使用 Ansible 实现此网络设计:

以下表格概述了 Cisco 节点上的管理 IP 地址,Ansible 将使用这些地址连接到设备:

设备 角色 供应商 管理端口 管理 IP
access01 访问交换机 Cisco IOS 15.1 Ethernet0/0 172.20.1.18
access02 访问交换机 Cisco IOS 15.1 Ethernet0/0 172.20.1.19
core01 核心交换机 Cisco IOS 15.1 Ethernet0/0 172.20.1.20
core02 核心交换机 Cisco IOS 15.1 Ethernet0/0 172.20.1.21
wan01 WAN 路由器 Cisco IOS-XE 16.6.1 GigabitEthernet1 172.20.1.22
wan02 WAN 路由器 Cisco IOS-XE 16.6.1 GigabitEthernet1 172.20.1.23

本章涵盖的主要配方如下:

  • 构建 Ansible 网络清单

  • 连接到 Cisco IOS 设备

  • 配置基本系统信息

  • 在 IOS 设备上配置接口

  • 在 IOS 设备上配置 L2 VLAN

  • 配置干线和访问接口

  • 配置接口 IP 地址

  • 在 IOS 设备上配置 OSPF

  • 收集 IOS 设备信息

  • 在 IOS 设备上验证网络可达性

  • 从 IOS 设备检索操作数据

  • 使用 pyATS 和 Ansible 验证网络状态

技术要求

本章的代码文件可以在这里找到:

github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch2_ios

本章基于的软件版本如下:

  • Cisco IOS 15.1

  • Cisco IOS-XE 16.6.1

  • Ansible 2.9

  • Python 3.6.8

查看以下视频以查看代码的实际操作:

bit.ly/34F8xPW

构建 Ansible 网络清单

在这个配方中,我们将概述如何构建和组织 Ansible 清单,以描述前一节中概述的网络设置。

准备工作

确保 Ansible 已经安装在控制机上。

操作步骤...

  1. 创建一个名为ch2_ios的新目录。

  2. 在这个新文件夹里,创建hosts文件,内容如下:

$ cat hosts
 [access]
 access01 Ansible_host=172.20.1.18
 access02 Ansible_host=172.20.1.19

[core]
 core01 Ansible_host=172.20.1.20
 core02 Ansible_host=172.20.1.21

[wan]
 wan01 Ansible_host=172.20.1.22
 wan02 Ansible_host=172.20.1.23

[lan:children]
 access
 core

[network:children]
 lan
 wan
  1. 创建Ansible.cfg文件,内容如下:
$ cat Ansible.cfg

[defaults]
 inventory=hosts
 retry_files_enabled=False
 gathering=explicit

它是如何工作的...

我们使用hosts文件构建了 Ansible 清单,并以以下方式定义了多个组,以便对我们拓扑中的不同设备进行分组:

  • 我们创建了access组,其中包括我们拓扑中的访问交换机(access01access02)。

  • 我们创建了core组,将所有作为访问交换机上所有 VLAN 的 L3 终止的核心交换机分组在一起。

  • 我们创建了wan组,将所有我们的 Cisco IOS-XE 路由器分组在一起,它们将作为我们的 wan 路由器。

  • 我们创建了另一个名为lan的组,将访问组和核心组分组在一起。

  • 我们创建了network组,将lanwan组分组在一起。

最后,我们创建了Ansible.cfg文件,并配置它指向我们的hosts文件,以用作 Ansible 清单文件。我们禁用了设置模块,在针对网络节点运行 Ansible 时不需要它。

连接到 Cisco IOS 设备

在这个配方中,我们将概述如何通过 SSH 从 Ansible 连接到 Cisco IOS 设备,以便从 Ansible 开始管理设备。

准备工作

为了按照这个配方进行操作,应该按照前面的配方构建一个 Ansible 清单文件。必须配置 Ansible 控制机与网络中所有设备之间的 IP 可达性。

操作步骤...

  1. ch2_ios目录中,创建groups_vars文件夹。

  2. group_vars文件夹中,创建以下内容的network.yml文件:

$cat network.yml
Ansible_network_os: ios
Ansible_connection: network_cli
Ansible_user: lab
Ansible_password: lab123
Ansible_become: yes
Ansible_become_password: admin123
Ansible_become_method: enable
  1. 在所有 IOS 设备上,确保配置以下内容以设置 SSH 访问:
!
 hostname <device_hostname>
 !
 ip domain name <domain_name>
 !
 username lab secret 5 <password_for_lab_user>.
 !
 enable secret 5 <enable_password>.
 !
 line vty 0 4
 login local
 transport input SSH
 !
  1. 从配置模式在 Cisco IOS 设备上生成 SSH 密钥,如下面的代码片段所示:
(config)#crypto key generate rsa
 Choose the size of the key modulus in the range of 360 to 4096 for your
 General Purpose Keys. Choosing a key modulus greater than 512 may take
 a few minutes.
How many bits in the modulus [512]: 2048
 % Generating 2048 bit RSA keys, keys will be non-exportable...
 [OK] (elapsed time was 0 seconds)
  1. 使用以下突出显示的参数更新Ansible.cfg文件:
$ cat Ansible.cfg
[defaults]
 host_key_checking=False

工作原理...

在我们的示例网络中,我们将使用 SSH 来建立 Ansible 与我们的 Cisco 设备之间的连接。在这个设置中,Ansible 将使用 SSH 来建立与我们的 Cisco 设备的连接,以开始对其进行管理。我们将使用用户名/密码身份验证来验证我们的 Ansible 控制节点与我们的 Cisco 设备。

在 Cisco 设备上,我们必须确保存在 SSH 密钥,以便在 Cisco 设备上有一个功能性的 SSH 服务器。以下代码片段概述了在生成 SSH 密钥之前 Cisco 设备上的 SSH 服务器的状态:

wan01#show ip SSH
SSH Disabled - version 2.0
%Please create RSA keys to enable SSH (and of atleast 768 bits for SSH v2).
Authentication methods:publickey,keyboard-interactive,password
Authentication Publickey Algorithms:x509v3-SSH-rsa,SSH-rsa
Hostkey Algorithms:x509v3-SSH-rsa,SSH-rsa
Encryption Algorithms:aes128-ctr,aes192-ctr,aes256-ctr
MAC Algorithms:hmac-sha2-256,hmac-sha2-512,hmac-sha1,hmac-sha1-96
KEX Algorithms:diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1
Authentication timeout: 120 secs; Authentication retries: 3
Minimum expected Diffie Hellman key size : 2048 bits
IOS Keys in SECSH format(SSH-rsa, base64 encoded): NONE

一旦我们创建了 SSH 密钥,Cisco 设备上的 SSH 服务器就可以运行,并准备好接受来自 Ansible 控制节点的 SSH 连接。

在 Ansible 机器上,我们在network.yml文件中包含了与受管设备建立 SSH 连接所需的所有变量。根据我们的清单文件,网络组包括拓扑中的所有设备,因此我们在此文件中配置的所有属性将应用于清单中的所有设备。以下是我们在文件中包含的属性的详细信息:

  • Ansible_connection: 这确定了 Ansible 如何连接到设备。在这种情况下,我们将其设置为network_cli,以指示我们将使用 SSH 连接到网络设备。

  • Ansible_network_os: 当使用network_cli作为连接插件连接到网络设备时,我们必须指示 Ansible 将连接到哪个网络 OS,以便使用正确的 SSH 参数与设备连接。在这种情况下,我们将其设置为ios,因为我们拓扑中的所有设备都是基于 IOS 的设备。

  • Ansible_user: 此参数指定 Ansible 将用于与网络设备建立 SSH 会话的用户名。

  • Ansible_password: 此参数指定 Ansible 将用于与网络设备建立 SSH 会话的密码。

  • Ansible_become: 这指示 Ansible 在配置或执行受管设备上的show命令时,使用enable命令进入特权模式。在我们的情况下,我们将其设置为yes,因为我们需要特权模式来配置设备。

  • Ansible_become_password: 这指定了用于在受管 IOS 设备上进入特权模式的enable密码。

  • Ansible_become_method: 此选项指定进入特权模式时要使用的方法。在我们的情况下,这是 IOS 设备上的enable命令。

在这个示例中,我已经以明文形式定义了 SSH 密码和enable密码,仅仅是为了简单起见;然而,这是极不鼓励的。我们应该使用Ansible-vault来保护密码,就像在上一章的Ansible Vault示例中所概述的那样。

在 Cisco 设备上,我们设置了所需的用户名和密码,以便 Ansible 可以打开 SSH 连接到受管的 Cisco IOS 设备。我们还配置了一个enable密码,以便能够进入特权模式,并进行配置更改。一旦我们将所有这些配置应用到设备上,我们就准备好设置 Ansible 了。

在任何 SSH 连接中,当 SSH 客户端(在我们的情况下是 Ansible 控制节点)连接到 SSH 服务器(在我们的情况下是 Cisco 设备)时,服务器会在客户端登录之前向客户端发送其公钥的副本。这用于在客户端和服务器之间建立安全通道,并向客户端验证服务器,以防止任何中间人攻击。因此,在涉及新设备的新 SSH 会话开始时,我们会看到以下提示:

$SSH lab@172.20.1.18
The authenticity of host '172.20.1.18 (172.20.1.18)' can't be established.
RSA key fingerprint is SHA256:KnWOalnENZfPokYYdIG3Ogm9HDnXIwjh/it3cqdiRRQ.
RSA key fingerprint is MD5:af:18:4b:4e:84:19:a6:8d:82:17:51:d5:ee:eb:16:8d.
Are you sure you want to continue connecting (yes/no)?

当 SSH 客户端启动 SSH 连接到客户端时,SSH 服务器会向客户端发送其公钥,以便向客户端进行身份验证。客户端在其本地已知的hosts文件(在~/.SSH/known_hosts/etc/SSH/SSH_known_hosts文件中)中搜索公钥。如果在其本地已知的hosts文件中找不到此计算机的公钥,它将提示用户将此新密钥添加到其本地数据库中,这就是我们在启动 SSH 连接时看到的提示。

为了简化 Ansible 控制节点与其远程管理的hosts之间的 SSH 连接设置,我们可以禁用此主机检查。我们可以通过在Ansible.cfg配置文件中将host_key_checking设置为False来告诉 Ansible 忽略主机密钥并不将其添加到已知的hosts文件中。

禁用主机密钥检查不是最佳实践,我们只是将其显示为实验室设置。在下一节中,我们将概述在 Ansible 和其远程管理设备之间建立 SSH 连接的另一种方法。

还有更多...

如果我们需要验证我们将连接到的 SSHhosts的身份,并因此启用host_key_checking,我们可以使用 Ansible 自动将远程管理的hosts的 SSH 公钥添加到~/.SSH/known_hosts文件中。我们创建一个新的 Ansible playbook,将在 Ansible 控制机器上使用ssk-keyscan命令连接到远程设备。然后收集远程机器的 SSH 公钥并将其添加到~/.SSH/known_hosts文件中。该方法在此处概述:

  1. 创建一个新的playbook pb_gather_SSH_keys.yml文件,并添加以下 play:
- name: "Play2: Record Keys in Known Hosts file"
 hosts: localhost
 vars:
 - hosts_file: "~/.SSH/known_hosts"
tasks:
 - name: create know hosts file
 file:
 path: "{{ hosts_file }}"
 state: file
 changed_when: false
  1. 更新 playbook 并在同一 playbook 中添加另一个 play 以保存和存储远程管理节点的 SSH 公钥:
- name: "Play2: Record Keys in Known Hosts file"
 hosts: localhost
 vars:
 - hosts_file: "~/.SSH/known_hosts"
 tasks:
 - name: create know hosts file
 file:
 path: "{{ hosts_file }}"
 state: file
 changed_when: false
 - name: Populate the known_hosts file
 blockinfile:
 block: |
 {% for host in groups['all'] if  hostvars[host].SSH_keys.stdout != '' 
%}
 {{ hostvars[host].SSH_keys.stdout}}
 {% endfor %}
 path: "{{ hosts_file }}"
 create: yes

在我们的新 playbook 中,我们通过将hosts参数设置为all来针对所有受管设备进行 play。在此 play 中,我们有一个单独的任务,我们在 Ansible 控制节点上运行(使用delegate_to localhost)以发出SSH-keyscan命令,该命令返回远程设备的 SSH 公钥,如下所示:

$ SSH-keyscan 172.20.1.22

# 172.20.1.22:22 SSH-2.0-Cisco-1.25
 172.20.1.22 SSH-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDTwrH4phzRnW/RsC8eXMh/accIErRfkgffDWBGSdEX0r9EwAa6p2uFMWj8dq6kvrREuhqpgFyMoWmpgdx5Cr+10kEonr8So5yHhOhqG1SJO9RyzAb93H0P0ro5DXFK8A/Ww+m++avyZ9dShuWGxKj9CDM6dxFLg9ZU/9vlzkwtyKF/+mdWNGoSiCbcBg7LrOgZ7Id7oxnhEhkrVIa+IxxGa5Pwc73eR45Uf7QyYZXPC0RTOm6aH2f9+8oj+vQMsAzXmeudpRgAu151qUH3nEG9HIgUxwhvmi4MaTC+psmsGg2x26PKTOeX9eLs4RHquVS3nySwv4arqVzDqWf6aruJ

在此任务中,我们使用delegate_to等于localhost,因为 Ansible 将尝试连接到远程设备并默认在远程设备上发出命令。在我们的情况下,这不是我们需要的;我们需要从 Ansible 控制节点发出此命令。因此,我们使用delegate_to等于localhost来强制执行此行为。

我们通过将hosts设置为“localhost”在 Ansible 控制主机上运行第二个 play,并执行任务以创建已知的主机文件(如果尚未存在),并使用SSH_keys变量在此文件中填充我们在第一个 play 中捕获的数据。我们在 Ansible 控制机器上运行此 playbook,以在运行任何 playbook 之前存储来自远程管理节点的 SSH 密钥。

配置基本系统信息

在这个示例中,我们将概述如何在 Cisco IOS 设备上配置基本系统参数,例如设置主机名、DNS 服务器和 NTP 服务器。根据我们在本章开头概述的网络设置,我们将在所有 Cisco IOS 设备上配置以下信息:

  • DNS 服务器 172.20.1.250 和 172.20.1.251

  • NTP 服务器 172.20.1.17

准备就绪

必须存在一个 Ansible 清单文件,以及通过 SSH 连接到 Cisco IOS 设备的 Ansible 配置。

如何操作...

  1. group_vars/network.yml文件中,添加以下系统参数:
$ cat group_vars/network.yml
<-- Output Trimmed for brevity ------>
name_servers:
 - 172.20.1.250
 - 172.20.1.251
ntp_server: 172.20.1.17
  1. 创建一个名为pb_build_network.yml的新播放文件,包含以下信息:
$ cat pb_build_network.yml
 ---
- name: "PLAY 1: Configure All Lan Switches"
 hosts: lan
 tags: lan
 tasks:
 - name: "Configure Hostname and Domain Name"
 ios_system:
 hostname: "{{ inventory_hostname }}"
 domain_name: "{{ domain_name }}"
 lookup_enabled: no
 name_servers: "{{ name_servers }}"
 - name: "Configure NTP"
 ios_ntp:
 server: "{{ ntp_server }}"
 logging: true
 state: present

它是如何工作的...

network.yml文件中,我们将name_servers变量定义为 DNS 服务器列表,并定义ntp_servers变量,它定义了我们要在 IOS 设备上配置的 NTP 服务器。在network.yml文件中定义这些参数将这些变量应用于网络组中的所有设备。

我们创建了一个播放文件,第一个播放目标是lan组中的所有hosts(包括访问设备和核心设备),在这个播放中,我们引用了两个任务:

  • ios_system:这在设备上设置主机名和 DNS 服务器。

  • ios_ntp:这在 IOS 设备上配置 NTP 并启用 NTP 事件的日志记录。

这两个模块都是声明性的 Ansible 模块,我们只需确定与我们基础设施相关的状态。Ansible 将此声明转换为必要的 IOS 命令。这些模块检索设备的配置,并将当前状态与我们的预期状态进行比较(在它们上配置了 DNS 和 NTP),然后,如果当前状态与这些模块定义的预期状态不符,Ansible 将对设备应用所需的配置。

当我们在所有 LAN 设备上运行这些任务时,以下配置将被推送到设备上:

!
 ip name-server 172.20.1.250 172.20.1.251
 no ip domain lookup
 ip domain name lab.net
 !
 ntp logging
 ntp server 172.20.1.17
 !

另请参阅...

有关ios_systemios_ntp模块以及这些模块支持的不同参数的更多信息,请参考以下网址:

在 IOS 设备上配置接口

在这个示例中,我们将概述如何在基于 Cisco IOS 的设备上配置基本接口属性,例如设置接口描述、接口最大传输单元(MTU)和启用interfaces。我们将配置拓扑中的所有链路的链路 MTU 为 1,500,并且设置为全双工。

准备工作

要按照这个示例进行操作,假设已经设置了 Ansible 清单,并且 Ansible 控制节点与已经放置的 Cisco 设备之间具有 IP 可达性。

如何操作...

  1. group_vars/network.yml文件中,添加以下内容来定义通用接口参数:
$ cat group_vars/network.yml
<-- Output Trimmed for brevity ------>
intf_duplex: full
intf_mtu: 1500
  1. group_vars文件夹下创建一个新文件lan.yml,包含以下数据来定义我们的 Cisco 设备上的interfaces
$ cat group_vars/lan.yaml

interfaces:
 core01:
 - name: Ethernet0/1
 description: access01_e0/1
 mode: trunk
 - name: Ethernet0/2
 description: access02_e0/1
 mode: trunk
 - name: Ethernet0/3
 description: core01_e0/3
 mode: trunk
 <--   Output Trimmed for brevity ------>
 access01:
 - name: Ethernet0/1
 description: core01_e0/1
 mode: trunk
 - name: Ethernet0/2
 description: core02_e0/1
 mode: trunk
 - name: Ethernet0/3
 description: Data_vlan
 mode: access
 vlan: 10
  1. 更新pb_build_network.yml播放文件,增加以下任务来设置interfaces
 - name: "P1T3: Configure Interfaces"
 ios_interface:
 name: "{{ item.name }}"
 description: "{{ item.description }}"
 duplex: "{{ intf_duplex }}"
 mtu: "{{ intf_mtu }}"
 state: up
 loop: "{{ interfaces[inventory_hostname] }}"
 register: ios_intf

它是如何工作的...

在这个示例中,我们概述了如何在 IOS 设备上配置物理接口。我们首先声明适用于所有接口的通用参数(接口双工和 MTU)。这些参数在network.yml文件下定义。接下来,我们在lan.yml文件下定义了所有我们的 LAN 设备的特定接口参数,以应用于所有设备。所有这些参数都在interfaces字典数据结构中声明。

我们更新我们的手册,增加了一个新任务,用于配置网络中所有 LAN 设备的物理参数。我们使用ios_interface模块来配置所有interface参数,并使用interfaces数据结构在每个节点上循环所有interfaces。我们将状态设置为up,以指示interface应该存在并且可操作。

另请参阅...

有关ios_interface模块以及这些模块支持的不同参数的更多信息,请参考以下网址:docs.Ansible.com/Ansible/latest/modules/ios_interface_module.html

在 IOS 设备上配置 L2 VLAN

在本配方中,我们将概述如何根据本章介绍中讨论的网络拓扑在 Cisco IOS 设备上配置 L2 VLAN。我们将概述如何将 VLAN 声明为 Ansible 变量,以及如何使用适当的 Ansible 模块在网络上提供这些 VLAN。

准备工作

我们将在本章讨论的先前配方的基础上继续构建,以继续配置样本拓扑中所有 LAN 设备上的 L2 VLAN。

如何做...

  1. 使用以下代码更新group_vars/lan.yml文件中的 VLAN 定义:
$ cat group_vars/lan.yaml

vlans:
 - name: Data
 vlan_id: 10
 - name: Voice
 vlan_id: 20
 - name: Web
 vlan_id: 100
  1. 使用以下任务更新pb_build.yml playbook 以提供 VLAN:
 - name: "P1T4: Create L2 VLANs"
 ios_vlan:
 vlan_id: "{{ item.vlan_id }}"
 name: "{{ item.name  }}"
 loop: "{{ vlans }}"
 tags: vlan

它是如何工作的...

group_vars/lan.yml文件中,我们定义了一个vlans列表数据结构,其中包含我们需要应用于所有核心和接入交换机的 VLAN 定义。此变量将对所有核心和接入交换机可用,并且 Ansible 将使用此变量来在远程设备上提供所需的 VLAN。

我们使用另一个声明性模块ios_vlan,它接受 VLAN 定义(名称和 VLAN ID)并在远程托管设备上配置这些 VLAN。它从设备中提取现有配置,并将其与需要存在的设备列表进行比较,仅推送增量。

我们使用loop结构遍历vlans列表中的所有项目,并在所有设备上配置所有相应的 VLAN。

在设备上运行此任务后,以下是一个接入交换机的输出:

access01#sh vlan
VLAN Name                             Status    Ports
---- -------------------------------- --------- -------------------------------
1    default                          active    Et1/0, Et1/1, Et1/2, Et1/3
10   Data                             active    Et0/3
20   Voice                            active
100  Web                              active

配置干道和接入接口

在这个配方中,我们将展示如何在基于 Cisco IOS 的设备上配置接入和干道接口,以及如何将接口映射到接入 VLAN,以及如何在干道上允许特定的 VLAN。

准备工作

根据我们的样本拓扑,我们将配置设备上的接口。如表所示,我们只显示access01core01的 VLAN - 其他设备是完全相同的副本:

设备 接口 模式 VLANs
Core01 Ethernet0/1 干道 10,20,100
Core01 Ethernet0/2 干道 10,20,100
Core01 Ethernet0/3 干道 10,20,100,200
Access01 Ethernet0/1 干道 10,20,100
Access01 Ethernet0/2 干道 10,20,100
Access01 Ethernet0/3 接入 10

如何做...

  1. group_vars下创建一个新的core.yml文件,并包括以下core_vlans定义:
core_vlans:
 - name: l3_core_vlan
 vlan_id: 200
 interface: Ethernet0/3
  1. 使用以下任务更新pb_build_network.yml playbook 以配置所有干道端口:
 - name: "Configure L2 Trunks"
 ios_l2_interface:
 name: "{{ item.name }}"
 mode: "{{ item.mode }}"
 trunk_allowed_vlans: "{{ vlans | map(attribute='vlan_id') | join(',') }}"
 state: present
 loop: "{{ interfaces[inventory_hostname] |
selectattr('mode','equalto','trunk') | list }}"
 - name: "Enable dot1q Trunks"
 ios_config:
 lines:
 - switchport trunk encapsulation dot1q
 parents: interface {{item.name}}
 loop: "{{ interfaces[inventory_hostname] |
selectattr('mode','equalto','trunk') | list }}"
 tags: dot1q
  1. 使用以下任务更新 playbook 以配置所有接入端口:
 - name: "Configure Access Ports"
 ios_l2_interface:
 name: "{{ item.name }}"
 mode: "{{ item.mode}}"
 access_vlan: "{{ item.vlan }}"
 state: present
 loop: "{{ interfaces[inventory_hostname] |
selectattr('mode','equalto','access') | list }}"

它是如何工作的...

我们在lan.yml文件中使用相同的数据结构,该数据结构定义了 LAN 网络中的所有接口并描述其类型(接入/干道)。对于接入端口,我们定义了哪个接入接口属于哪个 VLAN。我们将引用此列表数据结构来配置lan组中所有设备上的接入和干道端口。

我们的layer2网络中的接口有以下两个选项之一:

接入

  • 我们使用ios_l2_interfaceaccess_vlan参数在接口上配置正确的接入 VLAN。

  • 我们使用selectattr jinja2过滤器仅选择每个设备的接入接口,并且仅匹配模式等于access的一个接口,并且我们循环遍历每个设备的此列表。

干道

  • 我们使用ios_l2_interfacetrunk_allowed_vlans参数将所有 VLAN 添加到干道端口上,包括接入和核心交换机。

  • 我们使用 Jinja2 的mapjoin过滤器创建允许的 VLAN 列表,并将此过滤器应用于vlans列表数据结构。这将输出类似于以下内容的字符串:10,20,100

  • 我们使用selectattr Jinja2 过滤器从每个节点的接口数据结构中仅选择 trunk 端口。

  • 我们需要将这些 trunk 端口配置为dot1q端口;但是,ios_l2_interface上仍未启用此属性。因此,我们使用另一个模块ios_config发送所需的 Cisco IOS 命令来设置dot1q trunk。

以下输出概述了作为示例应用于access01设备的配置,用于访问和 trunk 端口:

!
interface Ethernet0/3   >> Access Port
 description Data_vlan
 switchport access vlan 10
 switchport mode access

 !
interface Ethernet0/1    >> Trunk Port
 description core01_e0/1
 switchport trunk encapsulation dot1q
 switchport trunk allowed vlan 10,20,100
 switchport mode trunk

另请参阅...

有关ios_l2_interface和这些模块支持的不同参数的更多信息,请参阅以下网址:

docs.Ansible.com/Ansible/latest/modules/ios_l2_interface_module.html

配置接口 IP 地址

在本教程中,我们将探讨如何在 Cisco IOS 设备上配置接口 IP 地址。我们将使用示例拓扑在两个核心交换机上配置 VLAN 接口。我们将概述如何在核心交换机之间为所有 VLAN 接口配置 VRRP。我们将配置以下 IP 地址:

接口 前缀 VRRP IP 地址
VLAN10 10.1.10.0/24 10.1.10.254
VLAN20 10.1.20.0/24 10.1.20.254
VLAN100 10.1.100.0/24 10.1.100.254

准备工作

本教程假定接口和 VLAN 已根据本章中的先前教程进行了配置。

如何做...

  1. 更新group_vars/core.yml文件,使用以下数据定义 SVI 接口:
$ cat group_vars/core.yml
<-- Output Trimmed for brevity ------>
svi_interfaces:
 - name: Vlan10
 ipv4: 10.1.10.0/24
 vrrp: yes
 ospf: passive
 -  name: Vlan20
 ipv4: 10.1.20.0/24
 vrrp: yes
 ospf: passive
 -  name: Vlan100
 ipv4: 10.1.100.0/24
 vrrp: yes
 ospf: passive
  1. host_vars文件夹下创建core01.ymlcore02.yml文件,并添加以下内容:
$ cat host_vars/core01.yml
 hst_svi_id: 1
 hst_vrrp_priority: 100
$ cat host_vars/core02.yml
 hst_svi_id: 2
 hst_vrrp_priority: 50
  1. 更新pb_build_network.yml playbook,添加以下任务以创建和启用 L3 SVI 接口:
- name: "PLAY 2: Configure Core Switches"
 hosts: core
 tags: l3_core
 tasks:
<-- Output Trimmed for brevity ------>
 - name: "Create L3 VLAN Interfaces"
 ios_l3_interface:
 name: "{{item.name }}"
 ipv4: "{{item.ipv4 | ipv4(hst_svi_id)}}"
 loop: "{{svi_interfaces}}"
 tags: l3_svi
 - name: "Enable the VLAN Interfaces"
 ios_interface:
 name: "{{ item.name }}"
 state: up
 loop: "{{ svi_interfaces }}"
  1. 更新 playbook,添加以下任务以在 SVI 接口上设置 VRRP 配置:
 - name: "Create VRRP Configs"
 ios_config:
 parents: interface {{ item.name }}
 lines:
 - vrrp {{item.name.split('Vlan')[1]}} priority {{ hst_vrrp_priority }}
 - vrrp {{item.name.split('Vlan')[1]}} ip {{item.ipv4 | ipv4(254)|ipaddr('address')}}
 loop: "{{svi_interfaces | selectattr('vrrp','equalto',true) | list }}"

它是如何工作的...

在本节中,我们正在为核心交换机上的 L3 VLAN 接口配置 IP 地址,并在所有 L3 VLAN 接口上配置 VRRP 以提供 L3 冗余。

我们正在使用一个名为svi_interfaces的新列表数据结构,它描述了所有带有 L3 IP 地址的 SVI 接口,以及一些额外的参数来控制这些接口上配置的 VRRP 和 OSPF。我们还在每个核心路由器上设置了两个新变量,hst_svi_idhst_vrrp_priority,我们将在 playbook 中使用它们来控制每个核心交换机上的 IP 地址,以及 VRPP 优先级。

我们使用ios_l3_interface Ansible 模块在 VLAN 接口上设置 IPv4 地址。在每个核心交换机上,我们循环遍历svi_interfaces数据结构,对于每个 VLAN,我们在相应的 VLAN 接口上配置 IPv4 地址。我们使用 Ansible 的ipaddr过滤器确定每个路由器上配置的 IP 地址,以及hst_svi_id参数{{item.ipv4 | ipv4(hst_svi_id)}}。例如,对于 VLAN10,我们将为core01分配10.1.10.1/24,为core02分配10.1.10.2/24

在首次创建 Cisco IOS 设备上的 VLAN 接口时,它们处于关闭状态,因此我们需要启用它们。我们使用ios_interface模块启用接口。

对于 VRRP 部分,我们将再次使用ios_config模块在所有 VLAN 接口上设置 VRRP 配置,并使用hst_vrrp_priority正确设置core01作为所有 VLAN 的主 VRRP。

在运行 playbook 后,以下是推送到设备上的配置示例:

Core01
 ========
 !
 interface Vlan10
 ip address 10.1.10.1 255.255.255.0
 vrrp 10 ip 10.1.10.254
 !
Core02
 =======
 !
 interface Vlan10
 ip address 10.1.10.2 255.255.255.0
 vrrp 10 ip 10.1.10.254
 vrrp 10 priority 50

另请参阅...

有关ios_l3_interface和这些模块支持的不同参数的更多信息,请参阅以下网址:

docs.Ansible.com/Ansible/latest/modules/ios_l3_interface_module.html

在 IOS 设备上配置 OSPF

在本教程中,我们将概述如何使用 Ansible 在 Cisco IOS 设备上配置 OSPF。使用我们的示例网络拓扑,我们将在核心交换机和 WAN 路由器之间设置 OSPF,并通过 OSPF 广告 SVI 接口。

准备工作

本教程假设所有接口已经配置了正确的 IP 地址,并且遵循了前面教程中概述的相同流程。

操作步骤

  1. 使用以下数据更新group_vars/core.yml文件,定义核心交换机和 WAN 路由器之间的核心链路:
core_l3_links:
 core01:
 - name: Ethernet1/0
 description: wan01_Gi2
 ipv4: 10.3.1.0/30
 ospf: yes
 ospf_metric: 100
 peer: wan01
 core02:
 - name: Ethernet1/0
 description: wan02_Gi2
 ipv4: 10.3.1.4/30
 ospf: yes
 ospf_metric: 200
 peer: wan02
  1. 更新pb_build_network.ymlplaybook,添加以下任务来设置 OSPF:
- name: "PLAY 2: Configure Core Switches"
 hosts: core
 tags: l3_core
 tasks:
< -------- Snippet -------- >
 - name: "P2T9: Configure OSPF On Interfaces"
 ios_config:
 parents: interface {{ item.name }}
 lines:
 - ip ospf {{ ospf_process }} area {{ ospf_area }}
 - ip ospf network point-to-point
 - ip ospf cost {{item.ospf_metric | default(ospf_metric)}}
 loop: "{{ (svi_interfaces + core_l3_links[inventory_hostname]) | selectattr('ospf') | list }}"
 - name: "P2T10: Configure OSPF Passive Interfaces"
 ios_config:
 parents: router ospf {{ ospf_process }}
 lines: passive-interface {{item.name}}
 loop: "{{ (svi_interfaces + core_l3_links[inventory_hostname]) | selectattr('ospf','equalto','passive') | list }}"

工作原理

我们在core.yml文件中创建了另一个字典数据结构,描述了核心交换机和 WAN 路由器之间的 L3 链路。我们指定它们是否会运行 OSPF 以及这些链路上的 OSPF 度量。

目前,Ansible 没有提供用于管理基于 IOS 设备的 OSPF 配置的声明性模块。因此,我们需要使用ios_config模块推送所需的配置。我们使用ios_config创建了两个单独的任务,以便在每个设备上推送与 OSPF 相关的配置。在第一个任务中,我们在每个接口下配置了接口相关的参数,并循环遍历了svi_interfacecore_l3_interfaces数据结构,以在所有 OSPF 启用的接口上启用 OSPF。我们使用 Jinja2 的selectattr过滤器来选择所有具有设置为yes/true的 OSPF 属性的接口。

在最后一个任务中,我们将被动接口配置应用到所有已启用被动标志的接口上。我们使用 Jinja2 的selectattr过滤器来仅选择那些被动参数设置为yes/true的接口。

收集 IOS 设备信息

在本教程中,我们将概述如何使用 Ansible 从 Cisco 设备中收集设备信息。这些信息包括序列号、IOS 版本以及设备上的所有接口。Ansible 在托管的 IOS 设备上执行多个命令以收集这些信息。

准备工作

Ansible 控制器必须与托管网络设备具有 IP 连接,并且 IOS 设备上必须启用 SSH。

操作步骤

  1. 在与以下信息相同的ch2_ios文件夹中创建一个名为pb_collect_facts.yml的新 playbook:
---
- name: "PLAY 1: Collect Device Facts"
 hosts: core,wan
 tasks:
 - name: "P1T1: Gather Device Facts"
 ios_facts:
 register: device_facts
 - debug: var=device_facts

工作原理

我们对corewan组中的所有节点运行这个新的 playbook,并使用ios_facts模块从托管的 IOS 设备中收集信息。在本教程中,我们使用 debug 模块打印从ios_facts模块收集的信息。以下是从ios_facts模块中发现的信息的一个子集:

ok: [core01 -> localhost] => {
 "Ansible_facts": {
 "net_all_ipv4_addresses": [
 "172.20.1.20",
< ---------- Snippet ------------ >
 "10.1.100.1"
 ],
 "net_hostname": "core01",
 "net_interfaces": {
 < ---------- Snippet ------------ >
 "Vlan10": {
 "bandwidth": 1000000,
 "description": null,
 "duplex": null,
 "ipv4": [
 {
 "address": "10.1.10.1",
 "subnet": "24"
 }
 ],
 "lineprotocol": "up",
 "macaddress": "aabb.cc80.e000",
 "mediatype": null,
 "mtu": 1500,
 "operstatus": "up",
 "type": "Ethernet SVI"
 },

 },
 "net_iostype": "IOS",
 "net_serialnum": "67109088",
 "net_system": "ios",
 "net_version": "15.1",
 }
 < ------------ Snippet ------------ >
 }

从前面的输出中,我们可以看到ios_facts模块从设备中捕获的一些主要信息,包括以下内容:

  • net_all_ipv4_addresses:这个列表数据结构包含了在 IOS 设备上所有接口上配置的所有 IPv4 地址。

  • net_interfaces:这个字典数据结构捕获了该设备上所有接口的状态和操作状态,以及其他重要信息,比如描述和操作状态。

  • net_serialnum:这捕获了设备的序列号。

  • net_version:这捕获了设备上运行的 IOS 版本。

还有更多...

使用从ios_facts模块收集的信息,我们可以为网络的当前状态生成结构化报告,并在进一步的任务中使用这些报告。在本节中,我们将概述如何修改我们的 playbook 来构建这个报告。

pb_collect_facts.ymlplaybook 中添加一个新任务,如下所示:

- name: "P1T2: Write Device Facts"
 blockinfile:
 path: ./facts.yml
 create: yes
 block: |
 device_facts:
 {% for host in play_hosts %}
 {% set node = hostvars[host] %}
 {{ node.Ansible_net_hostname }}:
 serial_number: {{ node.Ansible_net_serialnum }}
 ios_version: {{ node.Ansible_net_version }}
 {% endfor %}
 all_loopbacks:
 {% for host in play_hosts %}
 {% set node = hostvars[host] %}
 {% if node.Ansible_net_interfaces is defined %}
 {% if node.Ansible_net_interfaces.Loopback0 is defined %}
 - {{ node.Ansible_net_interfaces.Loopback0.ipv4[0].address }}
 {% endif %}
 {% endif %}
 {% endfor %}
 run_once: yes
 delegate_to: localhost

我们使用blockinfile模块构建一个名为facts.yml的 YAML 文件。我们在blockinfile模块中使用 Jinja2 表达式来自定义和选择我们想要从ios_facts任务捕获的 Ansible 事实中捕获的信息。当我们运行pb_collect_facts.yml playbook 时,我们生成了facts.yml文件,其中包含以下数据:

device_facts:
 wan01:
 serial_number: 90L4XVVPL7V
 ios_version: 16.06.01
 wan02:
 serial_number: 9UOFOO7FH19
 ios_version: 16.06.01
 core01:
 serial_number: 67109088
 ios_version: 15.1
 core02:
 serial_number: 67109104
 ios_version: 15.1
all_loopbacks:
 - 10.100.1.3
 - 10.100.1.4
 - 10.100.1.1
 - 10.100.1.2

另请参阅...

有关ios_facts和这些模块支持的不同参数的更多信息,请参考以下 URL:

docs.Ansible.com/Ansible/latest/modules/ios_facts_module.html

验证 IOS 设备的网络可达性

在这个示例中,我们将概述如何使用 Ansible 通过ping来验证网络可达性。 ICMP 允许我们验证网络上的正确转发。使用 Ansible 执行此任务为我们提供了一个强大的工具来验证正确的流量转发,因为我们可以同时从每个节点执行此任务并收集所有结果以供进一步检查。

准备工作

这个示例是基于章节介绍中概述的网络设置构建的,我假设网络已经根据本章中的所有先前示例构建好了。

如何做...

  1. 创建一个名为pb_net_validate.yml的新 playbook,并添加以下任务以存储所有 SVI IP 地址:
---
 - name: "PLay 1: Validate Network Reachability"
 hosts: core,wan
 vars:
 host_id: 10
 packet_count: 10
 tasks:
 - name: "Get all SVI Prefixes"
 set_fact:
 all_svi_prefixes: "{{ svi_interfaces | selectattr('vrrp') |
 map(attribute='ipv4') | list }}"
 run_once: yes
 delegate_to: localhost
 tags: svi
  1. 更新pb_net_validate.yml playbook,以 ping 所有 SVI interfaces的以下任务:
 - name: "Ping Hosts in all VLANs"
 ios_ping:
 dest: "{{ item | ipaddr(10) | ipaddr('address') }}"
 loop: "{{ all_svi_prefixes }}"
 ignore_errors: yes
 tags: svi

它是如何工作的...

在这个 playbook 中,我们使用ios_ping模块,该模块登录到 Ansible 清单中定义的每个节点,并 pingdest属性指定的目的地。在这个示例 playbook 中,我们想要验证对数据、语音和 Web VLAN 中的单个主机的网络可达性,并选择这些 VLAN 中的第十个主机(只是一个例子)。为了构建我们在第一个任务中设置的所有 VLAN 前缀,我们添加一个名为all_svi_prefixes的新变量,并使用多个jinja2过滤器仅收集运行 VRRP 的那些前缀(以删除任何核心 VLAN)。我们仅获取这些 SVI interfaces的 IPv4 属性。在运行第一个任务后,以下是此新变量的内容:

ok: [core01 -> localhost] => {
 "all_svi_prefixes": [
 "10.1.10.0/24",
 "10.1.20.0/24",
 "10.1.100.0/24"
 ]
}

我们将这个新的列表数据结构提供给ios_ping模块,并指定我们需要在每个子网中 ping 第十个主机。只要 ping 成功,任务就会成功。但是,如果从路由器/交换机到此主机存在连接问题,任务将失败。我们使用ignore_errors参数来忽略可能由于主机不可达/关闭而发生的任何失败,并运行任何后续任务。以下代码片段概述了成功运行:

TASK [P1T2: Ping Hosts in all VLANs] *****************************
 ok: [core01] => (item=10.1.10.0/24)
 ok: [core02] => (item=10.1.10.0/24)
 ok: [wan01] => (item=10.1.10.0/24)
 ok: [wan02] => (item=10.1.10.0/24)
 ok: [core01] => (item=10.1.20.0/24)
 ok: [core02] => (item=10.1.20.0/24)
 ok: [core01] => (item=10.1.100.0/24)
 ok: [wan01] => (item=10.1.20.0/24)
 ok: [wan02] => (item=10.1.20.0/24)
 ok: [core02] => (item=10.1.100.0/24)
 ok: [wan01] => (item=10.1.100.0/24)
 ok: [wan02] => (item=10.1.100.0/24)

从 IOS 设备检索操作数据

在这个示例中,我们将概述如何在 IOS 设备上执行操作命令,并将这些输出存储到文本文件中以供进一步处理。这允许我们在执行任何部署后的预验证或后验证期间捕获来自 IOS 设备的任何操作命令,以便我们可以比较结果。

准备工作

为了按照这个示例进行操作,应该有一个 Ansible 清单文件,并且网络应该已经按照先前的示例设置好了。

如何做...

  1. 创建一个名为pb_op_cmds.yml的新 playbook,并填充以下任务以创建保存设备输出的目录结构:
---
 - name: "Play 1: Execute Operational Commands"
 hosts: network
 vars:
 config_folder: "configs"
 op_folder: "op_data"
 op_cmds:
 - show ip ospf neighbor
 - show ip route
 tasks:
 - name: "P1T1: Build Directories to Store Data"
 block:
 - name: "Create folder to store Device config"
 file:
 path: "{{ config_folder }}"
 state: directory
 - name: "Create Folder to store operational commands"
 file:
 path: "{{ op_folder }}"
 state: directory
 run_once: yes
 delegate_to: localhost
  1. 更新pb_op_cmds.yml playbook,并填充以下任务以从设备中检索运行配置:
 - name: "P1T2: Get Running configs from Devices"
 ios_command:
 commands: show running-config
 register: show_run
 - name: "P1T3: Save Running Config per Device"
 copy:
 content: "{{ show_run.stdout[0] }}"
 dest: "{{ config_folder }}/{{ inventory_hostname }}.cfg"
  1. 更新 playbook 并填充以下任务以从设备中检索操作命令并保存它:
 - name: "P1T4: Create Folder per Device"
 file:
 path: "{{ op_folder}}/{{ inventory_hostname }}"
 state: directory
 delegate_to: localhost
 - name: "P1T5: Get Operational Data from Devices"
 ios_command:
 commands: "{{ item }}"
 register: op_output
 loop: "{{ op_cmds }}"
 - name: "P1T6: Save output per each node"
 copy:
 content: "{{ item.stdout[0] }}"
 dest: "{{ op_folder}}/{{ inventory_hostname }}/{{item.item | replace(' ', '_')}}.txt"
 loop: "{{ op_output.results }}"

它是如何工作的...

在这个示例中,我们使用ios_command模块来在 IOS 设备上执行操作命令,并将它们保存到文本文件中。为了实现这个目标,我们执行以下步骤:

  • 我们创建将存储输出的文件夹,并创建一个名为configs的文件夹来存储所有设备的运行配置。我们还创建一个op_data文件来存储我们将从设备获取的操作命令的输出。

  • 然后我们在清单中的所有 IOS 设备上执行show running命令,并将输出注册到一个名为show_run的新变量中。

  • 我们使用复制模块将上一个任务的输出保存到每个设备的文件中。命令运行的输出保存在stdout变量中。由于我们执行了单个命令,stdout变量只有一个项目(stdout[0])。

一旦我们执行了这个任务,我们可以看到configs文件夹如下所示:

$ tree configs/
 configs/
 ├── access01.cfg
 ├── access02.cfg
 ├── core01.cfg
 ├── core02.cfg
 ├── isp01.cfg
 ├── wan01.cfg
 └── wan02.cfg

接下来,我们为每个节点创建一个文件夹,以存储我们将在 IOS 设备上执行的多个show命令的输出。

我们使用ios_command模块在设备上执行show命令,并将所有输出保存在一个名为op_output的新变量中。我们使用复制执行命令show ip route,并创建一个名为show_ip_route.txt的文件来保存此命令的输出。

运行此任务后,我们可以看到op_data文件夹的当前结构如下:

$ tree op_data/
 op_data/
 ├── access01
 │ ├── show_ip_ospf_neighbor.txt
 │ └── show_ip_route.txt
 ├── access02
 │ ├── show_ip_ospf_neighbor.txt
 │ └── show_ip_route.txt
 ├── core01
 │ ├── show_ip_ospf_neighbor.txt
 │ └── show_ip_route.txt
 ├── core02
 │ ├── show_ip_ospf_neighbor.txt
 │ └── show_ip_route.txt
 ├── isp01
 │ ├── show_ip_ospf_neighbor.txt
 │ └── show_ip_route.txt
 ├── wan01
 │ ├── show_ip_ospf_neighbor.txt
 │ └── show_ip_route.txt
 └── wan02
 ├── show_ip_ospf_neighbor.txt
 └── show_ip_route.txt

我们可以检查其中一个文件的内容,以确认所有数据都已存储:

$ head op_data/core01/show_ip_ospf_neighbor.txt

Neighbor ID     Pri   State           Dead Time   Address         Interface
10.100.1.3        0   FULL/  -        00:00:37    10.3.1.2        Ethernet1/0
10.100.1.2        0   FULL/  -        00:00:36    10.1.200.2      Vlan200

使用 pyATS 和 Ansible 验证网络状态

在这个示例中,我们将概述如何使用 Ansible 和 Cisco pyATS Python 库在 Cisco 设备上执行和解析操作命令。使用这些解析的命令,我们可以验证网络的各个方面。

准备工作

这个示例假设网络已经按照之前的所有示例中概述的方式构建和配置。

如何做...

  1. 安装 pyATS 所需的 Python 库:
$ sudo pip3 install pyats genie
  1. 创建roles目录,然后创建带有以下数据的requirements.yml文件:

 $ cat roles/requirements.yml
- src: https://github.com/CiscoDevNet/Ansible-pyats
 scm: git
 name: Ansible-pyats
  1. 按照以下代码安装Ansible-pyats角色:
 $ Ansible-galaxy install -r requirements.yml
  1. 创建一个名为pb_validate_pyats.yml的新 playbook,并填充以下任务以收集wan设备的ospf neighbor
---
 - name: Network Validation with pyATS
 hosts: wan
 roles:
 - Ansible-pyats
 vars:
 Ansible_connection: local
 tasks:
 - pyats_parse_command:
 command: show ip ospf neighbor
 register: ospf_output
 vars:
 Ansible_connection: network_cli
  1. 使用以下任务更新 playbook 以提取 OSPF 对等体信息的数据:
 - name: "FACT >> Pyats OSPF Info"
 set_fact:
 pyats_ospf_data: "{{ ospf_output.structured.interfaces }}"

 - name: " FACT >> Set OSPF peers"
 set_fact:
 OSPF_PEERS: "{{ wan_l3_links[inventory_hostname] | selectattr('ospf','equalto',true) | list }}"
  1. 使用以下任务更新 playbook 以验证 OSPF 对等体和 OSPF 对等状态:
 - name: Validate Number of OSPF Peers
 assert:
 that:
 - pyats_ospf_data | length == OSPF_PEERS | length
 loop: "{{ OSPF_PEERS }}"

 - name: Validate All Peers are in Full State
 assert:
 that:
 - pyats_ospf_data[item.name] | json_query('neighbors.*.state') | first == 'FULL/ -'
 loop: "{{ OSPF_PEERS }}"

工作原理...

在这个示例中,我们将探讨如何使用pyATS框架进行网络验证。pyATS是由思科开发的用于网络测试的测试框架的开源 Python 库。Genie是另一个 Python 库,提供了将基于 CLI 的输出转换为我们可以在自动化脚本中使用的 Python 数据结构的解析能力。思科发布了一个使用 pyATS 和 Genie 库的 Ansible 角色。在这个角色中,有多个模块可以用来构建更健壮的 Ansible 验证 playbook,以验证网络状态。为了开始使用这个角色,我们需要执行以下步骤:

  1. 使用python-pip安装pyatsenie Python 包。

  2. 使用 Ansible-galaxy 安装Ansible-pyats角色。

在这个示例中,我们使用了Ansible-pyats角色中的一个模块,即pyats_parse_command。该模块在远程管理设备上执行操作命令,并返回该命令的 CLI 输出和解析的结构化输出。以下代码片段概述了此模块在wan01设备上的ip ospf neigbor命令返回的结构化数据:

"structured": {
 "interfaces": {
 "GigabitEthernet2": {
 "neighbors": {
 "10.100.1.1": {
 "address": "10.3.1.1",
 "dead_time": "00:00:37",
 "priority": 0,
 "state": "FULL/ -"
 }
 }
 }
 }
}

我们将此模块返回的数据保存到ospf_output变量中,并使用set_fact模块来捕获此模块返回的结构化数据,然后将其保存到一个新变量pyats_ospf_data中。然后,我们使用set_fact模块来过滤在wan_l3_interfaces中定义的链接,只保留为 OSPF 启用的端口。

使用pyats_parse_command返回的结构化数据,我们可以验证这些数据,并使用assert模块将其与我们的 OSPF 对等体定义进行比较,以验证正确的 OSPF 对等体数量及其状态。

为了提取 OSPF 对等体状态,我们使用json_query过滤器来过滤返回的数据,并仅提供每个邻居的 OSPF 状态。

我们在 play 级别将Ansible_connection设置为local,并在pyats_parse_command任务级别将其设置为network_cli,因为我们只需要在此任务中连接到设备。所有其他任务可以在 Ansible 机器上本地运行。

另请参阅...

有关 PyATS 和 Genie 库以及如何将它们用于网络测试的更多信息,请参考以下网址:

developer.cisco.com/docs/pyats/#!introduction/pyats-genie

有关json_query及其语法的更多信息,请参考以下网址:

docs.Ansible.com/Ansible/latest/user_guide/playbooks_filters.html#json-query-filter

jmespath.org/tutorial.html

第三章:使用 Ansible 自动化 Juniper 设备的服务提供商

在本章中,我们将概述如何在典型的服务提供商SP)环境中自动化运行 Junos OS 软件的 Juniper 设备。我们将探讨如何使用 Ansible 与 Juniper 设备交互,以及如何使用各种 Ansible 模块在 Juniper 设备上配置不同的服务和协议。我们将以以下示例网络图为基础进行说明,该示例网络图显示了基本 SP 网络的拓扑:

以下表格概述了我们示例拓扑中的设备及其各自的管理Internet ProtocolsIPs):

设备 角色 供应商 管理(MGMT)端口 MGMT IP
mxp01 P 路由器 Juniper vMX 14.1 fxp0 172.20.1.2
mxp02 P 路由器 Juniper vMX 14.1 fxp0 172.20.1.3
mxpe01 PE 路由器 Juniper vMX 14.1 fxp0 172.20.1.4
mxpe02 PE 路由器 Juniper vMX 17.1 fxp0 172.20.1.5

本章涵盖的主要操作如下:

  • 构建网络清单

  • 连接并对 Juniper 设备进行身份验证

  • 在 Junos OS 设备上启用网络配置协议NETCONF

  • 在 Juniper 设备上配置通用系统选项

  • 在 Juniper 设备上配置接口

  • 在 Juniper 设备上配置开放最短路径优先OSPF

  • 在 Juniper 设备上配置多协议标签交换MPLS

  • 在 Juniper 设备上配置边界网关协议BGP

  • 在 Juniper 设备上部署配置

  • 在 Juniper 设备上配置第 3 层虚拟专用网络L3VPN)服务

  • 使用 Ansible 收集 Juniper 设备信息

  • 验证 Juniper 设备的网络可达性

  • 从 Juniper 设备检索操作数据

  • 使用 PyEZ 操作表验证网络状态

技术要求

本章的代码文件可以在此处找到:github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch3_junos

本章基于以下软件版本:

  • 运行 CentOS 7 的 Ansible 机器

  • Ansible 2.9

  • Juniper Virtual MX (vMX)运行 Junos OS 14.1R8 和 Junos OS 17.1R1 版本

查看以下视频以查看代码的实际操作:

bit.ly/3ajF4Mp

构建网络清单

在本操作中,我们将概述如何构建和组织 Ansible 清单,以描述先前概述的示例 SP 网络设置。Ansible 清单是 Ansible 的关键部分,因为它定义并分组应由 Ansible 管理的设备。

准备工作

我们创建一个新文件夹,用于存放本章中创建的所有文件。新文件夹名为ch3_junos

操作步骤...

  1. 在新文件夹ch3_junos中,我们创建一个hosts文件,内容如下:
$ cat hosts

[pe]
mxpe01 Ansible_host=172.20.1.3
mxpe02 Ansible_host=172.20.1.4

[p]
mxp01 Ansible_host=172.20.1.2
mxp02 Ansible_host=172.20.1.6

[junos]
mxpe[01:02]
mxp[01:02]

[core:children]
pe
p
  1. 创建一个Ansible.cfg文件,如下所示:
$ cat Ansible.cfg

 [defaults]
 inventory=hosts
 retry_files_enabled=False
 gathering=explicit
 host_key_checking=False

工作原理...

我们使用hosts文件构建 Ansible 清单,并定义多个组,以便对网络基础设施中的不同设备进行分组。

  • 我们创建PE组,引用拓扑中所有 MPLS Provider Edge (PE)节点。

  • 我们创建P组,引用拓扑中所有 MPLS Provider (P)节点。

  • 我们创建junos组,引用所有运行 Junos OS 的设备。

  • 我们创建core parent组,引用PEP组。

最后,我们创建Ansible.cfg文件并配置它指向我们的hosts文件,用作 Ansible 清单文件。我们将gathering设置为explicit,以禁用默认运行的 setup 模块,该模块用于发现受管主机的事实。禁用 setup 模块是强制性的,因为针对网络设备运行 setup 模块时会失败。

我们可以通过输入以下命令来验证我们的 Ansible 清单是否结构良好并正确编写:

$ Ansible-inventory --list

 "all": {
 "children": [
 "core",
 "junos",
 "ungrouped"
 ]
 },
 "core": {
 "children": [
 "p",
 "pe"
 ]
 },
 "junos": {
 "hosts": [
 "mxp01",
 "mxp02",
 "mxpe01",
 "mxpe02"
 ]
 },
 "p": {
 "hosts": [
 "mxp01",
 "mxp02"
 ]
 },
 "pe": {
 "hosts": [
 "mxpe01",
 "mxpe02"
 ]
 }

连接和身份验证 Juniper 设备

在这个示例中,我们将概述如何通过Secure ShellSSH)从 Ansible 连接和身份验证 Juniper 设备,以开始管理 Juniper 设备。我们将概述如何使用 SSH 密钥作为身份验证方法来建立 Ansible 和 Juniper 设备之间的通信。

准备工作

为了按照这个示例操作,应该按照上一个示例构建一个 Ansible 清单文件。Ansible 控制机器和网络中所有设备之间的 IP 可达性必须配置好。

如何操作...

  1. 在 Ansible 机器上,在我们的ch3_junos工作目录中创建私钥和公钥,如下所示:
$ SSH-keygen -t rsa -b 2048 -f Ansible_SSH_key

Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in Ansible_SSH_key.
Your public key has been saved in Ansible_SSH_key.pub.
The key fingerprint is:
SHA256:aCqgMYKAWIkv3nVz/q9cYp+2n3doD9jpgw/jeWWcVWI Ansible@centos7.localdomain
  1. 捕获在上一步创建的公钥,如下所示:
$ cat Ansible_SSH_key.pub
 SSH-rsa SSH-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0/wvdC5ycAanRorlfMYDMAv5OTcYAALlE2bdboajsQPQNEw1Li3N0J50OJBWXX+FFQuF7JKpM32vNQjQN7BgyaBWQGxv+Nj0ViVP+8X8Wuif0m6bFxBYSaPbIbGogDjPu4qU90Iv48NGOZpcPLqZthtuN7yZKPshX/0YJtXd2quUsVhzVpJnncXZMb4DZQeOin7+JVRRrDz6KP6meIylf35mhG3CV5VqpoMjYTzkDiHwIrFWVMydd4C77RQu27N2HozUtZgJy9KD8qIJYVdP6skzvp49IdInwhjOA+CugFQuhYhHSoQxRxpws5RZlvrN7/0h0Ahc3OwHaUWD+P7lz Ansible@centos7.localdomain
  1. 在 Juniper 设备上,添加一个名为admin的新用户,并指定我们将使用 SSH 密钥对对该用户进行身份验证。将在 Ansible 机器上创建的公钥复制到设备上,如下所示:
[edit system login]
Ansible@mxpe01# show
user admin {
 uid 2001;
 class super-user;
 authentication {
 SSH-rsa " SSH-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0/wvdC5ycAanRorlfMYDMAv5OTcYAALlE2bdboajsQPQNEw1Li3N0J50OJBWXX+FFQuF7JKpM32vNQjQN7BgyaBWQGxv+Nj0ViVP+8X8Wuif0m6bFxBYSaPbIbGogDjPu4qU90Iv48NGOZpcPLqZthtuN7yZKPshX/0YJtXd2quUsVhzVpJnncXZMb4DZQeOin7+JVRRrDz6KP6meIylf35mhG3CV5VqpoMjYTzkDiHwIrFWVMydd4C77RQu27N2HozUtZgJy9KD8qIJYVdP6skzvp49IdInwhjOA+CugFQuhYhHSoQxRxpws5RZlvrN7/0h0Ahc3OwHaUWD+P7lz Ansible@centos7.localdomain"; ## SECRET-DATA
 }
}

它是如何工作的...

我们首先在 Ansible 控制机器上使用SSH-keygen命令创建公钥和私钥,并指定以下选项:

  • 我们使用-t选项指定加密算法,并将其设置为rsa

  • 我们使用-b选项指定加密密钥的大小,并将大小设置为2048位。

  • 我们使用-f选项指定保存私钥和公钥的位置,并指定将生成的公钥和私钥的名称,即Ansible_SSH_key

一旦我们运行命令,我们将看到生成了以下两个文件(私钥和公钥),如下所示:

$ ls -la | grep Ansible_SSH_key
 -rw------- 1 Ansible Ansible 1679 Dec 31 23:41 Ansible_SSH_key
 -rw-r--r-- 1 Ansible Ansible 409 Dec 31 23:41 Ansible_SSH_key.pub

在我们的清单中的所有 Juniper 设备上,我们创建admin用户,并指定我们将使用 SSH 密钥进行身份验证。我们将在这个新用户的authentication部分下粘贴在 Ansible 控制机器上创建的公钥的内容。通过这个配置,任何拥有相应私钥的主机都可以作为admin用户进行身份验证并登录到 Juniper 设备。

为了测试和验证我们是否成功从计算节点登录到 Junos OS 设备,我们可以使用下面代码中显示的 Ansible 命令进行测试:

$ Ansible all -m ping -u admin --private-key Ansible_SSH_key -c network_cli

mxp02 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}
mxpe02 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}
mxpe01 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}
mxp01 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}

我们使用-u选项指定连接设备的用户名,并使用–private-key选项指定私钥。最后,我们使用-c选项来指定用于连接到受管设备的连接插件,这种情况下,我们使用network_cli连接插件来与受管 Juniper 设备建立 SSH 会话。

还有更多...

为了在我们的 playbooks 中使用生成的 SSH 密钥,我们可以在 Ansible 中的主机或组变量中指定用户名和 SSH 私钥文件,以便对 Juniper 设备进行身份验证。在我们的情况下,我们将这些变量设置为junos组的组变量。我们创建group_vars目录,并创建junos.yml文件,并按照下面的代码指定变量:

$ cat group_vars/junos.yml

Ansible_user: admin
 Ansible_SSH_private_key_file: Ansible_SSH_key

我们再次使用Ansible命令测试 Ansible 与我们的设备之间的连接;但是这次不指定任何参数,如下所示:

$ Ansible all -m ping -c network_cli

mxp02 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}
mxpe02 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}
mxpe01 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}
mxp01 | SUCCESS => {
 "changed": false,
 "ping": "pong"
} 

在 Junos OS 设备上启用 NETCONF

在这个示例中,我们将概述如何在 Junos OS 设备上启用 NETCONF 协议。这个任务非常关键,因为我们将在以后的所有示例中使用 NETCONF API 来管理 Juniper 设备。与传统的 SSH 访问方法相比,NETCONF API 提供了几个优势,这就是为什么我们将在与 Junos OS 设备的所有交互中使用它的原因。

准备工作

作为这个配方的先决条件,必须存在一个 Ansible 清单文件,并且必须部署和工作 SSH 认证,就像之前的配方一样。

如何做...

  1. 创建一个名为pb_jnpr_net_build.yml的新 playbook,如下面的代码所示:
$ cat pb_jnpr_net_build.yml

- name: Build Juniper SP Network
 hosts: junos
 tasks:
 - name: "Enable NETCONF"
 junos_netconf:
 netconf_port: 830
 state: present
 vars:
 Ansible_connection: network_cli
 tags: netconf
  1. 使用以下代码更新group_vars/junos.yml文件中的连接详情:
$ cat group_vars/junos.yml

Ansible_network_os: junos
Ansible_connection: netconf

它是如何工作的...

为了开始通过 NETCONF 与 Junos OS 设备进行交互,我们首先需要启用它,因此我们需要最初通过 SSH 连接到设备并启用 NETCONF。这就是为什么我们使用network_cli Ansible 连接来通过传统 SSH 连接到 Junos OS 设备。为了使用network_cli连接插件,我们需要将Ansible_network_os设置为junos

由于我们将在所有接下来的配方中使用 NETCONF API 与 Juniper 设备进行所有交互,因此我们只为此 playbook 中的junos_netconf任务启用了network_cli插件,通过vars属性。然而,对于我们将在此 playbook 中添加的所有未来任务,我们将使用group_vars/junos.yml文件中Ansible_connection属性中指定的netconf连接。

我们创建一个名为pb_jnpr_net_build.yml的新 playbook,在第一个任务中,我们使用junos_netconf模块在远程 Junos OS 设备上启用 NETCONF 协议。我们指定将使用的 NETCONF 端口(默认情况下为830),并且我们概述了这个配置必须通过state: present指令存在于远程设备上。

一旦我们运行 playbook,我们将看到所有的 Junos OS 设备都配置了 NETCONF,如下面的代码所示:

admin@mxpe01# show system services
SSH;
netconf {
 SSH {
 port 830;
 }
}

在 Juniper 设备上配置通用系统选项

在这个配方中,我们将概述如何在 Juniper 设备上配置一些通用的系统选项,比如主机名和域名系统DNS)服务器,并为用户进行配置。

准备工作

为了遵循这个配方,假设已经设置了一个 Ansible 清单,并且根据之前的配方,在所有 Juniper 设备上都启用了 NETCONF。

如何做...

  1. 使用以下参数更新group_vars/all.yml文件,以定义各种系统级参数,如dns和系统用户,如下面的代码所示:
$ cat group_vars/all.yml
tmp_dir: ./tmp
config_dir: ./configs
global:
 dns:
 - 172.20.1.1
 - 172.20.1.15
 root_pwd: $1$ciI4raxU$XfCVzABJKdALim0aWVMql0
 users:
 -   role: super-user
 SSH_key: Ansible_SSH_key.pub
 username: admin
 -   hash: $1$mR940Z9C$ipX9sLKTRDeljQXvWFfJm1
 passwd: 14161C180506262E757A60
 role: super-user
 username: Ansible
  1. 创建一个名为pb_jnpr_basic_config.yml的新 playbook,其中包含以下任务,用于在 Juniper 设备上设置dnshostname和系统用户:
$ cat pb_jnpr_basic_config.yml
---
- name: Configure Juniper Devices
 hosts: junos
 tasks:
 - name: "Conifgure Basic System config"
 junos_system:
 hostname: "{{ inventory_hostname }}"
 name_servers: "{{ global.dns }}"
 state: present
 - name: "Configure Users"
 junos_user:
 name: "{{ item.username }}"
 role: "{{ item.role }}"
 SSHkey: "{{ lookup ('file', item.SSH_key) }}"
 state: present
 with_items: "{{ global.users | selectattr('SSH_key','defined') | list }}"

它是如何工作的...

Ansible 提供了声明性模块来配置 Juniper 设备上的各种系统级参数。junos_system Ansible 模块使我们能够设置 Juniper 设备上的主机名和 DNS 服务器。junos_user模块为我们提供了在 Juniper 设备上设置系统用户的基本参数的能力。在这个例子中,我们设置了所有使用 SSH 密钥作为他们的认证方法的用户,并且我们循环遍历users数据结构,并且只选择定义了SSH_key选项的用户。

一旦我们运行这个 playbook,我们可以看到设备上的配置已经更新,如下面的代码块所示:

$ Ansible@mxpe01# show system
host-name mxpe01;
}
name-server {
 172.20.1.1;
 172.20.1.15;
}
login {
 user admin {
 uid 2001;
 class super-user;
 authentication {
 SSH-rsa "SSH-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0/wvdC5ycAanRorlfMYDMAv5OTcYAALlE2bdboajsQPQNEw1Li3N0J50OJBWXX+FFQuF7JKpM32vNQjQN7BgyaBWQGxv+Nj0ViVP+8X8Wuif0m6bFxBYSaPbIbGogDjPu4qU90Iv48NGOZpcPLqZthtuN7yZKPshX/0YJtXd2quUsVhzVpJnncXZMb4DZQeOin7+JVRRrDz6KP6meIylf35mhG3CV5VqpoMjYTzkDiHwIrFWVMydd4C77RQu27N2HozUtZgJy9KD8qIJYVdP6skzvp49IdInwhjOA+CugFQuhYhHSoQxRxpws5RZlvrN7/0h0Ahc3OwHaUWD+P7lz Ansible@centos7.localdomain"; ## SECRET-DATA
 }
 }

还有更多...

我们在本节中概述的声明性 Ansible 模块提供了一种简单的方法来配置 Juniper 设备的基本系统级参数。然而,它们可能不涵盖我们需要在 Juniper 设备上设置的所有参数。为了更好地控制和灵活地配置 Juniper 设备的系统级参数,我们可以使用 Jinja2 模板以及 Ansible 的template模块来生成我们部署所需的特定系统级配置。在本节中,我们将概述这种方法以实现这个目标,并且这是我们将在后续的配方中使用的方法来为其他设备生成配置。

我们将重用这种方法为 Juniper 设备的不同部分(如系统、接口、OSPF 和 MPLS)生成配置。我们将创建一个 Ansible 角色,以包含生成最终配置所需的所有 Jinja2 模板和任务。以下步骤概述了创建角色以及使用该角色生成配置所需的步骤:

  1. 创建一个新的roles目录,并添加一个名为build_router_config的新角色,目录结构如下:
$ tree roles/
 roles/
 └── build_router_config
 ├── tasks
 └── templates
  1. tasks文件夹下,创建一个名为build_config_dir.yml的 YAML 文件,以创建存储将生成的配置的所需文件夹,如下所示:
$ cat roles/build_router_config/tasks/build_config_dir.yml

---
- name: Create Config Directory
 file: path={{config_dir}} state=directory
 run_once: yes

- name: Create Temp Directory per Node
 file: path={{tmp_dir}}/{{inventory_hostname}} state=directory

- name: SET FACT >> Build Directory
 set_fact:
 build_dir: "{{tmp_dir}}/{{inventory_hostname}}"
  1. templates文件夹下,创建一个名为junos的新文件夹,在该文件夹中创建一个名为mgmt.j2的新 Jinja2 模板,内容如下:
$ cat roles/build_router_config/templates/junos/mgmt.j2

system {
 host-name {{inventory_hostname}};
 no-redirects;
{%  if global.dns is defined %}
 name-server {
{%      for dns_server in global.dns %}
 {{dns_server}};
{%      endfor %}
 }
{%  endif %}
 root-authentication {
 encrypted-password "{{ global.root_pwd}}"; ## SECRET-DATA
 }
 login {
{%      for user in global.users if user.hash is defined %}
 user {{ user.username }} {
 class super-user;
 authentication {
 encrypted-password "{{user.hash}}"; ## SECRET-DATA
 }
 }
{%      endfor %}
{%      for user in global.users if user.SSH_key is defined %}
 user {{ user.username }} {
 class {{ user.role }};
 authentication {
 SSH-rsa "{{lookup('file',user.SSH_key)}}"; ## SECRET-DATA
 }
 }
{%      endfor %}
 }
}
  1. tasks文件夹下,创建一个名为build_device_config.yml的新的 YAML 文件,其中包含以下任务来创建系统配置:
$ cat roles/build_router_config/tasks/build_device_config.yml

---
- name: "System Configuration"
 template:
 src: "{{Ansible_network_os}}/mgmt.j2"
 dest: "{{build_dir}}/00_mgmt.cfg"
 tags: mgmt
  1. tasks文件夹下创建一个名为main.yml的文件,其中包含以下任务:
$ cat roles/build_router_config/tasks/main.yml

---
- name: Build Required Directories
 import_tasks: build_config_dir.yml

- name: Build Device Configuration
 import_tasks: build_device_config.yml

- name: "Remove Old Assembled Config"
 file:
 path: "{{config_dir}}/{{ inventory_hostname }}.cfg"
 state: absent

- name: Build Final Device Configuration
 assemble:
 src: "{{ build_dir }}"
 dest: "{{config_dir}}/{{ inventory_hostname }}.cfg"

- name: Remove Build Directory
 file: path={{ tmp_dir }} state=absent
 run_once: yes
  1. 使用以下任务更新pb_jnpr_net_build.yml播放手册,以为我们清单中的所有 Juniper 设备生成配置:
$ cat pb_jnpr_net_build.yml

- name: Build Device Configuration
 import_role:
 name: build_router_config
 vars:
 Ansible_connection: local
 tags: build

在这种方法中,我们创建一个名为build_router_config的角色,并创建一个名为mgmt.j2的新 Jinja2 模板,其中包含 Junos OS 系统级配置的模板。我们使用 Ansible template模块来使用group_vars/all.yml文件中定义的 Ansible 变量渲染 Jinja2 模板。为了保存每个设备的配置,我们创建configs文件夹目录,其中存储每个设备的最终配置。

由于我们将使用这种方法为每个部分(MGMT、OSPF、MPLS 等)生成配置,我们将每个部分分割为单独的 Jinja2 模板,并将每个部分生成为单独的文件。我们使用assemble模块将所有这些不同部分组合成单个配置文件,并将其存储在configs目录中。这是每个设备的finalassembled配置文件。我们将每个部分的临时配置片段存储在每个设备的临时文件夹中,并在播放手册运行结束时删除此临时文件夹。这是因为我们已经为设备组装了最终配置,不再需要这些配置片段。

在这个手册中,我们将Ansible_connection设置为local,因为我们不需要连接到设备来运行角色中的任何任务。我们只在 Ansible 控制机上生成配置,因此所有任务都需要在 Ansible 控制机上本地运行。因此,无需连接到远程管理节点。

运行播放手册后,我们可以看到configs目录中创建了以下配置文件:

$ tree configs/
 configs/
 ├── mxp01.cfg
 ├── mxp02.cfg
 ├── mxpe01.cfg
 └── mxpe02.cfg

我们可以看到为mxpe01设备生成的配置,如下所示:

$ cat configs/mxpe01.cfg
system {
 host-name mxpe01;
 no-redirects;
 name-server {
 172.20.1.1;
 172.20.1.15;
 }
 root-authentication {
 encrypted-password "$1$ciI4raxU$XfCVzABJKdALim0aWVMql0"; ## SECRET-DATA
 }
 login {
 user Ansible {
 class super-user;
 authentication {
 encrypted-password "$1$mR940Z9C$ipX9sLKTRDeljQXvWFfJm1"; ##
SECRET-DATA
 }
 }
 user admin {
 class super-user;
 authentication {
 SSH-rsa "SSH-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC0/wvdC5ycAanRorlfMYDMAv5OTcYAALlE2bdboajsQPQNEw1Li3N0J50OJBWXX+FFQuF7JKpM32vNQjQN7BgyaBWQGxv+Nj0ViVP+8X8Wuif0m6bFxBYSaPbIbGogDjPu4qU90Iv48NGOZpcPLqZthtuN7yZKPshX/0YJtXd2quUsVhzVpJnncXZMb4DZQeOin7+JVRRrDz6KP6meIylf35mhG3CV5VqpoMjYTzkDiHwIrFWVMydd4C77RQu27N2HozUtZgJy9KD8qIJYVdP6skzvp49IdInwhjOA+CugFQuhYhHSoQxRxpws5RZlvrN7/0h0Ahc3OwHaUWD+P7lz Ansible@centos7.localdomain"; ## SECRET-DATA
 }
 }
 }
}

在随后的步骤中,我们将概述如何使用另一个 Ansible 模块将生成的配置推送到 Juniper 设备。

另请参阅...

有关 Ansible template模块和该模块支持的不同参数的更多信息,请参考以下网址:docs.ansible.com/ansible/latest/modules/template_module.html

有关 Ansible assemble模块和该模块支持的不同参数的更多信息,请参考以下网址:docs.ansible.com/ansible/latest/modules/assemble_module.html

配置 Juniper 设备上的接口

在这个步骤中,我们将概述如何管理 Juniper 设备上的接口。这使我们能够为我们的接口设置不同的参数,如最大传输单元MTU)和 Juniper 设备上的 IP 地址。

准备工作

为了按照这个步骤进行操作,假设已经设置了 Ansible 清单,并且在所有 Juniper 设备上启用了 NETCONF,就像之前的步骤一样。

如何做…

  1. 更新group_vars/all.yml YAML 文件,包括我们示例网络拓扑中所有点对点P2P)和环回接口的以下数据:
p2p_ip:
 mxp01:
 - {port: ge-0/0/0, ip: 10.1.1.2 , peer: mxpe01, pport: ge-0/0/0, peer_ip: 10.1.1.3}
 - {port: ge-0/0/1, ip: 10.1.1.4 , peer: mxpe02, pport: ge-0/0/0, peer_ip: 10.1.1.5}
 - {port: ge-0/0/3, ip: 10.1.1.0 , peer: mxp02, pport: ge-0/0/3, peer_ip: 10.1.1.1}
 mxp02:
 <-- Output Trimmed for brevity ------>
 mxpe01:
 <-- Output Trimmed for brevity ------>
 mxpe02:
 <-- Output Trimmed for brevity ------>
 xrpe03:
 <-- Output Trimmed for brevity ------>
lo_ip:
 mxp01: 10.100.1.254/32
 mxp02: 10.100.1.253/32
 mxpe01: 10.100.1.1/32
 mxpe02: 10.100.1.2/32
 xrpe03: 10.100.1.3/32
  1. pb_jnpr_basic_config.yml playbook 中更新以下任务,以在我们的 Juniper 设备上设置接口:
- name: "Configure the Physical Interfaces"
 junos_interface:
 name: "{{ item.port }}"
 enabled: true
 description: "peer:{{item.peer}} remote_port:{{item.pport }}"
 mtu: "{{ global.mtu | default(1500) }}"
 with_items: "{{p2p_ip[inventory_hostname]}}"
 tags: intf

- name: "Configure IP Addresses"
 junos_l3_interface:
 name: "{{ item.port }}"
 ipv4: "{{ item.ip }}/{{ global.p2p_prefix }}"
 state: present
 with_items: "{{ p2p_ip[inventory_hostname] }}"
 tags: intf

它是如何工作的…

我们在group_vars/all.yml文件中的两个主要数据结构中定义了示例网络拓扑中所有接口的所有数据。我们使用p2p_ip字典来模拟示例网络中所有 P2P IP 地址,并使用lo_ip字典来指定节点的环回 IP 地址。

我们使用junos_interface Ansible 模块来启用接口并为接口设置基本参数,如 MTU 和描述。我们循环遍历每个设备的p2p_ip数据结构,并为网络清单中的所有设备上的每个接口设置正确的参数。我们使用junos_l3_interface Ansible 模块在示例网络拓扑中的所有设备上设置正确的 IPv4 地址。

一旦我们运行了 playbook,我们可以看到接口已经按照要求配置,就像在mxpe01设备上显示的那样:

Ansible@mxpe01# show interfaces
ge-0/0/0 {
 description "peer:mxp01 remote_port:ge-0/0/0";
 mtu 1500;
 unit 0 {
 family inet {
 address 10.1.1.3/31;
 }
 }
}
ge-0/0/1 {
 description "peer:mxp02 remote_port:ge-0/0/0";
 mtu 1500;
 unit 0 {
 family inet {
 address 10.1.1.9/31;
 }
 }
}

还有更多…

如果我们需要对接口配置有更多的控制,并设置声明式 Ansible 模块中未涵盖的参数,我们可以使用 Jinja2 模板来实现这个目标。使用与我们在上一步骤中概述的完全相同的方法,我们可以为我们的 Juniper 设备生成所需的接口配置。

使用我们在上一步骤中创建的相同的 Ansible 角色,我们可以扩展它以为我们的 Juniper 设备生成接口配置。我们使用以下步骤来完成这个任务:

  1. templates文件夹中创建一个名为intf.j2的新的 Jinja2 模板文件,包含以下数据:
$ cat roles/build_router_config/templates/junos/intf.j2

interfaces {
{% for intf in p2p_ip[inventory_hostname] | sort(attribute='port') %}
 {{ intf.port.split('.')[0] }} {
 description "peer:{{intf.peer}} -- peer_port: {{intf.pport}}"
 unit 0 {
 family inet {
 address {{intf.ip}}/{{global.p2p_prefix}};
 }
 family mpls;
 }
 }
 {% endif %}
 {% endfor %}
 lo0 {
 unit 0 {
 family inet {
 address {{lo_ip[inventory_hostname]}};
 }
 }
 }
  1. tasks目录下更新build_device_config.yml文件,添加新任务以生成接口配置,如下所示:
$ cat roles/build_router_config/tasks/build_device_config.yml

<-- Output Trimmed for brevity ------>

- name: "Interface Configuration"
 template:
 src: "{{Ansible_network_os}}/intf.j2"
 dest: "{{build_dir}}/01_intf.cfg"
 tags: intf

这是在运行 playbook 后为mxp02设备生成的接口配置:

interfaces {
 ge-0/0/0 {
 description "peer:mxpe01 -- peer_port: ge-0/0/1"
 unit 0 {
 family inet {
 address 10.1.1.8/31;
 }
 family mpls;
 }
 }
 ge-0/0/1 {
 description "peer:mxpe02 -- peer_port: ge-0/0/1"
 unit 0 {
 family inet {
 address 10.1.1.10/31;
 }
 family mpls;
 }
 }
<--   Output Trimmed for brevity ------>

 lo0 {
 unit 0 {
 family inet {
 address 10.100.1.253/32;
 }
 }
 }

在 Juniper 设备上配置 OSPF

在这个步骤中,我们将概述如何在 Juniper 设备上配置 OSPF 作为我们示例网络拓扑中的内部网关协议IGP),以及不同的 OSPF 参数,如 OSPF 链路类型和 OSPF 接口成本。

如何做…

  1. templates/junos目录中创建一个名为ospf.j2的新的 Jinja2 文件,包含以下数据:
$ cat roles/build_router_config/templates/junos/ospf.j2

 protocols {
 ospf {
 area {{global.ospf_area}} {
{%          for intf in p2p_ip[inventory_hostname]|sort(attribute='port') %}
 interface {{ intf.port }} {
 interface-type p2p;
 metric {{intf.cost | default(100)}};
 }
{%          endfor %}
 interface lo0.0 {
 passive;
 }
 }
 }
}
  1. tasks文件夹中的junos_build_config.yml文件中,添加以下任务:
$ cat roles/build_router_config/tasks/build_device_config.yml

<-- Output Trimmed for brevity ------>

- name: "OSPF Configuration"
 template:
 src: "{{Ansible_network_os}}/ospf.j2"
 dest: "{{config_dir}}/{{ inventory_hostname }}/02_ospf.cfg"

它是如何工作的…

我们使用在all.yml文件中声明的p2p_ip数据结构中声明的相同接口数据,以便在我们的示例网络中的网络设备上配置 OSPF。我们使用在templates/junos目录下定义的新的 Jinja2 模板来捕获需要在 Juniper 设备上实现的 OSPF 配置参数(OSPF 成本、OSPF 接口类型等)。

tasks/Juniper_build_config.yml文件中,添加一个使用ospf.j2 Jinja2 模板来渲染 Jinja2 模板,并输出我们 Ansible 清单中每个设备的 OSPF 配置部分的新任务。

在运行带有新任务的 playbook 后,以下片段概述了为mxpe01设备生成的 OSPF 配置:

$ cat configs/mxpe01.cfg

 <--   Output Trimmed for brevity ------>

protocols {
 ospf {
 area 0 {
 interface ge-0/0/0 {
 interface-type p2p;
 metric 100;
 }
 interface ge-0/0/1 {
 interface-type p2p;
 metric 100;
 }
 interface lo0.0 {
 passive;
 }
 }
 }
}

在 Juniper 设备上配置 MPLS

在这个方法中,我们将概述如何在 Juniper 设备上配置 MPLS 和一些相关协议,如标签分发协议LDP)和资源预留协议RSVP)。我们将概述如何使用 Ansible 和 Jinja2 模板生成所需的 MPLS 配置。

如何做...

  1. templates/junos目录下创建一个名为mpls.j2的新的 Jinja2 文件,包含以下数据:
$ cat roles/build_router_config/templates/junos/mpls.j2

 protocols {
 ldp {
{%      for intf in p2p_ip[inventory_hostname]|sort(attribute='port') %}
 interface {{intf.port}}.{{intf.vlan|default('0')}};
{%      endfor %}
 interface lo0.0;
 }
 rsvp {
{%      for intf in p2p_ip[inventory_hostname]|sort(attribute='port') %}
 interface {{intf.port}}.{{intf.vlan|default('0')}};
{%      endfor %}
 }
 mpls {
{%      for intf in p2p_ip[inventory_hostname]|sort(attribute='port') %}
 interface {{intf.port}}.{{intf.vlan|default('0')}};
{%      endfor %}
 }
}
  1. tasks文件夹中的build_device_config.yml文件中,添加以下任务:
$ cat roles/build_router_config/tasks/build_device_config.yml

<-- Output Trimmed for brevity ------>

- name: "MPLS Configuration"
 template:
 src: "{{Ansible_network_os}}/mpls.j2"
 dest: "{{config_dir}}/{{ inventory_hostname }}/03_mpls.cfg"

它是如何工作的...

我们使用与配置接口和 OSPF 相同的方法,通过使用 Jinja2 模板为我们库存中的 Juniper 设备生成所需的 MPLS 配置,以下是mxpe02路由器的 MPLS 配置示例:

protocols {
    ldp {
        interface ge-0/0/0.0;
        interface ge-0/0/1.0;
        interface lo0.0;
    }
    rsvp {
        interface ge-0/0/0.0;
        interface ge-0/0/1.0;
    }
    mpls {
        interface ge-0/0/0.0;
        interface ge-0/0/1.0;
    }
}

在 Juniper 设备上配置 BGP

在这个方法中,我们将概述如何在 Juniper 设备上配置 BGP。我们将概述如何设置 BGP 和 BGP路由反射器RR)作为我们示例拓扑的一部分,以及支持虚拟专用网络VPN)服务所需的所有 BGP 地址族。

如何做...

  1. 更新group_vars/all.yml文件,包含以下 BGP 信息:
bgp_topo:
  rr: mxp01
  af:
  - inet
  - inet-vpn
  1. 对于 Ansible 库存中的每个节点,我们在host_vars目录下创建一个名为bgp.yml的文件。该文件保存每个节点的 BGP 信息和 BGP 对等方。这是mxpe01设备的示例:
$ cat host_vars/mxpe01/bgp.yml

bgp_asn: 65400

bgp_peers:
 - local_as: 65400
 peer: 10.100.1.254
 remote_as: 65400
  1. templates/junos目录下创建一个名为bgp.j2的新的 Jinja2 文件,包含以下数据:
$ cat roles/build_router_config/templates/junos/bgp.j2

 protocols {
{%  if bgp_peers is defined %}
 bgp {
 group Core {
 type internal;
 local-address {{ lo_ip[inventory_hostname] | ipaddr('address')}};
{%          if bgp_topo.rr == inventory_hostname %}
 cluster {{ lo_ip[inventory_hostname].split('/')[0] }};
{%          endif %}
{%          for af in bgp_topo.af %}
{%          if af == 'inet' %}
 family inet {
 unicast;
 }
{%          endif %}
{%          if af == 'inet-vpn' %}
 family inet-vpn {
 unicast;
 }
{%          endif %}
<--   Output Trimmed for brevity ------>
{%          endfor %}
{%          for p in bgp_peers %}
 neighbor {{ p.peer}};
{%          endfor %}
 }
 }
{%  endif %}
}
  1. tasks文件夹中的build_device_config.yml文件中,添加以下突出显示的任务:
$ cat roles/build_router_config/tasks/build_device_config.yml

<-- Output Trimmed for brevity ------>

- name: "BGP Configuration"
 template:
 src: "{{Ansible_network_os}}/bgp.j2"
 dest: "{{config_dir}}/{{ inventory_hostname }}/04_bgp.cfg"

它是如何工作的...

与之前的方法类似,我们使用 Jinja2 模板为 Juniper 设备生成 BGP 配置。然而,在本节中,我们在两个不同的位置声明 BGP 参数,即group_varshost_vars目录。在group_vars/all.yml文件中,我们声明了 BGP 拓扑的整体参数,例如我们将使用的 RR 以及我们将配置哪些地址族。对于库存中的每个节点,我们在host_vars目录中创建一个目录,并在该目录中创建一个bgp.yml文件。这个新的 YAML 文件保存了我们库存中每个节点的 BGP 对等方。我们使用这两个位置定义的数据来为每个设备渲染 BGP 配置。

这是mxp01路由器的 BGP 配置示例,它是我们拓扑中的 RR:

protocols {
 bgp {
 group Core {
 type internal;
 local-address 10.100.1.254;
 cluster 10.100.1.254;
 family inet {
 unicast;
 }
 family inet-vpn {
 unicast;
 }
 neighbor 10.100.1.1;
 neighbor 10.100.1.2;
 neighbor 10.100.1.3;
 }
 }
}

在 Juniper 设备上部署配置

在这个方法中,我们将概述如何使用 Ansible 在 Juniper 设备上推送我们通过 Jinja2 模板在所有先前部分生成的配置。这使我们能够将我们创建的任何自定义配置推送到 Juniper 设备。

准备工作

此方法要求 Juniper 设备上启用 NETCONF。

如何做...

  1. pb_junos_push_con文件中,添加以下任务:
$ cat pb_jnpr_net_build.yml

<-- Output Trimmed for brevity ------>

- name: "Deploy Configuration"
 junos_config:
 src: "{{config_dir}}/{{ inventory_hostname }}.cfg"

它是如何工作的...

在先前的方法中,我们使用assemble模块来将 Juniper 设备的不同部分的配置(如接口、OSPF、MPLS 和 BGP)分组到单个配置文件中。该文件存储在每个设备的configs文件夹中。

我们使用junos_config模块来将我们生成的配置文件推送到网络库存中的每个设备。我们可以使用update参数来控制我们要推送的配置如何与设备上的现有配置合并。它支持以下选项:

  • 合并:这会导致我们文件中的配置与设备上的配置(候选配置)合并。这是默认选项。

  • 覆盖/更新:这会导致我们文件中的配置覆盖受管设备上的完整配置。

我们可以使用check模式以干运行模式运行我们的 playbook。在这种情况下,我们将推送配置到设备,而不提交配置。这使我们能够检查将推送到设备的更改。可以按以下方式完成:

$ Ansible-playbook pb_jnpr_net_build.yml -l mxpe01 --check –diff

我们使用-check选项以检查模式(干运行)运行 playbook,并使用-diff选项以输出将推送到我们设备的更改。

还有更多...

junos_config模块还支持 Junos OS 支持的回滚功能,因此我们可以添加另一个任务来回滚配置并控制其运行,如下所示:

$ cat pb_jnpr_net_build.yml

<-- Output Trimmed for brevity ------>

- name: "Rollback config"
 junos_config:
 rollback: "{{ rollback | default('1') | int }}"
 tags: rollback, never

在前面的 playbook 中,我们回滚到配置的上一个版本。但是,通过更改rollback属性中的数字,我们可以控制要回滚到的配置版本。此外,我们使用标签来仅在 playbook 运行期间指定rollback标签时执行此任务,如下面的代码片段所示:

$ Ansible-playbook pb_jnpr_net_build.yml --tags rollback -l mxpe01

我们可以指定另一个回滚点,如下所示:

$ Ansible-playbook pb_jnpr_net_build.yml --tags rollback -l mxpe01 –e rollback=2

另请参阅...

有关junos_config模块和此模块支持的不同参数的更多信息,请参阅以下网址:docs.ansible.com/ansible/latest/modules/junos_config_module.html.

在 Juniper 设备上配置 L3VPN 服务

在本教程中,我们将概述如何使用各种 Ansible 模块在 Juniper 设备上建模和配置 L3VPN。这使我们能够使用基础设施即代码IaC)实践来建模我们的服务,并利用 Ansible 来部署和推送所需的配置,以在 Juniper 设备上部署 L3VPN。

准备工作

Juniper 设备上必须启用 NETCONF 才能在本教程中使用 Ansible 模块。

如何做...

  1. 创建一个名为l3vpn.yml的新文件,内容如下:
---
l3vpns:
 vpna:
 state: present
 rt: "target:{{bgp_asn}}:10"
 rd: "1:10"
 sites:
 - node: mxpe01
 port: ge-0/0/3.10
 ip: 172.10.1.1/24
 - node: mxpe02
 port: ge-0/0/3.10
 ip: 172.10.2.1/24
 vpnb:
 state: present
 rt: "target:{{bgp_asn}}:20"
 rd: "1:20"
 sites:
 - node: mxpe01
 port: ge-0/0/3.20
 ip: 172.20.1.1/24
 - node: mxpe02
 port: ge-0/0/3.20
 ip: 172.20.2.1/24
  1. 创建一个名为pb_junos_l3vpn.yml的新 playbook,其中包含以下任务,以配置 PE-客户边缘CE)链路:
---
- name: "Deploy L3VPNs on Juniper Devices"
 hosts: pe
 vars_files:
 - "l3vpn.yml"
 tasks:
 - name: "Set VPN Interfaces"
 set_fact:
 l3vpn_intfs: "{{ l3vpn_intfs|default([]) +
 l3vpns[item.key].sites |
 selectattr('node','equalto',inventory_hostname) | list}}"
 with_dict: "{{l3vpns}}"
 delegate_to: localhost

 - name: "Configure Interfaces for L3VPN Sites"
 junos_config:
 lines:
 - set interfaces {{ item.port.split('.')[0]}} vlan-tagging
 - set interfaces {{ item.port}} vlan-id {{ item.port.split('.')[1] }}
 loop: "{{ l3vpn_intfs }}"
  1. pb_junos_l3vpn.yml中添加以下任务以设置 PE-CE 链路上的 P2P IP 地址:
- name: "Configure IP address for L3VPN Interfaces"
 junos_l3_interface:
 name: "{{ item.port.split('.')[0]}}"
 ipv4: "{{ item.ip }}"
 unit: "{{ item.port.split('.')[1] }}"
 loop: "{{l3vpn_intfs}}"
 tags: intf_ip
  1. pb_junos_l3vpn.yml中添加以下任务以在 PE 节点上配置虚拟路由和转发VRFs):
- name: "Configure L3VPNs"
 junos_vrf:
 name: "{{ item.key }}"
 rd: "{{item.value.rd}}"
 target: "{{ item.value.rt }}"
 interfaces: "{{ l3vpns[item.key].sites |
 map(attribute='port') | list }}"
 state: "{{ item.value.state }}"
 with_dict: "{{l3vpns}}"
 when: inventory_hostname in (l3vpns[item.key].sites | map(attribute='node') | list)
 tags: l3vpn

工作原理...

我们创建一个名为l3vpn.yml的新的 YAML 文件,描述和建模我们想要在拓扑上的所有 Juniper 设备上实现的 L3VPN 拓扑和数据。我们在新的 playbook 中包含这个文件,以便在我们的网络设备上配置 L3VPN。

pb_junos_l3vpn.yml playbook 中,我们使用l3vpn.yml文件中的数据来捕获所需的数据以配置 L3VPN。

在我们的 playbook 中的第一个任务中,我们创建一个名为l3vpn_intfs的新变量,该变量捕获我们在l3vpn.yml文件中定义的所有 VPN 的每个 PE 设备上的所有 L3VPN 接口。我们在此文件中循环遍历所有 L3VPN,并为属于特定节点的所有接口创建一个新的列表数据结构。以下代码片段概述了mxpe01l3vpn_intfs的新数据结构:

ok: [mxpe01 -> localhost] => {
 "l3vpn_intfs": [
 {
 "ip": "172.10.1.1/24",
 "node": "mxpe01",
 "port": "ge-0/0/3.10"
 },
 {
 "ip": "172.20.1.1/24",
 "node": "mxpe01",
 "port": "ge-0/0/3.20"
 }
 ]
}

接下来,在我们的 playbook 中,我们将我们的 L3VPN 服务的配置分为多个任务:

  • 我们使用junos_config模块配置所有属于 L3VPN 的接口,以便在这些接口上配置虚拟局域网VLANs)。

  • 我们使用junos_l3_interface模块在所有属于我们的 L3VPN 模型的接口上应用 IPv4 地址。

  • 我们使用junos_vrf模块根据我们的 L3VPN 数据模型在节点上配置正确的路由实例。

在运行此 playbook 后,以下概述了应用在mxpe01上的 L3VPN 配置:


Ansible@mxpe01> show configuration routing-instances
vpna {
 instance-type vrf;
 interface ge-0/0/3.10;
 route-distinguisher 1:10;
 vrf-target target:65400:10;
 vrf-table-label;
}
vpnb {
 instance-type vrf;
 interface ge-0/0/3.20;
 route-distinguisher 1:20;
 vrf-target target:65400:20;
 vrf-table-label;
}

另请参阅...

有关junos_vrf模块和该模块支持的不同参数以在 Juniper 设备上配置 L3VPN 的更多信息,请参考以下网址:docs.ansible.com/ansible/latest/modules/junos_vrf_module.html#junos-vrf-module. 

使用 Ansible 收集 Juniper 设备信息

在这个配方中,我们将检索 Ansible 为 Juniper 设备收集的基本系统信息。这些基本系统信息为我们提供了有关 Juniper 设备的基本健康检查,我们可以用来验证其操作状态。

准备工作

Juniper 设备上必须启用 NETCONF 才能在此配方中使用 Ansible 模块。

如何做...

  1. 创建一个新的 playbook,pb_jnpr_facts.yml,使用以下任务来收集信息:
$ cat pb_jnpr_facts.yml

---
- name: Collect and Validate Juniper Facts
 hosts: junos
 tasks:
 - name: Collect Juniper Facts
 junos_facts:
  1. 使用以下任务更新pb_jnpr_facts.yml playbook,为清单中的每个节点创建一个信息报告:
 - name: Create Facts Folder
 file: path=device_facts state=directory
 run_once: yes

 - name: Create Basic Device Facts Report
 blockinfile:
 path: "device_facts/{{ inventory_hostname }}.txt"
 block: |
 device_name: {{ Ansible_net_hostname }}
 model: {{ Ansible_net_system }} {{ Ansible_net_model }}
 os_version: {{ Ansible_net_version }}
 serial_number: {{ Ansible_net_serialnum }}
 create: yes
  1. 使用以下任务更新 playbook 以验证核心接口的操作状态:
 - name: Validate all Core Interface are Operational
 assert:
 that:
 - Ansible_net_interfaces[item.port]['oper-status'] == 'up'
 fail_msg: "Interface {{item.port}} is not Operational "
 loop: "{{ p2p_ip[inventory_hostname] }}"

工作原理...

Ansible 提供了一个信息收集模块,用于收集 Juniper 设备的基本系统属性,并以一致和结构化的数据结构返回这些信息。我们可以使用该模块收集的信息来验证设备的基本属性和操作状态,并可以使用这些数据生成捕获设备状态的简单报告。

在这个配方中,我们使用junos_facts模块来收集所有 Juniper 设备的设备信息。该模块返回 Ansible 为每个设备收集的基本信息,并存储在多个变量中,如下所示:

"Ansible_net_serialnum": "VM5D112EFB39",
"Ansible_net_system": "junos",
"Ansible_net_version": "17.1R1.8",
"Ansible_network_os": "junos",

我们使用这些数据来使用blockinfile模块为每个设备构建信息报告,并使用这些数据来验证每个设备的核心接口的操作状态。

一旦我们运行了 playbook,我们就可以看到为每个设备生成了一个信息报告,如下所示:

$ tree device_facts/

device_facts/
 ├── mxp01.txt
 ├── mxp02.txt
 ├── mxpe01.txt
 └── mxpe02.txt

 $ cat device_facts/mxp01.txt

device_name: mxp01
 model: junos vmx
 os_version: 14.1R4.8
 serial_number: VM5701F131C6

在最后一个任务中,我们使用assert模块来验证所有 Juniper 设备上的所有核心接口是否都处于操作状态。Ansible 将设备的所有接口的操作状态存储在Ansible_net_interfaces下。我们使用这个数据结构中的数据来验证操作状态是否正常。如果所有核心接口都处于操作状态,任务将成功,否则任务将失败。

另请参阅...

有关junos_facts模块和该模块支持的不同参数的更多信息,请参考以下网址:docs.ansible.com/ansible/latest/modules/junos_facts_module.html.

验证 Juniper 设备的网络可达性

在这个配方中,我们将概述如何使用 Ansible 在 Juniper 设备上验证通过ping进行网络可达性。这将使我们能够验证样本网络拓扑中的网络可达性和流量转发。

准备工作

这个配方假设网络已经建立和配置,就像在所有先前的配方中所概述的那样。

如何做...

  1. 创建一个名为pb_junos_ping.yml的新 playbook,使用以下任务来 ping 样本网络中的所有核心 loopback:
---
- name: "Validate Core Reachability"
 hosts: junos
 tasks:
 - name: "Ping Across All Loopback Interfaces"
 junos_ping:
 dest: "{{ item.value.split('/')[0] }}"
 interface: lo0.0
 size: 512
 with_dict: "{{lo_ip}}"
 vars:
 Ansible_connection: network_cli
 register: ping_rst
 ignore_errors: yes
  1. 使用以下任务更新pb_junos_ping.yml playbook,创建一个自定义报告来捕获 ping 的结果:
 - name: Create Ping Report
 blockinfile:
 block: |
 Node | Destination | Packet Loss | Delay |
 -----| ------------| ------------| ------|
 {% for node in play_hosts %}
 {% for result in hostvars[node].ping_rst.results %}
 {% if result.rtt is defined %}
 {{ node }} | {{ result.item.value }} | {{ result.packet_loss }} | {{ result.rtt.avg }}
 {% else %}
 {{ node }} | {{ result.item.value }} | {{ result.packet_loss }} | 'N/A'
 {% endif %}
 {% endfor %}
 {% endfor %}
 path: ./ping_report.md
 create: yes
 run_once: yes

工作原理...

我们使用junos_ping模块从网络清单中的所有节点向group_vars/all.yml文件中定义的lo_ip数据结构中定义的所有环回接口执行 ping。此模块连接到每个设备并对所有目的地执行 ping,并验证 ping 数据包是否到达其预期目的地。此模块需要使用network_cli连接插件,因此我们将此参数作为任务变量提供,以覆盖组级别在组级别定义的 NETCONF 连接插件。

我们注册模块的输出,以便使用这些数据生成 ping 报告。最后,我们将ignore_errors设置为yes,以忽略可能遇到的任何 ping 任务失败,并确保我们将运行后续任务以创建报告。

我们使用blockinfile模块创建 Markdown 中的自定义报告。我们使用表格布局来捕获 ping 结果并显示捕获这些 ping 结果的表格。以下片段捕获了为mxpe01 ping 测试报告生成的表格:

$ cat ping_report.md

# BEGIN ANSIBLE MANAGED BLOCK
 Node | Destination | Packet Loss | Delay |
 -----| ------------| ------------| ------|
 mxpe01 | 10.100.1.254/32 | 0% | 3.75
 mxpe01 | 10.100.1.253/32 | 0% | 2.09
 mxpe01 | 10.100.1.1/32 | 0% | 0.27
 mxpe01 | 10.100.1.2/32 | 0% | 4.72
 mxpe01 | 10.100.1.3/32 | 100% | 'N/A'
 # END ANSIBLE MANAGED BLOCK

以下是 ping 结果的渲染 Markdown 表格:

另请参阅...

有关junos_ping模块和此模块支持的不同参数的更多信息,请参阅以下网址:docs.ansible.com/ansible/latest/modules/junos_ping_module.html.

从 Juniper 设备检索操作数据

在此步骤中,我们将概述如何在 Juniper 设备上执行操作命令并将这些输出存储为文本文件以进行进一步处理。

准备工作

NETCONF 必须在 Juniper 设备上启用,以便按照此步骤进行操作。

如何做...

  1. 安装jxmlease Python 包,如下所示:
$ pip3 install jxmlease
  1. 创建一个名为pb_get_ospf_peers.yml的新剧本,并填充以下任务以提取 OSPF 对等信息:
---
- name: "Get OSPF Status"
 hosts: junos
 tasks:
 - name: "Get OSPF Neighbours Data"
 junos_command:
 commands: show ospf neighbor
 display: xml
 register: ospf_output

 - name: "Extract OSPF Neighbour Data"
 set_fact:
 ospf_peers: "{{ ospf_output.output[0]['rpc-reply']\
 ['ospf-neighbor-information']['ospf-neighbor'] }}"
  1. 更新pb_get_ospf_peers.yml剧本,使用以下任务验证所有节点上的所有 OSPF 对等关系是否处于Full状态:
 - name: "Validate All OSPF Peers are in Full State"
 assert:
 that: item['ospf-neighbor-state'] == 'Full'
 fail_msg: "Peer on Interface {{item['interface-name']}} is Down"
 success_msg: "Peer on Interface {{item['interface-name']}} is UP"
 loop: "{{ospf_peers}}"

工作原理...

使用 NETCONF API 与 Juniper 设备交互的一个优势是,我们可以获取我们在 Juniper 设备上执行的所有操作命令的结构化输出。设备通过 NETCONF 会话返回给我们的输出是 XML 格式的,Ansible 使用名为jxmlease的 Python 库来解码此 XML 并将其转换为 JSON 以进行更好的表示。这就是为什么我们的第一个任务是安装jxmlease Python 包。

我们使用junos_command模块向 Juniper 设备发送操作命令,并指定我们需要 XML 作为从节点返回的输出格式。Ansible 使用jxmlease包将此 XML 数据结构转换为 JSON。我们使用register关键字将此数据保存到名为ospf_output的新变量中。以下是从此命令返回的 JSON 数据的示例:

 "msg": [
 {
 "rpc-reply": {
 "ospf-neighbor-information": {
 "ospf-neighbor": [
 {
 "activity-timer": "34",
 "interface-name": "ge-0/0/0.0",
 "neighbor-address": "10.1.1.2",
 "neighbor-id": "10.100.1.254",
 "neighbor-priority": "128",
 "ospf-neighbor-state": "Full"
 },
 {
 "activity-timer": "37",
 "interface-name": "ge-0/0/1.0",
 "neighbor-address": "10.1.1.8",
 "neighbor-id": "10.100.1.253",
 "neighbor-priority": "128",
 "ospf-neighbor-state": "Full"
 }
 ]
 }
 }
 }
 ]

所有这些数据结构都包含在ospf_output.output[0]变量中,我们使用set_fact模块来捕获ospf-neigbour数据。之后,我们使用assert模块循环遍历此数据结构中的所有 OSPF 对等关系,并验证 OSPF 邻居状态是否等于Full。如果所有 OSPF 对等关系都处于Full状态,则任务将成功。但是,如果 OSPF 状态处于任何其他状态,则任务将失败。

还有更多...

如果我们需要以文本格式从 Juniper 设备获取操作数据以进行日志收集,我们可以使用junos_command模块,而不使用xml显示选项,如下所示在此新剧本中:

$ cat pb_collect_output.yml

---
- name: Collect Network Logs
 hosts: junos
 vars:
 log_folder: "logs"
 op_cmds:
 - show ospf neighbor
 tasks:
 - name: "P1T1: Build Directories to Store Data"
 block:
 - name: "Create folder to store Device config"
 file:
 path: "{{ log_folder }}"
 state: directory
 run_once: yes
 delegate_to: localhost

 - name: "P1T2: Get Running configs from Devices"
 junos_command:
 commands: "{{ item }}"
 loop: "{{ op_cmds }}"
 register: logs_output

 - name: "P1T3: Save Running Config per Device"
 copy:
 content: "{{ item.stdout[0] }}"
 dest: "{{ log_folder }}/{{inventory_hostname}}_{{ item.item | regex_replace(' ','_') }}.txt"
 loop: "{{ logs_output.results }}"
 delegate_to: localhost

这个 playbook 将从所有设备收集show ospf neigbor命令,并将它们存储在一个名为logs的新文件夹中。运行 playbook 后,这是logs文件夹的内容:

$ tree logs
 logs
 ├── mxp01_show_ospf_neighbor.txt
 ├── mxp02_show_ospf_neighbor.txt
 ├── mxpe01_show_ospf_neighbor.txt
 └── mxpe02_show_ospf_neighbor.txt

我们可以检查其中一个文件的内容,以确认已捕获所需的输出:


 $ cat logs/mxpe01_show_ospf_neighbor.txt

Address Interface State ID Pri Dead
 10.1.1.2 ge-0/0/0.0 Full 10.100.1.254 128 35
 10.1.1.8 ge-0/0/1.0 Full 10.100.1.253 128 37

使用 PyEZ 操作表验证网络状态

在这个配方中,我们将概述如何使用 Juniper 自定义 Ansible 模块来验证网络状态。我们将使用 Juniper PyEZ Python 库和 PyEZ 操作表和视图来验证 Junos OS 设备的操作状态。

准备工作

在遵循本配方时,Juniper 设备上必须启用 NETCONF。

如何操作...

  1. 安装junos-eznc Python 包,如下所示:
$ pip3 install junos-eznc
  1. 使用Ansible-galaxy安装Juniper.junos Ansible 角色,如下所示:
$ Ansible-galaxy install Juniper.junos
  1. 创建一个名为pb_jnpr_pyez_table.yml的新 playbook,并填充以下任务以使用 PyEZ 表提取 BGP 对等信息:
$ cat pb_jnpr_pyez_table.yml

---
- name: Validate BGP State using PyEZ Tables
 hosts: junos
 roles:
 - Juniper.junos
 tasks:
 - name: Retrieve BGP Neighbor Information Using PyEZ Table
 Juniper_junos_table:
 file: "bgp.yml"
 register: jnpr_pyez_bgp
  1. 使用以下任务更新 playbook 以验证所有节点上的所有 BGP 对等体是否正常运行:
 - name: Validate all BGP Peers are operational
 assert:
 that:
 - item.peer in jnpr_pyez_bgp.resource | map(attribute='peer_id') | list
 fail_msg: " BGP Peer {{ item.peer }} is Not Operational"
 loop: "{{ bgp_peers }}"

工作原理...

除了我们在所有先前的配方中概述的预安装在 Ansible 中的内置 Juniper 模块之外,Juniper 还维护并不是 Ansible 发布的一部分的其他 Ansible 模块。这些模块打包在 Ansible Galaxy 中维护的 Ansible 角色中,所有这些模块都基于 Juniper PyEZ Python 库,该库也由 Juniper 开发和维护。

Juniper PyEZ Python 库提供了一个简单而强大的 API,以便与 Juniper 设备进行交互,并简化了使用 Python 管理 Juniper 设备的方法。由 Juniper 维护的 Ansible 模块都依赖于 PyEZ Python 库,因此我们需要执行的第一个任务是确保 PyEZ(junos-eznc)已安装在我们的 Ansible 控制机上。

由 Juniper 维护和开发的 Ansible 模块打包为 Ansible 角色,并且它们提供了多个具有额外功能的模块,与作为 Ansible 发布的一部分的内置 Juniper 模块相比。我们使用 Ansible Galaxy 安装此角色,以开始利用这些额外的模块。以下片段概述了此角色的额外模块:

$ tree ~/.Ansible/roles/Juniper.junos/library/

/home/Ansible/.Ansible/roles/Juniper.junos/library/
 ├── Juniper_junos_command.py
 ├── Juniper_junos_config.py
 ├── Juniper_junos_facts.py
 ├── Juniper_junos_jsnapy.py
 ├── Juniper_junos_ping.py
 ├── Juniper_junos_pmtud.py
 ├── Juniper_junos_rpc.py
 ├── Juniper_junos_software.py
 ├── Juniper_junos_srx_cluster.py
 ├── Juniper_junos_system.py
 └── Juniper_junos_table.py

在这个配方中,我们概述了如何使用Juniper_junos_table Ansible 模块,该模块使用 PyEZ 表和视图来执行 Juniper 设备上的操作命令,并从 Juniper 设备中提取特定信息。它还将这些信息解析为一致的数据结构,我们可以在自动化脚本中利用。在我们的 playbook 中,我们的第一个任务是使用Juniper_junos_table模块,使用bgp.yml表定义(作为junos-eznc安装的一部分)来获取设备上的 BGP 对等体,并以一致的数据结构返回相关信息。以下片段概述了Juniper_junos_table返回的 BGP 数据,用于mxpe01上的 BGP 信息:

ok: [mxpe01] => {
 "jnpr_pyez_bgp": {
 "changed": false,
 "failed": false,
 "msg": "Successfully retrieved 1 items from bgpTable.",
 "resource": [
 {
 "local_address": "10.100.1.1+179",
 "local_as": "65400",
 "local_id": "10.100.1.1",
 "peer_as": "65400",
 "peer_id": "10.100.1.254",
 "route_received": [
 "0",
 "2",
 "1",
 "1"
 ]
 }
 ],
 }
}

我们 playbook 中的最后一个任务是使用assert模块来验证我们所有的 BGP 对等体(在host_vars目录下定义)是否存在于 BGP 表中返回的数据结构中,这表明所有的 BGP 对等体都是正常运行的。

另请参阅...

有关由 Juniper 维护的 Juniper Ansible 模块的更多信息,请参阅以下网址:www.juniper.net/documentation/en_US/junos-ansible/topics/reference/general/junos-ansible-modules-overview.html.

有关 PyEZ 表和视图的更多信息,请参考以下网址:www.Juniper.net/documentation/en_US/junos-pyez/topics/concept/junos-pyez-tables-and-views-overview.html

第四章:使用 Arista 和 Ansible 构建数据中心网络

在本章中,我们将概述如何在典型的数据中心环境中自动化 Arista 交换机,采用叶脊架构。我们将探讨如何使用 Ansible 与 Arista 设备进行交互,以及如何使用各种 Ansible 模块在 Arista 交换机上部署虚拟局域网VLANs)和虚拟可扩展局域网VXLANs),并在 Arista 交换机上使用边界网关协议/以太网虚拟专用网络BGP/EVPN)设置。我们将以以下示例网络图为基础,说明基本叶脊数据中心网络DCN)的示例网络图:

以下表格概述了我们示例拓扑中的设备及其各自的管理互联网协议IP):

设备 角色 供应商 管理(MGMT)端口 MGMT IP
Spine01 脊柱交换机 Arista vEOS 4.20 Management1 172.20.1.35
Spine02 脊柱交换机 Arista vEOS 4.20 Management1 172.20.1.36
Leaf01 叶子交换机 Arista vEOS 4.20 Management1 172.20.1.41
Leaf02 叶子交换机 Arista vEOS 4.20 Management1 172.20.1.42
Leaf03 叶子交换机 Arista vEOS 4.20 Management1 172.20.1.43
Leaf04 叶子交换机 Arista vEOS 4.20 Management1 172.20.1.44

本章涵盖的主要配方如下:

  • 构建 Ansible 网络清单

  • 连接到并使用 Ansible 对 Arista 设备进行身份验证

  • 在 Arista 设备上启用可扩展操作系统EOSAPIeAPI

  • 在 Arista 设备上配置通用系统选项

  • 在 Arista 设备上配置接口

  • 在 Arista 设备上配置底层 BGP

  • 在 Arista 设备上配置覆盖 BGP/EVPN

  • 在 Arista 设备上部署配置

  • 在 Arista 设备上配置 VLAN

  • 在 Arista 设备上配置 VXLAN 隧道

  • 收集 Arista 设备信息

  • 从 Arista 设备检索操作数据

技术要求

本章中所有配方的代码可以在以下 GitHub 存储库中找到:

github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch4_arista.

本章基于以下软件版本:

  • 运行 CentOS 7 的 Ansible 机器

  • Ansible 2.9

  • Arista 虚拟化 EOSvEOS)运行 EOS 4.20.1F

观看以下视频以查看代码的实际操作:

bit.ly/3coydTp

构建 Ansible 网络清单

在本配方中,我们将概述如何构建和组织 Ansible 清单,以描述我们示例的叶脊直流DC)网络。Ansible 清单是 Ansible 的重要组成部分,它描述并分组应由 Ansible 管理的设备。

准备工作

我们需要创建一个新的文件夹,用于存放本章中将创建的所有文件。新文件夹的名称应为ch4_arista

如何做...

  1. 在新文件夹(ch4_arista)中,我们创建一个带有以下内容的hosts文件:
$ cat hosts

[leaf]
 leaf01 ansible_host=172.20.1.41
 leaf02 ansible_host=172.20.1.42
 leaf03 ansible_host=172.20.1.43
 leaf04 ansible_host=172.20.1.44

[spine]
 spine01 ansible_host=172.20.1.35
 spine02 ansible_host=172.20.1.36

[arista:children]
 leaf
 spine
  1. 创建一个ansible.cfg文件,如下面的代码块所示:
$ cat ansible.cfg

[defaults]
 inventory=hosts
 retry_files_enabled=False
 gathering=explicit
 host_key_checking=False

它是如何工作的...

定义 Ansible 清单是强制性的,以便描述和分类应由 Ansible 管理的网络中的设备。在 Ansible 清单中,我们还通过ansible_host参数指定 Ansible 将与这些受管设备通信的 IP 地址。

我们使用hosts文件构建了 Ansible 清单,并定义了多个组,以便对我们拓扑中的不同设备进行分组。这些组如下:

  • 我们创建了leaf组,它引用了我们拓扑中的所有leaf交换机。

  • 我们创建了spine组,它引用了我们拓扑中的所有spine交换机。

  • 我们创建了arista组,它引用了leafspine两个组。

最后,我们创建了ansible.cfg文件,并配置它指向我们的hosts文件,用作 Ansible 清单文件。此外,我们禁用了setup模块(通过将gathering设置为explicit),这在针对网络节点运行 Ansible 时是不需要的。

从 Ansible 连接和认证 Arista 设备

在这个示例中,我们将概述如何通过安全外壳SSH)从 Ansible 连接到 Arista 设备,以便从 Ansible 开始管理这些设备。我们将使用用户名和密码来对我们拓扑中的 Arista 设备进行认证。

准备工作

为了按照这个示例进行操作,应该按照之前的示例构建一个 Ansible 清单文件。Ansible 控制机与网络中所有设备之间的 IP 可达性也必须得到实现。

操作步骤...

  1. ch4_arista文件夹中创建一个group_vars文件夹。

  2. group_vars文件夹中,创建一个名为arista.yml的文件,包含以下内容:

ansible_network_os: eos
ansible_connection: network_cli
ansible_user: ansible
ansible_ssh_pass: ansible123
  1. 在 Arista 交换机上,我们配置了用户名和密码,并启用了 SSH,如下所示:
!
username Ansible privilege 15 role network-admin secret sha512
$6$mfU4Ei0AORd6rage$5YObhOI1g0wNBK5onaKDpYJhLZ9138maJKgcOznzFdpM25Tf3rb0PWSojUSM
RQY0Y7.cexCFj5aFLY17tuNU1
!

 !
management ssh
 idle-timeout 300
 authentication mode password
 login timeout 300
!
  1. 在 Arista 交换机上,使用正确的 IP 地址配置管理接口,并将它们放置在所需的管理虚拟路由和转发VRF)中,如下所示:
vrf definition MGMT
!
 ip routing vrf MGMT
 !
interface Management1
 vrf forwarding MGMT
 ip address *$Ansible_host$
*   no lldp transmit
 no lldp receive
!

工作原理...

我们在group_vars目录下的arista.yml文件中指定了我们将在所有 Arista 交换机上配置的用户名和密码。这将应用这些参数到我们清单中的所有 Arista 交换机。在 Arista 交换机上,我们设置了用户名和密码并启用了 SSH,并在管理接口上设置了正确的 IP 地址(在我们的清单中使用的ansible_host参数中使用的 IP 地址)。我们配置了管理 VRF,并将管理接口与此 VRF 关联起来。

我们在 Ansible 变量中以明文指定了 SSH 密码。这只适用于实验室设置;然而,在生产环境中,我们应该使用 Ansible vault 来保护任何敏感信息,就像在前面的章节中概述的那样。

在这个阶段,我们使用network_cli连接方法来使用 SSH 连接到 Arista 交换机。我们可以使用以下命令验证 Ansible 控制器是否能够到达并正确登录到设备:

$ ansible arista -m ping

 leaf03 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}
leaf04 | SUCCESS => {
 "changed": false,
 "ping": "pong"
}
 <-- Output Omitted for brevity -->

在 Arista 设备上启用 eAPI

在这个示例中,我们将概述如何在 Arista 设备上启用 eAPI。eAPI 是 Arista 设备上的表述状态传输RESTful)API,它简化了这些设备的管理,并提供了一个一致且强大的 API 来管理它们。这个任务非常关键,因为我们将在以后的示例中使用 eAPI 来管理 Arista 设备。

准备工作

作为这个示例的先决条件,必须存在一个 Ansible 清单文件。SSH 认证也应该已经部署并且正常工作,就像之前的示例一样。

操作步骤...

  1. group_vars文件夹中创建一个名为all.yml的文件,其中包含以下管理 VRF 数据:
$ cat  group_vars/all.yml

global:
 mgmt_vrf: MGMT
  1. 创建一个名为pb_eos_enable_eapi.yml的新 playbook,如下所示:
 $ cat pb_eos_eanble_eapi.yml
 ---
- name: "Enable eAPI on Arista Switches"
 hosts: arista
 vars:
 ansible_connection: network_cli
 tasks:
 - name: "Enable eAPI"
 eos_eapi:
 https_port: 443
 https: yes
 state: started
  1. 使用以下任务更新pb_eos_enable_eapi.yml playbook,以在管理 VRF 下启用 eAPI:
 - name: "Enable eAPI under VRF"
 eos_eapi:
 state: started
 vrf: "{{global.mgmt_vrf}}"
  1. group_vars文件夹中更新arista.yml文件,设置连接设置以使用 eAPI 作为连接插件:
$ cat group_vars/arista.yml

ansible_network_os: eos
ansible_connection: httpapi
ansible_httpapi_use_ssl: yes
ansible_httpapi_validate_certs: no

工作原理...

为了开始通过 eAPI 与 Arista 设备进行交互,我们首先需要启用它;因此,我们需要首先通过 SSH 登录到设备并启用 eAPI。这就是为什么在这个示例中,我们使用network_cli Ansible 连接来通过传统的 SSH 连接到 Arista 设备。由于我们将在所有未来的示例中使用 eAPI 与 Arista 设备进行交互,我们只在 playbook 级别的vars参数下启用了network_cli,以覆盖ansible_connection设置的任何组或主机级别的设置。

我们创建了一个名为pb_eos_enable_eapi.yml的新 playbook,在第一个任务中,我们使用了eos_eapi模块来在远程 Arista 设备上启用 eAPI 协议。我们指定将使用超文本传输安全协议HTTPS)和标准的 HTTPS 端口443。在第二个任务中,我们使用了eos_eapi模块,以在特定的 VRF 下仅启用 eAPI,这是我们用来管理设备的管理 VRF。

最后,为了开始使用 eAPI 管理 Arista 设备,我们修改了 Ansible 连接设置,这些设置我们在group_vars/arista.yml文件中定义,并包括以下设置:

  • ansible_connection设置为httpapi

  • ansible_httpapi_use_ssl设置为yes,以强制使用 HTTPS 而不是 HTTP。

  • ansible_httpapi_validate_certs设置为no,以禁用证书验证(因为我们使用的是 Arista 设备上的默认证书,该证书未经受信任的证书颁发机构CA)签名)。

一旦我们运行了 playbook,我们将看到所有 Arista 设备都配置了 eAPI,如下面的代码块所示:

!
management api http-commands
 no shutdown
 !
 vrf MGMT
 no shutdown
!

我们可以通过以下命令验证我们是否使用了正确的连接设置,并且 Ansible 能够使用 eAPI 与 Arista 设备进行通信:

$ ansible all -m ping -l leaf01 -vvvv 

<172.20.1.41> attempting to start connection
<172.20.1.41> using connection plugin httpapi
<172.20.1.41> loaded API plugin for network_os eos
**<172.20.1.41> ESTABLISH HTTP(S) CONNECTFOR USER: ansible TO** https://172.20.1.41:443

另请参阅...

有关eos_eapi模块以及该模块支持的不同参数的更多信息,请参考以下网址:docs.ansible.com/ansible/latest/modules/eos_eapi_module.html

在 Arista 设备上配置通用系统选项

在这个示例中,我们将概述如何配置一些基本的系统选项,比如主机名和域名系统DNS)服务器,并在 Arista 设备上配置用户。我们将了解如何使用各种 Ansible 模块设置所有这些系统级参数,并概述管理这些参数的不同方法。

准备工作

要按照这个示例进行操作,假定已经设置了 Ansible 清单,并且根据之前的示例在所有 Arista 设备上启用了 eAPI。

如何做...

  1. 更新group_vars/all.yml文件,使用通用系统参数,如下面的代码块所示:
$ cat  group_vars/all.yml

 <-- Output Omitted for brevity -->

 global:
 dns:
 - 172.20.1.1
 - 172.20.1.15
 site: DC1
 users:
 -   password: ansible123
 privilege: 15
 role: network-admin
 username: ansible
  1. 创建一个新的 playbook,pb_arista_basic_config.yml,并添加以下任务来设置 DNS 和主机名:
$ cat pb_arista_basic_config.yml --- - name: "Configure Basic Configuration on Arista Fabric"
 hosts: arista tasks: - name: "Conifgure Basic System config" eos_system: hostname: " {{global.site|lower}}-{{inventory_hostname}}" name_servers: "{{ global.dns }}" state: present
  1. 更新pb_arista_basic_config.yml playbook,添加以下任务以在 Arista 设备上创建用户:
 - name: "Configure Users"
 eos_user:
 name: "{{ item.username }}"
 role: "{{ item.role | default('network-admin') }}"
 privilege: "{{ item.privilege | default(15)}}"
 configured_password: "{{ item.password }}"
 state: present
 loop: "{{ global.users }}"

工作原理...

Ansible 提供了不同的声明性模块,以管理 Arista 交换机上的不同资源。在这个示例中,我们概述了如何使用eos_systemeos_user Ansible 模块来配置 Arista 设备上的基本系统属性。我们首先在group_vars/all.yml文件中定义了要使用的数据,并包括我们想要配置的 DNS 和用户。我们创建了pb_arista_basic_config.yml playbook,其中包括了设置 Arista 交换机上基本设置所需的所有任务。

playbook 中的第一个任务使用了eos_system Ansible 模块,在所有 Arista 设备上设置了 DNS 和主机名。第二个任务使用了eos_user Ansible 模块,在 Arista 交换机上设置了系统用户。在最后一个任务中,我们循环遍历了在group_vars/all.yml文件中定义的users数据结构,以在这个list数据结构中为每个用户进行配置。

一旦我们运行了 playbook,我们可以看到 Arista 交换机的配置已更新,如下面的代码块所示:

!
hostname dc1-leaf01
ip name-server vrf default 172.20.1.1
ip name-server vrf default 172.20.1.15
!

还有更多...

在本节中,我们概述了声明性的 Ansible 模块,它们提供了一种简单的方法来配置 Arista 设备的基本系统级参数;但是,它们可能无法涵盖我们需要在 Arista 交换机上设置的所有参数。为了更多地控制和灵活地配置系统级参数,我们可以使用 Jinja2 模板以及template Ansible 模块来生成我们部署所需的特定系统级配置。在本节中,我们将概述这种方法以实现这一目标。这将是我们在后续的配方中使用的方法,用于生成其他配置部分的配置,这些部分没有内置的 Ansible 模块可以满足我们的所有要求。

我们将重用这种方法为 Arista 设备生成不同部分的配置,例如系统、接口和 BGP。我们将创建一个 Ansible 角色,以包含生成我们将推送到设备的最终配置所需的所有 Jinja2 模板和任务。以下过程概述了创建角色所需的步骤以及生成配置所需的 playbook:

我们将使用与第三章中使用的相同的角色结构和任务,使用 Ansible 自动化服务提供商中的 Juniper 设备,来生成 Juniper 设备的配置。唯一的区别在于我们将使用 Jinja2 模板来生成 Arista 设备的特定配置。

  1. 创建一个新的roles目录,并添加一个名为dc_fabirc_config的新角色,具有以下目录结构:
$ tree roles/
roles/
└── dc_fabric_config
 ├── tasks
 └── templates
  1. tasks文件夹中,创建一个build_config_dir.yml文件,以创建所需的文件夹来存储将生成的配置,如下所示:
$ cat roles/dc_fabric_config/tasks/build_config_dir.yml

---
- name: Create Config Directory
 file: path={{config_dir}}   state=directory
 run_once: yes
- name: Create Temp Directory per Node
 file: path={{tmp_dir}}/{{inventory_hostname}}  state=directory
- name: SET FACT >> Build Directory
 set_fact:
 build_dir: "{{tmp_dir}}/{{inventory_hostname}}"
  1. templates文件夹中,创建一个名为eos的新文件夹,在该文件夹中创建一个名为mgmt.j2的新的 Jinja2 模板,如下面的代码块所示:
 $ cat roles/dc_fabric_config/templates/eos/mgmt.j2

!
hostname {{global.site|lower}}-{{inventory_hostname}}
!
!
spanning-tree mode none
!
aaa authorization exec default local
!
{% for user in global.users%}
username {{user.name}} privilege {{user.privilege}} role
{{user.role|default('network-admin')}} secret {{user.password}}
{% endfor%}
!
{% for dns_server in global.dns%}
ip name-server vrf default {{ dns_server }}
{% endfor %}
!
  1. tasks文件夹中,创建一个名为build_device_config.yml的新的 YAML 文件,以创建系统配置,如下面的代码块所示:
$ cat roles/dc_fabric_config/tasks/build_device_config.yml

---
- name: "System Configuration"
 template:
 src: "{{ansible_network_os}}/mgmt.j2"
 dest: "{{build_dir}}/00_mgmt.cfg"
 tags: mgmt
  1. tasks文件夹中创建一个main.yml文件,其中包含以下任务:
$ cat roles/build_router_config/tasks/main.yml

---
- name: Build Required Directories
 import_tasks: build_config_dir.yml
- name: Build Device Configuration
 import_tasks: build_device_config.yml

 - name: "Remove Old Assembled Config"
 file:
 path: "{{config_dir}}/{{ inventory_hostname }}.cfg"
 state: absent
- name: Build Final Device Configuration
 assemble:
 src: "{{ build_dir }}"
 dest: "{{config_dir}}/{{ inventory_hostname }}.cfg"
- name: Remove Build Directory
 file: path={{ tmp_dir }}  state=absent
 run_once: yes
  1. 创建一个名为pb_arista_dc_fabric.yml的新 playbook,以生成清单中所有arista设备的配置:
$ cat pb_arista_dc_fabric.yml

---
- name: "Build Arista DC Fabric"
 hosts: arista
 tasks:
 - name: Generate DC Fabric Configuration
 import_role:
 name: dc_fabric_config
 delegate_to: localhost

使用这种方法,我们创建了一个名为dc_fabric_config的角色,并创建了一个名为mgmt.j2的新的 Jinja2 模板,其中包含arista系统级配置的模板。我们使用template Ansible 模块来使用在group_vars/all.yml文件中定义的 Ansible 变量渲染 Jinja2 模板。为了保存每个设备的配置,我们创建了configs文件夹目录,用于存储每个设备的最终配置。

由于我们正在利用 Jinja2 方法来为每个部分(MGMT、接口、BGP 等)生成配置,我们将每个部分分成单独的 Jinja2 模板,并将每个部分生成到单独的文件中。我们使用assemble模块来将所有这些不同的部分组合成单个配置文件,我们将其存储在configs目录中,这是每个设备的最终和组装的配置文件。我们将每个设备的临时组装部分存储在临时文件夹中,并在 playbook 运行结束时删除此临时文件夹。

在此 playbook 中,我们在import_role任务中使用delegate_to localhost。由于在此角色中的所有任务中我们不需要连接到远程设备,因此所有这些任务应在 Ansible 控制机上运行,以便将文件存储在 Ansible 机器上。因此,我们使用delegate_to localhost 来在 Ansible 控制机上运行所有任务。

一旦我们运行pb_junos_net_build.yml playbook,我们可以看到在configs目录中创建了以下配置文件,目前只有配置的管理部分:

$ tree configs/
configs/
├── leaf01.cfg
├── leaf02.cfg
├── leaf03.cfg
├── leaf04.cfg
├── spine01.cfg
└── spine02.cfg

我们可以查看为其中一个设备(例如leaf01)生成的配置,如下面的代码块所示:

!
hostname dc1-leaf01
!
snmp-server enable traps
!
spanning-tree mode none
!
aaa authorization exec default local
!
username ansible privilege 15 role network-admin secret ansible123
!
ip name-server vrf default 172.20.1.1
ip name-server vrf default 172.20.1.15
!

在这个阶段,我们已经为清单中的所有arista交换机生成了系统配置;然而,我们仍然没有将这个配置推送到设备上。在后续的配方中,我们将概述如何将配置推送到arista设备。

在 Arista 设备上配置接口

在这个配方中,我们将概述如何在 Arista 设备上配置不同的接口参数,比如接口描述和 IP 地址信息。我们将概述如何使用各种 Ansible 模块与 Arista 设备上的接口进行交互,以及如何在样本网络拓扑中的所有 Arista 设备上设置接口。

准备工作

我们假设网络清单已经就位,并且在 Arista 交换机上已经启用了 eAPI,就像之前的配方一样。

操作步骤...

  1. group_vars/all.yml文件中添加以下内容,描述样本 DC 网络中的接口:
p2p_ip:
 leaf01:
 - {port: Ethernet8, ip: 172.31.1.1 , peer: spine01, pport: Ethernet1, peer_ip: 172.31.1.0}
 - {port: Ethernet9, ip: 172.31.1.11 , peer: spine02, pport: Ethernet1, peer_ip: 172.31.1.10}
 leaf02:
 < -- Output Omitted for brevity -->
 leaf03:
 < -- Output Omitted for brevity -->
 leaf04:
 < -- Output Omitted for brevity -->
 spine01:
 < -- Output Omitted for brevity -->
 spine02:
 < -- Output Omitted for brevity -->

lo_ip:
 leaf01: 10.100.1.1/32
 leaf02: 10.100.1.2/32
 leaf03: 10.100.1.3/32
 leaf04: 10.100.1.4/32
 spine01: 10.100.1.254/32
 spine02: 10.100.1.253/32
  1. 更新pb_arista_basic_config.yml playbook,添加以下任务以启用接口并在所有布线接口上设置描述:
- name: "Configure the Physical Interfaces"
 eos_interface:
 name: "{{ item.port }}"
 enabled: true
 description: "{{global.site}} | Rpeer:{{item.peer}} | Rport:{{item.pport}}"
 with_items: "{{p2p_ip[inventory_hostname]}}"
  1. 更新pb_arista_basic_config.yml playbook,添加以下任务以在所有点对点P2P)布线链路上设置 IPv4 地址:
- name: "Configure IP Addresses"
 eos_l3_interface:
 name: "{{ item.port }}"
 ipv4: "{{ item.ip }}/{{ global.p2p_prefix }}"
 state: present
 with_items: "{{ p2p_ip[inventory_hostname] }}"

工作原理...

我们在group_vars/all.yml文件中的两个主要数据结构中定义了样本网络拓扑中所有接口的所有数据。我们使用p2p_ip字典来模拟样本网络中所有 P2P IP 地址,并使用lo_ip字典来指定节点的环回 IP 地址。

我们使用eos_interface Ansible 模块来启用接口并设置接口的基本参数,比如接口描述。我们遍历了每个设备的p2p_ip数据结构,并为网络清单中的所有设备上的每个接口设置了正确的参数。我们使用eos_l3_interface Ansible 模块在样本网络拓扑中的所有设备上设置了正确的 IPv4 地址。

更多信息...

如果我们需要更多地控制接口配置,并设置声明性 Ansible 模块中未涵盖的参数,我们可以使用 Jinja2 模板来实现这个目标。使用与前一节中系统配置相同的方法,我们可以为 Juniper 设备生成所需的接口配置。

使用前一节中创建的相同 Ansible 角色,我们可以扩展它以生成 Arista 设备的接口配置。我们使用以下步骤来完成这项任务:

  1. templates文件夹中创建一个名为intf.js的新 Jinja2 模板文件,内容如下:
$ cat roles/dc_fabric_config/templates/eos/intf.j2

{% set node_intfs = p2p_ip[inventory_hostname] %}
{% for p in node_intfs| sort(attribute='port') %}
!
interface {{p.port}}
 description "{{global.site}} | Rpeer: {{p.peer}} | Rport: {{p.pport}}"
 no switchport
 ip address {{p.ip}}/{{global.p2p_prefix}}
{% endfor %}
!
!
interface Loopback0
 ip address {{lo_ip[inventory_hostname]}}
!
  1. tasks目录中更新build_device_config.yml文件,添加新任务以生成接口配置:
$ cat roles/dc_fabric_config/tasks/build_device_config.yml

<-- Output Trimmed for brevity ------>

- name: "Interface Configuration"
 template:
 src: "{{ansible_network_os}}/intf.j2"
 dest: "{{build_dir}}/01_intf.cfg"
 tags: intf
  1. 一旦我们运行pb_arista_dc_fabric.yml playbook,我们将为我们的设备生成配置,例如为leaf01更新了interface部分:
$ cat configs/leaf01.cfg

< -- Output Omitted for brevity -->

!
interface Ethernet8
 description "DC1 | Rpeer: spine01 | Rport: Ethernet1"
 no switchport
 ip address 172.31.1.1/31
!
interface Ethernet9
 description "DC1 | Rpeer: spine02 | Rport: Ethernet1"
 no switchport
 ip address 172.31.1.11/31
!
!
interface Loopback0
 ip address 10.100.1.1/32
!

另请参阅...

有关eos_interface模块以及该模块支持的不同参数的更多信息,请参考以下网址:docs.ansible.com/ansible/latest/modules/eos_interface_module.html

有关eos_l3_interface模块以及此模块支持的不同参数的更多信息,请参阅以下网址:docs.ansible.com/ansible/latest/modules/eos_l3_interface_module.html

在 Arista 设备上配置底层 BGP

在这个教程中,我们将概述如何为我们样例叶/脊柱 DC 结构配置 eBGP 作为底层路由协议。我们将建立 eBGP 对等设置,使用叶交换机和脊柱交换机之间的 P2P IP 地址。BGP 自治系统号(ASN)分配如下表所示:

节点 BGP ASN
Spine01 65100
Spine02 65100
Leaf01 65001
Leaf02 65002
Leaf03 65003
Leaf04 65004

准备工作

在这个教程中,我们假设接口和 IP 地址信息已根据以前的教程进行了配置。

操作步骤...

  1. 创建一个host_vars目录,并为清单中的每个设备创建一个文件夹。在每个文件夹中,创建一个名为underlay_bgp.yml的新的 YAML 文件,其中包含 BGP 对等详细信息。以下是我们清单中leaf01设备的一个示例:
## Leaf01 BGP Data ###
bgp_asn: 65001
bgp_peers:
 - peer: spine01
 peer_ip: 172.31.1.0
 remote_as: 65100
 - peer: spine02
 peer_ip: 172.31.1.10
 remote_as: 65100
  1. templates/eos目录中创建一个名为underlay_bgp.j2的新的 Jinja2 文件,其中包含以下数据。这个模板是我们将用来控制 DC 结构中 BGP 广播的prefix-list
$ cat roles/dc_fabric_config/templates/eos/underlay_bgp.j2 {% set bgp_grp = 'LEAF' if 'spine' in inventory_hostname else 'SPINE' %}
!
route-map loopback permit 10
 match ip address prefix-list loopback
!
{% if 'spine' in inventory_hostname %}
!
ip prefix-list loopback
{% for node,ip in lo_ip.items() | sort %}
{% if 'leaf' in node or inventory_hostname in node %}
 seq {{loop.index + 10 }} permit {{ip}}
{% endif %}
{% endfor %}
!
{% else %}
!
ip prefix-list loopback
 seq 10 permit {{lo_ip[inventory_hostname]}}
!
{% endif %}
  1. templates/eos目录中更新underlay_bgp.j2 Jinja2 文件,其中包含以下代码块中显示的 BGP 模板:
$ cat roles/dc_fabric_config/templates/eos/underlay_bgp.j2

!
router bgp {{bgp_asn}}
 router-id {{lo_ip[inventory_hostname].split('/')[0]}}
 maximum-paths 2
 bgp bestpath tie-break router-id
 neighbor {{ bgp_grp }} peer-group
 neighbor {{ bgp_grp }} description "Peer Group for All {{bgp_grp}} Nodes"
 neighbor {{ bgp_grp }} graceful-restart-helper
 neighbor {{ bgp_grp }} send-community standard extended
 neighbor {{ bgp_grp }} maximum-routes 100000 warning-only
{% for p in bgp_peers %}
 neighbor {{ p.peer_ip}} peer-group {{ bgp_grp }}
 neighbor {{ p.peer_ip}} remote-as {{p.remote_as}}
{% endfor %}
 redistribute connected route-map loopback
 !
 address-family ipv4
 neighbor {{ bgp_grp }} activate
 neighbor {{ bgp_grp }} route-map loopback out
!
  1. tasks文件夹中的build_config.yml文件中,添加以下任务以呈现底层 BGP 配置:
$ cat roles/dc_fabric_config/tasks/build_device_config.yml

< -- Output Omitted for brevity -->

- name: "Underlay BGP Configuration"
 template:
 src: "{{ansible_network_os}}/underlay_bgp.j2"
 dest: "{{config_dir}}/{{ inventory_hostname }}/03_bgp.cfg"

工作原理...

根据我们的设计,我们将在叶和脊柱节点之间运行 eBGP,并且我们拓扑中的每个叶交换机都将有自己的 BGP ASN。描述这种设置的最佳方法是使用host_vars文件夹在每个主机基础上包含所有这些数据。我们为每个节点创建了一个文件夹,以在此文件夹下包含所有相关的主机数据。我们创建了一个 YAML 文件来保存每个设备的 BGP 信息,因此如果需要为另一个协议添加更多主机特定数据,我们可以轻松地添加一个新文件:

$ tree host_vars
 host_vars
 ├── leaf01
 │ └── underlay_bgp.yml
 ├── leaf02
 │ └── underlay_bgp.yml
 ├── leaf03
 │ └── underlay_bgp.yml
 ├── leaf04
 │ └── underlay_bgp.yml
 ├── spine01
 │ └── underlay_bgp.yml
 └── spine02
 └── underlay_bgp.yml

tasks/build_device_config.yml文件中,我们添加了一个新任务,该任务使用underlay_bgp.j2 Jinja2 模板来呈现 Jinja2 模板,并输出我们 Ansible 清单中每个设备的底层 BGP 配置部分。

对于每个设备,我们生成了一个prefix-list来匹配将广播到其 eBGP 对等体的所有前缀,具体标准如下:

  • 对于脊柱交换机,我们会广播所有叶回环 IP 地址以及脊柱回环接口。

  • 对于叶交换机,我们只会广播回环 IP 地址。

在运行新任务的 playbook 后,以下片段概述了leaf01设备生成的 BGP 配置:

$ cat configs/leaf01/04_bgp.cfg

!
route-map loopback permit 10
 match ip address prefix-list loopback
!
ip prefix-list loopback
 seq 10 permit 10.100.1.1/32
!
router bgp 65001
 router-id 10.100.1.1
 maximum-paths 2
 bgp bestpath tie-break router-id
 neighbor SPINE peer-group
 neighbor SPINE description "Peer Group for All SPINE Nodes"
 neighbor SPINE graceful-restart-helper
 neighbor SPINE send-community standard extended
 neighbor SPINE maximum-routes 100000 warning-only
 neighbor 172.31.1.0 peer-group SPINE
 neighbor 172.31.1.0 remote-as 65100
 neighbor 172.31.1.10 peer-group SPINE
 neighbor 172.31.1.10 remote-as 65100
 redistribute connected route-map loopback
 !
 address-family ipv4
 neighbor SPINE activate
 neighbor SPINE route-map loopback out
! 

在 Arista 设备上配置覆盖 BGP EVPN

在这个教程中,我们将概述如何使用 Ansible 在我们的样例拓扑中为 VXLAN 隧道的叶脊柱 DC 结构配置覆盖 BGP EVPN 作为控制平面。

准备工作

这个教程假设 P2P IP 地址和回环接口已根据以前的教程进行了配置。此外,底层 BGP 配置应该已经根据以前的教程生成。

操作步骤...

  1. templates/eos目录中创建一个名为overlay_bgp.j2的新的 Jinja2 文件,其中包含以下数据:
$ cat roles/dc_fabric_config/templates/eos/overlay_bgp.j2

{% set bgp_evpn_grp = 'LEAF_EVPN' if 'spine' in inventory_hostname else 'SPINE_EVPN' %}

service routing protocols model multi-agent
!
router bgp {{bgp_asn}}

 neighbor {{ bgp_evpn_grp }} peer-group
 neighbor {{ bgp_evpn_grp }} description "Peer Group for All {{bgp_evpn_grp}} EVPN Nodes"
 neighbor {{ bgp_evpn_grp }} graceful-restart-helper
 neighbor {{ bgp_evpn_grp }} send-community extended
 neighbor {{ bgp_evpn_grp }} maximum-routes 100000 warning-only
 neighbor {{ bgp_evpn_grp }} ebgp-multihop 2
 neighbor {{ bgp_evpn_grp }} update-source Loopback0
{% for p in bgp_peers %}
 neighbor {{ lo_ip[p.peer].split('/')[0]}} peer-group {{ bgp_evpn_grp }}
 neighbor {{ lo_ip[p.peer].split('/')[0]}} remote-as {{p.remote_as}}
{% endfor %}
 !
 address-family evpn
 neighbor {{ bgp_evpn_grp }} activate
 !
 address-family ipv4
 no neighbor {{ bgp_evpn_grp }} activate
!
  1. tasks文件夹中的build_config.yml文件中,添加以下突出显示的任务:
$ cat tasks/build_config.yml

< -- Output Omitted for brevity -->

- name: "Overlay BGP EVPN Configuration"
 template:
 src: "{{ansible_network_os}}/overlay_bgp.j2"
 dest: "{{config_dir}}/{{ inventory_hostname }}/04_evpn.cfg"

工作原理...

在这个教程中,我们使用了类似的方法来配置底层 eBGP 的方法。我们构建了一个 Jinja2 模板,用于为清单中的 Arista 设备生成所需的 BGP EVPN 配置。以下代码块显示了leaf01交换机的 BGP EVPN 配置示例:

service routing protocols model multi-agent
!
router bgp 65001

 neighbor SPINE_EVPN peer-group
 neighbor SPINE_EVPN description "Peer Group for All SPINE_EVPN EVPN Nodes"
 neighbor SPINE_EVPN graceful-restart-helper
 neighbor SPINE_EVPN send-community extended
 neighbor SPINE_EVPN maximum-routes 100000 warning-only
 neighbor SPINE_EVPN ebgp-multihop 2
 neighbor SPINE_EVPN update-source Loopback0
 neighbor 10.100.1.254 peer-group SPINE_EVPN
 neighbor 10.100.1.254 remote-as 65100
 neighbor 10.100.1.253 peer-group SPINE_EVPN
 neighbor 10.100.1.253 remote-as 65100
 !
 address-family evpn
 neighbor SPINE_EVPN activate
 !
 address-family ipv4
 no neighbor SPINE_EVPN activate
! 

在 Arista 设备上部署配置

在这个教程中,我们将概述如何将配置推送到 Arista 设备。我们将使用在之前的教程中生成的配置来为我们的拓扑中的设备进行配置。我们将学习如何使用适当的 Ansible 模块与 Arista 配置进行交互,以便根据预期的网络设计正确配置设备。

准备工作

这个教程要求在 Arista 设备上启用 eAPI。

如何做...

pb_arista_dc_fabric.yml文件中,添加以下任务来将配置部署到 Arista 交换机:

- name: "Deploy Configuration"
 eos_config:
 src: "{{config_dir}}/{{ inventory_hostname }}.cfg"
 replace: config
 save_when: changed
 tags: deploy

工作原理...

在之前的教程中,我们生成了 Arista 交换机配置的不同部分,比如接口和底层/覆盖 BGP。我们使用assemble Ansible 模块将配置的不同部分组合成一个包含所有设备配置的单个配置文件。在这个教程中,我们使用了eos_config模块来将配置文件推送到 Arista 交换机。

eos_config模块中,我们使用了src参数来指定我们想要加载到设备中的配置文件的位置。我们使用了replace指令和config选项来替换目标设备上的所有配置,使用src选项中指定的新配置。因此,设备上的配置完全由 Ansible 管理和控制。这也意味着,如果有任何在 Ansible playbook 之外实施的配置,一旦我们运行 playbook 并将新配置推送到设备上,该配置将被删除。

最后,我们使用save_when参数并将其设置为changed,以便将运行配置复制到startup-config并保存配置。只有在任务改变了设备上的配置时,我们才执行此操作。

另请参阅...

有关eos_config模块和该模块支持的不同参数的更多信息,请参考以下网址:docs.ansible.com/ansible/latest/modules/eos_config_module.html

在 Arista 设备上配置 VLAN

在这个教程中,我们将概述如何在 Arista 交换机上配置 VLAN。我们将在数据中心网络中构建的 VLAN 如下表所示:

节点 接口 接口类型 VLAN
Leaf01 以太网 1 接入 10
Leaf02 以太网 1 接入 20
Leaf03 以太网 1 接入 10
Leaf03 以太网 2 接入 20
Leaf04 以太网 1 接入 10
Leaf04 以太网 2 接入 20

准备工作

这个教程假设底层和覆盖 BGP 配置已经按照之前的教程生成。

如何做...

  1. 创建一个名为vlan_design.yml的新的 YAML 文件,用于保存我们数据中心网络设备的 VLAN 设计,如下所示的代码块:
$ cat vlan_design.yml
vlan_data:
 leaf01:
 - id: 10
 description: DB
 ports:
 - Ethernet1
 leaf02:
 - id: 20
 description: web
 ports:
 - Ethernet1
 < -- Output Omitted for brevity -->
  1. roles文件夹中创建一个新的角色provision_vlans,结构如下:
$ tree roles/provision_vlans/
 roles/provision_vlans/
 ├── tasks
 │ └── main.yml
 ├── templates
 └── vars
 └── main.yml
  1. tasks/main.yml文件中,包括以下任务来配置我们数据中心网络中的 VLAN:
$ cat roles/provision_vlans/tasks/main.yml

---
- name: Deploy VLANs on DC Fabric
 eos_vlan:
 name: "VLAN_{{vlan.id}}_{{ vlan.description }}"
 vlan_id: "{{ vlan.id }}"
 state: present
 interfaces: "{{ vlan.ports }}"
 loop: "{{ vlan_data[inventory_hostname] }}"
 loop_control:
 loop_var: vlan
 tags: vlans
  1. 创建一个新的 playbook,pb_deploy_vlans.yml,使用该角色来为我们的数据中心网络中的 VLAN 进行配置,如下所示的代码块:

 $ cat pb_deploy_vlans.yml

---
- name: Provision VLANs on DC Fabric
 hosts: arista
 vars_files: vlan_design.yml
 tasks:
 - name: Deploy Vlans on DC Fabric
 import_role:
 name: provision_vlans
 when: inventory_hostname in vlan_data.keys()

工作原理...

为了在我们的数据中心网络中为 VLAN 进行配置,我们在一个名为vlan_design.yml的 YAML 文件中对我们的 VLAN 成员资格进行了建模和定义。该文件对我们网络中所有交换机上的所有 VLAN 进行了建模,存储在vlan_data字典中。该字典中的每个键都是设备,值是一个字典列表,每个字典对应一个单独的 VLAN 定义。

我们创建了一个特定的角色provision_vlans,用于在我们的网络中为 VLAN 进行配置,该角色中的初始任务使用了eos_vlan Ansible 模块来配置 VLAN。我们循环遍历每个节点特定的vlan_data并为这些 VLAN 进行配置。

我们创建了一个pb_deploy_vlans.yml playbook,使用这个角色来部署 VLAN。我们使用vars_files参数读取vlan_design.yml文件,并使用import_roles导入provision_vlans角色。我们使用when指令,只在我们 VLAN design文件中定义的设备上调用这个角色。

一旦我们运行我们的 playbook,我们可以看到 VLAN 已经部署到我们的 fabric 中,例如在leaf03中:

dc1-leaf03#sh vlan
 VLAN Name Status Ports
 ----- -------------------------------- --------- -------------------------
 1 default active Et3, Et4, Et5, Et6, Et7
 10 VLAN_10_DB active Et1
 20 VLAN_20_web active Et2

另请参阅...

有关eos_vlan模块以及此模块支持的不同参数的更多信息,请参阅以下网址:docs.ansible.com/ansible/latest/modules/eos_vlan_module.html

在 Arista 设备上配置 VXLAN 隧道

在这个配方中,我们将概述如何使用 BGP EVPN 配置 VXLAN 隧道,跨我们的叶脉冲 fabric。在类似我们示例拓扑的 IP fabric 中,我们需要有 VXLAN 隧道来传输 L2 VLANs。以下表格概述了我们将在整个 fabric 中使用的 VLAN 到虚拟网络标识符(VNI)映射:

VLAN VNI
10 1010
20 1020

准备工作

这个配方假设 BGP EVPN 已经部署在我们的 fabric 中,并且所有 VLAN 已经配置。

如何做...

  1. provision_vlans角色的vars/main.yml文件中更新以下变量。这将定义存储 VXLAN 配置的目录:
$ cat roles/provision_vlans/vars/main.yml
 ---
 config_dir: ./vxlan_configs
  1. 在我们的provision_vlans角色中,创建一个templates文件夹。然后,在其中创建一个eos文件夹。之后,创建一个 Jinja2 vxlan.j2文件,内容如下:
$ cat roles/provision_vlans/templates/eos/vxlan.j2

{% set vlans = vlan_data[inventory_hostname] %}
{% set all_vlans = vlans | map(attribute='id') | list %}
!
interface Vxlan1
 vxlan source-interface Loopback0
{% for vlan in all_vlans %}
 vxlan vlan {{ vlan }} vni 10{{vlan}}
{% endfor %}
!
router bgp {{bgp_asn}}
!
{% for vlan in all_vlans %}
 vlan {{ vlan }}
 rd {{lo_ip[inventory_hostname].split('/')[0]}}:10{{vlan}}
 route-target both 10{{vlan}}:10{{vlan}}
 redistribute learned
{% endfor %}
 !
  1. provision_vlans角色的tasks/main.yml文件中更新以下任务,以生成 VXLAN 配置:
- name: Create VXLAN Configs Folder
 file: path={{config_dir}} state=directory
 run_once: yes
 delegate_to: localhost
 tags: vxlan

- name: "Create VXLAN Configuration"
 template:
 src: "{{ansible_network_os}}/vxlan.j2"
 dest: "{{config_dir}}/{{ inventory_hostname }}.cfg"
 delegate_to: localhost
 tags: vxlan
  1. 使用以下任务更新tasks/main.yml文件,以在我们的 DC fabric 交换机上部署 VXLAN 配置:
- name: "Deploy Configuration"
 eos_config:
 src: "{{config_dir}}/{{ inventory_hostname }}.cfg"
 save_when: changed
 tags: vxlan

工作原理...

在上一个配方中,我们概述了如何在 DC fabric 中部署 VLAN。然而,在 IP fabric 中,我们需要有隧道来传输 L2 VLANs。在这个配方中,我们概述了如何使用 BGP EVPN 建立 VXLAN 隧道,以传输 L2 VLANs 并完成 DC fabric 中的 VLAN 配置任务。

由于 VXLAN 隧道与相应的 VLAN 紧密耦合,我们在provision_vlans角色中包括了 VXLAN 隧道的设置。我们使用 Jinja2 模板和template Ansible 模块来生成每个交换机上所需的 VXLAN 和 BGP 配置。我们创建了一个新的文件夹来存放我们将为每个交换机生成的 VXLAN 配置。我们利用template Ansible 模块,使用 Jinja2 模板渲染我们在vlan_design.yml文件中定义的 VLAN 数据,以生成每个交换机的 VXLAN 配置。

一旦我们运行更新后的 playbook,我们可以看到新的文件夹被创建,并且为所有交换机生成了配置:

$ tree vxlan_configs/
vxlan_configs/
├── leaf01.cfg
├── leaf02.cfg
├── leaf03.cfg
└── leaf04.cfg

以下代码块显示了为leaf01交换机生成的 VXLAN 配置的示例配置:

$ cat vxlan_configs/leaf01.cfg

interface Vxlan1
 vxlan source-interface Loopback0
 vxlan udp-port 4789
 vxlan vlan 10 vni 1010
!
router bgp 65001
!
 vlan 10
 rd 10.100.1.1:1010
 route-target both 1010:1010
 redistribute learned
 !

收集 Arista 设备事实

在这个配方中,我们将概述如何检索由 Ansible 收集的 Arista 设备的基本系统事实,运行 Arista EOS 软件。这些基本系统事实为我们提供了关于 Arista 设备的基本健康检查,我们可以用来验证其操作状态。

准备工作

在 Arista 设备上必须启用 eAPI,以便在这个配方中使用 Ansible 模块。

如何做...

  1. 创建一个新的 playbook,pb_arista_facts.yml,包含以下任务来收集事实:
$ cat pb_jnpr_facts.yml

---
- name: Collect and Validate Arista DC Fabric Facts
 hosts: arista
 tasks:
 - name: Collect Arista Device Facts
 eos_facts:
  1. 使用以下任务更新pb_arista_facts.yml playbook,以验证所有 fabric 接口的操作状态:
 - name: Validate all DC Fabric Interface are Operational
 assert:
 that:
 - ansible_net_interfaces[item.port].lineprotocol == 'up'
 fail_msg: "Interface {{item.port}} is not Operational "
 loop: "{{ p2p_ip[inventory_hostname] }}"
  1. 更新 playbook,添加以下任务来验证所有布线接口的正确 IP 地址分配:
- name: Validate all DC Fabric Interface are has Correct IP
 assert:
 that:
 - ansible_net_interfaces[item.port].ipv4.address == item.ip
 fail_msg: "Interface {{item.port}} has Wrong IP Address"
 loop: "{{ p2p_ip[inventory_hostname] }}"

工作原理...

Ansible 提供了一个收集 Arista 设备基本系统属性的事实收集模块,并以一致和结构化的数据结构返回这些事实。我们可以使用这个模块收集的事实来验证设备的基本属性和操作状态。

在这个示例中,我们使用eos_facts模块来收集所有 Arista 设备的设备信息。这个模块返回了 Ansible 为每个设备收集的基本信息,并存储在多个变量中。我们感兴趣的主要变量是ansible_net_interfaces变量,它保存了设备上所有接口的操作状态。以下片段概述了存储在这个变量中的数据的样本:

"ansible_net_interfaces": {
 "Ethernet8": {
 "bandwidth": 0,
 "description": "DC1 | Rpeer: spine01 | Rport: Ethernet1",
 "duplex": "duplexFull",
 "ipv4": {
 "address": "172.31.1.1",
 "masklen": 31
 },
 "lineprotocol": "up",
 "macaddress": "50:00:00:03:37:66",
 "mtu": 1500,
 "operstatus": "connected",
 "type": "routed"
 }
}

我们使用 Ansible 检索的数据,并存储在ansible_net_interfaces变量中,以验证所有布线接口是否操作,并且它们是否按照我们的设计分配了正确的 IP 地址。我们使用assert模块来执行此验证,并循环遍历每个设备的p2p_ip数据结构,以验证只有我们的布线接口的状态。

参见...

有关eos_facts模块和该模块支持的不同参数的更多信息,请参阅以下网址:docs.ansible.com/ansible/latest/modules/eos_facts_module.html

从 Arista 设备中检索操作数据

在这个示例中,我们将概述如何在 Arista 设备上执行操作命令,并使用输出来验证设备的状态。

准备工作

为了按照这个示例进行操作,Arista 设备上必须启用 eAPI。

操作步骤...

  1. 创建一个名为pb_get_vlans.yml的新 playbook,并填充它以在所有叶子交换机上执行show vlan命令,并将输出存储在一个变量中:
---
- name: " Play 1: Retrieve All VLANs from Arista Switches"
 hosts: leaf
 vars_files: vlan_design.yml
 tasks:
 - name: "Get All VLANs"
 eos_command:
 commands: show vlan | json
 register: show_vlan
  1. 更新pb_get_vlans.yml playbook,并填充以下任务来比较和验证设备上配置的正确 VLAN:
 - name: "Validate VLANs are Present"
 assert:
 that: (item.vlan | string) in show_vlan.stdout[0].vlans.keys()
 fail_msg: "VLAN:{{ item.vlan }} is NOT configured "
 success_msg: "VLAN:{{ item.vlan }} is configured "
 loop: "{{ access_interfaces[inventory_hostname] }}"
 delegate_to: localhost

工作原理...

我们使用eos_command Ansible 模块在 Arista 交换机上执行操作命令,并为了返回结构化输出,我们在命令中使用json关键字来返回操作命令的 JSON 输出(如果支持)。在这个例子中,我们发送了show vlan命令来获取设备上配置的 VLAN 列表,并将输出收集在show_vlan变量中。以下片段概述了我们从设备中获得的输出,这些输出存储在这个变量中:

ok: [leaf01] => {
 "show_vlan": {
 < -- Output Omitted for brevity -->
 "stdout": [
 {
 "vlans": {
 "1": {
 "dynamic": false,
 "interfaces": {
 < -- Output Omitted for brevity -->
 },
 "name": "default",
 "status": "active"
 },
 "10": {
 "dynamic": false,
 "interfaces": {
 "Ethernet1": {
 "privatePromoted": false
 },
 "Vxlan1": {
 "privatePromoted": false
 }
 },
 "name": "VLAN_10",
 "status": "active"
 }
 }
 }
 ] 

我们使用assert模块来验证我们设计中定义的 VLAN(在vlans_design.yml文件中)是否都配置并且对每个交换机都是操作状态。我们将在这个文件中定义的 VLAN 与我们使用eos_command模块从设备中检索的输出(存储在show_vlan变量中)进行比较,以确保每个 VLAN 在交换机上都是活动的。

我们在assert语句中使用string Jinja2 过滤器,因为我们的vlan_design.yml文件中定义的 VLAN 是整数。然而,存储在show_vlan变量中的 VLAN 是字符串。因此,为了使assert语句成功,我们需要确保类型是相似的。

参见...

有关eos_command模块和该模块支持的不同参数的更多信息,请参阅以下网址:docs.ansible.com/ansible/latest/modules/eos_command_module.html

第五章:使用 F5 LTM 和 Ansible 自动化应用交付

在本章中,我们将概述如何自动化运行作为负载均衡器LBs)或本地流量管理器LTM)设备的 F5 BIG-IP 平台。我们将探讨如何使用 Ansible 与 F5 LTM 节点交互,以及如何通过各种 Ansible 模块对这些设备进行加速应用部署。我们将以以下样本网络图为基础进行说明。该图显示了连接到直流DC)交换机的单个 F5 LTM 节点:

本章主要涵盖的配方如下:

  • 构建 Ansible 网络清单

  • 连接和认证到 BIG-IP 设备

  • 在 BIG-IP 设备上配置通用系统选项

  • 在 BIG-IP 设备上配置接口和干线

  • 在 BIG-IP 设备上配置虚拟局域网VLANs)和自身互联网协议self-IPs

  • 在 BIG-IP 设备上配置静态路由

  • 在 BIG-IP 设备上部署节点

  • 在 BIG-IP 设备上配置负载均衡池

  • 在 BIG-IP 设备上配置虚拟服务器

  • 从 BIG-IP 节点检索操作数据

技术要求

本章中使用的所有代码都可以在以下 GitHub 存储库中找到:github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch5_f5

本章基于以下软件版本:

  • 运行 CentOS 7 的 Ansible 机器

  • Ansible 2.9

  • 运行 BIG-IP 13.1.1,Build 0.0.4 final 的 F5 BIG-IP 设备

查看以下视频以查看代码的实际操作:

bit.ly/2RE5tOL

构建 Ansible 网络清单

在这个示例中,我们将概述如何构建和组织我们的 Ansible 清单,以描述我们的样本 F5 BIG-IP 节点。构建 Ansible 清单是告诉 Ansible 如何连接到受管设备的强制步骤。

做好准备

我们将创建一个新的文件夹,用于存放本章中创建的所有文件。新文件夹名为ch5_f5

如何操作...

  1. 在新文件夹ch5_f5中,我们创建一个hosts文件,其中包含以下内容:
$ cat hosts
[ltm]
ltm01 Ansible_host=172.20.1.34
  1. 创建一个Ansible.cfg文件,如下所示:
$ cat Ansible.cfg
[defaults]
inventory=hosts
retry_files_enabled=False
gathering=explicit
host_key_checking=False

工作原理...

由于我们的网络拓扑中只有一个 LTM 节点,这简化了我们的 Ansible 清单文件。在我们的hosts文件中,我们创建一个单一组(称为ltm),并在其中指定一个单一节点,名为ltm01。我们使用Ansible_host参数指定节点的管理 IP 地址。

BIG-IP 设备的管理端口必须配置此 IP 地址,并且 Ansible 控制机与 BIG-IP 节点之间的 IP 连接必须通过此管理端口建立。

最后,我们创建Ansible.cfg文件并配置它指向我们的hosts文件,以用作 Ansible 清单文件。我们禁用了 setup 模块,在针对网络节点运行 Ansible 时不需要它。

连接和认证到 BIG-IP 设备

在这个示例中,我们将概述如何通过 BIG-IP 设备暴露的表述状态转移REST)API 从 Ansible 连接到 BIG-IP 节点,以便从 Ansible 开始管理这些设备。我们将使用用户名和密码来对我们拓扑中的 BIG-IP 节点进行身份验证。

做好准备

为了按照这个示例进行操作,应该按照前面的示例构建一个 Ansible 清单文件。Ansible 控制机和网络中所有设备之间必须建立 IP 可达性。

如何操作...

  1. ch5_f5文件夹中,创建一个group_vars文件夹。

  2. 创建一个新的group_vars/all.yml文件,其中包含以下连接参数设置:

conn_parameters:
 user: admin
 password: admin
 server: "{{ Ansible_host }}"
 server_port: 443
 validate_certs: no
admin_passwd: NewP@sswd
users:
 - name: Ansible
 passwd: Ansible123
 role: all:admin
 state: present
  1. 创建一个名为pb_f5_onboard.yml的新的 playbook,其中包含以下任务来创建新的系统用户:
- name: Onboarding a New LTM
 hosts: ltm01
 connection: local
 tasks:
 - name: "P1T1: Create new Users"
 bigip_user:
 username_credential: "{{ item.name }}"
 password_credential: "{{ item.passwd }}"
 partition_access: "{{ item.role }}"
 state: "{{ item.state | default('present')}}"
 provider: "{{ conn_parameters }}"
 loop: "{{ users }}"
  1. 使用以下任务更新pb_f5_onboard.yml playbook 以更新管理员用户帐户:
 - name: "P1T1: Update admin Password"
 bigip_user:
 username_credential: admin
 password_credential: "{{ admin_passwd }}"
 state: present
 provider: "{{ conn_parameters }}"

它是如何工作的...

Ansible 使用 F5 LTM 节点上的 REST API 来管理 BIG-IP 节点。Ansible 建立一个 HTTPS 连接到 BIG-IP 节点,并将其用作调用 BIG-IP 节点上的 REST API 的传输机制。为了与 BIG-IP 系统建立 HTTPS 连接,我们需要提供一些参数,以便 Ansible 启动并与 BIG-IP 节点建立连接。这些参数包括以下内容:

  • 用于认证 BIG-IP REST API 的用户名/密码

  • IP 地址和端口,通过这些我们可以访问 BIG-IP 节点上的 REST API 端点

  • 我们验证通过 HTTPS 会话协商的 BIG-IP 节点的证书

我们将所有这些参数包含在一个名为conn_parameters的字典中,并将其包含在group_vars/all.yml文件中,以便应用于任何 BIG-IP 节点。

默认情况下,新的 LTM 设备使用admin/admin默认用户名和密码用于图形用户界面GUI)和 REST API 访问。我们将这些凭据作为conn_parameters字典中的用户和密码变量,并将Ansible_host变量指定为可以通过端口443建立 REST API 的 IP 地址。最后,我们禁用证书验证,因为 BIG-IP 节点上的证书是自签名的。

我们创建一个名为users的新变量,其中包含我们要在 LTM 上配置的所有新用户,以及他们的角色/权限。在这种情况下,我们希望为 Ansible 用户在 LTM 节点上的所有分区提供管理权限。

我们为新的 LTM 节点创建一个新的 playbook。在第一个任务中,我们使用bigip_user模块创建新用户,并使用provider属性提供参数以建立 HTTPS 连接。我们循环遍历users变量中的所有用户以进行配置。

第二个任务还使用bigip_user模块来更新 LTM 上的默认admin配置,并将此默认密码更改为admin_passwd变量中指定的新密码。

在 playbook 级别上,我们将连接设置为local。这是因为我们将从 Ansible 控制机建立 HTTPS 连接,并且我们希望阻止 Ansible 使用Secure ShellSSH)连接到 LTM 节点。

以下截图显示了在 BIG-IP 节点上创建的新的 Ansible 用户:

以下截图显示了使用 playbook 创建的 Ansible 用户的详细信息:

出于简单起见,我们使用明文密码;但是,永远不应该使用明文密码。应该使用 Ansible Vault 来保护密码。

还有更多...

在添加新的 Ansible 用户后,我们使用新创建的用户更新conn_parameters字典。我们可以使用这个用户开始管理 LTM 节点,如下所示:

$ cat group_vars/all.yml
conn_parameters:
 user: Ansible
 password: Ansible123
 server: "{{ Ansible_host }}"
 server_port: 443
 validate_certs: no
< -- Output Omitted for brevity --> 

在 BIG-IP 设备上配置通用系统选项

在这个步骤中,我们将概述如何在 BIG-IP 节点上配置一些基本的系统选项,如主机名、域名系统DNS)和网络时间协议NTP)。我们将了解如何使用各种可用的 Ansible 模块设置所有这些系统级参数。

准备工作

为了按照这个步骤进行操作,假定已经设置了 Ansible 清单。Ansible 和 BIG-IP 节点之间已经建立了 IP 连接,并且具有正确的用户凭据。

如何做...

  1. 使用以下系统级参数更新group_vars/all.yml文件:
$ cat group_vars/all.yml
< -- Output Omitted for brevity -->
domain: lab.net
nms_servers:
 - 172.20.1.250
  1. 创建一个名为tasks的新文件夹,并创建一个名为f5_system.yml的文件,其中包含以下内容:
$ cat tasks/f5_system.yml
---
- name: "Setup BIG-IP Hostname"
 bigip_hostname:
 hostname: "{{ inventory_hostname }}.{{ domain }}"
 provider: "{{ conn_parameters }}"
- name: "Setup BIG-IP DNS Servers"
 bigip_device_dns:
 ip_version: '4'
 name_servers: "{{ nms_servers }}"
 provider: "{{ conn_parameters }}"
- name: "Setup BIG-IP NTP Servers"
 bigip_device_ntp:
 ntp_servers: "{{ nms_servers }}"
 provider: "{{ conn_parameters }}"
  1. pb_f5_onboard.yml文件中,添加以下突出显示的任务:
$ cat pb_f5_onboard.yml
< -- Output Omitted for brevity -->
- name: "P1T3: Configure System Parameters"
 import_tasks: "tasks/f5_system.yml"
 tags: system

它是如何工作的...

为了在 BIG-IP 节点上配置各种系统参数,我们为每个任务使用单独的模块。我们将所有这些任务分组在一个名为f5_system.yml的文件夹下,并在此文件夹中使用三个单独的任务/模块,如下所示:

  • bigip_hostname来设置主机名

  • bigip_device_dns来设置 BIG-IP 节点将使用的 DNS 服务器

  • bigip_device_ntp来设置 BIG-IP 节点上的 NTP 服务器

所有这些模块都使用conn_parameters字典来正确设置如何与 BIG-IP 节点的 REST API 进行通信。在我们的示例拓扑中,我们使用单个服务器作为 DNS 和 NTP。我们在group_vars/all.yml文件中使用nms_servers变量来描述它,以应用于我们 Ansible 清单中的所有节点。

为了配置主机名,我们需要为设备提供一个完全合格的域名FQDN)。因此,我们在group_vars/all.yml文件中再次配置我们的域,并与设备名称一起使用以设置其主机名。

运行此 playbook 后,我们可以看到配置已应用到 BIG-IP 节点。以下截图显示主机名已正确配置:

NTP 配置已正确部署,如下截图所示:

DNS 已正确配置,如下截图所示:

在 BIG-IP 设备上配置接口和干线

在这个步骤中,我们将概述如何在 BIG-IP 设备上设置干线。BIG-IP 节点上的干线端口用于通过将多个接口组合成单个逻辑接口来为设备提供增加的冗余性。这与传统网络供应商中的端口通道非常相似。

准备工作

为了按照这个步骤进行,假定已经设置了 Ansible 清单。Ansible 和 BIG-IP 节点之间已经建立了 IP 连接,并且具有正确的用户凭据。

如何做...

  1. 创建一个host_vars文件夹,并创建一个名为ltm01.yml的文件,其中包含以下内容:
$ cat host_vars/ltm01.yml
----
phy_interfaces:
 - 1.1
 - 1.2
trunks:
 - name: po1
 members: "{{ phy_interfaces }}"
  1. tasks文件夹下,添加一个名为f5_interfaces.yml的新文件,其中包含以下内容:
$ cat tasks/f5_interfaces.yml
---
- name: Create a Port channel on BIG-IP
 bigip_trunk:
 name: "{{ item.name}}"
 interfaces: "{{ item.members }}"
 link_selection_policy: maximum-bandwidth
 frame_distribution_hash: destination-mac
 lacp_enabled: no
 provider: "{{ conn_parameters }}"
 state: present
 loop: "{{ trunks }}"
  1. 使用以下新任务更新pb_f5_onboard.yml playbook:
$ cat pb_f5_onboard.yml
< -- Output omitted for brevity -->
- name: "P1T4: Configure Interfaces"
 import_tasks: "tasks/f5_interfaces.yml"
 tags: intfs

它是如何工作的...

我们在host_vars文件夹下的一个名为ltm01.yml的文件中为 LTM 设备定义特定于主机的数据。在这个文件中,我们在phy_interfaces变量下定义了 LTM 节点上的物理接口。我们定义了另一个名为trunks的变量,以定义设备上可用的干线。在trunks变量中,我们引用phy_interfaces变量,以限制数据重复。

f5_interfaces.yml任务文件中,我们添加了一个新任务,使用bigip_trunk模块在 BIG-IP 节点上配置所需的干线。我们循环遍历trunks数据结构,以配置所有所需的干线端口。在这个任务中,我们提供了不同的参数来调整干线属性(例如禁用链路聚合控制协议LACP))并设置正确的方法来在干线端口之间分发帧。

运行 playbook 后,我们可以看到所需的干线接口已经配置,如下截图所示:

另请参阅...

有关bigip_trunk Ansible 模块的更多信息,以及如何在 BIG-IP 节点上部署干线端口的不同选项,请参阅以下网址:docs.Ansible.com/Ansible/latest/modules/bigip_trunk_module.html

在 BIG-IP 设备上配置 VLAN 和自 IP

在本教程中,我们将概述如何在 BIG-IP 节点上配置 VLAN。 BIG-IP 节点上的 VLAN 对于通过 BIG-IP LTM 节点托管的不同应用程序的流量分离至关重要。它们对于指定外部(面向互联网)和内部(面向服务器)域至关重要。我们还将概述如何在我们配置的 VLAN 接口上分配 IP 地址。

准备就绪

为了按照本教程进行操作,假定已经设置了 Ansible 清单。 Ansible 与 BIG-IP 节点之间已经建立了 IP 连接,并且具有正确的用户凭据。由于此设置中的所有 VLAN 都将部署在干线端口上,因此我们需要根据先前的教程已经配置好干线端口。

操作步骤...

  1. host_vars文件夹下的host_vars/ltm01.yml文件中使用以下 VLAN 数据进行更新:
$ cat host_vars/ltm01.yml
< -- Output Omitted for brevity -->
vlans:
 - vlan: 100
 description: Extrnal VLAN (Internet)
 ip: 10.1.100.254/24
 tagged_intf: po1
 - vlan: 10
 description: Server VLAN10 (Internal)
 ip: 10.1.10.254/24
 tagged_intf: po1
  1. tasks文件夹下的f5_interfaces.yml文件中更新任务,以配置 VLAN,如下所示:
$ cat tasks/f5_interfaces.yml
< -- Output Omitted for brevity -->
- name: Create VLANs on BIG-IP
 bigip_vlan:
 tagged_interfaces: "{{ item.tagged_intf }}"
 name: "VL{{item.vlan}}"
 description: "{{ item.description }}"
 tag: "{{item.vlan}}"
 provider: "{{ conn_parameters }}"
 state: present
 loop: "{{ vlans }}"
  1. tasks文件夹下的f5_interfaces.yml文件中更新任务,以在相应的 VLAN 上配置 IP 地址,如下所示:
$ cat tasks/f5_interfaces.yml
< -- Output Omitted for brevity -->
- name: Provision IP addresses on BIG-IP
 bigip_selfip:
 address: "{{ item.ip | ipv4('address') }}"
 name: "VL{{ item.vlan }}_IP"
 netmask: "{{ item.ip | ipv4('netmask') }}"
 vlan: "VL{{ item.vlan }}"
 provider: "{{ conn_parameters }}"
 state: present
 loop: "{{ vlans }}"

工作原理...

我们在host_vars/ltm01.yml中添加vlans数据结构,以声明我们需要在 LTM 节点上配置的所有 VLAN,以及与该 VLAN 相关联的 IP 地址。

我们使用bigip_vlan模块更新f5_interfaces.yml文件,以在 BIG-IP 节点上配置 VLAN,并循环遍历vlans数据结构以提取设置所需 VLAN 的所有必要参数。接下来,我们使用bigip_selfip Ansible 模块添加另一个任务,以在 VLAN 上部署 IP 地址。

再次运行 playbook 后,我们可以看到 BIG-IP 节点上的 VLAN 和自身 IP,如下截图所示:

正确的 IP 地址已根据以下截图正确配置在 VLAN 接口上:

另请参阅...

有关如何在 BIG-IP 节点上部署 VLAN 和自身 IP 的更多选项,请参考以下网址:

bigip-vlan

docs.ansible.com/ansible/latest/modules/bigip_vlan_module.html

bigip-selfip

docs.Ansible.com/Ansible/latest/modules/bigip_selfip_module.html#bigip-selfip-module

在 BIG-IP 设备上配置静态路由

在在 BIG-IP 设备上部署 VLAN 和 IP 地址后,我们需要配置 BIG-IP 节点上的路由,以便到达外部目的地。我们在我们的拓扑中使用静态路由,以在 LTM 节点上配置所需的路由。在本教程中,我们将概述如何在 BIG-IP 设备上配置静态路由。

准备就绪

为了按照本教程进行操作,假定已经设置了 Ansible 清单,并且 Ansible 与 BIG-IP 节点之间已经建立了 IP 连接,并且具有正确的用户凭据。此外,我们需要根据先前的教程在 BIG-IP 节点上部署 VLAN 和 IP 地址。

操作步骤...

  1. 使用以下路由数据更新host_vars/ltm01.yml文件:
$ cat host_vars/ltm01.yml
< -- Output Omitted for brevity -->
routes:
 - dst: 0.0.0.0/0
 gw: 10.1.100.1
 name: default_route
  1. 使用以下任务更新pb_f5_onboard.yml文件:
$ cat pb_f5_onboard.yml
< -- Output Omitted for brevity -->
- name: "P1T5: Setup External Routing"
 bigip_static_route:
 destination: "{{ item.dst.split('/')[0] }}"
 netmask: "{{item.dst | ipv4('prefix')}}"
 gateway_address: "{{ item.gw }}"
 name: "{{ item.name }}"
 provider: "{{ conn_parameters }}"
 loop: "{{ routes }}"
 tags: routing

工作原理...

我们在host_vars/ltm01.yml文件下添加routes数据结构,以声明需要在 LTM 节点上配置的所有静态路由。

我们使用bigip_static_route模块更新pb_f5_onboard.yml playbook,以配置静态路由,并循环遍历routes数据结构,以在设备上配置所有所需的路由。

再次运行 playbook 后,我们可以看到正确的静态路由,如下截图所示:

在 BIG-IP 设备上部署节点

使用 BIG-IP LTM 部署应用程序需要跨多个服务器对应用程序流量进行负载均衡。这要求我们定义托管应用程序的服务器/实例。在 BIG-IP 中,这些实例称为节点,并且它们使用唯一的 IP 地址标识每个服务器。在这个教程中,我们将开始在 BIG-IP 设备上部署一个新的应用程序(Web 服务器),并使用 Ansible 来配置承载此服务的节点。

准备工作

BIG-IP 的基本设置应该已经按照之前的教程完成,必须部署正确的 VLAN 以到达这些节点(物理服务器)。

如何操作...

  1. 创建一个名为web_app.yml的新的 YAML 文件,内容如下:
---
vip: 10.1.100.100
vip_port: 443
endpoint: dev.internet.net
pool_name: dev_web_app
pool_members:
 - ip: 10.1.10.10
 name: "dev01.internal.net"
 port: 443
 - ip: 10.1.10.11
 name: "dev01\. internal.net"
 port: 443
  1. 创建一个名为pb_f5_deploy_app.yml的新的 Ansible playbook,内容如下:
---
- name: Deploying a New App on BIG-IP
 hosts: ltm01
 connection: local
 vars_file: web_app.yml
 tasks:
 - name: "Create Nodes on BIG-IP"
 bigip_node:
 address: "{{ item.ip }}"
 name: "{{ item.name }}"
 provider: "{{ conn_parameters }}"
 state: present
 loop: "{{ pool_members }}"

工作原理...

我们在名为web_app.yaml的 YAML 文件中定义了新的 Web 应用程序的所有参数,该应用程序应该托管在 BIG-IP LTM 设备上。在这个文件中,我们包括了一个pool_members参数,用来概述将承载应用程序的 Web 服务器。我们使用这个参数在 BIG-IP LTM 上创建节点。

我们为应用程序部署创建一个新的 playbook,名为pb_f5_deploy_app.yml。我们包括web_app.yml文件,以便访问为此应用程序定义的所有参数。我们使用bigip_node模块创建一个新的节点,并循环遍历从web_app.yml文件中派生的pool_members参数,以在 BIG-IP 设备上创建所有必需的节点。为了连接到 BIG-IP 节点,我们使用与之前相同的提供者属性,并使用group_vars/all.yml文件中定义的conn_parameters参数来建立与 BIG-IP 的连接。

运行这个 playbook,我们创建了所有必需的节点,如下截图所示:

在 BIG-IP 设备上配置负载均衡池

在 BIG-IP 上创建节点后,我们需要为我们部署的应用程序创建一个负载均衡池,并将我们创建的节点中的池成员分配到这个池中。在这个教程中,我们将概述如何在 BIG-IP 节点上配置负载均衡池,以及如何将成员分配到负载均衡池中。

准备工作

这个教程假设之前的所有教程都已经实施,并且 BIG-IP 上的节点已经按照之前的教程进行了配置。

如何操作...

  1. 更新pb_f5_deploy_app.yml playbook,添加以下任务以创建一个新的池:
- name: Create New LB Pool
 bigip_pool:
 name: "POOL_{{ website }}_{{ vip_port }}"
 lb_method: round-robin
 state: present
 provider: "{{ conn_parameters }}"
  1. 更新pb_f5_deploy_app.yml playbook,添加以下任务以将池成员分配给新创建的池:
- name: Add Members to the Pool
 bigip_pool_member:
 pool: "POOL_{{ website }}_{{ vip_port }}"
 host: "{{ item.ip }}"
 name: "{{ item.name }}"
 port: "{{ item.port }}"
 description: "Web Server for {{ website }}"
 provider: "{{ conn_parameters }}"
 loop: "{{ pool_members }}"

工作原理...

在这个教程中,我们使用bigip_pool模块在 BIG-IP 系统上创建一个负载均衡池,并指定应该在此池上使用的负载均衡技术。在这个例子中,我们使用round-robin技术。我们使用从web_app.yml文件中提取的不同参数(主要是网站和vip_port)来创建池名称。

接下来,我们使用bigip_pool_member模块将池成员分配给这个新创建的池,并循环遍历web_app.yml文件中定义的所有pool_members

我们可以看到,所有这些过程都创建了一种一致的方法来定义池名称,并将所需的池成员分配给正确的池成员。所有信息都是从一个单一的定义文件中检索出来,该文件描述并概述了服务的部署方式。

运行这两个任务,我们将看到池已经正确创建,并且具有正确的池成员,如下截图所示:

以下截图显示了当前的成员:

另请参阅...

在这个食谱中,我们概述了使用 Ansible 模块在 BIG-IP 节点上提供负载均衡池的基本用法。然而,这些模块还有更多的选项,例如为每个成员指定负载均衡比率,以及为整个池附加监视器。请参考以下链接以获取更多选项:

在 BIG-IP 设备上配置虚拟服务器

在 BIG-IP LTM 上部署应用程序的最后一部分是在 BIG-IP LTM 节点上配置虚拟服务器,并为该虚拟服务器在 BIG-IP 节点上创建虚拟 IP(VIP)。在这个食谱中,我们概述了如何使用 Ansible 部署虚拟服务器。

准备工作

这个食谱假设所有先前的食谱都已完成,并且负载均衡池和池成员已经配置好。

如何操作...

  1. 使用以下任务更新pb_f5_deploy_app.yml playbook:
- name: Create Virtual Server
 bigip_virtual_server:
 name: "{{ website }}_{{ vip_port }}_VS"
 destination: "{{ vip }}"
 port: "{{ vip_port}}"
 pool: "POOL_{{ website }}_{{ vip_port }}"
 description: "VIP for {{ website }}"
 profiles:
 - http
 - name: clientssl
 context: client-side
 - name: serverssl
 context: server-side
 state: present
 provider: "{{ conn_parameters }}"

工作原理...

我们使用bigip_virtual_server模块在 BIG-IP 设备上提供所需的虚拟服务器,通过指定web_app.yml文件中定义的参数。我们还定义并提供需要应用于新创建的虚拟服务器的配置文件。这些配置文件是 HTTP 和 SSL 配置文件。这些配置文件已经默认在 BIG-IP 节点上创建,在需要创建自定义配置文件的情况下,我们需要使用适当的 Ansible 模块在单独的任务中创建这些配置文件。

运行最后一个任务,我们可以看到虚拟服务器已创建,如下面的截图所示:

在最后一个任务中,我们在 LTM 节点上创建了一个功能性的服务 VIP,以便开始处理我们新网站的 HTTP 请求,并将流量在负载均衡组中的所有实例之间进行负载均衡。

另请参阅...

在这个食谱中,我们讨论了使用 Ansible 模块在 BIG-IP 节点上提供虚拟服务器的基本用法。然而,还有更多的选项可用于调整需要部署的虚拟服务器的配置。

还有更多的 Ansible 模块可以让您创建可以附加到虚拟服务器的配置文件,以下是一些这些模块的链接:

从 BIG-IP 节点检索操作数据

在这个食谱中,我们概述了如何检索 BIG-IP 设备上不同组件的操作数据,例如 BIG-IP 节点的网络状态,如接口和 VLAN,以及与应用程序交付相关的组件的数据,如虚拟服务器和池。

准备工作

为了按照这个步骤进行操作,假设已经设置了 Ansible 清单,并且 Ansible 与 BIG-IP 节点之间已经建立了 IP 连接,并且具有正确的用户凭据。

如何做...

  1. 创建一个新的 Ansible playbook,pb_f5_validate.yml,内容如下:
---
- name: Validating BIG-IP Health
 hosts: ltm01
 connection: local
 tasks:
 - name: Collect Device Facts from BIG-IP
 bigip_device_facts:
 gather_subset:
 - interfaces
 provider: "{{ conn_parameters }}"
 register: bigip_facts
  1. 使用以下内容更新 playbook,以过滤接口事实的新任务:
 - name: Set Device Links
 set_fact:
 net_intfs: "{{ net_intfs | default([]) +
 bigip_facts.interfaces | selectattr('name','equalto',item|string) | list }}"
 loop: "{{ phy_interfaces }}"
  1. 使用以下内容更新pb_f5_validate.yml playbook,以验证接口状态的新任务:
 - name: Validate All Interface are operational
 assert:
 that:
 - item.enabled == 'yes'
 fail_msg: " Interface {{ item.name }} is Down"
 loop: "{{net_intfs}}"

它是如何工作的...

在 BIG-IP 节点上支持的 REST API 使用不同的方法从设备中检索操作数据,并以 JSON 格式输出所有这些数据。以下代码段概述了使用bigip_device_facts模块从 BIG-IP 节点收集的接口状态:

"bigip_facts": {
< -- Output Omitted for brevity -->
 "interfaces": [
 {
 "active_media_type": "10000T-FD",
 "bundle": "not-supported",
 "bundle_speed": "not-supported",
 "enabled": "yes",
 "flow_control": "tx-rx",
 "full_path": "1.1",
 "if_index": 48,
 "lldp_admin": "txonly",
 "mac_address": "00:50:00:00:01:01",
 "media_sfp": "auto",
 "mtu": 1500,
 "name": "1.1",
 < -- Output Omitted for brevity -->
 }

我们使用bigip_device_facts从 BIG-IP 节点检索操作事实,并且仅使用gather_subset限制从节点检索的数据。我们只包括interfaces选项来获取接口数据。我们将所有检索到的输出保存到bigip_facts变量中。

我们为设备创建了一个名为net_intfs的新事实。这个新事实的唯一用途是过滤从上一个任务中检索到的接口事实,以匹配我们在phy_interfaces参数(在host_vars文件夹下定义)中为我们的设备定义的接口。这个新参数将只包括我们在设计中声明的接口的接口事实。

我们使用assert模块来验证我们为应用程序定义的所有接口是否从检索到的数据中启用和运行,并且我们循环遍历net_intfs变量(它是一个列表)来确认它们都已启用。

还有更多...

如果我们需要获取部署在 LTM 节点上的应用程序的操作数据,我们创建一个新的 playbook 来验证应用程序部署,如下所示,使用bigip_device_facts模块。我们将检索到的数据限制为只有虚拟服务器。我们使用assert语句来验证数据,就像我们在之前的 playbook 中所做的那样。以下代码显示了用于应用程序部署验证的 playbook 内容。

  1. 我们创建一个新的 playbook,pb_f5_app_validate.yml,其中包含收集virtual-servers事实的以下任务:
---
- name: Validating BIG-IP App Health
 hosts: ltm01
 connection: local
 vars_files: web_app.yml
 tasks:
 - name: Collect Virtual-Servers Facts from BIG-IP
 bigip_device_facts:
 gather_subset:
 - virtual-servers
 provider: "{{ conn_parameters }}"
 register: bigip_app_facts
  1. 我们使用以下任务更新 playbook,以过滤virtual-servers事实:
    - name: Create Virtual Server Name Fact
 set_fact:
 vs_name: "{{ website }}_{{ vip_port }}_VS" - name: Create App Virtual Servers
 set_fact:
 app_vs: "{{ app_vs | default([]) +
 bigip_app_facts.virtual_servers | selectattr('name','equalto',vs_name) | list }}"
  1. 我们使用以下任务更新 playbook 来验证我们应用程序的虚拟服务器的状态:
    - name: Validate Virtual Address Status
 assert:
 that:
 - item.enabled == 'yes'
 - item.destination_address == vip
 - item.destination_port == vip_port
 fail_msg: " {{ item.name }} is No Setup Correctly"
 loop: "{{app_vs}}"

这些验证 playbook 可以扩展到验证虚拟服务器上的多个参数。此外,我们还可以验证其他组件,如 LTM 负载均衡池,以构建更全面的应用程序部署验证。

另请参阅...

有关 Ansible bigip_device_facts模块以及我们可以从 BIG-IP 节点检索的所有信息的更多信息,请访问以下网站:docs.Ansible.com/Ansible/latest/modules/bigip_device_facts_module.html

第六章:使用 NAPALM 和 Ansible 管理多供应商网络

Network Automation and Programmability Abstraction Layer with Multivendor support (NAPALM),顾名思义,是一个旨在与不同供应商设备交互的多供应商 Python 库,并且它提供了一种一致的方法来与所有这些设备进行交互,无论使用的是哪种供应商设备。

在之前的章节中,我们已经看到如何使用 Ansible 与不同的网络设备进行交互。然而,对于每个供应商操作系统,我们都必须使用不同的 Ansible 模块来支持特定的操作系统。此外,我们看到从每个供应商操作系统返回的数据完全不同。虽然编写多供应商设备的 playbook 仍然是可能的,但它需要使用多个不同的模块,并且我们需要处理这些设备返回的不同数据结构。这是 NAPALM 试图解决的主要问题。NAPALM 试图提供一个抽象和一致的 API 来与多个供应商操作系统进行交互,而 NAPALM 从这些不同的供应商操作系统返回的数据是规范化和一致的。

NAPALM 根据此节点支持的最常见 API 与每个设备进行交互,并且这个 API 被社区广泛采用。以下图表概述了 NAPALM 如何与最常见的网络设备进行交互,以及 NAPALM 用于与这些设备上的 API 进行交互的库:

由于 NAPALM 试图提供一种与网络设备交互的一致方法,它支持特定一组供应商设备。NAPALM 还支持在这些设备上执行的最常见任务,例如设备配置,检索接口的操作状态,Border Gate Protocol (BGP)和Link Layer Discovery Protocol (LLDP)等。有关支持的设备以及与这些设备交互时支持的方法的更多信息,请查看以下链接:napalm.readthedocs.io/en/latest/support/index.html

在本章中,我们将概述如何使用 NAPALM 和 Ansible 自动化多供应商网络。我们将概述如何管理这些不同供应商操作系统的配置,以及如何从这些设备中检索操作状态。我们将以基本服务提供商网络的以下示例网络图为基础进行说明:

以下表格概述了我们示例拓扑中的设备及其各自的管理Internet Protocols (IPs):

设备 角色 供应商 管理(MGMT)端口 MGMT IP
mxp01 P 路由器 Juniper vMX 14.1 fxp0 172.20.1.2
mxp02 P 路由器 Juniper vMX 14.1 fxp0 172.20.1.3
mxpe01 PE 路由器 Juniper vMX 14.1 fxp0 172.20.1.4
mxpe01 PE 路由器 Juniper vMX 17.1 fxp0 172.20.1.5
xrpe03 PE 路由器 Cisco XRv 6.1.2 Mgmt0/0/CPU0/0 172.20.1.6

本章涵盖的主要内容如下:

  • 安装 NAPALM 并与 Ansible 集成

  • 构建 Ansible 网络清单

  • 使用 Ansible 连接和认证网络设备

  • 构建设备配置

  • 使用 NAPALM 在网络设备上部署配置

  • 使用 NAPALM 收集设备信息

  • 使用 NAPALM 验证网络可达性

  • 使用 NAPALM 验证和审计网络

技术要求

本章的代码文件可以在此处找到:github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch6_napalm

本章中将需要以下软件:

  • 运行 CentOS 7 的 Ansible 机器

  • Ansible 2.9

  • Juniper Virtual MX (vMX) 路由器运行 Junos OS 14.1R8 和 Junos OS 17.1R1 版本

  • 运行 IOS XR 6.1.2 的 Cisco XRv 路由器

查看以下视频以查看代码的实际操作:

bit.ly/2Veox8j

安装 NAPALM 并与 Ansible 集成

在这个示例中,我们概述了如何安装 NAPALM 并将其集成到与 Ansible 一起工作。这个任务是强制性的,因为 NAPALM Ansible 模块不是默认随 Ansible 一起提供的核心模块的一部分。因此,为了开始使用这些模块,我们需要安装 NAPALM 及其所有 Ansible 模块。然后,我们需要告诉 Ansible 在哪里找到它,并开始使用 NAPALM 团队为 Ansible 开发的特定模块。

准备工作

Ansible 和 Python 3 需要安装在机器上,还需要安装python3-pip包,我们将用它来安装 NAPALM。

如何做…

  1. 安装napalm-ansible Python 包,如下面的代码片段所示:
$ pip3 install napalm-ansible
  1. 运行napalm-ansible命令,如下面的代码块所示:
$ napalm-ansible
  1. 为了确保 Ansible 可以使用 NAPALM 模块,您必须将以下配置添加到您的 Ansible 配置文件(ansible.cfg)中:
[defaults]
 library = /usr/local/lib/python3.6/site-packages/napalm_ansible/modules
 action_plugins = /usr/local/lib/python3.6/site-packages/napalm_ansible/plugins/action

有关 Ansible 配置文件的更多详细信息,请访问docs.ansible.com/ansible/latest/intro_configuration.html

  1. 创建一个名为ch6_napalm的新文件夹,并创建ansible.cfg文件,更新如下代码块所示:
$ cat ansible.cfg
[defaults]
inventory=hosts
retry_files_enabled=False
gathering=explicit
host_key_checking=False
library = /usr/local/lib/python3.6/site-packages/napalm_ansible/modules
action_plugins = /usr/local/lib/python3.6/site-packages/napalm_ansible/plugins/action

它是如何工作的…

由于 NAPALM 包和相应的 NAPALM Ansible 模块不是默认随 Ansible 一起提供和安装的核心模块的一部分,我们需要在系统上安装它,以便开始使用 NAPALM Ansible 模块。NAPALM 团队已经发布了一个特定的 Python 包来安装 NAPALM 以及所有 Ansible 模块和所有依赖项,以便从 Ansible 内部开始使用 NAPALM。这个包是napalm-ansible。由于我们使用 Python 3,我们将使用pip3程序来安装这个包。

为了告诉 Ansible 模块安装在哪里,我们需要将这些模块的路径输入到 Ansible 中。NAPALM 团队还提供了如何找到 NAPALM 模块安装路径以及如何通过napalm-ansible程序将其集成到 Ansible 的简单说明。我们执行napalm-ansible命令,它输出了我们需要包含在ansible.cfg文件中的所需配置,以便 Ansible 可以找到我们将要使用的 NAPALM 模块。

我们使用从napalm-ansible命令获得的输出更新ansible.cfg文件。然后,我们更新库和动作插件选项,告诉 Ansible 在搜索模块或动作插件时将这些文件夹包括在其路径中。在ansible.cfg文件中,我们包括了我们在前几章中使用的正常配置。

构建 Ansible 网络清单

在这个示例中,我们将概述如何构建和组织我们的 Ansible 清单,以描述本章中概述的样本服务提供商网络设置。构建 Ansible 清单是一个强制性步骤,为了告诉 Ansible 如何连接到受管设备。在 NAPALM 的情况下,我们需要将网络中的不同节点分类到 NAPALM 支持的正确供应商类型中。

如何做…

  1. 在新文件夹(ch6_napalm)中,我们创建一个包含以下内容的hosts文件:
$ cat hosts
[pe]
 mxpe01 ansible_host=172.20.1.3
 mxpe02 ansible_host=172.20.1.4
 xrpe03 ansible_host=172.20.1.5

[p]
 mxp01 ansible_host=172.20.1.2
 mxp02 ansible_host=172.20.1.6

[junos]
 mxpe01
 mxpe02
 mxp01
 mxp02

[iosxr]
 xrpe03
 [sp_core:children]
 pe
 p

它是如何工作的…

我们使用hosts文件构建了 Ansible 清单,并定义了多个组,以便对基础设施进行分段,如下所示:

  • 我们创建了PE组,它引用了我们拓扑中的所有多协议标签交换MPLS提供者边缘PE)节点。

  • 我们创建了P组,它引用了我们拓扑中的所有 MPLS 提供者P)节点。

  • 我们创建了junos组,以引用我们拓扑中的所有 Juniper 设备。

  • 我们创建了iosxr组来引用所有运行 IOS-XR 的节点。

在使用 NAPALM 时,根据供应商或操作系统对组进行分割和定义是一种最佳实践,因为我们使用这些组来指定 NAPALM 识别远程管理节点的供应商所需的参数,并建立与该远程节点的网络连接的方式。在下一个教程中,我们将概述我们将如何使用这些组(junosiosxr),以及我们将包括哪些参数以便 NAPALM 能够与远程管理节点建立连接。

使用 Ansible 连接和认证网络设备

在这个教程中,我们将概述如何使用 Ansible 连接到 Juniper 和 IOS-XR 节点,以便开始与设备进行交互。

准备工作

为了按照这个教程进行操作,应该按照之前的教程构建一个 Ansible 清单文件。此外,必须配置 Ansible 控制机与网络中所有设备之间的 IP 可达性。

如何做…

  1. 在 Juniper 设备上,配置用户名和密码,如下所示:
system {
 login {
 user ansible {
 class super-user;
 authentication {
 encrypted-password "$1$mR940Z9C$ipX9sLKTRDeljQXvWFfJm1"; ## ansible123
 }
 }
 }
}
  1. 在 Cisco IOS-XR 设备上,配置用户名和密码,如下所示:
!
 username ansible
 group root-system
 password 7 14161C180506262E757A60 # ansible123
!
  1. 在 Juniper 设备上启用网络配置协议NETCONF),如下所示:
system {
 services {
 netconf {
 ssh {
 port 830;
 }
 }
 }
}
  1. 在 IOS-XR 设备上,我们需要启用安全外壳SSH),以及启用xml-agent,如下所示:
!
xml agent tty
iteration off
!
xml agent
!
ssh server v2
ssh server vrf default
  1. 在 Ansible 机器上,在ch6_napalm文件夹中创建group_vars目录,并创建junos.ymliosxr.yml文件,如下所示:
$ cat group_vars/iosxr.yml
---
 ansible_network_os: junos
 ansible_connection: netconf

 $ cat group_vars/junos.yml
---
 ansible_network_os: iosxr
 ansible_connection: network_cli
  1. group_vars文件夹下,创建带有以下登录详细信息的all.yml文件:
$ cat group_vars/all.yml
ansible_user: ansible
 ansible_ssh_pass: ansible123

工作原理…

NAPALM 为 NAPALM 支持的每个供应商设备使用特定的传输 API。它使用这个 API 来连接设备,所以在我们的示例拓扑中,我们需要在 Juniper 设备上启用 NETCONF。对于 Cisco IOS-XR 设备,我们需要启用 SSH,并在 IOS-XR 设备上启用 XML agent。

在远程节点上配置用于在 Ansible 控制机上进行身份验证的用户名/密码。我们在设备上执行所有这些步骤,以使它们准备好与 NAPALM 进行通信。

在生产中使用 IOS-XR 设备上的传统xml agent不被推荐,并且需要根据 Cisco 文档进行评估。有关更多详细信息,请参阅www.cisco.com/c/en/us/td/docs/routers/asr9000/software/asr9k_r5-3/sysman/command/reference/b-sysman-cr53xasr/b-sysman-cr53xasr_chapter_01010.html

在 Ansible 机器上,我们根据每个供应商设置ansible_connection参数(juniper使用netconfiosxr使用network_cli),并指定ansible_network_os参数来指定供应商操作系统。所有这些参数都在junos.ymliosxr.ymlgroup_vars层次结构下定义,对应于我们在清单中定义的用于根据供应商操作系统对设备进行分组的组。最后,我们在all.yml文件中通过ansible_useransible_ssh_pass指定用户名和密码,因为我们使用相同的用户来对 Juniper 和 Cisco 设备进行身份验证。

为了测试和验证,我们可以使用 Ansible 的ping模块从 Ansible 控制机与设备进行通信,如下所示:

$ ansible all -m ping
mxpe01 | SUCCESS => {
  "changed": false,
  "ping": "pong"
}
mxpe02 | SUCCESS => {
  "changed": false,
  "ping": "pong"
}
mxp02 | SUCCESS => {
  "changed": false,
  "ping": "pong"
}
mxp01 | SUCCESS => {
  "changed": false,
  "ping": "pong"
}
xrpe03 | SUCCESS => {
  "changed": false,
  "ping": "pong"
} 

构建设备配置

NAPALM 不提供声明性模块来配置受管设备上的各种系统参数,比如接口的 BGP,服务质量QoS)等。但是,它提供了一个通用的 API 来将基于文本的配置推送到所有设备,因此需要以文本格式存在设备的配置,以便推送所需的配置。在这个配方中,我们将为所有设备创建配置。这是我们将在下一个配方中使用 NAPALM 推送到设备的配置。

准备工作

作为这个配方的先决条件,必须存在一个 Ansible 清单文件。

如何做…

  1. 创建一个roles文件夹,并在此文件夹内创建一个名为build_router_config的新角色,如下所示:
$ mkdir roles && mkdir roles/build_router_config
  1. 使用与我们在第三章《使用 Ansible 自动化 Juniper 设备的服务提供商》中为 Juniper 设备开发的build_router_config角色完全相同的内容(Jinja2 模板和任务)来为设备生成配置。目录布局应如下代码块所示:
$ tree roles/build_router_config/

roles/build_router_config/
 ├── tasks
 │ ├── build_config_dir.yml
 │ ├── build_device_config.yml
 │ └── main.yml
 └── templates
 └── junos
 ├── bgp.j2
 ├── intf.j2
 ├── mgmt.j2
 ├── mpls.j2
 └── ospf.j2
  1. templates文件夹下创建一个名为iosxr的新文件夹,并使用 Jinja2 模板填充它,以用于不同的 IOS-XR 配置部分,如下代码块所示:
$ tree roles/build_router_config/templates/iosxr/
 roles/build_router_config/templates/iosxr/
 ├── bgp.j2
 ├── intf.j2
 ├── mgmt.j2
 ├── mpls.j2
 └── ospf.j2
  1. 更新group_vars/all.yml文件,填入描述我们网络拓扑所需的数据,如下代码块所示:
$ cat group_vars/all.yml
tmp_dir: ./tmp
config_dir: ./configs
p2p_ip:
< -- Output Omitted for brevity -->
  xrpe03:
    - {port: GigabitEthernet0/0/0/0, ip: 10.1.1.7 , peer: mxp01, pport: ge-0/0/2, peer_ip: 10.1.1.6}
    - {port: GigabitEthernet0/0/0/1, ip: 10.1.1.13 , peer: mxp02, pport: ge-0/0/2, peer_ip: 10.1.1.12}

lo_ip:
  mxp01: 10.100.1.254/32
  mxp02: 10.100.1.253/32
  mxpe01: 10.100.1.1/32
  mxpe02: 10.100.1.2/32
  xrpe03: 10.100.1.3/32
  1. host_vars目录中为每个主机创建一个特定的目录,并在每个目录中创建带有以下 BGP 对等内容的bgp.yml文件:
$ cat host_vars/xrpe03/bgp.yml
bgp_asn: 65400
bgp_peers:
  - local_as: 65400
    peer: 10.100.1.254
    remote_as: 65400
  1. 创建一个名为pb_napalm_net_build.yml的新 playbook,该 playbook 利用build_router_config角色来生成设备配置,如下代码块所示:
$ cat pb_napalm_net_build.yml
---
- name: " Generate and Deploy Configuration on All Devices"
 hosts: sp_core
 tasks:
 - name: Build Device Configuration
 import_role:
 name: build_router_config
 delegate_to: localhost
 tags: build

工作原理…

在这个配方中,我们的主要目标是创建设备配置,我们将在示例拓扑中的设备上部署。我们使用了与我们在第三章《使用 Ansible 自动化 Juniper 设备的服务提供商》中用于生成 Juniper 设备配置的相同的 Ansible 角色。这个角色的唯一补充是我们为 IOS XR 添加了所需的 Jinja2 模板。

以下是步骤的快速解释,作为快速回顾:

  • 通过 Ansible 变量对网络进行建模

我们在group_vars/all.yml文件中描述了网络拓扑的不同方面,比如点对点P2P)接口,环回接口和开放最短路径优先OSPF)参数,这些都是在不同的数据结构下。对于任何特定主机的数据,我们使用host_vars目录来填充所有特定于特定节点的变量/参数,而在我们的情况下,我们使用这种方法来为每个节点的 BGP 数据概述bgp_peers变量。这为我们提供了填充 Jinja2 模板所需的所有必要数据,以生成我们示例网络中每个设备的最终配置。

  • 构建 Jinja2 模板

我们将所有的 Jinja2 模板放在我们的角色内的templates文件夹中,并且我们根据供应商 OS 对我们的 Jinja2 模板进行分段,每个都放在一个单独的文件夹中。接下来,我们为配置的每个部分创建一个 Jinja2 模板。以下代码片段概述了模板文件夹的目录结构:

templates/
 ├── iosxr
 │ ├── bgp.j2
 │ ├── intf.j2
 │ ├── mgmt.j2
 │ ├── mpls.j2
 │ └── ospf.j2
 └── junos
 ├── bgp.j2
 ├── intf.j2
 ├── ospf.j2
 ├── mgmt.j2
 └── mpls.j2

有关此配方中使用的不同 Jinja2 模板的详细解释以及它们如何使用定义的 Ansible 变量来生成最终配置,请参阅本书的第三章《使用 Ansible 自动化 Juniper 设备的服务提供商》,因为我们对 JunOS 和 IOS-XR 设备使用了完全相同的网络拓扑和相同的数据结构。

运行此 playbook 将在configs文件夹中为我们 Ansible 清单中的所有设备生成配置,如下代码块所示:

$ tree configs/
configs/
 ├── mxp01.cfg
 ├── mxp02.cfg
 ├── mxpe01.cfg
 ├── mxpe02.cfg
 └── xrpe03.cfg 

使用 NAPALM 在网络设备上部署配置

在这个配方中,我们将概述如何使用 Ansible 和 NAPALM 在不同的供应商设备上推送配置。NAPALM 提供了一个用于配置管理的单个 Ansible 模块,该模块允许我们使用单一的通用方法在 NAPALM 支持的任何供应商设备上推送任何配置,大大简化了 Ansible playbooks。

准备工作

要按照本配方进行操作,您需要已经设置好一个 Ansible 清单,并且在 Ansible 控制器和网络设备之间建立了网络可达性。我们将要推送到设备的配置是我们在上一个配方中生成的配置。

如何做...

  1. 更新pb_napalm_net_build.yml playbook 文件,并添加以下代码块中显示的任务:
$ cat pb_napalm_net_build.yml

---
- name: " Play 1: Deploy Config on All JunOS Devices"
  hosts: sp_core
  tasks:

< -- Output Omitted for brevity -->

    - name: "P1T5: Deploy Configuration"
      napalm_install_config:
        hostname: "{{ ansible_host }}"
        username: "{{ ansible_user }}"
        password: "{{ ansible_ssh_pass }}"
        dev_os: "{{ ansible_network_os }}"
        config_file: "{{config_dir}}/{{ inventory_hostname }}.cfg"
        commit_changes: "{{commit | default('no')}}"
        replace_config: yes
      tags: deploy, never

工作原理...

正如前面所述,NAPALM 提供了一个单一的 Ansible 模块,用于将配置推送到网络设备。它要求配置存在于一个文本文件中。当它连接到网络设备时,它会将配置推送到相应的设备上。

由于我们使用了一个可以在 NAPALM 支持的所有供应商 OS 设备上使用的单个配置模块,并且 NAPALM 使用不同的连接 API 来管理设备,因此我们需要告诉模块设备的供应商 OS。我们还需要提供其他参数,例如用户名/密码,以便登录并与设备进行身份验证。

napalm_install_config模块需要以下强制参数,以便正确登录到受管设备并将配置推送到设备上:

  • hostname:这是我们可以通过其到达设备的 IP 地址。我们为此参数提供ansible_host的值。

  • username/password:这是连接到设备的用户名和密码。我们需要提供ansible_useransible_ssh_pass属性。

  • dev_os:此参数提供了 NAPALM 需要的供应商 OS 名称,以便选择与设备通信的正确 API 和正确库。对于此选项,我们提供ansible_network_os参数。

  • napalm_install_config模块使用以下参数来管理远程设备上的配置:

  • config_file:这提供了包含需要推送到受管设备的设备配置的配置文件的路径。

  • commit_changes:这告诉设备是否提交配置。NAPALM 提供了一种一致的配置提交方法,即使对于默认不支持的设备(例如 Cisco IOS 设备)也是如此。

  • replace_config:此参数控制如何在设备上的现有配置和config_file参数中的配置之间进行合并。在我们的情况下,由于我们生成了整个设备配置,并且所有配置部分都在 Ansible 下进行管理,我们用我们生成的配置替换整个配置。这将导致设备上任何不在我们配置文件中的配置被删除。

根据本配方中概述的配置,当我们使用deploy标签运行 playbook 时,NAPALM 将连接到设备并推送配置。但是,它不会在远程设备上提交配置,因为我们已经将commit_changes的默认值指定为no。如果我们需要在远程设备上推送和提交配置,可以在运行 playbook 时将commit参数的值设置为yes,如下面的代码片段所示:

$ ansible-playbook pb_napalm_net_build.yml --tags deploy --e commit=yes

还有更多...

napalm_install_config模块提供了额外的选项来控制如何管理远程设备上的配置,例如配置差异。通过此选项,我们可以收集设备上运行配置与我们将通过 NAPALM 推送的配置之间的差异。可以通过以下方式启用此选项:

  • 创建一个名为config_diff的文件夹,用于存储 NAPALM 捕获的配置差异,如下面的代码块所示:
$ cat group_vars/all.yml

< -- Output Omitted for brevity -->
config_diff_dir: ./config_diff

$ cat tasks/build_config_dir.yml

- name: "Create Config Diff Directory"
 file: path={{config_diff_dir}} state=directory
 run_once: yes
  • 更新pb_napalm_net_build.yml playbook,如下面的代码块所示:
$ cat pb_napalm_net_build.yml

---
- name: "Generate and Deploy Configuration on All Devices"
 hosts: sp_core
 tasks:

< -- Output Omitted for brevity -->

 - name: "Deploy Configuration"
 napalm_install_config:
 hostname: "{{ ansible_host }}"
 username: "{{ ansible_user }}"
 password: "{{ ansible_ssh_pass }}"
 dev_os: "{{ ansible_network_os }}"
 config_file: "{{config_dir}}/{{ inventory_hostname }}.cfg"
 diff_file: "{{ config_diff_dir}}/{{ inventory_hostname }}_diff.txt"
 commit_changes: "{{commit | default('no')}}"
 replace_config: yes
 tags: deploy, never

接下来,我们创建一个新的文件夹,用于存放我们为每个设备生成的所有配置差异文件,并将diff_file参数添加到napalm_install_config模块。这将为每个设备收集配置差异,并将其保存到每个设备的config_diff目录中。

当我们在设备上运行具有修改配置的 playbook 时,我们可以看到为每个设备生成的config_diff文件,如下面的代码块所示:

$ tree config_diff/
config_diff/
 ├── mxp01_diff.txt
 ├── mxpe01_diff.txt
 ├── mxpe02_diff.txt
 └── xrpe03_diff.txt

使用 NAPALM 收集设备事实

在这个配方中,我们将概述如何使用 NAPALM fact-gathering Ansible 模块从网络设备中收集操作状态。这可以用来验证跨多供应商设备的网络状态,因为 NAPALM Ansible 的事实收集模块在 NAPALM 支持的所有供应商操作系统中返回一致的数据结构。

准备工作

为了跟随这个配方,假设 Ansible 清单已经就位,并且 Ansible 控制器与网络之间的网络可达性已经建立。最后,网络已根据以前的配方进行了配置。

如何做…

  1. 创建一个名为pb_napalm_get_facts.yml的 Ansible playbook,内容如下:
$ cat cat pb_napalm_get_facts.yml

---
- name: " Collect Network Facts using NAPALM"
 hosts: sp_core
 tasks:
 - name: "P1T1: Collect NAPALM Facts"
 napalm_get_facts:
 hostname: "{{ ansible_host }}"
 username: "{{ ansible_user }}"
 password: "{{ ansible_ssh_pass }}"
 dev_os: "{{ ansible_network_os }}"
 filter:
 - bgp_neighbors
  1. 使用以下任务更新 playbook 以验证 NAPALM 事实模块返回的数据:
$ cat pb_napalm_get_facts.yml

< -- Output Omitted for brevity -->

- name: Validate All BGP Routers ID is correct
 assert:
 that: napalm_bgp_neighbors.global.router_id == lo_ip[inventory_hostname].split('/')[0]
 when: napalm_bgp_neighbors

- name: Validate Correct Number of BGP Peers
 assert:
 that: bgp_peers | length == napalm_bgp_neighbors.global.peers.keys() | length
 when: bgp_peers is defined

- name: Validate All BGP Sessions Are UP
 assert:
 that: napalm_bgp_neighbors.global.peers[item.peer].is_up == true
 loop: "{{ bgp_peers }}"
 when: bgp_peers is defined

工作原理…

我们使用napalm_get_facts Ansible 模块从网络设备中检索操作状态。我们提供与napalm_install_config使用的相同参数(主机名用户名/密码dev_os),以便能够连接到设备并从这些设备收集所需的操作状态。

为了控制我们使用 NAPALM 检索的信息,我们使用filter参数并提供我们需要检索的必要信息。在这个例子中,我们将检索的数据限制为bgp_neighbors

napalm_get_facts模块返回从节点检索的数据作为 Ansible 事实。这些数据可以从napalm_bgp_neighbors变量中检索,该变量存储从设备检索到的所有 NAPALM BGP 事实。

以下片段概述了从 Junos OS 设备检索到的napalm_bgp_neighbors的输出:

ok: [mxpe02] => {
  "napalm_bgp_neighbors": {
    "global": {
      "peers": {
        "10.100.1.254": {
          "address_family": {
            "ipv4": {
              "accepted_prefixes": 0,
              "received_prefixes": 0,
              "sent_prefixes": 0
            },
 < -- Output Omitted for brevity -->
          },
          "description": "",
          "is_enabled": true,
          "is_up": true,
          "local_as": 65400,
          "remote_as": 65400,
          "remote_id": "10.100.1.254",
          "uptime": 247307
        }
      },
      "router_id": "10.100.1.2"
    }
  }
}

以下片段概述了从 IOS-XR 设备检索到的napalm_bgp_neighbors的输出:

ok: [xrpe03] => {
  "napalm_bgp_neighbors": {
    "global": {
      "peers": {
        "10.100.1.254": {
          "address_family": {

< -- Output Omitted for brevity -->
          },
          "description": "",
          "is_enabled": false,
          "is_up": true,
          "local_as": 65400,
          "remote_as": 65400,
          "remote_id": "10.100.1.254",
          "uptime": 247330
        }
      },
      "router_id": "10.100.1.3"
    }
  }
}

正如我们所看到的,不同网络供应商的 NAPALM 返回的 BGP 信息数据在不同网络供应商之间是一致的。这简化了解析这些数据,并允许我们运行更简单的 playbook 来验证网络状态。

我们使用 NAPALM 返回的数据来比较和验证网络的操作状态与我们使用 Ansible 变量(如bgp_peers)定义的网络设计。我们使用assert模块来验证多个 BGP 信息,例如以下内容:

  • 正确的 BGP 对数

  • BGP 路由器 ID

  • 所有 BGP 会话都是可操作的

在不同的assert模块中使用when语句的情况下,我们的拓扑中有一个不运行 BGP 的路由器(mxp02是一个例子)。因此,我们跳过这些节点上的这些检查。

另请参阅…

napalm_get_fact模块可以根据供应商设备支持的设备和供应商支持的事实级别检索网络设备的大量信息。例如,它支持几乎所有已知网络供应商的接口、IP 地址和 LLDP 对等体的检索。

有关napalm_get_facts模块的完整文档,请查看以下 URL:

napalm.readthedocs.io/en/latest/integrations/ansible/modules/napalm_get_facts/index.html

有关 NAPALM 支持的完整 facts/getters 及其针对供应商设备的支持矩阵,请参考以下 URL:

napalm.readthedocs.io/en/latest/support/.

使用 NAPALM 验证网络可达性

在这个步骤中,我们将概述如何利用 NAPALM 及其 Ansible 模块来验证网络在整个网络中的可达性。这个验证执行从受管设备到我们指定的目的地的 ping,以确保网络中的转发路径按预期工作。

准备工作

要按照这个步骤进行,假设已经有一个 Ansible 清单,并且 Ansible 控制器与网络之间已建立网络可达性。在这个步骤中,假设网络已根据相关的先前步骤进行了配置。

如何做…

  1. 创建一个名为pb_napalm_ping.yml的新 playbook,内容如下:
$ cat pb_napalm_ping.yml

---
- name: " Validation Traffic Forwarding with NAPALM"
 hosts: junos:&pe
 vars:
 rr: 10.100.1.254
 max_delay: 5 # This is 5 msec
 tasks:
 - name: "P1T1: Ping Remote Destination using NAPALM"
 napalm_ping:
 hostname: "{{ ansible_host }}"
 username: "{{ ansible_user }}"
 password: "{{ ansible_ssh_pass }}"
 dev_os: "{{ ansible_network_os }}"
 destination: "{{ rr }}"
 count: 2
 register: rr_ping
  1. 更新 playbook,添加如下代码块中所示的验证任务:
$ cat pb_napalm_ping.yml

< -- Output Omitted for brevity -->
- name: Validate Packet Loss is Zero and No Delay
 assert:
 that:
 - rr_ping.ping_results.keys() | list | first == 'success'
 - rr_ping.ping_results['success'].packet_loss == 0
 - rr_ping.ping_results['success'].rtt_avg < max_delay

工作原理…

NAPALM 提供了另一个 Ansible 模块napalm_ping,它连接到远程受管设备,并从远程受管设备向我们指定的目的地执行 ping。使用这个模块,我们能够验证受管设备与指定目的地之间的转发路径。

这个napalm_ping模块目前不支持 Cisco IOS-XR 设备,这就是为什么我们只选择 Junos OS 组中的所有 PE 设备。在我们的 playbook 中,我们使用junos:&pe模式来做到这一点。

在我们的示例中,我们创建了一个新的 playbook,并在 playbook 本身中使用vars参数指定我们要 ping 的目的地以及我们 ping 数据包的最大延迟。然后,我们使用napalm_ping模块连接到我们拓扑中的 MPLS PE 设备(只有 Junos OS 设备),从所有这些 PE 节点向我们指定的目的地(在我们的情况下,这是我们的路由反射器RR)路由器的环回)执行ping。我们将所有这些数据存储在一个名为rr_ping的变量中。

以下代码片段显示了从napalm_ping返回的输出:

"ping_results": {
 "success": {
 "packet_loss": 0,
 "probes_sent": 2,
 "results": [
 {
 "ip_address": "10.100.1.254",
 "rtt": 2.808
 },
 {
 "ip_address": "10.100.1.254",
 "rtt": 1.91
 }
 ],
 "rtt_avg": 2.359,
 "rtt_max": 2.808,
 "rtt_min": 1.91,
 "rtt_stddev": 0.449
 }
}

最后,我们使用assert模块来验证并比较 NAPALM 返回的结果与我们的要求(ping 成功,没有丢包,延迟小于max_delay)。

使用 NAPALM 验证和审计网络

在这个步骤中,我们将概述如何通过定义网络的预期状态并让 NAPALM 验证网络的实际/操作状态是否与我们的预期状态匹配来验证网络的操作状态。这对于网络审计和网络基础设施的合规性报告非常有用。

准备工作

要按照这个步骤进行,假设已经有一个 Ansible 清单,并且 Ansible 控制器与网络之间已建立网络可达性。最后,网络已根据先前概述的步骤进行了配置。

如何做…

  1. 创建一个名为napalm_validate的新文件夹,并为每个设备创建一个 YAML 文件。我们将验证其状态,如下面的代码块所示:
$ cat napalm_validate/mxpe01.yml

---
- get_interfaces_ip:
 ge-0/0/0.0:
 ipv4:
 10.1.1.3:
 prefix_length: 31
- get_bgp_neighbors:
 global:
 router_id: 10.100.1.1
  1. 创建一个名为pb_napalm_validation.yml的新 playbook,内容如下:
$ cat pb_napalm_validation.yml

---
- name: " Validating Network State via NAPALM"
 hosts: pe
 tasks:
 - name: "P1T1: Validation with NAPALM"
 napalm_validate:
 hostname: "{{ ansible_host }}"
 username: "{{ ansible_user }}"
 password: "{{ ansible_ssh_pass }}"
 dev_os: "{{ ansible_network_os }}"
 validation_file: "napalm_validate/{{ inventory_hostname}}.yml"
 ignore_errors: true
 register: net_validate
  1. 更新 playbook,创建一个文件夹,用于存储每个设备的合规性报告,内容如下代码块所示:
$ cat pb_napalm_validation.yml

< -- Output Omitted for brevity -->

- name: Create Compliance Report Folder
  file: path=compliance_folder state=directory

- name: Clean Last Compliance Report
  file: path=compliance_folder/{{inventory_hostname}}.txt state=absent

- name: Create Compliance Report
  copy:
    content: "{{ net_validate.compliance_report | to_nice_yaml }}"
    dest: "compliance_folder/{{ inventory_hostname }}.txt"

工作原理…

NAPALM 提供了另一个用于网络验证的模块,即napalm_validate模块。该模块主要用于执行审计并为网络基础设施生成合规性报告。主要思想是声明网络的预期状态,并在 YAML 文档中定义它。这个 YAML 文件有一个特定的格式,遵循不同 NAPALM 事实生成的相同结构。在这个 YAML 文件中,我们指定了我们想要从网络中检索的 NAPALM 事实,以及网络的预期输出。

我们将这些验证文件提供给napalm_validate模块,NAPALM 将连接到设备,检索这些验证文件中指定的事实,并将从网络中检索的输出与这些验证文件中声明的网络状态进行比较。

接下来,NAPALM 生成一个compliance_report对象,其中包含比较的结果以及网络是否符合这些验证文件。我们还设置了ignore_errors参数,以便在设备不符合时继续执行此 playbook 中的其他任务,这样我们就可以在生成的合规性报告中捕获这个合规性问题。

最后,我们将输出保存在一个名为compliance_folder的单独文件夹中,为每个节点复制compliance_report参数的内容,并使用to_nice_yaml过滤器进行格式化。

以下是为mxpe01设备生成的正确合规性报告的代码片段:

complies: true
get_bgp_neighbors:
 complies: true
 extra: []
 missing: []
 present:
 global:
 complies: true
 nested: true
get_interfaces_ip:
 complies: true
 extra: []
 missing: []
 present:
 ge-0/0/0.0:
 complies: true
 nested: true
skipped: []

另请参阅...

有关验证部署和napalm_validate的其他选项的更多信息,请查看以下网址:

第七章:使用 Ansible 部署和操作 AWS 网络资源

云是改变多个行业的一项技术。它对 IT 的整体基础设施、应用程序的部署方式以及为云采用而进行架构设计都产生了重大影响。

AWS 是主要的云提供商之一。它提供了多个网络资源和服务,以构建可扩展和高可用的网络设计,以在 AWS 云上托管应用程序。

云采用的主要支柱之一是自动化以及我们能够多快地部署工作负载。每个云提供商都有自己的自动化能力。在 AWS 的情况下,这是一个名为 CloudFormation 的服务,它使我们能够使用基础设施即代码(IaC)描述 AWS 基础设施,并在 AWS 云上部署基础设施。然而,与 CloudFormation 相比,Ansible 的优势在于其能够描述/部署所有云提供商(包括 AWS)的资源。这使我们能够在多云环境中使用一致的工具来部署我们的工作负载。

Ansible 提供了多个模块来与 AWS 云进行交互,以提供和控制不同的资源。

在本章中,我们将专注于部署 AWS 提供的基本网络服务,这些服务允许我们在 AWS 中构建可扩展的网络设计。我们将在我们的示例中使用以下样本 AWS 网络设计,并概述如何使用 Ansible 构建这个网络:

本章涵盖的主要教程如下:

  • 安装 AWS SDK

  • 构建 Ansible 清单

  • 认证到您的 AWS 账户

  • 使用 Ansible 部署 VPC

  • 使用 Ansible 部署子网

  • 使用 Ansible 部署 IGWs

  • 使用 Ansible 控制 VPC 内的路由

  • 使用 Ansible 部署网络 ACL

  • 使用 Ansible 进行部署验证

  • 使用 Ansible 取消部署 AWS 资源

技术要求

本章中使用的 GitHub 代码可以在这里找到:

github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch7_aws

本章基于以下软件版本:

  • 运行 CentOS 7 的 Ansible 机器

  • Ansible 2.9

  • Python 3.6.8

查看以下视频,了解代码的实际操作:

bit.ly/3ckoAFe

安装 AWS SDK

在这个教程中,我们将概述如何安装 Ansible 所需的 Python 库,以开始与 AWS 编排系统进行交互。这一步是强制性的,因为这些 Python 库必须安装在 Ansible 控制机器上,以便所有的 Ansible AWS 模块都能正常工作。

准备工作

您需要在机器上拥有 sudo 访问权限,以安装所需的 Python 库。此外,您需要安装python-pip包,因为我们将使用pip来安装所需的 Python 库。

如何操作...

  1. 我们可以测试任何 Ansible AWS 模块,以检查所需的 Python 库是否已安装:
$ ansible localhost -m aws_az_facts

localhost | FAILED! => {
 "changed": false,
 "msg": "boto3 required for this module"
 }
  1. 安装botoboto3包,如下所示:
$ sudo pip3 install boto3 boto

工作原理...

与 AWS 编排系统 API 交互的 Python SDK 库是botoboto3。这些 Python 包必须存在于 Ansible 控制机器上,因为在 Ansible 中,所有的 AWS 模块都依赖于这些 Python 包中的一个来操作。我们可以通过使用上述第一步来运行任何 AWS 模块(例如aws_az_facts)使用ansible命令来检查系统上是否已安装了这个包。如果boto3库不存在,我们将收到一条错误消息,告知我们boto3未安装。

我们可以使用 Python pip 程序使用pip3命令安装botoboto3包,这将安装所有需要安装和运行包的依赖项。在这个阶段,我们已经拥有了运行所有 Ansible AWS 模块所需的一切。

构建 Ansible 清单

在这个示例中,我们将概述如何构建一个 Ansible 清单,以描述我们将在 AWS 公共云中构建的基础设施网络设置。这是为了定义我们将部署基础设施的所有地区中的所有 VPC 而必须的步骤。

如何做到...

  1. 创建一个新的ch7_aws文件夹,并在其中创建一个hosts文件,如下所示:
$ cat hosts

[us]
 us_prod_vpc

[eu]
 eu_prod_vpc

[prod_vpcs]
 us_prod_vpc
 eu_prod_vpc
  1. ch7_aws内创建ansible.cfg文件,内容如下所示:
$ cat ansible.cfg

[defaults]
 inventory=hosts
 vault_password_file=~/.ansible_vault_passwd
 gathering=explicit
 transport=local
 retry_files_enabled=False
 action_warnings=False

它是如何工作的...

我们创建了主机的 Ansible 清单文件,现在我们需要在清单中声明我们的 VPC 作为节点,类似于我们如何定义网络节点。唯一的例外是 VPC 没有管理 IP 地址,因此我们不为这些 VPC 指定ansible_host参数。

我们需要在我们的清单文件中创建以下组:

  • 一个 US 组,将所有美国的 VPC 分组在一起

  • 一个 EU 组,将所有欧洲的 VPC 分组在一起

  • prod_vpcs,将所有我们的生产 VPC 分组在一起

我们还需要定义ansible.cfg文件,其中包含我们在所有先前示例中使用的所有配置选项。我们需要指定包含我们将用于加密所有敏感信息的加密密码的 vault 密码文件。

身份验证到您的 AWS 账户

在这个示例中,我们将概述如何创建所需的凭据,以便以编程方式对我们的 AWS 账户进行身份验证,并且如何使用 Ansible Vault 来保护这些凭据。这是为了能够在所有后续示例中运行任何 Ansible 模块而必须的步骤。

准备工作

Ansible 控制器必须具有互联网访问权限,并且 Ansible 清单必须按照上一个示例中的说明进行设置。此外,执行这些步骤的用户必须具有在 AWS 账户上创建新用户所需的访问权限。

如何做到...

  1. 使用IAM创建一个新用户,具有编程访问权限,如下所示:

  1. 为这个新用户分配正确的 IAM 策略,允许他们创建应该管理的所有网络资源(或者为简单起见,使用完全访问策略):

  1. 完成用户创建,在最后一页,添加用户向导将显示访问密钥 ID 和秘密访问密钥的.csv文件以供下载。这些参数将用于对该账户的 AWS API 进行身份验证:

  1. 使用 Ansible Vault 加密访问密钥 ID 和秘密访问密钥,如下所示:
$ ansible-vault encrypt_string <ACCESS_KEY_ID> --name aws_access_key

$ ansible-vault encrypt_string <SECRET_ACCESS_KEY> --name aws_secret_key_id

  1. ch7_aws内创建group_vars,并在group_vars内创建all.yml文件。使用在上一步中使用ansible-vault加密的密码填充all.yml文件:
ansible_connection: local

aws_access_key: !vault |
 $ANSIBLE_VAULT;1.1;AES256
 37623631653336633662666138353639653365323637323665353731386661343164393664333434
 3430306562623532366137663835636138613633633835660a656235363130303035383965663464
 39326130613433643861653933623032393735376466333861326537646563643736356632303435
 6631326531666461310a353461396431303765393830636432363430323438373635383132336462
 37356163643662623633643965386465656563613533613938666232343063396261

aws_secret_key_id: !vault |
 $ANSIBLE_VAULT;1.1;AES256
 38353165666437393262303035646531666139623963643066623066346633393964633438626539
 6266623937343036376266373463623266316462613139660a336664353564623531393332613433
 34336363393962666633363630393631376135656666623862373966643935386665363733376133
 6236326462326566320a653364336464363963623136363362666632396133613863376166343135
 37373839316430643337353564373062333232656136393564613132333065316638383739326238
 3530386534303033636463626664346234653136353534633265

它是如何工作的...

第一步是确保用户帐户通过 API 对 AWS 控制台具有编程访问权限。为了使用户能够对 AWS API 进行身份验证,用户必须被分配两个密码,这些密码在用户创建或用户请求更改密码时由 AWS 生成。这两个密码是访问密钥 ID 和秘密访问密钥。这两个密码仅在创建时可见,并且 AWS 会在 CSV 文件中提供它们供您下载。此外,我们需要确保此用户具有正确的 IAM 权限来创建必要的资源(VPC、子网、路由表等)。因此,在我们的示例中,为这个新用户分配了管理员策略,这使他们可以完全访问 AWS 账户以创建任何资源(如 EC2 实例、VPC、子网等)。我们概述了创建新用户的步骤,如果用户已经具有编程访问权限和所需的 IAM 权限,则这些步骤是可选的;我们只是为了完整性而演示了这一点。

由于我们在 CSV 文件中以明文形式生成了 AWS 为该账户生成的密码,我们可以使用 Ansible Vault 加密这些密码,并将它们存储在group_vars/all.yml文件中,以便在创建 VPC 的所有资源时使用这些凭据。我们将这些密码加密后存储到aws_access_keyaws_secret_key_id参数中,使用ansible-vault encrypt_string命令进行加密。Ansible Vault 使用我们在ansible.cfg文件中声明的 Vault 密码文件,该文件具有我们将用于加密所有这些密码的加密密码。

在下一个教程中,我们将概述如何使用我们创建的这些加密变量来在创建 VPC 时进行 AWS 控制台身份验证。

使用 Ansible 部署 VPC

在这个教程中,我们将概述如何使用 Ansible 部署 AWS VPC。AWS VPC 是 AWS 中的基础网络构造,可以被视为管理员在其 AWS 账户中创建的云中的虚拟数据中心。为了开始构建 AWS 中的任何其他基础设施相关服务,必须首先创建一个 VPC。我们将概述如何描述所有必需的 VPC 以及如何使用 Ansible 自动化它们的创建。

准备就绪

要连接到 AWS API,AWS 控制机器必须连接到互联网。AWS 账户也必须按照前面的教程准备好,具有所需的 AWS 凭据。

如何做...

  1. group_vars目录下创建us.ymleu.yml文件,并填写这些文件与 AWS 区域名称定义,如下所示:
$ cat group_vars/eu.yml
 aws_region: eu-west-1

$ cat group_vars/eu.yml
 aws_region: us-east-1
  1. host_vars目录下创建eu_prod_vpc.ymlus_prod_vpc.yml文件,并填写 VPC 参数,如下所示:
$ cat host_vars/eu_prod_vpc.yml

vpc_name: EU_Prod_Public_VPC
vpc_cidr: 10.3.0.0/16
vpc_tags:
 role: prod
 region: eu EU $ cat host_vars/us_prod_vpc.yml
vpc_name: US_Prod_Public_VPC
vpc_cidr: 10.1.0.0/16
vpc_tags:
 role: prod
 region: US
  1. 创建一个新的 playbook,pb_aws_net_build.yml,并填写如下:
$ cat pb_aws_net_build.yml

- name: Create all AWS Networks
 hosts: prod_vpcs
 environment:
 AWS_ACCESS_KEY: "{{ aws_access_key }}"
 AWS_SECRET_KEY: "{{ aws_secret_key_id }}"
 tasks:
 - name: Create New VPC
 ec2_vpc_net:
 cidr_block: "{{ vpc_cidr }}"
 region: "{{ aws_region }}"
 name: "{{ vpc_name }}"
 state: "{{ vpc_state | default('present') }}"
 tags: "{{ vpc_tags }}"
 register: create_vpc

它是如何工作的...

AWS 具有全球存在,并将其基础设施的每个部分分隔到世界的每个部分中的区域。AWS 区域是世界某个地区的 AWS 设施集合,AWS 中的每个区域被认为是一个具有自己的编排和管理系统的隔离故障域。因此,当我们创建 VPC 时,我们需要指定将在哪个区域部署此 VPC,因此我们需要在我们的 Ansible 变量中描述这些信息。在我们的情况下,我们将所有 VPC 的 AWS 区域指定为us-east-1和所有 VPC 的 AWS 区域指定为eu-west-1。这是通过在eu.ymlus.yml文件以及group_vars目录下定义aws_region变量来实现的。

AWS 区域的这种逻辑对于大多数 AWS 中特定于区域的服务至关重要,我们将构建的所有网络构造都是特定于区域的。对于几乎所有的 AWS Ansible 模块,我们需要指定 AWS 区域,以便发起正确的 API 调用到指定区域的正确 API 端点。这是因为每个区域的 API 端点具有不同的 FQDN。有关 AWS 在所有区域中所有服务的 API 端点的更多信息,请使用以下链接:

docs.aws.amazon.com/general/latest/gr/rande.html

我们需要在host_vars目录下为每个 VPC 声明变量,并为每个 VPC 创建一个 YAML 文件。我们需要指定 VPC 名称、前缀和应分配给 VPC 的标记。最后,我们需要创建 Ansible 剧本来构建我们的基础设施,并在剧本中使用一个新选项,即环境。此选项在剧本执行期间创建临时环境变量(AWS_ACCESS_KEYAWS_SECRET_KEY)。这些环境变量的值设置为group_vars/all.yml文件中定义的aws_access_keyaws_secret_key_id变量的相同值。这样,在剧本执行期间,这些环境变量中包含的值可以用于验证每个任务中的 AWS 模块的所有 API 调用。

我们可以使用ec2_vpc_net Ansible 模块在 AWS 云上创建 VPC,并可以使用region属性指定将部署此 VPC 的 AWS 区域。我们需要定义其 IP 前缀、名称和任何相关标记。所有这些信息都来自我们为此 VPC 在host_vars文件中定义的变量。

当模块创建 VPC 时,它会返回创建的 VPC 的所有信息,我们可以将这些信息保存在一个名为create_vpc的新变量中。

以下是 VPC 创建任务返回的数据片段:

"create_vpc": {
 "vpc": {
 "cidr_block": "10.1.0.0/16",

< -- Output Omitted for brevity -->

 "dhcp_options_id": "dopt-b983c8c2",
 "id": "vpc-0d179be0eb66847f3",
 "instance_tenancy": "default",
 "is_default": false,
 "owner_id": "955645556619",
 "state": "available",
 "tags": {
 "Name": "US_Prod_Public_VPC",
 "region": "US",
 "role": "prod"
 }
 }
}

以下屏幕截图概述了从控制台在 AWS 上创建的 VPC:

另请参阅

有关ec2_vpc_net模块和此模块中可用的其他参数的更多信息,请使用以下 URL:

docs.ansible.com/ansible/latest/modules/ec2_vpc_net_module.html

使用 Ansible 部署子网

在这个配方中,我们将概述如何使用 Ansible 在 AWS VPC 中部署子网。子网是 AWS 中的基本网络构造,以便为部署在 AWS 云上的应用程序提供更弹性。通过将子网映射到不同的可用区,可以实现额外的弹性。使用这种逻辑,我们可以通过将资源分布到不同的可用区来为我们的部署提供高可用性。

准备工作

Ansible 控制机必须具有互联网可达性,并且 VPC 必须根据之前的配方进行预配。

如何做...

  1. 使用如下所示的子网数据更新host_vars/eu_prod_vpc.yml文件。对于host_vars/us_prod_vpc.yml也是一样,包括所有子网的数据:
$ cat host_vars/eu_prod_vpc.yml

< -- Output Omitted for brevity -->

vpc_subnets:
 eu-prod-public-a:
 cidr: 10.3.1.0/24
 az: "{{ aws_region }}a"
 tags: "{{ vpc_tags }}"
 public: true

 eu-prod-public-b:
 cidr: 10.3.2.0/24
 az: "{{ aws_region}}b"
 tags: "{{ vpc_tags }}"
 public: true
  1. 更新pb_aws_net_build.yml剧本,并填充新任务以构建子网:

- name: "set fact: VPC ID"
 set_fact:
 vpc_id: "{{ create_vpc.vpc.id }}"

- name: create VPC subnets
 ec2_vpc_subnet:
 region: "{{ aws_region }}"
 vpc_id: "{{ vpc_id }}"
 cidr: "{{ item.value.cidr }}"
 az: "{{ item.value.az }}"
 tags: "{{item.value.tags | combine({ 'Name': item.key })}}"
 with_dict: "{{ vpc_subnets }}"
 register: create_vpc_subnets

工作原理...

可用区是在 AWS 区域内为物理基础设施提供弹性的构造。为了有效使用可用区,我们需要将 VPC 内的基础设施分配到区域内的不同可用区。这是通过使用 AWS 子网来实现的。

在我们的示例部署中,我们使用了两个子网,分布在两个可用区,以提供我们的网络设置高可用性。我们使用vpc_subnets变量声明我们将在每个 VPC 中部署的子网。这些变量包括我们将在每个子网中使用的 CIDR(必须是 VPC CIDR 的子集),我们希望将此子网附加到的可用区,最后,我们希望分配给此子网的标签。我们使用 AWS 区域加上后缀(abc等)构建可用区的名称。这是 AWS 用于命名区域内可用区的命名约定。

为了在 AWS 中创建子网,我们需要将子网与其父 VPC 关联。为了做到这一点,我们需要在创建子网的 API 调用期间指定vpc-id参数。这个vpc-id是 AWS 在创建 VPC 时分配给 VPC 的唯一标识符。我们从创建 VPC 的任务中获取这个值,并将此任务的输出保存到vpc_create变量中。我们可以使用这个变量来检索 VPC 的 ID,并使用set_fact模块将其分配给vpc-id变量。

最后,我们可以使用ec2_vpc_subnet模块构建子网,以在每个 VPC 中创建必要的子网,并循环遍历vpc_subnets数据结构,以构建所有所需的子网。

以下截图显示了在我们的US_Prod VPC 中在 AWS 云上正确规定的子网:

以下是分配给此子网的标签:

另请参阅

有关ec2_vpc_subnet模块和此模块中可用的其他参数的更多信息,请使用以下 URL:

docs.ansible.com/ansible/latest/modules/ec2_vpc_subnet_module.html#ec2-vpc-subnet-module

使用 Ansible 部署 IGWs

在这个配方中,我们将概述如何使用 Ansible 部署Internet GatewaysIGWs)到我们的 AWS VPC。IGWs 是我们从 VPC 到互联网的出口点,以便到达公共外部目的地。由于我们正在构建一个面向公众的服务,我们需要从我们的 VPC 到互联网的可达性。这是通过 AWS 云中的 IGW 构造实现的。

准备工作

Ansible 控制机必须具有互联网可达性,并且 VPC 必须根据先前的配方进行了规定。

如何操作...

  1. 更新eu_prod_vpc.yml文件与 IGW 数据,如下所示,并对us_prod_vpc.yml执行相同操作:
$ cat host_vars/eu_prod_vpc.yml

< -- Output Omitted for brevity -->

igw_name: eu_prod_igw

$ cat host_vars/eu_prod_vpc.yml

< -- Output Omitted for brevity -->

igw_name: us_prod_igw
  1. 更新pb_aws_net_build.yml playbook,并填充新任务以构建 IGW 节点:

- name: Create IGW
 ec2_vpc_igw:
 region: "{{ aws_region }}"
 vpc_id: "{{ vpc_id }}"
 state: present
 tags: "{{ vpc_tags | combine({'Name': igw_name}) }}"
 register: vpc_igw_create

- name: Extract VPC IGW ID
 set_fact:
 igw_id: "{{ vpc_igw_create.gateway_id }}"

工作原理...

IGW 网络构造是我们从 VPC 到互联网公共目的地的出口点。IGW 附加到 VPC,并为位于 VPC 内的任何资源(如 EC2 或 RDS 实例)提供互联网连接。为了创建 IGW,我们需要指定要将此 IGW 附加到的 VPC。因此,我们需要 VPC 的 ID。

正如我们在前面的配方中讨论的,当我们创建 VPC 时,我们会得到 VPC ID,并且我们可以使用一个单独的任务保存这个变量。我们可以在 IGW 的创建过程中使用这个变量的值。我们可以使用ec2_vpc_igw模块创建 IGW,并指定我们希望将此 IGW 部署到的区域。我们还可以指定 IGW 将附加到的 VPC ID。最后,我们可以指定要分配给 IGW 节点的标签。IGW 标签是可选的,但在使用自动部署时非常重要,因为它们允许我们引用我们创建的对象。我们将在后续的配方中概述在部署验证和事实收集时使用标签。

当我们部署新的 IGW 时,ec2_vpc_igw模块返回在 AWS 内部配置的 IGW 参数。一个特别重要的参数是igw-id。此参数唯一标识了配置的 IGW 节点,我们在引用此 IGW 节点相关的任何操作时必须使用它。

以下是由ec2_vpc_igw返回的 IGW 参数片段,我们在us_prod_vpc中的 IGW 节点中捕获了这些参数,并将其存储在vpc_igw_create变量中:

ok: [us_prod_vpc] => {
 "vpc_igw_create": {
 "changed": true,
 "failed": false,
 "gateway_id": "igw-05d3e4c664486790b",
 "tags": {
 "Name": "us_prod_igw",
 "region": "US",
 "role": "prod"
 },
 "vpc_id": "vpc-0abc32281330c9bc6"
 }
}

在上一个任务中,我们捕获了ec2_vpc_igw返回的gateway-id变量,并将其存储在一个新变量igw_id中,我们将在后续任务中引用 IGW 节点时使用它。

以下截图概述了在 VPC 中配置和附加的 IGW 节点:

另请参阅

有关ec2_igw_vpc模块和此模块中可用的其他参数的更多信息,请使用以下 URL:

docs.ansible.com/ansible/latest/modules/ec2_vpc_igw_module.html#ec2-vpc-igw-module

使用 Ansible 控制 VPC 内的路由

在这个示例中,我们将概述如何调整 AWS VPC 内的路由,以控制 VPC 内子网中的流量转发。通过控制 VPC 内的路由,我们可以自定义 VPC 设计以及 VPC 内的流量转发方式,以及如何将流量发送到外部目的地。

准备工作

Ansible 控制机必须具有互联网可达性,并且 VPC 必须按照上一个示例进行配置。

操作步骤

  1. 更新eu_prod_vpc.yml文件,其中包含路由表数据,如下所示,并对us_prod_vpc.yml执行相同操作:
$ cat host_vars/eu_prod_vpc.yml

< -- Output Omitted for brevity -->

route_table:
 tags:
 Name: eu_public_rt
 igw:
 - dest: 0.0.0.0/0
 gateway_id: "{{ igw_id }}"
 public:
 - eu-prod-public-a
 - eu-prod-public-b
  1. 更新pb_aws_net_build.yml playbook,并填充以下任务以将路由表附加到我们创建的 VPC:
- name: Get Default VPC Route Table
 ec2_vpc_route_table_facts:
 region: "{{ aws_region }}"
 filters:
 vpc-id: "{{ vpc_id }}"
 register: vpc_route_table_facts
 tags: rt

- name: Extract Route Table IDs
 set_fact:
 rt_id: "{{vpc_route_table_facts.route_tables[0].id }}"
 tags: rt
  1. 更新 playbook 并填充以下任务以更新所需路由的路由表:
- name: Update Default VPC Route Table
 ec2_vpc_route_table :
 region: "{{ aws_region }}"
 vpc_id: "{{ vpc_id }}"
 route_table_id: "{{ rt_id }}"
 routes: "{{ route_table.igw }}"
 subnets: "{{ route_table.public }}"
 lookup: id
 state: present
 tags: "{{ vpc_tags | combine(route_table.tags) }}"

工作原理

到目前为止,我们已经设置了 VPC、子网和 IGW。但是,尽管 IGW 节点已连接到互联网并附加到 VPC,但 VPC 相关联的路由表仍未更新,也没有路由指向 IGW,因此 VPC 内的流量都不会使用 IGW 节点。

以下是在更改路由表之前us_prod_vpc的默认路由表的片段:

AWS VPC 具有默认路由表,该路由表分配给 VPC 和所有未分配特定路由表的子网。因此,默认情况下,VPC 中的所有子网都与 VPC 的默认路由表相关联。

以下是一个截图,显示了在us_prod_vpc中创建的子网与默认路由表相关联的情况:

在我们为每个 VPC 声明的 VPC 定义中,我们包含了一个名为route_table的新数据结构,其中包含我们需要调整 VPC 的路由表并将所有子网与其关联的所有信息。

在这个示例中,我们将执行的第一个任务是获取与我们创建的 VPC 相关联的默认路由表的 ID。我们将使用ec2_vpc_route_table_facts模块来获取路由表的信息,并提供 VPC ID 来唯一标识 VPC。我们可以将默认路由表的 ID 存储在新变量rt_id中。

以下是我们从ec2_vpnc_facts模块中检索到的路由表信息的片段:

ok: [us_prod_vpc] => {
 "vpc_route_table_facts": {
 "route_tables": [
 {
 < -- Output Omitted for brevity --> ],
 "id": "rtb-0b6669ba5fd9eb9c8",
 "routes": [
 {
 "destination_cidr_block": "10.1.0.0/16",
 "gateway_id": "local",

< -- Output Omitted for brevity -->

 }
 ],
 "tags": {},
 "vpc_id": "vpc-005b1dcb981791d86"
 }
 ]
 }
} 

一旦我们有了与 VPC 关联的路由表的 ID,我们可以使用ec2_vpc_route_table模块来调整与 VPC 关联的默认路由表的路由表。我们必须提供 VPC 和路由表 ID 以唯一标识我们要修改的确切路由表。我们可以指定要在路由表中注入的路由和要与此路由表关联的子网。我们可以注入默认路由并将其指向我们在上一个配方中使用igw-id创建的 IGW。

在调整路由后,以下截图概述了我们 VPC 的路由表:

以下截图概述了我们在 VPC 中有的两个子网现在与此默认路由表关联:

另请参阅

有关与 AWS VPC 路由表交互的多个模块以及相关模块的更多信息,请使用以下链接:

使用 Ansible 部署网络 ACL

在本配方中,我们将概述如何在 AWS 上部署网络 ACLsNACLs)。NACLs 是 AWS 中可用的安全解决方案之一,用于保护在 AWS 云中部署的计算资源。在本配方中,我们将概述如何描述和自动化在 AWS 中部署 NACLs 的过程。

准备工作

Ansible 控制机必须具有互联网可达性,以便到达 AWS API 端点,并且 VPC 和子网必须根据先前的配方进行预配。

如何操作...

  1. 使用 NACL 定义数据更新eu_prod_vpc.yml文件,如下所示,并对us_prod_vpc.yml执行相同操作:
$ cat host_vars/eu_prod_vpc.yml

< -- Output Omitted for brevity -->

network_acls:
 - name: EU_Prod_ACLs
 subnets: "{{ vpc_subnets.keys() | list }}"
 ingress_rules:
 - [100,'tcp','allow','0.0.0.0/0',null,null,80,80]
 - [200,'tcp','allow','0.0.0.0/0',null,null,443,443]
  1. 更新pb_aws_net_build.yml playbook,并填充以下任务以创建 NACLs:
- name: Create Network ACLs
 ec2_vpc_nacl:
 region: "{{ aws_region }}"
 vpc_id: "{{ vpc_id }}"
 name: "{{ item.name }}"
 subnets: "{{ item.subnets }}"
 ingress: "{{ item.ingress_rules }}"
 tags: "{{ vpc_tags | combine({'Name':item.name}) }}"
 loop: "{{ network_acls }}"

工作原理...

AWS NACL 是有能力基于 L3 和 L4 IP 地址信息允许或拒绝 IP 流量的无状态 ACL。它们在子网级别执行,并与子网关联以保护在子网上预配的所有资源。它们可以阻止入口(进入子网的流量)或出口(离开子网的流量)方向的流量。NACL 中的规则是根据规则编号进行处理的,因此第一个匹配的规则将应用于流量流向。

所有子网都附加了默认 NACL,并且 AWS 为默认 NACL 设置了以下规则:

  • 在入口处,允许所有流量。以下截图概述了应用于默认 NACL 的规则:

  • 在出口处,允许所有流量。以下截图概述了应用于默认 NACL 的规则:

在我们的示例设置中,我们将在所有子网上应用 NACL,强制执行以下安全策略:

  • 必须允许所有 TCP 流量到端口80443

  • 任何其他流量应该被丢弃。

默认情况下,任何 NACL 的末尾都有一个DENY规则,会丢弃所有流量。

我们定义了network_acls数据结构,其中包含 NACL 定义和设置所有所需字段以在我们的 EU 和 US 地区的所有子网上设置所需的 NACLs。在此数据结构中,我们需要定义以下参数:

  • Name:这是 NACL 的名称,它作为标识符。

  • Subnets:这定义了应与此 NACL 关联的子网。我们使用我们的vpc_subnets定义中的数据来构建此列表。

  • Ingress_rules:这定义了应作为此 NACL 的一部分应用的所有规则,方向为入站。

  • Engress_rules:这定义了应作为此 NACL 的一部分应用的所有规则,方向为出站。

我们可以在 playbook 中创建一个新任务,使用ec2_net_nacl来配置 NACL 并将其附加到我们所有的子网。

以下截图概述了在EU_prod VPC 中部署的新 NACL:

以下截图概述了与我们在EU_prod VPC 中的 NACL 相关联的子网:

另请参阅

有关ec2_net_nacl Ansible 模块以及此模块支持的不同参数的更多信息,请参阅以下 URL:

docs.ansible.com/ansible/latest/modules/ec2_vpc_nacl_module.html

使用 Ansible 进行部署验证

在这个配方中,我们将概述如何收集 AWS 中不同网络组件的操作状态,例如 VPC 和子网,并检查我们的部署是否按照我们的设计实施。

准备工作

Ansible 控制机必须具有互联网可达性,并且我们在先前的配方中概述的所有网络组件都应该就位。

如何做...

  1. 创建一个新的pb_vpc_validate.yml playbook,并填充它以验证 VPC 的构建任务:
$ cat pb_vpc_validate.yml

- name: Validate VPC Build
 hosts: all
 gather_facts: no
 environment:
 AWS_ACCESS_KEY: "{{ aws_access_key }}"
 AWS_SECRET_KEY: "{{ aws_secret_key_id }}"
 AWS_REGION: "{{ aws_region }}"
 tasks:
 - name: Get VPC facts
 ec2_vpc_net_facts:
 filters:
 "tag:Name": "{{ vpc_name }}"
 register: vpc_facts

    - name: Validate VPC Info
 assert:
 that:
 - vpc_facts.vpcs[0].cidr_block == vpc_cidr
 - vpc_facts.vpcs[0].tags.Name == vpc_name
 when: vpc_facts.vpcs != []
  1. 使用以下任务更新 playbook 以收集 AWS 子网的信息:
 - name: Extract VPC ID
 set_fact:
 vpc_id: "{{ vpc_facts.vpcs[0].id }}"

 - name: Get Subnet facts
 ec2_vpc_subnet_facts:
 filters:
 vpc-id: "{{ vpc_id }}"
 register: vpc_subnet_facts
 tags: subnet
  1. 使用以下任务更新 playbook 以验证 AWS 子网的状态:
 - name: Validate VPC Subnets Info
 assert:
 that:
 - vpc_subnet_facts.subnets |
 selectattr('tags.Name','equalto',item.key) |
 map(attribute='cidr_block') |
 list | first == item.value.cidr

 - vpc_subnet_facts.subnets |
 selectattr('tags.Name','equalto',item.key) |
 map(attribute='availability_zone') |
 list | first == item.value.az

 with_dict: "{{ vpc_subnets }}"

工作原理...

我们可以创建一个新的 playbook,使用ec2_vpc_net_factsec2_vpc_subnet_facts Ansible 模块来收集 VPC 和子网的信息。我们可以收集这些模块返回的数据,并使用assert模块来验证状态,如下所示:

  1. VPCs:
  • 检查为 VPC 分配的名称是否按照我们的设计进行了配置。

  • 检查为 VPC 分配的 CIDR 块是否按照我们的设计部署。

  1. 子网:
  • 检查为子网分配的 CIDR 是否正确配置。

  • 检查子网是否在正确的可用区中进行了配置。

我们可以通过将事实模块返回的操作状态与我们在group_varshost_vars变量中为每个 VPC 定义的元数据进行比较,执行所有前述验证。

ec2_vpc_net_facts任务中,我们使用filters参数仅基于其Name标签选择我们的 VPC。默认情况下,此模块将返回此区域内所有 VPC 的信息。

ec2_vpc_subnet_facts任务中,我们使用filters参数仅检索我们的 VPC 的子网数据,因为默认情况下,此模块将返回此区域内所有 VPC 的所有子网信息。

另请参阅

有关 AWS 中不同网络资源的事实收集模块的更多信息,请使用以下链接:

使用 Ansible 在 AWS 上解除资源

在这个配方中,我们将概述如何在 AWS 中解除完整的网络及其所有相关网络资源。这概述了我们如何可以使用 Ansible 轻松构建和拆除云上的资源,只需执行一个简单的 playbook。

准备工作

Ansible 控制机必须具有互联网可达性,并且我们在前面的配方中概述的所有网络组件都应该就位。

如何做...

  1. 创建一个新的pb_delete_vpc.yml playbook,包括以下任务来收集 VPC 的事实:
$ cat pb_delete_vpc.yml

- name: Delete all VPC resources
 hosts: all
 gather_facts: no
 environment:
 AWS_ACCESS_KEY: "{{ aws_access_key }}"
 AWS_SECRET_KEY: "{{ aws_secret_key_id }}"
 AWS_REGION: "{{ aws_region }}"
 tasks:
 - name: Get VPC facts
 ec2_vpc_net_facts:
 filters:
 "tag:Name": "{{ vpc_name }}"
 register: vpc_facts

 - name: Extract VPC ID
 set_fact:
 vpc_id: "{{ vpc_facts.vpcs[0].id }}"
  1. 更新 playbook,添加以下任务以删除 VPC 内的所有子网和 IGW 节点:
 - name: Start Delete VPC Resources
 block:
 - name: Delete Subnets
 ec2_vpc_subnet:
 cidr: "{{ item.value.cidr }}"
 vpc_id: "{{ vpc_id }}"
 state: absent
 with_dict: "{{ vpc_subnets }}" - name: Delete IGW
 ec2_vpc_igw:
 vpc_id: "{{ vpc_id }}"
 state: absent
  1. 更新 playbook,添加以下任务以删除所有 NACLs:
 - name: Delete NACLs
 ec2_vpc_nacl:
 name: "{{ item.name }}"
 vpc_id: "{{ vpc_id }}"
 state: absent
 loop: "{{ network_acls }}"
  1. 更新 playbook,添加最终任务以删除所有 VPCs:
 - name: Delete VPC
 ec2_vpc_net:
 cidr_block: "{{ vpc_cidr }}"
 name: "{{ vpc_name }}"
 state: absent
 when: vpc_id is defined

工作原理...

我们可以从收集 VPC 事实开始我们的新 playbook,以获取部署的 VPC 的 VPC ID。一旦我们获得了这些信息,我们就可以开始删除资源。然而,删除资源的顺序很重要。我们需要先删除任何依赖资源,因此我们必须先删除子网,然后才能删除 VPC。例如,如果有 EC2 实例连接到子网,我们必须先删除这些 EC2 实例,然后才能删除子网。因此,在我们的情况下,我们需要先删除子网,然后是 IGW 节点,最后才是删除 VPC。

在所有这些任务中,我们使用的是前面配方中概述的完全相同的模块。唯一的变化是我们将状态设置为不存在,并且我们提供所需的 VPC ID 来唯一标识我们需要从中删除所需资源的 VPC。

最后,当我们开始删除 VPC 内的资源时,我们首先验证是否存在 VPC ID。如果资源已经被删除并且我们再次运行 playbook,删除步骤将被跳过,因为facts任务不会检索到 VPC ID。

第八章:使用 Ansible 部署和操作 Azure 网络资源

在上一章中,我们探讨了如何在 AWS 云上提供网络资源以及如何使用 Ansible 作为编排引擎在 AWS 上部署这些资源。在本章中,我们将看看另一个主要的云提供商,微软及其 Azure 云服务。

Azure 在 Azure 云上提供多个网络服务,以便在 Azure 云上部署高度可扩展的云解决方案。Ansible 提供多个模块,用于与 Azure 云中的多个服务进行交互,并且是在 Azure 云上自动化部署的优秀工具。我们将探讨 Azure 中可用的基本网络构造,并概述如何使用 Ansible 中的多个模块来构建和验证 Azure 云中以下基本网络设置:

本章涵盖的主要配方如下:

  • 安装 Azure SDK

  • 构建 Ansible 清单

  • 验证您的 Azure 帐户

  • 创建资源组

  • 创建虚拟网络

  • 创建子网

  • 构建用户定义的路由

  • 部署网络安全组

  • 使用 Ansible 进行部署验证

  • 使用 Ansible 停用 Azure 资源

技术要求

要开始使用 Azure,您需要创建一个帐户。您可以在Azure.microsoft.com/en-au/free/.上设置一个免费帐户。

以下链接是本章中使用的 GitHub 代码:

github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch8_azure

本章基于的软件版本如下:

  • 运行 CentOS 7 的 Ansible 机器

  • Ansible 2.9

  • Python 3.6.8

查看以下视频,了解代码的实际操作:

bit.ly/3esy3fS

安装 Azure SDK

在这个配方中,我们将概述如何安装所需的 Python 库,以开始使用 Ansible 与 Azure 编排系统进行交互。这一步是强制性的,因为这些 Python 库必须安装在 Ansible 控制机器上,以便所有 Ansible Azure 模块正常工作。

准备工作

您需要在机器上拥有 sudo 访问权限,以安装 Azure Python 库。您还需要安装 Python,并使用 Python PIP 软件包来安装 Azure 软件包。

操作步骤…

  1. 安装boto3软件包,如下所示:
$ sudo pip3 install 'Ansible[Azure]'
  1. 创建一个名为ch8_Azure的新文件夹,以托管本章的所有代码:
$ mkdir ch8_Azure

工作原理…

Ansible 的默认安装不包括运行 Ansible Azure 模块所需的所有 Python 模块。这就是为什么我们的第一步是安装所需的 Python 库。我们使用 Python pip 程序安装所有这些软件包。我们可以使用以下代码验证所有 Azure 模块是否已安装:

$ pip3 list | grep Azure

Azure-cli-core                 2.0.35

Azure-cli-nspkg                3.0.2

Azure-common                   1.1.11

Azure-graphrbac                0.40.0

Azure-keyvault                 1.0.0a1

 <<  ---  Output Omitted for brevity  -- >>

如前所述,需要安装多个 Python 软件包才能开始使用 Ansible 与 Azure API 进行交互。完成这一步后,我们现在已经准备好在 Azure 中构建我们的 playbooks 和基础设施了。

另请参阅…

有关如何开始使用 Ansible 与 Azure 云进行交互的更多信息,请参阅以下链接:

docs.Ansible.com/Ansible/latest/scenario_guides/guide_Azure.html

构建 Ansible 清单

在这个配方中,我们将概述如何构建一个 Ansible 清单,以描述我们将在 Azure 公共云中构建的网络基础设施设置。这是一个必要的步骤,因为我们将定义我们将在其中部署基础设施的所有地区的所有虚拟网络。

操作步骤…

  1. ch8_Azure目录中创建hosts文件,并填入以下数据:
$ cat hosts

[az_net]

eu_az_net

us_az_net

[eu]

eu_az_net

[us]

us_az_net
  1. 创建包含以下内容的Ansible.cfg文件:
$ cat Ansible.cfg

[defaults]

inventory=hosts

retry_files_enabled=False

gathering=explicit

host_key_checking=False

action_warnings=False
  1. 创建group_var文件夹和eu.ymlus.yml文件,其中包含以下代码:
$ cat group_var/eu.yml

---

region: westeurope

$ cat group_var/us.yml

---

region: eastus 

工作原理…

我们创建了主机的 Ansible 清单文件,并声明了我们将在 Azure 云中提供的不同虚拟网络。我们还创建了两个描述每个虚拟网络位置的组。

简而言之,我们创建了以下组来定义和分组我们的虚拟网络:

  • az_net:这将对我们在 Azure 云中的所有虚拟网络进行分组。

  • eu:列出欧盟地区的所有虚拟网络(将映射到 Azure 云中的特定区域,我们稍后将概述)。

  • us:列出美国地区的所有虚拟网络(将映射到 Azure 云中的特定区域,我们稍后将概述)。

我们可以使用此区域分组来指定在 Azure 云中使用此虚拟网络的确切区域。我们可以在eu.ymlus.yml文件的group_vars目录下定义名为region的变量来声明确切的区域。

我们将在随后的教程中使用此变量,以在相应的 Azure 区域部署我们的资源。

身份验证到您的 Azure 帐户

在本教程中,我们将概述如何创建所需的凭据,以便从 Ansible 对我们的 Azure 帐户进行编程身份验证。我们还将学习如何使用 Ansible Vault 来保护这些凭据。为了能够在以下教程中运行任何 Ansible 模块,这一步是必需的。

准备工作

Ansible 控制器必须具有互联网访问权限,并且必须按照上一个教程中的说明设置 Ansible 清单。执行这些步骤的用户必须具有 Azure 门户的管理访问权限,以便能够创建所需的资源,从而实现与 Azure API 的程序交互。

如何操作…

  1. 使用具有管理权限的帐户登录 Azure 门户:

portal.Azure.com/

  1. 在主页上,选择 Azure 活动目录:

  1. 从左侧面板中选择应用程序注册:

  1. 单击“新注册”选项,并提供以下信息以创建新应用程序。蓝色突出显示的选项是此处的活动选项:

  1. 单击注册按钮后,将创建新应用程序并显示其信息,如下截图所示(我们需要 client_id 和 tenant_id 数据):

  1. 在左侧面板中选择证书和密码:

  1. 单击“新客户端密码”:

  1. 为此应用程序指定密码名称,并选择其到期日期:

  1. 创建后,记下显示的秘密字符串(这是我们能够以纯文本形式看到此密码的唯一时间):

  1. 转到所有服务并选择订阅:

  1. 单击订阅名称(在我这里是免费试用):

  1. 记录订阅 ID 字符串(因为我们需要它进行身份验证),然后在左侧单击访问控制(IAM)选项卡:

  1. 单击“添加角色分配”,并将“参与者”角色分配给我们创建的 Ansible 应用程序:

  1. 在 Ansible 控制节点上,创建一个新文件,用于保存我们的 Ansible Vault 密码:
$ echo ‘AzureV@uLT2019’ > .vault_pass
  1. 使用 Ansible Vault 创建一个名为Azure_secret.yml的新文件,如下所示:
$ Ansible-vault create Azure_secret.yml --vault-password-file=.vault_pass
  1. 填充Azure_secret.yml文件,使用我们从 Azure 门户获取的client_idtenant_idsubscription_id的数据,以及我们为应用程序创建的密钥:
---

tenant_id: XXX-XXXXXXXX

client_id: XXX-XXXX

subscription_id: XXX-XXXXX

secret: XXX-XXXX

工作原理…

为了能够以编程方式访问 Azure API(这是 Ansible 与 Azure 云通信以提供资源的方式),我们需要在 Azure 帐户中创建一个称为服务主体的构造。这个服务主体类似于用户,但只能访问 Azure 帐户的 API。我们可以创建这个服务主体并将其称为 Ansible。然后我们可以在访问管理中为其分配贡献者角色,以便能够在我们的帐户中创建资源。为了使用这个服务主体对 Azure API 进行身份验证,我们需要提供以下四个组件:

  • Client_id

  • Tenant_id

  • Subscription_id

  • 服务主体密码

我们可以使用本步骤中概述的步骤在 Azure 门户中找到所有这些信息。我们可以创建一个名为Azure_secrets.yml的新文件,使用 Ansible Vault 进行加密,并将所有前述变量放入该文件中。

我们将在所有后续步骤中使用这些参数来对我们的 Azure 帐户进行身份验证并创建所需的基础设施。

另请参阅…

有关如何创建新服务主体的更多信息,请使用以下 URL:

docs.microsoft.com/en-au/Azure/active-directory/develop/howto-create-service-principal-portal

有关可以分配给用户/应用程序的 Azure 内置角色的更多信息,请使用以下 URL:

docs.microsoft.com/en-au/Azure/role-based-access-control/built-in-roles

创建资源组

在这个步骤中,我们将概述如何在 Azure 中部署资源组。资源组是 Azure 资源管理器部署模型的一部分,这是在 Azure 云中部署资源的首选方法。这是因为它允许我们将相似的资源(如 VM、VM NIC 和 VM IP 地址)在一个单一容器中进行分组,这个容器就是资源组。我们将使用资源组来部署所有相关资源。

准备工作

Ansible 控制机必须连接到互联网,并能够访问 Azure 公共 API 端点。Azure 帐户应按照前面的步骤进行配置。

操作步骤…

  1. 更新group_vars下的eu.ymlus.yml文件,使用以下数据定义资源组的名称:
$ cat group_vars/eu.yml

rg_name: "rg_{{ inventory_hostname }}"

$ cat group_vars/eu.yml

rg_name: "rg_{{ inventory_hostname }}"
  1. 创建一个名为pb_build_Azure_net.yml的新 playbook,内容如下:
---
- name: Build Azure Network Infrastructure
 hosts: all
 connection: local
 vars_files:
 - Azure_secret.yml
 tasks:
 - name: Create Resource group
 Azure_rm_resourcegroup:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 location: "{{ region }}"
 subscription_id: "{{ subscription_id }}"
 name: "{{ rg_name }}"
 state: "{{ state | default('present') }}"

工作原理…

我们在描述每个区域的 YAML 文件中声明我们将在每个区域部署的资源组的名称。我们使用rg_name参数来保存资源组的名称。我们使用Azure_rm_resourcegroup Ansible 模块来在 Azure 上创建资源组。它需要以下参数来对 Azure API 进行身份验证并部署资源组:

  • location参数,描述我们将部署此资源组的区域

  • tenantsecretclient_idsubscription_id参数,用于对我们的 Azure 帐户进行身份验证

  • name参数,即我们的资源组的名称

在我们的 playbook 中,我们使用vars_files参数读取Azure_secrets.yml文件,以捕获该文件中存储的所有参数。我们将连接设置为local,以指示 Ansible 在 Ansible 控制机上本地运行 playbook,并且不尝试 SSH 到清单中定义的主机。这是强制性的,因为所有 Azure 模块都需要从 Ansible 控制机运行,以调用 Azure 编排系统的 REST API 调用。

运行 playbook 后,我们可以在 Azure 门户上看到资源组已经配置好,如下截图所示:

另请参阅...

有关 Ansible 中 Azure 资源模块的更多信息,以及此模块支持的所有其他参数,请使用以下 URL:

docs.Ansible.com/Ansible/latest/modules/Azure_rm_resourcegroup_module.html

创建虚拟网络

Azure 云中的虚拟网络是我们的虚拟数据中心,类似于物理数据中心,它将我们的所有基础设施分组在一起。我们可以在相同区域和不同区域中拥有多个虚拟网络,并且我们可以在这些虚拟网络中部署我们的基础设施。在这个配方中,我们将概述如何在 Azure 中定义和配置虚拟网络。

准备工作

Ansible 控制机必须连接到互联网,能够访问 Azure 公共 API 端点,并且 Azure 账户应该按照前面的配方进行配置。资源组也应该按照前面的配方进行配置。

如何做...

  1. 使用group_vars下的eu.ymlus.yml文件更新虚拟网络的名称和 CIDR 地址:
$ cat group_vars/eu.yml
vnet_name: "vn_{{ inventory_hostname }}"
vnet_cidr: 10.1.0.0/16
$ cat group_vars/us.yml
vnet_name: "vn_{{ inventory_hostname }}"
vnet_cidr: 10.2.0.0/16
  1. 使用任务更新pb_build_Azure_net.yml playbook 来创建虚拟网络:
    - name: Create Virtual Networks
 Azure_rm_virtualnetwork:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 location: "{{ region }}"
 subscription_id: "{{ subscription_id }}"
 resource_group: "{{ rg_name}}"
 name: "{{ vnet_name }}"
 address_prefixes_cidr: "{{ vnet_cidr }}"
 state: "{{ state | default('present') }}"

工作原理...

为了创建虚拟网络,我们需要提供其名称,以及此虚拟网络将占用的 CIDR IP 范围。我们在区域的 YAML 文件中定义这两个参数为vnet_namevnet_cidr。我们使用Azure_rm_virtualnetwork Ansible 模块来创建所有必需的虚拟网络,并提供以下参数:

  • resource_group中的资源组名称。

  • location参数描述了我们将部署此资源组的区域。

  • name参数中的每个子网的名称,以及address_prefixes_cidr参数中的 CIDR IP 范围。

  • “租户”、“密钥”、“客户端 ID”和“订阅 ID”参数都用于对我们的 Azure 账户进行身份验证。

运行 playbook 后,我们可以看到虚拟网络已经创建,如下截图所示:

另请参阅...

有关 Ansible 中 Azure 虚拟网络模块的更多信息,以及此模块支持的所有其他参数,请使用以下 URL:

docs.Ansible.com/Ansible/latest/modules/Azure_rm_virtualnetwork_module.html

创建子网

子网是 Azure 云中用于对虚拟网络进行分段的网络构造。它用于为我们提供工具,将我们的虚拟网络分隔成不同的路由和安全域,以便我们可以控制每个子网内的不同路由和安全行为。在这个配方中,我们将概述如何在 Azure 云中定义和配置子网。

准备工作

Ansible 控制机必须连接到互联网,能够访问 Azure 公共 API 端点。Azure 账户应该按照前面的配方进行配置。资源组和虚拟网络也应该按照前面的配方进行配置。

如何做...

  1. 使用子网信息更新group_vars下的eu.ymlus.yml文件:
$ cat group_vars/eu.yml
subnets:
 - name: web_tier
 cidr: 10.1.1.0/24
 - name: db_tier
 cidr: 10.1.2.0/24

$ cat group_vars/us.yml
subnets:
 - name: web_tier
 cidr: 10.2.1.0/24
 - name: db_tier
 cidr: 10.2.2.0/24    
  1. 使用任务更新pb_build_Azure_net.yml playbook 来创建子网:
 - name: Create Subnets
 Azure_rm_subnet:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 subscription_id: "{{ subscription_id }}"
 resource_group: "{{ rg_name}}"
 name: "{{ item.name}}"
 virtual_network_name:  "{{ vnet_name }}"
 address_prefix_cidr: "{{ item.cidr }}"
 state: "{{ state | default('present') }}"
 loop: "{{ subnets }}"
 loop_control:
 label: "{{ item.name }}"

工作原理...

为了在虚拟网络中创建子网,我们需要提供虚拟网络和子网的 CIDR 前缀,该前缀必须在虚拟网络的 CIDR 内。我们在子网的数据结构中定义这些内容,其中包括我们想要配置的每个子网的名称和 CIDR。我们可以使用Azure_rm_subnet Ansible 模块来创建所有必需的子网,并且可以循环遍历子网的数据结构以提供必需的参数。

运行 playbook 后,我们可以看到在每个虚拟网络中创建的子网,如下截图所示:

另请参阅...

有关 Ansible 中 Azure 子网模块的更多信息,以及此模块支持的所有其他参数,请使用以下 URL:

docs.Ansible.com/Ansible/latest/modules/Azure_rm_subnet_module.html

构建用户定义的路由

在这个配方中,我们将概述如何使用用户定义的路由器控制子网内的路由。这个用户定义的路由对象可以与特定的子网关联。我们可以定义自定义路由来调整 Azure 云中子网内的转发行为。

准备工作

Ansible 控制机必须连接到互联网,并能够访问 Azure 公共 API 端点。Azure 帐户应按照前面的配方进行配置。资源组、虚拟网络和子网也应按照前面的配方进行配置。

如何做...

  1. group_vars下的eu.ymlus.yml文件中更新route_tables数据,如下所示:
$ cat group_vars/eu.yml  group_vars/us.yml
route_tables:
 - name: db_tier_rt
 subnet: db_tier
 routes:
 - name: Default Route
 prefix: 0.0.0.0/0
 nh: none
  1. 使用以下任务更新pb_build_Azure_net.yml playbook 以创建自定义路由表:
 - name: Create Custom Route Table
 Azure_rm_routetable:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 subscription_id: "{{ subscription_id }}"
 resource_group: "{{ rg_name}}"
 name: "{{ item.name}}"
 state: "{{ state | default('present') }}"
 loop: "{{ route_tables }}"
 tags: routing
  1. 使用以下任务更新 playbook 以在自定义路由表中配置路由:
 - name: Provision Routes
 Azure_rm_route:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 subscription_id: "{{ subscription_id }}"
 resource_group: "{{ rg_name}}"
 route_table_name: "{{ item.0.name }}"
 name: "{{ item.1.name}}"
 address_prefix: "{{ item.1.prefix }}"
 next_hop_type: "{{ item.1.nh }}"
 state: "{{ state | default('present') }}"
 with_subelements:
 - "{{ route_tables }}"
 - routes
 tags: routing
  1. 使用以下任务更新 playbook 以将自定义路由与子网关联:
 - name: Attach Route Table to Subnet
 Azure_rm_subnet:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 subscription_id: "{{ subscription_id }}"
 resource_group: "{{ rg_name}}"
 name: "{{ item.subnet}}"
 virtual_network_name:  "{{ vnet_name }}"
 route_table: "{{ item.name }}"
 state: "{{ state | default('present') }}"
 loop: "{{ route_tables }}"
 loop_control:
 label: "{{ item.name }}"
 tags: routing

工作原理...

在我们的设置中,我们有两个子网(webDB),我们需要为DB子网提供不同的路由处理,以便它不具有公共互联网访问权限。我们可以通过创建新的自定义路由表并安装next-hop设置为none的默认路由来实现这一点,以丢弃所有面向互联网的流量。

我们需要在route_tables变量中定义我们的自定义路由表,并将其包含在每个区域定义中。然后,我们可以使用Azure_rm_routetable Ansible 模块在特定资源组中创建路由表,并使用Azure_rm_route模块在每个路由表中创建所需的路由。最后,我们可以使用Azure_rm_subnet模块将路由表附加到特定子网,以修改此子网的默认路由行为。

以下截图概述了创建的新路由表:

以下截图概述了一个路由表的确切细节,自定义路由以及此自定义路由所附加的子网:

另请参阅...

有关 Ansible 中 Azure 路由表模块的更多信息,以及此模块支持的所有其他参数,请使用以下 URL:

部署网络安全组

云环境中的安全性至关重要,Azure 云提供了不同的工具和服务来帮助构建应用程序的安全云环境。在这个示例中,我们将看一下其中一个服务:网络安全组NSG)。NSG 是一个有状态的防火墙,可以附加到虚拟机或子网,以限制通过虚拟机或子网流动的流量。在这个示例中,我们将概述如何在 Azure 云上定义和配置 NSG。

准备工作

Ansible 控制机必须连接到互联网,并能够到达 Azure 公共 API 端点。Azure 帐户应按照前面的示例进行配置。资源组、虚拟网络和子网也应按照前面的示例进行配置。

操作步骤...

  1. 使用 ACL 数据更新group_vars下的eu.ymlus.yml文件,如下所示:
$ cat group_vars/eu.yml  group_vars/us.yml
acls:
 - name: Inbound_Web_Tier
 subnet: web_tier
 rules:
 - name: Allow_HTTP_Internet
 destination_address_prefix: 10.1.1.0/24
 direction: Inbound
 access: Allow
 protocol: Tcp
 destination_port_range:
 - 80
 - 443
 priority: 101
  1. 使用以下任务更新pb_build_Azure_net.yml playbook,创建安全组并填充其所有规则:
 - name: Create new Security Group
 Azure_rm_securitygroup:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 subscription_id: "{{ subscription_id }}"
 resource_group: "{{ rg_name}}"
 name: "{{ item.name }}"
 purge_rules: yes
 rules: "{{ item.rules }}"
 loop: "{{ acls }}"
 Tags: security
  1. 使用以下任务更新 playbook,将安全组与相应的子网关联起来:
 - name: Attach Security Group to Subnet
 Azure_rm_subnet:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 subscription_id: "{{ subscription_id }}"
 resource_group: "{{ rg_name}}"
 name: "{{ item.subnet}}"
 virtual_network_name:  "{{ vnet_name }}"
 security_group: "{{ item.name }}"
 state: "{{ state | default('present') }}"
 loop: "{{ acls }}"
 tags: security

工作原理...

Azure 提供了附加到子网的默认 NSG。这些为部署在这些子网中的计算资源提供基本安全控制。入站流量的默认策略包括以下默认规则:

  • 允许虚拟网络 CIDR 范围之间的入站流量(子网间通信)。

  • 允许来自 Azure 负载均衡器的入站流量。

  • 拒绝其他任何流量。

在出站方向,默认规则如下:

  • 允许虚拟网络 CIDR 之间的出站流量(子网间通信)。

  • 允许出站流量到互联网。

  • 拒绝其他任何流量。

Azure NSG 提供了一种机制,通过定义一个自定义 NSG 来增强 Azure 应用的默认 NSG,该自定义 NSG 附加到默认 NSG 上。结果 NSG 根据每个规则的优先级值进行评估(具有较低值的规则首先进行评估),一旦匹配规则,规则就适用于通过子网传输的流量。

由于我们在Web_tier子网中部署了一个 Web 应用程序,我们需要允许入站 HTTP 和 HTTPs 流量到该子网。因此,我们可以创建一个 ACL 定义来创建一个自定义 NSG,并在入站方向上定义所需的参数,以允许这些流量。

我们可以使用Azure_rm_securitygroup Ansible 模块循环遍历所有自定义 ACL,并创建 NSG 和相应的规则。我们可以使用Azure_rm_subnet将安全组附加到子网。

以下截图显示了定义的新 NSG:

以下截图显示了定义的结果 NSG 规则(包括自定义和默认)的入站和出站方向:

另请参阅...

有关 Ansible 中 Azure NSG 模块的更多信息,以及此模块支持的所有其他参数,请使用以下 URL:

docs.Ansible.com/Ansible/latest/modules/Azure_rm_securitygroup_module.html

使用 Ansible 进行部署验证

Ansible 提供了多个模块来收集在 Azure 中部署的不同资源的操作状态。我们可以使用这些模块来验证 Azure 云中我们网络的当前状态。这提供了一种编程方法来验证部署,而无需通过 GUI 登录到门户网站来检查基础设施中不同组件的状态。在这个示例中,我们将概述如何使用多个模块来验证我们已部署的资源组和虚拟网络。

准备工作

Ansible 控制机必须连接到互联网,并能够访问 Azure 公共 API 端点。Azure 帐户还应按照前面的示例进行配置。

如何操作...

  1. 创建一个新文件~/.Azure/credentials,用于存储所有连接到 Azure 的凭据,如下所示:
$ cat ~/.Azure/credentials
[default]
subscription_id=XXX-XXXX-XXXX
client_id=XXX-XXXX-XXXX
secret=XXX-XXXX-XXXX
tenant=XXX-XXXX-XXXX
  1. 创建一个新的 playbookpb_validate_Azure_net.yml,以验证我们的部署,并包括以下任务来收集资源组事实并验证它:
$ cat pb_validate_Azure_net.yml
- name: Build Azure Network Infrastructure
 hosts: all
 connection: local
 tasks:
 - name: Get Resource Facts
 Azure_rm_resourcegroup_facts:
 name: "{{ rg_name }}"
 register: rg_facts
 tags: rg_facts
 - name: Validate Resource Group is Deployed
 assert:
 that:
 - rg.name == rg_name
 - rg.properties.provisioningState == 'Succeeded'
 - rg.location == region
 loop: "{{ Azure_resourcegroups }}"
 loop_control:
 loop_var: rg
 tags: rg_facts
  1. 更新pb_validate_Azure_net.yml playbook,包括收集虚拟网络事实并验证其状态的任务:
    - name: Validate Virtual Network is Deployed
      Azure_rm_virtualnetwork_facts:
        resource_group: "{{ rg_name }}"
      register: vnet_facts
      tags: vnet_facts
    - name: Validate Virtual Networks are Deployed
      assert:
        that:
          - vnet.name == vnet_name
          - vnet.properties.provisioningState == 'Succeeded'
          - vnet.properties.addressSpace.addressPrefixes | length == 1
          - vnet.properties.addressSpace.addressPrefixes[0] == vnet_cidr
      loop: "{{ Azure_virtualnetworks }}"
      loop_control:
      loop_var: vnet
      tags: vnet_facts

工作原理...

在这个示例中,我们概述了一种连接到 Azure 云的替代方法。我们创建了~/.Azure/credentials文件,并将需要连接到 Azure API(tenant_idclient_id等)的相同信息放入其中。由于我们在文件中有这些信息,我们不需要在我们的 Ansible 模块中包含这些参数。

为了验证我们的部署,Ansible 提供了多个事实模块,用于收集 Azure 云中多个对象的操作状态。在这个示例中,我们概述了两个用于收集资源组和虚拟网络事实的模块。我们可以使用Azure_rm_resourcegroup_facts模块收集资源组事实,使用Azure_rm_virtualnetwork_facts收集虚拟网络事实。所有 Azure 事实模块都将这些模块检索的数据注册为 Ansible 事实,这就是为什么我们不需要在自定义定义的变量中注册模块返回的数据。

Azure_rm_resourcegroup_facts模块将输出保存在Azure_resourcegroups Ansible 事实中,我们使用assert模块循环遍历此变量中的所有资源组。然后,我们可以确认它是否使用正确的参数创建。

以下是来自Azure_resourcegroups的片段:

ok: [eu_az_net] => {
 "Azure_resourcegroups": [
 {
 "id": "/subscriptions/bc20fdc0-70fa-46ef-9b80-3db8aa88a25c/resourceGroups/rg_eu_az_net",
 "location": "westeurope",
 "name": "rg_eu_az_net",
 "properties": {
 "provisioningState": "Succeeded"
 }
 }
 ]
}

我们可以使用完全相同的技术来收集使用Azure_rm_virtualnetwork_facts部署的虚拟网络的事实,并使用assert模块验证其状态。

另请参阅...

有关 Azure 中不同网络资源的多个模块的事实收集的更多信息,请使用以下链接:

使用 Ansible 取消 Azure 资源

与我们可以使用自动化大规模创建资源类似,一旦我们决定不再需要这些资源,我们也可以销毁这些资源。使用 Ansible 和 Azure 实施的资源组,这变得更加简化-通过一次正确参数的单个 API 调用,我们可以废弃我们定义的资源组中的所有资源。在本配方中,我们将概述如何执行此操作,以销毁到目前为止我们已经提供的所有资源。

准备工作

Ansible 控制机必须连接到互联网,并能够访问 Azure 公共 API 端点。Azure 帐户还应按照前面的配方进行配置。

如何操作...

  1. 创建一个新的pb_destroy_Azure_net.yml playbook,并添加以下任务以删除所有资源组:
$ cat pb_destroy_Azure_net.yml
---- name: Decomission Azure Infrastructure
 hosts: all
 connection: local
 vars:
 state: absent
 vars_files:
 - Azure_secret.yml
 tasks:
 - name: Delete Resource group
 Azure_rm_resourcegroup:
 tenant: "{{ tenant_id }}"
 client_id: "{{ client_id }}"
 secret: "{{ secret }}"
 location: "{{ region }}"
 subscription_id: "{{ subscription_id }}"
 name: "{{ rg_name }}"
 force_delete_nonempty: yes
 state: "{{ state | default('present') }}"

工作原理...

我们可以使用Azure_rm_resourcegroup Ansible 模块来销毁资源组中的所有资源,以及删除资源组本身。我们可以向模块提供两个重要的参数,以执行delete功能:

  • state设置为absent

  • 包括force_delete_nonempty参数,并将其设置为yes

设置了这些参数后,资源组中的所有资源(虚拟网络、子网等)将被删除,资源组本身也将被删除。

以下输出显示我们的两个资源组不再存在:

以下输出还确认了运行 playbook 后所有虚拟网络都已被删除:

上述截图显示所有虚拟网络已被删除。

第九章:使用 Ansible 部署和操作 GCP 网络资源

Google Cloud 是公共云中的重要参与者之一,它在其Google Cloud PlatformGCP)云上提供了一套全面的服务和功能。在本章中,我们将探讨如何使用 Ansible 自动化在 GCP 云上提供资源,并如何使用各种 Ansible 模块来编排在 GCP 云上构建虚拟网络。

在本章中,我们将使用一个简单的网络设置来说明在 GCP 上使用不同 Ansible 模块构建示例网络的方法。以下图表概述了我们将构建的示例网络:

本章将涵盖以下内容:

  • 安装 GCP SDK

  • 构建 Ansible 清单

  • 验证您的 GCP 账户

  • 创建 GCP VPC 网络

  • 创建子网

  • 在 GCP 中部署防火墙规则

  • 在 GCP 中部署虚拟机

  • 调整 VPC 内的路由

  • 使用 Ansible 验证 GCP 部署

  • 使用 Ansible 取消部署 GCP 资源

技术要求

为了开始使用 GCP,我们需要创建一个账户。您可以在cloud.google.com/free/上设置一个免费的 GCP 账户。

本章中使用的 GitHub 代码可以在github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch9_gcp找到。

本章基于以下软件版本:

  • CentOS 7

  • Ansible 2.9

  • Python 3.6.8

查看以下视频以查看代码的实际操作:

bit.ly/3erVlSN

安装 GCP SDK

在本教程中,我们将概述如何安装所需的 Python 库,以便开始使用 Ansible 与 GCP 编排系统进行交互。这一步是强制性的,因为必须在 Ansible 控制机器上安装所需的 Python 库,以便所有 Ansible GCP 模块正常工作。

准备工作

您需要在机器上拥有sudo访问权限才能安装 GCP Python 库。您还需要安装 Python 和 Python pip 包,我们将使用它来安装 GCP 包。

操作步骤...

  1. 按照以下代码安装requests包:
$ sudo pip3 install requests
  1. 按照以下代码安装 Google 认证包:
$ sudo pip3 install google-auth
  1. 创建一个名为ch9_gcp的新文件夹,用于存放本章的所有代码:
$ mkdir ch9_gcp

工作原理...

默认安装的 Ansible 不包括执行 GCP 云模块所需的所有必需的 Python 模块。在本教程中,我们安装了所有 GCP 模块所需的两个 Python 包。第一个包是requests包,主要用于调用 Google 编排系统的 REST API 调用,另一个包是google-auth包,用于对 API 进行身份验证。

另请参阅...

有关如何使用 Ansible 开始与 GCP 进行交互的更多信息,请参阅docs.ansible.com/ansible/latest/scenario_guides/guide_gce.html

构建 Ansible 清单

在本教程中,我们将概述如何构建一个 Ansible 清单,以描述我们将在 GCP 公共云中构建的网络基础架构。这是一个强制性的步骤,我们需要采取这一步骤来定义我们在其中部署基础架构的所有地区的所有 VPC 网络。

操作步骤...

  1. ch9_gcp目录中创建hosts文件,并填入以下数据:
$ cat hosts

[gcp_vpc]
demo_gcp_vpc
  1. 创建ansible.cfg文件,并填入以下内容:
$ cat ansible.cfg

[defaults]
inventory=hosts
retry_files_enabled=False
gathering=explicit
host_key_checking=False
action_warnings=False
  1. 创建group_vars文件夹和gcp_vpc.yml,其中将包含定义我们在此 VPC 中的基础架构的所有变量:
$ mkdir -p group_var/gcp_vpc.yml
  1. 在我们的主文件夹(ch9_gcp)中创建roles目录。该文件夹将包括我们用来创建 GCP 基础架构的所有角色:
$ mkdir -p roles

工作原理...

我们创建了hosts Ansible 清单文件,并声明了我们将在 GCP 云中配置的所有 VPC。在我们的示例设置中,我们有一个单独的 VPC,所以我们创建了一个名为gcp_vpc的单一组,其中包括我们的 VPC(demo_gcp_vpc)。

我们创建了group_vars/gcp_vpc.yml文件,其中将包含我们在此 VPC 中定义基础架构的所有变量。

此时,我们的目录布局如下:

$ tree ch9_gcp
 .
 ├── ansible.cfg
 ├── group_vars
 │ └── gcp_vpc.yml
 ├── hosts
 └── roles

对 GCP 帐户进行身份验证

在本教程中,我们将概述如何创建所需的凭据,以便从 Ansible 对我们的 GCP 帐户进行编程身份验证。这是您需要采取的强制步骤,以便能够在以下教程中运行任何 Ansible 模块。

准备工作

Ansible 控制器必须具有互联网访问权限。此外,执行这些步骤的用户必须具有 GCP 控制台的管理员访问权限,以便创建所需的资源以启用与 GCP API 的编程交互。

如何做...

  1. 使用管理员帐户登录到 GCP 控制台。

  2. 从主控制台中,选择 IAM & admin | Manage Resources。在 GCP 中创建一个新项目,该项目将容纳我们在 GCP 中构建的所有基础设施:

  1. 从主控制台转到 IAM & admin | Service accounts:

  1. 为新的 Ansible 用户创建一个新的服务帐户:

  1. 为这个新的服务帐户分配适当的角色,以便您可以在此 GCP 项目中创建/编辑/删除资源:

  1. 创建并下载将用于对此用户进行身份验证的私钥:

  1. 将下载的 JSON 密钥文件复制到项目目录ch9_gcp并将其重命名为gcp_ansible_secret.json

  2. 在 GCP 控制台上,选择 API & Services 并为当前项目启用 Google Compute Engine API:

它是如何工作的...

为了能够以编程方式访问 GCP API(这是 Ansible 与 GCP 云通信以配置资源的方式),我们需要在我们的 GCP 项目中创建一个称为服务帐户的特殊帐户。此服务帐户类似于用户,但只能访问 GCP 项目的 API。我们创建了此服务帐户并将其称为 Ansible,并为其分配了项目所有者角色,以便在 GCP 项目中具有创建资源的全部权限(在生产环境中,应为此服务帐户分配更严格的角色)。

为了使用此服务帐户对 GCP API 进行身份验证,GCP 为我们提供了一个 JSON 文件,其中包含此帐户的身份信息。此 JSON 文件中包含的主要参数如下:

  • 此服务帐户的私钥

  • 此帐户的类型

  • Project_id

  • Client_id

  • client_email

我们保存这个 JSON 文件并将其复制到我们的目录中,因为我们将在所有的 playbooks 中引用它,以便在 GCP 云上配置资源。最后一步是在我们的 GCP 项目中启用 API;我们需要启用 GCP 计算引擎 API,以便开始与此 API 进行交互,因为默认情况下,在 GCP 项目中 API 访问是被禁用的。

还有更多...

保存所有身份验证信息以对 GCP API 进行身份验证的 JSON 文件是一个关键文件,应该进行安全保护,因此我们将使用 Ansible vault 来保护此文件。

我们创建了一个名为vault_pass的新文件,其中包含我们的 Ansible vault 密码,并更新我们的ansible.cfg文件以指向它,如下面的代码所示:

$ cat ansible.cfg

[defaults]
 vault_password_file=vault_pass

我们使用 Ansible vault 加密 JSON 文件,如下面的代码所示:

$ ansible-vault encrypt gcp-ansible-secret.json

在这个阶段,我们的 JSON 文件是安全的,所有内容都使用vault_pass文件中声明的密码进行加密。

另请参阅...

有关如何在 GCP 中创建新服务账号的更多信息,请访问cloud.google.com/iam/docs/creating-managing-service-accounts

创建 GCP VPC 网络

在 GCP 中,VPC 是用于对所有资源进行分组的主要网络构造。我们可以将它们视为云中的虚拟数据中心。我们需要定义我们的 VPC,以准备好我们的云环境来托管我们的应用程序。在这个示例中,我们将概述如何在 GCP 中定义和配置 VPC。

准备工作

Ansible 控制机必须连接到具有对 GCP 公共 API 端点的可达性的互联网,并且 GCP 帐户应按照前面的示例进行配置。

操作步骤...

  1. 创建一个名为gcp_account_info.yml的新的 YAML 文件,并包含我们的 GCP 登录参数的以下数据:
$ cat gcp_account_info.yml
 ---
 service_account_file: gcp_credentials.json
 project: "gcp-ansible-demo"
 auth_kind: serviceaccount
  1. 创建一个名为gcp_net_build的新 Ansible 角色,如下所示:
$ cd roles
$ ansible-galaxy init gcp_net_build
  1. 更新gcp_net_build/tasks/main.yml文件,添加以下任务以创建我们的 VPC:
- name: Create a New GCP VPC
 gcp_compute_network:
 name: "{{ vpc_name | regex_replace('_','-') }}"
 routing_config:
 routing_mode: "REGIONAL"
 auto_create_subnetworks: no
 state: present
 auth_kind: "{{ auth_kind }}"
 project: "{{ project }}"
 service_account_file: "{{ service_account_file }}"
 register: gcp_vpc
 tags: gcp_vpc
  1. 创建group_vars文件夹,并为 VPC 创建gcp_vpc.yml文件,包括以下数据:
$ cat group_vars/gcp_vpc.yml
 ---
 vpc_name: ansible-demo-vpc
  1. 创建pb_gcp_env_build.yml剧本,包括以下任务以读取保险库加密的 JSON 文件:
---
- name: Build GCP Environment
 hosts: all
 connection: local
 gather_facts: no
 force_handlers: true
 vars_files:
 - gcp_account_info.yml
 tasks:
 - name: Read the Vault Encrypted JSON File
 copy:
 content: "{{ lookup('file','gcp-ansible-secret.json') }}"
 dest: "{{ service_account_file }}"
 notify: Clean tmp Decrypted Files
 tags: always
  1. 使用以下任务更新pb_gcp_env_build.yml剧本以创建所需的 VPC:
 - name: Build GCP Network
 import_role:
 name: gcp_net_build
 tags: gcp_net_build
  1. 更新剧本,包括以下处理程序以删除临时 JSON 凭据文件,如下所示:
 handlers:
 - name: Clean tmp Decrypted Files
 file:
 path: "{{ service_account_file }}"
 state: absent

工作原理...

在这个示例中,我们在之前创建的项目中创建并部署了 GCP VPC。我们使用了 Ansible 角色来构建 GCP 网络的所有组件,第一个任务是使用 Ansible 模块gcp_compute_network创建 VPC。

为了使用任何 Ansible GCP 模块,我们需要对每个模块触发的每个 API 调用进行身份验证,并且我们需要提供以下信息以验证 API 调用:

  • Auth_kind:身份验证类型—在我们的情况下是serviceaccount

  • Project:这是我们创建的当前项目的项目名称。

  • Service_account_file:这是我们创建服务账号时下载的 JSON 文件。

由于我们使用 Ansible vault 来加密保存所有身份验证信息的 JSON 文件的所有内容,因此我们需要在剧本执行期间解密此文件以使用该文件中的数据。此外,由于我们不直接读取此 JSON 文件的内容,而是使用所有 GCP Ansible 模块中的serivce_account_file参数指向它,我们创建了一个任务来使用lookup模块读取此 JSON 文件的内容,并将这些数据存储在临时文件中。通过这种方法,我们可以读取此 JSON 文件中的加密数据,并创建一个新的临时 JSON 文件,其中包含明文数据。我们还可以使用此临时 JSON 文件作为service_account_file的输入。我们使用了一个处理程序任务来在播放结束时删除此临时文件。在播放级别上,我们使用了force_handlers来确保运行处理程序部分内的所有任务,即使我们播放中的任何任务失败。这意味着我们确保保存我们的凭据的明文 JSON 文件始终被删除。

我们将所有前述参数分组并放入gcp_account_info.yml文件中,并将此文件包含在我们的剧本中。我们使用gcp_compute_network模块创建了 VPC,并提供了以下信息以部署 VPC:

  • Name:我们新 VPC 的名称。

  • Auto_create_subnetwork:将其设置为no,因为我们想要创建自定义 VPC 网络,而不是自动模式 VPC 网络。

  • Routing_config:将其设置为Regional,以阻止不同区域子网之间的路由传播。

我们需要强调的一个明显的点是,GCP 中的 VPC 具有全局范围,这意味着它们不绑定到特定区域,而是跨越 GCP 云中的所有区域。另一方面,子网是特定于区域的;然而,由于我们创建了自定义 VPC,在任何区域默认情况下都不会创建子网,我们完全控制在哪里定义我们的子网。在 VPC 范围方面,这种逻辑与 AWS 和 GCP 的 VPC 范围有所不同。

使用gcp_compute_network模块创建 VPC 时,我们必须提供 VPC 名称。在这个任务中,我们使用了regex_replace Ansible 过滤器,以确保 VPC 名称不包含下划线字符(_),因为这不是 VPC 名称中的有效字符。我们使用这个过滤器来替换下划线的任何出现,用破折号(-)来确保 VPC 名称符合 GCP VPC 命名标准。

一旦我们运行我们的 playbook,我们可以看到 VPC 已经创建,在 GCP 控制台上可以看到:

以下片段概述了 Ansible 模块在创建 VPC 后返回的参数:

ok: [demo_gcp_vpc] => {
 "gcp_vpc": {
 "autoCreateSubnetworks": false,
 "changed": true,
 "creationTimestamp": "2019-11-26T12:49:51.130-08:00",
 "failed": false,
 "id": "8661055357091590400",
 "kind": "compute#network",
 "name": "demo-gcp-vpc",
 "routingConfig": {
 "routingMode": "REGIONAL"
 },
 "selfLink": "https://www.googleapis.com/compute/v1/projects/gcp-ansible-demo/global/networks/demo-gcp-vpc"
 }
} 

这些信息很重要,我们将在后续的教程中使用它来创建子网,以便我们可以注册此任务的输出到gcp_vpc变量中,以便在以后的任务中引用它。

还有更多...

默认情况下,当我们在 GCP 中创建一个新项目时,会为该项目创建一个名为default的自动模式 VPC。建议我们删除这个默认网络,因为我们将依赖我们的自定义 VPC 来容纳所有的计算工作负载。

我们可以在我们的项目中看到这个默认 VPC 是存在的,并且它在 GCP 云中的每个区域都有子网,如下面的截图所示:

我创建了一个名为pb_gcp_delete_default_vpc.yml的 playbook 来删除默认 VPC 以及附加到它的所有默认防火墙规则。

另请参阅...

有关 Ansible 中 GCP 虚拟私有云模块以及此模块支持的所有其他参数的更多信息,请访问docs.ansible.com/ansible/latest/modules/gcp_compute_network_module.html#gcp-compute-network-module

创建子网

我们使用子网来分隔我们的 GCP VPC,这是一个工具,允许我们将计算工作负载放入特定区域。此外,子网为我们提供了工具,将我们的虚拟网络分隔成不同的路由和安全域,我们可以控制以提供不同的路由和安全行为在每个子网内。在这个教程中,我们将概述如何在 GCP 云中定义和配置子网。

准备工作

Ansible 控制机必须连接到互联网,可以访问 GCP 公共 API 端点,并且 GCP 帐户应该按照前面的教程进行配置。此外,GCP VPC 需要按照前面的教程进行创建。

如何做...

  1. 使用以下代码更新group_vars/gcp_vpc.yml文件中的子网数据:
$ cat group_vars/gcp_vpc.yml

subnets:
 - name: anz-web
 cidr: 10.1.1.0/24
 region: australia-southeast1

 - name: anz-db
 cidr: 10.1.2.0/24
 region: australia-southeast1

 - name: anz-bastion
 cidr: 10.1.3.0/24
 region: australia-southeast1
  1. 使用以下任务更新gcp_net_build/tasks/main.yml文件来创建我们的子网:
- name: Create Subnets
 gcp_compute_subnetwork:
 name: "{{ subnet.name }}"
 ip_cidr_range: "{{ subnet.cidr }}"
 network: "{{ gcp_vpc}}"
 region: "{{ subnet.region }}"
 state: present
 auth_kind: "{{ auth_kind }}"
 project: "{{ project }}"
 service_account_file: "{{ service_account_file }}"
 loop: "{{ subnets }}"
 loop_control:
 loop_var: subnet
 register: gcp_subnets

工作原理...

在这个教程中,我们创建了我们将在部署中使用的子网。在我们的子网定义中需要注意的第一件事是,我们为每个子网定义了一个区域。这是强制性的,因为正如我们讨论的那样,在 GCP 中,子网具有区域范围,而 VPC 具有全局范围。我们为每个子网定义了一个 CIDR 范围,以及它的名称。

我们使用gcp_compute_subnet模块来创建所有子网。我们使用了之前讨论过的相同参数进行身份验证。为了创建子网,我们指定了以下参数:

  • 名称:我们的子网名称。

  • 区域:此子网将部署的区域。

  • Ip_cidr_range:此子网的 CIDR 块。

  • 网络:我们希望此子网成为其中一部分的 VPC 的引用。我们从创建 VPC 的输出中获取此参数。我们提供gcp_vpc变量,这是来自我们 VPC 创建任务的注册变量。

运行 playbook 后,我们可以看到所有子网都已创建,如下截图所示:

另请参阅...

有关 Ansible 中 GCP 子网模块以及此模块支持的所有其他参数的更多信息,请访问docs.ansible.com/ansible/latest/modules/gcp_compute_subnetwork_module.html

在 GCP 中部署防火墙规则

GCP 提供了许多工具,以强制执行 GCP 云客户环境中的安全性。防火墙规则是 GCP 中支持的最基本的安全工具之一,以实施 VPC 中所有工作负载的第一级防御。在本配方中,我们将概述如何在 GCP 云中定义和配置防火墙规则。

准备工作

Ansible 控制机必须连接到互联网,并能够访问 GCP 公共 API 端点,GCP 帐户应按照前面的配方进行配置。此外,VPC 和子网应按照前面的配方进行配置。

如何做...

  1. 使用以下防火墙规则更新group_vars/gcp_vpc.yml,以保护 Web 和 DB 层之间的流量。
$ cat group_vars/gcp_vpc.yml

fw_rules:
 - name: allow_sql_from_anz-web_to_anz-db
 type: allow
 direction: ingress
 priority: 10
 apply_to: anz-db
 src_tag: anz-web
 dest_tag:
 protocol: tcp
 port: 3389
 state: present
  1. 使用以下防火墙规则更新group_vars/gcp_vpc.yml,以保护流量到 Web 层:
 - name: allow_internet_to-anz-web
 type: allow
 direction: ingress
 priority: 10
 src: 0.0.0.0/0
 apply_to: anz-web
 protocol: tcp
 port: 80,443
 state: present
  1. 使用以下防火墙规则更新group_vars/gcp_vpc.yml,以允许ssh仅限于堡垒主机:
 - name: allow_ssh_to_anz-bastion
 type: allow
 direction: ingress
 priority: 10
 src: 0.0.0.0/0
 apply_to: anz-bastion
 protocol: tcp
 port: 22
 state: present

 - name: allow_ssh_from_bastion_only
 type: allow
 direction: ingress
 priority: 10
 src_tag: anz-bastion
 apply_to: anz-web,anz-db
 protocol: tcp
 port: 22
 state: present
  1. 使用以下任务更新roles/gcp_net_build/tasks.main.yml文件以创建所有必需的防火墙规则:
- name: Create Allow Firewall Rules
 gcp_compute_firewall:
 name: "{{ rule.name | regex_replace('_','-') }}"
 network: {selfLink: "{{ gcp_vpc.selfLink }}"}
 priority: "{{ rule.priority | default(omit) }}"
 direction: "{{ rule.direction | upper | mandatory }}"
 allowed:
 - ip_protocol: "{{ rule.protocol }}"
 ports: "{{ (rule.port|string).split(',') }}"
 source_ranges: "{{ rule.src | default(omit) }}"
 source_tags: "{{ omit if rule.src_tag is not defined else rule.src_tag.split(',') }}"
 destination_ranges: "{{ rule.dest | default(omit) }}"
 target_tags: "{{ omit if rule.apply_to is not defined else rule.apply_to.split(',') }}"
 auth_kind: "{{ auth_kind }}"
 project: "{{ project }}"
 service_account_file: "{{ service_account_file }}"
 loop: "{{ fw_rules | selectattr('type','equalto','allow') | list }}"
 loop_control:
 loop_var: rule
 tags: gcp_fw_rules

工作原理...

GCP 中的防火墙规则是应用于 VPC 中的主机的有状态防火墙规则。GCP 中的防火墙规则可以应用于入站或出站方向,并且有一些默认防火墙规则被定义并应用于 VPC 中的所有主机,如下所示:

  • 在入站方向上,对于任何目标到新自定义 VPC 中的任何主机的所有流量,默认情况下会有一个拒绝所有

  • 在出站方向上,对于新自定义 VPC 中的任何主机发出的所有流量,默认情况下会有一个允许所有

有了上述默认规则,并且由于所有防火墙规则都是有状态的,VPC 中的任何主机发起的任何通信都将被允许;但是,来自 VPC 外部的任何发起的流量将被拒绝。

GCP 防火墙规则可以根据以下标准匹配流量:

  • 源/目标 IPv4 范围

  • IP 协议号

  • TCP/UDP 端口号

  • 网络标记

除了网络标记之外,所有前述标准都是相当明显的。网络标记是特殊的元数据,可应用于 VPC 中的任何主机,以标识和分组这些主机。我们可以使用这些网络标记来作为防火墙规则中的匹配标准,并且仅将防火墙规则应用于 VPC 中的一部分主机。

有了所有这些信息,我们希望在示例网络中的主机上实施以下安全策略:

  • 所有 HTTP/HTTPs 流量应仅允许到我们所有的 Web 服务器。

  • 外部的 SSH 访问应该仅限于我们的堡垒主机。

  • 对我们的 Web 和 DB 服务器的 SSH 访问仅限于堡垒主机。

  • 仅允许来自 Web 到 DB 服务器的 SQL 流量。

我们在一个新的数据结构fw_rules中定义了我们的防火墙规则,这是我们需要应用于我们的 VPC 的所有规则的列表。我们在所有策略中使用网络标记,以便将正确的防火墙规则应用于应强制执行此规则的主机。

我们使用 Ansible 模块gcp_compute_firewall来迭代所有防火墙策略并应用它们。在此模块中,我们可以定义匹配条件,可以基于源/目标 IPv4 地址范围,也可以基于源和目标网络标记。我们定义了我们的任务,以便如果在我们的防火墙规则中未定义参数(例如,源 IPv4 范围),我们应该从提供给模块的参数列表中删除此参数。我们使用omit过滤器来实现此逻辑。

GCP 中的所有防火墙规则都有优先级字段,该字段定义了规则相对于其他规则的优先级以及在处理方面的优先级。没有特定优先级的规则将获得优先级值 1,000。GCP 应用于 VPC 的默认防火墙规则具有优先级值65535,因此我们定义的任何规则都将优先于它们。在所有规则中,我们指定优先级值为10

运行以下任务后,我们可以看到以下规则应用于我们的 VPC,如下截图所示:

另请参阅...

有关 Ansible 中 GCP 防火墙模块以及此模块支持的所有其他参数的更多信息,请访问docs.ansible.com/ansible/latest/modules/gcp_compute_firewall_module.html

在 GCP 中部署 VMs

在本教程中,我们将概述如何在 GCP 中的 VPC 中使用我们部署的正确子网部署虚拟机(使用 Google Compute Engine)。我们还将分配正确的网络标记,以强制执行这些机器上的正确安全策略。

准备工作

Ansible 控制机必须连接到具有对 GCP 公共 API 端点的可达性的互联网,并且 VPC、子网和防火墙规则需要按照前几章中概述的方式部署。

操作步骤...

  1. 更新group_vars/gcp_vpc.yml文件,包括描述我们将在所有 VM 上使用的 flavor 和 OS 的所需信息:
$ cat group_vars/gcp_vpc.yml

compute_node_flavor: f1-micro
compute_node_images: projects/centos-cloud/global/images/family/centos-7
compute_node_image_size: 10
  1. 更新group_vars/gcp_vpc.yml文件,包括描述我们计算节点所需信息:
$ cat group_vars/gcp_vpc.yml
compute_nodes:
 - name: web-server-1
 network: anz-web
 has_internet: yes
 zone: australia-southeast1-a

< -- Output Omitted for Brevity -- >

 - name: db-server-1
 network: anz-db
 has_internet: no
 zone: australia-southeast1-a

< -- Output Omitted for Brevity -- >

 - name: bastion-host
 network: anz-bastion
 ip: 10.1.3.253
 has_internet: yes
 ip_forwarding: yes
 zone: australia-southeast1-a
  1. 创建一个新的 Ansible 角色(gcp_vm_build)来在 GCP 上部署 VM 工作负载:
$ cd roles
$ ansible-galaxy init gcp_vm_build
  1. 使用以下任务更新gcp_vm_build/tasks/main.yml文件以创建 VM 的磁盘:
- name: create a disk for {{ node.name }}
 gcp_compute_disk:
 name: "{{ node.name | regex_replace('_','-') }}-disk"
 size_gb: "{{compute_node_image_size }}"
 source_image: "{{ compute_node_images }}"
 zone: "{{ node.zone }}"
 auth_kind: "{{ auth_kind }}"
 project: "{{ project }}"
 service_account_file: "{{ service_account_file }}"
 state: present
 register: gcp_vm_disk

  1. 使用以下任务更新gcp_vm_build/tasks/main.yml文件以创建没有公共 IP 地址的 VM:
- name: create a {{ node.name }} instance with no Internet
 gcp_compute_instance:
 name: "{{ node.name | regex_replace('_','-') }}"
 machine_type: "{{ compute_node_flavor }}"
 disks:
 - source: "{{ gcp_vm_disk }}"
 boot: 'true'
 network_interfaces:
 - network: "{{ gcp_vpc }}"
 subnetwork: "{{ gcp_subnets.results |
 selectattr('name','equalto',node.network) |
 list | first }}"
 metadata:
 tier: "{{ node.name.split('-')[0] }}"
 tags:
 items: "{{ node.network }}"
 zone: "{{ node.zone }}"
 auth_kind: "{{ auth_kind }}"
 project: "{{ project }}"
 service_account_file: "{{ service_account_file }}"
 state: present
 when: not node.has_internet
  1. 使用以下任务更新gcp_vm_build/tasks/main.yml文件以创建具有公共 IP 地址的 VM:
- name: create an {{ node.name }} instance with Internet
 gcp_compute_instance:
 name: "{{ node.name | regex_replace('_','-') }}"
 machine_type: f1-micro
 can_ip_forward: "{{ node.ip_forwarding if node.ip_forwarding is defined else 'no' }}"
 disks:
 - source: "{{ gcp_vm_disk }}"
 boot: 'true'
 network_interfaces:
 - network: "{{ gcp_vpc }}"
 network_ip: "{{ node.ip if node.ip is defined else omit }}"
 subnetwork: "{{ gcp_subnets.results |
 selectattr('name','equalto',node.network) |
 list | first }}"
 access_configs:
 - name: External NAT
 type: ONE_TO_ONE_NAT
 metadata:
 tier: "{{ node.name.split('-')[0] }}"
 zone: "{{ node.zone }}"
 tags:
 items: "{{ node.network }}"
 auth_kind: "{{ auth_kind }}"
 project: "{{ project }}"
 service_account_file: "{{ service_account_file }}"
 state: present
 register: vm_data
 when: node.has_internet
  1. 使用以下任务更新pb_gcp_env_build.yml剧本以创建我们定义的所有所需 VM:
 - name: Build VM Instances
 include_role:
 name: gcp_vm_build
 loop: "{{ compute_nodes }}"
 loop_control:
 loop_var: node

工作原理...

根据我们示例网络的设计,我们将在两个不同的可用性区域中部署两个 Web 服务器和两个数据库服务器。然后,我们将在单个 AZ 中构建一个堡垒主机,因为它仅用于管理。我们在compute_nodes变量中定义了所有所需的机器,并且对于每台机器,我们指定了以下我们在规划期间将使用的参数:

  • 名称:机器的名称

  • 网络:指定我们将部署此计算机的子网,并强制执行正确的网络标记

  • 区域:指定我们要部署此计算机的区域

  • has_internet:表示此计算机是否应获取公共 IP 地址

我们创建了一个新的角色来部署我们的计算工作负载,并定义了以下主要部分:

  • 为 VM 创建磁盘:初始任务是创建将容纳这些机器的操作系统的磁盘。我们使用gcp_compute_disk Ansible 模块来定义这些磁盘,并指定了以下参数:

  • 名称:这是此磁盘的名称。

  • Image_source:指定机器将运行的操作系统-在我们的示例中,所有机器都将运行 CentOS。

  • 区域:指定将创建此磁盘的可用性区域。

  • Size_gb:指定将要部署的磁盘大小。

  • 创建 VMs:在创建磁盘之后,我们使用gcp_compute_instance模块创建了 VMs,该模块使用以下参数来部署 VM:

  • Name:这个 VM 的名称。

  • Machine_type:指定我们用于这些机器的实例类型。

  • Disks:一个字典,指定我们将与这台机器一起使用的磁盘。我们提供gcp_vm_disk变量,这是我们在前一个任务中部署磁盘时获得的。

  • Network_interfaces:一个字典,指定我们需要在哪个子网和 VPC 上部署这个实例。对于 VPC,我们提供gcp_vpc变量,这是我们在部署 VPC 时获得的值。

  • Zone:指定我们将在哪个可用区部署我们的 VM。

  • 标签:指定我们将分配给这些 VM 的网络标签。这些标签与我们在防火墙规则中使用的标签相同,以便引用我们的计算节点。

除了前面的参数,我们还有access_configs参数(它是一个字典),用于指定计算节点是否会获得公共 IP 地址。如果 VM 获得公共 IP 地址,我们将access_configs中的 name 参数设置为 external NAT,type 参数设置为ONE_TO_ONE_NAT。如果机器不需要公共 IP 地址,我们将省略access_configs字典。

在我们的设置中,所有的 Web 服务器和堡垒主机都应该获得一个公共 IP 地址;然而,我们的数据库服务器不应该直接连接互联网,因此不应该为它们分配公共 IP 地址。我们使用has_internet参数来区分这一点,在计算节点定义中使用这个参数来选择在 VM 配置期间使用的正确任务。

一旦我们使用新角色运行剧本来创建 VMs,我们将看到每个 VM 的所有磁盘都已创建,如下截图所示:

此外,所有的 VM 都是在正确的子网中创建的,如下截图所示:

一旦我们的 VM 使用了正确的网络标签创建,我们可以验证我们的防火墙规则只应用于基于这些网络标签的 VM。以下代码段概述了防火墙规则allow-internet-to-anz-web以及它是如何仅应用于 Web 服务器的:

另请参阅...

有关 Ansible 中 GCP 实例和磁盘模块以及这些模块支持的所有其他参数的更多信息,请访问以下链接:

调整 VPC 内的路由

在这个配方中,我们将概述如何控制 GCP VPC 内的路由,以强制执行主机的自定义路由决策。这使我们能够完全控制 VPC 内主机的路由。

准备工作

Ansible 控制机必须连接到互联网,能够访问 GCP 公共 API 端点,并且 GCP 帐户应该按照前面的配方进行配置。此外,资源组、虚拟网络和子网应该按照前面的配方进行部署。

操作步骤...

  1. 更新group_vars/gcp_vpc.yml文件,包括所需的路由数据,如下所示:
$ cat group_vars/gcp_vpc.yml
route_tables:
 - name: db_tier_rt
 subnet: db_tier
 routes:
 - name: Default Route
 prefix: 0.0.0.0/0
 nh: none
  1. 更新pb_gcp_env_build.yml剧本,添加以下任务以在 GCP 中创建路由:
- name: Create the Route
 gcp_compute_route:
 name: "{{ route.name }}"
 dest_range: "{{ route.dest}}"
 network: {selfLink: "{{ gcp_vpc.selfLink }}"}
 next_hop_ip: "{{ route.nh }}"
 tags: "{{ route.apply_to.split(',') | default(omit) }}"
 state: present
 auth_kind: "{{ auth_kind }}"
 project: "{{ project }}"
 service_account_file: "{{ service_account_file }}"
 loop: "{{ cutom_routes }}"
 loop_control:
 loop_var: route
 tags: gcp_route

工作原理..

在我们的示例设置中,根据当前的路由和防火墙规则,我们的数据库服务器无法连接到互联网;但是,我们需要能够从这些服务器访问互联网,以安装软件或执行补丁。为了实现这个目标,我们将使用我们的堡垒主机作为 NAT 实例,为我们的数据库服务器提供互联网访问。为了实现这一目标,我们需要调整 VPC 中所有数据库服务器的路由。

在 GCP 中,我们有一个默认路由,指向 VPC 中的互联网网关。这个默认路由存在于 VPC 中,并应用于 VPC 内的所有主机。以下是我们 VPC 的路由表:

然而,由于现有的防火墙规则以及所有数据库服务器都没有外部公共 IP 地址,数据库服务器将无法访问互联网。我们需要调整数据库服务器的路由,指向执行 NAT 的堡垒主机。我们还需要保留原始的默认路由,因为这是我们的 Web 和堡垒主机用来访问互联网的主要路径。

我们使用custom_routes列表数据结构定义需要应用的自定义路由,并使用gcp_compute_route Ansible 模块循环遍历这个数据结构,创建所有需要的路由。我们使用在 DB 主机上应用的网络标记,以强制仅在具有此网络标记的主机上应用此路由。运行这个新任务后,VPC 的更新路由表如下截图所示:

我们可以将路由的下一跳设置为 IP 地址或实例标识;但是,为了简单起见,我们使用了 IP 地址,并在 VM 定义中选择了堡垒主机的静态 IP 地址,以便在我们的路由设置中轻松引用这个 IP 地址。

我们在主要 playbook 中创建了这个路由任务,因为我们需要有堡垒 VM 才能设置下一跳 IP 地址的路由。如果在 VM 被部署之前创建路由,路由将被创建;但是,任务将失败,并显示警告,指出我们的路由的下一跳 IP 地址不存在。

另请参阅...

有关 Ansible 中 GCP 路由模块和此模块支持的所有其他参数的更多信息,请转到docs.ansible.com/ansible/latest/modules/gcp_compute_route_module.html#gcp-compute-route-module

使用 Ansible 验证 GCP 部署

Ansible 提供了多个模块来收集我们在 GCP 中创建的不同资源的操作状态,我们可以使用这些模块来验证 GCP 云中我们网络资源的当前状态。这提供了一种编程方法来验证部署,而无需登录到图形用户界面GUI)通过门户网站检查 GCP 中部署的不同组件的状态。在本示例中,我们将概述如何使用一些 Ansible 模块来验证我们部署的网络子网。

准备工作

Ansible 控制机必须连接到互联网,并能够到达 GCP 公共 API 端点,GCP 帐户应按照前面的示例进行配置。

如何做...

  1. 创建一个新的pb_gcp_net_validate.yml playbook,并添加以下任务以收集 VPC 子网信息:
$ cat pb_gcp_net_validate.yml

---
- name: Build GCP Environment
 hosts: all
 connection: local
 gather_facts: no
 force_handlers: True
 vars_files:
 - gcp_account_info.yml
 tasks:
 - name: Get Subnet Facts
 gcp_compute_subnetwork_facts:
 region: "{{ subnets | map(attribute='region') | list | first }}"
 auth_kind: "{{ auth_kind }}"
 project: "{{ project }}"
 service_account_file: "{{ service_account_file }}"
 register: gcp_vpc_subnets
  1. 使用以下任务更新 playbook 以验证部署的所有子网上的 IP 前缀:
 - name: Validate all Subnets are Deployed
 assert:
 that:
 - gcp_vpc_subnets['items'] | selectattr('name','equalto',item.name) |
 map(attribute='ipCidrRange') | list | first
 == item.cidr
 loop: "{{ subnets }}"

它是如何工作的...

我们创建了一个新的 playbook,用于验证我们在 GCP 项目中部署的所有子网。Ansible 提供多个模块来收集 GCP 中不同资源(子网、VPC、虚拟机等)的操作状态或事实。在本例中,我们使用gcp_compute_subnetwork_facts模块来收集我们部署的子网事实。我们将此模块返回的所有数据注册到一个新变量gcp_vpc_subnets中。最后,我们使用assert模块循环遍历所有子网定义,验证所有这些子网上分配的 IP 前缀是否正确并与我们的设计一致。

我们可以使用其他收集事实的模块来验证部署的其他方面,并使用多个assert语句来确保所有部署的资源与我们的设计一致。

另请参阅...

有关其他 GCP 收集事实的模块的更多信息,请访问以下链接:

使用 Ansible 销毁 GCP 资源

与使用自动化规模创建资源类似,一旦决定不再需要这些资源,我们可以销毁这些资源。我们使用与在 GCP 中创建资源相同的 Ansible 模块来销毁这些资源。

准备工作

Ansible 控制机必须连接到互联网,并能够访问 GCP 公共 API 端点,GCP 帐户应按照前面的示例进行配置。

如何操作...

  1. 创建一个新的pb_gcp_env_destroy.yml playbook,并添加以下任务以读取保险柜加密的 JSON 文件:
$ cat pb_gcp_env_destroy.yml

---
- name: Decommission GCP Resources
 hosts: all
 connection: local
 force_handlers: True
 environment:
 GCP_SERVICE_ACCOUNT_FILE: "{{ service_account_file }}"
 GCP_AUTH_KIND: 'serviceaccount'
 vars_files:
 - gcp_account_info.yml
 tasks:
 - name: Read the Vault Encrypted JSON File
 copy:
 content: "{{ lookup('file','gcp-ansible-secret.json') }}"
 dest: "{{ service_account_file }}"
 notify: Clean tmp Decrypted Files
 tags: always
  1. 更新pb_gcp_env_destroy.yml playbook,并添加以下任务以收集 VPC 信息:
 - name: Get VPC Facts
 gcp_compute_network_facts:
 project: "{{ project }}"
 register: gcp_vpc
  1. 使用以下任务更新 playbook 以删除所有 VM:
 - name: Delete Instance {{ node.name }}
 gcp_compute_instance:
 name: "{{ node.name | regex_replace('_','-') }}"
 zone: "{{ node.zone }}"
 project: "{{ project }}"
 state: absent
 loop: "{{ compute_nodes }}"
 loop_control:
 loop_var: node
  1. 使用以下任务更新 playbook 以删除我们在 VPC 中为所有 VM 创建的所有磁盘:
 - name: Delete disks for {{ node.name }}
 gcp_compute_disk:
 name: "{{ node.name | regex_replace('_','-') }}-disk"
 zone: "{{ node.zone }}"
 project: "{{ project }}"
 state: absent
 loop: "{{ compute_nodes }}"
 loop_control:
 loop_var: node
  1. 使用以下任务更新 playbook 以删除 VPC 中的所有防火墙规则:
 - name: Delete All Firewall Rules
 gcp_compute_firewall:
 name: "{{ rule.name | regex_replace('_','-') }}"
 network: "{{ gcp_vpc }}"
 project: "{{ project }}"
 state: absent
 loop: "{{ fw_rules }}"
 loop_control:
 loop_var: rule
 tags: gcp_fw_rules
  1. 使用以下任务更新 playbook 以删除 VPC 中的所有自定义路由:
- name: Delete all Routes
 gcp_compute_route:
 name: "{{ route.name }}"
 dest_range: "{{ route.dest}}"
 network: "{{ gcp_vpc }}"
 project: "{{ project }}"
 state: absent
 loop: "{{ custom_routes }}"
 loop_control:
 loop_var: route
 when:
 - custom_routes is defined
  1. 使用以下任务更新 playbook 以删除 VPC 中的所有子网:
 - name: Delete GCP Subnets
 gcp_compute_subnetwork:
 name: "{{ subnet.name }}"
 ip_cidr_range: "{{ subnet.cidr }}"
 network: "{{ gcp_vpc }}"
 region: "{{ subnet.region }}"
 project: "{{ project }}"
 state: absent
 loop: "{{ subnets }}"
 loop_control:
 loop_var: subnet
  1. 使用以下任务更新 playbook 以删除所有 VPC:
 - name: Delete GCP VPC
 gcp_compute_network:
 name: "{{ vpc_name | regex_replace('_','-') }}"
 project: "{{ project }}"
 state: absent

工作原理...

我们创建了一个新的 playbook,用于销毁我们样本网络设计中的所有资源。我们使用了与在 GCP 云中部署资源相同的模块;但是,我们使用了state: absent来删除所有这些资源。

在销毁资源时唯一需要注意的是删除这些资源的顺序。如果仍有依赖于要删除的资源的活动依赖资源,我们就不能删除任何资源。例如,我们不能在没有先删除使用此磁盘的 VM 的情况下删除磁盘。

运行 playbook 后,我们可以看到所有 VM 都已删除,如下面的截图所示:

此外,所有我们的 VPC 和子网也已删除,如下面的截图所示:

上述截图显示当前项目中没有本地 VPC 网络。

第十章:使用 Batfish 和 Ansible 进行网络验证

在本书的所有先前章节中,我们使用了多个示例来概述如何使用 Ansible 执行网络验证——我们通过利用 Ansible 中可用的不同模块来实现这一点。在所有这些情况下,我们在将配置推送到网络设备后执行了网络验证。然后,我们收集了网络状态并验证其是否与我们预期的状态一致。然而,我们可能希望在将配置推送到设备之前验证网络状态。此外,可能需要验证预期的网络状态是否符合要求,甚至在不触及网络的情况下进行验证。但是我们该如何做到呢?

Batfish 是一个针对这种用例的开源项目。它的主要目标是提供一个离线网络验证工具,以验证网络配置的多个方面。Batfish 可以为网络提供安全性、合规性和流量转发的验证和正确性保证。它使用我们网络设备的设备配置来构建我们网络的中立数据模型和转发树,然后我们可以使用它来验证网络状态和验证网络内的正确流量转发。以下图表概述了 Batfish 的高级架构以及其工作原理:

Batfish 使用客户端/服务器模型。通过这种模型,我们运行一个 Batfish 服务器实例(用 Java 编写),并使用一个名为 Pybatfish 的客户端软件开发工具包(用 Python 编写)与服务器进行通信。然后,我们使用网络配置文件初始化我们网络的快照,并且基于这个网络快照,Batfish 服务器为我们的网络计算了一个数据模型。使用客户端,我们可以开始使用这个供应商中立的数据模型来验证我们的网络。

Batfish 团队开发了多个 Ansible 模块,这些模块包装了 Pybatfish 客户端库,以检索 Batfish 服务器生成的数据模型。这些模块允许我们针对网络模型执行不同的查询,以验证我们的网络状态。接下来的图表概述了 Ansible、Pybatfish 和 Batfish 服务器之间的交互。

在本章中,我们将概述如何安装 Batfish 以及如何将其与 Ansible 集成,以便开始使用它来验证网络状态,而不是将配置推送到我们的设备。这种组合非常强大,并且可以轻松扩展到构建用于网络配置更改的完整持续集成/持续部署(CI/CD)流水线。Batfish 可以成为在将配置推送到生产网络设备之前提供预验证的重要组成部分。

我们将使用以下网络拓扑,该拓扑在第四章中使用,构建数据中心网络与 Arista 和 Ansible,来概述如何使用 Ansible 和 Batfish 验证样本叶脊网络拓扑:

本章涵盖的主要内容有:

  • 安装 Batfish

  • 将 Batfish 与 Ansible 集成

  • 生成网络配置

  • 为 Batfish 创建网络快照

  • 使用 Ansible 初始化网络快照

  • 从 Batfish 收集网络信息

  • 使用 Batfish 验证流量转发

  • 使用 Batfish 验证访问控制列表(ACLs)

技术要求

本章中描述的所有代码都可以在以下网址找到:github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch10_batfish

本章基于以下软件版本:

  • 运行 CentOS 7.7 的 Ansible 机器

  • 托管 Batfish 容器的 CentOS 7.7 机器

  • Python 3.6.8

  • Ansible 2.9

  • Arista 虚拟化可扩展操作系统vEOS)运行 EOS 4.20.1F

查看以下视频以查看代码的实际操作:

bit.ly/3bhke1A

安装 Batfish

在这个步骤中,我们将概述如何安装 Batfish 容器(Batfish 架构中的服务器组件)并启动它,以便从 Ansible 开始与其进行交互。这是一个必要的基础步骤,以便开始使用 Batfish 验证我们的网络。

准备工作

如本章介绍所述,我们将在单独的 Linux 机器上安装 Batfish。这台机器需要具有互联网连接,以便能够安装 Docker 并下载 Batfish 容器。

如何做…

  1. 在 CentOS Linux 机器上安装 Docker,如下面的网址所示:

docs.docker.com/install/linux/docker-ce/centos/

  1. 一旦 Docker 安装并运行,下载 Docker 容器,如下面的代码片段所示:
$ sudo docker pull batfish/batfish
  1. 启动 Batfish 容器,如下面的代码片段所示:
$ sudo docker run -d -p 9997:9997 -p 9996:9996 batfish/batfish

它是如何工作的…

Batfish 提供了多种安装和运行 Batfish 服务器的选项。然而,最简单和最推荐的方法是运行一个包含 Batfish 服务器的 Docker 容器。为了运行这个 Docker 容器,我们首先需要在 CentOS Linux 机器上安装 Docker。在我们的情况下,Docker 可以安装在不同的 Linux 发行版上,也可以安装在 macOS 和 Windows 上。

一旦安装了 Docker,我们使用 docker pull 命令将 Batfish 容器下载到我们的 Linux 机器上,并使用 docker run 命令启动 Docker 容器。我们必须从容器中暴露 传输控制协议TCP)端口 99969997,并将它们映射到 Linux 机器上,使用 -p 指令。我们将这些端口映射到 Linux 机器上的相同端口。这些端口用于从远程客户端(安装在 Ansible 控制机器上的 Pybatfish 客户端库)与 Batfish 服务器进行交互。

Batfish 提供两个 Docker 容器:batfish/batfishbatfish/allinonebatfish/allinone 容器包含 Batfish 服务器和 Pybatfish 客户端库。它还安装了 Jupyter Notebook Python 库以及一些示例笔记本,以开始与 Batfish 服务器进行交互。但是,我们不会使用这种方法。相反,我们将使用 batfish/batfish 容器,它只包含 Batfish 服务器。

另请参阅…

有关 Batfish 及其安装方法的更多信息,请访问以下网址:

将 Batfish 与 Ansible 集成

为了将 Batfish 与 Ansible 集成,我们需要安装所需的 Python 包。这样做将允许 Ansible 与 Batfish 服务器进行通信。在这个步骤中,我们将概述如何安装这些 Python 包,以及如何安装运行所需的 Batfish Ansible 模块所需的 Ansible 角色。

准备工作

为了按照这个步骤进行操作,Ansible 控制器必须具有互联网连接。这将允许我们安装 Batfish 所需的依赖项。

如何做…

  1. 在 Ansible 控制器上安装 Batfish 客户端 python3 包,如下面的代码片段所示:
$ sudo python3 -m pip install --upgrade git+https://github.com/batfish/pybatfish.git
  1. 下载 batfish Ansible 角色到 roles 文件夹中,如下面的代码片段所示:
$ ansible-galaxy install batfish.base

它是如何工作的…

在这个步骤中,我们正在设置 Ansible 和 Batfish 之间的集成。这通过两个步骤完成:

  1. 在 Ansible 控制器上,我们需要安装pybatfish Python 库,这是与 Batfish 服务器交互的 Batfish 客户端 SDK。这个包是 Ansible 模块所需的。这些将被用于在我们的 playbooks 中与 Batfish 服务器交互。

  2. 其次,我们安装了 Batfish 团队开发的batfish角色,以便与 Batfish 服务器交互并验证网络设备配置。这个 Ansible 角色包含了运行 Batfish 自定义 Ansible 模块所需的所有 Python 脚本。为了将此角色安装到 Ansible 控制机上,我们使用ansible-galaxy

我们可以验证pybatfish是否正确安装,如下所示:

$ pip3 freeze | grep batfish
pybatfish==0.36.0

现在我们可以探索由ansible-galaxy下载的已安装角色:

$ ansible-galaxy list batfish.base
# /home/ansible/.ansible/roles

以下是此角色的 Python 源代码列表,该列表位于此角色的library文件夹中:

$tree ~/.ansible/roles/batfish.base/library/
/home/ansible/.ansible/roles/batfish.base/library/
├── bf_assert.py
├── bf_extract_facts.py
├── bf_init_snapshot.py
├── bf_session.py
├── bf_set_snapshot.py
├── bf_upload_diagnostics.py
└── bf_validate_facts.py

通过完成这两个步骤,Ansible 控制器已经准备好开始与我们在上一个示例中部署的 Batfish 服务器进行交互。

由于我们没有在ansible-galaxy install命令上指定任何额外的参数,这些角色将默认安装在~/.ansible/roles路径上。

另请参阅...

有关 Pybatfish 和 Batfish 开发的 Ansible 角色以及与 Ansible 一起使用的更多信息,请查看此页面:github.com/batfish/batfish/blob/master/README.md

生成网络配置

要开始使用 Batfish 进行分析和验证,我们需要向 Batfish 服务器提供我们网络设备的配置。在这个示例中,我们将概述如何使用 Ansible 生成这个配置。Batfish 是一种离线网络验证工具,拥有完整的网络配置是实现正确的网络验证的必要步骤之一。

准备工作

这里没有特定的要求,除了在 Ansible 控制机上安装了 Ansible。

如何做...

  1. 创建一个名为ch10_batfish的新文件夹,其中将包含所有我们的变量和 playbooks。

  2. 填充所有变量以描述我们的网络在group_vars/all.yml文件和host_vars文件夹中。在这里,我们使用的是第四章中概述的完全相同的变量,使用 Arista 和 Ansible 构建数据中心网络

  3. ch10_batfish文件夹内创建一个roles文件夹,以容纳我们将创建的所有角色。

  4. 创建一个名为generate_fabric_config的新角色,如下所示:

$ cd ch10_batfish
$ ansible-galaxy init --init-path roles generate_fabric_config
  1. 构建templates文件夹中的所有 Jinja2 模板,以创建接口、管理和边界网关协议BGP)配置。

  2. tasks/main.yml文件中包含构建配置所需的所有任务。同样,我们使用的是第四章中已经讨论过的完全相同的步骤和模块,使用 Arista 和 Ansible 构建数据中心网络,以构建此示例网络的配置。

  3. 创建ansible_host清单,如下面的代码块所示:

$ cat hosts [leaf] leaf01   ansible_host=172.20.1.41 leaf02    ansible_host=172.20.1.42 leaf03    ansible_host=172.20.1.43 leaf04    ansible_host=172.20.1.44 [spine] spine01     ansible_host=172.20.1.35 spine02     ansible_host=172.20.1.36
[arista:children] leaf spine
  1. 现在,创建一个名为pb_build_fabric_config.yml的新 playbook,如下所示:
$ cat pb_build_fabric_config.yml
---
- name: Build DC Fabric Config
 hosts: all
 connection: local
 gather_facts: no
 vars:
 tmp_dir: tmp
 config_dir: configs
 roles:
 - generate_fabric_config

工作原理...

在这个示例中,我们使用 Ansible 来生成我们示例拓扑中网络设备的配置。我们还使用了我们在第四章中讨论过的完全相同的数据和变量结构,使用 Arista 和 Ansible 构建数据中心网络。我们使用 YAML 文件将所有基础设施定义分组在group_varshost_vars文件夹中。我们还使用了在第四章中使用的完全相同的 Jinja2 模板,使用 Arista 和 Ansible 构建数据中心网络,以生成接口、BGP 和设备管理配置的配置片段。

我们使用ansible-galaxy init命令构建角色骨架,并使用--init-path指令指定在哪里创建这个新角色。

以下输出概述了我们用于生成设备配置的新角色的结构:

$ tree roles/generate_fabric_config
roles/generate_fabric_config
├── meta
│   └── main.yml
├── tasks
│   └── main.yml
└── templates
 ├── intf.j2
 ├── mgmt.j2
 ├── overlay_bgp.j2
 └── underlay_bgp.j2

在这一点上,我们创建一个新的 playbook 来生成设备配置,并使用connection本地参数,因为我们需要在 Ansible 控制器节点上捕获网络设备的配置。运行完 playbook 后,我们将得到所有设备的配置在configs文件夹中,如下面的代码块所示:

$ tree ch10_batfish/configs
configs
├── leaf01.cfg
├── leaf02.cfg
├── leaf03.cfg
├── leaf04.cfg
├── spine01.cfg
└── spine02.cfg

为 Batfish 创建网络快照

为了让 Batfish 能够使用设备的配置文件来分析网络,这些文件需要按特定顺序结构。这样 Batfish 服务器就可以轻松地摄取这些数据。

在这个教程中,我们将概述如何正确地结构和准备我们的网络配置文件,以便 Batfish 服务消费。

准备工作

设备配置应该已经生成,就像在上一个教程中演示的那样。

操作步骤...

  1. 创建一个名为pb_batfish_analyis.yml的新 playbook,并添加以下任务以创建一个新文件夹。这个文件夹将存放batfish分析所需的网络配置:
$ cat pb_batfish_analyis.yml
---
- name: Extract network device facts using Batfish and Ansible
 hosts: all
 gather_facts: no
 roles:
 - batfish.base
 vars:
 ansible_connection: local
 batfish_host: 172.20.100.101
 config_dir: configs
 batfish_network_folder: batfish_net_snapshot
 batfish_analysis_folder: batfish_analysis
 tasks:

 - name: Create a Batfish Config Directory
 file:
 path: "{{ batfish_network_folder }}"
 state: directory    run_once: yes
  1. 更新名为pb_batfish_analyis.yml的 playbook,添加以下任务以将所有配置文件复制到新文件夹中:
- name: copy All configs to Batfish Directory
 copy:
 src: "{{ config_dir }}"
 dest: "{{ batfish_network_folder }}"
 run_once: yes

工作原理…

为了开始对我们的网络进行分析,我们创建一个新的 playbook,用于执行所有必需的任务,并使用 Batfish 验证网络配置。在这个 playbook 中,我们使用以下参数:

  • 我们在网络中的所有节点上运行 playbook。这是因为我们需要在随后的任务中引用每个节点的参数(如环回互联网协议IP))。

  • 我们将ansible_connection参数设置为local,因为我们不需要连接到设备,所有任务将在 Ansible 机器上本地运行。

  • 我们指定 Batfish 服务器机器的 IP 地址,该服务器托管batfish容器。这将在随后的所有任务中用于与 Batfish 服务器通信。

为了让 Batfish 开始分析设备的配置,设备的配置文件需要按特定顺序结构在一个目录中。这一步通常被称为为 Batfish 分析准备网络快照。

在这里,我们为 Batfish 分析创建一个新的 playbook。在第一个任务中,我们创建configs文件夹,这将是 Batfish 用来检索网络设备配置的基础。

在第二个任务中,我们使用copy模块将网络设备的配置文件复制到configs文件夹中。一旦我们运行了包含指定任务的 playbook,我们将得到 Batfish 分析所需的以下目录结构:

 $ tree ch10_batfish/batfish_net_snapshot/

 batfish_net_snapshot
└── configs
 ├── leaf01.cfg
 ├── leaf02.cfg
 ├── leaf03.cfg
 ├── leaf04.cfg
 ├── spine01.cfg
 └── spine02.cfg

在所有任务中,我们都使用run_once参数,因为我们只想创建文件夹并复制文件一次。如果我们省略此选项,将会对清单中的每个节点运行这些任务,这在这种情况下并不理想。

另请参阅...

有关 Batfish 网络快照所需的目录结构的更多信息,请访问此链接:pybatfish.readthedocs.io/en/latest/notebooks/interacting.html#Uploading-configurations

使用 Ansible 初始化网络快照

在这个教程中,我们将概述如何在 Ansible 和 Batfish 服务器之间建立会话。除此之外,我们还将看看如何初始化我们在上一步准备的网络快照,并将其发送到 Batfish 服务器。

准备工作

如前一篇文章所述,设备配置是在此时生成的,并且网络快照已经打包好了。此外,现在还在 TCP 端口 9996 和 9997 上提供了 Ansible 控制器与 Batfish 服务器之间的 IP 可达性。

如何做...

  1. 使用以下任务更新pb_batfish_analyis.yml剧本,以与 Batfish 服务器开始会话:
 - name: Setup connection to Batfish service
 bf_session:
 host: "{{ batfish_host }}"
 name: local_batfish
 register: bf_session
 run_once: yes
  1. 更新pb_batfish_analyis.yml剧本以在 Batfish 服务器上初始化网络快照:
 - name: Initialize the Network Snapshot
 bf_init_snapshot:
 network: arista_dc_fabric
 snapshot: arista_dc_fabric_config
 snapshot_data: "{{ batfish_network_folder }}"
 overwrite: true
 run_once: yes
 register: bf_snapshot

工作原理...

在剧本中,我们使用了从ansible-galaxy下载的batfish.base Ansible 角色与 Batfish 服务器进行交互。该角色提供了多个模块,我们使用这些模块来启动 Ansible 控制机和 Batfish 服务器之间的集成。

第一个模块是bf_session。该模块在 Batfish 客户端(在本例中为 Ansible)和 Batfish 服务器之间打开会话,以便开始在两者之间交换数据。第二个模块br_init_snapshot初始化了我们在 Ansible 控制器上创建的网络快照(设备配置文件)。然后将它们发送到 Batfish 服务器,以便在 Batfish 服务器上开始分析,并且 Batfish 服务器根据这些配置文件为我们的网络构建中立的数据模型。

bf_init_session模块返回了 Batfish 解析配置的状态,以及在解码配置时是否出现任何问题。我们将这个返回值捕获在bf_snapshot变量中。以下代码段概述了 Batfish 在提供的网络快照上执行的解析状态:

ok: [localhost] => {
 "bf_snapshot": {
 "ansible_facts": {

 "bf_network": "arista_dc_fabric",
 "bf_snapshot": "arista_dc_fabric_config"
 },
 "result": {
 "network": "arista_dc_fabric",
 "snapshot": "arista_dc_fabric_config"
 },
 "summary": "Snapshot 'arista_dc_fabric_config' created in network
'arista_dc_fabric'",
 "warnings": [ 
 "Your snapshot was successfully initialized but Batfish failed to fully recognize some lines in one or more input files. Some unrecognized configuration lines are not uncommon for new networks, and it is often fine to proceed with further analysis.
 ]
 }
}

我们可以忽略我们收到的警告,因为它不会影响我们的分析。

从 Batfish 收集网络事实

Batfish 可以生成一个代表从提供给 Batfish 的配置文件中发现的关键事实的供应商中立的数据模型。在本篇文章中,我们将概述如何收集 Batfish 发现的这些事实,并如何使用这些信息来验证设备的网络配置是否符合预期状态。

准备工作

网络配置已经生成,并且网络快照已经与 Batfish 服务器同步。

如何做...

  1. 使用以下任务更新pb_batfish_analyis.yml剧本,以收集 Batfish 生成的事实:
 - name: Retrieve Batfish Facts
 bf_extract_facts:
 output_directory: "{{ batfish_analysis_folder }}/bf_facts"
 run_once: yes
 register: bf_facts
  1. 使用以下任务更新pb_batfish_analysis.yml剧本,以验证生成的接口配置:
 - name: Validate all Interfaces are Operational and Have correct IP
 assert:
 that:
 - bf_facts.result.nodes[inventory_hostname].Interfaces[item.port].Active
== true
 - bf_facts.result.nodes[inventory_hostname].Interfaces[item.port].Primary_Address ==
 item.ip + '/' + global.p2p_prefix | string
 loop: "{{ p2p_ip[inventory_hostname] }}"

工作原理...

Batfish 处理网络快照(设备配置)并为配置的不同部分生成供应商中立的数据模型。这些被认为是 Batfish 从输入配置文件生成并收集的事实。我们使用bf_extract_facts Ansible 模块来提取这些事实,然后可以将其保存到一个目录以供进一步分析。

在我们的情况下,我们将 Batfish 分析保存在bf_facts文件夹中,并且该模块生成了一个唯一的 YAML 文件,其中包含了每个设备的这个中立数据模型。以下代码段概述了我们样本拓扑中一个设备(leaf01)的接口数据模型:

nodes:
 leaf01:
 Interfaces:
 Ethernet8:
 Active: true
 All_Prefixes:
 - 172.31.1.1/31
 < --- Output Omitted for brevity --->
 Declared_Names:
 - Ethernet8
 Description: '"DC1 | Rpeer: spine01 | Rport: Ethernet1"'
 < --- Output Omitted for brevity --->
 MTU: 1500
 < --- Output Omitted for brevity --->
 Primary_Address: 172.31.1.1/31
 Primary_Network: 172.31.1.0/31
 < --- Output Omitted for brevity --->
 Speed: 1000000000.0

该模块返回相同的数据结构,我们将这个结果保存在一个名为bf_facts的新变量中。我们使用这个变量中的数据来验证我们生成的配置的设备的预期网络状态。我们还使用assert模块来循环遍历我们数据模型中声明的每个节点的所有接口。然后,我们比较 Batfish 生成的数据模型中这些参数的值,以确保我们所有的接口都是可操作的,并且所有的 IP 地址都配置正确。

还有更多...

Batfish 还提供了不同的内置assert测试,以对其生成的数据模型执行验证。这使其能够对可能影响网络的关键问题提供更简单和更健壮的验证。以下是使用 Batfish 已有的内置assert的任务:

- name: Validate BGP Sessions and Undefined References
 bf_assert:
 assertions:
 - type: assert_no_undefined_references
        name: Confirm we have no undefined references
 - type: assert_no_incompatible_bgp_sessions
 name: Confirm we have no incompatible BGP sessions
 run_once: yes

我们可以在上述代码块中看到两个断言:

  • assert_no_undefined_references:这个断言验证所有配置块是否存在且有效。例如,所有前缀列表都存在,没有未定义引用到缺失的前缀列表。这确保生成的配置是合理的,不包括对未声明对象的未定义引用。

  • Assert_no_incompatible_bgp_sessions:这个断言验证所有 BGP 会话是否正确配置,并且 BGP 对等体的配置之间没有不匹配。这也确保生成的配置是有效的,生成的 BGP 会话将是可操作的。

如果我们需要验证这些测试是否会捕捉到配置中的错误,我们可以通过关闭主配置文件中叶子和脊柱交换机之间的链路来进行验证,如下面的代码块所示:

$ cat configs/leaf01.cfg

!
interface Ethernet8
 description "DC1 | Rpeer: spine01 | Rport: Ethernet1"
 no switchport
 *shutdown*   ip address 172.31.1.1/31
!

此配置更改应该使leaf01spine01节点之间的底层 BGP 会话中断。

当我们再次运行我们的剧本时,将会看到以下错误消息:

TASK [Validate BGP Sessions and Undefined References] ****************************************************************************************************        "result": [

 {
 "details": "Assertion passed",
 "name": "Confirm we have no undefined references",
 "status": "Pass",
 "type": "assert_no_undefined_references"
 },
 {
 "details": "Found incompatible BGP session(s), when none were expected\n[{'Node': 'leaf01', 'VRF': 'default', 'Local_AS': 65001, 'Local_Interface': None, 'Local_IP': '172.31.1.1', 'Remote_AS': '65100', 'Remote_Node': None, 'Remote_Interface': None, 'Remote_IP': '172.31.1.0', 'Session_Type': 'EBGP_SINGLEHOP', 'Configured_Status': 'INVALID_LOCAL_IP'}]",
 "name": "Confirm we have no incompatible BGP sessions",
 "status": "Fail",
 "type": "assert_no_incompatible_bgp_sessions"
 }
 ],
 "summary": "1 of 2 assertions failed"
 }

从输出中,我们可以看到第一个断言成功了,这意味着我们的配置中没有未定义的引用。然而,第二个断言失败了,因为现在有一个 BGP 会话失败了。

另请参阅...

有关 Batfish Ansible 模块支持的所有可用断言的更多信息,请查看以下链接:

使用 Batfish 验证流量转发

在本教程中,我们将概述如何验证网络中的流量转发。这是通过 Batfish 使用从设备配置生成的转发表来实现的。在进行任何更改之前,验证网络内的正确流量转发非常有用。

准备工作

网络配置已经生成,并且网络快照已经与 Batfish 服务器同步。

如何做...

  1. 使用以下任务更新pb_batfish_analyis.yml剧本,以验证我们拓扑中的流量转发:
- name: Validate Traffic Forwarding in the Fabric
 bf_assert:
 assertions:
 - type: assert_all_flows_succeed
 name: confirm host is reachable for traffic received
 parameters:
 startLocation: "{{ item.0 }}"
 headers:
 dstIps: "{{ item.1.value.ip }}"
 srcIps: "{{ lo_ip[item.0].ip }}"
 with_nested:
 - "{{ play_hosts }}"
 - "{{ lo_ip | dict2items }}"
 run_once: yes

工作原理...

Batfish 提供了一种内置的验证方法,用于验证网络拓扑内端点之间的正确流量转发。这是通过使用assert_all_flows_succeed方法实现的。该方法验证给定端点之间的所有流量是否成功。为了使 Batfish 验证任何给定流的流量流动,我们需要提供以下信息:

  • 开始节点位置

  • 流的源 IP

  • 流的目标 IP 地址

Batfish 将使用其生成的数据模型为网络拓扑中的所有节点构建转发表,并验证我们正在测试的流量是否在网络中转发。

在我们的示例拓扑中,我们希望验证所有节点的回环 IP 地址的所有流量是否可以到达所有远程节点的目标回环 IP 地址。我们使用with_nested循环结构来循环遍历我们清单中的所有节点,并在lo_ip数据结构中循环遍历所有回环 IP 地址。这将测试我们清单中的所有节点是否可以到达其他所有节点的远程回环。

当我们运行这个测试时,我们会发现除了从spine01spine02的流量和从spine02spine01的反向流量之外,所有流量都正常工作,如下面的代码块所示:

*### Traffic from Spine01 to Spine02 Failing

*                "msg": "1 of 1 assertions failed",
 "result": [
 {
 "details": "Found a flow that failed, when expected to succeed\n[{'Flow': Flow(dscp=0, dstIp='10.100.1.253', dstPort=0, ecn=0, fragmentOffset=0, icmpCode=0, icmpVar=8, ingressInterface=None, ingressNode='spine01', ingressVrf='default', ipProtocol='ICMP', packetLength=0, srcIp='10.100.1.254', srcPort=0, state='NEW', tag='BASE', tcpFlagsAck=0, tcpFlagsCwr=0, tcpFlagsEce=0, tcpFlagsFin=0, tcpFlagsPsh=0, tcpFlagsRst=0, tcpFlagsSyn=0, tcpFlagsUrg=0), 'Traces': ListWrapper([((ORIGINATED(default), NO_ROUTE))]), 'TraceCount': 1}]",
 "name": "confirm host is reachable for traffic received",
 "status": "Fail",
 "type": "assert_all_flows_succeed"
 }
 ],
 "summary": "1 of 1 assertions failed"
 }

在实时网络中,我们可以检查实时节点上的路由,以验证我们从 Batfish 得出的发现:

dc1-spine01#sh ip route 10.100.1.253

VRF: default
Codes: C - connected, S - static, K - kernel,
 O - OSPF, IA - OSPF inter area, E1 - OSPF external type 1,
 E2 - OSPF external type 2, N1 - OSPF NSSA external type 1,
 N2 - OSPF NSSA external type2, B I - iBGP, B E - eBGP,
 R - RIP, I L1 - IS-IS level 1, I L2 - IS-IS level 2,
 O3 - OSPFv3, A B - BGP Aggregate, A O - OSPF Summary,
 NG - Nexthop Group Static Route, V - VXLAN Control Service,
 DH - Dhcp client installed default route

Gateway of last resort is not set

在检查我们的网络配置后,我们可以看到前面的输出是正确的。这是可能的,因为我们在所有leaf交换机上使用路由映射,只广播本地环回 IP 地址,并且我们不会重新广播来自leaf节点的任何其他 IP 地址。

此外,spine节点之间没有 BGP 会话,因此它们之间没有流量路径。因此,为了完成我们的测试并使其成功,我们将仅测试所有源自leaf节点到所有目的地的流量。

我们不会测试源自spine节点的流量。在这里,您可以看到修改后的任务:

- bf_assert:

    assertions:
      - type: assert_all_flows_succeed
        name: confirm host is reachable for traffic received
        parameters:
          startLocation: "{{ item.0 }}"
          headers:
            dstIps: "{{ item.1.value.ip }}"
            srcIps: "{{ lo_ip[item.0].ip }}"
    with_nested:
      - "{{ play_hosts }}"
      - "{{ lo_ip | dict2items }}"
    when: '"spine" not in item.0'
    run_once: yes

再次运行测试后,所有流量都通过了,任务成功。

使用 Batfish 验证 ACL

在本教程中,我们将概述如何使用 Batfish 验证 ACL 条目,并验证这些 ACL 定义的正确流量处理。这使我们能够将 Batfish 和 Ansible 作为审计工具,强制执行基础设施的正确安全合规性。

准备工作

设备配置已生成,并且网络快照已打包,如前面的教程所述。

操作步骤…

  1. 使用以下 ACL 条目更新leaf03leaf04上的网络配置以保护 Web 虚拟局域网VLAN):
!
ip access-list WEB_VLAN_IN
 10 deny ip host 172.20.10.10 any
 20 permit tcp 172.20.10.0/24 any eq https

!
ip access-list WEB_VLAN_OUT
 10 permit tcp any 172.20.10.0/24 eq https
!
  1. 使用以下任务更新pb_batfish_analyis.yml playbook,以验证我们 Web VLAN 的正确出口 ACL 行为:
- name: Validate Internet to Web Servers
 bf_assert:
 assertions:
 - type: assert_filter_permits
 name: Confirm Internet Access to Web Servers
 parameters:
 filters: "{{ web_acl }}"
 headers:
 dstIps: "{{ web_server_subnet}}"
 srcIps: "0.0.0.0/0"
 dstPorts: '443'
 ipProtocols: 'TCP'
 vars:
 web_acl: WEB_VLAN_OUT
 web_server_subnet: 172.20.10.0/24
 run_once: yes
  1. 使用以下任务更新pb_batfish_analyis.yml playbook,以验证我们 VLAN 的正确入口 ACL 行为:
- name: Validate Server {{ web_server }} is Denied
 bf_assert:
 assertions:
 - type: assert_filter_denies
 name: Confirm Traffic is Denied
 parameters:
 filters: "{{ web_acl_in }}"
 headers:
 dstIps: "0.0.0.0/0"
 srcIps: "{{ web_server}}"
 vars:
 web_acl_in: WEB_VLAN_IN
 web_server: 172.20.10.10
 run_once: yes

工作原理…

Batfish 是另一个用于验证 ACL 处理流量的强大工具。这使我们能够验证特定 ACL 是否允许或拒绝特定流量。Batfish 还提供了一个强大的工具,用于验证涉及 ACL 的网络更改。此外,它可以用作防范实施可能影响网络上的实时流量或导致违反安全策略的恶意 ACL 更改的保障。

我们再次使用bf_assert Batfish 模块,但是在这种情况下,用于验证 ACL。我们使用了该模块中实现的另外两个assert方法,如下所示:

  • assert_filter_permits方法测试并验证 ACL 正确允许特定流量。

  • assert_filter_denies方法测试并验证 ACL 拒绝特定流量。

在我们的 playbook 中,我们创建了两个单独的任务。第一个任务使用assert_filter_permits方法验证从互联网到我们的 Web 服务器子网的所有流量是否被允许。我们使用headers参数来指定要验证的所有流量的 IP 头信息。

然后,我们创建第二个任务,使用assert_filter_denies方法,测试特定 Web 服务器是否被阻止与任何目的地通信。

当我们再次运行我们的 playbook 时,我们可以看到所有任务都成功完成,这表明我们示例网络中 ACL 的行为符合预期。

为了验证我们的过滤器是否工作正常,我们将通过允许超文本传输安全协议HTTPS)流量到被拒绝的 Web 服务器(172.20.10.10)来引入 ACL 过滤器的问题,如下面的代码片段所示:

!
ip access-list WEB_VLAN_IN
 05 permit tcp host 172.20.10.10 any eq ssh
 10 deny ip host 172.20.10.10 any
 20 permit tcp 172.20.10.0/24 any eq https
!

当我们再次运行我们的 playbook 时,我们可以看到最后一个任务出现错误。这个错误显示特定流量流被允许,而预期应该被 ACL 拒绝,如下面的代码块所示:

 "result": [
 {
 "details": "Found a flow that was permitted, when expected to be denied\n[{'Node': 'leaf03', 'Filter_Name': 'WEB_VLAN_IN', 'Flow': Flow(dscp=0, dstIp='0.0.0.0', dstPort=22, ecn=0, fragmentOffset=0, icmpCode=0, icmpVar=0, ingressInterface=None, ingressNode='leaf03', ingressVrf='default', ipProtocol='TCP', packetLength=0, srcIp='172.20.10.10', srcPort=0, state='NEW', tag='BASE', tcpFlagsAck=0, tcpFlagsCwr=0, tcpFlagsEce=0, tcpFlagsFin=0, tcpFlagsPsh=0, tcpFlagsRst=0, tcpFlagsSyn=0, tcpFlagsUrg=0), 'Action': 'PERMIT', 'Line_Content': '05 permit tcp host 172.20.10.10 any eq ssh', 'Trace': AclTrace(events=[AclTraceEvent(class_name='org.batfish.datamodel.acl.PermittedByIpAccessListLine', description='Flow permitted by extended ipv4 access-list named WEB_VLAN_IN, index 0: 05 permit tcp host 172.20.10.10 any eq ssh', lineDescription='05 permit tcp host 172.20.10.10 any eq ssh')])}]",
 "name": "Confirm Traffic is Denied",
 "status": "Fail",
 "type": "assert_filter_denies"
 }
 ]

这个简单的例子表明,我们可以创建更复杂的断言规则,以强制执行网络内的正确安全策略。此外,我们可以利用 Batfish 来验证网络范围内该策略的正确执行。

第十一章:使用 Ansible 和 NetBox 构建网络库存

在本书的前几章中,我们使用存储在 YAML 文件中的 Ansible 变量描述了网络基础设施。虽然这种方法完全可接受,但对于在整个组织中采用自动化来说并非最佳解决方案。我们需要将我们的网络库存、IP 地址和 VLAN 放在一个中央系统中,这将作为我们网络的真相来源。该系统应具有强大而强大的 API,其他自动化和 OSS/BSS 系统可以查询该 API 以检索和更新网络库存。

NetBox 是一个用于网络基础设施的开源库存系统,最初由 DigitalOcean 的网络工程团队开发,用于记录他们的数据中心基础设施。它是一个简单但功能强大且高度可扩展的库存系统,可以作为关于我们网络的真相来源。它允许我们记录和描述任何网络基础设施上的以下功能:

  • IP 地址管理(IPAM):IP 网络和地址、VRF 和 VLAN

  • 设备机架:按组和站点组织

  • 设备:设备类型和安装位置

  • 连接:设备之间的网络、控制台和电源连接

  • 虚拟化:虚拟机和集群

  • 数据电路:长途通信电路和供应商

  • 秘密:敏感凭据的加密存储

NetBox 是一个基于 Django 的 Python 应用程序,使用 PostgreSQL 作为后端数据存储和 NGINX 作为前端 Web 服务器,以及其他可选组件一起运行以提供 NetBox 系统。它有一个强大的 REST API,可以用于检索或更新 NetBox 数据库中存储的数据。

在本章中,我们将概述 Ansible 和 NetBox 之间集成的以下三个主要用例:

  • Ansible 可用于在 NetBox 中填充各种类型的网络信息,例如站点、设备和 IP 地址。以下图表概述了在这种用例中 Ansible 和 NetBox 之间的高级集成:

  • NetBox 可以作为 Ansible 的动态清单来源,用于检索和构建 Ansible 清单。以下图表概述了这种集成:

  • NetBox 可以作为 Ansible 所需的数据信息的来源,用于配置和配置网络设备。以下图表概述了这种用例:

我们将使用由两个数据中心站点组成的示例网络,每个站点都有脊柱或叶子结构。我们将对所有信息进行建模并填充到 NetBox 中。以下表格捕捉了这个示例网络基础设施:

站点 设备 角色
DC1 dc1-spine01 脊柱交换机
DC1 dc1-spine02 脊柱交换机
DC1 dc1-leaf01 叶子交换机
DC1 dc1-leaf02 叶子交换机
DC2 dc2-spine01 脊柱交换机
DC2 dc2-spine02 脊柱交换机
DC2 dc2-leaf01 叶子交换机
DC2 dc2-leaf02 叶子交换机

本章涵盖的主要内容如下:

  • 安装 NetBox

  • 将 NetBox 与 Ansible 集成

  • 在 NetBox 中填充站点

  • 在 NetBox 中填充设备

  • 在 NetBox 中填充接口

  • 在 NetBox 中填充 IP 地址

  • 在 NetBox 中填充 IP 前缀

  • 使用 NetBox 作为 Ansible 的动态清单来源

  • 使用 NetBox 数据生成配置

技术要求

本章中使用的所有代码都可以在以下 GitHub 存储库中找到:

github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch11_netbox

本章基于以下软件版本发布:

  • 运行 CentOS 7 的 Ansible 机器

  • Ansible 2.9

  • Python 3.6.8

  • Arista vEOS 运行 EOS 4.20.1F

  • NetBox v2.6.5 在 CentOS 7 Linux 机器上运行

安装 NetBox

在这个配方中,我们将概述如何使用 Docker 容器安装 NetBox,以及如何启动所有必需的容器,以使 NetBox 服务器正常运行。使用 Docker 容器安装 NetBox 是最简单的入门方式。

准备工作

为了在 Linux 机器上开始安装 NetBox,机器需要具有互联网连接,以从 Docker Hub 拉取 NetBox 操作所需的 Docker 镜像。

如何操作…

  1. 使用以下 URL 在您的 CentOS Linux 机器上安装 Docker:

docs.docker.com/install/linux/docker-ce/centos/

  1. 使用以下 URL 安装 Docker Compose:

docs.docker.com/compose/install/

  1. 将 NetBox 存储库克隆到一个新目录(netbox_src)中,如下所示:
$ git clone [`github.com/netbox-community/netbox-docker.git`](https://github.com/netbox-community/netbox-docker.git) netbox_src
  1. 切换到netbox_src目录,并使用docker-compose拉取所有必需的 Docker 镜像,如下所示:
$ cd netbox_src
$ /usr/local/bin/docker-compose pull
  1. 更新docker-compose.yml文件,设置 NGINX Web 服务器的正确端口:
$ cat docker-compose.yml
 ß--- Output Omitted for brevity -->
 nginx:
 command: nginx -c /etc/netbox-nginx/nginx.conf
 image: nginx:1.17-alpine
 depends_on:
 - netbox
 ports:
 - 80:8080  >>  # This will make NGINX listen on port 80 on the host machine
  1. 启动所有 Docker 容器,如下所示:
$ /usr/local/bin/docker-compose up -d 

工作原理…

如本章介绍的,NetBox 由多个服务组成,这些服务集成在一起以提供所需的 NetBox 应用程序。使用 Docker 容器的最简单安装方法是使用 Docker 容器。我们使用一个docker-compose定义文件来描述不同 Docker 容器之间的交互,以提供 NetBox 应用程序。以下图表概述了 NetBox 的高级架构,以及每个服务如何在自己的容器中运行:

在这个配方中,我们描述了使用 Docker 和docker-compose安装 NetBox 所需的步骤,这大大简化了导致 NetBox 服务器正常运行的安装步骤。NetBox 背后的开发人员创建了运行 NetBox 所需的 Docker 镜像,并使用docker-compose文件描述了不同 NetBox 组件之间的整体交互,以建立 NetBox 服务器。所有 NetBox 设置说明,以及使用 Docker 容器构建和部署 NetBox 的 Docker 文件和docker-compose文件,都可以在github.com/netbox-community/netbox-docker找到。

在我们的 Linux 机器上安装了 Docker 和docker-compose后,我们克隆了 GitHub 存储库,并编辑了docker-compose.yml文件,以设置 NGINX Web 服务器在主机上监听的端口。最后,我们运行了docker-compose pull命令,下载了docker-compose.yml文件中定义的所有 Docker 容器,并运行了docker-compose来启动所有 Docker 容器。

一旦所有 Docker 容器都被下载并启动,我们可以在https://<netbox-server-ip>/访问 NetBox。

这将带我们到以下页面:

默认用户名是admin,密码是admin

有更多

为了简化 NetBox 的安装,我在本章的代码中创建了一个 Ansible 角色来部署 NetBox。要使用这个角色,我们需要执行以下步骤:

  1. 在 Ansible 控制机上,克隆以下章节代码:
git clone git@github.com:PacktPublishing/Network-Automation-Cookbook.git
  1. 使用正确的 IP 地址更新hosts文件,用于您的 NetBox 服务器:
$ cat hosts
< --- Output omitted for bevitry --- > 
[netbox]
netbox  ansible_host=172.20.100.111
  1. 运行pb_deploy_netbox.yml Ansible playbook:
$ ansible-playbook pb_deploy_netbox.yml

另请参阅…

有关如何使用 Docker 容器安装 NetBox 的更多信息,请访问github.com/netbox-community/netbox-docker

将 NetBox 与 Ansible 集成

在这个步骤中,我们将概述如何通过 NetBox API 集成 Ansible 和 NetBox。这种集成是强制性的,因为它将允许我们通过 Ansible playbook 填充 NetBox 数据库,并且在后续的步骤中使用 NetBox 作为我们的动态清单源来创建 Ansible 清单。

准备工作

NetBox 应按照上一个步骤中的说明进行安装,并且 IP 需要在 Ansible 控制机和 NetBox 服务器之间进行延伸。Ansible 将通过端口 80 与 NetBox 通信,因此 NetBox 服务器上需要打开此端口。

如何做...

  1. 在 Ansible 控制机上安装pynetboxPython 包:
$ sudo pip3 install pynetbox
  1. 使用管理员用户详细信息登录到 NetBox 服务器,然后单击“管理”选项卡创建一个新用户,如下所示:

  1. 创建一个新用户并设置其用户名和密码:

  1. 为这个新用户分配超级用户权限,以便您可以写入 NetBox 数据库:

  1. 为这个新用户创建一个新的令牌:

  1. 转到令牌屏幕,找到我们为 Ansible 用户创建的新令牌:

  1. ch11_netbox项目目录中,创建我们的hostsAnsible 清单文件,如下所示:
$ cat hosts
[dc1]
dc1-spine01     ansible_host=172.20.1.41
dc1-spine02     ansible_host=172.20.1.42dc1-leaf01      ansible_host=172.20.1.35
dc1-leaf02      ansible_host=172.20.1.3

[dc2]
dc2-spine01     ansible_host=172.20.2.41dc2-spine02     ansible_host=172.20.2.42dc2-leaf01      ansible_host=172.20.2.35
dc2-leaf02      ansible
host=172.20.2.36

[leaf]
dc[1:2]-leaf0[1:2]

[spine]
dc[1:2]-spine0[1:2]
  1. 创建group_vars文件夹和all.yml文件,并填充文件,如下所示:
---
netbox_url: http://172.20.100.111
netbox_token: 08be88e25b23ca40a9338d66518bd57de69d4305

如何工作...

在这个步骤中,我们正在设置 Ansible 和 NetBox 之间的集成。为了开始使用 Ansible 模块填充 NetBox 数据库,我们安装了pynetboxPython 模块。这个模块对于我们在本章中将要使用的所有 NetBox Ansible 模块是必需的。

在 NetBox 网站上,我们首先创建了一个具有完整管理员权限的新用户。这授予了用户在 NetBox 数据库中创建、编辑或删除任何对象的全部权限。然后,我们创建了一个令牌,该令牌将用于验证来自 Ansible 到 NetBox 的所有 API 请求。

最后,我们创建了我们的 Ansible 清单,并在 Ansible 变量中声明了两个参数,netbox_urlnetbox_token,用于保存 API 端点和 NetBox 上 Ansible 用户的令牌。

另请参阅...

有关与 NetBox 交互的pynetboxPython 库的更多信息,请访问pynetbox.readthedocs.io/en/latest/

在 NetBox 中填充站点

在这个步骤中,我们将概述如何在 NetBox 中创建站点。站点是 NetBox 中的逻辑构造,允许我们根据它们的物理位置对基础设施进行分组。在我们开始声明设备并将它们放置在这些站点之前,我们需要定义我们的站点。

准备工作

确保按照上一个步骤中的说明,建立 Ansible 和 NetBox 之间的集成。

如何做...

  1. 更新group_vars/all.yml文件,包含关于我们物理站点的以下数据:
sites:
 - name: DC1
 description: "Main Data Center in Sydney"
 location: Sydney
 - name: DC2
 description: "Main Data Center in KSA"
 location: Riyadh
  1. ch11_netbox下创建一个新的roles目录。

  2. 创建一个名为build_netbox_db的新的 Ansible 角色,并填充tasks/main.yml文件,如下所示:

$ cat roles/build_netbox_db/tasks/main.yml
---
- name: Create NetBox Sites
 netbox_site:
 netbox_token: "{{ netbox_token }}"
 netbox_url: "{{ netbox_url }}"
 data:
 name: "{{ item.name | lower }}"
 description: "{{ item.description | default(omit) }}"
 physical_address: "{{ item.location | default(omit) }}"
 state: "{{ netbox_state }}"
 loop: "{{ sites }}"
 run_once: yes
 tags: netbox_sites
  1. 更新defaults/main.yml文件,添加以下数据:
$ cat roles/build_netbox_db/defaults/main.yml
---
netbox_state: present
  1. 创建一个名为pb_build_netbox_db.yml的新 playbook,并添加以下内容:
$ cat pb_build_netbox_db.yml
---
- name: Populate NetBox DataBase
 hosts: all
 gather_facts: no
 vars:
 ansible_connection: local
 tasks:
 - import_role:
 name: build_netbox_db

它是如何工作的...

在这个步骤中,我们首先填充了样本网络中的站点,并在group_vars下的all.yml文件中定义了sites数据结构,描述了我们数据中心的物理位置。我们创建了一个 Ansible 角色,以便填充 NetBox 数据库,并且我们在这个角色中执行的第一个任务是使用netbox_site模块在我们的网络中创建所有站点。我们循环遍历了sites数据结构中定义的所有站点,并使用netbox_site模块将数据推送到 NetBox。

我们创建了一个新的 playbook,这将是我们的主 playbook,用来将我们的网络库存内容填充到 NetBox 中,并引用了我们创建的角色,以便开始执行该角色内的所有任务。

一旦我们运行这个 playbook,站点就会在 NetBox 中填充,如下所示:

另请参阅…

有关netbox_site模块的更多信息,请访问docs.ansible.com/ansible/latest/modules/netbox_site_module.html

在 NetBox 中填充设备

在这个配方中,我们将概述如何在 NetBox 中创建和填充网络设备。这将包括声明设备型号和制造商,以及它们在我们网络中的角色。这将帮助我们建立我们网络基础设施的准确清单,我们可以在本章的最后一个配方中使用 NetBox 构建 Ansible 的动态清单。

准备工作

Ansible 和 NetBox 的集成应该已经就位,并且站点应该在 NetBox 中定义和填充,如前一篇中所述。这是至关重要的,因为当我们开始在 NetBox 中填充设备时,我们需要将它们与现有站点联系起来。

如何做…

  1. 更新group_vars/all.yml文件,包括devices信息,如下所示:
$ cat group_vars/all.yml

 < --- Output Omitted for brevity --- >

 devices:
 - role: Leaf_Switch
 type: 7020SR
 vendor: Arista
 color: 'f44336'  # red
 - role: Spine_Switch
 type: 7050CX3
 ru: 2
 vendor: Arista
 color: '2196f3'  # blue
  1. 创建group_vars/leaf.ymlgroup_vars/spine.yml文件,然后用以下信息更新它们:
$ cat group_vars/leaf.yml

---
device_model: 7020SR
device_role: Leaf_Switch
vendor: Arista
$ cat group_vars/spine.yml
---
device_model: 7050CX3
device_role: Spine_Switch
vendor: Arista
  1. 创建一个新任务,为我们库存中的所有设备创建制造商,放在tasks/create_device_vendors.yml文件中,如下所示:
$ cat roles/build_netbox_db/tasks/create_device_vendors.yml

- name: NetBox Device  // Get Existing Vendors
 uri: url: "{{ netbox_url }}/api/dcim/manufacturers/?name={{ device }}" method: GET headers: Authorization: "Token {{ netbox_token }}" Accept: 'application/json' return_content: yes body_format: json status_code: [200, 201] register: netbox_vendors run_once: yes tags: device_vendors - name: NetBox Device  // Create Device Vendors
 uri: url: "{{ netbox_url }}/api/dcim/manufacturers/" method: POST headers: Authorization: "Token {{ netbox_token }}" Accept: 'application/json' return_content: yes body_format: json body: name: "{{ device }}" slug: "{{ device | lower }}" status_code: [200, 201] when: - netbox_vendors.json.count == 0 - netbox_state == 'present' run_once: yes tags: device_vendors
  1. 更新tasks/main.yml文件,包括create_device_vendors.yml文件,如下所示:
$ cat roles/build_netbox_db/tasks/main.yml < --- Output Omitted for brevity --- > - name: Create NetBox Device Vendors
 include_tasks: create_device_vendors.yml loop: "{{ devices | map(attribute='vendor') | list | unique}}" loop_control: loop_var: device run_once: yes tags: device_vendors
  1. 创建一个新任务,为我们库存中的所有网络设备创建所有设备型号,放在tasks/create_device_types.yml文件中,如下所示:
$ cat roles/build_netbox_db/tasks/create_device_types.yml - name: NetBox Device  // Get Existing Device Types
 uri: url: "{{ netbox_url }}/api/dcim/device-types/?model={{ device.type }}" method: GET headers: Authorization: "Token {{ netbox_token }}" Accept: 'application/json' return_content: yes body_format: json status_code: [200, 201] register: netbox_device_types run_once: yes tags: device_types - name: NetBox Device  // Create New Device Types
 uri: url: "{{ netbox_url }}/api/dcim/device-types/" method: POST headers: Authorization: "Token {{ netbox_token }}" Accept: 'application/json' return_content: yes body_format: json body: model: "{{ device.type }}" manufacturer: { name: "{{ device.vendor }}"} slug: "{{ device.type | regex_replace('-','_') | lower  }}" u_height: "{{ device.ru | default(1) }}" status_code: [200, 201] when: - netbox_device_types.json.count == 0 - netbox_state != 'absent' register: netbox_device_types run_once: yes tags: device_types
  1. 更新tasks/main.yml文件,包括create_device_types.yml文件,如下所示:
$ cat roles/build_netbox_db/tasks/main.yml
< --- Output Omitted for brevity --- >
- name: Create NetBox Device Types
 include_tasks: create_device_types.yml
 loop: "{{ devices }}"
 loop_control:
 loop_var: device
 run_once: yes
 tags: device_types
  1. 创建一个新任务,为我们库存中的所有网络设备创建所有设备角色,放在tasks/create_device_roles.yml文件中,如下所示:
$ cat roles/build_netbox_db/tasks/create_device_roles.yml - name: NetBox Device  // Get Existing Device Roles
 uri: url: "{{ netbox_url }}/api/dcim/device-roles/?name={{ device.role}}" method: GET headers: Authorization: "Token {{ netbox_token }}" Accept: 'application/json' return_content: yes body_format: json status_code: [200, 201] register: netbox_device_role tags: device_roles - name: NetBox Device  // Create New Device Roles
 uri: url: "{{ netbox_url }}/api/dcim/device-roles/" method: POST headers: Authorization: "Token {{ netbox_token }}" Accept: 'application/json' return_content: yes body_format: json body: name: "{{ device.role }}" slug: "{{ device.role | lower }}" color: "{{ device.color }}" status_code: [200, 201] when: - netbox_device_role.json.count == 0 - netbox_state != 'absent' register: netbox_device_role tags: device_roles
  1. 更新tasks/main.yml文件,包括create_device_roles.yml文件,如下所示:
$ cat roles/build_netbox_db/tasks/main.yml < --- Output Omitted for brevity --- > - name: Create NetBox Device Roles
 include_tasks: create_device_roles.yml loop: "{{ devices }}" loop_control: loop_var: device run_once: yes tags: device_roles
  1. 创建一个新任务,将我们库存中的所有设备填充到tasks/create_device.yml文件中,如下所示:
---
- name: Provision NetBox Devices
 netbox_device:
 data:
 name: "{{ inventory_hostname }}"
 device_role: "{{ device_role }}"
 device_type: "{{ device_model }}"
 status: Active
 site: "{{ inventory_hostname.split('-')[0] }}"
 netbox_token: "{{ netbox_token }}"
 netbox_url: "{{ netbox_url }}"
 state: "{{ netbox_state }}"
 register: netbox_device
 tags: netbox_devices
  1. 更新tasks/main.yml文件,包括create_device.yml文件,如下所示:
$ cat roles/build_netbox_db/tasks/main.yml < --- Output Omitted for brevity --- > - name: Create NetBox Device
 include_tasks: create_device.yml tags: netbox_devices

它是如何工作的…

为了在 NetBox 中填充我们的网络设备,我们首先需要填充与 NetBox 中设备相关的以下参数:

  • 我们所有网络设备的所有制造商

  • 我们的网络设备的所有设备型号

  • 将分配给每个网络设备的所有设备角色

Ansible 中没有预先构建的模块可以填充所有这些信息并在 NetBox 中构建这些对象。因此,为了在 NetBox 中填充这些信息,我们需要使用URI模块,它允许我们触发 REST API 调用到每个对象的正确 API 端点。要执行所有这些任务,请按照以下步骤进行:

  1. 首先,使用GET方法查询 API 端点,以在 NetBox DB 中获取匹配的对象。

  2. 如果对象不存在,我们可以使用POST REST调用并提供必要的数据来创建一个。

  3. 如果对象已经存在,我们可以跳过前面的步骤。

使用先前的方法,我们正在模拟 Ansible 模块的幂等性特性。当我们运行我们的 playbook 时,我们可以看到所有的设备类型都已经填充到了 NetBox 中:

此外,我们设备的所有设备角色都已经填充,如下所示:

一旦我们已经构建了在 NetBox 中定义设备所需的所有对象(如设备角色和设备类型),我们可以使用netbox_device Ansible 内置模块在我们的 Ansible 库存中创建所有设备。以下截图概述了在 NetBox 数据库中正确填充的所有设备:

在这个教程中,我们使用URI模块触发 API 调用到 NetBox API,以便在其数据库中创建对象。为了更多了解可用的 API 以及每个 API 调用需要传递哪些参数,我们需要查看 NetBox 的 API 文档。API 的文档包含在 NetBox 安装中,可以通过http:///api/docs/访问。

另请参阅...

在 NetBox 中填充接口

在这个教程中,我们将概述如何在 NetBox 中填充网络设备的接口。这为我们提供了设备的完整清单,并将允许我们为网络设备上的每个接口分配 IP 地址,以及对我们的网络中的网络链接进行建模。

准备工作

为了创建网络接口,设备需要已经在之前的教程中创建好。

如何做…

  1. 更新group_vars/all.yml文件,包括每个数据中心网络布局中的点对点链接,如下所示:
p2p_ip:
  dc1-leaf01:
    - {port: Ethernet8, ip: 172.10.1.1/31, peer: dc1-spine01, pport: Ethernet1,
peer_ip: 172.10.1.0/31}
    - {port: Ethernet9, ip: 172.10.1.5/31, peer: dc1-spine02, pport: Ethernet1,
peer_ip: 172.10.1.4/31}
< --- Output Omitted for brevity --- >
  dc2-leaf01:
    - {port: Ethernet8, ip: 172.11.1.1/31, peer: dc2-spine01, pport: Ethernet1, peer_ip: 172.11.1.0/31}
    - {port: Ethernet9, ip: 172.11.1.5/31, peer: dc2-spine02, pport: Ethernet1, peer_ip: 172.11.1.4/31}
  1. 创建一个新任务来为我们库存中所有网络设备创建所有接口,在tasks/create_device_intf.yml文件中进行,如下所示:
$ cat roles/build_netbox_db/tasks/create_device_intf.yml --- - name: Create Fabric Interfaces on Devices
 netbox_interface: netbox_token: "{{ netbox_token }}" netbox_url: "{{ netbox_url }}" data: device: "{{ inventory_hostname }}" name: "{{ item.port }}" description: "{{ item.type | default('CORE') }} | {{ item.peer }}| {{
item.pport }}" enabled: true mode: Access state: "{{ netbox_state }}" loop: "{{ p2p_ip[inventory_hostname] }}" when: p2p_ip is defined tags: netbox_intfs
  1. 更新tasks/main.yml文件,包括create_device_intfs.yml文件,如下所示:
$ cat roles/build_netbox_db/tasks/main.yml
< --- Output Omitted for brevity --- >
- name: Create NetBox Device Interfaces  include_tasks: create_device_intf.yml
 tags: netbox_intfs

它是如何工作的...

为了填充我们数据中心布局中的所有点对点接口,我们首先创建了p2p_ip数据结构,其中包含建模这些点对点链接所需的所有参数。然后我们使用netbox_interface模块在 NetBox 中创建了所有这些链接。使用相同的模块并遵循完全相同的流程,我们可以在网络设备上建模管理(带外管理)和环回接口。

以下截图显示了 NetBox 中一个设备的接口以及接口的填充情况:

另请参阅...

有关用于在 NetBox 上创建接口的 Ansible 模块的更多信息,请访问docs.ansible.com/ansible/latest/modules/netbox_interface_module.html#netbox-interface-module

在 NetBox 中填充 IP 地址

在这个教程中,我们将概述如何在 NetBox 中创建 IP 地址,以及如何将这些地址绑定到每个网络设备的接口上。

准备工作

我们库存中每个设备上的网络接口需要在 NetBox 中定义和填充,如前一篇文章所述。

如何做…

  1. 创建一个新任务来创建所有连接到网络接口的 IP 地址。这是针对我们库存中所有网络设备在tasks/create_device_intf_ip.yml文件中进行的,如下所示:
$ cat roles/build_netbox_db/tasks/create_device_intf.yml
---
- name: Create Fabric IPs
 netbox_ip_address:
 netbox_token: "{{ netbox_token }}"
 netbox_url: "{{ netbox_url }}"
 data:
 address: "{{ item.ip }}"
 interface:
 name: "{{ item.port }}"
 device: "{{ inventory_hostname }}"
 state: "{{ netbox_state }}"
 loop: "{{ p2p_ip[inventory_hostname] }}"
 tags: netbox_ip
  1. 更新tasks/main.yml文件,包括create_device_intf_ip.yml文件,如下所示:
$ cat roles/build_netbox_db/tasks/main.yml < --- Output Omitted for brevity --- - name: Create NetBox Device Interfaces IP Address
 include_tasks: create_device_intf_ip.yml tags: netbox_ip

它是如何工作的...

为了填充数据中心布线中使用的所有点对点 IP 地址,我们在p2p_ip数据结构中捕获了这些信息,该数据结构包含了我们数据中心布线中每个接口上分配的所有 IP 地址。我们使用netbox_ip_address模块循环遍历这个数据结构,并填充数据中心布线中每个设备上每个接口分配的所有 IP 地址。管理和环回接口也是同样的过程。

以下屏幕截图显示了我们的设备(dc1-leaf01)中接口分配的 IP 地址:

另请参阅...

有关用于在 NetBox 上创建 IP 地址的 Ansible 模块的更多信息,请访问docs.ansible.com/ansible/latest/modules/netbox_ip_address_module.html#netbox-ip-address-module

在 NetBox 中填充 IP 前缀

在这个示例中,我们将看看如何在 NetBox 中创建 IP 前缀。这使我们能够利用 NetBox 作为我们的 IPAM 解决方案,管理网络中的 IP 地址分配。

准备工作

在填充 IP 子网或前缀到 NetBox 时,不需要特定的要求,只要我们不将这些前缀绑定到特定站点。如果我们将一些子网绑定到特定站点,那么这些站点需要在分配之前在 NetBox 中定义。

如何做...

  1. 更新group_vars/all.yml文件,包括 IP 前缀信息,如下:
$ cat group_vars/all.yml

 < --- Output Omitted for brevity --- >
 subnets:
 -   prefix: 172.10.1.0/24
 role: p2p_subnet
 site: dc1
 -   prefix: 172.11.1.0/24
 role: p2p_subnet
 site: dc2
 -   prefix: 10.100.1.0/24
 role: loopback_subnet
 site: dc1
 -   prefix: 10.100.2.0/24
 role: loopback_subnet
 site: dc2
 -   prefix: 172.20.1.0/24
 role: oob_mgmt_subnet
 site: dc1
 -   prefix: 172.20.2.0/24
 role: oob_mgmt_subnet
 site: dc2
  1. 更新我们角色定义中的tasks/main.yml文件,包括以下任务:
$ cat roles/build_netbox_db/tasks/main.yml < --- Output Omitted for brevity --- > - name: Create IP Prefixes
 netbox_prefix: netbox_token: "{{ netbox_token }}" netbox_url: "{{ netbox_url }}" data: prefix: "{{ item.prefix }}" site: "{{ item.site | default(omit) }}" status: Active state: "{{ netbox_state }}" loop: "{{ subnets }}" loop_control: label: "{{ item.prefix }}" run_once: yes tags: netbox_prefix

工作原理...

我们在group_vars/all.yml文件中定义了我们的子网,位于subnets数据结构下,然后使用netbox_prefix模块循环遍历这个数据结构,并在 NetBox 中填充前缀。

以下屏幕截图显示了 NetBox 中填充的前缀及其各自的利用率:

另请参阅...

有关用于在 NetBox 上创建 IP 前缀的 Ansible 模块的更多信息,请访问docs.ansible.com/ansible/latest/modules/netbox_prefix_module.html#netbox-prefix-module

将 NetBox 用作 Ansible 的动态清单来源

在这个示例中,我们将概述如何将 NetBox 用作动态清单来源。通过这种方法,NetBox 将拥有我们网络基础设施的清单,我们将使用可用的不同分组(如站点、设备角色等)来为 Ansible 构建一个动态清单,并根据 NetBox 对它们进行分组。

准备工作

NetBox 和 Ansible 之间的集成需要按照前面的示例中所述的方式进行。

如何做...

  1. 在您的主目录中,创建一个名为netbox_dynamic_inventory的新文件夹。

  2. 在这个新目录中,创建一个名为netbox_inventory_source.yml的新的 YAML 文件,内容如下:

$ cat netbox_dynamic_inventory/netbox_inventory_source.yml --- plugin: netbox api_endpoint: http://172.20.100.111 token: 08be88e25b23ca40a9338d66518bd57de69d4305 group_by:
 - device_roles - sites
  1. 创建一个名为pb_create_report.yml的新 playbook,内容如下:
$ cat netbox_dynamic_inventory/pb_create_report.yml

--- - name: Create Report from Netbox Data
 hosts: all gather_facts: no connection: local tasks: - name: Build Report blockinfile: block: | netbox_data: {% for node in play_hosts %} - { node: {{ node }} , type: {{ hostvars[node].device_types[0] }} , mgmt_ip: {{ hostvars[node].ansible_host }} } {% endfor %} path: ./netbox_report.yaml create: yes delegate_to: localhost run_once: yes

工作原理...

到目前为止,在本书中我们所概述的所有示例和示例中,我们都使用了一个静态清单文件(在大多数情况下是hosts),在那里我们定义了我们的清单,Ansible 在执行我们的 playbook 之前会解析它。在这个示例中,我们将使用不同的清单来源:动态清单。在这种情况下,我们没有一个保存我们清单的静态文件,但是我们将在执行时动态构建我们的清单。在这个示例中,我们的所有清单都在 NetBox 中维护,我们已经将 NetBox 用作我们的清单来源。

对于 Ansible 来说,要使用动态清单源,必须有一个插件来与清单源通信,以检索我们的清单和与之相关的任何变量。从版本 2.9 开始,Ansible 引入了 NetBox 作为可以用作清单源的插件。为了使用这个插件,我们需要定义一个 YAML 文件,概述 Ansible 与 NetBox API 通信所需的不同参数。强制性参数如下:

  • 插件名称:在我们的情况下,是NetBox

  • **Api_endpoint**:我们的 NetBox 服务器的 API 端点

  • 令牌:我们创建的用于在 Ansible 和我们的 NetBox 服务器之间建立通信的身份验证令牌

在 YAML 声明文件中,我们可以指定如何对来自 NetBox 的清单进行分组。我们可以使用group_by属性来概述我们将用于分组基础设施的参数。在我们的情况下,我们使用device_rolessites来分组我们的基础设施。

还有更多

我们可以通过执行以下命令来测试我们的动态清单,以查看 Ansible 如何生成清单:

$ ansible-inventory --list -i netbox_inventory_source.yml

以下是前述命令的输出片段。它概述了从 NetBox 检索的单个设备的主机变量:

{
 "_meta": { "hostvars": { "dc1-leaf01": { "ansible_host": "172.20.1.35", "device_roles": [ "Leaf_Switch" ], "device_types": [ "7020SR" ], "manufacturers": [ "Arista" ], "primary_ip4": "172.20.1.35", "sites": [ "dc1" ] },

以下代码片段显示了 Ansible 基于 NetBox 的分组构建的组:

    "all": {
        "children": [
            "device_roles_Leaf_Switch",
            "device_roles_Spine_Switch",
            "sites_dc1",
            "sites_dc2",
            "ungrouped"
        ]
    },
    "device_roles_Leaf_Switch": {
        "hosts": [
            "dc1-leaf01",
            "dc1-leaf02",
            "dc2-leaf01",
            "dc2-leaf02"
        ]
    },

我们已经创建了一个新的剧本来测试 Ansible 和 NetBox 之间的集成,并确保我们可以使用从 NetBox 检索的数据作为动态清单源。使用我们的新剧本,我们可以为 NetBox 动态清单中的每个设备创建一个简单的报告,以及从 NetBox 发送的一些参数。

当我们运行剧本时,我们会得到以下报告:

$ ansible-playbook pb_create_report.yml -i netbox_inventory_source.yml
$ cat netbox_report.yml
# BEGIN ANSIBLE MANAGED BLOCK
netbox_data:
 - { node: dc1-leaf01 , type: 7020SR , mgmt_ip: 172.20.1.35 }
 - { node: dc1-leaf02 , type: 7020SR , mgmt_ip: 172.20.1.36 }
 - { node: dc2-leaf01 , type: 7020SR , mgmt_ip: 172.20.2.35 }
 - { node: dc2-leaf02 , type: 7020SR , mgmt_ip: 172.20.2.36 }
 - { node: dc1-spine01 , type: 7050CX3 , mgmt_ip: 172.20.1.41 }
 - { node: dc1-spine02 , type: 7050CX3 , mgmt_ip: 172.20.1.42 }
 - { node: dc2-spine01 , type: 7050CX3 , mgmt_ip: 172.20.2.41 }
 - { node: dc2-spine02 , type: 7050CX3 , mgmt_ip: 172.20.2.42 }
# END ANSIBLE MANAGED BLOCK

另请参阅…

有关 NetBox 插件的更多信息,请访问docs.ansible.com/ansible/latest/plugins/inventory/netbox.html

要了解有关 Ansible 动态清单的更多信息,请访问docs.ansible.com/ansible/latest/user_guide/intro_dynamic_inventory.html

使用 NetBox 生成配置

在这个示例中,我们将概述如何使用从 NetBox 检索的数据生成配置并将配置推送到网络设备。

准备工作

在这个示例中,我们将继续使用 NetBox 作为我们的动态清单源,因此需要实现前一个示例中概述的所有配置。

如何做…

  1. netbox_dynamic_inventory目录下,创建netbox_data.yml文件,内容如下:
$ cat netbox_data.yml
---
netbox_url: http://172.20.100.111
netbox_token: 08be88e25b23ca40a9338d66518bd57de69d4305
  1. 创建pb_build_config.yml剧本,其中包含一个初始任务,用于读取netbox_data.yml文件,如下所示:
$ cat pb_build_config.yml --- - name: Create Report from Netbox Data
 hosts: all gather_facts: no connection: local tasks: - name: Read netbox Data include_vars: netbox_data.yml run_once: yes
  1. 更新pb_build_config.yml剧本,包括一个任务,查询 NetBox 当前设备的数据库中的所有接口:
 - name: Get Data from Netbox
 uri:
 url: "{{ netbox_url }}/api/dcim/interfaces/?device={{ inventory_hostname
}}"
 method: GET
 headers:
 Authorization: "Token {{ netbox_token }}"
 Accept: 'application/json'
 return_content: yes
 body_format: json
 status_code: [200, 201]
 register: netbox_interfaces
 delegate_to: localhost
 run_once: yes
  1. 使用以下任务更新剧本,将配置推送到设备:
 - name: Push Config
 eos_config:
 lines:
 - description {{ port.description }}
 parent: interface {{ port.name }}
 loop: "{{ netbox_interfaces.json.results }}"
 loop_control:
 loop_var: port
 vars:
 ansible_connection: network_cli
 ansible_network_os: eos

工作原理…

为了运行我们的剧本,我们需要使用 NetBox 动态清单脚本作为我们的清单源,并执行剧本,如下所示:

$ ansible-playbook pb_build_config.yml -i netbox_inventory_source.yml

在这个示例中,我们将使用 NetBox 作为我们的真相来源,用于构建我们的清单以及检索给定设备上的接口。我们将使用GET API调用 NetBox 上的接口端点,并通过指定仅针对此特定设备的接口来过滤此 API 调用。实现这一点的 API 调用是api/dcim/interfaces/?device=<deivce-name>/

以下代码片段显示了我们从 NetBox 获取的响应:

ok: [dc1-spine01] => {
 "netbox_interfaces": {
 "api_version": "2.6",
 "changed": false,
 "connection": "close",
 "json": {
 "results": [
 {
 "description": "CORE | dc1-leaf01| Ethernet8",
 "device": {
 "display_name": "dc1-spine01",
 "id": 44,
 "name": "dc1-spine01",
 "url": "http://172.20.100.111/api/dcim/devices/44/"
 },
 "enabled": true,
 <-- Output Omitted for Brevity -->     
                    "name": "Ethernet1",
<-- Output Omitted for Brevity -->     
                },

我们将使用从 API 检索到的数据来配置网络中所有设备的所有端口的描述,根据 NetBox 数据库中的数据。在这种情况下,我们将使用eos_config将这些数据推送到我们的 Arista EOS 设备上。我们可以循环遍历从 NetBox 返回的数据,这些数据存储在netbox_interfaces.json.results中,并从中提取接口名称和描述。我们还可以使用eos_config模块推送这些信息,以在网络中的所有设备上设置正确的描述。

第十二章:使用 AWX 和 Ansible 简化自动化

在本书的所有先前章节中,我们一直在使用 Ansible,更具体地说是 Ansible Engine,并且使用 Ansible 提供的命令行界面CLI)选项执行不同的自动化任务。然而,在跨多个团队的 IT 企业中大规模使用 Ansible 可能具有挑战性。这就是为什么我们将介绍Ansible Web eXecutableAWX)框架。AWX 是一个开源项目,是 Red Hat Ansible Tower 的上游项目。

AWX 是 Ansible Engine 的包装器,并提供了额外的功能,以简化在企业中跨不同团队规模运行 Ansible。它提供了多个附加功能,如下:

  • 基于图形用户界面(GUI)的界面

AWX 提供了一个可视化仪表板来执行 Ansible playbook 并监视其状态,以及提供有关 AWX 中不同对象的不同统计信息。

  • 基于角色的访问控制(RBAC)

AWX 在 AWX 界面中的所有对象上提供 RBAC,例如 Ansible playbook、Ansible 清单和机器凭据。这种 RBAC 提供了对谁可以创建/编辑/删除 AWX 中不同组件的细粒度控制。这为将简单的自动化任务委托给运维团队提供了一个非常强大的框架,设计团队可以专注于开发 playbook 和工作流程。AWX 提供了定义不同用户并根据其工作角色分配特权的能力。

  • 清单管理

AWX 提供了一个 GUI 来定义清单,可以将其定义为静态或动态,并且具有定义主机和组的能力,类似于 Ansible 遵循的结构。

  • 凭据管理

AWX 为凭据提供了集中管理,例如用于访问组织中不同系统(如服务器和网络设备)的密码和安全外壳SSH)密钥。一旦创建了所有凭据,它们就会被加密,无法以纯文本格式检索。这提供了对这些敏感信息更多的安全控制。

  • 集中式日志记录

AWX 在 AWX 节点上收集所有自动化任务的日志,因此可以进行审计以了解谁在哪些节点上运行了哪些 playbook,以及这些 playbook 的状态如何。

  • 表述状态转移(RESTful)应用程序编程接口(API)

AWX 提供了丰富的 API,允许我们从 API 执行自动化任务;这简化了将 Ansible 与已经存在于典型企业环境中的其他编排和工单系统集成。此外,您可以使用 API 检索从 GUI 访问的所有信息,例如清单。

AWX 项目由多个开源软件项目捆绑在一起,以提供先前列出的所有功能,并构建 AWX 自动化框架。以下图表概述了 AWX 框架内的不同组件:

AWX 可以使用不同的部署工具部署,例如 Docker Compose、Docker Swarm 或 Kubernetes。它可以作为独立应用程序部署,也可以作为集群部署(使用 Kubernetes 或 Docker Swarm)。使用集群更复杂;然而,它可以为整个 AWX 部署提供额外的弹性。

这些是本章涵盖的主要内容:

  • 安装 AWX

  • 在 AWX 上管理用户和团队

  • 在 AWX 上创建网络清单

  • 在 AWX 上管理网络凭据

  • 在 AWX 上创建项目

  • 在 AWX 上创建模板

  • 在 AWX 上创建工作流模板

  • 使用 AWX API 运行自动化任务

技术要求

本章中提供的所有代码都可以在以下网址找到:

github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch12_awx

本章基于以下软件版本:

  • 运行 Ubuntu 16.04 的 Ansible/AWX 机器

  • Ansible 2.9

  • AWX 9.0.0

有关 AWX 项目的更多信息,请查看以下链接:

安装 AWX

AWX 可以以多种不同的方式部署;然而,最方便的方式是使用容器部署。在本文中,我们将概述如何使用 Docker 容器安装 AWS,以便开始与 AWX 界面进行交互。

准备工作

准备一个新的 Ubuntu 16.04 机器,我们将在其上部署 AWX-它必须具有互联网连接。

如何做…

  1. 确保 Python 3 已安装在 Ubuntu Linux 机器上,并且 pip 已安装并升级到最新版本:
$ python –version
Python 3.5.2

$ sudo apt-get install python3-pip

$ sudo pip3 install --upgrade pip

$ pip3 --version
pip 19.3.1 from /usr/local/lib/python3.5/dist-packages/pip (python 3.5)
  1. 在 Linux 机器上安装 Ansible,如下面的代码片段所示:
$ sudo pip3 install ansible==2.9
  1. 在 Ubuntu Linux 机器上安装 Docker,使用以下 URL:docs.docker.com/install/linux/docker-ce/ubuntu/

  2. 在 Ubuntu 机器上安装 Docker Compose,使用以下 URL:docs.docker.com/compose/install/

  3. 安装dockerdocker-compose Python 模块,如下面的代码片段所示:

$ sudo pip3 install docker docker-compose
  1. 按照以下 URL 在 Ubuntu Linux 机器上安装 Node.js 10.x 和Node Package Managernpm)6.x,使用Personal Package ArchivePPA)方法获取确切和更新的版本:www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-16-04

  2. 创建一个名为ch12_awx的新目录,并将 AWX 项目 GitHub 存储库克隆到一个名为awx_src的新目录中:

$ mkdir ch12_awx

$ cd ch12_awx

$ git clone [`github.com/ansible/awx`](https://github.com/ansible/awx) awx_src
  1. 切换到安装目录并运行安装 playbook:
$ cd awx_src/installer

$ ansible-playbook -i inventory install.yml

工作原理…

正如介绍中所概述的,AWX 由多个组件组合在一起以提供完整的框架。这意味着可以通过安装每个组件并对其进行配置来部署 AWX,然后集成所有这些不同的产品以创建 AWX 框架。另一种选择是使用基于容器的部署,在微服务架构中为每个组件创建一个容器,并将它们组合在一起。基于容器的方法是推荐的方法,这也是我们用来部署 AWX 的方法。

由于我们将使用容器,因此需要在这些不同的组件之间进行编排;因此,我们需要一个容器编排工具。AWX 支持在 Kubernetes、OpenShift 和docker-compose上部署,其中最简单的是docker-compose。因此,本文档中概述的方法就是这种方法。

AWX 安装程序要求在部署节点上存在 Ansible,因为安装程序是基于 Ansible playbooks 的。这些 playbooks 构建/下载 AWX 不同组件的容器(PostgreSQL、NGINX 等),创建docker-compose声明文件,并启动容器。因此,我们的第一步是安装 Ansible。然后,我们需要安装dockerdocker-compose,以及安装和正确运行 AWX 容器所需的其他依赖项。

一旦我们安装了所有这些先决条件,我们就准备安装 AWX。我们克隆 AWX 项目的 GitHub 存储库,在这个存储库中,有一个installer目录,其中包含了部署容器的所有 Ansible 角色和 playbook。installer目录有一个inventory文件,定义了我们将部署 AWX 框架的主机;在这种情况下,它是本地主机。inventory文件还列出了其他变量,如管理员密码,以及 PostgreSQL 和 RabbitMQ 数据库的密码。由于这是一个演示部署,我们不会更改这些变量,而是使用这些默认参数进行部署。

安装完成后,我们可以验证所有 Docker 容器是否正常运行,如下所示:

$ sudo docker ps

这给我们以下输出:

容器 ID 状态 镜像 端口 命令 创建时间 名称
225b95337b6d Up 2 hours ansible/awx_task:7.0.0 8052/tcp "/tini -- /bin/sh -c…" 30 hours ago awx_task
2ca06bd1cd87 Up 2 hours ansible/awx_web:7.0.0 0.0.0.0:80->8052/tcp "/tini -- /bin/sh -c…" 30 hours ago awx_web
66f560c62a9c Up 2 hours memcached:alpine 11211/tcp "docker-entrypoint.s…" 30 hours ago awx_memcached
fe4ccccdb511 Up 2 hours postgres:10 5432/tcp "docker-entrypoint.s…" 30 hours ago awx_postgres
24c997d5991c Up 2 hours ansible/awx_rabbitmq:3.7.4 4369/tcp, 5671-5672/tcp, 15671-15672/tcp, 25672/tcp "docker-entrypoint.s…" 30 hours ago awx_rabbitmq

我们可以通过打开 Web 浏览器并使用以下凭据连接到机器的IP地址来登录 AWX GUI:

  • 用户名:admin

  • 密码:password

可以在下图中看到:

一旦我们登录 AWX,我们将看到主要的仪表板,以及左侧面板上可用于配置的所有选项(组织、团队、项目等):

还有更多...

为了简化 AWX 的所有先决条件的部署,我包含了一个名为deploy_awx.yml的 Ansible playbook,以及用于编排所有 AWX 组件部署的多个角色。我们可以使用这个 playbook 来部署 AWX 组件,如下所示:

  1. 按照本教程在机器上安装 Ansible。

  2. 克隆本章的 GitHub 存储库。

  3. 切换到ch12_awx文件夹,如下所示:

$ cd ch12_awx
  1. 从这个目录里面,运行 playbook:
$ ansible-playbook -i awx_inventory deploy_awx.yml

另请参阅...

有关 AWX 安装的更多信息,请查看以下链接:

github.com/ansible/awx/blob/devel/INSTALL.md

在 AWX 上管理用户和团队

在本教程中,我们将概述如何在 AWX 中创建用户和团队。这是实施 RBAC 并强制执行组织内不同团队的特权的方法,以便更好地控制可以在 AWX 平台上执行的不同活动。

准备工作

AWX 应按照前面的教程进行部署,并且所有以下任务必须使用admin用户帐户执行。

如何做…

  1. 为所有网络团队创建一个新的组织,如下图所示,通过从左侧面板选择组织并按下保存按钮:

  1. 通过从左侧面板选择团队,在网络组织中为设计团队创建一个新团队:

  1. 在网络组织中为运维团队创建另一个团队,如下图所示:

  1. 通过选择用户按钮,在网络组织中创建一个core用户,如下图所示:

  1. 将这个新用户分配给Network_Design团队,从左侧面板点击 TEAMS 标签,然后选择Network_Design团队。点击 USERS,然后将core用户添加到这个团队,如下截图所示:

  1. 重复上述步骤,创建一个noc用户,并将其分配给Network_Operation团队。

  2. 对于Network_Design团队,将项目管理员、凭证管理员和库存管理员权限分配给组织,如下截图所示:

工作原理…

AWX 的主要特性之一是其 RBAC,这是通过 AWX 内的不同对象实现的。这些对象主要是组织、用户和团队。由于 AWX 应该是企业规模的自动化框架,组织内的不同团队需要在 AWX 中共存。这些团队中的每一个都管理着自己的设备,并维护着自己的 playbooks,以管理其受管基础设施。在 AWX 中,组织是我们区分企业内不同组织的方法。在我们的示例中,我们创建了一个网络组织,将负责网络基础设施的所有团队和用户分组在一起。

在组织内,我们有不同角色的不同用户,他们应该对我们的中央自动化 AWX 框架具有不同级别的访问权限。为了简化为每个用户分配正确角色的过程,我们使用团队的概念来将具有相似特权/角色的用户分组。因此,在我们的情况下,我们创建了两个团队Network_DesignNetwork_Operation团队。这两个团队的角色和权限描述如下:

  • Network_Design团队负责创建 playbooks 和创建网络清单,以及访问这些设备的正确凭据。

  • Network_Operation团队有权查看这些清单,并执行由设计团队开发的 playbook。

这些不同的构造共同工作,为每个用户构建了精细的 RBAC,利用了 AWX 框架。

由于我们已经将项目管理员、库存管理员和凭证管理员角色分配给Network_Design团队,因此该团队内的所有用户都能够仅在 Network 组织内创建/编辑/删除和使用所有这些对象。

另请参阅...

有关 RBAC 和如何使用用户和团队的更多信息,请查看以下链接以了解 Ansible Tower:

在 AWX 上创建网络清单

在本教程中,我们将概述如何在 AWX 中创建网络清单。清单是基础,因为它们描述了我们的网络基础设施,并为我们提供了有效地对我们的网络设备进行分组的能力。

准备工作

AWX 必须已安装并可访问,并且用户帐户必须按照前面的教程部署。

如何操作…

  1. 通过转到左侧导航栏上的 INVENTORIES 标签,创建一个名为mpls_core的新清单,如下截图所示:

  1. 创建一个名为junos的新组,如下截图所示:

  1. 使用类似的方法创建iosxrpeP组。在 mpls_core 清单下的最终组结构应该类似于下面截图中显示的结构:

  1. 在 HOSTS 选项卡下创建mxpe01主机设备,并在 VARIABLES 部分创建ansible_host变量,如下截图所示:

  1. 重复相同的过程来创建剩余的主机。

  2. 进入我们创建的junos组,并添加相应的主机,如下截图所示:

  1. 对所有剩余的组重复这个步骤。

  2. 创建mpls_core清单后,我们将为Network_Operation组授予对该清单的读取权限,如下截图所示:

工作原理…

在这个配方中,我们正在为我们的网络建立清单。这是定义我们所有 Ansible playbooks 中使用的清单文件的确切步骤。下面的代码块显示了我们通常在使用 Ansible 时定义的静态清单文件,以及我们如何使用 AWX 中的清单定义相同的结构:

[pe]
mxpe01    ansible_host=172.20.1.3
mxpe02    ansible_host=172.20.1.4
xrpe03    ansible_host=172.20.1.5

[p]
mxp01     ansible_host=172.20.1.2
mxp02     ansible_host=172.20.1.6
[junos]
mxpe01
mxpe02
mxp01
mxp02

[iosxr]
xrpe03

我们可以在组或主机级别为我们的清单定义变量。在我们的情况下,我们为每个主机定义了ansible_host变量,以便告诉 AWX 如何访问清单中的每个主机。

我们更新清单的权限,以便运维团队可以对其进行读取,以查看其组件。由于设计团队拥有清单管理员权限,设计团队对网络组织中创建的所有清单都拥有完全的管理权限。我们可以查看清单的权限,如下截图所示:

在 AWX 上管理网络凭据

为了让 AWX 开始与我们的基础设施进行交互并运行所需的 playbook,我们需要定义正确的网络凭据来登录到我们的网络基础设施。在这个配方中,我们概述了如何创建所需的网络凭据,以便 AWX 登录到网络设备并开始在我们管理的网络清单上执行 playbook。我们还将概述如何在 AWX 中使用 RBAC,以便在组织内不同团队之间轻松共享这些敏感数据。

准备工作…

AWX 必须被安装并且可达,用户账户必须被部署,就像在之前的配方中所概述的那样。

如何做…

  1. 在左侧导航栏的 CREDENTIALS 选项卡中,创建访问网络设备所需的登录凭据。我们将使用 Machine 凭据类型,因为我们将使用新的连接模块,如network_cliNETCONFhttpapi来访问设备。指定用于登录设备的用户名和密码:

  1. 更新我们创建的凭据的权限,以便Network_Design团队是凭据管理员,Network_Operation团队具有只读权限。以下是凭据权限的应用方式:

工作原理…

在这个配方中,我们创建了访问网络设备所需的网络凭据,并在 AWX GUI 界面上指定了登录到设备所需的用户名和密码。当我们在 AWX 界面上输入密码时,它会被加密,然后以加密格式存储在 PostgreSQL 数据库中,我们无法以明文查看。这在 AWX 框架内提供了额外的密码处理安全性,并提供了一个简单的程序来在组织内共享和利用敏感信息,因此Admin或授权用户可以创建和编辑凭据,并可以向所需的用户/团队授予对这些凭据的用户权限。这些用户只使用凭据,但他们没有任何管理权限来查看或更改它们。与使用 Ansible 和ansible-vault相比,这大大简化了密码管理。

AWX 提供不同的凭据类型来访问不同的资源,如物理基础设施、云提供商和版本控制系统(VCS)。在我们的情况下,我们使用机器凭据类型,因为我们使用 SSH 连接到我们的网络基础设施,需要用户名和密码。

另请参阅...

有关 AWX 凭据的更多信息,请查看以下 URL:

docs.ansible.com/ansible-tower/latest/html/userguide/credentials.html

在 AWX 上创建项目

在本教程中,我们将概述如何在 AWX 上创建项目。在 AWX 中,项目是一个表示 Ansible 剧本(或剧本)的对象,其中包括执行此剧本所需的所有相关文件和文件夹。

准备工作

AWX 必须已安装并可访问,并且必须部署用户帐户,如前一教程中所述。

如何做…

  1. 创建一个新目录awx_sample_project,用于保存我们的 AWX 项目的所有文件和文件夹。

  2. 创建一个group_vars/all.yml剧本,内容如下:

p2p_ip:
 xrpe03:
 - {port: GigabitEthernet0/0/0/0, ip: 10.1.1.7/31 , peer: mxp01, pport: ge-0/0/2, peer_ip: 10.1.1.6/31}
 - {port: GigabitEthernet0/0/0/1, ip: 10.1.1.13/31 , peer: mxp02, pport: ge-0/0/2, peer_ip: 10.1.1.12/31}
  1. 创建一个group_vars/iosxr.yml剧本,内容如下:
ansible_network_os: iosxr
ansible_connection: network_cli
  1. 创建一个group_vars/junos.yml剧本,内容如下:
ansible_network_os: junos
ansible_connection: netconf
  1. 创建一个pb_deploy_interfaces.yml剧本,内容如下:
---
- name: get facts
 hosts: all
 gather_facts: no
 tasks:
 - name: Enable Interface
 iosxr_interface:
 name: "{{ item.port }}"
 enabled: yes
 loop: "{{ p2p_ip[inventory_hostname] }}"
 - name: Configure IP address
 iosxr_config:
 lines:
 - ipv4 address {{ item.ip | ipaddr('address') }} {{item.ip | ipaddr('netmask') }}
 parents: interface {{ item.port }}
 loop: "{{ p2p_ip[inventory_hostname] }}"
  1. 创建一个pb_validate_interfaces.yml剧本,内容如下:
---
- name: Get IOS-XR Facts
 hosts: iosxr
 gather_facts: no
 tasks:
 - iosxr_facts:
 tags: collect_facts
 - name: Validate all Interfaces are Operational
 assert:
 that:
 - ansible_net_interfaces[item.port].operstatus == 'up'
 loop: "{{ p2p_ip[inventory_hostname] }}"
 - name: Validate all Interfaces with Correct IP
 assert:
 that:
 - ansible_net_interfaces[item.port].ipv4.address == item.ip.split('/')[0]
 loop: "{{ p2p_ip[inventory_hostname] }}"
  1. 我们的新文件夹将具有以下目录结构:
.
├── group_vars
│   ├── all.yml
│   ├── iosxr.yml
│   └── junos.yml
├── pb_deploy_interfaces.yml
└── pb_validate_interface.yml
  1. 在您的 GitHub 帐户上,创建一个名为awx_sample_project的新公共仓库:

  1. 在我们的awx_sample_repo项目文件夹中,初始化一个 Git 仓库并将其链接到我们在上一步创建的 GitHub 仓库,如下代码块所示:
git init
git commit -m “Initial commit”
git add remote origin git@github.com:kokasha/awx_sample_project.git
git push origin master
  1. 在 AWX 界面上,根据 Git 创建一个新项目,如下截图所示:

它是如何工作的…

AWX 的主要目标之一是简化与 Ansible 剧本的协作,以及简化运行和执行 Ansible 剧本的方式。为了实现这些目标,与 AWX 一起使用 Ansible 剧本的最佳和最常见方法是使用存储和跟踪在 Git 版本控制中的 AWX 项目。这种方法允许我们将用于我们的 Ansible 剧本的代码开发(存储和版本控制使用 Git)与剧本执行(将由 AWX 处理)分开。

我们遵循与使用 Ansible 开发项目相同的逻辑,通过创建一个文件夹来保存我们项目的所有文件和文件夹。这包括group_varshost_vars文件夹,用于指定我们的变量,我们还定义了项目所需的不同剧本。我们将所有这些文件和文件夹保存在一个 Git 仓库中,并将它们托管在 GitHub 或 GitLab 等 Git VCS 上。

为了让 AWX 开始使用我们开发的剧本,我们在 AWX 中创建一个新项目,并选择基于 Git,然后提供包含此项目的 Git 仓库的 URL。我们还提供所需的任何其他信息,例如要使用哪个分支;如果这是一个私有 Git 仓库,我们提供访问它所需的凭据。

完成此步骤后,AWX 界面将获取此 Git 仓库的所有内容并将其下载到此位置,默认情况下为/var/lib/awx/projects。在此阶段,我们在 AWX 节点上本地存储了此仓库的所有内容,以便开始运行我们的剧本来针对我们的网络节点。

另请参阅...

有关 AWX 项目的更多信息,请查看以下 URL:

在 AWX 上创建模板

在这个配方中,我们将概述如何在 AWX 中组合清单、凭据和项目,以创建模板。AWX 中的模板允许我们为 Ansible playbooks 创建标准的运行环境,可以根据用户的角色执行不同的用户。

准备工作

AWX 界面必须安装,并且必须创建凭据、清单和项目,如前面的配方中所述。

如何做到…

  1. 在 AWX 中创建一个名为provision_interfaces的新模板,并为其分配我们创建的清单和凭据。我们将使用awx_sample_project目录,如下截图所示:

  1. 我们更新了此模板的权限,以便Network_Design团队是ADMINNetwork_Operation团队具有 EXECUTE 角色,如下截图所示:

  1. 使用相同的步骤再次创建一个名为interface_validation的模板,使用pb_validate_interfaces.yml playbook。

它是如何工作的…

在这个配方中,我们概述了如何组合我们之前配置的所有不同部分,以便在 AWX 上执行我们的 playbooks。AWX 使用模板来创建这种标准的执行环境,我们可以使用它来从 AWX 运行我们的 Ansible playbooks。

我们使用给定名称创建了模板,并指定了不同的参数,以创建此环境以执行我们的 playbook,如下所示:

  • 我们提供了我们要执行 playbook 的清单。

  • 我们提供了执行 playbook 所需的所有必要凭据(可以是一个或多个凭据)。

  • 我们提供了我们将选择要运行的 playbook 的项目。

  • 我们从这个项目中选择了 playbook。

我们可以在我们的模板中指定其他可选参数,例如以下内容:

  • 在执行此 playbook 时,是否运行此 playbook 或使用检查模式。

  • 我们是否要在清单上设置限制,以便针对其的子集进行目标定位。

  • 我们想要指定的任何 Ansible 标记。

最后,我们可以为组织中的所有用户定制此模板的权限,在我们的情况下,我们为Network_Design团队提供 ADMIN 角色,为Network_Operation团队提供 EXECUTE 角色。在这种情况下,Network_Operation团队可以执行此 playbook,而Network_Design团队可以编辑和更改此模板的不同参数。

一旦我们保存了这个模板,我们可以从中启动一个作业,并从导航栏左侧的 JOBS 选项卡监视其结果:

我们还可以像在 Ansible 中一样,通过单击相应的作业来查看此 playbook 运行的详细信息,如下截图所示:

另请参阅…

有关 AWX 模板以及可用于自定义模板的不同选项的更多信息,请查看以下 URL:

docs.ansible.com/ansible-tower/latest/html/userguide/job_templates.html

在 AWX 上创建工作流模板

在这个配方中,我们将概述如何使用工作流模板在 AWX 上创建更复杂的模板,以运行多个 playbook 以实现共同的目标。这是一个高级功能,我们在 AWX 中组合多个模板以完成任务。

准备工作

AWX 模板按照前一章中的配置进行配置。

如何做到…

  1. 从 TEMPLATES 选项卡中,创建一个 NEW WORKFLOW JOB TEMPLATE,如下截图所示:

  1. 使用工作流可视化器,创建如下截图中概述的工作流:

  1. 根据以下截图在工作流模板上分配正确的权限:

工作原理…

如果我们的自动化任务需要运行多个 playbook 以实现我们的目标,我们可以使用 AWX 中的工作流模板功能来协调多个模板以实现此目标。模板可以根据工作流模板中包含的任务的成功和失败的不同标准进行组合。

在我们的示例中,我们使用工作流模板来在 IOS-XR 节点上配置接口;然后,我们验证所有配置是否正确应用,并且当前的网络状态是否符合我们的要求。我们将provision_interface模板和validate_interfaces模板组合在一起以实现这一目标。我们首先配置接口,在此任务成功后,我们运行验证 playbook。

我们可以在“作业”选项卡中检查组合工作流的状态,如下截图所示:

此外,我们可以通过在“作业”选项卡中点击工作流名称并查看该工作流中每个任务的详细信息来深入了解此工作流的详细信息:

另请参阅…

有关 AWX 工作流模板的更多信息,请查看以下 URL:

使用 AWX API 运行自动化任务

在本教程中,我们将概述如何使用 AWX API 在 AWX 上启动作业。AWX 的主要功能之一是提供强大的 API,以便与 AWX 系统交互,查询 AWX 中的所有对象,并从 AWX 框架执行自动化任务,如模板和工作流模板。我们还可以使用 API 列出所有用户/团队以及在 AWX 界面上可用和配置的所有不同资源。

准备就绪

AWX 界面必须已安装并可访问,并且必须根据前几章的概述配置模板和工作流模板。

为了执行与 AWX API 交互的命令,我们将使用curl命令来启动 HTTP 请求到 AWX 端点。这需要在机器上安装 cURL。

操作步骤…

  1. 通过列出通过此 API 可用的所有资源来开始探索 AWX API,如下面的代码片段所示:
curl -X GET  http://172.20.100.110/api/v2/
  1. 使用以下 REST API 调用收集在 AWX 界面上配置的所有作业模板,并获取每个作业模板的 ID:
curl -X GET --user admin:password  http://172.20.100.110/api/v2/job_templates/ -s | jq
  1. 使用以下 REST API 调用在 AWX 界面上配置的作业模板中启动作业模板。在此示例中,我们正在启动 ID=7job_Templates
curl -X POST --user admin:password http://172.20.100.110/api/v2/job_templates/7/launch/ -s | jq
  1. 使用以下调用获取从前面的 API 调用启动的作业的状态。ID=35是从前面的 API 调用中检索到的,用于启动作业模板:
curl -X GET --user admin:password http://172.20.100.110/api/v2/jobs/35/ | jq
  1. 使用以下 API 调用收集在 AWX 界面上配置的所有工作流模板,并记录每个模板的 ID:
curl -X GET --user admin:password http://172.20.100.110/api/v2/workflow_job_templates/ -s | jq
  1. 使用从前面的 API 调用中检索到的 ID 启动工作流作业模板:
curl -X POST --user admin:password http://172.20.100.110/api/v2/workflow_job_templates/14/launch/ -s | jq

工作原理…

AWX 提供了一个简单而强大的 REST API,用于检索和检查 AWX 系统的所有对象和组件。使用此 API,我们可以与 AWX 界面交互,以启动自动化任务,并检索这些任务的执行状态。在本教程中,我们概述了如何使用 cURL 命令行工具与 AWX API 进行交互;如何使用其他工具如 Postman 与 API 进行交互;以及如何使用任何编程语言,如 Python 或 Go,构建更复杂的脚本和应用程序,以消耗 AWX API。在我们的所有示例中,我们都使用jq Linux 实用程序,以便以良好的格式输出每个 API 调用返回的 JSON 数据。

我们首先通过检查http://<AWX Node IP>/api/v2/统一资源标识符URI)来探索通过 AWX API 发布的所有端点,这将返回通过此 API 可用的所有端点。以下是这个输出的一部分:

$ curl -X GET http://172.20.100.110/api/v2/ -s | jq
{
 "ping": "/api/v2/ping/",
 "users": "/api/v2/users/",
 "projects": "/api/v2/projects/",
 "project_updates": "/api/v2/project_updates/",
 "teams": "/api/v2/teams/",
 "credentials": "/api/v2/credentials/",
 "inventory": "/api/v2/inventories/",
 "groups": "/api/v2/groups/",
 "hosts": "/api/v2/hosts/",
 "job_templates": "/api/v2/job_templates/",
 "jobs": "/api/v2/jobs/",
}

然后,我们通过访问相应的 API 端点列出在 AWX 界面上配置的所有作业模板。这个 API 调用使用GET方法,并且必须经过身份验证;这就是为什么我们使用--user选项来传递用户的用户名和密码。以下代码片段概述了这个调用返回的一些值:

$ curl -X GET --user admin:password  http://172.20.100.110/api/v2/job_templates/ -s | jq
 {
 "id": 9,
 "type": "job_template",
 "url": "/api/v2/job_templates/9/",
 "created": "2019-12-18T22:07:15.830364Z",
 "modified": "2019-12-18T22:08:12.887390Z",
 "name": "provision_interfaces",
 "description": "",
 "job_type": "run",
< --- Output Omitted  -- >
}

这个 API 调用返回了在 AWX 界面上配置的所有作业模板的列表;然而,我们关心的最重要的项目是每个作业模板的id字段。这是 AWX 数据库中每个作业模板的唯一主键,用于标识每个作业模板;使用这个id字段,我们可以开始与每个作业模板进行交互,在本文中概述的示例中,我们通过向特定的作业模板发出POST请求来启动作业模板。

一旦我们启动作业模板,这将在 AWX 节点上触发一个作业,并且我们将获得相应的作业 ID 作为我们触发的POST请求的结果。使用这个作业 ID,我们可以通过向作业 API 端点发出GET请求并提供相应的作业 ID 来检查执行的作业的状态。我们使用类似的方法来启动工作流模板,只是使用不同的 URI 端点来处理工作流。

还有更多...

为了列出和启动特定的作业模板或工作流模板,我们可以在 API 调用中使用模板的名称,而不是使用id字段。例如,我们示例中启动provision_interfaces作业模板的 API 调用如下所示:

$ curl -X POST --user admin:password  http://172.20.100.110/api/v2/job_templates/provision_interfaces/launch/ -s | jq
{
 "job": 3,
 "ignored_fields": {},
 "id": 3,
 "type": "job",
< --- Output Omitted  -- >
 "launch_type": "manual",
 "status": "pending",
< --- Output Omitted  -- >
}

可以按照相同的过程来调用工作流模板,使用它的名称作为参数。

另请参阅...

有关 AWX API 的更多信息,请查看以下网址:

第十三章:Ansible 的高级技术和最佳实践

在本章中,我们将探讨一些高级功能和技术,以及一些最佳实践,以便为网络自动化构建更清晰和更健壮的 Ansible playbooks。所有这些技术都可以与前几章的所有代码一起使用。

本章涵盖的教程如下:

  • 在虚拟环境中安装 Ansible

  • 验证 YAML 和 Ansible playbooks

  • 计算 Ansible playbooks 的执行时间

  • 使用 Ansible 验证用户输入

  • check模式运行 Ansible

  • 控制 Ansible 中的并行性和滚动更新

  • 配置 Ansible 中的事实缓存

  • 为 Ansible 创建自定义 Python 过滤器

技术要求

本章中描述的所有代码都可以通过以下 URL 找到:github.com/PacktPublishing/Network-Automation-Cookbook/tree/master/ch13_ansible_best_practice

本章需要以下内容:

  • 运行 CentOS 7 的 Ansible 机器

  • Ansible 2.9

  • Python 3.6.8

在虚拟环境中安装 Ansible

在本教程中,我们将概述如何在 Python 虚拟环境中安装 Ansible,以便为开发和运行我们的 playbooks 提供一个隔离和封闭的环境。

准备工作

Python 3 必须已经安装在您的 Linux 机器上。

操作步骤如下:

  1. 创建一个名为dev的新 Python 虚拟环境,并激活它如下:
$ python3 -m venv dev
$ source dev/bin/activate
  1. 在这个新的虚拟环境中安装 Ansible,操作如下:
$ (dev) $ pip3 install ansible==2.9

工作原理...

如本书第一章所述,我们可以使用以下两种方法之一安装 Ansible:

  • 在我们的 Linux 机器上使用软件包管理器

  • 使用 Python PIP 软件包管理器

在这两种选项中,我们都是使用系统级 Python 运行 Ansible。这意味着当我们安装任何其他包或脚本(例如亚马逊网络服务AWS)或 Azure 包)时,我们是在系统级别安装/升级这些包。在某些情况下,我们可能安装与系统上现有包冲突的包,这可能会影响其他脚本。Python 虚拟环境主要是为这种情况构建的。虚拟环境提供了一个隔离的运行时环境,我们可以在其中完全独立于系统级别包安装我们的 Python 包。因此,我们可以以完全隔离和独立的方式运行同一包的不同版本(例如 Ansible)。

在本教程中,我们概述了如何使用venv Python 模块创建一个新的 Python 虚拟环境。我们使用python命令和-m选项来调用venv模块,这允许我们创建一个新的虚拟环境。我们使用venv Python 模块创建一个名为dev的新虚拟环境,它将创建dev文件夹来容纳我们的新虚拟环境。

为了开始使用这个新的虚拟环境,我们需要激活它。我们使用source命令来运行位于dev文件夹(~/dev/bin/activate)中的激活脚本。这个脚本将激活虚拟环境,并将我们放在这个新创建的环境中。我们可以验证我们当前的 Python 可执行文件位于这个新环境中,而不是与系统级 Python 相关联,如下面的代码片段所示:

(dev)$ which python
~/dev/bin/python
 (dev)$ python --version
Python 3.6.8

一旦我们进入虚拟环境,我们使用python-pip命令来在虚拟环境中安装 Ansible。我们可以验证 Ansible 已安装并且正在使用我们的新虚拟环境,如下面的代码块所示:

(dev)$ ansible --version
ansible 2.9
 config file = None
 configured module search path = ['/home/vagrant/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
 ansible python module location = /home/vagrant/dev/lib64/python3.6/site-packages/ansible
 *executable location = /home/vagrant/dev/bin/ansible*  python version = 3.6.8 (default, Aug  7 2019, 17:28:10) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]

在这个阶段,我们已经在这个虚拟环境中安装了 Ansible。但是,默认情况下,当运行 Ansible 时,它将尝试使用位于/usr/bin/python的系统级 Python。为了覆盖这种行为并强制 Ansible 使用我们的新虚拟环境,我们需要为所有主机设置一个变量以使用这个新虚拟环境,我们可以在清单文件中进行设置,如下面的代码片段所示:

$ cat hosts
[all:vars]
ansible_python_interpreter=~/dev/bin/python 

验证 YAML 和 Ansible playbooks

在这个示例中,我们将概述如何使用Yamllintansible-lint工具来验证 YAML 文件和 Ansible playbooks,以确保我们的 YAML 文档具有正确的语法,并验证我们的 Ansible playbooks。

准备就绪

Python 和 PIP 软件包管理器必须已经安装在您的 Linux 机器上,并且还必须安装 Ansible。

如何做...

  1. 安装yamllint,如下面的代码片段所示:
$ sudo pip3 install yamllint
  1. 安装ansible-lint,如下面的代码片段所示:
$ sudo pip3 install ansible-lint
  1. 切换到您的 Ansible 项目目录,如下所示:
$ cd ch13_ansible_best_practice
  1. 运行yamllint,如下面的代码片段所示:
# run yamllint on all files in this folder
$ yamllint
  1. 运行ansible-lint,如下面的代码片段所示:
# run ansible-lint on this specific ansible-playbook
$ ansible-lint pb_build_datamodel.yml

工作原理...

我们使用 YAML 文档来声明我们的网络拓扑和我们运行 playbooks 或生成设备配置所需的不同参数。由于我们将定期编辑这些文件以更新我们的网络拓扑并添加新服务,我们需要确保这些文件的所有更改都经过验证,并且这些文件的语法在我们在 playbooks 中导入/使用这些文件之前是正确的。验证 YAML 文件最常用的工具之一是Yamllint程序,它读取 YAML 文档并分析其语法错误和最佳实践格式,输出分析结果。我们使用 PIP 软件包管理器安装这个工具。

在我们的示例中,我们有一个典型的 Ansible 项目,目录结构如下截图所示:

我们通过运行Yamllint来分析此文件夹中的所有 YAML 文档,如前一节所述。下面的截图概述了Yamllint命令在 Ansible 项目文件夹上的输出:

上述输出概述了Yamllint命令在此文件夹中所有 YAML 文件中发现的问题,并提供了关于每个文件中识别的问题的非常清晰的输出。这些问题可以被标识为错误或警告,这会影响Yamllint命令的返回代码。

因此,在所有文件中的问题都被标识为warning的情况下,返回代码是0,这意味着 YAML 文档是有效的。但是,它们有一些小问题需要修复:

# no errors or only warning
$ echo $?
0

如果问题被标识为error,返回代码不是0,这意味着 YAML 文档有一个需要修复的重大问题:

# errors are present
$ echo $?
1

返回代码至关重要,因为它表示Yamllint命令是否成功,这在构建持续集成/持续部署CI/CD)流水线以自动化基础设施的配置非常重要。流水线中的一个步骤将是对所有 YAML 文件进行 lint 以确保文件正确,如果Yamllint命令成功,它将返回代码0

Yamllint命令捕获了 YAML 文档中的所有语法错误。然而,ansible-lint提供了对ansible-playbook代码的更全面检查,特别是验证 playbook 是否遵循良好的编码实践。运行它非常有用,因为它可以用来验证 playbooks 和 Ansible 角色的正确样式,并会提示 playbooks 中的任何问题。

当我们为我们的 playbook 运行ansible-lint命令时,我们可以看到它捕获了以下错误:

输出非常详细,因为它概述了 playbook 中第7行的任务没有名称,这不符合 Ansible 最佳实践。命令的返回代码为2,这表明命令失败了。一旦我们纠正了这个问题,就不会显示任何错误,并且返回代码将为0

还有更多...

Yamllint程序可以通过在项目目录结构中包含一个yamllint文件来进行自定义,该文件包括需要修改的规则。因此,在我们的示例中,当我们运行yamllint命令时,我们可以看到其中一个问题是行长度超过了> 80个字符,这是一个错误,因为这是yamllint遵循的默认规则:

我们可以修改我们的文件并尝试更改yamllint抱怨的行的长度,或者我们可以指定这不应该是一个问题,只应该触发一个warning。我们使用后一种方法,并在我们的目录中创建.yamllint文件并添加以下规则:

---
extends: default
rules:
 line-length:
 level: warning

因此,当我们再次在我们的文件夹上运行yamllint命令时,我们可以看到所有先前的行长度消息已更改为警告:

对于ansible-lint,我们可以使用以下命令检查ansible-lint用于验证给定 playbook 或角色的所有当前规则:

$ ansible-lint -L
$ ansible-lint -T

-L选项将输出所有规则和每个规则的简短描述。

-T选项将输出ansible-lint使用的所有规则/标签。

我们可以运行我们的ansible-lint命令来忽略特定的规则/标签,如下面的代码片段所示:

$ ansible-lint -x task pb_build_datamodel.yml

这将导致ansible-lint忽略所有带有task标签的规则;这样,我们可以影响ansible-lint应用于验证我们的 playbook 的规则。

另请参阅...

计算 Ansible playbook 的执行时间

在这个配方中,我们将概述如何获取 Ansible playbook 中各种任务执行所需的时间。这可以帮助我们了解 playbook 运行期间哪个特定任务或角色占用了最多的时间,并帮助我们优化我们的 playbook。

如何做到...

  1. 更新ansible.cfg文件以包括以下行:
[defaults]
 < --- Output Omitted for brevity ---->
callback_whitelist=timer, profile_tasks, profile_roles
  1. 列出ansible-playbook代码中的所有任务以供参考:
$ ansible-playbook pb_generate_config.yml --list-tasks
  1. 运行 Ansible playbook:
$ ansible-playbook pb_generate_config.yml

它是如何工作的...

Ansible 提供了多个回调插件,我们可以使用这些插件来在响应事件时向 Ansible 添加新的行为。其中最有用的回调插件之一是timer插件;它提供了测量 Ansible playbook 中任务和角色的执行时间的功能。我们可以通过在ansible.cfg文件中将这些插件列入白名单来启用此功能:

  • Timer:此插件提供 playbook 的执行时间摘要。

  • Profile_tasks:这为我们提供了 playbook 中每个任务的执行时间摘要。

  • Profile_roles:这为我们提供了 playbook 中每个角色所花费的时间的摘要。

我们使用--list-tasks选项列出 playbook 中的所有任务,以验证将在我们的 playbook 中执行的所有任务。以下是我们示例 playbook 中的任务片段:

然后运行 playbook 并检查新添加的详细执行摘要,如下面的屏幕截图所示:

摘要的第一部分概述了角色(generate_config)的执行时间,以及使用post_task部分的不同模块(我们在post_task部分仅使用fileassemble模块)。摘要的下一部分概述了 playbook 中每个任务的执行时间(包括角色内的任务的细分)。最后,我们得到了整个 playbook 的总体执行时间的摘要,以一行显示。

另请参阅...

有关回调插件、profile_tasksprofile_roles插件以及timer的更多信息,请参考以下网址:

使用 Ansible 验证用户输入

在这个示例中,我们将概述如何使用 Ansible 验证输入数据。我们在 Ansible 中非常依赖于从网络中检索或在hostgroup变量中声明的信息,以便执行不同的任务,比如生成配置或配置设备。在我们开始使用这些信息之前,我们需要能够在进一步处理 playbook 之前验证这些数据的结构和有效性。

如何做到...

  1. ACLs.yml中创建一个ACLs定义,如下面的代码块所示:
---
ACLs:
 INFRA_ACL:
 - src: 10.1.1.0/24
 dst: any
 dport: ssh
 state: present
 - src: 10.2.1.0/24
 dst: any
 app: udp
 dport: snmp
 state: present
  1. validate_acl.yml文件中创建一个新的验证任务,如下面的代码块所示:
---
- include_vars: ACLs.yml
- name: Validate ACL is Defined
 assert:
 that:
 - ACLs is defined
 - "'INFRA_ACL' in ACLs.keys()"
 - ACLs.INFRA_ACL|length > 0
- name: Validate Rules are Valid
 assert:
 that:
 - item.src is defined
 - item.dst is defined
 - item.src | ipaddr
 loop: "{{ ACLs.INFRA_ACL }}"
  1. 创建一个新的 playbook 来创建访问控制列表ACLs)并推送到网络设备,如下面的代码块所示:
---
- name: Configure ACL on IOS-XR
 hosts: all
 tasks:
 - name: Validate Input Data
 import_tasks: validate_acls.yml
 run_once: yes
 delegate_to: localhost
 tags: validate
 - name: Create ACL Config
 template:
 src: acl.j2
 dest: acl_conf.cfg
 delegate_to: localhost
 run_once: yes
 - name: Provision ACLs
 iosxr_config:
 src: acl_conf.cfg
 match: line

它是如何工作的...

在这个示例 playbook 中,我们想要将 ACL 配置推送到我们的基础设施。我们使用template模块生成配置,并使用iosxr_config模块推送配置。我们所有的 ACL 定义都在ACLs.yml文件中声明。我们希望验证ACLs.yml文件中包含的输入数据,因为这是我们依赖的数据,以便生成我们的配置。

我们创建一个validate_acl.ymltasks文件,其中包含多个任务来验证我们将用于生成配置的数据的结构和内容。我们首先使用include_vars参数导入我们的数据,然后定义两个主要任务来验证我们的数据:

  • 第一个任务是验证所需的数据结构是否存在,并且数据结构是否符合我们期望的正确格式。

  • 第二个任务是验证每个防火墙规则的内容。

在所有这些验证任务中,我们使用assert模块来测试和验证我们的条件语句,并且我们可以定义更全面的检查输入数据结构,以涵盖数据的所有可能性。

使用这种方法,我们可以验证输入数据的有效性,并确保我们的数据是健全的,以便由 playbook 中的后续任务进行处理。

在检查模式下运行 Ansible

在这个示例中,我们将概述如何在 Ansible 中以干运行模式运行我们的 playbook。这种模式也称为check模式,在这种模式下,Ansible 不会对远程管理的节点进行任何更改。我们可以将其视为对我们的 playbook 进行模拟运行,以便了解 Ansible 将进行哪些更改,如果我们以check模式执行 playbook。

如何做到...

  1. ACLs.yml文件中更新我们的 ACL 声明,如下面的代码片段所示:
---
ACLs:
 INFRA_ACL:
< --- Output Omitted for brevity -- >
 - src: 10.3.2.0/24
 dst: 10.2.2.0/24
 dport: dns
 state: present
  1. 使用check模式运行pb_push_acl.yml配置 playbook,如下面的代码片段所示:
$ ansible-playbook pb_push_acl.yml -l den-core01  --check

工作原理...

当我们使用check模式运行 playbook 时,远程系统不会进行任何更改,我们可以看到 playbook 运行的输出,如下面的截图所示:

此输出概述了我们为 ACL 生成的配置文件将发生更改(将添加新规则);但是,provision ACLs 任务没有报告任何更改。这是因为配置文件没有更改,因为我们是在check模式下运行 playbook,所以在这种情况下,此任务仍在使用未修改的配置文件,因此不会实施任何更改。

我们还可以在运行 playbook 时使用--diff标志来检查将发生的更改,如下面的代码片段所示:

$ ansible-playbook pb_push_acl.yml -l den-core01  --check --diff

当我们使用--diff标志时,我们会得到以下输出,并且它概述了将在我们的配置文件上发生的更改:

还有更多...

我们可以使用check模式作为开关来运行或跳过任务。因此,在某些情况下,当我们以check模式运行时,我们不希望连接到设备并在设备上推送任何配置,因为不会有任何更改。使用check模式,我们可以构建我们的 playbook 以跳过这些任务,如下面的代码块所示:

- name: Configure ACL on IOS-XR
 hosts: all
 serial: 1
 tags: deploy
 tasks:
 - name: Backup Config
 iosxr_config:
 backup:
 *when: not ansible_check_mode*    - name: Deploy ACLs
 iosxr_config:
 src: acl_conf.cfg
 match: line
 *when: not ansible_check_mode*

在我们的tasks中,我们添加了when指令,并且我们正在检查ansible_check_mode参数的值。当我们在check模式下运行 playbook 时,此参数设置为true。因此,在每个任务中,我们都在检查check模式是否设置,如果是,我们将在 playbook 运行期间跳过这些任务。如果 playbook 以正常模式运行(不使用check模式),这些任务将正常执行。

另请参阅...

有关在check模式下运行我们的 playbook 的更多信息,请参阅以下网址:docs.ansible.com/ansible/latest/user_guide/playbooks_checkmode.html

在 Ansible 中控制并行性和滚动更新

默认情况下,Ansible 会并行运行任务。在本教程中,我们将概述如何控制 Ansible 的并行执行以及如何修改此默认行为。我们还将探讨滚动更新的概念以及如何在 Ansible 中利用它们。

如何做...

  1. 更新ansible.cfg文件以控制并行执行,如下面的代码片段所示:
[defaults]
forks=2
  1. 更新pb_push_acl.yml文件以设置网络设备上配置推送的滚动更新,如下面的代码块所示:
- name: Configure ACL on IOS-XR
 hosts: all
 serial: 1
  tags: deploy
 tasks:
 - name: Backup Config
 iosxr_config:
 backup:
 - name: Deploy ACLs
 iosxr_config:
 src: acl_conf.cfg
 match: line

工作原理...

默认情况下,Ansible 通过并行在所有在 playbook 中标识的设备上执行每个任务来工作。默认情况下,对于每个任务,Ansible 将分叉五个并行线程(称为 forks)并在清单中的五个节点上并行执行这些线程。一旦这些任务完成,它将以五个节点的批次目标剩余的设备清单。它在 playbook 中执行的每个任务上执行此操作。使用ansible.cfg文件中的forks关键字,我们可以修改 Ansible 正在使用的默认fork值并控制 Ansible 在每个任务执行期间目标的并行节点数。这可以加快我们的 playbook 执行速度;但是,它需要更多的资源,包括内存和 CPU 功率在 Ansible 控制节点上。

当使用大量的 forks 时,请注意任何local_action步骤可能会在本地机器上分叉 Python 解释器,因此您可能希望限制local_actiondelegated步骤的数量或在单独的 plays 中。有关更多信息,请参阅www.ansible.com/blog/ansible-performance-tuning

我们可以修改的另一个选项来控制 playbook 执行是,默认情况下,Ansible 在 playbook 中标识的所有节点上运行每个任务,并且只有在所有节点完成前一个任务后才会从一个任务转到另一个任务。我们可能希望在多种情况下修改这种行为,比如将配置推送到网络设备或升级网络设备。我们可能希望以串行方式在每个节点上执行 playbook - 这意味着 Ansible 会选择每个节点(或节点组),并在其上执行 playbook;一旦这一批完成,就会选择另一批,并再次运行 playbook。这种方法允许我们以滚动方式部署更改,如果我们的某个节点失败,我们可以停止 playbook 执行。这个配置是使用 playbook 中的serial关键字来控制的。它指示 Ansible 使用serial选项标识的主机数量开始执行 play,然后在这一批上执行所有任务,然后转到另一批,并在该批上执行完整的 playbook,依此类推。

另请参阅...

有关 Ansible forks 和滚动更新的更多信息,请参考以下网址:docs.ansible.com/ansible/latest/user_guide/playbooks_delegation.html

在 Ansible 中配置事实缓存

在这个示例中,我们将概述如何在 Ansible 中设置和配置事实缓存。这是一个重要的功能,可以帮助我们在需要从基础架构收集事实时优化和加快 playbook 的执行时间。

操作步骤...

  1. 更新ansible.cfg文件以启用事实缓存,并设置所需的文件夹来存储缓存:
[defaults]
< --- Output Omitted for brevity -->
fact_caching=yaml
fact_caching_connection=./fact_cache
  1. 创建一个新的pb_get_facts.yml playbook,使用不同的方法从网络收集事实:
---
- name: Collect Network Facts
 hosts: all
 tasks:
 - name: Collect Facts Using Built-in Fact Modules
 iosxr_facts:
 gather_subset:
 - interfaces
 - name: Collect Using NAPALM Facts
 napalm_get_facts:
 hostname: "{{ ansible_host }}"
 username: "{{ ansible_user }}"
 password: "{{ ansible_ssh_pass }}"
 dev_os: "{{ ansible_network_os }}"
 filter:
 - interfaces
 - name: Set and Cache Custom Fact
 set_fact:
 site: Egypt
 cacheable: yes
  1. 在清单中的单个节点上运行新的 Ansible playbook:
$ ansible-playbook pb_validate_from_cache.yml -l den-core01

工作原理...

Ansible 是一个强大的工具,可以收集有关基础架构操作状态的信息,并且我们可以使用这些信息来生成配置、构建报告,以及验证基础架构的状态。在基础架构状态非常稳定的情况下,我们可能不需要在每次 playbook 运行期间从设备收集网络事实。在这些情况下,我们可能选择使用事实缓存来加快 playbook 的执行。我们从 Ansible 控制节点上存储的位置读取设备的事实(网络状态),而不是连接到设备并从实时网络中收集信息。

ansible.cfg文件中启用事实缓存,并在该文件中设置我们将用于存储事实数据的后端类型。有多种选项,从 YAML 或 JSON 文件到将这些数据存储到redisMemcached数据库。在我们的示例中,为了简单起见,我们将使用 YAML 文件来存储从设备收集的事实。我们还指定了存储此信息的文件夹位置。

完成这些步骤后,我们可以运行我们的 playbook 来收集网络事实。在这个示例 playbook 中,我们使用不同的模块(方法),如下:

  • iosxr_facts:这是 Ansible 网络模块中的内置模块,用于从 IOS-XR 设备收集事实(对于大多数受 Ansible 支持的供应商的网络设备,都有一个针对每个供应商的事实收集模块)。

  • napalm_get_facts:这是来自网络自动化和可编程性抽象层与多供应商支持NAPALM)的自定义模块,需要安装以收集事实;但它不是核心 Ansible 模块的一部分。

  • set_fact:我们使用set_fact模块在 playbook 运行期间设置自定义事实,并使用cacheable选项指示模块将这个新的缓存变量写入我们的缓存。

一旦我们运行 playbook,我们可以检查新文件夹是否创建,并且我们清单中的每个节点都在这个位置存储了一个新的 YAML 文件。这些模块收集的所有事实都保存在这些 YAML 文件中,如下面的截图所示:

还有更多...

一旦我们配置了事实缓存,我们就可以开始在任何其他 playbook 中使用我们缓存中声明的 Ansible 变量,如下面的代码示例所示:

---
- name: Validate Cache Data
 vars:
 ansible_connection: local
 hosts: all
 tasks:
 - name: Validate all Interfaces
 assert:
 that:
 - item.value.operstatus == 'up'
 with_dict: "{{ ansible_net_interfaces }}"
 - name: Validate Custom Fact
 assert:
 that:
 - site == 'Egypt'

在上面的 playbook 中,我们正在利用从缓存中收集的变量(在本例中为ansible_net_interfaces)并对清单中的设备运行任务。我们需要考虑,默认情况下,缓存中的条目仅在特定时间内有效,由我们的缓存的超时值控制,以确保我们的缓存中的任何过时状态不会被考虑。这个值由fact_caching_timeout选项控制,可以在ansible.cfg文件中设置。

另请参阅...

有关 Ansible 事实缓存的更多信息,请参考以下网址:

为 Ansible 创建自定义 Python 过滤器

Ansible 提供了丰富的 Jinja2 过滤器以及一些额外的内置过滤器来操作数据;然而,在某些情况下,您可能会发现没有可用的过滤器来满足您的需求。在这个示例中,我们将概述如何在 Python 中构建自定义过滤器,以扩展 Ansible 功能来操作数据。

如何做...

  1. 在项目目录(ch13_ansible_best_practice)中,创建一个名为filter_plugins的新文件夹。

  2. filter_plugins文件夹下创建一个名为filter.py的新的 Python 脚本,内容如下:

class FilterModule(object):
 def filters(self):
 return {
 'acl_state': self.acl_state
 }
 def acl_state(self,acl_def):
 for acl_name, acl_rules in acl_def.items():
 for rule in acl_rules:
 rule['state'] = rule['state'].upper()
 return acl_def
  1. 创建一个新的 Ansible playbook,pb_test_custom_filter.yml,内容如下:
---
 - name: Test Custom Filter
 hosts: all
 vars:
 ansible_connection: local
 tasks:
 - name: Read ACL data
 include_vars: ACLs.yml
 run_once: yes
 - name: Apply Our Custom Filter
 set_fact:
 standard_acl: "{{ ACLs | acl_state }}"
 run_once: yes
 - name: Display Output After Filter
 debug: var=standard_acl

它是如何工作的...

我们可以扩展 Ansible 提供的filter库,并使用 Python 创建自定义过滤器。为了实现我们的自定义过滤器,我们在项目目录下创建一个名为filter_plugins的文件夹,并创建一个 Python 脚本,可以使用任何名称(在我们的示例中使用了filter.py)。

为了让 Ansible 捕捉并处理这些过滤器,自定义的 Python 过滤器必须放置在名为filter_plugins的文件夹中。

在这个 Python 脚本中,我们创建了一个名为FilterModule的 Python 类。在这个类中,我们声明了一个名为filters的函数,它返回我们定义的所有自定义过滤器的字典。然后,我们开始创建我们的过滤器,声明一个名为acl_state的函数,它接受我们在 playbook 中传递的acl_def变量(这是我们 ACL 的定义)。在这个示例中,我们只是获取我们 ACL 状态的定义并将其更改为大写。然后我们返回新修改的 ACL 定义。

我们像往常一样创建一个 Ansible playbook,并从ACLs.yml文件中读取我们的 ACL 定义。然后,我们创建一个新任务,使用set_fact模块设置一个自定义事实,并将我们的 ACL 数据结构传递给我们创建的自定义过滤器(acl_state)。我们将自定义过滤器的返回值保存到一个名为standard_acl的新变量中,并在下一个任务中使用debug模块输出这个新变量的值。

以下片段概述了我们 ACL 的新值以及 ACL 定义中的状态参数如何更改为大写:

还有更多...

我们在上一个示例中概述了如何将变量定义传递给我们的自定义过滤器;然而,我们也可以传递多个字段给我们的自定义过滤器,以便更好地控制过滤器的返回值。为了概述这一点,我们将创建另一个自定义过滤器,它将获取 ACL 定义以及一个字段变量,并根据这个字段,将 ACL 定义中的这个字段的值更改为大写。以下是修改后的filter.py Python 脚本:

class FilterModule(object):

< -- Output Omitted for brevity -- >
    def custom_acl(self,acl_def,field=None):
 for acl_name, acl_rules in acl_def.items():
 for rule in acl_rules:
 if field and field in rule.keys():
 rule[field] = rule[field].upper()
 return acl_def
 def filters(self):
 return {
 'acl_state': self.acl_state,
 'custom_acl': self.custom_acl
 }

以下是剧本中修改后任务的输出,使用我们的新自定义过滤器:

 - name: Apply Our Custom Filter
 set_fact:
 standard_acl: "{{ ACLs | acl_state }}"
 final_acl: "{{ ACLs | custom_acl('dports') }}"
 run_once: yes
 - name: Display Output After Filter
 debug: var=final_acl

以下是应用新自定义过滤器后final_acl文件的输出:

前面的截图显示了应用新自定义过滤器后的输出。

posted @ 2024-05-20 12:04  绝不原创的飞龙  阅读(26)  评论(0编辑  收藏  举报