第308篇:Ansible Network Modules 实战

关键词

Ansible、Network Modules、Playbook、网络自动化、批量配置、多厂商支持、事实收集、配置编排


一、Ansible 网络自动化概述

1.1 Ansible 在网络领域的位置

Ansible 的架构优势:

Control Node(控制节点) ┌────────────────────────────────────┐ └────────────────────────────────────┘ ▼ ▼ ▼ ┌────────────────────────────────────┐ └────────────────────────────────────┘ ▼ ▼ ▼ ┌──────┐ ┌──────┐ ┌──────┐ Ansible Engine ┌──────┐ ┌──────┐ ┌──────────┐ └──────┘ └──────┘ └──────────┘ Network Collections ┌─────────┐ ┌─────────┐ ┌──────┐ └─────────┘ └─────────┘ └──────┘ 华为 设备 变量 华为 VRP 思科 设备 Play 思科 IOS Juniper 设备 角色(Role) JunOS

Ansible 网络自动化的特点: ┌─ 无需 Agent:通过 SSH/NETCONF 管理 ├─ 幂等性:重复执行结果一致 ├─ 声明式:描述"目标状态" ├─ 批量:一个命令管理 1000+ 设备 └─ 多厂商:统一接口调用不同厂商模块

1.2 环境准备

# 安装 Ansible
python -m pip install ansible

# 验证
ansible --version

# 安装网络集合
ansible-galaxy collection install community.network
ansible-galaxy collection install huawei.network
ansible-galaxy collection install cisco.ios
ansible-galaxy collection install cisco.nxos
ansible-galaxy collection install junipernetworks.junos

# 查看已安装的集合
ansible-galaxy collection list

1.3 项目结构

ansible_network/
├── ansible.cfg          # Ansible 配置
├── inventory.yml        # 设备清单
├── group_vars/          # 组变量
│   ├── all.yml          # 所有设备通用变量
│   ├── core.yml         # 核心设备变量
│   └── access.yml       # 接入设备变量
├── host_vars/           # 主机变量
│   ├── CORE-SW01.yml
│   └── ACC-SW01.yml
├── playbooks/           # Playbook
│   ├── collect_facts.yml
│   ├── deploy_vlan.yml
│   ├── backup_config.yml
│   └── verify.yml
├── templates/           # Jinja2 模板
│   └── switch_config.j2
└── roles/               # 角色(可选)
    ├── ospf_config/
    └── bgp_config/

二、Inventory 与连接

2.1 定义设备清单

# inventory.yml — 网络设备清单

# 分组管理
all:
  children:
    core:
      hosts:
        CORE-SW01:
          ansible_host: 10.0.0.1
        CORE-RT01:
          ansible_host: 10.0.0.2
      vars:
        role: core
        snmp_location: "DC-A"

    access:
      hosts:
        ACC-SW01:
          ansible_host: 10.0.1.1
        ACC-SW02:
          ansible_host: 10.0.1.2
        ACC-SW03:
          ansible_host: 10.0.1.3
      vars:
        role: access
        snmp_location: "B1-Floor"

  vars:
    # 全局连接参数
    ansible_connection: ansible.netcommon.network_cli
    ansible_network_os: huawei.vrp.vrp
    ansible_user: admin
    ansible_password: "{{ vault_admin_password }}"
    ansible_ssh_common_args: "-o StrictHostKeyChecking=no"

2.2 ansible.cfg 配置

# ansible.cfg — Ansible 配置
[defaults]
inventory = inventory.yml
host_key_checking = False
gathering = explicit
timeout = 30
command_warnings = False
interpreter_python = auto_silent

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

2.3 连接测试

# 测试所有设备的连通性
ansible all -m huawei.vrp.vrp_ping -a "dest=127.0.0.1"

# 收集设备版本(单条命令)
ansible core -m huawei.vrp.vrp_command \
  -a "commands='display version'"

# 查看收集到的 Facts
ansible CORE-SW01 -m huawei.vrp.vrp_facts

三、核心模块实战

3.1 命令执行模块

