第304篇:Netmiko 批量执行配置命令

关键词

Netmiko、批量执行、配置变更、多厂商支持、send_command、send_config_set、设备驱动、自动化脚本


一、Netmiko 简介

1.1 什么是 Netmiko

Netmiko 是对 Paramiko 的 高级封装库,专门为网络设备 SSH 操作优化:

Netmiko 的优势:

对比项 Paramiko Netmiko
封装层次 厂商支持 分页处理 配置模式 命令等待 错误检测 代码行数 底层 raw 手动处理 手动 send 手动切换 time.sleep 无 30+ 行 高级 API 90+ 厂商 自动处理 自动进入 智能等待 内置检测 3-5 行

安装:

python -m pip install netmiko

1.2 支持的网络设备

Netmiko 支持的主要厂商(90+ 驱动):

  华为:   huawei、huawei_vrpv8、huawei_smartax
  思科:   cisco_ios、cisco_xr、cisco_nxos、cisco_asa
  华三:   hp_comware、hp_procurve
  锐捷:   ruijie_os
  中兴:   zte_zxros
  Juniper:juniper_junos
  Arista: arista_eos
  Fortinet:fortinet

  查看完整列表:
  from netmiko import ssh_dispatcher
  print(list(ssh_dispatcher.platforms.keys()))

二、Netmiko 基础操作

2.1 连接与执行命令

#!/usr/bin/env python3
# netmiko_basic.py — Netmiko 基础连接

from netmiko import ConnectHandler

# 设备信息
device = {
    "device_type": "huawei",       # 驱动类型
    "host": "192.168.1.1",
    "username": "admin",
    "password": "admin123",
    "port": 22,                     # 默认 22
    "timeout": 30,                  # 超时秒数
    "verbose": False,               # 日志输出
}

# 建立连接
conn = ConnectHandler(**device)
print(f"已连接到 {device['host']}")

# 执行 show 命令
output = conn.send_command("display interface brief")
print(output)

# 执行多条命令
output = conn.send_commands([
    "display ip interface brief",
    "display bgp peer",
])
print(output)

# 关闭连接
conn.disconnect()

2.2 发送配置命令

#!/usr/bin/env python3
# netmiko_config.py — 配置模式操作

from netmiko import ConnectHandler
from netmiko.exceptions import ConfigException

device = {
    "device_type": "huawei",
    "host": "192.168.1.1",
    "username": "admin",
    "password": "admin123",
}

conn = ConnectHandler(**device)

# 进入系统视图并执行配置
config_commands = [
    "sysname CORP-CORE-01",
    "interface GigabitEthernet0/0/1",
    "description TRUNK_TO_SW01",
    "port link-type trunk",
    "port trunk allow-pass vlan 10 20 30",
    "quit",
    "interface GigabitEthernet0/0/2",
    "description LINK_TO_FW01",
    "port link-type access",
    "port default vlan 100",
    "quit",
]

# send_config_set 自动进入/退出配置模式
output = conn.send_config_set(config_commands)
print("配置输出:")
print(output)

# 验证配置
result = conn.send_command("display current-configuration interface GigabitEthernet0/0/1")
print(result)

conn.disconnect()

三、批量多设备操作

3.1 批量执行命令

#!/usr/bin/env python3
# batch_show.py — 批量收集设备信息

from netmiko import ConnectHandler
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

# 设备清单
DEVICES = [
    {"device_type": "huawei", "host": "192.168.1.1",
     "username": "admin", "password": "admin123"},
    {"device_type": "huawei", "host": "192.168.1.2",
     "username": "admin", "password": "admin123"},
    {"device_type": "huawei", "host": "192.168.1.3",
     "username": "admin", "password": "admin123"},
    {"device_type": "cisco_ios", "host": "192.168.2.1",
     "username": "admin", "password": "admin123"},
]

def collect_device(dev):
    """单设备收集"""
    try:
        conn = ConnectHandler(**dev)
        conn.enable()  # 思科需要 enable
        output = conn.send_command("display version" if "huawei" in dev["device_type"] else "show version")
        conn.disconnect()
        return (dev["host"], True, output)
    except Exception as e:
        return (dev["host"], False, str(e))

