Skip to content

Latest commit

 

History

History
1245 lines (961 loc) · 46 KB

File metadata and controls

1245 lines (961 loc) · 46 KB

五、Python 自动化框架——超越基础

第 1 章对 TCP/IP 协议套件和 Python的回顾中,我们查看了一些基本结构,以启动并运行 Ansible。我们使用 Ansible 库存文件、变量和剧本。我们还研究了一些为 Cisco、Juniper 和 Arista 设备使用网络模块的示例。

在本章中,我们将进一步巩固从前几章中获得的知识,并深入探讨 Ansible 更高级的主题。很多关于 Ansible 的书都写过,Ansible 的内容超出了我们两章的篇幅。这里的目标是介绍 Ansible 的大部分特性和功能,我相信作为一名网络工程师,您将需要这些特性和功能,并尽可能缩短学习过程。

需要指出的是,如果您对第 4 章Python 自动化框架–Ansible Basics中提出的一些观点不清楚,现在是回顾和回顾它们的好时机,因为它们是本章的先决条件。

在本章中,我们将探讨以下主题:

  • 易变条件句
  • 易变环
  • 模板
  • 组和主机变量
  • 可移动的拱顶
  • 可替代角色
  • 编写自己的模块

我们有很多地方要覆盖,所以让我们开始吧!

易变条件句

Ansible 条件语句类似于编程语言中的条件语句。在第 1 章对 TCP/IP 协议套件和 Python**的回顾中,我们看到 Python 使用条件语句仅使用if.. thenwhile语句执行代码的一部分。在 Ansible 中,它使用条件关键字仅在满足条件时运行任务。在许多情况下,游戏或任务的执行可能取决于事实、变量或先前任务结果的值。例如,如果您有升级路由器映像的重头戏,您希望在进入下一重头戏重新启动路由器之前,包括一个确保新路由器映像在设备上的步骤。

