第304篇:Netmiko 批量执行配置命令
关键词:Netmiko、批量配置、多厂商兼容、配置模板、批量变更、幂等性
一、引言
第303篇中,我们使用Paramiko直接管理SSH连接和Shell交互。然而,当需要对接华为、思科、Juniper等多品牌设备时,每个厂商的CLI提示符、配置模式、命令语法各不相同,手工编写适配代码的工作量巨大。
Netmiko在Paramiko之上构建了一层设备抽象层,内置了100+厂商的平台驱动(Driver),自动识别提示符、自动进入配置模式、自动处理分页输出。这使得网络工程师可以用同一套代码管理异构设备,将批量配置的效率提升到一个新的高度。
二、Netmiko 安装与设备类型识别
2.1 安装
pip install netmiko
2.2 设备平台类型速查
Netmiko通过device_type参数选择对应的驱动。常用厂商映射:
| 厂商 | device_type | 配置模式进入命令 | 提示符特征 |
|---|---|---|---|
| 华为VRP | huawei |
system-view |
<hostname> → [hostname] |
| 华为VRP5 | huawei_5g |
system-view |
特殊提示符 |
| 华三H3C | hp_comware |
system-view |
<hostname> → [hostname] |
| 思科IOS | cisco_ios |
configure terminal |
> → # |
| 思科IOS-XE | cisco_xe |
configure terminal |
> → # |
| 思科NX-OS | cisco_nxos |
configure terminal |
> → # |
| Juniper JunOS | juniper_junos |
configure |
> → # |
| Nokia SR OS | nokia_sros |
configure |
A> → A# |
| Arista EOS | arista_eos |
configure terminal |
> → # |
from netmiko import ConnectHandler
# 核心连接参数结构
device_params = {
"device_type": "huawei", # 必须指定
"host": "192.168.1.1",
"username": "admin",
"password": "H@wa123",
"port": 22, # 默认22
"secret": "H@wa123", # enable密码(如需要)
"timeout": 30, # 连接超时(秒)
"session_timeout": 30, # 单次操作超时(秒)
"auth_timeout": 10, # 认证超时(秒)
"banner_timeout": 15, # Banner超时(秒)
}
# 建立连接
net_connect = ConnectHandler(**device_params)
# 执行单条显示命令
output = net_connect.send_command("display version")
print(output)
# 关闭连接
net_connect.disconnect()
三、发送命令的三种方式
3.1 send_command:执行单条命令(只读)
from netmiko import ConnectHandler
net_connect = ConnectHandler(**device_params)
# 基本用法
output = net_connect.send_command("display ip interface brief")
# 使用expect_string等待特定字符串出现
output = net_connect.send_command(
"ping 10.0.0.1",
expect_string=r"#.*$", # 等待回到命令行提示符
delay_factor=2 # 延迟倍率(慢设备增大此值)
)
# 长输出:使用read_timeout延长读取时间
output = net_connect.send_command(
"display ip routing-table",
read_timeout=120 # 等待120秒读取完整输出
)
net_connect.disconnect()
3.2 send_config_set:下发配置命令
from netmiko import ConnectHandler
net_connect = ConnectHandler(**device_params)
# 方式1:传入命令列表
config_commands = [
"vlan 100",
"description SERVER_VLAN",
"quit",
"interface GigabitEthernet0/0/10",
"port link-type access",
"port default vlan 100",
]
result = net_connect.send_config_set(config_commands)
print(result)
# 方式2:传入多行字符串
config_text = """
vlan 200
description BACKUP_VLAN
quit
interface GigabitEthernet0/0/11
port link-type access
port default vlan 200
"""
result = net_connect.send_config_set(config_text.strip().splitlines())
print(result)
net_connect.disconnect()
3.3 save_config:保存配置
# 下发配置后立即保存
net_connect.send_config_set(config_commands)
net_connect.save_config() # 自动执行厂商对应的保存命令
# 华为: save force
# 思科: copy running-config startup-config
# Juniper: commit
四、批量设备配置实战
4.1 标准批量配置流程
from netmiko import ConnectHandler
from concurrent.futures import ThreadPoolExecutor, as_completed
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
# 设备清单
devices = [
{"host": "192.168.1.1", "username": "admin", "password": "H@wa123"},
{"host": "192.168.1.2", "username": "admin", "password": "H@wa123"},
{"host": "192.168.1.3", "username": "admin", "password": "H@wa123"},
{"host": "192.168.1.4", "username": "admin", "password": "H@wa123"},
{"host": "192.168.1.5", "username": "admin", "password": "H@wa123"},
]
# 待下发配置
new_vlans = [
"vlan 300",
"description HR_VLAN",
"quit",
"vlan 301",
"description FINANCE_VLAN",
"quit",
]
def deploy_config(device):
"""在单台设备上部署配置"""
device_params = {
"device_type": "huawei",
**device
}
try:
logging.info(f"连接设备: {device['host']}")
net_connect = ConnectHandler(**device_params)
# 步骤1:执行前验证——检查VLAN是否已存在
existing = net_connect.send_command("display vlan 300")
if "VlanId300" in existing:
logging.info(f" VLAN 300已存在,跳过配置")
net_connect.disconnect()
return {"host": device["host"], "status": "skipped", "reason": "vlan_exists"}
# 步骤2:下发配置
logging.info(f" 下发配置到: {device['host']}")
result = net_connect.send_config_set(new_vlans)
# 步骤3:保存配置
net_connect.save_config()
logging.info(f" 配置已保存: {device['host']}")
# 步骤4:验证配置
verify = net_connect.send_command("display vlan 300")
if "VlanId300" in verify:
logging.info(f" 验证通过: {device['host']}")
return {"host": device["host"], "status": "success"}
else:
logging.warning(f" 验证失败: {device['host']}")
return {"host": device["host"], "status": "failed", "reason": "verify_failed"}
except Exception as e:
logging.error(f" 异常: {device['host']} - {e}")
return {"host": device["host"], "status": "error", "reason": str(e)}
finally:
try:
net_connect.disconnect()
except:
pass
# 使用线程池并发执行(最多3台并行)
results = []
with ThreadPoolExecutor(max_workers=3) as executor:
futures = {executor.submit(deploy_config, dev): dev for dev in devices}
for future in as_completed(futures):
result = future.result()
results.append(result)
# 汇总报告
print("\n" + "=" * 50)
print("批量部署报告")
print("=" * 50)
success = sum(1 for r in results if r["status"] == "success")
failed = sum(1 for r in results if r["status"] == "failed")
errors = sum(1 for r in results if r["status"] == "error")
skipped = sum(1 for r in results if r["status"] == "skipped")
print(f"成功: {success}")
print(f"跳过: {skipped}")
print(f"失败: {failed}")
print(f"异常: {errors}")
4.2 从CSV读取设备清单
import csv
def load_devices_from_csv(csv_path):
"""从CSV文件加载设备清单"""
devices = []
with open(csv_path, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
devices.append({
"host": row["ip"],
"username": row.get("username", "admin"),
"password": row.get("password", "default"),
"device_type": row.get("device_type", "huawei"),
})
return devices
# devices.csv 格式:
# ip,username,password,device_type
# 192.168.1.1,admin,H@wa123,huawei
# 192.168.1.2,admin,C1sc0!,cisco_ios
# 192.168.1.3,admin,Jun1per!,juniper_junos
devices = load_devices_from_csv("devices.csv")
五、配置回滚与幂等性
5.1 变更前备份 + 自动回滚
from netmiko import ConnectHandler
from datetime import datetime
import os
class SafeConfigDeployer:
"""安全配置部署器:支持备份与回滚"""
def __init__(self, device_params):
self.device_params = device_params
self.net_connect = None
self.backup_file = None
def connect(self):
self.net_connect = ConnectHandler(**self.device_params)
def backup_config(self, backup_dir="./backups"):
"""备份当前运行配置"""
os.makedirs(backup_dir, exist_ok=True)
hostname = self.net_connect.find_prompt().strip()
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
self.backup_file = f"{backup_dir}/{hostname}_{ts}_rollback.cfg"
config = self.net_connect.send_command("display current-configuration")
with open(self.backup_file, "w", encoding="utf-8") as f:
f.write(f"# 备份时间: {ts}\n")
f.write(f"# 设备: {hostname}\n")
f.write(config)
logging.info(f"配置已备份: {self.backup_file}")
return self.backup_file
def deploy_and_verify(self, config_commands, verify_commands):
"""部署配置并验证"""
# 下发配置
self.net_connect.send_config_set(config_commands)
self.net_connect.save_config()
# 验证
for cmd, expected in verify_commands:
output = self.net_connect.send_command(cmd)
if expected not in output:
raise VerificationError(
f"验证失败: '{cmd}' 输出中未找到 '{expected}'"
)
def rollback(self):
"""回滚到备份配置"""
if not self.backup_file:
raise RuntimeError("无备份文件,无法回滚")
logging.warning("正在回滚配置...")
# 读取备份文件中的配置
with open(self.backup_file, "r", encoding="utf-8") as f:
backup_config = f.read()
# 通过Shell交互模式回滚(华为VRP)
shell = self.net_connect.config_mode()
# 此处简化处理,实际需解析配置并反向操作
self.net_connect.exit_config_mode()
logging.info("回滚完成")
def disconnect(self):
if self.net_connect:
self.net_connect.disconnect()
# 使用示例
deployer = SafeConfigDeployer({
"device_type": "huawei",
"host": "192.168.1.1",
"username": "admin",
"password": "H@wa123"
})
try:
deployer.connect()
deployer.backup_config()
deployer.deploy_and_verify(
config_commands=["vlan 400", "description TEST", "quit"],
verify_commands=[
("display vlan 400", "VlanId400"),
("display current-configuration | include TEST", "TEST"),
]
)
logging.info("部署成功")
except Exception as e:
logging.error(f"部署失败: {e},执行回滚")
deployer.rollback()
finally:
deployer.disconnect()
5.2 幂等性检查器
class IdempotentChecker:
"""配置幂等性检查器"""
def __init__(self, net_connect):
self.nc = net_connect
def check_vlan_exists(self, vlan_id):
"""检查VLAN是否已存在"""
output = self.nc.send_command(f"display vlan {vlan_id}")
return f"VlanId{vlan_id}" in output
def check_interface_description(self, interface, description):
"""检查接口描述是否为期望值"""
output = self.nc.send_command(f"display current-configuration interface {interface}")
return description in output
def check_bgp_neighbor(self, neighbor_ip):
"""检查BGP邻居是否已配置"""
output = self.nc.send_command("display bgp peer")
return neighbor_ip in output
# 使用示例:只在需要时才下发配置
checker = IdempotentChecker(net_connect)
if not checker.check_vlan_exists(400):
net_connect.send_config_set(["vlan 400", "description TEST_VLAN", "quit"])
logging.info("VLAN 400已创建")
else:
logging.info("VLAN 400已存在,跳过")
六、故障案例:Netmiko delay_factor 引发的超时
6.1 故障现象
批量配置脚本在连接老旧的CE6850交换机时频繁超时:
netmiko.exceptions.NetmikoTimeoutException:
Timed-out reading channel, data not available.
6.2 排查过程
步骤1:确认设备本身正常
# 手动SSH登录,命令执行正常,无延迟
ssh admin@10.10.10.1
<CE6850> display version # 立即返回
步骤2:降低线程并发数
将max_workers从10降至2,超时仍然发生。
步骤3:分析Netmiko日志 开启调试日志:
import logging
logging.getLogger("paramiko").setLevel(logging.DEBUG)
logging.getLogger("netmiko").setLevel(logging.DEBUG)
日志显示:命令已发送,但Netmiko未能识别到命令返回后的提示符。
6.3 根因定位
Netmiko通过正则表达式匹配设备提示符来判断命令是否执行完毕。老旧VRP版本的提示符格式为:
<CE6850>
但设备在CPU负载高时,提示符前会出现多余空格或延迟,导致匹配失败。
6.4 解决方案
from netmiko import ConnectHandler
net_connect = ConnectHandler(
device_type="huawei",
host="192.168.1.1",
username="admin",
password="H@wa123",
# 关键参数调整:
delay_factor=2, # 增大延迟倍率(默认1)
global_delay_factor=2, # 全局延迟倍率
fast_cli=False, # 禁用快速CLI(老旧设备可能不兼容)
session_timeout=60, # 增加会话超时
)
# 对特定慢命令使用额外的delay_factor
output = net_connect.send_command(
"display ip routing-table",
delay_factor=3,
read_timeout=120
)
七、Netmiko 高级技巧
7.1 自定义设备驱动
当内置驱动不匹配时,可基于现有驱动扩展:
from netmiko.cisco.cisco_ios import CiscoIosBase
class CustomHuaweiDriver(CiscoIosBase):
"""基于CiscoIOS驱动的自定义华为驱动"""
def session_preparation(self):
self.ansi_escape_codes = True
self._test_channel_read(pattern=r"[<\[]\w+[>\]]")
self.set_base_prompt()
def config_mode(self, config_command="system-view"):
return super().config_mode(config_command=config_command)
def exit_config_mode(self):
return super().exit_config_mode()
def save_config(self, cmd="save force"):
return super().save_config(cmd=cmd)
# 注册自定义驱动
from netmiko import Dispatch
Dispatch["custom_huawei"] = CustomHuaweiDriver
# 使用
net_connect = ConnectHandler(
device_type="custom_huawei",
host="192.168.1.1",
username="admin",
password="H@wa123"
)
7.2 配置差异检查
from netmiko import ConnectHandler
net_connect = ConnectHandler(**device_params)
# 获取运行配置
running = net_connect.send_command("display current-configuration")
# 获取启动配置
startup = net_connect.send_command("display saved-configuration")
# 比对
running_lines = set(running.splitlines())
startup_lines = set(startup.splitlines())
new_in_running = running_lines - startup_lines
removed_from_startup = startup_lines - running_lines
if new_in_running:
print(f"运行配置中有 {len(new_in_running)} 行未保存的变更")
for line in list(new_in_running)[:10]:
print(f" + {line}")
if not new_in_running and not removed_from_startup:
print("运行配置与启动配置一致")
net_connect.disconnect()
八、总结
| 知识点 | 要点 |
|---|---|
| ConnectHandler | 统一的多厂商设备连接接口 |
| device_type | 根据厂商选择驱动,内置100+平台 |
| send_command | 执行只读命令,支持expect_string和read_timeout |
| send_config_set | 下发配置命令,自动进入/退出配置模式 |
| save_config | 自动执行厂商对应保存命令 |
| delay_factor | 老旧设备需增大此值避免超时 |
| 批量部署 | 线程池并发 + 幂等性检查 + 变更前后验证 |
| 安全操作 | 变更前备份、验证失败自动回滚 |
思考题
- Netmiko如何处理不同厂商的配置模式差异?如果目标设备是Nokia SR OS,
send_config_set下发的第一条命令和思科IOS有何不同? - 为什么批量配置时需要"幂等性检查"?如果一个脚本每天定时执行一次,幂等性缺失会导致什么后果?
- 设计一个批量BGP邻居配置方案:要求支持灰度发布(先10%设备、再50%、最后100%),每批次完成后自动检查BGP状态,异常则中止后续批次。
下篇预告
第305篇:NAPALM——统一网络设备管理接口。屏蔽厂商差异,用同一套API管理华为、思科、Juniper设备的配置、状态和路由信息。