# 并行收集(提高效率)
print("开始批量收集...")
start = time.time()

with ThreadPoolExecutor(max_workers=5) as executor:
    futures = {executor.submit(collect_device, dev): dev for dev in DEVICES}
    for future in as_completed(futures):
        host, ok, data = future.result()
        status = "✓" if ok else "✗"
        print(f"[{status}] {host}")
        if ok:
            # 只打印前 5 行
            lines = data.strip().split("\n")
            for line in lines[:5]:
                print(f"  {line}")

elapsed = time.time() - start
print(f"\n总耗时:{elapsed:.1f} 秒")
print(f"4 台设备串行需 ~20 秒,并行仅 {elapsed:.1f} 秒")

3.2 批量下发配置

#!/usr/bin/env python3
# batch_config.py — 批量配置变更

from netmiko import ConnectHandler
from concurrent.futures import ThreadPoolExecutor, as_completed
import datetime

# 要添加的新 VLAN
NEW_VLANS = [
    {"id": 100, "name": "VLAN100_IT"},
    {"id": 200, "name": "VLAN200_HR"},
    {"id": 300, "name": "VLAN300_FINANCE"},
]

# 设备列表(核心交换机)
SWITCHES = [
    {"device_type": "huawei", "host": "192.168.100.1",
     "username": "admin", "password": "admin123"},
    {"device_type": "huawei", "host": "192.168.100.2",
     "username": "admin", "password": "admin123"},
]

def deploy_vlans(sw):
    """在交换机上创建 VLAN"""
    conn = ConnectHandler(**sw)
    hostname = conn.send_command("display current-configuration | include sysname")
    hostname = hostname.replace("sysname ", "").strip()

    commands = []
    for vlan in NEW_VLANS:
        commands.append(f"vlan {vlan['id']}")
        commands.append(f"name {vlan['name']}")
        commands.append("quit")

    print(f"[{hostname}] 即将创建 {len(NEW_VLANS)} 个 VLAN...")
    output = conn.send_config_set(commands)
    conn.save()  # 保存配置
    conn.disconnect()
    return (hostname, True, f"VLAN {[v['id'] for v in NEW_VLANS]} 创建成功")

# 执行
with ThreadPoolExecutor(max_workers=2) as ex:
    futures = [ex.submit(deploy_vlans, sw) for sw in SWITCHES]
    for f in as_completed(futures):
        name, ok, msg = f.result()
        print(f"[{'✓' if ok else '✗'}] {name}: {msg}")

四、高级功能

4.1 分页与超时处理

#!/usr/bin/env python3
# netmiko_advanced.py — 高级功能

from netmiko import ConnectHandler
from netmiko.exceptions import (
    NetmikoTimeoutException,
    NetmikoAuthenticationException,
    ReadTimeout,
)

device = {
    "device_type": "huawei",
    "host": "192.168.1.1",
    "username": "admin",
    "password": "admin123",
}

try:
    conn = ConnectHandler(**device)
except NetmikoAuthenticationException:
    print("认证失败,请检查用户名密码")
    exit(1)
except NetmikoTimeoutException:
    print("连接超时,请检查连通性")
    exit(1)

# 1. 处理超长输出
output = conn.send_command(
    "display current-configuration",
    read_timeout=60,           # 增加超时(秒)
    max_loops=500,             # 最大循环次数
)
print(f"配置共 {len(output.splitlines())} 行")

# 2. 使用正则匹配
output = conn.send_command(
    "display ip interface brief",
    use_textfsm=False,         # 是否启用 TextFSM 解析
)
print(output)

# 3. 命令终止字符串
output = conn.send_command(
    "display bgp routing-table",
    expect_string=r"<[\w\-]+>",  # 自定义提示符
    read_timeout=120,
)

# 4. 命令回退
output = conn.send_command(
    "display this",            # 当不确定是否在配置模式
    cmd_verify=False,          # 跳过命令回显验证
)