# playbooks/collect_facts.yml
---
- name: 收集网络设备信息
  hosts: all
  gather_facts: no

  tasks:
    - name: 执行 show 命令
      huawei.vrp.vrp_command:
        commands:
          - display version
          - display device
          - display interface brief
      register: result

    - name: 显示结果摘要
      debug:
        msg:
          - "设备: {{ inventory_hostname }}"
          - "版本信息: {{ result.stdout_lines[0][:3] }}"
          - "接口数: {{ result.stdout_lines[2] | length }}"

3.2 配置管理模块

# playbooks/deploy_vlan.yml
---
- name: 批量创建 VLAN
  hosts: access
  gather_facts: no

  vars:
    vlans:
      - {id: 100, name: "VLAN100_SALES"}
      - {id: 200, name: "VLAN200_ENG"}
      - {id: 300, name: "VLAN300_FINANCE"}

  tasks:
    - name: 创建 VLAN
      huawei.vrp.vrp_vlan:
        vlan_id: "{{ item.id }}"
        name: "{{ item.name }}"
        state: present
      loop: "{{ vlans }}"
      register: vlan_result

    - name: 显示 VLAN 创建结果
      debug:
        msg: "VLAN {{ item.item.id }} ({{ item.item.name }}): {{ '✓' if item.changed else '─' }}"
      loop: "{{ vlan_result.results }}"

    - name: 保存配置
      huawei.vrp.vrp_config:
        save: true

3.3 接口配置模块

# playbooks/deploy_interfaces.yml
---
- name: 配置交换机接口
  hosts: ACC-SW01
  gather_facts: no

  tasks:
    - name: 配置 Trunk 上行口
      huawei.vrp.vrp_interface:
        name: GigabitEthernet0/0/1
        description: "UPLINK-TO-CORE"
        mode: trunk
        trunk_allowed_vlans: "10,20,30,100"
        state: present

    - name: 配置 Access 接入口
      huawei.vrp.vrp_interface:
        name: "{{ item.name }}"
        description: "{{ item.desc }}"
        mode: access
        access_vlan: "{{ item.vlan }}"
        state: present
      loop:
        - {name: GigabitEthernet0/0/2, desc: "PC-FLOOR1", vlan: 10}
        - {name: GigabitEthernet0/0/3, desc: "PC-FLOOR2", vlan: 20}
        - {name: GigabitEthernet0/0/4, desc: "SERVER-ROOM", vlan: 100}

    - name: 保存配置
      huawei.vrp.vrp_config:
        save: true

3.4 配置备份

# playbooks/backup_config.yml
---
- name: 全网配置备份
  hosts: all
  gather_facts: no

  vars:
    backup_dir: "/backup/{{ ansible_date_time.date }}/"

  tasks:
    - name: 创建备份目录
      delegate_to: localhost
      file:
        path: "{{ backup_dir }}"
        state: directory

    - name: 备份运行配置
      huawei.vrp.vrp_command:
        commands: display current-configuration
      register: config

    - name: 写入备份文件
      delegate_to: localhost
      copy:
        content: "{{ config.stdout[0] }}"
        dest: "{{ backup_dir }}/{{ inventory_hostname }}_running.cfg"

    - name: 备份启动配置
      huawei.vrp.vrp_command:
        commands: display saved-configuration
      register: saved_config

    - name: 写入备份文件
      delegate_to: localhost
      copy:
        content: "{{ saved_config.stdout[0] }}"
        dest: "{{ backup_dir }}/{{ inventory_hostname }}_startup.cfg"

    - name: 显示结果
      debug:
        msg: "✓ {{ inventory_hostname }} 配置已备份到 {{ backup_dir }}"

四、多厂商 Playbook

4.1 多厂商混合管理

