网络自动化秘籍(五)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第十一章:使用 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 中。以下表格捕捉了这个示例网络基础设施:

站点设备角色
DC1dc1-spine01脊柱交换机
DC1dc1-spine02脊柱交换机
DC1dc1-leaf01叶子交换机
DC1dc1-leaf02叶子交换机
DC2dc2-spine01脊柱交换机
DC2dc2-spine02脊柱交换机
DC2dc2-leaf01叶子交换机
DC2dc2-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 hoursansible/awx_task:7.0.0 8052/tcp/tini -- /bin/sh -c…30 hours ago awx_task
2ca06bd1cd87 Up 2 hoursansible/awx_web:7.0.0 0.0.0.0:80->8052/tcp/tini -- /bin/sh -c…30 hours ago awx_web
66f560c62a9c Up 2 hoursmemcached:alpine 11211/tcpdocker-entrypoint.s…30 hours ago awx_memcached
fe4ccccdb511 Up 2 hourspostgres:10 5432/tcpdocker-entrypoint.s…30 hours ago awx_postgres
24c997d5991c Up 2 hoursansible/awx_rabbitmq:3.7.4 4369/tcp, 5671-5672/tcp, 15671-15672/tcp, 25672/tcpdocker-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文件的输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

前面的截图显示了应用新自定义过滤器后的输出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值