第308篇:Ansible 网络模块——Playbook 管理设备

关键词:Ansible、Playbook、Inventory、网络模块、幂等性、声明式自动化、配置管理


一、引言

在前几篇中,我们逐步积累了Python编程、SSH连接、多厂商抽象、模板引擎和结构化数据格式的能力。但这些能力分散在不同工具中,需要一个统一的框架来串联。

Ansible是IT自动化领域使用最广泛的编排工具。它采用无代理(Agentless)架构,通过SSH连接目标设备;使用声明式(Declarative)语法编写Playbook,只描述"期望状态"而非"执行步骤";内置的幂等性引擎确保重复执行不会产生副作用。

对于网络工程师而言,Ansible意味着:编写一份Playbook,就能批量管理数百台异构设备的配置、状态和合规性。


二、Ansible 安装与架构

2.1 安装

# 安装Ansible核心
pip install ansible-core

# 安装网络集合(包含所有网络厂商模块)
ansible-galaxy collection install ansible.netcommon
ansible-galaxy collection install ansible.utils
ansible-galaxy collection install cisco.ios
ansible-galaxy collection install arista.eos
ansible-galaxy collection install junipernetworks.junos

# 华为设备需要安装社区驱动
ansible-galaxy collection install hetzner.hpeoneview
# 或使用社区贡献的华为模块
pip install ansible-modules-for-huawei

2.2 核心组件

核心组件 - 图示

组件 说明
Playbook 自动化任务剧本,YAML格式,声明式语法
Inventory 目标设备清单,定义主机、分组、变量
Modules 执行具体操作的模块(如ios_config、eos_command)
Roles 将Playbook结构化封装为可复用单元
Vault 加密敏感信息(密码、密钥)
Connection Plugin SSH连接插件,负责与设备建立会话

2.3 目录结构

ansible-network/
- ansible.cfg              # Ansible配置文件
- inventory/
  - hosts.yaml           # 设备清单
  - group_vars/
    - core.yaml        # 核心交换机变量
    - aggregation.yaml # 汇聚交换机变量
    - access.yaml      # 接入交换机变量
  - host_vars/
    - core-sw-01.yaml  # 单机变量
- playbooks/
  - deploy_vlans.yaml    # VLAN部署剧本
  - deploy_bgp.yaml      # BGP部署剧本
  - backup_configs.yaml  # 配置备份剧本
  - site_provisioning.yaml # 站点级部署
- roles/
  - network_base/        # 基础配置角色
    - tasks/
    - templates/
    - defaults/
    - vars/
  - bgp_deploy/          # BGP部署角色
    - tasks/
    - templates/
    - defaults/
- templates/
  - switch_base.j2       # 交换机基础配置模板
  - bgp_config.j2        # BGP配置模板
- vault_password           # Vault加密密码文件

2.4 ansible.cfg 基础配置

# ansible.cfg
[defaults]
# 设备清单路径
inventory = ./inventory/hosts.yaml

# 模块路径
collections_paths = ~/.ansible/collections

# 日志
log_path = ./logs/ansible.log

# 超时
timeout = 30

# 并行执行数(同时操作的设备数)
forks = 10

# 禁用事实收集(网络设备不需要)
gathering = explicit

# 主机密钥检查
host_key_checking = False

[privilege_escalation]
# 网络设备通常不需要become

[ssh_connection]
# 复用SSH连接
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True

三、Inventory 设备清单

3.1 静态清单(YAML格式)