# inventory_multivendor.yml
all:
  children:
    huawei:
      hosts:
        CORE-SW01:
          ansible_host: 10.0.0.1
      vars:
        ansible_network_os: huawei.vrp.vrp
        ansible_connection: ansible.netcommon.network_cli

    cisco:
      hosts:
        CORE-RT01:
          ansible_host: 10.0.0.2
      vars:
        ansible_network_os: cisco.ios.ios
        ansible_connection: ansible.netcommon.network_cli

    juniper:
      hosts:
        CORE-RT02:
          ansible_host: 10.0.0.3
      vars:
        ansible_network_os: junipernetworks.junos.junos
        ansible_connection: ansible.netcommon.netconf
# playbooks/multivendor_facts.yml
---
- name: 多厂商设备信息收集
  hosts: all
  gather_facts: no

  tasks:
    - name: 收集华为设备 Facts
      huawei.vrp.vrp_facts:
      when: ansible_network_os == 'huawei.vrp.vrp'
      register: huawei_facts

    - name: 收集思科设备 Facts
      cisco.ios.ios_facts:
      when: ansible_network_os == 'cisco.ios.ios'
      register: cisco_facts

    - name: 收集 Juniper Facts
      junipernetworks.junos.junos_facts:
      when: ansible_network_os == 'junipernetworks.junos.junos'
      register: juniper_facts

    - name: 显示统一结果
      debug:
        msg:
          - "设备: {{ inventory_hostname }}"
          - "厂商: {{ ansible_net_vendor | default('unknown') }}"
          - "型号: {{ ansible_net_model | default('unknown') }}"
          - "版本: {{ ansible_net_version | default('unknown') }}"

4.2 厂商无关的通用操作

# playbooks/generic_ping.yml
---
- name: 通用网络验证
  hosts: all
  gather_facts: no

  tasks:
    - name: 检查网关连通性
      ansible.netcommon.net_ping:
        dest: "{{ gateway_ip }}"
      vars:
        gateway_ip: "10.0.0.254"
      register: ping_result

    - name: 显示 Ping 结果
      debug:
        msg: "{{ inventory_hostname }} → {{ gateway_ip }}: {{ ping_result.stdout }}"

五、高级应用

5.1 模板+Playbook 组合

# playbooks/deploy_from_template.yml
---
- name: 基于模板部署设备配置
  hosts: all
  gather_facts: no

  vars:
    ntp_server: "203.0.113.1"

  tasks:
    - name: 生成配置(本地渲染)
      delegate_to: localhost
      template:
        src: "templates/device_base.j2"
        dest: "generated/{{ inventory_hostname }}_config.cfg"
      run_once: no

    - name: 将生成的配置加载到设备
      huawei.vrp.vrp_config:
        src: "generated/{{ inventory_hostname }}_config.cfg"
        match: none   # 不匹配现有配置
        save: true
# templates/device_base.j2 — Jinja2 模板
! 设备: {{ inventory_hostname }}
! 角色: {{ role | default("unknown") }}

{% if role == "core" %}
sysname {{ inventory_hostname }}
#
interface LoopBack0
 ip address {{ loopback_ip }} 255.255.255.255
#
ospf 1 router-id {{ loopback_ip }}
 area 0.0.0.0
  network {{ loopback_ip }} 0.0.0.0
#
{% elif role == "access" %}
sysname {{ inventory_hostname }}
#
vlan batch {{ vlans | default([1]) | join(" ") }}
#
interface GigabitEthernet0/0/1
 port link-type trunk
 port trunk allow-pass vlan all
#
{% endif %}
ntp-service unicast-server {{ ntp_server }}
#
return

5.2 滚动变更

# playbooks/rolling_update.yml
---
- name: 滚动配置变更(一次一台,避免影响业务)
  hosts: access
  serial: 1                # 一次只变更一台
  gather_facts: no

  vars:
    new_snmp_config: |
      snmp-agent sys-info contact IT-Department
      snmp-agent sys-info location "Beijing-Office-B1"

  tasks:
    - name: 开始变更
      debug:
        msg: "===== 正在变更 {{ inventory_hostname }} ====="

    - name: 应用 SNMP 配置
      huawei.vrp.vrp_config:
        lines:
          - snmp-agent sys-info contact IT-Department
          - snmp-agent sys-info location "Beijing-Office-B1"

    - name: 验证配置
      huawei.vrp.vrp_command:
        commands: display snmp-agent sys-info
      register: verify

    - name: 检查结果
      assert:
        that:
          - "'IT-Department' in verify.stdout[0]"
          - "'Beijing' in verify.stdout[0]"
        success_msg: "✓ {{ inventory_hostname }} SNMP 配置验证通过"
        fail_msg: "✗ {{ inventory_hostname }} SNMP 配置验证失败"

    - name: 保存
      huawei.vrp.vrp_config:
        save: true

    - name: 变更完成
      debug:
        msg: "===== {{ inventory_hostname }} 变更完成 ====="