# 5. 全局延迟
conn.global_delay_factor = 2  # 所有命令延时 ×2(设备慢时使用)

conn.disconnect()

4.2 配置回滚

#!/usr/bin/env python3
# netmiko_rollback.py — 配置变更与回滚

from netmiko import ConnectHandler
import datetime

device = {
    "device_type": "huawei",
    "host": "192.168.1.1",
    "username": "admin",
    "password": "admin123",
}

def backup_config(conn):
    """备份当前配置"""
    backup = conn.send_command("display current-configuration")
    timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"backup_{device['host']}_{timestamp}.txt"
    with open(filename, "w", encoding="utf-8") as f:
        f.write(backup)
    print(f"配置已备份到 {filename}")
    return backup

def apply_config(conn, commands):
    """应用配置变更"""
    output = conn.send_config_set(commands)
    # 检查是否有错误
    if "%" in output or "Error" in output:
        print("配置出错,正在回滚...")
        return False
    conn.save()
    return True

def rollback_config(conn, backup):
    """按行回滚配置(简化版)"""
    # 注意:实际回滚需要 diff + reverse 逻辑
    conn.send_config_set(["rollback last-configuration"])
    conn.save()
    print("已回滚到上一个配置")

conn = ConnectHandler(**device)

# 1. 先备份
backup = backup_config(conn)

# 2. 应用变更
new_config = [
    "snmp-agent sys-info version v3",
    "snmp-agent group v3 readonlygroup privacy",
    "snmp-agent usm-user v3 readonlyuser readonlygroup",
]
success = apply_config(conn, new_config)

# 3. 验证
if success:
    result = conn.send_command("display snmp-agent sys-info version")
    print(f"SNMP 配置验证:{result}")
else:
    rollback_config(conn, backup)

conn.disconnect()

五、日志与异常处理

5.1 完整的生产级脚本

#!/usr/bin/env python3
# netmiko_prod.py — 生产级批量操作脚本

from netmiko import ConnectHandler
from netmiko.exceptions import (
    NetmikoTimeoutException,
    NetmikoAuthenticationException,
    ConfigException,
)
import logging
import time
from concurrent.futures import ThreadPoolExecutor, as_completed

# 日志配置
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.FileHandler("netmiko.log", encoding="utf-8"),
        logging.StreamHandler(),
    ]
)
log = logging.getLogger(__name__)

# 设备清单(可从文件/YAML 读取)
DEVICES = [
    {"host": "192.168.1.1", "device_type": "huawei"},
    {"host": "192.168.1.2", "device_type": "huawei"},
]

# 通用凭证(生产环境建议从环境变量读取)
USERNAME = "admin"
PASSWORD = "admin123"

def safe_connect(host, device_type):
    """安全连接"""
    try:
        conn = ConnectHandler(
            device_type=device_type,
            host=host,
            username=USERNAME,
            password=PASSWORD,
            timeout=30,
            banner_timeout=15,
        )
        log.info(f"连接成功: {host}")
        return conn
    except NetmikoAuthenticationException:
        log.error(f"认证失败: {host}")
    except NetmikoTimeoutException:
        log.error(f"连接超时: {host}")
    except Exception as e:
        log.error(f"未知错误 {host}: {e}")
    return None

def process_device(dev):
    """处理单设备"""
    conn = safe_connect(dev["host"], dev["device_type"])
    if not conn:
        return (dev["host"], False, "连接失败")

    try:
        # 收集信息
        hostname = conn.send_command("display current-configuration | include sysname")
        hostname = hostname.replace("sysname ", "").strip()
        log.info(f"设备名称: {hostname}")

        # 执行配置
        commands = [
            "ntp-service unicast-server 203.0.113.1",
            "clock timezone CST add 08:00:00",
        ]
        output = conn.send_config_set(commands)
        log.info(f"配置完成: {hostname}")

        # 验证
        verify = conn.send_command("display ntp-service status")
        conn.save()

        # 延迟断开(确保配置生效)
        time.sleep(2)
        conn.disconnect()

        return (dev["host"], True, f"{hostname} 配置成功")

    except ConfigException as e:
        log.error(f"配置错误 {dev['host']}: {e}")
        return (dev["host"], False, f"配置错误: {e}")
    except Exception as e:
        log.error(f"执行错误 {dev['host']}: {e}")
        return (dev["host"], False, f"执行错误: {e}")