# inventory/hosts.yaml
all:
  children:
    core:
      vars:
        ansible_network_os: huawei    # 华为可用 netmiko 连接
        ansible_connection: ansible.netcommon.network_cli
        ansible_user: admin
        ansible_password: "{{ vault_password }}"
        ansible_become: true
        ansible_become_method: enable
        ansible_become_password: "{{ vault_enable_password }}"
        device_role: core
        ospf_area: "0.0.0.0"
        bgp_local_as: 65001
      hosts:
        core-sw-01:
          ansible_host: 192.168.1.1
          mgmt_ip: 10.255.0.1
          router_id: "10.255.0.1"
        core-sw-02:
          ansible_host: 192.168.1.2
          mgmt_ip: 10.255.0.2
          router_id: "10.255.0.2"

    aggregation:
      vars:
        ansible_network_os: huawei
        ansible_connection: ansible.netcommon.network_cli
        ansible_user: admin
        ansible_password: "{{ vault_password }}"
        device_role: aggregation
        ospf_area: "0.0.0.1"
      hosts:
        agg-sw-01:
          ansible_host: 192.168.1.10
          router_id: "10.255.1.1"
        agg-sw-02:
          ansible_host: 192.168.1.11
          router_id: "10.255.1.2"

    access:
      vars:
        ansible_network_os: huawei
        ansible_connection: ansible.netcommon.network_cli
        ansible_user: admin
        ansible_password: "{{ vault_password }}"
        device_role: access
      hosts:
        access-sw-01:
          ansible_host: 192.168.1.20
        access-sw-02:
          ansible_host: 192.168.1.21

3.2 Vault 加密敏感信息

# 创建加密文件
ansible-vault create group_vars/all/vault.yaml

# 编辑加密文件
ansible-vault edit group_vars/all/vault.yaml

# 查看加密文件内容
ansible-vault view group_vars/all/vault.yaml

# 运行Playbook时提供密码
ansible-playbook playbooks/deploy_vlans.yaml --ask-vault-pass

# 或使用密码文件
ansible-playbook playbooks/deploy_vlans.yaml --vault-password-file vault_password
# group_vars/all/vault.yaml(加密存储)
vault_password: H@wa123!
vault_enable_password: En@ble123!

四、核心网络模块详解

4.1 command 模块:执行单条命令

# playbooks/show_version.yaml
---
- name: 采集所有核心交换机的版本信息
  hosts: core
  gather_facts: no

  tasks:
    - name: 获取设备版本
      ansible.netcommon.cli_command:
        command: display version
      register: version_output

    - name: 打印版本信息
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} -> {{ version_output.stdout_lines | first }}"

4.2 config 模块:下发配置

以思科IOS为例(华为设备类似):

# playbooks/deploy_vlans.yaml
---
- name: 批量部署VLAN配置
  hosts: core
  gather_facts: no

  tasks:
    - name: 下发VLAN配置
      ansible.netcommon.config:
        lines:
          - vlan 10
          - description USER_VLAN
          - vlan 20
          - description SERVER_VLAN
          - vlan 100
          - description MANAGEMENT
      register: config_result

    - name: 显示配置变更
      ansible.builtin.debug:
        var: config_result

4.3 ios_config / eos_config:配置模块

# playbooks/deploy_interfaces.yaml
---
- name: 配置接口
  hosts: core
  gather_facts: no

  tasks:
    - name: 配置接口描述和Trunk
      cisco.ios.ios_config:
        lines:
          - description To-Core-SW-02
          - switchport mode trunk
          - switchport trunk allowed vlan 10,20,100
        parents: interface GigabitEthernet0/1

    - name: 保存配置
      cisco.ios.ios_config:
        save_when: modified

4.4 template 模块:模板化配置