5.3 配置合规检查

# playbooks/compliance_check.yml
---
- name: 配置合规检查
  hosts: all
  gather_facts: no

  tasks:
    - name: 获取当前配置
      huawei.vrp.vrp_command:
        commands: display current-configuration
      register: current_config

    - name: 检查必须配置
      assert:
        that:
          - "'ntp-service unicast-server' in current_config.stdout[0]"
          - "'snmp-agent' in current_config.stdout[0]"
          - "'ssh server port 22' in current_config.stdout[0]"
        success_msg: "✓ {{ inventory_hostname }} 合规检查通过"
        fail_msg: "✗ {{ inventory_hostname }} 合规检查失败,缺少必要配置"

    - name: 检查安全基线
      block:
        - name: 检查 SSH 版本
          assert:
            that: "'ssh server' in current_config.stdout[0]"
            fail_msg: "SSH 未配置"

        - name: 检查密码策略
          huawei.vrp.vrp_command:
            commands: display aaa configuration
          register: aaa_config
          ignore_errors: yes

      rescue:
        - name: 合规异常告警
          debug:
            msg: "⚠ {{ inventory_hostname }} 存在安全合规风险"

六、Ansible 与 Python 集成

6.1 在 Python 中调用 Ansible

#!/usr/bin/env python3
# ansible_runner.py — Python 集成 Ansible

import ansible_runner
import json

def run_playbook(playbook_path, inventory_path, extra_vars=None):
    """运行 Ansible Playbook"""
    result = ansible_runner.run(
        playbook=playbook_path,
        inventory=inventory_path,
        extravars=extra_vars or {},
    )

    return {
        "status": result.status,
        "rc": result.rc,
        "stats": result.stats,
        "events": list(result.events),
    }

def run_adhoc(hosts, module, args, inventory_path="inventory.yml"):
    """运行 ad-hoc 命令"""
    result = ansible_runner.run(
        inventory=inventory_path,
        host_pattern=hosts,
        module=module,
        module_args=args,
    )
    return result

# 运行 Playbook
result = run_playbook(
    playbook_path="playbooks/backup_config.yml",
    inventory_path="inventory.yml",
)
print(f"状态: {result['status']}")
print(f"统计: {json.dumps(result['stats'], indent=2)}")

# 运行 ad-hoc
result = run_adhoc(
    hosts="all",
    module="huawei.vrp.vrp_command",
    args="commands='display clock'",
)
print(f"返回码: {result.rc}")

6.2 使用 Ansible 事实数据

#!/usr/bin/env python3
# ansible_facts_consumer.py — 消费 Ansible 收集的数据

import json

def analyze_network_facts(facts_file):
    """分析 Ansible 收集的设备事实"""
    with open(facts_file) as f:
        data = json.load(f)

    report = {
        "total_devices": len(data),
        "by_vendor": {},
        "by_model": {},
        "os_versions": {},
        "total_interfaces": 0,
        "interfaces_down": 0,
    }

    for host, facts in data.items():
        vendor = facts.get("ansible_net_vendor", "unknown")
        model = facts.get("ansible_net_model", "unknown")
        version = facts.get("ansible_net_version", "unknown")

        report["by_vendor"][vendor] = report["by_vendor"].get(vendor, 0) + 1
        report["by_model"][model] = report["by_model"].get(model, 0) + 1

        if version not in report["os_versions"]:
            report["os_versions"][version] = []
        report["os_versions"][version].append(host)

        interfaces = facts.get("ansible_net_interfaces", {})
        report["total_interfaces"] += len(interfaces)
        report["interfaces_down"] += sum(
            1 for v in interfaces.values() if not v.get("operstatus") == "up"
        )

    return report