# 主流程
log.info("=== 批量操作开始 ===")
start = time.time()
success_count = 0

with ThreadPoolExecutor(max_workers=5) as ex:
    futures = {ex.submit(process_device, dev): dev for dev in DEVICES}
    for future in as_completed(futures):
        host, ok, msg = future.result()
        if ok:
            success_count += 1
        log.info(f"[{'✓' if ok else '✗'}] {host}: {msg}")

elapsed = time.time() - start
log.info(f"=== 完成: {success_count}/{len(DEVICES)} 成功, 耗时 {elapsed:.1f} 秒 ===")

六、Netmiko 与 Paramiko 对比

6.1 使用对比

#!/usr/bin/env python3
# compare_vs.py — Paramiko vs Netmiko 对比

"""

Paramiko 实现(约 35 行):
┌──────────────────────────────────────┐
│ import paramiko                      │
│ import time                          │
│                                      │
│ client = paramiko.SSHClient()        │
│ client.set_missing_host_key_policy(  │
│     paramiko.AutoAddPolicy())        │
│ client.connect(host, port,           │
│     username, password)              │
│                                      │
│ channel = client.invoke_shell()      │
│ channel.send("screen-length 0 temp") │
│ time.sleep(1)                        │
│ channel.send("display int brief\n")  │
│ time.sleep(2)                        │
│ output = channel.recv(65535)         │
│ print(output.decode())               │
│ client.close()                       │
└──────────────────────────────────────┘

Netmiko 实现(约 8 行):
┌──────────────────────────────────────┐
│ from netmiko import ConnectHandler   │
│                                      │
│ device = {                           │
│   "device_type": "huawei",           │
│   "host": "192.168.1.1",             │
│   "username": "admin",               │
│   "password": "admin123",            │
│ }                                    │
│                                      │
│ conn = ConnectHandler(**device)      │
│ output = conn.send_command(          │
│     "display interface brief")       │
│ print(output)                        │
│ conn.disconnect()                    │
└──────────────────────────────────────┘

结论:日常操作优先使用 Netmiko,
特殊需求(交互式命令、原始 SSH 通道)用 Paramiko
"""

6.2 选型建议

何时用 Netmiko:
  ┌─ 90% 日常运维需求
  ├─ 多厂商设备管理
  ├─ 批量命令执行
  ├─ 配置变更下发
  └─ 监控数据采集

何时用 Paramiko:
  ┌─ 需要交互式 CLI(如 dialog)
  ├─ 需要原始 SSH 通道
  ├─ 自定义协议扩展
  └─ 学习 SSH 协议原理

推荐路径:
  Paramiko(理解原理)→ Netmiko(高效工作)

七、最佳实践总结

Netmiko 使用原则:

  1. 连接管理
  ┌─ 用 ThreadPoolExecutor 实现并行
  ├─ 设置合理的 timeout(默认 30s)
  ├─ 用完调用 disconnect()
  └─ 捕获 NetmikoTimeoutException

  2. 命令执行
  ┌─ 查询命令用 send_command
  ├─ 配置命令用 send_config_set
  ├─ 长输出设置 read_timeout
  └─ 慢设备用 global_delay_factor

  3. 错误处理
  ┌─ 配置前备份当前配置
  ├─ 配置后验证关键参数
  ├─ 失败时记录完整日志
  └─ 准备回滚方案

  4. 安全实践
  ┌─ 凭证从环境变量读取(非硬编码)
  ├─ 使用 SFTP 替代 TFTP
  ├─ 记录变更审计日志
  └─ 变更操作添加确认步骤

下篇预告:第305篇 — NAPALM 统一网络设备管理接口,将介绍如何通过 NAPALM 用一套 API 管理多厂商网络设备,实现配置的 Getter/Replacer/Committer 模式。