# playbooks/deploy_from_template.yaml
---
- name: 从模板生成并部署完整配置
  hosts: all
  gather_facts: no

  tasks:
    - name: 渲染配置模板
      ansible.builtin.template:
        src: templates/{{ device_role }}_config.j2
        dest: /tmp/{{ inventory_hostname }}_generated.cfg
        mode: '0644'
      delegate_to: localhost

    - name: 从模板文件部署配置
      ansible.netcommon.config:
        src: /tmp/{{ inventory_hostname }}_generated.cfg
      register: deploy_result

    - name: 部署结果
      ansible.builtin.debug:
        msg: >
          {{ inventory_hostname }} 部署完成 |
          变更行数: {{ deploy_result.changed }}
{# templates/core_config.j2 #}
! Generated by Ansible on {{ ansible_date_time.iso8601 }}
! Device: {{ inventory_hostname }}

hostname {{ hostname }}

{# VLAN配置 #}
{% for vlan_id, vlan_info in vlans.items() %}
vlan {{ vlan_id }}
 description {{ vlan_info.description }}
{% if vlan_info.ipv4_subnet %}
 ip interface {{ vlan_info.ipv4_subnet }}
{% endif %}
{% endfor %}

{# 接口配置 #}
{% for iface in interfaces %}
interface {{ iface.name }}
 description {{ iface.description }}
{% if iface.type == "trunk" %}
 switchport mode trunk
 switchport trunk allowed vlan {{ iface.allowed_vlans }}
{% elif iface.type == "access" %}
 switchport mode access
 switchport access vlan {{ iface.pvid }}
{% endif %}
{% endfor %}

{# OSPF配置 #}
{% if ospf_enabled | default(false) %}
router ospf {{ ospf_process_id | default(1) }}
 router-id {{ router_id }}
{% for network in ospf_networks %}
 network {{ network.prefix }} {{ network.wildcard }} area {{ network.area }}
{% endfor %}
{% endif %}

五、高级 Playbook 技巧

5.1 条件执行(when)

---
- name: 按角色执行不同操作
  hosts: all
  gather_facts: no

  tasks:
    # 仅对核心设备执行BGP配置
    - name: 部署BGP配置(仅核心)
      ansible.netcommon.config:
        lines: "{{ bgp_config_lines }}"
      when: device_role == "core"

    # 仅对汇聚设备执行OSPF配置
    - name: 部署OSPF配置(仅汇聚)
      ansible.netcommon.config:
        lines: "{{ ospf_config_lines }}"
      when: device_role == "aggregation"

    # 仅对接入设备执行STP配置
    - name: 部署STP配置(仅接入)
      ansible.netcommon.config:
        lines: "{{ stp_config_lines }}"
      when: device_role == "access"

    # 对所有设备执行基础安全配置
    - name: 部署SSH安全配置
      ansible.netcommon.config:
        lines:
          - ip ssh version 2
          - ip ssh maxstartups 10
          - line vty 0 15
          - transport input ssh

5.2 循环(with_items / loop)

---
- name: 批量配置VLAN
  hosts: all
  gather_facts: no

  tasks:
    - name: 创建VLAN列表
      ansible.builtin.debug:
        msg: "正在为 {{ inventory_hostname }} 创建 {{ item.vlan_id }} ({{ item.name }})"
      loop: "{{ vlans }}"
      loop_control:
        label: "{{ item.vlan_id }} - {{ item.name }}"

    - name: 批量配置接口描述
      ansible.netcommon.cli_command:
        command: "display current-configuration interface {{ item }}"
      loop:
        - GigabitEthernet0/0/1
        - GigabitEthernet0/0/2
        - GigabitEthernet0/0/3
      register: interface_configs

    - name: 打印结果
      ansible.builtin.debug:
        var: interface_configs.results

5.3 错误处理与异常处理

---
- name: 安全部署配置(含回滚)
  hosts: core
  gather_facts: no

  tasks:
    # 步骤1:备份当前配置
    - name: 备份当前运行配置
      ansible.netcommon.cli_command:
        command: display current-configuration
      register: backup_config

    - name: 保存备份到本地
      ansible.builtin.copy:
        content: "{{ backup_config.stdout }}"
        dest: "backups/{{ inventory_hostname }}_pre_{{ ansible_date_time.iso8601 }}.cfg"
      delegate_to: localhost

    # 步骤2:部署新配置
    - name: 部署新配置
      ansible.netcommon.config:
        lines: "{{ new_config_lines }}"
      register: deploy_result

    # 步骤3:验证配置
    - name: 验证关键接口状态
      ansible.netcommon.cli_command:
        command: display interface brief
      register: verify_output

    - name: 检查接口Up状态
      ansible.builtin.assert:
        that:
          - "'up  up' in verify_output.stdout"
        fail_msg: "验证失败:有关键接口Down"
        success_msg: "验证通过:关键接口均Up"

    # 步骤4:保存配置
    - name: 保存配置到设备
      ansible.netcommon.cli_command:
        command: save force

  rescue:
    # 异常处理:自动回滚
    - name: 告警:部署失败,准备回滚
      ansible.builtin.debug:
        msg: "WARNING: {{ inventory_hostname }} 配置部署失败,执行回滚"

    - name: 回滚配置
      ansible.netcommon.config:
        lines: "{{ rollback_config_lines }}"
      ignore_errors: true

    - name: 记录回滚事件
      ansible.builtin.debug:
        msg: "已执行回滚,请人工检查 {{ inventory_hostname }}"

5.4 异步执行与轮询

---
- name: 异步执行长时间任务
  hosts: core
  gather_facts: no

  tasks:
    - name: 异步执行全量配置备份
      ansible.netcommon.cli_command:
        command: display current-configuration
      async: 300          # 最长等待300秒
      poll: 10            # 每10秒轮询一次
      register: backup_async

    - name: 检查异步任务状态
      ansible.builtin.async_status:
        jid: "{{ backup_async.ansible_job_id }}"
      register: backup_result
      until: backup_result.finished
      retries: 30

六、Roles:可复用角色

6.1 角色结构

roles/
- network_base/
  - tasks/
    - main.yaml              # 入口
    - ssh_config.yaml        # SSH配置
    - vlan_config.yaml       # VLAN配置
    - interface_config.yaml  # 接口配置
    - save_config.yaml       # 保存配置
  - templates/
    - base_config.j2         # 基础配置模板
  - defaults/
    - main.yaml              # 默认变量(最低优先级)
  - vars/
    - main.yaml              # 角色变量(高优先级)
  - handlers/
    - main.yaml              # 处理器
  - meta/
    - main.yaml              # 角色依赖

6.2 角色实现示例

# roles/network_base/defaults/main.yaml
---
base_vlan_list: []
base_interfaces: []
ssh_max_sessions: 10
save_config_after_change: true
# roles/network_base/tasks/main.yaml
---
- name: 执行SSH安全配置
  ansible.builtin.include_tasks: ssh_config.yaml

- name: 执行VLAN配置
  ansible.builtin.include_tasks: vlan_config.yaml
  when: base_vlan_list | length > 0

- name: 执行接口配置
  ansible.builtin.include_tasks: interface_config.yaml
  when: base_interfaces | length > 0

- name: 保存配置
  ansible.builtin.include_tasks: save_config.yaml
  when: save_config_after_change
# roles/network_base/tasks/vlan_config.yaml
---
- name: 批量创建VLAN
  ansible.netcommon.config:
    lines:
      - "vlan {{ item.vlan_id }}"
      - "description {{ item.name }}"
  loop: "{{ base_vlan_list }}"
  loop_control:
    label: "VLAN {{ item.vlan_id }}"

6.3 使用角色

# playbooks/deploy_with_roles.yaml
---
- name: 部署所有设备基础配置
  hosts: all
  gather_facts: no

  pre_tasks:
    - name: 显示设备信息
      ansible.builtin.debug:
        msg: "正在配置 {{ inventory_hostname }} ({{ device_role }})"

  roles:
    - role: network_base
      when: device_role is defined

    - role: bgp_deploy
      when: device_role == "core"

    - role: ospf_deploy
      when: device_role in ["core", "aggregation"]

  post_tasks:
    - name: 验证关键接口
      ansible.netcommon.cli_command:
        command: display interface brief
      register: final_check

    - name: 记录最终状态
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} 部署完成"

七、故障案例:Ansible幂等性失效导致配置重复

7.1 故障现象

运维团队每日定时执行Ansible Playbook部署ACL策略,一周后发现某台核心交换机上出现了10份完全相同的ACL规则,导致CPU利用率飙升至95%。

7.2 排查过程

Playbook片段:

# 问题代码
- name: 应用ACL策略
  ansible.netcommon.config:
    lines:
      - traffic-policy ACL_SERVER inbound

config模块的默认行为是在接口下追加命令。如果接口上已存在该命令,config模块不会移除旧的再添加,而是直接追加,导致重复。

7.3 解决方案

# 方案1:使用replace模式(推荐)
- name: 应用ACL策略(替换模式)
  ansible.netcommon.config:
    lines:
      - traffic-policy ACL_SERVER inbound
    parents: interface GigabitEthernet0/0/1
    replace: line   # 替换匹配行,而非追加

# 方案2:使用before/after先清除旧配置
- name: 应用ACL策略(先清除再添加)
  ansible.netcommon.config:
    lines:
      - traffic-policy ACL_SERVER inbound
    parents: interface GigabitEthernet0/0/1
    before:
      - undo traffic-policy inbound

# 方案3:使用diff模式预览变更
- name: 预览ACL配置变更
  ansible.netcommon.config:
    lines:
      - traffic-policy ACL_SERVER inbound
    parents: interface GigabitEthernet0/0/1
    diff_against: intended   # 与期望配置对比
    diff_ignore_lines:       # 忽略注释行
      - '^!'

7.4 预防措施

# 在ansible.cfg中设置全局diff
[defaults]
diff = True

# 或在Playbook中启用
---
- name: 部署配置(启用diff预览)
  hosts: all
  gather_facts: no
  diff: true   # 启用diff输出

八、多线程设备管理与并发控制

8.1 多线程采集示例

import concurrent.futures
import paramiko
import threading
from queue import Queue

# 设备清单
devices = [
    {"host": "192.168.1.1", "user": "admin", "password": "pass1", "role": "core"},
    {"host": "192.168.1.2", "user": "admin", "password": "pass2", "role": "core"},
    {"host": "192.168.1.10", "user": "admin", "password": "pass3", "role": "agg"},
    {"host": "192.168.1.11", "user": "admin", "password": "pass4", "role": "agg"},
]

def collect_device_info(device):
    """从单台设备采集信息"""
    try:
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(device["host"], username=device["user"],
                   password=device["password"], timeout=10)

        # 采集版本信息
        stdin, stdout, stderr = ssh.exec_command("display version")
        version = stdout.read().decode()

        # 采集接口状态
        stdin, stdout, stderr = ssh.exec_command("display interface brief")
        interfaces = stdout.read().decode()

        # 采集CPU/内存
        stdin, stdout, stderr = ssh.exec_command("display device")
        resources = stdout.read().decode()

        ssh.close()

        return {
            "host": device["host"],
            "role": device["role"],
            "version": version.strip(),
            "interfaces": interfaces.strip(),
            "resources": resources.strip(),
            "status": "success"
        }
    except Exception as e:
        return {
            "host": device["host"],
            "status": "failed",
            "error": str(e)
        }

# 使用ThreadPoolExecutor并发采集
def concurrent_collection(device_list, max_workers=10):
    """多线程并发采集"""
    results = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_device = {
            executor.submit(collect_device_info, dev): dev
            for dev in device_list
        }
        for future in concurrent.futures.as_completed(future_to_device):
            result = future.result()
            results.append(result)
            status = "✓" if result["status"] == "success" else "✗"
            print(f"{status} {result['host']} ({result.get('role', 'unknown')})")
    return results

# 执行并发采集
results = concurrent_collection(devices, max_workers=5)

8.2 asyncio 异步采集

import asyncio
import asyncssh
import json

async def async_collect_device(device):
    """异步采集单台设备信息"""
    try:
        async with asyncssh.connect(
            device["host"],
            username=device["user"],
            password=device["password"],
            known_hosts=None,
            timeout=10
        ) as conn:
            # 并发执行多条命令
            results = await asyncio.gather(
                conn.run("display version"),
                conn.run("display interface brief"),
                conn.run("display device"),
                return_exceptions=True
            )

            return {
                "host": device["host"],
                "role": device["role"],
                "version": results[0].stdout.strip() if not isinstance(results[0], Exception) else "error",
                "interfaces": results[1].stdout.strip() if not isinstance(results[1], Exception) else "error",
                "resources": results[2].stdout.strip() if not isinstance(results[2], Exception) else "error",
                "status": "success"
            }
    except Exception as e:
        return {
            "host": device["host"],
            "status": "failed",
            "error": str(e)
        }

async def batch_async_collection(device_list):
    """批量异步采集"""
    tasks = [async_collect_device(dev) for dev in device_list]
    results = await asyncio.gather(*tasks, return_exceptions=True)

    valid_results = []
    for result in results:
        if isinstance(result, dict):
            valid_results.append(result)
        else:
            print(f"Unexpected error: {result}")

    return valid_results

# 执行
# results = asyncio.run(batch_async_collection(devices))

8.3 并发控制策略

控制方式 实现 适用场景 优缺点
ThreadPoolExecutor max_workers参数 设备数量<100 简单、但线程开销大
asyncio.gather 并发任务列表 设备数量100-1000 高效、低资源消耗
信号量(Semaphore) asyncio.Semaphore 需要限流 精确控制并发数
批处理 分批处理 设备数量>1000 避免内存溢出
速率限制 time.sleep/token bucket 设备SSH连接数有限 防止设备过载
import asyncio

async def rate_limited_collect(device, semaphore):
    """使用信号量限制并发的采集"""
    async with semaphore:  # 最多同时连接10台设备
        result = await async_collect_device(device)
        await asyncio.sleep(0.1)  # 速率限制:每次操作间隔100ms
        return result

async def controlled_collection(device_list, max_concurrent=10):
    """带并发控制的批量采集"""
    semaphore = asyncio.Semaphore(max_concurrent)
    tasks = [rate_limited_collect(dev, semaphore) for dev in device_list]
    return await asyncio.gather(*tasks, return_exceptions=True)

# 使用:最多同时连接10台设备
# results = asyncio.run(controlled_collection(large_device_list, max_concurrent=10))

8.4 Ansible并发参数调优

# ansible.cfg 并发优化
[defaults]
# 并行执行数(同时操作的设备数)
forks = 20

# 超时时间
timeout = 30

# 线程数(内部使用)
internal_poll_interval = 0.001

[ssh_connection]
# SSH控制持久化(减少连接建立开销)
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
# 扫描端口时间
control_path_dir = /tmp/.ansible/cp

九、总结

知识点 要点
架构 无代理、SSH连接、声明式Playbook、幂等执行
Inventory 分层分组、变量继承、Vault加密
command模块 执行只读命令、采集信息
config模块 下发配置、支持追加/替换模式
template模块 Jinja2模板渲染后下发配置
Roles 任务结构化封装、可复用
高级特性 when条件、loop循环、rescue回滚、async异步
避坑 注意config模块的追加行为,使用replace模式确保幂等
多线程采集 ThreadPoolExecutor并发采集设备信息
异步采集 asyncio+asyncssh实现高效异步采集
并发控制 信号量限流、批处理、速率限制

思考题

  1. Ansible的"声明式"与"过程式"自动化有何区别?请各举一个网络配置场景说明。
  2. config模块的lines参数与src参数有何不同?什么时候应该使用src而非lines?
  3. 设计一个Ansible Role来管理完整的BGP部署:支持peer配置、地址族启用、路由策略应用、GR配置,且每个子任务可按需启用/跳过。

下篇预告

第309篇:Ansible Tower/AWX——自动化调度。从命令行走向Web界面,实现Playbook的可视化调度、权限管理和执行审计。