# 模拟数据(实际从 Ansible 的 JSON 文件读取)
sample_data = {
    "CORE-SW01": {
        "ansible_net_vendor": "Huawei",
        "ansible_net_model": "CE12808",
        "ansible_net_version": "V800R022C00",
        "ansible_net_interfaces": {
            "GE0/0/1": {"operstatus": "up"},
            "GE0/0/2": {"operstatus": "up"},
            "GE0/0/3": {"operstatus": "down"},
        },
    }
}

report = analyze_network_facts.__wrapped__ if False else None
# 直接用 sample_data
report = {
    "total_devices": len(sample_data),
    "by_vendor": {},
    "by_model": {},
    "os_versions": {},
    "total_interfaces": 0,
    "interfaces_down": 0,
}

for host, facts in sample_data.items():
    vendor = facts.get("ansible_net_vendor", "unknown")
    model = facts.get("ansible_net_model", "unknown")
    version = facts.get("ansible_net_version", "unknown")
    report["by_vendor"][vendor] = report["by_vendor"].get(vendor, 0) + 1
    report["by_model"][model] = report["by_model"].get(model, 0) + 1

    if version not in report["os_versions"]:
        report["os_versions"][version] = []
    report["os_versions"][version].append(host)

    interfaces = facts.get("ansible_net_interfaces", {})
    report["total_interfaces"] += len(interfaces)
    report["interfaces_down"] += sum(
        1 for v in interfaces.values() if not v.get("operstatus") == "up"
    )

print(json.dumps(report, indent=2))

七、最佳实践

7.1 Playbook 规范

Ansible 网络 Playbook 编写规范:

  1. 命名规范
  ┌─ playbook 名:动作_目标.yml
  │  deploy_vlan.yml
  │  backup_config.yml
  │  verify_ospf.yml
  └─ 变量:snake_case

  2. 幂等性
  ┌─ 配置模块支持 state: present/absent
  ├─ 用 changed_when 控制变更检测
  ├─ 用 --check 模式预览变更
  └─ 先 check 后执行

  3. 安全
  ┌─ 使用 ansible-vault 加密密码
  ├─ 敏感变量不写在 Playbook 中
  ├─ 日志不输出密码
  └─ SSH 密钥认证优先

  4. 执行控制
  ┌─ serial: 控制并行度
  ├─ throttle: 限制并发数
  ├─ max_fail_percentage: 容忍失败率
  └─ any_errors_fatal: 关键任务严格模式

7.2 常见问题

常见问题与解决:

  Q1: SSH 连接超时
  解决: 增加 timeout, 启用 pipelining

  Q2: 配置命令报错
  解决: 使用 ignore_errors 配合 assert 验证

  Q3: 大批量设备执行慢
  解决: serial 控制并发, forks 调优

  Q4: 配置回滚
  解决: 先 backup, 再 commit, 失败时 restore

  Q5: 不同厂商命令差异
  解决: 使用 when 判断 ansible_network_os

八、总结

Ansible 网络自动化的核心价值:

  声明式配置管理
  ┌─ 描述"配置应该是什么样"
  ├─ 非"如何配置"的脚本
  └─ 幂等执行,安全可靠

  多厂商统一
  ┌─ 不同厂商的模块接口类似
  ├─ Playbook 结构一致
  └─ 厂商差异在模块层屏蔽

  从脚本到编排
  ┌─ ad-hoc:单条命令批量执行
  ├─ Playbook:多步骤编排
  ├─ Roles:复用配置逻辑
  └─ AWX/Tower:可视化平台

  学习路径:
  Playbook → Variables → Templates → Roles → AWX
  基础        数据      复用        抽象     平台

下篇预告:第309篇 — AWX/Tower 可视化管理平台,将介绍如何通过 AWX/Tower 将 Ansible 自动化提升到企业级平台层面。