第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实现高效异步采集 |
| 并发控制 | 信号量限流、批处理、速率限制 |
思考题
- Ansible的"声明式"与"过程式"自动化有何区别?请各举一个网络配置场景说明。
config模块的lines参数与src参数有何不同?什么时候应该使用src而非lines?- 设计一个Ansible Role来管理完整的BGP部署:支持peer配置、地址族启用、路由策略应用、GR配置,且每个子任务可按需启用/跳过。
下篇预告
第309篇:Ansible Tower/AWX——自动化调度。从命令行走向Web界面,实现Playbook的可视化调度、权限管理和执行审计。