第305篇:NAPALM 统一网络设备管理接口
关键词
NAPALM、多厂商统一 API、配置管理、Getter 方法、配置替换、配置提交、网络自动化、Python 网络库
一、NAPALM 简介
1.1 什么是 NAPALM
NAPALM(Network Automation and Programmability Abstraction Layer with Multivendor support)是一个 Python 库,为网络设备提供 统一的跨厂商管理接口:
NAPALM 的设计理念:
| 你的脚本/应用 ▼ ┌────────────────────────────────────┐ └────────────────────────────────────┘ ▼ ┌────────────────────────────────────┐ | NAPALM 统一 API ┌───────┐ ┌───────┐ ┌───────┐ └───────┘ └───────┘ └───────┘ 底层驱动(Driver) ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ | get_ facts 华为 VRP | config replace 思科 IOS-XR | commit/ rollback Juniper JunOS | Arista EOS | |||||
|---|---|---|---|---|---|---|---|---|---|---|
Netmiko vs NAPALM 定位差异: ┌─ Netmiko:CLI 驱动层,发送命令 + 回显解析 ├─ NAPALM:抽象 API 层,统一数据模型 + 配置管理 └─ 两者可以互补使用
安装:
python -m pip install napalm
1.2 NAPALM 三大核心能力
NAPALM 提供三类操作:
1. 数据收集(Getter)
┌─ get_facts():设备基本信息
├─ get_interfaces():接口状态
├─ get_interfaces_ip():接口 IP
├─ get_lldp_neighbors():LLDP 邻居
├─ get_bgp_neighbors():BGP 邻居
├─ get_environment():电源/风扇/温度
├─ get_arp_table():ARP 表
├─ get_mac_address_table():MAC 表
├─ get_route_to():路由表
├─ get_ntp_peers():NTP 配置
└─ get_ntp_servers():NTP 服务器
2. 配置管理
┌─ load_replace_candidate():加载替换配置
├─ load_merge_candidate():加载合并配置
├─ compare_config():比较配置差异
├─ commit_config():提交变更
├─ discard_config():丢弃候选配置
└─ rollback():回滚
3. 验证与文件
├─ ping():设备 ping
├─ traceroute():路由追踪
└─ config_control():保存配置
二、数据收集实战
2.1 连接与获取设备信息
#!/usr/bin/env python3
# napalm_getter.py — 使用 Getter 收集设备数据
from napalm import get_network_driver
import json
# 1. 选择驱动
driver = get_network_driver("huawei_vrp") # 华为 VRP
# 2. 创建设备对象
device = driver(
hostname="192.168.1.1",
username="admin",
password="admin123",
optional_args={"port": 22},
)
try:
# 3. 打开连接
device.open()
print("连接成功")
# 4. 收集数据
facts = device.get_facts()
print("\n=== 设备基本信息 ===")
print(f"厂商: {facts.get('vendor')}")
print(f"型号: {facts.get('model')}")
print(f"序列号: {facts.get('serial_number')}")
print(f"OS 版本: {facts.get('os_version')}")
print(f"主机名: {facts.get('hostname')}")
print(f"运行时间: {facts.get('uptime')} 秒")
print(f"接口数: {len(facts.get('interface_list', []))}")
# 5. 接口信息
interfaces = device.get_interfaces()
print("\n=== 接口状态 ===")
for name, info in interfaces.items():
status = "UP" if info.get("is_up") else "DOWN"
speed = info.get("speed", 0)
print(f" {name}: {status}, {speed}Mbps")
# 6. 环境信息
env = device.get_environment()
print("\n=== 环境状态 ===")
for fan_name, fan_info in env.get("fans", {}).items():
print(f" 风扇 {fan_name}: {fan_info.get('status', 'N/A')}")
for ps_name, ps_info in env.get("power", {}).items():
print(f" 电源 {ps_name}: {ps_info.get('status', 'N/A')}")
for temp_name, temp_info in env.get("temperature", {}).items():
print(f" 温度 {temp_name}: {temp_info.get('temperature', 'N/A')}°C")
# 7. LLDP 邻居
lldp = device.get_lldp_neighbors()
print("\n=== LLDP 邻居 ===")
for port, neighbors in lldp.items():
for neighbor in neighbors:
print(f" {port} → {neighbor['hostname']} ({neighbor['port']})")
# 8. 路由表
routes = device.get_route_to("0.0.0.0/0")
print("\n=== 默认路由 ===")
for route in routes:
print(f" 下一跳: {route.get('next_hop')}, 出接口: {route.get('interface')}")
finally:
device.close()
2.2 保存结果为 JSON
#!/usr/bin/env python3
# napalm_collect.py — 批量采集并保存 JSON
from napalm import get_network_driver
import json
import os
from datetime import datetime
DEVICES = {
"core-sw01": {
"driver": "huawei_vrp",
"hostname": "192.168.1.1",
"username": "admin",
"password": "admin123",
},
"core-router01": {
"driver": "cisco_xr",
"hostname": "192.168.2.1",
"username": "admin",
"password": "admin123",
},
}
OUTPUT_DIR = "napalm_collection"
# 要收集的 Getter 方法
GETTERS = [
"get_facts",
"get_interfaces",
"get_interfaces_ip",
"get_lldp_neighbors",
"get_bgp_neighbors",
"get_environment",
"get_arp_table",
"get_mac_address_table",
]
os.makedirs(OUTPUT_DIR, exist_ok=True)
for name, dev_info in DEVICES.items():
print(f"\n=== 正在采集 {name} ===")
driver = get_network_driver(dev_info["driver"])
device = driver(
hostname=dev_info["hostname"],
username=dev_info["username"],
password=dev_info["password"],
)
try:
device.open()
device_data = {}
for getter in GETTERS:
try:
method = getattr(device, getter)
result = method()
device_data[getter] = result
print(f" ✓ {getter}")
except NotImplementedError:
print(f" ─ {getter}(本设备不支持)")
device_data[getter] = None
except Exception as e:
print(f" ✗ {getter}: {e}")
device_data[getter] = str(e)
# 保存
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = os.path.join(OUTPUT_DIR, f"{name}_{timestamp}.json")
with open(filename, "w", encoding="utf-8") as f:
json.dump(device_data, f, indent=2, ensure_ascii=False)
print(f" 已保存到 {filename}")
except Exception as e:
print(f" 连接失败: {e}")
finally:
device.close()
三、配置管理实战
3.1 配置替换(Replace)
#!/usr/bin/env python3
# napalm_replace.py — 配置替换
from napalm import get_network_driver
from napalm.base.exceptions import (
ReplaceConfigException,
MergeConfigException,
)
driver = get_network_driver("huawei_vrp")
device = driver(
hostname="192.168.1.1",
username="admin",
password="admin123",
)
# 新的配置内容(完整替换)
new_config = """
!
interface GigabitEthernet0/0/1
description UPLINK_TO_CORE
port link-type trunk
port trunk allow-pass vlan 10 20 30 40 50
undo shutdown
!
interface GigabitEthernet0/0/2
description ACCESS_TO_OFFICE
port link-type access
port default vlan 100
undo shutdown
!
snmp-agent
snmp-agent community read public
snmp-agent sys-info contact IT-Department
!
ntp-service unicast-server 203.0.113.1
!
"""
try:
device.open()
print("已连接")
# 1. 加载候选配置(替换模式)
print("加载候选配置...")
device.load_replace_candidate(config=new_config)
# 2. 比较差异
diff = device.compare_config()
if diff:
print("配置差异:")
print(diff)
# 3. 确认提交
confirm = input("是否提交变更?(yes/no): ")
if confirm.lower() == "yes":
device.commit_config()
print("✓ 配置已提交")
else:
device.discard_config()
print("─ 已丢弃候选配置")
else:
print("无配置变更")
except ReplaceConfigException as e:
print(f"配置替换失败: {e}")
device.rollback()
finally:
device.close()
3.2 配置合并(Merge)
#!/usr/bin/env python3
# napalm_merge.py — 配置合并(增量变更)
from napalm import get_network_driver
from napalm.base.exceptions import MergeConfigException
driver = get_network_driver("huawei_vrp")
device = driver(
hostname="192.168.1.1",
username="admin",
password="admin123",
)
# 增量变更:只包含要新增/修改的部分
merge_config = """
!
vlan batch 200 201 202
!
interface Vlanif200
ip address 10.0.200.1 255.255.255.0
!
interface Vlanif201
ip address 10.0.201.1 255.255.255.0
!
interface Vlanif202
ip address 10.0.202.1 255.255.255.0
!
ospf 1
area 0.0.0.0
network 10.0.200.0 0.0.0.255
network 10.0.201.0 0.0.0.255
network 10.0.202.0 0.0.0.255
!
"""
try:
device.open()
# 1. 备份当前配置
current = device.get_config()
backup = current.get("running", "")
print(f"当前配置长度: {len(backup)} 字符")
# 2. 加载合并配置
print("加载增量配置...")
device.load_merge_candidate(config=merge_config)
# 3. 比较差异
diff = device.compare_config()
lines = diff.split("\n")
added = [l for l in lines if l.startswith("+")]
removed = [l for l in lines if l.startswith("-")]
print(f"变更摘要:新增 {len(added)} 行,删除 {len(removed)} 行")
print("\n详细差异:")
print(diff)
# 4. 提交
device.commit_config()
print("✓ 增量配置已提交")
except MergeConfigException as e:
print(f"配置合并失败: {e}")
device.rollback()
finally:
device.close()
3.3 配置回滚
#!/usr/bin/env python3
# napalm_rollback.py — 配置回滚
from napalm import get_network_driver
import time
driver = get_network_driver("huawei_vrp")
device = driver(
hostname="192.168.1.1",
username="admin",
password="admin123",
)
# 模拟:变更前保存配置回滚点
try:
device.open()
# 保存当前配置到文件(回滚点)
config = device.get_config()
with open("rollback_point.cfg", "w") as f:
f.write(config.get("running", ""))
print("✓ 已保存回滚点")
# ... 进行配置变更 ...
# 如果变更失败,回滚
print("触发回滚...")
device.rollback()
print("✓ 已回滚到上一个配置")
# 验证
time.sleep(2)
current = device.get_config()
print(f"回滚后配置长度: {len(current.get('running', ''))} 字符")
finally:
device.close()
四、跨厂商一致性
4.1 同一代码管理多厂商设备
#!/usr/bin/env python3
# napalm_multivendor.py — 跨厂商统一管理
from napalm import get_network_driver
from concurrent.futures import ThreadPoolExecutor, as_completed
import json
DEVICES = [
{"name": "huawei-core-01", "driver": "huawei_vrp",
"hostname": "192.168.1.1", "username": "admin", "password": "admin123"},
{"name": "cisco-core-01", "driver": "ios",
"hostname": "192.168.2.1", "username": "admin", "password": "admin123"},
{"name": "juniper-core-01", "driver": "junos",
"hostname": "192.168.3.1", "username": "admin", "password": "admin123"},
{"name": "arista-core-01", "driver": "eos",
"hostname": "192.168.4.1", "username": "admin", "password": "admin123"},
]
def collect_facts(dev_info):
"""设备事实收集(统一 Getter 调用)"""
driver = get_network_driver(dev_info["driver"])
device = driver(
hostname=dev_info["hostname"],
username=dev_info["username"],
password=dev_info["password"],
timeout=30,
)
result = {"name": dev_info["name"], "facts": None, "error": None}
try:
device.open()
facts = device.get_facts() # 统一 API
interfaces = device.get_interfaces_ip() # 统一 API
lldp = device.get_lldp_neighbors() # 统一 API
bgp = device.get_bgp_neighbors() # 统一 API
result["facts"] = {
"hostname": facts.get("hostname"),
"vendor": facts.get("vendor"),
"model": facts.get("model"),
"os_version": facts.get("os_version"),
"uptime": facts.get("uptime"),
"interface_count": len(facts.get("interface_list", [])),
"lldp_neighbors": sum(len(n) for n in lldp.values()),
"bgp_peers": len(bgp.get("global", {}).get("peers", {})),
}
print(f" ✓ {dev_info['name']}: {facts.get('vendor')} {facts.get('model')}")
except Exception as e:
result["error"] = str(e)
print(f" ✗ {dev_info['name']}: {e}")
finally:
device.close()
return result
print("=== 跨厂商设备信息采集 ===")
results = []
with ThreadPoolExecutor(max_workers=4) as ex:
futures = [ex.submit(collect_facts, dev) for dev in DEVICES]
for f in as_completed(futures):
results.append(f.result())
# 输出汇总
print("\n=== 采集汇总 ===")
print(f"{'设备名':<20} {'厂商':<10} {'型号':<20} {'接口数':<8} {'BGP邻居':<8}")
print("-" * 66)
for r in results:
if r["facts"]:
f = r["facts"]
print(f"{r['name']:<20} {f['vendor']:<10} {f['model']:<20} "
f"{f['interface_count']:<8} {f['bgp_peers']:<8}")
else:
print(f"{r['name']:<20} {'ERROR':<10} {r['error']:<30}")
# 保存汇总
with open("multivendor_summary.json", "w") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print("\n汇总已保存到 multivendor_summary.json")
4.2 NAPALM 驱动的关键差异
不同厂商的 NAPALM 支持程度:
| 驱动 | Getter | Replace | Merge |
|---|---|---|---|
| huawei_vrp ios (思科) ios_xr nxos junos eos (Arista) fortios panos (PaloAlto) | ✓ ✓ ✓ ✓ ✓ ✓ 部分 部分 | ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ | ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ |
注意事项: ┌─ huawei_vrp 需要设备开启 SSH 并支持 display current-configuration ├─ get_lldp_neighbors 在部分交换机支持 ├─ commit_config 在思科 IOS 上需要 archive 配置 └─ rollback 在某些厂商只支持一次回滚
五、NAPALM + Netmiko 组合使用
5.1 取长补短
#!/usr/bin/env python3
# napalm_netmiko_combo.py — 组合使用
from napalm import get_network_driver
from netmiko import ConnectHandler
import json
class HybridManager:
"""NAPALM + Netmiko 混合管理器"""
def __init__(self, host, username, password, device_type):
self.host = host
self.username = username
self.password = password
self.device_type = device_type
# NAPALM 驱动映射
napalm_drivers = {
"huawei": "huawei_vrp",
"cisco_ios": "ios",
"cisco_xr": "ios_xr",
"juniper_junos": "junos",
}
self.napalm_driver = napalm_drivers.get(device_type, device_type)
def collect_structured(self):
"""NAPALM:结构化数据收集"""
driver = get_network_driver(self.napalm_driver)
device = driver(
hostname=self.host,
username=self.username,
password=self.password,
)
data = {}
try:
device.open()
data["facts"] = device.get_facts()
data["interfaces"] = device.get_interfaces()
data["lldp"] = device.get_lldp_neighbors()
data["bgp"] = device.get_bgp_neighbors()
finally:
device.close()
return data
def execute_custom(self, commands):
"""Netmiko:执行 NAPALM 不支持的 CLI 命令"""
conn = ConnectHandler(
device_type=self.device_type,
host=self.host,
username=self.username,
password=self.password,
)
results = {}
try:
for cmd in commands:
results[cmd] = conn.send_command(cmd)
finally:
conn.disconnect()
return results
# 使用示例
manager = HybridManager(
host="192.168.1.1",
username="admin",
password="admin123",
device_type="huawei",
)
# 用 NAPALM 采集结构化数据
structured = manager.collect_structured()
print(f"设备: {structured['facts'].get('hostname')}")
print(f"接口数: {len(structured['interfaces'])}")
# 用 Netmiko 执行自定义命令
custom_commands = [
"display memory-usage",
"display cpu-usage",
"display logbuffer | tail 5",
]
custom = manager.execute_custom(custom_commands)
for cmd, output in custom.items():
print(f"\n{cmd}:")
print(output[:200])
六、最佳实践
6.1 安全与可靠操作
NAPALM 生产使用规范:
1. 配置变更流程
┌─ load_replace/merge_candidate → 加载
├─ compare_config → 审查差异
├─ 人工确认 → commit_config
└─ 失败 → rollback
2. 连接管理
┌─ 总是用 try/finally 确保 close()
├─ 设置合理的 timeout
├─ 使用 optional_args 传连接参数
└─ 避免频繁连接断开(复用连接)
3. 错误处理
┌─ 捕获 ReplaceConfigException
├─ 捕获 MergeConfigException
├─ 捕获 CommandErrorException
└─ 记录所有操作到日志
4. 变更控制
┌─ 变更前备份 running-config
├─ 变更后验证关键服务
├─ 保留回滚点 24-48 小时
└─ 写入变更审计日志
6.2 NAPALM 适用场景
推荐使用 NAPALM 的场景:
┌─ 多厂商统一监控平台
│ 用 get_facts/get_interfaces/get_environment
│ 统一采集所有厂商的设备状态
├─ 配置合规检查
│ 用 get_config 获取 running-config
│ 通过 Jinja2 模板对比检查
├─ 自动化配置部署
│ 用 load_replace_candidate + commit_config
│ 实现配置的自动化批量部署
├─ 网络配置备份
│ 用 get_config 获取 startup/running
│ 定时备份到 Git 仓库
└─ 变更审计
用 compare_config 显示变更差异
用 commit_config 记录变更时间戳
不推荐使用 NAPALM 的场景:
┌─ 需要大量自定义 CLI 命令
│ 用 Netmiko 更灵活
├─ 仅操作单一厂商
│ 用厂商 SDK 更高效
└─ 交互式命令
Paramiko 更可控
七、总结
NAPALM 的核心价值:
1. 统一抽象
┌─ 一套 API 管理所有厂商
├─ 相同方法名、相同返回结构
└─ 厂商差异由 Driver 层屏蔽
2. 结构化数据
┌─ 返回 Python 字典(非文本回显)
├─ 可直接用于数据库/监控系统
└─ 省去正则解析的烦恼
3. 配置管理标准化
┌─ Candidate 模式(先试后提交)
├─ diff 能力(变更前审查)
└─ commit/rollback(安全变更)
学习路径:
Paramiko → Netmiko → NAPALM → Ansible
底层控制 高效操作 统一 API 配置编排
下篇预告:第306篇 — Jinja2 模板引擎:配置模板化,将介绍如何使用 Jinja2 模板将设备配置与数据分离,实现配置的自动化生成与管理。