*在本节中,我们将讨论所有模块都支持的when子句,以及 Ansible networking 命令模块中支持的唯一条件状态。其中一些条件如下:

  • 等于(eq
  • 不等于(neq
  • 大于(gt
  • 大于或等于(ge
  • 小于(lt
  • 小于或等于(le
  • 包含

when 子句

当您需要检查变量的输出或播放执行结果并采取相应行动时,when子句非常有用。当我们查看 Ansible 2.5 最佳实践结构时,我们看到了 Python 自动化框架第 4 章when子句的一个快速示例—Ansible Basics。如果您还记得,该任务仅在设备的网络操作系统为 Cisco IOS 时运行。让我们看看它在chapter5_1.yml中使用的另一个例子:

    ---
    - name: IOS Command Output
      hosts: "iosv-devices"
      gather_facts: false
      connection: local
      vars:
        cli:
          host: "{{ ansible_host }}"
          username: "{{ username }}"
          password: "{{ password }}"
          transport: cli
      tasks:
        - name: show hostname
          ios_command:
            commands:
              - show run | i hostname
                provider: "{{ cli }}"
            register: output
        - name: show output
          when: '"iosv-2" in "{{ output.stdout }}"'
          debug:
            msg: '{{ output }}'

我们已经在第 4 章Python 自动化框架–Ansible Basics*、*中看到了该剧本中的所有元素,直到第一个任务结束。对于剧本中的第二个任务,我们使用when子句检查输出是否包含iosv-2关键字。如果为 true,我们将继续执行任务,该任务使用调试模块显示输出。运行 playbook 时,我们将看到以下输出:

    <skip>
    TASK [show output]  
    *************************************************************
    skipping: [ios-r1]
 ok: [ios-r2] => {
 "msg": {
 "changed": false,
 "stdout": [
 "hostname iosv-2"
 ],
 "stdout_lines": [
 [
 "hostname iosv-2"
 ]
 ],
 "warnings": []
 }
 }
    <skip>

我们可以看到,iosv-r1设备从输出中被跳过,因为子句没有通过。我们可以在chapter5_2.yml中进一步扩展此示例,仅在满足条件时应用某些配置更改:

    <skip> 
    tasks:
      - name: show hostname
        ios_command:
          commands:
            - show run | i hostname
          provider: "{{ cli }}"
        register: output
      - name: config example
        when: '"iosv-2" in "{{ output.stdout }}"'
        ios_config:
          lines:
            - logging buffered 30000
          provider: "{{ cli }}"

我们可以在这里看到执行输出:

 TASK [config example] 
 **********************************************************
 skipping: [ios-r1]
 changed: [ios-r2] 
 PLAY RECAP 
 ***********************************************************
 ios-r1 : ok=1 changed=0 unreachable=0 failed=0
 ios-r2 : ok=2 changed=1 unreachable=0 failed=0

再次注意,在执行输出中,ios-r2是在跳过ios-r1时应用的唯一更改。在这种情况下,日志缓冲区大小仅在ios-r2上更改

在使用 setup 或 facts 模块的情况下,when子句也非常有用–您可以根据最初收集的一些facts进行操作。例如,下面的语句将通过在子句中放置一个条件语句来确保只对具有主要发行版16的 Ubuntu 主机进行操作:

when: ansible_os_family == "Debian" and ansible_lsb.major_release|int >= 16

For more conditionals, check out the Ansible conditionals documentation (http://docs.ansible.com/ansible/playbooks_conditionals.html).

可解释的网络事实

在 2.5 之前,Ansible networking 附带了许多特定于网络的事实模块。存在网络事实模块,但供应商之间的命名和使用不同。从 2.5 版开始,Ansible 开始标准化其网络事实模块的使用。Ansible 网络事实模块从系统收集信息,并将结果存储在前缀为ansible_net_的事实中。这些模块收集的数据记录在模块文档中的返回值中。对于 Ansible 网络模块来说,这是一个相当大的里程碑,因为默认情况下,抽象事实收集过程对您来说是一项繁重的工作。

让我们使用我们在第 4 章Python 自动化框架——Ansible 基础知识、Ansible 2.5 最佳实践中看到的相同结构,但在此基础上进行扩展,看看ios_facts模块是如何用于收集事实的。回顾一下,我们的清单文件包含两个 iOS 主机,主机变量位于host_vars目录中:

$ cat hosts
[ios-devices]
iosv-1
iosv-2

$ cat host_vars/iosv-1
---
ansible_host: 172.16.1.20
ansible_user: cisco
ansible_ssh_pass: cisco
ansible_connection: network_cli
ansible_network_os: ios
ansbile_become: yes
ansible_become_method: enable
ansible_become_pass: cisco

我们的剧本将有三个任务。第一个任务将使用ios_facts模块为我们的两个网络设备收集事实。第二个任务将显示为两个设备中的每个设备收集和存储的某些事实。您将看到,我们显示的事实是默认的ansible_net事实,而不是第一个任务中注册的变量。第三个任务将显示我们为iosv-1主机收集的所有事实:

$ cat my_playbook.yml
---
- name: Chapter 5 Ansible 2.5 network facts
 connection: network_cli
 gather_facts: false
 hosts: all
 tasks:
 - name: Gathering facts via ios_facts module
 ios_facts:
 when: ansible_network_os == 'ios'

 - name: Display certain facts
 debug:
 msg: "The hostname is {{ ansible_net_hostname }} running {{ ansible_net_version }}"

 - name: Display all facts for a host
 debug:
 var: hostvars['iosv-1']

当我们运行 playbook 时,您可以看到前两个任务的结果是我们所期望的:

$ ansible-playbook -i hosts my_playbook.yml

PLAY [Chapter 5 Ansible 2.5 network facts] *************************************

TASK [Gathering facts via ios_facts module] ************************************
ok: [iosv-2]
ok: [iosv-1]

TASK [Display certain facts] ***************************************************
ok: [iosv-2] => {
 "msg": "The hostname is iosv-2 running 15.6(3)M2"
}
ok: [iosv-1] => {
 "msg": "The hostname is iosv-1 running 15.6(3)M2"
}

第三个任务将显示为 iOS 设备收集的所有网络设备事实。为 iOS 设备收集了大量信息,这些信息有助于满足您的网络自动化需求:

TASK [Display all facts for a host] ********************************************
ok: [iosv-1] => {
 "hostvars['iosv-1']": {
 "ansbile_become": true,
 "ansible_become_method": "enable",
 "ansible_become_pass": "cisco",
 "ansible_check_mode": false,
 "ansible_connection": "network_cli",
 "ansible_diff_mode": false,
 "ansible_facts": {
 "net_all_ipv4_addresses": [
 "10.0.0.5",
 "172.16.1.20",
 "192.168.0.1"
 ],
 "net_all_ipv6_addresses": [],
 "net_filesystems": [
 "flash0:"
 ],
 "net_gather_subset": [
 "hardware",
 "default",
 "interfaces"
 ],
 "net_hostname": "iosv-1",
 "net_image": "flash0:/vios-adventerprisek9-m",
 "net_interfaces": {
 "GigabitEthernet0/0": {
 "bandwidth": 1000000,
 "description": "OOB Management",
 "duplex": "Full",
 "ipv4": [
 {
 "address": "172.16.1.20",
 "subnet": "24"
 }
[skip]

ANFILE 2.5 中的网络 FACTS 模块是简化您的工作流程并将其与其他服务器模块相媲美的一大步。

网络模块条件

让我们来看看另一个网络设备条件的例子,通过使用我们在本章开头看到的比较关键字。我们可以利用 IOSv 和 Arista EOS 都以 JSON 格式为show命令提供输出的事实。例如,我们可以检查接口的状态:

 arista1#sh interfaces ethernet 1/3 | json
 {
 "interfaces": {
 "Ethernet1/3": {
 "interfaceStatistics": {
 <skip>
 "outPktsRate": 0.0
 },
 "name": "Ethernet1/3",
 "interfaceStatus": "disabled",
 "autoNegotiate": "off",
 <skip>
 }
 arista1#

如果我们有一个我们想要执行的操作,并且它依赖于Ethernet1/3被禁用以不影响用户,例如确保没有用户主动连接到Ethernet1/3,我们可以在chapter5_3.yml剧本中使用以下任务。使用eos_command模块采集接口状态输出,并在进入下一个任务前使用waitforeq关键字检查接口状态:

    <skip>
     tasks:
       - name: "sh int ethernet 1/3 | json"
         eos_command:
           commands:
             - "show interface ethernet 1/3 | json"
           provider: "{{ cli }}"
           waitfor:
             - "result[0].interfaces.Ethernet1/3.interfaceStatus eq 
    disabled"
         register: output
       - name: show output
         debug:
           msg: "Interface Disabled, Safe to Proceed"

满足条件后,将执行第二个任务:

 TASK [sh int ethernet 1/3 | json] 
 **********************************************
 ok: [arista1]

 TASK [show output] 
 *************************************************************
 ok: [arista1] => {
 "msg": "Interface Disabled, Safe to Proceed"
 }

如果接口处于活动状态,则在执行第一个任务后会出现如下错误:

 TASK [sh int ethernet 1/3 | json] 
 **********************************************
 fatal: [arista1]: FAILED! => {"changed": false, "commands": ["show 
 interface ethernet 1/3 | json | json"], "failed": true, "msg": 
 "matched error in response: show interface ethernet 1/3 | json | 
 jsonrn% Invalid input (privileged mode required)rn********1>"}
 to retry, use: --limit 
 @/home/echou/Master_Python_Networking/Chapter5/chapter5_3.retry

 PLAY RECAP 
 ******************************************************************
 arista1 : ok=0 changed=0 unreachable=0 failed=1

检查其他条件,如containsgreater thanless than,因为它们适合您的情况。

易变环

Ansible 在剧本中提供了许多循环,例如标准循环、文件循环、子元素循环、do-Till 循环等等。在本节中,我们将介绍两种最常用的循环形式:标准循环和哈希值循环。

标准回路

剧本中的标准循环通常用于多次轻松执行类似任务。标准循环的语法非常简单:{{ item }}变量是在with_items列表上循环的占位符。例如,看看chapter5_4.yml剧本中的以下部分:

      tasks:
        - name: echo loop items
          command: echo {{ item }}
          with_items: ['r1', 'r2', 'r3', 'r4', 'r5']   

它将使用相同的echo命令循环五个列表项:

TASK [echo loop items] *********************************************************
changed: [192.168.199.185] => (item=r1)
changed: [192.168.199.185] => (item=r2)
changed: [192.168.199.185] => (item=r3)
changed: [192.168.199.185] => (item=r4)
changed: [192.168.199.185] => (item=r5)

我们将结合chapter5_5.yml剧本中的标准环路和网络命令模块,向设备添加多个 VLAN:

 tasks:
   - name: add vlans
     eos_config:
       lines:
           - vlan {{ item }}
       provider: "{{ cli }}"
     with_items:
         - 100
         - 200
         - 300

with_items列表也可以从变量中读取,这为剧本的结构提供了更大的灵活性:

vars:
  vlan_numbers: [100, 200, 300]
<skip>
tasks:
  - name: add vlans
    eos_config:
      lines:
          - vlan {{ item }}
      provider: "{{ cli }}"
    with_items: "{{ vlan_numbers }}"

当涉及到在剧本中执行冗余任务时,标准循环非常节省时间。它还通过减少任务所需的行数,使剧本更具可读性。

在下一节中,我们将了解字典上的循环。

翻阅字典

在一个简单的列表上循环是很好的。然而,我们通常有一个实体,它有多个与之关联的属性。如果您考虑上一节中的vlan示例,每个vlan都有几个独特的属性,例如vlan描述、网关 IP 地址,以及其他可能的属性。通常,我们可以使用字典来表示实体,以便将多个属性合并到实体中。

让我们在上一节的vlan示例上展开chapter5_6.yml中的词典示例。我们为三个vlans定义了字典值,每个都有一个嵌套的字典用于描述和 IP 地址:

    <skip> 
    vars:
       cli:
         host: "{{ ansible_host }}"
         username: "{{ username }}"
         password: "{{ password }}"
         transport: cli
       vlans: {
           "100": {"description": "floor_1", "ip": "192.168.10.1"},
           "200": {"description": "floor_2", "ip": "192.168.20.1"}
           "300": {"description": "floor_3", "ip": "192.168.30.1"}
       }

我们可以使用每个项目的键作为vlan编号来配置第一个任务add vlans

     tasks:
       - name: add vlans
         nxos_config:
           lines:
             - vlan {{ item.key }}
           provider: "{{ cli }}"
         with_dict: "{{ vlans }}"

我们可以继续配置vlan接口。请注意,我们使用parents参数来唯一标识应该检查命令的部分。这是因为描述和 IP 地址都是在配置中的interface vlan <number>小节下配置的:

  - name: configure vlans
    nxos_config:
       lines:
         - description {{ item.value.name }}
         - ip address {{ item.value.ip }}/24
       provider: "{{ cli }}"
       parents: interface vlan {{ item.key }}
    with_dict: "{{ vlans }}"

执行后,您将看到字典循环:

TASK [configure vlans] *********************************************************
changed: [nxos-r1] => (item={'key': u'300', 'value': {u'ip': u'192.168.30.1', u'name': u'floor_3'}})
changed: [nxos-r1] => (item={'key': u'200', 'value': {u'ip': u'192.168.20.1', u'name': u'floor_2'}})
changed: [nxos-r1] => (item={'key': u'100', 'value': {u'ip': u'192.168.10.1', u'name': u'floor_1'}})

让我们检查预期配置是否应用于设备:

nx-osv-1# sh run | i vlan
<skip>
vlan 1,10,100,200,300
nx-osv-1#
nx-osv-1# sh run | section "interface Vlan100"
interface Vlan100
 description floor_1
 ip address 192.168.10.1/24
nx-osv-1#

For more loop types of Ansible, feel free to check out the documentation (http://docs.ansible.com/ansible/playbooks_loops.html).

在使用词典的最初几次中,循环使用词典需要一些练习。但就像标准循环一样,在字典上循环将是你的工具带中一个无价的工具。

模板

在我的记忆中,作为一名网络工程师,我一直使用一种网络模板。根据我的经验,许多网络设备的网络配置部分是相同的,特别是如果这些设备在网络中扮演相同的角色。

大多数情况下,当我们需要配置新设备时,我们会以模板的形式使用相同的配置,替换必要的字段,然后将文件复制到新设备。使用 Ansible,您可以使用模板模块(自动完成所有工作 http://docs.ansible.com/ansible/template_module.html )。

我们使用的基本模板文件使用 Jinja2 模板语言(http://jinja.pocoo.org/docs/ )。我们在第 4*章【Python 自动化框架–Ansible Basics】*中简要讨论了 Jinja2 模板语言,我们将在这里进一步了解它。就像 Ansible 一样,Jinja2 有自己的语法和循环和条件的方法;幸运的是,为了我们的目的,我们只需要知道它的基本知识。Ansible 模板是我们将在日常任务中使用的一个重要工具,本节我们将花更多的时间来探索它。我们将通过逐步建立从简单到更复杂的剧本来学习语法。

模板使用的基本语法非常简单;您只需要指定源文件和要将其复制到的目标位置。

我们现在将创建一个空文件:

$ touch file1

然后,我们将使用下面的剧本将file1复制到file2。请注意,剧本仅在控制机器上执行。接下来,我们将指定源文件和目标文件的路径作为template模块的参数:

---
- name: Template Basic
  hosts: localhost

  tasks:
    - name: copy one file to another
      template:
        src=./file1
        dest=./file2

我们不需要在 playbook 执行期间指定主机文件,因为本地主机在默认情况下可用。但是,您将收到一条警告:

$ ansible-playbook chapter5_7.yml
 [WARNING]: provided hosts list is empty, only localhost is available
<skip>
TASK [copy one file to another] ************************************************

changed: [localhost]
<skip>

源文件可以有任何扩展名,但由于它们是通过 Jinja2 模板引擎处理的,所以让我们创建一个名为nxos.j2的文本文件作为模板源。模板将遵循 Jinja2 惯例,即使用双大括号指定变量:

    hostname {{ item.value.hostname }}
    feature telnet
    feature ospf
    feature bgp
    feature interface-vlan

    username {{ item.value.username }} password {{ item.value.password 
    }} role network-operator

Jinja2 模板

让我们也相应地修改剧本。在chapter5_8.yml中,我们将进行以下更改:

  1. 将源文件更改为nxos.j2
  2. 将目标文件更改为变量
  3. 将变量值作为我们将在模板中替换的字典提供:
    ---
    - name: Template Looping
      hosts: localhost

      vars:
        nexus_devices: {
          "nx-osv-1": {"hostname": "nx-osv-1", "username": "cisco", 
    "password": "cisco"}
        }

      tasks:
        - name: create router configuration files
          template:
            src=./nxos.j2
            dest=./{{ item.key }}.conf
          with_dict: "{{ nexus_devices }}"

运行 playbook 后,您将找到名为nx-osv-1.conf的目标文件,其中包含已填写的值,可供使用:

$ cat nx-osv-1.conf
hostname nx-osv-1

feature telnet
feature ospf
feature bgp
feature interface-vlan

username cisco password cisco role network-operator

Jinja2 环路

我们还可以循环浏览 Jinja2 中的列表和词典。我们将在nxos.j2中使用这两个作为循环:

    {% for vlan_num in item.value.vlans %}
    vlan {{ vlan_num }}
    {% endfor %}

    {% for vlan_interface in item.value.vlan_interfaces %}
    interface {{ vlan_interface.int_num }}
      ip address {{ vlan_interface.ip }}/24
    {% endfor %}

chapter5_8.yml剧本中提供额外的列表和字典变量:

   vars:
     nexus_devices: {
       "nx-osv-1": {
       "hostname": "nx-osv-1",
       "username": "cisco",
       "password": "cisco",
       "vlans": [100, 200, 300],
       "vlan_interfaces": [
          {"int_num": "100", "ip": "192.168.10.1"},
          {"int_num": "200", "ip": "192.168.20.1"},
          {"int_num": "300", "ip": "192.168.30.1"}
        ]
       }
     }

运行 playbook,您将看到路由器配置中填写的vlanvlan_interfaces配置。

金甲 2 号

Jinja2 还支持if条件检查。让我们添加此字段,以便为某些设备启用 netflow 功能。我们将在nxos.j2模板中添加以下内容:

    {% if item.value.netflow_enable %}
    feature netflow
    {% endif %}

我们将在剧本中列出不同之处:

    vars:
      nexus_devices: {
      <skip>
             "netflow_enable": True
      <skip>
     }

我们将进行的最后一步是通过将vlan接口部分放置在true-false条件检查中,使nxos.j2更具可伸缩性。在现实世界中,通常情况下,我们会有多台了解vlan信息的设备,但只有一台设备作为客户端主机的网关:

    {% if item.value.l3_vlan_interfaces %}
    {% for vlan_interface in item.value.vlan_interfaces %}
    interface {{ vlan_interface.int_num }}
     ip address {{ vlan_interface.ip }}/24
    {% endfor %}
    {% endif %}

我们还将在剧本中添加第二个设备,称为nx-osv-2

     vars:
       nexus_devices: {
       <skip>
         "nx-osv-2": {
           "hostname": "nx-osv-2",
           "username": "cisco",
           "password": "cisco",
           "vlans": [100, 200, 300],
           "l3_vlan_interfaces": False,
           "netflow_enable": False
         }
        <skip>
     }

我们现在已经准备好运行我们的剧本:

$ ansible-playbook chapter5_8.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'

PLAY [Template Looping] ********************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [create router configuration files] ***************************************
ok: [localhost] => (item={'value': {u'username': u'cisco', u'password': u'cisco', u'hostname': u'nx-osv-2', u'netflow_enable': False, u'vlans': [100, 200, 300], u'l3_vlan_interfaces': False}, 'key': u'nx-osv-2'})
ok: [localhost] => (item={'value': {u'username': u'cisco', u'password': u'cisco', u'hostname': u'nx-osv-1', u'vlan_interfaces': [{u'int_num': u'100', u'ip': u'192.168.10.1'}, {u'int_num': u'200', u'ip': u'192.168.20.1'}, {u'int_num': u'300', u'ip': u'192.168.30.1'}], u'netflow_enable': True, u'vlans': [100, 200, 300], u'l3_vlan_interfaces': True}, 'key': u'nx-osv-1'})

PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0

让我们检查两个配置文件中的差异,以确保条件更改正在发生:

$ cat nx-osv-1.conf
hostname nx-osv-1

feature telnet
feature ospf
feature bgp
feature interface-vlan

feature netflow

username cisco password cisco role network-operator

vlan 100
vlan 200
vlan 300

interface 100
 ip address 192.168.10.1/24
interface 200
 ip address 192.168.20.1/24
interface 300
 ip address 192.168.30.1/24

$ cat nx-osv-2.conf
hostname nx-osv-2

feature telnet
feature ospf
feature bgp
feature interface-vlan

username cisco password cisco role network-operator

vlan 100
vlan 200
vlan 300

整洁,嗯?这当然可以为我们节省大量的时间来完成之前需要重复复制和粘贴的事情。就个人而言,模板模块对我来说是一个巨大的游戏规则改变者。仅仅这个模块就足以激励我在几年前学习和使用 Ansible。

我们的剧本越来越长了。在下一节中,我们将看到如何通过将变量文件卸载到组和目录中来优化 playbook

组和主机变量

请注意,在前面的剧本chapter5_8.yml中,我们在nexus_devices变量下重复了两台设备的用户名和密码变量:

    vars:
      nexus_devices: {
        "nx-osv-1": {
          "hostname": "nx-osv-1",
          "username": "cisco",
          "password": "cisco",
          "vlans": [100, 200, 300],
        <skip>
        "nx-osv-2": {
          "hostname": "nx-osv-2",
          "username": "cisco",
          "password": "cisco",
          "vlans": [100, 200, 300],
        <skip>

这并不理想。如果我们需要更新用户名和密码值,我们需要记住在两个位置进行更新。这增加了管理负担,也增加了出错的机会。为了获得最佳实践,Ansible 建议我们使用group_varshost_vars目录来分离变量。

For more Ansible best practices, check out http://docs.ansible.com/ansible/playbooks_best_practices.html.

组变量

默认情况下,Ansible 将在名为group_vars的 playbook 的同一目录中查找可应用于组的变量。默认情况下,它将查找与清单文件中的组名匹配的文件名。例如,如果我们在库存文件中有一个名为[nexus-devices]的组,那么我们可以在group_vars下有一个名为nexus-devices的文件来存放可以应用于该组的所有变量。

我们还可以使用一个名为all的特殊文件来包含应用于所有组的变量。

我们将为用户名和密码变量使用此功能。首先,我们将创建group_vars目录:

$ mkdir group_vars

然后,我们可以创建一个名为all的 YAML 文件,其中包含用户名和密码:

$ cat group_vars/all
---
username: cisco
password: cisco

然后,我们可以在剧本中使用变量:

    vars:
      nexus_devices: {
       "nx-osv-1": {
          "hostname": "nx-osv-1",
          "username": "{{ username }}",
          "password": "{{ password }}",
          "vlans": [100, 200, 300],
        <skip>
         "nx-osv-2": {
          "hostname": "nx-osv-2",
          "username": "{{ username }}",
          "password": "{{ password }}",
          "vlans": [100, 200, 300],
        <skip>

主变量

我们可以用与组变量相同的格式进一步分离主机变量。这就是我们如何在第 4 章Python 自动化框架–Ansible 基础以及本章前面的 Ansible 2.5 剧本示例中应用变量的方法:

$ mkdir host_vars

在我们的例子中,我们在本地主机上执行命令,host_vars下的文件应该相应地命名,例如host_vars/localhost。在我们的host_vars/localhost文件中,我们还可以保留group_vars中声明的变量:

$ cat host_vars/localhost
---
"nexus_devices":
 "nx-osv-1":
 "hostname": "nx-osv-1"
 "username": "{{ username }}"
 "password": "{{ password }}"
 "vlans": [100, 200, 300]
 "l3_vlan_interfaces": True
 "vlan_interfaces": [
 {"int_num": "100", "ip": "192.168.10.1"},
 {"int_num": "200", "ip": "192.168.20.1"},
 {"int_num": "300", "ip": "192.168.30.1"}
 ]
 "netflow_enable": True

 "nx-osv-2":
 "hostname": "nx-osv-2"
 "username": "{{ username }}"
 "password": "{{ password }}"
 "vlans": [100, 200, 300]
 "l3_vlan_interfaces": False
 "netflow_enable": False

在我们分离出变量后,playbook 现在变得非常轻量级,只包含我们操作的逻辑:

 $ cat chapter5_9.yml
 ---
 - name: Ansible Group and Host Variables
 hosts: localhost

 tasks:
 - name: create router configuration files
 template:
 src=./nxos.j2
 dest=./{{ item.key }}.conf
 with_dict: "{{ nexus_devices }}"

group_varshost_vars目录不仅可以减少我们的操作开销,还可以通过允许我们使用 Ansible Vault 加密敏感信息来帮助保护文件的安全,我们将在下面介绍。

可移动的拱顶

从上一节可以看到,在大多数情况下,Ansible 变量提供敏感信息,如用户名和密码。在变量周围设置一些安全措施是个好主意,这样我们就可以防范它们。Ansible 保险库(https://docs.ansible.com/ansible/2.5/user_guide/vault.html 为文件提供加密,使其以明文形式显示。

所有 Ansible Vault 功能都以ansible-vault命令开始。您可以通过“创建”选项手动创建加密文件。系统将要求您输入密码。如果您试图查看该文件,您会发现该文件不是明文。如果您下载了本书示例,我使用的密码只是单词password

$ ansible-vault create secret.yml
Vault password: <password>

$ cat secret.yml
$ANSIBLE_VAULT;1.1;AES256
336564626462373962326635326361323639323635353630646665656430353261383737623<skip>653537333837383863636530356464623032333432386139303335663262
3962

要编辑或查看加密文件,我们将使用edit选项通过view选项编辑或查看文件:

$ ansible-vault edit secret.yml 
Vault password:

$ ansible-vault view secret.yml 
Vault password:

让我们对group_vars/all and host_vars/localhost变量文件进行加密:

$ ansible-vault encrypt group_vars/all host_vars/localhost
Vault password:
Encryption successful

现在,当我们运行 playbook 时,我们将得到一条解密失败的错误消息:

ERROR! Decryption failed on /home/echou/Master_Python_Networking/Chapter5/Vaults/group_vars/all

在运行剧本时,我们需要使用--ask-vault-pass选项:

$ ansible-playbook chapter5_10.yml --ask-vault-pass
Vault password:

对于访问的任何 Vault 加密文件,将在内存中进行解密。

Prior to Ansible 2.4, Ansible Vault required all the files to be encrypted with the same password. Since Ansible 2.4 and later, you can use vault ID to supply a different password file (https://docs.ansible.com/ansible/2.5/user_guide/vault.html#multiple-vault-passwords).

我们还可以将密码保存在文件中,并确保特定文件具有受限权限:

$ chmod 400 ~/.vault_password.txt
$ ls -lia ~/.vault_password.txt 
809496 -r-------- 1 echou echou 9 Feb 18 12:17 /home/echou/.vault_password.txt

然后我们可以使用--vault-password-file选项执行剧本:

$ ansible-playbook chapter5_10.yml --vault-password-file ~/.vault_password.txt

我们也可以只加密一个字符串,并使用encrypt_string选项(将加密的字符串嵌入剧本中 https://docs.ansible.com/ansible/2.5/user_guide/vault.html#use-加密字符串以创建要嵌入 yaml中的加密变量:

$ ansible-vault encrypt_string
New Vault password:
Confirm New Vault password:
Reading plaintext input from stdin. (ctrl-d to end input)
new_user_password
!vault |
 $ANSIBLE_VAULT;1.1;AES256
 616364386438393262623139623561613539656664383834643338323966623836343737373361326134663232623861313338383534613865303864616364380a626365393665316133616462643831653332663263643734363863666632636464636563616265303665626364636562316635636462323135663163663331320a62356361326639333165393962663962306630303761656435633966633437613030326633336438366264626464366138323666376239656633623233353832

Encryption successful

然后可以将字符串作为变量放置在 playbook 文件中。在下一节中,我们将使用includeroles进一步优化我们的剧本

Ansible 包括和角色

处理复杂任务的最佳方法是将它们分解成更小的部分。当然,这种方法在 Python 和网络工程中都很常见。在 Python 中,我们将复杂的代码分解为函数、类、模块和包。在网络中,我们还将大型网络划分为机架、行、集群和数据中心等部分。在 Ansible 中,我们可以使用rolesincludes将大型剧本分割并组织成多个文件。分解一个大型 Ansible playbook 可以简化结构,因为每个文件只关注较少的任务。它还允许重用剧本的各个部分。

Ansible include 语句

随着剧本规模的扩大,许多任务和剧本可以在不同的剧本中共享,这一点最终将变得显而易见。Ansibleinclude语句类似于许多 Linux 配置文件,它们只是告诉机器以与直接写入文件相同的方式扩展文件。我们可以在剧本和任务中使用include语句。这里,我们将看一个扩展任务的简单示例。

假设我们想要显示两个不同剧本的输出。我们可以制作一个名为show_output.yml的单独 YAML 文件作为附加任务:

    ---
    - name: show output
        debug:
          var: output

然后,我们可以在多个 playbook 中重用此任务,例如在chapter5_11_1.yml中,除了在末尾注册输出和 include 语句外,它看起来与上一个 playbook 基本相同:

    ---
    - name: Ansible Group and Host Varibles
      hosts: localhost

      tasks:
        - name: create router configuration files
          template:
            src=./nxos.j2
            dest=./{{ item.key }}.conf
          with_dict: "{{ nexus_devices }}"
          register: output

        - include: show_output.yml

另一个剧本chapter5_11_2.yml可以以同样的方式重用show_output.yml

    ---
    - name: show users
      hosts: localhost

      tasks:
        - name: show local users
          command: who
          register: output

        - include: show_output.yml

请注意,两个剧本都使用相同的变量名output,因为在show_output.yml中,为了简单起见,我们硬编码了变量名。您还可以将变量传递到包含的文件中。

可替代角色

Ansible 角色将逻辑功能与物理主机分离,以更好地适应您的网络。例如,您可以构建角色,如 spines、leafs、core 以及 Cisco、Juniper 和 Arista。同一物理主机可以属于多个角色;例如,设备可以同时属于 Juniper 和 core。这种灵活性允许我们执行操作,例如升级所有 Juniper 设备,而不必担心设备在网络层中的位置。

Ansible 角色可以基于已知的文件基础结构自动加载某些变量、任务和处理程序。关键是这是我们自动包含的已知文件结构。事实上,你可以把角色想象成 Ansible 预先做出的include陈述。

Ansible 剧本角色文档(http://docs.ansible.com/ansible/playbooks_roles.html#roles 描述了我们可以配置的角色目录列表。我们不需要全部使用它们。在我们的示例中,我们将只修改tasks and the vars文件夹。但是,最好了解 Ansible 角色目录结构中的所有可用选项。

以下是我们将用作角色示例的内容:

├── chapter5_12.yml
├── chapter5_13.yml
├── hosts
└── roles
 ├── cisco_nexus
 │   ├── defaults
 │   ├── files
 │   ├── handlers
 │   ├── meta
 │   ├── tasks
 │   │   └── main.yml
 │   ├── templates
 │   └── vars
 │       └── main.yml
 └── spines
 ├── defaults
 ├── files
 ├── handlers
 ├── tasks
 │   └── main.yml
 ├── templates
 └── vars
 └── main.yml

您可以看到,在顶层,我们有 hosts 文件和 playbook。我们还有一个名为roles的文件夹。在文件夹中,我们定义了两个角色:cisco_nexusspines。除tasks and vars文件夹外,角色下的大多数子文件夹均为空。每个文件中都有一个名为main.yml的文件。这是默认行为:main.yml文件是您在剧本中指定角色时自动包含在剧本中的入口点。如果需要拆分其他文件,可以在main.yml文件中使用 include 语句。

下面是我们的场景:

  • 我们有两台思科 Nexus 设备,nxos-r1nxos-r2。我们将使用cisco_nexus角色为他们配置日志服务器以及所有人的日志链接状态。
  • 此外,NXS-r1 也是一种 spine 设备,我们希望在其中配置更详细的日志记录,可能是因为 spine 在我们的网络中处于更关键的位置。

对于我们的cisco_nexus角色,我们在roles/cisco_nexus/vars/main.yml中有以下变量:

---
cli:
  host: "{{ ansible_host }}"
  username: cisco
  password: cisco
  transport: cli

我们在roles/cisco_nexus/tasks/main.yml中有以下配置任务:

---
- name: configure logging parameters
  nxos_config:
    lines:
      - logging server 191.168.1.100
      - logging event link-status default
    provider: "{{ cli }}"

我们的 playbook 非常简单,因为它只需要根据cisco_nexus role指定我们想要配置的主机:

---
- name: playbook for cisco_nexus role
  hosts: "cisco_nexus"
  gather_facts: false
  connection: local

  roles:
    - cisco_nexus

当您运行 playbook 时,playbook 将包含在cisco_nexus角色中定义的任务和变量,并相应地配置设备。

对于我们的spine角色,我们将有一个额外的任务,在roles/spines/tasks/mail.yml中进行更详细的登录:

---
- name: change logging level
  nxos_config:
    lines:
      - logging level local7 7
    provider: "{{ cli }}"

在我们的剧本中,我们可以指定它同时包含cisco_nexusspines的角色:

---
- name: playbook for spine role
  hosts: "spines"
  gather_facts: false
  connection: local

  roles:
    - cisco_nexus
    - spines

当我们按此顺序包含这两个角色时,将执行cisco_nexus角色任务,然后是 spines 角色:

TASK [cisco_nexus : configure logging parameters] ******************************
changed: [nxos-r1]

TASK [spines : change logging level] *******************************************
ok: [nxos-r1]

Ansible 角色具有灵活性和可伸缩性,就像 Python 函数和类一样。一旦您的代码增长超过某个级别,为了可维护性,将其分解成更小的部分几乎总是一个好主意。

You can find more examples of roles in the Ansible examples Git repository at https://github.com/ansible/ansible-examples.

Ansible 星系https://docs.ansible.com/ansible/latest/reference_appendices/galaxy.html 是一个免费的社区站点,用于查找、共享和协作角色。您可以在 Ansible Galaxy 上看到 Ansible 角色提供的 Juniper 网络示例:

JUNOS Role on Ansible Galaxy ( https://galaxy.ansible.com/Juniper/junos)

在下一节中,我们将了解如何编写自己的自定义 Ansible 模块

编写自己的自定义模块

现在,您可能会觉得 Ansible 中的网络管理在很大程度上取决于为您的任务找到合适的模块。这种逻辑当然有很多道理。模块提供了一种抽象托管主机和控制机器之间交互的方法;它们使我们能够专注于业务逻辑。到目前为止,我们已经看到主要供应商为 Cisco、Juniper 和 Arista 提供了广泛的模块。

以 Cisco Nexus 模块为例,除了管理 BGP 邻居(nxos_bgp和 aaa 服务器(nxos_aaa_server等特定任务外。大多数供应商还提供运行任意显示(nxos_config和配置命令(nxos_config的方法。这通常涵盖我们的大多数用例。

Starting with Ansible 2.5, there is also the streamline naming and usage of network facts modules. 

如果您正在使用的设备当前没有用于您正在查找的任务的模块,该怎么办?在本节中,我们将通过编写我们自己的自定义模块来了解几种可以纠正这种情况的方法。

第一个自定义模块

编写自定义模块不需要太复杂;事实上,它甚至不需要使用 Python。但是,由于我们已经熟悉 Python,我们将在自定义模块中使用 Python。我们假设该模块是我们自己和我们的团队将使用的模块,而无需提交回 Ansible,因此我们将暂时忽略一些文档和格式

If you are interested in developing modules that can be submitted upstream to Ansible, please consult the developing modules guide from Ansible (https://docs.ansible.com/ansible/latest/dev_guide/developing_modules.html). 

默认情况下,如果我们在与 playbook 相同的目录中创建一个名为library的文件夹,Ansible 将在模块搜索路径中包含该目录。因此,我们可以将自定义模块放在目录中,并可以在剧本中使用它。定制模块的要求非常简单:模块需要的只是将 JSON 输出返回到 playbook。

回想一下在第 3 章API 和意图驱动网络中,我们使用以下 NXAPI Python 脚本与 NX-OS 设备进行通信:

    import requests
    import json

    url='http://172.16.1.142/ins'
    switchuser='cisco'
    switchpassword='cisco'

    myheaders={'content-type':'application/json-rpc'}
    payload=[
     {
       "jsonrpc": "2.0",
       "method": "cli",
       "params": {
         "cmd": "show version",
         "version": 1.2
       },
       "id": 1
     }
    ]
    response = requests.post(url,data=json.dumps(payload),   
    headers=myheaders,auth=(switchuser,switchpassword)).json()

    print(response['result']['body']['sys_ver_str'])

当我们执行它时,我们只收到系统版本。我们只需将最后一行修改为 JSON 输出,如下代码所示:

    version = response['result']['body']['sys_ver_str']
    print json.dumps({"version": version})

我们将此文件放在library文件夹下:

$ ls -a library/
. .. custom_module_1.py

在我们的剧本中,我们可以使用动作插件(https://docs.ansible.com/ansible/dev_guide/developing_plugins.htmlchapter5_14.yml,调用此自定义模块:

    ---
    - name: Your First Custom Module
      hosts: localhost
      gather_facts: false
      connection: local

      tasks:
        - name: Show Version
          action: custom_module_1
          register: output

        - debug:
            var: output

注意,就像ssh连接一样,我们在本地执行模块,模块向外进行 API 调用。执行此剧本时,您将获得以下输出:

$ ansible-playbook chapter5_14.yml
 [WARNING]: provided hosts list is empty, only localhost is available

PLAY [Your First Custom Module] ************************************************

TASK [Show Version] ************************************************************
ok: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => {
 "output": {
 "changed": false,
 "version": "7.3(0)D1(1)"
 }
}

PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0

如您所见,您可以编写 API 支持的任何模块,Ansible 将乐于接受任何返回的 JSON 输出。

第二个自定义模块

在最后一个模块的基础上,让我们利用模块开发文档(中所述的 Ansible 中的通用模块样板文件 http://docs.ansible.com/ansible/dev_guide/developing_modules_general.html 。我们将修改最后一个自定义模块并创建custom_module_2.py以接收剧本中的输入。

首先,我们将从ansible.module_utils.basic导入样板代码:

    from ansible.module_utils.basic import AnsibleModule

    if __name__ == '__main__':
        main()

从那里,我们可以定义主要功能,我们将在那里存放我们的代码。我们已经导入了AnsibleModule,它提供了许多用于处理返回和解析参数的通用代码。在下面的示例中,我们将解析hostusernamepassword的三个参数,并将它们设置为必填字段:

    def main():
        module = AnsibleModule(
          argument_spec = dict(
          host = dict(required=True),
          username = dict(required=True),
          password = dict(required=True)
      )
    )

然后可以在我们的代码中检索和使用这些值:

     device = module.params.get('host')
     username = module.params.get('username')
     password = module.params.get('password')

     url='http://' + host + '/ins'
     switchuser=username
     switchpassword=password

最后,我们将按照退出代码返回值:

    module.exit_json(changed=False, msg=str(data))

我们的新 playbook,chapter5_15.yml看起来与上一个 playbook 相同,只是现在我们可以在 playbook 中传递不同设备的值:

     tasks:
       - name: Show Version
         *action: custom_module_1 host="172.16.1.142" username="cisco"* 
 *password="cisco"*
         register: output

执行时,此剧本将产生与上一个剧本完全相同的输出。但是,因为我们在自定义模块中使用参数,所以现在可以将自定义模块传递给其他人使用,而不需要他们知道我们模块的详细信息。他们可以在剧本中写入自己的用户名、密码和主机 IP

当然,这是一个功能性但不完整的模块。首先,我们没有执行任何错误检查,也没有提供任何使用文档。然而,它很好地展示了构建自定义模块是多么容易。另外一个好处是,我们看到了如何使用已经生成的现有脚本,并将其转换为自定义 Ansible 模块。

总结

在这一章中,我们涵盖了很多领域。基于之前对 Ansible 的了解,我们扩展到更高级的主题,如条件、循环和模板。我们研究了如何使用主机变量、组变量、include 语句和角色使我们的剧本更具可伸缩性。我们还研究了如何用 Ansible 保险库保护我们的剧本。最后,我们使用 Python 来创建自己的自定义模块。

Ansible 是一个非常灵活的 Python 框架,可用于网络自动化。它提供了另一个抽象层,与基于 Pexpect 和 API 的脚本相分离。它本质上是声明性的,因为它在符合我们的意图方面更具表现力。根据您的需要和网络环境,它可能是您可以用来节省时间和精力的理想框架。

第 6 章Python 网络安全中,我们将用 Python 来研究网络安全。*